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>
79 lines
3.5 KiB
C#
79 lines
3.5 KiB
C#
using foodmarket.Domain.Catalog;
|
||
using foodmarket.Domain.Common;
|
||
|
||
namespace foodmarket.Domain.Inventory;
|
||
|
||
public enum LossStatus
|
||
{
|
||
Draft = 0,
|
||
Posted = 1,
|
||
}
|
||
|
||
/// <summary>Причина списания. Из этого классификатора строятся ABC/XYZ-отчёты
|
||
/// и фискальная отчётность (порча/просрочка) в KZ. Не строй наценочную логику
|
||
/// поверх этих значений — используй ровно как business reason.</summary>
|
||
public enum LossReason
|
||
{
|
||
/// <summary>Брак (производственный/транспортный).</summary>
|
||
Defect = 0,
|
||
/// <summary>Просрочка / истёк срок годности.</summary>
|
||
Expired = 1,
|
||
/// <summary>Повреждение (механическое, при хранении/перевозке).</summary>
|
||
Damage = 2,
|
||
/// <summary>Недостача (выявлена инвентаризацией; для оформления свыше — отдельный документ инвентаризации).</summary>
|
||
Shortage = 3,
|
||
/// <summary>Прочая причина (в комментарии).</summary>
|
||
Other = 99,
|
||
}
|
||
|
||
/// <summary>Документ списания со склада: брак, просрочка, недостача, повреждение.
|
||
/// При проведении создаёт <see cref="StockMovement"/> с типом
|
||
/// <see cref="MovementType.WriteOff"/> и отрицательным Quantity.</summary>
|
||
public class Loss : TenantEntity
|
||
{
|
||
public string Number { get; set; } = "";
|
||
public DateTime Date { get; set; } = DateTime.UtcNow;
|
||
public LossStatus Status { get; set; } = LossStatus.Draft;
|
||
|
||
public Guid StoreId { get; set; }
|
||
public Store Store { get; set; } = null!;
|
||
|
||
public Guid CurrencyId { get; set; }
|
||
public Currency Currency { get; set; } = null!;
|
||
|
||
public LossReason Reason { get; set; } = LossReason.Other;
|
||
|
||
public string? Notes { get; set; }
|
||
|
||
/// <summary>Сумма по строкам = Σ Quantity·UnitCost. Балансовая стоимость списания.</summary>
|
||
public decimal Total { get; set; }
|
||
|
||
/// <summary>Если документ создан «на основании инвентаризации» (недостача) —
|
||
/// ссылка на неё. Иначе null (обычное списание брака/просрочки).</summary>
|
||
public Guid? InventoryDocId { get; set; }
|
||
|
||
public DateTime? PostedAt { get; set; }
|
||
public Guid? PostedByUserId { get; set; }
|
||
|
||
public ICollection<LossLine> Lines { get; set; } = new List<LossLine>();
|
||
/// <summary>Id сущности в МойСкладе — для идемпотентного переноса истории.</summary>
|
||
public string? MoySkladExternalId { get; set; }
|
||
}
|
||
|
||
public class LossLine : TenantEntity
|
||
{
|
||
public Guid LossId { get; set; }
|
||
public Loss Loss { get; set; } = null!;
|
||
|
||
public Guid ProductId { get; set; }
|
||
public Product Product { get; set; } = null!;
|
||
|
||
public decimal Quantity { get; set; }
|
||
/// <summary>Балансовая цена единицы (снимок Product.Cost на момент создания строки;
|
||
/// можно переопределить). Используется в Total и в отчётности.</summary>
|
||
public decimal UnitCost { get; set; }
|
||
public decimal LineTotal { get; set; }
|
||
|
||
public int SortOrder { get; set; }
|
||
}
|