Fehler beseitigt, Leistungspositonen pro Stundenzettel, Kundenpreis berücksichtigt,
Prototyp Handy ansicht
This commit is contained in:
parent
9627e4fea4
commit
6bfc565121
10 changed files with 1295 additions and 83 deletions
232
card.php
232
card.php
|
|
@ -13,6 +13,7 @@ if (!$res) die("Include of main fails");
|
|||
|
||||
require_once DOL_DOCUMENT_ROOT.'/commande/class/commande.class.php';
|
||||
require_once DOL_DOCUMENT_ROOT.'/product/class/product.class.php';
|
||||
require_once DOL_DOCUMENT_ROOT.'/societe/class/societe.class.php';
|
||||
require_once DOL_DOCUMENT_ROOT.'/core/class/html.formfile.class.php';
|
||||
require_once DOL_DOCUMENT_ROOT.'/core/class/doleditor.class.php';
|
||||
dol_include_once('/stundenzettel/class/stundenzettel.class.php');
|
||||
|
|
@ -233,8 +234,9 @@ if ($action == 'add_leistung' && $permissiontoadd) {
|
|||
$time_start = GETPOST('time_start', 'alpha');
|
||||
$time_end = GETPOST('time_end', 'alpha');
|
||||
$description = GETPOST('leistung_description', 'restricthtml');
|
||||
$fk_product = GETPOST('fk_product', 'int');
|
||||
|
||||
$result = $object->addLeistung($user, $date, $time_start, $time_end, $description);
|
||||
$result = $object->addLeistung($user, $date, $time_start, $time_end, $description, $fk_product);
|
||||
if ($result > 0) {
|
||||
setEventMessages($langs->trans('RecordSaved'), null, 'mesgs');
|
||||
} else {
|
||||
|
|
@ -259,8 +261,9 @@ if ($action == 'update_leistung' && $permissiontoadd) {
|
|||
$time_start = GETPOST('time_start', 'alpha');
|
||||
$time_end = GETPOST('time_end', 'alpha');
|
||||
$description = GETPOST('leistung_description', 'restricthtml');
|
||||
$fk_product = GETPOST('fk_product', 'int');
|
||||
|
||||
$result = $object->updateLeistung($leistung_id, $date, $time_start, $time_end, $description);
|
||||
$result = $object->updateLeistung($leistung_id, $date, $time_start, $time_end, $description, $fk_product);
|
||||
if ($result > 0) {
|
||||
setEventMessages($langs->trans('RecordModified'), null, 'mesgs');
|
||||
} else {
|
||||
|
|
@ -423,16 +426,31 @@ if ($action == 'add_product' && $permissiontoadd) {
|
|||
exit;
|
||||
}
|
||||
|
||||
// Entfällt hinzufügen (Produkt aus Auftrag das nicht verbaut wird)
|
||||
// Entfällt hinzufügen (Produkt aus Auftrag oder Mehraufwand das nicht verbaut wird)
|
||||
if ($action == 'add_entfaellt' && $permissiontoadd) {
|
||||
$entfaellt_product_raw = GETPOST('entfaellt_product', 'alpha');
|
||||
$qty = GETPOST('entfaellt_qty', 'int');
|
||||
$reason = GETPOST('entfaellt_description', 'restricthtml');
|
||||
|
||||
// Prüfen ob es ein Freitext-Produkt ist (Format: "freetext_ROWID")
|
||||
// Prüfen ob es ein Freitext-Produkt, Mehraufwand oder normales Produkt ist
|
||||
$fk_product = 0;
|
||||
$freetext_description = '';
|
||||
if (strpos($entfaellt_product_raw, 'freetext_') === 0) {
|
||||
$commandedet_id = 0;
|
||||
$mehraufwand_id = 0;
|
||||
|
||||
if (strpos($entfaellt_product_raw, 'mehraufwand_') === 0) {
|
||||
// Mehraufwand-Produkt (Format: "mehraufwand_ROWID")
|
||||
$mehraufwand_id = (int)substr($entfaellt_product_raw, 12);
|
||||
// Produkt-ID und Beschreibung aus stundenzettel_product laden
|
||||
$sqlMehr = "SELECT fk_product, description FROM ".MAIN_DB_PREFIX."stundenzettel_product WHERE rowid = ".((int)$mehraufwand_id);
|
||||
$resqlMehr = $db->query($sqlMehr);
|
||||
if ($resqlMehr && ($objMehr = $db->fetch_object($resqlMehr))) {
|
||||
$fk_product = (int)$objMehr->fk_product;
|
||||
if ($fk_product == 0) {
|
||||
$freetext_description = $objMehr->description;
|
||||
}
|
||||
}
|
||||
} elseif (strpos($entfaellt_product_raw, 'freetext_') === 0) {
|
||||
// Freitext-Produkt aus dem Auftrag
|
||||
$commandedet_id = (int)substr($entfaellt_product_raw, 9);
|
||||
// Beschreibung aus commandedet laden
|
||||
|
|
@ -455,7 +473,18 @@ if ($action == 'add_entfaellt' && $permissiontoadd) {
|
|||
|
||||
if ($fk_product > 0 || !empty($freetext_description)) {
|
||||
// Server-seitige Validierung: Prüfen ob Menge noch verfügbar ist
|
||||
if ($object->fk_commande > 0) {
|
||||
if ($mehraufwand_id > 0) {
|
||||
// Mehraufwand-Validierung: Prüfe verfügbare Menge
|
||||
$sqlCheck = "SELECT qty, qty_done FROM ".MAIN_DB_PREFIX."stundenzettel_product WHERE rowid = ".((int)$mehraufwand_id);
|
||||
$resqlCheck = $db->query($sqlCheck);
|
||||
if ($resqlCheck && ($objCheck = $db->fetch_object($resqlCheck))) {
|
||||
$qty_available = $objCheck->qty - $objCheck->qty_done;
|
||||
if ($qty > $qty_available) {
|
||||
setEventMessages($langs->trans('ErrorQtyExceedsAvailable', $qty_available), null, 'errors');
|
||||
$error++;
|
||||
}
|
||||
}
|
||||
} elseif ($object->fk_commande > 0) {
|
||||
if ($fk_product > 0) {
|
||||
// Produkt-Validierung
|
||||
$sqlCheck = "SELECT cd.qty,";
|
||||
|
|
@ -503,20 +532,49 @@ if ($action == 'add_entfaellt' && $permissiontoadd) {
|
|||
}
|
||||
|
||||
if (!$error) {
|
||||
// Produkt zum Stundenzettel hinzufügen mit origin='omitted'
|
||||
$result = $object->addProduct(
|
||||
$fk_product,
|
||||
0, // fk_commandedet
|
||||
0, // fk_manager_line
|
||||
0, // qty_original
|
||||
$qty, // qty_done (Menge die entfällt)
|
||||
'omitted', // origin (entfällt)
|
||||
$description // description (Grund)
|
||||
);
|
||||
if ($result > 0) {
|
||||
setEventMessages($langs->trans('RecordSaved'), null, 'mesgs');
|
||||
if ($mehraufwand_id > 0) {
|
||||
// Mehraufwand: Menge vom qty_done erhöhen statt neuen Eintrag erstellen
|
||||
// Oder: Menge vom Mehraufwand reduzieren und als Entfällt anlegen
|
||||
// Wir reduzieren qty des Mehraufwands und legen einen neuen Entfällt-Eintrag an
|
||||
$sqlUpdateMehr = "UPDATE ".MAIN_DB_PREFIX."stundenzettel_product SET qty = qty - ".((int)$qty);
|
||||
$sqlUpdateMehr .= " WHERE rowid = ".((int)$mehraufwand_id);
|
||||
$db->query($sqlUpdateMehr);
|
||||
|
||||
// Entfällt-Eintrag mit Hinweis auf Mehraufwand anlegen
|
||||
$entfaelltDesc = $langs->trans("Mehraufwand").': '.$description;
|
||||
if (!empty($reason)) {
|
||||
$entfaelltDesc .= ' - '.$reason;
|
||||
}
|
||||
$result = $object->addProduct(
|
||||
$fk_product,
|
||||
0, // fk_commandedet
|
||||
0, // fk_manager_line
|
||||
0, // qty_original
|
||||
$qty, // qty_done (Menge die entfällt)
|
||||
'omitted', // origin (entfällt)
|
||||
$entfaelltDesc // description (Grund)
|
||||
);
|
||||
if ($result > 0) {
|
||||
setEventMessages($langs->trans('RecordSaved'), null, 'mesgs');
|
||||
} else {
|
||||
setEventMessages($object->error, null, 'errors');
|
||||
}
|
||||
} else {
|
||||
setEventMessages($object->error, null, 'errors');
|
||||
// Produkt zum Stundenzettel hinzufügen mit origin='omitted'
|
||||
$result = $object->addProduct(
|
||||
$fk_product,
|
||||
0, // fk_commandedet
|
||||
0, // fk_manager_line
|
||||
0, // qty_original
|
||||
$qty, // qty_done (Menge die entfällt)
|
||||
'omitted', // origin (entfällt)
|
||||
$description // description (Grund)
|
||||
);
|
||||
if ($result > 0) {
|
||||
setEventMessages($langs->trans('RecordSaved'), null, 'mesgs');
|
||||
} else {
|
||||
setEventMessages($object->error, null, 'errors');
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
|
@ -724,6 +782,9 @@ $_GET['mainmenu'] = 'stundenzettel';
|
|||
|
||||
llxHeader('', $title, '', '', 0, 0, '', '', '', 'mod-stundenzettel page-card');
|
||||
|
||||
// Mobile CSS einbinden
|
||||
print '<link rel="stylesheet" type="text/css" href="'.dol_buildpath('/stundenzettel/css/stundenzettel-mobile.css', 1).'?v='.filemtime(dol_buildpath('/stundenzettel/css/stundenzettel-mobile.css', 0)).'">';
|
||||
|
||||
// JavaScript für Mengenprüfung
|
||||
print '<script type="text/javascript">
|
||||
function plusQtyWithCheck(rowid, qtyOriginal, totalQtyAllStz) {
|
||||
|
|
@ -859,6 +920,11 @@ elseif ($object->id > 0) {
|
|||
// BEREICH: LEISTUNGEN (immer anzeigen außer bei Notizen)
|
||||
// =============================================
|
||||
if ($subtab != 'notes') {
|
||||
// Standard-Leistung vom Kunden laden
|
||||
$thirdparty = new Societe($db);
|
||||
$thirdparty->fetch($object->fk_soc);
|
||||
$thirdparty->fetch_optionals();
|
||||
|
||||
// Prüfen ob eine Leistung bearbeitet wird
|
||||
$editLeistungId = GETPOST('edit_leistung', 'int');
|
||||
|
||||
|
|
@ -876,6 +942,7 @@ elseif ($object->id > 0) {
|
|||
print '<th>'.$langs->trans("LeistungTimeStart").'</th>';
|
||||
print '<th>'.$langs->trans("LeistungTimeEnd").'</th>';
|
||||
print '<th class="center">'.$langs->trans("Duration").'</th>';
|
||||
print '<th>'.$langs->trans("DefaultService").'</th>';
|
||||
print '<th>'.$langs->trans("Description").'</th>';
|
||||
if ($object->status == Stundenzettel::STATUS_DRAFT && $permissiontoadd) {
|
||||
print '<th class="center" width="40"></th>'; // Edit
|
||||
|
|
@ -904,6 +971,10 @@ elseif ($object->id > 0) {
|
|||
print $hours.'h '.sprintf('%02d', $mins).'min';
|
||||
}
|
||||
print '</td>';
|
||||
// Service/Product Selection
|
||||
print '<td>';
|
||||
$form->select_produits($leistung->fk_product, 'fk_product', 1, 0, 0, -1, 2, '', 0, array(), 0, '1', 0, 'maxwidth200');
|
||||
print '</td>';
|
||||
print '<td><textarea name="leistung_description" class="flat" rows="5" style="width: 300px; resize: vertical;">'.dol_escape_htmltag($leistung->description).'</textarea></td>';
|
||||
// Save Button
|
||||
print '<td class="center">';
|
||||
|
|
@ -928,6 +999,14 @@ elseif ($object->id > 0) {
|
|||
print $hours.'h '.sprintf('%02d', $mins).'min';
|
||||
}
|
||||
print '</td>';
|
||||
// Leistungsposition anzeigen
|
||||
print '<td>';
|
||||
if (!empty($leistung->fk_product)) {
|
||||
print dol_escape_htmltag($leistung->product_label);
|
||||
} else {
|
||||
print '<span class="opacitymedium">'.$langs->trans("NotSet").'</span>';
|
||||
}
|
||||
print '</td>';
|
||||
print '<td>'.dol_htmlentitiesbr($leistung->description).'</td>';
|
||||
if ($object->status == Stundenzettel::STATUS_DRAFT && $permissiontoadd) {
|
||||
// Edit Button
|
||||
|
|
@ -943,7 +1022,7 @@ elseif ($object->id > 0) {
|
|||
}
|
||||
}
|
||||
} else {
|
||||
$colspan = ($object->status == Stundenzettel::STATUS_DRAFT && $permissiontoadd) ? 7 : 5;
|
||||
$colspan = ($object->status == Stundenzettel::STATUS_DRAFT && $permissiontoadd) ? 8 : 6;
|
||||
print '<tr class="oddeven"><td colspan="'.$colspan.'" class="opacitymedium">'.$langs->trans("NoRecordFound").'</td></tr>';
|
||||
}
|
||||
|
||||
|
|
@ -989,6 +1068,16 @@ elseif ($object->id > 0) {
|
|||
print '<td class="center">';
|
||||
print '</td>';
|
||||
|
||||
// Leistungsposition (Service/Dienstleistung)
|
||||
print '<td>';
|
||||
// Standard-Leistung vom Kunden holen falls vorhanden
|
||||
$defaultServiceId = 0;
|
||||
if (!empty($thirdparty->array_options['options_stundenzettel_default_service'])) {
|
||||
$defaultServiceId = $thirdparty->array_options['options_stundenzettel_default_service'];
|
||||
}
|
||||
$form->select_produits($defaultServiceId, 'fk_product', 1, 0, 0, -1, 2, '', 0, array(), 0, '1', 0, 'maxwidth200');
|
||||
print '</td>';
|
||||
|
||||
// Beschreibung (Tagesbeschreibung - größeres Textarea, 5 Zeilen)
|
||||
print '<td>';
|
||||
print '<textarea name="leistung_description" class="flat" rows="5" style="width: 300px; resize: vertical;" placeholder="'.$langs->trans("Description").'"></textarea>';
|
||||
|
|
@ -1012,7 +1101,7 @@ elseif ($object->id > 0) {
|
|||
if ($totalDuration > 0) {
|
||||
$hours = floor($totalDuration / 60);
|
||||
$mins = $totalDuration % 60;
|
||||
$colspan = ($object->status == Stundenzettel::STATUS_DRAFT && $permissiontoadd) ? 7 : 5;
|
||||
$colspan = ($object->status == Stundenzettel::STATUS_DRAFT && $permissiontoadd) ? 8 : 6;
|
||||
print '<tr class="liste_total">';
|
||||
print '<td colspan="3" class="right"><strong>'.$langs->trans("Total").'</strong></td>';
|
||||
print '<td class="center"><strong>'.$hours.'h '.sprintf('%02d', $mins).'min</strong></td>';
|
||||
|
|
@ -1237,6 +1326,13 @@ elseif ($object->id > 0) {
|
|||
print '<div class="div-table-responsive-no-min" style="margin-top: 15px;">';
|
||||
print '<table class="noborder centpercent">';
|
||||
print '<tr class="liste_titre"><th colspan="5" style="border-bottom: 2px solid #dee2e6;"><strong>'.$langs->trans("Entfaellt").'</strong> - <span class="opacitymedium" style="font-weight:normal;">'.$langs->trans("EntfaelltDesc").'</span></th></tr>';
|
||||
print '<tr class="liste_titre">';
|
||||
print '<th>'.$langs->trans("Product").'</th>';
|
||||
print '<th class="center" style="width:80px;">'.$langs->trans("Qty").'</th>';
|
||||
print '<th style="width:200px;">'.$langs->trans("Reason").'</th>';
|
||||
print '<th class="center" style="width:40px;"></th>'; // Save
|
||||
print '<th class="center" style="width:40px;"></th>'; // Delete
|
||||
print '</tr>';
|
||||
|
||||
// Zuerst bereits erfasste Entfällt-Produkte anzeigen
|
||||
$entfaelltProducts = array();
|
||||
|
|
@ -1354,28 +1450,76 @@ elseif ($object->id > 0) {
|
|||
print '<input type="hidden" name="action" value="add_entfaellt">';
|
||||
|
||||
// Produkt-Auswahl - NUR Produkte aus dem Auftrag mit verfügbarer Menge
|
||||
print '<td>';
|
||||
if ($hasAvailableProducts) {
|
||||
print '<select name="entfaellt_product" class="flat minwidth300" id="entfaellt_product_select" onchange="updateMaxQty(this)">';
|
||||
print '<option value="" data-max="1">-- '.$langs->trans("SelectProducts").' --</option>';
|
||||
foreach ($orderProducts as $op) {
|
||||
// Nur Produkte anzeigen wo noch etwas entfallen kann
|
||||
if ($op->qty_available <= 0) continue;
|
||||
|
||||
if ($op->fk_product > 0) {
|
||||
print '<option value="'.$op->fk_product.'" data-commandedet="'.$op->rowid.'" data-max="'.((int)$op->qty_available).'">';
|
||||
print $op->product_ref.' - '.$op->product_label;
|
||||
print ' ('.$langs->trans("QtyRemaining").': '.((int)$op->qty_available).')';
|
||||
print '</option>';
|
||||
} else {
|
||||
// Freitext-Positionen aus dem Auftrag - Beschreibung als Bezeichnung
|
||||
$desc = strip_tags($op->description);
|
||||
$descShort = (strlen($desc) > 50) ? substr($desc, 0, 47).'...' : $desc;
|
||||
print '<option value="freetext_'.$op->rowid.'" data-commandedet="'.$op->rowid.'" data-description="'.dol_escape_htmltag($desc).'" data-max="'.((int)$op->qty_available).'">';
|
||||
print $descShort.' ('.$langs->trans("QtyRemaining").': '.((int)$op->qty_available).')';
|
||||
print '</option>';
|
||||
// Mehraufwand-Produkte mit verbleibender Menge laden (aus allen Stundenzetteln des Auftrags)
|
||||
$mehraufwandAvailable = array();
|
||||
if ($object->fk_commande > 0) {
|
||||
$sqlMehraufwand = "SELECT sp.rowid, sp.fk_product, sp.qty, sp.qty_done, sp.description,";
|
||||
$sqlMehraufwand .= " p.ref as product_ref, p.label as product_label";
|
||||
$sqlMehraufwand .= " FROM ".MAIN_DB_PREFIX."stundenzettel_product sp";
|
||||
$sqlMehraufwand .= " JOIN ".MAIN_DB_PREFIX."stundenzettel s ON s.rowid = sp.fk_stundenzettel";
|
||||
$sqlMehraufwand .= " LEFT JOIN ".MAIN_DB_PREFIX."product p ON p.rowid = sp.fk_product";
|
||||
$sqlMehraufwand .= " WHERE s.fk_commande = ".((int)$object->fk_commande);
|
||||
$sqlMehraufwand .= " AND sp.origin = 'additional'";
|
||||
$sqlMehraufwand .= " AND (sp.qty - sp.qty_done) > 0"; // Nur wo noch etwas verfügbar ist
|
||||
$sqlMehraufwand .= " ORDER BY p.label, sp.description";
|
||||
$resqlMehraufwand = $db->query($sqlMehraufwand);
|
||||
if ($resqlMehraufwand) {
|
||||
while ($objM = $db->fetch_object($resqlMehraufwand)) {
|
||||
$objM->qty_available = $objM->qty - $objM->qty_done;
|
||||
$mehraufwandAvailable[] = $objM;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$hasMehraufwandProducts = (count($mehraufwandAvailable) > 0);
|
||||
|
||||
print '<td>';
|
||||
if ($hasAvailableProducts || $hasMehraufwandProducts) {
|
||||
print '<select name="entfaellt_product" class="flat minwidth300" id="entfaellt_product_select" onchange="updateMaxQty(this)">';
|
||||
print '<option value="" data-max="1">-- '.$langs->trans("SelectProducts").' --</option>';
|
||||
|
||||
// Produkte aus Auftrag
|
||||
if ($hasAvailableProducts) {
|
||||
print '<optgroup label="'.$langs->trans("FromOrder").'">';
|
||||
foreach ($orderProducts as $op) {
|
||||
if ($op->qty_available <= 0) continue;
|
||||
|
||||
if ($op->fk_product > 0) {
|
||||
print '<option value="'.$op->fk_product.'" data-commandedet="'.$op->rowid.'" data-max="'.((int)$op->qty_available).'">';
|
||||
print $op->product_ref.' - '.$op->product_label;
|
||||
print ' ('.$langs->trans("QtyRemaining").': '.((int)$op->qty_available).')';
|
||||
print '</option>';
|
||||
} else {
|
||||
$desc = strip_tags($op->description);
|
||||
$descShort = (strlen($desc) > 50) ? substr($desc, 0, 47).'...' : $desc;
|
||||
print '<option value="freetext_'.$op->rowid.'" data-commandedet="'.$op->rowid.'" data-description="'.dol_escape_htmltag($desc).'" data-max="'.((int)$op->qty_available).'">';
|
||||
print $descShort.' ('.$langs->trans("QtyRemaining").': '.((int)$op->qty_available).')';
|
||||
print '</option>';
|
||||
}
|
||||
}
|
||||
print '</optgroup>';
|
||||
}
|
||||
|
||||
// Mehraufwand-Produkte mit verbleibender Menge
|
||||
if ($hasMehraufwandProducts) {
|
||||
print '<optgroup label="'.$langs->trans("Mehraufwand").'">';
|
||||
foreach ($mehraufwandAvailable as $mp) {
|
||||
if ($mp->fk_product > 0) {
|
||||
print '<option value="mehraufwand_'.$mp->rowid.'" data-max="'.((int)$mp->qty_available).'">';
|
||||
print $mp->product_ref.' - '.$mp->product_label;
|
||||
print ' ('.$langs->trans("QtyRemaining").': '.((int)$mp->qty_available).')';
|
||||
print '</option>';
|
||||
} else {
|
||||
$desc = strip_tags($mp->description);
|
||||
$descShort = (strlen($desc) > 50) ? substr($desc, 0, 47).'...' : $desc;
|
||||
print '<option value="mehraufwand_'.$mp->rowid.'" data-description="'.dol_escape_htmltag($desc).'" data-max="'.((int)$mp->qty_available).'">';
|
||||
print $descShort.' ('.$langs->trans("QtyRemaining").': '.((int)$mp->qty_available).')';
|
||||
print '</option>';
|
||||
}
|
||||
}
|
||||
print '</optgroup>';
|
||||
}
|
||||
|
||||
print '</select>';
|
||||
} else {
|
||||
print '<small class="opacitymedium">'.$langs->trans("AllProductsUsedOrOmitted").'</small>';
|
||||
|
|
@ -1389,7 +1533,7 @@ elseif ($object->id > 0) {
|
|||
|
||||
// Grund
|
||||
print '<td style="width:200px;">';
|
||||
print '<input type="text" name="entfaellt_description" class="flat" style="width:190px;" placeholder="'.$langs->trans("Reason").'">';
|
||||
print '<input type="text" name="entfaellt_description" class="flat" style="width:100%;" placeholder="'.$langs->trans("Reason").'">';
|
||||
print '</td>';
|
||||
|
||||
// Hinzufügen-Button (colspan für beide Button-Spalten)
|
||||
|
|
@ -1549,9 +1693,9 @@ elseif ($object->id > 0) {
|
|||
print '<input type="number" name="mehraufwand_qty" class="flat" style="width:70px; text-align:center;" value="1" min="1">';
|
||||
print '</td>';
|
||||
|
||||
// Grund (optional)
|
||||
// Grund
|
||||
print '<td style="width:200px;">';
|
||||
print '<input type="text" name="mehraufwand_reason" class="flat" style="width:100%;" placeholder="'.$langs->trans("Reason").' ('.$langs->trans("Optional").')">';
|
||||
print '<input type="text" name="mehraufwand_reason" class="flat" style="width:100%;" placeholder="'.$langs->trans("Reason").'">';
|
||||
print '</td>';
|
||||
|
||||
// Hinzufügen-Button (colspan für Save, Delete)
|
||||
|
|
|
|||
|
|
@ -92,6 +92,16 @@ class Stundenzettel extends CommonObject
|
|||
*/
|
||||
public $note_public;
|
||||
|
||||
/**
|
||||
* @var float|null Stundenpreis (NULL = Standard verwenden)
|
||||
*/
|
||||
public $hourly_rate;
|
||||
|
||||
/**
|
||||
* @var int 1 = Stundenpreis wurde manuell geändert
|
||||
*/
|
||||
public $hourly_rate_is_custom = 0;
|
||||
|
||||
/**
|
||||
* @var array Leistungen
|
||||
*/
|
||||
|
|
@ -139,7 +149,8 @@ class Stundenzettel extends CommonObject
|
|||
|
||||
$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 .= "date_stundenzettel, datec, status, note_private, note_public,";
|
||||
$sql .= "hourly_rate, hourly_rate_is_custom";
|
||||
$sql .= ") VALUES (";
|
||||
$sql .= "'".$this->db->escape($this->ref)."',";
|
||||
$sql .= ((int)$conf->entity).",";
|
||||
|
|
@ -150,7 +161,9 @@ class Stundenzettel extends CommonObject
|
|||
$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 .= ($this->note_public ? "'".$this->db->escape($this->note_public)."'" : "NULL").",";
|
||||
$sql .= (isset($this->hourly_rate) && $this->hourly_rate !== null ? ((float)$this->hourly_rate) : "NULL").",";
|
||||
$sql .= ((int)$this->hourly_rate_is_custom);
|
||||
$sql .= ")";
|
||||
|
||||
dol_syslog(get_class($this)."::create", LOG_DEBUG);
|
||||
|
|
@ -184,7 +197,8 @@ class Stundenzettel extends CommonObject
|
|||
{
|
||||
$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 .= " s.date_valid, s.status, s.note_private, s.note_public, s.tms,";
|
||||
$sql .= " s.hourly_rate, s.hourly_rate_is_custom";
|
||||
$sql .= " FROM ".MAIN_DB_PREFIX.$this->table_element." as s";
|
||||
if ($ref) {
|
||||
$sql .= " WHERE s.ref = '".$this->db->escape($ref)."'";
|
||||
|
|
@ -212,6 +226,8 @@ class Stundenzettel extends CommonObject
|
|||
$this->status = $obj->status;
|
||||
$this->note_private = $obj->note_private;
|
||||
$this->note_public = $obj->note_public;
|
||||
$this->hourly_rate = $obj->hourly_rate;
|
||||
$this->hourly_rate_is_custom = $obj->hourly_rate_is_custom;
|
||||
|
||||
// Load lines
|
||||
$this->fetchLeistungen();
|
||||
|
|
@ -242,7 +258,9 @@ class Stundenzettel extends CommonObject
|
|||
$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 .= " note_public = ".($this->note_public ? "'".$this->db->escape($this->note_public)."'" : "NULL").",";
|
||||
$sql .= " hourly_rate = ".(isset($this->hourly_rate) && $this->hourly_rate !== null && $this->hourly_rate !== '' ? ((float)$this->hourly_rate) : "NULL").",";
|
||||
$sql .= " hourly_rate_is_custom = ".((int)$this->hourly_rate_is_custom);
|
||||
$sql .= " WHERE rowid = ".((int)$this->id);
|
||||
|
||||
dol_syslog(get_class($this)."::update", LOG_DEBUG);
|
||||
|
|
@ -378,10 +396,12 @@ class Stundenzettel extends CommonObject
|
|||
{
|
||||
$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";
|
||||
$sql = "SELECT l.rowid, l.fk_user, l.fk_product, l.date_leistung, l.time_start, l.time_end, l.duration, l.description, l.rang,";
|
||||
$sql .= " p.ref as product_ref, p.label as product_label";
|
||||
$sql .= " FROM ".MAIN_DB_PREFIX."stundenzettel_leistung as l";
|
||||
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."product as p ON l.fk_product = p.rowid";
|
||||
$sql .= " WHERE l.fk_stundenzettel = ".((int)$this->id);
|
||||
$sql .= " ORDER BY l.rang, l.date_leistung, l.time_start";
|
||||
|
||||
$resql = $this->db->query($sql);
|
||||
if ($resql) {
|
||||
|
|
@ -432,7 +452,7 @@ class Stundenzettel extends CommonObject
|
|||
* @param string $description Description
|
||||
* @return int <0 if KO, >0 if OK
|
||||
*/
|
||||
public function addLeistung($user, $date, $time_start = null, $time_end = null, $description = '')
|
||||
public function addLeistung($user, $date, $time_start = null, $time_end = null, $description = '', $fk_product = null)
|
||||
{
|
||||
global $langs;
|
||||
|
||||
|
|
@ -475,10 +495,11 @@ class Stundenzettel extends CommonObject
|
|||
}
|
||||
|
||||
$sql = "INSERT INTO ".MAIN_DB_PREFIX."stundenzettel_leistung (";
|
||||
$sql .= "fk_stundenzettel, fk_user, date_leistung, time_start, time_end, duration, description, rang";
|
||||
$sql .= "fk_stundenzettel, fk_user, fk_product, date_leistung, time_start, time_end, duration, description, rang";
|
||||
$sql .= ") VALUES (";
|
||||
$sql .= ((int)$this->id).",";
|
||||
$sql .= ((int)$user->id).",";
|
||||
$sql .= ($fk_product > 0 ? ((int)$fk_product) : "NULL").",";
|
||||
$sql .= "'".$this->db->idate($date)."',";
|
||||
$sql .= ($time_start ? "'".$this->db->escape($time_start)."'" : "NULL").",";
|
||||
$sql .= ($time_end ? "'".$this->db->escape($time_end)."'" : "NULL").",";
|
||||
|
|
@ -502,9 +523,10 @@ class Stundenzettel extends CommonObject
|
|||
* @param string $time_start Start time
|
||||
* @param string $time_end End time
|
||||
* @param string $description Description
|
||||
* @param int $fk_product Product/Service ID
|
||||
* @return int <0 if KO, >0 if OK
|
||||
*/
|
||||
public function updateLeistung($leistung_id, $date, $time_start = null, $time_end = null, $description = '')
|
||||
public function updateLeistung($leistung_id, $date, $time_start = null, $time_end = null, $description = '', $fk_product = null)
|
||||
{
|
||||
global $langs;
|
||||
|
||||
|
|
@ -539,6 +561,7 @@ class Stundenzettel extends CommonObject
|
|||
}
|
||||
|
||||
$sql = "UPDATE ".MAIN_DB_PREFIX."stundenzettel_leistung SET";
|
||||
$sql .= " fk_product = ".($fk_product > 0 ? ((int)$fk_product) : "NULL").",";
|
||||
$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").",";
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ class modStundenzettel extends DolibarrModules
|
|||
$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';
|
||||
$this->version = '1.2.0';
|
||||
|
||||
// Autor
|
||||
$this->editor_name = 'Data IT Solution';
|
||||
|
|
@ -107,7 +107,7 @@ class modStundenzettel extends DolibarrModules
|
|||
|
||||
// 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__'
|
||||
'order:+stundenzettel:Stundenzettel:stundenzettel@stundenzettel:$user->hasRight("stundenzettel","read"):/custom/stundenzettel/stundenzettel_commande.php?id=__ID__&tab=products&noredirect=1'
|
||||
);
|
||||
|
||||
// Boxen/Widgets
|
||||
|
|
@ -320,6 +320,9 @@ class modStundenzettel extends DolibarrModules
|
|||
// Extrafeld "Standard-Leistung" für Kunden anlegen
|
||||
$this->createExtraFieldDefaultService();
|
||||
|
||||
// Stundenpreis-Felder hinzufügen (Update 1.2.0)
|
||||
$this->addHourlyRateFields();
|
||||
|
||||
$sql = array();
|
||||
|
||||
return $this->_init($sql, $options);
|
||||
|
|
@ -454,6 +457,35 @@ class modStundenzettel extends DolibarrModules
|
|||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fügt die Leistungsposition zur Leistungstabelle hinzu (Update 1.2.0)
|
||||
* Wird bei jeder Modulaktivierung ausgeführt - IF NOT EXISTS verhindert Fehler
|
||||
*
|
||||
* @return int 1 if OK, -1 if error
|
||||
*/
|
||||
private function addHourlyRateFields()
|
||||
{
|
||||
// Spalte fk_product zur Leistungstabelle hinzufügen falls nicht vorhanden
|
||||
$sql1 = "ALTER TABLE ".MAIN_DB_PREFIX."stundenzettel_leistung
|
||||
ADD COLUMN IF NOT EXISTS fk_product INTEGER DEFAULT NULL
|
||||
COMMENT 'Verknüpfung zur Leistungsposition (Dienstleistung)'";
|
||||
|
||||
$resql1 = $this->db->query($sql1);
|
||||
if (!$resql1) {
|
||||
// Fallback für ältere MySQL-Versionen ohne IF NOT EXISTS
|
||||
$sql1b = "SHOW COLUMNS FROM ".MAIN_DB_PREFIX."stundenzettel_leistung LIKE 'fk_product'";
|
||||
$resql1b = $this->db->query($sql1b);
|
||||
if ($resql1b && $this->db->num_rows($resql1b) == 0) {
|
||||
$sql1c = "ALTER TABLE ".MAIN_DB_PREFIX."stundenzettel_leistung
|
||||
ADD COLUMN fk_product INTEGER DEFAULT NULL";
|
||||
$this->db->query($sql1c);
|
||||
}
|
||||
}
|
||||
|
||||
dol_syslog("modStundenzettel::addHourlyRateFields Service product field checked/added", LOG_DEBUG);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Funktion beim Deaktivieren des Moduls
|
||||
*
|
||||
|
|
|
|||
330
css/stundenzettel-mobile.css
Normal file
330
css/stundenzettel-mobile.css
Normal file
|
|
@ -0,0 +1,330 @@
|
|||
/**
|
||||
* Stundenzettel Mobile CSS
|
||||
* Responsive Styles für Touch-Geräte
|
||||
*
|
||||
* Wird automatisch bei Bildschirmbreite < 768px aktiv
|
||||
*/
|
||||
|
||||
/* ============================================
|
||||
MOBILE STYLES (max-width: 768px)
|
||||
============================================ */
|
||||
@media screen and (max-width: 768px) {
|
||||
|
||||
/* Allgemeine Anpassungen */
|
||||
.mod-stundenzettel .fichecenter {
|
||||
padding: 0 5px;
|
||||
}
|
||||
|
||||
/* Tabs kompakter */
|
||||
.mod-stundenzettel .tabsElem a {
|
||||
padding: 8px 10px !important;
|
||||
font-size: 12px !important;
|
||||
}
|
||||
|
||||
/* Banner kompakter */
|
||||
.mod-stundenzettel .arearef {
|
||||
padding: 10px !important;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
LEISTUNGEN TABELLE -> KARTEN
|
||||
============================================ */
|
||||
.mod-stundenzettel table.noborder tr.liste_titre th {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Jede Zeile wird zur Karte */
|
||||
.mod-stundenzettel table.noborder tr.oddeven {
|
||||
display: block;
|
||||
background: #fff;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
margin: 10px 0;
|
||||
padding: 12px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.mod-stundenzettel table.noborder tr.oddeven td {
|
||||
display: block;
|
||||
text-align: left !important;
|
||||
padding: 4px 0;
|
||||
border: none !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
/* Labels vor den Werten (via CSS content) */
|
||||
.mod-stundenzettel table.noborder tr.oddeven td:before {
|
||||
content: attr(data-label);
|
||||
font-weight: bold;
|
||||
color: #666;
|
||||
display: inline-block;
|
||||
min-width: 80px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
/* Zeit-Inputs nebeneinander */
|
||||
.mod-stundenzettel input[type="time"],
|
||||
.mod-stundenzettel select[name="time_start"],
|
||||
.mod-stundenzettel select[name="time_end"] {
|
||||
width: 45% !important;
|
||||
display: inline-block;
|
||||
font-size: 16px !important; /* Verhindert Zoom auf iOS */
|
||||
padding: 10px !important;
|
||||
}
|
||||
|
||||
/* Textarea volle Breite */
|
||||
.mod-stundenzettel textarea {
|
||||
width: 100% !important;
|
||||
min-height: 80px;
|
||||
font-size: 16px !important;
|
||||
padding: 10px !important;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Number Inputs größer */
|
||||
.mod-stundenzettel input[type="number"] {
|
||||
width: 80px !important;
|
||||
font-size: 18px !important;
|
||||
padding: 12px 8px !important;
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
/* Text Inputs */
|
||||
.mod-stundenzettel input[type="text"] {
|
||||
width: 100% !important;
|
||||
font-size: 16px !important;
|
||||
padding: 10px !important;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
BUTTONS - TOUCH-FREUNDLICH
|
||||
============================================ */
|
||||
.mod-stundenzettel .fas,
|
||||
.mod-stundenzettel .far {
|
||||
font-size: 1.5em !important;
|
||||
padding: 8px !important;
|
||||
}
|
||||
|
||||
/* Action Buttons (Plus/Minus/Save/Delete) */
|
||||
.mod-stundenzettel td .fa-plus-circle,
|
||||
.mod-stundenzettel td .fa-minus-circle,
|
||||
.mod-stundenzettel td .fa-save,
|
||||
.mod-stundenzettel td .fa-trash,
|
||||
.mod-stundenzettel td .fa-pencil-alt {
|
||||
font-size: 1.8em !important;
|
||||
padding: 12px !important;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/* Button-Reihe horizontal */
|
||||
.mod-stundenzettel .mobile-actions {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
margin-top: 10px;
|
||||
padding-top: 10px;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
PRODUKTE SECTION
|
||||
============================================ */
|
||||
|
||||
/* Produkt-Karte */
|
||||
.mod-stundenzettel table.noborder tr.oddeven td:first-child {
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
padding-bottom: 8px !important;
|
||||
border-bottom: 1px solid #eee !important;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
/* Mengen-Steuerung als Inline-Block */
|
||||
.mod-stundenzettel .qty-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 15px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
/* Plus/Minus Buttons größer */
|
||||
.mod-stundenzettel a[onclick*="plusQty"],
|
||||
.mod-stundenzettel a[onclick*="Qty"] {
|
||||
padding: 15px !important;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
SUMMENZEILEN
|
||||
============================================ */
|
||||
.mod-stundenzettel tr.liste_total {
|
||||
display: block;
|
||||
background: #f8f9fa;
|
||||
border: 2px solid #28a745;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
margin: 10px 0;
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
.mod-stundenzettel tr.liste_total td {
|
||||
display: inline-block;
|
||||
text-align: center !important;
|
||||
padding: 5px !important;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
SECTION HEADER
|
||||
============================================ */
|
||||
.mod-stundenzettel h3 {
|
||||
font-size: 18px;
|
||||
padding: 10px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border-radius: 8px;
|
||||
margin: 15px 0 10px 0;
|
||||
}
|
||||
|
||||
.mod-stundenzettel tr.liste_titre th[colspan] {
|
||||
display: block !important;
|
||||
font-size: 16px;
|
||||
padding: 12px !important;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
margin: 15px 0 5px 0;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
FORMULAR-CARDS (Hinzufügen)
|
||||
============================================ */
|
||||
.mod-stundenzettel tr.oddeven form {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.mod-stundenzettel tr.oddeven td[colspan] {
|
||||
padding: 10px 0 !important;
|
||||
}
|
||||
|
||||
/* Produkt-Auswahl Dropdown */
|
||||
.mod-stundenzettel select.minwidth200,
|
||||
.mod-stundenzettel select.minwidth300 {
|
||||
width: 100% !important;
|
||||
font-size: 16px !important;
|
||||
padding: 12px !important;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
MERKZETTEL/NOTIZEN
|
||||
============================================ */
|
||||
.mod-stundenzettel .fa-check-square,
|
||||
.mod-stundenzettel .fa-square {
|
||||
font-size: 1.8em !important;
|
||||
padding: 10px !important;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
BADGES
|
||||
============================================ */
|
||||
.mod-stundenzettel .badge {
|
||||
font-size: 12px !important;
|
||||
padding: 4px 8px !important;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
ACTION BUTTONS (Freigeben/Löschen)
|
||||
============================================ */
|
||||
.mod-stundenzettel .tabsAction {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.mod-stundenzettel .tabsAction a.butAction,
|
||||
.mod-stundenzettel .tabsAction a.butActionDelete {
|
||||
flex: 1 1 45%;
|
||||
text-align: center;
|
||||
padding: 15px 10px !important;
|
||||
font-size: 14px !important;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
HIDE UNNECESSARY ELEMENTS ON MOBILE
|
||||
============================================ */
|
||||
.mod-stundenzettel .opacitymedium small {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
SELECT2 DROPDOWNS FIX
|
||||
============================================ */
|
||||
.mod-stundenzettel .select2-container {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.mod-stundenzettel .select2-selection {
|
||||
min-height: 44px !important;
|
||||
font-size: 16px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
EXTRA SMALL DEVICES (< 480px)
|
||||
============================================ */
|
||||
@media screen and (max-width: 480px) {
|
||||
|
||||
/* Noch kompaktere Tabs */
|
||||
.mod-stundenzettel .tabsElem a {
|
||||
padding: 6px 8px !important;
|
||||
font-size: 11px !important;
|
||||
}
|
||||
|
||||
/* Buttons nebeneinander */
|
||||
.mod-stundenzettel .tabsAction a.butAction,
|
||||
.mod-stundenzettel .tabsAction a.butActionDelete {
|
||||
flex: 1 1 100%;
|
||||
}
|
||||
|
||||
/* Zeit-Inputs untereinander */
|
||||
.mod-stundenzettel input[type="time"],
|
||||
.mod-stundenzettel select[name="time_start"],
|
||||
.mod-stundenzettel select[name="time_end"] {
|
||||
width: 100% !important;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
LANDSCAPE MODE ADJUSTMENTS
|
||||
============================================ */
|
||||
@media screen and (max-width: 768px) and (orientation: landscape) {
|
||||
.mod-stundenzettel table.noborder tr.oddeven {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.mod-stundenzettel table.noborder tr.oddeven td {
|
||||
flex: 1 1 auto;
|
||||
min-width: 120px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
TOUCH FEEDBACK
|
||||
============================================ */
|
||||
@media (hover: none) and (pointer: coarse) {
|
||||
.mod-stundenzettel a:active,
|
||||
.mod-stundenzettel button:active {
|
||||
opacity: 0.7;
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.mod-stundenzettel .fas:active,
|
||||
.mod-stundenzettel .far:active {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
|
|
@ -217,7 +217,7 @@ ResetStundenzettel = Status zurücksetzen
|
|||
StundenzettelReset = Stundenzettel-Status wurde zurückgesetzt
|
||||
|
||||
# Standard-Leistung
|
||||
DefaultService = Standard-Leistung
|
||||
DefaultService = Leistungsposition
|
||||
DefaultServiceDesc = Standard-Dienstleistung für Stundenzettel (wird beim Kunden hinterlegt)
|
||||
DefaultServiceFromCustomer = Standard-Leistung vom Kunden
|
||||
NoDefaultServiceSet = Keine Standard-Leistung hinterlegt
|
||||
|
|
@ -225,9 +225,27 @@ SetDefaultServiceInCustomer = Standard-Leistung beim Kunden hinterlegen
|
|||
PlannedHours = Geplante Stunden
|
||||
TotalHours = Gesamtstunden
|
||||
HoursFromOrder = Stunden aus Auftrag
|
||||
NotSet = Nicht gesetzt
|
||||
SelectService = Leistungsposition wählen
|
||||
|
||||
# Stundenpreis
|
||||
HourlyRate = Stundenpreis
|
||||
CustomPrice = Abweichender Preis
|
||||
CustomerPrice = Kundenpreis
|
||||
StandardPrice = Standardpreis
|
||||
CustomHourlyRateSet = Es wurde ein abweichender Stundenpreis gesetzt
|
||||
CustomerSpecificPrice = Kundenspezifischer Preis
|
||||
StandardServicePrice = Standard-Produktpreis
|
||||
ResetToDefault = Auf Standard zurücksetzen
|
||||
Reset = Zurücksetzen
|
||||
|
||||
# Rechnungsübernahme Stunden
|
||||
InvoiceHoursMode = Stunden-Übernahme
|
||||
InvoiceHoursModeTotal = Gesamtstunden auf einer Zeile
|
||||
InvoiceHoursModePerDay = Pro Tag eine Zeile
|
||||
SelectInvoiceHoursMode = Wie sollen die Arbeitsstunden übernommen werden?
|
||||
|
||||
# Lieferauflistung Leistungen
|
||||
perStundenzettel = pro Stundenzettel
|
||||
Entries = Einträge
|
||||
incl = inkl.
|
||||
|
|
|
|||
|
|
@ -87,3 +87,182 @@ function stundenzettel_prepare_head($object)
|
|||
|
||||
return $head;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare array of tabs for Stundenzettel Commande page (order-level view)
|
||||
* Diese Tabs werden immer angezeigt, unabhängig davon ob ein Stundenzettel ausgewählt ist
|
||||
*
|
||||
* @param Commande $order Das Auftrags-Objekt
|
||||
* @param int $stundenzettel_id Optional: ID des ausgewählten Stundenzettels
|
||||
* @return array Array of tabs
|
||||
*/
|
||||
function stundenzettel_commande_prepare_head($order, $stundenzettel_id = 0)
|
||||
{
|
||||
global $db, $langs, $conf, $user;
|
||||
|
||||
$langs->load("stundenzettel@stundenzettel");
|
||||
|
||||
$h = 0;
|
||||
$head = array();
|
||||
|
||||
// Tab 1: Kundenauftrag (Link zurück zum Auftrag)
|
||||
$head[$h][0] = DOL_URL_ROOT.'/commande/card.php?id='.$order->id;
|
||||
$head[$h][1] = $langs->trans("Order");
|
||||
$head[$h][2] = 'order';
|
||||
$h++;
|
||||
|
||||
// Tab 2: Produktliste
|
||||
$head[$h][0] = dol_buildpath('/stundenzettel/stundenzettel_commande.php', 1).'?id='.$order->id.'&tab=products&noredirect=1'.($stundenzettel_id > 0 ? '&stundenzettel_id='.$stundenzettel_id : '');
|
||||
$head[$h][1] = $langs->trans("ProductList");
|
||||
$head[$h][2] = 'products';
|
||||
$h++;
|
||||
|
||||
// Tab 3: Stundenzettel (aktueller/ausgewählter Stundenzettel - card.php)
|
||||
// Wenn kein Stundenzettel ausgewählt, den letzten offenen für diesen Auftrag suchen
|
||||
$activeStundenzettelId = $stundenzettel_id;
|
||||
if ($activeStundenzettelId <= 0) {
|
||||
$sqlActive = "SELECT rowid FROM ".MAIN_DB_PREFIX."stundenzettel";
|
||||
$sqlActive .= " WHERE fk_commande = ".((int)$order->id);
|
||||
$sqlActive .= " AND status = 0"; // Nur Entwürfe
|
||||
$sqlActive .= " ORDER BY date_stundenzettel DESC, rowid DESC";
|
||||
$sqlActive .= " LIMIT 1";
|
||||
$resqlActive = $db->query($sqlActive);
|
||||
if ($resqlActive && $db->num_rows($resqlActive) > 0) {
|
||||
$objActive = $db->fetch_object($resqlActive);
|
||||
$activeStundenzettelId = $objActive->rowid;
|
||||
}
|
||||
}
|
||||
|
||||
if ($activeStundenzettelId > 0) {
|
||||
// Lade Stundenzettel für Badge-Berechnung
|
||||
require_once dol_buildpath('/stundenzettel/class/stundenzettel.class.php', 0);
|
||||
$tmpStz = new Stundenzettel($db);
|
||||
$tmpStz->fetch($activeStundenzettelId);
|
||||
$tmpStz->fetchLeistungen();
|
||||
$tmpStz->fetchProducts();
|
||||
|
||||
$nbItems = count($tmpStz->leistungen) + count($tmpStz->products);
|
||||
|
||||
$head[$h][0] = dol_buildpath('/stundenzettel/card.php', 1).'?id='.$activeStundenzettelId;
|
||||
$head[$h][1] = $langs->trans("Stundenzettel");
|
||||
if ($nbItems > 0) {
|
||||
$head[$h][1] .= '<span class="badge marginleftonlyshort">'.$nbItems.'</span>';
|
||||
}
|
||||
$head[$h][2] = 'card';
|
||||
$h++;
|
||||
}
|
||||
|
||||
// Tab 4: Alle Stundenzettel (Liste aller Stundenzettel für diesen Auftrag)
|
||||
$sql = "SELECT COUNT(*) as nb FROM ".MAIN_DB_PREFIX."stundenzettel WHERE fk_commande = ".((int)$order->id);
|
||||
$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='.$order->id.'&tab=stundenzettel&noredirect=1'.($stundenzettel_id > 0 ? '&stundenzettel_id='.$stundenzettel_id : '');
|
||||
$head[$h][1] = $langs->trans("StundenzettelList");
|
||||
if ($nbStundenzettel > 0) {
|
||||
$head[$h][1] .= '<span class="badge marginleftonlyshort">'.$nbStundenzettel.'</span>';
|
||||
}
|
||||
$head[$h][2] = 'stundenzettel';
|
||||
$h++;
|
||||
|
||||
// Tab 4: Lieferauflistung (Tracking)
|
||||
$head[$h][0] = dol_buildpath('/stundenzettel/stundenzettel_commande.php', 1).'?id='.$order->id.'&tab=tracking&noredirect=1'.($stundenzettel_id > 0 ? '&stundenzettel_id='.$stundenzettel_id : '');
|
||||
$head[$h][1] = $langs->trans("DeliveryTracking");
|
||||
$head[$h][2] = 'tracking';
|
||||
$h++;
|
||||
|
||||
return $head;
|
||||
}
|
||||
|
||||
/**
|
||||
* Holt den kundenspezifischen Preis für ein Produkt
|
||||
* Falls kein kundenspezifischer Preis existiert, wird der Standardpreis zurückgegeben
|
||||
*
|
||||
* @param DoliDB $db Datenbankverbindung
|
||||
* @param int $fk_product Produkt-ID
|
||||
* @param int $fk_soc Kunden-ID (Societe)
|
||||
* @param Product|null $product Optional: bereits geladenes Produkt-Objekt
|
||||
* @return array Array mit 'price' (HT), 'price_ttc', 'tva_tx', 'price_base_type', 'is_customer_price'
|
||||
*/
|
||||
function getCustomerPrice($db, $fk_product, $fk_soc, $product = null) {
|
||||
global $conf;
|
||||
|
||||
$now = dol_now();
|
||||
|
||||
// Suche kundenspezifischen Preis in der Tabelle product_customer_price
|
||||
$sql = "SELECT price, price_ttc, tva_tx, price_base_type";
|
||||
$sql .= " FROM ".MAIN_DB_PREFIX."product_customer_price";
|
||||
$sql .= " WHERE fk_product = ".((int)$fk_product);
|
||||
$sql .= " AND fk_soc = ".((int)$fk_soc);
|
||||
$sql .= " AND entity IN (".getEntity('productprice').")";
|
||||
// Prüfe Gültigkeitszeitraum (date_begin <= now und (date_end IS NULL oder date_end >= now))
|
||||
$sql .= " AND date_begin <= '".$db->idate($now)."'";
|
||||
$sql .= " AND (date_end IS NULL OR date_end >= '".$db->idate($now)."')";
|
||||
$sql .= " ORDER BY date_begin DESC";
|
||||
$sql .= " LIMIT 1";
|
||||
|
||||
$resql = $db->query($sql);
|
||||
if ($resql && $db->num_rows($resql) > 0) {
|
||||
$obj = $db->fetch_object($resql);
|
||||
return array(
|
||||
'price' => (float)$obj->price,
|
||||
'price_ttc' => (float)$obj->price_ttc,
|
||||
'tva_tx' => (float)$obj->tva_tx,
|
||||
'price_base_type' => $obj->price_base_type,
|
||||
'is_customer_price' => true
|
||||
);
|
||||
}
|
||||
|
||||
// Kein kundenspezifischer Preis gefunden - lade Standardpreis vom Produkt
|
||||
if ($product === null) {
|
||||
require_once DOL_DOCUMENT_ROOT.'/product/class/product.class.php';
|
||||
$product = new Product($db);
|
||||
$product->fetch($fk_product);
|
||||
}
|
||||
|
||||
return array(
|
||||
'price' => (float)$product->price,
|
||||
'price_ttc' => (float)$product->price_ttc,
|
||||
'tva_tx' => (float)$product->tva_tx,
|
||||
'price_base_type' => $product->price_base_type,
|
||||
'is_customer_price' => false
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Holt den effektiven Stundenpreis für einen Stundenzettel
|
||||
* Berücksichtigt: 1. Manuell gesetzter Preis im Stundenzettel
|
||||
* 2. Kundenspezifischer Preis
|
||||
* 3. Standard-Produktpreis
|
||||
*
|
||||
* @param DoliDB $db Datenbankverbindung
|
||||
* @param Stundenzettel $stundenzettel Das Stundenzettel-Objekt
|
||||
* @param int $defaultServiceId ID der Standard-Leistung
|
||||
* @return array Array mit 'price', 'source' ('custom', 'customer', 'standard')
|
||||
*/
|
||||
function getEffectiveHourlyRate($db, $stundenzettel, $defaultServiceId) {
|
||||
// 1. Prüfe ob manueller Preis im Stundenzettel gesetzt
|
||||
if ($stundenzettel->hourly_rate_is_custom && $stundenzettel->hourly_rate !== null) {
|
||||
return array(
|
||||
'price' => (float)$stundenzettel->hourly_rate,
|
||||
'source' => 'custom'
|
||||
);
|
||||
}
|
||||
|
||||
// 2. Hole kundenspezifischen oder Standard-Preis
|
||||
if ($defaultServiceId > 0) {
|
||||
$priceInfo = getCustomerPrice($db, $defaultServiceId, $stundenzettel->fk_soc);
|
||||
return array(
|
||||
'price' => $priceInfo['price'],
|
||||
'source' => $priceInfo['is_customer_price'] ? 'customer' : 'standard'
|
||||
);
|
||||
}
|
||||
|
||||
// 3. Kein Preis verfügbar
|
||||
return array(
|
||||
'price' => 0,
|
||||
'source' => 'none'
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,10 @@ CREATE TABLE llx_stundenzettel (
|
|||
-- Status: 0=Entwurf, 1=Freigegeben, 2=In Rechnung übertragen, 9=Storniert
|
||||
status TINYINT DEFAULT 0 NOT NULL,
|
||||
|
||||
-- Stundenpreis (abweichend vom Standard)
|
||||
hourly_rate DOUBLE(24,8) DEFAULT NULL, -- Stundenpreis (NULL = Standard verwenden)
|
||||
hourly_rate_is_custom TINYINT DEFAULT 0 NOT NULL, -- 1 = manuell geändert
|
||||
|
||||
-- Notizen
|
||||
note_private TEXT,
|
||||
note_public TEXT,
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ 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
|
||||
fk_product INTEGER DEFAULT NULL, -- Verknüpfung zur Leistungsposition (Dienstleistung)
|
||||
|
||||
-- Zeitraum
|
||||
date_leistung DATE NOT NULL, -- Datum der Leistung
|
||||
|
|
|
|||
8
sql/update_1.2.0.sql
Normal file
8
sql/update_1.2.0.sql
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
-- ============================================================================
|
||||
-- Stundenzettel Update auf Version 1.2.0
|
||||
-- Fügt Leistungsposition zu Arbeitszeiten hinzu
|
||||
-- ============================================================================
|
||||
|
||||
-- Leistungsposition (Dienstleistung) pro Arbeitszeit
|
||||
ALTER TABLE llx_stundenzettel_leistung
|
||||
ADD COLUMN IF NOT EXISTS fk_product INTEGER DEFAULT NULL COMMENT 'Verknüpfung zur Leistungsposition (Dienstleistung)';
|
||||
|
|
@ -17,6 +17,7 @@ require_once DOL_DOCUMENT_ROOT.'/core/lib/order.lib.php';
|
|||
require_once DOL_DOCUMENT_ROOT.'/product/class/product.class.php';
|
||||
require_once DOL_DOCUMENT_ROOT.'/societe/class/societe.class.php';
|
||||
dol_include_once('/stundenzettel/class/stundenzettel.class.php');
|
||||
dol_include_once('/stundenzettel/lib/stundenzettel.lib.php');
|
||||
|
||||
// Load translation files
|
||||
$langs->loadLangs(array("stundenzettel@stundenzettel", "orders", "products"));
|
||||
|
|
@ -712,11 +713,16 @@ if ($action == 'confirm_transfer_invoice' && GETPOST('confirm', 'alpha') == 'yes
|
|||
$product = new Product($db);
|
||||
$product->fetch($addProd->fk_product);
|
||||
|
||||
// Kundenspezifischen Preis holen (falls vorhanden)
|
||||
$customerPriceInfo = getCustomerPrice($db, $addProd->fk_product, $order->socid, $product);
|
||||
$usePrice = $customerPriceInfo['price'];
|
||||
$useTvaTx = $customerPriceInfo['tva_tx'];
|
||||
|
||||
$productResult = $facture->addline(
|
||||
$product->label, // 1: desc
|
||||
$product->price, // 2: pu_ht
|
||||
$usePrice, // 2: pu_ht - kundenspezifischer Preis
|
||||
$addProd->total_qty, // 3: qty
|
||||
$product->tva_tx, // 4: txtva
|
||||
$useTvaTx, // 4: txtva - kundenspezifischer MwSt-Satz
|
||||
0, // 5: txlocaltax1
|
||||
0, // 6: txlocaltax2
|
||||
$addProd->fk_product, // 7: fk_product
|
||||
|
|
@ -808,6 +814,137 @@ if ($action == 'confirm_transfer_invoice' && GETPOST('confirm', 'alpha') == 'yes
|
|||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// ARBEITSZEITEN aus Stundenzetteln hinzufügen
|
||||
// (Gruppiert nach Leistungsposition/Produkt)
|
||||
// ============================================
|
||||
|
||||
// Standard-Leistung vom Kunden laden (Fallback wenn keine Leistung gewählt)
|
||||
$societe = new Societe($db);
|
||||
$societe->fetch($order->socid);
|
||||
$societe->fetch_optionals();
|
||||
$defaultServiceId = isset($societe->array_options['options_stundenzettel_default_service']) ? (int)$societe->array_options['options_stundenzettel_default_service'] : 0;
|
||||
|
||||
// Alle Arbeitszeiten nach Leistungsposition gruppiert sammeln
|
||||
$sqlHours = "SELECT ";
|
||||
$sqlHours .= " COALESCE(l.fk_product, ".(int)$defaultServiceId.") as product_id,";
|
||||
$sqlHours .= " p.ref as product_ref, p.label as product_label, p.tva_tx, p.fk_unit,";
|
||||
$sqlHours .= " SUM(l.duration) as total_minutes";
|
||||
$sqlHours .= " FROM ".MAIN_DB_PREFIX."stundenzettel s";
|
||||
$sqlHours .= " INNER JOIN ".MAIN_DB_PREFIX."stundenzettel_leistung l ON l.fk_stundenzettel = s.rowid";
|
||||
$sqlHours .= " LEFT JOIN ".MAIN_DB_PREFIX."product p ON p.rowid = COALESCE(l.fk_product, ".(int)$defaultServiceId.")";
|
||||
$sqlHours .= " WHERE s.fk_commande = ".((int)$order->id);
|
||||
$sqlHours .= " AND s.status >= 1"; // Nur validierte Stundenzettel
|
||||
$sqlHours .= " GROUP BY COALESCE(l.fk_product, ".(int)$defaultServiceId."), p.ref, p.label, p.tva_tx, p.fk_unit";
|
||||
$sqlHours .= " HAVING SUM(l.duration) > 0";
|
||||
$sqlHours .= " ORDER BY p.label";
|
||||
|
||||
$resqlHours = $db->query($sqlHours);
|
||||
$hasWorkHours = ($resqlHours && $db->num_rows($resqlHours) > 0);
|
||||
|
||||
if ($hasWorkHours) {
|
||||
// Arbeitszeit-Section erstellen (nur wenn Sections im Auftrag)
|
||||
$arbeitszeitSectionResult = 0;
|
||||
if ($orderHasSections) {
|
||||
$arbeitszeitSectionResult = $facture->addline(
|
||||
$langs->trans('Leistungen'),
|
||||
0, 0, 0, 0, 0, 0, 0, '', '', 0, 0, '', 'HT', 0,
|
||||
9, // product_type für Titel
|
||||
$rang++,
|
||||
100 // special_code = 100 für Section
|
||||
);
|
||||
|
||||
if ($arbeitszeitSectionResult > 0) {
|
||||
$invoiceManagerLines[] = array(
|
||||
'type' => 'section',
|
||||
'fk_facturedet' => $arbeitszeitSectionResult,
|
||||
'title' => $langs->trans('Leistungen'),
|
||||
'parent' => null
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Arbeitszeiten hinzufügen (pro Leistungsposition)
|
||||
$arbeitszeitSubtotal = 0;
|
||||
while ($objHours = $db->fetch_object($resqlHours)) {
|
||||
$productId = (int)$objHours->product_id;
|
||||
$hoursWorked = $objHours->total_minutes / 60;
|
||||
|
||||
// Produkt-Preis ermitteln (kundenspezifisch oder Standard)
|
||||
$priceInfo = getCustomerPrice($db, $productId, $order->socid);
|
||||
$hourlyPrice = $priceInfo['price'];
|
||||
$tvaTx = !empty($objHours->tva_tx) ? $objHours->tva_tx : $priceInfo['tva_tx'];
|
||||
|
||||
// Beschreibung
|
||||
$lineDesc = !empty($objHours->product_label) ? $objHours->product_label : $langs->trans('DefaultService');
|
||||
|
||||
// Rechnungszeile hinzufügen
|
||||
$hoursResult = $facture->addline(
|
||||
$lineDesc, // 1: desc
|
||||
$hourlyPrice, // 2: pu_ht
|
||||
$hoursWorked, // 3: qty (Stunden)
|
||||
$tvaTx, // 4: txtva
|
||||
0, // 5: txlocaltax1
|
||||
0, // 6: txlocaltax2
|
||||
$productId, // 7: fk_product
|
||||
0, // 8: remise_percent
|
||||
'', // 9: date_start
|
||||
'', // 10: date_end
|
||||
0, // 11: fk_code_ventilation
|
||||
0, // 12: info_bits
|
||||
0, // 13: fk_remise_except
|
||||
'HT', // 14: price_base_type
|
||||
0, // 15: pu_ttc
|
||||
1, // 16: type (1 = Dienstleistung)
|
||||
$rang++, // 17: rang
|
||||
0, // 18: special_code
|
||||
'', // 19: origin
|
||||
0, // 20: origin_id
|
||||
0, // 21: fk_parent_line
|
||||
null, // 22: fk_fournprice
|
||||
0, // 23: pa_ht
|
||||
'', // 24: label
|
||||
array(), // 25: array_options
|
||||
100, // 26: situation_percent
|
||||
0, // 27: fk_prev_id
|
||||
$objHours->fk_unit // 28: fk_unit
|
||||
);
|
||||
|
||||
if ($hoursResult > 0) {
|
||||
if ($orderHasSections && $arbeitszeitSectionResult > 0) {
|
||||
$invoiceManagerLines[] = array(
|
||||
'type' => 'product',
|
||||
'fk_facturedet' => $hoursResult,
|
||||
'title' => null,
|
||||
'parent' => $arbeitszeitSectionResult
|
||||
);
|
||||
}
|
||||
$arbeitszeitSubtotal += $hourlyPrice * $hoursWorked;
|
||||
} elseif ($hoursResult < 0) {
|
||||
$error++;
|
||||
setEventMessages($facture->error, $facture->errors, 'errors');
|
||||
}
|
||||
}
|
||||
|
||||
// Zwischensumme für Arbeitszeit
|
||||
if ($arbeitszeitSectionResult > 0 && $arbeitszeitSubtotal > 0) {
|
||||
$subtotalLabel = 'Zwischensumme: '.$langs->trans('Leistungen');
|
||||
$subtotalResult = $facture->addline(
|
||||
$subtotalLabel,
|
||||
0, 1, 0, 0, 0, 0, 0, '', '', 0, 0, '', 'HT', 0,
|
||||
9, $rang++, 102, 0, '', 0, 0
|
||||
);
|
||||
if ($subtotalResult > 0) {
|
||||
$invoiceManagerLines[] = array(
|
||||
'type' => 'subtotal',
|
||||
'fk_facturedet' => $subtotalResult,
|
||||
'title' => $subtotalLabel,
|
||||
'parent' => $arbeitszeitSectionResult
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// llx_facture_lines_manager für Rechnung erstellen
|
||||
if (!$error && count($invoiceManagerLines) > 0) {
|
||||
$lineOrder = 1;
|
||||
|
|
@ -891,7 +1028,6 @@ $_GET['mainmenu'] = 'stundenzettel';
|
|||
// Stundenzettel laden wenn ID übergeben wurde
|
||||
$stundenzettelObj = null;
|
||||
if ($stundenzettel_id > 0) {
|
||||
dol_include_once('/stundenzettel/lib/stundenzettel.lib.php');
|
||||
$stundenzettelObj = new Stundenzettel($db);
|
||||
if ($stundenzettelObj->fetch($stundenzettel_id) <= 0) {
|
||||
$stundenzettelObj = null;
|
||||
|
|
@ -947,22 +1083,15 @@ $hasRemainingProducts = (count($remainingProducts) > 0);
|
|||
$title = $langs->trans("Stundenzettel").' - '.$order->ref;
|
||||
llxHeader('', $title, '', '', 0, 0, '', '', '', 'mod-stundenzettel page-commande');
|
||||
|
||||
// Tabs - Stundenzettel-Tabs wenn aus Stundenzettel aufgerufen, sonst Auftrags-Tabs
|
||||
// Aktiven Tab-Key basierend auf $tab bestimmen
|
||||
$activeTabKey = 'productlist'; // Default
|
||||
if ($tab == 'stundenzettel') {
|
||||
$activeTabKey = 'stundenzettel_list';
|
||||
} elseif ($tab == 'tracking') {
|
||||
$activeTabKey = 'tracking';
|
||||
}
|
||||
// Mobile CSS einbinden
|
||||
print '<link rel="stylesheet" type="text/css" href="'.dol_buildpath('/stundenzettel/css/stundenzettel-mobile.css', 1).'?v='.filemtime(dol_buildpath('/stundenzettel/css/stundenzettel-mobile.css', 0)).'">';
|
||||
|
||||
if ($stundenzettelObj) {
|
||||
$head = stundenzettel_prepare_head($stundenzettelObj);
|
||||
dol_fiche_head($head, $activeTabKey, $langs->trans("Stundenzettel"), -1, 'clock');
|
||||
} else {
|
||||
$head = commande_prepare_head($order);
|
||||
dol_fiche_head($head, 'stundenzettel', $langs->trans("CustomerOrder"), -1, 'order');
|
||||
}
|
||||
// Tabs - Immer die Stundenzettel-Commande-Tabs verwenden
|
||||
// Aktiven Tab-Key basierend auf $tab bestimmen
|
||||
$activeTabKey = $tab; // products, stundenzettel, oder tracking
|
||||
|
||||
$head = stundenzettel_commande_prepare_head($order, $stundenzettel_id);
|
||||
dol_fiche_head($head, $activeTabKey, $langs->trans("Stundenzettel"), -1, 'clock');
|
||||
|
||||
$form = new Form($db);
|
||||
|
||||
|
|
@ -1070,8 +1199,17 @@ if ($defaultServiceProduct || $plannedHours > 0) {
|
|||
print '<td style="padding-right:20px;"><strong>'.img_picto('', 'service', 'class="pictofixedwidth"').$langs->trans("DefaultService").':</strong></td>';
|
||||
print '<td>';
|
||||
if ($defaultServiceProduct) {
|
||||
// Kundenspezifischen Preis für die Standard-Leistung holen
|
||||
$defaultServicePriceInfo = getCustomerPrice($db, $defaultServiceProduct->id, $order->socid, $defaultServiceProduct);
|
||||
$displayPrice = $defaultServicePriceInfo['price'];
|
||||
$isCustomerPrice = $defaultServicePriceInfo['is_customer_price'];
|
||||
|
||||
print $defaultServiceProduct->getNomUrl(1).' - '.$defaultServiceProduct->label;
|
||||
print ' <span class="opacitymedium">('.price($defaultServiceProduct->price, 0, $langs, 1, -1, -1, $conf->currency).'/Std.)</span>';
|
||||
print ' <span class="opacitymedium">('.price($displayPrice, 0, $langs, 1, -1, -1, $conf->currency).'/Std.)';
|
||||
if ($isCustomerPrice) {
|
||||
print ' <span class="badge badge-status4" title="'.$langs->trans("CustomerSpecificPrice").'">Kundenpreis</span>';
|
||||
}
|
||||
print '</span>';
|
||||
} else {
|
||||
print '<span class="opacitymedium">'.$langs->trans("NoDefaultServiceSet").'</span>';
|
||||
print ' <a href="'.DOL_URL_ROOT.'/societe/card.php?socid='.$order->socid.'" class="button small">'.$langs->trans("SetDefaultServiceInCustomer").'</a>';
|
||||
|
|
@ -2048,7 +2186,15 @@ if ($tab == 'tracking') {
|
|||
$resqlDetails = $db->query($sqlDetails);
|
||||
if ($resqlDetails) {
|
||||
while ($objD = $db->fetch_object($resqlDetails)) {
|
||||
$key = $objD->fk_commandedet > 0 ? $objD->fk_commandedet : 'prod_'.$objD->fk_product;
|
||||
// Key-Logik: fk_commandedet > prod_X > freetext_hash
|
||||
if ($objD->fk_commandedet > 0) {
|
||||
$key = $objD->fk_commandedet;
|
||||
} elseif ($objD->fk_product > 0) {
|
||||
$key = 'prod_'.$objD->fk_product;
|
||||
} else {
|
||||
// Freitext: Hash der Beschreibung als Key
|
||||
$key = 'freetext_'.md5(trim($objD->description));
|
||||
}
|
||||
if (!isset($trackingDetails[$key])) {
|
||||
$trackingDetails[$key] = array();
|
||||
}
|
||||
|
|
@ -2082,14 +2228,15 @@ if ($tab == 'tracking') {
|
|||
$sql .= " COALESCE((SELECT SUM(sp.qty_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 = cd.rowid AND sp.origin IN ('order', 'added')), 0) as qty_delivered,";
|
||||
// Mehraufwand
|
||||
// Mehraufwand - nur für Produkte mit fk_product > 0 (Freitext-Mehraufwand wird separat angezeigt)
|
||||
$sql .= " COALESCE((SELECT SUM(sp2.qty_done) FROM ".MAIN_DB_PREFIX."stundenzettel_product sp2";
|
||||
$sql .= " JOIN ".MAIN_DB_PREFIX."stundenzettel s2 ON s2.rowid = sp2.fk_stundenzettel";
|
||||
$sql .= " WHERE sp2.fk_product = cd.fk_product AND sp2.origin = 'additional' AND s2.fk_commande = ".((int)$order->id)."), 0) as qty_additional,";
|
||||
// Entfällt
|
||||
$sql .= " WHERE sp2.fk_product = cd.fk_product AND cd.fk_product > 0 AND sp2.origin = 'additional' AND s2.fk_commande = ".((int)$order->id)."), 0) as qty_additional,";
|
||||
// Entfällt - für Produkte via fk_product, für Freitext via fk_commandedet
|
||||
$sql .= " COALESCE((SELECT SUM(sp3.qty_done) FROM ".MAIN_DB_PREFIX."stundenzettel_product sp3";
|
||||
$sql .= " JOIN ".MAIN_DB_PREFIX."stundenzettel s3 ON s3.rowid = sp3.fk_stundenzettel";
|
||||
$sql .= " WHERE sp3.fk_product = cd.fk_product AND sp3.origin = 'omitted' AND s3.fk_commande = ".((int)$order->id)."), 0) as qty_omitted";
|
||||
$sql .= " WHERE sp3.origin = 'omitted' AND s3.fk_commande = ".((int)$order->id);
|
||||
$sql .= " AND ((sp3.fk_product = cd.fk_product AND cd.fk_product > 0) OR sp3.fk_commandedet = cd.rowid)), 0) as qty_omitted";
|
||||
$sql .= " FROM ".MAIN_DB_PREFIX."commandedet as cd";
|
||||
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."product as p ON p.rowid = cd.fk_product";
|
||||
$sql .= " WHERE cd.fk_commande = ".((int)$order->id);
|
||||
|
|
@ -2264,6 +2411,165 @@ if ($tab == 'tracking') {
|
|||
}
|
||||
}
|
||||
|
||||
// =============================================
|
||||
// ZUSÄTZLICHE PRODUKTE: Mehraufwand die NICHT im Auftrag sind
|
||||
// (Produkte und Freitext die direkt als Mehraufwand hinzugefügt wurden)
|
||||
// =============================================
|
||||
|
||||
// Alle Produkt-IDs aus dem Auftrag sammeln (für Ausschluss)
|
||||
$orderProductIds = array();
|
||||
$sqlOrderProds = "SELECT fk_product FROM ".MAIN_DB_PREFIX."commandedet WHERE fk_commande = ".((int)$order->id)." AND fk_product > 0";
|
||||
$resOrderProds = $db->query($sqlOrderProds);
|
||||
if ($resOrderProds) {
|
||||
while ($objOP = $db->fetch_object($resOrderProds)) {
|
||||
$orderProductIds[] = $objOP->fk_product;
|
||||
}
|
||||
}
|
||||
|
||||
// Mehraufwand-Produkte laden die NICHT im Auftrag sind
|
||||
$sqlMehraufwand = "SELECT sp.fk_product, sp.description,";
|
||||
$sqlMehraufwand .= " p.ref as product_ref, p.label as product_label,";
|
||||
$sqlMehraufwand .= " SUM(sp.qty) as qty_ordered,";
|
||||
$sqlMehraufwand .= " SUM(sp.qty_done) as qty_delivered";
|
||||
$sqlMehraufwand .= " FROM ".MAIN_DB_PREFIX."stundenzettel_product sp";
|
||||
$sqlMehraufwand .= " JOIN ".MAIN_DB_PREFIX."stundenzettel s ON s.rowid = sp.fk_stundenzettel";
|
||||
$sqlMehraufwand .= " LEFT JOIN ".MAIN_DB_PREFIX."product p ON p.rowid = sp.fk_product";
|
||||
$sqlMehraufwand .= " WHERE s.fk_commande = ".((int)$order->id);
|
||||
$sqlMehraufwand .= " AND sp.origin = 'additional'";
|
||||
if (!empty($orderProductIds)) {
|
||||
$sqlMehraufwand .= " AND (sp.fk_product NOT IN (".implode(',', $orderProductIds).") OR sp.fk_product = 0)";
|
||||
}
|
||||
$sqlMehraufwand .= " GROUP BY sp.fk_product, sp.description, p.ref, p.label";
|
||||
$sqlMehraufwand .= " ORDER BY p.ref, sp.description";
|
||||
|
||||
$resqlMehr = $db->query($sqlMehraufwand);
|
||||
$hasMehraufwandProducts = false;
|
||||
|
||||
if ($resqlMehr && $db->num_rows($resqlMehr) > 0) {
|
||||
// Separator-Zeile für Mehraufwand
|
||||
print '<tr class="liste_titre">';
|
||||
print '<td colspan="5" style="background-color: #e8f5e9;"><strong>'.$langs->trans("Mehraufwand").'</strong> <span class="opacitymedium">('.$langs->trans("MehraufwandDesc").')</span></td>';
|
||||
print '</tr>';
|
||||
|
||||
while ($objMehr = $db->fetch_object($resqlMehr)) {
|
||||
$hasMehraufwandProducts = true;
|
||||
$detailRowId++;
|
||||
$qty_ordered_mehr = (float)$objMehr->qty_ordered;
|
||||
$qty_delivered_mehr = (float)$objMehr->qty_delivered;
|
||||
$qty_remaining_mehr = $qty_ordered_mehr - $qty_delivered_mehr;
|
||||
|
||||
// Details für Mehraufwand laden
|
||||
$detailsMehr = array();
|
||||
if ($objMehr->fk_product > 0) {
|
||||
$prodKey = 'prod_'.$objMehr->fk_product;
|
||||
if (isset($trackingDetails[$prodKey])) {
|
||||
$detailsMehr = $trackingDetails[$prodKey];
|
||||
}
|
||||
} else {
|
||||
// Freitext: Hash der Beschreibung als Key
|
||||
$freetextKey = 'freetext_'.md5(trim($objMehr->description));
|
||||
if (isset($trackingDetails[$freetextKey])) {
|
||||
$detailsMehr = $trackingDetails[$freetextKey];
|
||||
}
|
||||
}
|
||||
$hasDetailsMehr = !empty($detailsMehr);
|
||||
|
||||
print '<tr class="oddeven">';
|
||||
|
||||
// Produkt/Beschreibung
|
||||
print '<td>';
|
||||
if ($hasDetailsMehr) {
|
||||
print '<span class="tracking-toggle" onclick="toggleTrackingDetails(\'tdetail_'.$detailRowId.'\')" style="cursor:pointer; margin-right:5px;">';
|
||||
print '<span class="fas fa-chevron-right tracking-arrow" id="tarrow_tdetail_'.$detailRowId.'"></span>';
|
||||
print '</span>';
|
||||
}
|
||||
print '<span class="badge badge-success" style="margin-right:5px;">'.$langs->trans("Mehraufwand").'</span>';
|
||||
if ($objMehr->fk_product > 0) {
|
||||
print '<a href="'.DOL_URL_ROOT.'/product/card.php?id='.$objMehr->fk_product.'">';
|
||||
print img_picto('', 'product', 'class="pictofixedwidth"');
|
||||
print $objMehr->product_ref.' - '.$objMehr->product_label;
|
||||
print '</a>';
|
||||
} else {
|
||||
// Freitext
|
||||
print img_picto('', 'generic', 'class="pictofixedwidth"');
|
||||
$desc = !empty($objMehr->description) ? strip_tags($objMehr->description) : '-';
|
||||
if (strlen($desc) > 50) $desc = substr($desc, 0, 47).'...';
|
||||
print '<span class="opacitymedium">'.$desc.'</span>';
|
||||
}
|
||||
print '</td>';
|
||||
|
||||
// Bestellt (Mehraufwand-Menge)
|
||||
print '<td class="right"><strong>'.formatQty($qty_ordered_mehr).'</strong></td>';
|
||||
$total_ordered += $qty_ordered_mehr;
|
||||
|
||||
// Geliefert
|
||||
print '<td class="right">'.formatQty($qty_delivered_mehr).'</td>';
|
||||
$total_delivered += $qty_delivered_mehr;
|
||||
|
||||
// Verbleibend
|
||||
print '<td class="right">';
|
||||
if ($qty_remaining_mehr > 0) {
|
||||
print '<span class="badge badge-warning">'.formatQty($qty_remaining_mehr).'</span>';
|
||||
} elseif ($qty_remaining_mehr == 0) {
|
||||
print '<span class="badge badge-success">0</span>';
|
||||
} else {
|
||||
print '<span class="badge badge-info">'.formatQty($qty_remaining_mehr).'</span>';
|
||||
}
|
||||
print '</td>';
|
||||
$total_remaining += $qty_remaining_mehr;
|
||||
|
||||
// Status
|
||||
print '<td>';
|
||||
if ($qty_remaining_mehr <= 0) {
|
||||
print '<span class="badge badge-success">'.$langs->trans("TrackingDone").'</span>';
|
||||
} elseif ($qty_delivered_mehr > 0) {
|
||||
print '<span class="badge badge-warning">'.$langs->trans("TrackingPartial").'</span>';
|
||||
} else {
|
||||
print '<span class="badge badge-secondary">'.$langs->trans("TrackingOpen").'</span>';
|
||||
}
|
||||
print '</td>';
|
||||
|
||||
print '</tr>';
|
||||
|
||||
// Detail-Zeile für Mehraufwand
|
||||
if ($hasDetailsMehr) {
|
||||
print '<tr id="tdetail_'.$detailRowId.'" class="tracking-detail-row" style="display:none;">';
|
||||
print '<td colspan="5" style="padding: 0 0 0 30px; background-color: #fafafa;">';
|
||||
print '<table class="noborder" style="width:100%; margin: 5px 0;">';
|
||||
print '<tr class="liste_titre" style="background-color: #f0f0f0;">';
|
||||
print '<th style="width:120px;">'.$langs->trans("Stundenzettel").'</th>';
|
||||
print '<th style="width:100px;">'.$langs->trans("Date").'</th>';
|
||||
print '<th style="width:100px;">'.$langs->trans("Type").'</th>';
|
||||
print '<th class="right" style="width:80px;">'.$langs->trans("Qty").'</th>';
|
||||
print '<th>'.$langs->trans("Reason").'</th>';
|
||||
print '</tr>';
|
||||
|
||||
foreach ($detailsMehr as $det) {
|
||||
print '<tr class="oddeven">';
|
||||
print '<td><a href="'.dol_buildpath('/stundenzettel/card.php', 1).'?id='.$det->fk_stundenzettel.'">'.$det->stz_ref.'</a></td>';
|
||||
print '<td>'.dol_print_date($db->jdate($det->date_stundenzettel), 'day').'</td>';
|
||||
print '<td><span class="badge badge-success">'.$langs->trans("Mehraufwand").'</span></td>';
|
||||
print '<td class="right">'.formatQty($det->qty_done).'</td>';
|
||||
print '<td>'.((!empty($det->description)) ? dol_escape_htmltag($det->description) : '<span class="opacitymedium">-</span>').'</td>';
|
||||
print '</tr>';
|
||||
}
|
||||
|
||||
print '</table>';
|
||||
print '</td>';
|
||||
print '</tr>';
|
||||
}
|
||||
}
|
||||
|
||||
// Neue Summenzeile mit Mehraufwand
|
||||
print '<tr class="liste_total">';
|
||||
print '<td><strong>'.$langs->trans("Total").' ('.$langs->trans("Mehraufwand").' '.$langs->trans("incl").')</strong></td>';
|
||||
print '<td class="right"><strong>'.formatQty($total_ordered).'</strong></td>';
|
||||
print '<td class="right"><strong>'.formatQty($total_delivered).'</strong></td>';
|
||||
print '<td class="right"><strong>'.formatQty($total_remaining).'</strong></td>';
|
||||
print '<td></td>';
|
||||
print '</tr>';
|
||||
}
|
||||
|
||||
print '</table>';
|
||||
print '</div>';
|
||||
|
||||
|
|
@ -2307,9 +2613,176 @@ if ($tab == 'tracking') {
|
|||
}
|
||||
</script>';
|
||||
|
||||
// =============================================
|
||||
// BEREICH: LEISTUNGEN / ARBEITSZEITEN
|
||||
// =============================================
|
||||
print '<div class="fichehalfleft" style="margin-top: 20px;">';
|
||||
print '<div class="titre inline-block">'.$langs->trans("Leistungen").' / '.$langs->trans("TotalHours").'</div>';
|
||||
|
||||
// Leistungen nach Leistungsposition gruppiert laden
|
||||
$sqlLeistungen = "SELECT ";
|
||||
$sqlLeistungen .= " COALESCE(l.fk_product, 0) as service_id,";
|
||||
$sqlLeistungen .= " p.ref as service_ref, p.label as service_label,";
|
||||
$sqlLeistungen .= " SUM(l.duration) as total_minutes,";
|
||||
$sqlLeistungen .= " COUNT(l.rowid) as entry_count";
|
||||
$sqlLeistungen .= " FROM ".MAIN_DB_PREFIX."stundenzettel_leistung l";
|
||||
$sqlLeistungen .= " JOIN ".MAIN_DB_PREFIX."stundenzettel s ON s.rowid = l.fk_stundenzettel";
|
||||
$sqlLeistungen .= " LEFT JOIN ".MAIN_DB_PREFIX."product p ON p.rowid = l.fk_product";
|
||||
$sqlLeistungen .= " WHERE s.fk_commande = ".((int)$order->id);
|
||||
$sqlLeistungen .= " GROUP BY COALESCE(l.fk_product, 0), p.ref, p.label";
|
||||
$sqlLeistungen .= " ORDER BY p.ref, p.label";
|
||||
|
||||
$resqlLeistungen = $db->query($sqlLeistungen);
|
||||
$totalMinutesAll = 0;
|
||||
|
||||
print '<div class="div-table-responsive-no-min">';
|
||||
print '<table class="noborder centpercent">';
|
||||
print '<tr class="liste_titre">';
|
||||
print '<th>'.$langs->trans("DefaultService").'</th>';
|
||||
print '<th class="right">'.$langs->trans("TotalHours").'</th>';
|
||||
print '<th class="right">'.$langs->trans("Entries").'</th>';
|
||||
print '</tr>';
|
||||
|
||||
if ($resqlLeistungen) {
|
||||
$numLeistungen = $db->num_rows($resqlLeistungen);
|
||||
if ($numLeistungen > 0) {
|
||||
while ($objL = $db->fetch_object($resqlLeistungen)) {
|
||||
$totalMinutesAll += $objL->total_minutes;
|
||||
$hours = floor($objL->total_minutes / 60);
|
||||
$mins = $objL->total_minutes % 60;
|
||||
|
||||
print '<tr class="oddeven">';
|
||||
|
||||
// Leistungsposition
|
||||
print '<td>';
|
||||
if ($objL->service_id > 0) {
|
||||
print '<a href="'.DOL_URL_ROOT.'/product/card.php?id='.$objL->service_id.'">';
|
||||
print img_picto('', 'service', 'class="pictofixedwidth"');
|
||||
print $objL->service_ref.' - '.$objL->service_label;
|
||||
print '</a>';
|
||||
} else {
|
||||
print '<span class="opacitymedium">'.img_picto('', 'service', 'class="pictofixedwidth"').$langs->trans("NotSet").'</span>';
|
||||
}
|
||||
print '</td>';
|
||||
|
||||
// Stunden
|
||||
print '<td class="right"><strong>'.sprintf('%d:%02d h', $hours, $mins).'</strong></td>';
|
||||
|
||||
// Anzahl Einträge
|
||||
print '<td class="right">'.$objL->entry_count.'</td>';
|
||||
|
||||
print '</tr>';
|
||||
}
|
||||
|
||||
// Summenzeile
|
||||
$totalHours = floor($totalMinutesAll / 60);
|
||||
$totalMins = $totalMinutesAll % 60;
|
||||
print '<tr class="liste_total">';
|
||||
print '<td><strong>'.$langs->trans("Total").'</strong></td>';
|
||||
print '<td class="right"><strong>'.sprintf('%d:%02d h', $totalHours, $totalMins).'</strong></td>';
|
||||
print '<td class="right"></td>';
|
||||
print '</tr>';
|
||||
|
||||
} else {
|
||||
print '<tr class="oddeven"><td colspan="3" class="opacitymedium">'.$langs->trans("NoRecordFound").'</td></tr>';
|
||||
}
|
||||
}
|
||||
|
||||
print '</table>';
|
||||
print '</div>';
|
||||
|
||||
// Details pro Stundenzettel
|
||||
print '<br>';
|
||||
print '<div class="titre inline-block">'.$langs->trans("Leistungen").' '.$langs->trans("perStundenzettel").'</div>';
|
||||
|
||||
$sqlLeistDetail = "SELECT l.rowid, l.fk_stundenzettel, l.fk_product, l.date_leistung,";
|
||||
$sqlLeistDetail .= " l.time_start, l.time_end, l.duration, l.description,";
|
||||
$sqlLeistDetail .= " s.ref as stz_ref, s.date_stundenzettel,";
|
||||
$sqlLeistDetail .= " p.ref as service_ref, p.label as service_label";
|
||||
$sqlLeistDetail .= " FROM ".MAIN_DB_PREFIX."stundenzettel_leistung l";
|
||||
$sqlLeistDetail .= " JOIN ".MAIN_DB_PREFIX."stundenzettel s ON s.rowid = l.fk_stundenzettel";
|
||||
$sqlLeistDetail .= " LEFT JOIN ".MAIN_DB_PREFIX."product p ON p.rowid = l.fk_product";
|
||||
$sqlLeistDetail .= " WHERE s.fk_commande = ".((int)$order->id);
|
||||
$sqlLeistDetail .= " ORDER BY s.date_stundenzettel DESC, l.date_leistung, l.time_start";
|
||||
|
||||
$resqlLeistDetail = $db->query($sqlLeistDetail);
|
||||
|
||||
print '<div class="div-table-responsive-no-min">';
|
||||
print '<table class="noborder centpercent">';
|
||||
print '<tr class="liste_titre">';
|
||||
print '<th>'.$langs->trans("Stundenzettel").'</th>';
|
||||
print '<th>'.$langs->trans("Date").'</th>';
|
||||
print '<th>'.$langs->trans("LeistungTimeStart").' - '.$langs->trans("LeistungTimeEnd").'</th>';
|
||||
print '<th class="right">'.$langs->trans("LeistungDuration").'</th>';
|
||||
print '<th>'.$langs->trans("DefaultService").'</th>';
|
||||
print '<th>'.$langs->trans("Description").'</th>';
|
||||
print '</tr>';
|
||||
|
||||
if ($resqlLeistDetail) {
|
||||
$numDetail = $db->num_rows($resqlLeistDetail);
|
||||
if ($numDetail > 0) {
|
||||
while ($objLD = $db->fetch_object($resqlLeistDetail)) {
|
||||
$hours = floor($objLD->duration / 60);
|
||||
$mins = $objLD->duration % 60;
|
||||
|
||||
print '<tr class="oddeven">';
|
||||
|
||||
// Stundenzettel
|
||||
print '<td>';
|
||||
print '<a href="'.dol_buildpath('/stundenzettel/card.php', 1).'?id='.$objLD->fk_stundenzettel.'">';
|
||||
print $objLD->stz_ref;
|
||||
print '</a>';
|
||||
print '</td>';
|
||||
|
||||
// Datum
|
||||
print '<td>'.dol_print_date($db->jdate($objLD->date_leistung), 'day').'</td>';
|
||||
|
||||
// Zeit
|
||||
print '<td>';
|
||||
if ($objLD->time_start && $objLD->time_end) {
|
||||
print substr($objLD->time_start, 0, 5).' - '.substr($objLD->time_end, 0, 5);
|
||||
} else {
|
||||
print '<span class="opacitymedium">-</span>';
|
||||
}
|
||||
print '</td>';
|
||||
|
||||
// Dauer
|
||||
print '<td class="right">'.sprintf('%d:%02d h', $hours, $mins).'</td>';
|
||||
|
||||
// Leistungsposition
|
||||
print '<td>';
|
||||
if ($objLD->fk_product > 0) {
|
||||
print '<span class="badge badge-primary">'.$objLD->service_ref.'</span>';
|
||||
} else {
|
||||
print '<span class="opacitymedium">-</span>';
|
||||
}
|
||||
print '</td>';
|
||||
|
||||
// Beschreibung
|
||||
print '<td>';
|
||||
if (!empty($objLD->description)) {
|
||||
$desc = strip_tags($objLD->description);
|
||||
if (strlen($desc) > 50) $desc = substr($desc, 0, 47).'...';
|
||||
print dol_escape_htmltag($desc);
|
||||
} else {
|
||||
print '<span class="opacitymedium">-</span>';
|
||||
}
|
||||
print '</td>';
|
||||
|
||||
print '</tr>';
|
||||
}
|
||||
} else {
|
||||
print '<tr class="oddeven"><td colspan="6" class="opacitymedium">'.$langs->trans("NoRecordFound").'</td></tr>';
|
||||
}
|
||||
}
|
||||
|
||||
print '</table>';
|
||||
print '</div>';
|
||||
print '</div>'; // fichehalfleft
|
||||
|
||||
// Button: In Rechnung übertragen (nur wenn alles erledigt)
|
||||
if ($total_remaining <= 0 && $total_delivered > 0) {
|
||||
print '<div class="center" style="margin-top:20px;">';
|
||||
print '<div class="center clearboth" style="margin-top:20px;">';
|
||||
print '<a class="butAction" href="?id='.$order->id.'&action=transfer_to_invoice">';
|
||||
print img_picto('', 'bill', 'class="pictofixedwidth"').$langs->trans("TransferToInvoice");
|
||||
print '</a>';
|
||||
|
|
|
|||
Loading…
Reference in a new issue