fix(migration): импорт переносит дубль-ШК и дубль-артикул карточки (дизамбиг артикула, пары товар-ШК)
Some checks are pending
Auto-tag / Create date-tag (push) Waiting to run
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
Docker API / Build + push API (push) Waiting to run
Docker API / Deploy API on stage (push) Blocked by required conditions

- create: артикул-коллизия → суффикс msId (перенести карточку, не падать на unique)
- create/update: ШК не уникален → добавляем код каждой карточке (пары товар|код против
  повторов), дубли ловит «Проверка данных»

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
nns 2026-10-09 20:21:03 +05:00
parent b1bdf5c228
commit 150947c5fb

View file

@ -317,11 +317,21 @@ await foreach (var c in _client.StreamCounterpartiesAsync(token, ct))
.ToDictionaryAsync(p => p.Article!, p => p, StringComparer.OrdinalIgnoreCase, ct); .ToDictionaryAsync(p => p.Article!, p => p, StringComparer.OrdinalIgnoreCase, ct);
// Barcode → Product id (ограничиваем tenant'ом). Нужен для merge-логики. // Barcode → Product id (ограничиваем tenant'ом). Нужен для merge-логики.
var existingProductIdByBarcode = await _db.ProductBarcodes // Код НЕ уникален (Phase44), поэтому при коллизии оставляем ПЕРВОГО владельца
// (для merge-by-barcode legacy). Для «одна карточка — свой ШК» используем
// ownBarcodePairs ниже.
var existingProductIdByBarcode = (await _db.ProductBarcodes
.Where(b => b.OrganizationId == orgId) .Where(b => b.OrganizationId == orgId)
.ToDictionaryAsync(b => b.Code, b => b.ProductId, ct); .Select(b => new { b.Code, b.ProductId }).ToListAsync(ct))
.GroupBy(x => x.Code).ToDictionary(g => g.Key, g => g.First().ProductId);
var existingBarcodeSet = new HashSet<string>(existingProductIdByBarcode.Keys); var existingBarcodeSet = new HashSet<string>(existingProductIdByBarcode.Keys);
// Пары (товар, код) — чтобы один и тот же код можно было добавить РАЗНЫМ товарам,
// но не дублировать один код у одного товара при повторных прогонах.
var ownBarcodePairs = new HashSet<(Guid, string)>(
(await _db.ProductBarcodes.Where(b => b.OrganizationId == orgId)
.Select(b => new { b.ProductId, b.Code }).ToListAsync(ct))
.Select(x => (x.ProductId, x.Code)));
// Дедуп внутри входа: один и тот же barcode встречается у нескольких // Дедуп внутри входа: один и тот же barcode встречается у нескольких
// товаров MoySklad → импортируем первый, остальные пропускаем в отчёт. // товаров MoySklad → импортируем первый, остальные пропускаем в отчёт.
@ -425,14 +435,17 @@ await foreach (var p in _client.StreamProductsAsync(token, ct, includeArchived:
if (string.IsNullOrEmpty(existing.MoySkladExternalId)) if (string.IsNullOrEmpty(existing.MoySkladExternalId))
existing.MoySkladExternalId = p.Id; existing.MoySkladExternalId = p.Id;
// Добавляем новые штрихкоды, оставляя уже существующие как есть. // Добавляем ШК карточки, если у ЭТОГО товара его ещё нет (дубль кода у
// РАЗНЫХ товаров допустим — Phase44). Так баркод-дубль карточки получает
// свой штрихкод, а повторный прогон не плодит дубли-строки.
foreach (var b in barcodes) foreach (var b in barcodes)
{ {
if (existingBarcodeSet.Contains(b.Code)) continue; if (ownBarcodePairs.Contains((existing.Id, b.Code))) continue;
b.OrganizationId = orgId; b.OrganizationId = orgId;
existing.Barcodes.Add(b); existing.Barcodes.Add(b);
existingBarcodeSet.Add(b.Code); existingBarcodeSet.Add(b.Code);
existingProductIdByBarcode[b.Code] = existing.Id; ownBarcodePairs.Add((existing.Id, b.Code));
existingProductIdByBarcode.TryAdd(b.Code, existing.Id);
} }
// Прайс retail: если у существующего товара нет цены этого типа — // Прайс retail: если у существующего товара нет цены этого типа —
@ -468,11 +481,17 @@ await foreach (var p in _client.StreamProductsAsync(token, ct, includeArchived:
} }
else else
{ {
// Артикул уникален в орг (индекс). Если он уже занят ДРУГОЙ карточкой —
// дизамбигуируем суффиксом msId, чтобы перенести карточку отдельно
// (а не падать на уникальности и не схлопывать её).
var uniqueArticle = article;
if (!string.IsNullOrWhiteSpace(uniqueArticle) && existingByArticle.ContainsKey(uniqueArticle))
uniqueArticle = Trim(article, 480) + "·" + (p.Id ?? Guid.NewGuid().ToString("N"))[..8];
var product = new Product var product = new Product
{ {
OrganizationId = orgId, OrganizationId = orgId,
Name = Trim(p.Name, 500) ?? "(без имени)", Name = Trim(p.Name, 500) ?? "(без имени)",
Article = Trim(article, 500), Article = Trim(uniqueArticle, 500),
Description = p.Description, Description = p.Description,
UnitOfMeasureId = uomId, UnitOfMeasureId = uomId,
Vat = vat, Vat = vat,
@ -497,14 +516,10 @@ await foreach (var p in _client.StreamProductsAsync(token, ct, includeArchived:
}); });
} }
// ШК НЕ уникален (Phase44): переносим штрихкод КАЖДОЙ карточки, даже если
// такой код уже у другого товара (дубль ловит «Проверка данных» → dup-barcode).
foreach (var b in barcodes) foreach (var b in barcodes)
{ {
if (existingBarcodeSet.Contains(b.Code))
{
errors.Add($"{p.Name}: штрихкод {b.Code} уже занят, пропущен.");
if (progress is not null) progress.Errors = errors;
continue;
}
b.OrganizationId = orgId; b.OrganizationId = orgId;
product.Barcodes.Add(b); product.Barcodes.Add(b);
existingBarcodeSet.Add(b.Code); existingBarcodeSet.Add(b.Code);
@ -512,7 +527,7 @@ await foreach (var p in _client.StreamProductsAsync(token, ct, includeArchived:
_db.Products.Add(product); _db.Products.Add(product);
if (p.Id is not null) existingByMsId[p.Id] = product; if (p.Id is not null) existingByMsId[p.Id] = product;
if (!string.IsNullOrWhiteSpace(article)) existingByArticle[article] = product; if (!string.IsNullOrWhiteSpace(product.Article)) existingByArticle[product.Article!] = product;
foreach (var b in product.Barcodes) existingProductIdByBarcode[b.Code] = product.Id; foreach (var b in product.Barcodes) existingProductIdByBarcode[b.Code] = product.Id;
created++; created++;
if (progress is not null) progress.Created = created; if (progress is not null) progress.Created = created;