Skip to main content

Lot/Expiry Stock Validation in POS Implementation Guide

This 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.

POS Create page

Download​

The Problem​

In 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 Solution​

This 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 Structure​

your-laravel-project/
├── public/
│ └── js/
│ └── pos.js # 📝 MODIFY
└── resources/
└── views/
└── sale_pos/
└── product_row.blade.php # 📝 MODIFY

Files Overview​

FileActionDescription
product_row.blade.php📝 ModifyAdd dynamic stock display span with qty_available_text
pos.js📝 ModifyUpdate 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

Prerequisites​

  • Ultimate POS with lot number/expiry date feature enabled
  • Products with lot numbers configured
  • Existing POS functionality working

Implementation Steps​

Step 1: Update POS Product Row Template​

Update 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.js​

Update 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.js​

Update 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 Works​

  1. Product Added to POS: Shows total available stock for the product
  2. Lot Selected: Stock display updates to show only that lot's available quantity
  3. Quantity Validation: If user enters more than lot stock, validation error appears
  4. Lot Deselected: Stock display resets to total product stock
  5. Sub-unit Changed: Stock display updates with converted quantity

Verification Steps​

After implementation, verify that:

  1. Add a product with lot/expiry to POS
  2. Check stock display shows "Current Stock: X"
  3. Select a lot from the dropdown
  4. Stock display updates to show that lot's quantity
  5. Enter quantity exceeding lot stock - validation error appears
  6. Clear lot selection - stock display resets to total
  7. Change sub-unit (if available) - stock display converts correctly

Comparison with Stock Transfer​

FeatureStock TransferPOS (Before)POS (After)
Dynamic stock display✅❌✅
Lot quantity validation✅✅✅
Visual feedback on lot change✅❌✅
Sub-unit stock conversion✅❌✅
"Current Stock" label✅❌✅

Troubleshooting​

Issue: Stock display not updating​

Solution:

  • 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 working​

Solution:

  • Ensure allow_overselling is not enabled in POS settings
  • Check that lot options have data-qty_available attribute

Issue: Stock shows NaN or undefined​

Solution:

  • Verify lot numbers have proper qty_available data attributes
  • Check that __currency_trans_from_en function exists in common.js

Download Implementation Files​

The modified files are available for download:

Conclusion​

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.

💬 Discussion & Questions

Please sign in to join the discussion.

Loading comments...

💛 Support this project

Premium Login