Files
Autoparts-DB/scripts/import_estrada_st01.py
consultoria-as a236187f3a 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
2026-05-26 04:24:07 +00:00

311 lines
9.6 KiB
Python

#!/usr/bin/env python3
"""
Nexus Autoparts — Import Strada TODO.st01 Inventory
Parses fixed-width report and imports into tenant DB using PostgreSQL COPY.
Usage:
python3 import_estrada_st01.py --tenant=28 --file=/tmp/todo_st01.txt --branch=1
"""
import argparse
import csv
import io
import os
import sys
import tempfile
import time
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'pos'))
import psycopg2
from tenant_db import get_tenant_conn
# Margins applied to Cost A
MARGIN_PRICE_1 = 1.35 # mostrador / retail
MARGIN_PRICE_2 = 1.25 # taller / workshop
MARGIN_PRICE_3 = 1.15 # mayoreo / wholesale
def parse_number(val, default=0):
"""Parse a string that might be '$1,234.56' or '.000' or empty."""
if not val:
return default
cleaned = str(val).replace('$', '').replace(',', '').replace(' ', '').strip()
if cleaned == '.':
return default
try:
return float(cleaned)
except ValueError:
return default
def parse_st01_line(line):
"""Parse a single line from TODO.st01 fixed-width format."""
line = line.rstrip('\n')
# Skip short lines
if len(line) < 80:
return None
# Skip header/footer lines
if 'Page:' in line or 'General Inventory' in line or 'STRADA AUTO' in line:
return None
if 'Tienda:' in line and 'Page:' in line:
return None
if line.strip().startswith('Line Part Number'):
return None
if line.strip().startswith('----'):
return None
# Fixed-width columns based on header:
# Line Part Number Description Addl description Pcode Instk Min 1 Max 1 Cost A Cost B codes[1]
# ---- ---------------- ---------------- ---------------- ----- ------- ------ ------ ---------- ---------- --------
marca = line[0:5].strip()
part_number_raw = line[5:21].strip()
description = line[22:39].strip()
addl_desc = line[40:56].strip()
pcode = line[56:61].strip()
instk = line[62:70].strip()
min1 = line[71:77].strip()
max1 = line[78:84].strip()
cost_a = line[85:95].strip()
cost_b = line[96:106].strip()
codes = line[107:].strip() if len(line) > 107 else ''
if not part_number_raw:
return None
# Build composite part number: MARCA-NUMERO
# Some marcas are empty (fallback to pcode or generic)
marca_safe = marca if marca else 'GEN'
part_number = f"{marca_safe}-{part_number_raw}"
# Build name from description + additional description
name = description
if addl_desc and addl_desc != '?':
name = f"{description} {addl_desc}".strip()
if not name:
return None
cost = parse_number(cost_a, 0)
stock = int(parse_number(instk, 0))
min_stock = int(parse_number(min1, 0))
max_stock = int(parse_number(max1, 0))
# Calculate prices with margins
price_1 = round(cost * MARGIN_PRICE_1, 2) if cost > 0 else 0
price_2 = round(cost * MARGIN_PRICE_2, 2) if cost > 0 else 0
price_3 = round(cost * MARGIN_PRICE_3, 2) if cost > 0 else 0
return {
'part_number': part_number,
'name': name[:300], # truncate to schema limit
'brand': marca_safe,
'cost': cost,
'price_1': price_1,
'price_2': price_2,
'price_3': price_3,
'stock': stock,
'min_stock': min_stock,
'max_stock': max_stock,
'location': pcode,
'unit': 'PZA',
'tax_rate': 0.16,
}
def import_st01(tenant_id, file_path, branch_id=1):
print(f"=== Importing Strada inventory ===")
print(f"Tenant: {tenant_id}")
print(f"File: {file_path}")
print(f"Branch: {branch_id}")
print()
# Parse file
print("Parsing fixed-width file...")
parsed = []
skipped = 0
start_time = time.time()
with open(file_path, 'r', encoding='utf-8', errors='replace') as f:
for line in f:
row = parse_st01_line(line)
if row is None:
skipped += 1
continue
parsed.append(row)
parse_time = time.time() - start_time
print(f" Parsed: {len(parsed):,} rows")
print(f" Skipped: {skipped:,} lines (headers/empty)")
print(f" Time: {parse_time:.1f}s")
print()
if not parsed:
print("ERROR: No data rows found.")
return
# Show sample
print("Sample rows:")
for row in parsed[:5]:
print(f" {row['part_number']:20} | {row['name'][:40]:40} | stk={row['stock']:>4} | cost={row['cost']:>10.2f} | p1={row['price_1']:>10.2f}")
print()
# Connect to tenant DB
conn = get_tenant_conn(tenant_id)
cur = conn.cursor()
# Ensure branch exists
cur.execute("SELECT id FROM branches WHERE id = %s", (branch_id,))
if not cur.fetchone():
print(f"ERROR: Branch {branch_id} does not exist. Creating default branch...")
cur.execute("INSERT INTO branches (name) VALUES ('Principal') RETURNING id")
branch_id = cur.fetchone()[0]
conn.commit()
print(f" Created branch id={branch_id}")
# Step 1: Create temp table and COPY data
print("Creating temp table...")
cur.execute("""
DROP TABLE IF EXISTS tmp_st01_import;
CREATE TEMP TABLE tmp_st01_import (
part_number VARCHAR(100),
name VARCHAR(300),
brand VARCHAR(100),
cost NUMERIC(12,2),
price_1 NUMERIC(12,2),
price_2 NUMERIC(12,2),
price_3 NUMERIC(12,2),
stock INTEGER,
min_stock INTEGER,
max_stock INTEGER,
location VARCHAR(50),
unit VARCHAR(20),
tax_rate NUMERIC(5,4)
);
""")
conn.commit()
print("COPY to temp table...")
csv_buffer = io.StringIO()
writer = csv.writer(csv_buffer, lineterminator='\n')
for row in parsed:
writer.writerow([
row['part_number'],
row['name'],
row['brand'],
row['cost'],
row['price_1'],
row['price_2'],
row['price_3'],
row['stock'],
row['min_stock'],
row['max_stock'],
row['location'],
row['unit'],
row['tax_rate'],
])
csv_buffer.seek(0)
cur.copy_expert(
"""COPY tmp_st01_import (
part_number, name, brand, cost, price_1, price_2, price_3,
stock, min_stock, max_stock, location, unit, tax_rate
) FROM STDIN WITH (FORMAT CSV)""",
csv_buffer
)
conn.commit()
cur.execute("SELECT COUNT(*) FROM tmp_st01_import")
copied = cur.fetchone()[0]
print(f" Copied: {copied:,} rows")
print()
# Step 2: Insert into inventory
print("Inserting into inventory...")
start_time = time.time()
cur.execute("""
INSERT INTO inventory (
branch_id, part_number, name, brand,
cost, price_1, price_2, price_3,
min_stock, max_stock, location, unit, tax_rate, is_active
)
SELECT
%s, part_number, name, brand,
cost, price_1, price_2, price_3,
min_stock, max_stock, location, unit, tax_rate, TRUE
FROM tmp_st01_import
ON CONFLICT (branch_id, part_number) DO UPDATE SET
name = EXCLUDED.name,
brand = EXCLUDED.brand,
cost = EXCLUDED.cost,
price_1 = EXCLUDED.price_1,
price_2 = EXCLUDED.price_2,
price_3 = EXCLUDED.price_3,
min_stock = EXCLUDED.min_stock,
max_stock = EXCLUDED.max_stock,
location = EXCLUDED.location,
is_active = TRUE
""", (branch_id,))
conn.commit()
insert_time = time.time() - start_time
cur.execute("SELECT COUNT(*) FROM inventory WHERE branch_id = %s", (branch_id,))
total_inventory = cur.fetchone()[0]
print(f" Inventory rows: {total_inventory:,}")
print(f" Time: {insert_time:.1f}s")
print()
# Step 3: Create stock operations for items with stock > 0
print("Creating initial stock operations...")
start_time = time.time()
cur.execute("""
INSERT INTO inventory_operations (
inventory_id, branch_id, operation_type, quantity,
cost_at_time, reference_type, notes
)
SELECT
i.id,
i.branch_id,
'INITIAL',
t.stock,
i.cost,
'IMPORT',
'Importado desde TODO.st01'
FROM tmp_st01_import t
JOIN inventory i ON i.part_number = t.part_number AND i.branch_id = %s
WHERE t.stock > 0
""", (branch_id,))
conn.commit()
ops_time = time.time() - start_time
cur.execute("SELECT COUNT(*) FROM inventory_operations WHERE operation_type = 'INITIAL'")
total_ops = cur.fetchone()[0]
print(f" Stock operations created: {total_ops:,}")
print(f" Time: {ops_time:.1f}s")
print()
# Cleanup
cur.execute("DROP TABLE IF EXISTS tmp_st01_import")
conn.commit()
cur.close()
conn.close()
print("=== Import complete ===")
print(f"Total inventory items: {total_inventory:,}")
print(f"Items with stock > 0: {total_ops:,}")
def main():
parser = argparse.ArgumentParser(description='Import Strada TODO.st01 into Nexus tenant')
parser.add_argument('--tenant', type=int, required=True, help='Tenant ID')
parser.add_argument('--file', required=True, help='Path to TODO.st01 file')
parser.add_argument('--branch', type=int, default=1, help='Branch ID (default: 1)')
args = parser.parse_args()
import_st01(args.tenant, args.file, args.branch)
if __name__ == '__main__':
main()