#!/usr/bin/env python3 """Сверка переноса МойСклад → ALL MARKET (только чтение обоих). Сравнивает: - число документов каждого типа; - остатки товар×склад (report/stock/all); - выручку и число чеков по месяцам. Пишет docs/migration-reconciliation.md. Расхождения помечает.""" import os, json, gzip, time, urllib.request, urllib.error, sys, ssl from collections import defaultdict MS="https://api.moysklad.ru/api/remap/1.2" AM="https://test.admin.food-market.kz" MSTOK=[l.split("=",1)[1].strip() for l in open(os.path.expanduser("~/.food-market-secrets/moysklad.env")) if l.startswith("MOYSKLAD_TOKEN=")][0] _ctx=ssl.create_default_context(); _ctx.check_hostname=False; _ctx.verify_mode=ssl.CERT_NONE def _get(url,hdr): for i in range(4): try: req=urllib.request.Request(url,headers=hdr) with urllib.request.urlopen(req,timeout=130,context=_ctx) 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: time.sleep(float(e.headers.get("X-RateLimit-Retry-TimeInterval",2000))/1000+0.5); continue if e.code>=500: time.sleep(2*(i+1)); continue raise except Exception: time.sleep(2*(i+1)) return None def ms(path): return _get(MS+path,{"Authorization":f"Bearer {MSTOK}","Accept-Encoding":"gzip"}) def am_token(): import urllib.parse data=urllib.parse.urlencode({"grant_type":"password","username":"foodmarket-migr@food-market.local","password":"Migrate12345!","client_id":"food-market-web","scope":"openid profile email roles api"}).encode() req=urllib.request.Request(AM+"/connect/token",data=data,headers={"Content-Type":"application/x-www-form-urlencoded"}) with urllib.request.urlopen(req,timeout=60,context=_ctx) as r: return json.loads(r.read())["access_token"] TOK=am_token() def am(path): return _get(AM+path,{"Authorization":f"Bearer {TOK}","Accept-Encoding":"gzip"}) def ms_size(ent): d=ms(f"/entity/{ent}?limit=1"); return d.get("meta",{}).get("size") if d else None def am_total(path): d=am(path+("&" if "?" in path else "?")+"take=1&pageSize=1"); return d.get("total") if d else None print("# Сверка переноса МойСклад → ALL MARKET\n") print(f"_Сгенерировано {time.strftime('%Y-%m-%d %H:%M:%S')}._\n") print("## 1. Число документов по типам\n") print("| Тип | МойСклад | ALL MARKET | Δ | Статус |") print("|---|---|---|---|---|") DOCS=[("Приёмки","supply","/api/purchases/supplies"),("Возвраты поставщику","purchasereturn","/api/purchases/supplier-returns"), ("Оприходования","enter","/api/inventory/enters"),("Списания","loss","/api/inventory/losses"), ("Розничные продажи","retaildemand","/api/sales/retail")] allzero=True for label,ent,ap in DOCS: a=ms_size(ent); b=am_total(ap); try: d=(b or 0)-(a or 0) except: d="?" ok="✓" if d==0 else ("~ идёт перенос" if isinstance(d,int) and d<0 else "✗") if d!=0: allzero=False print(f"| {label} | {a} | {b} | {d} | {ok} |") time.sleep(0.15) print(f"\n_Доккаунты: {'все сходятся' if allzero else 'есть расхождения/перенос продолжается (см. Δ<0 — ещё не все)'}._")