dolibarr.stundenzettel/core/modules/modStundenzettel.class.php
data 15fec1e8df Fix: CSS-Einbindung mit file_exists() absichern
- filemtime() warf Fehler wenn CSS-Datei nicht existiert
- Alle Update 1.2.0 Felder werden jetzt bei Aktivierung geprüft/angelegt
- SQL-Update-Skript erweitert

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-08 18:45:37 +01:00

535 lines
20 KiB
PHP

<?php
/* Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*/
/**
* \defgroup stundenzettel Module Stundenzettel
* \brief Stundenzettel-Verwaltung für Aufträge
*/
/**
* \file core/modules/modStundenzettel.class.php
* \ingroup stundenzettel
* \brief Modulbeschreibung und Setup
*/
include_once DOL_DOCUMENT_ROOT.'/core/modules/DolibarrModules.class.php';
/**
* Class modStundenzettel
*/
class modStundenzettel extends DolibarrModules
{
/**
* Constructor
*
* @param DoliDB $db Database handler
*/
public function __construct($db)
{
global $langs, $conf;
$this->db = $db;
// Modul-ID (muss eindeutig sein)
$this->numero = 500200;
// Familie/Kategorie
$this->family = "crm";
// Position im Menü (zwischen Einkauf=40 und Rechnung=50)
$this->module_position = '45';
// Modulname
$this->name = preg_replace('/^mod/i', '', get_class($this));
// Beschreibung
$this->description = "Stundenzettel-Verwaltung für Aufträge - Dokumentation von Arbeitszeiten und Materialverbrauch";
$this->descriptionlong = "Verwaltet Stundenzettel für Kundenaufträge. Ermöglicht die Dokumentation von Arbeitszeiten, verbrauchten Materialien und Notizen. Integration mit SubtotalTitle für Produktgruppen-Unterstützung.";
// Version
$this->version = '1.2.0';
// Autor
$this->editor_name = 'Data IT Solution';
$this->editor_url = 'https://data-it-solution.de';
// Konstanten
$this->const_name = 'MAIN_MODULE_'.strtoupper($this->name);
// Pfade
$this->special = 0;
$this->picto = 'clock';
// Abhängigkeiten
$this->depends = array('modCommande'); // Aufträge erforderlich
$this->requiredby = array();
$this->conflictwith = array();
// PHP-Version
$this->phpmin = array(7, 4);
// Dolibarr-Version
$this->need_dolibarr_version = array(16, 0);
// Daten-Verzeichnisse
$this->dirs = array('/stundenzettel/temp');
// Konfiguration
$this->config_page_url = array("setup.php@stundenzettel");
// Konstanten
$this->const = array(
0 => array(
'STUNDENZETTEL_ADDON',
'chaine',
'mod_stundenzettel_standard',
'Nummernkreis für Stundenzettel',
0,
'current',
1
),
1 => array(
'STUNDENZETTEL_TIME_INPUT_MODE',
'chaine',
'dropdown',
'Zeiteingabe-Modus: dropdown (15-Min-Takt) oder text (freie Eingabe)',
0,
'current',
1
),
);
// Tabs - Tab im Auftrag (order = commande)
$this->tabs = array(
'order:+stundenzettel:Stundenzettel:stundenzettel@stundenzettel:$user->hasRight("stundenzettel","read"):/custom/stundenzettel/stundenzettel_commande.php?id=__ID__&tab=products&noredirect=1'
);
// Boxen/Widgets
$this->boxes = array(
0 => array(
'file' => 'box_stundenzettel_recent@stundenzettel',
'note' => 'Zuletzt bearbeitete Stundenzettel',
'enabledbydefaulton' => 'Home'
),
1 => array(
'file' => 'box_stundenzettel_open@stundenzettel',
'note' => 'Offene Stundenzettel',
'enabledbydefaulton' => 'Home'
),
);
// Cronjobs
$this->cronjobs = array();
// Berechtigungen
$this->rights = array();
$this->rights_class = 'stundenzettel';
$r = 0;
// Lesen
$this->rights[$r][0] = $this->numero + $r;
$this->rights[$r][1] = 'Stundenzettel lesen';
$this->rights[$r][3] = 0;
$this->rights[$r][4] = 'read';
$this->rights[$r][5] = '';
$r++;
// Erstellen
$this->rights[$r][0] = $this->numero + $r;
$this->rights[$r][1] = 'Stundenzettel erstellen';
$this->rights[$r][3] = 0;
$this->rights[$r][4] = 'write';
$this->rights[$r][5] = '';
$r++;
// Freigeben
$this->rights[$r][0] = $this->numero + $r;
$this->rights[$r][1] = 'Stundenzettel freigeben';
$this->rights[$r][3] = 0;
$this->rights[$r][4] = 'validate';
$this->rights[$r][5] = '';
$r++;
// Löschen
$this->rights[$r][0] = $this->numero + $r;
$this->rights[$r][1] = 'Stundenzettel löschen';
$this->rights[$r][3] = 0;
$this->rights[$r][4] = 'delete';
$this->rights[$r][5] = '';
$r++;
// Hauptmenü
$this->menu = array();
$r = 0;
// Top-Menü
$this->menu[$r] = array(
'fk_menu' => '',
'type' => 'top',
'titre' => 'Stundenzettel',
'prefix' => img_picto('', 'clock', 'class="pictofixedwidth"'),
'mainmenu' => 'stundenzettel',
'leftmenu' => '',
'url' => '/stundenzettel/index.php',
'langs' => 'stundenzettel@stundenzettel',
'position' => 45,
'enabled' => '$conf->stundenzettel->enabled',
'perms' => '$user->hasRight("stundenzettel", "read")',
'target' => '',
'user' => 0
);
$r++;
// Linkes Menü - Übersicht
$this->menu[$r] = array(
'fk_menu' => 'fk_mainmenu=stundenzettel',
'type' => 'left',
'titre' => 'Übersicht',
'prefix' => img_picto('', 'home', 'class="pictofixedwidth"'),
'mainmenu' => 'stundenzettel',
'leftmenu' => 'stundenzettel_index',
'url' => '/stundenzettel/index.php',
'langs' => 'stundenzettel@stundenzettel',
'position' => 100,
'enabled' => '$conf->stundenzettel->enabled',
'perms' => '$user->hasRight("stundenzettel", "read")',
'target' => '',
'user' => 0
);
$r++;
// Linkes Menü - Liste
$this->menu[$r] = array(
'fk_menu' => 'fk_mainmenu=stundenzettel',
'type' => 'left',
'titre' => 'Alle Stundenzettel',
'prefix' => img_picto('', 'list', 'class="pictofixedwidth"'),
'mainmenu' => 'stundenzettel',
'leftmenu' => 'stundenzettel_list',
'url' => '/stundenzettel/list.php',
'langs' => 'stundenzettel@stundenzettel',
'position' => 110,
'enabled' => '$conf->stundenzettel->enabled',
'perms' => '$user->hasRight("stundenzettel", "read")',
'target' => '',
'user' => 0
);
$r++;
// Linkes Menü - Neuer Stundenzettel
$this->menu[$r] = array(
'fk_menu' => 'fk_mainmenu=stundenzettel',
'type' => 'left',
'titre' => 'Neuer Stundenzettel',
'prefix' => img_picto('', 'add', 'class="pictofixedwidth"'),
'mainmenu' => 'stundenzettel',
'leftmenu' => 'stundenzettel_new',
'url' => '/stundenzettel/card.php?action=create',
'langs' => 'stundenzettel@stundenzettel',
'position' => 120,
'enabled' => '$conf->stundenzettel->enabled',
'perms' => '$user->hasRight("stundenzettel", "write")',
'target' => '',
'user' => 0
);
$r++;
// Linkes Menü - Offene Stundenzettel (Entwürfe)
$this->menu[$r] = array(
'fk_menu' => 'fk_mainmenu=stundenzettel',
'type' => 'left',
'titre' => 'Offene Stundenzettel',
'prefix' => img_picto('', 'statut0', 'class="pictofixedwidth"'),
'mainmenu' => 'stundenzettel',
'leftmenu' => 'stundenzettel_open',
'url' => '/stundenzettel/list.php?search_status=0',
'langs' => 'stundenzettel@stundenzettel',
'position' => 130,
'enabled' => '$conf->stundenzettel->enabled',
'perms' => '$user->hasRight("stundenzettel", "read")',
'target' => '',
'user' => 0
);
$r++;
// Linkes Menü - Meine Stundenzettel
$this->menu[$r] = array(
'fk_menu' => 'fk_mainmenu=stundenzettel',
'type' => 'left',
'titre' => 'Meine Stundenzettel',
'prefix' => img_picto('', 'user', 'class="pictofixedwidth"'),
'mainmenu' => 'stundenzettel',
'leftmenu' => 'stundenzettel_my',
'url' => '/stundenzettel/list.php?search_author=__USER_ID__',
'langs' => 'stundenzettel@stundenzettel',
'position' => 140,
'enabled' => '$conf->stundenzettel->enabled',
'perms' => '$user->hasRight("stundenzettel", "read")',
'target' => '',
'user' => 0
);
$r++;
// Linkes Menü - Freigegebene Stundenzettel
$this->menu[$r] = array(
'fk_menu' => 'fk_mainmenu=stundenzettel',
'type' => 'left',
'titre' => 'Freigegeben',
'prefix' => img_picto('', 'statut4', 'class="pictofixedwidth"'),
'mainmenu' => 'stundenzettel',
'leftmenu' => 'stundenzettel_validated',
'url' => '/stundenzettel/list.php?search_status=1',
'langs' => 'stundenzettel@stundenzettel',
'position' => 150,
'enabled' => '$conf->stundenzettel->enabled',
'perms' => '$user->hasRight("stundenzettel", "read")',
'target' => '',
'user' => 0
);
$r++;
}
/**
* Funktion beim Aktivieren des Moduls
*
* @param string $options Options when enabling module
* @return int 1 if OK, 0 if KO
*/
public function init($options = '')
{
global $conf;
$result = $this->_load_tables('/stundenzettel/sql/');
if ($result < 0) {
return -1;
}
// Extrafeld "Auftragsbeschreibung" für Aufträge anlegen
$this->createExtraFieldOrderDescription();
// View für Dienstleistungen erstellen (für sellist-Filter)
$this->createServicesView();
// Extrafeld "Standard-Leistung" für Kunden anlegen
$this->createExtraFieldDefaultService();
// Stundenpreis-Felder hinzufügen (Update 1.2.0)
$this->addHourlyRateFields();
$sql = array();
return $this->_init($sql, $options);
}
/**
* Erstellt das Extrafeld "Auftragsbeschreibung" für Aufträge (commande)
*
* @return int 1 if created or exists, -1 if error
*/
private function createExtraFieldOrderDescription()
{
global $langs;
require_once DOL_DOCUMENT_ROOT.'/core/class/extrafields.class.php';
$extrafields = new ExtraFields($this->db);
// Prüfen ob Extrafeld bereits existiert
$extrafields->fetch_name_optionals_label('commande');
if (!isset($extrafields->attributes['commande']['label']['auftragsbeschreibung'])) {
// Extrafeld anlegen: Textarea für Auftragsbeschreibung
$result = $extrafields->addExtraField(
'auftragsbeschreibung', // attrname - Feldname
'Auftragsbeschreibung', // label - Anzeigename
'text', // type - Feldtyp (text = Textarea)
100, // pos - Position
'', // size - Größe (leer für text)
'commande', // elementtype - Objekttyp
0, // unique
0, // required
'', // default_value
array('options' => array()), // param
1, // alwayseditable
'', // perms
1, // list - In Liste anzeigen
'', // ishidden
0, // computed
'', // entity
'', // langfile
'', // enabled
0, // totalizable
0, // printable
array('css' => '', 'cssview' => '', 'csslist' => '') // moreparams
);
if ($result < 0) {
dol_syslog("modStundenzettel::createExtraFieldOrderDescription Error creating extrafield: ".$extrafields->error, LOG_ERR);
return -1;
}
dol_syslog("modStundenzettel::createExtraFieldOrderDescription Extrafield 'auftragsbeschreibung' created successfully", LOG_DEBUG);
}
return 1;
}
/**
* Erstellt eine View für Dienstleistungen (fk_product_type = 1)
* Diese View wird für das sellist-Extrafeld verwendet
*
* @return int 1 if created, -1 if error
*/
private function createServicesView()
{
$sql = "CREATE OR REPLACE VIEW ".MAIN_DB_PREFIX."product_services AS
SELECT rowid, ref, label, description, fk_product_type, entity, tosell, tobuy
FROM ".MAIN_DB_PREFIX."product
WHERE fk_product_type = 1";
$resql = $this->db->query($sql);
if (!$resql) {
dol_syslog("modStundenzettel::createServicesView Error: ".$this->db->lasterror(), LOG_ERR);
return -1;
}
dol_syslog("modStundenzettel::createServicesView View created successfully", LOG_DEBUG);
return 1;
}
/**
* Erstellt das Extrafeld "Standard-Leistung" für Kunden (societe)
*
* @return int 1 if created or exists, -1 if error
*/
private function createExtraFieldDefaultService()
{
global $langs;
require_once DOL_DOCUMENT_ROOT.'/core/class/extrafields.class.php';
$extrafields = new ExtraFields($this->db);
// Prüfen ob Extrafeld bereits existiert
$extrafields->fetch_name_optionals_label('societe');
if (!isset($extrafields->attributes['societe']['label']['stundenzettel_default_service'])) {
// Extrafeld anlegen: sellist mit View für nur Dienstleistungen
$result = $extrafields->addExtraField(
'stundenzettel_default_service', // attrname - Feldname
'Standard-Leistung (Stundenzettel)', // label - Anzeigename
'sellist', // type - Feldtyp (sellist = SQL-basierte Auswahlliste)
200, // pos - Position
'', // size - Größe
'societe', // elementtype - Objekttyp (Kunden/Lieferanten)
0, // unique
0, // required
'', // default_value
array('options' => array('product_services:label:rowid' => null)), // param - View mit nur Dienstleistungen
1, // alwayseditable
'', // perms
1, // list - In Liste anzeigen
'', // ishidden
0, // computed
'', // entity
'', // langfile
'$conf->stundenzettel->enabled', // enabled - nur wenn Modul aktiv
0, // totalizable
0, // printable
array('css' => '', 'cssview' => '', 'csslist' => '') // moreparams
);
if ($result < 0) {
dol_syslog("modStundenzettel::createExtraFieldDefaultService Error creating extrafield: ".$extrafields->error, LOG_ERR);
return -1;
}
dol_syslog("modStundenzettel::createExtraFieldDefaultService Extrafield 'stundenzettel_default_service' created successfully", LOG_DEBUG);
}
return 1;
}
/**
* Fügt die neuen Felder für Update 1.2.0 hinzu
* Wird bei jeder Modulaktivierung ausgeführt - IF NOT EXISTS verhindert Fehler
*
* @return int 1 if OK, -1 if error
*/
private function addHourlyRateFields()
{
// 1. Spalte fk_product zur Leistungstabelle hinzufügen falls nicht vorhanden
$sql1 = "ALTER TABLE ".MAIN_DB_PREFIX."stundenzettel_leistung
ADD COLUMN IF NOT EXISTS fk_product INTEGER DEFAULT NULL
COMMENT 'Verknüpfung zur Leistungsposition (Dienstleistung)'";
$resql1 = $this->db->query($sql1);
if (!$resql1) {
// Fallback für ältere MySQL-Versionen ohne IF NOT EXISTS
$sql1b = "SHOW COLUMNS FROM ".MAIN_DB_PREFIX."stundenzettel_leistung LIKE 'fk_product'";
$resql1b = $this->db->query($sql1b);
if ($resql1b && $this->db->num_rows($resql1b) == 0) {
$sql1c = "ALTER TABLE ".MAIN_DB_PREFIX."stundenzettel_leistung
ADD COLUMN fk_product INTEGER DEFAULT NULL";
$this->db->query($sql1c);
}
}
// 2. Spalte hourly_rate zur Haupttabelle hinzufügen falls nicht vorhanden
$sql2 = "ALTER TABLE ".MAIN_DB_PREFIX."stundenzettel
ADD COLUMN IF NOT EXISTS hourly_rate DOUBLE(24,8) DEFAULT NULL
COMMENT 'Stundenpreis (NULL = Standard verwenden)'";
$resql2 = $this->db->query($sql2);
if (!$resql2) {
// Fallback für ältere MySQL-Versionen ohne IF NOT EXISTS
$sql2b = "SHOW COLUMNS FROM ".MAIN_DB_PREFIX."stundenzettel LIKE 'hourly_rate'";
$resql2b = $this->db->query($sql2b);
if ($resql2b && $this->db->num_rows($resql2b) == 0) {
$sql2c = "ALTER TABLE ".MAIN_DB_PREFIX."stundenzettel
ADD COLUMN hourly_rate DOUBLE(24,8) DEFAULT NULL";
$this->db->query($sql2c);
}
}
// 3. Spalte hourly_rate_is_custom zur Haupttabelle hinzufügen falls nicht vorhanden
$sql3 = "ALTER TABLE ".MAIN_DB_PREFIX."stundenzettel
ADD COLUMN IF NOT EXISTS hourly_rate_is_custom TINYINT DEFAULT 0 NOT NULL
COMMENT '1 = manuell geändert'";
$resql3 = $this->db->query($sql3);
if (!$resql3) {
// Fallback für ältere MySQL-Versionen ohne IF NOT EXISTS
$sql3b = "SHOW COLUMNS FROM ".MAIN_DB_PREFIX."stundenzettel LIKE 'hourly_rate_is_custom'";
$resql3b = $this->db->query($sql3b);
if ($resql3b && $this->db->num_rows($resql3b) == 0) {
$sql3c = "ALTER TABLE ".MAIN_DB_PREFIX."stundenzettel
ADD COLUMN hourly_rate_is_custom TINYINT DEFAULT 0 NOT NULL";
$this->db->query($sql3c);
}
}
dol_syslog("modStundenzettel::addHourlyRateFields All 1.2.0 fields checked/added", LOG_DEBUG);
return 1;
}
/**
* Funktion beim Deaktivieren des Moduls
*
* @param string $options Options when disabling module
* @return int 1 if OK, 0 if KO
*/
public function remove($options = '')
{
$sql = array();
return $this->_remove($sql, $options);
}
}