yashdave182 commited on
Commit
f7a5c50
·
1 Parent(s): 839b88c

minor changes

Browse files
Files changed (1) hide show
  1. database.py +478 -265
database.py CHANGED
@@ -4,17 +4,19 @@ import os
4
  import logging
5
  from datetime import datetime, timedelta
6
  from typing import List, Dict, Any, Optional
7
- import random # Add this import
 
8
  # Set up logging
9
  logging.basicConfig(level=logging.INFO)
10
  logger = logging.getLogger(__name__)
11
 
 
12
  class DatabaseManager:
13
  def __init__(self, db_path="sales_management.db"):
14
  self.db_path = db_path
15
  self._is_logging = False # Prevent recursion
16
  self.init_database()
17
-
18
  def get_connection(self):
19
  """Get database connection with error handling"""
20
  try:
@@ -24,14 +26,14 @@ class DatabaseManager:
24
  except sqlite3.Error as e:
25
  logger.error(f"Database connection error: {e}")
26
  raise
27
-
28
  def init_database(self):
29
  """Initialize database with all tables and relationships"""
30
  conn = self.get_connection()
31
-
32
  try:
33
  # Customers table
34
- conn.execute('''
35
  CREATE TABLE IF NOT EXISTS customers (
36
  customer_id INTEGER PRIMARY KEY AUTOINCREMENT,
37
  customer_code TEXT UNIQUE,
@@ -44,10 +46,10 @@ class DatabaseManager:
44
  created_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
45
  updated_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP
46
  )
47
- ''')
48
-
49
  # Distributors table
50
- conn.execute('''
51
  CREATE TABLE IF NOT EXISTS distributors (
52
  distributor_id INTEGER PRIMARY KEY AUTOINCREMENT,
53
  name TEXT NOT NULL,
@@ -62,10 +64,10 @@ class DatabaseManager:
62
  created_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
63
  updated_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP
64
  )
65
- ''')
66
-
67
  # Products table
68
- conn.execute('''
69
  CREATE TABLE IF NOT EXISTS products (
70
  product_id INTEGER PRIMARY KEY AUTOINCREMENT,
71
  product_name TEXT UNIQUE NOT NULL,
@@ -76,10 +78,10 @@ class DatabaseManager:
76
  is_active INTEGER DEFAULT 1,
77
  created_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP
78
  )
79
- ''')
80
-
81
  # Sales table
82
- conn.execute('''
83
  CREATE TABLE IF NOT EXISTS sales (
84
  sale_id INTEGER PRIMARY KEY AUTOINCREMENT,
85
  invoice_no TEXT UNIQUE NOT NULL,
@@ -93,10 +95,10 @@ class DatabaseManager:
93
  updated_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
94
  FOREIGN KEY (customer_id) REFERENCES customers (customer_id) ON DELETE SET NULL
95
  )
96
- ''')
97
-
98
  # Sale items table
99
- conn.execute('''
100
  CREATE TABLE IF NOT EXISTS sale_items (
101
  item_id INTEGER PRIMARY KEY AUTOINCREMENT,
102
  sale_id INTEGER,
@@ -108,10 +110,10 @@ class DatabaseManager:
108
  FOREIGN KEY (sale_id) REFERENCES sales (sale_id) ON DELETE CASCADE,
109
  FOREIGN KEY (product_id) REFERENCES products (product_id) ON DELETE SET NULL
110
  )
111
- ''')
112
-
113
  # Payments table
114
- conn.execute('''
115
  CREATE TABLE IF NOT EXISTS payments (
116
  payment_id INTEGER PRIMARY KEY AUTOINCREMENT,
117
  sale_id INTEGER,
@@ -125,30 +127,32 @@ class DatabaseManager:
125
  created_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
126
  FOREIGN KEY (sale_id) REFERENCES sales (sale_id) ON DELETE CASCADE
127
  )
128
- ''')
129
-
130
  # Demos table
131
- conn.execute('''
132
  CREATE TABLE IF NOT EXISTS demos (
133
  demo_id INTEGER PRIMARY KEY AUTOINCREMENT,
134
  customer_id INTEGER,
135
  distributor_id INTEGER,
136
  demo_date DATE,
 
137
  product_id INTEGER,
138
  quantity_provided INTEGER,
139
  follow_up_date DATE,
140
  conversion_status TEXT DEFAULT 'Not Converted',
141
  notes TEXT,
 
142
  created_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
143
  updated_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
144
  FOREIGN KEY (customer_id) REFERENCES customers (customer_id) ON DELETE SET NULL,
145
  FOREIGN KEY (distributor_id) REFERENCES distributors (distributor_id) ON DELETE SET NULL,
146
  FOREIGN KEY (product_id) REFERENCES products (product_id) ON DELETE SET NULL
147
  )
148
- ''')
149
-
150
  # WhatsApp logs table
151
- conn.execute('''
152
  CREATE TABLE IF NOT EXISTS whatsapp_logs (
153
  log_id INTEGER PRIMARY KEY AUTOINCREMENT,
154
  customer_id INTEGER,
@@ -161,10 +165,10 @@ class DatabaseManager:
161
  FOREIGN KEY (customer_id) REFERENCES customers (customer_id) ON DELETE SET NULL,
162
  FOREIGN KEY (distributor_id) REFERENCES distributors (distributor_id) ON DELETE SET NULL
163
  )
164
- ''')
165
-
166
  # Follow-ups table
167
- conn.execute('''
168
  CREATE TABLE IF NOT EXISTS follow_ups (
169
  follow_up_id INTEGER PRIMARY KEY AUTOINCREMENT,
170
  customer_id INTEGER,
@@ -180,10 +184,10 @@ class DatabaseManager:
180
  FOREIGN KEY (distributor_id) REFERENCES distributors (distributor_id) ON DELETE SET NULL,
181
  FOREIGN KEY (demo_id) REFERENCES demos (demo_id) ON DELETE SET NULL
182
  )
183
- ''')
184
-
185
  # System logs table
186
- conn.execute('''
187
  CREATE TABLE IF NOT EXISTS system_logs (
188
  log_id INTEGER PRIMARY KEY AUTOINCREMENT,
189
  log_type TEXT,
@@ -194,10 +198,10 @@ class DatabaseManager:
194
  created_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
195
  user_info TEXT
196
  )
197
- ''')
198
-
199
  # Rollback logs table
200
- conn.execute('''
201
  CREATE TABLE IF NOT EXISTS rollback_logs (
202
  rollback_id INTEGER PRIMARY KEY AUTOINCREMENT,
203
  table_name TEXT,
@@ -208,10 +212,10 @@ class DatabaseManager:
208
  rollback_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
209
  rolled_back_by TEXT
210
  )
211
- ''')
212
-
213
  # Offers table
214
- conn.execute('''
215
  CREATE TABLE IF NOT EXISTS offers (
216
  offer_id INTEGER PRIMARY KEY AUTOINCREMENT,
217
  offer_name TEXT NOT NULL,
@@ -225,10 +229,10 @@ class DatabaseManager:
225
  created_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
226
  FOREIGN KEY (product_id) REFERENCES products (product_id) ON DELETE SET NULL
227
  )
228
- ''')
229
-
230
  # Demo teams table
231
- conn.execute('''
232
  CREATE TABLE IF NOT EXISTS demo_teams (
233
  team_id INTEGER PRIMARY KEY AUTOINCREMENT,
234
  team_name TEXT NOT NULL,
@@ -238,24 +242,25 @@ class DatabaseManager:
238
  status TEXT DEFAULT 'Active',
239
  created_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP
240
  )
241
- ''')
242
-
243
  conn.commit()
244
  logger.info("Database tables initialized successfully")
245
-
246
  except sqlite3.Error as e:
247
  logger.error(f"Error initializing database: {e}")
248
  raise
249
  finally:
250
  conn.close()
251
-
252
  self.initialize_default_data()
 
253
  self.create_indexes()
254
-
255
  def create_indexes(self):
256
  """Create indexes for better performance"""
257
  conn = self.get_connection()
258
-
259
  try:
260
  # Create indexes for frequently queried columns
261
  indexes = [
@@ -269,62 +274,107 @@ class DatabaseManager:
269
  "CREATE INDEX IF NOT EXISTS idx_demos_date ON demos(demo_date)",
270
  "CREATE INDEX IF NOT EXISTS idx_sale_items_sale_id ON sale_items(sale_id)",
271
  "CREATE INDEX IF NOT EXISTS idx_follow_ups_date ON follow_ups(follow_up_date)",
272
- "CREATE INDEX IF NOT EXISTS idx_whatsapp_customer_id ON whatsapp_logs(customer_id)"
273
  ]
274
-
275
  for index_sql in indexes:
276
  conn.execute(index_sql)
277
-
278
  conn.commit()
279
  logger.info("Database indexes created successfully")
280
-
281
  except sqlite3.Error as e:
282
  logger.error(f"Error creating indexes: {e}")
283
  finally:
284
  conn.close()
285
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
286
  def initialize_default_data(self):
