docs: audit of our domain entities vs. live MoySklad API
Cross-checked every entity (Product, Counterparty, Supply, RetailSale, Stock, Store, RetailPoint, Organization, ProductGroup, Barcode, Price, PriceType, Country, Currency, VatRate, UoM) against real responses from MoySklad's API — a flat list of: - fields we have and MS doesn't (to justify or drop) - fields MS has and we don't (to add) - semantic mismatches (e.g. MS holds prices in kopecks, our decimal) Report only, no code changes — to be discussed with the user before touching models/migrations. Priorities are split into P1 (import parity: ExternalCode, Code, TrackingType enum, PaymentItemType, KZ entrepreneur type), P2 (semantic fixes: RetailSale payment sums, Overhead on supply, legal fields on Organization), P3 (nice-to-have), and a list of deliberate divergences (why our VatRate/StockMovement exist even though MS doesn't model them that way). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
afbf01304a
commit
495f0aabee
464
docs/audit-moysklad.md
Normal file
464
docs/audit-moysklad.md
Normal file
|
|
@ -0,0 +1,464 @@
|
|||
# Аудит наших доменных сущностей vs. MoySklad API
|
||||
|
||||
Источник правды — живой MoySklad API `/api/remap/1.2/`. Проверялись ключи на реальных ответах (`?limit=2` на нашем аккаунте). Цель: каждая наша сущность должна либо повторять MoySklad, либо иметь явно оправданное отличие. Никаких «выдуманных» полей.
|
||||
|
||||
Условные обозначения:
|
||||
- **⛔** — у нас есть поле, которого нет у MoySklad → либо оправдать комментарием, либо удалить.
|
||||
- **➕** — у MoySklad есть поле, которого нет у нас → потенциально добавить.
|
||||
- **⚠️** — важный нюанс (тип, семантика, обязательность).
|
||||
|
||||
---
|
||||
|
||||
## Counterparty → `entity/counterparty`
|
||||
|
||||
Ключи MoySklad (из ответа API, верхний уровень): `accountId, accounts, archived, bonusPoints, bonusProgram, companyType, created, externalCode, files, group, id, meta, name, notes, owner, salesAmount, shared, state, tags, updated` + расширяемые: `legalTitle, legalAddress, inn, kpp, ogrn, ogrnip, certificateNumber, certificateDate, phone, email, actualAddress, description, discountCardNumber, priceType, sex, salesChannel`.
|
||||
|
||||
| Наше поле | MoySklad | Комментарий |
|
||||
|---|---|---|
|
||||
| `Name` | `name` | ОК |
|
||||
| `LegalName` | `legalTitle` | rename? или доп. комментарий-алиас |
|
||||
| `Kind` (CounterpartyKind) | **нет** | ⛔ уже исправили enum (`Unspecified/Supplier/Customer/Both`), но MoySklad не имеет этого поля вообще — он различает контрагентов через `tags` или через `state` (статус в пайплайне продаж/закупок). **TODO:** либо оставить Kind только как UI-фильтр (не импортировать из MoySklad), либо перейти на теги |
|
||||
| `Type` (LegalEntity/Individual) | `companyType` | ⚠️ у MoySklad 3 значения: `legal`, `individual`, `entrepreneur` (ИП!). У нас ИП отсутствует — **добавить `IndividualEntrepreneur` в enum** (для РК актуально) |
|
||||
| `Bin` (БИН, РК) | `inn` (12-значный БИН пишется туда) | ⚠️ MoySklad для всех рынков использует `inn` — 12 цифр это ИИН РФ, 12 цифр РК — БИН. Мы вынесли `Bin` отдельно, при импорте MoySklad кладёт в `inn`. **TODO:** документировать маппинг Bin ↔ inn |
|
||||
| `Iin` (ИИН, РК) | `inn` (тот же) | ⚠️ same — MoySklad не различает |
|
||||
| `TaxNumber` | `inn` | дубль |
|
||||
| `CountryId` | `country` (extended, по `meta`) | ⚠️ MoySklad не на верхнем уровне — тянется при `?expand=country` |
|
||||
| `Address` | `actualAddress` | ОК |
|
||||
| `Phone` | `phone` | ОК |
|
||||
| `Email` | `email` | ОК |
|
||||
| `BankName, BankAccount, Bik` | `accounts` (массив объектов) | ⚠️ у MoySklad это **коллекция счетов** (до нескольких банков). У нас одиночные поля — **либо сделать коллекцию Accounts, либо документировать "берём первый"** |
|
||||
| `ContactPerson` | `contactpersons` (sub-endpoint) | ⚠️ у MoySklad это отдельный endpoint `counterparty/{id}/contactpersons` — массив. У нас скалярное поле |
|
||||
| `Notes` | `description` (или `notes` разные в разных версиях API?) | ⚠️ в ответе API было `notes` — ОК |
|
||||
| `IsActive` | `archived` (inverse) | ОК |
|
||||
| — | `tags` (массив) | ➕ **добавить** — удобно для классификации (в том числе заменой Kind) |
|
||||
| — | `state` (ссылка на состояние в пайплайне) | ➕ отложить до Phase N (CRM) |
|
||||
| — | `bonusPoints, bonusProgram, discountCardNumber` | ➕ отложить до дисконтных карт |
|
||||
| — | `salesAmount` (вычисляемое) | не храним |
|
||||
| — | `priceType` (персональный тип цены) | ➕ полезно для опта; добавить `Guid? DefaultPriceTypeId` |
|
||||
|
||||
**TODO:**
|
||||
1. Enum `CounterpartyType`: добавить `IndividualEntrepreneur = 3`.
|
||||
2. Коллекция `CounterpartyAccount` (BankName/BankAccount/Bik/IsDefault) — или явный комментарий «храним только основной».
|
||||
3. Коллекция `CounterpartyTag` (string) — для классификации при импорте из MoySklad.
|
||||
4. Поле `DefaultPriceTypeId` → `PriceType` (для опта/персональной цены).
|
||||
5. Комментарий на `Bin/Iin/TaxNumber`: при импорте из MoySklad все три могут прилететь из одного поля `inn` — логика различения по длине (12 цифр РК-формат) / по companyType.
|
||||
|
||||
---
|
||||
|
||||
## Organization → `entity/organization`
|
||||
|
||||
Ключи MS: `accountId, accounts, archived, bonusPoints, bonusProgram, companyType, companyVat__ru, created, email, externalCode, group, id, isEgaisEnable, meta, name, owner, payerVat, shared, updated` + extended: `legalTitle, legalAddress, actualAddress, inn, kpp, ogrn, ogrnip, okpo, director, chiefAccountant, phone, fax, utmUrl`.
|
||||
|
||||
| Наше | MS | Комментарий |
|
||||
|---|---|---|
|
||||
| `Name` | `name` | ОК |
|
||||
| `CountryCode` | **нет** | ⛔ у MoySklad нет — у них multi-tenant через account. У нас — multi-tenant через Organization, но CountryCode неочевиден. Оставить как есть, документировать почему (нам нужно для налоговых/локальных настроек) |
|
||||
| `Bin` | `inn` | то же что и Counterparty |
|
||||
| `Address` | `actualAddress` | ОК |
|
||||
| `Phone` | `phone` | ОК |
|
||||
| `Email` | `email` | ОК |
|
||||
| `IsActive` | `archived` inverse | ОК |
|
||||
| — | `legalTitle, legalAddress` | ➕ для офиц. документов |
|
||||
| — | `kpp, ogrn, ogrnip, okpo` | РФ-специфично, пропускаем для РК |
|
||||
| — | `payerVat` (bool, плательщик НДС) | ➕ полезно — есть ли НДС у нашей организации |
|
||||
| — | `director, chiefAccountant` | ➕ для подписей на накладных |
|
||||
| — | `accounts` (банковские) | ➕ аналогично Counterparty |
|
||||
| — | `isEgaisEnable` | РФ, пропускаем |
|
||||
|
||||
**TODO:**
|
||||
1. `LegalName`, `LegalAddress`, `PayerVat` (bool), `DirectorName`, `ChiefAccountantName` — для накладных/счетов.
|
||||
2. `CountryCode` оставить + `<see langword="…"/>` комментарий почему у нас есть, а у MS нет.
|
||||
|
||||
---
|
||||
|
||||
## Product → `entity/product`
|
||||
|
||||
Ключи MS: `accountId, archived, barcodes, buyPrice, code, discountProhibited, externalCode, files, group, id, images, isSerialTrackable, meta, minPrice, name, owner, pathName, paymentItemType, productFolder, salePrices, shared, supplier, trackingType, uom, updated, useParentVat, variantsCount, volume, weight` + optional: `article, country, description, effectiveVat, minPrice.currency, taxSystem, vat, tnved, syncId, modifications`.
|
||||
|
||||
| Наше | MS | Комментарий |
|
||||
|---|---|---|
|
||||
| `Name` | `name` | ОК |
|
||||
| `Article` | `article` | ОК |
|
||||
| `Description` | `description` | ОК |
|
||||
| `UnitOfMeasureId` | `uom.meta` | ОК |
|
||||
| `VatRateId` | `vat` (число) + `useParentVat` | ⚠️ у MS НДС хранится как число (20, 10, 12, 0) прямо на товаре. **Мы отдельная сущность VatRate**. Обоснование: нам нужно хранить локализованные названия ("НДС 12%", "Без НДС"), is-default, и позволять разным организациям иметь разные ставки. НО — при импорте надо резолвить число в VatRate по organization_id |
|
||||
| `ProductGroupId` | `productFolder.meta` | ОК |
|
||||
| `DefaultSupplierId` | `supplier.meta` | ОК (у MS тоже одиночная ссылка) |
|
||||
| `CountryOfOriginId` | `country.meta` | ОК |
|
||||
| `IsService` | `paymentItemType` (одно из значений = "SERVICE") | ⚠️ у MS это enum с ~10 значений; у нас bool. **TODO:** либо enum, либо документировать что мы учитываем только IsService |
|
||||
| `IsWeighed` | **нет** | ⛔ у MS этого нет; характеристика ритейла, нам нужно для касс с весами. **Оставить, документировать.** |
|
||||
| `IsAlcohol` | `tnved` (класс товара) или через group | ⚠️ у MS через tnved-код или through type классификаторы. Наше bool — упрощение. **Оставить с комментарием.** |
|
||||
| `IsMarked` | `trackingType` (enum: NOT_TRACKED, BEER_ALCOHOL, …) | ⚠️ У MS это enum из 10+ вариантов маркировки. Наш `IsMarked: bool` — потеря информации. **TODO:** заменить на enum `TrackingType` (NOT_TRACKED/TOBACCO/ALCOHOL/SHOES/MEDICINE/…) |
|
||||
| `MinStock, MaxStock` | `minimumBalance` (число), `stock` (runtime) | ⚠️ у MS есть только `minimumBalance` (нижняя граница). MaxStock — наш |
|
||||
| `PurchasePrice, PurchaseCurrencyId` | `buyPrice.value, buyPrice.currency.meta` | ОК (MS упаковывает в объект, мы разнесли — **одно и то же**) |
|
||||
| `ImageUrl` | `images` (массив через sub-endpoint) | ⚠️ у MS images коллекция, у нас одна + отдельная ProductImage. ОК, двойная запись для UX |
|
||||
| `IsActive` | `archived` inverse | ОК |
|
||||
| `Prices` (collection) | `salePrices` (массив inline в MS) | ⚠️ у MS цены — **массив внутри товара**, у нас — отдельная таблица. Оба норм; просто маппинг при sync |
|
||||
| `Barcodes` (collection) | `barcodes` (массив inline) | ОК |
|
||||
| `Images` (collection) | `images` (sub-endpoint) | ОК |
|
||||
| — | `code` | ➕ внутренний код (отличается от `article`). **Добавить `Code`** |
|
||||
| — | `externalCode` | ➕ используется при импорте/ERP-интеграциях. **Добавить `ExternalCode`** (актуально для импорта из MoySklad, 1C) |
|
||||
| — | `discountProhibited` | ➕ «запрет скидок» — полезно на кассе |
|
||||
| — | `minPrice.value/currency` | ➕ минимальная отпускная цена. **Добавить `MinPrice` + `MinPriceCurrencyId`** |
|
||||
| — | `paymentItemType` | ➕ для фискализации: «товар/услуга/работа/подарочная карта/…». **Добавить enum `PaymentItemType`** (нужно для 54-ФЗ / КZ fiscal receipts) |
|
||||
| — | `tnved` | ➕ код ТН ВЭД для трансграничной торговли |
|
||||
| — | `volume, weight` | ➕ для логистики (доставка) |
|
||||
| — | `variantsCount` | runtime агрегат, не храним |
|
||||
| — | `files` | ➕ вложения (паспорта качества, фото упаковки) — отложить |
|
||||
|
||||
**TODO:**
|
||||
1. Добавить `Code`, `ExternalCode` на Product.
|
||||
2. Заменить `IsMarked` на enum `TrackingType`.
|
||||
3. Добавить `MinPrice`, `MinPriceCurrencyId`.
|
||||
4. Добавить enum `PaymentItemType` + поле.
|
||||
5. Поля `Volume`, `Weight`, `DiscountProhibited`.
|
||||
6. Запомнить маппинг: `useParentVat` → наследовать НДС от ProductGroup (у нас сейчас не реализовано, надо подумать).
|
||||
|
||||
---
|
||||
|
||||
## ProductGroup → `entity/productfolder`
|
||||
|
||||
Ключи MS: `accountId, archived, externalCode, group, id, meta, name, owner, pathName, shared, updated, useParentVat` + `vat, effectiveVat, productFolder` (родитель).
|
||||
|
||||
| Наше | MS | Комментарий |
|
||||
|---|---|---|
|
||||
| `Name` | `name` | ОК |
|
||||
| `ParentId` | `productFolder.meta` | ОК (MS использует то же имя для родителя что и для самой сущности) |
|
||||
| `Path` | `pathName` | ОК |
|
||||
| `SortOrder` | **нет** | ⛔ у MS нет сортировки групп. Оставить, это UX |
|
||||
| `IsActive` | `archived` inverse | ОК |
|
||||
| — | `externalCode` | ➕ для импорта |
|
||||
| — | `vat, useParentVat` | ➕ ставка НДС по умолчанию для товаров группы |
|
||||
|
||||
**TODO:**
|
||||
1. Добавить `ExternalCode`.
|
||||
2. Добавить `VatRateId?` + `UseParentVat: bool` (для наследования).
|
||||
|
||||
---
|
||||
|
||||
## ProductBarcode → `product.barcodes[]`
|
||||
|
||||
У MS barcode — объект внутри product: `{type: 'ean13'|'ean8'|'code128'|'upc'|'gtin', value: '...'}`. Отдельной сущности нет.
|
||||
|
||||
| Наше | MS | Комментарий |
|
||||
|---|---|---|
|
||||
| `Code` | `value` | ОК |
|
||||
| `Type` | `type` | ⚠️ MS использует строки ('ean13', 'gtin', …) — мы уже enum |
|
||||
| `IsPrimary` | **нет** | ⛔ у MS нет — первый считается основным. **Оставить с комментарием — у нас явная пометка.** |
|
||||
|
||||
OK, расхождений существенных нет.
|
||||
|
||||
---
|
||||
|
||||
## ProductPrice → `product.salePrices[]`
|
||||
|
||||
У MS цены — массив объектов в product: `{value, currency: {meta}, priceType: {meta}}`. Отдельной сущности нет.
|
||||
|
||||
Наше — отдельная таблица. Это **нормализованный вариант** — оправдано если цен много и есть выборки по PriceType. **TODO:** маппинг при импорте — проитерировать salePrices и создать ProductPrice per PriceType.
|
||||
|
||||
---
|
||||
|
||||
## PriceType → `entity/pricetype`
|
||||
|
||||
Ключи MS (из context): `id, name, externalCode`.
|
||||
|
||||
| Наше | MS | Комментарий |
|
||||
|---|---|---|
|
||||
| `Name` | `name` | ОК |
|
||||
| `IsDefault` | **нет** | ⛔ у MS — default определяется порядком или отдельно в настройках аккаунта. **Оставить** |
|
||||
| `IsRetail` | **нет** | ⛔ наш флаг «используется на кассе». **Оставить** |
|
||||
| `SortOrder` | **нет** | ⛔ UX. **Оставить** |
|
||||
| — | `externalCode` | ➕ для импорта |
|
||||
|
||||
**TODO:**
|
||||
1. `ExternalCode`.
|
||||
|
||||
---
|
||||
|
||||
## Country → `entity/country`
|
||||
|
||||
Ключи MS: `code, description, externalCode, id, meta, name, updated`.
|
||||
|
||||
| Наше | MS | Комментарий |
|
||||
|---|---|---|
|
||||
| `Code` | `code` | ⚠️ у MS формат ISO3166-1 **alpha-2 или числовой** — у нас alpha-2 |
|
||||
| `Name` | `name` | ОК |
|
||||
| `SortOrder` | **нет** | ⛔ UX |
|
||||
| — | `description` | ➕ |
|
||||
| — | `externalCode` | ➕ |
|
||||
|
||||
OK, мелочь.
|
||||
|
||||
---
|
||||
|
||||
## Currency → `entity/currency`
|
||||
|
||||
Ключи MS: `archived, code, default, fullName, id, indirect, isoCode, majorUnit, meta, minorUnit, multiplicity, name, rate, rateUpdateType, system`.
|
||||
|
||||
| Наше | MS | Комментарий |
|
||||
|---|---|---|
|
||||
| `Code` | `isoCode` или `code` | ⚠️ у MS `isoCode` (строка "KZT") и `code` (цифровой "398") — у нас `Code` = строка ISO |
|
||||
| `Name` | `name` | ОК |
|
||||
| `Symbol` | **нет** | ⛔ у MS нет символа "₸" — но это UX. **Оставить** |
|
||||
| `MinorUnit` | `minorUnit` | ОК |
|
||||
| `IsActive` | `archived` inverse | ОК |
|
||||
| — | `default` (валюта аккаунта) | ➕ |
|
||||
| — | `rate, rateUpdateType` | ➕ курс к базовой валюте (при мульти-валютности) |
|
||||
| — | `multiplicity, indirect` | конвертация; если не мульти-валютные — не надо |
|
||||
| — | `fullName` | ➕ «Тенге Казахстана» vs «KZT» |
|
||||
|
||||
**TODO:**
|
||||
1. Добавить `IsDefault: bool` (ровно одна валюта = true per tenant, или глобально).
|
||||
2. `Rate, RateUpdateType` + `FullName` — отложить до мульти-валютности.
|
||||
|
||||
---
|
||||
|
||||
## VatRate — у MoySklad нет `entity/vatrate`
|
||||
|
||||
⚠️ У MS **ставки НДС хранятся как числовое поле на товаре** (`vat`). Отдельной таблицы нет — набор значений {0, 5, 7, 10, 12, 18, 20} и «без НДС» встроен в систему.
|
||||
|
||||
Наше `VatRate` — отдельная сущность. **Обоснование сохранить:**
|
||||
1. Локализованное название ("НДС 12%", "Без НДС").
|
||||
2. IsDefault per organization.
|
||||
3. Разные организации в разных налоговых режимах (с НДС / УСН).
|
||||
4. При добавлении новой ставки (например, на случай гипотетического увеличения в РК) — не править перечисление в коде.
|
||||
|
||||
Но: **следите**, чтобы у товара хранился `VatRateId`, а не отдельно `vat: decimal`. При импорте из MS мапим число в запись VatRate.
|
||||
|
||||
**Комментарий в коде нужен** — явно сказать, что мы отклонились от MoySklad сознательно.
|
||||
|
||||
---
|
||||
|
||||
## UnitOfMeasure → `entity/uom`
|
||||
|
||||
Ключи MS: `code, description, externalCode, id, meta, name, updated`.
|
||||
|
||||
| Наше | MS | Комментарий |
|
||||
|---|---|---|
|
||||
| `Code` (ОКЕИ) | `code` | ОК, MS использует ОКЕИ-коды (796, 166, 112) |
|
||||
| `Symbol` | **нет** | ⛔ у MS только `name` ("штука"). Мы вынесли "шт" отдельно для коротких надписей на ценниках/кассовых чеках. **Оставить.** |
|
||||
| `Name` | `name` | ОК |
|
||||
| `DecimalPlaces` | **нет** | ⛔ у MS на уровне продукта (`variantsCount`?), а не UoM. Наш `DecimalPlaces` определяет можно ли дробные количества (0=штучный, 3=весовой). **Оставить — важно для UX касс.** |
|
||||
| `IsBase` | **нет** | ⛔ наше «базовая единица организации». Мелочь, оставить |
|
||||
| `IsActive` | `archived` inverse (у MS есть `archived` в uom? перепроверить) | ⚠️ в нашем ответе API archived не было — у MS uom этого поля может не быть, потому что единицы системные |
|
||||
| — | `description` | ➕ |
|
||||
| — | `externalCode` | ➕ |
|
||||
|
||||
**TODO:**
|
||||
1. `ExternalCode`.
|
||||
|
||||
---
|
||||
|
||||
## Store → `entity/store`
|
||||
|
||||
Ключи MS: `accountId, address, archived, externalCode, group, id, meta, name, owner, pathName, shared, slots, updated, zones`.
|
||||
|
||||
| Наше | MS | Комментарий |
|
||||
|---|---|---|
|
||||
| `Name` | `name` | ОК |
|
||||
| `Code` | `externalCode`? или отдельно? | ⚠️ у MS только `externalCode`. **Добавить ExternalCode или rename Code→ExternalCode** |
|
||||
| `Kind` (Warehouse/RetailFloor) | **нет** | ⛔ у MS такого деления нет. Обоснование: нам нужно отличать «склад» от «торгового зала» для UI и настроек касс. **Оставить с комментарием** |
|
||||
| `Address` | `address` | ОК |
|
||||
| `Phone` | **нет** | ⛔ у MS нет. Оставить |
|
||||
| `ManagerName` | **нет** | ⛔ у MS нет. Оставить |
|
||||
| `IsMain` | **нет** (но можно проставить через default) | ⛔ Оставить |
|
||||
| `IsActive` | `archived` inverse | ОК |
|
||||
| — | `pathName` | ➕ (если будут иерархические склады) |
|
||||
| — | `slots` (ячейки склада) | ➕ отложить |
|
||||
| — | `zones` (зоны склада) | ➕ отложить |
|
||||
|
||||
**TODO:**
|
||||
1. `ExternalCode` (или переименовать Code → ExternalCode).
|
||||
|
||||
---
|
||||
|
||||
## RetailPoint → `entity/retailstore`
|
||||
|
||||
У MS это **«Точка продаж» / кассовое место**. Огромное количество полей (~60) — в основном фискальные настройки.
|
||||
|
||||
| Наше | MS | Комментарий |
|
||||
|---|---|---|
|
||||
| `Name` | `name` | ОК |
|
||||
| `Code` | `externalCode` | rename or add |
|
||||
| `StoreId` | `store.meta` | ОК |
|
||||
| `Address` | **нет** (возможно `organization.actualAddress`) | ⛔ адрес не у точки, а у организации/склада. Пересмотреть, куда класть |
|
||||
| `Phone` | **нет** | ⛔ |
|
||||
| `FiscalSerial` | **нет такого поля**; есть `fiscalType`, `fiscalMemoryNumber`?, `ofdEnabled` | ⚠️ у MS фискальные настройки множественные. У нас один скаляр — упрощение. **TODO:** уточнить по мере подключения ККМ |
|
||||
| `FiscalRegNumber` | `ofdEnabled` + `ofdSettings` | same |
|
||||
| `IsActive` | `active, archived` | MS различает active и archived — у нас только IsActive |
|
||||
| — | `priceType.meta` | ➕ тип цены для этой точки — **важно** |
|
||||
| — | `allowCustomPrice` | ➕ разрешить ручную цену на кассе |
|
||||
| — | `allowCreateProducts` | ➕ создать товар прямо на кассе |
|
||||
| — | `discountEnable, discountMaxPercent` | ➕ скидки на кассе |
|
||||
| — | `cashiers` (коллекция) | ➕ кто может работать за кассой |
|
||||
| — | `sellReserves` | ➕ продавать резерв |
|
||||
| — | `receiptTemplate` | ➕ шаблон чека |
|
||||
| — | `returnFromClosedShiftEnabled` | ➕ возврат из закрытой смены |
|
||||
| — | `requiredBirthdate/Email/Phone/Fio/Sex/DiscountCardNumber` | ➕ обязательные поля при продаже |
|
||||
| — | `markingSellingMode, marksCheckMode, sendMarksForCheck` | ➕ маркировка товаров |
|
||||
|
||||
**TODO:**
|
||||
1. Обязательно: `DefaultPriceTypeId` (ссылка на `PriceType`).
|
||||
2. Настройки кассы (скоп Phase 3 — касса): `AllowCustomPrice`, `AllowCreateProducts`, `SellReserves`, `DiscountMaxPercent`, `RequireCustomer...` — добавлять по мере реализации POS.
|
||||
3. Коллекция `RetailPointCashier` (user_id, может ли работать).
|
||||
|
||||
---
|
||||
|
||||
## Supply → `entity/supply` + `supply/{id}/positions`
|
||||
|
||||
Document keys: `accountId, agent, applicable, created, externalCode, files, group, id, meta, moment, name, organization, owner, payedSum, positions, printed, published, rate, shared, store, sum, updated, vatEnabled, vatIncluded, vatSum`.
|
||||
|
||||
| Наше | MS | Комментарий |
|
||||
|---|---|---|
|
||||
| `Number` | `name` | ⚠️ у MS «номер документа» = `name`. У нас `Number` — семантически то же |
|
||||
| `Date` | `moment` | ОК |
|
||||
| `Status` (Draft/Posted) | `applicable` (bool) | ⚠️ у MS это bool «проведён или нет». У нас enum Draft/Posted → эквивалентно |
|
||||
| `SupplierId` | `agent.meta` | ⚠️ у MS вместо `supplier` общее слово `agent` (контрагент) |
|
||||
| `StoreId` | `store.meta` | ОК |
|
||||
| `CurrencyId` | `rate.currency.meta` | ⚠️ MS упаковывает в rate объект с курсом |
|
||||
| `SupplierInvoiceNumber` | **нет на верхнем уровне**; есть в `attributes` | ⛔ у MS через custom attributes. Оставить |
|
||||
| `SupplierInvoiceDate` | same | same |
|
||||
| `Notes` | `description` | rename или комментарий |
|
||||
| `Total` | `sum` | ОК |
|
||||
| `PostedAt` | `updated` (когда applicable ставится true) | ⚠️ у MS нет выделенного поля; мы отдельно фиксируем |
|
||||
| `PostedByUserId` | `owner.meta` | условно |
|
||||
| — | `vatEnabled` | ➕ |
|
||||
| — | `vatIncluded` | ➕ НДС включён в цену |
|
||||
| — | `vatSum` | ➕ суммарный НДС документа |
|
||||
| — | `payedSum` | ➕ сколько оплачено |
|
||||
| — | `organization.meta` | ⚠️ у MS документ привязан к организации. **У нас TenantEntity несёт OrganizationId — уже есть** |
|
||||
| — | `printed, published` | ➕ распечатан/опубликован |
|
||||
| — | `overhead` (доп.расходы) | ➕ доставка/таможня — **важно для фактической себестоимости** |
|
||||
|
||||
**Supply.Positions (SupplyLine) → supply/{id}/positions:**
|
||||
|
||||
Ключи MS: `accountId, assortment, discount, id, meta, overhead, price, quantity, vat, vatEnabled`.
|
||||
|
||||
| Наше (SupplyLine) | MS position | Комментарий |
|
||||
|---|---|---|
|
||||
| `ProductId` | `assortment.meta` | ⚠️ у MS `assortment` = может быть product ИЛИ variant ИЛИ service ИЛИ bundle. Мы только продукт |
|
||||
| `Quantity` | `quantity` | ОК |
|
||||
| `UnitPrice` | `price` | ⚠️ у MS `price` — в копейках (integer `100 = 1.00`). У нас decimal. **Маппинг при импорте: делить на 100** |
|
||||
| `LineTotal` | **нет** (вычисляется) | ⛔ у MS не хранится |
|
||||
| `SortOrder` | **нет** | ⛔ наш UX |
|
||||
| — | `discount` | ➕ строковая скидка |
|
||||
| — | `vat` | ➕ ставка НДС на позицию |
|
||||
| — | `vatEnabled` | ➕ |
|
||||
| — | `overhead` | ➕ доля накладных (для себестоимости) |
|
||||
|
||||
**TODO Supply:**
|
||||
1. Поля: `VatEnabled`, `VatIncluded`, `VatSum`, `PayedSum`, `Overhead`.
|
||||
2. Lines: `Discount` (decimal), `VatPercent` (snapshot, уже подобное есть в RetailSaleLine), `VatEnabled`.
|
||||
3. Комментарий: MS `price` в копейках — при импорте делить.
|
||||
|
||||
---
|
||||
|
||||
## RetailSale → `entity/retaildemand` + `retaildemand/{id}/positions`
|
||||
|
||||
Document keys: огромный список, ключевое: `agent, applicable, cashSum, noCashSum, qrSum, prepaymentCashSum, prepaymentNoCashSum, prepaymentQrSum, advancePaymentSum, fiscal, retailShift, retailStore, store, positions, rate, sum, vatEnabled, vatIncluded, vatSum, name, moment, organization, syncId`.
|
||||
|
||||
| Наше | MS | Комментарий |
|
||||
|---|---|---|
|
||||
| `Number` | `name` | ОК |
|
||||
| `Date` | `moment` | ОК |
|
||||
| `Status` | `applicable` | ⚠️ bool vs enum |
|
||||
| `StoreId` | `store.meta` | ОК |
|
||||
| `RetailPointId` | `retailStore.meta` | ОК |
|
||||
| `CustomerId` | `agent.meta` | ОК (nullable если не знаем покупателя) |
|
||||
| `CashierUserId` | **нет напрямую**; `retailShift` → cashier | ⚠️ |
|
||||
| `CurrencyId` | `rate.currency.meta` | ОК |
|
||||
| `Subtotal, DiscountTotal, Total` | `sum` (= Total) | ⚠️ MS **не хранит subtotal и discount total отдельно** — только total. Но цена в позиции уже после скидки? Нет — `positions[].discount` хранится, total = sum(price*qty - discount) |
|
||||
| `Payment` (PaymentMethod enum) | **cashSum + noCashSum + qrSum** | ⚠️ MS — **не enum, а суммы по видам оплаты**. Т.е. при mixed-оплате можно часть наличными + часть картой. **Наш enum Payment + PaidCash + PaidCard — неполный.** TODO: добавить `PaidQr` + убрать enum в пользу «сколько чем заплачено» |
|
||||
| `PaidCash` | `cashSum` | ⚠️ у MS в копейках |
|
||||
| `PaidCard` | `noCashSum` | ⚠️ в копейках |
|
||||
| `Notes` | `description` | ОК |
|
||||
| `PostedAt` | — | наш |
|
||||
| `PostedByUserId` | `owner.meta` | условно |
|
||||
| — | `qrSum` | ➕ **добавить `PaidQr`** (QR-оплата актуальна для КZ) |
|
||||
| — | `retailShift.meta` | ➕ кассовая смена (отложить) |
|
||||
| — | `fiscal` | ➕ пробит ли фискально |
|
||||
| — | `syncId` | ➕ идентификатор для офлайн-касс (при резинхроне) |
|
||||
| — | `prepaymentCashSum/NoCashSum/QrSum, advancePaymentSum` | ➕ предоплаты |
|
||||
|
||||
**RetailSale.Positions (RetailSaleLine) → retaildemand/{id}/positions:**
|
||||
|
||||
Ключи: `accountId, assortment, discount, id, meta, price, quantity, vat, vatEnabled`.
|
||||
|
||||
| Наше (RetailSaleLine) | MS | Комментарий |
|
||||
|---|---|---|
|
||||
| `ProductId` | `assortment.meta` | ОК |
|
||||
| `Quantity` | `quantity` | ОК |
|
||||
| `UnitPrice` | `price` | ⚠️ копейки |
|
||||
| `Discount` | `discount` | ОК |
|
||||
| `LineTotal` | вычисляется | наш |
|
||||
| `VatPercent` | `vat` | ОК (snapshot) |
|
||||
| `SortOrder` | — | наш UX |
|
||||
| — | `vatEnabled` | ➕ |
|
||||
|
||||
**TODO RetailSale:**
|
||||
1. Добавить `PaidQr: decimal`.
|
||||
2. **Убрать `PaymentMethod` enum** в пользу денормализованных `PaidCash, PaidCard, PaidQr, PaidBonus` + computed `Method` (если все кроме одного = 0 → Cash/Card/QR, иначе Mixed). Либо оставить enum + PayHint, но быть готовым к частичной оплате.
|
||||
3. `VatEnabled, VatIncluded, VatSum` (сумма НДС на документ — вычисляется).
|
||||
4. Комментарий: MS `price/cashSum/noCashSum` в копейках при импорте.
|
||||
|
||||
---
|
||||
|
||||
## Stock → `report/stock/bystore`
|
||||
|
||||
У MS **нет отдельной сущности "Stock"** — это **отчёт**. Ответ `report/stock/bystore` содержит:
|
||||
```json
|
||||
{ "meta": {...}, "stockByStore": [ { "name": "Склад №1", "meta": {...}, "stock": 10.0, "reserve": 2.0, "inTransit": 0.0, "quantity": 12.0 } ] }
|
||||
```
|
||||
Т.е. по каждому (product, store) — stock (сколько есть), reserve (резерв), inTransit (в пути), quantity = stock+inTransit.
|
||||
|
||||
У нас `Stock` — **материализованный агрегат** (Quantity, ReservedQuantity, computed Available). Это **технически наше решение**, не требование бизнеса.
|
||||
|
||||
**TODO:**
|
||||
1. Комментарий в Stock.cs: объяснить, что это материализация (у MS динамический репорт).
|
||||
2. **Добавить `InTransit: decimal`** — товар в пути (между складами при перемещении).
|
||||
|
||||
---
|
||||
|
||||
## StockMovement — у MoySklad такой сущности нет
|
||||
|
||||
⚠️ MS **не хранит journal движений** в явном виде — остатки рассчитываются в реальном времени из документов (supply, retaildemand, loss, enter, move и т.д.). Поэтому на вопрос «почему на складе минус 5» MS возвращает историю документов, а не journal.
|
||||
|
||||
Наше `StockMovement` — **явный immutable journal**. Обоснование:
|
||||
1. Мгновенный ответ на «почему такой остаток» без пробегания по всем документам.
|
||||
2. Атомарные корректировки при баг-фиксах миграций.
|
||||
3. Упрощённая репликация в офлайн-кассы.
|
||||
|
||||
Это **сознательное отклонение** от MS — должно быть задокументировано в коде и в `docs/`. **TODO:** комментарий в StockMovement.cs + упоминание в `CLAUDE.md`.
|
||||
|
||||
---
|
||||
|
||||
## Свод по приоритетам
|
||||
|
||||
### Приоритет 1 — базовая совместимость импорта (на этой неделе):
|
||||
- Product: `Code`, `ExternalCode`, `TrackingType` (enum) вместо `IsMarked`, `MinPrice`/`MinPriceCurrencyId`, `PaymentItemType` (enum)
|
||||
- Counterparty: `CounterpartyType.IndividualEntrepreneur`, `ExternalCode`, tags (коллекция)
|
||||
- ProductGroup: `ExternalCode`
|
||||
- PriceType: `ExternalCode`
|
||||
- Country, Currency, UnitOfMeasure, Store: `ExternalCode`
|
||||
- RetailPoint: `DefaultPriceTypeId`
|
||||
|
||||
### Приоритет 2 — смысловые (следующая итерация):
|
||||
- RetailSale: `PaidQr`, убрать enum PaymentMethod в пользу суммовых полей
|
||||
- Supply: `Overhead`, `VatSum`, `VatEnabled`, `VatIncluded`
|
||||
- Organization: `LegalName`, `LegalAddress`, `PayerVat`, `DirectorName`
|
||||
- Product: `Volume, Weight, DiscountProhibited`
|
||||
- Stock: `InTransit`
|
||||
|
||||
### Приоритет 3 — при необходимости:
|
||||
- Counterparty: коллекция `Account`, `DefaultPriceType`
|
||||
- ProductGroup: `VatRateId?` + `UseParentVat`
|
||||
- RetailPoint: кассовые настройки (allowCustomPrice, discountMaxPercent, cashiers...)
|
||||
- Store: slots, zones
|
||||
|
||||
### Сознательно не копируем MS:
|
||||
- `CounterpartyKind` (Supplier/Customer/Both) — у нас enum, у MS теги. Оставляем для UX/фильтрации.
|
||||
- `Store.Kind` (Warehouse vs RetailFloor) — у MS нет, нам нужно.
|
||||
- `VatRate` как отдельная сущность — у MS число на товаре. У нас справочник ради локализации.
|
||||
- `StockMovement` journal — у MS нет. Выбор архитектуры.
|
||||
- `Product.IsWeighed` / `IsAlcohol` — упрощения под ритейл.
|
||||
- `UnitOfMeasure.Symbol`, `DecimalPlaces` — UX.
|
||||
Loading…
Reference in a new issue