Skip to main content

Adding "Print Stock Need" to home dashboard

📁 Complete Implementation Tree

ultimate-pos/
├── app/
│ ├── Http/
│ │ └── Controllers/
│ │ └── HomeController.php # ✅ Update getProductStockAlert method
│ └── Utils/
│ └── ProductUtil.php # ✅ Update getProductAlert method
├── resources/
│ └── views/
│ └── home/
│ └── index.blade.php # ✅ Add print button + JavaScript
├── lang/
│ └── en/
│ └── lang_v1.php # ✅ Add new translations
├── public/
│ ├── js/
│ │ └── home.js # ⚪ Add location filter event (optional)
│ └── uploads/
│ └── business_logos/
│ └── company-logo.png # 🖼️ Company logo file
└──

📦 Download Starter Files

Download Implementation Template
Password: ultimatepos_psn_2025

This zip file contains the complete folder structure with placeholder files to help you get started quickly. Extract it to see the exact organization needed for implementation.

🔧 File Modifications Summary

FileChanges RequiredStatus
resources/views/home/index.blade.phpAdd print button + JavaScript✅ Required
app/Http/Controllers/HomeController.phpUpdate getProductStockAlert() method✅ Required
app/Utils/ProductUtil.phpUpdate getProductAlert() method✅ Required
lang/en/lang_v1.phpAdd new translations✅ Required
public/js/home.jsAdd location filter event⚪ Optional

Screenshots and Results

1. Ultimate POS Interface with Print Button

Ultimate POS Stock Alert Table with Print Button The Ultimate POS dashboard showing the Product Stock Alert section with the new "Print Stock Need" button. The table displays all 6 columns correctly: Product, Location, Current Stock, Alert Quantity, Expected Stock, and Stock Need.

2. Professional Print Preview

Professional Print Preview The generated print report showing professional formatting with:

  • Company branding (ERP)
  • Report title and generation timestamp
  • Summary statistics (Total Products: 1, Critical Items: 1, Total Current Stock: 8.00, etc.)
  • Formatted data table with color-coded Stock Need column
  • Footer with print details

3. Browser Print Dialog

Browser Print Dialog with PDF Option Browser print dialog showing "Save as PDF" option with the print preview visible in the background. Users can save the report as PDF or print directly to a physical printer.

Implementation Success

These screenshots demonstrate the successful implementation of the "Print Stock Need" feature:

Button Integration: The print button is properly integrated into the Ultimate POS interface
Data Display: All 6 columns are displaying correctly with proper formatting
Professional Output: The print preview shows a clean, professional report layout
Statistics Working: Summary statistics are calculating and displaying accurately
PDF Export: Users can save the report as PDF or print directly

Key Achievements:

  • Current Stock: 8.00 Pc(s) - displaying correctly
  • Alert Quantity: 11.00 Pc(s) - showing the reorder threshold
  • Stock Need: 3 Pc(s) - calculated correctly (11 - 8 = 3)
  • Stock Coverage: 72.7% - calculated as (8/11 * 100 = 72.7%)
  • Color Coding: Stock Need shows in red since it's > 0 (critical)

Overview

This guide will walk you through adding a "Print Stock Need" button to the home dashboard's stock alert section, allowing users to generate a printable report of products that need restocking.

The "Print Stock Need" feature allows users to generate a printable report showing:

  • Product names
  • Locations
  • Stock needed quantities
  • Formatted with professional styling

Prerequisites

  • Access to Ultimate POS codebase
  • Understanding of Laravel Blade templates
  • Basic JavaScript knowledge
  • Familiarity with Ultimate POS structure

Step-by-Step Implementation

Step 1: Locate the Target File

Navigate to the home index view file:

resources/views/home/index.blade.php

Step 2: Find the Stock Alert Section

Look for the @can('stock_report.view') section that contains the product stock alert card. The table structure should have these columns:

<table class="table table-bordered table-striped" id="stock_alert_table" style="width: 100%;">
<thead>
<tr>
<th style="min-width: 300px">@lang('sale.product')</th>
<th>@lang('business.location')</th>
<th>@lang('report.current_stock')</th>
<th>@lang('product.alert_quantity')</th>
<th>@lang('lang_v1.expected_stock')</th>
<th>@lang('lang_v1.stock_need')</th>
</tr>
</thead>
</table>

Step 3: Add the Print Button

