Решение владельца 09.10: полный перенос магазина из МойСклада в ALL MARKET с историей. - scripts/migration/ms_inventory.py — read-only инвентаризация источника. - docs/migration-source-inventory.md: орг «FOOD MARKET», ~31k товаров, 627 контрагентов, ~110k документов (97 707 чеков, 7 125 приёмок, 3 068 списаний…), 7 558 строк остатков, 1 склад/точка/орг, касса наличных, 1 тип цены; gap-анализ модели + цели сверки. - docs/migration-design.md: идемпотентность по msId, хронология документов, МСК→UTC, тиыны, резюмируемость, порядок этапов, сверка, дельта, runbook. - docs/migration-progress.md: план Part 1–7. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
63 lines
6.1 KiB
Markdown
63 lines
6.1 KiB
Markdown
# Мигратор МойСклад → ALL MARKET — дизайн
|
||
|
||
Основано на инвентаризации (`migration-source-inventory.md`): ~31k товаров, 627 контрагентов,
|
||
~110k документов (97 707 чеков). Существующий импортёр (`MoySkladImportService`) уже делает
|
||
справочники product/productfolder/counterparty с идемпотентностью по `MoySkladExternalId`.
|
||
|
||
## Принципы
|
||
- **Идемпотентность по msId:** у КАЖДОЙ перенесённой сущности хранить `MoySkladExternalId`
|
||
(уже есть у Product/ProductGroup — расширить на Counterparty/Store/RetailPoint/Employee/
|
||
UnitOfMeasure и ВСЕ документы + денежные операции). Повторный запуск — upsert по msId,
|
||
без дублей (ключ — только id МойСклада; номера документов повторяются по годам).
|
||
- **Хронологический порядок документов** (по `moment`), чтобы остатки/себестоимость/деньги
|
||
складывались корректно: справочники → смены → приёмки/оприходования/возвраты поставщику →
|
||
продажи/розн.возвраты → списания → инвентаризации → внесения/выплаты/ордера/платежи.
|
||
- **Время:** `moment` МойСклада — МСК (UTC+3). Хранить в UTC = moment − 3ч. Фильтр дельты
|
||
`updated>=` — тоже МСК.
|
||
- **Деньги:** суммы МойСклада в ТИЫНАХ → ÷100. cashSum/noCashSum чека → нал/безнал оплата.
|
||
- **Авторы/статусы/связи:** owner(кассир)→Employee; applicable→проведён; retailsalesreturn.demand→
|
||
исходный чек (связь «на основании»); комментарии (description)→Notes.
|
||
- **Резюмируемость:** курсор по сущности (последний перенесённый msId/offset/updated) в
|
||
`migration_state` (или ImportJob.progress). Падение → продолжение с места.
|
||
- **Прогресс/ошибки:** на каждую сущность — счётчики ok/skip/err + журнал ошибок (ImportJob).
|
||
|
||
## Порядок этапов (Hangfire, по шагам, каждый резюмируемый)
|
||
1. uom → UnitOfMeasure (59; маппинг кода на наши ОКЕИ, иначе по имени, иначе создать).
|
||
2. pricetype → PriceType (1 «Цена продажи» = системный).
|
||
3. productfolder → ProductGroup (есть); product (+barcodes) → Product (есть); 31k постранично.
|
||
4. counterparty → Counterparty (есть; флаги поставщик/покупатель).
|
||
5. store → Store; retailstore → RetailPoint (+ денежный счёт кассы, наличные).
|
||
6. employee → Employee/User (3; для owner чеков).
|
||
7. Документы в хронологии (батчами по msId-страницам, expand≤100): supply, purchasereturn,
|
||
enter, loss, inventory, retailshift, retaildemand, retailsalesreturn, retaildrawercashin/out,
|
||
cashin, paymentout. Каждая позиция: assortment→Product по msId, qty, price(тиыны), discount.
|
||
|
||
## Производительность
|
||
- Чтение: лимит 45 req/3с, таймаут ≥120с, повтор страниц ≤4. 110k докумов страницами по 100
|
||
(+expand) ≈ тысячи запросов → десятки минут чтения; запись батчами в БД. Полный прогон —
|
||
несколько часов, поэтому фоновый Hangfire с резюмируемым курсором, не блокирующий.
|
||
- Повторный запуск: upsert по msId, 0 дублей; измерять время в ImportJob.
|
||
|
||
## Сверка (Part 4, авто → migration-reconciliation.md)
|
||
Цели (из источника, read-only):
|
||
- Остатки: по товар×склад `report/stock/all` (7558 строк; нулевые НЕ отдаёт) == наши остатки.
|
||
- Долги: `report/counterparty` (взаиморасчёты) == наши долги поставщиков/покупателей.
|
||
- Деньги: остаток кассы точки (чеки+внесения−выплаты−ордера) == МойСклад.
|
||
- Выручка/чеки по месяцам: Σ retaildemand.sum и count по месяцам == наши.
|
||
- Число документов каждого типа == таблица инвентаризации.
|
||
Расхождения — разобрать построчно/объяснить.
|
||
|
||
## Дельта (Part 5)
|
||
После полного переноса — Hangfire раз в N минут: `filter=updated>=<последний_курсор МСК>` по
|
||
каждому типу + удалённые (через `/audit` или сверку id-множеств) → upsert/удаление. В день
|
||
перехода — финальный прогон дельты, сверка, переключение кассы на ALL MARKET.
|
||
|
||
## Runbook дня перехода (Part 7) — кратко
|
||
1. Накануне: полный перенос + сверка = 0. 2. Утром: закрыть смену в МойСкладе, финальная дельта,
|
||
сверка кассы/остатков. 3. Переключить POS на ALL MARKET-орг. 4. Проверить продажу/возврат/смену.
|
||
5. Откат: вернуть POS на МойСклад (данные там не трогались). Детали — отдельно при реализации Part 5–6.
|
||
|
||
## Статус
|
||
- [x] Part 1 (инвентаризация + цели сверки + gap-анализ).
|
||
- [ ] Part 2–7: спроектированы; реализация + прогон (часы) — продолжение после перехода-инфраструктуры.
|