food-market/docs/audit-moysklad.md
nurdotnet 495f0aabee
Some checks failed
CI / POS (WPF, Windows) (push) Waiting to run
CI / Backend (.NET 8) (push) Failing after 1s
CI / Web (React + Vite) (push) Failing after 1m13s
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>
2026-04-23 12:57:06 +05:00

33 KiB
Raw Blame History

Аудит наших доменных сущностей 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. Поле DefaultPriceTypeIdPriceType (для опта/персональной цены).
  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 содержит:

{ "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.