Restructure project #3

Merged
h7x4 merged 10 commits from restructure-project into master 2023-09-02 21:18:04 +02:00
67 changed files with 2687 additions and 11798 deletions

8
.gitignore vendored
View File

@ -1,2 +1,8 @@
result
result-*
result-*
dist
test.db
.ruff_cache

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

View File

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

BIN
data

Binary file not shown.

176
db.py
View File

@ -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()

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

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()

7
dibbler/db.py Normal file
View File

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

View File

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

130
dibbler/lib/helpers.py Normal file
View File

@ -0,0 +1,130 @@
import pwd
import subprocess
import os
import signal
from sqlalchemy import or_, and_
from ..models 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)

View File

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

View File

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