food-market/src/food-market.domain/Purchases/Supply.cs
nns 2ed9f04cb2 feat(migration): документный мигратор МойСклад (приёмки) + externalId на документах
Part 2 (документы) — проверенный шаблон на приёмках:
- Phase41: MoySkladExternalId у counterparties/stores/units/retail_points/employees и
  документов (supplies/enters/losses/inventories/supplier_returns/retail_sales/
  cash_shifts/money_transactions) + индексы. Свойства на доменных сущностях.
- MoySkladClient.StreamDocumentsAsync: документы в хронологии (order=moment,asc), expand
  позиций/связей (limit=100), обработка 429/сбоев.
- MoySkladDocumentImportService.ImportSuppliesAsync: идемпотентно по msId, резюмируемо
  (skip перенесённых), moment МСК→UTC, суммы из тиынов, поставщик по externalId контрагента,
  товары по externalId, проведение → движения остатков (StockService). Журнал ошибок.
- Импорт контрагентов теперь пишет MoySkladExternalId (нужно для ссылок в документах).
- Эндпоинт POST /api/admin/moysklad/import-supplies (фон, job). Остальные типы — по шаблону.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 13:53:52 +05:00

77 lines
3.1 KiB
C#

using foodmarket.Domain.Catalog;
using foodmarket.Domain.Common;
namespace foodmarket.Domain.Purchases;
public enum SupplyStatus
{
Draft = 0,
Posted = 1,
}
public class Supply : TenantEntity, IVersionedEntity
{
/// <summary>Postgres `xmin` — optimistic concurrency token. См. IVersionedEntity.</summary>
public uint Xmin { get; set; }
/// <summary>Human-readable document number, unique per organization (e.g. "П-2026-000001").</summary>
public string Number { get; set; } = "";
public DateTime Date { get; set; } = DateTime.UtcNow;
public SupplyStatus Status { get; set; } = SupplyStatus.Draft;
public Guid SupplierId { get; set; }
public Counterparty Supplier { get; set; } = null!;
public Guid StoreId { get; set; }
public Store Store { get; set; } = null!;
public Guid CurrencyId { get; set; }
public Currency Currency { get; set; } = null!;
public string? Notes { get; set; }
/// <summary>Sum of line totals. Computed on save.</summary>
public decimal Total { get; set; }
public DateTime? PostedAt { get; set; }
public Guid? PostedByUserId { get; set; }
/// <summary>Сколько из <see cref="Total"/> оплачено — сумма всех
/// проведённых <c>MoneyTransactionLink.LinkedSum</c>, где
/// <c>SupplyId</c>=этой приёмки. Пересчитывается в <c>MoneyService</c>
/// при проведении/отмене платежа. Инвариант: <c>0 ≤ PayedSum ≤ Total</c>.
/// Статус оплаты получаем сравнением: PayedSum==0 — не оплачена,
/// 0&lt;PayedSum&lt;Total — частично, PayedSum&gt;=Total — оплачена.</summary>
public decimal PayedSum { get; set; }
/// <summary>Id сущности в МойСкладе — для идемпотентного переноса истории.</summary>
public string? MoySkladExternalId { get; set; }
public ICollection<SupplyLine> Lines { get; set; } = new List<SupplyLine>();
}
public class SupplyLine : TenantEntity
{
public Guid SupplyId { get; set; }
public Supply Supply { get; set; } = null!;
public Guid ProductId { get; set; }
public Product Product { get; set; } = null!;
public decimal Quantity { get; set; }
public decimal UnitPrice { get; set; }
public decimal LineTotal { get; set; }
public int SortOrder { get; set; }
/// <summary>Если true — пользователь вручную задал розничную цену для
/// этой строки (через UI приёмки). При Posting автонаценка по Group.MarkupPercent
/// для этой строки пропускается.</summary>
public bool RetailPriceManuallyOverridden { get; set; }
/// <summary>Розничная цена, которую пользователь вписал в колонке «Розничная»
/// строки приёмки. Применяется к Product.Prices[default] при Posting.</summary>
public decimal? RetailPriceOverride { get; set; }
}