diff --git a/open_workshop_report/__init__.py b/open_workshop_report/__init__.py new file mode 100644 index 0000000..55ec7fc --- /dev/null +++ b/open_workshop_report/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import reports diff --git a/open_workshop_report/__manifest__.py b/open_workshop_report/__manifest__.py new file mode 100644 index 0000000..f720958 --- /dev/null +++ b/open_workshop_report/__manifest__.py @@ -0,0 +1,39 @@ +{ + 'name': 'Open Workshop Reports', + 'license': 'AGPL-3', + 'version': '18.0.1.0.0', + 'summary': 'Nutzungsstatistiken und Reports für Open Workshop', + 'description': """ + Open Workshop Reports + ===================== + + Dieses Modul bietet: + - Dashboard mit Nutzungsstatistiken + - Pivot-Tabellen und Diagramme für POS Sessions + - Monatliche/Historische Auswertungen + - PDF-Reports für Team-Meetings + """, + 'depends': [ + 'base', + 'point_of_sale', + 'open_workshop_pos', + ], + 'author': 'matthias.lotz', + 'category': 'Point of Sale', + 'data': [ + 'security/ir.model.access.csv', + 'data/cron_data.xml', + 'views/dashboard_views.xml', + 'views/usage_stats_views.xml', + 'reports/usage_report_templates.xml', + 'reports/usage_reports.xml', + ], + 'assets': { + 'web.assets_backend': [ + 'open_workshop_report/static/src/css/dashboard.css', + ], + }, + 'installable': True, + 'application': False, + 'auto_install': False, +} diff --git a/open_workshop_report/data/cron_data.xml b/open_workshop_report/data/cron_data.xml new file mode 100644 index 0000000..ca66070 --- /dev/null +++ b/open_workshop_report/data/cron_data.xml @@ -0,0 +1,13 @@ + + + + + Generate Monthly Usage Statistics + + code + model.cron_generate_monthly_stats() + 1 + months + + + diff --git a/open_workshop_report/generate_stats.py b/open_workshop_report/generate_stats.py new file mode 100644 index 0000000..f9af704 --- /dev/null +++ b/open_workshop_report/generate_stats.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 +""" +Skript zum Generieren von Nutzungsstatistiken aus vorhandenen POS-Sessions +Aufruf: docker compose exec odoo-dev python3 /mnt/extra-addons/open_workshop/open_workshop_report/generate_stats.py +""" + +import sys +sys.path.insert(0, '/usr/lib/python3/dist-packages') + +import odoo +from odoo import api, SUPERUSER_ID +from datetime import datetime, timedelta +from dateutil.relativedelta import relativedelta + +# Konfiguration +DB_NAME = 'hh18' +CONFIG_FILE = '/etc/odoo/odoo-dev.conf' + +# Odoo initialisieren +odoo.tools.config.parse_config(['-c', CONFIG_FILE]) +odoo.cli.server.report_configuration() + +# Registry laden +registry = odoo.registry(DB_NAME) + +def generate_monthly_stats(): + """Generiert monatliche Statistiken für die letzten 12 Monate""" + with registry.cursor() as cr: + env = api.Environment(cr, SUPERUSER_ID, {}) + + UsageStats = env['open_workshop.usage.stats'] + + # Finde älteste und neueste Session + cr.execute(""" + SELECT MIN(start_at), MAX(stop_at) + FROM pos_session + WHERE state = 'closed' + """) + result = cr.fetchone() + + if not result or not result[0]: + print("Keine geschlossenen POS-Sessions gefunden!") + return + + oldest_date = result[0] + newest_date = result[1] or datetime.now() + + print(f"Generiere Statistiken von {oldest_date} bis {newest_date}") + + # Generiere Statistiken für jeden Monat + current_date = oldest_date.replace(day=1) + end_date = newest_date.replace(day=1) + + stats_created = 0 + + while current_date <= end_date: + period_start = current_date + period_end = (current_date + relativedelta(months=1)) - timedelta(seconds=1) + period_name = current_date.strftime('%B %Y') + + # Prüfe ob Statistik bereits existiert + existing = UsageStats.search([ + ('period_start', '=', period_start), + ('period_type', '=', 'month') + ], limit=1) + + if not existing: + # Finde alle Sessions in diesem Monat + sessions = env['pos.session'].search([ + ('state', '=', 'closed'), + ('start_at', '>=', period_start), + ('start_at', '<=', period_end) + ]) + + if sessions: + # Erstelle Statistik + UsageStats.create({ + 'period_name': period_name, + 'period_type': 'month', + 'period_start': period_start, + 'period_end': period_end, + 'session_ids': [(6, 0, sessions.ids)] + }) + stats_created += 1 + print(f"✓ {period_name}: {len(sessions)} Sessions") + else: + print(f"- {period_name}: Keine Sessions") + else: + print(f"→ {period_name}: Bereits vorhanden") + + # Nächster Monat + current_date = current_date + relativedelta(months=1) + + cr.commit() + print(f"\n✅ {stats_created} Statistik-Einträge erstellt!") + +if __name__ == '__main__': + try: + generate_monthly_stats() + except Exception as e: + print(f"❌ Fehler: {e}") + import traceback + traceback.print_exc() + sys.exit(1) diff --git a/open_workshop_report/models/__init__.py b/open_workshop_report/models/__init__.py new file mode 100644 index 0000000..7281618 --- /dev/null +++ b/open_workshop_report/models/__init__.py @@ -0,0 +1 @@ +from . import usage_stats diff --git a/open_workshop_report/models/usage_stats.py b/open_workshop_report/models/usage_stats.py new file mode 100644 index 0000000..cec7926 --- /dev/null +++ b/open_workshop_report/models/usage_stats.py @@ -0,0 +1,114 @@ +# -*- coding: utf-8 -*- +from odoo import api, fields, models, _ +from datetime import datetime, timedelta +from dateutil.relativedelta import relativedelta + + +class UsageStats(models.Model): + """Nutzungsstatistiken für Open Workshop""" + _name = 'open_workshop.usage.stats' + _description = 'Open Workshop Nutzungsstatistiken' + _order = 'period_start desc' + _rec_name = 'period_name' + + period_name = fields.Char('Zeitraum', compute='_compute_period_name', store=True) + period_start = fields.Date('Beginn', required=True) + period_end = fields.Date('Ende', required=True) + period_type = fields.Selection([ + ('day', 'Tag'), + ('week', 'Woche'), + ('month', 'Monat'), + ('quarter', 'Quartal'), + ('year', 'Jahr'), + ], string='Periodentyp', default='month', required=True) + + # Statistiken + total_sessions = fields.Integer('Gesamt Nutzungen', compute='_compute_stats', store=True) + total_revenue = fields.Monetary('Gesamtumsatz', compute='_compute_stats', store=True, currency_field='currency_id') + avg_per_day = fields.Float('Durchschnitt pro Tag', compute='_compute_stats', store=True, digits=(5, 2)) + unique_users = fields.Integer('Einzigartige Nutzer', compute='_compute_stats', store=True) + + currency_id = fields.Many2one('res.currency', string='Währung', + default=lambda self: self.env.company.currency_id) + + # POS Session IDs für diese Periode + session_ids = fields.Many2many('pos.session', string='POS Sessions', compute='_compute_stats', store=True) + + @api.depends('period_start', 'period_end', 'period_type') + def _compute_period_name(self): + for record in self: + if record.period_type == 'month': + record.period_name = record.period_start.strftime('%B %Y') + elif record.period_type == 'week': + record.period_name = f"KW {record.period_start.strftime('%W/%Y')}" + elif record.period_type == 'day': + record.period_name = record.period_start.strftime('%d.%m.%Y') + else: + record.period_name = f"{record.period_start.strftime('%d.%m.%Y')} - {record.period_end.strftime('%d.%m.%Y')}" + + @api.depends('period_start', 'period_end') + def _compute_stats(self): + for record in self: + # Konvertiere Date zu Datetime für die Suche + start_datetime = datetime.combine(record.period_start, datetime.min.time()) + end_datetime = datetime.combine(record.period_end, datetime.max.time()) + + # Suche alle POS Sessions in diesem Zeitraum + sessions = self.env['pos.session'].search([ + ('start_at', '>=', start_datetime), + ('start_at', '<=', end_datetime), + ('state', 'in', ['closed', 'closing_control']), + ]) + + record.session_ids = sessions + record.total_sessions = len(sessions) + record.total_revenue = sum(sessions.mapped('total_payments_amount')) + + # Berechne Durchschnitt pro Tag + days = (record.period_end - record.period_start).days + 1 + record.avg_per_day = record.total_sessions / days if days > 0 else 0.0 + + # Einzigartige Nutzer (über POS Orders) + orders = self.env['pos.order'].search([ + ('session_id', 'in', sessions.ids), + ('partner_id', '!=', False), + ]) + record.unique_users = len(orders.mapped('partner_id')) + + @api.model + def generate_monthly_stats(self, year=None, month=None): + """Generiere Statistiken für einen bestimmten Monat""" + if not year: + year = datetime.now().year + if not month: + month = datetime.now().month + + # Berechne Start und Ende des Monats + period_start = datetime(year, month, 1).date() + if month == 12: + period_end = datetime(year + 1, 1, 1).date() - timedelta(days=1) + else: + period_end = datetime(year, month + 1, 1).date() - timedelta(days=1) + + # Suche oder erstelle Statistik-Eintrag + stats = self.search([ + ('period_start', '=', period_start), + ('period_end', '=', period_end), + ('period_type', '=', 'month'), + ], limit=1) + + if not stats: + stats = self.create({ + 'period_start': period_start, + 'period_end': period_end, + 'period_type': 'month', + }) + + return stats + + @api.model + def cron_generate_monthly_stats(self): + """Cron-Job: Generiere Statistiken für letzten Monat""" + today = datetime.now() + last_month = today - relativedelta(months=1) + self.generate_monthly_stats(last_month.year, last_month.month) diff --git a/open_workshop_report/reports/__init__.py b/open_workshop_report/reports/__init__.py new file mode 100644 index 0000000..cffe1ae --- /dev/null +++ b/open_workshop_report/reports/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +# Reports are loaded via XML data files diff --git a/open_workshop_report/reports/usage_report_templates.xml b/open_workshop_report/reports/usage_report_templates.xml new file mode 100644 index 0000000..a95f833 --- /dev/null +++ b/open_workshop_report/reports/usage_report_templates.xml @@ -0,0 +1,121 @@ + + + + + + diff --git a/open_workshop_report/reports/usage_reports.xml b/open_workshop_report/reports/usage_reports.xml new file mode 100644 index 0000000..ea1868d --- /dev/null +++ b/open_workshop_report/reports/usage_reports.xml @@ -0,0 +1,13 @@ + + + + + Nutzungsstatistik PDF + open_workshop.usage.stats + qweb-pdf + open_workshop_report.usage_report_document + open_workshop_report.usage_report_document + + report + + diff --git a/open_workshop_report/security/ir.model.access.csv b/open_workshop_report/security/ir.model.access.csv new file mode 100644 index 0000000..33298c9 --- /dev/null +++ b/open_workshop_report/security/ir.model.access.csv @@ -0,0 +1,3 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_usage_stats_user,access.usage.stats.user,model_open_workshop_usage_stats,base.group_user,1,0,0,0 +access_usage_stats_manager,access.usage.stats.manager,model_open_workshop_usage_stats,point_of_sale.group_pos_manager,1,1,1,1 diff --git a/open_workshop_report/static/src/css/dashboard.css b/open_workshop_report/static/src/css/dashboard.css new file mode 100644 index 0000000..baad973 --- /dev/null +++ b/open_workshop_report/static/src/css/dashboard.css @@ -0,0 +1,66 @@ +/* Dashboard Styling for Open Workshop Usage Stats */ + +.o_dashboard { + background: #f9f9f9; + padding: 20px; +} + +.o_dashboard .o_aggregate { + background: white; + border-radius: 8px; + padding: 20px; + margin: 10px; + box-shadow: 0 2px 8px rgba(0,0,0,0.1); + text-align: center; + transition: transform 0.2s; +} + +.o_dashboard .o_aggregate:hover { + transform: translateY(-5px); + box-shadow: 0 4px 12px rgba(0,0,0,0.15); +} + +.o_dashboard .o_aggregate .o_value { + font-size: 2.5em; + font-weight: bold; + margin-bottom: 5px; +} + +.o_dashboard .o_aggregate .o_label { + color: #666; + font-size: 0.9em; + text-transform: uppercase; + letter-spacing: 1px; +} + +.o_dashboard .o_aggregate.total_sessions .o_value { + color: #0066cc; +} + +.o_dashboard .o_aggregate.avg_per_day .o_value { + color: #28a745; +} + +.o_dashboard .o_aggregate.unique_users .o_value { + color: #17a2b8; +} + +.o_dashboard .o_aggregate.total_revenue .o_value { + color: #ffc107; +} + +.o_dashboard .o_graph_view, +.o_dashboard .o_pivot_view { + background: white; + border-radius: 8px; + padding: 20px; + margin: 10px; + box-shadow: 0 2px 8px rgba(0,0,0,0.1); +} + +.o_dashboard h3 { + color: #333; + border-bottom: 2px solid #0066cc; + padding-bottom: 10px; + margin-bottom: 20px; +} diff --git a/open_workshop_report/views/dashboard_views.xml b/open_workshop_report/views/dashboard_views.xml new file mode 100644 index 0000000..43f99fe --- /dev/null +++ b/open_workshop_report/views/dashboard_views.xml @@ -0,0 +1,69 @@ + + + + + open_workshop.usage.dashboard + open_workshop.usage.stats + + + + + + + + + +
+
+ +
+
+
+
+
Nutzungen
+ +
+
+
⌀/Tag
+ +
+
+
+
+
Nutzer
+ +
+
+
Umsatz
+ +
+
+
+
+
+
+
+
+
+ + + + Nutzungs-Dashboard + open_workshop.usage.stats + kanban,graph,pivot + + + + + + + + +
diff --git a/open_workshop_report/views/usage_stats_views.xml b/open_workshop_report/views/usage_stats_views.xml new file mode 100644 index 0000000..d00e67c --- /dev/null +++ b/open_workshop_report/views/usage_stats_views.xml @@ -0,0 +1,123 @@ + + + + + open_workshop.usage.stats.tree + open_workshop.usage.stats + + + + + + + + + + + + + + + + open_workshop.usage.stats.form + open_workshop.usage.stats + +
+ +
+

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + + + open_workshop.usage.stats.pivot + open_workshop.usage.stats + + + + + + + + + + + + + + open_workshop.usage.stats.graph.bar + open_workshop.usage.stats + + + + + + + + + + + open_workshop.usage.stats.graph.line + open_workshop.usage.stats + + + + + + + + + + + + Nutzungsstatistiken + open_workshop.usage.stats + graph,pivot,list,form + {'group_by': ['period_start:month']} + +

+ Keine Statistiken vorhanden +

+

+ Klicken Sie auf "Statistiken generieren" um monatliche Nutzungsstatistiken zu erstellen. +

+
+
+ + + +