From 8fdc1a1599d51a3828b88f67dde15e25cb8bba48 Mon Sep 17 00:00:00 2001
From: Tomas Peterka <tomas.peterka@nexedi.com>
Date: Tue, 9 Jan 2018 18:22:19 +0100
Subject: [PATCH] [hal_json+renderjs] Implement "stat" line for ListBox

/reviewed-on https://lab.nexedi.com/nexedi/erp5/merge_requests/528
---
 .../ERP5Document_getHateoas.py                |  69 ++++-
 .../erp5_ui_test/FooModule_statMethod.py      |  12 +
 .../erp5_ui_test/FooModule_statMethod.xml     |  66 +++++
 .../erp5_ui_test/FooModule_statQuantity.py    |   6 +
 .../erp5_ui_test/FooModule_statQuantity.xml   |  66 +++++
 .../web_page_module/rjs_gadget_erp5_jio_js.js |  46 +--
 .../rjs_gadget_erp5_jio_js.xml                |   4 +-
 .../rjs_gadget_erp5_listbox_html.html         |  59 ++--
 .../rjs_gadget_erp5_listbox_html.xml          |   4 +-
 .../rjs_gadget_erp5_listbox_js.js             | 262 +++++++++++-------
 .../rjs_gadget_erp5_listbox_js.xml            |  10 +-
 .../rjs_gadget_erp5_nojqm_css.css             |  38 ++-
 .../erp5_web_renderjs_ui/erp5css.less.txt     |  53 ++--
 .../testEmptyListboxWithStat.xml              |  58 ++++
 .../testEmptyListboxWithStat.zpt              |  47 ++++
 .../testHideItem.zpt                          |  15 -
 .../testStatColumns.xml                       |  58 ++++
 .../testStatColumns.zpt                       |  73 +++++
 .../testStatMethod.xml                        |  58 ++++
 .../testStatMethod.zpt                        |  74 +++++
 .../testStatMissing.xml                       |  58 ++++
 .../testStatMissing.zpt                       |  53 ++++
 .../Zuite_CommonTemplateForRenderjsUi.zpt     |   2 +-
 23 files changed, 989 insertions(+), 202 deletions(-)
 create mode 100644 bt5/erp5_ui_test/SkinTemplateItem/portal_skins/erp5_ui_test/FooModule_statMethod.py
 create mode 100644 bt5/erp5_ui_test/SkinTemplateItem/portal_skins/erp5_ui_test/FooModule_statMethod.xml
 create mode 100644 bt5/erp5_ui_test/SkinTemplateItem/portal_skins/erp5_ui_test/FooModule_statQuantity.py
 create mode 100644 bt5/erp5_ui_test/SkinTemplateItem/portal_skins/erp5_ui_test/FooModule_statQuantity.xml
 create mode 100644 bt5/erp5_web_renderjs_ui_test/PathTemplateItem/portal_tests/renderjs_ui_listbox_zuite/testEmptyListboxWithStat.xml
 create mode 100644 bt5/erp5_web_renderjs_ui_test/PathTemplateItem/portal_tests/renderjs_ui_listbox_zuite/testEmptyListboxWithStat.zpt
 create mode 100644 bt5/erp5_web_renderjs_ui_test/PathTemplateItem/portal_tests/renderjs_ui_listbox_zuite/testStatColumns.xml
 create mode 100644 bt5/erp5_web_renderjs_ui_test/PathTemplateItem/portal_tests/renderjs_ui_listbox_zuite/testStatColumns.zpt
 create mode 100644 bt5/erp5_web_renderjs_ui_test/PathTemplateItem/portal_tests/renderjs_ui_listbox_zuite/testStatMethod.xml
 create mode 100644 bt5/erp5_web_renderjs_ui_test/PathTemplateItem/portal_tests/renderjs_ui_listbox_zuite/testStatMethod.zpt
 create mode 100644 bt5/erp5_web_renderjs_ui_test/PathTemplateItem/portal_tests/renderjs_ui_listbox_zuite/testStatMissing.xml
 create mode 100644 bt5/erp5_web_renderjs_ui_test/PathTemplateItem/portal_tests/renderjs_ui_listbox_zuite/testStatMissing.zpt

diff --git a/bt5/erp5_hal_json_style/SkinTemplateItem/portal_skins/erp5_hal_json_style/ERP5Document_getHateoas.py b/bt5/erp5_hal_json_style/SkinTemplateItem/portal_skins/erp5_hal_json_style/ERP5Document_getHateoas.py
index 797b7bea1b..09cb3709d0 100644
--- a/bt5/erp5_hal_json_style/SkinTemplateItem/portal_skins/erp5_hal_json_style/ERP5Document_getHateoas.py
+++ b/bt5/erp5_hal_json_style/SkinTemplateItem/portal_skins/erp5_hal_json_style/ERP5Document_getHateoas.py
@@ -47,10 +47,26 @@ MARKER = []
 
 if REQUEST is None:
   REQUEST = context.REQUEST
-  # raise Unauthorized
+
 if response is None:
   response = REQUEST.RESPONSE
 
+
+def toBasicTypes(obj):
+  """Ensure that  obj contains only basic types."""
+  if obj is None:
+    return obj
+  if isinstance(obj, (bool, int, float, long, str, unicode)):
+    return obj
+  if isinstance(obj, (tuple, list)):
+    return [toBasicTypes(x) for x in obj]
+  try:
+    return {toBasicTypes(key): toBasicTypes(obj[key]) for key in obj}
+  except:
+    log('Cannot convert {!s} to basic types {!s}'.format(type(obj), obj), level=100)
+  return obj
+
+
 # http://stackoverflow.com/a/13105359
 def byteify(string):
   if isinstance(string, dict):
@@ -684,6 +700,8 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key
       "lines": lines,
       "default_params": default_params,
       "list_method": list_method_name,
+      "show_stat": field.get_value('stat_method') != "" or len(field.get_value('stat_columns')) > 0,
+      "show_count": field.get_value('count_method') != "",
       "query": url_template_dict["jio_search_template"] % {
         "query": make_query({
           "query": sql_catalog.buildQuery(
@@ -1478,6 +1496,55 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
       contents_list.append(contents_item)
     result_dict['_embedded']['contents'] = contents_list
 
+    # Compute statistics if the search issuer was ListBox
+    # or in future if the stats (SUM) are required by JIO call
+    source_field_meta_type = source_field.meta_type if source_field is not None else ""
+    if source_field_meta_type == "ProxyField":
+      source_field_meta_type = source_field.getRecursiveTemplateField().meta_type
+
+    if source_field is not None and source_field_meta_type == "ListBox":
+      contents_stat_list = []
+      # in case the search was issued by listbox we can provide results of
+      # stat_method and count_method back to the caller
+      # XXX: we should check whether they asked for it
+      stat_method = source_field.get_value('stat_method')
+      stat_columns = source_field.get_value('stat_columns')
+      # support only selection_name for stat methods because any `selection` is deprecated
+      # and should be removed
+      # Romain wants full backward compatibility so putting `selection` back in parameters
+      selection_name = source_field.get_value('selection_name')
+      if selection_name and 'selection_name' not in catalog_kw:
+        catalog_kw['selection_name'] = selection_name
+        catalog_kw['selection'] = context.getPortalObject().portal_selections.getSelectionFor(selection_name, REQUEST)
+
+      contents_stat = {}
+      if len(stat_columns) > 0:
+        # prefer stat per column (follow original ListBox.py implementation)
+        for stat_name, stat_script in stat_columns:
+          contents_stat[stat_name] = getattr(traversed_document, stat_script)(REQUEST=REQUEST, **catalog_kw)
+        contents_stat_list.append(contents_stat)
+      elif stat_method != "" and stat_method.getMethodName() != list_method:
+        # general stat_method is second in priority list - should return dictionary or list of dictionaries
+        # where all "fields" should be accessible by their "select" name (no "listbox_" prefix)
+        stat_method_result = getattr(traversed_document, stat_method.getMethodName())(REQUEST=REQUEST, **catalog_kw)
+        # stat method can return simple dictionary or subscriptable object thus we put it into one-item list
+        if stat_method_result is not None and not isinstance(stat_method_result, (list, tuple)):
+          stat_method_result = [stat_method_result, ]
+        contents_stat_list = toBasicTypes(stat_method_result) or []
+
+      for contents_stat in contents_stat_list:
+        for key, value in contents_stat.items():
+          if key in editable_field_dict:
+            contents_stat[key] = renderField(
+              traversed_document, editable_field_dict[key], listbox_form, value, key=editable_field_dict[key].id + '__sum')
+
+      if len(contents_stat_list) > 0:
+        result_dict['_embedded']['sum'] = contents_stat_list
+
+    # We should cleanup the selection if it exists in catalog params BUT
+    # we cannot because it requires escalated Permission.'modifyPortal' so
+    # the correct solution would be to ReportSection.popReport but unfortunately
+    # we don't have it anymore because we are asynchronous
     return result_dict
 
   elif mode == 'form':
diff --git a/bt5/erp5_ui_test/SkinTemplateItem/portal_skins/erp5_ui_test/FooModule_statMethod.py b/bt5/erp5_ui_test/SkinTemplateItem/portal_skins/erp5_ui_test/FooModule_statMethod.py
new file mode 100644
index 0000000000..a2f361717c
--- /dev/null
+++ b/bt5/erp5_ui_test/SkinTemplateItem/portal_skins/erp5_ui_test/FooModule_statMethod.py
@@ -0,0 +1,12 @@
+"""Compute stats from actual Foo Lines on a Foo object"""
+column_list = ['getQuantity', 'id']
+result = {c: 0.0 for c in column_list}
+
+for line in context.contentValues(portal_type="Foo"):
+  for column in column_list:
+    value = getattr(line, column)
+    if callable(value):
+      value = value()
+    result[column] = result[column] + float(value)
+
+return [result, ]
diff --git a/bt5/erp5_ui_test/SkinTemplateItem/portal_skins/erp5_ui_test/FooModule_statMethod.xml b/bt5/erp5_ui_test/SkinTemplateItem/portal_skins/erp5_ui_test/FooModule_statMethod.xml
new file mode 100644
index 0000000000..efa6d726e9
--- /dev/null
+++ b/bt5/erp5_ui_test/SkinTemplateItem/portal_skins/erp5_ui_test/FooModule_statMethod.xml
@@ -0,0 +1,66 @@
+<?xml version="1.0"?>
+<ZopeData>
+  <record id="1" aka="AAAAAAAAAAE=">
+    <pickle>
+      <global name="Python Script" module="erp5.portal_type"/>
+    </pickle>
+    <pickle>
+      <dictionary>
+        <item>
+            <key> <string>Script_magic</string> </key>
+            <value> <int>3</int> </value>
+        </item>
+        <item>
+            <key> <string>_bind_names</string> </key>
+            <value>
+              <object>
+                <klass>
+                  <global name="NameAssignments" module="Shared.DC.Scripts.Bindings"/>
+                </klass>
+                <tuple/>
+                <state>
+                  <dictionary>
+                    <item>
+                        <key> <string>_asgns</string> </key>
+                        <value>
+                          <dictionary>
+                            <item>
+                                <key> <string>name_container</string> </key>
+                                <value> <string>container</string> </value>
+                            </item>
+                            <item>
+                                <key> <string>name_context</string> </key>
+                                <value> <string>context</string> </value>
+                            </item>
+                            <item>
+                                <key> <string>name_m_self</string> </key>
+                                <value> <string>script</string> </value>
+                            </item>
+                            <item>
+                                <key> <string>name_subpath</string> </key>
+                                <value> <string>traverse_subpath</string> </value>
+                            </item>
+                          </dictionary>
+                        </value>
+                    </item>
+                  </dictionary>
+                </state>
+              </object>
+            </value>
+        </item>
+        <item>
+            <key> <string>_params</string> </key>
+            <value> <string>**kwargs</string> </value>
+        </item>
+        <item>
+            <key> <string>id</string> </key>
+            <value> <string>FooModule_statMethod</string> </value>
+        </item>
+        <item>
+            <key> <string>portal_type</string> </key>
+            <value> <string>Python Script</string> </value>
+        </item>
+      </dictionary>
+    </pickle>
+  </record>
+</ZopeData>
diff --git a/bt5/erp5_ui_test/SkinTemplateItem/portal_skins/erp5_ui_test/FooModule_statQuantity.py b/bt5/erp5_ui_test/SkinTemplateItem/portal_skins/erp5_ui_test/FooModule_statQuantity.py
new file mode 100644
index 0000000000..7aad111814
--- /dev/null
+++ b/bt5/erp5_ui_test/SkinTemplateItem/portal_skins/erp5_ui_test/FooModule_statQuantity.py
@@ -0,0 +1,6 @@
+counter = 0
+
+for value in context.contentValues():
+  counter = counter + int(value.getQuantity())
+
+return counter
diff --git a/bt5/erp5_ui_test/SkinTemplateItem/portal_skins/erp5_ui_test/FooModule_statQuantity.xml b/bt5/erp5_ui_test/SkinTemplateItem/portal_skins/erp5_ui_test/FooModule_statQuantity.xml
new file mode 100644
index 0000000000..b0d7234476
--- /dev/null
+++ b/bt5/erp5_ui_test/SkinTemplateItem/portal_skins/erp5_ui_test/FooModule_statQuantity.xml
@@ -0,0 +1,66 @@
+<?xml version="1.0"?>
+<ZopeData>
+  <record id="1" aka="AAAAAAAAAAE=">
+    <pickle>
+      <global name="Python Script" module="erp5.portal_type"/>
+    </pickle>
+    <pickle>
+      <dictionary>
+        <item>
+            <key> <string>Script_magic</string> </key>
+            <value> <int>3</int> </value>
+        </item>
+        <item>
+            <key> <string>_bind_names</string> </key>
+            <value>
+              <object>
+                <klass>
+                  <global name="NameAssignments" module="Shared.DC.Scripts.Bindings"/>
+                </klass>
+                <tuple/>
+                <state>
+                  <dictionary>
+                    <item>
+                        <key> <string>_asgns</string> </key>
+                        <value>
+                          <dictionary>
+                            <item>
+                                <key> <string>name_container</string> </key>
+                                <value> <string>container</string> </value>
+                            </item>
+                            <item>
+                                <key> <string>name_context</string> </key>
+                                <value> <string>context</string> </value>
+                            </item>
+                            <item>
+                                <key> <string>name_m_self</string> </key>
+                                <value> <string>script</string> </value>
+                            </item>
+                            <item>
+                                <key> <string>name_subpath</string> </key>
+                                <value> <string>traverse_subpath</string> </value>
+                            </item>
+                          </dictionary>
+                        </value>
+                    </item>
+                  </dictionary>
+                </state>
+              </object>
+            </value>
+        </item>
+        <item>
+            <key> <string>_params</string> </key>
+            <value> <string>selection, **kwargs</string> </value>
+        </item>
+        <item>
+            <key> <string>id</string> </key>
+            <value> <string>FooModule_statQuantity</string> </value>
+        </item>
+        <item>
+            <key> <string>portal_type</string> </key>
+            <value> <string>Python Script</string> </value>
+        </item>
+      </dictionary>
+    </pickle>
+  </record>
+</ZopeData>
diff --git a/bt5/erp5_web_renderjs_ui/PathTemplateItem/web_page_module/rjs_gadget_erp5_jio_js.js b/bt5/erp5_web_renderjs_ui/PathTemplateItem/web_page_module/rjs_gadget_erp5_jio_js.js
index 6a2a8c0915..f8e12b7184 100644
--- a/bt5/erp5_web_renderjs_ui/PathTemplateItem/web_page_module/rjs_gadget_erp5_jio_js.js
+++ b/bt5/erp5_web_renderjs_ui/PathTemplateItem/web_page_module/rjs_gadget_erp5_jio_js.js
@@ -178,27 +178,33 @@
         ]
       )
         .push(function (catalog_json) {
-          var data = catalog_json._embedded.contents,
-            count = data.length,
-            k,
-            uri,
-            item,
-            result = [];
-          for (k = 0; k < count; k += 1) {
-            item = data[k];
-            uri = new URI(item._links.self.href);
-            delete item._links;
-            result.push({
-              id: uri.segment(2),
-              doc: {},
-              value: item
-            });
-          }
+          var data = catalog_json._embedded.contents || [],
+            summary = catalog_json._embedded.sum || [],
+            count = catalog_json._embedded.count;
           return {
-            data: {
-              rows: result,
-              total_rows: result.length
-            }
+            "data": {
+              "rows": data.map(function (item) {
+                var uri = new URI(item._links.self.href);
+                delete item._links;
+                return {
+                  "id": uri.segment(2),
+                  "doc": {},
+                  "value": item
+                };
+              }),
+              "total_rows": data.length
+            },
+            "sum": {
+              "rows": summary.map(function (item, index) {
+                return {
+                  "id": '/#summary' + index, // this is obviously wrong. @Romain help please!
+                  "doc": {},
+                  "value": item
+                };
+              }),
+              "total_rows": summary.length
+            },
+            "count": count
           };
         });
     })
diff --git a/bt5/erp5_web_renderjs_ui/PathTemplateItem/web_page_module/rjs_gadget_erp5_jio_js.xml b/bt5/erp5_web_renderjs_ui/PathTemplateItem/web_page_module/rjs_gadget_erp5_jio_js.xml
index 17adcde780..80760bcd12 100644
--- a/bt5/erp5_web_renderjs_ui/PathTemplateItem/web_page_module/rjs_gadget_erp5_jio_js.xml
+++ b/bt5/erp5_web_renderjs_ui/PathTemplateItem/web_page_module/rjs_gadget_erp5_jio_js.xml
@@ -230,7 +230,7 @@
             </item>
             <item>
                 <key> <string>serial</string> </key>
-                <value> <string>947.45414.13002.10052</string> </value>
+                <value> <string>963.59331.40212.55432</string> </value>
             </item>
             <item>
                 <key> <string>state</string> </key>
@@ -248,7 +248,7 @@
                     </tuple>
                     <state>
                       <tuple>
-                        <float>1449753994.81</float>
+                        <float>1512454358.33</float>
                         <string>UTC</string>
                       </tuple>
                     </state>
diff --git a/bt5/erp5_web_renderjs_ui/PathTemplateItem/web_page_module/rjs_gadget_erp5_listbox_html.html b/bt5/erp5_web_renderjs_ui/PathTemplateItem/web_page_module/rjs_gadget_erp5_listbox_html.html
index 85e34c32cf..0c1e25f79c 100644
--- a/bt5/erp5_web_renderjs_ui/PathTemplateItem/web_page_module/rjs_gadget_erp5_listbox_html.html
+++ b/bt5/erp5_web_renderjs_ui/PathTemplateItem/web_page_module/rjs_gadget_erp5_listbox_html.html
@@ -60,18 +60,18 @@
        </thead>
       </table>
     </script>
-    
+
     <script id="listbox-hidden-tbody-template" type="text/x-handlebars-template">
      <table>
        <tbody class="tbody">
-        {{#each body_value}}
+        {{#each row_list}}
           <tr>
            {{#if ../show_anchor}}
              <th>
                <a class="ui-link ui-btn ui-corner-all ui-icon-carat-r ui-btn-icon-notext" href="{{jump}}"></a>
              </th>
            {{/if}}
-           {{#each tr_value}}
+           {{#each cell_list}}
               <td>
                 {{#if type}}
                   {{#if editable}}
@@ -82,7 +82,7 @@
                     </a>
                   {{/if}}
                 {{else}}
-                  <a href="{{href}}" class="ui-link">{{text}}</a>
+                  <a href="{{href}}" class="ui-link">{{default}}</a>
                 {{/if}}
               </td>
            {{/each}}
@@ -101,7 +101,7 @@
     <script id="listbox-show-tbody-template" type="text/x-handlebars-template">
        <table>
        <tbody class="tbody">
-        {{#each body_value}}
+        {{#each row_list}}
            <tr>
              {{#if ../show_anchor}}
                 <th>
@@ -111,7 +111,7 @@
              <td>
                <input value="{{value}}" type="checkbox" checked="true" class="hide_element">
              </td>
-           {{#each tr_value}}
+           {{#each cell_list}}
               <td>
                 {{#if type}}
                   {{#if editable}}
@@ -122,7 +122,7 @@
                     </a>
                   {{/if}}
                 {{else}}
-                  <a href="{{href}}" class="ui-link">{{text}}</a>
+                  <a href="{{href}}" class="ui-link">{{default}}</a>
                 {{/if}}
               </td>
            {{/each}}
@@ -140,19 +140,40 @@
     <script id="listbox-tfoot-template" type="text/x-handlebars-template">
       <table>
        <tfoot class="ui-bar-inherit tfoot">
-        <th colspan="{{colspan}}">
-          <div class="ui-controlgroup ui-controlgroup-horizontal ui-corner-all ui-paging-menu">
-            <div class="ui-controlgroup-controls">
-              <a class="{{previous_classname}}" data-i18n="Previous" href="{{previous_url}}">Previous</a>
-              <a class="{{next_classname}}" data-i18n="Next" href="{{next_url}}">Next</a>
-              <span class="ui-btn ui-disabled" data-i18n="{{record}}">{{record}}</span>
-            </div>
-          </div>
-        </th>
+       {{#each row_list}}
+         <tr>
+           {{#if ../show_anchor}}
+             <td>Total</td>
+           {{/if}}
+           {{#each cell_list}}
+           <td>
+             {{#if type}}
+               <div class="editable_div" data-column="{{column}}" data-line="{{line}}"></div>
+             {{else}}
+               {{#if default}}
+                 {{default}}
+               {{else}}
+                 {{#unless ../../show_anchor }}
+                   {{#if @first}}
+                     Total
+                   {{/if}}
+                 {{/unless}}
+               {{/if}}
+             {{/if}}
+           </td>
+           {{/each}}
+         </tr>
+       {{/each}}
        </tfoot>
       </table>
     </script>
 
+    <script id="listbox-nav-template" type="text/x-handlebars-template">
+      <a class="{{previous_classname}}" data-i18n="Previous" href="{{previous_url}}">Previous</a>
+      <a class="{{next_classname}}" data-i18n="Next" href="{{next_url}}">Next</a>
+      <span class="ui-disabled ui-right" data-i18n="{{record}}">{{record}}</span>
+    </script>
+
     <script id="listbox-template" type="text/x-handlebars-template">
       <div class="ui-table-header ui-header ui-bar-c ui-corner-all">
         <h1 data-i18n="{{title}}" class="ui-title ui-override-theme">{{title}}<span> <span class="listboxloader ui-icon-spinner ui-btn-icon-left"></span></span></h1>
@@ -165,11 +186,11 @@
         <tbody></tbody>
         <tfoot class="ui-bar-inherit tfoot"></tfoot>
       </table>
+      <nav></nav>
     </div>
   </script>
-  
-  
-  
+
+
   <script id="error-message-template" type="text/x-handlebars-template">
     <div class="ui-listbox-error">
 		   <a class="ui-btn ui-corner-all ui-btn-inline" href="{{reset_url}}">
diff --git a/bt5/erp5_web_renderjs_ui/PathTemplateItem/web_page_module/rjs_gadget_erp5_listbox_html.xml b/bt5/erp5_web_renderjs_ui/PathTemplateItem/web_page_module/rjs_gadget_erp5_listbox_html.xml
index e06ae3c88d..4029320e53 100644
--- a/bt5/erp5_web_renderjs_ui/PathTemplateItem/web_page_module/rjs_gadget_erp5_listbox_html.xml
+++ b/bt5/erp5_web_renderjs_ui/PathTemplateItem/web_page_module/rjs_gadget_erp5_listbox_html.xml
@@ -234,7 +234,7 @@
             </item>
             <item>
                 <key> <string>serial</string> </key>
-                <value> <string>964.27340.60822.54681</string> </value>
+                <value> <string>964.45882.29366.36147</string> </value>
             </item>
             <item>
                 <key> <string>state</string> </key>
@@ -252,7 +252,7 @@
                     </tuple>
                     <state>
                       <tuple>
-                        <float>1514393372.0</float>
+                        <float>1515514305.55</float>
                         <string>UTC</string>
                       </tuple>
                     </state>
diff --git a/bt5/erp5_web_renderjs_ui/PathTemplateItem/web_page_module/rjs_gadget_erp5_listbox_js.js b/bt5/erp5_web_renderjs_ui/PathTemplateItem/web_page_module/rjs_gadget_erp5_listbox_js.js
index c5bfa03cc8..d32384ad7c 100644
--- a/bt5/erp5_web_renderjs_ui/PathTemplateItem/web_page_module/rjs_gadget_erp5_listbox_js.js
+++ b/bt5/erp5_web_renderjs_ui/PathTemplateItem/web_page_module/rjs_gadget_erp5_listbox_js.js
@@ -28,6 +28,11 @@
                          .innerHTML,
     listbox_tfoot_template = Handlebars.compile(listbox_tfoot_source),
 
+    listbox_nav_source = gadget_klass.__template_element
+                         .getElementById("listbox-nav-template")
+                         .innerHTML,
+    listbox_nav_template = Handlebars.compile(listbox_nav_source),
+
     listbox_source = gadget_klass.__template_element
                          .getElementById("listbox-template")
                          .innerHTML,
@@ -41,88 +46,80 @@
     loading_class_list = ['ui-icon-spinner', 'ui-btn-icon-left'],
     disabled_class = 'ui-disabled';
 
+  function renderSubField(gadget, element, sub_field_json) {
+    sub_field_json.editable = sub_field_json.editable && gadget.state.editable;
+    return gadget.declareGadget(
+      'gadget_erp5_label_field.html',
+      {
+        element: element,
+        scope: sub_field_json.key
+      }
+    )
+      .push(function (cell_gadget) {
+        gadget.props.cell_gadget_list.push(cell_gadget);
+        return cell_gadget.render({
+          field_type: sub_field_json.type,
+          field_json: sub_field_json,
+          label: false
+        });
+      });
+  }
 
-  function renderEditableField(gadget, element, column_list) {
+  function renderEditableField(gadget, element, column_list, field_table) {
     var i,
       promise_list = [],
-      uid_value_dict = {},
-      uid_value,
       column,
       line,
       element_list = element.querySelectorAll(".editable_div");
-    gadget.props.listbox_uid_dict = {};
-    gadget.props.cell_gadget_list = [];
-    function renderSubCell(element, sub_field_json) {
-      sub_field_json.editable = sub_field_json.editable && gadget.state.editable; // XXX 
-      return gadget.declareGadget('gadget_erp5_label_field.html', {element: element, scope: sub_field_json.key})
-        .push(function (cell_gadget) {
-          gadget.props.cell_gadget_list.push(cell_gadget);
-          return cell_gadget.render({
-            field_type: sub_field_json.type,
-            field_json: sub_field_json,
-            label: false
-          });
-        });
-    }
+
     for (i = 0; i < element_list.length; i += 1) {
       column = element_list[i].getAttribute("data-column");
       line = element_list[i].getAttribute("data-line");
-      if (gadget.props.listbox_uid_dict.key === undefined) {
-        gadget.props.listbox_uid_dict.key = gadget.state.allDocs_result.data.rows[line].value["listbox_uid:list"].key;
-        gadget.props.listbox_uid_dict.value = [gadget.state.allDocs_result.data.rows[line].value["listbox_uid:list"].value];
-        uid_value_dict[gadget.state.allDocs_result.data.rows[line].value["listbox_uid:list"].value] = null;
-      } else {
-        uid_value = gadget.state.allDocs_result.data.rows[line].value["listbox_uid:list"].value;
-        if (!uid_value_dict.hasOwnProperty(uid_value)) {
-          uid_value_dict[uid_value] = null;
-          gadget.props.listbox_uid_dict.value.push(uid_value);
-        }
-      }
-      promise_list.push(renderSubCell(element_list[i],
-        gadget.state.allDocs_result.data.rows[line].value[column_list[column][0]] || ""));
+
+      promise_list.push(renderSubField(
+        gadget,
+        element_list[i],
+        field_table[line].cell_list[column] || ""
+      ));
     }
     return RSVP.all(promise_list);
   }
 
+  /**Put resulting `row_list` into `template` together with necessary gadget.state parameters.
 
-  function renderListboxTbody(gadget, template, body_value) {
-    var tmp,
+  First, it removes all similar containers from within the table! Currently it is tricky
+  to have multiple tbody/thead/tfoot elements! Feel free to refactor.
+
+  Example call: renderTablePart(gadget, compiled_template, row_list, "tbody");
+  **/
+  function renderTablePart(gadget, template, row_list, container_name) {
+    var container,
       column_list = JSON.parse(gadget.state.column_list_json);
 
     return gadget.translateHtml(template(
       {
-        "body_value": body_value,
+        "row_list": row_list,
         "show_anchor": gadget.state.show_anchor,
         "column_list": column_list
       }
     ))
-      .push(function (my_html) {
-        tmp = document.createElement("tbody");
-        tmp.innerHTML = my_html;
-        return renderEditableField(gadget, tmp, column_list);
+      .push(function (table_part_html) {
+        container = document.createElement(container_name);
+        container.innerHTML = table_part_html;
+        return renderEditableField(gadget, container, column_list, row_list);
       })
       .push(function () {
         var table =  gadget.element.querySelector("table"),
-          tbody = table.querySelector("tbody");
-        table.removeChild(tbody);
-        table.appendChild(tmp);
+          old_container = table.querySelector(container_name);
+        if (old_container) {
+          table.replaceChild(container, old_container);
+        } else {
+          table.appendChild(container);
+        }
+        return table;
       });
   }
 
-
-  function renderListboxTfoot(gadget, foot) {
-    return gadget.translateHtml(listbox_tfoot_template(
-      {
-        "colspan": foot.colspan,
-        "previous_classname": foot.previous_classname,
-        "previous_url": foot.previous_url,
-        "record": foot.record,
-        "next_classname": foot.next_classname,
-        "next_url": foot.next_url
-      }
-    ));
-  }
-
   /** Clojure to ease finding in lists of lists by the first item **/
   function hasSameFirstItem(a) {
     return function (b) {
@@ -137,7 +134,10 @@
     // Init local properties
     .ready(function () {
       this.props = {
+        // holds references to all editable sub-fields
         cell_gadget_list: [],
+        // ERP5 needs listbox_uid:list with UIDs of editable sub-documents
+        // so it can search for them in REQUEST.form under <field.id>_<sub-document.uid>
         listbox_uid_dict: {}
       };
     })
@@ -272,6 +272,9 @@
             sort_list_json: JSON.stringify(result_list[1] || field_json.sort.map(jioize_sort)),
 
             show_anchor: field_json.show_anchor,
+            show_stat: field_json.show_stat,
+            show_count: field_json.show_count,
+
             line_icon: field_json.line_icon,
             query: field_json.query,
             query_string: query_string,
@@ -438,6 +441,8 @@
           });
       }
 
+      /* Function `fetchLineContent` calls changeState({"allDocs_result": JIO.allDocs()})
+         so this if gets re-evaluated later with allDocs_result defined. */
       if (gadget.state.allDocs_result === undefined) {
         // Trigger line content calculation
         result_queue
@@ -453,7 +458,6 @@
 
       } else if ((modification_dict.hasOwnProperty('show_line_selector')) ||
           (modification_dict.hasOwnProperty('allDocs_result'))) {
-
         // Render the listbox content
         result_queue
           .push(function () {
@@ -463,7 +467,7 @@
               counter;
 
             column_list = JSON.parse(gadget.state.column_list_json);
-
+            // for actual allDocs_result structure see ref:gadget_erp5_jio.js
             if (lines === 0) {
               lines = allDocs_result.data.total_rows;
               counter = allDocs_result.data.total_rows;
@@ -471,7 +475,7 @@
               counter = Math.min(allDocs_result.data.total_rows, lines);
             }
             sort_list = JSON.parse(gadget.state.sort_list_json);
-
+            // Every line points to a sub-document so we need those links
             for (i = 0; i < counter; i += 1) {
               promise_list.push(
                 gadget.getUrlFor({
@@ -492,31 +496,52 @@
                 return RSVP.all(promise_list);
               })
 
-              .push(function (result_list) {
-                var value,
-                  body_value = [],
-                  tr_value = [],
-                  tmp_url,
-                  listbox_tbody_template;
+              .push(function (line_link_list) {
+                var row_list = [],
+                  value,
+                  cell_list,
+                  listbox_tbody_template,
+                  setNonEditable = function (cell) {cell.editable = false; };
+                // reset list of UIDs of editable sub-documents
+                gadget.props.listbox_uid_dict = {
+                  key: undefined,
+                  value: []
+                };
+                // clear list of previous sub-gadgets
+                gadget.props.cell_gadget_list = [];
 
                 for (i = 0; i < counter; i += 1) {
-                  tmp_url = result_list[i];
-                  tr_value = [];
+                  cell_list = [];
                   for (j = 0; j < column_list.length; j += 1) {
                     value = allDocs_result.data.rows[i].value[column_list[j][0]] || "";
-                    tr_value.push({
-                      "type": value.type,
-                      "editable": value.editable && gadget.state.editable,
-                      "href": tmp_url,
-                      "text": value,
-                      "line": i,
-                      "column": j
-                    });
+                    // value can be simply just a value in case of non-editable field
+                    // thus we construct "field_json" manually and insert the value in "default"
+                    if (value.constructor !== Object) {
+                      value = {
+                        'editable': 0,
+                        'default': value
+                      };
+                    }
+                    value.href = line_link_list[i];
+                    value.editable = value.editable && gadget.state.editable;
+                    value.line = i;
+                    value.column = j;
+                    cell_list.push(value);
+                  }
+                  // note row's editable UID into gadget.props.listbox_uid_dict if exists to send it back to ERP5
+                  // together with ListBox data. The listbox_uid_dict has quite surprising structure {key: <key>, value: <uid-array>}
+                  if (allDocs_result.data.rows[i].value['listbox_uid:list'] !== undefined) {
+                    gadget.props.listbox_uid_dict.key = allDocs_result.data.rows[i].value['listbox_uid:list'].key;
+                    gadget.props.listbox_uid_dict.value.push(allDocs_result.data.rows[i].value['listbox_uid:list'].value);
+                    // we could come up with better name than "value" for almost everything ^^
+                  } else {
+                    // if the document does not have listbox_uid:list then no gadget should be editable
+                    cell_list.forEach(setNonEditable);
                   }
-                  body_value.push({
-                    "value": allDocs_result.data.rows[i].value.uid,
-                    "jump": tmp_url,
-                    "tr_value": tr_value,
+                  row_list.push({
+                    "uid": allDocs_result.data.rows[i].value.uid,
+                    "jump": line_link_list[i],
+                    "cell_list": cell_list,
                     "line_icon": gadget.state.line_icon
                   });
                 }
@@ -527,7 +552,7 @@
                   listbox_tbody_template = listbox_hidden_tbody_template;
                 }
 
-                return renderListboxTbody(gadget, listbox_tbody_template, body_value);
+                return renderTablePart(gadget, listbox_tbody_template, row_list, "tbody");
               })
               .push(function () {
                 var prev_param = {},
@@ -551,31 +576,70 @@
 
               })
               .push(function (url_list) {
-                var foot = {};
-                foot.colspan = column_list.length + gadget.state.show_anchor +
-                  (gadget.state.line_icon ? 1 : 0) + gadget.state.show_line_selector;
-                foot.previous_classname = "ui-btn ui-icon-carat-l ui-btn-icon-left responsive ui-first-child";
-                foot.previous_url = url_list[0];
-                foot.next_classname = "ui-btn ui-icon-carat-r ui-btn-icon-right responsive ui-last-child";
-                foot.next_url = url_list[1];
+                var record,
+                  previous_url = url_list[0],
+                  next_url = url_list[1],
+                  previous_classname = "ui-btn ui-icon-carat-l ui-btn-icon-left responsive ui-first-child",
+                  next_classname = "ui-btn ui-icon-carat-r ui-btn-icon-right responsive ui-last-child";
+
                 if ((gadget.state.begin_from === 0) && (counter === 0)) {
-                  foot.record = variable.translated_no_record;
+                  record = variable.translated_no_record;
                 } else if ((allDocs_result.data.rows.length <= lines) && (gadget.state.begin_from === 0)) {
-                  foot.record = counter + " " + variable.translated_records;
+                  record = counter + " " + variable.translated_records;
                 } else {
-                  foot.record = variable.translated_records + " " + (((gadget.state.begin_from + lines) / lines - 1) * lines + 1) + " - " + (((gadget.state.begin_from + lines) / lines - 1) * lines + counter);
+                  record = variable.translated_records + " " + (((gadget.state.begin_from + lines) / lines - 1) * lines + 1) + " - " + (((gadget.state.begin_from + lines) / lines - 1) * lines + counter);
                 }
 
                 if (gadget.state.begin_from === 0) {
-                  foot.previous_classname += " ui-disabled";
+                  previous_classname += " ui-disabled";
                 }
                 if (allDocs_result.data.rows.length <= lines) {
-                  foot.next_classname += " ui-disabled";
+                  next_classname += " ui-disabled";
                 }
-                return renderListboxTfoot(gadget, foot);
+                return gadget.translateHtml(
+                  listbox_nav_template({
+                    "previous_classname": previous_classname,
+                    "previous_url": previous_url,
+                    "record": record,
+                    "next_classname": next_classname,
+                    "next_url": next_url
+                  })
+                )
+                  .push(function (listbox_nav_html) {
+                    gadget.element.querySelector('nav').innerHTML = listbox_nav_html;
+                  });
               })
-              .push(function (my_html) {
-                gadget.element.querySelector(".tfoot").innerHTML = my_html;
+              .push(function () {
+                var result_sum = (gadget.state.allDocs_result.sum || {}).rows || [], // render summary footer if available
+                  summary = result_sum.map(function (row, row_index) {
+                    var row_editability = row['listbox_uid:list'] !== undefined;
+                    return {
+                      "uid": 'summary' + row_index,
+                      "cell_list": column_list.map(function (col_name, col_index) {
+                        var field_json = row.value[col_name[0]] || "";
+                        if (field_json.constructor !== Object) {
+                          field_json = {'default': field_json, 'editable': 0};
+                        }
+                        field_json.editable = field_json.editable && row_editability;
+                        field_json.column = col_index;
+                        field_json.line = row_index;
+                        return field_json;
+                      })
+                    };
+                  }),
+                  element;
+
+                if (counter === 0) {
+                  // do not render footer (summary) when no data in Listbox because it is ugly
+                  element = gadget.element.querySelector("table tfoot tr");
+                  if (element !== null) {
+                    element.remove();
+                  }
+                  return null;
+                }
+                return renderTablePart(gadget, listbox_tfoot_template, summary, "tfoot");
+              })
+              .push(function () {
                 var loading_element_classList = gadget.element.querySelector(".listboxloader").classList;
                 loading_element_classList.remove.apply(loading_element_classList, loading_class_list);
               });
@@ -616,7 +680,8 @@
 
       var gadget = this,
         select_list = [],
-        limit_options,
+        limit_options = [],
+        aggregation_option_list = [],
         column_list = JSON.parse(gadget.state.column_list_json),
         i;
 
@@ -631,6 +696,12 @@
         limit_options = [gadget.state.begin_from, gadget.state.lines + 1];
       }
 
+      if (gadget.state.show_stat === true) {
+        aggregation_option_list.push("sum");
+      }
+      if (gadget.state.show_count === true) {
+        aggregation_option_list.push("count");
+      }
 
       return gadget.jio_allDocs({
         // XXX Not jIO compatible, but until a better api is found...
@@ -638,6 +709,7 @@
         "query": gadget.state.query_string,
         "limit": limit_options,
         "select_list": select_list,
+        // "aggregation": aggregation_option_list
         "sort_on": JSON.parse(gadget.state.sort_list_json)
       })
         .push(function (result) {
@@ -647,7 +719,7 @@
 
         }, function (error) {
           // do not crash interface if allDocs fails
-          //this will catch all error, not only search criteria invalid error
+          // this will catch all error, not only search criteria invalid error
           if (error instanceof RSVP.CancellationError) {
             throw error;
           }
diff --git a/bt5/erp5_web_renderjs_ui/PathTemplateItem/web_page_module/rjs_gadget_erp5_listbox_js.xml b/bt5/erp5_web_renderjs_ui/PathTemplateItem/web_page_module/rjs_gadget_erp5_listbox_js.xml
index 950e8869f2..d9f492a851 100644
--- a/bt5/erp5_web_renderjs_ui/PathTemplateItem/web_page_module/rjs_gadget_erp5_listbox_js.xml
+++ b/bt5/erp5_web_renderjs_ui/PathTemplateItem/web_page_module/rjs_gadget_erp5_listbox_js.xml
@@ -75,12 +75,6 @@
               <none/>
             </value>
         </item>
-        <item>
-            <key> <string>content_type</string> </key>
-            <value>
-              <none/>
-            </value>
-        </item>
         <item>
             <key> <string>default_reference</string> </key>
             <value> <string>gadget_erp5_field_listbox.js</string> </value>
@@ -242,7 +236,7 @@
             </item>
             <item>
                 <key> <string>serial</string> </key>
-                <value> <string>964.28739.16428.44868</string> </value>
+                <value> <string>964.47502.56518.25890</string> </value>
             </item>
             <item>
                 <key> <string>state</string> </key>
@@ -260,7 +254,7 @@
                     </tuple>
                     <state>
                       <tuple>
-                        <float>1514478789.32</float>
+                        <float>1515602869.96</float>
                         <string>UTC</string>
                       </tuple>
                     </state>
diff --git a/bt5/erp5_web_renderjs_ui/PathTemplateItem/web_page_module/rjs_gadget_erp5_nojqm_css.css b/bt5/erp5_web_renderjs_ui/PathTemplateItem/web_page_module/rjs_gadget_erp5_nojqm_css.css
index 0f80fd26d2..dbea631c60 100644
--- a/bt5/erp5_web_renderjs_ui/PathTemplateItem/web_page_module/rjs_gadget_erp5_nojqm_css.css
+++ b/bt5/erp5_web_renderjs_ui/PathTemplateItem/web_page_module/rjs_gadget_erp5_nojqm_css.css
@@ -1305,24 +1305,30 @@ div[data-gadget-scope='erp5_searchfield'] div.search_parsed_value button {
 .document_table table {
   width: 100%;
   text-align: left;
+  /* end-of tbody, tfoot*/
 }
 .document_table table th,
 .document_table table td {
   vertical-align: middle;
+  padding: 3pt;
 }
-.document_table table thead {
+.document_table table thead,
+.document_table table tfoot {
   background-color: #0E81C2;
   color: #FFFFFF;
 }
-.document_table table thead a {
+.document_table table thead a,
+.document_table table tfoot a {
   color: #FFFFFF;
   text-decoration: underline;
 }
-.document_table table thead tr th {
+.document_table table thead tr th,
+.document_table table tfoot tr th {
   padding: 6pt 3pt;
 }
 @media not screen and (min-width: 45em) {
-  .document_table table thead {
+  .document_table table thead,
+  .document_table table tfoot {
     display: none;
   }
 }
@@ -1342,7 +1348,6 @@ div[data-gadget-scope='erp5_searchfield'] div.search_parsed_value button {
 @media not screen and (max-width: 85em), only screen and (min-width: 45em) and (max-width: 85em) {
   .document_table table tbody a {
     display: block;
-    padding: 3pt;
   }
 }
 @media not screen and (min-width: 45em) {
@@ -1411,41 +1416,46 @@ div[data-gadget-scope='erp5_searchfield'] div.search_parsed_value button {
     content: " ~ ";
   }
 }
-.document_table table tfoot .ui-controlgroup-controls {
+.document_table nav {
   display: flex;
   padding-top: 6pt;
   border-top: 2px solid rgba(0, 0, 0, 0.14902);
 }
-.document_table table tfoot .ui-controlgroup-controls span {
+.document_table nav span {
   opacity: .3;
   flex: 2;
   text-align: right;
+  float: right;
 }
-.document_table table tfoot .ui-controlgroup-controls a {
+.document_table nav a {
   padding: 6pt;
   border: 1px solid rgba(0, 0, 0, 0.14);
   border-radius: 0.325em;
   margin-right: 6pt;
 }
-.document_table table tfoot .ui-controlgroup-controls a::before {
+.document_table nav a::before {
   margin-right: 6pt;
 }
-.document_table table tfoot .ui-controlgroup-controls a:hover,
-.document_table table tfoot .ui-controlgroup-controls a:active {
+.document_table nav a:hover,
+.document_table nav a:active {
   background-color: #e0e0e0;
 }
-.document_table table tfoot .ui-controlgroup-controls a:last-of-type {
+.document_table nav a:last-of-type {
   margin-right: 0;
 }
+.document_table nav a:hover,
+.document_table nav a:active {
+  background-color: #e0e0e0;
+}
 @media not screen and (min-width: 45em) {
-  .document_table table tfoot .ui-controlgroup-controls a {
+  .document_table nav a {
     overflow: hidden;
     text-indent: -9999px;
     white-space: nowrap;
   }
 }
 @media not screen and (min-width: 45em) {
-  .document_table table tfoot .ui-controlgroup-controls a::before {
+  .document_table nav a::before {
     float: left;
     text-indent: 6pt;
   }
diff --git a/bt5/erp5_web_renderjs_ui/SkinTemplateItem/portal_skins/erp5_web_renderjs_ui/erp5css.less.txt b/bt5/erp5_web_renderjs_ui/SkinTemplateItem/portal_skins/erp5_web_renderjs_ui/erp5css.less.txt
index e129e603a7..70ae43683d 100644
--- a/bt5/erp5_web_renderjs_ui/SkinTemplateItem/portal_skins/erp5_web_renderjs_ui/erp5css.less.txt
+++ b/bt5/erp5_web_renderjs_ui/SkinTemplateItem/portal_skins/erp5_web_renderjs_ui/erp5css.less.txt
@@ -1523,9 +1523,10 @@ div[data-gadget-scope='erp5_searchfield'] {
     th, td {
       // line-height: 1.5em;
       vertical-align: middle;
+      padding: @half-margin-size;
     }
 
-    thead {
+    thead, tfoot {
       background-color: @colorsubheaderbackground;
       color: @white;
 
@@ -1565,7 +1566,6 @@ div[data-gadget-scope='erp5_searchfield'] {
       @media @desktop, @tablet {
         a {
           display: block;
-          padding: @half-margin-size;
         }
       }
 
@@ -1646,40 +1646,43 @@ div[data-gadget-scope='erp5_searchfield'] {
         }
       }
     }
+  }
 
+  nav {
+    display: flex;
+    padding-top: @margin-size;
+    border-top: 2px solid rgba(0, 0, 0, 0.14902);
 
-    tfoot .ui-controlgroup-controls {
-      display: flex;
-      padding-top: @margin-size;
-      border-top: 2px solid rgba(0, 0, 0, 0.14902);
+    span {
+      opacity: .3;
+      flex: 2;
+      text-align: right;
+      float: right;
+    }
+    a {
+      .button();
 
-      span {
-        opacity: .3;
-        flex: 2;
-        text-align: right;
+      margin-right: @margin-size;
+      &:last-of-type {
+        margin-right: 0;
       }
-      a {
-        .button();
 
-        margin-right: @margin-size;
-        &:last-of-type {
-          margin-right: 0;
-        }
+      &:hover, &:active {
+        background-color: @colorblocklinkbackground;
+      }
 
-        @media @smartphone {
-          .hide_text(@width: initial);
-        }
+      @media @smartphone {
+        .hide_text(@width: initial);
+      }
 
-        &::before {
+      &::before {
 
-          @media @smartphone {
-            float: left;
-            text-indent: @margin-size;
-          }
+        @media @smartphone {
+          float: left;
+          text-indent: @margin-size;
         }
       }
     }
-
   }
 }
 
diff --git a/bt5/erp5_web_renderjs_ui_test/PathTemplateItem/portal_tests/renderjs_ui_listbox_zuite/testEmptyListboxWithStat.xml b/bt5/erp5_web_renderjs_ui_test/PathTemplateItem/portal_tests/renderjs_ui_listbox_zuite/testEmptyListboxWithStat.xml
new file mode 100644
index 0000000000..2fc3ac7b12
--- /dev/null
+++ b/bt5/erp5_web_renderjs_ui_test/PathTemplateItem/portal_tests/renderjs_ui_listbox_zuite/testEmptyListboxWithStat.xml
@@ -0,0 +1,58 @@
+<?xml version="1.0"?>
+<ZopeData>
+  <record id="1" aka="AAAAAAAAAAE=">
+    <pickle>
+      <global name="ZopePageTemplate" module="Products.PageTemplates.ZopePageTemplate"/>
+    </pickle>
+    <pickle>
+      <dictionary>
+        <item>
+            <key> <string>_bind_names</string> </key>
+            <value>
+              <object>
+                <klass>
+                  <global name="NameAssignments" module="Shared.DC.Scripts.Bindings"/>
+                </klass>
+                <tuple/>
+                <state>
+                  <dictionary>
+                    <item>
+                        <key> <string>_asgns</string> </key>
+                        <value>
+                          <dictionary>
+                            <item>
+                                <key> <string>name_subpath</string> </key>
+                                <value> <string>traverse_subpath</string> </value>
+                            </item>
+                          </dictionary>
+                        </value>
+                    </item>
+                  </dictionary>
+                </state>
+              </object>
+            </value>
+        </item>
+        <item>
+            <key> <string>content_type</string> </key>
+            <value> <string>text/html</string> </value>
+        </item>
+        <item>
+            <key> <string>expand</string> </key>
+            <value> <int>0</int> </value>
+        </item>
+        <item>
+            <key> <string>id</string> </key>
+            <value> <string>testEmptyListboxWithStat</string> </value>
+        </item>
+        <item>
+            <key> <string>output_encoding</string> </key>
+            <value> <string>utf-8</string> </value>
+        </item>
+        <item>
+            <key> <string>title</string> </key>
+            <value> <unicode></unicode> </value>
+        </item>
+      </dictionary>
+    </pickle>
+  </record>
+</ZopeData>
diff --git a/bt5/erp5_web_renderjs_ui_test/PathTemplateItem/portal_tests/renderjs_ui_listbox_zuite/testEmptyListboxWithStat.zpt b/bt5/erp5_web_renderjs_ui_test/PathTemplateItem/portal_tests/renderjs_ui_listbox_zuite/testEmptyListboxWithStat.zpt
new file mode 100644
index 0000000000..59a80ead79
--- /dev/null
+++ b/bt5/erp5_web_renderjs_ui_test/PathTemplateItem/portal_tests/renderjs_ui_listbox_zuite/testEmptyListboxWithStat.zpt
@@ -0,0 +1,47 @@
+<html xmlns:tal="http://xml.zope.org/namespaces/tal"
+      xmlns:metal="http://xml.zope.org/namespaces/metal">
+<!--
+Ensure empty listbox does not show stat line even though it receives stat data.
+-->
+<head>
+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+<title>Test RenderJS UI ListBox No Stat Line on No Data</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+<thead>
+<tr><td rowspan="1" colspan="3">Test RenderJS UI ListBox No Stat Line on No Data</td></tr>
+</thead><tbody>
+<tal:block metal:use-macro="here/Zuite_CommonTemplate/macros/init" />
+
+<!-- Clean Up -->
+<tr><td>open</td>
+    <td>${base_url}/foo_module/ListBoxZuite_reset</td><td></td></tr>
+<tr><td>assertTextPresent</td>
+    <td>Reset Successfully.</td><td></td></tr>
+
+<!-- Shortcut for full renderjs url -->
+<tr><td>store</td>
+    <td>${base_url}/web_site_module/renderjs_runner</td>
+    <td>renderjs_url</td></tr>
+
+<tal:block metal:use-macro="here/Zuite_CommonTemplate/macros/wait_for_activities" />
+
+<!-- Let's set up the default sort correctly: id | ASC -->
+<tr><td>open</td>
+    <td>${base_url}/FooModule_viewFooList/listbox/ListBox_setPropertyList?field_stat_method=FooModule_statMethod</td><td></td></tr>
+<tr><td>assertTextPresent</td>
+    <td>Set Successfully.</td><td></td></tr>
+
+<tr><td>open</td>
+    <td>${renderjs_url}/#/foo_module</td><td></td></tr>
+<tr><td>waitForElementPresent</td>
+    <td>//div[contains(@data-gadget-url, 'gadget_erp5_field_listbox.html')]//nav/span[@data-i18n="No records"]</td><td></td></tr>
+
+<tr><td>assertElementNotPresent</td>
+    <td>//div[contains(@data-gadget-url, 'gadget_erp5_field_listbox.html')]//table/tfoot/tr</td>
+    <td></td></tr>
+
+</tbody></table>
+</body>
+</html>
\ No newline at end of file
diff --git a/bt5/erp5_web_renderjs_ui_test/PathTemplateItem/portal_tests/renderjs_ui_listbox_zuite/testHideItem.zpt b/bt5/erp5_web_renderjs_ui_test/PathTemplateItem/portal_tests/renderjs_ui_listbox_zuite/testHideItem.zpt
index b2c1ee59c8..0606092e08 100644
--- a/bt5/erp5_web_renderjs_ui_test/PathTemplateItem/portal_tests/renderjs_ui_listbox_zuite/testHideItem.zpt
+++ b/bt5/erp5_web_renderjs_ui_test/PathTemplateItem/portal_tests/renderjs_ui_listbox_zuite/testHideItem.zpt
@@ -128,11 +128,6 @@
   <td>//thead/tr/th[3]</td>
   <td>Quantity</td>
 </tr>
-<tr>
-  <td>assertElementPresent</td>
-  <td>//tfoot/tr/th[@colspan="3"]</td>
-  <td></td>
-</tr>
 
 
 <tr>
@@ -169,11 +164,6 @@
   <td>//thead/tr/th[4]</td>
   <td>Quantity</td>
 </tr>
-<tr>
-  <td>assertElementPresent</td>
-  <td>//tfoot/tr/th[@colspan="4"]</td>
-  <td></td>
-</tr>
 
 <!-- Line checkbox -->
 
@@ -233,11 +223,6 @@
   <td>//thead/tr/th[3]</td>
   <td>Quantity</td>
 </tr>
-<tr>
-  <td>assertElementPresent</td>
-  <td>//tfoot/tr/th[@colspan="3"]</td>
-  <td></td>
-</tr>
 
 <!-- only one element present -->
 <tr>
diff --git a/bt5/erp5_web_renderjs_ui_test/PathTemplateItem/portal_tests/renderjs_ui_listbox_zuite/testStatColumns.xml b/bt5/erp5_web_renderjs_ui_test/PathTemplateItem/portal_tests/renderjs_ui_listbox_zuite/testStatColumns.xml
new file mode 100644
index 0000000000..f48a19ffe3
--- /dev/null
+++ b/bt5/erp5_web_renderjs_ui_test/PathTemplateItem/portal_tests/renderjs_ui_listbox_zuite/testStatColumns.xml
@@ -0,0 +1,58 @@
+<?xml version="1.0"?>
+<ZopeData>
+  <record id="1" aka="AAAAAAAAAAE=">
+    <pickle>
+      <global name="ZopePageTemplate" module="Products.PageTemplates.ZopePageTemplate"/>
+    </pickle>
+    <pickle>
+      <dictionary>
+        <item>
+            <key> <string>_bind_names</string> </key>
+            <value>
+              <object>
+                <klass>
+                  <global name="NameAssignments" module="Shared.DC.Scripts.Bindings"/>
+                </klass>
+                <tuple/>
+                <state>
+                  <dictionary>
+                    <item>
+                        <key> <string>_asgns</string> </key>
+                        <value>
+                          <dictionary>
+                            <item>
+                                <key> <string>name_subpath</string> </key>
+                                <value> <string>traverse_subpath</string> </value>
+                            </item>
+                          </dictionary>
+                        </value>
+                    </item>
+                  </dictionary>
+                </state>
+              </object>
+            </value>
+        </item>
+        <item>
+            <key> <string>content_type</string> </key>
+            <value> <string>text/html</string> </value>
+        </item>
+        <item>
+            <key> <string>expand</string> </key>
+            <value> <int>0</int> </value>
+        </item>
+        <item>
+            <key> <string>id</string> </key>
+            <value> <string>testStatColumns</string> </value>
+        </item>
+        <item>
+            <key> <string>output_encoding</string> </key>
+            <value> <string>utf-8</string> </value>
+        </item>
+        <item>
+            <key> <string>title</string> </key>
+            <value> <unicode></unicode> </value>
+        </item>
+      </dictionary>
+    </pickle>
+  </record>
+</ZopeData>
diff --git a/bt5/erp5_web_renderjs_ui_test/PathTemplateItem/portal_tests/renderjs_ui_listbox_zuite/testStatColumns.zpt b/bt5/erp5_web_renderjs_ui_test/PathTemplateItem/portal_tests/renderjs_ui_listbox_zuite/testStatColumns.zpt
new file mode 100644
index 0000000000..6bb81377d9
--- /dev/null
+++ b/bt5/erp5_web_renderjs_ui_test/PathTemplateItem/portal_tests/renderjs_ui_listbox_zuite/testStatColumns.zpt
@@ -0,0 +1,73 @@
+<html xmlns:tal="http://xml.zope.org/namespaces/tal"
+      xmlns:metal="http://xml.zope.org/namespaces/metal">
+<!--
+Ensure Stat Column methods are executed correctly and result displayed in tfoot element of the listbox table.
+
+-  if anchor, then text "Total" is present
+-  columns which are not present in Stat Columns do not display any data
+-->
+<head>
+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+<title>Test RenderJS ListBox Stat Columns</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+<thead>
+<tr><td rowspan="1" colspan="3">Test RenderJS ListBox Stat Columns</td></tr>
+</thead><tbody>
+<tal:block metal:use-macro="here/Zuite_CommonTemplate/macros/init" />
+
+<!-- Clean Up -->
+<tr><td>open</td>
+    <td>${base_url}/foo_module/ListBoxZuite_reset</td><td></td></tr>
+<tr><td>assertTextPresent</td>
+    <td>Reset Successfully.</td><td></td></tr>
+
+<!-- Shortcut for full renderjs url -->
+<tr><td>store</td>
+    <td>${base_url}/web_site_module/renderjs_runner</td>
+    <td>renderjs_url</td></tr>
+
+<!-- Create Foo objects with IDs 0-9 -->
+<tr><td>open</td>
+    <td>${base_url}/foo_module/FooModule_createObjects?start:int=1&amp;num:int=2</td><td></td></tr>
+<tr><td>assertTextPresent</td>
+    <td>Created Successfully.</td><td></td></tr>
+
+<tal:block metal:use-macro="here/Zuite_CommonTemplate/macros/wait_for_activities" />
+
+<!-- Let's set up stat column property on listbox -->
+<tr><td>open</td>
+    <td>${base_url}/FooModule_viewFooList/listbox/ListBox_setPropertyList?field_stat_columns=getQuantity+%7C+FooModule_statQuantity+%0A+title+%7C+FooModule_statTitle</td><td></td></tr>
+<tr><td>assertTextPresent</td>
+    <td>Set Successfully.</td><td></td></tr>
+
+<tr><td>open</td>
+    <td>${renderjs_url}/#/foo_module</td><td></td></tr>
+<tr><td>waitForElementPresent</td>
+    <td>//div[contains(@data-gadget-url, 'gadget_erp5_field_listbox.html')]//nav/span[@data-i18n="2 Records"]</td><td></td></tr>
+
+<tr><td>store</td>
+    <td>//div[contains(@data-gadget-url, 'gadget_erp5_field_listbox.html')]//table</td>
+    <td>listbox_table</td></tr>
+
+<!-- Default sort on ID column has to be ASCENDING -->
+<tr><td>assertFloat</td>
+    <td>${listbox_table}/tbody/tr[1]/td[3]/a</td>
+    <td>9</td></tr>
+<tr><td>assertFloat</td>
+    <td>${listbox_table}/tbody/tr[2]/td[3]/a</td>
+    <td>8</td></tr>
+<tr><td>assertText</td><!-- This tests that "Total" appears when first column has no stat defined -->
+    <td>${listbox_table}/tfoot/tr[1]/td[1]</td>
+    <td>Total</td></tr>
+<tr><td>assertText</td><!-- Test multiple Stat Columns -->
+    <td>${listbox_table}/tfoot/tr[1]/td[2]</td>
+    <td>Foos</td></tr>
+<tr><td>assertFloat</td>
+    <td>${listbox_table}/tfoot/tr[1]/td[3]</td>
+    <td>17</td></tr>
+
+</tbody></table>
+</body>
+</html>
\ No newline at end of file
diff --git a/bt5/erp5_web_renderjs_ui_test/PathTemplateItem/portal_tests/renderjs_ui_listbox_zuite/testStatMethod.xml b/bt5/erp5_web_renderjs_ui_test/PathTemplateItem/portal_tests/renderjs_ui_listbox_zuite/testStatMethod.xml
new file mode 100644
index 0000000000..dcd6f40d26
--- /dev/null
+++ b/bt5/erp5_web_renderjs_ui_test/PathTemplateItem/portal_tests/renderjs_ui_listbox_zuite/testStatMethod.xml
@@ -0,0 +1,58 @@
+<?xml version="1.0"?>
+<ZopeData>
+  <record id="1" aka="AAAAAAAAAAE=">
+    <pickle>
+      <global name="ZopePageTemplate" module="Products.PageTemplates.ZopePageTemplate"/>
+    </pickle>
+    <pickle>
+      <dictionary>
+        <item>
+            <key> <string>_bind_names</string> </key>
+            <value>
+              <object>
+                <klass>
+                  <global name="NameAssignments" module="Shared.DC.Scripts.Bindings"/>
+                </klass>
+                <tuple/>
+                <state>
+                  <dictionary>
+                    <item>
+                        <key> <string>_asgns</string> </key>
+                        <value>
+                          <dictionary>
+                            <item>
+                                <key> <string>name_subpath</string> </key>
+                                <value> <string>traverse_subpath</string> </value>
+                            </item>
+                          </dictionary>
+                        </value>
+                    </item>
+                  </dictionary>
+                </state>
+              </object>
+            </value>
+        </item>
+        <item>
+            <key> <string>content_type</string> </key>
+            <value> <string>text/html</string> </value>
+        </item>
+        <item>
+            <key> <string>expand</string> </key>
+            <value> <int>0</int> </value>
+        </item>
+        <item>
+            <key> <string>id</string> </key>
+            <value> <string>testStatMethod</string> </value>
+        </item>
+        <item>
+            <key> <string>output_encoding</string> </key>
+            <value> <string>utf-8</string> </value>
+        </item>
+        <item>
+            <key> <string>title</string> </key>
+            <value> <unicode></unicode> </value>
+        </item>
+      </dictionary>
+    </pickle>
+  </record>
+</ZopeData>
diff --git a/bt5/erp5_web_renderjs_ui_test/PathTemplateItem/portal_tests/renderjs_ui_listbox_zuite/testStatMethod.zpt b/bt5/erp5_web_renderjs_ui_test/PathTemplateItem/portal_tests/renderjs_ui_listbox_zuite/testStatMethod.zpt
new file mode 100644
index 0000000000..3e1a3666d4
--- /dev/null
+++ b/bt5/erp5_web_renderjs_ui_test/PathTemplateItem/portal_tests/renderjs_ui_listbox_zuite/testStatMethod.zpt
@@ -0,0 +1,74 @@
+<html xmlns:tal="http://xml.zope.org/namespaces/tal"
+      xmlns:metal="http://xml.zope.org/namespaces/metal">
+<!--
+Ensure stat_method gets executed and result displayed in tfoot element of the listbox table.
+
+-  if anchor, then text "Total" is present
+-  columns for which stat_method does not return any data remain empty
+-->
+<head>
+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+<title>Test RenderJS UI ListBox Stat Method</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+<thead>
+<tr><td rowspan="1" colspan="3">Test RenderJS UI ListBox Stat Method</td></tr>
+</thead><tbody>
+<tal:block metal:use-macro="here/Zuite_CommonTemplate/macros/init" />
+
+<!-- Clean Up -->
+<tr><td>open</td>
+    <td>${base_url}/foo_module/ListBoxZuite_reset</td><td></td></tr>
+<tr><td>assertTextPresent</td>
+    <td>Reset Successfully.</td><td></td></tr>
+
+<!-- Shortcut for full renderjs url -->
+<tr><td>store</td>
+    <td>${base_url}/web_site_module/renderjs_runner</td>
+    <td>renderjs_url</td></tr>
+
+<!-- Create Foo objects with IDs 0-9 -->
+<tr><td>open</td>
+    <td>${base_url}/foo_module/FooModule_createObjects?start:int=1&amp;num:int=3</td><td></td></tr>
+<tr><td>assertTextPresent</td>
+    <td>Created Successfully.</td><td></td></tr>
+
+<tal:block metal:use-macro="here/Zuite_CommonTemplate/macros/wait_for_activities" />
+
+<!-- Let's set up the default sort correctly: id | ASC -->
+<tr><td>open</td>
+    <td>${base_url}/FooModule_viewFooList/listbox/ListBox_setPropertyList?field_stat_method=FooModule_statMethod</td><td></td></tr>
+<tr><td>assertTextPresent</td>
+    <td>Set Successfully.</td><td></td></tr>
+
+<tr><td>open</td>
+    <td>${renderjs_url}/#/foo_module</td><td></td></tr>
+<tr><td>waitForElementPresent</td>
+    <td>//div[contains(@data-gadget-url, 'gadget_erp5_field_listbox.html')]//nav/span[@data-i18n="3 Records"]</td><td></td></tr>
+
+<tr><td>store</td>
+    <td>//div[contains(@data-gadget-url, 'gadget_erp5_field_listbox.html')]//table</td>
+    <td>listbox_table</td></tr>
+
+<!-- Default sort on ID column has to be ASCENDING -->
+<tr><td>assertFloat</td>
+    <td>${listbox_table}/tbody/tr[1]/td[3]/a</td>
+    <td>9</td></tr>
+<tr><td>assertFloat</td>
+    <td>${listbox_table}/tbody/tr[2]/td[3]/a</td>
+    <td>8</td></tr>
+<tr><td>assertFloat</td>
+    <td>${listbox_table}/tbody/tr[3]/td[3]/a</td>
+    <td>7</td></tr>
+<tr><td>assertFloat</td><!-- This tests that "Total" does not appear when first column has stat defined -->
+    <td>${listbox_table}/tfoot/tr[1]/td[1]</td>
+    <td>6</td></tr>
+<tr><td>assertFloat</td>
+    <td>${listbox_table}/tfoot/tr[1]/td[3]</td>
+    <td>24</td></tr>
+
+
+</tbody></table>
+</body>
+</html>
\ No newline at end of file
diff --git a/bt5/erp5_web_renderjs_ui_test/PathTemplateItem/portal_tests/renderjs_ui_listbox_zuite/testStatMissing.xml b/bt5/erp5_web_renderjs_ui_test/PathTemplateItem/portal_tests/renderjs_ui_listbox_zuite/testStatMissing.xml
new file mode 100644
index 0000000000..cde2def16c
--- /dev/null
+++ b/bt5/erp5_web_renderjs_ui_test/PathTemplateItem/portal_tests/renderjs_ui_listbox_zuite/testStatMissing.xml
@@ -0,0 +1,58 @@
+<?xml version="1.0"?>
+<ZopeData>
+  <record id="1" aka="AAAAAAAAAAE=">
+    <pickle>
+      <global name="ZopePageTemplate" module="Products.PageTemplates.ZopePageTemplate"/>
+    </pickle>
+    <pickle>
+      <dictionary>
+        <item>
+            <key> <string>_bind_names</string> </key>
+            <value>
+              <object>
+                <klass>
+                  <global name="NameAssignments" module="Shared.DC.Scripts.Bindings"/>
+                </klass>
+                <tuple/>
+                <state>
+                  <dictionary>
+                    <item>
+                        <key> <string>_asgns</string> </key>
+                        <value>
+                          <dictionary>
+                            <item>
+                                <key> <string>name_subpath</string> </key>
+                                <value> <string>traverse_subpath</string> </value>
+                            </item>
+                          </dictionary>
+                        </value>
+                    </item>
+                  </dictionary>
+                </state>
+              </object>
+            </value>
+        </item>
+        <item>
+            <key> <string>content_type</string> </key>
+            <value> <string>text/html</string> </value>
+        </item>
+        <item>
+            <key> <string>expand</string> </key>
+            <value> <int>0</int> </value>
+        </item>
+        <item>
+            <key> <string>id</string> </key>
+            <value> <string>testStatMissing</string> </value>
+        </item>
+        <item>
+            <key> <string>output_encoding</string> </key>
+            <value> <string>utf-8</string> </value>
+        </item>
+        <item>
+            <key> <string>title</string> </key>
+            <value> <unicode></unicode> </value>
+        </item>
+      </dictionary>
+    </pickle>
+  </record>
+</ZopeData>
diff --git a/bt5/erp5_web_renderjs_ui_test/PathTemplateItem/portal_tests/renderjs_ui_listbox_zuite/testStatMissing.zpt b/bt5/erp5_web_renderjs_ui_test/PathTemplateItem/portal_tests/renderjs_ui_listbox_zuite/testStatMissing.zpt
new file mode 100644
index 0000000000..ea6584cd7a
--- /dev/null
+++ b/bt5/erp5_web_renderjs_ui_test/PathTemplateItem/portal_tests/renderjs_ui_listbox_zuite/testStatMissing.zpt
@@ -0,0 +1,53 @@
+<html xmlns:tal="http://xml.zope.org/namespaces/tal"
+      xmlns:metal="http://xml.zope.org/namespaces/metal">
+<!--
+Ensure no stat line is displayed when no stat_method and no stat_columns are defined.
+-->
+<head>
+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+<title>Test RenderJS UI ListBox Stat Missing</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+<thead>
+<tr><td rowspan="1" colspan="3">Test RenderJS UI ListBox Stat Missing</td></tr>
+</thead><tbody>
+<tal:block metal:use-macro="here/Zuite_CommonTemplate/macros/init" />
+
+<!-- Clean Up -->
+<tr><td>open</td>
+    <td>${base_url}/foo_module/ListBoxZuite_reset</td><td></td></tr>
+<tr><td>assertTextPresent</td>
+    <td>Reset Successfully.</td><td></td></tr>
+
+<!-- Shortcut for full renderjs url -->
+<tr><td>store</td>
+    <td>${base_url}/web_site_module/renderjs_runner</td>
+    <td>renderjs_url</td></tr>
+
+<!-- Create Foo objects with IDs 0-9 -->
+<tr><td>open</td>
+    <td>${base_url}/foo_module/FooModule_createObjects?start:int=1&amp;num:int=3</td><td></td></tr>
+<tr><td>assertTextPresent</td>
+    <td>Created Successfully.</td><td></td></tr>
+
+<tal:block metal:use-macro="here/Zuite_CommonTemplate/macros/wait_for_activities" />
+
+<!-- Let's set up the default sort correctly: id | ASC -->
+<tr><td>open</td>
+    <td>${base_url}/FooModule_viewFooList/listbox/ListBox_setPropertyList</td><td></td></tr>
+<tr><td>assertTextPresent</td>
+    <td>Set Successfully.</td><td></td></tr>
+
+<tr><td>open</td>
+    <td>${renderjs_url}/#/foo_module</td><td></td></tr>
+<tr><td>waitForElementPresent</td>
+    <td>//div[contains(@data-gadget-url, 'gadget_erp5_field_listbox.html')]//nav/span[@data-i18n="3 Records"]</td><td></td></tr>
+
+<tr><td>assertElementNotPresent</td>
+    <td>//div[contains(@data-gadget-url, 'gadget_erp5_field_listbox.html')]//table/tfoot/tr</td>
+    <td></td></tr>
+
+</tbody></table>
+</body>
+</html>
\ No newline at end of file
diff --git a/bt5/erp5_web_renderjs_ui_test/SkinTemplateItem/portal_skins/erp5_web_renderjs_ui_test/Zuite_CommonTemplateForRenderjsUi.zpt b/bt5/erp5_web_renderjs_ui_test/SkinTemplateItem/portal_skins/erp5_web_renderjs_ui_test/Zuite_CommonTemplateForRenderjsUi.zpt
index b0d0593a1e..ca8bb820e4 100644
--- a/bt5/erp5_web_renderjs_ui_test/SkinTemplateItem/portal_skins/erp5_web_renderjs_ui_test/Zuite_CommonTemplateForRenderjsUi.zpt
+++ b/bt5/erp5_web_renderjs_ui_test/SkinTemplateItem/portal_skins/erp5_web_renderjs_ui_test/Zuite_CommonTemplateForRenderjsUi.zpt
@@ -247,7 +247,7 @@
     </tr>
     <tr>
       <td>waitForElementPresent</td>
-      <td>//div[contains(@data-gadget-url, 'gadget_erp5_field_listbox.html')]//tfoot//span[contains(@data-i18n, "Records")]</td>
+      <td>//div[contains(@data-gadget-url, 'gadget_erp5_field_listbox.html')]//nav//span[contains(@data-i18n, "Records")]</td>
       <td></td>
     </tr>
     <tr>
-- 
2.30.9