Migrate ExcelIO from ClosedXML to NPOI

This commit is contained in:
Joseph Doherty
2026-02-06 17:27:09 -05:00
parent 070d915b12
commit dd18a05408
26 changed files with 3034 additions and 2805 deletions
+213 -234
View File
@@ -1,234 +1,213 @@
using ClosedXML.Excel; using JdeScoping.Core.Interfaces;
using JdeScoping.Core.Interfaces; using JdeScoping.Core.Models.SearchResults;
using JdeScoping.Core.Models.SearchResults; using JdeScoping.ExcelIO.Options;
using JdeScoping.ExcelIO.Options; using JdeScoping.ExcelIO.Formatting;
using JdeScoping.ExcelIO.Formatting; using JdeScoping.ExcelIO.Generators;
using JdeScoping.ExcelIO.Generators; using JdeScoping.ExcelIO.Mapping;
using JdeScoping.ExcelIO.Mapping; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options;
using Microsoft.Extensions.Options; using NPOI.SS.UserModel;
using NPOI.XSSF.UserModel;
// Use Core's SearchModel for the public interface
using CoreSearchModel = JdeScoping.Core.Models.SearchResults.SearchModel; using CoreSearchModel = JdeScoping.Core.Models.SearchResults.SearchModel;
// Use ExcelIO's SearchModel which contains criteria filter properties for CriteriaSheetGenerator using ExcelSearchModel = JdeScoping.ExcelIO.Models.Reporting.SearchModel;
using ExcelSearchModel = JdeScoping.ExcelIO.Models.Reporting.SearchModel;
namespace JdeScoping.ExcelIO;
namespace JdeScoping.ExcelIO;
/// <summary>
/// <summary> /// Service for generating Excel export files from search results.
/// Service for generating Excel export files from search results. /// </summary>
/// </summary> public class ExcelExportService : IExcelExportService
public class ExcelExportService : IExcelExportService {
{ private readonly ILogger<ExcelExportService> _logger;
private readonly ILogger<ExcelExportService> _logger; private readonly IOptions<ExcelExportOptions> _options;
private readonly IOptions<ExcelExportOptions> _options; private readonly CriteriaSheetGenerator _criteriaGenerator;
private readonly CriteriaSheetGenerator _criteriaGenerator; private readonly FluentTableWriter _tableWriter;
private readonly FluentTableWriter _tableWriter; private readonly ExcelMapRegistry _registry;
private readonly ExcelMapRegistry _registry;
/// <summary>
/// <summary> /// Initializes a new instance of the ExcelExportService class.
/// Initializes a new instance of the ExcelExportService class. /// </summary>
/// </summary> /// <param name="logger">Logger instance.</param>
/// <param name="logger">Logger instance.</param> /// <param name="options">Excel export options.</param>
/// <param name="options">Excel export options.</param> /// <param name="criteriaGenerator">Criteria sheet generator.</param>
/// <param name="criteriaGenerator">Criteria sheet generator.</param> /// <param name="tableWriter">Fluent table writer.</param>
/// <param name="tableWriter">Fluent table writer.</param> /// <param name="registry">Excel map registry.</param>
/// <param name="registry">Excel map registry.</param> public ExcelExportService(
public ExcelExportService( ILogger<ExcelExportService> logger,
ILogger<ExcelExportService> logger, IOptions<ExcelExportOptions> options,
IOptions<ExcelExportOptions> options, CriteriaSheetGenerator criteriaGenerator,
CriteriaSheetGenerator criteriaGenerator, FluentTableWriter tableWriter,
FluentTableWriter tableWriter, ExcelMapRegistry registry)
ExcelMapRegistry registry) {
{ _logger = logger;
_logger = logger; _options = options;
_options = options; _criteriaGenerator = criteriaGenerator;
_criteriaGenerator = criteriaGenerator; _tableWriter = tableWriter;
_tableWriter = tableWriter; _registry = registry;
_registry = registry; }
}
/// <inheritdoc />
/// <inheritdoc /> public async Task<byte[]> GenerateAsync(CoreSearchModel search, CancellationToken cancellationToken = default)
public async Task<byte[]> GenerateAsync(CoreSearchModel search, CancellationToken cancellationToken = default) {
{ ArgumentNullException.ThrowIfNull(search);
ArgumentNullException.ThrowIfNull(search);
var excelModel = MapToExcelModel(search);
// Map Core SearchModel to ExcelIO SearchModel for internal processing return await GenerateInternalAsync(excelModel, cancellationToken);
var excelModel = MapToExcelModel(search); }
return await GenerateInternalAsync(excelModel, cancellationToken);
} /// <summary>
/// Maps Core SearchModel to ExcelIO SearchModel for Excel generation.
/// <summary> /// </summary>
/// Maps Core SearchModel to ExcelIO SearchModel for Excel generation. private static ExcelSearchModel MapToExcelModel(CoreSearchModel source)
/// </summary> {
private static ExcelSearchModel MapToExcelModel(CoreSearchModel source) return new ExcelSearchModel
{ {
return new ExcelSearchModel Id = source.Id,
{ UserName = source.UserName,
Id = source.Id, Name = source.Name,
UserName = source.UserName, SubmitDt = source.SubmitDt,
Name = source.Name, StartDt = source.StartDt,
SubmitDt = source.SubmitDt, EndDt = source.EndDt,
StartDt = source.StartDt, ExtractMisData = source.ExtractMisData,
EndDt = source.EndDt, Results = source.Results,
ExtractMisData = source.ExtractMisData, MisResults = source.MisResults,
Results = source.Results, MisNonMatchResults = source.MisNonMatchResults
MisResults = source.MisResults, };
MisNonMatchResults = source.MisNonMatchResults }
};
} /// <summary>
/// Internal method that generates an Excel file from the ExcelIO search model.
/// <summary> /// </summary>
/// Internal method that generates an Excel file from the ExcelIO search model. /// <param name="search">ExcelIO search model with criteria and results.</param>
/// </summary> /// <param name="cancellationToken">Cancellation token.</param>
/// <param name="search">ExcelIO search model with criteria and results.</param> /// <returns>Excel file as byte array.</returns>
/// <param name="cancellationToken">Cancellation token.</param> private async Task<byte[]> GenerateInternalAsync(ExcelSearchModel search, CancellationToken cancellationToken = default)
/// <returns>Excel file as byte array.</returns> {
private async Task<byte[]> GenerateInternalAsync(ExcelSearchModel search, CancellationToken cancellationToken = default) using var scope = _logger.BeginScope(new Dictionary<string, object>
{ {
using var scope = _logger.BeginScope(new Dictionary<string, object> ["SearchId"] = search.Id,
{ ["SearchName"] = search.Name
["SearchId"] = search.Id, });
["SearchName"] = search.Name
}); _logger.LogInformation("Starting Excel export generation");
_logger.LogInformation("Starting Excel export generation"); // NPOI operations are synchronous, wrap in Task.Run for non-blocking.
return await Task.Run(() =>
// ClosedXML operations are synchronous, wrap in Task.Run for non-blocking {
return await Task.Run(() => cancellationToken.ThrowIfCancellationRequested();
{
cancellationToken.ThrowIfCancellationRequested(); using IWorkbook workbook = new XSSFWorkbook();
using var workbook = new XLWorkbook(); _logger.LogDebug("Generating Search Criteria sheet");
_criteriaGenerator.Generate(workbook, search);
// 1. Always generate Search Criteria sheet (first tab)
_logger.LogDebug("Generating Search Criteria sheet"); cancellationToken.ThrowIfCancellationRequested();
_criteriaGenerator.Generate(workbook, search);
_logger.LogDebug("Generating Search Results sheet");
cancellationToken.ThrowIfCancellationRequested(); GenerateResultsSheet(workbook, search.Results);
// 2. Always generate Search Results sheet (second tab) cancellationToken.ThrowIfCancellationRequested();
_logger.LogDebug("Generating Search Results sheet");
GenerateResultsSheet(workbook, search.Results); if (search.ExtractMisData && search.MisResults != null && search.MisResults.Count > 0)
{
cancellationToken.ThrowIfCancellationRequested(); _logger.LogDebug("Generating MIS Info sheet with {Count} records", search.MisResults.Count);
GenerateMisInfoSheet(workbook, search.MisResults);
// 3. Conditionally generate MIS Info sheet }
if (search.ExtractMisData && search.MisResults != null && search.MisResults.Count > 0) else if (search.ExtractMisData)
{ {
_logger.LogDebug("Generating MIS Info sheet with {Count} records", search.MisResults.Count); _logger.LogWarning("ExtractMisData is true but MisResults is null or empty");
GenerateMisInfoSheet(workbook, search.MisResults); }
}
else if (search.ExtractMisData) cancellationToken.ThrowIfCancellationRequested();
{
_logger.LogWarning("ExtractMisData is true but MisResults is null or empty"); if (search.ExtractMisData && search.MisNonMatchResults != null && search.MisNonMatchResults.Count > 0)
} {
_logger.LogDebug("Generating Investigation sheet with {Count} records", search.MisNonMatchResults.Count);
cancellationToken.ThrowIfCancellationRequested(); GenerateInvestigationSheet(workbook, search.MisNonMatchResults);
}
// 4. Conditionally generate Investigation sheet
if (search.ExtractMisData && search.MisNonMatchResults != null && search.MisNonMatchResults.Count > 0) cancellationToken.ThrowIfCancellationRequested();
{
_logger.LogDebug("Generating Investigation sheet with {Count} records", search.MisNonMatchResults.Count); using var stream = new MemoryStream();
GenerateInvestigationSheet(workbook, search.MisNonMatchResults); workbook.Write(stream, leaveOpen: true);
} var result = stream.ToArray();
cancellationToken.ThrowIfCancellationRequested(); _logger.LogInformation("Excel export generation completed. Size: {Size} bytes", result.Length);
// Save to byte array if (_options.Value.DebugWriteToFile)
using var stream = new MemoryStream(); {
workbook.SaveAs(stream); WriteDebugCopy(search.Id, result);
var result = stream.ToArray(); }
_logger.LogInformation("Excel export generation completed. Size: {Size} bytes", result.Length); return result;
}, cancellationToken);
// Optional: write debug copy to disk }
if (_options.Value.DebugWriteToFile)
{ private void GenerateResultsSheet(IWorkbook workbook, List<SearchResult> results)
WriteDebugCopy(search.Id, result); {
} var map = _registry.GetMap<SearchResult>();
var tabName = map.TabName ?? "Search Results";
return result;
var worksheet = workbook.CreateSheet(tabName);
}, cancellationToken); var table = _tableWriter.WriteTable(worksheet, 1, 1, results);
}
if (table != null)
private void GenerateResultsSheet(XLWorkbook workbook, List<SearchResult> results) {
{ WorksheetProtector.UnlockExtensionArea(worksheet, table.LastRow, table.LastCol);
var map = _registry.GetMap<SearchResult>(); WorksheetProtector.ApplyProtection(worksheet, _options.Value.DataSheetPassword);
var tabName = map.TabName ?? "Search Results"; }
}
var worksheet = workbook.Worksheets.Add(tabName);
var table = _tableWriter.WriteTable(worksheet, 1, 1, results); private void GenerateMisInfoSheet(IWorkbook workbook, List<MisSearchResult> misResults)
{
if (table != null) var map = _registry.GetMap<MisSearchResult>();
{ var tabName = map.TabName ?? "MIS Info";
// Apply protection with editable extension area
var lastRow = table.RangeAddress.LastAddress.RowNumber; var worksheet = workbook.CreateSheet(tabName);
var lastCol = table.RangeAddress.LastAddress.ColumnNumber; var table = _tableWriter.WriteTable(worksheet, 1, 1, misResults);
WorksheetProtector.UnlockExtensionArea(worksheet, lastRow, lastCol); if (table != null)
WorksheetProtector.ApplyProtection(worksheet, _options.Value.DataSheetPassword); {
} WorksheetProtector.UnlockExtensionArea(worksheet, table.LastRow, table.LastCol);
} WorksheetProtector.ApplyProtection(worksheet, _options.Value.DataSheetPassword);
}
private void GenerateMisInfoSheet(XLWorkbook workbook, List<MisSearchResult> misResults) }
{
var map = _registry.GetMap<MisSearchResult>(); private void GenerateInvestigationSheet(IWorkbook workbook, List<MisNonMatchSearchResult> misNonMatchResults)
var tabName = map.TabName ?? "MIS Info"; {
var map = _registry.GetMap<MisNonMatchSearchResult>();
var worksheet = workbook.Worksheets.Add(tabName); var tabName = map.TabName ?? "Investigation";
var table = _tableWriter.WriteTable(worksheet, 1, 1, misResults);
var worksheet = workbook.CreateSheet(tabName);
if (table != null) var table = _tableWriter.WriteTable(worksheet, 1, 1, misNonMatchResults);
{
// Apply protection with editable extension area if (table != null)
var lastRow = table.RangeAddress.LastAddress.RowNumber; {
var lastCol = table.RangeAddress.LastAddress.ColumnNumber; WorksheetProtector.UnlockExtensionArea(worksheet, table.LastRow, table.LastCol);
WorksheetProtector.ApplyProtection(worksheet, _options.Value.DataSheetPassword);
WorksheetProtector.UnlockExtensionArea(worksheet, lastRow, lastCol); }
WorksheetProtector.ApplyProtection(worksheet, _options.Value.DataSheetPassword); }
}
} private void WriteDebugCopy(int searchId, byte[] result)
{
private void GenerateInvestigationSheet(XLWorkbook workbook, List<MisNonMatchSearchResult> misNonMatchResults) try
{ {
var map = _registry.GetMap<MisNonMatchSearchResult>(); var directory = _options.Value.DebugOutputDirectory;
var tabName = map.TabName ?? "Investigation"; if (!Directory.Exists(directory))
{
var worksheet = workbook.Worksheets.Add(tabName); Directory.CreateDirectory(directory);
var table = _tableWriter.WriteTable(worksheet, 1, 1, misNonMatchResults); }
if (table != null) var debugPath = Path.Combine(
{ directory,
// Apply protection with editable extension area $"Search_{searchId}_{DateTime.Now:yyyyMMdd_HHmmss}.xlsx");
var lastRow = table.RangeAddress.LastAddress.RowNumber; File.WriteAllBytes(debugPath, result);
var lastCol = table.RangeAddress.LastAddress.ColumnNumber; _logger.LogDebug("Debug copy written to {Path}", debugPath);
}
WorksheetProtector.UnlockExtensionArea(worksheet, lastRow, lastCol); catch (Exception ex)
WorksheetProtector.ApplyProtection(worksheet, _options.Value.DataSheetPassword); {
} _logger.LogWarning(ex, "Failed to write debug copy");
} }
}
private void WriteDebugCopy(int searchId, byte[] result) }
{
try
{
var directory = _options.Value.DebugOutputDirectory;
if (!Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
var debugPath = Path.Combine(
directory,
$"Search_{searchId}_{DateTime.Now:yyyyMMdd_HHmmss}.xlsx");
File.WriteAllBytes(debugPath, result);
_logger.LogDebug("Debug copy written to {Path}", debugPath);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to write debug copy");
}
}
}
@@ -1,49 +1,90 @@
using ClosedXML.Excel; using NPOI.SS.UserModel;
using NPOI.SS.Util;
namespace JdeScoping.ExcelIO.Formatting; using NPOI.XSSF.UserModel;
/// <summary> namespace JdeScoping.ExcelIO.Formatting;
/// Header cell formatting utilities.
/// </summary> /// <summary>
public static class HeaderFormatter /// Header cell formatting utilities.
{ /// </summary>
/// <summary> public static class HeaderFormatter
/// Applies header formatting to a cell. {
/// </summary> private static readonly byte[] GainsboroRgb = [0xDC, 0xDC, 0xDC];
/// <param name="cell">The cell to format.</param>
/// <param name="text">Optional text to set in the cell.</param> /// <summary>
public static void ApplyHeaderFormat(IXLCell cell, string? text = null) /// Applies header formatting to a cell.
{ /// </summary>
cell.Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center; /// <param name="cell">The cell to format.</param>
cell.Style.Font.Bold = true; /// <param name="text">Optional text to set in the cell.</param>
cell.Style.Fill.BackgroundColor = XLColor.Gainsboro; public static void ApplyHeaderFormat(ICell cell, string? text = null)
{
if (!string.IsNullOrEmpty(text)) var workbook = cell.Sheet.Workbook;
{ var style = workbook.CreateCellStyle();
cell.Value = text; style.Alignment = HorizontalAlignment.Center;
} style.FillPattern = FillPattern.SolidForeground;
}
if (style is XSSFCellStyle xssfStyle)
/// <summary> {
/// Applies header formatting to a range. var color = new XSSFColor();
/// </summary> color.SetRgb(GainsboroRgb);
/// <param name="range">The range to format.</param> xssfStyle.SetFillForegroundColor(color);
/// <param name="text">Optional text to set in the first cell.</param> }
/// <param name="merge">Whether to merge the range.</param> else
public static void ApplyHeaderFormat(IXLRange range, string? text = null, bool merge = false) {
{ style.FillForegroundColor = IndexedColors.Grey25Percent.Index;
range.Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center; }
range.Style.Font.Bold = true;
range.Style.Fill.BackgroundColor = XLColor.Gainsboro; var font = workbook.CreateFont();
font.IsBold = true;
if (merge) style.SetFont(font);
{
range.Merge(); cell.CellStyle = style;
}
if (!string.IsNullOrEmpty(text))
if (!string.IsNullOrEmpty(text)) {
{ cell.SetCellValue(text);
range.FirstCell().Value = text; }
} }
}
} /// <summary>
/// Applies header formatting to a range.
/// </summary>
/// <param name="sheet">The worksheet containing the range.</param>
/// <param name="firstRow">First row number (1-based).</param>
/// <param name="firstCol">First column number (1-based).</param>
/// <param name="lastRow">Last row number (1-based).</param>
/// <param name="lastCol">Last column number (1-based).</param>
/// <param name="text">Optional text to set in the first cell.</param>
/// <param name="merge">Whether to merge the range.</param>
public static void ApplyHeaderFormat(
ISheet sheet,
int firstRow,
int firstCol,
int lastRow,
int lastCol,
string? text = null,
bool merge = false)
{
for (var row = firstRow; row <= lastRow; row++)
{
var npoiRow = sheet.GetRow(row - 1) ?? sheet.CreateRow(row - 1);
for (var col = firstCol; col <= lastCol; col++)
{
var cell = npoiRow.GetCell(col - 1) ?? npoiRow.CreateCell(col - 1);
ApplyHeaderFormat(cell);
}
}
if (merge)
{
sheet.AddMergedRegion(new CellRangeAddress(firstRow - 1, lastRow - 1, firstCol - 1, lastCol - 1));
}
if (!string.IsNullOrEmpty(text))
{
var row = sheet.GetRow(firstRow - 1) ?? sheet.CreateRow(firstRow - 1);
var cell = row.GetCell(firstCol - 1) ?? row.CreateCell(firstCol - 1);
cell.SetCellValue(text);
}
}
}
@@ -1,62 +1,81 @@
using ClosedXML.Excel; using NPOI.SS.UserModel;
using NPOI.XSSF.UserModel;
namespace JdeScoping.ExcelIO.Formatting;
namespace JdeScoping.ExcelIO.Formatting;
/// <summary>
/// Worksheet protection utilities. /// <summary>
/// </summary> /// Worksheet protection utilities.
public static class WorksheetProtector /// </summary>
{ public static class WorksheetProtector
/// <summary> {
/// Applies standard protection to a worksheet with the given password. /// <summary>
/// </summary> /// Applies standard protection to a worksheet with the given password.
/// <param name="worksheet">The worksheet to protect.</param> /// </summary>
/// <param name="password">The protection password.</param> /// <param name="worksheet">The worksheet to protect.</param>
public static void ApplyProtection(IXLWorksheet worksheet, string password) /// <param name="password">The protection password.</param>
{ public static void ApplyProtection(ISheet worksheet, string password)
var protection = worksheet.Protect(password); {
worksheet.ProtectSheet(password);
// Allow these operations
protection.AllowElement(XLSheetProtectionElements.DeleteColumns); if (worksheet is not XSSFSheet xssfSheet)
protection.AllowElement(XLSheetProtectionElements.DeleteRows); {
protection.AllowElement(XLSheetProtectionElements.AutoFilter); return;
protection.AllowElement(XLSheetProtectionElements.FormatCells); }
protection.AllowElement(XLSheetProtectionElements.FormatColumns);
protection.AllowElement(XLSheetProtectionElements.FormatRows); // Set to false to allow these operations while sheet protection is enabled.
protection.AllowElement(XLSheetProtectionElements.SelectLockedCells); xssfSheet.LockDeleteColumns(false);
protection.AllowElement(XLSheetProtectionElements.SelectUnlockedCells); xssfSheet.LockDeleteRows(false);
protection.AllowElement(XLSheetProtectionElements.EditObjects); xssfSheet.LockAutoFilter(false);
protection.AllowElement(XLSheetProtectionElements.Sort); xssfSheet.LockFormatCells(false);
} xssfSheet.LockFormatColumns(false);
xssfSheet.LockFormatRows(false);
/// <summary> xssfSheet.LockSelectLockedCells(false);
/// Applies criteria sheet protection (simpler, just password). xssfSheet.LockSelectUnlockedCells(false);
/// </summary> xssfSheet.LockObjects(false);
/// <param name="worksheet">The worksheet to protect.</param> xssfSheet.LockSort(false);
/// <param name="password">The protection password.</param> xssfSheet.EnableLocking();
public static void ApplyCriteriaProtection(IXLWorksheet worksheet, string password) }
{
worksheet.Protect(password); /// <summary>
} /// Applies criteria sheet protection (simpler, just password).
/// </summary>
/// <summary> /// <param name="worksheet">The worksheet to protect.</param>
/// Unlocks a range for user editing beyond the data area. /// <param name="password">The protection password.</param>
/// </summary> public static void ApplyCriteriaProtection(ISheet worksheet, string password)
/// <param name="worksheet">The worksheet containing the range.</param> {
/// <param name="lastDataRow">The last row of data.</param> worksheet.ProtectSheet(password);
/// <param name="lastDataCol">The last column of data.</param> }
/// <param name="extensionRows">Number of rows to unlock beyond data (default 1000).</param>
/// <param name="extensionCols">Number of columns to unlock beyond data (default 1000).</param> /// <summary>
public static void UnlockExtensionArea( /// Unlocks a range for user editing beyond the data area.
IXLWorksheet worksheet, /// </summary>
int lastDataRow, /// <param name="worksheet">The worksheet containing the range.</param>
int lastDataCol, /// <param name="lastDataRow">The last row of data.</param>
int extensionRows = 1000, /// <param name="lastDataCol">The last column of data.</param>
int extensionCols = 1000) /// <param name="extensionRows">Number of rows to unlock beyond data (default 1000).</param>
{ /// <param name="extensionCols">Number of columns to unlock beyond data (default 1000).</param>
var extensionRange = worksheet.Range( public static void UnlockExtensionArea(
1, lastDataCol + 1, ISheet worksheet,
lastDataRow + extensionRows, lastDataCol + extensionCols); int lastDataRow,
extensionRange.Style.Protection.Locked = false; int lastDataCol,
} int extensionRows = 1000,
} int extensionCols = 1000)
{
var workbook = worksheet.Workbook;
var unlockedStyle = workbook.CreateCellStyle();
unlockedStyle.IsLocked = false;
// Apply unlocked default style to extension columns (NPOI uses 0-based columns).
var startCol = lastDataCol;
var endCol = lastDataCol + extensionCols - 1;
for (var col = startCol; col <= endCol; col++)
{
worksheet.SetDefaultColumnStyle(col, unlockedStyle);
}
// Touch at least one extension cell to ensure style materializes in workbook XML.
var row = worksheet.GetRow(0) ?? worksheet.CreateRow(0);
var cell = row.GetCell(startCol) ?? row.CreateCell(startCol);
cell.CellStyle = unlockedStyle;
}
}
@@ -1,193 +1,134 @@
using ClosedXML.Excel; using JdeScoping.ExcelIO.Options;
using JdeScoping.ExcelIO.Options; using JdeScoping.ExcelIO.Formatting;
using JdeScoping.ExcelIO.Formatting; using JdeScoping.ExcelIO.Models.Reporting;
using JdeScoping.ExcelIO.Models.Reporting; using Microsoft.Extensions.Options;
using Microsoft.Extensions.Options; using NPOI.SS.UserModel;
namespace JdeScoping.ExcelIO.Generators; namespace JdeScoping.ExcelIO.Generators;
/// <summary> /// <summary>
/// Generates the Search Criteria sheet for Excel export. /// Generates the Search Criteria sheet for Excel export.
/// </summary> /// </summary>
public class CriteriaSheetGenerator public class CriteriaSheetGenerator
{ {
private readonly IOptions<ExcelExportOptions> _options; private const int ExcelMaxColumnWidth = 255 * 256;
private readonly FluentTableWriter _tableWriter;
private readonly IOptions<ExcelExportOptions> _options;
/// <summary> private readonly FluentTableWriter _tableWriter;
/// Initializes a new instance of the CriteriaSheetGenerator class.
/// </summary> /// <summary>
/// <param name="options">Excel export options.</param> /// Initializes a new instance of the CriteriaSheetGenerator class.
/// <param name="tableWriter">Fluent table writer.</param> /// </summary>
public CriteriaSheetGenerator( /// <param name="options">Excel export options.</param>
IOptions<ExcelExportOptions> options, /// <param name="tableWriter">Fluent table writer.</param>
FluentTableWriter tableWriter) public CriteriaSheetGenerator(
{ IOptions<ExcelExportOptions> options,
_options = options; FluentTableWriter tableWriter)
_tableWriter = tableWriter; {
} _options = options;
_tableWriter = tableWriter;
/// <summary> }
/// Generates the Search Criteria sheet.
/// </summary> /// <summary>
/// <param name="workbook">The workbook to add the sheet to.</param> /// Generates the Search Criteria sheet.
/// <param name="search">The search model with criteria.</param> /// </summary>
public void Generate(XLWorkbook workbook, SearchModel search) /// <param name="workbook">The workbook to add the sheet to.</param>
{ /// <param name="search">The search model with criteria.</param>
var worksheet = workbook.Worksheets.Add("Search Criteria"); public void Generate(IWorkbook workbook, SearchModel search)
var row = 1; {
var worksheet = workbook.CreateSheet("Search Criteria");
// Write name and user var row = 1;
HeaderFormatter.ApplyHeaderFormat(worksheet.Cell(row, 1), "Search Name");
worksheet.Cell(row, 2).Value = search.Name; HeaderFormatter.ApplyHeaderFormat(GetOrCreateCell(worksheet, row, 1), "Search Name");
GetOrCreateCell(worksheet, row, 2).SetCellValue(search.Name);
HeaderFormatter.ApplyHeaderFormat(worksheet.Cell(++row, 1), "User Name");
worksheet.Cell(row, 2).Value = search.UserName; HeaderFormatter.ApplyHeaderFormat(GetOrCreateCell(worksheet, ++row, 1), "User Name");
GetOrCreateCell(worksheet, row, 2).SetCellValue(search.UserName);
// Skip row
row++; row++;
// Write timestamps HeaderFormatter.ApplyHeaderFormat(GetOrCreateCell(worksheet, ++row, 1), "Submit timestamp");
HeaderFormatter.ApplyHeaderFormat(worksheet.Cell(++row, 1), "Submit timestamp"); GetOrCreateCell(worksheet, row, 2).SetCellValue(FormatTimestamp(search.SubmitDt));
worksheet.Cell(row, 2).Value = FormatTimestamp(search.SubmitDt);
HeaderFormatter.ApplyHeaderFormat(GetOrCreateCell(worksheet, ++row, 1), "Start timestamp");
HeaderFormatter.ApplyHeaderFormat(worksheet.Cell(++row, 1), "Start timestamp"); GetOrCreateCell(worksheet, row, 2).SetCellValue(FormatTimestamp(search.StartDt));
worksheet.Cell(row, 2).Value = FormatTimestamp(search.StartDt);
HeaderFormatter.ApplyHeaderFormat(GetOrCreateCell(worksheet, ++row, 1), "Completed timestamp");
HeaderFormatter.ApplyHeaderFormat(worksheet.Cell(++row, 1), "Completed timestamp"); GetOrCreateCell(worksheet, row, 2).SetCellValue(FormatTimestamp(search.EndDt));
worksheet.Cell(row, 2).Value = FormatTimestamp(search.EndDt);
row++;
// Skip row
row++; var timespanData = new List<TimespanFilter>
{
// Write timespan filter table new() { MinimumDt = search.MinimumDt, MaximumDt = search.MaximumDt }
var timespanData = new List<TimespanFilter> };
{
new() { MinimumDt = search.MinimumDt, MaximumDt = search.MaximumDt } var timespanTable = _tableWriter.WriteTable(worksheet, ++row, 1, timespanData);
}; row = GetNextTableStartRow(timespanTable, row);
var timespanTable = _tableWriter.WriteTable(worksheet, ++row, 1, timespanData);
if (timespanTable != null) var workOrderTable = _tableWriter.WriteTable(worksheet, row, 1, search.WorkOrderFilter);
{ row = GetNextTableStartRow(workOrderTable, row);
row = timespanTable.RangeAddress.LastAddress.RowNumber + 3;
} var itemNumberTable = _tableWriter.WriteTable(worksheet, row, 1, search.ItemNumberFilter);
else row = GetNextTableStartRow(itemNumberTable, row);
{
row += 4; var profitCenterTable = _tableWriter.WriteTable(worksheet, row, 1, search.ProfitCenterFilter);
} row = GetNextTableStartRow(profitCenterTable, row);
// Write work order filter table var workCenterTable = _tableWriter.WriteTable(worksheet, row, 1, search.WorkCenterFilter);
var workOrderTable = _tableWriter.WriteTable(worksheet, row, 1, search.WorkOrderFilter); row = GetNextTableStartRow(workCenterTable, row);
if (workOrderTable != null)
{ var componentLotTable = _tableWriter.WriteTable(worksheet, row, 1, search.ComponentLotFilter);
row = workOrderTable.RangeAddress.LastAddress.RowNumber + 3; row = GetNextTableStartRow(componentLotTable, row);
}
else var operatorTable = _tableWriter.WriteTable(worksheet, row, 1, search.OperatorFilter);
{ row = GetNextTableStartRow(operatorTable, row);
row += 4;
} var itemOpMisTable = _tableWriter.WriteTable(worksheet, row, 1, search.ItemOperationMisFilter);
row = GetNextTableStartRow(itemOpMisTable, row);
// Write item number filter table
var itemNumberTable = _tableWriter.WriteTable(worksheet, row, 1, search.ItemNumberFilter); HeaderFormatter.ApplyHeaderFormat(worksheet, row, 1, row, 2, "Extract MIS data?", merge: true);
if (itemNumberTable != null) GetOrCreateCell(worksheet, ++row, 1).SetCellValue(search.ExtractMisData ? "YES" : "NO");
{
row = itemNumberTable.RangeAddress.LastAddress.RowNumber + 3; for (var column = 1; column <= 4; column++)
} {
else worksheet.AutoSizeColumn(column - 1);
{ var padded = (int)(worksheet.GetColumnWidth(column - 1) * ExcelFormats.CriteriaPaddingFactor);
row += 4; worksheet.SetColumnWidth(column - 1, padded > ExcelMaxColumnWidth ? ExcelMaxColumnWidth : padded);
} }
// Write profit center filter table WorksheetProtector.ApplyCriteriaProtection(worksheet, _options.Value.CriteriaSheetPassword);
var profitCenterTable = _tableWriter.WriteTable(worksheet, row, 1, search.ProfitCenterFilter); }
if (profitCenterTable != null)
{ private static int GetNextTableStartRow(TableWriteResult? result, int fallbackRow)
row = profitCenterTable.RangeAddress.LastAddress.RowNumber + 3; {
} return result != null ? result.LastRow + 3 : fallbackRow + 4;
else }
{
row += 4; private static ICell GetOrCreateCell(ISheet sheet, int rowNumber1Based, int colNumber1Based)
} {
var row = sheet.GetRow(rowNumber1Based - 1) ?? sheet.CreateRow(rowNumber1Based - 1);
// Write work center filter table return row.GetCell(colNumber1Based - 1) ?? row.CreateCell(colNumber1Based - 1);
var workCenterTable = _tableWriter.WriteTable(worksheet, row, 1, search.WorkCenterFilter); }
if (workCenterTable != null)
{ private string FormatTimestamp(DateTime? dateTime)
row = workCenterTable.RangeAddress.LastAddress.RowNumber + 3; {
} if (!dateTime.HasValue)
else {
{ return string.Empty;
row += 4; }
}
var options = _options.Value;
// Write component lot filter table var targetTimezone = TimeZoneInfo.FindSystemTimeZoneById(options.TimezoneId);
var componentLotTable = _tableWriter.WriteTable(worksheet, row, 1, search.ComponentLotFilter); var dt = dateTime.Value;
if (componentLotTable != null)
{ var localTime = dt.Kind switch
row = componentLotTable.RangeAddress.LastAddress.RowNumber + 3; {
} DateTimeKind.Utc => TimeZoneInfo.ConvertTimeFromUtc(dt, targetTimezone),
else DateTimeKind.Local => TimeZoneInfo.ConvertTime(dt, TimeZoneInfo.Local, targetTimezone),
{ _ => dt
row += 4; };
}
return $"{localTime:MMM dd, yyyy hh:mm:ss tt} {options.TimezoneAbbreviation}";
// Write operator filter table }
var operatorTable = _tableWriter.WriteTable(worksheet, row, 1, search.OperatorFilter); }
if (operatorTable != null)
{
row = operatorTable.RangeAddress.LastAddress.RowNumber + 3;
}
else
{
row += 4;
}
// Write item/operation/MIS filter table
var itemOpMisTable = _tableWriter.WriteTable(worksheet, row, 1, search.ItemOperationMisFilter);
if (itemOpMisTable != null)
{
row = itemOpMisTable.RangeAddress.LastAddress.RowNumber + 3;
}
else
{
row += 4;
}
// Write extract MIS data option
var headerRange = worksheet.Range(row, 1, row, 2);
HeaderFormatter.ApplyHeaderFormat(headerRange, "Extract MIS data?", merge: true);
worksheet.Cell(++row, 1).Value = search.ExtractMisData ? "YES" : "NO";
// Auto-fit columns with 15% padding
for (var column = 1; column <= 4; column++)
{
worksheet.Column(column).AdjustToContents();
worksheet.Column(column).Width *= ExcelFormats.CriteriaPaddingFactor;
}
// Apply protection
WorksheetProtector.ApplyCriteriaProtection(worksheet, _options.Value.CriteriaSheetPassword);
}
private string FormatTimestamp(DateTime? dateTime)
{
if (!dateTime.HasValue)
{
return string.Empty;
}
var options = _options.Value;
var targetTimezone = TimeZoneInfo.FindSystemTimeZoneById(options.TimezoneId);
var dt = dateTime.Value;
// Convert to target timezone based on the source DateTime's Kind
var localTime = dt.Kind switch
{
DateTimeKind.Utc => TimeZoneInfo.ConvertTimeFromUtc(dt, targetTimezone),
DateTimeKind.Local => TimeZoneInfo.ConvertTime(dt, TimeZoneInfo.Local, targetTimezone),
// Unspecified - database values are stored in target timezone, no conversion needed
_ => dt
};
return $"{localTime:MMM dd, yyyy hh:mm:ss tt} {options.TimezoneAbbreviation}";
}
}
@@ -1,82 +1,92 @@
using ClosedXML.Excel; using JdeScoping.ExcelIO.Formatting;
using JdeScoping.ExcelIO.Formatting; using JdeScoping.ExcelIO.Utilities;
using JdeScoping.ExcelIO.Utilities; using NPOI.SS.UserModel;
using NPOI.XSSF.UserModel;
namespace JdeScoping.ExcelIO.Generators;
namespace JdeScoping.ExcelIO.Generators;
/// <summary>
/// Generates data entry templates for bulk upload. /// <summary>
/// </summary> /// Generates data entry templates for bulk upload.
public class DataEntryTemplateGenerator /// </summary>
{ public class DataEntryTemplateGenerator
/// <summary> {
/// Generates a single-column data entry template. /// <summary>
/// </summary> /// Generates a single-column data entry template.
/// <typeparam name="T">The type of data items.</typeparam> /// </summary>
/// <param name="sourceData">Optional source data to pre-populate.</param> /// <typeparam name="T">The type of data items.</typeparam>
/// <param name="headerText">Header text for the column.</param> /// <param name="sourceData">Optional source data to pre-populate.</param>
/// <returns>The Excel file as a byte array.</returns> /// <param name="headerText">Header text for the column.</param>
public byte[] Generate<T>(IEnumerable<T>? sourceData, string headerText) /// <returns>The Excel file as a byte array.</returns>
{ public byte[] Generate<T>(IEnumerable<T>? sourceData, string headerText)
using var workbook = new XLWorkbook(); {
var worksheet = workbook.Worksheets.Add("Data Entry Template"); using var workbook = new XSSFWorkbook();
var worksheet = workbook.CreateSheet("Data Entry Template");
// Header var style = workbook.CreateCellStyle();
var headerCell = worksheet.Cell(1, 1); style.DataFormat = workbook.CreateDataFormat().GetFormat(ExcelFormats.StdFormat);
HeaderFormatter.ApplyHeaderFormat(headerCell, headerText); worksheet.SetDefaultColumnStyle(0, style);
worksheet.Column(1).Width = 45;
var headerCell = GetOrCreateCell(worksheet, 1, 1);
// Data (if provided) HeaderFormatter.ApplyHeaderFormat(headerCell, headerText);
if (sourceData != null) worksheet.SetColumnWidth(0, 45 * 256);
{
var row = 2; if (sourceData != null)
foreach (var item in sourceData) {
{ var row = 2;
worksheet.Cell(row++, 1).Value = CellValueConverter.ConvertToXlValue(item); foreach (var item in sourceData)
} {
} var cell = GetOrCreateCell(worksheet, row++, 1);
CellValueConverter.SetCellValue(cell, item);
// All cells as text }
worksheet.Column(1).Style.NumberFormat.Format = ExcelFormats.StdFormat; }
using var stream = new MemoryStream(); using var stream = new MemoryStream();
workbook.SaveAs(stream); workbook.Write(stream, leaveOpen: true);
return stream.ToArray(); return stream.ToArray();
} }
/// <summary> /// <summary>
/// Generates a multi-column data entry template. /// Generates a multi-column data entry template.
/// </summary> /// </summary>
/// <param name="sourceData">Optional source data to pre-populate (array of rows, each row is array of values).</param> /// <param name="sourceData">Optional source data to pre-populate (array of rows, each row is array of values).</param>
/// <param name="headers">Header texts for each column.</param> /// <param name="headers">Header texts for each column.</param>
/// <returns>The Excel file as a byte array.</returns> /// <returns>The Excel file as a byte array.</returns>
public byte[] Generate(object[][]? sourceData, string[] headers) public byte[] Generate(object[][]? sourceData, string[] headers)
{ {
using var workbook = new XLWorkbook(); using var workbook = new XSSFWorkbook();
var worksheet = workbook.Worksheets.Add("Data Entry Template"); var worksheet = workbook.CreateSheet("Data Entry Template");
// Headers var dataFormat = workbook.CreateDataFormat();
for (var col = 0; col < headers.Length; col++)
{ for (var col = 0; col < headers.Length; col++)
HeaderFormatter.ApplyHeaderFormat(worksheet.Cell(1, col + 1), headers[col]); {
worksheet.Column(col + 1).Width = 65; var style = workbook.CreateCellStyle();
worksheet.Column(col + 1).Style.NumberFormat.Format = ExcelFormats.StdFormat; style.DataFormat = dataFormat.GetFormat(ExcelFormats.StdFormat);
} worksheet.SetDefaultColumnStyle(col, style);
// Data HeaderFormatter.ApplyHeaderFormat(GetOrCreateCell(worksheet, 1, col + 1), headers[col]);
if (sourceData != null) worksheet.SetColumnWidth(col, 65 * 256);
{ }
for (var row = 0; row < sourceData.Length; row++)
{ if (sourceData != null)
for (var col = 0; col < sourceData[row].Length; col++) {
{ for (var row = 0; row < sourceData.Length; row++)
worksheet.Cell(row + 2, col + 1).Value = CellValueConverter.ConvertToXlValue(sourceData[row][col]); {
} for (var col = 0; col < sourceData[row].Length; col++)
} {
} var cell = GetOrCreateCell(worksheet, row + 2, col + 1);
CellValueConverter.SetCellValue(cell, sourceData[row][col]);
using var stream = new MemoryStream(); }
workbook.SaveAs(stream); }
return stream.ToArray(); }
}
} using var stream = new MemoryStream();
workbook.Write(stream, leaveOpen: true);
return stream.ToArray();
}
private static ICell GetOrCreateCell(ISheet sheet, int rowNumber1Based, int colNumber1Based)
{
var row = sheet.GetRow(rowNumber1Based - 1) ?? sheet.CreateRow(rowNumber1Based - 1);
return row.GetCell(colNumber1Based - 1) ?? row.CreateCell(colNumber1Based - 1);
}
}
@@ -1,142 +1,248 @@
using ClosedXML.Excel; using JdeScoping.ExcelIO.Formatting;
using JdeScoping.ExcelIO.Formatting; using JdeScoping.ExcelIO.Mapping;
using JdeScoping.ExcelIO.Mapping; using JdeScoping.ExcelIO.Utilities;
using JdeScoping.ExcelIO.Utilities; using NPOI.SS;
using NPOI.SS.UserModel;
namespace JdeScoping.ExcelIO.Generators; using NPOI.SS.Util;
using NPOI.XSSF.UserModel;
/// <summary>
/// Writes Excel tables using fluent mapping configuration. namespace JdeScoping.ExcelIO.Generators;
/// </summary>
public sealed class FluentTableWriter /// <summary>
{ /// Result metadata for a written table region.
private readonly ExcelMapRegistry _registry; /// </summary>
public sealed class TableWriteResult
/// <summary> {
/// Initializes a new instance of the <see cref="FluentTableWriter"/> class. public int FirstRow { get; init; }
/// </summary> public int LastRow { get; init; }
/// <param name="registry">The registry containing Excel column maps for types.</param> public int FirstCol { get; init; }
public FluentTableWriter(ExcelMapRegistry registry) public int LastCol { get; init; }
{ public XSSFTable? Table { get; init; }
_registry = registry; }
}
/// <summary>
/// <summary> /// Writes Excel tables using fluent mapping configuration.
/// Writes a table to the worksheet using the registered map for type T. /// </summary>
/// </summary> public sealed class FluentTableWriter
/// <typeparam name="T">The type of objects to write to the table.</typeparam> {
/// <param name="worksheet">The Excel worksheet to write to.</param> private const int ExcelMaxColumnWidth = 255 * 256;
/// <param name="startRow">The starting row number (1-based).</param>
/// <param name="startCol">The starting column number (1-based).</param> private readonly ExcelMapRegistry _registry;
/// <param name="data">The data to write to the table.</param>
/// <param name="tableNameOverride">Optional override for the table name; uses map name if null.</param> /// <summary>
/// <param name="showHeader">Whether to show a merged header above the table.</param> /// Initializes a new instance of the <see cref="FluentTableWriter"/> class.
/// <param name="headerText">Optional text to display in the merged header.</param> /// </summary>
/// <returns>The created Excel table, or null if no columns are configured.</returns> /// <param name="registry">The registry containing Excel column maps for types.</param>
public IXLTable? WriteTable<T>( public FluentTableWriter(ExcelMapRegistry registry)
IXLWorksheet worksheet, {
int startRow, _registry = registry;
int startCol, }
IEnumerable<T> data,
string? tableNameOverride = null, /// <summary>
bool showHeader = false, /// Writes a table to the worksheet using the registered map for type T.
string? headerText = null) /// </summary>
{ /// <typeparam name="T">The type of objects to write to the table.</typeparam>
var map = _registry.GetMap<T>(); /// <param name="worksheet">The Excel worksheet to write to.</param>
var columns = map.Columns; /// <param name="startRow">The starting row number (1-based).</param>
var tableName = tableNameOverride ?? map.TableName ?? typeof(T).Name; /// <param name="startCol">The starting column number (1-based).</param>
var header = headerText ?? map.TabName ?? string.Empty; /// <param name="data">The data to write to the table.</param>
/// <param name="tableNameOverride">Optional override for the table name; uses map name if null.</param>
if (columns.Count == 0) /// <param name="showHeader">Whether to show a merged header above the table.</param>
return null; /// <param name="headerText">Optional text to display in the merged header.</param>
/// <returns>The created table metadata, or null if no columns are configured.</returns>
var dataList = data.ToList(); public TableWriteResult? WriteTable<T>(
var baseRow = startRow; ISheet worksheet,
int startRow,
// Write merged header if requested int startCol,
if (showHeader && !string.IsNullOrEmpty(header)) IEnumerable<T> data,
{ string? tableNameOverride = null,
var mergedHeaderRange = worksheet.Range(baseRow, startCol, baseRow, startCol + columns.Count - 1); bool showHeader = false,
HeaderFormatter.ApplyHeaderFormat(mergedHeaderRange, header, merge: true); string? headerText = null)
baseRow++; {
} var map = _registry.GetMap<T>();
var columns = map.Columns;
// Write column headers var tableName = tableNameOverride ?? map.TableName ?? typeof(T).Name;
var col = startCol; var header = headerText ?? map.TabName ?? string.Empty;
foreach (var column in columns)
{ if (columns.Count == 0)
var cell = worksheet.Cell(baseRow, col); {
HeaderFormatter.ApplyHeaderFormat(cell, column.HeaderText); return null;
}
// Pre-set column formatting
worksheet.Column(col).Style.Alignment.WrapText = column.WrapText; var dataList = data.ToList();
if (!column.AutoWidth) var baseRow = startRow;
{
worksheet.Column(col).Width = column.Width; if (showHeader && !string.IsNullOrEmpty(header))
} {
HeaderFormatter.ApplyHeaderFormat(
col++; worksheet,
} baseRow,
startCol,
// Write data rows baseRow,
var row = baseRow + 1; startCol + columns.Count - 1,
foreach (var item in dataList) header,
{ merge: true);
col = startCol; baseRow++;
foreach (var column in columns) }
{
var value = column.ValueGetter(item!); var dataFormat = worksheet.Workbook.CreateDataFormat();
worksheet.Cell(row, col).Value = CellValueConverter.ConvertToXlValue(value); var columnStyles = new ICellStyle[columns.Count];
col++;
} // Write column headers and set column defaults.
row++; for (var i = 0; i < columns.Count; i++)
} {
var col = startCol + i;
// Handle empty data case var columnStyle = worksheet.Workbook.CreateCellStyle();
if (dataList.Count == 0) if (!string.IsNullOrEmpty(columns[i].Format))
{ {
row = baseRow + 1; columnStyle.DataFormat = dataFormat.GetFormat(columns[i].Format);
} }
// Create table range columnStyle.WrapText = columns[i].WrapText;
var dataRange = worksheet.Range( columnStyles[i] = columnStyle;
baseRow, startCol,
baseRow + dataList.Count, startCol + columns.Count - 1); var headerRow = GetOrCreateRow(worksheet, baseRow);
var headerCell = GetOrCreateCell(headerRow, col);
// Create table HeaderFormatter.ApplyHeaderFormat(headerCell, columns[i].HeaderText);
var table = dataRange.CreateTable(tableName); }
table.Theme = XLTableTheme.TableStyleLight18;
table.ShowTotalsRow = false; // Write data rows.
var row = baseRow + 1;
// Apply column formatting foreach (var item in dataList)
col = startCol; {
var tableStartRow = table.RangeAddress.FirstAddress.RowNumber; var npoiRow = GetOrCreateRow(worksheet, row);
var tableEndRow = table.RangeAddress.LastAddress.RowNumber;
for (var i = 0; i < columns.Count; i++)
foreach (var column in columns) {
{ var col = startCol + i;
// Apply number format var column = columns[i];
worksheet.Range(tableStartRow, col, tableEndRow, col) var value = column.ValueGetter(item!);
.Style.NumberFormat.Format = column.Format; var cell = GetOrCreateCell(npoiRow, col);
CellValueConverter.SetCellValue(cell, value);
// Apply column width cell.CellStyle = columnStyles[i];
if (column.WrapText && !column.AutoWidth) }
{
worksheet.Column(col).Width = column.Width; row++;
} }
else if (column.AutoWidth)
{ if (dataList.Count == 0)
worksheet.Column(col).AdjustToContents(); {
worksheet.Column(col).Width *= ExcelFormats.DataPaddingFactor; row = baseRow + 1;
} }
else
{ var firstRow = baseRow;
worksheet.Column(col).Width = column.Width; var lastRow = baseRow + dataList.Count;
} var firstCol = startCol;
var lastCol = startCol + columns.Count - 1;
col++;
} var table = CreateTableIfSupported(worksheet, tableName, firstRow, lastRow, firstCol, lastCol);
return table; // Preserve filter behavior.
} worksheet.SetAutoFilter(new CellRangeAddress(firstRow - 1, lastRow - 1, firstCol - 1, lastCol - 1));
}
// Apply column widths.
for (var i = 0; i < columns.Count; i++)
{
var column = columns[i];
var colIndex = startCol + i - 1;
if (column.WrapText && !column.AutoWidth)
{
worksheet.SetColumnWidth(colIndex, ToNpoiWidth(column.Width));
}
else if (column.AutoWidth)
{
worksheet.AutoSizeColumn(colIndex);
var width = worksheet.GetColumnWidth(colIndex);
var padded = (int)(width * ExcelFormats.DataPaddingFactor);
worksheet.SetColumnWidth(colIndex, ClampWidth(padded));
}
else
{
worksheet.SetColumnWidth(colIndex, ToNpoiWidth(column.Width));
}
}
return new TableWriteResult
{
FirstRow = firstRow,
LastRow = lastRow,
FirstCol = firstCol,
LastCol = lastCol,
Table = table
};
}
private static IRow GetOrCreateRow(ISheet sheet, int rowNumber1Based)
{
return sheet.GetRow(rowNumber1Based - 1) ?? sheet.CreateRow(rowNumber1Based - 1);
}
private static ICell GetOrCreateCell(IRow row, int colNumber1Based)
{
return row.GetCell(colNumber1Based - 1) ?? row.CreateCell(colNumber1Based - 1);
}
private static XSSFTable? CreateTableIfSupported(
ISheet worksheet,
string tableName,
int firstRow,
int lastRow,
int firstCol,
int lastCol)
{
if (worksheet is not XSSFSheet xssfSheet)
{
return null;
}
var safeName = ToSafeTableName(tableName);
var area = new AreaReference(
new CellReference(firstRow - 1, firstCol - 1),
new CellReference(lastRow - 1, lastCol - 1),
SpreadsheetVersion.EXCEL2007);
var table = xssfSheet.CreateTable();
table.Name = safeName;
table.DisplayName = safeName;
table.StyleName = "TableStyleLight18";
table.CellReferences = area;
table.UpdateReferences();
table.UpdateHeaders();
return table;
}
private static string ToSafeTableName(string tableName)
{
var chars = tableName.Where(ch => char.IsLetterOrDigit(ch) || ch == '_').ToArray();
var value = new string(chars);
if (string.IsNullOrWhiteSpace(value))
{
return "Table1";
}
if (!char.IsLetter(value[0]) && value[0] != '_')
{
value = $"_{value}";
}
return value;
}
private static int ToNpoiWidth(double widthInChars)
{
return ClampWidth((int)(widthInChars * 256));
}
private static int ClampWidth(int width)
{
if (width < 0)
{
return 0;
}
return width > ExcelMaxColumnWidth ? ExcelMaxColumnWidth : width;
}
}
@@ -6,10 +6,10 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ClosedXML" Version="0.105.0" /> <PackageReference Include="NPOI" Version="2.7.5" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.1" /> <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.1" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.1" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.1" /> <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.1" />
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.1" /> <PackageReference Include="Microsoft.Extensions.Options" Version="10.0.1" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="10.0.1" /> <PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="10.0.1" />
@@ -1,136 +1,166 @@
using ClosedXML.Excel; using JdeScoping.Core.Interfaces;
using JdeScoping.Core.Interfaces; using JdeScoping.Core.ViewModels;
using JdeScoping.Core.ViewModels; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging; using NPOI.SS.UserModel;
namespace JdeScoping.ExcelIO.Parsing; namespace JdeScoping.ExcelIO.Parsing;
/// <summary> /// <summary>
/// Service for parsing Excel files uploaded by users. /// Service for parsing Excel files uploaded by users.
/// </summary> /// </summary>
public class ExcelParserService : IExcelParserService public class ExcelParserService : IExcelParserService
{ {
private readonly ILogger<ExcelParserService> _logger; private readonly ILogger<ExcelParserService> _logger;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ExcelParserService"/> class. /// Initializes a new instance of the <see cref="ExcelParserService"/> class.
/// </summary> /// </summary>
/// <param name="logger">The logger instance.</param> /// <param name="logger">The logger instance.</param>
public ExcelParserService(ILogger<ExcelParserService> logger) public ExcelParserService(ILogger<ExcelParserService> logger)
{ {
_logger = logger; _logger = logger;
} }
/// <inheritdoc /> /// <inheritdoc />
public List<long> ParseWorkOrders(Stream fileStream) public List<long> ParseWorkOrders(Stream fileStream)
{ {
using var workbook = new XLWorkbook(fileStream); using var workbook = WorkbookFactory.Create(ResetStream(fileStream));
var worksheet = workbook.Worksheet(1); var worksheet = workbook.GetSheetAt(0);
var formatter = new DataFormatter();
var workOrderNumbers = new List<long>();
var lastRow = worksheet.LastRowUsed()?.RowNumber() ?? 1; var workOrderNumbers = new List<long>();
var lastRow = worksheet.LastRowNum + 1;
for (var row = 2; row <= lastRow; row++)
{ for (var row = 2; row <= lastRow; row++)
var cellValue = worksheet.Cell(row, 1).GetString()?.Trim(); {
if (long.TryParse(cellValue, out var woNumber)) var cellValue = GetCellText(worksheet, row, 1, formatter);
{ if (long.TryParse(cellValue, out var woNumber))
workOrderNumbers.Add(woNumber); {
} workOrderNumbers.Add(woNumber);
} }
}
return workOrderNumbers;
} return workOrderNumbers;
}
/// <inheritdoc />
public List<string> ParseItems(Stream fileStream) /// <inheritdoc />
{ public List<string> ParseItems(Stream fileStream)
using var workbook = new XLWorkbook(fileStream); {
var worksheet = workbook.Worksheet(1); using var workbook = WorkbookFactory.Create(ResetStream(fileStream));
var worksheet = workbook.GetSheetAt(0);
var itemNumbers = new List<string>(); var formatter = new DataFormatter();
var lastRow = worksheet.LastRowUsed()?.RowNumber() ?? 1;
var itemNumbers = new List<string>();
for (var row = 2; row <= lastRow; row++) var lastRow = worksheet.LastRowNum + 1;
{
var cellValue = worksheet.Cell(row, 1).GetString()?.Trim(); for (var row = 2; row <= lastRow; row++)
if (!string.IsNullOrEmpty(cellValue)) {
{ var cellValue = GetCellText(worksheet, row, 1, formatter);
itemNumbers.Add(cellValue); if (!string.IsNullOrEmpty(cellValue))
} {
} itemNumbers.Add(cellValue);
}
return itemNumbers; }
}
return itemNumbers;
/// <inheritdoc /> }
public List<LotViewModel> ParseComponentLots(Stream fileStream)
{ /// <inheritdoc />
using var workbook = new XLWorkbook(fileStream); public List<LotViewModel> ParseComponentLots(Stream fileStream)
var worksheet = workbook.Worksheet(1); {
using var workbook = WorkbookFactory.Create(ResetStream(fileStream));
var lotViewModels = new List<LotViewModel>(); var worksheet = workbook.GetSheetAt(0);
var lastRow = worksheet.LastRowUsed()?.RowNumber() ?? 1; var formatter = new DataFormatter();
for (var row = 2; row <= lastRow; row++) var lotViewModels = new List<LotViewModel>();
{ var lastRow = worksheet.LastRowNum + 1;
var lotNumber = worksheet.Cell(row, 1).GetString()?.Trim() ?? string.Empty;
var itemNumber = worksheet.Cell(row, 2).GetString()?.Trim() ?? string.Empty; for (var row = 2; row <= lastRow; row++)
{
if (!string.IsNullOrEmpty(lotNumber)) var lotNumber = GetCellText(worksheet, row, 1, formatter);
{ var itemNumber = GetCellText(worksheet, row, 2, formatter);
lotViewModels.Add(new LotViewModel
{ if (!string.IsNullOrEmpty(lotNumber))
LotNumber = lotNumber, {
ItemNumber = itemNumber lotViewModels.Add(new LotViewModel
}); {
} LotNumber = lotNumber,
} ItemNumber = itemNumber
});
return lotViewModels; }
} }
/// <inheritdoc /> return lotViewModels;
public List<PartOperationViewModel> ParsePartOperations(Stream fileStream) }
{
using var workbook = new XLWorkbook(fileStream); /// <inheritdoc />
var worksheet = workbook.Worksheet(1); public List<PartOperationViewModel> ParsePartOperations(Stream fileStream)
{
var partOperations = new List<PartOperationViewModel>(); using var workbook = WorkbookFactory.Create(ResetStream(fileStream));
var lastRow = worksheet.LastRowUsed()?.RowNumber() ?? 1; var worksheet = workbook.GetSheetAt(0);
var formatter = new DataFormatter();
for (var row = 2; row <= lastRow; row++)
{ var partOperations = new List<PartOperationViewModel>();
try var lastRow = worksheet.LastRowNum + 1;
{
var itemNumber = worksheet.Cell(row, 1).GetString()?.Trim() ?? string.Empty; for (var row = 2; row <= lastRow; row++)
var operationNumber = worksheet.Cell(row, 2).GetString()?.Trim() ?? string.Empty; {
var misNumber = worksheet.Cell(row, 3).GetString()?.Trim() ?? string.Empty; try
var misRevision = worksheet.Cell(row, 4).GetString()?.Trim() ?? string.Empty; {
var itemNumber = GetCellText(worksheet, row, 1, formatter);
// Remove decimal places from operation number var operationNumber = GetCellText(worksheet, row, 2, formatter);
if (!string.IsNullOrEmpty(operationNumber) && operationNumber.Contains('.')) var misNumber = GetCellText(worksheet, row, 3, formatter);
{ var misRevision = GetCellText(worksheet, row, 4, formatter);
operationNumber = operationNumber[..operationNumber.IndexOf('.')];
} if (!string.IsNullOrEmpty(operationNumber) && operationNumber.Contains('.'))
{
if (!string.IsNullOrEmpty(itemNumber) && !string.IsNullOrEmpty(operationNumber)) operationNumber = operationNumber[..operationNumber.IndexOf('.')];
{ }
partOperations.Add(new PartOperationViewModel
{ if (!string.IsNullOrEmpty(itemNumber) && !string.IsNullOrEmpty(operationNumber))
ItemNumber = itemNumber, {
OperationNumber = operationNumber, partOperations.Add(new PartOperationViewModel
MisNumber = misNumber, {
MisRevision = misRevision ItemNumber = itemNumber,
}); OperationNumber = operationNumber,
} MisNumber = misNumber,
} MisRevision = misRevision
catch (Exception ex) });
{ }
_logger.LogWarning(ex, "Failed to parse row {RowNumber} in part operations sheet", row); }
} catch (Exception ex)
} {
_logger.LogWarning(ex, "Failed to parse row {RowNumber} in part operations sheet", row);
return partOperations; }
} }
}
return partOperations;
}
private static string GetCellText(ISheet sheet, int rowNumber1Based, int colNumber1Based, DataFormatter formatter)
{
var row = sheet.GetRow(rowNumber1Based - 1);
if (row == null)
{
return string.Empty;
}
var cell = row.GetCell(colNumber1Based - 1);
if (cell == null)
{
return string.Empty;
}
return formatter.FormatCellValue(cell).Trim();
}
private static Stream ResetStream(Stream stream)
{
if (stream.CanSeek)
{
stream.Position = 0;
}
return stream;
}
}
@@ -1,70 +1,85 @@
using ClosedXML.Excel; using JdeScoping.Core.Interfaces;
using JdeScoping.Core.Interfaces; using NPOI.SS.UserModel;
using NPOI.XSSF.UserModel;
namespace JdeScoping.ExcelIO.Templates;
namespace JdeScoping.ExcelIO.Templates;
/// <summary>
/// Service for generating Excel template files for data entry. /// <summary>
/// </summary> /// Service for generating Excel template files for data entry.
public class ExcelTemplateService : IExcelTemplateService /// </summary>
{ public class ExcelTemplateService : IExcelTemplateService
/// <inheritdoc /> {
public byte[] GenerateSingleColumn<T>(IEnumerable<T> data, string headerText) /// <inheritdoc />
{ public byte[] GenerateSingleColumn<T>(IEnumerable<T> data, string headerText)
using var workbook = new XLWorkbook(); {
var worksheet = workbook.Worksheets.Add("Template"); using var workbook = new XSSFWorkbook();
var worksheet = workbook.CreateSheet("Template");
// Write header
worksheet.Cell(1, 1).Value = headerText; var headerCell = GetOrCreateCell(worksheet, 1, 1);
worksheet.Cell(1, 1).Style.Font.Bold = true; headerCell.SetCellValue(headerText);
// Write data var boldStyle = workbook.CreateCellStyle();
var row = 2; var boldFont = workbook.CreateFont();
foreach (var item in data) boldFont.IsBold = true;
{ boldStyle.SetFont(boldFont);
worksheet.Cell(row, 1).Value = item?.ToString() ?? string.Empty; headerCell.CellStyle = boldStyle;
row++;
} var row = 2;
foreach (var item in data)
// Auto-fit column width {
worksheet.Column(1).AdjustToContents(); GetOrCreateCell(worksheet, row++, 1).SetCellValue(item?.ToString() ?? string.Empty);
}
using var stream = new MemoryStream();
workbook.SaveAs(stream); worksheet.AutoSizeColumn(0);
return stream.ToArray();
} using var stream = new MemoryStream();
workbook.Write(stream, leaveOpen: true);
/// <inheritdoc /> return stream.ToArray();
public byte[] GenerateMultiColumn(object?[][] data, string[] headers) }
{
using var workbook = new XLWorkbook(); /// <inheritdoc />
var worksheet = workbook.Worksheets.Add("Template"); public byte[] GenerateMultiColumn(object?[][] data, string[] headers)
{
// Write headers using var workbook = new XSSFWorkbook();
for (var col = 0; col < headers.Length; col++) var worksheet = workbook.CreateSheet("Template");
{
worksheet.Cell(1, col + 1).Value = headers[col]; var boldStyle = workbook.CreateCellStyle();
worksheet.Cell(1, col + 1).Style.Font.Bold = true; var boldFont = workbook.CreateFont();
} boldFont.IsBold = true;
boldStyle.SetFont(boldFont);
// Write data
for (var row = 0; row < data.Length; row++) for (var col = 0; col < headers.Length; col++)
{ {
for (var col = 0; col < data[row].Length; col++) var headerCell = GetOrCreateCell(worksheet, 1, col + 1);
{ headerCell.SetCellValue(headers[col]);
var value = data[row][col]; headerCell.CellStyle = boldStyle;
if (value != null) }
{
worksheet.Cell(row + 2, col + 1).Value = value.ToString(); for (var row = 0; row < data.Length; row++)
} {
} for (var col = 0; col < data[row].Length; col++)
} {
var value = data[row][col];
// Auto-fit column widths if (value != null)
worksheet.Columns().AdjustToContents(); {
GetOrCreateCell(worksheet, row + 2, col + 1).SetCellValue(value.ToString());
using var stream = new MemoryStream(); }
workbook.SaveAs(stream); }
return stream.ToArray(); }
}
} for (var col = 0; col < headers.Length; col++)
{
worksheet.AutoSizeColumn(col);
}
using var stream = new MemoryStream();
workbook.Write(stream, leaveOpen: true);
return stream.ToArray();
}
private static ICell GetOrCreateCell(ISheet sheet, int rowNumber1Based, int colNumber1Based)
{
var row = sheet.GetRow(rowNumber1Based - 1) ?? sheet.CreateRow(rowNumber1Based - 1);
return row.GetCell(colNumber1Based - 1) ?? row.CreateCell(colNumber1Based - 1);
}
}
@@ -1,31 +1,51 @@
using ClosedXML.Excel; using NPOI.SS.UserModel;
namespace JdeScoping.ExcelIO.Utilities; namespace JdeScoping.ExcelIO.Utilities;
/// <summary> /// <summary>
/// Utility class for converting .NET objects to ClosedXML cell values. /// Utility class for converting .NET objects to NPOI cell values.
/// </summary> /// </summary>
public static class CellValueConverter public static class CellValueConverter
{ {
/// <summary> /// <summary>
/// Converts a .NET object to an XLCellValue for use in ClosedXML worksheets. /// Sets an NPOI cell value from a .NET object.
/// </summary> /// </summary>
/// <param name="value">The value to convert.</param> /// <param name="cell">The target cell.</param>
/// <returns>An XLCellValue suitable for setting as a cell value.</returns> /// <param name="value">The value to write.</param>
public static XLCellValue ConvertToXlValue(object? value) public static void SetCellValue(ICell cell, object? value)
{ {
return value switch switch (value)
{ {
null => Blank.Value, case null:
string s => s, cell.SetCellType(CellType.Blank);
int i => i, break;
long l => l, case string s:
decimal d => d, cell.SetCellValue(s);
double dbl => dbl, break;
float f => f, case int i:
DateTime dt => dt, cell.SetCellValue(i);
bool b => b, break;
_ => value.ToString() ?? string.Empty case long l:
}; cell.SetCellValue(l);
} break;
} case decimal d:
cell.SetCellValue((double)d);
break;
case double dbl:
cell.SetCellValue(dbl);
break;
case float f:
cell.SetCellValue(f);
break;
case DateTime dt:
cell.SetCellValue(dt);
break;
case bool b:
cell.SetCellValue(b);
break;
default:
cell.SetCellValue(value.ToString() ?? string.Empty);
break;
}
}
}
@@ -1,431 +1,427 @@
using ClosedXML.Excel; using JdeScoping.ExcelIO.Options;
using JdeScoping.ExcelIO.Options; using JdeScoping.ExcelIO.Generators;
using JdeScoping.ExcelIO.Generators; using JdeScoping.ExcelIO.Mapping;
using JdeScoping.ExcelIO.Mapping; using JdeScoping.ExcelIO.Mapping.Maps;
using JdeScoping.ExcelIO.Mapping.Maps; using JdeScoping.ExcelIO.Models.Reporting;
using JdeScoping.ExcelIO.Models.Reporting; using JdeScoping.ExcelIO.Tests.Fixtures;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Shouldly; using NPOI.XSSF.UserModel;
using Xunit; using Shouldly;
using Xunit;
namespace JdeScoping.ExcelIO.Tests;
namespace JdeScoping.ExcelIO.Tests;
public class CriteriaSheetGeneratorTests
{ public class CriteriaSheetGeneratorTests
private readonly CriteriaSheetGenerator _generator; {
private readonly IOptions<ExcelExportOptions> _options; private readonly CriteriaSheetGenerator _generator;
public CriteriaSheetGeneratorTests() public CriteriaSheetGeneratorTests()
{ {
_options = Microsoft.Extensions.Options.Options.Create(new ExcelExportOptions var options = Microsoft.Extensions.Options.Options.Create(new ExcelExportOptions
{ {
CriteriaSheetPassword = "TestPassword" CriteriaSheetPassword = "TestPassword"
}); });
var registry = CreateTestRegistry(); var registry = CreateTestRegistry();
var tableWriter = new FluentTableWriter(registry); var tableWriter = new FluentTableWriter(registry);
_generator = new CriteriaSheetGenerator(_options, tableWriter); _generator = new CriteriaSheetGenerator(options, tableWriter);
} }
private static ExcelMapRegistry CreateTestRegistry() private static ExcelMapRegistry CreateTestRegistry()
{ {
var registry = new ExcelMapRegistry(); var registry = new ExcelMapRegistry();
registry.Register(new TimespanFilterMap()); registry.Register(new TimespanFilterMap());
registry.Register(new WorkOrderFilterEntryMap()); registry.Register(new WorkOrderFilterEntryMap());
registry.Register(new ItemNumberFilterEntryMap()); registry.Register(new ItemNumberFilterEntryMap());
registry.Register(new ProfitCenterFilterEntryMap()); registry.Register(new ProfitCenterFilterEntryMap());
registry.Register(new WorkCenterFilterEntryMap()); registry.Register(new WorkCenterFilterEntryMap());
registry.Register(new OperatorFilterEntryMap()); registry.Register(new OperatorFilterEntryMap());
registry.Register(new ComponentLotFilterEntryMap()); registry.Register(new ComponentLotFilterEntryMap());
registry.Register(new ItemOperationMisFilterEntryMap()); registry.Register(new ItemOperationMisFilterEntryMap());
return registry; return registry;
} }
[Fact] [Fact]
public void Generate_CreatesSearchCriteriaSheet() public void Generate_CreatesSearchCriteriaSheet()
{ {
using var workbook = new XLWorkbook(); using var workbook = new XSSFWorkbook();
var search = CreateMinimalSearchModel(); var search = CreateMinimalSearchModel();
_generator.Generate(workbook, search); _generator.Generate(workbook, search);
workbook.Worksheets.TryGetWorksheet("Search Criteria", out var worksheet).ShouldBeTrue(); workbook.GetSheet("Search Criteria").ShouldNotBeNull();
worksheet.ShouldNotBeNull(); }
}
[Fact]
[Fact] public void Generate_ContainsSearchName()
public void Generate_ContainsSearchName() {
{ using var workbook = new XSSFWorkbook();
using var workbook = new XLWorkbook(); var search = CreateMinimalSearchModel();
var search = CreateMinimalSearchModel(); search.Name = "Test Search Name";
search.Name = "Test Search Name";
_generator.Generate(workbook, search);
_generator.Generate(workbook, search);
var worksheet = workbook.GetSheet("Search Criteria");
var worksheet = workbook.Worksheet("Search Criteria"); ExcelTestHelpers.GetCellText(worksheet, 1, 1).ShouldBe("Search Name");
worksheet.Cell(1, 1).Value.GetText().ShouldBe("Search Name"); ExcelTestHelpers.GetCellText(worksheet, 1, 2).ShouldBe("Test Search Name");
worksheet.Cell(1, 2).Value.GetText().ShouldBe("Test Search Name"); }
}
[Fact]
[Fact] public void Generate_ContainsUserName()
public void Generate_ContainsUserName() {
{ using var workbook = new XSSFWorkbook();
using var workbook = new XLWorkbook(); var search = CreateMinimalSearchModel();
var search = CreateMinimalSearchModel(); search.UserName = "testuser";
search.UserName = "testuser";
_generator.Generate(workbook, search);
_generator.Generate(workbook, search);
var worksheet = workbook.GetSheet("Search Criteria");
var worksheet = workbook.Worksheet("Search Criteria"); ExcelTestHelpers.GetCellText(worksheet, 2, 1).ShouldBe("User Name");
worksheet.Cell(2, 1).Value.GetText().ShouldBe("User Name"); ExcelTestHelpers.GetCellText(worksheet, 2, 2).ShouldBe("testuser");
worksheet.Cell(2, 2).Value.GetText().ShouldBe("testuser"); }
}
[Fact]
[Fact] public void Generate_ContainsTimestamps()
public void Generate_ContainsTimestamps() {
{ using var workbook = new XSSFWorkbook();
using var workbook = new XLWorkbook(); var search = CreateMinimalSearchModel();
var search = CreateMinimalSearchModel(); search.SubmitDt = new DateTime(2024, 1, 15, 10, 30, 0);
var submitDt = new DateTime(2024, 1, 15, 10, 30, 0); search.StartDt = new DateTime(2024, 1, 15, 10, 31, 0);
var startDt = new DateTime(2024, 1, 15, 10, 31, 0); search.EndDt = new DateTime(2024, 1, 15, 10, 35, 0);
var endDt = new DateTime(2024, 1, 15, 10, 35, 0);
search.SubmitDt = submitDt; _generator.Generate(workbook, search);
search.StartDt = startDt;
search.EndDt = endDt; var worksheet = workbook.GetSheet("Search Criteria");
ExcelTestHelpers.GetCellText(worksheet, 4, 1).ShouldBe("Submit timestamp");
_generator.Generate(workbook, search); ExcelTestHelpers.GetCellText(worksheet, 4, 2).ShouldContain("Jan 15, 2024");
ExcelTestHelpers.GetCellText(worksheet, 5, 1).ShouldBe("Start timestamp");
var worksheet = workbook.Worksheet("Search Criteria"); ExcelTestHelpers.GetCellText(worksheet, 5, 2).ShouldContain("Jan 15, 2024");
worksheet.Cell(4, 1).Value.GetText().ShouldBe("Submit timestamp"); ExcelTestHelpers.GetCellText(worksheet, 6, 1).ShouldBe("Completed timestamp");
worksheet.Cell(4, 2).Value.GetText().ShouldContain("Jan 15, 2024"); ExcelTestHelpers.GetCellText(worksheet, 6, 2).ShouldContain("Jan 15, 2024");
}
worksheet.Cell(5, 1).Value.GetText().ShouldBe("Start timestamp");
worksheet.Cell(5, 2).Value.GetText().ShouldContain("Jan 15, 2024"); [Fact]
public void Generate_ContainsTimespanFilterTable()
worksheet.Cell(6, 1).Value.GetText().ShouldBe("Completed timestamp"); {
worksheet.Cell(6, 2).Value.GetText().ShouldContain("Jan 15, 2024"); using var workbook = new XSSFWorkbook();
} var search = CreateMinimalSearchModel();
search.MinimumDt = new DateTime(2024, 1, 1);
[Fact] search.MaximumDt = new DateTime(2024, 12, 31);
public void Generate_ContainsTimespanFilterTable()
{ _generator.Generate(workbook, search);
using var workbook = new XLWorkbook();
var search = CreateMinimalSearchModel(); var worksheet = workbook.GetSheet("Search Criteria");
search.MinimumDt = new DateTime(2024, 1, 1); ExcelTestHelpers.TableExists(worksheet, "Timespan_Filter").ShouldBeTrue();
search.MaximumDt = new DateTime(2024, 12, 31); }
_generator.Generate(workbook, search); [Fact]
public void Generate_ContainsWorkOrderFilterTable()
var worksheet = workbook.Worksheet("Search Criteria"); {
var tables = worksheet.Tables; using var workbook = new XSSFWorkbook();
tables.Any(t => t.Name == "Timespan_Filter").ShouldBeTrue(); var search = CreateMinimalSearchModel();
} search.WorkOrderFilter.Add(new WorkOrderFilterEntry { WorkOrderNumber = 12345, ItemNumber = "ITEM-001" });
[Fact] _generator.Generate(workbook, search);
public void Generate_ContainsWorkOrderFilterTable()
{ var worksheet = workbook.GetSheet("Search Criteria");
using var workbook = new XLWorkbook(); ExcelTestHelpers.TableExists(worksheet, "Work_Order_Filter").ShouldBeTrue();
var search = CreateMinimalSearchModel(); }
search.WorkOrderFilter.Add(new WorkOrderFilterEntry { WorkOrderNumber = 12345, ItemNumber = "ITEM-001" });
[Fact]
_generator.Generate(workbook, search); public void Generate_ContainsItemNumberFilterTable()
{
var worksheet = workbook.Worksheet("Search Criteria"); using var workbook = new XSSFWorkbook();
var tables = worksheet.Tables; var search = CreateMinimalSearchModel();
tables.Any(t => t.Name == "Work_Order_Filter").ShouldBeTrue(); search.ItemNumberFilter.Add(new ItemNumberFilterEntry { ItemNumber = "ITEM-001" });
}
_generator.Generate(workbook, search);
[Fact]
public void Generate_ContainsItemNumberFilterTable() var worksheet = workbook.GetSheet("Search Criteria");
{ ExcelTestHelpers.TableExists(worksheet, "Item_Number_Filter").ShouldBeTrue();
using var workbook = new XLWorkbook(); }
var search = CreateMinimalSearchModel();
search.ItemNumberFilter.Add(new ItemNumberFilterEntry { ItemNumber = "ITEM-001" }); [Fact]
public void Generate_ContainsProfitCenterFilterTable()
_generator.Generate(workbook, search); {
using var workbook = new XSSFWorkbook();
var worksheet = workbook.Worksheet("Search Criteria"); var search = CreateMinimalSearchModel();
var tables = worksheet.Tables; search.ProfitCenterFilter.Add(new ProfitCenterFilterEntry { Code = "PC01", Description = "Profit Center 1" });
tables.Any(t => t.Name == "Item_Number_Filter").ShouldBeTrue();
} _generator.Generate(workbook, search);
[Fact] var worksheet = workbook.GetSheet("Search Criteria");
public void Generate_ContainsProfitCenterFilterTable() ExcelTestHelpers.TableExists(worksheet, "Profit_Center_Filter").ShouldBeTrue();
{ }
using var workbook = new XLWorkbook();
var search = CreateMinimalSearchModel(); [Fact]
search.ProfitCenterFilter.Add(new ProfitCenterFilterEntry { Code = "PC01", Description = "Profit Center 1" }); public void Generate_ContainsWorkCenterFilterTable()
{
_generator.Generate(workbook, search); using var workbook = new XSSFWorkbook();
var search = CreateMinimalSearchModel();
var worksheet = workbook.Worksheet("Search Criteria"); search.WorkCenterFilter.Add(new WorkCenterFilterEntry { Code = "WC01", Description = "Work Center 1" });
var tables = worksheet.Tables;
tables.Any(t => t.Name == "Profit_Center_Filter").ShouldBeTrue(); _generator.Generate(workbook, search);
}
var worksheet = workbook.GetSheet("Search Criteria");
[Fact] ExcelTestHelpers.TableExists(worksheet, "Work_Center_Filter").ShouldBeTrue();
public void Generate_ContainsWorkCenterFilterTable() }
{
using var workbook = new XLWorkbook(); [Fact]
var search = CreateMinimalSearchModel(); public void Generate_ContainsOperatorFilterTable()
search.WorkCenterFilter.Add(new WorkCenterFilterEntry { Code = "WC01", Description = "Work Center 1" }); {
using var workbook = new XSSFWorkbook();
_generator.Generate(workbook, search); var search = CreateMinimalSearchModel();
search.OperatorFilter.Add(new OperatorFilterEntry { UserId = "OP01", FullName = "Operator 1" });
var worksheet = workbook.Worksheet("Search Criteria");
var tables = worksheet.Tables; _generator.Generate(workbook, search);
tables.Any(t => t.Name == "Work_Center_Filter").ShouldBeTrue();
} var worksheet = workbook.GetSheet("Search Criteria");
ExcelTestHelpers.TableExists(worksheet, "Operator_Filter").ShouldBeTrue();
[Fact] }
public void Generate_ContainsOperatorFilterTable()
{ [Fact]
using var workbook = new XLWorkbook(); public void Generate_ContainsComponentLotFilterTable()
var search = CreateMinimalSearchModel(); {
search.OperatorFilter.Add(new OperatorFilterEntry { UserId = "OP01", FullName = "Operator 1" }); using var workbook = new XSSFWorkbook();
var search = CreateMinimalSearchModel();
_generator.Generate(workbook, search); search.ComponentLotFilter.Add(new ComponentLotFilterEntry { LotNumber = "LOT001" });
var worksheet = workbook.Worksheet("Search Criteria"); _generator.Generate(workbook, search);
var tables = worksheet.Tables;
tables.Any(t => t.Name == "Operator_Filter").ShouldBeTrue(); var worksheet = workbook.GetSheet("Search Criteria");
} ExcelTestHelpers.TableExists(worksheet, "Component_Lot_Filter").ShouldBeTrue();
}
[Fact]
public void Generate_ContainsComponentLotFilterTable() [Fact]
{ public void Generate_ContainsItemOperationMisFilterTable()
using var workbook = new XLWorkbook(); {
var search = CreateMinimalSearchModel(); using var workbook = new XSSFWorkbook();
search.ComponentLotFilter.Add(new ComponentLotFilterEntry { LotNumber = "LOT001" }); var search = CreateMinimalSearchModel();
search.ItemOperationMisFilter.Add(new ItemOperationMisFilterEntry
_generator.Generate(workbook, search); {
ItemNumber = "ITEM-001",
var worksheet = workbook.Worksheet("Search Criteria"); OperationNumber = "10",
var tables = worksheet.Tables; MisNumber = "MIS-001"
tables.Any(t => t.Name == "Component_Lot_Filter").ShouldBeTrue(); });
}
_generator.Generate(workbook, search);
[Fact]
public void Generate_ContainsItemOperationMisFilterTable() var worksheet = workbook.GetSheet("Search Criteria");
{ ExcelTestHelpers.TableExists(worksheet, "Item_Operation_MIS_Filter").ShouldBeTrue();
using var workbook = new XLWorkbook(); }
var search = CreateMinimalSearchModel();
search.ItemOperationMisFilter.Add(new ItemOperationMisFilterEntry [Fact]
{ public void Generate_ContainsExtractMisDataIndicator_WhenTrue()
ItemNumber = "ITEM-001", {
OperationNumber = "10", using var workbook = new XSSFWorkbook();
MisNumber = "MIS-001" var search = CreateMinimalSearchModel();
}); search.ExtractMisData = true;
_generator.Generate(workbook, search); _generator.Generate(workbook, search);
var worksheet = workbook.Worksheet("Search Criteria"); var worksheet = workbook.GetSheet("Search Criteria");
var tables = worksheet.Tables; var yesNo = FindYesNo(worksheet);
tables.Any(t => t.Name == "Item_Operation_MIS_Filter").ShouldBeTrue(); yesNo.ShouldBe("YES");
} }
[Fact] [Fact]
public void Generate_ContainsExtractMisDataIndicator_WhenTrue() public void Generate_ContainsExtractMisDataIndicator_WhenFalse()
{ {
using var workbook = new XLWorkbook(); using var workbook = new XSSFWorkbook();
var search = CreateMinimalSearchModel(); var search = CreateMinimalSearchModel();
search.ExtractMisData = true; search.ExtractMisData = false;
_generator.Generate(workbook, search); _generator.Generate(workbook, search);
var worksheet = workbook.Worksheet("Search Criteria"); var worksheet = workbook.GetSheet("Search Criteria");
// Find the "Extract MIS data?" header and check for YES var yesNo = FindYesNo(worksheet);
var cells = worksheet.CellsUsed(); yesNo.ShouldBe("NO");
var extractMisCell = cells.FirstOrDefault(c => c.Value.ToString() == "YES" || c.Value.ToString() == "NO"); }
extractMisCell.ShouldNotBeNull();
extractMisCell.Value.GetText().ShouldBe("YES"); [Fact]
} public void Generate_AppliesHeaderFormatting()
{
[Fact] using var workbook = new XSSFWorkbook();
public void Generate_ContainsExtractMisDataIndicator_WhenFalse() var search = CreateMinimalSearchModel();
{
using var workbook = new XLWorkbook(); _generator.Generate(workbook, search);
var search = CreateMinimalSearchModel();
search.ExtractMisData = false; var worksheet = workbook.GetSheet("Search Criteria");
var headerCell = ExcelTestHelpers.GetCell(worksheet, 1, 1)!;
_generator.Generate(workbook, search); (headerCell.CellStyle.FontIndex >= 0 &&
workbook.GetFontAt(headerCell.CellStyle.FontIndex).IsBold).ShouldBeTrue();
var worksheet = workbook.Worksheet("Search Criteria"); ExcelTestHelpers.GetFillForegroundRgb(headerCell).ShouldBe([0xDC, 0xDC, 0xDC]);
// Find the "Extract MIS data?" header and check for NO }
var cells = worksheet.CellsUsed();
var extractMisCell = cells.FirstOrDefault(c => c.Value.ToString() == "YES" || c.Value.ToString() == "NO"); [Fact]
extractMisCell.ShouldNotBeNull(); public void Generate_AppliesProtection()
extractMisCell.Value.GetText().ShouldBe("NO"); {
} using var workbook = new XSSFWorkbook();
var search = CreateMinimalSearchModel();
[Fact]
public void Generate_AppliesHeaderFormatting() _generator.Generate(workbook, search);
{
using var workbook = new XLWorkbook(); var worksheet = workbook.GetSheet("Search Criteria");
var search = CreateMinimalSearchModel(); ExcelTestHelpers.IsSheetProtected(worksheet).ShouldBeTrue();
}
_generator.Generate(workbook, search);
[Fact]
var worksheet = workbook.Worksheet("Search Criteria"); public void Generate_TablesHaveLight18Style()
// Search Name header should be bold with Gainsboro background {
var headerCell = worksheet.Cell(1, 1); using var workbook = new XSSFWorkbook();
headerCell.Style.Font.Bold.ShouldBeTrue(); var search = CreateMinimalSearchModel();
headerCell.Style.Fill.BackgroundColor.ShouldBe(XLColor.Gainsboro); search.WorkOrderFilter.Add(new WorkOrderFilterEntry { WorkOrderNumber = 12345, ItemNumber = "ITEM-001" });
}
_generator.Generate(workbook, search);
[Fact]
public void Generate_AppliesProtection() var worksheet = workbook.GetSheet("Search Criteria");
{ var table = ExcelTestHelpers.GetTableByName(worksheet, "Work_Order_Filter");
using var workbook = new XLWorkbook(); table.StyleName.ShouldBe("TableStyleLight18");
var search = CreateMinimalSearchModel(); }
_generator.Generate(workbook, search); [Fact]
public void Generate_FilterTables_Have2BlankRowSpacing()
var worksheet = workbook.Worksheet("Search Criteria"); {
worksheet.Protection.IsProtected.ShouldBeTrue(); using var workbook = new XSSFWorkbook();
} var search = CreateMinimalSearchModel();
search.WorkOrderFilter.Add(new WorkOrderFilterEntry { WorkOrderNumber = 12345, ItemNumber = "ITEM-001" });
[Fact] search.ItemNumberFilter.Add(new ItemNumberFilterEntry { ItemNumber = "ITEM-001" });
public void Generate_TablesHaveLight18Style()
{ _generator.Generate(workbook, search);
using var workbook = new XLWorkbook();
var search = CreateMinimalSearchModel(); var worksheet = workbook.GetSheet("Search Criteria");
search.WorkOrderFilter.Add(new WorkOrderFilterEntry { WorkOrderNumber = 12345, ItemNumber = "ITEM-001" }); var woTable = ExcelTestHelpers.GetTableByName(worksheet, "Work_Order_Filter");
var itemTable = ExcelTestHelpers.GetTableByName(worksheet, "Item_Number_Filter");
_generator.Generate(workbook, search);
var gap = itemTable.StartCellReference.Row - woTable.EndCellReference.Row;
var worksheet = workbook.Worksheet("Search Criteria"); gap.ShouldBeGreaterThanOrEqualTo(3);
var table = worksheet.Tables.First(t => t.Name == "Work_Order_Filter"); }
table.Theme.ShouldBe(XLTableTheme.TableStyleLight18);
} [Fact]
public void Generate_NullTimestamps_ShowEmptyValue()
[Fact] {
public void Generate_FilterTables_Have2BlankRowSpacing() using var workbook = new XSSFWorkbook();
{ var search = CreateMinimalSearchModel();
using var workbook = new XLWorkbook(); search.SubmitDt = null;
var search = CreateMinimalSearchModel(); search.StartDt = null;
search.WorkOrderFilter.Add(new WorkOrderFilterEntry { WorkOrderNumber = 12345, ItemNumber = "ITEM-001" }); search.EndDt = null;
search.ItemNumberFilter.Add(new ItemNumberFilterEntry { ItemNumber = "ITEM-001" });
_generator.Generate(workbook, search);
_generator.Generate(workbook, search);
var worksheet = workbook.GetSheet("Search Criteria");
var worksheet = workbook.Worksheet("Search Criteria"); ExcelTestHelpers.GetCellText(worksheet, 4, 2).ShouldBe(string.Empty);
var woTable = worksheet.Tables.First(t => t.Name == "Work_Order_Filter"); ExcelTestHelpers.GetCellText(worksheet, 5, 2).ShouldBe(string.Empty);
var itemTable = worksheet.Tables.First(t => t.Name == "Item_Number_Filter"); ExcelTestHelpers.GetCellText(worksheet, 6, 2).ShouldBe(string.Empty);
}
// There should be 2 blank rows between tables. With the header row of the next table, that's a gap of 3
// Looking at CriteriaSheetGenerator: row = table.RangeAddress.LastAddress.RowNumber + 3 [Fact]
// This means the next table starts 3 rows after the last row, leaving 2 blank rows in between public void Generate_ColumnsAreAutoFitWithPadding()
var gap = itemTable.RangeAddress.FirstAddress.RowNumber - woTable.RangeAddress.LastAddress.RowNumber; {
// Gap includes header row of next table, so: 2 blank rows + 1 header = gap of 3 using var workbook = new XSSFWorkbook();
// But with table header (Timespan_Filter has ShowHeader=true), add 1 more var search = CreateMinimalSearchModel();
gap.ShouldBeGreaterThanOrEqualTo(3); search.Name = "A Very Long Search Name That Needs Extra Width";
}
_generator.Generate(workbook, search);
[Fact]
public void Generate_NullTimestamps_ShowEmptyValue() var worksheet = workbook.GetSheet("Search Criteria");
{ ExcelTestHelpers.GetColumnWidthChars(worksheet, 1).ShouldBeGreaterThan(0);
using var workbook = new XLWorkbook(); ExcelTestHelpers.GetColumnWidthChars(worksheet, 2).ShouldBeGreaterThan(0);
var search = CreateMinimalSearchModel(); }
search.SubmitDt = null;
search.StartDt = null; [Fact]
search.EndDt = null; public void Generate_MultipleFiltersWithData_CreatesAllTables()
{
_generator.Generate(workbook, search); using var workbook = new XSSFWorkbook();
var search = CreateFullSearchModel();
var worksheet = workbook.Worksheet("Search Criteria");
worksheet.Cell(4, 2).Value.ToString().ShouldBe(string.Empty); _generator.Generate(workbook, search);
worksheet.Cell(5, 2).Value.ToString().ShouldBe(string.Empty);
worksheet.Cell(6, 2).Value.ToString().ShouldBe(string.Empty); var worksheet = workbook.GetSheet("Search Criteria");
} var tableCount = ((XSSFSheet)worksheet).GetTables().Count;
[Fact] tableCount.ShouldBe(8);
public void Generate_ColumnsAreAutoFitWithPadding() ExcelTestHelpers.TableExists(worksheet, "Timespan_Filter").ShouldBeTrue();
{ ExcelTestHelpers.TableExists(worksheet, "Work_Order_Filter").ShouldBeTrue();
using var workbook = new XLWorkbook(); ExcelTestHelpers.TableExists(worksheet, "Item_Number_Filter").ShouldBeTrue();
var search = CreateMinimalSearchModel(); ExcelTestHelpers.TableExists(worksheet, "Profit_Center_Filter").ShouldBeTrue();
search.Name = "A Very Long Search Name That Needs Extra Width"; ExcelTestHelpers.TableExists(worksheet, "Work_Center_Filter").ShouldBeTrue();
ExcelTestHelpers.TableExists(worksheet, "Component_Lot_Filter").ShouldBeTrue();
_generator.Generate(workbook, search); ExcelTestHelpers.TableExists(worksheet, "Operator_Filter").ShouldBeTrue();
ExcelTestHelpers.TableExists(worksheet, "Item_Operation_MIS_Filter").ShouldBeTrue();
var worksheet = workbook.Worksheet("Search Criteria"); }
// Columns should have been adjusted - verify they have non-default width
worksheet.Column(1).Width.ShouldBeGreaterThan(0); [Fact]
worksheet.Column(2).Width.ShouldBeGreaterThan(0); public void Generate_TimestampFormat_IncludesESTSuffix()
} {
using var workbook = new XSSFWorkbook();
[Fact] var search = CreateMinimalSearchModel();
public void Generate_MultipleFiltersWithData_CreatesAllTables() search.SubmitDt = new DateTime(2024, 1, 15, 10, 30, 45);
{
using var workbook = new XLWorkbook(); _generator.Generate(workbook, search);
var search = CreateFullSearchModel();
var worksheet = workbook.GetSheet("Search Criteria");
_generator.Generate(workbook, search); var timestampValue = ExcelTestHelpers.GetCellText(worksheet, 4, 2);
timestampValue.ShouldContain("EST");
var worksheet = workbook.Worksheet("Search Criteria"); timestampValue.ShouldContain("10:30:45");
var tables = worksheet.Tables; }
// Should have 8 filter tables private static string FindYesNo(NPOI.SS.UserModel.ISheet worksheet)
tables.Count().ShouldBe(8); {
tables.Any(t => t.Name == "Timespan_Filter").ShouldBeTrue(); for (var row = 0; row <= worksheet.LastRowNum; row++)
tables.Any(t => t.Name == "Work_Order_Filter").ShouldBeTrue(); {
tables.Any(t => t.Name == "Item_Number_Filter").ShouldBeTrue(); var npoiRow = worksheet.GetRow(row);
tables.Any(t => t.Name == "Profit_Center_Filter").ShouldBeTrue(); if (npoiRow == null)
tables.Any(t => t.Name == "Work_Center_Filter").ShouldBeTrue(); {
tables.Any(t => t.Name == "Component_Lot_Filter").ShouldBeTrue(); continue;
tables.Any(t => t.Name == "Operator_Filter").ShouldBeTrue(); }
tables.Any(t => t.Name == "Item_Operation_MIS_Filter").ShouldBeTrue();
} for (var col = 0; col < npoiRow.LastCellNum; col++)
{
[Fact] var text = ExcelTestHelpers.GetCellText(worksheet, row + 1, col + 1);
public void Generate_TimestampFormat_IncludesESTSuffix() if (text == "YES" || text == "NO")
{ {
using var workbook = new XLWorkbook(); return text;
var search = CreateMinimalSearchModel(); }
search.SubmitDt = new DateTime(2024, 1, 15, 10, 30, 45); }
}
_generator.Generate(workbook, search);
return string.Empty;
var worksheet = workbook.Worksheet("Search Criteria"); }
var timestampValue = worksheet.Cell(4, 2).Value.GetText();
timestampValue.ShouldContain("EST"); private static SearchModel CreateMinimalSearchModel()
timestampValue.ShouldContain("10:30:45"); {
} return new SearchModel
{
private static SearchModel CreateMinimalSearchModel() Id = 1,
{ Name = "Test Search",
return new SearchModel UserName = "testuser",
{ SubmitDt = DateTime.Now.AddHours(-1),
Id = 1, StartDt = DateTime.Now.AddMinutes(-30),
Name = "Test Search", EndDt = DateTime.Now,
UserName = "testuser", ExtractMisData = false
SubmitDt = DateTime.Now.AddHours(-1), };
StartDt = DateTime.Now.AddMinutes(-30), }
EndDt = DateTime.Now,
ExtractMisData = false private static SearchModel CreateFullSearchModel()
}; {
} return new SearchModel
{
private static SearchModel CreateFullSearchModel() Id = 1,
{ Name = "Full Search",
return new SearchModel UserName = "testuser",
{ SubmitDt = DateTime.Now.AddHours(-1),
Id = 1, StartDt = DateTime.Now.AddMinutes(-30),
Name = "Full Search", EndDt = DateTime.Now,
UserName = "testuser", MinimumDt = new DateTime(2024, 1, 1),
SubmitDt = DateTime.Now.AddHours(-1), MaximumDt = new DateTime(2024, 12, 31),
StartDt = DateTime.Now.AddMinutes(-30), ExtractMisData = true,
EndDt = DateTime.Now, WorkOrderFilter = [new WorkOrderFilterEntry { WorkOrderNumber = 12345, ItemNumber = "ITEM-001" }],
MinimumDt = new DateTime(2024, 1, 1), ItemNumberFilter = [new ItemNumberFilterEntry { ItemNumber = "ITEM-001" }],
MaximumDt = new DateTime(2024, 12, 31), ProfitCenterFilter = [new ProfitCenterFilterEntry { Code = "PC01", Description = "Profit Center 1" }],
ExtractMisData = true, WorkCenterFilter = [new WorkCenterFilterEntry { Code = "WC01", Description = "Work Center 1" }],
WorkOrderFilter = [new WorkOrderFilterEntry { WorkOrderNumber = 12345, ItemNumber = "ITEM-001" }], OperatorFilter = [new OperatorFilterEntry { UserId = "OP01", FullName = "Operator 1" }],
ItemNumberFilter = [new ItemNumberFilterEntry { ItemNumber = "ITEM-001" }], ComponentLotFilter = [new ComponentLotFilterEntry { LotNumber = "LOT001" }],
ProfitCenterFilter = [new ProfitCenterFilterEntry { Code = "PC01", Description = "Profit Center 1" }], ItemOperationMisFilter = [new ItemOperationMisFilterEntry { ItemNumber = "ITEM-001", OperationNumber = "10", MisNumber = "MIS-001" }]
WorkCenterFilter = [new WorkCenterFilterEntry { Code = "WC01", Description = "Work Center 1" }], };
OperatorFilter = [new OperatorFilterEntry { UserId = "OP01", FullName = "Operator 1" }], }
ComponentLotFilter = [new ComponentLotFilterEntry { LotNumber = "LOT001" }], }
ItemOperationMisFilter = [new ItemOperationMisFilterEntry { ItemNumber = "ITEM-001", OperationNumber = "10", MisNumber = "MIS-001" }]
};
}
}
@@ -1,145 +1,137 @@
using ClosedXML.Excel; using JdeScoping.ExcelIO.Generators;
using JdeScoping.ExcelIO.Generators; using JdeScoping.ExcelIO.Tests.Fixtures;
using Shouldly; using Shouldly;
using Xunit; using Xunit;
namespace JdeScoping.ExcelIO.Tests; namespace JdeScoping.ExcelIO.Tests;
public class DataEntryTemplateGeneratorTests public class DataEntryTemplateGeneratorTests
{ {
private readonly DataEntryTemplateGenerator _generator = new(); private readonly DataEntryTemplateGenerator _generator = new();
[Fact] [Fact]
public void Generate_SingleColumn_ReturnsValidExcel() public void Generate_SingleColumn_ReturnsValidExcel()
{ {
var result = _generator.Generate<string>(null, "Test Header"); var result = _generator.Generate<string>(null, "Test Header");
result.ShouldNotBeNull(); result.ShouldNotBeNull();
result.Length.ShouldBeGreaterThan(0); result.Length.ShouldBeGreaterThan(0);
using var stream = new MemoryStream(result); using var workbook = ExcelTestHelpers.OpenWorkbook(result);
using var workbook = new XLWorkbook(stream); workbook.NumberOfSheets.ShouldBe(1);
workbook.Worksheets.Count.ShouldBe(1); }
}
[Fact]
[Fact] public void Generate_SingleColumn_HasCorrectHeader()
public void Generate_SingleColumn_HasCorrectHeader() {
{ var result = _generator.Generate<string>(null, "Item Numbers");
var result = _generator.Generate<string>(null, "Item Numbers");
using var workbook = ExcelTestHelpers.OpenWorkbook(result);
using var stream = new MemoryStream(result); var worksheet = workbook.GetSheetAt(0);
using var workbook = new XLWorkbook(stream);
var worksheet = workbook.Worksheets.First(); ExcelTestHelpers.GetCellText(worksheet, 1, 1).ShouldBe("Item Numbers");
}
worksheet.Cell(1, 1).Value.GetText().ShouldBe("Item Numbers");
} [Fact]
public void Generate_SingleColumn_HasHeaderFormatting()
[Fact] {
public void Generate_SingleColumn_HasHeaderFormatting() var result = _generator.Generate<string>(null, "Test Header");
{
var result = _generator.Generate<string>(null, "Test Header"); using var workbook = ExcelTestHelpers.OpenWorkbook(result);
var worksheet = workbook.GetSheetAt(0);
using var stream = new MemoryStream(result); var headerCell = ExcelTestHelpers.GetCell(worksheet, 1, 1)!;
using var workbook = new XLWorkbook(stream);
var worksheet = workbook.Worksheets.First(); (headerCell.CellStyle.FontIndex >= 0 &&
var headerCell = worksheet.Cell(1, 1); workbook.GetFontAt(headerCell.CellStyle.FontIndex).IsBold).ShouldBeTrue();
ExcelTestHelpers.GetFillForegroundRgb(headerCell).ShouldBe([0xDC, 0xDC, 0xDC]);
headerCell.Style.Font.Bold.ShouldBeTrue(); }
headerCell.Style.Fill.BackgroundColor.ShouldBe(XLColor.Gainsboro);
} [Fact]
public void Generate_SingleColumn_WithData_PopulatesRows()
[Fact] {
public void Generate_SingleColumn_WithData_PopulatesRows() var data = new List<string> { "Item1", "Item2", "Item3" };
{
var data = new List<string> { "Item1", "Item2", "Item3" }; var result = _generator.Generate(data, "Items");
var result = _generator.Generate(data, "Items"); using var workbook = ExcelTestHelpers.OpenWorkbook(result);
var worksheet = workbook.GetSheetAt(0);
using var stream = new MemoryStream(result);
using var workbook = new XLWorkbook(stream); ExcelTestHelpers.GetCellText(worksheet, 2, 1).ShouldBe("Item1");
var worksheet = workbook.Worksheets.First(); ExcelTestHelpers.GetCellText(worksheet, 3, 1).ShouldBe("Item2");
ExcelTestHelpers.GetCellText(worksheet, 4, 1).ShouldBe("Item3");
worksheet.Cell(2, 1).Value.GetText().ShouldBe("Item1"); }
worksheet.Cell(3, 1).Value.GetText().ShouldBe("Item2");
worksheet.Cell(4, 1).Value.GetText().ShouldBe("Item3"); [Fact]
} public void Generate_SingleColumn_SetsTextFormat()
{
[Fact] var result = _generator.Generate<string>(null, "Test");
public void Generate_SingleColumn_SetsTextFormat()
{ using var workbook = ExcelTestHelpers.OpenWorkbook(result);
var result = _generator.Generate<string>(null, "Test"); var worksheet = workbook.GetSheetAt(0);
using var stream = new MemoryStream(result); worksheet.GetColumnStyle(0).GetDataFormatString().ShouldBe("@");
using var workbook = new XLWorkbook(stream); }
var worksheet = workbook.Worksheets.First();
[Fact]
worksheet.Column(1).Style.NumberFormat.Format.ShouldBe("@"); public void Generate_MultiColumn_ReturnsValidExcel()
} {
var headers = new[] { "Column A", "Column B", "Column C" };
[Fact]
public void Generate_MultiColumn_ReturnsValidExcel() var result = _generator.Generate(null, headers);
{
var headers = new[] { "Column A", "Column B", "Column C" }; result.ShouldNotBeNull();
var result = _generator.Generate(null, headers); using var workbook = ExcelTestHelpers.OpenWorkbook(result);
var worksheet = workbook.GetSheetAt(0);
result.ShouldNotBeNull();
ExcelTestHelpers.GetCellText(worksheet, 1, 1).ShouldBe("Column A");
using var stream = new MemoryStream(result); ExcelTestHelpers.GetCellText(worksheet, 1, 2).ShouldBe("Column B");
using var workbook = new XLWorkbook(stream); ExcelTestHelpers.GetCellText(worksheet, 1, 3).ShouldBe("Column C");
var worksheet = workbook.Worksheets.First(); }
worksheet.Cell(1, 1).Value.GetText().ShouldBe("Column A"); [Fact]
worksheet.Cell(1, 2).Value.GetText().ShouldBe("Column B"); public void Generate_MultiColumn_WithData_PopulatesRows()
worksheet.Cell(1, 3).Value.GetText().ShouldBe("Column C"); {
} var headers = new[] { "Name", "Value" };
var data = new[]
[Fact] {
public void Generate_MultiColumn_WithData_PopulatesRows() new object[] { "Row1", 100 },
{ new object[] { "Row2", 200 }
var headers = new[] { "Name", "Value" }; };
var data = new[]
{ var result = _generator.Generate(data, headers);
new object[] { "Row1", 100 },
new object[] { "Row2", 200 } using var workbook = ExcelTestHelpers.OpenWorkbook(result);
}; var worksheet = workbook.GetSheetAt(0);
var result = _generator.Generate(data, headers); ExcelTestHelpers.GetCellText(worksheet, 2, 1).ShouldBe("Row1");
ExcelTestHelpers.GetCellNumber(worksheet, 2, 2).ShouldBe(100);
using var stream = new MemoryStream(result); ExcelTestHelpers.GetCellText(worksheet, 3, 1).ShouldBe("Row2");
using var workbook = new XLWorkbook(stream); ExcelTestHelpers.GetCellNumber(worksheet, 3, 2).ShouldBe(200);
var worksheet = workbook.Worksheets.First(); }
worksheet.Cell(2, 1).Value.GetText().ShouldBe("Row1"); [Fact]
worksheet.Cell(2, 2).Value.GetNumber().ShouldBe(100); public void Generate_MultiColumn_SetsColumnWidth()
worksheet.Cell(3, 1).Value.GetText().ShouldBe("Row2"); {
worksheet.Cell(3, 2).Value.GetNumber().ShouldBe(200); var headers = new[] { "Column A", "Column B" };
}
var result = _generator.Generate(null, headers);
[Fact]
public void Generate_MultiColumn_SetsColumnWidth() using var workbook = ExcelTestHelpers.OpenWorkbook(result);
{ var worksheet = workbook.GetSheetAt(0);
var headers = new[] { "Column A", "Column B" };
ExcelTestHelpers.GetColumnWidthChars(worksheet, 1).ShouldBe(65);
var result = _generator.Generate(null, headers); ExcelTestHelpers.GetColumnWidthChars(worksheet, 2).ShouldBe(65);
}
using var stream = new MemoryStream(result);
using var workbook = new XLWorkbook(stream); [Fact]
var worksheet = workbook.Worksheets.First(); public void Generate_SingleColumn_SetsColumnWidth()
{
worksheet.Column(1).Width.ShouldBe(65); var result = _generator.Generate<string>(null, "Test");
worksheet.Column(2).Width.ShouldBe(65);
} using var workbook = ExcelTestHelpers.OpenWorkbook(result);
var worksheet = workbook.GetSheetAt(0);
[Fact]
public void Generate_SingleColumn_SetsColumnWidth() ExcelTestHelpers.GetColumnWidthChars(worksheet, 1).ShouldBe(45);
{ }
var result = _generator.Generate<string>(null, "Test"); }
using var stream = new MemoryStream(result);
using var workbook = new XLWorkbook(stream);
var worksheet = workbook.Worksheets.First();
worksheet.Column(1).Width.ShouldBe(45);
}
}
@@ -1,249 +1,228 @@
using ClosedXML.Excel; using JdeScoping.Core.Models.SearchResults;
using JdeScoping.Core.Models.SearchResults; using JdeScoping.ExcelIO.Options;
using JdeScoping.ExcelIO.Options; using JdeScoping.ExcelIO.Generators;
using JdeScoping.ExcelIO.Generators; using JdeScoping.ExcelIO.Mapping;
using JdeScoping.ExcelIO.Mapping; using JdeScoping.ExcelIO.Mapping.Maps;
using JdeScoping.ExcelIO.Mapping.Maps; using JdeScoping.ExcelIO.Tests.Fixtures;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using NSubstitute; using NSubstitute;
using Shouldly; using Shouldly;
using Xunit; using Xunit;
namespace JdeScoping.ExcelIO.Tests; namespace JdeScoping.ExcelIO.Tests;
public class ExcelExportServiceTests public class ExcelExportServiceTests
{ {
private readonly ExcelExportService _service; private readonly ExcelExportService _service;
private readonly ILogger<ExcelExportService> _logger;
private readonly IOptions<ExcelExportOptions> _options; public ExcelExportServiceTests()
{
public ExcelExportServiceTests() var logger = Substitute.For<ILogger<ExcelExportService>>();
{ var options = Microsoft.Extensions.Options.Options.Create(new ExcelExportOptions
_logger = Substitute.For<ILogger<ExcelExportService>>(); {
_options = Microsoft.Extensions.Options.Options.Create(new ExcelExportOptions CriteriaSheetPassword = "TestCriteriaPass",
{ DataSheetPassword = "TestDataPass"
CriteriaSheetPassword = "TestCriteriaPass", });
DataSheetPassword = "TestDataPass"
}); var registry = CreateTestRegistry();
var tableWriter = new FluentTableWriter(registry);
var registry = CreateTestRegistry(); var criteriaGenerator = new CriteriaSheetGenerator(options, tableWriter);
var tableWriter = new FluentTableWriter(registry);
var criteriaGenerator = new CriteriaSheetGenerator(_options, tableWriter); _service = new ExcelExportService(logger, options, criteriaGenerator, tableWriter, registry);
}
_service = new ExcelExportService(_logger, _options, criteriaGenerator, tableWriter, registry);
} private static ExcelMapRegistry CreateTestRegistry()
{
private static ExcelMapRegistry CreateTestRegistry() var registry = new ExcelMapRegistry();
{
var registry = new ExcelMapRegistry(); registry.Register(new SearchResultMap());
registry.Register(new MisSearchResultMap());
// Search result maps registry.Register(new MisNonMatchSearchResultMap());
registry.Register(new SearchResultMap());
registry.Register(new MisSearchResultMap()); registry.Register(new TimespanFilterMap());
registry.Register(new MisNonMatchSearchResultMap()); registry.Register(new WorkOrderFilterEntryMap());
registry.Register(new ItemNumberFilterEntryMap());
// Filter entry maps registry.Register(new ProfitCenterFilterEntryMap());
registry.Register(new TimespanFilterMap()); registry.Register(new WorkCenterFilterEntryMap());
registry.Register(new WorkOrderFilterEntryMap()); registry.Register(new OperatorFilterEntryMap());
registry.Register(new ItemNumberFilterEntryMap()); registry.Register(new ComponentLotFilterEntryMap());
registry.Register(new ProfitCenterFilterEntryMap()); registry.Register(new ItemOperationMisFilterEntryMap());
registry.Register(new WorkCenterFilterEntryMap());
registry.Register(new OperatorFilterEntryMap()); return registry;
registry.Register(new ComponentLotFilterEntryMap()); }
registry.Register(new ItemOperationMisFilterEntryMap());
[Fact]
return registry; public async Task GenerateAsync_ReturnsValidExcelBytes()
} {
var search = CreateMinimalSearchModel();
[Fact]
public async Task GenerateAsync_ReturnsValidExcelBytes() var result = await _service.GenerateAsync(search);
{
var search = CreateMinimalSearchModel(); result.ShouldNotBeNull();
result.Length.ShouldBeGreaterThan(0);
var result = await _service.GenerateAsync(search);
using var workbook = ExcelTestHelpers.OpenWorkbook(result);
result.ShouldNotBeNull(); workbook.NumberOfSheets.ShouldBeGreaterThanOrEqualTo(2);
result.Length.ShouldBeGreaterThan(0); }
// Verify it's a valid Excel file [Fact]
using var stream = new MemoryStream(result); public async Task GenerateAsync_CreatesSearchCriteriaSheet()
using var workbook = new XLWorkbook(stream); {
workbook.Worksheets.Count.ShouldBeGreaterThanOrEqualTo(2); var search = CreateMinimalSearchModel();
}
var result = await _service.GenerateAsync(search);
[Fact]
public async Task GenerateAsync_CreatesSearchCriteriaSheet() using var workbook = ExcelTestHelpers.OpenWorkbook(result);
{
var search = CreateMinimalSearchModel(); workbook.GetSheet("Search Criteria").ShouldNotBeNull();
}
var result = await _service.GenerateAsync(search);
[Fact]
using var stream = new MemoryStream(result); public async Task GenerateAsync_CreatesSearchResultsSheet()
using var workbook = new XLWorkbook(stream); {
var search = CreateMinimalSearchModel();
workbook.Worksheets.TryGetWorksheet("Search Criteria", out var criteriaSheet).ShouldBeTrue();
criteriaSheet.ShouldNotBeNull(); var result = await _service.GenerateAsync(search);
}
using var workbook = ExcelTestHelpers.OpenWorkbook(result);
[Fact]
public async Task GenerateAsync_CreatesSearchResultsSheet() workbook.GetSheet("Search Results").ShouldNotBeNull();
{ }
var search = CreateMinimalSearchModel();
[Fact]
var result = await _service.GenerateAsync(search); public async Task GenerateAsync_WithMisData_CreatesMisInfoSheet()
{
using var stream = new MemoryStream(result); var search = CreateSearchModelWithMisData();
using var workbook = new XLWorkbook(stream);
var result = await _service.GenerateAsync(search);
workbook.Worksheets.TryGetWorksheet("Search Results", out var resultsSheet).ShouldBeTrue();
resultsSheet.ShouldNotBeNull(); using var workbook = ExcelTestHelpers.OpenWorkbook(result);
}
workbook.NumberOfSheets.ShouldBe(4);
[Fact] workbook.GetSheet("MIS Info").ShouldNotBeNull();
public async Task GenerateAsync_WithMisData_CreatesMisInfoSheet() }
{
var search = CreateSearchModelWithMisData(); [Fact]
public async Task GenerateAsync_WithMisData_CreatesInvestigationSheet()
var result = await _service.GenerateAsync(search); {
var search = CreateSearchModelWithMisData();
using var stream = new MemoryStream(result);
using var workbook = new XLWorkbook(stream); var result = await _service.GenerateAsync(search);
workbook.Worksheets.Count.ShouldBe(4); // Criteria, Results, MIS Info, Investigation using var workbook = ExcelTestHelpers.OpenWorkbook(result);
workbook.Worksheets.TryGetWorksheet("MIS Info", out var misSheet).ShouldBeTrue();
misSheet.ShouldNotBeNull(); workbook.GetSheet("Investigation").ShouldNotBeNull();
} }
[Fact] [Fact]
public async Task GenerateAsync_WithMisData_CreatesInvestigationSheet() public async Task GenerateAsync_WithoutMisData_DoesNotCreateMisSheets()
{ {
var search = CreateSearchModelWithMisData(); var search = CreateMinimalSearchModel();
search.ExtractMisData = false;
var result = await _service.GenerateAsync(search);
var result = await _service.GenerateAsync(search);
using var stream = new MemoryStream(result);
using var workbook = new XLWorkbook(stream); using var workbook = ExcelTestHelpers.OpenWorkbook(result);
workbook.Worksheets.TryGetWorksheet("Investigation", out var investigationSheet).ShouldBeTrue(); workbook.NumberOfSheets.ShouldBe(2);
investigationSheet.ShouldNotBeNull(); }
}
[Fact]
[Fact] public async Task GenerateAsync_CancellationRequested_ThrowsOperationCanceled()
public async Task GenerateAsync_WithoutMisData_DoesNotCreateMisSheets() {
{ var search = CreateMinimalSearchModel();
var search = CreateMinimalSearchModel(); var cts = new CancellationTokenSource();
search.ExtractMisData = false; cts.Cancel();
var result = await _service.GenerateAsync(search); await Should.ThrowAsync<OperationCanceledException>(
() => _service.GenerateAsync(search, cts.Token));
using var stream = new MemoryStream(result); }
using var workbook = new XLWorkbook(stream);
[Fact]
workbook.Worksheets.Count.ShouldBe(2); // Only Criteria and Results public async Task GenerateAsync_CriteriaSheet_ContainsSearchName()
} {
var search = CreateMinimalSearchModel();
[Fact] search.Name = "Test Search Name";
public async Task GenerateAsync_CancellationRequested_ThrowsOperationCanceled()
{ var result = await _service.GenerateAsync(search);
var search = CreateMinimalSearchModel();
var cts = new CancellationTokenSource(); using var workbook = ExcelTestHelpers.OpenWorkbook(result);
cts.Cancel(); var criteriaSheet = workbook.GetSheet("Search Criteria");
await Should.ThrowAsync<OperationCanceledException>( ExcelTestHelpers.GetCellText(criteriaSheet, 1, 2).ShouldBe("Test Search Name");
() => _service.GenerateAsync(search, cts.Token)); }
}
[Fact]
[Fact] public async Task GenerateAsync_CriteriaSheet_ContainsUserName()
public async Task GenerateAsync_CriteriaSheet_ContainsSearchName() {
{ var search = CreateMinimalSearchModel();
var search = CreateMinimalSearchModel(); search.UserName = "testuser";
search.Name = "Test Search Name";
var result = await _service.GenerateAsync(search);
var result = await _service.GenerateAsync(search);
using var workbook = ExcelTestHelpers.OpenWorkbook(result);
using var stream = new MemoryStream(result); var criteriaSheet = workbook.GetSheet("Search Criteria");
using var workbook = new XLWorkbook(stream);
var criteriaSheet = workbook.Worksheet("Search Criteria"); ExcelTestHelpers.GetCellText(criteriaSheet, 2, 2).ShouldBe("testuser");
}
criteriaSheet.Cell(1, 2).Value.GetText().ShouldBe("Test Search Name");
} [Fact]
public async Task GenerateAsync_ResultsSheet_ContainsResultData()
[Fact] {
public async Task GenerateAsync_CriteriaSheet_ContainsUserName() var search = CreateMinimalSearchModel();
{ search.Results.Add(new SearchResult
var search = CreateMinimalSearchModel(); {
search.UserName = "testuser"; WorkOrderNumber = 12345,
ItemNumber = "ITEM-001",
var result = await _service.GenerateAsync(search); LotNumber = "LOT-001",
Flagged = true
using var stream = new MemoryStream(result); });
using var workbook = new XLWorkbook(stream);
var criteriaSheet = workbook.Worksheet("Search Criteria"); var result = await _service.GenerateAsync(search);
criteriaSheet.Cell(2, 2).Value.GetText().ShouldBe("testuser"); using var workbook = ExcelTestHelpers.OpenWorkbook(result);
} var resultsSheet = workbook.GetSheet("Search Results");
[Fact] ExcelTestHelpers.GetCellText(resultsSheet, 1, 1).ShouldBe("Work Order Number");
public async Task GenerateAsync_ResultsSheet_ContainsResultData() ExcelTestHelpers.GetCellNumber(resultsSheet, 2, 1).ShouldBe(12345);
{ }
var search = CreateMinimalSearchModel();
search.Results.Add(new SearchResult private static SearchModel CreateMinimalSearchModel()
{ {
WorkOrderNumber = 12345, return new SearchModel
ItemNumber = "ITEM-001", {
LotNumber = "LOT-001", Id = 1,
Flagged = true Name = "Test Search",
}); UserName = "testuser",
SubmitDt = DateTime.Now.AddHours(-1),
var result = await _service.GenerateAsync(search); StartDt = DateTime.Now.AddMinutes(-30),
EndDt = DateTime.Now,
using var stream = new MemoryStream(result); ExtractMisData = false,
using var workbook = new XLWorkbook(stream); Results = []
var resultsSheet = workbook.Worksheet("Search Results"); };
}
// Check header row
resultsSheet.Cell(1, 1).Value.GetText().ShouldBe("Work Order Number"); private static SearchModel CreateSearchModelWithMisData()
{
// Check data row return new SearchModel
resultsSheet.Cell(2, 1).Value.GetNumber().ShouldBe(12345); {
} Id = 1,
Name = "Test Search with MIS",
private static SearchModel CreateMinimalSearchModel() UserName = "testuser",
{ SubmitDt = DateTime.Now.AddHours(-1),
return new SearchModel StartDt = DateTime.Now.AddMinutes(-30),
{ EndDt = DateTime.Now,
Id = 1, ExtractMisData = true,
Name = "Test Search", Results = [
UserName = "testuser", new SearchResult { WorkOrderNumber = 12345, Flagged = true }
SubmitDt = DateTime.Now.AddHours(-1), ],
StartDt = DateTime.Now.AddMinutes(-30), MisResults = [
EndDt = DateTime.Now, new MisSearchResult { ItemNumber = "ITEM-001", MisNumber = "MIS-001" }
ExtractMisData = false, ],
Results = [] MisNonMatchResults = [
}; new MisNonMatchSearchResult { WorkOrderNumber = 12345, ItemNumber = "ITEM-001" }
} ]
};
private static SearchModel CreateSearchModelWithMisData() }
{ }
return new SearchModel
{
Id = 1,
Name = "Test Search with MIS",
UserName = "testuser",
SubmitDt = DateTime.Now.AddHours(-1),
StartDt = DateTime.Now.AddMinutes(-30),
EndDt = DateTime.Now,
ExtractMisData = true,
Results = [
new SearchResult { WorkOrderNumber = 12345, Flagged = true }
],
MisResults = [
new MisSearchResult { ItemNumber = "ITEM-001", MisNumber = "MIS-001" }
],
MisNonMatchResults = [
new MisNonMatchSearchResult { WorkOrderNumber = 12345, ItemNumber = "ITEM-001" }
]
};
}
}
@@ -1,18 +1,139 @@
using ClosedXML.Excel; using NPOI.SS.UserModel;
using NPOI.SS.Util;
namespace JdeScoping.ExcelIO.Tests.Fixtures; using NPOI.XSSF.UserModel;
public static class ExcelTestHelpers namespace JdeScoping.ExcelIO.Tests.Fixtures;
{
public static List<string> GetHeadersFromSheet(IXLWorksheet sheet) public static class ExcelTestHelpers
{ {
var headers = new List<string>(); private static readonly DataFormatter Formatter = new();
var col = 1;
while (!sheet.Cell(1, col).IsEmpty()) public static List<string> GetHeadersFromSheet(ISheet sheet)
{ {
headers.Add(sheet.Cell(1, col).Value.GetText()); var headers = new List<string>();
col++; var headerRow = sheet.GetRow(0);
} if (headerRow == null)
return headers; {
} return headers;
} }
var col = 0;
while (true)
{
var cell = headerRow.GetCell(col);
var text = cell == null ? string.Empty : Formatter.FormatCellValue(cell);
if (string.IsNullOrEmpty(text))
{
break;
}
headers.Add(text);
col++;
}
return headers;
}
public static string GetCellText(ISheet sheet, int row1Based, int col1Based)
{
var cell = GetCell(sheet, row1Based, col1Based);
return cell == null ? string.Empty : Formatter.FormatCellValue(cell);
}
public static double GetCellNumber(ISheet sheet, int row1Based, int col1Based)
{
var cell = GetCell(sheet, row1Based, col1Based);
if (cell == null)
{
return 0;
}
if (cell.CellType == CellType.Numeric)
{
return cell.NumericCellValue;
}
var text = Formatter.FormatCellValue(cell);
return double.TryParse(text, out var value) ? value : 0;
}
public static ICell? GetCell(ISheet sheet, int row1Based, int col1Based)
{
return sheet.GetRow(row1Based - 1)?.GetCell(col1Based - 1);
}
public static XSSFSheet GetXssfSheet(IWorkbook workbook, string sheetName)
{
return (XSSFSheet)workbook.GetSheet(sheetName)!;
}
public static XSSFWorkbook OpenWorkbook(byte[] bytes)
{
return new XSSFWorkbook(new MemoryStream(bytes));
}
public static double GetColumnWidthChars(ISheet sheet, int col1Based)
{
return sheet.GetColumnWidth(col1Based - 1) / 256d;
}
public static bool IsSheetProtected(ISheet sheet)
{
return sheet is XSSFSheet xssf && xssf.IsSheetLocked;
}
public static byte[]? GetFillForegroundRgb(ICell cell)
{
if (cell.CellStyle is XSSFCellStyle xssfStyle)
{
return xssfStyle.FillForegroundXSSFColor?.RGB;
}
return null;
}
public static bool IsMerged(ISheet sheet, int firstRow1Based, int lastRow1Based, int firstCol1Based, int lastCol1Based)
{
for (var i = 0; i < sheet.NumMergedRegions; i++)
{
var region = sheet.GetMergedRegion(i);
if (region.FirstRow == firstRow1Based - 1 &&
region.LastRow == lastRow1Based - 1 &&
region.FirstColumn == firstCol1Based - 1 &&
region.LastColumn == lastCol1Based - 1)
{
return true;
}
}
return false;
}
public static XSSFTable GetFirstTable(ISheet sheet)
{
return ((XSSFSheet)sheet).GetTables().First();
}
public static XSSFTable GetTableByName(ISheet sheet, string name)
{
return ((XSSFSheet)sheet).GetTables().First(t => t.Name == name || t.DisplayName == name);
}
public static int GetTableRowCount(XSSFTable table)
{
// header row + data rows
return table.RowCount;
}
public static bool TableExists(ISheet sheet, string tableName)
{
return ((XSSFSheet)sheet).GetTables().Any(t => t.Name == tableName || t.DisplayName == tableName);
}
public static bool RegionIntersectsCell(CellRangeAddress region, int row1Based, int col1Based)
{
var row = row1Based - 1;
var col = col1Based - 1;
return region.IsInRange(row, col);
}
}
@@ -1,68 +1,67 @@
using ClosedXML.Excel; using JdeScoping.Core.Models.SearchResults;
using JdeScoping.Core.Models.SearchResults; using JdeScoping.ExcelIO.Generators;
using JdeScoping.ExcelIO.Generators; using JdeScoping.ExcelIO.Mapping;
using JdeScoping.ExcelIO.Mapping; using JdeScoping.ExcelIO.Mapping.Maps;
using JdeScoping.ExcelIO.Mapping.Maps; using JdeScoping.ExcelIO.Options;
using JdeScoping.ExcelIO.Options; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging; using NPOI.XSSF.UserModel;
using Microsoft.Extensions.Options; using NSubstitute;
using NSubstitute;
namespace JdeScoping.ExcelIO.Tests.Fixtures;
namespace JdeScoping.ExcelIO.Tests.Fixtures;
public abstract class WorkbookFixtureBase : IDisposable
public abstract class WorkbookFixtureBase : IDisposable {
{ public XSSFWorkbook Workbook { get; }
public XLWorkbook Workbook { get; } public SearchModel SearchModel { get; }
public SearchModel SearchModel { get; }
protected abstract SearchModel CreateSearchModel();
protected abstract SearchModel CreateSearchModel();
protected WorkbookFixtureBase()
protected WorkbookFixtureBase() {
{ SearchModel = CreateSearchModel();
SearchModel = CreateSearchModel(); var service = CreateExportService();
var service = CreateExportService(); var bytes = service.GenerateAsync(SearchModel).GetAwaiter().GetResult();
var bytes = service.GenerateAsync(SearchModel).GetAwaiter().GetResult(); Workbook = new XSSFWorkbook(new MemoryStream(bytes));
Workbook = new XLWorkbook(new MemoryStream(bytes)); }
}
private static ExcelExportService CreateExportService()
private static ExcelExportService CreateExportService() {
{ var logger = Substitute.For<ILogger<ExcelExportService>>();
var logger = Substitute.For<ILogger<ExcelExportService>>(); var options = Microsoft.Extensions.Options.Options.Create(new ExcelExportOptions
var options = Microsoft.Extensions.Options.Options.Create(new ExcelExportOptions {
{ CriteriaSheetPassword = "TestCriteriaPass",
CriteriaSheetPassword = "TestCriteriaPass", DataSheetPassword = "TestDataPass"
DataSheetPassword = "TestDataPass" });
});
var registry = CreateTestRegistry();
var registry = CreateTestRegistry(); var tableWriter = new FluentTableWriter(registry);
var tableWriter = new FluentTableWriter(registry); var criteriaGenerator = new CriteriaSheetGenerator(options, tableWriter);
var criteriaGenerator = new CriteriaSheetGenerator(options, tableWriter);
return new ExcelExportService(logger, options, criteriaGenerator, tableWriter, registry);
return new ExcelExportService(logger, options, criteriaGenerator, tableWriter, registry); }
}
private static ExcelMapRegistry CreateTestRegistry()
private static ExcelMapRegistry CreateTestRegistry() {
{ var registry = new ExcelMapRegistry();
var registry = new ExcelMapRegistry();
registry.Register(new SearchResultMap());
registry.Register(new SearchResultMap()); registry.Register(new MisSearchResultMap());
registry.Register(new MisSearchResultMap()); registry.Register(new MisNonMatchSearchResultMap());
registry.Register(new MisNonMatchSearchResultMap()); registry.Register(new TimespanFilterMap());
registry.Register(new TimespanFilterMap()); registry.Register(new WorkOrderFilterEntryMap());
registry.Register(new WorkOrderFilterEntryMap()); registry.Register(new ItemNumberFilterEntryMap());
registry.Register(new ItemNumberFilterEntryMap()); registry.Register(new ProfitCenterFilterEntryMap());
registry.Register(new ProfitCenterFilterEntryMap()); registry.Register(new WorkCenterFilterEntryMap());
registry.Register(new WorkCenterFilterEntryMap()); registry.Register(new OperatorFilterEntryMap());
registry.Register(new OperatorFilterEntryMap()); registry.Register(new ComponentLotFilterEntryMap());
registry.Register(new ComponentLotFilterEntryMap()); registry.Register(new ItemOperationMisFilterEntryMap());
registry.Register(new ItemOperationMisFilterEntryMap());
return registry;
return registry; }
}
public void Dispose()
public void Dispose() {
{ Workbook.Dispose();
Workbook.Dispose(); GC.SuppressFinalize(this);
GC.SuppressFinalize(this); }
} }
}
@@ -1,80 +1,81 @@
using ClosedXML.Excel; using JdeScoping.ExcelIO.Formatting;
using JdeScoping.ExcelIO.Formatting; using JdeScoping.ExcelIO.Tests.Fixtures;
using Shouldly; using NPOI.XSSF.UserModel;
using Xunit; using Shouldly;
using Xunit;
namespace JdeScoping.ExcelIO.Tests;
namespace JdeScoping.ExcelIO.Tests;
public class HeaderFormatterTests
{ public class HeaderFormatterTests
[Fact] {
public void ApplyHeaderFormat_Cell_AppliesCorrectStyling() [Fact]
{ public void ApplyHeaderFormat_Cell_AppliesCorrectStyling()
using var workbook = new XLWorkbook(); {
var worksheet = workbook.Worksheets.Add("Test"); using var workbook = new XSSFWorkbook();
var cell = worksheet.Cell(1, 1); var worksheet = workbook.CreateSheet("Test");
var row = worksheet.CreateRow(0);
HeaderFormatter.ApplyHeaderFormat(cell, "Test Header"); var cell = row.CreateCell(0);
cell.Value.GetText().ShouldBe("Test Header"); HeaderFormatter.ApplyHeaderFormat(cell, "Test Header");
cell.Style.Font.Bold.ShouldBeTrue();
cell.Style.Alignment.Horizontal.ShouldBe(XLAlignmentHorizontalValues.Center); ExcelTestHelpers.GetCellText(worksheet, 1, 1).ShouldBe("Test Header");
cell.Style.Fill.BackgroundColor.ShouldBe(XLColor.Gainsboro); cell.CellStyle.GetFont(workbook).IsBold.ShouldBeTrue();
} cell.CellStyle.Alignment.ShouldBe(NPOI.SS.UserModel.HorizontalAlignment.Center);
ExcelTestHelpers.GetFillForegroundRgb(cell).ShouldBe([0xDC, 0xDC, 0xDC]);
[Fact] }
public void ApplyHeaderFormat_Cell_WithoutText_AppliesOnlyStyling()
{ [Fact]
using var workbook = new XLWorkbook(); public void ApplyHeaderFormat_Cell_WithoutText_AppliesOnlyStyling()
var worksheet = workbook.Worksheets.Add("Test"); {
var cell = worksheet.Cell(1, 1); using var workbook = new XSSFWorkbook();
cell.Value = "Original"; var worksheet = workbook.CreateSheet("Test");
var row = worksheet.CreateRow(0);
HeaderFormatter.ApplyHeaderFormat(cell); var cell = row.CreateCell(0);
cell.SetCellValue("Original");
cell.Value.GetText().ShouldBe("Original");
cell.Style.Font.Bold.ShouldBeTrue(); HeaderFormatter.ApplyHeaderFormat(cell);
}
ExcelTestHelpers.GetCellText(worksheet, 1, 1).ShouldBe("Original");
[Fact] cell.CellStyle.GetFont(workbook).IsBold.ShouldBeTrue();
public void ApplyHeaderFormat_Range_AppliesCorrectStyling() }
{
using var workbook = new XLWorkbook(); [Fact]
var worksheet = workbook.Worksheets.Add("Test"); public void ApplyHeaderFormat_Range_AppliesCorrectStyling()
var range = worksheet.Range(1, 1, 1, 3); {
using var workbook = new XSSFWorkbook();
HeaderFormatter.ApplyHeaderFormat(range, "Header", merge: false); var worksheet = workbook.CreateSheet("Test");
range.FirstCell().Value.GetText().ShouldBe("Header"); HeaderFormatter.ApplyHeaderFormat(worksheet, 1, 1, 1, 3, "Header", merge: false);
foreach (var cell in range.Cells())
{ ExcelTestHelpers.GetCellText(worksheet, 1, 1).ShouldBe("Header");
cell.Style.Font.Bold.ShouldBeTrue(); for (var col = 1; col <= 3; col++)
cell.Style.Fill.BackgroundColor.ShouldBe(XLColor.Gainsboro); {
} var cell = ExcelTestHelpers.GetCell(worksheet, 1, col)!;
} cell.CellStyle.GetFont(workbook).IsBold.ShouldBeTrue();
ExcelTestHelpers.GetFillForegroundRgb(cell).ShouldBe([0xDC, 0xDC, 0xDC]);
[Fact] }
public void ApplyHeaderFormat_Range_WithMerge_MergesCells() }
{
using var workbook = new XLWorkbook(); [Fact]
var worksheet = workbook.Worksheets.Add("Test"); public void ApplyHeaderFormat_Range_WithMerge_MergesCells()
var range = worksheet.Range(1, 1, 1, 3); {
using var workbook = new XSSFWorkbook();
HeaderFormatter.ApplyHeaderFormat(range, "Merged Header", merge: true); var worksheet = workbook.CreateSheet("Test");
range.IsMerged().ShouldBeTrue(); HeaderFormatter.ApplyHeaderFormat(worksheet, 1, 1, 1, 3, "Merged Header", merge: true);
range.FirstCell().Value.GetText().ShouldBe("Merged Header");
} ExcelTestHelpers.IsMerged(worksheet, 1, 1, 1, 3).ShouldBeTrue();
ExcelTestHelpers.GetCellText(worksheet, 1, 1).ShouldBe("Merged Header");
[Fact] }
public void ApplyHeaderFormat_Range_WithoutMerge_DoesNotMergeCells()
{ [Fact]
using var workbook = new XLWorkbook(); public void ApplyHeaderFormat_Range_WithoutMerge_DoesNotMergeCells()
var worksheet = workbook.Worksheets.Add("Test"); {
var range = worksheet.Range(1, 1, 1, 3); using var workbook = new XSSFWorkbook();
var worksheet = workbook.CreateSheet("Test");
HeaderFormatter.ApplyHeaderFormat(range, "Not Merged", merge: false);
HeaderFormatter.ApplyHeaderFormat(worksheet, 1, 1, 1, 3, "Not Merged", merge: false);
range.IsMerged().ShouldBeFalse();
} ExcelTestHelpers.IsMerged(worksheet, 1, 1, 1, 3).ShouldBeFalse();
} }
}
@@ -1,86 +1,87 @@
using ClosedXML.Excel; using JdeScoping.ExcelIO.Tests.Fixtures;
using JdeScoping.ExcelIO.Tests.Fixtures; using NPOI.SS.UserModel;
using Shouldly; using NPOI.XSSF.UserModel;
using Xunit; using Shouldly;
using Xunit;
namespace JdeScoping.ExcelIO.Tests.Integration;
namespace JdeScoping.ExcelIO.Tests.Integration;
public class InvestigationSheetTests : IClassFixture<WithMisDataFixture>
{ public class InvestigationSheetTests : IClassFixture<WithMisDataFixture>
private readonly XLWorkbook _workbook; {
private readonly IXLWorksheet _sheet; private readonly XSSFWorkbook _workbook;
private readonly List<string> _headers; private readonly ISheet _sheet;
private readonly List<string> _headers;
public InvestigationSheetTests(WithMisDataFixture fixture)
{ public InvestigationSheetTests(WithMisDataFixture fixture)
_workbook = fixture.Workbook; {
_sheet = _workbook.Worksheet("Investigation"); _workbook = fixture.Workbook;
_headers = ExcelTestHelpers.GetHeadersFromSheet(_sheet); _sheet = _workbook.GetSheet("Investigation")!;
} _headers = ExcelTestHelpers.GetHeadersFromSheet(_sheet);
}
[Fact]
public void InvestigationSheet_Exists() [Fact]
{ public void InvestigationSheet_Exists()
_workbook.Worksheets.TryGetWorksheet("Investigation", out _).ShouldBeTrue(); {
} _workbook.GetSheet("Investigation").ShouldNotBeNull();
}
[Fact]
public void ColumnCount_Is12() [Fact]
{ public void ColumnCount_Is12()
_headers.Count.ShouldBe(12); {
} _headers.Count.ShouldBe(12);
}
[Fact]
public void ColumnHeaders_MatchExpected() [Fact]
{ public void ColumnHeaders_MatchExpected()
_headers.ShouldContain("Work Center Code"); {
_headers.ShouldContain("Work Order Number"); _headers.ShouldContain("Work Center Code");
_headers.ShouldContain("Work Order Start Date"); _headers.ShouldContain("Work Order Number");
_headers.ShouldContain("Job Step Number"); _headers.ShouldContain("Work Order Start Date");
_headers.ShouldContain("Function Operation Description"); _headers.ShouldContain("Job Step Number");
_headers.ShouldContain("Job Step End Date"); _headers.ShouldContain("Function Operation Description");
_headers.ShouldContain("Function Code"); _headers.ShouldContain("Job Step End Date");
_headers.ShouldContain("Was Job Step Added?"); _headers.ShouldContain("Function Code");
_headers.ShouldContain("Matched Job Step Number"); _headers.ShouldContain("Was Job Step Added?");
_headers.ShouldContain("Item Number"); _headers.ShouldContain("Matched Job Step Number");
_headers.ShouldContain("Item Description"); _headers.ShouldContain("Item Number");
_headers.ShouldContain("Routing Type"); _headers.ShouldContain("Item Description");
} _headers.ShouldContain("Routing Type");
}
[Fact]
public void ColumnOrder_MatchesSpec() [Fact]
{ public void ColumnOrder_MatchesSpec()
_headers[0].ShouldBe("Work Center Code"); {
_headers[1].ShouldBe("Work Order Number"); _headers[0].ShouldBe("Work Center Code");
_headers[2].ShouldBe("Work Order Start Date"); _headers[1].ShouldBe("Work Order Number");
_headers[3].ShouldBe("Job Step Number"); _headers[2].ShouldBe("Work Order Start Date");
_headers[4].ShouldBe("Function Operation Description"); _headers[3].ShouldBe("Job Step Number");
_headers[5].ShouldBe("Job Step End Date"); _headers[4].ShouldBe("Function Operation Description");
_headers[6].ShouldBe("Function Code"); _headers[5].ShouldBe("Job Step End Date");
_headers[7].ShouldBe("Was Job Step Added?"); _headers[6].ShouldBe("Function Code");
_headers[8].ShouldBe("Matched Job Step Number"); _headers[7].ShouldBe("Was Job Step Added?");
_headers[9].ShouldBe("Item Number"); _headers[8].ShouldBe("Matched Job Step Number");
_headers[10].ShouldBe("Item Description"); _headers[9].ShouldBe("Item Number");
_headers[11].ShouldBe("Routing Type"); _headers[10].ShouldBe("Item Description");
} _headers[11].ShouldBe("Routing Type");
}
[Fact]
public void TableStyle_IsLight18() [Fact]
{ public void TableStyle_IsLight18()
var table = _sheet.Tables.First(); {
table.Theme.ShouldBe(XLTableTheme.TableStyleLight18); var table = ExcelTestHelpers.GetFirstTable(_sheet);
} table.StyleName.ShouldBe("TableStyleLight18");
}
[Fact]
public void Protection_IsEnabled() [Fact]
{ public void Protection_IsEnabled()
_sheet.Protection.IsProtected.ShouldBeTrue(); {
} ((XSSFSheet)_sheet).IsSheetLocked.ShouldBeTrue();
}
[Fact]
public void DataRow_ContainsExpectedValues() [Fact]
{ public void DataRow_ContainsExpectedValues()
_sheet.Cell(2, 2).Value.GetNumber().ShouldBe(12345); {
_sheet.Cell(2, 10).Value.GetText().ShouldBe("ITEM-001"); ExcelTestHelpers.GetCellNumber(_sheet, 2, 2).ShouldBe(12345);
} ExcelTestHelpers.GetCellText(_sheet, 2, 10).ShouldBe("ITEM-001");
} }
}
@@ -1,24 +1,24 @@
using ClosedXML.Excel; using JdeScoping.ExcelIO.Tests.Fixtures;
using JdeScoping.ExcelIO.Tests.Fixtures; using NPOI.XSSF.UserModel;
using Shouldly; using Shouldly;
using Xunit; using Xunit;
namespace JdeScoping.ExcelIO.Tests.Integration; namespace JdeScoping.ExcelIO.Tests.Integration;
public class LargeDataSetTests : IClassFixture<LargeDataSetFixture> public class LargeDataSetTests : IClassFixture<LargeDataSetFixture>
{ {
private readonly XLWorkbook _workbook; private readonly XSSFWorkbook _workbook;
public LargeDataSetTests(LargeDataSetFixture fixture) public LargeDataSetTests(LargeDataSetFixture fixture)
{ {
_workbook = fixture.Workbook; _workbook = fixture.Workbook;
} }
[Fact] [Fact]
public void TableRowCount_Is1001() public void TableRowCount_Is1001()
{ {
var sheet = _workbook.Worksheet("Search Results"); var sheet = _workbook.GetSheet("Search Results")!;
var table = sheet.Tables.First(); var table = ExcelTestHelpers.GetFirstTable(sheet);
table.RowCount().ShouldBe(1001); // 1 header + 1000 data rows ExcelTestHelpers.GetTableRowCount(table).ShouldBe(1001);
} }
} }
@@ -1,48 +1,48 @@
using ClosedXML.Excel; using JdeScoping.ExcelIO.Tests.Fixtures;
using JdeScoping.ExcelIO.Tests.Fixtures; using NPOI.XSSF.UserModel;
using Shouldly; using Shouldly;
using Xunit; using Xunit;
namespace JdeScoping.ExcelIO.Tests.Integration; namespace JdeScoping.ExcelIO.Tests.Integration;
public class MinimalSearchTests : IClassFixture<MinimalSearchFixture> public class MinimalSearchTests : IClassFixture<MinimalSearchFixture>
{ {
private readonly XLWorkbook _workbook; private readonly XSSFWorkbook _workbook;
public MinimalSearchTests(MinimalSearchFixture fixture) public MinimalSearchTests(MinimalSearchFixture fixture)
{ {
_workbook = fixture.Workbook; _workbook = fixture.Workbook;
} }
[Fact] [Fact]
public void SheetCount_IsTwo() public void SheetCount_IsTwo()
{ {
_workbook.Worksheets.Count.ShouldBe(2); _workbook.NumberOfSheets.ShouldBe(2);
} }
[Fact] [Fact]
public void SearchCriteriaSheet_Exists() public void SearchCriteriaSheet_Exists()
{ {
_workbook.Worksheets.TryGetWorksheet("Search Criteria", out _).ShouldBeTrue(); _workbook.GetSheet("Search Criteria").ShouldNotBeNull();
} }
[Fact] [Fact]
public void SearchResultsSheet_Exists() public void SearchResultsSheet_Exists()
{ {
_workbook.Worksheets.TryGetWorksheet("Search Results", out _).ShouldBeTrue(); _workbook.GetSheet("Search Results").ShouldNotBeNull();
} }
[Fact] [Fact]
public void SearchCriteriaSheet_IsProtected() public void SearchCriteriaSheet_IsProtected()
{ {
var sheet = _workbook.Worksheet("Search Criteria"); var sheet = (XSSFSheet)_workbook.GetSheet("Search Criteria")!;
sheet.Protection.IsProtected.ShouldBeTrue(); sheet.IsSheetLocked.ShouldBeTrue();
} }
[Fact] [Fact]
public void SearchResultsSheet_IsProtected() public void SearchResultsSheet_IsProtected()
{ {
var sheet = _workbook.Worksheet("Search Results"); var sheet = (XSSFSheet)_workbook.GetSheet("Search Results")!;
sheet.Protection.IsProtected.ShouldBeTrue(); sheet.IsSheetLocked.ShouldBeTrue();
} }
} }
@@ -1,130 +1,131 @@
using ClosedXML.Excel; using JdeScoping.ExcelIO.Tests.Fixtures;
using JdeScoping.ExcelIO.Tests.Fixtures; using NPOI.SS.UserModel;
using Shouldly; using NPOI.XSSF.UserModel;
using Xunit; using Shouldly;
using Xunit;
namespace JdeScoping.ExcelIO.Tests.Integration;
namespace JdeScoping.ExcelIO.Tests.Integration;
public class MisInfoSheetTests : IClassFixture<WithMisDataFixture>
{ public class MisInfoSheetTests : IClassFixture<WithMisDataFixture>
private readonly XLWorkbook _workbook; {
private readonly IXLWorksheet _sheet; private readonly XSSFWorkbook _workbook;
private readonly List<string> _headers; private readonly ISheet _sheet;
private readonly List<string> _headers;
public MisInfoSheetTests(WithMisDataFixture fixture)
{ public MisInfoSheetTests(WithMisDataFixture fixture)
_workbook = fixture.Workbook; {
_sheet = _workbook.Worksheet("MIS Info"); _workbook = fixture.Workbook;
_headers = ExcelTestHelpers.GetHeadersFromSheet(_sheet); _sheet = _workbook.GetSheet("MIS Info")!;
} _headers = ExcelTestHelpers.GetHeadersFromSheet(_sheet);
}
[Fact]
public void SheetCount_IsFour() [Fact]
{ public void SheetCount_IsFour()
_workbook.Worksheets.Count.ShouldBe(4); {
} _workbook.NumberOfSheets.ShouldBe(4);
}
[Fact]
public void MisInfoSheet_Exists() [Fact]
{ public void MisInfoSheet_Exists()
_workbook.Worksheets.TryGetWorksheet("MIS Info", out _).ShouldBeTrue(); {
} _workbook.GetSheet("MIS Info").ShouldNotBeNull();
}
[Fact]
public void ColumnCount_Is19() [Fact]
{ public void ColumnCount_Is19()
_headers.Count.ShouldBe(19); {
} _headers.Count.ShouldBe(19);
}
[Fact]
public void ColumnHeaders_MatchExpected() [Fact]
{ public void ColumnHeaders_MatchExpected()
_headers.ShouldContain("Item Number"); {
_headers.ShouldContain("MIS Job Step Sequence Number"); _headers.ShouldContain("Item Number");
_headers.ShouldContain("MIS Number"); _headers.ShouldContain("MIS Job Step Sequence Number");
_headers.ShouldContain("MIS Revision"); _headers.ShouldContain("MIS Number");
_headers.ShouldContain("Item Description"); _headers.ShouldContain("MIS Revision");
_headers.ShouldContain("MIS Release Status"); _headers.ShouldContain("Item Description");
_headers.ShouldContain("MIS Release Date"); _headers.ShouldContain("MIS Release Status");
_headers.ShouldContain("Branch Code"); _headers.ShouldContain("MIS Release Date");
_headers.ShouldContain("Job Step Sequence Number"); _headers.ShouldContain("Branch Code");
_headers.ShouldContain("Matched Sequence Number"); _headers.ShouldContain("Job Step Sequence Number");
_headers.ShouldContain("Matched to F3112Z1?"); _headers.ShouldContain("Matched Sequence Number");
_headers.ShouldContain("Matched to F3003?"); _headers.ShouldContain("Matched to F3112Z1?");
_headers.ShouldContain("Function Operation Description"); _headers.ShouldContain("Matched to F3003?");
_headers.ShouldContain("Char Number"); _headers.ShouldContain("Function Operation Description");
_headers.ShouldContain("Test Description"); _headers.ShouldContain("Char Number");
_headers.ShouldContain("Sampling Type"); _headers.ShouldContain("Test Description");
_headers.ShouldContain("Sampling Value"); _headers.ShouldContain("Sampling Type");
_headers.ShouldContain("Tools & Gauges"); _headers.ShouldContain("Sampling Value");
_headers.ShouldContain("Work Instructions"); _headers.ShouldContain("Tools & Gauges");
} _headers.ShouldContain("Work Instructions");
}
[Fact]
public void ColumnOrder_MatchesSpec() [Fact]
{ public void ColumnOrder_MatchesSpec()
_headers[0].ShouldBe("Item Number"); {
_headers[1].ShouldBe("MIS Job Step Sequence Number"); _headers[0].ShouldBe("Item Number");
_headers[2].ShouldBe("MIS Number"); _headers[1].ShouldBe("MIS Job Step Sequence Number");
_headers[3].ShouldBe("MIS Revision"); _headers[2].ShouldBe("MIS Number");
_headers[4].ShouldBe("Item Description"); _headers[3].ShouldBe("MIS Revision");
_headers[5].ShouldBe("MIS Release Status"); _headers[4].ShouldBe("Item Description");
_headers[6].ShouldBe("MIS Release Date"); _headers[5].ShouldBe("MIS Release Status");
_headers[7].ShouldBe("Branch Code"); _headers[6].ShouldBe("MIS Release Date");
_headers[8].ShouldBe("Job Step Sequence Number"); _headers[7].ShouldBe("Branch Code");
_headers[9].ShouldBe("Matched Sequence Number"); _headers[8].ShouldBe("Job Step Sequence Number");
_headers[10].ShouldBe("Matched to F3112Z1?"); _headers[9].ShouldBe("Matched Sequence Number");
_headers[11].ShouldBe("Matched to F3003?"); _headers[10].ShouldBe("Matched to F3112Z1?");
_headers[12].ShouldBe("Function Operation Description"); _headers[11].ShouldBe("Matched to F3003?");
_headers[13].ShouldBe("Char Number"); _headers[12].ShouldBe("Function Operation Description");
_headers[14].ShouldBe("Test Description"); _headers[13].ShouldBe("Char Number");
_headers[15].ShouldBe("Sampling Type"); _headers[14].ShouldBe("Test Description");
_headers[16].ShouldBe("Sampling Value"); _headers[15].ShouldBe("Sampling Type");
_headers[17].ShouldBe("Tools & Gauges"); _headers[16].ShouldBe("Sampling Value");
_headers[18].ShouldBe("Work Instructions"); _headers[17].ShouldBe("Tools & Gauges");
} _headers[18].ShouldBe("Work Instructions");
}
[Fact]
public void TableStyle_IsLight18() [Fact]
{ public void TableStyle_IsLight18()
var table = _sheet.Tables.First(); {
table.Theme.ShouldBe(XLTableTheme.TableStyleLight18); var table = ExcelTestHelpers.GetFirstTable(_sheet);
} table.StyleName.ShouldBe("TableStyleLight18");
}
[Fact]
public void Protection_IsEnabled() [Fact]
{ public void Protection_IsEnabled()
_sheet.Protection.IsProtected.ShouldBeTrue(); {
} ((XSSFSheet)_sheet).IsSheetLocked.ShouldBeTrue();
}
[Fact]
public void DataRow_ContainsExpectedValues() [Fact]
{ public void DataRow_ContainsExpectedValues()
_sheet.Cell(2, 1).Value.GetText().ShouldBe("ITEM-001"); {
_sheet.Cell(2, 3).Value.GetText().ShouldBe("MIS-001"); ExcelTestHelpers.GetCellText(_sheet, 2, 1).ShouldBe("ITEM-001");
} ExcelTestHelpers.GetCellText(_sheet, 2, 3).ShouldBe("MIS-001");
}
[Fact]
public void TestDescriptionColumn_IsWrapped() [Fact]
{ public void TestDescriptionColumn_IsWrapped()
var colIndex = _headers.IndexOf("Test Description") + 1; {
_sheet.Column(colIndex).Width.ShouldBe(65); var colIndex = _headers.IndexOf("Test Description") + 1;
_sheet.Column(colIndex).Style.Alignment.WrapText.ShouldBeTrue(); ExcelTestHelpers.GetColumnWidthChars(_sheet, colIndex).ShouldBe(65);
} ExcelTestHelpers.GetCell(_sheet, 2, colIndex)!.CellStyle.WrapText.ShouldBeTrue();
}
[Fact]
public void ToolsGaugesColumn_IsWrapped() [Fact]
{ public void ToolsGaugesColumn_IsWrapped()
var colIndex = _headers.IndexOf("Tools & Gauges") + 1; {
_sheet.Column(colIndex).Width.ShouldBe(65); var colIndex = _headers.IndexOf("Tools & Gauges") + 1;
_sheet.Column(colIndex).Style.Alignment.WrapText.ShouldBeTrue(); ExcelTestHelpers.GetColumnWidthChars(_sheet, colIndex).ShouldBe(65);
} ExcelTestHelpers.GetCell(_sheet, 2, colIndex)!.CellStyle.WrapText.ShouldBeTrue();
}
[Fact]
public void WorkInstructionsColumn_IsWrapped() [Fact]
{ public void WorkInstructionsColumn_IsWrapped()
var colIndex = _headers.IndexOf("Work Instructions") + 1; {
_sheet.Column(colIndex).Width.ShouldBe(65); var colIndex = _headers.IndexOf("Work Instructions") + 1;
_sheet.Column(colIndex).Style.Alignment.WrapText.ShouldBeTrue(); ExcelTestHelpers.GetColumnWidthChars(_sheet, colIndex).ShouldBe(65);
} ExcelTestHelpers.GetCell(_sheet, 2, colIndex)!.CellStyle.WrapText.ShouldBeTrue();
} }
}
@@ -1,75 +1,77 @@
using ClosedXML.Excel; using JdeScoping.ExcelIO.Tests.Fixtures;
using JdeScoping.ExcelIO.Tests.Fixtures; using NPOI.SS.UserModel;
using Shouldly; using NPOI.XSSF.UserModel;
using Xunit; using Shouldly;
using Xunit;
namespace JdeScoping.ExcelIO.Tests.Integration;
namespace JdeScoping.ExcelIO.Tests.Integration;
public class ProtectionAndStyleTests : IClassFixture<WithMisDataFixture>
{ public class ProtectionAndStyleTests : IClassFixture<WithMisDataFixture>
private readonly XLWorkbook _workbook; {
private readonly XSSFWorkbook _workbook;
public ProtectionAndStyleTests(WithMisDataFixture fixture)
{ public ProtectionAndStyleTests(WithMisDataFixture fixture)
_workbook = fixture.Workbook; {
} _workbook = fixture.Workbook;
}
[Fact]
public void AllDataSheets_AreProtected() [Fact]
{ public void AllDataSheets_AreProtected()
_workbook.Worksheet("Search Results").Protection.IsProtected.ShouldBeTrue(); {
_workbook.Worksheet("MIS Info").Protection.IsProtected.ShouldBeTrue(); ((XSSFSheet)_workbook.GetSheet("Search Results")!).IsSheetLocked.ShouldBeTrue();
_workbook.Worksheet("Investigation").Protection.IsProtected.ShouldBeTrue(); ((XSSFSheet)_workbook.GetSheet("MIS Info")!).IsSheetLocked.ShouldBeTrue();
} ((XSSFSheet)_workbook.GetSheet("Investigation")!).IsSheetLocked.ShouldBeTrue();
}
[Fact]
public void Protection_AllowsFiltering() [Fact]
{ public void Protection_AllowsFiltering()
var sheet = _workbook.Worksheet("Search Results"); {
sheet.Protection.AllowedElements.HasFlag(XLSheetProtectionElements.AutoFilter).ShouldBeTrue(); var sheet = (XSSFSheet)_workbook.GetSheet("Search Results")!;
} sheet.IsAutoFilterLocked.ShouldBeFalse();
}
[Fact]
public void Protection_AllowsSorting() [Fact]
{ public void Protection_AllowsSorting()
var sheet = _workbook.Worksheet("Search Results"); {
sheet.Protection.AllowedElements.HasFlag(XLSheetProtectionElements.Sort).ShouldBeTrue(); var sheet = (XSSFSheet)_workbook.GetSheet("Search Results")!;
} sheet.IsSortLocked.ShouldBeFalse();
}
[Fact]
public void Protection_AllowsFormatting() [Fact]
{ public void Protection_AllowsFormatting()
var sheet = _workbook.Worksheet("Search Results"); {
sheet.Protection.AllowedElements.HasFlag(XLSheetProtectionElements.FormatCells).ShouldBeTrue(); var sheet = (XSSFSheet)_workbook.GetSheet("Search Results")!;
sheet.Protection.AllowedElements.HasFlag(XLSheetProtectionElements.FormatColumns).ShouldBeTrue(); sheet.IsFormatCellsLocked.ShouldBeFalse();
sheet.Protection.AllowedElements.HasFlag(XLSheetProtectionElements.FormatRows).ShouldBeTrue(); sheet.IsFormatColumnsLocked.ShouldBeFalse();
} sheet.IsFormatRowsLocked.ShouldBeFalse();
}
[Fact]
public void AllTables_UseLight18Style() [Fact]
{ public void AllTables_UseLight18Style()
_workbook.Worksheet("Search Results").Tables.First().Theme.ShouldBe(XLTableTheme.TableStyleLight18); {
_workbook.Worksheet("MIS Info").Tables.First().Theme.ShouldBe(XLTableTheme.TableStyleLight18); ExcelTestHelpers.GetFirstTable(_workbook.GetSheet("Search Results")!).StyleName.ShouldBe("TableStyleLight18");
_workbook.Worksheet("Investigation").Tables.First().Theme.ShouldBe(XLTableTheme.TableStyleLight18); ExcelTestHelpers.GetFirstTable(_workbook.GetSheet("MIS Info")!).StyleName.ShouldBe("TableStyleLight18");
} ExcelTestHelpers.GetFirstTable(_workbook.GetSheet("Investigation")!).StyleName.ShouldBe("TableStyleLight18");
}
[Fact]
public void HeaderCells_HaveCorrectFormatting() [Fact]
{ public void HeaderCells_HaveCorrectFormatting()
var sheet = _workbook.Worksheet("Search Criteria"); {
var headerCell = sheet.Cell(1, 1); var sheet = _workbook.GetSheet("Search Criteria")!;
headerCell.Style.Font.Bold.ShouldBeTrue(); var headerCell = ExcelTestHelpers.GetCell(sheet, 1, 1)!;
headerCell.Style.Fill.BackgroundColor.ShouldBe(XLColor.Gainsboro); (headerCell.CellStyle.FontIndex >= 0 &&
headerCell.Style.Alignment.Horizontal.ShouldBe(XLAlignmentHorizontalValues.Center); _workbook.GetFontAt(headerCell.CellStyle.FontIndex).IsBold).ShouldBeTrue();
} ExcelTestHelpers.GetFillForegroundRgb(headerCell).ShouldBe([0xDC, 0xDC, 0xDC]);
headerCell.CellStyle.Alignment.ShouldBe(HorizontalAlignment.Center);
[Fact] }
public void CriteriaTimestamp_MatchesLegacyFormat()
{ [Fact]
var sheet = _workbook.Worksheet("Search Criteria"); public void CriteriaTimestamp_MatchesLegacyFormat()
var timestamp = sheet.Cell(4, 2).Value.GetText(); {
timestamp.ShouldContain("Jan 15, 2024"); var sheet = _workbook.GetSheet("Search Criteria")!;
timestamp.ShouldContain("02:30:45"); var timestamp = ExcelTestHelpers.GetCellText(sheet, 4, 2);
timestamp.ShouldContain("EST"); timestamp.ShouldContain("Jan 15, 2024");
} timestamp.ShouldContain("02:30:45");
} timestamp.ShouldContain("EST");
}
}
@@ -1,89 +1,90 @@
using ClosedXML.Excel; using JdeScoping.ExcelIO.Tests.Fixtures;
using JdeScoping.ExcelIO.Tests.Fixtures; using NPOI.SS.UserModel;
using Shouldly; using NPOI.XSSF.UserModel;
using Xunit; using Shouldly;
using Xunit;
namespace JdeScoping.ExcelIO.Tests.Integration;
namespace JdeScoping.ExcelIO.Tests.Integration;
public class SearchResultsSheetTests : IClassFixture<WithResultsFixture>
{ public class SearchResultsSheetTests : IClassFixture<WithResultsFixture>
private readonly XLWorkbook _workbook; {
private readonly IXLWorksheet _sheet; private readonly XSSFWorkbook _workbook;
private readonly List<string> _headers; private readonly ISheet _sheet;
private readonly List<string> _headers;
public SearchResultsSheetTests(WithResultsFixture fixture)
{ public SearchResultsSheetTests(WithResultsFixture fixture)
_workbook = fixture.Workbook; {
_sheet = _workbook.Worksheet("Search Results"); _workbook = fixture.Workbook;
_headers = ExcelTestHelpers.GetHeadersFromSheet(_sheet); _sheet = _workbook.GetSheet("Search Results")!;
} _headers = ExcelTestHelpers.GetHeadersFromSheet(_sheet);
}
[Fact]
public void ColumnCount_Is19() [Fact]
{ public void ColumnCount_Is19()
_headers.Count.ShouldBe(19); {
} _headers.Count.ShouldBe(19);
}
[Fact]
public void ColumnHeaders_MatchExpected() [Fact]
{ public void ColumnHeaders_MatchExpected()
_headers.ShouldContain("Work Order Number"); {
_headers.ShouldContain("Work Order Branch Code"); _headers.ShouldContain("Work Order Number");
_headers.ShouldContain("Lot Number"); _headers.ShouldContain("Work Order Branch Code");
_headers.ShouldContain("Item Number"); _headers.ShouldContain("Lot Number");
_headers.ShouldContain("Planning Family"); _headers.ShouldContain("Item Number");
_headers.ShouldContain("Stocking Type"); _headers.ShouldContain("Planning Family");
_headers.ShouldContain("Order Quantity"); _headers.ShouldContain("Stocking Type");
_headers.ShouldContain("Held Quantity"); _headers.ShouldContain("Order Quantity");
_headers.ShouldContain("Scrapped Quantity"); _headers.ShouldContain("Held Quantity");
_headers.ShouldContain("Shipped Quantity"); _headers.ShouldContain("Scrapped Quantity");
_headers.ShouldContain("Operation Step Branch Code"); _headers.ShouldContain("Shipped Quantity");
_headers.ShouldContain("Operation Step"); _headers.ShouldContain("Operation Step Branch Code");
_headers.ShouldContain("Operation Step Description"); _headers.ShouldContain("Operation Step");
_headers.ShouldContain("Function Operation Description"); _headers.ShouldContain("Operation Step Description");
_headers.ShouldContain("Operation Step Update Timestamp"); _headers.ShouldContain("Function Operation Description");
_headers.ShouldContain("Status Code"); _headers.ShouldContain("Operation Step Update Timestamp");
_headers.ShouldContain("Status Description"); _headers.ShouldContain("Status Code");
_headers.ShouldContain("Status Update Timestamp"); _headers.ShouldContain("Status Description");
_headers.ShouldContain("Inclusion Reason"); _headers.ShouldContain("Status Update Timestamp");
} _headers.ShouldContain("Inclusion Reason");
}
[Fact]
public void ColumnOrder_MatchesSpec() [Fact]
{ public void ColumnOrder_MatchesSpec()
_headers[0].ShouldBe("Work Order Number"); {
_headers[1].ShouldBe("Work Order Branch Code"); _headers[0].ShouldBe("Work Order Number");
_headers[2].ShouldBe("Lot Number"); _headers[1].ShouldBe("Work Order Branch Code");
_headers[3].ShouldBe("Item Number"); _headers[2].ShouldBe("Lot Number");
_headers[4].ShouldBe("Planning Family"); _headers[3].ShouldBe("Item Number");
_headers[5].ShouldBe("Stocking Type"); _headers[4].ShouldBe("Planning Family");
_headers[6].ShouldBe("Order Quantity"); _headers[5].ShouldBe("Stocking Type");
_headers[7].ShouldBe("Held Quantity"); _headers[6].ShouldBe("Order Quantity");
_headers[8].ShouldBe("Scrapped Quantity"); _headers[7].ShouldBe("Held Quantity");
_headers[9].ShouldBe("Shipped Quantity"); _headers[8].ShouldBe("Scrapped Quantity");
_headers[10].ShouldBe("Operation Step Branch Code"); _headers[9].ShouldBe("Shipped Quantity");
_headers[11].ShouldBe("Operation Step"); _headers[10].ShouldBe("Operation Step Branch Code");
_headers[12].ShouldBe("Operation Step Description"); _headers[11].ShouldBe("Operation Step");
_headers[13].ShouldBe("Function Operation Description"); _headers[12].ShouldBe("Operation Step Description");
_headers[14].ShouldBe("Operation Step Update Timestamp"); _headers[13].ShouldBe("Function Operation Description");
_headers[15].ShouldBe("Status Code"); _headers[14].ShouldBe("Operation Step Update Timestamp");
_headers[16].ShouldBe("Status Description"); _headers[15].ShouldBe("Status Code");
_headers[17].ShouldBe("Status Update Timestamp"); _headers[16].ShouldBe("Status Description");
_headers[18].ShouldBe("Inclusion Reason"); _headers[17].ShouldBe("Status Update Timestamp");
} _headers[18].ShouldBe("Inclusion Reason");
}
[Fact]
public void TableStyle_IsLight18() [Fact]
{ public void TableStyle_IsLight18()
var table = _sheet.Tables.First(); {
table.Theme.ShouldBe(XLTableTheme.TableStyleLight18); var table = ExcelTestHelpers.GetFirstTable(_sheet);
} table.StyleName.ShouldBe("TableStyleLight18");
}
[Fact]
public void DataRow_ContainsExpectedValues() [Fact]
{ public void DataRow_ContainsExpectedValues()
_sheet.Cell(2, 1).Value.GetNumber().ShouldBe(12345); {
_sheet.Cell(2, 3).Value.GetText().ShouldBe("LOT-001"); ExcelTestHelpers.GetCellNumber(_sheet, 2, 1).ShouldBe(12345);
_sheet.Cell(2, 4).Value.GetText().ShouldBe("ITEM-001"); ExcelTestHelpers.GetCellText(_sheet, 2, 3).ShouldBe("LOT-001");
} ExcelTestHelpers.GetCellText(_sheet, 2, 4).ShouldBe("ITEM-001");
} }
}
@@ -8,10 +8,11 @@
<IsTestProject>true</IsTestProject> <IsTestProject>true</IsTestProject>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="xunit" Version="2.9.3" /> <PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.2"> <PackageReference Include="NPOI" Version="2.7.5" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
</PackageReference> </PackageReference>
@@ -1,179 +1,171 @@
using ClosedXML.Excel; using JdeScoping.ExcelIO.Parsing;
using JdeScoping.ExcelIO.Parsing; using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Logging.Abstractions; using NPOI.XSSF.UserModel;
using Shouldly; using Shouldly;
using Xunit; using Xunit;
namespace JdeScoping.ExcelIO.Tests.Parsing; namespace JdeScoping.ExcelIO.Tests.Parsing;
public class ExcelParserServiceTests public class ExcelParserServiceTests
{ {
private readonly ExcelParserService _service = new(NullLogger<ExcelParserService>.Instance); private readonly ExcelParserService _service = new(NullLogger<ExcelParserService>.Instance);
[Fact] [Fact]
public void ParseWorkOrders_ReturnsWorkOrderNumbers() public void ParseWorkOrders_ReturnsWorkOrderNumbers()
{ {
// Arrange var excelData = CreateWorkOrderExcel([12345, 67890, 11111]);
var excelData = CreateWorkOrderExcel([12345, 67890, 11111]);
using var stream = new MemoryStream(excelData);
// Act var result = _service.ParseWorkOrders(stream);
using var stream = new MemoryStream(excelData);
var result = _service.ParseWorkOrders(stream); result.Count.ShouldBe(3);
result.ShouldContain(12345);
// Assert result.ShouldContain(67890);
result.Count.ShouldBe(3); result.ShouldContain(11111);
result.ShouldContain(12345); }
result.ShouldContain(67890);
result.ShouldContain(11111); [Fact]
} public void ParseWorkOrders_SkipsInvalidNumbers()
{
[Fact] using var workbook = new XSSFWorkbook();
public void ParseWorkOrders_SkipsInvalidNumbers() var worksheet = workbook.CreateSheet("Sheet1");
{ worksheet.CreateRow(0).CreateCell(0).SetCellValue("Work Order");
// Arrange worksheet.CreateRow(1).CreateCell(0).SetCellValue("12345");
using var workbook = new XLWorkbook(); worksheet.CreateRow(2).CreateCell(0).SetCellValue("not-a-number");
var worksheet = workbook.Worksheets.Add("Sheet1"); worksheet.CreateRow(3).CreateCell(0).SetCellValue("67890");
worksheet.Cell(1, 1).Value = "Work Order";
worksheet.Cell(2, 1).Value = "12345"; using var ms = new MemoryStream();
worksheet.Cell(3, 1).Value = "not-a-number"; workbook.Write(ms, leaveOpen: true);
worksheet.Cell(4, 1).Value = "67890"; ms.Position = 0;
using var ms = new MemoryStream(); var result = _service.ParseWorkOrders(ms);
workbook.SaveAs(ms);
ms.Position = 0; result.Count.ShouldBe(2);
}
// Act
var result = _service.ParseWorkOrders(ms); [Fact]
public void ParseItems_ReturnsItemNumbers()
// Assert {
result.Count.ShouldBe(2); var excelData = CreateItemExcel(["ITEM-001", "ITEM-002"]);
}
using var stream = new MemoryStream(excelData);
[Fact] var result = _service.ParseItems(stream);
public void ParseItems_ReturnsItemNumbers()
{ result.Count.ShouldBe(2);
// Arrange result.ShouldContain("ITEM-001");
var excelData = CreateItemExcel(["ITEM-001", "ITEM-002"]); result.ShouldContain("ITEM-002");
}
// Act
using var stream = new MemoryStream(excelData); [Fact]
var result = _service.ParseItems(stream); public void ParseComponentLots_ReturnsLotViewModels()
{
// Assert var excelData = CreateComponentLotExcel([("LOT001", "ITEM-001"), ("LOT002", "ITEM-002")]);
result.Count.ShouldBe(2);
result.ShouldContain("ITEM-001"); using var stream = new MemoryStream(excelData);
result.ShouldContain("ITEM-002"); var result = _service.ParseComponentLots(stream);
}
result.Count.ShouldBe(2);
[Fact] result[0].LotNumber.ShouldBe("LOT001");
public void ParseComponentLots_ReturnsLotViewModels() result[0].ItemNumber.ShouldBe("ITEM-001");
{ }
// Arrange
var excelData = CreateComponentLotExcel([("LOT001", "ITEM-001"), ("LOT002", "ITEM-002")]); [Fact]
public void ParsePartOperations_ReturnsPartOperations()
// Act {
using var stream = new MemoryStream(excelData); var excelData = CreatePartOperationExcel([("ITEM-001", "100", "MIS001", "A")]);
var result = _service.ParseComponentLots(stream);
using var stream = new MemoryStream(excelData);
// Assert var result = _service.ParsePartOperations(stream);
result.Count.ShouldBe(2);
result[0].LotNumber.ShouldBe("LOT001"); result.Count.ShouldBe(1);
result[0].ItemNumber.ShouldBe("ITEM-001"); result[0].ItemNumber.ShouldBe("ITEM-001");
} result[0].OperationNumber.ShouldBe("100");
result[0].MisNumber.ShouldBe("MIS001");
[Fact] result[0].MisRevision.ShouldBe("A");
public void ParsePartOperations_ReturnsPartOperations() }
{
// Arrange [Fact]
var excelData = CreatePartOperationExcel([("ITEM-001", "100", "MIS001", "A")]); public void ParsePartOperations_TruncatesDecimalOperationNumbers()
{
// Act var excelData = CreatePartOperationExcel([("ITEM-001", "100.5", "MIS001", "A")]);
using var stream = new MemoryStream(excelData);
var result = _service.ParsePartOperations(stream); using var stream = new MemoryStream(excelData);
var result = _service.ParsePartOperations(stream);
// Assert
result.Count.ShouldBe(1); result[0].OperationNumber.ShouldBe("100");
result[0].ItemNumber.ShouldBe("ITEM-001"); }
result[0].OperationNumber.ShouldBe("100");
result[0].MisNumber.ShouldBe("MIS001"); private static byte[] CreateWorkOrderExcel(long[] workOrderNumbers)
result[0].MisRevision.ShouldBe("A"); {
} using var workbook = new XSSFWorkbook();
var worksheet = workbook.CreateSheet("Sheet1");
[Fact] worksheet.CreateRow(0).CreateCell(0).SetCellValue("Work Order Number");
public void ParsePartOperations_TruncatesDecimalOperationNumbers() for (var i = 0; i < workOrderNumbers.Length; i++)
{ {
// Arrange worksheet.CreateRow(i + 1).CreateCell(0).SetCellValue(workOrderNumbers[i]);
var excelData = CreatePartOperationExcel([("ITEM-001", "100.5", "MIS001", "A")]); }
// Act using var stream = new MemoryStream();
using var stream = new MemoryStream(excelData); workbook.Write(stream, leaveOpen: true);
var result = _service.ParsePartOperations(stream); return stream.ToArray();
}
// Assert
result[0].OperationNumber.ShouldBe("100"); private static byte[] CreateItemExcel(string[] itemNumbers)
} {
using var workbook = new XSSFWorkbook();
private static byte[] CreateWorkOrderExcel(long[] workOrderNumbers) var worksheet = workbook.CreateSheet("Sheet1");
{ worksheet.CreateRow(0).CreateCell(0).SetCellValue("Item Number");
using var workbook = new XLWorkbook(); for (var i = 0; i < itemNumbers.Length; i++)
var worksheet = workbook.Worksheets.Add("Sheet1"); {
worksheet.Cell(1, 1).Value = "Work Order Number"; worksheet.CreateRow(i + 1).CreateCell(0).SetCellValue(itemNumbers[i]);
for (var i = 0; i < workOrderNumbers.Length; i++) }
{
worksheet.Cell(i + 2, 1).Value = workOrderNumbers[i]; using var stream = new MemoryStream();
} workbook.Write(stream, leaveOpen: true);
using var stream = new MemoryStream(); return stream.ToArray();
workbook.SaveAs(stream); }
return stream.ToArray();
} private static byte[] CreateComponentLotExcel((string LotNumber, string ItemNumber)[] lots)
{
private static byte[] CreateItemExcel(string[] itemNumbers) using var workbook = new XSSFWorkbook();
{ var worksheet = workbook.CreateSheet("Sheet1");
using var workbook = new XLWorkbook(); var header = worksheet.CreateRow(0);
var worksheet = workbook.Worksheets.Add("Sheet1"); header.CreateCell(0).SetCellValue("Lot Number");
worksheet.Cell(1, 1).Value = "Item Number"; header.CreateCell(1).SetCellValue("Item Number");
for (var i = 0; i < itemNumbers.Length; i++)
{ for (var i = 0; i < lots.Length; i++)
worksheet.Cell(i + 2, 1).Value = itemNumbers[i]; {
} var row = worksheet.CreateRow(i + 1);
using var stream = new MemoryStream(); row.CreateCell(0).SetCellValue(lots[i].LotNumber);
workbook.SaveAs(stream); row.CreateCell(1).SetCellValue(lots[i].ItemNumber);
return stream.ToArray(); }
}
using var stream = new MemoryStream();
private static byte[] CreateComponentLotExcel((string LotNumber, string ItemNumber)[] lots) workbook.Write(stream, leaveOpen: true);
{ return stream.ToArray();
using var workbook = new XLWorkbook(); }
var worksheet = workbook.Worksheets.Add("Sheet1");
worksheet.Cell(1, 1).Value = "Lot Number"; private static byte[] CreatePartOperationExcel((string ItemNumber, string OpNumber, string MisNumber, string MisRevision)[] operations)
worksheet.Cell(1, 2).Value = "Item Number"; {
for (var i = 0; i < lots.Length; i++) using var workbook = new XSSFWorkbook();
{ var worksheet = workbook.CreateSheet("Sheet1");
worksheet.Cell(i + 2, 1).Value = lots[i].LotNumber; var header = worksheet.CreateRow(0);
worksheet.Cell(i + 2, 2).Value = lots[i].ItemNumber; header.CreateCell(0).SetCellValue("Item Number");
} header.CreateCell(1).SetCellValue("Operation Number");
using var stream = new MemoryStream(); header.CreateCell(2).SetCellValue("MIS Number");
workbook.SaveAs(stream); header.CreateCell(3).SetCellValue("MIS Revision");
return stream.ToArray();
} for (var i = 0; i < operations.Length; i++)
{
private static byte[] CreatePartOperationExcel((string ItemNumber, string OpNumber, string MisNumber, string MisRevision)[] operations) var row = worksheet.CreateRow(i + 1);
{ row.CreateCell(0).SetCellValue(operations[i].ItemNumber);
using var workbook = new XLWorkbook(); row.CreateCell(1).SetCellValue(operations[i].OpNumber);
var worksheet = workbook.Worksheets.Add("Sheet1"); row.CreateCell(2).SetCellValue(operations[i].MisNumber);
worksheet.Cell(1, 1).Value = "Item Number"; row.CreateCell(3).SetCellValue(operations[i].MisRevision);
worksheet.Cell(1, 2).Value = "Operation Number"; }
worksheet.Cell(1, 3).Value = "MIS Number";
worksheet.Cell(1, 4).Value = "MIS Revision"; using var stream = new MemoryStream();
for (var i = 0; i < operations.Length; i++) workbook.Write(stream, leaveOpen: true);
{ return stream.ToArray();
worksheet.Cell(i + 2, 1).Value = operations[i].ItemNumber; }
worksheet.Cell(i + 2, 2).Value = operations[i].OpNumber; }
worksheet.Cell(i + 2, 3).Value = operations[i].MisNumber;
worksheet.Cell(i + 2, 4).Value = operations[i].MisRevision;
}
using var stream = new MemoryStream();
workbook.SaveAs(stream);
return stream.ToArray();
}
}
@@ -1,96 +1,80 @@
using ClosedXML.Excel; using JdeScoping.ExcelIO.Templates;
using JdeScoping.ExcelIO.Templates; using JdeScoping.ExcelIO.Tests.Fixtures;
using Shouldly; using Shouldly;
using Xunit; using Xunit;
namespace JdeScoping.ExcelIO.Tests.Templates; namespace JdeScoping.ExcelIO.Tests.Templates;
public class ExcelTemplateServiceTests public class ExcelTemplateServiceTests
{ {
private readonly ExcelTemplateService _service = new(); private readonly ExcelTemplateService _service = new();
[Fact] [Fact]
public void GenerateSingleColumn_CreatesValidExcel() public void GenerateSingleColumn_CreatesValidExcel()
{ {
// Arrange var data = new[] { 12345L, 67890L };
var data = new[] { 12345L, 67890L };
var result = _service.GenerateSingleColumn(data, "Work Order Number");
// Act
var result = _service.GenerateSingleColumn(data, "Work Order Number"); result.ShouldNotBeNull();
result.Length.ShouldBeGreaterThan(0);
// Assert
result.ShouldNotBeNull(); using var workbook = ExcelTestHelpers.OpenWorkbook(result);
result.Length.ShouldBeGreaterThan(0); var worksheet = workbook.GetSheetAt(0);
// Verify content ExcelTestHelpers.GetCellText(worksheet, 1, 1).ShouldBe("Work Order Number");
using var stream = new MemoryStream(result); ExcelTestHelpers.GetCellText(worksheet, 2, 1).ShouldBe("12345");
using var workbook = new XLWorkbook(stream); ExcelTestHelpers.GetCellText(worksheet, 3, 1).ShouldBe("67890");
var worksheet = workbook.Worksheet(1); }
worksheet.Cell(1, 1).GetString().ShouldBe("Work Order Number"); [Fact]
worksheet.Cell(2, 1).GetString().ShouldBe("12345"); public void GenerateMultiColumn_CreatesValidExcel()
worksheet.Cell(3, 1).GetString().ShouldBe("67890"); {
} var data = new[]
{
[Fact] new object?[] { "ITEM-001", "Description 1" },
public void GenerateMultiColumn_CreatesValidExcel() new object?[] { "ITEM-002", "Description 2" }
{ };
// Arrange var headers = new[] { "Item Number", "Description" };
var data = new[]
{ var result = _service.GenerateMultiColumn(data, headers);
new object?[] { "ITEM-001", "Description 1" },
new object?[] { "ITEM-002", "Description 2" } result.ShouldNotBeNull();
}; result.Length.ShouldBeGreaterThan(0);
var headers = new[] { "Item Number", "Description" };
using var workbook = ExcelTestHelpers.OpenWorkbook(result);
// Act var worksheet = workbook.GetSheetAt(0);
var result = _service.GenerateMultiColumn(data, headers);
ExcelTestHelpers.GetCellText(worksheet, 1, 1).ShouldBe("Item Number");
// Assert ExcelTestHelpers.GetCellText(worksheet, 1, 2).ShouldBe("Description");
result.ShouldNotBeNull(); ExcelTestHelpers.GetCellText(worksheet, 2, 1).ShouldBe("ITEM-001");
result.Length.ShouldBeGreaterThan(0); ExcelTestHelpers.GetCellText(worksheet, 2, 2).ShouldBe("Description 1");
}
// Verify content
using var stream = new MemoryStream(result); [Fact]
using var workbook = new XLWorkbook(stream); public void GenerateSingleColumn_HandlesEmptyData()
var worksheet = workbook.Worksheet(1); {
var result = _service.GenerateSingleColumn(Array.Empty<string>(), "Header");
worksheet.Cell(1, 1).GetString().ShouldBe("Item Number");
worksheet.Cell(1, 2).GetString().ShouldBe("Description"); result.ShouldNotBeNull();
worksheet.Cell(2, 1).GetString().ShouldBe("ITEM-001"); result.Length.ShouldBeGreaterThan(0);
worksheet.Cell(2, 2).GetString().ShouldBe("Description 1"); }
}
[Fact]
[Fact] public void GenerateMultiColumn_HandlesNullValues()
public void GenerateSingleColumn_HandlesEmptyData() {
{ var data = new[]
// Act {
var result = _service.GenerateSingleColumn(Array.Empty<string>(), "Header"); new object?[] { "ITEM-001", null }
};
// Assert var headers = new[] { "Item", "Value" };
result.ShouldNotBeNull();
result.Length.ShouldBeGreaterThan(0); var result = _service.GenerateMultiColumn(data, headers);
}
result.ShouldNotBeNull();
[Fact] using var workbook = ExcelTestHelpers.OpenWorkbook(result);
public void GenerateMultiColumn_HandlesNullValues() var worksheet = workbook.GetSheetAt(0);
{
// Arrange ExcelTestHelpers.GetCellText(worksheet, 2, 2).ShouldBe(string.Empty);
var data = new[] }
{ }
new object?[] { "ITEM-001", null }
};
var headers = new[] { "Item", "Value" };
// Act
var result = _service.GenerateMultiColumn(data, headers);
// Assert
result.ShouldNotBeNull();
using var stream = new MemoryStream(result);
using var workbook = new XLWorkbook(stream);
var worksheet = workbook.Worksheet(1);
worksheet.Cell(2, 2).GetString().ShouldBe(string.Empty);
}
}
@@ -1,80 +1,78 @@
using ClosedXML.Excel; using JdeScoping.ExcelIO.Formatting;
using JdeScoping.ExcelIO.Formatting; using JdeScoping.ExcelIO.Tests.Fixtures;
using Shouldly; using NPOI.XSSF.UserModel;
using Xunit; using Shouldly;
using Xunit;
namespace JdeScoping.ExcelIO.Tests;
namespace JdeScoping.ExcelIO.Tests;
public class WorksheetProtectorTests
{ public class WorksheetProtectorTests
[Fact] {
public void ApplyProtection_ProtectsWorksheet() [Fact]
{ public void ApplyProtection_ProtectsWorksheet()
using var workbook = new XLWorkbook(); {
var worksheet = workbook.Worksheets.Add("Test"); using var workbook = new XSSFWorkbook();
var worksheet = (XSSFSheet)workbook.CreateSheet("Test");
WorksheetProtector.ApplyProtection(worksheet, "TestPassword");
WorksheetProtector.ApplyProtection(worksheet, "TestPassword");
worksheet.Protection.IsProtected.ShouldBeTrue();
} worksheet.IsSheetLocked.ShouldBeTrue();
}
[Fact]
public void ApplyProtection_AllowsSpecifiedOperations() [Fact]
{ public void ApplyProtection_AllowsSpecifiedOperations()
using var workbook = new XLWorkbook(); {
var worksheet = workbook.Worksheets.Add("Test"); using var workbook = new XSSFWorkbook();
var worksheet = (XSSFSheet)workbook.CreateSheet("Test");
WorksheetProtector.ApplyProtection(worksheet, "TestPassword");
WorksheetProtector.ApplyProtection(worksheet, "TestPassword");
// Check that specified operations are allowed
worksheet.Protection.AllowedElements.HasFlag(XLSheetProtectionElements.DeleteColumns).ShouldBeTrue(); worksheet.IsDeleteColumnsLocked.ShouldBeFalse();
worksheet.Protection.AllowedElements.HasFlag(XLSheetProtectionElements.DeleteRows).ShouldBeTrue(); worksheet.IsDeleteRowsLocked.ShouldBeFalse();
worksheet.Protection.AllowedElements.HasFlag(XLSheetProtectionElements.AutoFilter).ShouldBeTrue(); worksheet.IsAutoFilterLocked.ShouldBeFalse();
worksheet.Protection.AllowedElements.HasFlag(XLSheetProtectionElements.FormatCells).ShouldBeTrue(); worksheet.IsFormatCellsLocked.ShouldBeFalse();
worksheet.Protection.AllowedElements.HasFlag(XLSheetProtectionElements.FormatColumns).ShouldBeTrue(); worksheet.IsFormatColumnsLocked.ShouldBeFalse();
worksheet.Protection.AllowedElements.HasFlag(XLSheetProtectionElements.FormatRows).ShouldBeTrue(); worksheet.IsFormatRowsLocked.ShouldBeFalse();
worksheet.Protection.AllowedElements.HasFlag(XLSheetProtectionElements.SelectLockedCells).ShouldBeTrue(); worksheet.IsSelectLockedCellsLocked.ShouldBeFalse();
worksheet.Protection.AllowedElements.HasFlag(XLSheetProtectionElements.SelectUnlockedCells).ShouldBeTrue(); worksheet.IsSelectUnlockedCellsLocked.ShouldBeFalse();
worksheet.Protection.AllowedElements.HasFlag(XLSheetProtectionElements.EditObjects).ShouldBeTrue(); worksheet.IsObjectsLocked.ShouldBeFalse();
worksheet.Protection.AllowedElements.HasFlag(XLSheetProtectionElements.Sort).ShouldBeTrue(); worksheet.IsSortLocked.ShouldBeFalse();
} }
[Fact] [Fact]
public void ApplyProtection_AllowsDeleteRows() public void ApplyProtection_AllowsDeleteRows()
{ {
using var workbook = new XLWorkbook(); using var workbook = new XSSFWorkbook();
var worksheet = workbook.Worksheets.Add("Test"); var worksheet = (XSSFSheet)workbook.CreateSheet("Test");
WorksheetProtector.ApplyProtection(worksheet, "TestPassword"); WorksheetProtector.ApplyProtection(worksheet, "TestPassword");
// DeleteRows should be allowed worksheet.IsDeleteRowsLocked.ShouldBeFalse();
worksheet.Protection.AllowedElements.HasFlag(XLSheetProtectionElements.DeleteRows).ShouldBeTrue(); }
}
[Fact]
[Fact] public void ApplyCriteriaProtection_ProtectsWorksheet()
public void ApplyCriteriaProtection_ProtectsWorksheet() {
{ using var workbook = new XSSFWorkbook();
using var workbook = new XLWorkbook(); var worksheet = (XSSFSheet)workbook.CreateSheet("Test");
var worksheet = workbook.Worksheets.Add("Test");
WorksheetProtector.ApplyCriteriaProtection(worksheet, "CriteriaPassword");
WorksheetProtector.ApplyCriteriaProtection(worksheet, "CriteriaPassword");
worksheet.IsSheetLocked.ShouldBeTrue();
worksheet.Protection.IsProtected.ShouldBeTrue(); }
}
[Fact]
[Fact] public void UnlockExtensionArea_UnlocksSpecifiedRange()
public void UnlockExtensionArea_UnlocksSpecifiedRange() {
{ using var workbook = new XSSFWorkbook();
using var workbook = new XLWorkbook(); var worksheet = (XSSFSheet)workbook.CreateSheet("Test");
var worksheet = workbook.Worksheets.Add("Test");
WorksheetProtector.UnlockExtensionArea(worksheet, 10, 5, 100, 100);
// First, set some cells to locked (default)
worksheet.Range(1, 1, 10, 5).Style.Protection.Locked = true; var extensionStyle = worksheet.GetColumnStyle(5);
extensionStyle.IsLocked.ShouldBeFalse();
WorksheetProtector.UnlockExtensionArea(worksheet, 10, 5, 100, 100);
var extensionCell = ExcelTestHelpers.GetCell(worksheet, 1, 6)!;
// Extension area should be unlocked extensionCell.CellStyle.IsLocked.ShouldBeFalse();
var extensionCell = worksheet.Cell(1, 6); }
extensionCell.Style.Protection.Locked.ShouldBeFalse(); }
}
}