287
  """Initialize with default products and demo teams"""
288
  default_products = [
289
- ('1 LTR PLASTIC JAR', 'PLASTIC_JAR', 1.0, 'Regular', 95),
290
- ('2 LTR PLASTIC JAR', 'PLASTIC_JAR', 2.0, 'Regular', 185),
291
- ('5 LTR PLASTIC JAR', 'PLASTIC_JAR', 5.0, 'Regular', 460),
292
- ('5 LTR STEEL BARNI', 'STEEL_BARNI', 5.0, 'Premium', 680),
293
- ('10 LTR STEEL BARNI', 'STEEL_BARNI', 10.0, 'Premium', 1300),
294
- ('20 LTR STEEL BARNI', 'STEEL_BARNI', 20.0, 'Premium', 2950),
295
- ('20 LTR PLASTIC CAN', 'PLASTIC_CAN', 20.0, 'Regular', 2400),
296
- ('1 LTR PET BOTTLE', 'PET_BOTTLE', 1.0, 'Regular', 85)
297
  ]
298
-
299
  default_teams = [
300
- ('Team A - North Region', 'Rajesh Kumar', 'Mohan, Suresh, Priya', 'Amiyad, Amvad, Ankalav'),
301
- ('Team B - South Region', 'Sunil Patel', 'Anita, Vijay, Deepak', 'Petlad, Borsad, Vadodara')
 
 
 
 
 
 
 
 
 
 
302
  ]
303
-
304
  conn = self.get_connection()
305
  try:
306
  # Insert default products
307
  for product in default_products:
308
- conn.execute('''
 
309
  INSERT OR IGNORE INTO products (product_name, packing_type, capacity_ltr, category, standard_rate)
310
  VALUES (?, ?, ?, ?, ?)
311
- ''', product)
312
-
 
 
313
  # Insert default demo teams
314
  for team in default_teams:
315
- conn.execute('''
 
316
  INSERT OR IGNORE INTO demo_teams (team_name, team_leader, team_members, assigned_villages)
317
  VALUES (?, ?, ?, ?)
318
- ''', team)
319
-
 
 
320
  conn.commit()
321
  logger.info("Default data initialized successfully")
322
-
323
  except sqlite3.Error as e:
324
  logger.error(f"Error initializing default data: {e}")
325
  finally:
326
  conn.close()
327
-
328
  def _execute_query_internal(self, query: str, params: tuple = None) -> List[tuple]:
329
  """Internal method to execute SQL query without logging"""
330
  conn = self.get_connection()
@@ -334,47 +384,63 @@ class DatabaseManager:
334
  cursor.execute(query, params)
335
  else:
336
  cursor.execute(query)
337
-
338
  # Only try to fetch results for SELECT queries
339
- if query.strip().upper().startswith('SELECT'):
340
  result = cursor.fetchall()
 
 
 
341
  else:
342
  result = []
343
-
344
  conn.commit()
345
  return result
346
-
347
  except sqlite3.Error as e:
348
  logger.error(f"Database query error: {e}")
349
  conn.rollback()
350
  raise
351
  finally:
352
  conn.close()
353
-
354
- def execute_query(self, query: str, params: tuple = None, log_action: bool = True) -> List[tuple]:
 
 
355
  """Execute a SQL query with comprehensive error handling"""
356
  try:
357
  result = self._execute_query_internal(query, params)
358
-
359
  # Log the query execution (but avoid recursion)
360
  if log_action and not self._is_logging:
361
  try:
362
  self._is_logging = True
363
- self._execute_query_internal('''
 
364
  INSERT INTO system_logs (log_type, log_message, table_name, record_id, action)
365
  VALUES (?, ?, ?, ?, ?)
366
- ''', ('QUERY_EXECUTION', f"Executed query: {query[:100]}...", None, None, 'EXECUTE'))
 
 
 
 
 
 
 
 
367
  except Exception as e:
368
  logger.error(f"Error logging system action: {e}")
369
  finally:
370
  self._is_logging = False
371
-
372
  return result
373
  except Exception as e:
374
  logger.error(f"Error in execute_query: {e}")
375
  return [] # Return empty list instead of raising exception
376
-
377
- def get_dataframe(self, table_name: str = None, query: str = None, params: tuple = None) -> pd.DataFrame:
 
 
378
  """Get table data as DataFrame with flexible query support"""
379
  conn = self.get_connection()
380
  try:
@@ -384,101 +450,157 @@ class DatabaseManager:
384
  df = pd.read_sql_query(f"SELECT * FROM {table_name}", conn)
385
  return df
386
  except Exception as e:
387
- logger.error(f"Error getting DataFrame for {table_name if table_name else 'query'}: {e}")
 
 
388
  # Return empty DataFrame with proper structure
389
  return pd.DataFrame()
390
  finally:
391
  conn.close()
392
-
393
- def add_customer(self, name: str, mobile: str = "", village: str = "", taluka: str = "",
394
- district: str = "", customer_code: str = None) -> int:
 
 
 
 
 
 
 
395
  """Add a new customer with duplicate handling"""
396
-
397
  # Generate customer code if not provided
398
  if not customer_code:
399
  customer_code = f"CUST{datetime.now().strftime('%Y%m%d%H%M%S')}{random.randint(100, 999)}"
400
-
401
  try:
402
  # Check if customer already exists (by mobile or similar name+village)
403
  existing_customer = self.execute_query(
404
- 'SELECT customer_id FROM customers WHERE mobile = ? OR (name = ? AND village = ?)',
405
  (mobile, name, village),
406
- log_action=False
407
  )
408
-
409
  if existing_customer:
410
  # Customer already exists, return existing ID
411
  return existing_customer[0][0]
412
-
413
  # If customer_code already exists, generate a new one
414
  max_attempts = 5
415
  for attempt in range(max_attempts):
416
  try:
417
- result = self.execute_query('''
 
418
  INSERT INTO customers (customer_code, name, mobile, village, taluka, district)
419
  VALUES (?, ?, ?, ?, ?, ?)
420
- ''', (customer_code, name, mobile, village, taluka, district), log_action=False)
 
 
 
421
  break
422
  except sqlite3.IntegrityError as e:
423
- if "UNIQUE constraint failed: customers.customer_code" in str(e) and attempt < max_attempts - 1:
 
 
 
424
  # Generate new unique customer code
425
  customer_code = f"CUST{datetime.now().strftime('%Y%m%d%H%M%S')}{random.randint(1000, 9999)}"
426
  continue
427
  else:
428
  raise e
429
-
430
  # Get the inserted customer_id
431
- customer_id = self.execute_query('SELECT last_insert_rowid()', log_action=False)[0][0]
432
-
433
- self.log_system_action('CUSTOMER_ADD', f"Added customer: {name}", 'customers', customer_id, 'INSERT')
434
-
 
 
 
 
 
 
 
 
435
  return customer_id
436
  except Exception as e:
437
  logger.error(f"Error adding customer: {e}")
438
  # Return a fallback - this won't be in database but prevents crashes
439
  return -1
440
- def add_distributor(self, name: str, village: str = "", taluka: str = "", district: str = "",
441
- mantri_name: str = "", mantri_mobile: str = "", sabhasad_count: int = 0,
442
- contact_in_group: int = 0, status: str = "Active") -> int:
 
 
 
 
 
 
 
 
 
 
443
  """Add a new distributor with duplicate handling"""
444
-
445
  try:
446
  # Check if distributor already exists
447
  existing_distributor = self.execute_query(
448
- 'SELECT distributor_id FROM distributors WHERE name = ? AND village = ? AND taluka = ?',
449
  (name, village, taluka),
450
- log_action=False
451
  )
452
-
453
  if existing_distributor:
454
  # Distributor already exists, return existing ID
455
  return existing_distributor[0][0]
456
-
457
  # Insert new distributor
458
- self.execute_query('''
459
- INSERT INTO distributors (name, village, taluka, district, mantri_name, mantri_mobile,
 
460
  sabhasad_count, contact_in_group, status)
461
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
462
- ''', (name, village, taluka, district, mantri_name, mantri_mobile,
463
- sabhasad_count, contact_in_group, status), log_action=False)
464
-
 
 
 
 
 
 
 
 
 
 
 
 
465
  # Get the inserted distributor_id
466
- distributor_id = self.execute_query('SELECT last_insert_rowid()', log_action=False)[0][0]
467
-
468
- self.log_system_action('DISTRIBUTOR_ADD', f"Added distributor: {name}", 'distributors', distributor_id, 'INSERT')
469
-
 
 
 
 
 
 
 
 
470
  return distributor_id
471
-
472
  except Exception as e:
473
  logger.error(f"Error adding distributor: {e}")
474
  return -1
 
475
  def get_distributor_by_location(self, village: str, taluka: str) -> Optional[Dict]:
476
  """Get distributor by village and taluka"""
477
  try:
478
  result = self.execute_query(
479
- 'SELECT * FROM distributors WHERE village = ? AND taluka = ?',
480
  (village, taluka),
481
- log_action=False
482
  )
