Add barcode modal with configurable barcode type setting

- Add barcode_type column to tenants table (default: code128)
- Add barcode type setting in tenant settings page (Code 128, EAN-13, QR Code)
- Replace barcode page link with modal button on tenant dashboard
- Add barcode modal that displays product barcode using tenant's configured type
- Remove old generate_barcode_page route, function, and template
- Clean up unused imports in admin.py
This commit is contained in:
2025-10-20 17:49:18 -04:00
parent 7bb8c0d969
commit bb01ca2110
6 changed files with 152 additions and 255 deletions

View File

@@ -112,6 +112,25 @@ def update_tenant_credentials(tenant_id):
flash('Credentials updated successfully.', 'success') flash('Credentials updated successfully.', 'success')
return redirect(f'/{tenant_id}/settings') return redirect(f'/{tenant_id}/settings')
@app.route('/<tenant:tenant_id>/settings/barcode', methods=['POST'])
def update_tenant_barcode_type(tenant_id):
# Get tenant
tenant = database.get_tenant(tenant_id)
if tenant is None:
return jsonify({"error": "Tenant not found"}), 404
barcode_type = request.form.get('barcode_type', '').strip()
if not barcode_type:
flash('Barcode type is required.', 'error')
return redirect(f'/{tenant_id}/settings')
# Update barcode type
database.update_tenant_barcode_type(tenant_id, barcode_type)
flash('Barcode type updated successfully.', 'success')
return redirect(f'/{tenant_id}/settings')
def check_basic_auth(auth_header, tenant_id): def check_basic_auth(auth_header, tenant_id):
"""Validate Basic authentication credentials for a tenant""" """Validate Basic authentication credentials for a tenant"""
if not auth_header or not auth_header.startswith('Basic '): if not auth_header or not auth_header.startswith('Basic '):
@@ -381,14 +400,13 @@ def serve_barcode(tenant_id, filename):
# Import product management routes # Import product management routes
from routes.admin import (add_product, edit_product, delete_product, from routes.admin import (add_product, edit_product, delete_product,
generate_barcode, generate_barcode_page, manage_ar_fields) generate_barcode, manage_ar_fields)
# Register product management routes (remove /admin/ from paths) # Register product management routes (remove /admin/ from paths)
app.add_url_rule('/<tenant:tenant_id>/add', 'admin.add_product', add_product, methods=['GET', 'POST']) app.add_url_rule('/<tenant:tenant_id>/add', 'admin.add_product', add_product, methods=['GET', 'POST'])
app.add_url_rule('/<tenant:tenant_id>/edit/<product_id>', 'admin.edit_product', edit_product, methods=['GET', 'POST']) app.add_url_rule('/<tenant:tenant_id>/edit/<product_id>', 'admin.edit_product', edit_product, methods=['GET', 'POST'])
app.add_url_rule('/<tenant:tenant_id>/delete/<product_id>', 'admin.delete_product', delete_product, methods=['POST']) app.add_url_rule('/<tenant:tenant_id>/delete/<product_id>', 'admin.delete_product', delete_product, methods=['POST'])
app.add_url_rule('/<tenant:tenant_id>/generate_barcode/<product_id>/<code_type>', 'admin.generate_barcode', generate_barcode) app.add_url_rule('/<tenant:tenant_id>/generate_barcode/<product_id>/<code_type>', 'admin.generate_barcode', generate_barcode)
app.add_url_rule('/<tenant:tenant_id>/generate_barcode_page/<product_id>', 'admin.generate_barcode_page', generate_barcode_page)
app.add_url_rule('/<tenant:tenant_id>/ar_fields', 'admin.manage_ar_fields', manage_ar_fields, methods=['GET', 'POST']) app.add_url_rule('/<tenant:tenant_id>/ar_fields', 'admin.manage_ar_fields', manage_ar_fields, methods=['GET', 'POST'])
# Register API routes with tenant prefix # Register API routes with tenant prefix

View File

