feat: Kategorie-Baumansicht mit Inline-Editing, AJAX-Aktionen und Admin-Einstellungen (v1.5)
- Kategorie-Baum mit Farben, Auf-/Zuklappen und Inline-Bearbeitung - AJAX-Handler für Produkt- und Kategorieaktionen (Best EK, Status-Toggle) - Admin: Ref-Schema und Standard-Aufklapp-Einstellung - CSS/JS erweitert für Baumansicht und modale Dialoge - bin/ zu .gitignore hinzugefügt Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
4b7046132c
commit
1c3f88e6e5
22 changed files with 237 additions and 30 deletions
1
.gitignore
vendored
Normal file → Executable file
1
.gitignore
vendored
Normal file → Executable file
|
|
@ -3,3 +3,4 @@
|
|||
*.swo
|
||||
*~
|
||||
.DS_Store
|
||||
bin/
|
||||
|
|
|
|||
0
COPYING
Normal file → Executable file
0
COPYING
Normal file → Executable file
0
ChangeLog.md
Normal file → Executable file
0
ChangeLog.md
Normal file → Executable file
0
PRODUKT-UMBENENNUNG.md
Normal file → Executable file
0
PRODUKT-UMBENENNUNG.md
Normal file → Executable file
0
README.md
Normal file → Executable file
0
README.md
Normal file → Executable file
0
REFERENZ-SCHEMA.md
Normal file → Executable file
0
REFERENZ-SCHEMA.md
Normal file → Executable file
0
admin/about.php
Normal file → Executable file
0
admin/about.php
Normal file → Executable file
0
admin/setup.php
Normal file → Executable file
0
admin/setup.php
Normal file → Executable file
34
ajax/product_actions.php
Normal file → Executable file
34
ajax/product_actions.php
Normal file → Executable file
|
|
@ -491,6 +491,40 @@ switch ($action) {
|
|||
}
|
||||
break;
|
||||
|
||||
case 'update_margin':
|
||||
if (!$user->hasRight('produktverwaltung', 'write')) {
|
||||
$response['error'] = 'Permission denied';
|
||||
break;
|
||||
}
|
||||
if (empty($productId)) {
|
||||
$response['error'] = 'Missing product_id';
|
||||
break;
|
||||
}
|
||||
|
||||
$marginValue = GETPOST('value', 'alpha');
|
||||
if ($marginValue === '' || $marginValue === null) {
|
||||
$response['error'] = 'Missing value';
|
||||
break;
|
||||
}
|
||||
|
||||
$marginValue = (float) price2num($marginValue);
|
||||
|
||||
// Update preisbot_margin extrafield directly
|
||||
$sql = "UPDATE ".MAIN_DB_PREFIX."product_extrafields SET preisbot_margin = ".((float) $marginValue)." WHERE fk_object = ".((int) $productId);
|
||||
$resql = $db->query($sql);
|
||||
if ($resql) {
|
||||
// Check if row existed
|
||||
if ($db->affected_rows($resql) == 0) {
|
||||
// Maybe no extrafields row yet - insert
|
||||
$sql2 = "INSERT INTO ".MAIN_DB_PREFIX."product_extrafields (fk_object, preisbot_margin) VALUES (".((int) $productId).", ".((float) $marginValue).")";
|
||||
$db->query($sql2);
|
||||
}
|
||||
$response['success'] = true;
|
||||
} else {
|
||||
$response['error'] = 'Database error';
|
||||
}
|
||||
break;
|
||||
|
||||
case 'update_description':
|
||||
if (!$user->hasRight('produktverwaltung', 'write')) {
|
||||
$response['error'] = 'Permission denied';
|
||||
|
|
|
|||
0
build/buildzip.php
Normal file → Executable file
0
build/buildzip.php
Normal file → Executable file
0
build/makepack-produktverwaltung.conf
Normal file → Executable file
0
build/makepack-produktverwaltung.conf
Normal file → Executable file
2
core/modules/modProduktVerwaltung.class.php
Normal file → Executable file
2
core/modules/modProduktVerwaltung.class.php
Normal file → Executable file
|
|
@ -76,7 +76,7 @@ class modProduktVerwaltung extends DolibarrModules
|
|||
$this->editor_squarred_logo = ''; // Must be image filename into the module/img directory followed with @modulename. Example: 'myimage.png@produktverwaltung'
|
||||
|
||||
// Possible values for version are: 'development', 'experimental', 'dolibarr', 'dolibarr_deprecated', 'experimental_deprecated' or a version string like 'x.y.z'
|
||||
$this->version = '1.0';
|
||||
$this->version = '1.6';
|
||||
// Url to the file with your last numberversion of this module
|
||||
//$this->url_last_version = 'http://www.example.com/versionmodule.txt';
|
||||
|
||||
|
|
|
|||
104
css/produktverwaltung.css
Normal file → Executable file
104
css/produktverwaltung.css
Normal file → Executable file
|
|
@ -75,8 +75,19 @@
|
|||
}
|
||||
.pv-tree ul {
|
||||
list-style: none;
|
||||
padding-left: 24px;
|
||||
padding-left: 20px;
|
||||
margin: 0;
|
||||
border-left: 2px solid #dde3ea;
|
||||
}
|
||||
/* Abstand zwischen Geschwister-Kategorien auf Ebene 1 (Root) */
|
||||
.pv-tree > li + li {
|
||||
margin-top: 12px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid #dde3ea;
|
||||
}
|
||||
/* Abstand zwischen Geschwister-Kategorien auf tieferen Ebenen */
|
||||
.pv-tree ul > li + li {
|
||||
margin-top: 4px;
|
||||
}
|
||||
.pv-tree li {
|
||||
margin: 0;
|
||||
|
|
@ -93,6 +104,24 @@
|
|||
font-weight: 600;
|
||||
font-size: 0.95em;
|
||||
}
|
||||
/* Ebene 1: größer */
|
||||
.pv-tree > li > .pv-category-header {
|
||||
font-size: 1.05em;
|
||||
padding: 6px 10px;
|
||||
background: var(--colorbacktitle1, #f0f4f8);
|
||||
border: 1px solid var(--colorborder, #dde3ea);
|
||||
}
|
||||
/* Ebene 2 */
|
||||
.pv-tree > li > .pv-category-children > ul > li > .pv-category-header {
|
||||
font-size: 0.95em;
|
||||
padding-left: 6px;
|
||||
border-left: 3px solid #b0bec5;
|
||||
}
|
||||
/* Ebene 3+ */
|
||||
.pv-tree > li > .pv-category-children > ul > li .pv-category-children ul .pv-category-header {
|
||||
font-size: 0.9em;
|
||||
color: #555;
|
||||
}
|
||||
.pv-category-header:hover {
|
||||
background: var(--colorbacklinepairhover, #f5f5f5);
|
||||
}
|
||||
|
|
@ -141,7 +170,7 @@
|
|||
opacity: 1;
|
||||
}
|
||||
.pv-category-children {
|
||||
/* animated via JS */
|
||||
padding: 2px 0 4px 0;
|
||||
}
|
||||
.pv-category-children.collapsed {
|
||||
display: none;
|
||||
|
|
@ -153,6 +182,13 @@
|
|||
border-collapse: collapse;
|
||||
width: calc(100% - 22px);
|
||||
font-size: 0.9em;
|
||||
table-layout: fixed;
|
||||
}
|
||||
.pv-products th,
|
||||
.pv-products td {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.pv-products th {
|
||||
background: var(--colorbacktitle1, #f8f8f8);
|
||||
|
|
@ -162,7 +198,6 @@
|
|||
font-size: 0.85em;
|
||||
color: #555;
|
||||
border-bottom: 2px solid var(--colorborder, #ddd);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.pv-products td {
|
||||
padding: 3px 8px;
|
||||
|
|
@ -175,7 +210,6 @@
|
|||
.pv-products .pv-col-status {
|
||||
width: 40px;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.pv-products .pv-col-status .fas {
|
||||
font-size: 0.75em;
|
||||
|
|
@ -188,30 +222,25 @@
|
|||
color: #ccc;
|
||||
}
|
||||
.pv-products .pv-col-ref {
|
||||
width: 160px;
|
||||
width: 200px;
|
||||
font-family: monospace;
|
||||
font-weight: 600;
|
||||
}
|
||||
.pv-products .pv-col-label {
|
||||
/* flex */
|
||||
/* nimmt den restlichen Platz */
|
||||
}
|
||||
.pv-products .pv-col-desc {
|
||||
max-width: 200px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
width: 180px;
|
||||
font-size: 0.85em;
|
||||
color: #666;
|
||||
}
|
||||
.pv-products .pv-col-price {
|
||||
width: 80px;
|
||||
width: 110px;
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.pv-products .pv-col-margin {
|
||||
width: 60px;
|
||||
width: 70px;
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.pv-supplier-tag {
|
||||
display: inline-block;
|
||||
|
|
@ -236,10 +265,17 @@
|
|||
.pv-margin-warn {
|
||||
color: #e74c3c;
|
||||
}
|
||||
.pv-margin-extreme {
|
||||
color: #e74c3c;
|
||||
font-weight: 700;
|
||||
}
|
||||
.pv-margin-extreme .fas {
|
||||
font-size: 0.8em;
|
||||
margin-right: 2px;
|
||||
}
|
||||
.pv-products .pv-col-actions {
|
||||
width: 110px;
|
||||
width: 120px;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.pv-products .pv-col-actions a,
|
||||
.pv-products .pv-col-actions button {
|
||||
|
|
@ -320,6 +356,42 @@
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
/* === Archiv Sektion === */
|
||||
.pv-archive {
|
||||
margin-top: 15px;
|
||||
border: 2px solid #bdc3c7;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
opacity: 0.7;
|
||||
}
|
||||
.pv-archive:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
.pv-archive-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
background: #ecf0f1;
|
||||
border-bottom: 1px solid #bdc3c7;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
color: #7f8c8d;
|
||||
}
|
||||
.pv-archive-header:hover {
|
||||
background: #dfe6e9;
|
||||
}
|
||||
.pv-archive-body {
|
||||
padding: 0;
|
||||
}
|
||||
.pv-archive-body.collapsed {
|
||||
display: none;
|
||||
}
|
||||
.pv-archive .pv-products {
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* === Kategorie-Zuweisungs-Dialog === */
|
||||
.pv-dialog-overlay {
|
||||
position: fixed;
|
||||
|
|
|
|||
0
export.php
Normal file → Executable file
0
export.php
Normal file → Executable file
0
img/README.md
Normal file → Executable file
0
img/README.md
Normal file → Executable file
51
js/produktverwaltung.js
Normal file → Executable file
51
js/produktverwaltung.js
Normal file → Executable file
|
|
@ -80,6 +80,15 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
});
|
||||
}
|
||||
|
||||
// Archiv header toggle
|
||||
var archiveHeader = document.querySelector('.pv-archive-header');
|
||||
if (archiveHeader) {
|
||||
archiveHeader.addEventListener('click', function() {
|
||||
var body = document.querySelector('.pv-archive-body');
|
||||
if (body) body.classList.toggle('collapsed');
|
||||
});
|
||||
}
|
||||
|
||||
// === Ref-Schema toggle ===
|
||||
var schemaBox = document.querySelector('.pv-ref-schema');
|
||||
if (schemaBox) {
|
||||
|
|
@ -187,10 +196,15 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
function startEditing(el) {
|
||||
if (el.querySelector('.pv-edit-input')) return; // Already editing
|
||||
|
||||
var field = el.dataset.field; // 'ref', 'label', or 'description'
|
||||
var field = el.dataset.field; // 'ref', 'label', 'description', or 'margin'
|
||||
var productId = el.dataset.productId;
|
||||
var currentValue = el.textContent.trim();
|
||||
|
||||
// Margin-Feld: nur den Zahlenwert extrahieren (ohne %, ohne Warn-Icons)
|
||||
if (field === 'margin') {
|
||||
currentValue = currentValue.replace(/[^0-9,.\-]/g, '').trim();
|
||||
}
|
||||
|
||||
var input = document.createElement('input');
|
||||
input.type = 'text';
|
||||
input.className = 'pv-edit-input';
|
||||
|
|
@ -198,6 +212,11 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
if (field === 'ref') {
|
||||
input.style.textTransform = 'uppercase';
|
||||
}
|
||||
if (field === 'margin') {
|
||||
input.style.width = '70px';
|
||||
input.style.textAlign = 'right';
|
||||
input.placeholder = '%';
|
||||
}
|
||||
|
||||
el.textContent = '';
|
||||
el.appendChild(input);
|
||||
|
|
@ -215,6 +234,30 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
return;
|
||||
}
|
||||
|
||||
// Margin-Feld: preisbot_margin Extrafield speichern
|
||||
if (field === 'margin') {
|
||||
var parsedVal = newValue.replace(',', '.');
|
||||
el.classList.add('pv-saving');
|
||||
ajaxCall('update_margin', {
|
||||
product_id: productId,
|
||||
value: parsedVal
|
||||
}, function(response) {
|
||||
el.classList.remove('pv-saving');
|
||||
if (response.success) {
|
||||
el.textContent = newValue.replace('.', ',') + '%';
|
||||
showToast(pvLang.saveSuccess || 'Gespeichert', 'success');
|
||||
} else {
|
||||
el.textContent = currentValue ? currentValue + '%' : '-';
|
||||
showToast(response.error || pvLang.saveError || 'Fehler', 'error');
|
||||
}
|
||||
}, function() {
|
||||
el.classList.remove('pv-saving');
|
||||
el.textContent = currentValue ? currentValue + '%' : '-';
|
||||
showToast(pvLang.saveError || 'Fehler beim Speichern', 'error');
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
el.classList.add('pv-saving');
|
||||
ajaxCall('update_' + field, {
|
||||
product_id: productId,
|
||||
|
|
@ -236,7 +279,11 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
}
|
||||
|
||||
function cancel() {
|
||||
el.textContent = currentValue;
|
||||
if (field === 'margin') {
|
||||
el.textContent = currentValue ? currentValue + '%' : '-';
|
||||
} else {
|
||||
el.textContent = currentValue;
|
||||
}
|
||||
}
|
||||
|
||||
input.addEventListener('keydown', function(e) {
|
||||
|
|
|
|||
1
langs/de_DE/produktverwaltung.lang
Normal file → Executable file
1
langs/de_DE/produktverwaltung.lang
Normal file → Executable file
|
|
@ -65,6 +65,7 @@ DefaultExpandedHelp = Wenn aktiviert, werden alle Kategorien beim Seitenaufruf a
|
|||
# Status
|
||||
OnSale = Verkaufbar
|
||||
OnBuy = Beziehbar
|
||||
ArchivedProducts = Archiv (nicht verkaufbar / nicht beziehbar)
|
||||
CalcMargin = Aufschlag
|
||||
|
||||
# Kategorien
|
||||
|
|
|
|||
0
langs/en_US/produktverwaltung.lang
Normal file → Executable file
0
langs/en_US/produktverwaltung.lang
Normal file → Executable file
0
lib/produktverwaltung.lib.php
Normal file → Executable file
0
lib/produktverwaltung.lib.php
Normal file → Executable file
0
modulebuilder.txt
Normal file → Executable file
0
modulebuilder.txt
Normal file → Executable file
74
produktverwaltungindex.php
Normal file → Executable file
74
produktverwaltungindex.php
Normal file → Executable file
|
|
@ -87,11 +87,13 @@ if (is_array($fullTree)) {
|
|||
// Load all products with their category assignments
|
||||
$productsPerCat = array(); // cat_id => array of products
|
||||
$allProductIds = array();
|
||||
$archivedProducts = array(); // tosell=0 AND tobuy=0
|
||||
|
||||
$sql = "SELECT DISTINCT p.rowid, p.ref, p.label, p.description, p.price, p.price_ttc,";
|
||||
$sql .= " p.tobuy, p.tosell, p.fk_product_type";
|
||||
$sql .= ", (SELECT pfp.unitprice FROM ".MAIN_DB_PREFIX."product_fournisseur_price as pfp WHERE pfp.fk_product = p.rowid ORDER BY pfp.unitprice ASC LIMIT 1) as best_buy_price";
|
||||
$sql .= ", (SELECT LEFT(COALESCE(NULLIF(s2.name_alias,''), s2.nom), 3) FROM ".MAIN_DB_PREFIX."product_fournisseur_price as pfp2 LEFT JOIN ".MAIN_DB_PREFIX."societe as s2 ON s2.rowid = pfp2.fk_soc WHERE pfp2.fk_product = p.rowid ORDER BY pfp2.unitprice ASC LIMIT 1) as best_buy_supplier";
|
||||
$sql .= ", (SELECT pfe3.kaufmenge FROM ".MAIN_DB_PREFIX."product_fournisseur_price as pfp3 LEFT JOIN ".MAIN_DB_PREFIX."product_fournisseur_price_extrafields as pfe3 ON pfe3.fk_object = pfp3.rowid WHERE pfp3.fk_product = p.rowid ORDER BY pfp3.unitprice ASC LIMIT 1) as best_buy_kaufmenge";
|
||||
if ($hasMarginField) {
|
||||
$sql .= ", pe.preisbot_margin";
|
||||
}
|
||||
|
|
@ -116,6 +118,7 @@ if ($resql) {
|
|||
'price_ttc' => $obj->price_ttc,
|
||||
'best_buy_price' => $obj->best_buy_price,
|
||||
'best_buy_supplier' => $obj->best_buy_supplier,
|
||||
'best_buy_kaufmenge' => $obj->best_buy_kaufmenge,
|
||||
'tosell' => $obj->tosell,
|
||||
'tobuy' => $obj->tobuy,
|
||||
'fk_product_type' => $obj->fk_product_type,
|
||||
|
|
@ -125,6 +128,12 @@ if ($resql) {
|
|||
|
||||
$allProductIds[$obj->rowid] = true;
|
||||
|
||||
// Archiv: weder verkaufbar noch beziehbar → separat sammeln
|
||||
if (empty($obj->tosell) && empty($obj->tobuy)) {
|
||||
$archivedProducts[$obj->rowid] = $productData;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!empty($obj->fk_categorie)) {
|
||||
$catId = (int) $obj->fk_categorie;
|
||||
if (!isset($productsPerCat[$catId])) {
|
||||
|
|
@ -142,6 +151,7 @@ $productsWithoutCat = array();
|
|||
$sql2 = "SELECT p.rowid, p.ref, p.label, p.description, p.price, p.price_ttc, p.tobuy, p.tosell, p.fk_product_type";
|
||||
$sql2 .= ", (SELECT pfp.unitprice FROM ".MAIN_DB_PREFIX."product_fournisseur_price as pfp WHERE pfp.fk_product = p.rowid ORDER BY pfp.unitprice ASC LIMIT 1) as best_buy_price";
|
||||
$sql2 .= ", (SELECT LEFT(COALESCE(NULLIF(s2.name_alias,''), s2.nom), 3) FROM ".MAIN_DB_PREFIX."product_fournisseur_price as pfp2 LEFT JOIN ".MAIN_DB_PREFIX."societe as s2 ON s2.rowid = pfp2.fk_soc WHERE pfp2.fk_product = p.rowid ORDER BY pfp2.unitprice ASC LIMIT 1) as best_buy_supplier";
|
||||
$sql2 .= ", (SELECT pfe3.kaufmenge FROM ".MAIN_DB_PREFIX."product_fournisseur_price as pfp3 LEFT JOIN ".MAIN_DB_PREFIX."product_fournisseur_price_extrafields as pfe3 ON pfe3.fk_object = pfp3.rowid WHERE pfp3.fk_product = p.rowid ORDER BY pfp3.unitprice ASC LIMIT 1) as best_buy_kaufmenge";
|
||||
if ($hasMarginField) {
|
||||
$sql2 .= ", pe.preisbot_margin";
|
||||
}
|
||||
|
|
@ -156,7 +166,7 @@ $sql2 .= " ORDER BY p.ref ASC";
|
|||
$resql2 = $db->query($sql2);
|
||||
if ($resql2) {
|
||||
while ($obj = $db->fetch_object($resql2)) {
|
||||
$productsWithoutCat[$obj->rowid] = array(
|
||||
$productData2 = array(
|
||||
'id' => $obj->rowid,
|
||||
'ref' => $obj->ref,
|
||||
'label' => $obj->label,
|
||||
|
|
@ -165,11 +175,18 @@ if ($resql2) {
|
|||
'price_ttc' => $obj->price_ttc,
|
||||
'best_buy_price' => $obj->best_buy_price,
|
||||
'best_buy_supplier' => $obj->best_buy_supplier,
|
||||
'best_buy_kaufmenge' => $obj->best_buy_kaufmenge,
|
||||
'tosell' => $obj->tosell,
|
||||
'tobuy' => $obj->tobuy,
|
||||
'fk_product_type' => $obj->fk_product_type,
|
||||
'margin' => $hasMarginField ? $obj->preisbot_margin : null,
|
||||
);
|
||||
// Archiv: weder verkaufbar noch beziehbar
|
||||
if (empty($obj->tosell) && empty($obj->tobuy)) {
|
||||
$archivedProducts[$obj->rowid] = $productData2;
|
||||
} else {
|
||||
$productsWithoutCat[$obj->rowid] = $productData2;
|
||||
}
|
||||
}
|
||||
$db->free($resql2);
|
||||
}
|
||||
|
|
@ -312,6 +329,20 @@ if ($countWithout > 0) {
|
|||
print '</div>';
|
||||
}
|
||||
|
||||
// === Archiv (tosell=0 AND tobuy=0) ===
|
||||
$countArchived = count($archivedProducts);
|
||||
if ($countArchived > 0) {
|
||||
print '<div class="pv-archive">';
|
||||
print '<div class="pv-archive-header">';
|
||||
print '<span class="fas fa-archive" style="color:#95a5a6;"></span>';
|
||||
print ' '.$langs->trans("ArchivedProducts").' <span class="badge" style="background:#95a5a6;color:#fff;">'.$countArchived.'</span>';
|
||||
print '</div>';
|
||||
print '<div class="pv-archive-body collapsed">';
|
||||
renderProductTable($archivedProducts, $hasMarginField, $canEdit, $canDelete, 0, $langs, $conf);
|
||||
print '</div>';
|
||||
print '</div>';
|
||||
}
|
||||
|
||||
llxFooter();
|
||||
$db->close();
|
||||
|
||||
|
|
@ -447,14 +478,21 @@ function renderProductTable($products, $hasMarginField, $canEdit, $canDelete, $c
|
|||
$description = dol_escape_htmltag(dol_trunc($prod['description'], 80));
|
||||
$bestBuyPrice = !empty($prod['best_buy_price']) ? $prod['best_buy_price'] : 0;
|
||||
$sellPrice = $prod['price'];
|
||||
$kaufmenge = !empty($prod['best_buy_kaufmenge']) ? (float) $prod['best_buy_kaufmenge'] : 0;
|
||||
|
||||
// kaufmenge-Korrektur: Wenn kaufmenge > 1, wird EK * kaufmenge als korrigierter EK pro VK-Einheit genutzt
|
||||
$correctedBuyPrice = $bestBuyPrice;
|
||||
if ($kaufmenge > 1 && $bestBuyPrice > 0) {
|
||||
$correctedBuyPrice = $bestBuyPrice * $kaufmenge;
|
||||
}
|
||||
|
||||
// Calculate actual margin: ((VK - EK) / EK * 100)
|
||||
$calcMargin = '';
|
||||
if ($bestBuyPrice > 0 && $sellPrice > 0) {
|
||||
$calcMargin = round(($sellPrice - $bestBuyPrice) / $bestBuyPrice * 100, 1);
|
||||
if ($correctedBuyPrice > 0 && $sellPrice > 0) {
|
||||
$calcMargin = round(($sellPrice - $correctedBuyPrice) / $correctedBuyPrice * 100, 1);
|
||||
}
|
||||
|
||||
print '<tr data-product-id="'.$productId.'" data-best-buy-price="'.($bestBuyPrice > 0 ? $bestBuyPrice : '').'" data-sell-price="'.($sellPrice > 0 ? $sellPrice : '').'">';
|
||||
print '<tr data-product-id="'.$productId.'" data-best-buy-price="'.($bestBuyPrice > 0 ? $bestBuyPrice : '').'" data-corrected-buy-price="'.($correctedBuyPrice > 0 ? $correctedBuyPrice : '').'" data-sell-price="'.($sellPrice > 0 ? $sellPrice : '').'">';
|
||||
|
||||
// Status (tosell/tobuy) - nur aktive anzeigen
|
||||
print '<td class="pv-col-status">';
|
||||
|
|
@ -493,13 +531,17 @@ function renderProductTable($products, $hasMarginField, $canEdit, $canDelete, $c
|
|||
}
|
||||
print '</td>';
|
||||
|
||||
// Best EK (günstigster Lieferantenpreis)
|
||||
// Best EK (günstigster Lieferantenpreis, bei kaufmenge > 1 korrigiert)
|
||||
$displayBuyPrice = $correctedBuyPrice;
|
||||
print '<td class="pv-col-price">';
|
||||
if ($bestBuyPrice > 0) {
|
||||
print price($bestBuyPrice, 0, $langs, 1, -1, 2);
|
||||
print price($displayBuyPrice, 0, $langs, 1, -1, 2);
|
||||
if (!empty($prod['best_buy_supplier'])) {
|
||||
print ' <span class="pv-supplier-tag">'.dol_escape_htmltag($prod['best_buy_supplier']).'</span>';
|
||||
}
|
||||
if ($kaufmenge > 1) {
|
||||
print ' <span class="pv-supplier-tag" title="EK-Stück: '.price($bestBuyPrice, 0, $langs, 1, -1, 2).' × '.(int)$kaufmenge.'" style="background:#e67e22;">×'.(int)$kaufmenge.'</span>';
|
||||
}
|
||||
} else {
|
||||
print '-';
|
||||
}
|
||||
|
|
@ -510,22 +552,32 @@ function renderProductTable($products, $hasMarginField, $canEdit, $canDelete, $c
|
|||
print $sellPrice > 0 ? price($sellPrice, 0, $langs, 1, -1, 2) : '-';
|
||||
print '</td>';
|
||||
|
||||
// Stored Margin % (preisbot_margin)
|
||||
// Stored Margin % (preisbot_margin) - editierbar per Doppelklick
|
||||
if ($hasMarginField) {
|
||||
print '<td class="pv-col-margin">';
|
||||
print !empty($prod['margin']) ? price($prod['margin'], 0, $langs, 1, -1, 1).'%' : '-';
|
||||
$marginVal = !empty($prod['margin']) ? price($prod['margin'], 0, $langs, 1, -1, 1).'%' : '-';
|
||||
if ($canEdit) {
|
||||
print '<span class="pv-editable" data-field="margin" data-product-id="'.$productId.'">'.$marginVal.'</span>';
|
||||
} else {
|
||||
print $marginVal;
|
||||
}
|
||||
print '</td>';
|
||||
}
|
||||
|
||||
// Calculated Margin %
|
||||
// Calculated Margin % (nur Anzeige, nicht editierbar)
|
||||
print '<td class="pv-col-margin">';
|
||||
if ($calcMargin !== '') {
|
||||
$marginClass = '';
|
||||
if ($hasMarginField && !empty($prod['margin'])) {
|
||||
$warnIcon = '';
|
||||
// Extreme Margen: < 0% oder > 500% → rot mit Warnsymbol
|
||||
if ((float) $calcMargin < 0 || (float) $calcMargin > 500) {
|
||||
$marginClass = ' pv-margin-extreme';
|
||||
$warnIcon = '<span class="fas fa-exclamation-triangle" title="'.(($kaufmenge > 1) ? 'Kaufmenge: '.$kaufmenge : 'Ungewöhnliche Marge - prüfen!').'"></span> ';
|
||||
} elseif ($hasMarginField && !empty($prod['margin'])) {
|
||||
$diff = abs((float) $calcMargin - (float) $prod['margin']);
|
||||
$marginClass = $diff > 2 ? ' pv-margin-warn' : ' pv-margin-ok';
|
||||
}
|
||||
print '<span class="pv-calc-margin'.$marginClass.'">'.price($calcMargin, 0, $langs, 1, -1, 1).'%</span>';
|
||||
print '<span class="pv-calc-margin'.$marginClass.'">'.$warnIcon.price($calcMargin, 0, $langs, 1, -1, 1).'%</span>';
|
||||
} else {
|
||||
print '-';
|
||||
}
|
||||
|
|
|
|||
0
sql/dolibarr_allversions.sql
Normal file → Executable file
0
sql/dolibarr_allversions.sql
Normal file → Executable file
Loading…
Reference in a new issue