diff --git a/admin/setup.php b/admin/setup.php
index e02a070..da17ad5 100644
--- a/admin/setup.php
+++ b/admin/setup.php
@@ -67,6 +67,10 @@ if ($action === 'save_const') {
'BERICHT_TAB_ON_PROPAL' => GETPOST('tab_propal', 'int') ? '1' : '0',
'BERICHT_BURN_ANNOTATIONS' => GETPOST('burn', 'int') ? '1' : '0',
'BERICHT_LIBREOFFICE_BIN' => GETPOST('lobin', 'alphanohtml'),
+ 'BERICHT_WHISPER_URL' => GETPOST('whisper_url', 'alphanohtml'),
+ 'BERICHT_WHISPER_MODE' => GETPOST('whisper_mode', 'alphanohtml'),
+ 'BERICHT_WHISPER_API_KEY' => GETPOST('whisper_key', 'alphanohtml'),
+ 'BERICHT_WHISPER_LANG' => GETPOST('whisper_lang', 'alphanohtml'),
);
foreach ($consts as $k => $v) {
dolibarr_set_const($db, $k, $v, 'chaine', 0, '', $conf->entity);
@@ -205,6 +209,25 @@ print '
| '.$langs->trans("BerichtSetupLibreOfficeBin").'
print '';
print ' |
';
+print 'đ Whisper-Transkription (Sprachnotizen â Text) |
';
+print '| Whisper-URL | ';
+print '';
+print ' Leer lassen um Transkription zu deaktivieren. Bei OpenAI: https://api.openai.com';
+print ' |
';
+print '| Modus | ';
+$mode_val = getDolGlobalString('BERICHT_WHISPER_MODE', 'whispercpp');
+print '';
+print ' |
';
+print '| API-Key (nur OpenAI) | ';
+print '';
+print ' |
';
+print '| Sprache | ';
+print '';
+print ' |
';
+
print '';
print '
';
print '';
diff --git a/ajax/save_as_template.php b/ajax/save_as_template.php
new file mode 100644
index 0000000..112e155
--- /dev/null
+++ b/ajax/save_as_template.php
@@ -0,0 +1,50 @@
+hasRight('bericht', 'write')) bericht_ajax_fail('Permission denied', 403);
+
+$berichtid = (int) ($_POST['berichtid'] ?? 0);
+$label = trim((string) ($_POST['label'] ?? ''));
+if (!$berichtid) bericht_ajax_fail('berichtid fehlt');
+if (empty($label)) bericht_ajax_fail('Label erforderlich');
+
+$src = new Bericht($db);
+if ($src->fetch($berichtid) <= 0) bericht_ajax_fail('Bericht nicht gefunden', 404);
+
+// Vorlage als neuer Bericht
+$tpl = new Bericht($db);
+$tpl->element_type = $src->element_type;
+$tpl->fk_element = 0;
+$tpl->titel = $src->titel;
+$tpl->auftragsnummer = '';
+$tpl->template_odt = $src->template_odt;
+$tpl->page_format = $src->page_format;
+$tpl->page_orientation = $src->page_orientation;
+$tpl->is_template = 1;
+$tpl->template_label = $label;
+if ($tpl->create($user) <= 0) bericht_ajax_fail('Erstellen fehlgeschlagen', 500);
+
+// Seiten kopieren (mit source_path-Referenz â die Vorlagen zeigen auf die Quell-Assets)
+$src_pages = BerichtPage::fetchAllForBericht($db, $src->id);
+foreach ($src_pages as $order => $p) {
+ $np = new BerichtPage($db);
+ $np->fk_bericht = $tpl->id;
+ $np->page_order = $order + 1;
+ $np->source_type = $p->source_type;
+ $np->source_path = $p->source_path;
+ $np->source_page = $p->source_page;
+ $np->note = $p->note;
+ $np->layout = $p->layout;
+ $np->image_scale = $p->image_scale;
+ $np->image_align = $p->image_align;
+ $np->create();
+}
+
+bericht_ajax_ok(array('template_id' => $tpl->id, 'label' => $label));
diff --git a/api/odt_templates.php b/api/odt_templates.php
new file mode 100644
index 0000000..45c7d43
--- /dev/null
+++ b/api/odt_templates.php
@@ -0,0 +1,16 @@
+ $fn,
+ 'label' => preg_replace('/\.odt$/i', '', $fn),
+ );
+}
+$default = getDolGlobalString('BERICHT_DEFAULT_TEMPLATE', '');
+api_ok(array('templates' => $out, 'default' => $default));
diff --git a/api/reports.php b/api/reports.php
index 3b22f42..9592884 100644
--- a/api/reports.php
+++ b/api/reports.php
@@ -57,6 +57,57 @@ if (!$id && $action === '') {
api_ok(array('reports' => $items, 'count' => count($items)));
}
+/* ----- POST: neuen Bericht anlegen ----- */
+if (!$id && $_SERVER['REQUEST_METHOD'] === 'POST' && $action === 'create') {
+ if (!$user->hasRight('bericht', 'write')) api_fail('Permission denied', 403);
+ $in = api_input();
+ $el_type = (string) ($in['element_type'] ?? 'order');
+ $el_id = (int) ($in['element_id'] ?? 0);
+ $titel = trim((string) ($in['titel'] ?? ''));
+ $format = (string) ($in['page_format'] ?? 'A4');
+ $orient = (string) ($in['page_orientation'] ?? 'P');
+ $template_id = (int) ($in['template_id'] ?? 0);
+ $template_odt = (string) ($in['template_odt'] ?? '');
+
+ if (!$el_id) api_fail('element_id erforderlich');
+ if (!in_array($el_type, array('order', 'invoice', 'propal'), true)) api_fail('element_type ungĂŒltig');
+
+ // Auftragsnummer ermitteln
+ $parent = bericht_fetch_parent($db, $el_type, $el_id);
+ if (!$parent) api_fail('Parent nicht gefunden', 404);
+ $auftragsnummer = '';
+ if ($el_type === 'invoice') {
+ $auftragsnummer = $parent->array_options['options_auftragsnummer'] ?? $parent->ref_client ?? $parent->ref;
+ } else {
+ $auftragsnummer = $parent->ref;
+ }
+
+ if ($template_id > 0) {
+ $new_id = Bericht::createFromTemplate($db, $user, $template_id, $el_type, $el_id, $auftragsnummer);
+ if (!$new_id) api_fail('Vorlage konnte nicht angewendet werden', 500);
+ // Nachbearbeiten falls abweichende Meta
+ $b = new Bericht($db);
+ $b->fetch($new_id);
+ if ($titel) $b->titel = $titel;
+ if (in_array($format, array('A4','A3','A5','Letter'), true)) $b->page_format = $format;
+ if (in_array($orient, array('P','L'), true)) $b->page_orientation = $orient;
+ if ($template_odt) $b->template_odt = $template_odt;
+ $b->update($user);
+ api_ok(array('bericht_id' => $new_id));
+ }
+
+ $b = new Bericht($db);
+ $b->element_type = $el_type;
+ $b->fk_element = $el_id;
+ $b->titel = $titel ?: ('Bericht '.$auftragsnummer);
+ $b->auftragsnummer = $auftragsnummer;
+ $b->template_odt = $template_odt ?: getDolGlobalString('BERICHT_DEFAULT_TEMPLATE', '');
+ $b->page_format = in_array($format, array('A4','A3','A5','Letter'), true) ? $format : 'A4';
+ $b->page_orientation = in_array($orient, array('P','L'), true) ? $orient : 'P';
+ if ($b->create($user) <= 0) api_fail('Anlegen fehlgeschlagen', 500);
+ api_ok(array('bericht_id' => $b->id));
+}
+
if (!$id) api_fail('id erforderlich');
$bericht = new Bericht($db);
diff --git a/api/templates.php b/api/templates.php
new file mode 100644
index 0000000..0a2157e
--- /dev/null
+++ b/api/templates.php
@@ -0,0 +1,61 @@
+id);
+ $items[] = array(
+ 'id' => (int) $t->id,
+ 'label' => $t->template_label ?: $t->titel ?: $t->ref,
+ 'titel' => $t->titel,
+ 'page_format' => $t->page_format,
+ 'page_orientation' => $t->page_orientation,
+ 'template_odt' => $t->template_odt,
+ 'page_count' => count($pages),
+ );
+ }
+ api_ok(array('templates' => $items, 'count' => count($items)));
+}
+
+if ($method === 'POST' && $action === 'create_from_template') {
+ if (!$user->hasRight('bericht', 'write')) api_fail('Permission denied', 403);
+ $in = api_input();
+ $tpl_id = (int) ($in['template_id'] ?? 0);
+ $el_type = (string) ($in['element_type'] ?? 'order');
+ $el_id = (int) ($in['element_id'] ?? 0);
+ $auftrag = (string) ($in['auftragsnummer'] ?? '');
+
+ if (!$tpl_id || !$el_id) api_fail('template_id + element_id erforderlich');
+ if (!in_array($el_type, array('order', 'invoice', 'propal'), true)) api_fail('element_type ungĂŒltig');
+
+ // Auftragsnummer auto-fĂŒllen wenn leer
+ if (empty($auftrag)) {
+ $parent = bericht_fetch_parent($db, $el_type, $el_id);
+ if ($parent) {
+ if ($el_type === 'invoice') {
+ $auftrag = $parent->array_options['options_auftragsnummer'] ?? $parent->ref_client ?? $parent->ref;
+ } else {
+ $auftrag = $parent->ref;
+ }
+ }
+ }
+
+ $new_id = Bericht::createFromTemplate($db, $user, $tpl_id, $el_type, $el_id, $auftrag);
+ if (!$new_id) api_fail('Erstellung fehlgeschlagen', 500);
+
+ api_ok(array('bericht_id' => $new_id));
+}
+
+api_fail('Methode/Action nicht unterstĂŒtzt', 405);
diff --git a/api/transcribe.php b/api/transcribe.php
new file mode 100644
index 0000000..3f5bbb1
--- /dev/null
+++ b/api/transcribe.php
@@ -0,0 +1,92 @@
+ rtrim($whisper_url, '/').'/v1/audio/transcriptions',
+ CURLOPT_POST => true,
+ CURLOPT_POSTFIELDS => array(
+ 'file' => $cfile,
+ 'model' => 'whisper-1',
+ 'language' => $language,
+ ),
+ CURLOPT_HTTPHEADER => $api_key ? array('Authorization: Bearer '.$api_key) : array(),
+ CURLOPT_RETURNTRANSFER => true,
+ CURLOPT_TIMEOUT => 120,
+ ));
+} else {
+ // whisper.cpp server /inference
+ curl_setopt_array($ch, array(
+ CURLOPT_URL => rtrim($whisper_url, '/').'/inference',
+ CURLOPT_POST => true,
+ CURLOPT_POSTFIELDS => array(
+ 'file' => $cfile,
+ 'language' => $language,
+ 'response_format' => 'json',
+ ),
+ CURLOPT_RETURNTRANSFER => true,
+ CURLOPT_TIMEOUT => 120,
+ ));
+}
+
+$resp = curl_exec($ch);
+$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+$err = curl_error($ch);
+curl_close($ch);
+
+if ($err) api_fail('Whisper-Fehler: '.$err, 502);
+if ($status !== 200) api_fail('Whisper HTTP '.$status.': '.substr($resp, 0, 200), 502);
+
+$data = json_decode($resp, true);
+$text = '';
+if (is_array($data)) {
+ $text = $data['text'] ?? ($data['transcription'] ?? '');
+} else {
+ $text = (string) $resp;
+}
+$text = trim($text);
+
+api_ok(array('text' => $text, 'language' => $language));
diff --git a/bericht_card.php b/bericht_card.php
index 1c51a00..d0d3da7 100644
--- a/bericht_card.php
+++ b/bericht_card.php
@@ -79,8 +79,17 @@ if ($action === 'unlink' && $berichtid > 0 && $user->hasRight('bericht', 'write'
exit;
}
-// Aktion: neuen Bericht anlegen
+// Aktion: neuen Bericht anlegen (ggf. aus Vorlage)
if ($action === 'create' && $user->hasRight('bericht', 'write')) {
+ $tpl_id = GETPOSTINT('template_id');
+ if ($tpl_id > 0) {
+ $new_id = Bericht::createFromTemplate($db, $user, $tpl_id, $element, $parent->id, $auftragsnummer);
+ if ($new_id) {
+ header("Location: ".$_SERVER['PHP_SELF'].'?berichtid='.$new_id);
+ exit;
+ }
+ setEventMessages('Vorlage konnte nicht angewendet werden', null, 'errors');
+ }
$b = new Bericht($db);
$b->element_type = $element;
$b->fk_element = $parent->id;
@@ -168,11 +177,20 @@ if (!$bericht) {
print '