@@ -117,6 +117,14 @@ def init_database():
) )
''') ''')
# Add barcode_type column to tenants table if it doesn't exist
try:
cursor.execute('ALTER TABLE tenants ADD COLUMN barcode_type TEXT DEFAULT "code128"')
conn.commit()
except sqlite3.OperationalError:
# Column already exists
pass
conn.commit() conn.commit()
def migrate_from_json(): def migrate_from_json():
@@ -377,7 +385,7 @@ def get_tenant(tenant_id: str) -> Optional[Dict[str, Any]]:
cursor = conn.cursor() cursor = conn.cursor()
# Check if tenant exists # Check if tenant exists
cursor.execute('SELECT id, name, username, password FROM tenants WHERE id = ?', (tenant_id,)) cursor.execute('SELECT id, name, username, password, created_at, barcode_type FROM tenants WHERE id = ?', (tenant_id,))
row = cursor.fetchone() row = cursor.fetchone()
if row: if row:
@@ -385,7 +393,9 @@ def get_tenant(tenant_id: str) -> Optional[Dict[str, Any]]:
'id': row['id'], 'id': row['id'],
'name': row['name'], 'name': row['name'],
'username': row['username'], 'username': row['username'],
'password': row['password'] 'password': row['password'],
'created_at': row['created_at'],
'barcode_type': row['barcode_type'] or 'code128'
} }
return None return None
@@ -402,7 +412,7 @@ def get_or_create_tenant(tenant_id: str, username: str = None, password: str = N
cursor = conn.cursor() cursor = conn.cursor()
# Check if tenant exists # Check if tenant exists
cursor.execute('SELECT id, name, username, password FROM tenants WHERE id = ?', (tenant_id,)) cursor.execute('SELECT id, name, username, password, created_at, barcode_type FROM tenants WHERE id = ?', (tenant_id,))
row = cursor.fetchone() row = cursor.fetchone()
if row: if row:
@@ -410,7 +420,9 @@ def get_or_create_tenant(tenant_id: str, username: str = None, password: str = N
'id': row['id'], 'id': row['id'],
'name': row['name'], 'name': row['name'],
'username': row['username'], 'username': row['username'],
'password': row['password'] 'password': row['password'],
'created_at': row['created_at'],
'barcode_type': row['barcode_type'] or 'code128'
} }
else: else:
# Create new tenant with default credentials # Create new tenant with default credentials
@@ -427,11 +439,17 @@ def get_or_create_tenant(tenant_id: str, username: str = None, password: str = N
# Initialize default AR fields for the new tenant # Initialize default AR fields for the new tenant
init_default_ar_fields(tenant_id) init_default_ar_fields(tenant_id)
# Get the created_at timestamp and barcode_type from the database
cursor.execute('SELECT created_at, barcode_type FROM tenants WHERE id = ?', (tenant_id,))
created_row = cursor.fetchone()
return { return {
'id': tenant_id, 'id': tenant_id,
'name': display_name, 'name': display_name,
'username': default_username, 'username': default_username,
'password': default_password 'password': default_password,
'created_at': created_row['created_at'] if created_row else None,
'barcode_type': created_row['barcode_type'] if created_row else 'code128'
} }
def update_tenant_credentials(tenant_id: str, username: str, password: str): def update_tenant_credentials(tenant_id: str, username: str, password: str):
@@ -448,6 +466,20 @@ def update_tenant_credentials(tenant_id: str, username: str, password: str):
''', (username, password, tenant_id)) ''', (username, password, tenant_id))
conn.commit() conn.commit()
def update_tenant_barcode_type(tenant_id: str, barcode_type: str):
"""Update tenant barcode type"""
# Normalize tenant_id to lowercase
tenant_id = tenant_id.lower()
with get_db() as conn:
cursor = conn.cursor()
cursor.execute('''
UPDATE tenants
SET barcode_type = ?, updated_at = CURRENT_TIMESTAMP
WHERE id = ?
''', (barcode_type, tenant_id))
conn.commit()
def get_all_tenants() -> List[Dict[str, Any]]: def get_all_tenants() -> List[Dict[str, Any]]:
"""Get all tenants""" """Get all tenants"""
with get_db() as conn: with get_db() as conn:

View File

@@ -1,24 +1,9 @@
from flask import render_template, request, redirect, url_for, flash, jsonify, send_file from flask import render_template, request, redirect, url_for, flash, jsonify
import os import os
import json
import uuid
import io
from werkzeug.utils import secure_filename
import sys import sys
sys.path.append(os.path.dirname(os.path.dirname(__file__))) sys.path.append(os.path.dirname(os.path.dirname(__file__)))
import database import database
# Import barcode generator class, but handle the case if it fails
try:
from utils.barcode_generator import BarcodeGenerator
BARCODE_GENERATOR_AVAILABLE = True
except ImportError:
BARCODE_GENERATOR_AVAILABLE = False
class BarcodeGenerator:
@staticmethod
def check_dependencies():
return {'qrcode': False, 'barcode': False, 'pillow': False}
DATA_FOLDER = os.path.join(os.path.dirname(__file__), '../data') DATA_FOLDER = os.path.join(os.path.dirname(__file__), '../data')
PRODUCTS_FILE = os.path.join(DATA_FOLDER, 'products.json') PRODUCTS_FILE = os.path.join(DATA_FOLDER, 'products.json')
UPLOAD_FOLDER = os.path.join(DATA_FOLDER, 'images') UPLOAD_FOLDER = os.path.join(DATA_FOLDER, 'images')
@@ -344,40 +329,6 @@ def generate_barcode(tenant_id, product_id, code_type):
# Redirect to the main barcode endpoint which generates dynamically # Redirect to the main barcode endpoint which generates dynamically
return redirect(f'/{tenant_id}/barcodes/{product_id}_{barcode_type}.png') return redirect(f'/{tenant_id}/barcodes/{product_id}_{barcode_type}.png')
def generate_barcode_page(tenant_id, product_id):
"""Show a page with different barcode options for a product"""
products = load_products(tenant_id)
if product_id not in products:
flash('Product not found.')
return redirect(f'/{tenant_id}/')
# Extract product data
product_data = {}
for field in products[product_id]:
field_name = field["fieldName"][1:] if field["fieldName"].startswith('_') else field["fieldName"]
product_data[field_name] = field["value"]
# Add product ID
product_data['id'] = product_id
# Check if barcode generation is available
dependency_status = {}
if BARCODE_GENERATOR_AVAILABLE:
dependency_status = BarcodeGenerator.check_dependencies()
else:
dependency_status = {
'qrcode': False,
'barcode': False,
'pillow': False
}
return render_template('admin/generate_barcode.html',
product_id=product_id,
product=product_data,
dependencies=dependency_status,
tenant_id=tenant_id)
def manage_credentials(tenant_id): def manage_credentials(tenant_id):
"""Manage tenant login credentials""" """Manage tenant login credentials"""
tenant = database.get_or_create_tenant(tenant_id) tenant = database.get_or_create_tenant(tenant_id)

View File

@@ -1,150 +0,0 @@
{% extends "layout.html" %}
{% block title %}Generate Barcodes - KCAP Demo Server{% endblock %}
{% block content %}
<div class="row mt-4">
<div class="col-md-12">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h2>Generate Barcodes for Product: {{ product_id }}</h2>
<a href="/{{ tenant_id }}/" class="btn btn-outline-secondary">Back to Products</a>
</div>
<div class="card-body">
{% if not dependencies.qrcode or not dependencies.barcode or not dependencies.pillow %}
<div class="alert alert-warning mb-4">
<h4 class="alert-heading">Missing Dependencies</h4>
<p>Some barcode generation features are unavailable because required packages are not installed:</p>
<ul>
{% if not dependencies.qrcode %}
<li><strong>QR Code:</strong> The 'qrcode' package is required for QR code generation</li>
{% endif %}
{% if not dependencies.barcode %}
<li><strong>Barcodes:</strong> The 'python-barcode' package is required for EAN-13 and Code 128 generation</li>
{% endif %}
{% if not dependencies.pillow %}
<li><strong>Image Processing:</strong> The 'Pillow' package is required for image processing</li>
{% endif %}
</ul>
<hr>
<p class="mb-0">To install the required dependencies, run: <code>pip install -r requirements.txt</code></p>
</div>
{% endif %}
<div class="row">
<div class="col-md-4">
<div class="card">
<div class="card-header">
<h3>QR Code</h3>
</div>
<div class="card-body text-center">
<img src="{{ url_for('admin.generate_barcode', tenant_id=tenant_id, product_id=product_id, code_type='qrcode') }}" alt="QR Code" class="img-fluid mb-3" style="max-width: 250px;">
<p class="text-muted">QR Code contains a link to the product AR info.</p>
<a href="{{ url_for('admin.generate_barcode', tenant_id=tenant_id, product_id=product_id, code_type='qrcode') }}" class="btn btn-primary" download="product_{{ product_id }}_qrcode.png">Download QR Code</a>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header">
<h3>EAN-13 Barcode</h3>
</div>
<div class="card-body text-center">
<img src="{{ url_for('admin.generate_barcode', tenant_id=tenant_id, product_id=product_id, code_type='ean13') }}" alt="EAN-13 Barcode" class="img-fluid mb-3" style="max-width: 250px;">
<p class="text-muted">Standard EAN-13 barcode format.</p>
<a href="{{ url_for('admin.generate_barcode', tenant_id=tenant_id, product_id=product_id, code_type='ean13') }}" class="btn btn-primary" download="product_{{ product_id }}_ean13.png">Download EAN-13</a>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header">
<h3>Code 128 Barcode</h3>
</div>
<div class="card-body text-center">
<img src="{{ url_for('admin.generate_barcode', tenant_id=tenant_id, product_id=product_id, code_type='code128') }}" alt="Code 128 Barcode" class="img-fluid mb-3" style="max-width: 250px;">
<p class="text-muted">High-density alphanumeric barcode.</p>
<a href="{{ url_for('admin.generate_barcode', tenant_id=tenant_id, product_id=product_id, code_type='code128') }}" class="btn btn-primary" download="product_{{ product_id }}_code128.png">Download Code 128</a>
</div>
</div>
</div>
</div>
<div class="row mt-4">
<div class="col-md-12">
<div class="card">
<div class="card-header">
<h3>Product Information</h3>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-4">
<p><strong>Product ID:</strong> {{ product_id }}</p>
<p><strong>Price:</strong> {{ product.price }}</p>
</div>
<div class="col-md-4">
<p><strong>AR Info URL:</strong>
<a href="{{ url_for('get_ar_info', tenant_id=tenant_id, barcode=product_id) }}" target="_blank">
{{ url_for('get_ar_info', tenant_id=tenant_id, barcode=product_id, _external=True) }}
</a>
</p>
</div>
<div class="col-md-4">
{% if product.image %}
<img src="{{ product.image }}" alt="Product Image" class="img-thumbnail" style="max-height: 100px;">
{% else %}
<p>No product image available</p>
{% endif %}
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row mt-4">
<div class="col-md-12">
<div class="card">
<div class="card-header">
<h3>Print All Codes</h3>
</div>
<div class="card-body">
<p>Use the button below to open a printable version of all barcodes for this product.</p>
<button class="btn btn-success" onclick="window.print()">Print Barcodes</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_css %}
<style>
@media print {
.navbar, .card-header, .btn, .text-muted, .alert {
display: none;
}
.card {
border: none;
margin-bottom: 1cm;
}
.card-body {
text-align: center;
}
.row {
display: block;
}
.col-md-4 {
width: 100%;
max-width: 100%;
flex: 0 0 100%;
page-break-after: always;
}
}
</style>
{% endblock %}

View File

