feat: allow multiple barcodes to be associated with a product
chore: consistent renaming from ean to barcode
This commit is contained in:
parent
a7150e123e
commit
583107ac63
20 changed files with 294 additions and 124 deletions
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
''')
|
||||
|
|
34
matemat/db/primitives/Barcode.py
Normal file
34
matemat/db/primitives/Barcode.py
Normal 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))
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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(); });
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
|
||||
{% endblock %}
|
||||
|
||||
{% block eanwebsocket %}
|
||||
document.location = "/?ean=" + e.data;
|
||||
{% block barcodewebsocket %}
|
||||
document.location = "/?barcode=" + e.data;
|
||||
{% endblock %}
|
||||
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -40,6 +40,6 @@
|
|||
|
||||
{% endblock %}
|
||||
|
||||
{% block eanwebsocket %}
|
||||
document.location = "/?ean=" + e.data;
|
||||
{% block barcodewebsocket %}
|
||||
document.location = "/?barcode=" + e.data;
|
||||
{% endblock %}
|
||||
|
|
|
@ -33,6 +33,6 @@
|
|||
|
||||
{% endblock %}
|
||||
|
||||
{% block eanwebsocket %}
|
||||
document.location = "/?ean=" + e.data;
|
||||
{% block barcodewebsocket %}
|
||||
document.location = "/?barcode=" + e.data;
|
||||
{% endblock %}
|
||||
|
|
|
@ -26,6 +26,6 @@
|
|||
|
||||
{% endblock %}
|
||||
|
||||
{% block eanwebsocket %}
|
||||
document.location = "/?ean=" + e.data;
|
||||
{% block barcodewebsocket %}
|
||||
document.location = "/?barcode=" + e.data;
|
||||
{% endblock %}
|
||||
|
|
|
@ -22,6 +22,6 @@
|
|||
|
||||
{% endblock %}
|
||||
|
||||
{% block eanwebsocket %}
|
||||
document.location = "/?ean=" + e.data;
|
||||
{% block barcodewebsocket %}
|
||||
document.location = "/?barcode=" + e.data;
|
||||
{% endblock %}
|
||||
|
|
Loading…
Reference in a new issue