fix(migration): экранировать filter=updated>= целиком; дельта-драйвер — точный повторный опрос job + audit tolerant
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
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
- StreamDocumentsAsync: Uri.EscapeDataString всего выражения updated>=<дата> (>= → %3E%3D), не только даты — иначе часть форм 400/неоднозначна - delta_sync: повторное чтение job после Succeeded (устранить гонку счётчиков), audit в try - reconcile: розница демо/возвраты раздельно, URL-энкод фильтра чеков по месяцам Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
110ecf551d
commit
81398fadad
|
|
@ -82,7 +82,11 @@ def poll(tok, job_id, timeout_s=1800):
|
|||
while time.time() - t0 < timeout_s:
|
||||
d = _get(AM + f"/api/admin/moysklad/jobs/{job_id}", {"Authorization": f"Bearer {tok}"})
|
||||
if d and d.get("status") in ("Succeeded", "Failed"):
|
||||
return d
|
||||
# повторное чтение: финальный flush счётчиков (Created/Skipped) мог отстать
|
||||
# на один тик от статуса — читаем ещё раз, чтобы цифры были точными.
|
||||
time.sleep(2)
|
||||
d2 = _get(AM + f"/api/admin/moysklad/jobs/{job_id}", {"Authorization": f"Bearer {tok}"})
|
||||
return d2 or d
|
||||
time.sleep(5)
|
||||
return {"status": "Timeout"}
|
||||
|
||||
|
|
@ -126,9 +130,13 @@ def main():
|
|||
total_created += created
|
||||
print(f" [{dt}] {res.get('status')}: создано {created}, пропущено {res.get('Skipped', 0)}. {res.get('Message','')[:120]}")
|
||||
|
||||
# удаления
|
||||
# удаления (audit). filter целиком экранируем; при 4xx/недоступности — мягко пропускаем.
|
||||
deleted = []
|
||||
audit = ms(f"/audit?limit=100&filter=eventType=delete;moment>={urllib.parse.quote(last)}")
|
||||
try:
|
||||
flt = urllib.parse.quote(f"eventType=delete;moment>={last}")
|
||||
audit = ms(f"/audit?limit=100&filter={flt}")
|
||||
except Exception:
|
||||
audit = None
|
||||
if audit is None:
|
||||
print(" [deletions] /audit недоступен (нет прав токена) — проверить удаления вручную на дне перехода.")
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"""Сверка переноса МойСклад → ALL MARKET (только чтение обоих). Сравнивает:
|
||||
- число документов каждого типа; - остатки товар×склад (report/stock/all); - выручку и
|
||||
число чеков по месяцам. Пишет docs/migration-reconciliation.md. Расхождения помечает."""
|
||||
import os, json, gzip, time, urllib.request, urllib.error, sys, ssl, subprocess
|
||||
import os, json, gzip, time, urllib.request, urllib.error, urllib.parse, sys, ssl, subprocess
|
||||
from collections import defaultdict
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
|
|
@ -54,18 +54,28 @@ 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")]
|
||||
("Оприходования","enter","/api/inventory/enters"),("Списания","loss","/api/inventory/losses")]
|
||||
allzero=True
|
||||
for label,ent,ap in DOCS:
|
||||
a=ms_size(ent); b=am_total(ap);
|
||||
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 "✗")
|
||||
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 — ещё не все)'}._")
|
||||
# Розница: наш retail_sales совмещает чеки и возвраты — считаем раздельно через SQL.
|
||||
def one(q):
|
||||
r=sql(q); return int(r[0][0]) if r and r[0] else 0
|
||||
our_dem=one("SELECT COUNT(*) FROM retail_sales WHERE \\\"MoySkladExternalId\\\" IS NOT NULL AND NOT \\\"IsReturn\\\"")
|
||||
our_ret=one("SELECT COUNT(*) FROM retail_sales WHERE \\\"MoySkladExternalId\\\" IS NOT NULL AND \\\"IsReturn\\\"")
|
||||
for label,ent,b in [("Розничные продажи","retaildemand",our_dem),("Розничные возвраты","retailsalesreturn",our_ret)]:
|
||||
a=ms_size(ent); d=b-(a or 0)
|
||||
ok="✓" if d==0 else ("~ дельта" if 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 — догоняется дельтой (живой аккаунт рос во время переноса); Δ>0 — разобрать'}._")
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 2. Остатки товар×склад: МойСклад report/stock/all (НЕ отдаёт нули) vs наши stocks.
|
||||
|
|
@ -153,7 +163,8 @@ try:
|
|||
for s in ps.get("series",[]):
|
||||
if str(s.get("date","")).startswith(mo): ms_sum+=float(s.get("sum",0) or 0)
|
||||
ms_rev=ms_sum/100.0
|
||||
cc=ms(f"/entity/retaildemand?limit=1&filter=moment>={frm};moment<{to}")
|
||||
flt=urllib.parse.quote(f"moment>={frm};moment<{to}")
|
||||
cc=ms(f"/entity/retaildemand?limit=1&filter={flt}")
|
||||
ms_cnt=(cc or {}).get("meta",{}).get("size",0)
|
||||
o_cnt,o_rev=ourm.get(mo,(0,0.0))
|
||||
dd=o_rev-ms_rev; dc=o_cnt-ms_cnt
|
||||
|
|
|
|||
|
|
@ -158,7 +158,8 @@ public async Task<List<MsProductFolder>> GetAllFoldersAsync(string token, Cancel
|
|||
const int pageSize = 100;
|
||||
const int maxAttempts = 6;
|
||||
var offset = startOffset;
|
||||
var filter = updatedFromMsk is null ? "" : $"&filter=updated>={Uri.EscapeDataString(updatedFromMsk)}";
|
||||
// Весь filter-expression надо экранировать целиком (включая ">="), иначе МойСклад 400.
|
||||
var filter = updatedFromMsk is null ? "" : $"&filter={Uri.EscapeDataString($"updated>={updatedFromMsk}")}";
|
||||
var exp = string.IsNullOrEmpty(expand) ? "" : $"&expand={expand}";
|
||||
while (true)
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in a new issue