Big cleanup ദ്ദി^._.^)
This commit is contained in:
0
dibbler/__init__.py
Normal file
0
dibbler/__init__.py
Normal file
66
dibbler/barcode_helpers.py
Normal file
66
dibbler/barcode_helpers.py
Normal 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
24
dibbler/cli.py
Normal 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
6
dibbler/conf.py
Normal 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
130
dibbler/helpers.py
Normal 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
4
dibbler/makedb.py
Normal file
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/python
|
||||
from .models.db import db
|
||||
|
||||
db.Base.metadata.create_all(db.engine)
|
||||
8
dibbler/menus/__init__.py
Normal file
8
dibbler/menus/__init__.py
Normal 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
139
dibbler/menus/addstock.py
Normal 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
217
dibbler/menus/buymenu.py
Normal 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
187
dibbler/menus/editing.py
Normal 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
108
dibbler/menus/faq.py
Normal 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.
|
||||
''')]
|
||||
504
dibbler/menus/helpermenus.py
Normal file
504
dibbler/menus/helpermenus.py
Normal 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
60
dibbler/menus/mainmenu.py
Normal 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
202
dibbler/menus/miscmenus.py
Normal 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()
|
||||
45
dibbler/menus/printermenu.py
Normal file
45
dibbler/menus/printermenu.py
Normal 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
102
dibbler/menus/stats.py
Normal 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()
|
||||
0
dibbler/models/__init__.py
Normal file
0
dibbler/models/__init__.py
Normal file
177
dibbler/models/db.py
Normal file
177
dibbler/models/db.py
Normal 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()
|
||||
84
dibbler/printer_helpers.py
Normal file
84
dibbler/printer_helpers.py
Normal 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)
|
||||
0
dibbler/scripts/__init__.py
Normal file
0
dibbler/scripts/__init__.py
Normal file
15
dibbler/scripts/slabbedasker.py
Normal file
15
dibbler/scripts/slabbedasker.py
Normal 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()
|
||||
203
dibbler/scripts/statistikk.py
Normal file
203
dibbler/scripts/statistikk.py
Normal 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()
|
||||
417
dibbler/statistikkHelpers.py
Normal file
417
dibbler/statistikkHelpers.py
Normal 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
86
dibbler/text_based.py
Executable 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()
|
||||
Reference in New Issue
Block a user