From 72a78291b23c1fe38996e2c76e9a8228aa1bc367 Mon Sep 17 00:00:00 2001
From: Oliver <oliver.henry.walters@gmail.com>
Date: Tue, 15 Apr 2025 20:49:05 +1000
Subject: [PATCH] Order extra lines (#632)

* Define classes for extra line item

* Display PO extra line items

- Also, some refactoring

* Support extra line items for sales order

* linting fixes

* Update release notes
---
 assets/release_notes.md                       |   2 +
 lib/api_form.dart                             |   2 +-
 lib/inventree/company.dart                    |  14 ++-
 lib/inventree/model.dart                      |   8 ++
 lib/inventree/orders.dart                     |  39 ++++++
 lib/inventree/part.dart                       |  26 ++++
 lib/inventree/purchase_order.dart             |  43 ++++++-
 lib/inventree/sales_order.dart                |  42 ++++++-
 lib/inventree/stock.dart                      |  23 ++++
 lib/l10n/app_en.arb                           |   6 +
 lib/widget/company/company_detail.dart        |  20 +--
 lib/widget/company/company_list.dart          |  11 +-
 .../company/manufacturer_part_detail.dart     |  10 +-
 lib/widget/company/supplier_part_detail.dart  |  13 +-
 lib/widget/order/extra_line_detail.dart       | 112 +++++++++++++++++
 lib/widget/order/po_extra_line_list.dart      | 116 +++++++++++++++++
 lib/widget/order/po_line_detail.dart          |  14 +--
 lib/widget/order/purchase_order_detail.dart   |  35 ++++--
 lib/widget/order/purchase_order_list.dart     |  16 +--
 lib/widget/order/sales_order_detail.dart      |  34 +++--
 lib/widget/order/sales_order_list.dart        |  16 +--
 lib/widget/order/so_extra_line_list.dart      | 118 ++++++++++++++++++
 lib/widget/order/so_line_detail.dart          |   3 +-
 lib/widget/paginator.dart                     |   4 +
 lib/widget/part/bom_list.dart                 |   3 +-
 lib/widget/part/category_display.dart         |  21 +---
 lib/widget/part/category_list.dart            |   8 +-
 lib/widget/part/part_detail.dart              |  17 +--
 lib/widget/part/part_list.dart                |   3 +-
 lib/widget/part/part_suppliers.dart           |   8 +-
 lib/widget/stock/location_display.dart        |  20 +--
 lib/widget/stock/location_list.dart           |   8 +-
 lib/widget/stock/stock_detail.dart            |  17 +--
 lib/widget/stock/stock_list.dart              |   3 +-
 34 files changed, 642 insertions(+), 193 deletions(-)
 create mode 100644 lib/widget/order/extra_line_detail.dart
 create mode 100644 lib/widget/order/po_extra_line_list.dart
 create mode 100644 lib/widget/order/so_extra_line_list.dart

diff --git a/assets/release_notes.md b/assets/release_notes.md
index 40382d3a..d1c05907 100644
--- a/assets/release_notes.md
+++ b/assets/release_notes.md
@@ -2,6 +2,8 @@
 ---
 - Adds ability to create new companies from the app
 - Allow creation of line items against pending sales orders
+- Support "extra line items" for purchase orders
+- Support "extra line items" for sales orders
 - Display start date for purchase orders
 - Display start date for sales orders
 - Updated search functionality
diff --git a/lib/api_form.dart b/lib/api_form.dart
index a906d908..c1cabbd4 100644
--- a/lib/api_form.dart
+++ b/lib/api_form.dart
@@ -1463,7 +1463,7 @@ class _APIFormWidgetState extends State<APIFormWidget> {
         // Form submission / validation error
         showSnackIcon(
           L10().formError,
-          success: false
+          success: false,
         );
 
         // Update field errors
diff --git a/lib/inventree/company.dart b/lib/inventree/company.dart
index 18e543e4..072f0fe2 100644
--- a/lib/inventree/company.dart
+++ b/lib/inventree/company.dart
@@ -1,12 +1,14 @@
 import "dart:async";
 
+import "package:flutter/material.dart";
 import "package:inventree/api.dart";
 import "package:inventree/inventree/model.dart";
 import "package:inventree/inventree/purchase_order.dart";
+import "package:inventree/widget/company/company_detail.dart";
 
 
 /*
- * The InvenTreeCompany class repreents the Company model in the InvenTree database.
+ * The InvenTreeCompany class represents the Company model in the InvenTree database.
  */
 
 class InvenTreeCompany extends InvenTreeModel {
@@ -20,6 +22,16 @@ class InvenTreeCompany extends InvenTreeModel {
 
   static const String MODEL_TYPE = "company";
 
+  @override
+  Future<Object?> goToDetailPage(BuildContext context) async {
+    return Navigator.push(
+      context,
+      MaterialPageRoute(
+        builder: (context) => CompanyDetailWidget(this)
+      )
+    );
+  }
+
   @override
   List<String> get rolesRequired => ["purchase_order", "sales_order", "return_order"];
 
diff --git a/lib/inventree/model.dart b/lib/inventree/model.dart
index 0673d191..af5b64b5 100644
--- a/lib/inventree/model.dart
+++ b/lib/inventree/model.dart
@@ -48,6 +48,12 @@ class InvenTreeModel {
   // Construct an InvenTreeModel from a JSON data object
   InvenTreeModel.fromJson(this.jsondata);
 
+  // Navigate to a detail page for this item
+  Future<Object?> goToDetailPage(BuildContext context) async {
+    // Default implementation does not do anything...
+    return null;
+  }
+
   // Update whenever the model is loaded from the server
   DateTime? lastReload;
 
@@ -311,6 +317,8 @@ class InvenTreeModel {
   InvenTreeAPI get api => InvenTreeAPI();
 
   int get pk => getInt("pk");
+
+  String get pkString => pk.toString();
   
   // Some common accessors
   String get name => getString("name");
diff --git a/lib/inventree/orders.dart b/lib/inventree/orders.dart
index b22247a5..d06ddc86 100644
--- a/lib/inventree/orders.dart
+++ b/lib/inventree/orders.dart
@@ -119,4 +119,43 @@ class InvenTreeOrderLine extends InvenTreeModel {
   String get partImage => getString("thumbnail", subKey: "part_detail");
 
   String get targetDate => getDateString("target_date");
+}
+
+
+/*
+ * Generic class representing an "ExtraLineItem"
+ */
+class InvenTreeExtraLineItem extends InvenTreeModel {
+
+  InvenTreeExtraLineItem() : super();
+
+  InvenTreeExtraLineItem.fromJson(Map<String, dynamic> json) : super.fromJson(json);
+
+  int get orderId => getInt("order");
+
+  double get quantity => getDouble("quantity");
+
+  String get reference => getString("reference");
+
+  double get price => getDouble("price");
+
+  String get priceCurrency => getString("price_currency");
+
+  @override
+  Map<String, Map<String, dynamic>> formFields() {
+    return {
+      "order": {
+        // The order cannot be edited
+        "hidden": true,
+      },
+      "reference": {},
+      "description": {},
+      "quantity": {},
+      "price": {},
+      "price_currency": {},
+      "link": {},
+      "notes": {},
+    };
+  }
+
 }
\ No newline at end of file
diff --git a/lib/inventree/part.dart b/lib/inventree/part.dart
index 3070c85f..32c8e508 100644
--- a/lib/inventree/part.dart
+++ b/lib/inventree/part.dart
@@ -10,6 +10,8 @@ import "package:inventree/l10.dart";
 import "package:inventree/inventree/stock.dart";
 import "package:inventree/inventree/company.dart";
 import "package:inventree/inventree/model.dart";
+import "package:inventree/widget/part/category_display.dart";
+import "package:inventree/widget/part/part_detail.dart";
 
 
 /*
@@ -29,6 +31,18 @@ class InvenTreePartCategory extends InvenTreeModel {
   @override
   List<String> get rolesRequired => ["part"];
 
+  // Navigate to a detail page for this item
+  @override
+  Future<Object?> goToDetailPage(BuildContext context) async {
+    // Default implementation does not do anything...
+    return Navigator.push(
+        context,
+        MaterialPageRoute(
+            builder: (context) => CategoryDisplayWidget(this)
+        )
+    );
+  }
+
   @override
   Map<String, Map<String, dynamic>> formFields() {
 
@@ -202,6 +216,18 @@ class InvenTreePart extends InvenTreeModel {
   @override
   List<String> get rolesRequired => ["part"];
 
+  // Navigate to a detail page for this item
+  @override
+  Future<Object?> goToDetailPage(BuildContext context) async {
+    // Default implementation does not do anything...
+    return Navigator.push(
+        context,
+        MaterialPageRoute(
+            builder: (context) => PartDetailWidget(this)
+        )
+    );
+  }
+
   @override
   Map<String, Map<String, dynamic>> formFields() {
     return {
diff --git a/lib/inventree/purchase_order.dart b/lib/inventree/purchase_order.dart
index 72f12219..1b8bc7c5 100644
--- a/lib/inventree/purchase_order.dart
+++ b/lib/inventree/purchase_order.dart
@@ -1,10 +1,12 @@
-import "package:flutter/cupertino.dart";
+import "package:flutter/material.dart";
 import "package:flutter_tabler_icons/flutter_tabler_icons.dart";
 import "package:inventree/api.dart";
 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/widget/order/extra_line_detail.dart";
+import "package:inventree/widget/order/purchase_order_detail.dart";
 import "package:inventree/widget/progress.dart";
 
 import "package:inventree/api_form.dart";
@@ -26,6 +28,16 @@ class InvenTreePurchaseOrder extends InvenTreeOrder {
   @override
   String get URL => "order/po/";
 
+  @override
+  Future<Object?> goToDetailPage(BuildContext context) async {
+    return Navigator.push(
+        context,
+        MaterialPageRoute(
+            builder: (context) => PurchaseOrderDetailWidget(this)
+        )
+    );
+  }
+
   static const String MODEL_TYPE = "purchaseorder";
 
   @override
@@ -310,6 +322,35 @@ class InvenTreePOLineItem extends InvenTreeOrderLine {
   }
 }
 
+
+class InvenTreePOExtraLineItem extends InvenTreeExtraLineItem {
+
+  InvenTreePOExtraLineItem() : super();
+
+  InvenTreePOExtraLineItem.fromJson(Map<String, dynamic> json) : super.fromJson(json);
+
+  @override
+  InvenTreeModel createFromJson(Map<String, dynamic> json) => InvenTreePOExtraLineItem.fromJson(json);
+
+  @override
+  String get URL => "order/po-extra-line/";
+
+  @override
+  List<String> get rolesRequired => ["purchase_order"];
+
+  @override
+  Future<Object?> goToDetailPage(BuildContext context) async {
+    return Navigator.push(
+      context,
+      MaterialPageRoute(
+        builder: (context) => ExtraLineDetailWidget(this)
+      )
+    );
+  }
+
+}
+
+
 /*
  * Class representing an attachment file against a PurchaseOrder object
  */
diff --git a/lib/inventree/sales_order.dart b/lib/inventree/sales_order.dart
index ea82d786..babf2a25 100644
--- a/lib/inventree/sales_order.dart
+++ b/lib/inventree/sales_order.dart
@@ -1,12 +1,15 @@
 
 
+import "package:flutter/material.dart";
+import "package:inventree/api.dart";
 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";
 import "package:inventree/widget/progress.dart";
+import "package:inventree/widget/order/extra_line_detail.dart";
+import "package:inventree/widget/order/sales_order_detail.dart";
 
 
 /*
@@ -31,6 +34,16 @@ class InvenTreeSalesOrder extends InvenTreeOrder {
 
   String get allocate_url => "${url}allocate/";
 
+  @override
+  Future<Object?> goToDetailPage(BuildContext context) async {
+    return Navigator.push(
+      context,
+      MaterialPageRoute(
+          builder: (context) => SalesOrderDetailWidget(this)
+      )
+    );
+  }
+
   @override
   Map<String, Map<String, dynamic>> formFields() {
     Map<String, Map<String, dynamic>> fields = {
@@ -239,6 +252,31 @@ class InvenTreeSOLineItem extends InvenTreeOrderLine {
 }
 
 
+class InvenTreeSOExtraLineItem extends InvenTreeExtraLineItem {
+  InvenTreeSOExtraLineItem() : super();
+
+  InvenTreeSOExtraLineItem.fromJson(Map<String, dynamic> json) : super.fromJson(json);
+
+  @override
+  InvenTreeModel createFromJson(Map<String, dynamic> json) => InvenTreeSOExtraLineItem.fromJson(json);
+
+  @override
+  String get URL => "order/so-extra-line/";
+
+  @override
+  List<String> get rolesRequired => ["sales_order"];
+
+  @override
+  Future<Object?> goToDetailPage(BuildContext context) async {
+    return Navigator.push(
+        context,
+        MaterialPageRoute(
+            builder: (context) => ExtraLineDetailWidget(this)
+        )
+    );
+  }
+}
+
 /*
  * Class representing a sales order shipment
  */
diff --git a/lib/inventree/stock.dart b/lib/inventree/stock.dart
index 3f8ca972..96b2a2fa 100644
--- a/lib/inventree/stock.dart
+++ b/lib/inventree/stock.dart
@@ -1,11 +1,14 @@
 import "dart:async";
 
+import "package:flutter/material.dart";
 import "package:inventree/api.dart";
 import "package:inventree/helpers.dart";
 import "package:inventree/l10.dart";
 
 import "package:inventree/inventree/part.dart";
 import "package:inventree/inventree/model.dart";
+import "package:inventree/widget/stock/location_display.dart";
+import "package:inventree/widget/stock/stock_detail.dart";
 
 
 
@@ -157,6 +160,16 @@ class InvenTreeStockItem extends InvenTreeModel {
   @override
   List<String> get rolesRequired => ["stock"];
 
+  @override
+  Future<Object?> goToDetailPage(BuildContext context) async {
+    return Navigator.push(
+      context,
+      MaterialPageRoute(
+        builder: (context) => StockDetailWidget(this)
+      )
+    );
+  }
+
   // Return a set of fields to transfer this stock item via dialog
   Map<String, dynamic> transferFields() {
     Map<String, dynamic> fields = {
@@ -648,6 +661,16 @@ class InvenTreeStockLocation extends InvenTreeModel {
 
   String get pathstring => getString("pathstring");
 
+  @override
+  Future<Object?> goToDetailPage(BuildContext context) async {
+    return Navigator.push(
+      context,
+      MaterialPageRoute(
+        builder: (context) => LocationDisplayWidget(this)
+      )
+    );
+  }
+
   @override
   Map<String, Map<String, dynamic>> formFields() {
     Map<String, Map<String, dynamic>> fields = {
diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb
index effb1255..683812be 100644
--- a/lib/l10n/app_en.arb
+++ b/lib/l10n/app_en.arb
@@ -417,6 +417,12 @@
   "expiryStale": "Stale",
   "@expiryStale": {},
 
+  "extraLineItem": "Extra Line Item",
+  "@extraLineItem": {},
+
+  "extraLineItems": "Extra Line Items",
+  "@extraLineItems": {},
+
   "feedback": "Feedback",
   "@feedback": {},
 
diff --git a/lib/widget/company/company_detail.dart b/lib/widget/company/company_detail.dart
index c0ab4814..10073869 100644
--- a/lib/widget/company/company_detail.dart
+++ b/lib/widget/company/company_detail.dart
@@ -6,19 +6,15 @@ import "package:inventree/l10.dart";
 import "package:inventree/api.dart";
 import "package:inventree/app_colors.dart";
 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/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/company/supplier_part_list.dart";
-import "package:inventree/widget/order/sales_order_detail.dart";
-import "package:inventree/widget/order/purchase_order_detail.dart";
 
 
 /*
@@ -121,13 +117,7 @@ class _CompanyDetailState extends RefreshableState<CompanyDetailWidget> {
 
           if (data.containsKey("pk")) {
             var order = InvenTreeSalesOrder.fromJson(data);
-
-            Navigator.push(
-                context,
-                MaterialPageRoute(
-                    builder: (context) => SalesOrderDetailWidget(order)
-                )
-            );
+            order.goToDetailPage(context);
           }
         }
     );
@@ -150,13 +140,7 @@ class _CompanyDetailState extends RefreshableState<CompanyDetailWidget> {
 
           if (data.containsKey("pk")) {
             var order = InvenTreePurchaseOrder.fromJson(data);
-
-            Navigator.push(
-                context,
-                MaterialPageRoute(
-                    builder: (context) => PurchaseOrderDetailWidget(order)
-                )
-            );
+            order.goToDetailPage(context);
           }
         }
     );
diff --git a/lib/widget/company/company_list.dart b/lib/widget/company/company_list.dart
index e44b6bea..8b0f03ca 100644
--- a/lib/widget/company/company_list.dart
+++ b/lib/widget/company/company_list.dart
@@ -11,7 +11,6 @@ import "package:inventree/inventree/model.dart";
 
 import "package:inventree/widget/paginator.dart";
 import "package:inventree/widget/refreshable_state.dart";
-import "package:inventree/widget/company/company_detail.dart";
 
 
 /*
@@ -48,13 +47,7 @@ class _CompanyListWidgetState extends RefreshableState<CompanyListWidget> {
 
         if (data.containsKey("pk")) {
           var company = InvenTreeCompany.fromJson(data);
-
-          Navigator.push(
-            context,
-            MaterialPageRoute(
-              builder: (context) => CompanyDetailWidget(company)
-            )
-          );
+          company.goToDetailPage(context);
         }
       }
     );
@@ -137,7 +130,7 @@ class _CompanyListState extends PaginatedSearchState<PaginatedCompanyList> {
       subtitle: Text(company.description),
       leading: InvenTreeAPI().getThumbnail(company.image),
       onTap: () async {
-        Navigator.push(context, MaterialPageRoute(builder: (context) => CompanyDetailWidget(company)));
+        company.goToDetailPage(context);
       },
     );
   }
diff --git a/lib/widget/company/manufacturer_part_detail.dart b/lib/widget/company/manufacturer_part_detail.dart
index 046cc971..6d8fd095 100644
--- a/lib/widget/company/manufacturer_part_detail.dart
+++ b/lib/widget/company/manufacturer_part_detail.dart
@@ -13,9 +13,6 @@ import "package:inventree/inventree/part.dart";
 import "package:inventree/widget/refreshable_state.dart";
 import "package:inventree/widget/snacks.dart";
 import "package:inventree/widget/progress.dart";
-
-import "package:inventree/widget/part/part_detail.dart";
-import "package:inventree/widget/company/company_detail.dart";
 import "package:url_launcher/url_launcher.dart";
 
 /*
@@ -114,8 +111,7 @@ class _ManufacturerPartDisplayState extends RefreshableState<ManufacturerPartDet
             hideLoadingOverlay();
 
             if (part is InvenTreePart) {
-              Navigator.push(context, MaterialPageRoute(
-                  builder: (context) => PartDetailWidget(part)));
+              part.goToDetailPage(context);
             }
           },
         )
@@ -134,9 +130,7 @@ class _ManufacturerPartDisplayState extends RefreshableState<ManufacturerPartDet
               hideLoadingOverlay();
 
               if (supplier is InvenTreeCompany) {
-                Navigator.push(context, MaterialPageRoute(
-                    builder: (context) => CompanyDetailWidget(supplier)
-                ));
+                supplier.goToDetailPage(context);
               }
             }
         )
diff --git a/lib/widget/company/supplier_part_detail.dart b/lib/widget/company/supplier_part_detail.dart
index c287f0a6..9b8e561c 100644
--- a/lib/widget/company/supplier_part_detail.dart
+++ b/lib/widget/company/supplier_part_detail.dart
@@ -15,9 +15,7 @@ import "package:inventree/inventree/company.dart";
 import "package:inventree/widget/progress.dart";
 import "package:inventree/widget/refreshable_state.dart";
 import "package:inventree/widget/snacks.dart";
-import "package:inventree/widget/company/company_detail.dart";
 import "package:inventree/widget/company/manufacturer_part_detail.dart";
-import "package:inventree/widget/part/part_detail.dart";
 
 
 /*
@@ -126,8 +124,7 @@ class _SupplierPartDisplayState extends RefreshableState<SupplierPartDetailWidge
             hideLoadingOverlay();
 
             if (part is InvenTreePart) {
-              Navigator.push(context, MaterialPageRoute(
-                  builder: (context) => PartDetailWidget(part)));
+              part.goToDetailPage(context);
             }
           },
         )
@@ -169,9 +166,7 @@ class _SupplierPartDisplayState extends RefreshableState<SupplierPartDetailWidge
           hideLoadingOverlay();
 
           if (supplier is InvenTreeCompany) {
-            Navigator.push(context, MaterialPageRoute(
-              builder: (context) => CompanyDetailWidget(supplier)
-            ));
+            supplier.goToDetailPage(context);
           }
         }
       )
@@ -200,9 +195,7 @@ class _SupplierPartDisplayState extends RefreshableState<SupplierPartDetailWidge
             hideLoadingOverlay();
 
             if (supplier is InvenTreeCompany) {
-              Navigator.push(context, MaterialPageRoute(
-                builder: (context) => CompanyDetailWidget(supplier)
-              ));
+              supplier.goToDetailPage(context);
             }
           }
         )
diff --git a/lib/widget/order/extra_line_detail.dart b/lib/widget/order/extra_line_detail.dart
new file mode 100644
index 00000000..bdf7dbce
--- /dev/null
+++ b/lib/widget/order/extra_line_detail.dart
@@ -0,0 +1,112 @@
+import "package:flutter/material.dart";
+import "package:flutter_tabler_icons/flutter_tabler_icons.dart";
+import "package:inventree/helpers.dart";
+
+import "package:inventree/l10.dart";
+import "package:inventree/widget/refreshable_state.dart";
+import "package:inventree/widget/snacks.dart";
+
+import "package:inventree/inventree/orders.dart";
+
+
+class ExtraLineDetailWidget extends StatefulWidget {
+  const ExtraLineDetailWidget(this.item, {Key? key}) : super(key: key);
+
+  final InvenTreeExtraLineItem item;
+
+  @override
+  _ExtraLineDetailWidgetState createState() => _ExtraLineDetailWidgetState();
+}
+
+class _ExtraLineDetailWidgetState extends RefreshableState<ExtraLineDetailWidget> {
+
+  _ExtraLineDetailWidgetState();
+
+  @override
+  String getAppBarTitle() => L10().extraLineItem;
+
+  @override
+  List<Widget> appBarActions(BuildContext context) {
+    List<Widget> actions = [];
+
+    if (widget.item.canEdit) {
+      actions.add(
+        IconButton(
+          icon: Icon(TablerIcons.edit),
+          onPressed: () {
+            _editLineItem(context);
+          }
+        )
+      );
+    }
+
+    return actions;
+  }
+
+  // Function to request data for this page
+  @override
+  Future<void> request(BuildContext context) async {
+    await widget.item.reload();
+  }
+
+  // Callback to edit this line item
+  Future<void> _editLineItem(BuildContext context) async {
+    var fields = widget.item.formFields();
+
+    widget.item.editForm(
+        context,
+        L10().editLineItem,
+        fields: fields,
+        onSuccess: (data) async {
+          refresh(context);
+          showSnackIcon(L10().lineItemUpdated, success: true);
+        }
+    );
+  }
+
+  @override
+  List<Widget> getTiles(BuildContext context) {
+    List<Widget> tiles = [];
+
+    tiles.add(
+        ListTile(
+          title: Text(L10().reference),
+          trailing: Text(widget.item.reference),
+        )
+    );
+
+    tiles.add(
+        ListTile(
+          title: Text(L10().description),
+          trailing: Text(widget.item.description),
+        )
+    );
+
+    tiles.add(
+      ListTile(
+        title: Text(L10().quantity),
+        trailing: Text(widget.item.quantity.toString()),
+      )
+    );
+
+    tiles.add(
+      ListTile(
+        title: Text(L10().unitPrice),
+        trailing: Text(
+          renderCurrency(widget.item.price, widget.item.priceCurrency)
+        )
+      )
+    );
+
+    if (widget.item.notes.isNotEmpty) {
+      tiles.add(
+        ListTile(
+          title: Text(L10().notes),
+          subtitle: Text(widget.item.notes),
+        )
+      );
+    }
+
+    return tiles;
+  }
+}
\ No newline at end of file
diff --git a/lib/widget/order/po_extra_line_list.dart b/lib/widget/order/po_extra_line_list.dart
new file mode 100644
index 00000000..4c382976
--- /dev/null
+++ b/lib/widget/order/po_extra_line_list.dart
@@ -0,0 +1,116 @@
+import "package:flutter/material.dart";
+import "package:flutter_speed_dial/flutter_speed_dial.dart";
+import "package:flutter_tabler_icons/flutter_tabler_icons.dart";
+
+import "package:inventree/l10.dart";
+import "package:inventree/inventree/model.dart";
+import "package:inventree/inventree/purchase_order.dart";
+import "package:inventree/widget/paginator.dart";
+import "package:inventree/widget/refreshable_state.dart";
+import "package:inventree/widget/snacks.dart";
+
+
+class POExtraLineListWidget extends StatefulWidget {
+
+  const POExtraLineListWidget(this.order, {this.filters = const {}, Key? key}) : super(key: key);
+
+  final InvenTreePurchaseOrder order;
+
+  final Map<String, String> filters;
+
+  @override
+  _PurchaseOrderExtraLineListWidgetState createState() => _PurchaseOrderExtraLineListWidgetState();
+}
+
+class _PurchaseOrderExtraLineListWidgetState extends RefreshableState<POExtraLineListWidget> {
+
+  _PurchaseOrderExtraLineListWidgetState();
+
+  @override
+  String getAppBarTitle() => L10().extraLineItems;
+
+  Future<void> _addLineItem(BuildContext context) async {
+
+    var fields = InvenTreePOExtraLineItem().formFields();
+
+    fields["order"]?["value"] = widget.order.pk;
+
+    InvenTreePOExtraLineItem().createForm(
+      context,
+      L10().lineItemAdd,
+      fields: fields,
+      onSuccess: (data) async {
+        refresh(context);
+        showSnackIcon(L10().lineItemUpdated, success: true);
+      }
+    );
+  }
+
+  @override
+  List<SpeedDialChild> actionButtons(BuildContext context) {
+    List<SpeedDialChild> actions = [];
+
+    if (widget.order.canEdit) {
+      actions.add(
+        SpeedDialChild(
+          child: Icon(TablerIcons.circle_plus, color: Colors.green),
+          label: L10().lineItemAdd,
+          onTap: () {
+            _addLineItem(context);
+          }
+        )
+      );
+    }
+
+    return actions;
+  }
+
+  @override
+  Widget getBody(BuildContext context) {
+    return PaginatedPOExtraLineList(widget.filters);
+  }
+}
+
+
+class PaginatedPOExtraLineList extends PaginatedSearchWidget {
+
+  const PaginatedPOExtraLineList(Map<String, String> filters) : super(filters: filters);
+
+  @override
+  String get searchTitle => L10().extraLineItems;
+
+  @override
+  _PaginatedPOExtraLineListState createState() => _PaginatedPOExtraLineListState();
+
+}
+
+class _PaginatedPOExtraLineListState extends PaginatedSearchState<PaginatedPOExtraLineList> {
+
+  _PaginatedPOExtraLineListState() : super();
+
+  @override
+  String get prefix => "po_extra_line_";
+
+  @override
+  Future<InvenTreePageResponse?> requestPage(int limit, int offset, Map<String, String> params) async {
+    final page = await InvenTreePOExtraLineItem().listPaginated(limit, offset, filters: params);
+    return page;
+  }
+
+  @override
+  Widget buildItem(BuildContext context, InvenTreeModel model) {
+
+    InvenTreePOExtraLineItem line = model as InvenTreePOExtraLineItem;
+
+    return ListTile(
+      title: Text(line.reference),
+      subtitle: Text(line.description),
+      trailing: Text(line.quantity.toString()),
+      onTap: () {
+        line.goToDetailPage(context).then((_) {
+          refresh();
+        });
+      },
+    );
+  }
+}
\ No newline at end of file
diff --git a/lib/widget/order/po_line_detail.dart b/lib/widget/order/po_line_detail.dart
index 6ae896e1..689def82 100644
--- a/lib/widget/order/po_line_detail.dart
+++ b/lib/widget/order/po_line_detail.dart
@@ -13,8 +13,6 @@ import "package:inventree/inventree/purchase_order.dart";
 import "package:inventree/inventree/stock.dart";
 
 import "package:inventree/widget/progress.dart";
-import "package:inventree/widget/part/part_detail.dart";
-import "package:inventree/widget/stock/location_display.dart";
 import "package:inventree/widget/refreshable_state.dart";
 import "package:inventree/widget/snacks.dart";
 import "package:inventree/widget/company/supplier_part_detail.dart";
@@ -157,7 +155,7 @@ class _POLineDetailWidgetState extends RefreshableState<POLineDetailWidget> {
           hideLoadingOverlay();
 
           if (part is InvenTreePart) {
-            Navigator.push(context, MaterialPageRoute(builder: (context) => PartDetailWidget(part)));
+            part.goToDetailPage(context);
           }
         },
       )
@@ -187,14 +185,8 @@ class _POLineDetailWidgetState extends RefreshableState<POLineDetailWidget> {
           title: Text(L10().destination),
           subtitle: Text(destination!.name),
           leading: Icon(TablerIcons.map_pin, color: COLOR_ACTION),
-          onTap: () =>
-          {
-            Navigator.push(
-                context,
-                MaterialPageRoute(
-                    builder: (context) => LocationDisplayWidget(destination)
-                )
-            )
+          onTap: () => {
+            destination!.goToDetailPage(context)
           }
       ));
     }
diff --git a/lib/widget/order/purchase_order_detail.dart b/lib/widget/order/purchase_order_detail.dart
index 2afc66dc..12332ae1 100644
--- a/lib/widget/order/purchase_order_detail.dart
+++ b/lib/widget/order/purchase_order_detail.dart
@@ -14,12 +14,12 @@ import "package:inventree/inventree/stock.dart";
 import "package:inventree/inventree/purchase_order.dart";
 
 import "package:inventree/widget/dialogs.dart";
+import "package:inventree/widget/order/po_extra_line_list.dart";
 import "package:inventree/widget/stock/location_display.dart";
 import "package:inventree/widget/order/po_line_list.dart";
 
 
 import "package:inventree/widget/attachment_widget.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";
@@ -47,11 +47,11 @@ class _PurchaseOrderDetailState extends RefreshableState<PurchaseOrderDetailWidg
   _PurchaseOrderDetailState();
   
   List<InvenTreePOLineItem> lines = [];
+  int extraLineCount = 0;
 
   InvenTreeStockLocation? destination;
 
   int completedLines = 0;
-
   int attachmentCount = 0;
 
   bool showCameraShortcut = true;
@@ -296,6 +296,15 @@ class _PurchaseOrderDetailState extends RefreshableState<PurchaseOrderDetailWidg
         });
       }
     }
+
+    // Count number of "extra line items" against this order
+    InvenTreePOExtraLineItem().count(filters: {"order": widget.order.pk.toString() }).then((int value) {
+      if (mounted) {
+        setState(() {
+          extraLineCount = value;
+        });
+      }
+    });
   }
 
   // Edit the currently displayed PurchaseOrder
@@ -368,12 +377,7 @@ class _PurchaseOrderDetailState extends RefreshableState<PurchaseOrderDetailWidg
         subtitle: Text(supplier.name),
         leading: Icon(TablerIcons.building, color: COLOR_ACTION),
         onTap: () {
-          Navigator.push(
-            context,
-            MaterialPageRoute(
-              builder: (context) => CompanyDetailWidget(supplier)
-            )
-          );
+          supplier.goToDetailPage(context);
         },
       ));
     }
@@ -415,6 +419,21 @@ class _PurchaseOrderDetailState extends RefreshableState<PurchaseOrderDetailWidg
       trailing: Text("${completedLines} /  ${widget.order.lineItemCount}", style: TextStyle(color: lineColor)),
     ));
 
+    // Extra line items
+    tiles.add(ListTile(
+      title: Text(L10().extraLineItems),
+      leading: Icon(TablerIcons.clipboard_list, color: COLOR_ACTION),
+      trailing: Text(extraLineCount.toString()),
+      onTap: () => {
+        Navigator.push(
+          context,
+          MaterialPageRoute(
+            builder: (context) => POExtraLineListWidget(widget.order, filters: {"order": widget.order.pk.toString()})
+          )
+        )
+      },
+    ));
+
     tiles.add(ListTile(
       title: Text(L10().totalPrice),
       leading: Icon(TablerIcons.currency_dollar),
diff --git a/lib/widget/order/purchase_order_list.dart b/lib/widget/order/purchase_order_list.dart
index 2f150046..8351ac46 100644
--- a/lib/widget/order/purchase_order_list.dart
+++ b/lib/widget/order/purchase_order_list.dart
@@ -5,7 +5,6 @@ import "package:flutter_tabler_icons/flutter_tabler_icons.dart";
 import "package:inventree/inventree/company.dart";
 import "package:inventree/inventree/model.dart";
 import "package:inventree/widget/paginator.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";
@@ -69,13 +68,7 @@ class _PurchaseOrderListWidgetState extends RefreshableState<PurchaseOrderListWi
 
         if (data.containsKey("pk")) {
           var order = InvenTreePurchaseOrder.fromJson(data);
-
-          Navigator.push(
-            context,
-            MaterialPageRoute(
-              builder: (context) => PurchaseOrderDetailWidget(order)
-            )
-          );
+          order.goToDetailPage(context);
         }
       }
     );
@@ -184,12 +177,7 @@ class _PaginatedPurchaseOrderListState extends PaginatedSearchState<PaginatedPur
         ),
       ),
       onTap: () async {
-        Navigator.push(
-          context,
-          MaterialPageRoute(
-            builder: (context) => PurchaseOrderDetailWidget(order)
-          )
-        );
+        order.goToDetailPage(context);
       },
     );
   }
diff --git a/lib/widget/order/sales_order_detail.dart b/lib/widget/order/sales_order_detail.dart
index f853e68f..45af4c72 100644
--- a/lib/widget/order/sales_order_detail.dart
+++ b/lib/widget/order/sales_order_detail.dart
@@ -7,6 +7,7 @@ import "package:inventree/barcode/sales_order.dart";
 import "package:inventree/inventree/company.dart";
 import "package:inventree/inventree/sales_order.dart";
 import "package:inventree/preferences.dart";
+import "package:inventree/widget/order/so_extra_line_list.dart";
 import "package:inventree/widget/order/so_line_list.dart";
 import "package:inventree/widget/order/so_shipment_list.dart";
 import "package:inventree/widget/refreshable_state.dart";
@@ -18,7 +19,6 @@ import "package:inventree/widget/attachment_widget.dart";
 import "package:inventree/widget/dialogs.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";
 
 /*
@@ -40,6 +40,7 @@ class _SalesOrderDetailState extends RefreshableState<SalesOrderDetailWidget> {
   _SalesOrderDetailState();
 
   List<InvenTreeSOLineItem> lines = [];
+  int extraLineCount = 0;
 
   bool showCameraShortcut = true;
   bool supportsProjectCodes = false;
@@ -270,6 +271,15 @@ class _SalesOrderDetailState extends RefreshableState<SalesOrderDetailWidget> {
         });
       }
     });
+
+    // Count number of "extra line items" against this order
+    InvenTreeSOExtraLineItem().count(filters: {"order": widget.order.pk.toString() }).then((int value) {
+      if (mounted) {
+        setState(() {
+          extraLineCount = value;
+        });
+      }
+    });
   }
 
   // Edit the current SalesOrder instance
@@ -340,12 +350,7 @@ class _SalesOrderDetailState extends RefreshableState<SalesOrderDetailWidget> {
         subtitle: Text(customer.name),
         leading: Icon(TablerIcons.user, color: COLOR_ACTION),
         onTap: () {
-          Navigator.push(
-              context,
-              MaterialPageRoute(
-                  builder: (context) => CompanyDetailWidget(customer)
-              )
-          );
+          customer.goToDetailPage(context);
         }
       ));
     }
@@ -370,6 +375,21 @@ class _SalesOrderDetailState extends RefreshableState<SalesOrderDetailWidget> {
       trailing: Text("${widget.order.completedLineItemCount} / ${widget.order.lineItemCount}", style: TextStyle(color: lineColor)),
     ));
 
+    // Extra line items
+    tiles.add(ListTile(
+      title: Text(L10().extraLineItems),
+      leading: Icon(TablerIcons.clipboard_list, color: COLOR_ACTION),
+      trailing: Text(extraLineCount.toString()),
+      onTap: () => {
+        Navigator.push(
+            context,
+            MaterialPageRoute(
+                builder: (context) => SOExtraLineListWidget(widget.order, filters: {"order": widget.order.pk.toString()})
+            )
+        )
+      },
+    ));
+
     // Shipment progress
     if (widget.order.shipmentCount > 0) {
       tiles.add(ListTile(
diff --git a/lib/widget/order/sales_order_list.dart b/lib/widget/order/sales_order_list.dart
index 7366b49c..46264c47 100644
--- a/lib/widget/order/sales_order_list.dart
+++ b/lib/widget/order/sales_order_list.dart
@@ -3,7 +3,6 @@ import "package:flutter/material.dart";
 import "package:flutter_speed_dial/flutter_speed_dial.dart";
 import "package:flutter_tabler_icons/flutter_tabler_icons.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";
@@ -67,13 +66,7 @@ class _SalesOrderListWidgetState extends RefreshableState<SalesOrderListWidget>
 
           if (data.containsKey("pk")) {
             var order = InvenTreeSalesOrder.fromJson(data);
-
-            Navigator.push(
-              context,
-              MaterialPageRoute(
-                builder: (context) => SalesOrderDetailWidget(order)
-              )
-            );
+            order.goToDetailPage(context);
           }
         }
     );
@@ -167,12 +160,7 @@ class _PaginatedSalesOrderListState extends PaginatedSearchState<PaginatedSalesO
         )
       ),
       onTap: () async {
-        Navigator.push(
-          context,
-          MaterialPageRoute(
-            builder: (context) => SalesOrderDetailWidget(order)
-          )
-        );
+        order.goToDetailPage(context);
       }
     );
 
diff --git a/lib/widget/order/so_extra_line_list.dart b/lib/widget/order/so_extra_line_list.dart
new file mode 100644
index 00000000..efbce2e3
--- /dev/null
+++ b/lib/widget/order/so_extra_line_list.dart
@@ -0,0 +1,118 @@
+import "package:flutter/material.dart";
+import "package:flutter_speed_dial/flutter_speed_dial.dart";
+import "package:flutter_tabler_icons/flutter_tabler_icons.dart";
+
+import "package:inventree/l10.dart";
+
+import "package:inventree/inventree/model.dart";
+import "package:inventree/inventree/sales_order.dart";
+
+import "package:inventree/widget/paginator.dart";
+import "package:inventree/widget/refreshable_state.dart";
+import "package:inventree/widget/snacks.dart";
+
+
+class SOExtraLineListWidget extends StatefulWidget {
+
+  const SOExtraLineListWidget(this.order, {this.filters = const {}, Key? key}) : super(key: key);
+
+  final InvenTreeSalesOrder order;
+
+  final Map<String, String> filters;
+
+  @override
+  _SalesOrderExtraLineListWidgetState createState() => _SalesOrderExtraLineListWidgetState();
+}
+
+class _SalesOrderExtraLineListWidgetState extends RefreshableState<SOExtraLineListWidget> {
+
+  _SalesOrderExtraLineListWidgetState();
+
+  @override
+  String getAppBarTitle() => L10().extraLineItems;
+
+  Future<void> _addLineItem(BuildContext context) async {
+
+    var fields = InvenTreeSOExtraLineItem().formFields();
+
+    fields["order"]?["value"] = widget.order.pk;
+
+    InvenTreeSOExtraLineItem().createForm(
+        context,
+        L10().lineItemAdd,
+        fields: fields,
+        onSuccess: (data) async {
+          refresh(context);
+          showSnackIcon(L10().lineItemUpdated, success: true);
+        }
+    );
+  }
+
+  @override
+  List<SpeedDialChild> actionButtons(BuildContext context) {
+    List<SpeedDialChild> actions = [];
+
+    if (widget.order.canEdit) {
+      actions.add(
+          SpeedDialChild(
+              child: Icon(TablerIcons.circle_plus, color: Colors.green),
+              label: L10().lineItemAdd,
+              onTap: () {
+                _addLineItem(context);
+              }
+          )
+      );
+    }
+
+    return actions;
+  }
+
+  @override
+  Widget getBody(BuildContext context) {
+    return PaginatedSOExtraLineList(widget.filters);
+  }
+}
+
+
+class PaginatedSOExtraLineList extends PaginatedSearchWidget {
+
+  const PaginatedSOExtraLineList(Map<String, String> filters) : super(filters: filters);
+
+  @override
+  String get searchTitle => L10().extraLineItems;
+
+  @override
+  _PaginatedSOExtraLineListState createState() => _PaginatedSOExtraLineListState();
+
+}
+
+class _PaginatedSOExtraLineListState extends PaginatedSearchState<PaginatedSOExtraLineList> {
+
+  _PaginatedSOExtraLineListState() : super();
+
+  @override
+  String get prefix => "so_extra_line_";
+
+  @override
+  Future<InvenTreePageResponse?> requestPage(int limit, int offset, Map<String, String> params) async {
+    final page = await InvenTreeSOExtraLineItem().listPaginated(limit, offset, filters: params);
+    return page;
+  }
+
+  @override
+  Widget buildItem(BuildContext context, InvenTreeModel model) {
+
+    InvenTreeSOExtraLineItem line = model as InvenTreeSOExtraLineItem;
+
+    return ListTile(
+      title: Text(line.reference),
+      subtitle: Text(line.description),
+      trailing: Text(line.quantity.toString()),
+      onTap: () {
+        line.goToDetailPage(context).then((_) {
+          refresh();
+        });
+      },
+    );
+  }
+}
\ No newline at end of file
diff --git a/lib/widget/order/so_line_detail.dart b/lib/widget/order/so_line_detail.dart
index 9dcd218b..09545137 100644
--- a/lib/widget/order/so_line_detail.dart
+++ b/lib/widget/order/so_line_detail.dart
@@ -15,7 +15,6 @@ 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/widget/snacks.dart";
 
 import "package:inventree/app_colors.dart";
@@ -192,7 +191,7 @@ class _SOLineDetailWidgetState extends RefreshableState<SoLineDetailWidget> {
           hideLoadingOverlay();
 
           if (part is InvenTreePart) {
-            Navigator.push(context, MaterialPageRoute(builder: (context) => PartDetailWidget(part)));
+            part.goToDetailPage(context);
           }
         }
       )
diff --git a/lib/widget/paginator.dart b/lib/widget/paginator.dart
index bc3df6d6..a16a0ef4 100644
--- a/lib/widget/paginator.dart
+++ b/lib/widget/paginator.dart
@@ -257,6 +257,10 @@ abstract class PaginatedSearchState<T extends PaginatedSearchWidget> extends Sta
   // Pagination controller
   final PagingController<int, InvenTreeModel> _pagingController = PagingController(firstPageKey: 0);
 
+  void refresh() {
+    _pagingController.refresh();
+  }
+
   @override
   void initState() {
     _pagingController.addPageRequestListener((pageKey) {
diff --git a/lib/widget/part/bom_list.dart b/lib/widget/part/bom_list.dart
index 9dcbe664..77a169be 100644
--- a/lib/widget/part/bom_list.dart
+++ b/lib/widget/part/bom_list.dart
@@ -11,7 +11,6 @@ import "package:inventree/inventree/model.dart";
 import "package:inventree/inventree/part.dart";
 
 import "package:inventree/widget/paginator.dart";
-import "package:inventree/widget/part/part_detail.dart";
 import "package:inventree/widget/progress.dart";
 import "package:inventree/widget/refreshable_state.dart";
 
@@ -159,7 +158,7 @@ class _PaginatedBomListState extends PaginatedSearchState<PaginatedBomList> {
         hideLoadingOverlay();
 
         if (part is InvenTreePart) {
-          Navigator.push(context, MaterialPageRoute(builder: (context) => PartDetailWidget(part)));
+          part.goToDetailPage(context);
         }
       },
     );
diff --git a/lib/widget/part/category_display.dart b/lib/widget/part/category_display.dart
index 79dd6025..7ca833ee 100644
--- a/lib/widget/part/category_display.dart
+++ b/lib/widget/part/category_display.dart
@@ -11,7 +11,6 @@ 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/part_detail.dart";
 import "package:inventree/widget/refreshable_state.dart";
 
 
@@ -164,7 +163,7 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
                   hideLoadingOverlay();
 
                   if (cat is InvenTreePartCategory) {
-                    Navigator.push(context, MaterialPageRoute(builder: (context) => CategoryDisplayWidget(cat)));
+                    cat.goToDetailPage(context);
                   }
                 }
               },
@@ -255,13 +254,9 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
 
         if (data.containsKey("pk")) {
           var cat = InvenTreePartCategory.fromJson(data);
-
-          Navigator.push(
-            context,
-            MaterialPageRoute(
-              builder: (context) => CategoryDisplayWidget(cat)
-            )
-          );
+          cat.goToDetailPage(context).then((_) {
+            refresh(context);
+          });
         } else {
           refresh(context);
         }
@@ -285,13 +280,7 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
 
         if (data.containsKey("pk")) {
           var part = InvenTreePart.fromJson(data);
-
-          Navigator.push(
-            context,
-            MaterialPageRoute(
-              builder: (context) => PartDetailWidget(part)
-            )
-          );
+          part.goToDetailPage(context);
         }
       }
     );
diff --git a/lib/widget/part/category_list.dart b/lib/widget/part/category_list.dart
index cc5a40e2..c0aaa865 100644
--- a/lib/widget/part/category_list.dart
+++ b/lib/widget/part/category_list.dart
@@ -2,7 +2,6 @@ import "package:flutter/material.dart";
 
 import "package:inventree/inventree/model.dart";
 import "package:inventree/inventree/part.dart";
-import "package:inventree/widget/part/category_display.dart";
 import "package:inventree/widget/paginator.dart";
 import "package:inventree/widget/refreshable_state.dart";
 
@@ -100,12 +99,7 @@ class _PaginatedPartCategoryListState extends PaginatedSearchState<PaginatedPart
       trailing: Text("${category.partcount}"),
       leading: category.customIcon == null ? null : Icon(category.customIcon),
       onTap: () {
-        Navigator.push(
-          context,
-          MaterialPageRoute(
-            builder: (context) => CategoryDisplayWidget(category)
-          )
-        );
+        category.goToDetailPage(context);
       },
     );
   }
diff --git a/lib/widget/part/part_detail.dart b/lib/widget/part/part_detail.dart
index ee18c558..51c7d01e 100644
--- a/lib/widget/part/part_detail.dart
+++ b/lib/widget/part/part_detail.dart
@@ -23,7 +23,6 @@ import "package:inventree/widget/part/category_display.dart";
 import "package:inventree/widget/refreshable_state.dart";
 import "package:inventree/widget/part/part_image_widget.dart";
 import "package:inventree/widget/snacks.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";
 
@@ -347,10 +346,7 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
             height: 32,
           ),
           onTap: () {
-            Navigator.push(
-              context,
-              MaterialPageRoute(builder: (context) => PartDetailWidget(parentPart!))
-            );
+            parentPart?.goToDetailPage(context);
           }
         )
       );
@@ -371,8 +367,7 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
                 hideLoadingOverlay();
 
                 if (cat is InvenTreePartCategory) {
-                  Navigator.push(context, MaterialPageRoute(
-                      builder: (context) => CategoryDisplayWidget(cat)));
+                  cat.goToDetailPage(context);
                 }
               }
             },
@@ -674,13 +669,7 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
 
           if (data.containsKey("pk")) {
             var item = InvenTreeStockItem.fromJson(data);
-
-            Navigator.push(
-                context,
-                MaterialPageRoute(
-                    builder: (context) => StockDetailWidget(item)
-                )
-            );
+            item.goToDetailPage(context);
           }
         }
     );
diff --git a/lib/widget/part/part_list.dart b/lib/widget/part/part_list.dart
index 80dbdbd7..8433e0b7 100644
--- a/lib/widget/part/part_list.dart
+++ b/lib/widget/part/part_list.dart
@@ -7,7 +7,6 @@ import "package:inventree/inventree/model.dart";
 import "package:inventree/inventree/part.dart";
 
 import "package:inventree/widget/paginator.dart";
-import "package:inventree/widget/part/part_detail.dart";
 import "package:inventree/widget/refreshable_state.dart";
 
 
@@ -132,7 +131,7 @@ class _PaginatedPartListState extends PaginatedSearchState<PaginatedPartList> {
       ),
       leading: InvenTreeAPI().getThumbnail(part.thumbnail),
       onTap: () {
-        Navigator.push(context, MaterialPageRoute(builder: (context) => PartDetailWidget(part)));
+        part.goToDetailPage(context);
       },
     );
   }
diff --git a/lib/widget/part/part_suppliers.dart b/lib/widget/part/part_suppliers.dart
index ae3788d3..73b5cb32 100644
--- a/lib/widget/part/part_suppliers.dart
+++ b/lib/widget/part/part_suppliers.dart
@@ -7,7 +7,6 @@ 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/company_detail.dart";
 import "package:inventree/widget/refreshable_state.dart";
 
 class PartSupplierWidget extends StatefulWidget {
@@ -58,12 +57,7 @@ class _PartSupplierState extends RefreshableState<PartSupplierWidget> {
         var company = await InvenTreeCompany().get(_part.supplierId);
 
         if (company != null && company is InvenTreeCompany) {
-          Navigator.push(
-              context,
-              MaterialPageRoute(
-                  builder: (context) => CompanyDetailWidget(company)
-              )
-          );
+          company.goToDetailPage(context);
         }
       },
     );
diff --git a/lib/widget/stock/location_display.dart b/lib/widget/stock/location_display.dart
index 07a8464d..b12f834c 100644
--- a/lib/widget/stock/location_display.dart
+++ b/lib/widget/stock/location_display.dart
@@ -15,7 +15,6 @@ 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/stock_detail.dart";
 import "package:inventree/widget/stock/stock_list.dart";
 import "package:inventree/labels.dart";
 
@@ -279,13 +278,7 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
 
           if (data.containsKey("pk")) {
             var loc = InvenTreeStockLocation.fromJson(data);
-
-            Navigator.push(
-                context,
-                MaterialPageRoute(
-                    builder: (context) => LocationDisplayWidget(loc)
-                )
-            );
+            loc.goToDetailPage(context);
           }
         }
     );
@@ -317,13 +310,7 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
 
           if (data.containsKey("pk")) {
             var item = InvenTreeStockItem.fromJson(data);
-
-            Navigator.push(
-                context,
-                MaterialPageRoute(
-                    builder: (context) => StockDetailWidget(item)
-                )
-            );
+            item.goToDetailPage(context);
           }
         }
     );
@@ -367,8 +354,7 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
                   hideLoadingOverlay();
 
                   if (loc is InvenTreeStockLocation) {
-                    Navigator.push(context, MaterialPageRoute(
-                        builder: (context) => LocationDisplayWidget(loc)));
+                    loc.goToDetailPage(context);
                   }
                 }
               },
diff --git a/lib/widget/stock/location_list.dart b/lib/widget/stock/location_list.dart
index c8506a9c..05cb204a 100644
--- a/lib/widget/stock/location_list.dart
+++ b/lib/widget/stock/location_list.dart
@@ -2,7 +2,6 @@ import "package:flutter/material.dart";
 
 import "package:inventree/inventree/model.dart";
 import "package:inventree/inventree/stock.dart";
-import "package:inventree/widget/stock/location_display.dart";
 import "package:inventree/widget/paginator.dart";
 
 import "package:inventree/widget/refreshable_state.dart";
@@ -87,12 +86,7 @@ class _PaginatedStockLocationListState extends PaginatedSearchState<PaginatedSto
       trailing: Text("${location.itemcount}"),
       leading: location.customIcon == null ? null : Icon(location.customIcon),
       onTap: () {
-        Navigator.push(
-          context,
-          MaterialPageRoute(
-            builder: (context) => LocationDisplayWidget(location)
-          )
-        );
+        location.goToDetailPage(context);
       },
     );
   }
diff --git a/lib/widget/stock/stock_detail.dart b/lib/widget/stock/stock_detail.dart
index 50331f2d..a3006fd5 100644
--- a/lib/widget/stock/stock_detail.dart
+++ b/lib/widget/stock/stock_detail.dart
@@ -17,14 +17,10 @@ import "package:inventree/preferences.dart";
 import "package:inventree/inventree/company.dart";
 import "package:inventree/inventree/stock.dart";
 import "package:inventree/inventree/part.dart";
-import "package:inventree/widget/company/company_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/order/sales_order_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";
@@ -531,7 +527,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
             hideLoadingOverlay();
 
             if (part is InvenTreePart) {
-              Navigator.push(context, MaterialPageRoute(builder: (context) => PartDetailWidget(part)));
+              part.goToDetailPage(context);
             }
           }
         },
@@ -574,8 +570,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
               hideLoadingOverlay();
 
               if (loc is InvenTreeStockLocation) {
-                Navigator.push(context, MaterialPageRoute(
-                    builder: (context) => LocationDisplayWidget(loc)));
+                loc.goToDetailPage(context);
               }
             }
           },
@@ -690,9 +685,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
           leading: Icon(TablerIcons.truck_delivery, color: COLOR_ACTION),
           trailing: Text(salesOrder?.reference ?? ""),
           onTap: () {
-            Navigator.push(context, MaterialPageRoute(
-              builder: (context) => SalesOrderDetailWidget(salesOrder!)
-            ));
+            salesOrder?.goToDetailPage(context);
           }
         )
       );
@@ -706,9 +699,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
           leading: Icon(TablerIcons.building_store, color: COLOR_ACTION),
           trailing: Text(customer?.name ?? ""),
           onTap: () {
-            Navigator.push(context, MaterialPageRoute(
-              builder: (context) => CompanyDetailWidget(customer!)
-            ));
+            customer?.goToDetailPage(context);
           },
         )
       );
diff --git a/lib/widget/stock/stock_list.dart b/lib/widget/stock/stock_list.dart
index dd36a303..044700fd 100644
--- a/lib/widget/stock/stock_list.dart
+++ b/lib/widget/stock/stock_list.dart
@@ -5,7 +5,6 @@ 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/stock_detail.dart";
 import "package:inventree/api.dart";
 
 
@@ -146,7 +145,7 @@ class _PaginatedStockItemListState extends PaginatedSearchState<PaginatedStockIt
         )
       ),
       onTap: () {
-        Navigator.push(context, MaterialPageRoute(builder: (context) => StockDetailWidget(item)));
+        item.goToDetailPage(context);
       },
     );
   }