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
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:
parent
b1bdf5c228
commit
150947c5fb
|
|
@ -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;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue