Compare commits
5 Commits
65c34314b0
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| a8f642a1c9 | |||
| 704e01669f | |||
| 3604a46421 | |||
| 0f7e157406 | |||
| b51e8bcc2a |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -10,6 +10,7 @@ ENV/
|
||||
*.egg-info/
|
||||
build/
|
||||
*.egg
|
||||
build_log.txt
|
||||
|
||||
# PyInstaller
|
||||
*.spec.bak
|
||||
|
||||
294
README.md
294
README.md
@@ -1,152 +1,172 @@
|
||||
# Database Manager Kivy App
|
||||
# Database Search & Update — User Guide
|
||||
|
||||
A simple Kivy application for managing a MariaDB database with two columns (id and mass) for the `offsystemsCounting` table.
|
||||
A fullscreen touchscreen application for querying and updating article/box weight (mass) records stored in a MariaDB database.
|
||||
|
||||
## Features
|
||||
---
|
||||
|
||||
- **Search**: Look up records by ID
|
||||
- **Add/Update**: Add new records or update existing ones
|
||||
- **Delete**: Remove records from the database
|
||||
- **View All**: Display all records in the database
|
||||
- **Auto-create**: Table is created automatically if it doesn't exist
|
||||
## UI Layout (top to bottom)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### MariaDB Server Setup
|
||||
|
||||
1. **Install MariaDB server** (if not already installed):
|
||||
```bash
|
||||
sudo apt update
|
||||
sudo apt install mariadb-server
|
||||
```
|
||||
|
||||
2. **Start MariaDB service**:
|
||||
```bash
|
||||
sudo systemctl start mariadb
|
||||
sudo systemctl enable mariadb
|
||||
```
|
||||
|
||||
3. **Secure MariaDB installation**:
|
||||
```bash
|
||||
sudo mysql_secure_installation
|
||||
```
|
||||
|
||||
4. **Create the database and user**:
|
||||
```bash
|
||||
sudo mysql -u root -p
|
||||
```
|
||||
|
||||
Then run these SQL commands:
|
||||
```sql
|
||||
CREATE DATABASE cantare_injectie;
|
||||
CREATE USER 'omron'@'localhost' IDENTIFIED BY 'Initial01!';
|
||||
GRANT ALL PRIVILEGES ON cantare_injectie.* TO 'omron'@'localhost';
|
||||
FLUSH PRIVILEGES;
|
||||
EXIT;
|
||||
```
|
||||
|
||||
5. **Test the connection**:
|
||||
```bash
|
||||
mysql -u omron -p cantare_injectie
|
||||
```
|
||||
Enter password: `Initial01!`
|
||||
|
||||
## Installation
|
||||
|
||||
1. Install Python 3.7+ if not already installed
|
||||
2. Install the required dependencies:
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
Or using virtual environment:
|
||||
```bash
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
1. **Make sure MariaDB server is running**:
|
||||
```bash
|
||||
sudo systemctl status mariadb
|
||||
```
|
||||
|
||||
2. **Run the application**:
|
||||
```bash
|
||||
python main.py
|
||||
```
|
||||
Or:
|
||||
```bash
|
||||
chmod +x run_app.sh
|
||||
./run_app.sh
|
||||
```
|
||||
|
||||
3. **To search for a record:**
|
||||
- Enter an ID in the "ID" field (max 20 characters)
|
||||
- Click "Search"
|
||||
- If found, the mass will be displayed in the "Mass" field
|
||||
|
||||
4. **To add or update a record:**
|
||||
- Enter both ID and mass value
|
||||
- Click "Add/Update"
|
||||
- If the ID exists, it will be updated; otherwise, a new record will be created
|
||||
|
||||
5. **To delete a record:**
|
||||
- Enter the ID to delete
|
||||
- Click "Delete"
|
||||
- Confirm the deletion in the popup
|
||||
|
||||
6. **To refresh the display:**
|
||||
- Click "Refresh All" to reload all data from the database
|
||||
|
||||
## Database Structure
|
||||
|
||||
The app connects to MariaDB database `cantare_injectie` with the following table structure:
|
||||
|
||||
```sql
|
||||
CREATE TABLE offsystemsCounting (
|
||||
id VARCHAR(20) PRIMARY KEY,
|
||||
mass REAL NOT NULL
|
||||
)
|
||||
```
|
||||
┌─────────────────────────────────────┬────────┐
|
||||
│ Database Search & Update │ Exit │
|
||||
├──────┬──────────────────────────────┴────────┤
|
||||
│ ID: │ [scan / type here] │
|
||||
│ Mass:│ [current mass] Last update: ... │
|
||||
├──────┴───────────────────────────────────────┤
|
||||
│ Article type detected: PRODUCT [Override] │
|
||||
├───────────────┬──────────────┬───────────────┤
|
||||
│ Add/Update │ Reset Values │ Settings │
|
||||
├───────────────┴──────────────┴───────────────┤
|
||||
│ Update Values │
|
||||
│ ID: [read-only — original barcode] │
|
||||
│ Mass: [editable] │
|
||||
│ [Confirm Add/Update] [Delete] │
|
||||
├──────────────────────────────────────────────┤
|
||||
│ Status bar │
|
||||
├──────────────────────────────────────────────┤
|
||||
│ [ 1 ] [ 2 ] [ 3 ] │
|
||||
│ [ 4 ] [ 5 ] [ 6 ] Numeric keypad │
|
||||
│ [ 7 ] [ 8 ] [ 9 ] │
|
||||
│ [ . ] [ 0 ] [ ⌫ ] │
|
||||
│ [ Enter ] │
|
||||
└──────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Files
|
||||
---
|
||||
|
||||
- `main.py`: Main Kivy application
|
||||
- `database_manager.py`: MariaDB database operations class
|
||||
- `requirements.txt`: Python dependencies
|
||||
- `test_database.py`: Test script for database operations
|
||||
- `run_app.sh`: Startup script
|
||||
- `README.md`: This documentation
|
||||
## Article / Box Auto-Detection
|
||||
|
||||
## Connection Settings
|
||||
When a barcode is entered in the **ID** field the app automatically classifies it:
|
||||
|
||||
The application connects to MariaDB with these settings:
|
||||
- **Host**: localhost
|
||||
- **Database**: cantare_injectie
|
||||
- **User**: omron
|
||||
- **Password**: Initial01!
|
||||
- **Table**: offsystemsCounting
|
||||
| Rule | Type | DB key used |
|
||||
|---|---|---|
|
||||
| Exactly 8 digits | **BOX** | Leading zeros stripped — e.g. `00000003` → `3` |
|
||||
| Anything else | **PRODUCT** | Used as-is |
|
||||
|
||||
## Troubleshooting
|
||||
The detected type is shown in the **mode bar** (blue = PRODUCT, amber = BOX).
|
||||
|
||||
1. **Connection Error 1698**:
|
||||
- Make sure MariaDB is running: `sudo systemctl start mariadb`
|
||||
- Verify user exists and has correct password
|
||||
- Check database exists: `SHOW DATABASES;`
|
||||
### Manual Override
|
||||
Press **Override type** to flip the detected type between BOX and PRODUCT.
|
||||
The mode bar shows `[Manual]` while an override is active.
|
||||
Typing a new ID resets the override back to auto-detection.
|
||||
|
||||
2. **Access Denied**:
|
||||
- Verify user permissions: `SHOW GRANTS FOR 'omron'@'localhost';`
|
||||
- Reset password if needed: `ALTER USER 'omron'@'localhost' IDENTIFIED BY 'Initial01!';`
|
||||
---
|
||||
|
||||
3. **Database/Table doesn't exist**:
|
||||
- The application will create the table automatically
|
||||
- Make sure the database `cantare_injectie` exists
|
||||
## Typical Workflow
|
||||
|
||||
## Notes
|
||||
### 1 — Search
|
||||
1. Scan or type the article/box code into the **ID** field.
|
||||
2. Press **Enter** on the keypad or the physical Enter key.
|
||||
3. The app queries the database and fills:
|
||||
- **Mass** — current stored weight
|
||||
- **Last update** — date and time of the last mass change
|
||||
|
||||
- IDs must be unique and max 20 characters
|
||||
- Mass values must be valid decimal numbers
|
||||
- The app includes comprehensive error handling and user feedback
|
||||
- All database operations use parameterized queries for security
|
||||
If the article is not in the database yet, Mass stays empty and the status bar shows "not found".
|
||||
|
||||
### 2 — Add / Update
|
||||
1. After a search (found or not found), press **Add/Update**.
|
||||
2. The **Update Values** frame activates:
|
||||
- **ID** (read-only) — shows the original barcode as scanned
|
||||
- **Mass** — pre-filled with the current value, fully editable; keyboard focus moves here automatically
|
||||
3. Clear the Mass field and type the new weight using the numeric keypad or a physical keyboard.
|
||||
4. Press **Confirm Add/Update**.
|
||||
- If the record exists → `UPDATE mass, t_update WHERE id = ?`
|
||||
- If the record is new → `INSERT` with the current timestamp
|
||||
5. On success all fields are cleared and focus returns to the **ID** field.
|
||||
|
||||
### 3 — Delete
|
||||
1. After a search (found), press **Add/Update** to load the record into the Update Values frame.
|
||||
2. Press **Delete** → a confirmation popup appears.
|
||||
3. Confirm to permanently remove the record from the database.
|
||||
|
||||
### 4 — Reset Values
|
||||
Clears all fields (ID, Mass, Update Values frame) and returns focus to the **ID** field without touching the database.
|
||||
|
||||
---
|
||||
|
||||
## Numeric Keypad
|
||||
|
||||
| Key | Action |
|
||||
|---|---|
|
||||
| `0`–`9` | Append digit to the active field |
|
||||
| `.` | Append decimal point |
|
||||
| `⌫` | Delete last character |
|
||||
| **Enter** | Trigger database search (when ID field is active) |
|
||||
|
||||
The keypad always targets the currently focused field (ID or Mass).
|
||||
|
||||
---
|
||||
|
||||
## Settings
|
||||
|
||||
Press **Settings** to change the database server address.
|
||||
|
||||
| Field | Default |
|
||||
|---|---|
|
||||
| Server IP / hostname | `localhost` (loaded from `config.json`) |
|
||||
|
||||
**Test Connection** checks connectivity before saving.
|
||||
**Save** persists the new address to `config.json` next to the executable.
|
||||
|
||||
---
|
||||
|
||||
## Log Files
|
||||
|
||||
Both files are created next to `config.json` (same folder as the `.exe` when running the built binary, or the project root when running from source).
|
||||
|
||||
### `app_actions.log`
|
||||
One line per database action:
|
||||
|
||||
```
|
||||
2026-04-09 15:30:45 | SEARCH_FOUND | id=4 | mass=3446.0, t_update=None
|
||||
2026-04-09 15:31:02 | UPDATE | id=4 | mass=3440.0
|
||||
2026-04-09 15:45:10 | INSERT | id=12345 | mass=1200.0
|
||||
2026-04-09 16:00:00 | DELETE | id=7 | rows_deleted=1
|
||||
```
|
||||
|
||||
| Action | Description |
|
||||
|---|---|
|
||||
| `APP_START` | Application launched |
|
||||
| `SEARCH_FOUND` | ID found in database |
|
||||
| `SEARCH_NOT_FOUND` | ID not in database |
|
||||
| `UPDATE` | Mass updated on existing record |
|
||||
| `INSERT` | New record created |
|
||||
| `DELETE` | Record removed |
|
||||
| `DELETE_NOT_FOUND` | Delete attempted on non-existent ID |
|
||||
| `LOG_PURGE` | Old log entries removed at startup |
|
||||
| `*_ERROR` | Any database exception |
|
||||
|
||||
Lines older than **30 days** are automatically removed each time the app starts.
|
||||
|
||||
### `db_debug.log`
|
||||
Verbose internal debug log (SQL statements, connection events, row counts) for troubleshooting.
|
||||
|
||||
---
|
||||
|
||||
## Database
|
||||
|
||||
| Detail | Value |
|
||||
|---|---|
|
||||
| Engine | MariaDB / MySQL |
|
||||
| Database | `cantare_injectie` |
|
||||
| Table | `offsystemsCounting` |
|
||||
| Columns | `id VARCHAR(20)` · `mass REAL` · `t_update DATETIME` |
|
||||
|
||||
The table and the `t_update` column are created automatically on first run if they do not exist.
|
||||
|
||||
---
|
||||
|
||||
## Running from Source
|
||||
|
||||
```powershell
|
||||
cd "C:\Users\Dell\Desktop\db_interface"
|
||||
.\venv\Scripts\python.exe main.py
|
||||
```
|
||||
|
||||
## Running the Built Binary
|
||||
|
||||
```
|
||||
dist\DatabaseApp.exe
|
||||
```
|
||||
|
||||
Copy your existing `config.json` next to the `.exe` to keep the saved server IP, or set it via **Settings** on first launch.
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
# Windows Setup Instructions
|
||||
|
||||
## Prerequisites
|
||||
1. Python 3.8 or higher
|
||||
2. MariaDB or MySQL server (local or remote)
|
||||
|
||||
## Installation Steps
|
||||
|
||||
### 1. Install Python
|
||||
- Download from: https://www.python.org/downloads/
|
||||
- During installation, check "Add Python to PATH"
|
||||
|
||||
### 2. Setup the Application
|
||||
Open Command Prompt in the application folder and run:
|
||||
|
||||
```cmd
|
||||
# Create virtual environment
|
||||
python -m venv venv
|
||||
|
||||
# Activate virtual environment
|
||||
venv\Scripts\activate
|
||||
|
||||
# Install dependencies
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### 3. Database Setup (if using local database)
|
||||
- Install MariaDB or MySQL
|
||||
- Run the setup script:
|
||||
```cmd
|
||||
mysql -u root -p < setup_user.sql
|
||||
```
|
||||
|
||||
### 4. Run the Application
|
||||
|
||||
#### Option A: Using the batch file
|
||||
Simply double-click `run_app.bat`
|
||||
|
||||
#### Option B: Using command line
|
||||
```cmd
|
||||
venv\Scripts\activate
|
||||
python main.py
|
||||
```
|
||||
|
||||
## Configuration
|
||||
- Use the **Settings** button in the app to configure the database server IP address
|
||||
- Default connection:
|
||||
- Host: localhost
|
||||
- Database: cantare_injectie
|
||||
- User: omron
|
||||
- Password: Initial01!
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Missing modules error
|
||||
```cmd
|
||||
venv\Scripts\activate
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### Database connection error
|
||||
- Check if MariaDB/MySQL service is running
|
||||
- Verify database credentials
|
||||
- Use Settings button to update server IP address
|
||||
|
||||
### Kivy installation issues
|
||||
```cmd
|
||||
pip install --upgrade pip
|
||||
pip install kivy --pre --extra-index-url https://kivy.org/downloads/simple/
|
||||
```
|
||||
|
||||
## Features
|
||||
- **Fullscreen mode**: App starts in fullscreen by default
|
||||
- **Search**: Enter ID and press Enter
|
||||
- **Add/Update**: Click Add/Update button to enable editing
|
||||
- **Delete**: Use Delete button in update section
|
||||
- **Reset**: Clear all fields with Reset Values button
|
||||
- **Settings**: Configure database server IP address
|
||||
|
||||
## Keyboard Shortcuts
|
||||
- **F11** or **Esc**: Exit fullscreen
|
||||
- **Enter**: Search for ID (when in ID field)
|
||||
@@ -22,6 +22,13 @@ def build_executable():
|
||||
else:
|
||||
print("No icon file found (optional)")
|
||||
|
||||
# Locate mysql connector locales inside the venv
|
||||
import site
|
||||
venv_site = os.path.join('venv', 'Lib', 'site-packages')
|
||||
mysql_locales = os.path.join(venv_site, 'mysql', 'connector', 'locales')
|
||||
add_data_sep = ';' if sys.platform == 'win32' else ':'
|
||||
mysql_locales_arg = f'--add-data={mysql_locales}{add_data_sep}mysql/connector/locales'
|
||||
|
||||
# PyInstaller command - simplified to avoid module collection issues
|
||||
cmd = [
|
||||
'pyinstaller',
|
||||
@@ -29,11 +36,17 @@ def build_executable():
|
||||
'--onefile',
|
||||
'--windowed',
|
||||
'--hidden-import=mysql.connector',
|
||||
'--hidden-import=mysql.connector.locales',
|
||||
'--hidden-import=mysql.connector.locales.eng',
|
||||
'--hidden-import=mysql.connector.plugins',
|
||||
'--hidden-import=mysql.connector.plugins.mysql_native_password',
|
||||
'--hidden-import=mysql.connector.plugins.caching_sha2_password',
|
||||
'--hidden-import=kivy.core.window.window_sdl2',
|
||||
'--hidden-import=win32timezone',
|
||||
'--exclude-module=_tkinter',
|
||||
'--exclude-module=matplotlib',
|
||||
'--exclude-module=numpy',
|
||||
mysql_locales_arg,
|
||||
] + icon_param + ['main.py']
|
||||
|
||||
try:
|
||||
|
||||
@@ -4,6 +4,8 @@ from typing import List, Tuple, Optional
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
# When frozen by PyInstaller, __file__ points to a temp folder that is deleted on exit.
|
||||
# sys.executable points to the .exe location, which is persistent.
|
||||
@@ -13,6 +15,58 @@ else:
|
||||
_BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
CONFIG_FILE = os.path.join(_BASE_DIR, 'config.json')
|
||||
LOG_FILE = os.path.join(_BASE_DIR, 'db_debug.log')
|
||||
ACTION_LOG_FILE = os.path.join(_BASE_DIR, 'app_actions.log')
|
||||
|
||||
# ---- Action log helpers (module-level, no class dependency) ----
|
||||
|
||||
def _log_action(action: str, record_id: str = '', detail: str = '') -> None:
|
||||
"""Append one structured line to app_actions.log."""
|
||||
ts = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
line = f"{ts} | {action:<18} | id={record_id:<20} | {detail}\n"
|
||||
try:
|
||||
with open(ACTION_LOG_FILE, 'a', encoding='utf-8') as f:
|
||||
f.write(line)
|
||||
except Exception as e:
|
||||
print(f"Action log write error: {e}")
|
||||
|
||||
|
||||
def purge_old_action_logs(days: int = 30) -> None:
|
||||
"""Remove lines older than *days* from app_actions.log."""
|
||||
if not os.path.exists(ACTION_LOG_FILE):
|
||||
return
|
||||
cutoff = datetime.now() - timedelta(days=days)
|
||||
kept = []
|
||||
removed = 0
|
||||
try:
|
||||
with open(ACTION_LOG_FILE, 'r', encoding='utf-8') as f:
|
||||
for line in f:
|
||||
# Every valid line starts with YYYY-MM-DD HH:MM:SS
|
||||
try:
|
||||
ts = datetime.strptime(line[:19], '%Y-%m-%d %H:%M:%S')
|
||||
if ts >= cutoff:
|
||||
kept.append(line)
|
||||
else:
|
||||
removed += 1
|
||||
except ValueError:
|
||||
kept.append(line) # malformed line — keep it
|
||||
with open(ACTION_LOG_FILE, 'w', encoding='utf-8') as f:
|
||||
f.writelines(kept)
|
||||
if removed:
|
||||
_log_action('LOG_PURGE', '', f'removed {removed} entries older than {days} days')
|
||||
print(f"Action log purged: {removed} old entries removed")
|
||||
except Exception as e:
|
||||
print(f"Action log purge error: {e}")
|
||||
|
||||
# File logger – appends on every run so history is preserved
|
||||
logging.basicConfig(
|
||||
filename=LOG_FILE,
|
||||
level=logging.DEBUG,
|
||||
format='%(asctime)s [%(levelname)s] %(message)s',
|
||||
datefmt='%Y-%m-%d %H:%M:%S',
|
||||
)
|
||||
log = logging.getLogger('db_manager')
|
||||
log.info('=== DatabaseManager module loaded ===')
|
||||
|
||||
class DatabaseManager:
|
||||
"""
|
||||
@@ -61,7 +115,8 @@ class DatabaseManager:
|
||||
user=self.user,
|
||||
password=self.password,
|
||||
connection_timeout=5,
|
||||
use_pure=True
|
||||
use_pure=True,
|
||||
autocommit=True
|
||||
)
|
||||
if test_conn.is_connected():
|
||||
test_conn.close()
|
||||
@@ -70,123 +125,187 @@ class DatabaseManager:
|
||||
return False, str(e)
|
||||
return False, "Connection failed"
|
||||
|
||||
def get_connection(self):
|
||||
"""Get a database connection."""
|
||||
try:
|
||||
if self.connection is None or not self.connection.is_connected():
|
||||
self.connection = mysql.connector.connect(
|
||||
def _new_conn(self):
|
||||
"""Open and return a fresh connection. Caller must close it."""
|
||||
return mysql.connector.connect(
|
||||
host=self.host,
|
||||
database=self.database,
|
||||
user=self.user,
|
||||
password=self.password,
|
||||
connection_timeout=5,
|
||||
use_pure=True
|
||||
use_pure=True,
|
||||
autocommit=True
|
||||
)
|
||||
|
||||
def get_connection(self):
|
||||
"""Get a reusable connection (kept for test_connection compatibility)."""
|
||||
try:
|
||||
if self.connection is None or not self.connection.is_connected():
|
||||
log.info(f'Opening persistent connection to {self.host}/{self.database}')
|
||||
self.connection = self._new_conn()
|
||||
log.info('Connection opened OK')
|
||||
return self.connection
|
||||
except Error as e:
|
||||
log.error(f'Connection error: {e}')
|
||||
print(f"Database connection error: {e}")
|
||||
return None
|
||||
|
||||
def init_database(self):
|
||||
"""Initialize the database connection and create the table if it doesn't exist."""
|
||||
# Purge action log entries older than 30 days on every startup
|
||||
purge_old_action_logs(30)
|
||||
_log_action('APP_START', '', f'host={self.host}')
|
||||
try:
|
||||
conn = self.get_connection()
|
||||
if conn and conn.is_connected():
|
||||
cursor = conn.cursor()
|
||||
conn = self._new_conn()
|
||||
cursor = conn.cursor(buffered=True)
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS offsystemsCounting (
|
||||
id VARCHAR(20) PRIMARY KEY,
|
||||
mass REAL NOT NULL
|
||||
)
|
||||
''')
|
||||
conn.commit()
|
||||
# Add t_update column if it doesn't exist yet (MySQL-compatible check)
|
||||
cursor.execute("""
|
||||
SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = %s
|
||||
AND TABLE_NAME = 'offsystemsCounting'
|
||||
AND COLUMN_NAME = 't_update'
|
||||
""", (self.database,))
|
||||
col_exists = cursor.fetchone()[0]
|
||||
cursor.close()
|
||||
if not col_exists:
|
||||
log.info("Adding t_update column to offsystemsCounting")
|
||||
c2 = conn.cursor()
|
||||
c2.execute("ALTER TABLE offsystemsCounting ADD COLUMN t_update DATETIME DEFAULT NULL")
|
||||
c2.close()
|
||||
log.info("t_update column added")
|
||||
conn.close()
|
||||
log.info("init_database complete")
|
||||
print(f"Connected to MariaDB database: {self.database}")
|
||||
print("Table 'offsystemsCounting' ready")
|
||||
except Error as e:
|
||||
log.error(f"Database initialization error: {e}")
|
||||
print(f"Database initialization error: {e}")
|
||||
|
||||
def read_all_data(self) -> List[Tuple[str, float]]:
|
||||
"""Read all data from the database."""
|
||||
try:
|
||||
conn = self.get_connection()
|
||||
if conn and conn.is_connected():
|
||||
cursor = conn.cursor()
|
||||
conn = self._new_conn()
|
||||
cursor = conn.cursor(buffered=True)
|
||||
cursor.execute("SELECT id, mass FROM offsystemsCounting ORDER BY id")
|
||||
return cursor.fetchall()
|
||||
rows = cursor.fetchall()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
return rows
|
||||
except Error as e:
|
||||
log.error(f"read_all_data error: {e}")
|
||||
print(f"Error reading data: {e}")
|
||||
return []
|
||||
|
||||
def search_by_id(self, record_id: str) -> Optional[Tuple[str, float]]:
|
||||
"""Search for a record by ID."""
|
||||
def search_by_id(self, record_id: str) -> Optional[Tuple]:
|
||||
"""Search for a record by ID. Returns (id, mass, t_update) or None."""
|
||||
log.info(f'search_by_id: looking up id={record_id!r}')
|
||||
try:
|
||||
conn = self.get_connection()
|
||||
if conn and conn.is_connected():
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT id, mass FROM offsystemsCounting WHERE id = %s", (record_id,))
|
||||
return cursor.fetchone()
|
||||
conn = self._new_conn()
|
||||
cursor = conn.cursor(buffered=True)
|
||||
cursor.execute("SELECT id, mass, t_update FROM offsystemsCounting WHERE id = %s", (record_id,))
|
||||
row = cursor.fetchone()
|
||||
# buffered=True already fetched the full result; no extra drain needed
|
||||
cursor.close()
|
||||
conn.close()
|
||||
log.info(f'search_by_id: result={row}')
|
||||
if row:
|
||||
_log_action('SEARCH_FOUND', record_id, f'mass={row[1]}, t_update={row[2]}')
|
||||
else:
|
||||
_log_action('SEARCH_NOT_FOUND', record_id, '')
|
||||
return row
|
||||
except Error as e:
|
||||
log.error(f'search_by_id error: {e}')
|
||||
_log_action('SEARCH_ERROR', record_id, str(e))
|
||||
print(f"Error searching data: {e}")
|
||||
return None
|
||||
|
||||
def add_or_update_record(self, record_id: str, mass: float) -> bool:
|
||||
"""Add a new record or update existing one if ID already exists."""
|
||||
"""Update mass/t_update for an existing record, or INSERT if it doesn't exist yet."""
|
||||
log.info(f'add_or_update_record: id={record_id!r} mass={mass}')
|
||||
try:
|
||||
conn = self.get_connection()
|
||||
if conn and conn.is_connected():
|
||||
cursor = conn.cursor()
|
||||
conn = self._new_conn()
|
||||
cursor = conn.cursor(buffered=True)
|
||||
|
||||
# Check if record exists
|
||||
existing = self.search_by_id(record_id)
|
||||
|
||||
if existing:
|
||||
# Update existing record
|
||||
cursor.execute(
|
||||
"UPDATE offsystemsCounting SET mass = %s WHERE id = %s",
|
||||
(mass, record_id)
|
||||
# Try UPDATE first
|
||||
update_sql = (
|
||||
"UPDATE offsystemsCounting "
|
||||
"SET mass = %s, t_update = NOW() "
|
||||
"WHERE id = %s"
|
||||
)
|
||||
print(f"Updated record: {record_id} = {mass}")
|
||||
log.debug(f'Executing SQL: {update_sql} | params=({mass}, {record_id!r})')
|
||||
cursor.execute(update_sql, (mass, record_id))
|
||||
affected = cursor.rowcount
|
||||
|
||||
if affected == 0:
|
||||
# Record does not exist yet — INSERT it
|
||||
insert_sql = (
|
||||
"INSERT INTO offsystemsCounting (id, mass, t_update) "
|
||||
"VALUES (%s, %s, NOW())"
|
||||
)
|
||||
log.debug(f'Executing SQL: {insert_sql} | params=({record_id!r}, {mass})')
|
||||
cursor.execute(insert_sql, (record_id, mass))
|
||||
affected = cursor.rowcount
|
||||
log.info(f'add_or_update_record: inserted new record, rowcount={affected}')
|
||||
_log_action('INSERT', record_id, f'mass={mass}')
|
||||
print(f"Inserted new record: {record_id} = {mass}")
|
||||
else:
|
||||
# Insert new record
|
||||
cursor.execute(
|
||||
"INSERT INTO offsystemsCounting (id, mass) VALUES (%s, %s)",
|
||||
(record_id, mass)
|
||||
)
|
||||
print(f"Added new record: {record_id} = {mass}")
|
||||
log.info(f'add_or_update_record: updated existing record, rowcount={affected}')
|
||||
_log_action('UPDATE', record_id, f'mass={mass}')
|
||||
print(f"Updated record: {record_id} = {mass} (rowcount={affected})")
|
||||
|
||||
conn.commit()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
return True
|
||||
except Error as e:
|
||||
log.error(f'add_or_update_record error: {e}')
|
||||
_log_action('UPDATE_ERROR', record_id, str(e))
|
||||
print(f"Error adding/updating record: {e}")
|
||||
return False
|
||||
|
||||
def delete_record(self, record_id: str) -> bool:
|
||||
"""Delete a record by ID."""
|
||||
log.info(f'delete_record: id={record_id!r}')
|
||||
try:
|
||||
conn = self.get_connection()
|
||||
if conn and conn.is_connected():
|
||||
cursor = conn.cursor()
|
||||
conn = self._new_conn()
|
||||
cursor = conn.cursor(buffered=True)
|
||||
cursor.execute("DELETE FROM offsystemsCounting WHERE id = %s", (record_id,))
|
||||
|
||||
if cursor.rowcount > 0:
|
||||
conn.commit()
|
||||
deleted = cursor.rowcount
|
||||
# DML produces no result set — do NOT fetchall()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
if deleted > 0:
|
||||
log.info(f'delete_record: deleted {deleted} row(s)')
|
||||
_log_action('DELETE', record_id, f'rows_deleted={deleted}')
|
||||
print(f"Deleted record: {record_id}")
|
||||
return True
|
||||
else:
|
||||
log.info(f'delete_record: no row found for id={record_id!r}')
|
||||
_log_action('DELETE_NOT_FOUND', record_id, '')
|
||||
print(f"No record found with ID: {record_id}")
|
||||
return False
|
||||
except Error as e:
|
||||
log.error(f'delete_record error: {e}')
|
||||
_log_action('DELETE_ERROR', record_id, str(e))
|
||||
print(f"Error deleting record: {e}")
|
||||
return False
|
||||
|
||||
def get_record_count(self) -> int:
|
||||
"""Get the total number of records in the database."""
|
||||
try:
|
||||
conn = self.get_connection()
|
||||
if conn and conn.is_connected():
|
||||
cursor = conn.cursor()
|
||||
conn = self._new_conn()
|
||||
cursor = conn.cursor(buffered=True)
|
||||
cursor.execute("SELECT COUNT(*) FROM offsystemsCounting")
|
||||
return cursor.fetchone()[0]
|
||||
count = cursor.fetchone()[0]
|
||||
# fetchone() on a buffered cursor is fully consumed — no drain needed
|
||||
cursor.close()
|
||||
conn.close()
|
||||
return count
|
||||
except Error as e:
|
||||
print(f"Error getting record count: {e}")
|
||||
return 0
|
||||
|
||||
BIN
dist/DatabaseApp.exe
vendored
BIN
dist/DatabaseApp.exe
vendored
Binary file not shown.
141
main.py
141
main.py
@@ -3,7 +3,6 @@ from kivy.config import Config
|
||||
Config.set('kivy', 'keyboard_mode', 'system')
|
||||
from kivy.app import App
|
||||
from kivy.uix.boxlayout import BoxLayout
|
||||
from kivy.uix.anchorlayout import AnchorLayout
|
||||
from kivy.uix.floatlayout import FloatLayout
|
||||
from kivy.uix.gridlayout import GridLayout
|
||||
from kivy.uix.label import Label
|
||||
@@ -20,57 +19,33 @@ class DatabaseApp(App):
|
||||
super().__init__(**kwargs)
|
||||
self.db_manager = DatabaseManager()
|
||||
self.active_numpad_input = None
|
||||
self._pending_record_id = None # resolved (trimmed) ID locked at show_update_frame time
|
||||
|
||||
def build(self):
|
||||
# Set window to fullscreen first so Window.height reflects the screen
|
||||
Window.fullscreen = 'auto'
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Responsive sizing: derive all dimensions from the actual screen height
|
||||
# so the layout fits on any display (800p, 900p, 1080p, etc.)
|
||||
# Responsive sizing: derive dimensions from the actual screen height
|
||||
# ------------------------------------------------------------------
|
||||
wh = Window.height # actual screen height after fullscreen
|
||||
wh = Window.height
|
||||
|
||||
# Numpad occupies 29% of screen height (fixed proportion)
|
||||
h_numpad_wr = int(wh * 0.29)
|
||||
|
||||
# Main layout outer padding and spacing (scaled)
|
||||
# Scale factor (1.0 at 1080p, down to 0.65 on small screens)
|
||||
s = max(0.65, min(1.0, wh / 1080.0))
|
||||
pad_v = max(8, int(20 * s)) # top / bottom padding
|
||||
pad_h = max(8, int(30 * s)) # left / right padding
|
||||
m_spacing = max(4, int(10 * s)) # gap between content and numpad
|
||||
|
||||
# Space available for the 6 content rows (after numpad + padding + gap)
|
||||
avail_total = wh - h_numpad_wr - 2 * pad_v - m_spacing
|
||||
c_spacing = max(4, int(12 * s)) # gap between content rows
|
||||
avail_items = avail_total - c_spacing * 5 # 6 rows → 5 gaps
|
||||
|
||||
# Distribute height proportionally among rows
|
||||
# Reference weights: title=50, search=100, mode=38, buttons=65, update=187, status=40
|
||||
_w = [50, 100, 38, 65, 187, 40]
|
||||
_t = sum(_w)
|
||||
def _h(weight):
|
||||
return max(24, int(avail_items * weight / _t))
|
||||
|
||||
h_title = _h(_w[0])
|
||||
h_search = _h(_w[1])
|
||||
h_row = max(20, h_search // 2)
|
||||
h_mode = _h(_w[2])
|
||||
h_buttons = _h(_w[3])
|
||||
h_update = _h(_w[4])
|
||||
h_status = _h(_w[5])
|
||||
|
||||
# Update-frame internal heights
|
||||
# Padding / spacing (all scaled)
|
||||
pad_v = max(8, int(20 * s))
|
||||
pad_h = max(8, int(30 * s))
|
||||
m_spacing = max(4, int(10 * s))
|
||||
c_spacing = max(4, int(12 * s))
|
||||
sp = max(6, int(10 * s))
|
||||
upd_pad = max(4, int(8 * s))
|
||||
upd_spc = max(4, int(8 * s))
|
||||
h_upd_title = max(20, int(h_update * 0.20))
|
||||
h_upd_row = max(20, int(h_update * 0.24))
|
||||
h_upd_inputs = h_upd_row * 2 + upd_spc
|
||||
h_upd_btns = max(28, h_update - h_upd_title - h_upd_inputs - 2*upd_pad - 2*upd_spc)
|
||||
|
||||
# Numpad internal heights
|
||||
np_pad_v = max(3, int(6 * s))
|
||||
np_spc = max(3, int(6 * s))
|
||||
|
||||
# Numpad (fixed height at bottom – 29 % of screen)
|
||||
h_numpad_wr = int(wh * 0.29)
|
||||
h_enter_btn = max(34, int(h_numpad_wr * 0.24))
|
||||
h_numpad_gr = h_numpad_wr - h_enter_btn - 2 * np_pad_v - np_spc
|
||||
|
||||
@@ -83,7 +58,11 @@ class DatabaseApp(App):
|
||||
f_mode = max(10, int(16 * s))
|
||||
f_override = max(10, int(14 * s))
|
||||
f_status = max(12, int(20 * s))
|
||||
sp = max(6, int(10 * s)) # generic widget spacing
|
||||
|
||||
# Proportional size_hint_y weights for the 6 content rows
|
||||
# title | search | mode | buttons | update-frame | status
|
||||
_w = [50, 100, 38, 65, 187, 40]
|
||||
_t = sum(_w)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Build UI
|
||||
@@ -98,12 +77,11 @@ class DatabaseApp(App):
|
||||
pos_hint={'x': 0, 'y': 0}
|
||||
)
|
||||
|
||||
# --- Content container (fills remaining space above numpad) ---
|
||||
content_layout = BoxLayout(orientation='vertical', spacing=c_spacing, size_hint_y=None)
|
||||
content_layout.bind(minimum_height=content_layout.setter('height'))
|
||||
# --- Content container: size_hint_y=1 so it fills all space above numpad ---
|
||||
content_layout = BoxLayout(orientation='vertical', spacing=c_spacing, size_hint_y=1)
|
||||
|
||||
# Title row: title + Exit button
|
||||
title_row = BoxLayout(orientation='horizontal', size_hint_y=None, height=h_title, spacing=sp)
|
||||
title_row = BoxLayout(orientation='horizontal', size_hint_y=_w[0]/_t, spacing=sp)
|
||||
title = Label(text='Database Search & Update', font_size=f_title, bold=True)
|
||||
title_row.add_widget(title)
|
||||
exit_btn = Button(
|
||||
@@ -121,8 +99,8 @@ class DatabaseApp(App):
|
||||
|
||||
# Search section (ID + Mass)
|
||||
search_layout = GridLayout(
|
||||
cols=2, size_hint_y=None, height=h_search,
|
||||
spacing=sp, row_force_default=True, row_default_height=h_row
|
||||
cols=2, size_hint_y=_w[1]/_t,
|
||||
spacing=sp
|
||||
)
|
||||
search_layout.add_widget(Label(text='ID:', size_hint_x=0.25, font_size=f_normal, bold=True))
|
||||
self.id_input = TextInput(
|
||||
@@ -135,17 +113,28 @@ class DatabaseApp(App):
|
||||
self.id_input.bind(focus=self.on_id_input_focus)
|
||||
search_layout.add_widget(self.id_input)
|
||||
search_layout.add_widget(Label(text='Mass:', size_hint_x=0.25, font_size=f_normal, bold=True))
|
||||
# Mass input + last-update label share the 0.75 right side equally
|
||||
mass_row = BoxLayout(orientation='horizontal', size_hint_x=0.75, spacing=sp)
|
||||
self.mass_input = TextInput(
|
||||
multiline=False, size_hint_x=0.75,
|
||||
multiline=False, size_hint_x=0.5,
|
||||
hint_text='Mass (read-only)',
|
||||
readonly=True, font_size=f_normal, padding=[7, 7]
|
||||
)
|
||||
search_layout.add_widget(self.mass_input)
|
||||
mass_row.add_widget(self.mass_input)
|
||||
self.last_update_label = Label(
|
||||
text='Last update: never',
|
||||
size_hint_x=0.5, font_size=max(9, int(13 * s)),
|
||||
bold=False, color=(0.7, 0.7, 0.7, 1),
|
||||
halign='left', valign='middle'
|
||||
)
|
||||
self.last_update_label.bind(size=self.last_update_label.setter('text_size'))
|
||||
mass_row.add_widget(self.last_update_label)
|
||||
search_layout.add_widget(mass_row)
|
||||
content_layout.add_widget(search_layout)
|
||||
|
||||
# Mode indicator row
|
||||
self.manual_override = None
|
||||
mode_row = BoxLayout(orientation='horizontal', size_hint_y=None, height=h_mode, spacing=sp)
|
||||
mode_row = BoxLayout(orientation='horizontal', size_hint_y=_w[2]/_t, spacing=sp)
|
||||
self.mode_label = Label(
|
||||
text='Article type detected: PRODUCT',
|
||||
size_hint_x=0.75, font_size=f_mode, bold=True, color=(0.4, 0.8, 1, 1)
|
||||
@@ -160,7 +149,7 @@ class DatabaseApp(App):
|
||||
content_layout.add_widget(mode_row)
|
||||
|
||||
# Action buttons (Add/Update, Reset, Settings)
|
||||
button_layout = GridLayout(cols=3, size_hint_y=None, height=h_buttons, spacing=sp)
|
||||
button_layout = GridLayout(cols=3, size_hint_y=_w[3]/_t, spacing=sp)
|
||||
add_update_btn = Button(text='Add/Update', font_size=f_btn, bold=True)
|
||||
add_update_btn.bind(on_press=self.show_update_frame)
|
||||
button_layout.add_widget(add_update_btn)
|
||||
@@ -175,16 +164,15 @@ class DatabaseApp(App):
|
||||
# Update frame
|
||||
self.update_frame = BoxLayout(
|
||||
orientation='vertical', padding=upd_pad, spacing=upd_spc,
|
||||
size_hint_y=None, height=h_update
|
||||
size_hint_y=_w[4]/_t
|
||||
)
|
||||
self.update_frame_label = Label(
|
||||
text='Update Values', size_hint_y=None, height=h_upd_title,
|
||||
text='Update Values', size_hint_y=0.20,
|
||||
font_size=f_btn, bold=True
|
||||
)
|
||||
self.update_frame.add_widget(self.update_frame_label)
|
||||
update_inputs = GridLayout(
|
||||
cols=2, spacing=sp, size_hint_y=None, height=h_upd_inputs,
|
||||
row_force_default=True, row_default_height=h_upd_row
|
||||
cols=2, spacing=sp, size_hint_y=0.52
|
||||
)
|
||||
update_inputs.add_widget(Label(text='ID:', size_hint_x=0.25, font_size=f_normal, bold=True))
|
||||
self.update_id_input = TextInput(
|
||||
@@ -198,7 +186,7 @@ class DatabaseApp(App):
|
||||
self.update_mass_input.bind(focus=self.on_mass_input_focus)
|
||||
update_inputs.add_widget(self.update_mass_input)
|
||||
self.update_frame.add_widget(update_inputs)
|
||||
update_buttons = GridLayout(cols=2, size_hint_y=None, height=h_upd_btns, spacing=sp)
|
||||
update_buttons = GridLayout(cols=2, size_hint_y=0.28, spacing=sp)
|
||||
self.update_confirm_btn = Button(text='Confirm Add/Update', disabled=True, font_size=f_btn, bold=True)
|
||||
self.update_confirm_btn.bind(on_press=self.add_update_record)
|
||||
update_buttons.add_widget(self.update_confirm_btn)
|
||||
@@ -212,16 +200,12 @@ class DatabaseApp(App):
|
||||
|
||||
# Status label
|
||||
self.status_label = Label(
|
||||
text='Ready', size_hint_y=None, height=h_status,
|
||||
text='Ready', size_hint_y=_w[5]/_t,
|
||||
color=(0, 0.8, 0, 1), font_size=f_status, bold=True
|
||||
)
|
||||
content_layout.add_widget(self.status_label)
|
||||
|
||||
# Wrap content in an AnchorLayout that fills all space above the numpad
|
||||
# so the content block is vertically centred regardless of screen size
|
||||
content_anchor = AnchorLayout(anchor_x='center', anchor_y='center', size_hint_y=1)
|
||||
content_anchor.add_widget(content_layout)
|
||||
main_layout.add_widget(content_anchor)
|
||||
main_layout.add_widget(content_layout)
|
||||
|
||||
# --- Numeric keypad ---
|
||||
numpad_wrapper = BoxLayout(
|
||||
@@ -299,7 +283,7 @@ class DatabaseApp(App):
|
||||
self._refocus_active()
|
||||
|
||||
def set_update_frame_enabled(self, enabled):
|
||||
self.update_id_input.readonly = not enabled
|
||||
self.update_id_input.readonly = True # ID is always readonly – always set from search
|
||||
self.update_mass_input.readonly = not enabled
|
||||
self.update_confirm_btn.disabled = not enabled
|
||||
self.delete_btn.disabled = not enabled
|
||||
@@ -341,17 +325,21 @@ class DatabaseApp(App):
|
||||
self.override_btn.background_color = (0.6, 0.2, 0.6, 1) if is_override else (0.3, 0.3, 0.3, 1)
|
||||
|
||||
def show_update_frame(self, instance):
|
||||
# If no value in search, copy from search fields
|
||||
record_id = self.id_input.text.strip()
|
||||
mass_text = self.mass_input.text.strip()
|
||||
self.set_update_frame_enabled(True)
|
||||
# If mass field is empty, just clear update frame
|
||||
if not record_id:
|
||||
self.update_id_input.text = ''
|
||||
self.update_mass_input.text = ''
|
||||
self._pending_record_id = None
|
||||
return
|
||||
self.update_id_input.text = record_id
|
||||
# Lock in the resolved (trimmed) DB id now; display original scan for the operator
|
||||
self._pending_record_id = self._resolve_id(record_id)
|
||||
self.update_id_input.text = record_id # show original barcode value
|
||||
self.update_mass_input.text = mass_text
|
||||
# Direct numpad and keyboard focus to mass field so operator can immediately enter new mass
|
||||
self.active_numpad_input = self.update_mass_input
|
||||
Clock.schedule_once(lambda dt: setattr(self.update_mass_input, 'focus', True), 0.05)
|
||||
|
||||
def search_record(self, instance):
|
||||
record_id = self.id_input.text.strip()
|
||||
@@ -372,11 +360,17 @@ class DatabaseApp(App):
|
||||
def _update(dt):
|
||||
if record:
|
||||
self.mass_input.text = str(record[1])
|
||||
t_update = record[2] if len(record) > 2 else None
|
||||
if t_update:
|
||||
self.last_update_label.text = f'Last update: {t_update.strftime("%d/%m/%Y %H:%M")}'
|
||||
else:
|
||||
self.last_update_label.text = 'Last update: never'
|
||||
self.show_status(f"Found: {record[0]} = {record[1]}")
|
||||
self.highlight_record(resolved_id)
|
||||
else:
|
||||
self.show_status(f"ID '{record_id}' not found in database", error=True)
|
||||
self.mass_input.text = ""
|
||||
self.last_update_label.text = 'Last update: never'
|
||||
self.show_status(f"ID '{record_id}' not found in database", error=True)
|
||||
Clock.schedule_once(_update)
|
||||
except Exception as e:
|
||||
err = str(e)
|
||||
@@ -385,7 +379,7 @@ class DatabaseApp(App):
|
||||
|
||||
def add_update_record(self, instance):
|
||||
"""Add or update a record from the update frame."""
|
||||
record_id = self.update_id_input.text.strip()
|
||||
record_id = self._pending_record_id
|
||||
mass_text = self.update_mass_input.text.strip()
|
||||
if not record_id or not mass_text:
|
||||
self.show_status("Please enter both ID and mass in update frame", error=True)
|
||||
@@ -406,9 +400,16 @@ class DatabaseApp(App):
|
||||
def _update(dt):
|
||||
if success:
|
||||
self.show_status(f"Successfully added/updated: {record_id} = {mass}")
|
||||
# Clear all fields and return focus to ID input
|
||||
self.update_id_input.text = ""
|
||||
self.update_mass_input.text = ""
|
||||
self.id_input.text = ""
|
||||
self.mass_input.text = ""
|
||||
self.last_update_label.text = 'Last update: never'
|
||||
self._pending_record_id = None
|
||||
self.set_update_frame_enabled(False)
|
||||
self.active_numpad_input = self.id_input
|
||||
Clock.schedule_once(lambda dt2: setattr(self.id_input, 'focus', True), 0.05)
|
||||
self.refresh_data(None)
|
||||
else:
|
||||
self.show_status("Failed to add/update record", error=True)
|
||||
@@ -420,11 +421,10 @@ class DatabaseApp(App):
|
||||
|
||||
def delete_record(self, instance):
|
||||
"""Delete a record using the update frame fields."""
|
||||
record_id = self.update_id_input.text.strip()
|
||||
record_id = self._pending_record_id
|
||||
if not record_id:
|
||||
self.show_status("Please enter an ID in the update fields to delete", error=True)
|
||||
return
|
||||
|
||||
# Confirm deletion
|
||||
self.show_confirmation_popup(
|
||||
f"Are you sure you want to delete ID '{record_id}'?",
|
||||
@@ -463,6 +463,11 @@ class DatabaseApp(App):
|
||||
"""Reset/clear the first ID and mass fields and set focus on ID field."""
|
||||
self.id_input.text = ""
|
||||
self.mass_input.text = ""
|
||||
self.last_update_label.text = 'Last update: never'
|
||||
self.update_id_input.text = ""
|
||||
self.update_mass_input.text = ""
|
||||
self._pending_record_id = None
|
||||
self.set_update_frame_enabled(False)
|
||||
self.active_numpad_input = self.id_input
|
||||
self.id_input.focus = True
|
||||
self.show_status("Fields cleared", error=False)
|
||||
|
||||
20
run_app.sh
20
run_app.sh
@@ -1,20 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Startup script for the Database Manager Kivy App
|
||||
|
||||
echo "Starting Database Manager App..."
|
||||
echo "================================"
|
||||
|
||||
# Check if virtual environment exists
|
||||
if [ ! -d "venv" ]; then
|
||||
echo "Virtual environment not found. Creating one..."
|
||||
python3 -m venv venv
|
||||
echo "Installing dependencies..."
|
||||
source venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
else
|
||||
source venv/bin/activate
|
||||
fi
|
||||
|
||||
echo "Launching application..."
|
||||
python main.py
|
||||
@@ -1,82 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# MariaDB Database Setup Script
|
||||
# This script helps set up the MariaDB database for the Kivy app
|
||||
|
||||
echo "MariaDB Database Setup for Kivy App"
|
||||
echo "=================================="
|
||||
echo
|
||||
|
||||
# Check if MariaDB is installed
|
||||
if ! command -v mysql &> /dev/null; then
|
||||
echo "❌ MariaDB is not installed."
|
||||
echo "Please install MariaDB first:"
|
||||
echo " sudo apt update"
|
||||
echo " sudo apt install mariadb-server"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if MariaDB service is running
|
||||
if ! systemctl is-active --quiet mariadb; then
|
||||
echo "⚠️ MariaDB service is not running."
|
||||
echo "Starting MariaDB service..."
|
||||
sudo systemctl start mariadb
|
||||
|
||||
if systemctl is-active --quiet mariadb; then
|
||||
echo "✅ MariaDB service started successfully."
|
||||
else
|
||||
echo "❌ Failed to start MariaDB service."
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "✅ MariaDB service is running."
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "Setting up database and user..."
|
||||
echo "You will be prompted for the MySQL root password."
|
||||
echo
|
||||
|
||||
# Create the setup SQL script
|
||||
cat > /tmp/setup_db.sql << EOF
|
||||
CREATE DATABASE IF NOT EXISTS cantare_injectie;
|
||||
CREATE USER IF NOT EXISTS 'omron'@'localhost' IDENTIFIED BY 'Initial01!';
|
||||
GRANT ALL PRIVILEGES ON cantare_injectie.* TO 'omron'@'localhost';
|
||||
FLUSH PRIVILEGES;
|
||||
|
||||
USE cantare_injectie;
|
||||
CREATE TABLE IF NOT EXISTS offsystemsCountin (
|
||||
id VARCHAR(20) PRIMARY KEY,
|
||||
mass REAL NOT NULL
|
||||
);
|
||||
|
||||
-- Show the created database and table
|
||||
SHOW DATABASES LIKE 'cantare_injectie';
|
||||
DESCRIBE offsystemsCounting;
|
||||
EOF
|
||||
|
||||
# Execute the SQL script
|
||||
mysql -u root -p < /tmp/setup_db.sql
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
echo
|
||||
echo "✅ Database setup completed successfully!"
|
||||
echo
|
||||
echo "Database details:"
|
||||
echo " Host: localhost"
|
||||
echo " Database: cantare_injectie"
|
||||
echo " User: omron"
|
||||
echo " Password: Initial01!"
|
||||
echo " Table: offsystemsCounting"
|
||||
echo
|
||||
echo "You can now run the Kivy application:"
|
||||
echo " python main.py"
|
||||
echo
|
||||
else
|
||||
echo
|
||||
echo "❌ Database setup failed."
|
||||
echo "Please check the error messages above."
|
||||
fi
|
||||
|
||||
# Clean up
|
||||
rm -f /tmp/setup_db.sql
|
||||
@@ -1,8 +0,0 @@
|
||||
-- Create user and grant privileges for MariaDB
|
||||
CREATE USER IF NOT EXISTS 'omron'@'localhost' IDENTIFIED BY 'Initial01!';
|
||||
GRANT ALL PRIVILEGES ON cantare_injectie.* TO 'omron'@'localhost';
|
||||
FLUSH PRIVILEGES;
|
||||
|
||||
-- Show the user and their privileges
|
||||
SELECT User, Host FROM mysql.user WHERE User='omron';
|
||||
SHOW GRANTS FOR 'omron'@'localhost';
|
||||
101
test_database.py
101
test_database.py
@@ -1,101 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script for the MariaDB Database Manager
|
||||
This script tests the basic functionality without the GUI
|
||||
"""
|
||||
|
||||
from database_manager import DatabaseManager
|
||||
|
||||
def test_database_operations():
|
||||
print("Testing MariaDB Database Manager...")
|
||||
print("-" * 40)
|
||||
|
||||
# Initialize database
|
||||
db = DatabaseManager()
|
||||
|
||||
# Test 1: Add some initial data
|
||||
print("1. Adding initial test data...")
|
||||
test_data = [
|
||||
("SYS001", 125.5),
|
||||
("SYS002", 89.7),
|
||||
("SYS003", 234.1)
|
||||
]
|
||||
|
||||
for record_id, mass in test_data:
|
||||
success = db.add_or_update_record(record_id, mass)
|
||||
print(f" Added {record_id}: {'✓' if success else '✗'}")
|
||||
|
||||
print()
|
||||
|
||||
# Test 2: Read all data
|
||||
print("2. Reading all data...")
|
||||
records = db.read_all_data()
|
||||
for record in records:
|
||||
print(f" ID: {record[0]}, Mass: {record[1]}")
|
||||
print(f" Total records: {len(records)}")
|
||||
print()
|
||||
|
||||
# Test 3: Search for existing record
|
||||
print("3. Searching for existing record 'SYS001'...")
|
||||
result = db.search_by_id("SYS001")
|
||||
if result:
|
||||
print(f" Found: {result[0]} = {result[1]}")
|
||||
else:
|
||||
print(" Not found")
|
||||
print()
|
||||
|
||||
# Test 4: Search for non-existing record
|
||||
print("4. Searching for non-existing record 'SYS999'...")
|
||||
result = db.search_by_id("SYS999")
|
||||
if result:
|
||||
print(f" Found: {result[0]} = {result[1]}")
|
||||
else:
|
||||
print(" Not found - this is expected!")
|
||||
print()
|
||||
|
||||
# Test 5: Add the missing record
|
||||
print("5. Adding the missing record 'SYS999'...")
|
||||
success = db.add_or_update_record("SYS999", 456.8)
|
||||
print(f" Added SYS999: {'✓' if success else '✗'}")
|
||||
|
||||
# Verify it was added
|
||||
result = db.search_by_id("SYS999")
|
||||
if result:
|
||||
print(f" Verification: Found {result[0]} = {result[1]}")
|
||||
print()
|
||||
|
||||
# Test 6: Update existing record
|
||||
print("6. Updating existing record 'SYS001'...")
|
||||
success = db.add_or_update_record("SYS001", 150.0)
|
||||
print(f" Updated SYS001: {'✓' if success else '✗'}")
|
||||
|
||||
result = db.search_by_id("SYS001")
|
||||
if result:
|
||||
print(f" New value: {result[0]} = {result[1]}")
|
||||
print()
|
||||
|
||||
# Test 7: Final count
|
||||
print("7. Final database state...")
|
||||
records = db.read_all_data()
|
||||
print(f" Total records: {len(records)}")
|
||||
for record in records:
|
||||
print(f" {record[0]} = {record[1]}")
|
||||
print()
|
||||
|
||||
# Test 8: Delete a record
|
||||
print("8. Testing delete functionality...")
|
||||
success = db.delete_record("SYS999")
|
||||
print(f" Deleted SYS999: {'✓' if success else '✗'}")
|
||||
|
||||
final_records = db.read_all_data()
|
||||
print(f" Final record count: {len(final_records)}")
|
||||
print()
|
||||
|
||||
# Close connection
|
||||
db.close_connection()
|
||||
|
||||
print("✓ All tests completed successfully!")
|
||||
print("Note: Test records remain in the MariaDB database.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_database_operations()
|
||||
Reference in New Issue
Block a user