Restructure project #3

Merged
h7x4 merged 10 commits from restructure-project into master 2023-09-02 21:18:04 +02:00
49 changed files with 597 additions and 10337 deletions
Showing only changes of commit c25e5cec27 - Show all commits

4
.gitignore vendored
View File

@ -1,2 +1,4 @@
result
result-*
result-*
dist

30
ALTER
View File

@ -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=>

BIN
data

Binary file not shown.

View File

@ -1,7 +1,4 @@
{ pkgs ? import <nixos-unstable> { } }:
rec {
{
dibbler = pkgs.callPackage ./nix/dibbler.nix { };
}

0
dibbler/__init__.py Normal file
View File

24
dibbler/cli.py Normal file
View File

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

6
dibbler/conf.py Normal file
View File

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

View File

@ -1,10 +1,12 @@
from db import *
from sqlalchemy import or_, and_
import pwd
import subprocess
import os
import signal
from sqlalchemy import or_, and_
from .models.db import *
def search_user(string, session, ignorethisflag=None):
string = string.lower()
exact_match = session.query(User).filter(or_(User.name == string, User.card == string, User.rfid == string)).first()

View File

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

View File

@ -1,8 +1,15 @@
from math import ceil
import sqlalchemy
from db import Product, User, Transaction, PurchaseEntry, Purchase
from text_interface.helpermenus import Menu
from dibbler.models.db import (
Product,
Purchase,
PurchaseEntry,
Transaction,
User,
)
from .helpermenus import Menu
class AddStockMenu(Menu):

View File

@ -1,7 +1,15 @@
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.db import (
Product,
Purchase,
PurchaseEntry,
Transaction,
User,
)
from .helpermenus import Menu
class BuyMenu(Menu):
@ -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,7 @@ 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("")
@ -158,8 +166,8 @@ When finished, write an empty line to confirm the purchase.\n'''
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},',
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.

View File

@ -1,6 +1,7 @@
import sqlalchemy
from db import User, Product
from text_interface.helpermenus import Menu, Selector
from dibbler.models.db import User, Product
from .helpermenus import Menu, Selector
__all__ = ["AddUserMenu", "AddProductMenu", "EditProductMenu", "AdjustStockMenu", "CleanupStockMenu", "EditUserMenu"]

View File

@ -5,10 +5,20 @@ 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.models.db import User, Session
from dibbler.helpers import (
search_user,
search_product,
guess_data_type,
argmax,
)
from . import (
context_commands,
local_help_commands,
help_commands,
exit_commands,
)
class ExitMenu(Exception):

View File

@ -4,11 +4,12 @@ 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.models.db import Session
from . import faq_commands, restart_commands
from .buymenu import BuyMenu
from .faq import FAQMenu
from .helpermenus import Menu
def restart():

View File

@ -1,8 +1,10 @@
import conf
import sqlalchemy
from db import Transaction, Product, User
from helpers import less
from text_interface.helpermenus import Menu, Selector
from dibbler.conf import config
from dibbler.models.db import Transaction, Product, User
from dibbler.helpers import less
from .helpermenus import Menu, Selector
class TransferMenu(Menu):
@ -53,12 +55,12 @@ class ShowUserMenu(Menu):
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) + ')'),
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, conf.user_recent_transaction_limit)
self.print_transactions(user, config.getint('limits', 'user_recent_transaction_limit'))
elif what == 'products':
self.print_purchased_products(user)
elif what == 'transactions-all':

View File

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

View File

@ -1,10 +1,10 @@
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
from sqlalchemy import desc, func
from dibbler.helpers import less
from dibbler.models.db import PurchaseEntry, Product, User
from dibbler.statistikkHelpers import statisticsTextOnly
from .helpermenus import Menu
__all__ = ["ProductPopularityMenu", "ProductRevenueMenu", "BalanceMenu", "LoggedStatisticsMenu"]
@ -77,8 +77,8 @@ class BalanceMenu(Menu):
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_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

View File

View File

@ -1,15 +1,16 @@
from math import ceil, floor
import datetime
from sqlalchemy import Column, Integer, String, ForeignKey, create_engine, DateTime, Boolean
from sqlalchemy.orm import sessionmaker, relationship, backref
from sqlalchemy.ext.declarative import declarative_base
from math import ceil, floor
import datetime
import conf
engine = create_engine(conf.db_url)
from dibbler.conf import config
engine = create_engine(config.get('database', 'url'))
Base = declarative_base()
Session = sessionmaker(bind=engine)
class User(Base):
__tablename__ = 'users'
name = Column(String(10), primary_key=True)

View File

@ -1,16 +1,13 @@
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",):

View File

View File

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

View File

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

View File

@ -1,11 +1,11 @@
#! /usr/bin/env python
# -*- coding: UTF-8 -*-
import datetime
from collections import defaultdict
import operator
from helpers import *
import sys
import db
from .helpers import *
from .models.db import *;
def getUser():
while 1:

View File

@ -5,26 +5,36 @@ 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 *
from .helpers import *
from .menus.addstock import AddStockMenu
from .menus.buymenu import BuyMenu
from .menus.editing import *
from .menus.faq import FAQMenu
from .menus.helpermenus import Menu
from .menus.mainmenu import MainMenu
from .menus.miscmenus import (
ProductSearchMenu,
TransferMenu,
AdjustCreditMenu,
UserListMenu,
ShowUserMenu,
ProductListMenu,
)
from .menus.printermenu import PrintLabelMenu
from .menus.stats import *
from .conf import config
random.seed()
if __name__ == '__main__':
if not conf.stop_allowed:
def main():
if not config.getboolean('general', 'stop_allowed'):
signal.signal(signal.SIGQUIT, signal.SIG_IGN)
if not conf.stop_allowed:
if not config.getboolean('general', 'stop_allowed'):
signal.signal(signal.SIGTSTP, signal.SIG_IGN)
main = MainMenu('Dibbler main menu',
@ -53,7 +63,7 @@ if __name__ == '__main__':
],
exit_msg='happy happy joy joy',
exit_confirm_msg='Really quit Dibbler?')
if not conf.quit_allowed:
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
@ -65,8 +75,12 @@ if __name__ == '__main__':
except:
print('Something went wrong.')
print(f'{sys.exc_info()[0]}: {sys.exc_info()[1]}')
if conf.show_tracebacks:
if config.getboolean('general', 'show_tracebacks'):
traceback.print_tb(sys.exc_info()[2])
else:
break
print('Restarting main menu.')
if __name__ == '__main__':
main()

18
example-config.ini Normal file
View File

@ -0,0 +1,18 @@
[general]
quit_allowed = true
stop_allowed = false
show_tracebacks = true
input_encoding = 'utf8'
[database]
url = postgresql://robertem@127.0.0.1/pvvvv
[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

View File

@ -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",