Skip to content

Commit 099bd72

Browse files
committed
Initial commit
0 parents  commit 099bd72

File tree

3 files changed

+159
-0
lines changed

3 files changed

+159
-0
lines changed

einkauf.db

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"items_per_section": {"Obst/Gem\u00fcse": ["Zwiebel", "M\u00f6hren", "Zuccini", "Aubergine", "Staudensellerie mit Bl\u00e4ttern", "Knoblauch (Knolle)", "Oliven\u00f6l", "Petersilie", "Basilikum", "Tomaten (m\u00f6glichst ohne viel Wasser - meistens als Chef-Tomaten oder so bezeichnet)", "Zitrone", "frische Himbeeren", "Suppengem\u00fcse-Set (Knollenselerie+M\u00f6hren+....)", "Tomaten", "Knoblauchzehen", "gutes Oliven\u00f6l", "Basilikum, frisch", "Knollen-Sellerie", "Zehen Knoblauch", "Kartoffeln", "Porree", "Bio-M\u00f6hren", "Raps\u00d6l zum Anbraten", "Cocktailtomaten", "Salatgurke", "Paprika", "Kohlrabi", "Limetten", "Bananen", "\u00c4pfel", "Birnen", "Orangen", "Mandarinen", "Kiwis", "Pflaumen", "Trauben kernlos gr\u00fcn", "Trauben kernlos rot", "Radieschen", "Kartoffeln mehlig kochend", "Zucchini", "Knoblauch", "Shitake Pilze", "Brokkoli", "Lauchzwiebeln", "Schalotten (Zwiebeln tun es notfalls auch)", "Austernpilze", "Kreuzk\u00fcmmelsamen", "Spinat Frisch", "Rote Zwiebeln", "Hokkaido K\u00fcrbis mid. 400gr", "Limette", "Karotten", "Koriander", "Cashew Bio", "Ingwer", "Salat", "Gurke"], "Milch & Co": ["Schmand oder Cr\u00e8me fra\u00eeche", "Milch", "Sahne", "Naturjoghurt", "Butter", "Creme fraiche", "Oatly Barista Edition", "Frischmilch 3,5% Fett", "Frischmilch 3,5% Fett zus\u00e4tzlich (zweckgebunden f\u00fcr Abendbrei Nora)", "Eier", "Margarine", "Ei", "Hafermilch", "Alsan oder \u00e4hnlich", "Buttermilch natur", "Joghurt (am liebsten Bio) Himbeer, Zitrone oder Erdbeer", "1 Glas griechischer Joghurt Ehrmann 10 % Fett", "Joghurt natur 500gr"], "K\u00fchltheke K\u00e4se": ["Geriebener K\u00e4se (Gauda oder Emmentaler oder Mischung aus beidem)", "Mascarpone", "Parmesan", "Geriebener K\u00e4se gern w\u00fcrziger wie Bergk\u00e4se, ansonsten normal (falls m\u00f6glich labfrei... aber wird's bei Bergk\u00e4se wohl eher nicht geben, daher dann lieber Bergk\u00e4se statt labfreier Gouda :)", "Scheibenk\u00e4se Gouda (am besten mittelalt) mikobielles Lab/ Lab Austausstoff", "Weichk\u00e4se", "Frischk\u00e4se Kr\u00e4uter", "Frischk\u00e4se Brunch Tomaten", "Ziegenk\u00e4se in Scheiben", "Parmesan (optional bzw. falls von der Bolognese \u00fcbrig)"], "Feculants": ["Lasagneplatten", "Pasta", "Mehl", "Buchweizenmehl", "Weizenmehl Typ 550", "Mehl 405 oder sonstiges", "Weizenmehl", "Penne", "Fussili", "Reis", "Fusilli", "Graupen", "Tagilatelle"], "Dosen": ["Tomaten, passiert oder gehackt (oder eine Mischung daraus)", "Tomatenmark", "Gehackte Tomaten", "Dosenpilze", "Maisdose", "Tomatenmark (Menge in Tube)", "st\u00fcckige Tomaten", "Kichererbsen getrocknet", "Dosen St\u00fcckige Tomaten"], "Fr\u00fchst\u00fccksareal": ["Chiabatta-Brot (Frisch)", "Kaffee, gemahlen", "Marmelade, (Himbeere+..)", "Nutella", "Veganer Aufstrich tomaten oder so", "Honig, fest", "Haferflocken (fein/zart)", "Schokom\u00fcsli weniger s\u00fcss (Dr. Oetker o. K\u00f6lln)", "Aufbackbr\u00f6tchen Weizen", "Aufbackbr\u00f6tchen K\u00f6rner", "Ein lockeres Graubrot", "Tee Fenchel Anis K\u00fcmmel"], "Einmach (in Gl\u00e4sern)": ["Apfelkompott aus dem Glas", "Essiggurken", "Kirschen im Glas", "gr\u00fcne Oliven ohne Stein aus dem Glas ohne Kr\u00e4uter (gibt es beim Aldi auch)", "Pilze im Glas"], "TK": ["gefrorene Himbeeren", "Blattspinat TK", "Erbsen (TK)", "Petersilie (TK)", "S\u00fcsskartoffelpommes", "P\u00f6mmes", "Morzarella sticks", "Riffelkroketten", "Fischst\u00e4bchen", "TK Beerenmischung", "Rahmspinat TK (in W\u00fcrfeln, portionierbar) fein gehackt"], "Backzubeh\u00f6r & Co": ["Mandelbl\u00e4ttchen", "Vanillezucker (kein Vanellinzucker)", "Leinsamenschrot (nicht ent\u00f6lt!)", "Backpulver", "Puderzucker", "Zucker", "Vanillezucker", "Trockenhefe (f\u00fcr die schnelle Kindervariante)", "Vanillepuddingpulver", "Rosinen"], "Fleischtheke": ["Hack (Rind)", "Speckw\u00fcrfel", "Bacon", "leckeren gekochten Schinken", "kleine N\u00fcrnberger Rostbratwurst (gibt es beim Aldi)"], "Alkohol": ["Kochwein Rot", "Alkoholfreier Rotwein", "Kochwein (rot)"], "Getr\u00e4nke": ["Pfanner Gr\u00fcner Tee um den Einkaufslistengott zu bes\u00e4nftigen", "Traubensaft (gern den vom Aldi)"], "Internationales & Saucen (hinter Gem\u00fcse)": ["Gem\u00fcsebr\u00fche", "Tomatenso\u00dfe (einfach, ohne Fleisch z.B. Barilla Basilico)", "Senf Thomy mittelscharf", "Sojasauce", "Kokosmilch", "Currypaste (vegan)", "Weisse Misopasto", "Gem\u00fcsebr\u00fche in D\u00f6ppen", "Creamed Coconut", "Kokusmilch 400ml"], "K\u00fchltheke Salate": ["Veggi Hackfleisch, z.B. M\u00fchlen-Hack", "Vegetarische Maultaschen (falls nicht verf\u00fcgbar, normale Maultaschen und f\u00fcr Veggis vegetarische Alternative kaufen (z.B. Tortellini vegetarisch)", "Veganes M\u00fchlenmett (R\u00fcgenwalder M\u00fchle)", "Veganer Schinkenspicker Salat Kr\u00e4uter (R\u00fcgenwalder M\u00fchle)", "Veganer Eiersalat", "Hefe", "Schupfnudeln", "R\u00e4uchertofu"], "Gew\u00fcrze": ["Chilliflocken", "Kardamom", "Salz", "getrocknete Tomaten (ohne \u00d6l)", "Italienische Kr\u00e4utermischung", "getrockenete Steinpilze", "Rose Harissa"], "S\u00fc\u00dfwaren": ["gute Schokolade (hoher Schokoladenanteil)", "gute vollmilchschokolade", "Pistazien gesalzen und ger\u00f6stet", "Lakritze", "Spekulatius", "Pringles (orange)", "Schokolade (Marzipan, N\u00fcsse, Vollmilch etc.)", "M&Ms", "Schokobons", "Haribo und \u00e4hnliches (-> Traditionell ist das Kachel's Job)", "Katjes vegtarisch/vegan", "Riffle Chips salzig", "N\u00fcsse"], "Haushalt": ["Kosmetikt\u00fccherbox", "Taschent\u00fccher", "Handseife", "Klopapier (10er-Pack)", "K\u00fcchenpapier", "Schwammt\u00fccher", "Sp\u00fclschw\u00e4mme", "Stahlschw\u00e4mme", "K\u00fcchenrolle (4er-Pack)", "Geschirrsp\u00fcltabs", "Handsp\u00fclmittel", "M\u00fcllbeutel riesengro\u00df", "Frischhaltefolie", "Alufolie", "Allesreiniger in der Spr\u00fchflasche"]}, "equivalence": {"Zwiebeln": "Zwiebel", "Knollensellerie": "Knollen-Sellerie", "M\u00f6hre": "M\u00f6hren"}}

