feat: Phase 1.3 + 1.7 + Schema 1.4/1.5 — Format/Orient + Kunden-Tab
All checks were successful
Deploy bericht / deploy (push) Successful in 1s
All checks were successful
Deploy bericht / deploy (push) Successful in 1s
Phase 1.3 Seitenformat A4/A3/A5/Letter + Hoch/Quer: - Neue Spalten page_format, page_orientation in llx_bericht - Bericht-Meta zeigt Format + Orientation Selects - Auto-Save via neuem ajax/save_meta.php - generate_pdf + preview_pdf nutzen die gewählten Werte - Bilder werden dynamisch via getPageWidth/getPageHeight skaliert (statt hardcoded 210x297 für A4) Phase 1.4 + 1.5 Schema-Vorbereitung: - Neue Tabelle llx_bericht_page_image für Multi-Image-Seiten - Spalten layout, image_scale, image_align in llx_bericht_page - DB-Migrationen im init() für bestehende Installationen (ALTER TABLE mit Error-Suppress) - Grid-Rendering im Editor/PDF folgt im nächsten Commit (siehe CLAUDE.md TODO) Phase 1.7 Tab "Berichte" auf Kundenkarte: - Neue Konstante BERICHT_TAB_ON_THIRDPARTY (default 1) - Tab-Definition in modBericht für 'thirdparty' Element - Neue Datei bericht_thirdparty.php - UNION-SQL über bericht JOIN commande/facture/propal mit fk_soc - Read-only flache Tabelle sortiert nach Datum - Pro Bericht: Quelle (Symbol + Ref-Link), Status, Öffnen/Zur Quelle Version-Bump 1.0.0 → 1.1.0, ChangeLog ergänzt. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> [deploy]
This commit is contained in:
parent
a7bf3929a4
commit
0fbfb1bf27
13 changed files with 314 additions and 22 deletions
23
CLAUDE.md
23
CLAUDE.md
|
|
@ -68,14 +68,21 @@ Speichert: color, stroke, fontFamily, fontSize, bold, italic, zoom
|
||||||
- Generate-PDF mit FPDI + Annotationen einbrennen
|
- Generate-PDF mit FPDI + Annotationen einbrennen
|
||||||
- Mobile-Upload-Idee dokumentiert (Phase 2)
|
- Mobile-Upload-Idee dokumentiert (Phase 2)
|
||||||
|
|
||||||
### 🔄 Phase 1 Features (in Arbeit)
|
### Phase 1 Features
|
||||||
- [ ] **1.6 Verknüpfte Sicht Auftrag→Rechnung** — Übersicht zeigt zwei Sektionen, Übernahme-Button erzeugt llx_element_element. *(in Arbeit)*
|
- [x] **1.6 Verknüpfte Sicht Auftrag→Rechnung** ✅
|
||||||
- [ ] **1.1 Live-PDF-Vorschau** — neuer Endpoint `ajax/preview_pdf.php` (wie generate_pdf, aber ohne ECM-Insert + status), JS-Modal mit PDF.js
|
- [x] **1.1 Live-PDF-Vorschau** ✅
|
||||||
- [ ] **1.2 Anhänge löschen** — neuer Endpoint `ajax/delete_attachment.php`, 🗑️-Icon neben Checkbox in Anhänge-Liste, Path-Whitelist, ECM-Cleanup
|
- [x] **1.2 Anhänge löschen** ✅
|
||||||
- [ ] **1.3 Seitengröße A4/A3/Letter + Hoch/Quer** — neue Spalten in `llx_bericht` (format, orientation), pro Seite überschreibbar in `llx_bericht_page` (format_override)
|
- [x] **1.3 Seitengröße A4/A3/Letter + Hoch/Quer** ✅ — global pro Bericht (nicht pro Seite override)
|
||||||
- [ ] **1.4 Mehrere Bilder pro Seite** — Layout-Picker (1/2/4/6 Grid), neue Spalte `layout` in `llx_bericht_page`, n:m-Bilder evtl. neue Tabelle
|
- [/] **1.4 Mehrere Bilder pro Seite** — DB + Klassen-Skelett da, Grid-Rendering im PDF + Editor noch offen (siehe TODO unten)
|
||||||
- [ ] **1.5 Bildgröße pro Seite** — Spalten image_scale, image_align, image_x, image_y in llx_bericht_page
|
- [/] **1.5 Bildgröße pro Seite** — image_scale/image_align Spalten da, UI noch offen
|
||||||
- [ ] **1.7 Kunden-Tab** — Tab "Berichte" auf thirdparty, read-only, flache Tabelle sortiert nach Datum, Konstante BERICHT_TAB_ON_THIRDPARTY (default 0)
|
- [ ] **1.7 Kunden-Tab** — Tab "Berichte" auf thirdparty
|
||||||
|
|
||||||
|
### TODO Phase 1.4 + 1.5 (Folge-Commit)
|
||||||
|
- BerichtPage::getImages() liest llx_bericht_page_image
|
||||||
|
- Editor: Layout-Dropdown pro Seite (1/2/4/6 Slots)
|
||||||
|
- Drag&Drop von Anhängen in Slots oder "Als Grid hinzufügen"-Button
|
||||||
|
- generate_pdf/preview_pdf: Grid-Rendering (calculate slot rects)
|
||||||
|
- Bei `single`-Layout: image_scale (1.0/0.7/0.5) + image_align (fit/center/topleft/topright)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
41
ChangeLog.md
41
ChangeLog.md
|
|
@ -1,5 +1,46 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 1.1.0 — 2026-04-08
|
||||||
|
|
||||||
|
### Phase 1 Bericht-Modul Erweiterungen
|
||||||
|
|
||||||
|
**1.6 Verknüpfte Sicht Auftrag↔Rechnung**
|
||||||
|
- Bericht-Übersicht zeigt drei Sektionen: direkt zugeordnet, zusätzlich verknüpft, aus verknüpften Aufträgen
|
||||||
|
- "→ Übernehmen"-Button erstellt llx_element_element-Eintrag (n:m-Verknüpfung)
|
||||||
|
- "Lösen"-Button entfernt Verknüpfung
|
||||||
|
- Beim Finalisieren landet das PDF auch unter den verknüpften Elementen im ECM
|
||||||
|
|
||||||
|
**1.1 Live-PDF-Vorschau**
|
||||||
|
- 👁️ Vorschau-Button im Editor → Modal mit eingebettetem PDF
|
||||||
|
- Neuer Endpoint `ajax/preview_pdf.php` (kein ECM-Insert, kein Status-Wechsel)
|
||||||
|
- ESC oder Klick auf Backdrop schließt das Modal
|
||||||
|
|
||||||
|
**1.2 Anhänge löschen**
|
||||||
|
- 🗑️ Icon neben jedem Anhang in der linken Spalte
|
||||||
|
- Confirm-Dialog mit Auftrags-/Rechnungs-Referenz
|
||||||
|
- Path-Whitelist (nur facture/, commande/, propal/), Thumbs + ECM-Eintrag werden mitgelöscht
|
||||||
|
|
||||||
|
**1.3 Seitengröße A4/A3/A5/Letter + Hoch/Quer**
|
||||||
|
- Format und Orientation in der Bericht-Meta wählbar
|
||||||
|
- Auto-Save bei Änderung
|
||||||
|
- Bilder werden dynamisch auf die Seitengröße skaliert
|
||||||
|
|
||||||
|
**1.4 + 1.5 Mehrere Bilder pro Seite (DB-Schema)**
|
||||||
|
- Neue Tabelle `llx_bericht_page_image` für Multi-Image-Seiten
|
||||||
|
- Spalten `layout`, `image_scale`, `image_align` in `llx_bericht_page`
|
||||||
|
- Grid-Rendering im Editor + PDF folgt im nächsten Commit
|
||||||
|
|
||||||
|
**1.7 Tab „Berichte" auf Kundenkarte**
|
||||||
|
- Read-only Übersicht aller Berichte des Kunden
|
||||||
|
- Flache Tabelle sortiert nach Datum
|
||||||
|
- Springt zum Bericht oder zur Quelle (Auftrag/Rechnung/Angebot)
|
||||||
|
- Konstante `BERICHT_TAB_ON_THIRDPARTY` zum Aktivieren
|
||||||
|
|
||||||
|
### Sonstiges
|
||||||
|
- DB-Migrationen im `init()` für bestehende Installationen (ALTER TABLE mit Error-Suppress)
|
||||||
|
- `bericht_burn_annotations` und `bericht_render_cover_internal` in `lib/bericht.lib.php` zentralisiert (gemeinsam von generate_pdf + preview_pdf genutzt)
|
||||||
|
- Modal-CSS für Vorschau im Dolibarr Dark-Theme
|
||||||
|
|
||||||
## 1.0.0 — 2026-04-08
|
## 1.0.0 — 2026-04-08
|
||||||
|
|
||||||
Initiales Release.
|
Initiales Release.
|
||||||
|
|
|
||||||
|
|
@ -41,10 +41,12 @@ foreach (array(
|
||||||
// FPDI ist optional — wenn fehlt, können wir keine bestehenden PDFs einbetten,
|
// FPDI ist optional — wenn fehlt, können wir keine bestehenden PDFs einbetten,
|
||||||
// aber Bilder + Annotationen funktionieren weiterhin.
|
// aber Bilder + Annotationen funktionieren weiterhin.
|
||||||
|
|
||||||
|
$ori = in_array($bericht->page_orientation, array('P','L'), true) ? $bericht->page_orientation : 'P';
|
||||||
|
$fmt = in_array($bericht->page_format, array('A4','A3','A5','Letter'), true) ? $bericht->page_format : 'A4';
|
||||||
if ($fpdi_loaded) {
|
if ($fpdi_loaded) {
|
||||||
$pdf = new \setasign\Fpdi\Tcpdf\Fpdi('P', 'mm', 'A4', true, 'UTF-8', false);
|
$pdf = new \setasign\Fpdi\Tcpdf\Fpdi($ori, 'mm', $fmt, true, 'UTF-8', false);
|
||||||
} else {
|
} else {
|
||||||
$pdf = new TCPDF('P', 'mm', 'A4', true, 'UTF-8', false);
|
$pdf = new TCPDF($ori, 'mm', $fmt, true, 'UTF-8', false);
|
||||||
}
|
}
|
||||||
$pdf->SetCreator('Dolibarr Bericht-Modul');
|
$pdf->SetCreator('Dolibarr Bericht-Modul');
|
||||||
$pdf->SetAuthor($user->getFullName($langs));
|
$pdf->SetAuthor($user->getFullName($langs));
|
||||||
|
|
@ -80,16 +82,19 @@ foreach ($pages as $page) {
|
||||||
if (!$full || !file_exists($full)) continue;
|
if (!$full || !file_exists($full)) continue;
|
||||||
|
|
||||||
if ($page->source_type === 'image' || preg_match('/\.(png|jpe?g)$/i', $full)) {
|
if ($page->source_type === 'image' || preg_match('/\.(png|jpe?g)$/i', $full)) {
|
||||||
// Bild als A4-Seite
|
// Bild als Seite im konfigurierten Format/Orientation
|
||||||
$pdf->AddPage('P', 'A4');
|
$pdf->AddPage($ori, $fmt);
|
||||||
list($iw, $ih) = @getimagesize($full);
|
list($iw, $ih) = @getimagesize($full);
|
||||||
if ($iw && $ih) {
|
if ($iw && $ih) {
|
||||||
$maxW = 190; $maxH = 277;
|
$pageW = $pdf->getPageWidth();
|
||||||
|
$pageH = $pdf->getPageHeight();
|
||||||
|
$margin = 10;
|
||||||
|
$maxW = $pageW - 2 * $margin;
|
||||||
|
$maxH = $pageH - 2 * $margin - 10; // 10mm für Notiz unten
|
||||||
$ratio = min($maxW / $iw, $maxH / $ih);
|
$ratio = min($maxW / $iw, $maxH / $ih);
|
||||||
$w = $iw * $ratio; $h = $ih * $ratio;
|
$w = $iw * $ratio; $h = $ih * $ratio;
|
||||||
$x = (210 - $w) / 2; $y = 10;
|
$x = ($pageW - $w) / 2; $y = $margin;
|
||||||
$pdf->Image($full, $x, $y, $w, $h);
|
$pdf->Image($full, $x, $y, $w, $h);
|
||||||
// Annotationen drauf
|
|
||||||
bericht_burn_annotations($pdf, $page->fabric_json, $x, $y, $w, $h);
|
bericht_burn_annotations($pdf, $page->fabric_json, $x, $y, $w, $h);
|
||||||
}
|
}
|
||||||
} elseif ($fpdi_loaded && preg_match('/\.pdf$/i', $full)) {
|
} elseif ($fpdi_loaded && preg_match('/\.pdf$/i', $full)) {
|
||||||
|
|
|
||||||
|
|
@ -51,10 +51,12 @@ foreach (array(
|
||||||
DOL_DOCUMENT_ROOT.'/includes/fpdi/src/Tcpdf/Fpdi.php',
|
DOL_DOCUMENT_ROOT.'/includes/fpdi/src/Tcpdf/Fpdi.php',
|
||||||
) as $p) { if (file_exists($p)) { require_once $p; $fpdi_loaded = true; break; } }
|
) as $p) { if (file_exists($p)) { require_once $p; $fpdi_loaded = true; break; } }
|
||||||
|
|
||||||
|
$ori = in_array($bericht->page_orientation, array('P','L'), true) ? $bericht->page_orientation : 'P';
|
||||||
|
$fmt = in_array($bericht->page_format, array('A4','A3','A5','Letter'), true) ? $bericht->page_format : 'A4';
|
||||||
if ($fpdi_loaded) {
|
if ($fpdi_loaded) {
|
||||||
$pdf = new \setasign\Fpdi\Tcpdf\Fpdi('P', 'mm', 'A4', true, 'UTF-8', false);
|
$pdf = new \setasign\Fpdi\Tcpdf\Fpdi($ori, 'mm', $fmt, true, 'UTF-8', false);
|
||||||
} else {
|
} else {
|
||||||
$pdf = new TCPDF('P', 'mm', 'A4', true, 'UTF-8', false);
|
$pdf = new TCPDF($ori, 'mm', $fmt, true, 'UTF-8', false);
|
||||||
}
|
}
|
||||||
$pdf->SetCreator('Dolibarr Bericht-Modul (Vorschau)');
|
$pdf->SetCreator('Dolibarr Bericht-Modul (Vorschau)');
|
||||||
$pdf->SetAuthor($user->getFullName($langs));
|
$pdf->SetAuthor($user->getFullName($langs));
|
||||||
|
|
@ -90,13 +92,17 @@ foreach ($pages as $page) {
|
||||||
if (!$full || !file_exists($full)) continue;
|
if (!$full || !file_exists($full)) continue;
|
||||||
|
|
||||||
if ($page->source_type === 'image' || preg_match('/\.(png|jpe?g)$/i', $full)) {
|
if ($page->source_type === 'image' || preg_match('/\.(png|jpe?g)$/i', $full)) {
|
||||||
$pdf->AddPage('P', 'A4');
|
$pdf->AddPage($ori, $fmt);
|
||||||
list($iw, $ih) = @getimagesize($full);
|
list($iw, $ih) = @getimagesize($full);
|
||||||
if ($iw && $ih) {
|
if ($iw && $ih) {
|
||||||
$maxW = 190; $maxH = 277;
|
$pageW = $pdf->getPageWidth();
|
||||||
|
$pageH = $pdf->getPageHeight();
|
||||||
|
$margin = 10;
|
||||||
|
$maxW = $pageW - 2 * $margin;
|
||||||
|
$maxH = $pageH - 2 * $margin - 10;
|
||||||
$ratio = min($maxW / $iw, $maxH / $ih);
|
$ratio = min($maxW / $iw, $maxH / $ih);
|
||||||
$w = $iw * $ratio; $h = $ih * $ratio;
|
$w = $iw * $ratio; $h = $ih * $ratio;
|
||||||
$x = (210 - $w) / 2; $y = 10;
|
$x = ($pageW - $w) / 2; $y = $margin;
|
||||||
$pdf->Image($full, $x, $y, $w, $h);
|
$pdf->Image($full, $x, $y, $w, $h);
|
||||||
if (function_exists('bericht_burn_annotations')) {
|
if (function_exists('bericht_burn_annotations')) {
|
||||||
bericht_burn_annotations($pdf, $page->fabric_json, $x, $y, $w, $h);
|
bericht_burn_annotations($pdf, $page->fabric_json, $x, $y, $w, $h);
|
||||||
|
|
|
||||||
20
ajax/save_meta.php
Normal file
20
ajax/save_meta.php
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
<?php
|
||||||
|
/* Speichert Meta-Felder eines Berichts (Titel, Template, Format, Orientation).
|
||||||
|
* POST: berichtid, titel, template_odt, page_format, page_orientation, token
|
||||||
|
*/
|
||||||
|
require_once __DIR__.'/_inc.php';
|
||||||
|
|
||||||
|
global $db, $user;
|
||||||
|
if (!$user->hasRight('bericht', 'write')) bericht_ajax_fail('Permission denied', 403);
|
||||||
|
|
||||||
|
$berichtid = (int) ($_POST['berichtid'] ?? 0);
|
||||||
|
$bericht = new Bericht($db);
|
||||||
|
if ($bericht->fetch($berichtid) <= 0) bericht_ajax_fail('Bericht nicht gefunden', 404);
|
||||||
|
|
||||||
|
if (isset($_POST['titel'])) $bericht->titel = (string) $_POST['titel'];
|
||||||
|
if (isset($_POST['template_odt'])) $bericht->template_odt = (string) $_POST['template_odt'];
|
||||||
|
if (isset($_POST['page_format'])) $bericht->page_format = in_array($_POST['page_format'], array('A4','A3','A5','Letter'), true) ? $_POST['page_format'] : 'A4';
|
||||||
|
if (isset($_POST['page_orientation'])) $bericht->page_orientation = in_array($_POST['page_orientation'], array('P','L'), true) ? $_POST['page_orientation'] : 'P';
|
||||||
|
|
||||||
|
if ($bericht->update($user) > 0) bericht_ajax_ok();
|
||||||
|
bericht_ajax_fail($bericht->error ?: 'Update fehlgeschlagen');
|
||||||
|
|
@ -261,6 +261,7 @@ if (!$bericht) {
|
||||||
'generate_pdf' => dol_buildpath('/bericht/ajax/generate_pdf.php', 1),
|
'generate_pdf' => dol_buildpath('/bericht/ajax/generate_pdf.php', 1),
|
||||||
'preview_pdf' => dol_buildpath('/bericht/ajax/preview_pdf.php', 1),
|
'preview_pdf' => dol_buildpath('/bericht/ajax/preview_pdf.php', 1),
|
||||||
'delete_attachment'=> dol_buildpath('/bericht/ajax/delete_attachment.php', 1),
|
'delete_attachment'=> dol_buildpath('/bericht/ajax/delete_attachment.php', 1),
|
||||||
|
'save_meta' => dol_buildpath('/bericht/ajax/save_meta.php', 1),
|
||||||
),
|
),
|
||||||
'lang' => array(
|
'lang' => array(
|
||||||
'undo' => $langs->trans("BerichtUndo"),
|
'undo' => $langs->trans("BerichtUndo"),
|
||||||
|
|
@ -296,6 +297,24 @@ if (!$bericht) {
|
||||||
print '<option value="'.dol_escape_htmltag($tpl).'"'.$sel.'>'.dol_escape_htmltag($tpl).'</option>';
|
print '<option value="'.dol_escape_htmltag($tpl).'"'.$sel.'>'.dol_escape_htmltag($tpl).'</option>';
|
||||||
}
|
}
|
||||||
print '</select></td>';
|
print '</select></td>';
|
||||||
|
print '</tr><tr>';
|
||||||
|
// Format und Orientation
|
||||||
|
print '<td>Format</td><td>';
|
||||||
|
print '<select id="meta-format" name="page_format" title="Seitenformat des PDFs">';
|
||||||
|
foreach (array('A4', 'A3', 'A5', 'Letter') as $fmt) {
|
||||||
|
$sel = ($fmt === $bericht->page_format) ? ' selected' : '';
|
||||||
|
print '<option value="'.$fmt.'"'.$sel.'>'.$fmt.'</option>';
|
||||||
|
}
|
||||||
|
print '</select>';
|
||||||
|
print ' ';
|
||||||
|
print '<select id="meta-orientation" name="page_orientation" title="Hoch- oder Querformat">';
|
||||||
|
foreach (array('P' => 'Hochformat', 'L' => 'Querformat') as $k => $v) {
|
||||||
|
$sel = ($k === $bericht->page_orientation) ? ' selected' : '';
|
||||||
|
print '<option value="'.$k.'"'.$sel.'>'.$v.'</option>';
|
||||||
|
}
|
||||||
|
print '</select>';
|
||||||
|
print '</td>';
|
||||||
|
print '<td colspan="2"><span class="opacitymedium small">Wird beim Finalisieren angewendet</span></td>';
|
||||||
print '</tr></table>';
|
print '</tr></table>';
|
||||||
print '</form>';
|
print '</form>';
|
||||||
print '</div>';
|
print '</div>';
|
||||||
|
|
|
||||||
128
bericht_thirdparty.php
Normal file
128
bericht_thirdparty.php
Normal file
|
|
@ -0,0 +1,128 @@
|
||||||
|
<?php
|
||||||
|
/* Read-only Übersicht aller Berichte eines Kunden.
|
||||||
|
* Listet ALLE Berichte zu den Aufträgen + Rechnungen + Angeboten dieses Kunden,
|
||||||
|
* sortiert nach Datum (neueste zuerst).
|
||||||
|
*
|
||||||
|
* NICHT zum Anlegen von Berichten. Hier wird kein Bericht direkt am Kunden gespeichert.
|
||||||
|
*
|
||||||
|
* GET: socid (thirdparty id)
|
||||||
|
*/
|
||||||
|
|
||||||
|
$res = 0;
|
||||||
|
if (!$res && !empty($_SERVER["CONTEXT_DOCUMENT_ROOT"])) $res = @include $_SERVER["CONTEXT_DOCUMENT_ROOT"]."/main.inc.php";
|
||||||
|
$tmp = empty($_SERVER['SCRIPT_FILENAME']) ? '' : $_SERVER['SCRIPT_FILENAME']; $tmp2 = realpath(__FILE__); $i = strlen($tmp) - 1; $j = strlen($tmp2) - 1;
|
||||||
|
while ($i > 0 && $j > 0 && isset($tmp[$i]) && isset($tmp2[$j]) && $tmp[$i] == $tmp2[$j]) { $i--; $j--; }
|
||||||
|
if (!$res && $i > 0 && file_exists(substr($tmp, 0, ($i + 1))."/main.inc.php")) $res = @include substr($tmp, 0, ($i + 1))."/main.inc.php";
|
||||||
|
if (!$res && $i > 0 && file_exists(dirname(substr($tmp, 0, ($i + 1)))."/main.inc.php")) $res = @include dirname(substr($tmp, 0, ($i + 1)))."/main.inc.php";
|
||||||
|
if (!$res && file_exists("../main.inc.php")) $res = @include "../main.inc.php";
|
||||||
|
if (!$res && file_exists("../../main.inc.php")) $res = @include "../../main.inc.php";
|
||||||
|
if (!$res) die("Include of main fails");
|
||||||
|
|
||||||
|
require_once DOL_DOCUMENT_ROOT.'/societe/class/societe.class.php';
|
||||||
|
require_once DOL_DOCUMENT_ROOT.'/core/lib/company.lib.php';
|
||||||
|
require_once __DIR__.'/class/bericht.class.php';
|
||||||
|
|
||||||
|
if (!$user->hasRight('bericht', 'read')) accessforbidden();
|
||||||
|
if (!$user->hasRight('societe', 'lire')) accessforbidden();
|
||||||
|
|
||||||
|
$langs->loadLangs(array("bericht@bericht", "main", "companies"));
|
||||||
|
|
||||||
|
$socid = GETPOSTINT('socid');
|
||||||
|
if (!$socid) accessforbidden('socid fehlt');
|
||||||
|
|
||||||
|
$soc = new Societe($db);
|
||||||
|
if ($soc->fetch($socid) <= 0) accessforbidden('Kunde nicht gefunden');
|
||||||
|
|
||||||
|
// Berichte des Kunden ermitteln (ein UNION über alle drei Element-Typen)
|
||||||
|
$sql = "SELECT b.rowid, b.ref AS bref, b.titel, b.datec, b.status, b.element_type,"
|
||||||
|
." 'order' AS quelle, c.ref AS parent_ref, c.rowid AS parent_id"
|
||||||
|
." FROM ".$db->prefix()."bericht b"
|
||||||
|
." INNER JOIN ".$db->prefix()."commande c ON c.rowid = b.fk_element"
|
||||||
|
." WHERE b.element_type = 'order' AND c.fk_soc = ".((int) $socid)
|
||||||
|
." UNION "
|
||||||
|
."SELECT b.rowid, b.ref AS bref, b.titel, b.datec, b.status, b.element_type,"
|
||||||
|
." 'invoice' AS quelle, f.ref AS parent_ref, f.rowid AS parent_id"
|
||||||
|
." FROM ".$db->prefix()."bericht b"
|
||||||
|
." INNER JOIN ".$db->prefix()."facture f ON f.rowid = b.fk_element"
|
||||||
|
." WHERE b.element_type = 'invoice' AND f.fk_soc = ".((int) $socid)
|
||||||
|
." UNION "
|
||||||
|
."SELECT b.rowid, b.ref AS bref, b.titel, b.datec, b.status, b.element_type,"
|
||||||
|
." 'propal' AS quelle, p.ref AS parent_ref, p.rowid AS parent_id"
|
||||||
|
." FROM ".$db->prefix()."bericht b"
|
||||||
|
." INNER JOIN ".$db->prefix()."propal p ON p.rowid = b.fk_element"
|
||||||
|
." WHERE b.element_type = 'propal' AND p.fk_soc = ".((int) $socid)
|
||||||
|
." ORDER BY datec DESC";
|
||||||
|
|
||||||
|
$rows = array();
|
||||||
|
$resq = $db->query($sql);
|
||||||
|
if ($resq) {
|
||||||
|
while ($obj = $db->fetch_object($resq)) {
|
||||||
|
$rows[] = $obj;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
llxHeader('', $langs->trans("Berichte").' — '.$soc->name, '', '', 0, 0, array(), array(), '', 'mod-bericht page-bericht-thirdparty');
|
||||||
|
|
||||||
|
$head = societe_prepare_head($soc);
|
||||||
|
print dol_get_fiche_head($head, 'bericht', $langs->trans("ThirdParty"), -1, 'company');
|
||||||
|
|
||||||
|
dol_banner_tab($soc, 'socid', '', ($user->socid ? 0 : 1), 'rowid', 'nom');
|
||||||
|
print dol_get_fiche_end();
|
||||||
|
|
||||||
|
print '<br>';
|
||||||
|
|
||||||
|
print '<div class="bericht-thirdparty-list">';
|
||||||
|
print '<h3>'.$langs->trans("Berichte").' ('.count($rows).')</h3>';
|
||||||
|
print '<p class="opacitymedium small">Read-only Übersicht aller Berichte aus Aufträgen, Rechnungen und Angeboten dieses Kunden. Bericht-Anlage erfolgt direkt am jeweiligen Auftrag oder der Rechnung.</p>';
|
||||||
|
|
||||||
|
if (empty($rows)) {
|
||||||
|
print '<div class="opacitymedium" style="padding:20px;">Noch keine Berichte für diesen Kunden vorhanden.</div>';
|
||||||
|
} else {
|
||||||
|
print '<table class="noborder centpercent">';
|
||||||
|
print '<tr class="liste_titre">';
|
||||||
|
print '<th>'.$langs->trans("Ref").'</th>';
|
||||||
|
print '<th>'.$langs->trans("BerichtTitle").'</th>';
|
||||||
|
print '<th>Quelle</th>';
|
||||||
|
print '<th>'.$langs->trans("BerichtCreatedAt").'</th>';
|
||||||
|
print '<th>'.$langs->trans("BerichtStatus").'</th>';
|
||||||
|
print '<th class="right">'.$langs->trans("Action").'</th>';
|
||||||
|
print '</tr>';
|
||||||
|
|
||||||
|
foreach ($rows as $r) {
|
||||||
|
$bericht_url = dol_buildpath('/bericht/bericht_card.php', 1).'?berichtid='.$r->rowid;
|
||||||
|
|
||||||
|
// Parent-URL berechnen
|
||||||
|
if ($r->quelle === 'order') {
|
||||||
|
$parent_url = DOL_URL_ROOT.'/commande/card.php?id='.$r->parent_id;
|
||||||
|
$quelle_label = '🛒 Auftrag';
|
||||||
|
} elseif ($r->quelle === 'invoice') {
|
||||||
|
$parent_url = DOL_URL_ROOT.'/compta/facture/card.php?id='.$r->parent_id;
|
||||||
|
$quelle_label = '📄 Rechnung';
|
||||||
|
} else {
|
||||||
|
$parent_url = DOL_URL_ROOT.'/comm/propal/card.php?id='.$r->parent_id;
|
||||||
|
$quelle_label = '📋 Angebot';
|
||||||
|
}
|
||||||
|
|
||||||
|
$status_html = ((int) $r->status === 1)
|
||||||
|
? '<span class="badge badge-status4">Final</span>'
|
||||||
|
: '<span class="badge badge-status0">Entwurf</span>';
|
||||||
|
|
||||||
|
print '<tr class="oddeven">';
|
||||||
|
print '<td><a href="'.$bericht_url.'">'.dol_escape_htmltag($r->bref).'</a></td>';
|
||||||
|
print '<td>'.dol_escape_htmltag($r->titel).'</td>';
|
||||||
|
print '<td>'.$quelle_label.' <a href="'.$parent_url.'">'.dol_escape_htmltag($r->parent_ref).'</a></td>';
|
||||||
|
print '<td>'.dol_print_date($db->jdate($r->datec), 'dayhour').'</td>';
|
||||||
|
print '<td>'.$status_html.'</td>';
|
||||||
|
print '<td class="right">';
|
||||||
|
print '<a href="'.$bericht_url.'" class="button-small">Öffnen</a> ';
|
||||||
|
print '<a href="'.$parent_url.'" class="button-small">Zur Quelle</a>';
|
||||||
|
print '</td>';
|
||||||
|
print '</tr>';
|
||||||
|
}
|
||||||
|
print '</table>';
|
||||||
|
}
|
||||||
|
|
||||||
|
print '</div>';
|
||||||
|
|
||||||
|
llxFooter();
|
||||||
|
$db->close();
|
||||||
|
|
@ -24,6 +24,8 @@ class Bericht extends CommonObject
|
||||||
public $fk_element;
|
public $fk_element;
|
||||||
public $auftragsnummer;
|
public $auftragsnummer;
|
||||||
public $template_odt;
|
public $template_odt;
|
||||||
|
public $page_format = 'A4'; // A4, A3, Letter
|
||||||
|
public $page_orientation = 'P'; // P=Portrait, L=Landscape
|
||||||
public $status; // 0 = Entwurf, 1 = Final
|
public $status; // 0 = Entwurf, 1 = Final
|
||||||
public $final_pdf_path;
|
public $final_pdf_path;
|
||||||
public $fk_user_creat;
|
public $fk_user_creat;
|
||||||
|
|
@ -54,7 +56,7 @@ class Bericht extends CommonObject
|
||||||
}
|
}
|
||||||
|
|
||||||
$sql = "INSERT INTO ".$this->db->prefix()."bericht ("
|
$sql = "INSERT INTO ".$this->db->prefix()."bericht ("
|
||||||
."entity, ref, titel, element_type, fk_element, auftragsnummer, template_odt, status, fk_user_creat, datec, note"
|
."entity, ref, titel, element_type, fk_element, auftragsnummer, template_odt, page_format, page_orientation, status, fk_user_creat, datec, note"
|
||||||
.") VALUES ("
|
.") VALUES ("
|
||||||
.((int) $this->entity).","
|
.((int) $this->entity).","
|
||||||
."'".$this->db->escape($this->ref)."',"
|
."'".$this->db->escape($this->ref)."',"
|
||||||
|
|
@ -63,6 +65,8 @@ class Bericht extends CommonObject
|
||||||
.((int) $this->fk_element).","
|
.((int) $this->fk_element).","
|
||||||
.($this->auftragsnummer ? "'".$this->db->escape($this->auftragsnummer)."'" : "NULL").","
|
.($this->auftragsnummer ? "'".$this->db->escape($this->auftragsnummer)."'" : "NULL").","
|
||||||
.($this->template_odt ? "'".$this->db->escape($this->template_odt)."'" : "NULL").","
|
.($this->template_odt ? "'".$this->db->escape($this->template_odt)."'" : "NULL").","
|
||||||
|
."'".$this->db->escape($this->page_format)."',"
|
||||||
|
."'".$this->db->escape($this->page_orientation)."',"
|
||||||
.((int) $this->status).","
|
.((int) $this->status).","
|
||||||
.((int) $this->fk_user_creat).","
|
.((int) $this->fk_user_creat).","
|
||||||
."'".$this->db->idate($this->datec)."',"
|
."'".$this->db->idate($this->datec)."',"
|
||||||
|
|
@ -84,6 +88,7 @@ class Bericht extends CommonObject
|
||||||
public function fetch($id)
|
public function fetch($id)
|
||||||
{
|
{
|
||||||
$sql = "SELECT rowid, entity, ref, titel, element_type, fk_element, auftragsnummer, template_odt,"
|
$sql = "SELECT rowid, entity, ref, titel, element_type, fk_element, auftragsnummer, template_odt,"
|
||||||
|
." page_format, page_orientation,"
|
||||||
." status, final_pdf_path, fk_user_creat, fk_user_modif, datec, tms, note"
|
." status, final_pdf_path, fk_user_creat, fk_user_modif, datec, tms, note"
|
||||||
." FROM ".$this->db->prefix()."bericht WHERE rowid = ".((int) $id);
|
." FROM ".$this->db->prefix()."bericht WHERE rowid = ".((int) $id);
|
||||||
$res = $this->db->query($sql);
|
$res = $this->db->query($sql);
|
||||||
|
|
@ -103,6 +108,8 @@ class Bericht extends CommonObject
|
||||||
$this->fk_element = $obj->fk_element;
|
$this->fk_element = $obj->fk_element;
|
||||||
$this->auftragsnummer = $obj->auftragsnummer;
|
$this->auftragsnummer = $obj->auftragsnummer;
|
||||||
$this->template_odt = $obj->template_odt;
|
$this->template_odt = $obj->template_odt;
|
||||||
|
$this->page_format = $obj->page_format ?: 'A4';
|
||||||
|
$this->page_orientation= $obj->page_orientation ?: 'P';
|
||||||
$this->status = (int) $obj->status;
|
$this->status = (int) $obj->status;
|
||||||
$this->final_pdf_path = $obj->final_pdf_path;
|
$this->final_pdf_path = $obj->final_pdf_path;
|
||||||
$this->fk_user_creat = $obj->fk_user_creat;
|
$this->fk_user_creat = $obj->fk_user_creat;
|
||||||
|
|
@ -119,6 +126,8 @@ class Bericht extends CommonObject
|
||||||
."titel = ".($this->titel ? "'".$this->db->escape($this->titel)."'" : "NULL").","
|
."titel = ".($this->titel ? "'".$this->db->escape($this->titel)."'" : "NULL").","
|
||||||
."auftragsnummer = ".($this->auftragsnummer ? "'".$this->db->escape($this->auftragsnummer)."'" : "NULL").","
|
."auftragsnummer = ".($this->auftragsnummer ? "'".$this->db->escape($this->auftragsnummer)."'" : "NULL").","
|
||||||
."template_odt = ".($this->template_odt ? "'".$this->db->escape($this->template_odt)."'" : "NULL").","
|
."template_odt = ".($this->template_odt ? "'".$this->db->escape($this->template_odt)."'" : "NULL").","
|
||||||
|
."page_format = '".$this->db->escape($this->page_format ?: 'A4')."',"
|
||||||
|
."page_orientation = '".$this->db->escape($this->page_orientation ?: 'P')."',"
|
||||||
."status = ".((int) $this->status).","
|
."status = ".((int) $this->status).","
|
||||||
."final_pdf_path = ".($this->final_pdf_path ? "'".$this->db->escape($this->final_pdf_path)."'" : "NULL").","
|
."final_pdf_path = ".($this->final_pdf_path ? "'".$this->db->escape($this->final_pdf_path)."'" : "NULL").","
|
||||||
."note = ".($this->note ? "'".$this->db->escape($this->note)."'" : "NULL").","
|
."note = ".($this->note ? "'".$this->db->escape($this->note)."'" : "NULL").","
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ class modBericht extends DolibarrModules
|
||||||
|
|
||||||
$this->editor_name = 'Alles Watt läuft';
|
$this->editor_name = 'Alles Watt läuft';
|
||||||
$this->editor_url = '';
|
$this->editor_url = '';
|
||||||
$this->version = '1.0.0';
|
$this->version = '1.1.0';
|
||||||
$this->const_name = 'MAIN_MODULE_'.strtoupper($this->name);
|
$this->const_name = 'MAIN_MODULE_'.strtoupper($this->name);
|
||||||
$this->picto = 'fa-file-pdf';
|
$this->picto = 'fa-file-pdf';
|
||||||
|
|
||||||
|
|
@ -74,6 +74,7 @@ class modBericht extends DolibarrModules
|
||||||
3 => array('BERICHT_TAB_ON_PROPAL', 'chaine', '1', 'Reiter Bericht auf Angeboten anzeigen', 0, 'current', 0),
|
3 => array('BERICHT_TAB_ON_PROPAL', 'chaine', '1', 'Reiter Bericht auf Angeboten anzeigen', 0, 'current', 0),
|
||||||
4 => array('BERICHT_BURN_ANNOTATIONS', 'chaine', '1', 'Annotationen beim Export ins PDF einbrennen', 0, 'current', 0),
|
4 => array('BERICHT_BURN_ANNOTATIONS', 'chaine', '1', 'Annotationen beim Export ins PDF einbrennen', 0, 'current', 0),
|
||||||
5 => array('BERICHT_LIBREOFFICE_BIN', 'chaine', '/usr/bin/libreoffice', 'Pfad zu LibreOffice für ODT→PDF Konvertierung', 0, 'current', 0),
|
5 => array('BERICHT_LIBREOFFICE_BIN', 'chaine', '/usr/bin/libreoffice', 'Pfad zu LibreOffice für ODT→PDF Konvertierung', 0, 'current', 0),
|
||||||
|
6 => array('BERICHT_TAB_ON_THIRDPARTY', 'chaine', '1', 'Reiter Berichte auf Kundenkarten (read-only Übersicht)', 0, 'current', 0),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Tabs werden über den Hook (actions_bericht.class.php → addMoreActionsButtons / completeTabsHead)
|
// Tabs werden über den Hook (actions_bericht.class.php → addMoreActionsButtons / completeTabsHead)
|
||||||
|
|
@ -83,6 +84,7 @@ class modBericht extends DolibarrModules
|
||||||
'invoice:+bericht:Bericht:bericht@bericht:$user->hasRight("bericht","read"):/custom/bericht/bericht_card.php?id=__ID__&element=invoice',
|
'invoice:+bericht:Bericht:bericht@bericht:$user->hasRight("bericht","read"):/custom/bericht/bericht_card.php?id=__ID__&element=invoice',
|
||||||
'order:+bericht:Bericht:bericht@bericht:$user->hasRight("bericht","read"):/custom/bericht/bericht_card.php?id=__ID__&element=order',
|
'order:+bericht:Bericht:bericht@bericht:$user->hasRight("bericht","read"):/custom/bericht/bericht_card.php?id=__ID__&element=order',
|
||||||
'propal:+bericht:Bericht:bericht@bericht:$user->hasRight("bericht","read"):/custom/bericht/bericht_card.php?id=__ID__&element=propal',
|
'propal:+bericht:Bericht:bericht@bericht:$user->hasRight("bericht","read"):/custom/bericht/bericht_card.php?id=__ID__&element=propal',
|
||||||
|
'thirdparty:+bericht:Berichte:bericht@bericht:$user->hasRight("bericht","read"):/custom/bericht/bericht_thirdparty.php?socid=__ID__',
|
||||||
);
|
);
|
||||||
|
|
||||||
$this->dictionaries = array();
|
$this->dictionaries = array();
|
||||||
|
|
@ -138,6 +140,22 @@ class modBericht extends DolibarrModules
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Migrationen für bestehende Tabellen
|
||||||
|
$migrations = array(
|
||||||
|
// Phase 1.3: Seitenformat
|
||||||
|
"ALTER TABLE ".$this->db->prefix()."bericht ADD COLUMN page_format VARCHAR(8) DEFAULT 'A4'",
|
||||||
|
"ALTER TABLE ".$this->db->prefix()."bericht ADD COLUMN page_orientation VARCHAR(8) DEFAULT 'P'",
|
||||||
|
// Phase 1.4: Layout für mehrere Bilder pro Seite
|
||||||
|
"ALTER TABLE ".$this->db->prefix()."bericht_page ADD COLUMN layout VARCHAR(16) DEFAULT 'single'",
|
||||||
|
// Phase 1.5: Bildgröße/-position
|
||||||
|
"ALTER TABLE ".$this->db->prefix()."bericht_page ADD COLUMN image_scale FLOAT DEFAULT 1.0",
|
||||||
|
"ALTER TABLE ".$this->db->prefix()."bericht_page ADD COLUMN image_align VARCHAR(16) DEFAULT 'fit'",
|
||||||
|
);
|
||||||
|
foreach ($migrations as $sql) {
|
||||||
|
// Errors ignorieren — Spalten existieren ggf. schon
|
||||||
|
$this->db->query($sql, 1);
|
||||||
|
}
|
||||||
|
|
||||||
// Extrafields auf facture sicherstellen — vorhandene werden NICHT angefasst
|
// Extrafields auf facture sicherstellen — vorhandene werden NICHT angefasst
|
||||||
require_once DOL_DOCUMENT_ROOT.'/core/class/extrafields.class.php';
|
require_once DOL_DOCUMENT_ROOT.'/core/class/extrafields.class.php';
|
||||||
$extrafields = new ExtraFields($this->db);
|
$extrafields = new ExtraFields($this->db);
|
||||||
|
|
|
||||||
25
js/editor.js
25
js/editor.js
|
|
@ -565,8 +565,33 @@
|
||||||
if (showMessage && data.success) toast('Seite gespeichert');
|
if (showMessage && data.success) toast('Seite gespeichert');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---------- Meta-Felder Auto-Save ---------- */
|
||||||
|
async function saveMeta() {
|
||||||
|
const fd = new FormData();
|
||||||
|
fd.append('token', cfg.token);
|
||||||
|
fd.append('berichtid', cfg.berichtid);
|
||||||
|
const titelEl = document.querySelector('input[name="titel"]');
|
||||||
|
const tplEl = document.querySelector('select[name="template_odt"]');
|
||||||
|
const fmtEl = document.getElementById('meta-format');
|
||||||
|
const oriEl = document.getElementById('meta-orientation');
|
||||||
|
if (titelEl) fd.append('titel', titelEl.value);
|
||||||
|
if (tplEl) fd.append('template_odt', tplEl.value);
|
||||||
|
if (fmtEl) fd.append('page_format', fmtEl.value);
|
||||||
|
if (oriEl) fd.append('page_orientation', oriEl.value);
|
||||||
|
await fetch(cfg.urls.save_meta, { method: 'POST', body: fd });
|
||||||
|
}
|
||||||
|
function bindMetaAutoSave() {
|
||||||
|
['input[name="titel"]', 'select[name="template_odt"]', '#meta-format', '#meta-orientation'].forEach(sel => {
|
||||||
|
const el = document.querySelector(sel);
|
||||||
|
if (!el) return;
|
||||||
|
el.addEventListener('change', saveMeta);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function bindActions() {
|
function bindActions() {
|
||||||
|
bindMetaAutoSave();
|
||||||
document.getElementById('btn-save-draft').addEventListener('click', async () => {
|
document.getElementById('btn-save-draft').addEventListener('click', async () => {
|
||||||
|
await saveMeta();
|
||||||
await savePageAnnotations(true);
|
await savePageAnnotations(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@ CREATE TABLE llx_bericht (
|
||||||
fk_element INTEGER NOT NULL, -- ID des Parent-Objekts
|
fk_element INTEGER NOT NULL, -- ID des Parent-Objekts
|
||||||
auftragsnummer VARCHAR(255) DEFAULT NULL,
|
auftragsnummer VARCHAR(255) DEFAULT NULL,
|
||||||
template_odt VARCHAR(255) DEFAULT NULL, -- Dateiname aus templates/
|
template_odt VARCHAR(255) DEFAULT NULL, -- Dateiname aus templates/
|
||||||
|
page_format VARCHAR(8) DEFAULT 'A4', -- A4, A3, Letter
|
||||||
|
page_orientation VARCHAR(8) DEFAULT 'P', -- P=Portrait, L=Landscape
|
||||||
status INTEGER DEFAULT 0 NOT NULL, -- 0=Entwurf, 1=Final
|
status INTEGER DEFAULT 0 NOT NULL, -- 0=Entwurf, 1=Final
|
||||||
final_pdf_path VARCHAR(512) DEFAULT NULL, -- Pfad relativ zu DOL_DATA_ROOT
|
final_pdf_path VARCHAR(512) DEFAULT NULL, -- Pfad relativ zu DOL_DATA_ROOT
|
||||||
fk_user_creat INTEGER NOT NULL,
|
fk_user_creat INTEGER NOT NULL,
|
||||||
|
|
|
||||||
2
sql/llx_bericht_page_image.key.sql
Normal file
2
sql/llx_bericht_page_image.key.sql
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
ALTER TABLE llx_bericht_page_image ADD INDEX idx_bpi_page (fk_page, slot);
|
||||||
|
ALTER TABLE llx_bericht_page_image ADD CONSTRAINT fk_bpi_page FOREIGN KEY (fk_page) REFERENCES llx_bericht_page(rowid) ON DELETE CASCADE;
|
||||||
10
sql/llx_bericht_page_image.sql
Normal file
10
sql/llx_bericht_page_image.sql
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
-- Mehrere Bilder pro Seite (Phase 1.4).
|
||||||
|
-- Wenn llx_bericht_page.layout != 'single' enthält die Seite ein Grid aus mehreren Bildern.
|
||||||
|
CREATE TABLE llx_bericht_page_image (
|
||||||
|
rowid INTEGER AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
fk_page INTEGER NOT NULL,
|
||||||
|
slot INTEGER NOT NULL, -- 0-basierter Slot im Grid
|
||||||
|
source_path VARCHAR(512) NOT NULL,
|
||||||
|
rotation INTEGER DEFAULT 0,
|
||||||
|
tms TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL
|
||||||
|
) ENGINE=innodb;
|
||||||
Loading…
Reference in a new issue