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 { // 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 goToDetailPage(BuildContext context) async { + return Navigator.push( + context, + MaterialPageRoute( + builder: (context) => CompanyDetailWidget(this) + ) + ); + } + @override List 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 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 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> 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 get rolesRequired => ["part"]; + // Navigate to a detail page for this item + @override + Future goToDetailPage(BuildContext context) async { + // Default implementation does not do anything... + return Navigator.push( + context, + MaterialPageRoute( + builder: (context) => CategoryDisplayWidget(this) + ) + ); + } + @override Map> formFields() { @@ -202,6 +216,18 @@ class InvenTreePart extends InvenTreeModel { @override List get rolesRequired => ["part"]; + // Navigate to a detail page for this item + @override + Future goToDetailPage(BuildContext context) async { + // Default implementation does not do anything... + return Navigator.push( + context, + MaterialPageRoute( + builder: (context) => PartDetailWidget(this) + ) + ); + } + @override Map> 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 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 json) : super.fromJson(json); + + @override + InvenTreeModel createFromJson(Map json) => InvenTreePOExtraLineItem.fromJson(json); + + @override + String get URL => "order/po-extra-line/"; + + @override + List get rolesRequired => ["purchase_order"]; + + @override + Future 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 goToDetailPage(BuildContext context) async { + return Navigator.push( + context, + MaterialPageRoute( + builder: (context) => SalesOrderDetailWidget(this) + ) + ); + } + @override Map> formFields() { Map> fields = { @@ -239,6 +252,31 @@ class InvenTreeSOLineItem extends InvenTreeOrderLine { } +class InvenTreeSOExtraLineItem extends InvenTreeExtraLineItem { + InvenTreeSOExtraLineItem() : super(); + + InvenTreeSOExtraLineItem.fromJson(Map json) : super.fromJson(json); + + @override + InvenTreeModel createFromJson(Map json) => InvenTreeSOExtraLineItem.fromJson(json); + + @override + String get URL => "order/so-extra-line/"; + + @override + List get rolesRequired => ["sales_order"]; + + @override + Future 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 get rolesRequired => ["stock"]; + @override + Future 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 transferFields() { Map fields = { @@ -648,6 +661,16 @@ class InvenTreeStockLocation extends InvenTreeModel { String get pathstring => getString("pathstring"); + @override + Future goToDetailPage(BuildContext context) async { + return Navigator.push( + context, + MaterialPageRoute( + builder: (context) => LocationDisplayWidget(this) + ) + ); + } + @override Map> formFields() { Map> 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 { 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 { 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 { 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 { 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 PartDetailWidget(part))); + part.goToDetailPage(context); } }, ) @@ -134,9 +130,7 @@ class _ManufacturerPartDisplayState extends RefreshableState 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 PartDetailWidget(part))); + part.goToDetailPage(context); } }, ) @@ -169,9 +166,7 @@ class _SupplierPartDisplayState extends RefreshableState CompanyDetailWidget(supplier) - )); + supplier.goToDetailPage(context); } } ) @@ -200,9 +195,7 @@ class _SupplierPartDisplayState extends RefreshableState 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 { + + _ExtraLineDetailWidgetState(); + + @override + String getAppBarTitle() => L10().extraLineItem; + + @override + List appBarActions(BuildContext context) { + List 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 request(BuildContext context) async { + await widget.item.reload(); + } + + // Callback to edit this line item + Future _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 getTiles(BuildContext context) { + List 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 filters; + + @override + _PurchaseOrderExtraLineListWidgetState createState() => _PurchaseOrderExtraLineListWidgetState(); +} + +class _PurchaseOrderExtraLineListWidgetState extends RefreshableState { + + _PurchaseOrderExtraLineListWidgetState(); + + @override + String getAppBarTitle() => L10().extraLineItems; + + Future _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 actionButtons(BuildContext context) { + List 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 filters) : super(filters: filters); + + @override + String get searchTitle => L10().extraLineItems; + + @override + _PaginatedPOExtraLineListState createState() => _PaginatedPOExtraLineListState(); + +} + +class _PaginatedPOExtraLineListState extends PaginatedSearchState { + + _PaginatedPOExtraLineListState() : super(); + + @override + String get prefix => "po_extra_line_"; + + @override + Future requestPage(int limit, int offset, Map 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 { hideLoadingOverlay(); if (part is InvenTreePart) { - Navigator.push(context, MaterialPageRoute(builder: (context) => PartDetailWidget(part))); + part.goToDetailPage(context); } }, ) @@ -187,14 +185,8 @@ class _POLineDetailWidgetState extends RefreshableState { 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 lines = []; + int extraLineCount = 0; InvenTreeStockLocation? destination; int completedLines = 0; - int attachmentCount = 0; bool showCameraShortcut = true; @@ -296,6 +296,15 @@ class _PurchaseOrderDetailState extends RefreshableState CompanyDetailWidget(supplier) - ) - ); + supplier.goToDetailPage(context); }, )); } @@ -415,6 +419,21 @@ class _PurchaseOrderDetailState extends RefreshableState { + 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 PurchaseOrderDetailWidget(order) - ) - ); + order.goToDetailPage(context); } } ); @@ -184,12 +177,7 @@ class _PaginatedPurchaseOrderListState extends PaginatedSearchState 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 { _SalesOrderDetailState(); List lines = []; + int extraLineCount = 0; bool showCameraShortcut = true; bool supportsProjectCodes = false; @@ -270,6 +271,15 @@ class _SalesOrderDetailState extends RefreshableState { }); } }); + + // 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 { 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 { 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 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 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 filters; + + @override + _SalesOrderExtraLineListWidgetState createState() => _SalesOrderExtraLineListWidgetState(); +} + +class _SalesOrderExtraLineListWidgetState extends RefreshableState { + + _SalesOrderExtraLineListWidgetState(); + + @override + String getAppBarTitle() => L10().extraLineItems; + + Future _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 actionButtons(BuildContext context) { + List 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 filters) : super(filters: filters); + + @override + String get searchTitle => L10().extraLineItems; + + @override + _PaginatedSOExtraLineListState createState() => _PaginatedSOExtraLineListState(); + +} + +class _PaginatedSOExtraLineListState extends PaginatedSearchState { + + _PaginatedSOExtraLineListState() : super(); + + @override + String get prefix => "so_extra_line_"; + + @override + Future requestPage(int limit, int offset, Map 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 { 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 extends Sta // Pagination controller final PagingController _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 { 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 { hideLoadingOverlay(); if (cat is InvenTreePartCategory) { - Navigator.push(context, MaterialPageRoute(builder: (context) => CategoryDisplayWidget(cat))); + cat.goToDetailPage(context); } } }, @@ -255,13 +254,9 @@ class _CategoryDisplayState extends RefreshableState { 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 { 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 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 { height: 32, ), onTap: () { - Navigator.push( - context, - MaterialPageRoute(builder: (context) => PartDetailWidget(parentPart!)) - ); + parentPart?.goToDetailPage(context); } ) ); @@ -371,8 +367,7 @@ class _PartDisplayState extends RefreshableState { hideLoadingOverlay(); if (cat is InvenTreePartCategory) { - Navigator.push(context, MaterialPageRoute( - builder: (context) => CategoryDisplayWidget(cat))); + cat.goToDetailPage(context); } } }, @@ -674,13 +669,7 @@ class _PartDisplayState extends RefreshableState { 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 { ), 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 { 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 { 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 { 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 { 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 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 { hideLoadingOverlay(); if (part is InvenTreePart) { - Navigator.push(context, MaterialPageRoute(builder: (context) => PartDetailWidget(part))); + part.goToDetailPage(context); } } }, @@ -574,8 +570,7 @@ class _StockItemDisplayState extends RefreshableState { hideLoadingOverlay(); if (loc is InvenTreeStockLocation) { - Navigator.push(context, MaterialPageRoute( - builder: (context) => LocationDisplayWidget(loc))); + loc.goToDetailPage(context); } } }, @@ -690,9 +685,7 @@ class _StockItemDisplayState extends RefreshableState { 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 { 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 StockDetailWidget(item))); + item.goToDetailPage(context); }, ); }