fix(migration): приёмки — сохранение по одному документу + агрегация позиций по товару

Батч из 50 приёмок с общим товаром давал дубль строки stocks (ApplyMovementAsync
создавал новую строку, не видя ещё не сохранённую). Теперь: SaveChanges после КАЖДОГО
документа (следующий видит закоммиченные остатки) + агрегация позиций по товару в
документе (средневзвешенная цена) + ChangeTracker.Clear (не копим граф на 7k докумов).
Идемпотентно по msId — повторный запуск продолжает с места.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
nns 2026-10-09 14:31:27 +05:00
parent 10cd9f989b
commit 6fdc9e527a

View file

@ -71,7 +71,6 @@ private static DateTime MomentToUtc(string? moment)
var done = (await _db.Supplies.Where(s => s.MoySkladExternalId != null)
.Select(s => s.MoySkladExternalId!).ToListAsync(ct)).ToHashSet();
var batch = 0;
// expand=positions (НЕ positions.assortment — это раскрывает весь товар и делает
// страницу гигантской/зависающей). assortment.meta.href в позиции есть и так — из него
// берём id товара.
@ -102,15 +101,25 @@ private static DateTime MomentToUtc(string? moment)
PostedAt = posted ? momentUtc : null,
PostedByUserId = posted ? adminUserId : null,
};
// Агрегируем позиции по товару (один и тот же товар в документе — суммируем
// кол-во, цена — средневзвешенная): иначе два движения по одному товару дадут
// дубликат строки stocks в рамках одного SaveChanges.
var byProduct = new Dictionary<Guid, (decimal Qty, decimal Amount)>();
foreach (var pos in d.Positions?.Rows ?? new())
{
var prodMs = IdFromHref(pos.Assortment?.Meta?.Href);
if (prodMs is null || !products.TryGetValue(prodMs, out var pid)) continue; // услуга/неизвестный — пропуск позиции
if (prodMs is null || !products.TryGetValue(prodMs, out var pid)) continue; // услуга/неизвестный
var up = pos.Price / 100m;
var cur = byProduct.GetValueOrDefault(pid);
byProduct[pid] = (cur.Qty + pos.Quantity, cur.Amount + pos.Quantity * up);
}
foreach (var (pid, agg) in byProduct)
{
var unit = agg.Qty != 0 ? decimal.Round(agg.Amount / agg.Qty, 4) : 0m;
supply.Lines.Add(new foodmarket.Domain.Purchases.SupplyLine
{
OrganizationId = orgId, ProductId = pid, Quantity = pos.Quantity, UnitPrice = up,
LineTotal = decimal.Round(pos.Quantity * up, 2),
OrganizationId = orgId, ProductId = pid, Quantity = agg.Qty, UnitPrice = unit,
LineTotal = decimal.Round(agg.Amount, 2),
});
}
_db.Supplies.Add(supply);
@ -123,9 +132,12 @@ private static DateTime MomentToUtc(string? moment)
DocumentNumber: supply.Number, UnitCost: l.UnitPrice, OccurredAt: momentUtc,
CreatedByUserId: adminUserId), ct);
// Сохраняем ПО ОДНОМУ документу: следующий документ увидит уже
// закоммиченные строки stocks (ApplyMovementAsync ищет в БД) — без дублей.
await _db.SaveChangesAsync(ct);
_db.ChangeTracker.Clear(); // не копим tracked-граф на 7k документов
done.Add(d.Id);
progress.Created++;
if (++batch >= 50) { await _db.SaveChangesAsync(ct); batch = 0; }
}
catch (Exception ex)
{
@ -134,7 +146,6 @@ private static DateTime MomentToUtc(string? moment)
_log.LogWarning(ex, "supply import failed {Id}", d.Id);
}
}
if (batch > 0) await _db.SaveChangesAsync(ct);
return progress;
}