food-market/src/food-market.application/Inventory/InventoryResult.cs
nns 0d44cdabaf feat(inventory): модель МойСклада — инвентаризация не двигает остатки
Замечание владельца: инвентаризация стояла «Проведено» без оприходования/списания.
Переделано по МойСкладу (docs/inventory-moysklad-reference.md):

- InventoryStatus Draft→Completed (было Posted; значение 1 то же). Инвентаризация
  остатки НЕ двигает: endpoint complete замораживает строки без движений.
- create-enter (излишки diff>0) / create-loss (недостача diff<0) — создают черновики
  Оприходования/Списания со строками разницы + ссылка InventoryDocId. Повторно нельзя (409).
- Кнопки «Применить результаты» НЕТ (поправка Mac-Claude — не одобрено владельцем).
- unpost («вернуть в черновик») отменяет проведение и удаляет связанные документы.
- post/unpost — временная совместимость для мобильного «FM Склад» (post = завершить +
  создать и провести оба документа); помечено в docs/mobile-api.md.
- Enter/Loss: поле InventoryDocId (+ миграция Phase38) + в DTO «на основании инвентаризации №…».
- InventoryResult (application) — чистая разбивка diff→излишки/недостача (unit-тесты:
  разница/сумма/только посчитанные строки/округление).
- InventoryMigrationController (dry-run/apply): старые Posted+InventoryAdjustment →
  связанные проведённые документы переметкой движений (не задваивает), с проверкой
  неизменности остатков. Скрипт deploy/migrate-inventory-to-docs.sh (backup + запуск).
- Демо-сидеры: инвентаризации теперь Completed без прямых движений.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 20:34:05 +05:00

43 lines
2.3 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.

namespace foodmarket.Application.Inventory;
/// <summary>Чистая логика «разбить результат инвентаризации на строки
/// Оприходования (излишки) и Списания (недостача)». Вынесено для юнит-тестов.
///
/// Правила (по модели МойСклада):
/// - diff = факт − расчётное; строки с diff == 0 НЕ участвуют (непосчитанные/совпавшие
/// не трогаются);
/// - diff &gt; 0 → излишек → строка Оприходования, количество = diff;
/// - diff &lt; 0 → недостача → строка Списания, количество = |diff|;
/// - сумма строки = количество × UnitCost (снимок себестоимости).
/// Сама инвентаризация остатки НЕ двигает.</summary>
public static class InventoryResult
{
public readonly record struct ResultLine(Guid ProductId, decimal Quantity, decimal UnitCost)
{
public decimal LineTotal => decimal.Round(Quantity * UnitCost, 2);
}
public readonly record struct Split(IReadOnlyList<ResultLine> Surplus, IReadOnlyList<ResultLine> Shortage)
{
public bool HasSurplus => Surplus.Count > 0;
public bool HasShortage => Shortage.Count > 0;
public decimal SurplusTotal => Surplus.Sum(l => l.LineTotal);
public decimal ShortageTotal => Shortage.Sum(l => l.LineTotal);
}
/// <summary>Разбивает строки инвентаризации (productId, diff, unitCost) на излишки/недостачи.
/// diff==0 пропускаются. Количество в Списании положительное (|diff|).</summary>
public static Split Build(IEnumerable<(Guid ProductId, decimal Diff, decimal UnitCost)> lines)
{
var surplus = new List<ResultLine>();
var shortage = new List<ResultLine>();
foreach (var l in lines)
{
if (l.Diff > 0m) surplus.Add(new ResultLine(l.ProductId, l.Diff, l.UnitCost));
else if (l.Diff < 0m) shortage.Add(new ResultLine(l.ProductId, -l.Diff, l.UnitCost));
// diff == 0 — пропускаем
}
return new Split(surplus, shortage);
}
}