rairo commited on
Commit
8fab0f2
·
verified ·
1 Parent(s): d8ff170

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +108 -69
main.py CHANGED
@@ -183,7 +183,8 @@ def get_user_profile():
183
  def get_user_dashboard():
184
  """
185
  Retrieves and aggregates data for the user's dashboard with correct profit calculation.
186
- **FIXED**: Multiplies price and cost by quantity for accurate financial totals.
 
187
  """
188
  uid = verify_token(request.headers.get('Authorization'))
189
  if not uid: return jsonify({'error': 'Invalid or expired token'}), 401
@@ -202,42 +203,78 @@ def get_user_dashboard():
202
  bot_data_id = phone_number.lstrip('+')
203
  bot_user_ref = db.collection('users').document(bot_data_id)
204
 
205
- # --- THE FIX IS HERE ---
 
 
 
 
 
 
 
 
 
 
206
  sales_docs = bot_user_ref.collection('sales').stream()
207
- total_sales_revenue, total_cogs, sales_count = 0, 0, 0
208
  for doc in sales_docs:
209
  details = doc.to_dict().get('details', {})
210
- # Default quantity to 1 if not present
 
 
 
 
 
 
 
211
  quantity = int(details.get('quantity', 1))
212
  price = float(details.get('price', 0))
213
  cost = float(details.get('cost', 0))
214
 
215
- total_sales_revenue += price * quantity
216
- total_cogs += cost * quantity
217
  sales_count += 1
218
 
 
219
  expenses_docs = bot_user_ref.collection('expenses').stream()
220
- total_expenses = sum(float(doc.to_dict().get('details', {}).get('amount', 0)) for doc in expenses_docs)
221
-
222
- gross_profit = total_sales_revenue - total_cogs
223
- net_profit = gross_profit - total_expenses
224
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
225
  dashboard_data = {
226
- 'totalSalesRevenue': round(total_sales_revenue, 2),
227
- 'totalCostOfGoodsSold': round(total_cogs, 2),
228
- 'grossProfit': round(gross_profit, 2),
229
- 'totalExpenses': round(total_expenses, 2),
230
- 'netProfit': round(net_profit, 2),
231
  'salesCount': sales_count,
232
  }
233
- # --- END OF FIX ---
234
  return jsonify(dashboard_data), 200
235
 
236
  except Exception as e:
237
  logging.error(f"Error fetching dashboard data for user {uid} (phone: {phone_number}): {e}")
238
  return jsonify({'error': 'An error occurred while fetching your dashboard data.'}), 500
239
 
240
-
241
  # Replace your existing PUT /api/user/profile with this one.
242
  @app.route('/api/user/profile', methods=['PUT'])
243
  def update_user_profile():
@@ -723,20 +760,25 @@ def admin_remove_member_from_org(org_id, member_uid):
723
  @app.route('/api/admin/dashboard/stats', methods=['GET'])
724
  def get_admin_dashboard_stats():
725
  """
726
- Retrieves global statistics for the admin dashboard, including leaderboards.
727
- **FIXED**: Correctly groups expenses by the 'description' field for the leaderboard.
728
  """
729
  try:
730
  verify_admin_and_get_uid(request.headers.get('Authorization'))
731
 
732
- # --- Initialization ---
733
  all_users_docs = list(db.collection('users').stream())
734
  all_orgs_docs = list(db.collection('organizations').stream())
735
 
736
- user_sales_data = {}
737
- global_item_revenue = {}
738
- global_expense_totals = {} # This will be populated correctly now
 
739
  phone_to_user_map = {}
 
 
 
 
 
740
 
741
  # --- First Pass: Get user info and list of approved phones ---
742
  pending_approvals, approved_users, admin_count = 0, 0, 0
@@ -752,7 +794,8 @@ def get_admin_dashboard_stats():
752
  approved_phone_numbers.append(phone)
753
  phone_to_user_map[phone] = {
754
  'displayName': user_data.get('displayName', 'N/A'),
755
- 'uid': user_data.get('uid')
 
756
  }
