bakyt92 commited on
Commit
1572a5e
Β·
1 Parent(s): e9be307

create_wb_kpi_cards

Browse files
Files changed (2) hide show
  1. app.py +4 -4
  2. wildberries_client.py +43 -12
app.py CHANGED
@@ -269,10 +269,10 @@ def create_interface():
269
  Monitor your marketplace performance and predict inventory needs with AI-powered analytics.
270
 
271
  **Features:**
272
- - πŸ“Š Sales performance analysis
273
- - πŸ“¦ Inventory forecasting
274
- - ⚠️ Stockout risk alerts
275
- - πŸ“ˆ Interactive dashboards
276
  """)
277
 
278
  # API Token Configuration
 
269
  Monitor your marketplace performance and predict inventory needs with AI-powered analytics.
270
 
271
  **Features:**
272
+ - πŸ“Š Sales performance analysis with automatic return detection
273
+ - πŸ“¦ Inventory forecasting with AI-powered predictions
274
+ - ⚠️ Stockout risk alerts and notifications
275
+ - πŸ“ˆ Interactive dashboards with commission analysis
276
  """)
277
 
278
  # API Token Configuration
wildberries_client.py CHANGED
@@ -449,9 +449,18 @@ class WildberriesAPI:
449
  raise WildberriesAPIError(f"Failed to fetch orders data: {str(e)}")
450
 
451
  def _process_sales_data(self, df: pd.DataFrame) -> pd.DataFrame:
452
- """Process and clean sales data from API response (v1 sales endpoint)"""
 
 
 
 
 
 
 
 
 
453
 
