mirror of
https://github.com/HendrikRauh/inventree-app.git
synced 2026-02-04 12:13:18 +00:00
Part requirements (#761)
* Add setting to control if part requirements are shown * Split settings for Part and Stock * Fetch part requirements information * Add "building" indicator * Show order allocation progress for part requirements * Bump release notes * Remove unused import
This commit is contained in:
parent
e38c51e947
commit
ee553af93b
10 changed files with 357 additions and 70 deletions
|
|
@ -1,6 +1,7 @@
|
|||
### x.xx.x - Month Year
|
||||
---
|
||||
|
||||
- Display overall part requirements on part detail view
|
||||
- Support display of custom status codes
|
||||
- Fix default values for list sorting
|
||||
- Fix bug related to null values in list filters
|
||||
|
|
|
|||
|
|
@ -353,6 +353,9 @@ class InvenTreeAPI {
|
|||
// Supports separate search against "supplier" / "customer" / "manufacturer"
|
||||
bool get supportsSplitCompanySearch => apiVersion >= 315;
|
||||
|
||||
// Supports "requirements" information for specific part
|
||||
bool get supportsPartRequirements => apiVersion >= 350;
|
||||
|
||||
// Does the server support the "modern" (consolidated) parameter API?
|
||||
// Ref: https://github.com/inventree/InvenTree/pull/10699
|
||||
bool get supportsModernParameters => apiVersion >= 429;
|
||||
|
|
|
|||
|
|
@ -214,6 +214,27 @@ class InvenTreePart extends InvenTreeModel {
|
|||
});
|
||||
}
|
||||
|
||||
// Request requirements information for this part
|
||||
Future<InvenTreePartRequirements?> getRequirements() async {
|
||||
try {
|
||||
final response = await InvenTreeAPI().get(
|
||||
"/api/part/${pk}/requirements/",
|
||||
);
|
||||
if (response.isValid()) {
|
||||
final requirementsData = response.data;
|
||||
|
||||
if (requirementsData is Map<String, dynamic>) {
|
||||
return InvenTreePartRequirements.fromJson(requirementsData);
|
||||
}
|
||||
}
|
||||
} catch (e, stackTrace) {
|
||||
print("Exception while fetching requirements data for part $pk: $e");
|
||||
sentryReportError("getRequirements", e, stackTrace);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Request pricing data for this part
|
||||
Future<InvenTreePartPricing?> getPricing() async {
|
||||
try {
|
||||
|
|
@ -437,6 +458,43 @@ class InvenTreePart extends InvenTreeModel {
|
|||
InvenTreePart.fromJson(json);
|
||||
}
|
||||
|
||||
/*
|
||||
* Class representing requirements information for a given Part instance.
|
||||
*/
|
||||
class InvenTreePartRequirements extends InvenTreeModel {
|
||||
InvenTreePartRequirements() : super();
|
||||
|
||||
InvenTreePartRequirements.fromJson(Map<String, dynamic> json)
|
||||
: super.fromJson(json);
|
||||
|
||||
@override
|
||||
List<String> get rolesRequired => ["part"];
|
||||
|
||||
@override
|
||||
InvenTreeModel createFromJson(Map<String, dynamic> json) =>
|
||||
InvenTreePartRequirements.fromJson(json);
|
||||
|
||||
// Data accessors
|
||||
double get canBuild => getDouble("can_build");
|
||||
|
||||
double get ordering => getDouble("ordering");
|
||||
|
||||
double get building => getDouble("building");
|
||||
|
||||
double get scheduledToBuild => getDouble("scheduled_to_build");
|
||||
|
||||
double get requiredForBuildOrders => getDouble("required_for_build_orders");
|
||||
|
||||
double get allocatedToBuildOrders => getDouble("allocated_to_build_orders");
|
||||
|
||||
double get requiredForSalesOrders => getDouble("required_for_sales_orders");
|
||||
|
||||
double get allocatedToSalesOrders => getDouble("allocated_to_sales_orders");
|
||||
}
|
||||
|
||||
/*
|
||||
* Class representing pricing information for a given Part instance.
|
||||
*/
|
||||
class InvenTreePartPricing extends InvenTreeModel {
|
||||
InvenTreePartPricing() : super();
|
||||
|
||||
|
|
|
|||
|
|
@ -68,6 +68,12 @@
|
|||
"allocatedStock": "Allocated Stock",
|
||||
"@allocatedStock": {},
|
||||
|
||||
"allocatedToBuildOrders": "Allocated to Build Orders",
|
||||
"@allocatedToBuildOrders": {},
|
||||
|
||||
"allocatedToSalesOrders": "Allocated to Sales Orders",
|
||||
"@allocatedToSalesOrders": {},
|
||||
|
||||
"appReleaseNotes": "Display app release notes",
|
||||
"@appReleaseNotes": {},
|
||||
|
||||
|
|
@ -226,6 +232,12 @@
|
|||
"cameraInternalDetail": "Use internal camera to read barcodes",
|
||||
"@cameraInternalDetail": {},
|
||||
|
||||
"canBuild": "Can Build",
|
||||
"@canBuild": {},
|
||||
|
||||
"canBuildDetail": "Can be produced with current stock",
|
||||
"@canBuildDetail": {},
|
||||
|
||||
"cancel": "Cancel",
|
||||
"@cancel": {
|
||||
"description": "Cancel"
|
||||
|
|
@ -936,6 +948,12 @@
|
|||
"partPricingSettingDetail": "Display part pricing information",
|
||||
"@pricingSettingDetail": {},
|
||||
|
||||
"partRequirements": "Part Requirements",
|
||||
"@partRequirements": {},
|
||||
|
||||
"partRequirementsSettingDetail": "Display part requirements",
|
||||
"@partRequirementsSettingDetail": {},
|
||||
|
||||
"partSettings": "Part Settings",
|
||||
"@partSettings": {},
|
||||
|
||||
|
|
@ -1535,6 +1553,9 @@
|
|||
"stockLocations": "Stock Locations",
|
||||
"@stockLocations": {},
|
||||
|
||||
"stockSettings": "Stock Settings",
|
||||
"@stockSettings": {},
|
||||
|
||||
"stockTopLevel": "Top level stock location",
|
||||
"@stockTopLevel": {},
|
||||
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ const String INV_LABEL_DEFAULT_PLUGIN = "defaultLabelPlugin";
|
|||
// Part settings
|
||||
const String INV_PART_SHOW_BOM = "partShowBom";
|
||||
const String INV_PART_SHOW_PRICING = "partShowPricing";
|
||||
const String INV_PART_SHOW_REQUIREMENTS = "partShowRequirements";
|
||||
|
||||
// Stock settings
|
||||
const String INV_STOCK_SHOW_HISTORY = "stockShowHistory";
|
||||
|
|
|
|||
|
|
@ -15,9 +15,7 @@ class _InvenTreePartSettingsState extends State<InvenTreePartSettingsWidget> {
|
|||
|
||||
bool partShowBom = true;
|
||||
bool partShowPricing = true;
|
||||
bool stockShowHistory = false;
|
||||
bool stockShowTests = false;
|
||||
bool stockConfirmScan = false;
|
||||
bool partShowRequirements = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
|
@ -35,16 +33,8 @@ class _InvenTreePartSettingsState extends State<InvenTreePartSettingsWidget> {
|
|||
INV_PART_SHOW_PRICING,
|
||||
true,
|
||||
);
|
||||
stockShowHistory = await InvenTreeSettingsManager().getBool(
|
||||
INV_STOCK_SHOW_HISTORY,
|
||||
false,
|
||||
);
|
||||
stockShowTests = await InvenTreeSettingsManager().getBool(
|
||||
INV_STOCK_SHOW_TESTS,
|
||||
true,
|
||||
);
|
||||
stockConfirmScan = await InvenTreeSettingsManager().getBool(
|
||||
INV_STOCK_CONFIRM_SCAN,
|
||||
partShowRequirements = await InvenTreeSettingsManager().getBool(
|
||||
INV_PART_SHOW_REQUIREMENTS,
|
||||
false,
|
||||
);
|
||||
|
||||
|
|
@ -94,54 +84,19 @@ class _InvenTreePartSettingsState extends State<InvenTreePartSettingsWidget> {
|
|||
},
|
||||
),
|
||||
),
|
||||
Divider(),
|
||||
ListTile(
|
||||
title: Text(L10().stockItemHistory),
|
||||
subtitle: Text(L10().stockItemHistoryDetail),
|
||||
leading: Icon(TablerIcons.history),
|
||||
title: Text(L10().partRequirements),
|
||||
subtitle: Text(L10().partRequirementsSettingDetail),
|
||||
leading: Icon(TablerIcons.list),
|
||||
trailing: Switch(
|
||||
value: stockShowHistory,
|
||||
value: partShowRequirements,
|
||||
onChanged: (bool value) {
|
||||
InvenTreeSettingsManager().setValue(
|
||||
INV_STOCK_SHOW_HISTORY,
|
||||
INV_PART_SHOW_REQUIREMENTS,
|
||||
value,
|
||||
);
|
||||
setState(() {
|
||||
stockShowHistory = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
title: Text(L10().testResults),
|
||||
subtitle: Text(L10().testResultsDetail),
|
||||
leading: Icon(TablerIcons.test_pipe),
|
||||
trailing: Switch(
|
||||
value: stockShowTests,
|
||||
onChanged: (bool value) {
|
||||
InvenTreeSettingsManager().setValue(
|
||||
INV_STOCK_SHOW_TESTS,
|
||||
value,
|
||||
);
|
||||
setState(() {
|
||||
stockShowTests = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
title: Text(L10().confirmScan),
|
||||
subtitle: Text(L10().confirmScanDetail),
|
||||
leading: Icon(TablerIcons.qrcode),
|
||||
trailing: Switch(
|
||||
value: stockConfirmScan,
|
||||
onChanged: (bool value) {
|
||||
InvenTreeSettingsManager().setValue(
|
||||
INV_STOCK_CONFIRM_SCAN,
|
||||
value,
|
||||
);
|
||||
setState(() {
|
||||
stockConfirmScan = value;
|
||||
partShowRequirements = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import "package:flutter/material.dart";
|
||||
import "package:flutter_tabler_icons/flutter_tabler_icons.dart";
|
||||
import "package:inventree/settings/stock_settings.dart";
|
||||
import "package:package_info_plus/package_info_plus.dart";
|
||||
|
||||
import "package:inventree/app_colors.dart";
|
||||
|
|
@ -119,6 +120,20 @@ class _InvenTreeSettingsState extends State<InvenTreeSettingsWidget> {
|
|||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text(L10().stock),
|
||||
subtitle: Text(L10().stockSettings),
|
||||
leading: Icon(TablerIcons.packages, color: COLOR_ACTION),
|
||||
trailing: LinkIcon(),
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => InvenTreeStockSettingsWidget(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text(L10().purchaseOrder),
|
||||
subtitle: Text(L10().purchaseOrderSettings),
|
||||
|
|
|
|||
110
lib/settings/stock_settings.dart
Normal file
110
lib/settings/stock_settings.dart
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
import "package:flutter/material.dart";
|
||||
import "package:flutter_tabler_icons/flutter_tabler_icons.dart";
|
||||
import "package:inventree/app_colors.dart";
|
||||
import "package:inventree/l10.dart";
|
||||
import "package:inventree/preferences.dart";
|
||||
|
||||
class InvenTreeStockSettingsWidget extends StatefulWidget {
|
||||
@override
|
||||
_InvenTreeStockSettingsState createState() => _InvenTreeStockSettingsState();
|
||||
}
|
||||
|
||||
class _InvenTreeStockSettingsState extends State<InvenTreeStockSettingsWidget> {
|
||||
_InvenTreeStockSettingsState();
|
||||
|
||||
bool stockShowHistory = false;
|
||||
bool stockShowTests = false;
|
||||
bool stockConfirmScan = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
loadSettings();
|
||||
}
|
||||
|
||||
Future<void> loadSettings() async {
|
||||
stockShowHistory = await InvenTreeSettingsManager().getBool(
|
||||
INV_STOCK_SHOW_HISTORY,
|
||||
false,
|
||||
);
|
||||
stockShowTests = await InvenTreeSettingsManager().getBool(
|
||||
INV_STOCK_SHOW_TESTS,
|
||||
true,
|
||||
);
|
||||
stockConfirmScan = await InvenTreeSettingsManager().getBool(
|
||||
INV_STOCK_CONFIRM_SCAN,
|
||||
false,
|
||||
);
|
||||
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(L10().stockSettings),
|
||||
backgroundColor: COLOR_APP_BAR,
|
||||
),
|
||||
body: Container(
|
||||
child: ListView(
|
||||
children: [
|
||||
ListTile(
|
||||
title: Text(L10().stockItemHistory),
|
||||
subtitle: Text(L10().stockItemHistoryDetail),
|
||||
leading: Icon(TablerIcons.history),
|
||||
trailing: Switch(
|
||||
value: stockShowHistory,
|
||||
onChanged: (bool value) {
|
||||
InvenTreeSettingsManager().setValue(
|
||||
INV_STOCK_SHOW_HISTORY,
|
||||
value,
|
||||
);
|
||||
setState(() {
|
||||
stockShowHistory = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
title: Text(L10().testResults),
|
||||
subtitle: Text(L10().testResultsDetail),
|
||||
leading: Icon(TablerIcons.test_pipe),
|
||||
trailing: Switch(
|
||||
value: stockShowTests,
|
||||
onChanged: (bool value) {
|
||||
InvenTreeSettingsManager().setValue(
|
||||
INV_STOCK_SHOW_TESTS,
|
||||
value,
|
||||
);
|
||||
setState(() {
|
||||
stockShowTests = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
title: Text(L10().confirmScan),
|
||||
subtitle: Text(L10().confirmScanDetail),
|
||||
leading: Icon(TablerIcons.qrcode),
|
||||
trailing: Switch(
|
||||
value: stockConfirmScan,
|
||||
onChanged: (bool value) {
|
||||
InvenTreeSettingsManager().setValue(
|
||||
INV_STOCK_CONFIRM_SCAN,
|
||||
value,
|
||||
);
|
||||
setState(() {
|
||||
stockConfirmScan = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -54,6 +54,7 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
|
|||
bool allowLabelPrinting = false;
|
||||
bool showBom = false;
|
||||
bool showPricing = false;
|
||||
bool showRequirements = false;
|
||||
|
||||
int parameterCount = 0;
|
||||
int attachmentCount = 0;
|
||||
|
|
@ -62,6 +63,7 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
|
|||
int variantCount = 0;
|
||||
|
||||
InvenTreePartPricing? partPricing;
|
||||
InvenTreePartRequirements? partRequirements;
|
||||
|
||||
@override
|
||||
String getAppBarTitle() => L10().partDetails;
|
||||
|
|
@ -148,6 +150,12 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
|
|||
final bool result = await part.reload();
|
||||
|
||||
// Load page settings from local storage
|
||||
|
||||
showRequirements = await InvenTreeSettingsManager().getBool(
|
||||
INV_PART_SHOW_REQUIREMENTS,
|
||||
false,
|
||||
);
|
||||
|
||||
showPricing = await InvenTreeSettingsManager().getBool(
|
||||
INV_PART_SHOW_PRICING,
|
||||
true,
|
||||
|
|
@ -233,6 +241,23 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
|
|||
});
|
||||
}
|
||||
|
||||
// If show requirements information?
|
||||
if (showRequirements && api.supportsPartRequirements) {
|
||||
part.getRequirements().then((InvenTreePartRequirements? requirements) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
partRequirements = requirements;
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
partRequirements = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// If show pricing information?
|
||||
if (showPricing) {
|
||||
part.getPricing().then((InvenTreePartPricing? pricing) {
|
||||
|
|
@ -242,6 +267,12 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
|
|||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
partPricing = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Request the number of BOM items
|
||||
|
|
@ -434,6 +465,103 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
|
|||
);
|
||||
}
|
||||
|
||||
// Part "requirements"
|
||||
if (showRequirements &&
|
||||
api.supportsPartRequirements &&
|
||||
partRequirements != null) {
|
||||
// Assembly parts
|
||||
if (part.isAssembly) {
|
||||
// Scheduled to build
|
||||
if (partRequirements!.building > 0 ||
|
||||
partRequirements!.scheduledToBuild > 0) {
|
||||
tiles.add(
|
||||
ListTile(
|
||||
title: Text(L10().building),
|
||||
subtitle: ProgressBar(
|
||||
partRequirements!.building,
|
||||
maximum: partRequirements!.scheduledToBuild,
|
||||
),
|
||||
leading: Icon(TablerIcons.tools),
|
||||
trailing: ProgressText(
|
||||
partRequirements!.building,
|
||||
maximum: partRequirements!.scheduledToBuild,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Can build
|
||||
if (part.isActive) {
|
||||
tiles.add(
|
||||
ListTile(
|
||||
title: Text(L10().canBuild),
|
||||
subtitle: Text(L10().canBuildDetail),
|
||||
trailing: LargeText(
|
||||
simpleNumberString(partRequirements!.canBuild),
|
||||
),
|
||||
leading: Icon(TablerIcons.check),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Build requirements
|
||||
if (partRequirements!.requiredForBuildOrders > 0 ||
|
||||
partRequirements!.allocatedToBuildOrders > 0) {
|
||||
tiles.add(
|
||||
ListTile(
|
||||
title: Text(L10().allocatedToBuildOrders),
|
||||
subtitle: ProgressBar(
|
||||
partRequirements!.allocatedToBuildOrders,
|
||||
maximum: partRequirements!.requiredForBuildOrders,
|
||||
),
|
||||
trailing: ProgressText(
|
||||
partRequirements!.allocatedToBuildOrders,
|
||||
maximum: partRequirements!.requiredForBuildOrders,
|
||||
),
|
||||
leading: Icon(TablerIcons.tools),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Sales requirements
|
||||
if (part.isSalable) {
|
||||
if (partRequirements!.requiredForSalesOrders > 0 ||
|
||||
partRequirements!.allocatedToSalesOrders > 0) {
|
||||
tiles.add(
|
||||
ListTile(
|
||||
title: Text(L10().allocatedToSalesOrders),
|
||||
subtitle: ProgressBar(
|
||||
partRequirements!.allocatedToSalesOrders,
|
||||
maximum: partRequirements!.requiredForSalesOrders,
|
||||
),
|
||||
trailing: ProgressText(
|
||||
partRequirements!.allocatedToSalesOrders,
|
||||
maximum: partRequirements!.requiredForSalesOrders,
|
||||
),
|
||||
leading: Icon(TablerIcons.truck_delivery),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Ordering stats
|
||||
if (part.isPurchaseable && partRequirements!.ordering > 0) {
|
||||
// On order
|
||||
tiles.add(
|
||||
ListTile(
|
||||
title: Text(L10().onOrder),
|
||||
subtitle: Text(L10().onOrderDetails),
|
||||
leading: Icon(TablerIcons.shopping_cart),
|
||||
trailing: LargeText("${part.onOrderString}"),
|
||||
onTap: () {
|
||||
// TODO - Order views
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (showPricing && partPricing != null) {
|
||||
String pricing = formatPriceRange(
|
||||
partPricing?.overallMin,
|
||||
|
|
@ -462,22 +590,6 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
|
|||
);
|
||||
}
|
||||
|
||||
// Tiles for "purchaseable" parts
|
||||
if (part.isPurchaseable) {
|
||||
// On order
|
||||
tiles.add(
|
||||
ListTile(
|
||||
title: Text(L10().onOrder),
|
||||
subtitle: Text(L10().onOrderDetails),
|
||||
leading: Icon(TablerIcons.shopping_cart),
|
||||
trailing: LargeText("${part.onOrderString}"),
|
||||
onTap: () {
|
||||
// TODO - Order views
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Tiles for an "assembly" part
|
||||
if (part.isAssembly) {
|
||||
if (showBom && bomCount > 0) {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ import "dart:io";
|
|||
import "package:flutter/material.dart";
|
||||
import "package:flutter_overlay_loader/flutter_overlay_loader.dart";
|
||||
import "package:inventree/app_colors.dart";
|
||||
import "package:inventree/helpers.dart";
|
||||
import "package:inventree/widget/link_icon.dart";
|
||||
import "package:one_context/one_context.dart";
|
||||
|
||||
/*
|
||||
|
|
@ -25,6 +27,15 @@ Widget ProgressBar(double value, {double maximum = 1.0}) {
|
|||
);
|
||||
}
|
||||
|
||||
Widget ProgressText(double value, {double maximum = 1.0}) {
|
||||
Color textColor = value < maximum ? COLOR_WARNING : COLOR_SUCCESS;
|
||||
|
||||
String v = simpleNumberString(value);
|
||||
String m = simpleNumberString(maximum);
|
||||
|
||||
return LargeText("${v} / ${m}", color: textColor);
|
||||
}
|
||||
|
||||
/*
|
||||
* Construct a circular progress indicator
|
||||
*/
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue