Commit pending finance and Power BI work
This commit is contained in:
@@ -74,6 +74,8 @@ public class CentralSalesRecordServiceTests : IDisposable
|
||||
VatSumLocalCurrency = 7.6m,
|
||||
DocumentRate = 0.95m,
|
||||
CompanyCurrency = "CHF",
|
||||
PostingDate = new DateTime(2026, 4, 28),
|
||||
InvoiceDate = new DateTime(2026, 4, 29),
|
||||
Land = "Schweiz",
|
||||
DocumentType = "INV"
|
||||
}
|
||||
@@ -90,6 +92,8 @@ public class CentralSalesRecordServiceTests : IDisposable
|
||||
Assert.Equal(7.6m, row.VatSumLocalCurrency);
|
||||
Assert.Equal(0.95m, row.DocumentRate);
|
||||
Assert.Equal("CHF", row.CompanyCurrency);
|
||||
Assert.Equal(new DateTime(2026, 4, 28), row.PostingDate);
|
||||
Assert.Equal(new DateTime(2026, 4, 29), row.InvoiceDate);
|
||||
}
|
||||
|
||||
private sealed class NullAppEventLogService : IAppEventLogService
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TrafagSalesExporter.Data;
|
||||
using TrafagSalesExporter.Models;
|
||||
using TrafagSalesExporter.Services;
|
||||
|
||||
namespace TrafagSalesExporter.Tests;
|
||||
|
||||
public class FinanceReconciliationServiceTests : IDisposable
|
||||
{
|
||||
private readonly SqliteConnection _connection;
|
||||
private readonly TestDbContextFactory _dbFactory;
|
||||
|
||||
public FinanceReconciliationServiceTests()
|
||||
{
|
||||
_connection = new SqliteConnection("DataSource=:memory:");
|
||||
_connection.Open();
|
||||
|
||||
var options = new DbContextOptionsBuilder<AppDbContext>()
|
||||
.UseSqlite(_connection)
|
||||
.Options;
|
||||
|
||||
using var db = new AppDbContext(options);
|
||||
db.Database.EnsureCreated();
|
||||
|
||||
_dbFactory = new TestDbContextFactory(options);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_connection.Dispose();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildNetSalesReferenceRowsAsync_Uses_PostingDate_For_Year_Filter()
|
||||
{
|
||||
await using (var db = await _dbFactory.CreateDbContextAsync())
|
||||
{
|
||||
db.Sites.Add(BuildSite());
|
||||
db.FinanceReferences.Add(new FinanceReference { Key = "DE", Label = "Trafag DE", Year = 2025, CheckValue = 100m, IsActive = true });
|
||||
db.CentralSalesRecords.AddRange(
|
||||
BuildCentralRecord("TRDE", "Deutschland", 1, 1, 100m, new DateTime(2025, 1, 5), new DateTime(2024, 12, 31)),
|
||||
BuildCentralRecord("TRDE", "Deutschland", 2, 1, 999m, new DateTime(2024, 12, 31), new DateTime(2025, 1, 5)));
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
var service = new FinanceReconciliationService(_dbFactory);
|
||||
|
||||
var rows = await service.BuildNetSalesReferenceRowsAsync(2025);
|
||||
|
||||
var row = Assert.Single(rows);
|
||||
Assert.Equal(100m, row.ActualValue);
|
||||
Assert.Equal("OK", row.Status);
|
||||
Assert.Equal("Nettofakturawert Hauswaehrung pro Position", row.ValueField);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildNetSalesReferenceRowsAsync_Does_Not_Multiply_Repeated_Document_Header_Totals()
|
||||
{
|
||||
await using (var db = await _dbFactory.CreateDbContextAsync())
|
||||
{
|
||||
db.Sites.Add(BuildSite());
|
||||
db.FinanceReferences.Add(new FinanceReference { Key = "IT", Label = "Trafag IT", Year = 2025, CheckValue = 90m, IsActive = true });
|
||||
db.CentralSalesRecords.AddRange(
|
||||
BuildCentralRecord("TRIT", "Italien", 10, 1, 100m, new DateTime(2025, 2, 1), new DateTime(2025, 2, 1), vatLocal: 10m, salesPriceValue: 40m),
|
||||
BuildCentralRecord("TRIT", "Italien", 10, 2, 100m, new DateTime(2025, 2, 1), new DateTime(2025, 2, 1), vatLocal: 10m, salesPriceValue: 50m));
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
var service = new FinanceReconciliationService(_dbFactory);
|
||||
|
||||
var rows = await service.BuildNetSalesReferenceRowsAsync(2025);
|
||||
|
||||
var row = Assert.Single(rows);
|
||||
Assert.Equal(90m, row.ActualValue);
|
||||
Assert.Equal("Positions-Netto (Sales Price/Value)", row.ValueField);
|
||||
Assert.Contains(row.Candidates, c => c.Key == "NetDocumentLocalCurrencyPosition" && c.Value == 180m && !c.IsPreferred);
|
||||
Assert.Contains(row.Candidates, c => c.Key == "NetDocumentLocalCurrencyDocument" && c.Value == 90m && !c.IsPreferred);
|
||||
Assert.Contains(row.Candidates, c => c.Key == "SalesPriceValue" && c.Value == 90m && c.IsPreferred);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildNetSalesReferenceRowsAsync_Reports_India_As_Inr_House_Currency()
|
||||
{
|
||||
await using (var db = await _dbFactory.CreateDbContextAsync())
|
||||
{
|
||||
db.Sites.Add(BuildSite());
|
||||
db.FinanceReferences.Add(new FinanceReference { Key = "IN", Label = "Trafag IN", Year = 2025, CheckValue = 300m, IsActive = true });
|
||||
db.CentralSalesRecords.AddRange(
|
||||
BuildCentralRecord("TRIN", "Indien", 20, 1, 0m, new DateTime(2025, 3, 1), new DateTime(2025, 3, 1), salesPriceValue: 100m, salesCurrency: "USD"),
|
||||
BuildCentralRecord("TRIN", "Indien", 21, 1, 0m, new DateTime(2025, 3, 2), new DateTime(2025, 3, 2), salesPriceValue: 200m, salesCurrency: "EUR"));
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
var service = new FinanceReconciliationService(_dbFactory);
|
||||
|
||||
var rows = await service.BuildNetSalesReferenceRowsAsync(2025);
|
||||
|
||||
var row = Assert.Single(rows);
|
||||
Assert.Equal(300m, row.ActualValue);
|
||||
Assert.Equal("INR", row.ActualCurrency);
|
||||
Assert.Equal("INR", row.Currencies);
|
||||
Assert.All(row.Candidates, candidate => Assert.NotEqual("EUR, USD", candidate.Currency));
|
||||
}
|
||||
|
||||
private static CentralSalesRecord BuildCentralRecord(
|
||||
string tsc,
|
||||
string land,
|
||||
int documentEntry,
|
||||
int position,
|
||||
decimal documentTotalLocal,
|
||||
DateTime postingDate,
|
||||
DateTime invoiceDate,
|
||||
decimal vatLocal = 0m,
|
||||
decimal? salesPriceValue = null,
|
||||
string salesCurrency = "EUR")
|
||||
=> new()
|
||||
{
|
||||
StoredAtUtc = DateTime.UtcNow,
|
||||
SiteId = 1,
|
||||
SourceSystem = "TEST",
|
||||
ExtractionDate = DateTime.UtcNow,
|
||||
Tsc = tsc,
|
||||
DocumentEntry = documentEntry,
|
||||
InvoiceNumber = documentEntry.ToString(),
|
||||
PositionOnInvoice = position,
|
||||
SalesPriceValue = salesPriceValue ?? documentTotalLocal - vatLocal,
|
||||
SalesCurrency = salesCurrency,
|
||||
DocumentCurrency = salesCurrency,
|
||||
DocumentTotalLocalCurrency = documentTotalLocal,
|
||||
VatSumLocalCurrency = vatLocal,
|
||||
CompanyCurrency = "EUR",
|
||||
PostingDate = postingDate,
|
||||
InvoiceDate = invoiceDate,
|
||||
Land = land,
|
||||
DocumentType = "INV"
|
||||
};
|
||||
|
||||
private static Site BuildSite()
|
||||
=> new()
|
||||
{
|
||||
Id = 1,
|
||||
Schema = "TEST",
|
||||
TSC = "TEST",
|
||||
Land = "Test",
|
||||
SourceSystem = "TEST",
|
||||
IsActive = true
|
||||
};
|
||||
|
||||
private sealed class TestDbContextFactory : IDbContextFactory<AppDbContext>
|
||||
{
|
||||
private readonly DbContextOptions<AppDbContext> _options;
|
||||
|
||||
public TestDbContextFactory(DbContextOptions<AppDbContext> options)
|
||||
{
|
||||
_options = options;
|
||||
}
|
||||
|
||||
public AppDbContext CreateDbContext() => new(_options);
|
||||
|
||||
public Task<AppDbContext> CreateDbContextAsync(CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult(new AppDbContext(_options));
|
||||
}
|
||||
}
|
||||
@@ -140,12 +140,22 @@ public class ManualExcelDataSourceAdapterTests
|
||||
return Task.FromResult(tempPath);
|
||||
}
|
||||
|
||||
public Task<SharePointFileReference> ResolveLatestFileInFolderAsync(string tenantId, string clientId, string clientSecret, string siteUrl, string folderReference, string siteTsc)
|
||||
public Task<SharePointFileReference> ResolveLatestFileInFolderAsync(string tenantId, string clientId, string clientSecret, string siteUrl, string folderReference, string siteTsc, int? preferredYear = null)
|
||||
{
|
||||
LastResolvedTsc = siteTsc;
|
||||
return Task.FromResult(new SharePointFileReference(_latestFileReference, new DateTimeOffset(2026, 5, 1, 0, 0, 0, TimeSpan.Zero)));
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<SharePointFileReference>> ResolveManualImportFilesInFolderAsync(string tenantId, string clientId, string clientSecret, string siteUrl, string folderReference, string siteTsc, int? preferredYear = null)
|
||||
{
|
||||
LastResolvedTsc = siteTsc;
|
||||
IReadOnlyList<SharePointFileReference> result =
|
||||
[
|
||||
new(_latestFileReference, new DateTimeOffset(2026, 5, 1, 0, 0, 0, TimeSpan.Zero))
|
||||
];
|
||||
return Task.FromResult(result);
|
||||
}
|
||||
|
||||
public Task TestConnectionAsync(string tenantId, string clientId, string clientSecret, string siteUrl)
|
||||
=> Task.CompletedTask;
|
||||
}
|
||||
|
||||
@@ -48,10 +48,11 @@ public class ManualExcelImportServiceTests
|
||||
ws.Cell(2, 28).Value = "CHF";
|
||||
ws.Cell(2, 29).Value = "DAP";
|
||||
ws.Cell(2, 30).Value = "Alice";
|
||||
ws.Cell(2, 31).Value = "14.04.2026";
|
||||
ws.Cell(2, 32).Value = "10.04.2026";
|
||||
ws.Cell(2, 33).Value = "Deutschland";
|
||||
ws.Cell(2, 34).Value = "Invoice";
|
||||
ws.Cell(2, 31).Value = "13.04.2026";
|
||||
ws.Cell(2, 32).Value = "14.04.2026";
|
||||
ws.Cell(2, 33).Value = "10.04.2026";
|
||||
ws.Cell(2, 34).Value = "Deutschland";
|
||||
ws.Cell(2, 35).Value = "Invoice";
|
||||
});
|
||||
|
||||
try
|
||||
@@ -78,6 +79,7 @@ public class ManualExcelImportServiceTests
|
||||
Assert.Equal("CHF", row.CompanyCurrency);
|
||||
Assert.Equal("Deutschland", row.Land);
|
||||
Assert.Equal("Invoice", row.DocumentType);
|
||||
Assert.Equal(new DateTime(2026, 4, 13), row.PostingDate);
|
||||
Assert.Equal(new DateTime(2026, 4, 14), row.InvoiceDate);
|
||||
Assert.Equal(new DateTime(2026, 4, 10), row.OrderDate);
|
||||
}
|
||||
@@ -264,6 +266,7 @@ public class ManualExcelImportServiceTests
|
||||
Map(nameof(SalesRecord.CompanyCurrency), "Währung"),
|
||||
Map(nameof(SalesRecord.Incoterms2020), "Versandbedingung"),
|
||||
Map(nameof(SalesRecord.SalesResponsibleEmployee), "AdressNummer_V"),
|
||||
Map(nameof(SalesRecord.PostingDate), "Belegdatum-Rechnung"),
|
||||
Map(nameof(SalesRecord.InvoiceDate), "Belegdatum-Rechnung"),
|
||||
Map(nameof(SalesRecord.OrderDate), "BelegDatum Auftrag"),
|
||||
Map(nameof(SalesRecord.DocumentType), "=Manual Excel")
|
||||
@@ -287,6 +290,7 @@ public class ManualExcelImportServiceTests
|
||||
Assert.Equal("EUR", row.SalesCurrency);
|
||||
Assert.Equal("EUR", row.DocumentCurrency);
|
||||
Assert.Equal("EUR", row.CompanyCurrency);
|
||||
Assert.Equal(new DateTime(2026, 4, 27), row.PostingDate);
|
||||
Assert.Equal(new DateTime(2026, 4, 27), row.InvoiceDate);
|
||||
Assert.Equal(new DateTime(2026, 3, 9), row.OrderDate);
|
||||
Assert.Equal("Manual Excel", row.DocumentType);
|
||||
@@ -307,8 +311,8 @@ public class ManualExcelImportServiceTests
|
||||
};
|
||||
var filePath = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid():N}.csv");
|
||||
var csv = string.Join(Environment.NewLine,
|
||||
"\"TSC\";\"Land\";\"InvoiceNumber\";\"PositionOnInvoice\";\"Material\";\"Name\";\"ProductGroup\";\"Quantity\";\"CustomerNumber\";\"CustomerName\";\"CustomerCountry\";\"StandardCost\";\"StandardCostCurrency\";\"PurchaseOrderNumber\";\"SalesPriceValue\";\"SalesCurrency\";\"DocumentCurrency\";\"CompanyCurrency\";\"Incoterms2020\";\"SalesResponsibleEmployee\";\"InvoiceDate\";\"DocumentType\"",
|
||||
"\"TRES\";\"Spanien\";\"20241332\";\"20\";\"52871\";\"ECL1.0AP\";\"TRANS\";\"1.000000\";\"302208\";\"INTRONIK AUTOMATIZACION E INST. SL\";\"ESPANA\";\"160.760000\";\"EUR\";\"PC240330\";\"265.000000\";\"EUR\";\"EUR\";\"EUR\";\"EXW\";\"1\";\"2025-01-02 00:00:00\";\"Invoice\"");
|
||||
"\"TSC\";\"Land\";\"InvoiceNumber\";\"PositionOnInvoice\";\"Material\";\"Name\";\"ProductGroup\";\"Quantity\";\"CustomerNumber\";\"CustomerName\";\"CustomerCountry\";\"StandardCost\";\"StandardCostCurrency\";\"PurchaseOrderNumber\";\"SalesPriceValue\";\"SalesCurrency\";\"DocumentCurrency\";\"CompanyCurrency\";\"Incoterms2020\";\"SalesResponsibleEmployee\";\"LineRegistrationDate\";\"InvoiceDate\";\"DocumentType\"",
|
||||
"\"TRES\";\"Spanien\";\"20241332\";\"20\";\"52871\";\"ECL1.0AP\";\"TRANS\";\"1.000000\";\"302208\";\"INTRONIK AUTOMATIZACION E INST. SL\";\"ESPANA\";\"160.760000\";\"EUR\";\"PC240330\";\"265.000000\";\"EUR\";\"EUR\";\"EUR\";\"EXW\";\"1\";\"2025-01-03 00:00:00\";\"2025-01-02 00:00:00\";\"Invoice\"");
|
||||
await File.WriteAllTextAsync(filePath, csv);
|
||||
|
||||
try
|
||||
@@ -330,6 +334,7 @@ public class ManualExcelImportServiceTests
|
||||
Assert.Equal("EUR", row.SalesCurrency);
|
||||
Assert.Equal("EUR", row.DocumentCurrency);
|
||||
Assert.Equal("EUR", row.CompanyCurrency);
|
||||
Assert.Equal(new DateTime(2025, 1, 3), row.PostingDate);
|
||||
Assert.Equal(new DateTime(2025, 1, 2), row.InvoiceDate);
|
||||
Assert.Equal("Invoice", row.DocumentType);
|
||||
}
|
||||
@@ -339,6 +344,60 @@ public class ManualExcelImportServiceTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReadSalesRecordsAsync_Evaluates_Mapped_Multiply_Expression()
|
||||
{
|
||||
var site = new Site
|
||||
{
|
||||
TSC = "TRUK",
|
||||
Land = "England"
|
||||
};
|
||||
var filePath = CreateWorkbook(workbook =>
|
||||
{
|
||||
var ws = workbook.Worksheets.Add("Sales");
|
||||
ws.Cell(1, 1).Value = "Invoice Number";
|
||||
ws.Cell(1, 2).Value = "Position on invoice";
|
||||
ws.Cell(1, 3).Value = "Quantity";
|
||||
ws.Cell(1, 4).Value = "Sales Price/Value";
|
||||
ws.Cell(1, 5).Value = "invoice date";
|
||||
ws.Cell(2, 1).Value = "42885";
|
||||
ws.Cell(2, 2).Value = 9;
|
||||
ws.Cell(2, 3).Value = 7;
|
||||
ws.Cell(2, 4).Value = 123.45m;
|
||||
ws.Cell(2, 5).Value = "18.11.2025";
|
||||
});
|
||||
|
||||
var mappings = new List<ManualExcelColumnMapping>
|
||||
{
|
||||
Map(nameof(SalesRecord.InvoiceNumber), "Invoice Number"),
|
||||
Map(nameof(SalesRecord.PositionOnInvoice), "Position on invoice"),
|
||||
Map(nameof(SalesRecord.Quantity), "Quantity"),
|
||||
Map(nameof(SalesRecord.SalesPriceValue), "=[Sales Price/Value]*[Quantity]"),
|
||||
Map(nameof(SalesRecord.SalesCurrency), "=GBP"),
|
||||
Map(nameof(SalesRecord.CompanyCurrency), "=GBP"),
|
||||
Map(nameof(SalesRecord.PostingDate), "invoice date"),
|
||||
Map(nameof(SalesRecord.DocumentType), "=Manual Excel")
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
var service = new ManualExcelImportService();
|
||||
|
||||
var rows = await service.ReadSalesRecordsAsync(filePath, site, mappings);
|
||||
|
||||
var row = Assert.Single(rows);
|
||||
Assert.Equal(864.15m, row.SalesPriceValue);
|
||||
Assert.Equal(7m, row.Quantity);
|
||||
Assert.Equal("GBP", row.SalesCurrency);
|
||||
Assert.Equal("GBP", row.CompanyCurrency);
|
||||
Assert.Equal(new DateTime(2025, 11, 18), row.PostingDate);
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
private static string CreateWorkbook(Action<XLWorkbook> fillWorkbook)
|
||||
{
|
||||
var filePath = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid():N}.xlsx");
|
||||
@@ -382,6 +441,7 @@ public class ManualExcelImportServiceTests
|
||||
"Company Currency",
|
||||
"Incoterms 2020",
|
||||
"Sales responsible employee",
|
||||
"posting date",
|
||||
"invoice date",
|
||||
"order date",
|
||||
"Land",
|
||||
|
||||
Reference in New Issue
Block a user