Everything posted by Mahmoud
-
Vehicle Management System - Complete Implementation Guide
Vehicle Management System for Ultimate POSThis comprehensive guide covers the complete implementation of a Vehicle Management system in Ultimate POS with Laravel, including CRUD operations, transaction integration, and reporting capabilities. OverviewThe Vehicle Management system provides: ✅ Complete CRUD operations for vehicles ✅ Detailed vehicle information tracking (model, license, insurance, etc.) ✅ Vehicle assignment to sales and purchase transactions ✅ Comprehensive vehicle reports and analytics ✅ Excel export functionality ✅ Multi-business support with data isolation ✅ Permission-based access control ScreenshotsVehicle Index Page Vehicle Create Modal Vehicle Edit Modal Vehicle View Modal Vehicle Load Reports Sell list Sell create page POS create page Download Template for Phase 1vehicle-management-system-all-files.zip Database SetupSingle Comprehensive MigrationFile: database/migrations/2025_09_29_180000_create_vehicle_management_system.php <?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; return new class extends Migration { /** * Run the migrations. * * @return void */ public function up() { // Create vehicles table with all fields Schema::create('vehicles', function (Blueprint $table) { $table->id(); $table->integer('business_id')->unsigned(); $table->foreign('business_id')->references('id')->on('business')->onDelete('cascade'); // Basic Information $table->string('vehicle_number')->unique(); $table->string('driver_name'); $table->string('vehicle_type'); $table->enum('status', ['active', 'inactive'])->default('active'); // Vehicle Details $table->string('vehicle_model')->nullable(); $table->string('license_plate')->nullable(); $table->string('vin_number')->nullable(); $table->year('year')->nullable(); $table->string('color')->nullable(); $table->string('fuel_type')->nullable(); $table->string('engine_capacity')->nullable(); // Operational Data $table->decimal('current_mileage', 10, 2)->nullable(); $table->date('purchase_date')->nullable(); $table->decimal('purchase_price', 22, 4)->nullable(); $table->date('insurance_expiry')->nullable(); $table->date('registration_expiry')->nullable(); $table->date('last_service_date')->nullable(); $table->date('next_service_due')->nullable(); // Performance Tracking $table->decimal('fuel_efficiency', 8, 2)->nullable()->comment('L/100km'); $table->decimal('max_load_capacity', 10, 2)->nullable()->comment('in tons'); $table->decimal('daily_rate', 22, 4)->nullable(); $table->decimal('cost_per_km', 22, 4)->nullable(); // Contact & Assignment $table->string('driver_phone')->nullable(); $table->string('driver_license')->nullable(); $table->string('assigned_route')->nullable(); $table->string('home_location')->nullable(); // System Fields $table->integer('created_by')->unsigned(); $table->timestamps(); $table->softDeletes(); // Indexes $table->index('business_id'); $table->index('status'); $table->index('vehicle_type'); }); // Add vehicle_id to transactions table Schema::table('transactions', function (Blueprint $table) { $table->unsignedBigInteger('vehicle_id')->nullable()->after('business_id'); $table->foreign('vehicle_id') ->references('id') ->on('vehicles') ->onDelete('set null'); $table->index('vehicle_id'); }); } /** * Reverse the migrations. * * @return void */ public function down() { // Remove vehicle_id from transactions table Schema::table('transactions', function (Blueprint $table) { $table->dropForeign(['vehicle_id']); $table->dropIndex(['vehicle_id']); $table->dropColumn('vehicle_id'); }); // Drop vehicles table Schema::dropIfExists('vehicles'); } }; Run Migration: # Run the migration php artisan migrate # Or run specific file php artisan migrate --path=database/migrations/2025_09_29_180000_create_vehicle_management_system.php Why Single Migration Approach: ✅ Simple: Everything in one file ✅ Complete: Full table structure from the start ✅ Clean: No dependency management needed ✅ Fast: One-step deployment ✅ Easy Rollback: Single rollback removes everything cleanly What This Creates: ✅ Complete vehicles table with all 28 fields ✅ vehicle_id link to transactions table ✅ Proper indexes for performance ✅ Foreign key with SET NULL on delete ✅ Soft deletes support ✅ Business-level data isolation Migration ManagementTo check migration status: php artisan migrate:status | grep vehicle To rollback the migration: # Rollback the vehicle management system php artisan migrate:rollback --step=1 To rollback and re-run: php artisan migrate:refresh --step=1 Current Migration File: 2025_09_29_180000_create_vehicle_management_system.php - Complete vehicle management system Model ImplementationFile: app/Vehicle.php <?php namespace App; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; class Vehicle extends Model { use SoftDeletes; protected $guarded = ['id']; protected $fillable = [ 'vehicle_number', 'driver_name', 'vehicle_type', 'status', 'business_id', 'created_by', // Vehicle Details 'vehicle_model', 'license_plate', 'vin_number', 'year', 'color', 'fuel_type', 'engine_capacity', // Operational Data 'current_mileage', 'purchase_date', 'purchase_price', 'insurance_expiry', 'registration_expiry', 'last_service_date', 'next_service_due', // Performance Tracking 'fuel_efficiency', 'max_load_capacity', 'daily_rate', 'cost_per_km', // Contact & Assignment 'driver_phone', 'driver_license', 'assigned_route', 'home_location' ]; protected $casts = [ 'purchase_date' => 'date', 'insurance_expiry' => 'date', 'registration_expiry' => 'date', 'last_service_date' => 'date', 'next_service_due' => 'date', 'current_mileage' => 'decimal:2', 'purchase_price' => 'decimal:2', 'fuel_efficiency' => 'decimal:2', 'max_load_capacity' => 'decimal:2', 'daily_rate' => 'decimal:2', 'cost_per_km' => 'decimal:2', ]; /** * Get vehicles for dropdown */ public static function forDropdown($business_id, $show_none = false) { $vehicles = Vehicle::where('business_id', $business_id) ->where('status', 'active') ->orderBy('vehicle_number', 'asc') ->pluck('vehicle_number', 'id'); if ($show_none) { $vehicles->prepend(__('lang_v1.none'), ''); } return $vehicles; } /** * Accessor: Display name */ public function getDisplayNameAttribute() { return $this->vehicle_number . ' (' . $this->driver_name . ')'; } /** * Accessor: Full details */ public function getFullDetailsAttribute() { $details = $this->vehicle_number; if ($this->vehicle_model) { $details .= ' - ' . $this->vehicle_model; } if ($this->license_plate) { $details .= ' (' . $this->license_plate . ')'; } return $details; } /** * Scopes for filtering */ public function scopeActive($query) { return $query->where('status', 'active'); } public function scopeByFuelType($query, $fuel_type) { return $query->where('fuel_type', $fuel_type); } public function scopeInsuranceExpiringSoon($query, $days = 30) { return $query->whereDate('insurance_expiry', '<=', now()->addDays($days)) ->whereDate('insurance_expiry', '>=', now()); } } Controller ImplementationFile: app/Http/Controllers/VehicleController.php The controller includes: ✅ Index with DataTables ✅ Create/Store methods ✅ Edit/Update methods ✅ Delete (soft delete) ✅ Show (detail view) ✅ Reports method ✅ Excel export See docs/Vehicle-Management-System/files/VehicleController.php for complete code. View TemplatesAll view files are located in resources/views/vehicle/: 1. Index Page (index.blade.php)Lists all vehicles in DataTable Search and filter functionality Action buttons (Edit, Delete, View) 2. Create Modal (create.blade.php)Full vehicle form with all fields Validation Ajax submission 3. Edit Modal (edit.blade.php)Pre-populated form Update functionality 4. Show Page (show.blade.php)Detailed vehicle information Related transactions 5. Reports Page (reports.blade.php)Vehicle analytics Transaction history Excel export All view files are available in docs/Vehicle-Management-System/files/vehicle/ Transaction IntegrationSales (POS) Integration1. Add vehicle selection to POS create form: Edit resources/views/sale_pos/partials/pos_form.blade.php: <!-- Vehicle Selection --> <div class="col-md-4 col-sm-6"> <div class="form-group"> <label>@lang('lang_v1.vehicle'):</label> <div class="input-group"> <span class="input-group-addon"> <i class="fa fa-car"></i> </span> {!! Form::select('vehicle_id', $vehicles, null, [ 'class' => 'form-control select2', 'id' => 'vehicle_id', 'placeholder' => __('lang_v1.select_vehicle') ]) !!} </div> </div> </div> 2. Add vehicle selection to POS edit form: Edit resources/views/sale_pos/partials/pos_form_edit.blade.php: <!-- Vehicle Selection Row --> <div class="row"> <div class="col-md-4 col-sm-6"> <div class="form-group"> <label>@lang('lang_v1.vehicle'):</label> <div class="input-group"> <span class="input-group-addon"> <i class="fa fa-car"></i> </span> {!! Form::select('vehicle_id', $vehicles, $transaction->vehicle_id, [ 'class' => 'form-control select2', 'id' => 'vehicle_id', 'placeholder' => __('lang_v1.select_vehicle') ]) !!} </div> </div> </div> </div> 3. Update SellPosController: In app/Http/Controllers/SellPosController.php: // In create() method - add vehicles to view $vehicles = \App\Vehicle::forDropdown($business_id, true); return view('sale_pos.create') ->with(compact(..., 'vehicles')); // In edit() method - add vehicles to view $vehicles = \App\Vehicle::forDropdown($business_id, true); return view('sale_pos.edit') ->with(compact(..., 'vehicles')); // In store() method - save vehicle_id $input['vehicle_id'] = $request->has('vehicle_id') && !empty($request->input('vehicle_id')) ? $request->input('vehicle_id') : null; // In update() method - save vehicle_id $input['vehicle_id'] = $request->has('vehicle_id') && !empty($request->input('vehicle_id')) ? $request->input('vehicle_id') : null; 4. Update TransactionUtil: In app/Utils/TransactionUtil.php, add to updateSellTransaction() method: $update_date = [ // ... existing fields 'vehicle_id' => ! empty($input['vehicle_id']) ? $input['vehicle_id'] : null, // ... rest of fields ]; Purchase Integration1. Add vehicle selection to purchase create form: Edit resources/views/purchase/partials/purchase_entry_row.blade.php or main purchase form: <div class="col-sm-4"> <div class="form-group"> {!! Form::label('vehicle_id', __('lang_v1.vehicle') . ':') !!} <div class="input-group"> <span class="input-group-addon"> <i class="fa fa-car"></i> </span> {!! Form::select('vehicle_id', $vehicles, null, [ 'class' => 'form-control select2', 'placeholder' => __('lang_v1.select_vehicle') ]) !!} </div> </div> </div> 2. Update PurchaseController: Follow similar pattern as SellPosController to add vehicles to create/edit methods. Reporting SystemExport ClassFile: app/Exports/VehicleReportsExport.php Handles Excel export of vehicle transaction reports using Laravel Excel. Report Views1. Main Reports Page: Filter by date range Filter by vehicle Transaction type filter (sales/purchases/both) Summary statistics 2. PDF Export: Partial template at resources/views/vehicle/partials/pdf_export.blade.php Menu & PermissionsAdd Menu ItemEdit app/Http/Middleware/AdminSidebarMenu.php: // Vehicles menu (add before Settings Dropdown) if (auth()->user()->can('vehicle.view')) { $menu->url(action([\App\Http\Controllers\VehicleController::class, 'index']), __('lang_v1.vehicles'), [ 'icon' => '<svg aria-hidden="true" class="tw-size-5 tw-shrink-0" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M7 17m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0"></path> <path d="M17 17m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0"></path> <path d="M5 17h-2v-11a1 1 0 0 1 1 -1h9v12m-4 0h6m-6 -9h8l2 3v6"></path> </svg>', 'active' => request()->segment(1) == 'vehicles' ])->order(85); } Add PermissionsRun SQL or add to seeder: INSERT INTO permissions (name, guard_name, created_at, updated_at) VALUES ('vehicle.view', 'web', NOW(), NOW()), ('vehicle.create', 'web', NOW(), NOW()), ('vehicle.update', 'web', NOW(), NOW()), ('vehicle.delete', 'web', NOW(), NOW()); -- Assign to Admin role (adjust role_id as needed) INSERT INTO role_has_permissions (permission_id, role_id) SELECT id, 1 FROM permissions WHERE name LIKE 'vehicle.%'; Add RoutesEdit routes/web.php: Route::resource('vehicles', 'VehicleController'); Route::get('vehicles-reports', 'VehicleController@reports'); Route::get('vehicles-reports/export', 'VehicleController@exportReport'); Language KeysAdd to resources/lang/en/lang_v1.php: // Vehicles 'vehicles' => 'Vehicles', 'vehicle' => 'Vehicle', 'select_vehicle' => 'Select Vehicle', 'vehicle_help' => 'Optional: Assign a vehicle to this transaction', 'manage_your_vehicles' => 'Manage your vehicles', 'all_your_vehicles' => 'All Your Vehicles', 'add_vehicle' => 'Add Vehicle', 'edit_vehicle' => 'Edit Vehicle', 'vehicle_details' => 'Vehicle Details', 'vehicle_number' => 'Vehicle Number', 'driver_name' => 'Driver Name', 'driver_phone' => 'Driver Phone', 'driver_license' => 'Driver License', 'vehicle_type' => 'Vehicle Type', 'vehicle_model' => 'Vehicle Model', 'license_plate' => 'License Plate', 'vin_number' => 'VIN Number', 'fuel_type' => 'Fuel Type', 'engine_capacity' => 'Engine Capacity', 'current_mileage' => 'Current Mileage', 'purchase_price' => 'Purchase Price', 'insurance_expiry' => 'Insurance Expiry', 'registration_expiry' => 'Registration Expiry', 'last_service_date' => 'Last Service Date', 'next_service_due' => 'Next Service Due', 'fuel_efficiency' => 'Fuel Efficiency (L/100km)', 'max_load_capacity' => 'Max Load Capacity', 'daily_rate' => 'Daily Rate', 'cost_per_km' => 'Cost Per KM', 'assigned_route' => 'Assigned Route', 'home_location' => 'Home Location', 'vehicle_added_success' => 'Vehicle added successfully', 'vehicle_updated_success' => 'Vehicle updated successfully', 'vehicle_deleted_success' => 'Vehicle deleted successfully', 'vehicle_reports' => 'Vehicle Reports', 'vehicle_transactions' => 'Vehicle Transactions', 'no_vehicle_assigned' => 'No Vehicle Assigned', Complete File PackageAll implementation files are available in the files directory: Structure: docs/Vehicle-Management-System/files/ ├── Vehicle.php # Model ├── VehicleController.php # Controller ├── VehicleReportsExport.php # Excel Export ├── migrations/ │ ├── 2025_09_29_180000_create_vehicle_management_system.php │ └── create_vehicle_management_system.php # Alternative version └── vehicle/ # Views ├── index.blade.php ├── create.blade.php ├── edit.blade.php ├── show.blade.php ├── reports.blade.php └── partials/ └── pdf_export.blade.php Installation StepsCopy Model: cp files/Vehicle.php app/ Copy Controller: cp files/VehicleController.php app/Http/Controllers/ Copy Export: cp files/VehicleReportsExport.php app/Exports/ Copy Views: cp -r files/vehicle resources/views/ Copy Migration: cp files/migrations/2025_09_29_180000_create_vehicle_management_system.php database/migrations/ Run Migration: php artisan migrate Add Routes, Menu, Permissions (see sections above) Features Summary✅ Complete CRUD OperationsCreate vehicles with comprehensive details Edit existing vehicles View individual vehicle details Soft delete vehicles Status management (active/inactive) ✅ Transaction IntegrationAssign vehicles to sales transactions Assign vehicles to purchase transactions Track vehicle usage across all transactions Filter transactions by vehicle ✅ Advanced ReportingVehicle transaction history Sales and purchase summaries by vehicle Date range filtering Excel export functionality Performance metrics ✅ Business FeaturesMulti-business support Permission-based access control Business-level data isolation Soft deletes for data integrity Audit trail (created_by tracking) ✅ User ExperienceDataTables with server-side processing Ajax-powered modals Select2 dropdowns Responsive design Consistent with Ultimate POS UI TestingSample DataINSERT INTO vehicles (vehicle_number, driver_name, vehicle_type, vehicle_model, license_plate, status, business_id, created_by, created_at, updated_at) VALUES ('TRK-001', 'John Smith', 'Truck', 'Ford F-150', 'ABC-1234', 'active', 1, 1, NOW(), NOW()), ('VAN-002', 'Mike Johnson', 'Van', 'Mercedes Sprinter', 'XYZ-5678', 'active', 1, 1, NOW(), NOW()), ('TRK-003', 'David Wilson', 'Heavy Truck', 'Volvo FH16', 'DEF-9012', 'active', 1, 1, NOW(), NOW()); Test Checklist Create new vehicle Edit vehicle details Delete vehicle (soft delete) View vehicle details Assign vehicle to sale Assign vehicle to purchase Filter by vehicle in reports Export vehicle report to Excel Check permissions (view/create/edit/delete) Test multi-business isolation TroubleshootingCommon Issues1. Vehicle not showing in dropdown: Check vehicle status is 'active' Verify business_id matches current business Clear cache: php artisan cache:clear 2. Migration errors: Ensure transactions table exists first Check foreign key constraints Verify business table exists for foreign key reference 3. Permission denied: Verify permissions are seeded Check role assignments Clear permission cache: php artisan permission:cache-reset 4. Table already exists error: If you get "table vehicles already exists" error: # Check current status php artisan migrate:status | grep vehicle # If needed, rollback and start fresh php artisan migrate:rollback --step=1 php artisan migrate 5. Foreign key constraint errors: # Make sure transactions table exists mysql -u username -p database_name -e "SHOW TABLES LIKE 'transactions';" # If vehicle_id column exists but causing issues mysql -u username -p database_name -e "SHOW COLUMNS FROM transactions WHERE Field='vehicle_id';" # Manual cleanup if needed (use with caution) mysql -u username -p database_name -e "ALTER TABLE transactions DROP FOREIGN KEY transactions_vehicle_id_foreign;" mysql -u username -p database_name -e "ALTER TABLE transactions DROP COLUMN vehicle_id;" 6. Migration partially applied: If migration fails midway: # Check what exists php artisan migrate:status | grep vehicle mysql -u username -p database_name -e "SHOW TABLES LIKE 'vehicles';" # Force rollback php artisan migrate:rollback --step=1 --force # Or manual cleanup mysql -u username -p database_name -e "DROP TABLE IF EXISTS vehicles;" mysql -u username -p database_name -e "ALTER TABLE transactions DROP COLUMN IF EXISTS vehicle_id;" # Delete migration record mysql -u username -p database_name -e "DELETE FROM migrations WHERE migration LIKE '%vehicle%';" # Run again php artisan migrate Compatible with: Ultimate POS 6.x+ Laravel Version: 9.x / 10.x
-
Separating Purchase and Sell Report Permissions
This guide explains how to split the combined purchase_n_sell_report.view permission into separate purchase_report.view and sell_report.view permissions in Ultimate POS Laravel application. OverviewCurrently, Ultimate POS uses a single permission purchase_n_sell_report.view to control access to both purchase and sell reports. This guide will show you how to separate these into two distinct permissions for better role-based access control. Files to ModifyThe following files need to be updated: database/seeders/PermissionsTableSeeder.php - Add new permissions app/Http/Middleware/AdminSidebarMenu.php - Update menu visibility logic app/Http/Controllers/ReportController.php - Update permission checks resources/views/role/create.blade.php - Update role creation form resources/views/role/edit.blade.php - Update role editing form Step-by-Step ImplementationStep 1: Add New Permissions to SeederUpdate database/seeders/PermissionsTableSeeder.php: <?php namespace Database\Seeders; use Illuminate\Database\Seeder; use Spatie\Permission\Models\Permission; class PermissionsTableSeeder extends Seeder { public function run() { $data = [ // ... existing permissions ... // Replace the old combined permission with separate ones // ['name' => 'purchase_n_sell_report.view'], // Remove this line ['name' => 'purchase_report.view'], // Add this ['name' => 'sell_report.view'], // Add this // ... rest of existing permissions ... ]; $insert_data = []; $time_stamp = \Carbon::now()->toDateTimeString(); foreach ($data as $d) { $d['guard_name'] = 'web'; $d['created_at'] = $time_stamp; $insert_data[] = $d; } Permission::insert($insert_data); } } Step 2: Update AdminSidebarMenu MiddlewareIn app/Http/Middleware/AdminSidebarMenu.php, update the Reports dropdown section: // Find the Reports dropdown section and update the permission check if ( auth()->user()->can('purchase_report.view') || // New permission auth()->user()->can('sell_report.view') || // New permission auth()->user()->can('contacts_report.view') || auth()->user()->can('stock_report.view') || auth()->user()->can('tax_report.view') || auth()->user()->can('trending_product_report.view') || auth()->user()->can('sales_representative.view') || auth()->user()->can('register_report.view') || auth()->user()->can('expense_report.view') ) { $menu->dropdown( __('report.reports'), function ($sub) use ($enabled_modules, $is_admin) { // ... other report menu items ... // Update the purchase & sell report condition if ((in_array('purchases', $enabled_modules) || in_array('add_sale', $enabled_modules) || in_array('pos_sale', $enabled_modules)) && (auth()->user()->can('purchase_report.view') || auth()->user()->can('sell_report.view'))) { $sub->url( action([\App\Http\Controllers\ReportController::class, 'getPurchaseSell']), __('report.purchase_sell_report'), ['icon' => '', 'active' => request()->segment(2) == 'purchase-sell'] ); } // ... rest of menu items ... }, ['icon' => '...', 'id' => 'tour_step8'] )->order(55); } Step 3: Update ReportControllerIn app/Http/Controllers/ReportController.php, update the permission checks: /** * Shows product report of a business * * @return \Illuminate\Http\Response */ public function getPurchaseSell(Request $request) { // Update permission check to allow either permission if (!auth()->user()->can('purchase_report.view') && !auth()->user()->can('sell_report.view')) { abort(403, 'Unauthorized action.'); } $business_id = $request->session()->get('user.business_id'); //Return the details in ajax call if ($request->ajax()) { $start_date = $request->get('start_date'); $end_date = $request->get('end_date'); $location_id = $request->get('location_id'); $purchase_details = []; $sell_details = []; $transaction_totals = []; // Only fetch purchase data if user has purchase report permission if (auth()->user()->can('purchase_report.view')) { $purchase_details = $this->transactionUtil->getPurchaseTotals($business_id, $start_date, $end_date, $location_id); } // Only fetch sell data if user has sell report permission if (auth()->user()->can('sell_report.view')) { $sell_details = $this->transactionUtil->getSellTotals( $business_id, $start_date, $end_date, $location_id ); } // Only fetch transaction totals if user has either permission if (auth()->user()->can('purchase_report.view') || auth()->user()->can('sell_report.view')) { $transaction_types = [ 'purchase_return', 'sell_return', ]; $transaction_totals = $this->transactionUtil->getTransactionTotals( $business_id, $transaction_types, $start_date, $end_date, $location_id ); } $total_purchase_return_inc_tax = $transaction_totals['total_purchase_return_inc_tax'] ?? 0; $total_sell_return_inc_tax = $transaction_totals['total_sell_return_inc_tax'] ?? 0; $difference = [ 'total' => ($sell_details['total_sell_inc_tax'] ?? 0) - $total_sell_return_inc_tax - (($purchase_details['total_purchase_inc_tax'] ?? 0) - $total_purchase_return_inc_tax), 'due' => ($sell_details['invoice_due'] ?? 0) - ($purchase_details['purchase_due'] ?? 0), ]; return ['purchase' => $purchase_details, 'sell' => $sell_details, 'total_purchase_return' => $total_purchase_return_inc_tax, 'total_sell_return' => $total_sell_return_inc_tax, 'difference' => $difference, ]; } $business_locations = BusinessLocation::forDropdown($business_id, true); return view('report.purchase_sell') ->with(compact('business_locations')); } // Add similar updates to other report methods that used the old permission: /** * Shows product purchase report */ public function getproductPurchaseReport(Request $request) { if (!auth()->user()->can('purchase_report.view')) { abort(403, 'Unauthorized action.'); } // ... rest of the method } /** * Shows product sell report */ public function getproductSellReport(Request $request) { if (!auth()->user()->can('sell_report.view')) { abort(403, 'Unauthorized action.'); } // ... rest of the method } /** * Shows purchase payment report */ public function purchasePaymentReport(Request $request) { if (!auth()->user()->can('purchase_report.view')) { abort(403, 'Unauthorized action.'); } // ... rest of the method } /** * Shows sell payment report */ public function sellPaymentReport(Request $request) { if (!auth()->user()->can('sell_report.view')) { abort(403, 'Unauthorized action.'); } // ... rest of the method } /** * Shows items report */ public function itemsReport() { if (!auth()->user()->can('purchase_report.view') && !auth()->user()->can('sell_report.view')) { abort(403, 'Unauthorized action.'); } // ... rest of the method } Step 4: Update Role Creation FormIn resources/views/role/create.blade.php, replace the combined permission checkbox: <div class="row check_group"> <div class="col-md-1"> <h4>@lang( 'role.report' )</h4> </div> <div class="col-md-2"> <div class="checkbox"> <label> <input type="checkbox" class="check_all input-icheck" > {{ __( 'role.select_all' ) }} </label> </div> </div> <div class="col-md-9"> {{-- Remove this block: @if(in_array('purchases', $enabled_modules) || in_array('add_sale', $enabled_modules) || in_array('pos_sale', $enabled_modules)) <div class="col-md-12"> <div class="checkbox"> <label> {!! Form::checkbox('permissions[]', 'purchase_n_sell_report.view', false, [ 'class' => 'input-icheck']); !!} {{ __( 'role.purchase_n_sell_report.view' ) }} </label> </div> </div> @endif --}} {{-- Add these separate checkboxes: --}} @if(in_array('purchases', $enabled_modules)) <div class="col-md-12"> <div class="checkbox"> <label> {!! Form::checkbox('permissions[]', 'purchase_report.view', false, [ 'class' => 'input-icheck']); !!} {{ __( 'role.purchase_report.view' ) }} </label> </div> </div> @endif @if(in_array('add_sale', $enabled_modules) || in_array('pos_sale', $enabled_modules)) <div class="col-md-12"> <div class="checkbox"> <label> {!! Form::checkbox('permissions[]', 'sell_report.view', false, [ 'class' => 'input-icheck']); !!} {{ __( 'role.sell_report.view' ) }} </label> </div> </div> @endif {{-- ... rest of existing report permissions ... --}} </div> </div> Step 5: Update Role Edit FormIn resources/views/role/edit.blade.php, make the same changes as in the create form: <div class="row check_group"> <div class="col-md-1"> <h4>@lang( 'role.report' )</h4> </div> <div class="col-md-2"> <div class="checkbox"> <label> <input type="checkbox" class="check_all input-icheck" > {{ __( 'role.select_all' ) }} </label> </div> </div> <div class="col-md-9"> {{-- Remove this block: @if(in_array('purchases', $enabled_modules) || in_array('add_sale', $enabled_modules) || in_array('pos_sale', $enabled_modules)) <div class="col-md-12"> <div class="checkbox"> <label> {!! Form::checkbox('permissions[]', 'purchase_n_sell_report.view', in_array('purchase_n_sell_report.view', $role_permissions), [ 'class' => 'input-icheck']); !!} {{ __( 'role.purchase_n_sell_report.view' ) }} </label> </div> </div> @endif --}} {{-- Add these separate checkboxes: --}} @if(in_array('purchases', $enabled_modules)) <div class="col-md-12"> <div class="checkbox"> <label> {!! Form::checkbox('permissions[]', 'purchase_report.view', in_array('purchase_report.view', $role_permissions), [ 'class' => 'input-icheck']); !!} {{ __( 'role.purchase_report.view' ) }} </label> </div> </div> @endif @if(in_array('add_sale', $enabled_modules) || in_array('pos_sale', $enabled_modules)) <div class="col-md-12"> <div class="checkbox"> <label> {!! Form::checkbox('permissions[]', 'sell_report.view', in_array('sell_report.view', $role_permissions), [ 'class' => 'input-icheck']); !!} {{ __( 'role.sell_report.view' ) }} </label> </div> </div> @endif {{-- ... rest of existing report permissions ... --}} </div> </div> Database MigrationCreate a migration to add the new permissions and remove the old one: php artisan make:migration update_purchase_sell_report_permissions <?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; use Spatie\Permission\Models\Permission; use Spatie\Permission\Models\Role; class UpdatePurchaseSellReportPermissions extends Migration { public function up() { // Create new permissions Permission::create(['name' => 'purchase_report.view', 'guard_name' => 'web']); Permission::create(['name' => 'sell_report.view', 'guard_name' => 'web']); // Find roles that have the old permission and give them both new permissions $oldPermission = Permission::where('name', 'purchase_n_sell_report.view')->first(); if ($oldPermission) { $roles = $oldPermission->roles; foreach ($roles as $role) { $role->givePermissionTo(['purchase_report.view', 'sell_report.view']); } // Remove the old permission $oldPermission->delete(); } } public function down() { // Recreate the old permission $oldPermission = Permission::create(['name' => 'purchase_n_sell_report.view', 'guard_name' => 'web']); // Find roles that have the new permissions and give them the old one $purchasePermission = Permission::where('name', 'purchase_report.view')->first(); $sellPermission = Permission::where('name', 'sell_report.view')->first(); $rolesWithPurchase = $purchasePermission ? $purchasePermission->roles : collect(); $rolesWithSell = $sellPermission ? $sellPermission->roles : collect(); $allRoles = $rolesWithPurchase->merge($rolesWithSell)->unique('id'); foreach ($allRoles as $role) { $role->givePermissionTo('purchase_n_sell_report.view'); } // Remove new permissions if ($purchasePermission) { $purchasePermission->delete(); } if ($sellPermission) { $sellPermission->delete(); } } } Language File UpdatesAdd the new permission labels to your language files (e.g., resources/lang/en/role.php): 'purchase_report.view' => 'View Purchase Reports', 'sell_report.view' => 'View Sell Reports', TestingAfter implementing these changes: Run the migration: php artisan migrate Clear application cache: php artisan cache:clear Test role creation and editing forms Test menu visibility with different permission combinations Test report access with the new permissions BenefitsThis separation provides: Granular Control: Assign purchase and sell report permissions independently Better Security: Users only see reports they need access to Flexible Roles: Create roles for purchase-only or sales-only staff Maintainable Code: Clearer permission structure for future development NotesExisting roles with the old purchase_n_sell_report.view permission will automatically get both new permissions through the migration Consider updating any custom views or components that might reference the old permission name Update any API endpoints that might check for the old permission Test thoroughly in a development environment before deploying to production Summary of ChangesNew Permissions: purchase_report.view and sell_report.view Removed Permission: purchase_n_sell_report.view Updated Files: 5 core files modified Migration: Automatic conversion of existing roles Backward Compatibility: Migration handles existing role assignments Step 6: Update Routes with Middleware ProtectionUpdate the routes in web.php to include proper middleware protection for the new permissions: //Reports... (Update existing report routes) Route::get('/reports/purchase-report', [ReportController::class, 'purchaseReport']) ->middleware('can:purchase_report.view'); Route::get('/reports/sale-report', [ReportController::class, 'saleReport']) ->middleware('can:sell_report.view'); // Combined report - requires either permission Route::get('/reports/purchase-sell', [ReportController::class, 'getPurchaseSell']) ->middleware('can:purchase_report.view,sell_report.view'); // Product-specific reports Route::get('/reports/product-purchase-report', [ReportController::class, 'getproductPurchaseReport']) ->middleware('can:purchase_report.view'); Route::get('/reports/product-sell-report', [ReportController::class, 'getproductSellReport']) ->middleware('can:sell_report.view'); Route::get('/reports/product-sell-report-with-purchase', [ReportController::class, 'getproductSellReportWithPurchase']) ->middleware('can:sell_report.view'); Route::get('/reports/product-sell-grouped-report', [ReportController::class, 'getproductSellGroupedReport']) ->middleware('can:sell_report.view'); Route::get('/reports/product-sell-grouped-by', [ReportController::class, 'productSellReportBy']) ->middleware('can:sell_report.view'); // Payment reports Route::get('/reports/purchase-payment-report', [ReportController::class, 'purchasePaymentReport']) ->middleware('can:purchase_report.view'); Route::get('/reports/sell-payment-report', [ReportController::class, 'sellPaymentReport']) ->middleware('can:sell_report.view'); // Items report - requires either permission Route::get('/reports/items-report', [ReportController::class, 'itemsReport']) ->middleware('can:purchase_report.view,sell_report.view'); Alternative Approach: Route GroupsYou can also organize the routes using groups for better maintainability: // Purchase Reports Group Route::middleware(['can:purchase_report.view'])->group(function () { Route::get('/reports/purchase-report', [ReportController::class, 'purchaseReport']); Route::get('/reports/product-purchase-report', [ReportController::class, 'getproductPurchaseReport']); Route::get('/reports/purchase-payment-report', [ReportController::class, 'purchasePaymentReport']); }); // Sell Reports Group Route::middleware(['can:sell_report.view'])->group(function () { Route::get('/reports/sale-report', [ReportController::class, 'saleReport']); Route::get('/reports/product-sell-report', [ReportController::class, 'getproductSellReport']); Route::get('/reports/product-sell-report-with-purchase', [ReportController::class, 'getproductSellReportWithPurchase']); Route::get('/reports/product-sell-grouped-report', [ReportController::class, 'getproductSellGroupedReport']); Route::get('/reports/product-sell-grouped-by', [ReportController::class, 'productSellReportBy']); Route::get('/reports/sell-payment-report', [ReportController::class, 'sellPaymentReport']); }); // Combined Reports (require either permission) Route::middleware(['can:purchase_report.view,sell_report.view'])->group(function () { Route::get('/reports/purchase-sell', [ReportController::class, 'getPurchaseSell']); Route::get('/reports/items-report', [ReportController::class, 'itemsReport']); }); Additional ConsiderationsView UpdatesYou may also need to update the report view templates to conditionally show purchase or sell sections based on user permissions: <!-- In report.purchase_sell view --> @if(auth()->user()->can('purchase_report.view')) <!-- Purchase report section --> <div class="purchase-section"> <!-- Purchase data display --> </div> @endif @if(auth()->user()->can('sell_report.view')) <!-- Sell report section --> <div class="sell-section"> <!-- Sell data display --> </div> @endif @if(!auth()->user()->can('purchase_report.view') && !auth()->user()->can('sell_report.view')) <div class="alert alert-warning"> {{ __('lang_v1.no_permission_for_this_report') }} </div> @endif This comprehensive guide should help you successfully separate the purchase and sell report permissions in Ultimate POS while maintaining backward compatibility and providing better granular access control.
-
Fix Duplicate Records and Table Bloat
This guide addresses a common issue in Ultimate POS where the transaction_sell_lines_purchase_lines table develops duplicate records and becomes bloated, consuming excessive disk space. This typically happens due to data synchronization issues or improper cleanup processes. Table showing excessive size before optimization Table size successfully reduced after applying the fix SymptomsLarge table size (several GB) with relatively few records Duplicate entries in transaction data Poor database performance Disk space issues PrerequisitesBackup Required Always create a database backup before performing these operations! Database administrator access phpMyAdmin or MySQL command line access Maintenance window (operations will lock the table) SolutionStep 1: Verify the IssueFirst, check if you have duplicate records: SELECT stock_adjustment_line_id, purchase_line_id, quantity, qty_returned, created_at, updated_at, COUNT(*) as duplicate_count FROM transaction_sell_lines_purchase_lines GROUP BY stock_adjustment_line_id, purchase_line_id, quantity, qty_returned, created_at, updated_at HAVING COUNT(*) > 1; Check current table size: SELECT table_name, ROUND(((data_length + index_length) / 1024 / 1024), 2) AS "Size in MB" FROM information_schema.tables WHERE table_name = 'transaction_sell_lines_purchase_lines' AND table_schema = DATABASE(); Step 2: Remove Duplicate RecordsTable Lock Warning This operation will lock the table during execution. Perform during low-traffic periods. DELETE FROM transaction_sell_lines_purchase_lines WHERE id NOT IN ( SELECT min_id FROM ( SELECT MIN(id) as min_id FROM transaction_sell_lines_purchase_lines GROUP BY stock_adjustment_line_id, purchase_line_id, quantity, qty_returned, created_at, updated_at ) as keeper_ids ); This query: Keeps the record with the lowest id for each unique combination Removes all duplicate records Preserves data integrity by maintaining foreign key relationships Step 3: Force Table Rebuild (Fix Bloat)After removing duplicates, the table may still appear large because MySQL doesn't automatically reclaim space. Force a complete rebuild: -- Create new table with same structure CREATE TABLE transaction_sell_lines_purchase_lines_new LIKE transaction_sell_lines_purchase_lines; -- Copy all remaining data INSERT INTO transaction_sell_lines_purchase_lines_new SELECT * FROM transaction_sell_lines_purchase_lines; -- Replace the old table DROP TABLE transaction_sell_lines_purchase_lines; RENAME TABLE transaction_sell_lines_purchase_lines_new TO transaction_sell_lines_purchase_lines; Step 4: VerificationVerify duplicates are gone: SELECT COUNT(*) as duplicate_groups FROM ( SELECT stock_adjustment_line_id, purchase_line_id, quantity, qty_returned, created_at, updated_at FROM transaction_sell_lines_purchase_lines GROUP BY stock_adjustment_line_id, purchase_line_id, quantity, qty_returned, created_at, updated_at HAVING COUNT(*) > 1 ) as dup_check; Check final table size: SELECT table_name, ROUND(((data_length + index_length) / 1024 / 1024), 2) AS "Size in MB" FROM information_schema.tables WHERE table_name = 'transaction_sell_lines_purchase_lines' AND table_schema = DATABASE(); Check record count: SELECT COUNT(*) as total_records FROM transaction_sell_lines_purchase_lines; Expected ResultsDuplicate groups: 0 Table size: Significantly reduced (should be appropriate for the number of records) Record count: Only unique records remain Performance: Improved query speeds TroubleshootingphpMyAdmin LIMIT ErrorIf you encounter "LIMIT 0, 25" errors when creating temporary tables, this is due to phpMyAdmin automatically adding pagination. Use the single-query approach provided in Step 2 instead. InnoDB Optimize Not SupportedIf you see "Table does not support optimize, doing recreate + analyze instead" - this is normal for InnoDB tables and should fix the bloat issue. Large Table Size PersistsIf the table remains large after optimization, use the force rebuild method in Step 3. This completely recreates the table structure and eliminates any remaining bloat. PreventionTo prevent this issue from recurring: Regular maintenance: Schedule periodic duplicate checks Application review: Investigate why duplicates are being created Monitoring: Set up alerts for unusual table growth Backup strategy: Ensure regular backups before maintenance Additional NotesThis procedure is specifically tested with Ultimate POS systems The table structure includes fields: id, sell_line_id, stock_adjustment_line_id, purchase_line_id, quantity, qty_returned, created_at, updated_at All operations should be performed during maintenance windows Consider upgrading Ultimate POS if this is a recurring issue
-
Adding Price Groups to Product List And Purchase
This guide covers how to implement dynamic selling price groups in the Ultimate POS product list and integrate them with the purchase system. OverviewThe price groups feature allows you to: Display multiple selling price groups as columns in the product list Manage group-specific pricing for products Update price group values from purchase operations 📦 Download Starter Filesprice-groups-in-products-purchase.zip PrerequisitesUltimate POS Laravel application Basic understanding of Laravel, Blade templates, and DataTables Database with selling_price_groups and variation_group_prices tables Step 1: Display Price Groups in Product List1.1 Modify the Product List View TemplateFile: resources/views/product/partials/product_list.blade.php Add the dynamic price group headers after the existing table headers: @foreach ($price_groups as $price_group_key => $price_group) @php $colspan++; @endphp <th>{{ $price_group->name }}</th> @endforeach Location: Insert this code block in the table header section, typically after the standard product columns (SKU, Category, Brand, etc.). Here is the complete code for product_list.blade.php: @php $colspan = 15; $custom_labels = json_decode(session('business.custom_labels'), true); @endphp <table class="table table-bordered table-striped ajax_view hide-footer" id="product_table"> <thead> <tr> <th><input type="checkbox" id="select-all-row" data-table-id="product_table"></th> <th>{{__('lang_v1.product_image')}}</th> <th>@lang('messages.action')</th> <th>@lang('sale.product')</th> @can('view_purchase_price') @php $colspan++; @endphp <th>@lang('lang_v1.unit_perchase_price')</th> @endcan @can('access_default_selling_price') @php $colspan++; @endphp <th>@lang('lang_v1.selling_price')</th> @endcan <th>@lang('report.current_stock')</th> <th>@lang('product.product_type')</th> @foreach ($price_groups as $price_group_key => $price_group) @php $colspan++; @endphp <th>{{ $price_group->name }}</th> @endforeach <th>@lang('product.category')</th> <th>@lang('product.brand')</th> <th>@lang('product.tax')</th> <th>@lang('product.sku')</th> <th> @lang('purchase.business_location') @show_tooltip(__('lang_v1.product_business_location_tooltip')) </th> <th id="cf_1">{{ $custom_labels['product']['custom_field_1'] ?? '' }}</th> <th id="cf_2">{{ $custom_labels['product']['custom_field_2'] ?? '' }}</th> <th id="cf_3">{{ $custom_labels['product']['custom_field_3'] ?? '' }}</th> <th id="cf_4">{{ $custom_labels['product']['custom_field_4'] ?? '' }}</th> <th id="cf_5">{{ $custom_labels['product']['custom_field_5'] ?? '' }}</th> <th id="cf_6">{{ $custom_labels['product']['custom_field_6'] ?? '' }}</th> <th id="cf_7">{{ $custom_labels['product']['custom_field_7'] ?? '' }}</th> </tr> </thead> <tfoot> <tr> <td colspan="{{$colspan}}"> <div style="display: flex; width: 100%;"> @can('product.delete') {!! Form::open(['url' => action([\App\Http\Controllers\ProductController::class, 'massDestroy']), 'method' => 'post', 'id' => 'mass_delete_form' ]) !!} {!! Form::hidden('selected_rows', null, ['id' => 'selected_rows']); !!} {!! Form::submit(__('lang_v1.delete_selected'), array('class' => 'tw-dw-btn tw-dw-btn-outline tw-dw-btn-xs tw-dw-btn-error', 'id' => 'delete-selected')) !!} {!! Form::close() !!} @endcan @can('product.update') @if(config('constants.enable_product_bulk_edit')) {!! Form::open(['url' => action([\App\Http\Controllers\ProductController::class, 'bulkEdit']), 'method' => 'post', 'id' => 'bulk_edit_form' ]) !!} {!! Form::hidden('selected_products', null, ['id' => 'selected_products_for_edit']); !!} <button type="submit" class="tw-dw-btn tw-dw-btn-xs tw-dw-btn-outline tw-dw-btn-primary" id="edit-selected"> <i class="fa fa-edit"></i>{{__('lang_v1.bulk_edit')}}</button> {!! Form::close() !!} @endif <button type="button" class="tw-dw-btn tw-dw-btn-xs tw-dw-btn-outline tw-dw-btn-accent update_product_location" data-type="add">@lang('lang_v1.add_to_location')</button> <button type="button" class="tw-dw-btn tw-dw-btn-xs tw-dw-btn-outline tw-dw-btn-neutral update_product_location" data-type="remove">@lang('lang_v1.remove_from_location')</button> @endcan {!! Form::open(['url' => action([\App\Http\Controllers\ProductController::class, 'massDeactivate']), 'method' => 'post', 'id' => 'mass_deactivate_form' ]) !!} {!! Form::hidden('selected_products', null, ['id' => 'selected_products']); !!} {!! Form::submit(__('lang_v1.deactivate_selected'), array('class' => 'tw-dw-btn tw-dw-btn-xs tw-dw-btn-outline tw-dw-btn-warning', 'id' => 'deactivate-selected')) !!} {!! Form::close() !!} @show_tooltip(__('lang_v1.deactive_product_tooltip')) @if($is_woocommerce) <button type="button" class="tw-dw-btn tw-dw-btn-xs tw-dw-btn-outline tw-dw-btn-warning toggle_woocomerce_sync"> @lang('lang_v1.woocommerce_sync') </button> @endif </div> </td> </tr> </tfoot> </table> 1.2 Update DataTable Column ConfigurationFile: resources/views/product/index.blade.php Add the dynamic DataTable columns configuration: @foreach ($price_groups as $price_group) { data: 'group_price{{$price_group->id}}', name: 'group_price{{$price_group->id}}', searchable: false, orderable: false, width: '80px' }, @endforeach Location: Insert this within the DataTable columns array configuration, after the existing column definitions. Here is the complete code for the new DataTable columns: product_table = $('#product_table').DataTable({ processing: true, serverSide: true, fixedHeader: false, aaSorting: [ [3, 'asc'] ], scrollY: "75vh", scrollX: true, scrollCollapse: true, "ajax": { "url": "/products", "data": function(d) { d.type = $('#product_list_filter_type').val(); d.category_id = $('#product_list_filter_category_id').val(); d.brand_id = $('#product_list_filter_brand_id').val(); d.unit_id = $('#product_list_filter_unit_id').val(); d.tax_id = $('#product_list_filter_tax_id').val(); d.active_state = $('#active_state').val(); d.not_for_selling = $('#not_for_selling').is(':checked'); d.location_id = $('#location_id').val(); if ($('#repair_model_id').length == 1) { d.repair_model_id = $('#repair_model_id').val(); } if ($('#woocommerce_enabled').length == 1 && $('#woocommerce_enabled').is(':checked')) { d.woocommerce_enabled = 1; } d = __datatable_ajax_callback(d); } }, columnDefs: [{ "targets": [0, 1, 2], "orderable": false, "searchable": false }], columns: [{ data: 'mass_delete' }, { data: 'image', name: 'products.image', width: '40px' }, { data: 'action', name: 'action', width: '60px' }, { data: 'product', name: 'products.name', width: '160px' }, @can('view_purchase_price') { data: 'purchase_price', name: 'max_purchase_price', searchable: false, width: '60px' }, @endcan @can('access_default_selling_price') { data: 'selling_price', name: 'max_price', searchable: false, width: '60px' }, @endcan { data: 'current_stock', searchable: false, width: '80px' }, { data: 'type', name: 'products.type' }, @foreach ($price_groups as $price_group) { data: 'group_price{{$price_group->id}}', name: 'group_price{{$price_group->id}}', searchable: false, orderable: false, width: '80px' }, @endforeach { data: 'category', name: 'c1.name' }, { data: 'brand', name: 'brands.name' }, { data: 'tax', name: 'tax_rates.name', searchable: false }, { data: 'sku', name: 'products.sku' }, { data: 'product_locations', name: 'product_locations' }, { data: 'product_custom_field1', name: 'products.product_custom_field1', visible: $('#cf_1').text().length > 0 }, { data: 'product_custom_field2', name: 'products.product_custom_field2', visible: $('#cf_2').text().length > 0 }, { data: 'product_custom_field3', name: 'products.product_custom_field3', visible: $('#cf_3').text().length > 0 }, { data: 'product_custom_field4', name: 'products.product_custom_field4', visible: $('#cf_4').text().length > 0 }, { data: 'product_custom_field5', name: 'products.product_custom_field5', visible: $('#cf_5').text().length > 0 }, { data: 'product_custom_field6', name: 'products.product_custom_field6', visible: $('#cf_6').text().length > 0 }, { data: 'product_custom_field7', name: 'products.product_custom_field7', visible: $('#cf_7').text().length > 0 }, ], createdRow: function(row, data, dataIndex) { if ($('input#is_rack_enabled').val() == 1) { var target_col = 0; @can('product.delete') target_col = 1; @endcan $(row).find('td:eq(' + target_col + ') div').prepend( '<i style="margin:auto;" class="fa fa-plus-circle text-success cursor-pointer no-print rack-details" title="' + LANG.details + '"></i> '); } $(row).find('td:eq(0)').attr('class', 'selectable_td'); }, fnDrawCallback: function(oSettings) { __currency_convert_recursively($('#product_table')); }, }); 1.3 Enhanced Controller ImplementationFile: app/Http/Controllers/ProductController.php Key Changes in the index() method:1. Add Price Groups QueryBefore handling AJAX requests (if (request()->ajax()) {), add: // Get price groups for the business $price_groups = SellingPriceGroup::where('business_id', $business_id)->get(); 2. Dynamic Price Group ColumnsAdd this code after the existing column definitions: Original Pattern: return Datatables::of($products) // ... column definitions ... ->rawColumns(['action', 'image', 'mass_delete', 'product', 'selling_price', 'purchase_price', 'category', 'current_stock']) ->make(true); Enhanced Pattern with Price Groups: // Define base raw columns $raw_columns = [ 'action', 'image', 'sku', 'mass_delete', 'product', 'selling_price', 'purchase_price', 'category', 'current_stock', ]; // Create DataTables instance $datatables = Datatables::of($products) ->addColumn('product_locations', function ($row) { return $row->product_locations->implode('name', ', '); }) // ... other existing column definitions ... ; // Add dynamic price group columns foreach ($price_groups as $price_group) { $column_name = 'group_price' . $price_group->id; $raw_columns[] = $column_name; $datatables->editColumn($column_name, function ($row) use ($price_group) { $due_html = ''; $group_price = (float) 0; if (!empty($price_group->id)) { $variation_group_price = VariationGroupPrice::where('variation_id', $row->variation_id) ->where('price_group_id', $price_group->id) ->first(); if (!empty($variation_group_price)) { $group_price = (float) $variation_group_price->price_inc_tax; } } $due_html .= '<span class="group_price" data-orig-value="' . $group_price . '">' . $this->productUtil->num_f($group_price, true) . '</span>'; return $due_html; }); } // Apply raw columns and return return $datatables->setRowAttr([ 'data-href' => function ($row) { if (auth()->user()->can('product.view')) { return action([\App\Http\Controllers\ProductController::class, 'view'], [$row->id]); } else { return ''; } }, ]) ->rawColumns($raw_columns) ->make(true); Complete index method: Add this import at the top of ProductController.php with other imports: use App\VariationGroupPrice; public function index() { if (!auth()->user()->can('product.view') && !auth()->user()->can('product.create')) { abort(403, 'Unauthorized action.'); } $business_id = request()->session()->get('user.business_id'); $selling_price_group_count = SellingPriceGroup::countSellingPriceGroups($business_id); $is_woocommerce = $this->moduleUtil->isModuleInstalled('Woocommerce'); // Get price groups for the business $price_groups = SellingPriceGroup::where('business_id', $business_id)->get(); if (request()->ajax()) { // Filter by location $location_id = request()->get('location_id', null); $permitted_locations = auth()->user()->permitted_locations(); $query = Product::with(['media']) ->leftJoin('brands', 'products.brand_id', '=', 'brands.id') ->join('units', 'products.unit_id', '=', 'units.id') ->leftJoin('categories as c1', 'products.category_id', '=', 'c1.id') ->leftJoin('categories as c2', 'products.sub_category_id', '=', 'c2.id') ->leftJoin('tax_rates', 'products.tax', '=', 'tax_rates.id') ->join('variations as v', 'v.product_id', '=', 'products.id') ->leftJoin('variation_location_details as vld', function ($join) use ($permitted_locations) { $join->on('vld.variation_id', '=', 'v.id'); if ($permitted_locations != 'all') { $join->whereIn('vld.location_id', $permitted_locations); } }) ->whereNull('v.deleted_at') ->where('products.business_id', $business_id) ->where('products.type', '!=', 'modifier'); if (!empty($location_id) && $location_id != 'none') { if ($permitted_locations == 'all' || in_array($location_id, $permitted_locations)) { $query->whereHas('product_locations', function ($query) use ($location_id) { $query->where('product_locations.location_id', '=', $location_id); }); } } elseif ($location_id == 'none') { $query->doesntHave('product_locations'); } else { if ($permitted_locations != 'all') { $query->whereHas('product_locations', function ($query) use ($permitted_locations) { $query->whereIn('product_locations.location_id', $permitted_locations); }); } else { $query->with('product_locations'); } } $products = $query->select( 'products.id', 'v.id as variation_id', // Critical for price groups 'products.name as product', 'products.type', 'c1.name as category', 'c2.name as sub_category', 'units.actual_name as unit', 'brands.name as brand', 'tax_rates.name as tax', 'products.sku', 'products.image', 'products.enable_stock', 'products.is_inactive', 'products.not_for_selling', 'products.product_custom_field1', 'products.product_custom_field2', 'products.product_custom_field3', 'products.product_custom_field4', 'products.product_custom_field5', 'products.product_custom_field6', 'products.product_custom_field7', 'products.product_custom_field8', 'products.product_custom_field9', 'products.product_custom_field10', 'products.product_custom_field11', 'products.product_custom_field12', 'products.product_custom_field13', 'products.product_custom_field14', 'products.product_custom_field15', 'products.product_custom_field16', 'products.product_custom_field17', 'products.product_custom_field18', 'products.product_custom_field19', 'products.product_custom_field20', 'products.alert_quantity', DB::raw('SUM(vld.qty_available) as current_stock'), DB::raw('MAX(v.sell_price_inc_tax) as max_price'), DB::raw('MIN(v.sell_price_inc_tax) as min_price'), DB::raw('MAX(v.dpp_inc_tax) as max_purchase_price'), DB::raw('MIN(v.dpp_inc_tax) as min_purchase_price') ); // If WooCommerce is enabled, add field to query if ($is_woocommerce) { $products->addSelect('woocommerce_disable_sync'); } $products->groupBy('products.id'); // Apply filters $type = request()->get('type', null); if (!empty($type)) { $products->where('products.type', $type); } $category_id = request()->get('category_id', null); if (!empty($category_id)) { $products->where('products.category_id', $category_id); } $brand_id = request()->get('brand_id', null); if (!empty($brand_id)) { $products->where('products.brand_id', $brand_id); } $unit_id = request()->get('unit_id', null); if (!empty($unit_id)) { $products->where('products.unit_id', $unit_id); } $tax_id = request()->get('tax_id', null); if (!empty($tax_id)) { $products->where('products.tax', $tax_id); } $active_state = request()->get('active_state', null); if ($active_state == 'active') { $products->Active(); } if ($active_state == 'inactive') { $products->Inactive(); } $not_for_selling = request()->get('not_for_selling', null); if ($not_for_selling == 'true') { $products->ProductNotForSales(); } $woocommerce_enabled = request()->get('woocommerce_enabled', 0); if ($woocommerce_enabled == 1) { $products->where('products.woocommerce_disable_sync', 0); } if (!empty(request()->get('repair_model_id'))) { $products->where('products.repair_model_id', request()->get('repair_model_id')); } // Create DataTables instance $datatables = Datatables::of($products) ->addColumn('product_locations', function ($row) { return $row->product_locations->implode('name', ', '); }) ->editColumn('category', '{{$category}} @if(!empty($sub_category))<br/> -- {{$sub_category}}@endif') ->addColumn('action', function ($row) use ($selling_price_group_count) { $html = '<div class="btn-group"><button type="button" class="tw-dw-btn tw-dw-btn-xs tw-dw-btn-outline tw-dw-btn-info tw-w-max dropdown-toggle" data-toggle="dropdown" aria-expanded="false">'.__('messages.actions').'<span class="caret"></span><span class="sr-only">Toggle Dropdown</span></button><ul class="dropdown-menu dropdown-menu-left" role="menu"><li><a href="'.action([\App\Http\Controllers\LabelsController::class, 'show']).'?product_id='.$row->id.'" data-toggle="tooltip" title="'.__('lang_v1.label_help').'"><i class="fa fa-barcode"></i> '.__('barcode.labels').'</a></li>'; if (auth()->user()->can('product.view')) { $html .= '<li><a href="'.action([\App\Http\Controllers\ProductController::class, 'view'], [$row->id]).'" class="view-product"><i class="fa fa-eye"></i> '.__('messages.view').'</a></li>'; } if (auth()->user()->can('product.update')) { $html .= '<li><a href="'.action([\App\Http\Controllers\ProductController::class, 'edit'], [$row->id]).'"><i class="glyphicon glyphicon-edit"></i> '.__('messages.edit').'</a></li>'; } if (auth()->user()->can('product.delete')) { $html .= '<li><a href="'.action([\App\Http\Controllers\ProductController::class, 'destroy'], [$row->id]).'" class="delete-product"><i class="fa fa-trash"></i> '.__('messages.delete').'</a></li>'; } if ($row->is_inactive == 1) { $html .= '<li><a href="'.action([\App\Http\Controllers\ProductController::class, 'activate'], [$row->id]).'" class="activate-product"><i class="fas fa-check-circle"></i> '.__('lang_v1.reactivate').'</a></li>'; } $html .= '<li class="divider"></li>'; if ($row->enable_stock == 1 && auth()->user()->can('product.opening_stock')) { $html .= '<li><a href="#" data-href="'.action([\App\Http\Controllers\OpeningStockController::class, 'add'], ['product_id' => $row->id]).'" class="add-opening-stock"><i class="fa fa-database"></i> '.__('lang_v1.add_edit_opening_stock').'</a></li>'; } if (auth()->user()->can('product.view')) { $html .= '<li><a href="'.action([\App\Http\Controllers\ProductController::class, 'productStockHistory'], [$row->id]).'"><i class="fas fa-history"></i> '.__('lang_v1.product_stock_history').'</a></li>'; } if (auth()->user()->can('product.create')) { if ($selling_price_group_count > 0) { $html .= '<li><a href="'.action([\App\Http\Controllers\ProductController::class, 'addSellingPrices'], [$row->id]).'"><i class="fas fa-money-bill-alt"></i> '.__('lang_v1.add_selling_price_group_prices').'</a></li>'; } $html .= '<li><a href="'.action([\App\Http\Controllers\ProductController::class, 'create'], ['d' => $row->id]).'"><i class="fa fa-copy"></i> '.__('lang_v1.duplicate_product').'</a></li>'; } if (!empty($row->media->first())) { $html .= '<li><a href="'.$row->media->first()->display_url.'" download="'.$row->media->first()->display_name.'"><i class="fas fa-download"></i> '.__('lang_v1.product_brochure').'</a></li>'; } $html .= '</ul></div>'; return $html; }) ->editColumn('product', function ($row) use ($is_woocommerce) { $product = $row->is_inactive == 1 ? e($row->product).' <span class="label bg-gray">'.__('lang_v1.inactive').'</span>' : e($row->product); $product = $row->not_for_selling == 1 ? $product.' <span class="label bg-gray">'.__('lang_v1.not_for_selling').'</span>' : $product; if ($is_woocommerce && !$row->woocommerce_disable_sync) { $product = $product.'<br><i class="fab fa-wordpress"></i>'; } return $product; }) ->editColumn('image', function ($row) { return '<div style="display: flex;"><img src="'.$row->image_url.'" alt="Product image" class="product-thumbnail-small"></div>'; }) ->editColumn('type', '@lang("lang_v1." . $type)') ->addColumn('mass_delete', function ($row) { return '<input type="checkbox" class="row-select" value="'.$row->id.'">'; }) ->editColumn('current_stock', function ($row) { if ($row->enable_stock) { $stock = $this->productUtil->num_f($row->current_stock, false, null, true); return $stock.' '.$row->unit; } else { return '--'; } }) ->addColumn('purchase_price', '<div style="white-space: nowrap;">@format_currency($min_purchase_price) @if($max_purchase_price != $min_purchase_price && $type == "variable") - @format_currency($max_purchase_price)@endif </div>') ->addColumn('selling_price', '<div style="white-space: nowrap;">@format_currency($min_price) @if($max_price != $min_price && $type == "variable") - @format_currency($max_price)@endif </div>') ->filterColumn('products.sku', function ($query, $keyword) { $query->whereHas('variations', function ($q) use ($keyword) { $q->where('sub_sku', 'like', "%{$keyword}%"); }) ->orWhere('products.sku', 'like', "%{$keyword}%"); }); // Define base raw columns $raw_columns = [ 'action', 'image', 'mass_delete', 'product', 'selling_price', 'purchase_price', 'category', 'current_stock', 'product_locations' ]; // Add dynamic price group columns foreach ($price_groups as $price_group) { $column_name = 'group_price' . $price_group->id; $raw_columns[] = $column_name; $datatables->editColumn($column_name, function ($row) use ($price_group) { try { $group_price = 0; if (isset($row->variation_id) && !empty($price_group->id)) { $variation_group_price = VariationGroupPrice::where('variation_id', $row->variation_id) ->where('price_group_id', $price_group->id) ->first(); if ($variation_group_price) { $group_price = (float) $variation_group_price->price_inc_tax; } } return '<span class="group_price" data-orig-value="' . $group_price . '">' . $this->productUtil->num_f($group_price, true) . '</span>'; } catch (\Exception $e) { \Log::error('Price group error: ' . $e->getMessage()); return '<span class="group_price" data-orig-value="0">0.00</span>'; } }); } // Apply final configuration and return return $datatables ->setRowAttr([ 'data-href' => function ($row) { if (auth()->user()->can('product.view')) { return action([\App\Http\Controllers\ProductController::class, 'view'], [$row->id]); } else { return ''; } }, ]) ->rawColumns($raw_columns) ->make(true); } // Non-AJAX request - return view with data $rack_enabled = (request()->session()->get('business.enable_racks') || request()->session()->get('business.enable_row') || request()->session()->get('business.enable_position')); $categories = Category::forDropdown($business_id, 'product'); $brands = Brands::forDropdown($business_id); $units = Unit::forDropdown($business_id); $tax_dropdown = TaxRate::forBusinessDropdown($business_id, false); $taxes = $tax_dropdown['tax_rates']; $business_locations = BusinessLocation::forDropdown($business_id); $business_locations->prepend(__('lang_v1.none'), 'none'); $show_manufacturing_data = $this->moduleUtil->isModuleInstalled('Manufacturing') && (auth()->user()->can('superadmin') || $this->moduleUtil->hasThePermissionInSubscription($business_id, 'manufacturing_module')); // List product screen filter from module $pos_module_data = $this->moduleUtil->getModuleData('get_filters_for_list_product_screen'); $is_admin = $this->productUtil->is_admin(auth()->user()); return view('product.index') ->with(compact( 'price_groups', 'rack_enabled', 'categories', 'brands', 'units', 'taxes', 'business_locations', 'show_manufacturing_data', 'pos_module_data', 'is_woocommerce', 'is_admin' )); } Key Changes Explained: Variable Declaration: $raw_columns array is created to manage column names dynamically DataTables Instance: Store the DataTables object in $datatables variable for manipulation Dynamic Addition: Price group columns are added to both the DataTables instance and the raw columns array Final Assembly: Apply rawColumns() and make() at the end Critical Addition - Missing variation_id: The original query is missing the essential variation_id field needed for price group lookup. Add this to your select statement: $products = $query->select( 'products.id', 'v.id as variation_id', // ADD THIS LINE - Essential for price groups! 'products.name as product', 'products.type', // ... rest of existing fields ... ); Complete Integration Steps: Add variation_id to select Get price groups before AJAX handling Replace direct rawColumns with variable approach Add price group processing loop Update rawColumns call to use the variable 3. Pass Price Groups to Viewreturn view('product.index') ->with(compact( 'price_groups', // Add this line 'rack_enabled', 'categories', 'brands', 'units', 'taxes', 'business_locations', 'show_manufacturing_data', 'pos_module_data', 'is_woocommerce', 'is_admin' )); Key Implementation NotesPrice Group Loop Position: The price group columns are positioned after the product type column and before category/brand columns Colspan Management: Each price group dynamically increments the $colspan variable for proper table footer spanning DataTable Integration: Price groups are added as non-searchable, non-orderable columns in the DataTable configuration Variation ID Requirement: The variation_id field is essential for linking price groups to specific product variations Dynamic Column Generation: Price group columns are generated based on actual database records, not hardcoded TestingVerify Price Groups Display: Check that price group columns appear in the product list Test Data Population: Ensure price group values display correctly for products with assigned group prices Verify Filtering: Test location and other filters work with the enhanced query Performance Check: Monitor query performance with the additional joins and data Step 2: Integration with Purchase System2.1 Add Price Group Columns to Purchase Create FormFile: resources/views/purchase/create.blade.php Add price group column headers to the purchase product table. Locate the existing selling price header and add the price group headers after it: <th> @lang('purchase.unit_selling_price') <small>(@lang('product.inc_of_tax'))</small> </th> @foreach ($price_groups_all as $price_group_key => $price_group) <th>{{ $price_group->name }}</th> @endforeach Location: Add this code after the unit selling price header in the purchase table. 2.2 Add Price Group Input Fields to Purchase Entry RowFile: resources/views/purchase/partials/purchase_entry_row.blade.php Add price group input fields for each product row. Locate the lot number section and add the price group fields before it: @if(!empty($price_groups) && is_iterable($price_groups)) @foreach($price_groups as $price_group) <td> @php $group_price_group = $price_group->id; @endphp {!! Form::text('purchases['.$row_count.'][group_prices]['.$group_price_group.'][group_price]',!empty($variation_prices[$variation->id][$group_price_group]['price']) ? @num_format($variation_prices[$variation->id][$group_price_group]['price']) : 0, ['class' => 'form-control input_number input-sm']); !!} {!! Form::hidden('purchases['.$row_count.'][group_prices]['.$group_price_group.'][group_price_id]',$price_group->id); !!} @php $price_type = !empty($variation_prices[$variation->id][$group_price_group]['price_type']) ? $variation_prices[$variation->id][$group_price_group]['price_type'] : 'fixed'; @endphp <input type="hidden" name="purchases[{{$row_count}}][group_prices][{{$group_price_group}}][group_price_type]" value="{{ $price_type }}"> </td> @endforeach @endif Location: Add this code before the lot number section: @if(session('business.enable_lot_number')) @php $lot_number = !empty($imported_data['lot_number']) ? $imported_data['lot_number'] : null; @endphp <td> {!! Form::text('purchases[' . $row_count . '][lot_number]', $lot_number, ['class' => 'form-control input-sm']); !!} </td> @endif 2.3 Update Purchase Controller MethodsFile: app/Http/Controllers/PurchaseController.php 2.3.1 Import Required ModelAt the top of the file with other imports, ensure you have: use App\SellingPriceGroup; 2.3.2 Update create() MethodIn the create() method, add price groups data before returning the view. Locate the existing code: $common_settings = !empty(session('business.common_settings')) ? session('business.common_settings') : []; $price_groups = SellingPriceGroup::forDropdown($business_id); $price_groups_all = SellingPriceGroup::where('business_id', $business_id)->get(); return view('purchase.create')->with( compact( 'price_groups', 'price_groups_all', 'taxes', 'orderStatuses', 'business_locations', 'currency_details', 'default_purchase_status', 'customer_groups', 'types', 'shortcuts', 'payment_line', 'payment_types', 'accounts', 'bl_attributes', 'common_settings' ) ); 2.3.3 Update edit() MethodIn the edit() method, ensure price groups are loaded and variation prices are prepared. Add this code before return view('purchase.edit'): $variation_prices = []; $price_groups = SellingPriceGroup::where('business_id', $business_id)->active()->get(); foreach ($purchase->purchase_lines as $key => $value) { if (!empty($value->sub_unit_id)) { $formated_purchase_line = $this->productUtil->changePurchaseLineUnit($value, $business_id); $purchase->purchase_lines[$key] = $formated_purchase_line; } foreach ($value->variations->group_prices as $group_price) { $variation_prices[$group_price->variation_id][$group_price->price_group_id] = [ 'price' => $group_price->price_inc_tax, 'price_type' => $group_price->price_type ]; } } Then include the price groups in the view compact: return view('purchase.edit') ->with(compact( 'price_groups', 'variation_prices', 'taxes', 'purchase', 'orderStatuses', 'business_locations', 'business', 'currency_details', 'default_purchase_status', 'customer_groups', 'types', 'shortcuts', 'purchase_orders', 'common_settings' )); 2.3.4 Update getPurchaseEntryRow() MethodIn the getPurchaseEntryRow() method, add price group data loading: public function getPurchaseEntryRow(Request $request) { if (request()->ajax()) { $product_id = $request->input('product_id'); $variation_id = $request->input('variation_id'); $business_id = request()->session()->get('user.business_id'); $location_id = $request->input('location_id'); $is_purchase_order = $request->has('is_purchase_order'); $supplier_id = $request->input('supplier_id'); $hide_tax = 'hide'; if ($request->session()->get('business.enable_inline_tax') == 1) { $hide_tax = ''; } $currency_details = $this->transactionUtil->purchaseCurrencyDetails($business_id); if (!empty($product_id)) { $row_count = $request->input('row_count'); $product = Product::where('id', $product_id) ->with(['unit', 'second_unit', 'variations.group_prices']) // ADD variations.group_prices ->first(); $sub_units = $this->productUtil->getSubUnits($business_id, $product->unit->id, false, $product_id); $query = Variation::where('product_id', $product_id) ->with([ 'product_variation', 'group_prices', // ADD this line 'variation_location_details' => function ($q) use ($location_id) { $q->where('location_id', $location_id); }, ]); if ($variation_id !== '0') { $query->where('id', $variation_id); } $variations = $query->get(); $taxes = TaxRate::where('business_id', $business_id) ->ExcludeForTaxGroup() ->get(); $last_purchase_line = $this->getLastPurchaseLine($variation_id, $location_id, $supplier_id); // ADD these lines for price groups $price_groups = SellingPriceGroup::where('business_id', $business_id)->get(); $variation_prices = []; foreach ($product->variations as $variation) { foreach ($variation->group_prices as $group_price) { $variation_prices[$variation->id][$group_price->price_group_id] = [ 'price' => $group_price->price_inc_tax, 'price_type' => $group_price->price_type ]; } } return view('purchase.partials.purchase_entry_row') ->with(compact( 'product', 'price_groups', // ADD this 'variation_prices', // ADD this 'variations', 'row_count', 'variation_id', 'taxes', 'currency_details', 'hide_tax', 'sub_units', 'is_purchase_order', 'last_purchase_line' )); } } } 2.3.5 Update importPurchaseProducts() MethodIn the importPurchaseProducts() method, add price group support for imported products before $html = view('purchase.partials.imported_purchase_product_rows'): $price_groups = SellingPriceGroup::where('business_id', $business_id)->get(); $variation_prices = []; foreach ($formatted_data as $data) { if (!empty($data['product'])) { foreach ($data['product']->variations as $variation) { foreach ($variation->group_prices as $group_price) { $variation_prices[$variation->id][$group_price->price_group_id] = [ 'price' => $group_price->price_inc_tax, 'price_type' => $group_price->price_type ]; } } } } $html = view('purchase.partials.imported_purchase_product_rows') ->with(compact('formatted_data', 'taxes', 'currency_details', 'hide_tax', 'row_count', 'price_groups', 'variation_prices'))->render(); 2.4 Add Price Group Columns to Purchase Edit FormFile: resources/views/purchase/partials/edit_purchase_entry_row.blade.php Add price group column headers to the purchase edit table. Locate the existing selling price header and add the price group headers after it: @if(empty($is_purchase_order)) <th>@lang('purchase.unit_selling_price') <small>(@lang('product.inc_of_tax'))</small></th> @if(!empty($price_groups) && is_iterable($price_groups)) @foreach ($price_groups as $price_group_key => $price_group) <th>{{ $price_group->name }}</th> @endforeach @endif @if(session('business.enable_lot_number')) <th>@lang('lang_v1.lot_number')</th> @endif @if(session('business.enable_product_expiry')) <th>@lang('product.mfg_date') / @lang('product.exp_date')</th> @endif @endif Add price group input fields for each existing purchase line. Locate the selling price cell and add the price group fields after it: @if(empty($is_purchase_order)) <td> @if(session('business.enable_editing_product_from_purchase')) {!! Form::text('purchases[' . $loop->index . '][default_sell_price]', number_format($sp, $currency_precision, $currency_details->decimal_separator, $currency_details->thousand_separator), ['class' => 'form-control input-sm input_number default_sell_price', 'required']); !!} @else {{number_format($sp, $currency_precision, $currency_details->decimal_separator, $currency_details->thousand_separator)}} @endif </td> @if(!empty($price_groups) && is_iterable($price_groups)) @foreach($price_groups as $price_group) <td> @php $group_price_group = $price_group->id; $current_group_price = 0; if(isset($variation_prices[$purchase_line->variation_id][$group_price_group]['price'])) { $current_group_price = $variation_prices[$purchase_line->variation_id][$group_price_group]['price']; } $price_type = isset($variation_prices[$purchase_line->variation_id][$group_price_group]['price_type']) ? $variation_prices[$purchase_line->variation_id][$group_price_group]['price_type'] : 'fixed'; @endphp {!! Form::text('purchases['.$loop->index.'][group_prices]['.$group_price_group.'][group_price]', number_format($current_group_price, $currency_precision, $currency_details->decimal_separator, $currency_details->thousand_separator), ['class' => 'form-control input-sm input_number']); !!} {!! Form::hidden('purchases['.$loop->index.'][group_prices]['.$group_price_group.'][group_price_id]', $price_group->id); !!} <input type="hidden" name="purchases[{{$loop->index}}][group_prices][{{$group_price_group}}][group_price_type]" value="{{ $price_type }}"> </td> @endforeach @endif You can add a custom CSS rule to set minimum width for all form inputs on the edit page. Place this <style> block at the very top of your edit_purchase_entry_row.blade.php file, right after the PHP variables section: @php $hide_tax = ''; if(session()->get('business.enable_inline_tax') == 0){ $hide_tax = 'hide'; } $currency_precision = session('business.currency_precision', 2); $quantity_precision = session('business.quantity_precision', 2); @endphp {{-- Add a custom CSS rule to set minimum width for all form inputs --}} <style> #purchase_entry_table .form-control { min-width: 5rem; } </style> <div class="table-responsive"> <!-- rest of your table --> Or you can add a section CSS in resources/views/purchase/create.blade.php and resources/views/purchase/edit.blade.php pages: @section('css') <style> #purchase_entry_table .form-control { min-width: 5rem; } </style> @endsection 2.5 Update ProductUtil for Price Group ProcessingFile: app/Utils/ProductUtil.php Add the price group processing method to ProductUtil: public function createOrUpdateGroupPrice($group_prices, $variation_id) { if (!empty($variation_id)) { foreach ($group_prices as $key => $value) { // \Log::info("Processing group price key: $key", $value); // Check if the record already exists $variation_group_price = VariationGroupPrice::where('variation_id', $variation_id) ->where('price_group_id', $value['group_price_id']) ->first(); if (empty($variation_group_price)) { // \Log::info('Creating NEW variation group price record'); // Create new record $variation_group_price = new VariationGroupPrice(); $variation_group_price->variation_id = $variation_id; $variation_group_price->price_group_id = $value['group_price_id']; } else { // \Log::info('UPDATING existing variation group price record', [ // 'id' => $variation_group_price->id, // 'current_price' => $variation_group_price->price_inc_tax // ]); } // Convert price using num_uf $new_price = $this->num_uf($value['group_price']); // Update the values $variation_group_price->price_inc_tax = $new_price; $variation_group_price->price_type = $value['group_price_type']; // Save each record individually $result = $variation_group_price->save(); } } else { \Log::warning('Variation ID is empty - skipping price group update'); } } Update the createOrUpdatePurchaseLines method in ProductUtil to process price groups. Your createOrUpdatePurchaseLines method only processes the first element which only contains group_price 1. Elements 2-7 don't have product_id or variation_id, so they're skipped by the validation. Fix: Modify your createOrUpdatePurchaseLines method to handle this structure: /** * Add/Edit transaction purchase lines * * @param object $transaction * @param array $input_data * @param array $currency_details * @param bool $enable_product_editing * @param string $before_status = null * @return array */ public function createOrUpdatePurchaseLines($transaction, $input_data, $currency_details, $enable_product_editing, $before_status = null) { $updated_purchase_lines = []; $updated_purchase_line_ids = [0]; $exchange_rate = !empty($transaction->exchange_rate) ? $transaction->exchange_rate : 1; foreach ($input_data as $data) { // Skip if this is just a group_prices fragment without product data if (isset($data['group_prices']) && !isset($data['product_id'])) { // This is a price group fragment, merge it with the main purchase line continue; } // Validate required fields before processing if (!is_array($data) || empty($data['quantity']) || empty($data['product_id']) || empty($data['variation_id'])) { continue; } $multiplier = 1; if (isset($data['sub_unit_id']) && $data['sub_unit_id'] == $data['product_unit_id']) { unset($data['sub_unit_id']); } if (!empty($data['sub_unit_id'])) { $unit = Unit::find($data['sub_unit_id']); $multiplier = !empty($unit->base_unit_multiplier) ? $unit->base_unit_multiplier : 1; } $new_quantity = $this->num_uf($data['quantity']) * $multiplier; $new_quantity_f = $this->num_f($new_quantity); $old_qty = 0; //update existing purchase line if (isset($data['purchase_line_id'])) { $purchase_line = PurchaseLine::findOrFail($data['purchase_line_id']); $updated_purchase_line_ids[] = $purchase_line->id; $old_qty = $purchase_line->quantity; $this->updateProductStock($before_status, $transaction, $data['product_id'], $data['variation_id'], $new_quantity, $purchase_line->quantity, $currency_details); } else { //create newly added purchase lines $purchase_line = new PurchaseLine(); $purchase_line->product_id = $data['product_id']; $purchase_line->variation_id = $data['variation_id']; //Increase quantity only if status is received if ($transaction->status == 'received') { $this->updateProductQuantity($transaction->location_id, $data['product_id'], $data['variation_id'], $new_quantity_f, 0, $currency_details); } } // Collect all group prices from all array elements for this variation $all_group_prices = []; // Get group prices from current element if (isset($data['group_prices'])) { $all_group_prices = array_merge($all_group_prices, $data['group_prices']); } // Get group prices from other elements in the array foreach ($input_data as $other_data) { if (isset($other_data['group_prices']) && !isset($other_data['product_id'])) { $all_group_prices = array_merge($all_group_prices, $other_data['group_prices']); } } // Process all collected group prices if (!empty($all_group_prices)) { $this->createOrUpdateGroupPrice($all_group_prices, $data['variation_id']); } $purchase_line->quantity = $new_quantity; $purchase_line->pp_without_discount = ($this->num_uf($data['pp_without_discount'], $currency_details) * $exchange_rate) / $multiplier; $purchase_line->discount_percent = $this->num_uf($data['discount_percent'], $currency_details); $purchase_line->purchase_price = ($this->num_uf($data['purchase_price'], $currency_details) * $exchange_rate) / $multiplier; $purchase_line->purchase_price_inc_tax = ($this->num_uf($data['purchase_price_inc_tax'], $currency_details) * $exchange_rate) / $multiplier; $purchase_line->item_tax = ($this->num_uf($data['item_tax'], $currency_details) * $exchange_rate) / $multiplier; $purchase_line->tax_id = $data['purchase_line_tax_id']; $purchase_line->lot_number = !empty($data['lot_number']) ? $data['lot_number'] : null; $purchase_line->mfg_date = !empty($data['mfg_date']) ? $this->uf_date($data['mfg_date']) : null; $purchase_line->exp_date = !empty($data['exp_date']) ? $this->uf_date($data['exp_date']) : null; $purchase_line->sub_unit_id = !empty($data['sub_unit_id']) ? $data['sub_unit_id'] : null; $purchase_line->purchase_order_line_id = !empty($data['purchase_order_line_id']) ? $data['purchase_order_line_id'] : null; $purchase_line->purchase_requisition_line_id = !empty($data['purchase_requisition_line_id']) && $transaction->type == 'purchase_order' ? $data['purchase_requisition_line_id'] : null; if (!empty($data['secondary_unit_quantity'])) { $purchase_line->secondary_unit_quantity = $this->num_uf($data['secondary_unit_quantity']); } $updated_purchase_lines[] = $purchase_line; //Edit product price if ($enable_product_editing == 1 && $transaction->type == 'purchase') { if (isset($data['default_sell_price'])) { $variation_data['sell_price_inc_tax'] = ($this->num_uf($data['default_sell_price'], $currency_details)) / $multiplier; } $variation_data['pp_without_discount'] = ($this->num_uf($data['pp_without_discount'], $currency_details) * $exchange_rate) / $multiplier; $variation_data['variation_id'] = $purchase_line->variation_id; $variation_data['purchase_price'] = $purchase_line->purchase_price; $this->updateProductFromPurchase($variation_data); } if ($transaction->type == 'purchase_order') { //Update purchase requisition line quantity received $this->updatePurchaseOrderLine($purchase_line->purchase_requisition_line_id, $purchase_line->quantity, $old_qty); } //Update purchase order line quantity received $this->updatePurchaseOrderLine($purchase_line->purchase_order_line_id, $purchase_line->quantity, $old_qty); } //unset deleted purchase lines $delete_purchase_line_ids = []; $delete_purchase_lines = null; if (!empty($updated_purchase_line_ids)) { $delete_purchase_lines = PurchaseLine::where('transaction_id', $transaction->id) ->whereNotIn('id', $updated_purchase_line_ids) ->get(); if ($delete_purchase_lines->count()) { foreach ($delete_purchase_lines as $delete_purchase_line) { $delete_purchase_line_ids[] = $delete_purchase_line->id; //decrease deleted only if previous status was received if ($before_status == 'received') { $this->decreaseProductQuantity( $delete_purchase_line->product_id, $delete_purchase_line->variation_id, $transaction->location_id, $delete_purchase_line->quantity ); } //If purchase order line set decrease quantity if (!empty($delete_purchase_line->purchase_order_line_id)) { $this->updatePurchaseOrderLine($delete_purchase_line->purchase_order_line_id, 0, $delete_purchase_line->quantity); } //If purchase order line set decrease quantity if (!empty($delete_purchase_line->purchase_requisition_line_id)) { $this->updatePurchaseOrderLine($delete_purchase_line->purchase_requisition_line_id, 0, $delete_purchase_line->quantity); } } //unset if purchase order line from purchase lines if exists if ($transaction->type == 'purchase_order') { PurchaseLine::whereIn('purchase_order_line_id', $delete_purchase_line_ids) ->update(['purchase_order_line_id' => null]); } //Delete deleted purchase lines PurchaseLine::where('transaction_id', $transaction->id) ->whereIn('id', $delete_purchase_line_ids) ->delete(); } } //update purchase lines if (!empty($updated_purchase_lines)) { $transaction->purchase_lines()->saveMany($updated_purchase_lines); } return $delete_purchase_lines; } Location: Insert this code after the line where $purchase_line is created or found, and before the quantity assignment. 2.6 Update Import Template and ProcessingFile: import_purchase_products_template.xls The import template needs to be updated to include price group columns. The current template structure should be extended with additional columns for each price group. Add this import at the top of your PurchaseController.php file if it doesn't exist: use Maatwebsite\Excel\Facades\Excel; Original Template Structure:Column A: SKU (required) Column B: Quantity (required) Column C: Unit Cost Before Discount Column D: Discount Percent Column E: Tax Name Column F: Lot Number Column G: Manufacturing Date Column H: Expiry Date Updated Template Structure:Column A: SKU (required) Column B: Quantity (required) Column C: Unit Cost Before Discount Column D: Discount Percent Column E: Tax Name Column F: Lot Number Column G: Manufacturing Date Column H: Expiry Date Column I: Price Group 1 (if exists) Column J: Price Group 2 (if exists) Column K: Price Group 3 (if exists) ... (continue for each price group) Template Update Instructions:Add Dynamic Columns: The number of price group columns depends on how many selling price groups exist in your business Column Headers: Use the actual price group names as column headers Sample Data: Include sample price group values in the template Documentation: Update any accompanying documentation to explain the new columns Controller Update for Import ProcessingYou'll also need to update the importPurchaseProducts() method to process the additional price group columns: public function importPurchaseProducts(Request $request) { try { $file = $request->file('file'); $parsed_array = Excel::toArray([], $file); //Remove header row $imported_data = array_splice($parsed_array[0], 1); $business_id = $request->session()->get('user.business_id'); $location_id = $request->input('location_id'); $row_count = $request->input('row_count'); // Get price groups for processing $price_groups = SellingPriceGroup::where('business_id', $business_id)->active()->get(); $formatted_data = []; $row_index = 0; $error_msg = ''; foreach ($imported_data as $key => $value) { $row_index = $key + 1; $temp_array = []; if (!empty($value[0])) { $variation = Variation::where('sub_sku', trim($value[0])) ->join('products', 'products.id', '=', 'variations.product_id') ->where('products.business_id', $business_id) ->with([ 'product_variation', 'variation_location_details' => function ($q) use ($location_id) { $q->where('location_id', $location_id); }, ]) ->select('variations.*') ->first(); $temp_array['variation'] = $variation; if (empty($variation)) { $error_msg = __('lang_v1.product_not_found_exception', ['row' => $row_index, 'sku' => $value[0]]); break; } $product = Product::where('id', $variation->product_id) ->where('business_id', $business_id) ->with(['unit']) ->first(); if (empty($product)) { $error_msg = __('lang_v1.product_not_found_exception', ['row' => $row_index, 'sku' => $value[0]]); break; } $temp_array['product'] = $product; $sub_units = $this->productUtil->getSubUnits($business_id, $product->unit->id, false, $product->id); $temp_array['sub_units'] = $sub_units; } else { $error_msg = __('lang_v1.product_not_found_exception', ['row' => $row_index, 'sku' => $value[0]]); break; } if (!empty($value[1])) { $temp_array['quantity'] = $value[1]; } else { $error_msg = __('lang_v1.quantity_required', ['row' => $row_index]); break; } $temp_array['unit_cost_before_discount'] = !empty($value[2]) ? $value[2] : $variation->default_purchase_price; $temp_array['discount_percent'] = !empty($value[3]) ? $value[3] : 0; $tax_id = null; if (!empty($value[4])) { $tax_name = trim($value[4]); $tax = TaxRate::where('business_id', $business_id) ->where('name', 'like', "%{$tax_name}%") ->first(); $tax_id = $tax->id ?? $tax_id; } $temp_array['tax_id'] = $tax_id; $temp_array['lot_number'] = !empty($value[5]) ? $value[5] : null; $temp_array['mfg_date'] = !empty($value[6]) ? $this->productUtil->format_date($value[6]) : null; $temp_array['exp_date'] = !empty($value[7]) ? $this->productUtil->format_date($value[7]) : null; // Process price group columns (starting from column I = index 8) $price_group_data = []; foreach ($price_groups as $index => $price_group) { $column_index = 8 + $index; // Starting after expiry date column if (isset($value[$column_index]) && !empty($value[$column_index])) { $price_group_data[$price_group->id] = [ 'group_price' => $value[$column_index], 'group_price_id' => $price_group->id, 'group_price_type' => 'fixed' ]; } } $temp_array['group_prices'] = $price_group_data; $formatted_data[] = $temp_array; } if (!empty($error_msg)) { return [ 'success' => false, 'msg' => $error_msg, ]; } $hide_tax = 'hide'; if ($request->session()->get('business.enable_inline_tax') == 1) { $hide_tax = ''; } $taxes = TaxRate::where('business_id', $business_id) ->ExcludeForTaxGroup() ->get(); $currency_details = $this->transactionUtil->purchaseCurrencyDetails($business_id); $price_groups = SellingPriceGroup::where('business_id', $business_id)->get(); $variation_prices = []; foreach ($formatted_data as $data) { if (!empty($data['product'])) { foreach ($data['product']->variations as $variation) { foreach ($variation->group_prices as $group_price) { $variation_prices[$variation->id][$group_price->price_group_id] = [ 'price' => $group_price->price_inc_tax, 'price_type' => $group_price->price_type ]; } } } } $html = view('purchase.partials.imported_purchase_product_rows') ->with(compact('formatted_data', 'taxes', 'currency_details', 'hide_tax', 'row_count', 'price_groups', 'variation_prices'))->render(); return [ 'success' => true, 'msg' => __('lang_v1.imported'), 'html' => $html, ]; } catch (\Exception $e) { return [ 'success' => false, 'msg' => $e->getMessage(), ]; } } Template Generation Script (Optional)You could create a dynamic template generator that creates the Excel file based on current price groups: public function downloadImportTemplate() { $business_id = request()->session()->get('user.business_id'); $price_groups = SellingPriceGroup::where('business_id', $business_id)->active()->get(); // Create Excel file with dynamic headers $headers = [ 'SKU', 'Quantity', 'Unit Cost Before Discount', 'Discount Percent', 'Tax Name', 'Lot Number', 'Manufacturing Date', 'Expiry Date' ]; // Add price group headers foreach ($price_groups as $price_group) { $headers[] = $price_group->name; } // Generate Excel file with these headers // ... Excel generation logic ... } Important Notes:Backward Compatibility: Ensure the import still works if price group columns are missing Error Handling: Add validation for price group values in the import process Documentation: Update user documentation to explain the new template format Template Versioning: Consider versioning your templates if you have existing users Key Implementation FeaturesDynamic Price Group Headers: Price group columns are generated dynamically based on active selling price groups Pre-populated Values: When editing purchases, existing price group values are loaded and displayed Form Validation: Price group inputs use the same validation as other price fields Import Support: Price groups are included in the product import functionality Consistent UI: Price group fields follow the same styling and behavior as other input fields Testing the IntegrationCreate Purchase: Verify price group columns appear in the create purchase form Add Products: Ensure price group fields are populated when adding products to purchase Edit Purchase: Check that existing price group values are loaded correctly Import Products: Test that price group data is maintained during product import Data Persistence: Verify that price group values are saved and can be retrieved TroubleshootingCommon Issues:Missing Price Group Columns: Ensure $price_groups_all is passed to the create view Empty Price Group Fields: Verify $variation_prices array is properly structured Form Submission Errors: Check that price group data structure matches expected format Import Issues: Ensure price group logic is included in import processing Performance Considerations:Database Queries: Monitor the impact of additional joins and queries Frontend Rendering: Test with multiple price groups to ensure responsive UI Memory Usage: Check memory consumption with large datasets Best PracticesError Handling: Always include try-catch blocks for price group operations Data Validation: Validate price group data before processing Logging: Add appropriate logging for debugging price group issues User Experience: Provide clear feedback when price groups are updated Next StepsWith both Step 1 (Product List Display) and Step 2 (Purchase Integration) complete, you now have: ✅ Dynamic price group columns in product listings ✅ Price group management in purchase create/edit forms ✅ Automatic price group updates during purchase operations ✅ Support for product imports with price groups Your Ultimate POS now has full price group functionality integrated across the product and purchase systems!
-
Camera Barcode Scanner
Ultimate POS Camera Barcode Scanner Implementation GuideA comprehensive step-by-step tutorial for implementing a universal camera barcode scanner across all Ultimate POS modules. 📋 OverviewThis guide will help you implement a reusable camera barcode scanner component that works across all Ultimate POS pages including Sales, Purchase, Stock Management, and POS systems. Camera scanner button integrated seamlessly in POS interface Features✅ Universal compatibility across all modules ✅ Automatic page detection and appropriate behavior ✅ Reusable Blade component ✅ Fallback support for different search endpoints ✅ Multiple styling options ✅ HTTPS camera access with proper error handling ✅ Mobile-optimized scanning interface ✅ Real-time barcode detection Professional scanning interface with real-time camera feed 🚀 Quick StartPrerequisitesUltimate POS system HTTPS connection (required for camera access) Modern browser with camera support DependenciesjQuery (already included in Ultimate POS) Html5Qrcode library Bootstrap (already included in Ultimate POS) 📁 File Structure├── resources/views/components/ │ └── camera-barcode-scanner.blade.php # Reusable component ├── public/js/ │ └── camera-barcode-scanner.js # Universal JavaScript └── resources/views/layouts/partials/ └── javascripts.blade.php # Include scripts 🛠️ Step 1: Create the Reusable ComponentCreate the file resources/views/components/camera-barcode-scanner.blade.php: {{-- Camera Barcode Scanner Component Usage: <x-camera-barcode-scanner search-input-id="search_product" /> Props: - search-input-id: ID of the search input field (default: 'search_product') - button-class: Additional CSS classes for button (optional) - show-in-group: Whether to show in input-group-btn (default: true) - button-style: Button style - 'default', 'success', 'primary', 'link' (default: 'default') - full-width: Makes button full width (default: false) - button-text: Text for standalone buttons (default: 'Scan Barcode') --}} @props([ 'searchInputId' => 'search_product', 'buttonClass' => '', 'showInGroup' => true, 'buttonStyle' => 'default', 'fullWidth' => false, 'buttonText' => 'Scan Barcode' ]) @if($showInGroup) {{-- Camera Barcode Scanner Button for Input Groups --}} <button type="button" class="btn btn-default bg-white btn-flat camera-barcode-scanner-btn {{ $buttonClass }}" data-search-input="{{ $searchInputId }}" title="Scan Barcode"> <i class="fa fa-camera text-primary fa-lg"></i> </button> @else {{-- Standalone Camera Button --}} <button type="button" class="btn btn-{{ $buttonStyle }} camera-barcode-scanner-btn {{ $buttonClass }} @if($fullWidth) btn-block @endif" data-search-input="{{ $searchInputId }}" title="Scan Barcode"> <i class="fa fa-camera @if($buttonStyle === 'link') text-primary @endif"></i> {{ $buttonText }} </button> @endif {{-- Camera Scanner Modal (only include once per page) --}} @once <div id="camera_modal" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.9); z-index: 9999; text-align: center;"> <div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background: white; padding: 20px; border-radius: 10px; max-width: 90%; max-height: 90%;"> <div style="margin-bottom: 15px;"> <h4 style="margin: 0; color: #333;">📷 Barcode Scanner</h4> <button type="button" onclick="event.preventDefault(); event.stopPropagation(); CameraBarcodeScanner.closeModalOnly(); return false;" style="position: absolute; top: 10px; right: 15px; background: none; border: none; font-size: 20px; cursor: pointer;">×</button> </div> {{-- Html5Qrcode Scanner --}} <div id="reader" style="width: 100%; max-width: 500px; border: 2px solid #007bff; border-radius: 8px;"></div> <div id="status" style="margin-top: 15px; padding: 10px; background: #f8f9fa; border-radius: 5px; color: #333;"> Starting camera... </div> </div> </div> <style> #barcode_scanner_btn:hover, .camera-barcode-scanner-btn:hover { background-color: #e3f2fd !important; border-color: #2196f3 !important; } #reader video { border-radius: 5px; } #camera_modal { backdrop-filter: blur(3px); } @media (max-width: 768px) { #camera_modal > div { width: 95% !important; max-width: none !important; } } </style> @endonce 🔧 Step 2: Create the Universal JavaScriptCreate the file public/js/camera-barcode-scanner.js: /** * Universal Camera Barcode Scanner for Ultimate POS * Version: 2.0 * * Works with: POS, Sales, Purchase, Stock Adjustment, Transfers, etc. */ (function ($) { 'use strict'; window.CameraBarcodeScanner = { config: { cameraConfig: { facingMode: 'environment' }, scanConfig: { fps: 10, qrbox: { width: 300, height: 150 }, supportedScanTypes: [Html5QrcodeScanType.SCAN_TYPE_CAMERA], }, }, html5QrCode: null, isScanning: false, currentSearchInputId: 'search_product', currentPageType: null, init: function () { console.log('🎥 Initializing Universal Camera Barcode Scanner...'); try { this.html5QrCode = new Html5Qrcode('reader'); this.detectPageType(); this.bindEvents(); } catch (error) { console.error( '❌ Camera scanner initialization failed:', error ); } }, detectPageType: function () { const currentUrl = window.location.pathname; if (currentUrl.includes('/pos/')) { this.currentPageType = 'pos'; } else if (currentUrl.includes('/sells/')) { this.currentPageType = 'sell'; } else if ( currentUrl.includes('/purchases/') || currentUrl.includes('/purchase-order/') ) { this.currentPageType = 'purchase'; } else if (currentUrl.includes('/stock-adjustments/')) { this.currentPageType = 'stock_adjustment'; } else if (currentUrl.includes('/stock-transfers/')) { this.currentPageType = 'transfer'; } else if (currentUrl.includes('/purchase-return/')) { this.currentPageType = 'purchase_return'; } else { this.currentPageType = 'general'; } }, bindEvents: function () { const self = this; $(document).on( 'click', '.camera-barcode-scanner-btn, #barcode_scanner_btn', function (e) { e.preventDefault(); e.stopPropagation(); const searchInputId = $(this).data('search-input') || 'search_product'; self.currentSearchInputId = searchInputId; self.openCamera(); } ); $(document).on('keydown', function (e) { if (e.ctrlKey && e.key === 'b') { e.preventDefault(); $( '.camera-barcode-scanner-btn:visible, #barcode_scanner_btn:visible' ) .first() .click(); } }); }, openCamera: function () { if (!this.html5QrCode) return; if ( location.protocol !== 'https:' && location.hostname !== 'localhost' ) { alert('❌ Camera requires HTTPS connection'); return; } $('#camera_modal').show(); this.startCamera(); }, startCamera: function () { if (this.isScanning) return; const self = this; this.html5QrCode .start( this.config.cameraConfig, this.config.scanConfig, function (decodedText) { self.closeCamera(); self.processBarcodeForCurrentPage(decodedText); if (typeof toastr !== 'undefined') { toastr.success('Barcode scanned: ' + decodedText); } }, function () { $('#status').text( '📷 Scanning... Position barcode in view' ); } ) .then(() => { self.isScanning = true; $('#status').text('📷 Camera active'); }) .catch((err) => { $('#status').text('❌ Camera error: ' + err); setTimeout(() => self.closeCamera(), 2000); }); }, processBarcodeForCurrentPage: function (barcode) { const searchInput = $('#' + this.currentSearchInputId); if (searchInput.length) { searchInput.val(barcode).trigger('input').trigger('change'); } }, closeCamera: function () { if (this.isScanning && this.html5QrCode) { this.html5QrCode.stop().then(() => { this.isScanning = false; }); } $('#camera_modal').hide(); }, closeModalOnly: function () { this.closeCamera(); return false; }, }; $(document).ready(function () { if ( $('.camera-barcode-scanner-btn').length || $('#barcode_scanner_btn').length ) { CameraBarcodeScanner.init(); } }); })(jQuery); 📜 Step 3: Include Required ScriptsUpdate resources/views/layouts/partials/javascripts.blade.php: <!-- Camera Barcode Scanner Dependencies --> <script src="https://unpkg.com/[email protected]/html5-qrcode.min.js"></script> <script src="{{ asset('js/camera-barcode-scanner.js') }}"></script> 📑 Step 4: Implementation by Page Type🛒 POS SystemFile: resources/views/sale_pos/partials/pos_form.blade.php Solution 1: Default Settings<span class="input-group-btn"> {{-- Camera Barcode Scanner Button --}} <x-camera-barcode-scanner search-input-id="search_product" /> <!-- Other buttons... --> </span> Solution 2: Better Styling<span class="input-group-btn"> {{-- Camera Barcode Scanner Button --}} <x-camera-barcode-scanner search-input-id="search_product" button-class="pos-camera-btn" /> <!-- Other buttons... --> </span> 💰 Sales PagesFiles: resources/views/sell/create.blade.php, resources/views/sell/edit.blade.php Solution 1: Default Settings<div class="input-group"> <div class="input-group-btn"> <button type="button" class="btn btn-default bg-white btn-flat" data-toggle="modal" data-target="#configure_search_modal"> <i class="fas fa-search-plus"></i> </button> </div> {!! Form::text('search_product', null, ['class' => 'form-control mousetrap', 'id' => 'search_product', 'placeholder' => __('lang_v1.search_product_placeholder')]) !!} <span class="input-group-btn"> <x-camera-barcode-scanner search-input-id="search_product" /> <button type="button" class="btn btn-default bg-white btn-flat pos_add_quick_product"> <i class="fa fa-plus-circle text-primary fa-lg"></i> </button> </span> </div> Solution 2: Better Styling<div class="input-group"> <div class="input-group-btn"> <button type="button" class="btn btn-default bg-white btn-flat" data-toggle="modal" data-target="#configure_search_modal" title="{{__('lang_v1.configure_product_search')}}"> <i class="fas fa-search-plus"></i> </button> </div> {!! Form::text('search_product', null, ['class' => 'form-control mousetrap', 'id' => 'search_product', 'placeholder' => __('lang_v1.search_product_placeholder')]) !!} <span class="input-group-btn"> <x-camera-barcode-scanner search-input-id="search_product" button-class="sales-camera-btn" title="Scan Product Barcode" /> <button type="button" class="btn btn-default bg-white btn-flat pos_add_quick_product" data-href="{{action([\App\Http\Controllers\ProductController::class, 'quickAdd'])}}" data-container=".quick_add_product_modal"> <i class="fa fa-plus-circle text-primary fa-lg"></i> </button> </span> </div> 📦 Purchase PagesFiles: resources/views/purchase/create.blade.php, resources/views/purchase/edit.blade.php Solution 1: Default Settings<div class="col-sm-2"> <div class="form-group"> <x-camera-barcode-scanner search-input-id="search_product" /> <button tabindex="-1" type="button" class="btn btn-link btn-modal" data-href="{{action([\App\Http\Controllers\ProductController::class, 'quickAdd'])}}" data-container=".quick_add_product_modal"> <i class="fa fa-plus"></i> @lang('product.add_new_product') </button> </div> </div> Solution 2: Better Styling<div class="col-sm-2"> <div class="form-group"> <x-camera-barcode-scanner search-input-id="search_product" :show-in-group="false" button-style="link" button-text="📷 Scan Barcode" :full-width="true" /> <button tabindex="-1" type="button" class="btn btn-link btn-modal" data-href="{{action([\App\Http\Controllers\ProductController::class, 'quickAdd'])}}" data-container=".quick_add_product_modal"> <i class="fa fa-plus"></i> @lang('product.add_new_product') </button> </div> </div> 📋 Purchase Order PagesFiles: resources/views/purchase_order/create.blade.php, resources/views/purchase_order/edit.blade.php Solution 1: Default Settings<div class="col-sm-2"> <div class="form-group"> <x-camera-barcode-scanner search-input-id="search_product" /> <button tabindex="-1" type="button" class="btn btn-link btn-modal" data-href="{{action([\App\Http\Controllers\ProductController::class, 'quickAdd'])}}" data-container=".quick_add_product_modal"> <i class="fa fa-plus"></i> @lang('product.add_new_product') </button> </div> </div> Solution 2: Better Styling<div class="col-sm-2"> <div class="form-group"> <x-camera-barcode-scanner search-input-id="search_product" :show-in-group="false" button-style="link" button-text="🎥 Scan Product" :full-width="true" button-class="purchase-order-scanner" /> <button tabindex="-1" type="button" class="btn btn-link btn-modal" data-href="{{action([\App\Http\Controllers\ProductController::class, 'quickAdd'])}}" data-container=".quick_add_product_modal"> <i class="fa fa-plus"></i> @lang('product.add_new_product') </button> </div> </div> 🔄 Stock Transfer PagesFiles: resources/views/stock_transfer/create.blade.php, resources/views/stock_transfer/edit.blade.php Solution 1: Default Settings<div class="form-group"> <div class="input-group"> <span class="input-group-addon"> <i class="fa fa-search"></i> </span> {!! Form::text('search_product', null, ['class' => 'form-control', 'id' => 'search_product_for_srock_adjustment', 'placeholder' => __('stock_adjustment.search_product')]) !!} <span class="input-group-btn"> <x-camera-barcode-scanner search-input-id="search_product_for_srock_adjustment" /> </span> </div> </div> Solution 2: Better Styling<div class="form-group"> <div class="input-group"> <span class="input-group-addon"> <i class="fa fa-search"></i> </span> {!! Form::text('search_product', null, ['class' => 'form-control', 'id' => 'search_product_for_stock_transfer', 'placeholder' => __('lang_v1.search_product_placeholder')]) !!} <span class="input-group-btn"> <x-camera-barcode-scanner search-input-id="search_product_for_stock_transfer" button-class="transfer-scanner-btn" title="Scan Product for Transfer" /> </span> </div> </div> 📊 Stock Adjustment PagesFiles: resources/views/stock_adjustment/create.blade.php Solution 1: Default Settings<div class="form-group"> <div class="input-group"> <span class="input-group-addon"> <i class="fa fa-search"></i> </span> {!! Form::text('search_product', null, ['class' => 'form-control','id' => 'search_product_for_srock_adjustment','placeholder' => __('stock_adjustment.search_product'),'disabled',]) !!} <span class="input-group-btn"> <x-camera-barcode-scanner search-input-id="search_product_for_srock_adjustment" /> </span> </div> </div> Solution 2: Better Styling<div class="form-group"> <div class="input-group"> <span class="input-group-addon"> <i class="fa fa-search"></i> </span> {!! Form::text('search_product', null, ['class' => 'form-control','id' => 'search_product_for_stock_adjustment','placeholder' => __('stock_adjustment.search_product'),'disabled',]) !!} <span class="input-group-btn"> <x-camera-barcode-scanner search-input-id="search_product_for_stock_adjustment" button-class="adjustment-scanner-btn" title="Scan Product for Adjustment" /> </span> </div> </div> 🔙 Purchase Return PagesFiles: resources/views/purchase_return/create.blade.php, resources/views/purchase_return/edit.blade.php Solution 1: Default Settings<div class="col-sm-2"> <div class="form-group"> <x-camera-barcode-scanner search-input-id="search_product" /> <button tabindex="-1" type="button" class="btn btn-link btn-modal" data-href="{{action([\App\Http\Controllers\ProductController::class, 'quickAdd'])}}" data-container=".quick_add_product_modal"> <i class="fa fa-plus"></i> @lang('product.add_new_product') </button> </div> </div> Solution 2: Better Styling<div class="col-sm-2"> <div class="form-group"> <x-camera-barcode-scanner search-input-id="search_product" :show-in-group="false" button-style="link" button-text="📱 Scan Return" :full-width="true" button-class="return-scanner-btn" /> <button tabindex="-1" type="button" class="btn btn-link btn-modal" data-href="{{action([\App\Http\Controllers\ProductController::class, 'quickAdd'])}}" data-container=".quick_add_product_modal"> <i class="fa fa-plus"></i> @lang('product.add_new_product') </button> </div> </div> 🎛️ Component Configuration OptionsProps AvailableProp Type Default Description search-input-id String 'search_product' ID of the target search input button-class String '' Additional CSS classes :show-in-group Boolean true Input group button vs standalone button-style String 'default' Bootstrap button style :full-width Boolean false Full width button button-text String 'Scan Barcode' Text for standalone buttons Usage ExamplesInput Group Button (Default)<x-camera-barcode-scanner search-input-id="search_product" /> Standalone Link Button<x-camera-barcode-scanner search-input-id="search_product" :show-in-group="false" button-style="link" button-text="📷 Scan Barcode" :full-width="true" /> Success Button with Custom Class<x-camera-barcode-scanner search-input-id="search_product" :show-in-group="false" button-style="success" button-text="Scan Product" button-class="my-custom-class" /> 🔧 TroubleshootingCommon IssuesCamera Not WorkingProblem: Camera doesn't start Solution: Ensure HTTPS connection Check: Browser permissions for camera access Button Not RespondingProblem: Click doesn't open camera Solution: Check if scripts are loaded properly Check: Browser console for JavaScript errors Wrong Input FieldProblem: Barcode goes to wrong input Solution: Verify search-input-id matches actual input ID Check: Inspect element to confirm input ID Debug ModeAdd this to your page to debug: console.log('Scanner available:', CameraBarcodeScanner.isAvailable()); console.log('Current page type:', CameraBarcodeScanner.currentPageType); console.log('Target input:', CameraBarcodeScanner.currentSearchInputId); 🚀 Advanced FeaturesKeyboard ShortcutPress Ctrl + B to open camera scanner Works on any page with scanner buttons Mobile SupportResponsive design for mobile devices Touch-friendly buttons Mobile camera optimization Error HandlingGraceful fallback when camera unavailable Clear error messages for users Automatic retry mechanisms 📈 Performance TipsOptimizationSingle Modal: Modal included only once per page with @once Event Delegation: Uses event delegation for better performance Lazy Loading: Scanner initializes only when needed Memory Management: Proper cleanup when camera stops Best PracticesUse specific input IDs for better targeting Test on HTTPS environment Provide fallback for non-camera devices Keep component props simple and focused 🔒 Security ConsiderationsHTTPS RequirementCamera access requires HTTPS Development: Use localhost or HTTPS Production: Ensure SSL certificate PermissionsBrowser requests camera permission Users can deny and use manual input Graceful degradation when permission denied 📱 Browser CompatibilitySupported Browsers✅ Chrome 60+ ✅ Firefox 55+ ✅ Safari 11+ ✅ Edge 79+ ✅ Mobile browsers (iOS Safari, Chrome Mobile) Fallback SupportAutomatic fallback to manual input Works without camera on any device Progressive enhancement approach 🎉 ConclusionYou now have a universal camera barcode scanner implemented across all Ultimate POS modules! The component is: Reusable across all pages Flexible with multiple styling options Robust with error handling and fallbacks User-friendly with clear instructions Mobile-optimized for all devices Next StepsTest on all target pages Customize styling to match your theme Add additional features as needed Monitor user feedback and iterate Happy scanning! 🎯📱
-
Xtream Codes - Game Changer v2.2.7
- Xtream Codes - Game Changer v2.2.7
- Bicrypto - Crypto Trading Platform, Binary Trading, Investments, Blog, News & More! v6.1.6 + All Add-ons
- Bicrypto - Crypto Trading Platform, Binary Trading, Investments, Blog, News & More! v6.1.1
- Ultimate POS - Best ERP, Stock Management, Point of Sale & Invoicing application + Addons v7.0
- 📢 Download Restrictions on Some Free Files
- Grupo Chat - Chat Room & Private Chat - Video Chat & Audio Chat - AI Chat - PHP Group Chat Code v3.14 Extended License
- ShaunSocial - The PHP Social Network platform v1.5
- Qunzo – Fintech Wallet App and Web with User, Agent and Merchant Modules Including Admin Panel
- Qunzo – Fintech Wallet App and Web with User, Agent and Merchant Modules Including Admin Panel v1.1
- Bicrypto - Crypto Trading Platform, Binary Trading, Investments, Blog, News & More! v6.1.1
- Bicrypto - Crypto Trading Platform, Binary Trading, Investments, Blog, News & More! v6.1.6 + All Add-ons
- RocketWeb | Configurable Android WebView App Template v1.7.0
- RocketWeb | Configurable Android WebView App Template
- DTLive: Movie, TV Show, OTT, Live TV, Streaming Flutter App (Android, iOS, Web) with Admin Panel v1.8
Back to topAccount
Navigation
Search
Configure browser push notifications
Chrome (Android)
- Tap the lock icon next to the address bar.
- Tap Permissions → Notifications.
- Adjust your preference.
Chrome (Desktop)
- Click the padlock icon in the address bar.
- Select Site settings.
- Find Notifications and adjust your preference.
Safari (iOS 16.4+)
- Ensure the site is installed via Add to Home Screen.
- Open Settings App → Notifications.
- Find your app name and adjust your preference.
Safari (macOS)
- Go to Safari → Preferences.
- Click the Websites tab.
- Select Notifications in the sidebar.
- Find this website and adjust your preference.
Edge (Android)
- Tap the lock icon next to the address bar.
- Tap Permissions.
- Find Notifications and adjust your preference.
Edge (Desktop)
- Click the padlock icon in the address bar.
- Click Permissions for this site.
- Find Notifications and adjust your preference.
Firefox (Android)
- Go to Settings → Site permissions.
- Tap Notifications.
- Find this site in the list and adjust your preference.
Firefox (Desktop)
- Open Firefox Settings.
- Search for Notifications.
- Find this site in the list and adjust your preference.
- Xtream Codes - Game Changer v2.2.7