bericht/core/modules/modBericht.class.php
Eduard Wisch ca2b796b36
All checks were successful
Deploy bericht / deploy (push) Successful in 6s
Feature: Lieferschein-Unterschrift via ODT-Hook + PWA-Signatur-Workflow
- Neuer API-Endpoint api/shipments.php: Liste Lieferungen zu Auftrag, PDF-Stream, confirm (Unterschrift stempeln)
- ODT-Hook actions_bericht.class.php: ersetzt {signature} Platzhalter via odfphp->setImage, setzt {signer_name}/{signed_at}/{gps}
- Backup-Roundtrip: generateDocument-Backup → signed.pdf erzeugen → Original wiederherstellen
- JWT-Fallback in _jwt.php: ?jwt= Query-Param für <img>/<object> ohne Authorization-Header
- Admin: BERICHT_SIGNATURE_IMAGE_RATIO Feld, Toggle BERICHT_TAB_ON_SHIPMENT, Signature-Box-Editor
- DB: llx_bericht_signature_box für pro-Template mm-Box-Geometrie
- element_type='shipment' in modBericht + lib/bericht.lib.php
- element_element Richtung: commande=source, shipping=target (fk_target=expedition_id)
- DOL_DATA_ROOT-Auflösung für EXPEDITION_ADDON_PDF_ODT_PATH
- Sprachen: de_DE + en_US mit neuen Schlüsseln für Signatur-Workflow

[deploy]

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 06:48: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.1.0';
$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);
}
}