Замечание владельца: инвентаризация стояла «Проведено» без оприходования/списания. Переделано по МойСкладу (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>
43 lines
2.3 KiB
C#
43 lines
2.3 KiB
C#
namespace foodmarket.Application.Inventory;
|
||
|
||
/// <summary>Чистая логика «разбить результат инвентаризации на строки
|
||
/// Оприходования (излишки) и Списания (недостача)». Вынесено для юнит-тестов.
|
||
///
|
||
/// Правила (по модели МойСклада):
|
||
/// - diff = факт − расчётное; строки с diff == 0 НЕ участвуют (непосчитанные/совпавшие
|
||
/// не трогаются);
|
||
/// - diff > 0 → излишек → строка Оприходования, количество = diff;
|
||
/// - diff < 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);
|
||
}
|
||
}
|