mirror of
				https://github.com/inventree/inventree-app.git
				synced 2025-10-30 04:45:39 +00:00 
			
		
		
		
	Sales order support (#438)
* Add new models for SalesOrder - Create generic Order and OrderLine models with common functionality * Refactor - Move some widgets around - Cleanup directory structure * Add link to home screen and nav drawer * Add SalesOrder list widget * Linting fixes * Fix string * Refactor PurchaseOrderDetailWidget * Tweaks to existing code * linting * Fixes for drawer widget * Add "detail" page for SalesOrder * Add more tiles to SalesOrder detail * Allow editing of salesorder * add list filters for sales orders * Display list of line items * Customer updates - Display customer icon on home screen - Fetch sales orders for customer detail page * Cleanup company detail view * Create new sales order from list * Stricter typing for formFields method * Create new PurchaseOrder and SalesOrder from company deatil * Status code updates - Add function for name comparison - Remove hard-coded values * Update view permission checks for home widget * Add ability to manually add SalesOrderLineItem * Add nice progress bar widgets * Display detail view for sales order line item * edit SalesOrderLineItem * Fix unused import * Hide "shipped items" tab - Will be added in a future update
This commit is contained in:
		
							
								
								
									
										167
									
								
								lib/widget/part/bom_list.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										167
									
								
								lib/widget/part/bom_list.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,167 @@ | ||||
|  | ||||
| import "package:flutter/material.dart"; | ||||
| import "package:font_awesome_flutter/font_awesome_flutter.dart"; | ||||
|  | ||||
| import "package:inventree/api.dart"; | ||||
| import "package:inventree/helpers.dart"; | ||||
| import "package:inventree/l10.dart"; | ||||
|  | ||||
| import "package:inventree/inventree/bom.dart"; | ||||
| 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"; | ||||
|  | ||||
|  | ||||
| /* | ||||
|  * Widget for displaying a Bill of Materials for a specified Part instance | ||||
|  */ | ||||
| class BillOfMaterialsWidget extends StatefulWidget { | ||||
|  | ||||
|   const BillOfMaterialsWidget(this.part, {this.isParentComponent = true, Key? key}) : super(key: key); | ||||
|  | ||||
|   final InvenTreePart part; | ||||
|  | ||||
|   final bool isParentComponent; | ||||
|  | ||||
|   @override | ||||
|   _BillOfMaterialsState createState() => _BillOfMaterialsState(); | ||||
| } | ||||
|  | ||||
| class _BillOfMaterialsState extends RefreshableState<BillOfMaterialsWidget> { | ||||
|   _BillOfMaterialsState(); | ||||
|  | ||||
|   bool showFilterOptions = false; | ||||
|  | ||||
|   @override | ||||
|   String getAppBarTitle() { | ||||
|     if (widget.isParentComponent) { | ||||
|       return L10().billOfMaterials; | ||||
|     } else { | ||||
|       return L10().usedIn; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   List<Widget> appBarActions(BuildContext context) => [ | ||||
|     IconButton( | ||||
|       icon: FaIcon(FontAwesomeIcons.filter), | ||||
|       onPressed: () async { | ||||
|         setState(() { | ||||
|           showFilterOptions = !showFilterOptions; | ||||
|         }); | ||||
|       }, | ||||
|     ) | ||||
|   ]; | ||||
|  | ||||
|   @override | ||||
|   Widget getBody(BuildContext context) { | ||||
|  | ||||
|     Map<String, String> filters = {}; | ||||
|  | ||||
|     if (widget.isParentComponent) { | ||||
|       filters["part"] = widget.part.pk.toString(); | ||||
|     } else { | ||||
|       filters["uses"] = widget.part.pk.toString(); | ||||
|     } | ||||
|  | ||||
|     return Column( | ||||
|       children: [ | ||||
|         ListTile( | ||||
|           leading: InvenTreeAPI().getThumbnail(widget.part.thumbnail), | ||||
|           title: Text(widget.part.fullname), | ||||
|           subtitle: Text(widget.isParentComponent ? L10().billOfMaterials : L10().usedInDetails), | ||||
|           trailing: Text(L10().quantity), | ||||
|         ), | ||||
|         Divider(thickness: 1.25), | ||||
|         Expanded( | ||||
|           child: PaginatedBomList( | ||||
|             filters, | ||||
|             isParentPart: widget.isParentComponent, | ||||
|           ), | ||||
|         ), | ||||
|       ], | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
|  | ||||
| /* | ||||
|  * Create a paginated widget displaying a list of BomItem objects | ||||
|  */ | ||||
| class PaginatedBomList extends PaginatedSearchWidget { | ||||
|  | ||||
|   const PaginatedBomList(Map<String, String> filters, {this.isParentPart = true}) : super(filters: filters); | ||||
|  | ||||
|   final bool isParentPart; | ||||
|  | ||||
|   @override | ||||
|   String get searchTitle => L10().parts; | ||||
|  | ||||
|   @override | ||||
|   _PaginatedBomListState createState() => _PaginatedBomListState(); | ||||
| } | ||||
|  | ||||
|  | ||||
| class _PaginatedBomListState extends PaginatedSearchState<PaginatedBomList> { | ||||
|  | ||||
|   _PaginatedBomListState() : super(); | ||||
|  | ||||
|   @override | ||||
|   String get prefix => "bom_"; | ||||
|  | ||||
|   @override | ||||
|   Map<String, String> get orderingOptions => { | ||||
|     "quantity": L10().quantity, | ||||
|     "sub_part": L10().part, | ||||
|   }; | ||||
|  | ||||
|   @override | ||||
|   Map<String, Map<String, dynamic>> get filterOptions => { | ||||
|     "sub_part_assembly": { | ||||
|       "label": L10().filterAssembly, | ||||
|       "help_text": L10().filterAssemblyDetail, | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   @override | ||||
|   Future<InvenTreePageResponse?> requestPage(int limit, int offset, Map<String, String> params) async { | ||||
|  | ||||
|     final page = await InvenTreeBomItem().listPaginated(limit, offset, filters: params); | ||||
|  | ||||
|     return page; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget buildItem(BuildContext context, InvenTreeModel model) { | ||||
|  | ||||
|     InvenTreeBomItem bomItem = model as InvenTreeBomItem; | ||||
|  | ||||
|     InvenTreePart? subPart = widget.isParentPart ? bomItem.subPart : bomItem.part; | ||||
|  | ||||
|     String title = subPart?.fullname ?? "error - no name"; | ||||
|  | ||||
|     return ListTile( | ||||
|       title: Text(title), | ||||
|       subtitle: Text(bomItem.reference), | ||||
|       trailing: Text( | ||||
|         simpleNumberString(bomItem.quantity), | ||||
|         style: TextStyle(fontWeight: FontWeight.bold), | ||||
|       ), | ||||
|       leading: InvenTreeAPI().getThumbnail(subPart?.thumbnail ?? ""), | ||||
|       onTap: subPart == null ? null : () async { | ||||
|  | ||||
|         showLoadingOverlay(context); | ||||
|         var part = await InvenTreePart().get(subPart.pk); | ||||
|         hideLoadingOverlay(); | ||||
|  | ||||
|         if (part is InvenTreePart) { | ||||
|           Navigator.push(context, MaterialPageRoute(builder: (context) => PartDetailWidget(part))); | ||||
|         } | ||||
|       }, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										291
									
								
								lib/widget/part/category_display.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										291
									
								
								lib/widget/part/category_display.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,291 @@ | ||||
| import "package:flutter/material.dart"; | ||||
| import "package:flutter_speed_dial/flutter_speed_dial.dart"; | ||||
| import "package:font_awesome_flutter/font_awesome_flutter.dart"; | ||||
|  | ||||
| import "package:inventree/app_colors.dart"; | ||||
| import "package:inventree/l10.dart"; | ||||
|  | ||||
| import "package:inventree/inventree/part.dart"; | ||||
|  | ||||
| import "package:inventree/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"; | ||||
|  | ||||
|  | ||||
| class CategoryDisplayWidget extends StatefulWidget { | ||||
|  | ||||
|   const CategoryDisplayWidget(this.category, {Key? key}) : super(key: key); | ||||
|  | ||||
|   final InvenTreePartCategory? category; | ||||
|  | ||||
|   @override | ||||
|   _CategoryDisplayState createState() => _CategoryDisplayState(); | ||||
| } | ||||
|  | ||||
|  | ||||
| class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> { | ||||
|  | ||||
|   _CategoryDisplayState(); | ||||
|  | ||||
|   @override | ||||
|   String getAppBarTitle() => L10().partCategory; | ||||
|  | ||||
|   @override | ||||
|   List<Widget> appBarActions(BuildContext context) { | ||||
|     List<Widget> actions = []; | ||||
|  | ||||
|     if (widget.category != null) { | ||||
|       if (InvenTreePartCategory().canEdit) { | ||||
|         actions.add( | ||||
|           IconButton( | ||||
|             icon:  Icon(Icons.edit_square), | ||||
|             tooltip: L10().editCategory, | ||||
|             onPressed: () { | ||||
|               _editCategoryDialog(context); | ||||
|             }, | ||||
|           ) | ||||
|         ); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     return actions; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   List<SpeedDialChild> actionButtons(BuildContext context) { | ||||
|     List<SpeedDialChild> actions = []; | ||||
|  | ||||
|     if (InvenTreePart().canCreate) { | ||||
|      actions.add( | ||||
|        SpeedDialChild( | ||||
|          child: FaIcon(FontAwesomeIcons.shapes), | ||||
|          label: L10().partCreateDetail, | ||||
|          onTap: _newPart, | ||||
|        ) | ||||
|      ); | ||||
|     } | ||||
|  | ||||
|     if (InvenTreePartCategory().canCreate) { | ||||
|       actions.add( | ||||
|         SpeedDialChild( | ||||
|           child: FaIcon(FontAwesomeIcons.sitemap), | ||||
|           label: L10().categoryCreateDetail, | ||||
|           onTap: () { | ||||
|             _newCategory(context); | ||||
|           } | ||||
|         ) | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     return actions; | ||||
|   } | ||||
|  | ||||
|   void _editCategoryDialog(BuildContext context) { | ||||
|     final _cat = widget.category; | ||||
|  | ||||
|     // Cannot edit top-level category | ||||
|     if (_cat == null) { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     _cat.editForm( | ||||
|         context, | ||||
|         L10().editCategory, | ||||
|         onSuccess: (data) async { | ||||
|           refresh(context); | ||||
|           showSnackIcon(L10().categoryUpdated, success: true); | ||||
|         } | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Future<void> onBuild(BuildContext context) async { | ||||
|     refresh(context); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Future<void> request(BuildContext context) async { | ||||
|  | ||||
|     // Update the category | ||||
|     if (widget.category != null) { | ||||
|       final bool result = await widget.category?.reload() ?? false; | ||||
|  | ||||
|       if (!result) { | ||||
|         Navigator.of(context).pop(); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Widget getCategoryDescriptionCard({bool extra = true}) { | ||||
|     if (widget.category == null) { | ||||
|       return Card( | ||||
|         child: ListTile( | ||||
|           leading: FaIcon(FontAwesomeIcons.shapes), | ||||
|           title: Text( | ||||
|             L10().partCategoryTopLevel, | ||||
|             style: TextStyle(fontStyle: FontStyle.italic), | ||||
|           ) | ||||
|         ) | ||||
|       ); | ||||
|     } else { | ||||
|  | ||||
|       List<Widget> children = [ | ||||
|         ListTile( | ||||
|           title: Text("${widget.category?.name}", | ||||
|               style: TextStyle(fontWeight: FontWeight.bold) | ||||
|           ), | ||||
|           subtitle: Text("${widget.category?.description}"), | ||||
|           leading: widget.category!.customIcon ?? FaIcon(FontAwesomeIcons.sitemap), | ||||
|         ), | ||||
|       ]; | ||||
|  | ||||
|       if (extra) { | ||||
|         children.add( | ||||
|             ListTile( | ||||
|               title: Text(L10().parentCategory), | ||||
|               subtitle: Text("${widget.category?.parentPathString}"), | ||||
|               leading: FaIcon( | ||||
|                 FontAwesomeIcons.turnUp, | ||||
|                 color: COLOR_ACTION, | ||||
|               ), | ||||
|               onTap: () async { | ||||
|  | ||||
|                 int parentId = widget.category?.parentId ?? -1; | ||||
|  | ||||
|                 if (parentId < 0) { | ||||
|                   Navigator.push(context, MaterialPageRoute(builder: (context) => CategoryDisplayWidget(null))); | ||||
|                 } else { | ||||
|  | ||||
|                   showLoadingOverlay(context); | ||||
|                   var cat = await InvenTreePartCategory().get(parentId); | ||||
|                   hideLoadingOverlay(); | ||||
|  | ||||
|                   if (cat is InvenTreePartCategory) { | ||||
|                     Navigator.push(context, MaterialPageRoute(builder: (context) => CategoryDisplayWidget(cat))); | ||||
|                   } | ||||
|                 } | ||||
|               }, | ||||
|             ) | ||||
|         ); | ||||
|       } | ||||
|  | ||||
|       return Card( | ||||
|         child: Column( | ||||
|           children: children | ||||
|         ), | ||||
|       ); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   List<Widget> getTabIcons(BuildContext context) { | ||||
|  | ||||
|     return [ | ||||
|       Tab(text: L10().details), | ||||
|       Tab(text: L10().parts), | ||||
|     ]; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   List<Widget> getTabs(BuildContext context) { | ||||
|     return [ | ||||
|       Column(children: detailTiles()), | ||||
|       Column(children: partsTiles()), | ||||
|     ]; | ||||
|   } | ||||
|  | ||||
|   // Construct the "details" panel | ||||
|   List<Widget> detailTiles() { | ||||
|  | ||||
|     List<Widget> tiles = <Widget>[ | ||||
|       getCategoryDescriptionCard(), | ||||
|       Expanded( | ||||
|         child: PaginatedPartCategoryList( | ||||
|           { | ||||
|             "parent": widget.category?.pk.toString() ?? "null" | ||||
|           }, | ||||
|           title: L10().subcategories, | ||||
|         ), | ||||
|         flex: 10, | ||||
|       ) | ||||
|     ]; | ||||
|  | ||||
|     return tiles; | ||||
|   } | ||||
|  | ||||
|   // Construct the "parts" panel | ||||
|   List<Widget> partsTiles() { | ||||
|  | ||||
|     Map<String, String> filters = { | ||||
|       "category": widget.category?.pk.toString() ?? "null", | ||||
|     }; | ||||
|  | ||||
|     return [ | ||||
|       Expanded( | ||||
|         child: PaginatedPartList(filters), | ||||
|         flex: 10, | ||||
|       ) | ||||
|     ]; | ||||
|   } | ||||
|  | ||||
|   Future<void> _newCategory(BuildContext context) async { | ||||
|  | ||||
|     int pk = widget.category?.pk ?? -1; | ||||
|  | ||||
|     InvenTreePartCategory().createForm( | ||||
|       context, | ||||
|       L10().categoryCreate, | ||||
|       data: { | ||||
|         "parent": (pk > 0) ? pk : null, | ||||
|       }, | ||||
|       onSuccess: (result) async { | ||||
|  | ||||
|         Map<String, dynamic> data = result as Map<String, dynamic>; | ||||
|  | ||||
|         if (data.containsKey("pk")) { | ||||
|           var cat = InvenTreePartCategory.fromJson(data); | ||||
|  | ||||
|           Navigator.push( | ||||
|             context, | ||||
|             MaterialPageRoute( | ||||
|               builder: (context) => CategoryDisplayWidget(cat) | ||||
|             ) | ||||
|           ); | ||||
|         } else { | ||||
|           refresh(context); | ||||
|         } | ||||
|       } | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Future<void> _newPart() async { | ||||
|  | ||||
|     int pk = widget.category?.pk ?? -1; | ||||
|  | ||||
|     InvenTreePart().createForm( | ||||
|       context, | ||||
|       L10().partCreate, | ||||
|       data: { | ||||
|         "category": (pk > 0) ? pk : null | ||||
|       }, | ||||
|       onSuccess: (result) async { | ||||
|  | ||||
|         Map<String, dynamic> data = result as Map<String, dynamic>; | ||||
|  | ||||
|         if (data.containsKey("pk")) { | ||||
|           var part = InvenTreePart.fromJson(data); | ||||
|  | ||||
|           Navigator.push( | ||||
|             context, | ||||
|             MaterialPageRoute( | ||||
|               builder: (context) => PartDetailWidget(part) | ||||
|             ) | ||||
|           ); | ||||
|         } | ||||
|       } | ||||
|     ); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										112
									
								
								lib/widget/part/category_list.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								lib/widget/part/category_list.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,112 @@ | ||||
| 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"; | ||||
|  | ||||
| import "package:inventree/api.dart"; | ||||
| import "package:inventree/l10.dart"; | ||||
|  | ||||
| class PartCategoryList extends StatefulWidget { | ||||
|  | ||||
|   const PartCategoryList(this.filters); | ||||
|  | ||||
|   final Map<String, String> filters; | ||||
|  | ||||
|   @override | ||||
|   _PartCategoryListState createState() => _PartCategoryListState(); | ||||
|  | ||||
| } | ||||
|  | ||||
|  | ||||
| class _PartCategoryListState extends RefreshableState<PartCategoryList> { | ||||
|  | ||||
|   _PartCategoryListState(); | ||||
|  | ||||
|   @override | ||||
|   String getAppBarTitle() => L10().partCategories; | ||||
|  | ||||
|   @override | ||||
|   Widget getBody(BuildContext context) { | ||||
|     return PaginatedPartCategoryList(widget.filters); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class PaginatedPartCategoryList extends PaginatedSearchWidget { | ||||
|  | ||||
|   const PaginatedPartCategoryList(Map<String, String> filters, {String title = ""}) : super(filters: filters, title: title); | ||||
|  | ||||
|   @override | ||||
|   String get searchTitle => title.isNotEmpty ? title : L10().partCategories; | ||||
|  | ||||
|   @override | ||||
|   _PaginatedPartCategoryListState createState() => _PaginatedPartCategoryListState(); | ||||
| } | ||||
|  | ||||
|  | ||||
| class _PaginatedPartCategoryListState extends PaginatedSearchState<PaginatedPartCategoryList> { | ||||
|  | ||||
|   // _PaginatedPartCategoryListState(Map<String, String> filters, bool searchEnabled) : super(filters, searchEnabled); | ||||
|  | ||||
|   @override | ||||
|   String get prefix => "category_"; | ||||
|  | ||||
|   @override | ||||
|   Map<String, Map<String, dynamic>> get filterOptions => { | ||||
|     "cascade": { | ||||
|       "default": false, | ||||
|       "label": L10().includeSubcategories, | ||||
|       "help_text": L10().includeSubcategoriesDetail, | ||||
|       "tristate": false, | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   @override | ||||
|   Map<String, String> get orderingOptions { | ||||
|  | ||||
|     Map<String, String> options = { | ||||
|       "name": L10().name, | ||||
|       "level": L10().level, | ||||
|     }; | ||||
|  | ||||
|     // Note: API v69 changed 'parts' to 'part_count' | ||||
|     if (InvenTreeAPI().apiVersion >= 69) { | ||||
|       options["part_count"] = L10().parts; | ||||
|     } else { | ||||
|       options["parts"] = L10().parts; | ||||
|     } | ||||
|  | ||||
|     return options; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Future<InvenTreePageResponse?> requestPage(int limit, int offset, Map<String, String> params) async { | ||||
|  | ||||
|     final page = await InvenTreePartCategory().listPaginated(limit, offset, filters: params); | ||||
|  | ||||
|     return page; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget buildItem(BuildContext context, InvenTreeModel model) { | ||||
|  | ||||
|     InvenTreePartCategory category = model as InvenTreePartCategory; | ||||
|  | ||||
|     return ListTile( | ||||
|       title: Text(category.name), | ||||
|       subtitle: Text(category.pathstring), | ||||
|       trailing: Text("${category.partcount}"), | ||||
|       leading: category.customIcon, | ||||
|       onTap: () { | ||||
|         Navigator.push( | ||||
|           context, | ||||
|           MaterialPageRoute( | ||||
|             builder: (context) => CategoryDisplayWidget(category) | ||||
|           ) | ||||
|         ); | ||||
|       }, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										726
									
								
								lib/widget/part/part_detail.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										726
									
								
								lib/widget/part/part_detail.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,726 @@ | ||||
| import "package:flutter/material.dart"; | ||||
| import "package:flutter_speed_dial/flutter_speed_dial.dart"; | ||||
|  | ||||
| import "package:font_awesome_flutter/font_awesome_flutter.dart"; | ||||
|  | ||||
| import "package:inventree/app_colors.dart"; | ||||
| import "package:inventree/barcode/barcode.dart"; | ||||
| import "package:inventree/l10.dart"; | ||||
| import "package:inventree/helpers.dart"; | ||||
|  | ||||
| import "package:inventree/inventree/bom.dart"; | ||||
| import "package:inventree/inventree/part.dart"; | ||||
| import "package:inventree/inventree/stock.dart"; | ||||
| import "package:inventree/labels.dart"; | ||||
| import "package:inventree/preferences.dart"; | ||||
|  | ||||
| import "package:inventree/widget/attachment_widget.dart"; | ||||
| import "package:inventree/widget/part/bom_list.dart"; | ||||
| import "package:inventree/widget/part/part_list.dart"; | ||||
| import "package:inventree/widget/notes_widget.dart"; | ||||
| import "package:inventree/widget/part/part_parameter_widget.dart"; | ||||
| import "package:inventree/widget/progress.dart"; | ||||
| 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"; | ||||
|  | ||||
|  | ||||
| /* | ||||
|  * Widget for displaying a detail view of a single Part instance | ||||
|  */ | ||||
| class PartDetailWidget extends StatefulWidget { | ||||
|  | ||||
|   const PartDetailWidget(this.part, {Key? key}) : super(key: key); | ||||
|  | ||||
|   final InvenTreePart part; | ||||
|  | ||||
|   @override | ||||
|   _PartDisplayState createState() => _PartDisplayState(part); | ||||
|  | ||||
| } | ||||
|  | ||||
|  | ||||
| class _PartDisplayState extends RefreshableState<PartDetailWidget> { | ||||
|  | ||||
|   _PartDisplayState(this.part); | ||||
|  | ||||
|   InvenTreePart part; | ||||
|  | ||||
|   InvenTreePart? parentPart; | ||||
|  | ||||
|   int parameterCount = 0; | ||||
|  | ||||
|   bool showParameters = false; | ||||
|   bool showBom = false; | ||||
|  | ||||
|   int attachmentCount = 0; | ||||
|   int bomCount = 0; | ||||
|   int usedInCount = 0; | ||||
|   int variantCount = 0; | ||||
|  | ||||
|   List<Map<String, dynamic>> labels = []; | ||||
|  | ||||
|   @override | ||||
|   String getAppBarTitle() => L10().partDetails; | ||||
|  | ||||
|   @override | ||||
|   List<Widget> appBarActions(BuildContext context) { | ||||
|     List<Widget> actions = []; | ||||
|  | ||||
|     if (InvenTreePart().canEdit) { | ||||
|       actions.add( | ||||
|           IconButton( | ||||
|               icon: Icon(Icons.edit_square), | ||||
|               tooltip: L10().editPart, | ||||
|               onPressed: () { | ||||
|                 _editPartDialog(context); | ||||
|               } | ||||
|           ) | ||||
|       ); | ||||
|     } | ||||
|     return actions; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   List<SpeedDialChild> barcodeButtons(BuildContext context) { | ||||
|     List<SpeedDialChild> actions = []; | ||||
|  | ||||
|     if (InvenTreePart().canEdit) { | ||||
|       if (api.supportModernBarcodes) { | ||||
|         actions.add( | ||||
|             customBarcodeAction( | ||||
|                 context, this, | ||||
|                 widget.part.customBarcode, "part", | ||||
|                 widget.part.pk | ||||
|             ) | ||||
|         ); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     return actions; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   List<SpeedDialChild> actionButtons(BuildContext context) { | ||||
|     List<SpeedDialChild> actions = []; | ||||
|  | ||||
|     if (InvenTreeStockItem().canCreate) { | ||||
|       actions.add( | ||||
|           SpeedDialChild( | ||||
|               child: FaIcon(FontAwesomeIcons.box), | ||||
|               label: L10().stockItemCreate, | ||||
|               onTap: () { | ||||
|                 _newStockItem(context); | ||||
|               } | ||||
|           ) | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     if (labels.isNotEmpty) { | ||||
|       actions.add( | ||||
|         SpeedDialChild( | ||||
|           child: FaIcon(FontAwesomeIcons.print), | ||||
|           label: L10().printLabel, | ||||
|           onTap: () async { | ||||
|             selectAndPrintLabel( | ||||
|               context, | ||||
|               labels, | ||||
|               "part", | ||||
|               "part=${widget.part.pk}" | ||||
|             ); | ||||
|           } | ||||
|         ) | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     return actions; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Future<void> onBuild(BuildContext context) async { | ||||
|     refresh(context); | ||||
|  | ||||
|     if (mounted) { | ||||
|       setState(() {}); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Future<void> request(BuildContext context) async { | ||||
|  | ||||
|     final bool result = await part.reload(); | ||||
|  | ||||
|     if (!result || part.pk == -1) { | ||||
|       // Part could not be loaded, for some reason | ||||
|       Navigator.of(context).pop(); | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     // If the part points to a parent "template" part, request that too | ||||
|     int? templatePartId = part.variantOf; | ||||
|  | ||||
|     if (templatePartId == null) { | ||||
|       parentPart = null; | ||||
|     } else { | ||||
|       final result = await InvenTreePart().get(templatePartId); | ||||
|  | ||||
|       if (result != null && result is InvenTreePart) { | ||||
|         parentPart = result; | ||||
|       } else { | ||||
|         parentPart = null; | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     // Request part test templates | ||||
|     part.getTestTemplates().then((value) { | ||||
|       if (mounted) { | ||||
|         setState(() {}); | ||||
|       } | ||||
|     }); | ||||
|  | ||||
|     // Request the number of parameters for this part | ||||
|     if (api.supportsPartParameters) { | ||||
|       showParameters = await InvenTreeSettingsManager().getValue(INV_PART_SHOW_PARAMETERS, true) as bool; | ||||
|     } else { | ||||
|       showParameters = false; | ||||
|     } | ||||
|  | ||||
|     // Request the number of attachments | ||||
|     InvenTreePartAttachment().count( | ||||
|       filters: { | ||||
|         "part": part.pk.toString(), | ||||
|       } | ||||
|     ).then((int value) { | ||||
|       if (mounted) { | ||||
|         setState(() { | ||||
|           attachmentCount = value; | ||||
|         }); | ||||
|       } | ||||
|     }); | ||||
|  | ||||
|     showBom = await InvenTreeSettingsManager().getValue(INV_PART_SHOW_BOM, true) as bool; | ||||
|  | ||||
|     // Request the number of BOM items | ||||
|     InvenTreePart().count( | ||||
|       filters: { | ||||
|         "in_bom_for": part.pk.toString(), | ||||
|       } | ||||
|     ).then((int value) { | ||||
|       if (mounted) { | ||||
|         setState(() { | ||||
|           bomCount = value; | ||||
|         }); | ||||
|       } | ||||
|     }); | ||||
|  | ||||
|     // Request number of "used in" parts | ||||
|     InvenTreeBomItem().count( | ||||
|       filters: { | ||||
|         "uses": part.pk.toString(), | ||||
|       } | ||||
|     ).then((int value) { | ||||
|       if (mounted) { | ||||
|         setState(() { | ||||
|           usedInCount = value; | ||||
|         }); | ||||
|       } | ||||
|     }); | ||||
|  | ||||
|     // Request the number of variant items | ||||
|     InvenTreePart().count( | ||||
|       filters: { | ||||
|         "variant_of": part.pk.toString(), | ||||
|       } | ||||
|     ).then((int value) { | ||||
|       if (mounted) { | ||||
|         setState(() { | ||||
|           variantCount = value; | ||||
|         }); | ||||
|       } | ||||
|     }); | ||||
|  | ||||
|     List<Map<String, dynamic>> _labels = []; | ||||
|     bool allowLabelPrinting = await InvenTreeSettingsManager().getBool(INV_ENABLE_LABEL_PRINTING, true); | ||||
|     allowLabelPrinting &= api.supportsMixin("labels"); | ||||
|  | ||||
|     if (allowLabelPrinting) { | ||||
|       _labels = await getLabelTemplates("part", { | ||||
|         "part": widget.part.pk.toString(), | ||||
|       }); | ||||
|     } | ||||
|  | ||||
|     if (mounted) { | ||||
|       setState(() { | ||||
|         labels = _labels; | ||||
|       }); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   void _editPartDialog(BuildContext context) { | ||||
|  | ||||
|     part.editForm( | ||||
|       context, | ||||
|       L10().editPart, | ||||
|       onSuccess: (data) async { | ||||
|         refresh(context); | ||||
|         showSnackIcon(L10().partEdited, success: true); | ||||
|       } | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Widget headerTile() { | ||||
|     return Card( | ||||
|         child: ListTile( | ||||
|           title: Text("${part.fullname}"), | ||||
|           subtitle: Text("${part.description}"), | ||||
|           trailing: Text( | ||||
|             part.stockString(), | ||||
|             style: TextStyle( | ||||
|               fontSize: 20, | ||||
|             ) | ||||
|           ), | ||||
|           leading: GestureDetector( | ||||
|             child: api.getImage(part.thumbnail), | ||||
|             onTap: () { | ||||
|               Navigator.push( | ||||
|                 context, | ||||
|                 MaterialPageRoute( | ||||
|                   builder: (context) => PartImageWidget(part) | ||||
|                 ) | ||||
|               ).then((value) { | ||||
|                 refresh(context); | ||||
|               }); | ||||
|             }), | ||||
|         ), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   /* | ||||
|    * Build a list of tiles to display under the part description | ||||
|    */ | ||||
|   List<Widget> partTiles() { | ||||
|  | ||||
|     List<Widget> tiles = []; | ||||
|  | ||||
|     // Image / name / description | ||||
|     tiles.add( | ||||
|       headerTile() | ||||
|     ); | ||||
|  | ||||
|     if (loading) { | ||||
|       tiles.add(progressIndicator()); | ||||
|       return tiles; | ||||
|     } | ||||
|  | ||||
|     if (!part.isActive) { | ||||
|       tiles.add( | ||||
|         ListTile( | ||||
|           title: Text( | ||||
|               L10().inactive, | ||||
|               style: TextStyle( | ||||
|                 color: COLOR_DANGER | ||||
|               ) | ||||
|           ), | ||||
|           subtitle: Text( | ||||
|             L10().inactiveDetail, | ||||
|             style: TextStyle( | ||||
|               color: COLOR_DANGER | ||||
|             ) | ||||
|           ), | ||||
|           leading: FaIcon( | ||||
|               FontAwesomeIcons.circleExclamation, | ||||
|               color: COLOR_DANGER | ||||
|           ), | ||||
|         ) | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     if (parentPart != null) { | ||||
|       tiles.add( | ||||
|         ListTile( | ||||
|           title: Text(L10().templatePart), | ||||
|           subtitle: Text(parentPart!.fullname), | ||||
|           leading: api.getImage( | ||||
|             parentPart!.thumbnail, | ||||
|             width: 32, | ||||
|             height: 32, | ||||
|           ), | ||||
|           onTap: () { | ||||
|             Navigator.push( | ||||
|               context, | ||||
|               MaterialPageRoute(builder: (context) => PartDetailWidget(parentPart!)) | ||||
|             ); | ||||
|           } | ||||
|         ) | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     // Category information | ||||
|     if (part.categoryName.isNotEmpty) { | ||||
|       tiles.add( | ||||
|         ListTile( | ||||
|             title: Text(L10().partCategory), | ||||
|             subtitle: Text("${part.categoryName}"), | ||||
|             leading: FaIcon(FontAwesomeIcons.sitemap, color: COLOR_ACTION), | ||||
|             onTap: () async { | ||||
|               if (part.categoryId > 0) { | ||||
|  | ||||
|                 showLoadingOverlay(context); | ||||
|                 var cat = await InvenTreePartCategory().get(part.categoryId); | ||||
|                 hideLoadingOverlay(); | ||||
|  | ||||
|                 if (cat is InvenTreePartCategory) { | ||||
|                   Navigator.push(context, MaterialPageRoute( | ||||
|                       builder: (context) => CategoryDisplayWidget(cat))); | ||||
|                 } | ||||
|               } | ||||
|             }, | ||||
|           ) | ||||
|       ); | ||||
|     } else { | ||||
|       tiles.add( | ||||
|           ListTile( | ||||
|             title: Text(L10().partCategory), | ||||
|             subtitle: Text(L10().partCategoryTopLevel), | ||||
|             leading: FaIcon(FontAwesomeIcons.sitemap, color: COLOR_ACTION), | ||||
|             onTap: () { | ||||
|               Navigator.push(context, MaterialPageRoute( | ||||
|                   builder: (context) => CategoryDisplayWidget(null))); | ||||
|             }, | ||||
|           ) | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     // Display number of "variant" parts if any exist | ||||
|     if (variantCount > 0) { | ||||
|       tiles.add( | ||||
|           ListTile( | ||||
|             title: Text(L10().variants), | ||||
|             leading: FaIcon(FontAwesomeIcons.shapes, color: COLOR_ACTION), | ||||
|             trailing: Text(variantCount.toString()), | ||||
|             onTap: () { | ||||
|               Navigator.push( | ||||
|                   context, | ||||
|                   MaterialPageRoute( | ||||
|                       builder: (context) => PartList( | ||||
|                           { | ||||
|                             "variant_of": part.pk.toString(), | ||||
|                           }, | ||||
|                           title: L10().variants | ||||
|                       ) | ||||
|                   ) | ||||
|               ); | ||||
|             }, | ||||
|           ) | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     tiles.add( | ||||
|       ListTile( | ||||
|         title: Text(L10().availableStock), | ||||
|         subtitle: Text(L10().stockDetails), | ||||
|         leading: FaIcon(FontAwesomeIcons.boxesStacked), | ||||
|         trailing: Text( | ||||
|           part.stockString(), | ||||
|           style: TextStyle( | ||||
|             fontWeight: FontWeight.bold, | ||||
|           ), | ||||
|         ), | ||||
|       ), | ||||
|     ); | ||||
|  | ||||
|     // Tiles for "purchaseable" parts | ||||
|     if (part.isPurchaseable) { | ||||
|  | ||||
|       // On order | ||||
|       tiles.add( | ||||
|         ListTile( | ||||
|           title: Text(L10().onOrder), | ||||
|           subtitle: Text(L10().onOrderDetails), | ||||
|           leading: FaIcon(FontAwesomeIcons.cartShopping), | ||||
|           trailing: Text("${part.onOrderString}"), | ||||
|           onTap: () { | ||||
|             // TODO - Order views | ||||
|           }, | ||||
|         ) | ||||
|       ); | ||||
|  | ||||
|     } | ||||
|  | ||||
|     // Tiles for an "assembly" part | ||||
|     if (part.isAssembly) { | ||||
|  | ||||
|       if (showBom && bomCount > 0) { | ||||
|         tiles.add( | ||||
|             ListTile( | ||||
|                 title: Text(L10().billOfMaterials), | ||||
|                 leading: FaIcon(FontAwesomeIcons.tableList, color: COLOR_ACTION), | ||||
|                 trailing: Text(bomCount.toString()), | ||||
|                 onTap: () { | ||||
|                   Navigator.push(context, MaterialPageRoute( | ||||
|                       builder: (context) => BillOfMaterialsWidget(part, isParentComponent: true) | ||||
|                   )); | ||||
|                 }, | ||||
|             ) | ||||
|         ); | ||||
|       } | ||||
|  | ||||
|       if (part.building > 0) { | ||||
|         tiles.add( | ||||
|             ListTile( | ||||
|               title: Text(L10().building), | ||||
|               leading: FaIcon(FontAwesomeIcons.screwdriverWrench), | ||||
|               trailing: Text("${simpleNumberString(part.building)}"), | ||||
|               onTap: () { | ||||
|                 // TODO | ||||
|               }, | ||||
|             ) | ||||
|         ); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     if (part.isComponent) { | ||||
|       if (showBom && usedInCount > 0) { | ||||
|         tiles.add( | ||||
|           ListTile( | ||||
|             title: Text(L10().usedIn), | ||||
|             subtitle: Text(L10().usedInDetails), | ||||
|             leading: FaIcon(FontAwesomeIcons.layerGroup, color: COLOR_ACTION), | ||||
|             trailing: Text(usedInCount.toString()), | ||||
|               onTap: () { | ||||
|                 Navigator.push( | ||||
|                     context, | ||||
|                     MaterialPageRoute( | ||||
|                         builder: (context) => BillOfMaterialsWidget(part, isParentComponent: false) | ||||
|                     ) | ||||
|                 ); | ||||
|               } | ||||
|           ) | ||||
|         ); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     // Keywords? | ||||
|     if (part.keywords.isNotEmpty) { | ||||
|       tiles.add( | ||||
|           ListTile( | ||||
|             title: Text("${part.keywords}"), | ||||
|             leading: FaIcon(FontAwesomeIcons.tags), | ||||
|           ) | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     // External link? | ||||
|     if (part.link.isNotEmpty) { | ||||
|       tiles.add( | ||||
|           ListTile( | ||||
|             title: Text("${part.link}"), | ||||
|             leading: FaIcon(FontAwesomeIcons.link, color: COLOR_ACTION), | ||||
|             onTap: () { | ||||
|               part.openLink(); | ||||
|             }, | ||||
|           ) | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     // Tiles for "component" part | ||||
|     if (part.isComponent && part.usedInCount > 0) { | ||||
|  | ||||
|       tiles.add( | ||||
|         ListTile( | ||||
|           title: Text(L10().usedIn), | ||||
|           subtitle: Text(L10().usedInDetails), | ||||
|           leading: FaIcon(FontAwesomeIcons.sitemap), | ||||
|           trailing: Text("${part.usedInCount}"), | ||||
|           onTap: () { | ||||
|             // TODO | ||||
|           }, | ||||
|         ) | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     if (part.isPurchaseable) { | ||||
|  | ||||
|       if (part.supplierCount > 0) { | ||||
|         tiles.add( | ||||
|             ListTile( | ||||
|               title: Text(L10().suppliers), | ||||
|               leading: FaIcon(FontAwesomeIcons.industry, color: COLOR_ACTION), | ||||
|               trailing: Text("${part.supplierCount}"), | ||||
|                 onTap: () { | ||||
|                   Navigator.push( | ||||
|                     context, | ||||
|                     MaterialPageRoute(builder: (context) => SupplierPartList({ | ||||
|                       "part": part.pk.toString() | ||||
|                     })) | ||||
|                   ); | ||||
|                 }, | ||||
|             ) | ||||
|         ); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     // Notes field | ||||
|     tiles.add( | ||||
|         ListTile( | ||||
|           title: Text(L10().notes), | ||||
|           leading: FaIcon(FontAwesomeIcons.noteSticky, color: COLOR_ACTION), | ||||
|           trailing: Text(""), | ||||
|           onTap: () { | ||||
|             Navigator.push( | ||||
|                 context, | ||||
|                 MaterialPageRoute(builder: (context) => NotesWidget(part)) | ||||
|             ); | ||||
|           }, | ||||
|         ) | ||||
|     ); | ||||
|  | ||||
|     tiles.add( | ||||
|       ListTile( | ||||
|         title: Text(L10().attachments), | ||||
|         leading: FaIcon(FontAwesomeIcons.fileLines, color: COLOR_ACTION), | ||||
|         trailing: attachmentCount > 0 ? Text(attachmentCount.toString()) : null, | ||||
|         onTap: () { | ||||
|           Navigator.push( | ||||
|             context, | ||||
|             MaterialPageRoute( | ||||
|               builder: (context) => AttachmentWidget( | ||||
|                   InvenTreePartAttachment(), | ||||
|                   part.pk, | ||||
|                   part.canEdit | ||||
|                 ) | ||||
|             ) | ||||
|           ); | ||||
|         }, | ||||
|       ) | ||||
|     ); | ||||
|  | ||||
|     return tiles; | ||||
|  | ||||
|   } | ||||
|  | ||||
|   // Return tiles for each stock item | ||||
|   List<Widget> stockTiles() { | ||||
|     List<Widget> tiles = []; | ||||
|  | ||||
|     tiles.add(headerTile()); | ||||
|  | ||||
|     tiles.add( | ||||
|       ListTile( | ||||
|         title: Text( | ||||
|           L10().stockItems, | ||||
|           style: TextStyle(fontWeight: FontWeight.bold), | ||||
|         ), | ||||
|         subtitle: part.stockItems.isEmpty ? Text(L10().stockItemsNotAvailable) : null, | ||||
|         trailing: part.stockItems.isNotEmpty ? Text("${part.stockItems.length}") : null, | ||||
|       ) | ||||
|     ); | ||||
|  | ||||
|     return tiles; | ||||
|   } | ||||
|  | ||||
|   /* | ||||
|    * Launch a form to create a new StockItem for this part | ||||
|    */ | ||||
|   Future<void> _newStockItem(BuildContext context) async { | ||||
|  | ||||
|     var fields = InvenTreeStockItem().formFields(); | ||||
|  | ||||
|     // Serial number cannot be directly edited here | ||||
|     fields.remove("serial"); | ||||
|  | ||||
|     // Hide the "part" field | ||||
|     fields["part"]?["hidden"] = true; | ||||
|  | ||||
|     int? default_location = part.defaultLocation; | ||||
|  | ||||
|     Map<String, dynamic> data = { | ||||
|       "part": part.pk.toString() | ||||
|     }; | ||||
|  | ||||
|     if (default_location != null) { | ||||
|       data["location"] = default_location; | ||||
|     } | ||||
|  | ||||
|     if (part.isTrackable) { | ||||
|       // read the next available serial number | ||||
|       showLoadingOverlay(context); | ||||
|       var response = await api.get("/api/part/${part.pk}/serial-numbers/", expectedStatusCode: null); | ||||
|       hideLoadingOverlay(); | ||||
|  | ||||
|       if (response.isValid() && response.statusCode == 200) { | ||||
|         data["serial_numbers"] = response.data["next"] ?? response.data["latest"]; | ||||
|       } | ||||
|  | ||||
|       print("response: " + response.statusCode.toString() + response.data.toString()); | ||||
|  | ||||
|     } else { | ||||
|       // Cannot set serial numbers for non-trackable parts | ||||
|       fields.remove("serial_numbers"); | ||||
|     } | ||||
|  | ||||
|     print("data: ${data.toString()}"); | ||||
|  | ||||
|     InvenTreeStockItem().createForm( | ||||
|         context, | ||||
|         L10().stockItemCreate, | ||||
|         fields: fields, | ||||
|         data: data, | ||||
|         onSuccess: (result) async { | ||||
|  | ||||
|           Map<String, dynamic> data = result as Map<String, dynamic>; | ||||
|  | ||||
|           if (data.containsKey("pk")) { | ||||
|             var item = InvenTreeStockItem.fromJson(data); | ||||
|  | ||||
|             Navigator.push( | ||||
|                 context, | ||||
|                 MaterialPageRoute( | ||||
|                     builder: (context) => StockDetailWidget(item) | ||||
|                 ) | ||||
|             ); | ||||
|           } | ||||
|         } | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   List<Widget> getTabIcons(BuildContext context) { | ||||
|     List<Widget> icons = [ | ||||
|       Tab(text: L10().details), | ||||
|       Tab(text: L10().stock) | ||||
|     ]; | ||||
|  | ||||
|     if (showParameters) { | ||||
|       icons.add(Tab(text: L10().parameters)); | ||||
|     } | ||||
|  | ||||
|     return icons; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   List<Widget> getTabs(BuildContext context) { | ||||
|     List<Widget> tabs = [ | ||||
|       Center( | ||||
|         child: ListView( | ||||
|           children: ListTile.divideTiles( | ||||
|           context: context, | ||||
|           tiles: partTiles() | ||||
|           ).toList() | ||||
|         ) | ||||
|       ), | ||||
|       PaginatedStockItemList({"part": part.pk.toString()}) | ||||
|     ]; | ||||
|  | ||||
|     if (showParameters) { | ||||
|       tabs.add(PaginatedParameterList({"part": part.pk.toString()})); | ||||
|     } | ||||
|  | ||||
|     return tabs; | ||||
|   } | ||||
|  | ||||
| } | ||||
							
								
								
									
										78
									
								
								lib/widget/part/part_image_widget.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								lib/widget/part/part_image_widget.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,78 @@ | ||||
| import "dart:io"; | ||||
|  | ||||
| import "package:flutter/material.dart"; | ||||
|  | ||||
| import "package:font_awesome_flutter/font_awesome_flutter.dart"; | ||||
|  | ||||
| import "package:inventree/api.dart"; | ||||
| import "package:inventree/inventree/part.dart"; | ||||
| import "package:inventree/widget/fields.dart"; | ||||
| import "package:inventree/widget/refreshable_state.dart"; | ||||
| import "package:inventree/widget/snacks.dart"; | ||||
| import "package:inventree/l10.dart"; | ||||
|  | ||||
| class PartImageWidget extends StatefulWidget { | ||||
|  | ||||
|   const PartImageWidget(this.part, {Key? key}) : super(key: key); | ||||
|  | ||||
|   final InvenTreePart part; | ||||
|  | ||||
|   @override | ||||
|   _PartImageState createState() => _PartImageState(part); | ||||
|  | ||||
| } | ||||
|  | ||||
|  | ||||
| class _PartImageState extends RefreshableState<PartImageWidget> { | ||||
|  | ||||
|   _PartImageState(this.part); | ||||
|  | ||||
|   final InvenTreePart part; | ||||
|  | ||||
|   @override | ||||
|   Future<void> request(BuildContext context) async { | ||||
|     await part.reload(); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   String getAppBarTitle() => part.fullname; | ||||
|  | ||||
|   @override | ||||
|   List<Widget> appBarActions(BuildContext context) { | ||||
|  | ||||
|     List<Widget> actions = []; | ||||
|  | ||||
|     if (part.canEdit) { | ||||
|  | ||||
|       // File upload | ||||
|       actions.add( | ||||
|         IconButton( | ||||
|           icon: FaIcon(FontAwesomeIcons.fileArrowUp), | ||||
|           onPressed: () async { | ||||
|  | ||||
|             FilePickerDialog.pickFile( | ||||
|               onPicked: (File file) async { | ||||
|                 final result = await part.uploadImage(file); | ||||
|  | ||||
|                 if (!result) { | ||||
|                   showSnackIcon(L10().uploadFailed, success: false); | ||||
|                 } | ||||
|  | ||||
|                 refresh(context); | ||||
|               } | ||||
|             ); | ||||
|  | ||||
|           }, | ||||
|         ) | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     return actions; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget getBody(BuildContext context) { | ||||
|     return InvenTreeAPI().getImage(part.image); | ||||
|   } | ||||
|  | ||||
| } | ||||
							
								
								
									
										139
									
								
								lib/widget/part/part_list.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										139
									
								
								lib/widget/part/part_list.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,139 @@ | ||||
| import "package:flutter/material.dart"; | ||||
|  | ||||
| import "package:inventree/api.dart"; | ||||
| import "package:inventree/l10.dart"; | ||||
|  | ||||
| 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"; | ||||
|  | ||||
|  | ||||
| class PartList extends StatefulWidget { | ||||
|  | ||||
|   const PartList(this.filters, {this.title = ""}); | ||||
|  | ||||
|   final String title; | ||||
|  | ||||
|   final Map<String, String> filters; | ||||
|  | ||||
|   @override | ||||
|   _PartListState createState() => _PartListState(filters, title); | ||||
| } | ||||
|  | ||||
|  | ||||
| class _PartListState extends RefreshableState<PartList> { | ||||
|  | ||||
|   _PartListState(this.filters, this.title); | ||||
|  | ||||
|   final String title; | ||||
|  | ||||
|   final Map<String, String> filters; | ||||
|  | ||||
|   bool showFilterOptions = false; | ||||
|  | ||||
|   @override | ||||
|   String getAppBarTitle() => title.isNotEmpty ? title : L10().parts; | ||||
|  | ||||
|   @override | ||||
|   Widget getBody(BuildContext context) { | ||||
|     return PaginatedPartList(filters); | ||||
|   } | ||||
|  | ||||
| } | ||||
|  | ||||
|  | ||||
| class PaginatedPartList extends PaginatedSearchWidget { | ||||
|  | ||||
|   const PaginatedPartList(Map<String, String> filters) : super(filters: filters); | ||||
|  | ||||
|   @override | ||||
|   String get searchTitle => L10().parts; | ||||
|  | ||||
|   @override | ||||
|   _PaginatedPartListState createState() => _PaginatedPartListState(); | ||||
| } | ||||
|  | ||||
|  | ||||
| class _PaginatedPartListState extends PaginatedSearchState<PaginatedPartList> { | ||||
|  | ||||
|   _PaginatedPartListState() : super(); | ||||
|  | ||||
|   @override | ||||
|   String get prefix => "part_"; | ||||
|  | ||||
|   @override | ||||
|   Map<String, String> get orderingOptions => { | ||||
|     "name": L10().name, | ||||
|     "in_stock": L10().stock, | ||||
|     "IPN": L10().internalPartNumber, | ||||
|   }; | ||||
|  | ||||
|   @override | ||||
|   Map<String, Map<String, dynamic>> get filterOptions => { | ||||
|     "cascade": { | ||||
|       "default": true, | ||||
|       "label": L10().includeSubcategories, | ||||
|       "help_text": L10().includeSubcategoriesDetail, | ||||
|     }, | ||||
|     "active": { | ||||
|       "label": L10().filterActive, | ||||
|       "help_text": L10().filterActiveDetail, | ||||
|       "tristate": true, | ||||
|     }, | ||||
|     "assembly": { | ||||
|       "label": L10().filterAssembly, | ||||
|       "help_text": L10().filterAssemblyDetail | ||||
|     }, | ||||
|     "component": { | ||||
|       "label": L10().filterComponent, | ||||
|       "help_text": L10().filterComponentDetail, | ||||
|     }, | ||||
|     "is_template": { | ||||
|       "label": L10().filterTemplate, | ||||
|       "help_text": L10().filterTemplateDetail | ||||
|     }, | ||||
|     "trackable": { | ||||
|       "label": L10().filterTrackable, | ||||
|       "help_text": L10().filterTrackableDetail, | ||||
|     }, | ||||
|     "virtual": { | ||||
|       "label": L10().filterVirtual, | ||||
|       "help_text": L10().filterVirtualDetail, | ||||
|     }, | ||||
|     "has_stock": { | ||||
|       "label": L10().filterInStock, | ||||
|       "help_text": L10().filterInStockDetail, | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   @override | ||||
|   Future<InvenTreePageResponse?> requestPage(int limit, int offset, Map<String, String> params) async { | ||||
|     final page = await InvenTreePart().listPaginated(limit, offset, filters: params); | ||||
|     return page; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget buildItem(BuildContext context, InvenTreeModel model) { | ||||
|  | ||||
|     InvenTreePart part = model as InvenTreePart; | ||||
|  | ||||
|     return ListTile( | ||||
|       title: Text(part.fullname), | ||||
|       subtitle: Text(part.description), | ||||
|       trailing: Text( | ||||
|         part.stockString(), | ||||
|         style: TextStyle( | ||||
|           fontSize: 16, | ||||
|           fontWeight: FontWeight.bold | ||||
|         ) | ||||
|       ), | ||||
|       leading: InvenTreeAPI().getThumbnail(part.thumbnail), | ||||
|       onTap: () { | ||||
|         Navigator.push(context, MaterialPageRoute(builder: (context) => PartDetailWidget(part))); | ||||
|       }, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										143
									
								
								lib/widget/part/part_parameter_widget.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										143
									
								
								lib/widget/part/part_parameter_widget.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,143 @@ | ||||
| import "package:flutter/material.dart"; | ||||
| import "package:inventree/inventree/model.dart"; | ||||
|  | ||||
| import "package:inventree/l10.dart"; | ||||
| import "package:inventree/inventree/part.dart"; | ||||
| import "package:inventree/widget/paginator.dart"; | ||||
| import "package:inventree/widget/progress.dart"; | ||||
| import "package:inventree/widget/refreshable_state.dart"; | ||||
|  | ||||
| /* | ||||
|  * Widget for displaying a list of parameters associated with a given Part instance | ||||
|  */ | ||||
| class PartParameterWidget extends StatefulWidget { | ||||
|  | ||||
|   const PartParameterWidget(this.part); | ||||
|  | ||||
|   final InvenTreePart part; | ||||
|  | ||||
|   @override | ||||
|   _ParameterWidgetState createState() => _ParameterWidgetState(); | ||||
| } | ||||
|  | ||||
|  | ||||
| class _ParameterWidgetState extends RefreshableState<PartParameterWidget> { | ||||
|   _ParameterWidgetState(); | ||||
|  | ||||
|   @override | ||||
|   String getAppBarTitle() { | ||||
|     return L10().parameters; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   List<Widget> appBarActions(BuildContext context) { | ||||
|     return []; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget getBody(BuildContext context) { | ||||
|  | ||||
|     Map<String, String> filters = { | ||||
|       "part": widget.part.pk.toString() | ||||
|     }; | ||||
|  | ||||
|     return Column( | ||||
|       children: [ | ||||
|         Expanded( | ||||
|           child: PaginatedParameterList(filters) | ||||
|         ) | ||||
|       ], | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
|  | ||||
| /* | ||||
|  * Widget for displaying a paginated list of Part parameters | ||||
|  */ | ||||
| class PaginatedParameterList extends PaginatedSearchWidget { | ||||
|  | ||||
|   const PaginatedParameterList(Map<String, String> filters) : super(filters: filters); | ||||
|  | ||||
|   @override | ||||
|   String get searchTitle => L10().parameters; | ||||
|  | ||||
|   @override | ||||
|   _PaginatedParameterState createState() => _PaginatedParameterState(); | ||||
| } | ||||
|  | ||||
|  | ||||
| class _PaginatedParameterState extends PaginatedSearchState<PaginatedParameterList> { | ||||
|  | ||||
|   _PaginatedParameterState() : super(); | ||||
|  | ||||
|   @override | ||||
|   String get prefix => "parameters_"; | ||||
|  | ||||
|   @override | ||||
|   Map<String, String> get orderingOptions => { | ||||
|  | ||||
|   }; | ||||
|  | ||||
|   @override | ||||
|   Map<String, Map<String, dynamic>> get filterOptions => { | ||||
|     // TODO | ||||
|   }; | ||||
|  | ||||
|   @override | ||||
|   Future<InvenTreePageResponse?> requestPage(int limit, int offset, Map<String, String> params) async { | ||||
|  | ||||
|     final page = await InvenTreePartParameter().listPaginated(limit, offset, filters: params); | ||||
|  | ||||
|     return page; | ||||
|   } | ||||
|  | ||||
|   Future<void> editParameter(InvenTreePartParameter parameter) async { | ||||
|  | ||||
|     // Checkbox values are handled separately | ||||
|     if (parameter.is_checkbox) { | ||||
|       return; | ||||
|     } else { | ||||
|       parameter.editForm( | ||||
|           context, | ||||
|           L10().editParameter, | ||||
|           onSuccess: (data) async { | ||||
|             updateSearchTerm(); | ||||
|           } | ||||
|       ); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget buildItem(BuildContext context, InvenTreeModel model) { | ||||
|  | ||||
|     InvenTreePartParameter parameter = model as InvenTreePartParameter; | ||||
|  | ||||
|     return ListTile( | ||||
|       title: Text(parameter.name), | ||||
|       subtitle: Text(parameter.description), | ||||
|       trailing: parameter.is_checkbox | ||||
|         ? Switch( | ||||
|           value: parameter.as_bool, | ||||
|           onChanged: (bool value) { | ||||
|             if (parameter.canEdit) { | ||||
|               showLoadingOverlay(context); | ||||
|               parameter.update( | ||||
|                 values: { | ||||
|                   "data": value.toString() | ||||
|                 } | ||||
|               ).then((value) async{ | ||||
|                 hideLoadingOverlay(); | ||||
|                 updateSearchTerm(); | ||||
|               }); | ||||
|             } | ||||
|           }, | ||||
|       ) : Text(parameter.valueString), | ||||
|       onTap: parameter.is_checkbox ? null : () async { | ||||
|         if (parameter.canEdit) { | ||||
|           editParameter(parameter); | ||||
|         } | ||||
|       }, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										83
									
								
								lib/widget/part/part_suppliers.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								lib/widget/part/part_suppliers.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,83 @@ | ||||
| import "dart:core"; | ||||
|  | ||||
| import "package:inventree/l10.dart"; | ||||
|  | ||||
| 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 { | ||||
|  | ||||
|   const PartSupplierWidget(this.part, {Key? key}) : super(key: key); | ||||
|  | ||||
|   final InvenTreePart part; | ||||
|  | ||||
|   @override | ||||
|   _PartSupplierState createState() => _PartSupplierState(part); | ||||
|  | ||||
| } | ||||
|  | ||||
|  | ||||
| class _PartSupplierState extends RefreshableState<PartSupplierWidget> { | ||||
|  | ||||
|   _PartSupplierState(this.part); | ||||
|  | ||||
|   final InvenTreePart part; | ||||
|  | ||||
|   List<InvenTreeSupplierPart> _supplierParts = []; | ||||
|  | ||||
|   @override | ||||
|   Future<void> request(BuildContext context) async { | ||||
|     // TODO - Request list of suppliers for the part | ||||
|     await part.reload(); | ||||
|     _supplierParts = await part.getSupplierParts(); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   String getAppBarTitle() => L10().partSuppliers; | ||||
|  | ||||
|   @override | ||||
|   List<Widget> appBarActions(BuildContext contexts) { | ||||
|     // TODO | ||||
|     return []; | ||||
|   } | ||||
|  | ||||
|   Widget _supplierPartTile(BuildContext context, int index) { | ||||
|  | ||||
|     InvenTreeSupplierPart _part = _supplierParts[index]; | ||||
|  | ||||
|     return ListTile( | ||||
|       leading: InvenTreeAPI().getThumbnail(_part.supplierImage), | ||||
|       title: Text("${_part.SKU}"), | ||||
|       subtitle: Text("${_part.manufacturerName}: ${_part.MPN}"), | ||||
|       onTap: () async { | ||||
|         var company = await InvenTreeCompany().get(_part.supplierId); | ||||
|  | ||||
|         if (company != null && company is InvenTreeCompany) { | ||||
|           Navigator.push( | ||||
|               context, | ||||
|               MaterialPageRoute( | ||||
|                   builder: (context) => CompanyDetailWidget(company) | ||||
|               ) | ||||
|           ); | ||||
|         } | ||||
|       }, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget getBody(BuildContext context) { | ||||
|     return ListView.separated( | ||||
|       shrinkWrap: true, | ||||
|       physics: ClampingScrollPhysics(), | ||||
|       separatorBuilder: (_, __) => const Divider(height: 3), | ||||
|       itemCount: _supplierParts.length, | ||||
|       itemBuilder: _supplierPartTile, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
| } | ||||
		Reference in New Issue
	
	Block a user