# Аудит наших доменных сущностей 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` оставить + `` комментарий почему у нас есть, а у 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.