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