Spaces:
Sleeping
Sleeping
create_wb_kpi_cards
Browse files- app.py +4 -4
- 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 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 453 |
|
| 454 |
-
# Column mapping based on
|
| 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 |
-
'
|
|
|
|
| 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', '
|
| 500 |
-
'
|
| 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 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
#
|
| 536 |
-
|
| 537 |
-
|
| 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 |
|