454
- # Column mapping based on actual sales API response structure
455
  column_mapping = {
456
  'date': 'sale_date',
457
  'lastChangeDate': 'last_change_date',
@@ -470,12 +479,12 @@ class WildberriesAPI:
470
  'incomeID': 'income_id',
471
  'isSupply': 'is_supply',
472
  'isRealization': 'is_realization',
473
- 'totalPrice': 'total_price', # Already total price per item
 
474
  'discountPercent': 'discount_percent',
475
  'spp': 'spp_discount',
476
  'paymentSaleAmount': 'payment_sale_amount',
477
  'forPay': 'amount_for_pay', # What seller receives
478
- 'finishedPrice': 'finished_price',
479
  'priceWithDisc': 'price_with_discount',
480
  'saleID': 'sale_id',
481
  'sticker': 'sticker',
@@ -496,8 +505,8 @@ class WildberriesAPI:
496
 
497
  # Convert numeric columns
498
  numeric_columns = [
499
- 'total_price', 'discount_percent', 'spp_discount', 'payment_sale_amount',
500
- 'amount_for_pay', 'finished_price', 'price_with_discount', 'income_id'
501
  ]
502
  for col in numeric_columns:
503
  if col in df.columns:
@@ -512,13 +521,36 @@ class WildberriesAPI:
512
  else:
513
  df['product_name'] = 'Unknown Product'
514
 
515
- # Add quantity (each row represents 1 item sale/return)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
516
  df['quantity'] = 1
 
 
 
 
 
 
 
 
 
517
 
518
  # Calculate commission (difference between total_price and amount_for_pay)
 
519
  if 'total_price' in df.columns and 'amount_for_pay' in df.columns:
520
  df['sales_commission'] = df['total_price'] - df['amount_for_pay']
521
- # Handle negative commissions (returns)
522
  df['sales_commission'] = df['sales_commission'].fillna(0)
523
 
524
  # Add sale_amount for compatibility (use amount_for_pay as seller's net amount)
@@ -532,10 +564,9 @@ class WildberriesAPI:
532
  if 'current_stock' not in df.columns:
533
  df['current_stock'] = 0
534
 
535
- # Filter out negative total_price (returns) if needed for analysis
536
- # Note: Keep returns for complete data, but mark them
537
- if 'total_price' in df.columns:
538
- df['is_return'] = df['total_price'] < 0
539
 
540
  logger.info(f"Processed {len(df)} sales records")
541
 
 
449
  raise WildberriesAPIError(f"Failed to fetch orders data: {str(e)}")
450
 
451
  def _process_sales_data(self, df: pd.DataFrame) -> pd.DataFrame:
452
+ """
453
+ Process and clean sales data from API response (v1 sales endpoint)
454
+
455
+ Handles all 28 fields from the official Wildberries Sales API:
456
+ - Geographic: warehouseName, warehouseType, countryName, oblastOkrugName, regionName
457
+ - Product: supplierArticle, nmId, barcode, category, subject, brand, techSize
458
+ - Financial: finishedPrice (used as total_price), totalPrice (original_price), discountPercent, spp, paymentSaleAmount, forPay, priceWithDisc
459
+ - Operational: incomeID, isSupply, isRealization, saleID, sticker, gNumber, srid
460
+ - Temporal: date, lastChangeDate
461
+ """
462
 
463
+ # Column mapping based on official Wildberries Sales API schema (v1)
464
  column_mapping = {
465
  'date': 'sale_date',
466
  'lastChangeDate': 'last_change_date',
 
479
  'incomeID': 'income_id',
480
  'isSupply': 'is_supply',
481
  'isRealization': 'is_realization',
482
+ 'finishedPrice': 'total_price', # Final price with all discounts applied
483
+ 'totalPrice': 'original_price', # Original price without discounts
484
  'discountPercent': 'discount_percent',
485
  'spp': 'spp_discount',
486
  'paymentSaleAmount': 'payment_sale_amount',
487
  'forPay': 'amount_for_pay', # What seller receives
 
488
  'priceWithDisc': 'price_with_discount',
489
  'saleID': 'sale_id',
490
  'sticker': 'sticker',
 
505
 
506
  # Convert numeric columns
507
  numeric_columns = [
508
+ 'product_id', 'total_price', 'original_price', 'discount_percent', 'spp_discount',
509
+ 'payment_sale_amount', 'amount_for_pay', 'price_with_discount', 'income_id'
510
  ]
511
  for col in numeric_columns:
512
  if col in df.columns:
 
521
  else:
522
  df['product_name'] = 'Unknown Product'
523
 
524
+ # Identify returns using saleID prefix (R********* = return, S********* = sale)
525
+ if 'sale_id' in df.columns:
526
+ df['is_return'] = df['sale_id'].astype(str).str.startswith('R')
527
+ return_count = df['is_return'].sum()
528
+ sale_count = (~df['is_return']).sum()
529
+ logger.info(f"Identified {return_count} returns and {sale_count} sales based on saleID")
530
+ else:
531
+ # Fallback to negative total_price detection if saleID not available
532
+ if 'total_price' in df.columns:
533
+ df['is_return'] = df['total_price'] < 0
534
+ logger.warning("saleID not available, using total_price < 0 for return detection")
535
+ else:
536
+ df['is_return'] = False
537
+
538
+ # Add quantity with proper sign (negative for returns)
539
  df['quantity'] = 1
540
+ df.loc[df['is_return'], 'quantity'] = -1 # Returns have negative quantity
541
+
542
+ # Apply return logic to financial fields (make returns negative for accounting)
543
+ return_mask = df['is_return']
544
+ financial_fields = ['total_price', 'original_price', 'amount_for_pay', 'payment_sale_amount', 'price_with_discount']
545
+ for field in financial_fields:
546
+ if field in df.columns:
547
+ # Make returns negative if they're not already (some APIs might already send negative values)
548
+ df.loc[return_mask & (df[field] > 0), field] = -df.loc[return_mask & (df[field] > 0), field]
549
 
550
  # Calculate commission (difference between total_price and amount_for_pay)
551
+ # This will be negative for returns, which is correct for accounting
552
  if 'total_price' in df.columns and 'amount_for_pay' in df.columns:
553
  df['sales_commission'] = df['total_price'] - df['amount_for_pay']
 
554
  df['sales_commission'] = df['sales_commission'].fillna(0)
555
 
556
  # Add sale_amount for compatibility (use amount_for_pay as seller's net amount)
 
564
  if 'current_stock' not in df.columns:
565
  df['current_stock'] = 0
566
 
567
+ # Add transaction type for clarity
568
+ if 'transaction_type' not in df.columns:
569
+ df['transaction_type'] = df['is_return'].map({True: 'Return', False: 'Sale'})
 
570
 
571
  logger.info(f"Processed {len(df)} sales records")
572