Inside the header section, locate the div with classes tw-flex tw-items-center tw-flex-1 tw-min-w-0 tw-gap-1 and add the button:

<div class="tw-w-full sm:tw-w-1/2 md:tw-w-1/2 tw-flex tw-items-center tw-gap-2">
@if (count($all_locations) > 1)
{!! Form::select('stock_alert_location', $all_locations, null, [
'class' => 'form-control select2 tw-flex-grow',
'placeholder' => __('lang_v1.select_location'),
'id' => 'stock_alert_location',
]) !!}
@endif

<button id="print_alerte_de_stock" class="btn btn-primary tw-flex-none">
<i class="fa fa-print mr-1"></i>
@lang('messages.print') @lang('lang_v1.stock_need')
</button>
</div>

Step 4: Add Enhanced JavaScript Implementation

Add this JavaScript code at the bottom of your blade file, just before the closing @endcan:

<script>
$(document).ready(function(){
// Configuration object for easy customization
const printConfig = {
title: '{{ __("lang_v1.stock_need") }}',
columns: {
product: {
index: 0,
label: '{{ __("sale.product") }}',
enabled: true
},
location: {
index: 1,
label: '{{ __("business.location") }}',
enabled: true
},
currentStock: {
index: 2,
label: '{{ __("report.current_stock") }}',
enabled: true
},
alertQuantity: {
index: 3,
label: '{{ __("product.alert_quantity") }}',
enabled: true
},
expectedStock: {
index: 4,
label: '{{ __("lang_v1.expected_stock") }}',
enabled: true
},
stockNeed: {
index: 5,
label: '{{ __("lang_v1.stock_need") }}',
enabled: true
}
},
styles: {
pageMargin: '10mm',
fontSize: '12px',
headerFontSize: '16px',
tableFontSize: '11px'
}
};

$('#print_alerte_de_stock').click(function(){
// Validate table data
if (!validateTableData()) {
return;
}

// Show loading state
const originalText = $(this).html();
$(this).html('<i class="fa fa-spinner fa-spin mr-1"></i> {{ __("lang_v1.processing") }}...');
$(this).prop('disabled', true);

// Generate print window
setTimeout(() => {
generatePrintWindow();

// Reset button state
$(this).html(originalText);
$(this).prop('disabled', false);
}, 500);
});

function validateTableData() {
const tableBody = $('#stock_alert_table tbody');

if (tableBody.length === 0) {
alert('{{ __("lang_v1.table_not_found") }}');
return false;
}

const rows = tableBody.find('tr');
if (rows.length === 0 || rows.find('td:contains("{{ __("lang_v1.no_data_available") }}")').length > 0) {
alert('{{ __("lang_v1.no_data_available_print") }}');
return false;
}

return true;
}

function generatePrintWindow() {
const printWindow = window.open('', '_blank', 'width=800,height=600');

if (!printWindow) {
alert('{{ __("lang_v1.popup_blocked") }}');
return;
}

const printContent = generatePrintContent();
printWindow.document.write(printContent);
printWindow.document.close();

printWindow.onload = function() {
printWindow.focus();
printWindow.print();
};
}

function generatePrintContent() {
const currentDate = moment().format('LLLL');
const businessName = '{{ session("business.name") }}';
const selectedLocation = getSelectedLocation();

return `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>${printConfig.title}</title>
${generatePrintStyles()}
</head>
<body>
<div class="print-container">
${generatePrintHeader(businessName, selectedLocation)}
${generatePrintTable()}
${generatePrintFooter(currentDate)}
</div>
</body>
</html>
`;
}

function generatePrintStyles() {
return `
<style>
@media print {
@page {
margin: ${printConfig.styles.pageMargin};
size: A4;
}
body {
-webkit-print-color-adjust: exact !important;
color-adjust: exact !important;
}
}

body {
margin: 0;
padding: 0;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
font-size: ${printConfig.styles.fontSize};
line-height: 1.4;
color: #333;
}

.print-container {
max-width: 100%;
margin: 0 auto;
padding: 20px;
}

.print-header {
text-align: center;
margin-bottom: 30px;
border-bottom: 2px solid #2563eb;
padding-bottom: 15px;
}

.business-name {
font-size: 20px;
font-weight: bold;
color: #1e40af;
margin-bottom: 5px;
}

.report-title {
font-size: ${printConfig.styles.headerFontSize};
font-weight: bold;
color: #374151;
margin: 10px 0 5px 0;
}

.report-subtitle {
font-size: 12px;
color: #6b7280;
margin-bottom: 15px;
}

.report-filters {
font-size: 11px;
color: #4b5563;
margin-top: 10px;
}

table {
width: 100%;
border-collapse: collapse;
margin: 20px 0;
background: white;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}

th {
background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%);
color: white;
font-weight: 600;
padding: 12px 8px;
text-align: left;
font-size: ${printConfig.styles.tableFontSize};
border: 1px solid #1e40af;
text-transform: uppercase;
letter-spacing: 0.5px;
}

td {
padding: 10px 8px;
border: 1px solid #e5e7eb;
font-size: ${printConfig.styles.tableFontSize};
vertical-align: top;
}

tbody tr:nth-child(even) {
background-color: #f8fafc;
}

tbody tr:hover {
background-color: #e0f2fe;
}

.stock-need-highlight {
font-weight: bold;
color: #dc2626;
}

.print-footer {
margin-top: 30px;
padding-top: 15px;
border-top: 1px solid #e5e7eb;
text-align: center;
font-size: 10px;
color: #6b7280;
}

.summary-stats {
margin: 20px 0;
padding: 15px;
background: #f1f5f9;
border-radius: 8px;
border: 1px solid #cbd5e1;
}

.stat-item {
display: inline-block;
margin-right: 30px;
font-size: 11px;
}

.stat-label {
font-weight: bold;
color: #475569;
}

.stat-value {
color: #1e40af;
font-weight: bold;
}
</style>
`;
}

function generatePrintHeader(businessName, selectedLocation) {
const currentDate = moment().format('MMMM DD, YYYY');
const currentTime = moment().format('h:mm A');

// Company Logo Code - Add this section
const logoUrl = '{{ asset("uploads/business_logos/" . session("business.logo")) }}';
const logoHtml = logoUrl && '{{ session("business.logo") }}' ?
`<img src="${logoUrl}" alt="Company Logo" style="max-height: 60px; margin-bottom: 10px;">` : '';

return `
<div class="print-header">
${logoHtml}
<div class="business-name">${businessName}</div>
<div class="report-title">${printConfig.title}</div>
<div class="report-subtitle">{{ __('lang_v1.generated_on') }}: ${currentDate} {{ __('lang_v1.at') }} ${currentTime}</div>
${selectedLocation ? `<div class="report-filters">{{ __('business.location') }}: ${selectedLocation}</div>` : ''}
</div>
`;
}

function generatePrintTable() {
let tableHtml = '<table><thead><tr>';

// Generate table headers for enabled columns
Object.keys(printConfig.columns).forEach(key => {
const column = printConfig.columns[key];
if (column.enabled) {
tableHtml += `<th>${column.label}</th>`;
}
});

tableHtml += '</tr></thead><tbody>';

// Generate table rows
let totalProducts = 0;
let totalStockNeed = 0;
let totalCurrentStock = 0;
let totalAlertQuantity = 0;
let criticalItems = 0;

$('#stock_alert_table tbody tr').each(function() {
const row = $(this);
if (row.find('td').length > 0) {
tableHtml += '<tr>';

Object.keys(printConfig.columns).forEach(key => {
const column = printConfig.columns[key];
if (column.enabled) {
let cellContent = row.find('td').eq(column.index).html() || '';

// Extract numeric values for calculations
let numericValue = 0;
if (cellContent.includes('<span')) {
const match = cellContent.match(/>([\d.,-]+)</);
if (match) {
numericValue = parseFloat(match[1].replace(/[^\d.-]/g, '')) || 0;
}
} else {
numericValue = parseFloat(cellContent.replace(/[^\d.-]/g, '')) || 0;
}

// Special formatting and calculations based on column
if (key === 'stockNeed') {
totalStockNeed += numericValue;
if (numericValue > 0) criticalItems++;
cellContent = `<span class="stock-need-highlight">${cellContent}</span>`;
} else if (key === 'currentStock') {
totalCurrentStock += numericValue;
} else if (key === 'alertQuantity') {
totalAlertQuantity += numericValue;
}

tableHtml += `<td>${cellContent}</td>`;
}
});

tableHtml += '</tr>';
totalProducts++;
}
});

tableHtml += '</tbody></table>';

// Add enhanced summary statistics
const summaryHtml = `
<div class="summary-stats">
<div class="stat-item">
<span class="stat-label">{{ __('lang_v1.total_products') }}:</span>
<span class="stat-value">${totalProducts}</span>
</div>
<div class="stat-item">
<span class="stat-label">{{ __('lang_v1.critical_items') }}:</span>
<span class="stat-value">${criticalItems}</span>
</div>
<div class="stat-item">
<span class="stat-label">{{ __('lang_v1.total_current_stock') }}:</span>
<span class="stat-value">${totalCurrentStock.toFixed(2)}</span>
</div>
<div class="stat-item">
<span class="stat-label">{{ __('lang_v1.total_stock_needed') }}:</span>
<span class="stat-value">${totalStockNeed.toFixed(2)}</span>
</div>
<div class="stat-item">
<span class="stat-label">{{ __('lang_v1.coverage_ratio') }}:</span>
<span class="stat-value">${totalAlertQuantity > 0 ? ((totalCurrentStock / totalAlertQuantity) * 100).toFixed(1) + '%' : 'N/A'}</span>
</div>
</div>
`;

return summaryHtml + tableHtml;
}

function generatePrintFooter(currentDate) {
return `
<div class="print-footer">
<p>{{ __('lang_v1.printed_on') }}: ${currentDate}</p>
<p>{{ __('lang_v1.generated_by') }}: {{ auth()->user()->first_name }} {{ auth()->user()->last_name }}</p>
<p>{{ __('lang_v1.system_generated_report') }}</p>
</div>
`;
}

function getSelectedLocation() {
const locationSelect = $('#stock_alert_location');
if (locationSelect.length > 0 && locationSelect.val()) {
return locationSelect.find('option:selected').text();
}
return null;
}
});
</script>

Step 5: Update Controller Method

Update your getProductStockAlert() method in app/Http/Controllers/HomeController.php:

public function getProductStockAlert()
{
if (request()->ajax()) {
$business_id = request()->session()->get('user.business_id');
$permitted_locations = auth()->user()->permitted_locations();

$products = $this->productUtil->getProductAlert($business_id, $permitted_locations);

return Datatables::of($products)
->editColumn('product', function ($row) {
if ($row->type == 'single') {
return $row->product.' ('.$row->sku.')';
} else {
return $row->product.' - '.$row->product_variation.' - '.$row->variation.' ('.$row->sub_sku.')';
}
})
->editColumn('location', function ($row) {
return $row->location ?? __('lang_v1.all_locations');
})
->editColumn('stock', function ($row) {
$stock = $row->stock ? $row->stock : 0;
return '<span data-is_quantity="true" class="display_currency" data-currency_symbol=false>'.(float) $stock.'</span> '.$row->unit;
})
->editColumn('alert_quantity', function ($row) {
$alert_qty = $row->alert_quantity ?? 0;
return '<span data-is_quantity="true" class="display_currency" data-currency_symbol=false>'.(float) $alert_qty.'</span> '.$row->unit;
})
->addColumn('expected_stock', function ($row) {
$current_stock = $row->stock ? $row->stock : 0;
return '<span data-is_quantity="true" class="display_currency" data-currency_symbol=false>'.(float) $current_stock.'</span> '.$row->unit;
})
->addColumn('stock_need', function ($row) {
$current_stock = $row->stock ? $row->stock : 0;
$alert_quantity = $row->alert_quantity ?? 0;
$stock_need = max(0, $alert_quantity - $current_stock);

$class = $stock_need > 0 ? 'text-danger font-weight-bold' : 'text-success';
return '<span class="'.$class.'" data-is_quantity="true" data-currency_symbol=false>'.(float) $stock_need.'</span> '.$row->unit;
})
->removeColumn('product_id')
->removeColumn('type')
->removeColumn('sku')
->removeColumn('product_variation')
->removeColumn('variation')
->removeColumn('sub_sku')
->removeColumn('unit')
->rawColumns([2, 3, 4, 5])
->make(false);
}
}