757
  if user_data.get('isAdmin', False):
758
  admin_count += 1
@@ -761,55 +804,55 @@ def get_admin_dashboard_stats():
761
  org_stats = {'total': len(all_orgs_docs)}
762
 
763
  # --- Second Pass: Aggregate financial data ---
764
- total_sales_revenue, total_cogs, total_expenses, sales_count = 0, 0, 0, 0
765
 
766
  for phone in approved_phone_numbers:
767
  try:
768
  bot_data_id = phone.lstrip('+')
769
  bot_user_ref = db.collection('users').document(bot_data_id)
770
 
771
- user_sales_data[phone] = {'total_revenue': 0, 'item_sales': {}}
 
 
772
 
773
  # Process Sales
774
- sales_docs = bot_user_ref.collection('sales').stream()
775
- for sale_doc in sales_docs:
776
  details = sale_doc.to_dict().get('details', {})
777
- quantity = int(details.get('quantity', 1))
778
- price = float(details.get('price', 0))
779
- cost = float(details.get('cost', 0))
780
- item_name = details.get('item', 'Unknown Item')
781
- sale_revenue = price * quantity
782
 
783
- total_sales_revenue += sale_revenue
784
- total_cogs += cost * quantity
 
 
 
785
  sales_count += 1
786
 
787
- user_sales_data[phone]['total_revenue'] += sale_revenue
 
788
  user_sales_data[phone]['item_sales'][item_name] = user_sales_data[phone]['item_sales'].get(item_name, 0) + sale_revenue
789
  global_item_revenue[item_name] = global_item_revenue.get(item_name, 0) + sale_revenue
790
-
791
  # Process Expenses
792
- expenses_docs = bot_user_ref.collection('expenses').stream()
793
- for expense_doc in expenses_docs:
794
  details = expense_doc.to_dict().get('details', {})
795
- amount = float(details.get('amount', 0))
 
 
796
 
797
- # --- THE FIX IS HERE ---
798
- # Use the 'description' field for grouping, as seen in Firestore.
799
  category = details.get('description', 'Uncategorized')
800
- # --- END OF FIX ---
801
-
802
- total_expenses += amount
803
  global_expense_totals[category] = global_expense_totals.get(category, 0) + amount
804
-
805
  except Exception as e:
806
  logging.error(f"Admin stats: Could not process data for phone {phone}. Error: {e}")
807
  continue
808
 
809
- # --- Post-Processing: Calculate and Rank Leaderboards ---
810
-
811
- # Rank Top Users by Revenue
812
- sorted_users = sorted(user_sales_data.items(), key=lambda item: item[1]['total_revenue'], reverse=True)
813
  top_users_by_revenue = []
814
  for phone, data in sorted_users[:5]:
815
  user_info = phone_to_user_map.get(phone, {})
@@ -817,39 +860,35 @@ def get_admin_dashboard_stats():
817
  top_users_by_revenue.append({
818
  'displayName': user_info.get('displayName'),
819
  'uid': user_info.get('uid'),
820
- 'totalRevenue': round(data['total_revenue'], 2),
821
  'topSellingItem': top_item
822
  })
823
 
824
- # Rank Top Selling Items Globally
825
  sorted_items = sorted(global_item_revenue.items(), key=lambda item: item[1], reverse=True)
826
  top_selling_items = [{'item': name, 'totalRevenue': round(revenue, 2)} for name, revenue in sorted_items[:5]]
827
-
828
- # Rank Top Expenses Globally (This will now be correct)
829
  sorted_expenses = sorted(global_expense_totals.items(), key=lambda item: item[1], reverse=True)
830
  top_expenses = [{'category': name, 'totalAmount': round(amount, 2)} for name, amount in sorted_expenses[:5]]
831
 
