| | | 1 | | @using OzonOrdersWeb.ViewModels.OrderViewModels |
| | | 2 | | |
| | | 3 | | |
| | | 4 | | @model MultiplayEditOrderViewModel |
| | | 5 | | |
| | | 6 | | @{ |
| | 0 | 7 | | var viewModel = Model!; |
| | 0 | 8 | | var orders = viewModel.Orders ?? new List<OzonDomains.Models.Order>(); |
| | 0 | 9 | | var ordersCount = orders.Count; |
| | 0 | 10 | | ViewData["Title"] = $"Заказ поставщику ({ordersCount} заказов)"; |
| | 0 | 11 | | 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 | | |
| | 0 | 74 | | @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 | | |
| | 0 | 114 | | @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 | | } |
| | | 141 | | else |
| | | 142 | | { |
| | 0 | 143 | | @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 *@ |
| | 0 | 208 | | @if (Model.Orders?.Count > 0) |
| | | 209 | | { |
| | 0 | 210 | | @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 { '&': '&', '<': '<', '>': '>', '"': '"' }[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 | | |
| | 0 | 646 | | formData += '&userName=' + '@(User.Identity?.Name ?? "")' + '&comment=' + encodeURIComponent(txGetCommentFieldVa |
| | | 647 | | |
| | | 648 | | $.ajax({ |
| | 0 | 649 | | 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 | | } |