Step 6: Update ProductUtil Method

Update your getProductAlert() method in app/Utils/ProductUtil.php:

public function getProductAlert($business_id, $permitted_locations = null)
{
$query = VariationLocationDetails::join(
'product_variations as pv',
'variation_location_details.product_variation_id',
'=',
'pv.id'
)
->join(
'variations as v',
'variation_location_details.variation_id',
'=',
'v.id'
)
->join(
'products as p',
'variation_location_details.product_id',
'=',
'p.id'
)
->leftjoin(
'business_locations as l',
'variation_location_details.location_id',
'=',
'l.id'
)
->leftjoin('units as u', 'p.unit_id', '=', 'u.id')
->where('p.business_id', $business_id)
->where('p.enable_stock', 1)
->where('p.is_inactive', 0)
->whereNull('v.deleted_at')
->whereNotNull('p.alert_quantity')
->whereRaw('variation_location_details.qty_available <= p.alert_quantity');

//Check for permitted locations of a user
if (!empty($permitted_locations)) {
if ($permitted_locations != 'all') {
$query->whereIn('variation_location_details.location_id', $permitted_locations);
}
}

if (! empty(request()->input('location_id'))) {
$query->where('variation_location_details.location_id', request()->input('location_id'));
}

$products = $query->select(
'p.name as product',
'l.name as location',
'variation_location_details.qty_available as stock',
'p.alert_quantity',
'p.type',
'p.sku',
'pv.name as product_variation',
'v.name as variation',
'v.sub_sku',
'u.short_name as unit'
)
->groupBy('variation_location_details.id')
->orderBy('stock', 'asc');

return $products;
}

