From fcbe25f7ece514da17880927690da3ec988a0695 Mon Sep 17 00:00:00 2001 From: Macbook Date: Wed, 10 Dec 2025 23:03:32 +0300 Subject: [PATCH] =?UTF-8?q?=D0=97=D0=B0=D0=B2=D0=B5=D1=80=D1=88=D0=B5?= =?UTF-8?q?=D0=BD=D0=B0=20=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=B0=20=D1=81?= =?UTF-8?q?=D0=BE=20=D1=81=D0=BA=D0=BB=D0=B0=D0=B4=D0=B0=D0=BC=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/routers/__init__.py | 2 +- .../__pycache__/__init__.cpython-313.pyc | Bin 4315 -> 4331 bytes .../__pycache__/toolbox.cpython-313.pyc | Bin 3232 -> 5012 bytes .../__pycache__/toolkit.cpython-313.pyc | Bin 3064 -> 4492 bytes api/routers/toolbox.py | 33 ++ api/routers/toolkit.py | 21 +- api/static/js/index.js | 505 +++++++++++++++++- api/static/js/toast.js | 14 +- .../__pycache__/actions.cpython-313.pyc | Bin 26331 -> 26353 bytes db/handlers/__pycache__/stock.cpython-313.pyc | Bin 7572 -> 12612 bytes db/handlers/actions.py | 2 +- db/handlers/stock.py | 109 +++- db/schemas/__pycache__/stock.cpython-313.pyc | Bin 2420 -> 3951 bytes db/schemas/stock.py | 37 +- 14 files changed, 689 insertions(+), 34 deletions(-) diff --git a/api/routers/__init__.py b/api/routers/__init__.py index 7885478..fcd30fe 100644 --- a/api/routers/__init__.py +++ b/api/routers/__init__.py @@ -56,7 +56,7 @@ async def post_requests( case "toolkits": toolkits = await ToolkitHandler.getAll() categories = await CategoryHandler.getAll() - if toolkits: + if toolkits and categories: resultData["status"] = "ok" resultData["data"] = { "toolkits": toolkits, diff --git a/api/routers/__pycache__/__init__.cpython-313.pyc b/api/routers/__pycache__/__init__.cpython-313.pyc index d0e02ed2a9473c755aaa932215b2cb7d1216340e..d8cbcd334ea93bd9c9a4ada64b0f19dd571e2527 100644 GIT binary patch delta 245 zcmcbu_*#+oGcPX}0}v#0SZ3C2&S-jXcGZLNXq z6Bdl#(kv$|4S?*EnvCAEEGM<}fb3I1L*-dcNtyxKrv({7qzIT42a}RuQkv157bqbM O<^U~F-0a0akr4p-z&zFf delta 236 zcmaE@cw3S8GcPX}0}y0rn`h>3{6Nkz1x9aPmSc)`K=yHYMsER@;|ek$whg1V5X*5}6CnG9A)~hl%LyX` zAp4{eqqi8#No74C`;;J~w*<>6Au}NRG#4X?1qKutG5En1Bv8M&zs@TSCfu!%FFm_pXVc zu?2#RKM7logniPPn228t;ZImFY+r~mk?55YS5J&zj0vACnM(X(eBP@q&doP@pL_22 z{@&+#@4esizEcesJH7KBj~hWd`t7FVFT#X3L=8-IZfuv3jAXXYwZo0wI|M99Xow1< zC}Bdw;*hwz$(V2iBAo1$`Azf?9Y#a$C{-TsqGVSidYh{9y+h(escsb~yO$&j3&=hQ z-bR80UtE&!S-`KYEM}Egu33`rTfh$-^hauqwbXKtN7mKhLt4+Uo>bG%f#3waD{7E znJqNsGyY(sf|rcd5aTyCaLacFz>ToDjvyor4iaN0a43nS9^r63L7IsP5mSy%yaq5D z2S&0RZZtmTf@~`Q_1A83S(@_^RqP!R`p`;XDS+pF>t=XHlHLl0YOIx%mJ7#(MeqB`!)y=v)0c?k^T{mXAi1Dd= zI&_SEb|jff{oVqwdC9zD&YCmk7v^napF}F}k{mI#n{v^ssR~D^a>{dzZaTaO%hEzM=%T^**#94F;47Katn5m2F*bmDk06wG8fR%( zsI#2ZU#*B39b+n}N2pA$BH|*%Q$-w6r|~&mpXFoxO4tz65;Q_PQwYU4*}K{UX}O&m z$PP(>43eF*Ta2y5cz!QJaro78O^lKm@+)@L$-Wq9MD{Pm2fTJY3)}$V4%*R-2UKG= zMpZ&WDykjvXkhO~W`v3a2QXP2wgvb^^0tVH{45SDaZOEIY*JSaYZyKNS#A(V>QGYO zpG;W%kxV?TC-tKiJBpLTisc$j#fO!{N?LzNP8Ce_o6QidnID?7`J6eOKLvWZ4*e=< z=e9W`Wk}&j=1p5l)A{4(4RiYL2a-8$-jp)EFt$cAKZ2QL#*KU~e?EW0ydjx0`QzYR zfqu)J2DW4i@hZH}*lBDM(dKM6e~z&7Cvh7@gVK0Xz;->efLbC}4kk4n$H}2$F)1Y- zag|HjuIB2N;HQC&SJ_jv%=Otl6n0%$+YrF4gFZx20on}oq*IV?} zR|n>Noq1p9Wuf5f%55)tf)@tQ3{Ea9c%(T`d*0JNW%!&<<1eluD+Vs~o#~t83S!Hg z*p?UDrd}X`wF5| z@%I$O=88X95Ld zjuo7(#pc#Aemr!KZ!B@BX60pZ>U}fXe^)kNd*kbUU-g#|dXtLN@bDarVPu~SID!;JOlg=W z!R;G~!)iuXu(jJUs)EAEU1Kq$86A@=0Cx~W3f30HXJ{T=RMrds!eKR$NhvSk9>7RF zX-#a83dH?_b delta 822 zcmZuv&ubGw6rR~1vp>>oeze*qmKxfcvI@~dLlHz!i_i+9O~uQCY`dEpjomOi0X-!i zJbBPCg2hu0UV4rPPagdTJO~S=UW=EO{sX?*0ckY@`|)`1d++<^&3;(eEjcIItc~Ej z{kcRRgi~gQC(EwP}{TDCS`mY8+wB@jO&W0%p%XMV_93o4YQ1% z;lJHuo;~QF>*)<^B-uVgc0lUlX%QKNRAyxN*qQG9R@QY6MT@H>)~yFCYf+blG%=P# z{vMmdJ-myt%~#dMf(fKVYi&29MBNG*CH$Q_L-LTN1wK?C>17D*VOG1N;2eKQ>Y1#l zIRf&0KxTSH2wj~{h>Vy<#|038L<@r!a|t~!GSdRY`ly^PqA7q_P%G)7B?uFz+X;LY zK4DSR-iTh(3M9pZ_%pyE>Z4!z$&Z;qX6NpY{M^rB&1fnl2PHbqKUq~G{qgVCn_eoE z`ZNCZr$a;Hz>ROeT9-O2fiz8mupP4SKii2NvuN{qyzDbyUSYI**)H7YQu|dmaWqISA0-4~q~G$Fl*P5_Zyt$E``6m;9+O?nM-;!llt0FG3Q@d2vt jqs#lKc7QG&6J(FSz0fB|1Sux}U1avL;@}B-71)0OJolug diff --git a/api/routers/__pycache__/toolkit.cpython-313.pyc b/api/routers/__pycache__/toolkit.cpython-313.pyc index 660a310e33ec8389cf5eb0f14d3651b31b037de3..d8e472bd565d0fc1b9be796592ff6d5c558b2c9c 100644 GIT binary patch delta 1798 zcmah}O>7fK6rLHc?e%}0A8c%zIzLJ<5IYbkq(EpftrA-aC`;gw6y(^O)MVo|vl~IU zIHd&|7|v)&+Hnr+`*OxmeEf&?URDL(0-4g*_L&ZLXF3~WoelOF0x zdZ{;pMsPDaz@osSz7gMGlZm%d!7lKEqnnJ7W;EhCfKA2z;8pOr)!C3?*%DV-4~#k^XP%`P`N zvF<%B%hE(neeinKS`5L@=={a0bY9J=(=^OjN1^H$C6pP^MkclvXf7k_4pmmuQbJZ# zoyZf^&-ArGq7Ld}W?U{mChL}QQKfdqdjTDvuH@{?Ql1H&hVbO2!+DYFHelYDB%PZO zrZ!lFk>X zSV&Xx@BN_CzR;d(-)IlDr=_cazSf>-_aoYO+I{V3V1Lkl(jLKnT)Gu0-74MC9y5Ig z{5BRl!l$s;KZmllpa{A>caCd{2)J5CXbB=%I&mHib5*ZF+uWa|er~U2y9W=m^BQZ5 zlnfi4$Hbe`qGbs0G)+%u&Og}zT8LBP+Lk}^7ZXkWIEBT)7{Pp0%ae=Am30y+@nr)>8TTIy;_tNrjdu6L;>c(+~i6`N;zR~lN% z4Z9W^b}csSDf{+-AkbD0bSwlq76b9JKR%OO3AC33v4ucvG0<7|cg`gLu>0;Lm)>f+ z&VLpx@u8Q&#=DnG9sQ-?z)C3cWvH}QD1}ZhAM0BTC7)cmA1#M>m%_V$4|jZjr4&lO zvU59};JK~D2a9V?$;w zj>h(VY`&aIY_n!YEp$I6=MKBJ7kL(-ZmUV9`cPBQSf|vSq);~S<^yS? z@{|hC9#>#*D3^u8Qud4)RWdryXrpTLPs-UTN$jEhV8W(Cxddd*f-!!9+E&p%+Y5AL v6~*QrxQ@owNDSAnjbcRjYvE7A*UxKhBQs$QfAorYaQ(Ukk#pF9MuvX@c*VFd delta 538 zcmYjOJx|;~5Z(3J=e6OC4v;t{4IEOC6LA#?kN}A!iUd&t!jw*7&I_0`UNgHVxa$4@ z1?YYQMcVK?Ql?o6rB0WKCS`WcQp6Nb^WHqo$L`JI-?`wa(eR0hFLWcD^zMQdy}es{ zGn^+8iR_Q=9s{=Lfj3WL+9s>$Gy#6>AI??zbBvs*9=RJlq+zM2PS(v zI1(c7WokEtI*R^S+`k%qcW0L-P+`DjDGJG#NFGSkU`8KQ@j=v&58TehJl>Q{I9?9#`LUYUiV|C~( HteMI`mtTab diff --git a/api/routers/toolbox.py b/api/routers/toolbox.py index 26dcb86..1f5ba6d 100644 --- a/api/routers/toolbox.py +++ b/api/routers/toolbox.py @@ -1,4 +1,5 @@ from fastapi import APIRouter, Depends +from db.handlers.actions import StocksActions from db.handlers.stock import StockHandler from db.handlers.toolbox import ToolboxHandler from utils import requestDict, logger @@ -48,3 +49,35 @@ async def delete_toolbox(reqDict=Depends(requestDict)): if result: response["status"] = "ok" return response + + +@router.post("/fill", summary="Заполнение ящика") +async def fill_toolbox(reqDict=Depends(requestDict)): + logger.info(f"Заполнение ящика") + logger.info(reqDict.get("body")) + response = {"status": "error"} + toolboxId = reqDict.get("body").get("toolboxId") + userId = reqDict.get("body").get("userId") + reason = reqDict.get("body").get("reason") + items = reqDict.get("body").get("items") + successCount = 0 + for item in items: + success = await StocksActions.registration( + item.get("toolkit_id"), + toolboxId, + userId, + item.get("quantity"), + item.get("price"), + item.get("placement"), + reason, + ) + if success: + successCount += 1 + if successCount == len(items): + response["status"] = "ok" + else: + response["message"] = ( + f"Оприходовано {successCount} записей из {len(items)}. Проверьте остатки и повторите попытку" + ) + logger.info(response) + return response diff --git a/api/routers/toolkit.py b/api/routers/toolkit.py index a519130..88bfff4 100644 --- a/api/routers/toolkit.py +++ b/api/routers/toolkit.py @@ -1,6 +1,8 @@ from fastapi import APIRouter, Depends -from db.handlers.stock import StockHandler +from db.handlers.categories import CategoryHandler +from db.handlers.stock import PlacementHandler, StockHandler from db.handlers.toolbox import ToolboxHandler +from db.handlers.toolkit import ToolkitHandler from utils import requestDict, logger @@ -55,3 +57,20 @@ async def toolkit_request( response["status"] = "ok" response["data"] = stocksData return response + + +@router.post("/fill_prepare", summary="Подготовка заполнения ящика") +async def fill_toolbox(): + logger.info(f"Подготовка заполнения ящика") + response = {"status": "error"} + toolkits = await ToolkitHandler.getAll() + categories = await CategoryHandler.getAll() + placements = await PlacementHandler.getAll() + if toolkits and categories: + response["status"] = "ok" + response["data"] = { + "toolkits": sorted(toolkits, key=lambda toolkit: toolkit["title"]), + "categories": sorted(categories, key=lambda category: category["title"]), + "placements": [placement.toDict() for placement in placements], + } + return response diff --git a/api/static/js/index.js b/api/static/js/index.js index 4f61fea..7eeae57 100644 --- a/api/static/js/index.js +++ b/api/static/js/index.js @@ -879,8 +879,8 @@ async function loadToolboxContent(toolboxId) { function handleFillBtn() { if (accessData.tools_registration && !toolboxInfo.owner_id) { - contentContainer.querySelector('#fillToolbox').addEventListener('click', () => { - fillToolbox(toolboxInfo); + contentContainer.querySelector('#fillToolbox').addEventListener('click', async () => { + await fillToolbox(toolboxInfo); }); } else { contentContainer.querySelector('#fillToolbox').remove(); @@ -1047,9 +1047,504 @@ async function loadToolboxContent(toolboxId) { } } -function fillToolbox(toolboxInfo) { - console.log(toolboxInfo); - showInfo('Функционал еще в разработке', 'warning'); +async function fillToolbox(toolboxInfo) { + const allToolkitsData = await apiRequest('/toolkit/fill_prepare'); + + if (allToolkitsData.status !== 'ok') { + showInfo('Ошибка загрузки данных инструментов', 'error'); + return; + } + + // Проверяем, существует ли уже модальное окно + let modal = document.getElementById('fillToolboxModal'); + + if (modal) { + modal.remove(); + } + + // Создаем модальное окно + modal = document.createElement('div'); + modal.className = 'modal fade'; + modal.id = 'fillToolboxModal'; + modal.tabIndex = -1; + modal.setAttribute('aria-hidden', 'true'); + + modal.innerHTML = ` + + `; + + document.body.appendChild(modal); + + // Инициализация модального окна + const bsModal = new bootstrap.Modal(modal); + + // Получаем данные + const { toolkits, categories, placements } = allToolkitsData.data; + const placementMap = {}; + + // Создаем карту расположений для текущего склада + placements + .filter(p => p.toolbox_id === toolboxInfo.id) + .forEach(p => { + placementMap[p.toolkit_id] = p.placement; + }); + + // Создаем карту инструментов по категориям + const toolkitsByCategory = {}; + toolkits.forEach(tool => { + if (!toolkitsByCategory[tool.category_id]) { + toolkitsByCategory[tool.category_id] = []; + } + toolkitsByCategory[tool.category_id].push(tool); + }); + + // Функция для форматирования стоимости с разделителями + function formatCost(value) { + if (typeof value !== 'number') { + value = parseFloat(value) || 0; + } + return formatPrice(value); + } + + // Функция для создания строки формы + function createRow(rowIndex = 0) { + const rowId = `row-${Date.now()}-${rowIndex}`; + return ` + + + + + + + + + + + + + + + + + + + + + + + + `; + } + + // Инициализация первой строки + const rowsContainer = document.getElementById('fillToolboxRows'); + rowsContainer.innerHTML = createRow(0); + + // Добавляем обработчики событий + setupEventHandlers(); + + // Функция для обновления итогов + function updateTotals() { + const rows = rowsContainer.querySelectorAll('tr'); + let totalQuantity = 0; + let totalCost = 0; + let filledRows = 0; + + rows.forEach(row => { + const quantityInput = row.querySelector('.quantity'); + const costInput = row.querySelector('.cost'); + + if (quantityInput && costInput && + quantityInput.value && costInput.value) { + const quantity = parseInt(quantityInput.value) || 0; + const cost = parseFloat(costInput.value.replace(/[^0-9.,]/g, '').replace(',', '.')) || 0; + + totalQuantity += quantity; + totalCost += cost; + filledRows++; + } + }); + + document.getElementById('totalQuantity').textContent = totalQuantity; + document.getElementById('totalCost').textContent = formatCost(totalCost) + ' ₽'; + document.getElementById('totalRowsCount').textContent = + `${filledRows} позиций`; + } + + // Функция для расчета стоимости строки + function calculateRowCost(row) { + const quantityInput = row.querySelector('.quantity'); + const priceInput = row.querySelector('.price'); + const costInput = row.querySelector('.cost'); + + if (!quantityInput || !priceInput || !costInput) return; + + const quantity = parseFloat(quantityInput.value) || 0; + const price = parseFloat(priceInput.value.replace(',', '.')) || 0; + const cost = quantity * price; + + costInput.value = formatCost(cost) + ' ₽'; + updateTotals(); + } + + // Функция для настройки обработчиков событий + function setupEventHandlers() { + // Обработчик добавления строки + document.getElementById('addRowBtn').addEventListener('click', function () { + const newRow = createRow(rowsContainer.children.length); + rowsContainer.insertAdjacentHTML('beforeend', newRow); + + // Обновляем доступность кнопок удаления + const removeButtons = rowsContainer.querySelectorAll('.remove-row'); + if (removeButtons.length > 1) { + removeButtons.forEach(btn => btn.disabled = false); + } + + // Настраиваем обработчики для новой строки + const newRowElement = rowsContainer.lastElementChild; + setupRowHandlers(newRowElement); + }); + + // Настраиваем обработчики для всех строк + rowsContainer.querySelectorAll('tr').forEach(row => { + setupRowHandlers(row); + }); + + // Валидация поля обоснования + const reasonInput = document.getElementById('fillReason'); + reasonInput.addEventListener('input', function () { + if (this.value.length >= 10) { + this.classList.remove('is-invalid'); + this.classList.add('is-valid'); + } else if (this.value.length > 0) { + this.classList.add('is-invalid'); + this.classList.remove('is-valid'); + } else { + this.classList.remove('is-invalid', 'is-valid'); + } + }); + + // Обработчик отправки формы + document.getElementById('fillToolboxForm').addEventListener('submit', async function (e) { + e.preventDefault(); + + const submitBtn = document.getElementById('submitFillBtn'); + const spinner = document.getElementById('submitFillSpinner'); + const submitText = document.getElementById('submitFillText'); + + // Проверка заполнения всех обязательных полей + const rows = rowsContainer.querySelectorAll('tr'); + const items = []; + let isValid = true; + let errorMessage = ''; + + // Проверка обоснования + const reason = reasonInput.value.trim(); + if (reason.length < 10) { + isValid = false; + errorMessage = 'Введите обоснование (не менее 10 символов)'; + reasonInput.classList.add('is-invalid'); + reasonInput.scrollIntoView({ behavior: 'smooth', block: 'center' }); + } else { + reasonInput.classList.remove('is-invalid'); + reasonInput.classList.add('is-valid'); + } + + // Проверка строк с инструментами + rows.forEach((row, index) => { + const categorySelect = row.querySelector('.category-select'); + const toolkitSelect = row.querySelector('.toolkit-select'); + const quantityInput = row.querySelector('.quantity'); + const priceInput = row.querySelector('.price'); + + if (!categorySelect.value || !toolkitSelect.value || + !quantityInput.value || !priceInput.value) { + isValid = false; + errorMessage = `Заполните все обязательные поля в строке ${index + 1}`; + // Подсвечиваем проблемные поля + [categorySelect, toolkitSelect, quantityInput, priceInput].forEach(field => { + if (!field.value) { + field.classList.add('is-invalid'); + } + }); + return; + } + + items.push({ + toolkit_id: parseInt(toolkitSelect.value), + quantity: parseInt(quantityInput.value), + price: parseFloat(priceInput.value.replace(',', '.')), + placement: row.querySelector('.placement').value || null + }); + }); + + if (!isValid) { + document.getElementById('fillToolboxErrorMessage').textContent = errorMessage; + document.getElementById('fillToolboxError').classList.remove('d-none'); + document.getElementById('fillToolboxError').scrollIntoView({ behavior: 'smooth', block: 'center' }); + return; + } + + if (items.length === 0) { + document.getElementById('fillToolboxErrorMessage').textContent = 'Добавьте хотя бы одну позицию'; + document.getElementById('fillToolboxError').classList.remove('d-none'); + return; + } + + // Запрос подтверждения (двойное нажатие в течение 10 секунд) + const originalText = submitText.textContent; + submitText.textContent = 'Нажмите еще раз для подтверждения (10 сек)'; + submitBtn.disabled = true; + + let confirmed = false; + let timeout = setTimeout(() => { + if (!confirmed) { + submitText.textContent = originalText; + submitBtn.disabled = false; + document.getElementById('fillToolboxError').classList.add('d-none'); + } + }, 10000); + + submitBtn.addEventListener('click', async function confirmHandler(e) { + e.preventDefault(); + confirmed = true; + clearTimeout(timeout); + + // Показываем индикатор загрузки + submitBtn.disabled = true; + spinner.style.display = 'inline-block'; + + try { + const response = await apiRequest('/toolbox/fill', { + toolboxId: toolboxInfo.id, + items: items, + reason: reason, + userId: userData.id + }, 'POST'); + + if (response.status === 'ok') { + showInfo('Склад успешно пополнен', 'success'); + bsModal.hide(); + + // Обновляем содержимое склада + await loadToolboxContent(toolboxInfo.id); + } else { + throw new Error(response.message || 'Ошибка при пополнении склада'); + } + } catch (error) { + console.error('Ошибка при пополнении склада:', error); + document.getElementById('fillToolboxErrorMessage').textContent = + error.message || 'Произошла ошибка при пополнении склада'; + document.getElementById('fillToolboxError').classList.remove('d-none'); + + // Возвращаем кнопку в исходное состояние + submitBtn.disabled = false; + spinner.style.display = 'none'; + submitText.textContent = originalText; + } + + // Удаляем обработчик подтверждения + submitBtn.removeEventListener('click', confirmHandler); + }, { once: true }); + + submitBtn.disabled = false; + }); + } + + // Функция для настройки обработчиков строки + function setupRowHandlers(row) { + const rowId = row.id; + + // Обработчик выбора категории + const categorySelect = row.querySelector('.category-select'); + categorySelect.addEventListener('change', function () { + this.classList.remove('is-invalid'); + const toolkitSelect = row.querySelector('.toolkit-select'); + const quantityInput = row.querySelector('.quantity'); + const priceInput = row.querySelector('.price'); + const placementInput = row.querySelector('.placement'); + + // Очищаем и деактивируем зависимые поля + toolkitSelect.innerHTML = ''; + toolkitSelect.disabled = !this.value; + quantityInput.disabled = !this.value; + priceInput.disabled = !this.value; + placementInput.disabled = !this.value; + + if (!this.value) { + quantityInput.value = ''; + priceInput.value = ''; + placementInput.value = ''; + row.querySelector('.cost').value = '0.00 ₽'; + updateTotals(); + return; + } + + // Заполняем список инструментов выбранной категории + const categoryId = parseInt(this.value); + const categoryTools = toolkitsByCategory[categoryId] || []; + + categoryTools.forEach(tool => { + const option = document.createElement('option'); + option.value = tool.id; + option.textContent = tool.title; + toolkitSelect.appendChild(option); + }); + }); + + // Обработчик выбора инструмента + const toolkitSelect = row.querySelector('.toolkit-select'); + toolkitSelect.addEventListener('change', function () { + this.classList.remove('is-invalid'); + if (!this.value) return; + + const toolId = parseInt(this.value); + + // Автозаполнение расположения, если есть + if (placementMap[toolId]) { + row.querySelector('.placement').value = placementMap[toolId]; + } + }); + + // Обработчики изменения количества и цены + const quantityInput = row.querySelector('.quantity'); + const priceInput = row.querySelector('.price'); + + [quantityInput, priceInput].forEach(input => { + input.addEventListener('input', function () { + this.classList.remove('is-invalid'); + calculateRowCost(row); + }); + }); + + // Обработчик удаления строки + const removeBtn = row.querySelector('.remove-row'); + removeBtn.addEventListener('click', function () { + if (rowsContainer.children.length <= 1) return; + + row.remove(); + updateTotals(); + + // Если осталась одна строка, делаем кнопку удаления недоступной + if (rowsContainer.children.length === 1) { + rowsContainer.querySelector('.remove-row').disabled = true; + } + }); + } + + // Очистка при закрытии модального окна + modal.addEventListener('hidden.bs.modal', () => { + setTimeout(() => { + if (modal.parentNode) { + modal.remove(); + } + }, 300); + }); + + // Показываем модальное окно + bsModal.show(); + + // Устанавливаем фокус на первую категорию + setTimeout(() => { + const firstCategorySelect = rowsContainer.querySelector('.category-select'); + if (firstCategorySelect) { + firstCategorySelect.focus(); + } + }, 500); } async function deleteToolbox(toolboxId) { diff --git a/api/static/js/toast.js b/api/static/js/toast.js index a57791a..392bd24 100644 --- a/api/static/js/toast.js +++ b/api/static/js/toast.js @@ -18,28 +18,28 @@ export function showInfo(message, type = 'info') { // Определяем классы в зависимости от типа const typeConfig = { - 'success': { bgClass: 'bg-success', icon: 'bi-check-circle', delay: 5000 }, - 'error': { bgClass: 'bg-danger', icon: 'bi-exclamation-circle', delay: 10000 }, - 'info': { bgClass: 'bg-info', icon: 'bi-info-circle', delay: 3000 }, - 'warning': { bgClass: 'bg-warning', icon: 'bi-exclamation-triangle', delay: 8000 } + 'success': { bgClass: 'bg-success', icon: 'bi-check-circle', delay: 5000, textColor: 'text-white' }, + 'error': { bgClass: 'bg-danger', icon: 'bi-exclamation-circle', delay: 10000, textColor: 'text-white' }, + 'info': { bgClass: 'bg-info', icon: 'bi-info-circle', delay: 3000, textColor: 'text-dark' }, + 'warning': { bgClass: 'bg-warning', icon: 'bi-exclamation-triangle', delay: 8000, textColor: 'text-dark' } }; const config = typeConfig[type] || typeConfig.info; // Создаем тост const toast = document.createElement('div'); - toast.className = `toast ${config.bgClass} text-white`; + toast.className = `toast ${config.bgClass} ${config.textColor} shadow-sm`; toast.id = toastId; toast.setAttribute('role', 'alert'); toast.setAttribute('aria-live', 'assertive'); toast.setAttribute('aria-atomic', 'true'); toast.innerHTML = ` -
+
Уведомление Только что - +
${message} diff --git a/db/handlers/__pycache__/actions.cpython-313.pyc b/db/handlers/__pycache__/actions.cpython-313.pyc index 9321a6e5ff18c3efc77621244220ea6c6f55d565..ba8a3c440757133b624014fb88fbdc248d969a38 100644 GIT binary patch delta 1404 zcmZvbO>7%Q6vuZGd)Icn&Sq`L&N}{RXxc2nNtC!y(?VK=QVMO;WYY8lORj4#!D$k= z<8?#}rK$usBnt1)Q!51_4hX82RjAZM1yT>?P%fO@nSJz-{PB>e&r~%a@u%4KV^Q$y{!-3-MVGC zXui?kkCR2EI*)!M_?hG|y=)F1o4q6rf0?z*q7Epv z1($29)s=-=t6XChHUQf2NaA&r2v@pz)HWrP;kKL{PAkzDQSh(%=zBv4?MSqH&h$5{ zZn=DWJlP{mPH@c>%cC*_`>6qckMAGH3QUEe_e|I%b~uBd!WiM2g(83JuEFGV96p-R zBe;XQpQBwKFYfD!V4PpKJ8xYfZ$tjjO%ijS9QvH_^LziuJ-Jm9&p;AzVtO~p!)MdK zl2dT@=$d>A7tMh_^Ro6oS0zlgJLhJ`2r0wbY?`dXkF%57hd8B-_yBPUH2VF}Jch>+ zCpa#8Gb1A-8PoG0@8K!^bWEI}jAAcB_^^*hZfE&j#(%fcN~N~!D)_Fh*z7cXI~UtK zhEidVf>9Oqp0n14Iupj>;R`ced9}3Eu!S}fM>l3gtqOSa9bXBxIuFBaLCeC!!-5*tN` z14XF3)jxr3nPrkck@uu?uMhjYL~g+LWfAJQQRpN;*SuNIY;| zmlhHgkN_beA@wY}X_2C?P(@Xks*9?vs9>8FqEgKtfDo`i?TQumyUt56(*4bSeCOPA z&pCf@vR^lu`b1Sj9`Vht9Vq|sojT6gpES*eFBKwYQ1dK%xi9T8_n9pv#SE9crYc60 z7>XE?l2Qnptxzp9?oWG!QjT#-{L84 zk0-c`0PVmFlpX<|dpB^jGEmhm9Ux_`+&_v*#hEj|D*`+-3q4eZ>} zctr+#V?Y>~0CeD4;1GaE^JjnN#$-@FEV|0C}T~I=t$f zFISd|_F0EpB2UR|&UXZoEKO!(q6&7l#~(j{o!#^!^R*~#oQk(j8No)FIwTpoKay&f zY1YL0H9iWZPNz(bPL2dAk{=zxwT7Rg0$k=sK5G(7nGKmL3!-4-of*22ABrL5YEWQr z)$ZiO-J@bfnw^yos(W%_oTOD0I>KqKq)C>y0~rysL9 zdVg}mHwS&5(q|6!{kMN`BI~ki-|McOd7iOF`nK4^YP4A#Yg>nG5%>^TqojFfa2nGY zU{>G*MbG7Oy5ap_HRvDntGKMG4`Kz7NgAWCr?jbgv92q8p|WI)pHr(?jed9zI+-{> zW^Y60eaF67{v=I)od! zbUz`TE;`Oi!zs$ZQ6hOA-?(fg($~fEi5MQ917uMFRgCHp&x!i_vJ@E3XW7Ssd!~8bge(RtYC;tMk!5hT@ diff --git a/db/handlers/__pycache__/stock.cpython-313.pyc b/db/handlers/__pycache__/stock.cpython-313.pyc index 86d6949dd116f24e67abc83418c4794166850874..b89ba55bdf4fc6670f0ced44397e8656cce71fb8 100644 GIT binary patch literal 12612 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 delta 2889 zcma)8Urd|V6~FiU=YL=u8z?v!Ol$&%1WX8NVv|ritR^ zbHa$oM6)lRlSX7FLtpX{rZkX)WK8yvF{NKXON21Xj3sRS61!n)D4Lp6(|VXLeRQx! z1liV)sJR36k-Wf{?@CKaez`hiHPA40{ME4DnJ;Ex+1e<@c%Gt_gW5WE$uH zqOCR3gj7*?gSxc4*b^cV@IVs+@a3gx5|;T-h5P)N?PPP9CIO^9&D4~xW+qcQvw;g5 zLI^Mif5+C-2eycYQGT6VYrEYU{Yz(*IbjHT;;cF!Rt$Sq%jPvbl~1dNl*?*5zi(?= zkPW-07t*uZP)bi3*6D0cSDBh&RiJA)cZXx<5{C9Q3}OHqbL?63!dxn^XZ3mJ2cd%N zia=f`n}Q@aepN-Ro~xa|%-psF*DS%mI-HA_-x@5Y)*TBicf56rXYW`&@4DV}tu;j# zU2E3Zz5Y{g54~4kbgb7zKCkM%87+y#_QXFG@)S+d<>b06_Hpuq{`aevs(#^IPOiCP ztJaCtu?e8}y84zJ#gWh5?Kgaj^4}cpJKl!(2bKo->D8*%FL0~v&1&Kv(9p$|gNsuf}5`e@#zmq(WK2ijdW{4iAqayDWe7w`? z7V?yT*3jyr6Li#CF+=z#!6uIr5)Q-|-JOQwS`dqU@@`HB2rO}-x#|$HlH^NIiqkYK zC;6w1x^J0;txN)CL()<|Q5hbGA!YN^1%AO7iZ{YAEEvk{r4*ad3~OG!R6absH@jyh zrZO4BZOVsJ`Akk_-5_a(71|s8558tD9+*CEBejuj4fE=XKOUpMrty=~2XP;C*#Quy zD=~lpI`K33wl4GE9nfY?uU$J{Gu(NZ8q%53iJ@`K$5bu{`6y(jSOlgF3DP>l@Sd5t z%nS;P<|gF@N;yL{TvEw9Wk%KGxg5XRRHwATgx1S{+Y}18{Ke^YSI?@oXG0{O=(dKT zzS16}%k-r5u{*}C{tjBpd;Oa|kiO@=Fl)9KadlDC8)YyrNf&X@yL;&^B!GNR-bTp`6A@Yy6);;wLWvR4akO#)Qr$=4T=xB61OI-bj3~+inQVwfWBeV3x(WN;nlMl z{^!-FtRAj8TK7(lYrEW0bZQ2%m#t& zIWzg+E4D`XbFHV2?mxTk?X5B(N4$luRMWU$T5-Ce zUj{Ur)xQf6i=$ir3kMi^7{~BF9PA7ZzY|5y^u#y-*NGnvHaKFa@idZW`18R=DnPQ; z@z;X)_Jq6ao$awvW(m|ELGmERN;$@lbr4G$Tn}jF%Iw>XbNTS&JkaLgLR(ltnQ~j6MRw{{MjZ{(dBuJxI(c^J-u* z*b{H8yC#}lJVC_6f8YLs@;m~#3;+iCvmM=!0g-SdTIj86sO-`$r#sPd{YJ-Uf`a)y zcY^mGKFojB?Bajy^3x#yd)L1K7vzg)519QI+)RFEo-GzTHh`oZ3I5Jm7>FUMnXGQu zzV~Wc{jr`cut&oA4MI4-4Z@hdunc`u+t&%4E|zJB)hL6x?;zk6kgF*V!rPu^}Yqkklbb0x{(2 zTmjw@qbM+Utec#K$AO+r!}CIC*{L~Q)g~ty?q@zE%$#B^I7EWCUb_wCjRjH?WVyAd ze?@?8I3+n!)W0UcHmWT0FfI0#2=Z&8GQRqsL9Y@P(~ZrLQ1MKO0K1m?23Z+3Voipv@JhOvOXboptFO$PRo`%Drn)s8^P7?Clp)XQ*_>w9 zezWM8Beg$zhIQbTLS`0RQ#OAg*84Vy|Wt^cNbjMC61JdkPG Gk^cY bool: logger.info(f"Приход инструмента {toolkit_id} на складе {toolbox_id} ...") newStocks = await StockHandler.add( toolkit_id=toolkit_id, diff --git a/db/handlers/stock.py b/db/handlers/stock.py index 3463ccc..a0439e1 100644 --- a/db/handlers/stock.py +++ b/db/handlers/stock.py @@ -1,34 +1,109 @@ from sqlalchemy import select -from db.schemas.stock import Stock +from db.schemas.stock import Placement, Stock from utils import logger -def filterQuantity(stocksData, filtered): +async def filterQuantity(stocksData, filtered): def filterStock(stock): if stock.quantity > 0: return stock else: return False + def convertPlacementData(placementList: list) -> dict: + placementDict = {} + for placementData in placementList: + toolboxId = placementData.toolbox_id + toolkitId = placementData.toolkit_id + placement = placementData.placement + if toolboxId not in placementDict: + placementDict[toolboxId] = {} + if toolkitId not in placementDict[toolboxId]: + placementDict[toolboxId][toolkitId] = placement + return placementDict + + async def combinePlacementAndStock(stockData): + if stockData: + if isinstance(stockData, list): + placementList = await PlacementHandler.getAll() + placementDict = convertPlacementData(placementList) + for stock in stockData: + toolboxId = stock.get("toolbox_id") + toolkitId = stock.get("toolkit_id") + stock["placement"] = placementDict.get(toolboxId, {}).get( + toolkitId, None + ) + else: + toolboxId = stockData.get("toolbox_id") + toolkitId = stockData.get("toolkit_id") + placement = await PlacementHandler.get(toolboxId, toolkitId) + stockData["placement"] = placement.placement + return stockData + + exitData = [] if isinstance(stocksData, list): - if len(stocksData) == 0: - return [] - stocksData.sort(key=lambda stock: stock.created_at) - filteredStocks = ( - list(filter(filterStock, stocksData)) if filtered else stocksData - ) - return [stock.toDict() for stock in filteredStocks] if filteredStocks else [] + if len(stocksData) > 0: + stocksData.sort(key=lambda stock: stock.created_at) + filteredStocks = ( + list(filter(filterStock, stocksData)) if filtered else stocksData + ) + if filteredStocks: + exitData = [stock.toDict() for stock in filteredStocks] else: stock = filterStock(stocksData) if filtered else stocksData if stock: - return stock.toDict() + exitData = stock.toDict() + return await combinePlacementAndStock(exitData) + + +class PlacementHandler: + async def add(**kwargs): + placement = kwargs.get("placement", None) + if not placement: + return + toolboxId = kwargs.get("toolbox_id", None) + toolkitId = kwargs.get("toolkit_id", None) + if not (toolkitId and toolboxId): + logger.error("toolkit_id or toolbox_id not found") + return + chechExists = await PlacementHandler.get(toolboxId, toolkitId) + if chechExists: + if placement != chechExists.placement: + logger.info( + f"Обновление места хранения инструмента {toolkitId} в ящике {toolboxId} с {chechExists.placement} на {placement}" + ) + return await PlacementHandler.edit(chechExists, placement) else: - return {} + logger.info( + f"Новое место хранения инструмента {toolkitId} в ящике {toolboxId} успешно создано {placement}" + ) + return await Placement(**kwargs).save() + + async def get(toolboxId: int, toolkitId: int) -> Placement: + from db import CRUD + + return await CRUD.read( + select(Placement) + .where(Placement.toolbox_id == toolboxId) + .where(Placement.toolkit_id == toolkitId) + ) + + async def getAll() -> list[Placement]: + from db import CRUD + + return await CRUD.read(select(Placement), all=True) + + async def edit(toolboxId: int, toolkitId: int, placement: str): + placementDB = await PlacementHandler.get(toolboxId, toolkitId) + if placementDB and placementDB.placement != placement: + return await placementDB.edit(placement) class StockHandler: async def add(**kwargs) -> dict: newStock = await Stock(**kwargs).save() + if "placement" in kwargs and kwargs["placement"]: + await PlacementHandler.add(**kwargs) logger.info( f"Новая запись об инструменте {newStock.toolkit_data.title} на складе {newStock.toolbox_data.title} успешно создана" ) @@ -38,7 +113,7 @@ class StockHandler: from db import CRUD stocks = await CRUD.read(select(Stock), all=True) - return filterQuantity(stocks, filtered) + return await filterQuantity(stocks, filtered) async def get(stockId: int, filtered: bool = True, record: bool = False): from db import CRUD @@ -47,21 +122,21 @@ class StockHandler: if not stock: logger.error(f"Запись {stockId} об остатках не найдена") return {} - return filterQuantity(stock, filtered) if not record else stock + return await filterQuantity(stock, filtered) if not record else stock async def getByToolboxId(toolboxId: int, filtered: bool = True): from db import CRUD query = select(Stock).where(Stock.toolbox_id == toolboxId) stocks = await CRUD.read(query, True) - return filterQuantity(stocks, filtered) + return await filterQuantity(stocks, filtered) async def getByToolkitId(toolkitId: int, filtered: bool = True): from db import CRUD query = select(Stock).where(Stock.toolkit_id == toolkitId) stocks = await CRUD.read(query, True) - return filterQuantity(stocks, filtered) + return await filterQuantity(stocks, filtered) async def getByToolboxIdAndToolkitId( toolboxId: int, toolkitId: int, filtered: bool = True @@ -74,7 +149,7 @@ class StockHandler: .where(Stock.toolkit_id == toolkitId) ) stocks = await CRUD.read(query, True) - return filterQuantity(stocks, filtered) + return await filterQuantity(stocks, filtered) async def getByToolboxIdAndToolkitIdAndQPrice( toolboxId: int, toolkitId: int, price: float, filtered: bool = True @@ -88,7 +163,7 @@ class StockHandler: .where(Stock.price == price) ) stocks = await CRUD.read(query, True) - return filterQuantity(stocks, filtered) + return await filterQuantity(stocks, filtered) async def edit(stockId: int, **kwargs) -> dict: from db import CRUD diff --git a/db/schemas/__pycache__/stock.cpython-313.pyc b/db/schemas/__pycache__/stock.cpython-313.pyc index 4eda2c89c6dd736eda2b743ecfa90d1c83eef88d..f3e586ac3368ebd2e008a458c79efdad96543492 100644 GIT binary patch delta 1950 zcmb7^U2GIp6vywK*_oZ)ZTE|vcDvNFw5087se%+;DG+E2ZbM7qt_JLyP1#QC+K+AS z%o0q9tsgPqiD+wVL?TX_3DXjU`Er3S8661Wp>ZYU zpTF`djS8K9G@FZ_Y<1skBnzh{9?L1sBp2D-WAmphl9sv7q6xz5*cMwjzckeaF3kRVqzR6fLu}IBI))4zZp?y?qsGYo+N? zW74pUBNcH`I^BA&1|83o&Ka&})}4W;chndgDNkAziMI$%ZlPRs$gP1WA!Uh>nU*FZ zhUJE1G2A^##>sXv>{&m&myCd&B-_Zq4noGret8=iqZK+>k!_`D8B?b1H4SUTvbgQ* zJ6AAftYT@pB7{730`pjX3NS}j63Guc-s||F^J4#no?DV!tKSUQFL)Naeh8=TC}eBu zk2RIV9;p&qtJ~q&EgA0jsx`3oYE8SeGskd0L zDwOYp=OzUt`7R(&1MC_nv7N3HdQ>BTIdU@;d*}Sy=NEdv4{g7pZLbpXoyX{_$b;Dw z*{fu5sLSenDVw|cH?3^3hzPm18hB?HzDK2 zLLSpzJX5v|GoR1auWKm&MQC_r;r9Tce`rcY;)DzN1p5>n>oQy9iz9 z|EiOIC8CZVoW)jGa~cT7&s643OH5-vXlB17f`BuF{5Z4PT<~NQEPrUQ0Ej01UmpLm zt|l$d@(8O1jYL@xxP`@7C_!HK@E8l{^gR-bII}ty<@MuRQo8LMnj9$@Q^vHliXCVB zXV&LtmSfF%5}_mN07rNnP3U>Tn-S36q=415z_;-nJPkNv8T|Do@z3Fy;_`yeA1vo9z zjsE+w;q&|;+J`p#0JfVQeDDG6@8js`gmnbp`tIkrfStWQFmG?bRUH#wDjD@T%4k{^ zVKp(Ek_JDPGeaZCls=8fyO*pr0=G{7&%h{seH#R9Jkg zvE5ID>HI(3`usCMTkD=Gjg}{k1AH3%P&ejok?}P)Jx2Fk=v^alTg%Z#+D|We?h?E^ G8~hEXPNV$* delta 823 zcmZvaPfQa*7{zCHyWReylol$bQa4oFE7KE_ukH%?4QI@IC$96;R9?X ze~lQQq(-n8K5nGTt`$l3G~PWC6i6?4r)e1$uWF*^9T#x8APs;*q|kkDko(RqEyRfE zDM3GYBV+$3yT(O)P7C(|+^a>H#68$gPzBpx!$wudn@eT1X-mdh z32kioiD_2K2C=e6$(CnlwAq>5oCH+|@{iYYT2@d zh)xJr>1VO3rkU4DFnJ2T^(k+LUJkv^KD)I&`y&w9$-St54P0u8Ag&$)n(*JARv5^W z>cps6wwstaqeylkPVyAUM8P}|4>Pd?8ZnFItOH%<1Pw_&0v@Dk3C*cueu0<6Xc;4G zm6Z!{le>tKP7uy={xs2cS^7G%eNH5wPXM|nDbS)V={Z#BwmXsG2f-J)mt74|oa5Lo zR5D20%;zigi~9!br(N=0I7W4OvD3?N+gq#XMN~y=gnpDWp$re>RqzOxYxF>lCnosF z|0kzukWXBAfYqd`=an}U-C2L@+6xWSrQl#fkWwuVlm=RED2*O7nRM*jq;}HxL2YrU z9fHrM+n(!HvsAC3DV%2rzYdExJP;v-P0;@n%0V*mgE diff --git a/db/schemas/stock.py b/db/schemas/stock.py index cc980b1..a2e67a1 100644 --- a/db/schemas/stock.py +++ b/db/schemas/stock.py @@ -32,13 +32,15 @@ class Stock(Base): ) quantity = Column(Integer, nullable=False) price = Column(Float, nullable=False) - placement = Column(String, nullable=True) created_at = Column(DateTime, default=datetime.now) updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) def __init__(self, **kwargs): for key, value in kwargs.items(): - setattr(self, key, value) + try: + setattr(self, key, value) + except Exception: + pass def toDict(self): return utils.toDict(self) @@ -48,3 +50,34 @@ class Stock(Base): async def edit(self, **kwargs): return await CRUD.update(Stock, self.id, **kwargs) + + +class Placement(Base): + __tablename__ = "placements" + + id = Column(Integer, primary_key=True, index=True) + toolbox_id = Column( + Integer, ForeignKey("toolboxes.id", ondelete="CASCADE"), nullable=False + ) + toolkit_id = Column( + Integer, ForeignKey("toolkits.id", ondelete="CASCADE"), nullable=False + ) + placement = Column(String, nullable=False) + created_at = Column(DateTime, default=datetime.now) + updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) + + def __init__(self, **kwargs): + for key, value in kwargs.items(): + try: + setattr(self, key, value) + except Exception: + pass + + def toDict(self): + return utils.toDict(self) + + async def save(self) -> "Placement": + return await CRUD.create(self, refresh=True) + + async def edit(self, placement: str): + return await CRUD.update(Placement, self.id, placement=placement)