@@ -61,9 +61,12 @@
<a href="/{{ tenant.id }}/edit/{{ product_id }}" class="btn btn-sm btn-outline-primary"> <a href="/{{ tenant.id }}/edit/{{ product_id }}" class="btn btn-sm btn-outline-primary">
<i class="bi bi-pencil"></i> Edit <i class="bi bi-pencil"></i> Edit
</a> </a>
<a href="/{{ tenant.id }}/generate_barcode_page/{{ product_id }}" class="btn btn-sm btn-outline-success"> <button type="button" class="btn btn-sm btn-outline-success"
<i class="bi bi-upc-scan"></i> Barcodes data-bs-toggle="modal" data-bs-target="#barcodeModal"
</a> data-product-id="{{ product_id }}"
data-product-name="{% set matching_fields = product_data|selectattr('fieldName', 'equalto', '_name')|list %}{{ matching_fields[0].value if matching_fields else product_id }}">
<i class="bi bi-upc-scan"></i> Barcode
</button>
<button type="button" class="btn btn-sm btn-outline-danger" <button type="button" class="btn btn-sm btn-outline-danger"
data-bs-toggle="modal" data-bs-target="#deleteModal" data-bs-toggle="modal" data-bs-target="#deleteModal"
data-product-id="{{ product_id }}" data-product-id="{{ product_id }}"
@@ -98,6 +101,28 @@
</div> </div>
</div> </div>
<!-- Barcode Modal -->
<div class="modal fade" id="barcodeModal" tabindex="-1" aria-labelledby="barcodeModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="barcodeModalLabel">Product Barcode</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body text-center">
<h6 id="barcode-product-name" class="mb-3"></h6>
<div class="mb-3">
<img id="barcode-image" src="" alt="Product Barcode" class="img-fluid" style="max-width: 400px;">
</div>
<p class="text-muted"><small>Product ID: <code id="barcode-product-id"></code></small></p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<!-- Delete Confirmation Modal --> <!-- Delete Confirmation Modal -->
<div class="modal fade" id="deleteModal" tabindex="-1" aria-labelledby="deleteModalLabel" aria-hidden="true"> <div class="modal fade" id="deleteModal" tabindex="-1" aria-labelledby="deleteModalLabel" aria-hidden="true">
<div class="modal-dialog"> <div class="modal-dialog">
@@ -121,6 +146,31 @@
{% block extra_js %} {% block extra_js %}
<script> <script>
// Barcode modal functionality
const barcodeModal = document.getElementById('barcodeModal');
if (barcodeModal) {
barcodeModal.addEventListener('show.bs.modal', function (event) {
// Button that triggered the modal
const button = event.relatedTarget;
// Extract product info from data attributes
const productId = button.getAttribute('data-product-id');
const productName = button.getAttribute('data-product-name');
// Update modal content
const modalProductName = document.getElementById('barcode-product-name');
const modalProductId = document.getElementById('barcode-product-id');
const barcodeImage = document.getElementById('barcode-image');
modalProductName.textContent = productName;
modalProductId.textContent = productId;
// Set barcode image source using tenant's barcode type setting
const barcodeType = '{{ tenant.barcode_type or "code128" }}';
barcodeImage.src = '/{{ tenant.id }}/barcodes/' + productId + '_' + barcodeType + '.png';
});
}
// Delete confirmation modal functionality // Delete confirmation modal functionality
const deleteModal = document.getElementById('deleteModal'); const deleteModal = document.getElementById('deleteModal');
if (deleteModal) { if (deleteModal) {

View File

@@ -39,6 +39,29 @@
</form> </form>
</div> </div>
</div> </div>
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-upc-scan"></i> Barcode Settings</h5>
</div>
<div class="card-body">
<p class="text-muted">Configure the default barcode type for products.</p>
<form method="POST" action="/{{ tenant.id }}/settings/barcode">
<div class="mb-3">
<label for="barcode_type" class="form-label">Barcode Type</label>
<select class="form-select" id="barcode_type" name="barcode_type" required>
<option value="code128" {% if tenant.barcode_type == 'code128' %}selected{% endif %}>Code 128</option>
<option value="ean13" {% if tenant.barcode_type == 'ean13' %}selected{% endif %}>EAN-13</option>
<option value="qr" {% if tenant.barcode_type == 'qr' %}selected{% endif %}>QR Code</option>
</select>
<div class="form-text">This will be used when displaying product barcodes</div>
</div>
<button type="submit" class="btn btn-primary">
<i class="bi bi-save"></i> Update Barcode Type
</button>
</form>
</div>
</div>
</div> </div>
<!-- Custom AR Fields Section --> <!-- Custom AR Fields Section -->
@@ -90,12 +113,7 @@
<dd class="col-sm-9">{{ tenant.name }}</dd> <dd class="col-sm-9">{{ tenant.name }}</dd>
<dt class="col-sm-3">API Endpoint:</dt> <dt class="col-sm-3">API Endpoint:</dt>
<dd class="col-sm-9"> <dd class="col-sm-9"><code>{{ server_url }}/{{ tenant.id }}/arinfo</code></dd>
<code>{{ server_url }}/{{ tenant.id }}/arinfo</code>
<button type="button" class="btn btn-sm btn-outline-primary ms-2" data-bs-toggle="modal" data-bs-target="#qrModal">
<i class="bi bi-qr-code"></i> QR Code
</button>
</dd>
<dt class="col-sm-3">Created:</dt> <dt class="col-sm-3">Created:</dt>
<dd class="col-sm-9">{{ tenant.created_at }}</dd> <dd class="col-sm-9">{{ tenant.created_at }}</dd>
@@ -106,26 +124,4 @@
</div> </div>
</div> </div>
</div> </div>
<!-- QR Code Modal -->
<div class="modal fade" id="qrModal" tabindex="-1" aria-labelledby="qrModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="qrModalLabel">API Endpoint QR Code</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body text-center">
<p class="text-muted mb-3">Scan this QR code to access the AR API endpoint:</p>
<div class="mb-3">
<img src="/{{ tenant.id }}/qrcode/arinfo" alt="AR API QR Code" class="img-fluid" style="max-width: 300px;">
</div>
<code class="d-block mt-2">{{ server_url }}/{{ tenant.id }}/arinfo</code>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
{% endblock %} {% endblock %}