832
- # --- Final Assembly ---
833
- gross_profit = total_sales_revenue - total_cogs
834
- net_profit = gross_profit - total_expenses
835
-
 
 
 
 
836
  system_stats = {
837
- 'totalSalesRevenue': round(total_sales_revenue, 2),
838
- 'totalCostOfGoodsSold': round(total_cogs, 2),
839
- 'totalExpenses': round(total_expenses, 2),
840
- 'totalNetProfit': round(net_profit, 2),
841
  'totalSalesCount': sales_count,
842
  }
843
 
844
  return jsonify({
845
- 'userStats': user_stats,
846
- 'organizationStats': org_stats,
847
  'systemStats': system_stats,
848
- 'leaderboards': {
849
- 'topUsersByRevenue': top_users_by_revenue,
850
- 'topSellingItems': top_selling_items,
851
- 'topExpenses': top_expenses
852
- }
853
  }), 200
854
 
855
  except PermissionError as e:
 
183
  def get_user_dashboard():
184
  """
185
  Retrieves and aggregates data for the user's dashboard with correct profit calculation.
186
+ **FIXED**: Now segregates all financial totals by currency and intelligently infers
187
+ the currency for transactions where it is missing.
188
  """
189
  uid = verify_token(request.headers.get('Authorization'))
190
  if not uid: return jsonify({'error': 'Invalid or expired token'}), 401
 
203
  bot_data_id = phone_number.lstrip('+')
204
  bot_user_ref = db.collection('users').document(bot_data_id)
205
 
206
+ # --- REVISED LOGIC FOR MULTI-CURRENCY ---
207
+ # Use dictionaries to store totals for each currency
208
+ sales_revenue_by_currency = {}
209
+ cogs_by_currency = {}
210
+ expenses_by_currency = {}
211
+ sales_count = 0
212
+
213
+ # Establish the user's default currency as the initial fallback
214
+ last_seen_currency = user_data.get('defaultCurrency', 'USD')
215
+
216
+ # Process Sales
217
  sales_docs = bot_user_ref.collection('sales').stream()
 
218
  for doc in sales_docs:
219
  details = doc.to_dict().get('details', {})
220
+
221
+ # Infer currency: use the transaction's currency, or fall back to the last seen one
222
+ currency = details.get('currency')
223
+ if currency:
224
+ last_seen_currency = currency
225
+ else:
226
+ currency = last_seen_currency
227
+
228
  quantity = int(details.get('quantity', 1))
229
  price = float(details.get('price', 0))
230
  cost = float(details.get('cost', 0))
231
 
232
+ sales_revenue_by_currency[currency] = sales_revenue_by_currency.get(currency, 0) + (price * quantity)
233
+ cogs_by_currency[currency] = cogs_by_currency.get(currency, 0) + (cost * quantity)
234
  sales_count += 1
235
 
236
+ # Process Expenses
237
  expenses_docs = bot_user_ref.collection('expenses').stream()
238
+ for doc in expenses_docs:
239
+ details = doc.to_dict().get('details', {})
 
 
240
 
241
+ # Infer currency using the same logic
242
+ currency = details.get('currency')
243
+ if currency:
244
+ last_seen_currency = currency
245
+ else:
246
+ currency = last_seen_currency
247
+
248
+ amount = float(details.get('amount', 0))
249
+ expenses_by_currency[currency] = expenses_by_currency.get(currency, 0) + amount
250
+
251
+ # Calculate final totals by currency
252
+ all_currencies = set(sales_revenue_by_currency.keys()) | set(cogs_by_currency.keys()) | set(expenses_by_currency.keys())
253
+ gross_profit_by_currency = {}
254
+ net_profit_by_currency = {}
255
+
256
+ for curr in all_currencies:
257
+ revenue = sales_revenue_by_currency.get(curr, 0)
258
+ cogs = cogs_by_currency.get(curr, 0)
259
+ expenses = expenses_by_currency.get(curr, 0)
260
+ gross_profit_by_currency[curr] = round(revenue - cogs, 2)
261
+ net_profit_by_currency[curr] = round(revenue - cogs - expenses, 2)
262
+
263
  dashboard_data = {
264
+ 'totalSalesRevenueByCurrency': sales_revenue_by_currency,
265
+ 'totalCostOfGoodsSoldByCurrency': cogs_by_currency,
266
+ 'grossProfitByCurrency': gross_profit_by_currency,
267
+ 'totalExpensesByCurrency': expenses_by_currency,
268
+ 'netProfitByCurrency': net_profit_by_currency,
269
  'salesCount': sales_count,
270
  }
