WIP
This commit is contained in:
@@ -0,0 +1,245 @@
|
||||
using System.Globalization;
|
||||
using DevExpress.Office;
|
||||
using DevExpress.XtraRichEdit;
|
||||
using DevExpress.XtraRichEdit.API.Native;
|
||||
|
||||
namespace ROLAC.API.Services.Disbursement;
|
||||
|
||||
/// <summary>
|
||||
/// Renders a check on 8.5"x11" stock using the DevExpress Office (RichEdit) API:
|
||||
/// a check block on top followed by two identical ledger detail stubs. The layout is
|
||||
/// built programmatically (no external .docx template) and exported to PDF.
|
||||
/// </summary>
|
||||
public class CheckPrintService : ICheckPrintService
|
||||
{
|
||||
public Task<Stream> RenderPdfAsync(CheckPrintModel model)
|
||||
{
|
||||
using var server = new RichEditDocumentServer();
|
||||
var doc = server.Document;
|
||||
doc.BeginUpdate();
|
||||
try
|
||||
{
|
||||
doc.Unit = DocumentUnit.Inch;
|
||||
var section = doc.Sections[0];
|
||||
section.Page.Width = 8.5f;
|
||||
section.Page.Height = 11f;
|
||||
section.Margins.Left = section.Margins.Right = 0.6f;
|
||||
section.Margins.Top = section.Margins.Bottom = 0.5f;
|
||||
|
||||
BuildCheckBlock(doc, model);
|
||||
BuildStub(doc, model, "PAYMENT ADVICE — DETAIL");
|
||||
BuildStub(doc, model, "PAYMENT ADVICE — RECORD COPY");
|
||||
}
|
||||
finally
|
||||
{
|
||||
doc.EndUpdate();
|
||||
}
|
||||
|
||||
var ms = new MemoryStream();
|
||||
server.ExportToPdf(ms);
|
||||
ms.Position = 0;
|
||||
return Task.FromResult<Stream>(ms);
|
||||
}
|
||||
|
||||
public Task<Stream> RenderReceiptPdfAsync(CheckPrintModel model)
|
||||
{
|
||||
using var server = new RichEditDocumentServer();
|
||||
var document = server.Document;
|
||||
document.BeginUpdate();
|
||||
try
|
||||
{
|
||||
document.Unit = DocumentUnit.Inch;
|
||||
var section = document.Sections[0];
|
||||
section.Page.Width = 8.5f;
|
||||
section.Page.Height = 11f;
|
||||
section.Margins.Left = section.Margins.Right = 0.8f;
|
||||
section.Margins.Top = section.Margins.Bottom = 0.8f;
|
||||
|
||||
document.AppendHtmlText(BuildReceiptHtml(model));
|
||||
}
|
||||
finally
|
||||
{
|
||||
document.EndUpdate();
|
||||
}
|
||||
|
||||
var stream = new MemoryStream();
|
||||
server.ExportToPdf(stream);
|
||||
stream.Position = 0;
|
||||
return Task.FromResult<Stream>(stream);
|
||||
}
|
||||
|
||||
private static string BuildReceiptHtml(CheckPrintModel model)
|
||||
{
|
||||
var issuer = model.Issuer;
|
||||
var check = model.Check;
|
||||
|
||||
var issuerAddress = Encode(JoinAddress(issuer.Address, issuer.City, issuer.State, issuer.ZipCode));
|
||||
var payeeAddress = Encode(JoinAddress(check.PayeeAddress, check.PayeeCity, check.PayeeState, check.PayeeZip));
|
||||
|
||||
var detailRows = new System.Text.StringBuilder();
|
||||
foreach (var line in model.Lines)
|
||||
{
|
||||
var date = line.Expense?.ExpenseDate.ToString("MM/dd/yyyy") ?? "";
|
||||
detailRows.Append(
|
||||
"<tr>" +
|
||||
$"<td>{Encode(date)}</td>" +
|
||||
$"<td>{Encode(line.Description)}</td>" +
|
||||
$"<td align=\"right\">{Encode(FormatCurrency(line.Amount))}</td>" +
|
||||
"</tr>");
|
||||
}
|
||||
|
||||
var signedOn = check.ReceiptSignedAt.HasValue
|
||||
? check.ReceiptSignedAt.Value.ToLocalTime().ToString("MM/dd/yyyy HH:mm")
|
||||
: "";
|
||||
|
||||
var signatureBlock = "";
|
||||
if (model.SignatureImage is { Length: > 0 })
|
||||
{
|
||||
var base64 = Convert.ToBase64String(model.SignatureImage);
|
||||
signatureBlock =
|
||||
$"<p><img src=\"data:{model.SignatureContentType};base64,{base64}\" width=\"260\" /></p>";
|
||||
}
|
||||
|
||||
return
|
||||
"<div style=\"font-family:Arial;font-size:11pt;color:#111;\">" +
|
||||
"<h2 style=\"text-align:center;margin:0;\">Disbursement Receipt / 簽收收據</h2>" +
|
||||
$"<p style=\"text-align:center;margin:4px 0 16px 0;\"><b>{Encode(issuer.Name)}</b><br/>{issuerAddress}</p>" +
|
||||
|
||||
"<h3 style=\"margin:8px 0;\">Check Information / 支票資訊</h3>" +
|
||||
"<table border=\"1\" cellspacing=\"0\" cellpadding=\"5\" width=\"100%\" style=\"border-collapse:collapse;\">" +
|
||||
$"<tr><td width=\"22%\"><b>Check No. / 支票號碼</b></td><td width=\"28%\">{Encode(check.CheckNumber)}</td>" +
|
||||
$"<td width=\"22%\"><b>Date / 日期</b></td><td width=\"28%\">{check.CheckDate:MM/dd/yyyy}</td></tr>" +
|
||||
$"<tr><td><b>Payee / 收款人</b></td><td colspan=\"3\">{Encode(check.PayeeName)} {payeeAddress}</td></tr>" +
|
||||
$"<tr><td><b>Amount / 金額</b></td><td colspan=\"3\">{Encode(FormatCurrency(check.Amount))} — {Encode(model.AmountInWords)}</td></tr>" +
|
||||
$"<tr><td><b>Memo / 摘要</b></td><td colspan=\"3\">{Encode(check.Memo ?? "")}</td></tr>" +
|
||||
"</table>" +
|
||||
|
||||
"<h3 style=\"margin:16px 0 8px 0;\">Disbursement Detail / 撥款明細</h3>" +
|
||||
"<table border=\"1\" cellspacing=\"0\" cellpadding=\"5\" width=\"100%\" style=\"border-collapse:collapse;\">" +
|
||||
"<tr><th align=\"left\">Date / 日期</th><th align=\"left\">Description / 說明</th><th align=\"right\">Amount / 金額</th></tr>" +
|
||||
detailRows +
|
||||
$"<tr><td></td><td align=\"right\"><b>TOTAL / 合計</b></td><td align=\"right\"><b>{Encode(FormatCurrency(check.Amount))}</b></td></tr>" +
|
||||
"</table>" +
|
||||
|
||||
"<h3 style=\"margin:16px 0 8px 0;\">Acknowledgement of Receipt / 收款簽收</h3>" +
|
||||
"<p>I acknowledge receipt of the above payment in full. / 本人確認已如數收到上述款項。</p>" +
|
||||
$"<p><b>Received by / 簽收人:</b> {Encode(check.ReceiptSignedName ?? "")}<br/>" +
|
||||
$"<b>Date / 簽收日期:</b> {Encode(signedOn)}</p>" +
|
||||
signatureBlock +
|
||||
"</div>";
|
||||
}
|
||||
|
||||
private static string Encode(string? text) => System.Net.WebUtility.HtmlEncode(text ?? "");
|
||||
|
||||
private static void BuildCheckBlock(Document doc, CheckPrintModel m)
|
||||
{
|
||||
var issuer = m.Issuer;
|
||||
var check = m.Check;
|
||||
|
||||
AppendLine(doc, issuer.Name, bold: true, size: 13);
|
||||
var issuerAddr = JoinAddress(issuer.Address, issuer.City, issuer.State, issuer.ZipCode);
|
||||
if (!string.IsNullOrWhiteSpace(issuerAddr)) AppendLine(doc, issuerAddr, size: 9);
|
||||
if (!string.IsNullOrWhiteSpace(issuer.BankName)) AppendLine(doc, issuer.BankName, size: 9);
|
||||
|
||||
AppendLine(doc, "");
|
||||
AppendLine(doc, $"Check No: {check.CheckNumber} Date: {check.CheckDate:MM/dd/yyyy}", bold: true, size: 10);
|
||||
AppendLine(doc, "");
|
||||
|
||||
AppendLine(doc, $"PAY TO THE ORDER OF: {check.PayeeName}", bold: true, size: 11);
|
||||
var payeeAddr = JoinAddress(check.PayeeAddress, check.PayeeCity, check.PayeeState, check.PayeeZip);
|
||||
if (!string.IsNullOrWhiteSpace(payeeAddr)) AppendLine(doc, payeeAddr, size: 9);
|
||||
|
||||
AppendLine(doc, "");
|
||||
AppendLine(doc, $"AMOUNT: {FormatCurrency(check.Amount)}", bold: true, size: 12);
|
||||
AppendLine(doc, m.AmountInWords, size: 10);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(check.Memo)) { AppendLine(doc, ""); AppendLine(doc, $"Memo: {check.Memo}", size: 9); }
|
||||
|
||||
AppendLine(doc, "");
|
||||
AppendLine(doc, "____________________________________", size: 10);
|
||||
AppendLine(doc, "Authorized Signature", size: 8);
|
||||
AppendSeparator(doc);
|
||||
}
|
||||
|
||||
private static void BuildStub(Document doc, CheckPrintModel m, string title)
|
||||
{
|
||||
var check = m.Check;
|
||||
AppendLine(doc, title, bold: true, size: 10);
|
||||
AppendLine(doc, $"Check No: {check.CheckNumber} Date: {check.CheckDate:MM/dd/yyyy} Payee: {check.PayeeName}", size: 9);
|
||||
AppendLine(doc, "");
|
||||
|
||||
var rows = m.Lines.Count + 2; // header + lines + total
|
||||
var table = doc.Tables.Create(doc.Range.End, rows, 3, AutoFitBehaviorType.AutoFitToWindow);
|
||||
table.Borders.InsideHorizontalBorder.LineStyle = TableBorderLineStyle.Single;
|
||||
table.Borders.Top.LineStyle = table.Borders.Bottom.LineStyle = TableBorderLineStyle.Single;
|
||||
|
||||
SetCell(doc, table[0, 0], "Date", bold: true);
|
||||
SetCell(doc, table[0, 1], "Description", bold: true);
|
||||
SetCell(doc, table[0, 2], "Amount", bold: true, right: true);
|
||||
|
||||
for (var i = 0; i < m.Lines.Count; i++)
|
||||
{
|
||||
var line = m.Lines[i];
|
||||
var r = i + 1;
|
||||
// CheckLine snapshots description; date comes from the source expense if loaded.
|
||||
var date = line.Expense?.ExpenseDate.ToString("MM/dd/yyyy") ?? "";
|
||||
SetCell(doc, table[r, 0], date);
|
||||
SetCell(doc, table[r, 1], line.Description);
|
||||
SetCell(doc, table[r, 2], FormatCurrency(line.Amount), right: true);
|
||||
}
|
||||
|
||||
var totalRow = rows - 1;
|
||||
SetCell(doc, table[totalRow, 0], "");
|
||||
SetCell(doc, table[totalRow, 1], "TOTAL", bold: true, right: true);
|
||||
SetCell(doc, table[totalRow, 2], FormatCurrency(check.Amount), bold: true, right: true);
|
||||
|
||||
AppendLine(doc, "");
|
||||
if (check.ReceiptSignedAt is { } signedAt)
|
||||
AppendLine(doc, $"Received by: {check.ReceiptSignedName} on {signedAt:MM/dd/yyyy HH:mm}", size: 9);
|
||||
AppendSeparator(doc);
|
||||
}
|
||||
|
||||
// ── low-level helpers ──────────────────────────────────────────────────────
|
||||
|
||||
private static void AppendLine(Document doc, string text, bool bold = false, float size = 10)
|
||||
{
|
||||
var range = doc.AppendText(text + "\r\n");
|
||||
var cp = doc.BeginUpdateCharacters(range);
|
||||
cp.Bold = bold;
|
||||
cp.FontSize = size;
|
||||
doc.EndUpdateCharacters(cp);
|
||||
}
|
||||
|
||||
private static void AppendSeparator(Document doc)
|
||||
{
|
||||
AppendLine(doc, "");
|
||||
AppendLine(doc, "------------------------------------------------------------------------------------------", size: 8);
|
||||
AppendLine(doc, "");
|
||||
}
|
||||
|
||||
private static void SetCell(Document doc, TableCell cell, string text, bool bold = false, bool right = false)
|
||||
{
|
||||
var range = doc.InsertText(cell.ContentRange.Start, text);
|
||||
var cp = doc.BeginUpdateCharacters(range);
|
||||
cp.Bold = bold;
|
||||
cp.FontSize = 9;
|
||||
doc.EndUpdateCharacters(cp);
|
||||
if (right)
|
||||
{
|
||||
var pp = doc.BeginUpdateParagraphs(range);
|
||||
pp.Alignment = ParagraphAlignment.Right;
|
||||
doc.EndUpdateParagraphs(pp);
|
||||
}
|
||||
}
|
||||
|
||||
private static string FormatCurrency(decimal amount) =>
|
||||
amount.ToString("C2", CultureInfo.GetCultureInfo("en-US"));
|
||||
|
||||
private static string JoinAddress(string? addr, string? city, string? state, string? zip)
|
||||
{
|
||||
var cityLine = string.Join(", ",
|
||||
new[] { city, string.Join(" ", new[] { state, zip }.Where(s => !string.IsNullOrWhiteSpace(s))) }
|
||||
.Where(s => !string.IsNullOrWhiteSpace(s)));
|
||||
return string.Join(" ", new[] { addr, cityLine }.Where(s => !string.IsNullOrWhiteSpace(s)));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user