From d4cc15a7cf6d69f42b7c74bc40cd0fcb3787fc36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9A=D1=83=D1=80=D0=BD=D0=B0=D1=82=20=D0=90=D0=BD=D0=B4?= =?UTF-8?q?=D1=80=D0=B5=D0=B9?= Date: Fri, 13 Mar 2026 21:26:27 +0300 Subject: [PATCH] edit --- XLAB/DialogService.cs | 12 + XLAB/MainWindow.xaml | 2 + XLAB/MainWindowViewModel.cs | 352 +++++++++++++++++- XLAB/PsvDataService.cs | 386 +++++++++++++++++++- XLAB/PsvModels.cs | 41 +++ XLAB/SelectInstrumentTypeWindow.xaml | 93 +++++ XLAB/SelectInstrumentTypeWindow.xaml.cs | 20 + XLAB/SelectInstrumentTypeWindowViewModel.cs | 161 ++++++++ XLAB/XLAB.csproj | 9 + 9 files changed, 1067 insertions(+), 9 deletions(-) create mode 100644 XLAB/SelectInstrumentTypeWindow.xaml create mode 100644 XLAB/SelectInstrumentTypeWindow.xaml.cs create mode 100644 XLAB/SelectInstrumentTypeWindowViewModel.cs diff --git a/XLAB/DialogService.cs b/XLAB/DialogService.cs index 18b2a15..38ba5db 100644 --- a/XLAB/DialogService.cs +++ b/XLAB/DialogService.cs @@ -9,6 +9,8 @@ namespace XLAB IReadOnlyList ShowInstrumentPickerDialog(string customerName, IReadOnlyList instruments); + InstrumentTypeSelectionResult ShowInstrumentTypeDialog(string customerName, IReadOnlyList instrumentTypes); + VerificationEditResult ShowVerificationDialog( VerificationEditSeed seed, IReadOnlyList verifiers, @@ -52,6 +54,16 @@ namespace XLAB return result.HasValue && result.Value ? viewModel.GetSelectedItems() : null; } + public InstrumentTypeSelectionResult ShowInstrumentTypeDialog(string customerName, IReadOnlyList instrumentTypes) + { + var viewModel = new SelectInstrumentTypeWindowViewModel(customerName, instrumentTypes); + var window = new SelectInstrumentTypeWindow(viewModel); + window.Owner = _owner; + + var result = window.ShowDialog(); + return result.HasValue && result.Value ? viewModel.GetResult() : null; + } + public VerificationEditResult ShowVerificationDialog( VerificationEditSeed seed, IReadOnlyList verifiers, diff --git a/XLAB/MainWindow.xaml b/XLAB/MainWindow.xaml index 1dcf9ea..c76974a 100644 --- a/XLAB/MainWindow.xaml +++ b/XLAB/MainWindow.xaml @@ -198,6 +198,8 @@ + diff --git a/XLAB/MainWindowViewModel.cs b/XLAB/MainWindowViewModel.cs index c75341f..daddc08 100644 --- a/XLAB/MainWindowViewModel.cs +++ b/XLAB/MainWindowViewModel.cs @@ -53,6 +53,7 @@ namespace XLAB MarkLinePassedCommand = new RelayCommand(delegate { EditLineVerificationAsync(true); }, delegate { return CanEditSelectedLineVerification(); }); MarkLineRejectedCommand = new RelayCommand(delegate { EditLineVerificationAsync(false); }, delegate { return CanEditSelectedLineVerification(); }); OpenInstrumentPickerCommand = new RelayCommand(delegate { OpenInstrumentPickerAsync(); }, delegate { return !IsBusy && SelectedDocument != null && SelectedDocument.CustomerId.HasValue; }); + OpenInstrumentTypePickerCommand = new RelayCommand(delegate { OpenInstrumentTypePickerAsync(); }, delegate { return !IsBusy && SelectedDocument != null && SelectedDocument.CustomerId.HasValue; }); RefreshDocumentsCommand = new RelayCommand(delegate { RefreshDocumentsAsync(null, null); }, delegate { return !IsBusy; }); ResetLineVerificationCommand = new RelayCommand(delegate { ResetSelectedLineVerificationAsync(); }, delegate { return CanResetSelectedLineVerification(); }); SaveDocumentHeaderCommand = new RelayCommand(delegate { SaveDocumentAsync(); }, delegate { return CanSaveDocument(); }); @@ -169,6 +170,8 @@ namespace XLAB public ICommand OpenInstrumentPickerCommand { get; private set; } + public ICommand OpenInstrumentTypePickerCommand { get; private set; } + public ICommand RefreshDocumentsCommand { get; private set; } public ICommand ResetLineVerificationCommand { get; private set; } @@ -513,12 +516,141 @@ namespace XLAB && source.IndexOf(filter, StringComparison.OrdinalIgnoreCase) >= 0; } + private string BuildOpenDocumentConflictMessage(IEnumerable conflicts) + { + var materializedConflicts = NormalizeOpenDocumentConflicts(conflicts); + if (materializedConflicts.Count == 0) + { + return "Прибор уже находится в другой открытой ПСВ этого заказчика."; + } + + var preview = materializedConflicts + .Take(5) + .Select(delegate(OpenDocumentConflictInfo conflict) + { + return string.Format("зав. № {0} -> {1}", conflict.SerialNumber, conflict.DocumentNumber); + }) + .ToList(); + + var suffix = materializedConflicts.Count > preview.Count + ? string.Format(" Еще конфликтов: {0}.", materializedConflicts.Count - preview.Count) + : string.Empty; + + return string.Format( + "Приборы уже находятся в других открытых ПСВ этого заказчика: {0}.{1}", + string.Join("; ", preview.ToArray()), + suffix); + } + + private List FindOpenDocumentConflicts(IEnumerable candidateLines) + { + if (SelectedDocument == null || !SelectedDocument.CustomerId.HasValue || candidateLines == null) + { + return new List(); + } + + var conflicts = FindPendingOpenDocumentConflicts(SelectedDocument, candidateLines); + conflicts.AddRange(_service.FindOpenDocumentConflicts( + SelectedDocument.CustomerId.Value, + SelectedDocument.IsDraft ? null : SelectedDocument.DocumentNumber, + candidateLines)); + + return NormalizeOpenDocumentConflicts(conflicts); + } + + private List FindPendingOpenDocumentConflicts(PsvDocumentSummary currentDocument, IEnumerable candidateLines) + { + if (currentDocument == null || !currentDocument.CustomerId.HasValue || candidateLines == null) + { + return new List(); + } + + var candidateKeys = new HashSet( + candidateLines + .Where(delegate(PsvDocumentLine line) + { + return line != null + && line.TypeSizeId > 0 + && !string.IsNullOrWhiteSpace(line.SerialNumber); + }) + .Select(delegate(PsvDocumentLine line) { return line.OpenDocumentConflictKey; }), + StringComparer.OrdinalIgnoreCase); + + if (candidateKeys.Count == 0) + { + return new List(); + } + + var conflicts = new List(); + + foreach (var pendingLinesByDocument in _pendingLinesByDocumentKey) + { + if (string.Equals(pendingLinesByDocument.Key, currentDocument.DocumentKey, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + var otherDocument = Documents.FirstOrDefault(delegate(PsvDocumentSummary document) + { + return string.Equals(document.DocumentKey, pendingLinesByDocument.Key, StringComparison.OrdinalIgnoreCase); + }); + + if (otherDocument == null + || !otherDocument.CustomerId.HasValue + || otherDocument.CustomerId.Value != currentDocument.CustomerId.Value) + { + continue; + } + + foreach (var otherLine in pendingLinesByDocument.Value.Where(delegate(PsvDocumentLine line) + { + return line != null + && line.TypeSizeId > 0 + && !string.IsNullOrWhiteSpace(line.SerialNumber); + })) + { + if (!candidateKeys.Contains(otherLine.OpenDocumentConflictKey)) + { + continue; + } + + conflicts.Add(new OpenDocumentConflictInfo + { + DocumentNumber = otherDocument.DocumentNumber, + TypeSizeId = otherLine.TypeSizeId, + SerialNumber = otherLine.SerialNumber + }); + } + } + + return NormalizeOpenDocumentConflicts(conflicts); + } + + private static List NormalizeOpenDocumentConflicts(IEnumerable conflicts) + { + return (conflicts ?? Enumerable.Empty()) + .Where(delegate(OpenDocumentConflictInfo conflict) { return conflict != null; }) + .GroupBy(delegate(OpenDocumentConflictInfo conflict) + { + return string.Format( + "{0}|{1}|{2}", + conflict.DocumentNumber ?? string.Empty, + conflict.TypeSizeId, + conflict.SerialNumber ?? string.Empty); + }, StringComparer.OrdinalIgnoreCase) + .Select(delegate(IGrouping group) { return group.First(); }) + .OrderBy(delegate(OpenDocumentConflictInfo conflict) { return conflict.SerialNumber ?? string.Empty; }) + .ThenBy(delegate(OpenDocumentConflictInfo conflict) { return conflict.DocumentNumber ?? string.Empty; }) + .ToList(); + } + private PsvDocumentLine CreatePendingLine(AvailableInstrumentItem item) { return new PsvDocumentLine { CardId = 0, InstrumentId = item.InstrumentId, + TypeSizeId = item.TypeSizeId, SerialNumber = item.SerialNumber, InventoryNumber = item.InventoryNumber, CustomerName = item.CustomerName, @@ -547,6 +679,41 @@ namespace XLAB }; } + private PsvDocumentLine CreatePendingTypeLine(AvailableInstrumentItem item, string serialNumber) + { + return new PsvDocumentLine + { + CardId = 0, + InstrumentId = 0, + TypeSizeId = item.TypeSizeId, + SerialNumber = string.IsNullOrWhiteSpace(serialNumber) ? string.Empty : serialNumber.Trim(), + InventoryNumber = string.Empty, + CustomerName = SelectedDocument == null ? string.Empty : SelectedDocument.CustomerName, + InstrumentType = item.InstrumentType, + InstrumentName = item.InstrumentName, + MeasurementArea = item.MeasurementArea, + RangeText = item.RangeText, + RegistryNumber = item.RegistryNumber, + AccuracyText = item.AccuracyText, + VerificationType = string.Empty, + PeriodMonths = 0, + AcceptedOn = HeaderReceivedOn, + IssuedOn = null, + IsPassed = null, + VerificationPerformedOn = null, + VerifierId = null, + VerifierName = string.Empty, + StickerNumber = string.Empty, + VerificationDocumentFormId = null, + VerificationDocumentLinkTypeId = null, + VerificationDocumentNumber = string.Empty, + VerificationDocumentDate = null, + RejectionReason = string.Empty, + Notes = string.Empty, + IsPendingInsert = true + }; + } + private static void ApplyVerificationResultCore(PsvDocumentLine line, VerificationEditResult result) { if (line == null || result == null) @@ -1146,6 +1313,50 @@ namespace XLAB AddSelectedInstruments(selectedItems); } + private void OpenInstrumentTypePickerAsync() + { + if (SelectedDocument == null) + { + return; + } + + if (!SelectedDocument.CustomerId.HasValue) + { + _dialogService.ShowWarning("Сначала выберите заказчика в ПСВ."); + return; + } + + OpenInstrumentTypePickerCoreAsync(SelectedDocument.CustomerName); + } + + private async void OpenInstrumentTypePickerCoreAsync(string customerName) + { + IReadOnlyList instrumentTypes; + + try + { + IsBusy = true; + instrumentTypes = await Task.Run(delegate { return _service.LoadInstrumentTypes(); }); + } + catch (Exception ex) + { + _dialogService.ShowError(ex.Message); + return; + } + finally + { + IsBusy = false; + } + + var result = _dialogService.ShowInstrumentTypeDialog(customerName, instrumentTypes); + if (result == null || result.TypeItem == null) + { + return; + } + + AddSelectedTypeInstrument(result); + } + private void AddSelectedInstruments(IReadOnlyList selectedItems) { if (SelectedDocument == null) @@ -1160,13 +1371,45 @@ namespace XLAB _pendingLinesByDocumentKey[SelectedDocument.DocumentKey] = pendingLines; } + var candidateLines = selectedItems + .Where(delegate(AvailableInstrumentItem item) { return item != null; }) + .Select(CreatePendingLine) + .ToList(); + + List openDocumentConflicts; + try + { + openDocumentConflicts = FindOpenDocumentConflicts(candidateLines); + } + catch (Exception ex) + { + _dialogService.ShowError(ex.Message); + return; + } + + var openConflictKeys = new HashSet( + openDocumentConflicts.Select(delegate(OpenDocumentConflictInfo conflict) { return conflict.OpenDocumentConflictKey; }), + StringComparer.OrdinalIgnoreCase); + var duplicateKeys = new HashSet(DocumentLines.Select(delegate(PsvDocumentLine line) { return line.DuplicateKey; }), StringComparer.OrdinalIgnoreCase); var addedCount = 0; var skippedDuplicateCount = 0; + var skippedOpenDocumentCount = 0; var skippedWithoutTemplateCount = 0; foreach (var item in selectedItems) { + if (item == null) + { + continue; + } + + if (openConflictKeys.Contains(PsvDocumentLine.BuildOpenDocumentConflictKey(item.TypeSizeId, item.SerialNumber))) + { + skippedOpenDocumentCount++; + continue; + } + if (!item.HasTemplate) { skippedWithoutTemplateCount++; @@ -1208,11 +1451,98 @@ namespace XLAB messages.Add(string.Format("Пропущено без шаблона EKZMK: {0}.", skippedWithoutTemplateCount)); } - if (messages.Count > 0) + if (skippedOpenDocumentCount > 0) { - _dialogService.ShowInfo(string.Join(" ", messages.ToArray())); + messages.Add(string.Format("Пропущено из-за других открытых ПСВ: {0}.", skippedOpenDocumentCount)); + messages.Add(BuildOpenDocumentConflictMessage(openDocumentConflicts)); } + if (messages.Count > 0) + { + var message = string.Join(" ", messages.ToArray()); + if (addedCount == 0 && skippedOpenDocumentCount > 0) + { + _dialogService.ShowWarning(message); + } + else + { + _dialogService.ShowInfo(message); + } + } + + RaiseCommandStates(); + OnPropertyChanged("IsCustomerEditable"); + } + + private void AddSelectedTypeInstrument(InstrumentTypeSelectionResult result) + { + if (SelectedDocument == null || result == null || result.TypeItem == null) + { + return; + } + + List pendingLines; + if (!_pendingLinesByDocumentKey.TryGetValue(SelectedDocument.DocumentKey, out pendingLines)) + { + pendingLines = new List(); + _pendingLinesByDocumentKey[SelectedDocument.DocumentKey] = pendingLines; + } + + var serialNumber = string.IsNullOrWhiteSpace(result.SerialNumber) ? string.Empty : result.SerialNumber.Trim(); + if (string.IsNullOrWhiteSpace(serialNumber)) + { + _dialogService.ShowWarning("Введите заводской номер."); + return; + } + + if (!result.TypeItem.HasTemplate) + { + _dialogService.ShowWarning("Выбранный тип нельзя добавить: для него не найден шаблон EKZMK или период МК."); + return; + } + + var candidateLine = CreatePendingTypeLine(result.TypeItem, serialNumber); + List openDocumentConflicts; + try + { + openDocumentConflicts = FindOpenDocumentConflicts(new[] { candidateLine }); + } + catch (Exception ex) + { + _dialogService.ShowError(ex.Message); + return; + } + + if (openDocumentConflicts.Count > 0) + { + _dialogService.ShowWarning(BuildOpenDocumentConflictMessage(openDocumentConflicts)); + return; + } + + var duplicateKey = PsvDocumentLine.BuildDuplicateKey( + result.TypeItem.InstrumentType, + result.TypeItem.RangeText, + result.TypeItem.RegistryNumber, + serialNumber); + + if (DocumentLines.Any(delegate(PsvDocumentLine line) + { + return string.Equals(line.DuplicateKey, duplicateKey, StringComparison.OrdinalIgnoreCase); + })) + { + _dialogService.ShowWarning("Такой прибор уже есть в ПСВ."); + return; + } + + pendingLines.Add(candidateLine); + + if (SelectedDocument.IsDraft) + { + SelectedDocument.ItemCount = pendingLines.Count; + } + + LoadSelectedDocumentAsync(); + _dialogService.ShowInfo("Прибор по типу добавлен в ПСВ."); RaiseCommandStates(); OnPropertyChanged("IsCustomerEditable"); } @@ -1254,6 +1584,7 @@ namespace XLAB ((RelayCommand)MarkLinePassedCommand).RaiseCanExecuteChanged(); ((RelayCommand)MarkLineRejectedCommand).RaiseCanExecuteChanged(); ((RelayCommand)OpenInstrumentPickerCommand).RaiseCanExecuteChanged(); + ((RelayCommand)OpenInstrumentTypePickerCommand).RaiseCanExecuteChanged(); ((RelayCommand)RefreshDocumentsCommand).RaiseCanExecuteChanged(); ((RelayCommand)ResetLineVerificationCommand).RaiseCanExecuteChanged(); ((RelayCommand)SaveDocumentHeaderCommand).RaiseCanExecuteChanged(); @@ -1398,7 +1729,12 @@ namespace XLAB } var pendingLines = GetPendingLines(SelectedDocument) - .Where(delegate(PsvDocumentLine line) { return line.InstrumentId > 0; }) + .Where(delegate(PsvDocumentLine line) + { + return line != null + && (line.InstrumentId > 0 + || (line.TypeSizeId > 0 && !string.IsNullOrWhiteSpace(line.SerialNumber))); + }) .ToList(); if (SelectedDocument.IsDraft && pendingLines.Count == 0) { @@ -1406,11 +1742,19 @@ namespace XLAB return; } + var openDocumentConflicts = FindPendingOpenDocumentConflicts(SelectedDocument, pendingLines); + if (openDocumentConflicts.Count > 0) + { + _dialogService.ShowWarning(BuildOpenDocumentConflictMessage(openDocumentConflicts)); + return; + } + var request = new DocumentEditorResult { DocumentNumber = DocumentNumberEditor.Trim(), AcceptedOn = HeaderReceivedOn.Value, - IssuedOn = HeaderIssuedOn + IssuedOn = HeaderIssuedOn, + CustomerId = SelectedDocument.CustomerId ?? SelectedCustomerId }; var currentDocumentNumber = SelectedDocument.IsDraft ? null : SelectedDocument.DocumentNumber; diff --git a/XLAB/PsvDataService.cs b/XLAB/PsvDataService.cs index 0d402d3..ae6bc6f 100644 --- a/XLAB/PsvDataService.cs +++ b/XLAB/PsvDataService.cs @@ -210,6 +210,7 @@ ORDER BY MAX(m.DTPRM) DESC, m.NNZVPV DESC;"; SELECT m.IDEKZMK AS CardId, z.IDEKZ AS InstrumentId, + z.IDTPRZ AS TypeSizeId, z.NNZV AS SerialNumber, z.NNIN AS InventoryNumber, ownerOrg.NMFRPD AS CustomerName, @@ -279,6 +280,7 @@ ORDER BY areas.NMOI, names.NMTP, tips.TP, z.NNZV;"; { CardId = GetInt32(reader, "CardId"), InstrumentId = GetInt32(reader, "InstrumentId"), + TypeSizeId = GetInt32(reader, "TypeSizeId"), SerialNumber = GetString(reader, "SerialNumber"), InventoryNumber = GetString(reader, "InventoryNumber"), CustomerName = GetString(reader, "CustomerName"), @@ -425,6 +427,109 @@ ORDER BY names.NMTP, tips.TP, sizeInfo.DPZN, z.NNZV;"; return instruments; } + public IReadOnlyList LoadInstrumentTypes() + { + const string sql = @" +SELECT + 0 AS InstrumentId, + sizeInfo.IDTPRZ AS TypeSizeId, + CAST(N'' AS nvarchar(30)) AS SerialNumber, + CAST(N'' AS nvarchar(30)) AS InventoryNumber, + CAST(N'' AS nvarchar(255)) AS CustomerName, + tips.TP AS InstrumentType, + names.NMTP AS InstrumentName, + areas.NMOI AS MeasurementArea, + sizeInfo.NNGSRS AS RegistryNumber, + sizeInfo.DPZN AS RangeText, + sizeInfo.HRTC AS AccuracyText, + CASE + WHEN typeTemplate.LastDocumentNumber IS NOT NULL + OR periodByType.PRMK IS NOT NULL + THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) + END AS HasTemplate, + typeTemplate.LastDocumentNumber AS LastDocumentNumber, + typeTemplate.LastAcceptedOn AS LastAcceptedOn, + CASE + WHEN typeTemplate.LastDocumentNumber IS NOT NULL THEN N'Шаблон по типоразмеру' + WHEN periodByType.PRMK IS NOT NULL THEN N'Период из TPRMCP' + ELSE N'' + END AS TemplateSource +FROM dbo.TPRZ sizeInfo +LEFT JOIN dbo.TIPS tips ON tips.IDTIPS = sizeInfo.IDTIPS +LEFT JOIN dbo.SPNMTP names ON names.IDSPNMTP = tips.IDSPNMTP +LEFT JOIN dbo.SPOI areas ON areas.IDSPOI = tips.IDSPOI +OUTER APPLY +( + SELECT TOP (1) + history.NNZVPV AS LastDocumentNumber, + history.DTPRM AS LastAcceptedOn + FROM dbo.EKZMK history + JOIN dbo.EKZ instrumentOfSameType ON instrumentOfSameType.IDEKZ = history.IDEKZ + WHERE instrumentOfSameType.IDTPRZ = sizeInfo.IDTPRZ + ORDER BY ISNULL(history.DTPRM, CONVERT(datetime, '19000101', 112)) DESC, history.IDEKZMK DESC +) typeTemplate +OUTER APPLY +( + SELECT TOP (1) + t.PRMK + FROM dbo.TPRMCP t + WHERE t.IDTPRZ = sizeInfo.IDTPRZ + ORDER BY t.IDTPRMCP DESC +) periodByType +ORDER BY names.NMTP, tips.TP, sizeInfo.DPZN, sizeInfo.NNGSRS;"; + + var instrumentTypes = new List(); + + using (var connection = CreateConnection()) + using (var command = new SqlCommand(sql, connection)) + { + connection.Open(); + command.CommandTimeout = 60; + + using (var reader = command.ExecuteReader()) + { + while (reader.Read()) + { + instrumentTypes.Add(new AvailableInstrumentItem + { + InstrumentId = GetInt32(reader, "InstrumentId"), + TypeSizeId = GetInt32(reader, "TypeSizeId"), + SerialNumber = GetString(reader, "SerialNumber"), + InventoryNumber = GetString(reader, "InventoryNumber"), + CustomerName = GetString(reader, "CustomerName"), + InstrumentType = GetString(reader, "InstrumentType"), + InstrumentName = GetString(reader, "InstrumentName"), + MeasurementArea = GetString(reader, "MeasurementArea"), + RegistryNumber = GetString(reader, "RegistryNumber"), + RangeText = GetString(reader, "RangeText"), + AccuracyText = GetString(reader, "AccuracyText"), + HasTemplate = GetBoolean(reader, "HasTemplate"), + LastDocumentNumber = GetString(reader, "LastDocumentNumber"), + LastAcceptedOn = GetNullableDateTime(reader, "LastAcceptedOn"), + TemplateSource = GetString(reader, "TemplateSource") + }); + } + } + } + + return instrumentTypes; + } + + public IReadOnlyList FindOpenDocumentConflicts(int customerId, string currentDocumentNumber, IEnumerable candidateLines) + { + if (customerId <= 0 || candidateLines == null) + { + return new List(); + } + + using (var connection = CreateConnection()) + { + connection.Open(); + return LoadOpenDocumentConflicts(connection, null, customerId, currentDocumentNumber, candidateLines); + } + } + public void ResetLineVerification(int cardId) { if (cardId <= 0) @@ -576,9 +681,9 @@ ORDER BY names.NMTP, tips.TP, sizeInfo.DPZN, z.NNZV;"; var distinctPendingLines = pendingLines == null ? new List() : pendingLines - .Where(delegate(PsvDocumentLine line) { return line != null && line.InstrumentId > 0; }) - .GroupBy(delegate(PsvDocumentLine line) { return line.InstrumentId; }) - .Select(delegate(IGrouping group) { return group.First(); }) + .Where(IsPendingLineReadyForSave) + .GroupBy(GetPendingLineSaveKey, StringComparer.OrdinalIgnoreCase) + .Select(delegate(IGrouping group) { return group.First(); }) .ToList(); using (var connection = CreateConnection()) @@ -592,6 +697,20 @@ ORDER BY names.NMTP, tips.TP, sizeInfo.DPZN, z.NNZV;"; throw new InvalidOperationException(string.Format("ПСВ с номером \"{0}\" уже существует.", normalizedNumber)); } + if (document.CustomerId.HasValue) + { + var openDocumentConflicts = LoadOpenDocumentConflicts( + connection, + transaction, + document.CustomerId.Value, + currentDocumentNumber, + distinctPendingLines); + if (openDocumentConflicts.Count > 0) + { + throw new InvalidOperationException(BuildOpenDocumentConflictMessage(openDocumentConflicts)); + } + } + var updatedEkzMkCount = 0; if (!string.IsNullOrWhiteSpace(currentDocumentNumber)) { @@ -611,9 +730,18 @@ ORDER BY names.NMTP, tips.TP, sizeInfo.DPZN, z.NNZV;"; foreach (var pendingLine in distinctPendingLines) { + var duplicateKey = pendingLine.DuplicateKey; + if (string.IsNullOrWhiteSpace(duplicateKey)) + { + throw new InvalidOperationException("Не удалось определить ключ дубликата для строки ПСВ."); + } + var instrumentId = pendingLine.InstrumentId; - var instrumentIdentity = LoadInstrumentIdentity(connection, transaction, instrumentId); - if (instrumentIdentity == null) + var instrumentIdentity = new InstrumentIdentityInfo + { + DuplicateKey = duplicateKey + }; + if (string.IsNullOrWhiteSpace(duplicateKey)) { throw new InvalidOperationException(string.Format("Прибор IDEKZ={0} не найден.", instrumentId)); } @@ -624,6 +752,7 @@ ORDER BY names.NMTP, tips.TP, sizeInfo.DPZN, z.NNZV;"; continue; } + instrumentId = ResolveInstrumentIdForPendingLine(connection, transaction, document, pendingLine); var template = LoadTemplate(connection, transaction, instrumentId); if (template == null) { @@ -786,6 +915,62 @@ ORDER BY names.NMTP, tips.TP, sizeInfo.DPZN, z.NNZV;"; } } + private static string GetPendingLineSaveKey(PsvDocumentLine line) + { + if (line == null) + { + return string.Empty; + } + + return line.InstrumentId > 0 + ? string.Format("ID:{0}", line.InstrumentId) + : string.Format("NEW:{0}", line.DuplicateKey ?? string.Empty); + } + + private static bool IsPendingLineReadyForSave(PsvDocumentLine line) + { + return line != null + && (line.InstrumentId > 0 + || (line.TypeSizeId > 0 && !string.IsNullOrWhiteSpace(line.SerialNumber))); + } + + private static int ResolveInstrumentIdForPendingLine(SqlConnection connection, SqlTransaction transaction, DocumentEditorResult document, PsvDocumentLine pendingLine) + { + if (pendingLine == null) + { + throw new InvalidOperationException("Строка ПСВ для сохранения не задана."); + } + + if (pendingLine.InstrumentId > 0) + { + return pendingLine.InstrumentId; + } + + if (pendingLine.TypeSizeId <= 0) + { + throw new InvalidOperationException("Для новой строки ПСВ не указан типоразмер прибора."); + } + + var serialNumber = string.IsNullOrWhiteSpace(pendingLine.SerialNumber) ? null : pendingLine.SerialNumber.Trim(); + if (serialNumber == null) + { + throw new InvalidOperationException("Для новой строки ПСВ не указан заводской номер."); + } + + if (!document.CustomerId.HasValue) + { + throw new InvalidOperationException("Для добавления прибора по типу должен быть выбран заказчик ПСВ."); + } + + var existingInstrumentId = FindExistingInstrumentId(connection, transaction, pendingLine.TypeSizeId, document.CustomerId.Value, serialNumber); + if (existingInstrumentId.HasValue) + { + return existingInstrumentId.Value; + } + + return InsertInstrument(connection, transaction, pendingLine.TypeSizeId, document.CustomerId.Value, serialNumber); + } + private static List NormalizeCardIds(IEnumerable cardIds, string invalidCardMessage, string emptyListMessage) { if (cardIds == null) @@ -807,6 +992,196 @@ ORDER BY names.NMTP, tips.TP, sizeInfo.DPZN, z.NNZV;"; return normalizedCardIds; } + private static int? FindExistingInstrumentId(SqlConnection connection, SqlTransaction transaction, int typeSizeId, int customerId, string serialNumber) + { + const string sql = @" +SELECT TOP (1) z.IDEKZ +FROM dbo.EKZ z +WHERE z.IDTPRZ = @TypeSizeId + AND z.IDFRPDV = @CustomerId + AND z.NNZV = @SerialNumber + AND ISNULL(z.IsDeleted, 0) = 0 +ORDER BY z.IDEKZ DESC;"; + + using (var command = new SqlCommand(sql, connection, transaction)) + { + command.CommandTimeout = 60; + command.Parameters.Add("@TypeSizeId", SqlDbType.Int).Value = typeSizeId; + command.Parameters.Add("@CustomerId", SqlDbType.Int).Value = customerId; + command.Parameters.Add("@SerialNumber", SqlDbType.VarChar, 30).Value = serialNumber; + + var result = command.ExecuteScalar(); + return result == null || result == DBNull.Value ? (int?)null : Convert.ToInt32(result); + } + } + + private static int InsertInstrument(SqlConnection connection, SqlTransaction transaction, int typeSizeId, int customerId, string serialNumber) + { + const string sql = @" +INSERT INTO dbo.EKZ +( + IDTPRZ, + IDFRPDV, + KLSIPR, + NNZV, + GUIDEKZ, + IsDeleted +) +VALUES +( + @TypeSizeId, + @CustomerId, + @Klsipr, + @SerialNumber, + @GuidEkz, + 0 +); + +SELECT CAST(SCOPE_IDENTITY() AS int);"; + + using (var command = new SqlCommand(sql, connection, transaction)) + { + command.CommandTimeout = 60; + command.Parameters.Add("@TypeSizeId", SqlDbType.Int).Value = typeSizeId; + command.Parameters.Add("@CustomerId", SqlDbType.Int).Value = customerId; + command.Parameters.Add("@Klsipr", SqlDbType.Int).Value = 1; + command.Parameters.Add("@SerialNumber", SqlDbType.VarChar, 30).Value = serialNumber; + command.Parameters.Add("@GuidEkz", SqlDbType.UniqueIdentifier).Value = Guid.NewGuid(); + return Convert.ToInt32(command.ExecuteScalar()); + } + } + + private static string BuildOpenDocumentConflictMessage(IEnumerable conflicts) + { + var materializedConflicts = conflicts == null + ? new List() + : conflicts + .Where(delegate(OpenDocumentConflictInfo conflict) { return conflict != null; }) + .GroupBy(delegate(OpenDocumentConflictInfo conflict) + { + return string.Format( + "{0}|{1}|{2}", + conflict.DocumentNumber ?? string.Empty, + conflict.TypeSizeId, + conflict.SerialNumber ?? string.Empty); + }, StringComparer.OrdinalIgnoreCase) + .Select(delegate(IGrouping group) { return group.First(); }) + .OrderBy(delegate(OpenDocumentConflictInfo conflict) { return conflict.SerialNumber ?? string.Empty; }) + .ThenBy(delegate(OpenDocumentConflictInfo conflict) { return conflict.DocumentNumber ?? string.Empty; }) + .ToList(); + + if (materializedConflicts.Count == 0) + { + return "Прибор уже находится в другой открытой ПСВ этого заказчика."; + } + + var preview = materializedConflicts + .Take(5) + .Select(delegate(OpenDocumentConflictInfo conflict) + { + return string.Format("зав. № {0} -> {1}", conflict.SerialNumber, conflict.DocumentNumber); + }) + .ToList(); + + var suffix = materializedConflicts.Count > preview.Count + ? string.Format(" Еще конфликтов: {0}.", materializedConflicts.Count - preview.Count) + : string.Empty; + + return string.Format( + "Приборы уже находятся в других открытых ПСВ этого заказчика: {0}.{1}", + string.Join("; ", preview.ToArray()), + suffix); + } + + private static List LoadOpenDocumentConflicts( + SqlConnection connection, + SqlTransaction transaction, + int customerId, + string currentDocumentNumber, + IEnumerable candidateLines) + { + var normalizedCandidates = candidateLines == null + ? new List() + : candidateLines + .Where(delegate(PsvDocumentLine line) + { + return line != null + && line.TypeSizeId > 0 + && !string.IsNullOrWhiteSpace(line.SerialNumber); + }) + .GroupBy(delegate(PsvDocumentLine line) { return line.OpenDocumentConflictKey; }, StringComparer.OrdinalIgnoreCase) + .Select(delegate(IGrouping group) { return group.First(); }) + .ToList(); + + if (customerId <= 0 || normalizedCandidates.Count == 0) + { + return new List(); + } + + const string sql = @" +WITH ActiveDocuments AS +( + SELECT m.NNZVPV + FROM dbo.EKZMK m + WHERE NULLIF(LTRIM(RTRIM(m.NNZVPV)), N'') IS NOT NULL + GROUP BY m.NNZVPV + HAVING MAX(m.DTVDM) IS NULL +) +SELECT DISTINCT + m.NNZVPV AS DocumentNumber, + z.IDTPRZ AS TypeSizeId, + LTRIM(RTRIM(z.NNZV)) AS SerialNumber +FROM ActiveDocuments activeDocuments +JOIN dbo.EKZMK m ON m.NNZVPV = activeDocuments.NNZVPV +JOIN dbo.EKZ z ON z.IDEKZ = m.IDEKZ +WHERE z.IDFRPDV = @CustomerId + AND z.IDTPRZ = @TypeSizeId + AND LTRIM(RTRIM(z.NNZV)) = @SerialNumber + AND (@CurrentDocumentNumber = N'' OR m.NNZVPV <> @CurrentDocumentNumber) +ORDER BY m.NNZVPV;"; + + var conflicts = new List(); + + foreach (var candidateLine in normalizedCandidates) + { + using (var command = new SqlCommand(sql, connection, transaction)) + { + command.CommandTimeout = 60; + command.Parameters.Add("@CustomerId", SqlDbType.Int).Value = customerId; + command.Parameters.Add("@TypeSizeId", SqlDbType.Int).Value = candidateLine.TypeSizeId; + command.Parameters.Add("@SerialNumber", SqlDbType.VarChar, 30).Value = candidateLine.SerialNumber.Trim(); + command.Parameters.Add("@CurrentDocumentNumber", SqlDbType.NVarChar, 60).Value = currentDocumentNumber ?? string.Empty; + + using (var reader = command.ExecuteReader()) + { + while (reader.Read()) + { + conflicts.Add(new OpenDocumentConflictInfo + { + DocumentNumber = GetString(reader, "DocumentNumber"), + TypeSizeId = GetInt32(reader, "TypeSizeId"), + SerialNumber = GetString(reader, "SerialNumber") + }); + } + } + } + } + + return conflicts + .GroupBy(delegate(OpenDocumentConflictInfo conflict) + { + return string.Format( + "{0}|{1}|{2}", + conflict.DocumentNumber ?? string.Empty, + conflict.TypeSizeId, + conflict.SerialNumber ?? string.Empty); + }, StringComparer.OrdinalIgnoreCase) + .Select(delegate(IGrouping group) { return group.First(); }) + .OrderBy(delegate(OpenDocumentConflictInfo conflict) { return conflict.SerialNumber ?? string.Empty; }) + .ThenBy(delegate(OpenDocumentConflictInfo conflict) { return conflict.DocumentNumber ?? string.Empty; }) + .ToList(); + } + private static SqlConnection CreateConnection() { var connectionString = ConfigurationManager.ConnectionStrings["AsumsSql"]; @@ -1179,6 +1554,7 @@ VALUES NULL, NULL, @NNZVPV, + NULL, @NNNKL, @PRMK, @DTMKFK, diff --git a/XLAB/PsvModels.cs b/XLAB/PsvModels.cs index 8d22ac3..6b9cb1d 100644 --- a/XLAB/PsvModels.cs +++ b/XLAB/PsvModels.cs @@ -133,6 +133,8 @@ namespace XLAB public int InstrumentId { get; set; } + public int TypeSizeId { get; set; } + public string SerialNumber { get; set; } public string InventoryNumber { get; set; } @@ -197,6 +199,14 @@ namespace XLAB } } + public string OpenDocumentConflictKey + { + get + { + return BuildOpenDocumentConflictKey(TypeSizeId, SerialNumber); + } + } + public string ResultText { get @@ -237,6 +247,11 @@ namespace XLAB NormalizeKeyPart(serialNumber)); } + public static string BuildOpenDocumentConflictKey(int typeSizeId, string serialNumber) + { + return string.Format("{0}|{1}", typeSizeId, NormalizeKeyPart(serialNumber)); + } + private static string NormalizeKeyPart(string value) { return string.IsNullOrWhiteSpace(value) ? string.Empty : value.Trim().ToUpperInvariant(); @@ -352,6 +367,15 @@ namespace XLAB public DateTime AcceptedOn { get; set; } public DateTime? IssuedOn { get; set; } + + public int? CustomerId { get; set; } + } + + public sealed class InstrumentTypeSelectionResult + { + public AvailableInstrumentItem TypeItem { get; set; } + + public string SerialNumber { get; set; } } public sealed class VerificationEditSeed @@ -392,6 +416,23 @@ namespace XLAB public string VerifierName { get; set; } } + internal sealed class OpenDocumentConflictInfo + { + public string DocumentNumber { get; set; } + + public int TypeSizeId { get; set; } + + public string SerialNumber { get; set; } + + public string OpenDocumentConflictKey + { + get + { + return PsvDocumentLine.BuildOpenDocumentConflictKey(TypeSizeId, SerialNumber); + } + } + } + internal sealed class DocumentDeleteResult { public int DeletedEkzMkFctvlCount { get; set; } diff --git a/XLAB/SelectInstrumentTypeWindow.xaml b/XLAB/SelectInstrumentTypeWindow.xaml new file mode 100644 index 0000000..45330f9 --- /dev/null +++ b/XLAB/SelectInstrumentTypeWindow.xaml @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +