Решение владельца 09.10: полный перенос магазина из МойСклада в ALL MARKET с историей. - scripts/migration/ms_inventory.py — read-only инвентаризация источника. - docs/migration-source-inventory.md: орг «FOOD MARKET», ~31k товаров, 627 контрагентов, ~110k документов (97 707 чеков, 7 125 приёмок, 3 068 списаний…), 7 558 строк остатков, 1 склад/точка/орг, касса наличных, 1 тип цены; gap-анализ модели + цели сверки. - docs/migration-design.md: идемпотентность по msId, хронология документов, МСК→UTC, тиыны, резюмируемость, порядок этапов, сверка, дельта, runbook. - docs/migration-progress.md: план Part 1–7. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
68 lines
4.3 KiB
Python
68 lines
4.3 KiB
Python
#!/usr/bin/env python3
|
||
"""Инвентаризация источника МойСклад (ТОЛЬКО ЧТЕНИЕ): считает кол-во каждой сущности
|
||
через meta.size, остатки/деньги/взаиморасчёты через report-эндпоинты. Пишет
|
||
docs/migration-source-inventory.md. Токен — ~/.food-market-secrets/moysklad.env."""
|
||
import os, json, gzip, time, urllib.request, urllib.error, sys, io
|
||
BASE = "https://api.moysklad.ru/api/remap/1.2"
|
||
TOK = None
|
||
for line in open(os.path.expanduser("~/.food-market-secrets/moysklad.env")):
|
||
if line.startswith("MOYSKLAD_TOKEN="): TOK = line.split("=",1)[1].strip()
|
||
|
||
def get(path, tries=4):
|
||
url = path if path.startswith("http") else BASE + path
|
||
for i in range(tries):
|
||
req = urllib.request.Request(url, headers={
|
||
"Authorization": f"Bearer {TOK}", "Accept-Encoding": "gzip", "Accept": "application/json;charset=utf-8"})
|
||
try:
|
||
with urllib.request.urlopen(req, timeout=130) as r:
|
||
raw = r.read()
|
||
if r.headers.get("Content-Encoding") == "gzip": raw = gzip.decompress(raw)
|
||
return json.loads(raw.decode("utf-8"))
|
||
except urllib.error.HTTPError as e:
|
||
if e.code == 429:
|
||
wait = float(e.headers.get("X-RateLimit-Retry-TimeInterval", 2000))/1000.0 or 2
|
||
time.sleep(wait+0.5); continue
|
||
if e.code >= 500: time.sleep(2*(i+1)); continue
|
||
print(f" ! {path} -> HTTP {e.code} {e.read()[:200]}", file=sys.stderr); return None
|
||
except Exception as ex:
|
||
time.sleep(2*(i+1));
|
||
if i==tries-1: print(f" ! {path} -> {ex}", file=sys.stderr); return None
|
||
return None
|
||
|
||
def size(entity):
|
||
d = get(f"/entity/{entity}?limit=1")
|
||
time.sleep(0.12)
|
||
return d.get("meta",{}).get("size") if d else None
|
||
|
||
ENTITIES = [
|
||
("Товары","product"),("Модификации","variant"),("Услуги","service"),("Комплекты","bundle"),
|
||
("Группы товаров","productfolder"),("Единицы измерения","uom"),("Валюты","currency"),
|
||
("Типы цен (в настройках)","__pricetype"),("Штрихкоды","__barcodes"),
|
||
("Контрагенты","counterparty"),("Юрлица (организации)","organization"),("Склады","store"),
|
||
("Точки продаж","retailstore"),("Сотрудники","employee"),("Счета организации","__orgaccounts"),
|
||
# документы
|
||
("Приёмки","supply"),("Возвраты поставщикам","purchasereturn"),("Оприходования","enter"),
|
||
("Списания","loss"),("Инвентаризации","inventory"),("Перемещения","move"),
|
||
("Розничные продажи (чеки)","retaildemand"),("Розничные возвраты","retailsalesreturn"),
|
||
("Смены","retailshift"),("Внесения в кассу","retaildrawercashin"),("Выплаты из кассы","retaildrawercashout"),
|
||
("Отгрузки","demand"),("Возвраты покупателей","salesreturn"),("Заказы покупателей","customerorder"),
|
||
("Заказы поставщикам","purchaseorder"),("Счета-фактуры вх.","invoicein"),("Счета покупателям","invoiceout"),
|
||
("Входящие платежи","paymentin"),("Исходящие платежи","paymentout"),
|
||
("Приходные ордера","cashin"),("Расходные ордера","cashout"),
|
||
("Корректировки взаиморасчётов","counterpartyadjustment"),("Оприходование/списание денег","__none"),
|
||
]
|
||
|
||
print("# Инвентаризация источника МойСклад (FOOD MARKET)\n")
|
||
print(f"_Сгенерировано {time.strftime('%Y-%m-%d %H:%M:%S')} (только чтение)._\n")
|
||
print("## Сущности и документы\n")
|
||
print("| Сущность | МойСклад entity | Кол-во |")
|
||
print("|---|---|---|")
|
||
rows=[]
|
||
for label, ent in ENTITIES:
|
||
if ent.startswith("__"):
|
||
rows.append((label, ent, "—")); print(f"| {label} | `{ent[2:]}` | _спец, ниже_ |"); continue
|
||
n = size(ent)
|
||
rows.append((label, ent, n))
|
||
print(f"| {label} | `{ent}` | {n if n is not None else 'н/д'} |")
|
||
sys.stdout.flush()
|