483
  if result:
484
  return dict(result[0])
@@ -491,14 +613,15 @@ class DatabaseManager:
491
  """Check if distributor already exists"""
492
  try:
493
  result = self.execute_query(
494
- 'SELECT distributor_id FROM distributors WHERE name = ? AND village = ? AND taluka = ?',
495
  (name, village, taluka),
496
- log_action=False
497
  )
498
  return len(result) > 0
499
  except Exception as e:
500
  logger.error(f"Error checking distributor existence: {e}")
501
  return False
 
502
  # In your DatabaseManager class in database.py, replace the generate_invoice_number method:
503
 
504
  def generate_invoice_number(self):
@@ -506,16 +629,16 @@ class DatabaseManager:
506
  try:
507
  # Get current date components
508
  now = datetime.now()
509
- month = now.strftime('%m') # Two-digit month
510
- year = now.strftime('%y') # Two-digit year
511
-
512
  # Get the last invoice number for this month-year
513
  result = self.execute_query(
514
  "SELECT invoice_no FROM sales WHERE invoice_no LIKE ? ORDER BY sale_id DESC LIMIT 1",
515
  (f"INVCL{month}{year}%",),
516
- log_action=False
517
  )
518
-
519
  if result:
520
  last_invoice = result[0][0]
521
  # Extract serial number and increment
@@ -529,10 +652,10 @@ class DatabaseManager:
529
  else:
530
  # First invoice of the month-year
531
  new_serial = 1
532
-
533
  # Format: INVCL + month(2) + year(2) + serial(3 digits)
534
  return f"INVCL{month}{year}{new_serial:03d}"
535
-
536
  except Exception as e:
537
  logger.error(f"Error generating invoice number: {e}")
538
  # Fallback: timestamp-based
@@ -543,85 +666,119 @@ class DatabaseManager:
543
  """Generate automatic invoice number in format: PREFIXmmyyserial"""
544
  try:
545
  now = datetime.now()
546
- month = now.strftime('%m')
547
- year = now.strftime('%y')
548
-
549
  result = self.execute_query(
550
  "SELECT invoice_no FROM sales WHERE invoice_no LIKE ? ORDER BY sale_id DESC LIMIT 1",
551
  (f"{prefix}{month}{year}%",),
552
- log_action=False
553
  )
554
-
555
  if result:
556
  last_invoice = result[0][0]
557
  try:
558
  # Remove prefix and date part, get serial
559
- serial_part = last_invoice[len(prefix) + 4:] # prefix + 4 digits (mmyy)
 
 
560
  last_serial = int(serial_part)
561
  new_serial = last_serial + 1
562
  except ValueError:
563
  new_serial = 1
564
  else:
565
  new_serial = 1
566
-
567
  return f"{prefix}{month}{year}{new_serial:03d}"
568
-
569
  except Exception as e:
570
  logger.error(f"Error generating invoice number: {e}")
571
  return f"{prefix}{int(datetime.now().timestamp())}"
572
-
573
  # Add to your DatabaseManager class in database.py
574
 
575
- def add_sale(self, invoice_no: str, customer_id: int, sale_date, items: List[Dict],
576
- payments: List[Dict] = None, notes: str = "") -> int:
 
 
 
 
 
 
 
577
  """Add a new sale with items and optional payments - ENHANCED"""
578
  conn = self.get_connection()
579
  try:
580
  cursor = conn.cursor()
581
-
582
  # Calculate total amount and liters
583
- total_amount = sum(item['quantity'] * item['rate'] for item in items)
584
- total_liters = sum(item.get('liters', 0) for item in items)
585
-
586
- print(f"🔧 DEBUG: Creating sale - Invoice: {invoice_no}, Customer: {customer_id}, Total: {total_amount}") # DEBUG
587
-
 
 
588
  # Add sale record
589
- cursor.execute('''
 
590
  INSERT INTO sales (invoice_no, customer_id, sale_date, total_amount, total_liters, notes)
591
  VALUES (?, ?, ?, ?, ?, ?)
592
- ''', (invoice_no, customer_id, sale_date, total_amount, total_liters, notes))
593
-
 
 
594
  # Get the sale ID
595
  sale_id = cursor.lastrowid
596
  print(f"🔧 DEBUG: Sale created with ID: {sale_id}") # DEBUG
597
-
598
  # Add sale items
599
  for item in items:
600
- amount = item['quantity'] * item['rate']
601
- print(f"🔧 DEBUG: Adding item - Product: {item['product_id']}, Qty: {item['quantity']}, Rate: {item['rate']}") # DEBUG
602
-
603
- cursor.execute('''
 
 
 
604
  INSERT INTO sale_items (sale_id, product_id, quantity, rate, amount)
605
  VALUES (?, ?, ?, ?, ?)
606
- ''', (sale_id, item['product_id'], item['quantity'], item['rate'], amount))
607
-
 
 
 
 
 
 
 
 
608
  # Add payments if provided
609
  if payments:
610
  for payment in payments:
611
- cursor.execute('''
 
612
  INSERT INTO payments (sale_id, payment_date, payment_method, amount, rrn, reference)
613
  VALUES (?, ?, ?, ?, ?, ?)
614
- ''', (sale_id, payment['payment_date'], payment['method'],
615
- payment['amount'], payment.get('rrn', ''), payment.get('reference', '')))
616
-
 
 
 
 
 
 
 
 
617
  conn.commit()
618
-
619
  # Update payment status
620
  self._update_payment_status(sale_id)
621
-
622
  print(f"🔧 DEBUG: Sale {sale_id} completed successfully") # DEBUG
623
  return sale_id
624
-
625
  except Exception as e:
626
  conn.rollback()
627
  logger.error(f"Error adding sale: {e}")
@@ -629,41 +786,51 @@ class DatabaseManager:
629
  raise
630
  finally:
631
  conn.close()
632
-
633
  def _update_payment_status(self, sale_id: int):
634
  """Update payment status for a sale"""
635
  conn = self.get_connection()
636
  try:
637
  # Get total paid amount
638
  cursor = conn.cursor()
639
- cursor.execute('SELECT COALESCE(SUM(amount), 0) FROM payments WHERE sale_id = ?', (sale_id,))
 
 
 
640
  total_paid = cursor.fetchone()[0]
641
-
642
  # Get sale total
643
- cursor.execute('SELECT total_amount FROM sales WHERE sale_id = ?', (sale_id,))
 
 
644
  sale_total = cursor.fetchone()[0]
645
-
646
  # Determine payment status
647
  if total_paid >= sale_total:
648
- status = 'Paid'
649
  elif total_paid > 0:
650
- status = 'Partial'
651
  else:
652
- status = 'Pending'
653
-
654
  # Update status
655
- cursor.execute('UPDATE sales SET payment_status = ? WHERE sale_id = ?', (status, sale_id))
 
 
 
656
  conn.commit()
657
-
658
  except Exception as e:
659
  logger.error(f"Error updating payment status: {e}")
660
  finally:
661
  conn.close()
662
-
663
  def get_pending_payments(self) -> pd.DataFrame:
664
  """Get all pending payments with customer details"""
665
- return self.get_dataframe('sales', '''
666
- SELECT s.sale_id, s.invoice_no, s.sale_date, c.name as customer_name,
 
 
667
  c.mobile, c.village, s.total_amount,
668
  (s.total_amount - COALESCE(SUM(p.amount), 0)) as pending_amount,
669
  COALESCE(SUM(p.amount), 0) as paid_amount
@@ -674,12 +841,15 @@ class DatabaseManager:
674
  GROUP BY s.sale_id
675
  HAVING pending_amount > 0
676
  ORDER BY s.sale_date DESC
677
- ''')
678
-
 
679
  def get_demo_conversions(self) -> pd.DataFrame:
680
  """Get demo conversion statistics with details"""
681
- return self.get_dataframe('demos', '''
682
- SELECT d.*, c.name as customer_name, p.product_name,
 
 
683
  dist.name as distributor_name, c.village, c.taluka,
684
  CASE WHEN d.conversion_status = 'Converted' THEN 1 ELSE 0 END as converted
685
  FROM demos d
@@ -687,104 +857,124 @@ class DatabaseManager:
687
  LEFT JOIN products p ON d.product_id = p.product_id
688
  LEFT JOIN distributors dist ON d.distributor_id = dist.distributor_id
689
  ORDER BY d.demo_date DESC
690
- ''')
691
-
 
692
  def get_sales_analytics(self, start_date: str = None, end_date: str = None) -> Dict:
693
  """Get comprehensive sales analytics"""
694
  if not start_date:
695
- start_date = (datetime.now() - timedelta(days=30)).strftime('%Y-%m-%d')
696
  if not end_date:
697
- end_date = datetime.now().strftime('%Y-%m-%d')
698
-
699
- query = '''
700
- SELECT
701
  COUNT(*) as total_sales,
702
  SUM(total_amount) as total_revenue,
703
  AVG(total_amount) as avg_sale_value,
704
  COUNT(DISTINCT customer_id) as unique_customers,
705
  SUM(CASE WHEN payment_status = 'Paid' THEN 1 ELSE 0 END) as completed_payments,
706
  SUM(CASE WHEN payment_status IN ('Pending', 'Partial') THEN 1 ELSE 0 END) as pending_payments
707
- FROM sales
708
  WHERE sale_date BETWEEN ? AND ?
709
- '''
710
-
711
  result = self.execute_query(query, (start_date, end_date), log_action=False)
712
-
713
  if result:
714
  row = result[0]
715
  return {
716
- 'total_sales': row[0] or 0,
717
- 'total_revenue': row[1] or 0,
718
- 'avg_sale_value': row[2] or 0,
719
- 'unique_customers': row[3] or 0,
720
- 'completed_payments': row[4] or 0,
721
- 'pending_payments': row[5] or 0
722
  }
723
  return {}
724
-
725
- def log_system_action(self, log_type: str, message: str, table_name: str = None,
726
- record_id: int = None, action: str = None):
 
 
 
 
 
 
727
  """Log system actions for audit trail - without recursion"""
728
  if self._is_logging:
729
  return # Prevent recursion
730
-
731
  try:
732
  self._is_logging = True
733
- self._execute_query_internal('''
 
734
  INSERT INTO system_logs (log_type, log_message, table_name, record_id, action)
735
  VALUES (?, ?, ?, ?, ?)
736
- ''', (log_type, message, table_name, record_id, action))
 
 
737
  except Exception as e:
738
  logger.error(f"Error logging system action: {e}")
739
  finally:
740
  self._is_logging = False
741
-
742
- def create_rollback_point(self, table_name: str, record_id: int, old_data: str,
743
- new_data: str, action: str):
 
744
  """Create a rollback point for data changes"""
745
  try:
746
- self.execute_query('''
 
747
  INSERT INTO rollback_logs (table_name, record_id, old_data, new_data, action)
748
  VALUES (?, ?, ?, ?, ?)
749
- ''', (table_name, record_id, old_data, new_data, action), log_action=False)
 
 
 
750
  except Exception as e:
751
  logger.error(f"Error creating rollback point: {e}")
752
-
753
  def get_recent_activity(self, limit: int = 10) -> pd.DataFrame:
754
  """Get recent system activity"""
755
- return self.get_dataframe('system_logs', f'''
 
 
756
  SELECT log_type, log_message, table_name, record_id, action, created_date
757
- FROM system_logs
758
- ORDER BY created_date DESC
759
  LIMIT {limit}
760
- ''')
761
-
 
762
  def backup_database(self, backup_path: str = None):
763
  """Create a database backup"""
764
  if not backup_path:
765
  backup_path = f"backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}.db"
766
-
767
  try:
768
  conn = self.get_connection()
769
  backup_conn = sqlite3.connect(backup_path)
770
-
771
  with backup_conn:
772
  conn.backup(backup_conn)
773
-
774
  conn.close()
775
  backup_conn.close()
776
-
777
  logger.info(f"Database backup created: {backup_path}")
778
  return backup_path
779
-
780
  except Exception as e:
781
  logger.error(f"Error creating database backup: {e}")
782
  return None
783
-
784
  def get_village_wise_sales(self) -> pd.DataFrame:
785
  """Get sales data grouped by village"""
786
- return self.get_dataframe('sales', '''
787
- SELECT c.village, COUNT(s.sale_id) as total_sales,
 
 
788
  SUM(s.total_amount) as total_revenue,
789
  AVG(s.total_amount) as avg_sale_value,
790
  COUNT(DISTINCT s.customer_id) as unique_customers
@@ -793,101 +983,124 @@ class DatabaseManager:
793
  WHERE c.village IS NOT NULL AND c.village != ''
794
  GROUP BY c.village
795
  ORDER BY total_revenue DESC
796
- ''')
797
-
 
798
  def get_product_performance(self) -> pd.DataFrame:
799
  """Get product performance analytics"""
800
- return self.get_dataframe('sale_items', '''
 
 
801
  SELECT p.product_name, COUNT(si.item_id) as times_sold,
802
- SUM(si.quantity) as total_quantity,
803
  SUM(si.amount) as total_revenue,
804
  AVG(si.rate) as avg_rate
805
  FROM sale_items si
806
  JOIN products p ON si.product_id = p.product_id
807
  GROUP BY p.product_id, p.product_name
808
  ORDER BY total_revenue DESC
809
- ''')
810
-
 
811
  def get_upcoming_follow_ups(self) -> pd.DataFrame:
812
  """Get upcoming follow-ups"""
813
- return self.get_dataframe('follow_ups', '''
814
- SELECT f.*, c.name as customer_name, c.mobile,
 
 
815
  d.name as distributor_name, dm.demo_date
816
  FROM follow_ups f
817
  LEFT JOIN customers c ON f.customer_id = c.customer_id
818
  LEFT JOIN distributors d ON f.distributor_id = d.distributor_id
819
  LEFT JOIN demos dm ON f.demo_id = dm.demo_id
820
- WHERE f.follow_up_date >= date('now')
821
  AND f.status = 'Pending'
822
  ORDER BY f.follow_up_date ASC
823
  LIMIT 20
824
- ''')
825
-
 
826
  def get_whatsapp_logs(self, customer_id: int = None) -> pd.DataFrame:
827
  """Get WhatsApp communication logs"""
828
  if customer_id:
829
- return self.get_dataframe('whatsapp_logs', '''
 
 
830
  SELECT w.*, c.name as customer_name, c.mobile
831
  FROM whatsapp_logs w
832
  LEFT JOIN customers c ON w.customer_id = c.customer_id
833
  WHERE w.customer_id = ?
834
  ORDER BY w.sent_date DESC
835
- ''', (customer_id,))
 
 
836
  else:
837
- return self.get_dataframe('whatsapp_logs', '''
 
 
838
  SELECT w.*, c.name as customer_name, c.mobile
839
  FROM whatsapp_logs w
840
  LEFT JOIN customers c ON w.customer_id = c.customer_id
841
  ORDER BY w.sent_date DESC
842
  LIMIT 50
843
- ''')
844
-
 
845
  def cleanup_old_data(self, days: int = 365):
846
  """Clean up old data (logs, etc.) older than specified days"""
847
  try:
848
- cutoff_date = (datetime.now() - timedelta(days=days)).strftime('%Y-%m-%d')
849
-
850
  # Clean system logs
851
- self.execute_query('DELETE FROM system_logs WHERE created_date < ?', (cutoff_date,), log_action=False)
852
-
 
 
 
 
853
  # Clean rollback logs
854
- self.execute_query('DELETE FROM rollback_logs WHERE rollback_date < ?', (cutoff_date,), log_action=False)
855
-
 
 
 
 
856
  logger.info(f"Cleaned up data older than {days} days")
857
-
858
  except Exception as e:
859
  logger.error(f"Error cleaning up old data: {e}")
860
 
 
861
  # Utility function to check database health
862
  def check_database_health(db_path: str = "sales_management.db") -> Dict:
863
  """Check database health and statistics"""
864
  try:
865
  db = DatabaseManager(db_path)
866
-
867
  # Get table counts
868
- tables = ['customers', 'sales', 'distributors', 'demos', 'payments', 'products']
869
  counts = {}
870
-
871
  for table in tables:
872
  result = db.execute_query(f"SELECT COUNT(*) FROM {table}", log_action=False)
873
  counts[table] = result[0][0] if result else 0
874
-
875
  # Get database size
876
  db_size = os.path.getsize(db_path) if os.path.exists(db_path) else 0
877
-
878
  return {
879
- 'status': 'healthy',
880
- 'table_counts': counts,
881
- 'database_size_mb': round(db_size / (1024 * 1024), 2),
882
- 'last_backup': 'N/A', # You can implement backup tracking
883
- 'integrity_check': 'passed' # You can add actual integrity checks
884
  }
885
-
886
  except Exception as e:
887
  return {
888
- 'status': 'error',
889
- 'error': str(e),
890
- 'table_counts': {},
891
- 'database_size_mb': 0,
892
- 'integrity_check': 'failed'
893
- }
 
4
  import logging
5
  from datetime import datetime, timedelta
6
  from typing import List, Dict, Any, Optional
7
+ import random
8
+
9
  # Set up logging
10
  logging.basicConfig(level=logging.INFO)
11
  logger = logging.getLogger(__name__)
12
 
13
+
14
  class DatabaseManager:
15
  def __init__(self, db_path="sales_management.db"):
16
  self.db_path = db_path
17
  self._is_logging = False # Prevent recursion
18
  self.init_database()
19
+
20
  def get_connection(self):
21
  """Get database connection with error handling"""
22
  try:
 
26
  except sqlite3.Error as e:
27
  logger.error(f"Database connection error: {e}")
28
  raise
29
+
30
  def init_database(self):
31
  """Initialize database with all tables and relationships"""
32
  conn = self.get_connection()
33
+
34
  try:
35
  # Customers table
36
+ conn.execute("""
37
  CREATE TABLE IF NOT EXISTS customers (
38
  customer_id INTEGER PRIMARY KEY AUTOINCREMENT,
39
  customer_code TEXT UNIQUE,
 
46
  created_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
47
  updated_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP
48
  )
49
+ """)
50
+
51
  # Distributors table
52
+ conn.execute("""
53
  CREATE TABLE IF NOT EXISTS distributors (
54
  distributor_id INTEGER PRIMARY KEY AUTOINCREMENT,
55
  name TEXT NOT NULL,
 
64
  created_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
65
  updated_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP
66
  )
67
+ """)
68
+
69
  # Products table
70
+ conn.execute("""
71
  CREATE TABLE IF NOT EXISTS products (
72
  product_id INTEGER PRIMARY KEY AUTOINCREMENT,
73
  product_name TEXT UNIQUE NOT NULL,
 
78
  is_active INTEGER DEFAULT 1,
79
  created_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP
80
  )
81
+ """)
82
+
83
  # Sales table
84
+ conn.execute("""
85
  CREATE TABLE IF NOT EXISTS sales (
86
  sale_id INTEGER PRIMARY KEY AUTOINCREMENT,
87
  invoice_no TEXT UNIQUE NOT NULL,
 
95
  updated_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
96
  FOREIGN KEY (customer_id) REFERENCES customers (customer_id) ON DELETE SET NULL
97
  )
98
+ """)
99
+
100
  # Sale items table
101
+ conn.execute("""
102
  CREATE TABLE IF NOT EXISTS sale_items (
103
  item_id INTEGER PRIMARY KEY AUTOINCREMENT,
104
  sale_id INTEGER,
 
110
  FOREIGN KEY (sale_id) REFERENCES sales (sale_id) ON DELETE CASCADE,
111
  FOREIGN KEY (product_id) REFERENCES products (product_id) ON DELETE SET NULL
112
  )
113
+ """)
114
+
115
  # Payments table
116
+ conn.execute("""
117
  CREATE TABLE IF NOT EXISTS payments (
118
  payment_id INTEGER PRIMARY KEY AUTOINCREMENT,
119
  sale_id INTEGER,
 
127
  created_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
128
  FOREIGN KEY (sale_id) REFERENCES sales (sale_id) ON DELETE CASCADE
129
  )
130
+ """)
131
+
132
  # Demos table
133
+ conn.execute("""
134
  CREATE TABLE IF NOT EXISTS demos (
135
  demo_id INTEGER PRIMARY KEY AUTOINCREMENT,
136
  customer_id INTEGER,
137
  distributor_id INTEGER,
138
  demo_date DATE,
139
+ demo_time TIME,
140
  product_id INTEGER,
141
  quantity_provided INTEGER,
142
  follow_up_date DATE,
143
  conversion_status TEXT DEFAULT 'Not Converted',
144
  notes TEXT,
145
+ demo_location TEXT,
146
  created_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
147
  updated_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
148
  FOREIGN KEY (customer_id) REFERENCES customers (customer_id) ON DELETE SET NULL,
149
  FOREIGN KEY (distributor_id) REFERENCES distributors (distributor_id) ON DELETE SET NULL,
150
  FOREIGN KEY (product_id) REFERENCES products (product_id) ON DELETE SET NULL
151
  )
152
+ """)
153
+
154
  # WhatsApp logs table
155
+ conn.execute("""
156
  CREATE TABLE IF NOT EXISTS whatsapp_logs (
157
  log_id INTEGER PRIMARY KEY AUTOINCREMENT,
158
  customer_id INTEGER,
 
165
  FOREIGN KEY (customer_id) REFERENCES customers (customer_id) ON DELETE SET NULL,
166
  FOREIGN KEY (distributor_id) REFERENCES distributors (distributor_id) ON DELETE SET NULL
167
  )
168
+ """)
169
+
170
  # Follow-ups table
171
+ conn.execute("""
172
  CREATE TABLE IF NOT EXISTS follow_ups (
173
  follow_up_id INTEGER PRIMARY KEY AUTOINCREMENT,
174
  customer_id INTEGER,
 
184
  FOREIGN KEY (distributor_id) REFERENCES distributors (distributor_id) ON DELETE SET NULL,
185
  FOREIGN KEY (demo_id) REFERENCES demos (demo_id) ON DELETE SET NULL
186
  )
187
+ """)
188
+
189
  # System logs table
190
+ conn.execute("""
191
  CREATE TABLE IF NOT EXISTS system_logs (
192
  log_id INTEGER PRIMARY KEY AUTOINCREMENT,
193
  log_type TEXT,
 
198
  created_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
199
  user_info TEXT
200
  )
201
+ """)
202
+
203
  # Rollback logs table
204
+ conn.execute("""
205
  CREATE TABLE IF NOT EXISTS rollback_logs (
206
  rollback_id INTEGER PRIMARY KEY AUTOINCREMENT,
207
  table_name TEXT,
 
212
  rollback_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
213
  rolled_back_by TEXT
214
  )
215
+ """)
216
+
217
  # Offers table
218
+ conn.execute("""
219
  CREATE TABLE IF NOT EXISTS offers (
220
  offer_id INTEGER PRIMARY KEY AUTOINCREMENT,
221
  offer_name TEXT NOT NULL,
 
229
  created_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
230
  FOREIGN KEY (product_id) REFERENCES products (product_id) ON DELETE SET NULL
231
  )
232
+ """)
233
+
234
  # Demo teams table
235
+ conn.execute("""
236
  CREATE TABLE IF NOT EXISTS demo_teams (
237
  team_id INTEGER PRIMARY KEY AUTOINCREMENT,
238
  team_name TEXT NOT NULL,
 
242
  status TEXT DEFAULT 'Active',
243
  created_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP
244
  )
245
+ """)
246
+
247
  conn.commit()
248
  logger.info("Database tables initialized successfully")
249
+
250
  except sqlite3.Error as e:
251
  logger.error(f"Error initializing database: {e}")
252
  raise
253
  finally:
254
  conn.close()
255
+
256
  self.initialize_default_data()
257
+ self.migrate_database()
258
  self.create_indexes()
259
+
260
  def create_indexes(self):
261
  """Create indexes for better performance"""
262
  conn = self.get_connection()
263
+
264
  try:
265
  # Create indexes for frequently queried columns
266
  indexes = [
 
274
  "CREATE INDEX IF NOT EXISTS idx_demos_date ON demos(demo_date)",
275
  "CREATE INDEX IF NOT EXISTS idx_sale_items_sale_id ON sale_items(sale_id)",
276
  "CREATE INDEX IF NOT EXISTS idx_follow_ups_date ON follow_ups(follow_up_date)",
277
+ "CREATE INDEX IF NOT EXISTS idx_whatsapp_customer_id ON whatsapp_logs(customer_id)",
278
  ]
279
+
280
  for index_sql in indexes:
281
  conn.execute(index_sql)
282
+
283
  conn.commit()
284
  logger.info("Database indexes created successfully")
285
+
286
  except sqlite3.Error as e:
287
  logger.error(f"Error creating indexes: {e}")
288
  finally:
289
  conn.close()
290
+
291
+ def migrate_database(self):
292
+ """Migrate existing database to add missing columns"""
293
+ conn = self.get_connection()
294
+ try:
295
+ cursor = conn.cursor()
296
+
297
+ # Check if demo_time column exists
298
+ cursor.execute("PRAGMA table_info(demos)")
299
+ columns = [column[1] for column in cursor.fetchall()]
300
+
301
+ # Add demo_time column if it doesn't exist
302
+ if "demo_time" not in columns:
303
+ cursor.execute("ALTER TABLE demos ADD COLUMN demo_time TIME")
304
+ logger.info("Added demo_time column to demos table")
305
+
306
+ # Add demo_location column if it doesn't exist
307
+ if "demo_location" not in columns:
308
+ cursor.execute("ALTER TABLE demos ADD COLUMN demo_location TEXT")
309
+ logger.info("Added demo_location column to demos table")
310
+
311
+ conn.commit()
312
+ logger.info("Database migration completed successfully")
313
+
314
+ except sqlite3.Error as e:
315
+ logger.error(f"Error during database migration: {e}")
316
+ conn.rollback()
317
+ finally:
318
+ conn.close()
319
+
320
  def initialize_default_data(self):
321
  """Initialize with default products and demo teams"""
322
  default_products = [
323
+ ("1 LTR PLASTIC JAR", "PLASTIC_JAR", 1.0, "Regular", 95),
324
+ ("2 LTR PLASTIC JAR", "PLASTIC_JAR", 2.0, "Regular", 185),
325
+ ("5 LTR PLASTIC JAR", "PLASTIC_JAR", 5.0, "Regular", 460),
326
+ ("5 LTR STEEL BARNI", "STEEL_BARNI", 5.0, "Premium", 680),
327
+ ("10 LTR STEEL BARNI", "STEEL_BARNI", 10.0, "Premium", 1300),
328
+ ("20 LTR STEEL BARNI", "STEEL_BARNI", 20.0, "Premium", 2950),
329
+ ("20 LTR PLASTIC CAN", "PLASTIC_CAN", 20.0, "Regular", 2400),
330
+ ("1 LTR PET BOTTLE", "PET_BOTTLE", 1.0, "Regular", 85),
331
  ]
