This commit is contained in:
Hendrik Rauh 2026-02-03 10:58:18 +00:00 committed by GitHub
commit 69458f953b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 382 additions and 74 deletions

View file

@ -1,9 +1,11 @@
### x.xx.x - Month Year
### 0.22.0 - February 2026
---
- 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
- Updated translations
### 0.21.2 - January 2026
---

View file

@ -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;

View file

@ -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();

View file

@ -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": {},

View file

@ -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";

View file

@ -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;
});
},
),

View file

@ -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),

View 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;
});
},
),
),
],
),
),
);
}
}

View file

@ -61,6 +61,14 @@ abstract class PaginatedSearchState<T extends PaginatedSearchWidget>
backup,
);
if (result == "null") {
if (tristate) {
return null;
} else {
return backup;
}
}
return result;
}
@ -69,7 +77,7 @@ abstract class PaginatedSearchState<T extends PaginatedSearchWidget>
final String settings_key = "${prefix}filter_${key}";
if (value == null) {
await InvenTreeSettingsManager().removeValue(settings_key);
await InvenTreeSettingsManager().setValue(settings_key, "null");
} else {
await InvenTreeSettingsManager().setValue(settings_key, value);
}

View file

@ -123,10 +123,22 @@ class _PaginatedBomListState extends PaginatedSearchState<PaginatedBomList> {
@override
Map<String, Map<String, dynamic>> get filterOptions => {
"sub_part_active": {
"label": L10().filterActive,
"help_text": L10().filterActiveDetail,
"tristate": true,
"default": true,
},
"sub_part_assembly": {
"label": L10().filterAssembly,
"help_text": L10().filterAssemblyDetail,
},
"sub_part_virtual": {
"label": L10().filterVirtual,
"help_text": L10().filterVirtualDetail,
"tristate": true,
"default": true,
},
};
@override

View file

@ -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) {

View file

@ -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
*/

View file

@ -1,7 +1,7 @@
name: inventree
description: InvenTree stock management
version: 0.21.2+108
version: 0.22.0+109
environment:
sdk: ^3.8.1