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 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 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 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 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 BuildPrintedGroups(IEnumerable lines, bool includeClosedNotes) { return (lines ?? Enumerable.Empty()) .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 group) { return group.Key.InstrumentType; }, StringComparer.OrdinalIgnoreCase) .ThenBy(delegate(IGrouping group) { return group.Key.RegistryNumber; }, StringComparer.OrdinalIgnoreCase) .ThenBy(delegate(IGrouping group) { return group.Key.RangeText; }, StringComparer.OrdinalIgnoreCase) .Select(delegate(IGrouping 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 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 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 lines) { var parts = new List(); 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 { 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(); } } } }