feat(moysklad): import archived entities too (as IsActive=false)
Some checks are pending
CI / POS (WPF, Windows) (push) Waiting to run
CI / Backend (.NET 8) (push) Successful in 50s
CI / Web (React + Vite) (push) Successful in 25s
Docker Images / API image (push) Successful in 36s
Docker Images / Web image (push) Successful in 6s
Docker Images / Deploy stage (push) Successful in 18s
Some checks are pending
CI / POS (WPF, Windows) (push) Waiting to run
CI / Backend (.NET 8) (push) Successful in 50s
CI / Web (React + Vite) (push) Successful in 25s
Docker Images / API image (push) Successful in 36s
Docker Images / Web image (push) Successful in 6s
Docker Images / Deploy stage (push) Successful in 18s
Раньше архивных контрагентов/товаров MoySklad API по умолчанию не возвращает (default filter = active only). Для полной синхронизации MoySklad → food-market теперь делаем 2 прохода: сначала активных (default), затем filter=archived=true — отдаём всё одним потоком. В service убран skip-if-archived; archived записи импортируются c IsActive=false (уже было в ApplyCounterparty / ApplyProduct; продублировал для product folders: IsActive = !f.Archived). Клиент: рефакторинг — один generic StreamPagedAsync<T>(path, archivedOnly) вместо трёх копий постраничного цикла. Теперь пользовательский MoySklad-каталог мапится в food-market 1:1 включая архив. Счётчик "Пропущено" отныне значит только "уже существует и галка Перезаписать не стоит".
This commit is contained in:
parent
5f0692587a
commit
beae0ad604
|
|
@ -63,60 +63,53 @@ public async Task<MoySkladApiResult<MsOrganization>> WhoAmIAsync(string token, C
|
||||||
: MoySkladApiResult<MsOrganization>.Ok(org);
|
: MoySkladApiResult<MsOrganization>.Ok(org);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MoySklad list endpoints по умолчанию возвращают только активных (archived=false).
|
||||||
|
// Чтобы получить архивных, нужен явный filter=archived=true. Делаем два прохода:
|
||||||
|
// сначала активные (default), затем архивные — и отдаём всё одним потоком.
|
||||||
|
|
||||||
public async IAsyncEnumerable<MsProduct> StreamProductsAsync(
|
public async IAsyncEnumerable<MsProduct> StreamProductsAsync(
|
||||||
string token,
|
string token,
|
||||||
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken ct)
|
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken ct)
|
||||||
{
|
{
|
||||||
const int pageSize = 1000;
|
await foreach (var p in StreamPagedAsync<MsProduct>(token, "entity/product", archivedOnly: false, ct)) yield return p;
|
||||||
var offset = 0;
|
await foreach (var p in StreamPagedAsync<MsProduct>(token, "entity/product", archivedOnly: true, ct)) yield return p;
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
using var req = Build(HttpMethod.Get, $"entity/product?limit={pageSize}&offset={offset}", token);
|
|
||||||
using var res = await _http.SendAsync(req, ct);
|
|
||||||
res.EnsureSuccessStatusCode();
|
|
||||||
var page = await res.Content.ReadFromJsonAsync<MsListResponse<MsProduct>>(Json, ct);
|
|
||||||
if (page is null || page.Rows.Count == 0) yield break;
|
|
||||||
foreach (var p in page.Rows) yield return p;
|
|
||||||
if (page.Rows.Count < pageSize) yield break;
|
|
||||||
offset += pageSize;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async IAsyncEnumerable<MsCounterparty> StreamCounterpartiesAsync(
|
public async IAsyncEnumerable<MsCounterparty> StreamCounterpartiesAsync(
|
||||||
string token,
|
string token,
|
||||||
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken ct)
|
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken ct)
|
||||||
{
|
{
|
||||||
const int pageSize = 1000;
|
await foreach (var c in StreamPagedAsync<MsCounterparty>(token, "entity/counterparty", archivedOnly: false, ct)) yield return c;
|
||||||
var offset = 0;
|
await foreach (var c in StreamPagedAsync<MsCounterparty>(token, "entity/counterparty", archivedOnly: true, ct)) yield return c;
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
using var req = Build(HttpMethod.Get, $"entity/counterparty?limit={pageSize}&offset={offset}", token);
|
|
||||||
using var res = await _http.SendAsync(req, ct);
|
|
||||||
res.EnsureSuccessStatusCode();
|
|
||||||
var page = await res.Content.ReadFromJsonAsync<MsListResponse<MsCounterparty>>(Json, ct);
|
|
||||||
if (page is null || page.Rows.Count == 0) yield break;
|
|
||||||
foreach (var c in page.Rows) yield return c;
|
|
||||||
if (page.Rows.Count < pageSize) yield break;
|
|
||||||
offset += pageSize;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<List<MsProductFolder>> GetAllFoldersAsync(string token, CancellationToken ct)
|
public async Task<List<MsProductFolder>> GetAllFoldersAsync(string token, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var all = new List<MsProductFolder>();
|
var all = new List<MsProductFolder>();
|
||||||
var offset = 0;
|
await foreach (var f in StreamPagedAsync<MsProductFolder>(token, "entity/productfolder", archivedOnly: false, ct)) all.Add(f);
|
||||||
const int pageSize = 1000;
|
await foreach (var f in StreamPagedAsync<MsProductFolder>(token, "entity/productfolder", archivedOnly: true, ct)) all.Add(f);
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
using var req = Build(HttpMethod.Get, $"entity/productfolder?limit={pageSize}&offset={offset}", token);
|
|
||||||
using var res = await _http.SendAsync(req, ct);
|
|
||||||
res.EnsureSuccessStatusCode();
|
|
||||||
var page = await res.Content.ReadFromJsonAsync<MsListResponse<MsProductFolder>>(Json, ct);
|
|
||||||
if (page is null || page.Rows.Count == 0) break;
|
|
||||||
all.AddRange(page.Rows);
|
|
||||||
if (page.Rows.Count < pageSize) break;
|
|
||||||
offset += pageSize;
|
|
||||||
}
|
|
||||||
return all;
|
return all;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async IAsyncEnumerable<T> StreamPagedAsync<T>(
|
||||||
|
string token,
|
||||||
|
string path,
|
||||||
|
bool archivedOnly,
|
||||||
|
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken ct)
|
||||||
|
{
|
||||||
|
const int pageSize = 1000;
|
||||||
|
var offset = 0;
|
||||||
|
var filterSuffix = archivedOnly ? "&filter=archived=true" : "";
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
using var req = Build(HttpMethod.Get, $"{path}?limit={pageSize}&offset={offset}{filterSuffix}", token);
|
||||||
|
using var res = await _http.SendAsync(req, ct);
|
||||||
|
res.EnsureSuccessStatusCode();
|
||||||
|
var page = await res.Content.ReadFromJsonAsync<MsListResponse<T>>(Json, ct);
|
||||||
|
if (page is null || page.Rows.Count == 0) yield break;
|
||||||
|
foreach (var row in page.Rows) yield return row;
|
||||||
|
if (page.Rows.Count < pageSize) yield break;
|
||||||
|
offset += pageSize;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,7 @@ static foodmarket.Domain.Catalog.CounterpartyType ResolveType(string? companyTyp
|
||||||
await foreach (var c in _client.StreamCounterpartiesAsync(token, ct))
|
await foreach (var c in _client.StreamCounterpartiesAsync(token, ct))
|
||||||
{
|
{
|
||||||
total++;
|
total++;
|
||||||
if (c.Archived) { skipped++; continue; }
|
// Архивных не пропускаем — импортируем как IsActive=false (см. ApplyCounterparty).
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|
@ -144,11 +144,12 @@ await foreach (var c in _client.StreamCounterpartiesAsync(token, ct))
|
||||||
.IgnoreQueryFilters()
|
.IgnoreQueryFilters()
|
||||||
.ToDictionaryAsync(c => c.Name, c => c.Id, ct);
|
.ToDictionaryAsync(c => c.Name, c => c.Id, ct);
|
||||||
|
|
||||||
// Import folders first — build flat then link parents.
|
// Import folders first — build flat then link parents. Архивные тоже берём,
|
||||||
|
// помечаем IsActive=false — у MoySklad у productfolder есть archived.
|
||||||
var folders = await _client.GetAllFoldersAsync(token, ct);
|
var folders = await _client.GetAllFoldersAsync(token, ct);
|
||||||
var localGroupByMsId = new Dictionary<string, Guid>();
|
var localGroupByMsId = new Dictionary<string, Guid>();
|
||||||
var groupsCreated = 0;
|
var groupsCreated = 0;
|
||||||
foreach (var f in folders.Where(f => !f.Archived).OrderBy(f => f.PathName?.Length ?? 0))
|
foreach (var f in folders.OrderBy(f => f.PathName?.Length ?? 0))
|
||||||
{
|
{
|
||||||
if (f.Id is null) continue;
|
if (f.Id is null) continue;
|
||||||
var existing = await _db.ProductGroups.FirstOrDefaultAsync(
|
var existing = await _db.ProductGroups.FirstOrDefaultAsync(
|
||||||
|
|
@ -163,7 +164,7 @@ await foreach (var c in _client.StreamCounterpartiesAsync(token, ct))
|
||||||
OrganizationId = orgId,
|
OrganizationId = orgId,
|
||||||
Name = f.Name,
|
Name = f.Name,
|
||||||
Path = string.IsNullOrEmpty(f.PathName) ? f.Name : $"{f.PathName}/{f.Name}",
|
Path = string.IsNullOrEmpty(f.PathName) ? f.Name : $"{f.PathName}/{f.Name}",
|
||||||
IsActive = true,
|
IsActive = !f.Archived,
|
||||||
};
|
};
|
||||||
_db.ProductGroups.Add(g);
|
_db.ProductGroups.Add(g);
|
||||||
localGroupByMsId[f.Id] = g.Id;
|
localGroupByMsId[f.Id] = g.Id;
|
||||||
|
|
@ -188,7 +189,7 @@ await foreach (var c in _client.StreamCounterpartiesAsync(token, ct))
|
||||||
await foreach (var p in _client.StreamProductsAsync(token, ct))
|
await foreach (var p in _client.StreamProductsAsync(token, ct))
|
||||||
{
|
{
|
||||||
total++;
|
total++;
|
||||||
if (p.Archived) { skipped++; continue; }
|
// Архивных не пропускаем — импортируем как IsActive=false.
|
||||||
|
|
||||||
var article = string.IsNullOrWhiteSpace(p.Article) ? p.Code : p.Article;
|
var article = string.IsNullOrWhiteSpace(p.Article) ? p.Code : p.Article;
|
||||||
var alreadyByArticle = !string.IsNullOrWhiteSpace(article) && existingByArticle.ContainsKey(article);
|
var alreadyByArticle = !string.IsNullOrWhiteSpace(article) && existingByArticle.ContainsKey(article);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue