feat: allow multiple barcodes to be associated with a product

chore: consistent renaming from ean to barcode
This commit is contained in:
s3lph 2024-12-09 22:07:54 +01:00
parent a7150e123e
commit 583107ac63
Signed by: s3lph
GPG key ID: 0AA29A52FB33CFB5
20 changed files with 294 additions and 124 deletions

View file

@ -5,7 +5,7 @@ import crypt
from hmac import compare_digest
from datetime import datetime, UTC
from matemat.db.primitives import User, Token, Product, ReceiptPreference, Receipt, \
from matemat.db.primitives import User, Token, Product, Barcode, ReceiptPreference, Receipt, \
Transaction, Consumption, Deposit, Modification
from matemat.exceptions import AuthenticationError, DatabaseConsistencyError
from matemat.db import DatabaseWrapper
@ -440,12 +440,12 @@ class MatematDatabase(object):
products: List[Product] = []
with self.db.transaction(exclusive=False) as c:
for row in c.execute('''
SELECT product_id, name, price_member, price_non_member, custom_price, stock, stockable, ean
SELECT product_id, name, price_member, price_non_member, custom_price, stock, stockable
FROM products ORDER BY name
'''):
product_id, name, price_member, price_external, custom_price, stock, stockable, ean = row
product_id, name, price_member, price_external, custom_price, stock, stockable = row
products.append(
Product(product_id, name, price_member, price_external, custom_price, stockable, stock, ean))
Product(product_id, name, price_member, price_external, custom_price, stockable, stock))
return products
def get_product(self, pid: int) -> Product:
@ -456,36 +456,38 @@ class MatematDatabase(object):
with self.db.transaction(exclusive=False) as c:
# Fetch all values to construct the product
c.execute('''
SELECT product_id, name, price_member, price_non_member, custom_price, stock, stockable, ean
SELECT product_id, name, price_member, price_non_member, custom_price, stock, stockable
FROM products
WHERE product_id = :product_id''', {'product_id': pid})
row = c.fetchone()
if row is None:
raise ValueError(f'No product with product ID {pid} exists.')
# Unpack the row and construct the product
product_id, name, price_member, price_non_member, custom_price, stock, stockable, ean = row
return Product(product_id, name, price_member, price_non_member, custom_price, stockable, stock, ean)
product_id, name, price_member, price_non_member, custom_price, stock, stockable = row
return Product(product_id, name, price_member, price_non_member, custom_price, stockable, stock)
def get_product_by_ean(self, ean: str) -> Product:
def get_product_by_barcode(self, barcode: str) -> Product:
"""
Return a product identified by its EAN code.
:param ean: The product's EAN code.
Return a product identified by its barcode.
:param barcode: The product's barcode code.
"""
with self.db.transaction(exclusive=False) as c:
# Fetch all values to construct the product
c.execute('''
SELECT product_id, name, price_member, price_non_member, custom_price, stock, stockable, ean
FROM products
WHERE ean = :ean''', {'ean': ean})
SELECT p.product_id, p.name, price_member, price_non_member, custom_price, stock, stockable
FROM products AS p
JOIN barcodes AS b
ON b.product_id = p.product_id
WHERE b.barcode = :barcode''', {'barcode': barcode})
row = c.fetchone()
if row is None:
raise ValueError(f'No product with EAN code {ean} exists.')
raise ValueError(f'No product with barcode {barcode} exists.')
# Unpack the row and construct the product
product_id, name, price_member, price_non_member, custom_price, stock, stockable, ean = row
return Product(product_id, name, price_member, price_non_member, custom_price, stockable, stock, ean)
product_id, name, price_member, price_non_member, custom_price, stock, stockable = row
return Product(product_id, name, price_member, price_non_member, custom_price, stockable, stock)
def create_product(self, name: str, price_member: int, price_non_member: int, custom_price:
bool, stockable: bool, ean: str) -> Product:
bool, stockable: bool, barcode: str) -> Product:
"""
Creates a new product.
:param name: Name of the product.
@ -493,6 +495,7 @@ class MatematDatabase(object):
:param price_non_member: Price of the product for non-members.
:param custom_price: Whether the price is customizable. If yes, the price values are understood as minimum.
:param stockable: True if the product should be stockable, false otherwise.
:param barcode: If provided, a barcode this product is identified by.
:return: A Product object representing the created product.
:raises ValueError: If a product with the same name already exists.
"""
@ -502,19 +505,27 @@ class MatematDatabase(object):
if c.fetchone() is not None:
raise ValueError(f'A product with the name \'{name}\' already exists.')
c.execute('''
INSERT INTO products (name, price_member, price_non_member, custom_price, stock, stockable, ean)
VALUES (:name, :price_member, :price_non_member, :custom_price, 0, :stockable, :ean)
INSERT INTO products (name, price_member, price_non_member, custom_price, stock, stockable)
VALUES (:name, :price_member, :price_non_member, :custom_price, 0, :stockable)
''', {
'name': name,
'price_member': price_member,
'price_non_member': price_non_member,
'custom_price': custom_price,
'stockable': stockable,
'ean': ean,
})
c.execute('SELECT last_insert_rowid()')
product_id = int(c.fetchone()[0])
return Product(product_id, name, price_member, price_non_member, custom_price, stockable, 0, ean)
if barcode:
c.execute('''
INSERT INTO barcodes (barcode, product_id, name)
VALUES (:barcode, :product_id, :name)
''', {
'barcode': barcode,
'product_id': product_id,
'name': name,
})
return Product(product_id, name, price_member, price_non_member, custom_price, stockable, 0)
def change_product(self, product: Product, **kwargs) -> None:
"""
@ -533,7 +544,6 @@ class MatematDatabase(object):
custom_price: int = kwargs['custom_price'] if 'custom_price' in kwargs else product.custom_price
stock: int = kwargs['stock'] if 'stock' in kwargs else product.stock
stockable: bool = kwargs['stockable'] if 'stockable' in kwargs else product.stockable
ean: str = kwargs['ean'] if 'ean' in kwargs else product.ean
with self.db.transaction() as c:
c.execute('''
UPDATE products
@ -544,7 +554,6 @@ class MatematDatabase(object):
custom_price = :custom_price,
stock = :stock,
stockable = :stockable,
ean = :ean
WHERE product_id = :product_id
''', {
'product_id': product.id,
@ -554,7 +563,6 @@ class MatematDatabase(object):
'custom_price': custom_price,
'stock': stock,
'stockable': stockable,
'ean': ean
})
affected = c.execute('SELECT changes()').fetchone()[0]
if affected != 1:
@ -567,7 +575,6 @@ class MatematDatabase(object):
product.custom_price = custom_price
product.stock = stock
product.stockable = stockable
product.ean = ean
def delete_product(self, product: Product) -> None:
"""
@ -585,6 +592,61 @@ class MatematDatabase(object):
raise DatabaseConsistencyError(
f'delete_product should affect 1 products row, but affected {affected}')
def list_barcodes(self, pid: Optional[int] = None) -> List[Barcode]:
barcodes: List[Barcode] = []
with self.db.transaction(exclusive=False) as c:
if pid is not None:
rows = c.execute('''
SELECT barcode_id, barcode, product_id, name
FROM barcodes
WHERE product_id = :product_id
''', {'product_id': pid})
else:
rows = c.execute('''
SELECT barcode_id, barcode, product_id, name
FROM barcodes
''')
for row in rows:
barcode_id, barcode, product_id, name = row
barcodes.append(
Barcode(barcode_id, barcode, product_id, name))
return barcodes
def get_barcode(self, bcid: int) -> Barcode:
with self.db.transaction(exclusive=False) as c:
c.execute('''
SELECT barcode_id, barcode, product_id, name
FROM barcodes
WHERE barcode_id = :barcode_id
''', {'barcode_id': bcid})
barcode_id, barcode, product_id, name = c.fetchone()
return Barcode(barcode_id, barcode, product_id, name)
def add_barcode(self, product: Product, barcode: str, name: Optional[str]):
with self.db.transaction() as c:
c.execute('''
INSERT INTO barcodes (barcode, product_id, name)
VALUES (:barcode, :product_id, :name)
''', {
'barcode': barcode,
'product_id': product.id,
'name': name
})
c.execute('SELECT last_insert_rowid()')
bcid = int(c.fetchone()[0])
return Barcode(bcid, barcode, product.id, name)
def delete_barcode(self, barcode: Barcode):
with self.db.transaction() as c:
c.execute('''
DELETE FROM barcodes
WHERE barcode_id = :barcode_id
''', {'barcode_id': barcode.id})
affected = c.execute('SELECT changes()').fetchone()[0]
if affected != 1:
raise DatabaseConsistencyError(
f'delete_barcode should affect 1 token row, but affected {affected}')
def increment_consumption(self, user: User, product: Product, custom_price: int = None) -> None:
"""
Decrement the user's balance by the price of the product and create an entry in the statistics table.

View file

@ -374,3 +374,27 @@ def migrate_schema_10(c: sqlite3.Cursor):
c.execute('''
DROP TABLE users_old
''')
def migrate_schema_11(c: sqlite3.Cursor):
c.execute('''
CREATE TABLE barcodes (
barcode_id INTEGER PRIMARY KEY,
barcode TEXT UNIQUE NOT NULL,
product_id INTEGER NOT NULL,
name TEXT DEFAULT NULL,
FOREIGN KEY (product_id) REFERENCES products(product_id)
ON DELETE CASCADE ON UPDATE CASCADE
)
''')
c.execute('''
INSERT INTO barcodes (barcode, product_id, name)
SELECT ean, product_id, name FROM products
WHERE ean IS NOT NULL
''')
c.execute('''
DROP INDEX IF EXISTS _matemat_products_ean_unique
''')
c.execute('''
ALTER TABLE products DROP COLUMN ean
''')

View file

@ -0,0 +1,34 @@
from typing import Optional
class Barcode:
"""
Representation of a product barcode associated with a product.
:param _id: The barcode ID in the database.
:param barcode: The barcode strig.
:param product_id: The ID of the product this barcode belongs to.
:param name: The display name of the token:
"""
def __init__(self,
_id: int,
barcode: str,
product_id: int,
name: str) -> None:
self.id: int = _id
self.barcode: str = barcode
self.product_id: str = product_id
self.name = name
def __eq__(self, other) -> bool:
if not isinstance(other, Barcode):
return False
return self.id == other.id and \
self.barcode == other.barcode and \
self.product_id == other.product_id and \
self.name == other.name
def __hash__(self) -> int:
return hash((self.id, self.barcode, self.product_id, self.name))

View file

@ -11,12 +11,11 @@ class Product:
:param custom_price: If true, the user can choose the price to pay, but at least the regular price.
:param stock: The number of items of this product currently in stock, or None if not stockable.
:param stockable: Whether this product is stockable.
:param ean: The product's EAN code. May be None.
"""
def __init__(self, _id: int, name: str,
price_member: int, price_non_member: int, custom_price: bool,
stockable: bool, stock: int, ean: str) -> None:
stockable: bool, stock: int) -> None:
self.id: int = _id
self.name: str = name
self.price_member: int = price_member
@ -24,7 +23,6 @@ class Product:
self.custom_price: bool = custom_price
self.stock: int = stock
self.stockable: bool = stockable
self.ean: str = ean
def __eq__(self, other) -> bool:
if not isinstance(other, Product):
@ -35,9 +33,8 @@ class Product:
self.price_non_member == other.price_non_member and \
self.custom_price == other.custom_price and \
self.stock == other.stock and \
self.stockable == other.stockable and \
self.ean == other.ean
self.stockable == other.stockable
def __hash__(self) -> int:
return hash((self.id, self.name, self.price_member, self.price_non_member, self.custom_price,
self.stock, self.stockable, self.ean))
self.stock, self.stockable))

View file

@ -5,6 +5,7 @@ This package provides the 'primitive types' the Matemat software deals with - na
from .User import User
from .Token import Token
from .Product import Product
from .Barcode import Barcode
from .ReceiptPreference import ReceiptPreference
from .Transaction import Transaction, Consumption, Deposit, Modification
from .Receipt import Receipt

View file

@ -43,12 +43,12 @@ def admin():
# Fetch all existing users and products from the database
users = db.list_users()
tokens = db.list_tokens(uid)
products = db.list_products()
barcodes = db.list_barcodes()
# Render the "Admin" page
now = str(int(datetime.now(UTC).timestamp()))
return template.render('admin.html',
authuser=user, authlevel=authlevel, tokens=tokens, users=users, products=products,
authuser=user, authlevel=authlevel, users=users, products=products, barcodes=barcodes,
receipt_preference_class=ReceiptPreference, now=now,
setupname=config['InstanceName'], config_smtp_enabled=config['SmtpSendReceipts'])
@ -107,9 +107,9 @@ def handle_change(args: FormsDict, files: FormsDict, db: MatematDatabase):
price_non_member = parse_chf(str(args.pricenonmember))
custom_price = 'custom_price' in args
stockable = 'stockable' in args
ean = str(args.ean) or None
barcode = str(args.barcode) or None
# Create the product in the database
newproduct = db.create_product(name, price_member, price_non_member, custom_price, stockable, ean)
newproduct = db.create_product(name, price_member, price_non_member, custom_price, stockable, barcode)
# If a new product image was uploaded, process it
image = files.image.file.read() if 'image' in files else None
if image is not None and len(image) > 0:

View file

@ -23,16 +23,13 @@ def main_page():
products = db.list_products()
buyproduct = None
if request.params.ean:
if request.params.barcode:
try:
buyproduct = db.get_product_by_ean(request.params.ean)
Notification.success(
f'Login will purchase <strong>{buyproduct.name}</strong>. ' +
'Click <a class="alert-link" href="/">here</a> to abort.')
buyproduct = db.get_product_by_barcode(request.params.barcode)
except ValueError:
if not session.has(session_id, 'authenticated_user'):
try:
user, token = db.tokenlogin(str(request.params.ean))
user, token = db.tokenlogin(str(request.params.barcode))
# Set the user ID session variable
session.put(session_id, 'authenticated_user', user.id)
# Set the authlevel session variable (0 = none, 1 = token, 2 = touchkey, 3 = password)
@ -41,7 +38,7 @@ def main_page():
except AuthenticationError:
# Redirect to main page on token login error
pass
Notification.error(f'EAN code {request.params.ean} is not associated with any product.', decay=True)
Notification.error(f'Barcode {request.params.barcode} is not associated with any product.', decay=True)
redirect('/')
# Check whether a user is logged in
@ -49,7 +46,7 @@ def main_page():
# Fetch the user id and authentication level (touchkey vs password) from the session storage
uid: int = session.get(session_id, 'authenticated_user')
authlevel: int = session.get(session_id, 'authentication_level')
# If an EAN code was scanned, directly trigger the purchase
# If an barcode was scanned, directly trigger the purchase
if buyproduct:
redirect(f'/buy?pid={buyproduct.id}')
# Fetch the user object from the database (for name display, price calculation and admin check)
@ -65,6 +62,10 @@ def main_page():
# If there are no admin users registered, jump to the admin creation procedure
if not db.has_admin_users():
redirect('/userbootstrap')
if buyproduct:
Notification.success(
f'Login will purchase <strong>{buyproduct.name}</strong>. ' +
'Click <a class="alert-link" href="/">here</a> to abort.')
# If no user is logged in, fetch the list of users and render the userlist template
users = db.list_users(with_touchkey=True)
return template.render('userlist.html',

View file

@ -56,9 +56,10 @@ def modproduct():
redirect('/admin')
# Render the "Modify Product" page
barcodes = db.list_barcodes(modproduct_id)
now = str(int(datetime.now(UTC).timestamp()))
return template.render('modproduct.html',
authuser=authuser, product=product, authlevel=authlevel,
authuser=authuser, product=product, authlevel=authlevel, barcodes=barcodes,
setupname=config['InstanceName'], now=now)
@ -85,6 +86,30 @@ def handle_change(args: FormsDict, files: FormsDict, product: Product, db: Matem
except FileNotFoundError:
pass
elif change == 'addbarcode':
if 'barcode' not in args:
return
barcode = str(args.barcode)
name = None if 'name' not in args or len(args.name) == 0 else str(args.name)
try:
bcobj = db.add_barcode(product, barcode, name)
Notification.success(f'Barcode {name} added successfully', decay=True)
except DatabaseConsistencyError:
Notification.error(f'Barcode {barcode} already exists', decay=True)
elif change == 'delbarcode':
try:
bcid = id(str(request.params.barcode))
barcode = db.get_barcode(bcid)
except Exception as e:
Notification.error('Barcode not found', decay=True)
return
try:
db.delete_barcode(token)
except DatabaseConsistencyError:
Notification.error(f'Failed to delete barcode {barcode.name}', decay=True)
Notification.success(f'Barcode {barcode.name} removed', decay=True)
# Admin requested update of the product details
elif change == 'update':
# Only write a change if all properties of the product are present in the request arguments
@ -98,12 +123,11 @@ def handle_change(args: FormsDict, files: FormsDict, product: Product, db: Matem
custom_price = 'custom_price' in args
stock = int(str(args.stock))
stockable = 'stockable' in args
ean = str(args.ean) or None
# Attempt to write the changes to the database
try:
db.change_product(product,
name=name, price_member=price_member, price_non_member=price_non_member,
custom_price=custom_price, stock=stock, stockable=stockable, ean=ean)
custom_price=custom_price, stock=stock, stockable=stockable)
stock_provider = get_stock_provider()
if stock_provider.needs_update() and product.stockable:
stock_provider.set_stock(product, stock)

View file

@ -29,14 +29,14 @@ def render(name: str, **kwargs):
global __jinja_env
config = get_app_config()
template: jinja2.Template = __jinja_env.get_template(name)
wsacl = netaddr.IPSet([addr.strip() for addr in config.get('EanWebsocketAcl', '').split(',')])
if config.get('EanWebsocketUrl', '') and request.remote_addr in wsacl:
eanwebsocket = config.get('EanWebsocketUrl')
wsacl = netaddr.IPSet([addr.strip() for addr in config.get('BarcodeWebsocketAcl', '').split(',')])
if config.get('BarcodeWebsocketUrl', '') and request.remote_addr in wsacl:
bcwebsocket = config.get('BarcodeWebsocketUrl')
else:
eanwebsocket = None
bcwebsocket = None
return template.render(
__version__=__version__,
notifications=Notification.render(),
eanwebsocket=eanwebsocket,
barcodewebsocket=bcwebsocket,
**kwargs
).encode('utf-8')

View file

@ -41,8 +41,8 @@ InstanceName=Matemat
# Open a websocket connection on which to listen for scanned barcodes.
# Can be restricted so that e.g. the connection is only attempted when the client is localhost.
#
#EanWebsocketUrl=ws://localhost:47808/ws
#EanWebsocketAcl=::1,::ffff:127.0.0.0/104,127.0.0.0/8
#BarcodeWebsocketUrl=ws://localhost:47808/ws
#BarcodeWebsocketAcl=::1,::ffff:127.0.0.0/104,127.0.0.0/8
# Add static HTTP headers in this section

View file

@ -68,7 +68,7 @@
<table class="table table-striped">
<tr>
<th>Name</th>
<th>EAN code</th>
<th>Barcodes</th>
<th>Member price</th>
<th>Non-member price</th>
<th>Custom price</th>
@ -78,7 +78,7 @@
</tr>
<tr>
<td><input class="form-control" id="admin-newproduct-name" type="text" name="name" placeholder="New product name"></td>
<td><input class="form-control" id="admin-newproduct-ean" type="text" name="ean" placeholder="Scan to insert EAN"></td>
<td><input class="form-control" id="admin-newproduct-barcode" type="text" name="barcode" placeholder="Scan barcode to insert here"></td>
<td>
<div class="input-group mb-3">
<span class="input-group-text">CHF</span>
@ -99,7 +99,15 @@
{% for product in products %}
<tr>
<td>{{ product.name }}</td>
<td>{{ product.ean or '' }}</td>
<td>
{% set bcs = barcodes | selectattr('product_id', 'eq', product.id) | list %}
{% if bcs | length > 0 %}
{{ bcs[0].barcode }}
{% if bcs | length > 1 %}
<span class="badge bg-secondary">+{{ bcs | length - 1 }}</span>
{% endif %}
{% endif %}
</td>
<td>{{ product.price_member | chf }}</td>
<td>{{ product.price_non_member | chf }}</td>
<td>{{ '✓' if product.custom_price else '✗' }}</td>
@ -160,9 +168,9 @@
{% endblock %}
{% block eanwebsocket %}
let eaninput = document.getElementById("admin-newproduct-ean");
eaninput.value = e.data;
eaninput.select();
eaninput.scrollIntoView();
{% block barcodewebsocket %}
let bcinput = document.getElementById("admin-newproduct-barcode");
bcinput.value = e.data;
bcinput.select();
bcinput.scrollIntoView();
{% endblock %}

View file

@ -68,17 +68,17 @@
{% endblock %}
</footer>
{% if eanwebsocket %}
{% if barcodewebsocket %}
<script>
function connect() {
let socket = new WebSocket("{{ eanwebsocket }}");
let socket = new WebSocket("{{ barcodewebsocket }}");
socket.onclose = () => { setTimeout(connect, 1000); };
socket.onmessage = function (e) {
// Focus this tab - requires https://git.kabelsalat.ch/ccc-basel/barcode-utils
if (typeof window.extension_tabfocus === "function") {
window.extension_tabfocus();
}
{% block eanwebsocket %}{% endblock %}
{% block barcodewebsocket %}{% endblock %}
};
}
window.addEventListener("load", () => { connect(); });

View file

@ -24,7 +24,7 @@
{% endblock %}
{% block eanwebsocket %}
document.location = "/?ean=" + e.data;
{% block barcodewebsocket %}
document.location = "/?barcode=" + e.data;
{% endblock %}

View file

@ -2,68 +2,90 @@
{% block main %}
<section id="modproduct">
<section id="modproduct">
<h1>Modify {{ product.name }}</h1>
<form id="modproduct-form" method="post" action="/modproduct?change=update" enctype="multipart/form-data" accept-charset="UTF-8">
<label class="form-label" for="modproduct-name">Name: </label>
<input class="form-control" id="modproduct-name" type="text" name="name" value="{{ product.name }}" /><br/>
<label class="form-label" for="modproduct-name">Name: </label>
<input class="form-control" id="modproduct-name" type="text" name="name" value="{{ product.name }}" /><br/>
<label class="form-label" for="modproduct-ean">EAN code: </label>
<input class="form-control" id="modproduct-ean" type="text" name="ean" value="{{ product.ean or '' }}" /><br/>
<label class="form-label" for="modproduct-price-member">Member price: </label>
<div class="input-group mb-3">
<span class="input-group-text">CHF</span>
<input class="form-control" id="modproduct-price-member" type="number" step="0.01" name="pricemember" value="{{ product.price_member|chf(False) }}" />
</div>
<label class="form-label" for="modproduct-price-member">Member price: </label>
<div class="input-group mb-3">
<span class="input-group-text">CHF</span>
<input class="form-control" id="modproduct-price-member" type="number" step="0.01" name="pricemember" value="{{ product.price_member|chf(False) }}" />
</div>
<label class="form-label" for="modproduct-price-non-member">Non-member price: </label>
<div class="input-group mb-3">
<span class="input-group-text">CHF</span>
<input class="form-control" id="modproduct-price-non-member" type="number" step="0.01" name="pricenonmember" value="{{ product.price_non_member|chf(False) }}" />
</div>
<label class="form-label" for="modproduct-price-non-member">Non-member price: </label>
<div class="input-group mb-3">
<span class="input-group-text">CHF</span>
<input class="form-control" id="modproduct-price-non-member" type="number" step="0.01" name="pricenonmember" value="{{ product.price_non_member|chf(False) }}" />
</div>
<div class="form-check">
<div class="form-check">
<input class="form-check-input" id="modproduct-custom-price" type="checkbox" name="custom_price" {% if product.custom_price %} checked="checked" {% endif %} />
<label class="form-check-label" for="modproduct-custom-price"><abbr title="When 'Custom Price' is enabled, users choose the price to pay, but at least the prices given above">Custom Price</abbr></label>
</div>
</div>
<div class="form-check">
<div class="form-check">
<input class="form-check-input" id="modproduct-stockable" type="checkbox" name="stockable" {% if product.stockable %} checked="checked" {% endif %} />
<label class="form-check-label" for="modproduct-stockable">Stockable</label>
</div>
</div>
<label class="form-label" for="modproduct-balance">Stock: </label>
<input class="form-control" id="modproduct-balance" name="stock" type="text" value="{{ product.stock }}" /><br/>
<label class="form-label" for="modproduct-balance">Stock: </label>
<input class="form-control" id="modproduct-balance" name="stock" type="text" value="{{ product.stock }}" /><br/>
<label class="form-label" for="modproduct-image">
<img height="150" src="/static/upload/thumbnails/products/{{ product.id }}.png?cacheBuster={{ now }}" alt="Image of {{ product.name }}" />
</label><br/>
<input class="form-control" id="modproduct-image" type="file" name="image" accept="image/*" /><br/>
<label class="form-label" for="modproduct-image">
<img height="150" src="/static/upload/thumbnails/products/{{ product.id }}.png?cacheBuster={{ now }}" alt="Image of {{ product.name }}" />
</label><br/>
<input class="form-control" id="modproduct-image" type="file" name="image" accept="image/*" /><br/>
<input id="modproduct-productid" type="hidden" name="productid" value="{{ product.id }}" /><br/>
<input id="modproduct-productid" type="hidden" name="productid" value="{{ product.id }}" /><br/>
<input class="btn btn-primary" type="submit" value="Save changes">
<input class="btn btn-primary" type="submit" value="Save changes">
</form>
<h2>Barcodes</h2>
<form id="modproduct-barcode-form" method="post" action="/modproduct?change=addbarcode" accept-charset="UTF-8">
<input id="modproduct-barcode-productid" type="hidden" name="productid" value="{{ product.id }}" />
<table class="table table-striped">
<tr>
<th>Barcode</th>
<th>Name</th>
<th>Actions</th>
</tr>
<tr>
<td><input class="form-control" id="modproduct-barcode-barcode" type="text" name="barcode" value="" placeholder="Scan barcode to insert here"></td>
<td><input class="form-control" id="modproduct-barcode-name" type="text" name="name" value="" placeholder="Name for this barcode"></td>
<td><input class="btn btn-success" type="submit" value="Add barcode"></td>
</tr>
{% for barcode in barcodes %}
<tr>
<td>{{ barcode.barcode }}</td>
<td>{{ barcode.name }}</td>
<td><a class="btn btn-danger" href="/modproduct?change=delbarcode&barcode={{ barcode.id }}">Delete</a></td>
</tr>
{% endfor %}
</table>
</form>
<h2>Delete Product</h2>
<form id="modproduct-delproduct-form" method="post" action="/modproduct?change=del" accept-charset="UTF-8">
<input id="modproduct-delproduct-productid" type="hidden" name="productid" value="{{ product.id }}" /><br/>
<input class="btn btn-danger" type="submit" value="Delete product {{ product.name }}" />
<input id="modproduct-delproduct-productid" type="hidden" name="productid" value="{{ product.id }}" /><br/>
<input class="btn btn-danger" type="submit" value="Delete product {{ product.name }}" />
</form>
</section>
</section>
{{ super() }}
{{ super() }}
{% endblock %}
{% block eanwebsocket %}
let eaninput = document.getElementById("modproduct-ean");
eaninput.value = e.data;
eaninput.select();
eaninput.scrollIntoView();
{% block barcodewebsocket %}
let bcinput = document.getElementById("modproduct-barcode-barcode");
bcinput.value = e.data;
bcinput.select();
bcinput.scrollIntoView();
{% endblock %}

View file

@ -48,7 +48,7 @@
{% if product.custom_price %}
<a class="card h-100 text-bg-light" onclick="setup_custom_price({{ product.id }}, '{{ product.name}}');">
{% else %}
<a class="card h-100 text-bg-light" {% if product.ean %}id="a-buy-ean{{ product.ean }}"{% endif %} href="/buy?pid={{ product.id }}">
<a class="card h-100 text-bg-light" href="/buy?pid={{ product.id }}">
{% endif %}
<div class="card-header">
{{ product.name }}
@ -82,11 +82,6 @@
{% endblock %}
{% block eanwebsocket %}
let eaninput = document.getElementById("a-buy-ean" + e.data);
if (eaninput === null) {
document.location = "?ean=" + e.data;
} else {
eaninput.click();
}
{% block barcodewebsocket %}
document.location = "?barcode=" + e.data;
{% endblock %}

View file

@ -151,9 +151,11 @@
<section class="tab-pane fade pt-3" id="settings-tokens-tab-pane" role="tabpanel">
<h2>Tokens</h2>
<strong>Warning:</strong>
Login tokens are a convenience feature that if used may weaken security.
Make sure you only use tokens not easily accessible to other people.
<div class="alert alert-warning">
<strong>Warning: </strong>
Login tokens are a convenience feature that if used may weaken security.
Make sure you only use tokens not easily accessible to other people.
</div>
<form id="settings-newtoken-form" method="post" action="/settings?change=addtoken" accept-charset="UTF-8">
<table class="table table-striped">
@ -164,7 +166,7 @@
<th>Actions</th>
</tr>
<tr>
<td><input class="form-control" id="settings-newtoken-token" type="password" name="token" value="" placeholder="Scan to insert EAN"></td>
<td><input class="form-control" id="settings-newtoken-token" type="password" name="token" value="" placeholder="Scan barcode to insert here"></td>
<td><input class="form-control" id="settings-newtoken-name" type="text" name="name" value="" placeholder="New token name"></td>
<td></td>
<td><input class="btn btn-success" type="submit" value="Create Token"></td>
@ -186,7 +188,7 @@
{% endblock %}
{% block eanwebsocket %}
{% block barcodewebsocket %}
let tokeninput = document.getElementById("settings-newtoken-token");
tokeninput.value = e.data;
tokeninput.select();

View file

@ -40,6 +40,6 @@
{% endblock %}
{% block eanwebsocket %}
document.location = "/?ean=" + e.data;
{% block barcodewebsocket %}
document.location = "/?barcode=" + e.data;
{% endblock %}

View file

@ -33,6 +33,6 @@
{% endblock %}
{% block eanwebsocket %}
document.location = "/?ean=" + e.data;
{% block barcodewebsocket %}
document.location = "/?barcode=" + e.data;
{% endblock %}

View file

@ -26,6 +26,6 @@
{% endblock %}
{% block eanwebsocket %}
document.location = "/?ean=" + e.data;
{% block barcodewebsocket %}
document.location = "/?barcode=" + e.data;
{% endblock %}

View file

@ -22,6 +22,6 @@
{% endblock %}
{% block eanwebsocket %}
document.location = "/?ean=" + e.data;
{% block barcodewebsocket %}
document.location = "/?barcode=" + e.data;
{% endblock %}