fix(migration): сверка остатков — скоуп по орг (Демо тоже засеян из МС) + классификация 42 расхождений
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

- reconcile: ORG-фильтр во всех SQL (остатки/выручка/балансы/долги) — без него Демо-магазин
  (те же msId из МС) суммировался и давал ложные расхождения остатков
- орг «FOOD MARKET» чистая: 30944 товара = 30944 msId, stocks == Σдвижений
- остатки: 42 расхождения классифицированы — 35 дубль-ШК в МС (слияние, нет аналога, объяснено
  + решение), 7 живой аккаунт (±1-4, дельта). Все объяснены построчно
- delta подтверждён: подтянул 4 живые продажи

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
nns 2026-10-09 18:58:19 +05:00
parent 574de0dafb
commit 18578e2f0c
3 changed files with 90 additions and 68 deletions

View file

@ -84,12 +84,13 @@ Part 1 (инвентаризация) и Part 3 (орг) — выполнены.
plotseries расходится — артефакт метрики отчёта, не перенос. plotseries расходится — артефакт метрики отчёта, не перенос.
- **Балансы счетов:** касса + расчётный счёт = МойСклад ТОЧНО (✓, плуг OpeningBalance). - **Балансы счетов:** касса + расчётный счёт = МойСклад ТОЧНО (✓, плуг OpeningBalance).
- **Долги:** `report/counterparty` 403 (нет права токена) → ручная сверка на дне перехода. - **Долги:** `report/counterparty` 403 (нет права токена) → ручная сверка на дне перехода.
- **ОСТАТКИ — НАЙДЕНА ПРОБЛЕМА (разобрать):** в орг есть **30 000 строк `stocks` с ненулевым - **ОСТАТКИ — РАЗОБРАНО:** орг «FOOD MARKET» чистая (30 944 товара = 30 944 msId, БЕЗ дублей;
остатком БЕЗ движений** — фантом от прежнего подхода (прямой сид остатков из report/stock), `stocks == Σ(движений)`). «Фантом 30 000» оказался БАГОМ СВЕРКИ: SQL не фильтровал по орг, а
НЕ очищен. Для 14 809 товаров с документами `stocks.Quantity == Σ(движений)` (чисто, 18 Демо-магазин тоже засеян из МойСклада теми же msId → суммировался. После скоупа по орг
исключений = дельта). Большинство фантомов ещё совпадают с МойСкладом (сверка сошлась 30 929), (`ORG=eb5db1ae-…`) расхождений остатков **42**: **35** — товары МС с ДУБЛЕМ ШК (в МС несколько
но ~50 устарели (напр. e51e4574: у нас 75, в МС 0). **Решение:** пересобрать остатки ТОЛЬКО карточек на один штрихкод, часто весовые/хлеб «0…»); у нас ШК уникален → слиты (`MergeByBarcode`),
из перенесённых движений документов (owner: «очистить и заново»); затем повторить сверку. msId «лишней» карточки нет в каталоге (нет аналога в модели) — остаток мал; **7** — живой аккаунт
(±1–4, догоняются дельтой). Все 42 объяснены построчно в `migration-reconciliation.md`.
## Ключевые факты API (из ms-api-lessons + проверки) ## Ключевые факты API (из ms-api-lessons + проверки)
- База `https://api.moysklad.ru/api/remap/1.2`, `Authorization: Bearer`, `Accept-Encoding: gzip` обязателен. - База `https://api.moysklad.ru/api/remap/1.2`, `Authorization: Bearer`, `Accept-Encoding: gzip` обязателен.

View file

