<?xml version="1.0"?>
<rss version="2.0"><channel><title>Ultimate POS Tutorials</title><link>https://doniaweb.com/blogs/blog/18-ultimate-pos-tutorials/</link><description><![CDATA[<p>Your comprehensive guide to mastering the <a rel="" href="https://doniaweb.com/files/file/1477-ultimate-pos-best-erp-stock-management-point-of-sale-invoicing-application-addons/">Ultimate POS</a> system. We provide detailed tutorials, technical tips, and practical steps to help you manage your sales, inventory, and customers efficiently. Whether you are a developer or a business owner, you will find everything you need here to unlock the full potential of the system.</p>]]></description><language>en</language><item><title>Multiple Barcodes for Products</title><link>https://doniaweb.com/blogs/entry/27-multiple-barcodes-for-products/</link><description><![CDATA[<h1><strong>Multiple Barcodes for Products - Complete Implementation Guide</strong></h1><h2><strong>Overview</strong></h2><p>This guide will walk you through the complete process of adding <strong>multiple barcodes</strong> functionality to your Ultimate POS products. This feature allows you to:</p><ul><li><p>Assign <strong>multiple barcodes</strong> to products (in addition to the main SKU)</p></li><li><p><strong>Search products</strong> by any of their barcodes in POS, Purchase, and Universal Search</p></li><li><p><strong>Select which barcode</strong> to print on labels</p></li><li><p>Support different <strong>barcode types</strong> (C128, C39, EAN-13, etc.) for each barcode</p></li><li><p>Add optional <strong>descriptions</strong> to identify each barcode (e.g., "Supplier barcode", "Internal code")</p></li></ul><p>This is useful when:</p><ul><li><p>Products come with manufacturer barcodes but you also use internal codes</p></li><li><p>Suppliers provide their own barcode/SKU that differs from yours</p></li><li><p>You need to support legacy barcodes after rebranding</p></li></ul><p><img src="https://ultimate-pos-tutorials.vercel.app/assets/images/multi-barcode-product-add-button-9e23ce17bae281acd8f150d2dc0c5af7.png" alt="Multi barcode product add button" class="ipsRichText__align--block" width="2238" height="320" loading="lazy"> <img src="https://ultimate-pos-tutorials.vercel.app/assets/images/multi-barcode-product-add-a8979f78f3ab433e0c64814220e9eb50.png" alt="Multi barcode product add barcode" class="ipsRichText__align--block" width="2238" height="1202" loading="lazy"> <img src="https://ultimate-pos-tutorials.vercel.app/assets/images/multi-barcode-product-label-1b306b6878cf055b4e1797f13a91cdc9.png" alt="Multi barcode product print label" class="ipsRichText__align--block" width="2238" height="604" loading="lazy"></p><h2><strong>Download</strong></h2><p><a class="ipsAttachLink" data-fileid="34410" href="https://doniaweb.com/applications/core/interface/file/attachment.php?id=34410&amp;key=6b2b30c313f345d1361749b1ba1b38c4" data-fileext="zip" rel="">multi-barcode-products.zip</a></p><h2><strong>Step 1: Database Structure</strong></h2><h3><strong>Migration</strong></h3><p>Create a new migration for the <code>product_barcodes</code> table:</p><pre spellcheck="" class="ipsCode language-bash" data-language="Bash"><code>php artisan make:migration create_product_barcodes_table
</code></pre><p><strong>Migration Content:</strong></p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>&lt;?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()
    {
        Schema::create('product_barcodes', function (Blueprint $table) {
            $table-&gt;id();
            $table-&gt;unsignedInteger('product_id');
            $table-&gt;string('barcode', 191);
            $table-&gt;string('barcode_type', 20)-&gt;default('C128');
            $table-&gt;string('description', 191)-&gt;nullable();
            $table-&gt;timestamps();

            $table-&gt;foreign('product_id')-&gt;references('id')-&gt;on('products')-&gt;onDelete('cascade');
            $table-&gt;index('barcode');
            $table-&gt;unique(['product_id', 'barcode']);
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('product_barcodes');
    }
};
</code></pre><p><strong>Direct SQL (Alternative):</strong></p><pre spellcheck="" class="ipsCode language-sql" data-language="SQL"><code>CREATE TABLE `product_barcodes` (
  `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
  `product_id` INT UNSIGNED NOT NULL,
  `barcode` VARCHAR(191) NOT NULL,
  `barcode_type` VARCHAR(20) NOT NULL DEFAULT 'C128',
  `description` VARCHAR(191) NULL,
  `created_at` TIMESTAMP NULL,
  `updated_at` TIMESTAMP NULL,

  CONSTRAINT `fk_product_barcodes_product_id`
    FOREIGN KEY (`product_id`) REFERENCES `products`(`id`) ON DELETE CASCADE,

  UNIQUE KEY `unique_product_barcode` (`product_id`, `barcode`),
  INDEX `idx_product_barcodes_barcode` (`barcode`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
</code></pre><p><strong>Run Migration:</strong></p><pre spellcheck="" class="ipsCode language-bash" data-language="Bash"><code>php artisan migrate
</code></pre><h2><strong>Step 2: Create ProductBarcode Model</strong></h2><p><strong>Create File: </strong><code>app/ProductBarcode.php</code></p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>&lt;?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class ProductBarcode extends Model
{
    /**
     * The attributes that aren't mass assignable.
     *
     * @var array
     */
    protected $guarded = ['id'];

    /**
     * Get the product that owns the barcode.
     */
    public function product()
    {
        return $this-&gt;belongsTo(\App\Product::class);
    }
}
</code></pre><h2><strong>Step 3: Update Product Model</strong></h2><p><strong>File: </strong><code>app/Product.php</code></p><p>Add the relationship method at the end of the class (before the closing <code>}</code>):</p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>/**
 * Get the additional barcodes for the product.
 */
public function barcodes()
{
    return $this-&gt;hasMany(\App\ProductBarcode::class);
}
</code></pre><h2><strong>Step 4: Update ProductUtil</strong></h2><p><strong>File: </strong><code>app/Utils/ProductUtil.php</code></p><h3><strong>4.1 Add syncProductBarcodes Method</strong></h3><p>Add this method after the <code>generateSubSku()</code> method:</p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>/**
 * Sync product barcodes.
 *
 * @param  \App\Product  $product
 * @param  array  $barcodes
 * @return void
 */
public function syncProductBarcodes($product, $barcodes)
{
    // Delete existing barcodes
    $product-&gt;barcodes()-&gt;delete();

    // Add new barcodes
    if (!empty($barcodes)) {
        foreach ($barcodes as $barcode) {
            if (!empty($barcode['barcode'])) {
                $product-&gt;barcodes()-&gt;create([
                    'barcode' =&gt; $barcode['barcode'],
                    'barcode_type' =&gt; $barcode['barcode_type'] ?? 'C128',
                    'description' =&gt; $barcode['description'] ?? null,
                ]);
            }
        }
    }
}
</code></pre><h3><strong>4.2 Update filterProduct Method for Search</strong></h3><p>In the <code>filterProduct()</code> method, find the <strong>LIKE search block</strong> and add barcode search after <code>sub_sku</code>:</p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>if (in_array('sub_sku', $search_fields)) {
    $query-&gt;orWhere('sub_sku', 'like', '%'.$search_term.'%');
}

// Search in product_barcodes table
if (in_array('sku', $search_fields) || in_array('sub_sku', $search_fields)) {
    $query-&gt;orWhereExists(function ($subquery) use ($search_term) {
        $subquery-&gt;select(\DB::raw(1))
            -&gt;from('product_barcodes')
            -&gt;whereColumn('product_barcodes.product_id', 'products.id')
            -&gt;where('product_barcodes.barcode', 'like', '%'.$search_term.'%');
    });
}
</code></pre><p>Also add the same for the <strong>EXACT search block</strong>:</p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>if (in_array('sub_sku', $search_fields)) {
    $query-&gt;orWhere('sub_sku', $search_term);
}

// Search in product_barcodes table (exact match)
if (in_array('sku', $search_fields) || in_array('sub_sku', $search_fields)) {
    $query-&gt;orWhereExists(function ($subquery) use ($search_term) {
        $subquery-&gt;select(\DB::raw(1))
            -&gt;from('product_barcodes')
            -&gt;whereColumn('product_barcodes.product_id', 'products.id')
            -&gt;where('product_barcodes.barcode', $search_term);
    });
}
</code></pre><h2><strong>Step 5: Update ProductController</strong></h2><p><strong>File: </strong><code>app/Http/Controllers/ProductController.php</code></p><h3><strong>5.1 Update store() Method</strong></h3><p>After the supplier sync code, add:</p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>//Add product barcodes
$product_barcodes = $request-&gt;input('product_barcodes', []);
if (!empty($product_barcodes)) {
    $this-&gt;productUtil-&gt;syncProductBarcodes($product, $product_barcodes);
}
</code></pre><h3><strong>5.2 Update edit() Method</strong></h3><p>Load the barcodes relationship:</p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>$product = Product::where('business_id', $business_id)
                    -&gt;with(['product_locations', 'suppliers', 'barcodes'])
                    -&gt;where('id', $id)
                    -&gt;firstOrFail();
</code></pre><h3><strong>5.3 Update update() Method</strong></h3><p>After the supplier sync code, add:</p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>//Sync product barcodes
$product_barcodes = $request-&gt;input('product_barcodes', []);
$this-&gt;productUtil-&gt;syncProductBarcodes($product, $product_barcodes);
</code></pre><h2><strong>Step 6: Update Search in Other Controllers</strong></h2><h3><strong>6.1 SellPosController</strong></h3><p><strong>File: </strong><code>app/Http/Controllers/SellPosController.php</code></p><p>Find the search block in <code>getProductSuggestion()</code> method and update:</p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>//Include search
if (!empty($term)) {
    $products-&gt;where(function ($query) use ($term) {
        $query-&gt;where('p.name', 'like', '%' . $term . '%');
        $query-&gt;orWhere('sku', 'like', '%' . $term . '%');
        $query-&gt;orWhere('sub_sku', 'like', '%' . $term . '%');
        // Search in product_barcodes table
        $query-&gt;orWhereExists(function ($subquery) use ($term) {
            $subquery-&gt;select(\DB::raw(1))
                -&gt;from('product_barcodes')
                -&gt;whereColumn('product_barcodes.product_id', 'p.id')
                -&gt;where('product_barcodes.barcode', 'like', '%' . $term . '%');
        });
    });
}
</code></pre><h3><strong>6.2 PurchaseController</strong></h3><p><strong>File: </strong><code>app/Http/Controllers/PurchaseController.php</code></p><p>Update the search in <code>getProducts()</code> method:</p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>-&gt;where(function ($query) use ($term) {
    $query-&gt;where('products.name', 'like', '%'.$term.'%');
    $query-&gt;orWhere('sku', 'like', '%'.$term.'%');
    $query-&gt;orWhere('sub_sku', 'like', '%'.$term.'%');
    // Search in product_barcodes table
    $query-&gt;orWhereExists(function ($subquery) use ($term) {
        $subquery-&gt;select(\DB::raw(1))
            -&gt;from('product_barcodes')
            -&gt;whereColumn('product_barcodes.product_id', 'products.id')
            -&gt;where('product_barcodes.barcode', 'like', '%'.$term.'%');
    });
})
</code></pre><h3><strong>6.3 UniversalSearchController</strong></h3><p><strong>File: </strong><code>app/Http/Controllers/UniversalSearchController.php</code></p><p>Update the search in <code>searchProducts()</code> method:</p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>-&gt;where(function($q) use ($query) {
    $q-&gt;where('products.name', 'like', "%{$query}%")
      -&gt;orWhere('products.sku', 'like', "%{$query}%")
      -&gt;orWhere('variations.sub_sku', 'like', "%{$query}%")
      -&gt;orWhere('products.product_description', 'like', "%{$query}%")
      // Search in product_barcodes table
      -&gt;orWhereExists(function ($subquery) use ($query) {
          $subquery-&gt;select(\DB::raw(1))
              -&gt;from('product_barcodes')
              -&gt;whereColumn('product_barcodes.product_id', 'products.id')
              -&gt;where('product_barcodes.barcode', 'like', "%{$query}%");
      });
})
</code></pre><h2><strong>Step 7: Create Barcode Input Partial View</strong></h2><p><strong>Create File: </strong><code>resources/views/product/partials/product_barcodes.blade.php</code></p><p>This partial contains only the container for barcode rows - the add button is placed next to the SKU field.</p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>&lt;div class="col-sm-12"&gt;
    &lt;div class="form-group"&gt;
        &lt;div id="product_barcodes_container"&gt;
            @if(isset($product) &amp;&amp; $product-&gt;barcodes &amp;&amp; $product-&gt;barcodes-&gt;count() &gt; 0)
                @foreach($product-&gt;barcodes as $index =&gt; $barcode)
                    &lt;div class="barcode-row row" style="margin-bottom: 10px;" data-row-index="{{ $index }}"&gt;
                        &lt;div class="col-md-4"&gt;
                            &lt;input type="text" name="product_barcodes[{{ $index }}][barcode]"
                                   class="form-control"
                                   placeholder="@lang('product.barcode')"
                                   value="{{ $barcode-&gt;barcode }}"&gt;
                        &lt;/div&gt;
                        &lt;div class="col-md-3"&gt;
                            {!! Form::select("product_barcodes[{$index}][barcode_type]",
                                $barcode_types,
                                $barcode-&gt;barcode_type,
                                ['class' =&gt; 'form-control select2', 'style' =&gt; 'width: 100%;']
                            ) !!}
                        &lt;/div&gt;
                        &lt;div class="col-md-4"&gt;
                            &lt;input type="text" name="product_barcodes[{{ $index }}][description]"
                                   class="form-control"
                                   placeholder="@lang('product.barcode_description')"
                                   value="{{ $barcode-&gt;description }}"&gt;
                        &lt;/div&gt;
                        &lt;div class="col-md-1"&gt;
                            &lt;button type="button" class="btn btn-danger btn-xs remove-barcode-row" style="padding: 2px 6px;"&gt;
                                &lt;i class="fa fa-times" style="font-size: 12px;"&gt;&lt;/i&gt;
                            &lt;/button&gt;
                        &lt;/div&gt;
                    &lt;/div&gt;
                @endforeach
            @endif
        &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;

&lt;script type="text/template" id="barcode_row_template"&gt;
    &lt;div class="barcode-row row" style="margin-bottom: 10px;" data-row-index="__INDEX__"&gt;
        &lt;div class="col-md-4"&gt;
            &lt;input type="text" name="product_barcodes[__INDEX__][barcode]"
                   class="form-control"
                   placeholder="@lang('product.barcode')"&gt;
        &lt;/div&gt;
        &lt;div class="col-md-3"&gt;
            &lt;select name="product_barcodes[__INDEX__][barcode_type]" class="form-control"&gt;
                @foreach($barcode_types as $key =&gt; $value)
                    &lt;option value="{{ $key }}"&gt;{{ $value }}&lt;/option&gt;
                @endforeach
            &lt;/select&gt;
        &lt;/div&gt;
        &lt;div class="col-md-4"&gt;
            &lt;input type="text" name="product_barcodes[__INDEX__][description]"
                   class="form-control"
                   placeholder="@lang('product.barcode_description')"&gt;
        &lt;/div&gt;
        &lt;div class="col-md-1"&gt;
            &lt;button type="button" class="btn btn-danger btn-xs remove-barcode-row" style="padding: 2px 6px;"&gt;
                &lt;i class="fa fa-times" style="font-size: 12px;"&gt;&lt;/i&gt;
            &lt;/button&gt;
        &lt;/div&gt;
    &lt;/div&gt;
&lt;/script&gt;
</code></pre><h2><strong>Step 8: Update Product Create/Edit Views</strong></h2><h3><strong>8.1 Update create.blade.php</strong></h3><p><strong>File: </strong><code>resources/views/product/create.blade.php</code></p><p>First, update the SKU field to add the plus button (like Unit field):</p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>&lt;div class="col-sm-4"&gt;
    &lt;div class="form-group"&gt;
        {!! Form::label('sku', __('product.sku') . ':') !!} @show_tooltip(__('tooltip.sku'))
        &lt;div class="input-group"&gt;
            {!! Form::text('sku', null, ['class' =&gt; 'form-control',
            'placeholder' =&gt; __('product.sku')]); !!}
            &lt;span class="input-group-btn"&gt;
                &lt;button type="button" class="btn btn-default bg-white btn-flat" id="add_barcode_row" title="@lang('product.add_barcode')"&gt;
                    &lt;i class="fa fa-plus-circle text-primary fa-lg"&gt;&lt;/i&gt;
                &lt;/button&gt;
            &lt;/span&gt;
        &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;
</code></pre><p>Then, after the <code>barcode_type</code> field and the first <code>&lt;div class="clearfix"&gt;&lt;/div&gt;</code>, add:</p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>@include('product.partials.product_barcodes')

&lt;div class="clearfix"&gt;&lt;/div&gt;
</code></pre><h3><strong>8.2 Update edit.blade.php</strong></h3><p><strong>File: </strong><code>resources/views/product/edit.blade.php</code></p><p>Update the SKU field to add the plus button:</p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>&lt;div class="col-sm-4"&gt;
  &lt;div class="form-group"&gt;
    {!! Form::label('sku', __('product.sku')  . ':*') !!} @show_tooltip(__('tooltip.sku'))
    &lt;div class="input-group"&gt;
      {!! Form::text('sku', $product-&gt;sku, ['class' =&gt; 'form-control',
      'placeholder' =&gt; __('product.sku'), 'required']); !!}
      &lt;span class="input-group-btn"&gt;
        &lt;button type="button" class="btn btn-default bg-white btn-flat" id="add_barcode_row" title="@lang('product.add_barcode')"&gt;
          &lt;i class="fa fa-plus-circle text-primary fa-lg"&gt;&lt;/i&gt;
        &lt;/button&gt;
      &lt;/span&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;
</code></pre><p>Then include the partial after the <code>barcode_type</code> field:</p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>@include('product.partials.product_barcodes')

&lt;div class="clearfix"&gt;&lt;/div&gt;
</code></pre><h2><strong>Step 9: Add JavaScript for Dynamic Barcode Rows</strong></h2><p><strong>File: </strong><code>public/js/product.js</code></p><p>Add at the end of the file:</p><pre spellcheck="" class="ipsCode language-javascript" data-language="JavaScript"><code>// Product Additional Barcodes - Add new barcode row
$(document).on("click", "#add_barcode_row", function () {
  var rowIndex = $("#product_barcodes_container .barcode-row").length;
  var template = $("#barcode_row_template").html();
  template = template.replace(/__INDEX__/g, rowIndex);
  $("#product_barcodes_container").append(template);
});

// Product Additional Barcodes - Remove barcode row
$(document).on("click", ".remove-barcode-row", function () {
  $(this).closest(".barcode-row").remove();
  // Re-index remaining rows
  $("#product_barcodes_container .barcode-row").each(function (index) {
    $(this).attr("data-row-index", index);
    $(this)
      .find("input, select")
      .each(function () {
        var name = $(this).attr("name");
        if (name) {
          name = name.replace(
            /product_barcodes\[\d+\]/,
            "product_barcodes[" + index + "]"
          );
          $(this).attr("name", name);
        }
      });
  });
});
</code></pre><h2><strong>Step 10: Label Printing with Barcode Selection</strong></h2><h3><strong>10.1 Update LabelsController</strong></h3><p><strong>File: </strong><code>app/Http/Controllers/LabelsController.php</code></p><p>Update the <code>show()</code> method to load product barcodes:</p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>//Get products for the business
$products = [];
$price_groups = [];
if ($purchase_id) {
    $products = $this-&gt;transactionUtil-&gt;getPurchaseProducts($business_id, $purchase_id);
} elseif ($product_id) {
    $products = $this-&gt;productUtil-&gt;getDetailsFromProduct($business_id, $product_id);
}

// Load product barcodes for each product
if (!empty($products)) {
    foreach ($products as $product) {
        $product-&gt;product_barcodes = \App\ProductBarcode::where('product_id', $product-&gt;product_id)-&gt;get();
    }
}
</code></pre><p>Update the <code>addProductRow()</code> method:</p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>if (! empty($product_id)) {
    $index = $request-&gt;input('row_count');
    $products = $this-&gt;productUtil-&gt;getDetailsFromProduct($business_id, $product_id, $variation_id);

    // Load product barcodes for each product
    foreach ($products as $product) {
        $product-&gt;product_barcodes = \App\ProductBarcode::where('product_id', $product-&gt;product_id)-&gt;get();
    }

    $price_groups = SellingPriceGroup::where('business_id', $business_id)
                                -&gt;active()
                                -&gt;pluck('name', 'id');

    return view('labels.partials.show_table_rows')
            -&gt;with(compact('products', 'index', 'price_groups'));
}
</code></pre><p>Update the <code>preview()</code> method to handle selected barcode:</p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>foreach ($products as $value) {
    $details = $this-&gt;productUtil-&gt;getDetailsFromVariation($value['variation_id'], $business_id, null, false);

    // ... existing code for exp_date, packing_date, lot_number ...

    // Handle selected barcode for printing
    if (! empty($value['selected_barcode_id'])) {
        $selectedBarcode = \App\ProductBarcode::find($value['selected_barcode_id']);
        if ($selectedBarcode) {
            $details-&gt;print_barcode = $selectedBarcode-&gt;barcode;
            $details-&gt;print_barcode_type = $selectedBarcode-&gt;barcode_type;
        }
    }

    // ... rest of the code ...
}
</code></pre><h3><strong>10.2 Update Label Views</strong></h3><p><strong>File: </strong><code>resources/views/labels/show.blade.php</code></p><p>Add a new table header column:</p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>&lt;th&gt;@lang('lang_v1.selling_price_group')&lt;/th&gt;
&lt;th&gt;@lang('product.select_barcode_for_label')&lt;/th&gt;
</code></pre><p><strong>File: </strong><code>resources/views/labels/partials/show_table_rows.blade.php</code></p><p>Add barcode selector column after price group:</p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>&lt;td&gt;
    {!! Form::select('products[' . $row_index . '][price_group_id]', $price_groups, null, ['class' =&gt; 'form-control', 'placeholder' =&gt; __('lang_v1.none')]); !!}
&lt;/td&gt;
&lt;td&gt;
    @php
        $displaySku = !empty($product-&gt;sub_sku) ? $product-&gt;sub_sku : (!empty($product-&gt;sku) ? $product-&gt;sku : '');
    @endphp
    &lt;select name="products[{{ $row_index }}][selected_barcode_id]" class="form-control"&gt;
        &lt;option value=""&gt;@lang('product.use_sku')@if($displaySku) ({{ $displaySku }})@endif&lt;/option&gt;
        @if(isset($product-&gt;product_barcodes) &amp;&amp; $product-&gt;product_barcodes-&gt;count() &gt; 0)
            @foreach($product-&gt;product_barcodes as $barcode)
                &lt;option value="{{ $barcode-&gt;id }}"&gt;
                    {{ $barcode-&gt;barcode }}
                    @if($barcode-&gt;description) ({{ $barcode-&gt;description }}) @endif
                &lt;/option&gt;
            @endforeach
        @endif
    &lt;/select&gt;
&lt;/td&gt;
</code></pre><p><strong>File: </strong><code>resources/views/labels/partials/preview_2.blade.php</code></p><p>Update the barcode generation section:</p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>{{-- Barcode --}}
@php
    $printBarcode = $page_product-&gt;print_barcode ?? $page_product-&gt;sub_sku;
    $printBarcodeType = $page_product-&gt;print_barcode_type ?? $page_product-&gt;barcode_type;
@endphp
&lt;img style="max-width:90% !important;height: {{$barcode_details-&gt;height*0.24}}in !important; display: block;" src="data:image/png;base64,{{DNS1D::getBarcodePNG($printBarcode, $printBarcodeType, 3,90, array(0, 0, 0), false)}}"&gt;

&lt;span style="font-size: 10px !important"&gt;
    {{$printBarcode}}
&lt;/span&gt;
</code></pre><h2><strong>Step 11: Add Language Translations</strong></h2><p><strong>File: </strong><code>lang/en/product.php</code></p><p>Add these translations:</p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>'additional_barcodes' =&gt; 'Additional Barcodes',
'add_barcode' =&gt; 'Add Barcode',
'barcode' =&gt; 'Barcode',
'barcode_description' =&gt; 'Description (optional)',
'use_sku' =&gt; 'Use SKU',
'select_barcode_for_label' =&gt; 'Barcode for Label',
</code></pre><p><strong>File: </strong><code>lang/en/tooltip.php</code></p><p>Add this tooltip:</p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>'additional_barcodes' =&gt; 'Add multiple barcodes for this product. These barcodes can be used to search and identify the product in addition to the main SKU.',
</code></pre><h2><strong>Summary</strong></h2><p>After implementing all the steps above, you will have:</p><ol><li><p><strong>Multiple barcodes per product</strong> - Each product can have unlimited additional barcodes</p></li><li><p><strong>Barcode type support</strong> - Each barcode can have its own type (C128, C39, EAN-13, etc.)</p></li><li><p><strong>Optional descriptions</strong> - Add notes like "Supplier barcode" or "Old SKU"</p></li><li><p><strong>Full search integration</strong> - Products are searchable by any of their barcodes in:</p><ul><li><p>POS product search</p></li><li><p>Purchase product search</p></li><li><p>Universal search</p></li></ul></li><li><p><strong>Label printing selection</strong> - Choose which barcode to print on labels</p></li></ol><h3><strong>Files Modified Summary</strong></h3><div class="ipsRichText__table-wrapper"><table style="min-width: 40px;"><colgroup><col style="min-width:20px;"><col style="min-width:20px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p><strong>File</strong></p></th><th colspan="1" rowspan="1"><p><strong>Changes</strong></p></th></tr><tr><td colspan="1" rowspan="1"><p><code>app/ProductBarcode.php</code></p></td><td colspan="1" rowspan="1"><p>New model</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>app/Product.php</code></p></td><td colspan="1" rowspan="1"><p>Added <code>barcodes()</code> relationship</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>app/Utils/ProductUtil.php</code></p></td><td colspan="1" rowspan="1"><p>Added <code>syncProductBarcodes()</code>, updated <code>filterProduct()</code></p></td></tr><tr><td colspan="1" rowspan="1"><p><code>app/Http/Controllers/ProductController.php</code></p></td><td colspan="1" rowspan="1"><p>Updated <code>store()</code>, <code>edit()</code>, <code>update()</code></p></td></tr><tr><td colspan="1" rowspan="1"><p><code>app/Http/Controllers/SellPosController.php</code></p></td><td colspan="1" rowspan="1"><p>Updated search query</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>app/Http/Controllers/PurchaseController.php</code></p></td><td colspan="1" rowspan="1"><p>Updated search query</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>app/Http/Controllers/UniversalSearchController.php</code></p></td><td colspan="1" rowspan="1"><p>Updated search query</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>app/Http/Controllers/LabelsController.php</code></p></td><td colspan="1" rowspan="1"><p>Added barcode selection support</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>resources/views/product/partials/product_barcodes.blade.php</code></p></td><td colspan="1" rowspan="1"><p>New partial view</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>resources/views/product/create.blade.php</code></p></td><td colspan="1" rowspan="1"><p>Include barcode partial</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>resources/views/product/edit.blade.php</code></p></td><td colspan="1" rowspan="1"><p>Include barcode partial</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>resources/views/labels/show.blade.php</code></p></td><td colspan="1" rowspan="1"><p>Added column header</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>resources/views/labels/partials/show_table_rows.blade.php</code></p></td><td colspan="1" rowspan="1"><p>Added barcode selector</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>resources/views/labels/partials/preview_2.blade.php</code></p></td><td colspan="1" rowspan="1"><p>Use selected barcode</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>public/js/product.js</code></p></td><td colspan="1" rowspan="1"><p>Added dynamic row JavaScript</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>lang/en/product.php</code></p></td><td colspan="1" rowspan="1"><p>Added translations</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>lang/en/tooltip.php</code></p></td><td colspan="1" rowspan="1"><p>Added tooltip</p></td></tr></tbody></table></div>]]></description><guid isPermaLink="false">27</guid><pubDate>Wed, 17 Dec 2025 23:52:53 +0000</pubDate></item><item><title>Product History in POS</title><link>https://doniaweb.com/blogs/entry/26-product-history-in-pos/</link><description><![CDATA[<h1><strong>Product History Button in POS Screen</strong></h1><p>Add a <strong>Product History</strong> button to the POS header that opens a modal to search products by SKU or name and view their complete history including sales, purchases, stock movements, warranty, and expiry information.</p><p><img src="https://ultimate-pos-tutorials.vercel.app/assets/images/product-history-pos-038e5bf161a79e808feda3065eb4562b.png" alt="Product History in POS" class="ipsRichText__align--block" width="2840" height="1446" loading="lazy"></p><h2><strong>Download</strong></h2><p><a class="ipsAttachLink" data-fileid="34409" href="https://doniaweb.com/applications/core/interface/file/attachment.php?id=34409&amp;key=79e439081ca23a2ae9e6fe3d853dceac" data-fileext="zip" rel="">product-history-pos.zip</a></p><h2><strong>Features</strong></h2><ul><li><p>Search by <strong>SKU</strong> or <strong>Product Name</strong> (partial match supported)</p></li><li><p>Search by <strong>Variation SKU</strong> (sub_sku)</p></li><li><p><strong>5 History Tabs:</strong></p><ul><li><p><strong>Sales History</strong> - Invoice, date, customer, qty, price, subtotal, location</p></li><li><p><strong>Purchase History</strong> - Ref no, date, supplier, qty, price, subtotal, location</p></li><li><p><strong>Stock History</strong> - Date, transaction type, ref, location, qty in/out, balance</p></li><li><p><strong>Warranty Info</strong> - Invoice, date, customer, warranty details, status (Active/Expired)</p></li><li><p><strong>Expiry Info</strong> - Lot number, expiry date, location, stock, days to expiry, status</p></li></ul></li><li><p><strong>Pagination</strong> - 8 records per page with navigation controls</p></li><li><p><strong>Bilingual Support</strong> - English and Arabic translations</p></li><li><p><strong>Permission-Based Access</strong> - Each tab respects user permissions</p></li></ul><h2><strong>Permissions</strong></h2><p>The feature includes permission-based access control for each data section:</p><div class="ipsRichText__table-wrapper"><table style="min-width: 40px;"><colgroup><col style="min-width:20px;"><col style="min-width:20px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p><strong>Section</strong></p></th><th colspan="1" rowspan="1"><p><strong>Required Permission</strong></p></th></tr><tr><td colspan="1" rowspan="1"><p><strong>Base Access</strong></p></td><td colspan="1" rowspan="1"><p><code>product.view</code></p></td></tr><tr><td colspan="1" rowspan="1"><p><strong>Sales History</strong></p></td><td colspan="1" rowspan="1"><p><code>sell.view</code> or <code>direct_sell.view</code></p></td></tr><tr><td colspan="1" rowspan="1"><p><strong>Purchase History</strong></p></td><td colspan="1" rowspan="1"><p><code>purchase.view</code></p></td></tr><tr><td colspan="1" rowspan="1"><p><strong>Stock History</strong></p></td><td colspan="1" rowspan="1"><p><code>product.view</code></p></td></tr><tr><td colspan="1" rowspan="1"><p><strong>Warranty Info</strong></p></td><td colspan="1" rowspan="1"><p><code>sell.view</code> or <code>direct_sell.view</code></p></td></tr><tr><td colspan="1" rowspan="1"><p><strong>Expiry Info</strong></p></td><td colspan="1" rowspan="1"><p><code>purchase.view</code></p></td></tr></tbody></table></div><p>Users will only see data for tabs they have permission to access. If a user doesn't have the required permission, the tab will show empty data.</p><h2><strong>Files to Modify/Create</strong></h2><div class="ipsRichText__table-wrapper"><table style="min-width: 40px;"><colgroup><col style="min-width:20px;"><col style="min-width:20px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p><strong>File</strong></p></th><th colspan="1" rowspan="1"><p><strong>Action</strong></p></th></tr><tr><td colspan="1" rowspan="1"><p><code>resources/views/layouts/partials/header-pos.blade.php</code></p></td><td colspan="1" rowspan="1"><p>Modify</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>resources/views/sale_pos/partials/product_history_modal.blade.php</code></p></td><td colspan="1" rowspan="1"><p>Create</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>resources/views/sale_pos/create.blade.php</code></p></td><td colspan="1" rowspan="1"><p>Modify</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>resources/views/sale_pos/edit.blade.php</code></p></td><td colspan="1" rowspan="1"><p>Modify</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>app/Http/Controllers/SellPosController.php</code></p></td><td colspan="1" rowspan="1"><p>Modify</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>routes/web.php</code></p></td><td colspan="1" rowspan="1"><p>Modify</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>public/js/pos.js</code></p></td><td colspan="1" rowspan="1"><p>Modify</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>lang/en/lang_v1.php</code></p></td><td colspan="1" rowspan="1"><p>Modify</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>lang/ar/lang_v1.php</code></p></td><td colspan="1" rowspan="1"><p>Modify</p></td></tr></tbody></table></div><h2><strong>Installation Steps</strong></h2><h3><strong>Step 1: Add Button to POS Header</strong></h3><p>Edit <code>resources/views/layouts/partials/header-pos.blade.php</code></p><p>Find the calculator button and add the product history button after it:</p><pre spellcheck="" class="ipsCode" data-language="blade"><code>&lt;button type="button" id="product_history_btn" title="{{ __('lang_v1.product_history') }}"
    class="tw-shadow-[rgba(17,_17,_26,_0.1)_0px_0px_16px] tw-bg-white hover:tw-bg-white/60 tw-cursor-pointer tw-border-2 tw-flex tw-items-center tw-justify-center tw-rounded-md md:tw-w-8 tw-w-auto tw-h-8 tw-text-gray-600 pull-right"
    data-toggle="modal" data-target="#product_history_modal"&gt;
    &lt;strong class="!tw-m-3"&gt;
        &lt;i class="fas fa-history fa-lg tw-text-[#9333EA] !tw-text-sm" aria-hidden="true"&gt;&lt;/i&gt;
        &lt;span class="tw-inline md:tw-hidden"&gt;{{ __('lang_v1.product_history') }}&lt;/span&gt;
    &lt;/strong&gt;
&lt;/button&gt;
</code></pre><h3><strong>Step 2: Create Modal View</strong></h3><p>Create new file <code>resources/views/sale_pos/partials/product_history_modal.blade.php</code></p><p>Copy the content from: <a rel="external nofollow" href="https://ultimate-pos-tutorials.vercel.app/assets/files/product_history_modal.blade-4505e31b29c85f3a87f93e0765aa0cae.php">product_history_modal.blade.php</a></p><h3><strong>Step 3: Include Modal in POS Pages</strong></h3><p>Edit <code>resources/views/sale_pos/create.blade.php</code></p><p>Find <code>@include('sale_pos.partials.weighing_scale_modal')</code> and add after it:</p><pre spellcheck="" class="ipsCode" data-language="blade"><code>@include('sale_pos.partials.product_history_modal')
</code></pre><p>Edit <code>resources/views/sale_pos/edit.blade.php</code></p><p>Add the same include after <code>@include('sale_pos.partials.weighing_scale_modal')</code>:</p><pre spellcheck="" class="ipsCode" data-language="blade"><code>@include('sale_pos.partials.product_history_modal')
</code></pre><h3><strong>Step 4: Add Controller Method</strong></h3><p>Edit <code>app/Http/Controllers/SellPosController.php</code></p><p>Add the <code>getProductHistory</code> method before the closing brace of the class.</p><p>Copy the content from: <a rel="external nofollow" href="https://ultimate-pos-tutorials.vercel.app/assets/files/SellPosController_getProductHistory-4c9a75c30963a5141fcd3d9c4dcb30fc.php">SellPosController_getProductHistory.php</a></p><h3><strong>Step 5: Add Route</strong></h3><p>Edit <code>routes/web.php</code></p><p>Find the line:</p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>Route::get('/sells/pos/get-featured-products/{location_id}', [SellPosController::class, 'getFeaturedProducts']);
</code></pre><p>Add after it:</p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>Route::get('/sells/pos/get-product-history', [SellPosController::class, 'getProductHistory']);
</code></pre><h3><strong>Step 6: Add JavaScript</strong></h3><p>Edit <code>public/js/pos.js</code></p><p>Add the JavaScript code at the end of the file.</p><p>Copy the content from: <a rel="external nofollow" href="https://ultimate-pos-tutorials.vercel.app/assets/files/pos_product_history-832c4722dc39986bcdef719bb8843123.js">pos_product_history.js</a></p><h3><strong>Step 7: Add Translations</strong></h3><p>Edit <code>lang/en/lang_v1.php</code> and <code>lang/ar/lang_v1.php</code></p><p>Add the translations before the closing <code>];</code></p><p>Copy the content from: <a rel="external nofollow" href="https://ultimate-pos-tutorials.vercel.app/assets/files/lang_v1_translations-e918e8fdd4e1bb49eefd017c89e7eada.php">lang_v1_translations.php</a></p><h2><strong>Usage</strong></h2><ol><li><p>Go to POS screen</p></li><li><p>Click the purple <strong>history icon</strong> button in the header</p></li><li><p>Enter product SKU or name in the search box</p></li><li><p>Press <strong>Enter</strong> or click <strong>Search</strong></p></li><li><p>View history across all tabs (Sales, Purchases, Stock, Warranty, Expiry)</p></li><li><p>Use pagination controls to navigate through records</p></li></ol><h2><strong>Screenshots</strong></h2><h3><strong>Button Location</strong></h3><p>The button appears in the POS header with a purple history icon.</p><h3><strong>Modal View</strong></h3><ul><li><p>Product info displayed at top (image, name, SKU, current stock)</p></li><li><p>5 tabs for different history types</p></li><li><p>Pagination at bottom of each table</p></li></ul><h2><strong>Customization</strong></h2><h3><strong>Change Records Per Page</strong></h3><p>Edit <code>SellPosController.php</code> line with <code>$per_page = 8;</code> to change the number of records per page.</p><h3><strong>Change Button Color</strong></h3><p>Edit the button class in <code>header-pos.blade.php</code>:</p><ul><li><p>Current: <code>tw-text-[#9333EA]</code> (purple)</p></li><li><p>Change to any Tailwind color class</p></li></ul><h2><strong>Troubleshooting</strong></h2><h3><strong>Modal Not Opening</strong></h3><ul><li><p>Check if the modal include is added to both <code>create.blade.php</code> and <code>edit.blade.php</code></p></li><li><p>Clear browser cache and refresh</p></li></ul><h3><strong>No Data Showing</strong></h3><ul><li><p>Verify the route is added correctly</p></li><li><p>Check browser console for JavaScript errors</p></li><li><p>Verify controller method is added</p></li></ul><h3><strong>Translations Not Working</strong></h3><ul><li><p>Clear Laravel cache: <code>php artisan cache:clear</code></p></li><li><p>Check translation keys match exactly</p></li></ul>]]></description><guid isPermaLink="false">26</guid><pubDate>Wed, 17 Dec 2025 23:42:08 +0000</pubDate></item><item><title>Lot Stock Validation in POS</title><link>https://doniaweb.com/blogs/entry/25-lot-stock-validation-in-pos/</link><description><![CDATA[<h1><strong>Lot/Expiry Stock Validation in POS Implementation Guide</strong></h1><p>This guide shows how to add <strong>dynamic lot/expiry stock validation</strong> to the POS screen in Ultimate POS. When products with lot numbers or expiry dates are added to POS, selecting a specific lot will now dynamically update the available stock display and validate quantity against the selected lot's stock.</p><p><img src="https://ultimate-pos-tutorials.vercel.app/assets/images/lot-product-d7538620aaf887700d789a119a580168.png" alt="POS Create page" class="ipsRichText__align--block" width="1646" height="854" loading="lazy"></p><h2><strong>Download</strong></h2><p><a class="ipsAttachLink" data-fileid="34404" href="https://doniaweb.com/applications/core/interface/file/attachment.php?id=34404&amp;key=94bd52ddc7d636521bd4956398d899db" data-fileext="zip" rel="">lot-stock-validation-pos.zip</a></p><h2><strong>The Problem</strong></h2><p>In Stock Transfer, when you select a lot for a product:</p><ul><li><p>The available stock updates dynamically to show that lot's quantity</p></li><li><p>Validation prevents entering more than available in that lot</p></li><li><p>Visual feedback shows "Current Stock: X" that updates on lot selection</p></li></ul><p><strong>However, in POS this feature was missing</strong> - the stock display was static and didn't update when selecting different lots.</p><h2><strong>The Solution</strong></h2><p>This implementation adds the same lot stock validation behavior to POS that already exists in Stock Transfer:</p><ul><li><p>Dynamic stock display that updates when lot is selected</p></li><li><p>Real-time validation against selected lot's available quantity</p></li><li><p>Stock display updates when changing sub-units</p></li><li><p>Visual feedback with "Current Stock: X" label</p></li></ul><h2><strong>Implementation Tree Structure</strong></h2><pre spellcheck="" class="ipsCode" data-language="text"><code>your-laravel-project/
├── public/
│   └── js/
│       └── pos.js                                      # </code></pre><p><code><span class="ipsEmoji" title="">📝</span></code></p><p><code> MODIFY<br>└── resources/<br>    └── views/<br>        └── sale_pos/<br>            └── product_row.blade.php                   # <span class="ipsEmoji" title="">📝</span> MODIFY<br></code></p><h3><strong>Files Overview</strong></h3><div class="ipsRichText__table-wrapper"><table style="min-width: 60px;"><colgroup><col style="min-width:20px;"><col style="min-width:20px;"><col style="min-width:20px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p><strong>File</strong></p></th><th colspan="1" rowspan="1"><p><strong>Action</strong></p></th><th colspan="1" rowspan="1"><p><strong>Description</strong></p></th></tr><tr><td colspan="1" rowspan="1"><p><code>product_row.blade.php</code></p></td><td colspan="1" rowspan="1"><p><span class="ipsEmoji" title="">📝</span> <strong>Modify</strong></p></td><td colspan="1" rowspan="1"><p>Add dynamic stock display span with <code>qty_available_text</code></p></td></tr><tr><td colspan="1" rowspan="1"><p><code>pos.js</code></p></td><td colspan="1" rowspan="1"><p><span class="ipsEmoji" title="">📝</span> <strong>Modify</strong></p></td><td colspan="1" rowspan="1"><p>Update lot change handler and sub_unit change handler</p></td></tr></tbody></table></div><h2><strong>Features</strong></h2><ul><li><p><span class="ipsEmoji" title="">✅</span> Dynamic stock display updates when selecting lots</p></li><li><p><span class="ipsEmoji" title="">✅</span> Real-time quantity validation per lot</p></li><li><p><span class="ipsEmoji" title="">✅</span> Sub-unit changes update stock display correctly</p></li><li><p><span class="ipsEmoji" title="">✅</span> Shows "Current Stock: X" label like Stock Transfer</p></li><li><p><span class="ipsEmoji" title="">✅</span> Error message when quantity exceeds lot stock</p></li><li><p><span class="ipsEmoji" title="">✅</span> Works with both POS and Direct Sell screens</p></li></ul><h2><strong>Prerequisites</strong></h2><ul><li><p>Ultimate POS with lot number/expiry date feature enabled</p></li><li><p>Products with lot numbers configured</p></li><li><p>Existing POS functionality working</p></li></ul><h2><strong>Implementation Steps</strong></h2><h3><strong>Step 1: Update POS Product Row Template</strong></h3><p>Update your <code>resources/views/sale_pos/product_row.blade.php</code> file to add the dynamic stock display.</p><p><strong>Find this section (around line 117):</strong></p><pre spellcheck="" class="ipsCode" data-language="html"><code>&lt;small class="text-muted p-1"&gt;
    @if($product-&gt;enable_stock)
    {{ @num_format($product-&gt;qty_available) }} {{$product-&gt;unit}} @lang('lang_v1.in_stock')
    @else
        --
    @endif
&lt;/small&gt;
</code></pre><p><strong>Replace with:</strong></p><pre spellcheck="" class="ipsCode" data-language="html"><code>&lt;small class="text-muted p-1" style="white-space: nowrap;"&gt;
    @if($product-&gt;enable_stock)
    @lang('report.current_stock'): &lt;span class="qty_available_text"&gt;{{ @num_format($product-&gt;qty_available) }}&lt;/span&gt; {{$product-&gt;unit}}
    @else
        --
    @endif
&lt;/small&gt;
</code></pre><p><strong>Key changes:</strong></p><ul><li><p>Added <code>qty_available_text</code> span class for dynamic updates via JavaScript</p></li><li><p>Changed label to use <code>@lang('report.current_stock')</code> for consistency with Stock Transfer</p></li><li><p>Added <code>white-space: nowrap</code> to prevent text wrapping</p></li></ul><h3><strong>Step 2: Update Lot Number Change Handler in pos.js</strong></h3><p>Update the lot number change handler in <code>public/js/pos.js</code> to update the stock display.</p><p><strong>Find this section (around line 416):</strong></p><pre spellcheck="" class="ipsCode language-javascript" data-language="JavaScript"><code>//Change max quantity rule if lot number changes
$('table#pos_table tbody').on('change', 'select.lot_number', function () {
    var qty_element = $(this)
        .closest('tr')
        .find('input.pos_quantity');

    var tr = $(this).closest('tr');
    var multiplier = 1;
    // ... rest of the code
</code></pre><p><strong>Replace the entire lot_number change handler with:</strong></p><pre spellcheck="" class="ipsCode language-javascript" data-language="JavaScript"><code>//Change max quantity rule if lot number changes
$('table#pos_table tbody').on('change', 'select.lot_number', function () {
    var qty_element = $(this)
        .closest('tr')
        .find('input.pos_quantity');

    var tr = $(this).closest('tr');
    var qty_available_el = tr.find('.qty_available_text');
    var multiplier = 1;
    var unit_name = '';
    var sub_unit_length = tr.find('select.sub_unit').length;
    if (sub_unit_length &gt; 0) {
        var select = tr.find('select.sub_unit');
        multiplier = parseFloat(select.find(':selected').data('multiplier'));
        unit_name = select.find(':selected').data('unit_name');
    }
    var allow_overselling = qty_element.data('allow-overselling');
    if ($(this).val() &amp;&amp; !allow_overselling) {
        var lot_qty = $('option:selected', $(this)).data('qty_available');
        var max_err_msg = $('option:selected', $(this)).data('msg-max');

        if (sub_unit_length &gt; 0) {
            lot_qty = lot_qty / multiplier;
            var lot_qty_formated = __number_f(lot_qty, false);
            max_err_msg = __translate('lot_max_qty_error', {
                max_val: lot_qty_formated,
                unit_name: unit_name,
            });
        }

        qty_element.attr('data-rule-max-value', lot_qty);
        qty_element.attr('data-msg-max-value', max_err_msg);

        qty_element.rules('add', {
            'max-value': lot_qty,
            messages: {
                'max-value': max_err_msg,
            },
        });

        // Update the stock display text with lot quantity
        if (qty_available_el.length) {
            qty_available_el.text(__currency_trans_from_en(lot_qty, false));
        }
    } else {
        var default_qty = qty_element.data('qty_available');
        var default_err_msg = qty_element.data('msg_max_default');
        if (sub_unit_length &gt; 0) {
            default_qty = default_qty / multiplier;
            var lot_qty_formated = __number_f(default_qty, false);
            default_err_msg = __translate('pos_max_qty_error', {
                max_val: lot_qty_formated,
                unit_name: unit_name,
            });
        }

        qty_element.attr('data-rule-max-value', default_qty);
        qty_element.attr('data-msg-max-value', default_err_msg);

        qty_element.rules('add', {
            'max-value': default_qty,
            messages: {
                'max-value': default_err_msg,
            },
        });

        // Reset the stock display text to default quantity
        if (qty_available_el.length) {
            qty_available_el.text(__currency_trans_from_en(default_qty, false));
        }
    }
    qty_element.trigger('change');
});
</code></pre><p><strong>Key additions:</strong></p><ul><li><p>Added <code>qty_available_el</code> variable to find the stock display span</p></li><li><p>Added code to update stock display when lot is selected (lines with <code>qty_available_el.text(...)</code>)</p></li><li><p>Added code to reset stock display when lot selection is cleared</p></li></ul><h3><strong>Step 3: Update Sub Unit Change Handler in pos.js</strong></h3><p>Update the sub_unit change handler to also update the stock display.</p><p><strong>Find the sub_unit change handler section (around line 1406):</strong></p><pre spellcheck="" class="ipsCode language-javascript" data-language="JavaScript"><code>if (base_max_avlbl) {
    var max_avlbl = parseFloat(base_max_avlbl) / multiplier;
    var formated_max_avlbl = __number_f(max_avlbl);
    var unit_name = selected_option.data('unit_name');
    var max_err_msg = __translate(error_msg_line, {
        max_val: formated_max_avlbl,
        unit_name: unit_name,
    });
    qty_element.attr('data-rule-max-value', max_avlbl);
    qty_element.attr('data-msg-max-value', max_err_msg);
    qty_element.rules('add', {
        'max-value': max_avlbl,
        messages: {
            'max-value': max_err_msg,
        },
    });
    qty_element.trigger('change');
}
adjustComboQty(tr);
</code></pre><p><strong>Replace with:</strong></p><pre spellcheck="" class="ipsCode language-javascript" data-language="JavaScript"><code>if (base_max_avlbl) {
    var max_avlbl = parseFloat(base_max_avlbl) / multiplier;
    var formated_max_avlbl = __number_f(max_avlbl);
    var unit_name = selected_option.data('unit_name');
    var max_err_msg = __translate(error_msg_line, {
        max_val: formated_max_avlbl,
        unit_name: unit_name,
    });
    qty_element.attr('data-rule-max-value', max_avlbl);
    qty_element.attr('data-msg-max-value', max_err_msg);
    qty_element.rules('add', {
        'max-value': max_avlbl,
        messages: {
            'max-value': max_err_msg,
        },
    });

    // Update the stock display text with available quantity
    var qty_available_el = tr.find('.qty_available_text');
    if (qty_available_el.length) {
        qty_available_el.text(__currency_trans_from_en(max_avlbl, false));
    }

    qty_element.trigger('change');
}
adjustComboQty(tr);
</code></pre><p><strong>Key addition:</strong></p><ul><li><p>Added code to update stock display when sub_unit changes</p></li></ul><h2><strong>How It Works</strong></h2><ol><li><p><strong>Product Added to POS</strong>: Shows total available stock for the product</p></li><li><p><strong>Lot Selected</strong>: Stock display updates to show only that lot's available quantity</p></li><li><p><strong>Quantity Validation</strong>: If user enters more than lot stock, validation error appears</p></li><li><p><strong>Lot Deselected</strong>: Stock display resets to total product stock</p></li><li><p><strong>Sub-unit Changed</strong>: Stock display updates with converted quantity</p></li></ol><h2><strong>Verification Steps</strong></h2><p>After implementation, verify that:</p><ol><li><p><strong>Add a product with lot/expiry</strong> to POS</p></li><li><p><strong>Check stock display</strong> shows "Current Stock: X"</p></li><li><p><strong>Select a lot</strong> from the dropdown</p></li><li><p><strong>Stock display updates</strong> to show that lot's quantity</p></li><li><p><strong>Enter quantity exceeding lot stock</strong> - validation error appears</p></li><li><p><strong>Clear lot selection</strong> - stock display resets to total</p></li><li><p><strong>Change sub-unit</strong> (if available) - stock display converts correctly</p></li></ol><h2><strong>Comparison with Stock Transfer</strong></h2><div class="ipsRichText__table-wrapper"><table style="min-width: 80px;"><colgroup><col style="min-width:20px;"><col style="min-width:20px;"><col style="min-width:20px;"><col style="min-width:20px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p><strong>Feature</strong></p></th><th colspan="1" rowspan="1"><p><strong>Stock Transfer</strong></p></th><th colspan="1" rowspan="1"><p><strong>POS (Before)</strong></p></th><th colspan="1" rowspan="1"><p><strong>POS (After)</strong></p></th></tr><tr><td colspan="1" rowspan="1"><p>Dynamic stock display</p></td><td colspan="1" rowspan="1"><p><span class="ipsEmoji" title="">✅</span></p></td><td colspan="1" rowspan="1"><p><span class="ipsEmoji" title="">❌</span></p></td><td colspan="1" rowspan="1"><p><span class="ipsEmoji" title="">✅</span></p></td></tr><tr><td colspan="1" rowspan="1"><p>Lot quantity validation</p></td><td colspan="1" rowspan="1"><p><span class="ipsEmoji" title="">✅</span></p></td><td colspan="1" rowspan="1"><p><span class="ipsEmoji" title="">✅</span></p></td><td colspan="1" rowspan="1"><p><span class="ipsEmoji" title="">✅</span></p></td></tr><tr><td colspan="1" rowspan="1"><p>Visual feedback on lot change</p></td><td colspan="1" rowspan="1"><p><span class="ipsEmoji" title="">✅</span></p></td><td colspan="1" rowspan="1"><p><span class="ipsEmoji" title="">❌</span></p></td><td colspan="1" rowspan="1"><p><span class="ipsEmoji" title="">✅</span></p></td></tr><tr><td colspan="1" rowspan="1"><p>Sub-unit stock conversion</p></td><td colspan="1" rowspan="1"><p><span class="ipsEmoji" title="">✅</span></p></td><td colspan="1" rowspan="1"><p><span class="ipsEmoji" title="">❌</span></p></td><td colspan="1" rowspan="1"><p><span class="ipsEmoji" title="">✅</span></p></td></tr><tr><td colspan="1" rowspan="1"><p>"Current Stock" label</p></td><td colspan="1" rowspan="1"><p><span class="ipsEmoji" title="">✅</span></p></td><td colspan="1" rowspan="1"><p><span class="ipsEmoji" title="">❌</span></p></td><td colspan="1" rowspan="1"><p><span class="ipsEmoji" title="">✅</span></p></td></tr></tbody></table></div><h2><strong>Troubleshooting</strong></h2><h3><strong>Issue: Stock display not updating</strong></h3><p><strong>Solution:</strong></p><ul><li><p>Check that <code>qty_available_text</code> span class exists in the blade template</p></li><li><p>Verify JavaScript changes are in the correct location</p></li><li><p>Clear browser cache and reload</p></li></ul><h3><strong>Issue: Validation not working</strong></h3><p><strong>Solution:</strong></p><ul><li><p>Ensure <code>allow_overselling</code> is not enabled in POS settings</p></li><li><p>Check that lot options have <code>data-qty_available</code> attribute</p></li></ul><h3><strong>Issue: Stock shows NaN or undefined</strong></h3><p><strong>Solution:</strong></p><ul><li><p>Verify lot numbers have proper <code>qty_available</code> data attributes</p></li><li><p>Check that <code>__currency_trans_from_en</code> function exists in common.js</p></li></ul><h2><strong>Download Implementation Files</strong></h2><p>The modified files are available for download:</p><p><a class="ipsAttachLink" data-fileid="34408" href="https://doniaweb.com/applications/core/interface/file/attachment.php?id=34408&amp;key=41eab307244f2c796d8e031223c8f89e" data-fileext="php" rel="">product_row.blade-f7a5a8e53d01beeb572cf7773ac3f304.php</a></p><p><a class="ipsAttachLink" data-fileid="34407" href="https://doniaweb.com/applications/core/interface/file/attachment.php?id=34407&amp;key=09354c2e5151be3345304031b15a2f3e" data-fileext="js" rel="">pos_sub_unit_handler-02dc0360e26b3578e92f3838dc122857.js</a></p><p><a class="ipsAttachLink" data-fileid="34406" href="https://doniaweb.com/applications/core/interface/file/attachment.php?id=34406&amp;key=85108f587d062478569ce68ffeccafee" data-fileext="js" rel="">pos_lot_number_handler-8f6bd0948b5887a4ad0975f0f1f2fe10.js</a></p><h2><strong>Conclusion</strong></h2><p>This implementation brings the POS lot/expiry stock validation to feature parity with Stock Transfer, providing users with real-time feedback on available stock per lot and preventing over-selling from specific lots.</p>]]></description><guid isPermaLink="false">25</guid><pubDate>Wed, 17 Dec 2025 23:33:46 +0000</pubDate></item><item><title>Adding Multiple Suppliers to Products</title><link>https://doniaweb.com/blogs/entry/24-adding-multiple-suppliers-to-products/</link><description><![CDATA[<h1><strong>Adding Multiple Suppliers to Products - Complete Implementation Guide</strong></h1><h2><strong>Overview</strong></h2><p>This guide will walk you through the complete process of adding <strong>multiple suppliers</strong> functionality to your Ultimate POS products. This feature allows you to:</p><ul><li><p>Assign <strong>multiple suppliers</strong> to products during creation and editing</p></li><li><p>Filter products by supplier in the products list</p></li><li><p>Bulk update suppliers for multiple products</p></li><li><p>View all suppliers information in product details</p></li><li><p>Use many-to-many relationship between products and suppliers</p></li></ul><p><img src="https://ultimate-pos-tutorials.vercel.app/assets/images/quick-add-supplier-product-51c39a74a65b185fb198294d6fa0d057.png" alt="Adding Supplier to Products - Create" class="ipsRichText__align--block" width="2046" height="866" loading="lazy"> <img src="https://ultimate-pos-tutorials.vercel.app/assets/images/multiple-supplier-index-page-table-c7551118b105a7116de494053b1cba67.png" alt="Adding Supplier to Products - Index" class="ipsRichText__align--block" width="2294" height="1426" loading="lazy"> <img src="https://ultimate-pos-tutorials.vercel.app/assets/images/multiple-supplier-index-page-bulk-4920db91f287dad030f299f2e940ae45.png" alt="Adding Supplier to Products - Bulk" class="ipsRichText__align--block" width="1900" height="954" loading="lazy"></p><h2><strong>Download Complete Files</strong></h2><p><strong>Download All Updated Files</strong></p><p><a class="ipsAttachLink" data-fileid="34403" href="https://doniaweb.com/applications/core/interface/file/attachment.php?id=34403&amp;key=d7d08bc0a117b193e68f5d0c07b811e5" data-fileext="zip" rel="">adding-supplier-to-products.zip</a></p><h2><strong>Step 1: Database Structure</strong></h2><h3><strong>Migration and Direct SQL</strong></h3><p>Ultimate POS already has a <code>contacts</code> table that stores suppliers. For <strong>multiple suppliers per product</strong>, we need to create a <strong>pivot table</strong> (many-to-many relationship).</p><p><strong>Create Migration:</strong></p><pre spellcheck="" class="ipsCode language-bash" data-language="Bash"><code>php artisan make:migration create_product_supplier_table
</code></pre><p><strong>Migration Content:</strong></p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>&lt;?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up()
    {
        Schema::create('product_supplier', function (Blueprint $table) {
            $table-&gt;id();
            $table-&gt;unsignedInteger('product_id');
            $table-&gt;unsignedInteger('supplier_id');
            $table-&gt;timestamps();

            // Foreign keys
            $table-&gt;foreign('product_id')-&gt;references('id')-&gt;on('products')-&gt;onDelete('cascade');
            $table-&gt;foreign('supplier_id')-&gt;references('id')-&gt;on('contacts')-&gt;onDelete('cascade');

            // Prevent duplicate entries
            $table-&gt;unique(['product_id', 'supplier_id']);

            // Indexes for better performance
            $table-&gt;index('product_id');
            $table-&gt;index('supplier_id');
        });
    }

    public function down()
    {
        Schema::dropIfExists('product_supplier');
    }
};
</code></pre><p><strong>Direct SQL (Alternative):</strong></p><pre spellcheck="" class="ipsCode language-sql" data-language="SQL"><code>-- Create pivot table for product-supplier relationship
CREATE TABLE `product_supplier` (
  `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
  `product_id` INT UNSIGNED NOT NULL,
  `supplier_id` INT UNSIGNED NOT NULL,
  `created_at` TIMESTAMP NULL,
  `updated_at` TIMESTAMP NULL,

  -- Foreign keys
  CONSTRAINT `fk_product_supplier_product_id`
    FOREIGN KEY (`product_id`) REFERENCES `products`(`id`) ON DELETE CASCADE,
  CONSTRAINT `fk_product_supplier_supplier_id`
    FOREIGN KEY (`supplier_id`) REFERENCES `contacts`(`id`) ON DELETE CASCADE,

  -- Unique constraint to prevent duplicates
  UNIQUE KEY `unique_product_supplier` (`product_id`, `supplier_id`),

  -- Indexes for performance
  INDEX `idx_product_supplier_product_id` (`product_id`),
  INDEX `idx_product_supplier_supplier_id` (`supplier_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
</code></pre><p><strong>Run Migration:</strong></p><pre spellcheck="" class="ipsCode language-bash" data-language="Bash"><code>php artisan migrate
</code></pre><p><strong>Note:</strong> This creates a many-to-many relationship allowing each product to have multiple suppliers and each supplier to supply multiple products.</p><h2><strong>Step 2: Model Updates</strong></h2><h3><strong>Update Product Model</strong></h3><p><strong>File: </strong><code>app/Product.php</code></p><p>Add relationship method for <strong>many-to-many</strong> relationship:</p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>/**
 * Get the suppliers for the product (many-to-many).
 */
public function suppliers()
{
    return $this-&gt;belongsToMany(
        \App\Contact::class,
        'product_supplier',
        'product_id',
        'supplier_id'
    )-&gt;withTimestamps();
}
</code></pre><p><strong>File: </strong><code>app/Contact.php</code></p><p>Add relationship method for <strong>many-to-many</strong> relationship:</p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>/**
 * Get products supplied by this supplier (many-to-many)
 */
public function products()
{
    return $this-&gt;belongsToMany(
        \App\Product::class,
        'product_supplier',
        'supplier_id',
        'product_id'
    )-&gt;withTimestamps();
}

/**
 * Get products supplied by this supplier with stock enabled
 */
public function stockProducts()
{
    return $this-&gt;belongsToMany(
        \App\Product::class,
        'product_supplier',
        'supplier_id',
        'product_id'
    )-&gt;where('products.enable_stock', 1)
     -&gt;withTimestamps();
}
</code></pre><p><strong>Important Notes:</strong></p><ul><li><p>The <code>belongsToMany</code> relationship requires the pivot table name (<code>product_supplier</code>)</p></li><li><p>Use <code>sync()</code> method to attach/detach suppliers in controllers</p></li><li><p>The <code>withTimestamps()</code> method ensures the pivot table timestamps are maintained</p></li></ul><h2><strong>Step 3: Controller Updates</strong></h2><h3><strong>ProductController Changes</strong></h3><p><strong>File: </strong><code>app/Http/Controllers/ProductController.php</code></p><h4><strong>3.1 Update index() method</strong></h4><p><strong>Add suppliers to eager loading (around line 77):</strong></p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>$query = Product::with(['media', 'suppliers'])
</code></pre><p><strong>Add supplier filter to query (around line 162):</strong></p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>$supplier_id = request()-&gt;get('supplier_id', null);
if (!empty($supplier_id)) {
    $products-&gt;whereHas('suppliers', function ($query) use ($supplier_id) {
        $query-&gt;where('contacts.id', $supplier_id);
    });
}
</code></pre><p><strong>Add suppliers column to DataTables (after selling_price column):</strong></p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>-&gt;addColumn('suppliers', function ($row) {
    $suppliers = $row-&gt;suppliers-&gt;pluck('name')-&gt;toArray();
    return implode(', ', $suppliers);
})
</code></pre><p><strong>Add suppliers dropdown for view (around line 334):</strong></p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>$suppliers = \App\Contact::suppliersDropdown($business_id);
</code></pre><p><strong>Update compact statement to include suppliers:</strong></p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>return view('product.index')
    -&gt;with(compact(
        'rack_enabled',
        'categories',
        'brands',
        'units',
        'taxes',
        'business_locations',
        'show_manufacturing_data',
        'pos_module_data',
        'is_woocommerce',
        'is_admin',
        'suppliers'
    ));
</code></pre><h4><strong>3.2 Update create() method</strong></h4><p><strong>Add suppliers dropdown (around line 150):</strong></p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>$suppliers = \App\Contact::suppliersDropdown($business_id);
</code></pre><p><strong>Update compact statement:</strong></p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>return view('product.create')
    -&gt;with(compact('categories', 'brands', 'units', 'taxes', 'barcode_types', 
    'default_profit_percent', 'tax_attributes', 'barcode_default', 'business_locations', 
    'duplicate_product', 'sub_categories', 'rack_details', 'selling_price_group_count', 
    'module_form_parts', 'product_types', 'common_settings', 'warranties', 
    'pos_module_data', 'suppliers'));
</code></pre><h4><strong>3.3 Update store() method</strong></h4><p><strong>Add supplier sync after product creation (after product locations sync, around line 645):</strong></p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>// Sync product suppliers
$supplier_ids = $request-&gt;input('supplier_ids', []);
if (!empty($supplier_ids)) {
    $product-&gt;suppliers()-&gt;sync($supplier_ids);
}
</code></pre><h4><strong>3.4 Update edit() method</strong></h4><p><strong>Add suppliers dropdown (around line 350):</strong></p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>$suppliers = \App\Contact::suppliersDropdown($business_id);
</code></pre><p><strong>Update compact statement:</strong></p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>return view('product.edit')
    -&gt;with(compact('categories', 'brands', 'units', 'sub_units', 'taxes', 'tax_attributes', 
    'barcode_types', 'product', 'sub_categories', 'default_profit_percent', 'business_locations', 
    'rack_details', 'selling_price_group_count', 'module_form_parts', 'product_types', 
    'common_settings', 'warranties', 'pos_module_data', 'alert_quantity', 'suppliers'));
</code></pre><h4><strong>3.5 Update update() method</strong></h4><p><strong>Add supplier sync after product update (after product locations sync, around line 820):</strong></p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>// Sync product suppliers
$supplier_ids = $request-&gt;input('supplier_ids', []);
$product-&gt;suppliers()-&gt;sync($supplier_ids);
</code></pre><h4><strong>3.6 Update view() method</strong></h4><p><strong>Add suppliers to with array (around line 920):</strong></p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>$product = Product::where('business_id', $business_id)
    -&gt;with(['brand', 'unit', 'category', 'sub_category', 'product_tax', 'variations', 'variations.product_variation', 'variations.group_prices', 'variations.media', 'product_locations', 'warranty', 'media', 'suppliers'])
    -&gt;findOrFail($id);
</code></pre><h4><strong>3.7 Add bulk supplier update method</strong></h4><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>public function bulkUpdateSupplier(Request $request)
{
    if (!auth()-&gt;user()-&gt;can('product.update')) {
        abort(403, 'Unauthorized action.');
    }

    try {
        $selected_products = $request-&gt;input('selected_products');
        $supplier_ids = $request-&gt;input('supplier_ids', []);
        $business_id = $request-&gt;session()-&gt;get('user.business_id');

        if (empty($selected_products)) {
            $output = [
                'success' =&gt; 0,
                'msg' =&gt; __('lang_v1.no_products_selected')
            ];
            return $output;
        }

        $product_ids = explode(',', $selected_products);

        DB::beginTransaction();

        // Get products and sync suppliers for each
        $products = Product::where('business_id', $business_id)
            -&gt;whereIn('id', $product_ids)
            -&gt;get();

        foreach ($products as $product) {
            $product-&gt;suppliers()-&gt;sync($supplier_ids);
        }

        DB::commit();

        $output = [
            'success' =&gt; 1,
            'msg' =&gt; __('lang_v1.supplier_updated_success')
        ];
    } catch (\Exception $e) {
        DB::rollBack();
        \Log::emergency("File:" . $e-&gt;getFile() . "Line:" . $e-&gt;getLine() . "Message:" . $e-&gt;getMessage());

        $output = [
            'success' =&gt; 0,
            'msg' =&gt; __('messages.something_went_wrong')
        ];
    }

    return $output;
}
</code></pre><h4><strong>3.8 Update other methods</strong></h4><p><strong>quickAdd() method (around line 1500):</strong></p><p>Add suppliers dropdown:</p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>$suppliers = \App\Contact::suppliersDropdown($business_id, false);
</code></pre><p>Update the compact statement to include suppliers:</p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>return view('product.partials.quick_add_product')
    -&gt;with(compact('categories', 'brands', 'units', 'taxes', 'barcode_types', 'default_profit_percent', 'tax_attributes', 'product_name', 'locations', 'product_for', 'enable_expiry', 'enable_lot', 'module_form_parts', 'business_locations', 'common_settings', 'warranties', 'suppliers'));
</code></pre><p><strong>saveQuickProduct() method:</strong></p><p>The supplier_ids handling is done via sync relationship, NOT through form_fields. Add this code after syncing product_locations (around line 1610):</p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>//Add product suppliers
$supplier_ids = $request-&gt;input('supplier_ids');
if (!empty($supplier_ids)) {
    $product-&gt;suppliers()-&gt;sync($supplier_ids);
}
</code></pre><p><strong>bulkEdit() and bulkUpdate() methods:</strong></p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>// Add supplier handling as shown in previous implementation guide
</code></pre><h2><strong>Step 4: Routes</strong></h2><p><strong>File: </strong><code>routes/web.php</code></p><p>Add the bulk supplier update route:</p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>Route::post('/products/bulk-update-supplier', [ProductController::class, 'bulkUpdateSupplier'])
    -&gt;name('products.bulk-update-supplier');
</code></pre><h2><strong>Step 5: View Updates</strong></h2><h3><strong>5.1 Product Index Page</strong></h3><p><strong>File: </strong><code>resources/views/product/index.blade.php</code></p><p><strong>Add supplier filter (after brand filter around line 90):</strong></p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>&lt;div class="col-md-3"&gt;
    &lt;div class="form-group"&gt;
        {!! Form::label('supplier_id', __('contact.supplier') . ':') !!}
        {!! Form::select('supplier_id', $suppliers ?? [], null, [
            'class' =&gt; 'form-control select2',
            'style' =&gt; 'width:100%',
            'id' =&gt; 'product_list_filter_supplier_id',
            'placeholder' =&gt; __('lang_v1.all'),
        ]) !!}
    &lt;/div&gt;
&lt;/div&gt;
</code></pre><p><strong>Update JavaScript DataTable configuration:</strong></p><p>Add to ajax data function:</p><pre spellcheck="" class="ipsCode language-javascript" data-language="JavaScript"><code>d.supplier_id = $('#product_list_filter_supplier_id').val();
</code></pre><p>Add to columns array (after brand column):</p><pre spellcheck="" class="ipsCode language-javascript" data-language="JavaScript"><code>{
    data: 'suppliers',
    name: 'suppliers',
    orderable: false,
    searchable: false
},
</code></pre><p>Update change event listener:</p><pre spellcheck="" class="ipsCode language-javascript" data-language="JavaScript"><code>$(document).on('change',
    '#product_list_filter_type, #product_list_filter_category_id, #product_list_filter_brand_id, #product_list_filter_supplier_id, #product_list_filter_unit_id, #product_list_filter_tax_id, #location_id, #active_state, #repair_model_id',
    function() {
        // existing code
    });
</code></pre><p><strong>Add bulk supplier modal:</strong></p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>&lt;!-- Bulk Supplier Update Modal --&gt;
&lt;div class="modal fade" id="bulk_supplier_modal" tabindex="-1" role="dialog"&gt;
    &lt;div class="modal-dialog" role="document"&gt;
        &lt;div class="modal-content"&gt;
            &lt;div class="modal-header"&gt;
                &lt;button type="button" class="close" data-dismiss="modal"&gt;
                    &lt;span&gt;&amp;times;&lt;/span&gt;
                &lt;/button&gt;
                &lt;h4 class="modal-title"&gt;@lang('lang_v1.bulk_update_supplier')&lt;/h4&gt;
            &lt;/div&gt;
            &lt;form id="bulk_supplier_form" method="POST" action="{{ route('products.bulk-update-supplier') }}"&gt;
                @csrf
                &lt;div class="modal-body"&gt;
                    &lt;div class="form-group"&gt;
                        &lt;label for="bulk_supplier_id"&gt;@lang('contact.supplier'):&lt;/label&gt;
                        &lt;select name="supplier_id" id="bulk_supplier_id" class="form-control select2" style="width: 100%;"&gt;
                            &lt;option value=""&gt;@lang('lang_v1.none')&lt;/option&gt;
                            @if(isset($suppliers))
                                @foreach($suppliers as $id =&gt; $name)
                                    @if($id != '')
                                        &lt;option value="{{ $id }}"&gt;{{ $name }}&lt;/option&gt;
                                    @endif
                                @endforeach
                            @endif
                        &lt;/select&gt;
                    &lt;/div&gt;
                    &lt;input type="hidden" name="selected_products" id="bulk_selected_products"&gt;
                &lt;/div&gt;
                &lt;div class="modal-footer"&gt;
                    &lt;button type="button" class="btn btn-default" data-dismiss="modal"&gt;@lang('messages.close')&lt;/button&gt;
                    &lt;button type="submit" class="btn btn-primary"&gt;@lang('messages.update')&lt;/button&gt;
                &lt;/div&gt;
            &lt;/form&gt;
        &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;
</code></pre><p><strong>Add bulk supplier JavaScript:</strong></p><pre spellcheck="" class="ipsCode language-javascript" data-language="JavaScript"><code>// Bulk supplier update functionality
$(document).on('click', '.bulk-update-supplier', function(e) {
    e.preventDefault();
    var selected_rows = getSelectedRows();

    if (selected_rows.length &gt; 0) {
        $('#bulk_selected_products').val(selected_rows);
        $('#bulk_supplier_modal').modal('show');
        
        $('#bulk_supplier_id').select2({
            dropdownParent: $('#bulk_supplier_modal')
        });
    } else {
        swal('@lang("lang_v1.no_row_selected")');
    }
});

$(document).on('submit', '#bulk_supplier_form', function(e) {
    e.preventDefault();
    var form = $(this);
    var data = form.serialize();

    $.ajax({
        method: 'POST',
        url: form.attr('action'),
        dataType: 'json',
        data: data,
        beforeSend: function(xhr) {
            form.find('button[type="submit"]').attr('disabled', true);
        },
        success: function(result) {
            if (result.success == 1) {
                $('#bulk_supplier_modal').modal('hide');
                toastr.success(result.msg);
                product_table.ajax.reload();
            } else {
                toastr.error(result.msg);
            }
            form.find('button[type="submit"]').attr('disabled', false);
        },
        error: function(xhr) {
            toastr.error('@lang("messages.something_went_wrong")');
            form.find('button[type="submit"]').attr('disabled', false);
        }
    });
});
</code></pre><h3><strong>5.2 Product Table</strong></h3><p><strong>File: </strong><code>resources/views/product/partials/product_list.blade.php</code></p><p><strong>Add supplier column header (after brand):</strong></p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>&lt;th&gt;@lang('lang_v1.suppliers')&lt;/th&gt;
</code></pre><p><strong>Add bulk supplier button (in tfoot after existing buttons):</strong></p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>&amp;nbsp;
&lt;button type="button" class="tw-dw-btn tw-dw-btn-xs tw-dw-btn-outline tw-dw-btn-info bulk-update-supplier"&gt;
    &lt;i class="fa fa-users"&gt;&lt;/i&gt; @lang('lang_v1.bulk_update_supplier')
&lt;/button&gt;
</code></pre><h3><strong>5.3 Product Forms</strong></h3><p><strong>Create Form (</strong><code>resources/views/product/create.blade.php</code><strong>):</strong></p><p>Add after brand field (around line 87):</p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>&lt;div class="col-sm-4"&gt;
    &lt;div class="form-group"&gt;
        {!! Form::label('supplier_ids', __('lang_v1.suppliers') . ':') !!}
        &lt;div class="input-group"&gt;
            {!! Form::select('supplier_ids[]', $suppliers, !empty($duplicate_product) ? $duplicate_product-&gt;suppliers-&gt;pluck('id')-&gt;toArray() : null, ['class' =&gt; 'form-control select2', 'multiple', 'id' =&gt; 'supplier_ids']); !!}
            &lt;span class="input-group-btn"&gt;
                &lt;button type="button" @if(!auth()-&gt;user()-&gt;can('supplier.create')) disabled @endif class="btn btn-default bg-white btn-flat quick_add_supplier_btn" title="@lang('contact.add_supplier')"&gt;&lt;i class="fa fa-plus-circle text-primary fa-lg"&gt;&lt;/i&gt;&lt;/button&gt;
            &lt;/span&gt;
        &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;
</code></pre><p><strong>Edit Form (</strong><code>resources/views/product/edit.blade.php</code><strong>):</strong></p><p>Add after brand field (around line 98):</p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>&lt;div class="col-sm-4"&gt;
    &lt;div class="form-group"&gt;
        {!! Form::label('supplier_ids', __('lang_v1.suppliers') . ':') !!}
        &lt;div class="input-group"&gt;
            {!! Form::select('supplier_ids[]', $suppliers, $product-&gt;suppliers-&gt;pluck('id')-&gt;toArray(), ['class' =&gt; 'form-control select2', 'multiple', 'id' =&gt; 'supplier_ids']); !!}
            &lt;span class="input-group-btn"&gt;
                &lt;button type="button" @if(!auth()-&gt;user()-&gt;can('supplier.create')) disabled @endif class="btn btn-default bg-white btn-flat quick_add_supplier_btn" title="@lang('contact.add_supplier')"&gt;&lt;i class="fa fa-plus-circle text-primary fa-lg"&gt;&lt;/i&gt;&lt;/button&gt;
            &lt;/span&gt;
        &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;
</code></pre><p><strong>Note:</strong> The edit form pre-populates with the product's current suppliers using <code>$product-&gt;suppliers-&gt;pluck('id')-&gt;toArray()</code></p><h3><strong>5.4 Quick Add Supplier Modal (Simplified Form)</strong></h3><p>This section implements a <strong>simplified quick add supplier modal</strong> similar to the purchase page, with only essential fields: <strong>Supplier Name</strong> (or Business Name) and <strong>Mobile Number</strong>, plus Individual/Business toggle.</p><p><strong>File: </strong><code>resources/views/product/partials/quick_add_supplier_modal.blade.php</code> (Create this new file)</p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>&lt;!-- Quick Add Supplier Modal for Product Pages --&gt;
&lt;div class="modal fade" id="quick_add_supplier_modal" tabindex="-1" role="dialog" aria-labelledby="quickAddSupplierModalLabel"&gt;
    &lt;div class="modal-dialog" role="document"&gt;
        &lt;div class="modal-content"&gt;
            {!! Form::open(['url' =&gt; action([\App\Http\Controllers\ContactController::class, 'store']), 'method' =&gt; 'post', 'id' =&gt; 'quick_add_supplier_form']) !!}

            &lt;div class="modal-header"&gt;
                &lt;button type="button" class="close" data-dismiss="modal" aria-label="Close"&gt;
                    &lt;span aria-hidden="true"&gt;&amp;times;&lt;/span&gt;
                &lt;/button&gt;
                &lt;h4 class="modal-title" id="quickAddSupplierModalLabel"&gt;@lang('contact.add_supplier')&lt;/h4&gt;
            &lt;/div&gt;

            &lt;div class="modal-body"&gt;
                {!! Form::hidden('type', 'supplier') !!}

                &lt;div class="row"&gt;
                    &lt;!-- Individual / Business Toggle --&gt;
                    &lt;div class="col-md-12" style="margin-bottom: 15px;"&gt;
                        &lt;label class="radio-inline"&gt;
                            &lt;input type="radio" name="supplier_type_radio" class="supplier_type_radio" value="individual" checked&gt;
                            @lang('lang_v1.individual')
                        &lt;/label&gt;
                        &amp;nbsp;&amp;nbsp;&amp;nbsp;
                        &lt;label class="radio-inline"&gt;
                            &lt;input type="radio" name="supplier_type_radio" class="supplier_type_radio" value="business"&gt;
                            @lang('business.business')
                        &lt;/label&gt;
                    &lt;/div&gt;

                    &lt;div class="clearfix"&gt;&lt;/div&gt;

                    &lt;!-- Individual Fields --&gt;
                    &lt;div class="col-md-6 supplier_individual_fields"&gt;
                        &lt;div class="form-group"&gt;
                            {!! Form::label('first_name', __('business.first_name') . ':*') !!}
                            &lt;div class="input-group"&gt;
                                &lt;span class="input-group-addon"&gt;
                                    &lt;i class="fa fa-user"&gt;&lt;/i&gt;
                                &lt;/span&gt;
                                {!! Form::text('first_name', null, ['class' =&gt; 'form-control', 'placeholder' =&gt; __('business.first_name'), 'id' =&gt; 'supplier_first_name']); !!}
                            &lt;/div&gt;
                        &lt;/div&gt;
                    &lt;/div&gt;
                    &lt;div class="col-md-6 supplier_individual_fields"&gt;
                        &lt;div class="form-group"&gt;
                            {!! Form::label('last_name', __('business.last_name') . ':') !!}
                            &lt;div class="input-group"&gt;
                                &lt;span class="input-group-addon"&gt;
                                    &lt;i class="fa fa-user"&gt;&lt;/i&gt;
                                &lt;/span&gt;
                                {!! Form::text('last_name', null, ['class' =&gt; 'form-control', 'placeholder' =&gt; __('business.last_name'), 'id' =&gt; 'supplier_last_name']); !!}
                            &lt;/div&gt;
                        &lt;/div&gt;
                    &lt;/div&gt;

                    &lt;!-- Business Fields (Hidden by default) --&gt;
                    &lt;div class="col-md-12 supplier_business_fields" style="display: none;"&gt;
                        &lt;div class="form-group"&gt;
                            {!! Form::label('supplier_business_name', __('business.business_name') . ':*') !!}
                            &lt;div class="input-group"&gt;
                                &lt;span class="input-group-addon"&gt;
                                    &lt;i class="fa fa-briefcase"&gt;&lt;/i&gt;
                                &lt;/span&gt;
                                {!! Form::text('supplier_business_name', null, ['class' =&gt; 'form-control', 'placeholder' =&gt; __('business.business_name'), 'id' =&gt; 'supplier_business_name_input']); !!}
                            &lt;/div&gt;
                        &lt;/div&gt;
                    &lt;/div&gt;

                    &lt;!-- Mobile Number (Always visible) --&gt;
                    &lt;div class="col-md-6"&gt;
                        &lt;div class="form-group"&gt;
                            {!! Form::label('mobile', __('contact.mobile') . ':*') !!}
                            &lt;div class="input-group"&gt;
                                &lt;span class="input-group-addon"&gt;
                                    &lt;i class="fa fa-mobile"&gt;&lt;/i&gt;
                                &lt;/span&gt;
                                {!! Form::text('mobile', null, ['class' =&gt; 'form-control', 'required', 'placeholder' =&gt; __('contact.mobile'), 'id' =&gt; 'supplier_mobile']); !!}
                            &lt;/div&gt;
                        &lt;/div&gt;
                    &lt;/div&gt;

                    &lt;!-- Email (Optional) --&gt;
                    &lt;div class="col-md-6"&gt;
                        &lt;div class="form-group"&gt;
                            {!! Form::label('email', __('business.email') . ':') !!}
                            &lt;div class="input-group"&gt;
                                &lt;span class="input-group-addon"&gt;
                                    &lt;i class="fa fa-envelope"&gt;&lt;/i&gt;
                                &lt;/span&gt;
                                {!! Form::email('email', null, ['class' =&gt; 'form-control', 'placeholder' =&gt; __('business.email'), 'id' =&gt; 'supplier_email']); !!}
                            &lt;/div&gt;
                        &lt;/div&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
            &lt;/div&gt;

            &lt;div class="modal-footer"&gt;
                &lt;button type="button" class="tw-dw-btn tw-dw-btn-neutral tw-text-white" data-dismiss="modal"&gt;@lang('messages.close')&lt;/button&gt;
                &lt;button type="submit" class="tw-dw-btn tw-dw-btn-primary tw-text-white"&gt;@lang('messages.save')&lt;/button&gt;
            &lt;/div&gt;

            {!! Form::close() !!}
        &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;
</code></pre><p><strong>Include the modal in product create/edit pages:</strong></p><p>Add at the bottom of <code>resources/views/product/create.blade.php</code> and <code>resources/views/product/edit.blade.php</code>:</p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>@include('product.partials.quick_add_supplier_modal')
</code></pre><h3><strong>5.5 JavaScript for Quick Add Supplier</strong></h3><p>Add the following JavaScript to handle the quick add supplier modal. You can add this to <code>public/js/product.js</code> or include it in the product pages.</p><p><strong>Add to </strong><code>public/js/product.js</code><strong> or inline in product views:</strong></p><pre spellcheck="" class="ipsCode language-javascript" data-language="JavaScript"><code>// Quick Add Supplier Modal - Toggle Individual/Business fields
$(document).on('change', '.supplier_type_radio', function() {
    var selectedType = $(this).val();

    if (selectedType === 'individual') {
        $('.supplier_individual_fields').show();
        $('.supplier_business_fields').hide();
        // Update required attributes
        $('#supplier_first_name').prop('required', true);
        $('#supplier_business_name_input').prop('required', false);
    } else if (selectedType === 'business') {
        $('.supplier_individual_fields').hide();
        $('.supplier_business_fields').show();
        // Update required attributes
        $('#supplier_first_name').prop('required', false);
        $('#supplier_business_name_input').prop('required', true);
    }
});

// Open Quick Add Supplier Modal
$(document).on('click', '.quick_add_supplier_btn', function(e) {
    e.preventDefault();
    // Reset form
    $('#quick_add_supplier_form')[0].reset();
    // Reset to individual by default
    $('input[name="supplier_type_radio"][value="individual"]').prop('checked', true).trigger('change');
    // Show modal
    $('#quick_add_supplier_modal').modal('show');
});

// Handle Quick Add Supplier Form Submission
$(document).on('submit', '#quick_add_supplier_form', function(e) {
    e.preventDefault();

    var form = $(this);
    var submitBtn = form.find('button[type="submit"]');
    var formData = form.serialize();

    $.ajax({
        method: 'POST',
        url: form.attr('action'),
        data: formData,
        dataType: 'json',
        beforeSend: function() {
            submitBtn.prop('disabled', true);
            submitBtn.html('&lt;i class="fa fa-spinner fa-spin"&gt;&lt;/i&gt; ' + LANG.please_wait);
        },
        success: function(result) {
            if (result.success === true) {
                // Close modal
                $('#quick_add_supplier_modal').modal('hide');

                // Show success message
                toastr.success(result.msg);

                // Add the new supplier to the supplier_ids select2 dropdown
                var newOption = new Option(result.data.name, result.data.id, true, true);
                $('#supplier_ids').append(newOption).trigger('change');

                // Reset form
                form[0].reset();
            } else {
                toastr.error(result.msg);
            }
        },
        error: function(xhr) {
            var errorMsg = xhr.responseJSON &amp;&amp; xhr.responseJSON.message
                ? xhr.responseJSON.message
                : LANG.something_went_wrong;
            toastr.error(errorMsg);
        },
        complete: function() {
            submitBtn.prop('disabled', false);
            submitBtn.html(LANG.save);
        }
    });
});

// Initialize modal on page load
$(document).ready(function() {
    // Ensure individual fields are shown by default
    $('.supplier_individual_fields').show();
    $('.supplier_business_fields').hide();
});
</code></pre><h3><strong>5.6 Controller Update for AJAX Response</strong></h3><p>Update the <code>ContactController@store</code> method to return JSON response when called via AJAX:</p><p><strong>File: </strong><code>app/Http/Controllers/ContactController.php</code></p><p>Find the <code>store()</code> method and update the success response to handle AJAX requests:</p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>// At the end of the store() method, before the redirect, add:
if ($request-&gt;ajax()) {
    // Get the display name for the supplier
    $displayName = $contact-&gt;supplier_business_name
        ? $contact-&gt;supplier_business_name
        : trim($contact-&gt;prefix . ' ' . $contact-&gt;first_name . ' ' . $contact-&gt;middle_name . ' ' . $contact-&gt;last_name);

    return response()-&gt;json([
        'success' =&gt; true,
        'msg' =&gt; __('contact.added_success'),
        'data' =&gt; [
            'id' =&gt; $contact-&gt;id,
            'name' =&gt; $displayName,
            'mobile' =&gt; $contact-&gt;mobile,
            'type' =&gt; $contact-&gt;type
        ]
    ]);
}
</code></pre><p><strong>Full example of modified store() method ending:</strong></p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>// ... existing store logic ...

DB::commit();

$output = [
    'success' =&gt; true,
    'data' =&gt; $contact,
    'msg' =&gt; __('contact.added_success')
];

// Handle AJAX requests (for quick add modals)
if ($request-&gt;ajax()) {
    $displayName = $contact-&gt;supplier_business_name
        ? $contact-&gt;supplier_business_name
        : trim($contact-&gt;prefix . ' ' . $contact-&gt;first_name . ' ' . $contact-&gt;middle_name . ' ' . $contact-&gt;last_name);

    return response()-&gt;json([
        'success' =&gt; true,
        'msg' =&gt; __('contact.added_success'),
        'data' =&gt; [
            'id' =&gt; $contact-&gt;id,
            'name' =&gt; $displayName,
            'mobile' =&gt; $contact-&gt;mobile,
            'type' =&gt; $contact-&gt;type
        ]
    ]);
}

// Existing redirect logic for non-AJAX requests
return redirect('contacts')-&gt;with('status', $output);
</code></pre><h3><strong>5.7 Product View Modal</strong></h3><p><strong>File: </strong><code>resources/views/product/view-modal.blade.php</code></p><p>Add supplier information after brand:</p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>&lt;b&gt;@lang('contact.supplier'): &lt;/b&gt;
@if($product-&gt;suppliers-&gt;isNotEmpty())
    {{ $product-&gt;suppliers-&gt;pluck('name')-&gt;implode(', ') }}
@else
    --
@endif
&lt;br&gt;
</code></pre><p><strong>Note:</strong> Since a product can have multiple suppliers, we display them as a comma-separated list.</p><h3><strong>5.8 Quick Add Product Form</strong></h3><p><strong>File: </strong><code>resources/views/product/partials/quick_add_product.blade.php</code></p><p>Add after brand field (around line 57):</p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>&lt;div class="col-sm-4"&gt;
    &lt;div class="form-group"&gt;
        {!! Form::label('supplier_ids', __('contact.supplier') . ':') !!}
        &lt;div class="input-group"&gt;
            {!! Form::select('supplier_ids[]', $suppliers, null, ['class' =&gt; 'form-control select2', 'multiple', 'id' =&gt; 'quick_product_supplier_ids']); !!}
            &lt;span class="input-group-btn"&gt;
                &lt;button type="button" @if(!auth()-&gt;user()-&gt;can('supplier.create')) disabled @endif class="btn btn-default bg-white btn-flat quick_add_supplier_btn" title="@lang('contact.add_supplier')"&gt;&lt;i class="fa fa-plus-circle text-primary fa-lg"&gt;&lt;/i&gt;&lt;/button&gt;
            &lt;/span&gt;
        &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;
</code></pre><p><strong>Note:</strong> The quick add supplier modal will also work in this form since the JavaScript is event-delegated.</p><p><strong>Important Notes:</strong></p><ul><li><p>Use <code>supplier_ids[]</code> (array) for multiple supplier selection</p></li><li><p>Add <code>'multiple' =&gt; true</code> to enable multi-select</p></li><li><p>The quick add supplier button uses the same simplified modal</p></li><li><p>The field name must match what <code>saveQuickProduct()</code> expects</p></li></ul><h2><strong>Step 6: Language Files</strong></h2><p><strong>File: </strong><code>resources/lang/en/lang_v1.php</code></p><p>Add these translations:</p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>'bulk_update_supplier' =&gt; 'Bulk Update Supplier',
'supplier_updated_success' =&gt; 'Supplier updated successfully',
'no_products_selected' =&gt; 'No products selected',
</code></pre><h2><strong>Step 7: Testing</strong></h2><h3><strong>7.1 Basic Functionality</strong></h3><ol><li><p><strong>Create Product</strong>: Test creating a product with <strong>multiple supplier selection</strong></p></li><li><p><strong>Edit Product</strong>: Test editing existing products and changing/adding/removing suppliers</p></li><li><p><strong>View Product</strong>: Verify <strong>all suppliers</strong> appear in product details modal (comma-separated)</p></li><li><p><strong>Filter Products</strong>: Test filtering products by supplier in the index page</p></li></ol><h3><strong>7.2 Bulk Operations</strong></h3><ol><li><p><strong>Select Multiple Products</strong>: Use checkboxes to select multiple products</p></li><li><p><strong>Bulk Update Supplier</strong>: Click bulk supplier button and assign <strong>multiple suppliers</strong></p></li><li><p><strong>Verify Changes</strong>: Check that all selected products have the new suppliers (replaces existing)</p></li></ol><h3><strong>7.3 Edge Cases</strong></h3><ol><li><p><strong>No Supplier Selected</strong>: Test with empty/null supplier values (should work fine)</p></li><li><p><strong>Invalid Supplier</strong>: Test with non-existent supplier IDs</p></li><li><p><strong>Permission Testing</strong>: Test with different user permissions</p></li><li><p><strong>Large Datasets</strong>: Test with many products and many suppliers selected</p></li><li><p><strong>Duplicate Prevention</strong>: Try to assign the same supplier twice (should be prevented by unique constraint)</p></li></ol><h2><strong>Step 8: Optional Enhancements</strong></h2><h3><strong>8.1 Supplier Statistics</strong></h3><p>Add supplier-based reports showing:</p><ul><li><p>Products per supplier</p></li><li><p>Stock levels by supplier</p></li><li><p>Purchase history by supplier</p></li></ul><h3><strong>8.2 Advanced Filtering</strong></h3><p>Add more complex filtering options:</p><ul><li><p>Products without suppliers</p></li><li><p>Supplier-based stock alerts</p></li><li><p>Multi-supplier selection</p></li></ul><h3><strong>8.3 Import/Export</strong></h3><p>Update product import/export to include supplier information:</p><ul><li><p>CSV import with supplier names</p></li><li><p>Excel export with supplier details</p></li></ul><h2><strong>Conclusion</strong></h2><p>You have successfully implemented <strong>multiple suppliers</strong> functionality for products in Ultimate POS. This feature provides:</p><ul><li><p><span class="ipsEmoji" title="">✅</span> <strong>Multiple supplier assignment</strong> during product creation/editing</p></li><li><p><span class="ipsEmoji" title="">✅</span> <strong>Many-to-many relationship</strong> between products and suppliers</p></li><li><p><span class="ipsEmoji" title="">✅</span> Supplier filtering in product listings</p></li><li><p><span class="ipsEmoji" title="">✅</span> <strong>Bulk supplier updates</strong> for multiple products (with multiple supplier selection)</p></li><li><p><span class="ipsEmoji" title="">✅</span> <strong>All suppliers information</strong> displayed in product details</p></li><li><p><span class="ipsEmoji" title="">✅</span> Integration with existing supplier management</p></li><li><p><span class="ipsEmoji" title="">✅</span> <strong>Pivot table</strong> (<code>product_supplier</code>) for efficient relationship management</p></li><li><p><span class="ipsEmoji" title="">✅</span> Duplicate prevention via unique constraints</p></li><li><p><span class="ipsEmoji" title="">✅</span> <strong>Quick Add Supplier modal</strong> with simplified form (Individual/Business toggle, Name, Mobile)</p></li></ul><p>The implementation follows Ultimate POS conventions and maintains compatibility with existing features while supporting the flexibility of multiple suppliers per product.</p><h3><strong>Key Differences from Single Supplier Implementation</strong></h3><ul><li><p>Uses <code>belongsToMany</code> instead of <code>belongsTo</code>/<code>hasMany</code></p></li><li><p>Requires pivot table (<code>product_supplier</code>)</p></li><li><p>Form fields use <code>supplier_ids[]</code> instead of <code>supplier_id</code></p></li><li><p>Controllers use <code>sync()</code> method for relationship management</p></li><li><p>Views display comma-separated supplier names</p></li></ul><h3><strong>Quick Add Supplier Features</strong></h3><p>The simplified quick add supplier modal (similar to <code>/purchases/create</code>) provides:</p><ul><li><p><strong>Individual/Business toggle</strong> - Switch between individual supplier (First Name, Last Name) and business supplier (Business Name)</p></li><li><p><strong>Minimum required fields</strong> - Only Supplier Name and Mobile Number are required</p></li><li><p><strong>Optional Email field</strong> - For additional contact information</p></li><li><p><strong>AJAX submission</strong> - Seamless addition without page reload</p></li><li><p><strong>Auto-select</strong> - Newly added supplier is automatically selected in the dropdown</p></li></ul>]]></description><guid isPermaLink="false">24</guid><pubDate>Wed, 17 Dec 2025 23:19:22 +0000</pubDate></item><item><title>Vehicle Management System - Complete Implementation Guide</title><link>https://doniaweb.com/blogs/entry/23-vehicle-management-system-complete-implementation-guide/</link><description><![CDATA[<h1><strong>Vehicle Management System for Ultimate POS</strong></h1><p>This comprehensive guide covers the complete implementation of a Vehicle Management system in Ultimate POS with Laravel, including CRUD operations, transaction integration, and reporting capabilities.</p><h2><strong>Overview</strong></h2><p>The Vehicle Management system provides:</p><ul><li><p><span class="ipsEmoji" title="">✅</span> Complete CRUD operations for vehicles</p></li><li><p><span class="ipsEmoji" title="">✅</span> Detailed vehicle information tracking (model, license, insurance, etc.)</p></li><li><p><span class="ipsEmoji" title="">✅</span> Vehicle assignment to sales and purchase transactions</p></li><li><p><span class="ipsEmoji" title="">✅</span> Comprehensive vehicle reports and analytics</p></li><li><p><span class="ipsEmoji" title="">✅</span> Excel export functionality</p></li><li><p><span class="ipsEmoji" title="">✅</span> Multi-business support with data isolation</p></li><li><p><span class="ipsEmoji" title="">✅</span> Permission-based access control</p></li></ul><h2></h2><p></p><h2><strong>Screenshots</strong></h2><ul><li><p>Vehicle Index Page</p></li></ul><p><img src="https://ultimate-pos-tutorials.vercel.app/assets/images/vehicule-index-82748615b6d3d0ecfefe4cdb6ce9f639.png" alt="Vehicle Index Page" class="ipsRichText__align--block" width="2070" height="1348" loading="lazy"></p><ul><li><p>Vehicle Create Modal</p></li></ul><p><img src="https://ultimate-pos-tutorials.vercel.app/assets/images/vehicule-create-d2653867df7f93ac4284ea448af4dd38.png" alt="Vehicle Create Modal" class="ipsRichText__align--block" width="1464" height="726" loading="lazy"></p><ul><li><p>Vehicle Edit Modal</p></li></ul><p><img src="https://ultimate-pos-tutorials.vercel.app/assets/images/vehicule-edit-dbfdc4715ec62fb48193eaf4d42c1279.png" alt="Vehicle Edit Modal" class="ipsRichText__align--block" width="1454" height="706" loading="lazy"></p><ul><li><p>Vehicle View Modal</p></li></ul><p><img src="https://ultimate-pos-tutorials.vercel.app/assets/images/vehicule-view-8fc432f0d6267893e6b8cc2612ce5715.png" alt="Vehicle View Modal" class="ipsRichText__align--block" width="1828" height="894" loading="lazy"></p><ul><li><p>Vehicle Load Reports</p></li></ul><p><img src="https://ultimate-pos-tutorials.vercel.app/assets/images/vehicule-view-8fc432f0d6267893e6b8cc2612ce5715.png" alt="vehicle transactions and loads" class="ipsRichText__align--block" width="1828" height="894" loading="lazy"></p><ul><li><p>Sell list</p></li></ul><p><img src="https://ultimate-pos-tutorials.vercel.app/assets/images/vehicule-sell-5fca983685b2f9bad9e90a5eb9d05c28.png" alt="vehicle in sells list index" class="ipsRichText__align--block" width="2200" height="1218" loading="lazy"></p><ul><li><p>Sell create page</p></li></ul><p><img src="https://ultimate-pos-tutorials.vercel.app/assets/images/vehicule-sell-create-19719f8e4bcc4bb70866a70b67b317dd.png" alt="vehicle in sells create page" class="ipsRichText__align--block" width="2310" height="978" loading="lazy"></p><ul><li><p>POS create page</p></li></ul><p><img src="https://ultimate-pos-tutorials.vercel.app/assets/images/vehicule-pos-create-4d74827bed68e816a905f070d17008fb.png" alt="vehicle in POS create page" class="ipsRichText__align--block" width="1644" height="386" loading="lazy"></p><h2><strong>Download Template for Phase 1</strong></h2><p><a class="ipsAttachLink" data-fileid="34402" href="https://doniaweb.com/applications/core/interface/file/attachment.php?id=34402&amp;key=ce21df329c7e06872b4abaece89d232f" data-fileext="zip" rel="">vehicle-management-system-all-files.zip</a></p><hr><h2><strong>Database Setup</strong></h2><h3><strong>Single Comprehensive Migration</strong></h3><p><strong>File:</strong> <code>database/migrations/2025_09_29_180000_create_vehicle_management_system.php</code></p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>&lt;?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-&gt;id();
            $table-&gt;integer('business_id')-&gt;unsigned();
            $table-&gt;foreign('business_id')-&gt;references('id')-&gt;on('business')-&gt;onDelete('cascade');

            // Basic Information
            $table-&gt;string('vehicle_number')-&gt;unique();
            $table-&gt;string('driver_name');
            $table-&gt;string('vehicle_type');
            $table-&gt;enum('status', ['active', 'inactive'])-&gt;default('active');

            // Vehicle Details
            $table-&gt;string('vehicle_model')-&gt;nullable();
            $table-&gt;string('license_plate')-&gt;nullable();
            $table-&gt;string('vin_number')-&gt;nullable();
            $table-&gt;year('year')-&gt;nullable();
            $table-&gt;string('color')-&gt;nullable();
            $table-&gt;string('fuel_type')-&gt;nullable();
            $table-&gt;string('engine_capacity')-&gt;nullable();

            // Operational Data
            $table-&gt;decimal('current_mileage', 10, 2)-&gt;nullable();
            $table-&gt;date('purchase_date')-&gt;nullable();
            $table-&gt;decimal('purchase_price', 22, 4)-&gt;nullable();
            $table-&gt;date('insurance_expiry')-&gt;nullable();
            $table-&gt;date('registration_expiry')-&gt;nullable();
            $table-&gt;date('last_service_date')-&gt;nullable();
            $table-&gt;date('next_service_due')-&gt;nullable();

            // Performance Tracking
            $table-&gt;decimal('fuel_efficiency', 8, 2)-&gt;nullable()-&gt;comment('L/100km');
            $table-&gt;decimal('max_load_capacity', 10, 2)-&gt;nullable()-&gt;comment('in tons');
            $table-&gt;decimal('daily_rate', 22, 4)-&gt;nullable();
            $table-&gt;decimal('cost_per_km', 22, 4)-&gt;nullable();

            // Contact &amp; Assignment
            $table-&gt;string('driver_phone')-&gt;nullable();
            $table-&gt;string('driver_license')-&gt;nullable();
            $table-&gt;string('assigned_route')-&gt;nullable();
            $table-&gt;string('home_location')-&gt;nullable();

            // System Fields
            $table-&gt;integer('created_by')-&gt;unsigned();
            $table-&gt;timestamps();
            $table-&gt;softDeletes();

            // Indexes
            $table-&gt;index('business_id');
            $table-&gt;index('status');
            $table-&gt;index('vehicle_type');
        });

        // Add vehicle_id to transactions table
        Schema::table('transactions', function (Blueprint $table) {
            $table-&gt;unsignedBigInteger('vehicle_id')-&gt;nullable()-&gt;after('business_id');
            $table-&gt;foreign('vehicle_id')
                  -&gt;references('id')
                  -&gt;on('vehicles')
                  -&gt;onDelete('set null');
            $table-&gt;index('vehicle_id');
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        // Remove vehicle_id from transactions table
        Schema::table('transactions', function (Blueprint $table) {
            $table-&gt;dropForeign(['vehicle_id']);
            $table-&gt;dropIndex(['vehicle_id']);
            $table-&gt;dropColumn('vehicle_id');
        });

        // Drop vehicles table
        Schema::dropIfExists('vehicles');
    }
};
</code></pre><p><strong>Run Migration:</strong></p><pre spellcheck="" class="ipsCode language-bash" data-language="Bash"><code># 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
</code></pre><p><strong>Why Single Migration Approach:</strong></p><ul><li><p><span class="ipsEmoji" title="">✅</span> <strong>Simple:</strong> Everything in one file</p></li><li><p><span class="ipsEmoji" title="">✅</span> <strong>Complete:</strong> Full table structure from the start</p></li><li><p><span class="ipsEmoji" title="">✅</span> <strong>Clean:</strong> No dependency management needed</p></li><li><p><span class="ipsEmoji" title="">✅</span> <strong>Fast:</strong> One-step deployment</p></li><li><p><span class="ipsEmoji" title="">✅</span> <strong>Easy Rollback:</strong> Single rollback removes everything cleanly</p></li></ul><p><strong>What This Creates:</strong></p><ul><li><p><span class="ipsEmoji" title="">✅</span> Complete vehicles table with all 28 fields</p></li><li><p><span class="ipsEmoji" title="">✅</span> vehicle_id link to transactions table</p></li><li><p><span class="ipsEmoji" title="">✅</span> Proper indexes for performance</p></li><li><p><span class="ipsEmoji" title="">✅</span> Foreign key with SET NULL on delete</p></li><li><p><span class="ipsEmoji" title="">✅</span> Soft deletes support</p></li><li><p><span class="ipsEmoji" title="">✅</span> Business-level data isolation</p></li></ul><h3><strong>Migration Management</strong></h3><p><strong>To check migration status:</strong></p><pre spellcheck="" class="ipsCode language-bash" data-language="Bash"><code>php artisan migrate:status | grep vehicle
</code></pre><p><strong>To rollback the migration:</strong></p><pre spellcheck="" class="ipsCode language-bash" data-language="Bash"><code># Rollback the vehicle management system
php artisan migrate:rollback --step=1
</code></pre><p><strong>To rollback and re-run:</strong></p><pre spellcheck="" class="ipsCode language-bash" data-language="Bash"><code>php artisan migrate:refresh --step=1
</code></pre><p><strong>Current Migration File:</strong></p><ul><li><p><code>2025_09_29_180000_create_vehicle_management_system.php</code> - Complete vehicle management system</p></li></ul><hr><h2><strong>Model Implementation</strong></h2><p><strong>File:</strong> <code>app/Vehicle.php</code></p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>&lt;?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 &amp; Assignment
        'driver_phone', 'driver_license', 'assigned_route', 'home_location'
    ];

    protected $casts = [
        'purchase_date' =&gt; 'date',
        'insurance_expiry' =&gt; 'date',
        'registration_expiry' =&gt; 'date',
        'last_service_date' =&gt; 'date',
        'next_service_due' =&gt; 'date',
        'current_mileage' =&gt; 'decimal:2',
        'purchase_price' =&gt; 'decimal:2',
        'fuel_efficiency' =&gt; 'decimal:2',
        'max_load_capacity' =&gt; 'decimal:2',
        'daily_rate' =&gt; 'decimal:2',
        'cost_per_km' =&gt; 'decimal:2',
    ];

    /**
     * Get vehicles for dropdown
     */
    public static function forDropdown($business_id, $show_none = false)
    {
        $vehicles = Vehicle::where('business_id', $business_id)
            -&gt;where('status', 'active')
            -&gt;orderBy('vehicle_number', 'asc')
            -&gt;pluck('vehicle_number', 'id');

        if ($show_none) {
            $vehicles-&gt;prepend(__('lang_v1.none'), '');
        }

        return $vehicles;
    }

    /**
     * Accessor: Display name
     */
    public function getDisplayNameAttribute()
    {
        return $this-&gt;vehicle_number . ' (' . $this-&gt;driver_name . ')';
    }

    /**
     * Accessor: Full details
     */
    public function getFullDetailsAttribute()
    {
        $details = $this-&gt;vehicle_number;
        if ($this-&gt;vehicle_model) {
            $details .= ' - ' . $this-&gt;vehicle_model;
        }
        if ($this-&gt;license_plate) {
            $details .= ' (' . $this-&gt;license_plate . ')';
        }
        return $details;
    }

    /**
     * Scopes for filtering
     */
    public function scopeActive($query)
    {
        return $query-&gt;where('status', 'active');
    }

    public function scopeByFuelType($query, $fuel_type)
    {
        return $query-&gt;where('fuel_type', $fuel_type);
    }

    public function scopeInsuranceExpiringSoon($query, $days = 30)
    {
        return $query-&gt;whereDate('insurance_expiry', '&lt;=', now()-&gt;addDays($days))
                    -&gt;whereDate('insurance_expiry', '&gt;=', now());
    }
}
</code></pre><hr><h2><strong>Controller Implementation</strong></h2><p><strong>File:</strong> <code>app/Http/Controllers/VehicleController.php</code></p><p>The controller includes:</p><ul><li><p><span class="ipsEmoji" title="">✅</span> Index with DataTables</p></li><li><p><span class="ipsEmoji" title="">✅</span> Create/Store methods</p></li><li><p><span class="ipsEmoji" title="">✅</span> Edit/Update methods</p></li><li><p><span class="ipsEmoji" title="">✅</span> Delete (soft delete)</p></li><li><p><span class="ipsEmoji" title="">✅</span> Show (detail view)</p></li><li><p><span class="ipsEmoji" title="">✅</span> Reports method</p></li><li><p><span class="ipsEmoji" title="">✅</span> Excel export</p></li></ul><p>See <code>docs/Vehicle-Management-System/files/VehicleController.php</code> for complete code.</p><hr><h2><strong>View Templates</strong></h2><p>All view files are located in <code>resources/views/vehicle/</code>:</p><h3><strong>1. Index Page (</strong><code>index.blade.php</code><strong>)</strong></h3><ul><li><p>Lists all vehicles in DataTable</p></li><li><p>Search and filter functionality</p></li><li><p>Action buttons (Edit, Delete, View)</p></li></ul><h3><strong>2. Create Modal (</strong><code>create.blade.php</code><strong>)</strong></h3><ul><li><p>Full vehicle form with all fields</p></li><li><p>Validation</p></li><li><p>Ajax submission</p></li></ul><h3><strong>3. Edit Modal (</strong><code>edit.blade.php</code><strong>)</strong></h3><ul><li><p>Pre-populated form</p></li><li><p>Update functionality</p></li></ul><h3><strong>4. Show Page (</strong><code>show.blade.php</code><strong>)</strong></h3><ul><li><p>Detailed vehicle information</p></li><li><p>Related transactions</p></li></ul><h3><strong>5. Reports Page (</strong><code>reports.blade.php</code><strong>)</strong></h3><ul><li><p>Vehicle analytics</p></li><li><p>Transaction history</p></li><li><p>Excel export</p></li></ul><p>All view files are available in <code>docs/Vehicle-Management-System/files/vehicle/</code></p><hr><h2><strong>Transaction Integration</strong></h2><h3><strong>Sales (POS) Integration</strong></h3><p><strong>1. Add vehicle selection to POS create form:</strong></p><p>Edit <code>resources/views/sale_pos/partials/pos_form.blade.php</code>:</p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>&lt;!-- Vehicle Selection --&gt;
&lt;div class="col-md-4 col-sm-6"&gt;
    &lt;div class="form-group"&gt;
        &lt;label&gt;@lang('lang_v1.vehicle'):&lt;/label&gt;
        &lt;div class="input-group"&gt;
            &lt;span class="input-group-addon"&gt;
                &lt;i class="fa fa-car"&gt;&lt;/i&gt;
            &lt;/span&gt;
            {!! Form::select('vehicle_id', $vehicles, null, [
                'class' =&gt; 'form-control select2',
                'id' =&gt; 'vehicle_id',
                'placeholder' =&gt; __('lang_v1.select_vehicle')
            ]) !!}
        &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;
</code></pre><p><strong>2. Add vehicle selection to POS edit form:</strong></p><p>Edit <code>resources/views/sale_pos/partials/pos_form_edit.blade.php</code>:</p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>&lt;!-- Vehicle Selection Row --&gt;
&lt;div class="row"&gt;
    &lt;div class="col-md-4 col-sm-6"&gt;
        &lt;div class="form-group"&gt;
            &lt;label&gt;@lang('lang_v1.vehicle'):&lt;/label&gt;
            &lt;div class="input-group"&gt;
                &lt;span class="input-group-addon"&gt;
                    &lt;i class="fa fa-car"&gt;&lt;/i&gt;
                &lt;/span&gt;
                {!! Form::select('vehicle_id', $vehicles, $transaction-&gt;vehicle_id, [
                    'class' =&gt; 'form-control select2',
                    'id' =&gt; 'vehicle_id',
                    'placeholder' =&gt; __('lang_v1.select_vehicle')
                ]) !!}
            &lt;/div&gt;
        &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;
</code></pre><p><strong>3. Update SellPosController:</strong></p><p>In <code>app/Http/Controllers/SellPosController.php</code>:</p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>// In create() method - add vehicles to view
$vehicles = \App\Vehicle::forDropdown($business_id, true);
return view('sale_pos.create')
    -&gt;with(compact(..., 'vehicles'));

// In edit() method - add vehicles to view
$vehicles = \App\Vehicle::forDropdown($business_id, true);
return view('sale_pos.edit')
    -&gt;with(compact(..., 'vehicles'));

// In store() method - save vehicle_id
$input['vehicle_id'] = $request-&gt;has('vehicle_id') &amp;&amp; !empty($request-&gt;input('vehicle_id'))
    ? $request-&gt;input('vehicle_id') : null;

// In update() method - save vehicle_id
$input['vehicle_id'] = $request-&gt;has('vehicle_id') &amp;&amp; !empty($request-&gt;input('vehicle_id'))
    ? $request-&gt;input('vehicle_id') : null;
</code></pre><p><strong>4. Update TransactionUtil:</strong></p><p>In <code>app/Utils/TransactionUtil.php</code>, add to <code>updateSellTransaction()</code> method:</p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>$update_date = [
    // ... existing fields
    'vehicle_id' =&gt; ! empty($input['vehicle_id']) ? $input['vehicle_id'] : null,
    // ... rest of fields
];
</code></pre><h3><strong>Purchase Integration</strong></h3><p><strong>1. Add vehicle selection to purchase create form:</strong></p><p>Edit <code>resources/views/purchase/partials/purchase_entry_row.blade.php</code> or main purchase form:</p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>&lt;div class="col-sm-4"&gt;
    &lt;div class="form-group"&gt;
        {!! Form::label('vehicle_id', __('lang_v1.vehicle') . ':') !!}
        &lt;div class="input-group"&gt;
            &lt;span class="input-group-addon"&gt;
                &lt;i class="fa fa-car"&gt;&lt;/i&gt;
            &lt;/span&gt;
            {!! Form::select('vehicle_id', $vehicles, null, [
                'class' =&gt; 'form-control select2',
                'placeholder' =&gt; __('lang_v1.select_vehicle')
            ]) !!}
        &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;
</code></pre><p><strong>2. Update PurchaseController:</strong></p><p>Follow similar pattern as SellPosController to add vehicles to create/edit methods.</p><hr><h2><strong>Reporting System</strong></h2><h3><strong>Export Class</strong></h3><p><strong>File:</strong> <code>app/Exports/VehicleReportsExport.php</code></p><p>Handles Excel export of vehicle transaction reports using Laravel Excel.</p><h3><strong>Report Views</strong></h3><p><strong>1. Main Reports Page:</strong></p><ul><li><p>Filter by date range</p></li><li><p>Filter by vehicle</p></li><li><p>Transaction type filter (sales/purchases/both)</p></li><li><p>Summary statistics</p></li></ul><p><strong>2. PDF Export:</strong> Partial template at <code>resources/views/vehicle/partials/pdf_export.blade.php</code></p><hr><h2><strong>Menu &amp; Permissions</strong></h2><h3><strong>Add Menu Item</strong></h3><p>Edit <code>app/Http/Middleware/AdminSidebarMenu.php</code>:</p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>// Vehicles menu (add before Settings Dropdown)
if (auth()-&gt;user()-&gt;can('vehicle.view')) {
    $menu-&gt;url(action([\App\Http\Controllers\VehicleController::class, 'index']),
        __('lang_v1.vehicles'), [
        'icon' =&gt; '&lt;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"&gt;
            &lt;path stroke="none" d="M0 0h24v24H0z" fill="none"&gt;&lt;/path&gt;
            &lt;path d="M7 17m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0"&gt;&lt;/path&gt;
            &lt;path d="M17 17m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0"&gt;&lt;/path&gt;
            &lt;path d="M5 17h-2v-11a1 1 0 0 1 1 -1h9v12m-4 0h6m-6 -9h8l2 3v6"&gt;&lt;/path&gt;
        &lt;/svg&gt;',
        'active' =&gt; request()-&gt;segment(1) == 'vehicles'
    ])-&gt;order(85);
}
</code></pre><h3><strong>Add Permissions</strong></h3><p>Run SQL or add to seeder:</p><pre spellcheck="" class="ipsCode language-sql" data-language="SQL"><code>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.%';
</code></pre><h3><strong>Add Routes</strong></h3><p>Edit <code>routes/web.php</code>:</p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>Route::resource('vehicles', 'VehicleController');
Route::get('vehicles-reports', 'VehicleController@reports');
Route::get('vehicles-reports/export', 'VehicleController@exportReport');
</code></pre><hr><h2><strong>Language Keys</strong></h2><p>Add to <code>resources/lang/en/lang_v1.php</code>:</p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>// Vehicles
'vehicles' =&gt; 'Vehicles',
'vehicle' =&gt; 'Vehicle',
'select_vehicle' =&gt; 'Select Vehicle',
'vehicle_help' =&gt; 'Optional: Assign a vehicle to this transaction',
'manage_your_vehicles' =&gt; 'Manage your vehicles',
'all_your_vehicles' =&gt; 'All Your Vehicles',
'add_vehicle' =&gt; 'Add Vehicle',
'edit_vehicle' =&gt; 'Edit Vehicle',
'vehicle_details' =&gt; 'Vehicle Details',
'vehicle_number' =&gt; 'Vehicle Number',
'driver_name' =&gt; 'Driver Name',
'driver_phone' =&gt; 'Driver Phone',
'driver_license' =&gt; 'Driver License',
'vehicle_type' =&gt; 'Vehicle Type',
'vehicle_model' =&gt; 'Vehicle Model',
'license_plate' =&gt; 'License Plate',
'vin_number' =&gt; 'VIN Number',
'fuel_type' =&gt; 'Fuel Type',
'engine_capacity' =&gt; 'Engine Capacity',
'current_mileage' =&gt; 'Current Mileage',
'purchase_price' =&gt; 'Purchase Price',
'insurance_expiry' =&gt; 'Insurance Expiry',
'registration_expiry' =&gt; 'Registration Expiry',
'last_service_date' =&gt; 'Last Service Date',
'next_service_due' =&gt; 'Next Service Due',
'fuel_efficiency' =&gt; 'Fuel Efficiency (L/100km)',
'max_load_capacity' =&gt; 'Max Load Capacity',
'daily_rate' =&gt; 'Daily Rate',
'cost_per_km' =&gt; 'Cost Per KM',
'assigned_route' =&gt; 'Assigned Route',
'home_location' =&gt; 'Home Location',
'vehicle_added_success' =&gt; 'Vehicle added successfully',
'vehicle_updated_success' =&gt; 'Vehicle updated successfully',
'vehicle_deleted_success' =&gt; 'Vehicle deleted successfully',
'vehicle_reports' =&gt; 'Vehicle Reports',
'vehicle_transactions' =&gt; 'Vehicle Transactions',
'no_vehicle_assigned' =&gt; 'No Vehicle Assigned',
</code></pre><hr><h3><strong>Complete File Package</strong></h3><p>All implementation files are available in the <code>files</code> directory:</p><p><strong>Structure:</strong></p><pre spellcheck="" class="ipsCode" data-language="text"><code>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
</code></pre><h3><strong>Installation Steps</strong></h3><ol><li><p><strong>Copy Model:</strong></p><pre spellcheck="" class="ipsCode language-bash" data-language="Bash"><code>cp files/Vehicle.php app/
</code></pre></li><li><p><strong>Copy Controller:</strong></p><pre spellcheck="" class="ipsCode language-bash" data-language="Bash"><code>cp files/VehicleController.php app/Http/Controllers/
</code></pre></li><li><p><strong>Copy Export:</strong></p><pre spellcheck="" class="ipsCode language-bash" data-language="Bash"><code>cp files/VehicleReportsExport.php app/Exports/
</code></pre></li><li><p><strong>Copy Views:</strong></p><pre spellcheck="" class="ipsCode language-bash" data-language="Bash"><code>cp -r files/vehicle resources/views/
</code></pre></li><li><p><strong>Copy Migration:</strong></p><pre spellcheck="" class="ipsCode language-bash" data-language="Bash"><code>cp files/migrations/2025_09_29_180000_create_vehicle_management_system.php database/migrations/
</code></pre></li><li><p><strong>Run Migration:</strong></p><pre spellcheck="" class="ipsCode language-bash" data-language="Bash"><code>php artisan migrate
</code></pre></li><li><p><strong>Add Routes, Menu, Permissions</strong> (see sections above)</p></li></ol><hr><h2><strong>Features Summary</strong></h2><h3><strong><span class="ipsEmoji" title="">✅</span> Complete CRUD Operations</strong></h3><ul><li><p>Create vehicles with comprehensive details</p></li><li><p>Edit existing vehicles</p></li><li><p>View individual vehicle details</p></li><li><p>Soft delete vehicles</p></li><li><p>Status management (active/inactive)</p></li></ul><h3><strong><span class="ipsEmoji" title="">✅</span> Transaction Integration</strong></h3><ul><li><p>Assign vehicles to sales transactions</p></li><li><p>Assign vehicles to purchase transactions</p></li><li><p>Track vehicle usage across all transactions</p></li><li><p>Filter transactions by vehicle</p></li></ul><h3><strong><span class="ipsEmoji" title="">✅</span> Advanced Reporting</strong></h3><ul><li><p>Vehicle transaction history</p></li><li><p>Sales and purchase summaries by vehicle</p></li><li><p>Date range filtering</p></li><li><p>Excel export functionality</p></li><li><p>Performance metrics</p></li></ul><h3><strong><span class="ipsEmoji" title="">✅</span> Business Features</strong></h3><ul><li><p>Multi-business support</p></li><li><p>Permission-based access control</p></li><li><p>Business-level data isolation</p></li><li><p>Soft deletes for data integrity</p></li><li><p>Audit trail (created_by tracking)</p></li></ul><h3><strong><span class="ipsEmoji" title="">✅</span> User Experience</strong></h3><ul><li><p>DataTables with server-side processing</p></li><li><p>Ajax-powered modals</p></li><li><p>Select2 dropdowns</p></li><li><p>Responsive design</p></li><li><p>Consistent with Ultimate POS UI</p></li></ul><hr><h2><strong>Testing</strong></h2><h3><strong>Sample Data</strong></h3><pre spellcheck="" class="ipsCode language-sql" data-language="SQL"><code>INSERT 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());
</code></pre><h3><strong>Test Checklist</strong></h3><ul><li><p> Create new vehicle</p></li><li><p> Edit vehicle details</p></li><li><p> Delete vehicle (soft delete)</p></li><li><p> View vehicle details</p></li><li><p> Assign vehicle to sale</p></li><li><p> Assign vehicle to purchase</p></li><li><p> Filter by vehicle in reports</p></li><li><p> Export vehicle report to Excel</p></li><li><p> Check permissions (view/create/edit/delete)</p></li><li><p> Test multi-business isolation</p></li></ul><hr><h2><strong>Troubleshooting</strong></h2><h3><strong>Common Issues</strong></h3><p><strong>1. Vehicle not showing in dropdown:</strong></p><ul><li><p>Check vehicle status is 'active'</p></li><li><p>Verify business_id matches current business</p></li><li><p>Clear cache: <code>php artisan cache:clear</code></p></li></ul><p><strong>2. Migration errors:</strong></p><ul><li><p>Ensure transactions table exists first</p></li><li><p>Check foreign key constraints</p></li><li><p>Verify business table exists for foreign key reference</p></li></ul><p><strong>3. Permission denied:</strong></p><ul><li><p>Verify permissions are seeded</p></li><li><p>Check role assignments</p></li><li><p>Clear permission cache: <code>php artisan permission:cache-reset</code></p></li></ul><p><strong>4. Table already exists error:</strong> If you get "table vehicles already exists" error:</p><pre spellcheck="" class="ipsCode language-bash" data-language="Bash"><code># Check current status
php artisan migrate:status | grep vehicle

# If needed, rollback and start fresh
php artisan migrate:rollback --step=1
php artisan migrate
</code></pre><p><strong>5. Foreign key constraint errors:</strong></p><pre spellcheck="" class="ipsCode language-bash" data-language="Bash"><code># 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;"
</code></pre><p><strong>6. Migration partially applied:</strong> If migration fails midway:</p><pre spellcheck="" class="ipsCode language-bash" data-language="Bash"><code># 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
</code></pre><hr><p><strong>Compatible with:</strong> Ultimate POS 6.x+ <strong>Laravel Version:</strong> 9.x / 10.x</p>]]></description><guid isPermaLink="false">23</guid><pubDate>Wed, 17 Dec 2025 23:06:10 +0000</pubDate></item><item><title>Separating Purchase and Sell Report Permissions</title><link>https://doniaweb.com/blogs/entry/22-separating-purchase-and-sell-report-permissions/</link><description><![CDATA[<p>This guide explains how to split the combined <code>purchase_n_sell_report.view</code> permission into separate <code>purchase_report.view</code> and <code>sell_report.view</code> permissions in Ultimate POS Laravel application.</p><h2><strong>Overview</strong></h2><p>Currently, Ultimate POS uses a single permission <code>purchase_n_sell_report.view</code> 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.</p><p><img src="https://ultimate-pos-tutorials.vercel.app/assets/images/purchase_n_sell_permission-d4e732b154e96c6b4f2c20d869cb3442.png" alt="Role permission" class="ipsRichText__align--block" width="1624" height="916" loading="lazy"></p><h2><strong>Files to Modify</strong></h2><p>The following files need to be updated:</p><ol><li><p><code>database/seeders/PermissionsTableSeeder.php</code> - Add new permissions</p></li><li><p><code>app/Http/Middleware/AdminSidebarMenu.php</code> - Update menu visibility logic</p></li><li><p><code>app/Http/Controllers/ReportController.php</code> - Update permission checks</p></li><li><p><code>resources/views/role/create.blade.php</code> - Update role creation form</p></li><li><p><code>resources/views/role/edit.blade.php</code> - Update role editing form</p></li></ol><h2><strong>Step-by-Step Implementation</strong></h2><h3><strong>Step 1: Add New Permissions to Seeder</strong></h3><p>Update <code>database/seeders/PermissionsTableSeeder.php</code>:</p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>&lt;?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' =&gt; 'purchase_n_sell_report.view'], // Remove this line
            ['name' =&gt; 'purchase_report.view'],  // Add this
            ['name' =&gt; 'sell_report.view'],      // Add this
            
            // ... rest of existing permissions ...
        ];

        $insert_data = [];
        $time_stamp = \Carbon::now()-&gt;toDateTimeString();
        foreach ($data as $d) {
            $d['guard_name'] = 'web';
            $d['created_at'] = $time_stamp;
            $insert_data[] = $d;
        }
        Permission::insert($insert_data);
    }
}
</code></pre><h3><strong>Step 2: Update AdminSidebarMenu Middleware</strong></h3><p>In <code>app/Http/Middleware/AdminSidebarMenu.php</code>, update the Reports dropdown section:</p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>// Find the Reports dropdown section and update the permission check
if (
    auth()-&gt;user()-&gt;can('purchase_report.view') ||      // New permission
    auth()-&gt;user()-&gt;can('sell_report.view') ||          // New permission
    auth()-&gt;user()-&gt;can('contacts_report.view') ||
    auth()-&gt;user()-&gt;can('stock_report.view') ||
    auth()-&gt;user()-&gt;can('tax_report.view') ||
    auth()-&gt;user()-&gt;can('trending_product_report.view') ||
    auth()-&gt;user()-&gt;can('sales_representative.view') ||
    auth()-&gt;user()-&gt;can('register_report.view') ||
    auth()-&gt;user()-&gt;can('expense_report.view')
) {
    $menu-&gt;dropdown(
        __('report.reports'),
        function ($sub) use ($enabled_modules, $is_admin) {
            // ... other report menu items ...
            
            // Update the purchase &amp; sell report condition
            if ((in_array('purchases', $enabled_modules) || in_array('add_sale', $enabled_modules) || in_array('pos_sale', $enabled_modules)) &amp;&amp; 
                (auth()-&gt;user()-&gt;can('purchase_report.view') || auth()-&gt;user()-&gt;can('sell_report.view'))) {
                $sub-&gt;url(
                    action([\App\Http\Controllers\ReportController::class, 'getPurchaseSell']),
                    __('report.purchase_sell_report'),
                    ['icon' =&gt; '', 'active' =&gt; request()-&gt;segment(2) == 'purchase-sell']
                );
            }
            
            // ... rest of menu items ...
        },
        ['icon' =&gt; '...', 'id' =&gt; 'tour_step8']
    )-&gt;order(55);
}
</code></pre><h3><strong>Step 3: Update ReportController</strong></h3><p>In <code>app/Http/Controllers/ReportController.php</code>, update the permission checks:</p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>/**
 * Shows product report of a business
 *
 * @return \Illuminate\Http\Response
 */
public function getPurchaseSell(Request $request)
{
    // Update permission check to allow either permission
    if (!auth()-&gt;user()-&gt;can('purchase_report.view') &amp;&amp; !auth()-&gt;user()-&gt;can('sell_report.view')) {
        abort(403, 'Unauthorized action.');
    }

    $business_id = $request-&gt;session()-&gt;get('user.business_id');

    //Return the details in ajax call
    if ($request-&gt;ajax()) {
        $start_date = $request-&gt;get('start_date');
        $end_date = $request-&gt;get('end_date');
        $location_id = $request-&gt;get('location_id');

        $purchase_details = [];
        $sell_details = [];
        $transaction_totals = [];

        // Only fetch purchase data if user has purchase report permission
        if (auth()-&gt;user()-&gt;can('purchase_report.view')) {
            $purchase_details = $this-&gt;transactionUtil-&gt;getPurchaseTotals($business_id, $start_date, $end_date, $location_id);
        }

        // Only fetch sell data if user has sell report permission  
        if (auth()-&gt;user()-&gt;can('sell_report.view')) {
            $sell_details = $this-&gt;transactionUtil-&gt;getSellTotals(
                $business_id,
                $start_date,
                $end_date,
                $location_id
            );
        }

        // Only fetch transaction totals if user has either permission
        if (auth()-&gt;user()-&gt;can('purchase_report.view') || auth()-&gt;user()-&gt;can('sell_report.view')) {
            $transaction_types = [
                'purchase_return', 'sell_return',
            ];

            $transaction_totals = $this-&gt;transactionUtil-&gt;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' =&gt; ($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' =&gt; ($sell_details['invoice_due'] ?? 0) - ($purchase_details['purchase_due'] ?? 0),
        ];

        return ['purchase' =&gt; $purchase_details,
            'sell' =&gt; $sell_details,
            'total_purchase_return' =&gt; $total_purchase_return_inc_tax,
            'total_sell_return' =&gt; $total_sell_return_inc_tax,
            'difference' =&gt; $difference,
        ];
    }

    $business_locations = BusinessLocation::forDropdown($business_id, true);

    return view('report.purchase_sell')
                -&gt;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()-&gt;user()-&gt;can('purchase_report.view')) {
        abort(403, 'Unauthorized action.');
    }
    // ... rest of the method
}

/**
 * Shows product sell report
 */
public function getproductSellReport(Request $request)
{
    if (!auth()-&gt;user()-&gt;can('sell_report.view')) {
        abort(403, 'Unauthorized action.');
    }
    // ... rest of the method
}

/**
 * Shows purchase payment report
 */
public function purchasePaymentReport(Request $request)
{
    if (!auth()-&gt;user()-&gt;can('purchase_report.view')) {
        abort(403, 'Unauthorized action.');
    }
    // ... rest of the method
}

/**
 * Shows sell payment report
 */
public function sellPaymentReport(Request $request)
{
    if (!auth()-&gt;user()-&gt;can('sell_report.view')) {
        abort(403, 'Unauthorized action.');
    }
    // ... rest of the method
}

/**
 * Shows items report
 */
public function itemsReport()
{
    if (!auth()-&gt;user()-&gt;can('purchase_report.view') &amp;&amp; !auth()-&gt;user()-&gt;can('sell_report.view')) {
        abort(403, 'Unauthorized action.');
    }
    // ... rest of the method
}
</code></pre><h3><strong>Step 4: Update Role Creation Form</strong></h3><p>In <code>resources/views/role/create.blade.php</code>, replace the combined permission checkbox:</p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>&lt;div class="row check_group"&gt;
    &lt;div class="col-md-1"&gt;
        &lt;h4&gt;@lang( 'role.report' )&lt;/h4&gt;
    &lt;/div&gt;
    &lt;div class="col-md-2"&gt;
        &lt;div class="checkbox"&gt;
            &lt;label&gt;
                &lt;input type="checkbox" class="check_all input-icheck" &gt; {{ __( 'role.select_all' ) }}
            &lt;/label&gt;
        &lt;/div&gt;
    &lt;/div&gt;
    &lt;div class="col-md-9"&gt;
        {{-- Remove this block:
        @if(in_array('purchases', $enabled_modules) || in_array('add_sale', $enabled_modules) || in_array('pos_sale', $enabled_modules))
            &lt;div class="col-md-12"&gt;
                &lt;div class="checkbox"&gt;
                    &lt;label&gt;
                        {!! Form::checkbox('permissions[]', 'purchase_n_sell_report.view', false, 
                        [ 'class' =&gt; 'input-icheck']); !!} {{ __( 'role.purchase_n_sell_report.view' ) }}
                    &lt;/label&gt;
                &lt;/div&gt;
            &lt;/div&gt;
        @endif
        --}}
        
        {{-- Add these separate checkboxes: --}}
        @if(in_array('purchases', $enabled_modules))
            &lt;div class="col-md-12"&gt;
                &lt;div class="checkbox"&gt;
                    &lt;label&gt;
                        {!! Form::checkbox('permissions[]', 'purchase_report.view', false, 
                        [ 'class' =&gt; 'input-icheck']); !!} {{ __( 'role.purchase_report.view' ) }}
                    &lt;/label&gt;
                &lt;/div&gt;
            &lt;/div&gt;
        @endif
        
        @if(in_array('add_sale', $enabled_modules) || in_array('pos_sale', $enabled_modules))
            &lt;div class="col-md-12"&gt;
                &lt;div class="checkbox"&gt;
                    &lt;label&gt;
                        {!! Form::checkbox('permissions[]', 'sell_report.view', false, 
                        [ 'class' =&gt; 'input-icheck']); !!} {{ __( 'role.sell_report.view' ) }}
                    &lt;/label&gt;
                &lt;/div&gt;
            &lt;/div&gt;
        @endif
        
        {{-- ... rest of existing report permissions ... --}}
    &lt;/div&gt;
&lt;/div&gt;
</code></pre><h3><strong>Step 5: Update Role Edit Form</strong></h3><p>In <code>resources/views/role/edit.blade.php</code>, make the same changes as in the create form:</p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>&lt;div class="row check_group"&gt;
    &lt;div class="col-md-1"&gt;
        &lt;h4&gt;@lang( 'role.report' )&lt;/h4&gt;
    &lt;/div&gt;
    &lt;div class="col-md-2"&gt;
        &lt;div class="checkbox"&gt;
            &lt;label&gt;
                &lt;input type="checkbox" class="check_all input-icheck" &gt; {{ __( 'role.select_all' ) }}
            &lt;/label&gt;
        &lt;/div&gt;
    &lt;/div&gt;
    &lt;div class="col-md-9"&gt;
        {{-- Remove this block:
        @if(in_array('purchases', $enabled_modules) || in_array('add_sale', $enabled_modules) || in_array('pos_sale', $enabled_modules))
            &lt;div class="col-md-12"&gt;
                &lt;div class="checkbox"&gt;
                    &lt;label&gt;
                        {!! Form::checkbox('permissions[]', 'purchase_n_sell_report.view', in_array('purchase_n_sell_report.view', $role_permissions), 
                        [ 'class' =&gt; 'input-icheck']); !!} {{ __( 'role.purchase_n_sell_report.view' ) }}
                    &lt;/label&gt;
                &lt;/div&gt;
            &lt;/div&gt;
        @endif
        --}}
        
        {{-- Add these separate checkboxes: --}}
        @if(in_array('purchases', $enabled_modules))
            &lt;div class="col-md-12"&gt;
                &lt;div class="checkbox"&gt;
                    &lt;label&gt;
                        {!! Form::checkbox('permissions[]', 'purchase_report.view', in_array('purchase_report.view', $role_permissions), 
                        [ 'class' =&gt; 'input-icheck']); !!} {{ __( 'role.purchase_report.view' ) }}
                    &lt;/label&gt;
                &lt;/div&gt;
            &lt;/div&gt;
        @endif
        
        @if(in_array('add_sale', $enabled_modules) || in_array('pos_sale', $enabled_modules))
            &lt;div class="col-md-12"&gt;
                &lt;div class="checkbox"&gt;
                    &lt;label&gt;
                        {!! Form::checkbox('permissions[]', 'sell_report.view', in_array('sell_report.view', $role_permissions), 
                        [ 'class' =&gt; 'input-icheck']); !!} {{ __( 'role.sell_report.view' ) }}
                    &lt;/label&gt;
                &lt;/div&gt;
            &lt;/div&gt;
        @endif
        
        {{-- ... rest of existing report permissions ... --}}
    &lt;/div&gt;
&lt;/div&gt;
</code></pre><h2><strong>Database Migration</strong></h2><p>Create a migration to add the new permissions and remove the old one:</p><pre spellcheck="" class="ipsCode language-bash" data-language="Bash"><code>php artisan make:migration update_purchase_sell_report_permissions
</code></pre><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>&lt;?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' =&gt; 'purchase_report.view', 'guard_name' =&gt; 'web']);
        Permission::create(['name' =&gt; 'sell_report.view', 'guard_name' =&gt; 'web']);

        // Find roles that have the old permission and give them both new permissions
        $oldPermission = Permission::where('name', 'purchase_n_sell_report.view')-&gt;first();
        
        if ($oldPermission) {
            $roles = $oldPermission-&gt;roles;
            
            foreach ($roles as $role) {
                $role-&gt;givePermissionTo(['purchase_report.view', 'sell_report.view']);
            }
            
            // Remove the old permission
            $oldPermission-&gt;delete();
        }
    }

    public function down()
    {
        // Recreate the old permission
        $oldPermission = Permission::create(['name' =&gt; 'purchase_n_sell_report.view', 'guard_name' =&gt; 'web']);

        // Find roles that have the new permissions and give them the old one
        $purchasePermission = Permission::where('name', 'purchase_report.view')-&gt;first();
        $sellPermission = Permission::where('name', 'sell_report.view')-&gt;first();

        $rolesWithPurchase = $purchasePermission ? $purchasePermission-&gt;roles : collect();
        $rolesWithSell = $sellPermission ? $sellPermission-&gt;roles : collect();
        
        $allRoles = $rolesWithPurchase-&gt;merge($rolesWithSell)-&gt;unique('id');
        
        foreach ($allRoles as $role) {
            $role-&gt;givePermissionTo('purchase_n_sell_report.view');
        }

        // Remove new permissions
        if ($purchasePermission) {
            $purchasePermission-&gt;delete();
        }
        if ($sellPermission) {
            $sellPermission-&gt;delete();
        }
    }
}
</code></pre><h2><strong>Language File Updates</strong></h2><p>Add the new permission labels to your language files (e.g., <code>resources/lang/en/role.php</code>):</p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>'purchase_report.view' =&gt; 'View Purchase Reports',
'sell_report.view' =&gt; 'View Sell Reports',
</code></pre><h2><strong>Testing</strong></h2><p>After implementing these changes:</p><ol><li><p>Run the migration: <code>php artisan migrate</code></p></li><li><p>Clear application cache: <code>php artisan cache:clear</code></p></li><li><p>Test role creation and editing forms</p></li><li><p>Test menu visibility with different permission combinations</p></li><li><p>Test report access with the new permissions</p></li></ol><h2><strong>Benefits</strong></h2><p>This separation provides:</p><ul><li><p><strong>Granular Control</strong>: Assign purchase and sell report permissions independently</p></li><li><p><strong>Better Security</strong>: Users only see reports they need access to</p></li><li><p><strong>Flexible Roles</strong>: Create roles for purchase-only or sales-only staff</p></li><li><p><strong>Maintainable Code</strong>: Clearer permission structure for future development</p></li></ul><h2><strong>Notes</strong></h2><ul><li><p>Existing roles with the old <code>purchase_n_sell_report.view</code> permission will automatically get both new permissions through the migration</p></li><li><p>Consider updating any custom views or components that might reference the old permission name</p></li><li><p>Update any API endpoints that might check for the old permission</p></li><li><p>Test thoroughly in a development environment before deploying to production</p></li></ul><h2><strong>Summary of Changes</strong></h2><ol><li><p><strong>New Permissions</strong>: <code>purchase_report.view</code> and <code>sell_report.view</code></p></li><li><p><strong>Removed Permission</strong>: <code>purchase_n_sell_report.view</code></p></li><li><p><strong>Updated Files</strong>: 5 core files modified</p></li><li><p><strong>Migration</strong>: Automatic conversion of existing roles</p></li><li><p><strong>Backward Compatibility</strong>: Migration handles existing role assignments</p></li></ol><h2><strong>Step 6: Update Routes with Middleware Protection</strong></h2><p>Update the routes in <code>web.php</code> to include proper middleware protection for the new permissions:</p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>//Reports... (Update existing report routes)
Route::get('/reports/purchase-report', [ReportController::class, 'purchaseReport'])
    -&gt;middleware('can:purchase_report.view');

Route::get('/reports/sale-report', [ReportController::class, 'saleReport'])
    -&gt;middleware('can:sell_report.view');

// Combined report - requires either permission
Route::get('/reports/purchase-sell', [ReportController::class, 'getPurchaseSell'])
    -&gt;middleware('can:purchase_report.view,sell_report.view');

// Product-specific reports
Route::get('/reports/product-purchase-report', [ReportController::class, 'getproductPurchaseReport'])
    -&gt;middleware('can:purchase_report.view');

Route::get('/reports/product-sell-report', [ReportController::class, 'getproductSellReport'])
    -&gt;middleware('can:sell_report.view');

Route::get('/reports/product-sell-report-with-purchase', [ReportController::class, 'getproductSellReportWithPurchase'])
    -&gt;middleware('can:sell_report.view');

Route::get('/reports/product-sell-grouped-report', [ReportController::class, 'getproductSellGroupedReport'])
    -&gt;middleware('can:sell_report.view');

Route::get('/reports/product-sell-grouped-by', [ReportController::class, 'productSellReportBy'])
    -&gt;middleware('can:sell_report.view');

// Payment reports
Route::get('/reports/purchase-payment-report', [ReportController::class, 'purchasePaymentReport'])
    -&gt;middleware('can:purchase_report.view');

Route::get('/reports/sell-payment-report', [ReportController::class, 'sellPaymentReport'])
    -&gt;middleware('can:sell_report.view');

// Items report - requires either permission
Route::get('/reports/items-report', [ReportController::class, 'itemsReport'])
    -&gt;middleware('can:purchase_report.view,sell_report.view');
</code></pre><h3><strong>Alternative Approach: Route Groups</strong></h3><p>You can also organize the routes using groups for better maintainability:</p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>// Purchase Reports Group
Route::middleware(['can:purchase_report.view'])-&gt;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'])-&gt;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'])-&gt;group(function () {
    Route::get('/reports/purchase-sell', [ReportController::class, 'getPurchaseSell']);
    Route::get('/reports/items-report', [ReportController::class, 'itemsReport']);
});
</code></pre><h2><strong>Additional Considerations</strong></h2><h3><strong>View Updates</strong></h3><p>You may also need to update the report view templates to conditionally show purchase or sell sections based on user permissions:</p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>&lt;!-- In report.purchase_sell view --&gt;
@if(auth()-&gt;user()-&gt;can('purchase_report.view'))
    &lt;!-- Purchase report section --&gt;
    &lt;div class="purchase-section"&gt;
        &lt;!-- Purchase data display --&gt;
    &lt;/div&gt;
@endif

@if(auth()-&gt;user()-&gt;can('sell_report.view'))
    &lt;!-- Sell report section --&gt;
    &lt;div class="sell-section"&gt;
        &lt;!-- Sell data display --&gt;
    &lt;/div&gt;
@endif

@if(!auth()-&gt;user()-&gt;can('purchase_report.view') &amp;&amp; !auth()-&gt;user()-&gt;can('sell_report.view'))
    &lt;div class="alert alert-warning"&gt;
        {{ __('lang_v1.no_permission_for_this_report') }}
    &lt;/div&gt;
@endif
</code></pre><p>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.</p>]]></description><guid isPermaLink="false">22</guid><pubDate>Wed, 17 Dec 2025 22:52:18 +0000</pubDate></item><item><title>Fix Duplicate Records and Table Bloat</title><link>https://doniaweb.com/blogs/entry/21-fix-duplicate-records-and-table-bloat/</link><description><![CDATA[<p>This guide addresses a common issue in Ultimate POS where the <code>transaction_sell_lines_purchase_lines</code> table develops duplicate records and becomes bloated, consuming excessive disk space. This typically happens due to data synchronization issues or improper cleanup processes.</p><p><img src="https://ultimate-pos-tutorials.vercel.app/assets/images/fix-duplicate-records-and-table-bloat-1-161565977ce37abeae40c783d347beae.jpg" alt="Before - Table showing bloated size" class="ipsRichText__align--block" width="1280" height="334" loading="lazy"></p><p><em>Table showing excessive size before optimization</em></p><p><img src="https://ultimate-pos-tutorials.vercel.app/assets/images/fix-duplicate-records-and-table-bloat-2-5132eb17e3b254f8f79f8127729a6047.jpg" alt="After - Table size reduced after fix" class="ipsRichText__align--block" width="1219" height="389" loading="lazy"></p><p><em>Table size successfully reduced after applying the fix</em></p><h2><strong>Symptoms</strong></h2><ul><li><p>Large table size (several GB) with relatively few records</p></li><li><p>Duplicate entries in transaction data</p></li><li><p>Poor database performance</p></li><li><p>Disk space issues</p></li></ul><h2><strong>Prerequisites</strong></h2><p><strong>Backup Required</strong></p><p><strong>Always create a database backup before performing these operations!</strong></p><ul><li><p>Database administrator access</p></li><li><p>phpMyAdmin or MySQL command line access</p></li><li><p>Maintenance window (operations will lock the table)</p></li></ul><h2><strong>Solution</strong></h2><h3><strong>Step 1: Verify the Issue</strong></h3><p>First, check if you have duplicate records:</p><pre spellcheck="" class="ipsCode language-sql" data-language="SQL"><code>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(*) &gt; 1;
</code></pre><p>Check current table size:</p><pre spellcheck="" class="ipsCode language-sql" data-language="SQL"><code>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();
</code></pre><h3><strong>Step 2: Remove Duplicate Records</strong></h3><p><strong>Table Lock Warning</strong></p><p>This operation will lock the table during execution. Perform during low-traffic periods.</p><pre spellcheck="" class="ipsCode language-sql" data-language="SQL"><code>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
);
</code></pre><p>This query:</p><ul><li><p>Keeps the record with the lowest <code>id</code> for each unique combination</p></li><li><p>Removes all duplicate records</p></li><li><p>Preserves data integrity by maintaining foreign key relationships</p></li></ul><h3><strong>Step 3: Force Table Rebuild (Fix Bloat)</strong></h3><p>After removing duplicates, the table may still appear large because MySQL doesn't automatically reclaim space. Force a complete rebuild:</p><pre spellcheck="" class="ipsCode language-sql" data-language="SQL"><code>-- 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;
</code></pre><h3><strong>Step 4: Verification</strong></h3><p>Verify duplicates are gone:</p><pre spellcheck="" class="ipsCode language-sql" data-language="SQL"><code>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(*) &gt; 1
) as dup_check;
</code></pre><p>Check final table size:</p><pre spellcheck="" class="ipsCode language-sql" data-language="SQL"><code>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();
</code></pre><p>Check record count:</p><pre spellcheck="" class="ipsCode language-sql" data-language="SQL"><code>SELECT COUNT(*) as total_records FROM transaction_sell_lines_purchase_lines;
</code></pre><h2><strong>Expected Results</strong></h2><ul><li><p><strong>Duplicate groups:</strong> 0</p></li><li><p><strong>Table size:</strong> Significantly reduced (should be appropriate for the number of records)</p></li><li><p><strong>Record count:</strong> Only unique records remain</p></li><li><p><strong>Performance:</strong> Improved query speeds</p></li></ul><h2><strong>Troubleshooting</strong></h2><h3><strong>phpMyAdmin LIMIT Error</strong></h3><p>If 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.</p><h3><strong>InnoDB Optimize Not Supported</strong></h3><p>If you see "Table does not support optimize, doing recreate + analyze instead" - this is normal for InnoDB tables and should fix the bloat issue.</p><h3><strong>Large Table Size Persists</strong></h3><p>If 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.</p><h2><strong>Prevention</strong></h2><p>To prevent this issue from recurring:</p><ol><li><p><strong>Regular maintenance:</strong> Schedule periodic duplicate checks</p></li><li><p><strong>Application review:</strong> Investigate why duplicates are being created</p></li><li><p><strong>Monitoring:</strong> Set up alerts for unusual table growth</p></li><li><p><strong>Backup strategy:</strong> Ensure regular backups before maintenance</p></li></ol><h2><strong>Additional Notes</strong></h2><ul><li><p>This procedure is specifically tested with Ultimate POS systems</p></li><li><p>The table structure includes fields: <code>id</code>, <code>sell_line_id</code>, <code>stock_adjustment_line_id</code>, <code>purchase_line_id</code>, <code>quantity</code>, <code>qty_returned</code>, <code>created_at</code>, <code>updated_at</code></p></li><li><p>All operations should be performed during maintenance windows</p></li><li><p>Consider upgrading Ultimate POS if this is a recurring issue</p></li></ul>]]></description><guid isPermaLink="false">21</guid><pubDate>Wed, 17 Dec 2025 22:40:29 +0000</pubDate></item><item><title>Adding Price Groups to Product List And Purchase</title><link>https://doniaweb.com/blogs/entry/20-adding-price-groups-to-product-list-and-purchase/</link><description><![CDATA[<p>This guide covers how to implement dynamic selling price groups in the Ultimate POS product list and integrate them with the purchase system.</p><h2><strong>Overview</strong></h2><p>The price groups feature allows you to:</p><ul><li><p>Display multiple selling price groups as columns in the product list</p></li><li><p>Manage group-specific pricing for products</p></li><li><p>Update price group values from purchase operations</p></li></ul><p><img src="https://ultimate-pos-tutorials.vercel.app/assets/images/price-groups-in-products-list-83bd07ab92c6af72d75188947f62773d.png" alt="Product list" class="ipsRichText__align--block" width="2834" height="1372" loading="lazy"></p><p><img src="https://ultimate-pos-tutorials.vercel.app/assets/images/price-groups-in-purchase-edit-2c2f934bb0f8190bf3678d2a5a295d5b.png" alt="Edit Purchase" class="ipsRichText__align--block" width="2276" height="1230" loading="lazy"></p><p><img src="https://ultimate-pos-tutorials.vercel.app/assets/images/price-groups-in-purchase-add-45cdf908a0ee60ffc6f0091ed6643516.png" alt="Add Purchase" class="ipsRichText__align--block" width="2320" height="1332" loading="lazy"></p><h2><strong><span class="ipsEmoji" title="">📦</span> Download Starter Files</strong></h2><p><a class="ipsAttachLink" data-fileid="34401" href="https://doniaweb.com/applications/core/interface/file/attachment.php?id=34401&amp;key=f6c7c5ef65794f7207cb5480c803f006" data-fileext="zip" rel="">price-groups-in-products-purchase.zip</a></p><h2><strong>Prerequisites</strong></h2><ul><li><p>Ultimate POS Laravel application</p></li><li><p>Basic understanding of Laravel, Blade templates, and DataTables</p></li><li><p>Database with <code>selling_price_groups</code> and <code>variation_group_prices</code> tables</p></li></ul><h2><strong>Step 1: Display Price Groups in Product List</strong></h2><h3><strong>1.1 Modify the Product List View Template</strong></h3><p><strong>File:</strong> <code>resources/views/product/partials/product_list.blade.php</code></p><p>Add the dynamic price group headers after the existing table headers:</p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>@foreach ($price_groups as $price_group_key =&gt; $price_group)
    @php 
        $colspan++;
    @endphp
    &lt;th&gt;{{ $price_group-&gt;name }}&lt;/th&gt;
@endforeach
</code></pre><p><strong>Location:</strong> Insert this code block in the table header section, typically after the standard product columns (SKU, Category, Brand, etc.).</p><p>Here is the complete code for <code>product_list.blade.php</code>:</p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>@php 
    $colspan = 15;
    $custom_labels = json_decode(session('business.custom_labels'), true);
@endphp
&lt;table class="table table-bordered table-striped ajax_view hide-footer" id="product_table"&gt;
    &lt;thead&gt;
        &lt;tr&gt;
            &lt;th&gt;&lt;input type="checkbox" id="select-all-row" data-table-id="product_table"&gt;&lt;/th&gt;
            &lt;th&gt;{{__('lang_v1.product_image')}}&lt;/th&gt;
            &lt;th&gt;@lang('messages.action')&lt;/th&gt;
            &lt;th&gt;@lang('sale.product')&lt;/th&gt;
           
            @can('view_purchase_price')
                @php 
                    $colspan++;
                @endphp
                &lt;th&gt;@lang('lang_v1.unit_perchase_price')&lt;/th&gt;
            @endcan
            @can('access_default_selling_price')
                @php 
                    $colspan++;
                @endphp
                &lt;th&gt;@lang('lang_v1.selling_price')&lt;/th&gt;
            @endcan
            &lt;th&gt;@lang('report.current_stock')&lt;/th&gt;
            &lt;th&gt;@lang('product.product_type')&lt;/th&gt;
            
            @foreach ($price_groups as $price_group_key =&gt; $price_group)
                @php 
                    $colspan++;
                @endphp
                &lt;th&gt;{{ $price_group-&gt;name }}&lt;/th&gt;
            @endforeach

            &lt;th&gt;@lang('product.category')&lt;/th&gt;
            &lt;th&gt;@lang('product.brand')&lt;/th&gt;
            &lt;th&gt;@lang('product.tax')&lt;/th&gt;
            &lt;th&gt;@lang('product.sku')&lt;/th&gt;
            &lt;th&gt;
                @lang('purchase.business_location') 
                @show_tooltip(__('lang_v1.product_business_location_tooltip'))
            &lt;/th&gt;
            &lt;th id="cf_1"&gt;{{ $custom_labels['product']['custom_field_1'] ?? '' }}&lt;/th&gt;
            &lt;th id="cf_2"&gt;{{ $custom_labels['product']['custom_field_2'] ?? '' }}&lt;/th&gt;
            &lt;th id="cf_3"&gt;{{ $custom_labels['product']['custom_field_3'] ?? '' }}&lt;/th&gt;
            &lt;th id="cf_4"&gt;{{ $custom_labels['product']['custom_field_4'] ?? '' }}&lt;/th&gt;
            &lt;th id="cf_5"&gt;{{ $custom_labels['product']['custom_field_5'] ?? '' }}&lt;/th&gt;
            &lt;th id="cf_6"&gt;{{ $custom_labels['product']['custom_field_6'] ?? '' }}&lt;/th&gt;
            &lt;th id="cf_7"&gt;{{ $custom_labels['product']['custom_field_7'] ?? '' }}&lt;/th&gt;
        &lt;/tr&gt;
    &lt;/thead&gt;
    &lt;tfoot&gt;
        &lt;tr&gt;
            &lt;td colspan="{{$colspan}}"&gt;
                &lt;div style="display: flex; width: 100%;"&gt;
                    @can('product.delete')
                        {!! Form::open(['url' =&gt; action([\App\Http\Controllers\ProductController::class, 'massDestroy']), 'method' =&gt; 'post', 'id' =&gt; 'mass_delete_form' ]) !!}
                        {!! Form::hidden('selected_rows', null, ['id' =&gt; 'selected_rows']); !!}
                        {!! Form::submit(__('lang_v1.delete_selected'), array('class' =&gt; 'tw-dw-btn tw-dw-btn-outline tw-dw-btn-xs tw-dw-btn-error', 'id' =&gt; 'delete-selected')) !!}
                        {!! Form::close() !!}
                    @endcan

                    @can('product.update')
                        @if(config('constants.enable_product_bulk_edit'))
                            &amp;nbsp;
                            {!! Form::open(['url' =&gt; action([\App\Http\Controllers\ProductController::class, 'bulkEdit']), 'method' =&gt; 'post', 'id' =&gt; 'bulk_edit_form' ]) !!}
                            {!! Form::hidden('selected_products', null, ['id' =&gt; 'selected_products_for_edit']); !!}
                            &lt;button type="submit" class="tw-dw-btn tw-dw-btn-xs tw-dw-btn-outline tw-dw-btn-primary" id="edit-selected"&gt; &lt;i class="fa fa-edit"&gt;&lt;/i&gt;{{__('lang_v1.bulk_edit')}}&lt;/button&gt;
                            {!! Form::close() !!}
                        @endif
                        &amp;nbsp;
                        &lt;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"&gt;@lang('lang_v1.add_to_location')&lt;/button&gt;
                        &amp;nbsp;
                        &lt;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"&gt;@lang('lang_v1.remove_from_location')&lt;/button&gt;
                    @endcan
                
                    &amp;nbsp;
                    {!! Form::open(['url' =&gt; action([\App\Http\Controllers\ProductController::class, 'massDeactivate']), 'method' =&gt; 'post', 'id' =&gt; 'mass_deactivate_form' ]) !!}
                    {!! Form::hidden('selected_products', null, ['id' =&gt; 'selected_products']); !!}
                    {!! Form::submit(__('lang_v1.deactivate_selected'), array('class' =&gt; 'tw-dw-btn tw-dw-btn-xs tw-dw-btn-outline tw-dw-btn-warning', 'id' =&gt; 'deactivate-selected')) !!}
                    {!! Form::close() !!} @show_tooltip(__('lang_v1.deactive_product_tooltip'))
                    &amp;nbsp;
                    @if($is_woocommerce)
                        &lt;button type="button" class="tw-dw-btn tw-dw-btn-xs tw-dw-btn-outline tw-dw-btn-warning toggle_woocomerce_sync"&gt;
                            @lang('lang_v1.woocommerce_sync')
                        &lt;/button&gt;
                    @endif
                &lt;/div&gt;
            &lt;/td&gt;
        &lt;/tr&gt;
    &lt;/tfoot&gt;
&lt;/table&gt;
</code></pre><h3><strong>1.2 Update DataTable Column Configuration</strong></h3><p><strong>File:</strong> <code>resources/views/product/index.blade.php</code></p><p>Add the dynamic DataTable columns configuration:</p><pre spellcheck="" class="ipsCode language-javascript" data-language="JavaScript"><code>@foreach ($price_groups as $price_group)
    {
        data: 'group_price{{$price_group-&gt;id}}',
        name: 'group_price{{$price_group-&gt;id}}',
        searchable: false,
        orderable: false,
        width: '80px'
    },
@endforeach
</code></pre><p><strong>Location:</strong> Insert this within the DataTable <code>columns</code> array configuration, after the existing column definitions. Here is the complete code for the new DataTable columns:</p><pre spellcheck="" class="ipsCode language-javascript" data-language="JavaScript"><code>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 &amp;&amp; $('#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-&gt;id}}',
                name: 'group_price{{$price_group-&gt;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 &gt; 0
        },
        {
            data: 'product_custom_field2',
            name: 'products.product_custom_field2',
            visible: $('#cf_2').text().length &gt; 0
        },
        {
            data: 'product_custom_field3',
            name: 'products.product_custom_field3',
            visible: $('#cf_3').text().length &gt; 0
        },
        {
            data: 'product_custom_field4',
            name: 'products.product_custom_field4',
            visible: $('#cf_4').text().length &gt; 0
        },
        {
            data: 'product_custom_field5',
            name: 'products.product_custom_field5',
            visible: $('#cf_5').text().length &gt; 0
        },
        {
            data: 'product_custom_field6',
            name: 'products.product_custom_field6',
            visible: $('#cf_6').text().length &gt; 0
        },
        {
            data: 'product_custom_field7',
            name: 'products.product_custom_field7',
            visible: $('#cf_7').text().length &gt; 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(
                '&lt;i style="margin:auto;" class="fa fa-plus-circle text-success cursor-pointer no-print rack-details" title="' +
                LANG.details + '"&gt;&lt;/i&gt;&amp;nbsp;&amp;nbsp;');
        }
        $(row).find('td:eq(0)').attr('class', 'selectable_td');
    },
    fnDrawCallback: function(oSettings) {
        __currency_convert_recursively($('#product_table'));
    },
});
</code></pre><h3><strong>1.3 Enhanced Controller Implementation</strong></h3><p><strong>File:</strong> <code>app/Http/Controllers/ProductController.php</code></p><h4><strong>Key Changes in the </strong><code>index()</code><strong> method:</strong></h4><h4><strong>1. Add Price Groups Query</strong></h4><p>Before handling AJAX requests (<code>if (request()-&gt;ajax()) {</code>), add:</p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>// Get price groups for the business
$price_groups = SellingPriceGroup::where('business_id', $business_id)-&gt;get();
</code></pre><h4><strong>2. Dynamic Price Group Columns</strong></h4><p>Add this code after the existing column definitions:</p><blockquote class="ipsQuote" cite="" data-ipsquote=""><div class="ipsQuote_contents" data-ipstruncate=""><p><strong>Important Note:</strong> The original Ultimate POS code uses <code>-&gt;rawColumns([...])</code> directly in the DataTables chain. The enhanced version uses a <code>$raw_columns</code> variable for better management of dynamic columns.</p></div></blockquote><p><strong>Original Pattern:</strong></p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>return Datatables::of($products)
    // ... column definitions ...
    -&gt;rawColumns(['action', 'image', 'mass_delete', 'product', 'selling_price', 'purchase_price', 'category', 'current_stock'])
    -&gt;make(true);
</code></pre><p><strong>Enhanced Pattern with Price Groups:</strong></p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>// 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)
    -&gt;addColumn('product_locations', function ($row) {
        return $row-&gt;product_locations-&gt;implode('name', ', ');
    })
    // ... other existing column definitions ...
    ;

// Add dynamic price group columns
foreach ($price_groups as $price_group) {
    $column_name = 'group_price' . $price_group-&gt;id;
    $raw_columns[] = $column_name;
    
    $datatables-&gt;editColumn($column_name, function ($row) use ($price_group) {
        $due_html = '';
        $group_price = (float) 0;
        
        if (!empty($price_group-&gt;id)) {
            $variation_group_price = VariationGroupPrice::where('variation_id', $row-&gt;variation_id)
                -&gt;where('price_group_id', $price_group-&gt;id)
                -&gt;first();
            
            if (!empty($variation_group_price)) {
                $group_price = (float) $variation_group_price-&gt;price_inc_tax;
            }
        }
        
        $due_html .= '&lt;span class="group_price" data-orig-value="' . $group_price . '"&gt;' 
                   . $this-&gt;productUtil-&gt;num_f($group_price, true) . '&lt;/span&gt;';
        
        return $due_html;
    });
}

// Apply raw columns and return
return $datatables-&gt;setRowAttr([
    'data-href' =&gt; function ($row) {
        if (auth()-&gt;user()-&gt;can('product.view')) {
            return action([\App\Http\Controllers\ProductController::class, 'view'], [$row-&gt;id]);
        } else {
            return '';
        }
    },
])
-&gt;rawColumns($raw_columns)
-&gt;make(true);
</code></pre><p><strong>Complete index method:</strong></p><p><strong>Add this import at the top of ProductController.php with other imports:</strong></p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>use App\VariationGroupPrice;
</code></pre><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>public function index()
{
    if (!auth()-&gt;user()-&gt;can('product.view') &amp;&amp; !auth()-&gt;user()-&gt;can('product.create')) {
        abort(403, 'Unauthorized action.');
    }
    
    $business_id = request()-&gt;session()-&gt;get('user.business_id');
    $selling_price_group_count = SellingPriceGroup::countSellingPriceGroups($business_id);
    $is_woocommerce = $this-&gt;moduleUtil-&gt;isModuleInstalled('Woocommerce');

    // Get price groups for the business
    $price_groups = SellingPriceGroup::where('business_id', $business_id)-&gt;get();

    if (request()-&gt;ajax()) {
        // Filter by location
        $location_id = request()-&gt;get('location_id', null);
        $permitted_locations = auth()-&gt;user()-&gt;permitted_locations();

        $query = Product::with(['media'])
            -&gt;leftJoin('brands', 'products.brand_id', '=', 'brands.id')
            -&gt;join('units', 'products.unit_id', '=', 'units.id')
            -&gt;leftJoin('categories as c1', 'products.category_id', '=', 'c1.id')
            -&gt;leftJoin('categories as c2', 'products.sub_category_id', '=', 'c2.id')
            -&gt;leftJoin('tax_rates', 'products.tax', '=', 'tax_rates.id')
            -&gt;join('variations as v', 'v.product_id', '=', 'products.id')
            -&gt;leftJoin('variation_location_details as vld', function ($join) use ($permitted_locations) {
                $join-&gt;on('vld.variation_id', '=', 'v.id');
                if ($permitted_locations != 'all') {
                    $join-&gt;whereIn('vld.location_id', $permitted_locations);
                }
            })
            -&gt;whereNull('v.deleted_at')
            -&gt;where('products.business_id', $business_id)
            -&gt;where('products.type', '!=', 'modifier');

        if (!empty($location_id) &amp;&amp; $location_id != 'none') {
            if ($permitted_locations == 'all' || in_array($location_id, $permitted_locations)) {
                $query-&gt;whereHas('product_locations', function ($query) use ($location_id) {
                    $query-&gt;where('product_locations.location_id', '=', $location_id);
                });
            }
        } elseif ($location_id == 'none') {
            $query-&gt;doesntHave('product_locations');
        } else {
            if ($permitted_locations != 'all') {
                $query-&gt;whereHas('product_locations', function ($query) use ($permitted_locations) {
                    $query-&gt;whereIn('product_locations.location_id', $permitted_locations);
                });
            } else {
                $query-&gt;with('product_locations');
            }
        }

        $products = $query-&gt;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-&gt;addSelect('woocommerce_disable_sync');
        }

        $products-&gt;groupBy('products.id');

        // Apply filters
        $type = request()-&gt;get('type', null);
        if (!empty($type)) {
            $products-&gt;where('products.type', $type);
        }

        $category_id = request()-&gt;get('category_id', null);
        if (!empty($category_id)) {
            $products-&gt;where('products.category_id', $category_id);
        }

        $brand_id = request()-&gt;get('brand_id', null);
        if (!empty($brand_id)) {
            $products-&gt;where('products.brand_id', $brand_id);
        }

        $unit_id = request()-&gt;get('unit_id', null);
        if (!empty($unit_id)) {
            $products-&gt;where('products.unit_id', $unit_id);
        }

        $tax_id = request()-&gt;get('tax_id', null);
        if (!empty($tax_id)) {
            $products-&gt;where('products.tax', $tax_id);
        }

        $active_state = request()-&gt;get('active_state', null);
        if ($active_state == 'active') {
            $products-&gt;Active();
        }
        if ($active_state == 'inactive') {
            $products-&gt;Inactive();
        }
        
        $not_for_selling = request()-&gt;get('not_for_selling', null);
        if ($not_for_selling == 'true') {
            $products-&gt;ProductNotForSales();
        }

        $woocommerce_enabled = request()-&gt;get('woocommerce_enabled', 0);
        if ($woocommerce_enabled == 1) {
            $products-&gt;where('products.woocommerce_disable_sync', 0);
        }

        if (!empty(request()-&gt;get('repair_model_id'))) {
            $products-&gt;where('products.repair_model_id', request()-&gt;get('repair_model_id'));
        }

        // Create DataTables instance
        $datatables = Datatables::of($products)
            -&gt;addColumn('product_locations', function ($row) {
                return $row-&gt;product_locations-&gt;implode('name', ', ');
            })
            -&gt;editColumn('category', '{{$category}} @if(!empty($sub_category))&lt;br/&gt; -- {{$sub_category}}@endif')
            -&gt;addColumn('action', function ($row) use ($selling_price_group_count) {
                $html = '&lt;div class="btn-group"&gt;&lt;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"&gt;'.__('messages.actions').'&lt;span class="caret"&gt;&lt;/span&gt;&lt;span class="sr-only"&gt;Toggle Dropdown&lt;/span&gt;&lt;/button&gt;&lt;ul class="dropdown-menu dropdown-menu-left" role="menu"&gt;&lt;li&gt;&lt;a href="'.action([\App\Http\Controllers\LabelsController::class, 'show']).'?product_id='.$row-&gt;id.'" data-toggle="tooltip" title="'.__('lang_v1.label_help').'"&gt;&lt;i class="fa fa-barcode"&gt;&lt;/i&gt; '.__('barcode.labels').'&lt;/a&gt;&lt;/li&gt;';

                if (auth()-&gt;user()-&gt;can('product.view')) {
                    $html .= '&lt;li&gt;&lt;a href="'.action([\App\Http\Controllers\ProductController::class, 'view'], [$row-&gt;id]).'" class="view-product"&gt;&lt;i class="fa fa-eye"&gt;&lt;/i&gt; '.__('messages.view').'&lt;/a&gt;&lt;/li&gt;';
                }

                if (auth()-&gt;user()-&gt;can('product.update')) {
                    $html .= '&lt;li&gt;&lt;a href="'.action([\App\Http\Controllers\ProductController::class, 'edit'], [$row-&gt;id]).'"&gt;&lt;i class="glyphicon glyphicon-edit"&gt;&lt;/i&gt; '.__('messages.edit').'&lt;/a&gt;&lt;/li&gt;';
                }

                if (auth()-&gt;user()-&gt;can('product.delete')) {
                    $html .= '&lt;li&gt;&lt;a href="'.action([\App\Http\Controllers\ProductController::class, 'destroy'], [$row-&gt;id]).'" class="delete-product"&gt;&lt;i class="fa fa-trash"&gt;&lt;/i&gt; '.__('messages.delete').'&lt;/a&gt;&lt;/li&gt;';
                }

                if ($row-&gt;is_inactive == 1) {
                    $html .= '&lt;li&gt;&lt;a href="'.action([\App\Http\Controllers\ProductController::class, 'activate'], [$row-&gt;id]).'" class="activate-product"&gt;&lt;i class="fas fa-check-circle"&gt;&lt;/i&gt; '.__('lang_v1.reactivate').'&lt;/a&gt;&lt;/li&gt;';
                }

                $html .= '&lt;li class="divider"&gt;&lt;/li&gt;';

                if ($row-&gt;enable_stock == 1 &amp;&amp; auth()-&gt;user()-&gt;can('product.opening_stock')) {
                    $html .= '&lt;li&gt;&lt;a href="#" data-href="'.action([\App\Http\Controllers\OpeningStockController::class, 'add'], ['product_id' =&gt; $row-&gt;id]).'" class="add-opening-stock"&gt;&lt;i class="fa fa-database"&gt;&lt;/i&gt; '.__('lang_v1.add_edit_opening_stock').'&lt;/a&gt;&lt;/li&gt;';
                }

                if (auth()-&gt;user()-&gt;can('product.view')) {
                    $html .= '&lt;li&gt;&lt;a href="'.action([\App\Http\Controllers\ProductController::class, 'productStockHistory'], [$row-&gt;id]).'"&gt;&lt;i class="fas fa-history"&gt;&lt;/i&gt; '.__('lang_v1.product_stock_history').'&lt;/a&gt;&lt;/li&gt;';
                }

                if (auth()-&gt;user()-&gt;can('product.create')) {
                    if ($selling_price_group_count &gt; 0) {
                        $html .= '&lt;li&gt;&lt;a href="'.action([\App\Http\Controllers\ProductController::class, 'addSellingPrices'], [$row-&gt;id]).'"&gt;&lt;i class="fas fa-money-bill-alt"&gt;&lt;/i&gt; '.__('lang_v1.add_selling_price_group_prices').'&lt;/a&gt;&lt;/li&gt;';
                    }

                    $html .= '&lt;li&gt;&lt;a href="'.action([\App\Http\Controllers\ProductController::class, 'create'], ['d' =&gt; $row-&gt;id]).'"&gt;&lt;i class="fa fa-copy"&gt;&lt;/i&gt; '.__('lang_v1.duplicate_product').'&lt;/a&gt;&lt;/li&gt;';
                }

                if (!empty($row-&gt;media-&gt;first())) {
                    $html .= '&lt;li&gt;&lt;a href="'.$row-&gt;media-&gt;first()-&gt;display_url.'" download="'.$row-&gt;media-&gt;first()-&gt;display_name.'"&gt;&lt;i class="fas fa-download"&gt;&lt;/i&gt; '.__('lang_v1.product_brochure').'&lt;/a&gt;&lt;/li&gt;';
                }

                $html .= '&lt;/ul&gt;&lt;/div&gt;';

                return $html;
            })
            -&gt;editColumn('product', function ($row) use ($is_woocommerce) {
                $product = $row-&gt;is_inactive == 1 ? e($row-&gt;product).' &lt;span class="label bg-gray"&gt;'.__('lang_v1.inactive').'&lt;/span&gt;' : e($row-&gt;product);

                $product = $row-&gt;not_for_selling == 1 ? $product.' &lt;span class="label bg-gray"&gt;'.__('lang_v1.not_for_selling').'&lt;/span&gt;' : $product;

                if ($is_woocommerce &amp;&amp; !$row-&gt;woocommerce_disable_sync) {
                    $product = $product.'&lt;br&gt;&lt;i class="fab fa-wordpress"&gt;&lt;/i&gt;';
                }

                return $product;
            })
            -&gt;editColumn('image', function ($row) {
                return '&lt;div style="display: flex;"&gt;&lt;img src="'.$row-&gt;image_url.'" alt="Product image" class="product-thumbnail-small"&gt;&lt;/div&gt;';
            })
            -&gt;editColumn('type', '@lang("lang_v1." . $type)')
            -&gt;addColumn('mass_delete', function ($row) {
                return '&lt;input type="checkbox" class="row-select" value="'.$row-&gt;id.'"&gt;';
            })
            -&gt;editColumn('current_stock', function ($row) {
                if ($row-&gt;enable_stock) {
                    $stock = $this-&gt;productUtil-&gt;num_f($row-&gt;current_stock, false, null, true);
                    return $stock.' '.$row-&gt;unit;
                } else {
                    return '--';
                }
            })
            -&gt;addColumn('purchase_price', '&lt;div style="white-space: nowrap;"&gt;@format_currency($min_purchase_price) @if($max_purchase_price != $min_purchase_price &amp;&amp; $type == "variable") - @format_currency($max_purchase_price)@endif &lt;/div&gt;')
            -&gt;addColumn('selling_price', '&lt;div style="white-space: nowrap;"&gt;@format_currency($min_price) @if($max_price != $min_price &amp;&amp; $type == "variable") - @format_currency($max_price)@endif &lt;/div&gt;')
            -&gt;filterColumn('products.sku', function ($query, $keyword) {
                $query-&gt;whereHas('variations', function ($q) use ($keyword) {
                    $q-&gt;where('sub_sku', 'like', "%{$keyword}%");
                })
                -&gt;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-&gt;id;
            $raw_columns[] = $column_name;
            
            $datatables-&gt;editColumn($column_name, function ($row) use ($price_group) {
                try {
                    $group_price = 0;
                    
                    if (isset($row-&gt;variation_id) &amp;&amp; !empty($price_group-&gt;id)) {
                        $variation_group_price = VariationGroupPrice::where('variation_id', $row-&gt;variation_id)
                            -&gt;where('price_group_id', $price_group-&gt;id)
                            -&gt;first();
                        
                        if ($variation_group_price) {
                            $group_price = (float) $variation_group_price-&gt;price_inc_tax;
                        }
                    }
                    
                    return '&lt;span class="group_price" data-orig-value="' . $group_price . '"&gt;' 
                           . $this-&gt;productUtil-&gt;num_f($group_price, true) . '&lt;/span&gt;';
                           
                } catch (\Exception $e) {
                    \Log::error('Price group error: ' . $e-&gt;getMessage());
                    return '&lt;span class="group_price" data-orig-value="0"&gt;0.00&lt;/span&gt;';
                }
            });
        }

        // Apply final configuration and return
        return $datatables
            -&gt;setRowAttr([
                'data-href' =&gt; function ($row) {
                    if (auth()-&gt;user()-&gt;can('product.view')) {
                        return action([\App\Http\Controllers\ProductController::class, 'view'], [$row-&gt;id]);
                    } else {
                        return '';
                    }
                },
            ])
            -&gt;rawColumns($raw_columns)
            -&gt;make(true);
    }

    // Non-AJAX request - return view with data
    $rack_enabled = (request()-&gt;session()-&gt;get('business.enable_racks') || request()-&gt;session()-&gt;get('business.enable_row') || request()-&gt;session()-&gt;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-&gt;prepend(__('lang_v1.none'), 'none');

    $show_manufacturing_data = $this-&gt;moduleUtil-&gt;isModuleInstalled('Manufacturing') &amp;&amp; (auth()-&gt;user()-&gt;can('superadmin') || $this-&gt;moduleUtil-&gt;hasThePermissionInSubscription($business_id, 'manufacturing_module'));

    // List product screen filter from module
    $pos_module_data = $this-&gt;moduleUtil-&gt;getModuleData('get_filters_for_list_product_screen');

    $is_admin = $this-&gt;productUtil-&gt;is_admin(auth()-&gt;user());

    return view('product.index')
        -&gt;with(compact(
            'price_groups',
            'rack_enabled',
            'categories',
            'brands',
            'units',
            'taxes',
            'business_locations',
            'show_manufacturing_data',
            'pos_module_data',
            'is_woocommerce',
            'is_admin'
        ));
}
</code></pre><p><strong>Key Changes Explained:</strong></p><ol><li><p><strong>Variable Declaration</strong>: <code>$raw_columns</code> array is created to manage column names dynamically</p></li><li><p><strong>DataTables Instance</strong>: Store the DataTables object in <code>$datatables</code> variable for manipulation</p></li><li><p><strong>Dynamic Addition</strong>: Price group columns are added to both the DataTables instance and the raw columns array</p></li><li><p><strong>Final Assembly</strong>: Apply <code>rawColumns()</code> and <code>make()</code> at the end</p></li></ol><p><strong>Critical Addition - Missing variation_id:</strong></p><p>The original query is missing the essential <code>variation_id</code> field needed for price group lookup. Add this to your select statement:</p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>$products = $query-&gt;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 ...
);
</code></pre><p><strong>Complete Integration Steps:</strong></p><ol><li><p><strong>Add variation_id to select</strong></p></li><li><p><strong>Get price groups before AJAX handling</strong></p></li><li><p><strong>Replace direct rawColumns with variable approach</strong></p></li><li><p><strong>Add price group processing loop</strong></p></li><li><p><strong>Update rawColumns call to use the variable</strong></p></li></ol><h4><strong>3. Pass Price Groups to View</strong></h4><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>return view('product.index')
    -&gt;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'
    ));
</code></pre><h2><strong>Key Implementation Notes</strong></h2><ol><li><p><strong>Price Group Loop Position</strong>: The price group columns are positioned after the product type column and before category/brand columns</p></li><li><p><strong>Colspan Management</strong>: Each price group dynamically increments the <code>$colspan</code> variable for proper table footer spanning</p></li><li><p><strong>DataTable Integration</strong>: Price groups are added as non-searchable, non-orderable columns in the DataTable configuration</p></li><li><p><strong>Variation ID Requirement</strong>: The <code>variation_id</code> field is essential for linking price groups to specific product variations</p></li><li><p><strong>Dynamic Column Generation</strong>: Price group columns are generated based on actual database records, not hardcoded</p></li></ol><h2><strong>Testing</strong></h2><ol><li><p><strong>Verify Price Groups Display</strong>: Check that price group columns appear in the product list</p></li><li><p><strong>Test Data Population</strong>: Ensure price group values display correctly for products with assigned group prices</p></li><li><p><strong>Verify Filtering</strong>: Test location and other filters work with the enhanced query</p></li><li><p><strong>Performance Check</strong>: Monitor query performance with the additional joins and data</p></li></ol><hr><h2><strong>Step 2: Integration with Purchase System</strong></h2><h3><strong>2.1 Add Price Group Columns to Purchase Create Form</strong></h3><p><strong>File:</strong> <code>resources/views/purchase/create.blade.php</code></p><p>Add price group column headers to the purchase product table. Locate the existing selling price header and add the price group headers after it:</p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>&lt;th&gt;
    @lang('purchase.unit_selling_price')
    &lt;small&gt;(@lang('product.inc_of_tax'))&lt;/small&gt;
&lt;/th&gt;

@foreach ($price_groups_all as $price_group_key =&gt; $price_group)
    &lt;th&gt;{{ $price_group-&gt;name }}&lt;/th&gt;
@endforeach
</code></pre><p><strong>Location:</strong> Add this code after the unit selling price header in the purchase table.</p><h3><strong>2.2 Add Price Group Input Fields to Purchase Entry Row</strong></h3><p><strong>File:</strong> <code>resources/views/purchase/partials/purchase_entry_row.blade.php</code></p><p>Add price group input fields for each product row. Locate the lot number section and add the price group fields before it:</p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>@if(!empty($price_groups) &amp;&amp; is_iterable($price_groups))
    @foreach($price_groups as $price_group)
        &lt;td&gt;
            @php
                $group_price_group = $price_group-&gt;id;
            @endphp
            {!! Form::text('purchases['.$row_count.'][group_prices]['.$group_price_group.'][group_price]',!empty($variation_prices[$variation-&gt;id][$group_price_group]['price']) ? @num_format($variation_prices[$variation-&gt;id][$group_price_group]['price']) : 0, ['class' =&gt; 'form-control input_number input-sm']); !!}
            {!! Form::hidden('purchases['.$row_count.'][group_prices]['.$group_price_group.'][group_price_id]',$price_group-&gt;id); !!}
            @php
                $price_type = !empty($variation_prices[$variation-&gt;id][$group_price_group]['price_type']) ? $variation_prices[$variation-&gt;id][$group_price_group]['price_type'] : 'fixed';
            @endphp
            &lt;input type="hidden" name="purchases[{{$row_count}}][group_prices][{{$group_price_group}}][group_price_type]" value="{{ $price_type }}"&gt; 
        &lt;/td&gt;
    @endforeach
@endif
</code></pre><p><strong>Location:</strong> Add this code before the lot number section:</p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>@if(session('business.enable_lot_number'))
    @php
        $lot_number = !empty($imported_data['lot_number']) ? $imported_data['lot_number'] : null;
    @endphp
    &lt;td&gt;
        {!! Form::text('purchases[' . $row_count . '][lot_number]', $lot_number, ['class' =&gt; 'form-control input-sm']); !!}
    &lt;/td&gt;
@endif
</code></pre><h3><strong>2.3 Update Purchase Controller Methods</strong></h3><p><strong>File:</strong> <code>app/Http/Controllers/PurchaseController.php</code></p><h4><strong>2.3.1 Import Required Model</strong></h4><p>At the top of the file with other imports, ensure you have:</p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>use App\SellingPriceGroup;
</code></pre><h4><strong>2.3.2 Update </strong><code>create()</code><strong> Method</strong></h4><p>In the <code>create()</code> method, add price groups data before returning the view. Locate the existing code:</p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><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)-&gt;get();

return view('purchase.create')-&gt;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'
    )
);
</code></pre><h4><strong>2.3.3 Update </strong><code>edit()</code><strong> Method</strong></h4><p>In the <code>edit()</code> method, ensure price groups are loaded and variation prices are prepared. Add this code before <code>return view('purchase.edit')</code>:</p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>$variation_prices = [];

$price_groups = SellingPriceGroup::where('business_id', $business_id)-&gt;active()-&gt;get();
foreach ($purchase-&gt;purchase_lines as $key =&gt; $value) {
    if (!empty($value-&gt;sub_unit_id)) {
        $formated_purchase_line = $this-&gt;productUtil-&gt;changePurchaseLineUnit($value, $business_id);
        $purchase-&gt;purchase_lines[$key] = $formated_purchase_line;
    }

    foreach ($value-&gt;variations-&gt;group_prices as $group_price) {
        $variation_prices[$group_price-&gt;variation_id][$group_price-&gt;price_group_id] = [
            'price' =&gt; $group_price-&gt;price_inc_tax,
            'price_type' =&gt; $group_price-&gt;price_type
        ];
    }
}
</code></pre><p>Then include the price groups in the view compact:</p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>return view('purchase.edit')
    -&gt;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'
    ));
</code></pre><h4><strong>2.3.4 Update </strong><code>getPurchaseEntryRow()</code><strong> Method</strong></h4><p>In the <code>getPurchaseEntryRow()</code> method, add price group data loading:</p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>public function getPurchaseEntryRow(Request $request)
{
    if (request()-&gt;ajax()) {
        $product_id = $request-&gt;input('product_id');
        $variation_id = $request-&gt;input('variation_id');
        $business_id = request()-&gt;session()-&gt;get('user.business_id');
        $location_id = $request-&gt;input('location_id');
        $is_purchase_order = $request-&gt;has('is_purchase_order');
        $supplier_id = $request-&gt;input('supplier_id');

        $hide_tax = 'hide';
        if ($request-&gt;session()-&gt;get('business.enable_inline_tax') == 1) {
            $hide_tax = '';
        }

        $currency_details = $this-&gt;transactionUtil-&gt;purchaseCurrencyDetails($business_id);

        if (!empty($product_id)) {
            $row_count = $request-&gt;input('row_count');
            $product = Product::where('id', $product_id)
                -&gt;with(['unit', 'second_unit', 'variations.group_prices']) // ADD variations.group_prices
                -&gt;first();

            $sub_units = $this-&gt;productUtil-&gt;getSubUnits($business_id, $product-&gt;unit-&gt;id, false, $product_id);

            $query = Variation::where('product_id', $product_id)
                -&gt;with([
                    'product_variation',
                    'group_prices', // ADD this line
                    'variation_location_details' =&gt; function ($q) use ($location_id) {
                        $q-&gt;where('location_id', $location_id);
                    },
                ]);
            if ($variation_id !== '0') {
                $query-&gt;where('id', $variation_id);
            }

            $variations = $query-&gt;get();
            $taxes = TaxRate::where('business_id', $business_id)
                -&gt;ExcludeForTaxGroup()
                -&gt;get();

            $last_purchase_line = $this-&gt;getLastPurchaseLine($variation_id, $location_id, $supplier_id);

            // ADD these lines for price groups
            $price_groups = SellingPriceGroup::where('business_id', $business_id)-&gt;get();

            $variation_prices = [];
            foreach ($product-&gt;variations as $variation) {
                foreach ($variation-&gt;group_prices as $group_price) {
                    $variation_prices[$variation-&gt;id][$group_price-&gt;price_group_id] = [
                        'price' =&gt; $group_price-&gt;price_inc_tax,
                        'price_type' =&gt; $group_price-&gt;price_type
                    ];
                }
            }

            return view('purchase.partials.purchase_entry_row')
                -&gt;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'
                ));
        }
    }
}
</code></pre><h4><strong>2.3.5 Update </strong><code>importPurchaseProducts()</code><strong> Method</strong></h4><p>In the <code>importPurchaseProducts()</code> method, add price group support for imported products before <code>$html = view('purchase.partials.imported_purchase_product_rows')</code>:</p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>$price_groups = SellingPriceGroup::where('business_id', $business_id)-&gt;get();

$variation_prices = [];
foreach ($formatted_data as $data) {
    if (!empty($data['product'])) {
        foreach ($data['product']-&gt;variations as $variation) {
            foreach ($variation-&gt;group_prices as $group_price) {
                $variation_prices[$variation-&gt;id][$group_price-&gt;price_group_id] = [
                    'price' =&gt; $group_price-&gt;price_inc_tax,
                    'price_type' =&gt; $group_price-&gt;price_type
                ];
            }
        }
    }
}

$html = view('purchase.partials.imported_purchase_product_rows')
    -&gt;with(compact('formatted_data', 'taxes', 'currency_details', 'hide_tax', 'row_count', 'price_groups', 'variation_prices'))-&gt;render();
</code></pre><h3><strong>2.4 Add Price Group Columns to Purchase Edit Form</strong></h3><p><strong>File:</strong> <code>resources/views/purchase/partials/edit_purchase_entry_row.blade.php</code></p><p>Add price group column headers to the purchase edit table. Locate the existing selling price header and add the price group headers after it:</p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>@if(empty($is_purchase_order))
    &lt;th&gt;@lang('purchase.unit_selling_price') &lt;small&gt;(@lang('product.inc_of_tax'))&lt;/small&gt;&lt;/th&gt;
    
    @if(!empty($price_groups) &amp;&amp; is_iterable($price_groups))
        @foreach ($price_groups as $price_group_key =&gt; $price_group)
            &lt;th&gt;{{ $price_group-&gt;name }}&lt;/th&gt;
        @endforeach
    @endif
    
    @if(session('business.enable_lot_number'))
        &lt;th&gt;@lang('lang_v1.lot_number')&lt;/th&gt;
    @endif
    @if(session('business.enable_product_expiry'))
        &lt;th&gt;@lang('product.mfg_date') / @lang('product.exp_date')&lt;/th&gt;
    @endif
@endif
</code></pre><p>Add price group input fields for each existing purchase line. Locate the selling price cell and add the price group fields after it:</p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>@if(empty($is_purchase_order))
&lt;td&gt;
    @if(session('business.enable_editing_product_from_purchase'))
        {!! Form::text('purchases[' . $loop-&gt;index . '][default_sell_price]', number_format($sp, $currency_precision, $currency_details-&gt;decimal_separator, $currency_details-&gt;thousand_separator), ['class' =&gt; 'form-control input-sm input_number default_sell_price', 'required']); !!}
    @else
        {{number_format($sp, $currency_precision, $currency_details-&gt;decimal_separator, $currency_details-&gt;thousand_separator)}}
    @endif
&lt;/td&gt;

@if(!empty($price_groups) &amp;&amp; is_iterable($price_groups))
    @foreach($price_groups as $price_group)
        &lt;td&gt;
            @php
                $group_price_group = $price_group-&gt;id;
                $current_group_price = 0;
                if(isset($variation_prices[$purchase_line-&gt;variation_id][$group_price_group]['price'])) {
                    $current_group_price = $variation_prices[$purchase_line-&gt;variation_id][$group_price_group]['price'];
                }
                $price_type = isset($variation_prices[$purchase_line-&gt;variation_id][$group_price_group]['price_type']) ? 
                    $variation_prices[$purchase_line-&gt;variation_id][$group_price_group]['price_type'] : 'fixed';
            @endphp
            {!! Form::text('purchases['.$loop-&gt;index.'][group_prices]['.$group_price_group.'][group_price]', 
                number_format($current_group_price, $currency_precision, $currency_details-&gt;decimal_separator, $currency_details-&gt;thousand_separator), 
                ['class' =&gt; 'form-control input-sm input_number']); !!}
            {!! Form::hidden('purchases['.$loop-&gt;index.'][group_prices]['.$group_price_group.'][group_price_id]', $price_group-&gt;id); !!}
            &lt;input type="hidden" name="purchases[{{$loop-&gt;index}}][group_prices][{{$group_price_group}}][group_price_type]" value="{{ $price_type }}"&gt;
        &lt;/td&gt;
    @endforeach
@endif
</code></pre><p>You can add a custom CSS rule to set minimum width for all form inputs on the edit page. Place this <code>&lt;style&gt;</code> block at the very top of your edit_purchase_entry_row.blade.php file, right after the PHP variables section:</p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>@php
    $hide_tax = '';
    if(session()-&gt;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 --}}
&lt;style&gt;
#purchase_entry_table .form-control {
    min-width: 5rem;
}
&lt;/style&gt;

&lt;div class="table-responsive"&gt;
    &lt;!-- rest of your table --&gt;
</code></pre><p>Or you can add a section CSS in <code>resources/views/purchase/create.blade.php</code> and <code>resources/views/purchase/edit.blade.php</code> pages:</p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>@section('css')
&lt;style&gt;
    #purchase_entry_table .form-control {
        min-width: 5rem;
    }
&lt;/style&gt;
@endsection
</code></pre><p><img src="https://ultimate-pos-tutorials.vercel.app/assets/images/purchase_entry_table_min_width-4bd0541950f757594c9aace96a788173.png" alt="Set minimum width for all form inputs on the edit page" class="ipsRichText__align--block" width="1700" height="244" loading="lazy"></p><h3><strong>2.5 Update ProductUtil for Price Group Processing</strong></h3><p><strong>File:</strong> <code>app/Utils/ProductUtil.php</code></p><p>Add the price group processing method to <code>ProductUtil</code>:</p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>public function createOrUpdateGroupPrice($group_prices, $variation_id)
{
    if (!empty($variation_id)) {
        foreach ($group_prices as $key =&gt; $value) {
            // \Log::info("Processing group price key: $key", $value);

            // Check if the record already exists
            $variation_group_price = VariationGroupPrice::where('variation_id', $variation_id)
                -&gt;where('price_group_id', $value['group_price_id'])
                -&gt;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-&gt;variation_id = $variation_id;
                $variation_group_price-&gt;price_group_id = $value['group_price_id'];
            } else {
                // \Log::info('UPDATING existing variation group price record', [
                //     'id' =&gt; $variation_group_price-&gt;id,
                //     'current_price' =&gt; $variation_group_price-&gt;price_inc_tax
                // ]);
            }

            // Convert price using num_uf
            $new_price = $this-&gt;num_uf($value['group_price']);

            // Update the values
            $variation_group_price-&gt;price_inc_tax = $new_price;
            $variation_group_price-&gt;price_type = $value['group_price_type'];

            // Save each record individually
            $result = $variation_group_price-&gt;save();
        }
    } else {
        \Log::warning('Variation ID is empty - skipping price group update');
    }
}
</code></pre><p>Update the <code>createOrUpdatePurchaseLines</code> method in <code>ProductUtil</code> 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.</p><p><strong>Fix: Modify your createOrUpdatePurchaseLines method to handle this structure:</strong></p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>/**
 * 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-&gt;exchange_rate) ? $transaction-&gt;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']) &amp;&amp; !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']) &amp;&amp; $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-&gt;base_unit_multiplier) ? $unit-&gt;base_unit_multiplier : 1;
        }
        $new_quantity = $this-&gt;num_uf($data['quantity']) * $multiplier;

        $new_quantity_f = $this-&gt;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-&gt;id;
            $old_qty = $purchase_line-&gt;quantity;

            $this-&gt;updateProductStock($before_status, $transaction, $data['product_id'], $data['variation_id'], $new_quantity, $purchase_line-&gt;quantity, $currency_details);
        } else {
            //create newly added purchase lines
            $purchase_line = new PurchaseLine();
            $purchase_line-&gt;product_id = $data['product_id'];
            $purchase_line-&gt;variation_id = $data['variation_id'];

            //Increase quantity only if status is received
            if ($transaction-&gt;status == 'received') {
                $this-&gt;updateProductQuantity($transaction-&gt;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']) &amp;&amp; !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-&gt;createOrUpdateGroupPrice($all_group_prices, $data['variation_id']);
        }

        $purchase_line-&gt;quantity = $new_quantity;
        $purchase_line-&gt;pp_without_discount = ($this-&gt;num_uf($data['pp_without_discount'], $currency_details) * $exchange_rate) / $multiplier;
        $purchase_line-&gt;discount_percent = $this-&gt;num_uf($data['discount_percent'], $currency_details);
        $purchase_line-&gt;purchase_price = ($this-&gt;num_uf($data['purchase_price'], $currency_details) * $exchange_rate) / $multiplier;
        $purchase_line-&gt;purchase_price_inc_tax = ($this-&gt;num_uf($data['purchase_price_inc_tax'], $currency_details) * $exchange_rate) / $multiplier;
        $purchase_line-&gt;item_tax = ($this-&gt;num_uf($data['item_tax'], $currency_details) * $exchange_rate) / $multiplier;
        $purchase_line-&gt;tax_id = $data['purchase_line_tax_id'];
        $purchase_line-&gt;lot_number = !empty($data['lot_number']) ? $data['lot_number'] : null;
        $purchase_line-&gt;mfg_date = !empty($data['mfg_date']) ? $this-&gt;uf_date($data['mfg_date']) : null;
        $purchase_line-&gt;exp_date = !empty($data['exp_date']) ? $this-&gt;uf_date($data['exp_date']) : null;
        $purchase_line-&gt;sub_unit_id = !empty($data['sub_unit_id']) ? $data['sub_unit_id'] : null;
        $purchase_line-&gt;purchase_order_line_id = !empty($data['purchase_order_line_id']) ? $data['purchase_order_line_id'] : null;
        $purchase_line-&gt;purchase_requisition_line_id = !empty($data['purchase_requisition_line_id']) &amp;&amp; $transaction-&gt;type == 'purchase_order' ? $data['purchase_requisition_line_id'] : null;

        if (!empty($data['secondary_unit_quantity'])) {
            $purchase_line-&gt;secondary_unit_quantity = $this-&gt;num_uf($data['secondary_unit_quantity']);
        }

        $updated_purchase_lines[] = $purchase_line;

        //Edit product price
        if ($enable_product_editing == 1 &amp;&amp; $transaction-&gt;type == 'purchase') {
            if (isset($data['default_sell_price'])) {
                $variation_data['sell_price_inc_tax'] = ($this-&gt;num_uf($data['default_sell_price'], $currency_details)) / $multiplier;
            }
            $variation_data['pp_without_discount'] = ($this-&gt;num_uf($data['pp_without_discount'], $currency_details) * $exchange_rate) / $multiplier;
            $variation_data['variation_id'] = $purchase_line-&gt;variation_id;
            $variation_data['purchase_price'] = $purchase_line-&gt;purchase_price;

            $this-&gt;updateProductFromPurchase($variation_data);
        }

        if ($transaction-&gt;type == 'purchase_order') {
            //Update purchase requisition line quantity received
            $this-&gt;updatePurchaseOrderLine($purchase_line-&gt;purchase_requisition_line_id, $purchase_line-&gt;quantity, $old_qty);
        }

        //Update purchase order line quantity received
        $this-&gt;updatePurchaseOrderLine($purchase_line-&gt;purchase_order_line_id, $purchase_line-&gt;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-&gt;id)
            -&gt;whereNotIn('id', $updated_purchase_line_ids)
            -&gt;get();

        if ($delete_purchase_lines-&gt;count()) {
            foreach ($delete_purchase_lines as $delete_purchase_line) {
                $delete_purchase_line_ids[] = $delete_purchase_line-&gt;id;

                //decrease deleted only if previous status was received
                if ($before_status == 'received') {
                    $this-&gt;decreaseProductQuantity(
                        $delete_purchase_line-&gt;product_id,
                        $delete_purchase_line-&gt;variation_id,
                        $transaction-&gt;location_id,
                        $delete_purchase_line-&gt;quantity
                    );
                }

                //If purchase order line set decrease quantity
                if (!empty($delete_purchase_line-&gt;purchase_order_line_id)) {
                    $this-&gt;updatePurchaseOrderLine($delete_purchase_line-&gt;purchase_order_line_id, 0, $delete_purchase_line-&gt;quantity);
                }

                //If purchase order line set decrease quantity
                if (!empty($delete_purchase_line-&gt;purchase_requisition_line_id)) {
                    $this-&gt;updatePurchaseOrderLine($delete_purchase_line-&gt;purchase_requisition_line_id, 0, $delete_purchase_line-&gt;quantity);
                }
            }

            //unset if purchase order line from purchase lines if exists
            if ($transaction-&gt;type == 'purchase_order') {
                PurchaseLine::whereIn('purchase_order_line_id', $delete_purchase_line_ids)
                    -&gt;update(['purchase_order_line_id' =&gt; null]);
            }

            //Delete deleted purchase lines
            PurchaseLine::where('transaction_id', $transaction-&gt;id)
                -&gt;whereIn('id', $delete_purchase_line_ids)
                -&gt;delete();
        }
    }

    //update purchase lines
    if (!empty($updated_purchase_lines)) {
        $transaction-&gt;purchase_lines()-&gt;saveMany($updated_purchase_lines);
    }

    return $delete_purchase_lines;
}
</code></pre><p><strong>Location:</strong> Insert this code after the line where <code>$purchase_line</code> is created or found, and before the quantity assignment.</p><h3><strong>2.6 Update Import Template and Processing</strong></h3><p><strong>File:</strong> <code>import_purchase_products_template.xls</code></p><p>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.</p><p><strong>Add this import at the top of your PurchaseController.php file if it doesn't exist:</strong></p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>use Maatwebsite\Excel\Facades\Excel;
</code></pre><h4><strong>Original Template Structure:</strong></h4><pre spellcheck="" class="ipsCode" data-language="text"><code>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
</code></pre><h4><strong>Updated Template Structure:</strong></h4><pre spellcheck="" class="ipsCode" data-language="text"><code>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)
</code></pre><h4><strong>Template Update Instructions:</strong></h4><ol><li><p><strong>Add Dynamic Columns</strong>: The number of price group columns depends on how many selling price groups exist in your business</p></li><li><p><strong>Column Headers</strong>: Use the actual price group names as column headers</p></li><li><p><strong>Sample Data</strong>: Include sample price group values in the template</p></li><li><p><strong>Documentation</strong>: Update any accompanying documentation to explain the new columns</p></li></ol><h4><strong>Controller Update for Import Processing</strong></h4><p>You'll also need to update the <code>importPurchaseProducts()</code> method to process the additional price group columns:</p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>public function importPurchaseProducts(Request $request)
{
    try {
        $file = $request-&gt;file('file');

        $parsed_array = Excel::toArray([], $file);
        //Remove header row
        $imported_data = array_splice($parsed_array[0], 1);

        $business_id = $request-&gt;session()-&gt;get('user.business_id');
        $location_id = $request-&gt;input('location_id');
        $row_count = $request-&gt;input('row_count');
        // Get price groups for processing
        $price_groups = SellingPriceGroup::where('business_id', $business_id)-&gt;active()-&gt;get();

        $formatted_data = [];
        $row_index = 0;
        $error_msg = '';
        foreach ($imported_data as $key =&gt; $value) {
            $row_index = $key + 1;
            $temp_array = [];

            if (!empty($value[0])) {
                $variation = Variation::where('sub_sku', trim($value[0]))
                    -&gt;join('products', 'products.id', '=', 'variations.product_id')
                    -&gt;where('products.business_id', $business_id)
                    -&gt;with([
                        'product_variation',
                        'variation_location_details' =&gt; function ($q) use ($location_id) {
                            $q-&gt;where('location_id', $location_id);
                        },
                    ])
                    -&gt;select('variations.*')
                    -&gt;first();
                $temp_array['variation'] = $variation;

                if (empty($variation)) {
                    $error_msg = __('lang_v1.product_not_found_exception', ['row' =&gt; $row_index, 'sku' =&gt; $value[0]]);
                    break;
                }

                $product = Product::where('id', $variation-&gt;product_id)
                    -&gt;where('business_id', $business_id)
                    -&gt;with(['unit'])
                    -&gt;first();

                if (empty($product)) {
                    $error_msg = __('lang_v1.product_not_found_exception', ['row' =&gt; $row_index, 'sku' =&gt; $value[0]]);
                    break;
                }

                $temp_array['product'] = $product;

                $sub_units = $this-&gt;productUtil-&gt;getSubUnits($business_id, $product-&gt;unit-&gt;id, false, $product-&gt;id);

                $temp_array['sub_units'] = $sub_units;
            } else {
                $error_msg = __('lang_v1.product_not_found_exception', ['row' =&gt; $row_index, 'sku' =&gt; $value[0]]);
                break;
            }

            if (!empty($value[1])) {
                $temp_array['quantity'] = $value[1];
            } else {
                $error_msg = __('lang_v1.quantity_required', ['row' =&gt; $row_index]);
                break;
            }

            $temp_array['unit_cost_before_discount'] = !empty($value[2]) ? $value[2] : $variation-&gt;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)
                    -&gt;where('name', 'like', "%{$tax_name}%")
                    -&gt;first();

                $tax_id = $tax-&gt;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-&gt;productUtil-&gt;format_date($value[6]) : null;
            $temp_array['exp_date'] = !empty($value[7]) ? $this-&gt;productUtil-&gt;format_date($value[7]) : null;

            // Process price group columns (starting from column I = index 8)
            $price_group_data = [];
            foreach ($price_groups as $index =&gt; $price_group) {
                $column_index = 8 + $index; // Starting after expiry date column
                if (isset($value[$column_index]) &amp;&amp; !empty($value[$column_index])) {
                    $price_group_data[$price_group-&gt;id] = [
                        'group_price' =&gt; $value[$column_index],
                        'group_price_id' =&gt; $price_group-&gt;id,
                        'group_price_type' =&gt; 'fixed'
                    ];
                }
            }
            $temp_array['group_prices'] = $price_group_data;

            $formatted_data[] = $temp_array;
        }

        if (!empty($error_msg)) {
            return [
                'success' =&gt; false,
                'msg' =&gt; $error_msg,
            ];
        }

        $hide_tax = 'hide';
        if ($request-&gt;session()-&gt;get('business.enable_inline_tax') == 1) {
            $hide_tax = '';
        }

        $taxes = TaxRate::where('business_id', $business_id)
            -&gt;ExcludeForTaxGroup()
            -&gt;get();

        $currency_details = $this-&gt;transactionUtil-&gt;purchaseCurrencyDetails($business_id);

        $price_groups = SellingPriceGroup::where('business_id', $business_id)-&gt;get();

        $variation_prices = [];
        foreach ($formatted_data as $data) {
            if (!empty($data['product'])) {
                foreach ($data['product']-&gt;variations as $variation) {
                    foreach ($variation-&gt;group_prices as $group_price) {
                        $variation_prices[$variation-&gt;id][$group_price-&gt;price_group_id] = [
                            'price' =&gt; $group_price-&gt;price_inc_tax,
                            'price_type' =&gt; $group_price-&gt;price_type
                        ];
                    }
                }
            }
        }

        $html = view('purchase.partials.imported_purchase_product_rows')
            -&gt;with(compact('formatted_data', 'taxes', 'currency_details', 'hide_tax', 'row_count', 'price_groups', 'variation_prices'))-&gt;render();

        return [
            'success' =&gt; true,
            'msg' =&gt; __('lang_v1.imported'),
            'html' =&gt; $html,
        ];
    } catch (\Exception $e) {
        return [
            'success' =&gt; false,
            'msg' =&gt; $e-&gt;getMessage(),
        ];
    }
}
</code></pre><h4><strong>Template Generation Script (Optional)</strong></h4><p>You could create a dynamic template generator that creates the Excel file based on current price groups:</p><pre spellcheck="" class="ipsCode language-php" data-language="PHP"><code>public function downloadImportTemplate()
{
    $business_id = request()-&gt;session()-&gt;get('user.business_id');
    $price_groups = SellingPriceGroup::where('business_id', $business_id)-&gt;active()-&gt;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-&gt;name;
    }
    
    // Generate Excel file with these headers
    // ... Excel generation logic ...
}
</code></pre><h4><strong>Important Notes:</strong></h4><ol><li><p><strong>Backward Compatibility</strong>: Ensure the import still works if price group columns are missing</p></li><li><p><strong>Error Handling</strong>: Add validation for price group values in the import process</p></li><li><p><strong>Documentation</strong>: Update user documentation to explain the new template format</p></li><li><p><strong>Template Versioning</strong>: Consider versioning your templates if you have existing users</p></li></ol><hr><h2><strong>Key Implementation Features</strong></h2><ol><li><p><strong>Dynamic Price Group Headers</strong>: Price group columns are generated dynamically based on active selling price groups</p></li><li><p><strong>Pre-populated Values</strong>: When editing purchases, existing price group values are loaded and displayed</p></li><li><p><strong>Form Validation</strong>: Price group inputs use the same validation as other price fields</p></li><li><p><strong>Import Support</strong>: Price groups are included in the product import functionality</p></li><li><p><strong>Consistent UI</strong>: Price group fields follow the same styling and behavior as other input fields</p></li></ol><h2><strong>Testing the Integration</strong></h2><ol><li><p><strong>Create Purchase</strong>: Verify price group columns appear in the create purchase form</p></li><li><p><strong>Add Products</strong>: Ensure price group fields are populated when adding products to purchase</p></li><li><p><strong>Edit Purchase</strong>: Check that existing price group values are loaded correctly</p></li><li><p><strong>Import Products</strong>: Test that price group data is maintained during product import</p></li><li><p><strong>Data Persistence</strong>: Verify that price group values are saved and can be retrieved</p></li></ol><h2><strong>Troubleshooting</strong></h2><h3><strong>Common Issues:</strong></h3><ol><li><p><strong>Missing Price Group Columns</strong>: Ensure <code>$price_groups_all</code> is passed to the create view</p></li><li><p><strong>Empty Price Group Fields</strong>: Verify <code>$variation_prices</code> array is properly structured</p></li><li><p><strong>Form Submission Errors</strong>: Check that price group data structure matches expected format</p></li><li><p><strong>Import Issues</strong>: Ensure price group logic is included in import processing</p></li></ol><h3><strong>Performance Considerations:</strong></h3><ol><li><p><strong>Database Queries</strong>: Monitor the impact of additional joins and queries</p></li><li><p><strong>Frontend Rendering</strong>: Test with multiple price groups to ensure responsive UI</p></li><li><p><strong>Memory Usage</strong>: Check memory consumption with large datasets</p></li></ol><h2><strong>Best Practices</strong></h2><ol><li><p><strong>Error Handling</strong>: Always include try-catch blocks for price group operations</p></li><li><p><strong>Data Validation</strong>: Validate price group data before processing</p></li><li><p><strong>Logging</strong>: Add appropriate logging for debugging price group issues</p></li><li><p><strong>User Experience</strong>: Provide clear feedback when price groups are updated</p></li></ol><h2><strong>Next Steps</strong></h2><p>With both Step 1 (Product List Display) and Step 2 (Purchase Integration) complete, you now have:</p><ul><li><p><span class="ipsEmoji" title="">✅</span> Dynamic price group columns in product listings</p></li><li><p><span class="ipsEmoji" title="">✅</span> Price group management in purchase create/edit forms</p></li><li><p><span class="ipsEmoji" title="">✅</span> Automatic price group updates during purchase operations</p></li><li><p><span class="ipsEmoji" title="">✅</span> Support for product imports with price groups</p></li></ul><p>Your Ultimate POS now has full price group functionality integrated across the product and purchase systems!</p>]]></description><guid isPermaLink="false">20</guid><pubDate>Wed, 17 Dec 2025 21:48:45 +0000</pubDate></item><item><title>Camera Barcode Scanner</title><link>https://doniaweb.com/blogs/entry/19-camera-barcode-scanner/</link><description><![CDATA[<h1><strong>Ultimate POS Camera Barcode Scanner Implementation Guide</strong></h1><p>A comprehensive step-by-step tutorial for implementing a universal camera barcode scanner across all Ultimate POS modules.</p><h2><strong><span class="ipsEmoji" title="">📋</span> Overview</strong></h2><p>This 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.</p><p><img src="https://ultimate-pos-tutorials.vercel.app/assets/images/button-barcode-scanner-9c04393f973de15b4d6040fb7ad1ab44.png" alt="Camera Button in POS" class="ipsRichText__align--block" width="943" height="147" loading="lazy"> <em>Camera scanner button integrated seamlessly in POS interface</em></p><h3><strong>Features</strong></h3><ul><li><p><span class="ipsEmoji" title="">✅</span> Universal compatibility across all modules</p></li><li><p><span class="ipsEmoji" title="">✅</span> Automatic page detection and appropriate behavior</p></li><li><p><span class="ipsEmoji" title="">✅</span> Reusable Blade component</p></li><li><p><span class="ipsEmoji" title="">✅</span> Fallback support for different search endpoints</p></li><li><p><span class="ipsEmoji" title="">✅</span> Multiple styling options</p></li><li><p><span class="ipsEmoji" title="">✅</span> HTTPS camera access with proper error handling</p></li><li><p><span class="ipsEmoji" title="">✅</span> Mobile-optimized scanning interface</p></li><li><p><span class="ipsEmoji" title="">✅</span> Real-time barcode detection</p></li></ul><p><img src="https://ultimate-pos-tutorials.vercel.app/assets/images/barcode-scanning-e83a43e490099b1f1ce7a72f39e0f888.png" alt="Barcode Scanning Interface" class="ipsRichText__align--block" width="993" height="1600" loading="lazy"> <em>Professional scanning interface with real-time camera feed</em></p><hr><h2><strong><span class="ipsEmoji" title="">🚀</span> Quick Start</strong></h2><h3><strong>Prerequisites</strong></h3><ul><li><p>Ultimate POS system</p></li><li><p>HTTPS connection (required for camera access)</p></li><li><p>Modern browser with camera support</p></li></ul><h3><strong>Dependencies</strong></h3><ul><li><p>jQuery (already included in Ultimate POS)</p></li><li><p>Html5Qrcode library</p></li><li><p>Bootstrap (already included in Ultimate POS)</p></li></ul><hr><h2><strong><span class="ipsEmoji" title="">📁</span> File Structure</strong></h2><pre spellcheck="" class="ipsCode" data-language="text"><code>├── 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
</code></pre><hr><h2><strong><span class="ipsEmoji" title="">🛠️</span> Step 1: Create the Reusable Component</strong></h2><p>Create the file <code>resources/views/components/camera-barcode-scanner.blade.php</code>:</p><pre spellcheck="" class="ipsCode" data-language="blade"><code>{{--
    Camera Barcode Scanner Component
    Usage: &lt;x-camera-barcode-scanner search-input-id="search_product" /&gt;

    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' =&gt; 'search_product',
    'buttonClass' =&gt; '',
    'showInGroup' =&gt; true,
    'buttonStyle' =&gt; 'default',
    'fullWidth' =&gt; false,
    'buttonText' =&gt; 'Scan Barcode'
])

@if($showInGroup)
    {{-- Camera Barcode Scanner Button for Input Groups --}}
    &lt;button type="button"
            class="btn btn-default bg-white btn-flat camera-barcode-scanner-btn {{ $buttonClass }}"
            data-search-input="{{ $searchInputId }}"
            title="Scan Barcode"&gt;
        &lt;i class="fa fa-camera text-primary fa-lg"&gt;&lt;/i&gt;
    &lt;/button&gt;
@else
    {{-- Standalone Camera Button --}}
    &lt;button type="button"
            class="btn btn-{{ $buttonStyle }} camera-barcode-scanner-btn {{ $buttonClass }} @if($fullWidth) btn-block @endif"
            data-search-input="{{ $searchInputId }}"
            title="Scan Barcode"&gt;
        &lt;i class="fa fa-camera @if($buttonStyle === 'link') text-primary @endif"&gt;&lt;/i&gt; {{ $buttonText }}
    &lt;/button&gt;
@endif

{{-- Camera Scanner Modal (only include once per page) --}}
@once
&lt;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;"&gt;
    &lt;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%;"&gt;
        &lt;div style="margin-bottom: 15px;"&gt;
            &lt;h4 style="margin: 0; color: #333;"&gt;</code></pre><p><code><span class="ipsEmoji" title="">📷</span></code></p><p><code> Barcode Scanner&lt;/h4&gt;<br>            &lt;button type="button"<br>                    onclick="event.preventDefault(); event.stopPropagation(); CameraBarcodeScanner.closeModalOnly(); return false;"<br>                    style="position: absolute; top: 10px; right: 15px; background: none; border: none; font-size: 20px; cursor: pointer;"&gt;&amp;times;&lt;/button&gt;<br>        &lt;/div&gt;<br><br>        {{-- Html5Qrcode Scanner --}}<br>        &lt;div id="reader" style="width: 100%; max-width: 500px; border: 2px solid #007bff; border-radius: 8px;"&gt;&lt;/div&gt;<br><br>        &lt;div id="status" style="margin-top: 15px; padding: 10px; background: #f8f9fa; border-radius: 5px; color: #333;"&gt;<br>            Starting camera...<br>        &lt;/div&gt;<br>    &lt;/div&gt;<br>&lt;/div&gt;<br><br>&lt;style&gt;<br>    #barcode_scanner_btn:hover,<br>    .camera-barcode-scanner-btn:hover {<br>        background-color: #e3f2fd !important;<br>        border-color: #2196f3 !important;<br>    }<br><br>    #reader video {<br>        border-radius: 5px;<br>    }<br><br>    #camera_modal {<br>        backdrop-filter: blur(3px);<br>    }<br><br>    @media (max-width: 768px) {<br>        #camera_modal &gt; div {<br>            width: 95% !important;<br>            max-width: none !important;<br>        }<br>    }<br>&lt;/style&gt;<br>@endonce<br></code></p><hr><h2><strong><span class="ipsEmoji" title="">🔧</span> Step 2: Create the Universal JavaScript</strong></h2><p>Create the file <code>public/js/camera-barcode-scanner.js</code>:</p><pre spellcheck="" class="ipsCode language-javascript" data-language="JavaScript"><code>/**
 * 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('</code></pre><p><code><span class="ipsEmoji" title="">🎥</span></code></p><p><code> Initializing Universal Camera Barcode Scanner...');<br>            try {<br>                this.html5QrCode = new Html5Qrcode('reader');<br>                this.detectPageType();<br>                this.bindEvents();<br>            } catch (error) {<br>                console.error(<br>                    '<span class="ipsEmoji" title="">❌</span> Camera scanner initialization failed:',<br>                    error<br>                );<br>            }<br>        },<br><br>        detectPageType: function () {<br>            const currentUrl = window.location.pathname;<br><br>            if (currentUrl.includes('/pos/')) {<br>                this.currentPageType = 'pos';<br>            } else if (currentUrl.includes('/sells/')) {<br>                this.currentPageType = 'sell';<br>            } else if (<br>                currentUrl.includes('/purchases/') ||<br>                currentUrl.includes('/purchase-order/')<br>            ) {<br>                this.currentPageType = 'purchase';<br>            } else if (currentUrl.includes('/stock-adjustments/')) {<br>                this.currentPageType = 'stock_adjustment';<br>            } else if (currentUrl.includes('/stock-transfers/')) {<br>                this.currentPageType = 'transfer';<br>            } else if (currentUrl.includes('/purchase-return/')) {<br>                this.currentPageType = 'purchase_return';<br>            } else {<br>                this.currentPageType = 'general';<br>            }<br>        },<br><br>        bindEvents: function () {<br>            const self = this;<br><br>            $(document).on(<br>                'click',<br>                '.camera-barcode-scanner-btn, #barcode_scanner_btn',<br>                function (e) {<br>                    e.preventDefault();<br>                    e.stopPropagation();<br><br>                    const searchInputId =<br>                        $(this).data('search-input') || 'search_product';<br>                    self.currentSearchInputId = searchInputId;<br>                    self.openCamera();<br>                }<br>            );<br><br>            $(document).on('keydown', function (e) {<br>                if (e.ctrlKey &amp;&amp; e.key === 'b') {<br>                    e.preventDefault();<br>                    $(<br>                        '.camera-barcode-scanner-btn:visible, #barcode_scanner_btn:visible'<br>                    )<br>                        .first()<br>                        .click();<br>                }<br>            });<br>        },<br><br>        openCamera: function () {<br>            if (!this.html5QrCode) return;<br>            if (<br>                location.protocol !== 'https:' &amp;&amp;<br>                location.hostname !== 'localhost'<br>            ) {<br>                alert('<span class="ipsEmoji" title="">❌</span> Camera requires HTTPS connection');<br>                return;<br>            }<br><br>            $('#camera_modal').show();<br>            this.startCamera();<br>        },<br><br>        startCamera: function () {<br>            if (this.isScanning) return;<br><br>            const self = this;<br>            this.html5QrCode<br>                .start(<br>                    this.config.cameraConfig,<br>                    this.config.scanConfig,<br>                    function (decodedText) {<br>                        self.closeCamera();<br>                        self.processBarcodeForCurrentPage(decodedText);<br>                        if (typeof toastr !== 'undefined') {<br>                            toastr.success('Barcode scanned: ' + decodedText);<br>                        }<br>                    },<br>                    function () {<br>                        $('#status').text(<br>                            '<span class="ipsEmoji" title="">📷</span> Scanning... Position barcode in view'<br>                        );<br>                    }<br>                )<br>                .then(() =&gt; {<br>                    self.isScanning = true;<br>                    $('#status').text('<span class="ipsEmoji" title="">📷</span> Camera active');<br>                })<br>                .catch((err) =&gt; {<br>                    $('#status').text('<span class="ipsEmoji" title="">❌</span> Camera error: ' + err);<br>                    setTimeout(() =&gt; self.closeCamera(), 2000);<br>                });<br>        },<br><br>        processBarcodeForCurrentPage: function (barcode) {<br>            const searchInput = $('#' + this.currentSearchInputId);<br>            if (searchInput.length) {<br>                searchInput.val(barcode).trigger('input').trigger('change');<br>            }<br>        },<br><br>        closeCamera: function () {<br>            if (this.isScanning &amp;&amp; this.html5QrCode) {<br>                this.html5QrCode.stop().then(() =&gt; {<br>                    this.isScanning = false;<br>                });<br>            }<br>            $('#camera_modal').hide();<br>        },<br><br>        closeModalOnly: function () {<br>            this.closeCamera();<br>            return false;<br>        },<br>    };<br><br>    $(document).ready(function () {<br>        if (<br>            $('.camera-barcode-scanner-btn').length ||<br>            $('#barcode_scanner_btn').length<br>        ) {<br>            CameraBarcodeScanner.init();<br>        }<br>    });<br>})(jQuery);<br></code></p><hr><h2><strong><span class="ipsEmoji" title="">📜</span> Step 3: Include Required Scripts</strong></h2><p>Update <code>resources/views/layouts/partials/javascripts.blade.php</code>:</p><pre spellcheck="" class="ipsCode" data-language="blade"><code>&lt;!-- Camera Barcode Scanner Dependencies --&gt;
&lt;script src="https://unpkg.com/html5-qrcode@2.3.8/html5-qrcode.min.js"&gt;&lt;/script&gt;
&lt;script src="{{ asset('js/camera-barcode-scanner.js') }}"&gt;&lt;/script&gt;
</code></pre><hr><h2><strong><span class="ipsEmoji" title="">📑</span> Step 4: Implementation by Page Type</strong></h2><h3><strong><span class="ipsEmoji" title="">🛒</span> POS System</strong></h3><p><strong>File:</strong> <code>resources/views/sale_pos/partials/pos_form.blade.php</code></p><h4><strong>Solution 1: Default Settings</strong></h4><pre spellcheck="" class="ipsCode" data-language="blade"><code>&lt;span class="input-group-btn"&gt;
    {{-- Camera Barcode Scanner Button --}}
    &lt;x-camera-barcode-scanner search-input-id="search_product" /&gt;

    &lt;!-- Other buttons... --&gt;
&lt;/span&gt;
</code></pre><h4><strong>Solution 2: Better Styling</strong></h4><pre spellcheck="" class="ipsCode" data-language="blade"><code>&lt;span class="input-group-btn"&gt;
    {{-- Camera Barcode Scanner Button --}}
    &lt;x-camera-barcode-scanner
        search-input-id="search_product"
        button-class="pos-camera-btn" /&gt;

    &lt;!-- Other buttons... --&gt;
&lt;/span&gt;
</code></pre><hr><h3><strong><span class="ipsEmoji" title="">💰</span> Sales Pages</strong></h3><p><strong>Files:</strong> <code>resources/views/sell/create.blade.php</code>, <code>resources/views/sell/edit.blade.php</code></p><h4><strong>Solution 1: Default Settings</strong></h4><pre spellcheck="" class="ipsCode" data-language="blade"><code>&lt;div class="input-group"&gt;
    &lt;div class="input-group-btn"&gt;
        &lt;button type="button" class="btn btn-default bg-white btn-flat" data-toggle="modal" data-target="#configure_search_modal"&gt;
            &lt;i class="fas fa-search-plus"&gt;&lt;/i&gt;
        &lt;/button&gt;
    &lt;/div&gt;
    {!! Form::text('search_product', null, ['class' =&gt; 'form-control mousetrap', 'id' =&gt; 'search_product', 'placeholder' =&gt; __('lang_v1.search_product_placeholder')]) !!}
    &lt;span class="input-group-btn"&gt;
        &lt;x-camera-barcode-scanner search-input-id="search_product" /&gt;

        &lt;button type="button" class="btn btn-default bg-white btn-flat pos_add_quick_product"&gt;
            &lt;i class="fa fa-plus-circle text-primary fa-lg"&gt;&lt;/i&gt;
        &lt;/button&gt;
    &lt;/span&gt;
&lt;/div&gt;
</code></pre><h4><strong>Solution 2: Better Styling</strong></h4><pre spellcheck="" class="ipsCode" data-language="blade"><code>&lt;div class="input-group"&gt;
    &lt;div class="input-group-btn"&gt;
        &lt;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')}}"&gt;
            &lt;i class="fas fa-search-plus"&gt;&lt;/i&gt;
        &lt;/button&gt;
    &lt;/div&gt;
    {!! Form::text('search_product', null, ['class' =&gt; 'form-control mousetrap', 'id' =&gt; 'search_product', 'placeholder' =&gt; __('lang_v1.search_product_placeholder')]) !!}
    &lt;span class="input-group-btn"&gt;
        &lt;x-camera-barcode-scanner
            search-input-id="search_product"
            button-class="sales-camera-btn"
            title="Scan Product Barcode" /&gt;

        &lt;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"&gt;
            &lt;i class="fa fa-plus-circle text-primary fa-lg"&gt;&lt;/i&gt;
        &lt;/button&gt;
    &lt;/span&gt;
&lt;/div&gt;
</code></pre><hr><h3><strong><span class="ipsEmoji" title="">📦</span> Purchase Pages</strong></h3><p><strong>Files:</strong> <code>resources/views/purchase/create.blade.php</code>, <code>resources/views/purchase/edit.blade.php</code></p><h4><strong>Solution 1: Default Settings</strong></h4><pre spellcheck="" class="ipsCode" data-language="blade"><code>&lt;div class="col-sm-2"&gt;
    &lt;div class="form-group"&gt;
        &lt;x-camera-barcode-scanner search-input-id="search_product" /&gt;

        &lt;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"&gt;
            &lt;i class="fa fa-plus"&gt;&lt;/i&gt; @lang('product.add_new_product')
        &lt;/button&gt;
    &lt;/div&gt;
&lt;/div&gt;
</code></pre><h4><strong>Solution 2: Better Styling</strong></h4><pre spellcheck="" class="ipsCode" data-language="blade"><code>&lt;div class="col-sm-2"&gt;
    &lt;div class="form-group"&gt;
        &lt;x-camera-barcode-scanner
            search-input-id="search_product"
            :show-in-group="false"
            button-style="link"
            button-text="</code></pre><p><code><span class="ipsEmoji" title="">📷</span></code></p><p><code> Scan Barcode"<br>            :full-width="true" /&gt;<br><br>        &lt;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"&gt;<br>            &lt;i class="fa fa-plus"&gt;&lt;/i&gt; @lang('product.add_new_product')<br>        &lt;/button&gt;<br>    &lt;/div&gt;<br>&lt;/div&gt;<br></code></p><hr><h3><strong><span class="ipsEmoji" title="">📋</span> Purchase Order Pages</strong></h3><p><strong>Files:</strong> <code>resources/views/purchase_order/create.blade.php</code>, <code>resources/views/purchase_order/edit.blade.php</code></p><h4><strong>Solution 1: Default Settings</strong></h4><pre spellcheck="" class="ipsCode" data-language="blade"><code>&lt;div class="col-sm-2"&gt;
    &lt;div class="form-group"&gt;
        &lt;x-camera-barcode-scanner search-input-id="search_product" /&gt;

        &lt;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"&gt;
            &lt;i class="fa fa-plus"&gt;&lt;/i&gt; @lang('product.add_new_product')
        &lt;/button&gt;
    &lt;/div&gt;
&lt;/div&gt;
</code></pre><h4><strong>Solution 2: Better Styling</strong></h4><pre spellcheck="" class="ipsCode" data-language="blade"><code>&lt;div class="col-sm-2"&gt;
    &lt;div class="form-group"&gt;
        &lt;x-camera-barcode-scanner
            search-input-id="search_product"
            :show-in-group="false"
            button-style="link"
            button-text="</code></pre><p><code><span class="ipsEmoji" title="">🎥</span></code></p><p><code> Scan Product"<br>            :full-width="true"<br>            button-class="purchase-order-scanner" /&gt;<br><br>        &lt;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"&gt;<br>            &lt;i class="fa fa-plus"&gt;&lt;/i&gt; @lang('product.add_new_product')<br>        &lt;/button&gt;<br>    &lt;/div&gt;<br>&lt;/div&gt;<br></code></p><hr><h3><strong><span class="ipsEmoji" title="">🔄</span> Stock Transfer Pages</strong></h3><p><strong>Files:</strong> <code>resources/views/stock_transfer/create.blade.php</code>, <code>resources/views/stock_transfer/edit.blade.php</code></p><h4><strong>Solution 1: Default Settings</strong></h4><pre spellcheck="" class="ipsCode" data-language="blade"><code>&lt;div class="form-group"&gt;
    &lt;div class="input-group"&gt;
        &lt;span class="input-group-addon"&gt;
            &lt;i class="fa fa-search"&gt;&lt;/i&gt;
        &lt;/span&gt;
        {!! Form::text('search_product', null, ['class' =&gt; 'form-control', 'id' =&gt; 'search_product_for_srock_adjustment', 'placeholder' =&gt; __('stock_adjustment.search_product')]) !!}
        &lt;span class="input-group-btn"&gt;
            &lt;x-camera-barcode-scanner search-input-id="search_product_for_srock_adjustment" /&gt;
        &lt;/span&gt;
    &lt;/div&gt;
&lt;/div&gt;
</code></pre><h4><strong>Solution 2: Better Styling</strong></h4><pre spellcheck="" class="ipsCode" data-language="blade"><code>&lt;div class="form-group"&gt;
    &lt;div class="input-group"&gt;
        &lt;span class="input-group-addon"&gt;
            &lt;i class="fa fa-search"&gt;&lt;/i&gt;
        &lt;/span&gt;
        {!! Form::text('search_product', null, ['class' =&gt; 'form-control', 'id' =&gt; 'search_product_for_stock_transfer', 'placeholder' =&gt; __('lang_v1.search_product_placeholder')]) !!}
        &lt;span class="input-group-btn"&gt;
            &lt;x-camera-barcode-scanner
                search-input-id="search_product_for_stock_transfer"
                button-class="transfer-scanner-btn"
                title="Scan Product for Transfer" /&gt;
        &lt;/span&gt;
    &lt;/div&gt;
&lt;/div&gt;
</code></pre><hr><h3><strong><span class="ipsEmoji" title="">📊</span> Stock Adjustment Pages</strong></h3><p><strong>Files:</strong> <code>resources/views/stock_adjustment/create.blade.php</code></p><h4><strong>Solution 1: Default Settings</strong></h4><pre spellcheck="" class="ipsCode" data-language="blade"><code>&lt;div class="form-group"&gt;
    &lt;div class="input-group"&gt;
        &lt;span class="input-group-addon"&gt;
            &lt;i class="fa fa-search"&gt;&lt;/i&gt;
        &lt;/span&gt;
        {!! Form::text('search_product', null, ['class' =&gt; 'form-control','id' =&gt; 'search_product_for_srock_adjustment','placeholder' =&gt; __('stock_adjustment.search_product'),'disabled',]) !!}
        &lt;span class="input-group-btn"&gt;
            &lt;x-camera-barcode-scanner search-input-id="search_product_for_srock_adjustment" /&gt;
        &lt;/span&gt;
    &lt;/div&gt;
&lt;/div&gt;
</code></pre><h4><strong>Solution 2: Better Styling</strong></h4><pre spellcheck="" class="ipsCode" data-language="blade"><code>&lt;div class="form-group"&gt;
    &lt;div class="input-group"&gt;
        &lt;span class="input-group-addon"&gt;
            &lt;i class="fa fa-search"&gt;&lt;/i&gt;
        &lt;/span&gt;
        {!! Form::text('search_product', null, ['class' =&gt; 'form-control','id' =&gt; 'search_product_for_stock_adjustment','placeholder' =&gt; __('stock_adjustment.search_product'),'disabled',]) !!}
        &lt;span class="input-group-btn"&gt;
            &lt;x-camera-barcode-scanner
                search-input-id="search_product_for_stock_adjustment"
                button-class="adjustment-scanner-btn"
                title="Scan Product for Adjustment" /&gt;
        &lt;/span&gt;
    &lt;/div&gt;
&lt;/div&gt;
</code></pre><hr><h3><strong><span class="ipsEmoji" title="">🔙</span> Purchase Return Pages</strong></h3><p><strong>Files:</strong> <code>resources/views/purchase_return/create.blade.php</code>, <code>resources/views/purchase_return/edit.blade.php</code></p><h4><strong>Solution 1: Default Settings</strong></h4><pre spellcheck="" class="ipsCode" data-language="blade"><code>&lt;div class="col-sm-2"&gt;
    &lt;div class="form-group"&gt;
        &lt;x-camera-barcode-scanner search-input-id="search_product" /&gt;

        &lt;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"&gt;
            &lt;i class="fa fa-plus"&gt;&lt;/i&gt; @lang('product.add_new_product')
        &lt;/button&gt;
    &lt;/div&gt;
&lt;/div&gt;
</code></pre><h4><strong>Solution 2: Better Styling</strong></h4><pre spellcheck="" class="ipsCode" data-language="blade"><code>&lt;div class="col-sm-2"&gt;
    &lt;div class="form-group"&gt;
        &lt;x-camera-barcode-scanner
            search-input-id="search_product"
            :show-in-group="false"
            button-style="link"
            button-text="</code></pre><p><code><span class="ipsEmoji" title="">📱</span></code></p><p><code> Scan Return"<br>            :full-width="true"<br>            button-class="return-scanner-btn" /&gt;<br><br>        &lt;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"&gt;<br>            &lt;i class="fa fa-plus"&gt;&lt;/i&gt; @lang('product.add_new_product')<br>        &lt;/button&gt;<br>    &lt;/div&gt;<br>&lt;/div&gt;<br></code></p><hr><h2><strong><span class="ipsEmoji" title="">🎛️</span> Component Configuration Options</strong></h2><h3><strong>Props Available</strong></h3><div class="ipsRichText__table-wrapper"><table style="min-width: 80px;"><colgroup><col style="min-width:20px;"><col style="min-width:20px;"><col style="min-width:20px;"><col style="min-width:20px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p><strong>Prop</strong></p></th><th colspan="1" rowspan="1"><p><strong>Type</strong></p></th><th colspan="1" rowspan="1"><p><strong>Default</strong></p></th><th colspan="1" rowspan="1"><p><strong>Description</strong></p></th></tr><tr><td colspan="1" rowspan="1"><p><code>search-input-id</code></p></td><td colspan="1" rowspan="1"><p>String</p></td><td colspan="1" rowspan="1"><p><code>'search_product'</code></p></td><td colspan="1" rowspan="1"><p>ID of the target search input</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>button-class</code></p></td><td colspan="1" rowspan="1"><p>String</p></td><td colspan="1" rowspan="1"><p><code>''</code></p></td><td colspan="1" rowspan="1"><p>Additional CSS classes</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>:show-in-group</code></p></td><td colspan="1" rowspan="1"><p>Boolean</p></td><td colspan="1" rowspan="1"><p><code>true</code></p></td><td colspan="1" rowspan="1"><p>Input group button vs standalone</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>button-style</code></p></td><td colspan="1" rowspan="1"><p>String</p></td><td colspan="1" rowspan="1"><p><code>'default'</code></p></td><td colspan="1" rowspan="1"><p>Bootstrap button style</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>:full-width</code></p></td><td colspan="1" rowspan="1"><p>Boolean</p></td><td colspan="1" rowspan="1"><p><code>false</code></p></td><td colspan="1" rowspan="1"><p>Full width button</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>button-text</code></p></td><td colspan="1" rowspan="1"><p>String</p></td><td colspan="1" rowspan="1"><p><code>'Scan Barcode'</code></p></td><td colspan="1" rowspan="1"><p>Text for standalone buttons</p></td></tr></tbody></table></div><h3><strong>Usage Examples</strong></h3><h4><strong>Input Group Button (Default)</strong></h4><pre spellcheck="" class="ipsCode" data-language="blade"><code>&lt;x-camera-barcode-scanner search-input-id="search_product" /&gt;
</code></pre><h4><strong>Standalone Link Button</strong></h4><pre spellcheck="" class="ipsCode" data-language="blade"><code>&lt;x-camera-barcode-scanner
    search-input-id="search_product"
    :show-in-group="false"
    button-style="link"
    button-text="</code></pre><p><code><span class="ipsEmoji" title="">📷</span></code></p><p><code> Scan Barcode"<br>    :full-width="true" /&gt;<br></code></p><h4><strong>Success Button with Custom Class</strong></h4><pre spellcheck="" class="ipsCode" data-language="blade"><code>&lt;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" /&gt;
</code></pre><hr><h2><strong><span class="ipsEmoji" title="">🔧</span> Troubleshooting</strong></h2><h3><strong>Common Issues</strong></h3><h4><strong>Camera Not Working</strong></h4><ul><li><p><strong>Problem:</strong> Camera doesn't start</p></li><li><p><strong>Solution:</strong> Ensure HTTPS connection</p></li><li><p><strong>Check:</strong> Browser permissions for camera access</p></li></ul><h4><strong>Button Not Responding</strong></h4><ul><li><p><strong>Problem:</strong> Click doesn't open camera</p></li><li><p><strong>Solution:</strong> Check if scripts are loaded properly</p></li><li><p><strong>Check:</strong> Browser console for JavaScript errors</p></li></ul><h4><strong>Wrong Input Field</strong></h4><ul><li><p><strong>Problem:</strong> Barcode goes to wrong input</p></li><li><p><strong>Solution:</strong> Verify <code>search-input-id</code> matches actual input ID</p></li><li><p><strong>Check:</strong> Inspect element to confirm input ID</p></li></ul><h3><strong>Debug Mode</strong></h3><p>Add this to your page to debug:</p><pre spellcheck="" class="ipsCode language-javascript" data-language="JavaScript"><code>console.log('Scanner available:', CameraBarcodeScanner.isAvailable());
console.log('Current page type:', CameraBarcodeScanner.currentPageType);
console.log('Target input:', CameraBarcodeScanner.currentSearchInputId);
</code></pre><hr><h2><strong><span class="ipsEmoji" title="">🚀</span> Advanced Features</strong></h2><h3><strong>Keyboard Shortcut</strong></h3><ul><li><p>Press <code>Ctrl + B</code> to open camera scanner</p></li><li><p>Works on any page with scanner buttons</p></li></ul><h3><strong>Mobile Support</strong></h3><ul><li><p>Responsive design for mobile devices</p></li><li><p>Touch-friendly buttons</p></li><li><p>Mobile camera optimization</p></li></ul><h3><strong>Error Handling</strong></h3><ul><li><p>Graceful fallback when camera unavailable</p></li><li><p>Clear error messages for users</p></li><li><p>Automatic retry mechanisms</p></li></ul><hr><h2><strong><span class="ipsEmoji" title="">📈</span> Performance Tips</strong></h2><h3><strong>Optimization</strong></h3><ol><li><p><strong>Single Modal:</strong> Modal included only once per page with <code>@once</code></p></li><li><p><strong>Event Delegation:</strong> Uses event delegation for better performance</p></li><li><p><strong>Lazy Loading:</strong> Scanner initializes only when needed</p></li><li><p><strong>Memory Management:</strong> Proper cleanup when camera stops</p></li></ol><h3><strong>Best Practices</strong></h3><ol><li><p>Use specific input IDs for better targeting</p></li><li><p>Test on HTTPS environment</p></li><li><p>Provide fallback for non-camera devices</p></li><li><p>Keep component props simple and focused</p></li></ol><hr><h2><strong><span class="ipsEmoji" title="">🔒</span> Security Considerations</strong></h2><h3><strong>HTTPS Requirement</strong></h3><ul><li><p>Camera access requires HTTPS</p></li><li><p>Development: Use <code>localhost</code> or HTTPS</p></li><li><p>Production: Ensure SSL certificate</p></li></ul><h3><strong>Permissions</strong></h3><ul><li><p>Browser requests camera permission</p></li><li><p>Users can deny and use manual input</p></li><li><p>Graceful degradation when permission denied</p></li></ul><hr><h2><strong><span class="ipsEmoji" title="">📱</span> Browser Compatibility</strong></h2><h3><strong>Supported Browsers</strong></h3><ul><li><p><span class="ipsEmoji" title="">✅</span> Chrome 60+</p></li><li><p><span class="ipsEmoji" title="">✅</span> Firefox 55+</p></li><li><p><span class="ipsEmoji" title="">✅</span> Safari 11+</p></li><li><p><span class="ipsEmoji" title="">✅</span> Edge 79+</p></li><li><p><span class="ipsEmoji" title="">✅</span> Mobile browsers (iOS Safari, Chrome Mobile)</p></li></ul><h3><strong>Fallback Support</strong></h3><ul><li><p>Automatic fallback to manual input</p></li><li><p>Works without camera on any device</p></li><li><p>Progressive enhancement approach</p></li></ul><hr><h2><strong><span class="ipsEmoji" title="">🎉</span> Conclusion</strong></h2><p>You now have a universal camera barcode scanner implemented across all Ultimate POS modules! The component is:</p><ul><li><p><strong>Reusable</strong> across all pages</p></li><li><p><strong>Flexible</strong> with multiple styling options</p></li><li><p><strong>Robust</strong> with error handling and fallbacks</p></li><li><p><strong>User-friendly</strong> with clear instructions</p></li><li><p><strong>Mobile-optimized</strong> for all devices</p></li></ul><h3><strong>Next Steps</strong></h3><ol><li><p>Test on all target pages</p></li><li><p>Customize styling to match your theme</p></li><li><p>Add additional features as needed</p></li><li><p>Monitor user feedback and iterate</p></li></ol><p>Happy scanning! <span class="ipsEmoji" title="">🎯</span><span class="ipsEmoji" title="">📱</span></p>]]></description><guid isPermaLink="false">19</guid><pubDate>Wed, 17 Dec 2025 20:49:01 +0000</pubDate></item></channel></rss>
