Minor template refactor and cli.py user interface enhancement #6

Closed
subnixr wants to merge 15 commits from subnixr/macao-pos:cosettine into master
11 changed files with 427 additions and 115 deletions

View File

@ -58,12 +58,12 @@ python3 cli.py user add username password
Add some products with:
```
python3 cli.py product add "Birra media" 300
python3 cli.py product add "Birra grande" 400
python3 cli.py product add "Cocktail" 500
python3 cli.py product add "Vino" 400
python3 cli.py product add "Amaro" 200
python3 cli.py product add "Acqua" 100
python3 cli.py product add "Birra media" 3
python3 cli.py product add "Birra grande" 4
python3 cli.py product add "Cocktail" 5
python3 cli.py product add "Vino" 4
python3 cli.py product add "Amaro" 2
python3 cli.py product add "Acqua" 0.5
```
And finally add and event you can play with:

257
cli.py
View File

@ -5,7 +5,8 @@ from datetime import datetime
from pos.config import Config
from pos.logging import init_logging, get_logger
from pos.database import Database, User, Event, Product, Transaction
from pos.database import Database, User, Event, ProductCategory, Product,\
Transaction
config = Config()
conf_db = config.core['DATABASE']
@ -22,7 +23,10 @@ def get_total(transaction):
def get_income(event):
return sum(get_total(t) for t in event.transactions)
if event.transactions:
return sum(get_total(t) for t in event.transactions)
else:
return 0
@click.group()
@ -30,6 +34,14 @@ def cli():
pass
@cli.command('initdb')
def initdb():
with db.get_session() as session:
categories = session.query(ProductCategory).count()
if not categories:
session.add(ProductCategory(name='Default'))
@cli.group('user')
def user():
pass
@ -49,7 +61,7 @@ def user_add(username, password):
user = User(username=username, password=password)
with db.get_session() as session:
session.add(user)
print("User succesfully added.")
print("User successfully added.")
@user.command('list')
@ -63,6 +75,26 @@ def user_list():
print("No users found.")
@user.command('set')
@click.option('-p', '--password')
@click.argument('user_uid', type=click.INT)
def user_set(user_uid, password):
with db.get_session() as session:
user = session.query(User).get(user_uid)
if not user:
print("No user found with id #{}.".format(user_uid))
return
if password:
user.password = password
with db.get_session() as session:
session.add(user)
print("User successfully edited.")
@cli.group('event')
def event():
pass
@ -78,18 +110,48 @@ def tabulate_events(events):
return tabulate(tab, headers='firstrow')
def get_overlapping_events(session, starts_at, ends_at):
events = session.query(Event)
if ends_at is None:
events = events.filter(Event.starts_at <= starts_at)
else:
events = events.filter(Event.ends_at >= starts_at)\
.filter(Event.starts_at <= ends_at)
return events.all()
@event.command('add')
@click.argument('name')
@click.option('--start')
@click.option('--end')
def event_add(name, start, end):
start = datetime.strptime(start, "%Y-%m-%d %H:%M")
end = datetime.strptime(end, "%Y-%m-%d %H:%M")
event = Event(name=name, starts_at=start, ends_at=end)
@click.argument('starts_at')
@click.argument('ends_at', required=False)
def event_add(name, starts_at, ends_at):
starts_at = datetime.strptime(starts_at, "%Y-%m-%d %H:%M")
ends_at = (datetime.strptime(ends_at, "%Y-%m-%d %H:%M")
if ends_at else None)
if ends_at and starts_at >= ends_at:
print("Could now add event: specified start date ({}) "
"is past the end date ({})."
.format(starts_at.strftime("%Y-%m-%d %H:%M"),
ends_at.strftime("%Y-%m-%d %H:%M")))
return
with db.get_session() as session:
events = get_overlapping_events(session, starts_at, ends_at)
if events:
print("Could not add event: another event is overlapping the date "
"range you have specified.")
print(tabulate_events(events))
return
with db.get_session() as session:
event = Event(name=name, starts_at=starts_at, ends_at=ends_at)
session.add(event)
print("Event succesfully added.")
session.flush()
print("Event successfully added.")
print(tabulate_events([event]))
@event.command('list')
@ -103,32 +165,160 @@ def event_list():
print("No events found.")
@event.command('set')
@click.option('-n', '--name')
@click.option('-s', '--start')
@click.option('-e', '--end')
@click.argument('event_uid')
def event_set(event_uid, name, start, end):
with db.get_session() as session:
event = session.query(Event).get(event_uid)
if not event:
print("No event found with id #{}.".format(event_uid))
return
if name:
event.name = name
if start:
starts_at = datetime.strptime(start, "%Y-%m-%d %H:%M")
if starts_at >= event.ends_at:
print("Could not edit event #{}: specified start date ({}) "
"is past the end date ({})"
.format(event.uid,
starts_at.strftime("%Y-%m-%d %H:%M"),
event.ends_at.strftime("%Y-%m-%d %H:%M")))
return
event.starts_at = starts_at
if end:
if end == 'none':
event.ends_at = None
elif end == 'now':
event.ends_at = datetime.now()
else:
ends_at = datetime.strptime(end, "%Y-%m-%d %H:%M")
if ends_at <= event.starts_at:
print("Could not edit event #{}: specified end date ({}) "
"is before the start date ({})"
.format(event.uid,
ends_at.strftime("%Y-%m-%d %H:%M"),
event.starts_at.strftime("%Y-%m-%d %H:%M")))
return
event.ends_at = datetime.strptime(end, "%Y-%m-%d %H:%M")
if event.starts_at and event.ends_at:
with db.get_session() as session:
events = get_overlapping_events(session,
event.starts_at, event.ends_at)
if events:
print("Could not edit event: another event is overlapping the "
"date range you have specified.")
print(tabulate_events(events))
return
if any([name, start, end]):
with db.get_session() as session:
session.add(event)
session.flush()
print("Event successfully edited.")
print(tabulate_events([event]))
@cli.group('category')
def category():
pass
@category.command('add')
@click.option('-s', '--sort', type=click.INT)
@click.argument('name')
def category_add(name, sort):
category = ProductCategory(name=name)
if sort:
category.sort = sort
with db.get_session() as session:
session.add(category)
print("Category successfully added.")
def tabulate_categories(categories):
tab = [["UID", "Name", "Sort", "Created at"]]
for c in categories:
tab.append([c.uid, c.name, c.sort, c.created_at])
return tabulate(tab, headers='firstrow')
@category.command('list')
@click.option('-s', '--sort', is_flag=True)
def category_list(sort):
with db.get_session() as session:
categories = session.query(ProductCategory)
if sort:
categories = categories.order_by(ProductCategory.sort.asc())
categories = categories.all()
if categories:
print(tabulate_categories(categories))
else:
print("No categories found.")
@cli.group('product')
def product():
pass
def tabulate_products(products):
tab = [["UID", "Name", "Price", "Enabled", "Created at"]]
tab = [["UID", "Name", "Price", "Sort", "Category",
"Enabled", "Created at"]]
for p in products:
tab.append([p.uid, p.name, p.price, p.is_active, p.created_at])
tab.append([p.uid, p.name, p.price, p.sort, p.category.name,
p.is_active, p.created_at])
return tabulate(tab, headers='firstrow')
@product.command('add')
@click.argument('name')
@click.argument('price')
def product_add(name, price):
@click.argument('price', type=float)
@click.option('-s', '--sort', type=click.INT)
@click.option('-c', '--category', type=click.INT)
def product_add(name, price, sort, category):
price = int(price * 100)
product = Product(name=name, price=price)
if sort:
product.sort = sort
if category:
product.category_uid = category
else:
product.category_uid = 1
with db.get_session() as session:
session.add(product)
print("Product succesfully added.")
print("Product successfully added.")
@product.command('list')
def product_list():
@click.option('-s', '--sort', is_flag=True)
def product_list(sort):
with db.get_session() as session:
products = session.query(Product).all()
products = session.query(Product)
if sort:
products = products.order_by(Product.sort.asc())
products = products.all()
if products:
print(tabulate_products(products))
@ -136,6 +326,39 @@ def product_list():
print("No products found.")
@product.command('set')
@click.option('-n', '--name')
@click.option('-p', '--price', type=click.INT)
@click.option('-s', '--sort', type=click.INT)
@click.option('-c', '--category', type=click.INT)
@click.argument('product_uid')
def product_set(product_uid, name, price, sort, category):
with db.get_session() as session:
product = session.query(Product).get(product_uid)
if not product:
print("No product found with id #{}.".format(product_uid))
return
if name:
product.name = name
if price:
product.price = price
if sort:
product.sort = sort
if category:
product.category_uid = category
if any([name, price, sort, category]):
with db.get_session() as session:
session.add(product)
print("Event successfully edited.")
print(tabulate_products([product]))
@cli.group('transaction')
def transaction():
pass

View File

@ -7,3 +7,7 @@ Path = pos.db
[FLASK]
SECRET_KEY = CHANGE_ME_NOW!
[PRINTER]
Host = 192.168.1.100
Post = 9100

View File

@ -11,3 +11,7 @@ Password = secret
[FLASK]
SECRET_KEY = CHANGE_ME_NOW!
[PRINTER]
Host = 192.168.1.100
Post = 9100

View File

@ -94,9 +94,9 @@ class User(Base):
uid = Column(Integer, primary_key=True)
username = Column(String, nullable=False, unique=True)
password = Column(PasswordType(schemes=PASSWORD_SCHEMES), nullable=False)
created_at = Column(DateTime, nullable=False, default=datetime.now)
is_active = Column(Boolean, nullable=False, server_default='1')
is_authenticated = Column(Boolean, nullable=False, server_default='0')
created_at = Column(DateTime, nullable=False, default=datetime.now)
def get_id(self):
return u'{}'.format(self.uid)
@ -106,20 +106,35 @@ class Event(Base):
__tablename__ = 'events'
uid = Column(Integer, primary_key=True)
name = Column(String, nullable=False)
created_at = Column(DateTime, nullable=False, default=datetime.now)
starts_at = Column(DateTime, nullable=False, default=datetime.now)
ends_at = Column(DateTime)
created_at = Column(DateTime, nullable=False, default=datetime.now)
transactions = relationship('Transaction', lazy='joined')
class ProductCategory(Base):
__tablename__ = 'product_categories'
uid = Column(Integer, primary_key=True)
name = Column(String, nullable=False)
sort = Column(Integer, nullable=False, server_default='0')
created_at = Column(DateTime, nullable=False, default=datetime.now)
products = relationship('Product', lazy='joined')
class Product(Base):
__tablename__ = 'products'
uid = Column(Integer, primary_key=True)
name = Column(String, nullable=False)
price = Column(Integer, nullable=False)
created_at = Column(DateTime, nullable=False, default=datetime.now)
sort = Column(Integer, nullable=False, server_default='0')
category_uid = Column(Integer, ForeignKey('product_categories.uid'),
nullable=False)
is_active = Column(Boolean, nullable=False, server_default='1')
created_at = Column(DateTime, nullable=False, default=datetime.now)
category = relationship('ProductCategory', lazy='joined')
order_entry_association = Table(
@ -132,8 +147,8 @@ order_entry_association = Table(
class Transaction(Base):
__tablename__ = 'transactions'
uid = Column(Integer, primary_key=True)
created_at = Column(DateTime, nullable=False, default=datetime.now)
event_uid = Column(Integer, ForeignKey('events.uid'), nullable=False)
created_at = Column(DateTime, nullable=False, default=datetime.now)
event = relationship('Event')
orders = relationship('Order', lazy='joined',

View File

@ -8,3 +8,4 @@ tabulate
PyYAML
flask>=0.12.0
flask_login>=0.4.0
python-escpos

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

54
static/js/sell.js 100644
View File

@ -0,0 +1,54 @@
function updateTotal(amount) {
total_el = document.getElementById('total')
total = parseFloat(total_el.innerText)
total += amount
total_el.innerText = total.toFixed(2)
}
function renderBasketItem(uid, name) {
return '<button id="basketitem_' + uid + '"' +
'onclick="delProduct(' + uid + ')">' +
'1 x ' + name +
'</button>'
}
function addProduct(uid) {
// This is the hidden input element inside the form. We'll use this DOM
// element to keep informations about the product, such as the name,
// the price and the value inside the 'data-' tag.
form_el = document.getElementById('prod_' + uid)
basket = document.getElementById('basket')
basket_el = document.getElementById('basketitem_' + uid)
form_el.value = parseInt(form_el.value) + 1
// If there is not a button for the given product inside the basket
// div we'll create it.
if (basket_el === null) {
basket.innerHTML += renderBasketItem(uid, form_el.dataset.name)
} else {
// Otherwise we'll just update it.
console.log(form_el.value)
new_text = form_el.value + ' x ' + form_el.dataset.name
basket_el.innerText = new_text
}
updateTotal(parseFloat(form_el.dataset.price))
}
function delProduct(uid) {
form_el = document.getElementById('prod_' + uid)
basket = document.getElementById('basket')
basket_el = document.getElementById('basketitem_' + uid)
form_el.value = parseInt(form_el.value) - 1
if (form_el.value == 0) {
basket.removeChild(basket_el)
} else {
new_text = form_el.value + ' x ' + form_el.dataset.name
basket_el.innerText = new_text
}
updateTotal(parseFloat(-form_el.dataset.price))
}

View File

@ -1,7 +1,5 @@
html {
width: 100%;
height: 100%;
font-size: 16px;
font-size: 14px;
}
body {
@ -11,7 +9,7 @@ body {
main {
margin: 2rem;
margin: 1rem 2rem;
}
.alert {
@ -41,12 +39,6 @@ h1 {
h2 {
margin-bottom: 0.5rem;
font-size: 2rem;
text-align: center;
}
h3 {
margin-bottom: 0.2rem;
font-size: 1.5rem;
text-align: center;
}
@ -120,8 +112,8 @@ form#login > button:hover {
#products {
display: inline-block;
margin: 4rem auto;
max-width: 50%;
margin: 0.5rem auto;
width: 60%;
}
#products > button {
@ -138,33 +130,38 @@ form#login > button:hover {
display: block;
}
#basket {
#summary {
display: inline-block;
float: right;
margin: 4rem auto;
min-width: 30%;
margin: 0.5rem auto;
}
#basket {
margin: 2rem auto;
}
#basket > button {
display: block;
width: 15rem;
height: 3rem;
width: 20rem;
height: 4rem;
margin-bottom: 0.3rem;
padding: 0.2rem 0.5rem;
font-size: 1.3rem;
font-size: 1.5rem;
}
#sell {
display: inline-block;
float: right;
margin: auto;
min-width: 30%;
margin: 1rem auto;
}
#sell > button {
display: block;
width: 15rem;
height: 3rem;
width: 20rem;
height: 5rem;
padding: 0.2rem 0.5rem;
font-size: 1.3rem;
font-size: 2rem;
}
#sell > p {
font-size: 2rem;
margin-bottom: 0.5rem;
}

