santu24 commited on
Commit
83fb89d
·
1 Parent(s): 3092742

Commit Files

Browse files
Files changed (4) hide show
  1. app.py +353 -0
  2. create_db.py +42 -0
  3. database.py +138 -0
  4. requirements.txt +6 -0
app.py ADDED
@@ -0,0 +1,353 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import pandas as pd
3
+ import os
4
+ import re
5
+ import json
6
+ from PIL import Image
7
+ from datetime import datetime
8
+ from google.cloud import vision
9
+ from google.oauth2 import service_account
10
+
11
+ st.set_page_config(layout="wide")
12
+
13
+ import database as db
14
+
15
+ GCP_SERVICE_ACCOUNT_JSON = os.getenv("GCP_SERVICE_ACCOUNT_JSON")
16
+
17
+ def get_vision_client():
18
+ if not GCP_SERVICE_ACCOUNT_JSON:
19
+ raise Exception("GCP service account JSON is missing. Check your environment variables.")
20
+ credentials_dict = json.loads(GCP_SERVICE_ACCOUNT_JSON)
21
+ credentials = service_account.Credentials.from_service_account_info(credentials_dict)
22
+ return vision.ImageAnnotatorClient(credentials=credentials)
23
+
24
+ client = get_vision_client()
25
+
26
+ def parse_description(description):
27
+ """
28
+ A naive regex approach that grabs lines containing $XX.XX
29
+ and pairs them with the line above as the item name.
30
+ """
31
+ lines = description.split("\n")
32
+ items = []
33
+ price_pattern = r"\$(\d+\.\d{2})"
34
+
35
+ for i in range(1, len(lines)):
36
+ line = lines[i].strip()
37
+ prev_line = lines[i - 1].strip()
38
+ match = re.search(price_pattern, line)
39
+ if match:
40
+ item_name = prev_line
41
+ price = float(match.group(1))
42
+ items.append({"name": item_name, "price": price})
43
+
44
+ return items
45
+
46
+ def extract_invoice_data(image_data: bytes):
47
+ """
48
+ Calls Google Vision to detect text from the uploaded image bytes,
49
+ then uses parse_description to produce a list of item dicts.
50
+ """
51
+ image = vision.Image(content=image_data)
52
+ response = client.text_detection(image=image)
53
+ if response.error.message:
54
+ raise Exception(f"Vision API Error: {response.error.message}")
55
+
56
+ response_dict = vision.AnnotateImageResponse.to_dict(response)
57
+ annotations = response_dict.get("text_annotations", [])
58
+ if not annotations:
59
+ return []
60
+
61
+ description = annotations[0]["description"]
62
+ return parse_description(description)
63
+
64
+ def load_existing_data():
65
+ if 'logged_in' not in st.session_state:
66
+ st.session_state['logged_in'] = False
67
+ if 'email' not in st.session_state:
68
+ st.session_state['email'] = None
69
+ if 'section' not in st.session_state:
70
+ st.session_state['section'] = None
71
+ if 'participants' not in st.session_state:
72
+ st.session_state['participants'] = []
73
+ if 'items' not in st.session_state:
74
+ st.session_state['items'] = []
75
+ if 'selected_items' not in st.session_state:
76
+ st.session_state['selected_items'] = []
77
+ if 'submitted_items' not in st.session_state:
78
+ st.session_state['submitted_items'] = []
79
+ if 'submitted' not in st.session_state:
80
+ st.session_state['submitted'] = False
81
+ if 'sections_loaded' not in st.session_state:
82
+ st.session_state['sections_loaded'] = False
83
+ if 'existing_sections' not in st.session_state:
84
+ st.session_state['existing_sections'] = []
85
+
86
+ def load_section_from_db(section_name):
87
+ owner_email = st.session_state['email']
88
+ section_doc = db.get_section(owner_email, section_name)
89
+ if section_doc:
90
+ st.session_state['section'] = section_name
91
+ st.session_state['participants'] = section_doc['participants']
92
+ st.session_state['items'] = []
93
+ st.session_state['selected_items'] = []
94
+ st.session_state['submitted_items'] = []
95
+ st.session_state['submitted'] = False
96
+
97
+ def save_section_to_db(section_name, participants):
98
+ owner_email = st.session_state['email']
99
+ db.update_section(owner_email, section_name, participants)
100
+
101
+ def update_submitted_items():
102
+ owner_email = st.session_state['email']
103
+ section_name = st.session_state['section']
104
+ st.session_state['submitted_items'] = db.get_submitted_items(owner_email, section_name)
105
+
106
+ def get_most_bought_item():
107
+ owner_email = st.session_state['email']
108
+ section_name = st.session_state['section']
109
+ return db.get_most_bought_item(owner_email, section_name)
110
+
111
+ def main():
112
+ load_existing_data()
113
+ st.title("Billing Tracker")
114
+
115
+ if not st.session_state['logged_in']:
116
+ tab1, tab2 = st.tabs(["Login", "Sign Up"])
117
+
118
+ with tab1:
119
+ st.header("Login")
120
+ email = st.text_input("Email")
121
+ password = st.text_input("Password", type="password")
122
+ if st.button("Login"):
123
+ user_doc = db.get_user_by_email_and_password(email, password)
124
+ if user_doc:
125
+ st.session_state['logged_in'] = True
126
+ st.session_state['email'] = email
127
+ st.rerun()
128
+ else:
129
+ st.error("Invalid email or password")
130
+
131
+ with tab2:
132
+ st.header("Sign Up")
133
+ signup_email = st.text_input("New Email")
134
+ signup_password = st.text_input("New Password", type="password")
135
+ confirm_password = st.text_input("Confirm Password", type="password")
136
+ if st.button("Sign Up"):
137
+ if signup_password == confirm_password:
138
+ try:
139
+ db.create_user(signup_email, signup_password)
140
+ st.success("Sign up successful! Please log in.")
141
+ except ValueError:
142
+ st.error("Email already exists")
143
+ else:
144
+ st.error("Passwords do not match")
145
+
146
+ else:
147
+ with st.sidebar:
148
+ st.write(f"Welcome, {st.session_state['email']}")
149
+ if st.button("Sign Out"):
150
+ st.session_state.clear()
151
+ st.rerun()
152
+
153
+ st.header("Billing Sections")
154
+
155
+ if not st.session_state['sections_loaded']:
156
+ owner_email = st.session_state['email']
157
+ existing_sections = db.get_all_sections(owner_email)
158
+ st.session_state['sections_loaded'] = True
159
+ st.session_state['existing_sections'] = existing_sections
160
+
161
+ section_name = st.radio("Select a section",
162
+ ["Create New"] + st.session_state['existing_sections'])
163
+
164
+ if section_name == "Create New":
165
+ new_section_name = st.text_input("New Section Name")
166
+ participants_str = st.text_area("Participants (comma-separated)")
167
+ if st.button("Create Section"):
168
+ try:
169
+ participants_list = [p.strip() for p in participants_str.split(",") if p.strip()]
170
+ db.create_section(st.session_state['email'], new_section_name, participants_list)
171
+ st.session_state['section'] = new_section_name
172
+ st.session_state['participants'] = participants_list
173
+ st.session_state['items'] = []
174
+ st.session_state['selected_items'] = []
175
+ st.session_state['submitted_items'] = []
176
+ st.session_state['submitted'] = False
177
+ st.session_state['existing_sections'].append(new_section_name)
178
+ st.rerun()
179
+ except ValueError as e:
180
+ st.error(str(e))
181
+ else:
182
+ if st.button("Load Section"):
183
+ load_section_from_db(section_name)
184
+ update_submitted_items()
185
+ st.rerun()
186
+
187
+ if st.button("Delete Section"):
188
+ db.delete_section(st.session_state['email'], section_name)
189
+ st.session_state['existing_sections'].remove(section_name)
190
+ if st.session_state['section'] == section_name:
191
+ st.session_state['section'] = None
192
+ st.session_state['participants'] = []
193
+ st.session_state['items'] = []
194
+ st.session_state['selected_items'] = []
195
+ st.session_state['submitted_items'] = []
196
+ st.session_state['submitted'] = False
197
+ st.rerun()
198
+
199
+ if st.session_state['section'] and st.session_state['participants']:
200
+ col1, col2 = st.columns([3, 1])
201
+
202
+ with col1:
203
+ st.subheader(f"Section: {st.session_state['section']}")
204
+ st.write("Participants: " + ", ".join(st.session_state['participants']))
205
+
206
+ st.subheader("Bill Image (OCR)")
207
+ uploaded_file = st.file_uploader("Upload a bill image (jpg, jpeg, png)",
208
+ type=["jpg", "jpeg", "png"])
209
+
210
+ if uploaded_file is not None:
211
+ if st.button("Extract Items via OCR"):
212
+ try:
213
+ image_data = uploaded_file.read()
214
+ ocr_items = extract_invoice_data(image_data)
215
+ if ocr_items:
216
+ st.session_state['items'].extend(ocr_items)
217
+ st.success("OCR extraction successful. Items appended.")
218
+ else:
219
+ st.warning("No items found in the extracted text.")
220
+ except Exception as e:
221
+ st.error(f"OCR failed: {e}")
222
+
223
+ st.subheader("Manual Item Entry")
224
+ manual_item_name = st.text_input("Item Name", key="manual_item_name")
225
+ manual_item_price = st.number_input("Item Price", min_value=0.0, step=0.01, key="manual_item_price")
226
+
227
+ if st.button("Add Item Manually"):
228
+ if manual_item_name.strip():
229
+ st.session_state['items'].append({
230
+ "name": manual_item_name.strip(),
231
+ "price": manual_item_price
232
+ })
233
+ st.success(f"Added '{manual_item_name}' at ${manual_item_price:.2f}")
234
+ else:
235
+ st.warning("Please provide a non-empty item name.")
236
+
237
+ st.subheader("Available Items")
238
+ if st.session_state['items']:
239
+ st.write([f"{itm['name']} (${itm['price']})" for itm in st.session_state['items']])
240
+ else:
241
+ st.write("No items found yet.")
242
+
243
+ if st.session_state['items']:
244
+ not_submitted = [
245
+ itm["name"] for itm in st.session_state['items']
246
+ if itm["name"] not in st.session_state['submitted_items']
247
+ ]
248
+ selected_items = st.multiselect("Select Items to Tag", not_submitted, key="item_select")
249
+
250
+ if st.button("Tag Selected Items"):
251
+ st.session_state['selected_items'] = selected_items
252
+ st.session_state['submitted'] = True
253
+
254
+ if st.session_state['submitted'] and st.session_state['selected_items']:
255
+ chosen_participant = st.selectbox("Participant to Assign Items",
256
+ st.session_state['participants'],
257
+ key="participant_select")
258
+
259
+ if st.button("Confirm Assignment"):
260
+ for it in st.session_state['items']:
261
+ if it['name'] in st.session_state['selected_items']:
262
+ db.create_bill(
263
+ owner_email=st.session_state['email'],
264
+ section_name=st.session_state['section'],
265
+ participant=chosen_participant,
266
+ item=it['name'],
267
+ price=it['price']
268
+ )
269
+ st.session_state['submitted_items'].append(it['name'])
270
+ st.success("Items assigned successfully!")
271
+ st.session_state['submitted'] = False
272
+ st.session_state['selected_items'] = []
273
+
274
+ st.subheader("Billing History")
275
+ history = db.get_billing_history(st.session_state['email'], st.session_state['section'])
276
+ if history:
277
+ data_list = []
278
+ for row in history:
279
+ data_list.append({
280
+ "Participant": row["_id"],
281
+ "Total Price": row["total_price"],
282
+ "Last Updated": row["last_updated"]
283
+ })
284
+ df = pd.DataFrame(data_list)
285
+ st.dataframe(df)
286
+ else:
287
+ st.write("No bills recorded yet.")
288
+
289
+ with col2:
290
+ st.subheader("Most Bought Item")
291
+ top_item = get_most_bought_item()
292
+ if top_item:
293
+ item_name, count_val, price_val = top_item
294
+ st.write(f"Item: {item_name}, Count: {count_val}, Price: ${price_val}")
295
+ else:
296
+ st.write("No items purchased yet.")
297
+
298
+ st.subheader("Manage Participants")
299
+ new_participant = st.text_input("Add Participant")
300
+ if st.button("Add Participant"):
301
+ if new_participant.strip():
302
+ st.session_state['participants'].append(new_participant.strip())
303
+ save_section_to_db(st.session_state['section'], st.session_state['participants'])
304
+ st.rerun()
305
+
306
+ remove_part = st.selectbox("Remove Participant", st.session_state['participants'], key="remove_participant")
307
+ if st.button("Remove Participant"):
308
+ if remove_part:
309
+ st.session_state['participants'].remove(remove_part)
310
+ save_section_to_db(st.session_state['section'], st.session_state['participants'])
311
+ db.remove_items(
312
+ st.session_state['email'],
313
+ st.session_state['section'],
314
+ remove_part,
315
+ items_to_remove=None
316
+ )
317
+ st.rerun()
318
+
319
+ st.subheader("Remove Wrongly Tagged Items")
320
+ part_to_remove_from = st.selectbox("Select Participant",
321
+ st.session_state['participants'],
322
+ key="remove_participant_select")
323
+ if part_to_remove_from:
324
+ pipeline = [
325
+ {"$match": {
326
+ "owner_email": st.session_state['email'],
327
+ "section_name": st.session_state['section'],
328
+ "participant": part_to_remove_from
329
+ }}
330
+ ]
331
+ results = list(db.bills_coll.aggregate(pipeline))
332
+ if results:
333
+ tagged_items = [doc["item"] for doc in results]
334
+ remove_items_select = st.multiselect("Select Items to Remove",
335
+ tagged_items,
336
+ key="remove_item_select")
337
+ if st.button("Remove Items"):
338
+ db.remove_items(
339
+ st.session_state['email'],
340
+ st.session_state['section'],
341
+ part_to_remove_from,
342
+ remove_items_select
343
+ )
344
+ for itm in remove_items_select:
345
+ if itm in st.session_state['submitted_items']:
346
+ st.session_state['submitted_items'].remove(itm)
347
+ st.success("Items removed successfully!")
348
+ st.rerun()
349
+ else:
350
+ st.write("No items found for this participant.")
351
+
352
+ if __name__ == "__main__":
353
+ main()
create_db.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pymongo
2
+ import urllib.parse
3
+ import os
4
+
5
+ def create_db():
6
+ """
7
+ Connect to MongoDB Atlas and create collections for the Billing App.
8
+ """
9
+
10
+ raw_username = os.getenv("DB_USERNAME")
11
+ raw_password = os.getenv("DB_PASSWORD")
12
+
13
+ if not raw_username or not raw_password:
14
+ raise Exception("Database credentials are missing. Check your environment variables.")
15
+ cluster = "cluster0"
16
+
17
+ username = urllib.parse.quote_plus(raw_username)
18
+ password = urllib.parse.quote_plus(raw_password)
19
+
20
+ uri = f"mongodb+srv://{username}:{password}@cluster0.yxjok.mongodb.net/?retryWrites=true&w=majority&appName=Cluster0"
21
+
22
+ client = pymongo.MongoClient(uri)
23
+
24
+ db = client["billing_app"]
25
+
26
+ users_coll = db["users"]
27
+ sections_coll = db["sections"]
28
+ bills_coll = db["bills"]
29
+
30
+ users_coll.create_index("email", unique=True)
31
+ sections_coll.create_index(
32
+ [("owner_email", 1), ("section_name", 1)],
33
+ unique=True
34
+ )
35
+ bills_coll.create_index(
36
+ [("owner_email", 1), ("section_name", 1), ("participant", 1)]
37
+ )
38
+
39
+ print("Database and collections created (or already exist). Indexes applied.")
40
+
41
+ if __name__ == "__main__":
42
+ create_db()
database.py ADDED
@@ -0,0 +1,138 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from datetime import datetime
3
+ import pymongo
4
+ from urllib.parse import quote_plus
5
+ from bson.objectid import ObjectId
6
+
7
+ raw_username = os.getenv("DB_USERNAME")
8
+ raw_password = os.getenv("DB_PASSWORD")
9
+
10
+ if not raw_username or not raw_password:
11
+ raise Exception("Database credentials are missing. Check your environment variables.")
12
+
13
+ username = quote_plus(raw_username)
14
+ password = quote_plus(raw_password)
15
+
16
+ cluster = "cluster0.yxjok.mongodb.net"
17
+
18
+ DB_URI = f"mongodb+srv://{username}:{password}@{cluster}/?retryWrites=true&w=majority&appName=Cluster0"
19
+
20
+ client = pymongo.MongoClient(DB_URI)
21
+ db = client["billing_app"]
22
+
23
+ users_coll = db["users"]
24
+ sections_coll = db["sections"]
25
+ bills_coll = db["bills"]
26
+
27
+ sections_coll.create_index(
28
+ [("owner_email", 1), ("section_name", 1)],
29
+ unique=True
30
+ )
31
+
32
+ def create_user(email, password):
33
+ if users_coll.find_one({"email": email}):
34
+ raise ValueError("Email already exists")
35
+ users_coll.insert_one({"email": email, "password": password})
36
+
37
+ def get_user_by_email_and_password(email, password):
38
+ return users_coll.find_one({"email": email, "password": password})
39
+
40
+ def create_section(owner_email, section_name, participants_list):
41
+ """
42
+ Creates a new section for the given user.
43
+ Fails if that user already has a section with the same name.
44
+ """
45
+ existing = sections_coll.find_one({"owner_email": owner_email, "section_name": section_name})
46
+ if existing:
47
+ raise ValueError("Section with this name already exists for your account.")
48
+ sections_coll.insert_one({
49
+ "owner_email": owner_email,
50
+ "section_name": section_name,
51
+ "participants": participants_list
52
+ })
53
+
54
+ def update_section(owner_email, section_name, participants_list):
55
+ sections_coll.update_one(
56
+ {"owner_email": owner_email, "section_name": section_name},
57
+ {"$set": {"participants": participants_list}},
58
+ upsert=True
59
+ )
60
+
61
+ def get_section(owner_email, section_name):
62
+ return sections_coll.find_one({"owner_email": owner_email, "section_name": section_name})
63
+
64
+ def delete_section(owner_email, section_name):
65
+ sections_coll.delete_one({"owner_email": owner_email, "section_name": section_name})
66
+ bills_coll.delete_many({"owner_email": owner_email, "section_name": section_name})
67
+
68
+ def get_all_sections(owner_email):
69
+ """
70
+ Returns all sections for that specific user/email.
71
+ """
72
+ sections = sections_coll.find({"owner_email": owner_email})
73
+ return [sec["section_name"] for sec in sections]
74
+
75
+ def create_bill(owner_email, section_name, participant, item, price):
76
+ bills_coll.insert_one({
77
+ "owner_email": owner_email,
78
+ "section_name": section_name,
79
+ "participant": participant,
80
+ "item": item,
81
+ "price": float(price),
82
+ "timestamp": datetime.now().date().isoformat()
83
+ })
84
+
85
+ def get_submitted_items(owner_email, section_name):
86
+ pipeline = [
87
+ {"$match": {"owner_email": owner_email, "section_name": section_name}},
88
+ {"$group": {"_id": "$item"}}
89
+ ]
90
+ results = list(bills_coll.aggregate(pipeline))
91
+ return [r["_id"] for r in results]
92
+
93
+ def get_billing_history(owner_email, section_name):
94
+ pipeline = [
95
+ {"$match": {"owner_email": owner_email, "section_name": section_name}},
96
+ {
97
+ "$group": {
98
+ "_id": "$participant",
99
+ "total_price": {"$sum": "$price"},
100
+ "last_updated": {"$max": "$timestamp"}
101
+ }
102
+ }
103
+ ]
104
+ return list(bills_coll.aggregate(pipeline))
105
+
106
+ def remove_items(owner_email, section_name, participant, items_to_remove):
107
+ if items_to_remove is None:
108
+ bills_coll.delete_many({
109
+ "owner_email": owner_email,
110
+ "section_name": section_name,
111
+ "participant": participant
112
+ })
113
+ else:
114
+ for item in items_to_remove:
115
+ bills_coll.delete_many({
116
+ "owner_email": owner_email,
117
+ "section_name": section_name,
118
+ "participant": participant,
119
+ "item": item
120
+ })
121
+
122
+ def get_most_bought_item(owner_email, section_name):
123
+ pipeline = [
124
+ {"$match": {"owner_email": owner_email, "section_name": section_name}},
125
+ {
126
+ "$group": {
127
+ "_id": "$item",
128
+ "count": {"$sum": 1},
129
+ "max_price": {"$max": "$price"}
130
+ }
131
+ },
132
+ {"$sort": {"count": -1, "max_price": -1}},
133
+ {"$limit": 1}
134
+ ]
135
+ result = list(bills_coll.aggregate(pipeline))
136
+ if result:
137
+ return (result[0]["_id"], result[0]["count"], result[0]["max_price"])
138
+ return None
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ streamlit
2
+ pandas
3
+ pymongo[srv]
4
+ pymongo
5
+ Pillow
6
+ google-cloud-vision