Carregar ficheiros para "/"

This commit is contained in:
2024-12-19 09:04:35 -08:00
parent faa63ae8e5
commit a824cb1d07
5 changed files with 772 additions and 0 deletions

206
README.md Normal file
View File

@ -0,0 +1,206 @@
# Informação Técnica Automóvel
Um sistema desktop robusto para gerenciar documentos técnicos automotivos, desenvolvido com Python e PySide6.
## 🆕 Atualizações Recentes
### Dezembro 2024
- Renomeado o programa para "Informação Técnica Automóvel"
- Adicionada funcionalidade de abrir documentos com duplo clique
- Implementada busca avançada em Observações
- Melhorada a associação de documentos com pastas específicas
- Adicionado menu de contexto para documentos
- Corrigida a visualização de documentos nas pastas corretas
- Melhorada a interface de usuário com ordenação de colunas
- Adicionado destaque visual para resultados de busca em Observações
### Recursos Atualizados
- **Abertura de Documentos**:
- Duplo clique para abrir documentos
- Menu de contexto com opção "Abrir Documento"
- Abertura automática no aplicativo padrão do sistema
- **Busca Aprimorada**:
- Busca em todos os campos, incluindo Observações
- Destaque visual para termos encontrados
- Busca insensível a maiúsculas/minúsculas
- Suporte a busca parcial de palavras
- **Organização de Pastas**:
- Associação automática de documentos à pasta mais específica
- Estrutura hierárquica de pastas aprimorada
- Navegação intuitiva entre pastas
## 🚀 Funcionalidades
### 📁 Gerenciamento de Documentos
- Upload individual e em lote de documentos
- Organização em estrutura de pastas
- Suporte para diversos formatos de arquivo (PDF, imagens, etc.)
- Metadados detalhados (marca, modelo, ano, tipo, etc.)
- Visualização integrada de documentos
- Exportação e importação de documentos
### 🏷️ Organização e Categorização
- Sistema de tags coloridas
- Coleções personalizáveis
- Categorização por marca/modelo
- Estrutura hierárquica de pastas
- Filtros avançados
### 📊 Versionamento e Histórico
- Controle de versão de documentos
- Histórico de alterações
- Restauração de versões anteriores
- Comparação entre versões
- Registro de modificações
### 🔍 Busca e Visualização
- Busca avançada por metadados
- Visualização integrada de PDFs
- Pré-visualização de imagens
- Zoom e navegação de páginas
- Comparação visual de documentos
### 💬 Colaboração
- Sistema de comentários
- Anotações por página
- Tags compartilhadas
- Coleções colaborativas
- Exportação de relatórios
### 📈 Análise e Estatísticas
- Dashboard de estatísticas
- Gráficos de uso
- Relatórios personalizados
- Análise de espaço usado
- Métricas de documentos
### ⚙️ Recursos Avançados
- Backup automático
- Preferências personalizáveis
- Temas claro/escuro
- Documentos recentes
- Arrastar e soltar arquivos
## 🛠️ Tecnologias Utilizadas
- **Python**: Linguagem principal
- **PySide6**: Interface gráfica
- **SQLAlchemy**: ORM para banco de dados
- **PyMuPDF**: Manipulação de PDFs
- **OpenCV**: Processamento de imagens
- **NumPy**: Operações numéricas
- **Pillow**: Processamento de imagens
## 📋 Requisitos
- Python 3.8+
- Dependências listadas em `requirements.txt`
## 🚀 Instalação
1. Clone o repositório:
```bash
git clone [URL_DO_REPOSITÓRIO]
```
2. Instale as dependências:
```bash
pip install -r requirements.txt
```
3. Execute o aplicativo:
```bash
python main.py
```
## 🗂️ Estrutura do Projeto
```
gestao-documentos/
├── main.py # Ponto de entrada do aplicativo
├── models.py # Modelos do banco de dados
├── database.py # Configuração do banco de dados
├── document_form.py # Formulário de documentos
├── document_viewer.py # Visualizador de documentos
├── folder_view.py # Visualização de pastas
├── search_view.py # Interface de busca
├── version_control.py # Controle de versão
├── backup_manager.py # Gerenciamento de backup
├── tag_manager.py # Gerenciamento de tags
├── comments.py # Sistema de comentários
├── collections.py # Sistema de coleções
├── document_compare.py # Comparação de documentos
├── statistics_view.py # Visualização de estatísticas
├── preferences.py # Preferências do usuário
└── requirements.txt # Dependências do projeto
```
## 🎯 Principais Recursos
### Sistema de Arquivos
- Estrutura hierárquica de pastas
- Suporte para múltiplos formatos
- Upload em lote
- Organização flexível
### Metadados
- Informações detalhadas do veículo
- Dados técnicos do documento
- Tags personalizáveis
- Histórico de modificações
### Versionamento
- Controle de versões
- Histórico de alterações
- Comparação entre versões
- Restauração de versões
### Busca
- Busca por metadados
- Filtros avançados
- Resultados categorizados
- Histórico de busca
### Visualização
- Visualizador integrado
- Zoom e navegação
- Anotações e marcações
- Comparação lado a lado
### Organização
- Sistema de tags
- Coleções personalizadas
- Categorização automática
- Filtros inteligentes
### Backup
- Exportação de documentos
- Backup incremental
- Restauração seletiva
- Verificação de integridade
### Interface
- Design moderno
- Temas personalizáveis
- Atalhos de teclado
- Arrastar e soltar
## 🤝 Contribuição
1. Faça um Fork do projeto
2. Crie uma Branch para sua Feature (`git checkout -b feature/AmazingFeature`)
3. Commit suas mudanças (`git commit -m 'Add some AmazingFeature'`)
4. Push para a Branch (`git push origin feature/AmazingFeature`)
5. Abra um Pull Request
## 📝 Licença
Este projeto está sob a licença MIT. Veja o arquivo [LICENSE](LICENSE) para mais detalhes.
## 🎉 Agradecimentos
- Equipe de desenvolvimento
- Contribuidores
- Comunidade open source

6
requirements.txt Normal file
View File

@ -0,0 +1,6 @@
PySide6>=6.5.0
SQLAlchemy>=2.0.0
PyMuPDF>=1.23.0
opencv-python>=4.8.0
numpy>=1.24.0
Pillow>=10.0.0

184
search_view.py Normal file
View File

@ -0,0 +1,184 @@
from PySide6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QFormLayout,
QLineEdit, QComboBox, QPushButton, QTableWidget,
QTableWidgetItem, QSpinBox, QHeaderView, QMenu,
QMessageBox)
from PySide6.QtCore import Qt, Signal
from database import get_session
from models import Document
import os
from sqlalchemy import or_
from PySide6.QtGui import QColor
class SearchView(QWidget):
document_selected = Signal(int) # Signal emitted when document is selected
def __init__(self, parent=None):
super().__init__(parent)
self.setup_ui()
def setup_ui(self):
layout = QVBoxLayout(self)
# Search form
form_layout = QFormLayout()
# Search fields
self.marca_edit = QLineEdit()
self.modelo_edit = QLineEdit()
self.ano_spin = QSpinBox()
self.ano_spin.setRange(1900, 2100)
self.ano_spin.setSpecialValueText(" ") # Empty string for no filter
self.tipo_combo = QComboBox()
self.tipo_combo.addItems(["", "Elétrico", "Mecânico"])
self.variante_combo = QComboBox()
self.variante_combo.addItems([
"",
"Esquema",
"Esquema OEM",
"TSB",
"Esquemas Varios",
"Documentação de Instruções"
])
self.observacoes_edit = QLineEdit()
self.observacoes_edit.setPlaceholderText("Digite sua busca...")
form_layout.addRow("Marca:", self.marca_edit)
form_layout.addRow("Modelo:", self.modelo_edit)
form_layout.addRow("Ano:", self.ano_spin)
form_layout.addRow("Tipo:", self.tipo_combo)
form_layout.addRow("Variante:", self.variante_combo)
form_layout.addRow("Observações:", self.observacoes_edit)
# Search button
button_layout = QHBoxLayout()
self.search_button = QPushButton("Pesquisar")
self.search_button.clicked.connect(self.perform_search)
self.clear_button = QPushButton("Limpar")
self.clear_button.clicked.connect(self.clear_search)
button_layout.addWidget(self.search_button)
button_layout.addWidget(self.clear_button)
# Add layouts to main layout
layout.addLayout(form_layout)
layout.addLayout(button_layout)
# Results table
self.results_table = QTableWidget()
self.results_table.setColumnCount(7)
self.results_table.setHorizontalHeaderLabels([
"Marca", "Modelo", "Ano", "Tipo", "Variante",
"Nome do Arquivo", "Observações"
])
self.results_table.horizontalHeader().setSectionResizeMode(
QHeaderView.ResizeToContents)
self.results_table.setContextMenuPolicy(Qt.CustomContextMenu)
self.results_table.customContextMenuRequested.connect(
self.show_context_menu)
layout.addWidget(self.results_table)
def perform_search(self):
session = get_session()
query = session.query(Document)
# Apply filters
if self.marca_edit.text():
query = query.filter(Document.marca.ilike(f"%{self.marca_edit.text()}%"))
if self.modelo_edit.text():
query = query.filter(Document.modelo.ilike(f"%{self.modelo_edit.text()}%"))
if self.ano_spin.value() != self.ano_spin.minimum():
query = query.filter(Document.ano == self.ano_spin.value())
if self.tipo_combo.currentText():
query = query.filter(Document.tipo_documento == self.tipo_combo.currentText())
if self.variante_combo.currentText():
query = query.filter(Document.variante == self.variante_combo.currentText())
if self.observacoes_edit.text():
query = query.filter(Document.observacoes.ilike(f"%{self.observacoes_edit.text()}%"))
# Get results
documents = query.all()
# Update table
valid_documents = []
for doc in documents:
if os.path.exists(doc.file_path):
valid_documents.append(doc)
else:
# Optional: Remove documents with missing files
session.delete(doc)
if len(documents) != len(valid_documents):
session.commit()
documents = valid_documents
self.results_table.setRowCount(len(documents))
for row, doc in enumerate(documents):
self.results_table.setItem(row, 0, QTableWidgetItem(doc.marca))
self.results_table.setItem(row, 1, QTableWidgetItem(doc.modelo))
self.results_table.setItem(row, 2, QTableWidgetItem(str(doc.ano)))
self.results_table.setItem(row, 3, QTableWidgetItem(doc.tipo_documento))
self.results_table.setItem(row, 4, QTableWidgetItem(doc.variante))
self.results_table.setItem(row, 5, QTableWidgetItem(doc.file_name))
self.results_table.setItem(row, 6, QTableWidgetItem(doc.observacoes))
# Store document ID in the first column
self.results_table.item(row, 0).setData(Qt.UserRole, doc.id)
session.close()
def clear_search(self):
self.marca_edit.clear()
self.modelo_edit.clear()
self.ano_spin.setValue(self.ano_spin.minimum())
self.tipo_combo.setCurrentIndex(0)
self.variante_combo.setCurrentIndex(0)
self.observacoes_edit.clear()
self.results_table.setRowCount(0)
def show_context_menu(self, position):
menu = QMenu()
open_action = menu.addAction("Abrir Documento")
open_folder_action = menu.addAction("Abrir Pasta")
action = menu.exec_(self.results_table.mapToGlobal(position))
current_row = self.results_table.currentRow()
if current_row >= 0:
doc_id = self.results_table.item(current_row, 0).data(Qt.UserRole)
if action == open_action:
self.open_document(doc_id)
elif action == open_folder_action:
self.open_document_folder(doc_id)
def open_document(self, doc_id):
session = get_session()
document = session.query(Document).get(doc_id)
if document and os.path.exists(document.file_path):
self.document_selected.emit(doc_id)
else:
QMessageBox.warning(self, "Erro",
"Não foi possível abrir o documento.")
session.close()
def open_document_folder(self, doc_id):
session = get_session()
document = session.query(Document).get(doc_id)
if document and os.path.exists(document.file_path):
folder_path = os.path.dirname(document.file_path)
os.startfile(folder_path)
else:
QMessageBox.warning(self, "Erro",
"Não foi possível abrir a pasta do documento.")
session.close()

130
statistics_view.py Normal file
View File

@ -0,0 +1,130 @@
from PySide6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QPushButton,
QLabel, QTableWidget, QTableWidgetItem,
QHeaderView)
from PySide6.QtCore import Qt
from PySide6.QtCharts import QChart, QChartView, QPieSeries, QBarSeries, QBarSet
from PySide6.QtGui import QPainter
from database import get_session
from models import Document, Folder, Statistics
import json
import os
from datetime import datetime
class StatisticsView(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setup_ui()
def setup_ui(self):
layout = QVBoxLayout(self)
# Refresh button
self.refresh_btn = QPushButton("Atualizar Estatísticas")
self.refresh_btn.clicked.connect(self.update_statistics)
layout.addWidget(self.refresh_btn)
# Summary layout
summary_layout = QHBoxLayout()
# Total documents
self.total_docs_label = QLabel("Total de Documentos: 0")
summary_layout.addWidget(self.total_docs_label)
# Total folders
self.total_folders_label = QLabel("Total de Pastas: 0")
summary_layout.addWidget(self.total_folders_label)
# Storage used
self.storage_label = QLabel("Espaço Utilizado: 0 MB")
summary_layout.addWidget(self.storage_label)
layout.addLayout(summary_layout)
# Charts layout
charts_layout = QHBoxLayout()
# Document types chart
self.types_chart = QChart()
self.types_chart.setTitle("Documentos por Tipo")
types_view = QChartView(self.types_chart)
types_view.setRenderHint(QPainter.RenderHint.Antialiasing)
charts_layout.addWidget(types_view)
# Brands chart
self.brands_chart = QChart()
self.brands_chart.setTitle("Documentos por Marca")
brands_view = QChartView(self.brands_chart)
brands_view.setRenderHint(QPainter.RenderHint.Antialiasing)
charts_layout.addWidget(brands_view)
layout.addLayout(charts_layout)
self.update_statistics()
def update_statistics(self):
session = get_session()
try:
# Count documents and folders
total_docs = session.query(Document).count()
total_folders = session.query(Folder).count()
# Calculate storage used
storage_used = 0
for doc in session.query(Document).all():
if os.path.exists(doc.file_path):
storage_used += os.path.getsize(doc.file_path)
for version in doc.versions:
if os.path.exists(version.file_path):
storage_used += os.path.getsize(version.file_path)
# Update labels
self.total_docs_label.setText(f"Total de Documentos: {total_docs}")
self.total_folders_label.setText(f"Total de Pastas: {total_folders}")
self.storage_label.setText(
f"Espaço Utilizado: {storage_used / (1024*1024):.2f} MB")
# Document types statistics
types_series = QPieSeries()
types_count = {}
for doc in session.query(Document).all():
types_count[doc.tipo_documento] = types_count.get(
doc.tipo_documento, 0) + 1
for doc_type, count in types_count.items():
types_series.append(f"{doc_type} ({count})", count)
self.types_chart.removeAllSeries()
self.types_chart.addSeries(types_series)
# Brands statistics
brands_series = QBarSeries()
brands_count = {}
for doc in session.query(Document).all():
brands_count[doc.marca] = brands_count.get(doc.marca, 0) + 1
brands_set = QBarSet("Marcas")
brands = list(brands_count.keys())
values = [brands_count[brand] for brand in brands]
brands_set.append(values)
brands_series.append(brands_set)
self.brands_chart.removeAllSeries()
self.brands_chart.addSeries(brands_series)
# Save statistics
stats = Statistics(
date=datetime.utcnow(),
total_documents=total_docs,
total_folders=total_folders,
documents_by_type=json.dumps(types_count),
documents_by_brand=json.dumps(brands_count),
storage_used=storage_used
)
session.add(stats)
session.commit()
except Exception as e:
print(f"Error updating statistics: {str(e)}")
finally:
session.close()

246
tag_manager.py Normal file
View File

@ -0,0 +1,246 @@
from PySide6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QPushButton,
QTableWidget, QTableWidgetItem, QHeaderView,
QMessageBox, QInputDialog, QColorDialog, QLabel)
from PySide6.QtCore import Qt, Signal
from PySide6.QtGui import QColor
from database import get_session
from models import Tag, Document
import json
class TagManager(QWidget):
tags_updated = Signal()
def __init__(self, parent=None):
super().__init__(parent)
self.setup_ui()
def setup_ui(self):
layout = QVBoxLayout(self)
# Buttons
button_layout = QHBoxLayout()
self.add_tag_btn = QPushButton("Nova Tag")
self.add_tag_btn.clicked.connect(self.add_tag)
self.edit_tag_btn = QPushButton("Editar Tag")
self.edit_tag_btn.clicked.connect(self.edit_tag)
self.delete_tag_btn = QPushButton("Excluir Tag")
self.delete_tag_btn.clicked.connect(self.delete_tag)
button_layout.addWidget(self.add_tag_btn)
button_layout.addWidget(self.edit_tag_btn)
button_layout.addWidget(self.delete_tag_btn)
layout.addLayout(button_layout)
# Tags table
self.tags_table = QTableWidget()
self.tags_table.setColumnCount(3)
self.tags_table.setHorizontalHeaderLabels([
"Nome", "Cor", "Documentos"
])
header = self.tags_table.horizontalHeader()
header.setSectionResizeMode(QHeaderView.Stretch)
layout.addWidget(self.tags_table)
self.refresh_tags()
def refresh_tags(self):
session = get_session()
tags = session.query(Tag).all()
self.tags_table.setRowCount(len(tags))
for row, tag in enumerate(tags):
# Name
self.tags_table.setItem(row, 0, QTableWidgetItem(tag.name))
# Color
color_item = QTableWidgetItem()
color_item.setBackground(QColor(tag.color))
self.tags_table.setItem(row, 1, color_item)
# Document count
doc_count = len(tag.documents)
self.tags_table.setItem(row, 2, QTableWidgetItem(str(doc_count)))
session.close()
def add_tag(self):
name, ok = QInputDialog.getText(
self,
"Nova Tag",
"Nome da tag:"
)
if ok and name:
color = QColorDialog.getColor()
if color.isValid():
try:
session = get_session()
# Check if tag already exists
if session.query(Tag).filter(Tag.name == name).first():
QMessageBox.warning(self, "Erro",
"Uma tag com este nome já existe.")
return
new_tag = Tag(
name=name,
color=color.name()
)
session.add(new_tag)
session.commit()
self.refresh_tags()
self.tags_updated.emit()
except Exception as e:
QMessageBox.critical(self, "Erro",
f"Erro ao criar tag: {str(e)}")
finally:
session.close()
def edit_tag(self):
current_row = self.tags_table.currentRow()
if current_row < 0:
return
current_name = self.tags_table.item(current_row, 0).text()
new_name, ok = QInputDialog.getText(
self,
"Editar Tag",
"Nome da tag:",
text=current_name
)
if ok and new_name:
color = QColorDialog.getColor()
if color.isValid():
try:
session = get_session()
tag = session.query(Tag).filter(Tag.name == current_name).first()
if tag:
tag.name = new_name
tag.color = color.name()
session.commit()
self.refresh_tags()
self.tags_updated.emit()
except Exception as e:
QMessageBox.critical(self, "Erro",
f"Erro ao editar tag: {str(e)}")
finally:
session.close()
def delete_tag(self):
current_row = self.tags_table.currentRow()
if current_row < 0:
return
tag_name = self.tags_table.item(current_row, 0).text()
reply = QMessageBox.question(
self,
"Confirmar Exclusão",
f"Tem certeza que deseja excluir a tag '{tag_name}'?",
QMessageBox.Yes | QMessageBox.No
)
if reply == QMessageBox.Yes:
try:
session = get_session()
tag = session.query(Tag).filter(Tag.name == tag_name).first()
if tag:
session.delete(tag)
session.commit()
self.refresh_tags()
self.tags_updated.emit()
except Exception as e:
QMessageBox.critical(self, "Erro",
f"Erro ao excluir tag: {str(e)}")
finally:
session.close()
class DocumentTags(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.current_document = None
self.setup_ui()
def setup_ui(self):
layout = QVBoxLayout(self)
self.tags_layout = QHBoxLayout()
layout.addLayout(self.tags_layout)
# Add tag button
self.add_tag_btn = QPushButton("Adicionar Tag")
self.add_tag_btn.clicked.connect(self.add_tag_to_document)
layout.addWidget(self.add_tag_btn)
def load_document(self, doc_id):
session = get_session()
self.current_document = session.query(Document).get(doc_id)
self.refresh_tags()
session.close()
def refresh_tags(self):
# Clear existing tags
for i in reversed(range(self.tags_layout.count())):
self.tags_layout.itemAt(i).widget().setParent(None)
if self.current_document:
session = get_session()
document = session.query(Document).get(self.current_document.id)
for tag in document.tags:
self.add_tag_label(tag)
session.close()
def add_tag_label(self, tag):
label = QLabel(tag.name)
label.setStyleSheet(
f"background-color: {tag.color}; padding: 2px 5px; border-radius: 3px;"
)
self.tags_layout.addWidget(label)
def add_tag_to_document(self):
if not self.current_document:
return
session = get_session()
available_tags = session.query(Tag).all()
if not available_tags:
QMessageBox.warning(self, "Aviso",
"Não há tags disponíveis. Crie algumas tags primeiro.")
return
tag_names = [tag.name for tag in available_tags]
current_tags = [tag.name for tag in self.current_document.tags]
available_names = [n for n in tag_names if n not in current_tags]
if not available_names:
QMessageBox.information(self, "Aviso",
"Todas as tags já foram aplicadas a este documento.")
return
name, ok = QInputDialog.getItem(
self,
"Adicionar Tag",
"Selecione uma tag:",
available_names,
0,
False
)
if ok and name:
tag = session.query(Tag).filter(Tag.name == name).first()
document = session.query(Document).get(self.current_document.id)
document.tags.append(tag)
session.commit()
self.refresh_tags()
session.close()