duasatu
This commit is contained in:
parent
5a028c29d5
commit
6d3fe5ebba
|
@ -0,0 +1,98 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class CreateBranchTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
// Create branches table if it doesn't exist
|
||||
if (!Schema::hasTable('branches')) {
|
||||
Schema::create('branches', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->string('city')->nullable();
|
||||
$table->string('address')->nullable();
|
||||
$table->string('phone')->nullable();
|
||||
$table->string('email')->nullable();
|
||||
$table->boolean('is_active')->default(true);
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
// Insert default branch
|
||||
DB::table('branches')->insert([
|
||||
'name' => 'Toko Al Fatih Pusar',
|
||||
'city' => 'Lumajang',
|
||||
'address' => 'Toko Pusat',
|
||||
'phone' => '1234567890',
|
||||
'email' => 'alfatih@test.com',
|
||||
'is_active' => true,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now()
|
||||
]);
|
||||
}
|
||||
|
||||
// Add branch_id to all necessary tables
|
||||
$tables = [
|
||||
'sales',
|
||||
'purchases',
|
||||
'purchase_returns',
|
||||
'expenses',
|
||||
'sale_payments',
|
||||
'purchase_payments',
|
||||
'purchase_return_payments'
|
||||
];
|
||||
|
||||
foreach ($tables as $tableName) {
|
||||
if (Schema::hasTable($tableName)) {
|
||||
Schema::table($tableName, function (Blueprint $table) use ($tableName) {
|
||||
if (!Schema::hasColumn($tableName, 'branch_id')) {
|
||||
$table->unsignedBigInteger('branch_id')->default(1)->after('id');
|
||||
$table->foreign('branch_id')->references('id')->on('branches');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
// Remove branch_id from all tables
|
||||
$tables = [
|
||||
'sales',
|
||||
'purchases',
|
||||
'purchase_returns',
|
||||
'expenses',
|
||||
'sale_payments',
|
||||
'purchase_payments',
|
||||
'purchase_return_payments'
|
||||
];
|
||||
|
||||
foreach ($tables as $tableName) {
|
||||
if (Schema::hasTable($tableName)) {
|
||||
Schema::table($tableName, function (Blueprint $table) use ($tableName) {
|
||||
if (Schema::hasColumn($tableName, 'branch_id')) {
|
||||
$table->dropForeign(['branch_id']);
|
||||
$table->dropColumn('branch_id');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Drop branches table
|
||||
Schema::dropIfExists('branches');
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
@extends('core::layouts.master')
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title')
|
||||
Create Branch
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
@extends('core::layouts.master')
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title')
|
||||
Edit Branch
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
@extends('core::layouts.master')
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title')
|
||||
Branches
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
@extends('core::layouts.master')
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title')
|
||||
Branch Details
|
||||
|
|
|
@ -8,36 +8,36 @@ use Yajra\DataTables\Html\Column;
|
|||
use Yajra\DataTables\Html\Editor\Editor;
|
||||
use Yajra\DataTables\Html\Editor\Fields;
|
||||
use Yajra\DataTables\Services\DataTable;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class ProductDataTable extends DataTable
|
||||
{
|
||||
|
||||
public function dataTable($query)
|
||||
{
|
||||
return datatables()
|
||||
->eloquent($query)->with('category')
|
||||
->eloquent($query)
|
||||
->addColumn('action', function ($data) {
|
||||
return view('product::products.partials.actions', compact('data'));
|
||||
})
|
||||
->addColumn('product_image', function ($data) {
|
||||
$url = $data->getFirstMediaUrl('images', 'thumb');
|
||||
return '<img src="'.$url.'" border="0" width="50" class="img-thumbnail" align="center"/>';
|
||||
$url = $data->getFirstMediaUrl('images');
|
||||
return '<img src="'.$url.'" class="product-img-thumb" alt="">';
|
||||
})
|
||||
->addColumn('product_price', function ($data) {
|
||||
return format_currency($data->product_price);
|
||||
->addColumn('product_name', function ($data) {
|
||||
return '<a href="'.route('products.show', $data->id).'">'.$data->product_name.'</a>';
|
||||
})
|
||||
->addColumn('product_cost', function ($data) {
|
||||
return format_currency($data->product_cost);
|
||||
->addColumn('total_quantity', function ($data) {
|
||||
return $data->product_quantity;
|
||||
})
|
||||
->addColumn('product_quantity', function ($data) {
|
||||
return $data->product_quantity . ' ' . $data->product_unit;
|
||||
})
|
||||
->rawColumns(['product_image']);
|
||||
->rawColumns(['product_image', 'product_name', 'action']);
|
||||
}
|
||||
|
||||
public function query(Product $model)
|
||||
{
|
||||
return $model->newQuery()->with('category');
|
||||
return $model->newQuery()
|
||||
->with('category')
|
||||
->leftJoin(DB::raw('(SELECT product_id, SUM(quantity) as total_quantity FROM product_batches GROUP BY product_id) as pb'), 'products.id', '=', 'pb.product_id')
|
||||
->select('products.*', DB::raw('COALESCE(pb.total_quantity, 0) as product_quantity'));
|
||||
}
|
||||
|
||||
public function html()
|
||||
|
@ -49,12 +49,12 @@ class ProductDataTable 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(7)
|
||||
->orderBy(4)
|
||||
->buttons(
|
||||
Button::make('excel')
|
||||
->text('<i class="bi bi-file-earmark-excel-fill"></i> Excel'),
|
||||
->text('<i class="bi bi-file-earmark-excel"></i> Excel'),
|
||||
Button::make('print')
|
||||
->text('<i class="bi bi-printer-fill"></i> Print'),
|
||||
->text('<i class="bi bi-printer"></i> Print'),
|
||||
Button::make('reset')
|
||||
->text('<i class="bi bi-x-circle"></i> Reset'),
|
||||
Button::make('reload')
|
||||
|
@ -65,41 +65,35 @@ class ProductDataTable extends DataTable
|
|||
protected function getColumns()
|
||||
{
|
||||
return [
|
||||
Column::computed('product_image')
|
||||
Column::make('product_image')
|
||||
->title('Image')
|
||||
->className('text-center align-middle'),
|
||||
|
||||
Column::make('category.category_name')
|
||||
->title('Category')
|
||||
->className('text-center align-middle'),
|
||||
|
||||
Column::make('product_code')
|
||||
->title('Code')
|
||||
->className('text-center align-middle'),
|
||||
|
||||
Column::make('product_name')
|
||||
->title('Name')
|
||||
->className('text-center align-middle'),
|
||||
|
||||
Column::computed('product_cost')
|
||||
->title('Cost')
|
||||
Column::make('product_code')
|
||||
->title('Code')
|
||||
->className('text-center align-middle'),
|
||||
|
||||
Column::computed('product_price')
|
||||
->title('Price')
|
||||
Column::make('category.category_name')
|
||||
->title('Category')
|
||||
->className('text-center align-middle'),
|
||||
|
||||
Column::computed('product_quantity')
|
||||
Column::make('product_unit')
|
||||
->title('Unit')
|
||||
->className('text-center align-middle'),
|
||||
|
||||
Column::make('total_quantity')
|
||||
->title('Quantity')
|
||||
->className('text-center align-middle'),
|
||||
|
||||
Column::computed('action')
|
||||
->exportable(false)
|
||||
->printable(false)
|
||||
->className('text-center align-middle'),
|
||||
|
||||
Column::make('created_at')
|
||||
->visible(false)
|
||||
->className('text-center align-middle')
|
||||
->addClass('text-center'),
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -113,3 +107,4 @@ class ProductDataTable extends DataTable
|
|||
return 'Product_' . date('YmdHis');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ use Modules\Product\Notifications\NotifyQuantityAlert;
|
|||
use Spatie\MediaLibrary\HasMedia;
|
||||
use Spatie\MediaLibrary\InteractsWithMedia;
|
||||
use Spatie\MediaLibrary\MediaCollections\Models\Media;
|
||||
use App\Models\ProductBatch;
|
||||
|
||||
class Product extends Model implements HasMedia
|
||||
{
|
||||
|
@ -18,10 +19,20 @@ class Product extends Model implements HasMedia
|
|||
|
||||
protected $with = ['media'];
|
||||
|
||||
protected $appends = ['product_quantity'];
|
||||
|
||||
public function category() {
|
||||
return $this->belongsTo(Category::class, 'category_id', 'id');
|
||||
}
|
||||
|
||||
public function batches() {
|
||||
return $this->hasMany(ProductBatch::class, 'product_id', 'id');
|
||||
}
|
||||
|
||||
public function getProductQuantityAttribute() {
|
||||
return $this->batches()->sum('quantity');
|
||||
}
|
||||
|
||||
public function registerMediaCollections(): void {
|
||||
$this->addMediaCollection('images')
|
||||
->useFallbackUrl('/images/fallback_product_image.png');
|
||||
|
|
|
@ -16,21 +16,24 @@ use Modules\Upload\Entities\Upload;
|
|||
class ProductController extends Controller
|
||||
{
|
||||
|
||||
public function index(ProductDataTable $dataTable) {
|
||||
public function index(ProductDataTable $dataTable)
|
||||
{
|
||||
abort_if(Gate::denies('access_products'), 403);
|
||||
|
||||
return $dataTable->render('product::products.index');
|
||||
}
|
||||
|
||||
|
||||
public function create() {
|
||||
public function create()
|
||||
{
|
||||
abort_if(Gate::denies('create_products'), 403);
|
||||
|
||||
return view('product::products.create');
|
||||
}
|
||||
|
||||
|
||||
public function store(StoreProductRequest $request) {
|
||||
public function store(StoreProductRequest $request)
|
||||
{
|
||||
$product = Product::create($request->except('document'));
|
||||
|
||||
if ($request->has('document')) {
|
||||
|
@ -45,21 +48,24 @@ class ProductController extends Controller
|
|||
}
|
||||
|
||||
|
||||
public function show(Product $product) {
|
||||
public function show(Product $product)
|
||||
{
|
||||
abort_if(Gate::denies('show_products'), 403);
|
||||
|
||||
return view('product::products.show', compact('product'));
|
||||
}
|
||||
|
||||
|
||||
public function edit(Product $product) {
|
||||
public function edit(Product $product)
|
||||
{
|
||||
abort_if(Gate::denies('edit_products'), 403);
|
||||
|
||||
return view('product::products.edit', compact('product'));
|
||||
}
|
||||
|
||||
|
||||
public function update(UpdateProductRequest $request, Product $product) {
|
||||
public function update(UpdateProductRequest $request, Product $product)
|
||||
{
|
||||
$product->update($request->except('document'));
|
||||
|
||||
if ($request->has('document')) {
|
||||
|
@ -86,7 +92,8 @@ class ProductController extends Controller
|
|||
}
|
||||
|
||||
|
||||
public function destroy(Product $product) {
|
||||
public function destroy(Product $product)
|
||||
{
|
||||
abort_if(Gate::denies('delete_products'), 403);
|
||||
|
||||
$product->delete();
|
||||
|
|
|
@ -15,23 +15,18 @@ class StoreProductRequest extends FormRequest
|
|||
public function rules()
|
||||
{
|
||||
return [
|
||||
'product_name' => ['required', 'string', 'max:255'],
|
||||
'product_code' => ['required', 'string', 'max:255', 'unique:products,product_code'],
|
||||
'product_barcode_symbology' => ['required', 'string', 'max:255'],
|
||||
'product_unit' => ['required', 'string', 'max:255'],
|
||||
'product_quantity' => ['required', 'integer', 'min:1'],
|
||||
'product_cost' => ['required', 'numeric', 'max:2147483647'],
|
||||
'product_price' => ['required', 'numeric', 'max:2147483647'],
|
||||
'min_quantity_for_wholesale' => ['nullable', 'integer', 'min:0'],
|
||||
'wholesale_discount_percentage' => ['nullable', 'integer', 'min:0', 'max:100'],
|
||||
'product_stock_alert' => ['required', 'integer', 'min:0'],
|
||||
'product_order_tax' => ['nullable', 'integer', 'min:0', 'max:100'],
|
||||
'product_tax_type' => ['nullable', 'integer'],
|
||||
'product_note' => ['nullable', 'string', 'max:1000'],
|
||||
'category_id' => ['required', 'integer']
|
||||
'product_name' => 'required|string|max:255',
|
||||
'product_code' => 'required|string|max:255|unique:products,product_code',
|
||||
'category_id' => 'nullable|exists:categories,id',
|
||||
'product_unit' => 'nullable|string|max:50',
|
||||
'barcode_symbology' => 'nullable|string', // masih ada? kalau tidak, hapus juga
|
||||
'product_stock_alert' => 'nullable|numeric', // kalau sudah dihapus dari DB, hapus rule-nya juga
|
||||
'product_note' => 'nullable|string',
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*
|
||||
|
|
|
@ -18,14 +18,14 @@ class UpdateProductRequest extends FormRequest
|
|||
return [
|
||||
'product_name' => ['required', 'string', 'max:255'],
|
||||
'product_code' => ['required', 'string', 'max:255', 'unique:products,product_code,' . $this->product->id],
|
||||
'product_barcode_symbology' => ['required', 'string', 'max:255'],
|
||||
'product_barcode_symbology' => ['nullable', 'string', 'max:255'],
|
||||
'product_unit' => ['required', 'string', 'max:255'],
|
||||
'product_quantity' => ['required', 'integer', 'min:1'],
|
||||
'product_cost' => ['required', 'numeric', 'max:2147483647'],
|
||||
'product_price' => ['required', 'numeric', 'max:2147483647'],
|
||||
'product_quantity' => ['nullable', 'integer', 'min:0'],
|
||||
'product_cost' => ['nullable', 'numeric', 'max:2147483647'],
|
||||
'product_price' => ['nullable', 'numeric', 'max:2147483647'],
|
||||
'min_quantity_for_wholesale' => ['nullable', 'integer', 'min:0'],
|
||||
'wholesale_discount_percentage' => ['nullable', 'integer', 'min:0', 'max:100'],
|
||||
'product_stock_alert' => ['required', 'integer', 'min:0'],
|
||||
'product_stock_alert' => ['nullable', 'integer', 'min:0'],
|
||||
'product_order_tax' => ['nullable', 'integer', 'min:0', 'max:100'],
|
||||
'product_tax_type' => ['nullable', 'integer'],
|
||||
'product_note' => ['nullable', 'string', 'max:1000'],
|
||||
|
|
|
@ -57,68 +57,6 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="barcode_symbology">Barcode Symbology <span class="text-danger">*</span></label>
|
||||
<select class="form-control" name="product_barcode_symbology" id="barcode_symbology" required>
|
||||
<option value="" selected disabled>Select Symbology</option>
|
||||
<option value="C128">Code 128</option>
|
||||
<option value="C39">Code 39</option>
|
||||
<option value="UPCA">UPC-A</option>
|
||||
<option value="UPCE">UPC-E</option>
|
||||
<option selected value="EAN13">EAN-13</option><option value="EAN8">EAN-8</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="product_cost">Cost <span class="text-danger">*</span></label>
|
||||
<input id="product_cost" type="text" class="form-control" name="product_cost" required value="{{ old('product_cost') }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="product_price">Price <span class="text-danger">*</span></label>
|
||||
<input id="product_price" type="text" class="form-control" name="product_price" required value="{{ old('product_price') }}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="product_quantity">Quantity <span class="text-danger">*</span></label>
|
||||
<input type="number" class="form-control" name="product_quantity" required value="{{ old('product_quantity') }}" min="1">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="product_stock_alert">Alert Quantity <span class="text-danger">*</span></label>
|
||||
<input type="number" class="form-control" name="product_stock_alert" required value="{{ old('product_stock_alert', 0) }}" min="0" max="100">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="col-md-4">
|
||||
<div class="form-group">
|
||||
<label for="product_order_tax">Tax (%)</label>
|
||||
<input type="number" class="form-control" name="product_order_tax" value="{{ old('product_order_tax') }}" min="1">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="form-group">
|
||||
<label for="product_tax_type">Tax type</label>
|
||||
<select class="form-control" name="product_tax_type" id="product_tax_type">
|
||||
<option value="" selected >Select Tax Type</option>
|
||||
<option value="1">Exclusive</option>
|
||||
<option value="2">Inclusive</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="form-group">
|
||||
<label for="product_unit">Unit <i class="bi bi-question-circle-fill text-info" data-toggle="tooltip" data-placement="top" title="This short text will be placed after Product Quantity."></i> <span class="text-danger">*</span></label>
|
||||
<select class="form-control" name="product_unit" id="product_unit">
|
||||
|
@ -132,22 +70,10 @@
|
|||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="col-lg-4">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="product_price">Price <span class="text-danger">*</span></label>
|
||||
<input id="product_price" type="text" class="form-control" name="product_price" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<div class="form-group">
|
||||
<label for="min_quantity_for_wholesale">Minimal Quantity for Wholesale</label>
|
||||
<input id="min_quantity_for_wholesale" type="number" class="form-control" name="min_quantity_for_wholesale" min="0">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<div class="form-group">
|
||||
<label for="wholesale_discount_percentage">Wholesale Discount (%)</label>
|
||||
<input id="wholesale_discount_percentage" type="number" class="form-control" name="wholesale_discount_percentage" min="0" max="100">
|
||||
<label for="product_stock_alert">Stock Alert <i class="bi bi-question-circle-fill text-info" data-toggle="tooltip" data-placement="top" title="Minimum stock level to trigger alert"></i></label>
|
||||
<input type="number" class="form-control" name="product_stock_alert" value="{{ old('product_stock_alert', 0) }}" min="0">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -190,36 +116,27 @@
|
|||
<script>
|
||||
var uploadedDocumentMap = {}
|
||||
Dropzone.options.documentDropzone = {
|
||||
url: '{{ route('dropzone.upload') }}',
|
||||
url: "{{ route('dropzone.upload') }}",
|
||||
maxFilesize: 1,
|
||||
acceptedFiles: '.jpg, .jpeg, .png',
|
||||
acceptedFiles: ".jpeg,.jpg,.png,.gif",
|
||||
maxFiles: 3,
|
||||
addRemoveLinks: true,
|
||||
dictRemoveFile: "<i class='bi bi-x-circle text-danger'></i> remove",
|
||||
headers: {
|
||||
"X-CSRF-TOKEN": "{{ csrf_token() }}"
|
||||
'X-CSRF-TOKEN': "{{ csrf_token() }}"
|
||||
},
|
||||
success: function (file, response) {
|
||||
$('form').append('<input type="hidden" name="document[]" value="' + response.name + '">');
|
||||
uploadedDocumentMap[file.name] = response.name;
|
||||
$('form').append('<input type="hidden" name="document[]" value="' + response.name + '">')
|
||||
uploadedDocumentMap[file.name] = response.name
|
||||
},
|
||||
removedfile: function (file) {
|
||||
file.previewElement.remove();
|
||||
var name = '';
|
||||
file.previewElement.remove()
|
||||
var name = ''
|
||||
if (typeof file.file_name !== 'undefined') {
|
||||
name = file.file_name;
|
||||
name = file.file_name
|
||||
} else {
|
||||
name = uploadedDocumentMap[file.name];
|
||||
name = uploadedDocumentMap[file.name]
|
||||
}
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: "{{ route('dropzone.delete') }}",
|
||||
data: {
|
||||
'_token': "{{ csrf_token() }}",
|
||||
'file_name': `${name}`
|
||||
},
|
||||
});
|
||||
$('form').find('input[name="document[]"][value="' + name + '"]').remove();
|
||||
$('form').find('input[name="document[]"][value="' + name + '"]').remove()
|
||||
},
|
||||
init: function () {
|
||||
@if(isset($product) && $product->getMedia('images'))
|
||||
|
@ -235,28 +152,5 @@
|
|||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script src="{{ asset('js/jquery-mask-money.js') }}"></script>
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
$('#product_cost').maskMoney({
|
||||
prefix:'{{ settings()->currency->symbol }}',
|
||||
thousands:'{{ settings()->currency->thousand_separator }}',
|
||||
decimal:'{{ settings()->currency->decimal_separator }}',
|
||||
});
|
||||
$('#product_price').maskMoney({
|
||||
prefix:'{{ settings()->currency->symbol }}',
|
||||
thousands:'{{ settings()->currency->thousand_separator }}',
|
||||
decimal:'{{ settings()->currency->decimal_separator }}',
|
||||
});
|
||||
|
||||
$('#product-form').submit(function () {
|
||||
var product_cost = $('#product_cost').maskMoney('unmasked')[0];
|
||||
var product_price = $('#product_price').maskMoney('unmasked')[0];
|
||||
$('#product_cost').val(product_cost);
|
||||
$('#product_price').val(product_price);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
|
||||
|
|
|
@ -52,80 +52,6 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="barcode_symbology">Barcode Symbology <span class="text-danger">*</span></label>
|
||||
<select class="form-control" name="product_barcode_symbology" id="barcode_symbology" required>
|
||||
<option {{ $product->product_barcode_symbology == 'C128' ? 'selected' : '' }} value="C128">Code 128</option>
|
||||
<option {{ $product->product_barcode_symbology == 'C39' ? 'selected' : '' }} value="C39">Code 39</option>
|
||||
<option {{ $product->product_barcode_symbology == 'UPCA' ? 'selected' : '' }} value="UPCA">UPC-A</option>
|
||||
<option {{ $product->product_barcode_symbology == 'UPCE' ? 'selected' : '' }} value="UPCE">UPC-E</option>
|
||||
<option {{ $product->product_barcode_symbology == 'EAN13' ? 'selected' : '' }} value="EAN13">EAN-13</option>
|
||||
<option {{ $product->product_barcode_symbology == 'EAN8' ? 'selected' : '' }} value="EAN8">EAN-8</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="product_cost">Cost <span class="text-danger">*</span></label>
|
||||
<input id="product_cost" type="text" class="form-control" min="0" name="product_cost" required value="{{ $product->product_cost }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<div class="form-group">
|
||||
<label for="product_price">Price <span class="text-danger">*</span></label>
|
||||
<input id="product_price" type="text" class="form-control" name="product_price" required value="{{ $product->product_price }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<div class="form-group">
|
||||
<label for="min_quantity_for_wholesale">Minimal Quantity for Wholesale</label>
|
||||
<input id="min_quantity_for_wholesale" type="number" class="form-control" name="min_quantity_for_wholesale" min="0" value="{{ $product->min_quantity_for_wholesale }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<div class="form-group">
|
||||
<label for="wholesale_discount_percentage">Wholesale Discount (%)</label>
|
||||
<input id="wholesale_discount_percentage" type="number" class="form-control" name="wholesale_discount_percentage" min="0" max="100" value="{{ $product->wholesale_discount_percentage }}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="product_quantity">Quantity <span class="text-danger">*</span></label>
|
||||
<input type="number" class="form-control" name="product_quantity" required value="{{ $product->product_quantity }}" min="1">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="product_stock_alert">Alert Quantity <span class="text-danger">*</span></label>
|
||||
<input type="number" class="form-control" name="product_stock_alert" required value="{{ $product->product_stock_alert }}" min="0">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="col-md-4">
|
||||
<div class="form-group">
|
||||
<label for="product_order_tax">Tax (%)</label>
|
||||
<input type="number" class="form-control" name="product_order_tax" value="{{ $product->product_order_tax }}" min="0" max="100">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="form-group">
|
||||
<label for="product_tax_type">Tax type</label>
|
||||
<select class="form-control" name="product_tax_type" id="product_tax_type">
|
||||
<option value="" selected>None</option>
|
||||
<option {{ $product->product_tax_type == 1 ? 'selected' : '' }} value="1">Exclusive</option>
|
||||
<option {{ $product->product_tax_type == 2 ? 'selected' : '' }} value="2">Inclusive</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="form-group">
|
||||
<label for="product_unit">Unit <i class="bi bi-question-circle-fill text-info" data-toggle="tooltip" data-placement="top" title="This short text will be placed after Product Quantity."></i> <span class="text-danger">*</span></label>
|
||||
<select class="form-control" name="product_unit" id="product_unit" required>
|
||||
|
@ -137,6 +63,16 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="product_stock_alert">Stock Alert <i class="bi bi-question-circle-fill text-info" data-toggle="tooltip" data-placement="top" title="Minimum stock level to trigger alert"></i></label>
|
||||
<input type="number" class="form-control" name="product_stock_alert" value="{{ old('product_stock_alert', $product->product_stock_alert) }}" min="0">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="product_note">Note</label>
|
||||
<textarea name="product_note" id="product_note" rows="4 " class="form-control">{{ $product->product_note }}</textarea>
|
||||
|
@ -172,28 +108,27 @@
|
|||
<script>
|
||||
var uploadedDocumentMap = {}
|
||||
Dropzone.options.documentDropzone = {
|
||||
url: '{{ route('dropzone.upload') }}',
|
||||
url: "{{ route('dropzone.upload') }}",
|
||||
maxFilesize: 1,
|
||||
acceptedFiles: '.jpg, .jpeg, .png',
|
||||
acceptedFiles: ".jpeg,.jpg,.png,.gif",
|
||||
maxFiles: 3,
|
||||
addRemoveLinks: true,
|
||||
dictRemoveFile: "<i class='bi bi-x-circle text-danger'></i> remove",
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': "{{ csrf_token() }}"
|
||||
},
|
||||
success: function (file, response) {
|
||||
$('form').append('<input type="hidden" name="document[]" value="' + response.name + '">');
|
||||
uploadedDocumentMap[file.name] = response.name;
|
||||
$('form').append('<input type="hidden" name="document[]" value="' + response.name + '">')
|
||||
uploadedDocumentMap[file.name] = response.name
|
||||
},
|
||||
removedfile: function (file) {
|
||||
file.previewElement.remove();
|
||||
var name = '';
|
||||
file.previewElement.remove()
|
||||
var name = ''
|
||||
if (typeof file.file_name !== 'undefined') {
|
||||
name = file.file_name;
|
||||
name = file.file_name
|
||||
} else {
|
||||
name = uploadedDocumentMap[file.name];
|
||||
name = uploadedDocumentMap[file.name]
|
||||
}
|
||||
$('form').find('input[name="document[]"][value="' + name + '"]').remove();
|
||||
$('form').find('input[name="document[]"][value="' + name + '"]').remove()
|
||||
},
|
||||
init: function () {
|
||||
@if(isset($product) && $product->getMedia('images'))
|
||||
|
@ -209,31 +144,5 @@
|
|||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script src="{{ asset('js/jquery-mask-money.js') }}"></script>
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
$('#product_cost').maskMoney({
|
||||
prefix:'{{ settings()->currency->symbol }}',
|
||||
thousands:'{{ settings()->currency->thousand_separator }}',
|
||||
decimal:'{{ settings()->currency->decimal_separator }}',
|
||||
});
|
||||
$('#product_price').maskMoney({
|
||||
prefix:'{{ settings()->currency->symbol }}',
|
||||
thousands:'{{ settings()->currency->thousand_separator }}',
|
||||
decimal:'{{ settings()->currency->decimal_separator }}',
|
||||
});
|
||||
|
||||
$('#product_cost').maskMoney('mask');
|
||||
$('#product_price').maskMoney('mask');
|
||||
|
||||
$('#product-form').submit(function () {
|
||||
var product_cost = $('#product_cost').maskMoney('unmasked')[0];
|
||||
var product_price = $('#product_price').maskMoney('unmasked')[0];
|
||||
$('#product_cost').val(product_cost);
|
||||
$('#product_price').val(product_price);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
|
||||
|
|
|
@ -12,25 +12,15 @@
|
|||
|
||||
@section('content')
|
||||
<div class="container-fluid mb-4">
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-12">
|
||||
{!! \Milon\Barcode\Facades\DNS1DFacade::getBarCodeSVG($product->product_code, $product->product_barcode_symbology, 2, 110) !!}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-lg-9">
|
||||
<div class="card h-100">
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-striped mb-0">
|
||||
<tr>
|
||||
<th>Product Code</th>
|
||||
<td>{{ $product->product_code }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Barcode Symbology</th>
|
||||
<td>{{ $product->product_barcode_symbology }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<td>{{ $product->product_name }}</td>
|
||||
|
@ -40,62 +30,33 @@
|
|||
<td>{{ $product->category->category_name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Cost</th>
|
||||
<td>{{ format_currency($product->product_cost) }}</td>
|
||||
<th>Unit</th>
|
||||
<td>{{ $product->product_unit }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Price</th>
|
||||
<td>{{ format_currency($product->product_price) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Quantity</th>
|
||||
<td>{{ $product->product_quantity . ' ' . $product->product_unit }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Stock Worth</th>
|
||||
<td>
|
||||
COST:: {{ format_currency($product->product_cost * $product->product_quantity) }} /
|
||||
PRICE:: {{ format_currency($product->product_price * $product->product_quantity) }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Alert Quantity</th>
|
||||
<th>Stock Alert</th>
|
||||
<td>{{ $product->product_stock_alert }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Tax (%)</th>
|
||||
<td>{{ $product->product_order_tax ?? 'N/A' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Tax Type</th>
|
||||
<td>
|
||||
@if($product->product_tax_type == 1)
|
||||
Exclusive
|
||||
@elseif($product->product_tax_type == 2)
|
||||
Inclusive
|
||||
@else
|
||||
N/A
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Note</th>
|
||||
<td>{{ $product->product_note ?? 'N/A' }}</td>
|
||||
<td>{{ $product->product_note }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-3">
|
||||
<div class="card h-100">
|
||||
<div class="card-body">
|
||||
@forelse($product->getMedia('images') as $media)
|
||||
<img src="{{ $media->getUrl() }}" alt="Product Image" class="img-fluid img-thumbnail mb-2">
|
||||
@empty
|
||||
<img src="{{ $product->getFirstMediaUrl('images') }}" alt="Product Image" class="img-fluid img-thumbnail mb-2">
|
||||
@endforelse
|
||||
<h5 class="card-title">Product Images</h5>
|
||||
<div class="row">
|
||||
@foreach($product->getMedia('images') as $media)
|
||||
<div class="col-md-6 mb-2">
|
||||
<img src="{{ $media->getUrl() }}" alt="{{ $product->product_name }}" class="img-fluid">
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -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('<i class="bi bi-file-earmark-excel-fill"></i> Excel'),
|
||||
->text('<i class="bi bi-file-earmark-excel-fill"></i> Excel')
|
||||
->className('btn btn-success btn-sm no-corner'),
|
||||
Button::make('print')
|
||||
->text('<i class="bi bi-printer-fill"></i> Print'),
|
||||
->text('<i class="bi bi-printer-fill"></i> Print')
|
||||
->className('btn btn-primary btn-sm no-corner'),
|
||||
Button::make('reset')
|
||||
->text('<i class="bi bi-x-circle"></i> Reset'),
|
||||
->text('<i class="bi bi-x-circle"></i> Reset')
|
||||
->className('btn btn-warning btn-sm no-corner'),
|
||||
Button::make('reload')
|
||||
->text('<i class="bi bi-arrow-repeat"></i> 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'),
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
// Drop unused columns
|
||||
Schema::table('purchases', function (Blueprint $table) {
|
||||
$columns = ['tax_percentage', 'tax_amount', 'shipping_amount', 'status'];
|
||||
foreach ($columns as $column) {
|
||||
if (Schema::hasColumn('purchases', $column)) {
|
||||
$table->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');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
|
@ -0,0 +1,36 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('product_batches', function (Blueprint $table) {
|
||||
// Add purchase_id column if not exists
|
||||
if (!Schema::hasColumn('product_batches', 'purchase_id')) {
|
||||
$table->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');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
|
@ -0,0 +1,48 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
Schema::table('purchases', function (Blueprint $table) {
|
||||
// Drop unused columns
|
||||
$table->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');
|
||||
});
|
||||
}
|
||||
};
|
|
@ -0,0 +1,88 @@
|
|||
<?php
|
||||
|
||||
namespace Modules\Purchase\Entities;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class ProductBatch extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'purchase_id',
|
||||
'product_id',
|
||||
'branch_id',
|
||||
'batch_code',
|
||||
'quantity',
|
||||
'purchase_price',
|
||||
'expired_date',
|
||||
'created_by',
|
||||
'updated_by'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'quantity' => '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');
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
|
||||
|
|
|
@ -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';
|
||||
}
|
||||
|
||||
$purchase = Purchase::create([
|
||||
'date' => $request->date,
|
||||
'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,
|
||||
'payment_method' => $request->payment_method,
|
||||
'note' => $request->note,
|
||||
'tax_amount' => Cart::instance('purchase')->tax() * 100,
|
||||
'discount_amount' => Cart::instance('purchase')->discount() * 100,
|
||||
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'
|
||||
]);
|
||||
|
||||
foreach (Cart::instance('purchase')->content() as $cart_item) {
|
||||
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,
|
||||
]);
|
||||
|
||||
if ($request->status == 'Completed') {
|
||||
$product = Product::findOrFail($cart_item->id);
|
||||
$product->update([
|
||||
'product_quantity' => $product->product_quantity + $cart_item->qty
|
||||
]);
|
||||
}
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed',
|
||||
'errors' => $validator->errors()
|
||||
], 422);
|
||||
}
|
||||
|
||||
Cart::instance('purchase')->destroy();
|
||||
DB::beginTransaction();
|
||||
|
||||
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
|
||||
]);
|
||||
}
|
||||
try {
|
||||
// Calculate total amount
|
||||
$totalAmount = collect($request->products)->sum(function ($product) {
|
||||
return $product['quantity'] * $product['purchase_price'];
|
||||
});
|
||||
|
||||
toast('Purchase Created!', 'success');
|
||||
// Calculate due amount
|
||||
$dueAmount = $totalAmount - $request->paid_amount;
|
||||
|
||||
return redirect()->route('purchases.index');
|
||||
// Determine payment status
|
||||
$paymentStatus = $request->paid_amount == 0 ? 'Unpaid' :
|
||||
($dueAmount == 0 ? 'Paid' : 'Partial');
|
||||
|
||||
// 1. Simpan ke purchases
|
||||
$purchase = Purchase::create([
|
||||
'reference_no' => $request->reference_no,
|
||||
'supplier_id' => $request->supplier_id,
|
||||
'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,
|
||||
'user_id' => auth()->id(),
|
||||
'branch_id' => session(['active_branch' => 1]),// contoh ID cabang default
|
||||
'created_by' => auth()->user()->name,
|
||||
'updated_by' => auth()->user()->name
|
||||
]);
|
||||
|
||||
// 2. Simpan detail produk & batch
|
||||
foreach ($request->products as $product) {
|
||||
// Detail
|
||||
PurchaseDetail::create([
|
||||
'purchase_id' => $purchase->id,
|
||||
'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
|
||||
]);
|
||||
|
||||
// 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
|
||||
]);
|
||||
}
|
||||
|
||||
// 3. Simpan pembayaran (jika ada)
|
||||
if ($purchase->paid_amount > 0) {
|
||||
PurchasePayment::create([
|
||||
'purchase_id' => $purchase->id,
|
||||
'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
|
||||
]);
|
||||
}
|
||||
|
||||
DB::commit();
|
||||
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,230 @@
|
|||
<?php
|
||||
|
||||
namespace Modules\Purchase\Http\Livewire;
|
||||
|
||||
use Livewire\Component;
|
||||
use Modules\People\Entities\Supplier;
|
||||
use Modules\Product\Entities\Product;
|
||||
use Modules\Purchase\Entities\Purchase;
|
||||
use Modules\Purchase\Entities\PurchaseDetail;
|
||||
use Modules\Purchase\Entities\PurchasePayment;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class CreatePurchase extends Component
|
||||
{
|
||||
public $supplier_id;
|
||||
public $reference_no;
|
||||
public $date;
|
||||
public $payment_method;
|
||||
public $items = [];
|
||||
public $discount = 0;
|
||||
public $discount_percentage = 0;
|
||||
public $total_amount = 0;
|
||||
public $total_after_discount = 0;
|
||||
public $paid_amount = 0;
|
||||
public $due_amount = 0;
|
||||
public $note;
|
||||
|
||||
protected $rules = [
|
||||
'supplier_id' => '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()
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
<?php
|
||||
|
||||
namespace Modules\Purchase\Http\Livewire;
|
||||
|
||||
use Livewire\Component;
|
||||
use Livewire\WithPagination;
|
||||
use Modules\Purchase\Entities\Purchase;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class PurchaseList extends Component
|
||||
{
|
||||
use WithPagination;
|
||||
|
||||
public $search = '';
|
||||
public $dateFrom = '';
|
||||
public $dateTo = '';
|
||||
public $supplier_id = '';
|
||||
public $payment_status = '';
|
||||
public $perPage = 10;
|
||||
|
||||
protected $queryString = [
|
||||
'search' => ['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()
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
<?php
|
||||
|
||||
namespace Modules\Purchase\Http\Livewire;
|
||||
|
||||
use Livewire\Component;
|
||||
use Modules\Purchase\Entities\Purchase;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class PurchaseShow extends Component
|
||||
{
|
||||
public $purchase;
|
||||
public $purchase_id;
|
||||
public $payment_amount = 0;
|
||||
public $payment_method = 'Cash';
|
||||
public $payment_note = '';
|
||||
|
||||
protected $rules = [
|
||||
'payment_amount' => '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');
|
||||
}
|
||||
}
|
|
@ -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([
|
||||
|
|
|
@ -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'));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -2,138 +2,6 @@
|
|||
|
||||
@section('title', 'Create Purchase')
|
||||
|
||||
@section('breadcrumb')
|
||||
<ol class="breadcrumb border-0 m-0">
|
||||
<li class="breadcrumb-item"><a href="{{ route('home') }}">Home</a></li>
|
||||
<li class="breadcrumb-item"><a href="{{ route('purchases.index') }}">Purchases</a></li>
|
||||
<li class="breadcrumb-item active">Add</li>
|
||||
</ol>
|
||||
@endsection
|
||||
|
||||
@section('content')
|
||||
<div class="container-fluid mb-4">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<livewire:search-product/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-4">
|
||||
<div class="col-md-12">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
@include('utils.alerts')
|
||||
<form id="purchase-form" action="{{ route('purchases.store') }}" method="POST">
|
||||
@csrf
|
||||
|
||||
<div class="form-row">
|
||||
<div class="col-lg-4">
|
||||
<div class="form-group">
|
||||
<label for="reference">Reference <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" name="reference" required readonly value="PR">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<div class="from-group">
|
||||
<div class="form-group">
|
||||
<label for="supplier_id">Supplier <span class="text-danger">*</span></label>
|
||||
<select class="form-control" name="supplier_id" id="supplier_id" required>
|
||||
@foreach(\Modules\People\Entities\Supplier::all() as $supplier)
|
||||
<option value="{{ $supplier->id }}">{{ $supplier->supplier_name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<div class="from-group">
|
||||
<div class="form-group">
|
||||
<label for="date">Date <span class="text-danger">*</span></label>
|
||||
<input type="date" class="form-control" name="date" required value="{{ now()->format('Y-m-d') }}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<livewire:product-cart :cartInstance="'purchase'"/>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="col-lg-4">
|
||||
<div class="form-group">
|
||||
<label for="status">Status <span class="text-danger">*</span></label>
|
||||
<select class="form-control" name="status" id="status" required>
|
||||
<option value="Pending">Pending</option>
|
||||
<option value="Ordered">Ordered</option>
|
||||
<option value="Completed">Completed</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<div class="from-group">
|
||||
<div class="form-group">
|
||||
<label for="payment_method">Payment Method <span class="text-danger">*</span></label>
|
||||
<select class="form-control" name="payment_method" id="payment_method" required>
|
||||
<option value="Cash">Cash</option>
|
||||
<option value="Credit Card">Credit Card</option>
|
||||
<option value="Bank Transfer">Bank Transfer</option>
|
||||
<option value="Cheque">Cheque</option>
|
||||
<option value="Other">Other</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<div class="form-group">
|
||||
<label for="paid_amount">Amount Paid <span class="text-danger">*</span></label>
|
||||
<div class="input-group">
|
||||
<input id="paid_amount" type="text" class="form-control" name="paid_amount" required>
|
||||
<div class="input-group-append">
|
||||
<button id="getTotalAmount" class="btn btn-primary" type="button">
|
||||
<i class="bi bi-check-square"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="note">Note (If Needed)</label>
|
||||
<textarea name="note" id="note" rows="5" class="form-control"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
Create Purchase <i class="bi bi-check"></i>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<livewire:purchase::create-purchase />
|
||||
@endsection
|
||||
|
||||
@push('page_scripts')
|
||||
<script src="{{ asset('js/jquery-mask-money.js') }}"></script>
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
$('#paid_amount').maskMoney({
|
||||
prefix:'{{ settings()->currency->symbol }}',
|
||||
thousands:'{{ settings()->currency->thousand_separator }}',
|
||||
decimal:'{{ settings()->currency->decimal_separator }}',
|
||||
allowZero: true,
|
||||
});
|
||||
|
||||
$('#getTotalAmount').click(function () {
|
||||
$('#paid_amount').maskMoney('mask', {{ Cart::instance('purchase')->total() }});
|
||||
});
|
||||
|
||||
$('#purchase-form').submit(function () {
|
||||
var paid_amount = $('#paid_amount').maskMoney('unmasked')[0];
|
||||
$('#paid_amount').val(paid_amount);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
|
|
|
@ -14,27 +14,21 @@
|
|||
@endsection
|
||||
|
||||
@section('content')
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Purchases</h3>
|
||||
<div class="card-tools">
|
||||
<a href="{{ route('purchases.create') }}" class="btn btn-primary">
|
||||
Add Purchase <i class="bi bi-plus"></i>
|
||||
<i class="fas fa-plus"></i> Create Purchase
|
||||
</a>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="table-responsive">
|
||||
{!! $dataTable->table() !!}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{{ $dataTable->table() }}
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@push('page_scripts')
|
||||
{!! $dataTable->scripts() !!}
|
||||
@push('scripts')
|
||||
{{ $dataTable->scripts() }}
|
||||
@endpush
|
||||
|
|
|
@ -0,0 +1,252 @@
|
|||
@push('scripts')
|
||||
<script>
|
||||
document.addEventListener('livewire:init', function () {
|
||||
// Success Message
|
||||
Livewire.on('showSuccessMessage', function(data) {
|
||||
Swal.fire({
|
||||
title: 'Success!',
|
||||
text: data.message,
|
||||
icon: 'success',
|
||||
confirmButtonText: 'OK'
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed && data.redirect) {
|
||||
window.location.href = data.redirect;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Error Message
|
||||
Livewire.on('showErrorMessage', function(data) {
|
||||
Swal.fire({
|
||||
title: 'Error!',
|
||||
text: data.message,
|
||||
icon: 'error',
|
||||
confirmButtonText: 'OK'
|
||||
});
|
||||
});
|
||||
|
||||
// Confirmation Dialog
|
||||
Livewire.on('showConfirmDialog', function(data) {
|
||||
Swal.fire({
|
||||
title: data.title,
|
||||
text: data.text,
|
||||
icon: data.icon,
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: '#3085d6',
|
||||
cancelButtonColor: '#d33',
|
||||
confirmButtonText: data.confirmButtonText,
|
||||
cancelButtonText: data.cancelButtonText
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
Livewire.dispatch('confirmed');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
|
||||
<div>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Create Purchase</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form wire:submit.prevent="save">
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="form-group">
|
||||
<label>Reference No</label>
|
||||
<input type="text" class="form-control" wire:model="reference_no" readonly>
|
||||
@error('reference_no') <span class="text-danger">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="form-group">
|
||||
<label>Supplier</label>
|
||||
<select class="form-control" wire:model="supplier_id">
|
||||
<option value="">Select Supplier</option>
|
||||
@foreach($suppliers as $supplier)
|
||||
<option value="{{ $supplier->id }}">{{ $supplier->supplier_name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@error('supplier_id') <span class="text-danger">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="form-group">
|
||||
<label>Date</label>
|
||||
<input type="date" class="form-control" wire:model="date">
|
||||
@error('date') <span class="text-danger">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-4">
|
||||
<div class="col-md-4">
|
||||
<div class="form-group">
|
||||
<label>Payment Method</label>
|
||||
<select class="form-control" wire:model="payment_method">
|
||||
<option value="">Select Payment Method</option>
|
||||
<option value="cash">Cash</option>
|
||||
<option value="transfer">Transfer</option>
|
||||
<option value="credit">Credit</option>
|
||||
</select>
|
||||
@error('payment_method') <span class="text-danger">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="form-group">
|
||||
<label>Discount Percentage</label>
|
||||
<input type="number" class="form-control" wire:model="discount_percentage" min="0" max="100" step="0.01">
|
||||
@error('discount_percentage') <span class="text-danger">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="form-group">
|
||||
<label>Discount Amount</label>
|
||||
<input type="number" class="form-control" wire:model="discount" min="0" step="0.01">
|
||||
@error('discount') <span class="text-danger">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-4">
|
||||
<div class="col-md-12">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h4>Products</h4>
|
||||
<button type="button" class="btn btn-primary" wire:click="addItem">
|
||||
Add Product
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@error('items') <span class="text-danger">{{ $message }}</span> @enderror
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Product</th>
|
||||
<th>Quantity</th>
|
||||
<th>Purchase Price</th>
|
||||
<th>Unit Price</th>
|
||||
<th>Discount</th>
|
||||
<th>Tax</th>
|
||||
<th>Subtotal</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($items as $index => $item)
|
||||
<tr>
|
||||
<td>
|
||||
<select class="form-control" wire:model="items.{{ $index }}.product_id">
|
||||
<option value="">Select Product</option>
|
||||
@foreach($products as $product)
|
||||
<option value="{{ $product->id }}">{{ $product->product_name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@error("items.{$index}.product_id") <span class="text-danger">{{ $message }}</span> @enderror
|
||||
</td>
|
||||
<td>
|
||||
<input type="number" class="form-control" wire:model="items.{{ $index }}.qty" min="1">
|
||||
@error("items.{$index}.qty") <span class="text-danger">{{ $message }}</span> @enderror
|
||||
</td>
|
||||
<td>
|
||||
<input type="number" class="form-control" wire:model="items.{{ $index }}.purchase_price" min="0" step="0.01">
|
||||
@error("items.{$index}.purchase_price") <span class="text-danger">{{ $message }}</span> @enderror
|
||||
</td>
|
||||
<td>
|
||||
<input type="number" class="form-control" wire:model="items.{{ $index }}.unit_price" min="0" step="0.01">
|
||||
@error("items.{$index}.unit_price") <span class="text-danger">{{ $message }}</span> @enderror
|
||||
</td>
|
||||
<td>
|
||||
<div class="input-group">
|
||||
<input type="number" class="form-control" wire:model="items.{{ $index }}.discount" min="0" step="0.01">
|
||||
<select class="form-control" wire:model="items.{{ $index }}.discount_type">
|
||||
<option value="">Type</option>
|
||||
<option value="percentage">%</option>
|
||||
<option value="fixed">Fixed</option>
|
||||
</select>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<input type="number" class="form-control" wire:model="items.{{ $index }}.tax" min="0" step="0.01">
|
||||
</td>
|
||||
<td>
|
||||
{{ number_format($item['qty'] * $item['unit_price'], 2) }}
|
||||
</td>
|
||||
<td>
|
||||
<button type="button" class="btn btn-danger btn-sm" wire:click="removeItem({{ $index }})">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td colspan="6" class="text-right"><strong>Subtotal:</strong></td>
|
||||
<td colspan="2">
|
||||
<strong>{{ number_format($total_amount, 2) }}</strong>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="6" class="text-right"><strong>Discount:</strong></td>
|
||||
<td colspan="2">
|
||||
<strong>{{ number_format($discount, 2) }}</strong>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="6" class="text-right"><strong>Total After Discount:</strong></td>
|
||||
<td colspan="2">
|
||||
<strong>{{ number_format($total_after_discount, 2) }}</strong>
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-4">
|
||||
<div class="col-md-4">
|
||||
<div class="form-group">
|
||||
<label>Paid Amount</label>
|
||||
<input type="number" class="form-control" wire:model="paid_amount" min="0" step="0.01">
|
||||
@error('paid_amount') <span class="text-danger">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="form-group">
|
||||
<label>Due Amount</label>
|
||||
<input type="text" class="form-control" value="{{ number_format($due_amount, 2) }}" readonly>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="form-group">
|
||||
<label>Payment Status</label>
|
||||
<input type="text" class="form-control" value="{{ $paid_amount >= $total_after_discount ? 'Paid' : 'Due' }}" readonly>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-4">
|
||||
<div class="col-md-12">
|
||||
<div class="form-group">
|
||||
<label>Note</label>
|
||||
<textarea class="form-control" wire:model="note" rows="3"></textarea>
|
||||
@error('note') <span class="text-danger">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-4">
|
||||
<div class="col-md-12">
|
||||
<button type="submit" class="btn btn-primary">Create Purchase</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,134 @@
|
|||
<div>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h3 class="card-title">Purchase List</h3>
|
||||
</div>
|
||||
<div class="col-md-6 text-right">
|
||||
<a href="{{ route('purchases.create') }}" class="btn btn-primary">
|
||||
<i class="fas fa-plus"></i> Create Purchase
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<!-- Filters -->
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-2">
|
||||
<input wire:model.debounce.300ms="search" type="text" class="form-control" placeholder="Search...">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<input wire:model="dateFrom" type="date" class="form-control" placeholder="Date From">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<input wire:model="dateTo" type="date" class="form-control" placeholder="Date To">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<select wire:model="supplier_id" class="form-control">
|
||||
<option value="">All Suppliers</option>
|
||||
@foreach($suppliers as $supplier)
|
||||
<option value="{{ $supplier->id }}">{{ $supplier->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<select wire:model="payment_status" class="form-control">
|
||||
<option value="">All Status</option>
|
||||
<option value="Paid">Paid</option>
|
||||
<option value="Partial">Partial</option>
|
||||
<option value="Unpaid">Unpaid</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<select wire:model="perPage" class="form-control">
|
||||
<option value="10">10 per page</option>
|
||||
<option value="25">25 per page</option>
|
||||
<option value="50">50 per page</option>
|
||||
<option value="100">100 per page</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Table -->
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Reference</th>
|
||||
<th>Date</th>
|
||||
<th>Supplier</th>
|
||||
<th>Total Amount</th>
|
||||
<th>Paid Amount</th>
|
||||
<th>Due Amount</th>
|
||||
<th>Payment Status</th>
|
||||
<th>Created By</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($purchases as $purchase)
|
||||
<tr>
|
||||
<td>{{ $purchase->reference_no }}</td>
|
||||
<td>{{ $purchase->date->format('d/m/Y') }}</td>
|
||||
<td>{{ $purchase->supplier->name }}</td>
|
||||
<td>{{ number_format($purchase->total_amount, 2) }}</td>
|
||||
<td>{{ number_format($purchase->paid_amount, 2) }}</td>
|
||||
<td>{{ $purchase->due_amount ? number_format($purchase->due_amount, 2) : '0.00' }}</td>
|
||||
<td>
|
||||
<span class="badge badge-{{ $purchase->payment_status === 'Paid' ? 'success' : ($purchase->payment_status === 'Partial' ? 'warning' : 'danger') }}">
|
||||
{{ $purchase->payment_status }}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ $purchase->created_by }}</td>
|
||||
<td>
|
||||
<div class="btn-group">
|
||||
<a href="{{ route('purchases.show', $purchase->id) }}" class="btn btn-info btn-sm">
|
||||
<i class="fas fa-eye"></i>
|
||||
</a>
|
||||
@if($purchase->payment_status !== 'Paid')
|
||||
<a href="{{ route('purchases.edit', $purchase->id) }}" class="btn btn-primary btn-sm">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
<button wire:click="deletePurchase({{ $purchase->id }})" class="btn btn-danger btn-sm" onclick="confirm('Are you sure?') || event.stopImmediatePropagation()">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
@endif
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="9" class="text-center">No purchases found.</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div class="mt-3">
|
||||
{{ $purchases->links() }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (session()->has('message'))
|
||||
<div class="alert alert-success alert-dismissible fade show mt-3" role="alert">
|
||||
{{ session('message') }}
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if (session()->has('error'))
|
||||
<div class="alert alert-danger alert-dismissible fade show mt-3" role="alert">
|
||||
{{ session('error') }}
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
|
@ -0,0 +1,195 @@
|
|||
<div>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h3 class="card-title">Purchase Details</h3>
|
||||
</div>
|
||||
<div class="col-md-6 text-right">
|
||||
<a href="{{ route('purchases.index') }}" class="btn btn-secondary">
|
||||
<i class="fas fa-arrow-left"></i> Back to List
|
||||
</a>
|
||||
@if($purchase->payment_status !== 'Paid')
|
||||
<a href="{{ route('purchases.edit', $purchase->id) }}" class="btn btn-primary">
|
||||
<i class="fas fa-edit"></i> Edit
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<!-- Purchase Info -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-6">
|
||||
<h5>Purchase Information</h5>
|
||||
<table class="table table-bordered">
|
||||
<tr>
|
||||
<th>Reference No</th>
|
||||
<td>{{ $purchase->reference_no }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<td>{{ $purchase->date->format('d/m/Y') }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Supplier</th>
|
||||
<td>{{ $purchase->supplier->name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Created By</th>
|
||||
<td>{{ $purchase->created_by }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h5>Payment Information</h5>
|
||||
<table class="table table-bordered">
|
||||
<tr>
|
||||
<th>Total Amount</th>
|
||||
<td>{{ number_format($purchase->total_amount, 2) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Paid Amount</th>
|
||||
<td>{{ number_format($purchase->paid_amount, 2) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Due Amount</th>
|
||||
<td>{{ $purchase->due_amount ? number_format($purchase->due_amount, 2) : '0.00' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Payment Status</th>
|
||||
<td>
|
||||
<span class="badge badge-{{ $purchase->payment_status === 'Paid' ? 'success' : ($purchase->payment_status === 'Partial' ? 'warning' : 'danger') }}">
|
||||
{{ $purchase->payment_status }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Purchase Details -->
|
||||
<h5>Purchase Details</h5>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Product</th>
|
||||
<th>Quantity</th>
|
||||
<th>Price</th>
|
||||
<th>Subtotal</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($purchase->purchaseDetails as $detail)
|
||||
<tr>
|
||||
<td>{{ $detail->product_name }}</td>
|
||||
<td>{{ $detail->quantity }}</td>
|
||||
<td>{{ number_format($detail->price, 2) }}</td>
|
||||
<td>{{ number_format($detail->sub_total, 2) }}</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Payment History -->
|
||||
<h5 class="mt-4">Payment History</h5>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Amount</th>
|
||||
<th>Method</th>
|
||||
<th>Note</th>
|
||||
<th>Created By</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($purchase->purchasePayments as $payment)
|
||||
<tr>
|
||||
<td>{{ $payment->created_at->format('d/m/Y H:i') }}</td>
|
||||
<td>{{ number_format($payment->amount, 2) }}</td>
|
||||
<td>{{ $payment->payment_method }}</td>
|
||||
<td>{{ $payment->note }}</td>
|
||||
<td>{{ $payment->created_by }}</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="5" class="text-center">No payments found.</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Add Payment Form -->
|
||||
@if($purchase->payment_status !== 'Paid')
|
||||
<div class="card mt-4">
|
||||
<div class="card-header">
|
||||
<h5>Add Payment</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form wire:submit.prevent="addPayment">
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<div class="form-group">
|
||||
<label>Amount</label>
|
||||
<input type="number" wire:model="payment_amount" class="form-control" step="0.01" min="0" max="{{ $purchase->due_amount }}">
|
||||
@error('payment_amount') <span class="text-danger">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="form-group">
|
||||
<label>Payment Method</label>
|
||||
<select wire:model="payment_method" class="form-control">
|
||||
<option value="Cash">Cash</option>
|
||||
<option value="Bank Transfer">Bank Transfer</option>
|
||||
<option value="Credit Card">Credit Card</option>
|
||||
</select>
|
||||
@error('payment_method') <span class="text-danger">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="form-group">
|
||||
<label>Note</label>
|
||||
<input type="text" wire:model="payment_note" class="form-control">
|
||||
@error('payment_note') <span class="text-danger">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<div class="form-group">
|
||||
<label> </label>
|
||||
<button type="submit" class="btn btn-primary btn-block">
|
||||
Add Payment
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (session()->has('message'))
|
||||
<div class="alert alert-success alert-dismissible fade show mt-3" role="alert">
|
||||
{{ session('message') }}
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if (session()->has('error'))
|
||||
<div class="alert alert-danger alert-dismissible fade show mt-3" role="alert">
|
||||
{{ session('error') }}
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
|
@ -1,42 +1,41 @@
|
|||
<div class="btn-group dropleft">
|
||||
<button type="button" class="btn btn-ghost-primary dropdown rounded" data-toggle="dropdown" aria-expanded="false">
|
||||
<i class="bi bi-three-dots-vertical"></i>
|
||||
<div class="btn-group">
|
||||
<a href="{{ route('purchases.show', $id) }}" class="btn btn-info btn-sm">
|
||||
<i class="fas fa-eye"></i>
|
||||
</a>
|
||||
<a href="{{ route('purchases.edit', $id) }}" class="btn btn-primary btn-sm">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
<a href="{{ route('purchases.pdf', $id) }}" class="btn btn-secondary btn-sm" target="_blank">
|
||||
<i class="fas fa-print"></i>
|
||||
</a>
|
||||
<button type="button" class="btn btn-danger btn-sm" onclick="deletePurchase({{ $id }})">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
@can('access_purchase_payments')
|
||||
<a href="{{ route('purchase-payments.index', $data->id) }}" class="dropdown-item">
|
||||
<i class="bi bi-cash-coin mr-2 text-warning" style="line-height: 1;"></i> Show Payments
|
||||
</a>
|
||||
@endcan
|
||||
@can('access_purchase_payments')
|
||||
@if($data->due_amount > 0)
|
||||
<a href="{{ route('purchase-payments.create', $data->id) }}" class="dropdown-item">
|
||||
<i class="bi bi-plus-circle-dotted mr-2 text-success" style="line-height: 1;"></i> Add Payment
|
||||
</a>
|
||||
@endif
|
||||
@endcan
|
||||
@can('edit_purchases')
|
||||
<a href="{{ route('purchases.edit', $data->id) }}" class="dropdown-item">
|
||||
<i class="bi bi-pencil mr-2 text-primary" style="line-height: 1;"></i> Edit
|
||||
</a>
|
||||
@endcan
|
||||
@can('show_purchases')
|
||||
<a href="{{ route('purchases.show', $data->id) }}" class="dropdown-item">
|
||||
<i class="bi bi-eye mr-2 text-info" style="line-height: 1;"></i> Details
|
||||
</a>
|
||||
@endcan
|
||||
@can('delete_purchases')
|
||||
<button id="delete" class="dropdown-item" onclick="
|
||||
event.preventDefault();
|
||||
if (confirm('Are you sure? It will delete the data permanently!')) {
|
||||
document.getElementById('destroy{{ $data->id }}').submit()
|
||||
}">
|
||||
<i class="bi bi-trash mr-2 text-danger" style="line-height: 1;"></i> Delete
|
||||
<form id="destroy{{ $data->id }}" class="d-none" action="{{ route('purchases.destroy', $data->id) }}" method="POST">
|
||||
@csrf
|
||||
@method('delete')
|
||||
</form>
|
||||
</button>
|
||||
@endcan
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
function deletePurchase(id) {
|
||||
if (confirm('Are you sure you want to delete this purchase?')) {
|
||||
$.ajax({
|
||||
url: `/purchases/${id}`,
|
||||
type: 'DELETE',
|
||||
data: {
|
||||
_token: '{{ csrf_token() }}'
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
toastr.success(response.message);
|
||||
$('#purchases-table').DataTable().ajax.reload();
|
||||
} else {
|
||||
toastr.error(response.message);
|
||||
}
|
||||
},
|
||||
error: function(error) {
|
||||
toastr.error('An error occurred while deleting the purchase.');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@endpush
|
||||
|
|
|
@ -11,30 +11,36 @@
|
|||
|
|
||||
*/
|
||||
|
||||
Route::group(['middleware' => 'auth'], function () {
|
||||
|
||||
//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);
|
||||
|
||||
$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');
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Modules\Purchase\Http\Controllers\PurchaseController;
|
||||
use Modules\Purchase\Http\Livewire\CreatePurchase;
|
||||
|
||||
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');
|
||||
});
|
||||
|
||||
// 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');
|
||||
});
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\ProductBatch;
|
||||
use App\Http\Requests\ProductBatchRequest;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ProductBatchController extends Controller
|
||||
{
|
||||
public function store(ProductBatchRequest $request)
|
||||
{
|
||||
try {
|
||||
$batch = ProductBatch::addStock($request->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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use App\Models\Product;
|
||||
|
||||
class ProductBatchRequest extends FormRequest
|
||||
{
|
||||
public function authorize()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules()
|
||||
{
|
||||
$rules = [
|
||||
'product_id' => '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',
|
||||
];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
<?php
|
||||
|
||||
namespace App\Livewire\Purchase;
|
||||
|
||||
use Livewire\Component;
|
||||
|
||||
class CreatePurchase extends Component
|
||||
{
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.purchase.create-purchase');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,101 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class ProductBatch extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'product_id',
|
||||
'branch_id',
|
||||
'batch_code',
|
||||
'quantity',
|
||||
'purchase_price',
|
||||
'expired_date',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'expired_date' => '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);
|
||||
}
|
||||
}
|
|
@ -10290,5 +10290,5 @@
|
|||
"php": "^8.1"
|
||||
},
|
||||
"platform-dev": [],
|
||||
"plugin-api-version": "2.3.0"
|
||||
"plugin-api-version": "2.6.0"
|
||||
}
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
Schema::create('product_batches', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
};
|
|
@ -0,0 +1,36 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('products', function (Blueprint $table) {
|
||||
$table->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);
|
||||
});
|
||||
}
|
||||
};
|
|
@ -24,7 +24,10 @@
|
|||
<i class="bi bi-bell" style="font-size: 20px;"></i>
|
||||
<span class="badge badge-pill badge-danger">
|
||||
@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
|
||||
</span>
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
<div>
|
||||
{{-- The whole world belongs to you. --}}
|
||||
</div>
|
|
@ -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']);
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue