#!/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()