271
+ # --- END OF REVISED LOGIC ---
272
  return jsonify(dashboard_data), 200
273
 
274
  except Exception as e:
275
  logging.error(f"Error fetching dashboard data for user {uid} (phone: {phone_number}): {e}")
276
  return jsonify({'error': 'An error occurred while fetching your dashboard data.'}), 500
277
 
 
278
  # Replace your existing PUT /api/user/profile with this one.
279
  @app.route('/api/user/profile', methods=['PUT'])
280
  def update_user_profile():
 
760
  @app.route('/api/admin/dashboard/stats', methods=['GET'])
761
  def get_admin_dashboard_stats():
762
  """
763
+ Retrieves global statistics for the admin dashboard, including multi-currency totals
764
+ and leaderboards.
765
  """
766
  try:
767
  verify_admin_and_get_uid(request.headers.get('Authorization'))
768
 
 
769
  all_users_docs = list(db.collection('users').stream())
770
  all_orgs_docs = list(db.collection('organizations').stream())
771
 
772
+ # --- Initialize data structures ---
773
+ user_sales_data = {}
774
+ global_item_revenue = {}
775
+ global_expense_totals = {}
776
  phone_to_user_map = {}
777
+
778
+ # Global financial accumulators are now dictionaries
779
+ global_sales_rev_by_curr = {}
780
+ global_cogs_by_curr = {}
781
+ global_expenses_by_curr = {}
782
 
783
  # --- First Pass: Get user info and list of approved phones ---
784
  pending_approvals, approved_users, admin_count = 0, 0, 0
 
794
  approved_phone_numbers.append(phone)
795
  phone_to_user_map[phone] = {
796
  'displayName': user_data.get('displayName', 'N/A'),
797
+ 'uid': user_data.get('uid'),
798
+ 'defaultCurrency': user_data.get('defaultCurrency', 'USD') # Store default currency
799
  }
800
  if user_data.get('isAdmin', False):
801
  admin_count += 1
 
804
  org_stats = {'total': len(all_orgs_docs)}
805
 
806
  # --- Second Pass: Aggregate financial data ---
807
+ sales_count = 0
808
 
809
  for phone in approved_phone_numbers:
810
  try:
811
  bot_data_id = phone.lstrip('+')
812
  bot_user_ref = db.collection('users').document(bot_data_id)
813
 
814
+ user_sales_data[phone] = {'total_revenue_by_currency': {}, 'item_sales': {}}
815
+ user_info = phone_to_user_map.get(phone, {})
816
+ last_seen_currency = user_info.get('defaultCurrency', 'USD')
817
 
818
  # Process Sales
819
+ for sale_doc in bot_user_ref.collection('sales').stream():
 
820
  details = sale_doc.to_dict().get('details', {})
821
+ currency = details.get('currency')
822
+ if currency: last_seen_currency = currency
823
+ else: currency = last_seen_currency
 
 
824
 
825
+ quantity, price = int(details.get('quantity', 1)), float(details.get('price', 0))
826
+ sale_revenue = price * quantity
827
+
828
+ global_sales_rev_by_curr[currency] = global_sales_rev_by_curr.get(currency, 0) + sale_revenue
829
+ global_cogs_by_curr[currency] = global_cogs_by_curr.get(currency, 0) + (float(details.get('cost', 0)) * quantity)
830
  sales_count += 1
831
 
832
+ item_name = details.get('item', 'Unknown Item')
833
+ user_sales_data[phone]['total_revenue_by_currency'][currency] = user_sales_data[phone]['total_revenue_by_currency'].get(currency, 0) + sale_revenue
834
  user_sales_data[phone]['item_sales'][item_name] = user_sales_data[phone]['item_sales'].get(item_name, 0) + sale_revenue
