andrei 1 rok pred
rodič
commit
c6b0cb84dd
33 zmenil súbory, kde vykonal 1567 pridanie a 3 odobranie
  1. 0 3
      README.md
  2. 1 0
      Приложение/.gitignore
  3. 10 0
      Приложение/Api/Dockerfile
  4. 15 0
      Приложение/Api/docker-compose.yml
  5. 61 0
      Приложение/Api/main.py
  6. 5 0
      Приложение/Api/requirements.txt
  7. 266 0
      Приложение/App/add_card_window.py
  8. 8 0
      Приложение/App/card_widget.py
  9. 0 0
      Приложение/App/data.json
  10. 29 0
      Приложение/App/main.py
  11. 122 0
      Приложение/App/main_window.py
  12. 152 0
      Приложение/App/src/parse_text.py
  13. 382 0
      Приложение/App/ui/AddCardWin.ui
  14. 75 0
      Приложение/App/ui/JobCard.ui
  15. 147 0
      Приложение/App/ui/MainWin.ui
  16. 224 0
      Приложение/App/view_card_window.py
  17. BIN
      Приложение/App/ТрудовыеКнижки
  18. BIN
      Приложение/App/ТрудовыеКнижки.exe
  19. BIN
      Приложение/media/img.png
  20. BIN
      Приложение/media/img2.png
  21. BIN
      Приложение/media/img3.png
  22. BIN
      Приложение/media/img4.png
  23. BIN
      Приложение/media/img6.png
  24. BIN
      Приложение/media/img7.png
  25. BIN
      Приложение/media/Скринкаст.webm
  26. BIN
      Приложение/media/Текст к презентации.docx
  27. BIN
      Приложение/media/Трудовые книжки.pdf
  28. BIN
      Приложение/media/Трудовые книжки_compressed.pdf
  29. 70 0
      Приложение/readme.md
  30. BIN
      Приложение/Примеры трудовых книжек/Пример 1.jpg
  31. BIN
      Приложение/Примеры трудовых книжек/Пример 2.jpg
  32. BIN
      РЖД_Трудовые книжки.pdf
  33. BIN
      Скринкаст.webm

+ 0 - 3
README.md

@@ -1,3 +0,0 @@
-# hackathon-digital-breakthrough-2024-RZD
-
-Весной 2024 года команда Numerum участвовала в хакатоне "Цифровой прорыв: сезон ИИ", кейс от РЖД - Рукописное распознавание текста.

+ 1 - 0
Приложение/.gitignore

@@ -0,0 +1 @@
+.idea/

+ 10 - 0
Приложение/Api/Dockerfile

@@ -0,0 +1,10 @@
+FROM python:3.9
+WORKDIR /app
+COPY . /app
+RUN apt-get update && apt-get install -y \
+    libgl1-mesa-glx \
+    libglib2.0-0 \
+    tesseract-ocr \
+    tesseract-ocr-rus 
+RUN pip install -r requirements.txt
+CMD ["python", "main.py"]

+ 15 - 0
Приложение/Api/docker-compose.yml

@@ -0,0 +1,15 @@
+version: '3'
+services:
+  app:
+    container_name: app
+    restart: always
+    build: .
+    ports:
+      - "6543:6543"
+    volumes:
+      - .:/app
+    networks:
+      - api_network
+networks:
+  api_network:
+    driver: bridge

+ 61 - 0
Приложение/Api/main.py

@@ -0,0 +1,61 @@
+import random
+from string import ascii_lowercase, digits
+
+import cv2
+import numpy as np
+from flask import Flask, request, jsonify
+from pytesseract import pytesseract
+from waitress import serve
+
+app = Flask(__name__)
+app.config['SECRET_KEY'] = ''.join([random.choice(ascii_lowercase + digits) for _ in range(10)])
+
+
+def preprocess_image_first(image):
+    gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
+    gray = cv2.GaussianBlur(gray, (1, 1), 0)
+    gray = cv2.convertScaleAbs(gray, alpha=1.6, beta=0)
+    binary_image = cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 11, 2)
+    return binary_image
+
+
+def preprocess_image_second(image):
+    gray_img = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
+    gaussian_img = cv2.GaussianBlur(gray_img, (5, 5), 0)
+    ret, thresh_img = cv2.threshold(gaussian_img, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)
+    return thresh_img
+
+
+def ocr_image(image):
+    custom_config = r'--oem 3 --psm 6'
+    text = pytesseract.image_to_string(image, config=custom_config, lang='rus')
+    return text
+
+
+@app.route('/recognition', methods=['POST'])
+def text_recognition():
+    if 'image' not in request.files:
+        return jsonify({'error': 'No image file provided'}), 400
+
+    file = request.files['image']
+    img = cv2.imdecode(np.frombuffer(file.read(), np.uint8), cv2.IMREAD_COLOR)
+
+    response = []
+
+    preprocessed_image = preprocess_image_first(img)
+    text = ocr_image(preprocessed_image)
+    response.append(text)
+
+    preprocessed_image = preprocess_image_second(img)
+    text = ocr_image(preprocessed_image)
+    response.append(text)
+
+    return jsonify(response)
+
+
+def main():
+    serve(app, host='0.0.0.0', port=6543)
+
+
+if __name__ == '__main__':
+    main()

+ 5 - 0
Приложение/Api/requirements.txt

@@ -0,0 +1,5 @@
+Flask
+numpy
+opencv-python
+pytesseract
+waitress

+ 266 - 0
Приложение/App/add_card_window.py

