Index: src/sas/qtgui/Utilities/GuiUtils.py
===================================================================
--- src/sas/qtgui/Utilities/GuiUtils.py	(revision d4dac8028a51b020b8e01f0998d475e053206dea)
+++ src/sas/qtgui/Utilities/GuiUtils.py	(revision 57be490d989c9a52dd3d902191267a861890a9ef)
@@ -1,2 +1,3 @@
+# -*- coding: utf-8 -*-
 """
 Global defaults and various utility functions usable by the general GUI
@@ -248,4 +249,6 @@
     sendDataToGridSignal = QtCore.pyqtSignal(list)
 
+    # Action Save Analysis triggered
+    saveAnalysisSignal = QtCore.pyqtSignal()
 
 def updateModelItemWithPlot(item, update_data, name=""):
@@ -378,4 +381,27 @@
                  if str(i.text()) == filename])
     return item[0] if len(item)>0 else None
+
+def plotsFromModel(model_name, model_item):
+    """
+    Returns the list of plots for the item with model name in the model
+    """
+    assert isinstance(model_item, QtGui.QStandardItem)
+    assert isinstance(model_name, str)
+
+    plot_data = []
+    # Iterate over model looking for named items
+    for index in range(model_item.rowCount()):
+        item = model_item.child(index)
+        if isinstance(item.data(), (Data1D, Data2D)):
+            plot_data.append(item.data())
+        if model_name in str(item.text()):
+            #plot_data.append(item.child(0).data())
+            # Going 1 level deeper only
+            for index_2 in range(item.rowCount()):
+                item_2 = item.child(index_2)
+                if item_2 and isinstance(item_2.data(), (Data1D, Data2D)):
+                    plot_data.append(item_2.data())
+
+    return plot_data
 
 def plotsFromFilename(filename, model_item):
@@ -841,4 +867,55 @@
     return output.lstrip().rstrip()
 
+def replaceHTMLwithUTF8(html):
+    """
+    Replace some important HTML-encoded characters
+    with their UTF-8 equivalents
+    """
+    # Angstrom
+    html_out = html.replace("&#x212B;", "Ã
+")
+    # infinity
+    html_out = html_out.replace("&#x221e;", "â")
+    # +/-
+    html_out = html_out.replace("&#177;", "Â±")
+
+    return html_out
+
+def replaceHTMLwithASCII(html):
+    """
+    Replace some important HTML-encoded characters
+    with their ASCII equivalents
+    """
+    # Angstrom
+    html_out = html.replace("&#x212B;", "Ang")
+    # infinity
+    html_out = html_out.replace("&#x221e;", "inf")
+    # +/-
+    html_out = html_out.replace("&#177;", "+/-")
+
+    return html_out
+
+def convertUnitToUTF8(unit):
+    """
+    Convert ASCII unit display into UTF-8 symbol
+    """
+    if unit == "1/A":
+        return "Ã
+<sup>-1</sup>"
+    elif unit == "1/cm":
+        return "cm<sup>-1</sup>"
+    elif unit == "Ang":
+        return "Ã
+"
+    elif unit == "1e-6/Ang^2":
+        return "10<sup>-6</sup>/Ã
+<sup>2</sup>"
+    elif unit == "inf":
+        return "â"
+    elif unit == "-inf":
+        return "-â"
+    else:
+        return unit
+
 def convertUnitToHTML(unit):
     """
Index: src/sas/qtgui/Utilities/ReportDialog.py
===================================================================
--- src/sas/qtgui/Utilities/ReportDialog.py	(revision 57be490d989c9a52dd3d902191267a861890a9ef)
+++ src/sas/qtgui/Utilities/ReportDialog.py	(revision 57be490d989c9a52dd3d902191267a861890a9ef)
@@ -0,0 +1,183 @@
+import os
+import sys
+import re
+import logging
+from xhtml2pdf import pisa
+
+from PyQt5 import QtWidgets
+from PyQt5 import QtPrintSupport
+
+import sas.qtgui.Utilities.GuiUtils as GuiUtils
+
+from sas.qtgui.Utilities.UI.ReportDialogUI import Ui_ReportDialogUI
+
+
+class ReportDialog(QtWidgets.QDialog, Ui_ReportDialogUI):
+    """
+    Class for stateless grid-like printout of model parameters for mutiple models
+    """
+    def __init__(self, parent=None, report_list=None):
+
+        super(ReportDialog, self).__init__(parent._parent)
+        self.setupUi(self)
+
+        assert isinstance(report_list, list)
+        assert len(report_list) == 3
+
+        self.data_html, self.data_txt, self.data_images = report_list
+
+        # Fill in the table from input data
+        self.setupDialog(self.data_html)
+
+        # Command buttons
+        self.cmdPrint.clicked.connect(self.onPrint)
+        self.cmdSave.clicked.connect(self.onSave)
+
+    def setupDialog(self, output=None):
+        """
+        Display the HTML content in the browser.
+        """
+        if output is not None:
+            self.txtBrowser.setHtml(output)
+
+    def onPrint(self):
+        """
+        Display the print dialog and send the report to printer
+        """
+        # Define the printer
+        printer = QtPrintSupport.QPrinter()
+
+        # Display the print dialog
+        dialog = QtPrintSupport.QPrintDialog(printer)
+        dialog.setModal(True)
+        dialog.setWindowTitle("Print")
+        if dialog.exec_() != QtWidgets.QDialog.Accepted:
+            return
+
+        document = self.txtBrowser.document()
+        try:
+            # pylint chokes on this line with syntax-error
+            # pylint: disable=syntax-error doesn't seem to help
+            document.print(printer)
+        except Exception as ex:
+            # Printing can return various exceptions, let's catch them all
+            logging.error("Print report failed with: " + str(ex))
+
+    def onSave(self):
+        """
+        Display the Save As... prompt and save the report if instructed so
+        """
+        # Choose user's home directory
+        location = os.path.expanduser('~')
+        # Use a sensible filename default
+        default_name = os.path.join(location, 'fit_report.pdf')
+        kwargs = {
+            'parent'   : self,
+            'caption'  : 'Save Report',
+            'directory': default_name,
+            'filter'   : 'PDF file (*.pdf);;HTML file (*.html);;Text file (*.txt)',
+            'options'  : QtWidgets.QFileDialog.DontUseNativeDialog}
+        # Query user for filename.
+        filename_tuple = QtWidgets.QFileDialog.getSaveFileName(**kwargs)
+        filename = filename_tuple[0]
+        if not filename:
+            return
+        extension = filename_tuple[1]
+
+        try:
+            # extract extension from filter
+            # e.g. "PDF file (*.pdf)" -> ".pdf"
+            ext = extension[extension.find("(")+2:extension.find(")")]
+        except IndexError as ex:
+            # (ext) not found...
+            logging.error("Error while saving report. " + str(ex))
+            return
+        basename, extension = os.path.splitext(filename)
+        if not extension:
+            filename = '.'.join((filename, ext))
+
+        # Create files with charts
+        pictures = self.getPictures(basename)
+
+        # translate png references into html from base64 string to on-disk name
+        cleanr = re.compile('<img src="data:image.*>')
+        replacement_name = ""
+        for picture in pictures:
+            replacement_name += '<img src="'+ picture + '"><p></p>'
+
+        # <img src="data:image/png;.*>  => <img src=filename>
+        html = re.sub(cleanr, replacement_name, self.data_html)
+
+        if ext.lower() == ".txt":
+            txt_ascii = GuiUtils.replaceHTMLwithASCII(self.data_txt)
+            self.onTXTSave(txt_ascii, filename)
+        if ext.lower() == ".html":
+            self.onHTMLSave(html, filename)
+        if ext.lower() == ".pdf":
+            html_utf = GuiUtils.replaceHTMLwithUTF8(html)
+            pdf_success = self.HTML2PDF(html_utf, filename)
+            # delete images used to create the pdf
+            for pic_name in pictures:
+                os.remove(pic_name)
+            #open pdf viewer
+            if pdf_success == 0:
+                try:
+                    if os.name == 'nt':  # Windows
+                        os.startfile(filename)
+                    elif sys.platform == "darwin":  # Mac
+                        os.system("open %s" % filename)
+                except Exception as ex:
+                    # cannot open pdf.
+                    # We don't know what happened in os.* , so broad Exception is required
+                    logging.error(str(ex))
+
+    def getPictures(self, basename):
+        """
+        Returns list of saved MPL figures
+        """
+        # save figures
+        pictures = []
+        for num, image in enumerate(self.data_images):
+            pic_name = basename + '_img%s.png' % num
+            # save the image for use with pdf writer
+            image.savefig(pic_name)
+            pictures.append(pic_name)
+        return pictures
+
+    @staticmethod
+    def onTXTSave(data, filename):
+        """
+        Simple txt file serializatio
+        """
+        with open(filename, 'w') as f:
+            f.write(data)
+
+    @staticmethod
+    def onHTMLSave(html, filename):
+        """
+        HTML file write
+        """
+        with open(filename, 'w') as f:
+            f.write(html)
+
+    @staticmethod
+    def HTML2PDF(data, filename):
+        """
+        Create a PDF file from html source string.
+        Returns True is the file creation was successful.
+        : data: html string
+        : filename: name of file to be saved
+        """
+        try:
+            # open output file for writing (truncated binary)
+            with open(filename, "w+b") as resultFile:
+                # convert HTML to PDF
+                pisaStatus = pisa.CreatePDF(data.encode("UTF-8"),
+                                            dest=resultFile,
+                                            encoding='UTF-8')
+                return pisaStatus.err
+        except Exception as ex:
+            logging.error("Error creating pdf: " + str(ex))
+        return False
+
+
Index: src/sas/qtgui/Utilities/UI/ReportDialogUI.ui
===================================================================
--- src/sas/qtgui/Utilities/UI/ReportDialogUI.ui	(revision 57be490d989c9a52dd3d902191267a861890a9ef)
+++ src/sas/qtgui/Utilities/UI/ReportDialogUI.ui	(revision 57be490d989c9a52dd3d902191267a861890a9ef)
@@ -0,0 +1,79 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>ReportDialogUI</class>
+ <widget class="QDialog" name="ReportDialogUI">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>519</width>
+    <height>461</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Report: Fitting</string>
+  </property>
+  <layout class="QGridLayout" name="gridLayout">
+   <item row="0" column="0">
+    <widget class="QTextBrowser" name="txtBrowser"/>
+   </item>
+   <item row="1" column="0">
+    <layout class="QHBoxLayout" name="horizontalLayout">
+     <item>
+      <spacer name="horizontalSpacer">
+       <property name="orientation">
+        <enum>Qt::Horizontal</enum>
+       </property>
+       <property name="sizeHint" stdset="0">
+        <size>
+         <width>40</width>
+         <height>20</height>
+        </size>
+       </property>
+      </spacer>
+     </item>
+     <item>
+      <widget class="QPushButton" name="cmdPrint">
+       <property name="text">
+        <string>Print</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QPushButton" name="cmdSave">
+       <property name="text">
+        <string>Save</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QPushButton" name="cmdClose">
+       <property name="text">
+        <string>Close</string>
+       </property>
+      </widget>
+     </item>
+    </layout>
+   </item>
+  </layout>
+ </widget>
+ <resources/>
+ <connections>
+  <connection>
+   <sender>cmdClose</sender>
+   <signal>clicked()</signal>
+   <receiver>ReportDialogUI</receiver>
+   <slot>close()</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>426</x>
+     <y>433</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>259</x>
+     <y>230</y>
+    </hint>
+   </hints>
+  </connection>
+ </connections>
+</ui>
Index: src/sas/qtgui/Utilities/UnitTesting/GuiUtilsTest.py
===================================================================
--- src/sas/qtgui/Utilities/UnitTesting/GuiUtilsTest.py	(revision 6cb305acde0f8a18b6580f6801bf0cd6613b4587)
+++ src/sas/qtgui/Utilities/UnitTesting/GuiUtilsTest.py	(revision 57be490d989c9a52dd3d902191267a861890a9ef)
@@ -1,2 +1,3 @@
+# -*- coding: utf-8 -*-
 import sys
 import unittest
@@ -446,4 +447,93 @@
         self.assertEqual(yscale, "log")
 
+    def testReplaceHTMLwithUTF8(self):
+        ''' test single character replacement '''
+        s = None
+        with self.assertRaises(AttributeError):
+            result = replaceHTMLwithUTF8(s)
+
+        s = ""
+        self.assertEqual(replaceHTMLwithUTF8(s), s)
+
+        s = "aaaa"
+        self.assertEqual(replaceHTMLwithUTF8(s), s)
+
+        s = "&#x212B; &#x221e;      &#177;"
+        self.assertEqual(replaceHTMLwithUTF8(s), "Ã
+ â      Â±")
+
+    def testReplaceHTMLwithASCII(self):
+        ''' test single character replacement'''
+        s = None
+        with self.assertRaises(AttributeError):
+            result = replaceHTMLwithASCII(s)
+
+        s = ""
+        self.assertEqual(replaceHTMLwithASCII(s), s)
+
+        s = "aaaa"
+        self.assertEqual(replaceHTMLwithASCII(s), s)
+
+        s = "&#x212B; &#x221e;      &#177;"
+        self.assertEqual(replaceHTMLwithASCII(s), "Ang inf      +/-")
+
+    def testConvertUnitToUTF8(self):
+        ''' test unit string replacement'''
+        s = None
+        self.assertIsNone(convertUnitToUTF8(s))
+
+        s = ""
+        self.assertEqual(convertUnitToUTF8(s), s)
+
+        s = "aaaa"
+        self.assertEqual(convertUnitToUTF8(s), s)
+
+        s = "1/A"
+        self.assertEqual(convertUnitToUTF8(s), "Ã
+<sup>-1</sup>")
+
+        s = "Ang"
+        self.assertEqual(convertUnitToUTF8(s), "Ã
+")
+
+        s = "1e-6/Ang^2"
+        self.assertEqual(convertUnitToUTF8(s), "10<sup>-6</sup>/Ã
+<sup>2</sup>")
+
+        s = "inf"
+        self.assertEqual(convertUnitToUTF8(s), "â")
+
+        s = "1/cm"
+        self.assertEqual(convertUnitToUTF8(s), "cm<sup>-1</sup>")
+
+    def testConvertUnitToHTML(self):
+        ''' test unit string replacement'''
+        s = None
+        self.assertIsNone(convertUnitToHTML(s))
+
+        s = ""
+        self.assertEqual(convertUnitToHTML(s), s)
+
+        s = "aaaa"
+        self.assertEqual(convertUnitToHTML(s), s)
+
+        s = "1/A"
+        self.assertEqual(convertUnitToHTML(s), "&#x212B;<sup>-1</sup>")
+
+        s = "Ang"
+        self.assertEqual(convertUnitToHTML(s), "&#x212B;")
+
+        s = "1e-6/Ang^2"
+        self.assertEqual(convertUnitToHTML(s), "10<sup>-6</sup>/&#x212B;<sup>2</sup>")
+
+        s = "inf"
+        self.assertEqual(convertUnitToHTML(s), "&#x221e;")
+        s = "-inf"
+
+        self.assertEqual(convertUnitToHTML(s), "-&#x221e;")
+
+        s = "1/cm"
+        self.assertEqual(convertUnitToHTML(s), "cm<sup>-1</sup>")
+
     def testParseName(self):
         '''test parse out a string from the beinning of a string'''
Index: src/sas/qtgui/Utilities/UnitTesting/ReportDialogTest.py
===================================================================
--- src/sas/qtgui/Utilities/UnitTesting/ReportDialogTest.py	(revision 57be490d989c9a52dd3d902191267a861890a9ef)
+++ src/sas/qtgui/Utilities/UnitTesting/ReportDialogTest.py	(revision 57be490d989c9a52dd3d902191267a861890a9ef)
@@ -0,0 +1,150 @@
+import os
+import sys
+import unittest
+import logging
+from xhtml2pdf import pisa
+
+from unittest.mock import mock_open, patch
+
+from unittest.mock import MagicMock
+
+from PyQt5 import QtWidgets, QtPrintSupport
+
+# set up import paths
+import path_prepare
+
+import sas.qtgui.Utilities.GuiUtils as GuiUtils
+# Local
+from sas.qtgui.Utilities.ReportDialog import ReportDialog
+
+if not QtWidgets.QApplication.instance():
+    app = QtWidgets.QApplication(sys.argv)
+
+class ReportDialogTest(unittest.TestCase):
+    '''Test the report dialog'''
+    def setUp(self):
+        '''Create the dialog'''
+        class dummy_manager(object):
+            _parent = QtWidgets.QWidget()
+            def communicator(self):
+                return GuiUtils.Communicate()
+            def communicate(self):
+                return GuiUtils.Communicate()
+
+        test_html = "test_html"
+        test_txt = "test_txt"
+        test_images = []
+        self.test_list = [test_html, test_txt, test_images]
+        self.widget = ReportDialog(parent=dummy_manager(), report_list=self.test_list)
+
+    def tearDown(self):
+        '''Destroy the GUI'''
+        #self.widget.close()
+        self.widget = None
+
+    def testDefaults(self):
+        '''Look at the default state of the widget'''
+        self.assertIn(self.test_list[0], self.widget.txtBrowser.toHtml())
+        self.assertTrue(self.widget.txtBrowser.isReadOnly())
+
+    def testOnPrint(self):
+        ''' Printing the report '''
+        document = self.widget.txtBrowser.document()
+        document.print = MagicMock()
+
+        # test rejected dialog
+        QtPrintSupport.QPrintDialog.exec_ = MagicMock(return_value=QtWidgets.QDialog.Rejected)
+
+        # invoke the method
+        self.widget.onPrint()
+
+        # Assure printing was not done
+        self.assertFalse(document.print.called)
+
+        # test accepted dialog
+        QtPrintSupport.QPrintDialog.exec_ = MagicMock(return_value=QtWidgets.QDialog.Accepted)
+
+        # invoke the method
+        self.widget.onPrint()
+
+        # Assure printing was done
+        self.assertTrue(document.print.called)
+
+
+    def testOnSave(self):
+        ''' Saving the report to a file '''
+        # PDF save
+        QtWidgets.QFileDialog.getSaveFileName = MagicMock(return_value=["test.pdf", "(*.pdf)"])
+        os.startfile = MagicMock()
+        os.system = MagicMock()
+
+        # conversion failed
+        self.widget.HTML2PDF = MagicMock(return_value=1)
+
+        # invoke the method
+        self.widget.onSave()
+
+        # Check that the file wasn't saved
+        if os.name == 'nt':  # Windows
+            self.assertFalse(os.startfile.called)
+        elif sys.platform == "darwin":  # Mac
+            self.assertFalse(os.system.called)
+
+        # conversion succeeded
+        self.widget.HTML2PDF = MagicMock(return_value=0)
+
+        # invoke the method
+        self.widget.onSave()
+
+        # Check that the file was saved
+        if os.name == 'nt':  # Windows
+            self.assertTrue(os.startfile.called)
+        elif sys.platform == "darwin":  # Mac
+            self.assertTrue(os.system.called)
+
+        # TXT save
+        QtWidgets.QFileDialog.getSaveFileName = MagicMock(return_value=["test.txt", "(*.txt)"])
+        self.widget.onTXTSave = MagicMock()
+        # invoke the method
+        self.widget.onSave()
+
+        # Check that the file was saved
+        self.assertTrue(self.widget.onTXTSave)
+
+        # HTML save
+        QtWidgets.QFileDialog.getSaveFileName = MagicMock(return_value=["test.html", "(*.html)"])
+        self.widget.onHTMLSave = MagicMock()
+        # invoke the method
+        self.widget.onSave()
+
+        # Check that the file was saved
+        self.assertTrue(self.widget.onHTMLSave)
+
+    def testGetPictures(self):
+        ''' Saving MPL charts and returning filenames '''
+        pass
+
+    def testHTML2PDF(self):
+        ''' html to pdf conversion '''
+        class pisa_dummy(object):
+            err = 0
+        pisa.CreatePDF = MagicMock(return_value=pisa_dummy())
+        open = MagicMock(return_value="y")
+
+        data = self.widget.txtBrowser.toHtml()
+        return_value = self.widget.HTML2PDF(data, "b")
+
+        self.assertTrue(pisa.CreatePDF.called)
+        self.assertEqual(return_value, 0)
+
+        # Try to raise somewhere
+        pisa.CreatePDF.side_effect = Exception("Failed")
+
+        logging.error = MagicMock()
+
+        #run the method
+        return_value = self.widget.HTML2PDF(data, "c")
+
+        self.assertTrue(logging.error.called)
+        logging.error.assert_called_with("Error creating pdf: Failed")
+
