food-market/scripts/migration/ms_inventory.py
nns a2ee85bc12
Some checks are pending
Auto-tag / Create date-tag (push) Waiting to run
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
docs(migration): инвентаризация источника МойСклад + дизайн мигратора (Part 1)
Решение владельца 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>
2026-10-09 13:26:35 +05:00

68 lines
4.3 KiB
Python
Raw Permalink 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
"""Инвентаризация источника МойСклад (ТОЛЬКО ЧТЕНИЕ): считает кол-во каждой сущности
через 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()