332
+
333
  default_teams = [
334
+ (
335
+ "Team A - North Region",
336
+ "Rajesh Kumar",
337
+ "Mohan, Suresh, Priya",
338
+ "Amiyad, Amvad, Ankalav",
339
+ ),
340
+ (
341
+ "Team B - South Region",
342
+ "Sunil Patel",
343
+ "Anita, Vijay, Deepak",
344
+ "Petlad, Borsad, Vadodara",
345
+ ),
346
  ]
347
+
348
  conn = self.get_connection()
349
  try:
350
  # Insert default products
351
  for product in default_products:
352
+ conn.execute(
353
+ """
354
  INSERT OR IGNORE INTO products (product_name, packing_type, capacity_ltr, category, standard_rate)
355
  VALUES (?, ?, ?, ?, ?)
356
+ """,
357
+ product,
358
+ )
359
+
360
  # Insert default demo teams
361
  for team in default_teams:
362
+ conn.execute(
363
+ """
364
  INSERT OR IGNORE INTO demo_teams (team_name, team_leader, team_members, assigned_villages)
365
  VALUES (?, ?, ?, ?)
366
+ """,
367
+ team,
368
+ )
369
+
370
  conn.commit()
371
  logger.info("Default data initialized successfully")
372
+
373
  except sqlite3.Error as e:
374
  logger.error(f"Error initializing default data: {e}")
375
  finally:
376
  conn.close()
377
+
378
  def _execute_query_internal(self, query: str, params: tuple = None) -> List[tuple]:
379
  """Internal method to execute SQL query without logging"""
380
  conn = self.get_connection()
 
384
  cursor.execute(query, params)
385
  else:
386
  cursor.execute(query)
387
+
388
  # Only try to fetch results for SELECT queries
389
+ if query.strip().upper().startswith("SELECT"):
390
  result = cursor.fetchall()
391
+ elif query.strip().upper().startswith("INSERT"):
392
+ # For INSERT queries, return the lastrowid as a single-row result
393
+ result = [(cursor.lastrowid,)]
394
  else:
395
  result = []
396
+
397
  conn.commit()
398
  return result
399
+
400
  except sqlite3.Error as e:
401
  logger.error(f"Database query error: {e}")
402
  conn.rollback()
403
  raise
404
  finally:
405
  conn.close()
406
+
407
+ def execute_query(
408
+ self, query: str, params: tuple = None, log_action: bool = True
409
+ ) -> List[tuple]:
410
  """Execute a SQL query with comprehensive error handling"""
411
  try:
412
  result = self._execute_query_internal(query, params)
413
+
414
  # Log the query execution (but avoid recursion)
415
  if log_action and not self._is_logging:
416
  try:
417
  self._is_logging = True
418
+ self._execute_query_internal(
419
+ """
420
  INSERT INTO system_logs (log_type, log_message, table_name, record_id, action)
421
  VALUES (?, ?, ?, ?, ?)
422
+ """,
423
+ (
424
+ "QUERY_EXECUTION",
425
+ f"Executed query: {query[:100]}...",
426
+ None,
427
+ None,
428
+ "EXECUTE",
429
+ ),
430
+ )
431
  except Exception as e:
432
  logger.error(f"Error logging system action: {e}")
433
  finally:
434
  self._is_logging = False
435
+
436
  return result
437
  except Exception as e:
438
  logger.error(f"Error in execute_query: {e}")
439
  return [] # Return empty list instead of raising exception
440
+
441
+ def get_dataframe(
442
+ self, table_name: str = None, query: str = None, params: tuple = None
443
+ ) -> pd.DataFrame:
444
  """Get table data as DataFrame with flexible query support"""
445
  conn = self.get_connection()
446
  try:
 
450
  df = pd.read_sql_query(f"SELECT * FROM {table_name}", conn)
451
  return df
452
  except Exception as e:
453
+ logger.error(
454
+ f"Error getting DataFrame for {table_name if table_name else 'query'}: {e}"
455
+ )
456
  # Return empty DataFrame with proper structure
457
  return pd.DataFrame()
458
  finally:
459
  conn.close()
460
+
461
+ def add_customer(
462
+ self,
463
+ name: str,
464
+ mobile: str = "",
465
+ village: str = "",
466
+ taluka: str = "",
467
+ district: str = "",
468
+ customer_code: str = None,
469
+ ) -> int:
470
  """Add a new customer with duplicate handling"""
471
+
472
  # Generate customer code if not provided
473
  if not customer_code:
474
  customer_code = f"CUST{datetime.now().strftime('%Y%m%d%H%M%S')}{random.randint(100, 999)}"
475
+
476
  try:
477
  # Check if customer already exists (by mobile or similar name+village)
478
  existing_customer = self.execute_query(
479
+ "SELECT customer_id FROM customers WHERE mobile = ? OR (name = ? AND village = ?)",
480
  (mobile, name, village),
481
+ log_action=False,
482
  )
483
+
484
  if existing_customer:
485
  # Customer already exists, return existing ID
486
  return existing_customer[0][0]
487
+
488
  # If customer_code already exists, generate a new one
489
  max_attempts = 5
490
  for attempt in range(max_attempts):
491
  try:
492
+ result = self.execute_query(
493
+ """
494
  INSERT INTO customers (customer_code, name, mobile, village, taluka, district)
495
  VALUES (?, ?, ?, ?, ?, ?)
496
+ """,
497
+ (customer_code, name, mobile, village, taluka, district),
498
+ log_action=False,
499
+ )
500
  break
501
  except sqlite3.IntegrityError as e:
502
+ if (
503
+ "UNIQUE constraint failed: customers.customer_code" in str(e)
504
+ and attempt < max_attempts - 1
505
+ ):
506
  # Generate new unique customer code
507
  customer_code = f"CUST{datetime.now().strftime('%Y%m%d%H%M%S')}{random.randint(1000, 9999)}"
508
  continue
509
  else:
510
  raise e
511
+
512
  # Get the inserted customer_id
513
+ customer_id = self.execute_query(
514
+ "SELECT last_insert_rowid()", log_action=False
515
+ )[0][0]
516
+
517
+ self.log_system_action(
518
+ "CUSTOMER_ADD",
519
+ f"Added customer: {name}",
520
+ "customers",
521
+ customer_id,
522
+ "INSERT",
523
+ )
524
+
525
  return customer_id
526
  except Exception as e:
527
  logger.error(f"Error adding customer: {e}")
528
  # Return a fallback - this won't be in database but prevents crashes
529
  return -1
530
+
531
+ def add_distributor(
532
+ self,
533
+ name: str,
534
+ village: str = "",
535
+ taluka: str = "",
536
+ district: str = "",
537
+ mantri_name: str = "",
538
+ mantri_mobile: str = "",
539
+ sabhasad_count: int = 0,
540
+ contact_in_group: int = 0,
541
+ status: str = "Active",
542
+ ) -> int:
543
  """Add a new distributor with duplicate handling"""
544
+
545
  try:
546
  # Check if distributor already exists
547
  existing_distributor = self.execute_query(
548
+ "SELECT distributor_id FROM distributors WHERE name = ? AND village = ? AND taluka = ?",
549
  (name, village, taluka),
550
+ log_action=False,
551
  )
552
+
553
  if existing_distributor:
554
  # Distributor already exists, return existing ID
555
  return existing_distributor[0][0]
556
+
557
  # Insert new distributor
558
+ self.execute_query(
559
+ """
560
+ INSERT INTO distributors (name, village, taluka, district, mantri_name, mantri_mobile,
561
  sabhasad_count, contact_in_group, status)
562
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
563
+ """,
564
+ (
565
+ name,
566
+ village,
567
+ taluka,
568
+ district,
569
+ mantri_name,
570
+ mantri_mobile,
571
+ sabhasad_count,
572
+ contact_in_group,
573
+ status,
574
+ ),
575
+ log_action=False,
576
+ )
577
+
578
  # Get the inserted distributor_id
579
+ distributor_id = self.execute_query(
580
+ "SELECT last_insert_rowid()", log_action=False
581
+ )[0][0]
582
+
583
+ self.log_system_action(
584
+ "DISTRIBUTOR_ADD",
585
+ f"Added distributor: {name}",
586
+ "distributors",
587
+ distributor_id,
588
+ "INSERT",
589
+ )
590
+
591
  return distributor_id
592
+
593
  except Exception as e:
594
  logger.error(f"Error adding distributor: {e}")
595
  return -1
596
+
597
  def get_distributor_by_location(self, village: str, taluka: str) -> Optional[Dict]:
598
  """Get distributor by village and taluka"""
599
  try:
