Compare commits

...

5 Commits

Author SHA1 Message Date
65b0a25429 Add declarative JSON requests. 2017-09-25 21:46:23 +02:00
14cc8ec7c4 Refactor token validity check. 2017-09-25 21:22:41 +02:00
6b159b6eda Switch from Flask to aiohttp.
This repository now provides the REST API only.

Implemented:
 * Authorization

Need improvement:
 * Product listing

Missing:
 * Selling
2017-09-25 19:38:45 +02:00
fff8a4879e Clean little code 2017-09-25 15:02:14 +02:00
f2d9a01682 Add floating point input 2017-09-25 15:02:14 +02:00
17 changed files with 335 additions and 341 deletions

6
.gitignore vendored
View File

@ -97,3 +97,9 @@ ENV/
conf/ conf/
pos.db pos.db
pos.log* pos.log*
# vim swapfiles
*.swp
# IntelliJ
.idea

137
README.md
View File

@ -23,6 +23,7 @@ examples. The default path for configurations in `conf/` but you can also use
this order. this order.
For a testing environment you can just do: For a testing environment you can just do:
``` ```
mkdir conf mkdir conf
cp docs/config_core/core_debug_sqlite.ini conf/core.ini cp docs/config_core/core_debug_sqlite.ini conf/core.ini
@ -45,7 +46,7 @@ to change the SECRET_KEY value (`openssl rand -hex 32` will help you).
If you want to change the log file path open your `conf/logging.yaml` and If you want to change the log file path open your `conf/logging.yaml` and
change the `filename` field of the `file` entry inside the `handlers` category. change the `filename` field of the `file` entry inside the `handlers` category.
## Building the database ### Building the database
You also need to add some entries to the database. You also need to add some entries to the database.
@ -55,47 +56,137 @@ First of all add a new user. Get inside the virtualenv and then just do:
python3 cli.py user add username password python3 cli.py user add username password
``` ```
Add some categories with:
```
python3 cli.py category add "Birra"
python3 cli.py category add "Super"
python3 cli.py category add "Altro"
```
Add some products with: Add some products with:
``` ```
python3 cli.py product add "Birra media" 300 python3 cli.py product add -c 1 "Birra media" 3
python3 cli.py product add "Birra grande" 400 python3 cli.py product add -c 1 "Birra grande" 4
python3 cli.py product add "Cocktail" 500 python3 cli.py product add -c 2 "Vino" 4
python3 cli.py product add "Vino" 400 python3 cli.py product add -c 2 "Cocktail" 5
python3 cli.py product add "Amaro" 200 python3 cli.py product add -c 2 "Amaro" 2
python3 cli.py product add "Acqua" 100 python3 cli.py product add -c 3 "Acqua" 0.5
``` ```
And finally add and event you can play with: And finally add and event you can play with:
``` ```
python3 cli.py event add "My party" --start "2017-03-19 22:00" --end "2017-03-22 07:00" python3 cli.py event add "My party" "2017-03-19 22:00" "2017-03-22 07:00"
``` ```
## Running ## Running
You can run this software with from the virtualenv with: You can run this software within the virtualenv with:
``` ```
python3 web.py python3 web.py
``` ```
If you want to use a read httpd you can setup uwsgi as follows: ## REST API
``` ### Authentication
[uwsgi]
socket = 127.0.0.1:9000
chdir = /var/www/macao-pos * **URL**: `/api/token`
wsgi-file = web.py * **Method**: `POST`
virtualenv = env * **Success response**:
* **Code**: 200
* **Content**:
```
{
"token": "3ea90c63-4b92-465e-bee8-018a4c569252",
"created_at": "2017-09-25T18:50:38.620881",
"expires_at": "2017-09-25T18:50:38.620881"
}
```
* **Error response**:
* ***Malformed request***
* **Code**: 400
* **Content**:
```
{
"err": "malformed_request",
"msg": "Missing username and/or password keys."
}
```
* ***Invalid credentials***
* **Code**: 400
* **Content**:
```
{
"err": "invalid_credentials"
}
```
* **Sample call**:
```
curl -X POST \
-H "Accept: application/json" \
-d '{"username": "gino", "password": "paoli"}' \
"http://127.0.0.1:8080/api/token"
```
master ### Logout
workers = 1
max-requests = 200 * **URL**: `/api/token`
harakiri = 30 * **Method**: `DELETE`
die-on-term * **Success response**:
``` * **Code**: 200
* **Content**:
```
{}
```
* **Error response**:
* ***Malformed request***
* **Code**: 400
* **Content**:
```
{
"err": "malformed_request",
"msg": "Missing Authorization header."
}
```
* ***Unauthorizred***
* **Code**: 401
* **Content**:
```
{
"err": "unauthorized",
"msg": "The token is not valid."
}
```
* ***Forbidden***
* **Code**: 403
* **Content**:
```
{
"err": "forbidden",
"msg": "The token has expired."
}
```
* **Sample call**:
```
curl -X DELETE \
-H "Authorization: 3ea90c63-4b92-465e-bee8-018a4c569252" \
"http://127.0.0.1:8080/api/token"
```
## Contributing ## Contributing
Fork → commit → pull request Before pushing any commit make sure flake8 doesn't complain running:
```
flake8 web.py cli.py pos/*.py
```
1. Clone this repository
2. Create a new branch
3. Make your patch. Split it in different commits if it's huge
4. Fork this repo
5. Push your branch to your fork
6. Issue a pull request

7
cli.py
View File

@ -4,14 +4,14 @@ from tabulate import tabulate
from datetime import datetime from datetime import datetime
from pos.config import Config from pos.config import Config
from pos.logging import init_logging, get_logger from pos.logging import setup_logging, get_logger
from pos.database import Database, User, Event, ProductCategory, Product,\ from pos.database import Database, User, Event, ProductCategory, Product,\
Transaction Transaction
config = Config() config = Config()
conf_db = config.core['DATABASE'] conf_db = config.core['DATABASE']
init_logging(config.logging) setup_logging(config.logging)
log = get_logger('cli') log = get_logger('cli')
db = Database(**conf_db) db = Database(**conf_db)
@ -289,10 +289,11 @@ def tabulate_products(products):
@product.command('add') @product.command('add')
@click.argument('name') @click.argument('name')
@click.argument('price') @click.argument('price', type=float)
@click.option('-s', '--sort', type=click.INT) @click.option('-s', '--sort', type=click.INT)
@click.option('-c', '--category', type=click.INT) @click.option('-c', '--category', type=click.INT)
def product_add(name, price, sort, category): def product_add(name, price, sort, category):
price = int(price * 100)
product = Product(name=name, price=price) product = Product(name=name, price=price)
if sort: if sort:

View File

@ -1,13 +1,11 @@
[GENERAL] [GENERAL]
Debug = True Address = 127.0.0.1
Port = 8080
[DATABASE] [DATABASE]
Engine = sqlite Engine = sqlite
Path = pos.db Path = pos.db
[FLASK]
SECRET_KEY = CHANGE_ME_NOW!
[PRINTER] [PRINTER]
Host = 192.168.1.100 Host = 192.168.1.100
Post = 9100 Post = 9100

View File

@ -1,5 +1,6 @@
[GENERAL] [GENERAL]
Debug = False Address = 127.0.0.1
Port = 8080
[DATABASE] [DATABASE]
Engine = mysql+pymysql Engine = mysql+pymysql
@ -9,9 +10,6 @@ Database = pos
User = pos User = pos
Password = secret Password = secret
[FLASK]
SECRET_KEY = CHANGE_ME_NOW!
[PRINTER] [PRINTER]
Host = 192.168.1.100 Host = 192.168.1.100
Post = 9100 Post = 9100

View File

@ -15,9 +15,12 @@ handlers:
filename: pos.log filename: pos.log
loggers: loggers:
pos:
level: DEBUG
handlers: [console,file]
sqlalchemy: sqlalchemy:
level: INFO level: INFO
handlers: [console,file] handlers: [console,file]
aiohttp:
level: INFO
handlers: [console, file]
pos:
level: DEBUG
handlers: [console,file]

View File

@ -17,9 +17,12 @@ handlers:
backupCount: 3 backupCount: 3
loggers: loggers:
sqlalchemy:
level: ERROR
handlers: [console,file]
aiohttp:
level: ERROR
handlers: [console, file]
pos: pos:
level: WARNING level: WARNING
handlers: [console,file] handlers: [console,file]
sqlalchemy:
level: CRITICAL
handlers: [console,file]

View File

@ -3,8 +3,6 @@ from configparser import ConfigParser
import yaml import yaml
APP_NAME = 'pos'
CONFIG_PATHS = ['conf/', '~/.config/pos', '/usr/local/etc/pos', '/etc/pos'] CONFIG_PATHS = ['conf/', '~/.config/pos', '/usr/local/etc/pos', '/etc/pos']
CONFIG_FILES = { CONFIG_FILES = {
'core': 'core.ini', 'core': 'core.ini',
@ -14,10 +12,8 @@ CONFIG_FILES = {
def get_default_path(): def get_default_path():
for p in CONFIG_PATHS: for p in CONFIG_PATHS:
if all([ if all([os.path.isfile(os.path.join(p, f))
os.path.isfile(os.path.join(p, f)) for f in CONFIG_FILES.values()]):
for f in CONFIG_FILES.values()
]):
return p return p
else: else:
raise Exception('Unable to find a configuration folder.') raise Exception('Unable to find a configuration folder.')

View File

@ -1,18 +1,20 @@
from datetime import datetime from datetime import datetime, timedelta
from uuid import uuid4
from contextlib import contextmanager from contextlib import contextmanager
import sqlalchemy as sa import sqlalchemy as sa
import sqlalchemy.ext.declarative import sqlalchemy.ext.declarative
from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy import Table, Column, ForeignKey from sqlalchemy import Column, ForeignKey
from sqlalchemy import Integer, String, Boolean, DateTime from sqlalchemy import Integer, String, Boolean, DateTime
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from sqlalchemy_utils import force_auto_coercion from sqlalchemy_utils import force_auto_coercion
from sqlalchemy_utils.types.password import PasswordType from sqlalchemy_utils.types.password import PasswordType
from pos.logging import get_logger from pos.logging import get_logger
log = get_logger('database')
# The database URL must follow RFC 1738 in the form # The database URL must follow RFC 1738 in the form
# dialect+driver://username:password@host:port/database # dialect+driver://username:password@host:port/database
@ -23,11 +25,7 @@ ENGINE_SQLITE_MEMORY = "sqlite://"
PASSWORD_SCHEMES = ['pbkdf2_sha512'] PASSWORD_SCHEMES = ['pbkdf2_sha512']
Base = sqlalchemy.ext.declarative.declarative_base() Base = sqlalchemy.ext.declarative.declarative_base()
log = get_logger('database')
force_auto_coercion()
class Database: class Database:
@ -74,14 +72,15 @@ class Database:
Base.metadata.create_all(self.engine) Base.metadata.create_all(self.engine)
force_auto_coercion()
@contextmanager @contextmanager
def get_session(self): def get_session(self):
session = self.Session() session = self.Session()
try: try:
yield session yield session
except SQLAlchemyError as e: except SQLAlchemyError as e:
log.critical("Error performing transaction:") log.critical("Error performing transaction: {}".format(e))
log.critical(e)
session.rollback() session.rollback()
else: else:
session.commit() session.commit()
@ -95,12 +94,8 @@ class User(Base):
username = Column(String, nullable=False, unique=True) username = Column(String, nullable=False, unique=True)
password = Column(PasswordType(schemes=PASSWORD_SCHEMES), nullable=False) password = Column(PasswordType(schemes=PASSWORD_SCHEMES), nullable=False)
is_active = Column(Boolean, nullable=False, server_default='1') 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) created_at = Column(DateTime, nullable=False, default=datetime.now)
def get_id(self):
return u'{}'.format(self.uid)
class Event(Base): class Event(Base):
__tablename__ = 'events' __tablename__ = 'events'
@ -137,22 +132,14 @@ class Product(Base):
category = relationship('ProductCategory', lazy='joined') category = relationship('ProductCategory', lazy='joined')
order_entry_association = Table(
'order_entry_associations', Base.metadata,
Column('transaction_uid', Integer, ForeignKey('transactions.uid')),
Column('order_uid', Integer, ForeignKey('orders.uid'))
)
class Transaction(Base): class Transaction(Base):
__tablename__ = 'transactions' __tablename__ = 'transactions'
uid = Column(Integer, primary_key=True) uid = Column(Integer, primary_key=True)
event_uid = Column(Integer, ForeignKey('events.uid'), nullable=False) event_uid = Column(Integer, ForeignKey('events.uid'), nullable=False)
created_at = Column(DateTime, nullable=False, default=datetime.now) created_at = Column(DateTime, nullable=False, default=datetime.now)
event = relationship('Event') event = relationship('Event', lazy='joined')
orders = relationship('Order', lazy='joined', orders = relationship('Order', lazy='joined')
secondary=order_entry_association)
class Order(Base): class Order(Base):
@ -160,5 +147,28 @@ class Order(Base):
uid = Column(Integer, primary_key=True) uid = Column(Integer, primary_key=True)
product_uid = Column(Integer, ForeignKey('products.uid'), nullable=False) product_uid = Column(Integer, ForeignKey('products.uid'), nullable=False)
quantity = Column(Integer, nullable=False) quantity = Column(Integer, nullable=False)
transaction_uid = Column(Integer, ForeignKey('transactions.uid'),
nullable=False)
product = relationship('Product', lazy='joined') product = relationship('Product', lazy='joined')
transaction = relationship('Transaction', lazy='joined')
class AccessToken(Base):
__tablename__ = 'access_tokens'
uid = Column(Integer, primary_key=True)
user_uid = Column(Integer, ForeignKey('users.uid'), nullable=False)
token = Column(String(36), nullable=False, default=str(uuid4()))
is_active = Column(Boolean, nullable=False, server_default='1')
created_at = Column(DateTime, nullable=False, default=datetime.now)
expires_at = Column(DateTime, nullable=False,
default=(datetime.now() + timedelta(days=2)))
user = relationship('User', lazy='joined')
def is_valid(self):
return all([
self.is_active,
self.created_at < datetime.now(),
self.expires_at > datetime.now()
])

View File

@ -1,13 +1,11 @@
import logging import logging
import logging.config import logging.config
from pos.config import APP_NAME
root = logging.getLogger('pos')
root = logging.getLogger(APP_NAME) def setup_logging(config):
def init_logging(config):
logging.config.dictConfig(config) logging.config.dictConfig(config)

116
pos/rest.py Normal file
View File

@ -0,0 +1,116 @@
from functools import wraps
from aiohttp.web import json_response
from pos.database import User, ProductCategory, AccessToken
def needs(*needed):
def decorator(func):
@wraps(func)
async def wrapper(request):
request_json = await request.json()
if not all(k in request_json.keys() for k in needed) or
return json_response({
'err': 'malformed_request',
'msg': 'Missing one or more keys: {}.'.format(
", ".join(needed))
}, status=400)
else:
return func(request)
return wrapper
return decorator
def auth_required(func):
@wraps(func)
async def wrapper(request):
db = request.app['db']
headers = request.headers
if 'Authorization' not in headers.keys():
return json_response({'err': 'malformed_request',
'msg': 'Missing Authorization header.'},
status=400)
else:
remote_token = headers['Authorization']
with db.get_session() as session:
token = session.query(AccessToken) \
.filter_by(token=remote_token) \
.one_or_none()
if not token:
return json_response({'err': 'unauthorized',
'msg': 'The token is not valid.'},
status=401)
elif not token.is_valid():
return json_response({'err': 'forbidden',
'msg': 'The token has expired.'},
status=403)
else:
return await func(request)
return wrapper
@needs('username', 'password')
async def token_create(request):
db = request.app['db']
request_json = await request.json()
username = request_json['username']
password = request_json['password']
with db.get_session() as session:
user = session.query(User) \
.filter_by(username=username) \
.one_or_none()
if not user or user.password != password:
return json_response({'err': 'invalid_credentials'},
status=400)
with db.get_session() as session:
token = AccessToken(user=user)
session.add(token)
return json_response({
'token': token.token,
'created_at': token.created_at.isoformat(),
'expires_at': token.created_at.isoformat()
})
@auth_required
async def token_destroy(request):
db = request.app['db']
remote_token = request.headers['Authorization']
with db.get_session() as session:
token = session.query(AccessToken) \
.filter_by(token=remote_token) \
.one_or_none()
token.is_active = False
session.add(token)
return json_response({}, status=200)
@auth_required
async def product_list(request):
db = request.app['db']
with db.get_session() as session:
categories = session.query(ProductCategory).all()
return json_response({
'categories': [{
'uid': c.uid,
'name': c.name,
'products': [{
'uid': p.uid,
'name': p.name,
'price': p.price
} for p in c.products]
} for c in categories]
})

8
pos/routes.py Normal file
View File

@ -0,0 +1,8 @@
from pos.rest import product_list
from pos.rest import token_create, token_destroy
def setup_routes(app):
app.router.add_route('POST', '/api/token', token_create)
app.router.add_route('DELETE', '/api/token', token_destroy)
app.router.add_route('GET', '/api/products', product_list)

View File

@ -1,11 +1,9 @@
aiohttp
click>=6.0 click>=6.0
SQLAlchemy>=1.1.0 SQLAlchemy>=1.2.0
sqlalchemy_utils>=0.32.00 sqlalchemy_utils>=0.32.00
pymysql pymysql
babel
passlib passlib
tabulate tabulate
PyYAML PyYAML
flask>=0.12.0
flask_login>=0.4.0
python-escpos python-escpos

View File

@ -1,39 +1,3 @@
{% extends 'base.html' %}
{% block main %}
<div id="products">
{% for product in products %}
<button type="button"
onclick="addProduct({{ product.uid }})">
<span class="name">{{ product.name }}</span>
<span class="price">
{{ '{0:.02f}'.format(product.price / 100.0) }}
</span>
</button>
{% endfor %}
</div>
<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="{{ '{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">
function reset() {
{%- for product in products %}
document.getElementById('prod_{{ product.uid }}').value = 0
{%- endfor %}
}
function updateTotal(amount) { function updateTotal(amount) {
total_el = document.getElementById('total') total_el = document.getElementById('total')
total = parseFloat(total_el.innerText) total = parseFloat(total_el.innerText)
@ -41,6 +5,13 @@ function updateTotal(amount) {
total_el.innerText = total.toFixed(2) 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) { function addProduct(uid) {
// This is the hidden input element inside the form. We'll use this DOM // 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, // element to keep informations about the product, such as the name,
@ -54,12 +25,7 @@ function addProduct(uid) {
// If there is not a button for the given product inside the basket // If there is not a button for the given product inside the basket
// div we'll create it. // div we'll create it.
if (basket_el === null) { if (basket_el === null) {
new_basket_el = basket.innerHTML += renderBasketItem(uid, form_el.dataset.name)
'<button id="basketitem_' + uid + '"' +
'onclick="delProduct(' + uid + ')">' +
'1 x ' + form_el.dataset.name +
'</button>'
basket.innerHTML += new_basket_el
} else { } else {
// Otherwise we'll just update it. // Otherwise we'll just update it.
console.log(form_el.value) console.log(form_el.value)
@ -87,6 +53,4 @@ function delProduct(uid) {
updateTotal(parseFloat(-form_el.dataset.price)) updateTotal(parseFloat(-form_el.dataset.price))
} }
reset()
</script>
{% endblock %}

View File

@ -1,21 +0,0 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet"
href="{{ url_for('static', filename='style/reset.css') }}">
<link rel="stylesheet"
href="{{ url_for('static', filename='style/screen.css') }}">
<title>{{ title }}</title>
</head>
<body>
<main>{% block main %}{% endblock %}</main>
{% with alerts = get_flashed_messages(with_categories=True) %}
{% if alerts %}{% for category, message in alerts %}
<div class="alert {{ category }}">{{ message }}</div>
{% endfor %}{% endif %}
{% endwith %}
</body>
</html>

View File

@ -1,11 +0,0 @@
{% extends 'base.html' %}
{% block main %}
<h1>{{ title }}</h1>
<form id="login" action="/login" method="post">
<label for="username">Username</label>
<input id="username" name="username" type="text" required autofocus>
<label for="password">Password</label>
<input id="password" name="password" type="password" required>
<button type="submit">Login</button>
</form>
{% endblock %}

206
web.py Normal file → Executable file
View File

@ -1,197 +1,33 @@
from datetime import datetime #!/usr/bin/env python3
from time import sleep import asyncio
import escpos.printer from aiohttp import web
from flask import Flask, redirect, request, render_template, flash
from flask_login import LoginManager, login_user, logout_user, login_required
from pos.config import Config from pos.config import Config
from pos.logging import init_logging, get_logger from pos.logging import setup_logging, get_logger
from pos.database import Database, User, Product, Event, Transaction, Order from pos.database import Database
from pos.routes import setup_routes
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') log = get_logger('web')
printer = escpos.printer.Network(
conf_printer['Host'],
port=conf_printer.getint('Port'),
timeout=15)
def setup_app(loop, config):
app = web.Application(loop=loop)
db = Database(**conf_db) app['config'] = config
app['db'] = Database(**config.core['DATABASE'])
app = Flask(__name__) setup_routes(app)
app.config.update(
debug=debug,
SECRET_KEY=conf_flask['SECRET_KEY']
)
login_manager = LoginManager() return app
login_manager.init_app(app)
login_manager.login_view = 'login'
login_manager.login_message_category = 'error'
@login_manager.user_loader
def load_user(uid):
user = None
with db.get_session() as session:
user = session.query(User).get(uid)
return user
@app.route('/')
def home():
return redirect('/sell')
@app.route('/login', methods=['GET'])
def login_page():
return render_template('login.html', title='Login')
@app.route('/login', methods=['POST'])
def login():
username = request.form['username']
password = request.form['password']
if not all([username, password]):
flash("Fill all fields.", 'error')
return login_page(), 400
with db.get_session() as session:
user = session.query(User)\
.filter(User.username == username)\
.filter(User.is_active == 1)\
.one_or_none()
if user is None:
flash("User not found.", 'error')
return login_page(), 400
if user.password == password:
login_user(user)
session.add(user)
user.is_authenticated = True
flash("Succesfully logged in.", 'success')
return redirect('/')
else:
flash("Wrong password.", 'error')
return login_page(), 400
@app.route('/logout')
def logout():
logout_user()
return redirect('/')
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.is_(None))
| (Event.ends_at > datetime.now()))\
.one_or_none()
@app.route('/sell', methods=['GET'])
@login_required
def sell_page():
with db.get_session() as session:
products = get_products(session)
event = get_event(session)
return render_template('sell.html',
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():
with db.get_session() as session:
event = get_event(session)
if event is None:
flash("Can't perform order without an ongoing event!", 'error')
return sell_page()
quantities = request.form.values()
if not any(int(qty) > 0 for qty in quantities):
flash("Can't perform empty order!", 'error')
return sell_page()
with db.get_session() as session:
items = request.form.items()
orders = [
Order(product_uid=uid, quantity=qty)
for uid, qty in items
if int(qty) > 0
]
transaction = Transaction(event_uid=event.uid, orders=orders)
session.add(transaction)
session.flush()
print_transaction(transaction)
flash("Success!", 'success')
return sell_page()
if __name__ == '__main__': if __name__ == '__main__':
app.run(host='0.0.0.0', port=8080, debug=debug) config = Config()
setup_logging(config.logging)
loop = asyncio.get_event_loop()
app = setup_app(loop, config)
web.run_app(app,
host=config.core.get('GENERAL', 'Address'),
port=config.core.getint('GENERAL', 'Port'))