Trigger Scripts

Mit Trigger Scripts werden Skipte bezeichnet, die sich an bestimmten Stellen der Software in die Geschehnisse "einklinken" und nach sehr individuellen Gesichtspunkten Entscheidungen treffen können, die z.B. Eigenschaften eines Auftrages anpassen oder den weiteren Weg durch die Prozesse der Firma verändern.

Arten von Triggern

Trigger Scripts existieren in zwei verschiedenen Formen:
  • Event Trigger (Ereignis-Trigger)
  • Timed Trigger (Zeitgesteuerte Trigger oder Prozessstarter)

Event Trigger

Diese Ereignis-Trigger werden dazu an bestimmte Ereignisse gehängt - ein technischer Vorgang, der aber leicht zu verstehen und intuitiv zu verwenden ist wie Sie an einigen Beispielen schnell sehen werden:
  • OnDocumentChange: Wenn z.B. in einem Auftrag oder einer Rechnung eine Änderung vorgenommen wird
  • OnDocumentOfferPositionChange: Spezifischer als oben, wenn in einer Rechnung die Positionen der Artikel verändert werden
  • OnDocumentAddressChange: Spezifischer als oben, wenn in einem Beleg die Adresse verändert wird
  • OnDeliverynoteStockSwapOut: Beim Auslagern eines Lieferscheines
  • AfterAdressCreate: Nach dem Anlegen einer neuen Adresse
  • OnProductLabelPrint: Beim Ausdruck eines Artikel-Etiketts
Anwendungsbeispiele für Event Trigger Nun lassen Sie Ihrer Phantasie freien Lauf und folgen Sie uns auf die Reise durch ihr Xentral:
  • Sie möchten die Versandart dynamisch nach der Anzahl der Artikel in einem Auftrag und der PLZ der Lieferadresse anpassen?
    OnDocumentChange triggert ein Scripts, dass die Zahl der Artikel und die PLZ untersucht und den günstigsten Versanddienstleister

  • Sie möchten Ihren Kunden bei Erreichen eines Gesamtbestellwerts von € 100,- einen Aktionsrabatt gewähren?
    OnDocumentOfferPositionChange wird aufgerufen, sobald die Positionen eines Auftrages verändert werden. Dort können Sie auf alle Positionen und Artikel zugreifen, rasch die Gesamtsumme berechnen und einen Rabattartikel hinzufügen, sobald die Schwelle für den Gesamtrabatt erreicht ist. Ein Beispiel dafür finden Sie weiter unten unter Beispiel Gesamtrabatt.

  • Sie möchten beim Anlegen einer neuen Adresse durch Mitarbeiter des Vertriebsinnendienstes automatisch die Rolle "Kunde" vergeben, bei Mitarbeitern aus dem Einkauf die Rolle "Lieferant"?
    AfterAdressCreate ergänzt zusätzlich zur neuen Adresse eine Rolle je nach aktivem Benutzer des Systems (Vertrieb oder Einkauf).
Beipielsweise können so Versandeinstellungen nach den Artikeln eines Auftrages oder der Lieferadresse verändert werden. Ein anderes Beispiel wäre Abspaltung von Artikeln eines Auftrages bei Vorreservierung oder die Berechnung von Sonderrabatten bei der Auftragserstellung oder dem -import. Im Grunde existieren Trigger an allen Stellen von Xentral und so ermöglichen die Trigger Scripts auch die einfache Anpassung fast aller Prozesse. Ein wichtiges Beispiel ist dafür das OnDocumentChange Auf Basis des Beispiele erschließt sich die folgende auführlichere Liste von alleine.
Arbeiten mit Belegen (Documents)
  • BeforeDocumentChange, AfterDocumentChange
  • OnDocumentPositionChange
  • OnDocumentAddressChange
  • OnDocumentAttributeChange
  • OnDocumentStateChange / OnDocumentStatusChange
Arbeiten mit Stammdaten
  • BeforeProductCreate, BeforeProductChange, BeforeProductDelete, AfterProductCreate, AfterProductChange, AfterProductDelete,
  • BeforeAdressCreate, BeforeAdressChange, BeforeAdressDelete, AfterAdressCreate, AfterAdressChange, AfterAdressDelete
  • (zukünftig: BeforeUserCreate, BeforeUserChange, BeforeUserDelete, AfterUserCreate, AfterUserChange, AfterUserDelete)
Arbeiten mit Produktionen
  • BeforeProductionCreate, BeforeProductionEdit, BeforeProductionDelete, AfterProductionCreate, AfterProductionEdit, AfterProductionDelete
  • OnProductionStart, OnProductionFinish
  • OnProductionStockSwapOut, OnProductionStockSwapIn
Prozess-Trigger
  • Versandzentrum: OnShipment, OnAutoShipmentManuelStart, OnAutoShipmentPlusStart
  • Frankierung: BeforeStampCreate, AfterStampCreate
  • Scan der Trackingnummer: BeforeTrackingScan, AfterTrackingScan,
  • Lager: OnStockChange, OnStockSwapOut, OnStockSwapIn, OnDeliverynoteStockSwapOut
  • Drucken: OnPrint, OnLabelPrint, OnDocumentPrint, OnDocumentDeliveryNotePrint, OnDocumentInvoicePrint, OnDocumentOfferPrint, OnDocumentOrderPrint
  • E-Mail-Versand: OnEmailSend, OnDocumentEmailSend, OnDocumentDeliveryNoteEmailSend, OnDocumentInvoiceEmailSend, OnDocumentOfferEmailSend, OnDocumentOrderEmailSend
  • Benutzeraktionen: (zukünftig) OnUserAction
  • Benutzer-Session: (zukünftig) OnSessionStart, OnSessionEnd / OnSessionClose
  • Wareneingang: OnIncomingGoodsDocumentStart, OnIncomingGoodsDocumentEnd, OnIncomingGoodsArticleStart, OnIncomingGoodsArticleEnd

Timed Trigger

Timed Trigger (auch Prozessstarter) ermöglichen es, zeitgesteuerte Aktionen durchzuführen. Diese können periodisch (z.B. alle 15 Minuten, jede Stunde, alle 3 Tage) oder zu bestimmten Zeiten (z.B. Sonntags, 8:30 Uhr) ausgeführt werden.
Anwendungsbeispiele für Timed Trigger
Kurbeln Sie noch einmal Ihre Phantasie an und stellen Sie sich vor, was zeitgesteuerte Prozesse alles für Sie tun könnten:
  • Jede Nacht erstellen Sie eine Liste von Adressen, die weder als Kunde noch als Lieferant markiert sind und legen eine Aufgabe für Ihre Verwaltung an, die diese Fälle prüfen und bearbeiten soll.
  • Einmal wöchetlich erstellen Sie eine Liste, die alle Aufträge anzeigt, die noch nicht vollständig abgerechnet sind. Diese Liste lassen Sie sich als E-Mail zusenden.

Arbeiten mit Belegen (Event Trigger)

Mit Hilfe von Trigger Scripts können alle Daten eines Belegs (z.B. eines Auftrags) verändert werden. Dazu gehören z.B. die Veränderung von Positionen, Preisen, Beschreibungen, Rabatten, Versand- oder Zahlungsoptionen. Basis in diesem Beispiel ist das Event OnDocumentPositionChange. Dabei wird eine Datenanpassung vorgenommen und anschließend die normale Funktionalität der Funktion ausgeführt um das Standardverhalten von Xentral für die nicht veränderten Abläufe zu nutzen. Vorgehensweise vor Version 20.1: Überschreiben der Funtkion ANABREGSNeuberechnen()) in der Klasse ERPApi die hierfür überladen werden kann.

Ausführungszeitpunkt

Die Funktion wird immer aufgerufen wenn das Dokument nicht schreibgeschützt ist und verändert wird. Das gilt z.B. wenn
  • ein Beleg angelegt wird, auch beim Import aus einer Shop-Schnittstelle oder über EDI / Übertragen-Modul
  • ein API-Aufruf den Beleg verändert
  • Positionen des Belegs verändert werden und
  • durch Änderungen an Stammdaten des Belegs.

Belegarten

Die Funktion erhält zwei Parameter:
  • $art: Die Art des Belegs, also Angebote (angebot), Auftrag (auftrag), Rechnung (rechnung), Gutschrift (gutschrift)
  • $id: Die Datenbank-ID des Belegs
So kann frei auf allen Werten des Belegs gearbeitet werden und alle Daten in dieser Methode manipuliert werden. Dazu sollten vorzugsweise die ERP-eigenen Funktionen verwendet werden (siehe Fragen zur Programmierung). Das direkte Arbeiten auf der Datenbank ist aber ebenfalls möglich. Hinweis: Die Methode dient der Anpassung des entsprechenden Belegs und sollte daher auch nur zu diesem Zweck verwendet werden um unerwartetes Verhalten der Software zu vermeiden. So ist es z.B. für einen Benutzer nicht verständlich, wenn sich durch die Arbeit an einem Auftrag die Stammdaten der Kundenadresse ändern würden. Auch in Bezug auf Schreibzugriffe auf die DB-Tabellen sollten Sie sich auf die Daten des Belegs beschränken. Die entsprechenden Tabellen lauten:
  • auftrag
  • auftrag_positionen
  • rechnung
  • rechnung_positionen
  • lieferschein
  • lieferschein_positionen
  • gutschrift
  • gutschrift_positionen
Sie finden eine Übersicht der wichtigsten DB-Tabellen aus diesem Bereich unter Datenbank-Diagramm Belege und wichtigste Relationen

Vorgehensweise & Beispiel ''class.erpapi_custom.php''

Um die Funktion zu überladen muss lediglich eine Datei class.erpapi_custom.php erzeugt und im Verzeichnis www/lib/ (also parallel zur Datei class.erpapi.php) abgelegt werden. :entwickler:customizing_belegneuberechnen_howto_erpapi_custom.png

Beispiel: Gesamtrabatt

Das folgende Beispiel zeigt eine einfache Anwendung: Ab einer Gesamtsumme von EUR 100,- ($threshold, Wert netto) soll automatisch ein Rabattartikel ($rebateId) eingefügt werden. Dieser kann nur wieder gelöscht werden, wenn die Summe unter EUR 100,- sinkt, ansonsten wird er bei jeder Berechnung neu erzeugt. Der Rabatt soll dabei nciht in die Gesamtsumme eingerechnet werden, diese kann also auch unter EUR 100,- fallen. Das Beispiel zeigt die Überladung der Funktion ANABREGSNeuberechnen()
  • die Einschränkung auf Belegarten (siehe Sample 1),
  • die Verwendung der Artikelpositionen mit Artikel und Preis,
  • die Verwendung von Datenbank (siehe Sample 2),
  • die Verwendung von ERP-Funktionen (siehe Sample 3 und Sample 4) sowie
Zum Debugging können verschiedene Methoden angewendet werden. Siehe dazu Debugging bei der Entwicklung in Xentral.
Code
<?php

class erpAPICustom extends erpAPI
{
  public function __construct(&$app)
  {
    $this->app = $app;
    parent::__construct($app);
  } // end of constructor

  public function ANABREGSNeuberechnen($id, $art, $bool = false)
  {
    /* Choose the documents you want to manipulate  - Sample 1 */
    if($art === 'auftrag') {

      /* Custom definitions here */
      $rebateId = 15;        /* Set rebate article ID % */
      $threshold = 100;      /* Activate rebate for orders above EUR 100,- */
      $rebateExists = false; /* Assume rebate article not yet added */

      /* Get some information about positions from database - Sample 2 */
      $positionenSQL = sprintf('SELECT ap.* FROM %s_position ap  WHERE %s = %s', $art, $art, $id);
      $positionen = $this->app->DB->SelectArr($positionenSQL);

      /* Walk through positions and compute the price */
      $sum = 0;
      foreach ($positionen as $p) {
        $artikelId = $p['artikel'];
        $menge = $p['menge'];

        if ((int)$artikelId !== $rebateId) {
          $sum += $this->app->erp->GetVerkaufspreis($artikelId, $menge) * $menge; /* Sample 3 */
        } else {
          $rebateExists = true;
        }
      } // end of foreach

      if ($sum >= $threshold && !$rebateExists) {
        /* Sample 4 */
        $this->app->erp->AddAuftragPositionManuell($id, $rebateId, 0, 1, 'Rabatt ab € '.$this->app->erp->formatMoney($threshold, 'EUR'));
      } // end of if - add rebate if not exists
    } // end of if - Document type: Offer / Auftrag

    /* Call the main routine from parent class erpAPI and return */
    return parent::ANABREGSNeuberechnen($id, $art);
  } // end of function

} // end of class

Ergebnis
Im Ergebnis passt sich dann die Positionstabelle folgendermaßen an. :entwickler:customizing_belegneuberechnen_beispiel_positionstabelle.png Und der Beleg sieht entsprechend ebenfalls anders aus::entwickler:customizing_belegneuberechnen_beispiel_beleg.png

Beispiel: Schweizer Rappen-Rundung

In der Schweiz sind 1 und 2 Rappenmünzen nicht mehr im Umlauf. Aus diesem Grund runden Einzelhändler auf 5 oder 10 Rappen. Dabei gelten mathematische Rundungsgereln, es wird also auf- oder abgerundet. Die Rundung bezieht sich im Normalfall auf den Gesamtbetrag. Mit Hilfe der Funktion ANABREGSNeuberechnen() kann diese RUndung umgesetzt werden. Dazu muss der Gesamtbetrag in zwei Feldern gespeichert werden: extsoll und gesamtsumme.
Weitere Ideen für Ihre Projekte
  • Besonderer Preis oder Rabatt ab einer definierten Gesamtmenge
  • Kombination verschiedener Artikel oder ganzer Artikelkategorien in einer Rabattstaffelung
  • "Zahle x bekomme y"
  • Gesamt-Rabatt ab x Euro (siehe Beispiel oben)
  • Versandkostenfrei ab x Euro
  • Verwendung besonderer Erlöskonten nach bestimmten Kriterien
  • Anpassung der Versandart oder -kosten nach Zielgebiet, Positionen, Gesamtgewicht, o.ä.
  • Besondere Rundungen (siehe Bsipiel Schweizer Rappen-Rundung)

Beispiel: Lieferschein auslagern

Mit der Methode LieferscheinAuslagern wird der Lagerbestand angepasst. Den Algorithmus kann man dafür selbst entwickeln. Als Parameter bekommt man primär die ID des Lieferscheins, kann dann selbst die Positionen aus der Datenbank abfragen (ArtikelID und Menge) um dann im Lager in der Tabelle lager_platz_inhalt die korrekten Mengen zu entnehmen.
Code
function LieferscheinAuslagern($lieferschein,$anzeige_lagerplaetze_in_lieferschein=false, $standardlager = 0, $belegtyp = 'lieferschein', $chargenmhdnachprojekt = 0, $forceseriennummerngeliefertsetzen = false,$nurrestmenge = false, $lager_platz_vpe = 0, $lpiid = 0)
Die Funktion bekommt Werte aus Ihrem System übergeben, wie sie bei der Einrichtung von Ihnen, Ihrem Einrichter oder unserem Onboarding-Team festgelegt wurden. Diese übergebenen Werte können dann innerhalt der Funktion ausgewertet werden:
  • $lieferschein: ID des Lieferscheins
  • $anzeige_lagerplaetze_in_lieferschein: Anzeige der Lagerplätze im Lieferschein (System-Einstellung)
  • $standardlager: Standardlager (System-Einstellung), Defaultwert ist 0
  • $belegtyp - Typ des Beleges, Default ist 'lieferschein'
  • $chargenmhdnachprojekt: Nur zur Verwendung durch Xentral (System-Einstellung)
  • $forceseriennummerngeliefertsetzen: Nur zur Verwendung durch Xentral (System-Einstellung)
  • $nurrestmenge: Nur zur Verwendung durch Xentral (System-Einstellung)
  • $lager_platz_vpe: Nur zur Verwendung durch Xentral (System-Einstellung)
  • $lpiid: Nur zur Verwendung durch Xentral (System-Einstellung)
Nun werden die eigentlichen Entnahmen aus dem Lager vorgenommen. Dazu dient die Funktion LagerAuslagernRegal. Aufruf:
function LagerAuslagernRegal($artikel,$regal,$menge,$projekt,$grund,$importer="", $doctype = "", $doctypeid = 0, $lager_platz_vpe = 0, $lpiid = 0)
Beschreibung der Parameter:
  • $artikel: Artikel ID
  • $regal: Lagerplaz ID aus dem entnommen wird
  • $menge: Menge die entnommen wird
  • $projekt: Projekt für Lagerbewegung
  • $grund: Bemerkung für Lagerbewegungstabelle
  • $importer: Aktuelle Wert aus Prozess wird hier übergeben - aktuell bitte leer lassen, Default ist Leerstring
  • $doctype: Optionale Angabe für welchen Beleg entlagert wird, Defaultwert ist Leerstring
  • $doctypeid: Optionale Angabe für welchen Beleg (ID des Beleges) entlagert wird, Defaultwert ist 0
  • $lager_platz_vpe: Optional Angabe welche VPE ausgelagert wird, Defaultwert ist 0. Diese Angabe wird aktuell nur für Amazon verwendet
  • $lpiid: Optional: Aktueller Wert aus Prozess wird hier übergeben - aktuell bitte 0 übergeben Defaultwert ist 0

Timed Trigger

Timed Trigger bezeichnen individuelle Prozessstarter, die integriert werden können um Datenanalysen, Datenmanipulationen oder andere datenbankbasierte Aktionen auszuführen. Dabei kann sowohl direkt in die Datenbank geschrieben als auch externe Funktionen (wie z.B. API-Calls) aufgerufen werden.

Allgemein

Alle Timed Trigger (Prozessstarter) liegen in dem Verzeichnis ../xentral/cronjobs. Der Hauptprozess (starter2.php) wird jede Minute vom Heartbeat-Cronjob (system cronjob) aufgerufen.Die starter2.php prüft dabei alle Einträge der Datenbanktabelle prozessstarter und führt die Prozesse aus, die laut ihrer Zeitpunkts- bzw. Periodeneinstellung an der Reihe sind (siehe Handbuch Prozessstarter).

Timed Trigger anlegen

  1. Erstellen Sie die PHP Datei für den Prozessstarter Code im Ordner ../xentral/cronjobs.
    Verwenden Sie für den Dateinamen nur ASCII Zeichen und vermeiden Sie Leerzeichen.
  2. Navigieren Sie in der Xentral Oberfläche zu AdministrationEinstellungenProzessstarter und erzeugen Sie einen neuen Eintrag
  3. Legen Sie die Zeitpunkt- / Periodeneinstellung für Ihren Prozess sinnvoll fest.
  4. Tragen sie in das Feld Parameter den Namen Ihrer PHP Datei ohne Dateiendung ein. Anhand dieses Parameters wird die starter2.php die auszuführende PHP Datei im cronjob Ordner suchen und ausführen.

Timed Trigger manuell ausführen

