diff --git a/README.md b/README.md index b795efd..54f2dd0 100644 --- a/README.md +++ b/README.md @@ -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: diff --git a/cli.py b/cli.py index aa82a73..f7d9fb6 100755 --- a/cli.py +++ b/cli.py @@ -5,7 +5,8 @@ from datetime import datetime from pos.config import Config from pos.logging import init_logging, get_logger -from pos.database import Database, User, Event, Product, Transaction +from pos.database import Database, User, Event, ProductCategory, Product,\ + Transaction config = Config() conf_db = config.core['DATABASE'] @@ -22,7 +23,10 @@ def get_total(transaction): def get_income(event): - return sum(get_total(t) for t in event.transactions) + if event.transactions: + return sum(get_total(t) for t in event.transactions) + else: + return 0 @click.group() @@ -30,6 +34,14 @@ def cli(): pass +@cli.command('initdb') +def initdb(): + with db.get_session() as session: + categories = session.query(ProductCategory).count() + if not categories: + session.add(ProductCategory(name='Default')) + + @cli.group('user') def user(): pass @@ -49,7 +61,7 @@ def user_add(username, password): user = User(username=username, password=password) with db.get_session() as session: session.add(user) - print("User succesfully added.") + print("User successfully added.") @user.command('list') @@ -63,6 +75,26 @@ def user_list(): print("No users found.") +@user.command('set') +@click.option('-p', '--password') +@click.argument('user_uid', type=click.INT) +def user_set(user_uid, password): + with db.get_session() as session: + user = session.query(User).get(user_uid) + + if not user: + print("No user found with id #{}.".format(user_uid)) + return + + if password: + user.password = password + + with db.get_session() as session: + session.add(user) + + print("User successfully edited.") + + @cli.group('event') def event(): pass @@ -78,18 +110,48 @@ def tabulate_events(events): return tabulate(tab, headers='firstrow') +def get_overlapping_events(session, starts_at, ends_at): + events = session.query(Event) + + if ends_at is None: + events = events.filter(Event.starts_at <= starts_at) + else: + events = events.filter(Event.ends_at >= starts_at)\ + .filter(Event.starts_at <= ends_at) + + return events.all() + + @event.command('add') @click.argument('name') -@click.option('--start') -@click.option('--end') -def event_add(name, start, end): - start = datetime.strptime(start, "%Y-%m-%d %H:%M") - end = datetime.strptime(end, "%Y-%m-%d %H:%M") - event = Event(name=name, starts_at=start, ends_at=end) +@click.argument('starts_at') +@click.argument('ends_at', required=False) +def event_add(name, starts_at, ends_at): + starts_at = datetime.strptime(starts_at, "%Y-%m-%d %H:%M") + ends_at = (datetime.strptime(ends_at, "%Y-%m-%d %H:%M") + if ends_at else None) + + if ends_at and starts_at >= ends_at: + print("Could now add event: specified start date ({}) " + "is past the end date ({})." + .format(starts_at.strftime("%Y-%m-%d %H:%M"), + ends_at.strftime("%Y-%m-%d %H:%M"))) + return with db.get_session() as session: + events = get_overlapping_events(session, starts_at, ends_at) + if events: + print("Could not add event: another event is overlapping the date " + "range you have specified.") + print(tabulate_events(events)) + return + + with db.get_session() as session: + event = Event(name=name, starts_at=starts_at, ends_at=ends_at) session.add(event) - print("Event succesfully added.") + session.flush() + print("Event successfully added.") + print(tabulate_events([event])) @event.command('list') @@ -103,32 +165,160 @@ def event_list(): print("No events found.") +@event.command('set') +@click.option('-n', '--name') +@click.option('-s', '--start') +@click.option('-e', '--end') +@click.argument('event_uid') +def event_set(event_uid, name, start, end): + with db.get_session() as session: + event = session.query(Event).get(event_uid) + + if not event: + print("No event found with id #{}.".format(event_uid)) + return + + if name: + event.name = name + + if start: + starts_at = datetime.strptime(start, "%Y-%m-%d %H:%M") + + if starts_at >= event.ends_at: + print("Could not edit event #{}: specified start date ({}) " + "is past the end date ({})" + .format(event.uid, + starts_at.strftime("%Y-%m-%d %H:%M"), + event.ends_at.strftime("%Y-%m-%d %H:%M"))) + return + + event.starts_at = starts_at + + if end: + if end == 'none': + event.ends_at = None + elif end == 'now': + event.ends_at = datetime.now() + else: + ends_at = datetime.strptime(end, "%Y-%m-%d %H:%M") + + if ends_at <= event.starts_at: + print("Could not edit event #{}: specified end date ({}) " + "is before the start date ({})" + .format(event.uid, + ends_at.strftime("%Y-%m-%d %H:%M"), + event.starts_at.strftime("%Y-%m-%d %H:%M"))) + return + + event.ends_at = datetime.strptime(end, "%Y-%m-%d %H:%M") + + if event.starts_at and event.ends_at: + with db.get_session() as session: + events = get_overlapping_events(session, + event.starts_at, event.ends_at) + if events: + print("Could not edit event: another event is overlapping the " + "date range you have specified.") + print(tabulate_events(events)) + return + + if any([name, start, end]): + with db.get_session() as session: + session.add(event) + session.flush() + print("Event successfully edited.") + print(tabulate_events([event])) + + +@cli.group('category') +def category(): + pass + + +@category.command('add') +@click.option('-s', '--sort', type=click.INT) +@click.argument('name') +def category_add(name, sort): + category = ProductCategory(name=name) + + if sort: + category.sort = sort + + with db.get_session() as session: + session.add(category) + print("Category successfully added.") + + +def tabulate_categories(categories): + tab = [["UID", "Name", "Sort", "Created at"]] + for c in categories: + tab.append([c.uid, c.name, c.sort, c.created_at]) + return tabulate(tab, headers='firstrow') + + +@category.command('list') +@click.option('-s', '--sort', is_flag=True) +def category_list(sort): + with db.get_session() as session: + categories = session.query(ProductCategory) + + if sort: + categories = categories.order_by(ProductCategory.sort.asc()) + + categories = categories.all() + + if categories: + print(tabulate_categories(categories)) + else: + print("No categories found.") + + @cli.group('product') def product(): pass def tabulate_products(products): - tab = [["UID", "Name", "Price", "Enabled", "Created at"]] + tab = [["UID", "Name", "Price", "Sort", "Category", + "Enabled", "Created at"]] for p in products: - tab.append([p.uid, p.name, p.price, p.is_active, p.created_at]) + tab.append([p.uid, p.name, p.price, p.sort, p.category.name, + p.is_active, p.created_at]) return tabulate(tab, headers='firstrow') @product.command('add') @click.argument('name') -@click.argument('price') -def product_add(name, price): +@click.argument('price', type=float) +@click.option('-s', '--sort', type=click.INT) +@click.option('-c', '--category', type=click.INT) +def product_add(name, price, sort, category): + price = int(price * 100) product = Product(name=name, price=price) + + if sort: + product.sort = sort + + if category: + product.category_uid = category + else: + product.category_uid = 1 + with db.get_session() as session: session.add(product) - print("Product succesfully added.") + print("Product successfully added.") @product.command('list') -def product_list(): +@click.option('-s', '--sort', is_flag=True) +def product_list(sort): with db.get_session() as session: - products = session.query(Product).all() + products = session.query(Product) + + if sort: + products = products.order_by(Product.sort.asc()) + + products = products.all() if products: print(tabulate_products(products)) @@ -136,6 +326,39 @@ def product_list(): print("No products found.") +@product.command('set') +@click.option('-n', '--name') +@click.option('-p', '--price', type=click.INT) +@click.option('-s', '--sort', type=click.INT) +@click.option('-c', '--category', type=click.INT) +@click.argument('product_uid') +def product_set(product_uid, name, price, sort, category): + with db.get_session() as session: + product = session.query(Product).get(product_uid) + + if not product: + print("No product found with id #{}.".format(product_uid)) + return + + if name: + product.name = name + + if price: + product.price = price + + if sort: + product.sort = sort + + if category: + product.category_uid = category + + if any([name, price, sort, category]): + with db.get_session() as session: + session.add(product) + print("Event successfully edited.") + print(tabulate_products([product])) + + @cli.group('transaction') def transaction(): pass diff --git a/docs/config_core/core_debug_sqlite.ini b/docs/config_core/core_debug_sqlite.ini index 43f365e..2f847e3 100644 --- a/docs/config_core/core_debug_sqlite.ini +++ b/docs/config_core/core_debug_sqlite.ini @@ -7,3 +7,7 @@ Path = pos.db [FLASK] SECRET_KEY = CHANGE_ME_NOW! + +[PRINTER] +Host = 192.168.1.100 +Post = 9100 diff --git a/docs/config_core/core_production_mysql.ini b/docs/config_core/core_production_mysql.ini index af4207c..6ef2a25 100644 --- a/docs/config_core/core_production_mysql.ini +++ b/docs/config_core/core_production_mysql.ini @@ -11,3 +11,7 @@ Password = secret [FLASK] SECRET_KEY = CHANGE_ME_NOW! + +[PRINTER] +Host = 192.168.1.100 +Post = 9100 diff --git a/pos/database.py b/pos/database.py index cd5576a..1f7daa0 100644 --- a/pos/database.py +++ b/pos/database.py @@ -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', diff --git a/requirements.txt b/requirements.txt index 66f9508..920c23a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,3 +8,4 @@ tabulate PyYAML flask>=0.12.0 flask_login>=0.4.0 +python-escpos diff --git a/static/img/macao-logo-printer.png b/static/img/macao-logo-printer.png new file mode 100644 index 0000000..05d3042 Binary files /dev/null and b/static/img/macao-logo-printer.png differ diff --git a/static/js/sell.js b/static/js/sell.js new file mode 100644 index 0000000..fda2ffe --- /dev/null +++ b/static/js/sell.js @@ -0,0 +1,54 @@ +function updateTotal(amount) { + total_el = document.getElementById('total') + total = parseFloat(total_el.innerText) + total += amount + total_el.innerText = total.toFixed(2) +} + +function renderBasketItem(uid, name) { + return '' +} + +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)) +} diff --git a/static/style/screen.css b/static/style/screen.css index dc03d02..90c8ce7 100644 --- a/static/style/screen.css +++ b/static/style/screen.css @@ -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; } diff --git a/templates/sell.html b/templates/sell.html index 8445dfb..8b8c66f 100644 --- a/templates/sell.html +++ b/templates/sell.html @@ -1,76 +1,36 @@ {% extends 'base.html' %} {% block main %} -

{{ title }}

-{% if event %} -

{{ event.name }}

-

{{ event.starts_at }} → {{ event.ends_at }}

-{% else %} -

No ongoing event!

-{% endif %}
{% for product in products %} {% endfor %}
-
-
-
+
+
+ {% for product in products %} - + {% endfor %} -
+ {% endblock %} diff --git a/web.py b/web.py index cc8429e..39bd095 100644 --- a/web.py +++ b/web.py @@ -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()