Compare commits

...

16 Commits

Author SHA1 Message Date
crudo
68b59ce478 Merge branch 'master' of subnixr/macao-pos into master 2017-03-26 21:25:55 +02:00
1b3fa43a23 Clean little code 2017-03-26 21:17:07 +02:00
257eba61b0 Add floating point input 2017-03-26 21:09:46 +02:00
ce0092303f Update .gitignore to exclude rotated log files. 2017-03-26 20:37:15 +02:00
65e42c0c6e Add transaction total in sell page and style update to fit in a tablet. 2017-03-26 20:17:46 +02:00
8dab5b7ab2 Include UID in ticket. 2017-03-26 20:17:46 +02:00
71ba505c8b Implement transaction printing. 2017-03-26 20:17:46 +02:00
2cf749fd6f Fix query bug that prevented events with undefined end date being used. 2017-03-26 20:17:46 +02:00
a6e878c6fd Fix exception raising on 'event list' command when event has no transactions. 2017-03-26 20:17:46 +02:00
819a38e2d3 Database schema change. Implement ProductCategory.
* The user can now create product categories with the 'category add' command.
* The user can now add products to categories with the
  'product add --category INT' command.
2017-03-26 20:17:46 +02:00
729402ef6d Database schema change. Minor database refactoring. 2017-03-26 20:17:46 +02:00
b93478ac85 Minor cli.py refactoring. 2017-03-26 20:17:46 +02:00
c771aaadbc Database schema change and new UI feature. The user can now rearrange the products.
* Product has a new 'order' column.
* The user can now specify the sort order at product creation time with the
  'product add --sort INT' command.
* The user can now rearrange the products with the 'products set --sort INT'
  comand.
* The user can now show a sorted list of products in cli.py with the
  'product list --sort' command.
2017-03-26 20:17:46 +02:00
800be08471 Fix typos in cli.py. 2017-03-26 20:17:46 +02:00
22476bfb7b Implement 'user set' command in cli.py. 2017-03-26 20:17:45 +02:00
96ccea0899 Implement new features for cli.py 'event' command.
* The user can now create a new event with a not defined end date.
* The user can now edit the name and the start and end date of a given event.
* The software can now check if the event the user is creating or editing
  is overlapping another event already present in the database.
* The software will allow the shortcut keywords 'now' and 'none' in the
  'event set --end' command.
2017-03-26 20:17:38 +02:00
12 changed files with 430 additions and 116 deletions

2
.gitignore vendored
View File

@ -96,4 +96,4 @@ ENV/
# macao-pos files
conf/
pos.db
pos.log
pos.log*

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:

255
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):
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

56
static/js/sell.js Normal file
View File

@ -0,0 +1,56 @@
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 }}"
data-price="{{ '{0:.02f}'.format(product.price / 100.0) }}"
value="0">
{% endfor %}
<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()