food-market/src/food-market.domain/Inventory/StockMovement.cs
nurdotnet 9052d76871 phase2a: stock foundation (Stock + StockMovement) + OtherSystem counterparty import
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>
2026-04-22 00:51:07 +05:00

50 lines
1.9 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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&amp;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, // корректировка по результату инвентаризации
}