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/
|
*.egg-info/
|
||||||
build/
|
build/
|
||||||
*.egg
|
*.egg
|
||||||
|
build_log.txt
|
||||||
|
|
||||||
# PyInstaller
|
# PyInstaller
|
||||||
*.spec.bak
|
*.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
|
## UI Layout (top to bottom)
|
||||||
- **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
|
|
||||||
|
|
||||||
## Prerequisites
|
```
|
||||||
|
┌─────────────────────────────────────┬────────┐
|
||||||
### MariaDB Server Setup
|
│ Database Search & Update │ Exit │
|
||||||
|
├──────┬──────────────────────────────┴────────┤
|
||||||
1. **Install MariaDB server** (if not already installed):
|
│ ID: │ [scan / type here] │
|
||||||
```bash
|
│ Mass:│ [current mass] Last update: ... │
|
||||||
sudo apt update
|
├──────┴───────────────────────────────────────┤
|
||||||
sudo apt install mariadb-server
|
│ Article type detected: PRODUCT [Override] │
|
||||||
```
|
├───────────────┬──────────────┬───────────────┤
|
||||||
|
│ Add/Update │ Reset Values │ Settings │
|
||||||
2. **Start MariaDB service**:
|
├───────────────┴──────────────┴───────────────┤
|
||||||
```bash
|
│ Update Values │
|
||||||
sudo systemctl start mariadb
|
│ ID: [read-only — original barcode] │
|
||||||
sudo systemctl enable mariadb
|
│ Mass: [editable] │
|
||||||
```
|
│ [Confirm Add/Update] [Delete] │
|
||||||
|
├──────────────────────────────────────────────┤
|
||||||
3. **Secure MariaDB installation**:
|
│ Status bar │
|
||||||
```bash
|
├──────────────────────────────────────────────┤
|
||||||
sudo mysql_secure_installation
|
│ [ 1 ] [ 2 ] [ 3 ] │
|
||||||
```
|
│ [ 4 ] [ 5 ] [ 6 ] Numeric keypad │
|
||||||
|
│ [ 7 ] [ 8 ] [ 9 ] │
|
||||||
4. **Create the database and user**:
|
│ [ . ] [ 0 ] [ ⌫ ] │
|
||||||
```bash
|
│ [ Enter ] │
|
||||||
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
|
|
||||||
)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Files
|
---
|
||||||
|
|
||||||
- `main.py`: Main Kivy application
|
## Article / Box Auto-Detection
|
||||||
- `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
|
|
||||||
|
|
||||||
## Connection Settings
|
When a barcode is entered in the **ID** field the app automatically classifies it:
|
||||||
|
|
||||||
The application connects to MariaDB with these settings:
|
| Rule | Type | DB key used |
|
||||||
- **Host**: localhost
|
|---|---|---|
|
||||||
- **Database**: cantare_injectie
|
| Exactly 8 digits | **BOX** | Leading zeros stripped — e.g. `00000003` → `3` |
|
||||||
- **User**: omron
|
| Anything else | **PRODUCT** | Used as-is |
|
||||||
- **Password**: Initial01!
|
|
||||||
- **Table**: offsystemsCounting
|
|
||||||
|
|
||||||
## Troubleshooting
|
The detected type is shown in the **mode bar** (blue = PRODUCT, amber = BOX).
|
||||||
|
|
||||||
1. **Connection Error 1698**:
|
### Manual Override
|
||||||
- Make sure MariaDB is running: `sudo systemctl start mariadb`
|
Press **Override type** to flip the detected type between BOX and PRODUCT.
|
||||||
- Verify user exists and has correct password
|
The mode bar shows `[Manual]` while an override is active.
|
||||||
- Check database exists: `SHOW DATABASES;`
|
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**:
|
## Typical Workflow
|
||||||
- The application will create the table automatically
|
|
||||||
- Make sure the database `cantare_injectie` exists
|
|
||||||
|
|
||||||
## 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
|
If the article is not in the database yet, Mass stays empty and the status bar shows "not found".
|
||||||
- Mass values must be valid decimal numbers
|
|
||||||
- The app includes comprehensive error handling and user feedback
|
### 2 — Add / Update
|
||||||
- All database operations use parameterized queries for security
|
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:
|
else:
|
||||||
print("No icon file found (optional)")
|
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
|
# PyInstaller command - simplified to avoid module collection issues
|
||||||
cmd = [
|
cmd = [
|
||||||
'pyinstaller',
|
'pyinstaller',
|
||||||
@@ -29,11 +36,17 @@ def build_executable():
|
|||||||
'--onefile',
|
'--onefile',
|
||||||
'--windowed',
|
'--windowed',
|
||||||
'--hidden-import=mysql.connector',
|
'--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=kivy.core.window.window_sdl2',
|
||||||
'--hidden-import=win32timezone',
|
'--hidden-import=win32timezone',
|
||||||
'--exclude-module=_tkinter',
|
'--exclude-module=_tkinter',
|
||||||
'--exclude-module=matplotlib',
|
'--exclude-module=matplotlib',
|
||||||
'--exclude-module=numpy',
|
'--exclude-module=numpy',
|
||||||
|
mysql_locales_arg,
|
||||||
] + icon_param + ['main.py']
|
] + icon_param + ['main.py']
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ from typing import List, Tuple, Optional
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
import logging
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
# When frozen by PyInstaller, __file__ points to a temp folder that is deleted on exit.
|
# 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.
|
# sys.executable points to the .exe location, which is persistent.
|
||||||
@@ -12,7 +14,59 @@ if getattr(sys, 'frozen', False):
|
|||||||
else:
|
else:
|
||||||
_BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
_BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
|
||||||
CONFIG_FILE = os.path.join(_BASE_DIR, 'config.json')
|
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:
|
class DatabaseManager:
|
||||||
"""
|
"""
|
||||||
@@ -61,7 +115,8 @@ class DatabaseManager:
|
|||||||
user=self.user,
|
user=self.user,
|
||||||
password=self.password,
|
password=self.password,
|
||||||
connection_timeout=5,
|
connection_timeout=5,
|
||||||
use_pure=True
|
use_pure=True,
|
||||||
|
autocommit=True
|
||||||
)
|
)
|
||||||
if test_conn.is_connected():
|
if test_conn.is_connected():
|
||||||
test_conn.close()
|
test_conn.close()
|
||||||
@@ -70,123 +125,187 @@ class DatabaseManager:
|
|||||||
return False, str(e)
|
return False, str(e)
|
||||||
return False, "Connection failed"
|
return False, "Connection failed"
|
||||||
|
|
||||||
|
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,
|
||||||
|
autocommit=True
|
||||||
|
)
|
||||||
|
|
||||||
def get_connection(self):
|
def get_connection(self):
|
||||||
"""Get a database connection."""
|
"""Get a reusable connection (kept for test_connection compatibility)."""
|
||||||
try:
|
try:
|
||||||
if self.connection is None or not self.connection.is_connected():
|
if self.connection is None or not self.connection.is_connected():
|
||||||
self.connection = mysql.connector.connect(
|
log.info(f'Opening persistent connection to {self.host}/{self.database}')
|
||||||
host=self.host,
|
self.connection = self._new_conn()
|
||||||
database=self.database,
|
log.info('Connection opened OK')
|
||||||
user=self.user,
|
|
||||||
password=self.password,
|
|
||||||
connection_timeout=5,
|
|
||||||
use_pure=True
|
|
||||||
)
|
|
||||||
return self.connection
|
return self.connection
|
||||||
except Error as e:
|
except Error as e:
|
||||||
|
log.error(f'Connection error: {e}')
|
||||||
print(f"Database connection error: {e}")
|
print(f"Database connection error: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def init_database(self):
|
def init_database(self):
|
||||||
"""Initialize the database connection and create the table if it doesn't exist."""
|
"""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:
|
try:
|
||||||
conn = self.get_connection()
|
conn = self._new_conn()
|
||||||
if conn and conn.is_connected():
|
cursor = conn.cursor(buffered=True)
|
||||||
cursor = conn.cursor()
|
cursor.execute('''
|
||||||
cursor.execute('''
|
CREATE TABLE IF NOT EXISTS offsystemsCounting (
|
||||||
CREATE TABLE IF NOT EXISTS offsystemsCounting (
|
id VARCHAR(20) PRIMARY KEY,
|
||||||
id VARCHAR(20) PRIMARY KEY,
|
mass REAL NOT NULL
|
||||||
mass REAL NOT NULL
|
)
|
||||||
)
|
''')
|
||||||
''')
|
# Add t_update column if it doesn't exist yet (MySQL-compatible check)
|
||||||
conn.commit()
|
cursor.execute("""
|
||||||
print(f"Connected to MariaDB database: {self.database}")
|
SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||||
print("Table 'offsystemsCounting' ready")
|
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:
|
except Error as e:
|
||||||
|
log.error(f"Database initialization error: {e}")
|
||||||
print(f"Database initialization error: {e}")
|
print(f"Database initialization error: {e}")
|
||||||
|
|
||||||
def read_all_data(self) -> List[Tuple[str, float]]:
|
def read_all_data(self) -> List[Tuple[str, float]]:
|
||||||
"""Read all data from the database."""
|
"""Read all data from the database."""
|
||||||
try:
|
try:
|
||||||
conn = self.get_connection()
|
conn = self._new_conn()
|
||||||
if conn and conn.is_connected():
|
cursor = conn.cursor(buffered=True)
|
||||||
cursor = conn.cursor()
|
cursor.execute("SELECT id, mass FROM offsystemsCounting ORDER BY id")
|
||||||
cursor.execute("SELECT id, mass FROM offsystemsCounting ORDER BY id")
|
rows = cursor.fetchall()
|
||||||
return cursor.fetchall()
|
cursor.close()
|
||||||
|
conn.close()
|
||||||
|
return rows
|
||||||
except Error as e:
|
except Error as e:
|
||||||
|
log.error(f"read_all_data error: {e}")
|
||||||
print(f"Error reading data: {e}")
|
print(f"Error reading data: {e}")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def search_by_id(self, record_id: str) -> Optional[Tuple[str, float]]:
|
def search_by_id(self, record_id: str) -> Optional[Tuple]:
|
||||||
"""Search for a record by ID."""
|
"""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:
|
try:
|
||||||
conn = self.get_connection()
|
conn = self._new_conn()
|
||||||
if conn and conn.is_connected():
|
cursor = conn.cursor(buffered=True)
|
||||||
cursor = conn.cursor()
|
cursor.execute("SELECT id, mass, t_update FROM offsystemsCounting WHERE id = %s", (record_id,))
|
||||||
cursor.execute("SELECT id, mass FROM offsystemsCounting WHERE id = %s", (record_id,))
|
row = cursor.fetchone()
|
||||||
return 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:
|
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}")
|
print(f"Error searching data: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def add_or_update_record(self, record_id: str, mass: float) -> bool:
|
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:
|
try:
|
||||||
conn = self.get_connection()
|
conn = self._new_conn()
|
||||||
if conn and conn.is_connected():
|
cursor = conn.cursor(buffered=True)
|
||||||
cursor = conn.cursor()
|
|
||||||
|
# Try UPDATE first
|
||||||
# Check if record exists
|
update_sql = (
|
||||||
existing = self.search_by_id(record_id)
|
"UPDATE offsystemsCounting "
|
||||||
|
"SET mass = %s, t_update = NOW() "
|
||||||
if existing:
|
"WHERE id = %s"
|
||||||
# Update existing record
|
)
|
||||||
cursor.execute(
|
log.debug(f'Executing SQL: {update_sql} | params=({mass}, {record_id!r})')
|
||||||
"UPDATE offsystemsCounting SET mass = %s WHERE id = %s",
|
cursor.execute(update_sql, (mass, record_id))
|
||||||
(mass, record_id)
|
affected = cursor.rowcount
|
||||||
)
|
|
||||||
print(f"Updated record: {record_id} = {mass}")
|
if affected == 0:
|
||||||
else:
|
# Record does not exist yet — INSERT it
|
||||||
# Insert new record
|
insert_sql = (
|
||||||
cursor.execute(
|
"INSERT INTO offsystemsCounting (id, mass, t_update) "
|
||||||
"INSERT INTO offsystemsCounting (id, mass) VALUES (%s, %s)",
|
"VALUES (%s, %s, NOW())"
|
||||||
(record_id, mass)
|
)
|
||||||
)
|
log.debug(f'Executing SQL: {insert_sql} | params=({record_id!r}, {mass})')
|
||||||
print(f"Added new record: {record_id} = {mass}")
|
cursor.execute(insert_sql, (record_id, mass))
|
||||||
|
affected = cursor.rowcount
|
||||||
conn.commit()
|
log.info(f'add_or_update_record: inserted new record, rowcount={affected}')
|
||||||
return True
|
_log_action('INSERT', record_id, f'mass={mass}')
|
||||||
|
print(f"Inserted new record: {record_id} = {mass}")
|
||||||
|
else:
|
||||||
|
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})")
|
||||||
|
|
||||||
|
cursor.close()
|
||||||
|
conn.close()
|
||||||
|
return True
|
||||||
except Error as e:
|
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}")
|
print(f"Error adding/updating record: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def delete_record(self, record_id: str) -> bool:
|
def delete_record(self, record_id: str) -> bool:
|
||||||
"""Delete a record by ID."""
|
"""Delete a record by ID."""
|
||||||
|
log.info(f'delete_record: id={record_id!r}')
|
||||||
try:
|
try:
|
||||||
conn = self.get_connection()
|
conn = self._new_conn()
|
||||||
if conn and conn.is_connected():
|
cursor = conn.cursor(buffered=True)
|
||||||
cursor = conn.cursor()
|
cursor.execute("DELETE FROM offsystemsCounting WHERE id = %s", (record_id,))
|
||||||
cursor.execute("DELETE FROM offsystemsCounting WHERE id = %s", (record_id,))
|
deleted = cursor.rowcount
|
||||||
|
# DML produces no result set — do NOT fetchall()
|
||||||
if cursor.rowcount > 0:
|
cursor.close()
|
||||||
conn.commit()
|
conn.close()
|
||||||
print(f"Deleted record: {record_id}")
|
if deleted > 0:
|
||||||
return True
|
log.info(f'delete_record: deleted {deleted} row(s)')
|
||||||
else:
|
_log_action('DELETE', record_id, f'rows_deleted={deleted}')
|
||||||
print(f"No record found with ID: {record_id}")
|
print(f"Deleted record: {record_id}")
|
||||||
return False
|
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:
|
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}")
|
print(f"Error deleting record: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def get_record_count(self) -> int:
|
def get_record_count(self) -> int:
|
||||||
"""Get the total number of records in the database."""
|
"""Get the total number of records in the database."""
|
||||||
try:
|
try:
|
||||||
conn = self.get_connection()
|
conn = self._new_conn()
|
||||||
if conn and conn.is_connected():
|
cursor = conn.cursor(buffered=True)
|
||||||
cursor = conn.cursor()
|
cursor.execute("SELECT COUNT(*) FROM offsystemsCounting")
|
||||||
cursor.execute("SELECT COUNT(*) FROM offsystemsCounting")
|
count = cursor.fetchone()[0]
|
||||||
return cursor.fetchone()[0]
|
# fetchone() on a buffered cursor is fully consumed — no drain needed
|
||||||
|
cursor.close()
|
||||||
|
conn.close()
|
||||||
|
return count
|
||||||
except Error as e:
|
except Error as e:
|
||||||
print(f"Error getting record count: {e}")
|
print(f"Error getting record count: {e}")
|
||||||
return 0
|
return 0
|
||||||
|
|||||||
BIN
dist/DatabaseApp.exe
vendored
BIN
dist/DatabaseApp.exe
vendored
Binary file not shown.
169
main.py
169
main.py
@@ -3,7 +3,6 @@ from kivy.config import Config
|
|||||||
Config.set('kivy', 'keyboard_mode', 'system')
|
Config.set('kivy', 'keyboard_mode', 'system')
|
||||||
from kivy.app import App
|
from kivy.app import App
|
||||||
from kivy.uix.boxlayout import BoxLayout
|
from kivy.uix.boxlayout import BoxLayout
|
||||||
from kivy.uix.anchorlayout import AnchorLayout
|
|
||||||
from kivy.uix.floatlayout import FloatLayout
|
from kivy.uix.floatlayout import FloatLayout
|
||||||
from kivy.uix.gridlayout import GridLayout
|
from kivy.uix.gridlayout import GridLayout
|
||||||
from kivy.uix.label import Label
|
from kivy.uix.label import Label
|
||||||
@@ -20,70 +19,50 @@ class DatabaseApp(App):
|
|||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
self.db_manager = DatabaseManager()
|
self.db_manager = DatabaseManager()
|
||||||
self.active_numpad_input = None
|
self.active_numpad_input = None
|
||||||
|
self._pending_record_id = None # resolved (trimmed) ID locked at show_update_frame time
|
||||||
|
|
||||||
def build(self):
|
def build(self):
|
||||||
# Set window to fullscreen first so Window.height reflects the screen
|
# Set window to fullscreen first so Window.height reflects the screen
|
||||||
Window.fullscreen = 'auto'
|
Window.fullscreen = 'auto'
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Responsive sizing: derive all dimensions from the actual screen height
|
# Responsive sizing: derive dimensions from the actual screen height
|
||||||
# so the layout fits on any display (800p, 900p, 1080p, etc.)
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
wh = Window.height # actual screen height after fullscreen
|
wh = Window.height
|
||||||
|
|
||||||
# Numpad occupies 29% of screen height (fixed proportion)
|
# Scale factor (1.0 at 1080p, down to 0.65 on small screens)
|
||||||
h_numpad_wr = int(wh * 0.29)
|
s = max(0.65, min(1.0, wh / 1080.0))
|
||||||
|
|
||||||
# Main layout outer padding and spacing (scaled)
|
# Padding / spacing (all scaled)
|
||||||
s = max(0.65, min(1.0, wh / 1080.0))
|
pad_v = max(8, int(20 * s))
|
||||||
pad_v = max(8, int(20 * s)) # top / bottom padding
|
pad_h = max(8, int(30 * s))
|
||||||
pad_h = max(8, int(30 * s)) # left / right padding
|
m_spacing = max(4, int(10 * s))
|
||||||
m_spacing = max(4, int(10 * s)) # gap between content and numpad
|
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))
|
||||||
|
np_pad_v = max(3, int(6 * s))
|
||||||
|
np_spc = max(3, int(6 * s))
|
||||||
|
|
||||||
# Space available for the 6 content rows (after numpad + padding + gap)
|
# Numpad (fixed height at bottom – 29 % of screen)
|
||||||
avail_total = wh - h_numpad_wr - 2 * pad_v - m_spacing
|
h_numpad_wr = int(wh * 0.29)
|
||||||
c_spacing = max(4, int(12 * s)) # gap between content rows
|
h_enter_btn = max(34, int(h_numpad_wr * 0.24))
|
||||||
avail_items = avail_total - c_spacing * 5 # 6 rows → 5 gaps
|
h_numpad_gr = h_numpad_wr - h_enter_btn - 2 * np_pad_v - np_spc
|
||||||
|
|
||||||
# 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
|
|
||||||
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))
|
|
||||||
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
|
|
||||||
|
|
||||||
# Font sizes (scaled)
|
# Font sizes (scaled)
|
||||||
f_title = max(14, int(26 * s))
|
f_title = max(14, int(26 * s))
|
||||||
f_normal = max(11, int(18 * s))
|
f_normal = max(11, int(18 * s))
|
||||||
f_btn = max(12, int(20 * s))
|
f_btn = max(12, int(20 * s))
|
||||||
f_numpad = max(15, int(26 * s))
|
f_numpad = max(15, int(26 * s))
|
||||||
f_enter = max(14, int(24 * s))
|
f_enter = max(14, int(24 * s))
|
||||||
f_mode = max(10, int(16 * s))
|
f_mode = max(10, int(16 * s))
|
||||||
f_override = max(10, int(14 * s))
|
f_override = max(10, int(14 * s))
|
||||||
f_status = max(12, int(20 * 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
|
# Build UI
|
||||||
@@ -98,12 +77,11 @@ class DatabaseApp(App):
|
|||||||
pos_hint={'x': 0, 'y': 0}
|
pos_hint={'x': 0, 'y': 0}
|
||||||
)
|
)
|
||||||
|
|
||||||
# --- Content container (fills remaining space above numpad) ---
|
# --- Content container: size_hint_y=1 so it fills all space above numpad ---
|
||||||
content_layout = BoxLayout(orientation='vertical', spacing=c_spacing, size_hint_y=None)
|
content_layout = BoxLayout(orientation='vertical', spacing=c_spacing, size_hint_y=1)
|
||||||
content_layout.bind(minimum_height=content_layout.setter('height'))
|
|
||||||
|
|
||||||
# Title row: title + Exit button
|
# 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 = Label(text='Database Search & Update', font_size=f_title, bold=True)
|
||||||
title_row.add_widget(title)
|
title_row.add_widget(title)
|
||||||
exit_btn = Button(
|
exit_btn = Button(
|
||||||
@@ -121,8 +99,8 @@ class DatabaseApp(App):
|
|||||||
|
|
||||||
# Search section (ID + Mass)
|
# Search section (ID + Mass)
|
||||||
search_layout = GridLayout(
|
search_layout = GridLayout(
|
||||||
cols=2, size_hint_y=None, height=h_search,
|
cols=2, size_hint_y=_w[1]/_t,
|
||||||
spacing=sp, row_force_default=True, row_default_height=h_row
|
spacing=sp
|
||||||
)
|
)
|
||||||
search_layout.add_widget(Label(text='ID:', size_hint_x=0.25, font_size=f_normal, bold=True))
|
search_layout.add_widget(Label(text='ID:', size_hint_x=0.25, font_size=f_normal, bold=True))
|
||||||
self.id_input = TextInput(
|
self.id_input = TextInput(
|
||||||
@@ -135,17 +113,28 @@ class DatabaseApp(App):
|
|||||||
self.id_input.bind(focus=self.on_id_input_focus)
|
self.id_input.bind(focus=self.on_id_input_focus)
|
||||||
search_layout.add_widget(self.id_input)
|
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))
|
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(
|
self.mass_input = TextInput(
|
||||||
multiline=False, size_hint_x=0.75,
|
multiline=False, size_hint_x=0.5,
|
||||||
hint_text='Mass (read-only)',
|
hint_text='Mass (read-only)',
|
||||||
readonly=True, font_size=f_normal, padding=[7, 7]
|
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)
|
content_layout.add_widget(search_layout)
|
||||||
|
|
||||||
# Mode indicator row
|
# Mode indicator row
|
||||||
self.manual_override = None
|
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(
|
self.mode_label = Label(
|
||||||
text='Article type detected: PRODUCT',
|
text='Article type detected: PRODUCT',
|
||||||
size_hint_x=0.75, font_size=f_mode, bold=True, color=(0.4, 0.8, 1, 1)
|
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)
|
content_layout.add_widget(mode_row)
|
||||||
|
|
||||||
# Action buttons (Add/Update, Reset, Settings)
|
# 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 = Button(text='Add/Update', font_size=f_btn, bold=True)
|
||||||
add_update_btn.bind(on_press=self.show_update_frame)
|
add_update_btn.bind(on_press=self.show_update_frame)
|
||||||
button_layout.add_widget(add_update_btn)
|
button_layout.add_widget(add_update_btn)
|
||||||
@@ -175,16 +164,15 @@ class DatabaseApp(App):
|
|||||||
# Update frame
|
# Update frame
|
||||||
self.update_frame = BoxLayout(
|
self.update_frame = BoxLayout(
|
||||||
orientation='vertical', padding=upd_pad, spacing=upd_spc,
|
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(
|
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
|
font_size=f_btn, bold=True
|
||||||
)
|
)
|
||||||
self.update_frame.add_widget(self.update_frame_label)
|
self.update_frame.add_widget(self.update_frame_label)
|
||||||
update_inputs = GridLayout(
|
update_inputs = GridLayout(
|
||||||
cols=2, spacing=sp, size_hint_y=None, height=h_upd_inputs,
|
cols=2, spacing=sp, size_hint_y=0.52
|
||||||
row_force_default=True, row_default_height=h_upd_row
|
|
||||||
)
|
)
|
||||||
update_inputs.add_widget(Label(text='ID:', size_hint_x=0.25, font_size=f_normal, bold=True))
|
update_inputs.add_widget(Label(text='ID:', size_hint_x=0.25, font_size=f_normal, bold=True))
|
||||||
self.update_id_input = TextInput(
|
self.update_id_input = TextInput(
|
||||||
@@ -198,7 +186,7 @@ class DatabaseApp(App):
|
|||||||
self.update_mass_input.bind(focus=self.on_mass_input_focus)
|
self.update_mass_input.bind(focus=self.on_mass_input_focus)
|
||||||
update_inputs.add_widget(self.update_mass_input)
|
update_inputs.add_widget(self.update_mass_input)
|
||||||
self.update_frame.add_widget(update_inputs)
|
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 = Button(text='Confirm Add/Update', disabled=True, font_size=f_btn, bold=True)
|
||||||
self.update_confirm_btn.bind(on_press=self.add_update_record)
|
self.update_confirm_btn.bind(on_press=self.add_update_record)
|
||||||
update_buttons.add_widget(self.update_confirm_btn)
|
update_buttons.add_widget(self.update_confirm_btn)
|
||||||
@@ -212,16 +200,12 @@ class DatabaseApp(App):
|
|||||||
|
|
||||||
# Status label
|
# Status label
|
||||||
self.status_label = 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
|
color=(0, 0.8, 0, 1), font_size=f_status, bold=True
|
||||||
)
|
)
|
||||||
content_layout.add_widget(self.status_label)
|
content_layout.add_widget(self.status_label)
|
||||||
|
|
||||||
# Wrap content in an AnchorLayout that fills all space above the numpad
|
main_layout.add_widget(content_layout)
|
||||||
# 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)
|
|
||||||
|
|
||||||
# --- Numeric keypad ---
|
# --- Numeric keypad ---
|
||||||
numpad_wrapper = BoxLayout(
|
numpad_wrapper = BoxLayout(
|
||||||
@@ -299,7 +283,7 @@ class DatabaseApp(App):
|
|||||||
self._refocus_active()
|
self._refocus_active()
|
||||||
|
|
||||||
def set_update_frame_enabled(self, enabled):
|
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_mass_input.readonly = not enabled
|
||||||
self.update_confirm_btn.disabled = not enabled
|
self.update_confirm_btn.disabled = not enabled
|
||||||
self.delete_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)
|
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):
|
def show_update_frame(self, instance):
|
||||||
# If no value in search, copy from search fields
|
|
||||||
record_id = self.id_input.text.strip()
|
record_id = self.id_input.text.strip()
|
||||||
mass_text = self.mass_input.text.strip()
|
mass_text = self.mass_input.text.strip()
|
||||||
self.set_update_frame_enabled(True)
|
self.set_update_frame_enabled(True)
|
||||||
# If mass field is empty, just clear update frame
|
|
||||||
if not record_id:
|
if not record_id:
|
||||||
self.update_id_input.text = ''
|
self.update_id_input.text = ''
|
||||||
self.update_mass_input.text = ''
|
self.update_mass_input.text = ''
|
||||||
|
self._pending_record_id = None
|
||||||
return
|
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
|
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):
|
def search_record(self, instance):
|
||||||
record_id = self.id_input.text.strip()
|
record_id = self.id_input.text.strip()
|
||||||
@@ -372,11 +360,17 @@ class DatabaseApp(App):
|
|||||||
def _update(dt):
|
def _update(dt):
|
||||||
if record:
|
if record:
|
||||||
self.mass_input.text = str(record[1])
|
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.show_status(f"Found: {record[0]} = {record[1]}")
|
||||||
self.highlight_record(resolved_id)
|
self.highlight_record(resolved_id)
|
||||||
else:
|
else:
|
||||||
self.show_status(f"ID '{record_id}' not found in database", error=True)
|
|
||||||
self.mass_input.text = ""
|
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)
|
Clock.schedule_once(_update)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
err = str(e)
|
err = str(e)
|
||||||
@@ -385,7 +379,7 @@ class DatabaseApp(App):
|
|||||||
|
|
||||||
def add_update_record(self, instance):
|
def add_update_record(self, instance):
|
||||||
"""Add or update a record from the update frame."""
|
"""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()
|
mass_text = self.update_mass_input.text.strip()
|
||||||
if not record_id or not mass_text:
|
if not record_id or not mass_text:
|
||||||
self.show_status("Please enter both ID and mass in update frame", error=True)
|
self.show_status("Please enter both ID and mass in update frame", error=True)
|
||||||
@@ -406,9 +400,16 @@ class DatabaseApp(App):
|
|||||||
def _update(dt):
|
def _update(dt):
|
||||||
if success:
|
if success:
|
||||||
self.show_status(f"Successfully added/updated: {record_id} = {mass}")
|
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_id_input.text = ""
|
||||||
self.update_mass_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.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)
|
self.refresh_data(None)
|
||||||
else:
|
else:
|
||||||
self.show_status("Failed to add/update record", error=True)
|
self.show_status("Failed to add/update record", error=True)
|
||||||
@@ -420,11 +421,10 @@ class DatabaseApp(App):
|
|||||||
|
|
||||||
def delete_record(self, instance):
|
def delete_record(self, instance):
|
||||||
"""Delete a record using the update frame fields."""
|
"""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:
|
if not record_id:
|
||||||
self.show_status("Please enter an ID in the update fields to delete", error=True)
|
self.show_status("Please enter an ID in the update fields to delete", error=True)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Confirm deletion
|
# Confirm deletion
|
||||||
self.show_confirmation_popup(
|
self.show_confirmation_popup(
|
||||||
f"Are you sure you want to delete ID '{record_id}'?",
|
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."""
|
"""Reset/clear the first ID and mass fields and set focus on ID field."""
|
||||||
self.id_input.text = ""
|
self.id_input.text = ""
|
||||||
self.mass_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.active_numpad_input = self.id_input
|
||||||
self.id_input.focus = True
|
self.id_input.focus = True
|
||||||
self.show_status("Fields cleared", error=False)
|
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