This commit is contained in:
Курнат Андрей
2026-03-19 23:31:41 +03:00
parent ce3a3f02d2
commit a47a7a5a3b
104 changed files with 21982 additions and 0 deletions

587
XLAB2/PsvPrintService.cs Normal file
View File

@@ -0,0 +1,587 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
namespace XLAB2
{
internal sealed class PsvPrintService
{
private const int PrintDialogId = 88;
private const int WordAlertsNone = 0;
private const int WordCloseDoNotSaveChanges = 0;
public void PrintDocument(PsvDocumentSummary document, IReadOnlyList<PsvDocumentLine> lines)
{
if (document == null)
{
throw new ArgumentNullException("document");
}
if (lines == null)
{
throw new ArgumentNullException("lines");
}
if (lines.Count == 0)
{
throw new InvalidOperationException("В выбранном ПСВ нет строк для печати.");
}
var templateFileName = document.IssuedOn.HasValue ? "ClosePsv.docx" : "OpenPsv.docx";
PrintWordTemplate(
templateFileName,
document.DocumentNumber,
delegate(object wordDocument)
{
PopulatePsvTemplate((dynamic)wordDocument, document, lines);
});
}
public void PrintVerificationDocument(PsvDocumentLine line)
{
if (line == null)
{
throw new ArgumentNullException("line");
}
if (!line.IsPassed.HasValue)
{
throw new InvalidOperationException("Для печати документа о поверке не указан результат поверки.");
}
if (string.IsNullOrWhiteSpace(line.VerificationDocumentNumber))
{
throw new InvalidOperationException("Для печати документа о поверке не указан номер документа.");
}
if (!line.VerificationPerformedOn.HasValue && !line.VerificationDocumentDate.HasValue)
{
throw new InvalidOperationException("Для печати документа о поверке не указана дата поверки.");
}
if (!line.IsPassed.Value && string.IsNullOrWhiteSpace(line.RejectionReason))
{
throw new InvalidOperationException("Для печати извещения о непригодности не указана причина непригодности.");
}
var templateFileName = line.IsPassed.Value ? "Svid.docx" : "Izv.docx";
PrintWordTemplate(
templateFileName,
line.VerificationDocumentNumber,
delegate(object wordDocument)
{
PopulateVerificationTemplate((dynamic)wordDocument, line);
});
}
private static void PrintWordTemplate(string templateFileName, string printNumber, Action<object> populateDocument)
{
if (string.IsNullOrWhiteSpace(templateFileName))
{
throw new ArgumentException("Не указано имя шаблона печати.", "templateFileName");
}
if (populateDocument == null)
{
throw new ArgumentNullException("populateDocument");
}
var templatePath = ResolveTemplatePath(templateFileName);
var workingCopyPath = CreateWorkingCopy(templatePath, printNumber);
object wordApplication = null;
object wordDocument = null;
object dialogs = null;
object printDialog = null;
try
{
var wordType = Type.GetTypeFromProgID("Word.Application");
if (wordType == null)
{
throw new InvalidOperationException("Microsoft Word не найден. Печать ПСВ по шаблону DOCX недоступна.");
}
wordApplication = Activator.CreateInstance(wordType);
dynamic word = wordApplication;
word.Visible = true;
word.DisplayAlerts = WordAlertsNone;
wordDocument = word.Documents.Open(
FileName: workingCopyPath,
ConfirmConversions: false,
ReadOnly: false,
AddToRecentFiles: false,
Visible: true);
populateDocument(wordDocument);
((dynamic)wordDocument).Save();
word.Activate();
((dynamic)wordDocument).Activate();
dialogs = word.Dialogs;
printDialog = ((dynamic)dialogs).Item(PrintDialogId);
((dynamic)printDialog).Show();
((dynamic)wordDocument).Close(WordCloseDoNotSaveChanges);
wordDocument = null;
word.Quit();
wordApplication = null;
}
finally
{
ReleaseComObject(printDialog);
ReleaseComObject(dialogs);
ReleaseComObject(wordDocument);
ReleaseComObject(wordApplication);
TryDeleteFile(workingCopyPath);
}
}
private static void PopulatePsvTemplate(dynamic document, PsvDocumentSummary summary, IReadOnlyList<PsvDocumentLine> lines)
{
var groupedLines = BuildPrintedGroups(lines, summary.IssuedOn.HasValue);
ReplacePlaceholder(document, "number", NormalizeText(summary.DocumentNumber));
ReplacePlaceholder(document, "div", NormalizeText(summary.CustomerName));
ReplacePlaceholder(document, "date", FormatDate(summary.AcceptedOn));
ReplacePlaceholder(document, "count", lines.Count.ToString(CultureInfo.InvariantCulture));
ReplacePlaceholder(document, "person", string.Empty);
if (summary.IssuedOn.HasValue)
{
ReplacePlaceholder(document, "today", FormatDate(summary.IssuedOn));
ReplacePlaceholder(document, "good", lines.Count(delegate(PsvDocumentLine line) { return line.IsPassed == true; }).ToString(CultureInfo.InvariantCulture));
ReplacePlaceholder(document, "bad", lines.Count(delegate(PsvDocumentLine line) { return line.IsPassed == false; }).ToString(CultureInfo.InvariantCulture));
}
else
{
var dueDate = summary.AcceptedOn.HasValue
? summary.AcceptedOn.Value.AddDays(30)
: (DateTime?)null;
ReplacePlaceholder(document, "next", FormatDate(dueDate));
}
FillTable(document, groupedLines);
}
private static void PopulateVerificationTemplate(dynamic document, PsvDocumentLine line)
{
var verificationDate = ResolveVerificationDate(line);
ReplacePlaceholder(document, "number", NormalizeText(line.VerificationDocumentNumber));
ReplacePlaceholder(document, "nextpovdate", FormatDate(ResolveNextVerificationDate(line, verificationDate)));
ReplacePlaceholder(document, "name", NormalizeText(line.InstrumentName));
ReplacePlaceholder(document, "type (рег. № gr)", BuildVerificationTypeText(line));
ReplacePlaceholder(document, "serial", NormalizeText(line.SerialNumber));
ReplacePlaceholder(document, "erial", NormalizeText(line.SerialNumber));
ReplacePlaceholder(document, "method", string.Empty);
ReplacePlaceholder(document, "temp", string.Empty);
ReplacePlaceholder(document, "hum", string.Empty);
ReplacePlaceholder(document, "press", string.Empty);
ReplacePlaceholder(document, "person", NormalizeText(line.VerifierName));
ReplacePlaceholder(document, "reason", NormalizeText(line.RejectionReason));
ReplacePlaceholder(document, "date", FormatDate(verificationDate));
}
private static void FillTable(dynamic document, IReadOnlyList<PrintedGroupRow> rowsToPrint)
{
dynamic table = null;
try
{
table = document.Tables.Item(1);
for (var index = 0; index < rowsToPrint.Count; index++)
{
var rowData = rowsToPrint[index];
dynamic row = null;
try
{
row = table.Rows.Add();
SetCellText(row, 1, (index + 1).ToString(CultureInfo.InvariantCulture), true);
SetCellText(row, 2, rowData.InstrumentName, false);
SetCellText(row, 3, rowData.InstrumentType, false);
SetCellText(row, 4, rowData.RangeText, false);
SetCellText(row, 5, rowData.SerialNumberText, false);
SetCellText(row, 6, rowData.GroupCount.ToString(CultureInfo.InvariantCulture), true);
SetCellText(row, 7, rowData.PassedCount > 0 ? rowData.PassedCount.ToString(CultureInfo.InvariantCulture) : string.Empty, true);
SetCellText(row, 8, rowData.FailedCount > 0 ? rowData.FailedCount.ToString(CultureInfo.InvariantCulture) : string.Empty, true);
SetCellText(row, 9, rowData.Notes, false);
}
finally
{
ReleaseComObject(row);
}
}
}
finally
{
ReleaseComObject(table);
}
}
private static void SetCellText(dynamic row, int columnIndex, string value, bool centerAlign)
{
dynamic cell = null;
try
{
cell = row.Cells.Item(columnIndex);
cell.Range.Text = NormalizeText(value);
cell.Range.Bold = 0;
cell.Range.Font.Underline = 0;
cell.Range.ParagraphFormat.Alignment = centerAlign ? 1 : 0;
}
finally
{
ReleaseComObject(cell);
}
}
private static void ReplacePlaceholder(dynamic document, string placeholder, string replacement)
{
dynamic range = null;
dynamic find = null;
try
{
range = document.Content;
find = range.Find;
find.ClearFormatting();
find.Replacement.ClearFormatting();
find.Execute(
FindText: placeholder,
MatchCase: false,
MatchWholeWord: false,
MatchWildcards: false,
MatchSoundsLike: false,
MatchAllWordForms: false,
Forward: true,
Wrap: 1,
Format: false,
ReplaceWith: NormalizeText(replacement),
Replace: 2);
}
finally
{
ReleaseComObject(find);
ReleaseComObject(range);
}
}
private static IReadOnlyList<PrintedGroupRow> BuildPrintedGroups(IEnumerable<PsvDocumentLine> lines, bool includeClosedNotes)
{
return (lines ?? Enumerable.Empty<PsvDocumentLine>())
.GroupBy(delegate(PsvDocumentLine line)
{
return new PrintGroupKey
{
InstrumentType = NormalizeText(line == null ? null : line.InstrumentType),
RangeText = NormalizeText(line == null ? null : line.RangeText),
RegistryNumber = NormalizeText(line == null ? null : line.RegistryNumber)
};
})
.OrderBy(delegate(IGrouping<PrintGroupKey, PsvDocumentLine> group) { return group.Key.InstrumentType; }, StringComparer.OrdinalIgnoreCase)
.ThenBy(delegate(IGrouping<PrintGroupKey, PsvDocumentLine> group) { return group.Key.RegistryNumber; }, StringComparer.OrdinalIgnoreCase)
.ThenBy(delegate(IGrouping<PrintGroupKey, PsvDocumentLine> group) { return group.Key.RangeText; }, StringComparer.OrdinalIgnoreCase)
.Select(delegate(IGrouping<PrintGroupKey, PsvDocumentLine> group)
{
var materializedLines = group.Where(delegate(PsvDocumentLine line) { return line != null; }).ToList();
return new PrintedGroupRow
{
InstrumentName = BuildInstrumentNameText(materializedLines),
InstrumentType = group.Key.InstrumentType,
RangeText = group.Key.RangeText,
SerialNumberText = BuildSerialNumberText(materializedLines),
GroupCount = materializedLines.Count,
PassedCount = materializedLines.Count(delegate(PsvDocumentLine line) { return line.IsPassed == true; }),
FailedCount = materializedLines.Count(delegate(PsvDocumentLine line) { return line.IsPassed == false; }),
Notes = includeClosedNotes ? BuildClosedNotesText(materializedLines) : string.Empty
};
})
.ToList();
}
private static string BuildInstrumentNameText(IReadOnlyList<PsvDocumentLine> lines)
{
return string.Join("; ", lines
.Select(delegate(PsvDocumentLine line) { return NormalizeText(line.InstrumentName); })
.Where(delegate(string value) { return !string.IsNullOrWhiteSpace(value); })
.Distinct(StringComparer.OrdinalIgnoreCase)
.OrderBy(delegate(string value) { return value; }, StringComparer.OrdinalIgnoreCase)
.ToArray());
}
private static string BuildSerialNumberText(IReadOnlyList<PsvDocumentLine> lines)
{
var serialNumbers = lines
.Select(delegate(PsvDocumentLine line) { return NormalizeText(line.SerialNumber); })
.Where(delegate(string value) { return !string.IsNullOrWhiteSpace(value); })
.Distinct(StringComparer.OrdinalIgnoreCase)
.OrderBy(delegate(string value) { return value; }, StringComparer.OrdinalIgnoreCase)
.ToList();
if (serialNumbers.Count == 0)
{
return string.Empty;
}
if (serialNumbers.Count > 3)
{
return string.Format(CultureInfo.InvariantCulture, "{0} зав. номеров", serialNumbers.Count);
}
return string.Join(", ", serialNumbers.ToArray());
}
private static string BuildClosedNotesText(IReadOnlyList<PsvDocumentLine> lines)
{
var parts = new List<string>();
var verificationDocuments = lines
.Select(FormatVerificationDocument)
.Where(delegate(string value) { return !string.IsNullOrWhiteSpace(value); })
.Distinct(StringComparer.OrdinalIgnoreCase)
.OrderBy(delegate(string value) { return value; }, StringComparer.OrdinalIgnoreCase)
.ToList();
if (verificationDocuments.Count > 0)
{
parts.Add("Документы: " + string.Join("; ", verificationDocuments.ToArray()));
}
var stickerNumbers = lines
.Select(delegate(PsvDocumentLine line) { return NormalizeText(line.StickerNumber); })
.Where(delegate(string value) { return !string.IsNullOrWhiteSpace(value); })
.Distinct(StringComparer.OrdinalIgnoreCase)
.OrderBy(delegate(string value) { return value; }, StringComparer.OrdinalIgnoreCase)
.ToList();
if (stickerNumbers.Count > 0)
{
parts.Add("Наклейки: " + string.Join(", ", stickerNumbers.ToArray()));
}
return string.Join(". ", parts.ToArray());
}
private static string FormatVerificationDocument(PsvDocumentLine line)
{
if (line == null)
{
return string.Empty;
}
var number = NormalizeText(line.VerificationDocumentNumber);
var date = FormatDate(line.VerificationDocumentDate);
if (string.IsNullOrWhiteSpace(number))
{
return date;
}
if (string.IsNullOrWhiteSpace(date))
{
return number;
}
return string.Format(CultureInfo.InvariantCulture, "{0} от {1}", number, date);
}
private static string BuildVerificationTypeText(PsvDocumentLine line)
{
if (line == null)
{
return string.Empty;
}
var type = NormalizeText(line.InstrumentType);
var registryNumber = NormalizeText(line.RegistryNumber);
if (string.IsNullOrWhiteSpace(type))
{
return string.Empty;
}
return string.IsNullOrWhiteSpace(registryNumber)
? type
: string.Format(CultureInfo.InvariantCulture, "{0} (рег. № {1})", type, registryNumber);
}
private static DateTime? ResolveVerificationDate(PsvDocumentLine line)
{
if (line == null)
{
return null;
}
return line.VerificationPerformedOn ?? line.VerificationDocumentDate;
}
private static DateTime? ResolveNextVerificationDate(PsvDocumentLine line, DateTime? verificationDate)
{
if (line == null || !line.IsPassed.HasValue || !line.IsPassed.Value)
{
return null;
}
if (!verificationDate.HasValue || line.PeriodMonths <= 0)
{
return null;
}
return verificationDate.Value.AddMonths(line.PeriodMonths);
}
private static string ResolveTemplatePath(string fileName)
{
var baseDirectory = AppDomain.CurrentDomain.BaseDirectory;
var candidates = new[]
{
Path.Combine(baseDirectory, fileName),
Path.GetFullPath(Path.Combine(baseDirectory, "..", "..", fileName)),
Path.GetFullPath(Path.Combine(baseDirectory, "..", "..", "..", fileName))
};
var templatePath = candidates.FirstOrDefault(File.Exists);
if (templatePath == null)
{
throw new FileNotFoundException(string.Format("Не найден шаблон печати ПСВ: {0}.", fileName), fileName);
}
return templatePath;
}
private static string CreateWorkingCopy(string templatePath, string documentNumber)
{
var tempDirectory = Path.Combine(Path.GetTempPath(), "XLAB", "Print");
Directory.CreateDirectory(tempDirectory);
var safeDocumentNumber = string.IsNullOrWhiteSpace(documentNumber)
? "PSV"
: string.Concat(documentNumber.Where(delegate(char ch)
{
return !Path.GetInvalidFileNameChars().Contains(ch);
}));
if (string.IsNullOrWhiteSpace(safeDocumentNumber))
{
safeDocumentNumber = "PSV";
}
var tempFileName = string.Format(
CultureInfo.InvariantCulture,
"{0}_{1:yyyyMMdd_HHmmss_fff}_{2}",
safeDocumentNumber,
DateTime.Now,
Path.GetFileName(templatePath));
var workingCopyPath = Path.Combine(tempDirectory, tempFileName);
File.Copy(templatePath, workingCopyPath, true);
return workingCopyPath;
}
private static string FormatDate(DateTime? value)
{
return value.HasValue
? value.Value.ToString("dd.MM.yyyy", CultureInfo.InvariantCulture)
: string.Empty;
}
private static string NormalizeText(string value)
{
return string.IsNullOrWhiteSpace(value) ? string.Empty : value.Trim();
}
private static void TryDeleteFile(string path)
{
if (string.IsNullOrWhiteSpace(path) || !File.Exists(path))
{
return;
}
try
{
File.Delete(path);
}
catch (IOException)
{
}
catch (UnauthorizedAccessException)
{
}
}
private static void ReleaseComObject(object value)
{
if (value == null || !Marshal.IsComObject(value))
{
return;
}
try
{
Marshal.FinalReleaseComObject(value);
}
catch (ArgumentException)
{
}
}
private sealed class PrintedGroupRow
{
public int FailedCount { get; set; }
public int GroupCount { get; set; }
public string InstrumentName { get; set; }
public string InstrumentType { get; set; }
public string Notes { get; set; }
public int PassedCount { get; set; }
public string RangeText { get; set; }
public string SerialNumberText { get; set; }
}
private sealed class PrintGroupKey : IEquatable<PrintGroupKey>
{
public string InstrumentType { get; set; }
public string RangeText { get; set; }
public string RegistryNumber { get; set; }
public bool Equals(PrintGroupKey other)
{
return other != null
&& string.Equals(InstrumentType ?? string.Empty, other.InstrumentType ?? string.Empty, StringComparison.OrdinalIgnoreCase)
&& string.Equals(RangeText ?? string.Empty, other.RangeText ?? string.Empty, StringComparison.OrdinalIgnoreCase)
&& string.Equals(RegistryNumber ?? string.Empty, other.RegistryNumber ?? string.Empty, StringComparison.OrdinalIgnoreCase);
}
public override bool Equals(object obj)
{
return Equals(obj as PrintGroupKey);
}
public override int GetHashCode()
{
return string.Concat(
(InstrumentType ?? string.Empty).ToUpperInvariant(), "|",
(RangeText ?? string.Empty).ToUpperInvariant(), "|",
(RegistryNumber ?? string.Empty).ToUpperInvariant())
.GetHashCode();
}
}
}
}