Замечание владельца: инвентаризация стояла «Проведено» без оприходования/списания. Переделано по МойСкладу (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>
77 lines
2.6 KiB
C#
77 lines
2.6 KiB
C#
using foodmarket.Application.Inventory;
|
||
using Xunit;
|
||
|
||
namespace foodmarket.UnitTests;
|
||
|
||
/// <summary>Логика разбивки результата инвентаризации на Оприходование/Списание
|
||
/// (модель МойСклада). diff==0 не участвует; суммы = кол-во × себестоимость.</summary>
|
||
public class InventoryResultTests
|
||
{
|
||
private static (Guid, decimal, decimal) L(decimal diff, decimal cost) => (Guid.NewGuid(), diff, cost);
|
||
|
||
[Fact]
|
||
public void Surplus_goes_to_enter_shortage_to_loss()
|
||
{
|
||
var p1 = Guid.NewGuid(); var p2 = Guid.NewGuid();
|
||
var split = InventoryResult.Build(new[]
|
||
{
|
||
(p1, 3m, 100m), // излишек +3
|
||
(p2, -2m, 50m), // недостача -2
|
||
});
|
||
|
||
Assert.Single(split.Surplus);
|
||
Assert.Single(split.Shortage);
|
||
Assert.Equal(p1, split.Surplus[0].ProductId);
|
||
Assert.Equal(3m, split.Surplus[0].Quantity);
|
||
Assert.Equal(p2, split.Shortage[0].ProductId);
|
||
Assert.Equal(2m, split.Shortage[0].Quantity); // |-2|
|
||
}
|
||
|
||
[Fact]
|
||
public void Zero_diff_lines_are_ignored()
|
||
{
|
||
var split = InventoryResult.Build(new[]
|
||
{
|
||
L(0m, 100m), // совпало — не трогаем
|
||
L(0m, 999m),
|
||
L(1m, 10m), // только эта
|
||
});
|
||
Assert.Single(split.Surplus);
|
||
Assert.Empty(split.Shortage);
|
||
Assert.Equal(1m, split.Surplus[0].Quantity);
|
||
}
|
||
|
||
[Fact]
|
||
public void Sums_are_quantity_times_cost()
|
||
{
|
||
var split = InventoryResult.Build(new[]
|
||
{
|
||
L(2m, 150m), // +2 × 150 = 300
|
||
L(5m, 20m), // +5 × 20 = 100
|
||
L(-3m, 40m), // 3 × 40 = 120 (недостача)
|
||
});
|
||
Assert.Equal(400m, split.SurplusTotal);
|
||
Assert.Equal(120m, split.ShortageTotal);
|
||
Assert.True(split.HasSurplus);
|
||
Assert.True(split.HasShortage);
|
||
}
|
||
|
||
[Fact]
|
||
public void Line_total_rounds_to_two_decimals()
|
||
{
|
||
var split = InventoryResult.Build(new[] { L(3m, 548.6441m) });
|
||
// 3 × 548.6441 = 1645.9323 → округление до 2 знаков
|
||
Assert.Equal(1645.93m, split.Surplus[0].LineTotal);
|
||
}
|
||
|
||
[Fact]
|
||
public void No_diffs_gives_empty_split()
|
||
{
|
||
var split = InventoryResult.Build(new[] { L(0m, 10m), L(0m, 20m) });
|
||
Assert.False(split.HasSurplus);
|
||
Assert.False(split.HasShortage);
|
||
Assert.Equal(0m, split.SurplusTotal);
|
||
Assert.Equal(0m, split.ShortageTotal);
|
||
}
|
||
}
|