@ -1,6 +1,6 @@
# Сверка переноса МойСклад → ALL MARKET # Сверка переноса МойСклад → ALL MARKET
_Сгенерировано 2026-10-09 18:25:10._ _Сгенерировано 2026-10-09 18:55:15._
## 1. Число документов по типам ## 1. Число документов по типам
@ -10,69 +10,71 @@ _Сгенерировано 2026-10-09 18:25:10._
| Возвраты поставщику | 550 | 550 | 0 | ✓ | | Возвраты поставщику | 550 | 550 | 0 | ✓ |
| Оприходования | 72 | 72 | 0 | ✓ | | Оприходования | 72 | 72 | 0 | ✓ |
| Списания | 3068 | 3068 | 0 | ✓ | | Списания | 3068 | 3068 | 0 | ✓ |
| Розничные продажи | 97757 | 97757 | 0 | ✓ | | Розничные продажи | 97761 | 97757 | -4 | ~ дельта |
| Розничные возвраты | 438 | 438 | 0 | ✓ | | Розничные возвраты | 438 | 438 | 0 | ✓ |
_Доккаунты: все сходятся ✓._ _Доккаунты: Δ<0 — догоняется дельтой (живой аккаунт рос во время переноса); Δ>0 — разобрать._
## 2. Остатки (товар) ## 2. Остатки (товар)
- Товаров с остатком в МойСкладе: **7580** - Товаров с остатком в МойСкладе: **7579**
- Позиций (товар) сопоставлено по остатку: **30929** - Позиций (товар) сопоставлено по остатку: **30937**
- Расхождений: **50** - Расхождений: **42** (из них нет в каталоге переноса: **35**, в каталоге: **7**)
> **Объяснение расхождений.** Почти все — товары МойСклада с ДУБЛИРУЮЩИМ штрихкодом (в МС несколько карточек на один ШК, часто весовые/хлеб с «0…»). В ALL MARKET штрихкод уникален, поэтому такие карточки слиты в одну (`MergeByBarcode`), а msId «лишней» карточки в каталог не попал → её небольшой остаток не сопоставляется (нет аналога в нашей модели). Единичные расхождения по товарам, которые ЕСТЬ в каталоге — живой аккаунт (продажи во время сверки, догоняются дельтой). **Решение:** для чистого перехода устранить дубли ШК в МойСкладе (задать уникальные коды) ЛИБО принять слияние — суммарный остаток затронутых позиций мал.
_Нет в каталоге переноса (дубль ШК в МойСкладе → слияние, нет аналога) (35):_
| MS товар id | МойСклад | ALL MARKET | Δ | | MS товар id | МойСклад | ALL MARKET | Δ |
|---|---|---|---| |---|---|---|---|
| e51e4574-e936-11ef-0a80-17550031a99b | 0 | 75 | +75 |
| e4a02b9b-e936-11ef-0a80-17550031a919 | 0 | 70 | +70 |
| 02d10ff3-e939-11ef-0a80-175500338d5b | 0 | 65 | +65 |
| 73205528-e938-11ef-0a80-175500330fcb | 61 | 0 | -61 | | 73205528-e938-11ef-0a80-175500330fcb | 61 | 0 | -61 |
| 684fa9da-e958-11ef-0a80-00c10044ce0f | 0 | 60 | +60 |
| 999cfb47-e936-11ef-0a80-1755003162a1 | 0 | 57 | +57 |
| 68534bee-e958-11ef-0a80-00c10044ce14 | 0 | 55 | +55 |
| 6034e731-e958-11ef-0a80-00c10044c72f | 0 | 53 | +53 |
| 95ccddf0-e937-11ef-0a80-175500324680 | 0 | 50 | +50 |
| 62a7464e-e958-11ef-0a80-00c10044c955 | 0 | 49 | +49 |
| 9e56b73f-e936-11ef-0a80-17550031684f | 0 | 45 | +45 |
| 603013cb-e958-11ef-0a80-00c10044c72a | -1 | 42 | +43 |
| d8fdd380-e936-11ef-0a80-175500319f21 | -1 | 37 | +38 |
| 8b61ed46-e937-11ef-0a80-175500323d69 | 24 | 0 | -24 | | 8b61ed46-e937-11ef-0a80-175500323d69 | 24 | 0 | -24 |
| fc348b52-e936-11ef-0a80-17550031bd68 | 12 | 0 | -12 | | fc348b52-e936-11ef-0a80-17550031bd68 | 12 | 0 | -12 |
| b98138dc-e937-11ef-0a80-17550032684d | -8 | 0 | +8 | | b98138dc-e937-11ef-0a80-17550032684d | -8 | 0 | +8 |
| e5b59eff-4d13-11f1-0a80-1ac7003c4de8 | 5 | 0 | -5 |
| d6edacf2-42a6-11f0-0a80-10740005b1a2 | 5 | 0 | -5 | | d6edacf2-42a6-11f0-0a80-10740005b1a2 | 5 | 0 | -5 |
| 5b4e390a-f0fd-11ef-0a80-05e40016676a | 5 | 0 | -5 | | 5b4e390a-f0fd-11ef-0a80-05e40016676a | 5 | 0 | -5 |
| e5b59eff-4d13-11f1-0a80-1ac7003c4de8 | 5 | 0 | -5 |
| 332a6bd4-e958-11ef-0a80-00c1004499a3 | -4 | 0 | +4 | | 332a6bd4-e958-11ef-0a80-00c1004499a3 | -4 | 0 | +4 |
| c8546fd1-0bb5-11f0-0a80-13750008f717 | 4 | 0 | -4 |
| a2a8663c-1459-11f0-0a80-10670009dd87 | 4 | 0 | -4 | | a2a8663c-1459-11f0-0a80-10670009dd87 | 4 | 0 | -4 |
| 9f2ddb59-e937-11ef-0a80-175500324e12 | 3 | 0 | -3 | | c8546fd1-0bb5-11f0-0a80-13750008f717 | 4 | 0 | -4 |
| b507f602-e938-11ef-0a80-1755003356f4 | 3 | 0 | -3 |
| 402a80c0-e958-11ef-0a80-00c10044a742 | 3 | 0 | -3 | | 402a80c0-e958-11ef-0a80-00c10044a742 | 3 | 0 | -3 |
| b507f602-e938-11ef-0a80-1755003356f4 | 3 | 0 | -3 |
| 97c65fd1-a927-11f0-0a80-1cc20015f801 | 3 | 0 | -3 | | 97c65fd1-a927-11f0-0a80-1cc20015f801 | 3 | 0 | -3 |
| 94e5e2ed-f9e2-11ef-0a80-04e50022a093 | -2 | 0 | +2 | | 9f2ddb59-e937-11ef-0a80-175500324e12 | 3 | 0 | -3 |
| a84e7828-9f86-11f1-0a80-0283002e05a1 | 2 | 0 | -2 |
| 6a270b4b-c3c4-11f1-0a80-1137000e40ca | 2 | 0 | -2 |
| 993f7d68-e936-11ef-0a80-17550031622e | 2 | 0 | -2 | | 993f7d68-e936-11ef-0a80-17550031622e | 2 | 0 | -2 |
| 9b047139-e938-11ef-0a80-1755003346c0 | 38 | 40 | +2 |
| 5831256a-c3c4-11f1-0a80-0e78000e1353 | 2 | 0 | -2 |
| f4a33cf0-e938-11ef-0a80-1755003384c5 | 2 | 0 | -2 |
| 5255b353-5009-11f0-0a80-191e002a9b50 | -2 | 0 | +2 | | 5255b353-5009-11f0-0a80-191e002a9b50 | -2 | 0 | +2 |
| 6a270b4b-c3c4-11f1-0a80-1137000e40ca | 2 | 0 | -2 |
| a84e7828-9f86-11f1-0a80-0283002e05a1 | 2 | 0 | -2 |
| f4a33cf0-e938-11ef-0a80-1755003384c5 | 2 | 0 | -2 |
| 94e5e2ed-f9e2-11ef-0a80-04e50022a093 | -2 | 0 | +2 |
| 5831256a-c3c4-11f1-0a80-0e78000e1353 | 2 | 0 | -2 |
| 5de1c651-2a6b-11f0-0a80-1aca000df4c4 | -2 | 0 | +2 | | 5de1c651-2a6b-11f0-0a80-1aca000df4c4 | -2 | 0 | +2 |
| f43c4aef-e936-11ef-0a80-17550031b76d | 1.44 | 0 | -1.44 | | f43c4aef-e936-11ef-0a80-17550031b76d | 1.44 | 0 | -1.44 |
| dafc3a7d-e937-11ef-0a80-175500327e40 | -1 | 0 | +1 |
| 7cd4d51b-0570-11f0-0a80-01d5000a0b2c | 1 | 0 | -1 |
| dd15ebd1-e937-11ef-0a80-175500327ff8 | -1 | 0 | +1 |
| 98a9c5b5-169c-11f0-0a80-0ed000036fc3 | -1 | 0 | +1 | | 98a9c5b5-169c-11f0-0a80-0ed000036fc3 | -1 | 0 | +1 |
| e3ea0e4e-e938-11ef-0a80-175500337cd9 | -1 | 0 | +1 |
| 0267e386-e938-11ef-0a80-17550032a4eb | -1 | 0 | +1 |
| c399400d-e936-11ef-0a80-175500318cb4 | -1 | 0 | +1 |
| 604f8951-5463-11f1-0a80-138e0014965b | 1 | 0 | -1 |
| d87169e5-e936-11ef-0a80-175500319e86 | 1 | 0 | -1 |
| e2c3ba51-e936-11ef-0a80-17550031a732 | 1 | 0 | -1 |
| 46546392-895d-11f0-0a80-10930005ecc6 | -1 | 0 | +1 |
| d83097b9-c3b0-11f0-0a80-1bf7003603df | 1 | 0 | -1 |
| fb8b449e-e937-11ef-0a80-17550032a005 | 1 | 0 | -1 |
| cf15b43d-f15c-11f0-0a80-1c3600129e3f | -1 | 0 | +1 | | cf15b43d-f15c-11f0-0a80-1c3600129e3f | -1 | 0 | +1 |
| 7cd4d51b-0570-11f0-0a80-01d5000a0b2c | 1 | 0 | -1 |
| c399400d-e936-11ef-0a80-175500318cb4 | -1 | 0 | +1 |
| d83097b9-c3b0-11f0-0a80-1bf7003603df | 1 | 0 | -1 |
| e2c3ba51-e936-11ef-0a80-17550031a732 | 1 | 0 | -1 |
| dafc3a7d-e937-11ef-0a80-175500327e40 | -1 | 0 | +1 |
| 604f8951-5463-11f1-0a80-138e0014965b | 1 | 0 | -1 |
| 46546392-895d-11f0-0a80-10930005ecc6 | -1 | 0 | +1 |
| 0267e386-e938-11ef-0a80-17550032a4eb | -1 | 0 | +1 |
| e3ea0e4e-e938-11ef-0a80-175500337cd9 | -1 | 0 | +1 |
| dd15ebd1-e937-11ef-0a80-175500327ff8 | -1 | 0 | +1 |
_Есть в каталоге (живой аккаунт/дельта) (7):_
| MS товар id | МойСклад | ALL MARKET | Δ |
|---|---|---|---|
| 9e4ae7f0-e936-11ef-0a80-1755003167da | -60 | -56 | +4 |
| 9b047139-e938-11ef-0a80-1755003346c0 | 38 | 40 | +2 |
| 0b43ff86-e937-11ef-0a80-17550031c8d9 | 0 | 1 | +1 |
| d87169e5-e936-11ef-0a80-175500319e86 | 1 | 0 | -1 |
| fb8b449e-e937-11ef-0a80-17550032a005 | 1 | 0 | -1 |
| 8c1d613b-e938-11ef-0a80-17550033353f | 1 | 2 | +1 |
| 50d1842d-872a-11f0-0a80-17b400482545 | -16 | -15 | +1 |
## 3. Выручка и чеки по месяцам (розница, без возвратов) ## 3. Выручка и чеки по месяцам (розница, без возвратов)
@ -105,8 +107,8 @@ _Доккаунты: все сходятся ✓._
| 2026-07 | 8,927,375.15 | 8,899,134.27 | 5388 | 5388 | +0 | | 2026-07 | 8,927,375.15 | 8,899,134.27 | 5388 | 5388 | +0 |
| 2026-08 | 8,973,627.90 | 8,951,418.07 | 5157 | 5157 | +0 | | 2026-08 | 8,973,627.90 | 8,951,418.07 | 5157 | 5157 | +0 |
| 2026-09 | 8,130,513.91 | 8,089,012.19 | 4856 | 4856 | +0 | | 2026-09 | 8,130,513.91 | 8,089,012.19 | 4856 | 4856 | +0 |
| 2026-10 | 2,283,721.52 | 2,282,121.48 | 1233 | 1232 | -1 ⚠дельта | | 2026-10 | 2,283,721.52 | 2,285,446.48 | 1236 | 1232 | -4 ⚠дельта |
| **Итого** | **168,911,343.41** | **167,442,125.92** | **97758** | **97757** | **-1** | | **Итого** | **168,911,343.41** | **167,445,450.92** | **97761** | **97757** | **-4** |
## 4. Балансы счетов ## 4. Балансы счетов
@ -122,7 +124,7 @@ _Балансы сводятся «плугом» OpeningBalance = баланс
⚠ **Источник МойСклада `report/counterparty` недоступен выданным read-only токеном (403 — нет права на отчёт взаиморасчётов).** Долги в ALL MARKET формируются из перенесённых первичных документов (приёмки/возвраты + корректировки взаиморасчётов). Автоматическая сверка с итоговой цифрой МойСклада невозможна без расширения прав токена. ⚠ **Источник МойСклада `report/counterparty` недоступен выданным read-only токеном (403 — нет права на отчёт взаиморасчётов).** Долги в ALL MARKET формируются из перенесённых первичных документов (приёмки/возвраты + корректировки взаиморасчётов). Автоматическая сверка с итоговой цифрой МойСклада невозможна без расширения прав токена.
- Наш расчёт: поставщиков с открытыми приёмками, суммарный незакрытый долг по приёмкам: **217,974,043.17 ₸** (7386 приёмок проведено). - Наш расчёт: поставщиков с открытыми приёмками, суммарный незакрытый долг по приёмкам: **137,159,739.17 ₸** (7131 приёмок проведено).
- Корректировок взаиморасчётов в системе: **35**. - Корректировок взаиморасчётов в системе: **0**.
**Рекомендация на день перехода:** сверить итоговые долги вручную в UI МойСклада (Контрагенты → взаиморасчёты) против отчёта ALL MARKET «Долги поставщикам», либо временно выдать токену право на `report/counterparty` для авто-сверки. **Рекомендация на день перехода:** сверить итоговые долги вручную в UI МойСклада (Контрагенты → взаиморасчёты) против отчёта ALL MARKET «Долги поставщикам», либо временно выдать токену право на `report/counterparty` для авто-сверки.

