Big cleanup ദ്ദി^._.^)

This commit is contained in:
2023-08-29 18:32:49 +02:00
parent 6ab66771f2
commit c25e5cec27
49 changed files with 597 additions and 10337 deletions

0
dibbler/__init__.py Normal file
View File

View File

@@ -0,0 +1,66 @@
import os
from PIL import ImageFont
from barcode.writer import ImageWriter, mm2px
from brother_ql.devicedependent import label_type_specs
def px2mm(px, dpi=300):
return (25.4 * px)/dpi
class BrotherLabelWriter(ImageWriter):
def __init__(self, typ='62', max_height=350, rot=False, text=None):
super(BrotherLabelWriter, self).__init__()
assert typ in label_type_specs
self.rot = rot
if self.rot:
self._h, self._w = label_type_specs[typ]['dots_printable']
if self._w == 0 or self._w > max_height:
self._w = min(max_height, self._h / 2)
else:
self._w, self._h = label_type_specs[typ]['dots_printable']
if self._h == 0 or self._h > max_height:
self._h = min(max_height, self._w / 2)
self._xo = 0.0
self._yo = 0.0
self._title = text
def _init(self, code):
self.text = None
super(BrotherLabelWriter, self)._init(code)
def calculate_size(self, modules_per_line, number_of_lines, dpi=300):
x, y = super(BrotherLabelWriter, self).calculate_size(modules_per_line, number_of_lines, dpi)
self._xo = (px2mm(self._w)-px2mm(x))/2
self._yo = (px2mm(self._h)-px2mm(y))
assert self._xo >= 0
assert self._yo >= 0
return int(self._w), int(self._h)
def _paint_module(self, xpos, ypos, width, color):
super(BrotherLabelWriter, self)._paint_module(xpos+self._xo, ypos+self._yo, width, color)
def _paint_text(self, xpos, ypos):
super(BrotherLabelWriter, self)._paint_text(xpos+self._xo, ypos+self._yo)
def _finish(self):
if self._title:
width = self._w+1
height = 0
max_h = self._h - mm2px(self._yo, self.dpi)
fs = int(max_h / 1.2)
font_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "Stranger back in the Night.ttf")
font = ImageFont.truetype(font_path, 10)
while width > self._w or height > max_h:
font = ImageFont.truetype(font_path, fs)
width, height = font.getsize(self._title)
fs -= 1
pos = (
(self._w-width)//2,
0 - (height // 8)
)
self._draw.text(pos, self._title, font=font, fill=self.foreground)
return self._image

24
dibbler/cli.py Normal file
View File

@@ -0,0 +1,24 @@
import argparse
from dibbler.conf import config
parser = argparse.ArgumentParser()
parser.add_argument(
"-c",
"--config",
help="Path to the config file",
type=str,
required=False,
)
def main():
args = parser.parse_args()
config.read(args.config)
import dibbler.text_based as text_based
text_based.main()
if __name__ == "__main__":
main()

6
dibbler/conf.py Normal file
View File

@@ -0,0 +1,6 @@
# This module is supposed to act as a singleton and be filled
# with config variables by cli.py
import configparser
config = configparser.ConfigParser()

130
dibbler/helpers.py Normal file
View File

@@ -0,0 +1,130 @@
import pwd
import subprocess
import os
import signal
from sqlalchemy import or_, and_
from .models.db import *
def search_user(string, session, ignorethisflag=None):
string = string.lower()
exact_match = session.query(User).filter(or_(User.name == string, User.card == string, User.rfid == string)).first()
if exact_match:
return exact_match
user_list = session.query(User).filter(or_(User.name.ilike(f'%{string}%'),
User.card.ilike(f'%{string}%'),
User.rfid.ilike(f'%{string}%'))).all()
return user_list
def search_product(string, session, find_hidden_products=True):
if find_hidden_products:
exact_match = session.query(Product).filter(or_(Product.bar_code == string, Product.name == string)).first()
else:
exact_match = session.query(Product).filter(or_(Product.bar_code == string,
and_(Product.name == string, Product.hidden == False))).first()
if exact_match:
return exact_match
if find_hidden_products:
product_list = session.query(Product).filter(or_(Product.bar_code.ilike(f'%{string}%'),
Product.name.ilike(f'%{string}%'))).all()
else:
product_list = session.query(Product).filter(or_(Product.bar_code.ilike(f'%{string}%'),
and_(Product.name.ilike(f'%{string}%'),
Product.hidden == False))).all()
return product_list
def system_user_exists(username):
try:
pwd.getpwnam(username)
except KeyError:
return False
except UnicodeEncodeError:
return False
else:
return True
def guess_data_type(string):
if string.startswith('ntnu') and string[4:].isdigit():
return 'card'
if string.isdigit() and len(string) == 10:
return 'rfid'
if string.isdigit() and len(string) in [8,13]:
return 'bar_code'
# if string.isdigit() and len(string) > 5:
# return 'card'
if string.isalpha() and string.islower() and system_user_exists(string):
return 'username'
return None
# def retrieve_user(string, session):
# # first = session.query(User).filter(or_(User.name==string, User.card==string)).first()
# search = search_user(string,session)
# if isinstance(search,User):
# print "Found user "+search.name
# return search
# else:
# if len(search) == 0:
# print "No users found matching your search"
# return None
# if len(search) == 1:
# print "Found one user: "+list[0].name
# if confirm():
# return list[0]
# else:
# return None
# else:
# print "Found "+str(len(search))+" users:"
# return select_from_list(search)
# def confirm(prompt='Confirm? (y/n) '):
# while True:
# input = raw_input(prompt)
# if input in ["y","yes"]:
# return True
# elif input in ["n","no"]:
# return False
# else:
# print "Nonsense!"
# def select_from_list(list):
# while True:
# for i in range(len(list)):
# print i+1, " ) ", list[i].name
# choice = raw_input("Select user :\n")
# if choice in [str(x+1) for x in range(len(list))]:
# return list[int(choice)-1]
# else:
# return None
def argmax(d, all=False, value=None):
maxarg = None
maxargs = []
if value != None:
dd = d
d = {}
for key in list(dd.keys()):
d[key] = value(dd[key])
for key in list(d.keys()):
if maxarg == None or d[key] > d[maxarg]:
maxarg = key
if all:
return [k for k in list(d.keys()) if d[k] == d[maxarg]]
return maxarg
def less(string):
'''
Run less with string as input; wait until it finishes.
'''
# If we don't ignore SIGINT while running the `less` process,
# it will become a zombie when someone presses C-c.
int_handler = signal.signal(signal.SIGINT, signal.SIG_IGN)
env = dict(os.environ)
env['LESSSECURE'] = '1'
proc = subprocess.Popen('less', env=env, encoding='utf-8', stdin=subprocess.PIPE)
proc.communicate(string)
signal.signal(signal.SIGINT, int_handler)

4
dibbler/makedb.py Normal file
View File

@@ -0,0 +1,4 @@
#!/usr/bin/python
from .models.db import db
db.Base.metadata.create_all(db.engine)

View File

@@ -0,0 +1,8 @@
# -*- coding: utf-8 -*-
exit_commands = ['exit', 'abort', 'quit', 'bye', 'eat flaming death', 'q']
help_commands = ['help', '?']
context_commands = ['what', '??']
local_help_commands = ['help!', '???']
faq_commands = ['faq']
restart_commands = ['restart']

139
dibbler/menus/addstock.py Normal file
View File

@@ -0,0 +1,139 @@
from math import ceil
import sqlalchemy
from dibbler.models.db import (
Product,
Purchase,
PurchaseEntry,
Transaction,
User,
)
from .helpermenus import Menu
class AddStockMenu(Menu):
def __init__(self):
Menu.__init__(self, 'Add stock and adjust credit', uses_db=True)
self.help_text = '''
Enter what you have bought for PVVVV here, along with your user name and how
much money you're due in credits for the purchase when prompted.\n'''
self.users = []
self.users = []
self.products = {}
self.price = 0
def _execute(self):
questions = {
(False, False): 'Enter user id or a string of the form "<number> <product>"',
(False, True): 'Enter user id or more strings of the form "<number> <product>"',
(True, False): 'Enter a string of the form "<number> <product>"',
(True, True): 'Enter more strings of the form "<number> <product>", or an empty line to confirm'
}
self.users = []
self.products = {}
self.price = 0
while True:
self.print_info()
self.printc(questions[bool(self.users), bool(len(self.products))])
thing_price = 0
# Read in a 'thing' (product or user):
line = self.input_multiple(add_nonexisting=('user', 'product'), empty_input_permitted=True,
find_hidden_products=False)
if line:
(thing, amount) = line
if isinstance(thing, Product):
self.printc(f"{amount:d} of {thing.name} registered")
thing_price = self.input_int('What did you pay a piece?', 1, 100000, default=thing.price) * amount
self.price += thing_price
# once we get something in the
# purchase, we want to protect the
# user from accidentally killing it
self.exit_confirm_msg = 'Abort transaction?'
else:
if not self.complete_input():
if self.confirm('Not enough information entered. Abort transaction?', default=True):
return False
continue
break
# Add the thing to the pending adjustments:
self.add_thing_to_pending(thing, amount, thing_price)
self.perform_transaction()
def complete_input(self):
return bool(self.users) and len(self.products) and self.price
def print_info(self):
width = 6 + Product.name_length
print()
print(width * '-')
if self.price:
print(f"Amount to be credited:{self.price:>{width - 22}}")
if self.users:
print("Users to credit:")
for user in self.users:
print(f"\t{user.name}")
print()
print("Products", end="")
print("Amount".rjust(width - 8))
print(width * '-')
if len(self.products):
for product in list(self.products.keys()):
print(f"{product.name}", end="")
print(f"{self.products[product][0]}".rjust(width - len(product.name)))
print(width * '-')
def add_thing_to_pending(self, thing, amount, price):
if isinstance(thing, User):
self.users.append(thing)
elif thing in list(self.products.keys()):
print('Already added this product, adding amounts')
self.products[thing][0] += amount
self.products[thing][1] += price
else:
self.products[thing] = [amount, price]
def perform_transaction(self):
print('Did you pay a different price?')
if self.confirm('>', default=False):
self.price = self.input_int('How much did you pay?', 0, self.price, default=self.price)
description = self.input_str('Log message', length_range=(0, 50))
if description == '':
description = 'Purchased products for PVVVV, adjusted credit ' + str(self.price)
for product in self.products:
value = max(product.stock, 0) * product.price + self.products[product][1]
old_price = product.price
old_hidden = product.hidden
product.price = int(ceil(float(value) / (max(product.stock, 0) + self.products[product][0])))
product.stock = max(self.products[product][0], product.stock + self.products[product][0])
product.hidden = False
print(f"New stock for {product.name}: {product.stock:d}",
f"- New price: {product.price}" if old_price != product.price else "",
"- Removed hidden status" if old_hidden != product.hidden else "")
purchase = Purchase()
for user in self.users:
Transaction(user, purchase=purchase, amount=-self.price, description=description)
for product in self.products:
PurchaseEntry(purchase, product, -self.products[product][0])
purchase.perform_soft_purchase(-self.price, round_up=False)
self.session.add(purchase)
try:
self.session.commit()
print("Success! Transaction performed:")
# self.print_info()
for user in self.users:
print(f"User {user.name}'s credit is now {user.credit:d}")
except sqlalchemy.exc.SQLAlchemyError as e:
print(f'Could not perform transaction: {e}')

217
dibbler/menus/buymenu.py Normal file
View File

@@ -0,0 +1,217 @@
import sqlalchemy
from dibbler.conf import config
from dibbler.models.db import (
Product,
Purchase,
PurchaseEntry,
Transaction,
User,
)
from .helpermenus import Menu
class BuyMenu(Menu):
def __init__(self, session=None):
Menu.__init__(self, 'Buy', uses_db=True)
if session:
self.session = session
self.superfast_mode = False
self.help_text = '''
Each purchase may contain one or more products and one or more buyers.
Enter products (by name or bar code) and buyers (by name or bar code)
in any order. The information gathered so far is displayed after each
addition, and you can type 'what' at any time to redisplay it.
When finished, write an empty line to confirm the purchase.\n'''
@staticmethod
def credit_check(user):
"""
:param user:
:type user: User
:rtype: boolean
"""
assert isinstance(user, User)
return user.credit > config.getint('limits', 'low_credit_warning_limit')
def low_credit_warning(self, user, timeout=False):
assert isinstance(user, User)
print("***********************************************************************")
print("***********************************************************************")
print("")
print("$$\ $$\ $$$$$$\ $$$$$$$\ $$\ $$\ $$$$$$\ $$\ $$\ $$$$$$\\")
print("$$ | $\ $$ |$$ __$$\ $$ __$$\ $$$\ $$ |\_$$ _|$$$\ $$ |$$ __$$\\")
print("$$ |$$$\ $$ |$$ / $$ |$$ | $$ |$$$$\ $$ | $$ | $$$$\ $$ |$$ / \__|")
print("$$ $$ $$\$$ |$$$$$$$$ |$$$$$$$ |$$ $$\$$ | $$ | $$ $$\$$ |$$ |$$$$\\")
print("$$$$ _$$$$ |$$ __$$ |$$ __$$< $$ \$$$$ | $$ | $$ \$$$$ |$$ |\_$$ |")
print("$$$ / \$$$ |$$ | $$ |$$ | $$ |$$ |\$$$ | $$ | $$ |\$$$ |$$ | $$ |")
print("$$ / \$$ |$$ | $$ |$$ | $$ |$$ | \$$ |$$$$$$\ $$ | \$$ |\$$$$$$ |")
print("\__/ \__|\__| \__|\__| \__|\__| \__|\______|\__| \__| \______/")
print("")
print("***********************************************************************")
print("***********************************************************************")
print("")
print(f"USER {user.name} HAS LOWER CREDIT THAN {config.getint('limits', 'low_credit_warning_limit'):d}.")
print("THIS PURCHASE WILL CHARGE YOUR CREDIT TWICE AS MUCH.")
print("CONSIDER PUTTING MONEY IN THE BOX TO AVOID THIS.")
print("")
print("Do you want to continue with this purchase?")
if timeout:
print("THIS PURCHASE WILL AUTOMATICALLY BE PERFORMED IN 3 MINUTES!")
return self.confirm(prompt=">", default=True, timeout=180)
else:
return self.confirm(prompt=">", default=True)
def add_thing_to_purchase(self, thing, amount=1):
if isinstance(thing, User):
if thing.is_anonymous():
print('---------------------------------------------')
print('| You are now purchasing as the user anonym.|')
print('| You have to put money in the anonym-jar. |')
print('---------------------------------------------')
if not self.credit_check(thing):
if self.low_credit_warning(user=thing, timeout=self.superfast_mode):
Transaction(thing, purchase=self.purchase, penalty=2)
else:
return False
else:
Transaction(thing, purchase=self.purchase)
elif isinstance(thing, Product):
if self.purchase.entries:
for entry in self.purchase.entries:
if entry.product == thing:
entry.amount += amount
return True
PurchaseEntry(self.purchase, thing, amount)
return True
def _execute(self, initial_contents=None):
self.print_header()
self.purchase = Purchase()
self.exit_confirm_msg = None
self.superfast_mode = False
if initial_contents is None:
initial_contents = []
for thing, num in initial_contents:
self.add_thing_to_purchase(thing, num)
def is_product(candidate):
return isinstance(candidate[0], Product)
if len(initial_contents) > 0 and all(map(is_product, initial_contents)):
self.superfast_mode = True
print('***********************************************')
print('****** Buy menu is in SUPERFASTmode[tm]! ******')
print('*** The purchase will be stored immediately ***')
print('*** when you enter a user. ***')
print('***********************************************')
while True:
self.print_purchase()
self.printc({(False, False): 'Enter user or product identification',
(False, True): 'Enter user identification or more products',
(True, False): 'Enter product identification or more users',
(True, True): 'Enter more products or users, or an empty line to confirm'
}[(len(self.purchase.transactions) > 0,
len(self.purchase.entries) > 0)])
# Read in a 'thing' (product or user):
line = self.input_multiple(add_nonexisting=('user', 'product'), empty_input_permitted=True,
find_hidden_products=False)
if line is not None:
thing, num = line
else:
thing, num = None, 0
# Possibly exit from the menu:
if thing is None:
if not self.complete_input():
if self.confirm('Not enough information entered. Abort purchase?', default=True):
return False
continue
break
else:
# once we get something in the
# purchase, we want to protect the
# user from accidentally killing it
self.exit_confirm_msg = 'Abort purchase?'
# Add the thing to our purchase object:
if not self.add_thing_to_purchase(thing, amount=num):
continue
# In super-fast mode, we complete the purchase once we get a user:
if self.superfast_mode and isinstance(thing, User):
break
self.purchase.perform_purchase()
self.session.add(self.purchase)
try:
self.session.commit()
except sqlalchemy.exc.SQLAlchemyError as e:
print(f'Could not store purchase: {e}')
else:
print('Purchase stored.')
self.print_purchase()
for t in self.purchase.transactions:
if not t.user.is_anonymous():
print(f"User {t.user.name}'s credit is now {t.user.credit:d} kr")
if t.user.credit < config.getint('limits', 'low_credit_warning_limit'):
print(f'USER {t.user.name} HAS LOWER CREDIT THAN {config.getint("limits", "low_credit_warning_limit"):d},',
'AND SHOULD CONSIDER PUTTING SOME MONEY IN THE BOX.')
# Superfast mode skips a linebreak for some reason.
if self.superfast_mode:
print("")
return True
def complete_input(self):
return self.purchase.is_complete()
def format_purchase(self):
self.purchase.set_price()
transactions = self.purchase.transactions
entries = self.purchase.entries
if len(transactions) == 0 and len(entries) == 0:
return None
string = 'Purchase:'
string += '\n buyers: '
if len(transactions) == 0:
string += '(empty)'
else:
string += ', '.join(
[t.user.name + ("*" if not self.credit_check(t.user) else "") for t in transactions])
string += '\n products: '
if len(entries) == 0:
string += '(empty)'
else:
string += "\n "
string += '\n '.join([f'{e.amount:d}x {e.product.name} ({e.product.price:d} kr)' for e in entries])
if len(transactions) > 1:
string += f'\n price per person: {self.purchase.price_per_transaction():d} kr'
if any(t.penalty > 1 for t in transactions):
# TODO: Use penalty multiplier instead of 2
string += f' *({self.purchase.price_per_transaction() * 2:d} kr)'
string += f'\n total price: {self.purchase.price:d} kr'
if any(t.penalty > 1 for t in transactions):
total = sum(self.purchase.price_per_transaction() * t.penalty for t in transactions)
string += f'\n *total with penalty: {total} kr'
return string
def print_purchase(self):
info = self.format_purchase()
if info is not None:
self.set_context(info)

187
dibbler/menus/editing.py Normal file
View File

@@ -0,0 +1,187 @@
import sqlalchemy
from dibbler.models.db import User, Product
from .helpermenus import Menu, Selector
__all__ = ["AddUserMenu", "AddProductMenu", "EditProductMenu", "AdjustStockMenu", "CleanupStockMenu", "EditUserMenu"]
class AddUserMenu(Menu):
def __init__(self):
Menu.__init__(self, 'Add user', uses_db=True)
def _execute(self):
self.print_header()
username = self.input_str('Username (should be same as PVV username)', regex=User.name_re, length_range=(1, 10))
cardnum = self.input_str('Card number (optional)', regex=User.card_re, length_range=(0, 10))
cardnum = cardnum.lower()
rfid = self.input_str('RFID (optional)', regex=User.rfid_re, length_range=(0, 10))
user = User(username, cardnum, rfid)
self.session.add(user)
try:
self.session.commit()
print(f'User {username} stored')
except sqlalchemy.exc.IntegrityError as e:
print(f'Could not store user {username}: {e}')
self.pause()
class EditUserMenu(Menu):
def __init__(self):
Menu.__init__(self, 'Edit user', uses_db=True)
self.help_text = '''
The only editable part of a user is its card number and rfid.
First select an existing user, then enter a new card number for that
user, then rfid (write an empty line to remove the card number or rfid).
'''
def _execute(self):
self.print_header()
user = self.input_user('User')
self.printc(f'Editing user {user.name}')
card_str = f'"{user.card}"' if user.card is not None else 'empty'
user.card = self.input_str(f'Card number (currently {card_str})',
regex=User.card_re, length_range=(0, 10),
empty_string_is_none=True)
if user.card:
user.card = user.card.lower()
rfid_str = f'"{user.rfid}"' if user.rfid is not None else 'empty'
user.rfid = self.input_str(f'RFID (currently {rfid_str})',
regex=User.rfid_re, length_range=(0, 10),
empty_string_is_none=True)
try:
self.session.commit()
print(f'User {user.name} stored')
except sqlalchemy.exc.SQLAlchemyError as e:
print(f'Could not store user {user.name}: {e}')
self.pause()
class AddProductMenu(Menu):
def __init__(self):
Menu.__init__(self, 'Add product', uses_db=True)
def _execute(self):
self.print_header()
bar_code = self.input_str('Bar code', regex=Product.bar_code_re, length_range=(8, 13))
name = self.input_str('Name', regex=Product.name_re, length_range=(1, Product.name_length))
price = self.input_int('Price', 1, 100000)
product = Product(bar_code, name, price)
self.session.add(product)
try:
self.session.commit()
print(f'Product {name} stored')
except sqlalchemy.exc.SQLAlchemyError as e:
print(f'Could not store product {name}: {e}')
self.pause()
class EditProductMenu(Menu):
def __init__(self):
Menu.__init__(self, 'Edit product', uses_db=True)
def _execute(self):
self.print_header()
product = self.input_product('Product')
self.printc(f'Editing product {product.name}')
while True:
selector = Selector(f'Do what with {product.name}?',
items=[('name', 'Edit name'),
('price', 'Edit price'),
('barcode', 'Edit barcode'),
('hidden', 'Edit hidden status'),
('store', 'Store')])
what = selector.execute()
if what == 'name':
product.name = self.input_str('Name', default=product.name, regex=Product.name_re,
length_range=(1, product.name_length))
elif what == 'price':
product.price = self.input_int('Price', 1, 100000, default=product.price)
elif what == 'barcode':
product.bar_code = self.input_str('Bar code', default=product.bar_code, regex=Product.bar_code_re,
length_range=(8, 13))
elif what == 'hidden':
product.hidden = self.confirm(f'Hidden(currently {product.hidden})', default=False)
elif what == 'store':
try:
self.session.commit()
print(f'Product {product.name} stored')
except sqlalchemy.exc.SQLAlchemyError as e:
print(f'Could not store product {product.name}: {e}')
self.pause()
return
elif what is None:
print('Edit aborted')
return
else:
print('What what?')
class AdjustStockMenu(Menu):
def __init__(self):
Menu.__init__(self, 'Adjust stock', uses_db=True)
def _execute(self):
self.print_header()
product = self.input_product('Product')
print(f'The stock of this product is: {product.stock:d}')
print('Write the number of products you have added to the stock')
print('Alternatively, correct the stock for any mistakes')
add_stock = self.input_int('Added stock', -1000, 1000, zero_allowed=False)
if add_stock > 0:
print(f'You added {add_stock:d} to the stock of {product}')
else:
print(f'You removed {add_stock:d} from the stock of {product}')
product.stock += add_stock
try:
self.session.commit()
print('Stock is now stored')
self.pause()
except sqlalchemy.exc.SQLAlchemyError as e:
print(f'Could not store stock: {e}')
self.pause()
return
print(f'The stock is now {product.stock:d}')
class CleanupStockMenu(Menu):
def __init__(self):
Menu.__init__(self, 'Stock Cleanup', uses_db=True)
def _execute(self):
self.print_header()
products = self.session.query(Product).filter(Product.stock != 0).all()
print("Every product in stock will be printed.")
print("Entering no value will keep current stock or set it to 0 if it is negative.")
print("Entering a value will set current stock to that value.")
print("Press enter to begin.")
self.pause()
changed_products = []
for product in products:
oldstock = product.stock
product.stock = self.input_int(product.name, 0, 10000, default=max(0, oldstock))
self.session.add(product)
if oldstock != product.stock:
changed_products.append((product, oldstock))
try:
self.session.commit()
print('New stocks are now stored.')
self.pause()
except sqlalchemy.exc.SQLAlchemyError as e:
print(f'Could not store stock: {e}')
self.pause()
return
for p in changed_products:
print(p[0].name, ".", p[1], "->", p[0].stock)

108
dibbler/menus/faq.py Normal file
View File

@@ -0,0 +1,108 @@
# -*- coding: utf-8 -*-
from .helpermenus import MessageMenu, Menu
class FAQMenu(Menu):
def __init__(self):
Menu.__init__(self, 'Frequently Asked Questions')
self.items = [MessageMenu('What is the meaning with this program?',
'''
We want to avoid keeping lots of cash in PVVVV's money box and to
make it easy to pay for stuff without using money. (Without using
money each time, that is. You do of course have to pay for the things
you buy eventually).
Dibbler stores a "credit" amount for each user. When you register a
purchase in Dibbler, this amount is decreased. To increase your
credit, purchase products for dibbler, and register them using "Add
stock and adjust credit".
Alternatively, add money to the money box and use "Adjust credit" to
tell Dibbler about it.
'''),
MessageMenu('Can I still pay for stuff using cash?',
'''
Please put money in the money box and use "Adjust Credit" so that
dibbler can keep track of credit and purchases.'''),
MessageMenu('How do I exit from a submenu/dialog/thing?',
'Type "exit", "q", or ^d.'),
MessageMenu('What does "." mean?',
'''
The "." character, known as "full stop" or "period", is most often
used to indicate the end of a sentence.
It is also used by Dibbler to indicate that the program wants you to
read some text before continuing. Whenever some output ends with a
line containing only a period, you should read the lines above and
then press enter to continue.
'''),
MessageMenu('Why is the user interface so terribly unintuitive?',
'''
Answer #1: It is not.
Answer #2: We are trying to compete with PVV's microwave oven in
userfriendliness.
Answer #3: YOU are unintuitive.
'''),
MessageMenu('Why is there no help command?',
'There is. Have you tried typing "help"?'),
MessageMenu('Where are the easter eggs? I tried saying "moo", but nothing happened.',
'Don\'t say "moo".'),
MessageMenu('Why does the program speak English when all the users are Norwegians?',
'Godt spørsmål. Det virket sikkert som en god idé der og da.'),
MessageMenu('Why does the screen have strange colours?',
'''
Type "c" on the main menu to change the colours of the display, or
"cs" if you are a boring person.
'''),
MessageMenu('I found a bug; is there a reward?',
'''
No.
But if you are certain that it is a bug, not a feature, then you
should fix it (or better: force someone else to do it).
Follow this procedure:
1. Check out the Dibbler code: https://github.com/Programvareverkstedet/dibbler
2. Fix the bug.
3. Check that the program still runs (and, preferably, that the bug is
in fact fixed).
4. Commit.
5. Update the running copy from svn:
$ su -
# su -l -s /bin/bash pvvvv
$ cd dibbler
$ git pull
6. Type "restart" in Dibbler to replace the running process by a new
one using the updated files.
'''),
MessageMenu('My question isn\'t listed here; what do I do?',
'''
DON'T PANIC.
Follow this procedure:
1. Ask someone (or read the source code) and get an answer.
2. Check out the Dibbler code: https://github.com/Programvareverkstedet/dibbler
3. Add your question (with answer) to the FAQ and commit.
4. Update the running copy from svn:
$ su -
# su -l -s /bin/bash pvvvv
$ cd dibbler
$ git pull
5. Type "restart" in Dibbler to replace the running process by a new
one using the updated files.
''')]

View File

@@ -0,0 +1,504 @@
# -*- coding: utf-8 -*-
import re
import sys
from select import select
from dibbler.models.db import User, Session
from dibbler.helpers import (
search_user,
search_product,
guess_data_type,
argmax,
)
from . import (
context_commands,
local_help_commands,
help_commands,
exit_commands,
)
class ExitMenu(Exception):
pass
class Menu(object):
def __init__(self, name, items=None, prompt=None, end_prompt="> ",
return_index=True,
exit_msg=None, exit_confirm_msg=None, exit_disallowed_msg=None,
help_text=None, uses_db=False):
self.name = name
self.items = items if items is not None else []
self.prompt = prompt
self.end_prompt = end_prompt
self.return_index = return_index
self.exit_msg = exit_msg
self.exit_confirm_msg = exit_confirm_msg
self.exit_disallowed_msg = exit_disallowed_msg
self.help_text = help_text
self.context = None
self.uses_db = uses_db
self.session = None
def exit_menu(self):
if self.exit_disallowed_msg is not None:
print(self.exit_disallowed_msg)
return
if self.exit_confirm_msg is not None:
if not self.confirm(self.exit_confirm_msg, default=True):
return
raise ExitMenu()
def at_exit(self):
if self.exit_msg:
print(self.exit_msg)
def set_context(self, string, display=True):
self.context = string
if self.context is not None and display:
print(self.context)
def add_to_context(self, string):
self.context += string
def printc(self, string):
print(string)
if self.context is None:
self.context = string
else:
self.context += '\n' + string
def show_context(self):
print(self.header())
if self.context is not None:
print(self.context)
def item_is_submenu(self, i):
return isinstance(self.items[i], Menu)
def item_name(self, i):
if self.item_is_submenu(i):
return self.items[i].name
elif isinstance(self.items[i], tuple):
return self.items[i][1]
else:
return self.items[i]
def item_value(self, i):
if isinstance(self.items[i], tuple):
return self.items[i][0]
if self.return_index:
return i
return self.items[i]
def input_str(self, prompt=None, end_prompt=None, regex=None, length_range=(None, None),
empty_string_is_none=False, timeout=None, default=None):
if prompt is None:
prompt = self.prompt if self.prompt is not None else ""
if default is not None:
prompt += f" [{default}]"
if end_prompt is not None:
prompt += end_prompt
elif self.end_prompt is not None:
prompt += self.end_prompt
else:
prompt += " "
while True:
try:
if timeout:
# assuming line buffering
sys.stdout.write(prompt)
sys.stdout.flush()
rlist, _, _ = select([sys.stdin], [], [], timeout)
if not rlist:
# timeout occurred, simulate empty line
result = ''
else:
result = input(prompt).strip()
else:
result = input(prompt).strip()
except EOFError:
print('quit')
self.exit_menu()
continue
if result in exit_commands:
self.exit_menu()
continue
if result in help_commands:
self.general_help()
continue
if result in local_help_commands:
self.local_help()
continue
if result in context_commands:
self.show_context()
continue
if self.special_input_options(result):
continue
if empty_string_is_none and result == '':
return None
if default is not None and result == '':
return default
if regex is not None and not re.match(regex + '$', result):
print(f'Value must match regular expression "{regex}"')
continue
if length_range != (None, None):
length = len(result)
if ((length_range[0] and length < length_range[0]) or (length_range[1] and length > length_range[1])):
if length_range[0] and length_range[1]:
print(f'Value must have length in range [{length_range[0]:d}, {length_range[1]:d}]')
elif length_range[0]:
print(f'Value must have length at least {length_range[0]:d}')
else:
print(f'Value must have length at most {length_range[1]:d}')
continue
return result
def special_input_options(self, result):
"""
Handles special, magic input for input_str
Override this in subclasses to implement magic menu
choices. Return True if str was some valid magic menu
choice, False otherwise.
"""
return False
def special_input_choice(self, in_str):
"""
Handle choices which are not simply menu items.
Override this in subclasses to implement magic menu
choices. Return True if str was some valid magic menu
choice, False otherwise.
"""
return False
def input_choice(self, number_of_choices, prompt=None, end_prompt=None):
while True:
result = self.input_str(prompt, end_prompt)
if result == '':
print('Please enter something')
else:
if result.isdigit():
choice = int(result)
if choice == 0 and 10 <= number_of_choices:
return 10
if 0 < choice <= number_of_choices:
return choice
if not self.special_input_choice(result):
self.invalid_menu_choice(result)
def invalid_menu_choice(self, in_str):
print('Please enter a valid choice.')
def input_int(self, prompt=None, minimum=None, maximum=None, null_allowed=False, zero_allowed=True, default=None):
if minimum is not None and maximum is not None:
end_prompt = f"({minimum}-{maximum})>"
elif minimum is not None:
end_prompt = f"(>{minimum})>"
elif maximum is not None:
end_prompt = f"(<{maximum})>"
else:
end_prompt = ""
while True:
result = self.input_str(prompt + end_prompt, default=default)
if result == '' and null_allowed:
return False
try:
value = int(result)
if minimum is not None and value < minimum:
print(f'Value must be at least {minimum:d}')
continue
if maximum is not None and value > maximum:
print(f'Value must be at most {maximum:d}')
continue
if not zero_allowed and value == 0:
print("Value cannot be zero")
continue
return value
except ValueError:
print("Please enter an integer")
def input_user(self, prompt=None, end_prompt=None):
user = None
while user is None:
user = self.retrieve_user(self.input_str(prompt, end_prompt))
return user
def retrieve_user(self, search_str):
return self.search_ui(search_user, search_str, 'user')
def input_product(self, prompt=None, end_prompt=None):
product = None
while product is None:
product = self.retrieve_product(self.input_str(prompt, end_prompt))
return product
def retrieve_product(self, search_str):
return self.search_ui(search_product, search_str, 'product')
def input_thing(self, prompt=None, end_prompt=None, permitted_things=('user', 'product'),
add_nonexisting=(), empty_input_permitted=False, find_hidden_products=True):
result = None
while result is None:
search_str = self.input_str(prompt, end_prompt)
if search_str == '' and empty_input_permitted:
return None
result = self.search_for_thing(search_str, permitted_things, add_nonexisting, find_hidden_products)
return result
def input_multiple(self, prompt=None, end_prompt=None, permitted_things=('user', 'product'),
add_nonexisting=(), empty_input_permitted=False, find_hidden_products=True):
result = None
num = 0
while result is None:
search_str = self.input_str(prompt, end_prompt)
search_lst = search_str.split(" ")
if search_str == '' and empty_input_permitted:
return None
else:
result = self.search_for_thing(search_str, permitted_things, add_nonexisting, find_hidden_products)
num = 1
if (result is None) and (len(search_lst) > 1):
print('Interpreting input as "<number> <product>"')
try:
num = int(search_lst[0])
result = self.search_for_thing(" ".join(search_lst[1:]), permitted_things, add_nonexisting,
find_hidden_products)
# Her kan det legges inn en except ValueError,
# men da blir det fort mye plaging av brukeren
except Exception as e:
print(e)
return result, num
def search_for_thing(self, search_str, permitted_things=('user', 'product'),
add_non_existing=(), find_hidden_products=True):
search_fun = {'user': search_user,
'product': search_product}
results = {}
result_values = {}
for thing in permitted_things:
results[thing] = search_fun[thing](search_str, self.session, find_hidden_products)
result_values[thing] = self.search_result_value(results[thing])
selected_thing = argmax(result_values)
if not results[selected_thing]:
thing_for_type = {'card': 'user', 'username': 'user',
'bar_code': 'product', 'rfid': 'rfid'}
type_guess = guess_data_type(search_str)
if type_guess is not None and thing_for_type[type_guess] in add_non_existing:
return self.search_add(search_str)
# print('No match found for "%s".' % search_str)
return None
return self.search_ui2(search_str, results[selected_thing], selected_thing)
@staticmethod
def search_result_value(result):
if result is None:
return 0
if not isinstance(result, list):
return 3
if len(result) == 0:
return 0
if len(result) == 1:
return 2
return 1
def search_add(self, string):
type_guess = guess_data_type(string)
if type_guess == 'username':
print(f'"{string}" looks like a username, but no such user exists.')
if self.confirm(f'Create user {string}?'):
user = User(string, None)
self.session.add(user)
return user
return None
if type_guess == 'card':
selector = Selector(f'"{string}" looks like a card number, but no user with that card number exists.',
[('create', f'Create user with card number {string}'),
('set', f'Set card number of an existing user to {string}')])
selection = selector.execute()
if selection == 'create':
username = self.input_str('Username for new user (should be same as PVV username)',
User.name_re, (1, 10))
user = User(username, string)
self.session.add(user)
return user
if selection == 'set':
user = self.input_user('User to set card number for')
old_card = user.card
user.card = string
print(f'Card number of {user.name} set to {string} (was {old_card})')
return user
return None
if type_guess == 'bar_code':
print(f'"{string}" looks like the bar code for a product, but no such product exists.')
return None
def search_ui(self, search_fun, search_str, thing):
result = search_fun(search_str, self.session)
return self.search_ui2(search_str, result, thing)
def search_ui2(self, search_str, result, thing):
if not isinstance(result, list):
return result
if len(result) == 0:
print(f'No {thing}s matching "{search_str}"')
return None
if len(result) == 1:
msg = f'One {thing} matching "{search_str}": {str(result[0])}. Use this?'
if self.confirm(msg, default=True):
return result[0]
return None
limit = 9
if len(result) > limit:
select_header = f'{len(result):d} {thing}s matching "{search_str}"; showing first {limit:d}'
select_items = result[:limit]
else:
select_header = f'{len(result):d} {thing}s matching "{search_str}"'
select_items = result
selector = Selector(select_header, items=select_items,
return_index=False)
return selector.execute()
@staticmethod
def confirm(prompt, end_prompt=None, default=None, timeout=None):
return ConfirmMenu(prompt, end_prompt=None, default=default, timeout=timeout).execute()
def header(self):
return f"[{self.name}]"
def print_header(self):
print("")
print(self.header())
def pause(self):
self.input_str('.', end_prompt="")
@staticmethod
def general_help():
print('''
DIBBLER HELP
The following commands are recognized (almost) everywhere:
help, ? -- display this help
what, ?? -- redisplay the current context
help!, ??? -- display context-specific help (if any)
faq -- display frequently asked questions (with answers)
exit, quit, etc. -- exit from the current menu
When prompted for a user, you can type (parts of) the user name or
card number. When prompted for a product, you can type (parts of) the
product name or barcode.
About payment and "credit": When paying for something, use either
Dibbler or the good old money box -- never both at the same time.
Dibbler keeps track of a "credit" for each user, which is the amount
of money PVVVV owes the user. This value decreases with the
appropriate amount when you register a purchase, and you may increase
it by putting money in the box and using the "Adjust credit" menu.
''')
def local_help(self):
if self.help_text is None:
print('no help here')
else:
print('')
print(f'Help for {self.header()}:')
print(self.help_text)
def execute(self, **kwargs):
self.set_context(None)
try:
if self.uses_db and not self.session:
self.session = Session()
return self._execute(**kwargs)
except ExitMenu:
self.at_exit()
return None
finally:
if self.session is not None:
self.session.close()
self.session = None
def _execute(self, **kwargs):
while True:
self.print_header()
self.set_context(None)
if len(self.items) == 0:
self.printc('(empty menu)')
self.pause()
return None
for i in range(len(self.items)):
length = len(str(len(self.items)))
self.printc(f"{i + 1:>{length}} ) {self.item_name(i)}")
item_i = self.input_choice(len(self.items)) - 1
if self.item_is_submenu(item_i):
self.items[item_i].execute()
else:
return self.item_value(item_i)
class MessageMenu(Menu):
def __init__(self, name, message, pause_after_message=True):
Menu.__init__(self, name)
self.message = message.strip()
self.pause_after_message = pause_after_message
def _execute(self):
self.print_header()
print('')
print(self.message)
if self.pause_after_message:
self.pause()
class ConfirmMenu(Menu):
def __init__(self, prompt='confirm? ', end_prompt=": ", default=None, timeout=0):
Menu.__init__(self, 'question', prompt=prompt, end_prompt=end_prompt,
exit_disallowed_msg='Please answer yes or no')
self.default = default
self.timeout = timeout
def _execute(self):
options = {True: '[y]/n', False: 'y/[n]', None: 'y/n'}[self.default]
while True:
result = self.input_str(f'{self.prompt} ({options})', end_prompt=": ", timeout=self.timeout)
result = result.lower().strip()
if result in ['y', 'yes']:
return True
elif result in ['n', 'no']:
return False
elif self.default is not None and result == '':
return self.default
else:
print('Please answer yes or no')
class Selector(Menu):
def __init__(self, name, items=None, prompt='select', return_index=True, exit_msg=None, exit_confirm_msg=None,
help_text=None):
if items is None:
items = []
Menu.__init__(self, name, items, prompt, return_index=return_index, exit_msg=exit_msg)
def header(self):
return self.name
def print_header(self):
print(self.header())
def local_help(self):
if self.help_text is None:
print('This is a selection menu. Enter one of the listed numbers, or')
print('\'exit\' to go out and do something else.')
else:
print('')
print(f'Help for selector ({self.name}):')
print(self.help_text)

60
dibbler/menus/mainmenu.py Normal file
View File

@@ -0,0 +1,60 @@
# -*- coding: utf-8 -*-
import os
import random
import sys
from dibbler.models.db import Session
from . import faq_commands, restart_commands
from .buymenu import BuyMenu
from .faq import FAQMenu
from .helpermenus import Menu
def restart():
# Does not work if the script is not executable, or if it was
# started by searching $PATH.
os.execv(sys.argv[0], sys.argv)
class MainMenu(Menu):
def special_input_choice(self, in_str):
mv = in_str.split()
if len(mv) == 2 and mv[0].isdigit():
num = int(mv[0])
item_name = mv[1]
else:
num = 1
item_name = in_str
buy_menu = BuyMenu(Session())
thing = buy_menu.search_for_thing(item_name, find_hidden_products=False)
if thing:
buy_menu.execute(initial_contents=[(thing, num)])
self.show_context()
return True
return False
def special_input_options(self, result):
if result in faq_commands:
FAQMenu().execute()
return True
if result in restart_commands:
if self.confirm('Restart Dibbler?'):
restart()
pass
return True
elif result == 'c':
os.system('echo -e "\033[' + str(random.randint(40, 49)) + ';' + str(random.randint(30, 37)) + ';5m"')
os.system('clear')
self.show_context()
return True
elif result == 'cs':
os.system('echo -e "\033[0m"')
os.system('clear')
self.show_context()
return True
return False
def invalid_menu_choice(self, in_str):
print(self.show_context())

202
dibbler/menus/miscmenus.py Normal file
View File

@@ -0,0 +1,202 @@
import sqlalchemy
from dibbler.conf import config
from dibbler.models.db import Transaction, Product, User
from dibbler.helpers import less
from .helpermenus import Menu, Selector
class TransferMenu(Menu):
def __init__(self):
Menu.__init__(self, 'Transfer credit between users',
uses_db=True)
def _execute(self):
self.print_header()
amount = self.input_int('Transfer amount', 1, 100000)
self.set_context(f'Transferring {amount:d} kr', display=False)
user1 = self.input_user('From user')
self.add_to_context(f' from {user1.name}')
user2 = self.input_user('To user')
self.add_to_context(f' to {user2.name}')
comment = self.input_str('Comment')
self.add_to_context(f' (comment) {user2.name}')
t1 = Transaction(user1, amount,
f'transfer to {user2.name} "{comment}"')
t2 = Transaction(user2, -amount,
f'transfer from {user1.name} "{comment}"')
t1.perform_transaction()
t2.perform_transaction()
self.session.add(t1)
self.session.add(t2)
try:
self.session.commit()
print(f"Transferred {amount:d} kr from {user1} to {user2}")
print(f"User {user1}'s credit is now {user1.credit:d} kr")
print(f"User {user2}'s credit is now {user2.credit:d} kr")
print(f"Comment: {comment}")
except sqlalchemy.exc.SQLAlchemyError as e:
print(f'Could not perform transfer: {e}')
# self.pause()
class ShowUserMenu(Menu):
def __init__(self):
Menu.__init__(self, 'Show user', uses_db=True)
def _execute(self):
self.print_header()
user = self.input_user('User name, card number or RFID')
print(f'User name: {user.name}')
print(f'Card number: {user.card}')
print(f'RFID: {user.rfid}')
print(f'Credit: {user.credit} kr')
selector = Selector(f'What do you want to know about {user.name}?',
items=[('transactions', 'Recent transactions (List of last ' + str(
config.getint('limits', 'user_recent_transaction_limit')) + ')'),
('products', f'Which products {user.name} has bought, and how many'),
('transactions-all', 'Everything (List of all transactions)')])
what = selector.execute()
if what == 'transactions':
self.print_transactions(user, config.getint('limits', 'user_recent_transaction_limit'))
elif what == 'products':
self.print_purchased_products(user)
elif what == 'transactions-all':
self.print_transactions(user)
else:
print('What what?')
@staticmethod
def print_transactions(user, limit=None):
num_trans = len(user.transactions)
if limit is None:
limit = num_trans
if num_trans <= limit:
string = f"{user.name}'s transactions ({num_trans:d}):\n"
else:
string = f"{user.name}'s transactions ({num_trans:d}, showing only last {limit:d}):\n"
for t in user.transactions[-1:-limit - 1:-1]:
string += f" * {t.time.isoformat(' ')}: {'in' if t.amount < 0 else 'out'} {abs(t.amount)} kr, "
if t.purchase:
products = []
for entry in t.purchase.entries:
if abs(entry.amount) != 1:
amount = f"{abs(entry.amount)}x "
else:
amount = ""
product = f"{amount}{entry.product.name}"
products.append(product)
string += 'purchase ('
string += ', '.join(products)
string += ')'
if t.penalty > 1:
string += f' * {t.penalty:d}x penalty applied'
else:
string += t.description
string += '\n'
less(string)
@staticmethod
def print_purchased_products(user):
products = []
for ref in user.products:
product = ref.product
count = ref.count
if count > 0:
products.append((product, count))
num_products = len(products)
if num_products == 0:
print('No products purchased yet')
else:
text = ''
text += 'Products purchased:\n'
for product, count in products:
text += f'{product.name:<47} {count:>3}\n'
less(text)
class UserListMenu(Menu):
def __init__(self):
Menu.__init__(self, 'User list', uses_db=True)
def _execute(self):
self.print_header()
user_list = self.session.query(User).all()
total_credit = self.session.query(sqlalchemy.func.sum(User.credit)).first()[0]
line_format = '%-12s | %6s\n'
hline = '---------------------\n'
text = ''
text += line_format % ('username', 'credit')
text += hline
for user in user_list:
text += line_format % (user.name, user.credit)
text += hline
text += line_format % ('total credit', total_credit)
less(text)
class AdjustCreditMenu(Menu):
def __init__(self):
Menu.__init__(self, 'Adjust credit', uses_db=True)
def _execute(self):
self.print_header()
user = self.input_user('User')
print(f"User {user.name}'s credit is {user.credit:d} kr")
self.set_context(f'Adjusting credit for user {user.name}', display=False)
print('(Note on sign convention: Enter a positive amount here if you have')
print('added money to the PVVVV money box, a negative amount if you have')
print('taken money from it)')
amount = self.input_int('Add amount', -100000, 100000)
print('(The "log message" will show up in the transaction history in the')
print('"Show user" menu. It is not necessary to enter a message, but it')
print('might be useful to help you remember why you adjusted the credit)')
description = self.input_str('Log message', length_range=(0, 50))
if description == '':
description = 'manually adjusted credit'
transaction = Transaction(user, -amount, description)
transaction.perform_transaction()
self.session.add(transaction)
try:
self.session.commit()
print(f"User {user.name}'s credit is now {user.credit:d} kr")
except sqlalchemy.exc.SQLAlchemyError as e:
print(f'Could not store transaction: {e}')
# self.pause()
class ProductListMenu(Menu):
def __init__(self):
Menu.__init__(self, 'Product list', uses_db=True)
def _execute(self):
self.print_header()
text = ''
product_list = self.session.query(Product).filter(Product.hidden.is_(False)).order_by(Product.stock.desc())
total_value = 0
for p in product_list:
total_value += p.price * p.stock
line_format = '%-15s | %5s | %-' + str(Product.name_length) + 's | %5s \n'
text += line_format % ('bar code', 'price', 'name', 'stock')
text += 78 * '-' + '\n'
for p in product_list:
text += line_format % (p.bar_code, p.price, p.name, p.stock)
text += 78 * '-' + '\n'
text += line_format % ('Total value', total_value, '', '',)
less(text)
class ProductSearchMenu(Menu):
def __init__(self):
Menu.__init__(self, 'Product search', uses_db=True)
def _execute(self):
self.print_header()
self.set_context('Enter (part of) product name or bar code')
product = self.input_product()
print('Result: %s, price: %d kr, bar code: %s, stock: %d, hidden: %s' % (product.name, product.price,
product.bar_code, product.stock,
("Y" if product.hidden else "N")))
# self.pause()

View File

@@ -0,0 +1,45 @@
import re
from dibbler.conf import config
from dibbler.models.db import Product, User
from dibbler.printer_helpers import print_bar_code, print_name_label
from .helpermenus import Menu
class PrintLabelMenu(Menu):
def __init__(self):
Menu.__init__(self, 'Print a label', uses_db=True)
self.help_text = '''
Prints out a product bar code on the printer
Put it up somewhere in the vicinity.
'''
def _execute(self):
self.print_header()
thing = self.input_thing('Product/User')
if isinstance(thing, Product):
if re.match(r"^[0-9]{13}$", thing.bar_code):
bar_type = "ean13"
elif re.match(r"^[0-9]{8}$", thing.bar_code):
bar_type = "ean8"
else:
bar_type = "code39"
print_bar_code(
thing.bar_code,
thing.name,
barcode_type=bar_type,
rotate=config.getboolean('printer', 'rotate'),
printer_type="QL-700",
label_type=config.get('printer', 'label_type'),
)
elif isinstance(thing, User):
print_name_label(
text=thing.name,
label_type=config.get('printer', 'label_type'),
rotate=config.getboolean('printer', 'rotate'),
printer_type="QL-700"
)

102
dibbler/menus/stats.py Normal file
View File

@@ -0,0 +1,102 @@
from sqlalchemy import desc, func
from dibbler.helpers import less
from dibbler.models.db import PurchaseEntry, Product, User
from dibbler.statistikkHelpers import statisticsTextOnly
from .helpermenus import Menu
__all__ = ["ProductPopularityMenu", "ProductRevenueMenu", "BalanceMenu", "LoggedStatisticsMenu"]
class ProductPopularityMenu(Menu):
def __init__(self):
Menu.__init__(self, 'Products by popularity', uses_db=True)
def _execute(self):
self.print_header()
text = ''
sub = \
self.session.query(PurchaseEntry.product_id,
func.sum(PurchaseEntry.amount).label('purchase_count')) \
.filter(PurchaseEntry.amount > 0).group_by(PurchaseEntry.product_id) \
.subquery()
product_list = \
self.session.query(Product, sub.c.purchase_count) \
.outerjoin((sub, Product.product_id == sub.c.product_id)) \
.order_by(desc(sub.c.purchase_count)) \
.filter(sub.c.purchase_count is not None) \
.all()
line_format = '{0:10s} | {1:>45s}\n'
text += line_format.format('items sold', 'product')
text += '-' * (31 + Product.name_length) + '\n'
for product, number in product_list:
if number is None:
continue
text += line_format.format(str(number), product.name)
less(text)
class ProductRevenueMenu(Menu):
def __init__(self):
Menu.__init__(self, 'Products by revenue', uses_db=True)
def _execute(self):
self.print_header()
text = ''
sub = \
self.session.query(PurchaseEntry.product_id,
func.sum(PurchaseEntry.amount).label('purchase_count')) \
.filter(PurchaseEntry.amount > 0).group_by(PurchaseEntry.product_id) \
.subquery()
product_list = \
self.session.query(Product, sub.c.purchase_count) \
.outerjoin((sub, Product.product_id == sub.c.product_id)) \
.order_by(desc(sub.c.purchase_count * Product.price)) \
.filter(sub.c.purchase_count is not None) \
.all()
line_format = '{0:7s} | {1:10s} | {2:6s} | {3:>45s}\n'
text += line_format.format('revenue', 'items sold', 'price', 'product')
text += '-' * (31 + Product.name_length) + '\n'
for product, number in product_list:
if number is None:
continue
text += line_format.format(str(number * product.price), str(number), str(product.price), product.name)
less(text)
class BalanceMenu(Menu):
def __init__(self):
Menu.__init__(self, 'Total balance of PVVVV', uses_db=True)
def _execute(self):
self.print_header()
text = ''
total_value = 0
product_list = self.session.query(Product).filter(Product.stock > 0).all()
for p in product_list:
total_value += p.stock * p.price
total_positive_credit = self.session.query(func.sum(User.credit)).filter(User.credit > 0).first()[0]
total_negative_credit = self.session.query(func.sum(User.credit)).filter(User.credit < 0).first()[0]
total_credit = total_positive_credit + total_negative_credit
total_balance = total_value - total_credit
line_format = '%15s | %5d \n'
text += line_format % ('Total value', total_value)
text += 24 * '-' + '\n'
text += line_format % ('Positive credit', total_positive_credit)
text += line_format % ('Negative credit', total_negative_credit)
text += line_format % ('Total credit', total_credit)
text += 24 * '-' + '\n'
text += line_format % ('Total balance', total_balance)
less(text)
class LoggedStatisticsMenu(Menu):
def __init__(self):
Menu.__init__(self, 'Statistics from log', uses_db=True)
def _execute(self):
statisticsTextOnly()

View File

177
dibbler/models/db.py Normal file
View File

@@ -0,0 +1,177 @@
from math import ceil, floor
import datetime
from sqlalchemy import Column, Integer, String, ForeignKey, create_engine, DateTime, Boolean
from sqlalchemy.orm import sessionmaker, relationship, backref
from sqlalchemy.ext.declarative import declarative_base
from dibbler.conf import config
engine = create_engine(config.get('database', 'url'))
Base = declarative_base()
Session = sessionmaker(bind=engine)
class User(Base):
__tablename__ = 'users'
name = Column(String(10), primary_key=True)
card = Column(String(20))
rfid = Column(String(20))
credit = Column(Integer)
name_re = r"[a-z]+"
card_re = r"(([Nn][Tt][Nn][Uu])?[0-9]+)?"
rfid_re = r"[0-9a-fA-F]*"
def __init__(self, name, card, rfid=None, credit=0):
self.name = name
if card == '':
card = None
self.card = card
if rfid == '':
rfid = None
self.rfid = rfid
self.credit = credit
def __repr__(self):
return f"<User('{self.name}')>"
def __str__(self):
return self.name
def is_anonymous(self):
return self.card == '11122233'
class Product(Base):
__tablename__ = 'products'
product_id = Column(Integer, primary_key=True)
bar_code = Column(String(13))
name = Column(String(45))
price = Column(Integer)
stock = Column(Integer)
hidden = Column(Boolean, nullable=False, default=False)
bar_code_re = r"[0-9]+"
name_re = r".+"
name_length = 45
def __init__(self, bar_code, name, price, stock=0, hidden = False):
self.name = name
self.bar_code = bar_code
self.price = price
self.stock = stock
self.hidden = hidden
def __repr__(self):
return f"<Product('{self.name}', '{self.bar_code}', '{self.price}', '{self.stock}', '{self.hidden}')>"
def __str__(self):
return self.name
class UserProducts(Base):
__tablename__ = 'user_products'
user_name = Column(String(10), ForeignKey('users.name'), primary_key=True)
product_id = Column(Integer, ForeignKey("products.product_id"), primary_key=True)
count = Column(Integer)
sign = Column(Integer)
user = relationship(User, backref=backref('products', order_by=count.desc()), lazy='joined')
product = relationship(Product, backref="users", lazy='joined')
class PurchaseEntry(Base):
__tablename__ = 'purchase_entries'
id = Column(Integer, primary_key=True)
purchase_id = Column(Integer, ForeignKey("purchases.id"))
product_id = Column(Integer, ForeignKey("products.product_id"))
amount = Column(Integer)
product = relationship(Product, backref="purchases")
def __init__(self, purchase, product, amount):
self.product = product
self.product_bar_code = product.bar_code
self.purchase = purchase
self.amount = amount
def __repr__(self):
return f"<PurchaseEntry('{self.product.name}', '{self.amount}')>"
class Transaction(Base):
__tablename__ = 'transactions'
id = Column(Integer, primary_key=True)
time = Column(DateTime)
user_name = Column(String(10), ForeignKey('users.name'))
amount = Column(Integer)
description = Column(String(50))
purchase_id = Column(Integer, ForeignKey('purchases.id'))
penalty = Column(Integer)
user = relationship(User, backref=backref('transactions', order_by=time))
def __init__(self, user, amount=0, description=None, purchase=None, penalty=1):
self.user = user
self.amount = amount
self.description = description
self.purchase = purchase
self.penalty = penalty
def perform_transaction(self, ignore_penalty=False):
self.time = datetime.datetime.now()
if not ignore_penalty:
self.amount *= self.penalty
self.user.credit -= self.amount
class Purchase(Base):
__tablename__ = 'purchases'
id = Column(Integer, primary_key=True)
time = Column(DateTime)
price = Column(Integer)
transactions = relationship(Transaction, order_by=Transaction.user_name, backref='purchase')
entries = relationship(PurchaseEntry, backref=backref("purchase"))
def __init__(self):
pass
def __repr__(self):
return f"<Purchase({int(self.id):d}, {self.price:d}, '{self.time.strftime('%c')}')>"
def is_complete(self):
return len(self.transactions) > 0 and len(self.entries) > 0
def price_per_transaction(self, round_up=True):
if round_up:
return int(ceil(float(self.price)/len(self.transactions)))
else:
return int(floor(float(self.price)/len(self.transactions)))
def set_price(self, round_up=True):
self.price = 0
for entry in self.entries:
self.price += entry.amount*entry.product.price
if len(self.transactions) > 0:
for t in self.transactions:
t.amount = self.price_per_transaction(round_up=round_up)
def perform_purchase(self, ignore_penalty=False, round_up=True):
self.time = datetime.datetime.now()
self.set_price(round_up=round_up)
for t in self.transactions:
t.perform_transaction(ignore_penalty=ignore_penalty)
for entry in self.entries:
entry.product.stock -= entry.amount
def perform_soft_purchase(self, price, round_up=True):
self.time = datetime.datetime.now()
self.price = price
for t in self.transactions:
t.amount = self.price_per_transaction(round_up=round_up)
for t in self.transactions:
t.perform_transaction()

View File

@@ -0,0 +1,84 @@
import os
import datetime
import barcode
from brother_ql import BrotherQLRaster, create_label
from brother_ql.backends import backend_factory
from brother_ql.devicedependent import label_type_specs
from PIL import Image, ImageDraw, ImageFont
from .barcode_helpers import BrotherLabelWriter
def print_name_label(text, margin=10, rotate=False, label_type="62", printer_type="QL-700",):
if not rotate:
width, height = label_type_specs[label_type]['dots_printable']
else:
height, width = label_type_specs[label_type]['dots_printable']
font_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "ChopinScript.ttf")
fs = 2000
tw, th = width, height
if width == 0:
while th + 2*margin > height:
font = ImageFont.truetype(font_path, fs)
tw, th = font.getsize(text)
fs -= 1
width = tw+2*margin
elif height == 0:
while tw + 2*margin > width:
font = ImageFont.truetype(font_path, fs)
tw, th = font.getsize(text)
fs -= 1
height = th+2*margin
else:
while tw + 2*margin > width or th + 2*margin > height:
font = ImageFont.truetype(font_path, fs)
tw, th = font.getsize(text)
fs -= 1
xp = (width//2)-(tw//2)
yp = (height//2)-(th//2)
im = Image.new("RGB", (width, height), (255, 255, 255))
dr = ImageDraw.Draw(im)
dr.text((xp, yp), text, fill=(0, 0, 0), font=font)
now = datetime.datetime.now()
date = now.strftime("%Y-%m-%d")
dr.text((0, 0), date, fill=(0, 0, 0))
base_path = os.path.dirname(os.path.realpath(__file__))
fn = os.path.join(base_path, "bar_codes", text + ".png")
im.save(fn, "PNG")
print_image(fn, printer_type, label_type)
def print_bar_code(barcode_value, barcode_text, barcode_type="ean13", rotate=False, printer_type="QL-700",
label_type="62"):
bar_coder = barcode.get_barcode_class(barcode_type)
wr = BrotherLabelWriter(typ=label_type, rot=rotate, text=barcode_text, max_height=1000)
test = bar_coder(barcode_value, writer=wr)
base_path = os.path.dirname(os.path.realpath(__file__))
fn = test.save(os.path.join(base_path, "bar_codes", barcode_value))
print_image(fn, printer_type, label_type)
def print_image(fn, printer_type="QL-700", label_type="62"):
qlr = BrotherQLRaster(printer_type)
qlr.exception_on_warning = True
create_label(qlr, fn, label_type, threshold=70, cut=True)
be = backend_factory("pyusb")
list_available_devices = be['list_available_devices']
BrotherQLBackend = be['backend_class']
ad = list_available_devices()
assert ad
string_descr = ad[0]['string_descr']
printer = BrotherQLBackend(string_descr)
printer.write(qlr.data)

View File

View File

@@ -0,0 +1,15 @@
#!/usr/bin/python
from dibbler.models.db import *
def main():
# Start an SQL session
session=Session()
# Let's find all users with a negative credit
slabbedasker=session.query(User).filter(User.credit<0).all()
for slubbert in slabbedasker:
print(f"{slubbert.name}, {slubbert.credit}")
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,203 @@
#! /usr/bin/env python
# -*- coding: UTF-8 -*-
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
from dibbler.statistikkHelpers import *
def getInputType():
inp = 0
while not (inp == '1' or inp == '2' or inp == '3' or inp == '4'):
print('type 1 for user-statistics')
print('type 2 for product-statistics')
print('type 3 for global-statistics')
print('type 4 to enter loop-mode')
inp = input('')
return int(inp)
def getDateFile(date, n):
try:
if n==0:
inp = input('start date? (yyyy-mm-dd) ')
elif n==-1:
inp = input('end date? (yyyy-mm-dd) ')
year = inp.partition('-')
month = year[2].partition('-')
return datetime.date(int(year[0]), int(month[0]), int(month[2]))
except:
print('invalid date, setting start start date')
if n==0:
print('to date found on first line')
elif n==-1:
print('to date found on last line')
print(date)
return datetime.date(int(date.partition('-')[0]), int(date.partition('-')[2].partition('-')[0]), int(date.partition('-')[2].partition('-')[2]))
def dateToDateNumFile(date, startDate):
year = date.partition('-')
month = year[2].partition('-')
day = datetime.date(int(year[0]), int(month[0]), int(month[2]))
deltaDays = day-startDate
return int(deltaDays.days), day.weekday()
def getProducts(products):
product = []
products = products.partition('¤')
product.append(products[0])
while (products[1]=='¤'):
products = products[2].partition('¤')
product.append(products[0])
return product
def piePlot(dictionary, n):
keys = []
values = []
i=0
for key in sorted(dictionary, key=dictionary.get, reverse=True):
values.append(dictionary[key])
if i<n:
keys.append(key)
i += 1
else:
keys.append('')
plt.pie(values, labels=keys)
def datePlot(array, dateLine):
if (not array == []):
plt.bar(dateLine, array)
plt.gca().xaxis.set_major_formatter(mdates.DateFormatter('%b'))
def dayPlot(array, days):
if (not array == []):
for i in range(7):
array[i]=array[i]*7.0/days
plt.bar(list(range(7)), array)
plt.xticks(list(range(7)),[' mon',' tue',' wed',' thu',' fri',' sat',' sun'])
def graphPlot(array, dateLine):
if (not array == []):
plt.plot(dateLine, array)
plt.gca().xaxis.set_major_formatter(mdates.DateFormatter('%b'))
def plotUser(database, dateLine, user, n):
printUser(database, dateLine, user, n)
plt.subplot(221)
piePlot(database.personVareAntall[user], n)
plt.xlabel('antall varer kjøpt gjengitt i antall')
plt.subplot(222)
datePlot(database.personDatoVerdi[user], dateLine)
plt.xlabel('penger brukt over dato')
plt.subplot(223)
piePlot(database.personVareVerdi[user], n)
plt.xlabel('antall varer kjøpt gjengitt i verdi')
plt.subplot(224)
dayPlot(database.personUkedagVerdi[user], len(dateLine))
plt.xlabel('forbruk over ukedager')
plt.show()
def plotProduct(database, dateLine, product, n):
printProduct(database, dateLine, product, n)
plt.subplot(221)
piePlot(database.varePersonAntall[product], n)
plt.xlabel('personer som har handler produktet')
plt.subplot(222)
datePlot(database.vareDatoAntall[product], dateLine)
plt.xlabel('antall produkter handlet per dag')
#plt.subplot(223)
plt.subplot(224)
dayPlot(database.vareUkedagAntall[product], len(dateLine))
plt.xlabel('antall over ukedager')
plt.show()
def plotGlobal(database, dateLine, n):
printGlobal(database, dateLine, n)
plt.subplot(231)
piePlot(database.globalVareVerdi, n)
plt.xlabel('varer kjøpt gjengitt som verdi')
plt.subplot(232)
datePlot(database.globalDatoForbruk, dateLine)
plt.xlabel('forbruk over dato')
plt.subplot(233)
graphPlot(database.pengebeholdning, dateLine)
plt.xlabel('pengebeholdning over tid (negativ verdi utgjør samlet kreditt)')
plt.subplot(234)
piePlot(database.globalPersonForbruk, n)
plt.xlabel('penger brukt av personer')
plt.subplot(235)
dayPlot(database.globalUkedagForbruk, len(dateLine))
plt.xlabel('forbruk over ukedager')
plt.show()
def alt4menu(database, dateLine, useDatabase):
n=10
while 1:
print('\n1: user-statistics, 2: product-statistics, 3:global-statistics, n: adjust amount of data shown q:quit')
try:
inp = input('')
except:
continue
if inp == 'q':
break
elif inp == '1':
if i=='0':
user = input('input full username: ')
else:
user = getUser()
plotUser(database, dateLine, user, n)
elif inp == '2':
if i=='0':
product = input('input full product name: ')
else:
product = getProduct()
plotProduct(database, dateLine, product, n)
elif inp == '3':
plotGlobal(database, dateLine, n)
elif inp == 'n':
try:
n=int(input('set number to show '));
except:
pass
def main():
inputType=getInputType()
i = input('0:fil, 1:database \n? ')
if (inputType == 1):
if i=='0':
user = input('input full username: ')
else:
user = getUser()
product = ''
elif (inputType == 2):
if i=='0':
product = input('input full product name: ')
else:
product = getProduct()
user = ''
else :
product = ''
user = ''
if i=='0':
inputFile=input('logfil? ')
if inputFile=='':
inputFile='default.dibblerlog'
database, dateLine = buildDatabaseFromFile(inputFile, inputType, product, user)
else:
database, dateLine = buildDatabaseFromDb(inputType, product, user)
if (inputType==1):
plotUser(database, dateLine, user, 10)
if (inputType==2):
plotProduct(database, dateLine, product, 10)
if (inputType==3):
plotGlobal(database, dateLine, 10)
if (inputType==4):
alt4menu(database, dateLine, i)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,417 @@
#! /usr/bin/env python
# -*- coding: UTF-8 -*-
import datetime
from collections import defaultdict
from .helpers import *
from .models.db import *;
def getUser():
while 1:
string = input('user? ')
session = Session()
user = search_user(string, session)
session.close()
if not isinstance(user, list):
return user.name
i=0
if len(user)==0:
print('no matching string')
if len(user)==1:
print('antar: ', user[0].name, '\n')
return user[0].name
if len(user)>10:
continue
for u in user:
print(i, u.name)
i += 1
try:
n = int(input ('enter number:'))
except:
print('invalid input, restarting')
continue
if (n>-1) and (n<i):
return user[n].name
def getProduct():
while 1:
string = input('product? ')
session = Session()
product = search_product(string, session)
session.close()
if not isinstance(product, list):
return product.name
i=0
if len(product)==0:
print('no matching string')
if len(product)==1:
print('antar: ', product[0].name, '\n')
return product[0].name
if len(product)>10:
continue
for u in product:
print(i, u.name)
i += 1
try:
n = int(input ('enter number:'))
except:
print('invalid input, restarting')
continue
if (n>-1) and (n<i):
return product[n].name
class Database:
#for varer
varePersonAntall = defaultdict(dict) #varePersonAntall[Oreo][trygvrad] == 3
vareDatoAntall = defaultdict(list) #dict->array
vareUkedagAntall = defaultdict(list)
#for personer
personVareAntall = defaultdict(dict) #personVareAntall[trygvrad][Oreo] == 3
personVareVerdi = defaultdict(dict) #personVareVerdi[trygvrad][Oreo] == 30 #[kr]
personDatoVerdi = defaultdict(list) #dict->array
personUkedagVerdi = defaultdict(list)
#for global
personPosTransactions = {} # personPosTransactions[trygvrad] == 100 #trygvrad har lagt 100kr i boksen
personNegTransactions = {} # personNegTransactions[trygvrad» == 70 #trygvrad har tatt 70kr fra boksen
globalVareAntall = {}#globalVareAntall[Oreo] == 3
globalVareVerdi = {}#globalVareVerdi[Oreo] == 30 #[kr]
globalPersonAntall = {}#globalPersonAntall[trygvrad] == 3
globalPersonForbruk = {}#globalPersonVerdi == 30 #[kr]
globalUkedagForbruk = []
globalDatoVarer = []
globalDatoForbruk = []
pengebeholdning = []
class InputLine:
def __init__(self, u, p, t):
self.inputUser = u
self.inputProduct = p
self.inputType = t
def getDateDb(date, inp):
try:
year = inp.partition('-')
month = year[2].partition('-')
return datetime.datetime(int(year[0]), int(month[0]), int(month[2]))
except:
print('invalid date, setting date to date found in db')
print(date)
return date
def dateToDateNumDb(date, startDate):
deltaDays = date-startDate
return int(deltaDays.days), date.weekday()
def getInputType():
inp = 0
while not (inp == '1' or inp == '2' or inp == '3' or inp == '4'):
print('type 1 for user-statistics')
print('type 2 for product-statistics')
print('type 3 for global-statistics')
print('type 4 to enter loop-mode')
inp = input('')
return int(inp)
def getProducts(products):
product = []
products = products.partition('¤')
product.append(products[0])
while (products[1]=='¤'):
products = products[2].partition('¤')
product.append(products[0])
return product
def getDateFile(date, inp):
try:
year = inp.partition('-')
month = year[2].partition('-')
return datetime.date(int(year[0]), int(month[0]), int(month[2]))
except:
print('invalid date, setting date to date found on file file')
print(date)
return datetime.date(int(date.partition('-')[0]), int(date.partition('-')[2].partition('-')[0]), int(date.partition('-')[2].partition('-')[2]))
def dateToDateNumFile(date, startDate):
year = date.partition('-')
month = year[2].partition('-')
day = datetime.date(int(year[0]), int(month[0]), int(month[2]))
deltaDays = day-startDate
return int(deltaDays.days), day.weekday()
def clearDatabase(database):
database.varePersonAntall.clear()
database.vareDatoAntall.clear()
database.vareUkedagAntall.clear()
database.personVareAntall.clear()
database.personVareVerdi.clear()
database.personDatoVerdi.clear()
database.personUkedagVerdi.clear()
database.personPosTransactions.clear()
database.personNegTransactions.clear()
database.globalVareAntall.clear()
database.globalVareVerdi.clear()
database.globalPersonAntall.clear()
database.globalPersonForbruk.clear()
return(database)
def addLineToDatabase(database, inputLine):
if abs(inputLine.price)>90000:
return database
#fyller inn for varer
if (not inputLine.product=='') and ((inputLine.inputProduct=='') or (inputLine.inputProduct==inputLine.product)):
database.varePersonAntall[inputLine.product][inputLine.user] = database.varePersonAntall[inputLine.product].setdefault(inputLine.user,0) + 1
if inputLine.product not in database.vareDatoAntall:
database.vareDatoAntall[inputLine.product] = [0]*(inputLine.numberOfDays+1)
database.vareDatoAntall[inputLine.product][inputLine.dateNum] += 1
if inputLine.product not in database.vareUkedagAntall:
database.vareUkedagAntall[inputLine.product] = [0]*7
database.vareUkedagAntall[inputLine.product][inputLine.weekday] += 1
#fyller inn for personer
if (inputLine.inputUser=='') or (inputLine.inputUser==inputLine.user):
if not inputLine.product == '':
database.personVareAntall[inputLine.user][inputLine.product] = database.personVareAntall[inputLine.user].setdefault(inputLine.product,0) + 1
database.personVareVerdi[inputLine.user][inputLine.product] = database.personVareVerdi[inputLine.user].setdefault(inputLine.product,0) + inputLine.price
if inputLine.user not in database.personDatoVerdi:
database.personDatoVerdi[inputLine.user] = [0]*(inputLine.numberOfDays+1)
database.personDatoVerdi[inputLine.user][inputLine.dateNum] += inputLine.price
if inputLine.user not in database.personUkedagVerdi:
database.personUkedagVerdi[inputLine.user] = [0]*7
database.personUkedagVerdi[inputLine.user][inputLine.weekday] += inputLine.price
#fyller inn delt statistikk (genereres uansett)
if (inputLine.product==''):
if (inputLine.price>0):
database.personPosTransactions[inputLine.user] = database.personPosTransactions.setdefault(inputLine.user,0) + inputLine.price
else:
database.personNegTransactions[inputLine.user] = database.personNegTransactions.setdefault(inputLine.user,0) + inputLine.price
elif not (inputLine.inputType==1):
database.globalVareAntall[inputLine.product] = database.globalVareAntall.setdefault(inputLine.product,0) + 1
database.globalVareVerdi[inputLine.product] = database.globalVareVerdi.setdefault(inputLine.product,0) + inputLine.price
#fyller inn for global statistikk
if (inputLine.inputType==3) or (inputLine.inputType==4):
database.pengebeholdning[inputLine.dateNum] += inputLine.price
if not (inputLine.product==''):
database.globalPersonAntall[inputLine.user] = database.globalPersonAntall.setdefault(inputLine.user,0) + 1
database.globalPersonForbruk[inputLine.user] = database.globalPersonForbruk.setdefault(inputLine.user,0) + inputLine.price
database.globalDatoVarer[inputLine.dateNum] += 1
database.globalDatoForbruk[inputLine.dateNum] += inputLine.price
database.globalUkedagForbruk[inputLine.weekday] += inputLine.price
return database
def buildDatabaseFromDb(inputType, inputProduct, inputUser):
sdate = input('enter start date (yyyy-mm-dd)? ')
edate = input('enter end date (yyyy-mm-dd)? ')
print('building database...')
session = Session()
transaction_list = session.query(Transaction).all()
inputLine = InputLine(inputUser, inputProduct, inputType)
startDate = getDateDb(transaction_list[0].time, sdate)
endDate = getDateDb(transaction_list[-1].time, edate)
inputLine.numberOfDays = (endDate-startDate).days
database = Database()
database = clearDatabase(database)
if (inputType==3) or (inputType==4):
database.globalDatoVarer = [0]*(inputLine.numberOfDays+1)
database.globalDatoForbruk = [0]*(inputLine.numberOfDays+1)
database.globalUkedagForbruk = [0]*7
database.pengebeholdning = [0]*(inputLine.numberOfDays+1)
print('wait for it.... ')
for transaction in transaction_list:
if transaction.purchase:
products = [ent.product.name for ent in transaction.purchase.entries]
else:
products = []
products.append('')
inputLine.dateNum, inputLine.weekday = dateToDateNumDb(transaction.time, startDate)
if inputLine.dateNum<0 or inputLine.dateNum>(inputLine.numberOfDays):
continue
inputLine.user=transaction.user.name
inputLine.price=transaction.amount
for inputLine.product in products:
database=addLineToDatabase(database, inputLine )
inputLine.price = 0;
print('saving as default.dibblerlog...', end=' ')
f=open('default.dibblerlog','w')
line_format = '%s|%s|%s|%s|%s|%s\n'
transaction_list = session.query(Transaction).all()
for transaction in transaction_list:
if transaction.purchase:
products = '¤'.join([ent.product.name for ent in transaction.purchase.entries])
description = ''
else:
products = ''
description = transaction.description
line = line_format % ('purchase', transaction.time, products, transaction.user.name, transaction.amount, transaction.description)
f.write(line.encode('utf8'))
session.close()
f.close
#bygg database.pengebeholdning
if (inputType==3) or (inputType==4):
for i in range(inputLine.numberOfDays+1):
if i > 0:
database.pengebeholdning[i] +=database.pengebeholdning[i-1]
#bygg dateLine
day=datetime.timedelta(days=1)
dateLine=[]
dateLine.append(startDate)
for n in range(inputLine.numberOfDays):
dateLine.append(startDate+n*day)
print('done')
return database, dateLine
def buildDatabaseFromFile(inputFile, inputType, inputProduct, inputUser):
sdate = input('enter start date (yyyy-mm-dd)? ')
edate = input('enter end date (yyyy-mm-dd)? ')
f=open(inputFile)
try:
fileLines=f.readlines()
finally:
f.close()
inputLine = InputLine(inputUser, inputProduct, inputType)
startDate = getDateFile(fileLines[0].partition('|')[2].partition(' ')[0], sdate)
endDate = getDateFile(fileLines[-1].partition('|')[2].partition(' ')[0], edate)
inputLine.numberOfDays = (endDate-startDate).days
database = Database()
database = clearDatabase(database)
if (inputType==3) or (inputType==4):
database.globalDatoVarer = [0]*(inputLine.numberOfDays+1)
database.globalDatoForbruk = [0]*(inputLine.numberOfDays+1)
database.globalUkedagForbruk = [0]*7
database.pengebeholdning = [0]*(inputLine.numberOfDays+1)
for linje in fileLines:
if not (linje[0]=='#') and not (linje=='\n') :
#henter dateNum, products, user, price
restDel = linje.partition('|')
restDel = restDel[2].partition(' ')
inputLine.dateNum, inputLine.weekday = dateToDateNumFile(restDel[0], startDate)
if inputLine.dateNum<0 or inputLine.dateNum>(inputLine.numberOfDays):
continue
restDel=restDel[2].partition('|')
restDel=restDel[2].partition('|')
products = restDel[0]
restDel=restDel[2].partition('|')
inputLine.user=restDel[0]
inputLine.price=int(restDel[2].partition('|')[0])
for inputLine.product in getProducts(products):
database=addLineToDatabase(database, inputLine )
inputLine.price = 0;
#bygg database.pengebeholdning
if (inputType==3) or (inputType==4):
for i in range(inputLine.numberOfDays+1):
if i > 0:
database.pengebeholdning[i] +=database.pengebeholdning[i-1]
#bygg dateLine
day=datetime.timedelta(days=1)
dateLine=[]
dateLine.append(startDate)
for n in range(inputLine.numberOfDays):
dateLine.append(startDate+n*day)
return database, dateLine
def printTopDict(dictionary, n, k):
i=0
for key in sorted(dictionary, key=dictionary.get, reverse=k):
print(key, ': ',dictionary[key])
if i<n:
i += 1
else:
break
def printTopDict2(dictionary, dictionary2, n):
print('')
print('product : price[kr] ( number )')
i=0
for key in sorted(dictionary, key=dictionary.get, reverse=True):
print(key, ': ',dictionary[key], ' (', dictionary2[key], ') ')
if i<n:
i += 1
else:
break
def printWeekdays(week, days):
if week==[] or days==0:
return
print('mon: ', '%.2f'%(week[0]*7.0/days), ' tue: ', '%.2f'%(week[1]*7.0/days), ' wen: ', '%.2f'%(week[2]*7.0/days), ' thu: ', '%.2f'%(week[3]*7.0/days), ' fri: ', '%.2f'%(week[4]*7.0/days), ' sat: ','%.2f'%( week[5]*7.0/days), ' sun: ', '%.2f'%(week[6]*7.0/days))
print('forbruk per dag (snitt): ', '%.2f'%(sum(week)*1.0/days))
print('')
def printBalance(database, user):
forbruk = 0
if (user in database.personVareVerdi):
forbruk = sum([i for i in list(database.personVareVerdi[user].values())])
print('totalt kjøpt for: ', forbruk, end=' ')
if (user in database.personNegTransactions):
print('kr, totalt lagt til: ', -database.personNegTransactions[user], end=' ')
forbruk=-database.personNegTransactions[user]-forbruk
if (user in database.personPosTransactions):
print('kr, totalt tatt fra boks: ', database.personPosTransactions[user], end=' ')
forbruk=forbruk-database.personPosTransactions[user]
print('balanse: ', forbruk, 'kr', end=' ')
print('')
def printUser(database, dateLine, user, n):
printTopDict2(database.personVareVerdi[user], database.personVareAntall[user], n)
print('\nforbruk per ukedag [kr/dag],', end=' ')
printWeekdays(database.personUkedagVerdi[user], len(dateLine))
printBalance(database, user)
def printProduct(database, dateLine, product, n):
printTopDict(database.varePersonAntall[product], n, 1)
print('\nforbruk per ukedag [antall/dag],', end=' ')
printWeekdays(database.vareUkedagAntall[product], len(dateLine))
print('Det er solgt: ', database.globalVareAntall[product], product, 'til en verdi av: ', database.globalVareVerdi[product], 'kr')
def printGlobal(database, dateLine, n):
print('\nmest lagt til: ')
printTopDict(database.personNegTransactions, n, 0)
print('\nmest tatt fra:')
printTopDict(database.personPosTransactions, n, 1)
print('\nstørst forbruk:')
printTopDict(database.globalPersonForbruk, n, 1)
printTopDict2(database.globalVareVerdi, database.globalVareAntall, n)
print('\nforbruk per ukedag [kr/dag],', end=' ')
printWeekdays(database.globalUkedagForbruk, len(dateLine))
print('Det er solgt varer til en verdi av: ', sum(database.globalDatoForbruk), 'kr, det er lagt til', -sum([i for i in list(database.personNegTransactions.values())]), 'og tatt fra', sum([i for i in list(database.personPosTransactions.values())]), end=' ')
print('balansen blir:', database.pengebeholdning[len(dateLine)-1], 'der negative verdier representerer at brukere har kreditt tilgjengelig')
def alt4menuTextOnly(database, dateLine):
n=10
while 1:
print('\n1: user-statistics, 2: product-statistics, 3:global-statistics, n: adjust amount of data shown q:quit')
inp = input('')
if inp == 'q':
break
elif inp == '1':
try:
printUser(database, dateLine, getUser(), n)
except:
print('\n\nSomething is not right, (last date prior to first date?)')
elif inp == '2':
try:
printProduct(database, dateLine, getProduct(), n)
except:
print('\n\nSomething is not right, (last date prior to first date?)')
elif inp == '3':
try:
printGlobal(database, dateLine, n)
except:
print('\n\nSomething is not right, (last date prior to first date?)')
elif inp == 'n':
n=int(input('set number to show '));
def statisticsTextOnly():
inputType = 4
product = ''
user = ''
print('\n0: from file, 1: from database, q:quit')
inp = input('')
if inp == '1':
database, dateLine = buildDatabaseFromDb(inputType, product, user)
elif inp=='0' or inp == '':
database, dateLine = buildDatabaseFromFile('default.dibblerlog', inputType, product, user)
if not inp == 'q':
alt4menuTextOnly(database, dateLine)

86
dibbler/text_based.py Executable file
View File

@@ -0,0 +1,86 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
import random
import sys
import traceback
from .helpers import *
from .menus.addstock import AddStockMenu
from .menus.buymenu import BuyMenu
from .menus.editing import *
from .menus.faq import FAQMenu
from .menus.helpermenus import Menu
from .menus.mainmenu import MainMenu
from .menus.miscmenus import (
ProductSearchMenu,
TransferMenu,
AdjustCreditMenu,
UserListMenu,
ShowUserMenu,
ProductListMenu,
)
from .menus.printermenu import PrintLabelMenu
from .menus.stats import *
from .conf import config
random.seed()
def main():
if not config.getboolean('general', 'stop_allowed'):
signal.signal(signal.SIGQUIT, signal.SIG_IGN)
if not config.getboolean('general', 'stop_allowed'):
signal.signal(signal.SIGTSTP, signal.SIG_IGN)
main = MainMenu('Dibbler main menu',
items=[BuyMenu(),
ProductListMenu(),
ShowUserMenu(),
UserListMenu(),
AdjustCreditMenu(),
TransferMenu(),
AddStockMenu(),
Menu('Add/edit',
items=[AddUserMenu(),
EditUserMenu(),
AddProductMenu(),
EditProductMenu(),
AdjustStockMenu(),
CleanupStockMenu(), ]),
ProductSearchMenu(),
Menu('Statistics',
items=[ProductPopularityMenu(),
ProductRevenueMenu(),
BalanceMenu(),
LoggedStatisticsMenu()]),
FAQMenu(),
PrintLabelMenu()
],
exit_msg='happy happy joy joy',
exit_confirm_msg='Really quit Dibbler?')
if not config.getboolean('general', 'quit_allowed'):
main.exit_disallowed_msg = 'You can check out any time you like, but you can never leave.'
while True:
# noinspection PyBroadException
try:
main.execute()
except KeyboardInterrupt:
print('')
print('Interrupted.')
except:
print('Something went wrong.')
print(f'{sys.exc_info()[0]}: {sys.exc_info()[1]}')
if config.getboolean('general', 'show_tracebacks'):
traceback.print_tb(sys.exc_info()[2])
else:
break
print('Restarting main menu.')
if __name__ == '__main__':
main()