Add configurable finance rules and dashboard basis indicators
This commit is contained in:
@@ -35,6 +35,10 @@ public class ConfigTransferService : IConfigTransferService
|
||||
.ThenBy(x => x.CustomerNumber)
|
||||
.ThenBy(x => x.CustomerNameContains)
|
||||
.ToListAsync();
|
||||
var financeRules = await db.FinanceRules
|
||||
.OrderBy(x => x.SortOrder)
|
||||
.ThenBy(x => x.Id)
|
||||
.ToListAsync();
|
||||
var hanaServers = await db.HanaServers.OrderBy(x => x.Name).ToListAsync();
|
||||
var sites = await db.Sites.OrderBy(x => x.Land).ToListAsync();
|
||||
var rules = await db.FieldTransformationRules.OrderBy(x => x.SortOrder).ThenBy(x => x.Id).ToListAsync();
|
||||
@@ -106,6 +110,19 @@ public class ConfigTransferService : IConfigTransferService
|
||||
Notes = rule.Notes,
|
||||
IsActive = rule.IsActive
|
||||
}).ToList(),
|
||||
FinanceRules = financeRules.Select(rule => new FinanceRule
|
||||
{
|
||||
ScopeKey = rule.ScopeKey,
|
||||
Year = rule.Year,
|
||||
RuleType = rule.RuleType,
|
||||
FieldName = rule.FieldName,
|
||||
MatchType = rule.MatchType,
|
||||
MatchValue = rule.MatchValue,
|
||||
NumericValue = rule.NumericValue,
|
||||
Notes = rule.Notes,
|
||||
SortOrder = rule.SortOrder,
|
||||
IsActive = rule.IsActive
|
||||
}).ToList(),
|
||||
HanaServers = hanaServers.Select(server => new ConfigTransferHanaServer
|
||||
{
|
||||
Key = serverKeyMap[server.Id],
|
||||
@@ -206,6 +223,7 @@ public class ConfigTransferService : IConfigTransferService
|
||||
var existingExchangeRates = await db.CurrencyExchangeRates.ToListAsync();
|
||||
var existingFinanceReferences = await db.FinanceReferences.ToListAsync();
|
||||
var existingFinanceIntercompanyRules = await db.FinanceIntercompanyRules.ToListAsync();
|
||||
var existingFinanceRules = await db.FinanceRules.ToListAsync();
|
||||
var existingSites = await db.Sites.ToListAsync();
|
||||
var existingCentralRecords = await db.CentralSalesRecords.AsNoTracking().ToListAsync();
|
||||
var existingRules = await db.FieldTransformationRules.ToListAsync();
|
||||
@@ -235,6 +253,8 @@ public class ConfigTransferService : IConfigTransferService
|
||||
db.FinanceReferences.RemoveRange(existingFinanceReferences);
|
||||
if (package.FinanceIntercompanyRules.Count > 0 && existingFinanceIntercompanyRules.Count > 0)
|
||||
db.FinanceIntercompanyRules.RemoveRange(existingFinanceIntercompanyRules);
|
||||
if (package.FinanceRules.Count > 0 && existingFinanceRules.Count > 0)
|
||||
db.FinanceRules.RemoveRange(existingFinanceRules);
|
||||
if (existingExchangeRates.Count > 0) db.CurrencyExchangeRates.RemoveRange(existingExchangeRates);
|
||||
if (existingSites.Count > 0) db.Sites.RemoveRange(existingSites);
|
||||
if (existingServers.Count > 0) db.HanaServers.RemoveRange(existingServers);
|
||||
@@ -321,6 +341,23 @@ public class ConfigTransferService : IConfigTransferService
|
||||
}));
|
||||
}
|
||||
|
||||
if (package.FinanceRules.Count > 0)
|
||||
{
|
||||
db.FinanceRules.AddRange(package.FinanceRules.Select(rule => new FinanceRule
|
||||
{
|
||||
ScopeKey = rule.ScopeKey,
|
||||
Year = rule.Year,
|
||||
RuleType = rule.RuleType,
|
||||
FieldName = rule.FieldName,
|
||||
MatchType = rule.MatchType,
|
||||
MatchValue = rule.MatchValue,
|
||||
NumericValue = rule.NumericValue,
|
||||
Notes = rule.Notes,
|
||||
SortOrder = rule.SortOrder,
|
||||
IsActive = rule.IsActive
|
||||
}));
|
||||
}
|
||||
|
||||
var serverIdMap = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var server in package.HanaServers)
|
||||
{
|
||||
|
||||
@@ -46,6 +46,7 @@ public sealed class DashboardPageService : IDashboardPageService
|
||||
{
|
||||
SiteId = s.Id,
|
||||
Land = s.Land,
|
||||
DataBasis = ResolveDataBasis(s, sourceSystem),
|
||||
TSC = s.TSC,
|
||||
Schema = s.Schema,
|
||||
ServerName = string.Equals(sourceSystem?.ConnectionKind, SourceSystemConnectionKinds.SapGateway, StringComparison.OrdinalIgnoreCase)
|
||||
@@ -110,6 +111,32 @@ public sealed class DashboardPageService : IDashboardPageService
|
||||
return string.IsNullOrWhiteSpace(sourceSystem?.CentralServiceUrl) ? "SAP Gateway" : sourceSystem.CentralServiceUrl;
|
||||
}
|
||||
|
||||
private static string ResolveDataBasis(Site site, SourceSystemDefinition? sourceSystem)
|
||||
{
|
||||
if (string.Equals(sourceSystem?.ConnectionKind, SourceSystemConnectionKinds.ManualExcel, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var path = site.ManualImportFilePath ?? string.Empty;
|
||||
var extension = Path.GetExtension(path).TrimStart('.').ToUpperInvariant();
|
||||
|
||||
if (extension is "CSV")
|
||||
return "CSV-Datei";
|
||||
if (extension is "XLS" or "XLSX" or "XLSM")
|
||||
return "Excel-Datei";
|
||||
if (!string.IsNullOrWhiteSpace(path))
|
||||
return "Excel/CSV-Datei";
|
||||
|
||||
return "Manuelle Datei";
|
||||
}
|
||||
|
||||
if (string.Equals(sourceSystem?.ConnectionKind, SourceSystemConnectionKinds.SapGateway, StringComparison.OrdinalIgnoreCase))
|
||||
return "SAP Service";
|
||||
|
||||
if (string.Equals(sourceSystem?.ConnectionKind, SourceSystemConnectionKinds.Hana, StringComparison.OrdinalIgnoreCase))
|
||||
return "Server";
|
||||
|
||||
return string.IsNullOrWhiteSpace(site.SourceSystem) ? "-" : site.SourceSystem;
|
||||
}
|
||||
|
||||
private static List<ConsolidatedDashboardRow> BuildConsolidatedRows(ExportSettings settings)
|
||||
{
|
||||
var outputDirectory = ResolveConsolidatedOutputDirectory(settings);
|
||||
@@ -156,6 +183,7 @@ public sealed class DashboardRow
|
||||
{
|
||||
public int SiteId { get; set; }
|
||||
public string Land { get; set; } = string.Empty;
|
||||
public string DataBasis { get; set; } = string.Empty;
|
||||
public string TSC { get; set; } = string.Empty;
|
||||
public string Schema { get; set; } = string.Empty;
|
||||
public string ServerName { get; set; } = string.Empty;
|
||||
|
||||
@@ -192,4 +192,19 @@ CREATE TABLE FinanceIntercompanyRules (
|
||||
Notes TEXT NOT NULL DEFAULT '',
|
||||
IsActive INTEGER NOT NULL DEFAULT 1
|
||||
);";
|
||||
|
||||
internal static string GetFinanceRulesCreateSql() => @"
|
||||
CREATE TABLE FinanceRules (
|
||||
Id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
ScopeKey TEXT NOT NULL DEFAULT '',
|
||||
Year INTEGER NULL,
|
||||
RuleType TEXT NOT NULL DEFAULT 'Exclude',
|
||||
FieldName TEXT NOT NULL DEFAULT '',
|
||||
MatchType TEXT NOT NULL DEFAULT 'Contains',
|
||||
MatchValue TEXT NOT NULL DEFAULT '',
|
||||
NumericValue TEXT NULL,
|
||||
Notes TEXT NOT NULL DEFAULT '',
|
||||
SortOrder INTEGER NOT NULL DEFAULT 0,
|
||||
IsActive INTEGER NOT NULL DEFAULT 1
|
||||
);";
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ public class DatabaseSchemaMaintenanceService : IDatabaseSchemaMaintenanceServic
|
||||
EnsureCurrencyExchangeRateTable(db);
|
||||
EnsureFinanceReferenceTable(db);
|
||||
EnsureFinanceIntercompanyRuleTable(db);
|
||||
EnsureFinanceRuleTable(db);
|
||||
EnsureSourceSystemDefinitionTable(db);
|
||||
AddColumnIfMissing(db, "SourceSystemDefinitions", "CentralServiceUrl", "TEXT NOT NULL DEFAULT ''");
|
||||
EnsureSapSourceTable(db);
|
||||
@@ -317,6 +318,17 @@ CREATE TABLE IF NOT EXISTS CurrencyExchangeRates (
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
private static void EnsureFinanceRuleTable(AppDbContext db)
|
||||
{
|
||||
var conn = db.Database.GetDbConnection();
|
||||
if (conn.State != System.Data.ConnectionState.Open)
|
||||
conn.Open();
|
||||
|
||||
using var cmd = conn.CreateCommand();
|
||||
cmd.CommandText = DatabaseSchemaSql.GetFinanceRulesCreateSql().Replace("CREATE TABLE", "CREATE TABLE IF NOT EXISTS");
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
private static void EnsureSapJoinTable(AppDbContext db)
|
||||
{
|
||||
var conn = db.Database.GetDbConnection();
|
||||
|
||||
@@ -19,6 +19,7 @@ public class DatabaseSeedService : IDatabaseSeedService
|
||||
EnsureFinanceReferenceDefaults(db);
|
||||
EnsureBudgetExchangeRateDefaults(db);
|
||||
EnsureFinanceIntercompanyRuleDefaults(db);
|
||||
EnsureFinanceRuleDefaults(db);
|
||||
}
|
||||
|
||||
private static void SeedIfEmpty(AppDbContext db)
|
||||
@@ -893,4 +894,39 @@ public class DatabaseSeedService : IDatabaseSeedService
|
||||
if (changed)
|
||||
db.SaveChanges();
|
||||
}
|
||||
|
||||
private static void EnsureFinanceRuleDefaults(AppDbContext db)
|
||||
{
|
||||
if (!CanUseTable(db, "FinanceRules"))
|
||||
return;
|
||||
|
||||
var changed = false;
|
||||
foreach (var item in FinanceRuleEngine.CreateDefaultRules())
|
||||
{
|
||||
var exists = db.FinanceRules.Any(rule =>
|
||||
rule.ScopeKey == item.ScopeKey &&
|
||||
rule.RuleType == item.RuleType &&
|
||||
rule.FieldName == item.FieldName &&
|
||||
rule.MatchType == item.MatchType &&
|
||||
rule.MatchValue == item.MatchValue);
|
||||
|
||||
if (exists)
|
||||
continue;
|
||||
|
||||
db.FinanceRules.Add(item);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (changed)
|
||||
db.SaveChanges();
|
||||
}
|
||||
|
||||
private static bool CanUseTable(AppDbContext db, string tableName)
|
||||
{
|
||||
var conn = db.Database.GetDbConnection();
|
||||
if (conn.State != System.Data.ConnectionState.Open)
|
||||
conn.Open();
|
||||
|
||||
return DatabaseSchemaTools.GetTableColumns(conn, transaction: null, tableName).Count > 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,29 @@
|
||||
using ClosedXML.Excel;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TrafagSalesExporter.Data;
|
||||
using TrafagSalesExporter.Models;
|
||||
|
||||
namespace TrafagSalesExporter.Services;
|
||||
|
||||
public class ExcelExportService : IExcelExportService
|
||||
{
|
||||
private const int GermanyAlphaplanFinanceYear = 2025;
|
||||
private readonly IDbContextFactory<AppDbContext>? _dbFactory;
|
||||
|
||||
public ExcelExportService()
|
||||
{
|
||||
}
|
||||
|
||||
public ExcelExportService(IDbContextFactory<AppDbContext> dbFactory)
|
||||
{
|
||||
_dbFactory = dbFactory;
|
||||
}
|
||||
|
||||
public string CreateExcelFile(string outputDirectory, string tsc, DateTime fileDate, List<SalesRecord> records)
|
||||
{
|
||||
Directory.CreateDirectory(outputDirectory);
|
||||
var fileName = $"Sales_{tsc}_{fileDate:yyyy-MM-dd}.xlsx";
|
||||
var fullPath = Path.Combine(outputDirectory, fileName);
|
||||
WriteWorkbook(fullPath, records, includeFinanceHelpSheet: false);
|
||||
WriteWorkbookWithConfiguredRules(fullPath, records, includeFinanceHelpSheet: false);
|
||||
return fullPath;
|
||||
}
|
||||
|
||||
@@ -21,7 +32,7 @@ public class ExcelExportService : IExcelExportService
|
||||
Directory.CreateDirectory(outputDirectory);
|
||||
var fileName = $"Sales_All_{fileDate:yyyy-MM-dd}.xlsx";
|
||||
var fullPath = Path.Combine(outputDirectory, fileName);
|
||||
WriteWorkbook(fullPath, records, includeFinanceHelpSheet: true);
|
||||
WriteWorkbookWithConfiguredRules(fullPath, records, includeFinanceHelpSheet: true);
|
||||
return fullPath;
|
||||
}
|
||||
|
||||
@@ -36,9 +47,32 @@ public class ExcelExportService : IExcelExportService
|
||||
}
|
||||
|
||||
private static void WriteWorkbook(string fullPath, List<SalesRecord> records, bool includeFinanceHelpSheet)
|
||||
=> WriteWorkbook(fullPath, records, includeFinanceHelpSheet, FinanceRuleEngine.CreateDefaultRules());
|
||||
|
||||
private void WriteWorkbookWithConfiguredRules(string fullPath, List<SalesRecord> records, bool includeFinanceHelpSheet)
|
||||
=> WriteWorkbook(fullPath, records, includeFinanceHelpSheet, LoadFinanceRules());
|
||||
|
||||
private IReadOnlyList<FinanceRule> LoadFinanceRules()
|
||||
{
|
||||
if (_dbFactory is null)
|
||||
return FinanceRuleEngine.CreateDefaultRules();
|
||||
|
||||
using var db = _dbFactory.CreateDbContext();
|
||||
var rules = db.FinanceRules
|
||||
.AsNoTracking()
|
||||
.Where(rule => rule.IsActive)
|
||||
.OrderBy(rule => rule.SortOrder)
|
||||
.ThenBy(rule => rule.Id)
|
||||
.ToList();
|
||||
|
||||
return rules.Count == 0 ? FinanceRuleEngine.CreateDefaultRules() : rules;
|
||||
}
|
||||
|
||||
private static void WriteWorkbook(string fullPath, List<SalesRecord> records, bool includeFinanceHelpSheet, IReadOnlyList<FinanceRule> financeRules)
|
||||
{
|
||||
using var workbook = new XLWorkbook();
|
||||
var ws = workbook.Worksheets.Add("Sales");
|
||||
var financeRuleEngine = new FinanceRuleEngine(financeRules);
|
||||
|
||||
var headers = new[]
|
||||
{
|
||||
@@ -93,7 +127,6 @@ public class ExcelExportService : IExcelExportService
|
||||
}
|
||||
|
||||
var row = 2;
|
||||
var italyBlankSupplierCountryRows = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var record in records)
|
||||
{
|
||||
ws.Cell(row, 1).Value = record.ExtractionDate.ToString("dd.MM.yyyy HH:mm:ss");
|
||||
@@ -131,10 +164,10 @@ public class ExcelExportService : IExcelExportService
|
||||
ws.Cell(row, 33).Value = record.OrderDate?.ToString("dd.MM.yyyy") ?? string.Empty;
|
||||
ws.Cell(row, 34).Value = record.Land;
|
||||
ws.Cell(row, 35).Value = record.DocumentType;
|
||||
var financeDate = ResolveFinanceDate(record);
|
||||
var financeCountryKey = ResolveFinanceCountryKey(record.Land, record.Tsc);
|
||||
var financeInclude = ResolveFinanceInclude(record, financeCountryKey, italyBlankSupplierCountryRows);
|
||||
var financeNetSalesActual = ResolveFinanceNetSalesActual(record, financeCountryKey, financeInclude);
|
||||
var financeDate = financeRuleEngine.ResolveFinanceDate(record, financeCountryKey);
|
||||
var financeInclude = financeRuleEngine.ShouldInclude(record, financeCountryKey);
|
||||
var financeNetSalesActual = financeRuleEngine.ResolveNetSalesActual(record, financeCountryKey, financeInclude);
|
||||
ws.Cell(row, 36).Value = financeDate.Year;
|
||||
ws.Cell(row, 37).Value = financeCountryKey;
|
||||
ws.Cell(row, 38).Value = financeDate.ToString("dd.MM.yyyy");
|
||||
@@ -143,23 +176,24 @@ public class ExcelExportService : IExcelExportService
|
||||
ws.Cell(row, 41).Value = financeInclude && financeNetSalesActual != 0m ? "TRUE" : "FALSE";
|
||||
ws.Cell(row, 42).Value = financeInclude
|
||||
? "Sales Price/Value"
|
||||
: ResolveFinanceExclusionReason(record, financeCountryKey);
|
||||
: financeRuleEngine.ResolveExclusionReason(record, financeCountryKey);
|
||||
row++;
|
||||
}
|
||||
|
||||
ws.Columns().AdjustToContents();
|
||||
if (includeFinanceHelpSheet)
|
||||
{
|
||||
AddFinanceSummarySheet(workbook, records);
|
||||
AddFinanceSummarySheet(workbook, records, financeRules);
|
||||
AddFinanceHelpSheet(workbook);
|
||||
}
|
||||
|
||||
workbook.SaveAs(fullPath);
|
||||
}
|
||||
|
||||
private static void AddFinanceSummarySheet(XLWorkbook workbook, List<SalesRecord> records)
|
||||
private static void AddFinanceSummarySheet(XLWorkbook workbook, List<SalesRecord> records, IReadOnlyList<FinanceRule> financeRules)
|
||||
{
|
||||
var ws = workbook.Worksheets.Add("Finance Summary");
|
||||
var financeRuleEngine = new FinanceRuleEngine(financeRules);
|
||||
ws.Position = 1;
|
||||
ws.Cell(1, 1).Value = "Finance Summary";
|
||||
ws.Cell(1, 1).Style.Font.Bold = true;
|
||||
@@ -183,14 +217,13 @@ public class ExcelExportService : IExcelExportService
|
||||
ws.Cell(4, i + 1).Style.Font.Bold = true;
|
||||
}
|
||||
|
||||
var italyBlankSupplierCountryRows = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
var summaryRows = records
|
||||
.Select(record =>
|
||||
{
|
||||
var financeDate = ResolveFinanceDate(record);
|
||||
var countryKey = ResolveFinanceCountryKey(record.Land, record.Tsc);
|
||||
var rawInclude = ResolveFinanceInclude(record, countryKey, italyBlankSupplierCountryRows);
|
||||
var value = ResolveFinanceNetSalesActual(record, countryKey, rawInclude);
|
||||
var financeDate = financeRuleEngine.ResolveFinanceDate(record, countryKey);
|
||||
var rawInclude = financeRuleEngine.ShouldInclude(record, countryKey);
|
||||
var value = financeRuleEngine.ResolveNetSalesActual(record, countryKey, rawInclude);
|
||||
var include = rawInclude && value != 0m;
|
||||
return new
|
||||
{
|
||||
@@ -298,15 +331,6 @@ public class ExcelExportService : IExcelExportService
|
||||
ws.Columns().AdjustToContents();
|
||||
}
|
||||
|
||||
private static DateTime ResolveFinanceDate(SalesRecord record)
|
||||
{
|
||||
var countryKey = ResolveFinanceCountryKey(record.Land, record.Tsc);
|
||||
if (countryKey.Equals("DE", StringComparison.OrdinalIgnoreCase))
|
||||
return new DateTime(GermanyAlphaplanFinanceYear, 12, 31);
|
||||
|
||||
return record.PostingDate ?? record.InvoiceDate ?? record.ExtractionDate;
|
||||
}
|
||||
|
||||
private static string ResolveFinanceCurrency(SalesRecord record)
|
||||
=> ResolveFinanceCountryKey(record.Land, record.Tsc) switch
|
||||
{
|
||||
@@ -340,108 +364,6 @@ public class ExcelExportService : IExcelExportService
|
||||
return normalizedTsc.Replace("TR", string.Empty);
|
||||
}
|
||||
|
||||
private static bool ResolveFinanceInclude(SalesRecord record, string financeCountryKey, HashSet<string> italyBlankSupplierCountryRows)
|
||||
{
|
||||
if (financeCountryKey.Equals("DE", StringComparison.OrdinalIgnoreCase))
|
||||
return IsIncludedGermanyFinanceRow(record);
|
||||
|
||||
if (!financeCountryKey.Equals("IT", StringComparison.OrdinalIgnoreCase))
|
||||
return true;
|
||||
|
||||
if (IsExcludedItalyCustomer(record))
|
||||
return false;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(record.SupplierCountry))
|
||||
return true;
|
||||
|
||||
return italyBlankSupplierCountryRows.Add(BuildItalyBlankSupplierCountryDeduplicationKey(record));
|
||||
}
|
||||
|
||||
private static string ResolveFinanceExclusionReason(SalesRecord record, string financeCountryKey)
|
||||
{
|
||||
if (financeCountryKey.Equals("IT", StringComparison.OrdinalIgnoreCase) && IsExcludedItalyCustomer(record))
|
||||
return "Excluded IT customer: Trafag Italia";
|
||||
|
||||
if (financeCountryKey.Equals("IT", StringComparison.OrdinalIgnoreCase) && string.IsNullOrWhiteSpace(record.SupplierCountry))
|
||||
return "Excluded IT duplicate without Supplier country";
|
||||
|
||||
if (financeCountryKey.Equals("DE", StringComparison.OrdinalIgnoreCase))
|
||||
return ResolveGermanyExclusionReason(record);
|
||||
|
||||
return "Excluded";
|
||||
}
|
||||
|
||||
private static decimal ResolveFinanceNetSalesActual(SalesRecord record, string financeCountryKey, bool financeInclude)
|
||||
{
|
||||
if (!financeInclude)
|
||||
return 0m;
|
||||
|
||||
if (financeCountryKey.Equals("DE", StringComparison.OrdinalIgnoreCase) && IsGermanyCreditNote(record))
|
||||
return -Math.Abs(record.SalesPriceValue);
|
||||
|
||||
return record.SalesPriceValue;
|
||||
}
|
||||
|
||||
private static bool IsIncludedGermanyFinanceRow(SalesRecord record)
|
||||
=> !IsGermanyTrafagAgRecharge(record) &&
|
||||
!IsGermanyMagneticSenseRecharge(record) &&
|
||||
!IsGermanyCreditNoteAlreadyCapturedInPriorYear(record);
|
||||
|
||||
private static string ResolveGermanyExclusionReason(SalesRecord record)
|
||||
{
|
||||
if (IsGermanyTrafagAgRecharge(record))
|
||||
return "Excluded DE Weiterberechnung Trafag AG";
|
||||
if (IsGermanyMagneticSenseRecharge(record))
|
||||
return "Excluded DE Weiterberechnung Magnetic Sense";
|
||||
if (IsGermanyCreditNoteAlreadyCapturedInPriorYear(record))
|
||||
return "Excluded DE GS2510095 already captured in 2024";
|
||||
|
||||
return "Excluded DE";
|
||||
}
|
||||
|
||||
private static bool IsGermanyTrafagAgRecharge(SalesRecord record)
|
||||
=> NormalizeFinanceText(record.CustomerName) == "TRAFAG AG";
|
||||
|
||||
private static bool IsGermanyMagneticSenseRecharge(SalesRecord record)
|
||||
=> NormalizeFinanceText(record.CustomerName).Contains("MAGNETIC SENSE", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private static bool IsGermanyCreditNote(SalesRecord record)
|
||||
=> (record.InvoiceNumber ?? string.Empty).Trim().StartsWith("GS", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private static bool IsGermanyCreditNoteAlreadyCapturedInPriorYear(SalesRecord record)
|
||||
=> (record.InvoiceNumber ?? string.Empty).Trim().Equals("GS2510095", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private static bool IsExcludedItalyCustomer(SalesRecord record)
|
||||
=> NormalizeFinanceText(record.CustomerName).Contains("TRAFAG ITALIA", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private static string BuildItalyBlankSupplierCountryDeduplicationKey(SalesRecord record)
|
||||
=> string.Join("|",
|
||||
record.Tsc,
|
||||
record.DocumentType,
|
||||
record.DocumentEntry,
|
||||
record.InvoiceNumber,
|
||||
record.PositionOnInvoice,
|
||||
record.Material,
|
||||
record.Name,
|
||||
record.Quantity,
|
||||
record.CustomerNumber,
|
||||
record.CustomerName,
|
||||
record.SalesPriceValue,
|
||||
record.DocumentTotalForeignCurrency,
|
||||
record.DocumentTotalLocalCurrency,
|
||||
record.VatSumForeignCurrency,
|
||||
record.VatSumLocalCurrency,
|
||||
record.PostingDate?.ToString("O") ?? string.Empty,
|
||||
record.InvoiceDate?.ToString("O") ?? string.Empty);
|
||||
|
||||
private static string NormalizeFinanceText(string value)
|
||||
=> (value ?? string.Empty)
|
||||
.Replace("\u00e4", "ae", StringComparison.OrdinalIgnoreCase)
|
||||
.Replace("\u00f6", "oe", StringComparison.OrdinalIgnoreCase)
|
||||
.Replace("\u00fc", "ue", StringComparison.OrdinalIgnoreCase)
|
||||
.Trim()
|
||||
.ToUpperInvariant();
|
||||
|
||||
private static void WriteGenericWorkbook(string fullPath, string worksheetName, IReadOnlyList<IReadOnlyDictionary<string, object?>> rows)
|
||||
{
|
||||
using var workbook = new XLWorkbook();
|
||||
|
||||
@@ -31,35 +31,51 @@ public sealed class FinanceReconciliationService : IFinanceReconciliationService
|
||||
.AsNoTracking()
|
||||
.Where(r => r.IsActive)
|
||||
.ToListAsync();
|
||||
|
||||
var centralRows = await db.CentralSalesRecords
|
||||
var financeRules = await db.FinanceRules
|
||||
.AsNoTracking()
|
||||
.Where(r => (r.PostingDate ?? r.InvoiceDate ?? r.ExtractionDate).Year == year)
|
||||
.Select(r => new NetSalesActualSourceRow(
|
||||
r.Land,
|
||||
r.Tsc,
|
||||
r.DocumentEntry,
|
||||
r.InvoiceNumber,
|
||||
r.PositionOnInvoice,
|
||||
r.Material,
|
||||
r.Name,
|
||||
r.Quantity,
|
||||
r.DocumentType,
|
||||
r.PostingDate,
|
||||
r.InvoiceDate,
|
||||
r.ExtractionDate,
|
||||
r.CustomerNumber,
|
||||
r.CustomerName,
|
||||
r.SupplierCountry,
|
||||
r.SalesCurrency,
|
||||
r.DocumentCurrency,
|
||||
r.CompanyCurrency,
|
||||
r.SalesPriceValue,
|
||||
r.DocumentTotalForeignCurrency,
|
||||
r.DocumentTotalLocalCurrency,
|
||||
r.VatSumForeignCurrency,
|
||||
r.VatSumLocalCurrency))
|
||||
.Where(r => r.IsActive)
|
||||
.OrderBy(r => r.SortOrder)
|
||||
.ThenBy(r => r.Id)
|
||||
.ToListAsync();
|
||||
if (financeRules.Count == 0)
|
||||
financeRules = FinanceRuleEngine.CreateDefaultRules().ToList();
|
||||
var financeRuleEngine = new FinanceRuleEngine(financeRules);
|
||||
|
||||
var centralRecords = await db.CentralSalesRecords
|
||||
.AsNoTracking()
|
||||
.Select(r => new SalesRecord
|
||||
{
|
||||
Land = r.Land,
|
||||
Tsc = r.Tsc,
|
||||
DocumentEntry = r.DocumentEntry,
|
||||
InvoiceNumber = r.InvoiceNumber,
|
||||
PositionOnInvoice = r.PositionOnInvoice,
|
||||
Material = r.Material,
|
||||
Name = r.Name,
|
||||
Quantity = r.Quantity,
|
||||
DocumentType = r.DocumentType,
|
||||
PostingDate = r.PostingDate,
|
||||
InvoiceDate = r.InvoiceDate,
|
||||
ExtractionDate = r.ExtractionDate,
|
||||
CustomerNumber = r.CustomerNumber,
|
||||
CustomerName = r.CustomerName,
|
||||
SupplierCountry = r.SupplierCountry,
|
||||
SalesCurrency = r.SalesCurrency,
|
||||
DocumentCurrency = r.DocumentCurrency,
|
||||
CompanyCurrency = r.CompanyCurrency,
|
||||
SalesPriceValue = r.SalesPriceValue,
|
||||
DocumentTotalForeignCurrency = r.DocumentTotalForeignCurrency,
|
||||
DocumentTotalLocalCurrency = r.DocumentTotalLocalCurrency,
|
||||
VatSumForeignCurrency = r.VatSumForeignCurrency,
|
||||
VatSumLocalCurrency = r.VatSumLocalCurrency
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
var centralRows = centralRecords
|
||||
.Select(record => ApplyFinanceRules(record, year, financeRuleEngine))
|
||||
.Where(row => row is not null)
|
||||
.Select(row => row!)
|
||||
.ToList();
|
||||
|
||||
var groupedActuals = centralRows
|
||||
.GroupBy(r => ResolveReferenceKey(r.Land, r.Tsc), StringComparer.OrdinalIgnoreCase)
|
||||
@@ -149,13 +165,50 @@ public sealed class FinanceReconciliationService : IFinanceReconciliationService
|
||||
return result;
|
||||
}
|
||||
|
||||
private static NetSalesActualSourceRow? ApplyFinanceRules(SalesRecord record, int year, FinanceRuleEngine financeRuleEngine)
|
||||
{
|
||||
var referenceKey = ResolveReferenceKey(record.Land, record.Tsc);
|
||||
if (financeRuleEngine.ResolveFinanceDate(record, referenceKey).Year != year)
|
||||
return null;
|
||||
|
||||
var include = financeRuleEngine.ShouldInclude(record, referenceKey);
|
||||
if (!include)
|
||||
return null;
|
||||
|
||||
var salesPriceValue = financeRuleEngine.ResolveNetSalesActual(record, referenceKey, include);
|
||||
return new NetSalesActualSourceRow(
|
||||
record.Land,
|
||||
record.Tsc,
|
||||
record.DocumentEntry,
|
||||
record.InvoiceNumber,
|
||||
record.PositionOnInvoice,
|
||||
record.Material,
|
||||
record.Name,
|
||||
record.Quantity,
|
||||
record.DocumentType,
|
||||
record.PostingDate,
|
||||
record.InvoiceDate,
|
||||
record.ExtractionDate,
|
||||
record.CustomerNumber,
|
||||
record.CustomerName,
|
||||
record.SupplierCountry,
|
||||
record.SalesCurrency,
|
||||
record.DocumentCurrency,
|
||||
record.CompanyCurrency,
|
||||
salesPriceValue,
|
||||
record.DocumentTotalForeignCurrency,
|
||||
record.DocumentTotalLocalCurrency,
|
||||
record.VatSumForeignCurrency,
|
||||
record.VatSumLocalCurrency);
|
||||
}
|
||||
|
||||
private static NetSalesActual BuildNetSalesActual(
|
||||
string referenceKey,
|
||||
IEnumerable<NetSalesActualSourceRow> rows,
|
||||
IReadOnlyDictionary<string, decimal> budgetRatesToChf,
|
||||
IReadOnlyList<FinanceIntercompanyRule> intercompanyRules)
|
||||
{
|
||||
var rowList = ApplyCountryFinanceRules(referenceKey, rows).ToList();
|
||||
var rowList = rows.ToList();
|
||||
var houseCurrency = ResolveHouseCurrency(referenceKey, rowList);
|
||||
var documentRows = rowList
|
||||
.GroupBy(row => BuildDocumentKey(row.Tsc, row.DocumentType, row.DocumentEntry, row.InvoiceNumber), StringComparer.OrdinalIgnoreCase)
|
||||
@@ -244,50 +297,6 @@ public sealed class FinanceReconciliationService : IFinanceReconciliationService
|
||||
return repeatedGroups / (decimal)multiLineGroups.Count >= 0.8m;
|
||||
}
|
||||
|
||||
private static IEnumerable<NetSalesActualSourceRow> ApplyCountryFinanceRules(
|
||||
string referenceKey,
|
||||
IEnumerable<NetSalesActualSourceRow> rows)
|
||||
{
|
||||
if (!referenceKey.Equals("IT", StringComparison.OrdinalIgnoreCase))
|
||||
return rows;
|
||||
|
||||
var seenBlankSupplierCountryRows = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
return rows.Where(row =>
|
||||
{
|
||||
if (IsExcludedItalyCustomer(row))
|
||||
return false;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(row.SupplierCountry))
|
||||
return true;
|
||||
|
||||
return seenBlankSupplierCountryRows.Add(BuildItalyBlankSupplierCountryDeduplicationKey(row));
|
||||
});
|
||||
}
|
||||
|
||||
private static bool IsExcludedItalyCustomer(NetSalesActualSourceRow row)
|
||||
=> ResolveReferenceKey(row.Land, row.Tsc).Equals("IT", StringComparison.OrdinalIgnoreCase) &&
|
||||
NormalizeRuleText(row.CustomerName).Contains("TRAFAG ITALIA", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private static string BuildItalyBlankSupplierCountryDeduplicationKey(NetSalesActualSourceRow row)
|
||||
=> string.Join("|",
|
||||
row.Tsc,
|
||||
row.DocumentType,
|
||||
row.DocumentEntry,
|
||||
row.InvoiceNumber,
|
||||
row.PositionOnInvoice,
|
||||
row.Material,
|
||||
row.Name,
|
||||
row.Quantity,
|
||||
row.CustomerNumber,
|
||||
row.CustomerName,
|
||||
row.SalesPriceValue,
|
||||
row.DocumentTotalForeignCurrency,
|
||||
row.DocumentTotalLocalCurrency,
|
||||
row.VatSumForeignCurrency,
|
||||
row.VatSumLocalCurrency,
|
||||
row.PostingDate?.ToString("O") ?? string.Empty,
|
||||
row.InvoiceDate?.ToString("O") ?? string.Empty);
|
||||
|
||||
private static decimal ConvertHouseCurrencyNetToBudgetChf(
|
||||
string houseCurrency,
|
||||
NetSalesActualSourceRow row,
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
using System.Reflection;
|
||||
using TrafagSalesExporter.Models;
|
||||
|
||||
namespace TrafagSalesExporter.Services;
|
||||
|
||||
public sealed class FinanceRuleEngine
|
||||
{
|
||||
private readonly IReadOnlyList<FinanceRule> _rules;
|
||||
private readonly Dictionary<string, HashSet<string>> _deduplicationKeys = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private static readonly Dictionary<string, PropertyInfo> SalesRecordProperties = typeof(SalesRecord)
|
||||
.GetProperties(BindingFlags.Public | BindingFlags.Instance)
|
||||
.ToDictionary(x => x.Name, x => x, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public FinanceRuleEngine(IEnumerable<FinanceRule> rules)
|
||||
{
|
||||
_rules = rules
|
||||
.Where(rule => rule.IsActive)
|
||||
.OrderBy(rule => rule.SortOrder)
|
||||
.ThenBy(rule => rule.Id)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public DateTime ResolveFinanceDate(SalesRecord record, string countryKey)
|
||||
{
|
||||
var forceYear = _rules.FirstOrDefault(rule =>
|
||||
IsRuleInScope(rule, countryKey) &&
|
||||
rule.RuleType.Equals(FinanceRuleTypes.ForceYear, StringComparison.OrdinalIgnoreCase) &&
|
||||
RuleMatches(rule, record));
|
||||
|
||||
if (forceYear?.Year is > 0)
|
||||
return new DateTime(forceYear.Year.Value, 12, 31);
|
||||
|
||||
return record.PostingDate ?? record.InvoiceDate ?? record.ExtractionDate;
|
||||
}
|
||||
|
||||
public bool ShouldInclude(SalesRecord record, string countryKey)
|
||||
{
|
||||
foreach (var rule in _rules.Where(rule => IsRuleInScope(rule, countryKey)))
|
||||
{
|
||||
if (!RuleMatches(rule, record))
|
||||
continue;
|
||||
|
||||
if (rule.RuleType.Equals(FinanceRuleTypes.Exclude, StringComparison.OrdinalIgnoreCase))
|
||||
return false;
|
||||
|
||||
if (rule.RuleType.Equals(FinanceRuleTypes.DeduplicateBlankSupplierCountry, StringComparison.OrdinalIgnoreCase) &&
|
||||
string.IsNullOrWhiteSpace(record.SupplierCountry))
|
||||
{
|
||||
var seen = GetDeduplicationSet(rule, countryKey);
|
||||
return seen.Add(BuildBlankSupplierCountryDeduplicationKey(record));
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public decimal ResolveNetSalesActual(SalesRecord record, string countryKey, bool include)
|
||||
{
|
||||
if (!include)
|
||||
return 0m;
|
||||
|
||||
foreach (var rule in _rules.Where(rule => IsRuleInScope(rule, countryKey)))
|
||||
{
|
||||
if (!rule.RuleType.Equals(FinanceRuleTypes.NegateAmount, StringComparison.OrdinalIgnoreCase) ||
|
||||
!RuleMatches(rule, record))
|
||||
continue;
|
||||
|
||||
return -Math.Abs(record.SalesPriceValue);
|
||||
}
|
||||
|
||||
return record.SalesPriceValue;
|
||||
}
|
||||
|
||||
public string ResolveExclusionReason(SalesRecord record, string countryKey)
|
||||
{
|
||||
foreach (var rule in _rules.Where(rule => IsRuleInScope(rule, countryKey)))
|
||||
{
|
||||
if (!RuleMatches(rule, record))
|
||||
continue;
|
||||
|
||||
if (rule.RuleType.Equals(FinanceRuleTypes.Exclude, StringComparison.OrdinalIgnoreCase))
|
||||
return string.IsNullOrWhiteSpace(rule.Notes) ? $"Excluded {countryKey}" : rule.Notes;
|
||||
|
||||
if (rule.RuleType.Equals(FinanceRuleTypes.DeduplicateBlankSupplierCountry, StringComparison.OrdinalIgnoreCase) &&
|
||||
string.IsNullOrWhiteSpace(record.SupplierCountry))
|
||||
return string.IsNullOrWhiteSpace(rule.Notes) ? $"Excluded {countryKey} duplicate without Supplier country" : rule.Notes;
|
||||
}
|
||||
|
||||
return $"Excluded {countryKey}";
|
||||
}
|
||||
|
||||
public static IReadOnlyList<FinanceRule> CreateDefaultRules()
|
||||
=>
|
||||
[
|
||||
new FinanceRule
|
||||
{
|
||||
ScopeKey = "DE",
|
||||
Year = 2025,
|
||||
RuleType = FinanceRuleTypes.ForceYear,
|
||||
MatchType = FinanceRuleMatchTypes.Always,
|
||||
Notes = "DE Alphaplan Jahresfile 2025",
|
||||
SortOrder = 100
|
||||
},
|
||||
new FinanceRule
|
||||
{
|
||||
ScopeKey = "DE",
|
||||
RuleType = FinanceRuleTypes.Exclude,
|
||||
FieldName = nameof(SalesRecord.CustomerName),
|
||||
MatchType = FinanceRuleMatchTypes.Equal,
|
||||
MatchValue = "Trafag AG",
|
||||
Notes = "Excluded DE Weiterberechnung Trafag AG",
|
||||
SortOrder = 110
|
||||
},
|
||||
new FinanceRule
|
||||
{
|
||||
ScopeKey = "DE",
|
||||
RuleType = FinanceRuleTypes.Exclude,
|
||||
FieldName = nameof(SalesRecord.CustomerName),
|
||||
MatchType = FinanceRuleMatchTypes.Contains,
|
||||
MatchValue = "Magnetic Sense",
|
||||
Notes = "Excluded DE Weiterberechnung Magnetic Sense",
|
||||
SortOrder = 120
|
||||
},
|
||||
new FinanceRule
|
||||
{
|
||||
ScopeKey = "DE",
|
||||
RuleType = FinanceRuleTypes.Exclude,
|
||||
FieldName = nameof(SalesRecord.InvoiceNumber),
|
||||
MatchType = FinanceRuleMatchTypes.Equal,
|
||||
MatchValue = "GS2510095",
|
||||
Notes = "Excluded DE GS2510095 already captured in 2024",
|
||||
SortOrder = 130
|
||||
},
|
||||
new FinanceRule
|
||||
{
|
||||
ScopeKey = "DE",
|
||||
RuleType = FinanceRuleTypes.NegateAmount,
|
||||
FieldName = nameof(SalesRecord.InvoiceNumber),
|
||||
MatchType = FinanceRuleMatchTypes.StartsWith,
|
||||
MatchValue = "GS",
|
||||
Notes = "DE Gutschriften negativ",
|
||||
SortOrder = 140
|
||||
},
|
||||
new FinanceRule
|
||||
{
|
||||
ScopeKey = "IT",
|
||||
RuleType = FinanceRuleTypes.Exclude,
|
||||
FieldName = nameof(SalesRecord.CustomerName),
|
||||
MatchType = FinanceRuleMatchTypes.Contains,
|
||||
MatchValue = "Trafag Italia",
|
||||
Notes = "Excluded IT customer: Trafag Italia",
|
||||
SortOrder = 200
|
||||
},
|
||||
new FinanceRule
|
||||
{
|
||||
ScopeKey = "IT",
|
||||
RuleType = FinanceRuleTypes.DeduplicateBlankSupplierCountry,
|
||||
FieldName = nameof(SalesRecord.SupplierCountry),
|
||||
MatchType = FinanceRuleMatchTypes.IsBlank,
|
||||
Notes = "Excluded IT duplicate without Supplier country",
|
||||
SortOrder = 210
|
||||
}
|
||||
];
|
||||
|
||||
private HashSet<string> GetDeduplicationSet(FinanceRule rule, string countryKey)
|
||||
{
|
||||
var key = $"{countryKey}|{rule.Id}|{rule.SortOrder}|{rule.RuleType}";
|
||||
if (!_deduplicationKeys.TryGetValue(key, out var set))
|
||||
{
|
||||
set = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
_deduplicationKeys[key] = set;
|
||||
}
|
||||
|
||||
return set;
|
||||
}
|
||||
|
||||
private static bool IsRuleInScope(FinanceRule rule, string countryKey)
|
||||
=> string.IsNullOrWhiteSpace(rule.ScopeKey) ||
|
||||
rule.ScopeKey.Equals(countryKey, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private static bool RuleMatches(FinanceRule rule, SalesRecord record)
|
||||
{
|
||||
if (rule.MatchType.Equals(FinanceRuleMatchTypes.Always, StringComparison.OrdinalIgnoreCase))
|
||||
return true;
|
||||
|
||||
var value = ReadRecordValue(record, rule.FieldName);
|
||||
var normalizedValue = NormalizeFinanceText(value);
|
||||
var normalizedMatch = NormalizeFinanceText(rule.MatchValue);
|
||||
|
||||
return rule.MatchType switch
|
||||
{
|
||||
FinanceRuleMatchTypes.Equal => normalizedValue.Equals(normalizedMatch, StringComparison.OrdinalIgnoreCase),
|
||||
FinanceRuleMatchTypes.Contains => normalizedValue.Contains(normalizedMatch, StringComparison.OrdinalIgnoreCase),
|
||||
FinanceRuleMatchTypes.StartsWith => normalizedValue.StartsWith(normalizedMatch, StringComparison.OrdinalIgnoreCase),
|
||||
FinanceRuleMatchTypes.IsBlank => string.IsNullOrWhiteSpace(value),
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
|
||||
private static string ReadRecordValue(SalesRecord record, string fieldName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(fieldName))
|
||||
return string.Empty;
|
||||
|
||||
return SalesRecordProperties.TryGetValue(fieldName, out var property)
|
||||
? property.GetValue(record)?.ToString() ?? string.Empty
|
||||
: string.Empty;
|
||||
}
|
||||
|
||||
private static string BuildBlankSupplierCountryDeduplicationKey(SalesRecord record)
|
||||
=> string.Join("|",
|
||||
record.Tsc,
|
||||
record.DocumentType,
|
||||
record.DocumentEntry,
|
||||
record.InvoiceNumber,
|
||||
record.PositionOnInvoice,
|
||||
record.Material,
|
||||
record.Name,
|
||||
record.Quantity,
|
||||
record.CustomerNumber,
|
||||
record.CustomerName,
|
||||
record.SalesPriceValue,
|
||||
record.DocumentTotalForeignCurrency,
|
||||
record.DocumentTotalLocalCurrency,
|
||||
record.VatSumForeignCurrency,
|
||||
record.VatSumLocalCurrency,
|
||||
record.PostingDate?.ToString("O") ?? string.Empty,
|
||||
record.InvoiceDate?.ToString("O") ?? string.Empty);
|
||||
|
||||
private static string NormalizeFinanceText(string value)
|
||||
=> (value ?? string.Empty)
|
||||
.Replace("\u00e4", "ae", StringComparison.OrdinalIgnoreCase)
|
||||
.Replace("\u00f6", "oe", StringComparison.OrdinalIgnoreCase)
|
||||
.Replace("\u00fc", "ue", StringComparison.OrdinalIgnoreCase)
|
||||
.Trim()
|
||||
.ToUpperInvariant();
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TrafagSalesExporter.Data;
|
||||
using TrafagSalesExporter.Models;
|
||||
|
||||
namespace TrafagSalesExporter.Services;
|
||||
|
||||
public interface IFinanceRulesPageService
|
||||
{
|
||||
Task<List<FinanceRule>> LoadAsync();
|
||||
Task<List<FinanceRule>> SaveAllAsync(List<FinanceRule> rules);
|
||||
}
|
||||
|
||||
public sealed class FinanceRulesPageService : IFinanceRulesPageService
|
||||
{
|
||||
private readonly IDbContextFactory<AppDbContext> _dbFactory;
|
||||
|
||||
public FinanceRulesPageService(IDbContextFactory<AppDbContext> dbFactory)
|
||||
{
|
||||
_dbFactory = dbFactory;
|
||||
}
|
||||
|
||||
public async Task<List<FinanceRule>> LoadAsync()
|
||||
{
|
||||
await using var db = await _dbFactory.CreateDbContextAsync();
|
||||
var rules = await db.FinanceRules
|
||||
.AsNoTracking()
|
||||
.OrderBy(rule => rule.SortOrder)
|
||||
.ThenBy(rule => rule.Id)
|
||||
.ToListAsync();
|
||||
|
||||
if (rules.Count > 0)
|
||||
return rules;
|
||||
|
||||
return FinanceRuleEngine.CreateDefaultRules()
|
||||
.Select(CloneRule)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public async Task<List<FinanceRule>> SaveAllAsync(List<FinanceRule> rules)
|
||||
{
|
||||
await using var db = await _dbFactory.CreateDbContextAsync();
|
||||
db.FinanceRules.RemoveRange(db.FinanceRules);
|
||||
db.FinanceRules.AddRange(rules
|
||||
.OrderBy(rule => rule.SortOrder)
|
||||
.ThenBy(rule => rule.Id)
|
||||
.Select(CloneRule));
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return await db.FinanceRules
|
||||
.AsNoTracking()
|
||||
.OrderBy(rule => rule.SortOrder)
|
||||
.ThenBy(rule => rule.Id)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
private static FinanceRule CloneRule(FinanceRule rule)
|
||||
=> new()
|
||||
{
|
||||
ScopeKey = rule.ScopeKey.Trim().ToUpperInvariant(),
|
||||
Year = rule.Year,
|
||||
RuleType = string.IsNullOrWhiteSpace(rule.RuleType) ? FinanceRuleTypes.Exclude : rule.RuleType,
|
||||
FieldName = rule.FieldName ?? string.Empty,
|
||||
MatchType = string.IsNullOrWhiteSpace(rule.MatchType) ? FinanceRuleMatchTypes.Contains : rule.MatchType,
|
||||
MatchValue = rule.MatchValue ?? string.Empty,
|
||||
NumericValue = rule.NumericValue,
|
||||
Notes = rule.Notes ?? string.Empty,
|
||||
SortOrder = rule.SortOrder,
|
||||
IsActive = rule.IsActive
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user