Compare commits
3 commits
d3b1a7d3df
...
a517c32fee
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a517c32fee | ||
|
|
025d468492 | ||
|
|
955c3f5e77 |
|
|
@ -17,15 +17,19 @@
|
||||||
2. [~] **Мигратор — работает на реальных данных.**
|
2. [~] **Мигратор — работает на реальных данных.**
|
||||||
- [x] Справочники: товары **30 944**, контрагенты **628** (+externalId), группы **53**;
|
- [x] Справочники: товары **30 944**, контрагенты **628** (+externalId), группы **53**;
|
||||||
идемпотентность по `MoySkladExternalId`.
|
идемпотентность по `MoySkladExternalId`.
|
||||||
- [x] **Документный мигратор (сервер, фон, идемпотентный, резюмируемый)** — архитектура
|
- [x] **Документный мигратор (сервер, фон, идемпотентный, резюмируемый) — РАБОТАЕТ и
|
||||||
проверена на ПРИЁМКАХ: `MoySkladDocumentImportService` + `StreamDocumentsAsync`
|
ПРОВЕРЕН на ПРИЁМКАХ (полный перенос 7130 из 7125):** `MoySkladDocumentImportService`
|
||||||
(хронология order=moment,asc, expand позиций, 429/повторы, таймаут 130с),
|
+ `StreamDocumentsAsync` (хронология order=moment,asc, expand позиций, 429/повторы,
|
||||||
`Phase41` externalId на всех документах/справочниках. Приёмки переносятся ~10/с с
|
таймаут 130с), `Phase41` externalId на всех документах/справочниках.
|
||||||
движениями остатков; moment МСК→UTC, суммы из тиынов, поставщик по externalId,
|
Проверка на стенде: 7130 приёмок, самая ранняя «00224» — дата 2025-02-11 08:29 МСК →
|
||||||
товары по externalId, агрегация позиций по товару, SaveChanges по документу.
|
**05:29 UTC** (конверсия верна), поставщик «Обеды ПроВкус Астана» (по externalId),
|
||||||
Починены 3 бага: sum как decimal («900000.0»), тяжёлый expand, дубль stocks при батче.
|
сумма 9000 ₸ (из тиынов 900000), **513 разных поставщиков** (маппинг работает, не
|
||||||
- [ ] Остальные типы (retaildemand/loss/enter/inventory/supplier_returns/shifts/деньги) —
|
fallback), 7385 проведённых с движениями остатков. Починены 5 багов: sum как decimal
|
||||||
по ТОМУ ЖЕ проверенному шаблону (следующая итерация).
|
(«900000.0»), тяжёлый expand, дубль stocks при батче, каскад после сбоя SaveChanges,
|
||||||
|
дубль номеров (МойСклад повторяет по годам → дизамбигуация «номер-год»).
|
||||||
|
- [ ] Остальные типы по ТОМУ ЖЕ шаблону: retaildemand (97k, +оплата нал/безнал, смена,
|
||||||
|
кассир), retailsalesreturn (связь с чеком), loss, enter, inventory, supplier_returns,
|
||||||
|
retailshift, внесения/выплаты/ордера/платежи. Объём большой (97k чеков ≈ 2.5ч прогона).
|
||||||
- [ ] uom/склад/точка/сотрудники как отдельные справочники с externalId (пока товары на
|
- [ ] uom/склад/точка/сотрудники как отдельные справочники с externalId (пока товары на
|
||||||
дефолтных ед.; склад — единственный склад орг).
|
дефолтных ед.; склад — единственный склад орг).
|
||||||
3. [x] **Новая орг на стенде** «FOOD MARKET» (простой режим), admin `foodmarket-migr@food-market.local`.
|
3. [x] **Новая орг на стенде** «FOOD MARKET» (простой режим), admin `foodmarket-migr@food-market.local`.
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,16 @@ private static DateTime MomentToUtc(string? moment)
|
||||||
.Select(p => new { p.Id, p.MoySkladExternalId }).ToDictionaryAsync(x => x.MoySkladExternalId!, x => x.Id, ct);
|
.Select(p => new { p.Id, p.MoySkladExternalId }).ToDictionaryAsync(x => x.MoySkladExternalId!, x => x.Id, ct);
|
||||||
var done = (await _db.Supplies.Where(s => s.MoySkladExternalId != null)
|
var done = (await _db.Supplies.Where(s => s.MoySkladExternalId != null)
|
||||||
.Select(s => s.MoySkladExternalId!).ToListAsync(ct)).ToHashSet();
|
.Select(s => s.MoySkladExternalId!).ToListAsync(ct)).ToHashSet();
|
||||||
|
// Номера приёмок МойСклада ПОВТОРЯЮТСЯ по годам, а у нас (OrganizationId, Number)
|
||||||
|
// уникален. Сохраняем оригинальный номер где можно, при коллизии добавляем год/счётчик.
|
||||||
|
var usedNumbers = (await _db.Supplies.Select(s => s.Number).ToListAsync(ct)).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||||
|
string UniqueNumber(string baseName, int year)
|
||||||
|
{
|
||||||
|
if (usedNumbers.Add(baseName)) return baseName;
|
||||||
|
var withYear = $"{baseName}-{year}";
|
||||||
|
if (usedNumbers.Add(withYear)) return withYear;
|
||||||
|
for (var n = 2; ; n++) { var cand = $"{baseName}-{year}-{n}"; if (usedNumbers.Add(cand)) return cand; }
|
||||||
|
}
|
||||||
|
|
||||||
// expand=positions (НЕ positions.assortment — это раскрывает весь товар и делает
|
// expand=positions (НЕ positions.assortment — это раскрывает весь товар и делает
|
||||||
// страницу гигантской/зависающей). assortment.meta.href в позиции есть и так — из него
|
// страницу гигантской/зависающей). assortment.meta.href в позиции есть и так — из него
|
||||||
|
|
@ -86,10 +96,11 @@ private static DateTime MomentToUtc(string? moment)
|
||||||
: fallbackSupplier ?? throw new InvalidOperationException("Нет контрагента-поставщика");
|
: fallbackSupplier ?? throw new InvalidOperationException("Нет контрагента-поставщика");
|
||||||
var momentUtc = MomentToUtc(d.Moment);
|
var momentUtc = MomentToUtc(d.Moment);
|
||||||
var posted = d.Applicable;
|
var posted = d.Applicable;
|
||||||
|
var baseNumber = string.IsNullOrWhiteSpace(d.Name) ? $"МС-{d.Id[..8]}" : d.Name!;
|
||||||
var supply = new foodmarket.Domain.Purchases.Supply
|
var supply = new foodmarket.Domain.Purchases.Supply
|
||||||
{
|
{
|
||||||
OrganizationId = orgId,
|
OrganizationId = orgId,
|
||||||
Number = string.IsNullOrWhiteSpace(d.Name) ? $"МС-{d.Id[..8]}" : d.Name!,
|
Number = UniqueNumber(baseNumber, momentUtc.Year),
|
||||||
Date = momentUtc,
|
Date = momentUtc,
|
||||||
Status = posted ? foodmarket.Domain.Purchases.SupplyStatus.Posted : foodmarket.Domain.Purchases.SupplyStatus.Draft,
|
Status = posted ? foodmarket.Domain.Purchases.SupplyStatus.Posted : foodmarket.Domain.Purchases.SupplyStatus.Draft,
|
||||||
SupplierId = supplierId,
|
SupplierId = supplierId,
|
||||||
|
|
@ -141,6 +152,9 @@ private static DateTime MomentToUtc(string? moment)
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
// Критично: после сбоя SaveChanges очищаем трекер, иначе провалившиеся
|
||||||
|
// сущности остаются tracked и роняют ВСЕ последующие документы (каскад).
|
||||||
|
_db.ChangeTracker.Clear();
|
||||||
progress.Errors++;
|
progress.Errors++;
|
||||||
if (progress.ErrorSamples.Count < 20) progress.ErrorSamples.Add($"{d.Name}/{d.Id}: {ex.Message}");
|
if (progress.ErrorSamples.Count < 20) progress.ErrorSamples.Add($"{d.Name}/{d.Id}: {ex.Message}");
|
||||||
_log.LogWarning(ex, "supply import failed {Id}", d.Id);
|
_log.LogWarning(ex, "supply import failed {Id}", d.Id);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue