Restructure project #3
|
@ -1,2 +1,8 @@
|
|||
result
|
||||
result-*
|
||||
result-*
|
||||
|
||||
dist
|
||||
|
||||
test.db
|
||||
|
||||
.ruff_cache
|
30
ALTER
30
ALTER
|
@ -1,30 +0,0 @@
|
|||
torjehoa_dibblerdummy=> ALTER TABLE products ADD stock integer;
|
||||
|
||||
Table "torjehoa_dibblerdummy.products"
|
||||
Column | Type | Modifiers
|
||||
----------+-----------------------+-----------
|
||||
bar_code | character varying(13) | not null
|
||||
name | character varying(45) |
|
||||
price | integer |
|
||||
Indexes:
|
||||
"products_pkey" PRIMARY KEY, btree (bar_code)
|
||||
|
||||
torjehoa_dibblerdummy=> ALTER TABLE products ADD stock integer;
|
||||
ALTER TABLE
|
||||
torjehoa_dibblerdummy=> UPDATE products SET stock = 0;
|
||||
UPDATE 102
|
||||
torjehoa_dibblerdummy=> ALTER TABLE products ALTER stock SET NOT NULL;
|
||||
ALTER TABLE
|
||||
torjehoa_dibblerdummy=> \d products
|
||||
Table "torjehoa_dibblerdummy.products"
|
||||
Column | Type | Modifiers
|
||||
----------+-----------------------+-----------
|
||||
bar_code | character varying(13) | not null
|
||||
name | character varying(45) |
|
||||
price | integer |
|
||||
stock | integer | not null
|
||||
Indexes:
|
||||
"products_pkey" PRIMARY KEY, btree (bar_code)
|
||||
|
||||
torjehoa_dibblerdummy=>
|
||||
|
4
conf.py
4
conf.py
|
@ -1,8 +1,8 @@
|
|||
db_url = 'postgresql://robertem@127.0.0.1/pvvvv'
|
||||
db_url = "postgresql://robertem@127.0.0.1/pvvvv"
|
||||
quit_allowed = True
|
||||
stop_allowed = False
|
||||
show_tracebacks = True
|
||||
input_encoding = 'utf8'
|
||||
input_encoding = "utf8"
|
||||
|
||||
low_credit_warning_limit = -100
|
||||
user_recent_transaction_limit = 100
|
||||
|
|
176
db.py
176
db.py
|
@ -1,176 +0,0 @@
|
|||
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 math import ceil, floor
|
||||
import datetime
|
||||
import conf
|
||||
|
||||
engine = create_engine(conf.db_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()
|
|
@ -1,7 +1,4 @@
|
|||
{ pkgs ? import <nixos-unstable> { } }:
|
||||
|
||||
rec {
|
||||
|
||||
{
|
||||
dibbler = pkgs.callPackage ./nix/dibbler.nix { };
|
||||
|
||||
}
|
||||
|
|
|
@ -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()
|
|
@ -0,0 +1,7 @@
|
|||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from dibbler.conf import config
|
||||
|
||||
engine = create_engine(config.get("database", "url"))
|
||||
Session = sessionmaker(bind=engine)
|
|
@ -6,20 +6,20 @@ from brother_ql.devicedependent import label_type_specs
|
|||
|
||||
|
||||
def px2mm(px, dpi=300):
|
||||
return (25.4 * px)/dpi
|
||||
return (25.4 * px) / dpi
|
||||
|
||||
|
||||
class BrotherLabelWriter(ImageWriter):
|
||||
def __init__(self, typ='62', max_height=350, rot=False, text=None):
|
||||
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']
|
||||
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']
|
||||
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
|
||||
|
@ -31,36 +31,40 @@ class BrotherLabelWriter(ImageWriter):
|
|||
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)
|
||||
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))
|
||||
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)
|
||||
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)
|
||||
super(BrotherLabelWriter, self)._paint_text(xpos + self._xo, ypos + self._yo)
|
||||
|
||||
def _finish(self):
|
||||
if self._title:
|
||||
width = self._w+1
|
||||
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_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)
|
||||
)
|
||||
pos = ((self._w - width) // 2, 0 - (height // 8))
|
||||
self._draw.text(pos, self._title, font=font, fill=self.foreground)
|
||||
return self._image
|
|
@ -0,0 +1,130 @@
|
|||
import pwd
|
||||
import subprocess
|
||||
import os
|
||||
import signal
|
||||
|
||||
from sqlalchemy import or_, and_
|
||||
|
||||
from ..models import User, Product
|
||||
|
||||
|
||||
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 is 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 is 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 argmax(d, all=False, value=None):
|
||||
maxarg = None
|
||||
if value is not None:
|
||||
dd = d
|
||||
d = {}
|
||||
for key in list(dd.keys()):
|
||||
d[key] = value(dd[key])
|
||||
for key in list(d.keys()):
|
||||
if maxarg is 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)
|
|
@ -1,47 +1,50 @@
|
|||
import os
|
||||
import datetime
|
||||
|
||||
import barcode
|
||||
import datetime
|
||||
from PIL import Image
|
||||
from PIL import ImageDraw
|
||||
from PIL import ImageFont
|
||||
from brother_ql import BrotherQLRaster
|
||||
from brother_ql import create_label
|
||||
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
|
||||
from .barcode_helpers import BrotherLabelWriter
|
||||
|
||||
|
||||
def print_name_label(text, margin=10, rotate=False, label_type="62", printer_type="QL-700",):
|
||||
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']
|
||||
width, height = label_type_specs[label_type]["dots_printable"]
|
||||
else:
|
||||
height, width = label_type_specs[label_type]['dots_printable']
|
||||
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:
|
||||
while th + 2 * margin > height:
|
||||
font = ImageFont.truetype(font_path, fs)
|
||||
tw, th = font.getsize(text)
|
||||
fs -= 1
|
||||
width = tw+2*margin
|
||||
width = tw + 2 * margin
|
||||
elif height == 0:
|
||||
while tw + 2*margin > width:
|
||||
while tw + 2 * margin > width:
|
||||
font = ImageFont.truetype(font_path, fs)
|
||||
tw, th = font.getsize(text)
|
||||
fs -= 1
|
||||
height = th+2*margin
|
||||
height = th + 2 * margin
|
||||
else:
|
||||
while tw + 2*margin > width or th + 2*margin > height:
|
||||
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)
|
||||
xp = (width // 2) - (tw // 2)
|
||||
yp = (height // 2) - (th // 2)
|
||||
|
||||
im = Image.new("RGB", (width, height), (255, 255, 255))
|
||||
dr = ImageDraw.Draw(im)
|
||||
|
@ -58,8 +61,14 @@ def print_name_label(text, margin=10, rotate=False, label_type="62", printer_typ
|
|||
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"):
|
||||
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)
|
||||
|
||||
|
@ -75,12 +84,12 @@ def print_image(fn, printer_type="QL-700", label_type="62"):
|
|||
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']
|
||||
list_available_devices = be["list_available_devices"]
|
||||
BrotherQLBackend = be["backend_class"]
|
||||
|
||||
ad = list_available_devices()
|
||||
assert ad
|
||||
string_descr = ad[0]['string_descr']
|
||||
string_descr = ad[0]["string_descr"]
|
||||
|
||||
printer = BrotherQLBackend(string_descr)
|
||||
|
|
@ -0,0 +1,512 @@
|
|||
#! /usr/bin/env python
|
||||
# -*- coding: UTF-8 -*-
|
||||
|
||||
import datetime
|
||||
from collections import defaultdict
|
||||
|
||||
from .helpers import *
|
||||
from ..models import Transaction
|
||||
from ..db import Session
|
||||
|
||||
|
||||
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])
|
||||
else:
|
||||
products = ""
|
||||
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)
|
|
@ -0,0 +1,46 @@
|
|||
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,
|
||||
)
|
||||
|
||||
subparsers = parser.add_subparsers(
|
||||
title="subcommands",
|
||||
dest="subcommand",
|
||||
required=True,
|
||||
)
|
||||
subparsers.add_parser("loop", help="Run the dibbler loop")
|
||||
subparsers.add_parser("create-db", help="Create the database")
|
||||
subparsers.add_parser("slabbedasker", help="Find out who is slabbedasker")
|
||||
|
||||
|
||||
def main():
|
||||
args = parser.parse_args()
|
||||
config.read(args.config)
|
||||
|
||||
if args.subcommand == "loop":
|
||||
import dibbler.subcommands.loop as loop
|
||||
|
||||
loop.main()
|
||||
|
||||
elif args.subcommand == "create-db":
|
||||
import dibbler.subcommands.makedb as makedb
|
||||
|
||||
makedb.main()
|
||||
|
||||
elif args.subcommand == "slabbedasker":
|
||||
import dibbler.subcommands.slabbedasker as slabbedasker
|
||||
|
||||
slabbedasker.main()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
|
@ -0,0 +1,53 @@
|
|||
__all__ = [
|
||||
"AddProductMenu",
|
||||
"AddStockMenu",
|
||||
"AddUserMenu",
|
||||
"AdjustCreditMenu",
|
||||
"AdjustStockMenu",
|
||||
"BalanceMenu",
|
||||
"BuyMenu",
|
||||
"CleanupStockMenu",
|
||||
"EditProductMenu",
|
||||
"EditUserMenu",
|
||||
"FAQMenu",
|
||||
"LoggedStatisticsMenu",
|
||||
"MainMenu",
|
||||
"Menu",
|
||||
"PrintLabelMenu",
|
||||
"ProductListMenu",
|
||||
"ProductPopularityMenu",
|
||||
"ProductRevenueMenu",
|
||||
"ProductSearchMenu",
|
||||
"ShowUserMenu",
|
||||
"TransferMenu",
|
||||
"UserListMenu",
|
||||
]
|
||||
|
||||
from .addstock import AddStockMenu
|
||||
from .buymenu import BuyMenu
|
||||
from .editing import (
|
||||
AddUserMenu,
|
||||
EditUserMenu,
|
||||
AddProductMenu,
|
||||
EditProductMenu,
|
||||
AdjustStockMenu,
|
||||
CleanupStockMenu,
|
||||
)
|
||||
from .faq import FAQMenu
|
||||
from .helpermenus import Menu
|
||||
from .mainmenu import MainMenu
|
||||
from .miscmenus import (
|
||||
ProductSearchMenu,
|
||||
TransferMenu,
|
||||
AdjustCreditMenu,
|
||||
UserListMenu,
|
||||
ShowUserMenu,
|
||||
ProductListMenu,
|
||||
)
|
||||
from .printermenu import PrintLabelMenu
|
||||
from .stats import (
|
||||
ProductPopularityMenu,
|
||||
ProductRevenueMenu,
|
||||
BalanceMenu,
|
||||
LoggedStatisticsMenu,
|
||||
)
|
|
@ -1,16 +1,23 @@
|
|||
from math import ceil
|
||||
|
||||
import sqlalchemy
|
||||
from db import Product, User, Transaction, PurchaseEntry, Purchase
|
||||
from text_interface.helpermenus import Menu
|
||||
|
||||
from dibbler.models 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 = '''
|
||||
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'''
|
||||
much money you're due in credits for the purchase when prompted.\n"""
|
||||
self.users = []
|
||||
self.users = []
|
||||
self.products = {}
|
||||
|
@ -18,10 +25,19 @@ much money you're due in credits for the purchase when prompted.\n'''
|
|||
|
||||
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>"',
|
||||
(
|
||||
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'
|
||||
(
|
||||
True,
|
||||
True,
|
||||
): 'Enter more strings of the form "<number> <product>", or an empty line to confirm',
|
||||
}
|
||||
|
||||
self.users = []
|
||||
|
@ -34,24 +50,33 @@ much money you're due in credits for the purchase when prompted.\n'''
|
|||
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)
|
||||
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
|
||||
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?'
|
||||
self.exit_confirm_msg = "Abort transaction?"
|
||||
else:
|
||||
if not self.complete_input():
|
||||
if self.confirm('Not enough information entered. Abort transaction?', default=True):
|
||||
if self.confirm(
|
||||
"Not enough information entered. Abort transaction?",
|
||||
default=True,
|
||||
):
|
||||
return False
|
||||
continue
|
||||
break
|
||||
|
@ -67,7 +92,7 @@ much money you're due in credits for the purchase when prompted.\n'''
|
|||
def print_info(self):
|
||||
width = 6 + Product.name_length
|
||||
print()
|
||||
print(width * '-')
|
||||
print(width * "-")
|
||||
if self.price:
|
||||
print(f"Amount to be credited:{self.price:>{width - 22}}")
|
||||
if self.users:
|
||||
|
@ -77,41 +102,47 @@ much money you're due in credits for the purchase when prompted.\n'''
|
|||
print()
|
||||
print("Products", end="")
|
||||
print("Amount".rjust(width - 8))
|
||||
print(width * '-')
|
||||
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 * '-')
|
||||
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')
|
||||
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)
|
||||
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)
|
||||
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.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 "")
|
||||
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:
|
||||
|
@ -129,4 +160,4 @@ much money you're due in credits for the purchase when prompted.\n'''
|
|||
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}')
|
||||
print(f"Could not perform transaction: {e}")
|
|
@ -1,23 +1,31 @@
|
|||
import conf
|
||||
import sqlalchemy
|
||||
from db import User, Purchase, PurchaseEntry, Transaction, Product
|
||||
from text_interface.helpermenus import Menu
|
||||
|
||||
from dibbler.conf import config
|
||||
from dibbler.models 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)
|
||||
Menu.__init__(self, "Buy", uses_db=True)
|
||||
if session:
|
||||
self.session = session
|
||||
self.superfast_mode = False
|
||||
self.help_text = '''
|
||||
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'''
|
||||
When finished, write an empty line to confirm the purchase.\n"""
|
||||
|
||||
@staticmethod
|
||||
def credit_check(user):
|
||||
|
@ -29,7 +37,7 @@ When finished, write an empty line to confirm the purchase.\n'''
|
|||
"""
|
||||
assert isinstance(user, User)
|
||||
|
||||
return user.credit > conf.low_credit_warning_limit
|
||||
return user.credit > config.getint("limits", "low_credit_warning_limit")
|
||||
|
||||
def low_credit_warning(self, user, timeout=False):
|
||||
assert isinstance(user, User)
|
||||
|
@ -49,7 +57,9 @@ When finished, write an empty line to confirm the purchase.\n'''
|
|||
print("***********************************************************************")
|
||||
print("***********************************************************************")
|
||||
print("")
|
||||
print(f"USER {user.name} HAS LOWER CREDIT THAN {conf.low_credit_warning_limit:d}.")
|
||||
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("")
|
||||
|
@ -64,10 +74,10 @@ When finished, write an empty line to confirm the purchase.\n'''
|
|||
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('---------------------------------------------')
|
||||
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):
|
||||
|
@ -102,24 +112,32 @@ When finished, write an empty line to confirm the purchase.\n'''
|
|||
|
||||
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('***********************************************')
|
||||
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)])
|
||||
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)
|
||||
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:
|
||||
|
@ -128,7 +146,9 @@ When finished, write an empty line to confirm the purchase.\n'''
|
|||
# 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):
|
||||
if self.confirm(
|
||||
"Not enough information entered. Abort purchase?", default=True
|
||||
):
|
||||
return False
|
||||
continue
|
||||
break
|
||||
|
@ -136,7 +156,7 @@ When finished, write an empty line to confirm the purchase.\n'''
|
|||
# once we get something in the
|
||||
# purchase, we want to protect the
|
||||
# user from accidentally killing it
|
||||
self.exit_confirm_msg = 'Abort purchase?'
|
||||
self.exit_confirm_msg = "Abort purchase?"
|
||||
|
||||
# Add the thing to our purchase object:
|
||||
if not self.add_thing_to_purchase(thing, amount=num):
|
||||
|
@ -151,20 +171,22 @@ When finished, write an empty line to confirm the purchase.\n'''
|
|||
try:
|
||||
self.session.commit()
|
||||
except sqlalchemy.exc.SQLAlchemyError as e:
|
||||
print(f'Could not store purchase: {e}')
|
||||
print(f"Could not store purchase: {e}")
|
||||
else:
|
||||
print('Purchase stored.')
|
||||
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 < conf.low_credit_warning_limit:
|
||||
print(f'USER {t.user.name} HAS LOWER CREDIT THAN {conf.low_credit_warning_limit:d},',
|
||||
'AND SHOULD CONSIDER PUTTING SOME MONEY IN THE BOX.')
|
||||
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("")
|
||||
print("")
|
||||
return True
|
||||
|
||||
def complete_input(self):
|
||||
|
@ -176,30 +198,33 @@ When finished, write an empty line to confirm the purchase.\n'''
|
|||
entries = self.purchase.entries
|
||||
if len(transactions) == 0 and len(entries) == 0:
|
||||
return None
|
||||
string = 'Purchase:'
|
||||
string += '\n buyers: '
|
||||
string = "Purchase:"
|
||||
string += "\n buyers: "
|
||||
if len(transactions) == 0:
|
||||
string += '(empty)'
|
||||
string += "(empty)"
|
||||
else:
|
||||
string += ', '.join(
|
||||
[t.user.name + ("*" if not self.credit_check(t.user) else "") for t in transactions])
|
||||
string += '\n products: '
|
||||
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)'
|
||||
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])
|
||||
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'
|
||||
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" *({self.purchase.price_per_transaction() * 2:d} kr)"
|
||||
|
||||
string += f'\n total price: {self.purchase.price: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'
|
||||
string += f"\n *total with penalty: {total} kr"
|
||||
|
||||
return string
|
||||
|
|
@ -0,0 +1,216 @@
|
|||
import sqlalchemy
|
||||
|
||||
from dibbler.models 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)
|
|
@ -5,9 +5,11 @@ 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?',
|
||||
'''
|
||||
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
|
||||
|
@ -19,15 +21,18 @@ class FAQMenu(Menu):
|
|||
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?',
|
||||
'''
|
||||
""",
|
||||
),
|
||||
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?',
|
||||
'''
|
||||
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.
|
||||
|
||||
|
@ -35,29 +40,41 @@ class FAQMenu(Menu):
|
|||
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?',
|
||||
'''
|
||||
""",
|
||||
),
|
||||
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?',
|
||||
'''
|
||||
""",
|
||||
),
|
||||
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?',
|
||||
'''
|
||||
""",
|
||||
),
|
||||
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
|
||||
|
@ -83,9 +100,11 @@ class FAQMenu(Menu):
|
|||
|
||||
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?',
|
||||
'''
|
||||
""",
|
||||
),
|
||||
MessageMenu(
|
||||
"My question isn't listed here; what do I do?",
|
||||
"""
|
||||
DON'T PANIC.
|
||||
|
||||
Follow this procedure:
|
||||
|
@ -105,4 +124,6 @@ class FAQMenu(Menu):
|
|||
|
||||
5. Type "restart" in Dibbler to replace the running process by a new
|
||||
one using the updated files.
|
||||
''')]
|
||||
""",
|
||||
),
|
||||
]
|
|
@ -5,10 +5,19 @@ import re
|
|||
import sys
|
||||
from select import select
|
||||
|
||||
import conf
|
||||
from db import User, Session
|
||||
from helpers import search_user, search_product, guess_data_type, argmax
|
||||
from text_interface import context_commands, local_help_commands, help_commands, exit_commands
|
||||
from dibbler.db import Session
|
||||
from dibbler.models import User
|
||||
from dibbler.lib.helpers import (
|
||||
search_user,
|
||||
search_product,
|
||||
guess_data_type,
|
||||
argmax,
|
||||
)
|
||||
|
||||
exit_commands = ["exit", "abort", "quit", "bye", "eat flaming death", "q"]
|
||||
help_commands = ["help", "?"]
|
||||
context_commands = ["what", "??"]
|
||||
local_help_commands = ["help!", "???"]
|
||||
|
||||
|
||||
class ExitMenu(Exception):
|
||||
|
@ -16,10 +25,19 @@ class ExitMenu(Exception):
|
|||
|
||||
|
||||
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):
|
||||
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
|
||||
|
@ -59,7 +77,7 @@ class Menu(object):
|
|||
if self.context is None:
|
||||
self.context = string
|
||||
else:
|
||||
self.context += '\n' + string
|
||||
self.context += "\n" + string
|
||||
|
||||
def show_context(self):
|
||||
print(self.header())
|
||||
|
@ -84,8 +102,16 @@ class Menu(object):
|
|||
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):
|
||||
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:
|
||||
|
@ -105,13 +131,13 @@ class Menu(object):
|
|||
rlist, _, _ = select([sys.stdin], [], [], timeout)
|
||||
if not rlist:
|
||||
# timeout occurred, simulate empty line
|
||||
result = ''
|
||||
result = ""
|
||||
else:
|
||||
result = input(prompt).strip()
|
||||
else:
|
||||
result = input(prompt).strip()
|
||||
except EOFError:
|
||||
print('quit')
|
||||
print("quit")
|
||||
self.exit_menu()
|
||||
continue
|
||||
if result in exit_commands:
|
||||
|
@ -128,22 +154,26 @@ class Menu(object):
|
|||
continue
|
||||
if self.special_input_options(result):
|
||||
continue
|
||||
if empty_string_is_none and result == '':
|
||||
if empty_string_is_none and result == "":
|
||||
return None
|
||||
if default is not None and result == '':
|
||||
if default is not None and result == "":
|
||||
return default
|
||||
if regex is not None and not re.match(regex + '$', result):
|
||||
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 < 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}]')
|
||||
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}')
|
||||
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}')
|
||||
print(f"Value must have length at most {length_range[1]:d}")
|
||||
continue
|
||||
return result
|
||||
|
||||
|
@ -170,8 +200,8 @@ class Menu(object):
|
|||
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')
|
||||
if result == "":
|
||||
print("Please enter something")
|
||||
else:
|
||||
if result.isdigit():
|
||||
choice = int(result)
|
||||
|
@ -183,9 +213,17 @@ class Menu(object):
|
|||
self.invalid_menu_choice(result)
|
||||
|
||||
def invalid_menu_choice(self, in_str):
|
||||
print('Please enter a valid choice.')
|
||||
print("Please enter a valid choice.")
|
||||
|
||||
def input_int(self, prompt=None, minimum=None, maximum=None, null_allowed=False, zero_allowed=True, default=None):
|
||||
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:
|
||||
|
@ -197,15 +235,15 @@ class Menu(object):
|
|||
|
||||
while True:
|
||||
result = self.input_str(prompt + end_prompt, default=default)
|
||||
if result == '' and null_allowed:
|
||||
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}')
|
||||
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}')
|
||||
print(f"Value must be at most {maximum:d}")
|
||||
continue
|
||||
if not zero_allowed and value == 0:
|
||||
print("Value cannot be zero")
|
||||
|
@ -221,7 +259,7 @@ class Menu(object):
|
|||
return user
|
||||
|
||||
def retrieve_user(self, search_str):
|
||||
return self.search_ui(search_user, search_str, 'user')
|
||||
return self.search_ui(search_user, search_str, "user")
|
||||
|
||||
def input_product(self, prompt=None, end_prompt=None):
|
||||
product = None
|
||||
|
@ -230,47 +268,73 @@ class Menu(object):
|
|||
return product
|
||||
|
||||
def retrieve_product(self, search_str):
|
||||
return self.search_ui(search_product, search_str, 'product')
|
||||
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):
|
||||
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:
|
||||
if search_str == "" and empty_input_permitted:
|
||||
return None
|
||||
result = self.search_for_thing(search_str, permitted_things, add_nonexisting, find_hidden_products)
|
||||
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):
|
||||
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:
|
||||
if search_str == "" and empty_input_permitted:
|
||||
return None
|
||||
else:
|
||||
result = self.search_for_thing(search_str, permitted_things, add_nonexisting, find_hidden_products)
|
||||
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)
|
||||
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}
|
||||
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:
|
||||
|
@ -278,8 +342,12 @@ class Menu(object):
|
|||
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'}
|
||||
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)
|
||||
|
@ -301,32 +369,39 @@ class Menu(object):
|
|||
|
||||
def search_add(self, string):
|
||||
type_guess = guess_data_type(string)
|
||||
if type_guess == 'username':
|
||||
if type_guess == "username":
|
||||
print(f'"{string}" looks like a username, but no such user exists.')
|
||||
if self.confirm(f'Create user {string}?'):
|
||||
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}')])
|
||||
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))
|
||||
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')
|
||||
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})')
|
||||
print(f"Card number of {user.name} set to {string} (was {old_card})")
|
||||
return user
|
||||
return None
|
||||
if type_guess == 'bar_code':
|
||||
if type_guess == "bar_code":
|
||||
print(f'"{string}" looks like the bar code for a product, but no such product exists.')
|
||||
return None
|
||||
|
||||
|
@ -347,13 +422,14 @@ class Menu(object):
|
|||
return None
|
||||
limit = 9
|
||||
if len(result) > limit:
|
||||
select_header = f'{len(result):d} {thing}s matching "{search_str}"; showing first {limit:d}'
|
||||
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)
|
||||
selector = Selector(select_header, items=select_items, return_index=False)
|
||||
return selector.execute()
|
||||
|
||||
@staticmethod
|
||||
|
@ -368,11 +444,12 @@ class Menu(object):
|
|||
print(self.header())
|
||||
|
||||
def pause(self):
|
||||
self.input_str('.', end_prompt="")
|
||||
self.input_str(".", end_prompt="")
|
||||
|
||||
@staticmethod
|
||||
def general_help():
|
||||
print('''
|
||||
print(
|
||||
"""
|
||||
DIBBLER HELP
|
||||
|
||||
The following commands are recognized (almost) everywhere:
|
||||
|
@ -393,14 +470,15 @@ class Menu(object):
|
|||
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')
|
||||
print("no help here")
|
||||
else:
|
||||
print('')
|
||||
print(f'Help for {self.header()}:')
|
||||
print("")
|
||||
print(f"Help for {self.header()}:")
|
||||
print(self.help_text)
|
||||
|
||||
def execute(self, **kwargs):
|
||||
|
@ -422,7 +500,7 @@ class Menu(object):
|
|||
self.print_header()
|
||||
self.set_context(None)
|
||||
if len(self.items) == 0:
|
||||
self.printc('(empty menu)')
|
||||
self.printc("(empty menu)")
|
||||
self.pause()
|
||||
return None
|
||||
for i in range(len(self.items)):
|
||||
|
@ -443,37 +521,52 @@ class MessageMenu(Menu):
|
|||
|
||||
def _execute(self):
|
||||
self.print_header()
|
||||
print('')
|
||||
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')
|
||||
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]
|
||||
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 = self.input_str(
|
||||
f"{self.prompt} ({options})", end_prompt=": ", timeout=self.timeout
|
||||
)
|
||||
result = result.lower().strip()
|
||||
if result in ['y', 'yes']:
|
||||
if result in ["y", "yes"]:
|
||||
return True
|
||||
elif result in ['n', 'no']:
|
||||
elif result in ["n", "no"]:
|
||||
return False
|
||||
elif self.default is not None and result == '':
|
||||
elif self.default is not None and result == "":
|
||||
return self.default
|
||||
else:
|
||||
print('Please answer yes or no')
|
||||
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):
|
||||
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)
|
||||
|
@ -486,9 +579,9 @@ class Selector(Menu):
|
|||
|
||||
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.')
|
||||
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("")
|
||||
print(f"Help for selector ({self.name}):")
|
||||
print(self.help_text)
|
|
@ -1,14 +1,16 @@
|
|||
|
||||
# -*- coding: utf-8 -*-
|
||||
import os
|
||||
import random
|
||||
import sys
|
||||
|
||||
from db import Session
|
||||
from text_interface import faq_commands, restart_commands
|
||||
from text_interface.buymenu import BuyMenu
|
||||
from text_interface.faq import FAQMenu
|
||||
from text_interface.helpermenus import Menu
|
||||
from dibbler.db import Session
|
||||
|
||||
from .buymenu import BuyMenu
|
||||
from .faq import FAQMenu
|
||||
from .helpermenus import Menu
|
||||
|
||||
faq_commands = ["faq"]
|
||||
restart_commands = ["restart"]
|
||||
|
||||
|
||||
def restart():
|
||||
|
@ -39,18 +41,24 @@ class MainMenu(Menu):
|
|||
FAQMenu().execute()
|
||||
return True
|
||||
if result in restart_commands:
|
||||
if self.confirm('Restart Dibbler?'):
|
||||
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')
|
||||
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':
|
||||
elif result == "cs":
|
||||
os.system('echo -e "\033[0m"')
|
||||
os.system('clear')
|
||||
os.system("clear")
|
||||
self.show_context()
|
||||
return True
|
||||
return False
|
|
@ -0,0 +1,224 @@
|
|||
import sqlalchemy
|
||||
|
||||
from dibbler.conf import config
|
||||
from dibbler.models import Transaction, Product, User
|
||||
from dibbler.lib.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()
|
|
@ -0,0 +1,45 @@
|
|||
import re
|
||||
|
||||
from dibbler.conf import config
|
||||
from dibbler.models import Product, User
|
||||
from dibbler.lib.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",
|
||||
)
|
|
@ -0,0 +1,126 @@
|
|||
from sqlalchemy import desc, func
|
||||
|
||||
from dibbler.lib.helpers import less
|
||||
from dibbler.models import PurchaseEntry, Product, User
|
||||
from dibbler.lib.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,0 +1,44 @@
|
|||
from sqlalchemy import MetaData
|
||||
from sqlalchemy.orm import (
|
||||
DeclarativeBase,
|
||||
declared_attr,
|
||||
)
|
||||
from sqlalchemy.orm.collections import (
|
||||
InstrumentedDict,
|
||||
InstrumentedList,
|
||||
InstrumentedSet,
|
||||
)
|
||||
|
||||
|
||||
class Base(DeclarativeBase):
|
||||
metadata = MetaData(
|
||||
naming_convention={
|
||||
"ix": "ix_%(column_0_label)s",
|
||||
"uq": "uq_%(table_name)s_%(column_0_name)s",
|
||||
"ck": "ck_%(table_name)s_`%(constraint_name)s`",
|
||||
"fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
|
||||
"pk": "pk_%(table_name)s",
|
||||
}
|
||||
)
|
||||
|
||||
@declared_attr.directive
|
||||
def __tablename__(cls) -> str:
|
||||
return cls.__name__
|
||||
|
||||
def __repr__(self) -> str:
|
||||
columns = ", ".join(
|
||||
f"{k}={repr(v)}"
|
||||
for k, v in self.__dict__.items()
|
||||
if not any(
|
||||
[
|
||||
k.startswith("_"),
|
||||
# Ensure that we don't try to print out the entire list of
|
||||
# relationships, which could create an infinite loop
|
||||
isinstance(v, Base),
|
||||
isinstance(v, InstrumentedList),
|
||||
isinstance(v, InstrumentedSet),
|
||||
isinstance(v, InstrumentedDict),
|
||||
]
|
||||
)
|
||||
)
|
||||
return f"<{self.__class__.__name__}({columns})>"
|
|
@ -0,0 +1,47 @@
|
|||
from __future__ import annotations
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import (
|
||||
Boolean,
|
||||
Integer,
|
||||
String,
|
||||
)
|
||||
from sqlalchemy.orm import (
|
||||
Mapped,
|
||||
mapped_column,
|
||||
relationship,
|
||||
)
|
||||
|
||||
from .Base import Base
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .PurchaseEntry import PurchaseEntry
|
||||
from .UserProducts import UserProducts
|
||||
|
||||
|
||||
class Product(Base):
|
||||
__tablename__ = "products"
|
||||
|
||||
product_id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
bar_code: Mapped[str] = mapped_column(String(13))
|
||||
name: Mapped[str] = mapped_column(String(45))
|
||||
price: Mapped[int] = mapped_column(Integer)
|
||||
stock: Mapped[int] = mapped_column(Integer)
|
||||
hidden: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
||||
|
||||
purchases: Mapped[set[PurchaseEntry]] = relationship(back_populates="product")
|
||||
users: Mapped[set[UserProducts]] = relationship(back_populates="product")
|
||||
|
||||
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 __str__(self):
|
||||
return self.name
|
|
@ -0,0 +1,70 @@
|
|||
from __future__ import annotations
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from datetime import datetime
|
||||
import math
|
||||
|
||||
from sqlalchemy import (
|
||||
DateTime,
|
||||
Integer,
|
||||
)
|
||||
from sqlalchemy.orm import (
|
||||
Mapped,
|
||||
mapped_column,
|
||||
relationship,
|
||||
)
|
||||
|
||||
from .Base import Base
|
||||
from .Transaction import Transaction
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .PurchaseEntry import PurchaseEntry
|
||||
|
||||
|
||||
class Purchase(Base):
|
||||
__tablename__ = "purchases"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
time: Mapped[datetime] = mapped_column(DateTime)
|
||||
price: Mapped[int] = mapped_column(Integer)
|
||||
|
||||
transactions: Mapped[set[Transaction]] = relationship(
|
||||
back_populates="purchase", order_by="Transaction.user_name"
|
||||
)
|
||||
entries: Mapped[set[PurchaseEntry]] = relationship(back_populates="purchase")
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
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(math.ceil(float(self.price) / len(self.transactions)))
|
||||
else:
|
||||
return int(math.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()
|
|
@ -0,0 +1,37 @@
|
|||
from __future__ import annotations
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import (
|
||||
Integer,
|
||||
ForeignKey,
|
||||
)
|
||||
from sqlalchemy.orm import (
|
||||
Mapped,
|
||||
mapped_column,
|
||||
relationship,
|
||||
)
|
||||
|
||||
from .Base import Base
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .Product import Product
|
||||
from .Purchase import Purchase
|
||||
|
||||
|
||||
class PurchaseEntry(Base):
|
||||
__tablename__ = "purchase_entries"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
amount: Mapped[int] = mapped_column(Integer)
|
||||
|
||||
product_id: Mapped[int] = mapped_column(ForeignKey("products.product_id"))
|
||||
purchase_id: Mapped[int] = mapped_column(ForeignKey("purchases.id"))
|
||||
|
||||
product: Mapped[Product] = relationship(lazy="joined")
|
||||
purchase: Mapped[Purchase] = relationship(lazy="joined")
|
||||
|
||||
def __init__(self, purchase, product, amount):
|
||||
self.product = product
|
||||
self.product_bar_code = product.bar_code
|
||||
self.purchase = purchase
|
||||
self.amount = amount
|
|
@ -0,0 +1,52 @@
|
|||
from __future__ import annotations
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import (
|
||||
DateTime,
|
||||
ForeignKey,
|
||||
Integer,
|
||||
String,
|
||||
)
|
||||
from sqlalchemy.orm import (
|
||||
Mapped,
|
||||
mapped_column,
|
||||
relationship,
|
||||
)
|
||||
|
||||
from .Base import Base
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .User import User
|
||||
from .Purchase import Purchase
|
||||
|
||||
|
||||
class Transaction(Base):
|
||||
__tablename__ = "transactions"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
|
||||
time: Mapped[datetime] = mapped_column(DateTime)
|
||||
amount: Mapped[int] = mapped_column(Integer)
|
||||
penalty: Mapped[int] = mapped_column(Integer)
|
||||
description: Mapped[str | None] = mapped_column(String(50))
|
||||
|
||||
user_name: Mapped[str] = mapped_column(ForeignKey("users.name"))
|
||||
purchase_id: Mapped[int | None] = mapped_column(ForeignKey("purchases.id"))
|
||||
|
||||
user: Mapped[User] = relationship(lazy="joined")
|
||||
purchase: Mapped[Purchase] = relationship(lazy="joined")
|
||||
|
||||
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
|
|
@ -0,0 +1,49 @@
|
|||
from __future__ import annotations
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import (
|
||||
Integer,
|
||||
String,
|
||||
)
|
||||
from sqlalchemy.orm import (
|
||||
Mapped,
|
||||
mapped_column,
|
||||
relationship,
|
||||
)
|
||||
|
||||
from .Base import Base
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .UserProducts import UserProducts
|
||||
from .Transaction import Transaction
|
||||
|
||||
|
||||
class User(Base):
|
||||
__tablename__ = "users"
|
||||
name: Mapped[str] = mapped_column(String(10), primary_key=True)
|
||||
credit: Mapped[str] = mapped_column(Integer)
|
||||
card: Mapped[str | None] = mapped_column(String(20))
|
||||
rfid: Mapped[str | None] = mapped_column(String(20))
|
||||
|
||||
products: Mapped[set[UserProducts]] = relationship(back_populates="user")
|
||||
transactions: Mapped[set[Transaction]] = relationship(back_populates="user")
|
||||
|
||||
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 __str__(self):
|
||||
return self.name
|
||||
|
||||
def is_anonymous(self):
|
||||
return self.card == "11122233"
|
|
@ -0,0 +1,31 @@
|
|||
from __future__ import annotations
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import (
|
||||
Integer,
|
||||
ForeignKey,
|
||||
)
|
||||
from sqlalchemy.orm import (
|
||||
Mapped,
|
||||
mapped_column,
|
||||
relationship,
|
||||
)
|
||||
|
||||
from .Base import Base
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .User import User
|
||||
from .Product import Product
|
||||
|
||||
|
||||
class UserProducts(Base):
|
||||
__tablename__ = "user_products"
|
||||
|
||||
user_name: Mapped[str] = mapped_column(ForeignKey("users.name"), primary_key=True)
|
||||
product_id: Mapped[int] = mapped_column(ForeignKey("products.product_id"), primary_key=True)
|
||||
|
||||
count: Mapped[int] = mapped_column(Integer)
|
||||
sign: Mapped[int] = mapped_column(Integer)
|
||||
|
||||
user: Mapped[User] = relationship()
|
||||
product: Mapped[Product] = relationship()
|
|
@ -0,0 +1,17 @@
|
|||
__all__ = [
|
||||
'Base',
|
||||
'Product',
|
||||
'Purchase',
|
||||
'PurchaseEntry',
|
||||
'Transaction',
|
||||
'User',
|
||||
'UserProducts',
|
||||
]
|
||||
|
||||
from .Base import Base
|
||||
from .Product import Product
|
||||
from .Purchase import Purchase
|
||||
from .PurchaseEntry import PurchaseEntry
|
||||
from .Transaction import Transaction
|
||||
from .User import User
|
||||
from .UserProducts import UserProducts
|
|
@ -0,0 +1,79 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import random
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
from ..conf import config
|
||||
from ..lib.helpers import *
|
||||
from ..menus import *
|
||||
|
||||
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()
|
|
@ -0,0 +1,11 @@
|
|||
#!/usr/bin/python
|
||||
from dibbler.models import Base
|
||||
from dibbler.db import engine
|
||||
|
||||
|
||||
def main():
|
||||
Base.metadata.create_all(engine)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
|
@ -0,0 +1,18 @@
|
|||
#!/usr/bin/python
|
||||
|
||||
from dibbler.db import Session
|
||||
from dibbler.models import User
|
||||
|
||||
|
||||
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()
|
|
@ -0,0 +1,231 @@
|
|||
#! /usr/bin/env python
|
||||
|
||||
# TODO: fixme
|
||||
|
||||
# -*- coding: UTF-8 -*-
|
||||
import matplotlib.pyplot as plt
|
||||
import matplotlib.dates as mdates
|
||||
|
||||
from dibbler.lib.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()
|
|
@ -0,0 +1,19 @@
|
|||
[general]
|
||||
quit_allowed = true
|
||||
stop_allowed = false
|
||||
show_tracebacks = true
|
||||
input_encoding = 'utf8'
|
||||
|
||||
[database]
|
||||
; url = postgresql://robertem@127.0.0.1/pvvvv
|
||||
url = sqlite:///test.db
|
||||
|
||||
[limits]
|
||||
low_credit_warning_limit = -100
|
||||
user_recent_transaction_limit = 100
|
||||
|
||||
# See https://pypi.org/project/brother_ql/ for label types
|
||||
# Set rotate to False for endless labels
|
||||
[printer]
|
||||
label_type = "62"
|
||||
label_rotate = false
|
30
flake.lock
30
flake.lock
|
@ -1,12 +1,15 @@
|
|||
{
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1667395993,
|
||||
"narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=",
|
||||
"lastModified": 1692799911,
|
||||
"narHash": "sha256-3eihraek4qL744EvQXsK1Ha6C3CR7nnT8X2qWap4RNk=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f",
|
||||
"rev": "f9e7cf818399d17d347f847525c5a5a8032e4e44",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
@ -17,11 +20,11 @@
|
|||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1674839022,
|
||||
"narHash": "sha256-8F1U06t9glkgBC8gBfjoA2eeUb9MYRRp6NMKY3c0VEI=",
|
||||
"lastModified": 1693145325,
|
||||
"narHash": "sha256-Gat9xskErH1zOcLjYMhSDBo0JTBZKfGS0xJlIRnj6Rc=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "14b6cf7602c341e525a8fe17ac2349769376515e",
|
||||
"rev": "cddebdb60de376c1bdb7a4e6ee3d98355453fe56",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
@ -34,6 +37,21 @@
|
|||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
|
|
139
flake.nix
139
flake.nix
|
@ -4,125 +4,43 @@
|
|||
inputs.flake-utils.url = "github:numtide/flake-utils";
|
||||
|
||||
outputs = { self, nixpkgs, flake-utils }:
|
||||
{
|
||||
overlays.default = final: prev: {
|
||||
dibbler = prev.callPackage ./nix/dibbler.nix { };
|
||||
flake-utils.lib.eachDefaultSystem (system: let
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
in {
|
||||
packages = {
|
||||
default = self.packages.${system}.dibbler;
|
||||
dibbler = pkgs.callPackage ./nix/dibbler.nix {
|
||||
python3Packages = pkgs.python311Packages;
|
||||
};
|
||||
};
|
||||
} //
|
||||
|
||||
flake-utils.lib.eachDefaultSystem (system:
|
||||
let
|
||||
pkgs = import nixpkgs {
|
||||
inherit system;
|
||||
overlays = [ self.overlays.default ];
|
||||
apps = {
|
||||
default = self.apps.${system}.dibbler;
|
||||
dibbler = flake-utils.lib.mkApp {
|
||||
drv = self.packages.${system}.dibbler;
|
||||
};
|
||||
in {
|
||||
packages = rec {
|
||||
dibbler = pkgs.dibbler;
|
||||
# dibblerCross = pkgs.pkgsCross.aarch64-multiplatform.dibbler;
|
||||
default = dibbler;
|
||||
};
|
||||
apps = rec {
|
||||
dibbler = flake-utils.lib.mkApp {
|
||||
drv = self.packages.${system}.dibbler;
|
||||
};
|
||||
default = dibbler;
|
||||
};
|
||||
}
|
||||
) //
|
||||
};
|
||||
|
||||
{
|
||||
nixosModules.default = { config, pkgs, ... }: let
|
||||
inherit (nixpkgs.legacyPackages."x86_64-linux") lib;
|
||||
cfg = config.services.dibbler;
|
||||
in {
|
||||
options.services.dibbler = {
|
||||
package = lib.mkPackageOption pkgs "dibbler" { };
|
||||
config = lib.mkOption {
|
||||
default = ./conf.py;
|
||||
};
|
||||
};
|
||||
|
||||
config = let
|
||||
screen = "${pkgs.screen}/bin/screen";
|
||||
in {
|
||||
nixpkgs.overlays = [ self.overlays.default ];
|
||||
|
||||
boot = {
|
||||
consoleLogLevel = 0;
|
||||
enableContainers = false;
|
||||
loader.grub.enable = false;
|
||||
};
|
||||
|
||||
users = {
|
||||
groups.dibbler = { };
|
||||
users.dibbler = {
|
||||
group = "dibbler";
|
||||
extraGroups = [ "lp" ];
|
||||
isNormalUser = true;
|
||||
shell = ((pkgs.writeShellScriptBin "login-shell" "${screen} -x dibbler") // {shellPath = "/bin/login-shell";});
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
systemd.services.screen-daemon = {
|
||||
description = "Dibbler service screen";
|
||||
wantedBy = [ "default.target" ];
|
||||
serviceConfig = {
|
||||
ExecStartPre = "-${screen} -X -S dibbler kill";
|
||||
ExecStart = "${screen} -dmS dibbler -O -l ${cfg.package.override { conf = cfg.config; }}/bin/dibbler";
|
||||
ExecStartPost = "${screen} -X -S dibbler width 42 80";
|
||||
User = "dibbler";
|
||||
Group = "dibbler";
|
||||
Type = "forking";
|
||||
RemainAfterExit = false;
|
||||
Restart = "always";
|
||||
RestartSec = "5s";
|
||||
SuccessExitStatus = 1;
|
||||
};
|
||||
};
|
||||
|
||||
# https://github.com/NixOS/nixpkgs/issues/84105
|
||||
boot.kernelParams = [
|
||||
"console=ttyUSB0,9600"
|
||||
"console=tty1"
|
||||
devShells = {
|
||||
default = self.devShells.${system}.dibbler;
|
||||
dibbler = pkgs.mkShell {
|
||||
packages = with pkgs; [
|
||||
python311Packages.black
|
||||
ruff
|
||||
];
|
||||
systemd.services."serial-getty@ttyUSB0" = {
|
||||
enable = true;
|
||||
wantedBy = [ "getty.target" ]; # to start at boot
|
||||
serviceConfig.Restart = "always"; # restart when session is closed
|
||||
};
|
||||
|
||||
services = {
|
||||
openssh = {
|
||||
enable = true;
|
||||
permitRootLogin = "yes";
|
||||
};
|
||||
|
||||
getty.autologinUser = lib.mkForce "dibbler";
|
||||
udisks2.enable = false;
|
||||
};
|
||||
|
||||
networking.firewall.logRefusedConnections = false;
|
||||
console.keyMap = "no";
|
||||
programs.command-not-found.enable = false;
|
||||
i18n.supportedLocales = [ "en_US.UTF-8/UTF-8" ];
|
||||
environment.noXlibs = true;
|
||||
|
||||
documentation = {
|
||||
info.enable = false;
|
||||
man.enable = false;
|
||||
};
|
||||
|
||||
security = {
|
||||
polkit.enable = lib.mkForce false;
|
||||
audit.enable = false;
|
||||
};
|
||||
};
|
||||
};
|
||||
} //
|
||||
})
|
||||
|
||||
//
|
||||
|
||||
{
|
||||
# Note: using the module requires that you have applied the
|
||||
# overlay first
|
||||
nixosModules.default = import ./nix/module.nix;
|
||||
|
||||
images.skrot = self.nixosConfigurations.skrot.config.system.build.sdImage;
|
||||
|
||||
nixosConfigurations.skrot = nixpkgs.lib.nixosSystem {
|
||||
system = "aarch64-linux";
|
||||
modules = [
|
||||
|
@ -155,6 +73,5 @@
|
|||
})
|
||||
];
|
||||
};
|
||||
images.skrot = self.nixosConfigurations.skrot.config.system.build.sdImage;
|
||||
};
|
||||
}
|
||||
|
|
128
helpers.py
128
helpers.py
|
@ -1,128 +0,0 @@
|
|||
from db import *
|
||||
from sqlalchemy import or_, and_
|
||||
import pwd
|
||||
import subprocess
|
||||
import os
|
||||
import signal
|
||||
|
||||
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)
|
|
@ -1,4 +0,0 @@
|
|||
#!/usr/bin/python
|
||||
import db
|
||||
|
||||
db.Base.metadata.create_all(db.engine)
|
|
@ -1,47 +0,0 @@
|
|||
ALTER TABLE pvv_vv.products RENAME TO products_old;
|
||||
CREATE TABLE pvv_vv.products
|
||||
(
|
||||
product_id serial,
|
||||
bar_code character varying(13) NOT NULL,
|
||||
name character varying(45),
|
||||
price integer,
|
||||
stock integer NOT NULL,
|
||||
CONSTRAINT product_pkey PRIMARY KEY (product_id),
|
||||
CONSTRAINT barcode_unique UNIQUE (bar_code)
|
||||
)
|
||||
|
||||
INSERT INTO pvv_vv.products (bar_code, name, price, stock)
|
||||
SELECT bar_code, name, price, stock FROM products_old;
|
||||
|
||||
ALTER TABLE pvv_vv.purchase_entries RENAME TO purchase_entries_old;
|
||||
ALTER TABLE pvv_vv.purchase_entries_old
|
||||
RENAME CONSTRAINT purchase_entries_pkey TO purchase_entries_old_pkey;
|
||||
ALTER TABLE pvv_vv.purchase_entries_old
|
||||
RENAME CONSTRAINT purchase_entries_purchase_id_fkey TO purchase_entries_old_purchase_id_fkey;
|
||||
ALTER TABLE pvv_vv.purchase_entries_old
|
||||
RENAME CONSTRAINT purchase_entries_product_bar_code_fkey TO purchase_entries_old_product_bar_code_fkey;
|
||||
|
||||
CREATE TABLE pvv_vv.purchase_entries
|
||||
(
|
||||
id serial,
|
||||
purchase_id integer,
|
||||
product_id integer,
|
||||
amount integer,
|
||||
CONSTRAINT purchase_entries_pkey PRIMARY KEY (id),
|
||||
CONSTRAINT purchase_entries_product_id_fkey FOREIGN KEY (product_id)
|
||||
REFERENCES pvv_vv.products (product_id) MATCH SIMPLE
|
||||
ON UPDATE NO ACTION
|
||||
ON DELETE NO ACTION,
|
||||
CONSTRAINT purchase_entries_purchase_id_fkey FOREIGN KEY (purchase_id)
|
||||
REFERENCES pvv_vv.purchases (id) MATCH SIMPLE
|
||||
ON UPDATE NO ACTION
|
||||
ON DELETE NO ACTION
|
||||
);
|
||||
INSERT INTO purchase_entries (id, purchase_id, product_id, amount)
|
||||
SELECT peo.id, peo.purchase_id, p.product_id, peo.amount
|
||||
FROM purchase_entries_old AS peo
|
||||
JOIN products AS p ON p.bar_code = peo.product_bar_code;
|
||||
ALTER TABLE pvv_vv.transactions
|
||||
ADD COLUMN penalty integer DEFAULT 1;
|
||||
DROP TABLE products_old;
|
||||
DROP TABLE purchase_entries_old;
|
|
@ -1,38 +1,20 @@
|
|||
{ lib, python3Packages, fetchFromGitHub
|
||||
, conf ? ../conf.py
|
||||
{ lib
|
||||
, python3Packages
|
||||
, fetchFromGitHub
|
||||
}:
|
||||
|
||||
python3Packages.buildPythonApplication {
|
||||
pname = "dibbler";
|
||||
version = "unstable-2021-09-07";
|
||||
|
||||
format = "other";
|
||||
|
||||
src = lib.cleanSource ../.;
|
||||
|
||||
format = "pyproject";
|
||||
|
||||
nativeBuildInputs = with python3Packages; [ setuptools ];
|
||||
propagatedBuildInputs = with python3Packages; [
|
||||
brother-ql
|
||||
sqlalchemy
|
||||
matplotlib
|
||||
psycopg2
|
||||
python-barcode
|
||||
sqlalchemy
|
||||
];
|
||||
|
||||
preInstall = ''
|
||||
libdir=$out/lib/${python3Packages.python.libPrefix}/site-packages
|
||||
mkdir -p $out/bin $libdir
|
||||
'';
|
||||
|
||||
installPhase = ''
|
||||
runHook preInstall
|
||||
|
||||
libdir=$out/lib/${python3Packages.python.libPrefix}/site-packages
|
||||
mv * $libdir
|
||||
|
||||
cp ${conf} $libdir/
|
||||
|
||||
mv $libdir/text_based.py $out/bin/dibbler
|
||||
|
||||
runHook postInstall
|
||||
'';
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,84 @@
|
|||
{ config, pkgs, lib, ... }: let
|
||||
cfg = config.services.dibbler;
|
||||
in {
|
||||
options.services.dibbler = {
|
||||
package = lib.mkPackageOption pkgs "dibbler" { };
|
||||
config = lib.mkOption {
|
||||
default = ../conf.py;
|
||||
};
|
||||
};
|
||||
|
||||
config = let
|
||||
screen = "${pkgs.screen}/bin/screen";
|
||||
in {
|
||||
boot = {
|
||||
consoleLogLevel = 0;
|
||||
enableContainers = false;
|
||||
loader.grub.enable = false;
|
||||
};
|
||||
|
||||
users = {
|
||||
groups.dibbler = { };
|
||||
users.dibbler = {
|
||||
group = "dibbler";
|
||||
extraGroups = [ "lp" ];
|
||||
isNormalUser = true;
|
||||
shell = ((pkgs.writeShellScriptBin "login-shell" "${screen} -x dibbler") // {shellPath = "/bin/login-shell";});
|
||||
};
|
||||
};
|
||||
|
||||
systemd.services.screen-daemon = {
|
||||
description = "Dibbler service screen";
|
||||
wantedBy = [ "default.target" ];
|
||||
serviceConfig = {
|
||||
ExecStartPre = "-${screen} -X -S dibbler kill";
|
||||
ExecStart = "${screen} -dmS dibbler -O -l ${cfg.package}/bin/dibbler --config ${cfg.config} loop";
|
||||
ExecStartPost = "${screen} -X -S dibbler width 42 80";
|
||||
User = "dibbler";
|
||||
Group = "dibbler";
|
||||
Type = "forking";
|
||||
RemainAfterExit = false;
|
||||
Restart = "always";
|
||||
RestartSec = "5s";
|
||||
SuccessExitStatus = 1;
|
||||
};
|
||||
};
|
||||
|
||||
# https://github.com/NixOS/nixpkgs/issues/84105
|
||||
boot.kernelParams = [
|
||||
"console=ttyUSB0,9600"
|
||||
"console=tty1"
|
||||
];
|
||||
systemd.services."serial-getty@ttyUSB0" = {
|
||||
enable = true;
|
||||
wantedBy = [ "getty.target" ]; # to start at boot
|
||||
serviceConfig.Restart = "always"; # restart when session is closed
|
||||
};
|
||||
|
||||
services = {
|
||||
openssh = {
|
||||
enable = true;
|
||||
permitRootLogin = "yes";
|
||||
};
|
||||
|
||||
getty.autologinUser = lib.mkForce "dibbler";
|
||||
udisks2.enable = false;
|
||||
};
|
||||
|
||||
networking.firewall.logRefusedConnections = false;
|
||||
console.keyMap = "no";
|
||||
programs.command-not-found.enable = false;
|
||||
i18n.supportedLocales = [ "en_US.UTF-8/UTF-8" ];
|
||||
environment.noXlibs = true;
|
||||
|
||||
documentation = {
|
||||
info.enable = false;
|
||||
man.enable = false;
|
||||
};
|
||||
|
||||
security = {
|
||||
polkit.enable = lib.mkForce false;
|
||||
audit.enable = false;
|
||||
};
|
||||
};
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
[build-system]
|
||||
requires = ["setuptools", "setuptools-scm"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "dibbler"
|
||||
authors = []
|
||||
description = "EDB-system for PVV"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.11"
|
||||
license = {text = "BSD-3-Clause"}
|
||||
classifiers = [
|
||||
"Programming Language :: Python :: 3",
|
||||
]
|
||||
dependencies = [
|
||||
"SQLAlchemy >= 2.0, <2.1",
|
||||
"brother-ql",
|
||||
"matplotlib",
|
||||
"psycopg2 >= 2.8, <2.10",
|
||||
"python-barcode",
|
||||
]
|
||||
dynamic = ["version"]
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
include = ["dibbler*"]
|
||||
|
||||
[project.scripts]
|
||||
dibbler = "dibbler.main:main"
|
||||
|
||||
[tool.black]
|
||||
line-length = 100
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 100
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
brother-ql >= 0.9.4, <1
|
||||
SQLAlchemy >= 1.3.8, <1.4
|
||||
psycopg2-binary >= 2.8.3, <2.9
|
||||
python-barcode >= 0.10, <0.11
|
|
@ -1,9 +0,0 @@
|
|||
#!/usr/bin/python
|
||||
from db import *
|
||||
# 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}")
|
|
@ -1,47 +0,0 @@
|
|||
\documentclass[a4paper]{article}
|
||||
\usepackage[utf8]{inputenc}
|
||||
\usepackage{fullpage}
|
||||
\usepackage{pstricks}
|
||||
\usepackage{pst-barcode}
|
||||
|
||||
\title{Dibbler Special Barcodes}
|
||||
\date{}
|
||||
\begin{document}
|
||||
|
||||
\maketitle
|
||||
\setlength\tabcolsep{25pt}
|
||||
|
||||
|
||||
\begin{tabular}{ccc}
|
||||
\begin{pspicture}(3,1.2in)
|
||||
\psbarcode[scalex=0.8,scaley=0.8]{4242424242420}{includetext guardwhitespace}{ean13}
|
||||
\end{pspicture}
|
||||
&
|
||||
\begin{pspicture}(3,1.2in)
|
||||
\psbarcode[scalex=0.8,scaley=0.8]{7640140330815}{includetext guardwhitespace}{ean13}
|
||||
\end{pspicture}
|
||||
&
|
||||
\begin{pspicture}(3,1.2in)
|
||||
\psbarcode[scalex=0.8,scaley=0.8]{7024850087007}{includetext guardwhitespace}{ean13}
|
||||
\end{pspicture}
|
||||
\\
|
||||
PVV-skjorte &
|
||||
Nespresso-kapsel &
|
||||
Risengrynsgraut
|
||||
\\
|
||||
\begin{pspicture}(3,1.2in)
|
||||
\psbarcode[scalex=0.8,scaley=0.8]{7024850087014}{includetext guardwhitespace}{ean13}
|
||||
\end{pspicture}
|
||||
&
|
||||
\begin{pspicture}(3,1.2in)
|
||||
\psbarcode[scalex=0.8,scaley=0.8]{5000159410946}{includetext guardwhitespace}{ean13}
|
||||
\end{pspicture}
|
||||
&
|
||||
\\
|
||||
Rømmegraut
|
||||
&
|
||||
Snickers mini
|
||||
&
|
||||
\end{tabular}
|
||||
|
||||
\end{document}
|
|
@ -1,45 +0,0 @@
|
|||
\documentclass[a4paper]{article}
|
||||
\usepackage[utf8]{inputenc}
|
||||
\usepackage{fullpage}
|
||||
\usepackage{pstricks}
|
||||
\usepackage{pst-barcode}
|
||||
\usepackage{graphicx}
|
||||
|
||||
\title{User Barcodes}
|
||||
\date{}
|
||||
\begin{document}
|
||||
%
|
||||
|
||||
\maketitle
|
||||
\setlength\tabcolsep{40pt}
|
||||
|
||||
|
||||
%
|
||||
\begin{tabular}{ccc}
|
||||
\includegraphics[width=0.20\textwidth]{userimages/alfkjempestor}
|
||||
&
|
||||
\includegraphics[width=0.20\textwidth]{userimages/tirilane}
|
||||
\\
|
||||
\begin{pspicture}(3,1.0in)
|
||||
\psbarcode[scalex=0.8,scaley=0.8]{NTNU457343}{includetext guardwhitespace}{code39}
|
||||
\end{pspicture}
|
||||
&
|
||||
\begin{pspicture}(3,1.0in)
|
||||
\psbarcode[scalex=0.8,scaley=0.8]{NTNU318657}{includetext guardwhitespace}{code39}
|
||||
\end{pspicture}
|
||||
\\
|
||||
\includegraphics[width=0.20\textwidth]{userimages/oysteini}
|
||||
&
|
||||
\includegraphics[width=0.20\textwidth]{userimages/almelid}
|
||||
\\
|
||||
\begin{pspicture}(3,1.0in)
|
||||
\psbarcode[scalex=0.8,scaley=0.8]{NTNU458221}{includetext guardwhitespace}{code39}
|
||||
\end{pspicture}
|
||||
&
|
||||
\begin{pspicture}(3,1.0in)
|
||||
\psbarcode[scalex=0.8,scaley=0.8]{ALMELID}{includetext guardwhitespace}{code39}
|
||||
\end{pspicture}
|
||||
\\
|
||||
\end{tabular}
|
||||
%
|
||||
\end{document}
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
197
statistikk.py
197
statistikk.py
|
@ -1,197 +0,0 @@
|
|||
#! /usr/bin/env python
|
||||
# -*- coding: UTF-8 -*-
|
||||
import matplotlib.pyplot as plt
|
||||
from statistikkHelpers import *
|
||||
import matplotlib.dates as mdates
|
||||
|
||||
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
|
||||
#---------------------------------------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)
|
|
@ -1,417 +0,0 @@
|
|||
#! /usr/bin/env python
|
||||
# -*- coding: UTF-8 -*-
|
||||
import datetime
|
||||
from collections import defaultdict
|
||||
import operator
|
||||
from helpers import *
|
||||
import sys
|
||||
import db
|
||||
|
||||
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)
|
62
test.py
62
test.py
|
@ -1,62 +0,0 @@
|
|||
import os
|
||||
|
||||
import datetime
|
||||
from PIL import ImageFont
|
||||
from brother_ql.devicedependent import label_type_specs
|
||||
|
||||
from printer_helpers import print_bar_code
|
||||
|
||||
#label_type = "29x90"
|
||||
#rotate = True
|
||||
#barcode_value = "7050122105438"
|
||||
#barcode_text = "Chips"
|
||||
#printer_type = "QL-700"
|
||||
|
||||
|
||||
from PIL import Image, ImageMode, ImageDraw
|
||||
|
||||
|
||||
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_name_label("chrivi", label_type="29x90", rotate=True)
|
|
@ -1,72 +0,0 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import random
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
from helpers import *
|
||||
from text_interface.addstock import AddStockMenu
|
||||
from text_interface.buymenu import BuyMenu
|
||||
from text_interface.editing import *
|
||||
from text_interface.faq import FAQMenu
|
||||
from text_interface.helpermenus import Menu
|
||||
from text_interface.mainmenu import MainMenu
|
||||
from text_interface.miscmenus import ProductSearchMenu, TransferMenu, AdjustCreditMenu, UserListMenu, ShowUserMenu, \
|
||||
ProductListMenu
|
||||
from text_interface.printermenu import PrintLabelMenu
|
||||
from text_interface.stats import *
|
||||
|
||||
random.seed()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if not conf.stop_allowed:
|
||||
signal.signal(signal.SIGQUIT, signal.SIG_IGN)
|
||||
|
||||
if not conf.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 conf.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 conf.show_tracebacks:
|
||||
traceback.print_tb(sys.exc_info()[2])
|
||||
else:
|
||||
break
|
||||
print('Restarting main menu.')
|
|
@ -1,8 +0,0 @@
|
|||
# -*- 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']
|
|
@ -1,186 +0,0 @@
|
|||
import sqlalchemy
|
||||
from db import User, Product
|
||||
from text_interface.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)
|
|
@ -1,200 +0,0 @@
|
|||
import conf
|
||||
import sqlalchemy
|
||||
from db import Transaction, Product, User
|
||||
from helpers import less
|
||||
from text_interface.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(
|
||||
conf.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, conf.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()
|
|
@ -1,34 +0,0 @@
|
|||
import re
|
||||
|
||||
from db import Product, User
|
||||
from printer_helpers import print_bar_code, print_name_label
|
||||
from text_interface.helpermenus import Menu
|
||||
import conf
|
||||
|
||||
|
||||
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=conf.label_rotate,
|
||||
printer_type="QL-700", label_type=conf.label_type)
|
||||
elif isinstance(thing, User):
|
||||
print_name_label(text=thing.name, label_type=conf.label_type, rotate=conf.label_rotate,
|
||||
printer_type="QL-700")
|
|
@ -1,102 +0,0 @@
|
|||
import sqlalchemy
|
||||
from db import PurchaseEntry, Product, User
|
||||
from helpers import less
|
||||
from sqlalchemy import desc
|
||||
from sqlalchemy import func
|
||||
from statistikkHelpers import statisticsTextOnly
|
||||
from text_interface.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(sqlalchemy.func.sum(User.credit)).filter(User.credit > 0).first()[0]
|
||||
total_negative_credit = self.session.query(sqlalchemy.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()
|
|
@ -1,5 +0,0 @@
|
|||
db_url = 'postgresql://torjehoa_dibblerdummy:1234@postgres/torjehoa_dibblerdummy'
|
||||
quit_allowed = True
|
||||
stop_allowed = False
|
||||
show_tracebacks = True
|
||||
input_encoding = 'utf8'
|
265
ui.py
265
ui.py
|
@ -1,265 +0,0 @@
|
|||
import curses
|
||||
#from copy import deepcopy
|
||||
#import curses.panel
|
||||
import curses.textpad
|
||||
#import time
|
||||
import curses.ascii
|
||||
from db import *
|
||||
|
||||
|
||||
def cycle(list, index, up):
|
||||
if index <= 0 and up:
|
||||
return len(list)-1
|
||||
elif up:
|
||||
return index - 1
|
||||
elif index >= len(list)-1:
|
||||
return 0
|
||||
else:
|
||||
return index + 1
|
||||
|
||||
class MainMenu():
|
||||
def __init__(self, screen):
|
||||
self.screen = screen
|
||||
curses.curs_set(0) # hide the cursor
|
||||
self.size = screen.getmaxyx() # get screen size
|
||||
self.choices = [SubMenu("Purchase"), ChargeMenu(self.screen), SubMenu("View Transactions")]
|
||||
self.selected = 0
|
||||
|
||||
self.execute()
|
||||
|
||||
def execute(self):
|
||||
while 1:
|
||||
self.screen.clear()
|
||||
self.screen.border()
|
||||
for i in range(len(self.choices)):
|
||||
if i == self.selected:
|
||||
self.screen.addstr(i+1,1, str(i+1) + ") " + self.choices[i].text, curses.A_REVERSE)
|
||||
else:
|
||||
self.screen.addstr(i+1,1, str(i+1) + ") " + self.choices[i].text)
|
||||
self.screen.refresh()
|
||||
c = self.screen.getch()
|
||||
if c == ord('q') or c == 27:
|
||||
break
|
||||
elif c == 10: #return key
|
||||
self.choices[self.selected].execute()
|
||||
elif c == curses.KEY_UP:
|
||||
self.selected = cycle(self.choices, self.selected, True)
|
||||
elif c == curses.KEY_DOWN:
|
||||
self.selected = cycle(self.choices, self.selected, False)
|
||||
elif c >= 49 and c <= 48+len(self.choices): #number key
|
||||
self.choices[c-49].execute()
|
||||
|
||||
class SubMenu():
|
||||
def __init__(self, text):
|
||||
self.text = text
|
||||
|
||||
|
||||
class ChargeMenu(SubMenu):
|
||||
def __init__(self, screen):
|
||||
self.text = "Charge"
|
||||
self.screen = screen
|
||||
# self.size = self.screen.getmaxyx()
|
||||
# self.marked = 0
|
||||
# self.textbox = False
|
||||
# self.textpad = False
|
||||
# self.textwindow = False
|
||||
# self.edit_area = False
|
||||
# self.search_text = ""
|
||||
# self.session = False
|
||||
|
||||
def execute(self):
|
||||
self.make_windows()
|
||||
self.resultview = Selectable(self.resultwindow)
|
||||
|
||||
# Initialize the variables
|
||||
|
||||
self.marked = 0
|
||||
self.search_text = ""
|
||||
self.amount = ""
|
||||
# curses.curs_set(1)
|
||||
self.screen.move(2,2)
|
||||
self.screen.leaveok(1)
|
||||
self.session = Session()
|
||||
while 1:
|
||||
self.draw()
|
||||
c = self.screen.getch()
|
||||
if c == 27:
|
||||
break
|
||||
elif c == 9:
|
||||
self.switch_cursor()
|
||||
elif c == curses.KEY_RESIZE:
|
||||
self.resize()
|
||||
elif self.marked == 0:
|
||||
self.textpad_edit(c)
|
||||
self.textwindow.cursyncup()
|
||||
elif self.marked == 1:
|
||||
# self.amountpad.do_command(curses.ascii.SOH)
|
||||
# for char in self.amount:
|
||||
# self.amountpad.do_command(ord(char))
|
||||
# self.amountpad.do_command(curses.KEY_LEFT)
|
||||
self.amountpad_edit(c)
|
||||
self.amountwindow.cursyncup()
|
||||
self.check_calculation()
|
||||
elif self.marked == 2:
|
||||
self.resultview.do_command(c)
|
||||
self.check_calculation()
|
||||
self.session.close()
|
||||
|
||||
def check_calculation(self):
|
||||
if self.amount and self.resultview.list:
|
||||
self.set_calculation()
|
||||
else:
|
||||
self.calculation.clear()
|
||||
|
||||
|
||||
|
||||
def draw(self):
|
||||
# if self.marked == 0:
|
||||
# (y,x) = self.textwindow.getyx()
|
||||
# y += 1
|
||||
# x += 1
|
||||
# else:
|
||||
# (y,x) = self.screen.getyx()
|
||||
self.screen.clear()
|
||||
self.screen.border()
|
||||
self.textwindow.border()
|
||||
self.amountwindow.border()
|
||||
if self.marked == 0:
|
||||
self.textwindow.addstr(0,1, "[Username, card number or RFID]",curses.A_REVERSE)
|
||||
self.amountwindow.addstr(0,1,"[Amount to be transferred]")
|
||||
elif self.marked == 1:
|
||||
self.textwindow.addstr(0,1, "[Username, card number or RFID]")
|
||||
self.amountwindow.addstr(0,1,"[Amount to be transferred]",curses.A_REVERSE)
|
||||
else:
|
||||
self.textwindow.addstr(0,1, "[Username, card number or RFID]")
|
||||
self.amountwindow.addstr(0,1,"[Amount to be transferred]")
|
||||
self.resultview.draw()
|
||||
self.textwindow.addstr(1,1,self.search_text)
|
||||
self.amountwindow.addstr(1,1,self.amount)
|
||||
self.calculation.draw()
|
||||
# curses.curs_set(1)
|
||||
# self.screen.move(y,x)
|
||||
# curses.setsyx(y,x)
|
||||
# self.textwindow.move(y-2,x-2)
|
||||
self.screen.refresh()
|
||||
|
||||
def make_windows(self):
|
||||
self.size = self.screen.getmaxyx()
|
||||
self.textwindow = self.screen.subwin(3,self.size[1]/2-1,1,1)
|
||||
self.amountwindow = self.screen.subwin(3,self.size[1]/2-1,1,self.size[1]/2)
|
||||
self.edit_area = self.textwindow.subwin(1,self.size[1]/2-3,2,2)
|
||||
self.amount_area = self.amountwindow.subwin(1,self.size[1]/2-3,2,self.size[1]/2+1)
|
||||
self.resultwindow = self.screen.subwin(self.size[0]-5,self.size[1]/2-1,4,1)
|
||||
self.textpad = curses.textpad.Textbox(self.edit_area)
|
||||
self.textpad.stripspaces = True
|
||||
self.amountpad = curses.textpad.Textbox(self.amount_area)
|
||||
self.amountpad.stripspaces = True
|
||||
self.calcwindow = self.screen.subwin(self.size[0]-8,self.size[1]/2-1,4,self.size[1]/2)
|
||||
self.calculation = Calculation(self.calcwindow)
|
||||
|
||||
def resize(self):
|
||||
self.make_windows()
|
||||
self.resultview.window = self.resultwindow
|
||||
self.calculation.window = self.calcwindow
|
||||
|
||||
def switch_cursor(self):
|
||||
if self.marked == 4:
|
||||
# curses.curs_set(1)
|
||||
self.screen.move(2,1+len(self.search_text))
|
||||
self.marked = 0
|
||||
# self.textpad.do_command(curses.ascii.SOH)
|
||||
elif self.marked == 0:
|
||||
self.marked += 1
|
||||
self.screen.move(2,self.size[1]/2+2)
|
||||
else:
|
||||
curses.curs_set(0)
|
||||
self.marked += 1
|
||||
|
||||
def textpad_edit(self, ch):
|
||||
self.textpad.do_command(ch)
|
||||
self.search_text = self.textpad.gather().strip()
|
||||
self.resultview.set_list(self.session.query(User).filter(or_(User.user.like(str('%'+self.search_text+'%')),User.id.like('%'+self.search_text+'%'))).all())
|
||||
# self.resultview.draw()
|
||||
# self.resultwindow.refresh()
|
||||
|
||||
def amountpad_edit(self,ch):
|
||||
if ch >= 48 and ch <= 57:
|
||||
self.amountpad.do_command(ch)
|
||||
elif ch <= 31 or ch > 255:
|
||||
self.amountpad.do_command(ch)
|
||||
else:
|
||||
pass
|
||||
self.amount = self.amountpad.gather().strip()
|
||||
|
||||
def set_calculation(self):
|
||||
self.calculation.set_numbers([self.resultview.list[self.resultview.selected].credit, int(self.amount)])
|
||||
|
||||
class Selectable():
|
||||
def __init__(self, window, list = [], selected = 0):
|
||||
self.list=list
|
||||
self.window = window
|
||||
self.selected = selected
|
||||
# self.attribute = attribute
|
||||
|
||||
def draw(self):
|
||||
self.window.border()
|
||||
for i in range(len(self.list)):
|
||||
if i == self.selected:
|
||||
self.window.addstr(i+1,1,self.list[i].user,curses.A_REVERSE)
|
||||
else:
|
||||
self.window.addstr(i+1,1,self.list[i].user)
|
||||
self.window.addstr(0,1,"[Search results]")
|
||||
|
||||
def do_command(self,c):
|
||||
if c == curses.KEY_UP:
|
||||
self.selected = cycle(self.list, self.selected, True)
|
||||
if c == curses.KEY_DOWN:
|
||||
self.selected = cycle(self.list, self.selected, False)
|
||||
|
||||
def set_list(self,list):
|
||||
if len(list)-1 < self.selected:
|
||||
self.selected = len(list)-1
|
||||
self.list = list
|
||||
else:
|
||||
self.list = list
|
||||
|
||||
class Calculation():
|
||||
def __init__(self, window):
|
||||
self.window = window
|
||||
self.numbers = []
|
||||
self.size = self.window.getmaxyx()
|
||||
|
||||
def draw(self):
|
||||
self.window.clear()
|
||||
self.window.border()
|
||||
self.length = len(self.numbers)
|
||||
if self.length > 0:
|
||||
if self.size[0] >= self.length:
|
||||
for i in range(self.length-1):
|
||||
self.window.addstr((self.size[0]-self.length)/2+i,(self.size[1]+4-len(str(abs(self.numbers[i]))))/2,str(abs(self.numbers[i])))
|
||||
if i > 0:
|
||||
if self.numbers[i] >= 0:
|
||||
self.window.addstr((self.size[0]-self.length)/2+i,(self.size[1]-8)/2,'+')
|
||||
else:
|
||||
self.window.addstr((self.size[0]-self.length)/2+i,(self.size[1]-8)/2,'-')
|
||||
if self.numbers[self.length-1] >= 0:
|
||||
self.window.addstr((self.size[0]+self.length)/2-1,(self.size[1]-8)/2,'+'+(7-len(str(self.numbers[self.length-1])))*" "+str(self.numbers[self.length-1]),curses.A_UNDERLINE)
|
||||
else:
|
||||
self.window.addstr((self.size[0]+self.length)/2-1,(self.size[1]-8)/2,'-'+(7-len(str(abs(self.numbers[self.length-1]))))*" "+str(abs(self.numbers[self.length-1])),curses.A_UNDERLINE)
|
||||
self.window.addstr((self.size[0]+self.length)/2,(self.size[1]-8)/2,'='+(7-len(str(self.sum)))*" "+str(self.sum),curses.A_UNDERLINE)
|
||||
|
||||
|
||||
def add(self):
|
||||
self.sum = 0
|
||||
for item in self.numbers:
|
||||
self.sum += item
|
||||
|
||||
def clear(self):
|
||||
self.numbers = []
|
||||
self.sum = 0
|
||||
|
||||
def set_numbers(self, list):
|
||||
self.numbers = list
|
||||
self.add()
|
||||
|
||||
curses.wrapper(MainMenu)
|
|
@ -1,37 +0,0 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import sys
|
||||
import db
|
||||
from helpers import *
|
||||
|
||||
# Writes a log of all transactions to a text file.
|
||||
#
|
||||
# Usage:
|
||||
# ./write_logfile.py filename
|
||||
# or (writing to stdout):
|
||||
# ./write_logfile.py
|
||||
|
||||
def write_log(f):
|
||||
session = Session()
|
||||
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()
|
||||
|
||||
if len(sys.argv) < 2:
|
||||
write_log(sys.stdout)
|
||||
else:
|
||||
filename = sys.argv[1]
|
||||
print('Writing log to ' + filename)
|
||||
with open(filename, 'w') as f:
|
||||
write_log(f)
|
Loading…
Reference in New Issue