feat(migration): возвраты розницы + инвентаризация + дельта-синхронизация (updated>=)
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
- ImportRetailReturnsAsync (retailsalesreturn): связь с чеком по externalId demand, +остаток - ImportInventoriesAsync (inventory): Completed без движений, BookQty/ActualQty из calculated/quantity - Все 7 импортёров принимают updatedFromMsk → filter=updated>= (МСК) для дельты - Контроллер: ?updatedFrom=<МСК> прокинут во все типы Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
2d57ceab54
commit
5d11dd7434
|
|
@ -152,6 +152,8 @@ public async Task<ActionResult<object>> ImportSupplies([FromBody] ImportRequest
|
||||||
var orgId = _tenant.OrganizationId ?? throw new InvalidOperationException("No tenant.");
|
var orgId = _tenant.OrganizationId ?? throw new InvalidOperationException("No tenant.");
|
||||||
var type = HttpContext.Request.Query["type"].ToString(); // захватываем ДО Task.Run (HttpContext потом недоступен)
|
var type = HttpContext.Request.Query["type"].ToString(); // захватываем ДО Task.Run (HttpContext потом недоступен)
|
||||||
int startOffset = int.TryParse(HttpContext.Request.Query["offset"].ToString(), out var off) ? off : 0;
|
int startOffset = int.TryParse(HttpContext.Request.Query["offset"].ToString(), out var off) ? off : 0;
|
||||||
|
var upd = HttpContext.Request.Query["updatedFrom"].ToString(); // дельта: filter=updated>= (МСК "yyyy-MM-dd HH:mm:ss")
|
||||||
|
string? updFrom = string.IsNullOrWhiteSpace(upd) ? null : upd;
|
||||||
var job = _jobs.Create(string.IsNullOrEmpty(type) ? "supplies" : type);
|
var job = _jobs.Create(string.IsNullOrEmpty(type) ? "supplies" : type);
|
||||||
job.Stage = $"Перенос документов ({(string.IsNullOrEmpty(type) ? "приёмки" : type)}) с историей…";
|
job.Stage = $"Перенос документов ({(string.IsNullOrEmpty(type) ? "приёмки" : type)}) с историей…";
|
||||||
_ = Task.Run(async () =>
|
_ = Task.Run(async () =>
|
||||||
|
|
@ -166,11 +168,13 @@ public async Task<ActionResult<object>> ImportSupplies([FromBody] ImportRequest
|
||||||
var pr = new foodmarket.Infrastructure.Integrations.MoySklad.MoySkladDocumentImportService.DocImportProgress();
|
var pr = new foodmarket.Infrastructure.Integrations.MoySklad.MoySkladDocumentImportService.DocImportProgress();
|
||||||
switch (type)
|
switch (type)
|
||||||
{
|
{
|
||||||
case "enter": await docs.ImportEntersAsync(token, startOffset, pr, CancellationToken.None); break;
|
case "enter": await docs.ImportEntersAsync(token, startOffset, pr, CancellationToken.None, updFrom); break;
|
||||||
case "loss": await docs.ImportLossesAsync(token, startOffset, pr, CancellationToken.None); break;
|
case "loss": await docs.ImportLossesAsync(token, startOffset, pr, CancellationToken.None, updFrom); break;
|
||||||
case "purchasereturn": await docs.ImportSupplierReturnsAsync(token, startOffset, pr, CancellationToken.None); break;
|
case "purchasereturn": await docs.ImportSupplierReturnsAsync(token, startOffset, pr, CancellationToken.None, updFrom); break;
|
||||||
case "retaildemand": await docs.ImportRetailSalesAsync(token, startOffset, pr, CancellationToken.None); break;
|
case "retaildemand": await docs.ImportRetailSalesAsync(token, startOffset, pr, CancellationToken.None, updFrom); break;
|
||||||
default: await docs.ImportSuppliesAsync(token, startOffset, pr, CancellationToken.None); break;
|
case "retailsalesreturn": await docs.ImportRetailReturnsAsync(token, startOffset, pr, CancellationToken.None, updFrom); break;
|
||||||
|
case "inventory": await docs.ImportInventoriesAsync(token, startOffset, pr, CancellationToken.None, updFrom); break;
|
||||||
|
default: await docs.ImportSuppliesAsync(token, startOffset, pr, CancellationToken.None, updFrom); break;
|
||||||
}
|
}
|
||||||
job.Total = pr.Seen; job.Created = pr.Created; job.Skipped = pr.Skipped;
|
job.Total = pr.Seen; job.Created = pr.Created; job.Skipped = pr.Skipped;
|
||||||
job.Message = $"Приёмки: перенесено {pr.Created}, пропущено {pr.Skipped}, ошибок {pr.Errors}. " +
|
job.Message = $"Приёмки: перенесено {pr.Created}, пропущено {pr.Skipped}, ошибок {pr.Errors}. " +
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,7 @@ private static DateTime MomentToUtc(string? moment)
|
||||||
|
|
||||||
// ── Приёмки (supply) ────────────────────────────────────────────────────
|
// ── Приёмки (supply) ────────────────────────────────────────────────────
|
||||||
public async Task<DocImportProgress> ImportSuppliesAsync(string token, int startOffset,
|
public async Task<DocImportProgress> ImportSuppliesAsync(string token, int startOffset,
|
||||||
DocImportProgress progress, CancellationToken ct)
|
DocImportProgress progress, CancellationToken ct, string? updatedFromMsk = null)
|
||||||
{
|
{
|
||||||
var orgId = _tenant.OrganizationId ?? throw new InvalidOperationException("Tenant not set.");
|
var orgId = _tenant.OrganizationId ?? throw new InvalidOperationException("Tenant not set.");
|
||||||
progress.Entity = "supply";
|
progress.Entity = "supply";
|
||||||
|
|
@ -85,7 +85,7 @@ string UniqueNumber(string baseName, int year)
|
||||||
// страницу гигантской/зависающей). assortment.meta.href в позиции есть и так — из него
|
// страницу гигантской/зависающей). assortment.meta.href в позиции есть и так — из него
|
||||||
// берём id товара.
|
// берём id товара.
|
||||||
await foreach (var d in _client.StreamDocumentsAsync<MsDocument>(token, "entity/supply",
|
await foreach (var d in _client.StreamDocumentsAsync<MsDocument>(token, "entity/supply",
|
||||||
"positions,agent,store", startOffset, null, ct))
|
"positions,agent,store", startOffset, updatedFromMsk, ct))
|
||||||
{
|
{
|
||||||
progress.Seen++;
|
progress.Seen++;
|
||||||
if (d.Id is null || done.Contains(d.Id)) { progress.Skipped++; continue; }
|
if (d.Id is null || done.Contains(d.Id)) { progress.Skipped++; continue; }
|
||||||
|
|
@ -195,14 +195,14 @@ private async Task<DocMaps> LoadMapsAsync(Guid orgId, CancellationToken ct)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Оприходования (enter) ────────────────────────────────────────────────
|
// ── Оприходования (enter) ────────────────────────────────────────────────
|
||||||
public async Task<DocImportProgress> ImportEntersAsync(string token, int startOffset, DocImportProgress progress, CancellationToken ct)
|
public async Task<DocImportProgress> ImportEntersAsync(string token, int startOffset, DocImportProgress progress, CancellationToken ct, string? updatedFromMsk = null)
|
||||||
{
|
{
|
||||||
var orgId = _tenant.OrganizationId!.Value; progress.Entity = "enter";
|
var orgId = _tenant.OrganizationId!.Value; progress.Entity = "enter";
|
||||||
var m = await LoadMapsAsync(orgId, ct);
|
var m = await LoadMapsAsync(orgId, ct);
|
||||||
var done = (await _db.Enters.Where(x => x.MoySkladExternalId != null).Select(x => x.MoySkladExternalId!).ToListAsync(ct)).ToHashSet();
|
var done = (await _db.Enters.Where(x => x.MoySkladExternalId != null).Select(x => x.MoySkladExternalId!).ToListAsync(ct)).ToHashSet();
|
||||||
var used = (await _db.Enters.Select(x => x.Number).ToListAsync(ct)).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
var used = (await _db.Enters.Select(x => x.Number).ToListAsync(ct)).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||||
string Uniq(string b, int y) { if (used.Add(b)) return b; var w = $"{b}-{y}"; if (used.Add(w)) return w; for (var n = 2; ; n++) { var c = $"{b}-{y}-{n}"; if (used.Add(c)) return c; } }
|
string Uniq(string b, int y) { if (used.Add(b)) return b; var w = $"{b}-{y}"; if (used.Add(w)) return w; for (var n = 2; ; n++) { var c = $"{b}-{y}-{n}"; if (used.Add(c)) return c; } }
|
||||||
await foreach (var d in _client.StreamDocumentsAsync<MsDocument>(token, "entity/enter", "positions,store", startOffset, null, ct))
|
await foreach (var d in _client.StreamDocumentsAsync<MsDocument>(token, "entity/enter", "positions,store", startOffset, updatedFromMsk, ct))
|
||||||
{
|
{
|
||||||
progress.Seen++; if (d.Id is null || done.Contains(d.Id)) { progress.Skipped++; continue; }
|
progress.Seen++; if (d.Id is null || done.Contains(d.Id)) { progress.Skipped++; continue; }
|
||||||
try
|
try
|
||||||
|
|
@ -224,14 +224,14 @@ await foreach (var d in _client.StreamDocumentsAsync<MsDocument>(token, "entity/
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Списания (loss) ──────────────────────────────────────────────────────
|
// ── Списания (loss) ──────────────────────────────────────────────────────
|
||||||
public async Task<DocImportProgress> ImportLossesAsync(string token, int startOffset, DocImportProgress progress, CancellationToken ct)
|
public async Task<DocImportProgress> ImportLossesAsync(string token, int startOffset, DocImportProgress progress, CancellationToken ct, string? updatedFromMsk = null)
|
||||||
{
|
{
|
||||||
var orgId = _tenant.OrganizationId!.Value; progress.Entity = "loss";
|
var orgId = _tenant.OrganizationId!.Value; progress.Entity = "loss";
|
||||||
var m = await LoadMapsAsync(orgId, ct);
|
var m = await LoadMapsAsync(orgId, ct);
|
||||||
var done = (await _db.Losses.Where(x => x.MoySkladExternalId != null).Select(x => x.MoySkladExternalId!).ToListAsync(ct)).ToHashSet();
|
var done = (await _db.Losses.Where(x => x.MoySkladExternalId != null).Select(x => x.MoySkladExternalId!).ToListAsync(ct)).ToHashSet();
|
||||||
var used = (await _db.Losses.Select(x => x.Number).ToListAsync(ct)).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
var used = (await _db.Losses.Select(x => x.Number).ToListAsync(ct)).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||||
string Uniq(string b, int y) { if (used.Add(b)) return b; var w = $"{b}-{y}"; if (used.Add(w)) return w; for (var n = 2; ; n++) { var c = $"{b}-{y}-{n}"; if (used.Add(c)) return c; } }
|
string Uniq(string b, int y) { if (used.Add(b)) return b; var w = $"{b}-{y}"; if (used.Add(w)) return w; for (var n = 2; ; n++) { var c = $"{b}-{y}-{n}"; if (used.Add(c)) return c; } }
|
||||||
await foreach (var d in _client.StreamDocumentsAsync<MsDocument>(token, "entity/loss", "positions,store", startOffset, null, ct))
|
await foreach (var d in _client.StreamDocumentsAsync<MsDocument>(token, "entity/loss", "positions,store", startOffset, updatedFromMsk, ct))
|
||||||
{
|
{
|
||||||
progress.Seen++; if (d.Id is null || done.Contains(d.Id)) { progress.Skipped++; continue; }
|
progress.Seen++; if (d.Id is null || done.Contains(d.Id)) { progress.Skipped++; continue; }
|
||||||
try
|
try
|
||||||
|
|
@ -253,14 +253,14 @@ await foreach (var d in _client.StreamDocumentsAsync<MsDocument>(token, "entity/
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Возвраты поставщику (purchasereturn) ─────────────────────────────────
|
// ── Возвраты поставщику (purchasereturn) ─────────────────────────────────
|
||||||
public async Task<DocImportProgress> ImportSupplierReturnsAsync(string token, int startOffset, DocImportProgress progress, CancellationToken ct)
|
public async Task<DocImportProgress> ImportSupplierReturnsAsync(string token, int startOffset, DocImportProgress progress, CancellationToken ct, string? updatedFromMsk = null)
|
||||||
{
|
{
|
||||||
var orgId = _tenant.OrganizationId!.Value; progress.Entity = "purchasereturn";
|
var orgId = _tenant.OrganizationId!.Value; progress.Entity = "purchasereturn";
|
||||||
var m = await LoadMapsAsync(orgId, ct);
|
var m = await LoadMapsAsync(orgId, ct);
|
||||||
var done = (await _db.SupplierReturns.Where(x => x.MoySkladExternalId != null).Select(x => x.MoySkladExternalId!).ToListAsync(ct)).ToHashSet();
|
var done = (await _db.SupplierReturns.Where(x => x.MoySkladExternalId != null).Select(x => x.MoySkladExternalId!).ToListAsync(ct)).ToHashSet();
|
||||||
var used = (await _db.SupplierReturns.Select(x => x.Number).ToListAsync(ct)).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
var used = (await _db.SupplierReturns.Select(x => x.Number).ToListAsync(ct)).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||||
string Uniq(string b, int y) { if (used.Add(b)) return b; var w = $"{b}-{y}"; if (used.Add(w)) return w; for (var n = 2; ; n++) { var c = $"{b}-{y}-{n}"; if (used.Add(c)) return c; } }
|
string Uniq(string b, int y) { if (used.Add(b)) return b; var w = $"{b}-{y}"; if (used.Add(w)) return w; for (var n = 2; ; n++) { var c = $"{b}-{y}-{n}"; if (used.Add(c)) return c; } }
|
||||||
await foreach (var d in _client.StreamDocumentsAsync<MsDocument>(token, "entity/purchasereturn", "positions,agent,store", startOffset, null, ct))
|
await foreach (var d in _client.StreamDocumentsAsync<MsDocument>(token, "entity/purchasereturn", "positions,agent,store", startOffset, updatedFromMsk, ct))
|
||||||
{
|
{
|
||||||
progress.Seen++; if (d.Id is null || done.Contains(d.Id)) { progress.Skipped++; continue; }
|
progress.Seen++; if (d.Id is null || done.Contains(d.Id)) { progress.Skipped++; continue; }
|
||||||
try
|
try
|
||||||
|
|
@ -284,14 +284,14 @@ await foreach (var d in _client.StreamDocumentsAsync<MsDocument>(token, "entity/
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Розничные продажи (retaildemand) ─────────────────────────────────────
|
// ── Розничные продажи (retaildemand) ─────────────────────────────────────
|
||||||
public async Task<DocImportProgress> ImportRetailSalesAsync(string token, int startOffset, DocImportProgress progress, CancellationToken ct)
|
public async Task<DocImportProgress> ImportRetailSalesAsync(string token, int startOffset, DocImportProgress progress, CancellationToken ct, string? updatedFromMsk = null)
|
||||||
{
|
{
|
||||||
var orgId = _tenant.OrganizationId!.Value; progress.Entity = "retaildemand";
|
var orgId = _tenant.OrganizationId!.Value; progress.Entity = "retaildemand";
|
||||||
var m = await LoadMapsAsync(orgId, ct);
|
var m = await LoadMapsAsync(orgId, ct);
|
||||||
var done = (await _db.RetailSales.Where(x => x.MoySkladExternalId != null).Select(x => x.MoySkladExternalId!).ToListAsync(ct)).ToHashSet();
|
var done = (await _db.RetailSales.Where(x => x.MoySkladExternalId != null).Select(x => x.MoySkladExternalId!).ToListAsync(ct)).ToHashSet();
|
||||||
var used = (await _db.RetailSales.Select(x => x.Number).ToListAsync(ct)).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
var used = (await _db.RetailSales.Select(x => x.Number).ToListAsync(ct)).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||||
string Uniq(string b, int y) { if (used.Add(b)) return b; var w = $"{b}-{y}"; if (used.Add(w)) return w; for (var n = 2; ; n++) { var c = $"{b}-{y}-{n}"; if (used.Add(c)) return c; } }
|
string Uniq(string b, int y) { if (used.Add(b)) return b; var w = $"{b}-{y}"; if (used.Add(w)) return w; for (var n = 2; ; n++) { var c = $"{b}-{y}-{n}"; if (used.Add(c)) return c; } }
|
||||||
await foreach (var d in _client.StreamDocumentsAsync<MsDocument>(token, "entity/retaildemand", "positions,store", startOffset, null, ct))
|
await foreach (var d in _client.StreamDocumentsAsync<MsDocument>(token, "entity/retaildemand", "positions,store", startOffset, updatedFromMsk, ct))
|
||||||
{
|
{
|
||||||
progress.Seen++; if (d.Id is null || done.Contains(d.Id)) { progress.Skipped++; continue; }
|
progress.Seen++; if (d.Id is null || done.Contains(d.Id)) { progress.Skipped++; continue; }
|
||||||
try
|
try
|
||||||
|
|
@ -315,6 +315,73 @@ await foreach (var d in _client.StreamDocumentsAsync<MsDocument>(token, "entity/
|
||||||
return progress;
|
return progress;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Розничные возвраты (retailsalesreturn) ───────────────────────────────
|
||||||
|
public async Task<DocImportProgress> ImportRetailReturnsAsync(string token, int startOffset, DocImportProgress progress, CancellationToken ct, string? updatedFromMsk = null)
|
||||||
|
{
|
||||||
|
var orgId = _tenant.OrganizationId!.Value; progress.Entity = "retailsalesreturn";
|
||||||
|
var m = await LoadMapsAsync(orgId, ct);
|
||||||
|
var done = (await _db.RetailSales.Where(x => x.MoySkladExternalId != null && x.IsReturn).Select(x => x.MoySkladExternalId!).ToListAsync(ct)).ToHashSet();
|
||||||
|
var saleByMs = await _db.RetailSales.Where(x => x.MoySkladExternalId != null && !x.IsReturn)
|
||||||
|
.Select(x => new { x.Id, x.MoySkladExternalId }).ToDictionaryAsync(x => x.MoySkladExternalId!, x => x.Id, ct);
|
||||||
|
var used = (await _db.RetailSales.Select(x => x.Number).ToListAsync(ct)).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||||
|
string Uniq(string b, int y) { if (used.Add(b)) return b; var w = $"{b}-{y}возв"; if (used.Add(w)) return w; for (var n = 2; ; n++) { var c = $"{b}-{y}-{n}возв"; if (used.Add(c)) return c; } }
|
||||||
|
await foreach (var d in _client.StreamDocumentsAsync<MsDocument>(token, "entity/retailsalesreturn", "positions,store,demand", startOffset, updatedFromMsk, ct))
|
||||||
|
{
|
||||||
|
progress.Seen++; if (d.Id is null || done.Contains(d.Id)) { progress.Skipped++; continue; }
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var mom = MomentToUtc(d.Moment); var posted = d.Applicable;
|
||||||
|
var demandMs = IdFromHref(d.Demand?.Meta?.Href);
|
||||||
|
Guid? refSale = demandMs is not null && saleByMs.TryGetValue(demandMs, out var rs) ? rs : null;
|
||||||
|
var cash = d.CashSum / 100m; var card = d.NoCashSum / 100m; var total = d.Sum / 100m;
|
||||||
|
var pay = card > 0 ? foodmarket.Domain.Sales.PaymentMethod.Card : foodmarket.Domain.Sales.PaymentMethod.Cash;
|
||||||
|
var e = new foodmarket.Domain.Sales.RetailSale { OrganizationId = orgId, Number = Uniq(d.Name ?? $"МС-{d.Id[..8]}", mom.Year), Date = mom,
|
||||||
|
Status = posted ? foodmarket.Domain.Sales.RetailSaleStatus.Posted : foodmarket.Domain.Sales.RetailSaleStatus.Draft,
|
||||||
|
StoreId = m.StoreId, CurrencyId = m.CurrencyId, CashierUserId = m.AdminUserId, Payment = pay, PaidCash = cash, PaidCard = card,
|
||||||
|
Subtotal = total, Total = total, Notes = d.Description, MoySkladExternalId = d.Id, IsReturn = true, ReferenceSaleId = refSale,
|
||||||
|
PostedAt = posted ? mom : null, PostedByUserId = posted ? m.AdminUserId : null };
|
||||||
|
foreach (var (pid, a) in Aggregate(d, m.Products))
|
||||||
|
e.Lines.Add(new foodmarket.Domain.Sales.RetailSaleLine { OrganizationId = orgId, ProductId = pid, Quantity = a.Qty, UnitPrice = a.Qty != 0 ? decimal.Round(a.Amount / a.Qty, 4) : 0m, LineTotal = decimal.Round(a.Amount, 2) });
|
||||||
|
_db.RetailSales.Add(e);
|
||||||
|
if (posted) foreach (var l in e.Lines) await _stock.ApplyMovementAsync(new StockMovementDraft(l.ProductId, m.StoreId, l.Quantity, MovementType.CustomerReturn, "retail-return", e.Id, e.Number, l.UnitPrice, mom, m.AdminUserId), ct);
|
||||||
|
await _db.SaveChangesAsync(ct); _db.ChangeTracker.Clear(); done.Add(d.Id); progress.Created++;
|
||||||
|
}
|
||||||
|
catch (Exception ex) { _db.ChangeTracker.Clear(); progress.Errors++; if (progress.ErrorSamples.Count < 20) progress.ErrorSamples.Add($"{d.Name}: {ex.Message}"); _log.LogWarning(ex, "retailsalesreturn {Id}", d.Id); }
|
||||||
|
}
|
||||||
|
return progress;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Инвентаризации (inventory) — история без движения остатков (как в нашей модели) ──
|
||||||
|
public async Task<DocImportProgress> ImportInventoriesAsync(string token, int startOffset, DocImportProgress progress, CancellationToken ct, string? updatedFromMsk = null)
|
||||||
|
{
|
||||||
|
var orgId = _tenant.OrganizationId!.Value; progress.Entity = "inventory";
|
||||||
|
var m = await LoadMapsAsync(orgId, ct);
|
||||||
|
var done = (await _db.InventoryDocs.Where(x => x.MoySkladExternalId != null).Select(x => x.MoySkladExternalId!).ToListAsync(ct)).ToHashSet();
|
||||||
|
var used = (await _db.InventoryDocs.Select(x => x.Number).ToListAsync(ct)).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||||
|
string Uniq(string b, int y) { if (used.Add(b)) return b; var w = $"{b}-{y}"; if (used.Add(w)) return w; for (var n = 2; ; n++) { var c = $"{b}-{y}-{n}"; if (used.Add(c)) return c; } }
|
||||||
|
await foreach (var d in _client.StreamDocumentsAsync<MsDocument>(token, "entity/inventory", "positions,store", startOffset, updatedFromMsk, ct))
|
||||||
|
{
|
||||||
|
progress.Seen++; if (d.Id is null || done.Contains(d.Id)) { progress.Skipped++; continue; }
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var mom = MomentToUtc(d.Moment);
|
||||||
|
var e = new foodmarket.Domain.Inventory.InventoryDoc { OrganizationId = orgId, Number = Uniq(d.Name ?? $"МС-{d.Id[..8]}", mom.Year), Date = mom,
|
||||||
|
Status = foodmarket.Domain.Inventory.InventoryStatus.Completed, StoreId = m.StoreId, Notes = d.Description, MoySkladExternalId = d.Id };
|
||||||
|
var seen = new HashSet<Guid>();
|
||||||
|
foreach (var pos in d.Positions?.Rows ?? new())
|
||||||
|
{
|
||||||
|
var prodMs = IdFromHref(pos.Assortment?.Meta?.Href);
|
||||||
|
if (prodMs is null || !m.Products.TryGetValue(prodMs, out var pid) || !seen.Add(pid)) continue;
|
||||||
|
e.Lines.Add(new foodmarket.Domain.Inventory.InventoryLine { OrganizationId = orgId, ProductId = pid, BookQty = pos.CalculatedQuantity, ActualQty = pos.Quantity });
|
||||||
|
}
|
||||||
|
_db.InventoryDocs.Add(e);
|
||||||
|
await _db.SaveChangesAsync(ct); _db.ChangeTracker.Clear(); done.Add(d.Id); progress.Created++;
|
||||||
|
}
|
||||||
|
catch (Exception ex) { _db.ChangeTracker.Clear(); progress.Errors++; if (progress.ErrorSamples.Count < 20) progress.ErrorSamples.Add($"{d.Name}: {ex.Message}"); _log.LogWarning(ex, "inventory {Id}", d.Id); }
|
||||||
|
}
|
||||||
|
return progress;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Гарантирует склад орг с MoySkladExternalId = склад МойСклада (магазин
|
/// <summary>Гарантирует склад орг с MoySkladExternalId = склад МойСклада (магазин
|
||||||
/// с одним складом). Берём первый склад орг и помечаем его msId при первом вызове.</summary>
|
/// с одним складом). Берём первый склад орг и помечаем его msId при первом вызове.</summary>
|
||||||
private async Task<Guid> EnsureStoreAsync(Guid orgId, CancellationToken ct)
|
private async Task<Guid> EnsureStoreAsync(Guid orgId, CancellationToken ct)
|
||||||
|
|
|
||||||
|
|
@ -169,6 +169,7 @@ public class MsPosition
|
||||||
{
|
{
|
||||||
[JsonPropertyName("id")] public string? Id { get; set; }
|
[JsonPropertyName("id")] public string? Id { get; set; }
|
||||||
[JsonPropertyName("quantity")] public decimal Quantity { get; set; }
|
[JsonPropertyName("quantity")] public decimal Quantity { get; set; }
|
||||||
|
[JsonPropertyName("calculatedQuantity")] public decimal CalculatedQuantity { get; set; } // инвентаризация: учётное кол-во
|
||||||
[JsonPropertyName("price")] public decimal Price { get; set; } // тиыны за ед.
|
[JsonPropertyName("price")] public decimal Price { get; set; } // тиыны за ед.
|
||||||
[JsonPropertyName("discount")] public decimal Discount { get; set; } // % скидки
|
[JsonPropertyName("discount")] public decimal Discount { get; set; } // % скидки
|
||||||
[JsonPropertyName("vat")] public int Vat { get; set; }
|
[JsonPropertyName("vat")] public int Vat { get; set; }
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue