Merge branch 'staging'
This commit is contained in:
commit
bc7f04f360
25 changed files with 221 additions and 194 deletions
|
@ -95,28 +95,6 @@ build_debian:
|
||||||
only:
|
only:
|
||||||
- tags
|
- tags
|
||||||
|
|
||||||
build_archlinux:
|
|
||||||
stage: build
|
|
||||||
image: archlinux/base:latest # Use an archlinux image instead of the customized debian image.
|
|
||||||
script:
|
|
||||||
- pacman -Sy --noconfirm python python-setuptools python-pip python-wheel python-bottle python-jinja python-pillow python-magic base-devel
|
|
||||||
- export MATEMAT_VERSION=$(python -c 'import matemat; print(matemat.__version__)')
|
|
||||||
- cp -r static/ package/archlinux/matemat/usr/lib/matemat/static/
|
|
||||||
- cp -r templates/ package/archlinux/matemat/usr/lib/matemat/templates/
|
|
||||||
- python3 setup.py egg_info -d -b +master install --root=package/archlinux/matemat/ --prefix=/usr --optimize=1
|
|
||||||
- cd package/archlinux
|
|
||||||
- mv matemat/usr/bin/matemat matemat/usr/lib/matemat/matemat
|
|
||||||
- rm -rf matemat/usr/bin
|
|
||||||
- sed -re "s/__VERSION__/${MATEMAT_VERSION}/g" -i PKGBUILD
|
|
||||||
- sudo -u nobody makepkg -c
|
|
||||||
- sha256sum *.pkg.tar.zst > SHA256SUMS
|
|
||||||
artifacts:
|
|
||||||
paths:
|
|
||||||
- "package/archlinux/*.pkg.tar.zst"
|
|
||||||
- package/archlinux/SHA256SUMS
|
|
||||||
only:
|
|
||||||
- tags
|
|
||||||
|
|
||||||
|
|
||||||
release:
|
release:
|
||||||
stage: deploy
|
stage: deploy
|
||||||
|
|
15
CHANGELOG.md
15
CHANGELOG.md
|
@ -1,5 +1,20 @@
|
||||||
# Matemat Changelog
|
# Matemat Changelog
|
||||||
|
|
||||||
|
<!-- BEGIN RELEASE v0.2.8 -->
|
||||||
|
## Version 0.2.8
|
||||||
|
|
||||||
|
Feature release
|
||||||
|
|
||||||
|
### Changes
|
||||||
|
|
||||||
|
<!-- BEGIN CHANGES 0.2.8 -->
|
||||||
|
- Feature: Add "custom price" products
|
||||||
|
- Fix: Buying not working when using the NullDispenser
|
||||||
|
- Breaking: Remove Arch Linux packaging
|
||||||
|
<!-- END CHANGES 0.2.8 -->
|
||||||
|
|
||||||
|
<!-- END RELEASE v0.2.8 -->
|
||||||
|
|
||||||
<!-- BEGIN RELEASE v0.2.7 -->
|
<!-- BEGIN RELEASE v0.2.7 -->
|
||||||
## Version 0.2.7
|
## Version 0.2.7
|
||||||
|
|
||||||
|
|
|
@ -1,2 +1,2 @@
|
||||||
|
|
||||||
__version__ = '0.2.7'
|
__version__ = '0.2.8'
|
||||||
|
|
|
@ -354,11 +354,11 @@ class MatematDatabase(object):
|
||||||
products: List[Product] = []
|
products: List[Product] = []
|
||||||
with self.db.transaction(exclusive=False) as c:
|
with self.db.transaction(exclusive=False) as c:
|
||||||
for row in c.execute('''
|
for row in c.execute('''
|
||||||
SELECT product_id, name, price_member, price_non_member, stock, stockable
|
SELECT product_id, name, price_member, price_non_member, custom_price, stock, stockable
|
||||||
FROM products
|
FROM products
|
||||||
'''):
|
'''):
|
||||||
product_id, name, price_member, price_external, stock, stockable = row
|
product_id, name, price_member, price_external, custom_price, stock, stockable = row
|
||||||
products.append(Product(product_id, name, price_member, price_external, stockable, stock))
|
products.append(Product(product_id, name, price_member, price_external, custom_price, stockable, stock))
|
||||||
return products
|
return products
|
||||||
|
|
||||||
def get_product(self, pid: int) -> Product:
|
def get_product(self, pid: int) -> Product:
|
||||||
|
@ -369,7 +369,7 @@ class MatematDatabase(object):
|
||||||
with self.db.transaction(exclusive=False) as c:
|
with self.db.transaction(exclusive=False) as c:
|
||||||
# Fetch all values to construct the product
|
# Fetch all values to construct the product
|
||||||
c.execute('''
|
c.execute('''
|
||||||
SELECT product_id, name, price_member, price_non_member, stock, stockable
|
SELECT product_id, name, price_member, price_non_member, custom_price, stock, stockable
|
||||||
FROM products
|
FROM products
|
||||||
WHERE product_id = ?''',
|
WHERE product_id = ?''',
|
||||||
[pid])
|
[pid])
|
||||||
|
@ -377,15 +377,17 @@ class MatematDatabase(object):
|
||||||
if row is None:
|
if row is None:
|
||||||
raise ValueError(f'No product with product ID {pid} exists.')
|
raise ValueError(f'No product with product ID {pid} exists.')
|
||||||
# Unpack the row and construct the product
|
# Unpack the row and construct the product
|
||||||
product_id, name, price_member, price_non_member, stock, stockable = row
|
product_id, name, price_member, price_non_member, custom_price, stock, stockable = row
|
||||||
return Product(product_id, name, price_member, price_non_member, stockable, stock)
|
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, stockable: bool) -> Product:
|
def create_product(self, name: str, price_member: int, price_non_member: int, custom_price:
|
||||||
|
bool, stockable: bool) -> Product:
|
||||||
"""
|
"""
|
||||||
Creates a new product.
|
Creates a new product.
|
||||||
:param name: Name of the product.
|
:param name: Name of the product.
|
||||||
:param price_member: Price of the product for members.
|
:param price_member: Price of the product for members.
|
||||||
:param price_non_member: Price of the product for non-members.
|
: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 stockable: True if the product should be stockable, false otherwise.
|
||||||
:return: A Product object representing the created product.
|
:return: A Product object representing the created product.
|
||||||
:raises ValueError: If a product with the same name already exists.
|
:raises ValueError: If a product with the same name already exists.
|
||||||
|
@ -396,17 +398,18 @@ class MatematDatabase(object):
|
||||||
if c.fetchone() is not None:
|
if c.fetchone() is not None:
|
||||||
raise ValueError(f'A product with the name \'{name}\' already exists.')
|
raise ValueError(f'A product with the name \'{name}\' already exists.')
|
||||||
c.execute('''
|
c.execute('''
|
||||||
INSERT INTO products (name, price_member, price_non_member, stock, stockable)
|
INSERT INTO products (name, price_member, price_non_member, custom_price, stock, stockable)
|
||||||
VALUES (:name, :price_member, :price_non_member, 0, :stockable)
|
VALUES (:name, :price_member, :price_non_member, :custom_price, 0, :stockable)
|
||||||
''', {
|
''', {
|
||||||
'name': name,
|
'name': name,
|
||||||
'price_member': price_member,
|
'price_member': price_member,
|
||||||
'price_non_member': price_non_member,
|
'price_non_member': price_non_member,
|
||||||
|
'custom_price': custom_price,
|
||||||
'stockable': stockable
|
'stockable': stockable
|
||||||
})
|
})
|
||||||
c.execute('SELECT last_insert_rowid()')
|
c.execute('SELECT last_insert_rowid()')
|
||||||
product_id = int(c.fetchone()[0])
|
product_id = int(c.fetchone()[0])
|
||||||
return Product(product_id, name, price_member, price_non_member, stockable, 0)
|
return Product(product_id, name, price_member, price_non_member, custom_price, stockable, 0)
|
||||||
|
|
||||||
def change_product(self, product: Product, **kwargs) -> None:
|
def change_product(self, product: Product, **kwargs) -> None:
|
||||||
"""
|
"""
|
||||||
|
@ -422,6 +425,7 @@ class MatematDatabase(object):
|
||||||
name: str = kwargs['name'] if 'name' in kwargs else product.name
|
name: str = kwargs['name'] if 'name' in kwargs else product.name
|
||||||
price_member: int = kwargs['price_member'] if 'price_member' in kwargs else product.price_member
|
price_member: int = kwargs['price_member'] if 'price_member' in kwargs else product.price_member
|
||||||
price_non_member: int = kwargs['price_non_member'] if 'price_non_member' in kwargs else product.price_non_member
|
price_non_member: int = kwargs['price_non_member'] if 'price_non_member' in kwargs else product.price_non_member
|
||||||
|
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
|
stock: int = kwargs['stock'] if 'stock' in kwargs else product.stock
|
||||||
stockable: bool = kwargs['stockable'] if 'stockable' in kwargs else product.stockable
|
stockable: bool = kwargs['stockable'] if 'stockable' in kwargs else product.stockable
|
||||||
with self.db.transaction() as c:
|
with self.db.transaction() as c:
|
||||||
|
@ -431,6 +435,7 @@ class MatematDatabase(object):
|
||||||
name = :name,
|
name = :name,
|
||||||
price_member = :price_member,
|
price_member = :price_member,
|
||||||
price_non_member = :price_non_member,
|
price_non_member = :price_non_member,
|
||||||
|
custom_price = :custom_price,
|
||||||
stock = :stock,
|
stock = :stock,
|
||||||
stockable = :stockable
|
stockable = :stockable
|
||||||
WHERE product_id = :product_id
|
WHERE product_id = :product_id
|
||||||
|
@ -439,6 +444,7 @@ class MatematDatabase(object):
|
||||||
'name': name,
|
'name': name,
|
||||||
'price_member': price_member,
|
'price_member': price_member,
|
||||||
'price_non_member': price_non_member,
|
'price_non_member': price_non_member,
|
||||||
|
'custom_price': custom_price,
|
||||||
'stock': stock,
|
'stock': stock,
|
||||||
'stockable': stockable
|
'stockable': stockable
|
||||||
})
|
})
|
||||||
|
@ -450,6 +456,7 @@ class MatematDatabase(object):
|
||||||
product.name = name
|
product.name = name
|
||||||
product.price_member = price_member
|
product.price_member = price_member
|
||||||
product.price_non_member = price_non_member
|
product.price_non_member = price_non_member
|
||||||
|
product.custom_price = custom_price
|
||||||
product.stock = stock
|
product.stock = stock
|
||||||
product.stockable = stockable
|
product.stockable = stockable
|
||||||
|
|
||||||
|
@ -469,15 +476,18 @@ class MatematDatabase(object):
|
||||||
raise DatabaseConsistencyError(
|
raise DatabaseConsistencyError(
|
||||||
f'delete_product should affect 1 products row, but affected {affected}')
|
f'delete_product should affect 1 products row, but affected {affected}')
|
||||||
|
|
||||||
def increment_consumption(self, user: User, product: Product) -> None:
|
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.
|
Decrement the user's balance by the price of the product and create an entry in the statistics table.
|
||||||
|
|
||||||
:param user: The user buying a product.
|
:param user: The user buying a product.
|
||||||
:param product: The product the user is buying.
|
:param product: The product the user is buying.
|
||||||
|
:param custom_price: The custom price chosen by the user.
|
||||||
:raises DatabaseConsistencyError: If the user or the product does not exist in the database.
|
:raises DatabaseConsistencyError: If the user or the product does not exist in the database.
|
||||||
"""
|
"""
|
||||||
price: int = product.price_member if user.is_member else product.price_non_member
|
price: int = product.price_member if user.is_member else product.price_non_member
|
||||||
|
if product.custom_price and custom_price is not None:
|
||||||
|
price = max(price, custom_price)
|
||||||
with self.db.transaction() as c:
|
with self.db.transaction() as c:
|
||||||
c.execute('''
|
c.execute('''
|
||||||
INSERT INTO transactions (user_id, value, old_balance)
|
INSERT INTO transactions (user_id, value, old_balance)
|
||||||
|
|
|
@ -268,3 +268,11 @@ def migrate_schema_4_to_5(c: sqlite3.Cursor):
|
||||||
INSERT INTO products SELECT product_id, name, stock, 1, price_member, price_non_member FROM products_temp
|
INSERT INTO products SELECT product_id, name, stock, 1, price_member, price_non_member FROM products_temp
|
||||||
''')
|
''')
|
||||||
c.execute('DROP TABLE products_temp')
|
c.execute('DROP TABLE products_temp')
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_schema_5_to_6(c: sqlite3.Cursor):
|
||||||
|
# Add custom_price column
|
||||||
|
c.execute('''
|
||||||
|
ALTER TABLE products ADD COLUMN
|
||||||
|
custom_price INTEGER(1) DEFAULT 0;
|
||||||
|
''')
|
||||||
|
|
|
@ -8,17 +8,19 @@ class Product:
|
||||||
:param name: The product's name.
|
:param name: The product's name.
|
||||||
:param price_member: The price of a unit of this product for users marked as "members".
|
:param price_member: The price of a unit of this product for users marked as "members".
|
||||||
:param price_non_member: The price of a unit of this product for users NOT marked as "members".
|
:param price_non_member: The price of a unit of this product for users NOT marked as "members".
|
||||||
|
: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 stock: The number of items of this product currently in stock, or None if not stockable.
|
||||||
:param stockable: Whether this product is stockable.
|
:param stockable: Whether this product is stockable.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, _id: int, name: str,
|
def __init__(self, _id: int, name: str,
|
||||||
price_member: int, price_non_member: int,
|
price_member: int, price_non_member: int, custom_price: bool,
|
||||||
stockable: bool, stock: int) -> None:
|
stockable: bool, stock: int) -> None:
|
||||||
self.id: int = _id
|
self.id: int = _id
|
||||||
self.name: str = name
|
self.name: str = name
|
||||||
self.price_member: int = price_member
|
self.price_member: int = price_member
|
||||||
self.price_non_member: int = price_non_member
|
self.price_non_member: int = price_non_member
|
||||||
|
self.custom_price: bool = custom_price
|
||||||
self.stock: int = stock
|
self.stock: int = stock
|
||||||
self.stockable: bool = stockable
|
self.stockable: bool = stockable
|
||||||
|
|
||||||
|
@ -29,8 +31,10 @@ class Product:
|
||||||
self.name == other.name and \
|
self.name == other.name and \
|
||||||
self.price_member == other.price_member and \
|
self.price_member == other.price_member and \
|
||||||
self.price_non_member == other.price_non_member and \
|
self.price_non_member == other.price_non_member and \
|
||||||
|
self.custom_price == other.custom_price and \
|
||||||
self.stock == other.stock and \
|
self.stock == other.stock and \
|
||||||
self.stockable == other.stockable
|
self.stockable == other.stockable
|
||||||
|
|
||||||
def __hash__(self) -> int:
|
def __hash__(self) -> int:
|
||||||
return hash((self.id, self.name, self.price_member, self.price_non_member, self.stock, self.stockable))
|
return hash((self.id, self.name, self.price_member, self.price_non_member, self.custom_price,
|
||||||
|
self.stock, self.stockable))
|
||||||
|
|
|
@ -333,3 +333,83 @@ SCHEMAS[5] = [
|
||||||
ON DELETE SET NULL ON UPDATE CASCADE
|
ON DELETE SET NULL ON UPDATE CASCADE
|
||||||
);
|
);
|
||||||
''']
|
''']
|
||||||
|
|
||||||
|
|
||||||
|
SCHEMAS[6] = [
|
||||||
|
'''
|
||||||
|
CREATE TABLE users (
|
||||||
|
user_id INTEGER PRIMARY KEY,
|
||||||
|
username TEXT UNIQUE NOT NULL,
|
||||||
|
email TEXT DEFAULT NULL,
|
||||||
|
password TEXT NOT NULL,
|
||||||
|
touchkey TEXT DEFAULT NULL,
|
||||||
|
is_admin INTEGER(1) NOT NULL DEFAULT 0,
|
||||||
|
is_member INTEGER(1) NOT NULL DEFAULT 1,
|
||||||
|
balance INTEGER(8) NOT NULL DEFAULT 0,
|
||||||
|
lastchange INTEGER(8) NOT NULL DEFAULT 0,
|
||||||
|
receipt_pref INTEGER(1) NOT NULL DEFAULT 0,
|
||||||
|
created INTEGER(8) NOT NULL DEFAULT 0
|
||||||
|
);
|
||||||
|
''',
|
||||||
|
'''
|
||||||
|
CREATE TABLE products (
|
||||||
|
product_id INTEGER PRIMARY KEY,
|
||||||
|
name TEXT UNIQUE NOT NULL,
|
||||||
|
stock INTEGER(8) DEFAULT 0,
|
||||||
|
stockable INTEGER(1) DEFAULT 1,
|
||||||
|
price_member INTEGER(8) NOT NULL,
|
||||||
|
price_non_member INTEGER(8) NOT NULL,
|
||||||
|
custom_price INTEGER(1) DEFAULT 0
|
||||||
|
);
|
||||||
|
''',
|
||||||
|
'''
|
||||||
|
CREATE TABLE transactions ( -- "superclass" of the following 3 tables
|
||||||
|
ta_id INTEGER PRIMARY KEY,
|
||||||
|
user_id INTEGER DEFAULT NULL,
|
||||||
|
value INTEGER(8) NOT NULL,
|
||||||
|
old_balance INTEGER(8) NOT NULL,
|
||||||
|
date INTEGER(8) DEFAULT (STRFTIME('%s', 'now')),
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(user_id)
|
||||||
|
ON DELETE SET NULL ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
''',
|
||||||
|
'''
|
||||||
|
CREATE TABLE consumptions ( -- transactions involving buying a product
|
||||||
|
ta_id INTEGER PRIMARY KEY,
|
||||||
|
product TEXT NOT NULL,
|
||||||
|
FOREIGN KEY (ta_id) REFERENCES transactions(ta_id)
|
||||||
|
ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
''',
|
||||||
|
'''
|
||||||
|
CREATE TABLE deposits ( -- transactions involving depositing cash
|
||||||
|
ta_id INTEGER PRIMARY KEY,
|
||||||
|
FOREIGN KEY (ta_id) REFERENCES transactions(ta_id)
|
||||||
|
ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
''',
|
||||||
|
'''
|
||||||
|
CREATE TABLE modifications ( -- transactions involving balance modification by an admin
|
||||||
|
ta_id INTEGER NOT NULL,
|
||||||
|
agent TEXT NOT NULL,
|
||||||
|
reason TEXT DEFAULT NULL,
|
||||||
|
PRIMARY KEY (ta_id),
|
||||||
|
FOREIGN KEY (ta_id) REFERENCES transactions(ta_id)
|
||||||
|
ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
''',
|
||||||
|
'''
|
||||||
|
CREATE TABLE receipts ( -- receipts sent to the users
|
||||||
|
receipt_id INTEGER PRIMARY KEY,
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
first_ta_id INTEGER DEFAULT NULL,
|
||||||
|
last_ta_id INTEGER DEFAULT NULL,
|
||||||
|
date INTEGER(8) DEFAULT (STRFTIME('%s', 'now')),
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(user_id)
|
||||||
|
ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
|
FOREIGN KEY (first_ta_id) REFERENCES transactions(ta_id)
|
||||||
|
ON DELETE SET NULL ON UPDATE CASCADE,
|
||||||
|
FOREIGN KEY (last_ta_id) REFERENCES transactions(ta_id)
|
||||||
|
ON DELETE SET NULL ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
''']
|
||||||
|
|
|
@ -220,7 +220,7 @@ class DatabaseTest(unittest.TestCase):
|
||||||
def test_create_product(self) -> None:
|
def test_create_product(self) -> None:
|
||||||
with self.db as db:
|
with self.db as db:
|
||||||
with db.transaction() as c:
|
with db.transaction() as c:
|
||||||
db.create_product('Club Mate', 200, 200, True)
|
db.create_product('Club Mate', 200, 200, True, True)
|
||||||
c.execute("SELECT * FROM products")
|
c.execute("SELECT * FROM products")
|
||||||
row = c.fetchone()
|
row = c.fetchone()
|
||||||
self.assertEqual('Club Mate', row[1])
|
self.assertEqual('Club Mate', row[1])
|
||||||
|
@ -228,13 +228,14 @@ class DatabaseTest(unittest.TestCase):
|
||||||
self.assertEqual(1, row[3])
|
self.assertEqual(1, row[3])
|
||||||
self.assertEqual(200, row[4])
|
self.assertEqual(200, row[4])
|
||||||
self.assertEqual(200, row[5])
|
self.assertEqual(200, row[5])
|
||||||
|
self.assertEqual(1, row[6])
|
||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(ValueError):
|
||||||
db.create_product('Club Mate', 250, 250, False)
|
db.create_product('Club Mate', 250, 250, False, False)
|
||||||
|
|
||||||
def test_get_product(self) -> None:
|
def test_get_product(self) -> None:
|
||||||
with self.db as db:
|
with self.db as db:
|
||||||
with db.transaction(exclusive=False):
|
with db.transaction(exclusive=False):
|
||||||
created = db.create_product('Club Mate', 150, 250, False)
|
created = db.create_product('Club Mate', 150, 250, False, False)
|
||||||
product = db.get_product(created.id)
|
product = db.get_product(created.id)
|
||||||
self.assertEqual('Club Mate', product.name)
|
self.assertEqual('Club Mate', product.name)
|
||||||
self.assertEqual(150, product.price_member)
|
self.assertEqual(150, product.price_member)
|
||||||
|
@ -248,9 +249,9 @@ class DatabaseTest(unittest.TestCase):
|
||||||
# Test empty list
|
# Test empty list
|
||||||
products = db.list_products()
|
products = db.list_products()
|
||||||
self.assertEqual(0, len(products))
|
self.assertEqual(0, len(products))
|
||||||
db.create_product('Club Mate', 200, 200, True)
|
db.create_product('Club Mate', 200, 200, False, True)
|
||||||
db.create_product('Flora Power Mate', 200, 200, False)
|
db.create_product('Flora Power Mate', 200, 200, False, False)
|
||||||
db.create_product('Fritz Mate', 200, 250, True)
|
db.create_product('Fritz Mate', 200, 250, False, True)
|
||||||
products = db.list_products()
|
products = db.list_products()
|
||||||
self.assertEqual(3, len(products))
|
self.assertEqual(3, len(products))
|
||||||
productcheck = {}
|
productcheck = {}
|
||||||
|
@ -272,13 +273,14 @@ class DatabaseTest(unittest.TestCase):
|
||||||
|
|
||||||
def test_change_product(self) -> None:
|
def test_change_product(self) -> None:
|
||||||
with self.db as db:
|
with self.db as db:
|
||||||
product = db.create_product('Club Mate', 200, 200, True)
|
product = db.create_product('Club Mate', 200, 200, False, True)
|
||||||
db.change_product(product, name='Flora Power Mate', price_member=150, price_non_member=250,
|
db.change_product(product, name='Flora Power Mate', price_member=150, price_non_member=250,
|
||||||
stock=None, stockable=False)
|
custom_price=True, stock=None, stockable=False)
|
||||||
# Changes must be reflected in the passed object
|
# Changes must be reflected in the passed object
|
||||||
self.assertEqual('Flora Power Mate', product.name)
|
self.assertEqual('Flora Power Mate', product.name)
|
||||||
self.assertEqual(150, product.price_member)
|
self.assertEqual(150, product.price_member)
|
||||||
self.assertEqual(250, product.price_non_member)
|
self.assertEqual(250, product.price_non_member)
|
||||||
|
self.assertEqual(True, product.custom_price)
|
||||||
self.assertEqual(None, product.stock)
|
self.assertEqual(None, product.stock)
|
||||||
self.assertEqual(False, product.stockable)
|
self.assertEqual(False, product.stockable)
|
||||||
# Changes must be reflected in the database
|
# Changes must be reflected in the database
|
||||||
|
@ -286,11 +288,13 @@ class DatabaseTest(unittest.TestCase):
|
||||||
self.assertEqual('Flora Power Mate', checkproduct.name)
|
self.assertEqual('Flora Power Mate', checkproduct.name)
|
||||||
self.assertEqual(150, checkproduct.price_member)
|
self.assertEqual(150, checkproduct.price_member)
|
||||||
self.assertEqual(250, checkproduct.price_non_member)
|
self.assertEqual(250, checkproduct.price_non_member)
|
||||||
|
self.assertEqual(True, checkproduct.custom_price)
|
||||||
self.assertEqual(None, checkproduct.stock)
|
self.assertEqual(None, checkproduct.stock)
|
||||||
|
self.assertEqual(False, checkproduct.stockable)
|
||||||
product.id = -1
|
product.id = -1
|
||||||
with self.assertRaises(DatabaseConsistencyError):
|
with self.assertRaises(DatabaseConsistencyError):
|
||||||
db.change_product(product)
|
db.change_product(product)
|
||||||
product2 = db.create_product('Club Mate', 200, 200, True)
|
product2 = db.create_product('Club Mate', 200, 200, False, True)
|
||||||
product2.name = 'Flora Power Mate'
|
product2.name = 'Flora Power Mate'
|
||||||
with self.assertRaises(DatabaseConsistencyError):
|
with self.assertRaises(DatabaseConsistencyError):
|
||||||
# Should fail, as a product with the same name already exists.
|
# Should fail, as a product with the same name already exists.
|
||||||
|
@ -298,8 +302,8 @@ class DatabaseTest(unittest.TestCase):
|
||||||
|
|
||||||
def test_delete_product(self) -> None:
|
def test_delete_product(self) -> None:
|
||||||
with self.db as db:
|
with self.db as db:
|
||||||
product = db.create_product('Club Mate', 200, 200, True)
|
product = db.create_product('Club Mate', 200, 200, False, True)
|
||||||
product2 = db.create_product('Flora Power Mate', 200, 200, False)
|
product2 = db.create_product('Flora Power Mate', 200, 200, False, False)
|
||||||
|
|
||||||
self.assertEqual(2, len(db.list_products()))
|
self.assertEqual(2, len(db.list_products()))
|
||||||
db.delete_product(product)
|
db.delete_product(product)
|
||||||
|
@ -344,9 +348,9 @@ class DatabaseTest(unittest.TestCase):
|
||||||
db.deposit(user1, 1337)
|
db.deposit(user1, 1337)
|
||||||
db.deposit(user2, 4242)
|
db.deposit(user2, 4242)
|
||||||
db.deposit(user3, 1234)
|
db.deposit(user3, 1234)
|
||||||
clubmate = db.create_product('Club Mate', 200, 200, True)
|
clubmate = db.create_product('Club Mate', 200, 200, False, True)
|
||||||
florapowermate = db.create_product('Flora Power Mate', 150, 250, True)
|
florapowermate = db.create_product('Flora Power Mate', 150, 250, False, True)
|
||||||
fritzmate = db.create_product('Fritz Mate', 200, 200, True)
|
fritzmate = db.create_product('Fritz Mate', 200, 200, False, True)
|
||||||
|
|
||||||
# user1 is somewhat addicted to caffeine
|
# user1 is somewhat addicted to caffeine
|
||||||
for _ in range(3):
|
for _ in range(3):
|
||||||
|
@ -472,7 +476,7 @@ class DatabaseTest(unittest.TestCase):
|
||||||
|
|
||||||
admin: User = db.create_user('admin', 'supersecurepassword', 'admin@example.com', True, True)
|
admin: User = db.create_user('admin', 'supersecurepassword', 'admin@example.com', True, True)
|
||||||
user: User = db.create_user('user', 'supersecurepassword', 'user@example.com', True, True)
|
user: User = db.create_user('user', 'supersecurepassword', 'user@example.com', True, True)
|
||||||
product: Product = db.create_product('Flora Power Mate', 200, 200, True)
|
product: Product = db.create_product('Flora Power Mate', 200, 200, False, True)
|
||||||
|
|
||||||
# Create some transactions
|
# Create some transactions
|
||||||
db.change_user(user, agent=admin,
|
db.change_user(user, agent=admin,
|
||||||
|
@ -561,8 +565,8 @@ class DatabaseTest(unittest.TestCase):
|
||||||
user2: User = db.create_user('user2', 'supersecurepassword', 'user2@example.com', True, False)
|
user2: User = db.create_user('user2', 'supersecurepassword', 'user2@example.com', True, False)
|
||||||
user3: User = db.create_user('user3', 'supersecurepassword', 'user3@example.com', True, False)
|
user3: User = db.create_user('user3', 'supersecurepassword', 'user3@example.com', True, False)
|
||||||
user4: User = db.create_user('user4', 'supersecurepassword', 'user4@example.com', True, False)
|
user4: User = db.create_user('user4', 'supersecurepassword', 'user4@example.com', True, False)
|
||||||
flora: Product = db.create_product('Flora Power Mate', 200, 200, True)
|
flora: Product = db.create_product('Flora Power Mate', 200, 200, False, True)
|
||||||
club: Product = db.create_product('Club Mate', 200, 200, False)
|
club: Product = db.create_product('Club Mate', 200, 200, False, False)
|
||||||
|
|
||||||
# Create some transactions
|
# Create some transactions
|
||||||
db.deposit(user1, 1337)
|
db.deposit(user1, 1337)
|
||||||
|
|
|
@ -40,7 +40,7 @@ class DatabaseTransaction(object):
|
||||||
|
|
||||||
class DatabaseWrapper(object):
|
class DatabaseWrapper(object):
|
||||||
|
|
||||||
SCHEMA_VERSION = 5
|
SCHEMA_VERSION = 6
|
||||||
|
|
||||||
def __init__(self, filename: str) -> None:
|
def __init__(self, filename: str) -> None:
|
||||||
self._filename: str = filename
|
self._filename: str = filename
|
||||||
|
@ -87,6 +87,8 @@ class DatabaseWrapper(object):
|
||||||
migrate_schema_3_to_4(c)
|
migrate_schema_3_to_4(c)
|
||||||
if from_version <= 4 and to_version >= 5:
|
if from_version <= 4 and to_version >= 5:
|
||||||
migrate_schema_4_to_5(c)
|
migrate_schema_4_to_5(c)
|
||||||
|
if from_version <= 5 and to_version >= 6:
|
||||||
|
migrate_schema_5_to_6(c)
|
||||||
|
|
||||||
def connect(self) -> None:
|
def connect(self) -> None:
|
||||||
if self.is_connected():
|
if self.is_connected():
|
||||||
|
|
|
@ -4,5 +4,4 @@ from matemat.interfacing.dispenser import Dispenser
|
||||||
class NullDispenser(Dispenser):
|
class NullDispenser(Dispenser):
|
||||||
|
|
||||||
def dispense(self, product, amount):
|
def dispense(self, product, amount):
|
||||||
# no-op
|
return True
|
||||||
pass
|
|
||||||
|
|
|
@ -198,9 +198,10 @@ def handle_admin_change(args: FormsDict, files: FormsDict, db: MatematDatabase):
|
||||||
name = str(args.name)
|
name = str(args.name)
|
||||||
price_member = parse_chf(str(args.pricemember))
|
price_member = parse_chf(str(args.pricemember))
|
||||||
price_non_member = parse_chf(str(args.pricenonmember))
|
price_non_member = parse_chf(str(args.pricenonmember))
|
||||||
|
custom_price = 'custom_price' in args
|
||||||
stockable = 'stockable' in args
|
stockable = 'stockable' in args
|
||||||
# Create the user in the database
|
# Create the user in the database
|
||||||
newproduct = db.create_product(name, price_member, price_non_member, stockable)
|
newproduct = db.create_product(name, price_member, price_non_member, custom_price, stockable)
|
||||||
# If a new product image was uploaded, process it
|
# If a new product image was uploaded, process it
|
||||||
image = files.image.file.read() if 'image' in files else None
|
image = files.image.file.read() if 'image' in files else None
|
||||||
if image is not None and len(image) > 0:
|
if image is not None and len(image) > 0:
|
||||||
|
|
|
@ -26,8 +26,11 @@ def buy():
|
||||||
pid = int(str(request.params.pid))
|
pid = int(str(request.params.pid))
|
||||||
product = db.get_product(pid)
|
product = db.get_product(pid)
|
||||||
if c.get_dispenser().dispense(product, 1):
|
if c.get_dispenser().dispense(product, 1):
|
||||||
|
price = None
|
||||||
|
if 'price' in request.params:
|
||||||
|
price = int(str(request.params.price))
|
||||||
# Create a consumption entry for the (user, product) combination
|
# Create a consumption entry for the (user, product) combination
|
||||||
db.increment_consumption(user, product)
|
db.increment_consumption(user, product, price)
|
||||||
stock_provider = c.get_stock_provider()
|
stock_provider = c.get_stock_provider()
|
||||||
if stock_provider.needs_update():
|
if stock_provider.needs_update():
|
||||||
stock_provider.update_stock(product, -1)
|
stock_provider.update_stock(product, -1)
|
||||||
|
|
|
@ -93,13 +93,14 @@ def handle_change(args: FormsDict, files: FormsDict, product: Product, db: Matem
|
||||||
name = str(args.name)
|
name = str(args.name)
|
||||||
price_member = parse_chf(str(args.pricemember))
|
price_member = parse_chf(str(args.pricemember))
|
||||||
price_non_member = parse_chf(str(args.pricenonmember))
|
price_non_member = parse_chf(str(args.pricenonmember))
|
||||||
|
custom_price = 'custom_price' in args
|
||||||
stock = int(str(args.stock))
|
stock = int(str(args.stock))
|
||||||
stockable = 'stockable' in args
|
stockable = 'stockable' in args
|
||||||
# Attempt to write the changes to the database
|
# Attempt to write the changes to the database
|
||||||
try:
|
try:
|
||||||
db.change_product(product,
|
db.change_product(product,
|
||||||
name=name, price_member=price_member, price_non_member=price_non_member,
|
name=name, price_member=price_member, price_non_member=price_non_member,
|
||||||
stock=stock, stockable=stockable)
|
custom_price=custom_price, stock=stock, stockable=stockable)
|
||||||
stock_provider = get_stock_provider()
|
stock_provider = get_stock_provider()
|
||||||
if stock_provider.needs_update() and product.stockable:
|
if stock_provider.needs_update() and product.stockable:
|
||||||
stock_provider.set_stock(product, stock)
|
stock_provider.set_stock(product, stock)
|
||||||
|
|
|
@ -1,30 +0,0 @@
|
||||||
|
|
||||||
# Maintainer: s3lph <account-gitlab-ideynizv@kernelpanic.lol>
|
|
||||||
|
|
||||||
pkgname=matemat
|
|
||||||
pkgver=0.2.4
|
|
||||||
pkgrel=1
|
|
||||||
arch=('any')
|
|
||||||
|
|
||||||
pkgdesc='A soda machine stock-keeping webservice'
|
|
||||||
url='https://gitlab.com/s3lph/matemat'
|
|
||||||
licence=('MIT')
|
|
||||||
|
|
||||||
depends=(
|
|
||||||
'python'
|
|
||||||
'python-bottle'
|
|
||||||
'python-jinja'
|
|
||||||
'python-pillow'
|
|
||||||
'python-magic'
|
|
||||||
'file'
|
|
||||||
)
|
|
||||||
|
|
||||||
backup=(
|
|
||||||
'etc/matemat.conf'
|
|
||||||
)
|
|
||||||
|
|
||||||
install=$pkgname.install
|
|
||||||
|
|
||||||
package() {
|
|
||||||
cp -r ../matemat/* ../pkg/matemat/
|
|
||||||
}
|
|
|
@ -1,31 +0,0 @@
|
||||||
|
|
||||||
post_install() {
|
|
||||||
|
|
||||||
if ! getent group matemat >/dev/null; then
|
|
||||||
groupadd --system matemat
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! getent passwd matemat >/dev/null; then
|
|
||||||
useradd --system --create-home --gid matemat --home-dir /var/lib/matemat --shell /usr/bin/nologin matemat
|
|
||||||
fi
|
|
||||||
|
|
||||||
chown matemat:matemat /var/lib/matemat
|
|
||||||
chmod 0750 /var/lib/matemat
|
|
||||||
ln -sf /var/lib/matemat/upload /usr/lib/matemat/static/upload
|
|
||||||
|
|
||||||
systemctl daemon-reload
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
pre_remove() {
|
|
||||||
|
|
||||||
systemctl stop matemat.service
|
|
||||||
userdel matemat
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
post_remove() {
|
|
||||||
|
|
||||||
systemctl daemon-reload
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,32 +0,0 @@
|
||||||
[Matemat]
|
|
||||||
|
|
||||||
# The IP address to listen on
|
|
||||||
Address=::
|
|
||||||
# The TCP port to listen on
|
|
||||||
Port=80
|
|
||||||
|
|
||||||
# The log level, one of NONE, DEBUG, INFO, WARNING, ERROR, CRITICAL
|
|
||||||
LogLevel=DEBUG
|
|
||||||
|
|
||||||
[Pagelets]
|
|
||||||
|
|
||||||
# Name of the Matemat instance, shown in the web app
|
|
||||||
InstanceName=Matemat
|
|
||||||
|
|
||||||
#
|
|
||||||
# Configure SMTP credentials
|
|
||||||
#
|
|
||||||
# SmtpFrom=matemat@example.com
|
|
||||||
# SmtpSubj=Matemat Receipt
|
|
||||||
# SmtpHost=exmaple.com
|
|
||||||
# SmtpPort=587
|
|
||||||
# SmtpUser=matemat@example.com
|
|
||||||
# SmtpPass=supersecurepassword
|
|
||||||
|
|
||||||
#
|
|
||||||
# Enable to allow users to receive receipts via email
|
|
||||||
#
|
|
||||||
# SmtpSendReceipts=1
|
|
||||||
|
|
||||||
# Add static HTTP headers in this section
|
|
||||||
# [HttpHeaders]
|
|
|
@ -1,11 +0,0 @@
|
||||||
[Matemat]
|
|
||||||
|
|
||||||
StaticPath=/usr/lib/matemat/static
|
|
||||||
TemplatePath=/usr/lib/matemat/templates
|
|
||||||
|
|
||||||
LogTarget=stdout
|
|
||||||
|
|
||||||
[Pagelets]
|
|
||||||
|
|
||||||
UploadDir=/var/lib/matemat/upload
|
|
||||||
DatabaseFile=/var/lib/matemat/matemat.db
|
|
|
@ -1,12 +0,0 @@
|
||||||
[Unit]
|
|
||||||
Description=matemat
|
|
||||||
After=networking.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
ExecStart=/usr/bin/python -m matemat /etc/matemat.conf /usr/lib/matemat/matemat.conf
|
|
||||||
User=matemat
|
|
||||||
AmbientCapabilities=CAP_NET_BIND_SERVICE
|
|
||||||
NoNewPrivileges=true
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
|
@ -1,5 +1,5 @@
|
||||||
Package: matemat
|
Package: matemat
|
||||||
Version: 0.2.7
|
Version: 0.2.8
|
||||||
Maintainer: s3lph <account-gitlab-ideynizv@kernelpanic.lol>
|
Maintainer: s3lph <account-gitlab-ideynizv@kernelpanic.lol>
|
||||||
Section: web
|
Section: web
|
||||||
Priority: optional
|
Priority: optional
|
||||||
|
|
|
@ -84,14 +84,6 @@ def fetch_debian_url(base_url: str, job_ids: Dict[str, str]) -> Optional[Tuple[s
|
||||||
return debian_url, debian_sha_url
|
return debian_url, debian_sha_url
|
||||||
|
|
||||||
|
|
||||||
def fetch_arch_url(base_url: str, job_ids: Dict[str, str]) -> Optional[Tuple[str, str]]:
|
|
||||||
mybase: str = f'{base_url}/jobs/{job_ids["build_archlinux"]}/artifacts/raw'
|
|
||||||
arch_sha_url: str = f'{mybase}/package/archlinux/SHA256SUMS'
|
|
||||||
arch_filename: str = fetch_single_shafile(arch_sha_url)
|
|
||||||
arch_url: str = f'{mybase}/package/archlinux/{arch_filename}'
|
|
||||||
return arch_url, arch_sha_url
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
api_token: Optional[str] = os.getenv('GITLAB_API_TOKEN')
|
api_token: Optional[str] = os.getenv('GITLAB_API_TOKEN')
|
||||||
release_tag: Optional[str] = os.getenv('CI_COMMIT_TAG')
|
release_tag: Optional[str] = os.getenv('CI_COMMIT_TAG')
|
||||||
|
@ -125,7 +117,6 @@ def main():
|
||||||
|
|
||||||
wheel_url, wheel_sha_url = fetch_wheel_url(base_url, job_ids)
|
wheel_url, wheel_sha_url = fetch_wheel_url(base_url, job_ids)
|
||||||
debian_url, debian_sha_url = fetch_debian_url(base_url, job_ids)
|
debian_url, debian_sha_url = fetch_debian_url(base_url, job_ids)
|
||||||
arch_url, arch_sha_url = fetch_arch_url(base_url, job_ids)
|
|
||||||
|
|
||||||
augmented_changelog = f'''{changelog.strip()}
|
augmented_changelog = f'''{changelog.strip()}
|
||||||
|
|
||||||
|
@ -133,7 +124,6 @@ def main():
|
||||||
|
|
||||||
- [Python Wheel]({wheel_url}) ([sha256]({wheel_sha_url}))
|
- [Python Wheel]({wheel_url}) ([sha256]({wheel_sha_url}))
|
||||||
- [Debian Package]({debian_url}) ([sha256]({debian_sha_url}))
|
- [Debian Package]({debian_url}) ([sha256]({debian_sha_url}))
|
||||||
- [Arch Linux Package]({arch_url}) ([sha256]({arch_sha_url}))
|
|
||||||
- Docker image: registry.gitlab.com/{project_name}:{release_tag}'''
|
- Docker image: registry.gitlab.com/{project_name}:{release_tag}'''
|
||||||
|
|
||||||
post_body: str = json.dumps({'description': augmented_changelog})
|
post_body: str = json.dumps({'description': augmented_changelog})
|
||||||
|
|
|
@ -91,18 +91,32 @@
|
||||||
color: blue;
|
color: blue;
|
||||||
}
|
}
|
||||||
|
|
||||||
#deposit-amount {
|
#deposit-output {
|
||||||
grid-column-start: 1;
|
grid-column-start: 1;
|
||||||
grid-column-end: 4;
|
grid-column-end: 4;
|
||||||
text-align: right;
|
|
||||||
grid-row: 1;
|
grid-row: 1;
|
||||||
font-size: 50px;
|
|
||||||
line-heigt: 100px;
|
|
||||||
padding: 10px 0;
|
|
||||||
font-family: monospace;
|
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#deposit-title {
|
||||||
|
display: block;
|
||||||
|
font-family: sans-serif;
|
||||||
|
padding: 0 10px;
|
||||||
|
line-height: 30px;
|
||||||
|
font-size: 25px;
|
||||||
|
height: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#deposit-amount {
|
||||||
|
display: block;
|
||||||
|
text-align: right;
|
||||||
|
font-size: 50px;
|
||||||
|
line-height: 70px;
|
||||||
|
height: 70px;
|
||||||
|
padding: 0 10px;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
.numpad {
|
.numpad {
|
||||||
background: #f0f0f0;
|
background: #f0f0f0;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|
|
@ -4,25 +4,42 @@ Number.prototype.pad = function(size) {
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let product_id = null;
|
||||||
let deposit = '0';
|
let deposit = '0';
|
||||||
let button = document.createElement('div');
|
let button = document.createElement('div');
|
||||||
let input = document.getElementById('deposit-wrapper');
|
let input = document.getElementById('deposit-wrapper');
|
||||||
let amount = document.getElementById('deposit-amount');
|
let amount = document.getElementById('deposit-amount');
|
||||||
|
let title = document.getElementById('deposit-title');
|
||||||
button.classList.add('thumblist-item');
|
button.classList.add('thumblist-item');
|
||||||
button.classList.add('fakelink');
|
button.classList.add('fakelink');
|
||||||
button.innerText = 'Deposit';
|
button.innerText = 'Deposit';
|
||||||
button.onclick = (ev) => {
|
button.onclick = (ev) => {
|
||||||
|
product_id = null;
|
||||||
deposit = '0';
|
deposit = '0';
|
||||||
|
title.innerText = 'Deposit';
|
||||||
amount.innerText = (Math.floor(parseInt(deposit) / 100)) + '.' + (parseInt(deposit) % 100).pad();
|
amount.innerText = (Math.floor(parseInt(deposit) / 100)) + '.' + (parseInt(deposit) % 100).pad();
|
||||||
input.classList.add('show');
|
input.classList.add('show');
|
||||||
};
|
};
|
||||||
|
setup_custom_price = (pid, pname) => {
|
||||||
|
product_id = pid;
|
||||||
|
title.innerText = pname;
|
||||||
|
deposit = '0';
|
||||||
|
amount.innerText = (Math.floor(parseInt(deposit) / 100)) + '.' + (parseInt(deposit) % 100).pad();
|
||||||
|
input.classList.add('show');
|
||||||
|
};
|
||||||
deposit_key = (k) => {
|
deposit_key = (k) => {
|
||||||
if (k == 'ok') {
|
if (k == 'ok') {
|
||||||
window.location.href = '/deposit?n=' + parseInt(deposit);
|
if (product_id === null) {
|
||||||
|
window.location.href = '/deposit?n=' + parseInt(deposit);
|
||||||
|
} else {
|
||||||
|
window.location.href = '/buy?pid=' + product_id + '&price=' + parseInt(deposit);
|
||||||
|
}
|
||||||
deposit = '0';
|
deposit = '0';
|
||||||
|
product_id = null;
|
||||||
input.classList.remove('show');
|
input.classList.remove('show');
|
||||||
} else if (k == 'del') {
|
} else if (k == 'del') {
|
||||||
if (deposit == '0') {
|
if (deposit == '0') {
|
||||||
|
product_id = null;
|
||||||
input.classList.remove('show');
|
input.classList.remove('show');
|
||||||
}
|
}
|
||||||
deposit = deposit.substr(0, deposit.length - 1);
|
deposit = deposit.substr(0, deposit.length - 1);
|
||||||
|
|
|
@ -49,6 +49,9 @@
|
||||||
<label for="admin-newproduct-price-non-member">Non-member price: </label>
|
<label for="admin-newproduct-price-non-member">Non-member price: </label>
|
||||||
CHF <input id="admin-newproduct-price-non-member" type="number" step="0.01" name="pricenonmember" value="0" /><br/>
|
CHF <input id="admin-newproduct-price-non-member" type="number" step="0.01" name="pricenonmember" value="0" /><br/>
|
||||||
|
|
||||||
|
<label for="admin-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>
|
||||||
|
<input id="admin-custom-price" type="checkbox" name="custom_price" /><br/>
|
||||||
|
|
||||||
<label for="admin-newproduct-stockable">Stockable: </label>
|
<label for="admin-newproduct-stockable">Stockable: </label>
|
||||||
<input id="admin-newproduct-stockable" type="checkbox" name="stockable" checked="checked" /><br/>
|
<input id="admin-newproduct-stockable" type="checkbox" name="stockable" checked="checked" /><br/>
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,9 @@
|
||||||
<label for="modproduct-price-non-member">Non-member price: </label>
|
<label for="modproduct-price-non-member">Non-member price: </label>
|
||||||
CHF <input id="modproduct-price-non-member" type="number" step="0.01" name="pricenonmember" value="{{ product.price_non_member|chf(False) }}" /><br/>
|
CHF <input id="modproduct-price-non-member" type="number" step="0.01" name="pricenonmember" value="{{ product.price_non_member|chf(False) }}" /><br/>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
<input id="modproduct-custom-price" type="checkbox" name="custom_price" {% if product.custom_price %} checked="checked" {% endif %} /><br/>
|
||||||
|
|
||||||
<label for="modproduct-stockable">Stockable: </label>
|
<label for="modproduct-stockable">Stockable: </label>
|
||||||
<input id="modproduct-stockable" type="checkbox" name="stockable" {% if product.stockable %} checked="checked" {% endif %} /><br/>
|
<input id="modproduct-stockable" type="checkbox" name="stockable" {% if product.stockable %} checked="checked" {% endif %} /><br/>
|
||||||
|
|
||||||
|
|
|
@ -26,7 +26,10 @@
|
||||||
</div>
|
</div>
|
||||||
<div id="deposit-wrapper">
|
<div id="deposit-wrapper">
|
||||||
<div id="deposit-input">
|
<div id="deposit-input">
|
||||||
<span id="deposit-amount">0.00</span>
|
<div id="deposit-output">
|
||||||
|
<span id="deposit-title"></span>
|
||||||
|
<span id="deposit-amount">0.00</span>
|
||||||
|
</div>
|
||||||
{% for i in [('1', '1'), ('2', '2'), ('3', '3'), ('4', '4'), ('5', '5'), ('6', '6'), ('7', '7'), ('8', '8'), ('9', '9'), ('del', '✗'), ('0', '0'), ('ok', '✓')] %}
|
{% for i in [('1', '1'), ('2', '2'), ('3', '3'), ('4', '4'), ('5', '5'), ('6', '6'), ('7', '7'), ('8', '8'), ('9', '9'), ('del', '✗'), ('0', '0'), ('ok', '✓')] %}
|
||||||
<div class="numpad" id="numpad-{{ i.0 }}" onclick="deposit_key('{{ i.0 }}');">{{ i.1 }}</div>
|
<div class="numpad" id="numpad-{{ i.0 }}" onclick="deposit_key('{{ i.0 }}');">{{ i.1 }}</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
@ -38,8 +41,15 @@
|
||||||
{% for product in products %}
|
{% for product in products %}
|
||||||
{# Show an item per product, consisting of the name, image, price and stock, triggering a purchase on click #}
|
{# Show an item per product, consisting of the name, image, price and stock, triggering a purchase on click #}
|
||||||
<div class="thumblist-item">
|
<div class="thumblist-item">
|
||||||
|
{% if product.custom_price %}
|
||||||
|
<a onclick="setup_custom_price({{ product.id }}, '{{ product.name}}');">
|
||||||
|
{% else %}
|
||||||
<a href="/buy?pid={{ product.id }}">
|
<a href="/buy?pid={{ product.id }}">
|
||||||
|
{% endif %}
|
||||||
<span class="thumblist-title">{{ product.name }}</span>
|
<span class="thumblist-title">{{ product.name }}</span>
|
||||||
|
{% if product.custom_price %}
|
||||||
|
<span class="thumblist-detail">Custom Price</span><br/>
|
||||||
|
{% else %}
|
||||||
<span class="thumblist-detail">Price:
|
<span class="thumblist-detail">Price:
|
||||||
{% if authuser.is_member %}
|
{% if authuser.is_member %}
|
||||||
{{ product.price_member|chf }}
|
{{ product.price_member|chf }}
|
||||||
|
@ -47,6 +57,7 @@
|
||||||
{{ product.price_non_member|chf }}
|
{{ product.price_non_member|chf }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</span><br/>
|
</span><br/>
|
||||||
|
{% endif %}
|
||||||
<div class="imgcontainer">
|
<div class="imgcontainer">
|
||||||
<img src="/static/upload/thumbnails/products/{{ product.id }}.png" alt="Picture of {{ product.name }}"/>
|
<img src="/static/upload/thumbnails/products/{{ product.id }}.png" alt="Picture of {{ product.name }}"/>
|
||||||
{% set pstock = stock.get_stock(product) %}
|
{% set pstock = stock.get_stock(product) %}
|
||||||
|
|
Loading…
Reference in a new issue