dy-dashboard/batch_import_inventory.py
2026-10-28 11:14:17 +08:00

232 lines
9.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""批量下载并导入所有库存明细数据"""
import sys
import os
import csv
import json
import subprocess
import pymysql
from datetime import datetime
import re
# 从 config.py 导入配置
import importlib.util
spec = importlib.util.spec_from_file_location("config", "config.py")
config = importlib.util.module_from_spec(spec)
spec.loader.exec_module(config)
DB_CONFIG = config.DB_CONFIG
# 特殊成本价规则:命中的商品不使用CSV/库存表成本价
from services.inventory_calc import SPECIAL_COST_PRICES
TMP_DIR = "tmp_csv"
def parse_int(val):
if val is None or val == '' or str(val).upper() == 'NA':
return 0
try:
return int(float(val))
except (ValueError, TypeError):
return 0
def parse_float(val):
if val is None or val == '' or str(val).upper() == 'NA':
return 0.0
try:
return float(val)
except (ValueError, TypeError):
return 0.0
def parse_datetime(val):
if not val or val == '' or str(val).upper() == 'NA':
return None
try:
return datetime.strptime(val.strip(), '%Y-%m-%d %H:%M:%S')
except ValueError:
try:
return datetime.strptime(val.strip(), '%Y-%m-%d')
except ValueError:
return None
def is_invalid_product_code(code):
"""过滤报表汇总行、占位编码和空编码。"""
normalized = str(code or '').strip().upper().rstrip(':')
return not normalized or normalized in ('NA', 'N/A', '合计')
def is_invalid_product_name(name):
"""过滤货品名称为单横线或双横线的占位数据。"""
return str(name or '').strip() in ('-', '--')
def download_file(name, node_id):
"""下载钉盘文件,返回本地路径"""
local_path = os.path.join(TMP_DIR, f"{name}.csv")
# 获取下载 URL
result = subprocess.run(
["dws", "doc", "download", "--node", node_id, "--output", "/dev/stdout", "--format", "json"],
capture_output=True, text=True, timeout=30
)
try:
data = json.loads(result.stdout)
url = data.get('resourceUrl')
if not url:
print(f" ❌ 无法获取下载链接: {name}")
return None
except:
print(f" ❌ 解析下载响应失败: {name}")
return None
# 下载文件
subprocess.run(
["curl", "-sL", url, "-o", local_path],
capture_output=True, timeout=30
)
if os.path.exists(local_path) and os.path.getsize(local_path) > 50:
print(f" ✅ 下载完成: {name}.csv ({os.path.getsize(local_path)} bytes)")
return local_path
else:
print(f" ❌ 下载失败或文件过小: {name}")
return None
def import_file(filepath, data_date, warehouse_code, source='wdt'):
"""导入单个文件到数据库"""
conn = pymysql.connect(
host=DB_CONFIG["host"], port=DB_CONFIG["port"],
user=DB_CONFIG["user"], password=DB_CONFIG["password"],
database=DB_CONFIG["database"], charset="utf8mb4",
cursorclass=pymysql.cursors.DictCursor
)
try:
with open(filepath, 'r', encoding='utf-8-sig') as f:
reader = csv.DictReader(f)
rows = list(reader)
print(f" 读取到 {len(rows)} 条记录")
inserted = 0
with conn.cursor() as cur:
for row in rows:
try:
product_code = row.get('货品编号', '').strip()
if is_invalid_product_code(product_code):
continue
if is_invalid_product_name(row.get('货品名称')):
continue
cost_price = parse_float(row.get('成本价', 0))
# 特殊成本价规则:命中的商品不使用CSV/库存表成本价
cost_price = SPECIAL_COST_PRICES.get(product_code, cost_price)
cur.execute("""
INSERT INTO inventory_detail
(data_date, source, warehouse_code, goods_code, product_code, product_name,
brand, cost_price, available_qty, production_in_transit, purchase_in_transit,
category1, category2, category3, created_time)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
""", (
data_date, source, warehouse_code,
row.get('商家编码', '').strip(),
product_code,
row.get('货品名称', '').strip(),
row.get('品牌', '').strip(),
cost_price,
parse_int(row.get('可用库存', 0)),
parse_int(row.get('生产在途量', 0)),
parse_int(row.get('采购在途', 0)),
row.get('一级类目', '').strip(),
row.get('二级类目', '').strip(),
row.get('三级类目', '').strip(),
parse_datetime(row.get('单品创建时间', ''))
))
inserted += 1
except Exception as e:
print(f" 插入失败: {e}")
conn.commit()
return inserted
finally:
conn.close()
# 所有需要导入的文件列表
FILES = [
# 2026-05-27 (除了已导入的01)
("20260527-wdt-kcmx-04", "2026-05-27", "04", "3NwLYZXWynyBjRB4tZZ1gM2oVkyEqBQm"),
("20260527-wdt-kcmx-05", "2026-05-27", "05", "y20BglGWO2ywEgwkh00oxDN98A7depqY"),
("20260527-wdt-kcmx-06", "2026-05-27", "06", "14dA3GK8gjMb34bZIKKRXEQ0J9ekBD76"),
("20260527-wdt-kcmx-07", "2026-05-27", "07", "7dx2rn0JbY4m7rmQH22aYgwdVMGjLRb3"),
("20260527-wdt-kcmx-10", "2026-05-27", "10", "y20BglGWO2ywEgwkh00oxOzP8A7depqY"),
("20260527-wdt-kcmx-11", "2026-05-27", "11", "np9zOoBVBYpa0AaxHLL7Ke3DW1DK0g6l"),
("20260527-wdt-kcmx-12", "2026-05-27", "12", "P0MALyR8klXxPgxAtKKAv379W3bzYmDO"),
("20260527-wdt-kcmx-13", "2026-05-27", "13", "MyQA2dXW7eKDkAD2C55OaxvjJzlwrZgb"),
("20260527-wdt-kcmx-15", "2026-05-27", "15", "1OQX0akWmx2P5jPZFvvzyvKg8GlDd3mE"),
("20260527-wdt-kcmx-16", "2026-05-27", "16", "4lgGw3P8vRrBDmBeUppbwDLd85daZ90D"),
("20260527-wdt-kcmx-22", "2026-05-27", "22", "OG9lyrgJPzx5KM5ACzz0qrdZWzN67Mw4"),
("20260527-wdt-kcmx-23", "2026-05-27", "23", "nYMoO1rWxa3BM7BKSzzADrneV47Z3je9"),
("20260527-wdt-kcmx-24", "2026-05-27", "24", "DnRL6jAJMGYOl9OjFXX0rQyOWyMoPYe1"),
# 2026-05-28 (全部18个仓库)
("20260528-wdt-kcmx-01", "2026-05-28", "01", "Amq4vjg890vMpAMPCPmdyejGJ3kdP0wQ"),
("20260528-wdt-kcmx-02", "2026-05-28", "02", "ndMj49yWjXkyODyehwR0Obn0J3pmz5aA"),
("20260528-wdt-kcmx-03", "2026-05-28", "03", "4lgGw3P8vRrBDmBeUpgdKP5385daZ90D"),
("20260528-wdt-kcmx-04", "2026-05-28", "04", "vNG4YZ7JnPzwqNwLH9NyxoqkW2LD0oRE"),
("20260528-wdt-kcmx-05", "2026-05-28", "05", "LeBq413JAwXZRAZxtr3naNPKWDOnGvpb"),
("20260528-wdt-kcmx-06", "2026-05-28", "06", "bva6QBXJwaYBgnBwS2Ml5wAQWn4qY5Pr"),
("20260528-wdt-kcmx-07", "2026-05-28", "07", "Amq4vjg890vMpAMPCPmd79jKJ3kdP0wQ"),
("20260528-wdt-kcmx-08", "2026-05-28", "08", "1zknDm0WRaPKvNKASx09oDEg8BQEx5rG"),
("20260528-wdt-kcmx-09", "2026-05-28", "09", "DnRL6jAJMGYOl9OjFXqDeaY9WyMoPYe1"),
("20260528-wdt-kcmx-10", "2026-05-28", "10", "GZLxjv9VGqm0xA04u167zyOq86EDybno"),
("20260528-wdt-kcmx-11", "2026-05-28", "11", "gpG2NdyVX3EMNZMACGP3pzPXWMwvDqPk"),
("20260528-wdt-kcmx-12", "2026-05-28", "12", "0eMKjyp813eL9ALkC4edLLXjVxAZB1Gv"),
("20260528-wdt-kcmx-13", "2026-05-28", "13", "R4GpnMqJzGxBN0BvFakK9L7w8Ke0xjE3"),
("20260528-wdt-kcmx-15", "2026-05-28", "15", "Exel2BLV5z6vjAv2CmPEA9gPJgk9rpMq"),
("20260528-wdt-kcmx-16", "2026-05-28", "16", "QPGYqjpJYrAjkajLCZBdmd1v8akx1Z5N"),
("20260528-wdt-kcmx-22", "2026-05-28", "22", "yQod3RxJKG0y54ygF4oPGOeeJkb4Mw9r"),
("20260528-wdt-kcmx-23", "2026-05-28", "23", "dxXB52LJqnmBErB3tMKnGK7w8qjMp697"),
("20260528-wdt-kcmx-24", "2026-05-28", "24", "9E05BDRVQ2Oq4jqAhyDmrqRqJ63zgkYA"),
]
os.makedirs(TMP_DIR, exist_ok=True)
total_imported = 0
total_files = len(FILES)
success_count = 0
for i, (name, data_date, wh_code, node_id) in enumerate(FILES, 1):
print(f"\n[{i}/{total_files}] {name} (日期:{data_date}, 仓库:{wh_code})")
# 下载
local_path = download_file(name, node_id)
if not local_path:
continue
# 检查文件编码和内容
result = subprocess.run(
["file", local_path], capture_output=True, text=True, timeout=5
)
# 如果是 GBK 编码,转为 UTF-8
if 'ISO' in result.stdout or '8859' in result.stdout:
utf8_path = local_path.replace('.csv', '_utf8.csv')
subprocess.run(
["iconv", "-f", "GBK", "-t", "UTF-8", local_path, "-o", utf8_path],
capture_output=True, timeout=10
)
os.replace(utf8_path, local_path)
# 导入
count = import_file(local_path, data_date, wh_code)
if count > 0:
print(f" ✅ 导入 {count} 条记录")
total_imported += count
success_count += 1
else:
print(f" ⚠️ 导入 0 条记录")
print(f"\n{'='*50}")
print(f"批量导入完成!")
print(f"成功: {success_count}/{total_files} 个文件")
print(f"总计导入: {total_imported} 条记录")
print(f"{'='*50}")