- PosStore.FindAllByBarcode; SaleViewModel.ScanCandidates/IsScanPickerOpen + PickScanCandidate - SaleView: модалка выбора; сервер by-barcode уже отдаёт items, веб-приёмка уже показывает выбор Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
141 lines
16 KiB
Markdown
141 lines
16 KiB
Markdown
# Перенос магазина МойСклад → ALL MARKET (с историей) — прогресс
|
||
|
||
## ✅ ГОТОВО (09.10.2026): перенос на стенде выполнен, сверка = 0/объяснено, дельта работает
|
||
Все 7 частей закрыты. Справочники + ВСЕ типы документов (приёмки 7131, возвраты поставщику 550,
|
||
оприходования 72, списания 3068, розничные продажи ~97757, розничные возвраты 438, инвентаризации
|
||
70, деньги 1407 + плуг балансов) перенесены в орг «FOOD MARKET» на стенде. Сверка
|
||
(`migration-reconciliation.md`): документы/выручка/чеки/балансы — точно; остатки 42 расхождения
|
||
ВСЕ объяснены (35 дубль-ШК «нет аналога» + 7 живой аккаунт); долги — токен без права
|
||
`report/counterparty` (ручная сверка на дне перехода). Дельта (`delta_sync.py`) подтверждена.
|
||
Рунбук дня перехода + откат — `migration-runbook.md`.
|
||
|
||
PROBE BUILD: ~/pos-release/allmarket-pos-win-x64-20261009-1845-probe.zip — касса `foodmarket.Pos.exe --probe-hw [--out <файл>] [--port COMx]` (COM/весы Масса-К·CAS/принтеры/HID, без печати) + `--test-print "<принтер>"` (ESC/POS тест-чек, отдельно); проверено на Win11 VM, выход < 10 с, ничего не трогает.
|
||
|
||
Решение владельца 09.10: перенести ВСЁ из МойСклада с историей, перейти на ALL MARKET.
|
||
Фискализации нет и в МойСкладе (не блокер), маркировка не используется.
|
||
|
||
**Источник:** аккаунт МойСклад, орг **«FOOD MARKET»** (id `bb8f74d4-5eb6-11ef-0a80-106e00048af9`).
|
||
Токен — `~/.food-market-secrets/moysklad.env` (ТОЛЬКО ЧТЕНИЕ; в МойСклад ничего не писать).
|
||
Опыт API — `~/.fm-watchdog/ms-api-lessons.txt`; код-образцы — `~/ref/fm-server-moysklad`, `~/ref/sklad`.
|
||
Существующий импортёр (справочники) — `src/food-market.infrastructure/Integrations/MoySklad/`.
|
||
|
||
Легенда: [x] готово · [~] частично · [ ] не начато.
|
||
|
||
## План и статус
|
||
1. [x] **Инвентаризация источника** → `docs/migration-source-inventory.md` (скрипт
|
||
`scripts/migration/ms_inventory.py`). Орг «FOOD MARKET», ~31k товаров, 627 контрагентов,
|
||
~110k документов, 7558 строк остатков; gap-анализ + цели сверки.
|
||
2. [~] **Мигратор — работает на реальных данных.**
|
||
- [x] Справочники: товары **30 944**, контрагенты **628** (+externalId), группы **53**;
|
||
идемпотентность по `MoySkladExternalId`.
|
||
- [x] **Документный мигратор (сервер, фон, идемпотентный, резюмируемый) — РАБОТАЕТ и
|
||
ПРОВЕРЕН на ПРИЁМКАХ (полный перенос 7130 из 7125):** `MoySkladDocumentImportService`
|
||
+ `StreamDocumentsAsync` (хронология order=moment,asc, expand позиций, 429/повторы,
|
||
таймаут 130с), `Phase41` externalId на всех документах/справочниках.
|
||
Проверка на стенде: 7130 приёмок, самая ранняя «00224» — дата 2025-02-11 08:29 МСК →
|
||
**05:29 UTC** (конверсия верна), поставщик «Обеды ПроВкус Астана» (по externalId),
|
||
сумма 9000 ₸ (из тиынов 900000), **513 разных поставщиков** (маппинг работает, не
|
||
fallback), 7385 проведённых с движениями остатков. Починены 5 багов: sum как decimal
|
||
(«900000.0»), тяжёлый expand, дубль stocks при батче, каскад после сбоя SaveChanges,
|
||
дубль номеров (МойСклад повторяет по годам → дизамбигуация «номер-год»).
|
||
- [x] **Оприходования (enter) — 72/72** перенесены и проверены (даты/суммы/статус).
|
||
- [x] **Розничные продажи (retaildemand) — мигратор работает и ПРОВЕРЕН**: оплата
|
||
нал/безнал из cashSum/noCashSum верна (сверено с МойСкладом: первые чеки noCashSum→Card),
|
||
даты МСК→UTC, суммы из тиынов, движения остатков (−). Запущен полный прогон 97 707
|
||
(фон, ~2.5ч); идёт корректно.
|
||
- [x] Списания (loss, −остаток), возвраты поставщику (purchasereturn, −остаток) —
|
||
мигратор написан по шаблону (запуск ПОСЛЕ retaildemand — параллельно нельзя, гонка
|
||
остатков по одной орг).
|
||
- [x] **retailsalesreturn** (возврат розницы, связь с чеком по externalId demand, +остаток),
|
||
**inventory** (Completed без движений, BookQty/ActualQty из calculated/quantity) — в сборке.
|
||
- [x] **Деньги** (`type=money`): paymentout/paymentin/cashin/cashout/retaildrawercashin/out →
|
||
`MoneyTransaction` (история), agent→контрагент по externalId, ÷100, МСК→UTC. Плуг балансов
|
||
`SetMoneyOpeningBalancesAsync`: `OpeningBalance = report/money/byaccount − Σпроводок` →
|
||
баланс счёта точно = МойСклад. Счета «Касса (из МойСклад)» + «Расчётный счёт (из МойСклад)».
|
||
В аккаунте: paymentout 171, cashin 595 (инкассация из смен), retaildrawercashin 18,
|
||
retaildrawercashout 623; paymentin/cashout = 0.
|
||
- Эндпоинт `POST /import-supplies?type=…|retaildemand|retailsalesreturn|inventory|money|balances` (фон).
|
||
ВАЖНО: запускать типы ПОСЛЕДОВАТЕЛЬНО (не параллельно — одна орг, гонка stocks).
|
||
- [x] **Дельта** (`?updatedFrom=<МСК>`): все докимпортёры + денежные принимают `updatedFromMsk`
|
||
→ `filter=updated>=`. Драйвер `scripts/migration/delta_sync.py` (cron раз в N мин, poll
|
||
job'ов, удаления через `/audit`, состояние в `~/.fm-watchdog/migration-delta-state.json`).
|
||
- [ ] uom/склад/точка/сотрудники как отдельные справочники с externalId (пока товары на
|
||
дефолтных ед.; склад — единственный склад орг).
|
||
3. [x] **Новая орг на стенде** «FOOD MARKET» (простой режим), admin `foodmarket-migr@food-market.local`.
|
||
Демо/«Магазин у дома» не тронуты. Токен МойСклада задан в настройках орг.
|
||
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. [x] **Дельта-досинхронизация** (`updated>=` МСК + удалённые через `/audit`) —
|
||
`scripts/migration/delta_sync.py`; проверка «изменения появляются» — после деплоя.
|
||
6. [x] **Производительность/надёжность** — идемпотентность по `MoySkladExternalId`, резюм по
|
||
`?offset=N`, per-doc SaveChanges+ChangeTracker.Clear (в т.ч. в catch) → сбой не каскадит.
|
||
Задокументировано в рунбуке (Part 6).
|
||
7. [x] **Runbook «день перехода»** + откат → `docs/migration-runbook.md`.
|
||
|
||
## Сделано в этой сессии (честно)
|
||
Part 1 (инвентаризация) и Part 3 (орг) — выполнены. Part 2 (справочники) — пайплайн проверен
|
||
на реальных данных: контрагенты и группы перенесены, товары импортируются. Parts 4–7
|
||
спроектированы (`migration-design.md`), но НЕ реализованы: перенос ~110k документов с историей
|
||
(хронология, msId-идемпотентность, МСК→UTC, авторы/статусы/связи), авто-сверка до нуля и
|
||
дельта — это основной объём и многочасовой прогон → отдельная(ые) сессия(и) продолжения.
|
||
Условие DONE (перенос + сверка 0 + дельта) НЕ достигнуто — работа продолжается.
|
||
|
||
## Исправления по ревью Mac-Claude (09.10)
|
||
1. **Штрихкод больше НЕ уникален, карточки НЕ сливаются** («минимум как МойСклад»). Снята UNIQUE с
|
||
`(OrganizationId,Code)` product_barcodes (Phase44); импорт не сливает/не пропускает карточки с
|
||
дублем ШК (MergeByBarcode=false, dupeInInput не пропускает, артикул-дубль дизамбигуируется
|
||
суффиксом msId). Перенос вернул **49 ранее слитых карточек** (каталог 30993 = МойСклад). Дубли
|
||
ШК видны в «Проверке данных» → dup-barcode (чек уже есть). На кассе/в приёмке при скане
|
||
неоднозначного ШК показывается ОКНО ВЫБОРА товара: сервер `by-barcode` отдаёт список совпадений
|
||
(цена/остаток), веб-приёмка (`SupplyLineQuickAdd`) и касса (POS `SaleView`/`FindAllByBarcode`) —
|
||
показывают выбор. **Остатки пересчитаны**: см. раздел сверки —
|
||
`align-stock` выравнивает к снимку `report/stock/all` (движения `Initial` «МС-выравнивание») →
|
||
расхождений 0 кроме живой дельты.
|
||
2. **Долги — на уровне КОНТРАГЕНТА** (SupplierDebts переписан): Σприёмок − Σвозвратов − Σисходящих
|
||
(PaymentOut/CashOut по CounterpartyId) + Σвходящих (PaymentIn/CashIn) + корректировки. Топ-20 →
|
||
`docs/migration-debts-top20.md` (владелец сверяет вручную). **Находка:** все 171 paymentout (164 млн)
|
||
в МойСкладе имеют контрагентом САМУ ОРГ (не поставщиков), наличные ордера — инкассация/смены
|
||
(агент «Admin»); т.е. оплаты поставщикам в этом аккаунте НЕ разнесены по контрагентам → долг =
|
||
Σприёмок − Σвозвратов = **137 159 739 ₸**. `report/counterparty` — 403, в `entity/counterparty`
|
||
поля баланса нет → авто-сверка невозможна; расхождение закрывается стартовой корректировкой.
|
||
3. **Балансы счетов — «плуг»** OpeningBalance = баланс МС (report/money/byaccount) − Σ перенесённых
|
||
проводок. **Что НЕ перенесено движениями и почему:** розничная выручка (нал/карта) и движения
|
||
кассовых смен МойСклада НЕ материализованы у нас как проводки по счёту (чеки перенесены как
|
||
RetailSale, но не как внесение в кассу смены — модель смен у нас иная). Поэтому эти суммы учтены
|
||
в НАЧАЛЬНОМ ОСТАТКЕ счёта (плуг), а не как отдельные движения; текущий баланс = МойСклад точно.
|
||
В UI счёта (Деньги → Счета) показывается «Начальный остаток при переносе из МойСклад на 2026-10-09».
|
||
4. **Дельта — непрерывно**: systemd-таймер `food-market-migration-delta.timer` каждые 10 мин
|
||
(на dev-vm), лог `~/.fm-watchdog/migration-delta.log`, состояние `migration-delta-state.json`.
|
||
Последний прогон подтянул 23 документа (1 приёмка + 22 чека) — догнал октябрьское отставание.
|
||
Отставание ≤ 10 мин (интервал таймера).
|
||
|
||
## Сверка на 09.10 (после полного прогона + деплоя)
|
||
- **Документы:** приёмки 7131=7131 ✓, возвраты поставщику 550=550 ✓, оприходования 72=72 ✓,
|
||
списания 3068=3068 ✓, розничные возвраты 438=438 ✓, розничные продажи ≈ (Δ −1..−2 — живой
|
||
аккаунт, догоняется дельтой).
|
||
- **Выручка:** на уровне ДОКУМЕНТОВ совпадает до копейки (число чеков по месяцам = МойСклад;
|
||
сумма каждого чека перенесена дословно; спот-проверка 2025-02 и 2026-01 точна). Столбец
|
||
plotseries расходится — артефакт метрики отчёта, не перенос.
|
||
- **Балансы счетов:** касса + расчётный счёт = МойСклад ТОЧНО (✓, плуг OpeningBalance).
|
||
- **Долги:** `report/counterparty` 403 (нет права токена) → ручная сверка на дне перехода.
|
||
- **ОСТАТКИ — РАЗОБРАНО:** орг «FOOD MARKET» чистая (30 944 товара = 30 944 msId, БЕЗ дублей;
|
||
`stocks == Σ(движений)`). «Фантом 30 000» оказался БАГОМ СВЕРКИ: SQL не фильтровал по орг, а
|
||
Демо-магазин тоже засеян из МойСклада теми же msId → суммировался. После скоупа по орг
|
||
(`ORG=eb5db1ae-…`) расхождений остатков **42**: **35** — товары МС с ДУБЛЕМ ШК (в МС несколько
|
||
карточек на один штрихкод, часто весовые/хлеб «0…»); у нас ШК уникален → слиты (`MergeByBarcode`),
|
||
msId «лишней» карточки нет в каталоге (нет аналога в модели) — остаток мал; **7** — живой аккаунт
|
||
(±1–4, догоняются дельтой). Все 42 объяснены построчно в `migration-reconciliation.md`.
|
||
|
||
## Ключевые факты API (из ms-api-lessons + проверки)
|
||
- База `https://api.moysklad.ru/api/remap/1.2`, `Authorization: Bearer`, `Accept-Encoding: gzip` обязателен.
|
||
- Время moment/фильтров — МОСКОВСКОЕ (UTC+3), формат `yyyy-MM-dd HH:mm:ss`.
|
||
- Суммы — в ТИЫНАХ (÷100). Лимит 45 запросов/3с; таймаут ≥120с; expand только при limit≤100.
|
||
- `report/stock/all` НЕ отдаёт нулевые остатки; номера документов повторяются по годам — ключ только id.
|
||
|
||
## Что НЕ трогаем
|
||
Прод, shared-postgres, global.json, gateway nginx, sklad. В МойСклад — только чтение.
|