All checks were successful
Deploy bericht / deploy (push) Successful in 6s
- 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>
344 lines
16 KiB
PHP
344 lines
16 KiB
PHP
<?php
|
|
/* Lieferschein-Endpoints fuer die PWA.
|
|
*
|
|
* GET /api/shipments.php?order_id=<id> - Liste der Lieferungen zu einem Auftrag
|
|
* GET /api/shipments.php?id=<id> - Detail einer Lieferung (mit Bericht-Info wenn vorhanden)
|
|
* GET /api/shipments.php?id=<id>&action=pdf - Lieferschein-PDF (Original oder unterschrieben)
|
|
* POST /api/shipments.php?id=<id>&action=confirm
|
|
* multipart: file=<PNG Signatur>, 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 <ref>-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 <ref>-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);
|