final receipt update

This commit is contained in:
EugeneTes
2026-01-22 11:09:38 +01:00
parent 9fbc61dede
commit fcb192b797
31 changed files with 2679 additions and 655 deletions

View File

@@ -41,78 +41,77 @@ public class TemplateInterpreter
List<PrintCommand> commands,
StyleContext style)
{
var lineBuffer = new StringBuilder();
var hasInlineContent = false;
void FlushLineBuffer()
{
if (hasInlineContent)
{
var text = ApplyAlignment(lineBuffer.ToString(), style);
commands.Add(CreateCommand(text, style));
lineBuffer.Clear();
hasInlineContent = false;
}
}
foreach (var node in nodes)
{
InterpretNode(node, context, commands, style);
switch (node)
{
// Inline nodes - accumulate into line buffer
case TextNode textNode:
lineBuffer.Append(textNode.Text);
hasInlineContent = true;
break;
case BindingNode bindingNode:
var element = context.Resolve(bindingNode.Path);
lineBuffer.Append(_formatter.Format(element, bindingNode.Format));
hasInlineContent = true;
break;
// Styled text - flush buffer, emit styled content, continue accumulating
case StyledTextNode styledNode:
FlushLineBuffer();
InterpretStyledText(styledNode, context, commands, style);
break;
// Block nodes - flush buffer first, then process
case SeparatorNode separatorNode:
FlushLineBuffer();
InterpretSeparator(separatorNode, commands, style);
break;
case EmptyLineNode:
FlushLineBuffer();
commands.Add(CreateCommand(string.Empty, style));
break;
case IfNode ifNode:
FlushLineBuffer();
InterpretIf(ifNode, context, commands, style);
break;
case ForeachNode foreachNode:
FlushLineBuffer();
InterpretForeach(foreachNode, context, commands, style);
break;
case RowNode rowNode:
FlushLineBuffer();
InterpretRow(rowNode, context, commands, style);
break;
case ColumnNode columnNode:
FlushLineBuffer();
InterpretColumn(columnNode, context, commands, style);
break;
}
}
}
private void InterpretNode(
ITemplateNode node,
DataContext context,
List<PrintCommand> commands,
StyleContext style)
{
switch (node)
{
case TextNode textNode:
InterpretText(textNode, context, commands, style);
break;
case BindingNode bindingNode:
InterpretBinding(bindingNode, context, commands, style);
break;
case StyledTextNode styledNode:
InterpretStyledText(styledNode, context, commands, style);
break;
case SeparatorNode separatorNode:
InterpretSeparator(separatorNode, commands, style);
break;
case EmptyLineNode:
commands.Add(CreateCommand(string.Empty, style));
break;
case IfNode ifNode:
InterpretIf(ifNode, context, commands, style);
break;
case ForeachNode foreachNode:
InterpretForeach(foreachNode, context, commands, style);
break;
case RowNode rowNode:
InterpretRow(rowNode, context, commands, style);
break;
case ColumnNode columnNode:
InterpretColumn(columnNode, context, commands, style);
break;
}
}
private void InterpretText(
TextNode node,
DataContext context,
List<PrintCommand> commands,
StyleContext style)
{
var text = ApplyAlignment(node.Text, style);
commands.Add(CreateCommand(text, style));
}
private void InterpretBinding(
BindingNode node,
DataContext context,
List<PrintCommand> commands,
StyleContext style)
{
var element = context.Resolve(node.Path);
var text = _formatter.Format(element, node.Format);
text = ApplyAlignment(text, style);
commands.Add(CreateCommand(text, style));
// Flush any remaining content
FlushLineBuffer();
}
private void InterpretStyledText(
@@ -217,7 +216,11 @@ public class TemplateInterpreter
List<PrintCommand> commands,
StyleContext style)
{
var lineWidth = style.IsBig ? _profile.BigLineWidth : _profile.LineWidth;
// Apply row-level styles
var rowStyle = style.Clone();
ApplyStyles(node.Styles, rowStyle);
var lineWidth = rowStyle.IsBig ? _profile.BigLineWidth : _profile.LineWidth;
var rowBuilder = new StringBuilder();
foreach (var column in node.Columns)
@@ -270,7 +273,7 @@ public class TemplateInterpreter
rowBuilder.Append(aligned);
}
commands.Add(CreateCommand(rowBuilder.ToString(), style));
commands.Add(CreateCommand(rowBuilder.ToString(), rowStyle));
}
private void InterpretColumn(

View File

@@ -13,6 +13,7 @@ public class Lexer
private readonly List<LexerError> _errors = new();
private bool _inMultiLineComment;
private SourcePosition _multiLineCommentStart;
private bool _isCommentOnlyLine;
private static readonly Regex StyleStartRegex = new(@"^#([a-zA-Z0-9,:]+)#", RegexOptions.Compiled);
private static readonly Regex BindingRegex = new(@"^\{([^}]+)\}", RegexOptions.Compiled);
@@ -40,8 +41,14 @@ public class Lexer
while (_lineIndex < _lines.Count)
{
_isCommentOnlyLine = false;
TokenizeLine(_lines[_lineIndex]);
_tokens.Add(new Token(TokenType.NewLine, "\n", new SourcePosition(_lineIndex + 1, _columnIndex + 1)));
// Skip NewLine for comment-only lines and lines inside multi-line comments
if (!_isCommentOnlyLine && !_inMultiLineComment)
{
_tokens.Add(new Token(TokenType.NewLine, "\n", new SourcePosition(_lineIndex + 1, _columnIndex + 1)));
}
_lineIndex++;
_columnIndex = 0;
}
@@ -76,12 +83,14 @@ public class Lexer
}
else
{
// Comment close line with no content after
_isCommentOnlyLine = true;
_columnIndex = line.Length;
}
}
else
{
// Still in comment - skip entire line
// Still in comment - skip entire line (handled by _inMultiLineComment check in Tokenize)
_columnIndex = line.Length;
}
return;
@@ -107,6 +116,8 @@ public class Lexer
}
else
{
// Comment-only line (no content after *@)
_isCommentOnlyLine = true;
_columnIndex = line.Length;
}
}
@@ -114,12 +125,14 @@ public class Lexer
{
// Start of multi-line comment block
_inMultiLineComment = true;
_isCommentOnlyLine = true;
_multiLineCommentStart = new SourcePosition(lineNumber, leadingWhitespace + 1);
_columnIndex = line.Length;
}
else
{
// Single-line comment to end of line
_isCommentOnlyLine = true;
_columnIndex = line.Length;
}
return;
@@ -215,7 +228,7 @@ public class Lexer
return true;
case "@row":
_tokens.Add(new Token(TokenType.Row, string.Empty, position));
_tokens.Add(new Token(TokenType.Row, value, position));
_columnIndex = trimmed.Length + leadingWhitespace;
return true;

