diff --git a/lib/inventree/purchase_order.dart b/lib/inventree/purchase_order.dart index 54a2cb9c..877bbaba 100644 --- a/lib/inventree/purchase_order.dart +++ b/lib/inventree/purchase_order.dart @@ -1,3 +1,5 @@ +import 'package:inventree/inventree/company.dart'; + import 'model.dart'; // TODO: In the future, status codes should be retrieved from the server @@ -8,7 +10,7 @@ const int PO_STATUS_CANCELLED = 40; const int PO_STATUS_LOST = 50; const int PO_STATUS_RETURNED = 60; -class InvenTreePO extends InvenTreeModel { +class InvenTreePurchaseOrder extends InvenTreeModel { @override String get URL => "order/po/"; @@ -26,7 +28,14 @@ class InvenTreePO extends InvenTreeModel { }; } - InvenTreePO() : super(); + InvenTreePurchaseOrder() : super(); + + @override + Map defaultListFilters() { + return { + "supplier_detail": "true", + }; + } String get issueDate => jsondata['issue_date'] ?? ""; @@ -44,7 +53,14 @@ class InvenTreePO extends InvenTreeModel { int get responsible => jsondata['responsible'] ?? -1; - int get supplier => jsondata['supplier'] ?? -1; + int get supplierId => jsondata['supplier'] ?? -1; + + InvenTreeCompany get supplier { + + dynamic supplier_detail = jsondata["supplier_detail"] ?? {}; + + return InvenTreeCompany.fromJson(supplier_detail); + } String get supplierReference => jsondata['supplier_reference'] ?? ""; @@ -56,11 +72,11 @@ class InvenTreePO extends InvenTreeModel { bool get isFailed => this.status == PO_STATUS_CANCELLED || this.status == PO_STATUS_LOST || this.status == PO_STATUS_RETURNED; - InvenTreePO.fromJson(Map json) : super.fromJson(json); + InvenTreePurchaseOrder.fromJson(Map json) : super.fromJson(json); @override InvenTreeModel createFromJson(Map json) { - return InvenTreePO.fromJson(json); + return InvenTreePurchaseOrder.fromJson(json); } } diff --git a/lib/widget/home.dart b/lib/widget/home.dart index 39609ccf..bb136658 100644 --- a/lib/widget/home.dart +++ b/lib/widget/home.dart @@ -15,6 +15,7 @@ import 'package:inventree/settings/login.dart'; import 'package:inventree/widget/category_display.dart'; import 'package:inventree/widget/company_list.dart'; import 'package:inventree/widget/location_display.dart'; +import 'package:inventree/widget/purchase_order_list.dart'; import 'package:inventree/widget/search.dart'; import 'package:inventree/widget/spinner.dart'; import 'package:inventree/widget/drawer.dart'; @@ -85,6 +86,14 @@ class _InvenTreeHomePageState extends State { void _showPurchaseOrders(BuildContext context) { if (!InvenTreeAPI().checkConnection(context)) return; + + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => PurchaseOrderListWidget( + ) + ) + ); } @@ -238,7 +247,7 @@ class _InvenTreeHomePageState extends State { children: [ FaIcon( icon, - color: COLOR_CLICK, + color: callback == null ? Colors.grey : COLOR_CLICK, ), Divider( height: 10, @@ -334,6 +343,10 @@ class _InvenTreeHomePageState extends State { _showPurchaseOrders(context); } ), + _iconButton( + L10().salesOrders, + FontAwesomeIcons.truck, + ), _iconButton( L10().suppliers, FontAwesomeIcons.building, diff --git a/lib/widget/purchase_order_list.dart b/lib/widget/purchase_order_list.dart new file mode 100644 index 00000000..ed2bf7e8 --- /dev/null +++ b/lib/widget/purchase_order_list.dart @@ -0,0 +1,203 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; +import 'package:inventree/inventree/sentry.dart'; +import 'package:inventree/widget/paginator.dart'; +import 'package:inventree/widget/refreshable_state.dart'; + +import '../l10.dart'; + +import 'package:inventree/api.dart'; +import 'package:inventree/inventree/purchase_order.dart'; + +/* + * Widget class for displaying a list of Purchase Orders + */ +class PurchaseOrderListWidget extends StatefulWidget { + + PurchaseOrderListWidget({this.filters = const {}, Key? key}) : super(key: key); + + final Map filters; + + @override + _PurchaseOrderListWidgetState createState() => _PurchaseOrderListWidgetState(filters); +} + + +class _PurchaseOrderListWidgetState extends RefreshableState { + + _PurchaseOrderListWidgetState(this.filters); + + final Map filters; + + @override + String getAppBarTitle(BuildContext context) => L10().purchaseOrders; + + @override + Widget getBody(BuildContext context) { + return _PaginatedPurchaseOrderList(filters); + } +} + + +class _PaginatedPurchaseOrderList extends StatefulWidget { + + _PaginatedPurchaseOrderList(this.filters, {this.onTotalChanged}); + + final Map filters; + + Function(int)? onTotalChanged; + + @override + _PaginatedPurchaseOrderListState createState() => _PaginatedPurchaseOrderListState(filters, onTotalChanged); + +} + + +class _PaginatedPurchaseOrderListState extends State<_PaginatedPurchaseOrderList> { + + _PaginatedPurchaseOrderListState(this.filters, this.onTotalChanged); + + static const _pageSize = 25; + + String _searchTerm = ""; + + Function(int)? onTotalChanged; + + final Map filters; + + final PagingController _pagingController = PagingController(firstPageKey: 0); + + final TextEditingController searchController = TextEditingController(); + + @override + void initState() { + _pagingController.addPageRequestListener((pageKey) { + _fetchPage(pageKey); + }); + + super.initState(); + } + + @override + void dispose() { + _pagingController.dispose(); + super.dispose(); + } + + int resultCount = 0; + + Future _fetchPage(int pageKey) async { + try { + Map params = {}; + + params["search"] = _searchTerm; + + // Only return results for open purchase orders + params["outstanding"] = "true"; + + // Copy across provided filters + for (String key in filters.keys) { + params[key] = filters[key] ?? ''; + } + + final page = await InvenTreePurchaseOrder().listPaginated( + _pageSize, + pageKey, + filters: params + ); + + int pageLength = page?.length ?? 0; + int pageCount = page?.count ?? 0; + + final isLastPage = pageLength < _pageSize; + + List orders = []; + + if (page != null) { + for (var result in page.results) { + if (result is InvenTreePurchaseOrder) { + orders.add(result); + } else { + print("Result is not valid PurchaseOrder:"); + print(result.jsondata); + } + } + } + + if (isLastPage) { + _pagingController.appendLastPage(orders); + } else { + final int nextPageKey = pageKey + pageLength; + _pagingController.appendPage(orders, nextPageKey); + } + + if (onTotalChanged != null) { + onTotalChanged!(pageCount); + } + + setState(() { + resultCount = pageCount; + }); + } catch (error, stackTrace) { + print("Error! - ${error.toString()}"); + _pagingController.error = error; + + sentryReportError(error, stackTrace); + } + } + + void updateSearchTerm() { + _searchTerm = searchController.text; + _pagingController.refresh(); + } + + Widget _buildOrder(BuildContext context, InvenTreePurchaseOrder order) { + + var supplier = order.supplier; + + return ListTile( + title: Text(order.reference), + subtitle: Text(order.description), + leading: InvenTreeAPI().getImage( + supplier.thumbnail, + width: 40, + height: 40, + ), + onTap: () async { + // TODO - Display purchase order information + }, + ); + } + + @override + Widget build(BuildContext context) { + return Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + PaginatedSearchWidget(searchController, updateSearchTerm, resultCount), + Expanded( + child: CustomScrollView( + shrinkWrap: true, + physics: ClampingScrollPhysics(), + scrollDirection: Axis.vertical, + slivers: [ + PagedSliverList.separated( + pagingController: _pagingController, + builderDelegate: PagedChildBuilderDelegate( + itemBuilder: (context, item, index) { + return _buildOrder(context, item); + }, + noItemsFoundIndicatorBuilder: (context) { + return NoResultsWidget(L10().companyNoResults); + } + ), + separatorBuilder: (context, index) => const Divider(height: 1), + ) + ], + ) + ) + ], + ); + } +} \ No newline at end of file