@@ -0,0 +1,266 @@
+import datetime
+import os
+import uuid
+from json import loads, dumps
+
+import requests
+from PyQt5 import uic
+from PyQt5.QtCore import QTimer, pyqtSignal, QThread
+from PyQt5.QtGui import QPixmap
+from PyQt5.QtWidgets import QMainWindow, QSpinBox, QDateEdit, QPushButton, QTableWidgetItem, QFileDialog, QMessageBox, \
+    QListWidgetItem, QRadioButton
+
+from src.parse_text import parser_text
+from view_card_window import ViewCardWin
+
+PATH_TO_DATA_FILE = 'data.json'
+
+
+class AddCardWin(QMainWindow):
+    response_received = pyqtSignal(name='response_received')
+
+    def __init__(self, parent):
+        super().__init__()
+        uic.loadUi('ui/AddCardWin.ui', self)
+        self.par = parent
+        self.pushButton.clicked.connect(self.back)
+        self.dateEdit_2.setDate(datetime.date.today())
+        self.pushButton_2.clicked.connect(self.lets_scan)
+        self.pushButton_3.clicked.connect(self.save_data)
+        self.pushButton_4.clicked.connect(self.add_row)
+        self.tableWidget.verticalHeader().setVisible(False)
+        self.pushButton_5.clicked.connect(self.dump_data)
+        self.pushButton_6.setVisible(False)
+        self.widget.setVisible(False)
+        self.widget_2.setVisible(False)
+        self.widget_3.setVisible(False)
+
+        self.parsed_texts = []
+        self.all_words = []
+
+        QTimer.singleShot(100, self.resize_table)
+
+    def delete_row(self):
+        sender = self.sender()
+        self.tableWidget.removeRow(sender.row)
+
+        for row in range(sender.row, self.tableWidget.rowCount()):
+            self.tableWidget.cellWidget(row, 4).row -= 1
+
+    def add_row(self):
+        self.tableWidget.setRowCount(self.tableWidget.rowCount() + 1)
+
+        number_spin_box = QSpinBox(self)
+        number_spin_box.setMinimum(1)
+        number_spin_box.setMaximum(100000)
+        number_spin_box.setValue(self.tableWidget.rowCount())
+        self.tableWidget.setCellWidget(self.tableWidget.rowCount() - 1, 0, number_spin_box)
+
+        date_edit = QDateEdit()
+        date_edit.setDate(datetime.date.today())
+        date_edit.setMaximumDate(datetime.date.today())
+        date_edit.setCalendarPopup(True)
+        self.tableWidget.setCellWidget(self.tableWidget.rowCount() - 1, 1, date_edit)
+
+        delete_button = QPushButton()
+        delete_button.setText('Удалить')
+        delete_button.row = self.tableWidget.rowCount() - 1
+        delete_button.clicked.connect(self.delete_row)
+        self.tableWidget.setCellWidget(self.tableWidget.rowCount() - 1, 4, delete_button)
+
+        self.tableWidget.setItem(self.tableWidget.rowCount() - 1, 2, QTableWidgetItem(''))
+        self.tableWidget.setItem(self.tableWidget.rowCount() - 1, 3, QTableWidgetItem(''))
+
+        self.resize_table()
+
+    def resize_table(self):
+        if self.tableWidget.rowCount():
+            row_height = min([90, max([50, self.tableWidget.height() // self.tableWidget.rowCount()])])
+
+            for row in range(self.tableWidget.rowCount()):
+                self.tableWidget.setRowHeight(row, row_height)
+
+        column_width = max([100, self.tableWidget.width() // self.tableWidget.columnCount()])
+
+        for column in range(self.tableWidget.columnCount()):
+            self.tableWidget.setColumnWidth(column, column_width)
+
+    def resizeEvent(self, a0, QResizeEvent=None):
+        self.resize_table()
+
+    def save_data(self):
+        card_info = {
+            "uuid": str(uuid.uuid4()),
+            "title": {
+                "serial": self.lineEdit.text(),
+                "number": self.lineEdit_2.text(),
+                "first_name": self.lineEdit_4.text(),
+                "last_name": self.lineEdit_3.text(),
+                "patronymic": self.lineEdit_5.text(),
+                "birthday": str(self.dateEdit.date().toPyDate()),
+                "issue_date": str(self.dateEdit_2.date().toPyDate()),
+                "profession": self.lineEdit_6.text(),
+                "education": self.lineEdit_7.text()
+            },
+            "job": [
+                {
+                    "number": self.tableWidget.cellWidget(row, 0).value(),
+                    "date": str(self.tableWidget.cellWidget(row, 1).date().toPyDate()),
+                    "job_info": self.tableWidget.item(row, 2).text(),
+                    "basis": self.tableWidget.item(row, 3).text()
+                }
+                for row in range(self.tableWidget.rowCount())
+            ]
+        }
+
+        with open(PATH_TO_DATA_FILE, 'r', encoding='utf-8') as file:
+            data = loads(file.read())
+
+        data.append(card_info)
+
+        with open(PATH_TO_DATA_FILE, 'w', encoding='utf-8') as write_file:
+            write_file.write(dumps(data))
+
+        self.view_win = ViewCardWin(self.par, card_info)
+        self.view_win.show()
+        self.close()
+
+    def dump_data(self):
+        card_info = {
+            "title": {
+                "serial": self.lineEdit.text(),
+                "number": self.lineEdit_2.text(),
+                "first_name": self.lineEdit_4.text(),
+                "last_name": self.lineEdit_3.text(),
+                "patronymic": self.lineEdit_5.text(),
+                "birthday": str(self.dateEdit.date().toPyDate()),
+                "issue_date": str(self.dateEdit_2.date().toPyDate()),
+                "profession": self.lineEdit_6.text(),
+                "education": self.lineEdit_7.text()
+            },
+            "job": [
+                {
+                    "number": self.tableWidget.cellWidget(row, 0).value(),
+                    "date": str(self.tableWidget.cellWidget(row, 1).date().toPyDate()),
+                    "job_info": self.tableWidget.item(row, 2).text(),
+                    "basis": self.tableWidget.item(row, 3).text()
+                }
+                for row in range(self.tableWidget.rowCount())
+            ]
+        }
+
+        options = QFileDialog.Options()
+        directory = QFileDialog.getExistingDirectory(self, "Выберите путь сохранения", options=options)
+        if directory:
+            file_name = os.path.join(directory,
+                                     f'{card_info["title"]["last_name"]}_{card_info["title"]["first_name"]}.json')
+            with open(file_name, 'w', encoding='utf-8') as file:
+                file.write(dumps(card_info))
+
+            QMessageBox.information(self, 'Данные выгружены', 'Данные успешно выгружены в файл')
+
+    def lets_scan(self):
+        options = QFileDialog.Options()
+        file_name, _ = QFileDialog.getOpenFileName(self, "Select Image File", "", "Images (*.png *.jpg *.jpeg *.bmp)",
+                                                   options=options)
+        if file_name:
+            self.all_words = []
+            self.parsed_texts = []
+
+            layout = self.widget_3.children()[0]
+            count = layout.count()
+
+            for i in range(count):
+                item = layout.itemAt(i)
+                if item.widget():
+                    item.widget().close()
+
+            self.image_path = file_name
+            self.start_ocr_thread()
+            pixmap = QPixmap(self.image_path)
+            self.widget.setVisible(True)
+            self.label_11.setPixmap(pixmap)
+
+    def start_ocr_thread(self):
+        self.ocr_thread = OCRThread(self.image_path)
+        self.ocr_thread.ocr_completed.connect(self.on_ocr_completed)
+        self.ocr_thread.start()
+
+    def on_ocr_completed(self, result):
+        if 'Error: ' in result[0]:
+            QMessageBox.warning(self, 'Ошибка оцифровки', result[0])
+            return
+
+        self.widget_2.setVisible(True)
+        self.widget_3.setVisible(True)
+
+        layout = self.widget_3.children()[0]
+        count = layout.count()
+
+        for i in range(count):
+            item = layout.itemAt(i)
+            if item.widget():
+                item.widget().close()
+
+        for variant_index in range(len(result)):
+            parsed_text, all_word = parser_text(result[variant_index])
+            self.parsed_texts.append(parsed_text)
+            self.all_words.append(all_word)
+            radio_button = QRadioButton()
+            radio_button.setText(f'Вариант {str(variant_index + 1)}')
+            radio_button.toggled.connect(self.render_result)
+            radio_button.index = variant_index
+            self.widget_3.children()[0].addWidget(radio_button)
+            if variant_index == 0:
+                radio_button.setChecked(True)
+
+    def render_result(self):
+        self.listWidget.clear()
+        sender = self.sender()
+
+        parsed_text = self.parsed_texts[sender.index]
+        all_word = self.all_words[sender.index]
+
+        self.lineEdit.setText(parsed_text['title']['serial'])
+        self.lineEdit_2.setText(parsed_text['title']['number'])
+        self.lineEdit_3.setText(parsed_text['title']['last_name'])
+        self.lineEdit_4.setText(parsed_text['title']['first_name'])
+        self.lineEdit_5.setText(parsed_text['title']['patronymic'])
+        self.lineEdit_6.setText(parsed_text['title']['profession'])
+        self.lineEdit_7.setText(parsed_text['title']['education'])
+
+        for word in all_word:
+            list_item = QListWidgetItem()
+            list_item.setText(word)
+            self.listWidget.addItem(list_item)
+
+        self.resize_table()
+
+    def back(self):
+        self.par.show()
+        self.close()
+
+
+class OCRThread(QThread):
+    ocr_completed = pyqtSignal(list, name='ocr_completed')
+
+    def __init__(self, image_path):
+        super().__init__()
+        self.image_path = image_path
+
+    def run(self):
+        url = 'http://localhost:6543/recognition'
+        files = {'image': open(self.image_path, 'rb')}
+
+        try:
+            response = requests.post(url, files=files)
+        except Exception:
+            text = [f"Error: Ошибка подключения к серверу"]
+            self.ocr_completed.emit(text)
+            return
+
+        if response.status_code == 200:
+            text = response.json()
+        else:
+            text = [f"Error: {response.status_code}"]
+        self.ocr_completed.emit(text)

+ 8 - 0
Приложение/App/card_widget.py

@@ -0,0 +1,8 @@
+from PyQt5 import uic
+from PyQt5.QtWidgets import QWidget
+
+
+class CardWidget(QWidget):
+    def __init__(self):
+        super().__init__()
+        uic.loadUi('ui/JobCard.ui', self)

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 0 - 0
Приложение/App/data.json


+ 29 - 0
Приложение/App/main.py

@@ -0,0 +1,29 @@
+import sys
+
+from PyQt5.QtWidgets import QApplication
+from json import loads
+
+from main_window import MainWindow
+
+PATH_TO_DATA_FILE = 'data.json'
+
+
+def check_data_file():
+    try:
+        with open(PATH_TO_DATA_FILE, 'r', encoding='utf-8') as file:
+            loads(file.read())
+    except Exception:
+        with open(PATH_TO_DATA_FILE, 'w', encoding='utf-8') as file:
+            file.write('[]')
+
+
+def main():
+    check_data_file()
+    app = QApplication(sys.argv)
+    win = MainWindow()
+    win.show()
+    sys.exit(app.exec())
+
+
+if __name__ == '__main__':
+    main()

+ 122 - 0
Приложение/App/main_window.py

@@ -0,0 +1,122 @@
+import datetime
+import os
+from json import loads, dumps
+
+from PyQt5.QtWidgets import QMainWindow, QListWidgetItem, QListWidget, QMessageBox, QFileDialog
+from PyQt5 import uic
+
+from add_card_window import AddCardWin
+from card_widget import CardWidget
+from view_card_window import ViewCardWin
+
+PATH_TO_DATA_FILE = 'data.json'
+
+
+class MainWindow(QMainWindow):
+    def __init__(self):
+        super().__init__()
+        uic.loadUi('ui/MainWin.ui', self)
+        self.pushButton.setVisible(False)
+        self.pushButton.clicked.connect(self.clear_find)
+        self.lineEdit.textChanged.connect(self.check_find_string)
+        self.pushButton_2.clicked.connect(self.add_card)
+        self.pushButton_3.clicked.connect(self.dump_checked)
+        self.checkBox.stateChanged.connect(self.select_all)
+        self.cards_info = []
+        with open(PATH_TO_DATA_FILE, 'r', encoding='utf-8') as file:
+            self.cards_info = loads(file.read())
+        self.checked_cards = []
+        self.load_cards()
+
+    def select_all(self):
+        for item_index in range(self.listWidget.count()):
+            item = self.listWidget.item(item_index)
+            widget = self.listWidget.itemWidget(item)
+            widget.checkBox.blockSignals(True)
+            widget.checkBox.setChecked(self.checkBox.isChecked())
+            self.checked_cards.append(widget.checkBox.card)
+            widget.checkBox.blockSignals(False)
+
+    def dump_checked(self):
+        if not self.checked_cards:
+            QMessageBox.warning(self, 'Ошибка выгрузки данных', 'Не выбраны книжки для выгрузки')
+            return
+
+        options = QFileDialog.Options()
+        directory = QFileDialog.getExistingDirectory(self, "Выберите путь сохранения", options=options)
+
+        if directory:
+            dumping_card_info = []
+            for card in self.checked_cards:
+                copy_card = card.copy()
+                del copy_card['uuid']
+                dumping_card_info.append(copy_card)
+
+            file_name = os.path.join(directory,
+                                     f'Трудовые книжки - {str(datetime.date.today())}.json')
+            with open(file_name, 'w', encoding='utf-8') as file:
+                file.write(dumps(dumping_card_info))
+
+            QMessageBox.information(self, 'Данные выгружены', 'Данные успешно выгружены в файл')
+
+    def clear_find(self):
+        self.lineEdit.clear()
+
+    def add_card(self):
+        self.add_card_win = AddCardWin(self)
+        self.add_card_win.show()
+        self.close()
+
+    def check_find_string(self):
+        if self.lineEdit.text():
+            self.pushButton.setVisible(True)
+        else:
+            self.pushButton.setVisible(False)
+
+        self.find_card()
+
+    def find_card(self):
+        for item_index in range(self.listWidget.count()):
+            item = self.listWidget.item(item_index)
+            widget = self.listWidget.itemWidget(item)
+            if self.lineEdit.text().lower().strip() in widget.label.text().lower().strip():
+                item.setHidden(False)
+            else:
+                item.setHidden(True)
+
+    def load_cards(self):
+        self.listWidget.clear()
+        for card in self.cards_info:
+            card_widget = CardWidget()
+            list_item = QListWidgetItem()
+            card_widget.label.setText(
+                f'{card["title"]["last_name"]} {card["title"]["first_name"]} - {card["title"]["issue_date"]}')
+            card_widget.checkBox.stateChanged.connect(self.select_card)
+            card_widget.checkBox.card = card
+            card_widget.pushButton.card = card
+            card_widget.pushButton.clicked.connect(self.open_card)
+            list_item.setSizeHint(card_widget.sizeHint())
+            self.listWidget.addItem(list_item)
+            self.listWidget.setItemWidget(list_item, card_widget)
+
+    def select_card(self):
+        sender = self.sender()
+        if sender.isChecked():
+            self.checked_cards.append(sender.card.copy())
+        else:
+            del self.checked_cards[self.checked_cards.index(sender.card)]
+        self.checkBox.blockSignals(True)
+        self.checkBox.setChecked(len(self.checked_cards) == len(self.cards_info))
+        self.checkBox.blockSignals(False)
+
+    def open_card(self):
+        sender = self.sender()
+        self.view_card = ViewCardWin(self, sender.card)
+        self.view_card.show()
+        self.close()
+
+    def showEvent(self, event):
+        super(MainWindow, self).showEvent(event)
+        with open(PATH_TO_DATA_FILE, 'r', encoding='utf-8') as file:
+            self.cards_info = loads(file.read())
+        self.load_cards()

+ 152 - 0
Приложение/App/src/parse_text.py

@@ -0,0 +1,152 @@
+import datetime
+import re
+from string import punctuation
+
+
+def parser_text(text):
+    data = {
+        "title": {
+            "serial": "",
+            "number": "",
+            "first_name": "",
+            "last_name": "",
+            "patronymic": "",
+            "birthday": "",
+            "issue_date": "",
+            "profession": "",
+            "education": ""
+        }
+    }
+
+    serial_match = re.search(r'№\s*(\d+)', text)
+    if serial_match:
+        data["title"]["number"] = serial_match.group(1)
+
+    last_name_match = re.search(r'\bфамилия[:\s]+([А-ЯЁа-яё]+)', text, re.IGNORECASE)
+    text_rows = text.split('\n')
+    number_index = list(filter(lambda x: '№' in x, text_rows))
+    if not number_index:
+        next_index = 3
+    else:
+        next_index = text_rows.index(number_index[0])
+    if last_name_match:
+        data["title"]["last_name"] = last_name_match.group(1)
+    else:
+        if number_index:
+            number_index = next_index
+            for i in range(1, 4):
+                row = text_rows[number_index + i].split()
+                row_copy = row.copy()
+                if len(row) < 2:
+                    continue
+                row.sort(key=lambda x: (row_copy.index(x), len(x)))
+                row = list(filter(lambda x: len(x) > 3, row))
+                if row:
+                    if len(row[-1]) > 3:
+                        data["title"]["last_name"] = row[-1]
+                        next_index = number_index + i
+                        break
+
+    first_name_match = re.search(r'\bимя[:\s]+([А-ЯЁа-яё]+)', text, re.IGNORECASE)
+    if first_name_match:
+        data["title"]["first_name"] = first_name_match.group(1)
+    else:
+        if number_index:
+            number_index = next_index
+            for i in range(1, 4):
+                row = text_rows[number_index + i].split()
+                row_copy = row.copy()
+                if len(row) < 2:
+                    continue
+                row.sort(key=lambda x: (row_copy.index(x), len(x)))
+                row = list(filter(lambda x: len(x) > 3, row))
+                if row:
+                    if len(row[-1]) > 3:
+                        data["title"]["first_name"] = row[-1]
+                        next_index = number_index + i
+                        break
+
+    patronymic_match = re.search(r'\bотчество[:\s]+([А-ЯЁа-яё]+)', text, re.IGNORECASE)
+    if patronymic_match:
+        data["title"]["patronymic"] = patronymic_match.group(1)
+    else:
+        if number_index:
+            number_index = next_index
+            for i in range(1, 4):
+                row = text_rows[number_index + i].split()
+                row_copy = row.copy()
+                if len(row) < 2:
+                    continue
+                row.sort(key=lambda x: (row_copy.index(x), len(x)))
+                row = list(filter(lambda x: len(x) > 3, row))
+                if row:
+                    if len(row[-1]) > 3:
+                        data["title"]["patronymic"] = row[-1]
+                        next_index = number_index + i
+                        break
+
+    patronymic_match = re.search(r'\bние[:\s]+([А-ЯЁа-яё]+)', text, re.IGNORECASE)
+    if patronymic_match:
+        data["title"]["education"] = patronymic_match.group(1)
+    else:
+        if number_index:
+            number_index = next_index
+            for i in range(1, 4):
+                row = text_rows[number_index + i].split()
+                row_copy = row.copy()
+                if len(row) < 2:
+                    continue
+                row.sort(key=lambda x: (row_copy.index(x), len(x)))
+                row = list(filter(lambda x: len(x) > 3, row))
+                if row:
+                    if len(row[-1]) > 3:
+                        data["title"]["education"] = row[-1]
+                        next_index = number_index + i
+                        break
+
+    patronymic_match = re.search(r'\bость[:\s]+([А-ЯЁа-яё]+)', text, re.IGNORECASE)
+    if patronymic_match:
+        data["title"]["profession"] = patronymic_match.group(1)
+    else:
+        if number_index:
+            number_index = next_index
+            for i in range(1, 4):
+                row = text_rows[number_index + i].split()
+                row_copy = row.copy()
+                if len(row) < 2:
+                    continue
+                row.sort(key=lambda x: (row_copy.index(x), len(x)))
+                row = list(filter(lambda x: len(x) > 3, row))
+                if row:
+                    if len(row[-1]) > 3:
+                        data["title"]["profession"] = row[-1]
+                        next_index = number_index + i
+                        break
+
+    birthday_match = re.search(r'\bния[:\s]+([0-3]?[0-9] [а-яё]+ \d{4})', text, re.IGNORECASE)
+    if birthday_match is None:
+        birthday_match = re.search(r'(\d{2}\s*\.\s*\d{2}\s*\.\s*\d{4})', text, re.IGNORECASE)
+    if birthday_match:
+        try:
+            data["title"]["birthday"] = datetime.datetime.strptime(birthday_match.group(1), '%d %B %Y').strftime(
+                '%Y-%m-%d')
+        except ValueError:
+            pass
+
+    for key in data['title'].keys():
+        if key not in ['birthday', 'issue_date']:
+            data['title'][key] = replace_punctuation_and_lower(data['title'][key])
+
+    all_word = []
+    for word_row in [words.split() for words in text_rows]:
+        for word in word_row:
+            if len(word) > 3:
+                all_word.append(word)
+
+    return data, all_word
+
+
+def replace_punctuation_and_lower(text):
+    for symbol in punctuation + '`‘':
+        text = text.replace(symbol, '')
+    return text.lower().capitalize()

+ 382 - 0
Приложение/App/ui/AddCardWin.ui

@@ -0,0 +1,382 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>MainWindow</class>
+ <widget class="QMainWindow" name="MainWindow">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>1566</width>
+    <height>639</height>
+   </rect>
+  </property>
+  <property name="minimumSize">
+   <size>
+    <width>600</width>
+    <height>600</height>
+   </size>
+  </property>
+  <property name="font">
+   <font>
+    <pointsize>13</pointsize>
+   </font>
+  </property>
+  <property name="windowTitle">
+   <string>Трудовая книжка</string>
+  </property>
+  <widget class="QWidget" name="centralwidget">
+   <layout class="QGridLayout" name="gridLayout">
+    <item row="0" column="4">
+     <widget class="QPushButton" name="pushButton_3">
+      <property name="text">
+       <string>Сохранить</string>
+      </property>
+     </widget>
+    </item>
+    <item row="0" column="5">
+     <spacer name="horizontalSpacer_2">
+      <property name="orientation">
+       <enum>Qt::Horizontal</enum>
+      </property>
+      <property name="sizeHint" stdset="0">
+       <size>
+        <width>40</width>
+        <height>20</height>
+       </size>
+      </property>
+     </spacer>
+    </item>
+    <item row="0" column="7" rowspan="5" colspan="2">
+     <widget class="QWidget" name="widget_2" native="true">
+      <layout class="QGridLayout" name="gridLayout_8">
+       <item row="0" column="0">
+        <widget class="QLabel" name="label_12">
+         <property name="text">
+          <string>Распознанные слова</string>
+         </property>
+         <property name="alignment">
+          <set>Qt::AlignCenter</set>
+         </property>
+        </widget>
+       </item>
+       <item row="1" column="0">
+        <widget class="QListWidget" name="listWidget"/>
+       </item>
+      </layout>
+     </widget>
+    </item>
+    <item row="1" column="5" rowspan="4" colspan="2">
+     <layout class="QGridLayout" name="gridLayout_3">
+      <item row="0" column="0">
+       <spacer name="horizontalSpacer">
+        <property name="orientation">
+         <enum>Qt::Horizontal</enum>
+        </property>
+        <property name="sizeHint" stdset="0">
+         <size>
+          <width>40</width>
+          <height>20</height>
+         </size>
+        </property>
+       </spacer>
+      </item>
+      <item row="0" column="4">
+       <widget class="QPushButton" name="pushButton_4">
+        <property name="text">
+         <string>+</string>
+        </property>
+       </widget>
+      </item>
+      <item row="0" column="3">
+       <spacer name="horizontalSpacer_5">
+        <property name="orientation">
+         <enum>Qt::Horizontal</enum>
+        </property>
+        <property name="sizeHint" stdset="0">
+         <size>
+          <width>40</width>
+          <height>20</height>
+         </size>
+        </property>
+       </spacer>
+      </item>
+      <item row="0" column="2">
+       <widget class="QLabel" name="label_10">
+        <property name="text">
+         <string>Сведенья о работе</string>
+        </property>
+        <property name="alignment">
+         <set>Qt::AlignCenter</set>
+        </property>
+       </widget>
+      </item>
+      <item row="1" column="0" colspan="6">
+       <widget class="QTableWidget" name="tableWidget">
+        <column>
+         <property name="text">
+          <string>№ Записи</string>
+         </property>
+        </column>
+        <column>
+         <property name="text">
+          <string>Дата</string>
+         </property>
+        </column>
+        <column>
+         <property name="text">
+          <string>Сведенья</string>
+         </property>
+        </column>
+        <column>
+         <property name="text">
+          <string>Документ</string>
+         </property>
+        </column>
+        <column>
+         <property name="text">
+          <string>Удаление</string>
+         </property>
+        </column>
+       </widget>
+      </item>
+      <item row="0" column="1">
+       <spacer name="horizontalSpacer_6">
+        <property name="orientation">
+         <enum>Qt::Horizontal</enum>
+        </property>
+        <property name="sizeHint" stdset="0">
+         <size>
+          <width>40</width>
+          <height>20</height>
+         </size>
+        </property>
+       </spacer>
+      </item>
+     </layout>
+    </item>
+    <item row="0" column="2">
+     <widget class="QPushButton" name="pushButton">
+      <property name="text">
+       <string>Назад</string>
+      </property>
+     </widget>
+    </item>
+    <item row="0" column="3">
+     <spacer name="horizontalSpacer_3">
+      <property name="orientation">
+       <enum>Qt::Horizontal</enum>
+      </property>
+      <property name="sizeHint" stdset="0">
+       <size>
+        <width>40</width>
+        <height>20</height>
+       </size>
+      </property>
+     </spacer>
+    </item>
+    <item row="0" column="0" rowspan="5">
+     <spacer name="verticalSpacer">
+      <property name="orientation">
+       <enum>Qt::Vertical</enum>
+      </property>
+      <property name="sizeHint" stdset="0">
+       <size>
+        <width>20</width>
+        <height>40</height>
+       </size>
+      </property>
+     </spacer>
+    </item>
+    <item row="0" column="6">
+     <spacer name="horizontalSpacer_4">
+      <property name="orientation">
+       <enum>Qt::Horizontal</enum>
+      </property>
+      <property name="sizeHint" stdset="0">
+       <size>
+        <width>40</width>
+        <height>20</height>
+       </size>
+      </property>
+     </spacer>
+    </item>
+    <item row="1" column="2" rowspan="4" colspan="3">
+     <layout class="QGridLayout" name="gridLayout_2">
+      <item row="1" column="1" colspan="2">
+       <widget class="QLineEdit" name="lineEdit_2"/>
+      </item>
+      <item row="4" column="0">
+       <widget class="QLabel" name="label_5">
+        <property name="text">
+         <string>Отчество</string>
+        </property>
+       </widget>
+      </item>
+      <item row="3" column="0">
+       <widget class="QLabel" name="label_4">
+        <property name="text">
+         <string>Имя</string>
+        </property>
+       </widget>
+      </item>
+      <item row="3" column="1" colspan="2">
+       <widget class="QLineEdit" name="lineEdit_4"/>
+      </item>
+      <item row="0" column="1" colspan="2">
+       <widget class="QLineEdit" name="lineEdit"/>
+      </item>
+      <item row="7" column="0">
+       <widget class="QLabel" name="label_8">
+        <property name="text">
+         <string>Профессия/специальность</string>
+        </property>
+       </widget>
+      </item>
+      <item row="5" column="1" colspan="2">
+       <widget class="QDateEdit" name="dateEdit">
+        <property name="calendarPopup">
+         <bool>true</bool>
+        </property>
+       </widget>
+      </item>
+      <item row="11" column="0" colspan="3">
+       <widget class="QPushButton" name="pushButton_6">
+        <property name="text">
+         <string>Удалить книжку</string>
+        </property>
+       </widget>
+      </item>
+      <item row="2" column="0">
+       <widget class="QLabel" name="label_3">
+        <property name="text">
+         <string>Фамилия</string>
+        </property>
+       </widget>
+      </item>
+      <item row="8" column="0">
+       <widget class="QLabel" name="label_9">
+        <property name="text">
+         <string>Образование</string>
+        </property>
+       </widget>
+      </item>
+      <item row="5" column="0">
+       <widget class="QLabel" name="label_6">
+        <property name="text">
+         <string>Дата рождения</string>
+        </property>
+        <property name="wordWrap">
+         <bool>true</bool>
+        </property>
+       </widget>
+      </item>
+      <item row="9" column="0" colspan="3">
+       <widget class="QPushButton" name="pushButton_2">
+        <property name="text">
+         <string>Оцифровать главную страницу</string>
+        </property>
+       </widget>
+      </item>
+      <item row="6" column="0">
+       <widget class="QLabel" name="label_7">
+        <property name="text">
+         <string>Дата выдачи книжки</string>
+        </property>
+        <property name="wordWrap">
+         <bool>true</bool>
+        </property>
+       </widget>
+      </item>
+      <item row="10" column="0" colspan="3">
+       <widget class="QPushButton" name="pushButton_5">
+        <property name="text">
+         <string>Выгрузить в JSON</string>
+        </property>
+       </widget>
+      </item>
+      <item row="2" column="1" colspan="2">
+       <widget class="QLineEdit" name="lineEdit_3"/>
+      </item>
+      <item row="6" column="1" colspan="2">
+       <widget class="QDateEdit" name="dateEdit_2">
+        <property name="calendarPopup">
+         <bool>true</bool>
+        </property>
+       </widget>
+      </item>
+      <item row="4" column="1" colspan="2">
+       <widget class="QLineEdit" name="lineEdit_5"/>
+      </item>
+      <item row="1" column="0">
+       <widget class="QLabel" name="label_2">
+        <property name="text">
+         <string>Номер трудовой книжки</string>
+        </property>
+        <property name="wordWrap">
+         <bool>true</bool>
+        </property>
+       </widget>
+      </item>
+      <item row="8" column="1" colspan="2">
+       <widget class="QLineEdit" name="lineEdit_7"/>
+      </item>
+      <item row="7" column="1" colspan="2">
+       <widget class="QLineEdit" name="lineEdit_6"/>
+      </item>
+      <item row="0" column="0">
+       <widget class="QLabel" name="label">
+        <property name="text">
+         <string>Серия трудовой книжки</string>
+        </property>
+        <property name="wordWrap">
+         <bool>true</bool>
+        </property>
+       </widget>
+      </item>
+      <item row="12" column="0" colspan="3">
+       <widget class="QWidget" name="widget_3" native="true">
+        <layout class="QGridLayout" name="gridLayout_6"/>
+       </widget>
+      </item>
+     </layout>
+    </item>
+    <item row="0" column="1" rowspan="5">
+     <widget class="QWidget" name="widget" native="true">
+      <layout class="QGridLayout" name="gridLayout_4">
+       <item row="0" column="0">
+        <widget class="QLabel" name="label_11">
+         <property name="sizePolicy">
+          <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+           <horstretch>0</horstretch>
+           <verstretch>0</verstretch>
+          </sizepolicy>
+         </property>
+         <property name="minimumSize">
+          <size>
+           <width>400</width>
+           <height>222</height>
+          </size>
+         </property>
+         <property name="maximumSize">
+          <size>
+           <width>400</width>
+           <height>222</height>
+          </size>
+         </property>
+         <property name="text">
+          <string/>
+         </property>
+         <property name="scaledContents">
+          <bool>true</bool>
+         </property>
+        </widget>
+       </item>
+      </layout>
+     </widget>
+    </item>
+   </layout>
+  </widget>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>

+ 75 - 0
Приложение/App/ui/JobCard.ui

@@ -0,0 +1,75 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>Form</class>
+ <widget class="QWidget" name="Form">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>719</width>
+    <height>100</height>
+   </rect>
+  </property>
+  <property name="sizePolicy">
+   <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
+    <horstretch>0</horstretch>
+    <verstretch>0</verstretch>
+   </sizepolicy>
+  </property>
+  <property name="windowTitle">
+   <string>Form</string>
+  </property>
+  <property name="styleSheet">
+   <string notr="true">#Form {
+	border: 1px solid black;
+border-radius: 10px;
+}</string>
+  </property>
+  <layout class="QGridLayout" name="gridLayout">
+   <item row="0" column="0">
+    <widget class="QCheckBox" name="checkBox">
+     <property name="sizePolicy">
+      <sizepolicy hsizetype="Preferred" vsizetype="Expanding">
+       <horstretch>0</horstretch>
+       <verstretch>0</verstretch>
+      </sizepolicy>
+     </property>
+     <property name="text">
+      <string/>
+     </property>
+    </widget>
+   </item>
+   <item row="0" column="2">
+    <widget class="QPushButton" name="pushButton">
+     <property name="text">
+      <string>Открыть</string>
+     </property>
+    </widget>
+   </item>
+   <item row="0" column="1">
+    <widget class="QLabel" name="label">
+     <property name="sizePolicy">
+      <sizepolicy hsizetype="Expanding" vsizetype="Expanding">
+       <horstretch>0</horstretch>
+       <verstretch>0</verstretch>
+      </sizepolicy>
+     </property>
+     <property name="maximumSize">
+      <size>
+       <width>1000</width>
+       <height>16777215</height>
+      </size>
+     </property>
+     <property name="text">
+      <string/>
+     </property>
+     <property name="wordWrap">
+      <bool>true</bool>
+     </property>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>

+ 147 - 0
Приложение/App/ui/MainWin.ui

@@ -0,0 +1,147 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>MainWindow</class>
+ <widget class="QMainWindow" name="MainWindow">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>1063</width>
+    <height>780</height>
+   </rect>
+  </property>
+  <property name="sizePolicy">
+   <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
+    <horstretch>0</horstretch>
+    <verstretch>0</verstretch>
+   </sizepolicy>
+  </property>
+  <property name="minimumSize">
+   <size>
+    <width>600</width>
+    <height>600</height>
+   </size>
+  </property>
+  <property name="font">
+   <font>
+    <pointsize>13</pointsize>
+   </font>
+  </property>
+  <property name="windowTitle">
+   <string>Электронные трудовое книжки</string>
+  </property>
+  <widget class="QWidget" name="centralwidget">
+   <layout class="QGridLayout" name="gridLayout">
+    <item row="0" column="0" rowspan="2">
+     <layout class="QGridLayout" name="gridLayout_5">
+      <item row="1" column="3">
+       <widget class="QPushButton" name="pushButton_2">
+        <property name="text">
+         <string>Добавить</string>
+        </property>
+       </widget>
+      </item>
+      <item row="2" column="2" colspan="2">
+       <spacer name="horizontalSpacer">
+        <property name="orientation">
+         <enum>Qt::Horizontal</enum>
+        </property>
+        <property name="sizeHint" stdset="0">
+         <size>
+          <width>250</width>
+          <height>0</height>
+         </size>
+        </property>
+       </spacer>
+      </item>
+      <item row="1" column="5" colspan="3">
+       <widget class="QPushButton" name="pushButton">
+        <property name="text">
+         <string>Сбросить</string>
+        </property>
+       </widget>
+      </item>
+      <item row="0" column="1" colspan="7">
+       <spacer name="horizontalSpacer_3">
+        <property name="orientation">
+         <enum>Qt::Horizontal</enum>
+        </property>
+        <property name="sizeHint" stdset="0">
+         <size>
+          <width>40</width>
+          <height>10</height>
+         </size>
+        </property>
+       </spacer>
+      </item>
+      <item row="1" column="1" colspan="2">
+       <widget class="QPushButton" name="pushButton_3">
+        <property name="text">
+         <string>Выгрузить</string>
+        </property>
+       </widget>
+      </item>
+      <item row="0" column="0" rowspan="6">
+       <spacer name="verticalSpacer_3">
+        <property name="orientation">
+         <enum>Qt::Vertical</enum>
+        </property>
+        <property name="sizeHint" stdset="0">
+         <size>
+          <width>20</width>
+          <height>40</height>
+         </size>
+        </property>
+       </spacer>
+      </item>
+      <item row="1" column="4">
+       <widget class="QLineEdit" name="lineEdit">
+        <property name="placeholderText">
+         <string>Поиск по фио</string>
+        </property>
+       </widget>
+      </item>
+      <item row="5" column="1" colspan="7">
+       <spacer name="horizontalSpacer_2">
+        <property name="orientation">
+         <enum>Qt::Horizontal</enum>
+        </property>
+        <property name="sizeHint" stdset="0">
+         <size>
+          <width>40</width>
+          <height>20</height>
+         </size>
+        </property>
+       </spacer>
+      </item>
+      <item row="0" column="8" rowspan="6">
+       <spacer name="verticalSpacer_2">
+        <property name="orientation">
+         <enum>Qt::Vertical</enum>
+        </property>
+        <property name="sizeHint" stdset="0">
+         <size>
+          <width>20</width>
+          <height>40</height>
+         </size>
+        </property>
+       </spacer>
+      </item>
+      <item row="4" column="1" colspan="7">
+       <widget class="QListWidget" name="listWidget"/>
+      </item>
+      <item row="3" column="2">
+       <widget class="QCheckBox" name="checkBox">
+        <property name="text">
+         <string>Выделить все</string>
+        </property>
+       </widget>
+      </item>
+     </layout>
+    </item>
+   </layout>
+  </widget>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>

+ 224 - 0
Приложение/App/view_card_window.py

@@ -0,0 +1,224 @@
+import datetime
+import os
+from json import loads, dumps
+
+from PyQt5 import uic
+from PyQt5.QtCore import QTimer
+from PyQt5.QtWidgets import QMainWindow, QSpinBox, QDateEdit, QTableWidgetItem, QPushButton, QFileDialog, QMessageBox
+
+PATH_TO_DATA_FILE = 'data.json'
+
+
+class ViewCardWin(QMainWindow):
+    def __init__(self, parent, card):
+        super().__init__()
+        uic.loadUi('ui/AddCardWin.ui', self)
+        self.pushButton_2.hide()
+        self.pushButton.clicked.connect(self.back)
+        self.pushButton_3.clicked.connect(self.save_data)
+        self.par = parent
+        self.lineEdit.setText(card['title']['serial'])
+        self.lineEdit_2.setText(card['title']['number'])
+        self.lineEdit_3.setText(card['title']['last_name'])
+        self.lineEdit_4.setText(card['title']['first_name'])
+        self.lineEdit_5.setText(card['title']['patronymic'])
+
+        try:
+            self.dateEdit.setDate(datetime.datetime.strptime(card['title']['birthday'], '%Y-%m-%d'))
+        except Exception:
+            pass
+
+        try:
+            self.dateEdit_2.setDate(datetime.datetime.strptime(card['title']['issue_date'], '%Y-%m-%d'))
+        except Exception:
+            pass
+
+        self.lineEdit_6.setText(card['title']['profession'])
+        self.lineEdit_7.setText(card['title']['education'])
+
+        self.make_table(card)
+
+        self.card_info = card
+        self.pushButton_4.clicked.connect(self.add_row)
+        self.tableWidget.verticalHeader().setVisible(False)
+        self.widget.setVisible(False)
+        self.widget_2.setVisible(False)
+        self.widget_3.setVisible(False)
+
+
+        self.pushButton_5.clicked.connect(self.dump_data)
+        self.pushButton_6.clicked.connect(self.delete_work_book)
+
+        self.tableWidget.setHorizontalHeaderLabels(['№ Записи', 'Дата', 'Сведенья', 'Документ', 'Удаление'])
+
+        QTimer.singleShot(100, self.resize_table)
+
+    def delete_work_book(self):
+        with open(PATH_TO_DATA_FILE, 'r', encoding='utf-8') as file:
+            data = loads(file.read())
+
+        card_info = list(filter(lambda card: card['uuid'] == self.card_info['uuid'], data))[0]
+
+        del data[data.index(card_info)]
+
+        with open(PATH_TO_DATA_FILE, 'w', encoding='utf-8') as write_file:
+            write_file.write(dumps(data))
+
+        self.par.show()
+        self.close()
+
+    def dump_data(self):
+        self.save_data()
+
+        options = QFileDialog.Options()
+        directory = QFileDialog.getExistingDirectory(self, "Выберите путь сохранения", options=options)
+        if directory:
+            dump_data_json = self.card_info.copy()
+            if 'uuid' in dump_data_json:
+                del dump_data_json['uuid']
+            file_name = os.path.join(directory,
+                                     f'{dump_data_json["title"]["last_name"]}_{dump_data_json["title"]["first_name"]}.json')
+            with open(file_name, 'w', encoding='utf-8') as file:
+                file.write(dumps(dump_data_json))
+
+            QMessageBox.information(self, 'Данные выгружены', 'Данные успешно выгружены в файл')
+
+    def make_table(self, card):
+        self.tableWidget.clear()
+
+        self.tableWidget.setRowCount(len(card['job']))
+
+        for row in range(len(card['job'])):
+            number_spin_box = QSpinBox(self)
+            number_spin_box.setMinimum(1)
+            number_spin_box.setMaximum(100000)
+            number_spin_box.setValue(card['job'][row]['number'])
+            self.tableWidget.setCellWidget(row, 0, number_spin_box)
+            date_edit = QDateEdit()
+            date_edit.setDate(datetime.date.today())
+            date_edit.setMaximumDate(datetime.date.today())
+            date_edit.setCalendarPopup(True)
+
+            try:
+                date_edit.setDate(datetime.datetime.strptime(card['job'][row]['date'], '%Y-%m-%d'))
+            except Exception:
+                pass
+
+            self.tableWidget.setCellWidget(row, 1, date_edit)
+            self.tableWidget.setItem(row, 2, QTableWidgetItem(card['job'][row]['job_info']))
+            self.tableWidget.setItem(row, 3, QTableWidgetItem(card['job'][row]['basis']))
+
+            delete_button = QPushButton()
+            delete_button.setText('Удалить')
+            delete_button.row = row
+            delete_button.clicked.connect(self.delete_row)
+            self.tableWidget.setCellWidget(row, 4, delete_button)
+
+    def delete_row(self):
+        sender = self.sender()
+        self.tableWidget.removeRow(sender.row)
+
+        for row in range(sender.row, self.tableWidget.rowCount()):
+            self.tableWidget.cellWidget(row, 4).row -= 1
+
+    def add_row(self):
+        self.tableWidget.setRowCount(self.tableWidget.rowCount() + 1)
+
+        number_spin_box = QSpinBox(self)
+        number_spin_box.setMinimum(1)
+        number_spin_box.setMaximum(100000)
+        number_spin_box.setValue(self.tableWidget.rowCount())
+        self.tableWidget.setCellWidget(self.tableWidget.rowCount() - 1, 0, number_spin_box)
+
+        date_edit = QDateEdit()
+        date_edit.setDate(datetime.date.today())
+        date_edit.setMaximumDate(datetime.date.today())
+        date_edit.setCalendarPopup(True)
+        self.tableWidget.setCellWidget(self.tableWidget.rowCount() - 1, 1, date_edit)
+
+        delete_button = QPushButton()
+        delete_button.setText('Удалить')
+        delete_button.row = self.tableWidget.rowCount() - 1
+        delete_button.clicked.connect(self.delete_row)
+        self.tableWidget.setCellWidget(self.tableWidget.rowCount() - 1, 4, delete_button)
+
+        self.tableWidget.setItem(self.tableWidget.rowCount() - 1, 2, QTableWidgetItem(''))
+        self.tableWidget.setItem(self.tableWidget.rowCount() - 1, 3, QTableWidgetItem(''))
+
+        self.resize_table()
+
+    def resize_table(self):
+        if self.tableWidget.rowCount():
+            row_height = min([90, max([50, self.tableWidget.height() // self.tableWidget.rowCount()])])
+
+            for row in range(self.tableWidget.rowCount()):
+                self.tableWidget.setRowHeight(row, row_height)
+
+        column_width = max([100, self.tableWidget.width() // self.tableWidget.columnCount()])
+
+        for column in range(self.tableWidget.columnCount()):
+            self.tableWidget.setColumnWidth(column, column_width)
+
+    def resizeEvent(self, a0, QResizeEvent=None):
+        self.resize_table()
+
+    def save_data(self):
+        with open(PATH_TO_DATA_FILE, 'r', encoding='utf-8') as file:
+            data = loads(file.read())
+
+        card_info = list(filter(lambda card: card['uuid'] == self.card_info['uuid'], data))[0]
+
+        data[data.index(card_info)] = {
+            "uuid": self.card_info['uuid'],
+            "title": {
+                "serial": self.lineEdit.text(),
+                "number": self.lineEdit_2.text(),
+                "first_name": self.lineEdit_4.text(),
+                "last_name": self.lineEdit_3.text(),
+                "patronymic": self.lineEdit_5.text(),
+                "birthday": str(self.dateEdit.date().toPyDate()),
+                "issue_date": str(self.dateEdit_2.date().toPyDate()),
+                "profession": self.lineEdit_6.text(),
+                "education": self.lineEdit_7.text()
+            },
+            "job": [
+                {
+                    "number": self.tableWidget.cellWidget(row, 0).value(),
+                    "date": str(self.tableWidget.cellWidget(row, 1).date().toPyDate()),
+                    "job_info": self.tableWidget.item(row, 2).text(),
+                    "basis": self.tableWidget.item(row, 3).text()
+                }
+                for row in range(self.tableWidget.rowCount())
+            ]
+        }
+
+        self.card_info = {
+            "uuid": self.card_info['uuid'],
+            "title": {
+                "serial": self.lineEdit.text(),
+                "number": self.lineEdit_2.text(),
+                "first_name": self.lineEdit_4.text(),
+                "last_name": self.lineEdit_3.text(),
+                "patronymic": self.lineEdit_5.text(),
+                "birthday": str(self.dateEdit.date().toPyDate()),
+                "issue_date": str(self.dateEdit_2.date().toPyDate()),
+                "profession": self.lineEdit_6.text(),
+                "education": self.lineEdit_7.text()
+            },
+            "job": [
+                {
+                    "number": self.tableWidget.cellWidget(row, 0).value(),
+                    "date": str(self.tableWidget.cellWidget(row, 1).date().toPyDate()),
+                    "job_info": self.tableWidget.item(row, 2).text(),
+                    "basis": self.tableWidget.item(row, 3).text()
+                }
+                for row in range(self.tableWidget.rowCount())
+            ]
+        }
+
+        with open(PATH_TO_DATA_FILE, 'w', encoding='utf-8') as write_file:
+            write_file.write(dumps(data))
+
+    def back(self):
+        self.par.show()
+        self.close()

BIN
Приложение/App/ТрудовыеКнижки


BIN
Приложение/App/ТрудовыеКнижки.exe


BIN
Приложение/media/img.png


BIN
Приложение/media/img2.png


BIN
Приложение/media/img3.png


BIN
Приложение/media/img4.png


BIN
Приложение/media/img6.png


BIN
Приложение/media/img7.png


BIN
Приложение/media/Скринкаст.webm


BIN
Приложение/media/Текст к презентации.docx


BIN
Приложение/media/Трудовые книжки.pdf


BIN
Приложение/media/Трудовые книжки_compressed.pdf


+ 70 - 0
Приложение/readme.md

@@ -0,0 +1,70 @@
+# Электронные трудовые книжки
+
+## Описание системы
+
+Система представляет собой приложение Qt, сервис API, разработанный на языке Python с использованием фреймворков Qt,
+Flask платформы, предназначенной для разработки, развёртывания и запуска приложений в контейнерах - Docker. Система
+включает в себя следующие компоненты:
+
+Qt: Настольное приложение позволяет хранить трудовые книжки сотрудников, изменять их и выгружать в JSON, а также
+сканировать титульные странички книжек и оцифровывать
+
+Flask: API сервис имеет один метод и принимает в теле запроса изображение, после чего возвращает 2 варианта,
+распознанного на изображении, текста
+
+Docker: используется для контейнеризации приложения, что облегчает его развертывание и масштабирование.
+
+Цель системы - оптимизация процесса заполнения трудовых книжек и ускорение приёма на работу.
+
+## Запуск системы
+
+Настольное приложение является самодостаточным и может запускаться через исполняемые файлы, однако без запущенного API
+не сможет распознавать текст на фотографиях.
+
+Перед запуском системы убедитесь, что на вашем компьютере установлены Docker и Docker Compose. Если они не установлены,
+следуйте инструкциям на [официальном сайте Docker](https://www.docker.com/) для вашей операционной системы.
+
+Для запуска система откройте терминал в папке [Api](Api) и запустите одну из команд:  
+`docker compose up` или `docker-compose up`  
+Начнется запуск API.
+
+После запуска методы API будут доступны по адресу localhost:6543 (при условии, что конфигурация не была
+изменена), а приложение автоматически будет подключатся для распознавания текста.
+
+## Использование системы
+
+При запуске исполняемого файла откроется окно со списком трудовых книжек, а также строка поиска, кнопка создания новой
+книжки и кнопка выгрузки выбранных книжек.
+
+![img.png](media/img.png)
+
+Для выгрузки книжки или нескольких книжек в формате JSON нужно установить
+флажки у нужных книжек и нажать на кнопку "Выгрузить".
+
+![img.png](media/img3.png)
+
+Строка поиска автоматически включит сортировку при вводе ФИО,
+после чего появится кнопка сброса.
+
+![img.png](media/img2.png)
+
+В окне создания трудовой книжки можно вручную заполнить каждое поле и сразу выгрузить в JSON.
+
+![img.png](media/img4.png)
+
+Также, можно выбрать скан титульной страницы для автоматического заполнения полей.
+
+![img.png](media/img6.png)
+
+После сохранения книжка появится в общем списке, однако при необходимости ее можно не сохранять, а сразу выгрузить в
+формате JSON и выйти на главный экран.
+
+Каждую сохраненную трудовую книжку можно редактировать, выгрузить в формате JSON и удалить.
+
+![img.png](media/img7.png)
+
+## Примеры
+
+папке [Примеры трудовых книжек](%D0%9F%D1%80%D0%B8%D0%BC%D0%B5%D1%80%D1%8B%20%D1%82%D1%80%D1%83%D0%B4%D0%BE%D0%B2%D1%8B%D1%85%20%D0%BA%D0%BD%D0%B8%D0%B6%D0%B5%D0%BA)
+вы можете найти фотографии, которые сможете отсканировать через наше приложение.

BIN
Приложение/Примеры трудовых книжек/Пример 1.jpg


BIN
Приложение/Примеры трудовых книжек/Пример 2.jpg


BIN
РЖД_Трудовые книжки.pdf


BIN
Скринкаст.webm


Niektoré súbory nie sú zobrazené, pretože je v týchto rozdielových dátach zmenené mnoho súborov