Compare commits
5 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1b5221ed39 | |||
| bb761ebd89 | |||
| ea2609c66e | |||
| 458f393612 | |||
| 770a2b6416 |
23 changed files with 989 additions and 98 deletions
0
.claude/settings.json
Normal file → Executable file
0
.claude/settings.json
Normal file → Executable file
0
MIGRATION_MULTITYPE.md
Normal file → Executable file
0
MIGRATION_MULTITYPE.md
Normal file → Executable file
38
README.md
38
README.md
|
|
@ -6,18 +6,30 @@ Erweitert Rechnungen, Angebote und Kundenaufträge um **Sections**, **Textzeilen
|
||||||
|
|
||||||
## 🔑 ODT Template Schlüsselwörter
|
## 🔑 ODT Template Schlüsselwörter
|
||||||
|
|
||||||
Diese Variablen stehen in ODT-Templates zur Verfügung:
|
### Zeilen-Variablen (pro Zeile in row.lines)
|
||||||
|
|
||||||
| Variable | Wert | Beschreibung |
|
| Variable | Wert | Beschreibung |
|
||||||
|----------|------|--------------|
|
|----------|------|--------------|
|
||||||
| `{line_is_section}` | 1/0 | Zeile ist eine Section (Überschrift) |
|
| `{line_is_section}` | 1/"" | Zeile ist eine Section (Überschrift) |
|
||||||
| `{line_is_textline}` | 1/0 | Zeile ist eine Textzeile |
|
| `{line_is_textline}` | 1/"" | Zeile ist eine Textzeile |
|
||||||
| `{line_is_subtotal}` | 1/0 | Zeile ist eine Zwischensumme |
|
| `{line_is_subtotal}` | 1/"" | Zeile ist eine Zwischensumme |
|
||||||
| `{line_is_product}` | 1/0 | Zeile ist ein Produkt (inkl. Sections/Text/Subtotals) |
|
| `{line_is_product}` | 1/"" | Zeile ist ein Produkt mit Produktreferenz |
|
||||||
| `{line_is_normal}` | 1/0 | Zeile ist ein normales Produkt (KEIN Section/Text/Subtotal) |
|
| `{line_is_free_line}` | 1/"" | Zeile ist eine freie Zeile (ohne Produktreferenz) |
|
||||||
| `{line_is_special}` | 1/0 | Zeile ist Section, Text ODER Subtotal |
|
| `{line_is_normal}` | 1/"" | Zeile ist normal (special_code = 0) |
|
||||||
|
| `{line_is_special}` | 1/"" | Zeile ist Section, Text ODER Subtotal |
|
||||||
| `{line_special_code}` | 0-102 | special_code Wert der Zeile |
|
| `{line_special_code}` | 0-102 | special_code Wert der Zeile |
|
||||||
|
|
||||||
|
### Globale Variablen (für das gesamte Dokument)
|
||||||
|
|
||||||
|
| Variable | Wert | Beschreibung |
|
||||||
|
|----------|------|--------------|
|
||||||
|
| `{object_has_sections}` | 1/"" | Dokument enthält mindestens eine Section |
|
||||||
|
| `{object_has_textlines}` | 1/"" | Dokument enthält mindestens eine Textzeile |
|
||||||
|
| `{object_has_speciallines}` | 1/"" | Dokument enthält Sections, Textzeilen oder Subtotals |
|
||||||
|
| `{object_count_sections}` | Zahl | Anzahl der Sections im Dokument |
|
||||||
|
| `{object_count_textlines}` | Zahl | Anzahl der Textzeilen im Dokument |
|
||||||
|
| `{object_count_subtotals}` | Zahl | Anzahl der Zwischensummen im Dokument |
|
||||||
|
|
||||||
### special_code Werte
|
### special_code Werte
|
||||||
|
|
||||||
| Typ | special_code |
|
| Typ | special_code |
|
||||||
|
|
@ -30,6 +42,10 @@ Diese Variablen stehen in ODT-Templates zur Verfügung:
|
||||||
### ODT Template Beispiel
|
### ODT Template Beispiel
|
||||||
|
|
||||||
```
|
```
|
||||||
|
[!-- IF {object_has_sections} --]
|
||||||
|
Dieses Dokument enthält {object_count_sections} Section(s).
|
||||||
|
[!-- ENDIF {object_has_sections} --]
|
||||||
|
|
||||||
[!-- BEGIN row.lines --]
|
[!-- BEGIN row.lines --]
|
||||||
|
|
||||||
[!-- IF {line_is_section} --]
|
[!-- IF {line_is_section} --]
|
||||||
|
|
@ -42,9 +58,13 @@ Diese Variablen stehen in ODT-Templates zur Verfügung:
|
||||||
{line_desc}
|
{line_desc}
|
||||||
[!-- ENDIF {line_is_textline} --]
|
[!-- ENDIF {line_is_textline} --]
|
||||||
|
|
||||||
[!-- IF {line_is_normal} --]
|
[!-- IF {line_is_product} --]
|
||||||
|
{line_pos} {line_qty} {line_ref} {line_desc} {line_up_locale} € {line_price_ht_locale} €
|
||||||
|
[!-- ENDIF {line_is_product} --]
|
||||||
|
|
||||||
|
[!-- IF {line_is_free_line} --]
|
||||||
{line_pos} {line_qty} {line_desc} {line_up_locale} € {line_price_ht_locale} €
|
{line_pos} {line_qty} {line_desc} {line_up_locale} € {line_price_ht_locale} €
|
||||||
[!-- ENDIF {line_is_normal} --]
|
[!-- ENDIF {line_is_free_line} --]
|
||||||
|
|
||||||
[!-- IF {line_is_subtotal} --]
|
[!-- IF {line_is_subtotal} --]
|
||||||
───────────────────────────────────────
|
───────────────────────────────────────
|
||||||
|
|
|
||||||
0
ajax/add_to_section.php
Normal file → Executable file
0
ajax/add_to_section.php
Normal file → Executable file
0
ajax/check_subtotal.php
Normal file → Executable file
0
ajax/check_subtotal.php
Normal file → Executable file
0
ajax/cleanup_subtotals.php
Normal file → Executable file
0
ajax/cleanup_subtotals.php
Normal file → Executable file
0
ajax/fix_section_hierarchy.php
Normal file → Executable file
0
ajax/fix_section_hierarchy.php
Normal file → Executable file
0
ajax/fix_sections.php
Normal file → Executable file
0
ajax/fix_sections.php
Normal file → Executable file
565
ajax/import_from_origin.php
Normal file
565
ajax/import_from_origin.php
Normal file
|
|
@ -0,0 +1,565 @@
|
||||||
|
<?php
|
||||||
|
/* Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
|
||||||
|
*
|
||||||
|
* Import sections/textlines from origin document (Angebot→Auftrag→Rechnung)
|
||||||
|
*/
|
||||||
|
|
||||||
|
define('NOTOKENRENEWAL', 1);
|
||||||
|
|
||||||
|
$res = 0;
|
||||||
|
if (!$res && file_exists("../../../main.inc.php")) $res = @include "../../../main.inc.php";
|
||||||
|
if (!$res && file_exists("../../../../main.inc.php")) $res = @include "../../../../main.inc.php";
|
||||||
|
if (!$res) die("Include of main fails");
|
||||||
|
|
||||||
|
require_once DOL_DOCUMENT_ROOT.'/compta/facture/class/facture.class.php';
|
||||||
|
require_once DOL_DOCUMENT_ROOT.'/comm/propal/class/propal.class.php';
|
||||||
|
require_once DOL_DOCUMENT_ROOT.'/commande/class/commande.class.php';
|
||||||
|
dol_include_once('/subtotaltitle/lib/subtotaltitle.lib.php');
|
||||||
|
require_once __DIR__.'/../class/DocumentTypeHelper.class.php';
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
$action = GETPOST('action', 'alpha');
|
||||||
|
$target_id = GETPOST('target_id', 'int');
|
||||||
|
$target_type = GETPOST('target_type', 'alpha');
|
||||||
|
|
||||||
|
subtotaltitle_debug_log('📥 import_from_origin: action='.$action.', target_id='.$target_id.', target_type='.$target_type);
|
||||||
|
|
||||||
|
if (!$target_id || !$target_type) {
|
||||||
|
echo json_encode(array('success' => false, 'error' => 'Missing parameters'));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hole die richtigen Tabellennamen für Ziel-Dokumenttyp
|
||||||
|
$target_tables = DocumentTypeHelper::getTableNames($target_type);
|
||||||
|
if (!$target_tables) {
|
||||||
|
echo json_encode(array('success' => false, 'error' => 'Invalid target document type'));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ermittelt das Ursprungsdokument basierend auf Zieldokument
|
||||||
|
* Dolibarr speichert die Herkunft in origin/origin_id ODER in llx_element_element
|
||||||
|
*/
|
||||||
|
function getOriginDocument($db, $target_id, $target_type)
|
||||||
|
{
|
||||||
|
$target_tables = DocumentTypeHelper::getTableNames($target_type);
|
||||||
|
if (!$target_tables) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lade Zieldokument
|
||||||
|
$target_doc = DocumentTypeHelper::loadDocument($target_type, $target_id, $db);
|
||||||
|
if (!$target_doc) {
|
||||||
|
subtotaltitle_debug_log('❌ Zieldokument nicht gefunden: '.$target_type.' #'.$target_id);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
subtotaltitle_debug_log('🔍 Zieldokument geladen: element='.$target_doc->element.', origin='.($target_doc->origin ?? 'NULL').', origin_id='.($target_doc->origin_id ?? 'NULL'));
|
||||||
|
|
||||||
|
// Methode 1: Direkte Objekteigenschaften prüfen
|
||||||
|
$origin = $target_doc->origin ?? null;
|
||||||
|
$origin_id = $target_doc->origin_id ?? null;
|
||||||
|
|
||||||
|
// Methode 2: Falls nicht gesetzt, prüfe llx_element_element Tabelle
|
||||||
|
if (empty($origin) || empty($origin_id)) {
|
||||||
|
$elementType = $target_doc->element; // z.B. 'commande', 'facture', 'propal'
|
||||||
|
subtotaltitle_debug_log('🔍 Suche in element_element für '.$elementType.' #'.$target_id);
|
||||||
|
|
||||||
|
$sql_origin = "SELECT fk_source, sourcetype FROM ".MAIN_DB_PREFIX."element_element";
|
||||||
|
$sql_origin .= " WHERE fk_target = ".(int)$target_id;
|
||||||
|
$sql_origin .= " AND targettype = '".$db->escape($elementType)."'";
|
||||||
|
$sql_origin .= " LIMIT 1";
|
||||||
|
|
||||||
|
subtotaltitle_debug_log('SQL: '.$sql_origin);
|
||||||
|
|
||||||
|
$res_origin = $db->query($sql_origin);
|
||||||
|
if ($res_origin && $db->num_rows($res_origin) > 0) {
|
||||||
|
$obj_origin = $db->fetch_object($res_origin);
|
||||||
|
$origin = $obj_origin->sourcetype;
|
||||||
|
$origin_id = $obj_origin->fk_source;
|
||||||
|
subtotaltitle_debug_log('✅ Gefunden in element_element: '.$origin.' #'.$origin_id);
|
||||||
|
} else {
|
||||||
|
subtotaltitle_debug_log('❌ Kein Eintrag in element_element gefunden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prüfe ob origin gesetzt ist
|
||||||
|
if (empty($origin) || empty($origin_id)) {
|
||||||
|
subtotaltitle_debug_log('❌ Kein Ursprungsdokument verknüpft (weder direkt noch in element_element)');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mappe Dolibarr origin zu unserem document_type
|
||||||
|
$origin_type_map = array(
|
||||||
|
'propal' => 'propal',
|
||||||
|
'commande' => 'order',
|
||||||
|
'facture' => 'invoice',
|
||||||
|
'order_supplier' => null, // Lieferantenauftrag - nicht unterstützt
|
||||||
|
'invoice_supplier' => null // Lieferantenrechnung - nicht unterstützt
|
||||||
|
);
|
||||||
|
|
||||||
|
$origin_type = isset($origin_type_map[$origin]) ? $origin_type_map[$origin] : null;
|
||||||
|
if (!$origin_type) {
|
||||||
|
subtotaltitle_debug_log('❌ Nicht unterstützter Ursprungstyp: '.$origin);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lade Ursprungsdokument
|
||||||
|
$origin_doc = DocumentTypeHelper::loadDocument($origin_type, $origin_id, $db);
|
||||||
|
if (!$origin_doc) {
|
||||||
|
subtotaltitle_debug_log('❌ Ursprungsdokument nicht gefunden: '.$origin_type.' #'.$origin_id);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
subtotaltitle_debug_log('✅ Ursprungsdokument gefunden: '.$origin_type.' #'.$origin_id);
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'document' => $origin_doc,
|
||||||
|
'type' => $origin_type,
|
||||||
|
'id' => $origin_id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sucht die passende Produktzeile im Zieldokument basierend auf fk_product
|
||||||
|
*/
|
||||||
|
function findMatchingProductLine($db, $target_id, $target_type, $source_product_id)
|
||||||
|
{
|
||||||
|
if (!$source_product_id) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$target_tables = DocumentTypeHelper::getTableNames($target_type);
|
||||||
|
|
||||||
|
// Suche nach Zeile mit gleichem Produkt im Zieldokument
|
||||||
|
$sql = "SELECT rowid FROM ".MAIN_DB_PREFIX.$target_tables['lines_table'];
|
||||||
|
$sql .= " WHERE ".$target_tables['fk_parent']." = ".(int)$target_id;
|
||||||
|
$sql .= " AND fk_product = ".(int)$source_product_id;
|
||||||
|
$sql .= " LIMIT 1";
|
||||||
|
|
||||||
|
$resql = $db->query($sql);
|
||||||
|
if ($resql && $db->num_rows($resql) > 0) {
|
||||||
|
$obj = $db->fetch_object($resql);
|
||||||
|
return $obj->rowid;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action == 'check') {
|
||||||
|
// ========== PRÜFE OB IMPORT MÖGLICH IST ==========
|
||||||
|
|
||||||
|
$origin = getOriginDocument($db, $target_id, $target_type);
|
||||||
|
if (!$origin) {
|
||||||
|
echo json_encode(array(
|
||||||
|
'success' => true,
|
||||||
|
'has_origin' => false,
|
||||||
|
'message' => 'Kein Ursprungsdokument verknüpft'
|
||||||
|
));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$origin_tables = DocumentTypeHelper::getTableNames($origin['type']);
|
||||||
|
|
||||||
|
// Zähle Sections und Textlines im Ursprungsdokument
|
||||||
|
$sql = "SELECT COUNT(*) as cnt, line_type FROM ".MAIN_DB_PREFIX."facture_lines_manager";
|
||||||
|
$sql .= " WHERE ".$origin_tables['fk_parent']." = ".(int)$origin['id'];
|
||||||
|
$sql .= " AND document_type = '".$db->escape($origin['type'])."'";
|
||||||
|
$sql .= " AND line_type IN ('section', 'text')";
|
||||||
|
$sql .= " GROUP BY line_type";
|
||||||
|
|
||||||
|
$resql = $db->query($sql);
|
||||||
|
$counts = array('section' => 0, 'text' => 0);
|
||||||
|
while ($obj = $db->fetch_object($resql)) {
|
||||||
|
$counts[$obj->line_type] = $obj->cnt;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prüfe ob im Zieldokument schon Sections existieren
|
||||||
|
$sql_target = "SELECT COUNT(*) as cnt FROM ".MAIN_DB_PREFIX."facture_lines_manager";
|
||||||
|
$sql_target .= " WHERE ".$target_tables['fk_parent']." = ".(int)$target_id;
|
||||||
|
$sql_target .= " AND document_type = '".$db->escape($target_type)."'";
|
||||||
|
$sql_target .= " AND line_type = 'section'";
|
||||||
|
$res_target = $db->query($sql_target);
|
||||||
|
$obj_target = $db->fetch_object($res_target);
|
||||||
|
$has_existing = ($obj_target && $obj_target->cnt > 0);
|
||||||
|
|
||||||
|
// Ermittle Anzeigename für Ursprungsdokument
|
||||||
|
$origin_name = '';
|
||||||
|
$origin_ref = $origin['document']->ref;
|
||||||
|
switch ($origin['type']) {
|
||||||
|
case 'propal':
|
||||||
|
$origin_name = 'Angebot '.$origin_ref;
|
||||||
|
break;
|
||||||
|
case 'order':
|
||||||
|
$origin_name = 'Auftrag '.$origin_ref;
|
||||||
|
break;
|
||||||
|
case 'invoice':
|
||||||
|
$origin_name = 'Rechnung '.$origin_ref;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode(array(
|
||||||
|
'success' => true,
|
||||||
|
'has_origin' => true,
|
||||||
|
'origin_type' => $origin['type'],
|
||||||
|
'origin_id' => $origin['id'],
|
||||||
|
'origin_ref' => $origin_ref,
|
||||||
|
'origin_name' => $origin_name,
|
||||||
|
'sections_count' => (int)$counts['section'],
|
||||||
|
'textlines_count' => (int)$counts['text'],
|
||||||
|
'has_existing' => $has_existing,
|
||||||
|
'can_import' => ($counts['section'] > 0 || $counts['text'] > 0)
|
||||||
|
));
|
||||||
|
|
||||||
|
} elseif ($action == 'import') {
|
||||||
|
// ========== KOMPLETTER IMPORT MIT RANG-SYNCHRONISATION ==========
|
||||||
|
// Strategie:
|
||||||
|
// 1. Lösche bestehende Einträge in Manager-Tabelle für Zieldokument
|
||||||
|
// 2. Importiere alle Sections/Textlines/Subtotals aus Ursprung
|
||||||
|
// 3. Synchronisiere ALLE Produkte aus Ziel-Dolibarr-Tabelle in Manager-Tabelle
|
||||||
|
// 4. Ordne Produkte den Sections zu basierend auf fk_product Matching
|
||||||
|
// 5. Produkte die NUR im Zieldokument sind, kommen am Ende
|
||||||
|
// 6. Neu-Nummerierung line_order UND rang in beiden Tabellen
|
||||||
|
|
||||||
|
$origin = getOriginDocument($db, $target_id, $target_type);
|
||||||
|
if (!$origin) {
|
||||||
|
echo json_encode(array('success' => false, 'error' => 'Kein Ursprungsdokument gefunden'));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$origin_tables = DocumentTypeHelper::getTableNames($origin['type']);
|
||||||
|
|
||||||
|
// Starte Transaktion
|
||||||
|
$db->begin();
|
||||||
|
|
||||||
|
$imported_sections = 0;
|
||||||
|
$imported_textlines = 0;
|
||||||
|
$imported_subtotals = 0;
|
||||||
|
$product_assignments = 0;
|
||||||
|
$new_products = 0;
|
||||||
|
$section_mapping = array(); // Alte Section-ID => Neue Section-ID
|
||||||
|
|
||||||
|
try {
|
||||||
|
// ============================================================
|
||||||
|
// SCHRITT 1: Lösche bestehende Einträge in Manager-Tabelle
|
||||||
|
// ============================================================
|
||||||
|
subtotaltitle_debug_log('🗑️ Lösche bestehende Manager-Einträge...');
|
||||||
|
|
||||||
|
$sql_delete = "DELETE FROM ".MAIN_DB_PREFIX."facture_lines_manager";
|
||||||
|
$sql_delete .= " WHERE ".$target_tables['fk_parent']." = ".(int)$target_id;
|
||||||
|
$sql_delete .= " AND document_type = '".$db->escape($target_type)."'";
|
||||||
|
$db->query($sql_delete);
|
||||||
|
|
||||||
|
subtotaltitle_debug_log('✅ Bestehende Einträge gelöscht');
|
||||||
|
|
||||||
|
// FK-Werte für Zieldokument
|
||||||
|
$fk_facture = ($target_type === 'invoice') ? (int)$target_id : 'NULL';
|
||||||
|
$fk_propal = ($target_type === 'propal') ? (int)$target_id : 'NULL';
|
||||||
|
$fk_commande = ($target_type === 'order') ? (int)$target_id : 'NULL';
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// SCHRITT 2: Baue Mapping fk_product → Section aus Ursprung
|
||||||
|
// ============================================================
|
||||||
|
subtotaltitle_debug_log('🗺️ Erstelle fk_product → Section Mapping...');
|
||||||
|
|
||||||
|
$product_section_map = array(); // fk_product => origin_section_id
|
||||||
|
|
||||||
|
$sql_origin_products = "SELECT m.parent_section, d.fk_product FROM ".MAIN_DB_PREFIX."facture_lines_manager m";
|
||||||
|
$sql_origin_products .= " LEFT JOIN ".MAIN_DB_PREFIX.$origin_tables['lines_table']." d ON d.rowid = m.".$origin_tables['fk_line'];
|
||||||
|
$sql_origin_products .= " WHERE m.".$origin_tables['fk_parent']." = ".(int)$origin['id'];
|
||||||
|
$sql_origin_products .= " AND m.document_type = '".$db->escape($origin['type'])."'";
|
||||||
|
$sql_origin_products .= " AND m.line_type = 'product'";
|
||||||
|
$sql_origin_products .= " AND m.parent_section IS NOT NULL";
|
||||||
|
$sql_origin_products .= " AND d.fk_product IS NOT NULL";
|
||||||
|
|
||||||
|
$res_origin_products = $db->query($sql_origin_products);
|
||||||
|
while ($row = $db->fetch_object($res_origin_products)) {
|
||||||
|
$product_section_map[$row->fk_product] = $row->parent_section;
|
||||||
|
subtotaltitle_debug_log(' Mapping: fk_product='.$row->fk_product.' → Section #'.$row->parent_section);
|
||||||
|
}
|
||||||
|
|
||||||
|
subtotaltitle_debug_log('✅ '.count($product_section_map).' Produkt-Section Mappings erstellt');
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// SCHRITT 3: Hole ALLE Einträge aus Ursprung (sortiert nach line_order)
|
||||||
|
// ============================================================
|
||||||
|
subtotaltitle_debug_log('📦 Hole alle Einträge aus Ursprungsdokument...');
|
||||||
|
|
||||||
|
$sql_origin_all = "SELECT m.*, d.fk_product FROM ".MAIN_DB_PREFIX."facture_lines_manager m";
|
||||||
|
$sql_origin_all .= " LEFT JOIN ".MAIN_DB_PREFIX.$origin_tables['lines_table']." d ON d.rowid = m.".$origin_tables['fk_line'];
|
||||||
|
$sql_origin_all .= " WHERE m.".$origin_tables['fk_parent']." = ".(int)$origin['id'];
|
||||||
|
$sql_origin_all .= " AND m.document_type = '".$db->escape($origin['type'])."'";
|
||||||
|
$sql_origin_all .= " ORDER BY m.line_order";
|
||||||
|
|
||||||
|
$res_origin_all = $db->query($sql_origin_all);
|
||||||
|
|
||||||
|
// Sammle alle Einträge gruppiert
|
||||||
|
$origin_entries = array();
|
||||||
|
while ($entry = $db->fetch_object($res_origin_all)) {
|
||||||
|
$origin_entries[] = $entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
subtotaltitle_debug_log('✅ '.count($origin_entries).' Einträge aus Ursprung geladen');
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// SCHRITT 4: Hole ALLE Produktzeilen aus Zieldokument (Dolibarr-Tabelle)
|
||||||
|
// ============================================================
|
||||||
|
subtotaltitle_debug_log('📦 Hole alle Produkte aus Zieldokument...');
|
||||||
|
|
||||||
|
$sql_target_products = "SELECT rowid, fk_product, rang FROM ".MAIN_DB_PREFIX.$target_tables['lines_table'];
|
||||||
|
$sql_target_products .= " WHERE ".$target_tables['fk_parent']." = ".(int)$target_id;
|
||||||
|
$sql_target_products .= " ORDER BY rang";
|
||||||
|
|
||||||
|
$res_target_products = $db->query($sql_target_products);
|
||||||
|
$target_products = array();
|
||||||
|
while ($row = $db->fetch_object($res_target_products)) {
|
||||||
|
$target_products[$row->rowid] = $row;
|
||||||
|
}
|
||||||
|
|
||||||
|
subtotaltitle_debug_log('✅ '.count($target_products).' Produkte aus Zieldokument geladen');
|
||||||
|
|
||||||
|
// Sammle fk_products die schon zugeordnet werden (aus Ursprung)
|
||||||
|
$assigned_fk_products = array();
|
||||||
|
$assigned_line_ids = array();
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// SCHRITT 5: Importiere Structure aus Ursprung mit richtiger Reihenfolge
|
||||||
|
// ============================================================
|
||||||
|
subtotaltitle_debug_log('🏗️ Importiere Struktur aus Ursprungsdokument...');
|
||||||
|
|
||||||
|
$line_order = 10;
|
||||||
|
$rang = 1;
|
||||||
|
$new_entries = array(); // Sammle alle neuen Einträge für spätere Rang-Zuweisung
|
||||||
|
|
||||||
|
foreach ($origin_entries as $entry) {
|
||||||
|
if ($entry->line_type === 'section') {
|
||||||
|
// Section importieren
|
||||||
|
$sql_insert = "INSERT INTO ".MAIN_DB_PREFIX."facture_lines_manager";
|
||||||
|
$sql_insert .= " (fk_facture, fk_propal, fk_commande, document_type, line_type, title,";
|
||||||
|
$sql_insert .= " parent_section, show_subtotal, collapsed, line_order, in_facturedet, date_creation)";
|
||||||
|
$sql_insert .= " VALUES (".$fk_facture.", ".$fk_propal.", ".$fk_commande.",";
|
||||||
|
$sql_insert .= " '".$db->escape($target_type)."', 'section',";
|
||||||
|
$sql_insert .= " '".$db->escape($entry->title)."',";
|
||||||
|
$sql_insert .= " NULL,";
|
||||||
|
$sql_insert .= " ".(int)$entry->show_subtotal.",";
|
||||||
|
$sql_insert .= " ".(int)$entry->collapsed.",";
|
||||||
|
$sql_insert .= " ".(int)$line_order.",";
|
||||||
|
$sql_insert .= " 0, NOW())";
|
||||||
|
|
||||||
|
if (!$db->query($sql_insert)) {
|
||||||
|
throw new Exception('Fehler beim Erstellen der Section: '.$db->lasterror());
|
||||||
|
}
|
||||||
|
|
||||||
|
$new_section_id = $db->last_insert_id(MAIN_DB_PREFIX."facture_lines_manager");
|
||||||
|
$section_mapping[$entry->rowid] = $new_section_id;
|
||||||
|
$imported_sections++;
|
||||||
|
$line_order += 10;
|
||||||
|
|
||||||
|
subtotaltitle_debug_log('✅ Section: "'.$entry->title.'" (#'.$entry->rowid.' → #'.$new_section_id.')');
|
||||||
|
|
||||||
|
} elseif ($entry->line_type === 'text') {
|
||||||
|
// Textline importieren
|
||||||
|
$new_parent = isset($section_mapping[$entry->parent_section])
|
||||||
|
? (int)$section_mapping[$entry->parent_section]
|
||||||
|
: 'NULL';
|
||||||
|
|
||||||
|
$sql_insert = "INSERT INTO ".MAIN_DB_PREFIX."facture_lines_manager";
|
||||||
|
$sql_insert .= " (fk_facture, fk_propal, fk_commande, document_type, line_type, title,";
|
||||||
|
$sql_insert .= " parent_section, line_order, in_facturedet, date_creation)";
|
||||||
|
$sql_insert .= " VALUES (".$fk_facture.", ".$fk_propal.", ".$fk_commande.",";
|
||||||
|
$sql_insert .= " '".$db->escape($target_type)."', 'text',";
|
||||||
|
$sql_insert .= " '".$db->escape($entry->title)."',";
|
||||||
|
$sql_insert .= " ".$new_parent.",";
|
||||||
|
$sql_insert .= " ".(int)$line_order.",";
|
||||||
|
$sql_insert .= " 0, NOW())";
|
||||||
|
|
||||||
|
if (!$db->query($sql_insert)) {
|
||||||
|
throw new Exception('Fehler beim Erstellen der Textline: '.$db->lasterror());
|
||||||
|
}
|
||||||
|
|
||||||
|
$imported_textlines++;
|
||||||
|
$line_order += 10;
|
||||||
|
|
||||||
|
subtotaltitle_debug_log('✅ Textline: "'.$entry->title.'"');
|
||||||
|
|
||||||
|
} elseif ($entry->line_type === 'subtotal') {
|
||||||
|
// Subtotal importieren (nur wenn parent Section existiert)
|
||||||
|
if (!isset($section_mapping[$entry->parent_section])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$new_parent = (int)$section_mapping[$entry->parent_section];
|
||||||
|
|
||||||
|
$sql_insert = "INSERT INTO ".MAIN_DB_PREFIX."facture_lines_manager";
|
||||||
|
$sql_insert .= " (fk_facture, fk_propal, fk_commande, document_type, line_type, title,";
|
||||||
|
$sql_insert .= " parent_section, line_order, in_facturedet, date_creation)";
|
||||||
|
$sql_insert .= " VALUES (".$fk_facture.", ".$fk_propal.", ".$fk_commande.",";
|
||||||
|
$sql_insert .= " '".$db->escape($target_type)."', 'subtotal',";
|
||||||
|
$sql_insert .= " '".$db->escape($entry->title)."',";
|
||||||
|
$sql_insert .= " ".$new_parent.",";
|
||||||
|
$sql_insert .= " ".(int)$line_order.",";
|
||||||
|
$sql_insert .= " 0, NOW())";
|
||||||
|
|
||||||
|
if (!$db->query($sql_insert)) {
|
||||||
|
throw new Exception('Fehler beim Erstellen des Subtotals: '.$db->lasterror());
|
||||||
|
}
|
||||||
|
|
||||||
|
$imported_subtotals++;
|
||||||
|
$line_order += 10;
|
||||||
|
|
||||||
|
subtotaltitle_debug_log('✅ Subtotal für Section #'.$new_parent);
|
||||||
|
|
||||||
|
} elseif ($entry->line_type === 'product' && !empty($entry->fk_product)) {
|
||||||
|
// Produkt - finde passende Zeile im Zieldokument
|
||||||
|
$target_line_id = null;
|
||||||
|
|
||||||
|
foreach ($target_products as $tp_id => $tp) {
|
||||||
|
if ($tp->fk_product == $entry->fk_product && !isset($assigned_line_ids[$tp_id])) {
|
||||||
|
$target_line_id = $tp_id;
|
||||||
|
$assigned_line_ids[$tp_id] = true;
|
||||||
|
$assigned_fk_products[$entry->fk_product] = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($target_line_id) {
|
||||||
|
$new_parent = isset($section_mapping[$entry->parent_section])
|
||||||
|
? (int)$section_mapping[$entry->parent_section]
|
||||||
|
: 'NULL';
|
||||||
|
|
||||||
|
$sql_insert = "INSERT INTO ".MAIN_DB_PREFIX."facture_lines_manager";
|
||||||
|
$sql_insert .= " (fk_facture, fk_propal, fk_commande, ".$target_tables['fk_line'].", document_type,";
|
||||||
|
$sql_insert .= " line_type, parent_section, line_order, in_facturedet, date_creation)";
|
||||||
|
$sql_insert .= " VALUES (".$fk_facture.", ".$fk_propal.", ".$fk_commande.",";
|
||||||
|
$sql_insert .= " ".(int)$target_line_id.", '".$db->escape($target_type)."',";
|
||||||
|
$sql_insert .= " 'product', ".$new_parent.", ".(int)$line_order.", 1, NOW())";
|
||||||
|
|
||||||
|
if (!$db->query($sql_insert)) {
|
||||||
|
throw new Exception('Fehler beim Erstellen des Produkts: '.$db->lasterror());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Speichere für Rang-Update
|
||||||
|
$new_entries[] = array(
|
||||||
|
'type' => 'product',
|
||||||
|
'line_id' => $target_line_id,
|
||||||
|
'rang' => $rang
|
||||||
|
);
|
||||||
|
|
||||||
|
$product_assignments++;
|
||||||
|
$line_order += 10;
|
||||||
|
$rang++;
|
||||||
|
|
||||||
|
subtotaltitle_debug_log('✅ Produkt: fk_product='.$entry->fk_product.' → Section #'.$new_parent.' (Line #'.$target_line_id.')');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// SCHRITT 6: Füge neue Produkte hinzu (nur im Zieldokument)
|
||||||
|
// ============================================================
|
||||||
|
subtotaltitle_debug_log('➕ Füge neue Produkte hinzu (nur im Zieldokument)...');
|
||||||
|
|
||||||
|
foreach ($target_products as $tp_id => $tp) {
|
||||||
|
if (isset($assigned_line_ids[$tp_id])) {
|
||||||
|
continue; // Schon zugeordnet
|
||||||
|
}
|
||||||
|
|
||||||
|
// Produkt ist NEU - füge am Ende hinzu (ohne Section)
|
||||||
|
$sql_insert = "INSERT INTO ".MAIN_DB_PREFIX."facture_lines_manager";
|
||||||
|
$sql_insert .= " (fk_facture, fk_propal, fk_commande, ".$target_tables['fk_line'].", document_type,";
|
||||||
|
$sql_insert .= " line_type, parent_section, line_order, in_facturedet, date_creation)";
|
||||||
|
$sql_insert .= " VALUES (".$fk_facture.", ".$fk_propal.", ".$fk_commande.",";
|
||||||
|
$sql_insert .= " ".(int)$tp_id.", '".$db->escape($target_type)."',";
|
||||||
|
$sql_insert .= " 'product', NULL, ".(int)$line_order.", 1, NOW())";
|
||||||
|
|
||||||
|
if (!$db->query($sql_insert)) {
|
||||||
|
throw new Exception('Fehler beim Erstellen des neuen Produkts: '.$db->lasterror());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Speichere für Rang-Update
|
||||||
|
$new_entries[] = array(
|
||||||
|
'type' => 'product',
|
||||||
|
'line_id' => $tp_id,
|
||||||
|
'rang' => $rang
|
||||||
|
);
|
||||||
|
|
||||||
|
$new_products++;
|
||||||
|
$line_order += 10;
|
||||||
|
$rang++;
|
||||||
|
|
||||||
|
subtotaltitle_debug_log('➕ Neues Produkt: Line #'.$tp_id.' (fk_product='.$tp->fk_product.')');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// SCHRITT 7: Finale Neu-Nummerierung in beiden Tabellen
|
||||||
|
// ============================================================
|
||||||
|
subtotaltitle_debug_log('🔄 Finale Neu-Nummerierung in beiden Tabellen...');
|
||||||
|
|
||||||
|
// A) line_order in Manager-Tabelle (basierend auf tatsächlicher Reihenfolge)
|
||||||
|
$sql_reorder = "SELECT rowid, line_type, ".$target_tables['fk_line']." as fk_line FROM ".MAIN_DB_PREFIX."facture_lines_manager";
|
||||||
|
$sql_reorder .= " WHERE ".$target_tables['fk_parent']." = ".(int)$target_id;
|
||||||
|
$sql_reorder .= " AND document_type = '".$db->escape($target_type)."'";
|
||||||
|
$sql_reorder .= " ORDER BY line_order";
|
||||||
|
|
||||||
|
$res_reorder = $db->query($sql_reorder);
|
||||||
|
|
||||||
|
$final_order = 10;
|
||||||
|
$final_rang = 1;
|
||||||
|
$product_rang_updates = array();
|
||||||
|
|
||||||
|
while ($row = $db->fetch_object($res_reorder)) {
|
||||||
|
// Update line_order
|
||||||
|
$sql_upd = "UPDATE ".MAIN_DB_PREFIX."facture_lines_manager SET line_order = ".(int)$final_order;
|
||||||
|
$sql_upd .= " WHERE rowid = ".(int)$row->rowid;
|
||||||
|
$db->query($sql_upd);
|
||||||
|
|
||||||
|
// Sammle Rang für Produkte
|
||||||
|
if ($row->line_type === 'product' && $row->fk_line) {
|
||||||
|
$product_rang_updates[$row->fk_line] = $final_rang;
|
||||||
|
$final_rang++;
|
||||||
|
}
|
||||||
|
|
||||||
|
$final_order += 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
subtotaltitle_debug_log('✅ line_order neu nummeriert');
|
||||||
|
|
||||||
|
// B) rang in Dolibarr-Tabelle aktualisieren
|
||||||
|
subtotaltitle_debug_log('🔄 Aktualisiere rang in Dolibarr-Tabelle ('.$target_tables['lines_table'].')...');
|
||||||
|
|
||||||
|
foreach ($product_rang_updates as $line_id => $new_rang) {
|
||||||
|
$sql_rang = "UPDATE ".MAIN_DB_PREFIX.$target_tables['lines_table'];
|
||||||
|
$sql_rang .= " SET rang = ".(int)$new_rang;
|
||||||
|
$sql_rang .= " WHERE rowid = ".(int)$line_id;
|
||||||
|
$db->query($sql_rang);
|
||||||
|
subtotaltitle_debug_log(' Line #'.$line_id.' → rang='.$new_rang);
|
||||||
|
}
|
||||||
|
|
||||||
|
subtotaltitle_debug_log('✅ rang in Dolibarr-Tabelle aktualisiert ('.count($product_rang_updates).' Zeilen)');
|
||||||
|
|
||||||
|
// Commit Transaktion
|
||||||
|
$db->commit();
|
||||||
|
|
||||||
|
subtotaltitle_debug_log('✅ Import komplett: '.$imported_sections.' Sections, '.$imported_textlines.' Textlines, '.$imported_subtotals.' Subtotals, '.$product_assignments.' zugeordnete Produkte, '.$new_products.' neue Produkte');
|
||||||
|
|
||||||
|
echo json_encode(array(
|
||||||
|
'success' => true,
|
||||||
|
'imported_sections' => $imported_sections,
|
||||||
|
'imported_textlines' => $imported_textlines,
|
||||||
|
'imported_subtotals' => $imported_subtotals,
|
||||||
|
'product_assignments' => $product_assignments,
|
||||||
|
'new_products' => $new_products,
|
||||||
|
'message' => sprintf('%d Sections, %d Textlines, %d Produkte zugeordnet, %d neue Produkte',
|
||||||
|
$imported_sections, $imported_textlines, $product_assignments, $new_products)
|
||||||
|
));
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$db->rollback();
|
||||||
|
subtotaltitle_debug_log('❌ Import fehlgeschlagen: '.$e->getMessage());
|
||||||
|
echo json_encode(array('success' => false, 'error' => $e->getMessage()));
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
echo json_encode(array('success' => false, 'error' => 'Unknown action'));
|
||||||
|
}
|
||||||
0
ajax/repair_missing_subtotals.php
Normal file → Executable file
0
ajax/repair_missing_subtotals.php
Normal file → Executable file
|
|
@ -312,6 +312,80 @@ if ($action == 'add') {
|
||||||
|
|
||||||
echo json_encode(array('success' => true, 'total_ht' => $total_ht));
|
echo json_encode(array('success' => true, 'total_ht' => $total_ht));
|
||||||
|
|
||||||
|
} elseif ($action == 'remove_all') {
|
||||||
|
// ========== ALLE SPEZIALZEILEN UND VERWAISTE EINTRÄGE ENTFERNEN ==========
|
||||||
|
|
||||||
|
// document_id wird benötigt
|
||||||
|
$document_id = GETPOST('document_id', 'int');
|
||||||
|
if (!$document_id) {
|
||||||
|
echo json_encode(array('success' => false, 'error' => 'Missing document_id'));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$removed_count = 0;
|
||||||
|
$orphan_count = 0;
|
||||||
|
|
||||||
|
// 1. Entferne ALLE Einträge mit special_code 100, 101, 102 aus der Detail-Tabelle
|
||||||
|
// (unabhängig davon ob sie noch in der Manager-Tabelle existieren)
|
||||||
|
$sql_delete_all = "DELETE FROM ".MAIN_DB_PREFIX.$tables['lines_table'];
|
||||||
|
$sql_delete_all .= " WHERE ".$tables['fk_parent']." = ".(int)$document_id;
|
||||||
|
$sql_delete_all .= " AND special_code IN (100, 101, 102)";
|
||||||
|
|
||||||
|
subtotaltitle_debug_log('🗑️ Remove ALL special lines: '.$sql_delete_all);
|
||||||
|
|
||||||
|
if ($db->query($sql_delete_all)) {
|
||||||
|
$removed_count = $db->affected_rows($db->query("SELECT ROW_COUNT()"));
|
||||||
|
// Fallback: Zähle manuell wenn affected_rows nicht funktioniert
|
||||||
|
if ($removed_count === 0) {
|
||||||
|
// Zähle vorher
|
||||||
|
$sql_count = "SELECT COUNT(*) as cnt FROM ".MAIN_DB_PREFIX.$tables['lines_table'];
|
||||||
|
$sql_count .= " WHERE ".$tables['fk_parent']." = ".(int)$document_id;
|
||||||
|
$sql_count .= " AND special_code IN (100, 101, 102)";
|
||||||
|
// Da wir schon gelöscht haben, ist es jetzt 0
|
||||||
|
$removed_count = -1; // Unbekannt, aber erfolgreich
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Setze in_facturedet und fk_*det auf NULL für alle Manager-Einträge dieses Dokuments
|
||||||
|
$sql_reset = "UPDATE ".MAIN_DB_PREFIX."facture_lines_manager";
|
||||||
|
$sql_reset .= " SET ".$tables['fk_line']." = NULL, in_facturedet = 0";
|
||||||
|
$sql_reset .= " WHERE ".$tables['fk_parent']." = ".(int)$document_id;
|
||||||
|
$sql_reset .= " AND document_type = '".$db->escape($docType)."'";
|
||||||
|
$sql_reset .= " AND line_type IN ('section', 'text', 'subtotal')";
|
||||||
|
|
||||||
|
subtotaltitle_debug_log('🔄 Reset manager entries: '.$sql_reset);
|
||||||
|
$db->query($sql_reset);
|
||||||
|
|
||||||
|
// 3. Normalisiere die rang-Werte (schließe Lücken)
|
||||||
|
$sql_reorder = "SET @r = 0; UPDATE ".MAIN_DB_PREFIX.$tables['lines_table'];
|
||||||
|
$sql_reorder .= " SET rang = (@r := @r + 1)";
|
||||||
|
$sql_reorder .= " WHERE ".$tables['fk_parent']." = ".(int)$document_id;
|
||||||
|
$sql_reorder .= " ORDER BY rang";
|
||||||
|
|
||||||
|
// MySQL erlaubt kein SET in einer Anweisung mit UPDATE, also manuell:
|
||||||
|
$sql_get_lines = "SELECT rowid FROM ".MAIN_DB_PREFIX.$tables['lines_table'];
|
||||||
|
$sql_get_lines .= " WHERE ".$tables['fk_parent']." = ".(int)$document_id;
|
||||||
|
$sql_get_lines .= " ORDER BY rang";
|
||||||
|
$res_lines = $db->query($sql_get_lines);
|
||||||
|
|
||||||
|
$new_rang = 1;
|
||||||
|
while ($obj = $db->fetch_object($res_lines)) {
|
||||||
|
$sql_upd_rang = "UPDATE ".MAIN_DB_PREFIX.$tables['lines_table'];
|
||||||
|
$sql_upd_rang .= " SET rang = ".(int)$new_rang;
|
||||||
|
$sql_upd_rang .= " WHERE rowid = ".(int)$obj->rowid;
|
||||||
|
$db->query($sql_upd_rang);
|
||||||
|
$new_rang++;
|
||||||
|
}
|
||||||
|
|
||||||
|
subtotaltitle_debug_log('✅ Remove ALL completed: removed='.$removed_count);
|
||||||
|
|
||||||
|
echo json_encode(array(
|
||||||
|
'success' => true,
|
||||||
|
'removed' => $removed_count,
|
||||||
|
'orphans_cleaned' => $orphan_count,
|
||||||
|
'message' => 'Alle Spezialzeilen wurden entfernt'
|
||||||
|
));
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
echo json_encode(array('success' => false, 'error' => 'Unknown action'));
|
echo json_encode(array('success' => false, 'error' => 'Unknown action'));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
0
class/DocumentTypeHelper.class.php
Normal file → Executable file
0
class/DocumentTypeHelper.class.php
Normal file → Executable file
|
|
@ -180,6 +180,8 @@ class ActionsSubtotalTitle extends CommonHookActions
|
||||||
echo '<script type="text/javascript">'."\n";
|
echo '<script type="text/javascript">'."\n";
|
||||||
echo 'var subtotalTitleIsDraft = '.($this->isDraft ? 'true' : 'false').';'."\n";
|
echo 'var subtotalTitleIsDraft = '.($this->isDraft ? 'true' : 'false').';'."\n";
|
||||||
echo 'var subtotalTitleHasSections = '.($hasSections ? 'true' : 'false').';'."\n";
|
echo 'var subtotalTitleHasSections = '.($hasSections ? 'true' : 'false').';'."\n";
|
||||||
|
// AJAX-URL-Pfad für alle AJAX-Aufrufe
|
||||||
|
echo 'var subtotaltitleAjaxUrl = "'.dol_buildpath('/subtotaltitle/ajax/', 1).'";'."\n";
|
||||||
// Grip-Bild-Pfad für Drag&Drop (wie Dolibarr es macht)
|
// Grip-Bild-Pfad für Drag&Drop (wie Dolibarr es macht)
|
||||||
echo 'var subtotalTitleGripUrl = "'.DOL_URL_ROOT.'/theme/'.$conf->theme.'/img/grip.png";'."\n";
|
echo 'var subtotalTitleGripUrl = "'.DOL_URL_ROOT.'/theme/'.$conf->theme.'/img/grip.png";'."\n";
|
||||||
echo 'var subtotalTitleLang = {'."\n";
|
echo 'var subtotalTitleLang = {'."\n";
|
||||||
|
|
@ -233,7 +235,15 @@ class ActionsSubtotalTitle extends CommonHookActions
|
||||||
echo ' elementsRemovedWithErrors: '.json_encode($langs->trans('ElementsRemovedWithErrors')).','."\n";
|
echo ' elementsRemovedWithErrors: '.json_encode($langs->trans('ElementsRemovedWithErrors')).','."\n";
|
||||||
echo ' successSyncedToInvoice: '.json_encode($langs->trans('SuccessSyncedToInvoice')).','."\n";
|
echo ' successSyncedToInvoice: '.json_encode($langs->trans('SuccessSyncedToInvoice')).','."\n";
|
||||||
echo ' successRemovedFromInvoice: '.json_encode($langs->trans('SuccessRemovedFromInvoice')).','."\n";
|
echo ' successRemovedFromInvoice: '.json_encode($langs->trans('SuccessRemovedFromInvoice')).','."\n";
|
||||||
echo ' errorSyncing: '.json_encode($langs->trans('ErrorSyncing'))."\n";
|
echo ' errorSyncing: '.json_encode($langs->trans('ErrorSyncing')).','."\n";
|
||||||
|
// Import feature strings
|
||||||
|
echo ' importFromOrigin: '.json_encode($langs->trans('ImportFromOrigin')).','."\n";
|
||||||
|
echo ' importFromOriginTitle: '.json_encode($langs->trans('ImportFromOriginTitle')).','."\n";
|
||||||
|
echo ' importFromOriginConfirm: '.json_encode($langs->trans('ImportFromOriginConfirm')).','."\n";
|
||||||
|
echo ' importFromOriginSuccess: '.json_encode($langs->trans('ImportFromOriginSuccess')).','."\n";
|
||||||
|
echo ' importFromOriginNoOrigin: '.json_encode($langs->trans('ImportFromOriginNoOrigin')).','."\n";
|
||||||
|
echo ' importFromOriginNoData: '.json_encode($langs->trans('ImportFromOriginNoData')).','."\n";
|
||||||
|
echo ' importFromOriginError: '.json_encode($langs->trans('ImportFromOriginError'))."\n";
|
||||||
echo '};'."\n";
|
echo '};'."\n";
|
||||||
echo '</script>'."\n";
|
echo '</script>'."\n";
|
||||||
|
|
||||||
|
|
@ -245,6 +255,30 @@ class ActionsSubtotalTitle extends CommonHookActions
|
||||||
$jsSyncPath = dol_buildpath('/custom/subtotaltitle/js/subtotaltitle_sync.js', 1);
|
$jsSyncPath = dol_buildpath('/custom/subtotaltitle/js/subtotaltitle_sync.js', 1);
|
||||||
echo '<script type="text/javascript" src="'.$jsSyncPath.'"></script>'."\n";
|
echo '<script type="text/javascript" src="'.$jsSyncPath.'"></script>'."\n";
|
||||||
|
|
||||||
|
// Prüfe ob Dokument ein Ursprungsdokument hat (für Import-Feature)
|
||||||
|
// Methode 1: Direkte Objekteigenschaften
|
||||||
|
$hasOrigin = (!empty($object->origin) && !empty($object->origin_id));
|
||||||
|
|
||||||
|
// Methode 2: Falls nicht gesetzt, prüfe llx_element_element Tabelle
|
||||||
|
if (!$hasOrigin && $object->id > 0) {
|
||||||
|
$elementType = $object->element; // z.B. 'commande', 'facture', 'propal'
|
||||||
|
$sql_origin = "SELECT fk_source, sourcetype FROM ".MAIN_DB_PREFIX."element_element";
|
||||||
|
$sql_origin .= " WHERE fk_target = ".(int)$object->id;
|
||||||
|
$sql_origin .= " AND targettype = '".$db->escape($elementType)."'";
|
||||||
|
$sql_origin .= " LIMIT 1";
|
||||||
|
$res_origin = $db->query($sql_origin);
|
||||||
|
if ($res_origin && $db->num_rows($res_origin) > 0) {
|
||||||
|
$obj_origin = $db->fetch_object($res_origin);
|
||||||
|
$object->origin = $obj_origin->sourcetype;
|
||||||
|
$object->origin_id = $obj_origin->fk_source;
|
||||||
|
$hasOrigin = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debug-Log für Import-Feature
|
||||||
|
$logFile = '/srv/http/dolibarr/documents/subtotaltitle/debug.log';
|
||||||
|
error_log('['.date('Y-m-d H:i:s').'] Import-Check - element: '.($object->element ?? 'NULL').', origin: '.($object->origin ?? 'NULL').', origin_id: '.($object->origin_id ?? 'NULL').', hasOrigin: '.($hasOrigin ? 'true' : 'false')."\n", 3, $logFile);
|
||||||
|
|
||||||
// Buttons nur im Entwurfsstatus anzeigen
|
// Buttons nur im Entwurfsstatus anzeigen
|
||||||
if ($is_draft) {
|
if ($is_draft) {
|
||||||
// Textzeile-Button
|
// Textzeile-Button
|
||||||
|
|
@ -254,6 +288,16 @@ class ActionsSubtotalTitle extends CommonHookActions
|
||||||
echo ' }';
|
echo ' }';
|
||||||
echo '});</script>'."\n";
|
echo '});</script>'."\n";
|
||||||
|
|
||||||
|
// Import-Button (wenn Ursprungsdokument existiert)
|
||||||
|
// Button erscheint immer - die check-Action prüft ob es etwas zu importieren gibt
|
||||||
|
if ($hasOrigin) {
|
||||||
|
echo '<script>$(document).ready(function() {';
|
||||||
|
echo ' if ($(".tabsAction").length > 0 && $("#btnImportOrigin").length === 0) {';
|
||||||
|
echo ' $(".tabsAction").first().append(\'<a id="btnImportOrigin" class="butAction" href="#" onclick="importFromOrigin(); return false;" title="Produktgruppen aus Ursprungsdokument übernehmen">📥 Import</a>\');';
|
||||||
|
echo ' }';
|
||||||
|
echo '});</script>'."\n";
|
||||||
|
}
|
||||||
|
|
||||||
// Massenlösch-Button (ans ENDE der Hauptzeile) - NUR EINMAL EINFÜGEN
|
// Massenlösch-Button (ans ENDE der Hauptzeile) - NUR EINMAL EINFÜGEN
|
||||||
echo '<script>$(document).ready(function() {';
|
echo '<script>$(document).ready(function() {';
|
||||||
echo ' if ($("#btnMassDelete").length === 0) {';
|
echo ' if ($("#btnMassDelete").length === 0) {';
|
||||||
|
|
@ -1438,10 +1482,25 @@ class ActionsSubtotalTitle extends CommonHookActions
|
||||||
|
|
||||||
$html .= ' $(\'form[name="addproduct"]\').append(\'<input type="hidden" name="section_id" id="section_id" value="">\');';
|
$html .= ' $(\'form[name="addproduct"]\').append(\'<input type="hidden" name="section_id" id="section_id" value="">\');';
|
||||||
|
|
||||||
|
// Lade gespeicherte Auswahl aus sessionStorage (dokumentspezifisch)
|
||||||
|
$html .= ' var storageKey = \'subtotaltitle_section_\' + '.$document_id.';';
|
||||||
|
$html .= ' var savedSection = sessionStorage.getItem(storageKey);';
|
||||||
|
$html .= ' if (savedSection) {';
|
||||||
|
$html .= ' $(\'#section_id_dropdown\').val(savedSection);';
|
||||||
|
$html .= ' $(\'#section_id\').val(savedSection);';
|
||||||
|
$html .= ' console.log(\'[SubtotalTitle] Gespeicherte Section geladen:\', savedSection);';
|
||||||
|
$html .= ' }';
|
||||||
|
|
||||||
$html .= ' $(document).on(\'change\', \'#section_id_dropdown\', function() {';
|
$html .= ' $(document).on(\'change\', \'#section_id_dropdown\', function() {';
|
||||||
$html .= ' var val = $(this).val();';
|
$html .= ' var val = $(this).val();';
|
||||||
$html .= ' console.log(\'Section selected:\', val);';
|
$html .= ' console.log(\'Section selected:\', val);';
|
||||||
$html .= ' $(\'#section_id\').val(val);';
|
$html .= ' $(\'#section_id\').val(val);';
|
||||||
|
// Speichere Auswahl in sessionStorage (dokumentspezifisch)
|
||||||
|
$html .= ' if (val) {';
|
||||||
|
$html .= ' sessionStorage.setItem(storageKey, val);';
|
||||||
|
$html .= ' } else {';
|
||||||
|
$html .= ' sessionStorage.removeItem(storageKey);';
|
||||||
|
$html .= ' }';
|
||||||
$html .= ' });';
|
$html .= ' });';
|
||||||
|
|
||||||
$html .= '});';
|
$html .= '});';
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,7 @@ class modSubtotalTitle extends DolibarrModules
|
||||||
$this->editor_squarred_logo = ''; // Must be image filename into the module/img directory followed with @modulename. Example: 'myimage.png@subtotaltitle'
|
$this->editor_squarred_logo = ''; // Must be image filename into the module/img directory followed with @modulename. Example: 'myimage.png@subtotaltitle'
|
||||||
|
|
||||||
// Possible values for version are: 'development', 'experimental', 'dolibarr', 'dolibarr_deprecated', 'experimental_deprecated' or a version string like 'x.y.z'
|
// 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 = '4.1';
|
||||||
// Url to the file with your last numberversion of this module
|
// Url to the file with your last numberversion of this module
|
||||||
//$this->url_last_version = 'http://www.example.com/versionmodule.txt';
|
//$this->url_last_version = 'http://www.example.com/versionmodule.txt';
|
||||||
|
|
||||||
|
|
|
||||||
0
debug_sections.php
Normal file → Executable file
0
debug_sections.php
Normal file → Executable file
0
img/grip.png
Normal file → Executable file
0
img/grip.png
Normal file → Executable file
|
Before Width: | Height: | Size: 90 B After Width: | Height: | Size: 90 B |
|
|
@ -3,7 +3,7 @@ $(document).ready(function() {
|
||||||
var factureId = getFactureId();
|
var factureId = getFactureId();
|
||||||
if (!factureId) return;
|
if (!factureId) return;
|
||||||
|
|
||||||
$.get('/dolibarr/custom/subtotaltitle/ajax/get_line_orders.php', {
|
$.get(subtotaltitleAjaxUrl + 'get_line_orders.php', {
|
||||||
facture_id: factureId
|
facture_id: factureId
|
||||||
}, function(response) {
|
}, function(response) {
|
||||||
if (!response.success) return;
|
if (!response.success) return;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,24 @@
|
||||||
// DEBUG FLAG - true für Debug-Ausgaben, false für Produktiv
|
// DEBUG FLAG - true für Debug-Ausgaben, false für Produktiv
|
||||||
var SUBTOTAL_DEBUG = false;
|
var SUBTOTAL_DEBUG = false;
|
||||||
|
|
||||||
|
// Fallback für AJAX-URL wenn nicht von PHP gesetzt (z.B. bei direktem JS-Include)
|
||||||
|
if (typeof subtotaltitleAjaxUrl === 'undefined') {
|
||||||
|
// Versuche URL aus aktuellem Script-Pfad abzuleiten
|
||||||
|
var scripts = document.getElementsByTagName('script');
|
||||||
|
for (var i = 0; i < scripts.length; i++) {
|
||||||
|
var src = scripts[i].src || '';
|
||||||
|
if (src.indexOf('subtotaltitle') !== -1 && src.indexOf('/js/') !== -1) {
|
||||||
|
// Pfad: .../subtotaltitle/js/subtotaltitle.js -> .../subtotaltitle/ajax/
|
||||||
|
var subtotaltitleAjaxUrl = src.replace(/\/js\/.*$/, '/ajax/');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Letzter Fallback
|
||||||
|
if (typeof subtotaltitleAjaxUrl === 'undefined') {
|
||||||
|
var subtotaltitleAjaxUrl = '/custom/subtotaltitle/ajax/';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function debugLog(message) {
|
function debugLog(message) {
|
||||||
if (SUBTOTAL_DEBUG) {
|
if (SUBTOTAL_DEBUG) {
|
||||||
console.log(message);
|
console.log(message);
|
||||||
|
|
@ -202,7 +220,7 @@ function cleanupOrphanedSubtotals() {
|
||||||
var docInfo = getDocumentInfo();
|
var docInfo = getDocumentInfo();
|
||||||
if (!docInfo.id) return;
|
if (!docInfo.id) return;
|
||||||
|
|
||||||
$.post('/dolibarr/custom/subtotaltitle/ajax/cleanup_subtotals.php', {
|
$.post(subtotaltitleAjaxUrl + 'cleanup_subtotals.php', {
|
||||||
facture_id: docInfo.id,
|
facture_id: docInfo.id,
|
||||||
document_type: docInfo.type
|
document_type: docInfo.type
|
||||||
}, function(response) {
|
}, function(response) {
|
||||||
|
|
@ -327,13 +345,13 @@ function createNewSection() {
|
||||||
}
|
}
|
||||||
|
|
||||||
showInputDialog(
|
showInputDialog(
|
||||||
lang.sectionCreate || 'Positionsgruppe erstellen',
|
lang.sectionCreate || 'Produktgruppe erstellen',
|
||||||
lang.sectionName || 'Name der Positionsgruppe:',
|
lang.sectionName || 'Name der Produktgruppe:',
|
||||||
'',
|
'',
|
||||||
function(title) {
|
function(title) {
|
||||||
debugLog('Erstelle Section: ' + title + ' für ' + docInfo.type + ' ID ' + docInfo.id);
|
debugLog('Erstelle Section: ' + title + ' für ' + docInfo.type + ' ID ' + docInfo.id);
|
||||||
|
|
||||||
$.post('/dolibarr/custom/subtotaltitle/ajax/create_section.php', {
|
$.post(subtotaltitleAjaxUrl + 'create_section.php', {
|
||||||
facture_id: docInfo.id,
|
facture_id: docInfo.id,
|
||||||
document_type: docInfo.type,
|
document_type: docInfo.type,
|
||||||
title: title
|
title: title
|
||||||
|
|
@ -362,7 +380,7 @@ function moveSection(sectionId, direction) {
|
||||||
var docInfo = getDocumentInfo();
|
var docInfo = getDocumentInfo();
|
||||||
debugLog('🔄 Verschiebe Section ' + sectionId + ' ' + direction + ' (docType: ' + docInfo.type + ')');
|
debugLog('🔄 Verschiebe Section ' + sectionId + ' ' + direction + ' (docType: ' + docInfo.type + ')');
|
||||||
|
|
||||||
$.post('/dolibarr/custom/subtotaltitle/ajax/move_section.php', {
|
$.post(subtotaltitleAjaxUrl + 'move_section.php', {
|
||||||
section_id: sectionId,
|
section_id: sectionId,
|
||||||
direction: direction,
|
direction: direction,
|
||||||
document_type: docInfo.type
|
document_type: docInfo.type
|
||||||
|
|
@ -396,13 +414,13 @@ function renameSection(sectionId, currentTitle) {
|
||||||
var lang = (typeof subtotalTitleLang !== 'undefined') ? subtotalTitleLang : {};
|
var lang = (typeof subtotalTitleLang !== 'undefined') ? subtotalTitleLang : {};
|
||||||
|
|
||||||
showInputDialog(
|
showInputDialog(
|
||||||
lang.buttonEdit || 'Positionsgruppe umbenennen',
|
lang.sectionEdit || 'Produktgruppe umbenennen',
|
||||||
lang.sectionName || 'Name der Positionsgruppe:',
|
lang.sectionName || 'Name der Produktgruppe:',
|
||||||
currentTitle || '',
|
currentTitle || '',
|
||||||
function(newTitle) {
|
function(newTitle) {
|
||||||
debugLog('✏️ Benenne Section ' + sectionId + ' um zu: ' + newTitle);
|
debugLog('✏️ Benenne Section ' + sectionId + ' um zu: ' + newTitle);
|
||||||
|
|
||||||
$.post('/dolibarr/custom/subtotaltitle/ajax/rename_section.php', {
|
$.post(subtotaltitleAjaxUrl + 'rename_section.php', {
|
||||||
section_id: sectionId,
|
section_id: sectionId,
|
||||||
title: newTitle
|
title: newTitle
|
||||||
}, function(response) {
|
}, function(response) {
|
||||||
|
|
@ -434,7 +452,7 @@ function deleteSection(sectionId) {
|
||||||
function() {
|
function() {
|
||||||
debugLog('🗑️ Lösche leere Section ' + sectionId);
|
debugLog('🗑️ Lösche leere Section ' + sectionId);
|
||||||
|
|
||||||
$.post('/dolibarr/custom/subtotaltitle/ajax/delete_section.php', {
|
$.post(subtotaltitleAjaxUrl + 'delete_section.php', {
|
||||||
section_id: sectionId,
|
section_id: sectionId,
|
||||||
force: 0,
|
force: 0,
|
||||||
document_type: getDocumentType()
|
document_type: getDocumentType()
|
||||||
|
|
@ -478,7 +496,7 @@ function deleteSectionForce(sectionId) {
|
||||||
function() {
|
function() {
|
||||||
debugLog('Force-Delete Section ' + sectionId + ' mit Produkten: ' + JSON.stringify(productIds));
|
debugLog('Force-Delete Section ' + sectionId + ' mit Produkten: ' + JSON.stringify(productIds));
|
||||||
|
|
||||||
$.post('/dolibarr/custom/subtotaltitle/ajax/delete_section.php', {
|
$.post(subtotaltitleAjaxUrl + 'delete_section.php', {
|
||||||
section_id: sectionId,
|
section_id: sectionId,
|
||||||
force: 1,
|
force: 1,
|
||||||
product_ids: JSON.stringify(productIds),
|
product_ids: JSON.stringify(productIds),
|
||||||
|
|
@ -512,7 +530,7 @@ function insertEmptySections() {
|
||||||
|
|
||||||
debugLog('📦 Lade alle Sections für ' + docInfo.type + ' ID ' + docInfo.id);
|
debugLog('📦 Lade alle Sections für ' + docInfo.type + ' ID ' + docInfo.id);
|
||||||
|
|
||||||
$.get('/dolibarr/custom/subtotaltitle/ajax/get_sections.php', {
|
$.get(subtotaltitleAjaxUrl + 'get_sections.php', {
|
||||||
facture_id: docInfo.id,
|
facture_id: docInfo.id,
|
||||||
document_type: docInfo.type
|
document_type: docInfo.type
|
||||||
}, function(response) {
|
}, function(response) {
|
||||||
|
|
@ -751,7 +769,7 @@ function saveCurrentOrder() {
|
||||||
debugLog('🚀 Sende ' + updates.length + ' Updates...');
|
debugLog('🚀 Sende ' + updates.length + ' Updates...');
|
||||||
|
|
||||||
var docInfo = getDocumentInfo();
|
var docInfo = getDocumentInfo();
|
||||||
$.post('/dolibarr/custom/subtotaltitle/ajax/reorder_all.php', {
|
$.post(subtotaltitleAjaxUrl + 'reorder_all.php', {
|
||||||
facture_id: getFactureId(),
|
facture_id: getFactureId(),
|
||||||
document_type: docInfo.type,
|
document_type: docInfo.type,
|
||||||
new_order: JSON.stringify(updates)
|
new_order: JSON.stringify(updates)
|
||||||
|
|
@ -775,7 +793,7 @@ function moveProductToSection(productId, sectionId, newLineOrder) {
|
||||||
var lang = (typeof subtotalTitleLang !== 'undefined') ? subtotalTitleLang : {};
|
var lang = (typeof subtotalTitleLang !== 'undefined') ? subtotalTitleLang : {};
|
||||||
debugLog('🚀 Verschiebe Produkt ' + productId + ' zu Section ' + (sectionId || 'FREI') + ' auf Position ' + newLineOrder);
|
debugLog('🚀 Verschiebe Produkt ' + productId + ' zu Section ' + (sectionId || 'FREI') + ' auf Position ' + newLineOrder);
|
||||||
|
|
||||||
$.post('/dolibarr/custom/subtotaltitle/ajax/move_product.php', {
|
$.post(subtotaltitleAjaxUrl + 'move_product.php', {
|
||||||
product_id: productId,
|
product_id: productId,
|
||||||
new_section_id: sectionId || 0,
|
new_section_id: sectionId || 0,
|
||||||
new_line_order: newLineOrder
|
new_line_order: newLineOrder
|
||||||
|
|
@ -803,6 +821,33 @@ function addUnlinkColumn() {
|
||||||
var $table = $('#tablelines');
|
var $table = $('#tablelines');
|
||||||
if (!$table.length) return;
|
if (!$table.length) return;
|
||||||
|
|
||||||
|
// NUR auf Dokumentdetailseiten ausführen (card), NICHT auf Listen
|
||||||
|
// Prüfe ob wir auf einer gültigen Seite sind (invoicecard, propalcard, ordercard)
|
||||||
|
var url = window.location.href;
|
||||||
|
var isDocumentCard = (url.indexOf('/facture/card.php') !== -1 ||
|
||||||
|
url.indexOf('/propal/card.php') !== -1 ||
|
||||||
|
url.indexOf('/commande/card.php') !== -1);
|
||||||
|
|
||||||
|
// Zusätzliche Prüfung: Modul muss aktiv sein (subtotalTitleIsDraft wird von PHP gesetzt)
|
||||||
|
var isModuleActive = (typeof subtotalTitleIsDraft !== 'undefined');
|
||||||
|
|
||||||
|
if (!isDocumentCard) {
|
||||||
|
debugLog('🔗 Keine Dokumentdetailseite, Unlink-Spalte wird übersprungen');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isModuleActive) {
|
||||||
|
debugLog('🔗 SubtotalTitle Modul nicht aktiv auf dieser Seite, Unlink-Spalte wird übersprungen');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prüfe ob mindestens eine Produktgruppe (Section) vorhanden ist
|
||||||
|
var hasSections = ($('tr.section-header').length > 0);
|
||||||
|
if (!hasSections) {
|
||||||
|
debugLog('🔗 Keine Produktgruppen vorhanden, Unlink-Spalte wird übersprungen');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Prüfe ob schon ausgeführt
|
// Prüfe ob schon ausgeführt
|
||||||
if ($table.data('unlink-added')) {
|
if ($table.data('unlink-added')) {
|
||||||
debugLog('🔗 Unlink-Spalte bereits vorhanden, überspringe');
|
debugLog('🔗 Unlink-Spalte bereits vorhanden, überspringe');
|
||||||
|
|
@ -810,7 +855,7 @@ function addUnlinkColumn() {
|
||||||
}
|
}
|
||||||
$table.data('unlink-added', true);
|
$table.data('unlink-added', true);
|
||||||
|
|
||||||
debugLog('🔗 Füge Unlink-Spalte hinzu...');
|
debugLog('🔗 Füge Unlink-Spalte hinzu (Sections vorhanden)...');
|
||||||
|
|
||||||
// THEAD: Leere Spalte hinzufügen
|
// THEAD: Leere Spalte hinzufügen
|
||||||
$table.find('thead tr').each(function() {
|
$table.find('thead tr').each(function() {
|
||||||
|
|
@ -904,7 +949,7 @@ function assignLastProductToSection(sectionId, factureId) {
|
||||||
var docInfo = getDocumentInfo();
|
var docInfo = getDocumentInfo();
|
||||||
debugLog('🎯 Weise neustes Produkt zu Section ' + sectionId + ' zu (docType: ' + docInfo.type + ')...');
|
debugLog('🎯 Weise neustes Produkt zu Section ' + sectionId + ' zu (docType: ' + docInfo.type + ')...');
|
||||||
|
|
||||||
$.post('/dolibarr/custom/subtotaltitle/ajax/assign_last_product.php', {
|
$.post(subtotaltitleAjaxUrl + 'assign_last_product.php', {
|
||||||
facture_id: factureId,
|
facture_id: factureId,
|
||||||
section_id: sectionId,
|
section_id: sectionId,
|
||||||
document_type: docInfo.type
|
document_type: docInfo.type
|
||||||
|
|
@ -1127,7 +1172,7 @@ function insertLastSectionSubtotal() {
|
||||||
// Hole Subtotal-ID aus Datenbank (oder erstelle ihn falls nötig)
|
// Hole Subtotal-ID aus Datenbank (oder erstelle ihn falls nötig)
|
||||||
var docType = getDocumentType();
|
var docType = getDocumentType();
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: '/dolibarr/custom/subtotaltitle/ajax/check_subtotal.php',
|
url: subtotaltitleAjaxUrl + 'check_subtotal.php',
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
data: {
|
data: {
|
||||||
section_id: sectionId,
|
section_id: sectionId,
|
||||||
|
|
@ -1203,7 +1248,7 @@ function insertTextLines() {
|
||||||
|
|
||||||
debugLog('📝 Lade Textzeilen für ' + docInfo.type + ' ID ' + docInfo.id);
|
debugLog('📝 Lade Textzeilen für ' + docInfo.type + ' ID ' + docInfo.id);
|
||||||
|
|
||||||
$.get('/dolibarr/custom/subtotaltitle/ajax/get_textlines.php', {
|
$.get(subtotaltitleAjaxUrl + 'get_textlines.php', {
|
||||||
facture_id: docInfo.id,
|
facture_id: docInfo.id,
|
||||||
document_type: docInfo.type
|
document_type: docInfo.type
|
||||||
}, function(response) {
|
}, function(response) {
|
||||||
|
|
@ -1232,7 +1277,7 @@ function toggleSubtotal(sectionId, checkbox) {
|
||||||
debugLog('🔢 Toggle Subtotal für Section ' + sectionId + ': ' + show + ', docType: ' + docType);
|
debugLog('🔢 Toggle Subtotal für Section ' + sectionId + ': ' + show + ', docType: ' + docType);
|
||||||
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: '/dolibarr/custom/subtotaltitle/ajax/toggle_subtotal.php',
|
url: subtotaltitleAjaxUrl + 'toggle_subtotal.php',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
data: {
|
data: {
|
||||||
section_id: sectionId,
|
section_id: sectionId,
|
||||||
|
|
@ -1383,7 +1428,7 @@ function createTextLine() {
|
||||||
function(text) {
|
function(text) {
|
||||||
debugLog('Erstelle Textzeile für ' + docInfo.type + ' ID ' + docInfo.id);
|
debugLog('Erstelle Textzeile für ' + docInfo.type + ' ID ' + docInfo.id);
|
||||||
|
|
||||||
$.post('/dolibarr/custom/subtotaltitle/ajax/create_textline.php', {
|
$.post(subtotaltitleAjaxUrl + 'create_textline.php', {
|
||||||
facture_id: docInfo.id,
|
facture_id: docInfo.id,
|
||||||
document_type: docInfo.type,
|
document_type: docInfo.type,
|
||||||
text: text
|
text: text
|
||||||
|
|
@ -1417,7 +1462,7 @@ function editTextLine(textlineId, currentText) {
|
||||||
function(newText) {
|
function(newText) {
|
||||||
debugLog('✏️ Bearbeite Textzeile ' + textlineId);
|
debugLog('✏️ Bearbeite Textzeile ' + textlineId);
|
||||||
|
|
||||||
$.post('/dolibarr/custom/subtotaltitle/ajax/edit_textline.php', {
|
$.post(subtotaltitleAjaxUrl + 'edit_textline.php', {
|
||||||
textline_id: textlineId,
|
textline_id: textlineId,
|
||||||
text: newText
|
text: newText
|
||||||
}, function(response) {
|
}, function(response) {
|
||||||
|
|
@ -1449,7 +1494,7 @@ function deleteTextLine(textlineId) {
|
||||||
function() {
|
function() {
|
||||||
debugLog('Lösche Textzeile ' + textlineId);
|
debugLog('Lösche Textzeile ' + textlineId);
|
||||||
|
|
||||||
$.post('/dolibarr/custom/subtotaltitle/ajax/delete_textline.php', {
|
$.post(subtotaltitleAjaxUrl + 'delete_textline.php', {
|
||||||
textline_id: textlineId,
|
textline_id: textlineId,
|
||||||
document_type: getDocumentType()
|
document_type: getDocumentType()
|
||||||
}, function(response) {
|
}, function(response) {
|
||||||
|
|
@ -1482,7 +1527,7 @@ function removeFromSection(productId) {
|
||||||
var docType = getDocumentType();
|
var docType = getDocumentType();
|
||||||
debugLog('Entferne Produkt ' + productId + ' aus Section (docType: ' + docType + ')');
|
debugLog('Entferne Produkt ' + productId + ' aus Section (docType: ' + docType + ')');
|
||||||
|
|
||||||
$.post('/dolibarr/custom/subtotaltitle/ajax/remove_from_section.php', {
|
$.post(subtotaltitleAjaxUrl + 'remove_from_section.php', {
|
||||||
product_id: productId,
|
product_id: productId,
|
||||||
document_type: docType
|
document_type: docType
|
||||||
}, function(response) {
|
}, function(response) {
|
||||||
|
|
@ -1560,7 +1605,7 @@ function deleteMassSelected() {
|
||||||
|
|
||||||
showConfirmDialog('Letzte Warnung', '<div style="color:#c00;font-weight:bold;">' + msg2 + '</div>', function() {
|
showConfirmDialog('Letzte Warnung', '<div style="color:#c00;font-weight:bold;">' + msg2 + '</div>', function() {
|
||||||
// Zweite Bestätigung OK - jetzt löschen
|
// Zweite Bestätigung OK - jetzt löschen
|
||||||
$.post('/dolibarr/custom/subtotaltitle/ajax/mass_delete.php', {
|
$.post(subtotaltitleAjaxUrl + 'mass_delete.php', {
|
||||||
line_ids: JSON.stringify(selectedIds),
|
line_ids: JSON.stringify(selectedIds),
|
||||||
facture_id: getFactureId(),
|
facture_id: getFactureId(),
|
||||||
document_type: getDocumentType()
|
document_type: getDocumentType()
|
||||||
|
|
@ -1740,7 +1785,7 @@ function linkToNearestSection(lineId) {
|
||||||
debugLog(' AJAX Call: add_to_section.php');
|
debugLog(' AJAX Call: add_to_section.php');
|
||||||
|
|
||||||
// AJAX Call zum Backend
|
// AJAX Call zum Backend
|
||||||
$.post('/dolibarr/custom/subtotaltitle/ajax/add_to_section.php', {
|
$.post(subtotaltitleAjaxUrl + 'add_to_section.php', {
|
||||||
line_id: lineId,
|
line_id: lineId,
|
||||||
section_id: targetSection.id,
|
section_id: targetSection.id,
|
||||||
document_id: docInfo.id,
|
document_id: docInfo.id,
|
||||||
|
|
@ -1764,3 +1809,124 @@ function linkToNearestSection(lineId) {
|
||||||
'Abbrechen'
|
'Abbrechen'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Importiert Sections und Textzeilen aus dem Ursprungsdokument
|
||||||
|
* (z.B. Angebot → Auftrag, Auftrag → Rechnung)
|
||||||
|
*/
|
||||||
|
function importFromOrigin() {
|
||||||
|
var lang = (typeof subtotalTitleLang !== 'undefined') ? subtotalTitleLang : {};
|
||||||
|
var docInfo = getDocumentInfo();
|
||||||
|
|
||||||
|
if (!docInfo.id) {
|
||||||
|
showErrorAlert(lang.importFromOriginError || 'Fehler: Keine Dokument-ID gefunden');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
debugLog('📥 Prüfe Import für ' + docInfo.type + ' #' + docInfo.id);
|
||||||
|
|
||||||
|
// Zeige Ladehinweis
|
||||||
|
$('body').append('<div id="import-loading" style="position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.3);z-index:9999;display:flex;align-items:center;justify-content:center;"><div style="background:#fff;padding:20px 40px;border-radius:8px;box-shadow:0 2px 10px rgba(0,0,0,0.2);">Prüfe Ursprungsdokument...</div></div>');
|
||||||
|
|
||||||
|
// Prüfe zuerst ob Import möglich ist
|
||||||
|
$.post(subtotaltitleAjaxUrl + 'import_from_origin.php', {
|
||||||
|
action: 'check',
|
||||||
|
target_id: docInfo.id,
|
||||||
|
target_type: docInfo.type
|
||||||
|
}, function(response) {
|
||||||
|
$('#import-loading').remove();
|
||||||
|
debugLog('Check response: ' + JSON.stringify(response));
|
||||||
|
|
||||||
|
if (!response.success) {
|
||||||
|
showErrorAlert((lang.importFromOriginError || 'Fehler') + ': ' + (response.error || 'Unbekannter Fehler'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.has_origin) {
|
||||||
|
showErrorAlert(lang.importFromOriginNoOrigin || 'Kein Ursprungsdokument verknüpft. Dieses Dokument wurde nicht aus einem Angebot oder Auftrag erstellt.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.can_import) {
|
||||||
|
showErrorAlert(lang.importFromOriginNoData || 'Das Ursprungsdokument enthält keine Produktgruppen oder Textzeilen zum Importieren.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zeige Bestätigungsdialog mit Details
|
||||||
|
var confirmMsg = (lang.importFromOriginConfirm || 'Produktgruppen aus %origin% importieren?')
|
||||||
|
.replace('%origin%', response.origin_name);
|
||||||
|
|
||||||
|
confirmMsg += '<br><br><table style="margin:10px 0;width:100%;">';
|
||||||
|
confirmMsg += '<tr><td>Produktgruppen:</td><td style="text-align:right;font-weight:bold;">' + response.sections_count + '</td></tr>';
|
||||||
|
confirmMsg += '<tr><td>Textzeilen:</td><td style="text-align:right;font-weight:bold;">' + response.textlines_count + '</td></tr>';
|
||||||
|
confirmMsg += '</table>';
|
||||||
|
|
||||||
|
if (response.has_existing) {
|
||||||
|
confirmMsg += '<br><em style="color:#c00;">⚠️ Es existieren bereits Produktgruppen in diesem Dokument!</em>';
|
||||||
|
}
|
||||||
|
|
||||||
|
showConfirmDialog(
|
||||||
|
lang.importFromOriginTitle || 'Produktgruppen importieren',
|
||||||
|
confirmMsg,
|
||||||
|
function() {
|
||||||
|
// Führe Import durch
|
||||||
|
executeImport(docInfo, response.origin_name);
|
||||||
|
},
|
||||||
|
'Ja, importieren',
|
||||||
|
'Abbrechen'
|
||||||
|
);
|
||||||
|
}, 'json').fail(function(xhr, status, error) {
|
||||||
|
$('#import-loading').remove();
|
||||||
|
debugLog('AJAX Fehler: ' + status + ' ' + error);
|
||||||
|
showErrorAlert((lang.importFromOriginError || 'Fehler beim Prüfen') + ': ' + error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Führt den eigentlichen Import durch
|
||||||
|
*/
|
||||||
|
function executeImport(docInfo, originName) {
|
||||||
|
var lang = (typeof subtotalTitleLang !== 'undefined') ? subtotalTitleLang : {};
|
||||||
|
|
||||||
|
debugLog('📥 Starte Import...');
|
||||||
|
|
||||||
|
// Zeige Ladehinweis
|
||||||
|
$('body').append('<div id="import-loading" style="position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.3);z-index:9999;display:flex;align-items:center;justify-content:center;"><div style="background:#fff;padding:20px 40px;border-radius:8px;box-shadow:0 2px 10px rgba(0,0,0,0.2);">Importiere Produktgruppen...</div></div>');
|
||||||
|
|
||||||
|
$.post(subtotaltitleAjaxUrl + 'import_from_origin.php', {
|
||||||
|
action: 'import',
|
||||||
|
target_id: docInfo.id,
|
||||||
|
target_type: docInfo.type
|
||||||
|
}, function(response) {
|
||||||
|
$('#import-loading').remove();
|
||||||
|
debugLog('Import response: ' + JSON.stringify(response));
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
var successMsg = (lang.importFromOriginSuccess || 'Import erfolgreich!')
|
||||||
|
+ '<br><br>Importiert:'
|
||||||
|
+ '<br>• ' + response.imported_sections + ' Produktgruppen'
|
||||||
|
+ '<br>• ' + response.imported_textlines + ' Textzeilen';
|
||||||
|
|
||||||
|
if (response.product_assignments > 0) {
|
||||||
|
successMsg += '<br>• ' + response.product_assignments + ' Produktzuordnungen';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zeige Erfolg und lade neu
|
||||||
|
showConfirmDialog(
|
||||||
|
'Import erfolgreich',
|
||||||
|
successMsg,
|
||||||
|
function() {
|
||||||
|
safeReload();
|
||||||
|
},
|
||||||
|
'OK',
|
||||||
|
null // Kein Abbrechen-Button
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
showErrorAlert((lang.importFromOriginError || 'Fehler beim Import') + ': ' + (response.error || 'Unbekannter Fehler'));
|
||||||
|
}
|
||||||
|
}, 'json').fail(function(xhr, status, error) {
|
||||||
|
$('#import-loading').remove();
|
||||||
|
debugLog('AJAX Fehler: ' + status + ' ' + error);
|
||||||
|
showErrorAlert((lang.importFromOriginError || 'Fehler beim Import') + ': ' + error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ function syncToFacturedet(lineId, lineType) {
|
||||||
var docType = getDocumentTypeForSync();
|
var docType = getDocumentTypeForSync();
|
||||||
debugLog('Document type: ' + docType);
|
debugLog('Document type: ' + docType);
|
||||||
|
|
||||||
$.post('/dolibarr/custom/subtotaltitle/ajax/sync_to_facturedet.php', {
|
$.post(subtotaltitleAjaxUrl + 'sync_to_facturedet.php', {
|
||||||
action: 'add',
|
action: 'add',
|
||||||
line_id: lineId,
|
line_id: lineId,
|
||||||
line_type: lineType,
|
line_type: lineType,
|
||||||
|
|
@ -64,7 +64,7 @@ function removeFromFacturedet(lineId, lineType) {
|
||||||
var docType = getDocumentTypeForSync();
|
var docType = getDocumentTypeForSync();
|
||||||
debugLog('Document type: ' + docType);
|
debugLog('Document type: ' + docType);
|
||||||
|
|
||||||
$.post('/dolibarr/custom/subtotaltitle/ajax/sync_to_facturedet.php', {
|
$.post(subtotaltitleAjaxUrl + 'sync_to_facturedet.php', {
|
||||||
action: 'remove',
|
action: 'remove',
|
||||||
line_id: lineId,
|
line_id: lineId,
|
||||||
line_type: lineType,
|
line_type: lineType,
|
||||||
|
|
@ -144,27 +144,30 @@ function syncAllToFacturedet() {
|
||||||
var errors = 0;
|
var errors = 0;
|
||||||
var docType = getDocumentTypeForSync();
|
var docType = getDocumentTypeForSync();
|
||||||
|
|
||||||
|
// Zeige Loading-Hinweis
|
||||||
|
$('body').append('<div id="sync-loading" style="position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.3);z-index:9999;display:flex;align-items:center;justify-content:center;"><div style="background:#fff;padding:20px 40px;border-radius:8px;box-shadow:0 2px 10px rgba(0,0,0,0.2);">Synchronisiere... Bitte warten.</div></div>');
|
||||||
|
|
||||||
$unchecked.each(function() {
|
$unchecked.each(function() {
|
||||||
var lineId = $(this).data('line-id');
|
var lineId = $(this).data('line-id');
|
||||||
var lineType = $(this).data('line-type');
|
var lineType = $(this).data('line-type');
|
||||||
|
|
||||||
$.post('/dolibarr/custom/subtotaltitle/ajax/sync_to_facturedet.php', {
|
$.post(subtotaltitleAjaxUrl + 'sync_to_facturedet.php', {
|
||||||
action: 'add',
|
action: 'add',
|
||||||
line_id: lineId,
|
line_id: lineId,
|
||||||
line_type: lineType,
|
line_type: lineType,
|
||||||
document_type: docType
|
document_type: docType
|
||||||
}, function(response) {
|
}, function(response) {
|
||||||
done++;
|
done++;
|
||||||
if (response.success) {
|
if (!response.success) {
|
||||||
updateSyncCheckbox(lineId, true);
|
|
||||||
} else {
|
|
||||||
errors++;
|
errors++;
|
||||||
}
|
}
|
||||||
if (done >= total) {
|
if (done >= total) {
|
||||||
debugLog('✅ Sync abgeschlossen: ' + (total - errors) + ' erfolgreich, ' + errors + ' Fehler');
|
debugLog('✅ Sync abgeschlossen: ' + (total - errors) + ' erfolgreich, ' + errors + ' Fehler');
|
||||||
if (errors > 0) {
|
if (errors > 0) {
|
||||||
|
$('#sync-loading').remove();
|
||||||
showErrorAlert((total - errors) + ' von ' + total + ' Elementen hinzugefügt. ' + errors + ' Fehler aufgetreten.');
|
showErrorAlert((total - errors) + ' von ' + total + ' Elementen hinzugefügt. ' + errors + ' Fehler aufgetreten.');
|
||||||
} else {
|
} else {
|
||||||
|
// Direkt reloaden ohne UI-Update
|
||||||
safeReload();
|
safeReload();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -181,56 +184,42 @@ function syncAllToFacturedet() {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Entfernt ALLE Sections/Textzeilen/Subtotals aus facturedet
|
* Entfernt ALLE Sections/Textzeilen/Subtotals aus facturedet
|
||||||
|
* Inkl. verwaister Einträge die nicht mehr in der Manager-Tabelle existieren
|
||||||
*/
|
*/
|
||||||
function removeAllFromFacturedet() {
|
function removeAllFromFacturedet() {
|
||||||
var lang = (typeof subtotalTitleLang !== 'undefined') ? subtotalTitleLang : {};
|
var lang = (typeof subtotalTitleLang !== 'undefined') ? subtotalTitleLang : {};
|
||||||
|
|
||||||
var $checked = $('.sync-checkbox:checked');
|
|
||||||
var total = $checked.length;
|
|
||||||
|
|
||||||
if (total === 0) {
|
|
||||||
showErrorAlert(lang.noElementsInInvoice || 'Keine Elemente in der Rechnung vorhanden.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
showConfirmDialog(
|
showConfirmDialog(
|
||||||
'Alle aus Rechnung entfernen',
|
'Alle aus Rechnung entfernen',
|
||||||
(lang.confirmRemoveAll || 'ALLE Positionsgruppen-Elemente aus der Rechnung entfernen?') + '<br><br><em>Die Elemente bleiben in der Verwaltung erhalten.</em>',
|
(lang.confirmRemoveAll || 'ALLE Positionsgruppen-Elemente (Sections, Textzeilen, Zwischensummen) aus der Rechnung entfernen?') +
|
||||||
|
'<br><br><em>Inkl. verwaister Einträge. Die Elemente in der Verwaltung bleiben erhalten.</em>',
|
||||||
function() {
|
function() {
|
||||||
debugLog('📥 Remove ALL from facturedet...');
|
debugLog('📥 Remove ALL from facturedet (server-side)...');
|
||||||
|
|
||||||
var done = 0;
|
|
||||||
var errors = 0;
|
|
||||||
var docType = getDocumentTypeForSync();
|
var docType = getDocumentTypeForSync();
|
||||||
|
var documentId = getFactureId();
|
||||||
|
|
||||||
$checked.each(function() {
|
if (!documentId) {
|
||||||
var lineId = $(this).data('line-id');
|
showErrorAlert('Fehler: Keine Dokument-ID gefunden');
|
||||||
var lineType = $(this).data('line-type');
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
$.post('/dolibarr/custom/subtotaltitle/ajax/sync_to_facturedet.php', {
|
$.post(subtotaltitleAjaxUrl + 'sync_to_facturedet.php', {
|
||||||
action: 'remove',
|
action: 'remove_all',
|
||||||
line_id: lineId,
|
line_id: 1, // Dummy, wird benötigt wegen Parameter-Check
|
||||||
line_type: lineType,
|
document_id: documentId,
|
||||||
document_type: docType
|
document_type: docType
|
||||||
}, function(response) {
|
}, function(response) {
|
||||||
done++;
|
debugLog('Remove ALL response: ' + JSON.stringify(response));
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
updateSyncCheckbox(lineId, false);
|
debugLog('✅ Alle Spezialzeilen entfernt');
|
||||||
} else {
|
safeReload();
|
||||||
errors++;
|
} else {
|
||||||
}
|
showErrorAlert((lang.errorSyncing || 'Fehler') + ': ' + (response.error || 'Unbekannter Fehler'));
|
||||||
if (done >= total) {
|
}
|
||||||
debugLog('✅ Remove abgeschlossen: ' + (total - errors) + ' erfolgreich, ' + errors + ' Fehler');
|
}, 'json').fail(function(xhr, status, error) {
|
||||||
if (errors > 0) {
|
debugLog('AJAX Fehler: ' + status + ' ' + error);
|
||||||
showErrorAlert((total - errors) + ' von ' + total + ' Elementen entfernt. ' + errors + ' Fehler aufgetreten.');
|
showErrorAlert((lang.errorSyncing || 'Fehler') + ': ' + error);
|
||||||
} else {
|
|
||||||
safeReload();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, 'json').fail(function() {
|
|
||||||
done++;
|
|
||||||
errors++;
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
'Ja, alle entfernen',
|
'Ja, alle entfernen',
|
||||||
|
|
@ -258,7 +247,7 @@ function updateAllSubtotals() {
|
||||||
$subtotals.each(function() {
|
$subtotals.each(function() {
|
||||||
var lineId = $(this).data('line-id');
|
var lineId = $(this).data('line-id');
|
||||||
|
|
||||||
$.post('/dolibarr/custom/subtotaltitle/ajax/sync_to_facturedet.php', {
|
$.post(subtotaltitleAjaxUrl + 'sync_to_facturedet.php', {
|
||||||
action: 'update_subtotal',
|
action: 'update_subtotal',
|
||||||
line_id: lineId,
|
line_id: lineId,
|
||||||
document_type: docType
|
document_type: docType
|
||||||
|
|
|
||||||
|
|
@ -86,9 +86,9 @@ ButtonCancel = Abbrechen
|
||||||
# UI Elements - Section Actions
|
# UI Elements - Section Actions
|
||||||
#
|
#
|
||||||
SectionCreate = Produktgruppe erstellen
|
SectionCreate = Produktgruppe erstellen
|
||||||
SectionEdit = Section bearbeiten
|
SectionEdit = Produktgruppe bearbeiten
|
||||||
SectionDelete = Section löschen
|
SectionDelete = Produktgruppe löschen
|
||||||
SectionName = Section-Name
|
SectionName = Name der Produktgruppe
|
||||||
SectionSubtotal = Zwischensumme anzeigen
|
SectionSubtotal = Zwischensumme anzeigen
|
||||||
ProductCount = Produkte
|
ProductCount = Produkte
|
||||||
|
|
||||||
|
|
@ -143,3 +143,14 @@ ElementsRemovedWithErrors = %s von %s Elementen entfernt.\n%s Fehler aufgetreten
|
||||||
SuccessSyncedToInvoice = Erfolgreich zur Rechnung synchronisiert
|
SuccessSyncedToInvoice = Erfolgreich zur Rechnung synchronisiert
|
||||||
SuccessRemovedFromInvoice = Erfolgreich aus Rechnung entfernt
|
SuccessRemovedFromInvoice = Erfolgreich aus Rechnung entfernt
|
||||||
ErrorSyncing = Fehler beim Synchronisieren
|
ErrorSyncing = Fehler beim Synchronisieren
|
||||||
|
|
||||||
|
#
|
||||||
|
# UI Elements - Import from Origin
|
||||||
|
#
|
||||||
|
ImportFromOrigin = Produktgruppen importieren
|
||||||
|
ImportFromOriginTitle = Produktgruppen importieren
|
||||||
|
ImportFromOriginConfirm = Produktgruppen aus %origin% importieren?
|
||||||
|
ImportFromOriginSuccess = Import erfolgreich!
|
||||||
|
ImportFromOriginNoOrigin = Kein Ursprungsdokument verknüpft. Dieses Dokument wurde nicht aus einem Angebot oder Auftrag erstellt.
|
||||||
|
ImportFromOriginNoData = Das Ursprungsdokument enthält keine Produktgruppen oder Textzeilen zum Importieren.
|
||||||
|
ImportFromOriginError = Fehler beim Import
|
||||||
|
|
|
||||||
|
|
@ -85,10 +85,10 @@ ButtonCancel = Cancel
|
||||||
#
|
#
|
||||||
# UI Elements - Section Actions
|
# UI Elements - Section Actions
|
||||||
#
|
#
|
||||||
SectionCreate = Create section
|
SectionCreate = Create product group
|
||||||
SectionEdit = Edit section
|
SectionEdit = Edit product group
|
||||||
SectionDelete = Delete section
|
SectionDelete = Delete product group
|
||||||
SectionName = Section name
|
SectionName = Product group name
|
||||||
SectionSubtotal = Show subtotal
|
SectionSubtotal = Show subtotal
|
||||||
ProductCount = Products
|
ProductCount = Products
|
||||||
|
|
||||||
|
|
@ -143,3 +143,14 @@ ElementsRemovedWithErrors = %s of %s elements removed.\n%s errors occurred.
|
||||||
SuccessSyncedToInvoice = Successfully synced to invoice
|
SuccessSyncedToInvoice = Successfully synced to invoice
|
||||||
SuccessRemovedFromInvoice = Successfully removed from invoice
|
SuccessRemovedFromInvoice = Successfully removed from invoice
|
||||||
ErrorSyncing = Error syncing
|
ErrorSyncing = Error syncing
|
||||||
|
|
||||||
|
#
|
||||||
|
# UI Elements - Import from Origin
|
||||||
|
#
|
||||||
|
ImportFromOrigin = Import product groups
|
||||||
|
ImportFromOriginTitle = Import product groups
|
||||||
|
ImportFromOriginConfirm = Import product groups from %origin%?
|
||||||
|
ImportFromOriginSuccess = Import successful!
|
||||||
|
ImportFromOriginNoOrigin = No origin document linked. This document was not created from a proposal or order.
|
||||||
|
ImportFromOriginNoData = The origin document contains no product groups or text lines to import.
|
||||||
|
ImportFromOriginError = Error during import
|
||||||
|
|
|
||||||
0
sql/llx_facture_lines_manager.sql
Normal file → Executable file
0
sql/llx_facture_lines_manager.sql
Normal file → Executable file
|
|
@ -1,4 +0,0 @@
|
||||||
[Project]
|
|
||||||
CreatedFrom=
|
|
||||||
Manager=KDevCustomBuildSystem
|
|
||||||
Name=subtotaltitle
|
|
||||||
Loading…
Reference in a new issue