Column Details

The stock alert table displays the following information:

ColumnDescriptionPurpose
ProductProduct name with variationsIdentify which items need restocking
LocationBusiness location/warehouseTrack stock needs by location
Current StockAvailable quantity on handCurrent inventory level
Alert QuantityMinimum stock thresholdWhen to reorder (warning level)
Expected StockProjected stock after pending ordersFuture inventory prediction
Stock NeedRequired quantity to reach optimal levelHow much to order

Stock Calculation Logic

The system calculates stock needs based on:

  • Current Stock: Actual available inventory
  • Alert Quantity: Minimum threshold before reordering
  • Expected Stock: Current + incoming orders - pending sales
  • Stock Need: (Alert Quantity - Expected Stock) when negative indicates shortage

Advanced Customization Options

1. Customizing Columns

To modify which columns appear in the print report, edit the printConfig.columns object:

columns: {
product: { enabled: true },
location: { enabled: true },
currentStock: { enabled: true },
alertQuantity: { enabled: true },
expectedStock: { enabled: false }, // Hide this column
stockNeed: { enabled: true }
}

2. Styling Customization

Modify the printConfig.styles object for different styling:

styles: {
pageMargin: '15mm',
fontSize: '14px',
headerFontSize: '18px',
tableFontSize: '12px'
}

The logo code is already included in the generatePrintHeader() function. It uses Ultimate POS's standard logo storage location at public/uploads/business_logos/.

Language Translations

Add these translations to lang/en/lang_v1.php:

'stock_need' => 'Stock Need',
'print_selected_columns' => 'Print Selected Columns',
'printed_on' => 'Printed on',
'processing' => 'Processing',
'table_not_found' => 'Table not found',
'no_data_available_print' => 'No data available to print',
'popup_blocked' => 'Popup blocked. Please allow popups for this site.',
'generated_on' => 'Generated on',
'at' => 'at',
'total_products' => 'Total Products',
'total_stock_needed' => 'Total Stock Needed',
'critical_items' => 'Critical Items',
'total_current_stock' => 'Total Current Stock',
'coverage_ratio' => 'Stock Coverage',
'generated_by' => 'Generated by',
'system_generated_report' => 'This is a system generated report',
'export' => 'Export',
'export_pdf' => 'Export as PDF',
'export_excel' => 'Export as Excel',
'export_csv' => 'Export as CSV',
'all_categories' => 'All Categories',
'all_levels' => 'All Stock Levels',
'critical' => 'Critical',
'low' => 'Low',
'needed' => 'Needed',
'expected_stock' => 'Expected Stock',

Testing Checklist

  • Button appears for users with correct permissions
  • Print functionality works in all supported browsers
  • Styling renders correctly in print preview
  • All translations are working
  • All 6 columns display correctly
  • Stock Need calculations are accurate
  • Currency formatting works properly

This implementation provides a robust, customizable print solution that integrates seamlessly with Ultimate POS's existing architecture and styling conventions.

💬 Discussion & Questions

Please sign in to join the discussion.

Loading comments...

💛 Support this project

Binance ID:

478036326
Premium Login