V 1.1
This commit is contained in:
commit
9627e4fea4
25 changed files with 6996 additions and 0 deletions
127
README.md
Normal file
127
README.md
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
# Stundenzettel Modul für Dolibarr
|
||||
|
||||
**Version:** 1.1.0
|
||||
**Autor:** Data IT Solution
|
||||
**Kompatibilität:** Dolibarr 16.0+
|
||||
**Lizenz:** GPL v3
|
||||
|
||||
## Beschreibung
|
||||
|
||||
Das Stundenzettel-Modul ermöglicht die Verwaltung von Stundenzetteln für Kundenaufträge in Dolibarr. Es bietet eine umfassende Dokumentation von Arbeitszeiten, verbrauchten Materialien, Mehraufwand und Notizen.
|
||||
|
||||
## Funktionen
|
||||
|
||||
### Kernfunktionen
|
||||
|
||||
- **Stundenzettel-Verwaltung**: Erstellen, Bearbeiten und Löschen von Stundenzetteln pro Auftrag und Datum
|
||||
- **Leistungserfassung**: Zeiterfassung mit Start-/Endzeit und automatischer Dauerberechnung
|
||||
- **Zeitüberlappungsprüfung**: Verhindert doppelte Zeitbuchungen auf demselben Stundenzettel
|
||||
- **Produktverfolgung**: Dokumentation von verbrauchten Materialien aus dem Auftrag
|
||||
- **Mehraufwand**: Erfassung zusätzlicher Produkte/Dienstleistungen, die nicht im Auftrag waren
|
||||
- **Entfällt-Markierung**: Kennzeichnung von Produkten, die nicht verbaut werden müssen
|
||||
- **Notizen**: Merkzettel und Notizen für den nächsten Termin
|
||||
|
||||
### Freigabe & Rechnungsstellung
|
||||
|
||||
- **Stundenzettel-Freigabe**: Sperren von Stundenzetteln nach Fertigstellung
|
||||
- **Rechnungsübernahme**: Automatische Übernahme aller Produkte und Leistungen in eine Rechnung
|
||||
- **Stunden-Modus**: Wahlweise Übernahme als Gesamtstunden oder pro Tag
|
||||
|
||||
### Integration
|
||||
|
||||
- **SubtotalTitle-Integration**: Unterstützung für Produktgruppen aus dem SubtotalTitle-Modul
|
||||
- **Auftragsintegration**: Direkter Zugriff auf Stundenzettel über Aufträge
|
||||
- **Kundenintegration**: Standard-Leistung pro Kunde konfigurierbar
|
||||
|
||||
## Installation
|
||||
|
||||
1. Modul-Ordner nach `/custom/stundenzettel/` kopieren
|
||||
2. In Dolibarr einloggen
|
||||
3. Unter **Einstellungen > Module/Anwendungen** das Modul "Stundenzettel" aktivieren
|
||||
4. Das Modul erstellt automatisch:
|
||||
- Erforderliche Datenbanktabellen
|
||||
- Extrafeld "Auftragsbeschreibung" für Aufträge
|
||||
- Extrafeld "Standard-Leistung" für Kunden
|
||||
- Datenbank-View für Dienstleistungen
|
||||
|
||||
## Konfiguration
|
||||
|
||||
Die Moduleinstellungen finden Sie unter **Einstellungen > Module > Stundenzettel > Zahnrad-Symbol**:
|
||||
|
||||
| Einstellung | Beschreibung |
|
||||
|-------------|--------------|
|
||||
| **Zeiteingabe-Modus** | Dropdown (15-Minuten-Takt) oder Freitext (exakte Uhrzeit) |
|
||||
| **Standard-Filter** | Welcher Filter in der Produktliste standardmäßig angezeigt wird |
|
||||
| **Standard-Datum** | Aktuelles Datum oder Datum des letzten offenen Stundenzettels |
|
||||
| **Stunden-Übernahme** | Gesamtstunden auf einer Zeile oder pro Tag eine Zeile |
|
||||
|
||||
### Standard-Leistung beim Kunden
|
||||
|
||||
Sie können beim Kunden (unter **Kunden > Kundenkarte**) eine Standard-Leistung (Dienstleistung) hinterlegen. Diese wird dann bei allen Stundenzetteln für diesen Kunden angezeigt und kann für die Rechnungsstellung verwendet werden.
|
||||
|
||||
## Datenbanktabellen
|
||||
|
||||
| Tabelle | Beschreibung |
|
||||
|---------|--------------|
|
||||
| `llx_stundenzettel` | Haupttabelle für Stundenzettel |
|
||||
| `llx_stundenzettel_leistung` | Leistungen/Arbeitszeiten |
|
||||
| `llx_stundenzettel_product` | Verbrauchte Produkte |
|
||||
| `llx_stundenzettel_tracking` | Gesamtübersicht Mengen pro Auftrag |
|
||||
| `llx_stundenzettel_note` | Notizen und Merkzettel |
|
||||
| `llx_product_services` | View für Dienstleistungen (Extrafeld-Filter) |
|
||||
|
||||
## Extrafelder
|
||||
|
||||
| Feld | Element | Beschreibung |
|
||||
|------|---------|--------------|
|
||||
| `auftragsbeschreibung` | Auftrag | Zusätzliche Beschreibung für den Auftrag |
|
||||
| `stundenzettel_status` | Auftrag | Status der Stundenzettel (0=Offen, 1=Freigegeben, 2=Abgerechnet) |
|
||||
| `stundenzettel_default_service` | Kunde | Standard-Dienstleistung für Stundenzettel |
|
||||
|
||||
## Berechtigungen
|
||||
|
||||
| Berechtigung | Beschreibung |
|
||||
|--------------|--------------|
|
||||
| Lesen | Stundenzettel anzeigen |
|
||||
| Erstellen/Bearbeiten | Stundenzettel erstellen und bearbeiten |
|
||||
| Freigeben | Stundenzettel freigeben/sperren |
|
||||
| Löschen | Stundenzettel löschen |
|
||||
|
||||
## Workflow
|
||||
|
||||
1. **Stundenzettel erstellen**: Über Auftrag > Stundenzettel-Tab
|
||||
2. **Leistungen erfassen**: Arbeitszeiten mit Start-/Endzeit dokumentieren
|
||||
3. **Produkte dokumentieren**: Verbrauchte Materialien aus dem Auftrag erfassen
|
||||
4. **Mehraufwand hinzufügen**: Zusätzliche Produkte bei Bedarf
|
||||
5. **Notizen erstellen**: Merkzettel für nächsten Termin
|
||||
6. **Stundenzettel freigeben**: Nach Fertigstellung sperren
|
||||
7. **In Rechnung übernehmen**: Automatische Rechnungserstellung
|
||||
|
||||
## Changelog
|
||||
|
||||
### Version 1.1.0
|
||||
- Standard-Leistung beim Kunden hinterlegen (nur Dienstleistungen auswählbar)
|
||||
- Stunden-Übernahme Modus (Gesamt oder pro Tag)
|
||||
- Zeitüberlappungsprüfung für Leistungen
|
||||
- Verbessertes Setup-Seitenlayout
|
||||
- Datenbank-View für Dienstleistungs-Filter
|
||||
|
||||
### Version 1.0.0
|
||||
- Initiale Version
|
||||
- Grundlegende Stundenzettel-Verwaltung
|
||||
- Leistungs- und Produkterfassung
|
||||
- Mehraufwand und Entfällt-Funktionen
|
||||
- Notizen-System
|
||||
- Freigabe und Rechnungsübernahme
|
||||
- SubtotalTitle-Integration
|
||||
|
||||
## Support
|
||||
|
||||
Bei Fragen oder Problemen wenden Sie sich an:
|
||||
**Data IT Solution**
|
||||
E-Mail: data@data-it-solution.de
|
||||
Web: https://data-it-solution.de
|
||||
|
||||
## Lizenz
|
||||
|
||||
Dieses Modul steht unter der GNU General Public License v3. Siehe LICENSE-Datei für Details.
|
||||
184
admin/setup.php
Normal file
184
admin/setup.php
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
<?php
|
||||
/* Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
|
||||
*
|
||||
* Stundenzettel - Modulkonfiguration
|
||||
*/
|
||||
|
||||
// Load Dolibarr environment
|
||||
$res = 0;
|
||||
// Von /custom/stundenzettel/admin/ zu /main.inc.php = 3 Ebenen hoch
|
||||
if (!$res && file_exists("../../../main.inc.php")) $res = @include "../../../main.inc.php";
|
||||
if (!$res && file_exists("../../../../main.inc.php")) $res = @include "../../../../main.inc.php";
|
||||
if (!$res) die("Include of main fails");
|
||||
|
||||
require_once DOL_DOCUMENT_ROOT.'/core/lib/admin.lib.php';
|
||||
|
||||
// Load translation files
|
||||
$langs->loadLangs(array("admin", "stundenzettel@stundenzettel"));
|
||||
|
||||
// Access control
|
||||
if (!$user->admin) {
|
||||
accessforbidden();
|
||||
}
|
||||
|
||||
$action = GETPOST('action', 'aZ09');
|
||||
|
||||
/*
|
||||
* Actions
|
||||
*/
|
||||
|
||||
if ($action == 'setTIME_INPUT_MODE') {
|
||||
$value = GETPOST('value', 'alpha');
|
||||
if (dolibarr_set_const($db, 'STUNDENZETTEL_TIME_INPUT_MODE', $value, 'chaine', 0, '', $conf->entity) > 0) {
|
||||
setEventMessages($langs->trans("SetupSaved"), null, 'mesgs');
|
||||
} else {
|
||||
setEventMessages($langs->trans("Error"), null, 'errors');
|
||||
}
|
||||
header("Location: ".$_SERVER["PHP_SELF"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($action == 'setDEFAULT_FILTER') {
|
||||
$value = GETPOST('filter_value', 'alpha');
|
||||
if (dolibarr_set_const($db, 'STUNDENZETTEL_DEFAULT_FILTER', $value, 'chaine', 0, '', $conf->entity) > 0) {
|
||||
setEventMessages($langs->trans("SetupSaved"), null, 'mesgs');
|
||||
} else {
|
||||
setEventMessages($langs->trans("Error"), null, 'errors');
|
||||
}
|
||||
header("Location: ".$_SERVER["PHP_SELF"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($action == 'setDEFAULT_DATE') {
|
||||
$value = GETPOST('date_value', 'alpha');
|
||||
if (dolibarr_set_const($db, 'STUNDENZETTEL_DEFAULT_DATE', $value, 'chaine', 0, '', $conf->entity) > 0) {
|
||||
setEventMessages($langs->trans("SetupSaved"), null, 'mesgs');
|
||||
} else {
|
||||
setEventMessages($langs->trans("Error"), null, 'errors');
|
||||
}
|
||||
header("Location: ".$_SERVER["PHP_SELF"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($action == 'setINVOICE_HOURS_MODE') {
|
||||
$value = GETPOST('hours_mode_value', 'alpha');
|
||||
if (dolibarr_set_const($db, 'STUNDENZETTEL_INVOICE_HOURS_MODE', $value, 'chaine', 0, '', $conf->entity) > 0) {
|
||||
setEventMessages($langs->trans("SetupSaved"), null, 'mesgs');
|
||||
} else {
|
||||
setEventMessages($langs->trans("Error"), null, 'errors');
|
||||
}
|
||||
header("Location: ".$_SERVER["PHP_SELF"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
/*
|
||||
* View
|
||||
*/
|
||||
|
||||
$page_name = "StundenzettelSetup";
|
||||
llxHeader('', $langs->trans($page_name), '');
|
||||
|
||||
// Subheader
|
||||
$linkback = '<a href="'.DOL_URL_ROOT.'/admin/modules.php?restore_lastsearch_values=1">'.$langs->trans("BackToModuleList").'</a>';
|
||||
print load_fiche_titre($langs->trans($page_name), $linkback, 'title_setup');
|
||||
|
||||
// Configuration header
|
||||
$head = array();
|
||||
$h = 0;
|
||||
$head[$h][0] = $_SERVER["PHP_SELF"];
|
||||
$head[$h][1] = $langs->trans("Settings");
|
||||
$head[$h][2] = 'settings';
|
||||
$h++;
|
||||
|
||||
print dol_get_fiche_head($head, 'settings', $langs->trans("Stundenzettel"), -1, 'clock');
|
||||
|
||||
print '<table class="noborder centpercent">';
|
||||
print '<tr class="liste_titre">';
|
||||
print '<th class="titlefield">'.$langs->trans("Parameter").'</th>';
|
||||
print '<th style="width: 300px;">'.$langs->trans("Value").'</th>';
|
||||
print '<th class="right" style="width: 100px;"></th>';
|
||||
print '</tr>';
|
||||
|
||||
// Zeiteingabe-Modus
|
||||
$currentMode = getDolGlobalString('STUNDENZETTEL_TIME_INPUT_MODE', 'dropdown');
|
||||
print '<tr class="oddeven">';
|
||||
print '<td>'.$langs->trans("TimeInputMode").'</td>';
|
||||
print '<td>';
|
||||
print '<form method="POST" action="'.$_SERVER["PHP_SELF"].'" style="display: inline;">';
|
||||
print '<input type="hidden" name="token" value="'.newToken().'">';
|
||||
print '<input type="hidden" name="action" value="setTIME_INPUT_MODE">';
|
||||
print '<select name="value" class="flat minwidth200">';
|
||||
print '<option value="dropdown"'.($currentMode == 'dropdown' ? ' selected' : '').'>'.$langs->trans("TimeInputDropdown").'</option>';
|
||||
print '<option value="text"'.($currentMode == 'text' ? ' selected' : '').'>'.$langs->trans("TimeInputText").'</option>';
|
||||
print '</select>';
|
||||
print '</td>';
|
||||
print '<td class="right">';
|
||||
print '<input type="submit" class="button small" value="'.$langs->trans("Modify").'">';
|
||||
print '</form>';
|
||||
print '</td>';
|
||||
print '</tr>';
|
||||
|
||||
// Standard-Filter
|
||||
$currentFilter = getDolGlobalString('STUNDENZETTEL_DEFAULT_FILTER', 'open');
|
||||
print '<tr class="oddeven">';
|
||||
print '<td>'.$langs->trans("DefaultFilter").'<br><small class="opacitymedium">'.$langs->trans("DefaultFilterDesc").'</small></td>';
|
||||
print '<td>';
|
||||
print '<form method="POST" action="'.$_SERVER["PHP_SELF"].'" style="display: inline;">';
|
||||
print '<input type="hidden" name="token" value="'.newToken().'">';
|
||||
print '<input type="hidden" name="action" value="setDEFAULT_FILTER">';
|
||||
print '<select name="filter_value" class="flat minwidth200">';
|
||||
print '<option value="open"'.($currentFilter == 'open' ? ' selected' : '').'>'.$langs->trans("FilterOpen").'</option>';
|
||||
print '<option value="done"'.($currentFilter == 'done' ? ' selected' : '').'>'.$langs->trans("FilterDone").'</option>';
|
||||
print '<option value="all"'.($currentFilter == 'all' ? ' selected' : '').'>'.$langs->trans("FilterAll").'</option>';
|
||||
print '</select>';
|
||||
print '</td>';
|
||||
print '<td class="right">';
|
||||
print '<input type="submit" class="button small" value="'.$langs->trans("Modify").'">';
|
||||
print '</form>';
|
||||
print '</td>';
|
||||
print '</tr>';
|
||||
|
||||
// Standard-Datum für Stundenzettel
|
||||
$currentDate = getDolGlobalString('STUNDENZETTEL_DEFAULT_DATE', 'today');
|
||||
print '<tr class="oddeven">';
|
||||
print '<td>'.$langs->trans("DefaultDate").'<br><small class="opacitymedium">'.$langs->trans("DefaultDateDesc").'</small></td>';
|
||||
print '<td>';
|
||||
print '<form method="POST" action="'.$_SERVER["PHP_SELF"].'" style="display: inline;">';
|
||||
print '<input type="hidden" name="token" value="'.newToken().'">';
|
||||
print '<input type="hidden" name="action" value="setDEFAULT_DATE">';
|
||||
print '<select name="date_value" class="flat minwidth200">';
|
||||
print '<option value="today"'.($currentDate == 'today' ? ' selected' : '').'>'.$langs->trans("DateToday").'</option>';
|
||||
print '<option value="last_open"'.($currentDate == 'last_open' ? ' selected' : '').'>'.$langs->trans("DateLastOpen").'</option>';
|
||||
print '</select>';
|
||||
print '</td>';
|
||||
print '<td class="right">';
|
||||
print '<input type="submit" class="button small" value="'.$langs->trans("Modify").'">';
|
||||
print '</form>';
|
||||
print '</td>';
|
||||
print '</tr>';
|
||||
|
||||
// Stunden-Übernahme Modus
|
||||
$currentHoursMode = getDolGlobalString('STUNDENZETTEL_INVOICE_HOURS_MODE', 'total');
|
||||
print '<tr class="oddeven">';
|
||||
print '<td>'.$langs->trans("InvoiceHoursMode").'<br><small class="opacitymedium">'.$langs->trans("SelectInvoiceHoursMode").'</small></td>';
|
||||
print '<td>';
|
||||
print '<form method="POST" action="'.$_SERVER["PHP_SELF"].'" style="display: inline;">';
|
||||
print '<input type="hidden" name="token" value="'.newToken().'">';
|
||||
print '<input type="hidden" name="action" value="setINVOICE_HOURS_MODE">';
|
||||
print '<select name="hours_mode_value" class="flat minwidth200">';
|
||||
print '<option value="total"'.($currentHoursMode == 'total' ? ' selected' : '').'>'.$langs->trans("InvoiceHoursModeTotal").'</option>';
|
||||
print '<option value="perday"'.($currentHoursMode == 'perday' ? ' selected' : '').'>'.$langs->trans("InvoiceHoursModePerDay").'</option>';
|
||||
print '</select>';
|
||||
print '</td>';
|
||||
print '<td class="right">';
|
||||
print '<input type="submit" class="button small" value="'.$langs->trans("Modify").'">';
|
||||
print '</form>';
|
||||
print '</td>';
|
||||
print '</tr>';
|
||||
|
||||
print '</table>';
|
||||
|
||||
print dol_get_fiche_end();
|
||||
|
||||
llxFooter();
|
||||
$db->close();
|
||||
61
ajax/add_leistung.php
Normal file
61
ajax/add_leistung.php
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
<?php
|
||||
/* Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
|
||||
*
|
||||
* AJAX: Leistung hinzufügen
|
||||
*/
|
||||
|
||||
if (!defined('NOTOKENRENEWAL')) define('NOTOKENRENEWAL', '1');
|
||||
if (!defined('NOREQUIREMENU')) define('NOREQUIREMENU', '1');
|
||||
if (!defined('NOREQUIREHTML')) define('NOREQUIREHTML', '1');
|
||||
if (!defined('NOREQUIREAJAX')) define('NOREQUIREAJAX', '1');
|
||||
|
||||
// Load Dolibarr environment
|
||||
$res = 0;
|
||||
if (!$res && file_exists("../../main.inc.php")) $res = @include "../../main.inc.php";
|
||||
if (!$res && file_exists("../../../main.inc.php")) $res = @include "../../../main.inc.php";
|
||||
if (!$res && file_exists("../../../../main.inc.php")) $res = @include "../../../../main.inc.php";
|
||||
if (!$res) die("Include of main fails");
|
||||
|
||||
dol_include_once('/stundenzettel/class/stundenzettel.class.php');
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
// Security check
|
||||
if (!$user->hasRight('stundenzettel', 'write')) {
|
||||
echo json_encode(array('success' => false, 'error' => 'Permission denied'));
|
||||
exit;
|
||||
}
|
||||
|
||||
$stundenzettel_id = GETPOST('stundenzettel_id', 'int');
|
||||
$date = GETPOST('date', 'alpha');
|
||||
$time_start = GETPOST('time_start', 'alpha');
|
||||
$time_end = GETPOST('time_end', 'alpha');
|
||||
$description = GETPOST('description', 'restricthtml');
|
||||
|
||||
if (empty($stundenzettel_id)) {
|
||||
echo json_encode(array('success' => false, 'error' => 'Missing stundenzettel_id'));
|
||||
exit;
|
||||
}
|
||||
|
||||
$stundenzettel = new Stundenzettel($db);
|
||||
if ($stundenzettel->fetch($stundenzettel_id) <= 0) {
|
||||
echo json_encode(array('success' => false, 'error' => 'Stundenzettel not found'));
|
||||
exit;
|
||||
}
|
||||
|
||||
// Nur im Entwurf bearbeitbar
|
||||
if ($stundenzettel->status != Stundenzettel::STATUS_DRAFT) {
|
||||
echo json_encode(array('success' => false, 'error' => 'Stundenzettel is not in draft status'));
|
||||
exit;
|
||||
}
|
||||
|
||||
$result = $stundenzettel->addLeistung($user, $date, $time_start, $time_end, $description);
|
||||
|
||||
if ($result > 0) {
|
||||
echo json_encode(array(
|
||||
'success' => true,
|
||||
'leistung_id' => $result
|
||||
));
|
||||
} else {
|
||||
echo json_encode(array('success' => false, 'error' => 'Failed to add leistung'));
|
||||
}
|
||||
61
ajax/add_product.php
Normal file
61
ajax/add_product.php
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
<?php
|
||||
/* Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
|
||||
*
|
||||
* AJAX: Produkt hinzufügen
|
||||
*/
|
||||
|
||||
if (!defined('NOTOKENRENEWAL')) define('NOTOKENRENEWAL', '1');
|
||||
if (!defined('NOREQUIREMENU')) define('NOREQUIREMENU', '1');
|
||||
if (!defined('NOREQUIREHTML')) define('NOREQUIREHTML', '1');
|
||||
if (!defined('NOREQUIREAJAX')) define('NOREQUIREAJAX', '1');
|
||||
|
||||
// Load Dolibarr environment
|
||||
$res = 0;
|
||||
if (!$res && file_exists("../../main.inc.php")) $res = @include "../../main.inc.php";
|
||||
if (!$res && file_exists("../../../main.inc.php")) $res = @include "../../../main.inc.php";
|
||||
if (!$res && file_exists("../../../../main.inc.php")) $res = @include "../../../../main.inc.php";
|
||||
if (!$res) die("Include of main fails");
|
||||
|
||||
dol_include_once('/stundenzettel/class/stundenzettel.class.php');
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
// Security check
|
||||
if (!$user->hasRight('stundenzettel', 'write')) {
|
||||
echo json_encode(array('success' => false, 'error' => 'Permission denied'));
|
||||
exit;
|
||||
}
|
||||
|
||||
$stundenzettel_id = GETPOST('stundenzettel_id', 'int');
|
||||
$fk_product = GETPOST('fk_product', 'int');
|
||||
$qty = GETPOST('qty', 'int');
|
||||
$label = GETPOST('label', 'alpha');
|
||||
$description = GETPOST('description', 'restricthtml');
|
||||
|
||||
if (empty($stundenzettel_id)) {
|
||||
echo json_encode(array('success' => false, 'error' => 'Missing stundenzettel_id'));
|
||||
exit;
|
||||
}
|
||||
|
||||
$stundenzettel = new Stundenzettel($db);
|
||||
if ($stundenzettel->fetch($stundenzettel_id) <= 0) {
|
||||
echo json_encode(array('success' => false, 'error' => 'Stundenzettel not found'));
|
||||
exit;
|
||||
}
|
||||
|
||||
// Nur im Entwurf bearbeitbar
|
||||
if ($stundenzettel->status != Stundenzettel::STATUS_DRAFT) {
|
||||
echo json_encode(array('success' => false, 'error' => 'Stundenzettel is not in draft status'));
|
||||
exit;
|
||||
}
|
||||
|
||||
$result = $stundenzettel->addProduct($fk_product, null, null, $qty, 0, 'added', $description);
|
||||
|
||||
if ($result > 0) {
|
||||
echo json_encode(array(
|
||||
'success' => true,
|
||||
'product_line_id' => $result
|
||||
));
|
||||
} else {
|
||||
echo json_encode(array('success' => false, 'error' => 'Failed to add product'));
|
||||
}
|
||||
59
ajax/update_qty.php
Normal file
59
ajax/update_qty.php
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
<?php
|
||||
/* Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
|
||||
*
|
||||
* AJAX: Produktmenge aktualisieren
|
||||
*/
|
||||
|
||||
if (!defined('NOTOKENRENEWAL')) define('NOTOKENRENEWAL', '1');
|
||||
if (!defined('NOREQUIREMENU')) define('NOREQUIREMENU', '1');
|
||||
if (!defined('NOREQUIREHTML')) define('NOREQUIREHTML', '1');
|
||||
if (!defined('NOREQUIREAJAX')) define('NOREQUIREAJAX', '1');
|
||||
|
||||
// Load Dolibarr environment
|
||||
$res = 0;
|
||||
if (!$res && file_exists("../../main.inc.php")) $res = @include "../../main.inc.php";
|
||||
if (!$res && file_exists("../../../main.inc.php")) $res = @include "../../../main.inc.php";
|
||||
if (!$res && file_exists("../../../../main.inc.php")) $res = @include "../../../../main.inc.php";
|
||||
if (!$res) die("Include of main fails");
|
||||
|
||||
dol_include_once('/stundenzettel/class/stundenzettel.class.php');
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
// Security check
|
||||
if (!$user->hasRight('stundenzettel', 'write')) {
|
||||
echo json_encode(array('success' => false, 'error' => 'Permission denied'));
|
||||
exit;
|
||||
}
|
||||
|
||||
$stundenzettel_id = GETPOST('stundenzettel_id', 'int');
|
||||
$line_id = GETPOST('line_id', 'int');
|
||||
$qty_done = GETPOST('qty_done', 'int');
|
||||
|
||||
if (empty($stundenzettel_id) || empty($line_id)) {
|
||||
echo json_encode(array('success' => false, 'error' => 'Missing parameters'));
|
||||
exit;
|
||||
}
|
||||
|
||||
$stundenzettel = new Stundenzettel($db);
|
||||
if ($stundenzettel->fetch($stundenzettel_id) <= 0) {
|
||||
echo json_encode(array('success' => false, 'error' => 'Stundenzettel not found'));
|
||||
exit;
|
||||
}
|
||||
|
||||
// Nur im Entwurf bearbeitbar
|
||||
if ($stundenzettel->status != Stundenzettel::STATUS_DRAFT) {
|
||||
echo json_encode(array('success' => false, 'error' => 'Stundenzettel is not in draft status'));
|
||||
exit;
|
||||
}
|
||||
|
||||
$result = $stundenzettel->updateProductQty($line_id, $qty_done);
|
||||
|
||||
if ($result > 0) {
|
||||
echo json_encode(array(
|
||||
'success' => true,
|
||||
'qty_done' => $qty_done
|
||||
));
|
||||
} else {
|
||||
echo json_encode(array('success' => false, 'error' => 'Failed to update qty'));
|
||||
}
|
||||
954
class/stundenzettel.class.php
Normal file
954
class/stundenzettel.class.php
Normal file
|
|
@ -0,0 +1,954 @@
|
|||
<?php
|
||||
/* Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
|
||||
*
|
||||
* Stundenzettel - Business Object Klasse
|
||||
*/
|
||||
|
||||
require_once DOL_DOCUMENT_ROOT.'/core/class/commonobject.class.php';
|
||||
|
||||
/**
|
||||
* Class Stundenzettel
|
||||
*/
|
||||
class Stundenzettel extends CommonObject
|
||||
{
|
||||
/**
|
||||
* @var string ID to identify managed object
|
||||
*/
|
||||
public $element = 'stundenzettel';
|
||||
|
||||
/**
|
||||
* @var string Name of table without prefix
|
||||
*/
|
||||
public $table_element = 'stundenzettel';
|
||||
|
||||
/**
|
||||
* @var string Picto
|
||||
*/
|
||||
public $picto = 'clock';
|
||||
|
||||
// Status constants
|
||||
const STATUS_DRAFT = 0;
|
||||
const STATUS_VALIDATED = 1;
|
||||
const STATUS_INVOICED = 2;
|
||||
const STATUS_CANCELED = 9;
|
||||
|
||||
/**
|
||||
* @var string Ref
|
||||
*/
|
||||
public $ref;
|
||||
|
||||
/**
|
||||
* @var int Auftrag ID
|
||||
*/
|
||||
public $fk_commande;
|
||||
|
||||
/**
|
||||
* @var int Rechnung ID (nach Übertrag)
|
||||
*/
|
||||
public $fk_facture;
|
||||
|
||||
/**
|
||||
* @var int Kunde ID
|
||||
*/
|
||||
public $fk_soc;
|
||||
|
||||
/**
|
||||
* @var int Ersteller
|
||||
*/
|
||||
public $fk_user_author;
|
||||
|
||||
/**
|
||||
* @var int Freigebender User
|
||||
*/
|
||||
public $fk_user_valid;
|
||||
|
||||
/**
|
||||
* @var int|string Datum des Stundenzettels
|
||||
*/
|
||||
public $date_stundenzettel;
|
||||
|
||||
/**
|
||||
* @var int|string Erstelldatum
|
||||
*/
|
||||
public $datec;
|
||||
|
||||
/**
|
||||
* @var int|string Freigabedatum
|
||||
*/
|
||||
public $date_valid;
|
||||
|
||||
/**
|
||||
* @var int Status
|
||||
*/
|
||||
public $status;
|
||||
|
||||
/**
|
||||
* @var string Private Notizen
|
||||
*/
|
||||
public $note_private;
|
||||
|
||||
/**
|
||||
* @var string Öffentliche Notizen
|
||||
*/
|
||||
public $note_public;
|
||||
|
||||
/**
|
||||
* @var array Leistungen
|
||||
*/
|
||||
public $leistungen = array();
|
||||
|
||||
/**
|
||||
* @var array Produkte
|
||||
*/
|
||||
public $products = array();
|
||||
|
||||
/**
|
||||
* @var array Notizen (abhakbare Merkzettel)
|
||||
*/
|
||||
public $notes = array();
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param DoliDB $db Database handler
|
||||
*/
|
||||
public function __construct($db)
|
||||
{
|
||||
$this->db = $db;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create object into database
|
||||
*
|
||||
* @param User $user User that creates
|
||||
* @param bool $notrigger false=launch triggers after, true=disable triggers
|
||||
* @return int <0 if KO, Id of created object if OK
|
||||
*/
|
||||
public function create($user, $notrigger = false)
|
||||
{
|
||||
global $conf;
|
||||
|
||||
$error = 0;
|
||||
|
||||
// Generate ref
|
||||
if (empty($this->ref)) {
|
||||
$this->ref = $this->getNextNumRef();
|
||||
}
|
||||
|
||||
$this->db->begin();
|
||||
|
||||
$sql = "INSERT INTO ".MAIN_DB_PREFIX.$this->table_element." (";
|
||||
$sql .= "ref, entity, fk_commande, fk_soc, fk_user_author,";
|
||||
$sql .= "date_stundenzettel, datec, status, note_private, note_public";
|
||||
$sql .= ") VALUES (";
|
||||
$sql .= "'".$this->db->escape($this->ref)."',";
|
||||
$sql .= ((int)$conf->entity).",";
|
||||
$sql .= ((int)$this->fk_commande).",";
|
||||
$sql .= ((int)$this->fk_soc).",";
|
||||
$sql .= ((int)$user->id).",";
|
||||
$sql .= "'".$this->db->idate($this->date_stundenzettel)."',";
|
||||
$sql .= "'".$this->db->idate(dol_now())."',";
|
||||
$sql .= "0,"; // STATUS_DRAFT
|
||||
$sql .= ($this->note_private ? "'".$this->db->escape($this->note_private)."'" : "NULL").",";
|
||||
$sql .= ($this->note_public ? "'".$this->db->escape($this->note_public)."'" : "NULL");
|
||||
$sql .= ")";
|
||||
|
||||
dol_syslog(get_class($this)."::create", LOG_DEBUG);
|
||||
$resql = $this->db->query($sql);
|
||||
if (!$resql) {
|
||||
$error++;
|
||||
$this->errors[] = "Error ".$this->db->lasterror();
|
||||
}
|
||||
|
||||
if (!$error) {
|
||||
$this->id = $this->db->last_insert_id(MAIN_DB_PREFIX.$this->table_element);
|
||||
}
|
||||
|
||||
if (!$error) {
|
||||
$this->db->commit();
|
||||
return $this->id;
|
||||
} else {
|
||||
$this->db->rollback();
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load object in memory from the database
|
||||
*
|
||||
* @param int $id Id object
|
||||
* @param string $ref Ref
|
||||
* @return int <0 if KO, 0 if not found, >0 if OK
|
||||
*/
|
||||
public function fetch($id, $ref = null)
|
||||
{
|
||||
$sql = "SELECT s.rowid, s.ref, s.entity, s.fk_commande, s.fk_facture, s.fk_soc,";
|
||||
$sql .= " s.fk_user_author, s.fk_user_valid, s.date_stundenzettel, s.datec,";
|
||||
$sql .= " s.date_valid, s.status, s.note_private, s.note_public, s.tms";
|
||||
$sql .= " FROM ".MAIN_DB_PREFIX.$this->table_element." as s";
|
||||
if ($ref) {
|
||||
$sql .= " WHERE s.ref = '".$this->db->escape($ref)."'";
|
||||
} else {
|
||||
$sql .= " WHERE s.rowid = ".((int)$id);
|
||||
}
|
||||
|
||||
dol_syslog(get_class($this)."::fetch", LOG_DEBUG);
|
||||
$resql = $this->db->query($sql);
|
||||
if ($resql) {
|
||||
if ($this->db->num_rows($resql)) {
|
||||
$obj = $this->db->fetch_object($resql);
|
||||
|
||||
$this->id = $obj->rowid;
|
||||
$this->ref = $obj->ref;
|
||||
$this->entity = $obj->entity;
|
||||
$this->fk_commande = $obj->fk_commande;
|
||||
$this->fk_facture = $obj->fk_facture;
|
||||
$this->fk_soc = $obj->fk_soc;
|
||||
$this->fk_user_author = $obj->fk_user_author;
|
||||
$this->fk_user_valid = $obj->fk_user_valid;
|
||||
$this->date_stundenzettel = $this->db->jdate($obj->date_stundenzettel);
|
||||
$this->datec = $this->db->jdate($obj->datec);
|
||||
$this->date_valid = $this->db->jdate($obj->date_valid);
|
||||
$this->status = $obj->status;
|
||||
$this->note_private = $obj->note_private;
|
||||
$this->note_public = $obj->note_public;
|
||||
|
||||
// Load lines
|
||||
$this->fetchLeistungen();
|
||||
$this->fetchProducts();
|
||||
$this->fetchNotes();
|
||||
|
||||
return 1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
} else {
|
||||
$this->error = "Error ".$this->db->lasterror();
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update object into database
|
||||
*
|
||||
* @param User $user User that modifies
|
||||
* @param bool $notrigger false=launch triggers after, true=disable triggers
|
||||
* @return int <0 if KO, >0 if OK
|
||||
*/
|
||||
public function update($user, $notrigger = false)
|
||||
{
|
||||
$error = 0;
|
||||
|
||||
$sql = "UPDATE ".MAIN_DB_PREFIX.$this->table_element." SET";
|
||||
$sql .= " date_stundenzettel = '".$this->db->idate($this->date_stundenzettel)."',";
|
||||
$sql .= " note_private = ".($this->note_private ? "'".$this->db->escape($this->note_private)."'" : "NULL").",";
|
||||
$sql .= " note_public = ".($this->note_public ? "'".$this->db->escape($this->note_public)."'" : "NULL");
|
||||
$sql .= " WHERE rowid = ".((int)$this->id);
|
||||
|
||||
dol_syslog(get_class($this)."::update", LOG_DEBUG);
|
||||
$resql = $this->db->query($sql);
|
||||
if (!$resql) {
|
||||
$error++;
|
||||
$this->errors[] = "Error ".$this->db->lasterror();
|
||||
}
|
||||
|
||||
if (!$error) {
|
||||
return 1;
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate object
|
||||
*
|
||||
* @param User $user User that validates
|
||||
* @return int <0 if KO, >0 if OK
|
||||
*/
|
||||
public function validate($user)
|
||||
{
|
||||
$error = 0;
|
||||
|
||||
$this->db->begin();
|
||||
|
||||
$sql = "UPDATE ".MAIN_DB_PREFIX.$this->table_element." SET";
|
||||
$sql .= " status = ".self::STATUS_VALIDATED.",";
|
||||
$sql .= " fk_user_valid = ".((int)$user->id).",";
|
||||
$sql .= " date_valid = '".$this->db->idate(dol_now())."'";
|
||||
$sql .= " WHERE rowid = ".((int)$this->id);
|
||||
|
||||
dol_syslog(get_class($this)."::validate", LOG_DEBUG);
|
||||
$resql = $this->db->query($sql);
|
||||
if (!$resql) {
|
||||
$error++;
|
||||
$this->errors[] = "Error ".$this->db->lasterror();
|
||||
}
|
||||
|
||||
if (!$error) {
|
||||
$this->status = self::STATUS_VALIDATED;
|
||||
$this->fk_user_valid = $user->id;
|
||||
$this->date_valid = dol_now();
|
||||
|
||||
// Update tracking table
|
||||
$this->updateTracking();
|
||||
|
||||
$this->db->commit();
|
||||
return 1;
|
||||
} else {
|
||||
$this->db->rollback();
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set to draft
|
||||
*
|
||||
* @param User $user User that reopens
|
||||
* @return int <0 if KO, >0 if OK
|
||||
*/
|
||||
public function setDraft($user)
|
||||
{
|
||||
$sql = "UPDATE ".MAIN_DB_PREFIX.$this->table_element." SET";
|
||||
$sql .= " status = ".self::STATUS_DRAFT.",";
|
||||
$sql .= " fk_user_valid = NULL,";
|
||||
$sql .= " date_valid = NULL";
|
||||
$sql .= " WHERE rowid = ".((int)$this->id);
|
||||
|
||||
dol_syslog(get_class($this)."::setDraft", LOG_DEBUG);
|
||||
$resql = $this->db->query($sql);
|
||||
if ($resql) {
|
||||
$this->status = self::STATUS_DRAFT;
|
||||
return 1;
|
||||
} else {
|
||||
$this->error = "Error ".$this->db->lasterror();
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete object in database
|
||||
*
|
||||
* @param User $user User that deletes
|
||||
* @param bool $notrigger false=launch triggers after, true=disable triggers
|
||||
* @return int <0 if KO, >0 if OK
|
||||
*/
|
||||
public function delete($user, $notrigger = false)
|
||||
{
|
||||
$error = 0;
|
||||
|
||||
$this->db->begin();
|
||||
|
||||
// Delete notes
|
||||
$sql = "DELETE FROM ".MAIN_DB_PREFIX."stundenzettel_note WHERE fk_stundenzettel = ".((int)$this->id);
|
||||
$resql = $this->db->query($sql);
|
||||
if (!$resql) $error++;
|
||||
|
||||
// Delete leistungen
|
||||
$sql = "DELETE FROM ".MAIN_DB_PREFIX."stundenzettel_leistung WHERE fk_stundenzettel = ".((int)$this->id);
|
||||
$resql = $this->db->query($sql);
|
||||
if (!$resql) $error++;
|
||||
|
||||
// Delete products
|
||||
$sql = "DELETE FROM ".MAIN_DB_PREFIX."stundenzettel_product WHERE fk_stundenzettel = ".((int)$this->id);
|
||||
$resql = $this->db->query($sql);
|
||||
if (!$resql) $error++;
|
||||
|
||||
// Delete main record
|
||||
if (!$error) {
|
||||
$sql = "DELETE FROM ".MAIN_DB_PREFIX.$this->table_element." WHERE rowid = ".((int)$this->id);
|
||||
$resql = $this->db->query($sql);
|
||||
if (!$resql) $error++;
|
||||
}
|
||||
|
||||
if (!$error) {
|
||||
$this->db->commit();
|
||||
return 1;
|
||||
} else {
|
||||
$this->db->rollback();
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load leistungen
|
||||
*
|
||||
* @return int <0 if KO, number of lines if OK
|
||||
*/
|
||||
public function fetchLeistungen()
|
||||
{
|
||||
$this->leistungen = array();
|
||||
|
||||
$sql = "SELECT rowid, fk_user, date_leistung, time_start, time_end, duration, description, rang";
|
||||
$sql .= " FROM ".MAIN_DB_PREFIX."stundenzettel_leistung";
|
||||
$sql .= " WHERE fk_stundenzettel = ".((int)$this->id);
|
||||
$sql .= " ORDER BY rang, date_leistung, time_start";
|
||||
|
||||
$resql = $this->db->query($sql);
|
||||
if ($resql) {
|
||||
while ($obj = $this->db->fetch_object($resql)) {
|
||||
$this->leistungen[] = $obj;
|
||||
}
|
||||
return count($this->leistungen);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load products
|
||||
*
|
||||
* @return int <0 if KO, number of lines if OK
|
||||
*/
|
||||
public function fetchProducts()
|
||||
{
|
||||
$this->products = array();
|
||||
|
||||
// Produkte laden, bei Freitext-Produkten Beschreibung aus commandedet holen falls leer
|
||||
$sql = "SELECT sp.rowid, sp.fk_product, sp.fk_commandedet, sp.fk_manager_line,";
|
||||
$sql .= " sp.product_ref, sp.product_label,";
|
||||
$sql .= " CASE WHEN sp.description IS NULL OR sp.description = '' THEN cd.description ELSE sp.description END as description,";
|
||||
$sql .= " sp.qty_original, sp.qty_done, sp.qty_cumulated, sp.origin, sp.rang";
|
||||
$sql .= " FROM ".MAIN_DB_PREFIX."stundenzettel_product as sp";
|
||||
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."commandedet as cd ON cd.rowid = sp.fk_commandedet";
|
||||
$sql .= " WHERE sp.fk_stundenzettel = ".((int)$this->id);
|
||||
$sql .= " ORDER BY sp.rang";
|
||||
|
||||
$resql = $this->db->query($sql);
|
||||
if ($resql) {
|
||||
while ($obj = $this->db->fetch_object($resql)) {
|
||||
$this->products[] = $obj;
|
||||
}
|
||||
return count($this->products);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a leistung
|
||||
*
|
||||
* @param User $user User
|
||||
* @param string $date Date
|
||||
* @param string $time_start Start time
|
||||
* @param string $time_end End time
|
||||
* @param string $description Description
|
||||
* @return int <0 if KO, >0 if OK
|
||||
*/
|
||||
public function addLeistung($user, $date, $time_start = null, $time_end = null, $description = '')
|
||||
{
|
||||
global $langs;
|
||||
|
||||
// Calculate duration
|
||||
$duration = 0;
|
||||
if ($time_start && $time_end) {
|
||||
$start = strtotime($time_start);
|
||||
$end = strtotime($time_end);
|
||||
$duration = ($end - $start) / 60; // in minutes
|
||||
}
|
||||
|
||||
// Überlappungsprüfung: Prüfen ob für diese Zeit bereits eine Leistung existiert
|
||||
if ($time_start && $time_end) {
|
||||
$sqlCheck = "SELECT rowid, time_start, time_end FROM ".MAIN_DB_PREFIX."stundenzettel_leistung";
|
||||
$sqlCheck .= " WHERE fk_stundenzettel = ".((int)$this->id);
|
||||
$sqlCheck .= " AND time_start IS NOT NULL AND time_end IS NOT NULL";
|
||||
$resqlCheck = $this->db->query($sqlCheck);
|
||||
if ($resqlCheck) {
|
||||
$newStart = strtotime($time_start);
|
||||
$newEnd = strtotime($time_end);
|
||||
while ($objCheck = $this->db->fetch_object($resqlCheck)) {
|
||||
$existStart = strtotime($objCheck->time_start);
|
||||
$existEnd = strtotime($objCheck->time_end);
|
||||
// Überlappung: Start1 < End2 UND Start2 < End1
|
||||
if ($newStart < $existEnd && $existStart < $newEnd) {
|
||||
$this->error = $langs->trans("ErrorTimeOverlap", $objCheck->time_start, $objCheck->time_end);
|
||||
return -2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get next rang
|
||||
$sql = "SELECT MAX(rang) as maxrang FROM ".MAIN_DB_PREFIX."stundenzettel_leistung WHERE fk_stundenzettel = ".((int)$this->id);
|
||||
$resql = $this->db->query($sql);
|
||||
$rang = 0;
|
||||
if ($resql) {
|
||||
$obj = $this->db->fetch_object($resql);
|
||||
$rang = $obj->maxrang + 1;
|
||||
}
|
||||
|
||||
$sql = "INSERT INTO ".MAIN_DB_PREFIX."stundenzettel_leistung (";
|
||||
$sql .= "fk_stundenzettel, fk_user, date_leistung, time_start, time_end, duration, description, rang";
|
||||
$sql .= ") VALUES (";
|
||||
$sql .= ((int)$this->id).",";
|
||||
$sql .= ((int)$user->id).",";
|
||||
$sql .= "'".$this->db->idate($date)."',";
|
||||
$sql .= ($time_start ? "'".$this->db->escape($time_start)."'" : "NULL").",";
|
||||
$sql .= ($time_end ? "'".$this->db->escape($time_end)."'" : "NULL").",";
|
||||
$sql .= ((int)$duration).",";
|
||||
$sql .= "'".$this->db->escape($description)."',";
|
||||
$sql .= ((int)$rang);
|
||||
$sql .= ")";
|
||||
|
||||
$resql = $this->db->query($sql);
|
||||
if ($resql) {
|
||||
return $this->db->last_insert_id(MAIN_DB_PREFIX."stundenzettel_leistung");
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a leistung
|
||||
*
|
||||
* @param int $leistung_id Leistung ID
|
||||
* @param string $date Date
|
||||
* @param string $time_start Start time
|
||||
* @param string $time_end End time
|
||||
* @param string $description Description
|
||||
* @return int <0 if KO, >0 if OK
|
||||
*/
|
||||
public function updateLeistung($leistung_id, $date, $time_start = null, $time_end = null, $description = '')
|
||||
{
|
||||
global $langs;
|
||||
|
||||
// Calculate duration
|
||||
$duration = 0;
|
||||
if ($time_start && $time_end) {
|
||||
$start = strtotime($time_start);
|
||||
$end = strtotime($time_end);
|
||||
$duration = ($end - $start) / 60; // in minutes
|
||||
}
|
||||
|
||||
// Überlappungsprüfung: Prüfen ob für diese Zeit bereits eine andere Leistung existiert
|
||||
if ($time_start && $time_end) {
|
||||
$sqlCheck = "SELECT rowid, time_start, time_end FROM ".MAIN_DB_PREFIX."stundenzettel_leistung";
|
||||
$sqlCheck .= " WHERE fk_stundenzettel = ".((int)$this->id);
|
||||
$sqlCheck .= " AND rowid != ".((int)$leistung_id); // Aktuelle Leistung ausschließen
|
||||
$sqlCheck .= " AND time_start IS NOT NULL AND time_end IS NOT NULL";
|
||||
$resqlCheck = $this->db->query($sqlCheck);
|
||||
if ($resqlCheck) {
|
||||
$newStart = strtotime($time_start);
|
||||
$newEnd = strtotime($time_end);
|
||||
while ($objCheck = $this->db->fetch_object($resqlCheck)) {
|
||||
$existStart = strtotime($objCheck->time_start);
|
||||
$existEnd = strtotime($objCheck->time_end);
|
||||
// Überlappung: Start1 < End2 UND Start2 < End1
|
||||
if ($newStart < $existEnd && $existStart < $newEnd) {
|
||||
$this->error = $langs->trans("ErrorTimeOverlap", $objCheck->time_start, $objCheck->time_end);
|
||||
return -2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$sql = "UPDATE ".MAIN_DB_PREFIX."stundenzettel_leistung SET";
|
||||
$sql .= " date_leistung = '".$this->db->idate($date)."',";
|
||||
$sql .= " time_start = ".($time_start ? "'".$this->db->escape($time_start)."'" : "NULL").",";
|
||||
$sql .= " time_end = ".($time_end ? "'".$this->db->escape($time_end)."'" : "NULL").",";
|
||||
$sql .= " duration = ".((int)$duration).",";
|
||||
$sql .= " description = '".$this->db->escape($description)."'";
|
||||
$sql .= " WHERE rowid = ".((int)$leistung_id);
|
||||
$sql .= " AND fk_stundenzettel = ".((int)$this->id);
|
||||
|
||||
$resql = $this->db->query($sql);
|
||||
if ($resql) {
|
||||
return 1;
|
||||
}
|
||||
$this->error = $this->db->lasterror();
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a leistung
|
||||
*
|
||||
* @param int $leistung_id Leistung ID
|
||||
* @return int <0 if KO, >0 if OK
|
||||
*/
|
||||
public function deleteLeistung($leistung_id)
|
||||
{
|
||||
$sql = "DELETE FROM ".MAIN_DB_PREFIX."stundenzettel_leistung";
|
||||
$sql .= " WHERE rowid = ".((int)$leistung_id);
|
||||
$sql .= " AND fk_stundenzettel = ".((int)$this->id);
|
||||
|
||||
$resql = $this->db->query($sql);
|
||||
if ($resql) {
|
||||
return 1;
|
||||
}
|
||||
$this->error = $this->db->lasterror();
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a product
|
||||
*
|
||||
* @param int $fk_product Product ID
|
||||
* @param int $fk_commandedet Commandedet ID
|
||||
* @param int $fk_manager_line Manager line ID
|
||||
* @param float $qty_original Original qty
|
||||
* @param float $qty_done Done qty
|
||||
* @param string $origin Origin (order or added)
|
||||
* @param string $description Description (for free-text products)
|
||||
* @return int <0 if KO, >0 if OK
|
||||
*/
|
||||
public function addProduct($fk_product, $fk_commandedet = null, $fk_manager_line = null, $qty_original = 0, $qty_done = 0, $origin = 'order', $description = '')
|
||||
{
|
||||
global $db;
|
||||
|
||||
// Get product info
|
||||
$product_ref = '';
|
||||
$product_label = '';
|
||||
if ($fk_product > 0) {
|
||||
require_once DOL_DOCUMENT_ROOT.'/product/class/product.class.php';
|
||||
$prod = new Product($this->db);
|
||||
if ($prod->fetch($fk_product) > 0) {
|
||||
$product_ref = $prod->ref;
|
||||
$product_label = $prod->label;
|
||||
}
|
||||
}
|
||||
|
||||
// Get next rang
|
||||
$sql = "SELECT MAX(rang) as maxrang FROM ".MAIN_DB_PREFIX."stundenzettel_product WHERE fk_stundenzettel = ".((int)$this->id);
|
||||
$resql = $this->db->query($sql);
|
||||
$rang = 0;
|
||||
if ($resql) {
|
||||
$obj = $this->db->fetch_object($resql);
|
||||
$rang = $obj->maxrang + 1;
|
||||
}
|
||||
|
||||
$sql = "INSERT INTO ".MAIN_DB_PREFIX."stundenzettel_product (";
|
||||
$sql .= "fk_stundenzettel, fk_product, fk_commandedet, fk_manager_line,";
|
||||
$sql .= "product_ref, product_label, description, qty_original, qty_done, origin, rang";
|
||||
$sql .= ") VALUES (";
|
||||
$sql .= ((int)$this->id).",";
|
||||
$sql .= ($fk_product > 0 ? (int)$fk_product : "NULL").",";
|
||||
$sql .= ($fk_commandedet > 0 ? (int)$fk_commandedet : "NULL").",";
|
||||
$sql .= ($fk_manager_line > 0 ? (int)$fk_manager_line : "NULL").",";
|
||||
$sql .= "'".$this->db->escape($product_ref)."',";
|
||||
$sql .= "'".$this->db->escape($product_label)."',";
|
||||
$sql .= "'".$this->db->escape($description)."',";
|
||||
$sql .= ((float)$qty_original).",";
|
||||
$sql .= ((float)$qty_done).",";
|
||||
$sql .= "'".$this->db->escape($origin)."',";
|
||||
$sql .= ((int)$rang);
|
||||
$sql .= ")";
|
||||
|
||||
$resql = $this->db->query($sql);
|
||||
if ($resql) {
|
||||
return $this->db->last_insert_id(MAIN_DB_PREFIX."stundenzettel_product");
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update product qty_done
|
||||
*
|
||||
* @param int $line_id Line ID
|
||||
* @param float $qty_done Done qty
|
||||
* @return int <0 if KO, >0 if OK
|
||||
*/
|
||||
public function updateProductQty($line_id, $qty_done)
|
||||
{
|
||||
$sql = "UPDATE ".MAIN_DB_PREFIX."stundenzettel_product SET";
|
||||
$sql .= " qty_done = ".((float)$qty_done);
|
||||
$sql .= " WHERE rowid = ".((int)$line_id);
|
||||
$sql .= " AND fk_stundenzettel = ".((int)$this->id);
|
||||
|
||||
$resql = $this->db->query($sql);
|
||||
return $resql ? 1 : -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete product from stundenzettel
|
||||
*
|
||||
* @param int $line_id Line ID
|
||||
* @return int <0 if KO, >0 if OK
|
||||
*/
|
||||
public function deleteProduct($line_id)
|
||||
{
|
||||
$sql = "DELETE FROM ".MAIN_DB_PREFIX."stundenzettel_product";
|
||||
$sql .= " WHERE rowid = ".((int)$line_id);
|
||||
$sql .= " AND fk_stundenzettel = ".((int)$this->id);
|
||||
|
||||
$resql = $this->db->query($sql);
|
||||
return $resql ? 1 : -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update tracking table
|
||||
*
|
||||
* @return int <0 if KO, >0 if OK
|
||||
*/
|
||||
public function updateTracking()
|
||||
{
|
||||
// Sum up all products for this order across all stundenzettels
|
||||
foreach ($this->products as $prod) {
|
||||
if (!$prod->fk_commandedet) continue;
|
||||
|
||||
// Get total done qty for this commandedet
|
||||
$sql = "SELECT SUM(qty_done) as total_done FROM ".MAIN_DB_PREFIX."stundenzettel_product sp";
|
||||
$sql .= " JOIN ".MAIN_DB_PREFIX."stundenzettel s ON s.rowid = sp.fk_stundenzettel";
|
||||
$sql .= " WHERE sp.fk_commandedet = ".((int)$prod->fk_commandedet);
|
||||
$sql .= " AND s.status >= ".self::STATUS_VALIDATED;
|
||||
|
||||
$resql = $this->db->query($sql);
|
||||
$total_done = 0;
|
||||
if ($resql && ($obj = $this->db->fetch_object($resql))) {
|
||||
$total_done = $obj->total_done;
|
||||
}
|
||||
|
||||
// Update or insert tracking
|
||||
$sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."stundenzettel_tracking";
|
||||
$sql .= " WHERE fk_commande = ".((int)$this->fk_commande);
|
||||
$sql .= " AND fk_commandedet = ".((int)$prod->fk_commandedet);
|
||||
|
||||
$resql = $this->db->query($sql);
|
||||
if ($resql && $this->db->num_rows($resql) > 0) {
|
||||
// Update
|
||||
$obj = $this->db->fetch_object($resql);
|
||||
$remaining = $prod->qty_original - $total_done;
|
||||
$status = 'open';
|
||||
if ($total_done >= $prod->qty_original) {
|
||||
$status = 'done';
|
||||
} elseif ($total_done > 0) {
|
||||
$status = 'partial';
|
||||
}
|
||||
|
||||
$sql = "UPDATE ".MAIN_DB_PREFIX."stundenzettel_tracking SET";
|
||||
$sql .= " qty_delivered = ".((float)$total_done).",";
|
||||
$sql .= " qty_remaining = ".((float)$remaining).",";
|
||||
$sql .= " status = '".$this->db->escape($status)."'";
|
||||
$sql .= " WHERE rowid = ".((int)$obj->rowid);
|
||||
$this->db->query($sql);
|
||||
} else {
|
||||
// Insert
|
||||
$sql = "INSERT INTO ".MAIN_DB_PREFIX."stundenzettel_tracking (";
|
||||
$sql .= "fk_commande, fk_product, fk_commandedet, fk_manager_line,";
|
||||
$sql .= "product_ref, product_label, qty_ordered, qty_delivered, qty_remaining, status";
|
||||
$sql .= ") VALUES (";
|
||||
$sql .= ((int)$this->fk_commande).",";
|
||||
$sql .= ($prod->fk_product > 0 ? (int)$prod->fk_product : "NULL").",";
|
||||
$sql .= ((int)$prod->fk_commandedet).",";
|
||||
$sql .= ($prod->fk_manager_line > 0 ? (int)$prod->fk_manager_line : "NULL").",";
|
||||
$sql .= "'".$this->db->escape($prod->product_ref)."',";
|
||||
$sql .= "'".$this->db->escape($prod->product_label)."',";
|
||||
$sql .= ((float)$prod->qty_original).",";
|
||||
$sql .= ((float)$total_done).",";
|
||||
$sql .= ((float)($prod->qty_original - $total_done)).",";
|
||||
$sql .= "'open'";
|
||||
$sql .= ")";
|
||||
$this->db->query($sql);
|
||||
}
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get next reference number
|
||||
*
|
||||
* @return string Next ref
|
||||
*/
|
||||
public function getNextNumRef()
|
||||
{
|
||||
global $conf;
|
||||
|
||||
$prefix = 'SZ';
|
||||
$year = date('Y');
|
||||
|
||||
$sql = "SELECT MAX(CAST(SUBSTRING(ref, ".(strlen($prefix.$year.'-') + 1).") AS UNSIGNED)) as maxnum";
|
||||
$sql .= " FROM ".MAIN_DB_PREFIX.$this->table_element;
|
||||
$sql .= " WHERE ref LIKE '".$this->db->escape($prefix.$year)."-%'";
|
||||
$sql .= " AND entity = ".((int)$conf->entity);
|
||||
|
||||
$resql = $this->db->query($sql);
|
||||
if ($resql) {
|
||||
$obj = $this->db->fetch_object($resql);
|
||||
$num = $obj->maxnum + 1;
|
||||
} else {
|
||||
$num = 1;
|
||||
}
|
||||
|
||||
return $prefix.$year.'-'.sprintf('%05d', $num);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get status label
|
||||
*
|
||||
* @param int $mode 0=long label, 1=short label, 2=Picto + short label, 3=Picto, 4=Picto + long label, 5=Short label + Picto
|
||||
* @return string Label
|
||||
*/
|
||||
public function getLibStatut($mode = 0)
|
||||
{
|
||||
return $this->LibStatut($this->status, $mode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return label of status
|
||||
*
|
||||
* @param int $status Status
|
||||
* @param int $mode Mode
|
||||
* @return string Label
|
||||
*/
|
||||
public function LibStatut($status, $mode = 0)
|
||||
{
|
||||
global $langs;
|
||||
$langs->load("stundenzettel@stundenzettel");
|
||||
|
||||
if ($status == self::STATUS_DRAFT) {
|
||||
$statusType = 'status0';
|
||||
$label = $langs->trans("StatusDraft");
|
||||
} elseif ($status == self::STATUS_VALIDATED) {
|
||||
$statusType = 'status4';
|
||||
$label = $langs->trans("StatusValidated");
|
||||
} elseif ($status == self::STATUS_INVOICED) {
|
||||
$statusType = 'status6';
|
||||
$label = $langs->trans("StatusInvoiced");
|
||||
} elseif ($status == self::STATUS_CANCELED) {
|
||||
$statusType = 'status9';
|
||||
$label = $langs->trans("StatusCanceled");
|
||||
}
|
||||
|
||||
return dolGetStatus($label, '', '', $statusType, $mode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return URL link
|
||||
*
|
||||
* @param int $withpicto Picto
|
||||
* @param string $option Option
|
||||
* @return string Link
|
||||
*/
|
||||
public function getNomUrl($withpicto = 0, $option = '')
|
||||
{
|
||||
$result = '';
|
||||
$url = dol_buildpath('/stundenzettel/card.php?id='.$this->id, 1);
|
||||
|
||||
$label = '<u>'.$this->ref.'</u>';
|
||||
|
||||
if ($withpicto) {
|
||||
$result .= img_object('', $this->picto, 'class="pictofixedwidth"');
|
||||
}
|
||||
$result .= '<a href="'.$url.'" title="'.dol_escape_htmltag($label).'">'.$this->ref.'</a>';
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load notes (checkable memos)
|
||||
*
|
||||
* @return int <0 if KO, number of notes if OK
|
||||
*/
|
||||
public function fetchNotes()
|
||||
{
|
||||
$this->notes = array();
|
||||
|
||||
$sql = "SELECT rowid, fk_user, note, checked, rang, datec";
|
||||
$sql .= " FROM ".MAIN_DB_PREFIX."stundenzettel_note";
|
||||
$sql .= " WHERE fk_stundenzettel = ".((int)$this->id);
|
||||
$sql .= " ORDER BY rang, datec";
|
||||
|
||||
$resql = $this->db->query($sql);
|
||||
if ($resql) {
|
||||
while ($obj = $this->db->fetch_object($resql)) {
|
||||
$this->notes[] = $obj;
|
||||
}
|
||||
return count($this->notes);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a note
|
||||
*
|
||||
* @param User $user User
|
||||
* @param string $note Note text
|
||||
* @return int <0 if KO, >0 if OK (rowid)
|
||||
*/
|
||||
public function addNote($user, $note)
|
||||
{
|
||||
if (empty($note)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Get next rang
|
||||
$sql = "SELECT MAX(rang) as maxrang FROM ".MAIN_DB_PREFIX."stundenzettel_note WHERE fk_stundenzettel = ".((int)$this->id);
|
||||
$resql = $this->db->query($sql);
|
||||
$rang = 0;
|
||||
if ($resql) {
|
||||
$obj = $this->db->fetch_object($resql);
|
||||
$rang = $obj->maxrang + 1;
|
||||
}
|
||||
|
||||
$sql = "INSERT INTO ".MAIN_DB_PREFIX."stundenzettel_note (";
|
||||
$sql .= "fk_stundenzettel, fk_user, note, checked, rang, datec";
|
||||
$sql .= ") VALUES (";
|
||||
$sql .= ((int)$this->id).",";
|
||||
$sql .= ((int)$user->id).",";
|
||||
$sql .= "'".$this->db->escape($note)."',";
|
||||
$sql .= "0,"; // not checked
|
||||
$sql .= ((int)$rang).",";
|
||||
$sql .= "'".$this->db->idate(dol_now())."'";
|
||||
$sql .= ")";
|
||||
|
||||
$resql = $this->db->query($sql);
|
||||
if ($resql) {
|
||||
return $this->db->last_insert_id(MAIN_DB_PREFIX."stundenzettel_note");
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update note checked status
|
||||
*
|
||||
* @param int $note_id Note ID
|
||||
* @param int $checked 0=unchecked, 1=checked
|
||||
* @return int <0 if KO, >0 if OK
|
||||
*/
|
||||
public function updateNoteStatus($note_id, $checked)
|
||||
{
|
||||
$sql = "UPDATE ".MAIN_DB_PREFIX."stundenzettel_note SET";
|
||||
$sql .= " checked = ".((int)$checked);
|
||||
$sql .= " WHERE rowid = ".((int)$note_id);
|
||||
$sql .= " AND fk_stundenzettel = ".((int)$this->id);
|
||||
|
||||
$resql = $this->db->query($sql);
|
||||
return $resql ? 1 : -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a note
|
||||
*
|
||||
* @param int $note_id Note ID
|
||||
* @return int <0 if KO, >0 if OK
|
||||
*/
|
||||
public function deleteNote($note_id)
|
||||
{
|
||||
$sql = "DELETE FROM ".MAIN_DB_PREFIX."stundenzettel_note";
|
||||
$sql .= " WHERE rowid = ".((int)$note_id);
|
||||
$sql .= " AND fk_stundenzettel = ".((int)$this->id);
|
||||
|
||||
$resql = $this->db->query($sql);
|
||||
return $resql ? 1 : -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get unchecked notes (for display on order page)
|
||||
*
|
||||
* @return array Array of unchecked notes
|
||||
*/
|
||||
public function getUncheckedNotes()
|
||||
{
|
||||
$notes = array();
|
||||
|
||||
$sql = "SELECT rowid, fk_user, note, rang, datec";
|
||||
$sql .= " FROM ".MAIN_DB_PREFIX."stundenzettel_note";
|
||||
$sql .= " WHERE fk_stundenzettel = ".((int)$this->id);
|
||||
$sql .= " AND checked = 0";
|
||||
$sql .= " ORDER BY rang, datec";
|
||||
|
||||
$resql = $this->db->query($sql);
|
||||
if ($resql) {
|
||||
while ($obj = $this->db->fetch_object($resql)) {
|
||||
$notes[] = $obj;
|
||||
}
|
||||
}
|
||||
return $notes;
|
||||
}
|
||||
}
|
||||
469
core/modules/modStundenzettel.class.php
Normal file
469
core/modules/modStundenzettel.class.php
Normal file
|
|
@ -0,0 +1,469 @@
|
|||
<?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.1.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__'
|
||||
);
|
||||
|
||||
// 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();
|
||||
|
||||
$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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
239
index.php
Normal file
239
index.php
Normal file
|
|
@ -0,0 +1,239 @@
|
|||
<?php
|
||||
/* Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
|
||||
*
|
||||
* Stundenzettel - Dashboard/Übersicht
|
||||
*/
|
||||
|
||||
// Load Dolibarr environment
|
||||
$res = 0;
|
||||
if (!$res && file_exists("../main.inc.php")) $res = @include "../main.inc.php";
|
||||
if (!$res && file_exists("../../main.inc.php")) $res = @include "../../main.inc.php";
|
||||
if (!$res && file_exists("../../../main.inc.php")) $res = @include "../../../main.inc.php";
|
||||
if (!$res) die("Include of main fails");
|
||||
|
||||
require_once DOL_DOCUMENT_ROOT.'/core/class/html.formfile.class.php';
|
||||
require_once DOL_DOCUMENT_ROOT.'/commande/class/commande.class.php';
|
||||
|
||||
// Load translation files
|
||||
$langs->loadLangs(array("stundenzettel@stundenzettel", "orders"));
|
||||
|
||||
// Security check
|
||||
if (!$user->hasRight('stundenzettel', 'read')) {
|
||||
accessforbidden();
|
||||
}
|
||||
|
||||
$action = GETPOST('action', 'aZ09');
|
||||
|
||||
/*
|
||||
* View
|
||||
*/
|
||||
|
||||
$form = new Form($db);
|
||||
|
||||
// Linkes Menü aktivieren
|
||||
$_GET['mainmenu'] = 'stundenzettel';
|
||||
|
||||
llxHeader('', $langs->trans("Stundenzettel"), '', '', 0, 0, '', '', '', 'mod-stundenzettel page-index');
|
||||
|
||||
print load_fiche_titre($langs->trans("Stundenzettel"), '', 'clock');
|
||||
|
||||
print '<div class="fichecenter">';
|
||||
|
||||
// Statistik-Boxen
|
||||
print '<div class="fichethirdleft">';
|
||||
|
||||
// Box: Offene Stundenzettel
|
||||
print '<div class="div-table-responsive-no-min">';
|
||||
print '<table class="noborder centpercent">';
|
||||
print '<tr class="liste_titre">';
|
||||
print '<th colspan="2">'.$langs->trans("BoxOpenStundenzettel").'</th>';
|
||||
print '</tr>';
|
||||
|
||||
$sql = "SELECT COUNT(*) as nb FROM ".MAIN_DB_PREFIX."stundenzettel WHERE status = 0 AND entity = ".((int)$conf->entity);
|
||||
$resql = $db->query($sql);
|
||||
if ($resql) {
|
||||
$obj = $db->fetch_object($resql);
|
||||
print '<tr class="oddeven"><td>'.$langs->trans("StatusDraft").'</td><td class="right"><span class="badge badge-warning">'.$obj->nb.'</span></td></tr>';
|
||||
}
|
||||
|
||||
$sql = "SELECT COUNT(*) as nb FROM ".MAIN_DB_PREFIX."stundenzettel WHERE status = 1 AND entity = ".((int)$conf->entity);
|
||||
$resql = $db->query($sql);
|
||||
if ($resql) {
|
||||
$obj = $db->fetch_object($resql);
|
||||
print '<tr class="oddeven"><td>'.$langs->trans("StatusValidated").'</td><td class="right"><span class="badge badge-success">'.$obj->nb.'</span></td></tr>';
|
||||
}
|
||||
|
||||
print '</table>';
|
||||
print '</div>';
|
||||
|
||||
// Box: Schnellzugriff
|
||||
print '<br>';
|
||||
print '<div class="div-table-responsive-no-min">';
|
||||
print '<table class="noborder centpercent">';
|
||||
print '<tr class="liste_titre">';
|
||||
print '<th>'.$langs->trans("Actions").'</th>';
|
||||
print '</tr>';
|
||||
print '<tr class="oddeven">';
|
||||
print '<td>';
|
||||
print '<a class="butAction" href="'.dol_buildpath('/stundenzettel/card.php?action=create', 1).'">';
|
||||
print img_picto('', 'add', 'class="pictofixedwidth"').$langs->trans("CreateStundenzettel");
|
||||
print '</a>';
|
||||
print '</td>';
|
||||
print '</tr>';
|
||||
print '<tr class="oddeven">';
|
||||
print '<td>';
|
||||
print '<a class="butAction" href="'.dol_buildpath('/stundenzettel/list.php', 1).'">';
|
||||
print img_picto('', 'list', 'class="pictofixedwidth"').$langs->trans("StundenzettelList");
|
||||
print '</a>';
|
||||
print '</td>';
|
||||
print '</tr>';
|
||||
print '</table>';
|
||||
print '</div>';
|
||||
|
||||
print '</div>'; // fichethirdleft
|
||||
|
||||
print '<div class="fichetwothirdright">';
|
||||
|
||||
// Zuletzt bearbeitete Stundenzettel
|
||||
print '<div class="div-table-responsive-no-min">';
|
||||
print '<table class="noborder centpercent">';
|
||||
print '<tr class="liste_titre">';
|
||||
print '<th>'.$langs->trans("BoxRecentStundenzettel").'</th>';
|
||||
print '<th>'.$langs->trans("StundenzettelDate").'</th>';
|
||||
print '<th>'.$langs->trans("StundenzettelOrder").'</th>';
|
||||
print '<th>'.$langs->trans("StundenzettelCustomer").'</th>';
|
||||
print '<th class="right">'.$langs->trans("StundenzettelStatus").'</th>';
|
||||
print '</tr>';
|
||||
|
||||
$sql = "SELECT s.rowid, s.ref, s.date_stundenzettel, s.status, s.fk_commande, s.fk_soc,";
|
||||
$sql .= " c.ref as order_ref, soc.nom as soc_name";
|
||||
$sql .= " FROM ".MAIN_DB_PREFIX."stundenzettel as s";
|
||||
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."commande as c ON c.rowid = s.fk_commande";
|
||||
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."societe as soc ON soc.rowid = s.fk_soc";
|
||||
$sql .= " WHERE s.entity = ".((int)$conf->entity);
|
||||
$sql .= " ORDER BY s.tms DESC";
|
||||
$sql .= " LIMIT 10";
|
||||
|
||||
$resql = $db->query($sql);
|
||||
if ($resql) {
|
||||
$num = $db->num_rows($resql);
|
||||
if ($num > 0) {
|
||||
while ($obj = $db->fetch_object($resql)) {
|
||||
print '<tr class="oddeven">';
|
||||
|
||||
// Referenz
|
||||
print '<td>';
|
||||
print '<a href="'.dol_buildpath('/stundenzettel/card.php?id='.$obj->rowid, 1).'">';
|
||||
print img_picto('', 'clock', 'class="pictofixedwidth"').$obj->ref;
|
||||
print '</a>';
|
||||
print '</td>';
|
||||
|
||||
// Datum
|
||||
print '<td>'.dol_print_date($db->jdate($obj->date_stundenzettel), 'day').'</td>';
|
||||
|
||||
// Auftrag
|
||||
print '<td>';
|
||||
if ($obj->order_ref) {
|
||||
print '<a href="'.DOL_URL_ROOT.'/commande/card.php?id='.$obj->fk_commande.'">';
|
||||
print img_picto('', 'order', 'class="pictofixedwidth"').$obj->order_ref;
|
||||
print '</a>';
|
||||
}
|
||||
print '</td>';
|
||||
|
||||
// Kunde
|
||||
print '<td>';
|
||||
if ($obj->soc_name) {
|
||||
print '<a href="'.DOL_URL_ROOT.'/societe/card.php?socid='.$obj->fk_soc.'">';
|
||||
print img_picto('', 'company', 'class="pictofixedwidth"').$obj->soc_name;
|
||||
print '</a>';
|
||||
}
|
||||
print '</td>';
|
||||
|
||||
// Status
|
||||
print '<td class="right">';
|
||||
if ($obj->status == 0) {
|
||||
print '<span class="badge badge-warning">'.$langs->trans("StatusDraft").'</span>';
|
||||
} elseif ($obj->status == 1) {
|
||||
print '<span class="badge badge-success">'.$langs->trans("StatusValidated").'</span>';
|
||||
} elseif ($obj->status == 2) {
|
||||
print '<span class="badge badge-info">'.$langs->trans("StatusInvoiced").'</span>';
|
||||
}
|
||||
print '</td>';
|
||||
|
||||
print '</tr>';
|
||||
}
|
||||
} else {
|
||||
print '<tr class="oddeven"><td colspan="5" class="opacitymedium">'.$langs->trans("NoOpenStundenzettel").'</td></tr>';
|
||||
}
|
||||
} else {
|
||||
dol_print_error($db);
|
||||
}
|
||||
|
||||
print '</table>';
|
||||
print '</div>';
|
||||
|
||||
// Offene Aufträge mit Stundenzettel-Funktion
|
||||
print '<br>';
|
||||
print '<div class="div-table-responsive-no-min">';
|
||||
print '<table class="noborder centpercent">';
|
||||
print '<tr class="liste_titre">';
|
||||
print '<th colspan="4">'.$langs->trans("OpenOrders").' - '.$langs->trans("ActivateStundenzettel").'</th>';
|
||||
print '</tr>';
|
||||
|
||||
$sql = "SELECT c.rowid, c.ref, c.date_commande, c.total_ht, soc.nom as soc_name, soc.rowid as soc_id";
|
||||
$sql .= " FROM ".MAIN_DB_PREFIX."commande as c";
|
||||
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."societe as soc ON soc.rowid = c.fk_soc";
|
||||
$sql .= " WHERE c.entity = ".((int)$conf->entity);
|
||||
$sql .= " AND c.fk_statut IN (1, 2)"; // Bestätigt oder in Bearbeitung
|
||||
$sql .= " ORDER BY c.date_commande DESC";
|
||||
$sql .= " LIMIT 10";
|
||||
|
||||
$resql = $db->query($sql);
|
||||
if ($resql) {
|
||||
$num = $db->num_rows($resql);
|
||||
if ($num > 0) {
|
||||
while ($obj = $db->fetch_object($resql)) {
|
||||
print '<tr class="oddeven">';
|
||||
|
||||
// Auftrag
|
||||
print '<td>';
|
||||
print '<a href="'.DOL_URL_ROOT.'/commande/card.php?id='.$obj->rowid.'">';
|
||||
print img_picto('', 'order', 'class="pictofixedwidth"').$obj->ref;
|
||||
print '</a>';
|
||||
print '</td>';
|
||||
|
||||
// Kunde
|
||||
print '<td>';
|
||||
print '<a href="'.DOL_URL_ROOT.'/societe/card.php?socid='.$obj->soc_id.'">';
|
||||
print $obj->soc_name;
|
||||
print '</a>';
|
||||
print '</td>';
|
||||
|
||||
// Datum
|
||||
print '<td>'.dol_print_date($db->jdate($obj->date_commande), 'day').'</td>';
|
||||
|
||||
// Aktion
|
||||
print '<td class="right">';
|
||||
print '<a class="butActionSmall" href="'.dol_buildpath('/stundenzettel/stundenzettel_commande.php?id='.$obj->rowid, 1).'">';
|
||||
print img_picto('', 'clock', 'class="pictofixedwidth"').$langs->trans("Stundenzettel");
|
||||
print '</a>';
|
||||
print '</td>';
|
||||
|
||||
print '</tr>';
|
||||
}
|
||||
} else {
|
||||
print '<tr class="oddeven"><td colspan="4" class="opacitymedium">'.$langs->trans("NoRecordFound").'</td></tr>';
|
||||
}
|
||||
}
|
||||
|
||||
print '</table>';
|
||||
print '</div>';
|
||||
|
||||
print '</div>'; // fichetwothirdright
|
||||
|
||||
print '</div>'; // fichecenter
|
||||
|
||||
print '<div class="clearboth"></div>';
|
||||
|
||||
llxFooter();
|
||||
$db->close();
|
||||
233
langs/de_DE/stundenzettel.lang
Normal file
233
langs/de_DE/stundenzettel.lang
Normal file
|
|
@ -0,0 +1,233 @@
|
|||
# Stundenzettel Modul - Deutsche Sprachdatei
|
||||
# Copyright (C) 2026 Data IT Solution
|
||||
|
||||
# Modul
|
||||
Module500200Name = Stundenzettel
|
||||
Module500200Desc = Stundenzettel-Verwaltung für Aufträge
|
||||
|
||||
# Menü
|
||||
Stundenzettel = Stundenzettel
|
||||
StundenzettelList = Alle Stundenzettel
|
||||
StundenzettelNew = Neuer Stundenzettel
|
||||
StundenzettelOverview = Übersicht
|
||||
|
||||
# Felder
|
||||
StundenzettelRef = Stundenzettel-Nr.
|
||||
StundenzettelDate = Datum
|
||||
StundenzettelOrder = Auftrag
|
||||
OrderDescription = Auftragsbeschreibung
|
||||
StundenzettelCustomer = Kunde
|
||||
StundenzettelStatus = Status
|
||||
StundenzettelAuthor = Ersteller
|
||||
StundenzettelValidatedBy = Freigegeben von
|
||||
|
||||
# Status
|
||||
StatusDraft = Entwurf
|
||||
StatusValidated = Freigegeben
|
||||
StatusInvoiced = In Rechnung übertragen
|
||||
StatusCanceled = Storniert
|
||||
|
||||
# Aktionen
|
||||
CreateStundenzettel = Stundenzettel erstellen
|
||||
EditStundenzettel = Stundenzettel bearbeiten
|
||||
ValidateStundenzettel = Stundenzettel freigeben
|
||||
DeleteStundenzettel = Stundenzettel löschen
|
||||
CloneStundenzettel = Stundenzettel duplizieren
|
||||
TransferToInvoice = In Rechnung übertragen
|
||||
ActivateStundenzettel = Stundenzettel aktivieren
|
||||
ReopenStundenzettel = Stundenzettel wieder öffnen
|
||||
|
||||
# Leistungen
|
||||
Leistungen = Leistungen
|
||||
LeistungNew = Neue Leistung
|
||||
LeistungDate = Datum
|
||||
LeistungTimeStart = Beginn
|
||||
LeistungTimeEnd = Ende
|
||||
LeistungDuration = Dauer
|
||||
LeistungDescription = Beschreibung
|
||||
AddLeistung = Leistung hinzufügen
|
||||
|
||||
# Produkte
|
||||
Products = Produkte
|
||||
ProductsDesc = Hier können Sie Produkte aus dem Auftrag erfassen, die verbraucht wurden.
|
||||
ProductsFromOrder = Produkte aus Auftrag
|
||||
ProductList = Produktliste
|
||||
ProductsAdded = Hinzugefügte Produkte
|
||||
OtherProducts = Sonstige Produkte
|
||||
QtyOrdered = Beauftragt
|
||||
QtyFromOrder = Beauftragt
|
||||
QtyUsed = Erfasst
|
||||
QtyDelivered = Verbaut
|
||||
QtyDone = Erledigt
|
||||
QtyRemaining = Verbleibend
|
||||
QtyAdded = Hinzugefügt
|
||||
QtyRemoved = Entfallen
|
||||
AddProduct = Produkt hinzufügen
|
||||
AddMoreProducts = Weitere Produkte hinzufügen
|
||||
SelectProducts = Produkte auswählen
|
||||
NoProductsInOrder = Keine Produkte im Auftrag
|
||||
AllProductsUsedOrOmitted = Alle Produkte bereits verbaut oder als entfällt markiert
|
||||
TransferProducts = Produkte übernehmen
|
||||
TransferToStundenzettel = Übernehmen in Stundenzettel
|
||||
FromOrder = Aus Auftrag
|
||||
Added = Hinzugefügt
|
||||
AlreadyOnStundenzettel = Bereits auf diesem Stundenzettel
|
||||
BackToOrder = Zurück zum Auftrag
|
||||
Add = Hinzufügen
|
||||
FreeText = Freitext
|
||||
|
||||
# Tabs
|
||||
Card = Übersicht
|
||||
View = Anzeigen
|
||||
|
||||
# Tracking
|
||||
DeliveryTracking = Lieferauflistung
|
||||
TrackingStatus = Status
|
||||
TrackingOpen = Offen
|
||||
TrackingPartial = Teilweise erledigt
|
||||
TrackingDone = Erledigt
|
||||
|
||||
# Notizen
|
||||
Notes = Notizen
|
||||
NotesForNextVisit = Notizen für nächsten Termin
|
||||
NotesMemo = Merkzettel
|
||||
AddNote = Notiz hinzufügen
|
||||
NoteText = Notiz
|
||||
ConfirmDeleteNote = Diese Notiz wirklich löschen?
|
||||
NoNotesYet = Noch keine Notizen vorhanden
|
||||
OpenNotes = Offene Notizen
|
||||
CheckedNotes = Erledigte Notizen
|
||||
MarkAsDone = Als erledigt markieren
|
||||
MarkAsOpen = Wieder öffnen
|
||||
|
||||
# Meldungen
|
||||
ConfirmValidate = Stundenzettel wirklich freigeben?
|
||||
ConfirmDelete = Stundenzettel wirklich löschen?
|
||||
ConfirmDeleteLeistung = Diese Leistung wirklich löschen?
|
||||
RecordDeleted = Eintrag gelöscht
|
||||
ConfirmTransfer = Alle Produkte in Rechnung übertragen?
|
||||
AllProductsDocumented = Alle Produkte dokumentiert
|
||||
NotAllProductsDocumented = Nicht alle Produkte dokumentiert
|
||||
NoOpenStundenzettel = Keine offenen Stundenzettel
|
||||
StundenzettelCreated = Stundenzettel erstellt
|
||||
StundenzettelExistsForDate = Für diesen Auftrag und dieses Datum existiert bereits ein Stundenzettel
|
||||
StundenzettelValidated = Stundenzettel freigegeben
|
||||
StundenzettelDeleted = Stundenzettel gelöscht
|
||||
|
||||
# Mengenwarnung
|
||||
ConfirmExceedOrderQty = Auftragsmenge wird überschritten!
|
||||
OrderQty = Auftragsmenge
|
||||
TotalUsedAllStz = Gesamtmenge alle Stundenzettel
|
||||
ConfirmAdditionalWork = Mehraufwand bestätigen?
|
||||
|
||||
# Mehraufwand
|
||||
Mehraufwand = Mehraufwand
|
||||
MehraufwandDesc = Hier können Sie zusätzliche Produkte erfassen, die nicht im ursprünglichen Auftrag enthalten waren.
|
||||
AddMehraufwand = Mehraufwand hinzufügen
|
||||
OrFreeText = Oder Freitext
|
||||
FreeTextDescription = Freitext-Beschreibung
|
||||
ConfirmDeleteMehraufwand = Diesen Mehraufwand wirklich löschen?
|
||||
ErrorNoProductSelected = Bitte wählen Sie ein Produkt oder geben Sie eine Beschreibung ein
|
||||
AdditionalQty = Zusätzliche Menge
|
||||
FromStundenzettel = Aus Stundenzettel
|
||||
ProductExistsInOrder = Produkt im Auftrag gefunden, als Verbrauch hinzugefügt
|
||||
MehraufwandConflictTitle = Produkt bereits als Mehraufwand vorhanden
|
||||
MehraufwandConflictWarning = Folgende Produkte sind bereits als Mehraufwand erfasst
|
||||
MehraufwandConflictQuestion = Möchten Sie diese Produkte trotzdem als Verbrauch übernehmen?
|
||||
MehraufwandConflictExplanation = Wenn Sie fortfahren, wird das Produkt sowohl als Auftragsposition (Verbrauch) als auch als Mehraufwand geführt. Dies kann zu doppelter Berechnung führen.
|
||||
MehraufwandTransferTitle = Mehraufwand übernehmen
|
||||
MehraufwandTransferWarning = Folgende Produkte wurden als Mehraufwand erfasst
|
||||
MehraufwandTransferQuestion = Diese Produkte werden als normale Verbrauchsposition übernommen.
|
||||
MehraufwandTransferExplanation = Die Produkte werden in die Produktliste des Stundenzettels übernommen (nicht als Mehraufwand). Die ursprüngliche Mehraufwand-Zeile bleibt bestehen.
|
||||
|
||||
# Entfällt
|
||||
Entfaellt = Entfällt
|
||||
EntfaelltDesc = Hier können Sie Produkte aus dem Auftrag erfassen, die nicht verbaut werden müssen.
|
||||
AddEntfaellt = Entfällt hinzufügen
|
||||
Reason = Grund
|
||||
Optional = optional
|
||||
ConfirmDeleteEntfaellt = Diesen Eintrag wirklich löschen?
|
||||
|
||||
# Fehler
|
||||
ErrorNoOrder = Kein Auftrag ausgewählt
|
||||
ErrorOrderNotFound = Auftrag nicht gefunden
|
||||
ErrorNoProducts = Keine Produkte ausgewählt
|
||||
ErrorAlreadyValidated = Stundenzettel bereits freigegeben
|
||||
ErrorQtyExceedsAvailable = Menge überschreitet verfügbare Menge (max. %s)
|
||||
ErrorTimeOverlap = Zeitüberschneidung mit bestehender Leistung (%s - %s)
|
||||
ErrorStundenzettelReleased = Die Stundenzettel für diesen Auftrag wurden bereits freigegeben und können nicht mehr geändert werden
|
||||
|
||||
# Widgets
|
||||
BoxRecentStundenzettel = Zuletzt bearbeitete Stundenzettel
|
||||
BoxOpenStundenzettel = Offene Stundenzettel
|
||||
|
||||
# Berechtigungen
|
||||
PermissionRead = Stundenzettel lesen
|
||||
PermissionWrite = Stundenzettel erstellen/bearbeiten
|
||||
PermissionValidate = Stundenzettel freigeben
|
||||
PermissionDelete = Stundenzettel löschen
|
||||
|
||||
# Einstellungen
|
||||
StundenzettelSetup = Stundenzettel Einstellungen
|
||||
TimeInputMode = Zeiteingabe-Modus
|
||||
TimeInputDropdown = Dropdown (15-Minuten-Takt)
|
||||
TimeInputText = Freitext (exakte Uhrzeit)
|
||||
PreciseTimeEntry = Exakte Zeiterfassung
|
||||
DefaultFilter = Standard-Filter Produktliste
|
||||
DefaultFilterDesc = Welcher Filter soll standardmäßig in der Produktliste angezeigt werden?
|
||||
DefaultDate = Standard-Datum für Stundenzettel
|
||||
DefaultDateDesc = Welches Datum soll standardmäßig für neue Stundenzettel vorgeschlagen werden?
|
||||
DateToday = Immer aktuelles Datum
|
||||
DateLastOpen = Datum des letzten offenen Stundenzettels
|
||||
NewStundenzettelForToday = Neuer Stundenzettel für heute
|
||||
SelectedStundenzettelNotToday = Der ausgewählte Stundenzettel %s ist vom %s - es wird ein neuer für heute erstellt
|
||||
|
||||
# Filter
|
||||
Filter = Filter
|
||||
FilterAll = Alle
|
||||
FilterOpen = Offen
|
||||
FilterDone = Erledigt
|
||||
|
||||
# Sections
|
||||
ExpandAll = Alles ausklappen
|
||||
CollapseAll = Alles einklappen
|
||||
ShowDetails = Details anzeigen
|
||||
HideDetails = Details verbergen
|
||||
Type = Typ
|
||||
|
||||
# Freigabe und Rechnung
|
||||
ReleaseStundenzettel = Stundenzettel freigeben
|
||||
ReopenStundenzettel = Stundenzettel wiedereröffnen
|
||||
TransferToInvoice = In Rechnung übernehmen
|
||||
StundenzettelReleased = Stundenzettel wurden freigegeben
|
||||
StundenzettelReopened = Stundenzettel wurden wiedereröffnet
|
||||
AllStundenzettelMustBeValidated = Alle Stundenzettel müssen erst validiert werden
|
||||
RemainingProductsWarning = Folgende Produkte sind noch nicht vollständig dokumentiert
|
||||
ConfirmReleaseWithRemainingTitle = Freigabe mit offenen Produkten?
|
||||
ConfirmReleaseWithRemaining = Es gibt noch nicht vollständig dokumentierte Produkte. Wollen Sie die Stundenzettel trotzdem freigeben?
|
||||
ConfirmReleaseFinalTitle = Endgültige Freigabe bestätigen
|
||||
ConfirmReleaseFinal = Sind Sie WIRKLICH sicher? Nach der Freigabe können keine Änderungen mehr vorgenommen werden.
|
||||
ConfirmTransferInvoiceTitle = In Rechnung übernehmen?
|
||||
ConfirmTransferInvoice = Alle dokumentierten Produkte werden in eine neue Rechnung übertragen. Fortfahren?
|
||||
InvoiceCreated = Rechnung wurde erstellt
|
||||
CreatedFromStundenzettel = Erstellt aus Stundenzettel
|
||||
StundenzettelTransferredToInvoice = Stundenzettel wurden bereits in Rechnung übertragen
|
||||
StundenzettelReleasedNoChanges = Die Stundenzettel sind freigegeben. Keine Änderungen mehr möglich.
|
||||
ResetStundenzettel = Status zurücksetzen
|
||||
StundenzettelReset = Stundenzettel-Status wurde zurückgesetzt
|
||||
|
||||
# Standard-Leistung
|
||||
DefaultService = Standard-Leistung
|
||||
DefaultServiceDesc = Standard-Dienstleistung für Stundenzettel (wird beim Kunden hinterlegt)
|
||||
DefaultServiceFromCustomer = Standard-Leistung vom Kunden
|
||||
NoDefaultServiceSet = Keine Standard-Leistung hinterlegt
|
||||
SetDefaultServiceInCustomer = Standard-Leistung beim Kunden hinterlegen
|
||||
PlannedHours = Geplante Stunden
|
||||
TotalHours = Gesamtstunden
|
||||
HoursFromOrder = Stunden aus Auftrag
|
||||
|
||||
# Rechnungsübernahme Stunden
|
||||
InvoiceHoursMode = Stunden-Übernahme
|
||||
InvoiceHoursModeTotal = Gesamtstunden auf einer Zeile
|
||||
InvoiceHoursModePerDay = Pro Tag eine Zeile
|
||||
SelectInvoiceHoursMode = Wie sollen die Arbeitsstunden übernommen werden?
|
||||
89
lib/stundenzettel.lib.php
Normal file
89
lib/stundenzettel.lib.php
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
<?php
|
||||
/* Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
|
||||
*
|
||||
* Stundenzettel - Library functions
|
||||
*/
|
||||
|
||||
/**
|
||||
* Prepare array of tabs for Stundenzettel card
|
||||
*
|
||||
* @param Stundenzettel $object Object
|
||||
* @return array Array of tabs
|
||||
*/
|
||||
function stundenzettel_prepare_head($object)
|
||||
{
|
||||
global $db, $langs, $conf, $user;
|
||||
|
||||
$langs->load("stundenzettel@stundenzettel");
|
||||
|
||||
$h = 0;
|
||||
$head = array();
|
||||
|
||||
// Tab 1: Kundenauftrag (Link zum Auftrag) - immer am Anfang
|
||||
if ($object->fk_commande > 0) {
|
||||
$head[$h][0] = DOL_URL_ROOT.'/commande/card.php?id='.$object->fk_commande;
|
||||
$head[$h][1] = $langs->trans("Order");
|
||||
$head[$h][2] = 'order';
|
||||
$h++;
|
||||
}
|
||||
|
||||
// Tab 2: Produktliste (Link zu stundenzettel_commande.php mit Produktliste aus Auftrag)
|
||||
if ($object->fk_commande > 0) {
|
||||
$head[$h][0] = dol_buildpath('/stundenzettel/stundenzettel_commande.php', 1).'?id='.$object->fk_commande.'&tab=products&noredirect=1&stundenzettel_id='.$object->id;
|
||||
$head[$h][1] = $langs->trans("ProductList");
|
||||
$head[$h][2] = 'productlist';
|
||||
$h++;
|
||||
}
|
||||
|
||||
// Tab 3: Stundenzettel (Link zum aktiven Stundenzettel - card.php)
|
||||
$nbLeistungen = 0;
|
||||
if (!empty($object->leistungen)) {
|
||||
$nbLeistungen = count($object->leistungen);
|
||||
}
|
||||
$nbProducts = 0;
|
||||
if (!empty($object->products)) {
|
||||
$nbProducts = count($object->products);
|
||||
}
|
||||
$head[$h][0] = dol_buildpath('/stundenzettel/card.php', 1).'?id='.$object->id;
|
||||
$head[$h][1] = $langs->trans("Stundenzettel");
|
||||
$totalItems = $nbLeistungen + $nbProducts;
|
||||
if ($totalItems > 0) {
|
||||
$head[$h][1] .= '<span class="badge marginleftonlyshort">'.$totalItems.'</span>';
|
||||
}
|
||||
$head[$h][2] = 'card';
|
||||
$h++;
|
||||
|
||||
// Tab 4: Alle Stundenzettel (Liste aller Stundenzettel für diesen Auftrag)
|
||||
if ($object->fk_commande > 0) {
|
||||
// Anzahl Stundenzettel für diesen Auftrag zählen
|
||||
$sql = "SELECT COUNT(*) as nb FROM ".MAIN_DB_PREFIX."stundenzettel WHERE fk_commande = ".((int)$object->fk_commande);
|
||||
$resql = $db->query($sql);
|
||||
$nbStundenzettel = 0;
|
||||
if ($resql && ($obj = $db->fetch_object($resql))) {
|
||||
$nbStundenzettel = $obj->nb;
|
||||
}
|
||||
$head[$h][0] = dol_buildpath('/stundenzettel/stundenzettel_commande.php', 1).'?id='.$object->fk_commande.'&tab=stundenzettel&noredirect=1&stundenzettel_id='.$object->id;
|
||||
$head[$h][1] = $langs->trans("StundenzettelList");
|
||||
if ($nbStundenzettel > 0) {
|
||||
$head[$h][1] .= '<span class="badge marginleftonlyshort">'.$nbStundenzettel.'</span>';
|
||||
}
|
||||
$head[$h][2] = 'stundenzettel_list';
|
||||
$h++;
|
||||
}
|
||||
|
||||
// Tab 5: Lieferauflistung (Tracking)
|
||||
if ($object->fk_commande > 0) {
|
||||
$head[$h][0] = dol_buildpath('/stundenzettel/stundenzettel_commande.php', 1).'?id='.$object->fk_commande.'&tab=tracking&noredirect=1&stundenzettel_id='.$object->id;
|
||||
$head[$h][1] = $langs->trans("DeliveryTracking");
|
||||
$head[$h][2] = 'tracking';
|
||||
$h++;
|
||||
}
|
||||
|
||||
// Tab 6: Notizen
|
||||
$head[$h][0] = dol_buildpath('/stundenzettel/card.php', 1).'?id='.$object->id.'&tab=notes';
|
||||
$head[$h][1] = $langs->trans("Notes");
|
||||
$head[$h][2] = 'notes';
|
||||
$h++;
|
||||
|
||||
return $head;
|
||||
}
|
||||
216
list.php
Normal file
216
list.php
Normal file
|
|
@ -0,0 +1,216 @@
|
|||
<?php
|
||||
/* Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
|
||||
*
|
||||
* Stundenzettel - Liste
|
||||
*/
|
||||
|
||||
// Load Dolibarr environment
|
||||
$res = 0;
|
||||
if (!$res && file_exists("../main.inc.php")) $res = @include "../main.inc.php";
|
||||
if (!$res && file_exists("../../main.inc.php")) $res = @include "../../main.inc.php";
|
||||
if (!$res && file_exists("../../../main.inc.php")) $res = @include "../../../main.inc.php";
|
||||
if (!$res) die("Include of main fails");
|
||||
|
||||
require_once DOL_DOCUMENT_ROOT.'/core/class/html.formother.class.php';
|
||||
dol_include_once('/stundenzettel/class/stundenzettel.class.php');
|
||||
|
||||
// Load translation files
|
||||
$langs->loadLangs(array("stundenzettel@stundenzettel", "orders"));
|
||||
|
||||
// Security check
|
||||
if (!$user->hasRight('stundenzettel', 'read')) {
|
||||
accessforbidden();
|
||||
}
|
||||
|
||||
// Get parameters
|
||||
$action = GETPOST('action', 'aZ09');
|
||||
$massaction = GETPOST('massaction', 'alpha');
|
||||
$contextpage = GETPOST('contextpage', 'aZ') ? GETPOST('contextpage', 'aZ') : 'stundenzettellist';
|
||||
|
||||
$search_ref = GETPOST('search_ref', 'alpha');
|
||||
$search_order = GETPOST('search_order', 'alpha');
|
||||
$search_soc = GETPOST('search_soc', 'alpha');
|
||||
$search_status = GETPOST('search_status', 'int');
|
||||
$search_author = GETPOST('search_author', 'int');
|
||||
|
||||
// __USER_ID__ ersetzen falls verwendet
|
||||
if ($search_author == '__USER_ID__' || GETPOST('search_author', 'alpha') == '__USER_ID__') {
|
||||
$search_author = $user->id;
|
||||
}
|
||||
|
||||
$sortfield = GETPOST('sortfield', 'aZ09comma');
|
||||
$sortorder = GETPOST('sortorder', 'aZ09comma');
|
||||
$page = GETPOSTISSET('pageplusone') ? (GETPOST('pageplusone') - 1) : GETPOST("page", 'int');
|
||||
if (empty($page) || $page < 0) $page = 0;
|
||||
$limit = GETPOST('limit', 'int') ? GETPOST('limit', 'int') : $conf->liste_limit;
|
||||
$offset = $limit * $page;
|
||||
|
||||
if (!$sortfield) $sortfield = 's.date_stundenzettel';
|
||||
if (!$sortorder) $sortorder = 'DESC';
|
||||
|
||||
// Build SQL
|
||||
$sql = "SELECT s.rowid, s.ref, s.date_stundenzettel, s.status, s.fk_commande, s.fk_soc,";
|
||||
$sql .= " c.ref as order_ref, soc.nom as soc_name";
|
||||
$sql .= " FROM ".MAIN_DB_PREFIX."stundenzettel as s";
|
||||
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."commande as c ON c.rowid = s.fk_commande";
|
||||
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."societe as soc ON soc.rowid = s.fk_soc";
|
||||
$sql .= " WHERE s.entity = ".((int)$conf->entity);
|
||||
|
||||
if ($search_ref) {
|
||||
$sql .= natural_search('s.ref', $search_ref);
|
||||
}
|
||||
if ($search_order) {
|
||||
$sql .= natural_search('c.ref', $search_order);
|
||||
}
|
||||
if ($search_soc) {
|
||||
$sql .= natural_search('soc.nom', $search_soc);
|
||||
}
|
||||
if ($search_status !== '' && $search_status >= 0) {
|
||||
$sql .= " AND s.status = ".((int)$search_status);
|
||||
}
|
||||
if ($search_author > 0) {
|
||||
$sql .= " AND s.fk_user_author = ".((int)$search_author);
|
||||
}
|
||||
|
||||
$sql .= $db->order($sortfield, $sortorder);
|
||||
|
||||
// Count total
|
||||
$nbtotalofrecords = '';
|
||||
$resql = $db->query($sql);
|
||||
if ($resql) {
|
||||
$nbtotalofrecords = $db->num_rows($resql);
|
||||
}
|
||||
|
||||
$sql .= $db->plimit($limit + 1, $offset);
|
||||
|
||||
/*
|
||||
* View
|
||||
*/
|
||||
|
||||
$form = new Form($db);
|
||||
$formother = new FormOther($db);
|
||||
$objectstatic = new Stundenzettel($db);
|
||||
|
||||
$title = $langs->trans("StundenzettelList");
|
||||
|
||||
// Linkes Menü aktivieren
|
||||
$_GET['mainmenu'] = 'stundenzettel';
|
||||
|
||||
llxHeader('', $title, '', '', 0, 0, '', '', '', 'mod-stundenzettel page-list');
|
||||
|
||||
$param = '';
|
||||
if ($search_ref) $param .= '&search_ref='.urlencode($search_ref);
|
||||
if ($search_order) $param .= '&search_order='.urlencode($search_order);
|
||||
if ($search_soc) $param .= '&search_soc='.urlencode($search_soc);
|
||||
if ($search_status !== '') $param .= '&search_status='.urlencode($search_status);
|
||||
if ($search_author > 0) $param .= '&search_author='.urlencode($search_author);
|
||||
|
||||
print '<form method="POST" action="'.$_SERVER['PHP_SELF'].'" name="formfilter">';
|
||||
print '<input type="hidden" name="token" value="'.newToken().'">';
|
||||
|
||||
$newcardbutton = '';
|
||||
if ($user->hasRight('stundenzettel', 'write')) {
|
||||
$newcardbutton = '<a class="butActionNew" href="'.dol_buildpath('/stundenzettel/card.php?action=create', 1).'">';
|
||||
$newcardbutton .= '<span class="fa fa-plus-circle valignmiddle"></span>';
|
||||
$newcardbutton .= '</a>';
|
||||
}
|
||||
|
||||
print_barre_liste($title, $page, $_SERVER['PHP_SELF'], $param, $sortfield, $sortorder, '', $nbtotalofrecords, $nbtotalofrecords, 'clock', 0, $newcardbutton, '', $limit);
|
||||
|
||||
print '<div class="div-table-responsive-no-min">';
|
||||
print '<table class="tagtable nobottomiftotal liste">';
|
||||
|
||||
// Header
|
||||
print '<tr class="liste_titre_filter">';
|
||||
print '<td class="liste_titre"><input type="text" class="flat" name="search_ref" value="'.dol_escape_htmltag($search_ref).'" size="10"></td>';
|
||||
print '<td class="liste_titre"></td>';
|
||||
print '<td class="liste_titre"><input type="text" class="flat" name="search_order" value="'.dol_escape_htmltag($search_order).'" size="10"></td>';
|
||||
print '<td class="liste_titre"><input type="text" class="flat" name="search_soc" value="'.dol_escape_htmltag($search_soc).'" size="15"></td>';
|
||||
print '<td class="liste_titre center">';
|
||||
print '<select name="search_status" class="flat">';
|
||||
print '<option value="">--</option>';
|
||||
print '<option value="0"'.($search_status === '0' ? ' selected' : '').'>'.$langs->trans("StatusDraft").'</option>';
|
||||
print '<option value="1"'.($search_status === '1' ? ' selected' : '').'>'.$langs->trans("StatusValidated").'</option>';
|
||||
print '<option value="2"'.($search_status === '2' ? ' selected' : '').'>'.$langs->trans("StatusInvoiced").'</option>';
|
||||
print '</select>';
|
||||
print '</td>';
|
||||
print '<td class="liste_titre center">';
|
||||
print '<input type="image" class="liste_titre" name="button_search" src="'.img_picto($langs->trans("Search"), 'search.png', '', '', 1).'" title="'.dol_escape_htmltag($langs->trans("Search")).'">';
|
||||
print '<input type="image" class="liste_titre" name="button_removefilter" src="'.img_picto($langs->trans("RemoveFilter"), 'searchclear.png', '', '', 1).'" title="'.dol_escape_htmltag($langs->trans("RemoveFilter")).'">';
|
||||
print '</td>';
|
||||
print '</tr>';
|
||||
|
||||
print '<tr class="liste_titre">';
|
||||
print_liste_field_titre("Ref", $_SERVER['PHP_SELF'], "s.ref", "", $param, "", $sortfield, $sortorder);
|
||||
print_liste_field_titre("Date", $_SERVER['PHP_SELF'], "s.date_stundenzettel", "", $param, "", $sortfield, $sortorder);
|
||||
print_liste_field_titre("Order", $_SERVER['PHP_SELF'], "c.ref", "", $param, "", $sortfield, $sortorder);
|
||||
print_liste_field_titre("Customer", $_SERVER['PHP_SELF'], "soc.nom", "", $param, "", $sortfield, $sortorder);
|
||||
print_liste_field_titre("Status", $_SERVER['PHP_SELF'], "s.status", "", $param, "", $sortfield, $sortorder, 'center ');
|
||||
print_liste_field_titre("", $_SERVER['PHP_SELF'], "", "", $param, "", $sortfield, $sortorder, 'center ');
|
||||
print '</tr>';
|
||||
|
||||
$resql = $db->query($sql);
|
||||
if ($resql) {
|
||||
$num = $db->num_rows($resql);
|
||||
$i = 0;
|
||||
|
||||
while ($i < min($num, $limit)) {
|
||||
$obj = $db->fetch_object($resql);
|
||||
|
||||
print '<tr class="oddeven">';
|
||||
|
||||
// Ref
|
||||
print '<td>';
|
||||
print '<a href="'.dol_buildpath('/stundenzettel/card.php?id='.$obj->rowid, 1).'">';
|
||||
print img_picto('', 'clock', 'class="pictofixedwidth"').$obj->ref;
|
||||
print '</a>';
|
||||
print '</td>';
|
||||
|
||||
// Datum
|
||||
print '<td>'.dol_print_date($db->jdate($obj->date_stundenzettel), 'day').'</td>';
|
||||
|
||||
// Auftrag
|
||||
print '<td>';
|
||||
if ($obj->order_ref) {
|
||||
print '<a href="'.DOL_URL_ROOT.'/commande/card.php?id='.$obj->fk_commande.'">';
|
||||
print img_picto('', 'order', 'class="pictofixedwidth"').$obj->order_ref;
|
||||
print '</a>';
|
||||
}
|
||||
print '</td>';
|
||||
|
||||
// Kunde
|
||||
print '<td>';
|
||||
if ($obj->soc_name) {
|
||||
print '<a href="'.DOL_URL_ROOT.'/societe/card.php?socid='.$obj->fk_soc.'">';
|
||||
print $obj->soc_name;
|
||||
print '</a>';
|
||||
}
|
||||
print '</td>';
|
||||
|
||||
// Status
|
||||
print '<td class="center">'.$objectstatic->LibStatut($obj->status, 5).'</td>';
|
||||
|
||||
// Actions
|
||||
print '<td class="center">';
|
||||
print '<a href="'.dol_buildpath('/stundenzettel/card.php?id='.$obj->rowid, 1).'">'.img_picto($langs->trans("View"), 'eye').'</a>';
|
||||
print '</td>';
|
||||
|
||||
print '</tr>';
|
||||
$i++;
|
||||
}
|
||||
|
||||
if ($num == 0) {
|
||||
print '<tr class="oddeven"><td colspan="6" class="opacitymedium">'.$langs->trans("NoRecordFound").'</td></tr>';
|
||||
}
|
||||
|
||||
$db->free($resql);
|
||||
} else {
|
||||
dol_print_error($db);
|
||||
}
|
||||
|
||||
print '</table>';
|
||||
print '</div>';
|
||||
print '</form>';
|
||||
|
||||
llxFooter();
|
||||
$db->close();
|
||||
3
sql/dolibarr_allversions.sql
Normal file
3
sql/dolibarr_allversions.sql
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
--
|
||||
-- Script run when an upgrade of Dolibarr is done. Whatever is the Dolibarr version.
|
||||
--
|
||||
14
sql/llx_commande_extrafields_stundenzettel.sql
Normal file
14
sql/llx_commande_extrafields_stundenzettel.sql
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
-- ============================================================================
|
||||
-- Extrafeld für Stundenzettel-Status am Auftrag
|
||||
-- 0 = Offen (Stundenzettel können bearbeitet werden)
|
||||
-- 1 = Freigegeben (Stundenzettel gesperrt, bereit für Rechnung)
|
||||
-- 2 = In Rechnung übertragen
|
||||
-- ============================================================================
|
||||
|
||||
-- Extrafeld in extrafields-Tabelle registrieren
|
||||
INSERT INTO llx_extrafields (name, entity, elementtype, label, type, size, fieldunique, fieldrequired, pos, alwayseditable, perms, langs, list, printable, fielddefault, fieldcomputed, fk_user_author, fk_user_modif, datec, enabled, help)
|
||||
VALUES ('stundenzettel_status', 1, 'commande', 'StundenzettelStatus', 'int', '1', 0, 0, 100, 0, '', '', 0, 0, '0', '', NULL, NULL, NOW(), '1', '')
|
||||
ON DUPLICATE KEY UPDATE label = 'StundenzettelStatus';
|
||||
|
||||
-- Spalte in commande_extrafields hinzufügen falls nicht vorhanden
|
||||
ALTER TABLE llx_commande_extrafields ADD COLUMN IF NOT EXISTS stundenzettel_status INT DEFAULT 0;
|
||||
9
sql/llx_product_services.sql
Normal file
9
sql/llx_product_services.sql
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
-- ============================================================================
|
||||
-- View for services only (fk_product_type = 1)
|
||||
-- Used for the "Standard-Leistung" extrafield dropdown on customers
|
||||
-- ============================================================================
|
||||
|
||||
CREATE OR REPLACE VIEW llx_product_services AS
|
||||
SELECT rowid, ref, label, description, fk_product_type, entity, tosell, tobuy
|
||||
FROM llx_product
|
||||
WHERE fk_product_type = 1;
|
||||
1
sql/llx_stundenzettel.key.sql
Normal file
1
sql/llx_stundenzettel.key.sql
Normal file
|
|
@ -0,0 +1 @@
|
|||
-- Keys for llx_stundenzettel (already defined in main SQL)
|
||||
40
sql/llx_stundenzettel.sql
Normal file
40
sql/llx_stundenzettel.sql
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
-- ============================================================================
|
||||
-- Stundenzettel Haupttabelle
|
||||
-- ============================================================================
|
||||
|
||||
CREATE TABLE llx_stundenzettel (
|
||||
rowid INTEGER AUTO_INCREMENT PRIMARY KEY,
|
||||
ref VARCHAR(30) NOT NULL, -- Stundenzettel-Nummer (SZ2026-00001)
|
||||
entity INTEGER DEFAULT 1 NOT NULL,
|
||||
|
||||
-- Verknüpfungen
|
||||
fk_commande INTEGER NOT NULL, -- Verknüpfung zum Auftrag
|
||||
fk_facture INTEGER DEFAULT NULL, -- Verknüpfung zur Rechnung (nach Übertrag)
|
||||
fk_soc INTEGER NOT NULL, -- Kunde
|
||||
fk_user_author INTEGER NOT NULL, -- Ersteller
|
||||
fk_user_valid INTEGER DEFAULT NULL, -- Wer hat freigegeben
|
||||
|
||||
-- Datum
|
||||
date_stundenzettel DATE NOT NULL, -- Datum des Stundenzettels
|
||||
datec DATETIME, -- Erstelldatum
|
||||
date_valid DATETIME DEFAULT NULL, -- Freigabedatum
|
||||
|
||||
-- Status: 0=Entwurf, 1=Freigegeben, 2=In Rechnung übertragen, 9=Storniert
|
||||
status TINYINT DEFAULT 0 NOT NULL,
|
||||
|
||||
-- Notizen
|
||||
note_private TEXT,
|
||||
note_public TEXT,
|
||||
|
||||
-- Technisch
|
||||
import_key VARCHAR(14),
|
||||
model_pdf VARCHAR(255),
|
||||
tms TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
|
||||
-- Indizes
|
||||
UNIQUE KEY uk_stundenzettel_ref (ref, entity),
|
||||
INDEX idx_stundenzettel_commande (fk_commande),
|
||||
INDEX idx_stundenzettel_soc (fk_soc),
|
||||
INDEX idx_stundenzettel_date (date_stundenzettel),
|
||||
INDEX idx_stundenzettel_status (status)
|
||||
) ENGINE=InnoDB;
|
||||
1
sql/llx_stundenzettel_leistung.key.sql
Normal file
1
sql/llx_stundenzettel_leistung.key.sql
Normal file
|
|
@ -0,0 +1 @@
|
|||
-- Keys for llx_stundenzettel_leistung (already defined in main SQL)
|
||||
34
sql/llx_stundenzettel_leistung.sql
Normal file
34
sql/llx_stundenzettel_leistung.sql
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
-- ============================================================================
|
||||
-- Stundenzettel Leistungen (Arbeitszeiten)
|
||||
-- Mehrere Leistungen pro Stundenzettel möglich
|
||||
-- ============================================================================
|
||||
|
||||
CREATE TABLE llx_stundenzettel_leistung (
|
||||
rowid INTEGER AUTO_INCREMENT PRIMARY KEY,
|
||||
fk_stundenzettel INTEGER NOT NULL, -- Verknüpfung zum Stundenzettel
|
||||
fk_user INTEGER DEFAULT NULL, -- Welcher Mitarbeiter
|
||||
|
||||
-- Zeitraum
|
||||
date_leistung DATE NOT NULL, -- Datum der Leistung
|
||||
time_start TIME DEFAULT NULL, -- Startzeit
|
||||
time_end TIME DEFAULT NULL, -- Endzeit
|
||||
duration INTEGER DEFAULT 0, -- Dauer in Minuten (berechnet oder manuell)
|
||||
|
||||
-- Beschreibung
|
||||
description TEXT, -- Was wurde gemacht
|
||||
|
||||
-- Rang für Sortierung
|
||||
rang INTEGER DEFAULT 0,
|
||||
|
||||
-- Technisch
|
||||
tms TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
|
||||
-- Indizes
|
||||
INDEX idx_leistung_stundenzettel (fk_stundenzettel),
|
||||
INDEX idx_leistung_date (date_leistung),
|
||||
INDEX idx_leistung_user (fk_user),
|
||||
|
||||
-- Foreign Key
|
||||
CONSTRAINT fk_leistung_stundenzettel FOREIGN KEY (fk_stundenzettel)
|
||||
REFERENCES llx_stundenzettel(rowid) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB;
|
||||
29
sql/llx_stundenzettel_note.sql
Normal file
29
sql/llx_stundenzettel_note.sql
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
-- ============================================================================
|
||||
-- Stundenzettel Notizen (abhakbare Merkzettel)
|
||||
-- Mehrere Notizen pro Stundenzettel möglich
|
||||
-- ============================================================================
|
||||
|
||||
CREATE TABLE llx_stundenzettel_note (
|
||||
rowid INTEGER AUTO_INCREMENT PRIMARY KEY,
|
||||
fk_stundenzettel INTEGER NOT NULL, -- Verknüpfung zum Stundenzettel
|
||||
fk_user INTEGER DEFAULT NULL, -- Wer hat erstellt
|
||||
|
||||
-- Inhalt
|
||||
note TEXT NOT NULL, -- Die Notiz selbst
|
||||
checked TINYINT(1) DEFAULT 0, -- 0=offen, 1=abgehakt
|
||||
|
||||
-- Rang für Sortierung
|
||||
rang INTEGER DEFAULT 0,
|
||||
|
||||
-- Technisch
|
||||
datec DATETIME DEFAULT NULL, -- Erstellt am
|
||||
tms TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
|
||||
-- Indizes
|
||||
INDEX idx_note_stundenzettel (fk_stundenzettel),
|
||||
INDEX idx_note_checked (checked),
|
||||
|
||||
-- Foreign Key
|
||||
CONSTRAINT fk_note_stundenzettel FOREIGN KEY (fk_stundenzettel)
|
||||
REFERENCES llx_stundenzettel(rowid) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB;
|
||||
1
sql/llx_stundenzettel_product.key.sql
Normal file
1
sql/llx_stundenzettel_product.key.sql
Normal file
|
|
@ -0,0 +1 @@
|
|||
-- Keys for llx_stundenzettel_product (already defined in main SQL)
|
||||
43
sql/llx_stundenzettel_product.sql
Normal file
43
sql/llx_stundenzettel_product.sql
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
-- ============================================================================
|
||||
-- Stundenzettel Produkte
|
||||
-- Welche Produkte wurden verbaut/geliefert
|
||||
-- ============================================================================
|
||||
|
||||
CREATE TABLE llx_stundenzettel_product (
|
||||
rowid INTEGER AUTO_INCREMENT PRIMARY KEY,
|
||||
fk_stundenzettel INTEGER NOT NULL, -- Verknüpfung zum Stundenzettel
|
||||
|
||||
-- Produkt-Referenz
|
||||
fk_product INTEGER DEFAULT NULL, -- Produkt-ID (kann NULL sein bei freien Zeilen)
|
||||
fk_commandedet INTEGER DEFAULT NULL, -- Original-Zeile aus Auftrag
|
||||
fk_manager_line INTEGER DEFAULT NULL, -- Zeile aus llx_facture_lines_manager (für Produktgruppen)
|
||||
|
||||
-- Produktdaten (Kopie für Historie)
|
||||
product_ref VARCHAR(128), -- Produktreferenz
|
||||
product_label VARCHAR(255), -- Produktbezeichnung
|
||||
description TEXT, -- Beschreibung
|
||||
|
||||
-- Mengen
|
||||
qty_original DECIMAL(24,8) DEFAULT 0, -- Ursprüngliche Menge aus Auftrag
|
||||
qty_done DECIMAL(24,8) DEFAULT 0, -- Erledigte/verbaute Menge auf diesem Zettel
|
||||
qty_cumulated DECIMAL(24,8) DEFAULT 0, -- Kumuliert über alle Stundenzettel
|
||||
|
||||
-- Herkunft: 'order' = aus Auftrag, 'added' = manuell hinzugefügt
|
||||
origin VARCHAR(20) DEFAULT 'order',
|
||||
|
||||
-- Rang für Sortierung
|
||||
rang INTEGER DEFAULT 0,
|
||||
|
||||
-- Technisch
|
||||
tms TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
|
||||
-- Indizes
|
||||
INDEX idx_szproduct_stundenzettel (fk_stundenzettel),
|
||||
INDEX idx_szproduct_product (fk_product),
|
||||
INDEX idx_szproduct_commandedet (fk_commandedet),
|
||||
INDEX idx_szproduct_manager (fk_manager_line),
|
||||
|
||||
-- Foreign Key
|
||||
CONSTRAINT fk_szproduct_stundenzettel FOREIGN KEY (fk_stundenzettel)
|
||||
REFERENCES llx_stundenzettel(rowid) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB;
|
||||
1
sql/llx_stundenzettel_tracking.key.sql
Normal file
1
sql/llx_stundenzettel_tracking.key.sql
Normal file
|
|
@ -0,0 +1 @@
|
|||
-- Keys for llx_stundenzettel_tracking (already defined in main SQL)
|
||||
37
sql/llx_stundenzettel_tracking.sql
Normal file
37
sql/llx_stundenzettel_tracking.sql
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
-- ============================================================================
|
||||
-- Stundenzettel Tracking
|
||||
-- Gesamtübersicht der Mengen pro Auftrag (aggregiert über alle Stundenzettel)
|
||||
-- ============================================================================
|
||||
|
||||
CREATE TABLE llx_stundenzettel_tracking (
|
||||
rowid INTEGER AUTO_INCREMENT PRIMARY KEY,
|
||||
fk_commande INTEGER NOT NULL, -- Auftrag
|
||||
|
||||
-- Produkt-Referenz
|
||||
fk_product INTEGER DEFAULT NULL, -- Produkt-ID
|
||||
fk_commandedet INTEGER DEFAULT NULL, -- Original-Zeile aus Auftrag
|
||||
fk_manager_line INTEGER DEFAULT NULL, -- Zeile aus llx_facture_lines_manager
|
||||
|
||||
-- Produktdaten
|
||||
product_ref VARCHAR(128),
|
||||
product_label VARCHAR(255),
|
||||
|
||||
-- Mengen
|
||||
qty_ordered DECIMAL(24,8) DEFAULT 0, -- Bestellte Menge (aus Auftrag)
|
||||
qty_delivered DECIMAL(24,8) DEFAULT 0, -- Gelieferte Menge (Summe aller Stundenzettel)
|
||||
qty_added DECIMAL(24,8) DEFAULT 0, -- Zusätzlich hinzugefügt
|
||||
qty_removed DECIMAL(24,8) DEFAULT 0, -- Entfallen/Storniert
|
||||
qty_remaining DECIMAL(24,8) DEFAULT 0, -- Verbleibend (berechnet)
|
||||
|
||||
-- Status: 'open' = offen, 'partial' = teilweise, 'done' = erledigt
|
||||
status VARCHAR(20) DEFAULT 'open',
|
||||
|
||||
-- Technisch
|
||||
tms TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
|
||||
-- Indizes
|
||||
UNIQUE KEY uk_tracking_line (fk_commande, fk_commandedet),
|
||||
INDEX idx_tracking_commande (fk_commande),
|
||||
INDEX idx_tracking_product (fk_product),
|
||||
INDEX idx_tracking_status (status)
|
||||
) ENGINE=InnoDB;
|
||||
2325
stundenzettel_commande.php
Normal file
2325
stundenzettel_commande.php
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue