- Liste der Lieferungen zu einem Auftrag * GET /api/shipments.php?id= - Detail einer Lieferung (mit Bericht-Info wenn vorhanden) * GET /api/shipments.php?id=&action=pdf - Lieferschein-PDF (Original oder unterschrieben) * POST /api/shipments.php?id=&action=confirm * multipart: file=, signer_name, gps_lat?, gps_lon? * → stempelt PNG in das Lieferschein-PDF (Position aus llx_bericht_signature_box bzw. Default), * speichert Bericht (element_type='shipment'), setzt signed_status=1, * validiert+schliesst die Expedition wenn sie noch im Draft war. */ require_once __DIR__.'/_inc.php'; // Robust gegen unerwartete Fehler — sonst landet PHP-Fatal als leerer 500 ohne Hinweis set_error_handler(function ($errno, $errstr, $errfile, $errline) { if (!(error_reporting() & $errno)) return false; api_fail('PHP-Error: '.$errstr.' in '.basename($errfile).':'.$errline, 500); return true; }); set_exception_handler(function ($e) { api_fail('Exception: '.$e->getMessage().' in '.basename($e->getFile()).':'.$e->getLine(), 500); }); api_authenticate(); global $db, $user, $conf, $langs; require_once DOL_DOCUMENT_ROOT.'/expedition/class/expedition.class.php'; require_once DOL_DOCUMENT_ROOT.'/commande/class/commande.class.php'; require_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php'; $id = (int) ($_GET['id'] ?? 0); $order_id = (int) ($_GET['order_id'] ?? 0); $action = $_GET['action'] ?? ''; $method = $_SERVER['REQUEST_METHOD']; /* ----- LISTE der Lieferungen zu einem Auftrag ----- */ if ($order_id > 0 && !$id) { // llx_element_element: commande = source, shipping = target $sql = "SELECT e.rowid, e.ref, e.date_creation, e.date_delivery, e.date_expedition," ." e.fk_statut, e.signed_status, e.tracking_number, e.fk_soc," ." (SELECT COUNT(*) FROM ".$db->prefix()."bericht b WHERE b.element_type='shipment' AND b.fk_element=e.rowid) AS bericht_count" ." FROM ".$db->prefix()."expedition e" ." JOIN ".$db->prefix()."element_element ee ON ee.fk_target = e.rowid AND ee.targettype = 'shipping'" ." WHERE ee.fk_source = ".((int) $order_id)." AND ee.sourcetype = 'commande'" ." AND e.entity IN (".getEntity('expedition').")" ." ORDER BY e.date_creation DESC, e.rowid DESC"; $r = $db->query($sql); if (!$r) api_fail('DB-Fehler: '.$db->lasterror(), 500); $out = array(); while ($o = $db->fetch_object($r)) { $out[] = array( 'id' => (int) $o->rowid, 'ref' => $o->ref, 'date_creation' => $db->jdate($o->date_creation), 'date_delivery' => $db->jdate($o->date_delivery), 'date_expedition' => $db->jdate($o->date_expedition), 'status' => (int) $o->fk_statut, 'signed_status' => $o->signed_status !== null ? (int) $o->signed_status : null, 'tracking_number' => $o->tracking_number, 'soc_id' => (int) $o->fk_soc, 'bericht_count' => (int) $o->bericht_count, ); } api_ok(array('shipments' => $out, 'count' => count($out))); } if ($id <= 0) api_fail('id oder order_id fehlt'); // Lieferung + verknuepfter Auftrag laden $ctx = bericht_fetch_shipment_with_order($db, $id); if (!$ctx) api_fail('Lieferung nicht gefunden', 404); $shipment = $ctx['expedition']; $commande = $ctx['commande']; $kunde = $ctx['thirdparty']; /* ----- PDF-Stream ----- * Variante: ?variant=signed | unsigned | auto (default = auto: signed wenn signiert, sonst Original) */ if ($action === 'pdf' && $method === 'GET') { $variant = $_GET['variant'] ?? 'auto'; $shipment_signed = (int) ($shipment->signed_status ?? 0) === 1; $pdf_path = null; if ($variant === 'signed' || ($variant === 'auto' && $shipment_signed)) { // Suche explizit nach -signed.pdf $ref_sane = dol_sanitizeFileName($shipment->ref); $shipment_dir = $conf->expedition->multidir_output[$shipment->entity ?: $conf->entity].'/sending/'.$ref_sane; if (!is_dir($shipment_dir)) { $shipment_dir = $conf->expedition->multidir_output[$shipment->entity ?: $conf->entity].'/'.$ref_sane; } $candidate = $shipment_dir.'/'.$ref_sane.'-signed.pdf'; if (file_exists($candidate)) $pdf_path = $candidate; } if (!$pdf_path) { // Fallback / unsigned-Variante $pdf_path = bericht_get_shipment_pdf($db, $shipment, false); } if (!$pdf_path || !file_exists($pdf_path)) api_fail('Lieferschein-PDF nicht verfuegbar', 404); // Header zuruecksetzen — _inc.php hat application/json gesetzt header_remove('Content-Type'); header('Content-Type: application/pdf'); header('Content-Disposition: inline; filename="'.basename($pdf_path).'"'); header('Content-Length: '.filesize($pdf_path)); readfile($pdf_path); exit; } /* ----- DETAIL einer Lieferung ----- */ if ($method === 'GET' && !$action) { $bericht_id = 0; $bres = $db->query("SELECT rowid FROM ".$db->prefix()."bericht" ." WHERE element_type = 'shipment' AND fk_element = ".((int) $id) ." ORDER BY datec DESC LIMIT 1"); if ($bres && ($br = $db->fetch_object($bres))) $bericht_id = (int) $br->rowid; api_ok(array( 'shipment' => array( 'id' => (int) $shipment->id, 'ref' => $shipment->ref, 'date_creation' => $shipment->date_creation, 'date_delivery' => $shipment->date_delivery ?? null, 'date_expedition' => $shipment->date_expedition ?? null, 'status' => (int) ($shipment->statut ?? $shipment->fk_statut ?? 0), 'signed_status' => isset($shipment->signed_status) ? (int) $shipment->signed_status : null, 'tracking_number' => $shipment->tracking_number ?? null, ), 'order' => $commande ? array( 'id' => (int) $commande->id, 'ref' => $commande->ref, ) : null, 'customer' => array( 'id' => (int) ($kunde->id ?? 0), 'name' => $kunde->name ?? '', 'address' => $kunde->address ?? '', 'zip' => $kunde->zip ?? '', 'town' => $kunde->town ?? '', ), 'bericht_id' => $bericht_id, )); } /* ----- POST confirm: Unterschrift einstempeln ----- */ if ($action === 'confirm' && $method === 'POST') { if (!$user->hasRight('bericht', 'write')) api_fail('Schreibrechte fehlen', 403); if (empty($_FILES['file']['tmp_name'])) api_fail('file (PNG) fehlt'); $signer_name = trim((string) ($_POST['signer_name'] ?? '')); if ($signer_name === '') api_fail('signer_name fehlt'); $gps_lat = isset($_POST['gps_lat']) && $_POST['gps_lat'] !== '' ? (float) $_POST['gps_lat'] : null; $gps_lon = isset($_POST['gps_lon']) && $_POST['gps_lon'] !== '' ? (float) $_POST['gps_lon'] : null; // 1) Signatur-PNG zwischenspeichern (vor ODT-Render noetig) require_once __DIR__.'/../class/bericht.class.php'; $workdir = DOL_DATA_ROOT.'/bericht/work/shipment_'.((int) $shipment->id); if (!is_dir($workdir)) dol_mkdir($workdir); $sig_png = $workdir.'/signature_'.dol_print_date(dol_now(), '%Y%m%d_%H%M%S').'.png'; if (!move_uploaded_file($_FILES['file']['tmp_name'], $sig_png)) { api_fail('Signatur-Upload fehlgeschlagen', 500); } $signed_at = date('Y-m-d H:i:s'); $active_module = getDolGlobalString('EXPEDITION_ADDON_PDF', 'merou'); // Ziel-Pfad fuer signed-PDF (Expedition-ECM-Verzeichnis) $shipment_dir = $conf->expedition->multidir_output[$shipment->entity ?: $conf->entity].'/sending/'.dol_sanitizeFileName($shipment->ref); if (!is_dir($shipment_dir)) { $shipment_dir = $conf->expedition->multidir_output[$shipment->entity ?: $conf->entity].'/'.dol_sanitizeFileName($shipment->ref); } if (!is_dir($shipment_dir)) dol_mkdir($shipment_dir); $out_pdf = $shipment_dir.'/'.dol_sanitizeFileName($shipment->ref).'-signed.pdf'; $meta_render = array( 'signer_name' => $signer_name, 'signed_at' => $signed_at, 'gps_lat' => $gps_lat, 'gps_lon' => $gps_lon, 'shipment' => $shipment, 'order' => $commande, 'kunde' => $kunde, ); $template_name = $active_module; // fuer Audit-Meta $ok = false; if ($active_module === 'generic_shipment_odt') { // ODT-Pfad: Dolibarrs Standard-Pipeline nutzen (generateDocument), damit ALLE Substitutionen // (Kunde, Adresse, Lieferpositionen, Extrafelder, line-Iteration) sauber durchlaufen. // Unser actions_bericht::beforeODTSave-Hook stempelt die Unterschrift via setImage('signature'). // // Wichtig: generateDocument überschreibt IMMER das Standard-PDF am gleichen Pfad. // Damit das normale Lieferschein-PDF (ohne Unterschrift) erhalten bleibt, sichern wir // es vorher und stellen es nach der signed-Kopie wieder her. $template_name = 'generic_shipment_odt'; // 1) Original-PDF sichern (falls vorhanden) $original_pdf = bericht_get_shipment_pdf($db, $shipment, false); $backup_pdf = null; if ($original_pdf && file_exists($original_pdf) && !preg_match('/-signed\.pdf$/i', $original_pdf)) { $backup_pdf = $original_pdf.'.bericht-backup'; @copy($original_pdf, $backup_pdf); } // 2) generateDocument mit Hook (Signatur eingestempelt) $shipment->bericht_signature_png = $sig_png; $shipment->bericht_signature_meta = array( 'signer_name' => $signer_name, 'signed_at' => $signed_at, 'gps_lat' => $gps_lat, 'gps_lon' => $gps_lon, ); try { $rd = $shipment->generateDocument('generic_shipment_odt', $langs); if ($rd <= 0) api_fail('generateDocument fehlgeschlagen: '.($shipment->error ?: 'unbekannt'), 500); } catch (Throwable $e) { api_fail('Exception bei generateDocument: '.$e->getMessage(), 500); } // 3) Das frisch generierte (signed) PDF zu -signed.pdf kopieren // bericht_get_shipment_pdf mit include_signed=true findet die Standard-Datei // (die jetzt aber die signierte Version ist) $signed_in_place = bericht_get_shipment_pdf($db, $shipment, true); if (!$signed_in_place || !file_exists($signed_in_place)) { api_fail('Kein signiertes PDF nach generateDocument gefunden', 500); } if (!@copy($signed_in_place, $out_pdf)) { api_fail('Kopieren des signierten PDF fehlgeschlagen: '.$out_pdf, 500); } // 4) Original wieder herstellen — Hook-Property entfernen, dann nochmal generateDocument // Falls Backup vorhanden, kann der schnellere Weg verwendet werden. unset($shipment->bericht_signature_png); unset($shipment->bericht_signature_meta); if ($backup_pdf && file_exists($backup_pdf)) { @copy($backup_pdf, $signed_in_place); @unlink($backup_pdf); } else { // Es gab kein Original-Backup → neues unsigned-PDF regenerieren try { $shipment->generateDocument('generic_shipment_odt', $langs); } catch (Throwable $e) { dol_syslog('Restaurieren unsigned PDF fehlgeschlagen: '.$e->getMessage(), LOG_WARNING); } } $ok = true; } else { // PDF-Modul: Original-Lieferschein-PDF holen und Signatur per FPDI an konfigurierter Box-Position stempeln. $src_pdf = bericht_get_shipment_pdf($db, $shipment); if (!$src_pdf || !file_exists($src_pdf)) api_fail('Lieferschein-PDF konnte nicht erzeugt werden', 500); $box = bericht_get_signature_box($db, $template_name); $ok = bericht_stamp_signature_on_pdf($src_pdf, $sig_png, $box, array( 'signer_name' => $signer_name, 'signed_at' => $signed_at, 'gps_lat' => $gps_lat, 'gps_lon' => $gps_lon, 'shipment_ref' => $shipment->ref, ), $out_pdf); if (!$ok) api_fail('PDF-Stempel fehlgeschlagen', 500); } // 5) Bericht-Record (element_type='shipment') anlegen, damit wir die Signatur-Metadaten // in der gleichen Audit-Logik wie Berichts-Signaturen ablegen koennen $bericht = null; $existing = $db->query("SELECT rowid FROM ".$db->prefix()."bericht" ." WHERE element_type = 'shipment' AND fk_element = ".((int) $shipment->id) ." ORDER BY datec DESC LIMIT 1"); if ($existing && ($row = $db->fetch_object($existing))) { $bericht = new Bericht($db); $bericht->fetch((int) $row->rowid); } else { $bericht = new Bericht($db); $bericht->element_type = 'shipment'; $bericht->fk_element = (int) $shipment->id; $bericht->titel = 'Lieferschein '.$shipment->ref; $bericht->auftragsnummer = $commande ? $commande->ref : ''; $bericht->status = Bericht::STATUS_FINAL; $bericht->final_pdf_path = str_replace(DOL_DATA_ROOT.'/', '', $out_pdf); $bericht->create($user); } if ($bericht && $bericht->id) { $bericht->status = Bericht::STATUS_FINAL; $bericht->final_pdf_path = str_replace(DOL_DATA_ROOT.'/', '', $out_pdf); $bericht->update($user); } // Metadaten neben dem PNG ablegen (Audit-Spur) @file_put_contents($sig_png.'.meta.json', json_encode(array( 'shipment_id' => (int) $shipment->id, 'shipment_ref' => $shipment->ref, 'order_ref' => $commande ? $commande->ref : null, 'customer' => $kunde->name ?? '', 'signer_name' => $signer_name, 'signed_at' => $signed_at, 'signed_at_unix' => time(), 'user_id' => $user->id, 'user_login' => $user->login, 'gps_lat' => $gps_lat, 'gps_lon' => $gps_lon, 'template' => $template_name, 'render_mode' => ($active_module === 'generic_shipment_odt' ? 'odt' : 'pdf_stamp'), 'remote_ip' => $_SERVER['REMOTE_ADDR'] ?? '', 'signed_pdf' => str_replace(DOL_DATA_ROOT.'/', '', $out_pdf), ), JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); // 6) Expedition-Status: signed_status=1 + ggf. validieren+schliessen $status_changes = array(); if (method_exists($shipment, 'setSignedStatus')) { $shipment->setSignedStatus($user, 1, 0, 'BERICHT_SIGNED'); $status_changes[] = 'signed_status=1'; } else { // Fallback: direktes SQL falls Trait fehlt $db->query("UPDATE ".$db->prefix()."expedition SET signed_status = 1 WHERE rowid = ".((int) $shipment->id)); $status_changes[] = 'signed_status=1 (sql)'; } // Status-Workflow: Draft → Validated → Closed $current_status = (int) ($shipment->statut ?? $shipment->fk_statut ?? 0); if ($current_status === 0 || $current_status === Expedition::STATUS_DRAFT) { if (method_exists($shipment, 'valid')) { $rv = $shipment->valid($user); if ($rv > 0) $status_changes[] = 'validated'; } // erneut fetchen, damit setClosed konsistente Daten sieht $shipment->fetch((int) $shipment->id); } if (defined('Expedition::STATUS_CLOSED') && method_exists($shipment, 'setClosed')) { $rc = $shipment->setClosed(); if ($rc > 0) $status_changes[] = 'closed'; } elseif (method_exists($shipment, 'setClosed')) { $shipment->setClosed(); $status_changes[] = 'closed'; } api_ok(array( 'bericht_id' => (int) ($bericht->id ?? 0), 'signed_pdf' => str_replace(DOL_DATA_ROOT.'/', '', $out_pdf), 'signed_at' => $signed_at, 'status_changes' => $status_changes, )); } api_fail('Unbekannte Aktion', 400);