View File

@@ -1,6 +1,6 @@
namespace Inspectron.Epson.Templates.Language.Nodes;
public record RowNode(List<ColumnNode> Columns, SourcePosition Position) : ITemplateNode
public record RowNode(List<ColumnNode> Columns, IReadOnlyList<string> Styles, SourcePosition Position) : ITemplateNode
{
public IReadOnlyList<ITemplateNode> Children => Columns;
}

View File

@@ -254,6 +254,9 @@ public class Parser
private RowNode ParseRow()
{
var rowToken = Current;
var styles = string.IsNullOrWhiteSpace(rowToken.Value)
? Array.Empty<string>()
: rowToken.Value.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
Advance(); // consume @row
SkipNewlines();
@@ -300,7 +303,7 @@ public class Parser
AddError("Expected @endrow to close @row block", Current.Position);
}
return new RowNode(columns, rowToken.Position);
return new RowNode(columns, styles, rowToken.Position);
}
private ColumnNode ParseColumnDef()

View File

@@ -0,0 +1,64 @@
{
"Title": "Bar",
"TransactionDateTime": "2025-10-23T20:30:00",
"ReceiptNumber": "132018300",
"WaiterName": "Thomas Weber",
"WaiterId": "28 (Bar Staff)",
"TableNumber": "B5",
"SpecialInstruction": "VIP Table",
"Gangs": [
{
"Id": 1,
"Name": "Round",
"Dishes": [
{
"Number": 2,
"Name": "Aperol Spritz",
"Modifications": {
"Removed": [],
"Added": ["Extra Ice"]
},
"Comment": null
},
{
"Number": 1,
"Name": "Negroni",
"Modifications": {
"Removed": ["Orange Peel"],
"Added": []
},
"Comment": "Stirred, not shaken"
},
{
"Number": 3,
"Name": "San Pellegrino 50cl",
"Modifications": {
"Removed": [],
"Added": []
},
"Comment": null
}
]
}
],
"Dishes": [
{
"Number": 2,
"Name": "Espresso",
"Modifications": {
"Removed": [],
"Added": []
},
"Comment": null
},
{
"Number": 1,
"Name": "Cappuccino",
"Modifications": {
"Removed": [],
"Added": ["Oat Milk"]
},
"Comment": "Extra hot"
}
]
}

View File

@@ -0,0 +1,4 @@
{
"Title": "General Receipt",
"TransactionDateTime": "2025-12-01T14:00:00"
}

View File

@@ -0,0 +1,108 @@
{
"CompanyName": "Klingler Gastro AG",
"Address1": "Munzplatz 3",
"Address2": "CH-8001 Zurich",
"Phone": "043 321 22 22",
"ReceiptNumber": "1-81-202",
"DateTime": "2025-12-16T14:58:00",
"Guests": 2,
"Total": 160.00,
"Currency": "CHF",
"TotalInAlternateCurrency": 172.80,
"AlternateCurrency": "EUR",
"PaymentMethod": "MASTER",
"PaymentAmount": 160.00,
"WaiterName": "Yves",
"Terminal": "Hauptkasse ZH",
"TableNumber": "12",
"VatNumber": "CHE-449.635.880 MWST",
"ThankYouMessage": "Das Team bedankt sich herzlich fur Ihren",
"GoodbyeMessageLine1": "Besuch.",
"GoodbyeMessageLine2": "Auf Wiedersehen.",
"Items": [
{
"Quantity": 1,
"Description": "San Pellegrino 50cl",
"UnitPrice": 6.50,
"TotalPrice": 6.50,
"TaxCategory": "A"
},
{
"Quantity": 1,
"Description": "Panna 50cl",
"UnitPrice": 6.50,
"TotalPrice": 6.50,
"TaxCategory": "A"
},
{
"Quantity": 1,
"Description": "Granini Tomatensaft",
"UnitPrice": 5.50,
"TotalPrice": 5.50,
"TaxCategory": "A"
},
{
"Quantity": 2,
"Description": "Business Lunch Menu Seco",
"UnitPrice": 44.00,
"TotalPrice": 88.00,
"TaxCategory": "A"
},
{
"Quantity": 2,
"Description": "Brunello di Montalcino 1",
"UnitPrice": 16.00,
"TotalPrice": 32.00,
"TaxCategory": "A"
},
{
"Quantity": 2,
"Description": "Espresso",
"UnitPrice": 5.50,
"TotalPrice": 11.00,
"TaxCategory": "A"
},
{
"Quantity": 1,
"Description": "Tip",
"UnitPrice": 10.50,
"TotalPrice": 10.50,
"TaxCategory": "B"
}
],
"TaxBreakdown": [
{
"Category": "A",
"Rate": 8.1,
"Gross": 149.50,
"Net": 138.30,
"TaxAmount": 11.20,
"Currency": "CHF"
},
{
"Category": "B",
"Rate": 0,
"Gross": 10.50,
"Net": 10.50,
"TaxAmount": 0.00,
"Currency": "CHF"
}
],
"TerminalReceipt": {
"ReceiptType": "*** Kundenbeleg ***",
"BookingType": "Buchung",
"PaymentSystem": "TWINT",
"TransactionNumber": "XXXXXXXXXXXXXXX1494",
"TransactionDateTime": "2025-11-11T12:34:49",
"TerminalId": "31108834",
"AID": "A0000015749E",
"TransactionSeqCount": "6127",
"TransactionRefNo": "99036644599",
"AuthCode": "0d3396",
"AcquirerId": "2",
"EftAmount": 67.00,
"TipAmount": 6.70,
"TotalEftAmount": 73.70,
"Currency": "CHF"
}
}

View File

@@ -0,0 +1,60 @@
{
"Title": "Warme Kuche",
"TransactionDateTime": "2025-10-23T12:18:00",
"ReceiptNumber": "132018166",
"WaiterName": "Mariano Amato",
"WaiterId": "32 (VK Restaurant)",
"TableNumber": "22",
"SpecialInstruction": "Next dish",
"Gangs": [
{
"Id": 1,
"Name": "Gang",
"Dishes": [
{
"Number": 1,
"Name": "Avocado Sashimi",
"Modifications": {
"Removed": ["Wasabi"],
"Added": ["Extra Ginger"]
},
"Comment": "No soy sauce"
},
{
"Number": 1,
"Name": "Baby Spinach Salad with Truffl",
"Modifications": {
"Removed": [],
"Added": []
},
"Comment": null
}
]
},
{
"Id": 2,
"Name": "Gang",
"Dishes": [
{
"Number": 1,
"Name": "Beef Tataki",
"Modifications": {
"Removed": ["Onion"],
"Added": ["Extra Sauce"]
},
"Comment": "Medium rare"
},
{
"Number": 1,
"Name": "Salmon Taco",
"Modifications": {
"Removed": [],
"Added": ["Extra Lime"]
},
"Comment": null
}
]
}
],
"Dishes": []
}

View File

@@ -0,0 +1,39 @@
{
"Title": "Kalte Kuche",
"TransactionDateTime": "2025-11-15T18:45:00",
"ReceiptNumber": "132018200",
"WaiterName": "Sofia Mueller",
"WaiterId": "45 (VK Restaurant)",
"TableNumber": "8",
"SpecialInstruction": null,
"Gangs": [],
"Dishes": [
{
"Number": 2,
"Name": "Caesar Salad",
"Modifications": {
"Removed": ["Croutons"],
"Added": ["Extra Parmesan"]
},
"Comment": "Dressing on the side"
},
{
"Number": 1,
"Name": "Carpaccio",
"Modifications": {
"Removed": [],
"Added": []
},
"Comment": null
},
{
"Number": 3,
"Name": "Bruschetta",
"Modifications": {
"Removed": [],
"Added": ["Extra Basil"]
},
"Comment": null
}
]
}

View File

@@ -0,0 +1,58 @@
#center# {CompanyName} #
#center# {Address1} #
#center# {Address2} #
#center# {Phone} #
#bold# {left:Rechnung Nr. {ReceiptNumber}} {right:{DateTime:HH:mm dd.MM.yyyy}} #
Guests: {Guests}
@foreach item in Items
{item.Quantity}x {item.Description} {right:{item.UnitPrice:F2} {item.TotalPrice:F2} {item.TaxCategory}}
@end
{right:---------}
#big,bold,center# Summe: {Total:F2} {Currency} #
@if TotalInAlternateCurrency
{right:{TotalInAlternateCurrency:F2} {AlternateCurrency}}
@end
#bold# {PaymentMethod} {right:{PaymentAmount:F2} {Currency}} #
@if TaxBreakdown.count > 0
MwSt % Brutto Netto MwSt
@foreach tax in TaxBreakdown
{tax.Category}: {tax.Rate}% {tax.Gross:F2} {tax.Currency} {tax.Net:F2} {tax.Currency} {tax.TaxAmount:F2} {tax.Currency}
@end
@end
{right:Bedient von:} {WaiterName}
{right:Terminal:} {Terminal}
{right:Tisch:} {TableNumber}
#center# {VatNumber} #
@if TerminalReceipt
#center# {TerminalReceipt.ReceiptType} #
#center# {TerminalReceipt.BookingType} #
#center# {TerminalReceipt.PaymentSystem} #
{TerminalReceipt.TransactionNumber}
{TerminalReceipt.TransactionDateTime:dd.MM.yyyy} {right:{TerminalReceipt.TransactionDateTime:HH:mm:ss}}
Trm-Id: {right:{TerminalReceipt.TerminalId}}
AID: {right:{TerminalReceipt.AID}}
Trx. Seq-Cnt: {right:{TerminalReceipt.TransactionSeqCount}}
Trx. Ref-No: {right:{TerminalReceipt.TransactionRefNo}}
Auth. Code: {right:{TerminalReceipt.AuthCode}}
Acq-Id: {right:{TerminalReceipt.AcquirerId}}
EFT {TerminalReceipt.Currency}: {right:{TerminalReceipt.EftAmount:F2}}
Trinkgeld {TerminalReceipt.Currency}: {right:{TerminalReceipt.TipAmount:F2}}
Total-EFT {TerminalReceipt.Currency}: {right:{TerminalReceipt.TotalEftAmount:F2}}
---
@end
#center# {ThankYouMessage} #
#center# {GoodbyeMessageLine1} #
#center# {GoodbyeMessageLine2} #