600
  result = self.execute_query(
601
+ "SELECT * FROM distributors WHERE village = ? AND taluka = ?",
602
  (village, taluka),
603
+ log_action=False,
604
  )
605
  if result:
606
  return dict(result[0])
 
613
  """Check if distributor already exists"""
614
  try:
615
  result = self.execute_query(
616
+ "SELECT distributor_id FROM distributors WHERE name = ? AND village = ? AND taluka = ?",
617
  (name, village, taluka),
618
+ log_action=False,
619
  )
620
  return len(result) > 0
621
  except Exception as e:
622
  logger.error(f"Error checking distributor existence: {e}")
623
  return False
624
+
625
  # In your DatabaseManager class in database.py, replace the generate_invoice_number method:
626
 
627
  def generate_invoice_number(self):
 
629
  try:
630
  # Get current date components
631
  now = datetime.now()
632
+ month = now.strftime("%m") # Two-digit month
633
+ year = now.strftime("%y") # Two-digit year
634
+
635
  # Get the last invoice number for this month-year
636
  result = self.execute_query(
637
  "SELECT invoice_no FROM sales WHERE invoice_no LIKE ? ORDER BY sale_id DESC LIMIT 1",
638
  (f"INVCL{month}{year}%",),
639
+ log_action=False,
640
  )
641
+
642
  if result:
643
  last_invoice = result[0][0]
644
  # Extract serial number and increment
 
652
  else:
653
  # First invoice of the month-year
654
  new_serial = 1
655
+
656
  # Format: INVCL + month(2) + year(2) + serial(3 digits)
657
  return f"INVCL{month}{year}{new_serial:03d}"
658
+
659
  except Exception as e:
660
  logger.error(f"Error generating invoice number: {e}")
661
  # Fallback: timestamp-based
 
666
  """Generate automatic invoice number in format: PREFIXmmyyserial"""
667
  try:
668
  now = datetime.now()
669
+ month = now.strftime("%m")
670
+ year = now.strftime("%y")
671
+
672
  result = self.execute_query(
673
  "SELECT invoice_no FROM sales WHERE invoice_no LIKE ? ORDER BY sale_id DESC LIMIT 1",
674
  (f"{prefix}{month}{year}%",),
675
+ log_action=False,
676
  )
677
+
678
  if result:
679
  last_invoice = result[0][0]
680
  try:
681
  # Remove prefix and date part, get serial
682
+ serial_part = last_invoice[
683
+ len(prefix) + 4 :
684
+ ] # prefix + 4 digits (mmyy)
685
  last_serial = int(serial_part)
686
  new_serial = last_serial + 1
687
  except ValueError:
688
  new_serial = 1
689
  else:
690
  new_serial = 1
691
+
692
  return f"{prefix}{month}{year}{new_serial:03d}"
693
+
694
  except Exception as e:
695
  logger.error(f"Error generating invoice number: {e}")
696
  return f"{prefix}{int(datetime.now().timestamp())}"
697
+
698
  # Add to your DatabaseManager class in database.py
699
 
700
+ def add_sale(
701
+ self,
702
+ invoice_no: str,
703
+ customer_id: int,
704
+ sale_date,
705
+ items: List[Dict],
706
+ payments: List[Dict] = None,
707
+ notes: str = "",
708
+ ) -> int:
709
  """Add a new sale with items and optional payments - ENHANCED"""
710
  conn = self.get_connection()
711
  try:
712
  cursor = conn.cursor()
713
+
714
  # Calculate total amount and liters
715
+ total_amount = sum(item["quantity"] * item["rate"] for item in items)
716
+ total_liters = sum(item.get("liters", 0) for item in items)
717
+
718
+ print(
719
+ f"🔧 DEBUG: Creating sale - Invoice: {invoice_no}, Customer: {customer_id}, Total: {total_amount}"
720
+ ) # DEBUG
721
+
722
  # Add sale record
723
+ cursor.execute(
724
+ """
725
  INSERT INTO sales (invoice_no, customer_id, sale_date, total_amount, total_liters, notes)
726
  VALUES (?, ?, ?, ?, ?, ?)
727
+ """,
728
+ (invoice_no, customer_id, sale_date, total_amount, total_liters, notes),
729
+ )
730
+
731
  # Get the sale ID
732
  sale_id = cursor.lastrowid
733
  print(f"🔧 DEBUG: Sale created with ID: {sale_id}") # DEBUG
734
+
735
  # Add sale items
736
  for item in items:
737
+ amount = item["quantity"] * item["rate"]
738
+ print(
739
+ f"🔧 DEBUG: Adding item - Product: {item['product_id']}, Qty: {item['quantity']}, Rate: {item['rate']}"
740
+ ) # DEBUG
741
+
742
+ cursor.execute(
743
+ """
744
  INSERT INTO sale_items (sale_id, product_id, quantity, rate, amount)
745
  VALUES (?, ?, ?, ?, ?)
746
+ """,
747
+ (
748
+ sale_id,
749
+ item["product_id"],
750
+ item["quantity"],
751
+ item["rate"],
752
+ amount,
753
+ ),
754
+ )
755
+
756
  # Add payments if provided
757
  if payments:
758
  for payment in payments:
759
+ cursor.execute(
760
+ """
761
  INSERT INTO payments (sale_id, payment_date, payment_method, amount, rrn, reference)
762
  VALUES (?, ?, ?, ?, ?, ?)
763
+ """,
764
+ (
765
+ sale_id,
766
+ payment["payment_date"],
767
+ payment["method"],
768
+ payment["amount"],
769
+ payment.get("rrn", ""),
770
+ payment.get("reference", ""),
771
+ ),
772
+ )
773
+
774
  conn.commit()
775
+
776
  # Update payment status
777
  self._update_payment_status(sale_id)
778
+
779
  print(f"🔧 DEBUG: Sale {sale_id} completed successfully") # DEBUG
780
  return sale_id
781
+
782
  except Exception as e:
783
  conn.rollback()
784
  logger.error(f"Error adding sale: {e}")
 
786
  raise
787
  finally:
788
  conn.close()
789
+
790
  def _update_payment_status(self, sale_id: int):
791
  """Update payment status for a sale"""
792
  conn = self.get_connection()
793
  try:
794
  # Get total paid amount
795
  cursor = conn.cursor()
796
+ cursor.execute(
797
+ "SELECT COALESCE(SUM(amount), 0) FROM payments WHERE sale_id = ?",
798
+ (sale_id,),
799
+ )
800
  total_paid = cursor.fetchone()[0]
801
+
802
  # Get sale total
803
+ cursor.execute(
804
+ "SELECT total_amount FROM sales WHERE sale_id = ?", (sale_id,)
805
+ )
806
  sale_total = cursor.fetchone()[0]
807
+
808
  # Determine payment status
809
  if total_paid >= sale_total:
810
+ status = "Paid"
811
  elif total_paid > 0:
812
+ status = "Partial"
813
  else:
814
+ status = "Pending"
815
+
816
  # Update status
817
+ cursor.execute(
818
+ "UPDATE sales SET payment_status = ? WHERE sale_id = ?",
819
+ (status, sale_id),
820
+ )
821
  conn.commit()
822
+
823
  except Exception as e:
824
  logger.error(f"Error updating payment status: {e}")
825
  finally:
826
  conn.close()
827
+
828
  def get_pending_payments(self) -> pd.DataFrame:
829
  """Get all pending payments with customer details"""
830
+ return self.get_dataframe(
831
+ "sales",
832
+ """
833
+ SELECT s.sale_id, s.invoice_no, s.sale_date, c.name as customer_name,
834
  c.mobile, c.village, s.total_amount,
835
  (s.total_amount - COALESCE(SUM(p.amount), 0)) as pending_amount,
836
  COALESCE(SUM(p.amount), 0) as paid_amount
 
841
  GROUP BY s.sale_id
842
  HAVING pending_amount > 0
843
  ORDER BY s.sale_date DESC
844
+ """,
845
+ )
846
+
847
  def get_demo_conversions(self) -> pd.DataFrame:
848
  """Get demo conversion statistics with details"""
849
+ return self.get_dataframe(
850
+ "demos",
851
+ """
852
+ SELECT d.*, c.name as customer_name, p.product_name,
853
  dist.name as distributor_name, c.village, c.taluka,
854
  CASE WHEN d.conversion_status = 'Converted' THEN 1 ELSE 0 END as converted
855
  FROM demos d
 
857
  LEFT JOIN products p ON d.product_id = p.product_id
858
  LEFT JOIN distributors dist ON d.distributor_id = dist.distributor_id
859
  ORDER BY d.demo_date DESC
860
+ """,
861
+ )
862
+
863
  def get_sales_analytics(self, start_date: str = None, end_date: str = None) -> Dict:
864
  """Get comprehensive sales analytics"""
865
  if not start_date:
866
+ start_date = (datetime.now() - timedelta(days=30)).strftime("%Y-%m-%d")
867
  if not end_date:
