bericht/core/modules/modBericht.class.php
Eduard Wisch a8d8291c3a API 1.5.1: Thumbnails, Aufnahmezeit, Seitenfelder, Textnotizen
Die API-Seite zur Baustelle-PWA holt nach, was der Editor mit 1.5.0 bekommen hat.

photo.php kann jetzt size=thumb&w=<px> und rendert ueber bericht_attachment_thumb()
(GD, Cache, EXIF-Rotation) statt die Originaldatei auszuliefern. size=small konnte nur
ein von Dolibarr vorgefertigtes thumbs/<name>_small.<ext> liefern - fuer Uploads ueber
orders.php?action=upload_photo gibt es das nicht, dort laeuft kein vignette(). Die PWA
lud dadurch fuer jede Kachel das komplette Foto: gemessen 9,8 KB statt 235 KB je Kachel.
Antwort mit ETag aus der mtime, Folgeaufrufe enden mit 304.

orders.php?action=photos liefert taken_at (EXIF-Aufnahmezeit ueber bericht_file_taken_at,
sonst Dateidatum) und sortiert danach statt nach filemtime.

reports.php gibt je Seite title und composite_path aus. title konnte ueber pages.php
gesetzt werden, kam aber nie zurueck. composite_path ist das im Editor gebaute
Seitenbild - ohne das zeigen Clients nur das Rohbild ohne Anmerkungen, bei
Raster-Layouts eines von bis zu sechs Bildern und bei title_only gar keins.

Neu: api/note.php - Textnotizen zum Auftrag, aufgebaut wie die Sprachnotiz. Die Notiz
liegt als notiz_<betreff>_<datum>.txt im Auftragsverzeichnis und ist damit auch im
Dolibarr-Auftrag und in der Anhaenge-Spalte des Editors sichtbar. Liste, Lesen, Anlegen,
Aendern, Loeschen. Der file-Parameter wird gegen ^notiz_[A-Za-z0-9_.-]*\.txt$ geprueft;
gegen Pfad-Ausbrueche getestet.

ROADMAP.md wiederhergestellt - sie war mit eb37a4b geloescht worden, weil alle Punkte
abgehakt waren. Sie bleibt ab jetzt liegen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 18:11:42 +02:00