835
  global_item_revenue[item_name] = global_item_revenue.get(item_name, 0) + sale_revenue
836
+
837
  # Process Expenses
838
+ for expense_doc in bot_user_ref.collection('expenses').stream():
 
839
  details = expense_doc.to_dict().get('details', {})
840
+ currency = details.get('currency')
841
+ if currency: last_seen_currency = currency
842
+ else: currency = last_seen_currency
843
 
844
+ amount = float(details.get('amount', 0))
 
845
  category = details.get('description', 'Uncategorized')
846
+ global_expenses_by_curr[currency] = global_expenses_by_curr.get(currency, 0) + amount
 
 
847
  global_expense_totals[category] = global_expense_totals.get(category, 0) + amount
 
848
  except Exception as e:
849
  logging.error(f"Admin stats: Could not process data for phone {phone}. Error: {e}")
850
  continue
851
 
852
+ # --- Post-Processing: Leaderboards & Final Totals ---
853
+ # Note: Ranking users with multiple currencies is complex. This example ranks them by the revenue
854
+ # in their default currency. A more advanced version might convert to a single currency.
855
+ sorted_users = sorted(user_sales_data.items(), key=lambda item: item[1]['total_revenue_by_currency'].get(phone_to_user_map.get(item[0], {}).get('defaultCurrency', 'USD'), 0), reverse=True)
856
  top_users_by_revenue = []
857
  for phone, data in sorted_users[:5]:
858
  user_info = phone_to_user_map.get(phone, {})
 
860
  top_users_by_revenue.append({
861
  'displayName': user_info.get('displayName'),
862
  'uid': user_info.get('uid'),
863
+ 'revenueByCurrency': {k: round(v, 2) for k, v in data['total_revenue_by_currency'].items()},
864
  'topSellingItem': top_item
865
  })
866
 
 
867
  sorted_items = sorted(global_item_revenue.items(), key=lambda item: item[1], reverse=True)
868
  top_selling_items = [{'item': name, 'totalRevenue': round(revenue, 2)} for name, revenue in sorted_items[:5]]
 
 
869
  sorted_expenses = sorted(global_expense_totals.items(), key=lambda item: item[1], reverse=True)
870
  top_expenses = [{'category': name, 'totalAmount': round(amount, 2)} for name, amount in sorted_expenses[:5]]
871
 
872
+ all_currencies = set(global_sales_rev_by_curr.keys()) | set(global_cogs_by_curr.keys()) | set(global_expenses_by_curr.keys())
873
+ global_net_profit_by_curr = {}
874
+ for curr in all_currencies:
875
+ revenue = global_sales_rev_by_curr.get(curr, 0)
876
+ cogs = global_cogs_by_curr.get(curr, 0)
877
+ expenses = global_expenses_by_curr.get(curr, 0)
878
+ global_net_profit_by_curr[curr] = round(revenue - cogs - expenses, 2)
879
+
880
  system_stats = {
881
+ 'totalSalesRevenueByCurrency': {k: round(v, 2) for k, v in global_sales_rev_by_curr.items()},
882
+ 'totalCostOfGoodsSoldByCurrency': {k: round(v, 2) for k, v in global_cogs_by_curr.items()},
883
+ 'totalExpensesByCurrency': {k: round(v, 2) for k, v in global_expenses_by_curr.items()},
884
+ 'totalNetProfitByCurrency': global_net_profit_by_curr,
885
  'totalSalesCount': sales_count,
886
  }
887
 
888
  return jsonify({
889
+ 'userStats': user_stats, 'organizationStats': org_stats,
 
890
  'systemStats': system_stats,
891
+ 'leaderboards': {'topUsersByRevenue': top_users_by_revenue, 'topSellingItems': top_selling_items, 'topExpenses': top_expenses}
 
 
 
 
892
  }), 200
893
 
894
  except PermissionError as e: