Compare commits
7 Commits
master
...
docker-con
Author | SHA1 | Date | |
---|---|---|---|
|
836186f58b | ||
|
abb76836a1 | ||
65b0a25429 | |||
14cc8ec7c4 | |||
6b159b6eda | |||
fff8a4879e | |||
f2d9a01682 |
2
.dockerignore
Normal file
2
.dockerignore
Normal file
|
@ -0,0 +1,2 @@
|
|||
.git
|
||||
.idea
|
6
.gitignore
vendored
6
.gitignore
vendored
|
@ -97,3 +97,9 @@ ENV/
|
|||
conf/
|
||||
pos.db
|
||||
pos.log*
|
||||
|
||||
# vim swapfiles
|
||||
*.swp
|
||||
|
||||
# IntelliJ
|
||||
.idea
|
||||
|
|
14
Dockerfile
Normal file
14
Dockerfile
Normal file
|
@ -0,0 +1,14 @@
|
|||
FROM alpine:latest
|
||||
MAINTAINER Notisset <notisset@autistici.org>
|
||||
|
||||
#Pillow requires the following dependencies to build: musl-dev zlib-dev jpeg-dev gcc python3-dev
|
||||
RUN apk update && apk add musl-dev zlib-dev jpeg-dev gcc python3 python3-dev
|
||||
|
||||
COPY requirements.txt .
|
||||
RUN pip3 install -r requirements.txt
|
||||
|
||||
COPY . /var/www/site
|
||||
RUN sed -i '/Address/s/= .*/= 0.0.0.0/' /var/www/site/conf/core.ini
|
||||
|
||||
EXPOSE 8080
|
||||
ENTRYPOINT python3 /var/www/site/web.py
|
133
README.md
133
README.md
|
@ -23,6 +23,7 @@ examples. The default path for configurations in `conf/` but you can also use
|
|||
this order.
|
||||
|
||||
For a testing environment you can just do:
|
||||
|
||||
```
|
||||
mkdir conf
|
||||
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
|
||||
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.
|
||||
|
||||
|
@ -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
|
||||
```
|
||||
|
||||
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:
|
||||
|
||||
```
|
||||
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
|
||||
python3 cli.py product add -c 1 "Birra media" 3
|
||||
python3 cli.py product add -c 1 "Birra grande" 4
|
||||
python3 cli.py product add -c 2 "Vino" 4
|
||||
python3 cli.py product add -c 2 "Cocktail" 5
|
||||
python3 cli.py product add -c 2 "Amaro" 2
|
||||
python3 cli.py product add -c 3 "Acqua" 0.5
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
You can run this software with from the virtualenv with:
|
||||
You can run this software within the virtualenv with:
|
||||
|
||||
```
|
||||
python3 web.py
|
||||
```
|
||||
|
||||
If you want to use a read httpd you can setup uwsgi as follows:
|
||||
## REST API
|
||||
|
||||
### Authentication
|
||||
|
||||
* **URL**: `/api/token`
|
||||
* **Method**: `POST`
|
||||
* **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"
|
||||
```
|
||||
[uwsgi]
|
||||
socket = 127.0.0.1:9000
|
||||
|
||||
chdir = /var/www/macao-pos
|
||||
wsgi-file = web.py
|
||||
virtualenv = env
|
||||
### Logout
|
||||
|
||||
master
|
||||
workers = 1
|
||||
max-requests = 200
|
||||
harakiri = 30
|
||||
die-on-term
|
||||
* **URL**: `/api/token`
|
||||
* **Method**: `DELETE`
|
||||
* **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
|
||||
|
||||
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
|
||||
|
|
4
cli.py
4
cli.py
|
@ -4,14 +4,14 @@ from tabulate import tabulate
|
|||
from datetime import datetime
|
||||
|
||||
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,\
|
||||
Transaction
|
||||
|
||||
config = Config()
|
||||
conf_db = config.core['DATABASE']
|
||||
|
||||
init_logging(config.logging)
|
||||
setup_logging(config.logging)
|
||||
log = get_logger('cli')
|
||||
|
||||
db = Database(**conf_db)
|
||||
|
|
10
docker-compose.yml
Normal file
10
docker-compose.yml
Normal file
|
@ -0,0 +1,10 @@
|
|||
version: '2'
|
||||
services:
|
||||
app:
|
||||
build: .
|
||||
restart: always
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- .:/var/www/site
|
||||
working_dir: /var/www/site
|
|
@ -1,13 +1,11 @@
|
|||
[GENERAL]
|
||||
Debug = True
|
||||
Address = 127.0.0.1
|
||||
Port = 8080
|
||||
|
||||
[DATABASE]
|
||||
Engine = sqlite
|
||||
Path = pos.db
|
||||
|
||||
[FLASK]
|
||||
SECRET_KEY = CHANGE_ME_NOW!
|
||||
|
||||
[PRINTER]
|
||||
Host = 192.168.1.100
|
||||
Post = 9100
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
[GENERAL]
|
||||
Debug = False
|
||||
Address = 127.0.0.1
|
||||
Port = 8080
|
||||
|
||||
[DATABASE]
|
||||
Engine = mysql+pymysql
|
||||
|
@ -9,9 +10,6 @@ Database = pos
|
|||
User = pos
|
||||
Password = secret
|
||||
|
||||
[FLASK]
|
||||
SECRET_KEY = CHANGE_ME_NOW!
|
||||
|
||||
[PRINTER]
|
||||
Host = 192.168.1.100
|
||||
Post = 9100
|
||||
|
|
|
@ -15,9 +15,12 @@ handlers:
|
|||
filename: pos.log
|
||||
|
||||
loggers:
|
||||
pos:
|
||||
level: DEBUG
|
||||
handlers: [console,file]
|
||||
sqlalchemy:
|
||||
level: INFO
|
||||
handlers: [console,file]
|
||||
aiohttp:
|
||||
level: INFO
|
||||
handlers: [console, file]
|
||||
pos:
|
||||
level: DEBUG
|
||||
handlers: [console,file]
|
||||
|
|
|
@ -17,9 +17,12 @@ handlers:
|
|||
backupCount: 3
|
||||
|
||||
loggers:
|
||||
sqlalchemy:
|
||||
level: ERROR
|
||||
handlers: [console,file]
|
||||
aiohttp:
|
||||
level: ERROR
|
||||
handlers: [console, file]
|
||||
pos:
|
||||
level: WARNING
|
||||
handlers: [console,file]
|
||||
sqlalchemy:
|
||||
level: CRITICAL
|
||||
handlers: [console,file]
|
||||
|
|
|
@ -3,8 +3,6 @@ from configparser import ConfigParser
|
|||
import yaml
|
||||
|
||||
|
||||
APP_NAME = 'pos'
|
||||
|
||||
CONFIG_PATHS = ['conf/', '~/.config/pos', '/usr/local/etc/pos', '/etc/pos']
|
||||
CONFIG_FILES = {
|
||||
'core': 'core.ini',
|
||||
|
@ -14,10 +12,8 @@ CONFIG_FILES = {
|
|||
|
||||
def get_default_path():
|
||||
for p in CONFIG_PATHS:
|
||||
if all([
|
||||
os.path.isfile(os.path.join(p, f))
|
||||
for f in CONFIG_FILES.values()
|
||||
]):
|
||||
if all([os.path.isfile(os.path.join(p, f))
|
||||
for f in CONFIG_FILES.values()]):
|
||||
return p
|
||||
else:
|
||||
raise Exception('Unable to find a configuration folder.')
|
||||
|
|
|
@ -1,18 +1,20 @@
|
|||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
from uuid import uuid4
|
||||
from contextlib import contextmanager
|
||||
|
||||
import sqlalchemy as sa
|
||||
import sqlalchemy.ext.declarative
|
||||
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.orm import relationship
|
||||
|
||||
from sqlalchemy_utils import force_auto_coercion
|
||||
from sqlalchemy_utils.types.password import PasswordType
|
||||
|
||||
from pos.logging import get_logger
|
||||
|
||||
log = get_logger('database')
|
||||
|
||||
|
||||
# The database URL must follow RFC 1738 in the form
|
||||
# dialect+driver://username:password@host:port/database
|
||||
|
@ -23,11 +25,7 @@ ENGINE_SQLITE_MEMORY = "sqlite://"
|
|||
|
||||
PASSWORD_SCHEMES = ['pbkdf2_sha512']
|
||||
|
||||
|
||||
Base = sqlalchemy.ext.declarative.declarative_base()
|
||||
log = get_logger('database')
|
||||
|
||||
force_auto_coercion()
|
||||
|
||||
|
||||
class Database:
|
||||
|
@ -74,14 +72,15 @@ class Database:
|
|||
|
||||
Base.metadata.create_all(self.engine)
|
||||
|
||||
force_auto_coercion()
|
||||
|
||||
@contextmanager
|
||||
def get_session(self):
|
||||
session = self.Session()
|
||||
try:
|
||||
yield session
|
||||
except SQLAlchemyError as e:
|
||||
log.critical("Error performing transaction:")
|
||||
log.critical(e)
|
||||
log.critical("Error performing transaction: {}".format(e))
|
||||
session.rollback()
|
||||
else:
|
||||
session.commit()
|
||||
|
@ -95,12 +94,8 @@ class User(Base):
|
|||
username = Column(String, nullable=False, unique=True)
|
||||
password = Column(PasswordType(schemes=PASSWORD_SCHEMES), nullable=False)
|
||||
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)
|
||||
|
||||
|
||||
class Event(Base):
|
||||
__tablename__ = 'events'
|
||||
|
@ -137,22 +132,14 @@ class Product(Base):
|
|||
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):
|
||||
__tablename__ = 'transactions'
|
||||
uid = Column(Integer, primary_key=True)
|
||||
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',
|
||||
secondary=order_entry_association)
|
||||
event = relationship('Event', lazy='joined')
|
||||
orders = relationship('Order', lazy='joined')
|
||||
|
||||
|
||||
class Order(Base):
|
||||
|
@ -160,5 +147,28 @@ class Order(Base):
|
|||
uid = Column(Integer, primary_key=True)
|
||||
product_uid = Column(Integer, ForeignKey('products.uid'), nullable=False)
|
||||
quantity = Column(Integer, nullable=False)
|
||||
transaction_uid = Column(Integer, ForeignKey('transactions.uid'),
|
||||
nullable=False)
|
||||
|
||||
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()
|
||||
])
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
import logging
|
||||
import logging.config
|
||||
|
||||
from pos.config import APP_NAME
|
||||
|
||||
root = logging.getLogger('pos')
|
||||
|
||||
|
||||
root = logging.getLogger(APP_NAME)
|
||||
|
||||
|
||||
def init_logging(config):
|
||||
def setup_logging(config):
|
||||
logging.config.dictConfig(config)
|
||||
|
||||
|
||||
|
|
116
pos/rest.py
Normal file
116
pos/rest.py
Normal 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
8
pos/routes.py
Normal 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)
|
|
@ -1,11 +1,9 @@
|
|||
aiohttp
|
||||
click>=6.0
|
||||
SQLAlchemy>=1.1.0
|
||||
SQLAlchemy>=1.2.0b1
|
||||
sqlalchemy_utils>=0.32.00
|
||||
pymysql
|
||||
babel
|
||||
passlib
|
||||
tabulate
|
||||
PyYAML
|
||||
flask>=0.12.0
|
||||
flask_login>=0.4.0
|
||||
python-escpos
|
||||
|
|
|
@ -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>
|
|
@ -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 %}
|
|
@ -1,36 +0,0 @@
|
|||
{% 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" src="{{ url_for('static', filename='js/sell.js') }}">
|
||||
<script type="text/javascript">
|
||||
{%- for product in products %}
|
||||
document.getElementById('prod_{{ product.uid }}').value = 0
|
||||
{%- endfor %}
|
||||
</script>
|
||||
{% endblock %}
|
206
web.py
Normal file → Executable file
206
web.py
Normal file → Executable file
|
@ -1,197 +1,33 @@
|
|||
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
|
||||
#!/usr/bin/env python3
|
||||
import asyncio
|
||||
from aiohttp import web
|
||||
|
||||
from pos.config import Config
|
||||
from pos.logging import init_logging, get_logger
|
||||
from pos.database import Database, User, Product, Event, Transaction, Order
|
||||
from pos.logging import setup_logging, get_logger
|
||||
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')
|
||||
|
||||
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__)
|
||||
app.config.update(
|
||||
debug=debug,
|
||||
SECRET_KEY=conf_flask['SECRET_KEY']
|
||||
)
|
||||
setup_routes(app)
|
||||
|
||||
login_manager = LoginManager()
|
||||
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()
|
||||
return app
|
||||
|
||||
|
||||
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'))
|
||||
|
|
Loading…
Reference in New Issue
Block a user