feat(migration): сверка по числу документов + loss/purchasereturn/enter перенесены
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
Docker API / Build + push API (push) Waiting to run
Docker API / Deploy API on stage (push) Blocked by required conditions

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 <noreply@anthropic.com>
This commit is contained in:
nns 2026-10-09 16:01:29 +05:00
parent 9506f9edc8
commit 2d57ceab54
3 changed files with 75 additions and 1 deletions

View file

@ -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 «день перехода» + откат.

View file

@ -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 — ещё не все)._

View file

@ -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 — ещё не все)'}._")