Watch
1
0
Fork
You've already forked Seyyed_arc
0
forked from hesabix/arc
Seyyed_arc/extraScripts/migration/holoo2hesabix/Services/DocumentTransferService.vb
Babak Alizadeh a5875e7a3a Release Holoo2Hesabix 1.1.0 with production-ready accounting migration.
Harden Holoo-to-Hesabix document partition/mapping, sample E2E tooling, remove hardcoded SQL credentials from scripts, fix income AR account 10401, and publish Setup MSI 1.1.0.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-24 15:56:40 +03:30

2259 lines
110 KiB
VB.net
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

Imports System.Collections.Concurrent
Imports System.Net
Imports System.Threading
Imports Newtonsoft.Json.Linq
''' <summary>
''' انتقال اسناد سال‌به‌سال: FY، افتتاحیه، فاکتور، انبار، دریافت، چک، هزینه/درآمد.
''' سندهای Type=5 هلو (مرتبط با چک) عمداً کپی نمی‌شوند — GL از API چک ساخته می‌شود.
''' </summary>
Friend Class DocumentTransferService
Private ReadOnly _docs As New HolooDocumentReader()
' تک‌فاکتور با موازی‌سازی ملایم — بیش از ۳ کارگر روی این API باعث timeout دسته‌جمعی می‌شود
Private Const InvoiceChunk As Integer = 1
Private Const InvoiceParallelism As Integer = 3
Private Const InvoiceRequestTimeoutSeconds As Integer = 90
Private Const WarehouseChunk As Integer = 100
Private Const ReceiptChunk As Integer = 50
Private Const ExpenseChunk As Integer = 40
''' <summary>اگر >0 باشد، از هر ماژول اسناد فقط همین تعداد نمونه منتقل می‌شود (تست سریع).</summary>
Public Property SampleLimit As Integer = 0
Public Event ProgressChanged As EventHandler(Of TransferProgressEventArgs)
Private Function ApplySample(Of T)(rows As List(Of T), moduleTitle As String) As List(Of T)
If rows Is Nothing Then Return New List(Of T)()
If SampleLimit <= 0 OrElse rows.Count <= SampleLimit Then Return rows
RaiseProgress(moduleTitle, 0, rows.Count,
"حالت نمونه: " & SampleLimit.ToString() & " از " & rows.Count.ToString() & " سند")
Return rows.Take(SampleLimit).ToList()
End Function
Public Async Function RunAsync(
session As MigrationSession,
api As HesabixApiClient,
selectedModules As IEnumerable(Of MigrationModule),
currencyId As Integer,
Optional resetCheckpoint As Boolean = False,
Optional ct As CancellationToken = Nothing
) As Task(Of String)
Dim businessId = session.SelectedBusiness.Id
Dim store As New CheckpointStore(businessId, session.SelectedDatabase)
If resetCheckpoint Then
' reset کامل قبلاً در base انجام شده.
End If
Dim cp = store.LoadOrCreate(session.ApiBaseUrl, businessId, session.SqlSettings.Server, session.SelectedDatabase)
api.Configure(session.ApiBaseUrl, session.ApiKey)
Dim mods = selectedModules.Where(Function(m) MigrationModuleInfo.IsDocumentModule(m)).OrderBy(Function(m) CInt(m)).ToList()
If mods.Count = 0 Then Return store.FilePath
Dim years = If(session.Preflight?.FiscalYears, New List(Of DetectedFiscalYear)())
If years.Count = 0 Then
RaiseProgress("سال مالی", 0, 0, "سال مالی کشف‌شده وجود ندارد", True)
Return store.FilePath
End If
If mods.Contains(MigrationModule.FiscalYearsAndOpening) OrElse
mods.Contains(MigrationModule.Invoices) OrElse
mods.Contains(MigrationModule.ReceiptsPayments) OrElse
mods.Contains(MigrationModule.Checks) OrElse
mods.Contains(MigrationModule.ExpenseIncome) OrElse
mods.Contains(MigrationModule.ManualJournals) Then
Await EnsureFiscalYears(session, api, businessId, years, ct).ConfigureAwait(False)
End If
Dim personMap = cp.EnsureModule(MigrationModule.Persons.ToString())
Dim productMap = cp.EnsureModule(MigrationModule.Products.ToString())
Dim bankMap = cp.EnsureModule(MigrationModule.BankAccounts.ToString())
Dim cashMap = cp.EnsureModule(MigrationModule.CashRegisters.ToString())
Dim pettyMap = cp.EnsureModule(MigrationModule.PettyCash.ToString())
Dim warehouseMap = cp.EnsureModule(MigrationModule.Warehouses.ToString())
Dim invoiceMap = cp.EnsureModule(MigrationModule.Invoices.ToString())
RaiseProgress("نگاشت سرفصل", 0, 0, "ساخت نگاشت بانک/صندوق/هزینه از SARFASL...")
Dim mapper As New HolooSarfaslMapper()
Await Task.Run(Sub() mapper.Build(session.SqlSettings, bankMap, cashMap, pettyMap, personMap), ct).ConfigureAwait(False)
Dim defaultCashId = If(cashMap.Done.Count = 1, cashMap.Done.Values.First(), 0)
Dim defaultBankId = If(bankMap.Done.Count = 1, bankMap.Done.Values.First(), 0)
Dim defaultPettyId = If(pettyMap.Done.Count = 1, pettyMap.Done.Values.First(), 0)
' فقط وقتی دقیقاً یک برگ منتقل شده؛ DefaultLeaf* به‌عنوان dump ممنوع است
Try
Dim proof = Await Task.Run(Function() New BalanceProofService().BuildHolooProof(session.SqlSettings, businessId), ct).ConfigureAwait(False)
Dim proofPath = New BalanceProofService().SaveReport(proof, businessId, session.SelectedDatabase)
RaiseProgress("اثبات تراز", 0, 0, "گزارش پارتیشن هلو ذخیره شد: " & proofPath)
Catch ex As Exception
RaiseProgress("اثبات تراز", 0, 0, "هشدار گزارش اثبات: " & ex.Message, True)
End Try
Dim accountCodeMap As Dictionary(Of String, Integer) = Nothing
Dim catalog As SarfaslCatalog = Nothing
Dim profile = session.SarfaslProfile
If mods.Contains(MigrationModule.ExpenseIncome) OrElse
mods.Contains(MigrationModule.FiscalYearsAndOpening) OrElse
mods.Contains(MigrationModule.ManualJournals) Then
accountCodeMap = Await api.ListAccountCodeMapAsync(businessId, ct).ConfigureAwait(False)
catalog = New SarfaslCatalog(api, businessId, accountCodeMap)
End If
Dim moneyToCurrency As Dictionary(Of Integer, Integer) = Nothing
Dim isMultiCurrency = String.Equals(session.Preflight?.CurrencyMode?.Mode, "MultiCurrency", StringComparison.OrdinalIgnoreCase)
If isMultiCurrency Then
moneyToCurrency = Await BuildHolooMoneyToCurrencyMapAsync(session, api, currencyId, ct).ConfigureAwait(False)
End If
Dim firstYear = True
Dim earliestStart = years.Min(Function(y) y.StartDate)
For Each fy In years.OrderBy(Function(y) y.StartDate)
ct.ThrowIfCancellationRequested()
If fy.HesabixId <= 0 Then
RaiseProgress(fy.Title, 0, 0, "شناسه سال مالی حسابیکس نامشخص — رد شد", True)
Continue For
End If
RaiseProgress(fy.Title, 0, 0, "تعویض نرم سال مالی جاری...")
Await api.SetCurrentFiscalYearAsync(businessId, fy.HesabixId, ct).ConfigureAwait(False)
If firstYear AndAlso mods.Contains(MigrationModule.FiscalYearsAndOpening) Then
Await TransferOpeningBalance(
session, api, businessId, currencyId, fy,
personMap, bankMap, cashMap, pettyMap, warehouseMap, productMap,
mapper, accountCodeMap, catalog, profile,
cp, store, ct).ConfigureAwait(False)
End If
Dim yearInvoiceIds As New List(Of Integer)
If mods.Contains(MigrationModule.Invoices) Then
yearInvoiceIds = Await TransferInvoicesForYear(
session, api, businessId, currencyId, fy,
personMap, productMap, invoiceMap, mapper,
defaultCashId, defaultBankId, defaultPettyId,
moneyToCurrency, isMultiCurrency,
cp, store, ct).ConfigureAwait(False)
End If
If mods.Contains(MigrationModule.WarehouseDocs) Then
Dim whIds = yearInvoiceIds
If whIds.Count = 0 AndAlso mods.Contains(MigrationModule.Invoices) Then
' resume: فاکتورهای قبلاً منتقل‌شده همین سال را هم حواله کن
Dim yearRows = Await Task.Run(Function() _docs.ReadInvoicesInRange(session.SqlSettings, fy.StartDate, fy.EndDate), ct).ConfigureAwait(False)
whIds = yearRows.
Where(Function(r) invoiceMap.Done.ContainsKey(r.Key)).
Select(Function(r) invoiceMap.Done(r.Key)).
Where(Function(id) id > 0).
Distinct().
ToList()
End If
If whIds.Count > 0 Then
Await PostWarehouseForInvoices(api, businessId, whIds, cp, store, ct).ConfigureAwait(False)
End If
End If
If mods.Contains(MigrationModule.ReceiptsPayments) Then
Await TransferStandaloneReceipts(
session, api, businessId, currencyId, fy,
personMap, mapper, defaultCashId, defaultBankId, defaultPettyId,
cp, store, ct).ConfigureAwait(False)
End If
If mods.Contains(MigrationModule.Checks) Then
Await TransferChecksForYear(
session, api, businessId, currencyId, fy,
personMap, mapper, defaultBankId,
earliestStart, firstYear,
cp, store, ct).ConfigureAwait(False)
End If
If mods.Contains(MigrationModule.ExpenseIncome) Then
Await TransferExpenseIncomeForYear(
session, api, businessId, currencyId, fy,
personMap, mapper, accountCodeMap, catalog, profile,
defaultCashId, defaultBankId, defaultPettyId,
cp, store, ct).ConfigureAwait(False)
End If
If mods.Contains(MigrationModule.ManualJournals) Then
Await TransferManualJournalsForYear(
session, api, businessId, currencyId, fy,
personMap, mapper, accountCodeMap, catalog, profile,
defaultCashId, defaultBankId, defaultPettyId,
cp, store, ct).ConfigureAwait(False)
End If
firstYear = False
RaiseProgress(fy.Title, 1, 1, "پایان پردازش سال")
Next
If mods.Contains(MigrationModule.Invoices) AndAlso SampleLimit <= 0 AndAlso invoiceMap.Failed.Count = 0 Then
invoiceMap.Completed = True
store.Save(cp)
End If
Return store.FilePath
End Function
Private Async Function EnsureFiscalYears(
session As MigrationSession,
api As HesabixApiClient,
businessId As Integer,
years As List(Of DetectedFiscalYear),
ct As CancellationToken
) As Task
' سال پیش‌فرض کسب‌وکار جدید اغلب با آخرین سال هلو همپوشانی دارد؛ قبل از ensure جابه‌جا می‌کنیم.
Await AlignDefaultFiscalYearForMigration(api, businessId, years, ct).ConfigureAwait(False)
Dim slices = years.Select(Function(y) New HolooFiscalYearSlice With {
.Title = y.Title,
.StartDate = y.StartDate,
.EndDate = y.EndDate
}).ToList()
RaiseProgress("سال مالی", 0, years.Count, "ایجاد/بازیابی سال‌های مالی...")
Dim ensured = Await api.EnsureFiscalYearsAsync(businessId, slices, years.First().StartDate, ct).ConfigureAwait(False)
ApplyFiscalYearIds(years, ensured.Items)
If years.Any(Function(y) y.HesabixId <= 0) Then
Dim listed = Await api.ListFiscalYearsAsync(businessId, ct).ConfigureAwait(False)
ApplyFiscalYearIds(years, listed)
End If
Dim unresolved = years.Where(Function(y) y.HesabixId <= 0).Select(Function(y) y.Title).ToList()
If unresolved.Count > 0 Then
RaiseProgress("سال مالی", years.Count, years.Count,
"آماده: ایجاد " & ensured.CreatedCount.ToString() & " / موجود " & ensured.ReusedCount.ToString() &
" — بدون شناسه: " & String.Join("، ", unresolved), True)
Else
RaiseProgress("سال مالی", years.Count, years.Count,
"آماده: ایجاد " & ensured.CreatedCount.ToString() & " / موجود " & ensured.ReusedCount.ToString() &
" — همه " & years.Count.ToString() & " سال نگاشت شد")
End If
End Function
Private Async Function AlignDefaultFiscalYearForMigration(
api As HesabixApiClient,
businessId As Integer,
years As List(Of DetectedFiscalYear),
ct As CancellationToken
) As Task
If years Is Nothing OrElse years.Count = 0 Then Return
Dim listed As List(Of HesabixFiscalYear) = Nothing
Try
listed = Await api.ListFiscalYearsAsync(businessId, ct).ConfigureAwait(False)
Catch
Return
End Try
If listed Is Nothing OrElse listed.Count <> 1 Then Return
Dim only = listed(0)
Dim existingStart As Date
Dim existingEnd As Date
If Not TryParseApiDate(If(Not String.IsNullOrWhiteSpace(only.StartDate), only.StartDate, ""), existingStart) Then Return
' end از raw ترجیح دارد؛ اگر نبود از Start+1year تقریبی کافی نیست — فقط با start چک همپوشانی
Dim endRawOk = TryParseApiDate(If(only.EndDate, ""), existingEnd)
If Not endRawOk Then existingEnd = existingStart.AddYears(1).AddDays(-1)
Dim overlaps = years.Any(Function(y) y.StartDate <= existingEnd AndAlso y.EndDate >= existingStart)
If Not overlaps Then Return
Dim first = years.OrderBy(Function(y) y.StartDate).First()
RaiseProgress("سال مالی", 0, years.Count,
"سال پیش‌فرض همپوشان یافت شد — تنظیم به «" & first.Title & "»...")
Try
Await api.UpdateCurrentFiscalYearAsync(businessId, first.Title, first.StartDate, first.EndDate, ct).ConfigureAwait(False)
first.HesabixId = only.Id
Catch ex As Exception
RaiseProgress("سال مالی", 0, years.Count, "هشدار تنظیم سال پیش‌فرض: " & ex.Message, True)
End Try
End Function
Private Shared Sub ApplyFiscalYearIds(years As List(Of DetectedFiscalYear), items As IEnumerable(Of HesabixFiscalYear))
If items Is Nothing Then Return
For Each item In items
If item Is Nothing OrElse item.Id <= 0 Then Continue For
Dim match = MatchDetectedFiscalYear(years, item)
If match IsNot Nothing AndAlso match.HesabixId <= 0 Then
match.HesabixId = item.Id
End If
Next
End Sub
Private Shared Function MatchDetectedFiscalYear(years As List(Of DetectedFiscalYear), item As HesabixFiscalYear) As DetectedFiscalYear
Dim startParsed As Date
If TryParseApiDate(item.StartDate, startParsed) Then
Dim byDate = years.FirstOrDefault(Function(y) y.HesabixId <= 0 AndAlso y.StartDate = startParsed.Date)
If byDate IsNot Nothing Then Return byDate
' تحمل یک روز اختلاف به‌خاطر timezone/جلالی
byDate = years.FirstOrDefault(Function(y) y.HesabixId <= 0 AndAlso Math.Abs((y.StartDate - startParsed.Date).TotalDays) <= 1)
If byDate IsNot Nothing Then Return byDate
End If
If Not String.IsNullOrWhiteSpace(item.Title) Then
Dim byTitle = years.FirstOrDefault(Function(y) y.HesabixId <= 0 AndAlso
String.Equals(y.Title.Trim(), item.Title.Trim(), StringComparison.OrdinalIgnoreCase))
If byTitle IsNot Nothing Then Return byTitle
End If
Return Nothing
End Function
Private Shared Function TryParseApiDate(value As String, ByRef parsed As Date) As Boolean
parsed = Date.MinValue
If String.IsNullOrWhiteSpace(value) Then Return False
Dim s = value.Trim()
' ترجیح ISO گریگوری: yyyy-MM-dd
Dim exact As Date
If Date.TryParseExact(s, "yyyy-MM-dd", Globalization.CultureInfo.InvariantCulture,
Globalization.DateTimeStyles.None, exact) Then
parsed = exact.Date
Return True
End If
If Date.TryParseExact(s, "yyyy-MM-ddTHH:mm:ss", Globalization.CultureInfo.InvariantCulture,
Globalization.DateTimeStyles.AssumeUniversal Or Globalization.DateTimeStyles.AdjustToUniversal, exact) Then
parsed = exact.Date
Return True
End If
' جلالی نمایشی مثل 1402/01/01 را عمداً رد می‌کنیم تا با سال میلادی اشتباه نشود
If s.Contains("/"c) AndAlso s.Length >= 8 Then
Dim parts = s.Split("/"c)
Dim yPart As Integer
If parts.Length >= 1 AndAlso Integer.TryParse(parts(0), yPart) AndAlso yPart > 1300 AndAlso yPart < 1600 Then
Return False
End If
End If
If Date.TryParse(s, Globalization.CultureInfo.InvariantCulture, Globalization.DateTimeStyles.AssumeLocal, exact) Then
parsed = exact.Date
Return True
End If
Return False
End Function
Private Async Function TransferOpeningBalance(
session As MigrationSession,
api As HesabixApiClient,
businessId As Integer,
currencyId As Integer,
fy As DetectedFiscalYear,
personMap As ModuleCheckpoint,
bankMap As ModuleCheckpoint,
cashMap As ModuleCheckpoint,
pettyMap As ModuleCheckpoint,
warehouseMap As ModuleCheckpoint,
productMap As ModuleCheckpoint,
mapper As HolooSarfaslMapper,
accountCodeMap As Dictionary(Of String, Integer),
catalog As SarfaslCatalog,
profile As SarfaslProfile,
cp As TransferCheckpoint,
store As CheckpointStore,
ct As CancellationToken
) As Task
Dim modCp = cp.EnsureModule(MigrationModule.FiscalYearsAndOpening.ToString())
Dim obKey = "OB:" & fy.HesabixId.ToString()
If modCp.Done.ContainsKey(obKey) Then
RaiseProgress("افتتاحیه", 0, 0, "قبلاً ثبت شده — رد شد")
Return
End If
Dim sanadCode = Await Task.Run(Function() _docs.FindOpeningSanadCode(session.SqlSettings), ct).ConfigureAwait(False)
If sanadCode <= 0 Then
RaiseProgress("افتتاحیه", 0, 0, "سند افتتاحیه در هلو یافت نشد — رد شد", True)
Return
End If
Dim lines = Await Task.Run(Function() _docs.ReadOpeningLines(session.SqlSettings, sanadCode), ct).ConfigureAwait(False)
RaiseProgress("افتتاحیه", 0, lines.Count, "ساخت تراز افتتاحیه از سند " & sanadCode.ToString() & "...")
Dim arId = Await ResolveAccountId(accountCodeMap, api, businessId, "10401", ct).ConfigureAwait(False)
Dim apId = Await ResolveAccountId(accountCodeMap, api, businessId, "20201", ct).ConfigureAwait(False)
Dim bankAccId = Await ResolveAccountId(accountCodeMap, api, businessId, "10203", ct).ConfigureAwait(False)
Dim cashAccId = Await ResolveAccountId(accountCodeMap, api, businessId, "10202", ct).ConfigureAwait(False)
Dim pettyAccId = Await ResolveAccountId(accountCodeMap, api, businessId, "10201", ct).ConfigureAwait(False)
Dim invAccId = Await ResolveAccountId(accountCodeMap, api, businessId, "10102", ct).ConfigureAwait(False)
Dim notesPayId = Await ResolveAccountId(accountCodeMap, api, businessId, "20202", ct).ConfigureAwait(False)
Dim equityId = Await ResolveAccountId(accountCodeMap, api, businessId, "30106", ct).ConfigureAwait(False)
If equityId <= 0 Then equityId = Await ResolveAccountId(accountCodeMap, api, businessId, "30101", ct).ConfigureAwait(False)
Dim merged As New Dictionary(Of String, ObMergeBucket)(StringComparer.Ordinal)
Dim skipped = 0
Dim blockReasons As New List(Of String)
For Each ln In lines
If ln.Debit <= 0 AndAlso ln.Credit <= 0 Then Continue For
Dim col = If(ln.ColCode, "").Trim()
If col = "005" OrElse col = "006" Then Continue For
If col = "401" AndAlso mapper.IsLoan401(ln.MoienCode) Then
Dim loanAcctId As Integer = 0
If catalog IsNot Nothing Then
loanAcctId = Await catalog.EnsureLoanBusinessAccountAsync(ln.MoienCode, ln.SarfaslName, ct).ConfigureAwait(False)
End If
If loanAcctId <= 0 Then
skipped += 1
blockReasons.Add("وام بدون حساب: " & ln.SarfaslName)
Continue For
End If
Dim bLoan = GetOrAddBucket(merged, "A:LOAN:" & ln.MoienCode)
bLoan.AccountId = loanAcctId
bLoan.AccountCode = HolooSarfaslMapper.SuggestBusinessLoanCode(ln.MoienCode)
bLoan.Debit += ln.Debit
bLoan.Credit += ln.Credit
bLoan.Description = If(String.IsNullOrWhiteSpace(bLoan.Description), If(ln.SarfaslName, ln.Comment), bLoan.Description)
ElseIf col = "103" OrElse col = "401" Then
Dim personId = mapper.ResolvePersonOnCol(col, ln.MoienCode)
If personId <= 0 Then personId = ResolvePersonByMoien(personMap, ln.MoienCode)
If personId <= 0 Then
skipped += 1
blockReasons.Add("شخص یافت نشد: " & col & "|" & ln.MoienCode & " " & ln.SarfaslName)
Continue For
End If
Dim b = GetOrAddBucket(merged, "P:" & personId.ToString())
b.PersonId = personId
b.IsApSide = (col = "401") OrElse b.IsApSide
b.Debit += ln.Debit
b.Credit += ln.Credit
b.Description = If(String.IsNullOrWhiteSpace(b.Description), If(ln.SarfaslName, ln.Comment), b.Description)
ElseIf col = "102" Then
Dim bankId = mapper.ResolveBank(col, ln.MoienCode, ln.TafziliCode, Nothing, ln.SarfaslName)
If bankAccId <= 0 OrElse bankId <= 0 Then
skipped += 1
blockReasons.Add("بانک نگاشت نشد: " & ln.SarfaslName)
Continue For
End If
Dim b = GetOrAddBucket(merged, "B:" & bankId.ToString())
b.BankAccountId = bankId
b.AccountId = bankAccId
b.Debit += ln.Debit
b.Credit += ln.Credit
b.Description = If(String.IsNullOrWhiteSpace(b.Description), If(ln.SarfaslName, ln.Comment), b.Description)
ElseIf col = "101" Then
Dim isPetty As Boolean
Dim cashOrPettyId = mapper.ResolveCashOrPetty(col, ln.MoienCode, ln.TafziliCode, isPetty)
Dim acct = If(isPetty, pettyAccId, cashAccId)
If acct <= 0 OrElse cashOrPettyId <= 0 Then
skipped += 1
blockReasons.Add("صندوق/تنخواه نگاشت نشد: " & ln.SarfaslName)
Continue For
End If
Dim b = GetOrAddBucket(merged, If(isPetty, "T:", "C:") & cashOrPettyId.ToString())
b.AccountId = acct
If isPetty Then b.PettyCashId = cashOrPettyId Else b.CashRegisterId = cashOrPettyId
b.Debit += ln.Debit
b.Credit += ln.Credit
b.Description = If(String.IsNullOrWhiteSpace(b.Description), If(ln.SarfaslName, ln.Comment), b.Description)
ElseIf col = "402" Then
If notesPayId <= 0 Then
skipped += 1
blockReasons.Add("اسناد پرداختنی (402) — حساب 20202 یافت نشد")
Continue For
End If
Dim b = GetOrAddBucket(merged, "A:20202")
b.AccountId = notesPayId
b.AccountCode = "20202"
b.Debit += ln.Debit
b.Credit += ln.Credit
b.Description = If(String.IsNullOrWhiteSpace(b.Description), If(ln.SarfaslName, "اسناد پرداختنی"), b.Description)
Else
Dim code = ResolveMappedAccountCode(profile, col, ln.MoienCode, ln.SarfaslName)
If String.IsNullOrWhiteSpace(code) Then
skipped += 1
blockReasons.Add("سرفصل بدون نگاشت: " & col & "|" & ln.MoienCode & " " & ln.SarfaslName)
Continue For
End If
Dim acctId As Integer = 0
If accountCodeMap Is Nothing OrElse Not accountCodeMap.TryGetValue(code, acctId) OrElse acctId <= 0 Then
acctId = Await ResolveAccountId(accountCodeMap, api, businessId, code, ct).ConfigureAwait(False)
End If
If acctId <= 0 Then
skipped += 1
blockReasons.Add("کد حسابیکس یافت نشد: " & code)
Continue For
End If
Dim b = GetOrAddBucket(merged, "A:" & code)
b.AccountId = acctId
b.AccountCode = code
b.Debit += ln.Debit
b.Credit += ln.Credit
b.Description = If(String.IsNullOrWhiteSpace(b.Description), If(ln.SarfaslName, ln.Comment), b.Description)
End If
Next
If blockReasons.Count > 0 Then
RaiseProgress("افتتاحیه", 0, 0,
"نگاشت ناقص — افتتاحیه ثبت نمی‌شود (" & blockReasons.Count.ToString() & "): " &
String.Join("؛ ", blockReasons.Take(8)), True)
modCp.Failed(obKey) = "خطوط نگاشت‌نشده: " & blockReasons.Count.ToString()
store.Save(cp)
Return
End If
Dim accountLines As New JArray()
For Each kv In merged
Dim b = kv.Value
Dim debit = ApiDateFormat.RoundMoney(b.Debit)
Dim credit = ApiDateFormat.RoundMoney(b.Credit)
If debit > 0 AndAlso credit > 0 Then
If debit >= credit Then
debit = ApiDateFormat.RoundMoney(debit - credit)
credit = 0
Else
credit = ApiDateFormat.RoundMoney(credit - debit)
debit = 0
End If
End If
If debit <= 0 AndAlso credit <= 0 Then Continue For
If b.PersonId > 0 Then
Dim preferAp = b.IsApSide OrElse credit > debit
Dim acctId = If(preferAp, apId, arId)
If acctId <= 0 Then acctId = If(debit >= credit, arId, apId)
If acctId <= 0 Then Continue For
accountLines.Add(New JObject From {
{"account_id", acctId},
{"person_id", b.PersonId},
{"debit", debit},
{"credit", credit},
{"description", b.Description}
})
ElseIf b.BankAccountId > 0 Then
accountLines.Add(New JObject From {
{"account_id", If(b.AccountId > 0, b.AccountId, bankAccId)},
{"bank_account_id", b.BankAccountId},
{"debit", debit},
{"credit", credit},
{"description", b.Description}
})
ElseIf b.CashRegisterId > 0 OrElse b.PettyCashId > 0 Then
Dim jo As New JObject From {
{"account_id", b.AccountId},
{"debit", debit},
{"credit", credit},
{"description", b.Description}
}
If b.CashRegisterId > 0 Then jo("cash_register_id") = b.CashRegisterId Else jo("petty_cash_id") = b.PettyCashId
accountLines.Add(jo)
ElseIf b.AccountId > 0 Then
accountLines.Add(New JObject From {
{"account_id", b.AccountId},
{"debit", debit},
{"credit", credit},
{"description", b.Description}
})
End If
Next
' مقدار کالا برای کاردکس؛ cost_price=0 تا GL موجودی از Col106 دوبل نشود
Dim inventoryLines As New JArray()
Dim defaultWhId = If(warehouseMap.Done.Count = 1, warehouseMap.Done.Values.First(), 0)
Dim qtyRows = Await Task.Run(Function() _docs.ReadProductOpeningQuantities(session.SqlSettings), ct).ConfigureAwait(False)
For Each q In qtyRows
Dim productId As Integer = 0
If Not productMap.Done.TryGetValue(q.Item1, productId) OrElse productId <= 0 Then Continue For
Dim whId As Integer = 0
If q.Item4.HasValue AndAlso warehouseMap.Done.ContainsKey(q.Item4.Value.ToString()) Then
whId = warehouseMap.Done(q.Item4.Value.ToString())
ElseIf defaultWhId > 0 Then
whId = defaultWhId
End If
If whId <= 0 Then Continue For
inventoryLines.Add(New JObject From {
{"product_id", productId},
{"quantity", Math.Round(q.Item2, 4)},
{"description", "موجودی اول دوره Holoo"},
{"extra_info", New JObject From {
{"warehouse_id", whId},
{"movement", "in"},
{"cost_price", 0}
}}
})
Next
If accountLines.Count < 2 AndAlso inventoryLines.Count = 0 Then
RaiseProgress("افتتاحیه", 0, 0, "خطوط قابل نگاشت کافی نیست (رد شده: " & skipped.ToString() & ")", True)
Return
End If
' بستن اختلاف ریالی در کلاینت تا به وابستگی نسخه سرور (تلرانس 0.01) نباشد
If equityId > 0 AndAlso accountLines.Count > 0 Then
Dim sumDebit As Double = 0
Dim sumCredit As Double = 0
For Each lnTok As JToken In accountLines
Dim ln = TryCast(lnTok, JObject)
If ln Is Nothing Then Continue For
sumDebit += CDbl(If(ln("debit"), 0))
sumCredit += CDbl(If(ln("credit"), 0))
Next
Dim balDiff = ApiDateFormat.RoundMoney(sumDebit - sumCredit)
If Math.Abs(balDiff) >= 0.01 Then
If balDiff > 0 Then
accountLines.Add(New JObject From {
{"account_id", equityId},
{"debit", 0},
{"credit", balDiff},
{"description", "بستن اختلاف تراز افتتاحیه (مهاجرت)"}
})
Else
accountLines.Add(New JObject From {
{"account_id", equityId},
{"debit", Math.Abs(balDiff)},
{"credit", 0},
{"description", "بستن اختلاف تراز افتتاحیه (مهاجرت)"}
})
End If
RaiseProgress("افتتاحیه", 0, 0, "اختلاف " & balDiff.ToString("0.00") & " به حقوق صاحبان سهام بسته شد")
End If
End If
Dim payload As New JObject From {
{"fiscal_year_id", fy.HesabixId},
{"document_date", ApiDateFormat.ToIsoDate(fy.StartDate)},
{"currency_id", currencyId},
{"account_lines", accountLines},
{"inventory_lines", inventoryLines},
{"auto_balance_to_equity", True}
}
If invAccId > 0 Then payload("inventory_account_id") = invAccId
If equityId > 0 Then payload("equity_account_id") = equityId
Try
Await api.UpsertOpeningBalanceAsync(businessId, payload, ct).ConfigureAwait(False)
Await api.PostOpeningBalanceAsync(businessId, fy.HesabixId, ct).ConfigureAwait(False)
modCp.Done(obKey) = fy.HesabixId
modCp.Failed.Remove(obKey)
If modCp.Failed.Count = 0 Then modCp.Completed = True
store.Save(cp)
RaiseProgress("افتتاحیه", accountLines.Count, accountLines.Count,
"ثبت شد: " & accountLines.Count.ToString() & " خط حساب + " & inventoryLines.Count.ToString() &
" قلم موجودی (رد " & skipped.ToString() & ")")
Catch ex As Exception
modCp.Failed(obKey) = ex.Message
store.Save(cp)
RaiseProgress("افتتاحیه", 0, 0, "خطا: " & ex.Message, True)
End Try
End Function
Private Shared Function GetOrAddBucket(merged As Dictionary(Of String, ObMergeBucket), key As String) As ObMergeBucket
Dim b As ObMergeBucket = Nothing
If Not merged.TryGetValue(key, b) Then
b = New ObMergeBucket()
merged(key) = b
End If
Return b
End Function
Private Class ObMergeBucket
Public Property AccountId As Integer
Public Property AccountCode As String
Public Property PersonId As Integer
Public Property BankAccountId As Integer
Public Property CashRegisterId As Integer
Public Property PettyCashId As Integer
Public Property IsApSide As Boolean
Public Property Debit As Double
Public Property Credit As Double
Public Property Description As String
End Class
Private Shared Async Function ResolveAccountId(
map As Dictionary(Of String, Integer),
api As HesabixApiClient,
businessId As Integer,
code As String,
ct As CancellationToken
) As Task(Of Integer)
Dim id As Integer
If map IsNot Nothing AndAlso map.TryGetValue(code, id) AndAlso id > 0 Then Return id
Return Await api.FindAccountIdByCodeAsync(businessId, code, ct).ConfigureAwait(False)
End Function
Private Async Function TransferInvoicesForYear(
session As MigrationSession,
api As HesabixApiClient,
businessId As Integer,
currencyId As Integer,
fy As DetectedFiscalYear,
personMap As ModuleCheckpoint,
productMap As ModuleCheckpoint,
invoiceMap As ModuleCheckpoint,
mapper As HolooSarfaslMapper,
defaultCashId As Integer,
defaultBankId As Integer,
defaultPettyId As Integer,
moneyToCurrency As Dictionary(Of Integer, Integer),
isMultiCurrency As Boolean,
cp As TransferCheckpoint,
store As CheckpointStore,
ct As CancellationToken
) As Task(Of List(Of Integer))
Dim createdIds As New List(Of Integer)
RaiseProgress("فاکتور " & fy.Title, 0, 0, "خواندن فاکتورها از هلو...")
Dim rows = Await Task.Run(Function() _docs.ReadInvoicesInRange(session.SqlSettings, fy.StartDate, fy.EndDate), ct).ConfigureAwait(False)
Dim settleMap = Await Task.Run(
Function() _docs.ReadSettlementHintsForSanads(session.SqlSettings, rows.Select(Function(r) r.SanadCode)),
ct).ConfigureAwait(False)
Dim pendingRaw = rows.Where(Function(r) Not invoiceMap.Done.ContainsKey(r.Key)).ToList()
Dim pending As List(Of HolooInvoiceHeader)
If SampleLimit > 0 AndAlso pendingRaw.Count > SampleLimit Then
' از هر Fac_Type تا SampleLimit نمونه (مثلاً ۲ فروش + ۲ خرید)
pending = pendingRaw.
GroupBy(Function(r) If(r.FacType, "").Trim().ToUpperInvariant()).
SelectMany(Function(g) g.OrderBy(Function(x) x.FacDate).Take(SampleLimit)).
OrderBy(Function(x) x.FacDate).ThenBy(Function(x) x.FacCode).
ToList()
RaiseProgress("فاکتور " & fy.Title, 0, pendingRaw.Count,
"حالت نمونه: " & pending.Count.ToString() & " فاکتور متنوع از " & pendingRaw.Count.ToString())
Else
pending = pendingRaw
End If
RaiseProgress("فاکتور " & fy.Title, invoiceMap.Done.Count, rows.Count,
"آماده‌سازی " & pending.Count.ToString() & " فاکتور (از " & rows.Count.ToString() & ")...")
Dim queue As New ConcurrentQueue(Of PreparedInvoiceItem)()
Dim processed = rows.Count - pending.Count
For Each inv In pending
ct.ThrowIfCancellationRequested()
Dim hint As HolooInvoiceSettlementHint = Nothing
If inv.SanadCode > 0 Then settleMap.TryGetValue(inv.SanadCode, hint)
Dim failReason As String = Nothing
Dim payload = BuildInvoicePayload(
inv, currencyId, personMap, productMap, mapper, hint,
defaultCashId, defaultBankId, defaultPettyId,
moneyToCurrency, isMultiCurrency, failReason)
If payload Is Nothing Then
invoiceMap.Failed(inv.Key) = If(failReason, "نگاشت شخص/کالا/تسویه ناقص یا نوع نامعتبر")
processed += 1
RaiseProgress("فاکتور " & fy.Title, processed, rows.Count, "رد: " & inv.Key & " — " & invoiceMap.Failed(inv.Key), True)
Continue For
End If
queue.Enqueue(New PreparedInvoiceItem With {
.Key = inv.Key,
.Payload = payload
})
Next
Dim workerCount = Math.Max(1, Math.Min(InvoiceParallelism, Math.Max(1, queue.Count)))
RaiseProgress("فاکتور " & fy.Title, processed, rows.Count,
"ارسال موازی " & queue.Count.ToString() & " فاکتور با " & workerCount.ToString() & " کارگر...")
Dim state As New InvoiceParallelState With {
.Processed = processed,
.TotalRows = rows.Count,
.ModuleTitle = "فاکتور " & fy.Title,
.Gate = New Object(),
.CreatedIds = createdIds,
.SuccessSinceSave = 0
}
Dim workers As New List(Of Task)()
For i = 1 To workerCount
workers.Add(InvoiceWorkerLoopAsync(api, businessId, fy, queue, invoiceMap, store, cp, state, ct))
Next
Await Task.WhenAll(workers.ToArray()).ConfigureAwait(False)
SyncLock state.Gate
store.Save(cp)
End SyncLock
RaiseProgress(state.ModuleTitle, state.Processed, state.TotalRows,
"پایان فاکتور سال: موفق " & invoiceMap.Done.Count.ToString() &
" / شکست " & invoiceMap.Failed.Count.ToString())
Return createdIds
End Function
Private Class PreparedInvoiceItem
Public Property Key As String
Public Property Payload As JObject
End Class
Private Class InvoiceParallelState
Public Property Processed As Integer
Public Property TotalRows As Integer
Public Property ModuleTitle As String
Public Property Gate As Object
Public Property CreatedIds As List(Of Integer)
Public Property SuccessSinceSave As Integer
End Class
Private Async Function InvoiceWorkerLoopAsync(
api As HesabixApiClient,
businessId As Integer,
fy As DetectedFiscalYear,
queue As ConcurrentQueue(Of PreparedInvoiceItem),
invoiceMap As ModuleCheckpoint,
store As CheckpointStore,
cp As TransferCheckpoint,
state As InvoiceParallelState,
ct As CancellationToken
) As Task
While True
ct.ThrowIfCancellationRequested()
Dim batch As New List(Of PreparedInvoiceItem)()
Dim one As PreparedInvoiceItem = Nothing
While batch.Count < InvoiceChunk AndAlso queue.TryDequeue(one)
batch.Add(one)
End While
If batch.Count = 0 Then Exit While
Dim items As New JArray()
Dim keys As New List(Of String)()
For Each it In batch
items.Add(New JObject From {
{"client_ref", it.Key},
{"payload", it.Payload}
})
keys.Add(it.Key)
Next
Dim localProcessed As New IntHolder With {.Value = 0}
Dim localCreated As New List(Of Integer)()
Dim localMap As New ModuleCheckpoint()
' heartbeat: suppressProgress hides per-batch noise, but long API waits must still surface
SyncLock state.Gate
RaiseProgress(state.ModuleTitle, state.Processed, state.TotalRows,
"ارسال دسته " & batch.Count.ToString() & " فاکتور (صف ~" & queue.Count.ToString() & ")...")
End SyncLock
Await SendInvoiceChunkWithRetry(
api, businessId, fy, items, keys, localMap, localCreated, localProcessed, state.TotalRows, store, cp, ct,
skipCheckpointSave:=True, suppressProgress:=True).ConfigureAwait(False)
Dim showProgress As Boolean = False
Dim progressCurrent As Integer = 0
Dim progressMsg As String = Nothing
Dim progressIsError As Boolean = False
SyncLock state.Gate
For Each k In keys
state.Processed += 1
Dim doneId As Integer = 0
Dim failMsg As String = Nothing
If localMap.Done.TryGetValue(k, doneId) AndAlso doneId > 0 Then
invoiceMap.Done(k) = doneId
invoiceMap.Failed.Remove(k)
state.CreatedIds.Add(doneId)
state.SuccessSinceSave += 1
ElseIf localMap.Failed.TryGetValue(k, failMsg) Then
invoiceMap.Failed(k) = If(failMsg, "خطا")
progressIsError = True
progressMsg = "خطا: " & k & " — " & invoiceMap.Failed(k)
state.SuccessSinceSave = 0
Else
' timeout رها‌شده: Failed نکن تا resume بعدی دوباره صف کند
state.Processed -= 1
state.SuccessSinceSave = 0
End If
Next
progressCurrent = state.Processed
If state.SuccessSinceSave >= 5 OrElse (state.Processed Mod 10 = 0) OrElse progressIsError Then
store.Save(cp)
If Not progressIsError Then state.SuccessSinceSave = 0
End If
If progressIsError OrElse state.Processed Mod 5 = 0 OrElse state.Processed >= state.TotalRows Then
showProgress = True
If Not progressIsError Then
progressMsg = "موازی: " & invoiceMap.Done.Count.ToString() & " موفق / صف ~" & queue.Count.ToString()
End If
End If
End SyncLock
If showProgress Then
RaiseProgress(state.ModuleTitle, progressCurrent, state.TotalRows, progressMsg, progressIsError)
End If
End While
End Function
Private Shared Function IsTransientTimeout(ex As Exception) As Boolean
If ex Is Nothing Then Return False
If TypeOf ex Is OperationCanceledException Then
' CancelAfter per-request — transient. Parent cancel is checked by caller via ct.
Return True
End If
If TypeOf ex Is TaskCanceledException Then Return True
If TypeOf ex.InnerException Is TaskCanceledException Then Return True
If TypeOf ex.InnerException Is OperationCanceledException Then Return True
If TypeOf ex Is TimeoutException Then Return True
Dim apiEx = TryCast(ex, HesabixApiException)
If apiEx IsNot Nothing Then
If apiEx.StatusCode = 408 OrElse apiEx.StatusCode = 429 OrElse
apiEx.StatusCode = 500 OrElse
apiEx.StatusCode = 502 OrElse apiEx.StatusCode = 503 OrElse apiEx.StatusCode = 504 Then
Return True
End If
End If
Return IsTransientFailureMessage(If(ex.Message, ""))
End Function
Private Shared Function IsTransientFailureMessage(msg As String) As Boolean
If String.IsNullOrWhiteSpace(msg) Then Return False
Return msg.IndexOf("canceled", StringComparison.OrdinalIgnoreCase) >= 0 OrElse
msg.IndexOf("timeout", StringComparison.OrdinalIgnoreCase) >= 0 OrElse
msg.IndexOf("زمان", StringComparison.OrdinalIgnoreCase) >= 0 OrElse
msg.IndexOf("504", StringComparison.OrdinalIgnoreCase) >= 0 OrElse
msg.IndexOf("502", StringComparison.OrdinalIgnoreCase) >= 0 OrElse
msg.IndexOf("503", StringComparison.OrdinalIgnoreCase) >= 0 OrElse
msg.IndexOf("500", StringComparison.OrdinalIgnoreCase) >= 0 OrElse
msg.IndexOf("internal", StringComparison.OrdinalIgnoreCase) >= 0 OrElse
msg.IndexOf("خطای داخلی", StringComparison.OrdinalIgnoreCase) >= 0 OrElse
msg.IndexOf("خطا داخلي", StringComparison.OrdinalIgnoreCase) >= 0 OrElse
msg.IndexOf("temporarily", StringComparison.OrdinalIgnoreCase) >= 0 OrElse
msg.IndexOf("Unavailable", StringComparison.OrdinalIgnoreCase) >= 0
End Function
Private Async Function SendInvoiceChunkWithRetry(
api As HesabixApiClient,
businessId As Integer,
fy As DetectedFiscalYear,
items As JArray,
chunkKeys As List(Of String),
invoiceMap As ModuleCheckpoint,
createdIds As List(Of Integer),
processedHolder As IntHolder,
totalRows As Integer,
store As CheckpointStore,
cp As TransferCheckpoint,
ct As CancellationToken,
Optional skipCheckpointSave As Boolean = False,
Optional suppressProgress As Boolean = False
) As Task
If items Is Nothing OrElse items.Count = 0 Then Return
Dim attempt = 0
Const maxAttempts As Integer = 5
While attempt < maxAttempts
attempt += 1
If ct.IsCancellationRequested Then Throw New OperationCanceledException(ct)
Dim bulk As BulkUpsertResult = Nothing
Dim sendErr As Exception = Nothing
Try
Using invCts = CancellationTokenSource.CreateLinkedTokenSource(ct)
invCts.CancelAfter(TimeSpan.FromSeconds(InvoiceRequestTimeoutSeconds))
bulk = Await api.BulkUpsertInvoicesAsync(businessId, items, invCts.Token).ConfigureAwait(False)
End Using
Catch ex As Exception
If ct.IsCancellationRequested Then Throw
sendErr = ex
End Try
If sendErr Is Nothing AndAlso bulk IsNot Nothing Then
Dim byClientRef = IndexByClientRef(bulk)
Dim retryItems As New JArray()
Dim retryKeys As New List(Of String)()
For i = 0 To chunkKeys.Count - 1
Dim k = chunkKeys(i)
Dim r = ResolveItem(byClientRef, bulk, i, k)
If r IsNot Nothing AndAlso r.IsSuccess AndAlso r.EntityId > 0 Then
processedHolder.Value += 1
invoiceMap.Done(k) = r.EntityId
invoiceMap.Failed.Remove(k)
createdIds.Add(r.EntityId)
Else
Dim msg = If(r Is Nothing, "نتیجه برنگشت", If(r.Message, r.ErrorCode))
If IsTransientFailureMessage(If(msg, "")) AndAlso attempt < maxAttempts Then
retryItems.Add(items(i))
retryKeys.Add(k)
Else
processedHolder.Value += 1
invoiceMap.Failed(k) = If(msg, "خطا")
RaiseProgress("فاکتور " & fy.Title, processedHolder.Value, totalRows, "خطا: " & k & " — " & invoiceMap.Failed(k), True)
End If
End If
Next
If retryItems.Count > 0 AndAlso attempt < maxAttempts Then
Dim delayMs = 2000 * attempt
RaiseProgress("فاکتور " & fy.Title, processedHolder.Value, totalRows,
"خطای موقت روی " & retryItems.Count.ToString() & " فاکتور — صبر " & (delayMs \ 1000).ToString() & "ث...", True)
Await Task.Delay(delayMs, ct).ConfigureAwait(False)
items = retryItems
chunkKeys = retryKeys
Continue While
End If
If Not suppressProgress Then
RaiseProgress("فاکتور " & fy.Title, processedHolder.Value, totalRows,
"دسته: ایجاد " & bulk.Created.ToString() & " / شکست " & bulk.Failed.ToString())
End If
If Not skipCheckpointSave Then store.Save(cp)
Return
End If
If IsTransientTimeout(sendErr) AndAlso items.Count > 1 Then
Dim half = Math.Max(1, items.Count \ 2)
RaiseProgress("فاکتور " & fy.Title, processedHolder.Value, totalRows,
"timeout — شکستن دسته به " & half.ToString() & " + " & (items.Count - half).ToString() & "...", True)
Dim a As New JArray()
Dim b As New JArray()
Dim ak As New List(Of String)
Dim bk As New List(Of String)
For i = 0 To items.Count - 1
If i < half Then
a.Add(items(i)) : ak.Add(chunkKeys(i))
Else
b.Add(items(i)) : bk.Add(chunkKeys(i))
End If
Next
Await SendInvoiceChunkWithRetry(api, businessId, fy, a, ak, invoiceMap, createdIds, processedHolder, totalRows, store, cp, ct, skipCheckpointSave, suppressProgress).ConfigureAwait(False)
Await SendInvoiceChunkWithRetry(api, businessId, fy, b, bk, invoiceMap, createdIds, processedHolder, totalRows, store, cp, ct, skipCheckpointSave, suppressProgress).ConfigureAwait(False)
Return
End If
If IsTransientTimeout(sendErr) AndAlso attempt < maxAttempts Then
Dim delayMs = 3000 * attempt
RaiseProgress("فاکتور " & fy.Title, processedHolder.Value, totalRows,
"timeout/504 — صبر " & (delayMs \ 1000).ToString() & "ث و تلاش " & (attempt + 1).ToString() & "...", True)
Await Task.Delay(delayMs, ct).ConfigureAwait(False)
Continue While
End If
' timeout مکرر: Failed نکن تا resume بعدی دوباره صف کند
If IsTransientTimeout(sendErr) Then
RaiseProgress("فاکتور " & fy.Title, processedHolder.Value, totalRows,
"timeout پایدار روی " & chunkKeys.Count.ToString() & " فاکتور — برای resume بعدی رها شد", True)
Return
End If
Dim errMsg = If(sendErr Is Nothing, "خطای ناشناخته", sendErr.Message)
For Each k In chunkKeys
invoiceMap.Failed(k) = errMsg
processedHolder.Value += 1
RaiseProgress("فاکتور " & fy.Title, processedHolder.Value, totalRows, "خطای bulk: " & k & " — " & errMsg, True)
Next
If Not skipCheckpointSave Then store.Save(cp)
Return
End While
End Function
Private Class IntHolder
Public Property Value As Integer
End Class
Private Shared Function BuildInvoicePayload(
inv As HolooInvoiceHeader,
currencyId As Integer,
personMap As ModuleCheckpoint,
productMap As ModuleCheckpoint,
mapper As HolooSarfaslMapper,
settleHint As HolooInvoiceSettlementHint,
defaultCashId As Integer,
defaultBankId As Integer,
defaultPettyId As Integer,
moneyToCurrency As Dictionary(Of Integer, Integer),
isMultiCurrency As Boolean,
ByRef failReason As String
) As JObject
failReason = Nothing
Dim invType = HolooDocumentReader.MapFacTypeToInvoiceType(inv.FacType)
If String.IsNullOrWhiteSpace(invType) Then
failReason = "نوع فاکتور نامعتبر: " & inv.FacType.ToString()
Return Nothing
End If
Dim needsPerson = HolooDocumentReader.InvoiceRequiresPerson(inv.FacType)
Dim personId As Integer = 0
If needsPerson Then
If Not personMap.Done.TryGetValue(inv.CustomerCode, personId) OrElse personId <= 0 Then
failReason = "شخص نگاشت نشد: " & inv.CustomerCode
Return Nothing
End If
End If
Dim invCurrencyId = currencyId
If isMultiCurrency AndAlso moneyToCurrency IsNot Nothing Then
Dim foreignCode = inv.Lines.
Where(Function(ln) ln.MoneyCode > 0 AndAlso moneyToCurrency.ContainsKey(ln.MoneyCode)).
Select(Function(ln) ln.MoneyCode).
GroupBy(Function(c) c).
OrderByDescending(Function(g) g.Count()).
Select(Function(g) g.Key).
FirstOrDefault()
If foreignCode > 0 Then invCurrencyId = moneyToCurrency(foreignCode)
End If
Dim lines As New JArray()
Dim missingProducts As New List(Of String)()
For Each ln In inv.Lines
If String.IsNullOrWhiteSpace(ln.ArticleCode) OrElse ln.Quantity = 0 Then Continue For
Dim productId As Integer = 0
If Not productMap.Done.TryGetValue(ln.ArticleCode, productId) OrElse productId <= 0 Then
missingProducts.Add(ln.ArticleCode)
Continue For
End If
Dim unitPrice = ln.UnitPrice
If isMultiCurrency AndAlso ln.ForeignUnitPrice > 0 AndAlso ln.MoneyCode > 0 AndAlso
moneyToCurrency IsNot Nothing AndAlso moneyToCurrency.ContainsKey(ln.MoneyCode) AndAlso
moneyToCurrency(ln.MoneyCode) <> currencyId Then
unitPrice = ln.ForeignUnitPrice
End If
Dim taxAmt = ln.Levy + ln.Scot
Dim lineExtra As New JObject From {
{"unit_price", Math.Round(unitPrice, 2)},
{"line_discount", Math.Round(ln.LineDiscount, 2)},
{"tax_amount", Math.Round(taxAmt, 2)},
{"holoo_money_code", ln.MoneyCode}
}
lines.Add(New JObject From {
{"product_id", productId},
{"quantity", ln.Quantity},
{"extra_info", lineExtra}
})
Next
If missingProducts.Count > 0 Then
failReason = "کالای نگاشت‌نشده: " & String.Join(",", missingProducts.Distinct().Take(5))
Return Nothing
End If
If lines.Count = 0 Then
failReason = "هیچ خط کالای معتبری ندارد"
Return Nothing
End If
Dim cashId = 0
Dim bankId = 0
Dim extraUnsettleNote As String = Nothing
If settleHint IsNot Nothing Then
If Not String.IsNullOrWhiteSpace(settleHint.CashMoien) Then
Dim isPetty As Boolean
Dim resolved = mapper.ResolveCashOrPetty("101", settleHint.CashMoien, settleHint.CashTafzili, isPetty)
If resolved > 0 Then cashId = resolved
End If
If Not String.IsNullOrWhiteSpace(settleHint.BankMoien) OrElse Not String.IsNullOrWhiteSpace(settleHint.BankName) Then
Dim resolvedBank = mapper.ResolveBank("102", settleHint.BankMoien, settleHint.BankTafzili, Nothing, settleHint.BankName)
If resolvedBank > 0 Then bankId = resolvedBank
End If
End If
' بدون dump به sole-default: فقط resolve قطعی از hint/سرفصل
Dim payments As New JArray()
If inv.FNaghd > 0 AndAlso cashId > 0 Then
Dim isPettyPay As Boolean = False
If settleHint IsNot Nothing AndAlso Not String.IsNullOrWhiteSpace(settleHint.CashMoien) Then
mapper.ResolveCashOrPetty("101", settleHint.CashMoien, settleHint.CashTafzili, isPettyPay)
End If
If isPettyPay Then
payments.Add(New JObject From {
{"amount", Math.Round(inv.FNaghd, 2)},
{"transaction_type", "petty_cash"},
{"petty_cash_id", cashId},
{"transaction_date", ApiDateFormat.ToIsoDate(inv.FacDate)}
})
Else
payments.Add(New JObject From {
{"amount", Math.Round(inv.FNaghd, 2)},
{"transaction_type", "cash_register"},
{"cash_register_id", cashId},
{"transaction_date", ApiDateFormat.ToIsoDate(inv.FacDate)}
})
End If
ElseIf inv.FNaghd > 0 AndAlso cashId <= 0 Then
' تسویه نقد نمی‌نشیند؛ مانده نسیه می‌ماند (کل فاکتور Fail نمی‌شود)
extraUnsettleNote = "نقد بدون نگاشت صندوق"
End If
Dim cardOrHaval = inv.Card + inv.FHaval
If cardOrHaval > 0 AndAlso bankId > 0 Then
payments.Add(New JObject From {
{"amount", Math.Round(cardOrHaval, 2)},
{"transaction_type", "bank"},
{"bank_id", bankId},
{"transaction_date", ApiDateFormat.ToIsoDate(inv.FacDate)}
})
ElseIf cardOrHaval > 0 AndAlso bankId <= 0 Then
If String.IsNullOrWhiteSpace(extraUnsettleNote) Then
extraUnsettleNote = "کارت/حواله بدون نگاشت بانک"
Else
extraUnsettleNote &= "; کارت/حواله بدون نگاشت بانک"
End If
End If
' FCheck: مانده به‌صورت نسیه (AR/AP) می‌ماند؛ ماژول چک با ایجاد چک، AR→اسناد دریافتنی را می‌بندد
Dim extra As New JObject From {
{"post_inventory", False},
{"auto_post_warehouse", False},
{"ignore_credit_check", True},
{"source", "holoo"},
{"holoo_fac_type", inv.FacType},
{"holoo_fac_code", inv.FacCode},
{"holoo_sanad_code", inv.SanadCode}
}
If personId > 0 Then extra("person_id") = personId
If inv.Takhfif > 0 Then
extra("global_discount") = New JObject From {
{"type", "amount"},
{"value", Math.Round(inv.Takhfif, 2)}
}
End If
If inv.FCheck > 0 Then
extra("holoo_fcheck") = Math.Round(inv.FCheck, 2)
extra("holoo_check_settlement") = "via_checks_module"
End If
If Not String.IsNullOrWhiteSpace(extraUnsettleNote) Then
extra("holoo_unsettle_note") = extraUnsettleNote
End If
Dim payload As New JObject From {
{"invoice_type", invType},
{"document_date", ApiDateFormat.ToIsoDate(inv.FacDate)},
{"currency_id", invCurrencyId},
{"description", If(String.IsNullOrWhiteSpace(inv.Comment), "Holoo " & inv.Key, inv.Comment)},
{"lines", lines},
{"extra_info", extra}
}
If payments.Count > 0 Then payload("payments") = payments
Return payload
End Function
Private Async Function PostWarehouseForInvoices(
api As HesabixApiClient,
businessId As Integer,
invoiceIds As List(Of Integer),
cp As TransferCheckpoint,
store As CheckpointStore,
ct As CancellationToken
) As Task
Dim modCp = cp.EnsureModule(MigrationModule.WarehouseDocs.ToString())
Dim offset = 0
While offset < invoiceIds.Count
ct.ThrowIfCancellationRequested()
Dim take = Math.Min(WarehouseChunk, invoiceIds.Count - offset)
Dim chunk = invoiceIds.GetRange(offset, take)
Dim chunkKey = "WH:" & String.Join(",", chunk)
If modCp.Done.ContainsKey(chunkKey) Then
offset += take
Continue While
End If
Dim attempt = 0
Dim ok As Boolean = False
Dim lastErr As String = Nothing
While attempt < 3 AndAlso Not ok
attempt += 1
Dim retryDelayMs As Integer = 0
Try
Await api.BulkWarehouseOperationsAsync(businessId, chunk, "create_draft", ct).ConfigureAwait(False)
Await api.BulkWarehouseOperationsAsync(businessId, chunk, "post_drafts", ct).ConfigureAwait(False)
ok = True
Catch ex As Exception
lastErr = If(ex.Message, "خطا")
If (IsTransientTimeout(ex) OrElse IsTransientFailureMessage(lastErr)) AndAlso attempt < 3 Then
retryDelayMs = 2000 * attempt
End If
End Try
If Not ok AndAlso retryDelayMs > 0 Then
Await Task.Delay(retryDelayMs, ct).ConfigureAwait(False)
ElseIf Not ok Then
Exit While
End If
End While
If ok Then
modCp.Done(chunkKey) = chunk.Count
modCp.Failed.Remove(chunkKey)
For Each id In chunk
modCp.Done("INVWH:" & id.ToString()) = id
Next
RaiseProgress("انبار", offset + take, invoiceIds.Count, "حواله draft/post برای " & take.ToString() & " فاکتور")
Else
modCp.Failed(chunkKey) = If(lastErr, "خطای انبار")
RaiseProgress("انبار", offset + take, invoiceIds.Count, "خطای انبار: " & If(lastErr, "?"), True)
End If
store.Save(cp)
offset += take
End While
End Function
Private Async Function TransferStandaloneReceipts(
session As MigrationSession,
api As HesabixApiClient,
businessId As Integer,
currencyId As Integer,
fy As DetectedFiscalYear,
personMap As ModuleCheckpoint,
mapper As HolooSarfaslMapper,
defaultCashId As Integer,
defaultBankId As Integer,
defaultPettyId As Integer,
cp As TransferCheckpoint,
store As CheckpointStore,
ct As CancellationToken
) As Task
Dim modCp = cp.EnsureModule(MigrationModule.ReceiptsPayments.ToString())
Dim rows = Await Task.Run(Function() _docs.ReadReceiptLikeSanadsInRange(session.SqlSettings, fy.StartDate, fy.EndDate), ct).ConfigureAwait(False)
Dim pending = ApplySample(rows.Where(Function(r) Not modCp.Done.ContainsKey(r.Key)).ToList(), "دریافت/پرداخت " & fy.Title)
RaiseProgress("دریافت/پرداخت " & fy.Title, modCp.Done.Count, rows.Count, "ارسال " & pending.Count.ToString() & " سند...")
Dim offset = 0
Dim processed = rows.Count - pending.Count
While offset < pending.Count
ct.ThrowIfCancellationRequested()
Dim take = Math.Min(ReceiptChunk, pending.Count - offset)
Dim chunk = pending.GetRange(offset, take)
Dim items As New JArray()
Dim keys As New List(Of String)
For Each row In chunk
Dim personId = mapper.ResolvePerson(row.PersonMoien)
If personId <= 0 Then personId = ResolvePersonByMoien(personMap, row.PersonMoien)
If personId <= 0 Then
modCp.Failed(row.Key) = "شخص یافت نشد: " & row.PersonMoien
processed += 1
Continue For
End If
Dim payload = BuildReceiptPaymentPayload(row, currencyId, personId, mapper, defaultCashId, defaultBankId, defaultPettyId)
If payload Is Nothing Then
modCp.Failed(row.Key) = "حساب بانکی/صندوق قابل نگاشت نیست"
processed += 1
Continue For
End If
items.Add(New JObject From {{"client_ref", row.Key}, {"payload", payload}})
keys.Add(row.Key)
Next
If items.Count = 0 Then
offset += take
store.Save(cp)
Continue While
End If
Dim attempt = 0
Dim sent As Boolean = False
While attempt < 3 AndAlso Not sent
attempt += 1
Dim retryDelayMs As Integer = 0
Dim catchFail As Boolean = False
Dim catchMsg As String = Nothing
Try
Dim bulk = Await api.BulkUpsertReceiptsPaymentsAsync(businessId, items, True, ct).ConfigureAwait(False)
Dim byClientRef = IndexByClientRef(bulk)
Dim retryItems As New JArray()
Dim retryKeys As New List(Of String)()
For i = 0 To keys.Count - 1
Dim k = keys(i)
Dim r = ResolveItem(byClientRef, bulk, i, k)
If r IsNot Nothing AndAlso r.IsSuccess AndAlso r.EntityId > 0 Then
processed += 1
modCp.Done(k) = r.EntityId
modCp.Failed.Remove(k)
Else
Dim msg = If(r Is Nothing, "نتیجه برنگشت", If(r.Message, r.ErrorCode))
If IsTransientFailureMessage(If(msg, "")) AndAlso attempt < 3 Then
retryItems.Add(items(i))
retryKeys.Add(k)
Else
processed += 1
modCp.Failed(k) = If(msg, "خطا")
RaiseProgress("دریافت/پرداخت", processed, rows.Count, "خطا: " & k, True)
End If
End If
Next
If retryItems.Count > 0 AndAlso attempt < 3 Then
items = retryItems
keys = retryKeys
retryDelayMs = 2000 * attempt
Else
RaiseProgress("دریافت/پرداخت " & fy.Title, processed, rows.Count,
"دسته ایجاد " & bulk.Created.ToString() & " / شکست " & bulk.Failed.ToString())
sent = True
End If
Catch ex As Exception
If IsTransientTimeout(ex) AndAlso attempt < 3 Then
retryDelayMs = 3000 * attempt
Else
catchFail = True
catchMsg = ex.Message
sent = True
End If
End Try
If catchFail Then
For Each k In keys
modCp.Failed(k) = catchMsg
processed += 1
Next
RaiseProgress("دریافت/پرداخت", processed, rows.Count, "خطای bulk: " & catchMsg, True)
ElseIf retryDelayMs > 0 AndAlso Not sent Then
Await Task.Delay(retryDelayMs, ct).ConfigureAwait(False)
End If
End While
store.Save(cp)
offset += take
End While
If SampleLimit <= 0 AndAlso modCp.Failed.Count = 0 Then modCp.Completed = True
store.Save(cp)
End Function
Private Shared Function BuildReceiptPaymentPayload(
row As HolooReceiptLikeSanad,
currencyId As Integer,
personId As Integer,
mapper As HolooSarfaslMapper,
defaultCashId As Integer,
defaultBankId As Integer,
defaultPettyId As Integer
) As JObject
Dim amount = Math.Round(row.PersonAmount, 2)
If amount <= 0 Then Return Nothing
Dim accountLine As JObject = Nothing
If row.CounterCol = "101" Then
Dim isPetty As Boolean
Dim cashId = mapper.ResolveCashOrPetty(row.CounterCol, row.CounterMoien, row.CounterTafzili, isPetty)
' ممنوع: dump به صندوق/تنخواه پیش‌فرض — بدون resolve قطعی Fail
If cashId <= 0 Then Return Nothing
If isPetty Then
accountLine = New JObject From {
{"amount", amount},
{"transaction_type", "petty_cash"},
{"petty_cash_id", cashId},
{"transaction_date", ApiDateFormat.ToIsoDate(row.SanadDate)}
}
Else
accountLine = New JObject From {
{"amount", amount},
{"transaction_type", "cash_register"},
{"cash_register_id", cashId},
{"transaction_date", ApiDateFormat.ToIsoDate(row.SanadDate)}
}
End If
Else
Dim bankId = mapper.ResolveBank(row.CounterCol, row.CounterMoien, row.CounterTafzili, Nothing, row.CounterName)
If bankId <= 0 Then Return Nothing
accountLine = New JObject From {
{"amount", amount},
{"transaction_type", "bank"},
{"bank_id", bankId},
{"transaction_date", ApiDateFormat.ToIsoDate(row.SanadDate)}
}
End If
Return New JObject From {
{"document_type", If(row.IsReceipt, "receipt", "payment")},
{"document_date", ApiDateFormat.ToIsoDate(row.SanadDate)},
{"currency_id", currencyId},
{"description", If(String.IsNullOrWhiteSpace(row.Comment), "Holoo " & row.Key, row.Comment)},
{"person_lines", New JArray From {
New JObject From {{"person_id", personId}, {"amount", amount}}
}},
{"account_lines", New JArray From {accountLine}},
{"extra_info", New JObject From {{"source", "holoo"}, {"holoo_sanad_code", row.SanadCode}}}
}
End Function
Private Async Function TransferChecksForYear(
session As MigrationSession,
api As HesabixApiClient,
businessId As Integer,
currencyId As Integer,
fy As DetectedFiscalYear,
personMap As ModuleCheckpoint,
mapper As HolooSarfaslMapper,
defaultBankId As Integer,
earliestStart As Date,
isFirstYear As Boolean,
cp As TransferCheckpoint,
store As CheckpointStore,
ct As CancellationToken
) As Task
Dim modCp = cp.EnsureModule(MigrationModule.Checks.ToString())
Dim clearCp = cp.EnsureModule("ChecksClear")
Dim rows = Await Task.Run(Function() _docs.ReadChecksInRange(session.SqlSettings, fy.StartDate, fy.EndDate), ct).ConfigureAwait(False)
' ایجاد در سال صدور؛ چک‌های قبل از اولین سال کشف‌شده در سال اول ثبت می‌شوند
Dim toCreate = ApplySample(rows.Where(Function(r) Not r.IsVoid AndAlso Not modCp.Done.ContainsKey(r.Key) AndAlso (
(r.IssueDate >= fy.StartDate AndAlso r.IssueDate <= fy.EndDate) OrElse
(isFirstYear AndAlso r.IssueDate < earliestStart)
)).ToList(), "چک " & fy.Title)
RaiseProgress("چک " & fy.Title, modCp.Done.Count, rows.Count, "ایجاد " & toCreate.Count.ToString() & " چک...")
Dim i = 0
For Each row In toCreate
ct.ThrowIfCancellationRequested()
i += 1
Dim personCode = If(row.IsPayable, If(row.DestPersonCode, row.SourcePersonCode), row.SourcePersonCode)
Dim personId = mapper.ResolvePerson(personCode)
If personId <= 0 Then personId = ResolvePersonByMoien(personMap, personCode)
If personId <= 0 AndAlso Not String.IsNullOrWhiteSpace(row.DestPersonCode) Then
personId = mapper.ResolvePerson(row.DestPersonCode)
If personId <= 0 Then personId = ResolvePersonByMoien(personMap, row.DestPersonCode)
End If
If personId <= 0 Then
modCp.Failed(row.Key) = "شخص چک یافت نشد: " & personCode
RaiseProgress("چک", i, toCreate.Count, "رد شخص: " & row.Key, True)
store.Save(cp)
Continue For
End If
If row.Amount <= 0 Then
modCp.Failed(row.Key) = "مبلغ نامعتبر"
Continue For
End If
Dim issue = row.IssueDate
Dim due = row.DueDate
If due < issue Then due = issue
Dim checkNumber = If(String.IsNullOrWhiteSpace(row.CheckNumber), "HLO-" & row.CheckCode.ToString(), row.CheckNumber.Trim())
Dim payload As New JObject From {
{"type", If(row.IsPayable, "transferred", "received")},
{"person_id", personId},
{"issue_date", ApiDateFormat.ToIsoDate(issue)},
{"due_date", ApiDateFormat.ToIsoDate(due)},
{"check_number", checkNumber},
{"amount", Math.Round(row.Amount, 2)},
{"currency_id", currencyId},
{"document_date", ApiDateFormat.ToIsoDate(issue)},
{"document_description", If(String.IsNullOrWhiteSpace(row.Comment), "Holoo " & row.Key, row.Comment)}
}
If Not String.IsNullOrWhiteSpace(row.BankName) Then payload("bank_name") = row.BankName
Dim createdId As Integer = 0
Dim createError As String = Nothing
Dim attempt = 0
While attempt < 3 AndAlso createdId <= 0
attempt += 1
createError = Nothing
Dim retryDelayMs As Integer = 0
Try
createdId = Await api.CreateCheckAsync(businessId, payload, ct).ConfigureAwait(False)
Catch ex As Exception
createError = If(ex.Message, "خطا")
If (IsTransientTimeout(ex) OrElse IsTransientFailureMessage(createError)) AndAlso attempt < 3 Then
retryDelayMs = 2000 * attempt
End If
End Try
If createdId > 0 Then Exit While
If retryDelayMs > 0 Then
Await Task.Delay(retryDelayMs, ct).ConfigureAwait(False)
Continue While
End If
If createError IsNot Nothing Then Exit While
If attempt < 3 Then
Await Task.Delay(1500 * attempt, ct).ConfigureAwait(False)
End If
End While
If createdId <= 0 AndAlso createError IsNot Nothing AndAlso
createError.IndexOf("DUPLICATE", StringComparison.OrdinalIgnoreCase) >= 0 Then
payload("check_number") = "H" & row.CheckCode.ToString() & "-" & checkNumber
createError = Nothing
Try
createdId = Await api.CreateCheckAsync(businessId, payload, ct).ConfigureAwait(False)
Catch ex2 As Exception
createError = If(ex2.Message, "خطا")
End Try
End If
If createdId > 0 Then
modCp.Done(row.Key) = createdId
modCp.Failed.Remove(row.Key)
RaiseProgress("چک " & fy.Title, i, toCreate.Count, "ایجاد: " & CStr(payload("check_number")))
If row.IsReturned Then
Try
Await api.ReturnCheckAsync(createdId, "to_drawer", row.DueDate, ct).ConfigureAwait(False)
clearCp.Done("RET:" & row.CheckCode.ToString()) = createdId
Catch exRet As Exception
clearCp.Failed("RET:" & row.CheckCode.ToString()) = exRet.Message
RaiseProgress("عودت چک", i, toCreate.Count, "خطا عودت: " & row.Key & " — " & exRet.Message, True)
End Try
End If
Else
modCp.Failed(row.Key) = If(createError, "خطا")
RaiseProgress("چک", i, toCreate.Count, "خطا: " & row.Key & " — " & modCp.Failed(row.Key), True)
End If
store.Save(cp)
Next
' عودت چک‌هایی که قبلاً ایجاد شده و در این سال برگشت خورده‌اند
Dim returnCp = clearCp
Dim returnCandidates = rows.Where(Function(r) Not r.IsVoid AndAlso r.IsReturned AndAlso
r.DueDate >= fy.StartDate AndAlso r.DueDate <= fy.EndDate).ToList()
For Each row In returnCandidates
ct.ThrowIfCancellationRequested()
Dim retKey = "RET:" & row.CheckCode.ToString()
If returnCp.Done.ContainsKey(retKey) Then Continue For
Dim checkId As Integer = 0
If Not modCp.Done.TryGetValue(row.Key, checkId) OrElse checkId <= 0 Then
returnCp.Failed(retKey) = "چک هنوز ایجاد نشده"
Continue For
End If
Try
Await api.ReturnCheckAsync(checkId, "to_drawer", row.DueDate, ct).ConfigureAwait(False)
returnCp.Done(retKey) = checkId
returnCp.Failed.Remove(retKey)
Catch ex As Exception
returnCp.Failed(retKey) = ex.Message
RaiseProgress("عودت چک", 0, returnCandidates.Count, "خطا: " & row.Key & " — " & ex.Message, True)
End Try
store.Save(cp)
Next
' سپرده به جریان وصول (10404) برای چک‌های DarJaryan که هنوز پاس نشده‌اند
Dim depositCandidates = rows.Where(Function(r) Not r.IsVoid AndAlso Not r.IsReturned AndAlso r.IsInProcess AndAlso Not r.IsCleared AndAlso
r.ClearDate >= fy.StartDate AndAlso r.ClearDate <= fy.EndDate).ToList()
For Each row In depositCandidates
ct.ThrowIfCancellationRequested()
Dim depKey = "DEP:" & row.CheckCode.ToString()
If clearCp.Done.ContainsKey(depKey) Then Continue For
Dim checkId As Integer = 0
If Not modCp.Done.TryGetValue(row.Key, checkId) OrElse checkId <= 0 Then
clearCp.Failed(depKey) = "چک هنوز ایجاد نشده"
Continue For
End If
Dim bankId = mapper.ResolveBank("102", "", "", row.AccountNumber, row.BankName)
If bankId <= 0 Then
clearCp.Failed(depKey) = "بانک سپرده یافت نشد: " & row.AccountNumber
Continue For
End If
Try
Await api.DepositCheckAsync(checkId, bankId, row.ClearDate, ct).ConfigureAwait(False)
clearCp.Done(depKey) = checkId
clearCp.Failed.Remove(depKey)
Catch ex As Exception
clearCp.Failed(depKey) = ex.Message
RaiseProgress("سپرده چک", 0, depositCandidates.Count, "خطا DEP " & row.CheckCode.ToString() & ": " & ex.Message, True)
End Try
store.Save(cp)
Next
' وصول چک‌هایی که در این سال پاس شده‌اند (حتی اگر قبلاً ایجاد شده باشند)
Dim clearCandidates = rows.Where(Function(r) Not r.IsVoid AndAlso Not r.IsReturned AndAlso r.IsCleared AndAlso
r.ClearDate >= fy.StartDate AndAlso r.ClearDate <= fy.EndDate).ToList()
Dim cleared = 0
For Each row In clearCandidates
ct.ThrowIfCancellationRequested()
Dim clearKey = "CLR:" & row.CheckCode.ToString()
If clearCp.Done.ContainsKey(clearKey) Then Continue For
Dim checkId As Integer = 0
If Not modCp.Done.TryGetValue(row.Key, checkId) OrElse checkId <= 0 Then
clearCp.Failed(clearKey) = "چک هنوز ایجاد نشده"
Continue For
End If
Dim bankId = mapper.ResolveBank("102", "", "", row.AccountNumber, row.BankName)
If bankId <= 0 Then
clearCp.Failed(clearKey) = "بانک وصول یافت نشد: " & row.AccountNumber
Continue For
End If
Try
' اگر قبلاً در جریان وصول بوده و deposit نشده، اول deposit
If row.IsInProcess Then
Dim depKey = "DEP:" & row.CheckCode.ToString()
If Not clearCp.Done.ContainsKey(depKey) Then
Try
Await api.DepositCheckAsync(checkId, bankId, row.ClearDate, ct).ConfigureAwait(False)
clearCp.Done(depKey) = checkId
Catch
' ممکن است از قبل deposited باشد — clear را ادامه بده
End Try
End If
End If
Await api.ClearCheckAsync(checkId, bankId, row.ClearDate, ct).ConfigureAwait(False)
clearCp.Done(clearKey) = checkId
clearCp.Failed.Remove(clearKey)
cleared += 1
Catch ex As Exception
clearCp.Failed(clearKey) = ex.Message
RaiseProgress("وصول چک", cleared, clearCandidates.Count, "خطا CLR " & row.CheckCode.ToString() & ": " & ex.Message, True)
End Try
store.Save(cp)
Next
RaiseProgress("وصول چک " & fy.Title, cleared, clearCandidates.Count, "وصول " & cleared.ToString() & " از " & clearCandidates.Count.ToString())
If SampleLimit <= 0 AndAlso modCp.Failed.Count = 0 Then modCp.Completed = True
store.Save(cp)
End Function
Private Async Function TransferExpenseIncomeForYear(
session As MigrationSession,
api As HesabixApiClient,
businessId As Integer,
currencyId As Integer,
fy As DetectedFiscalYear,
personMap As ModuleCheckpoint,
mapper As HolooSarfaslMapper,
accountCodeMap As Dictionary(Of String, Integer),
catalog As SarfaslCatalog,
profile As SarfaslProfile,
defaultCashId As Integer,
defaultBankId As Integer,
defaultPettyId As Integer,
cp As TransferCheckpoint,
store As CheckpointStore,
ct As CancellationToken
) As Task
Dim modCp = cp.EnsureModule(MigrationModule.ExpenseIncome.ToString())
If accountCodeMap Is Nothing Then
accountCodeMap = Await api.ListAccountCodeMapAsync(businessId, ct).ConfigureAwait(False)
End If
If catalog Is Nothing Then catalog = New SarfaslCatalog(api, businessId, accountCodeMap)
Dim expenses = Await Task.Run(Function() _docs.ReadExpenseIncomeSanadsInRange(session.SqlSettings, fy.StartDate, fy.EndDate, False), ct).ConfigureAwait(False)
Dim incomes = Await Task.Run(Function() _docs.ReadExpenseIncomeSanadsInRange(session.SqlSettings, fy.StartDate, fy.EndDate, True), ct).ConfigureAwait(False)
Dim rows = expenses.Concat(incomes).OrderBy(Function(r) r.SanadDate).ThenBy(Function(r) r.SanadCode).ToList()
Dim pending = ApplySample(rows.Where(Function(r) Not modCp.Done.ContainsKey(r.Key)).ToList(), "هزینه/درآمد " & fy.Title)
RaiseProgress("هزینه/درآمد " & fy.Title, modCp.Done.Count, rows.Count, "ارسال " & pending.Count.ToString() & " سند متوازن...")
Dim offset = 0
Dim processed = rows.Count - pending.Count
While offset < pending.Count
ct.ThrowIfCancellationRequested()
Dim take = Math.Min(ExpenseChunk, pending.Count - offset)
Dim chunk = pending.GetRange(offset, take)
Dim items As New JArray()
Dim keys As New List(Of String)
For Each row In chunk
Dim payload = Await BuildExpenseIncomePayloadAsync(
row, currencyId, personMap, mapper, accountCodeMap, catalog, profile,
defaultCashId, defaultBankId, defaultPettyId, ct).ConfigureAwait(False)
If payload Is Nothing Then
modCp.Failed(row.Key) = "نگاشت حساب/طرف‌حساب ناقص"
processed += 1
Continue For
End If
items.Add(New JObject From {{"client_ref", row.Key}, {"payload", payload}})
keys.Add(row.Key)
Next
If items.Count = 0 Then
offset += take
store.Save(cp)
Continue While
End If
Dim attempt = 0
Dim sent As Boolean = False
While attempt < 3 AndAlso Not sent
attempt += 1
Dim retryDelayMs As Integer = 0
Dim catchFail As Boolean = False
Dim catchMsg As String = Nothing
Try
Dim bulk = Await api.BulkUpsertExpenseIncomeAsync(businessId, items, True, ct).ConfigureAwait(False)
Dim byClientRef = IndexByClientRef(bulk)
Dim retryItems As New JArray()
Dim retryKeys As New List(Of String)()
For i = 0 To keys.Count - 1
Dim k = keys(i)
Dim r = ResolveItem(byClientRef, bulk, i, k)
If r IsNot Nothing AndAlso r.IsSuccess AndAlso r.EntityId > 0 Then
processed += 1
modCp.Done(k) = r.EntityId
modCp.Failed.Remove(k)
Else
Dim msg = If(r Is Nothing, "نتیجه برنگشت", If(r.Message, r.ErrorCode))
If IsTransientFailureMessage(If(msg, "")) AndAlso attempt < 3 Then
retryItems.Add(items(i))
retryKeys.Add(k)
Else
processed += 1
modCp.Failed(k) = If(msg, "خطا")
RaiseProgress("هزینه/درآمد", processed, rows.Count, "خطا: " & k, True)
End If
End If
Next
If retryItems.Count > 0 AndAlso attempt < 3 Then
items = retryItems
keys = retryKeys
retryDelayMs = 2000 * attempt
Else
RaiseProgress("هزینه/درآمد " & fy.Title, processed, rows.Count,
"دسته ایجاد " & bulk.Created.ToString() & " / شکست " & bulk.Failed.ToString())
sent = True
End If
Catch ex As Exception
If IsTransientTimeout(ex) AndAlso attempt < 3 Then
retryDelayMs = 3000 * attempt
Else
catchFail = True
catchMsg = ex.Message
sent = True
End If
End Try
If catchFail Then
For Each k In keys
modCp.Failed(k) = catchMsg
processed += 1
Next
RaiseProgress("هزینه/درآمد", processed, rows.Count, "خطای bulk: " & catchMsg, True)
ElseIf retryDelayMs > 0 AndAlso Not sent Then
Await Task.Delay(retryDelayMs, ct).ConfigureAwait(False)
End If
End While
store.Save(cp)
offset += take
End While
If SampleLimit <= 0 AndAlso modCp.Failed.Count = 0 Then modCp.Completed = True
store.Save(cp)
End Function
Private Shared Async Function BuildExpenseIncomePayloadAsync(
row As HolooExpenseIncomeSanad,
currencyId As Integer,
personMap As ModuleCheckpoint,
mapper As HolooSarfaslMapper,
accountCodeMap As Dictionary(Of String, Integer),
catalog As SarfaslCatalog,
profile As SarfaslProfile,
defaultCashId As Integer,
defaultBankId As Integer,
defaultPettyId As Integer,
ct As CancellationToken
) As Task(Of JObject)
Dim itemLines As New JArray()
Dim itemTotal As Double = 0
For Each it In row.ItemLines
If it.Amount <= 0 Then Continue For
Dim name = If(String.IsNullOrWhiteSpace(it.Name),
If(row.IsIncome, mapper.IncomeName(it.MoienCode), mapper.ExpenseName(it.MoienCode)),
it.Name)
Dim code As String = Nothing
If profile IsNot Nothing Then
Dim pe = profile.Find(If(row.IsIncome, "702", "601"), it.MoienCode)
If pe IsNot Nothing AndAlso pe.Exempt Then Return Nothing
If pe IsNot Nothing AndAlso Not String.IsNullOrWhiteSpace(pe.OverrideCode) Then code = pe.OverrideCode.Trim()
End If
If String.IsNullOrWhiteSpace(code) Then
code = If(row.IsIncome,
HolooSarfaslMapper.MapIncomeToFixedCode(name, it.MoienCode),
HolooSarfaslMapper.MapExpenseToFixedCode(name, it.MoienCode))
End If
Dim acctId As Integer = 0
If Not String.IsNullOrWhiteSpace(code) Then
If accountCodeMap Is Nothing OrElse Not accountCodeMap.TryGetValue(code, acctId) OrElse acctId <= 0 Then
If catalog IsNot Nothing Then
acctId = Await catalog.ResolveOrCreateAsync(code, Nothing, Nothing, ct).ConfigureAwait(False)
End If
If acctId <= 0 Then Return Nothing
End If
ElseIf Not row.IsIncome AndAlso catalog IsNot Nothing AndAlso
(mapper.NeedsBusinessExpenseAccount(it.MoienCode) OrElse String.IsNullOrWhiteSpace(code)) Then
acctId = Await catalog.EnsureExpenseBusinessAccountAsync(it.MoienCode, name, ct).ConfigureAwait(False)
If acctId <= 0 Then Return Nothing
ElseIf row.IsIncome AndAlso catalog IsNot Nothing AndAlso String.IsNullOrWhiteSpace(code) Then
acctId = Await catalog.EnsureIncomeBusinessAccountAsync(it.MoienCode, name, ct).ConfigureAwait(False)
If acctId <= 0 Then Return Nothing
Else
Return Nothing
End If
Dim amt = Math.Round(it.Amount, 2)
itemTotal += amt
itemLines.Add(New JObject From {
{"account_id", acctId},
{"amount", amt},
{"description", name}
})
Next
If itemLines.Count = 0 Then Return Nothing
Dim counterLines As New JArray()
Dim cpTotal As Double = 0
For Each cpLine In row.CounterpartyLines
If cpLine.Amount <= 0 Then Continue For
Dim col = If(cpLine.ColCode, "").Trim()
Dim amt = Math.Round(cpLine.Amount, 2)
Dim jo As JObject = Nothing
If col = "103" OrElse (col = "401" AndAlso Not mapper.IsLoan401(cpLine.MoienCode)) Then
Dim personId = mapper.ResolvePersonOnCol(col, cpLine.MoienCode)
If personId <= 0 Then personId = ResolvePersonByMoien(personMap, cpLine.MoienCode)
If personId <= 0 Then Return Nothing
' درآمد+شخص: API قدیمی 1211 می‌خواست؛ از 10401 (کنترل دریافتنی فاکتور) استفاده می‌کنیم
Dim arId As Integer = 0
If row.IsIncome AndAlso accountCodeMap IsNot Nothing Then
accountCodeMap.TryGetValue("10401", arId)
End If
If row.IsIncome AndAlso arId > 0 Then
jo = New JObject From {
{"account_id", arId},
{"person_id", personId},
{"amount", amt},
{"transaction_date", ApiDateFormat.ToIsoDate(row.SanadDate)},
{"description", cpLine.Name}
}
Else
jo = New JObject From {
{"transaction_type", "person"},
{"person_id", personId},
{"amount", amt},
{"transaction_date", ApiDateFormat.ToIsoDate(row.SanadDate)},
{"description", cpLine.Name}
}
End If
ElseIf col = "401" AndAlso mapper.IsLoan401(cpLine.MoienCode) Then
Dim loanId = Await catalog.EnsureLoanBusinessAccountAsync(cpLine.MoienCode, cpLine.Name, ct).ConfigureAwait(False)
If loanId <= 0 Then Return Nothing
jo = New JObject From {
{"transaction_type", "account"},
{"account_id", loanId},
{"amount", amt},
{"transaction_date", ApiDateFormat.ToIsoDate(row.SanadDate)},
{"description", cpLine.Name}
}
ElseIf col = "101" Then
Dim isPetty As Boolean
Dim cashId = mapper.ResolveCashOrPetty(col, cpLine.MoienCode, cpLine.TafziliCode, isPetty)
If cashId <= 0 Then Return Nothing
If isPetty Then
jo = New JObject From {
{"transaction_type", "petty_cash"},
{"petty_cash_id", cashId},
{"amount", amt},
{"transaction_date", ApiDateFormat.ToIsoDate(row.SanadDate)},
{"description", cpLine.Name}
}
Else
jo = New JObject From {
{"transaction_type", "cash_register"},
{"cash_register_id", cashId},
{"amount", amt},
{"transaction_date", ApiDateFormat.ToIsoDate(row.SanadDate)},
{"description", cpLine.Name}
}
End If
ElseIf col = "102" Then
Dim bankId = mapper.ResolveBank(col, cpLine.MoienCode, cpLine.TafziliCode, Nothing, cpLine.Name)
If bankId <= 0 Then Return Nothing
jo = New JObject From {
{"transaction_type", "bank"},
{"bank_id", bankId},
{"amount", amt},
{"transaction_date", ApiDateFormat.ToIsoDate(row.SanadDate)},
{"description", cpLine.Name}
}
Else
Return Nothing
End If
counterLines.Add(jo)
cpTotal += amt
Next
If counterLines.Count = 0 Then Return Nothing
If Math.Abs(itemTotal - cpTotal) > 0.02 Then Return Nothing
Return New JObject From {
{"document_type", If(row.IsIncome, "income", "expense")},
{"document_date", ApiDateFormat.ToIsoDate(row.SanadDate)},
{"currency_id", currencyId},
{"description", If(String.IsNullOrWhiteSpace(row.Comment), "Holoo " & row.Key, row.Comment)},
{"item_lines", itemLines},
{"counterparty_lines", counterLines},
{"extra_info", New JObject From {{"source", "holoo"}, {"holoo_sanad_code", row.SanadCode}, {"holoo_bucket", "EXPENSE_INCOME"}}}
}
End Function
Private Async Function TransferManualJournalsForYear(
session As MigrationSession,
api As HesabixApiClient,
businessId As Integer,
currencyId As Integer,
fy As DetectedFiscalYear,
personMap As ModuleCheckpoint,
mapper As HolooSarfaslMapper,
accountCodeMap As Dictionary(Of String, Integer),
catalog As SarfaslCatalog,
profile As SarfaslProfile,
defaultCashId As Integer,
defaultBankId As Integer,
defaultPettyId As Integer,
cp As TransferCheckpoint,
store As CheckpointStore,
ct As CancellationToken
) As Task
Dim modCp = cp.EnsureModule(MigrationModule.ManualJournals.ToString())
If accountCodeMap Is Nothing Then
accountCodeMap = Await api.ListAccountCodeMapAsync(businessId, ct).ConfigureAwait(False)
End If
If catalog Is Nothing Then catalog = New SarfaslCatalog(api, businessId, accountCodeMap)
Dim rows = Await Task.Run(Function() _docs.ReadManualJournalsInRange(session.SqlSettings, fy.StartDate, fy.EndDate), ct).ConfigureAwait(False)
Dim pending = ApplySample(rows.Where(Function(r) Not modCp.Done.ContainsKey(r.Key)).ToList(), "اسناد دستی " & fy.Title)
RaiseProgress("اسناد دستی " & fy.Title, modCp.Done.Count, rows.Count, "ارسال " & pending.Count.ToString() & " سند متوازن...")
Dim i = 0
For Each row In pending
ct.ThrowIfCancellationRequested()
i += 1
If Not String.IsNullOrWhiteSpace(row.SkipReason) Then
modCp.Failed(row.Key) = row.SkipReason
RaiseProgress("اسناد دستی", i, pending.Count, "رد مبدأ: " & row.Key & " — " & row.SkipReason, True)
store.Save(cp)
Continue For
End If
Dim payload = Await BuildManualJournalPayloadAsync(
row, currencyId, personMap, mapper, accountCodeMap, catalog, profile,
defaultCashId, defaultBankId, defaultPettyId, ct).ConfigureAwait(False)
If payload Is Nothing Then
modCp.Failed(row.Key) = "نگاشت خطوط ناقص یا نامتوازن"
RaiseProgress("اسناد دستی", i, pending.Count, "رد: " & row.Key, True)
store.Save(cp)
Continue For
End If
Try
Dim docId = Await api.CreateManualDocumentAsync(businessId, payload, ct).ConfigureAwait(False)
If docId > 0 Then
modCp.Done(row.Key) = docId
modCp.Failed.Remove(row.Key)
Else
modCp.Failed(row.Key) = "شناسه سند برنگشت"
End If
Catch ex As Exception
modCp.Failed(row.Key) = ex.Message
RaiseProgress("اسناد دستی", i, pending.Count, "خطا: " & row.Key & " — " & ex.Message, True)
End Try
If i Mod 20 = 0 Then
RaiseProgress("اسناد دستی " & fy.Title, i, pending.Count, "پیشرفت...")
store.Save(cp)
End If
Next
If SampleLimit <= 0 AndAlso modCp.Failed.Count = 0 Then modCp.Completed = True
store.Save(cp)
RaiseProgress("اسناد دستی " & fy.Title, pending.Count, pending.Count,
"تمام — موفق " & modCp.Done.Count.ToString() & " / شکست " & modCp.Failed.Count.ToString())
End Function
Private Shared Async Function BuildManualJournalPayloadAsync(
row As HolooManualJournal,
currencyId As Integer,
personMap As ModuleCheckpoint,
mapper As HolooSarfaslMapper,
accountCodeMap As Dictionary(Of String, Integer),
catalog As SarfaslCatalog,
profile As SarfaslProfile,
defaultCashId As Integer,
defaultBankId As Integer,
defaultPettyId As Integer,
ct As CancellationToken
) As Task(Of JObject)
Dim lines As New JArray()
Dim bed As Double = 0
Dim bes As Double = 0
For Each ln In row.Lines
Dim col = If(ln.ColCode, "").Trim()
If col = "005" OrElse col = "006" Then Continue For
Dim debit = Math.Round(ln.Debit, 2)
Dim credit = Math.Round(ln.Credit, 2)
If debit <= 0 AndAlso credit <= 0 Then Continue For
Dim jo As JObject = Nothing
If col = "401" AndAlso mapper.IsLoan401(ln.MoienCode) Then
Dim loanId = Await catalog.EnsureLoanBusinessAccountAsync(ln.MoienCode, ln.SarfaslName, ct).ConfigureAwait(False)
If loanId <= 0 Then Return Nothing
jo = New JObject From {
{"account_id", loanId},
{"debit", debit},
{"credit", credit},
{"description", ln.SarfaslName}
}
ElseIf col = "103" OrElse col = "401" Then
Dim personId = mapper.ResolvePersonOnCol(col, ln.MoienCode)
If personId <= 0 Then personId = ResolvePersonByMoien(personMap, ln.MoienCode)
If personId <= 0 Then Return Nothing
Dim preferAp = (col = "401") OrElse credit > debit
Dim code = If(preferAp, "20201", "10401")
Dim acctId As Integer = 0
If Not accountCodeMap.TryGetValue(code, acctId) OrElse acctId <= 0 Then Return Nothing
jo = New JObject From {
{"account_id", acctId},
{"person_id", personId},
{"debit", debit},
{"credit", credit},
{"description", ln.SarfaslName}
}
ElseIf col = "102" Then
Dim bankId = mapper.ResolveBank(col, ln.MoienCode, ln.TafziliCode, Nothing, ln.SarfaslName)
Dim acctId As Integer = 0
If Not accountCodeMap.TryGetValue("10203", acctId) OrElse acctId <= 0 OrElse bankId <= 0 Then Return Nothing
jo = New JObject From {
{"account_id", acctId},
{"bank_account_id", bankId},
{"debit", debit},
{"credit", credit},
{"description", ln.SarfaslName}
}
ElseIf col = "101" Then
Dim isPetty As Boolean
Dim cashId = mapper.ResolveCashOrPetty(col, ln.MoienCode, ln.TafziliCode, isPetty)
Dim code = If(isPetty, "10201", "10202")
Dim acctId As Integer = 0
If Not accountCodeMap.TryGetValue(code, acctId) OrElse acctId <= 0 OrElse cashId <= 0 Then Return Nothing
jo = New JObject From {
{"account_id", acctId},
{"debit", debit},
{"credit", credit},
{"description", ln.SarfaslName}
}
If isPetty Then jo("petty_cash_id") = cashId Else jo("cash_register_id") = cashId
ElseIf col = "601" Then
Dim code As String = Nothing
If profile IsNot Nothing Then
Dim pe = profile.Find("601", ln.MoienCode)
If pe IsNot Nothing AndAlso pe.Exempt Then Return Nothing
If pe IsNot Nothing AndAlso Not String.IsNullOrWhiteSpace(pe.OverrideCode) Then code = pe.OverrideCode.Trim()
End If
If String.IsNullOrWhiteSpace(code) Then code = HolooSarfaslMapper.MapExpenseToFixedCode(ln.SarfaslName, ln.MoienCode)
Dim acctId As Integer = 0
If Not String.IsNullOrWhiteSpace(code) AndAlso accountCodeMap.TryGetValue(code, acctId) AndAlso acctId > 0 Then
' ok
Else
acctId = Await catalog.EnsureExpenseBusinessAccountAsync(ln.MoienCode, ln.SarfaslName, ct).ConfigureAwait(False)
If acctId <= 0 Then Return Nothing
End If
jo = New JObject From {
{"account_id", acctId},
{"debit", debit},
{"credit", credit},
{"description", ln.SarfaslName}
}
ElseIf col = "702" Then
Dim code As String = Nothing
If profile IsNot Nothing Then
Dim pe = profile.Find("702", ln.MoienCode)
If pe IsNot Nothing AndAlso pe.Exempt Then Return Nothing
If pe IsNot Nothing AndAlso Not String.IsNullOrWhiteSpace(pe.OverrideCode) Then code = pe.OverrideCode.Trim()
End If
If String.IsNullOrWhiteSpace(code) Then code = HolooSarfaslMapper.MapIncomeToFixedCode(ln.SarfaslName, ln.MoienCode)
Dim acctId As Integer = 0
If Not String.IsNullOrWhiteSpace(code) AndAlso accountCodeMap.TryGetValue(code, acctId) AndAlso acctId > 0 Then
' ok
Else
acctId = Await catalog.EnsureIncomeBusinessAccountAsync(ln.MoienCode, ln.SarfaslName, ct).ConfigureAwait(False)
If acctId <= 0 Then Return Nothing
End If
jo = New JObject From {
{"account_id", acctId},
{"debit", debit},
{"credit", credit},
{"description", ln.SarfaslName}
}
Else
Dim code = ResolveMappedAccountCode(profile, col, ln.MoienCode, ln.SarfaslName)
If String.IsNullOrWhiteSpace(code) Then Return Nothing
Dim acctId As Integer = 0
If Not accountCodeMap.TryGetValue(code, acctId) OrElse acctId <= 0 Then
If catalog IsNot Nothing Then
acctId = Await catalog.ResolveOrCreateAsync(code, Nothing, Nothing, ct).ConfigureAwait(False)
End If
If acctId <= 0 Then Return Nothing
End If
jo = New JObject From {
{"account_id", acctId},
{"debit", debit},
{"credit", credit},
{"description", ln.SarfaslName}
}
End If
lines.Add(jo)
bed += debit
bes += credit
Next
If lines.Count < 2 Then Return Nothing
If Math.Abs(bed - bes) > 0.02 Then Return Nothing
Dim bucket = If(If(row.Comment, "").IndexOf("[EI-FALLBACK]", StringComparison.OrdinalIgnoreCase) >= 0,
"EXPENSE_INCOME_FALLBACK", "MANUAL")
Return New JObject From {
{"document_date", ApiDateFormat.ToIsoDate(row.SanadDate)},
{"currency_id", currencyId},
{"description", If(String.IsNullOrWhiteSpace(row.Comment), "Holoo " & row.Key, row.Comment)},
{"lines", lines},
{"extra_info", New JObject From {
{"source", "holoo"},
{"holoo_sanad_code", row.SanadCode},
{"holoo_sanad_type", row.SanadType},
{"holoo_bucket", bucket}
}}
}
End Function
Private Shared Function ResolveMappedAccountCode(
profile As SarfaslProfile,
col As String,
moien As String,
sarfaslName As String
) As String
If profile IsNot Nothing Then
Dim pe = profile.Find(col, moien)
If pe IsNot Nothing Then
If pe.Exempt Then Return Nothing
If Not String.IsNullOrWhiteSpace(pe.OverrideCode) Then Return pe.OverrideCode.Trim()
End If
End If
Return HolooSarfaslMapper.MapColToFixedCode(col, moien, sarfaslName)
End Function
Private Shared Function ResolvePersonByMoien(personMap As ModuleCheckpoint, moien As String) As Integer
If String.IsNullOrWhiteSpace(moien) Then Return 0
Dim key = moien.Trim()
Dim id As Integer
If personMap.Done.TryGetValue(key, id) Then Return id
Dim trimmed = key.TrimStart("0"c)
If trimmed.Length > 0 AndAlso personMap.Done.TryGetValue(trimmed, id) Then Return id
For Each kv In personMap.Done
If String.Equals(kv.Key.TrimStart("0"c), key.TrimStart("0"c), StringComparison.OrdinalIgnoreCase) Then
Return kv.Value
End If
Next
Return 0
End Function
Private Async Function BuildHolooMoneyToCurrencyMapAsync(
session As MigrationSession,
api As HesabixApiClient,
defaultCurrencyId As Integer,
ct As CancellationToken
) As Task(Of Dictionary(Of Integer, Integer))
Dim map As New Dictionary(Of Integer, Integer)
Dim holooMoney = Await Task.Run(Function() New HolooBaseDataReader().ReadCurrencies(session.SqlSettings), ct).ConfigureAwait(False)
Dim hesabix = Await api.ListCurrenciesAsync(ct).ConfigureAwait(False)
For Each hm In holooMoney
Dim matched = hesabix.FirstOrDefault(Function(c) _
String.Equals(If(c.Title, "").Trim(), If(hm.Name, "").Trim(), StringComparison.OrdinalIgnoreCase) OrElse
String.Equals(If(c.Code, "").Trim(), If(hm.Name, "").Trim(), StringComparison.OrdinalIgnoreCase) OrElse
String.Equals(If(c.Code, "").Trim(), hm.Code.ToString(), StringComparison.OrdinalIgnoreCase))
If matched IsNot Nothing AndAlso matched.Id > 0 Then
map(hm.Code) = matched.Id
ElseIf hm.IsBase Then
map(hm.Code) = defaultCurrencyId
End If
Next
If Not map.ContainsKey(1) Then map(1) = defaultCurrencyId
Return map
End Function
Private Shared Function IndexByClientRef(bulk As BulkUpsertResult) As Dictionary(Of String, BulkUpsertItemResult)
Dim map As New Dictionary(Of String, BulkUpsertItemResult)(StringComparer.Ordinal)
If bulk Is Nothing OrElse bulk.Results Is Nothing Then Return map
For Each r In bulk.Results
If Not String.IsNullOrWhiteSpace(r.ClientRef) AndAlso Not map.ContainsKey(r.ClientRef) Then
map(r.ClientRef) = r
End If
Next
Return map
End Function
Private Shared Function ResolveItem(
byClientRef As Dictionary(Of String, BulkUpsertItemResult),
bulk As BulkUpsertResult,
indexInChunk As Integer,
clientRef As String
) As BulkUpsertItemResult
Dim found As BulkUpsertItemResult = Nothing
If byClientRef IsNot Nothing AndAlso byClientRef.TryGetValue(clientRef, found) Then Return found
If bulk IsNot Nothing AndAlso bulk.Results IsNot Nothing Then
For Each r In bulk.Results
If r.Index = indexInChunk Then Return r
Next
If indexInChunk >= 0 AndAlso indexInChunk < bulk.Results.Count Then Return bulk.Results(indexInChunk)
End If
Return Nothing
End Function
Private Sub RaiseProgress(moduleTitle As String, current As Integer, total As Integer, message As String, Optional isError As Boolean = False)
RaiseEvent ProgressChanged(Me, New TransferProgressEventArgs With {
.ModuleTitle = moduleTitle,
.Current = current,
.Total = total,
.Message = message,
.IsError = isError
})
End Sub
End Class