forked from crudo/macao-pos
Compare commits
3 Commits
docker-con
...
master
Author | SHA1 | Date | |
---|---|---|---|
|
68b59ce478 | ||
1b3fa43a23 | |||
257eba61b0 |
|
@ -1,2 +0,0 @@
|
||||||
.git
|
|
||||||
.idea
|
|
6
.gitignore
vendored
6
.gitignore
vendored
|
@ -97,9 +97,3 @@ ENV/
|
||||||
conf/
|
conf/
|
||||||
pos.db
|
pos.db
|
||||||
pos.log*
|
pos.log*
|
||||||
|
|
||||||
# vim swapfiles
|
|
||||||
*.swp
|
|
||||||
|
|
||||||
# IntelliJ
|
|
||||||
.idea
|
|
||||||
|
|
14
Dockerfile
14
Dockerfile
|
@ -1,14 +0,0 @@
|
||||||
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
|
|
135
README.md
135
README.md
|
@ -23,7 +23,6 @@ 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
|
||||||
|
@ -46,7 +45,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.
|
||||||
|
|
||||||
|
@ -56,137 +55,47 @@ 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 -c 1 "Birra media" 3
|
python3 cli.py product add "Birra media" 3
|
||||||
python3 cli.py product add -c 1 "Birra grande" 4
|
python3 cli.py product add "Birra grande" 4
|
||||||
python3 cli.py product add -c 2 "Vino" 4
|
python3 cli.py product add "Cocktail" 5
|
||||||
python3 cli.py product add -c 2 "Cocktail" 5
|
python3 cli.py product add "Vino" 4
|
||||||
python3 cli.py product add -c 2 "Amaro" 2
|
python3 cli.py product add "Amaro" 2
|
||||||
python3 cli.py product add -c 3 "Acqua" 0.5
|
python3 cli.py product add "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" "2017-03-19 22:00" "2017-03-22 07:00"
|
python3 cli.py event add "My party" --start "2017-03-19 22:00" --end "2017-03-22 07:00"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Running
|
## Running
|
||||||
|
|
||||||
You can run this software within the virtualenv with:
|
You can run this software with from the virtualenv with:
|
||||||
|
|
||||||
```
|
```
|
||||||
python3 web.py
|
python3 web.py
|
||||||
```
|
```
|
||||||
|
|
||||||
## REST API
|
If you want to use a read httpd you can setup uwsgi as follows:
|
||||||
|
|
||||||
### Authentication
|
```
|
||||||
|
[uwsgi]
|
||||||
|
socket = 127.0.0.1:9000
|
||||||
|
|
||||||
* **URL**: `/api/token`
|
chdir = /var/www/macao-pos
|
||||||
* **Method**: `POST`
|
wsgi-file = web.py
|
||||||
* **Success response**:
|
virtualenv = env
|
||||||
* **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"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Logout
|
master
|
||||||
|
workers = 1
|
||||||
* **URL**: `/api/token`
|
max-requests = 200
|
||||||
* **Method**: `DELETE`
|
harakiri = 30
|
||||||
* **Success response**:
|
die-on-term
|
||||||
* **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
|
||||||
|
|
||||||
Before pushing any commit make sure flake8 doesn't complain running:
|
Fork → commit → pull request
|
||||||
|
|
||||||
```
|
|
||||||
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 datetime import datetime
|
||||||
|
|
||||||
from pos.config import Config
|
from pos.config import Config
|
||||||
from pos.logging import setup_logging, get_logger
|
from pos.logging import init_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']
|
||||||
|
|
||||||
setup_logging(config.logging)
|
init_logging(config.logging)
|
||||||
log = get_logger('cli')
|
log = get_logger('cli')
|
||||||
|
|
||||||
db = Database(**conf_db)
|
db = Database(**conf_db)
|
||||||
|
|
|
@ -1,10 +0,0 @@
|
||||||
version: '2'
|
|
||||||
services:
|
|
||||||
app:
|
|
||||||
build: .
|
|
||||||
restart: always
|
|
||||||
ports:
|
|
||||||
- "8080:8080"
|
|
||||||
volumes:
|
|
||||||
- .:/var/www/site
|
|
||||||
working_dir: /var/www/site
|
|
|
@ -1,11 +1,13 @@
|
||||||
[GENERAL]
|
[GENERAL]
|
||||||
Address = 127.0.0.1
|
Debug = True
|
||||||
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
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
[GENERAL]
|
[GENERAL]
|
||||||
Address = 127.0.0.1
|
Debug = False
|
||||||
Port = 8080
|
|
||||||
|
|
||||||
[DATABASE]
|
[DATABASE]
|
||||||
Engine = mysql+pymysql
|
Engine = mysql+pymysql
|
||||||
|
@ -10,6 +9,9 @@ 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
|
||||||
|
|
|
@ -15,12 +15,9 @@ handlers:
|
||||||
filename: pos.log
|
filename: pos.log
|
||||||
|
|
||||||
loggers:
|
loggers:
|
||||||
sqlalchemy:
|
|
||||||
level: INFO
|
|
||||||
handlers: [console,file]
|
|
||||||
aiohttp:
|
|
||||||
level: INFO
|
|
||||||
handlers: [console, file]
|
|
||||||
pos:
|
pos:
|
||||||
level: DEBUG
|
level: DEBUG
|
||||||
handlers: [console,file]
|
handlers: [console,file]
|
||||||
|
sqlalchemy:
|
||||||
|
level: INFO
|
||||||
|
handlers: [console,file]
|
||||||
|
|
|
@ -17,12 +17,9 @@ 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]
|
||||||
|
|
|
@ -3,6 +3,8 @@ 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',
|
||||||
|
@ -12,8 +14,10 @@ CONFIG_FILES = {
|
||||||
|
|
||||||
def get_default_path():
|
def get_default_path():
|
||||||
for p in CONFIG_PATHS:
|
for p in CONFIG_PATHS:
|
||||||
if all([os.path.isfile(os.path.join(p, f))
|
if all([
|
||||||
for f in CONFIG_FILES.values()]):
|
os.path.isfile(os.path.join(p, f))
|
||||||
|
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.')
|
||||||
|
|
|
@ -1,20 +1,18 @@
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime
|
||||||
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 Column, ForeignKey
|
from sqlalchemy import Table, 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
|
||||||
|
@ -25,7 +23,11 @@ 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:
|
||||||
|
@ -72,15 +74,14 @@ 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: {}".format(e))
|
log.critical("Error performing transaction:")
|
||||||
|
log.critical(e)
|
||||||
session.rollback()
|
session.rollback()
|
||||||
else:
|
else:
|
||||||
session.commit()
|
session.commit()
|
||||||
|
@ -94,8 +95,12 @@ 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'
|
||||||
|
@ -132,14 +137,22 @@ 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', lazy='joined')
|
event = relationship('Event')
|
||||||
orders = relationship('Order', lazy='joined')
|
orders = relationship('Order', lazy='joined',
|
||||||
|
secondary=order_entry_association)
|
||||||
|
|
||||||
|
|
||||||
class Order(Base):
|
class Order(Base):
|
||||||
|
@ -147,28 +160,5 @@ 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()
|
|
||||||
])
|
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
import logging
|
import logging
|
||||||
import logging.config
|
import logging.config
|
||||||
|
|
||||||
|
from pos.config import APP_NAME
|
||||||
root = logging.getLogger('pos')
|
|
||||||
|
|
||||||
|
|
||||||
def setup_logging(config):
|
root = logging.getLogger(APP_NAME)
|
||||||
|
|
||||||
|
|
||||||
|
def init_logging(config):
|
||||||
logging.config.dictConfig(config)
|
logging.config.dictConfig(config)
|
||||||
|
|
||||||
|
|
||||||
|
|
116
pos/rest.py
116
pos/rest.py
|
@ -1,116 +0,0 @@
|
||||||
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]
|
|
||||||
})
|
|
|
@ -1,8 +0,0 @@
|
||||||
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,9 +1,11 @@
|
||||||
aiohttp
|
|
||||||
click>=6.0
|
click>=6.0
|
||||||
SQLAlchemy>=1.2.0b1
|
SQLAlchemy>=1.1.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
|
||||||
|
|
21
templates/base.html
Normal file
21
templates/base.html
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
<!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>
|
11
templates/login.html
Normal file
11
templates/login.html
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
{% 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 %}
|
36
templates/sell.html
Normal file
36
templates/sell.html
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
{% 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
Executable file → Normal file
206
web.py
Executable file → Normal file
|
@ -1,33 +1,197 @@
|
||||||
#!/usr/bin/env python3
|
from datetime import datetime
|
||||||
import asyncio
|
from time import sleep
|
||||||
from aiohttp import web
|
import escpos.printer
|
||||||
|
|
||||||
|
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 setup_logging, get_logger
|
from pos.logging import init_logging, get_logger
|
||||||
from pos.database import Database
|
from pos.database import Database, User, Product, Event, Transaction, Order
|
||||||
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)
|
|
||||||
|
|
||||||
app['config'] = config
|
db = Database(**conf_db)
|
||||||
app['db'] = Database(**config.core['DATABASE'])
|
|
||||||
|
|
||||||
setup_routes(app)
|
app = Flask(__name__)
|
||||||
|
app.config.update(
|
||||||
|
debug=debug,
|
||||||
|
SECRET_KEY=conf_flask['SECRET_KEY']
|
||||||
|
)
|
||||||
|
|
||||||
return 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()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
config = Config()
|
app.run(host='0.0.0.0', port=8080, debug=debug)
|
||||||
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