Normalize invoice provision periods in PDF generation, switch admin labels between single-date and range modes, add end-to-end tests, update package and frontend bundle versions, and note the invoice lifecycle docs update.
This commit is contained in:
@@ -0,0 +1,85 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Fuchs.intranet;
|
||||||
|
using Fuchs.Notifications;
|
||||||
|
using Fuchs.Services;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
using MigraDoc.DocumentObjectModel;
|
||||||
|
using Moq;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Fuchs.Tests;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The stored Leistungsdatum / -zeitraum must reach the invoice PDF's admin block (it was never
|
||||||
|
/// passed to the text blocks, so every invoice printed "Leistungszeitraum: -").
|
||||||
|
/// </summary>
|
||||||
|
public class InvoicePdfProvisionPeriodTests
|
||||||
|
{
|
||||||
|
private sealed class NoHttp : IHttpClientFactory
|
||||||
|
{
|
||||||
|
public HttpClient CreateClient(string name) => throw new InvalidOperationException("no HTTP expected");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("2026-06-18", "18.06.2026")]
|
||||||
|
[InlineData("2026-06-01 - 2026-06-30", "01.06.2026 - 30.06.2026")]
|
||||||
|
[InlineData("18.06.2026", "18.06.2026")]
|
||||||
|
[InlineData("01.06.2026 - 30.06.2026", "01.06.2026 - 30.06.2026")]
|
||||||
|
[InlineData("R2026-0258", "R2026-0258")]
|
||||||
|
[InlineData("", "")]
|
||||||
|
[InlineData(null, "")]
|
||||||
|
[InlineData("nach Absprache", "nach Absprache")]
|
||||||
|
public void ProvisionPeriodForDisplay_MixedFormats_ShowsGermanDates(string? input, string expected)
|
||||||
|
{
|
||||||
|
Assert.Equal(expected, InvoiceService.ProvisionPeriodForDisplay(input));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("18.06.2026", "18.06.2026", "Leistungsdatum")]
|
||||||
|
[InlineData("01.06.2026 - 30.06.2026", "01.06.2026 - 30.06.2026", "Leistungszeitraum")]
|
||||||
|
[InlineData("2026-06-18", "18.06.2026", "Leistungsdatum")]
|
||||||
|
[InlineData("2026-06-01 - 2026-06-30", "01.06.2026 - 30.06.2026", "Leistungszeitraum")]
|
||||||
|
[InlineData(" 18.06.2026 ", "18.06.2026", "Leistungsdatum")]
|
||||||
|
[InlineData("", "", "Leistungszeitraum")]
|
||||||
|
[InlineData(null, "", "Leistungszeitraum")]
|
||||||
|
public void ApplyProvisionPeriod_StoredValue_SetsGermanValueAndMatchingLabel(string? stored, string expectedValue, string expectedLabel)
|
||||||
|
{
|
||||||
|
var tb = new FuchsPdf.FdsTextBlocks();
|
||||||
|
|
||||||
|
InvoiceService.ApplyProvisionPeriod(tb, stored);
|
||||||
|
|
||||||
|
Assert.Equal(expectedValue, tb.ProvisionPeriod);
|
||||||
|
Assert.Equal(expectedLabel, tb.AdminProvLabel);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("18.06.2026", "18.06.2026", "Leistungsdatum")]
|
||||||
|
[InlineData("2026-06-01 - 2026-06-30", "01.06.2026 - 30.06.2026", "Leistungszeitraum")]
|
||||||
|
[InlineData(null, "", "Leistungszeitraum")]
|
||||||
|
public async Task GenerateInvoicePdfAsync_StoredProvisionPeriod_ReachesAdminBlock(string? stored, string expectedValue, string expectedLabel)
|
||||||
|
{
|
||||||
|
FuchsPdf.FdsTextBlocks? captured = null;
|
||||||
|
var pdf = new Mock<IPdfService>();
|
||||||
|
pdf.Setup(p => p.WriteLetterAsync(It.IsAny<FuchsPdf.FdsTextBlocks>(), It.IsAny<bool>()))
|
||||||
|
.Callback<FuchsPdf.FdsTextBlocks, bool>((tb, _) => captured = tb)
|
||||||
|
.ReturnsAsync(new Document());
|
||||||
|
var erechnung = new Mock<IERechnungService>();
|
||||||
|
erechnung.SetupGet(e => e.Enabled).Returns(false);
|
||||||
|
var config = new ConfigurationBuilder().Build();
|
||||||
|
var svc = new InvoiceService(new Fuchs_intranet(config), pdf.Object, new Mock<IBlobStorageService>().Object,
|
||||||
|
new Mock<IEventService>().Object, erechnung.Object, new NoHttp(), config, NullLogger<InvoiceService>.Instance);
|
||||||
|
var reg = new Dictionary<string, object?> { ["Id"] = "W7VAL", ["InvoiceId"] = "R2026-0258" };
|
||||||
|
if (stored != null) reg["ProvisionPeriod"] = stored;
|
||||||
|
var invoice = new FdsInvoiceData { InvoiceRegistration = new OCORE.OCORE_dictionaries.GenericObjectDictionary(reg) };
|
||||||
|
|
||||||
|
await svc.GenerateInvoicePdfAsync(invoice, draft: true);
|
||||||
|
|
||||||
|
Assert.NotNull(captured);
|
||||||
|
Assert.Equal(expectedValue, captured!.ProvisionPeriod);
|
||||||
|
Assert.Equal(expectedLabel, captured.AdminProvLabel);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -97,7 +97,9 @@ payload; see `EVAL_live_invoice_editing.md` for the rationale.
|
|||||||
it drives the EN 16931 eRechnung and composes the free-text `SendToAddress` for the
|
it drives the EN 16931 eRechnung and composes the free-text `SendToAddress` for the
|
||||||
PDF (see [`Concepts/erechnung-output.md`](Concepts/erechnung-output.md)). The **service
|
PDF (see [`Concepts/erechnung-output.md`](Concepts/erechnung-output.md)). The **service
|
||||||
date/period** (Leistungsdatum/-zeitraum) is likewise a structured German-date dialog
|
date/period** (Leistungsdatum/-zeitraum) is likewise a structured German-date dialog
|
||||||
(`$inv.eProvisionPeriod`) — a single date or a from/to range — mapped to BT-72 / BG-14.
|
(`$inv.eProvisionPeriod`) — a single date or a from/to range — mapped to BT-72 / BG-14
|
||||||
|
and printed in the PDF's admin block (`InvoiceService.ApplyProvisionPeriod`; legacy ISO
|
||||||
|
values are shown as German dates).
|
||||||
- **§13b reverse-charge** toggle (`$inv.sp13b`) — suppresses VAT lines/columns.
|
- **§13b reverse-charge** toggle (`$inv.sp13b`) — suppresses VAT lines/columns.
|
||||||
- **Set-pricing display mode** (`$inv.ssetmode` / `setSetmode`) — `SetPrice`
|
- **Set-pricing display mode** (`$inv.ssetmode` / `setSetmode`) — `SetPrice`
|
||||||
(default) / `SetOnly`; see `INVOICE_SET_PRICING.md`. Purely presentational
|
(default) / `SetOnly`; see `INVOICE_SET_PRICING.md`. Purely presentational
|
||||||
|
|||||||
+2
-2
@@ -51,10 +51,10 @@
|
|||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
|
||||||
<!-- New packages (needed for .NET 10) -->
|
<!-- New packages (needed for .NET 10) -->
|
||||||
<PackageReference Include="Microsoft.Data.SqlClient" Version="7.1.1" />
|
<PackageReference Include="Microsoft.Data.SqlClient" Version="7.1.1" />
|
||||||
<PackageReference Include="SixLabors.ImageSharp" Version="4.1.2" />
|
<PackageReference Include="SixLabors.ImageSharp" Version="4.1.3" />
|
||||||
<PackageReference Include="System.Configuration.ConfigurationManager" Version="10.0.12" />
|
<PackageReference Include="System.Configuration.ConfigurationManager" Version="10.0.12" />
|
||||||
<PackageReference Include="System.Drawing.Common" Version="10.0.12" />
|
<PackageReference Include="System.Drawing.Common" Version="10.0.12" />
|
||||||
<PackageReference Include="Azure.Storage.Blobs" Version="12.30.0" />
|
<PackageReference Include="Azure.Storage.Blobs" Version="12.30.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Folder Include="App_Data\cache\" />
|
<Folder Include="App_Data\cache\" />
|
||||||
|
|||||||
@@ -320,6 +320,22 @@ public class InvoiceService : IInvoiceService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Puts the stored Leistungsdatum / -zeitraum into the PDF admin block: German dates (legacy ISO
|
||||||
|
/// values converted) and the label the editor shows — "Leistungsdatum" for one date,
|
||||||
|
/// "Leistungszeitraum" for a from–to range.
|
||||||
|
/// </summary>
|
||||||
|
internal static void ApplyProvisionPeriod(FuchsPdf.FdsTextBlocks tb, string? provisionPeriod)
|
||||||
|
{
|
||||||
|
tb.ProvisionPeriod = ProvisionPeriodForDisplay(provisionPeriod).Trim();
|
||||||
|
tb.AdminProvLabel = tb.ProvisionPeriod.Contains(" - ") || tb.ProvisionPeriod.Length == 0
|
||||||
|
? "Leistungszeitraum" : "Leistungsdatum";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Shows ISO dates (yyyy-MM-dd) saved by older editor versions as German dd.MM.yyyy.</summary>
|
||||||
|
internal static string ProvisionPeriodForDisplay(string? period) =>
|
||||||
|
System.Text.RegularExpressions.Regex.Replace(period ?? "", @"\b(\d{4})-(\d{2})-(\d{2})\b", "$3.$2.$1");
|
||||||
|
|
||||||
public async Task<Document> GenerateInvoicePdfAsync(FdsInvoiceData invoice, bool draft)
|
public async Task<Document> GenerateInvoicePdfAsync(FdsInvoiceData invoice, bool draft)
|
||||||
{
|
{
|
||||||
using var act = FuchsTelemetry.StartActivity("invoice.render");
|
using var act = FuchsTelemetry.StartActivity("invoice.render");
|
||||||
@@ -339,6 +355,7 @@ public class InvoiceService : IInvoiceService
|
|||||||
// e-invoice issue date (ERechnungMapper), see InvoiceDates / ADR 0032.
|
// e-invoice issue date (ERechnungMapper), see InvoiceDates / ADR 0032.
|
||||||
AdminDatumValue = InvoiceDates.IssueDate(reg).ToDateTime(TimeOnly.MinValue)
|
AdminDatumValue = InvoiceDates.IssueDate(reg).ToDateTime(TimeOnly.MinValue)
|
||||||
};
|
};
|
||||||
|
ApplyProvisionPeriod(tb, reg?.getString("ProvisionPeriod"));
|
||||||
var inlineImages = await ResolveInlineImagesAsync(invoice);
|
var inlineImages = await ResolveInlineImagesAsync(invoice);
|
||||||
var doc = await _pdf.WriteLetterAsync(tb, draft);
|
var doc = await _pdf.WriteLetterAsync(tb, draft);
|
||||||
doc.Info.Title = reg?.getString("InvoiceTitle") ?? "";
|
doc.Info.Title = reg?.getString("InvoiceTitle") ?? "";
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user