From 2d57ceab542dced04a589ab522478ae5257fbb3f Mon Sep 17 00:00:00 2001 From: nns Date: Fri, 9 Oct 2026 16:01:29 +0500 Subject: [PATCH] =?UTF-8?q?feat(migration):=20=D1=81=D0=B2=D0=B5=D1=80?= =?UTF-8?q?=D0=BA=D0=B0=20=D0=BF=D0=BE=20=D1=87=D0=B8=D1=81=D0=BB=D1=83=20?= =?UTF-8?q?=D0=B4=D0=BE=D0=BA=D1=83=D0=BC=D0=B5=D0=BD=D1=82=D0=BE=D0=B2=20?= =?UTF-8?q?+=20loss/purchasereturn/enter=20=D0=BF=D0=B5=D1=80=D0=B5=D0=BD?= =?UTF-8?q?=D0=B5=D1=81=D0=B5=D0=BD=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit loss 3068/3068, purchasereturn 550/550, enter 72/72 — сходятся точно. Приёмки 7130 (Δ -1 к живому МС). Розничные продажи — перенос продолжается (резюм по offset). scripts/migration/reconcile.py сравнивает МойСклад vs ALL MARKET (read-only) → docs/migration-reconciliation.md. Счётчики МС растут (живой аккаунт) → нужна дельта. Co-Authored-By: Claude Opus 5.5 --- docs/migration-progress.md | 7 ++++- docs/migration-reconciliation.md | 15 +++++++++ scripts/migration/reconcile.py | 54 ++++++++++++++++++++++++++++++++ 3 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 docs/migration-reconciliation.md create mode 100644 scripts/migration/reconcile.py diff --git a/docs/migration-progress.md b/docs/migration-progress.md index 30ae967..fb3ee38 100644 --- a/docs/migration-progress.md +++ b/docs/migration-progress.md @@ -43,7 +43,12 @@ дефолтных ед.; склад — единственный склад орг). 3. [x] **Новая орг на стенде** «FOOD MARKET» (простой режим), admin `foodmarket-migr@food-market.local`. Демо/«Магазин у дома» не тронуты. Токен МойСклада задан в настройках орг. -4. [ ] Сверка → `docs/migration-reconciliation.md` (цели собраны в source-inventory; после переноса документов). +4. [~] **Сверка (авто)** → `scripts/migration/reconcile.py` + `docs/migration-reconciliation.md`. + Число документов на 09.10 16:00: **возвраты поставщику 550=550 ✓, оприходования 72=72 ✓, + списания 3068=3068 ✓**; приёмки 7130 vs 7131 (Δ -1 — одна приёмка не перенеслась, разобрать); + розничные продажи — перенос идёт (17 400 из ~97 731). Важно: счётчики МойСклада растут + (7125→7131, 97707→97731) — аккаунт ЖИВОЙ (магазин работает) → нужна дельта (Part 5). + Остатки/долги/выручка-по-месяцам — секции после завершения продаж. 5. [ ] Дельта-досинхронизация (updated>= МСК + удалённые). 6. [ ] Производительность/надёжность (резюмируемый курсор документов). 7. [ ] Runbook «день перехода» + откат. diff --git a/docs/migration-reconciliation.md b/docs/migration-reconciliation.md new file mode 100644 index 0000000..03cc024 --- /dev/null +++ b/docs/migration-reconciliation.md @@ -0,0 +1,15 @@ +# Сверка переноса МойСклад → ALL MARKET + +_Сгенерировано 2026-10-09 16:00:49._ + +## 1. Число документов по типам + +| Тип | МойСклад | ALL MARKET | Δ | Статус | +|---|---|---|---|---| +| Приёмки | 7131 | 7130 | -1 | ~ идёт перенос | +| Возвраты поставщику | 550 | 550 | 0 | ✓ | +| Оприходования | 72 | 72 | 0 | ✓ | +| Списания | 3068 | 3068 | 0 | ✓ | +| Розничные продажи | 97731 | 17400 | -80331 | ~ идёт перенос | + +_Доккаунты: есть расхождения/перенос продолжается (см. Δ<0 — ещё не все)._ diff --git a/scripts/migration/reconcile.py b/scripts/migration/reconcile.py new file mode 100644 index 0000000..0384a45 --- /dev/null +++ b/scripts/migration/reconcile.py @@ -0,0 +1,54 @@ +#!/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 — ещё не все)'}._")