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