View File

@ -1,76 +1,36 @@
{% extends 'base.html' %}
{% block main %}
<h1>{{ title }}</h1>
{% if event %}
<h2>{{ event.name }}</h2>
<h3>{{ event.starts_at }} → {{ event.ends_at }}</h3>
{% else %}
<h2>No ongoing event!</h2>
{% endif %}
<div id="products">
{% for product in products %}
<button type="button"
onclick="addProduct({{ product.uid }})">
<span class="name">{{ product.name }}</span>
<span class="price">{{ product.price }}</span>
<span class="price">
€ {{ '{0:.02f}'.format(product.price / 100.0) }}
</span>
</button>
{% endfor %}
</div>
<div id="basket">
</div>
<form id="sell" action="/sell" method="POST">
<div id="summary">
<div id="basket"></div>
<form id="sell" action="/sell" method="POST">
{% for product in products %}
<input type="hidden"
id="prod_{{ product.uid }}"
name="{{ product.uid }}"
data-name="{{ product.name }}"
data-price="{{ product.price }}"
value="0">
<input type="hidden"
id="prod_{{ product.uid }}"
name="{{ product.uid }}"
data-name="{{ product.name }}"
data-price="{{ '{0:.02f}'.format(product.price / 100.0) }}"
value="0">
{% endfor %}
<button type="submit">Print</input>
<p>Total: € <span id="total">0.00</span></p>
<button type="submit">Print</input>
</form>
</div>
<script type="text/javascript" src="{{ url_for('static', filename='js/sell.js') }}">
<script type="text/javascript">
function addProduct(uid) {
// This is the hidden input element inside the form. We'll use this DOM
// element to keep informations about the product, such as the name,
// the price and the value inside the 'data-' tag.
form_el = document.getElementById('prod_' + uid)
basket = document.getElementById('basket')
basket_el = document.getElementById('basketitem_' + uid)
form_el.value = parseInt(form_el.value) + 1
// If there is not a button for the given product inside the basket
// div we'll create it.
if (basket_el === null) {
new_basket_el =
'<button id="basketitem_' + uid + '"' +
'onclick="delProduct(' + uid + ')">' +
'1 x ' + form_el.dataset.name +
'</button>'
basket.innerHTML += new_basket_el
} else {
// Otherwise we'll just update it.
console.log(form_el.value)
new_text = form_el.value + ' x ' + form_el.dataset.name
basket_el.innerText = new_text
}
}
function delProduct(uid) {
form_el = document.getElementById('prod_' + uid)
basket = document.getElementById('basket')
basket_el = document.getElementById('basketitem_' + uid)
form_el.value = parseInt(form_el.value) - 1
if (form_el.value == 0) {
basket.removeChild(basket_el)
} else {
new_text = form_el.value + ' x ' + form_el.dataset.name
basket_el.innerText = new_text
}
}
{%- for product in products %}
document.getElementById('prod_{{ product.uid }}').value = 0
{%- endfor %}
</script>
{% endblock %}

62
web.py
View File

@ -1,4 +1,6 @@
from datetime import datetime
from time import sleep
import escpos.printer
from flask import Flask, redirect, request, render_template, flash
from flask_login import LoginManager, login_user, logout_user, login_required
@ -11,11 +13,20 @@ from pos.database import Database, User, Product, Event, Transaction, Order
config = Config()
debug = config.core['GENERAL'].getboolean('Debug', False)
conf_db = config.core['DATABASE']
conf_printer = config.core['PRINTER']
conf_flask = config.core['FLASK']
init_logging(config.logging)
log = get_logger('web')
printer = escpos.printer.Network(
conf_printer['Host'],
port=conf_printer.getint('Port'),
timeout=15)
db = Database(**conf_db)
app = Flask(__name__)
app.config.update(
debug=debug,
@ -27,8 +38,6 @@ login_manager.init_app(app)
login_manager.login_view = 'login'
login_manager.login_message_category = 'error'
db = Database(**conf_db)
@login_manager.user_loader
def load_user(uid):
@ -87,14 +96,15 @@ def logout():
def get_products(session):
return session.query(Product)\
.filter(Product.is_active == 1)\
.order_by(Product.sort.asc())\
.all()
def get_event(session):
return session.query(Event)\
.filter(Event.starts_at < datetime.now())\
.filter((Event.ends_at > datetime.now()) |
(Event.ends_at is None))\
.filter((Event.ends_at.is_(None))
| (Event.ends_at > datetime.now()))\
.one_or_none()
@ -109,6 +119,48 @@ def sell_page():
title='Sell', event=event, products=products)
def print_orders(transaction, cat, orders):
printer.open()
printer.set(align='CENTER')
printer.image('static/img/macao-logo-printer.png', impl='bitImageColumn')
printer.set(align='CENTER', text_type='B')
printer.text(transaction.event.name.upper())
printer.text("\n\n")
for o in orders:
printer.set(align='LEFT', width=2, height=2)
printer.text("{} x {}".format(o.quantity, o.product.name.upper()))
printer.text("\n")
printer.text("\n")
printer.set(align='RIGHT')
printer.text("{} #{}-{}-{}"
.format(datetime.strftime(transaction.created_at,
"%Y-%m-%d %H:%M"),
transaction.event.uid, transaction.uid, cat))
printer.cut()
printer.close()
sleep(0.7)
def print_transaction(transaction):
categorized_orders = {}
for o in transaction.orders:
uid = o.product.category_uid
if not categorized_orders.get(uid, False):
categorized_orders[uid] = []
categorized_orders[uid].append(o)
for cat, orders in categorized_orders.items():
print_orders(transaction, cat, orders)
@app.route('/sell', methods=['POST'])
@login_required
def sell():
@ -134,6 +186,8 @@ def sell():
transaction = Transaction(event_uid=event.uid, orders=orders)
session.add(transaction)
session.flush()
print_transaction(transaction)
flash("Success!", 'success')
return sell_page()