Skip to content
View in the app

A better way to browse. Learn more.

DoniaWeB

A full-screen app on your home screen with push notifications, badges and more.

To install this app on iOS and iPadOS
  1. Tap the Share icon in Safari
  2. Scroll the menu and tap Add to Home Screen.
  3. Tap Add in the top-right corner.
To install this app on Android
  1. Tap the 3-dot menu (⋮) in the top-right corner of the browser.
  2. Tap Add to Home screen or Install app.
  3. Confirm by tapping Install.
     

Mahmoud

Administrators
  • Joined

  • Last visited

Everything posted by Mahmoud

  1. Multiple Barcodes for Products - Complete Implementation GuideOverviewThis guide will walk you through the complete process of adding multiple barcodes functionality to your Ultimate POS products. This feature allows you to: Assign multiple barcodes to products (in addition to the main SKU) Search products by any of their barcodes in POS, Purchase, and Universal Search Select which barcode to print on labels Support different barcode types (C128, C39, EAN-13, etc.) for each barcode Add optional descriptions to identify each barcode (e.g., "Supplier barcode", "Internal code") This is useful when: Products come with manufacturer barcodes but you also use internal codes Suppliers provide their own barcode/SKU that differs from yours You need to support legacy barcodes after rebranding Downloadmulti-barcode-products.zip Step 1: Database StructureMigrationCreate a new migration for the product_barcodes table: php artisan make:migration create_product_barcodes_table Migration Content: <?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->id(); $table->unsignedInteger('product_id'); $table->string('barcode', 191); $table->string('barcode_type', 20)->default('C128'); $table->string('description', 191)->nullable(); $table->timestamps(); $table->foreign('product_id')->references('id')->on('products')->onDelete('cascade'); $table->index('barcode'); $table->unique(['product_id', 'barcode']); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists('product_barcodes'); } }; Direct SQL (Alternative): 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; Run Migration: php artisan migrate Step 2: Create ProductBarcode ModelCreate File: app/ProductBarcode.php <?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->belongsTo(\App\Product::class); } } Step 3: Update Product ModelFile: app/Product.php Add the relationship method at the end of the class (before the closing }): /** * Get the additional barcodes for the product. */ public function barcodes() { return $this->hasMany(\App\ProductBarcode::class); } Step 4: Update ProductUtilFile: app/Utils/ProductUtil.php 4.1 Add syncProductBarcodes MethodAdd this method after the generateSubSku() method: /** * Sync product barcodes. * * @param \App\Product $product * @param array $barcodes * @return void */ public function syncProductBarcodes($product, $barcodes) { // Delete existing barcodes $product->barcodes()->delete(); // Add new barcodes if (!empty($barcodes)) { foreach ($barcodes as $barcode) { if (!empty($barcode['barcode'])) { $product->barcodes()->create([ 'barcode' => $barcode['barcode'], 'barcode_type' => $barcode['barcode_type'] ?? 'C128', 'description' => $barcode['description'] ?? null, ]); } } } } 4.2 Update filterProduct Method for SearchIn the filterProduct() method, find the LIKE search block and add barcode search after sub_sku: if (in_array('sub_sku', $search_fields)) { $query->orWhere('sub_sku', 'like', '%'.$search_term.'%'); } // Search in product_barcodes table if (in_array('sku', $search_fields) || in_array('sub_sku', $search_fields)) { $query->orWhereExists(function ($subquery) use ($search_term) { $subquery->select(\DB::raw(1)) ->from('product_barcodes') ->whereColumn('product_barcodes.product_id', 'products.id') ->where('product_barcodes.barcode', 'like', '%'.$search_term.'%'); }); } Also add the same for the EXACT search block: if (in_array('sub_sku', $search_fields)) { $query->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->orWhereExists(function ($subquery) use ($search_term) { $subquery->select(\DB::raw(1)) ->from('product_barcodes') ->whereColumn('product_barcodes.product_id', 'products.id') ->where('product_barcodes.barcode', $search_term); }); } Step 5: Update ProductControllerFile: app/Http/Controllers/ProductController.php 5.1 Update store() MethodAfter the supplier sync code, add: //Add product barcodes $product_barcodes = $request->input('product_barcodes', []); if (!empty($product_barcodes)) { $this->productUtil->syncProductBarcodes($product, $product_barcodes); } 5.2 Update edit() MethodLoad the barcodes relationship: $product = Product::where('business_id', $business_id) ->with(['product_locations', 'suppliers', 'barcodes']) ->where('id', $id) ->firstOrFail(); 5.3 Update update() MethodAfter the supplier sync code, add: //Sync product barcodes $product_barcodes = $request->input('product_barcodes', []); $this->productUtil->syncProductBarcodes($product, $product_barcodes); Step 6: Update Search in Other Controllers6.1 SellPosControllerFile: app/Http/Controllers/SellPosController.php Find the search block in getProductSuggestion() method and update: //Include search if (!empty($term)) { $products->where(function ($query) use ($term) { $query->where('p.name', 'like', '%' . $term . '%'); $query->orWhere('sku', 'like', '%' . $term . '%'); $query->orWhere('sub_sku', 'like', '%' . $term . '%'); // Search in product_barcodes table $query->orWhereExists(function ($subquery) use ($term) { $subquery->select(\DB::raw(1)) ->from('product_barcodes') ->whereColumn('product_barcodes.product_id', 'p.id') ->where('product_barcodes.barcode', 'like', '%' . $term . '%'); }); }); } 6.2 PurchaseControllerFile: app/Http/Controllers/PurchaseController.php Update the search in getProducts() method: ->where(function ($query) use ($term) { $query->where('products.name', 'like', '%'.$term.'%'); $query->orWhere('sku', 'like', '%'.$term.'%'); $query->orWhere('sub_sku', 'like', '%'.$term.'%'); // Search in product_barcodes table $query->orWhereExists(function ($subquery) use ($term) { $subquery->select(\DB::raw(1)) ->from('product_barcodes') ->whereColumn('product_barcodes.product_id', 'products.id') ->where('product_barcodes.barcode', 'like', '%'.$term.'%'); }); }) 6.3 UniversalSearchControllerFile: app/Http/Controllers/UniversalSearchController.php Update the search in searchProducts() method: ->where(function($q) use ($query) { $q->where('products.name', 'like', "%{$query}%") ->orWhere('products.sku', 'like', "%{$query}%") ->orWhere('variations.sub_sku', 'like', "%{$query}%") ->orWhere('products.product_description', 'like', "%{$query}%") // Search in product_barcodes table ->orWhereExists(function ($subquery) use ($query) { $subquery->select(\DB::raw(1)) ->from('product_barcodes') ->whereColumn('product_barcodes.product_id', 'products.id') ->where('product_barcodes.barcode', 'like', "%{$query}%"); }); }) Step 7: Create Barcode Input Partial ViewCreate File: resources/views/product/partials/product_barcodes.blade.php This partial contains only the container for barcode rows - the add button is placed next to the SKU field. <div class="col-sm-12"> <div class="form-group"> <div id="product_barcodes_container"> @if(isset($product) && $product->barcodes && $product->barcodes->count() > 0) @foreach($product->barcodes as $index => $barcode) <div class="barcode-row row" style="margin-bottom: 10px;" data-row-index="{{ $index }}"> <div class="col-md-4"> <input type="text" name="product_barcodes[{{ $index }}][barcode]" class="form-control" placeholder="@lang('product.barcode')" value="{{ $barcode->barcode }}"> </div> <div class="col-md-3"> {!! Form::select("product_barcodes[{$index}][barcode_type]", $barcode_types, $barcode->barcode_type, ['class' => 'form-control select2', 'style' => 'width: 100%;'] ) !!} </div> <div class="col-md-4"> <input type="text" name="product_barcodes[{{ $index }}][description]" class="form-control" placeholder="@lang('product.barcode_description')" value="{{ $barcode->description }}"> </div> <div class="col-md-1"> <button type="button" class="btn btn-danger btn-xs remove-barcode-row" style="padding: 2px 6px;"> <i class="fa fa-times" style="font-size: 12px;"></i> </button> </div> </div> @endforeach @endif </div> </div> </div> <script type="text/template" id="barcode_row_template"> <div class="barcode-row row" style="margin-bottom: 10px;" data-row-index="__INDEX__"> <div class="col-md-4"> <input type="text" name="product_barcodes[__INDEX__][barcode]" class="form-control" placeholder="@lang('product.barcode')"> </div> <div class="col-md-3"> <select name="product_barcodes[__INDEX__][barcode_type]" class="form-control"> @foreach($barcode_types as $key => $value) <option value="{{ $key }}">{{ $value }}</option> @endforeach </select> </div> <div class="col-md-4"> <input type="text" name="product_barcodes[__INDEX__][description]" class="form-control" placeholder="@lang('product.barcode_description')"> </div> <div class="col-md-1"> <button type="button" class="btn btn-danger btn-xs remove-barcode-row" style="padding: 2px 6px;"> <i class="fa fa-times" style="font-size: 12px;"></i> </button> </div> </div> </script> Step 8: Update Product Create/Edit Views8.1 Update create.blade.phpFile: resources/views/product/create.blade.php First, update the SKU field to add the plus button (like Unit field): <div class="col-sm-4"> <div class="form-group"> {!! Form::label('sku', __('product.sku') . ':') !!} @show_tooltip(__('tooltip.sku')) <div class="input-group"> {!! Form::text('sku', null, ['class' => 'form-control', 'placeholder' => __('product.sku')]); !!} <span class="input-group-btn"> <button type="button" class="btn btn-default bg-white btn-flat" id="add_barcode_row" title="@lang('product.add_barcode')"> <i class="fa fa-plus-circle text-primary fa-lg"></i> </button> </span> </div> </div> </div> Then, after the barcode_type field and the first <div class="clearfix"></div>, add: @include('product.partials.product_barcodes') <div class="clearfix"></div> 8.2 Update edit.blade.phpFile: resources/views/product/edit.blade.php Update the SKU field to add the plus button: <div class="col-sm-4"> <div class="form-group"> {!! Form::label('sku', __('product.sku') . ':*') !!} @show_tooltip(__('tooltip.sku')) <div class="input-group"> {!! Form::text('sku', $product->sku, ['class' => 'form-control', 'placeholder' => __('product.sku'), 'required']); !!} <span class="input-group-btn"> <button type="button" class="btn btn-default bg-white btn-flat" id="add_barcode_row" title="@lang('product.add_barcode')"> <i class="fa fa-plus-circle text-primary fa-lg"></i> </button> </span> </div> </div> </div> Then include the partial after the barcode_type field: @include('product.partials.product_barcodes') <div class="clearfix"></div> Step 9: Add JavaScript for Dynamic Barcode RowsFile: public/js/product.js Add at the end of the file: // 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); } }); }); }); Step 10: Label Printing with Barcode Selection10.1 Update LabelsControllerFile: app/Http/Controllers/LabelsController.php Update the show() method to load product barcodes: //Get products for the business $products = []; $price_groups = []; if ($purchase_id) { $products = $this->transactionUtil->getPurchaseProducts($business_id, $purchase_id); } elseif ($product_id) { $products = $this->productUtil->getDetailsFromProduct($business_id, $product_id); } // Load product barcodes for each product if (!empty($products)) { foreach ($products as $product) { $product->product_barcodes = \App\ProductBarcode::where('product_id', $product->product_id)->get(); } } Update the addProductRow() method: if (! empty($product_id)) { $index = $request->input('row_count'); $products = $this->productUtil->getDetailsFromProduct($business_id, $product_id, $variation_id); // Load product barcodes for each product foreach ($products as $product) { $product->product_barcodes = \App\ProductBarcode::where('product_id', $product->product_id)->get(); } $price_groups = SellingPriceGroup::where('business_id', $business_id) ->active() ->pluck('name', 'id'); return view('labels.partials.show_table_rows') ->with(compact('products', 'index', 'price_groups')); } Update the preview() method to handle selected barcode: foreach ($products as $value) { $details = $this->productUtil->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->print_barcode = $selectedBarcode->barcode; $details->print_barcode_type = $selectedBarcode->barcode_type; } } // ... rest of the code ... } 10.2 Update Label ViewsFile: resources/views/labels/show.blade.php Add a new table header column: <th>@lang('lang_v1.selling_price_group')</th> <th>@lang('product.select_barcode_for_label')</th> File: resources/views/labels/partials/show_table_rows.blade.php Add barcode selector column after price group: <td> {!! Form::select('products[' . $row_index . '][price_group_id]', $price_groups, null, ['class' => 'form-control', 'placeholder' => __('lang_v1.none')]); !!} </td> <td> @php $displaySku = !empty($product->sub_sku) ? $product->sub_sku : (!empty($product->sku) ? $product->sku : ''); @endphp <select name="products[{{ $row_index }}][selected_barcode_id]" class="form-control"> <option value="">@lang('product.use_sku')@if($displaySku) ({{ $displaySku }})@endif</option> @if(isset($product->product_barcodes) && $product->product_barcodes->count() > 0) @foreach($product->product_barcodes as $barcode) <option value="{{ $barcode->id }}"> {{ $barcode->barcode }} @if($barcode->description) ({{ $barcode->description }}) @endif </option> @endforeach @endif </select> </td> File: resources/views/labels/partials/preview_2.blade.php Update the barcode generation section: {{-- Barcode --}} @php $printBarcode = $page_product->print_barcode ?? $page_product->sub_sku; $printBarcodeType = $page_product->print_barcode_type ?? $page_product->barcode_type; @endphp <img style="max-width:90% !important;height: {{$barcode_details->height*0.24}}in !important; display: block;" src="data:image/png;base64,{{DNS1D::getBarcodePNG($printBarcode, $printBarcodeType, 3,90, array(0, 0, 0), false)}}"> <span style="font-size: 10px !important"> {{$printBarcode}} </span> Step 11: Add Language TranslationsFile: lang/en/product.php Add these translations: 'additional_barcodes' => 'Additional Barcodes', 'add_barcode' => 'Add Barcode', 'barcode' => 'Barcode', 'barcode_description' => 'Description (optional)', 'use_sku' => 'Use SKU', 'select_barcode_for_label' => 'Barcode for Label', File: lang/en/tooltip.php Add this tooltip: 'additional_barcodes' => 'Add multiple barcodes for this product. These barcodes can be used to search and identify the product in addition to the main SKU.', SummaryAfter implementing all the steps above, you will have: Multiple barcodes per product - Each product can have unlimited additional barcodes Barcode type support - Each barcode can have its own type (C128, C39, EAN-13, etc.) Optional descriptions - Add notes like "Supplier barcode" or "Old SKU" Full search integration - Products are searchable by any of their barcodes in: POS product search Purchase product search Universal search Label printing selection - Choose which barcode to print on labels Files Modified SummaryFile Changes app/ProductBarcode.php New model app/Product.php Added barcodes() relationship app/Utils/ProductUtil.php Added syncProductBarcodes(), updated filterProduct() app/Http/Controllers/ProductController.php Updated store(), edit(), update() app/Http/Controllers/SellPosController.php Updated search query app/Http/Controllers/PurchaseController.php Updated search query app/Http/Controllers/UniversalSearchController.php Updated search query app/Http/Controllers/LabelsController.php Added barcode selection support resources/views/product/partials/product_barcodes.blade.php New partial view resources/views/product/create.blade.php Include barcode partial resources/views/product/edit.blade.php Include barcode partial resources/views/labels/show.blade.php Added column header resources/views/labels/partials/show_table_rows.blade.php Added barcode selector resources/views/labels/partials/preview_2.blade.php Use selected barcode public/js/product.js Added dynamic row JavaScript lang/en/product.php Added translations lang/en/tooltip.php Added tooltip
  2. Product History Button in POS ScreenAdd a Product History 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. Downloadproduct-history-pos.zip FeaturesSearch by SKU or Product Name (partial match supported) Search by Variation SKU (sub_sku) 5 History Tabs: Sales History - Invoice, date, customer, qty, price, subtotal, location Purchase History - Ref no, date, supplier, qty, price, subtotal, location Stock History - Date, transaction type, ref, location, qty in/out, balance Warranty Info - Invoice, date, customer, warranty details, status (Active/Expired) Expiry Info - Lot number, expiry date, location, stock, days to expiry, status Pagination - 8 records per page with navigation controls Bilingual Support - English and Arabic translations Permission-Based Access - Each tab respects user permissions PermissionsThe feature includes permission-based access control for each data section: Section Required Permission Base Access product.view Sales History sell.view or direct_sell.view Purchase History purchase.view Stock History product.view Warranty Info sell.view or direct_sell.view Expiry Info purchase.view 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. Files to Modify/CreateFile Action resources/views/layouts/partials/header-pos.blade.php Modify resources/views/sale_pos/partials/product_history_modal.blade.php Create resources/views/sale_pos/create.blade.php Modify resources/views/sale_pos/edit.blade.php Modify app/Http/Controllers/SellPosController.php Modify routes/web.php Modify public/js/pos.js Modify lang/en/lang_v1.php Modify lang/ar/lang_v1.php Modify Installation StepsStep 1: Add Button to POS HeaderEdit resources/views/layouts/partials/header-pos.blade.php Find the calculator button and add the product history button after it: <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"> <strong class="!tw-m-3"> <i class="fas fa-history fa-lg tw-text-[#9333EA] !tw-text-sm" aria-hidden="true"></i> <span class="tw-inline md:tw-hidden">{{ __('lang_v1.product_history') }}</span> </strong> </button> Step 2: Create Modal ViewCreate new file resources/views/sale_pos/partials/product_history_modal.blade.php Copy the content from: product_history_modal.blade.php Step 3: Include Modal in POS PagesEdit resources/views/sale_pos/create.blade.php Find @include('sale_pos.partials.weighing_scale_modal') and add after it: @include('sale_pos.partials.product_history_modal') Edit resources/views/sale_pos/edit.blade.php Add the same include after @include('sale_pos.partials.weighing_scale_modal'): @include('sale_pos.partials.product_history_modal') Step 4: Add Controller MethodEdit app/Http/Controllers/SellPosController.php Add the getProductHistory method before the closing brace of the class. Copy the content from: SellPosController_getProductHistory.php Step 5: Add RouteEdit routes/web.php Find the line: Route::get('/sells/pos/get-featured-products/{location_id}', [SellPosController::class, 'getFeaturedProducts']); Add after it: Route::get('/sells/pos/get-product-history', [SellPosController::class, 'getProductHistory']); Step 6: Add JavaScriptEdit public/js/pos.js Add the JavaScript code at the end of the file. Copy the content from: pos_product_history.js Step 7: Add TranslationsEdit lang/en/lang_v1.php and lang/ar/lang_v1.php Add the translations before the closing ]; Copy the content from: lang_v1_translations.php UsageGo to POS screen Click the purple history icon button in the header Enter product SKU or name in the search box Press Enter or click Search View history across all tabs (Sales, Purchases, Stock, Warranty, Expiry) Use pagination controls to navigate through records ScreenshotsButton LocationThe button appears in the POS header with a purple history icon. Modal ViewProduct info displayed at top (image, name, SKU, current stock) 5 tabs for different history types Pagination at bottom of each table CustomizationChange Records Per PageEdit SellPosController.php line with $per_page = 8; to change the number of records per page. Change Button ColorEdit the button class in header-pos.blade.php: Current: tw-text-[#9333EA] (purple) Change to any Tailwind color class TroubleshootingModal Not OpeningCheck if the modal include is added to both create.blade.php and edit.blade.php Clear browser cache and refresh No Data ShowingVerify the route is added correctly Check browser console for JavaScript errors Verify controller method is added Translations Not WorkingClear Laravel cache: php artisan cache:clear Check translation keys match exactly
  3. Lot/Expiry Stock Validation in POS Implementation GuideThis guide shows how to add dynamic lot/expiry stock validation 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. Downloadlot-stock-validation-pos.zip The ProblemIn Stock Transfer, when you select a lot for a product: The available stock updates dynamically to show that lot's quantity Validation prevents entering more than available in that lot Visual feedback shows "Current Stock: X" that updates on lot selection However, in POS this feature was missing - the stock display was static and didn't update when selecting different lots. The SolutionThis implementation adds the same lot stock validation behavior to POS that already exists in Stock Transfer: Dynamic stock display that updates when lot is selected Real-time validation against selected lot's available quantity Stock display updates when changing sub-units Visual feedback with "Current Stock: X" label Implementation Tree Structureyour-laravel-project/ ├── public/ │ └── js/ │ └── pos.js # 📝 MODIFY └── resources/ └── views/ └── sale_pos/ └── product_row.blade.php # 📝 MODIFY Files OverviewFile Action Description product_row.blade.php 📝 Modify Add dynamic stock display span with qty_available_text pos.js 📝 Modify Update lot change handler and sub_unit change handler Features✅ Dynamic stock display updates when selecting lots ✅ Real-time quantity validation per lot ✅ Sub-unit changes update stock display correctly ✅ Shows "Current Stock: X" label like Stock Transfer ✅ Error message when quantity exceeds lot stock ✅ Works with both POS and Direct Sell screens PrerequisitesUltimate POS with lot number/expiry date feature enabled Products with lot numbers configured Existing POS functionality working Implementation StepsStep 1: Update POS Product Row TemplateUpdate your resources/views/sale_pos/product_row.blade.php file to add the dynamic stock display. Find this section (around line 117): <small class="text-muted p-1"> @if($product->enable_stock) {{ @num_format($product->qty_available) }} {{$product->unit}} @lang('lang_v1.in_stock') @else -- @endif </small> Replace with: <small class="text-muted p-1" style="white-space: nowrap;"> @if($product->enable_stock) @lang('report.current_stock'): <span class="qty_available_text">{{ @num_format($product->qty_available) }}</span> {{$product->unit}} @else -- @endif </small> Key changes: Added qty_available_text span class for dynamic updates via JavaScript Changed label to use @lang('report.current_stock') for consistency with Stock Transfer Added white-space: nowrap to prevent text wrapping Step 2: Update Lot Number Change Handler in pos.jsUpdate the lot number change handler in public/js/pos.js to update the stock display. Find this section (around line 416): //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 Replace the entire lot_number change handler with: //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 > 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() && !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 > 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 > 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'); }); Key additions: Added qty_available_el variable to find the stock display span Added code to update stock display when lot is selected (lines with qty_available_el.text(...)) Added code to reset stock display when lot selection is cleared Step 3: Update Sub Unit Change Handler in pos.jsUpdate the sub_unit change handler to also update the stock display. Find the sub_unit change handler section (around line 1406): 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); Replace with: 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); Key addition: Added code to update stock display when sub_unit changes How It WorksProduct Added to POS: Shows total available stock for the product Lot Selected: Stock display updates to show only that lot's available quantity Quantity Validation: If user enters more than lot stock, validation error appears Lot Deselected: Stock display resets to total product stock Sub-unit Changed: Stock display updates with converted quantity Verification StepsAfter implementation, verify that: Add a product with lot/expiry to POS Check stock display shows "Current Stock: X" Select a lot from the dropdown Stock display updates to show that lot's quantity Enter quantity exceeding lot stock - validation error appears Clear lot selection - stock display resets to total Change sub-unit (if available) - stock display converts correctly Comparison with Stock TransferFeature Stock Transfer POS (Before) POS (After) Dynamic stock display ✅ ❌ ✅ Lot quantity validation ✅ ✅ ✅ Visual feedback on lot change ✅ ❌ ✅ Sub-unit stock conversion ✅ ❌ ✅ "Current Stock" label ✅ ❌ ✅ TroubleshootingIssue: Stock display not updatingSolution: Check that qty_available_text span class exists in the blade template Verify JavaScript changes are in the correct location Clear browser cache and reload Issue: Validation not workingSolution: Ensure allow_overselling is not enabled in POS settings Check that lot options have data-qty_available attribute Issue: Stock shows NaN or undefinedSolution: Verify lot numbers have proper qty_available data attributes Check that __currency_trans_from_en function exists in common.js Download Implementation FilesThe modified files are available for download: product_row.blade-f7a5a8e53d01beeb572cf7773ac3f304.php pos_sub_unit_handler-02dc0360e26b3578e92f3838dc122857.js pos_lot_number_handler-8f6bd0948b5887a4ad0975f0f1f2fe10.js ConclusionThis 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.
  4. Adding Multiple Suppliers to Products - Complete Implementation GuideOverviewThis guide will walk you through the complete process of adding multiple suppliers functionality to your Ultimate POS products. This feature allows you to: Assign multiple suppliers to products during creation and editing Filter products by supplier in the products list Bulk update suppliers for multiple products View all suppliers information in product details Use many-to-many relationship between products and suppliers Download Complete FilesDownload All Updated Files adding-supplier-to-products.zip Step 1: Database StructureMigration and Direct SQLUltimate POS already has a contacts table that stores suppliers. For multiple suppliers per product, we need to create a pivot table (many-to-many relationship). Create Migration: php artisan make:migration create_product_supplier_table Migration Content: <?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->id(); $table->unsignedInteger('product_id'); $table->unsignedInteger('supplier_id'); $table->timestamps(); // Foreign keys $table->foreign('product_id')->references('id')->on('products')->onDelete('cascade'); $table->foreign('supplier_id')->references('id')->on('contacts')->onDelete('cascade'); // Prevent duplicate entries $table->unique(['product_id', 'supplier_id']); // Indexes for better performance $table->index('product_id'); $table->index('supplier_id'); }); } public function down() { Schema::dropIfExists('product_supplier'); } }; Direct SQL (Alternative): -- 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; Run Migration: php artisan migrate Note: This creates a many-to-many relationship allowing each product to have multiple suppliers and each supplier to supply multiple products. Step 2: Model UpdatesUpdate Product ModelFile: app/Product.php Add relationship method for many-to-many relationship: /** * Get the suppliers for the product (many-to-many). */ public function suppliers() { return $this->belongsToMany( \App\Contact::class, 'product_supplier', 'product_id', 'supplier_id' )->withTimestamps(); } File: app/Contact.php Add relationship method for many-to-many relationship: /** * Get products supplied by this supplier (many-to-many) */ public function products() { return $this->belongsToMany( \App\Product::class, 'product_supplier', 'supplier_id', 'product_id' )->withTimestamps(); } /** * Get products supplied by this supplier with stock enabled */ public function stockProducts() { return $this->belongsToMany( \App\Product::class, 'product_supplier', 'supplier_id', 'product_id' )->where('products.enable_stock', 1) ->withTimestamps(); } Important Notes: The belongsToMany relationship requires the pivot table name (product_supplier) Use sync() method to attach/detach suppliers in controllers The withTimestamps() method ensures the pivot table timestamps are maintained Step 3: Controller UpdatesProductController ChangesFile: app/Http/Controllers/ProductController.php 3.1 Update index() methodAdd suppliers to eager loading (around line 77): $query = Product::with(['media', 'suppliers']) Add supplier filter to query (around line 162): $supplier_id = request()->get('supplier_id', null); if (!empty($supplier_id)) { $products->whereHas('suppliers', function ($query) use ($supplier_id) { $query->where('contacts.id', $supplier_id); }); } Add suppliers column to DataTables (after selling_price column): ->addColumn('suppliers', function ($row) { $suppliers = $row->suppliers->pluck('name')->toArray(); return implode(', ', $suppliers); }) Add suppliers dropdown for view (around line 334): $suppliers = \App\Contact::suppliersDropdown($business_id); Update compact statement to include suppliers: return view('product.index') ->with(compact( 'rack_enabled', 'categories', 'brands', 'units', 'taxes', 'business_locations', 'show_manufacturing_data', 'pos_module_data', 'is_woocommerce', 'is_admin', 'suppliers' )); 3.2 Update create() methodAdd suppliers dropdown (around line 150): $suppliers = \App\Contact::suppliersDropdown($business_id); Update compact statement: return view('product.create') ->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')); 3.3 Update store() methodAdd supplier sync after product creation (after product locations sync, around line 645): // Sync product suppliers $supplier_ids = $request->input('supplier_ids', []); if (!empty($supplier_ids)) { $product->suppliers()->sync($supplier_ids); } 3.4 Update edit() methodAdd suppliers dropdown (around line 350): $suppliers = \App\Contact::suppliersDropdown($business_id); Update compact statement: return view('product.edit') ->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')); 3.5 Update update() methodAdd supplier sync after product update (after product locations sync, around line 820): // Sync product suppliers $supplier_ids = $request->input('supplier_ids', []); $product->suppliers()->sync($supplier_ids); 3.6 Update view() methodAdd suppliers to with array (around line 920): $product = Product::where('business_id', $business_id) ->with(['brand', 'unit', 'category', 'sub_category', 'product_tax', 'variations', 'variations.product_variation', 'variations.group_prices', 'variations.media', 'product_locations', 'warranty', 'media', 'suppliers']) ->findOrFail($id); 3.7 Add bulk supplier update methodpublic function bulkUpdateSupplier(Request $request) { if (!auth()->user()->can('product.update')) { abort(403, 'Unauthorized action.'); } try { $selected_products = $request->input('selected_products'); $supplier_ids = $request->input('supplier_ids', []); $business_id = $request->session()->get('user.business_id'); if (empty($selected_products)) { $output = [ 'success' => 0, 'msg' => __('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) ->whereIn('id', $product_ids) ->get(); foreach ($products as $product) { $product->suppliers()->sync($supplier_ids); } DB::commit(); $output = [ 'success' => 1, 'msg' => __('lang_v1.supplier_updated_success') ]; } catch (\Exception $e) { DB::rollBack(); \Log::emergency("File:" . $e->getFile() . "Line:" . $e->getLine() . "Message:" . $e->getMessage()); $output = [ 'success' => 0, 'msg' => __('messages.something_went_wrong') ]; } return $output; } 3.8 Update other methodsquickAdd() method (around line 1500): Add suppliers dropdown: $suppliers = \App\Contact::suppliersDropdown($business_id, false); Update the compact statement to include suppliers: return view('product.partials.quick_add_product') ->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')); saveQuickProduct() method: The supplier_ids handling is done via sync relationship, NOT through form_fields. Add this code after syncing product_locations (around line 1610): //Add product suppliers $supplier_ids = $request->input('supplier_ids'); if (!empty($supplier_ids)) { $product->suppliers()->sync($supplier_ids); } bulkEdit() and bulkUpdate() methods: // Add supplier handling as shown in previous implementation guide Step 4: RoutesFile: routes/web.php Add the bulk supplier update route: Route::post('/products/bulk-update-supplier', [ProductController::class, 'bulkUpdateSupplier']) ->name('products.bulk-update-supplier'); Step 5: View Updates5.1 Product Index PageFile: resources/views/product/index.blade.php Add supplier filter (after brand filter around line 90): <div class="col-md-3"> <div class="form-group"> {!! Form::label('supplier_id', __('contact.supplier') . ':') !!} {!! Form::select('supplier_id', $suppliers ?? [], null, [ 'class' => 'form-control select2', 'style' => 'width:100%', 'id' => 'product_list_filter_supplier_id', 'placeholder' => __('lang_v1.all'), ]) !!} </div> </div> Update JavaScript DataTable configuration: Add to ajax data function: d.supplier_id = $('#product_list_filter_supplier_id').val(); Add to columns array (after brand column): { data: 'suppliers', name: 'suppliers', orderable: false, searchable: false }, Update change event listener: $(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 }); Add bulk supplier modal: <!-- Bulk Supplier Update Modal --> <div class="modal fade" id="bulk_supplier_modal" tabindex="-1" role="dialog"> <div class="modal-dialog" role="document"> <div class="modal-content"> <div class="modal-header"> <button type="button" class="close" data-dismiss="modal"> <span>&times;</span> </button> <h4 class="modal-title">@lang('lang_v1.bulk_update_supplier')</h4> </div> <form id="bulk_supplier_form" method="POST" action="{{ route('products.bulk-update-supplier') }}"> @csrf <div class="modal-body"> <div class="form-group"> <label for="bulk_supplier_id">@lang('contact.supplier'):</label> <select name="supplier_id" id="bulk_supplier_id" class="form-control select2" style="width: 100%;"> <option value="">@lang('lang_v1.none')</option> @if(isset($suppliers)) @foreach($suppliers as $id => $name) @if($id != '') <option value="{{ $id }}">{{ $name }}</option> @endif @endforeach @endif </select> </div> <input type="hidden" name="selected_products" id="bulk_selected_products"> </div> <div class="modal-footer"> <button type="button" class="btn btn-default" data-dismiss="modal">@lang('messages.close')</button> <button type="submit" class="btn btn-primary">@lang('messages.update')</button> </div> </form> </div> </div> </div> Add bulk supplier JavaScript: // Bulk supplier update functionality $(document).on('click', '.bulk-update-supplier', function(e) { e.preventDefault(); var selected_rows = getSelectedRows(); if (selected_rows.length > 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); } }); }); 5.2 Product TableFile: resources/views/product/partials/product_list.blade.php Add supplier column header (after brand): <th>@lang('lang_v1.suppliers')</th> Add bulk supplier button (in tfoot after existing buttons): &nbsp; <button type="button" class="tw-dw-btn tw-dw-btn-xs tw-dw-btn-outline tw-dw-btn-info bulk-update-supplier"> <i class="fa fa-users"></i> @lang('lang_v1.bulk_update_supplier') </button> 5.3 Product FormsCreate Form (resources/views/product/create.blade.php): Add after brand field (around line 87): <div class="col-sm-4"> <div class="form-group"> {!! Form::label('supplier_ids', __('lang_v1.suppliers') . ':') !!} <div class="input-group"> {!! Form::select('supplier_ids[]', $suppliers, !empty($duplicate_product) ? $duplicate_product->suppliers->pluck('id')->toArray() : null, ['class' => 'form-control select2', 'multiple', 'id' => 'supplier_ids']); !!} <span class="input-group-btn"> <button type="button" @if(!auth()->user()->can('supplier.create')) disabled @endif class="btn btn-default bg-white btn-flat quick_add_supplier_btn" title="@lang('contact.add_supplier')"><i class="fa fa-plus-circle text-primary fa-lg"></i></button> </span> </div> </div> </div> Edit Form (resources/views/product/edit.blade.php): Add after brand field (around line 98): <div class="col-sm-4"> <div class="form-group"> {!! Form::label('supplier_ids', __('lang_v1.suppliers') . ':') !!} <div class="input-group"> {!! Form::select('supplier_ids[]', $suppliers, $product->suppliers->pluck('id')->toArray(), ['class' => 'form-control select2', 'multiple', 'id' => 'supplier_ids']); !!} <span class="input-group-btn"> <button type="button" @if(!auth()->user()->can('supplier.create')) disabled @endif class="btn btn-default bg-white btn-flat quick_add_supplier_btn" title="@lang('contact.add_supplier')"><i class="fa fa-plus-circle text-primary fa-lg"></i></button> </span> </div> </div> </div> Note: The edit form pre-populates with the product's current suppliers using $product->suppliers->pluck('id')->toArray() 5.4 Quick Add Supplier Modal (Simplified Form)This section implements a simplified quick add supplier modal similar to the purchase page, with only essential fields: Supplier Name (or Business Name) and Mobile Number, plus Individual/Business toggle. File: resources/views/product/partials/quick_add_supplier_modal.blade.php (Create this new file) <!-- Quick Add Supplier Modal for Product Pages --> <div class="modal fade" id="quick_add_supplier_modal" tabindex="-1" role="dialog" aria-labelledby="quickAddSupplierModalLabel"> <div class="modal-dialog" role="document"> <div class="modal-content"> {!! Form::open(['url' => action([\App\Http\Controllers\ContactController::class, 'store']), 'method' => 'post', 'id' => 'quick_add_supplier_form']) !!} <div class="modal-header"> <button type="button" class="close" data-dismiss="modal" aria-label="Close"> <span aria-hidden="true">&times;</span> </button> <h4 class="modal-title" id="quickAddSupplierModalLabel">@lang('contact.add_supplier')</h4> </div> <div class="modal-body"> {!! Form::hidden('type', 'supplier') !!} <div class="row"> <!-- Individual / Business Toggle --> <div class="col-md-12" style="margin-bottom: 15px;"> <label class="radio-inline"> <input type="radio" name="supplier_type_radio" class="supplier_type_radio" value="individual" checked> @lang('lang_v1.individual') </label> &nbsp;&nbsp;&nbsp; <label class="radio-inline"> <input type="radio" name="supplier_type_radio" class="supplier_type_radio" value="business"> @lang('business.business') </label> </div> <div class="clearfix"></div> <!-- Individual Fields --> <div class="col-md-6 supplier_individual_fields"> <div class="form-group"> {!! Form::label('first_name', __('business.first_name') . ':*') !!} <div class="input-group"> <span class="input-group-addon"> <i class="fa fa-user"></i> </span> {!! Form::text('first_name', null, ['class' => 'form-control', 'placeholder' => __('business.first_name'), 'id' => 'supplier_first_name']); !!} </div> </div> </div> <div class="col-md-6 supplier_individual_fields"> <div class="form-group"> {!! Form::label('last_name', __('business.last_name') . ':') !!} <div class="input-group"> <span class="input-group-addon"> <i class="fa fa-user"></i> </span> {!! Form::text('last_name', null, ['class' => 'form-control', 'placeholder' => __('business.last_name'), 'id' => 'supplier_last_name']); !!} </div> </div> </div> <!-- Business Fields (Hidden by default) --> <div class="col-md-12 supplier_business_fields" style="display: none;"> <div class="form-group"> {!! Form::label('supplier_business_name', __('business.business_name') . ':*') !!} <div class="input-group"> <span class="input-group-addon"> <i class="fa fa-briefcase"></i> </span> {!! Form::text('supplier_business_name', null, ['class' => 'form-control', 'placeholder' => __('business.business_name'), 'id' => 'supplier_business_name_input']); !!} </div> </div> </div> <!-- Mobile Number (Always visible) --> <div class="col-md-6"> <div class="form-group"> {!! Form::label('mobile', __('contact.mobile') . ':*') !!} <div class="input-group"> <span class="input-group-addon"> <i class="fa fa-mobile"></i> </span> {!! Form::text('mobile', null, ['class' => 'form-control', 'required', 'placeholder' => __('contact.mobile'), 'id' => 'supplier_mobile']); !!} </div> </div> </div> <!-- Email (Optional) --> <div class="col-md-6"> <div class="form-group"> {!! Form::label('email', __('business.email') . ':') !!} <div class="input-group"> <span class="input-group-addon"> <i class="fa fa-envelope"></i> </span> {!! Form::email('email', null, ['class' => 'form-control', 'placeholder' => __('business.email'), 'id' => 'supplier_email']); !!} </div> </div> </div> </div> </div> <div class="modal-footer"> <button type="button" class="tw-dw-btn tw-dw-btn-neutral tw-text-white" data-dismiss="modal">@lang('messages.close')</button> <button type="submit" class="tw-dw-btn tw-dw-btn-primary tw-text-white">@lang('messages.save')</button> </div> {!! Form::close() !!} </div> </div> </div> Include the modal in product create/edit pages: Add at the bottom of resources/views/product/create.blade.php and resources/views/product/edit.blade.php: @include('product.partials.quick_add_supplier_modal') 5.5 JavaScript for Quick Add SupplierAdd the following JavaScript to handle the quick add supplier modal. You can add this to public/js/product.js or include it in the product pages. Add to public/js/product.js or inline in product views: // 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('<i class="fa fa-spinner fa-spin"></i> ' + 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 && 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(); }); 5.6 Controller Update for AJAX ResponseUpdate the ContactController@store method to return JSON response when called via AJAX: File: app/Http/Controllers/ContactController.php Find the store() method and update the success response to handle AJAX requests: // At the end of the store() method, before the redirect, add: if ($request->ajax()) { // Get the display name for the supplier $displayName = $contact->supplier_business_name ? $contact->supplier_business_name : trim($contact->prefix . ' ' . $contact->first_name . ' ' . $contact->middle_name . ' ' . $contact->last_name); return response()->json([ 'success' => true, 'msg' => __('contact.added_success'), 'data' => [ 'id' => $contact->id, 'name' => $displayName, 'mobile' => $contact->mobile, 'type' => $contact->type ] ]); } Full example of modified store() method ending: // ... existing store logic ... DB::commit(); $output = [ 'success' => true, 'data' => $contact, 'msg' => __('contact.added_success') ]; // Handle AJAX requests (for quick add modals) if ($request->ajax()) { $displayName = $contact->supplier_business_name ? $contact->supplier_business_name : trim($contact->prefix . ' ' . $contact->first_name . ' ' . $contact->middle_name . ' ' . $contact->last_name); return response()->json([ 'success' => true, 'msg' => __('contact.added_success'), 'data' => [ 'id' => $contact->id, 'name' => $displayName, 'mobile' => $contact->mobile, 'type' => $contact->type ] ]); } // Existing redirect logic for non-AJAX requests return redirect('contacts')->with('status', $output); 5.7 Product View ModalFile: resources/views/product/view-modal.blade.php Add supplier information after brand: <b>@lang('contact.supplier'): </b> @if($product->suppliers->isNotEmpty()) {{ $product->suppliers->pluck('name')->implode(', ') }} @else -- @endif <br> Note: Since a product can have multiple suppliers, we display them as a comma-separated list. 5.8 Quick Add Product FormFile: resources/views/product/partials/quick_add_product.blade.php Add after brand field (around line 57): <div class="col-sm-4"> <div class="form-group"> {!! Form::label('supplier_ids', __('contact.supplier') . ':') !!} <div class="input-group"> {!! Form::select('supplier_ids[]', $suppliers, null, ['class' => 'form-control select2', 'multiple', 'id' => 'quick_product_supplier_ids']); !!} <span class="input-group-btn"> <button type="button" @if(!auth()->user()->can('supplier.create')) disabled @endif class="btn btn-default bg-white btn-flat quick_add_supplier_btn" title="@lang('contact.add_supplier')"><i class="fa fa-plus-circle text-primary fa-lg"></i></button> </span> </div> </div> </div> Note: The quick add supplier modal will also work in this form since the JavaScript is event-delegated. Important Notes: Use supplier_ids[] (array) for multiple supplier selection Add 'multiple' => true to enable multi-select The quick add supplier button uses the same simplified modal The field name must match what saveQuickProduct() expects Step 6: Language FilesFile: resources/lang/en/lang_v1.php Add these translations: 'bulk_update_supplier' => 'Bulk Update Supplier', 'supplier_updated_success' => 'Supplier updated successfully', 'no_products_selected' => 'No products selected', Step 7: Testing7.1 Basic FunctionalityCreate Product: Test creating a product with multiple supplier selection Edit Product: Test editing existing products and changing/adding/removing suppliers View Product: Verify all suppliers appear in product details modal (comma-separated) Filter Products: Test filtering products by supplier in the index page 7.2 Bulk OperationsSelect Multiple Products: Use checkboxes to select multiple products Bulk Update Supplier: Click bulk supplier button and assign multiple suppliers Verify Changes: Check that all selected products have the new suppliers (replaces existing) 7.3 Edge CasesNo Supplier Selected: Test with empty/null supplier values (should work fine) Invalid Supplier: Test with non-existent supplier IDs Permission Testing: Test with different user permissions Large Datasets: Test with many products and many suppliers selected Duplicate Prevention: Try to assign the same supplier twice (should be prevented by unique constraint) Step 8: Optional Enhancements8.1 Supplier StatisticsAdd supplier-based reports showing: Products per supplier Stock levels by supplier Purchase history by supplier 8.2 Advanced FilteringAdd more complex filtering options: Products without suppliers Supplier-based stock alerts Multi-supplier selection 8.3 Import/ExportUpdate product import/export to include supplier information: CSV import with supplier names Excel export with supplier details ConclusionYou have successfully implemented multiple suppliers functionality for products in Ultimate POS. This feature provides: ✅ Multiple supplier assignment during product creation/editing ✅ Many-to-many relationship between products and suppliers ✅ Supplier filtering in product listings ✅ Bulk supplier updates for multiple products (with multiple supplier selection) ✅ All suppliers information displayed in product details ✅ Integration with existing supplier management ✅ Pivot table (product_supplier) for efficient relationship management ✅ Duplicate prevention via unique constraints ✅ Quick Add Supplier modal with simplified form (Individual/Business toggle, Name, Mobile) The implementation follows Ultimate POS conventions and maintains compatibility with existing features while supporting the flexibility of multiple suppliers per product. Key Differences from Single Supplier ImplementationUses belongsToMany instead of belongsTo/hasMany Requires pivot table (product_supplier) Form fields use supplier_ids[] instead of supplier_id Controllers use sync() method for relationship management Views display comma-separated supplier names Quick Add Supplier FeaturesThe simplified quick add supplier modal (similar to /purchases/create) provides: Individual/Business toggle - Switch between individual supplier (First Name, Last Name) and business supplier (Business Name) Minimum required fields - Only Supplier Name and Mobile Number are required Optional Email field - For additional contact information AJAX submission - Seamless addition without page reload Auto-select - Newly added supplier is automatically selected in the dropdown

Account

Navigation

Search

Search

Configure browser push notifications

Chrome (Android)
  1. Tap the lock icon next to the address bar.
  2. Tap Permissions → Notifications.
  3. Adjust your preference.
Chrome (Desktop)
  1. Click the padlock icon in the address bar.
  2. Select Site settings.
  3. Find Notifications and adjust your preference.