Während der Entwicklung muss der Prozessstarter normalerweise mehrmals zu Testzwecken manuell gestartet werden. Bewährt hat sich dafür folgende Vorgehensweise:
  • Arbeiten Sie nicht im Live-System, andernfalls haben die weiteren Punkte ggf. negative Auswirkungen.
  • Stellen Sie sicher, dass für das Entwicklungssystem kein Heartbeat-Cronjob registriert ist.
  • Schalten Sie alle Prozessstarter auf inaktiv bis auf den, den Sie bearbeiten.
  • Setzen Sie diese Einstellungen für den zu bearbeitenden Prozessstarter
  • Art: periodisch
  • Periode: 0
  • Starten Sie über eine Kommandozeile die starter2.php im cronjob Ordner.
  • Das Kommando kann z.B. lauten: php starter2.php .

Code schreiben

Beispiel: BMEcat-Export zum Update veränderter Artikel
Das folgende Beispiel erstellt zeitgesteuert einen Export der Produktdaten im BMEcat-Format, einem standardisierten Austauschformat für Katalogdaten im Katalogmanagement (siehe auch BMEcat auf Wikipedia). Dabei werden gezielt veränderte Artikel im Update-Format exportiert und nur Artikel gewählt, die seit dem letzten Ausführen des CRON-Jobs verändert wurden. In diesem Beispiel werden dafür nur Veränderungen an den Stammdaten der Artikel berücksichtigt, nicht aber z.B. veränderte Verkaufspreise.
<?php

use Xentral\Components\Database\Exception\QueryFailureException;
use Xentral\Components\Filesystem\Filesystem;
use Xentral\Components\Filesystem\FilesystemFactory;
use Xentral\Components\Util\StringUtil;

//predefined variables:

/** @var Application $app
 * access to services and functions(e.g. Database, erpAPI, PHPMail...)
 */

/** @var array $task
 * information about all current executed cronjobs
 */

/** @var int $task_index
 * index of current cronjob in $task array
 */


//information about the current running cronjob
$jobInfo = $task[$task_index];

//get the time of the last execution of this cronjob
$lastExec = $jobInfo['letzteausfuerhung'];

//it makes sense to keep some settings flexible (e.g. for test environment)
$outputDir = '';

$exportObject = new BmecatExportCronjob($app, $lastExec, $outputDir);
$exportObject->execute();

final class BmecatExportCronjob
{
    /** @var erpooSystem $app */
    private $app;

    /** @var array $data */
    private $data;

    /** @var string $lastExecution */
    private $lastExecution;

    /** @var SimpleXMLElement $xml */
    private $xml;

    /** @var string $outputDirectory */
    private $outputDirectory;

    /** @var bool $debugMode */
    private $debugMode = true; //set to false for production usage

    /**
     * @param erpooSystem $app
     * @param string      $lastExecution
     * @param string      $outputDirectory
     */
    public function __construct($app, $lastExecution, $outputDirectory = '')
    {
        $this->app = $app;
        $this->lastExecution = $lastExecution;
        if ($outputDirectory !== '') {
            $this->outputDirectory = $outputDirectory;
        } else {
            //get the userdata directory of Xentral as default value for output directory
            $this->outputDirectory = $app->Conf->WFuserdata;
        }
    }

    /**
     * Collects data for the BMECAT export and writes the result to a file
     *
     * @throws Exception
     *
     * @return void
     */
    public function execute()
    {
        $this->app->erp->LogFile('BMECAT_export started', __FILE__);

        //collect data for the export
        $this->data['header'] = $this->getHeader();
        $articles = $this->getArticles();

        //if there are no changed articles we leave a hint in the logfile and exit cleanly
        if (empty($articles)) {
            $this->app->erp->LogFile('BMECAT_export: No changed articles since last execution.');

            return;
        }

        $this->data['t_update_products']['_attr_prev_version'] = '0';
        $this->data['t_update_products']['article'] = $articles;

        //render the XML string according to BMECAT specs
        $this->xml = $this->toXml();

        //It is good practice to use filenames with timestamps for output to avoid colliding results
        $date = new DateTime('now');
        $filename = sprintf('bmecat_export_%s.xml', $date->format('Y-m-d_H_i_s'));

        //write the XML content to a file
        $this->writeXmlFile($this->xml, $filename);

        $this->app->erp->LogFile('BMECAT_export completed');
    }

    /**
     * Write the xml string to a file in the userdata directory
     *
     * uses Xentral FileSystem class to write the file.
     *
     * @param string $xml
     * @param string $filename
     */
    private function writeXmlFile($xml, $filename)
    {
        $path = sprintf('%s/%s', $this->outputDirectory, $filename);
        $this->debug('BMECAT_export write xml file', $path);

        //we do not want to overwrite old export files
        if (file_exists($path)) {
            throw new RuntimeException(sprintf('BMECAT_export Cannot overwrite file %s', $path));
        }

        /** @var FilesystemFactory $factory */
        $factory = $this->app->Container->get('FilesystemFactory');
        /** @var Filesystem $fileSystem */
        $fileSystem = $factory->createLocal('/');
        $fileSystem->write($path, $xml);
    }

    /**
     * transforms the data to xml string
     *
     * @return string
     */
    private function toXml()
    {
        $xml = new SimpleXMLElement('<BMECAT/>');
        $xml->addAttribute('version', '1.2');
        $this->appendXmlRecursive($xml, $this->data);

        return $xml->asXML();
    }

    /**
     * walks through the data recursively and buidls the data structure for bmecat
     *
     * @param SimpleXMLElement $startnode
     * @param array            $data
     * @param string           $parentName
     */
    private function appendXmlRecursive(SimpleXMLElement $startnode, $data, $parentName = '')
    {
        foreach ($data as $name => $value) {
            if ($parentName !== '') {
                $name = $parentName;
            }
            if (is_array($value)) {
                if (count(array_filter(array_keys($value), 'is_string')) === 0) {
                    $this->appendXmlRecursive($startnode, $value, $name);
                } else {
                    $child = $startnode->addChild(mb_strtoupper($name), '');
                    $this->appendXmlRecursive($child, $value);
                }
            } else {
                if (StringUtil::startsWith($name, '_attr_')) {
                    $startnode->addAttribute(substr($name, 6), $value);
                } else {
                    $startnode->addChild(mb_strtoupper($name), $value);
                }
            }
        }
    }

    /**
     * collects necessary data for the bmecat header
     *
     * @throws Exception
     *
     * @return array
     */
    private function getHeader()
    {
        $header = [];
        $header['generator_info'] = 'Xentral custom cronjob';
        $date = new DateTime('now');
        $header['catalog'] = [
            'language'        => 'deu',
            'catalog_id'      => 'standard_endkunden_katalog',
            'catalog_version' => '001.001',
            'datetime'        => [
                '_attr_type' => 'generation_date',
                'date'       => $date->format('Y-m-d'),
                'time'       => $date->format('H:i:s'),
            ],
        ];
        $header['supplier'] = ['suppliername' => $this->app->erp->Firmendaten('footer_0_1')];

        return $header;
    }

    /**
     * collects data about changed articles
     *
     * @return array
     */
    private function getArticles()
    {
        $data = [];
        //this query gets all articles that were edited since the cronjob's last execution
        $sql = sprintf(
            "SELECT a.id, a.typ, a.nummer, a.ean, a.name_de, a.katalogbezeichnung_de, a.katalogtext_de, 
                    a.hersteller, a.herstellernummer
            FROM artikel AS a 
            WHERE a.useredittimestamp > '%s' AND a.katalog = 1",
            $this->lastExecution
        );

        $articles = $this->app->DB->SelectArr($sql);

        $dbError = $this->app->DB->error();
        if (!empty($dbError)) {
            //this is a critical error, the cronjob can not continue
            //so log the error message and quit the cronjob with an error by throwing an exception
            $this->app->erp->LogFile('BMECAT_export article query failed', $this->app->DB->real_escape_string($sql));
            $this->app->erp->LogFile('BMECAT_export DB error', $this->app->DB->real_escape_string($dbError));

            throw new QueryFailureException('database query failed');
        }

        if (empty($articles)) {
            return [];
        }

        $this->debug(sprintf('BMECAT_export %s changed articles found', count($articles)));

        foreach ($articles as $article) {
            $item = [];
            $item['_attr_mode'] = 'update';
            $item['supplier_aid'] = $article['nummer'];
            $item['article_details']['description_short'] = $article['katalogbezeichnung_de'];
            $item['article_details']['description_long'] = $article['katalogtext_de'];
            if ($article['ean'] !== '') {
                $item['article_details']['ean'] = $article['ean'];
            }
            if ($article['herstellernummer'] !== '') {
                $item['article_details']['manufacturer_aid'] = $article['herstellernummer'];
            }
            if ($article['hersteller'] !== '') {
                $item['article_details']['manufacturer_name'] = $article['hersteller'];
            }
            $prices = $this->getPrices((int)$article['id']);
            foreach ($prices as $price) {
                $item['article_price_details']['article_price'][] = [
                    'price_amount'   => $price['preis'],
                    'price_currency' => $price['waehrung'],
                ];
            }
            $data[] = $item;
        }

        return $data;
    }

