You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ofbiz.apache.org by da...@apache.org on 2022/11/15 10:26:02 UTC

[ofbiz-framework] branch trunk updated: Improved: MacroFormRenderer refactoring of datetime fields (OFBIZ-12126)

This is an automated email from the ASF dual-hosted git repository.

danwatford pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/ofbiz-framework.git


The following commit(s) were added to refs/heads/trunk by this push:
     new 68c1084edc Improved: MacroFormRenderer refactoring of datetime fields (OFBIZ-12126)
68c1084edc is described below

commit 68c1084edc56872fe7621976fd5d88bc1312b180
Author: Daniel Watford <da...@watfordconsulting.com>
AuthorDate: Tue Nov 15 10:25:56 2022 +0000

    Improved: MacroFormRenderer refactoring of datetime fields (OFBIZ-12126)
    
    Part of the OFBIZ-11456 MacroFormRenderer refactoring effort.
    
    Rather than MacroFormRenderer producing and evaulating FTL strings, it now uses RenderableFtlElementsBuilder to create RenderableFtlMacroCall objects for datetime fields which are then passed to an FtlWriter for evaluation.
    
    Added tests to document how rendering parameters are generated for datetime fields.
    
    Simplified generation of selectable minute options for date-time fields used in time-dropdown mode.
    
    Moved typing of the date-time field's step, type, mask and clock attributes to DateTimeField model to ensure they are checked when the model is instantiated. The type and clock attributes are exposed by the model as boolean getters to check field types and whether a 12 or 24-hour clock should be used. These changes remove the need for client code to decode the various string attributes.
    
    Removed redundant code which enabled/disabled encoding of date-time field output
---
 .../apache/ofbiz/widget/model/ModelFormField.java  |  82 +++---
 .../ofbiz/widget/model/XmlWidgetFieldVisitor.java  |   6 +-
 .../widget/renderer/macro/MacroFormRenderer.java   | 284 +--------------------
 .../macro/RenderableFtlFormElementsBuilder.java    | 179 +++++++++++++
 .../renderer/macro/MacroCallParameterMatcher.java  |   8 +
 ...croCallParameterStringValueEndsWithMatcher.java |  45 ++++
 ...oCallParameterStringValueStartsWithMatcher.java |  45 ++++
 .../renderer/macro/MacroFormRendererTest.java      |  17 +-
 ...nderableFtlFormElementsBuilderDatetimeTest.java | 236 +++++++++++++++++
 .../template/macro/HtmlFormMacroLibrary.ftl        |  14 +-
 .../template/macro/XlsFormMacroLibrary.ftl         |   6 +-
 11 files changed, 595 insertions(+), 327 deletions(-)

diff --git a/framework/widget/src/main/java/org/apache/ofbiz/widget/model/ModelFormField.java b/framework/widget/src/main/java/org/apache/ofbiz/widget/model/ModelFormField.java
index 7e4143582a..25c7901639 100644
--- a/framework/widget/src/main/java/org/apache/ofbiz/widget/model/ModelFormField.java
+++ b/framework/widget/src/main/java/org/apache/ofbiz/widget/model/ModelFormField.java
@@ -88,7 +88,7 @@ import org.w3c.dom.Element;
 /**
  * Models the &lt;field&gt; element.
  *
- * @see <code>widget-form.xsd</code>
+ * @see <a href="https://ofbiz.apache.org/dtds/widget-form.xsd">widget-form.xsd</a>
  */
 public final class ModelFormField {
 
@@ -1272,11 +1272,11 @@ public final class ModelFormField {
      * @see <code>widget-form.xsd</code>
      */
     public static class DateTimeField extends FieldInfo {
-        private final String clock;
+        private final boolean isTwelveHour;
         private final FlexibleStringExpander defaultValue;
         private final String inputMethod;
-        private final String mask;
-        private final String step;
+        private final boolean useMask;
+        private final int step;
         private final String type;
 
         protected DateTimeField(DateTimeField original, ModelFormField modelFormField) {
@@ -1284,8 +1284,8 @@ public final class ModelFormField {
             this.defaultValue = original.defaultValue;
             this.type = original.type;
             this.inputMethod = original.inputMethod;
-            this.clock = original.clock;
-            this.mask = original.mask;
+            this.isTwelveHour = original.isTwelveHour;
+            this.useMask = original.useMask;
             this.step = original.step;
         }
 
@@ -1294,13 +1294,22 @@ public final class ModelFormField {
             this.defaultValue = FlexibleStringExpander.getInstance(element.getAttribute("default-value"));
             this.type = element.getAttribute("type");
             this.inputMethod = element.getAttribute("input-method");
-            this.clock = element.getAttribute("clock");
-            this.mask = element.getAttribute("mask");
-            String step = element.getAttribute("step");
-            if (step.isEmpty()) {
-                step = "1";
+            this.isTwelveHour = "12".equals(element.getAttribute("clock"));
+            this.useMask = "Y".equals(element.getAttribute("mask"));
+
+            final String stepAttribute = element.getAttribute("step");
+            if (stepAttribute.isEmpty()) {
+                this.step = 1;
+            } else {
+                try {
+                    this.step = Integer.parseInt(stepAttribute);
+                } catch (IllegalArgumentException e) {
+                    final String msg = "Could not read the step value of the datetime element: [" + stepAttribute
+                            + "]. Value must be an integer.";
+                    Debug.logError(msg, MODULE);
+                    throw new RuntimeException(msg, e);
+                }
             }
-            this.step = step;
         }
 
         public DateTimeField(int fieldSource, ModelFormField modelFormField) {
@@ -1308,9 +1317,9 @@ public final class ModelFormField {
             this.defaultValue = FlexibleStringExpander.getInstance("");
             this.type = "";
             this.inputMethod = "";
-            this.clock = "";
-            this.mask = "";
-            this.step = "1";
+            this.isTwelveHour = false;
+            this.useMask = false;
+            this.step = 1;
         }
 
         public DateTimeField(int fieldSource, String type) {
@@ -1318,9 +1327,9 @@ public final class ModelFormField {
             this.defaultValue = FlexibleStringExpander.getInstance("");
             this.type = type;
             this.inputMethod = "";
-            this.clock = "";
-            this.mask = "";
-            this.step = "1";
+            this.isTwelveHour = false;
+            this.useMask = false;
+            this.step = 1;
         }
 
         public DateTimeField(ModelFormField modelFormField) {
@@ -1328,9 +1337,9 @@ public final class ModelFormField {
             this.defaultValue = FlexibleStringExpander.getInstance("");
             this.type = "";
             this.inputMethod = "";
-            this.clock = "";
-            this.mask = "";
-            this.step = "1";
+            this.isTwelveHour = false;
+            this.useMask = false;
+            this.step = 1;
         }
 
         @Override
@@ -1344,11 +1353,10 @@ public final class ModelFormField {
         }
 
         /**
-         * Gets clock.
-         * @return the clock
+         * @return True if this field uses a 12-hour clock. If false then a 24-hour clock should be used.
          */
-        public String getClock() {
-            return this.clock;
+        public boolean isTwelveHour() {
+            return this.isTwelveHour;
         }
 
         /**
@@ -1400,26 +1408,32 @@ public final class ModelFormField {
 
         /**
          * Gets mask.
+         *
          * @return the mask
          */
-        public String getMask() {
-            return this.mask;
+        public boolean useMask() {
+            return this.useMask;
         }
 
         /**
          * Gets step.
+         *
          * @return the step
          */
-        public String getStep() {
+        public int getStep() {
             return this.step;
         }
 
-        /**
-         * Gets type.
-         * @return the type
-         */
-        public String getType() {
-            return type;
+        public final boolean isDateType() {
+            return "date".equals(type);
+        }
+
+        public final boolean isTimeType() {
+            return "time".equals(type);
+        }
+
+        public final boolean isTimestampType() {
+            return "timestamp".equals(type);
         }
 
         @Override
diff --git a/framework/widget/src/main/java/org/apache/ofbiz/widget/model/XmlWidgetFieldVisitor.java b/framework/widget/src/main/java/org/apache/ofbiz/widget/model/XmlWidgetFieldVisitor.java
index 4839acd588..402e0c877b 100644
--- a/framework/widget/src/main/java/org/apache/ofbiz/widget/model/XmlWidgetFieldVisitor.java
+++ b/framework/widget/src/main/java/org/apache/ofbiz/widget/model/XmlWidgetFieldVisitor.java
@@ -361,10 +361,10 @@ public class XmlWidgetFieldVisitor extends XmlAbstractWidgetVisitor implements M
 
     private void visitDateTimeFieldAttrs(DateTimeField field) throws Exception {
         visitAttribute("default-value", field.getDefaultValue());
-        visitAttribute("type", field.getType());
+        visitAttribute("type", field.isDateType() ? "date" : field.isTimeType() ? "time" : "timestamp");
         visitAttribute("input-method", field.getInputMethod());
-        visitAttribute("clock", field.getClock());
-        visitAttribute("mask", field.getMask());
+        visitAttribute("isTwelveHour", field.isTwelveHour());
+        visitAttribute("mask", field.useMask() ? "Y" : "N");
         visitAttribute("step", field.getStep());
     }
 
diff --git a/framework/widget/src/main/java/org/apache/ofbiz/widget/renderer/macro/MacroFormRenderer.java b/framework/widget/src/main/java/org/apache/ofbiz/widget/renderer/macro/MacroFormRenderer.java
index 89d917edd9..5ac065184d 100644
--- a/framework/widget/src/main/java/org/apache/ofbiz/widget/renderer/macro/MacroFormRenderer.java
+++ b/framework/widget/src/main/java/org/apache/ofbiz/widget/renderer/macro/MacroFormRenderer.java
@@ -23,7 +23,6 @@ import java.io.StringWriter;
 import java.io.UnsupportedEncodingException;
 import java.net.URI;
 import java.net.URLEncoder;
-import java.sql.Timestamp;
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.LinkedList;
@@ -80,7 +79,6 @@ import org.apache.ofbiz.widget.model.ModelFormField.SubmitField;
 import org.apache.ofbiz.widget.model.ModelFormField.TextField;
 import org.apache.ofbiz.widget.model.ModelFormField.TextFindField;
 import org.apache.ofbiz.widget.model.ModelFormField.TextareaField;
-import org.apache.ofbiz.widget.model.ModelFormFieldBuilder;
 import org.apache.ofbiz.widget.model.ModelScreenWidget;
 import org.apache.ofbiz.widget.model.ModelSingleForm;
 import org.apache.ofbiz.widget.model.ModelTheme;
@@ -95,8 +93,6 @@ import org.apache.ofbiz.widget.renderer.macro.renderable.RenderableFtl;
 import org.apache.ofbiz.widget.renderer.macro.renderable.RenderableFtlMacroCall;
 import org.jsoup.nodes.Element;
 
-import com.ibm.icu.util.Calendar;
-
 /**
  * Widget Library - Form Renderer implementation based on Freemarker macros
  */
@@ -259,267 +255,10 @@ public final class MacroFormRenderer implements FormStringRenderer {
     }
 
     @Override
-    public void renderDateTimeField(Appendable writer, Map<String, Object> context, DateTimeField dateTimeField) throws IOException {
-        ModelFormField modelFormField = dateTimeField.getModelFormField();
-        String paramName = modelFormField.getParameterName(context);
-        String defaultDateTimeString = dateTimeField.getDefaultDateTimeString(context);
-        boolean disabled = modelFormField.getDisabled(context);
-        String className = "";
-        String alert = "false";
-        String name = "";
-        String formattedMask = "";
-        String event = modelFormField.getEvent();
-        String action = modelFormField.getAction(context);
-        if (UtilValidate.isNotEmpty(modelFormField.getWidgetStyle())) {
-            className = modelFormField.getWidgetStyle();
-            if (modelFormField.shouldBeRed(context)) {
-                alert = "true";
-            }
-        }
-        boolean useTimeDropDown = "time-dropdown".equals(dateTimeField.getInputMethod());
-        String stepString = dateTimeField.getStep();
-        int step = 1;
-        StringBuilder timeValues = new StringBuilder();
-        if (useTimeDropDown && UtilValidate.isNotEmpty(step)) {
-            try {
-                step = Integer.parseInt(stepString);
-            } catch (IllegalArgumentException e) {
-                Debug.logWarning("Invalid value for step property for field[" + paramName + "] with input-method=\"time-dropdown\" "
-                        + " Found Value [" + stepString + "]  " + e.getMessage(), MODULE);
-            }
-            timeValues.append("[");
-            for (int i = 0; i <= 59;) {
-                if (i != 0) {
-                    timeValues.append(", ");
-                }
-                timeValues.append(i);
-                i += step;
-            }
-            timeValues.append("]");
-        }
-        Map<String, String> uiLabelMap = UtilGenerics.cast(context.get("uiLabelMap"));
-        if (uiLabelMap == null) {
-            Debug.logWarning("Could not find uiLabelMap in context", MODULE);
-        }
-        String localizedInputTitle = "";
-        String localizedIconTitle = "";
-        // whether the date field is short form, yyyy-mm-dd
-        boolean shortDateInput = ("date".equals(dateTimeField.getType()) || useTimeDropDown ? true : false);
-        if (useTimeDropDown) {
-            name = UtilHttp.makeCompositeParam(paramName, "date");
-        } else {
-            name = paramName;
-        }
-        // the default values for a timestamp
-        int size = 25;
-        int maxlength = 30;
-        if (shortDateInput) {
-            maxlength = 10;
-            size = maxlength;
-            if (uiLabelMap != null) {
-                localizedInputTitle = uiLabelMap.get("CommonFormatDate");
-            }
-        } else if ("time".equals(dateTimeField.getType())) {
-            maxlength = 8;
-            size = maxlength;
-            if (uiLabelMap != null) {
-                localizedInputTitle = uiLabelMap.get("CommonFormatTime");
-            }
-        } else {
-            if (uiLabelMap != null) {
-                localizedInputTitle = uiLabelMap.get("CommonFormatDateTime");
-            }
-        }
-        /*
-         * FIXME: Using a builder here is a hack. Replace the builder with appropriate code.
-         */
-        ModelFormFieldBuilder builder = new ModelFormFieldBuilder(modelFormField);
-        boolean memEncodeOutput = modelFormField.getEncodeOutput();
-        if (useTimeDropDown) {
-            // If time-dropdown deactivate encodingOutput for found hour and minutes
-            // FIXME: Encoding should be controlled by the renderer, not by the model.
-            builder.setEncodeOutput(false);
-        }
-        // FIXME: modelFormField.getEntry ignores shortDateInput when converting Date objects to Strings.
-        if (useTimeDropDown) {
-            builder.setEncodeOutput(memEncodeOutput);
-        }
-        modelFormField = builder.build();
-        String contextValue = modelFormField.getEntry(context, dateTimeField.getDefaultValue(context));
-        String value = contextValue;
-        if (UtilValidate.isNotEmpty(value)) {
-            if (value.length() > maxlength) {
-                value = value.substring(0, maxlength);
-            }
-        }
-        String id = modelFormField.getCurrentContainerId(context);
-        ModelForm modelForm = modelFormField.getModelForm();
-        String formName = FormRenderer.getCurrentFormName(modelForm, context);
-        String timeDropdown = dateTimeField.getInputMethod();
-        String timeDropdownParamName = "";
-        String classString = "";
-        boolean isTwelveHour = false;
-        String timeHourName = "";
-        int hour2 = 0;
-        int hour1 = 0;
-        int minutes = 0;
-        String timeMinutesName = "";
-        String amSelected = "";
-        String pmSelected = "";
-        String ampmName = "";
-        String compositeType = "";
-        // search for a localized label for the icon
-        if (uiLabelMap != null) {
-            localizedIconTitle = uiLabelMap.get("CommonViewCalendar");
-        }
-        if (!"time".equals(dateTimeField.getType())) {
-            String tempParamName;
-            if (useTimeDropDown) {
-                tempParamName = UtilHttp.makeCompositeParam(paramName, "date");
-            } else {
-                tempParamName = paramName;
-            }
-            timeDropdownParamName = tempParamName;
-            defaultDateTimeString = UtilHttp.encodeBlanks(modelFormField.getEntry(context, defaultDateTimeString));
-        }
-        // if we have an input method of time-dropdown, then render two
-        // dropdowns
-        if (useTimeDropDown) {
-            className = modelFormField.getWidgetStyle();
-            classString = (className != null ? className : "");
-            isTwelveHour = "12".equals(dateTimeField.getClock());
-            // set the Calendar to the default time of the form or now()
-            Calendar cal = null;
-            try {
-                Timestamp defaultTimestamp = Timestamp.valueOf(contextValue);
-                cal = Calendar.getInstance();
-                cal.setTime(defaultTimestamp);
-            } catch (IllegalArgumentException e) {
-                Debug.logWarning("Form widget field [" + paramName
-                        + "] with input-method=\"time-dropdown\" was not able to understand the default time [" + defaultDateTimeString
-                        + "]. The parsing error was: " + e.getMessage(), MODULE);
-            }
-            timeHourName = UtilHttp.makeCompositeParam(paramName, "hour");
-            if (cal != null) {
-                int hour = cal.get(Calendar.HOUR_OF_DAY);
-                hour2 = hour;
-                if (hour == 0) {
-                    hour = 12;
-                }
-                if (hour > 12) {
-                    hour -= 12;
-                }
-                hour1 = hour;
-                minutes = cal.get(Calendar.MINUTE);
-            }
-            timeMinutesName = UtilHttp.makeCompositeParam(paramName, "minutes");
-            compositeType = UtilHttp.makeCompositeParam(paramName, "compositeType");
-            // if 12 hour clock, write the AM/PM selector
-            if (isTwelveHour) {
-                amSelected = ((cal != null && cal.get(Calendar.AM_PM) == Calendar.AM) ? "selected" : "");
-                pmSelected = ((cal != null && cal.get(Calendar.AM_PM) == Calendar.PM) ? "selected" : "");
-                ampmName = UtilHttp.makeCompositeParam(paramName, "ampm");
-            }
-        }
-        //check for required field style on single forms
-        if (shouldApplyRequiredField(modelFormField)) {
-            String requiredStyle = modelFormField.getRequiredFieldStyle();
-            if (UtilValidate.isEmpty(requiredStyle)) {
-                requiredStyle = "required";
-            }
-            if (UtilValidate.isEmpty(className)) {
-                className = requiredStyle;
-            } else {
-                className = requiredStyle + " " + className;
-            }
-        }
-        String mask = dateTimeField.getMask();
-        if ("Y".equals(mask)) {
-            if ("date".equals(dateTimeField.getType())) {
-                formattedMask = "9999-99-99";
-            } else if ("time".equals(dateTimeField.getType())) {
-                formattedMask = "99:99:99";
-            } else if ("timestamp".equals(dateTimeField.getType())) {
-                formattedMask = "9999-99-99 99:99:99";
-            }
-        }
-        String isXMLHttpRequest = "false";
-        if ("XMLHttpRequest".equals(request.getHeader("X-Requested-With"))) {
-            isXMLHttpRequest = "true";
-        }
-        String tabindex = modelFormField.getTabindex();
-        StringWriter sr = new StringWriter();
-        sr.append("<@renderDateTimeField ");
-        sr.append("name=\"");
-        sr.append(name);
-        sr.append("\" className=\"");
-        sr.append(className);
-        sr.append("\" alert=\"");
-        sr.append(alert);
-        sr.append("\" value=\"");
-        sr.append(value);
-        sr.append("\" title=\"");
-        sr.append(localizedInputTitle);
-        sr.append("\" size=\"");
-        sr.append(Integer.toString(size));
-        sr.append("\" maxlength=\"");
-        sr.append(Integer.toString(maxlength));
-        sr.append("\" step=\"");
-        sr.append(Integer.toString(step));
-        sr.append("\" timeValues=\"");
-        sr.append(timeValues.toString());
-        sr.append("\" id=\"");
-        sr.append(id);
-        sr.append("\" event=\"");
-        sr.append(event);
-        sr.append("\" action=\"");
-        sr.append(action);
-        sr.append("\" dateType=\"");
-        sr.append(dateTimeField.getType());
-        sr.append("\" shortDateInput=");
-        sr.append(Boolean.toString(shortDateInput));
-        sr.append(" timeDropdownParamName=\"");
-        sr.append(timeDropdownParamName);
-        sr.append("\" defaultDateTimeString=\"");
-        sr.append(defaultDateTimeString);
-        sr.append("\" localizedIconTitle=\"");
-        sr.append(localizedIconTitle);
-        sr.append("\" timeDropdown=\"");
-        sr.append(timeDropdown);
-        sr.append("\" timeHourName=\"");
-        sr.append(timeHourName);
-        sr.append("\" classString=\"");
-        sr.append(classString);
-        sr.append("\" hour1=");
-        sr.append(Integer.toString(hour1));
-        sr.append(" hour2=");
-        sr.append(Integer.toString(hour2));
-        sr.append(" timeMinutesName=\"");
-        sr.append(timeMinutesName);
-        sr.append("\" minutes=");
-        sr.append(Integer.toString(minutes));
-        sr.append(" isTwelveHour=");
-        sr.append(Boolean.toString(isTwelveHour));
-        sr.append(" ampmName=\"");
-        sr.append(ampmName);
-        sr.append("\" amSelected=\"");
-        sr.append(amSelected);
-        sr.append("\" pmSelected=\"");
-        sr.append(pmSelected);
-        sr.append("\" compositeType=\"");
-        sr.append(compositeType);
-        sr.append("\" formName=\"");
-        sr.append(formName);
-        sr.append("\" mask=\"");
-        sr.append(formattedMask);
-        sr.append("\" tabindex=\"");
-        sr.append(tabindex);
-        sr.append("\" isXMLHttpRequest=\"");
-        sr.append(isXMLHttpRequest);
-        sr.append("\" disabled=");
-        sr.append(Boolean.toString(disabled));
-        sr.append(" />");
-        executeMacro(writer, (Locale) context.get("locale"), sr.toString());
+    public void renderDateTimeField(Appendable writer, Map<String, Object> context, DateTimeField dateTimeField) {
+        writeFtlElement(writer, renderableFtlFormElementsBuilder.dateTime(context, dateTimeField));
+
+        final ModelFormField modelFormField = dateTimeField.getModelFormField();
         this.addAsterisks(writer, context, modelFormField);
         this.appendTooltip(writer, context, modelFormField);
     }
@@ -1870,14 +1609,13 @@ public final class MacroFormRenderer implements FormStringRenderer {
         // the default values for a timestamp
         int size = 25;
         int maxlength = 30;
-        String dateType = dateFindField.getType();
-        if ("date".equals(dateType)) {
+        if (dateFindField.isDateType()) {
             maxlength = 10;
             size = maxlength;
             if (uiLabelMap != null) {
                 localizedInputTitle = uiLabelMap.get("CommonFormatDate");
             }
-        } else if ("time".equals(dateFindField.getType())) {
+        } else if (dateFindField.isTimeType()) {
             maxlength = 8;
             size = maxlength;
             if (uiLabelMap != null) {
@@ -1900,7 +1638,7 @@ public final class MacroFormRenderer implements FormStringRenderer {
         String defaultDateTimeString = "";
         StringBuilder imgSrc = new StringBuilder();
         // add calendar pop-up button and seed data IF this is not a "time" type date-find
-        if (!"time".equals(dateFindField.getType())) {
+        if (!dateFindField.isTimeType()) {
             ModelForm modelForm = modelFormField.getModelForm();
             formName = FormRenderer.getCurrentFormName(modelForm, context);
             defaultDateTimeString = UtilHttp.encodeBlanks(modelFormField.getEntry(context, dateFindField.getDefaultDateTimeString(context)));
@@ -1949,9 +1687,11 @@ public final class MacroFormRenderer implements FormStringRenderer {
         sr.append(Integer.toString(size));
         sr.append("\" maxlength=\"");
         sr.append(Integer.toString(maxlength));
-        sr.append("\" dateType=\"");
-        sr.append(dateType);
-        sr.append("\" formName=\"");
+        sr.append("\" isDateType=");
+        sr.append(Boolean.toString(dateFindField.isDateType()));
+        sr.append("\" isTimeType=");
+        sr.append(Boolean.toString(dateFindField.isTimeType()));
+        sr.append(" formName=\"");
         sr.append(formName);
         sr.append("\" defaultDateTimeString=\"");
         sr.append(defaultDateTimeString);
diff --git a/framework/widget/src/main/java/org/apache/ofbiz/widget/renderer/macro/RenderableFtlFormElementsBuilder.java b/framework/widget/src/main/java/org/apache/ofbiz/widget/renderer/macro/RenderableFtlFormElementsBuilder.java
index 8d256c5924..db0725f96e 100644
--- a/framework/widget/src/main/java/org/apache/ofbiz/widget/renderer/macro/RenderableFtlFormElementsBuilder.java
+++ b/framework/widget/src/main/java/org/apache/ofbiz/widget/renderer/macro/RenderableFtlFormElementsBuilder.java
@@ -20,6 +20,7 @@ package org.apache.ofbiz.widget.renderer.macro;
 
 import java.io.StringWriter;
 import java.net.URI;
+import java.sql.Timestamp;
 import java.util.ArrayList;
 import java.util.Iterator;
 import java.util.List;
@@ -27,11 +28,14 @@ import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
 import java.util.UUID;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
 
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import javax.servlet.http.HttpSession;
 
+import com.ibm.icu.util.Calendar;
 import org.apache.ofbiz.base.util.Debug;
 import org.apache.ofbiz.base.util.UtilFormatOut;
 import org.apache.ofbiz.base.util.UtilGenerics;
@@ -47,6 +51,7 @@ import org.apache.ofbiz.widget.model.ModelFormField.ContainerField;
 import org.apache.ofbiz.widget.model.ModelFormField.DisplayField;
 import org.apache.ofbiz.widget.model.ModelScreenWidget.Label;
 import org.apache.ofbiz.widget.model.ModelTheme;
+import org.apache.ofbiz.widget.renderer.FormRenderer;
 import org.apache.ofbiz.widget.renderer.Paginator;
 import org.apache.ofbiz.widget.renderer.VisualTheme;
 import org.apache.ofbiz.widget.renderer.macro.renderable.RenderableFtl;
@@ -364,6 +369,180 @@ public final class RenderableFtlFormElementsBuilder {
         return builder.build();
     }
 
+    public RenderableFtl dateTime(final Map<String, Object> context, final ModelFormField.DateTimeField dateTimeField) {
+
+        final ModelFormField modelFormField = dateTimeField.getModelFormField();
+        final ModelForm modelForm = modelFormField.getModelForm();
+
+        // Determine whether separate drop down select inputs be used for the hour/minute/am_pm components of the date-time.
+        boolean useTimeDropDown = "time-dropdown".equals(dateTimeField.getInputMethod());
+
+        final String paramName = modelFormField.getParameterName(context);
+
+        final RenderableFtlMacroCallBuilder macroCallBuilder = RenderableFtlMacroCall.builder()
+                .name("renderDateTimeField")
+                .booleanParameter("disabled", modelFormField.getDisabled(context))
+                .stringParameter("name", useTimeDropDown ? UtilHttp.makeCompositeParam(paramName, "date") : paramName)
+                .stringParameter("id", modelFormField.getCurrentContainerId(context))
+                .booleanParameter("isXMLHttpRequest", "XMLHttpRequest".equals(request.getHeader("X-Requested-With")))
+                .stringParameter("tabindex", modelFormField.getTabindex())
+                .stringParameter("event", modelFormField.getEvent())
+                .stringParameter("formName", FormRenderer.getCurrentFormName(modelForm, context))
+                .booleanParameter("alert", false)
+                .stringParameter("action", modelFormField.getAction(context));
+
+        // Set names for the various input components that might be rendered for this date-time field.
+        macroCallBuilder.stringParameter("timeHourName", UtilHttp.makeCompositeParam(paramName, "hour"))
+                .stringParameter("timeMinutesName", UtilHttp.makeCompositeParam(paramName, "minutes"))
+                .stringParameter("compositeType", UtilHttp.makeCompositeParam(paramName, "compositeType"))
+                .stringParameter("ampmName", UtilHttp.makeCompositeParam(paramName, "ampm"));
+
+        ArrayList<String> classNames = new ArrayList<>();
+        if (UtilValidate.isNotEmpty(modelFormField.getWidgetStyle())) {
+            classNames.add(modelFormField.getWidgetStyle());
+
+            if (modelFormField.shouldBeRed(context)) {
+                macroCallBuilder.booleanParameter("alert", true);
+            }
+        }
+
+        if (shouldApplyRequiredField(modelFormField)) {
+            String requiredStyle = modelFormField.getRequiredFieldStyle();
+            if (UtilValidate.isEmpty(requiredStyle)) {
+                requiredStyle = "required";
+            }
+            classNames.add(requiredStyle);
+        }
+        macroCallBuilder.stringParameter("className", String.join(" ", classNames));
+
+        String defaultDateTimeString = dateTimeField.getDefaultDateTimeString(context);
+
+        if (useTimeDropDown) {
+            final int step = dateTimeField.getStep();
+            final String timeValues = IntStream.range(0, 60)
+                    .filter(i -> i % step == 0)
+                    .mapToObj(Integer::toString)
+                    .collect(Collectors.joining(", ", "[", "]"));
+            macroCallBuilder.stringParameter("timeValues", timeValues)
+                    .intParameter("step", step);
+        }
+
+        Map<String, String> uiLabelMap = UtilGenerics.cast(context.get("uiLabelMap"));
+        if (uiLabelMap == null) {
+            Debug.logWarning("Could not find uiLabelMap in context", MODULE);
+        }
+
+        // whether the date field is short form, yyyy-mm-dd
+        boolean shortDateInput = dateTimeField.isDateType() || useTimeDropDown;
+        macroCallBuilder.booleanParameter("shortDateInput", shortDateInput);
+
+        // Set render properties based on the date-time field's type.
+        final int size;
+        final int maxlength;
+        final String formattedMask;
+        final String titleLabelMapKey;
+
+        if (shortDateInput) {
+            size = 10;
+            maxlength = 10;
+            formattedMask = "9999-99-99";
+            titleLabelMapKey = "CommonFormatDate";
+        } else if (dateTimeField.isTimeType()) {
+            size = 8;
+            maxlength = 8;
+            formattedMask = "99:99:99";
+            titleLabelMapKey = "CommonFormatTime";
+
+            macroCallBuilder.booleanParameter("isTimeType", true);
+        } else {
+            size = 25;
+            maxlength = 30;
+            formattedMask = "9999-99-99 99:99:99";
+            titleLabelMapKey = "CommonFormatDateTime";
+        }
+
+        macroCallBuilder.intParameter("size", size)
+                .intParameter("maxlength", maxlength);
+
+        if (dateTimeField.useMask()) {
+            macroCallBuilder.stringParameter("mask", formattedMask);
+        }
+
+        if (uiLabelMap != null) {
+            macroCallBuilder.stringParameter("title", uiLabelMap.get(titleLabelMapKey))
+                    .stringParameter("localizedIconTitle", uiLabelMap.get("CommonViewCalendar"));
+        }
+
+        final String contextValue = modelFormField.getEntry(context, dateTimeField.getDefaultValue(context));
+        final String value = UtilValidate.isNotEmpty(contextValue) && contextValue.length() > maxlength
+                ? contextValue.substring(0, maxlength)
+                : contextValue;
+
+        String timeDropdown = dateTimeField.getInputMethod();
+        String timeDropdownParamName = "";
+
+        if (!dateTimeField.isTimeType()) {
+            String tempParamName;
+            if (useTimeDropDown) {
+                tempParamName = UtilHttp.makeCompositeParam(paramName, "date");
+            } else {
+                tempParamName = paramName;
+            }
+            timeDropdownParamName = tempParamName;
+            defaultDateTimeString = UtilHttp.encodeBlanks(modelFormField.getEntry(context, defaultDateTimeString));
+        }
+
+        // If we have an input method of time-dropdown, then render two dropdowns
+        if (useTimeDropDown) {
+            // Set the class to apply to the time input components.
+            final String widgetStyle = modelFormField.getWidgetStyle();
+            macroCallBuilder.stringParameter("classString", widgetStyle != null ? widgetStyle : "");
+
+            // Set the Calendar to the field's context value, or the field's default if no context value exists.
+            final Calendar cal = Calendar.getInstance();
+            try {
+                if (contextValue != null) {
+                    Timestamp contextValueTimestamp = Timestamp.valueOf(contextValue);
+                    cal.setTime(contextValueTimestamp);
+                }
+            } catch (IllegalArgumentException e) {
+                Debug.logWarning("Form widget field [" + paramName
+                        + "] with input-method=\"time-dropdown\" was not able to understand the time [" + contextValue
+                        + "]. The parsing error was: " + e.getMessage(), MODULE);
+            }
+
+            if (cal != null) {
+                int hourOfDay = cal.get(Calendar.HOUR_OF_DAY);
+                int minutesOfHour = cal.get(Calendar.MINUTE);
+
+                // Set the hour value for when in 12-hour clock mode.
+                macroCallBuilder.intParameter("hour1", hourOfDay % 12);
+
+                // Set the hour value for when in 24-hour clock mode.
+                macroCallBuilder.intParameter("hour2", hourOfDay);
+
+                macroCallBuilder.intParameter("minutes", minutesOfHour);
+            }
+
+            boolean isTwelveHourClock = dateTimeField.isTwelveHour();
+            macroCallBuilder.booleanParameter("isTwelveHour", isTwelveHourClock);
+
+            // if using a 12-hour clock, write the AM/PM selector
+            if (isTwelveHourClock) {
+                macroCallBuilder.booleanParameter("amSelected", cal.get(Calendar.AM_PM) == Calendar.AM)
+                        .booleanParameter("pmSelected", cal.get(Calendar.AM_PM) == Calendar.PM);
+
+            }
+        }
+
+        macroCallBuilder.stringParameter("value", value)
+                .stringParameter("timeDropdownParamName", timeDropdownParamName)
+                .stringParameter("defaultDateTimeString", defaultDateTimeString)
+                .stringParameter("timeDropdown", timeDropdown);
+
+        return macroCallBuilder.build();
+    }
+
     public RenderableFtl makeHyperlinkString(final ModelFormField.SubHyperlink subHyperlink,
                                              final Map<String, Object> context) {
         if (subHyperlink == null || !subHyperlink.shouldUse(context)) {
diff --git a/framework/widget/src/test/java/org/apache/ofbiz/widget/renderer/macro/MacroCallParameterMatcher.java b/framework/widget/src/test/java/org/apache/ofbiz/widget/renderer/macro/MacroCallParameterMatcher.java
index 30c37bee3a..c142b0733a 100644
--- a/framework/widget/src/test/java/org/apache/ofbiz/widget/renderer/macro/MacroCallParameterMatcher.java
+++ b/framework/widget/src/test/java/org/apache/ofbiz/widget/renderer/macro/MacroCallParameterMatcher.java
@@ -88,6 +88,14 @@ public final class MacroCallParameterMatcher extends TypeSafeMatcher<Map.Entry<S
         return new MacroCallParameterMatcher(name, new MacroCallParameterStringValueMatcher(value));
     }
 
+    public static MacroCallParameterMatcher hasNameAndStringValueStartsWith(final String name, final String startsWith) {
+        return new MacroCallParameterMatcher(name, new MacroCallParameterStringValueStartsWithMatcher(startsWith));
+    }
+
+    public static MacroCallParameterMatcher hasNameAndStringValueEndsWith(final String name, final String endsWith) {
+        return new MacroCallParameterMatcher(name, new MacroCallParameterStringValueEndsWithMatcher(endsWith));
+    }
+
     public static MacroCallParameterMatcher hasNameAndIntegerValue(final String name, final int value) {
         return new MacroCallParameterMatcher(name, new MacroCallParameterIntegerValueMatcher(value));
     }
diff --git a/framework/widget/src/test/java/org/apache/ofbiz/widget/renderer/macro/MacroCallParameterStringValueEndsWithMatcher.java b/framework/widget/src/test/java/org/apache/ofbiz/widget/renderer/macro/MacroCallParameterStringValueEndsWithMatcher.java
new file mode 100644
index 0000000000..e53817ad9f
--- /dev/null
+++ b/framework/widget/src/test/java/org/apache/ofbiz/widget/renderer/macro/MacroCallParameterStringValueEndsWithMatcher.java
@@ -0,0 +1,45 @@
+/*******************************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *******************************************************************************/
+package org.apache.ofbiz.widget.renderer.macro;
+
+import org.hamcrest.Description;
+import org.hamcrest.TypeSafeMatcher;
+
+public final class MacroCallParameterStringValueEndsWithMatcher extends TypeSafeMatcher<Object> {
+    private final String endsWith;
+
+    public MacroCallParameterStringValueEndsWithMatcher(final String endsWith) {
+        this.endsWith = endsWith;
+    }
+
+    @Override
+    protected boolean matchesSafely(final Object item) {
+        return item != null && item instanceof String && ((String) item).endsWith(endsWith);
+    }
+
+    @Override
+    public void describeTo(final Description description) {
+        description.appendText("ends with '" + endsWith + "'");
+    }
+
+    @Override
+    protected void describeMismatchSafely(final Object item, final Description mismatchDescription) {
+        mismatchDescription.appendText("with value '" + item + "'");
+    }
+}
diff --git a/framework/widget/src/test/java/org/apache/ofbiz/widget/renderer/macro/MacroCallParameterStringValueStartsWithMatcher.java b/framework/widget/src/test/java/org/apache/ofbiz/widget/renderer/macro/MacroCallParameterStringValueStartsWithMatcher.java
new file mode 100644
index 0000000000..e544d6b864
--- /dev/null
+++ b/framework/widget/src/test/java/org/apache/ofbiz/widget/renderer/macro/MacroCallParameterStringValueStartsWithMatcher.java
@@ -0,0 +1,45 @@
+/*******************************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *******************************************************************************/
+package org.apache.ofbiz.widget.renderer.macro;
+
+import org.hamcrest.Description;
+import org.hamcrest.TypeSafeMatcher;
+
+public final class MacroCallParameterStringValueStartsWithMatcher extends TypeSafeMatcher<Object> {
+    private final String startsWith;
+
+    public MacroCallParameterStringValueStartsWithMatcher(final String startsWith) {
+        this.startsWith = startsWith;
+    }
+
+    @Override
+    protected boolean matchesSafely(final Object item) {
+        return item != null && item instanceof String && ((String) item).startsWith(startsWith);
+    }
+
+    @Override
+    public void describeTo(final Description description) {
+        description.appendText("starts with '" + startsWith + "'");
+    }
+
+    @Override
+    protected void describeMismatchSafely(final Object item, final Description mismatchDescription) {
+        mismatchDescription.appendText("with value '" + item + "'");
+    }
+}
diff --git a/framework/widget/src/test/java/org/apache/ofbiz/widget/renderer/macro/MacroFormRendererTest.java b/framework/widget/src/test/java/org/apache/ofbiz/widget/renderer/macro/MacroFormRendererTest.java
index eb5b3e244e..0c508c2cea 100644
--- a/framework/widget/src/test/java/org/apache/ofbiz/widget/renderer/macro/MacroFormRendererTest.java
+++ b/framework/widget/src/test/java/org/apache/ofbiz/widget/renderer/macro/MacroFormRendererTest.java
@@ -222,7 +222,7 @@ public class MacroFormRendererTest {
     }
 
     @Test
-    public void textAreaMacroRendered(@Mocked ModelFormField.TextareaField textareaField) throws IOException {
+    public void textAreaMacroRendered(@Mocked ModelFormField.TextareaField textareaField) {
         new Expectations() {
             {
                 renderableFtlFormElementsBuilder.textArea(withNotNull(), textareaField);
@@ -239,19 +239,20 @@ public class MacroFormRendererTest {
     }
 
     @Test
-    public void dateTimeMacroRendered(@Mocked ModelFormField.DateTimeField dateTimeField) throws IOException {
+    public void dateTimeMacroRendered(@Mocked ModelFormField.DateTimeField dateTimeField) {
         new Expectations() {
             {
-                modelFormField.getEntry(withNotNull(), anyString);
-                result = "2020-01-02";
-
-                dateTimeField.getInputMethod();
-                result = "date";
+                renderableFtlFormElementsBuilder.dateTime(withNotNull(), dateTimeField);
+                result = genericMacroCall;
             }
         };
 
+        genericTooltipRenderedExpectation(dateTimeField);
+
         macroFormRenderer.renderDateTimeField(appendable, ImmutableMap.of(), dateTimeField);
-        assertAndGetMacroString("renderDateTimeField", ImmutableMap.of("value", "2020-01-02"));
+
+        genericSingleMacroRenderedVerification();
+        genericTooltipRenderedVerification();
     }
 
     @Test
diff --git a/framework/widget/src/test/java/org/apache/ofbiz/widget/renderer/macro/RenderableFtlFormElementsBuilderDatetimeTest.java b/framework/widget/src/test/java/org/apache/ofbiz/widget/renderer/macro/RenderableFtlFormElementsBuilderDatetimeTest.java
new file mode 100644
index 0000000000..df577a1e5f
--- /dev/null
+++ b/framework/widget/src/test/java/org/apache/ofbiz/widget/renderer/macro/RenderableFtlFormElementsBuilderDatetimeTest.java
@@ -0,0 +1,236 @@
+package org.apache.ofbiz.widget.renderer.macro;
+
+import mockit.Expectations;
+import mockit.Injectable;
+import mockit.Mocked;
+import mockit.Tested;
+import org.apache.ofbiz.webapp.control.RequestHandler;
+import org.apache.ofbiz.widget.model.ModelFormField;
+import org.apache.ofbiz.widget.model.ModelTheme;
+import org.apache.ofbiz.widget.renderer.VisualTheme;
+import org.apache.ofbiz.widget.renderer.macro.renderable.RenderableFtl;
+import org.junit.Test;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+import java.util.HashMap;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+
+public class RenderableFtlFormElementsBuilderDatetimeTest {
+
+    @Injectable
+    private VisualTheme visualTheme;
+
+    @Injectable
+    private RequestHandler requestHandler;
+
+    @Injectable
+    private HttpServletRequest request;
+
+    @Injectable
+    private HttpServletResponse response;
+
+    @Mocked
+    private HttpSession httpSession;
+
+    @Mocked
+    private ModelTheme modelTheme;
+
+    @Mocked
+    private ModelFormField.ContainerField containerField;
+
+    @Mocked
+    private ModelFormField modelFormField;
+
+    @Tested
+    private RenderableFtlFormElementsBuilder renderableFtlFormElementsBuilder;
+
+    @Test
+    public void datetimeFieldSetsIdAndValue(@Mocked final ModelFormField.DateTimeField datetimeField) {
+        final int maxLength = 22;
+        new Expectations() {
+            {
+                modelFormField.getCurrentContainerId(withNotNull());
+                result = "CurrentDatetimeId";
+
+                modelFormField.getEntry(withNotNull(), anyString);
+                result = "DATETIMEVALUE";
+            }
+        };
+
+        final HashMap<String, Object> context = new HashMap<>();
+
+        final RenderableFtl renderableFtl = renderableFtlFormElementsBuilder.dateTime(context, datetimeField);
+        assertThat(renderableFtl, MacroCallMatcher.hasNameAndParameters("renderDateTimeField",
+                MacroCallParameterMatcher.hasNameAndStringValue("id", "CurrentDatetimeId"),
+                MacroCallParameterMatcher.hasNameAndStringValue("value", "DATETIMEVALUE")));
+    }
+
+    @Test
+    public void datetimeFieldSetsDisabledParameters(@Mocked final ModelFormField.DateTimeField datetimeField) {
+        new Expectations() {
+            {
+                modelFormField.getDisabled(withNotNull());
+                result = true;
+
+                modelFormField.getEntry(withNotNull(), anyString);
+                result = "DATETIMEVALUE";
+            }
+        };
+
+        final HashMap<String, Object> context = new HashMap<>();
+
+        final RenderableFtl renderableFtl = renderableFtlFormElementsBuilder.dateTime(context, datetimeField);
+        assertThat(renderableFtl, MacroCallMatcher.hasNameAndParameters("renderDateTimeField",
+                MacroCallParameterMatcher.hasNameAndBooleanValue("disabled", true)));
+    }
+
+    @Test
+    public void datetimeFieldSetsLengthAndMaskForDateType(@Mocked final ModelFormField.DateTimeField datetimeField) {
+        new Expectations() {
+            {
+                datetimeField.isDateType();
+                result = true;
+
+                datetimeField.useMask();
+                result = true;
+
+                modelFormField.getEntry(withNotNull(), anyString);
+                result = "DATETIMEVALUE";
+            }
+        };
+
+        final HashMap<String, Object> context = new HashMap<>();
+
+        final RenderableFtl renderableFtl = renderableFtlFormElementsBuilder.dateTime(context, datetimeField);
+        assertThat(renderableFtl, MacroCallMatcher.hasNameAndParameters("renderDateTimeField",
+                MacroCallParameterMatcher.hasNameAndStringValue("mask", "9999-99-99"),
+                MacroCallParameterMatcher.hasNameAndIntegerValue("size", 10),
+                MacroCallParameterMatcher.hasNameAndIntegerValue("maxlength", 10)));
+    }
+
+    @Test
+    public void datetimeFieldSetsLengthForTimeType(@Mocked final ModelFormField.DateTimeField datetimeField) {
+        new Expectations() {
+            {
+                datetimeField.isTimeType();
+                result = true;
+
+                datetimeField.useMask();
+                result = true;
+
+                modelFormField.getEntry(withNotNull(), anyString);
+                result = "DATETIMEVALUE";
+            }
+        };
+
+        final HashMap<String, Object> context = new HashMap<>();
+
+        final RenderableFtl renderableFtl = renderableFtlFormElementsBuilder.dateTime(context, datetimeField);
+        assertThat(renderableFtl, MacroCallMatcher.hasNameAndParameters("renderDateTimeField",
+                MacroCallParameterMatcher.hasNameAndStringValue("mask", "99:99:99"),
+                MacroCallParameterMatcher.hasNameAndIntegerValue("size", 8),
+                MacroCallParameterMatcher.hasNameAndIntegerValue("maxlength", 8)));
+    }
+
+    @Test
+    public void datetimeFieldSetsLengthForTimestampType(@Mocked final ModelFormField.DateTimeField datetimeField) {
+        new Expectations() {
+            {
+                datetimeField.isTimestampType();
+                result = true;
+                minTimes = 0;
+
+                datetimeField.useMask();
+                result = true;
+
+                modelFormField.getEntry(withNotNull(), anyString);
+                result = "DATETIMEVALUE";
+            }
+        };
+
+        final HashMap<String, Object> context = new HashMap<>();
+
+        final RenderableFtl renderableFtl = renderableFtlFormElementsBuilder.dateTime(context, datetimeField);
+        assertThat(renderableFtl, MacroCallMatcher.hasNameAndParameters("renderDateTimeField",
+                MacroCallParameterMatcher.hasNameAndStringValue("mask", "9999-99-99 99:99:99"),
+                MacroCallParameterMatcher.hasNameAndIntegerValue("size", 25),
+                MacroCallParameterMatcher.hasNameAndIntegerValue("maxlength", 30)));
+    }
+
+    @Test
+    public void datetimeFieldSetsTimeValuesForStepSize1(@Mocked final ModelFormField.DateTimeField datetimeField) {
+        new Expectations() {
+            {
+                datetimeField.getStep();
+                result = 1;
+
+                datetimeField.getInputMethod();
+                result = "time-dropdown";
+
+                modelFormField.getEntry(withNotNull(), anyString);
+                result = "DATETIMEVALUE";
+            }
+        };
+
+        final HashMap<String, Object> context = new HashMap<>();
+
+        final RenderableFtl renderableFtl = renderableFtlFormElementsBuilder.dateTime(context, datetimeField);
+        assertThat(renderableFtl, MacroCallMatcher.hasNameAndParameters("renderDateTimeField",
+                MacroCallParameterMatcher.hasNameAndStringValueStartsWith("timeValues", "[0, 1, 2, 3,"),
+                MacroCallParameterMatcher.hasNameAndStringValueEndsWith("timeValues", "56, 57, 58, 59]")));
+    }
+
+    @Test
+    public void datetimeFieldSetsTimeValuesForStepSize3(@Mocked final ModelFormField.DateTimeField datetimeField) {
+        new Expectations() {
+            {
+                datetimeField.getStep();
+                result = 3;
+
+                datetimeField.getInputMethod();
+                result = "time-dropdown";
+
+                modelFormField.getEntry(withNotNull(), anyString);
+                result = "DATETIMEVALUE";
+            }
+        };
+
+        final HashMap<String, Object> context = new HashMap<>();
+
+        final RenderableFtl renderableFtl = renderableFtlFormElementsBuilder.dateTime(context, datetimeField);
+        assertThat(renderableFtl, MacroCallMatcher.hasNameAndParameters("renderDateTimeField",
+                MacroCallParameterMatcher.hasNameAndStringValueStartsWith("timeValues", "[0, 3, 6, 9,"),
+                MacroCallParameterMatcher.hasNameAndStringValueEndsWith("timeValues", "48, 51, 54, 57]")));
+    }
+
+    @Test
+    public void datetimeFieldSetsValuesFor12HourClock(@Mocked final ModelFormField.DateTimeField datetimeField) {
+        new Expectations() {
+            {
+                datetimeField.getStep();
+                result = 1;
+
+                datetimeField.getInputMethod();
+                result = "time-dropdown";
+
+                datetimeField.isTwelveHour();
+                result = true;
+
+                modelFormField.getEntry(withNotNull(), anyString);
+                result = "2022-05-18 16:44:57";
+            }
+        };
+
+        final HashMap<String, Object> context = new HashMap<>();
+
+        final RenderableFtl renderableFtl = renderableFtlFormElementsBuilder.dateTime(context, datetimeField);
+        assertThat(renderableFtl, MacroCallMatcher.hasNameAndParameters("renderDateTimeField",
+                MacroCallParameterMatcher.hasNameAndIntegerValue("hour1", 4),
+                MacroCallParameterMatcher.hasNameAndIntegerValue("hour2", 16),
+                MacroCallParameterMatcher.hasNameAndBooleanValue("isTwelveHour", true),
+                MacroCallParameterMatcher.hasNameAndBooleanValue("pmSelected", true)));
+    }
+}
diff --git a/themes/common-theme/template/macro/HtmlFormMacroLibrary.ftl b/themes/common-theme/template/macro/HtmlFormMacroLibrary.ftl
index 94a9ba7b57..d1d1f6be1e 100644
--- a/themes/common-theme/template/macro/HtmlFormMacroLibrary.ftl
+++ b/themes/common-theme/template/macro/HtmlFormMacroLibrary.ftl
@@ -88,13 +88,13 @@ under the License.
   </textarea><#lt/>
 </#macro>
 
-<#macro renderDateTimeField name className alert dateType timeDropdownParamName defaultDateTimeString localizedIconTitle timeHourName timeMinutesName minutes isTwelveHour ampmName amSelected pmSelected compositeType timeDropdown="" classString="" hour1="" hour2="" shortDateInput="" title="" value="" size="" maxlength="" id="" formName="" mask="" event="" action="" step="" timeValues="" tabindex="" disabled=false isXMLHttpRequest="">
+<#macro renderDateTimeField name className alert timeDropdownParamName defaultDateTimeString localizedIconTitle timeHourName timeMinutesName minutes isTwelveHour ampmName compositeType isTimeType=false isDateType=false amSelected=false pmSelected=false timeDropdown="" classString="" hour1="" hour2="" shortDateInput="" title="" value="" size="" maxlength="" id="" formName="" mask="" event="" action="" step="" timeValues="" tabindex="" disabled=false isXMLHttpRequest="">
   <span class="view-calendar">
     <#local cultureInfo = Static["org.apache.ofbiz.common.JsLanguageFilesMappingUtil"].getFile("datejs", .locale)/>
     <#local datePickerLang = Static["org.apache.ofbiz.common.JsLanguageFilesMappingUtil"].getFile("jquery", .locale)/>
     <#local timePicker = "/common/js/node_modules/@chinchilla-software/jquery-ui-timepicker-addon/dist/jquery-ui-timepicker-addon.min.js,/common/js/node_modules/@chinchilla-software/jquery-ui-timepicker-addon/dist/jquery-ui-timepicker-addon.css"/>
     <#local timePickerLang = Static["org.apache.ofbiz.common.JsLanguageFilesMappingUtil"].getFile("dateTime", .locale)/>
-    <#if dateType!="time" >
+    <#if !isTimeType >
       <input type="text" name="${name}_i18n" <@renderClass className alert /> <@renderDisabled disabled />
         <#if tabindex?has_content> tabindex="${tabindex}"</#if>
         <#if title?has_content> title="${title}"</#if>
@@ -140,8 +140,8 @@ under the License.
         <#rt/>
         <#if isTwelveHour>
           <select name="${ampmName}" <@renderDisabled disabled /> <#if classString?has_content>class="${classString}"</#if>><#rt/>
-            <option value="AM" <#if "selected" == amSelected>selected="selected"</#if> >AM</option><#rt/>
-            <option value="PM" <#if "selected" == pmSelected>selected="selected"</#if>>PM</option><#rt/>
+            <option value="AM" <#if amSelected>selected="selected"</#if>>AM</option><#rt/>
+            <option value="PM" <#if pmSelected>selected="selected"</#if>>PM</option><#rt/>
           </select>
         <#rt/>
       </#if>
@@ -442,14 +442,14 @@ under the License.
   </#if>
 </#macro>
 
-<#macro renderDateFindField className alert id name dateType formName value defaultDateTimeString imgSrc localizedIconTitle defaultOptionFrom defaultOptionThru opEquals opSameDay opGreaterThanFromDayStart opGreaterThan opGreaterThan opLessThan opUpToDay opUpThruDay opIsEmpty conditionGroup="" localizedInputTitle="" value2="" size="" maxlength="" titleStyle="" tabindex="" disabled=false>
+<#macro renderDateFindField className alert id name formName value defaultDateTimeString imgSrc localizedIconTitle defaultOptionFrom defaultOptionThru opEquals opSameDay opGreaterThanFromDayStart opGreaterThan opGreaterThan opLessThan opUpToDay opUpThruDay opIsEmpty isTimeType=false isDateType=false conditionGroup="" localizedInputTitle="" value2="" size="" maxlength="" titleStyle="" tabindex="" disabled=false>
   <#if conditionGroup?has_content>
     <input type="hidden" name="${name}_grp" value="${conditionGroup}" <@renderDisabled disabled />/>
   </#if>
-  <#if dateType != "time">
+  <#if !isTimeType>
     <#local className = className + " date-time-picker"/>
   </#if>
-  <#local shortDateInput = "date" == dateType/>
+  <#local shortDateInput = isDateType/>
   <#local cultureInfo = Static["org.apache.ofbiz.common.JsLanguageFilesMappingUtil"].getFile("datejs", .locale)/>
   <#local datePickerLang = Static["org.apache.ofbiz.common.JsLanguageFilesMappingUtil"].getFile("jquery", .locale)/>
   <#local timePicker = "/common/js/node_modules/@chinchilla-software/jquery-ui-timepicker-addon/dist/jquery-ui-timepicker-addon.min.js,/common/js/node_modules/@chinchilla-software/jquery-ui-timepicker-addon/dist/jquery-ui-timepicker-addon.css"/>
diff --git a/themes/common-theme/template/macro/XlsFormMacroLibrary.ftl b/themes/common-theme/template/macro/XlsFormMacroLibrary.ftl
index 3ef8b5545e..d4c564854c 100644
--- a/themes/common-theme/template/macro/XlsFormMacroLibrary.ftl
+++ b/themes/common-theme/template/macro/XlsFormMacroLibrary.ftl
@@ -34,9 +34,9 @@ under the License.
 
 <#macro renderTextareaField name className alert cols rows maxlength id readonly value visualEditorEnable buttons tabindex language="" disabled=""></#macro>
 
-<#macro renderDateTimeField name className alert title value size maxlength id dateType shortDateInput timeDropdownParamName defaultDateTimeString localizedIconTitle timeDropdown timeHourName classString hour1 hour2 timeMinutesName minutes isTwelveHour ampmName amSelected pmSelected compositeType formName mask="" event="" action="" step="" timeValues="" tabindex="" disabled="" isXMLHttpRequest=""> 
-<#if dateType=="time" ><@renderItemField value "tf" className/>
-<#elseif dateType=="date"><@renderItemField value "dt" className/>
+<#macro renderDateTimeField name className alert title value size maxlength id isTimeType isDateType shortDateInput timeDropdownParamName defaultDateTimeString localizedIconTitle timeDropdown timeHourName classString hour1 hour2 timeMinutesName minutes isTwelveHour ampmName amSelected pmSelected compositeType formName mask="" event="" action="" step="" timeValues="" tabindex="" disabled="" isXMLHttpRequest="">
+<#if isTimeType ><@renderItemField value "tf" className/>
+<#elseif isDateType><@renderItemField value "dt" className/>
 <#else><@renderItemField value "dtf" className/></#if>
 </#macro>