< Summary

Information
Class: AspNetCoreGeneratedDocument.Areas_Studio2_Views_OrderToSupplierTransaction_CreateOrderToSupplierTransactionV2
Assembly: OzonOrdersWeb
File(s): /home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Areas/Studio2/Views/OrderToSupplierTransaction/CreateOrderToSupplierTransactionV2.cshtml
Line coverage
0%
Covered lines: 0
Uncovered lines: 12
Coverable lines: 12
Total lines: 682
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 30
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
ExecuteAsync()0%600240%
<ExecuteAsync()0%4260%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Areas/Studio2/Views/OrderToSupplierTransaction/CreateOrderToSupplierTransactionV2.cshtml

#LineLine coverage
 1@using OzonOrdersWeb.ViewModels.OrderViewModels
 2
 3
 4@model MultiplayEditOrderViewModel
 5
 6@{
 07    var viewModel = Model!;
 08    var orders = viewModel.Orders ?? new List<OzonDomains.Models.Order>();
 09    var ordersCount = orders.Count;
 010    ViewData["Title"] = $"Заказ поставщику ({ordersCount} заказов)";
 011    Layout = "~/Views/Shared/_Layout.cshtml";
 12}
 13
 14<partial name="~/Areas/Studio2/Views/Partials/Transaction/_TransactionPageStyles.cshtml"/>
 15<script src="~/js/Studio2/Orders/CreateOrderToSupplierTransactionV2/generateInfoRow.js" asp-append-version="true"></scri
 16<script>
 17    window.fillPriceFromWarehouseStock = false;
 18    // Пустую цену закупки заполняем учётной ценой 1С (стоимость остатка / количество).
 19    window.fillPriceFromOneCStock = true;
 20    // Проценка стартует в фоне сразу после загрузки, чтобы строки раскрывались мгновенно.
 21    window.txPrefetchQuotationOnLoad = true;
 22</script>
 23
 24<div id="loadingSpinner" class="spinner-overlay" style="display: none; background-color: inherit;">
 25    <div class="spinner-border text-primary" role="status">
 26        <span class="sr-only"></span>
 27    </div>
 28</div>
 29
 30<div class="modal fade" id="armtekPreviewModal" tabindex="-1" aria-labelledby="armtekPreviewModalLabel" aria-hidden="tru
 31    <div class="modal-dialog modal-lg modal-dialog-scrollable">
 32        <div class="modal-content">
 33            <div class="modal-header">
 34                <h5 class="modal-title" id="armtekPreviewModalLabel"><i class="fas fa-code me-2"></i>Запрос Armtek</h5>
 35                <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Закрыть"></button>
 36            </div>
 37            <div class="modal-body"><pre class="mb-0"><code id="armtekPreviewJson"></code></pre></div>
 38        </div>
 39    </div>
 40</div>
 41
 42<div id="messageContainer" class="position-fixed top-0 end-0 p-3" style="z-index: 1050;">
 43    <div id="flashMessage" class="toast" role="alert" aria-live="assertive" aria-atomic="true">
 44        <div class="toast-header">
 45            <strong class="me-auto">Сообщение</strong>
 46            <button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
 47        </div>
 48        <div class="toast-body">
 49            <span id="flashMessageText"></span>
 50        </div>
 51    </div>
 52</div>
 53
 54<div class="modal fade" id="cartModal" tabindex="-1" aria-labelledby="cartModalLabel" aria-hidden="true">
 55    <div class="modal-dialog modal-xl modal-dialog-scrollable">
 56        <div class="modal-content tx-cart-modal">
 57            <div class="modal-header tx-cart-modal-header">
 58                <div>
 59                    <h5 class="modal-title" id="cartModalLabel"><i class="fas fa-cart-shopping me-2 text-primary"></i>Ко
 60                    <div class="tx-cart-modal-hint">Для API-поставщиков можно добавить позиции в корзину либо сразу офор
 61                </div>
 62                <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Закрыть"></button>
 63            </div>
 64            <div class="modal-body tx-cart-modal-body" id="cartModalBody">
 65                <!-- Контент корзины будет вставлен сюда -->
 66            </div>
 67            <div class="modal-footer tx-cart-modal-footer">
 68                <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Закрыть</button>
 69            </div>
 70        </div>
 71    </div>
 72</div>
 73
 074@if (Model.Orders?.Count > 0)
 75{
 76    <form asp-area="Studio2" asp-controller="OrderToSupplierTransaction" asp-action="CreateOrderToSupplierTransactionV2"
 77          id="createOrderForm" method="post">
 78        <div asp-validation-summary="ModelOnly" class="text-danger"></div>
 79        <input type="hidden" asp-for="RedirectPage"/>
 80        <input type="hidden" asp-for="TrackingKey"/>
 81        <input type="hidden" id="avdBasketQueueJson" name="AvdBasketQueueJson"/>
 82        <input type="hidden" id="avdCheckoutQueueJson" name="AvdCheckoutQueueJson"/>
 83        <input type="hidden" id="japartsBasketQueueJson" name="JapartsBasketQueueJson"/>
 84        <input type="hidden" id="japartsCheckoutQueueJson" name="JapartsCheckoutQueueJson"/>
 85        <input type="hidden" id="bergBasketQueueJson" name="BergBasketQueueJson"/>
 86        <input type="hidden" id="bergCheckoutQueueJson" name="BergCheckoutQueueJson"/>
 87        <input type="hidden" id="armtekBasketQueueJson" name="ArmtekBasketQueueJson"/>
 88        <input type="hidden" id="armtekCheckoutQueueJson" name="ArmtekCheckoutQueueJson"/>
 89        <input type="hidden" id="mlAutoBasketQueueJson" name="MlAutoBasketQueueJson"/>
 90        <input type="hidden" id="mlAutoCheckoutQueueJson" name="MlAutoCheckoutQueueJson"/>
 91        <input type="hidden" id="mlAutoRuBasketQueueJson" name="MlAutoRuBasketQueueJson"/>
 92        <input type="hidden" id="mlAutoRuCheckoutQueueJson" name="MlAutoRuCheckoutQueueJson"/>
 93        <input type="hidden" id="forumAutoBasketQueueJson" name="ForumAutoBasketQueueJson"/>
 94        <input type="hidden" id="forumAutoCheckoutQueueJson" name="ForumAutoCheckoutQueueJson"/>
 95        <input type="hidden" id="favoritPartsBasketQueueJson" name="FavoritPartsBasketQueueJson"/>
 96        <input type="hidden" id="favoritPartsCheckoutQueueJson" name="FavoritPartsCheckoutQueueJson"/>
 97        <input type="hidden" id="favoritPartsIstraBasketQueueJson" name="FavoritPartsIstraBasketQueueJson"/>
 98        <input type="hidden" id="favoritPartsIstraCheckoutQueueJson" name="FavoritPartsIstraCheckoutQueueJson"/>
 99        <input type="hidden" id="favoritPartsRostovBasketQueueJson" name="FavoritPartsRostovBasketQueueJson"/>
 100        <input type="hidden" id="favoritPartsRostovCheckoutQueueJson" name="FavoritPartsRostovCheckoutQueueJson"/>
 101        <input type="hidden" id="moskvorechieBasketQueueJson" name="MoskvorechieBasketQueueJson"/>
 102        <input type="hidden" id="moskvorechieCheckoutQueueJson" name="MoskvorechieCheckoutQueueJson"/>
 103        <input type="hidden" id="profitLigaBasketQueueJson" name="ProfitLigaBasketQueueJson"/>
 104        <input type="hidden" id="mikadoBasketQueueJson" name="MikadoBasketQueueJson"/>
 105        <input type="hidden" id="zzapMoscowBasketQueueJson" name="ZZapMoscowBasketQueueJson"/>
 106        <input type="hidden" id="zzapMoscowCheckoutQueueJson" name="ZZapMoscowCheckoutQueueJson"/>
 107        <input type="hidden" id="profitLigaCheckoutQueueJson" name="ProfitLigaCheckoutQueueJson"/>
 108        <input type="hidden" id="motexBasketQueueJson" name="MotexBasketQueueJson"/>
 109        <input type="hidden" id="motexCheckoutQueueJson" name="MotexCheckoutQueueJson"/>
 110        <input type="hidden" id="shateMBasketQueueJson" name="ShateMBasketQueueJson"/>
 111        <input type="hidden" id="shateMCheckoutQueueJson" name="ShateMCheckoutQueueJson"/>
 112        <input type="hidden" id="shateMCheckoutOptionsJson" name="ShateMCheckoutOptionsJson"/>
 113
 0114        @await Html.PartialAsync("_TransactionTable", Model)
 115
 116        <div class="tx-page-options mt-3" id="shipInOzonContainer">
 117            <div class="form-check mb-0">
 118                <input class="form-check-input" type="checkbox" id="shipInOzon" name="shipInOzon" value="true" checked>
 119                <label class="form-check-label" for="shipInOzon">
 120                    <i class="fas fa-truck-fast me-1 text-primary"></i>
 121                    Отгрузить в Ozon после успешного проведения
 122                </label>
 123                <div id="shipInOzonHint" class="text-muted small mt-1" style="display:none;">
 124                    <i class="fas fa-circle-info me-1"></i>
 125                    После нажатия <b>Сохранить</b> система сначала проведет документ в Studio2. Если проведение успешно,
 126                    для Ozon-заказов будет выполнена отгрузка в Ozon API, и статус отправления в Ozon изменится на <b>«О
 127                </div>
 128            </div>
 129        </div>
 130
 131        <div class="tx-page-footer">
 132            <div class="input-group">
 133                <span class="input-group-btn">
 134                    <input type="submit" value="Сохранить" class="btn btn-primary"
 135                           data-bs-target="#staticBackdrop"/>
 136                </span>
 137            </div>
 138        </div>
 139    </form>
 140}
 141else
 142{
 0143    @await Html.PartialAsync("_TransactionErrorPanel")
 144}
 145
 146<div class="modal fade" id="staticBackdrop" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1"
 147     aria-labelledby="staticBackdropLabel" aria-hidden="true">
 148    <div class="modal-dialog">
 149        <div class="modal-content orders-modal orders-modal--primary">
 150            <div class="modal-header">
 151                <h5 class="modal-title" id="staticBackdropLabel">Создание журнала документов</h5>
 152                <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Закрыть"></button>
 153            </div>
 154            <div class="modal-body">
 155                <div class="d-flex justify-content-center">
 156                    <div class="spinner-border text-primary" role="status">
 157                        <span class="sr-only">.</span>
 158                    </div>
 159                </div>
 160            </div>
 161        </div>
 162    </div>
 163</div>
 164
 165<div>
 166    <a asp-action="Index" class="tx-page-back-link">← К списку заказов</a>
 167</div>
 168
 169<div class="modal fade" id="myModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
 170    <div class="modal-dialog modal-dialog-scrollable" role="document">
 171        <div class="modal-content orders-modal orders-modal--warning">
 172            <div class="modal-header">
 173                <span class="tx-issue-badge"><i class="fas fa-triangle-exclamation"></i></span>
 174                <div class="flex-grow-1">
 175                    <h4 class="modal-title" id="myModalLabel">Поля не заполнены</h4>
 176                    <div id="modalErrorMessage">
 177                        Одно или несколько полей не заполнены. Пожалуйста, заполните все поля перед отправкой формы.
 178                    </div>
 179                </div>
 180                <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Закрыть"></button>
 181            </div>
 182            <div class="modal-body">
 183                <div id="validationIssuesList"></div>
 184            </div>
 185            <div class="modal-footer">
 186                <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Закрыть</button>
 187            </div>
 188        </div>
 189    </div>
 190</div>
 191
 192
 193<!-- Модальное окно для отображения увеличенного изображения -->
 194<div class="modal fade" id="imageModal" tabindex="-1" aria-labelledby="imageModalLabel" aria-hidden="true">
 195    <div class="modal-dialog modal-dialog-centered">
 196        <div class="modal-content">
 197            <div class="modal-header">
 198                <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
 199            </div>
 200            <div class="modal-body text-center">
 201                <img id="modalImage" src="" alt="Full Image" class="img-fluid rounded border">
 202            </div>
 203        </div>
 204    </div>
 205</div>
 206
 207@* Скрипты таблицы нужны только когда она отрисована; на панели ошибки они падали на пустом DOM *@
 0208@if (Model.Orders?.Count > 0)
 209{
 0210@await Html.PartialAsync("_TransactionScript", Model)
 211<script>
 212    // Обработчик изменения статуса для снятия выделения ошибок в реальном времени
 213    $(document).ready(function () {
 214        function toggleShipInOzonHint() {
 215            $('#shipInOzonHint').toggle($('#shipInOzon').is(':checked'));
 216        }
 217
 218        var hasOzonOrdersInDocument = $('.transaction-order-row').toArray().some(function (row) {
 219            var clientType = (row.getAttribute('data-client-type') || '').trim().toUpperCase();
 220            return clientType === 'OZON';
 221        });
 222
 223        if (!hasOzonOrdersInDocument) {
 224            $('#shipInOzon').prop('checked', false);
 225            $('#shipInOzonContainer').hide();
 226        }
 227
 228        $('#shipInOzon').on('change', toggleShipInOzonHint);
 229        toggleShipInOzonHint();
 230
 231        $('.status-select').on('change', function () {
 232            var orderId = this.id.split('_')[1];
 233            var selectedOption = this.options[this.selectedIndex];
 234            var selectedText = selectedOption ? selectedOption.text.trim() : '';
 235
 236            // Если выбран статус "К отмене" или "Отменен" - снимаем выделение ошибок
 237            if (selectedText === 'К отмене' || selectedText === 'Отменен') {
 238                var purchasePriceField = $('#PurchasePrice_' + orderId);
 239                var supplierSelect = $('#SupplierId_' + orderId);
 240
 241                purchasePriceField.removeClass('is-invalid highlight-error');
 242                supplierSelect.removeClass('is-invalid');
 243            }
 244        });
 245
 246        // Обработчик изменения количества для снятия выделения при значении 0
 247        // и восстановления статуса при возврате количества > 0
 248        $('.quantity-input-field').on('input change', function () {
 249            var orderId = $(this).data('order-id');
 250            var currentQuantity = parseFloat($(this).val()) || 0;
 251            var statusSelect = document.getElementById('AppStatusId_' + orderId);
 252
 253            if (currentQuantity === 0) {
 254                var purchasePriceField = $('#PurchasePrice_' + orderId);
 255                var supplierSelect = $('#SupplierId_' + orderId);
 256
 257                purchasePriceField.removeClass('is-invalid highlight-error');
 258                supplierSelect.removeClass('is-invalid');
 259            } else if (currentQuantity > 0 && statusSelect) {
 260                // Если количество > 0 и текущий статус "К отмене", возвращаем основной статус
 261                var currentSelectedText = statusSelect.options[statusSelect.selectedIndex]?.text.trim();
 262                if (currentSelectedText === 'К отмене') {
 263                    // Находим первый статус, который не "Не указан" и не "К отмене"
 264                    var primaryOption = Array.from(statusSelect.options).find(opt => {
 265                        var text = opt.text.trim();
 266                        return text !== 'Не указан' && text !== 'К отмене';
 267                    });
 268                    if (primaryOption) {
 269                        statusSelect.value = primaryOption.value;
 270                    }
 271                }
 272            }
 273        });
 274    });
 275
 276    function getOzonSplitCandidates() {
 277        var candidates = [];
 278        $('.transaction-order-row').each(function () {
 279            var orderId = ($(this).data('order-id') || '').toString().trim();
 280            if (!orderId) {
 281                return;
 282            }
 283
 284            var clientType = (($(this).data('client-type') || '').toString().trim()).toUpperCase();
 285            if (clientType !== 'OZON') {
 286                return;
 287            }
 288
 289            var quantityInput = document.querySelector('.quantity-input-field[data-order-id="' + orderId + '"]');
 290            var quantity = quantityInput ? parseFloat(quantityInput.value) || 0 : parseFloat($(this).data('order-quantit
 291            if (quantity > 1) {
 292                candidates.push(orderId);
 293            }
 294        });
 295
 296        return candidates;
 297    }
 298
 299    async function splitOrdersBeforeSubmitAsync() {
 300        if (!$('#shipInOzon').is(':checked')) {
 301            return true;
 302        }
 303
 304        var candidateIds = getOzonSplitCandidates();
 305        if (candidateIds.length === 0 || typeof collectSplitOptionsForOrderIds !== 'function') {
 306            return true;
 307        }
 308
 309        var splitOptionsByOrderId;
 310        try {
 311            splitOptionsByOrderId = await collectSplitOptionsForOrderIds(candidateIds.join(','), 'transaction-ship');
 312        } catch (cancelErr) {
 313            return false;
 314        }
 315
 316        if (!splitOptionsByOrderId) {
 317            return true;
 318        }
 319
 320        var requests = [];
 321        Object.keys(splitOptionsByOrderId).forEach(function (orderId) {
 322            var cfg = splitOptionsByOrderId[orderId];
 323            if (!cfg || !cfg.splitOption) {
 324                return;
 325            }
 326            requests.push({
 327                orderId: orderId,
 328                splitOption: cfg.splitOption,
 329                onlyInSystem: cfg.onlyInSystem === true
 330            });
 331        });
 332
 333        if (requests.length === 0) {
 334            return true;
 335        }
 336
 337        var applySplitResponseToForm = function (splitData) {
 338            if (!splitData || !splitData.splits) {
 339                return;
 340            }
 341
 342            splitData.splits.forEach(function (split) {
 343                if (!split || !split.orderIds || !split.shipmentNumbers) {
 344                    return;
 345                }
 346
 347                for (var idx = 0; idx < split.orderIds.length && idx < split.shipmentNumbers.length; idx++) {
 348                    var currentOrderId = String(split.orderIds[idx] || '').trim();
 349                    var shipmentNumber = String(split.shipmentNumbers[idx] || '').trim();
 350                    if (!currentOrderId || !shipmentNumber) {
 351                        continue;
 352                    }
 353
 354                    var row = document.querySelector('.transaction-order-row[data-order-id="' + currentOrderId + '"]');
 355                    if (!row) {
 356                        continue;
 357                    }
 358
 359                    row.setAttribute('data-shipment-number', shipmentNumber);
 360                    var shipmentInput = row.querySelector('input[name$=".ShipmentNumber"]');
 361                    if (shipmentInput) {
 362                        shipmentInput.value = shipmentNumber;
 363                    }
 364                }
 365            });
 366        };
 367
 368        var executeRequests = async function () {
 369            for (var i = 0; i < requests.length; i++) {
 370                var req = requests[i];
 371                var body = 'ids=' + encodeURIComponent(req.orderId) +
 372                    '&splitOption=' + encodeURIComponent(req.splitOption) +
 373                    (req.onlyInSystem ? '&onlyInSystem=true' : '&onlyInSystem=false');
 374
 375                var response = await fetch('/Studio2/Orders/SplitOrders', {
 376                    method: 'POST',
 377                    headers: {
 378                        'Content-Type': 'application/x-www-form-urlencoded',
 379                        'X-Requested-With': 'XMLHttpRequest'
 380                    },
 381                    body: body
 382                });
 383
 384                var data = await response.json();
 385                if (!data.success) {
 386                    var errorText = data.message || ('Не удалось разделить заказ #' + req.orderId);
 387                    throw new Error(errorText);
 388                }
 389
 390                if (data.isOzonSplit === true) {
 391                    var hasSystemSplit = data.splits && data.splits.some(function (s) {
 392                        return s && s.orderIds && s.orderIds.length === 2;
 393                    });
 394                    if (!hasSystemSplit) {
 395                        throw new Error('Ozon split выполнен, но разделение в системе не подтверждено ответом.');
 396                    }
 397                }
 398
 399                applySplitResponseToForm(data);
 400            }
 401        };
 402
 403        try {
 404            if (window.PageLoadingBar && typeof window.PageLoadingBar.run === 'function') {
 405                await window.PageLoadingBar.run(async function () {
 406                    await executeRequests();
 407                });
 408            } else {
 409                await executeRequests();
 410            }
 411            showNotification('Разделение заказов выполнено. Продолжаю проведение.', 'success');
 412            return true;
 413        } catch (err) {
 414            showNotification('Ошибка разделения перед проведением: ' + (err.message || err), 'danger');
 415            return false;
 416        }
 417    }
 418
 419    let txSubmitInProgress = false;
 420
 421    // Серверная ошибка проведения приходит как HTTP 200 с полем error, и глобальный ajaxPrefilter
 422    // обрабатывает её сам, не вызывая success этой вьюхи. Поэтому флаг отправки снимаем отсюда:
 423    // иначе после первой неудачной попытки кнопка «Сохранить» молча перестаёт работать до перезагрузки.
 424    window.txResetSubmitState = function () {
 425        txSubmitInProgress = false;
 426        window.__criticalOperationInProgress = false;
 427        $('#staticBackdrop').modal('hide');
 428    };
 429
 430    // Проблемные строки текущей попытки отправки: модалка «Поля не заполнены» без них
 431    // сообщает только сам факт, а искать незаполненную ячейку приходится глазами по всей таблице.
 432    var txValidationIssues = [];
 433
 434    function txEscapeHtml(value) {
 435        return String(value ?? '').replace(/[&<>"]/g, function (ch) {
 436            return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[ch];
 437        });
 438    }
 439
 440    function txRowLabel(orderId) {
 441        var row = document.querySelector('.transaction-order-row[data-order-id="' + orderId + '"]');
 442        if (!row) {
 443            return 'Заказ №' + orderId;
 444        }
 445        var parts = [];
 446        if (row.dataset.shipmentNumber) parts.push(row.dataset.shipmentNumber);
 447        if (row.dataset.article) parts.push(row.dataset.article);
 448        if (!parts.length && row.dataset.productName) parts.push(row.dataset.productName);
 449        return parts.length ? parts.join(' · ') : 'Заказ №' + orderId;
 450    }
 451
 452    function txAddValidationIssue(orderId, fieldName) {
 453        var issue = txValidationIssues.find(function (x) { return x.orderId === String(orderId); });
 454        if (!issue) {
 455            issue = { orderId: String(orderId), label: txRowLabel(orderId), fields: [] };
 456            txValidationIssues.push(issue);
 457        }
 458        if (issue.fields.indexOf(fieldName) < 0) {
 459            issue.fields.push(fieldName);
 460        }
 461    }
 462
 463    function txFocusOrderRow(orderId) {
 464        var row = document.querySelector('.transaction-order-row[data-order-id="' + orderId + '"]');
 465        if (!row) return;
 466        row.scrollIntoView({ behavior: 'smooth', block: 'center' });
 467        var field = row.querySelector('.is-invalid, .highlight-error');
 468        if (field && typeof field.focus === 'function') {
 469            setTimeout(function () { field.focus({ preventScroll: true }); }, 350);
 470        }
 471    }
 472
 473    // Тело модалки общее с серверными ошибками проведения и может быть перерисовано
 474    // обработчиком в _TransactionScript, поэтому содержимое собираем в момент показа.
 475    function txRenderValidationIssues() {
 476        var modal = document.getElementById('myModal');
 477        if (!modal) return;
 478        var list = modal.querySelector('#validationIssuesList');
 479        var message = modal.querySelector('#modalErrorMessage');
 480        if (list) list.innerHTML = '';
 481        if (modal.__txServerErrorShown) return;
 482
 483        if (message) {
 484            message.textContent = txValidationIssues.length
 485                ? (txValidationIssues.length === 1
 486                    ? 'Одна строка не готова к проведению. Нажмите её, чтобы перейти к полю.'
 487                    : `Строк не готово к проведению: ${txValidationIssues.length}. Нажмите строку, чтобы перейти к полю.
 488                : 'Одно или несколько полей не заполнены. Пожалуйста, заполните все поля перед отправкой формы.';
 489        }
 490        if (!list) return;
 491        txValidationIssues.forEach(function (issue) {
 492            var row = document.querySelector('.transaction-order-row[data-order-id="' + issue.orderId + '"]');
 493            var image = row?.querySelector('.product-thumb')?.getAttribute('src') || '';
 494            var productName = row?.dataset.productName || '';
 495
 496            var item = document.createElement('button');
 497            item.type = 'button';
 498            item.className = 'tx-issue-row';
 499            item.innerHTML =
 500                (image
 501                    // Тег собран из частей: сканеры HTML не должны принять литерал за картинку (#2273).
 502                    ? '<img ' + 'class="tx-issue-thumb" src="' + txEscapeHtml(image) + '" alt="" loading="lazy"'
 503                        + ' onerror="this.classList.add(\'tx-issue-thumb-empty\');this.removeAttribute(\'src\')">'
 504                    : '<span class="tx-issue-thumb tx-issue-thumb-empty"><i class="fas fa-image"></i></span>')
 505                + '<span class="tx-issue-text">'
 506                + '<span class="tx-issue-title d-block">' + txEscapeHtml(issue.label) + '</span>'
 507                + (productName ? '<span class="tx-issue-product d-block">' + txEscapeHtml(productName) + '</span>' : '')
 508                + '<span class="tx-issue-fields">'
 509                + issue.fields.map(function (field) {
 510                    return '<span class="tx-issue-chip">' + txEscapeHtml(field) + '</span>';
 511                }).join('')
 512                + '</span>'
 513                + '</span>'
 514                + '<i class="fas fa-chevron-right tx-issue-arrow"></i>';
 515            item.addEventListener('click', function () {
 516                $('#myModal').modal('hide');
 517                txFocusOrderRow(issue.orderId);
 518            });
 519            list.appendChild(item);
 520        });
 521    }
 522
 523    $('#myModal').on('show.bs.modal shown.bs.modal', txRenderValidationIssues);
 524
 525    $('#createOrderForm').on('submit', async function (e) {
 526        e.preventDefault();
 527        if (txSubmitInProgress) {
 528            return;
 529        }
 530
 531        txValidationIssues = [];
 532        $('.purchase-price-field').each(function () {
 533            // Получаем ID заказа из поля цены
 534            var orderId = this.id.split('_')[1];
 535            var statusSelect = document.getElementById('AppStatusId_' + orderId);
 536            var quantityInput = document.querySelector('.quantity-input-field[data-order-id="' + orderId + '"]');
 537            var currentQuantity = quantityInput ? parseFloat(quantityInput.value) || 0 : 1;
 538
 539            // Пропускаем проверку если:
 540            // 1. Статус "Отменен" или "К отмене"
 541            // 2. Количество равно 0 (заказ уходит "К отмене")
 542            var selectedOption = statusSelect ? statusSelect.options[statusSelect.selectedIndex] : null;
 543            var selectedStatusText = selectedOption ? selectedOption.text.trim() : '';
 544            var skipValidation = selectedStatusText === "Отменен" ||
 545                                 selectedStatusText === "К отмене" ||
 546                                 currentQuantity === 0;
 547
 548            if (!skipValidation) {
 549                if ($(this).val() === '') {
 550                    txAddValidationIssue(orderId, 'Цена закупки');
 551                    // Визуальное выделение незаполненных полей
 552                    $(this).addClass('is-invalid');
 553                } else {
 554                    $(this).removeClass('is-invalid');
 555                }
 556            } else {
 557                $(this).removeClass('is-invalid');
 558            }
 559        });
 560
 561        $('.supplier-select').each(function () {
 562            // Получаем ID заказа из селекта поставщика
 563            var orderId = this.id.split('_')[1];
 564            var statusSelect = document.getElementById('AppStatusId_' + orderId);
 565            var quantityInput = document.querySelector('.quantity-input-field[data-order-id="' + orderId + '"]');
 566            var currentQuantity = quantityInput ? parseFloat(quantityInput.value) || 0 : 1;
 567
 568            // Пропускаем проверку если:
 569            // 1. Статус "Отменен" или "К отмене"
 570            // 2. Количество равно 0 (заказ уходит "К отмене")
 571            var selectedOption = statusSelect ? statusSelect.options[statusSelect.selectedIndex] : null;
 572            var selectedStatusText = selectedOption ? selectedOption.text.trim() : '';
 573            var skipValidation = selectedStatusText === "Отменен" ||
 574                                 selectedStatusText === "К отмене" ||
 575                                 currentQuantity === 0;
 576
 577            if (!skipValidation) {
 578                var selectedSupplierText = this.options[this.selectedIndex]?.text?.trim() ?? '';
 579                if ($(this).val() === '' || $(this).val() === '1'
 580                    || selectedSupplierText.startsWith('Не указан')
 581                    || selectedSupplierText.startsWith('Выберите')) {
 582                    txAddValidationIssue(orderId, 'Поставщик');
 583                    if (window.txMarkFieldInvalid) {
 584                        window.txMarkFieldInvalid(this);
 585                    } else {
 586                        $(this).addClass('is-invalid highlight-error');
 587                    }
 588                } else {
 589                    if (window.txClearFieldInvalid) {
 590                        window.txClearFieldInvalid(this);
 591                    } else {
 592                        $(this).removeClass('is-invalid highlight-error');
 593                    }
 594                }
 595            } else {
 596                if (window.txClearFieldInvalid) {
 597                    window.txClearFieldInvalid(this);
 598                } else {
 599                    $(this).removeClass('is-invalid highlight-error');
 600                }
 601            }
 602        });
 603
 604        if (txValidationIssues.length > 0) {
 605            var validationModal = document.getElementById('myModal');
 606            if (validationModal) validationModal.__txServerErrorShown = false;
 607            $('#myModal').modal('show');
 608            return;
 609        } else if (!validateOzonCancelSelections()) {
 610            return;
 611        } else {
 612            // Подтверждение спрашиваем после валидации (иначе пользователь подтвердит корзину и тут же
 613            // упрётся в «заполните поля»), но до разделения заказов: сплит делает реальные серверные
 614            // изменения и не должен выполняться для отправки, которую потом отменили.
 615            var cartItems = window.txGetCartItems ? window.txGetCartItems() : [];
 616            if (cartItems.length > 0 && window.txRequestCartConfirmation) {
 617                // Продолжаем вызовом функции, а не повторным submit-событием: jQuery.trigger('submit')
 618                // после обработчиков дёргает нативный form.submit(), и форма уходит обычным POST
 619                // в обход AJAX — браузер показывает JSON ответа вместо редиректа.
 620                window.txRequestCartConfirmation(function () { postTransactionAsync(); });
 621                return;
 622            }
 623
 624            await postTransactionAsync();
 625        }
 626    });
 627
 628    async function postTransactionAsync() {
 629        if (txSubmitInProgress) {
 630            return;
 631        }
 632
 633        var splitReady = await splitOrdersBeforeSubmitAsync();
 634        if (!splitReady) {
 635            return;
 636        }
 637
 638        txSubmitInProgress = true;
 639        $('#staticBackdrop').modal('show');
 640        window.__criticalOperationInProgress = true;
 641        // Состав корзины мог измениться в модалке подтверждения — пересобираем скрытые поля перед сериализацией.
 642        if (window.txSerializeCartQueues) window.txSerializeCartQueues();
 643        if (window.txPruneEmptyExtraExpenses) window.txPruneEmptyExtraExpenses(document.getElementById('createOrderForm'
 644        var formData = $('#createOrderForm').serialize();
 645
 0646        formData += '&userName=' + '@(User.Identity?.Name ?? "")' + '&comment=' + encodeURIComponent(txGetCommentFieldVa
 647
 648        $.ajax({
 0649            url: $('#createOrderForm').attr('action') || '@Url.Action("CreateOrderToSupplierTransactionV2", "OrderToSupp
 650            type: 'POST',
 651            data: formData,
 652            success: function (result) {
 653                if (result.redirectTo) {
 654                    // Корзину чистим только здесь: до успешного проведения она должна пережить
 655                    // любую неудачную попытку отправки.
 656                    if (window.txClearCartQueue) window.txClearCartQueue();
 657                    if (window.txClearLocalDraft) window.txClearLocalDraft();
 658                    window.location.href = result.redirectTo;
 659                } else {
 660                    txSubmitInProgress = false;
 661                    window.__criticalOperationInProgress = false;
 662                    $('#staticBackdrop').modal('hide');
 663                    alert('Произошла ошибка: не удалось получить URL для перенаправления.');
 664                }
 665            },
 666            error: function (xhr, status, error) {
 667                txSubmitInProgress = false;
 668                window.__criticalOperationInProgress = false;
 669                $('#staticBackdrop').modal('hide');
 670                var payloadBytes = new TextEncoder().encode(formData || '').length;
 671                var payloadKb = (payloadBytes / 1024).toFixed(2);
 672                var diagnostics = 'Диагностика запроса:\n' +
 673                    'URL: ' + this.url + '\n' +
 674                    'Отправлено данных: ' + payloadBytes + ' байт (' + payloadKb + ' KB)' + '\n' +
 675                    'Количество выбранных заказов: ' + ($('.transaction-order-row').length || $('.transaction-row').leng
 676
 677                showFriendlyRequestError(xhr, status, error, 'Ошибка при отправке формы', diagnostics);
 678            }
 679        });
 680    }
 681</script>
 682}

Methods/Properties

ExecuteAsync()
<ExecuteAsync()