OpenXE/www/lib/versandarten/parcelone.php
2021-05-21 08:49:41 +02:00

2173 lines
70 KiB
PHP

<?php
if (!class_exists('Versanddienstleister')) {
require_once dirname(__DIR__) . '/class.versanddienstleister.php';
}
/**
* Request & Response Exceptions
*/
interface ParcelOneExceptionInterface
{
}
/**
* Class ParcelOneReplacer
*
* Register some placeholders (with their values) as array
* and replace each substring of {KEY}, {key}, {{KEY}} or {{key}} with
* its value.
*/
class ParcelOneReplacer
{
private $replacements = [];
/**
* DPDe_Placeholders constructor.
*
* @param array $replacements
*/
public function __construct($replacements)
{
$replacements = array_filter($replacements, static function($e){
return is_string($e) || is_numeric($e);
});
$replacements = array_filter($replacements, 'is_string', ARRAY_FILTER_USE_KEY);
$replacements = array_change_key_case($replacements, CASE_UPPER);
$this->replacements = $replacements;
}
/**
* Search and replace variables in string.
*
* Replaces all placeholders in upper or lower case within single or double curly brackets
* @param string $message
*
* E.G.: Would convert "hello {customer}." to "hello awesome people."
* if array to __construct was like ['customer' => 'awesome people']
*
* @return string
*/
public function handle($message)
{
if (!is_string($message)) {
$type = gettype($message);
throw new RuntimeException(sprintf(
'Expected message as string, got %s.', $type
));
}
if (empty($this->replacements)) {
return $message;
}
foreach ($this->replacements as $key => $value) {
$value = (string) $value;
// key with one bracket
$key = '{' . trim($key, '{}') . '}';
$message = str_replace([$key, strtolower($key)], [$value, $value], $message);
// key with two brackets
$key = '{' . $key . '}';
$message = str_replace([$key, strtolower($key)], [$value, $value], $message);
}
return $message;
}
}
/**
* This abstract class is designed to simplify a new 'delivery tool'
*
* Class AbstractVersandart
*/
abstract class AbstractVersandartParcelone extends Versanddienstleister
{
public $einstellungen = [];
public $export_drucker;
public $paketmarke_drucker;
/**
* Copied from other 'versandart modules', added
* the RuntimeException for json decode.
*
* AbstractVersandart constructor.
*
* @param app_t $app
* @param int $id
*/
final public function __construct($app, $id)
{
$this->id = $id;
$this->app = &$app;
$settings = $this->app->DB->Select("SELECT einstellungen_json FROM versandarten WHERE id = '$id' LIMIT 1");
$this->paketmarke_drucker = $this->app->DB->Select("SELECT paketmarke_drucker FROM versandarten WHERE id = '$id' LIMIT 1");
$this->export_drucker = $this->app->DB->Select("SELECT export_drucker FROM versandarten WHERE id = '$id' LIMIT 1");
if ($settings) {
$settings = json_decode($settings, true);
if (json_last_error()) {
throw new RuntimeException(sprintf(
'JSON decode failed in %s with %s.', get_class($this), json_last_error_msg()
));
}
} else {
$settings = [];
}
$this->einstellungen = $settings;
$this->ctrHook();
}
/**
* Just a hook run if the ctr had his job done.
*
* @return void
*/
abstract protected function ctrHook();
/**
*
* Called via 'versandzentrum'
* Thanks to 'sevensenders.php'
*
* @param int|string $id
* @param $sid
*
* @return array
*/
public function PaketmarkeDrucken($id, $sid)
{
$adressdaten = $this->GetAdressdaten($id, $sid);
$ret = $this->Paketmarke($sid, $id, '', false, $adressdaten);
if ($sid !== 'lieferschein') {
return $ret;
}
$deliveryNoteArr = $this->app->DB->SelectRow("SELECT adresse, versandart, projekt FROM lieferschein WHERE id = '$id' LIMIT 1");
$adresse = $deliveryNoteArr['adresse'];
$project = $deliveryNoteArr['projekt'];
$versandart = $deliveryNoteArr['versandart'];
$addressValidation = 2;
if ($ret) {
$addressValidation = 1;
}
$tracking = null;
// $tracking = $this->tracking; // $this->tracking is not set in 'sevensenders.php'
if (isset($adressdaten['tracking'])) {
$tracking = $adressdaten['tracking'];
}
$this->app->DB->Insert("INSERT INTO versand (versandunternehmen, tracking,
versendet_am,abgeschlossen,lieferschein,freigegeben,firma,adresse,projekt,paketmarkegedruckt,adressvalidation)
VALUES ('$versandart','$tracking',NOW(),1,'$id',1,'1','$adresse','$project',1,'$addressValidation') ");
if ($addressValidation === 1) {
$this->app->erp->LieferscheinProtokoll($id, 'Paketmarke automatisch gedruckt');
} elseif ($addressValidation === 2) {
$this->app->erp->LieferscheinProtokoll($id, 'automatisches Paketmarke Drucken fehlgeschlagen');
}
return $ret;
}
/**
* This method is the 'main' part of all delivery tools.
*
* Available via Lager -> Lieferschein
* index.php?module=lieferschein&action=paketmarke&id={id}
* or Lager -> Versandzentrum
* index.php?module=versanderzeugen&action=frankieren&id={id}
* or Lager -> Retoure
* index.php?module=retoure&action=paketmarke&id={id}
*
* called via erpapi->Paketmarke($parsetarget,$sid="",$zusatz="",$typ="DHL") as:
* $error = $obj->Paketmarke($sid!=''?$sid:'lieferschein',($sid=='versand'?$id:$tid), $parsetarget, $error);
*
* Reads the address data and package data via '$this->app->Secure->GetPOST' or from '$adressdaten' array.
*
* @param string $doctyp 'lieferschein' / 'versand' / 'retoure'
* @param string|int $id '1'
* @param string $target '#TAB1'
* @param bool $error
* @param null|array $adressdaten
*
* @return array
*/
final public function Paketmarke($doctyp, $id, $target = '', $error = false, &$adressdaten = null)
{
if (is_string($id) && is_numeric($id)) {
$id = (int)$id;
}
if (!is_int($id)) {
$type = gettype($id);
throw new ArgumentTypeException('Expected id as integer, got ' . $type);
}
if (!is_string($doctyp)) {
$type = gettype($doctyp);
throw new ArgumentTypeException('Expected doctyp as string, got ' . $type);
}
$allowedTypes = ['lieferschein', 'versand', 'retoure'];
if ($adressdaten === null) {
$doctyp = $this->getModuleName($doctyp, $allowedTypes);
}
if (!in_array($doctyp, $allowedTypes, true)) {
throw new RuntimeException('Only \'Lieferschein\' is supported, got ' . $doctyp);
}
$this->validateSettings();
if (is_array($adressdaten) && !empty($adressdaten)) {
$address = [
'name' => $adressdaten['name'],
'name2' => $adressdaten['name2'],
'name3' => $adressdaten['name3'],
'street' => $adressdaten['strassekomplett'],
'street_no' => $adressdaten['hausnummer'],
'plz' => $adressdaten['plz'],
'ort' => $adressdaten['ort'],
'email' => $adressdaten['email'],
'phone' => $adressdaten['phone'],
'land' => $adressdaten['land'],
// bundesstaat
];
// $anzahl = (int)isset($adressdaten["anzahl"])?$adressdaten["anzahl"]:0;
// $nummeraufbeleg = "";//$this->app->Secure->GetPOST("nummeraufbeleg");
// if ($anzahl <= 0 || !is_int($anzahl)) $anzahl = 1;
// $laenge = isset($adressdaten["laenge"])?$adressdaten["laenge"]:'';
// $breite = isset($adressdaten["breite"])?$adressdaten["breite"]:'';
// $hoehe = isset($adressdaten["hoehe"])?$adressdaten["hoehe"]:'';
$cash_on_delivery = isset($adressdaten['Nachnahme']) ? $adressdaten['Nachnahme'] : 0;
$packageData = [
'kg1' => $adressdaten['standardkg'],
'drucken' => '1',
'anders' => '',
'tracking_again' => '',
'module' => $doctyp,
'versandmit' => '', // $this->app->Secure->GetPOST('versandmit'),
'trackingsubmit' => '', // $this->app->Secure->GetPOST('trackingsubmit'),
'versandmitbutton' => '', // $this->app->Secure->GetPOST('versandmitbutton'),
'tracking' => '', // $this->app->Secure->GetPOST('tracking'),
'trackingsubmitcancel' => '', // $this->app->Secure->GetPOST('trackingsubmitcancel'),
'retourenlabel' => '', // $this->app->Secure->GetPOST('retourenlabel'),
'nachnahme' => (int)$cash_on_delivery,
'product' => '',
];
} else {
$address = [
'name' => $this->app->Secure->GetPOST('name'),
'name2' => $this->app->Secure->GetPOST('name2'),
'name3' => $this->app->Secure->GetPOST('name3'),
'street' => $this->app->Secure->GetPOST('strasse'),
'street_no' => $this->app->Secure->GetPOST('hausnummer'),
'plz' => $this->app->Secure->GetPOST('plz'),
'ort' => $this->app->Secure->GetPOST('ort'),
'email' => $this->app->Secure->GetPOST('email'),
'phone' => $this->app->Secure->GetPOST('phone'),
'land' => $this->app->Secure->GetPOST('land'),
];
$packageData = [
'kg1' => $this->app->Secure->GetPOST('kg1'),
'drucken' => $this->app->Secure->GetPOST('drucken'),
'anders' => $this->app->Secure->GetPOST('anders'),
'tracking_again' => $this->app->Secure->GetGET('tracking_again'),
'module' => $this->app->Secure->GetGET('module'),
'versandmit' => $this->app->Secure->GetPOST('versandmit'),
'trackingsubmit' => $this->app->Secure->GetPOST('trackingsubmit'),
'versandmitbutton' => $this->app->Secure->GetPOST('versandmitbutton'),
'tracking' => $this->app->Secure->GetPOST('tracking'),
'trackingsubmitcancel' => $this->app->Secure->GetPOST('trackingsubmitcancel'),
'retourenlabel' => $this->app->Secure->GetPOST('retourenlabel'),
// 'product' => $this->app->Secure->GetPOST('products'),
'nachnahme' => $this->app->Secure->GetPOST('nachnahme'),
];
}
// $packageData['clients_reference'] = $this->app->Secure->GetPOST('clients_reference');
// $packageData['shipment_reference'] = $this->app->Secure->GetPOST('shipment_reference');
$packageData['service'] = $this->app->Secure->GetPOST('service');
/*
* This is in 'sevensenders.php' only called, if $addressdata === null. But why?
*/
$this->setNachnahmeCheckbox($doctyp, $id);
$ret = [];
if (!empty($address)) {
try {
// throw new RuntimeException('Ooops - data missing');
$this->createPaketmarke($doctyp, $id, $target, $error, $address, $packageData);
} catch (Exception $e) {
$ret[] = $e->getMessage();
}
}
if ($target) {
$this->parseTemplate($target);
}
return $ret;
}
/**
* Get the module name.
*
* @param string $doctype
* @param array $allowedTypes
*
* @return string
*/
private function getModuleName($doctype, $allowedTypes)
{
// // in sevensenders:
// if($adressdaten === null){
// $module = $this->app->Secure->GetGET('module');
// }else{
// $module = $doctyp;
// }
$tmp = $this->app->Secure->GetGET('module');
if (is_array($tmp)) {
$tmp = array_filter($tmp);
$tmp = array_filter($tmp, 'is_string');
if (array_key_exists(0, $tmp)) {
$tmp = $tmp[0];
} else {
$tmp = '';
}
}
if (in_array($tmp, $allowedTypes, true)) {
return (string)$tmp;
}
return $doctype;
}
/**
* Check if all settings are set and not empty.
* Uses $this->settingsStructure() to get all
* required settings. Empty settings are not allowed!
* Respects select options (drop-down menus). Uses
* the shown labels in the exceptions.
*
* @throws RuntimeException
*/
protected function validateSettings()
{
$settings = $this->EinstellungenStruktur();
foreach ($settings as $key => $setting) {
$name = rtrim($setting['bezeichnung'], ':');
if (!array_key_exists($key, $this->einstellungen)) {
if (array_key_exists('optional', $setting) && $setting['optional'] === true) {
continue;
}
if (array_key_exists('typ', $setting) && strtolower((string)$setting['typ']) === 'checkbox') {
continue;
}
if (array_key_exists('default', $setting)) {
$value = $setting['default'];
// if default value is empty, don't run other validations.
$this->einstellungen[$key] = $value;
if (empty($value)) {
continue;
}
}
}
if (!array_key_exists($key, $this->einstellungen)) {
throw new RuntimeException(sprintf(
'Setting \'%s\' is missing.', $name
));
}
$value = $this->einstellungen[$key];
/*
* Check 'size' argument for maximum length.
*/
if (array_key_exists('maxLength', $setting) && is_numeric($setting['maxLength'])) {
$maxLength = (int) $setting['maxLength'];
if ($maxLength > 0 && strlen($value) > $maxLength) {
throw new RuntimeException(sprintf(
'Setting \'%s\' raised it\'s maximum length of %s.', $name, $maxLength
));
}
}
/*
* Respect select fields.
*/
if (array_key_exists('optionen', $setting) && $setting['type'] === 'select') {
$options = $setting['optionen'];
$keys = array_keys($options);
if (!in_array($value, $keys, true)) {
$values = array_values($options);
$options = implode(', ', $values);
throw new RuntimeException(sprintf(
'Setting \'%s\' is not allowed, allowed values are: %s.', $name, $options
));
}
continue;
}
if (array_key_exists('regex', $setting) && is_string($setting['regex'])) {
$pattern = $setting['regex'];
$pattern = trim($pattern, '/');
$pattern = ltrim($pattern, '^');
$pattern = rtrim($pattern, '$');
$pattern = '/^' . $pattern . '$/';
$match = preg_match($pattern, $value);
if ($match === false) {
throw new RuntimeException(sprintf(
'Invalid regex pattern for \'%s\'.', $name
));
}
if ($match !== 1) {
throw new RuntimeException(sprintf(
'Setting \'%s\' does not match it\'s regex pattern.', $name
));
}
}
}
}
/**
* Print the created file.
*
* @param string $filename
* @param string $content
* @param int $versandId
*
* @throws RuntimeException
*/
protected function printFile($filename, $content, $versandId)
{
$printer = $this->getPrinter();
if (!$printer) {
throw new RuntimeException('No printer configured.');
}
if (!is_string($filename)) {
$type = gettype($filename);
throw new RuntimeException(sprintf(
'Expected filename as string, got %s.', $type
));
}
if (empty($filename)) {
throw new RuntimeException(
'Empty filename is not supported.'
);
}
if (!is_string($content)) {
$type = gettype($content);
throw new RuntimeException(sprintf(
'Expected content as string, got %s.', $type
));
}
if (empty($content)) {
throw new RuntimeException(sprintf(
'Empty content for document %s is not supported.', $filename
));
}
$tmpPath = $this->app->erp->GetTMP();
$full = $tmpPath . $filename;
if (!file_put_contents($full, $content)) {
throw new RuntimeException(sprintf(
'Could not write file \'%s\'.', $filename
));
}
$spoolerId = $this->app->printer->Drucken($printer, $full);
unlink($full);
if($versandId && $spoolerId) {
$this->app->DB->Update(
sprintf(
'UPDATE versand SET lastspooler_id = %d, lastprinter = %d WHERE id = %d',
$spoolerId, $printer, $versandId
)
);
}
}
/**
* Return the html structure displayed in the
* 'versandarten' module to add some extra
* input fields for api keys etc.
*
* Displayed via class 'Versanddienstleister' located
* in ../class.versanddienstleister.php
*
* @return array
*/
abstract protected function EinstellungenStruktur();
/**
* Set the 'nachnahme' checkbox field.
*
* Thanks to 'sevensenders.php'
*
* @param string $doctyp
* @param int|string $id
*/
private function setNachnahmeCheckbox($doctyp, $id)
{
if ($doctyp === 'lieferschein') {
$lieferschein = $id;
} elseif ($doctyp === 'retoure') {
$lieferschein = $this->app->DB->Select("SELECT lieferschein FROM retoure WHERE id='$id' LIMIT 1");
} else {
$lieferschein = $this->app->DB->Select("SELECT lieferschein FROM versand WHERE id='$id' LIMIT 1");
if ($lieferschein <= 0) {
$lieferschein = $id;
}
}
$rechnung = $this->app->DB->Select("SELECT id FROM rechnung WHERE lieferschein='$lieferschein' LIMIT 1");
$zahlungsweise = $this->app->DB->Select("SELECT zahlungsweise FROM rechnung WHERE id='$rechnung' LIMIT 1");
if ($zahlungsweise === 'nachnahme') {
$this->app->Tpl->Set('NACHNAHME', 'checked="checked"');
}
}
/**
* Create the package labels.
*
* Called via Paketmarke or PaketmarkeDrucken
*
* @param string $doctyp
* @param string $id
* @param string $target
* @param bool $error
* @param array $adressdaten
* @param array $packageData
*
* @return array list of error messages
*/
abstract protected function createPaketmarke($doctyp, $id, $target, $error, $adressdaten, $packageData);
/**
* Parse the template.
*
* @param string $target
*
* @return void
*/
abstract protected function parseTemplate($target);
/**
* May change the tracking id before inserting into the db.
*
* @param string $tracking
*
* @return string
*/
public function TrackingReplace($tracking)
{
return $tracking;
}
/**
* Extract the child's name and create
* a default module name via 'ucfirst'.
*
* E.G. converts child's class name
* 'Versandart_parcelone' to 'Parcelone'.
*
* @return string
*/
public function GetBezeichnung()
{
$c = get_class($this);
$c = explode('_', $c);
$c = array_filter($c);
$c = array_pop($c);
$c = ucfirst($c);
return $c;
}
/**
* Load the data given by the current document type and it's id.
*
* @param string $documentTyp on of 'lieferschein', 'versand' or 'retoure'
* @param int $id
*
* @throws RuntimeException
*
* @return array
*/
protected function getDocumentByID($documentTyp, $id)
{
/*
* var $documentTyp may be:
* - 'lieferschein'
* - 'versand'
* - 'retoure'
*
* - 'auftrag'
*/
$documentTyp = $this->app->DB->real_escape_string($documentTyp);
$sql = sprintf('SELECT * FROM %s WHERE id = %s LIMIT 1', $documentTyp, $id);
$data = $this->app->DB->SelectArr($sql);
$error = $this->app->DB->error();
if ($error) {
$error = htmlspecialchars($error);
$msg = sprintf('SQL SelectArr error: \'%s\', query was \'%s\'', $error, $sql);
throw new RuntimeException($msg);
}
if (!is_array($data) || !array_key_exists(0, $data) || !is_array($data[0]) || !$data[0]) {
throw new RuntimeException(sprintf(
'No data for document %s with id %s found.', $documentTyp, $id
));
}
$data = $data[0];
return $data;
}
/**
* Return the 'standard Paketmarkendrucker'.
*
* Checks GetPOST: drucken
* Checks GetGET: tracking_again
*
* @return int
*/
protected function getPrinter()
{
if (is_numeric($this->paketmarke_drucker) && $this->paketmarke_drucker) {
return $this->paketmarke_drucker;
}
$printer = (int) $this->app->erp->GetStandardPaketmarkendrucker();
if ($printer) {
return $printer;
}
return $this->export_drucker;
}
/**
* Get the current user name as escaped string.
*
* @return string
*/
protected function getUserName()
{
$user = $this->app->User->GetName();
$user = $this->app->DB->real_escape_string($user);
return $user;
}
/**
* Load an address via it's id.
*
* @param string|int $id
* @param string|array $fields Filter these columns, default all
*
* @throws RuntimeException
*
* @return array
*/
protected function loadAddress($id, $fields = '*')
{
if (is_string($id) && is_numeric($id)) {
$id = (int)$id;
}
if (!is_int($id)) {
$type = gettype($id);
throw new ArgumentTypeException('Expected addressID as int, got ' . $type);
}
$fields = (array)$fields;
$fields = array_values($fields);
$fields = array_filter($fields);
$fields = array_filter($fields, 'is_string');
if (in_array('*', $fields, true)) {
$fields = '*';
} else {
$tmp = [];
foreach ($fields as $field) {
$tmp[] = $this->app->DB->real_escape_string($field);
}
$fields = implode(', ', $tmp);
}
$sql = sprintf('SELECT %s FROM adresse WHERE id =\'%s\'', $fields, $id);
$query = $this->app->DB->Query($sql);
$address = $query->fetch_array(MYSQLI_ASSOC);
$error = $this->app->DB->error();
if ($error) {
$error = htmlspecialchars($error);
$msg = sprintf('SQL query error: \'%s\', query was \'%s\'', $error, $sql);
throw new RuntimeException($msg);
}
if (!is_array($address)) {
$type = gettype($address);
throw new ArgumentTypeException('Expected address as array, got ' . $type);
}
return $address;
}
/**
* Get the package weight from user input.
*
* @param array $packageData
*
* @throws RuntimeException
*
* @return float
*/
protected function getWeight($packageData)
{
if (!is_array($packageData)) {
$type = gettype($packageData);
throw new ArgumentTypeException('Expected package data as array, got ' . $type);
}
if (!array_key_exists('kg1', $packageData)) {
throw new RuntimeException('Weight (kg1) is missing.');
}
$weight = $packageData['kg1'];
if (empty($weight) && $weight !== '0') {
// wtf?! why is '0' string empty?
throw new RuntimeException('The package weight is required.');
}
if (is_string($weight)) {
$weight = str_replace(',', '.', $weight);
if (is_numeric($weight)) {
$weight = (float)$weight;
}
}
if (!is_float($weight)) {
$type = gettype($weight);
throw new ArgumentTypeException('Expected weight as float, got ' . $type);
}
if ($weight < 0) {
throw new RuntimeException('A negative package weight is not supported.');
}
return $weight;
}
/**
* Build an html select element.
*
* @param string $nameID Name and ID of select element.
* @param array $select associative array
* @param string|int|null|array $selected
*
* @return string
*/
protected function buildSelectForm($nameID, $select, $selected = null)
{
$options = [];
$nameID = htmlspecialchars($nameID);
$options[] = sprintf('<select id="%s" name="%s" style="width:23em">', $nameID, $nameID);
foreach ($select as $key => $value) {
$key = htmlspecialchars($key);
$value = htmlspecialchars($value);
$mark = $selected === $key
? 'selected="selected"'
: '';
$options[] = sprintf('<option value="%s" %s>%s</option>', $key, $mark, $value);
}
$options[] = '</select>';
return implode('', $options);
}
}
class ArgumentTypeException extends RuntimeException implements ParcelOneExceptionInterface
{
}
class ResponseException extends RuntimeException implements ParcelOneExceptionInterface
{
}
class NotImplementedException extends RuntimeException implements ParcelOneExceptionInterface
{
}
class EmptyResponseException extends ResponseException implements ParcelOneExceptionInterface
{
}
class MissingArgumentException extends RuntimeException implements ParcelOneExceptionInterface
{
}
class MissingResponseFieldException extends ResponseException implements ParcelOneExceptionInterface
{
/**
* MissingResponseFieldException constructor.
*
* Change the exception message -> wrap it, so its enough to pass only the missed argument name.
*
* @param string $message
* @param int $code
* @param Throwable|null $previous
*
* @return static
*/
final public static function fromFieldName($message, $code = 0, Throwable $previous = null)
{
if (!$message || !is_string($message)) {
$message = 'A field is missing.';
}
$message = sprintf('The field %s is missing.', $message);
return new static($message, $code, $previous);
}
}
class SoapExtensionMissingException extends RuntimeException implements ParcelOneExceptionInterface
{
}
if (!class_exists('SoapHeader')) {
/**
* This is just a fix, if php comes without the soap extension.
*
* So the following header classes can extend this class without raising an unhandled exception just by loading this
* file. A check if the soap extension exists is located in Versandart_parcelone::EinstellungenStruktur() so the
* user cannot setup this delivery service. In addition, a check is located in
* AbstractParcelOneRequest::__construct() so it's not possible to execute a soap request if the extension is
* missing.
*
* Class SoapHeader
*/
class SoapHeader
{
public function __construct($namespace, $name, $data = null, $mustunderstand = false, $actor = '')
{
}
}
}
/**
* Define some constants if the soap extension is not available so at least the class instantiation runs as expected.
*
* @url: https://www.php.net/manual/en/soap.constants.php
*/
defined('SOAP_1_1') or define('SOAP_1_1', 1);
defined('WSDL_CACHE_NONE') or define('WSDL_CACHE_NONE', 0);
defined('SOAP_COMPRESSION_GZIP') or define('SOAP_COMPRESSION_GZIP', 0);
defined('SOAP_COMPRESSION_ACCEPT') or define('SOAP_COMPRESSION_ACCEPT', 32);
/**
* Headers from documentation package.
*
* classes:
* - AuthHeader
* - APIKeyHeader
* - CultureHeader
*
* Thanks to Jörk Sternsdorff from
* Awiwe Solutions GmbH
*/
class AuthHeader extends SoapHeader
{
private $wss_ns = 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd';
private $wsu_ns = 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd';
/**
* AuthHeader constructor.
*
* @param string $user
* @param string $pass
*/
public function __construct($user, $pass)
{
$created = gmdate('Y-m-d\TH:i:s\Z');
$nonce = mt_rand();
$passdigest = base64_encode(pack('H*', sha1(pack('H*', $nonce) . pack('a*', $created) . pack('a*', $pass))));
$auth = new stdClass();
$auth->Username = new SoapVar($user, XSD_STRING, NULL, $this->wss_ns, NULL, $this->wss_ns);
$auth->Password = new SoapVar($pass, XSD_STRING, NULL, $this->wss_ns, NULL, $this->wss_ns);
$auth->Nonce = new SoapVar($passdigest, XSD_STRING, NULL, $this->wss_ns, NULL, $this->wss_ns);
$auth->Created = new SoapVar($created, XSD_STRING, NULL, $this->wss_ns, NULL, $this->wsu_ns);
$username_token = new stdClass();
$username_token->UsernameToken = new SoapVar($auth, SOAP_ENC_OBJECT, NULL, $this->wss_ns, 'UsernameToken', $this->wss_ns);
$security_sv = new SoapVar(
new SoapVar($username_token, SOAP_ENC_OBJECT, NULL, $this->wss_ns, 'UsernameToken', $this->wss_ns),
SOAP_ENC_OBJECT, NULL, $this->wss_ns, 'Security', $this->wss_ns);
parent::__construct($this->wss_ns, 'Security', $security_sv, true);
}
}
class APIKeyHeader extends SoapHeader
{
public function __construct($apiKey)
{
parent::__construct('apikey', 'apikey', $apiKey, false);
}
}
class CultureHeader extends SoapHeader
{
public function __construct($culture)
{
parent::__construct('culture', 'culture', $culture, false);
}
}
/**
* Class AbstractParcelOneRequest
*
* This class holds only some functionality to
* build up the soap request. It supports some
* methods to set required header.
* Nothing more.
*
* @package ParcelOne
*/
abstract class AbstractParcelOneRequest
{
/*
* Support only PA1 as carrier.
*/
const DEFAULT_CARRIER = 'PA1';
/**
* this Key is used to identify this software and not the customer.
*/
const API_KEY = '0641C551-D23A-43BB-87A9-626DAE7FFE00';
/**
* The default software attribute is injected into every request.
* It's possible to 'override' this constant in child classes.
*/
const SOFTWARE = 'Xentral_ERP_Software';
const SERVICE = 'https://productionapi.awiwe.solutions/version4/shippingwcf/ShippingWCF.svc?wsdl';
const SANDBOX_SERVICE = 'https://sandboxapi.awiwe.solutions/version4/shippingwcfsandbox/shippingWCF.svc?wsdl';
const ENDPOINT = 'https://productionapi.awiwe.solutions/version4/shippingwcf/ShippingWCF.svc/Shippingwcf';
const SANDBOX_ENDPOINT = 'https://sandboxapi.awiwe.solutions/version4/shippingwcfsandbox/shippingWCF.svc/ShippingWCF';
/**
* @var AbstractParcelOneRequest|ParcelOneRequest
*/
static private $instance = null;
/**
* The Mandator ID
*
* ID at PARCEL.ONE, usually '1', if only one mandator.
*
* @var int 1
*/
protected $mandator = 1;
/**
* The Consigner ID
*
* ID at PARCEL.ONE, usually "1", if only one consigner.
*
* @var int
*/
protected $consigner = 1;
/**
* The carrier selected as second step in the settings.
*
* @var string default PA1 for Parcel.One
*/
protected $carrier = 'PA1';
/**
* @var string The product chosen in the settings.
*/
protected $product = '';
/**
* Options for the SoapClient constructor.
*
* Note: this property is private. Also child classes have to use
* the setOption / deleteOption methods.
*
* @var array
*/
private $options = [
'soap_version' => SOAP_1_1,
'exceptions' => true,
'trace' => false,
'cache_wsdl' => WSDL_CACHE_NONE,
'compression' => SOAP_COMPRESSION_ACCEPT | SOAP_COMPRESSION_GZIP,
'location' => 'https://shippingwcf.awiwe.net/Shippingwcf.svc/Shippingwcf',
'connection_timeout' => 300
];
/**
* @var array of RequestHeaderInterface
*/
private $soapHeaders = [];
/**
* @var string
*/
private $url;
/**
* Cache here the created soap client.
*
* @var SoapClient
*/
private $client = null;
/**
* ParcelOneRequest constructor.
*
* @param bool $production
* @param int $mandator
* @param int $consigner
*/
final private function __construct($production = false, $mandator = 1, $consigner = 1)
{
if (!class_exists('SoapClient')) {
throw new SoapExtensionMissingException('SOAP support is not configured.');
}
$this->mandator = $mandator;
$this->consigner = $consigner;
$this->url = self::SANDBOX_SERVICE;
$this->options['location'] = self::SANDBOX_ENDPOINT;
if ($production) {
$this->url = self::SERVICE;
$this->options['location'] = self::ENDPOINT;
}
}
/**
* Get the singleton.
*
* @param array $settings
* @return AbstractParcelOneRequest|ParcelOneRequest
*/
final public static function getInstance($settings = [])
{
if (self::$instance !== null) {
if (empty(self::$instance->carrier) && array_key_exists('carrier', $settings)) {
self::$instance->carrier = $settings['carrier'];
}
if (empty(self::$instance->product) && array_key_exists('product', $settings)) {
self::$instance->product = $settings['product'];
}
return self::$instance;
}
$required = ['kdnr', 'password', 'sandbox'];
foreach ($required as $requirenment) {
if (!array_key_exists($requirenment, $settings)) {
throw new MissingArgumentException(sprintf(
'Setting %s is missing.', $requirenment
));
}
}
$required = array_flip($required);
$settings = array_intersect_key($settings, $required);
$production = true;
if (array_key_exists('sandbox', $settings)) {
$production = $settings['sandbox'] !== '1';
}
if (!array_key_exists('mandator', $settings)) {
$settings['mandator'] = 1;
}
$mandator = (int)$settings['mandator'];
$mandator = max(1, $mandator);
if (!array_key_exists('consigner', $settings)) {
$settings['consigner'] = 1;
}
$consigner = (int)$settings['consigner'];
$consigner = max(1, $consigner);
if (!array_key_exists('country', $settings)) {
$settings['country'] = 'de-DE';
}
$instance = new static($production, $mandator, $consigner);
$instance
->addSoapHeader(new AuthHeader($settings['kdnr'], $settings['password']))
->addSoapHeader(new CultureHeader($settings['country']))
->addSoapHeader(new APIKeyHeader(self::API_KEY));
self::$instance = $instance;
return $instance;
}
/**
* Don't allow 'on the fly' calls.
*
* Only the implemented SOAP methods are supported.
*
* @param string $name
* @param array $arguments
*
* @throws NotImplementedException
*
* @return mixed
*/
final public function __call($name, $arguments)
{
$class = get_class($this);
$message = sprintf(
'Method %s->%s is not implemented.', $class, $name
);
throw new NotImplementedException($message);
}
/**
* Make one call to the PARCEL.ONE API
*
* @param string $name
* @param array $arguments
*
* @throws SoapFault
*
* @throws EmptyResponseException
* @return stdClass|mixed
*/
final protected function call($name, $arguments = null)
{
$client = $this->getClient();
if (!is_string($name)) {
$type = gettype($name);
throw new InvalidArgumentException(sprintf(
'Expected method name as string, got %s.', $type
));
}
if ($arguments === null) {
$arguments = [];
}
if (!is_array($arguments)) {
$type = gettype($arguments);
throw new InvalidArgumentException(sprintf(
'Expected %s\'s arguments as array, got %s.', $name, $type
));
}
$arguments['Software'] = self::SOFTWARE;
/*
* Do some magic ;)
*/
$callback = [$client, $name];
// $response = call_user_func($callback, $arguments);
$response = $callback($arguments);
if (empty($response)) {
$message = sprintf('Method %s returned empty body.', $name);
throw new EmptyResponseException($message);
}
return $response;
}
/**
* Return the SOAP client.
*
* Set it up, if not available now.
*
* @throws SoapFault
*
* @return SoapClient
*/
final protected function getClient()
{
$client = $this->client;
if ($client === null) {
$client = new SoapClient($this->url, $this->options);
$client->__setSoapHeaders($this->soapHeaders);
$this->client = $client;
}
return $this->client;
}
/**
* Convert the stdClass object into an array.
*
* This method uses recursive calls.
*
* Thanks to https://stackoverflow.com/a/18576919
*
* @param stdClass|array $array
*
* @return array
*/
final protected function convert2array($array)
{
if (is_array($array)) {
foreach ($array as $key => $value) {
if (is_array($value)) {
$array[$key] = $this->convert2array($value);
}
if ($value instanceof stdClass) {
$array[$key] = $this->convert2array((array)$value);
}
}
}
if ($array instanceof stdClass) {
return $this->convert2array((array)$array);
}
return $array;
}
/**
* @param SoapHeader $header
*
* @return $this
*/
private function addSoapHeader(SoapHeader $header)
{
$this->client = null;
$this->soapHeaders[] = $header;
return $this;
}
}
/**
* Class ParcelOneRequest
*
* Implement here all API methods we support.
*/
class ParcelOneRequest extends AbstractParcelOneRequest
{
/**
* Get available Products for a mandator and Carrier
*
* @param int|string $level : [0, 1, 2, ..]
* - 0 = all levels returned;
* - 1 = only 1 level returned (only products),
* - >=2 = 2 levels returned (products and services info)
*
* @throws SoapFault
*
* @return array|stdClass
*/
public function getProducts($level = 1)
{
$level = is_numeric($level) ? $level : 1;
$level = max(0, (int)$level);
$arguments = [
'Mandator' => $this->mandator, // string, as field in settings?
'level' => $level, // int
'CEP' => $this->carrier, // string Carrier abbreviation (UPS, PA1 for Parcel One, ...).
];
$response = $this->call(__FUNCTION__, $arguments);
if (!isset($response->getProductsResult)) {
throw MissingResponseFieldException::fromFieldName('getProductsResult');
}
if (!isset($response->getProductsResult->Product)) {
throw MissingResponseFieldException::fromFieldName('getProductsResult->Product');
}
$response = $response->getProductsResult->Product;
$response = $this->convert2array($response);
$products = [];
// get as first the default product
foreach ($response as $product) {
if (array_key_exists('Default', $product) && $product['Default']) {
$id = $product['ProductID'];
$name = $product['ProductName'];
$products[$id] = $name;
}
}
// then add all other products
foreach ($response as $product) {
if (array_key_exists('Default', $product) && !$product['Default']) {
$id = $product['ProductID'];
$name = $product['ProductName'];
$products[$id] = $name;
}
}
return $products;
}
/**
* Register Forward and Return Shipments.
*
* @param $shippingData
*
* @throws SoapFault
*
* @return array
*/
public function registerShipments($shippingData)
{
$default = [
// MandatorID: required - Mandator ID at PARCEL.ONE, usually "1", if only one mandator.
'MandatorID' => $this->mandator,
// ConsignerID: required - Consigner ID at PARCEL.ONE, usually "1", if only one consigner.
'ConsignerID' => $this->consigner,
// CEPID: required - Carrier specification, possible values so far: UPS, PA1, DHL.
'CEPID' => $this->carrier,
// Software: required - Software specification of client, if possible with version number.
'Software' => self::SOFTWARE,
/*
* PrintDocuments: required - Flag to indicate,
* if Documents (for DHL also Export Documents) should be returned with this request
* (0=no, 1=yes).
*/
'PrintDocuments' => 1,
// DocumentFormat: required - only Format.Type = "PDF" needed.
'DocumentFormat' => ['Type' => 'PDF'],
// LabelFormat: required - only Format.Type = either "GIF" or "PDF" needed.
'LabelFormat' => ['Type' => 'PDF'],
// PrintLabel: required - Flag to indicate, if Label should be returned with this request (0=no, 1=yes).
'PrintLabel' => 1,
];
$shippingData = array_merge($shippingData, $default);
$shippingData = ['ShippingData' => [$shippingData]];
$response = $this->call(__FUNCTION__, $shippingData);
if (!isset($response->registerShipmentsResult)) {
throw MissingResponseFieldException::fromFieldName('registerShipmentsResult');
}
$response = $this->convert2array($response);
if (!isset($response['registerShipmentsResult'])) {
throw new ResponseException('Missing registerShipmentsResult response field.');
}
$response = $response['registerShipmentsResult'];
if (!isset($response['ShipmentResult'])) {
throw new ResponseException('Missing ShipmentResult response field.');
}
$response = $response['ShipmentResult'];
foreach ($response['ActionResult']['Errors'] as $e) {
if (!is_array($e)) {
continue;
}
if (!array_key_exists('Message', $e) || !$e['Message']) {
continue;
}
$msg = $e['Message'];
if (array_key_exists('StatusCode', $e) && $e['StatusCode']) {
$msg .= ' (' . (string)$e['StatusCode'] . ')';
}
throw new ResponseException($msg);
}
return $response;
}
/**
* Get available Carriers for a mandator, optionally filtered by countries list.
*
* CEP[] getCEPs(string Mandator, int level, String[] Countries);
*
* @param int|string level: [0, 1, 2, ..]
* 0 = all levels returned;
* 1 = only 1 level returned (only products),
* >=2 = 2 levels returned (products and services info)
*
* @throws SoapFault
*
* @return array|stdClass
*/
public function getCEPs($level = 1)
{
$arguments = [
'Mandator' => $this->mandator,
'level' => max(1, (int) $level),
];
$response = $this->call(__FUNCTION__, $arguments);
if (!isset($response->getCEPsResult)) {
throw new ResponseException('Missing getCEPsResult field.');
}
if (!isset($response->getCEPsResult->CEP)) {
throw new ResponseException('Missing CEP field.');
}
$response = $response->getCEPsResult->CEP;
$response = $this->convert2array($response);
return $response;
}
/**
* @throws SoapFault
*/
public function getServices()
{
$arguments = [
'Mandator' => $this->mandator,
'CEP' => $this->carrier,
'Product' => $this->product,
];
$response = $this->call(__FUNCTION__, $arguments);
if (!isset($response->getServicesResult)) {
throw MissingResponseFieldException::fromFieldName('getServicesResult');
}
if (!isset($response->getServicesResult->Service)) {
throw MissingResponseFieldException::fromFieldName('getServicesResult::Service.');
}
$response = $response->getServicesResult->Service;
$response = $this->convert2array($response);
$services = [
'' => 'Kein Service'
];
// Search for the default service
foreach ($response as $service) {
if (array_key_exists('Default', $service) && $service['Default']) {
$id = $service['ServiceID'];
$name = $service['ServiceName'];
$services[$id] = $name;
}
}
// Append all other services
foreach ($response as $service) {
if (array_key_exists('Default', $service) && ! $service['Default']) {
$id = $service['ServiceID'];
$name = $service['ServiceName'];
$services[$id] = $name;
}
}
return $services;
}
// todo: implement here the necessary methods
}
/**
* API from PARCEL.ONE
*
* Note: the class name is expected as:
*
* Versandart_{filename}
*
* Where {filename} is lowercase and without the '.php' extension.
*
* @url: https://parcel.one/en
* @url: https://parcel.one/en/api
*/
class Versandart_parcelone extends AbstractVersandartParcelone
{
/**
* @inheritDoc
*/
public function GetBezeichnung()
{
return 'PARCEL.ONE';
}
protected function ctrHook()
{
$current = [
'kdnr' => $this->app->Secure->GetPOST('kdnr'),
'password' => $this->app->Secure->GetPOST('password'),
'carrier_product' => $this->app->Secure->GetPOST('carrier_product'),
];
$current = array_filter($current);
// $this->einstellungen += $current;
$this->einstellungen = array_merge($this->einstellungen, $current);
if (array_key_exists('carrier_product', $this->einstellungen)) {
list($carrier, $product) = explode('.', $this->einstellungen['carrier_product']);
$this->einstellungen['carrier'] = $carrier;
$this->einstellungen['product'] = $product;
}
}
/**
* @inheritDoc
*/
protected function EinstellungenStruktur()
{
if (!class_exists('SoapClient')) {
$message = 'PHP SOAP Extension ist nicht konfiguriert. Diese Versandart kann nicht genutzt werden.';
$this->app->Tpl->Add('MESSAGE', sprintf('<div class="error">%s</div>', $message));
return [];
}
$select = [];
$settings = array_filter($this->einstellungen);
// $this->app->Tpl->Add('MESSAGE', '<div class="error">' . json_encode($this->einstellungen) . '</div>');
// $this->app->Tpl->Add('MESSAGE', '<div class="error">' . json_encode($settings) . '</div>');
try {
if (!empty($settings)) {
$carriers = [];
if (!array_key_exists('kdnr', $settings) || !array_key_exists('password', $settings)) {
$this->app->Tpl->Add('MESSAGE', '<div class="error">' . 'Bitte gültige API-Zugangsdaten angeben' . '</div>');
} else {
$request = ParcelOneRequest::getInstance($this->einstellungen);
$carriers = $request->getCEPs(2);
if (!array_key_exists('carrier_product', $settings)) {
$this->app->Tpl->Add('MESSAGE', '<div class="info">API-Key erfolgreich überprüft</div>');
}
}
if (!array_key_exists(0, $carriers)) {
// i'm not sure about the structure if multiple carriers are available.
$tmp = $carriers;
$carriers = [];
$carriers[] = $tmp;
unset($tmp);
}
foreach ($carriers as $carrier) {
$cepID = $carrier['CEPID'];
$cepName = $carrier['CEPLongname'];
foreach ($carrier['Products']['Product'] as $product) {
$productID = $product['ProductID'];
$productName = $product['ProductName'];
$select[$cepID . '.' . $productID] = $cepName . ': ' . $productName;
}
}
}
}catch (Exception $e) {
$this->app->Tpl->Add('MESSAGE', '<div class="error">' . $e->getMessage() . '</div>');
$select = [];
}
if (empty($select)) {
$select = ['Bitte Zugangsdaten berichtigen'];
} else if (!array_key_exists('carrier_product', $settings)) {
$this->app->Tpl->Add('MESSAGE', '<div class="info">Bitte ein Produkt wählen</div>');
} else {
$tmp = [];
$firstID = $settings['carrier_product'];
// add the chosen one as first element to $tmp
foreach ($select as $id => $name) {
if ($id === $firstID) {
$tmp[$id] = $name;
}
}
// append all other elements
foreach ($select as $id => $name) {
if ($id !== $firstID) {
$tmp[$id] = $name;
}
}
$select = $tmp;
unset($tmp);
}
return [
'kdnr' => [
'typ' => 'text',
'bezeichnung' => 'Kundennummer:',
'size' => 40,
],
'password' => [
'typ' => 'text',
'bezeichnung' => 'Passwort:',
'size' => 40,
],
'country' => [
'typ' => 'text',
'bezeichnung' => 'Absender Land:',
'size' => 2,
'placeholder' => 'DE',
'default' => 'DE',
'regex' => '^[A-Z]{2}$'
],
'international' => [
'size' => 40,
'typ' => 'select',
'default' => 'CN23',
'bezeichnung' => 'Zolldokumente:',
'optionen' => [
'' => 'Nicht benötigt',
'CN22' => 'CN22',
'CN23' => 'CN23',
],
],
'ref1' => [
'size' => 40,
'typ' => 'text',
'default' => '',
'bezeichnung' => 'Referenz 1:',
'placeholder' => 'Referenz 1 auf Label',
'info'=>'{LIEFERSCHEIN}, {AUFTRAG}, {PROJEKT}, {IHREBESTELLNUMMER}, {INTERNET}',
],
'ref2' => [
'size' => 40,
'typ' => 'text',
'default' => '',
'bezeichnung' => 'Referenz 2:',
'placeholder' => 'Referenz 2 auf Label',
'info'=>'{LIEFERSCHEIN}, {AUFTRAG}, {PROJEKT}, {IHREBESTELLNUMMER}, {INTERNET}',
],
'carrier_product' => [
'size' => 40,
'typ' => 'select',
'bezeichnung' => 'Spediteur & Produkt:',
'optionen' => $select,
],
'standardgewicht' => [
'size' => 40,
'typ' => 'text',
'bezeichnung' => 'Standardgewicht'
],
'autotracking' => [
'typ' => 'checkbox',
'bezeichnung' => 'Tracking übernehmen:'
],
'sandbox' => [
'typ' => 'checkbox',
'bezeichnung' => 'Sandbox Anbindung:'
],
];
}
/**
* @inheritDoc
*
* @throws SoapFault
*/
protected function parseTemplate($target)
{
if (!array_key_exists('carrier', $this->einstellungen)) {
$this->app->Tpl->Add('MESSAGE', '<div class="error">Bitte die Einstellungen vervollständigen.</div>');
} else {
$request = ParcelOneRequest::getInstance($this->einstellungen);
$services = $request->getServices();
unset($services['']);
$html = '';
$frame = '<div><input type="checkbox" id="service_%s" name="service[]" value="%s"><label for="service_%s">%s</label></div>';
foreach ($services as $id => $service) {
$html .= sprintf($frame, $id, $id, $id, $service);
}
$services = $html;
$this->app->Tpl->Set('SERVICE', $services);
}
$this->app->Tpl->Parse($target, 'versandarten_parcelone.tpl');
}
public function VersandartMindestgewicht()
{
if(!empty($this->einstellungen['standardgewicht'])){
return str_replace(',','.',$this->einstellungen['standardgewicht']);
}
return 0;
}
/**
* @inheritDoc
*
* @throws SoapFault
*/
protected function createPaketmarke($doctyp, $id, $target, $error, $adressdaten, $packageData)
{
$versandId = $doctyp==='versand'?$id:0;
if (!array_key_exists('carrier', $this->einstellungen)) {
$this->app->Tpl->Add('MESSAGE', '<div class="error">Bitte die Einstellungen vervollständigen.</div>');
return false;
}
// lieferschein, retoure & versand contain all an 'adresse' field.
$document = $this->getDocumentByID($doctyp, $id);
if (!array_key_exists('adresse', $document)) {
throw new MissingArgumentException(sprintf(
'Missed field for adresse in %s.', $doctyp
));
}
$lieferscheinID = $id;
if (array_key_exists('lieferscheinid', $document)) {
$lieferscheinID = $document['lieferscheinid'];
}
$address = $this->loadAddress($document['adresse']);
$items = $this->loadDeliveryPositions($id, $doctyp);
$auftragnummer = $this->getAuftragNummer($lieferscheinID);
$projektabkuerzung = $this->getProjectShortName($lieferscheinID);
// Source: ups.php
$ihrebestellnummer = $this->app->DB->Select("SELECT ihrebestellnummer FROM lieferschein WHERE id='$lieferscheinID' LIMIT 1");
$lieferscheinnummer = $this->app->DB->Select("SELECT belegnr FROM lieferschein WHERE id='$lieferscheinID' LIMIT 1");
$internet = $this->app->DB->Select("SELECT a.internet FROM lieferschein l LEFT JOIN auftrag a ON a.id=l.auftragid WHERE l.id='$lieferscheinID' LIMIT 1");
$replacer = new ParcelOneReplacer([
'IHREBESTELLNUMMER' => $ihrebestellnummer,
'LIEFERSCHEIN' => $lieferscheinnummer,
'PROJEKT' => $projektabkuerzung,
'AUFTRAG' => $auftragnummer,
'INTERNET' => $internet
]);
/*
* ShipTo Reference provided by client, string, max length 20
*/
$clientsReference = $this->einstellungen['ref2'];
$clientsReference = $replacer->handle((string) $clientsReference);
$clientsReference = substr($clientsReference, 0, 20);
/*
* Private Address = 1, B2B = 0
*/
$private = $address['firma'] !== '1' ? 1 : 0;
/*
* ReturnShipmentIndicator: required -
* 0 = Forward Shipment,
* all values > 0 = Return Shipment.
*
* For UPS the following are available:
* 2-Print and Mail Return Label by UPS;
* 3-Return Service 1-Attempt;
* 5-Return Service 3-Attempt;
* 8-Electronic Return Label by URL;
* 9-Print Return Label.
*
* For DHL and Parcel One so far not available.
*
* We only support Parcel One, so:
* 0 -> forward,
* 1 -> return
*/
$returnShipment = (int)$doctyp === 'retoure';
/*
* Parcel ID assigned at Shipping.
* will be reassigned at successfully shipping.
*/
$packageID = '';
/*
* Shipment Reference field provided by client for identification, string, max length 20.
*/
$shipmentRef = $this->einstellungen['ref1']; // $packageData['shipment_reference'];
$shipmentRef = $replacer->handle((string) $shipmentRef);
$shipmentRef = substr($shipmentRef, 0, 20);
/*
* Certificate No to print on CN23.
*/
$certificateNo = '';
/*
* optional - Invoice No to print on CN23. max. length: 20
* $lieferschein['belegnummer'] // 'ihrebestellnummer'
*/
$invoiceNo = '';
/*
* optional item category
* possible values:
* - 1: Gift
* - 2: Documents
* - 3: Commercial Sample
* - 4: Returned Goods
* - 5: Other
*/
$itemCategory = $returnShipment ? 4 : 5;
$weight = $this->getWeight($packageData);
if (!$weight) {
foreach ($items as $item) {
$weight += (float) $item['NetWeight'];
}
}
$weight = number_format($weight, 3);
$product = $this->einstellungen['product'];
$international = $this->getInternationalDocumentType($adressdaten['land']);
$destinationCountry = $adressdaten['land'];
$originCountry = $this->einstellungen['country'];
$printInternationalDocuments = 0;
if($destinationCountry != $originCountry){
$printInternationalDocuments = (int)(!$this->app->erp->IsEU($destinationCountry));
}
$shipment = [
'ProductID' => $product,
'ShipmentRef' => $shipmentRef,
'ReturnShipmentIndicator' => $returnShipment,
'ShipToData' => [
'Name1' => $adressdaten['name'],
'Name2' => $adressdaten['name2'],
'Name3' => $adressdaten['name3'],
/*
* PrivateAddressIndicator: optional/required
* - 1 Private Address,
* - 0 B2B-Address.
*/
'PrivateAddressIndicator' => $private,
'ShipmentAddress' => [
'City' => $adressdaten['ort'],
'PostalCode' => $adressdaten['plz'],
'Street' => $adressdaten['street'],
'Streetno' => $adressdaten['street_no'],
'Country' => $adressdaten['land'],
// 'State' => '',
// 'District' => '',
],
'Reference' => $clientsReference,
],
'Packages' => [
[
'PackageWeight' => [
'Value' => $weight,
],
'PackageID' => $packageID,
'IntDocData' => [
'ContentsDesc' => $items,
'ShipToRef' => $shipmentRef,
'ItemCategory' => $itemCategory,
'InvoiceNo' => (string)$invoiceNo,
'Invoice' => (int)(bool)$invoiceNo,
'PrintInternationalDocuments' => (int) ! empty($international),
'Explanation' => $clientsReference,
'ConsignerCustomsID' => '',
'CertificateNo' => (string)$certificateNo,
'Certificate' => (int)(bool)$certificateNo,
'TotalWeightkg' => $weight, // $weight, // '34.000', // "34.000",
// 'Postage' => '2.45', // "2.45", // optional - Postage Amount // Porto
'InternationalDocumentFormat' => [
'Type' => 'PDF',
'Size' => $international,
],
],
],
],
];
$service = $packageData['service'];
if (!empty($service) && is_array($service)) {
$services = [];
foreach ($service as $serviceID) {
// Parameter Services - Array of ShipmentService:
// Parameters: optional - so far not in use, for future use.
// ServiceID: required - Service ID, e.g. NN for COD, WERT for insurance, SA for Saturday Delivery, etc.
// Value: optional - Currency and Value specification, e.g. for Insurance or COD. Exception for DHL-BulkyGoods: Amount determines kind of bulkgoods: 0=Lang, 1=L, 2=XL, 3=XXL, default=XXL
$services[] = ['ServiceID' => $serviceID];
}
$shipment['Services'] = $services;
}
$request = ParcelOneRequest::getInstance($this->einstellungen);
$response = $request->registerShipments($shipment);
if (!array_key_exists('ActionResult', $response)) {
throw MissingResponseFieldException::fromFieldName('ActionResult');
}
$number = $this->extractActionResultID($response);
if ($this->einstellungen['autotracking'] === '1') {
$lieferscheinID = $id;
if (array_key_exists('lieferschein', $document)) {
$lieferscheinID = $document['lieferschein'];
}
$versandID = $doctyp === 'versand' ? $id : 0;
$this->SetTracking($number, $versandID, $lieferscheinID);
unset($versandID, $lieferscheinID);
}
if (!$packageData['drucken'] && !$packageData['tracking_again']) {
return [];
}
/*
* Let's print the two documents.
*/
if (array_key_exists('LabelsAvailable', $response) && $response['LabelsAvailable'] === 1) {
$label = 'Label_' . $number . '.pdf';
$content = $response['PackageResults']['ShipmentPackageResult']['Label'];
$content = base64_decode($content, true);
$this->printFile($label, $content, $versandId);
unset($label, $content);
}
if (array_key_exists('InternationalDocumentsNeeded', $response) &&
array_key_exists('InternationalDocumentsResults', $response) &&
array_key_exists('InternationalDocumentsAvailable', $response) &&
is_array($response['InternationalDocumentsResults']) &&
!empty($response['InternationalDocumentsResults']) &&
$response['InternationalDocumentsNeeded'] === 1 &&
$response['InternationalDocumentsAvailable'] === 1 && !empty($response['InternationalDocumentsResults'])) {
$internationalFile = $international . '_' . $number . '.pdf';
$content = $response['InternationalDocumentsResults']['ShipmentDocumentsResult']['Document'];
$content = base64_decode($content, true);
$this->printFile($internationalFile, $content, $versandId);
}
return [];
}
/**
* Get the custom documents format.
*
* @param string $country like 'DE'
*
* @return string of '', 'CN22' or 'CN23';
*/
protected function getInternationalDocumentType($country)
{
$senderCountry = $this->app->erp->Firmendaten('land');
if ($country === $senderCountry) {
// no custom documents required
return '';
}
if ($this->app->erp->IstEU($country)) {
// no custom documents required
return '';
}
$international = $this->einstellungen['international'];
if (in_array($international, ['CN22', 'CN23'], true)) {
return $international;
}
return 'CN23';
}
/**
* Load contents & quantity for all items of one delivery.
*
* @param int|string $id delivery note id
* @param string $table
*
* @return array
*/
private function loadDeliveryPositions($id, $table)
{
if (is_string($id) && is_numeric($id)) {
$id = (int)$id;
}
if (!is_int($id)) {
$type = gettype($id);
throw new ArgumentTypeException('Expected order id as int, got ' . $type);
}
// $sql = '
//SELECT
// lp.menge,
// lp.bezeichnung,
// if(lp.zolleinzelwert >0 , lp.zolleinzelwert , ( ap.preis - (ap.preis / 100 * ap.rabatt ) ) ) as preis,
// lp.zolltarifnummer,
// if(lp.zollwaehrung != \'\' , lp.zollwaehrung, ap.waehrung) as waehrung,
// lp.artikel,
// lp.zolltarifnummer
//FROM
// lieferschein_position lp
//LEFT JOIN
// auftrag_position ap
//ON
// ap.id = lp.auftrag_position_id
// LEFT JOIN
// artikel a
// ON
// a.id = lp.artikel
// WHERE
// lp.lieferschein=\'%s\'
// AND
// ap.explodiert != 1
// AND
// a.lagerartikel = 1';
switch ($table) {
case 'versand':
$sql = '
SELECT
lp.bezeichnung as Contents,
lp.menge as Quantity,
lp.zollgesamtgewicht as NetWeight,
lp.zollgesamtwert as ItemValue,
lp.herkunftsland as Origin,
lp.zolltarifnummer as TariffNumber,
lp.zollwaehrung as Currency
FROM
lieferschein_position as lp,
versand as v
WHERE
lp.lieferschein = v.lieferschein
AND
v.id =\'%s\'';
break;
case 'lieferschein':
$sql = '
SELECT
bezeichnung as Contents,
menge as Quantity,
zollgesamtgewicht as NetWeight,
zollgesamtwert as ItemValue,
herkunftsland as Origin,
zolltarifnummer as TariffNumber,
zollwaehrung as Currency
FROM
lieferschein_position
WHERE
lieferschein =\'%s\'';
break;
case 'retoure':
$sql = '
SELECT
bezeichnung as Contents,
menge as Quantity,
herkunftsland as Origin,
zolltarifnummer as TariffNumber
FROM
retoure_position
WHERE
retoure =\'%s\'';
// zolleinzelgewicht as NetWeight,
// zolleinzelwert as ItemValue,
// zollwaehrung as Currency
break;
case 'auftrag':
$sql = '
SELECT
bezeichnung as Contents,
menge as Quantity,
zollgesamtgewicht as NetWeight,
zollgesamtwert as ItemValue,
herkunftsland as Origin,
zolltarifnummer as TariffNumber,
zollwaehrung as Currency
FROM
auftrag_position
WHERE
auftrag =\'%s\'';
break;
default:
throw new InvalidArgumentException(sprintf('Unknown table \'%s\'.', $table));
}
$sql = sprintf($sql, $id);
$positions = $this->app->DB->Query($sql);
$error = $this->app->DB->error();
if ($error) {
$error = htmlspecialchars($error);
$msg = sprintf('SQL query error: \'%s\', query was \'%s\'', $error, $sql);
throw new RuntimeException($msg);
}
$positions = $positions->fetch_all(MYSQLI_ASSOC);
$error = $this->app->DB->error();
if ($error) {
$error = htmlspecialchars($error);
$msg = sprintf('SQL query error: \'%s\', query was \'%s\'', $error, $sql);
throw new RuntimeException($msg);
}
if (!is_array($positions)) {
$type = gettype($positions);
throw new ArgumentTypeException('Expected positions as array, got ' . $type);
}
return (array)$positions;
}
/**
* Return a tracking number / package id
*
* @param $response
*
* @throws MissingResponseFieldException
*
* @return string|int
*/
private function extractActionResultID($response)
{
$number = null;
if (!is_array($response)) {
$type = gettype($response);
throw new ArgumentTypeException('Expected response as array, got ' . $type);
}
if (!array_key_exists('ActionResult', $response)) {
throw new MissingResponseFieldException('Field \'ActionResult\' in response is missing');
}
$actionResult = $response['ActionResult'];
if (array_key_exists('TrackingID', $actionResult)) {
return $actionResult['TrackingID'];
}
if (array_key_exists('ShipmentID', $actionResult)) {
return $actionResult['ShipmentID'];
}
if (array_key_exists('ShipmentRef', $actionResult)) {
return $actionResult['ShipmentRef'];
}
throw MissingResponseFieldException::fromFieldName('TrackingID/ShipmentID/ShipmentRef is missing.');
}
/**
* Source: ups.php
*
* @param int $lieferscheinID
* @return string
*/
public function getProjectShortName($lieferscheinID)
{
$projekt = $this->app->DB->Select("SELECT projekt FROM lieferschein WHERE id='$lieferscheinID' LIMIT 1");
return $this->app->DB->Select("SELECT abkuerzung FROM projekt WHERE id='$projekt' LIMIT 1");
}
/**
* Source: ups.php
*
* @param int $lieferscheinID
* @return int
*/
public function getAuftragNummer($lieferscheinID)
{
$auftragid = $this->app->DB->Select("SELECT auftragid FROM lieferschein WHERE id='$lieferscheinID' LIMIT 1");
if ($auftragid > 0 ) {
return $this->app->DB->Select("SELECT belegnr FROM auftrag WHERE id='$auftragid' LIMIT 1");
}
return '';
}
}