From c1e7e559ba54aaf2cea0b8a155f10d455ec4553b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Patrick=20M=C3=BCller?= <p.mueller@pcsg.de>
Date: Thu, 21 Jan 2021 16:26:14 +0100
Subject: [PATCH] feat: open items management (temp commit)

---
 .../OpenItemsList/getUserOpenItems.php        | 158 +++-
 ajax/backend/OpenItemsList/search.php         |  14 +-
 .../controls/OpenItems/OpenItems.Total.html   |  64 +-
 .../OpenItems/OpenItems.UserRecords.html      |   5 +
 bin/backend/controls/OpenItems/OpenItems.css  |  33 +-
 bin/backend/controls/OpenItems/OpenItems.js   | 789 ++++++++++++------
 events.xml                                    |   6 +
 locale.xml                                    |  95 +++
 settings.xml                                  |  23 +
 src/QUI/ERP/Customer/OpenItemsList/Events.php | 118 +++
 .../ERP/Customer/OpenItemsList/Handler.php    | 199 ++++-
 src/QUI/ERP/Customer/OpenItemsList/Item.php   |  12 +
 12 files changed, 1196 insertions(+), 320 deletions(-)
 create mode 100644 bin/backend/controls/OpenItems/OpenItems.UserRecords.html
 create mode 100644 src/QUI/ERP/Customer/OpenItemsList/Events.php

diff --git a/ajax/backend/OpenItemsList/getUserOpenItems.php b/ajax/backend/OpenItemsList/getUserOpenItems.php
index 71bb7aa..7c2a26b 100644
--- a/ajax/backend/OpenItemsList/getUserOpenItems.php
+++ b/ajax/backend/OpenItemsList/getUserOpenItems.php
@@ -1,8 +1,8 @@
 <?php
 
-use QUI\Utils\Grid;
 use QUI\Utils\Security\Orthos;
 use QUI\ERP\Customer\OpenItemsList\Handler;
+use QUI\Cache\Manager as QUICacheManager;
 
 /**
  * Search open items list
@@ -12,12 +12,151 @@
  */
 QUI::$Ajax->registerFunction(
     'package_quiqqer_customer_ajax_backend_OpenItemsList_getUserOpenItems',
-    function ($userId) {
+    function ($userId, $searchParams, $forceRefresh) {
         try {
-            $OpenItemsList = Handler::getOpenItemsList(QUI::getUsers()->get((int)$userId));
-            $items         = [];
+            $userId    = (int)$userId;
+            $cacheName = 'quiqqer/customer/openitems/'.$userId;
+            $refresh   = true;
 
-            foreach ($OpenItemsList->getItems() as $Item) {
+            if (empty($forceRefresh)) {
+                try {
+                    $openItems = QUICacheManager::get($cacheName);
+                    $refresh   = false;
+                } catch (\Exception $Exception) {
+                    // nothing - refresh cache
+                }
+            }
+
+            if ($refresh) {
+                $OpenItemsList = Handler::getOpenItemsList(QUI::getUsers()->get($userId));
+                $openItems     = $OpenItemsList->getItems();
+
+                QUICacheManager::set($cacheName, $openItems);
+            }
+
+            $searchParams = Orthos::clearArray(\json_decode($searchParams, true));
+
+            // Filter
+            if (!empty($searchParams['search'])) {
+                $search = \trim($searchParams['search']);
+
+                $openItems = \array_filter($openItems, function ($Item) use ($search) {
+                    return \mb_strpos($Item->getDocumentNo(), $search) !== false;
+                });
+            }
+
+            // Sort
+            if (!empty($searchParams['sortOn'])) {
+                $sortOn = $searchParams['sortOn'];
+            } else {
+                $sortOn = 'date';
+            }
+
+            $sortBy = 'DESC';
+
+            if (!empty($searchParams['sortBy'])) {
+                switch (\mb_strtoupper($searchParams['sortBy'])) {
+                    case 'ASC':
+                    case 'DESC':
+                        $sortBy = $searchParams['sortBy'];
+                        break;
+                }
+            }
+
+            \usort($openItems, function ($ItemA, $ItemB) use ($sortOn, $sortBy) {
+                /**
+                 * @var \QUI\ERP\Customer\OpenItemsList\Item $ItemA
+                 * @var \QUI\ERP\Customer\OpenItemsList\Item $ItemB
+                 */
+                switch ($sortOn) {
+                    case 'documentNo':
+                        $valA = (int)\preg_replace('#[^\d]#i', '', $ItemA->getDocumentNo());
+                        $valB = (int)\preg_replace('#[^\d]#i', '', $ItemB->getDocumentNo());
+                        break;
+
+                    case 'documentType':
+                        $valA = $ItemA->getDocumentType();
+                        $valB = $ItemB->getDocumentType();
+                        break;
+
+                    case 'dueDate':
+                        $valA = $ItemA->getDueDate();
+                        $valB = $ItemB->getDueDate();
+                        break;
+
+                    case 'net':
+                        $valA = $ItemA->getAmountTotalNet();
+                        $valB = $ItemB->getAmountTotalNet();
+                        break;
+
+                    case 'vat':
+                        $valA = $ItemA->getAmountTotalVat();
+                        $valB = $ItemB->getAmountTotalVat();
+                        break;
+
+                    case 'gross':
+                        $valA = $ItemA->getAmountTotalSum();
+                        $valB = $ItemB->getAmountTotalSum();
+                        break;
+
+                    case 'paid':
+                        $valA = $ItemA->getAmountPaid();
+                        $valB = $ItemB->getAmountPaid();
+                        break;
+
+                    case 'open':
+                        $valA = $ItemA->getAmountOpen();
+                        $valB = $ItemB->getAmountOpen();
+                        break;
+
+                    case 'dunningLevel':
+                        $valA = $ItemA->getDunningLevel();
+                        $valB = $ItemB->getDunningLevel();
+                        break;
+
+                    case 'daysDue':
+                        $valA = $ItemA->getDaysDue();
+                        $valB = $ItemB->getDaysDue();
+                        break;
+
+                    default:
+                        $valA = $ItemA->getDate();
+                        $valB = $ItemB->getDate();
+                }
+
+                if ($valA === $valB) {
+                    return 0;
+                }
+
+                if ($sortBy === 'ASC') {
+                    return $valA < $valB ? -1 : 1;
+                } else {
+                    return $valA < $valB ? 1 : -1;
+                }
+            });
+
+            // Pagination
+            $page = 1;
+
+            if (!empty($searchParams['page'])) {
+                $page = (int)$searchParams['page'];
+            }
+
+            $perPage = 10;
+
+            if (!empty($searchParams['perPage'])) {
+                $perPage = (int)$searchParams['perPage'];
+            }
+
+            $offset     = ($page - 1) * $perPage;
+            $totalCount = \count($openItems);
+            $openItems  = \array_splice($openItems, $offset, $perPage);
+
+            // Parse data for GRID display
+            $items = [];
+
+            /** @var \QUI\ERP\Customer\OpenItemsList\Item $Item */
+            foreach ($openItems as $Item) {
                 $documentType      = $Item->getDocumentType();
                 $documentTypeTitle = QUI::getLocale()->get(
                     'quiqqer/customer',
@@ -35,14 +174,15 @@ function ($userId) {
                     'gross'             => $Item->getAmountTotalSumFormatted(),
                     'paid'              => $Item->getAmountPaidFormatted(),
                     'open'              => $Item->getAmountOpenFormatted(),
-                    'dunningLevel'      => $Item->getDunningLevel()
+                    'dunningLevel'      => $Item->getDunningLevel() ?: '-',
+                    'daysDue'           => $Item->getDaysDue()
                 ];
             }
 
             return [
                 'data'  => $items,
-                'page'  => 1,
-                'total' => \count($items)
+                'page'  => $page,
+                'total' => $totalCount
             ];
         } catch (\Exception $Exception) {
             QUI\System\Log::writeException($Exception);
@@ -54,6 +194,6 @@ function ($userId) {
             ];
         }
     },
-    ['userId'],
+    ['userId', 'searchParams', 'forceRefresh'],
     ['Permission::checkAdminUser', Handler::PERMISSION_OPENITEMS_VIEW]
 );
diff --git a/ajax/backend/OpenItemsList/search.php b/ajax/backend/OpenItemsList/search.php
index 43b1f2e..8f91d1f 100644
--- a/ajax/backend/OpenItemsList/search.php
+++ b/ajax/backend/OpenItemsList/search.php
@@ -23,16 +23,26 @@ function ($searchParams) {
 
             $Grid = new Grid($searchParams);
 
+            if (!empty($searchParams['currency'])) {
+                $Currency = new \QUI\ERP\Currency\Currency($searchParams['currency']);
+            } else {
+                $Currency = \QUI\ERP\Currency\Handler::getDefaultCurrency();
+            }
+
             return [
                 'grid'   => $Grid->parseResult($result, $count),
-                'totals' => [] // @todo
+                'totals' => Handler::getTotals($result, $Currency)
             ];
         } catch (\Exception $Exception) {
             QUI\System\Log::writeException($Exception);
 
             return [
                 'grid'   => [],
-                'totals' => []
+                'totals' => [
+                    'display_gross_toPay' => '',
+                    'display_gross_paid'  => '',
+                    'display_gross_total' => ''
+                ]
             ];
         }
     },
diff --git a/bin/backend/controls/OpenItems/OpenItems.Total.html b/bin/backend/controls/OpenItems/OpenItems.Total.html
index aa376c4..3f80748 100644
--- a/bin/backend/controls/OpenItems/OpenItems.Total.html
+++ b/bin/backend/controls/OpenItems/OpenItems.Total.html
@@ -1,24 +1,40 @@
-<header>
-    <div>&nbsp;</div>
-    <div>Offen</div>
-    <div>Bezahlt</div>
-    <div>Gesamt</div>
-</header>
-<div class="quiqqer-customer-openitems-total-row">
-    <div class="quiqqer-customer-openitems-total-title">Netto</div>
-    <div class="quiqqer-customer-openitems-total-netto-open">{{display_netto_toPay}}</div>
-    <div class="quiqqer-customer-openitems-total-netto-paid">{{display_netto_paid}}</div>
-    <div class="quiqqer-customer-openitems-total-netto-total">{{display_netto_total}}</div>
-</div>
-<div class="quiqqer-customer-openitems-total-row">
-    <div class="quiqqer-customer-openitems-total-title">MwSt</div>
-    <div class="quiqqer-customer-openitems-total-open">{{display_vat_toPay}}</div>
-    <div class="quiqqer-customer-openitems-total-paid">{{display_vat_paid}}</div>
-    <div class="quiqqer-customer-openitems-total-total">{{display_vat_total}}</div>
-</div>
-<div class="quiqqer-customer-openitems-total-row">
-    <div class="quiqqer-customer-openitems-total-brutto-title">Brutto</div>
-    <div class="quiqqer-customer-openitems-total-brutto-open">{{display_brutto_toPay}}</div>
-    <div class="quiqqer-customer-openitems-total-brutto-paid">{{display_brutto_paid}}</div>
-    <div class="quiqqer-customer-openitems-total-brutto-total">{{display_brutto_total}}</div>
-</div>
+<table class="quiqqer-customer-openitems-totals-tbl">
+    <thead>
+    <tr>
+        <th>
+            {{headerNet}}
+        </th>
+        <th>
+            {{headerVat}}
+        </th>
+        <th>
+            {{headerGross}}
+        </th>
+        <th>
+            {{headerPaid}}
+        </th>
+        <th>
+            {{headerOpen}}
+        </th>
+    </tr>
+    </thead>
+    <tbody>
+    <tr>
+        <td>
+            {{display_net}}
+        </td>
+        <td>
+            {{display_vat}}
+        </td>
+        <td>
+            {{display_gross}}
+        </td>
+        <td>
+            {{display_paid}}
+        </td>
+        <td>
+            {{display_open}}
+        </td>
+    </tr>
+    </tbody>
+</table>
diff --git a/bin/backend/controls/OpenItems/OpenItems.UserRecords.html b/bin/backend/controls/OpenItems/OpenItems.UserRecords.html
new file mode 100644
index 0000000..5d447bd
--- /dev/null
+++ b/bin/backend/controls/OpenItems/OpenItems.UserRecords.html
@@ -0,0 +1,5 @@
+<div class="quiqqer-customer-openitems-userrecords-search">
+    <input type="search" placeholder="{{placeholderSearch}}"/>
+</div>
+<div class="quiqqer-customer-openitems-userrecords-list">
+</div>
\ No newline at end of file
diff --git a/bin/backend/controls/OpenItems/OpenItems.css b/bin/backend/controls/OpenItems/OpenItems.css
index a57f5a9..0f302dc 100644
--- a/bin/backend/controls/OpenItems/OpenItems.css
+++ b/bin/backend/controls/OpenItems/OpenItems.css
@@ -175,6 +175,23 @@
     background: #eaeff4;
 }
 
+.openItems-total {
+    background-color: #efefef;
+    height: 80px;
+    padding: 20px;
+    text-align: right;
+    width: 100%;
+}
+
+.openItems-total th {
+    text-align: right;
+    width: 20%;
+}
+
+.quiqqer-customer-openitems-totals-tbl {
+    width: 100%;
+}
+
 .payment-status-amountCell {
     text-align: right !important;
     padding-right: 5px;
@@ -183,14 +200,22 @@
 
 /** Open items grid
  ================================================= */
-.quiqqer-customer-openitems-details {
-    height: 500px;
+.quiqqer-customer-openitems-userrecords-list {
+    height: 400px;
 }
 
-.quiqqer-customer-openitems-details div.bDiv > ul {
+.quiqqer-customer-openitems-userrecords-list div.bDiv > ul {
     padding: 0 !important;
 }
 
-.quiqqer-customer-openitems-details div.bDiv > ul > li {
+.quiqqer-customer-openitems-userrecords-list div.bDiv > ul > li {
     overflow: hidden !important;
+}
+
+.quiqqer-customer-openitems-userrecords-search {
+    background-color: #efefef;
+    height: 50px;
+    padding: 10px;
+    text-align: right;
+    width: 100%;
 }
\ No newline at end of file
diff --git a/bin/backend/controls/OpenItems/OpenItems.js b/bin/backend/controls/OpenItems/OpenItems.js
index fc93994..a3e6976 100644
--- a/bin/backend/controls/OpenItems/OpenItems.js
+++ b/bin/backend/controls/OpenItems/OpenItems.js
@@ -11,20 +11,22 @@ define('package/quiqqer/customer/bin/backend/controls/OpenItems/OpenItems', [
     'qui/controls/desktop/Panel',
     'qui/controls/buttons/Button',
     'qui/controls/buttons/Separator',
-    'qui/controls/buttons/Select',
     'qui/controls/contextmenu/Item',
     'controls/grid/Grid',
 
+    'package/quiqqer/payment-transactions/bin/backend/controls/IncomingPayments/AddPaymentWindow',
+
     'Locale',
     'Ajax',
     'Mustache',
 
     'text!package/quiqqer/customer/bin/backend/controls/OpenItems/OpenItems.Total.html',
+    'text!package/quiqqer/customer/bin/backend/controls/OpenItems/OpenItems.UserRecords.html',
     'css!package/quiqqer/customer/bin/backend/controls/OpenItems/OpenItems.css',
     'css!package/quiqqer/erp/bin/backend/payment-status.css'
 
-], function (QUI, QUIPanel, QUIButton, QUISeparator, QUISelect, QUIContextMenuItem, Grid,
-             QUILocale, QUIAjax, Mustache, templateTotal) {
+], function (QUI, QUIPanel, QUIButton, QUISeparator, QUIContextMenuItem, Grid, AddPaymentWindow,
+             QUILocale, QUIAjax, Mustache, templateTotal, templateUserRecords) {
     "use strict";
 
     var lg = 'quiqqer/customer';
@@ -45,11 +47,16 @@ define('package/quiqqer/customer/bin/backend/controls/OpenItems/OpenItems', [
             '$onInject',
             '$onProcessChange',
             '$refreshButtonStatus',
-            '$onPDFExportButtonClick',
+            '$onClickShowOpenItemsList',
             '$onClickCopyProcess',
-            '$onClickOpenItemsDetails',
+            '$onClickOpenUserRecords',
             '$onClickOpenProcess',
-            '$onSearchKeyUp'
+            '$onSearchKeyUp',
+            '$refreshUserRecords',
+            '$refreshUserRecordsButtons',
+            '$onClickAddTransaction',
+            '$onClickOpenDocument',
+            '$onUserRecordsSearchKeyUp'
         ],
 
         initialize: function (options) {
@@ -70,7 +77,10 @@ define('package/quiqqer/customer/bin/backend/controls/OpenItems/OpenItems', [
             this.$periodFilter  = null;
             this.$loaded        = false;
 
-            this.$GridDetails = null;
+            this.$GridDetails              = null;
+            this.$currentRecordsUserId     = false;
+            this.$UserRecordsSearch        = null;
+            this.$currentUserRecordsSearch = '';
 
             this.addEvents({
                 onCreate: this.$onCreate,
@@ -92,41 +102,19 @@ define('package/quiqqer/customer/bin/backend/controls/OpenItems/OpenItems', [
                 return;
             }
 
-            var status = '';
-            var from   = '',
-                to     = '';
-
             this.$currentSearch = this.$Search.value;
 
-            if (this.$currentSearch !== '') {
-                this.disableFilter();
-            } else {
-                this.enableFilter();
-            }
-
             var sortOn = this.$Grid.options.sortOn;
 
-            switch (sortOn) {
-                case 'supplier_name':
-                case 'display_vatsum':
-                case 'display_paid':
-                case 'display_toPay':
-                    sortOn = false;
-                    break;
-            }
+            this.showTotal();
 
             this.$search({
-                perPage: this.$Grid.options.perPage,
-                page   : this.$Grid.options.page,
-                sortBy : this.$Grid.options.sortBy,
-                sortOn : sortOn,
-                search : this.$currentSearch,
-                filters: {
-                    from    : from,
-                    to      : to,
-                    status  : status,
-                    currency: this.$Currency.getAttribute('value')
-                }
+                perPage : this.$Grid.options.perPage,
+                page    : this.$Grid.options.page,
+                sortBy  : this.$Grid.options.sortBy,
+                sortOn  : sortOn,
+                search  : this.$currentSearch,
+                currency: this.$Currency.getAttribute('value')
             }).then(function (result) {
                 result.grid.data = result.grid.data.map(function (entry) {
                     return self.$parseGridRow(entry);
@@ -137,9 +125,47 @@ define('package/quiqqer/customer/bin/backend/controls/OpenItems/OpenItems', [
 
                 this.$Total.set(
                     'html',
-                    Mustache.render(templateTotal, result.total)
+                    Mustache.render(templateTotal, Object.merge({}, result.totals, {
+                        headerNet  : QUILocale.get(lg, 'panel.OpenItems.grid.net'),
+                        headerVat  : QUILocale.get(lg, 'panel.OpenItems.grid.vat'),
+                        headerGross: QUILocale.get(lg, 'panel.OpenItems.grid.gross'),
+                        headerPaid : QUILocale.get(lg, 'panel.OpenItems.grid.paid'),
+                        headerOpen : QUILocale.get(lg, 'panel.OpenItems.grid.open')
+                    }))
                 );
 
+                this.$currentRecordsUserId = null;
+
+                this.Loader.hide();
+            }.bind(this)).catch(function (err) {
+                console.error(err);
+                this.Loader.hide();
+            }.bind(this));
+        },
+
+        /**
+         * Refresh grid entry for a specific user
+         *
+         * @param {Number} userId
+         * @return {Promise}
+         */
+        $refreshUserEntry: function (userId) {
+            var self = this;
+
+            return this.$search({
+                userId: userId
+            }).then(function (result) {
+                var entries = self.$Grid.getData();
+
+                for (var i = 0, len = entries.length; i < len; i++) {
+                    var Entry = entries[i];
+
+                    if (Entry.userId === userId) {
+                        self.$Grid.setDataByRow(i, self.$parseGridRow(result.grid.data[0]));
+                        break;
+                    }
+                }
+
                 this.Loader.hide();
             }.bind(this)).catch(function (err) {
                 console.error(err);
@@ -160,7 +186,7 @@ define('package/quiqqer/customer/bin/backend/controls/OpenItems/OpenItems', [
                 buttons  = this.$Grid.getButtons();
 
             var PDF = buttons.filter(function (Button) {
-                return Button.getAttribute('name') === 'printPdf';
+                return Button.getAttribute('name') === 'showOpenItemsList';
             })[0];
 
             if (selected.length) {
@@ -242,30 +268,22 @@ define('package/quiqqer/customer/bin/backend/controls/OpenItems/OpenItems', [
                 pagination           : true,
                 serverSort           : true,
                 accordion            : true,
-                autoSectionToggle    : false,
+                autoSectionToggle    : true,
                 openAccordionOnClick : false,
-                toggleiconTitle      : '',
-                accordionLiveRenderer: this.$onClickOpenItemsDetails,
+                toggleiconTitle      : QUILocale.get(lg, 'panels.OpenItems.grid.toggle_title'),
+                accordionLiveRenderer: this.$onClickOpenUserRecords,
                 exportData           : true,
                 exportTypes          : {
                     csv : 'CSV',
                     json: 'JSON'
                 },
                 buttons              : [{
-                    name     : 'addTransaction',
-                    text     : QUILocale.get(lg, 'panels.OpenItems.btn.addTransaction'),
-                    textimage: 'fa fa-file-o',
-                    disabled : true,
-                    events   : {
-                        onClick: this.$onClickOpenProcess
-                    }
-                }, {
-                    name     : 'printPdf',
-                    text     : QUILocale.get(lg, 'panels.OpenItems.btn.pdf'),
+                    name     : 'showOpenItemsList',
+                    text     : QUILocale.get(lg, 'panels.OpenItems.btn.showOpenItemsList'),
                     textimage: 'fa fa-print',
                     disabled : true,
                     events   : {
-                        onClick: this.$onPDFExportButtonClick
+                        onClick: this.$onClickShowOpenItemsList
                     }
                 }],
                 columnModel          : [{
@@ -286,45 +304,53 @@ define('package/quiqqer/customer/bin/backend/controls/OpenItems/OpenItems', [
                     dataType : 'integer',
                     width    : 200
                 }, {
-                    header   : QUILocale.get(lg, 'panel.OpenItems.grid.netSum'),
+                    header   : QUILocale.get(lg, 'panel.OpenItems.grid.openItemsCount'),
+                    dataIndex: 'open_items_count',
+                    dataType : 'integer',
+                    width    : 100
+                }, {
+                    header   : QUILocale.get(lg, 'panel.OpenItems.grid.net'),
                     dataIndex: 'display_net_sum',
                     dataType : 'string',
                     width    : 100,
                     className: 'payment-status-amountCell'
                 }, {
-                    header   : QUILocale.get(lg, 'panel.OpenItems.grid.vatSum'),
+                    header   : QUILocale.get(lg, 'panel.OpenItems.grid.vat'),
                     dataIndex: 'display_vat_sum',
                     dataType : 'string',
                     width    : 100,
                     className: 'payment-status-amountCell'
                 }, {
-                    header   : QUILocale.get(lg, 'panel.OpenItems.grid.totalSum'),
+                    header   : QUILocale.get(lg, 'panel.OpenItems.grid.gross'),
                     dataIndex: 'display_total_sum',
                     dataType : 'string',
                     width    : 100,
                     className: 'payment-status-amountCell'
                 }, {
-                    header   : QUILocale.get(lg, 'panel.OpenItems.grid.paidSum'),
+                    header   : QUILocale.get(lg, 'panel.OpenItems.grid.paid'),
                     dataIndex: 'display_paid_sum',
                     dataType : 'string',
                     width    : 100,
                     className: 'payment-status-amountCell'
                 }, {
-                    header   : QUILocale.get(lg, 'panel.OpenItems.grid.openSum'),
+                    header   : QUILocale.get(lg, 'panel.OpenItems.grid.open'),
                     dataIndex: 'display_open_sum',
                     dataType : 'string',
                     width    : 100,
                     className: 'payment-status-amountCell'
+                }, {
+                    dataIndex: 'userId',
+                    dataType : 'integer',
+                    hidden   : true
                 }]
             });
 
             this.$Grid.addEvents({
                 onRefresh : this.refresh,
                 onClick   : this.$refreshButtonStatus,
-                onDblClick: this.$onClickOpenProcess
+                onDblClick: this.$onClickShowOpenItemsList
             });
 
-
             this.$Total = new Element('div', {
                 'class': 'openItems-total'
             }).inject(this.getContent());
@@ -346,7 +372,7 @@ define('package/quiqqer/customer/bin/backend/controls/OpenItems/OpenItems', [
 
             var size = Body.getSize();
 
-            this.$Grid.setHeight(size.y - 20);
+            this.$Grid.setHeight(size.y - 110);
             this.$Grid.setWidth(size.x - 20);
         },
 
@@ -387,6 +413,8 @@ define('package/quiqqer/customer/bin/backend/controls/OpenItems/OpenItems', [
                 self.$Currency.enable();
                 self.$Currency.setAttribute('value', currency.code);
                 self.$Currency.setAttribute('text', currency.code);
+
+                self.refresh();
             }, {
                 'package': 'quiqqer/currency'
             });
@@ -396,218 +424,33 @@ define('package/quiqqer/customer/bin/backend/controls/OpenItems/OpenItems', [
             });
         },
 
-        /**
-         * event: on panel destroy
-         */
-        $onDestroy: function () {
-            Processes.removeEvents({
-                onPostProcess: this.$onProcessChange
-            });
-        },
-
-        /**
-         * event: A change was made to a purchasing process
-         *
-         * Update single processes in table without refreshing everything
-         *
-         * @param {Object} ProcessesHandler
-         * @param {Number|Array} processIds
-         */
-        $onProcessChange: function (ProcessesHandler, processIds) {
-            if (!this.$Grid) {
-                return;
-            }
-
-            var self    = this,
-                rows    = this.$Grid.getData(),
-                IdToRow = {};
-
-            var i, j, len, jlen, processId;
-
-            if (typeof processIds === 'string' || typeof processIds === 'number') {
-                processIds = [processIds];
-            }
-
-            for (i = 0, len = processIds.length; i < len; i++) {
-                processId = processIds[i];
-
-                for (j = 0, jlen = rows.length; j < jlen; j++) {
-                    if (rows[j].id == processId) {
-                        IdToRow[processId] = j;
-                        break;
-                    }
-                }
-            }
-
-            if (!Object.getLength(IdToRow)) {
-                return;
-            }
-
-            return ProcessesHandler.getProcessList({
-                ids: processIds,
-            }).then(function (result) {
-                for (i = 0, len = result.grid.data.length; i < len; i++) {
-                    var processId = result.grid.data[i].id;
-
-                    if (processId in IdToRow) {
-                        self.$Grid.setDataByRow(IdToRow[processId], self.$parseGridRow(result.grid.data[i]));
-                    }
-                }
-            });
-        },
-
         //region Buttons events
 
         /**
          * event : on PDF Export button click
          */
-        $onPDFExportButtonClick: function (Button) {
+        $onClickShowOpenItemsList: function (Button) {
+            var self         = this;
             var selectedData = this.$Grid.getSelectedData();
 
             if (!selectedData.length) {
                 return;
             }
 
-            Button.setAttribute('textimage', 'fa fa-spinner fa-spin');
+            this.Loader.show();
 
-            return new Promise(function (resolve) {
-                require([
-                    'package/quiqqer/erp/bin/backend/controls/OutputDialog'
-                ], function (OutputDialog) {
-                    new OutputDialog({
-                        entityId  : selectedData[0].id,
-                        entityType: 'PurchasingProcess',
-                        events    : {
-                            onOpen: function (SubmitData) {
-                                Button.setAttribute('textimage', 'fa fa-print');
-                                resolve();
-                            }
+            require([
+                'package/quiqqer/erp/bin/backend/controls/OutputDialog'
+            ], function (OutputDialog) {
+                new OutputDialog({
+                    entityId  : selectedData[0].userId,
+                    entityType: 'OpenItemsList',
+                    events    : {
+                        onOpen: function () {
+                            self.Loader.hide();
                         }
-                    }).open();
-                });
-            });
-        },
-
-        /**
-         * Create a copy of a process as a draft
-         */
-        $onClickCopyProcess: function () {
-            var self     = this,
-                selected = this.$Grid.getSelectedData();
-
-            if (!selected.length) {
-                return Promise.resolve(false);
-            }
-
-            DialogUtils.openCopyDialog(selected[0].id_str).then(function (newId) {
-                ProcessPanels.openProcessDraft(newId);
-            });
-        },
-
-        /**
-         * Open the accordion details of the open items
-         *
-         * @param {Object} data
-         */
-        $onClickOpenItemsDetails: function (data) {
-            var self       = this,
-                Row        = self.$Grid.getDataByRow(data.row),
-                ParentNode = data.parent;
-
-            ParentNode.setStyle('padding', 10);
-            //ParentNode.set('html', '<div class="fa fa-spinner fa-spin"></div>');
-
-            ParentNode.addClass('quiqqer-customer-openitems-details');
-
-            this.$GridDetails = new Grid(ParentNode, {
-                pagination          : true,
-                serverSort          : false,
-                accordion           : false,
-                autoSectionToggle   : false,
-                openAccordionOnClick: false,
-                toggleiconTitle     : '',
-                // @todo Export aktivieren?
-                //exportData          : true,
-                //exportTypes         : {
-                //    csv : 'CSV',
-                //    json: 'JSON'
-                //},
-                buttons    : [{
-                    name     : 'addTransaction',
-                    text     : QUILocale.get(lg, 'panels.OpenItems.btn.addTransaction'),
-                    textimage: 'fa fa-file-o',
-                    disabled : true,
-                    events   : {
-                        onClick: this.$onClickAddTransaction
                     }
-                }],
-                columnModel: [{
-                    header   : QUILocale.get(lg, 'panel.OpenItems.grid.details.userId'),
-                    dataIndex: 'date',
-                    dataType : 'string',
-                    width    : 150
-                }, {
-                    header   : QUILocale.get(lg, 'panel.OpenItems.grid.details.customerName'),
-                    dataIndex: 'documentTypeTitle',
-                    dataType : 'string',
-                    width    : 200
-                }, {
-                    header   : QUILocale.get(lg, 'panel.OpenItems.grid.details.customerName'),
-                    dataIndex: 'documentNo',
-                    dataType : 'string',
-                    width    : 200
-                }, {
-                    header   : QUILocale.get(lg, 'panel.OpenItems.grid.details.netSum'),
-                    dataIndex: 'net',
-                    dataType : 'string',
-                    width    : 100,
-                    className: 'payment-status-amountCell'
-                }, {
-                    header   : QUILocale.get(lg, 'panel.OpenItems.grid.details.vatSum'),
-                    dataIndex: 'vat',
-                    dataType : 'string',
-                    width    : 100,
-                    className: 'payment-status-amountCell'
-                }, {
-                    header   : QUILocale.get(lg, 'panel.OpenItems.grid.details.totalSum'),
-                    dataIndex: 'gross',
-                    dataType : 'string',
-                    width    : 100,
-                    className: 'payment-status-amountCell'
-                }, {
-                    header   : QUILocale.get(lg, 'panel.OpenItems.grid.details.paidSum'),
-                    dataIndex: 'paid',
-                    dataType : 'string',
-                    width    : 100,
-                    className: 'payment-status-amountCell'
-                }, {
-                    header   : QUILocale.get(lg, 'panel.OpenItems.grid.details.openSum'),
-                    dataIndex: 'open',
-                    dataType : 'string',
-                    width    : 100,
-                    className: 'payment-status-amountCell'
-                }]
-            });
-
-            this.$GridDetails.addEvents({
-                //onRefresh : this.refresh,
-                //onClick   : this.$refreshButtonStatus,
-                //onDblClick: this.$onClickOpenProcess
-            });
-
-            this.Loader.show();
-
-            this.$getUserOpenItems(Row.userId).then(function (result) {
-
-                console.log(result);
-
-                self.$GridDetails.setData(result);
-                self.Loader.hide();
-
-                var size = ParentNode.getSize();
-
-                self.$GridDetails.setHeight(size.y - 120);
-                self.$GridDetails.setWidth(size.x - 20);
+                }).open();
             });
         },
 
@@ -760,18 +603,6 @@ define('package/quiqqer/customer/bin/backend/controls/OpenItems/OpenItems', [
             });
         },
 
-        /**
-         * Disable the filter
-         */
-        disableFilter: function () {
-        },
-
-        /**
-         * Enable the filter
-         */
-        enableFilter: function () {
-        },
-
         /**
          * key up event at the search input
          *
@@ -853,20 +684,430 @@ define('package/quiqqer/customer/bin/backend/controls/OpenItems/OpenItems', [
             });
         },
 
+        // region User records
+
+        /**
+         * Open the accordion details of the open items
+         *
+         * @param {Object} data
+         */
+        $onClickOpenUserRecords: function (data) {
+            var self       = this,
+                Row        = self.$Grid.getDataByRow(data.row),
+                ParentNode = data.parent;
+
+            if (Row.userId === this.$currentRecordsUserId) {
+                return;
+            }
+
+            this.$currentRecordsUserId = Row.userId;
+
+            ParentNode.setStyle('padding', 10);
+            //ParentNode.set('html', '<div class="fa fa-spinner fa-spin"></div>');
+
+            ParentNode.addClass('quiqqer-customer-openitems-userrecords');
+            ParentNode.set('html', Mustache.render(templateUserRecords, {
+                placeholderSearch: QUILocale.get(lg, 'panels.OpenItems.details.tpl.placeholderSearch')
+            }));
+
+            this.$UserRecordsSearch = ParentNode.getElement('.quiqqer-customer-openitems-userrecords-search input');
+
+            this.$UserRecordsSearch.addEvents({
+                keyup : this.$onUserRecordsSearchKeyUp,
+                search: this.$onUserRecordsSearchKeyUp,
+                click : this.$onUserRecordsSearchKeyUp
+            });
+
+            new QUIButton({
+                name  : 'searchUserRecords',
+                icon  : 'fa fa-search',
+                styles: {
+                    float : 'right',
+                    margin: 0
+                },
+                events: {
+                    onClick: function () {
+                        self.$refreshUserRecords(self.$GridDetails);
+                    }
+                }
+            }).inject(
+                ParentNode.getElement('.quiqqer-customer-openitems-userrecords-search'),
+                'bottom'
+            );
+
+            var GridParent = ParentNode.getElement('.quiqqer-customer-openitems-userrecords-list');
+
+            if (this.$GridDetails) {
+                this.$GridDetails.destroy();
+            }
+
+            this.$GridDetails = new Grid(GridParent, {
+                pagination          : true,
+                serverSort          : true,
+                accordion           : false,
+                autoSectionToggle   : false,
+                openAccordionOnClick: false,
+                toggleiconTitle     : '',
+                // @todo Export aktivieren?
+                //exportData          : true,
+                //exportTypes         : {
+                //    csv : 'CSV',
+                //    json: 'JSON'
+                //},
+                buttons    : [{
+                    name     : 'open',
+                    text     : QUILocale.get(lg, 'panels.OpenItems.details.btn.open'),
+                    textimage: 'fa fa-file-o',
+                    disabled : true,
+                    events   : {
+                        onClick: this.$onClickOpenDocument
+                    }
+                }, {
+                    name     : 'addTransaction',
+                    text     : QUILocale.get(lg, 'panels.OpenItems.details.btn.addTransaction'),
+                    textimage: 'fa fa-money',
+                    disabled : true,
+                    events   : {
+                        onClick: this.$onClickAddTransaction
+                    }
+                }],
+                columnModel: [{
+                    header   : QUILocale.get(lg, 'panel.OpenItems.grid.date'),
+                    dataIndex: 'date',
+                    dataType : 'string',
+                    width    : 100
+                }, {
+                    header   : QUILocale.get(lg, 'panel.OpenItems.grid.details.documentNo'),
+                    dataIndex: 'documentNo',
+                    dataType : 'string',
+                    width    : 125
+                }, {
+                    header   : QUILocale.get(lg, 'panel.OpenItems.grid.details.documentType'),
+                    dataIndex: 'documentTypeTitle',
+                    dataType : 'string',
+                    width    : 85
+                }, {
+                    header   : QUILocale.get(lg, 'panel.OpenItems.grid.net'),
+                    dataIndex: 'net',
+                    dataType : 'string',
+                    width    : 100,
+                    className: 'payment-status-amountCell'
+                }, {
+                    header   : QUILocale.get(lg, 'panel.OpenItems.grid.vat'),
+                    dataIndex: 'vat',
+                    dataType : 'string',
+                    width    : 100,
+                    className: 'payment-status-amountCell'
+                }, {
+                    header   : QUILocale.get(lg, 'panel.OpenItems.grid.gross'),
+                    dataIndex: 'gross',
+                    dataType : 'string',
+                    width    : 100,
+                    className: 'payment-status-amountCell'
+                }, {
+                    header   : QUILocale.get(lg, 'panel.OpenItems.grid.paid'),
+                    dataIndex: 'paid',
+                    dataType : 'string',
+                    width    : 100,
+                    className: 'payment-status-amountCell'
+                }, {
+                    header   : QUILocale.get(lg, 'panel.OpenItems.grid.open'),
+                    dataIndex: 'open',
+                    dataType : 'string',
+                    width    : 100,
+                    className: 'payment-status-amountCell'
+                }, {
+                    header   : QUILocale.get(lg, 'panel.OpenItems.grid.details.daysDue'),
+                    dataIndex: 'daysDue',
+                    dataType : 'integer',
+                    width    : 75
+                }, {
+                    header   : QUILocale.get(lg, 'panel.OpenItems.grid.details.dunningLevel'),
+                    dataIndex: 'dunningLevel',
+                    dataType : 'integer',
+                    width    : 75
+                }, {
+                    dataIndex: 'documentType',
+                    dataType : 'string',
+                    hidden   : true
+                }]
+            });
+
+            this.$GridDetails.addEvents({
+                onRefresh : this.$refreshUserRecords,
+                onClick   : this.$refreshUserRecordsButtons,
+                onDblClick: this.$onClickAddTransaction
+            });
+
+            this.$refreshUserRecords(this.$GridDetails, true);
+
+            var size = ParentNode.getSize();
+
+            self.$GridDetails.setHeight(size.y - 120);
+        },
+
+        /**
+         * Refresh grid button status of user open items records details
+         */
+        $refreshUserRecordsButtons: function () {
+            if (!this.$GridDetails) {
+                return;
+            }
+
+            var selected = this.$GridDetails.getSelectedData(),
+                buttons  = this.$GridDetails.getButtons();
+
+            var OpenDocument = buttons.filter(function (Button) {
+                return Button.getAttribute('name') === 'open';
+            })[0];
+
+            var AddTransaction = buttons.filter(function (Button) {
+                return Button.getAttribute('name') === 'addTransaction';
+            })[0];
+
+            if (selected.length) {
+                OpenDocument.enable();
+                AddTransaction.enable();
+                return;
+            }
+
+            OpenDocument.disable();
+            AddTransaction.disable();
+        },
+
+        /**
+         * If the user clicks the "add transaction to open item record" button
+         */
+        $onClickAddTransaction: function () {
+            var self     = this,
+                selected = this.$GridDetails.getSelectedData();
+
+            if (!selected.length) {
+                return;
+            }
+
+            var Row = selected[0];
+            var erpEntity;
+
+            switch (Row.documentType) {
+                case 'invoice':
+                    erpEntity = 'Invoice';
+                    break;
+
+                case 'order':
+                    erpEntity = 'Order';
+                    break;
+
+                default:
+                    return;
+            }
+
+            var submitTransaction = function (Win, Data) {
+                Win.Loader.show();
+
+                switch (erpEntity) {
+                    case 'Invoice':
+                        require(['package/quiqqer/invoice/bin/Invoices'], function (Invoices) {
+                            Invoices.addPaymentToInvoice(
+                                Row.documentNo,
+                                Data.amount,
+                                Data.payment_method
+                            ).then(function () {
+                                Win.close();
+
+                                self.$refreshUserEntry(self.$currentRecordsUserId).then(function () {
+                                    self.$refreshUserRecords(self.$GridDetails, true);
+                                });
+                            }).catch(function (err) {
+                                Win.Loader.hide();
+                            });
+                        });
+                        break;
+
+                    case 'Order':
+                        // @todo
+                        break;
+                }
+            };
+
+            new AddPaymentWindow({
+                entityId  : Row.documentNo,
+                entityType: erpEntity,
+                events    : {
+                    onSubmit: submitTransaction
+                }
+            }).open();
+        },
+
+        /**
+         * If the user clicks the "open open item record document" button
+         */
+        $onClickOpenDocument: function () {
+            var self     = this,
+                selected = this.$GridDetails.getSelectedData();
+
+            if (!selected.length) {
+                return;
+            }
+
+            var Row = selected[0];
+
+            switch (Row.documentType) {
+                case 'invoice':
+                    this.Loader.show();
+
+                    require(['package/quiqqer/invoice/bin/backend/utils/Panels'], function (InvoicePanels) {
+                        InvoicePanels.openInvoice(Row.documentNo).then(function () {
+                            self.Loader.hide();
+                        });
+                    });
+                    break;
+
+                case 'order':
+                    // @todo
+                    break;
+
+                default:
+                    return;
+            }
+        },
+
+        /**
+         * key up event at the user records search input
+         *
+         * @param {DOMEvent} event
+         */
+        $onUserRecordsSearchKeyUp: function (event) {
+            if (event.key === 'up' ||
+                event.key === 'down' ||
+                event.key === 'left' ||
+                event.key === 'right') {
+                return;
+            }
+
+            var SearchInput = event.target;
+
+            if (this.$searchDelay) {
+                clearTimeout(this.$searchDelay);
+            }
+
+            if (event.type === 'click') {
+                // workaround, cancel needs time to clear
+                (function () {
+                    if (this.$UserRecordsSearch.value !== this.$currentUserRecordsSearch) {
+                        this.$searchDelay = (function () {
+                            this.$refreshUserRecords(this.$GridDetails);
+                        }).delay(250, this);
+                    }
+                }).delay(100, this);
+            }
+
+            if (this.$UserRecordsSearch.value === this.$currentUserRecordsSearch) {
+                return;
+            }
+
+            if (event.key === 'enter') {
+                this.$searchDelay = (function () {
+                    this.$refreshUserRecords(this.$GridDetails);
+                }).delay(250, this);
+                return;
+            }
+        },
+
+        /**
+         * Refresh GRID with user open items records
+         *
+         * @param {Object} Grid
+         * @param {Boolean} [forceRefresh] - Force refresh of user open items records; otherwise try
+         * to fetch from cache
+         */
+        $refreshUserRecords: function (Grid, forceRefresh) {
+            if (!this.$GridDetails) {
+                return;
+            }
+
+            forceRefresh = forceRefresh || false;
+
+            var self   = this;
+            var sortOn = this.$GridDetails.options.sortOn;
+
+            switch (sortOn) {
+                case 'supplier_name':
+                case 'display_vatsum':
+                case 'display_paid':
+                case 'display_toPay':
+                    sortOn = false;
+                    break;
+            }
+
+            this.Loader.show();
+
+            this.$currentUserRecordsSearch = this.$UserRecordsSearch.value;
+
+            this.$getUserOpenItems(
+                this.$currentRecordsUserId,
+                {
+                    perPage: this.$GridDetails.options.perPage,
+                    page   : this.$GridDetails.options.page,
+                    sortBy : this.$GridDetails.options.sortBy,
+                    sortOn : sortOn,
+                    search : this.$currentUserRecordsSearch
+                },
+                forceRefresh
+            ).then(function (result) {
+                self.$GridDetails.setData(result);
+                self.Loader.hide();
+
+                self.$refreshUserRecordsButtons();
+            });
+        },
+
         /**
          * Get list of open items by user
          *
          * @param {Number} userId
+         * @param {Object} SearchParams
+         * @param {Boolean} forceRefresh
          * @return {Promise}
          */
-        $getUserOpenItems: function (userId) {
+        $getUserOpenItems: function (userId, SearchParams, forceRefresh) {
             return new Promise(function (resolve, reject) {
                 QUIAjax.get('package_quiqqer_customer_ajax_backend_OpenItemsList_getUserOpenItems', resolve, {
-                    'package': 'quiqqer/customer',
-                    userId   : userId,
-                    onError  : reject
+                    'package'   : 'quiqqer/customer',
+                    userId      : userId,
+                    searchParams: JSON.encode(SearchParams),
+                    forceRefresh: forceRefresh ? 1 : 0,
+                    onError     : reject
                 });
             });
+        },
+
+        // endregion
+
+        // region Totals
+
+        /**
+         * Show the total display
+         */
+        showTotal: function () {
+            this.getContent().setStyle('overflow', 'hidden');
+
+            return new Promise(function (resolve) {
+                this.$Total.setStyles({
+                    display: 'inline-block',
+                    opacity: 0
+                });
+
+                moofx(this.$Total).animate({
+                    bottom : 1,
+                    opacity: 1
+                }, {
+                    duration: 200,
+                    callback: resolve
+                });
+            }.bind(this));
         }
+
+        // endregion
     });
 });
diff --git a/events.xml b/events.xml
index e4ac932..bf0d2f5 100644
--- a/events.xml
+++ b/events.xml
@@ -7,4 +7,10 @@
     <event on="onQuiqqerOrderCustomerDataSaveEnd"
            fire="\QUI\ERP\Customer\EventHandler::onQuiqqerOrderCustomerDataSaveEnd"
     />
+
+    <!-- Open items -->
+    <event on="onTransactionCreate" fire="\QUI\ERP\Customer\OpenItemsList\Events::onTransactionCreate"/>
+    <event on="onQuiqqerInvoiceTemporaryInvoicePostEnd" fire="\QUI\ERP\Customer\OpenItemsList\Events::onQuiqqerInvoiceTemporaryInvoicePostEnd"/>
+    <event on="onQuiqqerOrderCreated" fire="\QUI\ERP\Customer\OpenItemsList\Events::onQuiqqerOrderCreated"/>
+
 </events>
diff --git a/locale.xml b/locale.xml
index 2306128..fed442a 100644
--- a/locale.xml
+++ b/locale.xml
@@ -33,6 +33,18 @@
             <de><![CDATA[Kunden]]></de>
             <en><![CDATA[Customer]]></en>
         </locale>
+        <locale name="openItems.settings.title">
+            <de><![CDATA[Offene Posten]]></de>
+            <en><![CDATA[Open Items]]></en>
+        </locale>
+        <locale name="customer.settings.considerOrders">
+            <de><![CDATA[Bestellungen berücksichtigen]]></de>
+            <en><![CDATA[Consider order]]></en>
+        </locale>
+        <locale name="customer.settings.considerOrders.description">
+            <de><![CDATA[Bei der Erfassung von offenen Posten werden auch Bestellungen einbezogen, sofern sie keine zugeordnete Rechnung haben]]></de>
+            <en><![CDATA[When capturing open items, purchase orders are also included if they do not have an assigned invoice]]></en>
+        </locale>
         <locale name="customer.settings.groupId">
             <de><![CDATA[Kundengruppe]]></de>
             <en><![CDATA[Customer group]]></en>
@@ -237,6 +249,89 @@ Best regards
     </groups>
 
     <groups name="quiqqer/customer" datatype="js">
+
+        <!-- OpenItems -->
+        <locale name="panels.OpenItems.grid.toggle_title">
+            <de><![CDATA[Zeige alle offenen Posten dieses Kunden]]></de>
+            <en><![CDATA[Show all open items of this customer]]></en>
+        </locale>
+        <locale name="panels.OpenItems.title">
+            <de><![CDATA[Offene Posten]]></de>
+            <en><![CDATA[Open Items]]></en>
+        </locale>
+        <locale name="panel.OpenItems.grid.date">
+            <de><![CDATA[Datum]]></de>
+            <en><![CDATA[Date]]></en>
+        </locale>
+        <locale name="panel.OpenItems.grid.details.documentType">
+            <de><![CDATA[Beleg-Typ]]></de>
+            <en><![CDATA[Document type]]></en>
+        </locale>
+        <locale name="panel.OpenItems.grid.details.documentNo">
+            <de><![CDATA[Beleg-Nr.]]></de>
+            <en><![CDATA[Document no.]]></en>
+        </locale>
+        <locale name="panel.OpenItems.grid.net">
+            <de><![CDATA[Netto]]></de>
+            <en><![CDATA[Net]]></en>
+        </locale>
+        <locale name="panel.OpenItems.grid.vat">
+            <de><![CDATA[MwSt.]]></de>
+            <en><![CDATA[VAT]]></en>
+        </locale>
+        <locale name="panel.OpenItems.grid.gross">
+            <de><![CDATA[Brutto]]></de>
+            <en><![CDATA[Gross]]></en>
+        </locale>
+        <locale name="panel.OpenItems.grid.paid">
+            <de><![CDATA[beglichen]]></de>
+            <en><![CDATA[paid]]></en>
+        </locale>
+        <locale name="panel.OpenItems.grid.open">
+            <de><![CDATA[offen]]></de>
+            <en><![CDATA[open]]></en>
+        </locale>
+        <locale name="panel.OpenItems.grid.userId">
+            <de><![CDATA[Kunden-Nr.]]></de>
+            <en><![CDATA[Customer no.]]></en>
+        </locale>
+        <locale name="panel.OpenItems.grid.customerName">
+            <de><![CDATA[Kunde]]></de>
+            <en><![CDATA[Customer]]></en>
+        </locale>
+        <locale name="panels.OpenItems.details.btn.open">
+            <de><![CDATA[Beleg öffnen]]></de>
+            <en><![CDATA[Open document]]></en>
+        </locale>
+        <locale name="panels.OpenItems.details.btn.addTransaction">
+            <de><![CDATA[Zahlung buchen]]></de>
+            <en><![CDATA[Book payment]]></en>
+        </locale>
+        <locale name="panel.OpenItems.grid.openItemsCount">
+            <de><![CDATA[Offene Posten]]></de>
+            <en><![CDATA[Open items]]></en>
+        </locale>
+        <locale name="panels.OpenItems.btn.showOpenItemsList">
+            <de><![CDATA[OPOS-Liste drucken / versenden]]></de>
+            <en><![CDATA[Print / send Open Items List]]></en>
+        </locale>
+        <locale name="panels.OpenItems.details.tpl.placeholderSearch">
+            <de><![CDATA[Beleg-Suche...]]></de>
+            <en><![CDATA[Document search...]]></en>
+        </locale>
+        <locale name="panels.OpenItems.search.placeholder">
+            <de><![CDATA[Kunden-Suche...]]></de>
+            <en><![CDATA[Cusomter search...]]></en>
+        </locale>
+        <locale name="panel.OpenItems.grid.details.daysDue">
+            <de><![CDATA[Tage offen]]></de>
+            <en><![CDATA[Days open]]></en>
+        </locale>
+        <locale name="panel.OpenItems.grid.details.dunningLevel">
+            <de><![CDATA[Mahnstufe]]></de>
+            <en><![CDATA[Dunning level]]></en>
+        </locale>
+
         <locale name="window.customer.creation.title">
             <de><![CDATA[Neuen Kunden anlegen]]></de>
             <en><![CDATA[Create new customer]]></en>
diff --git a/settings.xml b/settings.xml
index d548d03..0f8f930 100644
--- a/settings.xml
+++ b/settings.xml
@@ -12,6 +12,13 @@
                     <defaultValue>0</defaultValue>
                 </conf>
             </section>
+
+            <section name="openItems">
+                <conf name="considerOrders">
+                    <type><![CDATA[boolean]]></type>
+                    <defaultvalue>0</defaultvalue>
+                </conf>
+            </section>
         </config>
 
         <window name="ERP">
@@ -53,6 +60,22 @@
                         </input>
 
                     </settings>
+
+                    <settings title="openItems" name="openItems">
+                        <title>
+                            <locale group="quiqqer/customer" var="openItems.settings.title"/>
+                        </title>
+
+                        <input conf="openItems.considerOrders" type="checkbox">
+                            <text>
+                                <locale group="quiqqer/customer" var="customer.settings.considerOrders"/>
+                            </text>
+                            <description>
+                                <locale group="quiqqer/customer" var="customer.settings.considerOrders.description"/>
+                            </description>
+                        </input>
+
+                    </settings>
                 </category>
             </categories>
 
diff --git a/src/QUI/ERP/Customer/OpenItemsList/Events.php b/src/QUI/ERP/Customer/OpenItemsList/Events.php
new file mode 100644
index 0000000..e57cf8c
--- /dev/null
+++ b/src/QUI/ERP/Customer/OpenItemsList/Events.php
@@ -0,0 +1,118 @@
+<?php
+
+namespace QUI\ERP\Customer\OpenItemsList;
+
+use QUI;
+use QUI\ERP\Accounting\Payments\Transactions\Transaction;
+use QUI\ERP\Accounting\Invoice\InvoiceTemporary;
+use QUI\ERP\Accounting\Invoice\Invoice;
+use QUI\ERP\Order\Settings;
+
+/**
+ * Class Events
+ *
+ * Event handler for all events related to open items of customers
+ */
+class Events
+{
+    /**
+     * quiqqer/payment-transactions: onTransactionCreate
+     *
+     * Update open records of user if a transaction was made against one of his open items
+     *
+     * @param Transaction $Transaction
+     * @return void
+     */
+    public static function onTransactionCreate(Transaction $Transaction)
+    {
+        $userId = $Transaction->getAttribute('uid');
+
+        if (empty($userId)) {
+            return;
+        }
+
+        try {
+            $User = QUI::getUsers()->get($userId);
+        } catch (\Exception $Exception) {
+            QUI\System\Log::writeDebugException($Exception);
+            return;
+        }
+
+        try {
+            Handler::updateOpenItemsRecord($User);
+        } catch (\Exception $Exception) {
+            QUI\System\Log::writeException($Exception);
+        }
+    }
+
+    /**
+     * quiqqer/invoice: onQuiqqerInvoiceTemporaryInvoicePostEnd
+     *
+     * Update open records of user if an invoice is posted
+     *
+     * @param InvoiceTemporary $TempInvoice
+     * @param Invoice $Invoice
+     * @return void
+     */
+    public static function onQuiqqerInvoiceTemporaryInvoicePostEnd(
+        InvoiceTemporary $TempInvoice,
+        Invoice $Invoice
+    ): void {
+        try {
+            $User = $Invoice->getCustomer();
+        } catch (\Exception $Exception) {
+            QUI\System\Log::writeDebugException($Exception);
+            return;
+        }
+
+        try {
+            Handler::updateOpenItemsRecord($User);
+        } catch (\Exception $Exception) {
+            QUI\System\Log::writeException($Exception);
+        }
+    }
+
+    /**
+     * quiqqer/order: onQuiqqerOrderCreated
+     *
+     * Update open records of user if an order is created
+     *
+     * @param QUI\ERP\Order\Order $Order
+     */
+    public static function onQuiqqerOrderCreated(QUI\ERP\Order\Order $Order)
+    {
+        try {
+            $Conf           = QUI::getPackage('quiqqer/customer')->getConfig();
+            $considerOrders = $Conf->get('openItems', 'considerOrders');
+
+            if (empty($considerOrders)) {
+                return;
+            }
+        } catch (\Exception $Exception) {
+            QUI\System\Log::writeException($Exception);
+            return;
+        }
+
+        if (!empty($Order->getAttribute('no_invoice_auto_create'))) {
+            return;
+        }
+
+        // Do not track order that also are tracked via invoice
+        if (Settings::getInstance()->createInvoiceOnOrder()) {
+            return;
+        }
+
+        try {
+            $User = $Order->getCustomer();
+        } catch (\Exception $Exception) {
+            QUI\System\Log::writeDebugException($Exception);
+            return;
+        }
+
+        try {
+            Handler::updateOpenItemsRecord($User);
+        } catch (\Exception $Exception) {
+            QUI\System\Log::writeException($Exception);
+        }
+    }
+}
diff --git a/src/QUI/ERP/Customer/OpenItemsList/Handler.php b/src/QUI/ERP/Customer/OpenItemsList/Handler.php
index 733113b..d8b93d1 100644
--- a/src/QUI/ERP/Customer/OpenItemsList/Handler.php
+++ b/src/QUI/ERP/Customer/OpenItemsList/Handler.php
@@ -11,6 +11,7 @@
 use QUI\Utils\Grid;
 use QUI\Utils\Security\Orthos;
 use QUI\ERP\Customer\Customers;
+use QUI\ERP\Order\Handler as OrderHandler;
 
 /**
  * Class Handler
@@ -53,11 +54,28 @@ public static function getOpenItemsList(QUI\Interfaces\Users\User $User)
             $List->addItem(self::parseInvoiceToOpenItem($Invoice));
         }
 
+        try {
+            $Conf           = QUI::getPackage('quiqqer/customer')->getConfig();
+            $considerOrders = $Conf->get('openItems', 'considerOrders');
+
+            if (!empty($considerOrders)) {
+                $orders = self::getOpenOrders($User);
+
+                foreach ($orders as $Order) {
+                    $List->addItem(self::parseOrderToOpenItem($Order));
+                }
+            }
+        } catch (\Exception $Exception) {
+            QUI\System\Log::writeException($Exception);
+        }
+
         // @todo Fetch open dunnings
 
         return $List;
     }
 
+    // region Invoices
+
     /**
      * Get all open invoices of a user
      *
@@ -76,19 +94,19 @@ protected static function getOpenInvoices(QUI\Interfaces\Users\User $User)
             'select' => ['id'],
             'from'   => $Invoices->invoiceTable(),
             'where'  => [
-                'paid_status'      => [
+                'paid_status' => [
                     'type'  => 'NOT IN',
                     'value' => [
                         QUI\ERP\Constants::PAYMENT_STATUS_PAID,
                         QUI\ERP\Constants::PAYMENT_STATUS_CANCELED
                     ]
                 ],
-                'time_for_payment' => [
-                    'type'  => '<=',
-                    'value' => \date('Y-m-d H:i:s')
-                ],
-                'customer_id'      => $User->getId(),
-                'type'             => InvoiceHandler::TYPE_INVOICE
+//                'time_for_payment' => [
+//                    'type'  => '<=',
+//                    'value' => \date('Y-m-d H:i:s')
+//                ],
+                'customer_id' => $User->getId(),
+                'type'        => InvoiceHandler::TYPE_INVOICE
             ]
         ]);
 
@@ -180,6 +198,133 @@ protected static function parseInvoiceToOpenItem(Invoice $Invoice)
         return $Item;
     }
 
+    // endregion
+
+    // region Orders
+
+    /**
+     * Get all open orders of a user
+     *
+     * @param QUI\Interfaces\Users\User $User
+     * @return QUI\ERP\Order\Order[]
+     */
+    protected static function getOpenOrders(QUI\Interfaces\Users\User $User)
+    {
+        if (!QUI::getPackageManager()->isInstalled('quiqqer/order')) {
+            return [];
+        }
+
+        $Orders = OrderHandler::getInstance();
+
+        $result = QUI::getDataBase()->fetch([
+            'select' => ['id'],
+            'from'   => $Orders->table(),
+            'where'  => [
+                'paid_status' => [
+                    'type'  => 'NOT IN',
+                    'value' => [
+                        QUI\ERP\Constants::PAYMENT_STATUS_PAID,
+                        QUI\ERP\Constants::PAYMENT_STATUS_CANCELED,
+                        QUI\ERP\Constants::PAYMENT_STATUS_PLAN
+                    ]
+                ],
+                'customerId'  => $User->getId(),
+                'invoice_id'  => null
+            ]
+        ]);
+
+        $orders = [];
+
+        foreach ($result as $row) {
+            try {
+                $orders[] = $Orders->get($row['id']);
+            } catch (\Exception $Exception) {
+                QUI\System\Log::writeException($Exception);
+            }
+        }
+
+        return $orders;
+    }
+
+    /**
+     * Parses order data to an open item
+     *
+     * @param QUI\ERP\Order\Order $Order
+     * @return Item
+     */
+    protected static function parseOrderToOpenItem(QUI\ERP\Order\Order $Order)
+    {
+        $Item = new Item(self::DOCUMENT_TYPE_ORDER);
+
+        // Basic data
+        $Item->setDocumentNo($Order->getPrefixedId());
+        $Item->setDate(\date_create($Order->getAttribute('c_date')));
+
+        if (!empty($Order->getAttribute('payment_time'))) {
+            $Item->setDueDate(\date_create($Order->getAttribute('payment_time')));
+        }
+
+        // Invoice amounts
+        $paidStatus = $Order->getPaidStatusInformation();
+        $Item->setAmountPaid($paidStatus['paid']);
+        $Item->setAmountOpen($paidStatus['toPay']);
+
+        $OrderArticles = $Order->getArticles();
+        $calculations  = $OrderArticles->getCalculations();
+
+        $Item->setAmountTotalNet($calculations['nettoSum']);
+        $Item->setAmountTotalSum($calculations['sum']);
+
+        if (!empty($calculations['vatArray'])) {
+            $Item->setAmountTotalVat(\array_sum(\array_column($calculations['vatArray'], 'sum')));
+        }
+
+        $Item->setCurrency($Order->getCurrency());
+
+        // Latest transaction date
+        $Transactions = QUI\ERP\Accounting\Payments\Transactions\Handler::getInstance();
+        $transactions = $Transactions->getTransactionsByHash($Order->getHash());
+
+        if (!empty($transactions)) {
+            // Sort by date
+            \usort($transactions, function ($TransactionA, $TransactionB) {
+                /**
+                 * @var QUI\ERP\Accounting\Payments\Transactions\Transaction $TransactionA
+                 * @var QUI\ERP\Accounting\Payments\Transactions\Transaction $TransactionB
+                 */
+                $DateA = \date_create($TransactionA->getDate());
+                $DateB = \date_create($TransactionB->getDate());
+
+                if ($DateA === $DateB) {
+                    return 0;
+                }
+
+                return $DateA > $DateB ? -1 : 1;
+            });
+
+            $LatestTransactionDate = \date_create($transactions[0]->getDate());
+            $Item->setLastPaymentDate($LatestTransactionDate);
+        }
+
+        // Days due
+//        $Now            = \date_create();
+//        $TimeForPayment = \date_create($Invoice->getAttribute('time_for_payment'));
+//        $Item->setDaysDue($TimeForPayment->diff($Now)->days + 1);
+
+        // Check if dunning exist
+//        if (QUI::getPackageManager()->isInstalled('quiqqer/dunning')) {
+//            $DunningProcess = DunningsHandler::getInstance()->getDunningProcessByInvoiceId($Invoice->getCleanId());
+//
+//            if ($DunningProcess && $DunningProcess->getCurrentDunning()) {
+//                $Item->setDunningLevel($DunningProcess->getCurrentDunning()->getDunningLevel()->getLevel());
+//            }
+//        }
+
+        return $Item;
+    }
+
+    // endregion
+
     /**
      * Updates the open items record of a user with up-to-date item data.
      *
@@ -269,6 +414,14 @@ public static function searchOpenItems(array $searchParams)
             }
         }
 
+        if (!empty($searchParams['currency'])) {
+            $where[] = '`currency` = \''.Orthos::clear($searchParams['currency']).'\'';
+        }
+
+        if (!empty($searchParams['userId'])) {
+            $where[] = '`userId` = '.(int)$searchParams['userId'];
+        }
+
         // build WHERE query string
         if (!empty($where)) {
             $sql .= " WHERE ".implode(" AND ", $where);
@@ -389,6 +542,38 @@ public static function parseForGrid(array $result): array
         return $result;
     }
 
+    /**
+     * Calculate the totals for a set of customer open items
+     *
+     * @param array $entries - Database rows form customer_open_items table
+     * @param QUI\ERP\Currency\Currency $Currency
+     * @return array - Totals prepared for backend display
+     */
+    public static function getTotals(array $entries, QUI\ERP\Currency\Currency $Currency)
+    {
+        $net   = 0;
+        $vat   = 0;
+        $gross = 0;
+        $paid  = 0;
+        $open  = 0;
+
+        foreach ($entries as $entry) {
+            $net   += $entry['net_sum'];
+            $vat   += $entry['vat_sum'];
+            $gross += $entry['total_sum'];
+            $paid  += $entry['paid_sum'];
+            $open  += $entry['open_sum'];
+        }
+
+        return [
+            'display_net'   => $Currency->format($net),
+            'display_vat'   => $Currency->format($vat),
+            'display_gross' => $Currency->format($gross),
+            'display_paid'  => $Currency->format($paid),
+            'display_open'  => $Currency->format($open)
+        ];
+    }
+
     /**
      * Get table that contains open items
      *
diff --git a/src/QUI/ERP/Customer/OpenItemsList/Item.php b/src/QUI/ERP/Customer/OpenItemsList/Item.php
index bdf216e..277fc2b 100644
--- a/src/QUI/ERP/Customer/OpenItemsList/Item.php
+++ b/src/QUI/ERP/Customer/OpenItemsList/Item.php
@@ -231,6 +231,10 @@ public function getDate(): \DateTime
      */
     public function getDateFormatted()
     {
+        if (empty($this->Date)) {
+            return '-';
+        }
+
         return $this->getLocale()->formatDate($this->Date->getTimestamp());
     }
 
@@ -255,6 +259,10 @@ public function getLastPaymentDate()
      */
     public function getLastPaymentDateFormatted()
     {
+        if (empty($this->LastPaymentDate)) {
+            return '-';
+        }
+
         return $this->getLocale()->formatDate($this->LastPaymentDate->getTimestamp());
     }
 
@@ -279,6 +287,10 @@ public function getDueDate(): \DateTime
      */
     public function getDueDateFormatted()
     {
+        if (empty($this->DueDate)) {
+            return '-';
+        }
+
         return $this->getLocale()->formatDate($this->DueDate->getTimestamp());
     }
 
-- 
GitLab