868
+ end_date = datetime.now().strftime("%Y-%m-%d")
869
+
870
+ query = """
871
+ SELECT
872
  COUNT(*) as total_sales,
873
  SUM(total_amount) as total_revenue,
874
  AVG(total_amount) as avg_sale_value,
875
  COUNT(DISTINCT customer_id) as unique_customers,
876
  SUM(CASE WHEN payment_status = 'Paid' THEN 1 ELSE 0 END) as completed_payments,
877
  SUM(CASE WHEN payment_status IN ('Pending', 'Partial') THEN 1 ELSE 0 END) as pending_payments
878
+ FROM sales
879
  WHERE sale_date BETWEEN ? AND ?
880
+ """
881
+
882
  result = self.execute_query(query, (start_date, end_date), log_action=False)
883
+
884
  if result:
885
  row = result[0]
886
  return {
887
+ "total_sales": row[0] or 0,
888
+ "total_revenue": row[1] or 0,
889
+ "avg_sale_value": row[2] or 0,
890
+ "unique_customers": row[3] or 0,
891
+ "completed_payments": row[4] or 0,
892
+ "pending_payments": row[5] or 0,
893
  }
894
  return {}
895
+
896
+ def log_system_action(
897
+ self,
898
+ log_type: str,
899
+ message: str,
900
+ table_name: str = None,
901
+ record_id: int = None,
902
+ action: str = None,
903
+ ):
904
  """Log system actions for audit trail - without recursion"""
905
  if self._is_logging:
906
  return # Prevent recursion
907
+
908
  try:
909
  self._is_logging = True
910
+ self._execute_query_internal(
911
+ """
912
  INSERT INTO system_logs (log_type, log_message, table_name, record_id, action)
913
  VALUES (?, ?, ?, ?, ?)
914
+ """,
915
+ (log_type, message, table_name, record_id, action),
916
+ )
917
  except Exception as e:
918
  logger.error(f"Error logging system action: {e}")
919
  finally:
920
  self._is_logging = False
921
+
922
+ def create_rollback_point(
923
+ self, table_name: str, record_id: int, old_data: str, new_data: str, action: str
924
+ ):
925
  """Create a rollback point for data changes"""
926
  try:
927
+ self.execute_query(
928
+ """
929
  INSERT INTO rollback_logs (table_name, record_id, old_data, new_data, action)
930
  VALUES (?, ?, ?, ?, ?)
931
+ """,
932
+ (table_name, record_id, old_data, new_data, action),
933
+ log_action=False,
934
+ )
935
  except Exception as e:
936
  logger.error(f"Error creating rollback point: {e}")
937
+
938
  def get_recent_activity(self, limit: int = 10) -> pd.DataFrame:
939
  """Get recent system activity"""
940
+ return self.get_dataframe(
941
+ "system_logs",
942
+ f"""
943
  SELECT log_type, log_message, table_name, record_id, action, created_date
944
+ FROM system_logs
945
+ ORDER BY created_date DESC
946
  LIMIT {limit}
947
+ """,
948
+ )
949
+
950
  def backup_database(self, backup_path: str = None):
951
  """Create a database backup"""
952
  if not backup_path:
953
  backup_path = f"backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}.db"
954
+
955
  try:
956
  conn = self.get_connection()
957
  backup_conn = sqlite3.connect(backup_path)
958
+
959
  with backup_conn:
960
  conn.backup(backup_conn)
961
+
962
  conn.close()
963
  backup_conn.close()
964
+
965
  logger.info(f"Database backup created: {backup_path}")
966
  return backup_path
967
+
968
  except Exception as e:
969
  logger.error(f"Error creating database backup: {e}")
970
  return None
971
+
972
  def get_village_wise_sales(self) -> pd.DataFrame:
973
  """Get sales data grouped by village"""
974
+ return self.get_dataframe(
975
+ "sales",
976
+ """
977
+ SELECT c.village, COUNT(s.sale_id) as total_sales,
978
  SUM(s.total_amount) as total_revenue,
979
  AVG(s.total_amount) as avg_sale_value,
980
  COUNT(DISTINCT s.customer_id) as unique_customers
 
983
  WHERE c.village IS NOT NULL AND c.village != ''
984
  GROUP BY c.village
985
  ORDER BY total_revenue DESC
986
+ """,
987
+ )
988
+
989
  def get_product_performance(self) -> pd.DataFrame:
990
  """Get product performance analytics"""
991
+ return self.get_dataframe(
992
+ "sale_items",
993
+ """
994
  SELECT p.product_name, COUNT(si.item_id) as times_sold,
995
+ SUM(si.quantity) as total_quantity,
996
  SUM(si.amount) as total_revenue,
997
  AVG(si.rate) as avg_rate
998
  FROM sale_items si
999
  JOIN products p ON si.product_id = p.product_id
1000
  GROUP BY p.product_id, p.product_name
1001
  ORDER BY total_revenue DESC
1002
+ """,
1003
+ )
1004
+
1005
  def get_upcoming_follow_ups(self) -> pd.DataFrame:
1006
  """Get upcoming follow-ups"""
1007
+ return self.get_dataframe(
1008
+ "follow_ups",
1009
+ """
1010
+ SELECT f.*, c.name as customer_name, c.mobile,
1011
  d.name as distributor_name, dm.demo_date
1012
  FROM follow_ups f
1013
  LEFT JOIN customers c ON f.customer_id = c.customer_id
1014
  LEFT JOIN distributors d ON f.distributor_id = d.distributor_id
1015
  LEFT JOIN demos dm ON f.demo_id = dm.demo_id
1016
+ WHERE f.follow_up_date >= date('now')
1017
  AND f.status = 'Pending'
1018
  ORDER BY f.follow_up_date ASC
1019
  LIMIT 20
1020
+ """,
1021
+ )
1022
+
1023
  def get_whatsapp_logs(self, customer_id: int = None) -> pd.DataFrame:
1024
  """Get WhatsApp communication logs"""
1025
  if customer_id:
1026
+ return self.get_dataframe(
1027
+ "whatsapp_logs",
1028
+ """
1029
  SELECT w.*, c.name as customer_name, c.mobile
1030
  FROM whatsapp_logs w
1031
  LEFT JOIN customers c ON w.customer_id = c.customer_id
1032
  WHERE w.customer_id = ?
1033
  ORDER BY w.sent_date DESC
1034
+ """,
1035
+ (customer_id,),
1036
+ )
1037
  else:
1038
+ return self.get_dataframe(
1039
+ "whatsapp_logs",
1040
+ """
1041
  SELECT w.*, c.name as customer_name, c.mobile
1042
  FROM whatsapp_logs w
1043
  LEFT JOIN customers c ON w.customer_id = c.customer_id
1044
  ORDER BY w.sent_date DESC
1045
  LIMIT 50
1046
+ """,
1047
+ )
1048
+
1049
  def cleanup_old_data(self, days: int = 365):
1050
  """Clean up old data (logs, etc.) older than specified days"""
1051
  try:
1052
+ cutoff_date = (datetime.now() - timedelta(days=days)).strftime("%Y-%m-%d")
1053
+
1054
  # Clean system logs
1055
+ self.execute_query(
1056
+ "DELETE FROM system_logs WHERE created_date < ?",
1057
+ (cutoff_date,),
1058
+ log_action=False,
1059
+ )
1060
+
1061
  # Clean rollback logs
1062
+ self.execute_query(
1063
+ "DELETE FROM rollback_logs WHERE rollback_date < ?",
1064
+ (cutoff_date,),
1065
+ log_action=False,
1066
+ )
1067
+
1068
  logger.info(f"Cleaned up data older than {days} days")
1069
+
1070
  except Exception as e:
1071
  logger.error(f"Error cleaning up old data: {e}")
1072
 
1073
+
1074
  # Utility function to check database health
1075
  def check_database_health(db_path: str = "sales_management.db") -> Dict:
1076
  """Check database health and statistics"""
1077
  try:
1078
  db = DatabaseManager(db_path)
1079
+
1080
  # Get table counts
1081
+ tables = ["customers", "sales", "distributors", "demos", "payments", "products"]
1082
  counts = {}
1083
+
1084
  for table in tables:
1085
  result = db.execute_query(f"SELECT COUNT(*) FROM {table}", log_action=False)
1086
  counts[table] = result[0][0] if result else 0
1087
+
1088
  # Get database size
1089
  db_size = os.path.getsize(db_path) if os.path.exists(db_path) else 0
1090
+
1091
  return {
1092
+ "status": "healthy",
1093
+ "table_counts": counts,
1094
+ "database_size_mb": round(db_size / (1024 * 1024), 2),
1095
+ "last_backup": "N/A", # You can implement backup tracking
1096
+ "integrity_check": "passed", # You can add actual integrity checks
1097
  }
1098
+
1099
  except Exception as e:
1100
  return {
1101
+ "status": "error",
1102
+ "error": str(e),
1103
+ "table_counts": {},
1104
+ "database_size_mb": 0,
1105
+ "integrity_check": "failed",
1106
+ }