From bdd5470e68b4e843e9e2886b537295ed11298434 Mon Sep 17 00:00:00 2001
From: Oliver <oliver.henry.walters@gmail.com>
Date: Sun, 12 Nov 2023 23:13:22 +1100
Subject: [PATCH] Sales order support (#438)

* Add new models for SalesOrder

- Create generic Order and OrderLine models with common functionality

* Refactor

- Move some widgets around
- Cleanup directory structure

* Add link to home screen and nav drawer

* Add SalesOrder list widget

* Linting fixes

* Fix string

* Refactor PurchaseOrderDetailWidget

* Tweaks to existing code

* linting

* Fixes for drawer widget

* Add "detail" page for SalesOrder

* Add more tiles to SalesOrder detail

* Allow editing of salesorder

* add list filters for sales orders

* Display list of line items

* Customer updates

- Display customer icon on home screen
- Fetch sales orders for customer detail page

* Cleanup company detail view

* Create new sales order from list

* Stricter typing for formFields method

* Create new PurchaseOrder and SalesOrder from company deatil

* Status code updates

- Add function for name comparison
- Remove hard-coded values

* Update view permission checks for home widget

* Add ability to manually add SalesOrderLineItem

* Add nice progress bar widgets

* Display detail view for sales order line item

* edit SalesOrderLineItem

* Fix unused import

* Hide "shipped items" tab

- Will be added in a future update
---
 lib/api.dart                                  |  17 +
 lib/barcode/barcode.dart                      |  10 +-
 lib/generated/i18n.dart                       |   6 +-
 lib/inventree/company.dart                    |   6 +-
 lib/inventree/model.dart                      |   2 +-
 lib/inventree/orders.dart                     | 107 +++++++
 lib/inventree/part.dart                       |  10 +-
 lib/inventree/project_code.dart               |   2 +-
 lib/inventree/purchase_order.dart             | 127 ++------
 lib/inventree/sales_order.dart                | 190 +++++++++++
 lib/inventree/status_codes.dart               |  18 ++
 lib/inventree/stock.dart                      |  12 +-
 lib/l10n/app_en.arb                           |  44 ++-
 lib/preferences.dart                          |   1 +
 lib/settings/home_settings.dart               |  18 +-
 lib/widget/{ => company}/company_detail.dart  | 128 +++++++-
 lib/widget/{ => company}/company_list.dart    |   2 +-
 .../{ => company}/supplier_part_detail.dart   |   4 +-
 .../{ => company}/supplier_part_list.dart     |   2 +-
 lib/widget/drawer.dart                        |  82 +++--
 lib/widget/home.dart                          |  86 +++--
 lib/widget/{ => order}/po_line_detail.dart    |  29 +-
 lib/widget/{ => order}/po_line_list.dart      |   2 +-
 .../{ => order}/purchase_order_detail.dart    | 101 +++---
 .../{ => order}/purchase_order_list.dart      |  22 +-
 lib/widget/order/sales_order_detail.dart      | 303 ++++++++++++++++++
 lib/widget/order/sales_order_list.dart        | 176 ++++++++++
 lib/widget/order/so_line_detail.dart          | 164 ++++++++++
 lib/widget/order/so_line_list.dart            |  86 +++++
 lib/widget/{ => part}/bom_list.dart           |   2 +-
 lib/widget/{ => part}/category_display.dart   |   6 +-
 lib/widget/{ => part}/category_list.dart      |   2 +-
 lib/widget/{ => part}/part_detail.dart        |  18 +-
 lib/widget/{ => part}/part_image_widget.dart  |   0
 lib/widget/{ => part}/part_list.dart          |   2 +-
 .../{ => part}/part_parameter_widget.dart     |   0
 lib/widget/{ => part}/part_suppliers.dart     |   2 +-
 lib/widget/progress.dart                      |  28 ++
 lib/widget/search.dart                        |  12 +-
 lib/widget/{ => stock}/location_display.dart  |   6 +-
 lib/widget/{ => stock}/location_list.dart     |   2 +-
 lib/widget/{ => stock}/stock_detail.dart      |  10 +-
 .../{ => stock}/stock_item_history.dart       |   0
 .../{ => stock}/stock_item_test_results.dart  |   0
 lib/widget/{ => stock}/stock_list.dart        |   2 +-
 45 files changed, 1565 insertions(+), 284 deletions(-)
 create mode 100644 lib/inventree/orders.dart
 create mode 100644 lib/inventree/sales_order.dart
 rename lib/widget/{ => company}/company_detail.dart (64%)
 rename lib/widget/{ => company}/company_list.dart (97%)
 rename lib/widget/{ => company}/supplier_part_detail.dart (98%)
 rename lib/widget/{ => company}/supplier_part_list.dart (97%)
 rename lib/widget/{ => order}/po_line_detail.dart (89%)
 rename lib/widget/{ => order}/po_line_list.dart (97%)
 rename lib/widget/{ => order}/purchase_order_detail.dart (74%)
 rename lib/widget/{ => order}/purchase_order_list.dart (90%)
 create mode 100644 lib/widget/order/sales_order_detail.dart
 create mode 100644 lib/widget/order/sales_order_list.dart
 create mode 100644 lib/widget/order/so_line_detail.dart
 create mode 100644 lib/widget/order/so_line_list.dart
 rename lib/widget/{ => part}/bom_list.dart (98%)
 rename lib/widget/{ => part}/category_display.dart (97%)
 rename lib/widget/{ => part}/category_list.dart (97%)
 rename lib/widget/{ => part}/part_detail.dart (97%)
 rename lib/widget/{ => part}/part_image_widget.dart (100%)
 rename lib/widget/{ => part}/part_list.dart (98%)
 rename lib/widget/{ => part}/part_parameter_widget.dart (100%)
 rename lib/widget/{ => part}/part_suppliers.dart (96%)
 rename lib/widget/{ => stock}/location_display.dart (98%)
 rename lib/widget/{ => stock}/location_list.dart (97%)
 rename lib/widget/{ => stock}/stock_detail.dart (98%)
 rename lib/widget/{ => stock}/stock_item_history.dart (100%)
 rename lib/widget/{ => stock}/stock_item_test_results.dart (100%)
 rename lib/widget/{ => stock}/stock_list.dart (98%)

diff --git a/lib/api.dart b/lib/api.dart
index b8a2d7b1..28f85f39 100644
--- a/lib/api.dart
+++ b/lib/api.dart
@@ -620,6 +620,8 @@ class InvenTreeAPI {
     _globalSettings.clear();
     _userSettings.clear();
 
+    roles.clear();
+    _plugins.clear();
     serverInfo.clear();
     _connectionStatusChanged();
   }
@@ -672,6 +674,8 @@ class InvenTreeAPI {
 
     _connectionStatusChanged();
 
+    fetchStatusCodeData();
+
     return _connected;
   }
 
@@ -735,6 +739,10 @@ class InvenTreeAPI {
    */
   bool checkPermission(String role, String permission) {
 
+    if (!_connected) {
+      return false;
+    }
+
     // If we do not have enough information, assume permission is allowed
     if (roles.isEmpty) {
       debug("checkPermission - no roles defined!");
@@ -1624,11 +1632,20 @@ class InvenTreeAPI {
   InvenTreeStatusCode get StockHistoryStatus => _get_status_class("stock/track/status/");
   InvenTreeStatusCode get StockStatus => _get_status_class("stock/status/");
   InvenTreeStatusCode get PurchaseOrderStatus => _get_status_class("order/po/status/");
+  InvenTreeStatusCode get SalesOrderStatus => _get_status_class("order/so/status/");
 
   void clearStatusCodeData() {
     StockHistoryStatus.data.clear();
     StockStatus.data.clear();
     PurchaseOrderStatus.data.clear();
+    SalesOrderStatus.data.clear();
+  }
+
+  Future<void> fetchStatusCodeData({bool forceReload = true}) async {
+    StockHistoryStatus.load(forceReload: forceReload);
+    StockStatus.load(forceReload: forceReload);
+    PurchaseOrderStatus.load(forceReload: forceReload);
+    SalesOrderStatus.load(forceReload: forceReload);
   }
 
   int notification_counter = 0;
diff --git a/lib/barcode/barcode.dart b/lib/barcode/barcode.dart
index df110e6f..4ea044e1 100644
--- a/lib/barcode/barcode.dart
+++ b/lib/barcode/barcode.dart
@@ -23,13 +23,13 @@ import "package:inventree/inventree/purchase_order.dart";
 import "package:inventree/inventree/stock.dart";
 
 import "package:inventree/widget/dialogs.dart";
-import "package:inventree/widget/location_display.dart";
-import "package:inventree/widget/part_detail.dart";
-import "package:inventree/widget/purchase_order_detail.dart";
+import "package:inventree/widget/stock/location_display.dart";
+import "package:inventree/widget/part/part_detail.dart";
+import "package:inventree/widget/order/purchase_order_detail.dart";
 import "package:inventree/widget/refreshable_state.dart";
 import "package:inventree/widget/snacks.dart";
-import "package:inventree/widget/stock_detail.dart";
-import "package:inventree/widget/supplier_part_detail.dart";
+import "package:inventree/widget/stock/stock_detail.dart";
+import "package:inventree/widget/company/supplier_part_detail.dart";
 
 
 /*
diff --git a/lib/generated/i18n.dart b/lib/generated/i18n.dart
index 09177b85..ad002d5a 100644
--- a/lib/generated/i18n.dart
+++ b/lib/generated/i18n.dart
@@ -1,8 +1,8 @@
 
-import 'dart:async';
+import "dart:async';
 
-import 'package:flutter/foundation.dart';
-import 'package:flutter/material.dart';
+import "package:flutter/foundation.dart';
+import "package:flutter/material.dart';
 // ignore_for_file: non_constant_identifier_names
 // ignore_for_file: camel_case_types
 // ignore_for_file: prefer_single_quotes
diff --git a/lib/inventree/company.dart b/lib/inventree/company.dart
index 032ee41e..4ebbc987 100644
--- a/lib/inventree/company.dart
+++ b/lib/inventree/company.dart
@@ -22,7 +22,7 @@ class InvenTreeCompany extends InvenTreeModel {
   List<String> get rolesRequired => ["purchase_order", "sales_order", "return_order"];
 
   @override
-  Map<String, dynamic> formFields() {
+  Map<String, Map<String, dynamic>> formFields() {
     return {
       "name": {},
       "description": {},
@@ -121,8 +121,8 @@ class InvenTreeSupplierPart extends InvenTreeModel {
   List<String> get rolesRequired => ["part", "purchase_order"];
 
   @override
-  Map<String, dynamic> formFields() {
-    Map<String, dynamic> fields = {
+  Map<String, Map<String, dynamic>> formFields() {
+    Map<String, Map<String, dynamic>> fields = {
       "supplier": {},
       "SKU": {},
       "link": {},
diff --git a/lib/inventree/model.dart b/lib/inventree/model.dart
index abb7ec6b..fd4c29e5 100644
--- a/lib/inventree/model.dart
+++ b/lib/inventree/model.dart
@@ -230,7 +230,7 @@ class InvenTreeModel {
 
   // Fields for editing / creating this model
   // Override per-model
-  Map<String, dynamic> formFields() {
+  Map<String, Map<String, dynamic>> formFields() {
 
     return {};
   }
diff --git a/lib/inventree/orders.dart b/lib/inventree/orders.dart
new file mode 100644
index 00000000..0595b2d8
--- /dev/null
+++ b/lib/inventree/orders.dart
@@ -0,0 +1,107 @@
+/*
+ * Base model for various "orders" which share common properties
+ */
+
+
+import "package:inventree/inventree/model.dart";
+import "package:inventree/inventree/part.dart";
+
+
+/*
+ * Generic class representing an "order"
+ */
+class InvenTreeOrder extends InvenTreeModel {
+
+  InvenTreeOrder() : super();
+
+  InvenTreeOrder.fromJson(Map<String, dynamic> json) : super.fromJson(json);
+
+  String get issueDate => getString("issue_date");
+
+  String get completeDate => getString("complete_date");
+
+  String get creationDate => getString("creation_date");
+
+  String get targetDate => getString("target_date");
+
+  int get lineItemCount => getInt("line_items", backup: 0);
+
+  bool get overdue => getBool("overdue");
+
+  String get reference => getString("reference");
+
+  int get responsibleId => getInt("responsible");
+
+  // Project code information
+  int get projectCodeId => getInt("project_code");
+
+  String get projectCode => getString("code", subKey: "project_code_detail");
+
+  String get projectCodeDescription => getString("description", subKey: "project_code_detail");
+
+  bool get hasProjectCode => projectCode.isNotEmpty;
+
+  int get status => getInt("status");
+
+  String get statusText => getString("status_text");
+
+  double? get totalPrice {
+    String price = getString("total_price");
+
+    if (price.isEmpty) {
+      return null;
+    } else {
+      return double.tryParse(price);
+    }
+  }
+
+  // Return the currency for this order
+  // Note that the nomenclature in the API changed at some point
+  String get totalPriceCurrency {
+    if (jsondata.containsKey("order_currency")) {
+      return getString("order_currency");
+    } else if (jsondata.containsKey("total_price_currency")) {
+      return getString("total_price_currency");
+    } else {
+      return "";
+    }
+  }
+}
+
+
+/*
+ * Generic class representing an "order line"
+ */
+class InvenTreeOrderLine extends InvenTreeModel {
+
+  InvenTreeOrderLine() : super();
+
+  InvenTreeOrderLine.fromJson(Map<String, dynamic> json) : super.fromJson(json);
+
+  bool get overdue => getBool("overdue");
+
+  double get quantity => getDouble("quantity");
+
+  String get reference => getString("reference");
+
+  int get orderId => getInt("order");
+
+  InvenTreePart? get part {
+    dynamic part_detail = jsondata["part_detail"];
+
+    if (part_detail == null) {
+      return null;
+    } else {
+      return InvenTreePart.fromJson(part_detail as Map<String, dynamic>);
+    }
+  }
+
+  int get partId => getInt("pk", subKey: "part_detail");
+
+  String get partName => getString("name", subKey: "part_detail");
+
+  String get partImage => getString("thumbnail", subKey: "part_detail");
+
+  // TODO: Perhaps parse this as an actual date?
+  String get targetDate => getString("target_date");
+}
\ No newline at end of file
diff --git a/lib/inventree/part.dart b/lib/inventree/part.dart
index e335a119..e44494e1 100644
--- a/lib/inventree/part.dart
+++ b/lib/inventree/part.dart
@@ -27,9 +27,9 @@ class InvenTreePartCategory extends InvenTreeModel {
   List<String> get rolesRequired => ["part_category"];
 
   @override
-  Map<String, dynamic> formFields() {
+  Map<String, Map<String, dynamic>> formFields() {
 
-    Map<String, dynamic> fields = {
+    Map<String, Map<String, dynamic>> fields = {
       "name": {},
       "description": {},
       "parent": {},
@@ -140,9 +140,9 @@ class InvenTreePartParameter extends InvenTreeModel {
   InvenTreeModel createFromJson(Map<String, dynamic> json) => InvenTreePartParameter.fromJson(json);
 
   @override
-  Map<String, dynamic> formFields() {
+  Map<String, Map<String, dynamic>> formFields() {
 
-    Map<String, dynamic> fields = {
+    Map<String, Map<String, dynamic>> fields = {
       "header": {
         "type": "string",
         "read_only": true,
@@ -200,7 +200,7 @@ class InvenTreePart extends InvenTreeModel {
   List<String> get rolesRequired => ["part"];
 
   @override
-  Map<String, dynamic> formFields() {
+  Map<String, Map<String, dynamic>> formFields() {
     return {
       "name": {},
       "description": {},
diff --git a/lib/inventree/project_code.dart b/lib/inventree/project_code.dart
index 8e2c75b6..07a0c19d 100644
--- a/lib/inventree/project_code.dart
+++ b/lib/inventree/project_code.dart
@@ -17,7 +17,7 @@ class InvenTreeProjectCode extends InvenTreeModel {
   String get URL => "project-code/";
 
   @override
-  Map<String, dynamic> formFields() {
+  Map<String, Map<String, dynamic>> formFields() {
     return {
       "code": {},
       "description": {},
diff --git a/lib/inventree/purchase_order.dart b/lib/inventree/purchase_order.dart
index defb9f8a..80708948 100644
--- a/lib/inventree/purchase_order.dart
+++ b/lib/inventree/purchase_order.dart
@@ -1,22 +1,22 @@
 import "package:inventree/api.dart";
 import "package:inventree/helpers.dart";
 import "package:inventree/inventree/company.dart";
-import "package:inventree/inventree/part.dart";
 import "package:inventree/inventree/model.dart";
+import "package:inventree/inventree/orders.dart";
 
-const int PO_STATUS_PENDING = 10;
-const int PO_STATUS_PLACED = 20;
-const int PO_STATUS_COMPLETE = 30;
-const int PO_STATUS_CANCELLED = 40;
-const int PO_STATUS_LOST = 50;
-const int PO_STATUS_RETURNED = 60;
 
-class InvenTreePurchaseOrder extends InvenTreeModel {
+/*
+ * Class representing an individual PurchaseOrder instance
+ */
+class InvenTreePurchaseOrder extends InvenTreeOrder {
 
   InvenTreePurchaseOrder() : super();
 
   InvenTreePurchaseOrder.fromJson(Map<String, dynamic> json) : super.fromJson(json);
 
+  @override
+  InvenTreeModel createFromJson(Map<String, dynamic> json) => InvenTreePurchaseOrder.fromJson(json);
+
   @override
   String get URL => "order/po/";
 
@@ -26,8 +26,8 @@ class InvenTreePurchaseOrder extends InvenTreeModel {
   String get receive_url => "${url}receive/";
 
   @override
-  Map<String, dynamic> formFields() {
-    var fields = {
+  Map<String, Map<String, dynamic>> formFields() {
+    Map<String, Map<String, dynamic>> fields = {
       "reference": {},
       "supplier": {
         "filters": {
@@ -69,33 +69,8 @@ class InvenTreePurchaseOrder extends InvenTreeModel {
     };
   }
 
-  String get issueDate => getString("issue_date");
-
-  String get completeDate => getString("complete_date");
-
-  String get creationDate => getString("creation_date");
-
-  String get targetDate => getString("target_date");
-
-  int get lineItemCount => getInt("line_items", backup: 0);
-  
-  bool get overdue => getBool("overdue");
-
-  String get reference => getString("reference");
-
-  int get responsibleId => getInt("responsible");
-
   int get supplierId => getInt("supplier");
 
-  // Project code information
-  int get projectCodeId => getInt("project_code");
-
-  String get projectCode => getString("code", subKey: "project_code_detail");
-
-  String get projectCodeDescription => getString("description", subKey: "project_code_detail");
-
-  bool get hasProjectCode => projectCode.isNotEmpty;
-
   InvenTreeCompany? get supplier {
 
     dynamic supplier_detail = jsondata["supplier_detail"];
@@ -109,39 +84,13 @@ class InvenTreePurchaseOrder extends InvenTreeModel {
 
   String get supplierReference => getString("supplier_reference");
 
-  int get status => getInt("status");
+  bool get isOpen => api.PurchaseOrderStatus.isNameIn(status, ["PENDING", "PLACED"]);
 
-  String get statusText => getString("status_text");
+  bool get isPending => api.PurchaseOrderStatus.isNameIn(status, ["PENDING"]);
 
-  bool get isOpen => status == PO_STATUS_PENDING || status == PO_STATUS_PLACED;
+  bool get isPlaced => api.PurchaseOrderStatus.isNameIn(status, ["PLACED"]);
 
-  bool get isPending => status == PO_STATUS_PENDING;
-
-  bool get isPlaced => status == PO_STATUS_PLACED;
-
-  bool get isFailed => status == PO_STATUS_CANCELLED || status == PO_STATUS_LOST || status == PO_STATUS_RETURNED;
-
-  double? get totalPrice {
-    String price = getString("total_price");
-
-    if (price.isEmpty) {
-      return null;
-    } else {
-      return double.tryParse(price);
-    }
-  }
-
-  // Return the currency for this order
-  // Note that the nomenclature in the API changed at some point
-  String get totalPriceCurrency {
-    if (jsondata.containsKey("order_currency")) {
-      return getString("order_currency");
-    } else if (jsondata.containsKey("total_price_currency")) {
-      return getString("total_price_currency");
-    } else {
-      return "";
-    }
-  }
+  bool get isFailed => api.PurchaseOrderStatus.isNameIn(status, ["CANCELLED", "LOST", "RETURNED"]);
 
   Future<List<InvenTreePOLineItem>> getLineItems() async {
 
@@ -162,9 +111,6 @@ class InvenTreePurchaseOrder extends InvenTreeModel {
     return items;
   }
 
-  @override
-  InvenTreeModel createFromJson(Map<String, dynamic> json) => InvenTreePurchaseOrder.fromJson(json);
-
   /// Mark this order as "placed" / "issued"
   Future<void> issueOrder() async {
     // Order can only be placed when the order is 'pending'
@@ -185,12 +131,15 @@ class InvenTreePurchaseOrder extends InvenTreeModel {
   }
 }
 
-class InvenTreePOLineItem extends InvenTreeModel {
+class InvenTreePOLineItem extends InvenTreeOrderLine {
 
   InvenTreePOLineItem() : super();
 
   InvenTreePOLineItem.fromJson(Map<String, dynamic> json) : super.fromJson(json);
 
+  @override
+  InvenTreeModel createFromJson(Map<String, dynamic> json) => InvenTreePOLineItem.fromJson(json);
+
   @override
   String get URL => "order/po-line/";
 
@@ -198,7 +147,7 @@ class InvenTreePOLineItem extends InvenTreeModel {
   List<String> get rolesRequired => ["purchase_order"];
 
   @override
-  Map<String, dynamic> formFields() {
+  Map<String, Map<String, dynamic>> formFields() {
     return {
       "part": {
         // We cannot edit the supplier part field here
@@ -232,38 +181,24 @@ class InvenTreePOLineItem extends InvenTreeModel {
     };
   }
 
+  double get received => getDouble("received");
+
   bool get isComplete => received >= quantity;
 
-  double get quantity => getDouble("quantity");
+  double get progressRatio {
+    if (quantity <= 0 || received <= 0) {
+      return 0;
+    }
 
-  double get received => getDouble("received");
+    return received / quantity;
+  }
 
   String get progressString => simpleNumberString(received) + " / " + simpleNumberString(quantity);
 
   double get outstanding => quantity - received;
 
-  String get reference => getString("reference");
-
-  int get orderId => getInt("order");
-
   int get supplierPartId => getInt("part");
 
-  InvenTreePart? get part {
-    dynamic part_detail = jsondata["part_detail"];
-
-    if (part_detail == null) {
-      return null;
-    } else {
-      return InvenTreePart.fromJson(part_detail as Map<String, dynamic>);
-    }
-  }
-
-  int get partId => getInt("pk", subKey: "part_detail");
-  
-  String get partName => getString("name", subKey: "part_detail");
-
-  String get partImage => getString("thumbnail", subKey: "part_detail");
-
   InvenTreeSupplierPart? get supplierPart {
 
     dynamic detail = jsondata["supplier_part_detail"];
@@ -281,19 +216,13 @@ class InvenTreePOLineItem extends InvenTreeModel {
   
   String get purchasePriceCurrency => getString("purchase_price_currency");
 
-  String get purchasePriceString => getString("purchase_price_string");
-
   int get destination => getInt("destination");
 
   Map<String, dynamic> get destinationDetail => getMap("destination_detail");
-  
-  @override
-  InvenTreeModel createFromJson(Map<String, dynamic> json) => InvenTreePOLineItem.fromJson(json);
-
 }
 
 /*
- * Class representing an attachment file against a StockItem object
+ * Class representing an attachment file against a PurchaseOrder object
  */
 class InvenTreePurchaseOrderAttachment extends InvenTreeAttachment {
 
diff --git a/lib/inventree/sales_order.dart b/lib/inventree/sales_order.dart
new file mode 100644
index 00000000..1225cb86
--- /dev/null
+++ b/lib/inventree/sales_order.dart
@@ -0,0 +1,190 @@
+
+
+import "package:inventree/helpers.dart";
+import "package:inventree/inventree/company.dart";
+import "package:inventree/inventree/model.dart";
+import "package:inventree/inventree/orders.dart";
+
+import "package:inventree/api.dart";
+
+
+/*
+ * Class representing an individual SalesOrder
+ */
+class InvenTreeSalesOrder extends InvenTreeOrder {
+
+  InvenTreeSalesOrder() : super();
+
+  InvenTreeSalesOrder.fromJson(Map<String, dynamic> json) : super.fromJson(json);
+
+  @override
+  InvenTreeModel createFromJson(Map<String, dynamic> json) => InvenTreeSalesOrder.fromJson(json);
+
+  @override
+  String get URL => "order/so/";
+
+  @override
+  List<String> get rolesRequired => ["sales_order"];
+
+  @override
+  Map<String, Map<String, dynamic>> formFields() {
+    Map<String, Map<String, dynamic>> fields = {
+      "reference": {},
+      "customer": {
+        "filters": {
+          "is_customer": true,
+        }
+      },
+      "customer_reference": {},
+      "description": {},
+      "project_code": {},
+      "target_date": {},
+      "link": {},
+      "responsible": {},
+      "contact": {
+        "filters": {
+          "company": customerId,
+        }
+      }
+    };
+
+    if (!InvenTreeAPI().supportsProjectCodes) {
+      fields.remove("project_code");
+    }
+
+    if (!InvenTreeAPI().supportsContactModel) {
+      fields.remove("contact");
+    }
+
+    return fields;
+  }
+
+  @override
+  Map<String, String> defaultGetFilters() {
+    return {
+      "customer_detail": "true",
+    };
+  }
+
+  @override
+  Map<String, String> defaultListFilters() {
+    return {
+      "customer_detail": "true",
+    };
+  }
+
+  int get customerId => getInt("customer");
+
+  InvenTreeCompany? get customer {
+    dynamic customer_detail = jsondata["customer_detail"];
+
+    if (customer_detail == null) {
+      return null;
+    } else {
+      return InvenTreeCompany.fromJson(customer_detail as Map<String, dynamic>);
+    }
+  }
+
+  String get customerReference => getString("customer_reference");
+
+  bool get isOpen => api.SalesOrderStatus.isNameIn(status, ["PENDING", "IN_PROGRESS"]);
+
+  bool get isComplete => api.SalesOrderStatus.isNameIn(status, ["SHIPPED"]);
+
+}
+
+
+/*
+ * Class representing an individual line item in a SalesOrder
+ */
+class InvenTreeSOLineItem extends InvenTreeOrderLine {
+
+  InvenTreeSOLineItem() : super();
+
+  InvenTreeSOLineItem.fromJson(Map<String, dynamic> json) : super.fromJson(json);
+
+  @override
+  InvenTreeModel createFromJson(Map<String, dynamic> json) => InvenTreeSOLineItem.fromJson(json);
+
+  @override
+  String get URL => "order/so-line/";
+
+  @override
+  List<String> get rolesRequired => ["sales_order"];
+
+  @override
+  Map<String, Map<String, dynamic>> formFields() {
+    return {
+      "order": {
+        "hidden": true,
+      },
+      "part": {},
+      "quantity": {},
+      "reference": {},
+      "notes": {},
+      "link": {},
+    };
+  }
+
+  @override
+  Map<String, String> defaultGetFilters() {
+    return {
+      "part_detail": "true",
+    };
+  }
+
+  @override
+  Map<String, String> defaultListFilters() {
+    return {
+      "part_detail": "true",
+    };
+  }
+
+  double get allocated => getDouble("allocated");
+
+  bool get isAllocated => allocated >= quantity;
+
+  double get shipped => getDouble("shipped");
+
+  double get outstanding => quantity - shipped;
+
+  double get progressRatio {
+    if (quantity <= 0 || shipped <= 0) {
+      return 0;
+    }
+
+    return shipped / quantity;
+  }
+
+  String get progressString => simpleNumberString(shipped) + " / " + simpleNumberString(quantity);
+
+  bool get isComplete => shipped >= quantity;
+
+  double get available => getDouble("available_stock") + getDouble("available_variant_stock");
+
+  double get salePrice => getDouble("sale_price");
+
+  String get salePriceCurrency => getString("sale_price_currency");
+
+}
+
+
+/*
+ * Class representing an attachment file against a SalesOrder object
+ */
+class InvenTreeSalesOrderAttachment extends InvenTreeAttachment {
+
+  InvenTreeSalesOrderAttachment() : super();
+
+  InvenTreeSalesOrderAttachment.fromJson(Map<String, dynamic> json) : super.fromJson(json);
+
+  @override
+  InvenTreeModel createFromJson(Map<String, dynamic> json) => InvenTreeSalesOrderAttachment.fromJson(json);
+
+  @override
+  String get REFERENCE_FIELD => "order";
+
+  @override
+  String get URL => "order/po/attachment/";
+
+}
diff --git a/lib/inventree/status_codes.dart b/lib/inventree/status_codes.dart
index 8edb4ea0..9b75280d 100644
--- a/lib/inventree/status_codes.dart
+++ b/lib/inventree/status_codes.dart
@@ -105,6 +105,24 @@ class InvenTreeStatusCode {
     }
   }
 
+  // Return the 'name' (untranslated) associated with a given status code
+  String name(int status) {
+    Map<String, dynamic> _entry = entry(status);
+
+    String _name = (_entry["name"] ?? "") as String;
+
+    if (_name.isEmpty) {
+      debug("No match for status code ${status} at '${URL}'");
+    }
+
+    return _name;
+  }
+
+  // Test if the name associated with the given code is in the provided list
+  bool isNameIn(int code, List<String> names) {
+    return names.contains(name(code));
+  }
+
   // Return the 'color' associated with a given status code
   Color color(int status) {
     Map<String, dynamic> _entry = entry(status);
diff --git a/lib/inventree/stock.dart b/lib/inventree/stock.dart
index 22113d4e..4f455fe8 100644
--- a/lib/inventree/stock.dart
+++ b/lib/inventree/stock.dart
@@ -27,7 +27,7 @@ class InvenTreeStockItemTestResult extends InvenTreeModel {
   List<String> get rolesRequired => ["stock"];
 
   @override
-  Map<String, dynamic> formFields() {
+  Map<String, Map<String, dynamic>> formFields() {
     return {
       "stock_item": {"hidden": true},
       "test": {},
@@ -158,8 +158,8 @@ class InvenTreeStockItem extends InvenTreeModel {
   String get WEB_URL => "stock/item/";
 
   @override
-  Map<String, dynamic> formFields() {
-    return {
+  Map<String, Map<String, dynamic>> formFields() {
+    Map<String, Map<String, dynamic>> fields = {
       "part": {},
       "location": {},
       "quantity": {},
@@ -175,6 +175,8 @@ class InvenTreeStockItem extends InvenTreeModel {
       "packaging": {},
       "link": {},
     };
+
+    return fields;
   }
 
   @override
@@ -609,8 +611,8 @@ class InvenTreeStockLocation extends InvenTreeModel {
   String get pathstring => getString("pathstring");
 
   @override
-  Map<String, dynamic> formFields() {
-    Map<String, dynamic> fields = {
+  Map<String, Map<String, dynamic>> formFields() {
+    Map<String, Map<String, dynamic>> fields = {
       "name": {},
       "description": {},
       "parent": {},
diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb
index e1c1f2ea..fb8f91c8 100644
--- a/lib/l10n/app_en.arb
+++ b/lib/l10n/app_en.arb
@@ -235,9 +235,15 @@
   "credits": "Credits",
   "@credits": {},
 
+  "customer": "Customer",
+  "@customer": {},
+
   "customers": "Customers",
   "@customers": {},
 
+  "customerReference": "Customer Reference",
+  "@customerReference": {},
+
   "damaged": "Damaged",
   "@damaged": {},
 
@@ -440,15 +446,21 @@
   "homeShowPo":  "Show Purchase Orders",
   "@homeShowPo": {},
 
+  "homeShowPoDescription": "Show purchase order button on home screen",
+  "@homeShowPoDescription": {},
+
+  "homeShowSo": "Show Sales Orders",
+  "@homeShowSo": {},
+
+  "homeShowSoDescription": "Show sales order button on home screen",
+  "@homeShowSoDescription": {},
+
   "homeShowSubscribed": "Subscribed Parts",
   "@homeShowSubscribed": {},
 
   "homeShowSubscribedDescription": "Show subscribed parts on home screen",
   "@homeShowSubscsribedDescription": {},
 
-  "homeShowPoDescription": "Show purchase order button on home screen",
-  "@homeShowPoDescription": {},
-
   "homeShowSuppliers": "Show Suppliers",
   "@homeShowSuppliers": {},
 
@@ -576,6 +588,9 @@
   "level": "Level",
   "@level": {},
 
+  "lineItemAdd": "Add Line Item",
+  "@lineItemAdd": {},
+
   "lineItem": "Line Item",
   "@lineItem": {},
 
@@ -687,9 +702,15 @@
   "outstanding": "Outstanding",
   "@outstanding": {},
 
-  "outstandingOrderDetail": "Show outstanding items",
+  "outstandingOrderDetail": "Show outstanding orders",
   "@outstandingOrderDetail": {},
 
+  "overdue": "Overdue",
+  "@overdue": {},
+
+  "overdueDetail": "Show overdue orders",
+  "@overdueDetail": {},
+
   "packaging": "Packaging",
   "@packaging": {},
 
@@ -997,9 +1018,21 @@
   "returned": "Returned",
   "@returned": {},
 
+  "salesOrder": "Sales Order",
+  "@salesOrder": {},
+
   "salesOrders": "Sales Orders",
   "@salesOrders": {},
 
+  "salesOrderCreate": "New Sales Order",
+  "@saleOrderCreate": {},
+
+  "salesOrderEdit": "Edit Sales Order",
+  "@salesOrderEdit": {},
+
+  "salesOrderUpdated": "Sales order updated",
+  "@salesOrderUpdated": {},
+
   "save": "Save",
   "@save": {
     "description": "Save"
@@ -1125,6 +1158,9 @@
   "serverNotSelected": "Server not selected",
   "@serverNotSelected": {},
 
+  "shipped": "Shipped",
+  "@shipped": {},
+
   "sku": "SKU",
   "@sku": {},
 
diff --git a/lib/preferences.dart b/lib/preferences.dart
index 30105978..581a5fd8 100644
--- a/lib/preferences.dart
+++ b/lib/preferences.dart
@@ -11,6 +11,7 @@ import "package:path/path.dart";
 // Settings key values
 const String INV_HOME_SHOW_SUBSCRIBED = "homeShowSubscribed";
 const String INV_HOME_SHOW_PO = "homeShowPo";
+const String INV_HOME_SHOW_SO = "homeShowSo";
 const String INV_HOME_SHOW_MANUFACTURERS = "homeShowManufacturers";
 const String INV_HOME_SHOW_CUSTOMERS = "homeShowCustomers";
 const String INV_HOME_SHOW_SUPPLIERS = "homeShowSuppliers";
diff --git a/lib/settings/home_settings.dart b/lib/settings/home_settings.dart
index 926f31db..c1bee1f0 100644
--- a/lib/settings/home_settings.dart
+++ b/lib/settings/home_settings.dart
@@ -21,6 +21,7 @@ class _HomeScreenSettingsState extends State<HomeScreenSettingsWidget> {
   // Home screen settings
   bool homeShowSubscribed = true;
   bool homeShowPo = true;
+  bool homeShowSo = true;
   bool homeShowSuppliers = true;
   bool homeShowManufacturers = true;
   bool homeShowCustomers = true;
@@ -38,6 +39,7 @@ class _HomeScreenSettingsState extends State<HomeScreenSettingsWidget> {
 
     homeShowSubscribed = await InvenTreeSettingsManager().getValue(INV_HOME_SHOW_SUBSCRIBED, true) as bool;
     homeShowPo = await InvenTreeSettingsManager().getValue(INV_HOME_SHOW_PO, true) as bool;
+    homeShowSo = await InvenTreeSettingsManager().getValue(INV_HOME_SHOW_SO, true) as bool;
     homeShowManufacturers = await InvenTreeSettingsManager().getValue(INV_HOME_SHOW_MANUFACTURERS, true) as bool;
     homeShowCustomers = await InvenTreeSettingsManager().getValue(INV_HOME_SHOW_CUSTOMERS, true) as bool;
     homeShowSuppliers = await InvenTreeSettingsManager().getValue(INV_HOME_SHOW_SUPPLIERS, true) as bool;
@@ -85,6 +87,20 @@ class _HomeScreenSettingsState extends State<HomeScreenSettingsWidget> {
                       },
                     ),
                   ),
+                  ListTile(
+                    title: Text(L10().homeShowSo),
+                    subtitle: Text(L10().homeShowSoDescription),
+                    leading: FaIcon(FontAwesomeIcons.truck),
+                    trailing: Switch(
+                      value: homeShowSo,
+                      onChanged: (bool value) {
+                        InvenTreeSettingsManager().setValue(INV_HOME_SHOW_SO, value);
+                        setState(() {
+                          homeShowSo = value;
+                        });
+                      },
+                    ),
+                  ),
                   ListTile(
                     title: Text(L10().homeShowSuppliers),
                     subtitle: Text(L10().homeShowSuppliersDescription),
@@ -116,6 +132,7 @@ class _HomeScreenSettingsState extends State<HomeScreenSettingsWidget> {
                       },
                     ),
                   ),
+                  */
                   ListTile(
                     title: Text(L10().homeShowCustomers),
                     subtitle: Text(L10().homeShowCustomersDescription),
@@ -130,7 +147,6 @@ class _HomeScreenSettingsState extends State<HomeScreenSettingsWidget> {
                       },
                     ),
                   ),
-                   */
                 ]
             )
         )
diff --git a/lib/widget/company_detail.dart b/lib/widget/company/company_detail.dart
similarity index 64%
rename from lib/widget/company_detail.dart
rename to lib/widget/company/company_detail.dart
index 15684a16..bf83ee4d 100644
--- a/lib/widget/company_detail.dart
+++ b/lib/widget/company/company_detail.dart
@@ -9,12 +9,16 @@ import "package:inventree/helpers.dart";
 
 import "package:inventree/inventree/company.dart";
 import "package:inventree/inventree/purchase_order.dart";
+import "package:inventree/inventree/sales_order.dart";
 
 import "package:inventree/widget/attachment_widget.dart";
-import "package:inventree/widget/purchase_order_list.dart";
+import "package:inventree/widget/order/purchase_order_list.dart";
+import "package:inventree/widget/order/sales_order_list.dart";
 import "package:inventree/widget/refreshable_state.dart";
 import "package:inventree/widget/snacks.dart";
-import "package:inventree/widget/supplier_part_list.dart";
+import "package:inventree/widget/company/supplier_part_list.dart";
+import "package:inventree/widget/order/sales_order_detail.dart";
+import "package:inventree/widget/order/purchase_order_detail.dart";
 
 
 /*
@@ -36,10 +40,11 @@ class _CompanyDetailState extends RefreshableState<CompanyDetailWidget> {
 
   _CompanyDetailState();
 
-  List<InvenTreePurchaseOrder> outstandingOrders = [];
-  
   int supplierPartCount = 0;
 
+  int outstandingPurchaseOrders = 0;
+  int outstandingSalesOrders = 0;
+
   int attachmentCount = 0;
 
   @override
@@ -68,11 +73,87 @@ class _CompanyDetailState extends RefreshableState<CompanyDetailWidget> {
   List<SpeedDialChild> actionButtons(BuildContext context) {
     List<SpeedDialChild> actions = [];
 
-    // TODO - Actions for this company
+    if (widget.company.isCustomer && InvenTreeSalesOrder().canCreate) {
+      actions.add(SpeedDialChild(
+        child: FaIcon(FontAwesomeIcons.truck),
+        label: L10().salesOrderCreate,
+        onTap: () async {
+          _createSalesOrder(context);
+        }
+      ));
+    }
+
+    if (widget.company.isSupplier && InvenTreePurchaseOrder().canCreate) {
+      actions.add(SpeedDialChild(
+        child: FaIcon(FontAwesomeIcons.cartShopping),
+        label: L10().purchaseOrderCreate,
+        onTap: () async {
+          _createPurchaseOrder(context);
+        }
+      ));
+    }
 
     return actions;
   }
 
+  Future<void> _createSalesOrder(BuildContext context) async {
+    var fields = InvenTreeSalesOrder().formFields();
+
+    // Cannot set contact until company is locked in
+    fields.remove("contact");
+
+    fields["customer"]?["value"] = widget.company.pk;
+
+    InvenTreeSalesOrder().createForm(
+        context,
+        L10().salesOrderCreate,
+        fields: fields,
+        onSuccess: (result) async {
+          Map<String, dynamic> data = result as Map<String, dynamic>;
+
+          if (data.containsKey("pk")) {
+            var order = InvenTreeSalesOrder.fromJson(data);
+
+            Navigator.push(
+                context,
+                MaterialPageRoute(
+                    builder: (context) => SalesOrderDetailWidget(order)
+                )
+            );
+          }
+        }
+    );
+  }
+
+  Future<void> _createPurchaseOrder(BuildContext context) async {
+    var fields = InvenTreePurchaseOrder().formFields();
+
+    // Cannot set contact until company is locked in
+    fields.remove("contact");
+
+    fields["supplier"]?["value"] = widget.company.pk;
+
+    InvenTreePurchaseOrder().createForm(
+        context,
+        L10().purchaseOrderCreate,
+        fields: fields,
+        onSuccess: (result) async {
+          Map<String, dynamic> data = result as Map<String, dynamic>;
+
+          if (data.containsKey("pk")) {
+            var order = InvenTreePurchaseOrder.fromJson(data);
+
+            Navigator.push(
+                context,
+                MaterialPageRoute(
+                    builder: (context) => PurchaseOrderDetailWidget(order)
+                )
+            );
+          }
+        }
+    );
+  }
+
   @override
   Future<void> request(BuildContext context) async {
     final bool result = await widget.company.reload();
@@ -83,11 +164,18 @@ class _CompanyDetailState extends RefreshableState<CompanyDetailWidget> {
       return;
     }
 
-    if (widget.company.isSupplier) {
-      outstandingOrders =
-      await widget.company.getPurchaseOrders(outstanding: true);
-    }
+    outstandingPurchaseOrders = widget.company.isSupplier ?
+        await InvenTreePurchaseOrder().count(filters: {
+          "supplier": widget.company.pk.toString(),
+          "outstanding": "true"
+        }) : 0;
 
+    outstandingSalesOrders = widget.company.isCustomer ?
+        await InvenTreeSalesOrder().count(filters: {
+          "customer": widget.company.pk.toString(),
+          "outstanding": "true"
+        }) : 0;
+  
     InvenTreeSupplierPart().count(
         filters: {
           "supplier": widget.company.pk.toString()
@@ -224,7 +312,7 @@ class _CompanyDetailState extends RefreshableState<CompanyDetailWidget> {
         ListTile(
           title: Text(L10().purchaseOrders),
           leading: FaIcon(FontAwesomeIcons.cartShopping, color: COLOR_ACTION),
-          trailing: Text("${outstandingOrders.length}"),
+          trailing: Text("${outstandingPurchaseOrders}"),
           onTap: () {
             Navigator.push(
               context,
@@ -257,7 +345,25 @@ class _CompanyDetailState extends RefreshableState<CompanyDetailWidget> {
     }
 
     if (widget.company.isCustomer) {
-      // TODO - Add list of sales orders
+      tiles.add(
+        ListTile(
+          title: Text(L10().salesOrders),
+          leading: FaIcon(FontAwesomeIcons.truck, color: COLOR_ACTION),
+          trailing: Text("${outstandingSalesOrders}"),
+          onTap: () {
+            Navigator.push(
+              context,
+              MaterialPageRoute(
+                builder: (context) => SalesOrderListWidget(
+                  filters: {
+                    "customer": widget.company.pk.toString()
+                  }
+                )
+              )
+            );
+          }
+        )
+      );
     }
 
     if (widget.company.notes.isNotEmpty) {
diff --git a/lib/widget/company_list.dart b/lib/widget/company/company_list.dart
similarity index 97%
rename from lib/widget/company_list.dart
rename to lib/widget/company/company_list.dart
index a19762c2..b68dffd0 100644
--- a/lib/widget/company_list.dart
+++ b/lib/widget/company/company_list.dart
@@ -8,7 +8,7 @@ import "package:inventree/inventree/model.dart";
 
 import "package:inventree/widget/paginator.dart";
 import "package:inventree/widget/refreshable_state.dart";
-import "package:inventree/widget/company_detail.dart";
+import "package:inventree/widget/company/company_detail.dart";
 
 
 /*
diff --git a/lib/widget/supplier_part_detail.dart b/lib/widget/company/supplier_part_detail.dart
similarity index 98%
rename from lib/widget/supplier_part_detail.dart
rename to lib/widget/company/supplier_part_detail.dart
index fe39c803..6ca43cb2 100644
--- a/lib/widget/supplier_part_detail.dart
+++ b/lib/widget/company/supplier_part_detail.dart
@@ -10,8 +10,8 @@ import "package:inventree/l10.dart";
 import "package:inventree/inventree/part.dart";
 import "package:inventree/inventree/company.dart";
 
-import "package:inventree/widget/company_detail.dart";
-import "package:inventree/widget/part_detail.dart";
+import "package:inventree/widget/company/company_detail.dart";
+import "package:inventree/widget/part/part_detail.dart";
 import "package:inventree/widget/progress.dart";
 import "package:inventree/widget/refreshable_state.dart";
 import "package:inventree/widget/snacks.dart";
diff --git a/lib/widget/supplier_part_list.dart b/lib/widget/company/supplier_part_list.dart
similarity index 97%
rename from lib/widget/supplier_part_list.dart
rename to lib/widget/company/supplier_part_list.dart
index 3674f66c..5f412274 100644
--- a/lib/widget/supplier_part_list.dart
+++ b/lib/widget/company/supplier_part_list.dart
@@ -8,7 +8,7 @@ import "package:inventree/inventree/model.dart";
 
 import "package:inventree/widget/paginator.dart";
 import "package:inventree/widget/refreshable_state.dart";
-import "package:inventree/widget/supplier_part_detail.dart";
+import "package:inventree/widget/company/supplier_part_detail.dart";
 
 
 /*
diff --git a/lib/widget/drawer.dart b/lib/widget/drawer.dart
index 2ca8d88e..f38ad337 100644
--- a/lib/widget/drawer.dart
+++ b/lib/widget/drawer.dart
@@ -3,15 +3,17 @@ import "package:font_awesome_flutter/font_awesome_flutter.dart";
 
 import "package:inventree/api.dart";
 import "package:inventree/app_colors.dart";
-import "package:inventree/inventree/company.dart";
+import "package:inventree/inventree/part.dart";
 import "package:inventree/inventree/purchase_order.dart";
+import "package:inventree/inventree/sales_order.dart";
 import "package:inventree/inventree/stock.dart";
 import "package:inventree/l10.dart";
 import "package:inventree/settings/settings.dart";
-import "package:inventree/widget/category_display.dart";
+import "package:inventree/widget/order/sales_order_list.dart";
+import "package:inventree/widget/part/category_display.dart";
 import "package:inventree/widget/notifications.dart";
-import "package:inventree/widget/purchase_order_list.dart";
-import "package:inventree/widget/location_display.dart";
+import "package:inventree/widget/order/purchase_order_list.dart";
+import "package:inventree/widget/stock/location_display.dart";
 
 
 /*
@@ -28,6 +30,10 @@ class InvenTreeDrawer extends StatelessWidget {
     Navigator.of(context).pop();
   }
 
+  bool _checkConnection() {
+    return InvenTreeAPI().checkConnection();
+  }
+
   /*
    * Return to the 'home' screen.
    * This will empty the navigation stack.
@@ -43,37 +49,63 @@ class InvenTreeDrawer extends StatelessWidget {
   // Load "parts" page
   void _parts() {
     _closeDrawer();
-    Navigator.push(
-      context,
-      MaterialPageRoute(builder: (context) => CategoryDisplayWidget(null))
-    );
+
+    if (_checkConnection()) {
+      Navigator.push(
+          context,
+          MaterialPageRoute(builder: (context) => CategoryDisplayWidget(null))
+      );
+    }
   }
 
   // Load "stock" page
   void _stock() {
     _closeDrawer();
-    Navigator.push(
-      context,
-      MaterialPageRoute(builder: (context) => LocationDisplayWidget(null))
-    );
+
+    if (_checkConnection()) {
+      Navigator.push(
+          context,
+          MaterialPageRoute(builder: (context) => LocationDisplayWidget(null))
+      );
+    }
+  }
+
+  // Load "sales orders" page
+  void _salesOrders() {
+    _closeDrawer();
+
+    if (_checkConnection()) {
+      Navigator.push(
+          context,
+          MaterialPageRoute(
+              builder: (context) => SalesOrderListWidget(filters: {})
+          )
+      );
+    }
   }
   
   // Load "purchase orders" page
   void _purchaseOrders() {
     _closeDrawer();
 
-    Navigator.push(
-        context,
-        MaterialPageRoute(
-            builder: (context) => PurchaseOrderListWidget(filters: {})
-        )
-    );
+    if (_checkConnection()) {
+      Navigator.push(
+          context,
+          MaterialPageRoute(
+              builder: (context) => PurchaseOrderListWidget(filters: {})
+          )
+      );
+    }
   }
 
   // Load notifications screen
   void _notifications() {
     _closeDrawer();
-    Navigator.push(context, MaterialPageRoute(builder: (context) => NotificationWidget()));
+
+    if (_checkConnection()) {
+      Navigator.push(context,
+          MaterialPageRoute(builder: (context) => NotificationWidget()));
+    }
   }
 
   // Load settings widget
@@ -98,7 +130,7 @@ class InvenTreeDrawer extends StatelessWidget {
 
     tiles.add(Divider());
 
-    if (InvenTreeCompany().canView) {
+    if (InvenTreePart().canView) {
       tiles.add(
         ListTile(
           title: Text(L10().parts),
@@ -128,6 +160,16 @@ class InvenTreeDrawer extends StatelessWidget {
       );
     }
 
+    if (InvenTreeSalesOrder().canView) {
+      tiles.add(
+        ListTile(
+          title: Text(L10().salesOrders),
+          leading: FaIcon(FontAwesomeIcons.truck, color: COLOR_ACTION),
+          onTap: _salesOrders,
+        )
+      );
+    }
+
     if (tiles.length > 2) {
       tiles.add(Divider());
     }
diff --git a/lib/widget/home.dart b/lib/widget/home.dart
index b62daf77..e9034a7b 100644
--- a/lib/widget/home.dart
+++ b/lib/widget/home.dart
@@ -6,20 +6,25 @@ import "package:font_awesome_flutter/font_awesome_flutter.dart";
 
 import "package:inventree/api.dart";
 import "package:inventree/app_colors.dart";
+import "package:inventree/inventree/part.dart";
+import "package:inventree/inventree/purchase_order.dart";
+import "package:inventree/inventree/sales_order.dart";
+import "package:inventree/inventree/stock.dart";
 import "package:inventree/preferences.dart";
 import "package:inventree/l10.dart";
 import "package:inventree/settings/select_server.dart";
 import "package:inventree/user_profile.dart";
 
-import "package:inventree/widget/category_display.dart";
+import "package:inventree/widget/part/category_display.dart";
 import "package:inventree/widget/drawer.dart";
-import "package:inventree/widget/location_display.dart";
-import "package:inventree/widget/part_list.dart";
-import "package:inventree/widget/purchase_order_list.dart";
+import "package:inventree/widget/stock/location_display.dart";
+import "package:inventree/widget/part/part_list.dart";
+import "package:inventree/widget/order/purchase_order_list.dart";
+import "package:inventree/widget/order/sales_order_list.dart";
 import "package:inventree/widget/refreshable_state.dart";
 import "package:inventree/widget/snacks.dart";
 import "package:inventree/widget/spinner.dart";
-import "package:inventree/widget/company_list.dart";
+import "package:inventree/widget/company/company_list.dart";
 
 
 class InvenTreeHomePage extends StatefulWidget {
@@ -53,6 +58,7 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage> with BaseWidgetPr
   final homeKey = GlobalKey<ScaffoldState>();
 
   bool homeShowPo = false;
+  bool homeShowSo = false;
   bool homeShowSubscribed = false;
   bool homeShowManufacturers = false;
   bool homeShowCustomers = false;
@@ -97,6 +103,17 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage> with BaseWidgetPr
     );
   }
 
+  void _showSalesOrders(BuildContext context) {
+    if (!InvenTreeAPI().checkConnection()) return;
+
+    Navigator.push(
+        context,
+        MaterialPageRoute(
+            builder: (context) => SalesOrderListWidget(filters: {})
+        )
+    );
+  }
+
   void _showSuppliers(BuildContext context) {
     if (!InvenTreeAPI().checkConnection()) return;
 
@@ -110,12 +127,12 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage> with BaseWidgetPr
     Navigator.push(context, MaterialPageRoute(builder: (context) => CompanyListWidget(L10().manufacturers, {"is_manufacturer": "true"})));
   }
 
+  */
   void _showCustomers(BuildContext context) {
     if (!InvenTreeAPI().checkConnection()) return;
 
     Navigator.push(context, MaterialPageRoute(builder: (context) => CompanyListWidget(L10().customers, {"is_customer": "true"})));
   }
-   */
 
   void _selectProfile() {
     Navigator.push(
@@ -130,6 +147,7 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage> with BaseWidgetPr
 
     homeShowSubscribed = await InvenTreeSettingsManager().getValue(INV_HOME_SHOW_SUBSCRIBED, true) as bool;
     homeShowPo = await InvenTreeSettingsManager().getValue(INV_HOME_SHOW_PO, true) as bool;
+    homeShowSo = await InvenTreeSettingsManager().getValue(INV_HOME_SHOW_SO, true) as bool;
     homeShowManufacturers = await InvenTreeSettingsManager().getValue(INV_HOME_SHOW_MANUFACTURERS, true) as bool;
     homeShowCustomers = await InvenTreeSettingsManager().getValue(INV_HOME_SHOW_CUSTOMERS, true) as bool;
     homeShowSuppliers = await InvenTreeSettingsManager().getValue(INV_HOME_SHOW_SUPPLIERS, true) as bool;
@@ -207,17 +225,19 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage> with BaseWidgetPr
     ];
 
     // Parts
-    tiles.add(_listTile(
-      context,
-      L10().parts,
-      FontAwesomeIcons.shapes,
-      callback: () {
-        _showParts(context);
-      },
-    ));
+    if (InvenTreePart().canView) {
+      tiles.add(_listTile(
+        context,
+        L10().parts,
+        FontAwesomeIcons.shapes,
+        callback: () {
+          _showParts(context);
+        },
+      ));
+    }
 
     // Starred parts
-    if (homeShowSubscribed) {
+    if (homeShowSubscribed && InvenTreePart().canView) {
       tiles.add(_listTile(
         context,
         L10().partsStarred,
@@ -229,17 +249,19 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage> with BaseWidgetPr
     }
 
     // Stock button
-    tiles.add(_listTile(
-        context,
-        L10().stock,
-        FontAwesomeIcons.boxesStacked,
-        callback: () {
-          _showStock(context);
-        }
-    ));
+    if (InvenTreeStockItem().canView) {
+      tiles.add(_listTile(
+          context,
+          L10().stock,
+          FontAwesomeIcons.boxesStacked,
+          callback: () {
+            _showStock(context);
+          }
+      ));
+    }
 
     // Purchase orders
-    if (homeShowPo) {
+    if (homeShowPo && InvenTreePurchaseOrder().canView) {
       tiles.add(_listTile(
           context,
           L10().purchaseOrders,
@@ -250,8 +272,19 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage> with BaseWidgetPr
       ));
     }
 
+    if (homeShowSo && InvenTreeSalesOrder().canView) {
+      tiles.add(_listTile(
+        context,
+        L10().salesOrders,
+        FontAwesomeIcons.truck,
+        callback: () {
+          _showSalesOrders(context);
+        }
+      ));
+    }
+
     // Suppliers
-    if (homeShowSuppliers) {
+    if (homeShowSuppliers && InvenTreePurchaseOrder().canView) {
       tiles.add(_listTile(
           context,
           L10().suppliers,
@@ -277,7 +310,7 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage> with BaseWidgetPr
           }
       ));
     }
-
+    */
     // Customers
     if (homeShowCustomers) {
       tiles.add(_listTile(
@@ -289,7 +322,6 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage> with BaseWidgetPr
           }
       ));
     }
-     */
 
     return tiles;
   }
diff --git a/lib/widget/po_line_detail.dart b/lib/widget/order/po_line_detail.dart
similarity index 89%
rename from lib/widget/po_line_detail.dart
rename to lib/widget/order/po_line_detail.dart
index b8ac8aaa..d5510d93 100644
--- a/lib/widget/po_line_detail.dart
+++ b/lib/widget/order/po_line_detail.dart
@@ -7,17 +7,17 @@ import "package:inventree/app_colors.dart";
 import "package:inventree/helpers.dart";
 import "package:inventree/l10.dart";
 import "package:inventree/widget/progress.dart";
-import "package:inventree/widget/part_detail.dart";
+import "package:inventree/widget/part/part_detail.dart";
 
 import "package:inventree/widget/refreshable_state.dart";
 import "package:inventree/inventree/company.dart";
 import "package:inventree/inventree/part.dart";
 import "package:inventree/inventree/purchase_order.dart";
 import "package:inventree/widget/snacks.dart";
-import "package:inventree/widget/supplier_part_detail.dart";
+import "package:inventree/widget/company/supplier_part_detail.dart";
 
 /*
- * Widget for displaying detail view of a purchase order line item
+ * Widget for displaying detail view of a single PurchaseOrderLineItem
 */
 class POLineDetailWidget extends StatefulWidget {
 
@@ -171,7 +171,6 @@ class _POLineDetailWidgetState extends RefreshableState<POLineDetailWidget> {
         trailing: api.getThumbnail(widget.item.partImage),
         onTap: () async {
           showLoadingOverlay(context);
-          print("part id: ${widget.item.partId}");
           var part = await InvenTreePart().get(widget.item.partId);
           hideLoadingOverlay();
 
@@ -200,16 +199,32 @@ class _POLineDetailWidgetState extends RefreshableState<POLineDetailWidget> {
       )
     );
 
-    // Recevied
+    // Received quantity
     tiles.add(
       ListTile(
         title: Text(L10().received),
-        subtitle: Text(widget.item.received.toString()),
-        trailing: Text(widget.item.progressString, style: TextStyle(color: widget.item.isComplete ? COLOR_SUCCESS: COLOR_WARNING)),
+        subtitle: ProgressBar(widget.item.progressRatio),
+        trailing: Text(
+            widget.item.progressString,
+            style: TextStyle(
+                color: widget.item.isComplete ? COLOR_SUCCESS: COLOR_WARNING
+            )
+        ),
         leading: FaIcon(FontAwesomeIcons.boxOpen),
       )
     );
 
+    // Reference
+    if (widget.item.reference.isNotEmpty) {
+      tiles.add(
+          ListTile(
+            title: Text(L10().reference),
+            subtitle: Text(widget.item.reference),
+            leading: FaIcon(FontAwesomeIcons.hashtag),
+          )
+      );
+    }
+
     // Pricing information
     tiles.add(
       ListTile(
diff --git a/lib/widget/po_line_list.dart b/lib/widget/order/po_line_list.dart
similarity index 97%
rename from lib/widget/po_line_list.dart
rename to lib/widget/order/po_line_list.dart
index 27827f81..e7f53d4b 100644
--- a/lib/widget/po_line_list.dart
+++ b/lib/widget/order/po_line_list.dart
@@ -9,7 +9,7 @@ import "package:inventree/inventree/model.dart";
 import "package:inventree/inventree/purchase_order.dart";
 
 import "package:inventree/widget/paginator.dart";
-import "package:inventree/widget/po_line_detail.dart";
+import "package:inventree/widget/order/po_line_detail.dart";
 import "package:inventree/widget/progress.dart";
 
 /*
diff --git a/lib/widget/purchase_order_detail.dart b/lib/widget/order/purchase_order_detail.dart
similarity index 74%
rename from lib/widget/purchase_order_detail.dart
rename to lib/widget/order/purchase_order_detail.dart
index f03d4f0b..73fa4eb2 100644
--- a/lib/widget/purchase_order_detail.dart
+++ b/lib/widget/order/purchase_order_detail.dart
@@ -2,9 +2,8 @@ import "package:flutter/material.dart";
 import "package:flutter_speed_dial/flutter_speed_dial.dart";
 import "package:font_awesome_flutter/font_awesome_flutter.dart";
 import "package:inventree/widget/dialogs.dart";
-import "package:inventree/widget/po_line_list.dart";
+import "package:inventree/widget/order/po_line_list.dart";
 
-import "package:inventree/api.dart";
 import "package:inventree/app_colors.dart";
 import "package:inventree/barcode/barcode.dart";
 import "package:inventree/helpers.dart";
@@ -13,13 +12,17 @@ import "package:inventree/l10.dart";
 import "package:inventree/inventree/company.dart";
 import "package:inventree/inventree/purchase_order.dart";
 import "package:inventree/widget/attachment_widget.dart";
-import "package:inventree/widget/company_detail.dart";
+import "package:inventree/widget/company/company_detail.dart";
 import "package:inventree/widget/notes_widget.dart";
+import "package:inventree/widget/progress.dart";
 import "package:inventree/widget/refreshable_state.dart";
 import "package:inventree/widget/snacks.dart";
-import "package:inventree/widget/stock_list.dart";
+import "package:inventree/widget/stock/stock_list.dart";
 
 
+/*
+ * Widget for viewing a single PurchaseOrder instance
+ */
 class PurchaseOrderDetailWidget extends StatefulWidget {
 
   const PurchaseOrderDetailWidget(this.order, {Key? key}): super(key: key);
@@ -27,16 +30,14 @@ class PurchaseOrderDetailWidget extends StatefulWidget {
   final InvenTreePurchaseOrder order;
 
   @override
-  _PurchaseOrderDetailState createState() => _PurchaseOrderDetailState(order);
+  _PurchaseOrderDetailState createState() => _PurchaseOrderDetailState();
 }
 
 
 class _PurchaseOrderDetailState extends RefreshableState<PurchaseOrderDetailWidget> {
 
-  _PurchaseOrderDetailState(this.order);
-
-  final InvenTreePurchaseOrder order;
-
+  _PurchaseOrderDetailState();
+  
   List<InvenTreePOLineItem> lines = [];
 
   int completedLines = 0;
@@ -52,7 +53,7 @@ class _PurchaseOrderDetailState extends RefreshableState<PurchaseOrderDetailWidg
   List<Widget> appBarActions(BuildContext context) {
     List<Widget> actions = [];
 
-    if (order.canEdit) {
+    if (widget.order.canEdit) {
       actions.add(
         IconButton(
           icon: Icon(Icons.edit_square),
@@ -71,8 +72,8 @@ class _PurchaseOrderDetailState extends RefreshableState<PurchaseOrderDetailWidg
   List<SpeedDialChild> actionButtons(BuildContext context) {
     List<SpeedDialChild> actions = [];
 
-    if (order.canCreate) {
-      if (order.isPending) {
+    if (widget.order.canCreate) {
+      if (widget.order.isPending) {
         actions.add(
           SpeedDialChild(
             child: FaIcon(FontAwesomeIcons.paperPlane, color: Colors.blue),
@@ -84,7 +85,7 @@ class _PurchaseOrderDetailState extends RefreshableState<PurchaseOrderDetailWidg
         );
       }
 
-      if (order.isOpen) {
+      if (widget.order.isOpen) {
         actions.add(
           SpeedDialChild(
             child: FaIcon(FontAwesomeIcons.circleXmark, color: Colors.red),
@@ -109,7 +110,7 @@ class _PurchaseOrderDetailState extends RefreshableState<PurchaseOrderDetailWidg
       color: Colors.blue,
       acceptText: L10().issue,
       onAccept: () async {
-        await order.issueOrder().then((dynamic) {
+        await widget.order.issueOrder().then((dynamic) {
           refresh(context);
         });
       }
@@ -125,7 +126,7 @@ class _PurchaseOrderDetailState extends RefreshableState<PurchaseOrderDetailWidg
       color: Colors.red,
       acceptText: L10().cancel,
       onAccept: () async {
-        await order.cancelOrder().then((dynamic) {
+        await widget.order.cancelOrder().then((dynamic) {
           print("callback");
           refresh(context);
         });
@@ -145,7 +146,7 @@ class _PurchaseOrderDetailState extends RefreshableState<PurchaseOrderDetailWidg
           onTap:() async {
             scanBarcode(
               context,
-              handler: POReceiveBarcodeHandler(purchaseOrder: order),
+              handler: POReceiveBarcodeHandler(purchaseOrder: widget.order),
             );
           },
         )
@@ -158,11 +159,11 @@ class _PurchaseOrderDetailState extends RefreshableState<PurchaseOrderDetailWidg
 
   @override
   Future<void> request(BuildContext context) async {
-    await order.reload();
+    await widget.order.reload();
 
     await api.PurchaseOrderStatus.load();
 
-    lines = await order.getLineItems();
+    lines = await widget.order.getLineItems();
 
     supportProjectCodes = api.supportsProjectCodes && await api.getGlobalBooleanSetting("PROJECT_CODES_ENABLED");
 
@@ -174,16 +175,20 @@ class _PurchaseOrderDetailState extends RefreshableState<PurchaseOrderDetailWidg
       }
     }
 
-    attachmentCount = await InvenTreePurchaseOrderAttachment().count(
-      filters: {
-        "order": order.pk.toString()
+    InvenTreePurchaseOrderAttachment().count(filters: {
+      "order": widget.order.pk.toString()
+    }).then((int value) {
+      if (mounted) {
+        setState(() {
+          attachmentCount = value;
+        });
       }
-    );
+    });
   }
 
   // Edit the currently displayed PurchaseOrder
   Future <void> editOrder(BuildContext context) async {
-    var fields = order.formFields();
+    var fields = widget.order.formFields();
 
     // Cannot edit supplier field from here
     fields.remove("supplier");
@@ -198,7 +203,7 @@ class _PurchaseOrderDetailState extends RefreshableState<PurchaseOrderDetailWidg
       fields.remove("project_code");
     }
 
-    order.editForm(
+    widget.order.editForm(
       context,
       L10().purchaseOrderEdit,
       fields: fields,
@@ -211,17 +216,17 @@ class _PurchaseOrderDetailState extends RefreshableState<PurchaseOrderDetailWidg
 
   Widget headerTile(BuildContext context) {
 
-    InvenTreeCompany? supplier = order.supplier;
+    InvenTreeCompany? supplier = widget.order.supplier;
 
     return Card(
         child: ListTile(
-          title: Text(order.reference),
-          subtitle: Text(order.description),
-          leading: supplier == null ? null : InvenTreeAPI().getThumbnail(supplier.thumbnail),
+          title: Text(widget.order.reference),
+          subtitle: Text(widget.order.description),
+          leading: supplier == null ? null : api.getThumbnail(supplier.thumbnail),
           trailing: Text(
-            api.PurchaseOrderStatus.label(order.status),
+            api.PurchaseOrderStatus.label(widget.order.status),
             style: TextStyle(
-              color: api.PurchaseOrderStatus.color(order.status)
+              color: api.PurchaseOrderStatus.color(widget.order.status)
             ),
           )
         )
@@ -233,14 +238,14 @@ class _PurchaseOrderDetailState extends RefreshableState<PurchaseOrderDetailWidg
 
     List<Widget> tiles = [];
 
-    InvenTreeCompany? supplier = order.supplier;
+    InvenTreeCompany? supplier = widget.order.supplier;
 
     tiles.add(headerTile(context));
 
-    if (supportProjectCodes && order.hasProjectCode) {
+    if (supportProjectCodes && widget.order.hasProjectCode) {
       tiles.add(ListTile(
         title: Text(L10().projectCode),
-        subtitle: Text("${order.projectCode} - ${order.projectCodeDescription}"),
+        subtitle: Text("${widget.order.projectCode} - ${widget.order.projectCodeDescription}"),
         leading: FaIcon(FontAwesomeIcons.list),
       ));
     }
@@ -261,20 +266,24 @@ class _PurchaseOrderDetailState extends RefreshableState<PurchaseOrderDetailWidg
       ));
     }
 
-    if (order.supplierReference.isNotEmpty) {
+    if (widget.order.supplierReference.isNotEmpty) {
       tiles.add(ListTile(
         title: Text(L10().supplierReference),
-        subtitle: Text(order.supplierReference),
+        subtitle: Text(widget.order.supplierReference),
         leading: FaIcon(FontAwesomeIcons.hashtag),
       ));
     }
 
-    Color lineColor = completedLines < order.lineItemCount ? COLOR_WARNING : COLOR_SUCCESS;
+    Color lineColor = completedLines < widget.order.lineItemCount ? COLOR_WARNING : COLOR_SUCCESS;
 
     tiles.add(ListTile(
       title: Text(L10().lineItems),
+      subtitle: ProgressBar(
+        completedLines.toDouble(),
+        maximum: widget.order.lineItemCount.toDouble(),
+      ),
       leading: FaIcon(FontAwesomeIcons.clipboardCheck),
-      trailing: Text("${completedLines} /  ${order.lineItemCount}", style: TextStyle(color: lineColor)),
+      trailing: Text("${completedLines} /  ${widget.order.lineItemCount}", style: TextStyle(color: lineColor)),
     ));
 
     tiles.add(ListTile(
@@ -285,18 +294,18 @@ class _PurchaseOrderDetailState extends RefreshableState<PurchaseOrderDetailWidg
       ),
     ));
 
-    if (order.issueDate.isNotEmpty) {
+    if (widget.order.issueDate.isNotEmpty) {
       tiles.add(ListTile(
         title: Text(L10().issueDate),
-        subtitle: Text(order.issueDate),
+        subtitle: Text(widget.order.issueDate),
         leading: FaIcon(FontAwesomeIcons.calendarDays),
       ));
     }
 
-    if (order.targetDate.isNotEmpty) {
+    if (widget.order.targetDate.isNotEmpty) {
       tiles.add(ListTile(
         title: Text(L10().targetDate),
-        subtitle: Text(order.targetDate),
+        subtitle: Text(widget.order.targetDate),
         leading: FaIcon(FontAwesomeIcons.calendarDays),
       ));
     }
@@ -310,7 +319,7 @@ class _PurchaseOrderDetailState extends RefreshableState<PurchaseOrderDetailWidg
             Navigator.push(
               context,
               MaterialPageRoute(
-                builder: (context) => NotesWidget(order)
+                builder: (context) => NotesWidget(widget.order)
               )
             );
           },
@@ -329,8 +338,8 @@ class _PurchaseOrderDetailState extends RefreshableState<PurchaseOrderDetailWidg
               MaterialPageRoute(
                 builder: (context) => AttachmentWidget(
                     InvenTreePurchaseOrderAttachment(),
-                    order.pk,
-                    order.canEdit
+                    widget.order.pk,
+                    widget.order.canEdit
                 )
               )
             );
@@ -355,9 +364,9 @@ class _PurchaseOrderDetailState extends RefreshableState<PurchaseOrderDetailWidg
   List<Widget> getTabs(BuildContext context) {
     return [
       ListView(children: orderTiles(context)),
-      PaginatedPOLineList({"order": order.pk.toString()}),
+      PaginatedPOLineList({"order": widget.order.pk.toString()}),
       // ListView(children: lineTiles(context)),
-      PaginatedStockItemList({"purchase_order": order.pk.toString()}),
+      PaginatedStockItemList({"purchase_order": widget.order.pk.toString()}),
     ];
   }
 
diff --git a/lib/widget/purchase_order_list.dart b/lib/widget/order/purchase_order_list.dart
similarity index 90%
rename from lib/widget/purchase_order_list.dart
rename to lib/widget/order/purchase_order_list.dart
index 44046564..1eb851d9 100644
--- a/lib/widget/purchase_order_list.dart
+++ b/lib/widget/order/purchase_order_list.dart
@@ -5,7 +5,7 @@ import "package:font_awesome_flutter/font_awesome_flutter.dart";
 import "package:inventree/inventree/company.dart";
 import "package:inventree/inventree/model.dart";
 import "package:inventree/widget/paginator.dart";
-import "package:inventree/widget/purchase_order_detail.dart";
+import "package:inventree/widget/order/purchase_order_detail.dart";
 import "package:inventree/widget/refreshable_state.dart";
 import "package:inventree/l10.dart";
 import "package:inventree/api.dart";
@@ -22,15 +22,13 @@ class PurchaseOrderListWidget extends StatefulWidget {
   final Map<String, String> filters;
 
   @override
-  _PurchaseOrderListWidgetState createState() => _PurchaseOrderListWidgetState(filters);
+  _PurchaseOrderListWidgetState createState() => _PurchaseOrderListWidgetState();
 }
 
 
 class _PurchaseOrderListWidgetState extends RefreshableState<PurchaseOrderListWidget> {
 
-  _PurchaseOrderListWidgetState(this.filters);
-
-  final Map<String, String> filters;
+  _PurchaseOrderListWidgetState();
 
   @override
   String getAppBarTitle() => L10().purchaseOrders;
@@ -45,7 +43,7 @@ class _PurchaseOrderListWidgetState extends RefreshableState<PurchaseOrderListWi
           child: FaIcon(FontAwesomeIcons.circlePlus),
           label: L10().purchaseOrderCreate,
           onTap: () {
-            createPurchaseOrder(context);
+            _createPurchaseOrder(context);
           }
         )
       );
@@ -54,9 +52,11 @@ class _PurchaseOrderListWidgetState extends RefreshableState<PurchaseOrderListWi
     return actions;
   }
 
-  Future<void> createPurchaseOrder(BuildContext context) async {
+  // Launch form to create a new PurchaseOrder
+  Future<void> _createPurchaseOrder(BuildContext context) async {
     var fields = InvenTreePurchaseOrder().formFields();
 
+    // Cannot set contact until company is locked in
     fields.remove("contact");
 
     InvenTreePurchaseOrder().createForm(
@@ -104,7 +104,7 @@ class _PurchaseOrderListWidgetState extends RefreshableState<PurchaseOrderListWi
 
   @override
   Widget getBody(BuildContext context) {
-    return PaginatedPurchaseOrderList(filters);
+    return PaginatedPurchaseOrderList(widget.filters);
   }
 }
 
@@ -143,6 +143,11 @@ class _PaginatedPurchaseOrderListState extends PaginatedSearchState<PaginatedPur
       "label": L10().outstanding,
       "help_text": L10().outstandingOrderDetail,
       "tristate": true,
+    },
+    "overdue": {
+      "label": L10().overdue,
+      "help_text": L10().overdueDetail,
+      "tristate": true,
     }
   };
 
@@ -153,7 +158,6 @@ class _PaginatedPurchaseOrderListState extends PaginatedSearchState<PaginatedPur
     final page = await InvenTreePurchaseOrder().listPaginated(limit, offset, filters: params);
 
     return page;
-
   }
 
   @override
diff --git a/lib/widget/order/sales_order_detail.dart b/lib/widget/order/sales_order_detail.dart
new file mode 100644
index 00000000..73422dab
--- /dev/null
+++ b/lib/widget/order/sales_order_detail.dart
@@ -0,0 +1,303 @@
+
+import "package:flutter/material.dart";
+import "package:flutter_speed_dial/flutter_speed_dial.dart";
+import "package:font_awesome_flutter/font_awesome_flutter.dart";
+import "package:inventree/inventree/company.dart";
+import "package:inventree/inventree/sales_order.dart";
+import "package:inventree/widget/order/so_line_list.dart";
+import "package:inventree/widget/refreshable_state.dart";
+
+import "package:inventree/l10.dart";
+
+import "package:inventree/app_colors.dart";
+import "package:inventree/widget/attachment_widget.dart";
+import "package:inventree/widget/notes_widget.dart";
+import "package:inventree/widget/snacks.dart";
+import "package:inventree/widget/company/company_detail.dart";
+import "package:inventree/widget/progress.dart";
+
+/*
+ * Widget for viewing a single SalesOrder instance
+ */
+class SalesOrderDetailWidget extends StatefulWidget {
+
+  const SalesOrderDetailWidget(this.order, {Key? key}) : super(key: key);
+
+  final InvenTreeSalesOrder order;
+
+  @override
+  _SalesOrderDetailState createState() => _SalesOrderDetailState();
+}
+
+
+class _SalesOrderDetailState extends RefreshableState<SalesOrderDetailWidget> {
+
+  _SalesOrderDetailState();
+
+  List<InvenTreeSOLineItem> lines = [];
+
+  bool supportsProjectCodes = false;
+  int completedLines = 0;
+  int attachmentCount = 0;
+
+  @override
+  String getAppBarTitle() => L10().salesOrder;
+
+  @override
+  List<Widget> appBarActions(BuildContext context) {
+    List<Widget> actions = [];
+
+    if (widget.order.canEdit) {
+      actions.add(
+        IconButton(
+          icon: Icon(Icons.edit_square),
+          onPressed: () {
+            editOrder(context);
+          },
+        )
+      );
+    }
+
+    return actions;
+  }
+
+  // Add a new line item to this sales order
+  Future<void> _addLineItem(BuildContext context) async {
+    var fields = InvenTreeSOLineItem().formFields();
+
+    fields["order"]?["value"] = widget.order.pk;
+    fields["order"]?["hidden"] = true;
+
+    InvenTreeSOLineItem().createForm(
+        context,
+        L10().lineItemAdd,
+        fields: fields,
+        onSuccess: (result) async {
+          refresh(context);
+        }
+    );
+  }
+
+  @override
+  List<SpeedDialChild> actionButtons(BuildContext context) {
+    List<SpeedDialChild> actions = [];
+
+    // Add line item
+    if (widget.order.isOpen && InvenTreeSOLineItem().canCreate) {
+      actions.add(
+        SpeedDialChild(
+          child: FaIcon(FontAwesomeIcons.circlePlus),
+          label: L10().lineItemAdd,
+          onTap: () async {
+            _addLineItem(context);
+          }
+        )
+      );
+    }
+
+    return actions;
+  }
+
+  @override
+  List<SpeedDialChild> barcodeButtons(BuildContext context) {
+    List<SpeedDialChild> actions = [];
+
+    // TODO
+
+    return actions;
+  }
+
+  @override
+  Future<void> request(BuildContext context) async {
+    await widget.order.reload();
+    await api.SalesOrderStatus.load();
+
+    supportsProjectCodes = api.supportsProjectCodes && await api.getGlobalBooleanSetting("PROJECT_CODES_ENABLED");
+
+    completedLines = 0;
+
+    for (var line in lines) {
+      if (line.isComplete) {
+        completedLines += 1;
+      }
+    }
+
+    InvenTreeSalesOrderAttachment().count(filters: {
+      "order": widget.order.pk.toString()
+    }).then((int value) {
+      if (mounted) {
+        setState(() {
+          attachmentCount = value;
+        });
+      }
+    });
+  }
+
+  // Edit the current SalesOrder instance
+  Future<void> editOrder(BuildContext context) async {
+    var fields = widget.order.formFields();
+
+    fields.remove("customer");
+
+    // Contact model not supported by server
+    if (!api.supportsContactModel) {
+      fields.remove("contact");
+    }
+
+    // ProjectCode model not supported by server
+    if (!supportsProjectCodes) {
+      fields.remove("project_code");
+    }
+
+    widget.order.editForm(
+      context,
+      L10().salesOrderEdit,
+      fields: fields,
+      onSuccess: (data) async {
+        refresh(context);
+        showSnackIcon(L10().salesOrderUpdated, success: true);
+      }
+    );
+  }
+
+  // Construct header tile
+  Widget headerTile(BuildContext context) {
+    InvenTreeCompany? customer = widget.order.customer;
+
+    return Card(
+      child: ListTile(
+        title: Text(widget.order.reference),
+        subtitle: Text(widget.order.description),
+        leading: customer == null ? null : api.getThumbnail(customer.thumbnail),
+        trailing: Text(
+          api.SalesOrderStatus.label(widget.order.status),
+          style: TextStyle(
+              color: api.SalesOrderStatus.color(widget.order.status)
+          ),
+        ),
+      )
+    );
+  }
+
+  List<Widget> orderTiles(BuildContext context) {
+
+    List<Widget> tiles = [
+      headerTile(context)
+    ];
+
+    InvenTreeCompany? customer = widget.order.customer;
+
+    if (supportsProjectCodes && widget.order.hasProjectCode) {
+      tiles.add(ListTile(
+        title: Text(L10().projectCode),
+        subtitle: Text("${widget.order.projectCode} - ${widget.order.projectCodeDescription}"),
+        leading: FaIcon(FontAwesomeIcons.list),
+      ));
+    }
+
+    if (customer != null) {
+      tiles.add(ListTile(
+        title: Text(L10().customer),
+        subtitle: Text(customer.name),
+        leading: FaIcon(FontAwesomeIcons.userTie, color: COLOR_ACTION),
+        onTap: () {
+          Navigator.push(
+              context,
+              MaterialPageRoute(
+                  builder: (context) => CompanyDetailWidget(customer)
+              )
+          );
+        }
+      ));
+    }
+
+    if (widget.order.customerReference.isNotEmpty) {
+      tiles.add(ListTile(
+        title: Text(L10().customerReference),
+        subtitle: Text(widget.order.customerReference),
+        leading: FaIcon(FontAwesomeIcons.hashtag),
+      ));
+    }
+
+    Color lineColor = completedLines < widget.order.lineItemCount ? COLOR_WARNING : COLOR_SUCCESS;
+
+    tiles.add(ListTile(
+      title: Text(L10().lineItems),
+      subtitle: ProgressBar(
+        completedLines.toDouble(),
+        maximum: widget.order.lineItemCount.toDouble()
+      ),
+      leading: FaIcon(FontAwesomeIcons.clipboardCheck),
+      trailing: Text("${completedLines} / ${widget.order.lineItemCount}", style: TextStyle(color: lineColor)),
+    ));
+
+    // TODO: total price
+
+    if (widget.order.targetDate.isNotEmpty) {
+      tiles.add(ListTile(
+        title: Text(L10().targetDate),
+        subtitle: Text(widget.order.targetDate),
+        leading: FaIcon(FontAwesomeIcons.calendarDays),
+      ));
+    }
+
+    // Notes tile
+    tiles.add(
+      ListTile(
+        title: Text(L10().notes),
+        leading: FaIcon(FontAwesomeIcons.noteSticky, color: COLOR_ACTION),
+        onTap: () {
+          Navigator.push(
+            context,
+            MaterialPageRoute(
+            builder: (context) => NotesWidget(widget.order)
+            )
+          );
+        },
+      )
+    );
+
+    // Attachments
+    tiles.add(
+      ListTile(
+        title: Text(L10().attachments),
+        leading: FaIcon(FontAwesomeIcons.fileLines, color: COLOR_ACTION),
+        trailing: attachmentCount > 0 ? Text(attachmentCount.toString()) : null,
+        onTap: () {
+          Navigator.push(
+          context,
+          MaterialPageRoute(
+            builder: (context) => AttachmentWidget(
+            InvenTreeSalesOrderAttachment(),
+            widget.order.pk,
+            widget.order.canEdit
+            )
+          )
+        );
+        },
+      )
+    );
+
+    return tiles;
+  }
+
+  @override
+  List<Widget> getTabIcons(BuildContext context) {
+    return [
+      Tab(text: L10().details),
+      Tab(text: L10().lineItems),
+      // TODO: Add in the "shipped items" tab
+      // Tab(text: L10().shipped)
+    ];
+  }
+
+  @override
+  List<Widget> getTabs(BuildContext context) {
+    return [
+      ListView(children: orderTiles(context)),
+      PaginatedSOLineList({"order": widget.order.pk.toString()}),
+      // Center(), // TODO: Delivered stock
+    ];
+  }
+
+}
\ No newline at end of file
diff --git a/lib/widget/order/sales_order_list.dart b/lib/widget/order/sales_order_list.dart
new file mode 100644
index 00000000..a97de809
--- /dev/null
+++ b/lib/widget/order/sales_order_list.dart
@@ -0,0 +1,176 @@
+
+import "package:flutter/material.dart";
+import "package:flutter_speed_dial/flutter_speed_dial.dart";
+import "package:font_awesome_flutter/font_awesome_flutter.dart";
+import "package:inventree/inventree/sales_order.dart";
+import "package:inventree/widget/order/sales_order_detail.dart";
+import "package:inventree/widget/paginator.dart";
+
+import "package:inventree/widget/refreshable_state.dart";
+import "package:inventree/l10.dart";
+
+import "package:inventree/api.dart";
+import "package:inventree/inventree/company.dart";
+import "package:inventree/inventree/model.dart";
+
+
+class SalesOrderListWidget extends StatefulWidget {
+
+  const SalesOrderListWidget({this.filters = const {}, Key? key}) : super(key: key);
+
+  final Map<String, String> filters;
+
+  @override
+  _SalesOrderListWidgetState createState() => _SalesOrderListWidgetState();
+
+}
+
+class _SalesOrderListWidgetState extends RefreshableState<SalesOrderListWidget> {
+
+  _SalesOrderListWidgetState();
+
+  @override
+  String getAppBarTitle() => L10().salesOrders;
+
+  @override
+  List<SpeedDialChild> actionButtons(BuildContext context) {
+    List<SpeedDialChild> actions = [];
+
+    if (InvenTreeSalesOrder().canCreate) {
+      actions.add(
+          SpeedDialChild(
+              child: FaIcon(FontAwesomeIcons.circlePlus),
+              label: L10().salesOrderCreate,
+              onTap: () {
+                _createSalesOrder(context);
+              }
+          )
+      );
+    }
+
+    return actions;
+  }
+
+  // Launch form to create a new SalesOrder
+  Future<void> _createSalesOrder(BuildContext context) async {
+    var fields = InvenTreeSalesOrder().formFields();
+
+    // Cannot set contact until company is locked in
+    fields.remove("contact");
+
+    InvenTreeSalesOrder().createForm(
+        context,
+        L10().salesOrderCreate,
+        fields: fields,
+        onSuccess: (result) async {
+          Map<String, dynamic> data = result as Map<String, dynamic>;
+
+          if (data.containsKey("pk")) {
+            var order = InvenTreeSalesOrder.fromJson(data);
+
+            Navigator.push(
+              context,
+              MaterialPageRoute(
+                builder: (context) => SalesOrderDetailWidget(order)
+              )
+            );
+          }
+        }
+    );
+  }
+
+  @override
+  List<SpeedDialChild> barcodeButtons(BuildContext context) {
+    // TODO: return custom barcode actions
+    return [];
+  }
+
+  @override
+  Widget getBody(BuildContext context) {
+    return PaginatedSalesOrderList(widget.filters);
+  }
+
+}
+
+
+class PaginatedSalesOrderList extends PaginatedSearchWidget {
+
+  const PaginatedSalesOrderList(Map<String, String> filters) : super(filters: filters);
+
+  @override
+  String get searchTitle => L10().salesOrders;
+
+  @override
+  _PaginatedSalesOrderListState createState() => _PaginatedSalesOrderListState();
+
+}
+
+
+class _PaginatedSalesOrderListState extends PaginatedSearchState<PaginatedSalesOrderList> {
+
+  _PaginatedSalesOrderListState() : super();
+
+  @override
+  String get prefix => "so_";
+
+  @override
+  Map<String, String> get orderingOptions => {
+    "reference": L10().reference,
+    "status": L10().status,
+    "target_date": L10().targetDate,
+    "customer__name": L10().customer,
+  };
+
+  @override
+  Map<String, Map<String, dynamic>> get filterOptions => {
+    "outstanding": {
+      "label": L10().outstanding,
+      "help_text": L10().outstandingOrderDetail,
+      "tristate": true,
+    },
+    "overdue": {
+      "label": L10().overdue,
+      "help_text": L10().overdueDetail,
+      "tristate": true,
+    }
+  };
+
+  @override
+  Future<InvenTreePageResponse?> requestPage(int limit, int offset, Map<String, String> params) async {
+
+    await InvenTreeAPI().SalesOrderStatus.load();
+    final page = await InvenTreeSalesOrder().listPaginated(limit, offset, filters: params);
+
+    return page;
+  }
+
+  @override
+  Widget buildItem(BuildContext context, InvenTreeModel model) {
+
+    InvenTreeSalesOrder order = model as InvenTreeSalesOrder;
+
+    InvenTreeCompany? customer = order.customer;
+
+    return ListTile(
+      title: Text(order.reference),
+      subtitle: Text(order.description),
+      leading: customer == null ? null : InvenTreeAPI().getThumbnail(customer.thumbnail),
+      trailing: Text(
+        InvenTreeAPI().SalesOrderStatus.label(order.status),
+        style: TextStyle(
+          color: InvenTreeAPI().SalesOrderStatus.color(order.status),
+        )
+      ),
+      onTap: () async {
+        Navigator.push(
+          context,
+          MaterialPageRoute(
+            builder: (context) => SalesOrderDetailWidget(order)
+          )
+        );
+      }
+    );
+
+  }
+
+}
\ No newline at end of file
diff --git a/lib/widget/order/so_line_detail.dart b/lib/widget/order/so_line_detail.dart
new file mode 100644
index 00000000..e0ddd898
--- /dev/null
+++ b/lib/widget/order/so_line_detail.dart
@@ -0,0 +1,164 @@
+
+
+/*
+ * Widget for displaying detail view of a single SalesOrderLineItem
+ */
+import "package:flutter/material.dart";
+import "package:flutter_speed_dial/flutter_speed_dial.dart";
+import "package:font_awesome_flutter/font_awesome_flutter.dart";
+
+import "package:inventree/app_colors.dart";
+import "package:inventree/l10.dart";
+import "package:inventree/inventree/part.dart";
+import "package:inventree/inventree/sales_order.dart";
+import "package:inventree/widget/refreshable_state.dart";
+import "package:inventree/widget/progress.dart";
+import "package:inventree/widget/part/part_detail.dart";
+
+import "package:inventree/helpers.dart";
+import "package:inventree/widget/snacks.dart";
+
+
+class SoLineDetailWidget extends StatefulWidget {
+
+  const SoLineDetailWidget(this.item, {Key? key}) : super(key: key);
+
+  final InvenTreeSOLineItem item;
+
+  @override
+  _SOLineDetailWidgetState createState() => _SOLineDetailWidgetState();
+
+}
+
+
+class _SOLineDetailWidgetState extends RefreshableState<SoLineDetailWidget> {
+
+  _SOLineDetailWidgetState();
+
+  @override
+  String getAppBarTitle() => L10().lineItem;
+
+  @override
+  List<Widget> appBarActions(BuildContext context) {
+    List<Widget> actions = [];
+
+    if (widget.item.canEdit) {
+      actions.add(
+        IconButton(
+            icon: Icon(Icons.edit_square),
+            onPressed: () {
+              _editLineItem(context);
+            }),
+      );
+    }
+
+    return actions;
+  }
+
+  Future<void> _editLineItem(BuildContext context) async {
+    var fields = widget.item.formFields();
+
+    // Prevent editing of the line item
+    if (widget.item.shipped > 0) {
+      fields["part"]?["hidden"] = true;
+    }
+
+    widget.item.editForm(
+      context,
+      L10().editLineItem,
+      fields: fields,
+      onSuccess: (data) async {
+        refresh(context);
+        showSnackIcon(L10().lineItemUpdated, success: true);
+      }
+    );
+  }
+
+  @override
+  List<SpeedDialChild> actionButtons(BuildContext context) {
+    // TODO
+    return [];
+  }
+
+  @override
+  Future<void> request(BuildContext context) async {
+    await widget.item.reload();
+  }
+
+  @override
+  List<Widget> getTiles(BuildContext context) {
+    List<Widget> tiles = [];
+
+    // Reference to the part
+    tiles.add(
+      ListTile(
+        title: Text(L10().part),
+        subtitle: Text(widget.item.partName),
+        leading: FaIcon(FontAwesomeIcons.shapes, color: COLOR_ACTION),
+        trailing: api.getThumbnail(widget.item.partImage),
+        onTap: () async {
+          showLoadingOverlay(context);
+          var part = await InvenTreePart().get(widget.item.partId);
+          hideLoadingOverlay();
+
+          if (part is InvenTreePart) {
+            Navigator.push(context, MaterialPageRoute(builder: (context) => PartDetailWidget(part)));
+          }
+        }
+      )
+    );
+
+    // Shipped quantity
+    tiles.add(
+      ListTile(
+        title: Text(L10().shipped),
+        subtitle: ProgressBar(widget.item.progressRatio),
+        trailing: Text(
+          widget.item.progressString,
+          style: TextStyle(
+            color: widget.item.isComplete ? COLOR_SUCCESS : COLOR_WARNING
+          ),
+        ),
+        leading: FaIcon(FontAwesomeIcons.truck)
+      )
+    );
+
+    // Reference
+    if (widget.item.reference.isNotEmpty) {
+      tiles.add(
+        ListTile(
+          title: Text(L10().reference),
+          subtitle: Text(widget.item.reference),
+          leading: FaIcon(FontAwesomeIcons.hashtag)
+        )
+      );
+    }
+
+    // Note
+    if (widget.item.notes.isNotEmpty) {
+      tiles.add(
+          ListTile(
+            title: Text(L10().notes),
+            subtitle: Text(widget.item.notes),
+            leading: FaIcon(FontAwesomeIcons.noteSticky),
+          )
+      );
+    }
+
+    // External link
+    if (widget.item.link.isNotEmpty) {
+      tiles.add(
+          ListTile(
+            title: Text(L10().link),
+            subtitle: Text(widget.item.link),
+            leading: FaIcon(FontAwesomeIcons.link, color: COLOR_ACTION),
+            onTap: () async {
+              await openLink(widget.item.link);
+            },
+          )
+      );
+    }
+
+    return tiles;
+  }
+}
\ No newline at end of file
diff --git a/lib/widget/order/so_line_list.dart b/lib/widget/order/so_line_list.dart
new file mode 100644
index 00000000..6d7e1acd
--- /dev/null
+++ b/lib/widget/order/so_line_list.dart
@@ -0,0 +1,86 @@
+import "package:flutter/material.dart";
+import "package:inventree/l10.dart";
+import "package:inventree/widget/order/so_line_detail.dart";
+import "package:inventree/widget/paginator.dart";
+import "package:inventree/inventree/model.dart";
+import "package:inventree/inventree/sales_order.dart";
+import "package:inventree/inventree/part.dart";
+import "package:inventree/api.dart";
+import "package:inventree/app_colors.dart";
+import "package:inventree/widget/progress.dart";
+
+
+/*
+ * Paginated widget class for displaying a list of sales order line items
+ */
+
+class PaginatedSOLineList extends PaginatedSearchWidget {
+  const PaginatedSOLineList(Map<String, String> filters) : super(filters: filters);
+
+  @override
+  String get searchTitle => L10().lineItems;
+
+  @override
+  _PaginatedSOLineListState createState() => _PaginatedSOLineListState();
+
+}
+
+
+/*
+ * State class for PaginatedSOLineList
+ */
+class _PaginatedSOLineListState extends PaginatedSearchState<PaginatedSOLineList> {
+
+  _PaginatedSOLineListState() : super();
+
+  @override
+  String get prefix => "so_line_";
+
+  @override
+  Map<String, String> get orderingOptions => {
+    "part": L10().part,
+    "quantity": L10().quantity,
+  };
+
+  @override
+  Map<String, Map<String, dynamic>> get filterOptions => {
+
+  };
+
+  @override
+  Future<InvenTreePageResponse?> requestPage(int limit, int offset, Map<String, String> params) async {
+    final page = await InvenTreeSOLineItem().listPaginated(limit, offset, filters: params);
+    return page;
+  }
+
+  @override
+  Widget buildItem(BuildContext context, InvenTreeModel model) {
+    InvenTreeSOLineItem item = model as InvenTreeSOLineItem;
+    InvenTreePart? part = item.part;
+
+    if (part != null) {
+      return ListTile(
+        title: Text(part.name),
+        subtitle: Text(part.description),
+        leading: InvenTreeAPI().getThumbnail(part.thumbnail),
+        trailing: Text(item.progressString),
+        onTap: () async {
+          showLoadingOverlay(context);
+          await item.reload();
+          hideLoadingOverlay();
+          Navigator.push(
+            context,
+            MaterialPageRoute(
+              builder: (context) => SoLineDetailWidget(item))
+          );
+        }
+      );
+    } else {
+      return ListTile(
+        title: Text(L10().error),
+        subtitle: Text("Missing part detail", style: TextStyle(color: COLOR_DANGER)),
+      );
+    }
+  }
+
+}
diff --git a/lib/widget/bom_list.dart b/lib/widget/part/bom_list.dart
similarity index 98%
rename from lib/widget/bom_list.dart
rename to lib/widget/part/bom_list.dart
index a39b2773..d9e16bb3 100644
--- a/lib/widget/bom_list.dart
+++ b/lib/widget/part/bom_list.dart
@@ -11,7 +11,7 @@ import "package:inventree/inventree/model.dart";
 import "package:inventree/inventree/part.dart";
 
 import "package:inventree/widget/paginator.dart";
-import "package:inventree/widget/part_detail.dart";
+import "package:inventree/widget/part/part_detail.dart";
 import "package:inventree/widget/progress.dart";
 import "package:inventree/widget/refreshable_state.dart";
 
diff --git a/lib/widget/category_display.dart b/lib/widget/part/category_display.dart
similarity index 97%
rename from lib/widget/category_display.dart
rename to lib/widget/part/category_display.dart
index 261840af..c399cef5 100644
--- a/lib/widget/category_display.dart
+++ b/lib/widget/part/category_display.dart
@@ -7,11 +7,11 @@ import "package:inventree/l10.dart";
 
 import "package:inventree/inventree/part.dart";
 
-import "package:inventree/widget/category_list.dart";
-import "package:inventree/widget/part_list.dart";
+import "package:inventree/widget/part/category_list.dart";
+import "package:inventree/widget/part/part_list.dart";
 import "package:inventree/widget/progress.dart";
 import "package:inventree/widget/snacks.dart";
-import "package:inventree/widget/part_detail.dart";
+import "package:inventree/widget/part/part_detail.dart";
 import "package:inventree/widget/refreshable_state.dart";
 
 
diff --git a/lib/widget/category_list.dart b/lib/widget/part/category_list.dart
similarity index 97%
rename from lib/widget/category_list.dart
rename to lib/widget/part/category_list.dart
index c1c95514..a2031027 100644
--- a/lib/widget/category_list.dart
+++ b/lib/widget/part/category_list.dart
@@ -2,7 +2,7 @@ import "package:flutter/material.dart";
 
 import "package:inventree/inventree/model.dart";
 import "package:inventree/inventree/part.dart";
-import "package:inventree/widget/category_display.dart";
+import "package:inventree/widget/part/category_display.dart";
 import "package:inventree/widget/paginator.dart";
 import "package:inventree/widget/refreshable_state.dart";
 
diff --git a/lib/widget/part_detail.dart b/lib/widget/part/part_detail.dart
similarity index 97%
rename from lib/widget/part_detail.dart
rename to lib/widget/part/part_detail.dart
index 1079fb18..8009ee68 100644
--- a/lib/widget/part_detail.dart
+++ b/lib/widget/part/part_detail.dart
@@ -15,18 +15,18 @@ import "package:inventree/labels.dart";
 import "package:inventree/preferences.dart";
 
 import "package:inventree/widget/attachment_widget.dart";
-import "package:inventree/widget/bom_list.dart";
-import "package:inventree/widget/part_list.dart";
+import "package:inventree/widget/part/bom_list.dart";
+import "package:inventree/widget/part/part_list.dart";
 import "package:inventree/widget/notes_widget.dart";
-import "package:inventree/widget/part_parameter_widget.dart";
+import "package:inventree/widget/part/part_parameter_widget.dart";
 import "package:inventree/widget/progress.dart";
-import "package:inventree/widget/category_display.dart";
+import "package:inventree/widget/part/category_display.dart";
 import "package:inventree/widget/refreshable_state.dart";
-import "package:inventree/widget/part_image_widget.dart";
+import "package:inventree/widget/part/part_image_widget.dart";
 import "package:inventree/widget/snacks.dart";
-import "package:inventree/widget/stock_detail.dart";
-import "package:inventree/widget/stock_list.dart";
-import "package:inventree/widget/supplier_part_list.dart";
+import "package:inventree/widget/stock/stock_detail.dart";
+import "package:inventree/widget/stock/stock_list.dart";
+import "package:inventree/widget/company/supplier_part_list.dart";
 
 
 /*
@@ -634,7 +634,7 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
     fields.remove("serial");
 
     // Hide the "part" field
-    fields["part"]["hidden"] = true;
+    fields["part"]?["hidden"] = true;
 
     int? default_location = part.defaultLocation;
 
diff --git a/lib/widget/part_image_widget.dart b/lib/widget/part/part_image_widget.dart
similarity index 100%
rename from lib/widget/part_image_widget.dart
rename to lib/widget/part/part_image_widget.dart
diff --git a/lib/widget/part_list.dart b/lib/widget/part/part_list.dart
similarity index 98%
rename from lib/widget/part_list.dart
rename to lib/widget/part/part_list.dart
index 6def9b1b..80dbdbd7 100644
--- a/lib/widget/part_list.dart
+++ b/lib/widget/part/part_list.dart
@@ -7,7 +7,7 @@ import "package:inventree/inventree/model.dart";
 import "package:inventree/inventree/part.dart";
 
 import "package:inventree/widget/paginator.dart";
-import "package:inventree/widget/part_detail.dart";
+import "package:inventree/widget/part/part_detail.dart";
 import "package:inventree/widget/refreshable_state.dart";
 
 
diff --git a/lib/widget/part_parameter_widget.dart b/lib/widget/part/part_parameter_widget.dart
similarity index 100%
rename from lib/widget/part_parameter_widget.dart
rename to lib/widget/part/part_parameter_widget.dart
diff --git a/lib/widget/part_suppliers.dart b/lib/widget/part/part_suppliers.dart
similarity index 96%
rename from lib/widget/part_suppliers.dart
rename to lib/widget/part/part_suppliers.dart
index b9fb05ac..ae3788d3 100644
--- a/lib/widget/part_suppliers.dart
+++ b/lib/widget/part/part_suppliers.dart
@@ -7,7 +7,7 @@ import "package:inventree/api.dart";
 import "package:flutter/material.dart";
 import "package:inventree/inventree/part.dart";
 import "package:inventree/inventree/company.dart";
-import "package:inventree/widget/company_detail.dart";
+import "package:inventree/widget/company/company_detail.dart";
 import "package:inventree/widget/refreshable_state.dart";
 
 class PartSupplierWidget extends StatefulWidget {
diff --git a/lib/widget/progress.dart b/lib/widget/progress.dart
index 84174690..ff13ceea 100644
--- a/lib/widget/progress.dart
+++ b/lib/widget/progress.dart
@@ -2,6 +2,34 @@
 
 import "package:flutter/material.dart";
 import "package:flutter_overlay_loader/flutter_overlay_loader.dart";
+import "package:inventree/app_colors.dart";
+
+
+/*
+ * A simplified linear progress bar widget,
+ * with standardized color depiction
+ */
+Widget ProgressBar(
+  double value,
+{
+  double maximum = 1.0
+}) {
+
+  double v = 0;
+
+  if (value <= 0 || maximum <= 0) {
+    v = 0;
+  } else {
+    v = value / maximum;
+  }
+
+  return LinearProgressIndicator(
+    value: v,
+    backgroundColor: Colors.grey,
+    color: v >= 1 ? COLOR_SUCCESS : COLOR_WARNING,
+  );
+}
+
 
 /*
  * Construct a circular progress indicator
diff --git a/lib/widget/search.dart b/lib/widget/search.dart
index a719633d..a899c83c 100644
--- a/lib/widget/search.dart
+++ b/lib/widget/search.dart
@@ -11,13 +11,13 @@ import "package:inventree/inventree/part.dart";
 import "package:inventree/inventree/purchase_order.dart";
 import "package:inventree/inventree/stock.dart";
 
-import "package:inventree/widget/part_list.dart";
-import "package:inventree/widget/purchase_order_list.dart";
+import "package:inventree/widget/part/part_list.dart";
+import "package:inventree/widget/order/purchase_order_list.dart";
 import "package:inventree/widget/refreshable_state.dart";
-import "package:inventree/widget/stock_list.dart";
-import "package:inventree/widget/category_list.dart";
-import "package:inventree/widget/company_list.dart";
-import "package:inventree/widget/location_list.dart";
+import "package:inventree/widget/stock/stock_list.dart";
+import "package:inventree/widget/part/category_list.dart";
+import "package:inventree/widget/company/company_list.dart";
+import "package:inventree/widget/stock/location_list.dart";
 
 
 // Widget for performing database-wide search
diff --git a/lib/widget/location_display.dart b/lib/widget/stock/location_display.dart
similarity index 98%
rename from lib/widget/location_display.dart
rename to lib/widget/stock/location_display.dart
index 4e89f86a..e44e726e 100644
--- a/lib/widget/location_display.dart
+++ b/lib/widget/stock/location_display.dart
@@ -10,12 +10,12 @@ import "package:inventree/l10.dart";
 import "package:inventree/inventree/stock.dart";
 import "package:inventree/preferences.dart";
 
-import "package:inventree/widget/location_list.dart";
+import "package:inventree/widget/stock/location_list.dart";
 import "package:inventree/widget/progress.dart";
 import "package:inventree/widget/refreshable_state.dart";
 import "package:inventree/widget/snacks.dart";
-import "package:inventree/widget/stock_detail.dart";
-import "package:inventree/widget/stock_list.dart";
+import "package:inventree/widget/stock/stock_detail.dart";
+import "package:inventree/widget/stock/stock_list.dart";
 import "package:inventree/labels.dart";
 
 
diff --git a/lib/widget/location_list.dart b/lib/widget/stock/location_list.dart
similarity index 97%
rename from lib/widget/location_list.dart
rename to lib/widget/stock/location_list.dart
index fe396593..a8bc2fe9 100644
--- a/lib/widget/location_list.dart
+++ b/lib/widget/stock/location_list.dart
@@ -2,7 +2,7 @@ import "package:flutter/material.dart";
 
 import "package:inventree/inventree/model.dart";
 import "package:inventree/inventree/stock.dart";
-import "package:inventree/widget/location_display.dart";
+import "package:inventree/widget/stock/location_display.dart";
 import "package:inventree/widget/paginator.dart";
 
 import "package:inventree/widget/refreshable_state.dart";
diff --git a/lib/widget/stock_detail.dart b/lib/widget/stock/stock_detail.dart
similarity index 98%
rename from lib/widget/stock_detail.dart
rename to lib/widget/stock/stock_detail.dart
index b917e01a..d9b4edf2 100644
--- a/lib/widget/stock_detail.dart
+++ b/lib/widget/stock/stock_detail.dart
@@ -16,16 +16,16 @@ import "package:inventree/inventree/company.dart";
 import "package:inventree/inventree/stock.dart";
 import "package:inventree/inventree/part.dart";
 
-import "package:inventree/widget/supplier_part_detail.dart";
+import "package:inventree/widget/company/supplier_part_detail.dart";
 import "package:inventree/widget/dialogs.dart";
 import "package:inventree/widget/attachment_widget.dart";
-import "package:inventree/widget/location_display.dart";
-import "package:inventree/widget/part_detail.dart";
+import "package:inventree/widget/stock/location_display.dart";
+import "package:inventree/widget/part/part_detail.dart";
 import "package:inventree/widget/progress.dart";
 import "package:inventree/widget/refreshable_state.dart";
 import "package:inventree/widget/snacks.dart";
-import "package:inventree/widget/stock_item_history.dart";
-import "package:inventree/widget/stock_item_test_results.dart";
+import "package:inventree/widget/stock/stock_item_history.dart";
+import "package:inventree/widget/stock/stock_item_test_results.dart";
 import "package:inventree/widget/notes_widget.dart";
 
 
diff --git a/lib/widget/stock_item_history.dart b/lib/widget/stock/stock_item_history.dart
similarity index 100%
rename from lib/widget/stock_item_history.dart
rename to lib/widget/stock/stock_item_history.dart
diff --git a/lib/widget/stock_item_test_results.dart b/lib/widget/stock/stock_item_test_results.dart
similarity index 100%
rename from lib/widget/stock_item_test_results.dart
rename to lib/widget/stock/stock_item_test_results.dart
diff --git a/lib/widget/stock_list.dart b/lib/widget/stock/stock_list.dart
similarity index 98%
rename from lib/widget/stock_list.dart
rename to lib/widget/stock/stock_list.dart
index 3fa474ab..c24c35ca 100644
--- a/lib/widget/stock_list.dart
+++ b/lib/widget/stock/stock_list.dart
@@ -5,7 +5,7 @@ import "package:inventree/inventree/stock.dart";
 import "package:inventree/widget/paginator.dart";
 import "package:inventree/widget/refreshable_state.dart";
 import "package:inventree/l10.dart";
-import "package:inventree/widget/stock_detail.dart";
+import "package:inventree/widget/stock/stock_detail.dart";
 import "package:inventree/api.dart";