Domain:
- foodmarket.Domain.Inventory.Stock — materialized aggregate per (Product, Store)
with Quantity, ReservedQuantity, computed Available. Unique index on tenant+
product+store.
- foodmarket.Domain.Inventory.StockMovement — append-only journal with signed
quantity, optional UnitCost, MovementType enum (Initial, Supply, RetailSale,
WholesaleSale, CustomerReturn, SupplierReturn, TransferOut, TransferIn,
WriteOff, Enter, InventoryAdjustment), document linkage (type, id, number),
OccurredAt, CreatedBy, Notes.
Application:
- IStockService.ApplyMovementAsync draft — appends movement row + upserts
materialized Stock row in the same unit of work. Callers control SaveChanges
so a posting doc can bundle all lines atomically.
Infrastructure:
- StockService implementation over AppDbContext.
- InventoryConfigurations EF mapping (precision 18,4 on quantities/costs;
indexes for product+time, store+time, document lookup).
- Migration Phase2a_Stock applied to dev DB (tables stocks, stock_movements).
API (GET, read-only for now):
- /api/inventory/stock — filter by store, product, includeZero; joins product +
unit + store names; server-side pagination.
- /api/inventory/movements — journal filtered by store/product/date range;
movement type as string enum for UI labels.
- Both [Authorize] (any authenticated user).
OtherSystem:
- MsCounterparty DTO (name, legalTitle, inn, kpp, companyType, tags...).
- OtherSystemClient.StreamCounterpartiesAsync — paginated like products.
- OtherSystemImportService.ImportCounterpartiesAsync — maps tags → Kind (supplier /
customer / both), companyType → LegalEntity/Individual; dedup by Name;
defensive trim on all string fields; per-item try/catch; batches of 500.
- /api/admin/other-system/import-counterparties endpoint (Admin policy).
Web:
- /inventory/stock list page (store filter, include-zero toggle, search; shows
quantity/reserved/available with red-on-negative, grey-on-zero accents).
- /inventory/movements list page (store filter; colored quantity +/-, Russian
labels for each movement type).
- OtherSystem import page restructured: single token test + two import buttons
(Товары, Контрагенты) + reusable ImportResult panel that handles both.
- Sidebar: new "Остатки" group with Остатки + Движения; icons Boxes + History.
Uses the ListPageShell pattern introduced in 447ac65 — sticky top bar, sticky
table header, only the body scrolls.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
50 lines
1.9 KiB
C#
50 lines
1.9 KiB
C#
using foodmarket.Domain.Catalog;
|
||
using foodmarket.Domain.Common;
|
||
|
||
namespace foodmarket.Domain.Inventory;
|
||
|
||
// Immutable, append-only journal of every stock change.
|
||
// Stock table is a materialized aggregate over this journal.
|
||
public class StockMovement : TenantEntity
|
||
{
|
||
public Guid ProductId { get; set; }
|
||
public Product Product { get; set; } = null!;
|
||
|
||
public Guid StoreId { get; set; }
|
||
public Store Store { get; set; } = null!;
|
||
|
||
/// <summary>Signed quantity: positive = receipt, negative = issue.</summary>
|
||
public decimal Quantity { get; set; }
|
||
|
||
/// <summary>Per-unit cost at the time of movement (optional). Used for cost rollup / P&L.</summary>
|
||
public decimal? UnitCost { get; set; }
|
||
|
||
public MovementType Type { get; set; }
|
||
|
||
/// <summary>Source document discriminator, e.g. "supply", "retail-sale", "write-off", "enter", "transfer-out".</summary>
|
||
public string DocumentType { get; set; } = "";
|
||
|
||
public Guid? DocumentId { get; set; }
|
||
public string? DocumentNumber { get; set; }
|
||
|
||
public DateTime OccurredAt { get; set; } = DateTime.UtcNow;
|
||
|
||
public Guid? CreatedByUserId { get; set; }
|
||
public string? Notes { get; set; }
|
||
}
|
||
|
||
public enum MovementType
|
||
{
|
||
Initial = 0,
|
||
Supply = 1, // приёмка от поставщика
|
||
RetailSale = 2, // розничная продажа
|
||
WholesaleSale = 3, // оптовая отгрузка
|
||
CustomerReturn = 4, // возврат покупателя
|
||
SupplierReturn = 5, // возврат поставщику
|
||
TransferOut = 6, // перемещение со склада
|
||
TransferIn = 7, // перемещение на склад
|
||
WriteOff = 8, // списание
|
||
Enter = 9, // оприходование
|
||
InventoryAdjustment = 10, // корректировка по результату инвентаризации
|
||
}
|