    /**
     * Gets currently active prices of one article
     *
     * @param int $articleId
     *
     * @return array
     */
    private function getPrices($articleId)
    {
        $this->debug('get prices for article', $articleId);
        //this query gets all prices of the specified article that are active on the current date
        $sql = sprintf(
            "SELECT p.preis, p.waehrung, p.ab_menge
                FROM verkaufspreise AS p
                WHERE p.art = 'kunde' AND p.adresse = 0 AND p.geloescht = 0
                    AND (p.gueltig_bis = '0000-00-00' OR curdate() <= p.gueltig_bis OR isnull(gueltig_bis))
                    AND (p.gueltig_ab = '0000-00-00' OR curdate() >= p.gueltig_ab OR isnull(gueltig_ab))
                    AND p.artikel = %s
                ORDER BY p.ab_menge",
            $articleId
        );

        $prices = $this->app->DB->SelectArr($sql);

        $dbError = $this->app->DB->error();
        if (!empty($dbError)) {
            //this is a noncritical error, the cronjob can still continue, just log the error message
            $this->app->erp->LogFile('BMECAT_export price query failed', $this->app->DB->real_escape_string($sql));
            $this->app->erp->LogFile('BMECAT_export DB error', $this->app->DB->real_escape_string($dbError));
        }

        if (empty($prices)) {
            return [];
        }

        return $prices;
    }

    /**
     * Prints log message to logfile table
     * (only if debug mode is enabled)
     *
     * @param string     $message
     * @param mixed|null $dump
     */
    private function debug($message, $dump = null)
    {
        if ($this->debugMode === true) {
            $this->app->erp->LogFile($message, $dump);
        }
    }
}
Beispiel: BMEcat-Export zum Update veränderter Artikel
Das folgende Beispiel zeigt eine Routine zur Erstellung eines Dateianhangs an einen Beleg. Das Code-Beispiel registriert sich auf den Trigger beim Ändern eines Beleges und erstellt eine Datei, die dann als Anhang an den Beleg gehängt werden kann.
In Xentral existieren dazu z.B. zusätzliche Einzahlungsscheine bei Rechnungserstellung oder Gefahrgutdeklarationen, die nach diesem Muster im Systen verankert wurden. Denkbar ist aber jede Art von Dateianhang.
Solange die Datei als Typ Anhang hinterlegt wird, kann diese durch eine geeignete Konfiguration in den Projekteinstellungen automatisch mit der Rechnung gesendet oder beim Rechnungedruck mit ausgedruckt werden.

Integration in Xentral

  1. Die Datei muss im Ordner www/pages gespeichert werden
  2. Der Dateiname muss aus Kleinbuchstaben bestehen
  3. Der Klassenname muss mit einem Großbuchstaben beginnen, der Rest sind Kleinbuchstaben
  4. Wenn die Datei am Zielort liegt muss sie einmal aufgerufen werden damit der Hook registriert wird - in unserem Fall also: index.php?module=beispiel
  5. Auf der Seite selbst haben wir im Beispiel noch eine Checkbox eingebaut, mit der man die hinterlegte Logik an- und ausschalten kann. Beim ersten Aufruf muss man einmalig den Haken setzen. Dieser wird automatisch gespeichert - daher ist kein Speichern-Button nötig

Beispiel-Code


<?php

// TODO Wichtig! Nur der erste Buchstabe gross, kein CamelCase; 
// Der Name der Datei selbst muss klein geschrieben sein, also hier beispiel.php
class Beispiel
{

  /** @var erpooSystem $app */
  public $app;

  /**
   * @param erpooSystem $app
   * @param bool $intern
   */
  public function __construct($app, $intern = false)
  {
    $this->app = $app;
    if($intern){
      return;
    }

    $this->app->ActionHandlerInit($this);
    $this->app->ActionHandler('list','Liste');
    $this->app->DefaultActionHandler('list');
    $this->app->ActionHandlerListen($app);
  }

  public function Liste()
  {
    $this->Install();
    $this->app->YUI->AutoSaveKonfiguration('belegeanhangerzeugen', 'belegeanhangerzeugen');

    $belegeanhangerzeugen = $this->app->erp->GetKonfiguration('belegeanhangerzeugen');

    $checked = '';
    if(!empty($belegeanhangerzeugen)){
      $checked = 'checked=""';
    }
    $this->app->Tpl->Set('TAB1','Automatisch eine Anhangsdatei für Belege erzeugen:
     <input type="checkbox" name="belegeanhangerzeugen" id="belegeanhangerzeugen" '.$checked.'>' );

    $this->app->Tpl->Parse('PAGE','tabview.tpl');
  }


  public function Install()
  {
    $modulName = 'beispiel'; // TODO frei vergebbar, sollte nach dem ersten Mal aber nicht mehr verändert werden; z.b. Klassenname mit Kleinbuchstaben
    $this->app->erp->RegisterHook('ANABREGSNeuberechnenEnde', $modulName, 'createDocument');
  }

  public function createFile($tmpPath,$documentId, $documentType)
  {
    $belegId = (int)$documentId;
    $beleg = $this->app->DB->SelectRow(
      "SELECT *
      FROM {$documentType} AS r
      WHERE r.id = '{$belegId}'"
    );
    if (empty($beleg)) {
      return;
    }
    
    // TODO
    /*
     *  Hier wird die eigentliche Datei im z.b. temp-Verzeichnis erzeugt
     *
     */
  }

  public function createDocument($documentId, $documentType)
  {
    if(
      $documentType == 'auftrag' ||
      empty($documentId) ||
      $this->app->erp->GetKonfiguration('belegeanhangerzeugen')!=='on'
    ){
      return;
    }

    $tmpPath = '';  // TODO z.b. tempnam(sys_get_temp_dir(), 'beleg') . '.csv'
    $this->createFile($tmpPath,$documentId, $documentType); // TODO
    $fileTitle = ''; // TODO
    $fileDesc = '';  // TODO
    $fileName = '';  // TODO
    $fileHash = '';  // TODO z.b. md5(serialize($belegDatenObjekt));
    $bearbeiter = $this->app->User->GetName();

    // Prüfen ob vorher schon mal eine Datei zu Beleg generiert wurde
    $dateiCheck = $this->app->DB->SelectArr(sprintf(
      "SELECT d.id AS datei_id, dv.bemerkung AS datei_hash
		   FROM datei AS d
		   INNER JOIN datei_stichwoerter AS ds ON d.id = ds.datei
		   INNER JOIN datei_version AS dv ON d.id = dv.datei
		   WHERE ds.subjekt = 'anhang' AND ds.objekt = '%s'
			 AND d.titel = '%s' AND ds.parameter = '%s'
			 AND d.geloescht = 0
		   ORDER BY dv.id DESC
		   LIMIT 1",
      $documentType, $fileTitle, $documentId
    ));
    $dateiId = (int)$dateiCheck[0]['datei_id'];
    $dateiHash = $dateiCheck[0]['datei_hash'];

    if($dateiId > 0){
      // Beleg ist vorhanden > Prüfen ob sich der Inhalt geändert hat
      if($dateiHash !== $fileHash){
        // Hash in Bemerkungsfeld hat sich geändert > Neue Datei-Version hinterlegen
        // Hash stellt sicher dass Inhalte identisch sind.
        $this->app->erp->AddDateiVersion($dateiId, $bearbeiter, $fileName, $fileHash, $tmpPath);
      }
    }
    else{
      // Datei nicht vorhanden > Datei als Anhang zum Beleg anlegen
      $dateiId = $this->app->erp->CreateDatei($fileName, $fileTitle, $fileDesc, '', $tmpPath, $bearbeiter);
      $this->app->erp->AddDateiStichwort($dateiId, 'anhang', $documentType, $documentId);
    }
  }
}

Variablen
Innerhalb des Prozesses sind schon einige Variablen gesetzt, die nützliche Informationen und Funktionen liefern.
  • $app: Zugang zu verschiedenen Services
  • $app->DB Klasse DB: Zugriffe auf die Xentral Datenbank. (Wird häufig benötigt)
  • $app->Conf Klasse Config: Zugriff auf Datenbankverbindungsdaten und `userdata` Verzeichnis. (z.B. um Ergebnisdateien dort zu speichern)
  • $app->Container Klasse ServiceContainer: Zugriff auf weitere Services. (Wie z.B. FileSystem)
  • $app->erp Klasse erpAPI: Zugriff auf alle funktionen der erpAPI. (Hier finden Sie vorgefertigte Datenbankabfragen und Hilfsfunktionen)
  • $app->mail Klasse PHPMailer: Zugriff auf E-Mail Funktionen. (Nützlich um z.B. nach erfolgreicher Ausführung eine Benachrichtigung per E-Mail an den Administrator zu schicken)
Best Practice, Tipps, Tricks
  • Erstellen Sie eine Klasse für die Programmlogik ihres Prozessstarters und halten Sie den prozeduralen Code so klein wie möglich.
  • Mit der Funktion $app->erp->LogFile() werden Lognachrichten in der Tabelle Logfile erzeugt.
  • Setzen Sie eine Variable, mit der sie die Ausgabe detaillierter Lognachrichten an- und ausschalten können um im Live-Betrieb später Ressourcen zu schonen.
  • Wird eine Exception geworfen, die der Prozessstarter nicht selbst behandelt, so wird der Prozessstarter in der Übersicht als fehlerhaft markiert und in der Tabelle logfile kann die Fehlermeldung eingesehen werden. Beispiel Fehler im Logfile: Prozessstarter Fehler bei Aufruf des Moduls bmecatexportarticles: Cant write XML file
  • Diese Mechanik können Sie ausnutzen und eigene Exceptions werfen wenn z.B. wichtige Bedingungen für den Prozess nicht erfüllt sind oder ein kritischer Fehler auftritt.

Weitere Links: Wir freuen uns auch auf Ideen, Anregungen und Code-Beispiele in unserem Forum: https://forum.xentral.biz
War der Artikel hilfreich?
Vielen Dank für Ihr Feedback!