diff --git a/EpsonTest/Program.cs b/EpsonTest/Program.cs index 125957b..d58c249 100644 --- a/EpsonTest/Program.cs +++ b/EpsonTest/Program.cs @@ -3,9 +3,28 @@ using Inspectron.Epson.PrintServer.Printers.Utils.BarReceipt; using Inspectron.Epson.PrintServer.Printers.Utils.FinalReceipt; using Inspectron.Epson.PrintServer.Printers.Utils.InvoiceReceipt; using Inspectron.Epson.PrintServer.Printers.Utils.KitchenReceipt; +using Inspectron.Epson.PrintServer.Printers.Utils.MoneyReturnReceipt; using Inspectron.Epson.PrintServer.Printers.Utils.OrderItems; using System.Text.Json; +// Non-interactive mode: `dotnet run -- [html|epson]` +// receiptType: kitchen | final | invoice | bar | orderitems | moneyreturn (or 0-5) +if (args.Length > 0) +{ + int rt = ParseReceiptTypeArg(args[0]); + if (rt < 0) + { + Console.WriteLine($"Unknown receipt type: {args[0]}"); + return; + } + + int pt = args.Length > 1 && args[1].Equals("epson", StringComparison.OrdinalIgnoreCase) ? 1 : 0; + Console.WriteLine($"Running {GetReceiptTypeName(rt)} on {(pt == 0 ? "HTML" : "Epson")} printer...\n"); + await RunTest(rt, pt); + Console.WriteLine("\nDone."); + return; +} + while (true) { Console.Clear(); @@ -18,10 +37,11 @@ while (true) "InvoiceReceipt", "BarReceipt", "OrderItems", + "MoneyReturnReceipt", "Exit" }); - if (receiptType == 5) break; + if (receiptType == 6) break; var printerTarget = ShowMenu("Select printer target:", new[] { @@ -96,9 +116,21 @@ static string GetReceiptTypeName(int index) => index switch 2 => "InvoiceReceipt", 3 => "BarReceipt", 4 => "OrderItems", + 5 => "MoneyReturnReceipt", _ => "Unknown" }; +static int ParseReceiptTypeArg(string arg) => arg.ToLowerInvariant() switch +{ + "0" or "kitchen" or "kitchenreceipt" => 0, + "1" or "final" or "finalreceipt" => 1, + "2" or "invoice" or "invoicereceipt" => 2, + "3" or "bar" or "barreceipt" => 3, + "4" or "orderitems" => 4, + "5" or "moneyreturn" or "moneyreturnreceipt" or "refund" or "creditnote" => 5, + _ => -1 +}; + static async Task RunTest(int receiptType, int printerTarget) { switch (receiptType) @@ -108,6 +140,7 @@ static async Task RunTest(int receiptType, int printerTarget) case 2: await RunInvoiceReceiptTest(printerTarget); break; case 3: await RunBarReceiptTest(printerTarget); break; case 4: await RunOrderItemsTest(printerTarget); break; + case 5: await RunMoneyReturnReceiptTest(printerTarget); break; } } @@ -235,3 +268,29 @@ static async Task RunOrderItemsTest(int printerTarget) await TestHelpers.PrintCommandsToEpsonPrinterAsync(printer, printCommands); } } + +static async Task RunMoneyReturnReceiptTest(int printerTarget) +{ + var receipt = TestHelpers.BuildSampleMoneyReturnReceipt(); + var serializedReceipt = JsonSerializer.Serialize(receipt, new JsonSerializerOptions { WriteIndented = true }); + + var converter = new MoneyReturnReceiptConverter(lineWidth: 48, bigFontLineWidth: 24); + var printCommands = converter.Convert(serializedReceipt); + + TestHelpers.PrintCommandsToConsole(printCommands); + + if (printerTarget == 0) + { + var printer = await TestHelpers.CreateHtmlPrinterAsync(path: @".\money_return_test.html"); + await TestHelpers.PrintCommandsToHtmlPrinterAsync( + printer, + printCommands, + logoPath: "ristorante-klinglers.ch-logo_white_bg.png"); + Console.WriteLine("Output written to: money_return_test.html"); + } + else + { + var printer = await TestHelpers.CreateEpsonPrinterAsync(); + await TestHelpers.PrintCommandsToEpsonPrinterAsync(printer, printCommands); + } +} diff --git a/EpsonTest/TestHelpers.cs b/EpsonTest/TestHelpers.cs index 310febe..80b8392 100644 --- a/EpsonTest/TestHelpers.cs +++ b/EpsonTest/TestHelpers.cs @@ -5,6 +5,7 @@ using Inspectron.Epson.PrintServer.Printers.Utils.BarReceipt; using Inspectron.Epson.PrintServer.Printers.Utils.FinalRecipe; using Inspectron.Epson.PrintServer.Printers.Utils.InvoiceReceipt; using Inspectron.Epson.PrintServer.Printers.Utils.KitchenReceipt; +using Inspectron.Epson.PrintServer.Printers.Utils.MoneyReturnReceipt; using Inspectron.Epson.PrintServer.Printers.Utils.OrderItems; namespace EpsonTest; @@ -462,6 +463,66 @@ public static class TestHelpers }; } + // Mirrors money_return_receipt_data.json plus the credit-note specific fields + // shown in money_return_receipt.jpg (original invoice reference, refund type, tip). + public static MoneyReturnReceipt BuildSampleMoneyReturnReceipt() + { + var receipt = new MoneyReturnReceipt + { + CompanyName = "Big Mac Bistro", + Address1 = "Zelena 186", + Address2 = "79000 Lviv", + Phone = "080 039 47 69", + ReceiptNumber = "25", + DateTime = new DateTime(2026, 7, 4, 11, 8, 13), + Guests = 1, + Total = 20.00m, + Currency = "CHF", + PaymentMethod = "Cash", + PaymentAmount = 13.00m, + WaiterName = "Ronald McDonald", + VatNumber = " MWST", + ThankYouMessage = "Thank you for your order!", + GoodbyeMessageLine1 = "Auf Wiedersehen.", + GoodbyeMessageLine2 = "Powered by James", + // Credit-note specific fields + OriginalInvoiceNumber = "773", + OriginalDateTime = new DateTime(2026, 7, 4, 11, 7, 0), + RefundType = "Full refund", + Tip = 7.00m + }; + + receipt.Items.Add(new MoneyReturnReceiptItem + { + Quantity = 1, + Description = "Cloudy Bay Sauvignon Blanc 2024", + UnitPrice = 13.00m, + TotalPrice = 13.00m, + TaxCategory = "D" + }); + + receipt.Items.Add(new MoneyReturnReceiptItem + { + Quantity = 1, + Description = "Trinkgeld", + UnitPrice = 7.00m, + TotalPrice = 7.00m, + TaxCategory = "D" + }); + + receipt.TaxBreakdown.Add(new MoneyReturnTaxInfo + { + Category = "D", + Rate = 0m, + Gross = 20.00m, + Net = 20.00m, + TaxAmount = 0.00m, + Currency = "CHF" + }); + + return receipt; + } + #endregion #region Print Helpers diff --git a/Inspectron.Epson/PrintServer/Printers/Utils/MoneyReturnReceipt/Models.cs b/Inspectron.Epson/PrintServer/Printers/Utils/MoneyReturnReceipt/Models.cs new file mode 100644 index 0000000..4f03313 --- /dev/null +++ b/Inspectron.Epson/PrintServer/Printers/Utils/MoneyReturnReceipt/Models.cs @@ -0,0 +1,118 @@ +namespace Inspectron.Epson.PrintServer.Printers.Utils.MoneyReturnReceipt; + +/// +/// Data model for a refund / credit note ("money return") receipt. +/// Mirrors the sale receipt payload (see money_return_receipt_data.json) and adds +/// the credit-note specific fields that reference the original invoice being refunded. +/// +public class MoneyReturnReceipt +{ + public string CompanyName { get; set; } + public string Address1 { get; set; } + public string Address2 { get; set; } + public string Phone { get; set; } + + /// Credit note number (shown as "Credit note No. {ReceiptNumber}"). + public string? ReceiptNumber { get; set; } + + /// Date/time the credit note was issued. + public DateTime DateTime { get; set; } + + public int Guests { get; set; } + public List Items { get; set; } = new List(); + + /// Refunded amount (positive). Rendered negative on the receipt. + public decimal Total { get; set; } + public string Currency { get; set; } + + public decimal? TotalInAlternateCurrency { get; set; } + public string AlternateCurrency { get; set; } + + public List SplitPayments { get; set; } = new List(); + + /// Method the original payment was made with (e.g. "Cash"). + public string PaymentMethod { get; set; } + public decimal PaymentAmount { get; set; } + + public List TaxBreakdown { get; set; } = new List(); + + public bool IsDebtor { get; set; } = false; + public string WaiterName { get; set; } + public string Terminal { get; set; } + public string TableNumber { get; set; } + public string VatNumber { get; set; } + + public string ThankYouMessage { get; set; } + public string GoodbyeMessageLine1 { get; set; } + public string GoodbyeMessageLine2 { get; set; } + + public List TerminalReceipts { get; set; } = new List(); + public MoneyReturnDiscountInfo? DiscountInfo { get; set; } + + // --- Credit-note specific fields --- + + /// Number of the original invoice this credit note refunds ("Orig. invoice No."). + public string? OriginalInvoiceNumber { get; set; } + + /// Date/time of the original invoice ("Orig. date"). + public DateTime? OriginalDateTime { get; set; } + + /// Refund type, e.g. "Full refund" or "Partial refund" ("Type"). + public string? RefundType { get; set; } + + /// Tip amount included in the refund. Shown as "incl. tip" when non-zero. + public decimal Tip { get; set; } +} + +public class MoneyReturnReceiptItem +{ + public int Quantity { get; set; } + public string Description { get; set; } + public decimal UnitPrice { get; set; } + public decimal TotalPrice { get; set; } + public string TaxCategory { get; set; } + public List SubItems { get; set; } +} + +public class MoneyReturnTaxInfo +{ + public string Category { get; set; } + public decimal Rate { get; set; } + public decimal Gross { get; set; } + public decimal Net { get; set; } + public decimal TaxAmount { get; set; } + public string Currency { get; set; } +} + +public class MoneyReturnSplitPaymentInfo +{ + public string PaymentMethod { get; set; } + public decimal Amount { get; set; } + public string Currency { get; set; } +} + +public class MoneyReturnDiscountInfo +{ + public string Description { get; set; } + public decimal Amount { get; set; } + public string Currency { get; set; } +} + +public class MoneyReturnPaymentTerminalReceipt +{ + public string ReceiptType { get; set; } + public string BookingType { get; set; } + public string PaymentSystem { get; set; } + public string TransactionNumber { get; set; } + public DateTime TransactionDateTime { get; set; } + public string TerminalId { get; set; } + public string AID { get; set; } + public string TransactionSeqCount { get; set; } + public string TransactionRefNo { get; set; } + public string AuthCode { get; set; } + public string AcquirerId { get; set; } + public decimal EftAmount { get; set; } + public decimal TipAmount { get; set; } + public decimal TotalEftAmount { get; set; } + public string Currency { get; set; } +} diff --git a/Inspectron.Epson/PrintServer/Printers/Utils/MoneyReturnReceipt/MoneyReturnReceiptConverter.cs b/Inspectron.Epson/PrintServer/Printers/Utils/MoneyReturnReceipt/MoneyReturnReceiptConverter.cs new file mode 100644 index 0000000..80e2d42 --- /dev/null +++ b/Inspectron.Epson/PrintServer/Printers/Utils/MoneyReturnReceipt/MoneyReturnReceiptConverter.cs @@ -0,0 +1,22 @@ +using System.Text.Json; + +namespace Inspectron.Epson.PrintServer.Printers.Utils.MoneyReturnReceipt; + +public class MoneyReturnReceiptConverter : IReceiptConverter +{ + private readonly int _lineWidth; + private readonly int _bigFontLineWidth; + + public MoneyReturnReceiptConverter(int lineWidth, int bigFontLineWidth) + { + _lineWidth = lineWidth; + _bigFontLineWidth = bigFontLineWidth; + } + + public List Convert(string jsonContent) + { + var receipt = JsonSerializer.Deserialize(jsonContent); + var converter = new ReceiptConverter(_lineWidth, _bigFontLineWidth); + return converter.ConvertToPrintCommands(receipt); + } +} diff --git a/Inspectron.Epson/PrintServer/Printers/Utils/MoneyReturnReceipt/ReceiptConverter.cs b/Inspectron.Epson/PrintServer/Printers/Utils/MoneyReturnReceipt/ReceiptConverter.cs new file mode 100644 index 0000000..98f2459 --- /dev/null +++ b/Inspectron.Epson/PrintServer/Printers/Utils/MoneyReturnReceipt/ReceiptConverter.cs @@ -0,0 +1,149 @@ +namespace Inspectron.Epson.PrintServer.Printers.Utils.MoneyReturnReceipt; + +/// +/// Builds the print command list for a refund / credit note receipt. +/// Layout follows money_return_receipt.jpg: company header, "Refund / Credit note" +/// title, credit note + original invoice reference block, the negative credit total, +/// the (negated) VAT breakdown and the goodbye footer. +/// +public class ReceiptConverter +{ + private readonly int _lineWidth; + private readonly int _bigFontLineWidth; + + public ReceiptConverter(int lineWidth = 48, int bigFontLineWidth = 24) + { + _lineWidth = lineWidth; + _bigFontLineWidth = bigFontLineWidth; + } + + public List ConvertToPrintCommands(MoneyReturnReceipt receipt) + { + var commands = new List(); + + // Header - Company info + commands.Add(new PrintCommand(Center(receipt.CompanyName, false))); + commands.Add(new PrintCommand(Center(receipt.Address1, false))); + commands.Add(new PrintCommand(Center(receipt.Address2, false))); + commands.Add(new PrintCommand(Center(receipt.Phone, false))); + commands.Add(new PrintCommand("")); + commands.Add(new PrintCommand("")); + + // Title between separators + commands.Add(new PrintCommand(new string('-', _lineWidth))); + commands.Add(new PrintCommand(Center("Refund / Credit note", false), isBold: true)); + commands.Add(new PrintCommand(new string('-', _lineWidth))); + commands.Add(new PrintCommand("")); + + // Credit note number + issue date + string creditNoteLabel = receipt.ReceiptNumber != null + ? $"Credit note No. {receipt.ReceiptNumber}" + : "Credit note"; + commands.Add(new PrintCommand(Justify(creditNoteLabel, $"{receipt.DateTime:HH:mm dd.MM.yyyy}"))); + commands.Add(new PrintCommand("")); + + // Original invoice reference block + commands.Add(new PrintCommand(Justify("Orig. invoice No.:", receipt.OriginalInvoiceNumber ?? ""))); + if (receipt.OriginalDateTime.HasValue) + commands.Add(new PrintCommand(Justify("Orig. date:", $"{receipt.OriginalDateTime:HH:mm dd.MM.yyyy}"))); + commands.Add(new PrintCommand(Justify("Orig. payment method:", receipt.PaymentMethod ?? ""))); + if (!string.IsNullOrEmpty(receipt.RefundType)) + commands.Add(new PrintCommand(Justify("Type:", receipt.RefundType))); + commands.Add(new PrintCommand("")); + + commands.Add(new PrintCommand(new string('-', _lineWidth))); + commands.Add(new PrintCommand("")); + + // Included tip + if (receipt.Tip != 0) + { + commands.Add(new PrintCommand($"incl. tip {receipt.Currency}: {receipt.Tip:F2}".PadLeft(_lineWidth))); + commands.Add(new PrintCommand("")); + } + + // Credit total (refund is shown as a negative amount), big + bold, centered + string creditLine = $"Credit: {-receipt.Total:F2} {receipt.Currency}"; + commands.Add(new PrintCommand(Center(creditLine, true), true, true)); + commands.Add(new PrintCommand("")); + + // Alternate currency + if (receipt.TotalInAlternateCurrency.HasValue) + { + string altCurrencyLine = $"{-receipt.TotalInAlternateCurrency:F2} {receipt.AlternateCurrency}"; + commands.Add(new PrintCommand(altCurrencyLine.PadLeft(_lineWidth))); + commands.Add(new PrintCommand("")); + } + + // Refund payment method line (e.g. "Cash: -20.00 CHF") + if (receipt.SplitPayments != null && receipt.SplitPayments.Count > 0) + { + foreach (var split in receipt.SplitPayments) + commands.Add(new PrintCommand($"{split.PaymentMethod}: {-split.Amount:F2} {split.Currency}".PadLeft(_lineWidth))); + } + else + { + commands.Add(new PrintCommand($"{receipt.PaymentMethod}: {-receipt.Total:F2} {receipt.Currency}".PadLeft(_lineWidth))); + } + commands.Add(new PrintCommand("")); + + // Tax breakdown (amounts negated for the refund) + foreach (var tax in receipt.TaxBreakdown) + { + if (receipt.TaxBreakdown.IndexOf(tax) == 0) + { + string taxHeader = "VAT %".PadRight(_lineWidth / 4) + + "Gross".PadLeft(_lineWidth / 4) + + "Net".PadLeft(_lineWidth / 4) + + "VAT".PadLeft(_lineWidth / 4); + commands.Add(new PrintCommand(taxHeader)); + } + + string taxDetail = ($"{tax.Category}:" + $"{tax.Rate}%".PadLeft(5)).PadRight(_lineWidth / 4) + + $"{-tax.Gross:F2} {tax.Currency}".PadLeft(_lineWidth / 4) + + $"{-tax.Net:F2} {tax.Currency}".PadLeft(_lineWidth / 4) + + $"{-tax.TaxAmount:F2} {tax.Currency}".PadLeft(_lineWidth / 4); + commands.Add(new PrintCommand(taxDetail)); + } + + commands.Add(new PrintCommand("")); + + if (receipt.TaxBreakdown.Count(x => x.Category != null && x.Category.ToLower() != "d") == 0) + { + commands.Add(new PrintCommand("Not subject to value added tax")); + } + + commands.Add(new PrintCommand("")); + commands.Add(new PrintCommand("")); + + // Footer / goodbye + commands.Add(new PrintCommand(Center(receipt.ThankYouMessage, false))); + commands.Add(new PrintCommand(Center(receipt.GoodbyeMessageLine1, false))); + commands.Add(new PrintCommand(Center(receipt.GoodbyeMessageLine2, false))); + + return commands; + } + + private string Justify(string left, string right) + { + left ??= ""; + right ??= ""; + + int spaces = _lineWidth - left.Length - right.Length; + if (spaces < 1) spaces = 1; + + return left + new string(' ', spaces) + right; + } + + private string Center(string text, bool isBigFont) + { + int effectiveLineWidth = isBigFont ? _bigFontLineWidth : _lineWidth; + + if (string.IsNullOrEmpty(text) || text.Length >= effectiveLineWidth) + return text; + + int totalPadding = effectiveLineWidth - text.Length; + int leftPadding = totalPadding / 2; + + return new string(' ', leftPadding) + text; + } +} diff --git a/Inspectron.Epson/PrintServer/Printers/Utils/ReceiptConverterFactory.cs b/Inspectron.Epson/PrintServer/Printers/Utils/ReceiptConverterFactory.cs index 5bde730..8c65f0d 100644 --- a/Inspectron.Epson/PrintServer/Printers/Utils/ReceiptConverterFactory.cs +++ b/Inspectron.Epson/PrintServer/Printers/Utils/ReceiptConverterFactory.cs @@ -20,6 +20,7 @@ public class ReceiptConverterFactory : IReceiptConverterFactory EReceiptType.NextCourse => new KitchenReceipt.KitchenReceiptConverterAdapter(33, 20), EReceiptType.OrdersOverview => new OrderItemsReceiptConverter(48, 24), EReceiptType.Invoice => new InvoiceReceiptConverter(48, 24), + EReceiptType.MoneyReturn => new MoneyReturnReceipt.MoneyReturnReceiptConverter(48, 24), _ => throw new ArgumentException($"Unknown receipt type: {receiptType}") }; } @@ -30,6 +31,7 @@ public class ReceiptConverterFactory : IReceiptConverterFactory WorkareaTicket, NextCourse, OrdersOverview, - Invoice + Invoice, + MoneyReturn } } diff --git a/money_return_receipt.jpg b/money_return_receipt.jpg new file mode 100644 index 0000000..c07432c Binary files /dev/null and b/money_return_receipt.jpg differ diff --git a/money_return_receipt_data.json b/money_return_receipt_data.json new file mode 100644 index 0000000..95b1620 --- /dev/null +++ b/money_return_receipt_data.json @@ -0,0 +1,54 @@ +{ + "TotalInAlternateCurrency":null, + "AlternateCurrency":"", + "SplitPayments":null, + "PaymentMethod":"Cash", + "PaymentAmount":13, + "TaxBreakdown":[ + { + "Category":"D", + "Rate":0, + "Gross":20, + "Net":20, + "TaxAmount":0, + "Currency":"CHF" + } + ], + "WaiterName":"Ronald McDonald", + "Terminal":null, + "VatNumber":" MWST", + "GoodbyeMessageLine1":"Auf Wiedersehen.", + "GoodbyeMessageLine2":"Powered by James", + "TerminalReceipts":null, + "IsDebtor":false, + "DiscountInfo":null, + "CompanyName":"Big Mac Bistro", + "Address1":"Zelena 186", + "Address2":"79000 Lviv", + "Phone":"080 039 47 69", + "ReceiptNumber":"25", + "DateTime":"2026-07-04T11:08:13.861", + "Guests":1, + "Items":[ + { + "Quantity":1, + "Description":"Cloudy Bay Sauvignon Blanc 2024", + "UnitPrice":13, + "TotalPrice":13, + "TaxCategory":"D", + "SubItems":null + }, + { + "Quantity":1, + "Description":"Trinkgeld", + "UnitPrice":7, + "TotalPrice":7, + "TaxCategory":"D", + "SubItems":null + } + ], + "Total":20, + "Currency":"CHF", + "ThankYouMessage":"Thank you for your order!", + "TableNumber":null +} \ No newline at end of file