Compare commits

..

5 Commits

Author SHA1 Message Date
a8f642a1c9 docs: add README with full UI and workflow documentation 2026-04-09 11:05:43 +03:00
704e01669f fix: db update bug, add action log with 30-day purge, rebuild exe
- main.py: _pending_record_id locks resolved DB key at Add/Update time;
  show original barcode in update frame; auto-focus mass field on open;
  clear all fields and return focus to ID input after confirm/reset
- database_manager.py: buffered=True cursors on all SELECTs; no
  fetchall() after DML; replace ON DUPLICATE KEY UPDATE VALUES() with
  explicit UPDATE then INSERT fallback; add app_actions.log with
  structured per-action entries; purge_old_action_logs(30) on startup
- dist/DatabaseApp.exe: rebuilt single-file Windows binary (30.9 MB)
- remove unused files: README, WINDOWS_README, run_app.sh,
  setup_database.sh, setup_user.sql, test_database.py, sept.csv"
2026-04-09 11:00:37 +03:00
3604a46421 fix: restore collect_all for mysql.connector to fix no localization for language eng 2026-04-07 17:09:26 +03:00
0f7e157406 fix: dynamic height layout + bundle mysql locales to fix 'no localization for eng' error\n\n- Replace all fixed pixel heights in content_layout with size_hint_y proportions\n so the 6 rows fill available space on any screen (800p, 1080p, etc.)\n- Remove AnchorLayout wrapper that caused dead space above content\n- Bundle mysql/connector/locales in PyInstaller build (spec + build_windows.py)\n- Add mysql.connector.plugins hidden imports to spec and build script" 2026-04-07 16:52:10 +03:00
b51e8bcc2a updated to not view the log file 2026-04-07 16:05:41 +03:00
12 changed files with 458 additions and 10672 deletions

1
.gitignore vendored
View File

@@ -10,6 +10,7 @@ ENV/
*.egg-info/
build/
*.egg
build_log.txt
# PyInstaller
*.spec.bak

294
README.md
View File

@@ -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.

View File

@@ -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)

View File

@@ -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:

View File

@@ -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

Binary file not shown.

141
main.py
View File

@@ -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)

View File

@@ -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

10079
sept.csv

File diff suppressed because it is too large Load Diff

View File

@@ -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

View File

@@ -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';

View File

@@ -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()