feat: MercadoLibre integration + inventory bulk publish + WhatsApp bridge fixes
- Add MercadoLibre OAuth, listings, orders, webhooks and category search - New marketplace_external_bp.py, meli_service.py, marketplace_external_service.py - New marketplace_external.html/js with ML management UI - Inventory: bulk publish to ML with category autocomplete, listing type and shipping selectors - Inventory: new .btn--meli styles, select/label CSS fixes - WhatsApp bridge: rate limiting, 440/515/408 error handling, stale watchdog - DB migration v3.4_meli_integration.sql for marketplace_listings, orders, sync_queue - Add Celery tasks for ML sync and webhook processing - Sidebar: MercadoLibre navigation link
This commit is contained in:
82
scripts/sync_estrada_to_marketplace.py
Normal file
82
scripts/sync_estrada_to_marketplace.py
Normal file
@@ -0,0 +1,82 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Sync Strada inventory to marketplace warehouse_inventory.
|
||||
Matches by OEM part_number and cross-references.
|
||||
"""
|
||||
import os, sys, io
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'pos'))
|
||||
from tenant_db import get_tenant_conn, get_master_conn
|
||||
|
||||
BODEGA_ID = 7
|
||||
USER_ID = 1 # admin user in master DB
|
||||
CURRENCY = 'MXN'
|
||||
|
||||
def main():
|
||||
tenant_conn = get_tenant_conn(28)
|
||||
tenant_cur = tenant_conn.cursor()
|
||||
|
||||
# Get active inventory with stock
|
||||
tenant_cur.execute("""
|
||||
SELECT i.id, i.part_number, i.price_1, i.cost, COALESCE(iss.stock, 0) as stock
|
||||
FROM inventory i
|
||||
LEFT JOIN inventory_stock_summary iss ON iss.inventory_id = i.id
|
||||
WHERE i.is_active = true
|
||||
""")
|
||||
items = {r[0]: {'pn': r[1], 'price': r[2], 'cost': r[3], 'stock': r[4]} for r in tenant_cur.fetchall()}
|
||||
print(f"Active inventory: {len(items)}")
|
||||
|
||||
tenant_cur.close()
|
||||
tenant_conn.close()
|
||||
|
||||
# Get catalog mappings from master
|
||||
master = get_master_conn()
|
||||
master_cur = master.cursor()
|
||||
|
||||
master_cur.execute("SELECT id_part, oem_part_number FROM parts WHERE oem_part_number IS NOT NULL")
|
||||
oem_map = {r[1]: r[0] for r in master_cur.fetchall()}
|
||||
|
||||
master_cur.execute("SELECT part_id, cross_reference_number FROM part_cross_references")
|
||||
cross_map = {r[1]: r[0] for r in master_cur.fetchall()}
|
||||
|
||||
print(f"OEM parts: {len(oem_map)}, Cross-references: {len(cross_map)}")
|
||||
|
||||
# Build match list
|
||||
matched = []
|
||||
seen_parts = set()
|
||||
for item_id, data in items.items():
|
||||
pn = data['pn']
|
||||
raw = pn.split('-', 1)[1] if '-' in pn else pn
|
||||
|
||||
part_id = oem_map.get(raw) or oem_map.get(pn) or cross_map.get(raw) or cross_map.get(pn)
|
||||
if part_id and part_id not in seen_parts:
|
||||
seen_parts.add(part_id)
|
||||
price = data['price'] or data['cost'] or 0
|
||||
stock = data['stock'] or 0
|
||||
matched.append((USER_ID, part_id, price, stock, 1, 'Principal', BODEGA_ID, CURRENCY))
|
||||
|
||||
print(f"Matched items: {len(matched)}")
|
||||
|
||||
if not matched:
|
||||
print("Nothing to sync")
|
||||
return
|
||||
|
||||
# Bulk insert via COPY
|
||||
csv_buffer = io.StringIO()
|
||||
for row in matched:
|
||||
csv_buffer.write(','.join(str(c) for c in row) + '\n')
|
||||
csv_buffer.seek(0)
|
||||
|
||||
master_cur.copy_expert(
|
||||
"""COPY warehouse_inventory (user_id, part_id, price, stock_quantity, min_order_quantity, warehouse_location, bodega_id, currency)
|
||||
FROM STDIN WITH (FORMAT CSV)""",
|
||||
csv_buffer
|
||||
)
|
||||
|
||||
master.commit()
|
||||
master_cur.close()
|
||||
master.close()
|
||||
|
||||
print(f"Synced {len(matched)} items to warehouse_inventory")
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user