diff --git a/commerce_pos.links.menu.yml b/commerce_pos.links.menu.yml
index d958b53..5375811 100755
--- a/commerce_pos.links.menu.yml
+++ b/commerce_pos.links.menu.yml
@@ -29,4 +29,4 @@ commerce_pos.main:
   title: 'Point of Sale'
   route_name: 'commerce_pos.main'
   parent: 'commerce_pos.base'
-  description: 'Point of Sale Interface'
\ No newline at end of file
+  description: 'Point of Sale Interface'
diff --git a/commerce_pos.permissions.yml b/commerce_pos.permissions.yml
index ef877b2..9aaf12a 100755
--- a/commerce_pos.permissions.yml
+++ b/commerce_pos.permissions.yml
@@ -1,4 +1,4 @@
 'access commerce pos administration pages':
   title: 'Use the commerce point of sale administration pages'
 'access commerce pos pages':
-  title: 'Use the commerce point of sale standard functionality'
\ No newline at end of file
+  title: 'Use the commerce point of sale standard functionality'
diff --git a/modules/receipt/README.md b/modules/receipt/README.md
new file mode 100644
index 0000000..398fb19
--- /dev/null
+++ b/modules/receipt/README.md
@@ -0,0 +1,53 @@
+The receipt submodule provides receipt printing. You may configure custom
+receipt header/footer at `admin/commerce/config/pos/receipt`.
+
+This module depends on the `jQuery.print` plugin which should reside in your 
+site's `/libraries` directory.
+
+We can install `jQuery.print` using composer or by downloading manually.
+
+### Composer method
+1. Open composer.json file of your site.
+2. Add `"DoersGuild/jQuery.print": "master"` to the `"require"`.
+```
+"require": {
+    .
+    .
+    "DoersGuild/jQuery.print": "master"
+ }
+```
+3. Add `"libraries/{$name}": ["type:drupal-library"]` 
+to the `"installer-paths"`.
+```
+"installer-paths": {
+    .
+    .
+    "libraries/{$name}": ["type:drupal-library"]
+}
+```
+4. Add `DoersGuild/jQuery.print` as a new `package` to `"repositories"`.
+```
+"repositories": {
+    .
+    .
+    {
+        "type": "package",
+        "package": {
+            "name": "DoersGuild/jQuery.print",
+            "version": "master",
+            "type": "drupal-library",
+            "source": {
+                "url": "https://github.com/DoersGuild/jQuery.print.git",
+                "type": "git",
+                "reference": "origin/master"
+            }
+        }
+    }   
+}
+```
+5. Run `composer update DoersGuild/jQuery.print`.
+
+### Manual method
+1. Create `libraries` folder if it doesn't exists in the docroot of website.
+2. `cd` into libraries folder and run 
+`git clone https://github.com/DoersGuild/jQuery.print.git`.
diff --git a/modules/receipt/commerce_pos_receipt.info.yml b/modules/receipt/commerce_pos_receipt.info.yml
new file mode 100644
index 0000000..628b5f2
--- /dev/null
+++ b/modules/receipt/commerce_pos_receipt.info.yml
@@ -0,0 +1,8 @@
+name: commerce_pos_receipt
+description: Provides receipts printing for Commerce Point of Sale
+type: module
+core: 8.x
+package: Commerce (POS)
+configure: commerce_pos_receipt.settings
+dependencies:
+  - commerce_pos
diff --git a/modules/receipt/commerce_pos_receipt.install b/modules/receipt/commerce_pos_receipt.install
new file mode 100644
index 0000000..256dcee
--- /dev/null
+++ b/modules/receipt/commerce_pos_receipt.install
@@ -0,0 +1,15 @@
+<?php
+
+/**
+ * @file
+ * Contains commerce_pos_receipt.install.
+ */
+
+/**
+ * Implements hook_uninstall().
+ */
+function commerce_pos_receipt_uninstall() {
+  // Remove states.
+  Drupal::state()->delete('commerce_pos_receipt.header');
+  Drupal::state()->delete('commerce_pos_receipt.footer');
+}
diff --git a/modules/receipt/commerce_pos_receipt.libraries.yml b/modules/receipt/commerce_pos_receipt.libraries.yml
new file mode 100755
index 0000000..cd93c93
--- /dev/null
+++ b/modules/receipt/commerce_pos_receipt.libraries.yml
@@ -0,0 +1,21 @@
+receipt:
+  version: 1.x
+  css:
+    theme:
+      css/commerce_pos_receipt.css: {}
+  js:
+    js/commerce_pos_receipt.js: {}
+  dependencies:
+    - core/jquery
+    - core/drupal
+    - core/drupalSettings
+    - core/drupal.ajax
+jQuery.print:
+  remote: https://github.com/DoersGuild/jQuery.print
+  version: 1.5.1
+  license:
+    name: CC BY 3.0
+    url: https://github.com/DoersGuild/jQuery.print/blob/master/LICENSE
+    gpl-compatible: no
+  js:
+    /libraries/jQuery.print/jQuery.print.js: {}
diff --git a/modules/receipt/commerce_pos_receipt.links.action.yml b/modules/receipt/commerce_pos_receipt.links.action.yml
new file mode 100644
index 0000000..2535083
--- /dev/null
+++ b/modules/receipt/commerce_pos_receipt.links.action.yml
@@ -0,0 +1,5 @@
+commerce_pos_receipt_show_action:
+  route_name: commerce_pos_receipt.show
+  title: 'Show receipt'
+  appears_on:
+    - entity.commerce_order.canonical
diff --git a/modules/receipt/commerce_pos_receipt.links.menu.yml b/modules/receipt/commerce_pos_receipt.links.menu.yml
new file mode 100755
index 0000000..9553324
--- /dev/null
+++ b/modules/receipt/commerce_pos_receipt.links.menu.yml
@@ -0,0 +1,6 @@
+commerce_pos_receipt.configuration:
+  title: 'Receipt Configuration'
+  parent: commerce_pos.configuration
+  description: 'Configure the header and footers of the POS receipts.'
+  route_name: commerce_pos_receipt.settings
+  weight: 25
diff --git a/modules/receipt/commerce_pos_receipt.module b/modules/receipt/commerce_pos_receipt.module
new file mode 100644
index 0000000..627d979
--- /dev/null
+++ b/modules/receipt/commerce_pos_receipt.module
@@ -0,0 +1,81 @@
+<?php
+
+/**
+ * @file
+ * Contains commerce_pos_receipt.module.
+ */
+
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Url;
+
+/**
+ * Implements hook_help().
+ */
+function commerce_pos_receipt_help($route_name, RouteMatchInterface $route_match) {
+  switch ($route_name) {
+    // Main module help for the commerce_pos_receipt module.
+    case 'help.page.commerce_pos_receipt':
+      $output = '';
+      $output .= '<h3>' . t('About') . '</h3>';
+      $output .= '<p>' . t('Provides receipt printing for Commerce Point of Sale') . '</p>';
+      return $output;
+  }
+}
+
+/**
+ * Implements hook_theme().
+ */
+function commerce_pos_receipt_theme() {
+  $theme['commerce_pos_receipt'] = [
+    'variables' => [
+      'receipt' => NULL,
+    ],
+    'template' => 'commerce-pos-receipt',
+  ];
+
+  return $theme;
+}
+
+/**
+ * Implements hook_entity_operation().
+ */
+function commerce_pos_receipt_entity_operation(EntityInterface $entity) {
+  $operations = [];
+
+  if ($entity->getEntityTypeId() == 'commerce_order') {
+    $operations['show_receipt'] = [
+      'title' => t('Show receipt'),
+      'url' => Url::fromRoute('commerce_pos_receipt.show', ['commerce_order' => $entity->id()]),
+      'weight' => 50,
+    ];
+  }
+
+  return $operations;
+}
+
+/**
+ * Implements hook_form_FORM_ID_alter() for form changing.
+ */
+function commerce_pos_receipt_form_commerce_pos_alter(&$form, FormStateInterface $form_state, $form_id) {
+  if($form_state->get('step') == 'payment') {
+    $form['receipt'] = [
+      '#type' => 'container',
+    ];
+
+    $form['#attached']['library'][] = 'commerce_pos_receipt/receipt';
+    $form['#attached']['library'][] = 'commerce_pos_receipt/jQuery.print';
+
+    $form['#attached']['drupalSettings']['commercePosReceipt'] = [
+      'orderId' => $form_state->getValue('order_id'),
+    ];
+
+    $form['receipt']['contents'] = [
+      '#markup' => '<div id="commerce-pos-receipt"></div>',
+    ];
+
+    $form['actions']['finish']['#attributes']['class'][] = 'commerce-pos-receipt-button';
+  }
+
+}
diff --git a/modules/receipt/commerce_pos_receipt.permissions.yml b/modules/receipt/commerce_pos_receipt.permissions.yml
new file mode 100644
index 0000000..5d5153e
--- /dev/null
+++ b/modules/receipt/commerce_pos_receipt.permissions.yml
@@ -0,0 +1,3 @@
+administer pos receipt:
+  title: 'Administer POS receipt settings'
+  restrict access: TRUE
diff --git a/modules/receipt/commerce_pos_receipt.routing.yml b/modules/receipt/commerce_pos_receipt.routing.yml
new file mode 100644
index 0000000..c975e5b
--- /dev/null
+++ b/modules/receipt/commerce_pos_receipt.routing.yml
@@ -0,0 +1,30 @@
+commerce_pos_receipt.ajax:
+  path: '/admin/commerce/pos/{commerce_order}/ajax-receipt'
+  defaults:
+    _controller: '\Drupal\commerce_pos_receipt\Controller\PrintController::ajaxReceipt'
+    _title: 'Order Receipt'
+  options:
+    parameters:
+      commerce_order:
+        type: 'entity:commerce_order'
+  requirements:
+    _permission: 'administer commerce_order'
+
+commerce_pos_receipt.show:
+  path: '/admin/commerce/pos/{commerce_order}/show-receipt'
+  defaults:
+    _controller: '\Drupal\commerce_pos_receipt\Controller\PrintController::showReceipt'
+    _title: 'Order Receipt'
+  options:
+    parameters:
+      commerce_order:
+        type: 'entity:commerce_order'
+  requirements:
+    _permission: 'administer commerce_order'
+
+commerce_pos_receipt.settings:
+  path: '/admin/commerce/config/pos/receipt'
+  defaults:
+    _form: '\Drupal\commerce_pos_receipt\Form\ReceiptSettingsForm'
+  requirements:
+    _permission: 'administer pos receipt'
diff --git a/modules/receipt/config/install/commerce_pos_receipt.settings.yml b/modules/receipt/config/install/commerce_pos_receipt.settings.yml
new file mode 100644
index 0000000..c05db45
--- /dev/null
+++ b/modules/receipt/config/install/commerce_pos_receipt.settings.yml
@@ -0,0 +1,8 @@
+dependencies:
+  module:
+    - commerce_pos_receipt
+    - commerce_pos
+header: "(logo??)<br />Company Name/Store#<br />Address line 1<br />Address line 2<br />Phone #"
+footer: "Thanks for shopping at CompanyName!<br />Return Policy --<br />Exchange Policy --<br />Company Motto<br />Website --<br /><br />(optional)<br />Visit our website for a chance to --"
+header_format: "html"
+footer_format: "html"
diff --git a/modules/receipt/config/schema/commerce_pos_receipt.schema.yml b/modules/receipt/config/schema/commerce_pos_receipt.schema.yml
new file mode 100644
index 0000000..640f15d
--- /dev/null
+++ b/modules/receipt/config/schema/commerce_pos_receipt.schema.yml
@@ -0,0 +1,18 @@
+# Schema for the configuration files of the Receipt module.
+
+commerce_pos_receipt.settings:
+  type: config_object
+  label: 'Receipt settings'
+  mapping:
+    header:
+      type: string
+      label: 'Header'
+    footer:
+      type: string
+      label: 'Footer'
+    header_format:
+      type: string
+      label: Header format
+    footer_format:
+      type: string
+      label: Footer format
diff --git a/modules/receipt/css/commerce_pos_receipt.css b/modules/receipt/css/commerce_pos_receipt.css
new file mode 100644
index 0000000..16c0a76
--- /dev/null
+++ b/modules/receipt/css/commerce_pos_receipt.css
@@ -0,0 +1,5 @@
+#commerce-pos-receipt {
+  display: none;
+}
+
+/*# sourceMappingURL=commerce_pos_receipt.css.map */
diff --git a/modules/receipt/css/commerce_pos_receipt.css.map b/modules/receipt/css/commerce_pos_receipt.css.map
new file mode 100644
index 0000000..9d4296f
--- /dev/null
+++ b/modules/receipt/css/commerce_pos_receipt.css.map
@@ -0,0 +1 @@
+{"version":3,"file":"commerce_pos_receipt.css","sources":["commerce_pos_receipt.scss"],"sourcesContent":["#commerce-pos-receipt {\n  display: none;\n}\n"],"mappings":"AAAA,AAAA,qBAAqB,AAAC,CACpB,OAAO,CAAE,IAAK,CACf","names":[]}
\ No newline at end of file
diff --git a/modules/receipt/css/commerce_pos_receipt_print.css b/modules/receipt/css/commerce_pos_receipt_print.css
new file mode 100644
index 0000000..b13e3c2
--- /dev/null
+++ b/modules/receipt/css/commerce_pos_receipt_print.css
@@ -0,0 +1,3 @@
+@page{margin:5mm;size:72mm 200mm}.pos-receipt{color:#000;font-family:Arial, sans-serif;font-size:16px;font-weight:bold;text-transform:uppercase}.pos-receipt .pos-order-info{margin-bottom:1em;padding-bottom:1em;border-bottom:1px dashed #000}.pos-receipt table{width:100%;padding-bottom:1em;margin-bottom:1em;border-bottom:1px dashed #000}.pos-receipt td,.pos-receipt th{text-align:left;font-size:16px;font-weight:bold;text-transform:uppercase}.pos-receipt .payment-no-total td .payment-name,.pos-receipt .payment-no-total td.component-total{text-decoration:line-through}.pos-receipt .payment-no-total td span.commerce-pos-receipt-void-message{text-decoration:none}.pos-receipt .payment-status-void td span.commerce-pos-receipt-void-message{text-decoration:none}.pos-receipt td.component-total{text-align:right}.pos-receipt .receipt-hide{display:none}.pos-receipt .receipt-header,.pos-receipt .receipt-footer{text-align:center}.pos-receipt .receipt-header{margin-bottom:1em;padding-bottom:1em;border-bottom:1px dashed black}.pos-receipt .receipt-footer{margin-top:1em;padding-top:1em;margin-bottom:1em}.pos-receipt tr.line-item td,.pos-receipt tr.line-item-details td{border-bottom:1px solid #000}.pos-receipt tr.line-item.has-details td{border-bottom:0}.pos-receipt tr.last td{border-bottom:0}
+
+/*# sourceMappingURL=commerce_pos_receipt_print.css.map */
diff --git a/modules/receipt/css/commerce_pos_receipt_print.css.map b/modules/receipt/css/commerce_pos_receipt_print.css.map
new file mode 100644
index 0000000..04e15e8
--- /dev/null
+++ b/modules/receipt/css/commerce_pos_receipt_print.css.map
@@ -0,0 +1 @@
+{"version":3,"file":"commerce_pos_receipt_print.css","sources":["commerce_pos_receipt_print.scss"],"sourcesContent":["@page {\n  margin: 5mm;\n  size: 72mm 200mm;\n}\n\n.pos-receipt {\n  color: #000;\n  font-family: Arial, sans-serif;\n  font-size: 16px;\n  font-weight: bold;\n  text-transform: uppercase;\n}\n\n.pos-receipt .pos-order-info {\n  margin-bottom: 1em;\n  padding-bottom: 1em;\n  border-bottom: 1px dashed #000;\n}\n\n.pos-receipt .pos-order-info .transaction-type {\n}\n\n/* BASIC TABLE STYLES */\n.pos-receipt table {\n  width: 100%;\n  padding-bottom: 1em;\n  margin-bottom: 1em;\n  border-bottom: 1px dashed #000;\n}\n\n.pos-receipt td,\n\n.pos-receipt th {\n  text-align: left;\n  font-size: 16px;\n  font-weight: bold;\n  text-transform: uppercase;\n}\n\n.pos-receipt .payment-no-total td .payment-name,\n\n.pos-receipt .payment-no-total td.component-total {\n  text-decoration: line-through;\n}\n\n.pos-receipt .payment-no-total td span.commerce-pos-receipt-void-message {\n  text-decoration: none;\n}\n.pos-receipt .payment-status-void td span.commerce-pos-receipt-void-message {\n  text-decoration: none;\n}\n.pos-receipt td.component-total {\n  text-align: right;\n}\n.pos-receipt .receipt-hide {\n  display: none;\n}\n.pos-receipt .receipt-header,\n.pos-receipt .receipt-footer {\n  text-align: center;\n}\n.pos-receipt .receipt-header {\n  margin-bottom: 1em;\n  padding-bottom: 1em;\n  border-bottom: 1px dashed black;\n}\n.pos-receipt .receipt-footer {\n  margin-top: 1em;\n  padding-top: 1em;\n  margin-bottom: 1em;\n}\n.pos-receipt table.commerce-pos-order {\n  /*margin: 1em 0;*/\n}\n.pos-receipt tr.line-item td,\n.pos-receipt tr.line-item-details td {\n  border-bottom: 1px solid #000;\n}\n.pos-receipt tr.line-item.has-details td {\n  border-bottom: 0;\n}\n.pos-receipt tr.last td {\n  border-bottom: 0;\n}\n"],"mappings":"AAAA,KAAK,CACH,MAAM,CAAE,GAAI,CACZ,IAAI,CAAE,UAAW,CAGnB,AAAA,YAAY,AAAC,CACX,KAAK,CAAE,IAAK,CACZ,WAAW,CAAE,iBAAkB,CAC/B,SAAS,CAAE,IAAK,CAChB,WAAW,CAAE,IAAK,CAClB,cAAc,CAAE,SAAU,CAC3B,AAED,AAAa,YAAD,CAAC,eAAe,AAAC,CAC3B,aAAa,CAAE,GAAI,CACnB,cAAc,CAAE,GAAI,CACpB,aAAa,CAAE,eAAgB,CAChC,AAMD,AAAa,YAAD,CAAC,KAAK,AAAC,CACjB,KAAK,CAAE,IAAK,CACZ,cAAc,CAAE,GAAI,CACpB,aAAa,CAAE,GAAI,CACnB,aAAa,CAAE,eAAgB,CAChC,AAED,AAAa,YAAD,CAAC,EAAE,CAEf,AAAa,YAAD,CAAC,EAAE,AAAC,CACd,UAAU,CAAE,IAAK,CACjB,SAAS,CAAE,IAAK,CAChB,WAAW,CAAE,IAAK,CAClB,cAAc,CAAE,SAAU,CAC3B,AAED,AAAkC,YAAtB,CAAC,iBAAiB,CAAC,EAAE,CAAC,aAAa,CAE/C,AAAiC,YAArB,CAAC,iBAAiB,CAAC,EAAE,AAAA,gBAAgB,AAAC,CAChD,eAAe,CAAE,YAAa,CAC/B,AAED,AAAsC,YAA1B,CAAC,iBAAiB,CAAC,EAAE,CAAC,IAAI,AAAA,kCAAkC,AAAC,CACvE,eAAe,CAAE,IAAK,CACvB,AACD,AAAyC,YAA7B,CAAC,oBAAoB,CAAC,EAAE,CAAC,IAAI,AAAA,kCAAkC,AAAC,CAC1E,eAAe,CAAE,IAAK,CACvB,AACD,AAAe,YAAH,CAAC,EAAE,AAAA,gBAAgB,AAAC,CAC9B,UAAU,CAAE,KAAM,CACnB,AACD,AAAa,YAAD,CAAC,aAAa,AAAC,CACzB,OAAO,CAAE,IAAK,CACf,AACD,AAAa,YAAD,CAAC,eAAe,CAC5B,AAAa,YAAD,CAAC,eAAe,AAAC,CAC3B,UAAU,CAAE,MAAO,CACpB,AACD,AAAa,YAAD,CAAC,eAAe,AAAC,CAC3B,aAAa,CAAE,GAAI,CACnB,cAAc,CAAE,GAAI,CACpB,aAAa,CAAE,gBAAiB,CACjC,AACD,AAAa,YAAD,CAAC,eAAe,AAAC,CAC3B,UAAU,CAAE,GAAI,CAChB,WAAW,CAAE,GAAI,CACjB,aAAa,CAAE,GAAI,CACpB,AAID,AAA0B,YAAd,CAAC,EAAE,AAAA,UAAU,CAAC,EAAE,CAC5B,AAAkC,YAAtB,CAAC,EAAE,AAAA,kBAAkB,CAAC,EAAE,AAAC,CACnC,aAAa,CAAE,cAAe,CAC/B,AACD,AAAsC,YAA1B,CAAC,EAAE,AAAA,UAAU,AAAA,YAAY,CAAC,EAAE,AAAC,CACvC,aAAa,CAAE,CAAE,CAClB,AACD,AAAqB,YAAT,CAAC,EAAE,AAAA,KAAK,CAAC,EAAE,AAAC,CACtB,aAAa,CAAE,CAAE,CAClB","names":[]}
\ No newline at end of file
diff --git a/modules/receipt/js/commerce_pos_receipt.js b/modules/receipt/js/commerce_pos_receipt.js
new file mode 100644
index 0000000..c182ef5
--- /dev/null
+++ b/modules/receipt/js/commerce_pos_receipt.js
@@ -0,0 +1,61 @@
+(function ($, Drupal, drupalSettings) {
+
+  /**
+   * Ajax command to set the toolbar subtrees.
+   *
+   * @param {Drupal.Ajax} ajax
+   *   {@link Drupal.Ajax} object created by {@link Drupal.ajax}.
+   * @param {object} response
+   *   JSON response from the Ajax request.
+   * @param {number} [status]
+   *   XMLHttpRequest status.
+   */
+  Drupal.AjaxCommands.prototype.printReceipt = function (ajax, response, status) {
+    $(response.content).print({
+      globalStyles: false,
+      stylesheet: drupalSettings.commercePosReceipt.cssUrl,
+      deferred: $.Deferred().done(function(){
+        // Sloppy, should probaby be nice and scalable, but that might never be needed
+        $('.commerce-pos-receipt-button-done').click();
+      })
+    });
+  };
+
+  /**
+   * Catch the submit before it submits, so we can popup the print dialog first,
+   * then we trigger the submit for real once we've finished the print.
+   */
+  Drupal.behaviors.catchSubmits = {
+    attach: function (context, settings) {
+      $('.commerce-pos-receipt-button', context).click( function (e) {
+        // Normally you would use .once() here, but it applies at the end,
+        // and because of the chaining nature of our ajax, it doesn't fire until it is too late
+        // and we've clicked the button again and we're in a loop
+        $(this).addClass('commerce-pos-receipt-button-done');
+        $(this).removeClass('commerce-pos-receipt-button');
+        $(this).off();
+
+        e.preventDefault();
+
+        $.ajax(
+            {
+              url: "/admin/commerce/pos/" + drupalSettings.commercePosReceipt.orderId + '/ajax-receipt',
+              success: function (data) {
+                var ajaxObject = Drupal.ajax(
+                    {
+                      url: "",
+                      base: false,
+                      element: false,
+                      progress: false
+                    });
+
+                // Then, simulate an AJAX response having arrived, and let the
+                // Ajax system handle it.
+                ajaxObject.success(data, "success");
+              }
+            });
+      });
+    }
+  }
+
+}(jQuery, Drupal, drupalSettings));
diff --git a/modules/receipt/sass/commerce_pos_receipt.scss b/modules/receipt/sass/commerce_pos_receipt.scss
new file mode 100644
index 0000000..19c1a54
--- /dev/null
+++ b/modules/receipt/sass/commerce_pos_receipt.scss
@@ -0,0 +1,3 @@
+#commerce-pos-receipt {
+  display: none;
+}
diff --git a/modules/receipt/sass/commerce_pos_receipt_print.scss b/modules/receipt/sass/commerce_pos_receipt_print.scss
new file mode 100644
index 0000000..4b75325
--- /dev/null
+++ b/modules/receipt/sass/commerce_pos_receipt_print.scss
@@ -0,0 +1,84 @@
+@page {
+  margin: 5mm;
+  size: 72mm 200mm;
+}
+
+.pos-receipt {
+  color: #000;
+  font-family: Arial, sans-serif;
+  font-size: 16px;
+  font-weight: bold;
+  text-transform: uppercase;
+}
+
+.pos-receipt .pos-order-info {
+  margin-bottom: 1em;
+  padding-bottom: 1em;
+  border-bottom: 1px dashed #000;
+}
+
+.pos-receipt .pos-order-info .transaction-type {
+}
+
+/* BASIC TABLE STYLES */
+.pos-receipt table {
+  width: 100%;
+  padding-bottom: 1em;
+  margin-bottom: 1em;
+  border-bottom: 1px dashed #000;
+}
+
+.pos-receipt td,
+
+.pos-receipt th {
+  text-align: left;
+  font-size: 16px;
+  font-weight: bold;
+  text-transform: uppercase;
+}
+
+.pos-receipt .payment-no-total td .payment-name,
+
+.pos-receipt .payment-no-total td.component-total {
+  text-decoration: line-through;
+}
+
+.pos-receipt .payment-no-total td span.commerce-pos-receipt-void-message {
+  text-decoration: none;
+}
+.pos-receipt .payment-status-void td span.commerce-pos-receipt-void-message {
+  text-decoration: none;
+}
+.pos-receipt td.component-total {
+  text-align: right;
+}
+.pos-receipt .receipt-hide {
+  display: none;
+}
+.pos-receipt .receipt-header,
+.pos-receipt .receipt-footer {
+  text-align: center;
+}
+.pos-receipt .receipt-header {
+  margin-bottom: 1em;
+  padding-bottom: 1em;
+  border-bottom: 1px dashed black;
+}
+.pos-receipt .receipt-footer {
+  margin-top: 1em;
+  padding-top: 1em;
+  margin-bottom: 1em;
+}
+.pos-receipt table.commerce-pos-order {
+  /*margin: 1em 0;*/
+}
+.pos-receipt tr.line-item td,
+.pos-receipt tr.line-item-details td {
+  border-bottom: 1px solid #000;
+}
+.pos-receipt tr.line-item.has-details td {
+  border-bottom: 0;
+}
+.pos-receipt tr.last td {
+  border-bottom: 0;
+}
diff --git a/modules/receipt/src/Ajax/PrintReceiptCommand.php b/modules/receipt/src/Ajax/PrintReceiptCommand.php
new file mode 100644
index 0000000..b1b43e6
--- /dev/null
+++ b/modules/receipt/src/Ajax/PrintReceiptCommand.php
@@ -0,0 +1,34 @@
+<?php
+
+namespace Drupal\commerce_pos_receipt\Ajax;
+
+use Drupal\Core\Ajax\CommandInterface;
+
+/**
+ * AJAX command for retrieving data and printing a receipt.
+ */
+class PrintReceiptCommand implements CommandInterface {
+
+  protected $receipt;
+
+  /**
+   * Constructs a new PrintReceiptCommand object.
+   *
+   * @param string $receipt
+   *   ID of the wrapper.
+   */
+  public function __construct($receipt) {
+    $this->receipt = $receipt;
+  }
+
+  /**
+   * Return an array to be run through json_encode and sent to the client.
+   */
+  public function render() {
+    return [
+      'command' => 'printReceipt',
+      'content' => $this->receipt,
+    ];
+  }
+
+}
diff --git a/modules/receipt/src/Controller/PrintController.php b/modules/receipt/src/Controller/PrintController.php
new file mode 100644
index 0000000..c1b89c5
--- /dev/null
+++ b/modules/receipt/src/Controller/PrintController.php
@@ -0,0 +1,121 @@
+<?php
+
+namespace Drupal\commerce_pos_receipt\Controller;
+
+use Drupal\commerce_order\Entity\OrderInterface;
+use Drupal\commerce_pos_receipt\Ajax\PrintReceiptCommand;
+use Drupal\commerce_price\Entity\Currency;
+use Drupal\Core\Ajax\AjaxResponse;
+use Drupal\Core\Ajax\HtmlCommand;
+use Drupal\Core\Ajax\SettingsCommand;
+use Drupal\Core\Controller\ControllerBase;
+use Drupal\Core\Url;
+
+/**
+ * Class PrintController.
+ */
+class PrintController extends ControllerBase {
+
+  /**
+   * A controller callback.
+   */
+  public function ajaxReceipt(OrderInterface $commerce_order) {
+    $renderer = \Drupal::service('renderer');
+
+    $build = $this->showReceipt($commerce_order);
+    unset($build['#receipt']['print']);
+    $module_handler = \Drupal::service('module_handler');
+    $module_path = $module_handler->getModule('commerce_pos_receipt')->getPath();
+
+    $response = new AjaxResponse();
+
+    // TODO: could this be turned into 1 command, and if so, is that better?
+    $response->addCommand(new HtmlCommand('#commerce-pos-receipt', $renderer->render($build)));
+    $response->addCommand(new SettingsCommand([
+      'commercePosReceipt' => [
+        'cssUrl' => Url::fromUri('base:' . $module_path . '/css/commerce_pos_receipt_print.css', ['absolute' => TRUE])->toString(),
+      ],
+    ], TRUE));
+    $response->addCommand(new PrintReceiptCommand('#commerce-pos-receipt'));
+
+    return $response;
+  }
+
+  /**
+   * A controller callback.
+   */
+  public function showReceipt(OrderInterface $commerce_order) {
+
+    $number_formatter_factory = \Drupal::service('commerce_price.number_formatter_factory');
+    $number_formatter = $number_formatter_factory->createInstance();
+
+    $sub_total_price = $commerce_order->getSubtotalPrice();
+    $currency = Currency::load($sub_total_price->getCurrencyCode());
+    $formatted_amount = $number_formatter->formatCurrency($sub_total_price->getNumber(), $currency);
+
+    //In the future add a setting to display group or individual for same skus
+    $items = $commerce_order->getItems();
+    foreach ($items as $item) {
+      $totals[] = [
+        $item->getTitle() . ' (' . $item->getQuantity() . ')',
+        $number_formatter->formatCurrency($item->getAdjustedTotalPrice()->getNumber(), $currency),
+      ];
+    }
+
+    $totals[] = ['Subtotal', $formatted_amount];
+
+    // Commerce appears to have a bug where if no adjustments exist, it will
+    // return a 0 => null array, which will still trigger a foreach loop.
+    foreach ($commerce_order->collectAdjustments() as $key => $adjustment) {
+      if (!empty($adjustment)) {
+        $amount = $adjustment->getAmount();
+        $currency = Currency::load($amount->getCurrencyCode());
+        $formatted_amount = $number_formatter->formatCurrency($amount->getNumber(), $currency);
+
+        $totals[] = [
+          $adjustment->getLabel(),
+          $formatted_amount,
+        ];
+      }
+    }
+
+    // Collecting the total price on the cart.
+    $total_price = $commerce_order->getTotalPrice();
+    $formatted_amount = $number_formatter->formatCurrency($total_price->getNumber(), $currency);
+    $totals[] = ['Total', $formatted_amount];
+
+    $payment_storage = \Drupal::entityTypeManager()->getStorage('commerce_payment');
+    $payments = $payment_storage->loadMultipleByOrder($commerce_order);
+    foreach ($payments as $payment) {
+      $totals[] = ['Payment', $payment->getState()->getLabel()];
+    }
+    $ajax_url = URL::fromRoute('commerce_pos_receipt.ajax', ['commerce_order' => $commerce_order->id()], [
+      'attributes' => [
+        'class' => ['use-ajax', 'button'],
+      ],
+    ]);
+
+    $config = \Drupal::config('commerce_pos_receipt.settings');
+    $build = ['#theme' => 'commerce_pos_receipt'];
+    $build['#receipt'] = [
+      'header' => [
+        '#markup' => check_markup($config->get('header'), $config->get('header_format')),
+      ],
+      'body' => [
+        '#type' => 'table',
+        '#rows' => $totals,
+      ],
+      'footer' => [
+        '#markup' => check_markup($config->get('footer'), $config->get('footer_format')),
+      ],
+      'print' => [
+        '#title' => t('Print receipt'),
+        '#prefix' => '<div id="commerce-pos-receipt"></div>',
+        '#type' => 'link',
+        '#url' => $ajax_url,
+      ],
+    ];
+    return $build;
+  }
+
+}
diff --git a/modules/receipt/src/Form/ReceiptSettingsForm.php b/modules/receipt/src/Form/ReceiptSettingsForm.php
new file mode 100644
index 0000000..9a8e467
--- /dev/null
+++ b/modules/receipt/src/Form/ReceiptSettingsForm.php
@@ -0,0 +1,78 @@
+<?php
+
+namespace Drupal\commerce_pos_receipt\Form;
+
+use Drupal\Core\Form\ConfigFormBase;
+use Drupal\Core\Form\FormStateInterface;
+
+/**
+ * Define a Configuration form for persisting header/footer.
+ */
+class ReceiptSettingsForm extends ConfigFormBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state) {
+
+    $header = $this->config('commerce_pos_receipt.settings')->get('header');
+    $footer = $this->config('commerce_pos_receipt.settings')->get('footer');
+
+    $form['commerce_pos_receipt_header'] = [
+      '#type' => 'text_format',
+      '#title' => t('Header text'),
+      '#description' => t('This text will appear at the top of printed receipts.'),
+      '#default_value' => $header,
+      '#format' => NULL,
+    ];
+
+    $form['commerce_pos_receipt_footer'] = [
+      '#type' => 'text_format',
+      '#title' => t('Footer text'),
+      '#description' => t('This text will appear at the bottom of printed receipts.'),
+      '#default_value' => $footer,
+      '#format' => NULL,
+    ];
+
+    return parent::buildForm($form, $form_state);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    parent::submitForm($form, $form_state);
+
+    $values = $form_state->getValues();
+    $this->config('commerce_pos_receipt.settings')
+      ->set('header', $values['commerce_pos_receipt_header']['value'])
+      ->set('footer', $values['commerce_pos_receipt_footer']['value'])
+      ->set('header_format', $values['commerce_pos_receipt_header']['format'])
+      ->set('footer_format', $values['commerce_pos_receipt_footer']['format'])
+      ->save();
+  }
+
+  /**
+   * Gets the configuration names that will be editable.
+   *
+   * @return array
+   *   An array of configuration object names that are editable if called in
+   *   conjunction with the trait's config() method.
+   */
+  protected function getEditableConfigNames() {
+    return [
+      'commerce_pos_receipt.settings',
+    ];
+  }
+
+  /**
+   * Returns a unique string identifying the form.
+   *
+   * @return string
+   *   The unique string identifying the form.
+   */
+  public function getFormId() {
+    return 'commerce_pos_receipt_settings';
+  }
+
+}
diff --git a/modules/receipt/templates/commerce-pos-receipt.html.twig b/modules/receipt/templates/commerce-pos-receipt.html.twig
new file mode 100644
index 0000000..dd3eae8
--- /dev/null
+++ b/modules/receipt/templates/commerce-pos-receipt.html.twig
@@ -0,0 +1,20 @@
+{#
+/**
+* @file
+* Receipt document.
+*
+* Available variables:
+* - receipt: The receipt.
+*
+* @ingroup themeable
+*/
+#}
+
+{{ attach_library('commerce_pos_receipt/receipt') }}
+{{ attach_library('commerce_pos_receipt/jQuery.print') }}
+<div class="commerce-pos-receipt">
+    <div class="receipt-print">{{ receipt.print }}</div>
+    <div class="receipt-header">{{ receipt.header }}</div>
+    <div class="receipt-body">{{ receipt.body }}</div>
+    <div class="receipt-footer">{{ receipt.footer }}</div>
+</div>
diff --git a/modules/receipt/tests/src/Functional/LoadTest.php b/modules/receipt/tests/src/Functional/LoadTest.php
new file mode 100644
index 0000000..9e6fe69
--- /dev/null
+++ b/modules/receipt/tests/src/Functional/LoadTest.php
@@ -0,0 +1,46 @@
+<?php
+
+namespace Drupal\Tests\commerce_pos_receipt\Functional;
+
+use Drupal\Core\Url;
+use Drupal\Tests\BrowserTestBase;
+
+/**
+ * Simple test to ensure that main page loads with module enabled.
+ *
+ * @group commerce_pos_receipt
+ */
+class LoadTest extends BrowserTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = ['commerce_pos_receipt'];
+
+  /**
+   * A user with permission to administer site configuration.
+   *
+   * @var \Drupal\user\UserInterface
+   */
+  protected $user;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+    $this->user = $this->drupalCreateUser(['administer site configuration']);
+    $this->drupalLogin($this->user);
+  }
+
+  /**
+   * Tests that the home page loads with a 200 response.
+   */
+  public function testLoad() {
+    $this->drupalGet(Url::fromRoute('<front>'));
+    $this->assertResponse(200);
+  }
+
+}
diff --git a/src/Form/POSForm.php b/src/Form/POSForm.php
index a1af2a5..61264e1 100644
--- a/src/Form/POSForm.php
+++ b/src/Form/POSForm.php
@@ -2,9 +2,13 @@
 
 namespace Drupal\commerce_pos\Form;
 
+use Drupal\commerce_pos_receipt\Ajax\PrintReceiptCommand;
+use Drupal\commerce_pos_receipt\Controller\PrintController;
 use Drupal\commerce_price\Price;
 use Drupal\commerce_store\CurrentStore;
 use Drupal\Component\Datetime\TimeInterface;
+use Drupal\Core\Ajax\AjaxResponse;
+use Drupal\Core\Ajax\HtmlCommand;
 use Drupal\Core\Entity\ContentEntityForm;
 use Drupal\Core\Entity\EntityManagerInterface;
 use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
@@ -91,6 +95,10 @@ class POSForm extends ContentEntityForm {
    * Build the POS Order Form.
    */
   protected function buildOrderForm(array $form, FormStateInterface $form_state) {
+    /* @var \Drupal\commerce_order\Entity\Order $order */
+    $order = $this->entity;
+    $form_state->set('commerce_pos_order_id', $order->id());
+
     $form = parent::buildForm($form, $form_state);
 
     $form['customer'] = [
@@ -104,7 +112,7 @@ class POSForm extends ContentEntityForm {
       '#type' => 'container',
     ];
 
-    $form['actions']['submit']['#value'] = t('Add Payment');
+    $form['actions']['submit']['#value'] = t('Payments and Completion');
     // Ensure the user is redirected back to this page after deleting an order.
     if (isset($form['actions']['delete']['#url']) && $form['actions']['delete']['#url'] instanceof Url) {
       $form['actions']['delete']['#url']->mergeOptions([
@@ -129,6 +137,11 @@ class POSForm extends ContentEntityForm {
     $form['#suffix'] = '</div>';
     $form['#validate'][] = '::validatePaymentForm';
 
+    $form['order_id'] = [
+      '#type' => 'value',
+      '#value' => $order->id(),
+    ];
+
     $form['payment_gateway'] = [
       '#type' => 'container',
       '#tree' => TRUE,
@@ -207,7 +220,7 @@ class POSForm extends ContentEntityForm {
 
       $form['keypad']['add'] = [
         '#type' => 'submit',
-        '#value' => t('Add'),
+        '#value' => t('Add Payment'),
         '#name' => 'commerce-pos-pay-keypad-add',
         '#submit' => ['::submitForm'],
         '#payment_gateway_id' => $option_id,
@@ -215,23 +228,30 @@ class POSForm extends ContentEntityForm {
       ];
     }
 
-    $form['actions']['back'] = [
-      '#type' => 'submit',
-      '#value' => t('Back To Order'),
-      '#name' => 'commerce-pos-back-to-order',
-      '#submit' => ['::submitForm'],
-      '#element_key' => 'back-to-order',
-    ];
-
     $form['actions']['finish'] = [
       '#type' => 'submit',
-      '#value' => t('Finish'),
+      '#value' => t('Complete Order'),
       '#disabled' => !$balance_paid,
       '#name' => 'commerce-pos-finish',
       '#submit' => ['::submitForm'],
+      '#attributes' => [
+        'class' => ['use-ajax-submit'],
+      ],
+      '#ajax' => [
+        'callback' => [get_class($this), 'ajaxReceipt'],
+        'wrapper' => 'commerce-pos-receipt',
+      ],
       '#element_key' => 'finish-order',
     ];
 
+    $form['actions']['back'] = [
+      '#type' => 'submit',
+      '#value' => t('Back To Order'),
+      '#name' => 'commerce-pos-back-to-order',
+      '#submit' => ['::submitForm'],
+      '#element_key' => 'back-to-order',
+    ];
+
     return $form;
   }
 
@@ -290,6 +310,9 @@ class POSForm extends ContentEntityForm {
     if ($step == 'payment') {
       if ($triggering_element['#element_key'] == 'add-payment') {
         $this->submitPayment($form, $form_state);
+        // Save the payment, in case we leave and go to another screen. Missing a payment would be bad
+        // also helps if we're loading it somewhere else, like for the receipt trickyness.
+        $this->entity->save();
       }
       elseif ($triggering_element['#element_key'] == 'back-to-order') {
         $form_state->set('step', 'order');
@@ -314,11 +337,14 @@ class POSForm extends ContentEntityForm {
     $triggering_element = $form_state->getTriggeringElement();
     $store = $this->entity->getStore();
     $default_currency = $store->getDefaultCurrency();
+
+    // Right now all the payment methods are manual, we'll have to change this up
+    // once we want to support integrated payment methods
     $payment_gateway = $triggering_element['#payment_gateway_id'];
     $values = [
       'payment_gateway' => $payment_gateway,
       'order_id' => $this->entity->id(),
-      'state' => 'pending',
+      'state' => 'completed',
       'amount' => [
         'number' => $form_state->getValue('keypad')['amount'],
         'currency_code' => $default_currency->getCurrencyCode(),
@@ -378,8 +404,8 @@ class POSForm extends ContentEntityForm {
 
     $totals[] = [$this->t('Subtotal'), $formatted_amount];
 
-    // Commerce appears to have a bug where if not adjustments exist, it will return a
-    // 0 => null array, which will still trigger a foreach loop.
+    // Commerce appears to have a bug where if not adjustments exist, it
+    // will return a 0 => null array, which will still trigger a foreach loop.
     foreach ($order->collectAdjustments() as $key => $adjustment) {
       if (!empty($adjustment)) {
         $amount = $adjustment->getAmount();
@@ -516,4 +542,24 @@ class POSForm extends ContentEntityForm {
 
   }
 
+  /**
+   * AJAX callback for the finish button.
+   */
+  public function ajaxReceipt($form, $form_state) {
+    $renderer = \Drupal::service('renderer');
+    $callbackObject = $form_state->getBuildInfo()['callback_object'];
+    $commerce_order = $callbackObject->entity;
+
+    $printController = new PrintController();
+    $build = $printController->showReceipt($commerce_order);
+    unset($build['#receipt']['print']);
+    $response = new AjaxResponse();
+
+    // TODO: could this be turned into 1 command, and if so, is that better?
+    $response->addCommand(new HtmlCommand('#commerce-pos-receipt', $renderer->render($build)));
+    $response->addCommand(new PrintReceiptCommand('#commerce-pos-receipt'));
+
+    return $response;
+  }
+
 }
diff --git a/src/Plugin/Field/FieldWidget/PosOrderItemWidget.php b/src/Plugin/Field/FieldWidget/PosOrderItemWidget.php
index f9f2052..d991088 100644
--- a/src/Plugin/Field/FieldWidget/PosOrderItemWidget.php
+++ b/src/Plugin/Field/FieldWidget/PosOrderItemWidget.php
@@ -432,7 +432,9 @@ class PosOrderItemWidget extends WidgetBase implements WidgetInterface, Containe
    */
   protected function addOrderItem(FieldItemListInterface $items, array &$form, FormStateInterface &$form_state) {
     // Loading the product variation object.
-    $product_variation = ProductVariation::load($form_state->getValue(['order_items', 'target_id', 'product_selector']));
+    $product_variation = ProductVariation::load($form_state->getValue([
+      'order_items', 'target_id', 'product_selector',
+    ]));
     // If we've not loaded a product variation then exit doing nothing.
     if (!$product_variation) {
       // There's nothing to do.
diff --git a/tests/src/FunctionalJavascript/PosFormTest.php b/tests/src/FunctionalJavascript/PosFormTest.php
index ff8315a..3a71712 100644
--- a/tests/src/FunctionalJavascript/PosFormTest.php
+++ b/tests/src/FunctionalJavascript/PosFormTest.php
@@ -23,6 +23,7 @@ class PosFormTest extends JavascriptTestBase {
    */
   public static $modules = [
     'commerce_pos',
+    'commerce_pos_receipt',
   ];
 
   /**
@@ -42,20 +43,34 @@ class PosFormTest extends JavascriptTestBase {
     $register->save();
 
     $variations = [
-      $this->createProductionVariation(['title' => 'T-shirt XL', 'price' => new Price("23.20", 'USD')]),
+      $this->createProductionVariation([
+        'title' => 'T-shirt XL',
+        'price' => new Price("23.20", 'USD'),
+      ]),
       $this->createProductionVariation(['title' => 'T-shirt L']),
       $this->createProductionVariation(['title' => 'T-shirt M']),
     ];
 
-    $this->createProduct(['variations' => $variations, 'title' => 'T-shirt', 'stores' => [$test_store]]);
+    $this->createProduct([
+      'variations' => $variations,
+      'title' => 'T-shirt',
+      'stores' => [$test_store],
+    ]);
 
     $variations = [
-      $this->createProductionVariation(['title' => 'Jumper XL', 'price' => new Price("50", 'USD')]),
+      $this->createProductionVariation([
+        'title' => 'Jumper XL',
+        'price' => new Price("50", 'USD'),
+      ]),
       $this->createProductionVariation(['title' => 'Jumper L']),
       $this->createProductionVariation(['title' => 'Jumper M']),
     ];
 
-    $this->createProduct(['variations' => $variations, 'title' => 'Jumper', 'stores' => [$test_store]]);
+    $this->createProduct([
+      'variations' => $variations,
+      'title' => 'Jumper',
+      'stores' => [$test_store],
+    ]);
 
     // @todo work out the expected permissions to view products etc...
     $this->drupalLogin($this->rootUser);
@@ -141,7 +156,6 @@ class PosFormTest extends JavascriptTestBase {
     $web_assert->fieldValueEquals('order_items[target_id][order_items][0][unit_price][number]', '40.50');
     // (3 * 40.5) + (1 * 23.20)
     $web_assert->pageTextContains('To Pay $144.70');
-
     // Click on the buttons to remove all the jumpers.
     $this->getSession()->getPage()->findButton('remove_order_item_1')->click();
     $web_assert->assertWaitOnAjaxRequest();
@@ -212,6 +226,7 @@ class PosFormTest extends JavascriptTestBase {
     // Clicking finish will bring us back to the order item screen - processing
     // a new order.
     $this->click('input[name="commerce-pos-finish"]');
+    $this->waitForAjaxToFinish();
     $web_assert->pageTextContains('Total $0.00');
     $web_assert->pageTextNotContains('Cash');
     $web_assert->pageTextNotContains('To Pay');
@@ -248,4 +263,12 @@ class PosFormTest extends JavascriptTestBase {
     $this->assertCount(1, $results);
   }
 
+  /**
+   * Waits for jQuery to become active and animations to complete.
+   */
+  protected function waitForAjaxToFinish() {
+    $condition = "(0 === jQuery.active && 0 === jQuery(':animated').length)";
+    $this->assertJsCondition($condition, 10000);
+  }
+
 }
diff --git a/tests/src/Kernel/UPCTest.php b/tests/src/Kernel/UPCTest.php
index 6d5d4e9..c58e47f 100644
--- a/tests/src/Kernel/UPCTest.php
+++ b/tests/src/Kernel/UPCTest.php
@@ -70,7 +70,8 @@ class UPCTest extends CommerceKernelTestBase {
       $this->assertEquals($variation->get('field_upc')->getValue()[0]['value'], '12345');
     }
 
-    // Check that if we try and load a upc that doesn't exist we don't get anything.
+    // Check that if we try and load a upc that doesn't exist
+    // we don't get anything.
     $variations = $upc->lookup('77777');
     $this->assertEmpty($variations);
   }