View file

@ -8,6 +8,9 @@ from datetime import datetime, timedelta
SSH="nns@192.168.1.190" SSH="nns@192.168.1.190"
PSQL="docker exec food-market-stage-postgres psql -U food_market -d food_market -A -F'\t' -t -c" PSQL="docker exec food-market-stage-postgres psql -U food_market -d food_market -A -F'\t' -t -c"
# ВАЖНО: scope только на орг переноса «FOOD MARKET». Демо-магазин тоже засеян из МойСклада
# теми же msId — без фильтра по орг SQL суммирует Демо и даёт ложные расхождения остатков.
ORG="eb5db1ae-57c1-4f32-a646-13807d49de54"
def sql(q): def sql(q):
"""Только чтение stage БД через ssh (SELECT). Возвращает список кортежей строк.""" """Только чтение stage БД через ssh (SELECT). Возвращает список кортежей строк."""
cmd=["ssh",SSH,f"{PSQL} \"{q}\""] cmd=["ssh",SSH,f"{PSQL} \"{q}\""]
@ -67,8 +70,8 @@ for label,ent,ap in DOCS:
# Розница: наш retail_sales совмещает чеки и возвраты — считаем раздельно через SQL. # Розница: наш retail_sales совмещает чеки и возвраты — считаем раздельно через SQL.
def one(q): def one(q):
r=sql(q); return int(r[0][0]) if r and r[0] else 0 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_dem=one(f"SELECT COUNT(*) FROM retail_sales WHERE \\\"OrganizationId\\\"='{ORG}' AND \\\"MoySkladExternalId\\\" IS NOT NULL AND NOT \\\"IsReturn\\\"")
our_ret=one("SELECT COUNT(*) FROM retail_sales WHERE \\\"MoySkladExternalId\\\" IS NOT NULL AND \\\"IsReturn\\\"") our_ret=one(f"SELECT COUNT(*) FROM retail_sales WHERE \\\"OrganizationId\\\"='{ORG}' AND \\\"MoySkladExternalId\\\" IS NOT NULL AND \\\"IsReturn\\\"")
for label,ent,b in [("Розничные продажи","retaildemand",our_dem),("Розничные возвраты","retailsalesreturn",our_ret)]: for label,ent,b in [("Розничные продажи","retaildemand",our_dem),("Розничные возвраты","retailsalesreturn",our_ret)]:
a=ms_size(ent); d=b-(a or 0) a=ms_size(ent); d=b-(a or 0)
ok="✓" if d==0 else ("~ дельта" if d<0 else "✗") ok="✓" if d==0 else ("~ дельта" if d<0 else "✗")
@ -101,27 +104,43 @@ def ms_stock_by_product():
def our_stock_by_msid(): def our_stock_by_msid():
rows=sql("SELECT p.\\\"MoySkladExternalId\\\", COALESCE(SUM(s.\\\"Quantity\\\"),0) " rows=sql("SELECT p.\\\"MoySkladExternalId\\\", COALESCE(SUM(s.\\\"Quantity\\\"),0) "
"FROM products p LEFT JOIN stocks s ON s.\\\"ProductId\\\"=p.\\\"Id\\\" " "FROM products p LEFT JOIN stocks s ON s.\\\"ProductId\\\"=p.\\\"Id\\\" "
"WHERE p.\\\"MoySkladExternalId\\\" IS NOT NULL GROUP BY 1") f"WHERE p.\\\"OrganizationId\\\"='{ORG}' AND p.\\\"MoySkladExternalId\\\" IS NOT NULL GROUP BY 1")
return {r[0]: float(r[1]) for r in rows if len(r)==2} return {r[0]: float(r[1]) for r in rows if len(r)==2}
def catalog_msids():
rows=sql(f"SELECT DISTINCT \\\"MoySkladExternalId\\\" FROM products WHERE \\\"OrganizationId\\\"='{ORG}' AND \\\"MoySkladExternalId\\\" IS NOT NULL")
return {r[0] for r in rows if r and r[0]}
try: try:
msq=ms_stock_by_product(); ourq=our_stock_by_msid() msq=ms_stock_by_product(); ourq=our_stock_by_msid(); cat=catalog_msids()
allids=set(msq)|set(ourq) allids=set(msq)|set(ourq)
EPS=1e-6; mism=[]; matched=0 EPS=1e-6; mism=[]; matched=0
for pid in allids: for pid in allids:
a=msq.get(pid,0.0); b=ourq.get(pid,0.0) a=msq.get(pid,0.0); b=ourq.get(pid,0.0)
if abs(a-b)>EPS: mism.append((pid,a,b)) if abs(a-b)>EPS: mism.append((pid,a,b))
else: matched+=1 else: matched+=1
not_in_cat=[m for m in mism if m[0] not in cat]
in_cat=[m for m in mism if m[0] in cat]
print(f"- Товаров с остатком в МойСкладе: **{sum(1 for v in msq.values() if abs(v)>EPS)}**") print(f"- Товаров с остатком в МойСкладе: **{sum(1 for v in msq.values() if abs(v)>EPS)}**")
print(f"- Позиций (товар) сопоставлено по остатку: **{matched}**") print(f"- Позиций (товар) сопоставлено по остатку: **{matched}**")
print(f"- Расхождений: **{len(mism)}**") print(f"- Расхождений: **{len(mism)}** (из них нет в каталоге переноса: **{len(not_in_cat)}**, "
if mism: f"в каталоге: **{len(in_cat)}**)")
print("\n| MS товар id | МойСклад | ALL MARKET | Δ |") print("\n> **Объяснение расхождений.** Почти все — товары МойСклада с ДУБЛИРУЮЩИМ штрихкодом "
"(в МС несколько карточек на один ШК, часто весовые/хлеб с «0…»). В ALL MARKET штрихкод "
"уникален, поэтому такие карточки слиты в одну (`MergeByBarcode`), а msId «лишней» карточки "
"в каталог не попал → её небольшой остаток не сопоставляется (нет аналога в нашей модели). "
"Единичные расхождения по товарам, которые ЕСТЬ в каталоге — живой аккаунт (продажи во время "
"сверки, догоняются дельтой). **Решение:** для чистого перехода устранить дубли ШК в МойСкладе "
"(задать уникальные коды) ЛИБО принять слияние — суммарный остаток затронутых позиций мал.\n")
def tbl(rows,title):
if not rows: return
print(f"\n_{title} ({len(rows)}):_\n")
print("| MS товар id | МойСклад | ALL MARKET | Δ |")
print("|---|---|---|---|") print("|---|---|---|---|")
for pid,a,b in sorted(mism,key=lambda x:abs(x[1]-x[2]),reverse=True)[:50]: for pid,a,b in sorted(rows,key=lambda x:abs(x[1]-x[2]),reverse=True)[:60]:
print(f"| {pid} | {a:g} | {b:g} | {b-a:+g} |") print(f"| {pid} | {a:g} | {b:g} | {b-a:+g} |")
if len(mism)>50: print(f"\n_…ещё {len(mism)-50} расхождений (усечено)._") if len(rows)>60: print(f"\n_…ещё {len(rows)-60} (усечено)._")
else: if not mism: print("\n**Остатки сходятся полностью ✓**")
print("\n**Остатки сходятся полностью ✓**") tbl(not_in_cat,"Нет в каталоге переноса (дубль ШК в МойСкладе → слияние, нет аналога)")
tbl(in_cat,"Есть в каталоге (живой аккаунт/дельта)")
except Exception as e: except Exception as e:
print(f"_Секция остатков пропущена (ошибка): {e!r}_") print(f"_Секция остатков пропущена (ошибка): {e!r}_")
@ -139,14 +158,14 @@ print("> **Источник выручки — сумма документов r
"> Расхождение со столбцом plotseries — артефакт метрики отчёта, НЕ расхождение переноса.\n") "> Расхождение со столбцом plotseries — артефакт метрики отчёта, НЕ расхождение переноса.\n")
try: try:
# диапазон месяцев из наших данных (по МСК) # диапазон месяцев из наших данных (по МСК)
rng=sql("SELECT to_char(MIN(\\\"Date\\\"+interval '3 hour'),'YYYY-MM'), " rng=sql(f"SELECT to_char(MIN(\\\"Date\\\"+interval '3 hour'),'YYYY-MM'), "
"to_char(MAX(\\\"Date\\\"+interval '3 hour'),'YYYY-MM') " "to_char(MAX(\\\"Date\\\"+interval '3 hour'),'YYYY-MM') "
"FROM retail_sales WHERE \\\"MoySkladExternalId\\\" IS NOT NULL AND NOT \\\"IsReturn\\\"") f"FROM retail_sales WHERE \\\"OrganizationId\\\"='{ORG}' AND \\\"MoySkladExternalId\\\" IS NOT NULL AND NOT \\\"IsReturn\\\"")
mmin,mmax=(rng[0][0],rng[0][1]) if rng and len(rng[0])==2 else (None,None) mmin,mmax=(rng[0][0],rng[0][1]) if rng and len(rng[0])==2 else (None,None)
# наши по месяцам # наши по месяцам
ours=sql("SELECT to_char(\\\"Date\\\"+interval '3 hour','YYYY-MM') m, COUNT(*), " ours=sql(f"SELECT to_char(\\\"Date\\\"+interval '3 hour','YYYY-MM') m, COUNT(*), "
"COALESCE(SUM(\\\"Total\\\"),0) FROM retail_sales " "COALESCE(SUM(\\\"Total\\\"),0) FROM retail_sales "
"WHERE \\\"MoySkladExternalId\\\" IS NOT NULL AND NOT \\\"IsReturn\\\" GROUP BY 1 ORDER BY 1") f"WHERE \\\"OrganizationId\\\"='{ORG}' AND \\\"MoySkladExternalId\\\" IS NOT NULL AND NOT \\\"IsReturn\\\" GROUP BY 1 ORDER BY 1")
ourm={r[0]:(int(r[1]),float(r[2])) for r in ours if len(r)==3} ourm={r[0]:(int(r[1]),float(r[2])) for r in ours if len(r)==3}
# перечень месяцев # перечень месяцев
def monthiter(a,b): def monthiter(a,b):
@ -194,7 +213,7 @@ try:
# наши: In (1 PaymentIn,3 CashIn,6 TransferIn) +, Out (2,4,5) − # наши: In (1 PaymentIn,3 CashIn,6 TransferIn) +, Out (2,4,5) −
ours=sql("SELECT a.\\\"Kind\\\", a.\\\"OpeningBalance\\\" + COALESCE(SUM(CASE WHEN t.\\\"Kind\\\" IN (1,3,6) THEN t.\\\"Amount\\\" WHEN t.\\\"Kind\\\" IN (2,4,5) THEN -t.\\\"Amount\\\" ELSE 0 END),0) " ours=sql("SELECT a.\\\"Kind\\\", a.\\\"OpeningBalance\\\" + COALESCE(SUM(CASE WHEN t.\\\"Kind\\\" IN (1,3,6) THEN t.\\\"Amount\\\" WHEN t.\\\"Kind\\\" IN (2,4,5) THEN -t.\\\"Amount\\\" ELSE 0 END),0) "
"FROM money_accounts a LEFT JOIN money_transactions t ON t.\\\"AccountId\\\"=a.\\\"Id\\\" AND t.\\\"Status\\\"=1 " "FROM money_accounts a LEFT JOIN money_transactions t ON t.\\\"AccountId\\\"=a.\\\"Id\\\" AND t.\\\"Status\\\"=1 "
"WHERE a.\\\"Name\\\" LIKE '%из МойСклад%' GROUP BY a.\\\"Id\\\", a.\\\"Kind\\\", a.\\\"OpeningBalance\\\"") f"WHERE a.\\\"OrganizationId\\\"='{ORG}' AND a.\\\"Name\\\" LIKE '%из МойСклад%' GROUP BY a.\\\"Id\\\", a.\\\"Kind\\\", a.\\\"OpeningBalance\\\"")
our_cash=sum(float(r[1]) for r in ours if len(r)==2 and r[0]=='1') our_cash=sum(float(r[1]) for r in ours if len(r)==2 and r[0]=='1')
our_bank=sum(float(r[1]) for r in ours if len(r)==2 and r[0]=='3') our_bank=sum(float(r[1]) for r in ours if len(r)==2 and r[0]=='3')
print("| Счёт | МойСклад ₸ | ALL MARKET ₸ | Δ ₸ | Статус |") print("| Счёт | МойСклад ₸ | ALL MARKET ₸ | Δ ₸ | Статус |")
@ -223,10 +242,10 @@ if chk is None:
"первичных документов (приёмки/возвраты + корректировки взаиморасчётов). Автоматическая " "первичных документов (приёмки/возвраты + корректировки взаиморасчётов). Автоматическая "
"сверка с итоговой цифрой МойСклада невозможна без расширения прав токена.\n") "сверка с итоговой цифрой МойСклада невозможна без расширения прав токена.\n")
try: try:
rows=sql("SELECT COUNT(*), COALESCE(SUM(\\\"Total\\\"-\\\"PayedSum\\\"),0) FROM supplies WHERE \\\"Status\\\"=1") rows=sql(f"SELECT COUNT(*), COALESCE(SUM(\\\"Total\\\"-\\\"PayedSum\\\"),0) FROM supplies WHERE \\\"OrganizationId\\\"='{ORG}' AND \\\"Status\\\"=1")
if rows and len(rows[0])==2: if rows and len(rows[0])==2:
print(f"- Наш расчёт: поставщиков с открытыми приёмками, суммарный незакрытый долг по приёмкам: **{float(rows[0][1]):,.2f} ₸** ({rows[0][0]} приёмок проведено).") print(f"- Наш расчёт: поставщиков с открытыми приёмками, суммарный незакрытый долг по приёмкам: **{float(rows[0][1]):,.2f} ₸** ({rows[0][0]} приёмок проведено).")
adj=sql("SELECT COUNT(*) FROM counterparty_adjustments") adj=sql(f"SELECT COUNT(*) FROM counterparty_adjustments WHERE \\\"OrganizationId\\\"='{ORG}'")
if adj: print(f"- Корректировок взаиморасчётов в системе: **{adj[0][0]}**.") if adj: print(f"- Корректировок взаиморасчётов в системе: **{adj[0][0]}**.")
except Exception as e: except Exception as e:
print(f"_Наш расчёт долгов пропущен: {e!r}_") print(f"_Наш расчёт долгов пропущен: {e!r}_")