257 lines
13 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
/* Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
* GPL v3+ — siehe COPYING
*/
/**
* \defgroup bericht Module Bericht
* \brief Arbeitsberichte aus Anhängen erstellen, im Browser annotieren und an Rechnungen anhängen.
* \file htdocs/bericht/core/modules/modBericht.class.php
*/
include_once DOL_DOCUMENT_ROOT.'/core/modules/DolibarrModules.class.php';
class modBericht extends DolibarrModules
{
public function __construct($db)
{
global $conf, $langs;
$this->db = $db;
$this->numero = 500033; // Frei (500021 kollidiert mit BankImport, siehe llx_rights_def)
$this->rights_class = 'bericht';
$this->family = "other";
$this->module_position = '90';
$this->name = preg_replace('/^mod/i', '', get_class($this));
$this->description = "Arbeitsberichte aus Rechnungs-Anhängen erstellen, im Browser annotieren und als PDF an die Rechnung anhängen.";
$this->descriptionlong = "Fügt Rechnungen, Aufträgen und Angeboten einen Reiter 'Bericht' hinzu. Anhänge auswählen, im Browser mit Pfeilen/Kreisen/Rechtecken/Text annotieren, Seiten verwalten, Deckblatt aus ODT-Vorlage einfügen und als PDF unter Verknüpfte Dokumente speichern.";
$this->editor_name = 'Alles Watt läuft';
$this->editor_url = '';
$this->version = '1.5.1';
$this->const_name = 'MAIN_MODULE_'.strtoupper($this->name);
$this->picto = 'fa-file-pdf';
$this->module_parts = array(
'triggers' => 0,
'login' => 0,
'substitutions' => 0,
'menus' => 0,
'tpl' => 0,
'barcode' => 0,
'models' => 0,
'printing' => 0,
'theme' => 0,
'css' => array('/bericht/css/bericht.css'),
'js' => array(),
'hooks' => array('data' => array('odtgeneration')),
'moduleforexternal' => 0,
);
// Datenverzeichnisse
$this->dirs = array(
"/bericht/temp",
"/bericht/templates",
"/bericht/work",
);
// Konfigurationsseite im Admin-Bereich
$this->config_page_url = array("setup.php@bericht");
$this->hidden = false;
$this->depends = array();
$this->requiredby = array();
$this->conflictwith = array();
$this->langfiles = array("bericht@bericht");
$this->phpmin = array(7, 4);
$this->need_dolibarr_version = array(19, 0);
$this->need_javascript_ajax = 1;
// Konstanten beim Aktivieren anlegen
$this->const = array(
0 => array('BERICHT_DEFAULT_TEMPLATE', 'chaine', '', 'Standard ODT-Template für Deckblatt', 0, 'current', 0),
1 => array('BERICHT_TAB_ON_INVOICE', 'chaine', '1', 'Reiter Bericht auf Rechnungen anzeigen', 0, 'current', 0),
2 => array('BERICHT_TAB_ON_ORDER', 'chaine', '1', 'Reiter Bericht auf Aufträgen 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),
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),
7 => array('BERICHT_TAB_ON_SHIPMENT', 'chaine', '1', 'Reiter Bericht auf Lieferungen anzeigen', 0, 'current', 0),
8 => array('BERICHT_SIGNATURE_BOX_DEFAULT', 'chaine',
'{"page":"last","x_mm":120,"y_mm":230,"w_mm":70,"h_mm":35,"label":"Unterschrift Kunde"}',
'Standard-Geometrie der Unterschriftsbox auf Lieferschein-PDFs (JSON)', 0, 'current', 0),
9 => array('BERICHT_SIGNATURE_IMAGE_RATIO', 'chaine', '0.35',
'Groessen-Faktor fuer Signatur im ODT (0.3 ergibt ca. 6×3 cm bei 800×400 Pixel)', 0, 'current', 0),
);
// Tabs werden über den Hook (actions_bericht.class.php → addMoreActionsButtons / completeTabsHead)
// dynamisch hinzugefügt, weil wir die Sichtbarkeit pro Element-Typ über Konstanten steuern wollen.
// Statisch geht aber auch — sicherer und einfacher:
$this->tabs = array(
'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',
'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__',
'shipping:+bericht:Bericht:bericht@bericht:$user->hasRight("bericht","read"):/custom/bericht/bericht_card.php?id=__ID__&element=shipment',
);
$this->dictionaries = array();
$this->boxes = array();
// Kein Cronjob — Cleanup expired Upload-Tokens passiert on-demand
// beim Anlegen eines neuen Tokens (siehe BerichtUploadToken::create()).
$this->cronjobs = array();
// Rechte — wie Stundenzettel: [4]=perms, [5]=subperms (leer)
$this->rights = array();
$r = 0;
$this->rights[$r][0] = $this->numero + $r;
$this->rights[$r][1] = 'Berichte lesen';
$this->rights[$r][3] = 1; // Standard aktiviert
$this->rights[$r][4] = 'read';
$this->rights[$r][5] = '';
$r++;
$this->rights[$r][0] = $this->numero + $r;
$this->rights[$r][1] = 'Berichte erstellen und bearbeiten';
$this->rights[$r][3] = 1;
$this->rights[$r][4] = 'write';
$this->rights[$r][5] = '';
$r++;
$this->rights[$r][0] = $this->numero + $r;
$this->rights[$r][1] = 'Berichte löschen';
$this->rights[$r][3] = 1;
$this->rights[$r][4] = 'delete';
$this->rights[$r][5] = '';
$r++;
$this->rights[$r][0] = $this->numero + $r;
$this->rights[$r][1] = 'Modul Bericht administrieren (Templates verwalten)';
$this->rights[$r][3] = 0;
$this->rights[$r][4] = 'admin';
$this->rights[$r][5] = '';
$r++;
// Kein Menü — Berichte werden über Tabs auf Rechnungen/Aufträgen/Angeboten aufgerufen
$this->menu = array();
}
/**
* Beim Aktivieren ausgeführt: SQL laden, Verzeichnisse anlegen,
* vorhandene Extrafields auf llx_facture_extrafields prüfen und ggf. anlegen.
*/
public function init($options = '')
{
global $conf, $langs;
// SQL-Tabellen anlegen
$result = $this->_load_tables('/bericht/sql/');
if ($result < 0) {
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'",
// Phase 5.5: Bericht-Vorlagen
"ALTER TABLE ".$this->db->prefix()."bericht ADD COLUMN is_template TINYINT(1) DEFAULT 0",
"ALTER TABLE ".$this->db->prefix()."bericht ADD COLUMN template_label VARCHAR(255) DEFAULT NULL",
// Phase C: Titel pro Seite (Zwischentitel)
"ALTER TABLE ".$this->db->prefix()."bericht_page ADD COLUMN title VARCHAR(255) DEFAULT NULL",
// Phase 5.3: Versionierung
"ALTER TABLE ".$this->db->prefix()."bericht ADD COLUMN version INT DEFAULT 1",
"ALTER TABLE ".$this->db->prefix()."bericht ADD COLUMN fk_bericht_parent INT DEFAULT NULL",
// Phase 6: Composite-PNG (Client-WYSIWYG). Wenn gesetzt, wird die Seite
// komplett aus diesem einen PNG gerendert statt aus source_path + fabric_json.
// Der Editor rendert sein Fabric-Canvas bei jedem Save zu einem PNG und
// lädt es hoch — damit ist PDF-Output identisch mit Editor-Anzeige.
"ALTER TABLE ".$this->db->prefix()."bericht_page ADD COLUMN composite_path VARCHAR(512) DEFAULT NULL",
// Foto-Upload entkoppeln: Token an Dolibarr-Objekt statt an Bericht binden
"ALTER TABLE ".$this->db->prefix()."bericht_upload_token ADD COLUMN fk_element INTEGER NOT NULL DEFAULT 0",
"ALTER TABLE ".$this->db->prefix()."bericht_upload_token ADD COLUMN element_type VARCHAR(32) NOT NULL DEFAULT 'order'",
"UPDATE ".$this->db->prefix()."bericht_upload_token SET fk_element = fk_bericht, element_type = 'order' WHERE fk_element = 0 AND fk_bericht > 0",
// Phase 1.8: Lieferschein-Bestaetigung (PWA) - Signatur-Box-Geometrie pro PDF-Template
"CREATE TABLE IF NOT EXISTS ".$this->db->prefix()."bericht_signature_box ("
."rowid INT AUTO_INCREMENT PRIMARY KEY,"
."entity INT NOT NULL DEFAULT 1,"
."template_name VARCHAR(64) NOT NULL,"
."page VARCHAR(8) NOT NULL DEFAULT 'last',"
."x_mm DECIMAL(7,2) NOT NULL,"
."y_mm DECIMAL(7,2) NOT NULL,"
."w_mm DECIMAL(7,2) NOT NULL,"
."h_mm DECIMAL(7,2) NOT NULL,"
."label VARCHAR(128) DEFAULT 'Unterschrift Kunde',"
."fk_user_modif INT DEFAULT NULL,"
."tms TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL,"
."UNIQUE KEY idx_bsb_template (entity, template_name)"
.") ENGINE=innodb",
// Phase 5.9: Materialliste pro Auftrag
"CREATE TABLE IF NOT EXISTS ".$this->db->prefix()."bericht_material ("
."rowid INT AUTO_INCREMENT PRIMARY KEY,"
."element_type VARCHAR(32) NOT NULL,"
."fk_element INT NOT NULL,"
."label VARCHAR(255) NOT NULL,"
."qty FLOAT DEFAULT 1,"
."unit VARCHAR(16) DEFAULT 'Stk',"
."note TEXT DEFAULT NULL,"
."fk_user_creat INT NOT NULL,"
."datec DATETIME NOT NULL,"
."INDEX idx_bm_element (element_type, fk_element)"
.") ENGINE=innodb",
);
foreach ($migrations as $sql) {
// Errors ignorieren — Spalten existieren ggf. schon
$this->db->query($sql, 1);
}
// Extrafields auf facture sicherstellen — vorhandene werden NICHT angefasst
require_once DOL_DOCUMENT_ROOT.'/core/class/extrafields.class.php';
$extrafields = new ExtraFields($this->db);
$fields = array(
'auftragsnummer' => array('label' => 'Auftragsnummer', 'type' => 'varchar', 'size' => 255, 'pos' => 100),
'angebotsnummer' => array('label' => 'Angebotsnummer', 'type' => 'varchar', 'size' => 255, 'pos' => 101),
'rechnungsnummer' => array('label' => 'Rechnungsnummer', 'type' => 'varchar', 'size' => 255, 'pos' => 102),
'beschreibung' => array('label' => 'Auftragsbeschreibung', 'type' => 'text', 'size' => 2000, 'pos' => 103),
'hinweis' => array('label' => 'Hinweis', 'type' => 'varchar', 'size' => 255, 'pos' => 104),
);
foreach ($fields as $name => $def) {
// Existiert das Feld bereits? → nicht überschreiben
$check = $this->db->query("SELECT rowid FROM ".$this->db->prefix()."extrafields"
." WHERE name = '".$this->db->escape($name)."'"
." AND elementtype = 'facture'");
if ($check && $this->db->num_rows($check) > 0) {
continue;
}
$extrafields->addExtraField(
$name,
$def['label'],
$def['type'],
$def['pos'],
$def['size'],
'facture',
0, 0, '', '', 1, '', 0, 0, '', '', 'bericht@bericht', '1'
);
}
$sql = array();
return $this->_init($sql, $options);
}
/**
* Beim Deaktivieren: Konstanten/Permissions entfernen, Daten und Extrafields BLEIBEN erhalten.
*/
public function remove($options = '')
{
$sql = array();
return $this->_remove($sql, $options);
}
}