einkauf.py

+139
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
#!/sbin/env python
2+
import csv
3+
import sys
4+
from collections import defaultdict
5+
import locale
6+
import itertools
7+
8+
from rapidfuzz import fuzz
9+
import inquirer
10+
import json
11+
12+
locale.setlocale(locale.LC_ALL, 'de_DE.utf-8')
13+
14+
VALID_UNITS = {"g", "kg", "ml", "L", "Stück", "VE", "Pott"}
15+
16+
shopping_items = defaultdict(lambda: {unit: 0 for unit in VALID_UNITS})
17+
try:
18+
with open("einkauf.db", "r") as f:
19+
data = json.load(f)
20+
except FileNotFoundError:
21+
data = {}
22+
23+
items_per_section = defaultdict(list)
24+
if "items_per_section" in data:
25+
for sec in data["items_per_section"]:
26+
items_per_section[sec] = data["items_per_section"][sec]
27+
item_equivalence = {}
28+
if "equivalence" in data:
29+
item_equivalence = data["equivalence"]
30+
31+
32+
def write_einkaufdb(items_per_section, equivalence):
33+
db_data = {
34+
"items_per_section": items_per_section,
35+
"equivalence": equivalence
36+
}
37+
with open("einkauf.db", "w") as f:
38+
json.dump(db_data, f)
39+
40+
def pretty_print_amounts(amounts):
41+
to_be_printed = list(VALID_UNITS)
42+
output = ""
43+
for unit in to_be_printed:
44+
if amounts[unit] > 0:
45+
if output:
46+
output += " + "
47+
output += f"{amounts[unit]}{unit}"
48+
return output
49+
50+
def load_sections(sec_path):
51+
sections = {}
52+
with open(sec_path, "r") as f:
53+
reader = csv.reader(f)
54+
for line in reader:
55+
if line[1] in sections:
56+
print(f"ERROR: Key {line[1]} used for {line[0]} was already assigned to {sections[line[1]]}")
57+
sys.exit(-1)
58+
sections[line[1]] = line[0]
59+
60+
return sections
61+
62+
def check_match(products, new_product):
63+
potential_matches = []
64+
for product in products:
65+
if fuzz.ratio(product, new_product) > 85:
66+
potential_matches.append(product)
67+
68+
return potential_matches
69+
70+
def check_equivalence(new_product, products, equivalences):
71+
if new_product in equivalences:
72+
return equivalences[new_product]
73+
potential_matches = check_match(products, new_product)
74+
if potential_matches:
75+
potential_matches.append("Do not merge.")
76+
questions = [
77+
inquirer.List(
78+
"match",
79+
message=f"Do you want to merge *{new_product}* with one of the following?",
80+
choices=potential_matches
81+
)
82+
]
83+
answers = inquirer.prompt(questions)
84+
if answers["match"] != "Do not merge.":
85+
equivalences[new_product] = answers["match"]
86+
return answers["match"]
87+
else:
88+
return new_product
89+
else:
90+
return new_product
91+
92+
def select_section(product, sections):
93+
while True:
94+
res = input(f"In which category is {product}? [? for list]").strip()
95+
if res == "?":
96+
section_info = [f"{name} [{short}]" for short, name in sections.items()]
97+
print("\n".join(section_info))
98+
if res in sections:
99+
return sections[res]
100+
101+
with open(sys.argv[1], "r") as f:
102+
reader = csv.reader(f)
103+
for line in reader:
104+
ignore = line[0]
105+
item = line[1].strip()
106+
unit = line[3]
107+
if ignore == "1" or not item or unit not in VALID_UNITS:
108+
continue
109+
normalized_amount = line[2].replace(",", ".")
110+
try:
111+
amount = float(normalized_amount)
112+
except ValueError:
113+
print(f"WARN: {item} with amount {line[2]} is invalid and ignored")
114+
continue
115+
if unit == "kg":
116+
amount *= 1000
117+
unit = "g"
118+
if unit == "L":
119+
amount *= 1000
120+
unit = "ml"
121+
122+
if item not in shopping_items:
123+
item = check_equivalence(item, shopping_items.keys(), item_equivalence)
124+
125+
shopping_items[item][unit] += amount
126+
127+
sections = load_sections("sections.csv")
128+
for item in shopping_items.keys():
129+
if item not in itertools.chain.from_iterable(items_per_section.values()):
130+
sec = select_section(item, sections)
131+
items_per_section[sec].append(item)
132+
133+
write_einkaufdb(items_per_section, item_equivalence)
134+
135+
for section, section_items in items_per_section.items():
136+
section_items = {k: v for k,v in shopping_items.items() if k in section_items}
137+
print(f"\n* {section}:")
138+
for item, amounts in sorted(section_items.items(), key=lambda x: locale.strxfrm(x[0])):
139+
print(f"{item}: {pretty_print_amounts(amounts)}")

sections.csv

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
Öl/Fett,ö
2+
Obst/Gemüse,g
3+
Backzubehör & Co,b
4+
Feculants,fe
5+
Gewürze,gw
6+
Dosen,d
7+
Einmach (in Gläsern),eg
8+
Internationales & Saucen (hinter Gemüse),i
9+
Frühstücksareal,f
10+
Alkohol,a
11+
Fleischtheke,fl
12+
TK,tk
13+
Kühltheke Salate,ks
14+
Kühltheke Käse,kk
15+
Milch & Co,m
16+
Süßwaren,sw
17+
Haushalt,h
18+
Getränke,gt
19+
Standort Unklar,uk

0 commit comments

Comments
 (0)