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>
33 KiB
Аудит наших доменных сущностей 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:
- Enum
CounterpartyType: добавитьIndividualEntrepreneur = 3. - Коллекция
CounterpartyAccount(BankName/BankAccount/Bik/IsDefault) — или явный комментарий «храним только основной». - Коллекция
CounterpartyTag(string) — для классификации при импорте из MoySklad. - Поле
DefaultPriceTypeId→PriceType(для опта/персональной цены). - Комментарий на
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:
LegalName,LegalAddress,PayerVat(bool),DirectorName,ChiefAccountantName— для накладных/счетов.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:
- Добавить
Code,ExternalCodeна Product. - Заменить
IsMarkedна enumTrackingType. - Добавить
MinPrice,MinPriceCurrencyId. - Добавить enum
PaymentItemType+ поле. - Поля
Volume,Weight,DiscountProhibited. - Запомнить маппинг:
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:
- Добавить
ExternalCode. - Добавить
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:
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:
- Добавить
IsDefault: bool(ровно одна валюта = true per tenant, или глобально). Rate, RateUpdateType+FullName— отложить до мульти-валютности.
VatRate — у MoySklad нет entity/vatrate
⚠️ У MS ставки НДС хранятся как числовое поле на товаре (vat). Отдельной таблицы нет — набор значений {0, 5, 7, 10, 12, 18, 20} и «без НДС» встроен в систему.
Наше VatRate — отдельная сущность. Обоснование сохранить:
- Локализованное название ("НДС 12%", "Без НДС").
- IsDefault per organization.
- Разные организации в разных налоговых режимах (с НДС / УСН).
- При добавлении новой ставки (например, на случай гипотетического увеличения в РК) — не править перечисление в коде.
Но: следите, чтобы у товара хранился 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:
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:
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:
- Обязательно:
DefaultPriceTypeId(ссылка наPriceType). - Настройки кассы (скоп Phase 3 — касса):
AllowCustomPrice,AllowCreateProducts,SellReserves,DiscountMaxPercent,RequireCustomer...— добавлять по мере реализации POS. - Коллекция
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:
- Поля:
VatEnabled,VatIncluded,VatSum,PayedSum,Overhead. - Lines:
Discount(decimal),VatPercent(snapshot, уже подобное есть в RetailSaleLine),VatEnabled. - Комментарий: 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:
- Добавить
PaidQr: decimal. - Убрать
PaymentMethodenum в пользу денормализованныхPaidCash, PaidCard, PaidQr, PaidBonus+ computedMethod(если все кроме одного = 0 → Cash/Card/QR, иначе Mixed). Либо оставить enum + PayHint, но быть готовым к частичной оплате. VatEnabled, VatIncluded, VatSum(сумма НДС на документ — вычисляется).- Комментарий: 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:
- Комментарий в Stock.cs: объяснить, что это материализация (у MS динамический репорт).
- Добавить
InTransit: decimal— товар в пути (между складами при перемещении).
StockMovement — у MoySklad такой сущности нет
⚠️ MS не хранит journal движений в явном виде — остатки рассчитываются в реальном времени из документов (supply, retaildemand, loss, enter, move и т.д.). Поэтому на вопрос «почему на складе минус 5» MS возвращает историю документов, а не journal.
Наше StockMovement — явный immutable journal. Обоснование:
- Мгновенный ответ на «почему такой остаток» без пробегания по всем документам.
- Атомарные корректировки при баг-фиксах миграций.
- Упрощённая репликация в офлайн-кассы.
Это сознательное отклонение от 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 число на товаре. У нас справочник ради локализации.StockMovementjournal — у MS нет. Выбор архитектуры.Product.IsWeighed/IsAlcohol— упрощения под ритейл.UnitOfMeasure.Symbol,DecimalPlaces— UX.