2
0
mirror of https://github.com/inventree/inventree-app.git synced 2025-10-09 10:52:20 +00:00

Format Code and Add Format Checks to CI (#643)

* Remove unused lib/generated/i18n.dart

* Use `fvm dart format .`

* Add contributing guidelines

* Enforce dart format

* Add `dart format off` directive to generated files
This commit is contained in:
Ben Hagen
2025-06-24 01:55:01 +02:00
committed by GitHub
parent e9db6532e4
commit 4444884afa
100 changed files with 5332 additions and 5592 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -4,7 +4,6 @@ import "package:inventree/helpers.dart";
import "package:one_context/one_context.dart";
bool isDarkMode() {
if (!hasContext()) {
return false;
}

View File

@@ -10,7 +10,6 @@ import "package:inventree/widget/company/manufacturer_part_detail.dart";
import "package:inventree/widget/order/sales_order_detail.dart";
import "package:one_context/one_context.dart";
import "package:inventree/api.dart";
import "package:inventree/l10.dart";
@@ -35,10 +34,8 @@ import "package:inventree/widget/stock/stock_detail.dart";
import "package:inventree/widget/company/company_detail.dart";
import "package:inventree/widget/company/supplier_part_detail.dart";
// Signal a barcode scan success to the user
Future<void> barcodeSuccess(String msg) async {
barcodeSuccessTone();
showSnackIcon(msg, success: true);
}
@@ -47,24 +44,23 @@ Future<void> barcodeSuccess(String msg) async {
Future<void> barcodeFailure(String msg, dynamic extra) async {
barcodeFailureTone();
showSnackIcon(
msg,
success: false,
msg,
success: false,
onAction: () {
if (hasContext()) {
OneContext().showDialog(
builder: (BuildContext context) =>
SimpleDialog(
title: Text(L10().barcodeError),
children: <Widget>[
ListTile(
title: Text(L10().responseData),
subtitle: Text(extra.toString())
)
]
)
);
}
}
if (hasContext()) {
OneContext().showDialog(
builder: (BuildContext context) => SimpleDialog(
title: Text(L10().barcodeError),
children: <Widget>[
ListTile(
title: Text(L10().responseData),
subtitle: Text(extra.toString()),
),
],
),
);
}
},
);
}
@@ -75,15 +71,22 @@ Future<void> barcodeFailure(String msg, dynamic extra) async {
* - Returns a Future which resolves when the scanner is dismissed
* - The provided BarcodeHandler instance is used to handle the scanned barcode
*/
Future<Object?> scanBarcode(BuildContext context, {BarcodeHandler? handler}) async {
Future<Object?> scanBarcode(
BuildContext context, {
BarcodeHandler? handler,
}) async {
// Default to generic scan handler
handler ??= BarcodeScanHandler();
InvenTreeBarcodeController controller = CameraBarcodeController(handler);
// Select barcode controller based on user preference
final int barcodeControllerType = await InvenTreeSettingsManager().getValue(INV_BARCODE_SCAN_TYPE, BARCODE_CONTROLLER_CAMERA) as int;
final int barcodeControllerType =
await InvenTreeSettingsManager().getValue(
INV_BARCODE_SCAN_TYPE,
BARCODE_CONTROLLER_CAMERA,
)
as int;
switch (barcodeControllerType) {
case BARCODE_CONTROLLER_WEDGE:
@@ -95,14 +98,10 @@ Future<Object?> scanBarcode(BuildContext context, {BarcodeHandler? handler}) asy
}
return Navigator.of(context).push(
PageRouteBuilder(
pageBuilder: (context, _, _) => controller,
opaque: false,
)
PageRouteBuilder(pageBuilder: (context, _, _) => controller, opaque: false),
);
}
/*
* Class for general barcode scanning.
* Scan *any* barcode without context, and then redirect app to correct view.
@@ -116,19 +115,17 @@ Future<Object?> scanBarcode(BuildContext context, {BarcodeHandler? handler}) asy
* - PurchaseOrder
*/
class BarcodeScanHandler extends BarcodeHandler {
@override
String getOverlayText(BuildContext context) => L10().barcodeScanGeneral;
@override
Future<void> onBarcodeUnknown(Map<String, dynamic> data) async {
barcodeFailureTone();
showSnackIcon(
L10().barcodeNoMatch,
icon: TablerIcons.exclamation_circle,
success: false,
L10().barcodeNoMatch,
icon: TablerIcons.exclamation_circle,
success: false,
);
}
@@ -136,12 +133,13 @@ class BarcodeScanHandler extends BarcodeHandler {
* Response when a "Part" instance is scanned
*/
Future<void> handlePart(int pk) async {
var part = await InvenTreePart().get(pk);
if (part is InvenTreePart) {
OneContext().pop();
OneContext().push(MaterialPageRoute(builder: (context) => PartDetailWidget(part)));
OneContext().push(
MaterialPageRoute(builder: (context) => PartDetailWidget(part)),
);
}
}
@@ -149,13 +147,13 @@ class BarcodeScanHandler extends BarcodeHandler {
* Response when a "StockItem" instance is scanned
*/
Future<void> handleStockItem(int pk) async {
var item = await InvenTreeStockItem().get(pk);
if (item is InvenTreeStockItem) {
OneContext().pop();
OneContext().push(MaterialPageRoute(
builder: (context) => StockDetailWidget(item)));
OneContext().push(
MaterialPageRoute(builder: (context) => StockDetailWidget(item)),
);
}
}
@@ -163,13 +161,13 @@ class BarcodeScanHandler extends BarcodeHandler {
* Response when a "StockLocation" instance is scanned
*/
Future<void> handleStockLocation(int pk) async {
var loc = await InvenTreeStockLocation().get(pk);
if (loc is InvenTreeStockLocation) {
OneContext().pop();
OneContext().navigator.push(MaterialPageRoute(
builder: (context) => LocationDisplayWidget(loc)));
OneContext().navigator.push(
MaterialPageRoute(builder: (context) => LocationDisplayWidget(loc)),
);
}
}
@@ -177,13 +175,15 @@ class BarcodeScanHandler extends BarcodeHandler {
* Response when a "SupplierPart" instance is scanned
*/
Future<void> handleSupplierPart(int pk) async {
var supplierPart = await InvenTreeSupplierPart().get(pk);
if (supplierPart is InvenTreeSupplierPart) {
OneContext().pop();
OneContext().push(MaterialPageRoute(
builder: (context) => SupplierPartDetailWidget(supplierPart)));
OneContext().push(
MaterialPageRoute(
builder: (context) => SupplierPartDetailWidget(supplierPart),
),
);
}
}
@@ -195,8 +195,11 @@ class BarcodeScanHandler extends BarcodeHandler {
if (manufacturerPart is InvenTreeManufacturerPart) {
OneContext().pop();
OneContext().push(MaterialPageRoute(
builder: (context) => ManufacturerPartDetailWidget(manufacturerPart)));
OneContext().push(
MaterialPageRoute(
builder: (context) => ManufacturerPartDetailWidget(manufacturerPart),
),
);
}
}
@@ -205,8 +208,9 @@ class BarcodeScanHandler extends BarcodeHandler {
if (company is InvenTreeCompany) {
OneContext().pop();
OneContext().push(MaterialPageRoute(
builder: (context) => CompanyDetailWidget(company)));
OneContext().push(
MaterialPageRoute(builder: (context) => CompanyDetailWidget(company)),
);
}
}
@@ -218,8 +222,11 @@ class BarcodeScanHandler extends BarcodeHandler {
if (order is InvenTreePurchaseOrder) {
OneContext().pop();
OneContext().push(MaterialPageRoute(
builder: (context) => PurchaseOrderDetailWidget(order)));
OneContext().push(
MaterialPageRoute(
builder: (context) => PurchaseOrderDetailWidget(order),
),
);
}
}
@@ -229,8 +236,9 @@ class BarcodeScanHandler extends BarcodeHandler {
if (order is InvenTreeSalesOrder) {
OneContext().pop();
OneContext().push(MaterialPageRoute(
builder: (context) => SalesOrderDetailWidget(order)));
OneContext().push(
MaterialPageRoute(builder: (context) => SalesOrderDetailWidget(order)),
);
}
}
@@ -250,7 +258,6 @@ class BarcodeScanHandler extends BarcodeHandler {
InvenTreeManufacturerPart.MODEL_TYPE,
];
if (InvenTreeAPI().supportsOrderBarcodes) {
validModels.add(InvenTreePurchaseOrder.MODEL_TYPE);
validModels.add(InvenTreeSalesOrder.MODEL_TYPE);
@@ -274,7 +281,6 @@ class BarcodeScanHandler extends BarcodeHandler {
// A valid result has been found
if (pk > 0 && model.isNotEmpty) {
barcodeSuccessTone();
switch (model) {
@@ -312,35 +318,31 @@ class BarcodeScanHandler extends BarcodeHandler {
barcodeFailureTone();
showSnackIcon(
L10().barcodeUnknown,
success: false,
onAction: () {
if (hasContext()) {
OneContext().showDialog(
builder: (BuildContext context) =>
SimpleDialog(
title: Text(L10().unknownResponse),
children: <Widget>[
ListTile(
title: Text(L10().responseData),
subtitle: Text(data.toString()),
)
],
)
);
}
L10().barcodeUnknown,
success: false,
onAction: () {
if (hasContext()) {
OneContext().showDialog(
builder: (BuildContext context) => SimpleDialog(
title: Text(L10().unknownResponse),
children: <Widget>[
ListTile(
title: Text(L10().responseData),
subtitle: Text(data.toString()),
),
],
),
);
}
},
);
}
}
/*
* Barcode handler for finding a "unique" barcode (one that does not match an item in the database)
*/
class UniqueBarcodeHandler extends BarcodeHandler {
UniqueBarcodeHandler(this.callback, {this.overlayText = ""});
// Callback function when a "unique" barcode hash is found
@@ -360,11 +362,7 @@ class UniqueBarcodeHandler extends BarcodeHandler {
@override
Future<void> onBarcodeMatched(Map<String, dynamic> data) async {
if (!data.containsKey("hash") && !data.containsKey("barcode_hash")) {
showServerError(
"barcode/",
L10().missingData,
L10().barcodeMissingHash,
);
showServerError("barcode/", L10().missingData, L10().barcodeMissingHash);
} else {
String barcode;
@@ -373,12 +371,8 @@ class UniqueBarcodeHandler extends BarcodeHandler {
if (barcode.isEmpty) {
barcodeFailureTone();
showSnackIcon(
L10().barcodeError,
success: false,
);
showSnackIcon(L10().barcodeError, success: false);
} else {
barcodeSuccessTone();
// Close the barcode scanner
@@ -395,41 +389,43 @@ class UniqueBarcodeHandler extends BarcodeHandler {
Future<void> onBarcodeUnknown(Map<String, dynamic> data) async {
await onBarcodeMatched(data);
}
}
SpeedDialChild customBarcodeAction(BuildContext context, RefreshableState state, String barcode, String model, int pk) {
SpeedDialChild customBarcodeAction(
BuildContext context,
RefreshableState state,
String barcode,
String model,
int pk,
) {
if (barcode.isEmpty) {
return SpeedDialChild(
label: L10().barcodeAssign,
child: Icon(Icons.barcode_reader),
onTap: () {
var handler = UniqueBarcodeHandler((String barcode) {
InvenTreeAPI().linkBarcode({
model: pk.toString(),
"barcode": barcode,
}).then((bool result) {
showSnackIcon(
result ? L10().barcodeAssigned : L10().barcodeNotAssigned,
success: result
);
InvenTreeAPI()
.linkBarcode({model: pk.toString(), "barcode": barcode})
.then((bool result) {
showSnackIcon(
result ? L10().barcodeAssigned : L10().barcodeNotAssigned,
success: result,
);
state.refresh(context);
});
state.refresh(context);
});
});
scanBarcode(context, handler: handler);
}
},
);
} else {
return SpeedDialChild(
child: Icon(Icons.barcode_reader),
label: L10().barcodeUnassign,
onTap: () {
InvenTreeAPI().unlinkBarcode({
model: pk.toString()
}).then((bool result) {
InvenTreeAPI().unlinkBarcode({model: pk.toString()}).then((
bool result,
) {
showSnackIcon(
result ? L10().requestSuccessful : L10().requestFailed,
success: result,
@@ -437,7 +433,7 @@ SpeedDialChild customBarcodeAction(BuildContext context, RefreshableState state,
state.refresh(context);
});
}
},
);
}
}

View File

@@ -273,8 +273,9 @@ class _CameraBarcodeControllerState extends InvenTreeBarcodeControllerState {
}
Widget bottomCenterOverlay() {
String info_text =
scanning_paused ? L10().barcodeScanPaused : L10().barcodeScanPause;
String info_text = scanning_paused
? L10().barcodeScanPaused
: L10().barcodeScanPause;
String text = scanned_code.isNotEmpty ? scanned_code : info_text;

View File

@@ -11,7 +11,6 @@ import "package:inventree/widget/progress.dart";
* which is used to process the scanned barcode.
*/
class InvenTreeBarcodeController extends StatefulWidget {
const InvenTreeBarcodeController(this.handler, {Key? key}) : super(key: key);
final BarcodeHandler handler;
@@ -20,16 +19,17 @@ class InvenTreeBarcodeController extends StatefulWidget {
State<StatefulWidget> createState() => InvenTreeBarcodeControllerState();
}
/*
* Base state widget for the barcode controller.
* This defines the basic interface for the barcode controller.
*/
class InvenTreeBarcodeControllerState extends State<InvenTreeBarcodeController> {
class InvenTreeBarcodeControllerState
extends State<InvenTreeBarcodeController> {
InvenTreeBarcodeControllerState() : super();
final GlobalKey barcodeControllerKey = GlobalKey(debugLabel: "barcodeController");
final GlobalKey barcodeControllerKey = GlobalKey(
debugLabel: "barcodeController",
);
// Internal state flag to test if we are currently processing a barcode
bool processingBarcode = false;
@@ -40,7 +40,6 @@ class InvenTreeBarcodeControllerState extends State<InvenTreeBarcodeController>
* Barcode data should be passed as a string
*/
Future<void> handleBarcodeData(String? data) async {
// Check that the data is valid, and this view is still mounted
if (!mounted || data == null || data.isEmpty) {
return;
@@ -66,7 +65,9 @@ class InvenTreeBarcodeControllerState extends State<InvenTreeBarcodeController>
return;
}
int delay = await InvenTreeSettingsManager().getValue(INV_BARCODE_SCAN_DELAY, 500) as int;
int delay =
await InvenTreeSettingsManager().getValue(INV_BARCODE_SCAN_DELAY, 500)
as int;
Future.delayed(Duration(milliseconds: delay), () {
hideLoadingOverlay();
@@ -99,5 +100,4 @@ class InvenTreeBarcodeControllerState extends State<InvenTreeBarcodeController>
Widget build(BuildContext context) {
return Container();
}
}
}

View File

@@ -1,4 +1,3 @@
import "package:flutter/material.dart";
import "package:flutter_tabler_icons/flutter_tabler_icons.dart";
@@ -13,7 +12,6 @@ import "package:inventree/inventree/sentry.dart";
import "package:inventree/widget/dialogs.dart";
import "package:inventree/widget/snacks.dart";
/* Generic class which "handles" a barcode, by communicating with the InvenTree server,
* and handling match / unknown / error cases.
*
@@ -21,7 +19,6 @@ import "package:inventree/widget/snacks.dart";
* based on the response returned from the InvenTree server
*/
class BarcodeHandler {
BarcodeHandler();
// Return the text to display on the barcode overlay
@@ -57,23 +54,23 @@ class BarcodeHandler {
*
* Returns true only if the barcode scanner should remain open
*/
Future<void> processBarcode(String barcode,
{String url = "barcode/",
Map<String, dynamic> extra_data = const {}}) async {
Future<void> processBarcode(
String barcode, {
String url = "barcode/",
Map<String, dynamic> extra_data = const {},
}) async {
debug("Scanned barcode data: '${barcode}'");
barcode = barcode.trim();
// Empty barcode is invalid
if (barcode.isEmpty) {
barcodeFailureTone();
showSnackIcon(
L10().barcodeError,
icon: TablerIcons.exclamation_circle,
success: false
success: false,
);
return;
@@ -84,10 +81,7 @@ class BarcodeHandler {
try {
response = await InvenTreeAPI().post(
url,
body: {
"barcode": barcode,
...extra_data,
},
body: {"barcode": barcode, ...extra_data},
expectedStatusCode: null, // Do not show an error on "unexpected code"
);
} catch (error, stackTrace) {
@@ -113,17 +107,17 @@ class BarcodeHandler {
// We want to know about this one!
await sentryReportMessage(
"BarcodeHandler.processBarcode returned unexpected value",
context: {
"data": response.data?.toString() ?? "null",
"barcode": barcode,
"url": url,
"statusCode": response.statusCode.toString(),
"valid": response.isValid().toString(),
"error": response.error,
"errorDetail": response.errorDetail,
"className": "${this}",
}
"BarcodeHandler.processBarcode returned unexpected value",
context: {
"data": response.data?.toString() ?? "null",
"barcode": barcode,
"url": url,
"statusCode": response.statusCode.toString(),
"valid": response.isValid().toString(),
"error": response.error,
"errorDetail": response.errorDetail,
"className": "${this}",
},
);
} else if (data.containsKey("success")) {
await onBarcodeMatched(data);

View File

@@ -20,7 +20,6 @@ import "package:inventree/widget/snacks.dart";
* - If location or quantity information wasn't provided, show a form to fill it in
*/
class POReceiveBarcodeHandler extends BarcodeHandler {
POReceiveBarcodeHandler({this.purchaseOrder, this.location, this.lineItem});
InvenTreePurchaseOrder? purchaseOrder;
@@ -31,11 +30,15 @@ class POReceiveBarcodeHandler extends BarcodeHandler {
String getOverlayText(BuildContext context) => L10().barcodeReceivePart;
@override
Future<void> processBarcode(String barcode,
{String url = "barcode/po-receive/",
Map<String, dynamic> extra_data = const {}}) async {
final bool confirm = await InvenTreeSettingsManager().getBool(INV_PO_CONFIRM_SCAN, true);
Future<void> processBarcode(
String barcode, {
String url = "barcode/po-receive/",
Map<String, dynamic> extra_data = const {},
}) async {
final bool confirm = await InvenTreeSettingsManager().getBool(
INV_PO_CONFIRM_SCAN,
true,
);
final po_extra_data = {
"purchase_order": purchaseOrder?.pk,
@@ -50,7 +53,6 @@ class POReceiveBarcodeHandler extends BarcodeHandler {
@override
Future<void> onBarcodeMatched(Map<String, dynamic> data) async {
if (data.containsKey("lineitem") || data.containsKey("success")) {
barcodeSuccess(L10().receivedItem);
return;
@@ -66,7 +68,8 @@ class POReceiveBarcodeHandler extends BarcodeHandler {
}
final lineItemData = data["lineitem"] as Map<String, dynamic>;
if (!lineItemData.containsKey("pk") || !lineItemData.containsKey("purchase_order")) {
if (!lineItemData.containsKey("pk") ||
!lineItemData.containsKey("purchase_order")) {
barcodeFailureTone();
showSnackIcon(L10().missingData, success: false);
}
@@ -79,7 +82,8 @@ class POReceiveBarcodeHandler extends BarcodeHandler {
return;
}
InvenTreePOLineItem? lineItem = await InvenTreePOLineItem().get(lineItemId) as InvenTreePOLineItem?;
InvenTreePOLineItem? lineItem =
await InvenTreePOLineItem().get(lineItemId) as InvenTreePOLineItem?;
if (lineItem == null) {
barcodeFailureTone();
@@ -89,7 +93,9 @@ class POReceiveBarcodeHandler extends BarcodeHandler {
// Next, extract the "optional" fields
// Extract information from the returned server response
double? quantity = double.tryParse((lineItemData["quantity"] ?? "0").toString());
double? quantity = double.tryParse(
(lineItemData["quantity"] ?? "0").toString(),
);
int? destination = lineItemData["location"] as int?;
String? barcode = data["barcode_data"] as String?;
@@ -105,7 +111,7 @@ class POReceiveBarcodeHandler extends BarcodeHandler {
barcode: barcode,
onSuccess: () {
showSnackIcon(L10().receivedItem, success: true);
}
},
);
}
@@ -113,18 +119,16 @@ class POReceiveBarcodeHandler extends BarcodeHandler {
Future<void> onBarcodeUnknown(Map<String, dynamic> data) async {
barcodeFailureTone();
showSnackIcon(
data["error"] as String? ?? L10().barcodeError,
success: false
data["error"] as String? ?? L10().barcodeError,
success: false,
);
}
}
/*
* Barcode handler to add a line item to a purchase order
*/
class POAllocateBarcodeHandler extends BarcodeHandler {
POAllocateBarcodeHandler({this.purchaseOrder});
InvenTreePurchaseOrder? purchaseOrder;
@@ -133,21 +137,14 @@ class POAllocateBarcodeHandler extends BarcodeHandler {
String getOverlayText(BuildContext context) => L10().scanSupplierPart;
@override
Future<void> processBarcode(String barcode, {
Future<void> processBarcode(
String barcode, {
String url = "barcode/po-allocate/",
Map<String, dynamic> extra_data = const {}}
) {
Map<String, dynamic> extra_data = const {},
}) {
final po_extra_data = {"purchase_order": purchaseOrder?.pk, ...extra_data};
final po_extra_data = {
"purchase_order": purchaseOrder?.pk,
...extra_data,
};
return super.processBarcode(
barcode,
url: url,
extra_data: po_extra_data,
);
return super.processBarcode(barcode, url: url, extra_data: po_extra_data);
}
@override
@@ -189,10 +186,9 @@ class POAllocateBarcodeHandler extends BarcodeHandler {
@override
Future<void> onBarcodeUnhandled(Map<String, dynamic> data) async {
print("onBarcodeUnhandled:");
print(data.toString());
super.onBarcodeUnhandled(data);
}
}
}

View File

@@ -14,13 +14,11 @@ import "package:inventree/barcode/tones.dart";
import "package:inventree/widget/snacks.dart";
/*
* Barcode handler class for scanning a new part into a SalesOrder
*/
class SOAddItemBarcodeHandler extends BarcodeHandler {
SOAddItemBarcodeHandler({this.salesOrder});
InvenTreeSalesOrder? salesOrder;
@@ -30,7 +28,6 @@ class SOAddItemBarcodeHandler extends BarcodeHandler {
@override
Future<void> onBarcodeMatched(Map<String, dynamic> data) async {
// Extract the part ID from the returned data
int part_id = -1;
@@ -46,7 +43,6 @@ class SOAddItemBarcodeHandler extends BarcodeHandler {
var part = await InvenTreePart().get(part_id);
if (part is InvenTreePart) {
if (part.isSalable) {
// Dispose of the barcode scanner
if (OneContext.hasContext) {
@@ -68,23 +64,18 @@ class SOAddItemBarcodeHandler extends BarcodeHandler {
L10().lineItemAdd,
fields: fields,
);
} else {
barcodeFailureTone();
showSnackIcon(L10().partNotSalable, success: false);
}
} else {
// Failed to fetch part
return onBarcodeUnknown(data);
}
}
}
class SOAllocateStockHandler extends BarcodeHandler {
SOAllocateStockHandler({this.salesOrder, this.lineItem, this.shipment});
InvenTreeSalesOrder? salesOrder;
@@ -95,16 +86,16 @@ class SOAllocateStockHandler extends BarcodeHandler {
String getOverlayText(BuildContext context) => L10().allocateStock;
@override
Future<void> processBarcode(String barcode,
{
Future<void> processBarcode(
String barcode, {
String url = "barcode/so-allocate/",
Map<String, dynamic> extra_data = const {}}) {
Map<String, dynamic> extra_data = const {},
}) {
final so_extra_data = {
"sales_order": salesOrder?.pk,
"shipment": shipment?.pk,
"line": lineItem?.pk,
...extra_data
...extra_data,
};
return super.processBarcode(barcode, url: url, extra_data: so_extra_data);
@@ -121,8 +112,8 @@ class SOAllocateStockHandler extends BarcodeHandler {
@override
Future<void> onBarcodeUnhandled(Map<String, dynamic> data) async {
if (!data.containsKey("action_required") || !data.containsKey("line_item")) {
if (!data.containsKey("action_required") ||
!data.containsKey("line_item")) {
return super.onBarcodeUnhandled(data);
}
@@ -132,10 +123,7 @@ class SOAllocateStockHandler extends BarcodeHandler {
// Update fields with data gathered from the API response
fields["line_item"]?["value"] = data["line_item"];
Map<String, dynamic> stock_filters = {
"in_stock": true,
"available": true,
};
Map<String, dynamic> stock_filters = {"in_stock": true, "available": true};
if (data.containsKey("part")) {
stock_filters["part"] = data["part"];
@@ -147,9 +135,7 @@ class SOAllocateStockHandler extends BarcodeHandler {
fields["quantity"]?["value"] = data["quantity"];
fields["shipment"]?["value"] = data["shipment"];
fields["shipment"]?["filters"] = {
"order": salesOrder!.pk.toString()
};
fields["shipment"]?["filters"] = {"order": salesOrder!.pk.toString()};
final context = OneContext().context!;
@@ -157,20 +143,21 @@ class SOAllocateStockHandler extends BarcodeHandler {
context,
L10().allocateStock,
salesOrder!.allocate_url,
fields,
method: "POST",
icon: TablerIcons.transition_right,
onSuccess: (data) async {
fields,
method: "POST",
icon: TablerIcons.transition_right,
onSuccess: (data) async {
showSnackIcon(L10().allocated, success: true);
});
},
);
}
@override
Future<void> onBarcodeUnknown(Map<String, dynamic> data) async {
barcodeFailureTone();
showSnackIcon(
data["error"] as String? ?? L10().barcodeError,
success: false
data["error"] as String? ?? L10().barcodeError,
success: false,
);
}
}
}

View File

@@ -16,7 +16,6 @@ import "package:inventree/inventree/stock.dart";
import "package:inventree/widget/dialogs.dart";
import "package:inventree/widget/snacks.dart";
/*
* Generic class for scanning a StockLocation.
*
@@ -24,20 +23,17 @@ import "package:inventree/widget/snacks.dart";
* - Runs a "callback" function if a valid StockLocation is found
*/
class BarcodeScanStockLocationHandler extends BarcodeHandler {
@override
String getOverlayText(BuildContext context) => L10().barcodeScanLocation;
@override
Future<void> onBarcodeMatched(Map<String, dynamic> data) async {
// We expect that the barcode points to a 'stocklocation'
if (data.containsKey("stocklocation")) {
int _loc = (data["stocklocation"]?["pk"] ?? -1) as int;
// A valid stock location!
if (_loc > 0) {
debug("Scanned stock location ${_loc}");
final bool result = await onLocationScanned(_loc);
@@ -52,10 +48,7 @@ class BarcodeScanStockLocationHandler extends BarcodeHandler {
// If we get to this point, something went wrong during the scan process
barcodeFailureTone();
showSnackIcon(
L10().invalidStockLocation,
success: false,
);
showSnackIcon(L10().invalidStockLocation, success: false);
}
// Callback function which runs when a valid StockLocation is scanned
@@ -64,10 +57,8 @@ class BarcodeScanStockLocationHandler extends BarcodeHandler {
// Re-implement this for particular subclass
return false;
}
}
/*
* Generic class for scanning a StockItem
*
@@ -75,7 +66,6 @@ class BarcodeScanStockLocationHandler extends BarcodeHandler {
* - Runs a "callback" function if a valid StockItem is found
*/
class BarcodeScanStockItemHandler extends BarcodeHandler {
@override
String getOverlayText(BuildContext context) => L10().barcodeScanItem;
@@ -87,7 +77,6 @@ class BarcodeScanStockItemHandler extends BarcodeHandler {
// A valid stock location!
if (_item > 0) {
barcodeSuccessTone();
bool result = await onItemScanned(_item);
@@ -102,10 +91,7 @@ class BarcodeScanStockItemHandler extends BarcodeHandler {
// If we get to this point, something went wrong during the scan process
barcodeFailureTone();
showSnackIcon(
L10().invalidStockItem,
success: false,
);
showSnackIcon(L10().invalidStockItem, success: false);
}
// Callback function which runs when a valid StockItem is scanned
@@ -115,7 +101,6 @@ class BarcodeScanStockItemHandler extends BarcodeHandler {
}
}
/*
* Barcode handler for scanning a provided StockItem into a scanned StockLocation.
*
@@ -124,20 +109,20 @@ class BarcodeScanStockItemHandler extends BarcodeHandler {
* - The StockItem is transferred into the scanned location
*/
class StockItemScanIntoLocationHandler extends BarcodeScanStockLocationHandler {
StockItemScanIntoLocationHandler(this.item);
final InvenTreeStockItem item;
@override
Future<bool> onLocationScanned(int locationId) async {
final bool confirm = await InvenTreeSettingsManager().getBool(INV_STOCK_CONFIRM_SCAN, false);
final bool confirm = await InvenTreeSettingsManager().getBool(
INV_STOCK_CONFIRM_SCAN,
false,
);
bool result = false;
if (confirm) {
Map<String, dynamic> fields = item.transferFields();
// Override location with scanned value
@@ -152,7 +137,7 @@ class StockItemScanIntoLocationHandler extends BarcodeScanStockLocationHandler {
icon: TablerIcons.transfer,
onSuccess: (data) async {
showSnackIcon(L10().stockItemUpdated, success: true);
}
},
);
return true;
@@ -171,7 +156,6 @@ class StockItemScanIntoLocationHandler extends BarcodeScanStockLocationHandler {
}
}
/*
* Barcode handler for scanning stock item(s) into the specified StockLocation.
*
@@ -180,7 +164,6 @@ class StockItemScanIntoLocationHandler extends BarcodeScanStockLocationHandler {
* - The scanned StockItem is transferred into the provided StockLocation
*/
class StockLocationScanInItemsHandler extends BarcodeScanStockItemHandler {
StockLocationScanInItemsHandler(this.location);
final InvenTreeStockLocation location;
@@ -190,14 +173,16 @@ class StockLocationScanInItemsHandler extends BarcodeScanStockItemHandler {
@override
Future<bool> onItemScanned(int itemId) async {
final InvenTreeStockItem? item = await InvenTreeStockItem().get(itemId) as InvenTreeStockItem?;
final bool confirm = await InvenTreeSettingsManager().getBool(INV_STOCK_CONFIRM_SCAN, false);
final InvenTreeStockItem? item =
await InvenTreeStockItem().get(itemId) as InvenTreeStockItem?;
final bool confirm = await InvenTreeSettingsManager().getBool(
INV_STOCK_CONFIRM_SCAN,
false,
);
bool result = false;
if (item != null) {
// Item is already *in* the specified location
if (item.locationId == location.pk) {
barcodeFailureTone();
@@ -211,25 +196,26 @@ class StockLocationScanInItemsHandler extends BarcodeScanStockItemHandler {
fields["location"]?["value"] = location.pk;
launchApiForm(
OneContext().context!,
L10().transferStock,
InvenTreeStockItem.transferStockUrl(),
fields,
method: "POST",
icon: TablerIcons.transfer,
onSuccess: (data) async {
showSnackIcon(L10().stockItemUpdated, success: true);
}
OneContext().context!,
L10().transferStock,
InvenTreeStockItem.transferStockUrl(),
fields,
method: "POST",
icon: TablerIcons.transfer,
onSuccess: (data) async {
showSnackIcon(L10().stockItemUpdated, success: true);
},
);
return true;
} else {
result = await item.transferStock(location.pk);
showSnackIcon(
result ? L10().barcodeScanIntoLocationSuccess : L10().barcodeScanIntoLocationFailure,
success: result
result
? L10().barcodeScanIntoLocationSuccess
: L10().barcodeScanIntoLocationFailure,
success: result,
);
}
}
@@ -240,7 +226,6 @@ class StockLocationScanInItemsHandler extends BarcodeScanStockItemHandler {
}
}
/*
* Barcode handler class for scanning a StockLocation into another StockLocation
*
@@ -249,18 +234,14 @@ class StockLocationScanInItemsHandler extends BarcodeScanStockItemHandler {
* - The scanned StockLocation is set as the "parent" of the provided StockLocation
*/
class ScanParentLocationHandler extends BarcodeScanStockLocationHandler {
ScanParentLocationHandler(this.location);
final InvenTreeStockLocation location;
@override
Future<bool> onLocationScanned(int locationId) async {
final response = await location.update(
values: {
"parent": locationId.toString(),
},
values: {"parent": locationId.toString()},
expectedStatusCode: null,
);
@@ -269,22 +250,19 @@ class ScanParentLocationHandler extends BarcodeScanStockLocationHandler {
case 201:
barcodeSuccess(L10().barcodeScanIntoLocationSuccess);
return true;
case 400: // Invalid parent location chosen
case 400: // Invalid parent location chosen
barcodeFailureTone();
showSnackIcon(L10().invalidStockLocation, success: false);
return false;
default:
barcodeFailureTone();
showSnackIcon(
L10().barcodeScanIntoLocationFailure,
success: false,
actionText: L10().details,
onAction: () {
showErrorDialog(
L10().barcodeError,
response: response,
);
}
L10().barcodeScanIntoLocationFailure,
success: false,
actionText: L10().details,
onAction: () {
showErrorDialog(L10().barcodeError, response: response);
},
);
return false;
}

View File

@@ -5,19 +5,21 @@ import "package:inventree/preferences.dart";
* Play an audible 'success' alert to the user.
*/
Future<void> barcodeSuccessTone() async {
final bool en = await InvenTreeSettingsManager().getValue(INV_SOUNDS_BARCODE, true) as bool;
final bool en =
await InvenTreeSettingsManager().getValue(INV_SOUNDS_BARCODE, true)
as bool;
if (en) {
playAudioFile("sounds/barcode_scan.mp3");
}
}
Future <void> barcodeFailureTone() async {
final bool en = await InvenTreeSettingsManager().getValue(INV_SOUNDS_BARCODE, true) as bool;
Future<void> barcodeFailureTone() async {
final bool en =
await InvenTreeSettingsManager().getValue(INV_SOUNDS_BARCODE, true)
as bool;
if (en) {
playAudioFile("sounds/barcode_error.mp3");
}
}
}

View File

@@ -1,4 +1,3 @@
import "package:flutter/material.dart";
import "package:flutter/services.dart";
import "package:flutter_tabler_icons/flutter_tabler_icons.dart";
@@ -15,17 +14,14 @@ import "package:inventree/helpers.dart";
* intercepting barcode data which is entered as rapid keyboard presses
*/
class WedgeBarcodeController extends InvenTreeBarcodeController {
const WedgeBarcodeController(BarcodeHandler handler, {Key? key}) : super(handler, key: key);
const WedgeBarcodeController(BarcodeHandler handler, {Key? key})
: super(handler, key: key);
@override
State<StatefulWidget> createState() => _WedgeBarcodeControllerState();
}
class _WedgeBarcodeControllerState extends InvenTreeBarcodeControllerState {
_WedgeBarcodeControllerState() : super();
bool canScan = true;
@@ -40,7 +36,6 @@ class _WedgeBarcodeControllerState extends InvenTreeBarcodeControllerState {
@override
Future<void> pauseScan() async {
if (mounted) {
setState(() {
canScan = false;
@@ -50,7 +45,6 @@ class _WedgeBarcodeControllerState extends InvenTreeBarcodeControllerState {
@override
Future<void> resumeScan() async {
if (mounted) {
setState(() {
canScan = true;
@@ -60,7 +54,6 @@ class _WedgeBarcodeControllerState extends InvenTreeBarcodeControllerState {
// Callback for a single key press / scan
void handleKeyEvent(KeyEvent event) {
if (!scanning) {
return;
}
@@ -78,7 +71,8 @@ class _WedgeBarcodeControllerState extends InvenTreeBarcodeControllerState {
DateTime now = DateTime.now();
// Throw away old characters
if (_lastScanTime == null || _lastScanTime!.isBefore(now.subtract(Duration(milliseconds: 250)))) {
if (_lastScanTime == null ||
_lastScanTime!.isBefore(now.subtract(Duration(milliseconds: 250)))) {
_scannedCharacters.clear();
}
@@ -99,7 +93,6 @@ class _WedgeBarcodeControllerState extends InvenTreeBarcodeControllerState {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: COLOR_APP_BAR,
@@ -118,7 +111,7 @@ class _WedgeBarcodeControllerState extends InvenTreeBarcodeControllerState {
focusNode: _focusNode,
child: SizedBox(
child: CircularProgressIndicator(
color: scanning ? COLOR_ACTION : COLOR_PROGRESS
color: scanning ? COLOR_ACTION : COLOR_PROGRESS,
),
width: 64,
height: 64,
@@ -140,14 +133,14 @@ class _WedgeBarcodeControllerState extends InvenTreeBarcodeControllerState {
widget.handler.getOverlayText(context),
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.white)
color: Colors.white,
),
),
padding: EdgeInsets.all(20),
)
),
],
)
)
),
),
);
}
}
}

View File

@@ -1,7 +1,7 @@
/*
* For integration with sentry.io, fill out the SENTRY_DSN_KEY value below.
* This should be set to a valid DSN key, from your sentry.io account
*
*/
String SENTRY_DSN_KEY = "https://fea705aa4b8e4c598dcf9b146b3d1b86@o378676.ingest.sentry.io/5202450";
String SENTRY_DSN_KEY =
"https://fea705aa4b8e4c598dcf9b146b3d1b86@o378676.ingest.sentry.io/5202450";

View File

@@ -1,76 +0,0 @@
import "dart:async';
import "package:flutter/foundation.dart';
import "package:flutter/material.dart';
// ignore_for_file: non_constant_identifier_names
// ignore_for_file: camel_case_types
// ignore_for_file: prefer_single_quotes
//This file is automatically generated. DO NOT EDIT, all your changes would be lost.
class S implements WidgetsLocalizations {
const S();
static const GeneratedLocalizationsDelegate delegate = GeneratedLocalizationsDelegate();
static S of(BuildContext context) => Localizations.of<S>(context, WidgetsLocalizations);
@override
TextDirection get textDirection => TextDirection.ltr;
}
class en extends S {
const en();
}
class GeneratedLocalizationsDelegate extends LocalizationsDelegate<WidgetsLocalizations> {
const GeneratedLocalizationsDelegate();
List<Locale> get supportedLocales {
return const <Locale>[
const Locale("en", ""),
];
}
LocaleResolutionCallback resolution({Locale fallback}) {
return (Locale locale, Iterable<Locale> supported) {
final Locale languageLocale = new Locale(locale.languageCode, "");
if (supported.contains(locale))
return locale;
else if (supported.contains(languageLocale))
return languageLocale;
else {
final Locale fallbackLocale = fallback ?? supported.first;
return fallbackLocale;
}
};
}
@override
Future<WidgetsLocalizations> load(Locale locale) {
final String lang = getLang(locale);
switch (lang) {
case "en":
return new SynchronousFuture<WidgetsLocalizations>(const en());
default:
return new SynchronousFuture<WidgetsLocalizations>(const S());
}
}
@override
bool isSupported(Locale locale) => supportedLocales.contains(locale);
@override
bool shouldReload(GeneratedLocalizationsDelegate old) => false;
}
String getLang(Locale l) => l.countryCode != null && l.countryCode.isEmpty
? l.languageCode
: l.toString();

View File

@@ -17,8 +17,6 @@ import "package:audioplayers/audioplayers.dart";
import "package:inventree/l10.dart";
import "package:inventree/widget/snacks.dart";
List<String> debug_messages = [];
void clearDebugMessage() => debug_messages.clear();
@@ -44,14 +42,12 @@ bool debugContains(String msg, {bool raiseAssert = true}) {
}
if (raiseAssert) {
assert(result);
}
return result;
}
bool isTesting() {
return Platform.environment.containsKey("FLUTTER_TEST");
}
@@ -64,12 +60,10 @@ bool hasContext() {
}
}
/*
* Display a debug message if we are in testing mode, or running in debug mode
*/
void debug(dynamic msg) {
if (Platform.environment.containsKey("FLUTTER_TEST")) {
debug_messages.add(msg.toString());
}
@@ -77,13 +71,11 @@ void debug(dynamic msg) {
print("DEBUG: ${msg.toString()}");
}
/*
* Simplify string representation of a floating point value
* Basically, don't display fractional component if it is an integer
*/
String simpleNumberString(double number) {
if (number.toInt() == number) {
return number.toInt().toString();
} else {
@@ -98,7 +90,6 @@ String simpleNumberString(double number) {
* we will not attempt to play the sound
*/
Future<void> playAudioFile(String path) async {
// Debug message for unit testing
debug("Playing audio file: '${path}'");
@@ -110,21 +101,21 @@ Future<void> playAudioFile(String path) async {
// Specify context options for the audio player
// Ref: https://github.com/inventree/inventree-app/issues/582
player.setAudioContext(AudioContext(
android: AudioContextAndroid(
usageType: AndroidUsageType.notification,
audioFocus: AndroidAudioFocus.none,
player.setAudioContext(
AudioContext(
android: AudioContextAndroid(
usageType: AndroidUsageType.notification,
audioFocus: AndroidAudioFocus.none,
),
iOS: AudioContextIOS(),
),
iOS: AudioContextIOS()
));
);
player.play(AssetSource(path));
}
// Open an external URL
Future<void> openLink(String url) async {
final link = Uri.parse(url);
try {
@@ -134,24 +125,20 @@ Future<void> openLink(String url) async {
}
}
/*
* Helper function for rendering a money / currency object as a String
*/
String renderCurrency(double? amount, String currency, {int decimals = 2}) {
if (amount == null || amount.isInfinite || amount.isNaN) return "-";
currency = currency.trim();
if (currency.isEmpty) return "-";
CurrencyFormat fmt = CurrencyFormat.fromCode(currency.toLowerCase()) ?? CurrencyFormat.usd;
CurrencyFormat fmt =
CurrencyFormat.fromCode(currency.toLowerCase()) ?? CurrencyFormat.usd;
String value = CurrencyFormatter.format(
amount,
fmt
);
String value = CurrencyFormatter.format(amount, fmt);
return value;
}
@@ -163,8 +150,11 @@ bool isValidNumber(double? value) {
/*
* Render a "range" of prices between two values.
*/
String formatPriceRange(double? minPrice, double? maxPrice, { String? currency }) {
String formatPriceRange(
double? minPrice,
double? maxPrice, {
String? currency,
}) {
// Account for empty or null values
if (!isValidNumber(minPrice) && !isValidNumber(maxPrice)) {
return "-";

View File

@@ -1,4 +1,3 @@
import "package:inventree/inventree/model.dart";
import "package:inventree/inventree/part.dart";
@@ -6,13 +5,13 @@ import "package:inventree/inventree/part.dart";
* Class representing the BomItem database model
*/
class InvenTreeBomItem extends InvenTreeModel {
InvenTreeBomItem() : super();
InvenTreeBomItem.fromJson(Map<String, dynamic> json) : super.fromJson(json);
@override
InvenTreeModel createFromJson(Map<String, dynamic> json) => InvenTreeBomItem.fromJson(json);
InvenTreeModel createFromJson(Map<String, dynamic> json) =>
InvenTreeBomItem.fromJson(json);
@override
String get URL => "bom/";
@@ -28,7 +27,7 @@ class InvenTreeBomItem extends InvenTreeModel {
// Extract the 'reference' value associated with this BomItem
String get reference => getString("reference");
// Extract the 'quantity' value associated with this BomItem
double get quantity => getDouble("quantity");
@@ -57,8 +56,8 @@ class InvenTreeBomItem extends InvenTreeModel {
}
return null;
}
}
// Extract the ID of the related sub-part
int get subPartId => getInt("sub_part");
}
}

View File

@@ -6,13 +6,11 @@ import "package:inventree/inventree/model.dart";
import "package:inventree/inventree/purchase_order.dart";
import "package:inventree/widget/company/company_detail.dart";
/*
* The InvenTreeCompany class represents the Company model in the InvenTree database.
*/
class InvenTreeCompany extends InvenTreeModel {
InvenTreeCompany() : super();
InvenTreeCompany.fromJson(Map<String, dynamic> json) : super.fromJson(json);
@@ -26,14 +24,16 @@ class InvenTreeCompany extends InvenTreeModel {
Future<Object?> goToDetailPage(BuildContext context) async {
return Navigator.push(
context,
MaterialPageRoute(
builder: (context) => CompanyDetailWidget(this)
)
MaterialPageRoute(builder: (context) => CompanyDetailWidget(this)),
);
}
@override
List<String> get rolesRequired => ["purchase_order", "sales_order", "return_order"];
List<String> get rolesRequired => [
"purchase_order",
"sales_order",
"return_order",
];
@override
Map<String, Map<String, dynamic>> formFields() {
@@ -54,12 +54,16 @@ class InvenTreeCompany extends InvenTreeModel {
return fields;
}
String get image => (jsondata["image"] ?? jsondata["thumbnail"] ?? InvenTreeAPI.staticImage) as String;
String get image =>
(jsondata["image"] ?? jsondata["thumbnail"] ?? InvenTreeAPI.staticImage)
as String;
String get thumbnail => (jsondata["thumbnail"] ?? jsondata["image"] ?? InvenTreeAPI.staticThumb) as String;
String get thumbnail =>
(jsondata["thumbnail"] ?? jsondata["image"] ?? InvenTreeAPI.staticThumb)
as String;
String get website => getString("website");
String get phone => getString("phone");
String get email => getString("email");
@@ -73,22 +77,21 @@ class InvenTreeCompany extends InvenTreeModel {
bool get active => getBool("active", backup: true);
int get partSuppliedCount => getInt("part_supplied");
int get partManufacturedCount => getInt("parts_manufactured");
// Request a list of purchase orders against this company
Future<List<InvenTreePurchaseOrder>> getPurchaseOrders({bool? outstanding}) async {
Map<String, String> filters = {
"supplier": "${pk}"
};
int get partManufacturedCount => getInt("parts_manufactured");
// Request a list of purchase orders against this company
Future<List<InvenTreePurchaseOrder>> getPurchaseOrders({
bool? outstanding,
}) async {
Map<String, String> filters = {"supplier": "${pk}"};
if (outstanding != null) {
filters["outstanding"] = outstanding ? "true" : "false";
}
final List<InvenTreeModel> results = await InvenTreePurchaseOrder().list(
filters: filters
filters: filters,
);
List<InvenTreePurchaseOrder> orders = [];
@@ -103,18 +106,18 @@ class InvenTreeCompany extends InvenTreeModel {
}
@override
InvenTreeModel createFromJson(Map<String, dynamic> json) => InvenTreeCompany.fromJson(json);
InvenTreeModel createFromJson(Map<String, dynamic> json) =>
InvenTreeCompany.fromJson(json);
}
/*
* Class representing an attachment file against a Company object
*/
class InvenTreeCompanyAttachment extends InvenTreeAttachment {
InvenTreeCompanyAttachment() : super();
InvenTreeCompanyAttachment.fromJson(Map<String, dynamic> json) : super.fromJson(json);
InvenTreeCompanyAttachment.fromJson(Map<String, dynamic> json)
: super.fromJson(json);
@override
String get REFERENCE_FIELD => "company";
@@ -123,21 +126,23 @@ class InvenTreeCompanyAttachment extends InvenTreeAttachment {
String get REF_MODEL_TYPE => "company";
@override
String get URL => InvenTreeAPI().supportsModernAttachments ? "attachment/" : "company/attachment/";
String get URL => InvenTreeAPI().supportsModernAttachments
? "attachment/"
: "company/attachment/";
@override
InvenTreeModel createFromJson(Map<String, dynamic> json) => InvenTreeCompanyAttachment.fromJson(json);
InvenTreeModel createFromJson(Map<String, dynamic> json) =>
InvenTreeCompanyAttachment.fromJson(json);
}
/*
* The InvenTreeSupplierPart class represents the SupplierPart model in the InvenTree database
*/
class InvenTreeSupplierPart extends InvenTreeModel {
InvenTreeSupplierPart() : super();
InvenTreeSupplierPart.fromJson(Map<String, dynamic> json) : super.fromJson(json);
InvenTreeSupplierPart.fromJson(Map<String, dynamic> json)
: super.fromJson(json);
@override
String get URL => "company/part/";
@@ -180,37 +185,47 @@ class InvenTreeSupplierPart extends InvenTreeModel {
};
}
int get manufacturerId => getInt("pk", subKey: "manufacturer_detail");
String get manufacturerName => getString("name", subKey: "manufacturer_detail");
String get manufacturerName =>
getString("name", subKey: "manufacturer_detail");
String get MPN => getString("MPN", subKey: "manufacturer_part_detail");
String get manufacturerImage => (jsondata["manufacturer_detail"]?["image"] ?? jsondata["manufacturer_detail"]?["thumbnail"] ?? InvenTreeAPI.staticThumb) as String;
String get manufacturerImage =>
(jsondata["manufacturer_detail"]?["image"] ??
jsondata["manufacturer_detail"]?["thumbnail"] ??
InvenTreeAPI.staticThumb)
as String;
int get manufacturerPartId => getInt("manufacturer_part");
int get supplierId => getInt("supplier");
String get supplierName => getString("name", subKey: "supplier_detail");
String get supplierImage => (jsondata["supplier_detail"]?["image"] ?? jsondata["supplier_detail"]?["thumbnail"] ?? InvenTreeAPI.staticThumb) as String;
String get supplierImage =>
(jsondata["supplier_detail"]?["image"] ??
jsondata["supplier_detail"]?["thumbnail"] ??
InvenTreeAPI.staticThumb)
as String;
String get SKU => getString("SKU");
bool get active => getBool("active", backup: true);
int get partId => getInt("part");
String get partImage => (jsondata["part_detail"]?["thumbnail"] ?? InvenTreeAPI.staticThumb) as String;
String get partImage =>
(jsondata["part_detail"]?["thumbnail"] ?? InvenTreeAPI.staticThumb)
as String;
String get partName => getString("name", subKey: "part_detail");
Map<String, dynamic> get partDetail => getMap("part_detail");
String get partDescription => getString("description", subKey: "part_detail");
String get note => getString("note");
String get packaging => getString("packaging");
@@ -224,15 +239,15 @@ class InvenTreeSupplierPart extends InvenTreeModel {
}
@override
InvenTreeModel createFromJson(Map<String, dynamic> json) => InvenTreeSupplierPart.fromJson(json);
InvenTreeModel createFromJson(Map<String, dynamic> json) =>
InvenTreeSupplierPart.fromJson(json);
}
class InvenTreeManufacturerPart extends InvenTreeModel {
InvenTreeManufacturerPart() : super();
InvenTreeManufacturerPart.fromJson(Map<String, dynamic> json) : super.fromJson(json);
InvenTreeManufacturerPart.fromJson(Map<String, dynamic> json)
: super.fromJson(json);
@override
String URL = "company/part/manufacturer/";
@@ -255,10 +270,7 @@ class InvenTreeManufacturerPart extends InvenTreeModel {
@override
Map<String, String> defaultFilters() {
return {
"manufacturer_detail": "true",
"part_detail": "true",
};
return {"manufacturer_detail": "true", "part_detail": "true"};
}
int get partId => getInt("part");
@@ -269,18 +281,27 @@ class InvenTreeManufacturerPart extends InvenTreeModel {
String get partIPN => getString("IPN", subKey: "part_detail");
String get partImage => (jsondata["part_detail"]?["thumbnail"] ?? InvenTreeAPI.staticThumb) as String;
String get partImage =>
(jsondata["part_detail"]?["thumbnail"] ?? InvenTreeAPI.staticThumb)
as String;
int get manufacturerId => getInt("manufacturer");
String get manufacturerName => getString("name", subKey: "manufacturer_detail");
String get manufacturerName =>
getString("name", subKey: "manufacturer_detail");
String get manufacturerDescription => getString("description", subKey: "manufacturer_detail");
String get manufacturerDescription =>
getString("description", subKey: "manufacturer_detail");
String get manufacturerImage => (jsondata["manufacturer_detail"]?["image"] ?? jsondata["manufacturer_detail"]?["thumbnail"] ?? InvenTreeAPI.staticThumb) as String;
String get manufacturerImage =>
(jsondata["manufacturer_detail"]?["image"] ??
jsondata["manufacturer_detail"]?["thumbnail"] ??
InvenTreeAPI.staticThumb)
as String;
String get MPN => getString("MPN");
@override
InvenTreeModel createFromJson(Map<String, dynamic> json) => InvenTreeManufacturerPart.fromJson(json);
InvenTreeModel createFromJson(Map<String, dynamic> json) =>
InvenTreeManufacturerPart.fromJson(json);
}

View File

@@ -17,10 +17,8 @@ import "package:inventree/inventree/sentry.dart";
import "package:inventree/widget/dialogs.dart";
import "package:inventree/widget/fields.dart";
// Paginated response object
class InvenTreePageResponse {
InvenTreePageResponse() {
results = [];
}
@@ -31,7 +29,7 @@ class InvenTreePageResponse {
// Total number of results in the dataset
int count = 0;
int get length => results.length;
List<InvenTreeModel> results = [];
@@ -42,7 +40,6 @@ class InvenTreePageResponse {
* for interacting with InvenTree data.
*/
class InvenTreeModel {
InvenTreeModel();
// Construct an InvenTreeModel from a JSON data object
@@ -87,7 +84,6 @@ class InvenTreeModel {
// If a subKey is specified, we need to dig deeper into the JSON data
if (subKey.isNotEmpty) {
if (!data.containsKey(subKey)) {
debug("JSON data does not contain subKey '$subKey' for key '$key'");
return backup;
@@ -98,7 +94,6 @@ class InvenTreeModel {
if (sub_data is Map<String, dynamic>) {
data = (data[subKey] ?? {}) as Map<String, dynamic>;
}
}
if (data.containsKey(key)) {
@@ -109,7 +104,11 @@ class InvenTreeModel {
}
// Helper function to get sub-map from JSON data
Map<String, dynamic> getMap(String key, {Map<String, dynamic> backup = const {}, String subKey = ""}) {
Map<String, dynamic> getMap(
String key, {
Map<String, dynamic> backup = const {},
String subKey = "",
}) {
dynamic value = getValue(key, backup: backup, subKey: subKey);
if (value == null) {
@@ -152,7 +151,7 @@ class InvenTreeModel {
return double.tryParse(value.toString()) ?? backup;
}
double getDouble(String key, {double backup = 0.0, String subkey = "" }) {
double getDouble(String key, {double backup = 0.0, String subkey = ""}) {
double? value = getDoubleOrNull(key, backup: backup, subKey: subkey);
return value ?? backup;
}
@@ -194,7 +193,6 @@ class InvenTreeModel {
// Return the InvenTree web server URL for this object
String get webUrl {
if (api.isConnected()) {
String web = InvenTreeAPI().baseUrl;
@@ -205,7 +203,6 @@ class InvenTreeModel {
web = web.replaceAll("//", "/");
return web;
} else {
return "";
}
@@ -216,7 +213,9 @@ class InvenTreeModel {
*/
List<String> get rolesRequired {
// Default implementation should not be called
debug("rolesRequired() not implemented for model ${URL} - returning empty list");
debug(
"rolesRequired() not implemented for model ${URL} - returning empty list",
);
return [];
}
@@ -271,12 +270,17 @@ class InvenTreeModel {
// Fields for editing / creating this model
// Override per-model
Map<String, Map<String, dynamic>> formFields() {
return {};
}
Future<void> createForm(BuildContext context, String title, {String fileField = "", Map<String, dynamic> fields=const{}, Map<String, dynamic> data=const {}, Function(dynamic)? onSuccess}) async {
Future<void> createForm(
BuildContext context,
String title, {
String fileField = "",
Map<String, dynamic> fields = const {},
Map<String, dynamic> data = const {},
Function(dynamic)? onSuccess,
}) async {
if (fields.isEmpty) {
fields = formFields();
}
@@ -291,14 +295,17 @@ class InvenTreeModel {
method: "POST",
fileField: fileField,
);
}
/*
* Launch a modal form to edit the fields available to this model instance.
*/
Future<void> editForm(BuildContext context, String title, {Map<String, dynamic> fields=const {}, Function(dynamic)? onSuccess}) async {
Future<void> editForm(
BuildContext context,
String title, {
Map<String, dynamic> fields = const {},
Function(dynamic)? onSuccess,
}) async {
if (fields.isEmpty) {
fields = formFields();
}
@@ -310,9 +317,8 @@ class InvenTreeModel {
fields,
modelData: jsondata,
onSuccess: onSuccess,
method: "PATCH"
method: "PATCH",
);
}
// JSON data which defines this object
@@ -324,12 +330,12 @@ class InvenTreeModel {
int get pk => getInt("pk");
String get pkString => pk.toString();
// Some common accessors
String get name => getString("name");
String get description => getString("description");
String get notes => getString("notes");
int get parentId => getInt("parent");
@@ -387,8 +393,7 @@ class InvenTreeModel {
return "";
}
Future <void> goToInvenTreePage() async {
Future<void> goToInvenTreePage() async {
var uri = Uri.tryParse(webUrl);
if (uri != null && await canLaunchUrl(uri)) {
await launchUrl(uri);
@@ -397,8 +402,7 @@ class InvenTreeModel {
}
}
Future <void> openLink() async {
Future<void> openLink() async {
if (link.isNotEmpty) {
var uri = Uri.tryParse(link);
if (uri != null && await canLaunchUrl(uri)) {
@@ -408,16 +412,21 @@ class InvenTreeModel {
}
String get keywords => getString("keywords");
// Create a new object from JSON data (not a constructor!)
InvenTreeModel createFromJson(Map<String, dynamic> json) => InvenTreeModel.fromJson(json);
InvenTreeModel createFromJson(Map<String, dynamic> json) =>
InvenTreeModel.fromJson(json);
// Return the API detail endpoint for this Model object
String get url => "${URL}/${pk}/".replaceAll("//", "/");
// Search this Model type in the database
Future<List<InvenTreeModel>> search(String searchTerm, {Map<String, String> filters = const {}, int offset = 0, int limit = 25}) async {
Future<List<InvenTreeModel>> search(
String searchTerm, {
Map<String, String> filters = const {},
int offset = 0,
int limit = 25,
}) async {
Map<String, String> searchFilters = {};
for (String key in filters.keys) {
@@ -431,12 +440,13 @@ class InvenTreeModel {
final results = list(filters: searchFilters);
return results;
}
// Return the number of results that would meet a particular "query"
Future<int> count({Map<String, String> filters = const {}, String searchQuery = ""} ) async {
Future<int> count({
Map<String, String> filters = const {},
String searchQuery = "",
}) async {
var params = defaultListFilters();
filters.forEach((String key, String value) {
@@ -458,7 +468,7 @@ class InvenTreeModel {
} else {
return 0;
}
}
}
Map<String, String> defaultFilters() {
return {};
@@ -476,8 +486,11 @@ class InvenTreeModel {
/*
* Report error information to sentry, when a model operation fails.
*/
Future<void> reportModelError(String title, APIResponse response, {Map<String, String> context = const {}}) async {
Future<void> reportModelError(
String title,
APIResponse response, {
Map<String, String> context = const {},
}) async {
String dataString = response.data?.toString() ?? "null";
// If the response has "errorDetail" set, then the error has already been handled, and there is no need to continue
@@ -506,16 +519,12 @@ class InvenTreeModel {
context["dataType"] = response.data?.runtimeType.toString() ?? "null";
context["model"] = URL;
await sentryReportMessage(
title,
context: context,
);
await sentryReportMessage(title, context: context);
}
/// Delete the instance on the remote server
/// Returns true if the operation was successful, else false
Future<bool> delete() async {
// Return if we do not have a valid pk
if (pk < 0) {
return false;
@@ -523,18 +532,15 @@ class InvenTreeModel {
var response = await api.delete(url);
if (!response.isValid() || response.data == null || (response.data is! Map)) {
if (!response.isValid() ||
response.data == null ||
(response.data is! Map)) {
reportModelError(
"InvenTreeModel.delete() returned invalid response",
response,
);
showServerError(
url,
L10().serverError,
L10().errorDelete,
);
showServerError(url, L10().serverError, L10().errorDelete);
return false;
}
@@ -547,52 +553,40 @@ class InvenTreeModel {
* Reload this object, by requesting data from the server
*/
Future<bool> reload() async {
// If we do not have a valid pk (for some reason), exit immediately
if (pk < 0) {
return false;
}
var response = await api.get(url, params: defaultGetFilters(), expectedStatusCode: 200);
var response = await api.get(
url,
params: defaultGetFilters(),
expectedStatusCode: 200,
);
// A valid response has been returned
if (response.isValid() && response.statusCode == 200) {
// Returned data was not a valid JSON object
if (response.data == null || response.data is! Map) {
reportModelError(
"InvenTreeModel.reload() returned invalid response",
response,
context: {
"pk": pk.toString(),
}
"InvenTreeModel.reload() returned invalid response",
response,
context: {"pk": pk.toString()},
);
showServerError(
url,
L10().serverError,
L10().responseInvalid,
);
showServerError(url, L10().serverError, L10().responseInvalid);
return false;
}
} else {
switch (response.statusCode) {
case 404: // Object has been deleted
showSnackIcon(
L10().itemDeleted,
success: false,
);
showSnackIcon(L10().itemDeleted, success: false);
default:
String detail = L10().errorFetch;
detail += "\n${L10().statusCode}: ${response.statusCode}";
showServerError(
url,
L10().serverError,
detail
);
showServerError(url, L10().serverError, detail);
}
return false;
@@ -606,15 +600,15 @@ class InvenTreeModel {
}
// POST data to update the model
Future<APIResponse> update({Map<String, String> values = const {}, int? expectedStatusCode = 200}) async {
Future<APIResponse> update({
Map<String, String> values = const {},
int? expectedStatusCode = 200,
}) async {
var url = path.join(URL, pk.toString());
// Return if we do not have a valid pk
if (pk < 0) {
return APIResponse(
url: url,
);
return APIResponse(url: url);
}
if (!url.endsWith("/")) {
@@ -631,8 +625,10 @@ class InvenTreeModel {
}
// Return the detail view for the associated pk
Future<InvenTreeModel?> getModel(String pk, {Map<String, String> filters = const {}}) async {
Future<InvenTreeModel?> getModel(
String pk, {
Map<String, String> filters = const {},
}) async {
var url = path.join(URL, pk.toString());
if (!url.endsWith("/")) {
@@ -649,27 +645,18 @@ class InvenTreeModel {
var response = await api.get(url, params: params);
if (!response.isValid() || response.data == null || response.data is! Map) {
if (response.statusCode != -1) {
// Report error
reportModelError(
"InvenTreeModel.getModel() returned invalid response",
response,
context: {
"filters": filters.toString(),
"pk": pk,
}
"InvenTreeModel.getModel() returned invalid response",
response,
context: {"filters": filters.toString(), "pk": pk},
);
}
showServerError(
url,
L10().serverError,
L10().errorFetch,
);
showServerError(url, L10().serverError, L10().errorFetch);
return null;
}
lastReload = DateTime.now();
@@ -677,8 +664,10 @@ class InvenTreeModel {
return createFromJson(response.asMap());
}
Future<InvenTreeModel?> get(int pk, {Map<String, String> filters = const {}}) async {
Future<InvenTreeModel?> get(
int pk, {
Map<String, String> filters = const {},
}) async {
if (pk < 0) {
return null;
}
@@ -687,7 +676,6 @@ class InvenTreeModel {
}
Future<InvenTreeModel?> create(Map<String, dynamic> data) async {
if (data.containsKey("pk")) {
data.remove("pk");
}
@@ -700,20 +688,13 @@ class InvenTreeModel {
// Invalid response returned from server
if (!response.isValid() || response.data == null || response.data is! Map) {
reportModelError(
"InvenTreeModel.create() returned invalid response",
response,
context: {
"pk": pk.toString(),
}
"InvenTreeModel.create() returned invalid response",
response,
context: {"pk": pk.toString()},
);
showServerError(
URL,
L10().serverError,
L10().errorCreate,
);
showServerError(URL, L10().serverError, L10().errorCreate);
return null;
}
@@ -721,7 +702,11 @@ class InvenTreeModel {
return createFromJson(response.asMap());
}
Future<InvenTreePageResponse?> listPaginated(int limit, int offset, {Map<String, String> filters = const {}}) async {
Future<InvenTreePageResponse?> listPaginated(
int limit,
int offset, {
Map<String, String> filters = const {},
}) async {
var params = defaultListFilters();
for (String key in filters.keys) {
@@ -737,7 +722,6 @@ class InvenTreeModel {
* - In such a case, we want to concatenate them together
*/
if (params.containsKey("original_search")) {
String search = params["search"] ?? "";
String original = params["original_search"] ?? "";
@@ -759,18 +743,20 @@ class InvenTreeModel {
// First attempt is to look for paginated data, returned as a map
if (dataMap.isNotEmpty && dataMap.containsKey("count") && dataMap.containsKey("results")) {
if (dataMap.isNotEmpty &&
dataMap.containsKey("count") &&
dataMap.containsKey("results")) {
page.count = (dataMap["count"] ?? 0) as int;
page.results = [];
page.results = [];
List<dynamic> results = dataMap["results"] as List<dynamic>;
List<dynamic> results = dataMap["results"] as List<dynamic>;
for (dynamic result in results) {
page.addResult(createFromJson(result as Map<String, dynamic>));
}
for (dynamic result in results) {
page.addResult(createFromJson(result as Map<String, dynamic>));
}
return page;
return page;
}
// Second attempt is to look for a list of data (not paginated)
@@ -782,7 +768,7 @@ class InvenTreeModel {
for (var result in dataList) {
page.addResult(createFromJson(result as Map<String, dynamic>));
}
}
return page;
}
@@ -792,7 +778,9 @@ class InvenTreeModel {
}
// Return list of objects from the database, with optional filters
Future<List<InvenTreeModel>> list({Map<String, String> filters = const {}}) async {
Future<List<InvenTreeModel>> list({
Map<String, String> filters = const {},
}) async {
var params = defaultListFilters();
for (String key in filters.keys) {
@@ -821,7 +809,6 @@ class InvenTreeModel {
}
for (var d in data) {
// Create a new object (of the current class type
InvenTreeModel obj = createFromJson(d as Map<String, dynamic>);
@@ -847,7 +834,6 @@ class InvenTreeModel {
// Each filter must be matched
// Used for (e.g.) filtering returned results
bool filter(String filterString) {
List<String> filters = filterString.trim().toLowerCase().split(" ");
for (var f in filters) {
@@ -860,22 +846,20 @@ class InvenTreeModel {
}
}
/*
* Class representing a single plugin instance
*/
class InvenTreePlugin extends InvenTreeModel {
InvenTreePlugin() : super();
InvenTreePlugin.fromJson(Map<String, dynamic> json) : super.fromJson(json);
@override
InvenTreeModel createFromJson(Map<String, dynamic> json) => InvenTreePlugin.fromJson(json);
InvenTreeModel createFromJson(Map<String, dynamic> json) =>
InvenTreePlugin.fromJson(json);
@override
String get URL {
/* Note: The plugin API endpoint changed at API version 90,
* < 90 = 'plugin'
* >= 90 = 'plugins'
@@ -889,23 +873,24 @@ class InvenTreePlugin extends InvenTreeModel {
}
String get key => getString("key");
bool get active => getBool("active");
// Return the metadata struct for this plugin
Map<String, dynamic> get _meta => (jsondata["meta"] ?? {}) as Map<String, dynamic>;
Map<String, dynamic> get _meta =>
(jsondata["meta"] ?? {}) as Map<String, dynamic>;
String get humanName => (_meta["human_name"] ?? "") as String;
// Return the mixins struct for this plugin
Map<String, dynamic> get _mixins => (jsondata["mixins"] ?? {}) as Map<String, dynamic>;
Map<String, dynamic> get _mixins =>
(jsondata["mixins"] ?? {}) as Map<String, dynamic>;
bool supportsMixin(String mixin) {
return _mixins.containsKey(mixin);
}
}
/*
* Class representing a 'setting' object on the InvenTree server.
* There are two sorts of settings available from the server, via the API:
@@ -913,10 +898,10 @@ class InvenTreePlugin extends InvenTreeModel {
* - UserSetting (applicable only to the current user)
*/
class InvenTreeGlobalSetting extends InvenTreeModel {
InvenTreeGlobalSetting() : super();
InvenTreeGlobalSetting.fromJson(Map<String, dynamic> json) : super.fromJson(json);
InvenTreeGlobalSetting.fromJson(Map<String, dynamic> json)
: super.fromJson(json);
@override
InvenTreeGlobalSetting createFromJson(Map<String, dynamic> json) {
@@ -927,18 +912,17 @@ class InvenTreeGlobalSetting extends InvenTreeModel {
String get URL => "settings/global/";
String get key => getString("key");
String get value => getString("value");
String get type => getString("type");
String get value => getString("value");
String get type => getString("type");
}
class InvenTreeUserSetting extends InvenTreeGlobalSetting {
InvenTreeUserSetting() : super();
InvenTreeUserSetting.fromJson(Map<String, dynamic> json) : super.fromJson(json);
InvenTreeUserSetting.fromJson(Map<String, dynamic> json)
: super.fromJson(json);
@override
InvenTreeGlobalSetting createFromJson(Map<String, dynamic> json) {
@@ -949,22 +933,19 @@ class InvenTreeUserSetting extends InvenTreeGlobalSetting {
String get URL => "settings/user/";
}
class InvenTreeAttachment extends InvenTreeModel {
// Class representing an "attachment" file
InvenTreeAttachment() : super();
InvenTreeAttachment.fromJson(Map<String, dynamic> json) : super.fromJson(json);
InvenTreeAttachment.fromJson(Map<String, dynamic> json)
: super.fromJson(json);
@override
String get URL => "attachment/";
@override
Map<String, Map<String, dynamic>> formFields() {
Map<String, Map<String, dynamic>> fields = {
"link": {},
"comment": {}
};
Map<String, Map<String, dynamic>> fields = {"link": {}, "comment": {}};
if (!hasLink) {
fields.remove("link");
@@ -1004,13 +985,7 @@ class InvenTreeAttachment extends InvenTreeModel {
}
// Image formats
final List<String> img_formats = [
".png",
".jpg",
".gif",
".bmp",
".svg",
];
final List<String> img_formats = [".png", ".jpg", ".gif", ".bmp", ".svg"];
for (String fmt in img_formats) {
if (fn.endsWith(fmt)) {
@@ -1022,7 +997,7 @@ class InvenTreeAttachment extends InvenTreeModel {
}
String get comment => getString("comment");
DateTime? get uploadDate {
if (jsondata.containsKey("upload_date")) {
return DateTime.tryParse((jsondata["upload_date"] ?? "") as String);
@@ -1033,7 +1008,6 @@ class InvenTreeAttachment extends InvenTreeModel {
// Return a count of how many attachments exist against the specified model ID
Future<int> countAttachments(int modelId) {
Map<String, String> filters = {};
if (InvenTreeAPI().supportsModernAttachments) {
@@ -1046,8 +1020,12 @@ class InvenTreeAttachment extends InvenTreeModel {
return count(filters: filters);
}
Future<bool> uploadAttachment(File attachment, int modelId, {String comment = "", Map<String, String> fields = const {}}) async {
Future<bool> uploadAttachment(
File attachment,
int modelId, {
String comment = "",
Map<String, String> fields = const {},
}) async {
// Ensure that the correct reference field is set
Map<String, String> data = Map<String, String>.from(fields);
@@ -1058,15 +1036,14 @@ class InvenTreeAttachment extends InvenTreeModel {
}
if (InvenTreeAPI().supportsModernAttachments) {
url = "attachment/";
data["model_id"] = modelId.toString();
data["model_type"] = REF_MODEL_TYPE;
} else {
if (REFERENCE_FIELD.isEmpty) {
sentryReportMessage("uploadAttachment called with empty 'REFERENCE_FIELD'");
sentryReportMessage(
"uploadAttachment called with empty 'REFERENCE_FIELD'",
);
return false;
}
@@ -1074,24 +1051,21 @@ class InvenTreeAttachment extends InvenTreeModel {
}
final APIResponse response = await InvenTreeAPI().uploadFile(
url,
attachment,
method: "POST",
name: "attachment",
fields: data
url,
attachment,
method: "POST",
name: "attachment",
fields: data,
);
return response.successful();
}
Future<bool> uploadImage(int modelId, {String prefix = "InvenTree"}) async {
bool result = false;
await FilePickerDialog.pickImageFromCamera().then((File? file) {
if (file != null) {
String dir = path.dirname(file.path);
String ext = path.extension(file.path);
String now = DateTime.now().toIso8601String().replaceAll(":", "-");
@@ -1104,8 +1078,9 @@ class InvenTreeAttachment extends InvenTreeModel {
uploadAttachment(renamed, modelId).then((success) {
result = success;
showSnackIcon(
result ? L10().imageUploadSuccess : L10().imageUploadFailure,
success: result);
result ? L10().imageUploadSuccess : L10().imageUploadFailure,
success: result,
);
});
});
} catch (error, stackTrace) {
@@ -1118,14 +1093,10 @@ class InvenTreeAttachment extends InvenTreeModel {
return result;
}
/*
* Download this attachment file
*/
Future<void> downloadAttachment() async {
await InvenTreeAPI().downloadFile(attachment);
}
}

View File

@@ -5,10 +5,10 @@ import "package:inventree/inventree/model.dart";
*/
class InvenTreeNotification extends InvenTreeModel {
InvenTreeNotification() : super();
InvenTreeNotification.fromJson(Map<String, dynamic> json) : super.fromJson(json);
InvenTreeNotification.fromJson(Map<String, dynamic> json)
: super.fromJson(json);
@override
InvenTreeNotification createFromJson(Map<String, dynamic> json) {
@@ -20,15 +20,12 @@ class InvenTreeNotification extends InvenTreeModel {
@override
Map<String, String> defaultListFilters() {
// By default, only return 'unread' notifications
return {
"read": "false",
};
return {"read": "false"};
}
String get message => getString("message");
DateTime? get creationDate {
if (jsondata.containsKey("creation")) {
return DateTime.tryParse((jsondata["creation"] ?? "") as String);
@@ -41,7 +38,6 @@ class InvenTreeNotification extends InvenTreeModel {
* Dismiss this notification (mark as read)
*/
Future<void> dismiss() async {
if (api.apiVersion >= 82) {
// "Modern" API endpoint operates a little differently
await update(values: {"read": "true"});
@@ -49,5 +45,4 @@ class InvenTreeNotification extends InvenTreeModel {
await api.post("${url}read/");
}
}
}
}

View File

@@ -2,16 +2,13 @@
* Base model for various "orders" which share common properties
*/
import "package:inventree/inventree/model.dart";
import "package:inventree/inventree/part.dart";
/*
* Generic class representing an "order"
*/
class InvenTreeOrder extends InvenTreeModel {
InvenTreeOrder() : super();
InvenTreeOrder.fromJson(Map<String, dynamic> json) : super.fromJson(json);
@@ -34,7 +31,8 @@ class InvenTreeOrder extends InvenTreeModel {
int get shipmentCount => getInt("shipments_count", backup: 0);
int get completedShipmentCount => getInt("completed_shipments_count", backup: 0);
int get completedShipmentCount =>
getInt("completed_shipments_count", backup: 0);
bool get complete => completedLineItemCount >= lineItemCount;
@@ -46,14 +44,16 @@ class InvenTreeOrder extends InvenTreeModel {
String get responsibleName => getString("name", subKey: "responsible_detail");
String get responsibleLabel => getString("label", subKey: "responsible_detail");
String get responsibleLabel =>
getString("label", subKey: "responsible_detail");
// Project code information
int get projectCodeId => getInt("project_code");
String get projectCode => getString("code", subKey: "project_code_detail");
String get projectCodeDescription => getString("description", subKey: "project_code_detail");
String get projectCodeDescription =>
getString("description", subKey: "project_code_detail");
bool get hasProjectCode => projectCode.isNotEmpty;
@@ -84,12 +84,10 @@ class InvenTreeOrder extends InvenTreeModel {
}
}
/*
* Generic class representing an "order line"
*/
class InvenTreeOrderLine extends InvenTreeModel {
InvenTreeOrderLine() : super();
InvenTreeOrderLine.fromJson(Map<String, dynamic> json) : super.fromJson(json);
@@ -121,15 +119,14 @@ class InvenTreeOrderLine extends InvenTreeModel {
String get targetDate => getDateString("target_date");
}
/*
* Generic class representing an "ExtraLineItem"
*/
class InvenTreeExtraLineItem extends InvenTreeModel {
InvenTreeExtraLineItem() : super();
InvenTreeExtraLineItem.fromJson(Map<String, dynamic> json) : super.fromJson(json);
InvenTreeExtraLineItem.fromJson(Map<String, dynamic> json)
: super.fromJson(json);
int get orderId => getInt("order");
@@ -157,5 +154,4 @@ class InvenTreeExtraLineItem extends InvenTreeModel {
"notes": {},
};
}
}
}

View File

@@ -14,15 +14,14 @@ import "package:inventree/inventree/model.dart";
import "package:inventree/widget/part/category_display.dart";
import "package:inventree/widget/part/part_detail.dart";
/*
* Class representing the PartCategory database model
*/
class InvenTreePartCategory extends InvenTreeModel {
InvenTreePartCategory() : super();
InvenTreePartCategory.fromJson(Map<String, dynamic> json) : super.fromJson(json);
InvenTreePartCategory.fromJson(Map<String, dynamic> json)
: super.fromJson(json);
@override
String get URL => "part/category/";
@@ -37,16 +36,13 @@ class InvenTreePartCategory extends InvenTreeModel {
Future<Object?> goToDetailPage(BuildContext context) async {
// Default implementation does not do anything...
return Navigator.push(
context,
MaterialPageRoute(
builder: (context) => CategoryDisplayWidget(this)
)
context,
MaterialPageRoute(builder: (context) => CategoryDisplayWidget(this)),
);
}
@override
Map<String, Map<String, dynamic>> formFields() {
Map<String, Map<String, dynamic>> fields = {
"name": {},
"description": {},
@@ -58,9 +54,8 @@ class InvenTreePartCategory extends InvenTreeModel {
}
String get pathstring => getString("pathstring");
String get parentPathString {
String get parentPathString {
List<String> psplit = pathstring.split("/");
if (psplit.isNotEmpty) {
@@ -78,21 +73,22 @@ class InvenTreePartCategory extends InvenTreeModel {
// Return the number of parts in this category
// Note that the API changed from 'parts' to 'part_count' (v69)
int get partcount => (jsondata["part_count"] ?? jsondata["parts"] ?? 0) as int;
int get partcount =>
(jsondata["part_count"] ?? jsondata["parts"] ?? 0) as int;
@override
InvenTreeModel createFromJson(Map<String, dynamic> json) => InvenTreePartCategory.fromJson(json);
InvenTreeModel createFromJson(Map<String, dynamic> json) =>
InvenTreePartCategory.fromJson(json);
}
/*
* Class representing the PartTestTemplate database model
*/
class InvenTreePartTestTemplate extends InvenTreeModel {
InvenTreePartTestTemplate() : super();
InvenTreePartTestTemplate.fromJson(Map<String, dynamic> json) : super.fromJson(json);
InvenTreePartTestTemplate.fromJson(Map<String, dynamic> json)
: super.fromJson(json);
@override
String get URL => "part/test-template/";
@@ -104,16 +100,16 @@ class InvenTreePartTestTemplate extends InvenTreeModel {
String get testName => getString("test_name");
bool get required => getBool("required");
bool get requiresValue => getBool("requires_value");
bool get requiresAttachment => getBool("requires_attachment");
@override
InvenTreeModel createFromJson(Map<String, dynamic> json) => InvenTreePartTestTemplate.fromJson(json);
InvenTreeModel createFromJson(Map<String, dynamic> json) =>
InvenTreePartTestTemplate.fromJson(json);
bool passFailStatus() {
var result = latestResult();
if (result == null) {
@@ -134,17 +130,16 @@ class InvenTreePartTestTemplate extends InvenTreeModel {
return results.last;
}
}
/*
Class representing the PartParameter database model
*/
class InvenTreePartParameter extends InvenTreeModel {
InvenTreePartParameter() : super();
InvenTreePartParameter.fromJson(Map<String, dynamic> json) : super.fromJson(json);
InvenTreePartParameter.fromJson(Map<String, dynamic> json)
: super.fromJson(json);
@override
String get URL => "part/parameter/";
@@ -153,11 +148,11 @@ class InvenTreePartParameter extends InvenTreeModel {
List<String> get rolesRequired => ["part"];
@override
InvenTreeModel createFromJson(Map<String, dynamic> json) => InvenTreePartParameter.fromJson(json);
InvenTreeModel createFromJson(Map<String, dynamic> json) =>
InvenTreePartParameter.fromJson(json);
@override
Map<String, Map<String, dynamic>> formFields() {
Map<String, Map<String, dynamic>> fields = {
"header": {
"type": "string",
@@ -166,9 +161,7 @@ class InvenTreePartParameter extends InvenTreeModel {
"help_text": description,
"value": "",
},
"data": {
"type": "string",
}
"data": {"type": "string"},
};
return fields;
@@ -179,9 +172,9 @@ class InvenTreePartParameter extends InvenTreeModel {
@override
String get description => getString("description", subKey: "template_detail");
String get value => getString("data");
String get valueString {
String v = value;
@@ -196,15 +189,15 @@ class InvenTreePartParameter extends InvenTreeModel {
bool get as_bool => value.toLowerCase() == "true";
String get units => getString("units", subKey: "template_detail");
bool get is_checkbox => getBool("checkbox", subKey: "template_detail", backup: false);
bool get is_checkbox =>
getBool("checkbox", subKey: "template_detail", backup: false);
}
/*
* Class representing the Part database model
*/
class InvenTreePart extends InvenTreeModel {
InvenTreePart() : super();
InvenTreePart.fromJson(Map<String, dynamic> json) : super.fromJson(json);
@@ -222,10 +215,8 @@ class InvenTreePart extends InvenTreeModel {
Future<Object?> goToDetailPage(BuildContext context) async {
// Default implementation does not do anything...
return Navigator.push(
context,
MaterialPageRoute(
builder: (context) => PartDetailWidget(this)
)
context,
MaterialPageRoute(builder: (context) => PartDetailWidget(this)),
);
}
@@ -259,9 +250,7 @@ class InvenTreePart extends InvenTreeModel {
@override
Map<String, String> defaultFilters() {
return {
"category_detail": "true",
};
return {"category_detail": "true"};
}
// Cached list of stock items
@@ -270,27 +259,25 @@ class InvenTreePart extends InvenTreeModel {
int get stockItemCount => stockItems.length;
// Request stock items for this part
Future<void> getStockItems(BuildContext context, {bool showDialog=false}) async {
Future<void> getStockItems(
BuildContext context, {
bool showDialog = false,
}) async {
await InvenTreeStockItem()
.list(filters: {"part": "${pk}", "in_stock": "true"})
.then((var items) {
stockItems.clear();
await InvenTreeStockItem().list(
filters: {
"part": "${pk}",
"in_stock": "true",
},
).then((var items) {
stockItems.clear();
for (var item in items) {
if (item is InvenTreeStockItem) {
stockItems.add(item);
}
}
});
for (var item in items) {
if (item is InvenTreeStockItem) {
stockItems.add(item);
}
}
});
}
// Request pricing data for this part
Future<InvenTreePartPricing?> getPricing() async {
print("REQUEST PRICING FOR: ${pk}");
try {
@@ -311,15 +298,13 @@ class InvenTreePart extends InvenTreeModel {
}
int get supplierCount => getInt("suppliers", backup: 0);
// Request supplier parts for this part
Future<List<InvenTreeSupplierPart>> getSupplierParts() async {
List<InvenTreeSupplierPart> _supplierParts = [];
final parts = await InvenTreeSupplierPart().list(
filters: {
"part": "${pk}",
}
filters: {"part": "${pk}"},
);
for (var result in parts) {
@@ -338,13 +323,9 @@ class InvenTreePart extends InvenTreeModel {
// Request test templates from the serve
Future<void> getTestTemplates() async {
InvenTreePartTestTemplate().list(
filters: {
"part": "${pk}",
},
).then((var templates) {
InvenTreePartTestTemplate().list(filters: {"part": "${pk}"}).then((
var templates,
) {
testingTemplates.clear();
for (var t in templates) {
@@ -373,12 +354,12 @@ class InvenTreePart extends InvenTreeModel {
// Get the 'available stock' for this Part
double get unallocatedStock {
double unallocated = 0;
// Note that the 'available_stock' was not added until API v35
if (jsondata.containsKey("unallocated_stock")) {
unallocated = double.tryParse(jsondata["unallocated_stock"].toString()) ?? 0;
unallocated =
double.tryParse(jsondata["unallocated_stock"].toString()) ?? 0;
} else {
unallocated = inStock;
}
@@ -386,148 +367,150 @@ class InvenTreePart extends InvenTreeModel {
return max(0, unallocated);
}
String get unallocatedStockString => simpleNumberString(unallocatedStock);
String get unallocatedStockString => simpleNumberString(unallocatedStock);
String stockString({bool includeUnits = true}) {
String q = unallocatedStockString;
String stockString({bool includeUnits = true}) {
String q = unallocatedStockString;
if (unallocatedStock != inStock) {
q += " / ${inStockString}";
}
if (includeUnits && units.isNotEmpty) {
q += " ${units}";
}
return q;
if (unallocatedStock != inStock) {
q += " / ${inStockString}";
}
String get units => getString("units");
// Get the ID of the Part that this part is a variant of (or null)
int? get variantOf => jsondata["variant_of"] as int?;
// Get the number of units being build for this Part
double get building => getDouble("building");
// Get the number of BOMs this Part is used in (if it is a component)
int get usedInCount => jsondata.containsKey("used_in") ? getInt("used_in", backup: 0) : 0;
bool get isAssembly => getBool("assembly");
bool get isComponent => getBool("component");
bool get isPurchaseable => getBool("purchaseable");
bool get isSalable => getBool("salable");
bool get isActive => getBool("active");
bool get isVirtual => getBool("virtual");
bool get isTemplate => getBool("is_template");
bool get isTrackable => getBool("trackable");
// Get the IPN (internal part number) for the Part instance
String get IPN => getString("IPN");
// Get the revision string for the Part instance
String get revision => getString("revision");
// Get the category ID for the Part instance (or "null" if does not exist)
int get categoryId => getInt("category");
// Get the category name for the Part instance
String get categoryName {
// Inavlid category ID
if (categoryId <= 0) return "";
if (!jsondata.containsKey("category_detail")) return "";
return (jsondata["category_detail"]?["name"] ?? "") as String;
if (includeUnits && units.isNotEmpty) {
q += " ${units}";
}
// Get the category description for the Part instance
String get categoryDescription {
// Invalid category ID
if (categoryId <= 0) return "";
return q;
}
if (!jsondata.containsKey("category_detail")) return "";
String get units => getString("units");
return (jsondata["category_detail"]?["description"] ?? "") as String;
}
// Get the image URL for the Part instance
String get _image => getString("image");
// Get the ID of the Part that this part is a variant of (or null)
int? get variantOf => jsondata["variant_of"] as int?;
// Get the thumbnail URL for the Part instance
String get _thumbnail => getString("thumbnail");
// Get the number of units being build for this Part
double get building => getDouble("building");
// Return the fully-qualified name for the Part instance
String get fullname {
// Get the number of BOMs this Part is used in (if it is a component)
int get usedInCount =>
jsondata.containsKey("used_in") ? getInt("used_in", backup: 0) : 0;
String fn = getString("full_name");
bool get isAssembly => getBool("assembly");
if (fn.isNotEmpty) return fn;
bool get isComponent => getBool("component");
List<String> elements = [];
bool get isPurchaseable => getBool("purchaseable");
if (IPN.isNotEmpty) elements.add(IPN);
bool get isSalable => getBool("salable");
elements.add(name);
bool get isActive => getBool("active");
if (revision.isNotEmpty) elements.add(revision);
bool get isVirtual => getBool("virtual");
return elements.join(" | ");
}
bool get isTemplate => getBool("is_template");
// Return a path to the image for this Part
String get image {
// Use thumbnail as a backup
String img = _image.isNotEmpty ? _image : _thumbnail;
bool get isTrackable => getBool("trackable");
return img.isNotEmpty ? img : InvenTreeAPI.staticImage;
}
// Get the IPN (internal part number) for the Part instance
String get IPN => getString("IPN");
// Return a path to the thumbnail for this part
String get thumbnail {
// Use image as a backup
String img = _thumbnail.isNotEmpty ? _thumbnail : _image;
// Get the revision string for the Part instance
String get revision => getString("revision");
return img.isNotEmpty ? img : InvenTreeAPI.staticThumb;
}
// Get the category ID for the Part instance (or "null" if does not exist)
int get categoryId => getInt("category");
Future<bool> uploadImage(File image) async {
// Upload file against this part
final APIResponse response = await InvenTreeAPI().uploadFile(
url,
image,
method: "PATCH",
name: "image",
);
// Get the category name for the Part instance
String get categoryName {
// Inavlid category ID
if (categoryId <= 0) return "";
return response.successful();
}
if (!jsondata.containsKey("category_detail")) return "";
// Return the "starred" status of this part
bool get starred => getBool("starred");
return (jsondata["category_detail"]?["name"] ?? "") as String;
}
// Get the category description for the Part instance
String get categoryDescription {
// Invalid category ID
if (categoryId <= 0) return "";
if (!jsondata.containsKey("category_detail")) return "";
return (jsondata["category_detail"]?["description"] ?? "") as String;
}
// Get the image URL for the Part instance
String get _image => getString("image");
// Get the thumbnail URL for the Part instance
String get _thumbnail => getString("thumbnail");
// Return the fully-qualified name for the Part instance
String get fullname {
String fn = getString("full_name");
if (fn.isNotEmpty) return fn;
List<String> elements = [];
if (IPN.isNotEmpty) elements.add(IPN);
elements.add(name);
if (revision.isNotEmpty) elements.add(revision);
return elements.join(" | ");
}
// Return a path to the image for this Part
String get image {
// Use thumbnail as a backup
String img = _image.isNotEmpty ? _image : _thumbnail;
return img.isNotEmpty ? img : InvenTreeAPI.staticImage;
}
// Return a path to the thumbnail for this part
String get thumbnail {
// Use image as a backup
String img = _thumbnail.isNotEmpty ? _thumbnail : _image;
return img.isNotEmpty ? img : InvenTreeAPI.staticThumb;
}
Future<bool> uploadImage(File image) async {
// Upload file against this part
final APIResponse response = await InvenTreeAPI().uploadFile(
url,
image,
method: "PATCH",
name: "image",
);
return response.successful();
}
// Return the "starred" status of this part
bool get starred => getBool("starred");
@override
InvenTreeModel createFromJson(Map<String, dynamic> json) => InvenTreePart.fromJson(json);
InvenTreeModel createFromJson(Map<String, dynamic> json) =>
InvenTreePart.fromJson(json);
}
class InvenTreePartPricing extends InvenTreeModel {
InvenTreePartPricing() : super();
InvenTreePartPricing.fromJson(Map<String, dynamic> json) : super.fromJson(json);
InvenTreePartPricing.fromJson(Map<String, dynamic> json)
: super.fromJson(json);
@override
List<String> get rolesRequired => ["part"];
@override
InvenTreeModel createFromJson(Map<String, dynamic> json) => InvenTreePartPricing.fromJson(json);
InvenTreeModel createFromJson(Map<String, dynamic> json) =>
InvenTreePartPricing.fromJson(json);
// Price data accessors
String get currency => getString("currency", backup: "USD");
@@ -538,8 +521,10 @@ class InvenTreePartPricing extends InvenTreeModel {
double? get overrideMin => getDoubleOrNull("override_min");
double? get overrideMax => getDoubleOrNull("override_max");
String get overrideMinCurrency => getString("override_min_currency", backup: currency);
String get overrideMaxCurrency => getString("override_max_currency", backup: currency);
String get overrideMinCurrency =>
getString("override_min_currency", backup: currency);
String get overrideMaxCurrency =>
getString("override_max_currency", backup: currency);
double? get bomCostMin => getDoubleOrNull("bom_cost_min");
double? get bomCostMax => getDoubleOrNull("bom_cost_max");
@@ -563,15 +548,14 @@ class InvenTreePartPricing extends InvenTreeModel {
double? get saleHistoryMax => getDoubleOrNull("sale_history_max");
}
/*
* Class representing an attachment file against a Part object
*/
class InvenTreePartAttachment extends InvenTreeAttachment {
InvenTreePartAttachment() : super();
InvenTreePartAttachment.fromJson(Map<String, dynamic> json) : super.fromJson(json);
InvenTreePartAttachment.fromJson(Map<String, dynamic> json)
: super.fromJson(json);
@override
String get REFERENCE_FIELD => "part";
@@ -580,9 +564,11 @@ class InvenTreePartAttachment extends InvenTreeAttachment {
String get REF_MODEL_TYPE => "part";
@override
String get URL => InvenTreeAPI().supportsModernAttachments ? "attachment/" : "part/attachment/";
String get URL => InvenTreeAPI().supportsModernAttachments
? "attachment/"
: "part/attachment/";
@override
InvenTreeModel createFromJson(Map<String, dynamic> json) => InvenTreePartAttachment.fromJson(json);
InvenTreeModel createFromJson(Map<String, dynamic> json) =>
InvenTreePartAttachment.fromJson(json);
}

View File

@@ -1,17 +1,17 @@
import "package:inventree/inventree/model.dart";
/*
* Class representing the ProjectCode database model
*/
class InvenTreeProjectCode extends InvenTreeModel {
InvenTreeProjectCode() : super();
InvenTreeProjectCode.fromJson(Map<String, dynamic> json) : super.fromJson(json);
InvenTreeProjectCode.fromJson(Map<String, dynamic> json)
: super.fromJson(json);
@override
InvenTreeModel createFromJson(Map<String, dynamic> json) => InvenTreeProjectCode.fromJson(json);
InvenTreeModel createFromJson(Map<String, dynamic> json) =>
InvenTreeProjectCode.fromJson(json);
@override
String get URL => "project-code/";
@@ -20,10 +20,7 @@ class InvenTreeProjectCode extends InvenTreeModel {
@override
Map<String, Map<String, dynamic>> formFields() {
return {
"code": {},
"description": {},
};
return {"code": {}, "description": {}};
}
String get code => getString("code");

View File

@@ -12,18 +12,18 @@ import "package:inventree/widget/progress.dart";
import "package:inventree/api_form.dart";
import "package:inventree/l10.dart";
/*
* Class representing an individual PurchaseOrder instance
*/
class InvenTreePurchaseOrder extends InvenTreeOrder {
InvenTreePurchaseOrder() : super();
InvenTreePurchaseOrder.fromJson(Map<String, dynamic> json) : super.fromJson(json);
InvenTreePurchaseOrder.fromJson(Map<String, dynamic> json)
: super.fromJson(json);
@override
InvenTreeModel createFromJson(Map<String, dynamic> json) => InvenTreePurchaseOrder.fromJson(json);
InvenTreeModel createFromJson(Map<String, dynamic> json) =>
InvenTreePurchaseOrder.fromJson(json);
@override
String get URL => "order/po/";
@@ -31,10 +31,8 @@ class InvenTreePurchaseOrder extends InvenTreeOrder {
@override
Future<Object?> goToDetailPage(BuildContext context) async {
return Navigator.push(
context,
MaterialPageRoute(
builder: (context) => PurchaseOrderDetailWidget(this)
)
context,
MaterialPageRoute(builder: (context) => PurchaseOrderDetailWidget(this)),
);
}
@@ -50,9 +48,7 @@ class InvenTreePurchaseOrder extends InvenTreeOrder {
Map<String, Map<String, dynamic>> fields = {
"reference": {},
"supplier": {
"filters": {
"is_supplier": true,
},
"filters": {"is_supplier": true},
},
"supplier_reference": {},
"description": {},
@@ -63,9 +59,7 @@ class InvenTreePurchaseOrder extends InvenTreeOrder {
"link": {},
"responsible": {},
"contact": {
"filters": {
"company": supplierId,
}
"filters": {"company": supplierId},
},
};
@@ -82,20 +76,16 @@ class InvenTreePurchaseOrder extends InvenTreeOrder {
}
return fields;
}
@override
Map<String, String> defaultFilters() {
return {
"supplier_detail": "true",
};
return {"supplier_detail": "true"};
}
int get supplierId => getInt("supplier");
InvenTreeCompany? get supplier {
dynamic supplier_detail = jsondata["supplier_detail"];
if (supplier_detail == null) {
@@ -109,20 +99,26 @@ class InvenTreePurchaseOrder extends InvenTreeOrder {
int get destinationId => getInt("destination");
bool get isOpen => api.PurchaseOrderStatus.isNameIn(status, ["PENDING", "PLACED", "ON_HOLD"]);
bool get isOpen => api.PurchaseOrderStatus.isNameIn(status, [
"PENDING",
"PLACED",
"ON_HOLD",
]);
bool get isPending => api.PurchaseOrderStatus.isNameIn(status, ["PENDING", "ON_HOLD"]);
bool get isPending =>
api.PurchaseOrderStatus.isNameIn(status, ["PENDING", "ON_HOLD"]);
bool get isPlaced => api.PurchaseOrderStatus.isNameIn(status, ["PLACED"]);
bool get isFailed => api.PurchaseOrderStatus.isNameIn(status, ["CANCELLED", "LOST", "RETURNED"]);
bool get isFailed => api.PurchaseOrderStatus.isNameIn(status, [
"CANCELLED",
"LOST",
"RETURNED",
]);
Future<List<InvenTreePOLineItem>> getLineItems() async {
final results = await InvenTreePOLineItem().list(
filters: {
"order": "${pk}",
}
filters: {"order": "${pk}"},
);
List<InvenTreePOLineItem> items = [];
@@ -161,13 +157,14 @@ class InvenTreePurchaseOrder extends InvenTreeOrder {
}
class InvenTreePOLineItem extends InvenTreeOrderLine {
InvenTreePOLineItem() : super();
InvenTreePOLineItem.fromJson(Map<String, dynamic> json) : super.fromJson(json);
InvenTreePOLineItem.fromJson(Map<String, dynamic> json)
: super.fromJson(json);
@override
InvenTreeModel createFromJson(Map<String, dynamic> json) => InvenTreePOLineItem.fromJson(json);
InvenTreeModel createFromJson(Map<String, dynamic> json) =>
InvenTreePOLineItem.fromJson(json);
@override
String get URL => "order/po-line/";
@@ -198,10 +195,7 @@ class InvenTreePOLineItem extends InvenTreeOrderLine {
@override
Map<String, String> defaultFilters() {
return {
"part_detail": "true",
"order_detail": "true",
};
return {"part_detail": "true", "order_detail": "true"};
}
double get received => getDouble("received");
@@ -216,14 +210,14 @@ class InvenTreePOLineItem extends InvenTreeOrderLine {
return received / quantity;
}
String get progressString => simpleNumberString(received) + " / " + simpleNumberString(quantity);
String get progressString =>
simpleNumberString(received) + " / " + simpleNumberString(quantity);
double get outstanding => quantity - received;
int get supplierPartId => getInt("part");
InvenTreeSupplierPart? get supplierPart {
dynamic detail = jsondata["supplier_part_detail"];
if (detail == null) {
@@ -246,7 +240,7 @@ class InvenTreePOLineItem extends InvenTreeOrderLine {
String get SKU => getString("SKU", subKey: "supplier_part_detail");
double get purchasePrice => getDouble("purchase_price");
String get purchasePriceCurrency => getString("purchase_price_currency");
int get destinationId => getInt("destination");
@@ -256,7 +250,13 @@ class InvenTreePOLineItem extends InvenTreeOrderLine {
Map<String, dynamic> get destinationDetail => getMap("destination_detail");
// Receive this line item into stock
Future<void> receive(BuildContext context, {int? destination, double? quantity, String? barcode, Function? onSuccess}) async {
Future<void> receive(
BuildContext context, {
int? destination,
double? quantity,
String? barcode,
Function? onSuccess,
}) async {
// Infer the destination location from the line item if not provided
if (destinationId > 0) {
destination = destinationId;
@@ -274,20 +274,10 @@ class InvenTreePOLineItem extends InvenTreeOrderLine {
"hidden": true,
"value": pk,
},
"quantity": {
"parent": "items",
"nested": true,
"value": quantity,
},
"quantity": {"parent": "items", "nested": true, "value": quantity},
"location": {},
"status": {
"parent": "items",
"nested": true,
},
"batch_code": {
"parent": "items",
"nested": true,
},
"status": {"parent": "items", "nested": true},
"batch_code": {"parent": "items", "nested": true},
"barcode": {
"parent": "items",
"nested": true,
@@ -295,7 +285,7 @@ class InvenTreePOLineItem extends InvenTreeOrderLine {
"label": L10().barcodeAssign,
"value": barcode,
"required": false,
}
},
};
if (destination != null && destination > 0) {
@@ -306,31 +296,31 @@ class InvenTreePOLineItem extends InvenTreeOrderLine {
if (order != null) {
await launchApiForm(
context,
L10().receiveItem,
order.receive_url,
fields,
method: "POST",
icon: TablerIcons.transition_right,
onSuccess: (data) {
if (onSuccess != null) {
onSuccess();
}
context,
L10().receiveItem,
order.receive_url,
fields,
method: "POST",
icon: TablerIcons.transition_right,
onSuccess: (data) {
if (onSuccess != null) {
onSuccess();
}
},
);
}
}
}
class InvenTreePOExtraLineItem extends InvenTreeExtraLineItem {
InvenTreePOExtraLineItem() : super();
InvenTreePOExtraLineItem.fromJson(Map<String, dynamic> json) : super.fromJson(json);
InvenTreePOExtraLineItem.fromJson(Map<String, dynamic> json)
: super.fromJson(json);
@override
InvenTreeModel createFromJson(Map<String, dynamic> json) => InvenTreePOExtraLineItem.fromJson(json);
InvenTreeModel createFromJson(Map<String, dynamic> json) =>
InvenTreePOExtraLineItem.fromJson(json);
@override
String get URL => "order/po-extra-line/";
@@ -342,23 +332,19 @@ class InvenTreePOExtraLineItem extends InvenTreeExtraLineItem {
Future<Object?> goToDetailPage(BuildContext context) async {
return Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ExtraLineDetailWidget(this)
)
MaterialPageRoute(builder: (context) => ExtraLineDetailWidget(this)),
);
}
}
/*
* Class representing an attachment file against a PurchaseOrder object
*/
class InvenTreePurchaseOrderAttachment extends InvenTreeAttachment {
InvenTreePurchaseOrderAttachment() : super();
InvenTreePurchaseOrderAttachment.fromJson(Map<String, dynamic> json) : super.fromJson(json);
InvenTreePurchaseOrderAttachment.fromJson(Map<String, dynamic> json)
: super.fromJson(json);
@override
String get REFERENCE_FIELD => "order";
@@ -367,9 +353,11 @@ class InvenTreePurchaseOrderAttachment extends InvenTreeAttachment {
String get REF_MODEL_TYPE => "purchaseorder";
@override
String get URL => InvenTreeAPI().supportsModernAttachments ? "attachment/" : "order/po/attachment/";
String get URL => InvenTreeAPI().supportsModernAttachments
? "attachment/"
: "order/po/attachment/";
@override
InvenTreeModel createFromJson(Map<String, dynamic> json) => InvenTreePurchaseOrderAttachment.fromJson(json);
InvenTreeModel createFromJson(Map<String, dynamic> json) =>
InvenTreePurchaseOrderAttachment.fromJson(json);
}

View File

@@ -1,5 +1,3 @@
import "package:flutter/material.dart";
import "package:inventree/api.dart";
import "package:inventree/helpers.dart";
@@ -11,18 +9,18 @@ import "package:inventree/widget/progress.dart";
import "package:inventree/widget/order/extra_line_detail.dart";
import "package:inventree/widget/order/sales_order_detail.dart";
/*
* Class representing an individual SalesOrder
*/
class InvenTreeSalesOrder extends InvenTreeOrder {
InvenTreeSalesOrder() : super();
InvenTreeSalesOrder.fromJson(Map<String, dynamic> json) : super.fromJson(json);
InvenTreeSalesOrder.fromJson(Map<String, dynamic> json)
: super.fromJson(json);
@override
InvenTreeModel createFromJson(Map<String, dynamic> json) => InvenTreeSalesOrder.fromJson(json);
InvenTreeModel createFromJson(Map<String, dynamic> json) =>
InvenTreeSalesOrder.fromJson(json);
@override
String get URL => "order/so/";
@@ -38,9 +36,7 @@ class InvenTreeSalesOrder extends InvenTreeOrder {
Future<Object?> goToDetailPage(BuildContext context) async {
return Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SalesOrderDetailWidget(this)
)
MaterialPageRoute(builder: (context) => SalesOrderDetailWidget(this)),
);
}
@@ -49,9 +45,7 @@ class InvenTreeSalesOrder extends InvenTreeOrder {
Map<String, Map<String, dynamic>> fields = {
"reference": {},
"customer": {
"filters": {
"is_customer": true,
}
"filters": {"is_customer": true},
},
"customer_reference": {},
"description": {},
@@ -61,10 +55,8 @@ class InvenTreeSalesOrder extends InvenTreeOrder {
"link": {},
"responsible": {},
"contact": {
"filters": {
"company": customerId,
}
}
"filters": {"company": customerId},
},
};
if (!InvenTreeAPI().supportsProjectCodes) {
@@ -84,9 +76,7 @@ class InvenTreeSalesOrder extends InvenTreeOrder {
@override
Map<String, String> defaultFilters() {
return {
"customer_detail": "true",
};
return {"customer_detail": "true"};
}
Future<void> issueOrder() async {
@@ -124,28 +114,33 @@ class InvenTreeSalesOrder extends InvenTreeOrder {
String get customerReference => getString("customer_reference");
bool get isOpen => api.SalesOrderStatus.isNameIn(status, ["PENDING", "IN_PROGRESS", "ON_HOLD"]);
bool get isOpen => api.SalesOrderStatus.isNameIn(status, [
"PENDING",
"IN_PROGRESS",
"ON_HOLD",
]);
bool get isPending => api.SalesOrderStatus.isNameIn(status, ["PENDING", "ON_HOLD"]);
bool get isPending =>
api.SalesOrderStatus.isNameIn(status, ["PENDING", "ON_HOLD"]);
bool get isInProgress => api.SalesOrderStatus.isNameIn(status, ["IN_PROGRESS"]);
bool get isInProgress =>
api.SalesOrderStatus.isNameIn(status, ["IN_PROGRESS"]);
bool get isComplete => api.SalesOrderStatus.isNameIn(status, ["SHIPPED"]);
}
/*
* Class representing an individual line item in a SalesOrder
*/
class InvenTreeSOLineItem extends InvenTreeOrderLine {
InvenTreeSOLineItem() : super();
InvenTreeSOLineItem.fromJson(Map<String, dynamic> json) : super.fromJson(json);
InvenTreeSOLineItem.fromJson(Map<String, dynamic> json)
: super.fromJson(json);
@override
InvenTreeModel createFromJson(Map<String, dynamic> json) => InvenTreeSOLineItem.fromJson(json);
InvenTreeModel createFromJson(Map<String, dynamic> json) =>
InvenTreeSOLineItem.fromJson(json);
@override
String get URL => "order/so-line/";
@@ -156,13 +151,9 @@ class InvenTreeSOLineItem extends InvenTreeOrderLine {
@override
Map<String, Map<String, dynamic>> formFields() {
return {
"order": {
"hidden": true,
},
"order": {"hidden": true},
"part": {
"filters": {
"salable": true,
}
"filters": {"salable": true},
},
"quantity": {},
"reference": {},
@@ -172,33 +163,17 @@ class InvenTreeSOLineItem extends InvenTreeOrderLine {
}
Map<String, Map<String, dynamic>> allocateFormFields() {
return {
"line_item": {
"parent": "items",
"nested": true,
"hidden": true,
},
"stock_item": {
"parent": "items",
"nested": true,
"filters": {},
},
"quantity": {
"parent": "items",
"nested": true,
},
"shipment": {
"filters": {}
}
"line_item": {"parent": "items", "nested": true, "hidden": true},
"stock_item": {"parent": "items", "nested": true, "filters": {}},
"quantity": {"parent": "items", "nested": true},
"shipment": {"filters": {}},
};
}
@override
Map<String, String> defaultFilters() {
return {
"part_detail": "true",
};
return {"part_detail": "true"};
}
double get allocated => getDouble("allocated");
@@ -223,7 +198,8 @@ class InvenTreeSOLineItem extends InvenTreeOrderLine {
return unallocated;
}
String get allocatedString => simpleNumberString(allocated) + " / " + simpleNumberString(quantity);
String get allocatedString =>
simpleNumberString(allocated) + " / " + simpleNumberString(quantity);
double get shipped => getDouble("shipped");
@@ -239,26 +215,28 @@ class InvenTreeSOLineItem extends InvenTreeOrderLine {
return shipped / quantity;
}
String get progressString => simpleNumberString(shipped) + " / " + simpleNumberString(quantity);
String get progressString =>
simpleNumberString(shipped) + " / " + simpleNumberString(quantity);
bool get isComplete => shipped >= quantity;
double get available => getDouble("available_stock") + getDouble("available_variant_stock");
double get available =>
getDouble("available_stock") + getDouble("available_variant_stock");
double get salePrice => getDouble("sale_price");
String get salePriceCurrency => getString("sale_price_currency");
}
class InvenTreeSOExtraLineItem extends InvenTreeExtraLineItem {
InvenTreeSOExtraLineItem() : super();
InvenTreeSOExtraLineItem.fromJson(Map<String, dynamic> json) : super.fromJson(json);
InvenTreeSOExtraLineItem.fromJson(Map<String, dynamic> json)
: super.fromJson(json);
@override
InvenTreeModel createFromJson(Map<String, dynamic> json) => InvenTreeSOExtraLineItem.fromJson(json);
InvenTreeModel createFromJson(Map<String, dynamic> json) =>
InvenTreeSOExtraLineItem.fromJson(json);
@override
String get URL => "order/so-extra-line/";
@@ -269,10 +247,8 @@ class InvenTreeSOExtraLineItem extends InvenTreeExtraLineItem {
@override
Future<Object?> goToDetailPage(BuildContext context) async {
return Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ExtraLineDetailWidget(this)
)
context,
MaterialPageRoute(builder: (context) => ExtraLineDetailWidget(this)),
);
}
}
@@ -281,13 +257,14 @@ class InvenTreeSOExtraLineItem extends InvenTreeExtraLineItem {
* Class representing a sales order shipment
*/
class InvenTreeSalesOrderShipment extends InvenTreeModel {
InvenTreeSalesOrderShipment() : super();
InvenTreeSalesOrderShipment.fromJson(Map<String, dynamic> json) : super.fromJson(json);
InvenTreeSalesOrderShipment.fromJson(Map<String, dynamic> json)
: super.fromJson(json);
@override
InvenTreeModel createFromJson(Map<String, dynamic> json) => InvenTreeSalesOrderShipment.fromJson(json);
InvenTreeModel createFromJson(Map<String, dynamic> json) =>
InvenTreeSalesOrderShipment.fromJson(json);
@override
String get URL => "/order/so/shipment/";
@@ -318,19 +295,18 @@ class InvenTreeSalesOrderShipment extends InvenTreeModel {
bool get shipped => shipment_date != null && shipment_date!.isNotEmpty;
}
/*
* Class representing an attachment file against a SalesOrder object
*/
class InvenTreeSalesOrderAttachment extends InvenTreeAttachment {
InvenTreeSalesOrderAttachment() : super();
InvenTreeSalesOrderAttachment.fromJson(Map<String, dynamic> json) : super.fromJson(json);
InvenTreeSalesOrderAttachment.fromJson(Map<String, dynamic> json)
: super.fromJson(json);
@override
InvenTreeModel createFromJson(Map<String, dynamic> json) => InvenTreeSalesOrderAttachment.fromJson(json);
InvenTreeModel createFromJson(Map<String, dynamic> json) =>
InvenTreeSalesOrderAttachment.fromJson(json);
@override
String get REFERENCE_FIELD => "order";
@@ -339,6 +315,7 @@ class InvenTreeSalesOrderAttachment extends InvenTreeAttachment {
String get REF_MODEL_TYPE => "salesorder";
@override
String get URL => InvenTreeAPI().supportsModernAttachments ? "attachment/" : "order/so/attachment/";
String get URL => InvenTreeAPI().supportsModernAttachments
? "attachment/"
: "order/so/attachment/";
}

View File

@@ -11,7 +11,6 @@ import "package:inventree/dsn.dart";
import "package:inventree/preferences.dart";
Future<Map<String, dynamic>> getDeviceInfo() async {
// Extract device information
final DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
@@ -31,7 +30,6 @@ Future<Map<String, dynamic>> getDeviceInfo() async {
"identifierForVendor": iosDeviceInfo.identifierForVendor,
"isPhysicalDevice": iosDeviceInfo.isPhysicalDevice,
};
} else if (Platform.isAndroid) {
final androidDeviceInfo = await deviceInfo.androidInfo;
@@ -57,13 +55,11 @@ Future<Map<String, dynamic>> getDeviceInfo() async {
return device_info;
}
Map<String, dynamic> getServerInfo() => {
"version": InvenTreeAPI().serverVersion,
"apiVersion": InvenTreeAPI().apiVersion,
};
Future<Map<String, dynamic>> getAppInfo() async {
// Add app info
final package_info = await PackageInfo.fromPlatform();
@@ -76,7 +72,6 @@ Future<Map<String, dynamic>> getAppInfo() async {
};
}
bool isInDebugMode() {
bool inDebugMode = false;
@@ -85,8 +80,10 @@ bool isInDebugMode() {
return inDebugMode;
}
Future<bool> sentryReportMessage(String message, {Map<String, String>? context}) async {
Future<bool> sentryReportMessage(
String message, {
Map<String, String>? context,
}) async {
if (SENTRY_DSN_KEY.isEmpty) {
return false;
}
@@ -106,23 +103,22 @@ Future<bool> sentryReportMessage(String message, {Map<String, String>? context})
// We don't care about the server address, only the path and query parameters!
// Overwrite the provided URL
context["url"] = uri.path + "?" + uri.query;
} catch (error) {
// Ignore if any errors are thrown here
}
}
}
print("Sending user message to Sentry: ${message}, ${context}");
if (isInDebugMode()) {
print("----- In dev mode. Not sending message to Sentry.io -----");
return true;
}
final upload = await InvenTreeSettingsManager().getValue(INV_REPORT_ERRORS, true) as bool;
final upload =
await InvenTreeSettingsManager().getValue(INV_REPORT_ERRORS, true)
as bool;
if (!upload) {
print("----- Error reporting disabled -----");
@@ -152,12 +148,15 @@ Future<bool> sentryReportMessage(String message, {Map<String, String>? context})
}
}
/*
* Report an error message to sentry.io
*/
Future<void> sentryReportError(String source, dynamic error, StackTrace? stackTrace, {Map<String, String> context = const {}}) async {
Future<void> sentryReportError(
String source,
dynamic error,
StackTrace? stackTrace, {
Map<String, String> context = const {},
}) async {
if (sentryIgnoreError(error)) {
// No action on this error
return;
@@ -170,7 +169,6 @@ Future<void> sentryReportError(String source, dynamic error, StackTrace? stackTr
// check if you are running in dev mode using an assertion and omit sending
// the report.
if (isInDebugMode()) {
print("----- In dev mode. Not sending report to Sentry.io -----");
return;
}
@@ -179,7 +177,9 @@ Future<void> sentryReportError(String source, dynamic error, StackTrace? stackTr
return;
}
final upload = await InvenTreeSettingsManager().getValue(INV_REPORT_ERRORS, true) as bool;
final upload =
await InvenTreeSettingsManager().getValue(INV_REPORT_ERRORS, true)
as bool;
if (!upload) {
print("----- Error reporting disabled -----");
@@ -188,11 +188,12 @@ Future<void> sentryReportError(String source, dynamic error, StackTrace? stackTr
// Some errors are outside our control, and we do not want to "pollute" the uploaded data
if (source == "FlutterError.onError") {
String errorString = error.toString();
// Missing media file
if (errorString.contains("HttpException") && errorString.contains("404") && errorString.contains("/media/")) {
if (errorString.contains("HttpException") &&
errorString.contains("404") &&
errorString.contains("/media/")) {
return;
}
@@ -225,26 +226,28 @@ Future<void> sentryReportError(String source, dynamic error, StackTrace? stackTr
scope.setContexts("context", context);
});
Sentry.captureException(error, stackTrace: stackTrace).catchError((error) {
print("Error uploading information to Sentry.io:");
print(error);
return SentryId.empty();
}).then((response) {
print("Uploaded information to Sentry.io : ${response.toString()}");
});
Sentry.captureException(error, stackTrace: stackTrace)
.catchError((error) {
print("Error uploading information to Sentry.io:");
print(error);
return SentryId.empty();
})
.then((response) {
print("Uploaded information to Sentry.io : ${response.toString()}");
});
}
/*
* Test if a certain error should be ignored by Sentry
*/
bool sentryIgnoreError(dynamic error) {
// Ignore 404 errors for media files
if (error is HttpException) {
if (error.uri.toString().contains("/media/") && error.message.contains("404")) {
if (error.uri.toString().contains("/media/") &&
error.message.contains("404")) {
return true;
}
}
return false;
}
}

View File

@@ -11,12 +11,10 @@ import "package:inventree/api.dart";
import "package:inventree/app_colors.dart";
import "package:inventree/helpers.dart";
/*
* Base class definition for a "status code" definition.
*/
class InvenTreeStatusCode {
InvenTreeStatusCode(this.URL);
final String URL;
@@ -34,10 +32,7 @@ class InvenTreeStatusCode {
dynamic _entry = data[key];
if (_entry is Map<String, dynamic>) {
_choices.add({
"value": _entry["key"],
"display_name": _entry["label"]
});
_choices.add({"value": _entry["key"], "display_name": _entry["label"]});
}
}
@@ -46,7 +41,6 @@ class InvenTreeStatusCode {
// Load status code information from the server
Future<void> load({bool forceReload = false}) async {
// Return internally cached data
if (data.isNotEmpty && !forceReload) {
return;

View File

@@ -10,16 +10,14 @@ import "package:inventree/inventree/model.dart";
import "package:inventree/widget/stock/location_display.dart";
import "package:inventree/widget/stock/stock_detail.dart";
/*
* Class representing a test result for a single stock item
*/
class InvenTreeStockItemTestResult extends InvenTreeModel {
InvenTreeStockItemTestResult() : super();
InvenTreeStockItemTestResult.fromJson(Map<String, dynamic> json) : super.fromJson(json);
InvenTreeStockItemTestResult.fromJson(Map<String, dynamic> json)
: super.fromJson(json);
@override
String get URL => "stock/test/";
@@ -29,22 +27,16 @@ class InvenTreeStockItemTestResult extends InvenTreeModel {
@override
Map<String, String> defaultFilters() {
return {
"user_detail": "true",
"template_detail": "true",
};
return {"user_detail": "true", "template_detail": "true"};
}
@override
Map<String, Map<String, dynamic>> formFields() {
Map<String, Map<String, dynamic>> fields = {
"stock_item": {"hidden": true},
"test": {},
"template": {
"filters": {
"enabled": "true",
}
"filters": {"enabled": "true"},
},
"result": {},
"value": {},
@@ -68,44 +60,39 @@ class InvenTreeStockItemTestResult extends InvenTreeModel {
String get testName => getString("test");
bool get result => getBool("result");
String get value => getString("value");
String get attachment => getString("attachment");
String get username => getString("username", subKey: "user_detail");
String get date => getString("date");
@override
InvenTreeStockItemTestResult createFromJson(Map<String, dynamic> json) {
var result = InvenTreeStockItemTestResult.fromJson(json);
return result;
}
}
class InvenTreeStockItemHistory extends InvenTreeModel {
InvenTreeStockItemHistory() : super();
InvenTreeStockItemHistory.fromJson(Map<String, dynamic> json) : super.fromJson(json);
InvenTreeStockItemHistory.fromJson(Map<String, dynamic> json)
: super.fromJson(json);
@override
InvenTreeModel createFromJson(Map<String, dynamic> json) => InvenTreeStockItemHistory.fromJson(json);
InvenTreeModel createFromJson(Map<String, dynamic> json) =>
InvenTreeStockItemHistory.fromJson(json);
@override
String get URL => "stock/track/";
@override
Map<String, String> defaultFilters() {
// By default, order by decreasing date
return {
"ordering": "-date",
"user_detail": "true",
};
return {"ordering": "-date", "user_detail": "true"};
}
DateTime? get date => getDate("date");
@@ -113,7 +100,7 @@ class InvenTreeStockItemHistory extends InvenTreeModel {
String get dateString => getDateString("date");
String get label => getString("label");
// Return the "deltas" associated with this historical object
Map<String, dynamic> get deltas => getMap("deltas");
@@ -133,7 +120,6 @@ class InvenTreeStockItemHistory extends InvenTreeModel {
int? get user => getValue("user") as int?;
String get userString {
if (user != null) {
return getString("username", subKey: "user_detail");
} else {
@@ -142,12 +128,10 @@ class InvenTreeStockItemHistory extends InvenTreeModel {
}
}
/*
* Class representing a StockItem database instance
*/
class InvenTreeStockItem extends InvenTreeModel {
InvenTreeStockItem() : super();
InvenTreeStockItem.fromJson(Map<String, dynamic> json) : super.fromJson(json);
@@ -164,39 +148,18 @@ class InvenTreeStockItem extends InvenTreeModel {
Future<Object?> goToDetailPage(BuildContext context) async {
return Navigator.push(
context,
MaterialPageRoute(
builder: (context) => StockDetailWidget(this)
)
MaterialPageRoute(builder: (context) => StockDetailWidget(this)),
);
}
// Return a set of fields to transfer this stock item via dialog
Map<String, dynamic> transferFields() {
Map<String, dynamic> fields = {
"pk": {
"parent": "items",
"nested": true,
"hidden": true,
"value": pk,
},
"quantity": {
"parent": "items",
"nested": true,
"value": quantity,
},
"location": {
"value": locationId,
},
"status": {
"parent": "items",
"nested": true,
"value": status,
},
"packaging": {
"parent": "items",
"nested": true,
"value": packaging,
},
"pk": {"parent": "items", "nested": true, "hidden": true, "value": pk},
"quantity": {"parent": "items", "nested": true, "value": quantity},
"location": {"value": locationId},
"status": {"parent": "items", "nested": true, "value": status},
"packaging": {"parent": "items", "nested": true, "value": packaging},
"notes": {},
};
@@ -233,10 +196,7 @@ class InvenTreeStockItem extends InvenTreeModel {
"location": {},
"quantity": {},
"serial": {},
"serial_numbers": {
"label": L10().serialNumbers,
"type": "string",
},
"serial_numbers": {"label": L10().serialNumbers, "type": "string"},
"status": {},
"batch": {},
"purchase_price": {},
@@ -250,13 +210,12 @@ class InvenTreeStockItem extends InvenTreeModel {
@override
Map<String, String> defaultFilters() {
return {
"part_detail": "true",
"location_detail": "true",
"supplier_detail": "true",
"supplier_part_detail": "true",
"cascade": "false"
"cascade": "false",
};
}
@@ -265,21 +224,18 @@ class InvenTreeStockItem extends InvenTreeModel {
int get testTemplateCount => testTemplates.length;
// Get all the test templates associated with this StockItem
Future<void> getTestTemplates({bool showDialog=false}) async {
await InvenTreePartTestTemplate().list(
filters: {
"part": "${partId}",
"enabled": "true",
},
).then((var templates) {
testTemplates.clear();
Future<void> getTestTemplates({bool showDialog = false}) async {
await InvenTreePartTestTemplate()
.list(filters: {"part": "${partId}", "enabled": "true"})
.then((var templates) {
testTemplates.clear();
for (var t in templates) {
if (t is InvenTreePartTestTemplate) {
testTemplates.add(t);
}
}
});
for (var t in templates) {
if (t is InvenTreePartTestTemplate) {
testTemplates.add(t);
}
}
});
}
List<InvenTreeStockItemTestResult> testResults = [];
@@ -287,21 +243,17 @@ class InvenTreeStockItem extends InvenTreeModel {
int get testResultCount => testResults.length;
Future<void> getTestResults() async {
await InvenTreeStockItemTestResult()
.list(filters: {"stock_item": "${pk}", "user_detail": "true"})
.then((var results) {
testResults.clear();
await InvenTreeStockItemTestResult().list(
filters: {
"stock_item": "${pk}",
"user_detail": "true",
},
).then((var results) {
testResults.clear();
for (var r in results) {
if (r is InvenTreeStockItemTestResult) {
testResults.add(r);
}
}
});
for (var r in results) {
if (r is InvenTreeStockItemTestResult) {
testResults.add(r);
}
}
});
}
int get status => getInt("status");
@@ -313,7 +265,7 @@ class InvenTreeStockItem extends InvenTreeModel {
String get batch => getString("batch");
int get partId => getInt("part");
double? get purchasePrice {
String pp = getString("purchase_price");
@@ -334,7 +286,7 @@ class InvenTreeStockItem extends InvenTreeModel {
int get purchaseOrderId => getInt("purchase_order");
int get trackingItemCount => getInt("tracking_items", backup: 0);
bool get isBuilding => getBool("is_building");
int get salesOrderId => getInt("sales_order");
@@ -362,274 +314,275 @@ class InvenTreeStockItem extends InvenTreeModel {
String get stocktakeDateString => getDateString("stocktake_date");
String get partName {
String get partName {
String nm = "";
String nm = "";
// Use the detailed part information as priority
if (jsondata.containsKey("part_detail")) {
nm = (jsondata["part_detail"]?["full_name"] ?? "") as String;
}
// Backup if first value fails
if (nm.isEmpty) {
nm = getString("part__name");
}
return nm;
// Use the detailed part information as priority
if (jsondata.containsKey("part_detail")) {
nm = (jsondata["part_detail"]?["full_name"] ?? "") as String;
}
String get partDescription {
String desc = "";
// Use the detailed part description as priority
if (jsondata.containsKey("part_detail")) {
desc = (jsondata["part_detail"]?["description"] ?? "") as String;
}
if (desc.isEmpty) {
desc = getString("part__description");
}
return desc;
// Backup if first value fails
if (nm.isEmpty) {
nm = getString("part__name");
}
String get partImage {
String img = "";
return nm;
}
if (jsondata.containsKey("part_detail")) {
img = (jsondata["part_detail"]?["thumbnail"] ?? "") as String;
}
String get partDescription {
String desc = "";
if (img.isEmpty) {
img = getString("part__thumbnail");
}
return img;
// Use the detailed part description as priority
if (jsondata.containsKey("part_detail")) {
desc = (jsondata["part_detail"]?["description"] ?? "") as String;
}
/*
if (desc.isEmpty) {
desc = getString("part__description");
}
return desc;
}
String get partImage {
String img = "";
if (jsondata.containsKey("part_detail")) {
img = (jsondata["part_detail"]?["thumbnail"] ?? "") as String;
}
if (img.isEmpty) {
img = getString("part__thumbnail");
}
return img;
}
/*
* Return the Part thumbnail for this stock item.
*/
String get partThumbnail {
String get partThumbnail {
String thumb = "";
String thumb = "";
thumb = (jsondata["part_detail"]?["thumbnail"] ?? "") as String;
thumb = (jsondata["part_detail"]?["thumbnail"] ?? "") as String;
// Use "image" as a backup
if (thumb.isEmpty) {
thumb = (jsondata["part_detail"]?["image"] ?? "") as String;
}
// Try a different approach
if (thumb.isEmpty) {
thumb = getString("part__thumbnail");
}
// Still no thumbnail? Use the "no image" image
if (thumb.isEmpty) thumb = InvenTreeAPI.staticThumb;
return thumb;
// Use "image" as a backup
if (thumb.isEmpty) {
thumb = (jsondata["part_detail"]?["image"] ?? "") as String;
}
int get supplierPartId => getInt("supplier_part");
String get supplierImage {
String thumb = "";
if (jsondata.containsKey("supplier_part_detail")) {
thumb = (jsondata["supplier_part_detail"]?["supplier_detail"]?["image"] ?? "") as String;
} else if (jsondata.containsKey("supplier_detail")) {
thumb = (jsondata["supplier_detail"]?["image"] ?? "") as String;
}
return thumb;
// Try a different approach
if (thumb.isEmpty) {
thumb = getString("part__thumbnail");
}
String get supplierName => getString("supplier_name", subKey: "supplier_detail");
// Still no thumbnail? Use the "no image" image
if (thumb.isEmpty) thumb = InvenTreeAPI.staticThumb;
String get units => getString("units", subKey: "part_detail");
return thumb;
}
String get supplierSKU => getString("SKU", subKey: "supplier_part_detail");
int get supplierPartId => getInt("supplier_part");
String get serialNumber => getString("serial");
String get supplierImage {
String thumb = "";
double get quantity => getDouble("quantity");
if (jsondata.containsKey("supplier_part_detail")) {
thumb =
(jsondata["supplier_part_detail"]?["supplier_detail"]?["image"] ?? "")
as String;
} else if (jsondata.containsKey("supplier_detail")) {
thumb = (jsondata["supplier_detail"]?["image"] ?? "") as String;
}
String quantityString({bool includeUnits = true}){
return thumb;
}
String q = "";
String get supplierName =>
getString("supplier_name", subKey: "supplier_detail");
if (allocated > 0) {
q += simpleNumberString(available);
q += " / ";
}
String get units => getString("units", subKey: "part_detail");
q += simpleNumberString(quantity);
String get supplierSKU => getString("SKU", subKey: "supplier_part_detail");
if (includeUnits && units.isNotEmpty) {
String get serialNumber => getString("serial");
double get quantity => getDouble("quantity");
String quantityString({bool includeUnits = true}) {
String q = "";
if (allocated > 0) {
q += simpleNumberString(available);
q += " / ";
}
q += simpleNumberString(quantity);
if (includeUnits && units.isNotEmpty) {
q += " ${units}";
}
return q;
}
double get allocated => getDouble("allocated");
double get available => quantity - allocated;
int get locationId => getInt("location");
bool isSerialized() => serialNumber.isNotEmpty && quantity.toInt() == 1;
String serialOrQuantityDisplay() {
if (isSerialized()) {
return "SN ${serialNumber}";
} else if (allocated > 0) {
return "${available} / ${quantity}";
} else {
return simpleNumberString(quantity);
}
}
String get locationName {
if (locationId == -1 || !jsondata.containsKey("location_detail")) {
return "Unknown Location";
}
String loc = getString("name", subKey: "location_detail");
// Old-style name
if (loc.isEmpty) {
loc = getString("location__name");
}
return loc;
}
String get locationPathString {
if (locationId == -1 || !jsondata.containsKey("location_detail")) {
return L10().locationNotSet;
}
String _loc = getString("pathstring", subKey: "location_detail");
if (_loc.isNotEmpty) {
return _loc;
} else {
return locationName;
}
}
String get displayQuantity {
// Display either quantity or serial number!
if (serialNumber.isNotEmpty) {
return "SN: $serialNumber";
} else {
String q = simpleNumberString(quantity);
if (units.isNotEmpty) {
q += " ${units}";
}
return q;
}
}
double get allocated => getDouble("allocated");
@override
InvenTreeModel createFromJson(Map<String, dynamic> json) =>
InvenTreeStockItem.fromJson(json);
double get available => quantity - allocated;
int get locationId => getInt("location");
bool isSerialized() => serialNumber.isNotEmpty && quantity.toInt() == 1;
String serialOrQuantityDisplay() {
if (isSerialized()) {
return "SN ${serialNumber}";
} else if (allocated > 0) {
return "${available} / ${quantity}";
} else {
return simpleNumberString(quantity);
}
}
String get locationName {
if (locationId == -1 || !jsondata.containsKey("location_detail")) return "Unknown Location";
String loc = getString("name", subKey: "location_detail");
// Old-style name
if (loc.isEmpty) {
loc = getString("location__name");
}
return loc;
}
String get locationPathString {
if (locationId == -1 || !jsondata.containsKey("location_detail")) return L10().locationNotSet;
String _loc = getString("pathstring", subKey: "location_detail");
if (_loc.isNotEmpty) {
return _loc;
} else {
return locationName;
}
}
String get displayQuantity {
// Display either quantity or serial number!
if (serialNumber.isNotEmpty) {
return "SN: $serialNumber";
} else {
String q = simpleNumberString(quantity);
if (units.isNotEmpty) {
q += " ${units}";
}
return q;
}
}
@override
InvenTreeModel createFromJson(Map<String, dynamic> json) => InvenTreeStockItem.fromJson(json);
/*
/*
* Perform stocktake action:
*
* - Add
* - Remove
* - Count
*/
Future<bool> adjustStock(String endpoint, double q, {String? notes, int? location}) async {
// Serialized stock cannot be adjusted (unless it is a "transfer")
if (isSerialized() && location == null) {
return false;
}
// Cannot handle negative stock
if (q < 0) {
return false;
}
Map<String, dynamic> data = {};
data = {
"items": [
{
"pk": "${pk}",
"quantity": "${quantity}",
}
],
"notes": notes ?? "",
};
if (location != null) {
data["location"] = location;
}
var response = await api.post(
endpoint,
body: data,
);
return response.isValid() && (response.statusCode == 200 || response.statusCode == 201);
Future<bool> adjustStock(
String endpoint,
double q, {
String? notes,
int? location,
}) async {
// Serialized stock cannot be adjusted (unless it is a "transfer")
if (isSerialized() && location == null) {
return false;
}
Future<bool> countStock(double q, {String? notes}) async {
final bool result = await adjustStock("/stock/count/", q, notes: notes);
return result;
// Cannot handle negative stock
if (q < 0) {
return false;
}
Future<bool> addStock(double q, {String? notes}) async {
Map<String, dynamic> data = {};
final bool result = await adjustStock("/stock/add/", q, notes: notes);
data = {
"items": [
{"pk": "${pk}", "quantity": "${quantity}"},
],
"notes": notes ?? "",
};
return result;
if (location != null) {
data["location"] = location;
}
Future<bool> removeStock(double q, {String? notes}) async {
var response = await api.post(endpoint, body: data);
final bool result = await adjustStock("/stock/remove/", q, notes: notes);
return result;
}
Future<bool> transferStock(int location, {double? quantity, String? notes}) async {
double q = this.quantity;
if (quantity != null) {
q = quantity;
}
final bool result = await adjustStock(
"/stock/transfer/",
q,
notes: notes,
location: location,
);
return result;
}
return response.isValid() &&
(response.statusCode == 200 || response.statusCode == 201);
}
Future<bool> countStock(double q, {String? notes}) async {
final bool result = await adjustStock("/stock/count/", q, notes: notes);
return result;
}
Future<bool> addStock(double q, {String? notes}) async {
final bool result = await adjustStock("/stock/add/", q, notes: notes);
return result;
}
Future<bool> removeStock(double q, {String? notes}) async {
final bool result = await adjustStock("/stock/remove/", q, notes: notes);
return result;
}
Future<bool> transferStock(
int location, {
double? quantity,
String? notes,
}) async {
double q = this.quantity;
if (quantity != null) {
q = quantity;
}
final bool result = await adjustStock(
"/stock/transfer/",
q,
notes: notes,
location: location,
);
return result;
}
}
/*
* Class representing an attachment file against a StockItem object
*/
class InvenTreeStockItemAttachment extends InvenTreeAttachment {
InvenTreeStockItemAttachment() : super();
InvenTreeStockItemAttachment.fromJson(Map<String, dynamic> json) : super.fromJson(json);
InvenTreeStockItemAttachment.fromJson(Map<String, dynamic> json)
: super.fromJson(json);
@override
String get REFERENCE_FIELD => "stock_item";
@@ -638,18 +591,20 @@ class InvenTreeStockItemAttachment extends InvenTreeAttachment {
String get REF_MODEL_TYPE => "stockitem";
@override
String get URL => InvenTreeAPI().supportsModernAttachments ? "attachment/" : "stock/attachment/";
String get URL => InvenTreeAPI().supportsModernAttachments
? "attachment/"
: "stock/attachment/";
@override
InvenTreeModel createFromJson(Map<String, dynamic> json) => InvenTreeStockItemAttachment.fromJson(json);
InvenTreeModel createFromJson(Map<String, dynamic> json) =>
InvenTreeStockItemAttachment.fromJson(json);
}
class InvenTreeStockLocation extends InvenTreeModel {
InvenTreeStockLocation() : super();
InvenTreeStockLocation.fromJson(Map<String, dynamic> json) : super.fromJson(json);
InvenTreeStockLocation.fromJson(Map<String, dynamic> json)
: super.fromJson(json);
@override
String get URL => "stock/location/";
@@ -665,9 +620,7 @@ class InvenTreeStockLocation extends InvenTreeModel {
Future<Object?> goToDetailPage(BuildContext context) async {
return Navigator.push(
context,
MaterialPageRoute(
builder: (context) => LocationDisplayWidget(this)
)
MaterialPageRoute(builder: (context) => LocationDisplayWidget(this)),
);
}
@@ -684,7 +637,6 @@ class InvenTreeStockLocation extends InvenTreeModel {
}
String get parentPathString {
List<String> psplit = pathstring.split("/");
if (psplit.isNotEmpty) {
@@ -703,6 +655,6 @@ class InvenTreeStockLocation extends InvenTreeModel {
int get itemcount => (jsondata["items"] ?? 0) as int;
@override
InvenTreeModel createFromJson(Map<String, dynamic> json) => InvenTreeStockLocation.fromJson(json);
InvenTreeModel createFromJson(Map<String, dynamic> json) =>
InvenTreeStockLocation.fromJson(json);
}

View File

@@ -7,8 +7,7 @@ import "package:flutter/material.dart";
import "package:inventree/helpers.dart";
// Shortcut function to reduce boilerplate!
I18N L10()
{
I18N L10() {
// Testing mode - ignore context
if (!hasContext()) {
return I18NEn();
@@ -26,4 +25,4 @@ I18N L10()
// Fallback for "null" context
return I18NEn();
}
}

View File

@@ -90,6 +90,7 @@ def generate_locale_list(locales):
output.write(
"// This file is auto-generated by the 'collect_translations.py' script - do not edit it directly!\n\n"
)
output.write("// dart format off\n\n")
output.write('import "package:flutter/material.dart";\n\n')
output.write("const List<Locale> supported_locales = [\n")

View File

@@ -18,7 +18,6 @@ Future<void> selectAndPrintLabel(
String labelType,
String labelQuery,
) async {
if (!InvenTreeAPI().isConnected()) {
return;
}
@@ -44,10 +43,7 @@ Future<void> selectAndPrintLabel(
int pk = (label["pk"] ?? -1) as int;
if (name.isNotEmpty && pk > 0) {
label_options.add({
"display_name": name,
"value": pk,
});
label_options.add({"display_name": name, "value": pk});
}
}
@@ -57,13 +53,12 @@ Future<void> selectAndPrintLabel(
// Construct list of available plugins
for (var plugin in plugins) {
plugin_options.add({
"display_name": plugin.humanName,
"value": plugin.key,
});
plugin_options.add({"display_name": plugin.humanName, "value": plugin.key});
}
String selectedPlugin = await InvenTreeAPI().getUserSetting("LABEL_DEFAULT_PRINTER");
String selectedPlugin = await InvenTreeAPI().getUserSetting(
"LABEL_DEFAULT_PRINTER",
);
if (selectedPlugin.isNotEmpty) {
initial_plugin = selectedPlugin;
@@ -85,7 +80,7 @@ Future<void> selectAndPrintLabel(
"value": initial_plugin,
"choices": plugin_options,
"required": true,
}
},
};
launchApiForm(
@@ -99,18 +94,12 @@ Future<void> selectAndPrintLabel(
final plugin = data["plugin"];
if (template == null) {
showSnackIcon(
L10().labelSelectTemplate,
success: false,
);
showSnackIcon(L10().labelSelectTemplate, success: false);
return false;
}
if (plugin == null) {
showSnackIcon(
L10().labelSelectPrinter,
success: false,
);
showSnackIcon(L10().labelSelectPrinter, success: false);
return false;
}
@@ -123,42 +112,42 @@ Future<void> selectAndPrintLabel(
bool result = false;
if (labelId != -1 && pluginKey != null) {
showLoadingOverlay();
if (InvenTreeAPI().supportsModernLabelPrinting) {
// Modern label printing API uses a POST request to a single API endpoint.
await InvenTreeAPI().post(
"/label/print/",
body: {
"plugin": pluginKey,
"template": labelId,
"items": [instanceId]
}
).then((APIResponse response) {
await InvenTreeAPI()
.post(
"/label/print/",
body: {
"plugin": pluginKey,
"template": labelId,
"items": [instanceId],
},
)
.then((APIResponse response) {
if (response.isValid() &&
response.statusCode >= 200 &&
response.statusCode <= 201) {
var data = response.asMap();
if (response.isValid() && response.statusCode >= 200 &&
response.statusCode <= 201) {
var data = response.asMap();
if (data.containsKey("output")) {
String? label_file = (data["output"]) as String?;
if (data.containsKey("output")) {
String? label_file = (data["output"]) as String?;
if (label_file != null && label_file.isNotEmpty) {
// Attempt to open generated file
InvenTreeAPI().downloadFile(label_file);
}
if (label_file != null && label_file.isNotEmpty) {
// Attempt to open generated file
InvenTreeAPI().downloadFile(label_file);
result = true;
}
}
result = true;
}
}
});
});
} else {
// Legacy label printing API
// Uses a GET request to a specially formed URL which depends on the parameters
String url = "/label/${labelType}/${labelId}/print/?${labelQuery}&plugin=${pluginKey}";
String url =
"/label/${labelType}/${labelId}/print/?${labelQuery}&plugin=${pluginKey}";
await InvenTreeAPI().get(url).then((APIResponse response) {
if (response.isValid() && response.statusCode == 200) {
var data = response.asMap();
@@ -171,26 +160,20 @@ Future<void> selectAndPrintLabel(
}
}
});
}
}
hideLoadingOverlay();
hideLoadingOverlay();
if (result) {
showSnackIcon(
L10().printLabelSuccess,
success: true
);
} else {
showSnackIcon(
L10().printLabelFailure,
success: false,
);
if (result) {
showSnackIcon(L10().printLabelSuccess, success: true);
} else {
showSnackIcon(L10().printLabelFailure, success: false);
}
}
}
});
},
);
}
/*
* Discover which label templates are available for a given item
*/
@@ -198,8 +181,8 @@ Future<List<Map<String, dynamic>>> getLabelTemplates(
String labelType,
Map<String, String> data,
) async {
if (!InvenTreeAPI().isConnected() || !InvenTreeAPI().supportsMixin("labels")) {
if (!InvenTreeAPI().isConnected() ||
!InvenTreeAPI().supportsMixin("labels")) {
return [];
}
@@ -217,10 +200,7 @@ Future<List<Map<String, dynamic>>> getLabelTemplates(
List<Map<String, dynamic>> labels = [];
await InvenTreeAPI().get(
url,
params: data,
).then((APIResponse response) {
await InvenTreeAPI().get(url, params: data).then((APIResponse response) {
if (response.isValid() && response.statusCode == 200) {
for (var label in response.resultsList()) {
if (label is Map<String, dynamic>) {
@@ -231,4 +211,4 @@ Future<List<Map<String, dynamic>>> getLabelTemplates(
});
return labels;
}
}

View File

@@ -18,72 +18,73 @@ import "package:inventree/l10n/collected/app_localizations.dart";
import "package:inventree/settings/release.dart";
import "package:inventree/widget/home.dart";
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
final savedThemeMode = await AdaptiveTheme.getThemeMode();
await runZonedGuarded<Future<void>>(() async {
await runZonedGuarded<Future<void>>(
() async {
PackageInfo info = await PackageInfo.fromPlatform();
String pkg = info.packageName;
String version = info.version;
String build = info.buildNumber;
PackageInfo info = await PackageInfo.fromPlatform();
String pkg = info.packageName;
String version = info.version;
String build = info.buildNumber;
String release = "${pkg}@${version}:${build}";
String release = "${pkg}@${version}:${build}";
if (SENTRY_DSN_KEY.isNotEmpty) {
await Sentry.init((options) {
options.dsn = SENTRY_DSN_KEY;
options.release = release;
options.environment = isInDebugMode() ? "debug" : "release";
options.diagnosticLevel = SentryLevel.debug;
options.attachStacktrace = true;
});
}
if (SENTRY_DSN_KEY.isNotEmpty) {
await Sentry.init((options) {
options.dsn = SENTRY_DSN_KEY;
options.release = release;
options.environment = isInDebugMode() ? "debug" : "release";
options.diagnosticLevel = SentryLevel.debug;
options.attachStacktrace = true;
// Pass any flutter errors off to the Sentry reporting context!
FlutterError.onError = (FlutterErrorDetails details) async {
// Ensure that the error gets reported to sentry!
await sentryReportError(
"FlutterError.onError",
details.exception,
details.stack,
context: {
"context": details.context.toString(),
"summary": details.summary.toString(),
"library": details.library ?? "null",
},
);
};
final int orientation =
await InvenTreeSettingsManager().getValue(
INV_SCREEN_ORIENTATION,
SCREEN_ORIENTATION_SYSTEM,
)
as int;
List<DeviceOrientation> orientations = [];
switch (orientation) {
case SCREEN_ORIENTATION_PORTRAIT:
orientations.add(DeviceOrientation.portraitUp);
case SCREEN_ORIENTATION_LANDSCAPE:
orientations.add(DeviceOrientation.landscapeLeft);
default:
orientations.add(DeviceOrientation.portraitUp);
orientations.add(DeviceOrientation.landscapeLeft);
orientations.add(DeviceOrientation.landscapeRight);
}
SystemChrome.setPreferredOrientations(orientations).then((_) {
runApp(InvenTreeApp(savedThemeMode));
});
}
// Pass any flutter errors off to the Sentry reporting context!
FlutterError.onError = (FlutterErrorDetails details) async {
// Ensure that the error gets reported to sentry!
await sentryReportError(
"FlutterError.onError",
details.exception, details.stack,
context: {
"context": details.context.toString(),
"summary": details.summary.toString(),
"library": details.library ?? "null",
}
);
};
final int orientation = await InvenTreeSettingsManager().getValue(INV_SCREEN_ORIENTATION, SCREEN_ORIENTATION_SYSTEM) as int;
List<DeviceOrientation> orientations = [];
switch (orientation) {
case SCREEN_ORIENTATION_PORTRAIT:
orientations.add(DeviceOrientation.portraitUp);
case SCREEN_ORIENTATION_LANDSCAPE:
orientations.add(DeviceOrientation.landscapeLeft);
default:
orientations.add(DeviceOrientation.portraitUp);
orientations.add(DeviceOrientation.landscapeLeft);
orientations.add(DeviceOrientation.landscapeRight);
}
SystemChrome.setPreferredOrientations(orientations).then((_) {
runApp(
InvenTreeApp(savedThemeMode)
);
});
}, (Object error, StackTrace stackTrace) async {
sentryReportError("main.runZonedGuarded", error, stackTrace);
});
},
(Object error, StackTrace stackTrace) async {
sentryReportError("main.runZonedGuarded", error, stackTrace);
},
);
}
class InvenTreeApp extends StatefulWidget {
@@ -96,13 +97,11 @@ class InvenTreeApp extends StatefulWidget {
@override
InvenTreeAppState createState() => InvenTreeAppState(savedThemeMode);
static InvenTreeAppState? of(BuildContext context) => context.findAncestorStateOfType<InvenTreeAppState>();
static InvenTreeAppState? of(BuildContext context) =>
context.findAncestorStateOfType<InvenTreeAppState>();
}
class InvenTreeAppState extends State<StatefulWidget> {
InvenTreeAppState(this.savedThemeMode) : super();
// Custom _locale (default = null; use system default)
@@ -120,16 +119,17 @@ class InvenTreeAppState extends State<StatefulWidget> {
// Run app init routines in the background
Future<void> runInitTasks() async {
// Set the app locale (language)
Locale? locale = await InvenTreeSettingsManager().getSelectedLocale();
setLocale(locale);
// Display release notes if this is a new version
final String version = await InvenTreeSettingsManager().getValue("recentVersion", "") as String;
final String version =
await InvenTreeSettingsManager().getValue("recentVersion", "")
as String;
final PackageInfo info = await PackageInfo.fromPlatform();
if (version != info.version) {
// Save latest version to the settings database
await InvenTreeSettingsManager().setValue("recentVersion", info.version);
@@ -139,7 +139,7 @@ class InvenTreeAppState extends State<StatefulWidget> {
// Show the release notes
OneContext().push(
MaterialPageRoute(builder: (context) => ReleaseNotesWidget(notes))
MaterialPageRoute(builder: (context) => ReleaseNotesWidget(notes)),
);
}
}
@@ -155,7 +155,6 @@ class InvenTreeAppState extends State<StatefulWidget> {
@override
Widget build(BuildContext context) {
return AdaptiveTheme(
light: ThemeData(
brightness: Brightness.light,
@@ -168,7 +167,7 @@ class InvenTreeAppState extends State<StatefulWidget> {
useMaterial3: true,
),
initial: savedThemeMode ?? AdaptiveThemeMode.light,
builder: (light, dark) => MaterialApp(
builder: (light, dark) => MaterialApp(
theme: light,
darkTheme: dark,
debugShowCheckedModeBanner: false,
@@ -185,7 +184,7 @@ class InvenTreeAppState extends State<StatefulWidget> {
],
supportedLocales: supported_locales,
locale: _locale,
)
),
);
}
}

View File

@@ -6,7 +6,6 @@ import "package:path_provider/path_provider.dart";
import "package:sembast/sembast_io.dart";
import "package:path/path.dart";
// Settings key values
const String INV_HOME_SHOW_SUBSCRIBED = "homeShowSubscribed";
const String INV_HOME_SHOW_PO = "homeShowPo";
@@ -62,7 +61,6 @@ const int BARCODE_CONTROLLER_WEDGE = 1;
* Class for storing InvenTree preferences in a NoSql DB
*/
class InvenTreePreferencesDB {
InvenTreePreferencesDB._();
static final InvenTreePreferencesDB _singleton = InvenTreePreferencesDB._();
@@ -74,7 +72,6 @@ class InvenTreePreferencesDB {
bool isOpen = false;
Future<Database> get database async {
if (!isOpen) {
// Calling _openDatabase will also complete the completer with database instance
_openDatabase();
@@ -101,13 +98,11 @@ class InvenTreePreferencesDB {
}
}
/*
* InvenTree setings manager class.
* Provides functions for loading and saving settings, with provision for default values
*/
class InvenTreeSettingsManager {
factory InvenTreeSettingsManager() {
return _manager;
}
@@ -144,7 +139,6 @@ class InvenTreeSettingsManager {
}
Future<dynamic> getValue(String key, dynamic backup) async {
dynamic value = await store.record(key).get(await _db);
// Retrieve value
@@ -174,7 +168,6 @@ class InvenTreeSettingsManager {
// Store a key:value pair in the database
Future<void> setValue(String key, dynamic value) async {
// Encode null values as strings
value ??= "__null__";
@@ -182,5 +175,6 @@ class InvenTreeSettingsManager {
}
// Ensure we only ever create a single instance of this class
static final InvenTreeSettingsManager _manager = InvenTreeSettingsManager._internal();
static final InvenTreeSettingsManager _manager =
InvenTreeSettingsManager._internal();
}

View File

@@ -13,34 +13,30 @@ import "package:url_launcher/url_launcher.dart";
const String DOCS_URL = "https://docs.inventree.org/app";
class InvenTreeAboutWidget extends StatelessWidget {
const InvenTreeAboutWidget(this.info) : super();
final PackageInfo info;
Future <void> _releaseNotes(BuildContext context) async {
Future<void> _releaseNotes(BuildContext context) async {
// Load release notes from external file
String notes = await rootBundle.loadString("assets/release_notes.md");
Navigator.push(
context,
MaterialPageRoute(builder: (context) => ReleaseNotesWidget(notes))
context,
MaterialPageRoute(builder: (context) => ReleaseNotesWidget(notes)),
);
}
Future <void> _credits(BuildContext context) async {
Future<void> _credits(BuildContext context) async {
String notes = await rootBundle.loadString("assets/credits.md");
Navigator.push(
context,
MaterialPageRoute(builder: (context) => CreditsWidget(notes))
MaterialPageRoute(builder: (context) => CreditsWidget(notes)),
);
}
Future <void> _openDocs() async {
Future<void> _openDocs() async {
var docsUrl = Uri.parse(DOCS_URL);
if (await canLaunchUrl(docsUrl)) {
@@ -48,23 +44,24 @@ class InvenTreeAboutWidget extends StatelessWidget {
}
}
Future <void> _reportBug(BuildContext context) async {
Future<void> _reportBug(BuildContext context) async {
var url = Uri(
scheme: "https",
host: "github.com",
path: "inventree/inventree-app/issues/new?title=Enter+bug+description");
scheme: "https",
host: "github.com",
path: "inventree/inventree-app/issues/new?title=Enter+bug+description",
);
if (await canLaunchUrl(url)) {
await launchUrl(url);
}
}
Future <void> _translate() async {
Future<void> _translate() async {
var url = Uri(
scheme: "https",
host: "crowdin.com",
path: "/project/inventree");
scheme: "https",
host: "crowdin.com",
path: "/project/inventree",
);
if (await canLaunchUrl(url)) {
await launchUrl(url);
@@ -73,7 +70,6 @@ class InvenTreeAboutWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
List<Widget> tiles = [];
tiles.add(
@@ -82,41 +78,57 @@ class InvenTreeAboutWidget extends StatelessWidget {
L10().serverDetails,
style: TextStyle(fontWeight: FontWeight.bold),
),
)
),
);
if (InvenTreeAPI().isConnected()) {
tiles.add(
ListTile(
title: Text(L10().address),
subtitle: Text(InvenTreeAPI().baseUrl.isNotEmpty ? InvenTreeAPI().baseUrl : L10().notConnected),
leading: Icon(TablerIcons.globe),
trailing: InvenTreeAPI().isConnected() ? Icon(TablerIcons.circle_check, color: COLOR_SUCCESS) : Icon(TablerIcons.circle_x, color: COLOR_DANGER),
)
ListTile(
title: Text(L10().address),
subtitle: Text(
InvenTreeAPI().baseUrl.isNotEmpty
? InvenTreeAPI().baseUrl
: L10().notConnected,
),
leading: Icon(TablerIcons.globe),
trailing: InvenTreeAPI().isConnected()
? Icon(TablerIcons.circle_check, color: COLOR_SUCCESS)
: Icon(TablerIcons.circle_x, color: COLOR_DANGER),
),
);
tiles.add(
ListTile(
title: Text(L10().username),
subtitle: Text(InvenTreeAPI().username),
leading: InvenTreeAPI().username.isNotEmpty ? Icon(TablerIcons.user) : Icon(TablerIcons.user_cancel, color: COLOR_DANGER),
)
leading: InvenTreeAPI().username.isNotEmpty
? Icon(TablerIcons.user)
: Icon(TablerIcons.user_cancel, color: COLOR_DANGER),
),
);
tiles.add(
ListTile(
title: Text(L10().version),
subtitle: Text(InvenTreeAPI().serverVersion.isNotEmpty ? InvenTreeAPI().serverVersion : L10().notConnected),
subtitle: Text(
InvenTreeAPI().serverVersion.isNotEmpty
? InvenTreeAPI().serverVersion
: L10().notConnected,
),
leading: Icon(TablerIcons.info_circle),
)
),
);
tiles.add(
ListTile(
title: Text(L10().serverInstance),
subtitle: Text(InvenTreeAPI().serverInstance.isNotEmpty ? InvenTreeAPI().serverInstance : L10().notConnected),
subtitle: Text(
InvenTreeAPI().serverInstance.isNotEmpty
? InvenTreeAPI().serverInstance
: L10().notConnected,
),
leading: Icon(TablerIcons.server),
)
),
);
// Display extra tile if the server supports plugins
@@ -125,9 +137,8 @@ class InvenTreeAboutWidget extends StatelessWidget {
title: Text(L10().pluginSupport),
subtitle: Text(L10().pluginSupportDetail),
leading: Icon(TablerIcons.plug),
)
),
);
} else {
tiles.add(
ListTile(
@@ -136,8 +147,8 @@ class InvenTreeAboutWidget extends StatelessWidget {
L10().serverNotConnected,
style: TextStyle(fontStyle: FontStyle.italic),
),
leading: Icon(TablerIcons.exclamation_circle)
)
leading: Icon(TablerIcons.exclamation_circle),
),
);
}
@@ -147,23 +158,23 @@ class InvenTreeAboutWidget extends StatelessWidget {
L10().appDetails,
style: TextStyle(fontWeight: FontWeight.bold),
),
)
),
);
tiles.add(
ListTile(
title: Text(L10().packageName),
subtitle: Text("${info.packageName}"),
leading: Icon(TablerIcons.box)
)
leading: Icon(TablerIcons.box),
),
);
tiles.add(
ListTile(
title: Text(L10().version),
subtitle: Text("${info.version} - Build ${info.buildNumber}"),
leading: Icon(TablerIcons.info_circle)
)
leading: Icon(TablerIcons.info_circle),
),
);
tiles.add(
@@ -174,7 +185,7 @@ class InvenTreeAboutWidget extends StatelessWidget {
onTap: () {
_releaseNotes(context);
},
)
),
);
tiles.add(
@@ -184,8 +195,8 @@ class InvenTreeAboutWidget extends StatelessWidget {
leading: Icon(TablerIcons.balloon, color: COLOR_ACTION),
onTap: () {
_credits(context);
}
)
},
),
);
tiles.add(
@@ -196,7 +207,7 @@ class InvenTreeAboutWidget extends StatelessWidget {
onTap: () {
_openDocs();
},
)
),
);
tiles.add(
@@ -206,8 +217,8 @@ class InvenTreeAboutWidget extends StatelessWidget {
leading: Icon(TablerIcons.language, color: COLOR_ACTION),
onTap: () {
_translate();
}
)
},
),
);
tiles.add(
@@ -216,9 +227,9 @@ class InvenTreeAboutWidget extends StatelessWidget {
subtitle: Text(L10().reportBugDescription),
leading: Icon(TablerIcons.bug, color: COLOR_ACTION),
onTap: () {
_reportBug(context);
_reportBug(context);
},
)
),
);
return Scaffold(
@@ -227,11 +238,8 @@ class InvenTreeAboutWidget extends StatelessWidget {
backgroundColor: COLOR_APP_BAR,
),
body: ListView(
children: ListTile.divideTiles(
context: context,
tiles: tiles,
).toList(),
)
children: ListTile.divideTiles(context: context, tiles: tiles).toList(),
),
);
}
}
}

View File

@@ -16,17 +16,16 @@ import "package:inventree/preferences.dart";
import "package:inventree/widget/dialogs.dart";
import "package:inventree/widget/progress.dart";
class InvenTreeAppSettingsWidget extends StatefulWidget {
@override
_InvenTreeAppSettingsState createState() => _InvenTreeAppSettingsState();
}
class _InvenTreeAppSettingsState extends State<InvenTreeAppSettingsWidget> {
_InvenTreeAppSettingsState();
final GlobalKey<_InvenTreeAppSettingsState> _settingsKey = GlobalKey<_InvenTreeAppSettingsState>();
final GlobalKey<_InvenTreeAppSettingsState> _settingsKey =
GlobalKey<_InvenTreeAppSettingsState>();
// Sound settings
bool barcodeSounds = true;
@@ -48,16 +47,33 @@ class _InvenTreeAppSettingsState extends State<InvenTreeAppSettingsWidget> {
loadSettings(OneContext().context!);
}
Future <void> loadSettings(BuildContext context) async {
Future<void> loadSettings(BuildContext context) async {
showLoadingOverlay();
barcodeSounds = await InvenTreeSettingsManager().getValue(INV_SOUNDS_BARCODE, true) as bool;
serverSounds = await InvenTreeSettingsManager().getValue(INV_SOUNDS_SERVER, true) as bool;
reportErrors = await InvenTreeSettingsManager().getValue(INV_REPORT_ERRORS, true) as bool;
strictHttps = await InvenTreeSettingsManager().getValue(INV_STRICT_HTTPS, false) as bool;
screenOrientation = await InvenTreeSettingsManager().getValue(INV_SCREEN_ORIENTATION, SCREEN_ORIENTATION_SYSTEM) as int;
enableLabelPrinting = await InvenTreeSettingsManager().getValue(INV_ENABLE_LABEL_PRINTING, true) as bool;
barcodeSounds =
await InvenTreeSettingsManager().getValue(INV_SOUNDS_BARCODE, true)
as bool;
serverSounds =
await InvenTreeSettingsManager().getValue(INV_SOUNDS_SERVER, true)
as bool;
reportErrors =
await InvenTreeSettingsManager().getValue(INV_REPORT_ERRORS, true)
as bool;
strictHttps =
await InvenTreeSettingsManager().getValue(INV_STRICT_HTTPS, false)
as bool;
screenOrientation =
await InvenTreeSettingsManager().getValue(
INV_SCREEN_ORIENTATION,
SCREEN_ORIENTATION_SYSTEM,
)
as int;
enableLabelPrinting =
await InvenTreeSettingsManager().getValue(
INV_ENABLE_LABEL_PRINTING,
true,
)
as bool;
darkMode = AdaptiveTheme.of(context).mode.isDark;
@@ -71,19 +87,15 @@ class _InvenTreeAppSettingsState extends State<InvenTreeAppSettingsWidget> {
}
Future<void> _selectLocale(BuildContext context) async {
List<Map<String, dynamic>> options = [
{
"display_name": L10().languageDefault,
"value": null,
}
{"display_name": L10().languageDefault, "value": null},
];
// Construct a list of available locales
for (var locale in supported_locales) {
options.add({
"display_name": LocaleNames.of(context)!.nameOf(locale.toString()),
"value": locale.toString()
"value": locale.toString(),
});
}
@@ -93,7 +105,7 @@ class _InvenTreeAppSettingsState extends State<InvenTreeAppSettingsWidget> {
"type": "choice",
"choices": options,
"value": locale?.toString(),
}
},
};
launchApiForm(
@@ -103,7 +115,6 @@ class _InvenTreeAppSettingsState extends State<InvenTreeAppSettingsWidget> {
fields,
icon: TablerIcons.circle_check,
onSuccess: (Map<String, dynamic> data) async {
String locale_name = (data["locale"] ?? "") as String;
Locale? selected_locale;
@@ -124,18 +135,18 @@ class _InvenTreeAppSettingsState extends State<InvenTreeAppSettingsWidget> {
// Clear the cached status label information
InvenTreeAPI().clearStatusCodeData();
}
},
);
}
@override
Widget build(BuildContext context) {
String languageName = L10().languageDefault;
if (locale != null) {
languageName = LocaleNames.of(context)!.nameOf(locale.toString()) ?? L10().languageDefault;
languageName =
LocaleNames.of(context)!.nameOf(locale.toString()) ??
L10().languageDefault;
}
IconData orientationIcon = Icons.screen_rotation;
@@ -154,7 +165,7 @@ class _InvenTreeAppSettingsState extends State<InvenTreeAppSettingsWidget> {
key: _settingsKey,
appBar: AppBar(
title: Text(L10().appSettings),
backgroundColor: COLOR_APP_BAR
backgroundColor: COLOR_APP_BAR,
),
body: Container(
child: ListView(
@@ -183,8 +194,8 @@ class _InvenTreeAppSettingsState extends State<InvenTreeAppSettingsWidget> {
setState(() {
darkMode = value;
});
}
)
},
),
),
GestureDetector(
child: ListTile(
@@ -198,26 +209,43 @@ class _InvenTreeAppSettingsState extends State<InvenTreeAppSettingsWidget> {
L10().orientation,
[
ListTile(
leading: Icon(Icons.screen_rotation, color: screenOrientation == SCREEN_ORIENTATION_SYSTEM ? COLOR_ACTION : null),
leading: Icon(
Icons.screen_rotation,
color: screenOrientation == SCREEN_ORIENTATION_SYSTEM
? COLOR_ACTION
: null,
),
title: Text(L10().orientationSystem),
),
ListTile(
leading: Icon(Icons.screen_lock_portrait, color: screenOrientation == SCREEN_ORIENTATION_PORTRAIT ? COLOR_ACTION : null),
leading: Icon(
Icons.screen_lock_portrait,
color: screenOrientation == SCREEN_ORIENTATION_PORTRAIT
? COLOR_ACTION
: null,
),
title: Text(L10().orientationPortrait),
),
ListTile(
leading: Icon(Icons.screen_lock_landscape, color: screenOrientation == SCREEN_ORIENTATION_LANDSCAPE ? COLOR_ACTION : null),
leading: Icon(
Icons.screen_lock_landscape,
color: screenOrientation == SCREEN_ORIENTATION_LANDSCAPE
? COLOR_ACTION
: null,
),
title: Text(L10().orientationLandscape),
)
),
],
onSelected: (idx) async {
screenOrientation = idx as int;
InvenTreeSettingsManager().setValue(INV_SCREEN_ORIENTATION, screenOrientation);
InvenTreeSettingsManager().setValue(
INV_SCREEN_ORIENTATION,
screenOrientation,
);
setState(() {
});
}
setState(() {});
},
);
},
),
@@ -228,11 +256,14 @@ class _InvenTreeAppSettingsState extends State<InvenTreeAppSettingsWidget> {
trailing: Switch(
value: enableLabelPrinting,
onChanged: (bool value) {
InvenTreeSettingsManager().setValue(INV_ENABLE_LABEL_PRINTING, value);
InvenTreeSettingsManager().setValue(
INV_ENABLE_LABEL_PRINTING,
value,
);
setState(() {
enableLabelPrinting = value;
});
}
},
),
),
ListTile(
@@ -271,7 +302,7 @@ class _InvenTreeAppSettingsState extends State<InvenTreeAppSettingsWidget> {
},
),
),
ListTile(
ListTile(
title: Text(
L10().sounds,
style: TextStyle(fontWeight: FontWeight.bold),
@@ -300,7 +331,10 @@ class _InvenTreeAppSettingsState extends State<InvenTreeAppSettingsWidget> {
trailing: Switch(
value: barcodeSounds,
onChanged: (bool value) {
InvenTreeSettingsManager().setValue(INV_SOUNDS_BARCODE, value);
InvenTreeSettingsManager().setValue(
INV_SOUNDS_BARCODE,
value,
);
setState(() {
barcodeSounds = value;
});
@@ -308,9 +342,9 @@ class _InvenTreeAppSettingsState extends State<InvenTreeAppSettingsWidget> {
),
),
Divider(height: 1),
]
)
)
],
),
),
);
}
}
}

View File

@@ -7,44 +7,52 @@ import "package:inventree/app_colors.dart";
import "package:inventree/widget/dialogs.dart";
class InvenTreeBarcodeSettingsWidget extends StatefulWidget {
@override
_InvenTreeBarcodeSettingsState createState() => _InvenTreeBarcodeSettingsState();
_InvenTreeBarcodeSettingsState createState() =>
_InvenTreeBarcodeSettingsState();
}
class _InvenTreeBarcodeSettingsState
extends State<InvenTreeBarcodeSettingsWidget> {
_InvenTreeBarcodeSettingsState();
class _InvenTreeBarcodeSettingsState extends State<InvenTreeBarcodeSettingsWidget> {
int barcodeScanDelay = 500;
int barcodeScanType = BARCODE_CONTROLLER_CAMERA;
bool barcodeScanSingle = false;
_InvenTreeBarcodeSettingsState();
final TextEditingController _barcodeScanDelayController =
TextEditingController();
int barcodeScanDelay = 500;
int barcodeScanType = BARCODE_CONTROLLER_CAMERA;
bool barcodeScanSingle = false;
final TextEditingController _barcodeScanDelayController = TextEditingController();
@override
void initState() {
super.initState();
loadSettings();
}
@override
void initState() {
super.initState();
loadSettings();
}
Future<void> loadSettings() async {
barcodeScanDelay = await InvenTreeSettingsManager().getValue(INV_BARCODE_SCAN_DELAY, 500) as int;
barcodeScanType = await InvenTreeSettingsManager().getValue(INV_BARCODE_SCAN_TYPE, BARCODE_CONTROLLER_CAMERA) as int;
barcodeScanSingle = await InvenTreeSettingsManager().getBool(INV_BARCODE_SCAN_SINGLE, false);
barcodeScanDelay =
await InvenTreeSettingsManager().getValue(INV_BARCODE_SCAN_DELAY, 500)
as int;
barcodeScanType =
await InvenTreeSettingsManager().getValue(
INV_BARCODE_SCAN_TYPE,
BARCODE_CONTROLLER_CAMERA,
)
as int;
barcodeScanSingle = await InvenTreeSettingsManager().getBool(
INV_BARCODE_SCAN_SINGLE,
false,
);
if (mounted) {
setState(() {
});
setState(() {});
}
}
// Callback function to edit the barcode scan delay value
// TODO: Next time any new settings are added, refactor this into a generic function
Future<void> _editBarcodeScanDelay(BuildContext context) async {
_barcodeScanDelayController.text = barcodeScanDelay.toString();
return showDialog(
@@ -56,9 +64,7 @@ class _InvenTreeBarcodeSettingsState extends State<InvenTreeBarcodeSettingsWidge
onChanged: (value) {},
controller: _barcodeScanDelayController,
keyboardType: TextInputType.number,
decoration: InputDecoration(
hintText: L10().barcodeScanDelayDetail,
),
decoration: InputDecoration(hintText: L10().barcodeScanDelayDetail),
),
actions: <Widget>[
MaterialButton(
@@ -76,13 +82,18 @@ class _InvenTreeBarcodeSettingsState extends State<InvenTreeBarcodeSettingsWidge
textColor: Colors.white,
child: Text(L10().ok),
onPressed: () async {
int delay = int.tryParse(_barcodeScanDelayController.text) ?? barcodeScanDelay;
int delay =
int.tryParse(_barcodeScanDelayController.text) ??
barcodeScanDelay;
// Apply limits
if (delay < 100) delay = 100;
if (delay > 2500) delay = 2500;
InvenTreeSettingsManager().setValue(INV_BARCODE_SCAN_DELAY, delay);
InvenTreeSettingsManager().setValue(
INV_BARCODE_SCAN_DELAY,
delay,
);
setState(() {
barcodeScanDelay = delay;
Navigator.pop(context);
@@ -91,13 +102,12 @@ class _InvenTreeBarcodeSettingsState extends State<InvenTreeBarcodeSettingsWidge
),
],
);
}
},
);
}
@override
Widget build(BuildContext context) {
// Construct an icon for the barcode scanner input
Widget? barcodeInputIcon;
@@ -112,7 +122,7 @@ class _InvenTreeBarcodeSettingsState extends State<InvenTreeBarcodeSettingsWidge
return Scaffold(
appBar: AppBar(
title: Text(L10().barcodeSettings),
backgroundColor: COLOR_APP_BAR
backgroundColor: COLOR_APP_BAR,
),
body: Container(
child: ListView(
@@ -135,17 +145,20 @@ class _InvenTreeBarcodeSettingsState extends State<InvenTreeBarcodeSettingsWidge
title: Text(L10().scannerExternal),
subtitle: Text(L10().scannerExternalDetail),
leading: Icon(Icons.barcode_reader),
)
),
],
onSelected: (idx) async {
barcodeScanType = idx as int;
InvenTreeSettingsManager().setValue(INV_BARCODE_SCAN_TYPE, barcodeScanType);
InvenTreeSettingsManager().setValue(
INV_BARCODE_SCAN_TYPE,
barcodeScanType,
);
if (mounted) {
setState(() {});
}
}
},
);
}
},
),
ListTile(
title: Text(L10().barcodeScanDelay),
@@ -165,17 +178,19 @@ class _InvenTreeBarcodeSettingsState extends State<InvenTreeBarcodeSettingsWidge
trailing: Switch(
value: barcodeScanSingle,
onChanged: (bool v) {
InvenTreeSettingsManager().setValue(INV_BARCODE_SCAN_SINGLE, v);
InvenTreeSettingsManager().setValue(
INV_BARCODE_SCAN_SINGLE,
v,
);
setState(() {
barcodeScanSingle = v;
});
},
),
)
),
],
)
)
),
),
);
}
}
}

View File

@@ -1,4 +1,3 @@
import "package:flutter/material.dart";
import "package:flutter_tabler_icons/flutter_tabler_icons.dart";
import "package:inventree/app_colors.dart";
@@ -12,10 +11,10 @@ class HomeScreenSettingsWidget extends StatefulWidget {
}
class _HomeScreenSettingsState extends State<HomeScreenSettingsWidget> {
_HomeScreenSettingsState();
final GlobalKey<_HomeScreenSettingsState> _settingsKey = GlobalKey<_HomeScreenSettingsState>();
final GlobalKey<_HomeScreenSettingsState> _settingsKey =
GlobalKey<_HomeScreenSettingsState>();
// Home screen settings
bool homeShowSubscribed = true;
@@ -32,92 +31,113 @@ class _HomeScreenSettingsState extends State<HomeScreenSettingsWidget> {
loadSettings();
}
Future <void> loadSettings() async {
Future<void> loadSettings() async {
// Load initial settings
homeShowSubscribed = await InvenTreeSettingsManager().getValue(INV_HOME_SHOW_SUBSCRIBED, true) as bool;
homeShowPo = await InvenTreeSettingsManager().getValue(INV_HOME_SHOW_PO, true) as bool;
homeShowSo = await InvenTreeSettingsManager().getValue(INV_HOME_SHOW_SO, true) as bool;
homeShowManufacturers = await InvenTreeSettingsManager().getValue(INV_HOME_SHOW_MANUFACTURERS, true) as bool;
homeShowCustomers = await InvenTreeSettingsManager().getValue(INV_HOME_SHOW_CUSTOMERS, true) as bool;
homeShowSuppliers = await InvenTreeSettingsManager().getValue(INV_HOME_SHOW_SUPPLIERS, true) as bool;
homeShowSubscribed =
await InvenTreeSettingsManager().getValue(
INV_HOME_SHOW_SUBSCRIBED,
true,
)
as bool;
homeShowPo =
await InvenTreeSettingsManager().getValue(INV_HOME_SHOW_PO, true)
as bool;
homeShowSo =
await InvenTreeSettingsManager().getValue(INV_HOME_SHOW_SO, true)
as bool;
homeShowManufacturers =
await InvenTreeSettingsManager().getValue(
INV_HOME_SHOW_MANUFACTURERS,
true,
)
as bool;
homeShowCustomers =
await InvenTreeSettingsManager().getValue(INV_HOME_SHOW_CUSTOMERS, true)
as bool;
homeShowSuppliers =
await InvenTreeSettingsManager().getValue(INV_HOME_SHOW_SUPPLIERS, true)
as bool;
setState(() {
});
setState(() {});
}
@override
Widget build(BuildContext context) {
return Scaffold(
key: _settingsKey,
appBar: AppBar(
title: Text(L10().homeScreen),
backgroundColor: COLOR_APP_BAR,
),
body: Container(
child: ListView(
children: [
ListTile(
title: Text(L10().homeShowSubscribed),
subtitle: Text(L10().homeShowSubscribedDescription),
leading: Icon(TablerIcons.bell),
trailing: Switch(
value: homeShowSubscribed,
onChanged: (bool value) {
InvenTreeSettingsManager().setValue(INV_HOME_SHOW_SUBSCRIBED, value);
setState(() {
homeShowSubscribed = value;
});
},
)
),
ListTile(
title: Text(L10().homeShowPo),
subtitle: Text(L10().homeShowPoDescription),
leading: Icon(TablerIcons.shopping_cart),
trailing: Switch(
value: homeShowPo,
onChanged: (bool value) {
InvenTreeSettingsManager().setValue(INV_HOME_SHOW_PO, value);
setState(() {
homeShowPo = value;
});
},
),
),
ListTile(
title: Text(L10().homeShowSo),
subtitle: Text(L10().homeShowSoDescription),
leading: Icon(TablerIcons.truck),
trailing: Switch(
value: homeShowSo,
onChanged: (bool value) {
InvenTreeSettingsManager().setValue(INV_HOME_SHOW_SO, value);
setState(() {
homeShowSo = value;
});
},
),
),
ListTile(
title: Text(L10().homeShowSuppliers),
subtitle: Text(L10().homeShowSuppliersDescription),
leading: Icon(TablerIcons.building),
trailing: Switch(
value: homeShowSuppliers,
onChanged: (bool value) {
InvenTreeSettingsManager().setValue(INV_HOME_SHOW_SUPPLIERS, value);
setState(() {
homeShowSuppliers = value;
});
},
),
),
// TODO: When these features are improved, add them back in!
// Currently, the company display does not provide any value
/*
key: _settingsKey,
appBar: AppBar(
title: Text(L10().homeScreen),
backgroundColor: COLOR_APP_BAR,
),
body: Container(
child: ListView(
children: [
ListTile(
title: Text(L10().homeShowSubscribed),
subtitle: Text(L10().homeShowSubscribedDescription),
leading: Icon(TablerIcons.bell),
trailing: Switch(
value: homeShowSubscribed,
onChanged: (bool value) {
InvenTreeSettingsManager().setValue(
INV_HOME_SHOW_SUBSCRIBED,
value,
);
setState(() {
homeShowSubscribed = value;
});
},
),
),
ListTile(
title: Text(L10().homeShowPo),
subtitle: Text(L10().homeShowPoDescription),
leading: Icon(TablerIcons.shopping_cart),
trailing: Switch(
value: homeShowPo,
onChanged: (bool value) {
InvenTreeSettingsManager().setValue(INV_HOME_SHOW_PO, value);
setState(() {
homeShowPo = value;
});
},
),
),
ListTile(
title: Text(L10().homeShowSo),
subtitle: Text(L10().homeShowSoDescription),
leading: Icon(TablerIcons.truck),
trailing: Switch(
value: homeShowSo,
onChanged: (bool value) {
InvenTreeSettingsManager().setValue(INV_HOME_SHOW_SO, value);
setState(() {
homeShowSo = value;
});
},
),
),
ListTile(
title: Text(L10().homeShowSuppliers),
subtitle: Text(L10().homeShowSuppliersDescription),
leading: Icon(TablerIcons.building),
trailing: Switch(
value: homeShowSuppliers,
onChanged: (bool value) {
InvenTreeSettingsManager().setValue(
INV_HOME_SHOW_SUPPLIERS,
value,
);
setState(() {
homeShowSuppliers = value;
});
},
),
),
// TODO: When these features are improved, add them back in!
// Currently, the company display does not provide any value
/*
ListTile(
title: Text(L10().homeShowManufacturers),
subtitle: Text(L10().homeShowManufacturersDescription),
@@ -133,23 +153,26 @@ class _HomeScreenSettingsState extends State<HomeScreenSettingsWidget> {
),
),
*/
ListTile(
title: Text(L10().homeShowCustomers),
subtitle: Text(L10().homeShowCustomersDescription),
leading: Icon(TablerIcons.user),
trailing: Switch(
value: homeShowCustomers,
onChanged: (bool value) {
InvenTreeSettingsManager().setValue(INV_HOME_SHOW_CUSTOMERS, value);
setState(() {
homeShowCustomers = value;
});
},
),
),
]
)
)
ListTile(
title: Text(L10().homeShowCustomers),
subtitle: Text(L10().homeShowCustomersDescription),
leading: Icon(TablerIcons.user),
trailing: Switch(
value: homeShowCustomers,
onChanged: (bool value) {
InvenTreeSettingsManager().setValue(
INV_HOME_SHOW_CUSTOMERS,
value,
);
setState(() {
homeShowCustomers = value;
});
},
),
),
],
),
),
);
}
}
}

View File

@@ -1,4 +1,3 @@
import "package:flutter/material.dart";
import "package:flutter_tabler_icons/flutter_tabler_icons.dart";
@@ -9,21 +8,16 @@ import "package:inventree/api.dart";
import "package:inventree/widget/dialogs.dart";
import "package:inventree/widget/progress.dart";
class InvenTreeLoginWidget extends StatefulWidget {
const InvenTreeLoginWidget(this.profile) : super();
final UserProfile profile;
@override
_InvenTreeLoginState createState() => _InvenTreeLoginState();
}
class _InvenTreeLoginState extends State<InvenTreeLoginWidget> {
final formKey = GlobalKey<FormState>();
String username = "";
@@ -35,14 +29,12 @@ class _InvenTreeLoginState extends State<InvenTreeLoginWidget> {
// Attempt login
Future<void> _doLogin(BuildContext context) async {
// Save form
formKey.currentState?.save();
bool valid = formKey.currentState?.validate() ?? false;
if (valid) {
// Dismiss the keyboard
FocusScopeNode currentFocus = FocusScope.of(context);
@@ -53,7 +45,11 @@ class _InvenTreeLoginState extends State<InvenTreeLoginWidget> {
showLoadingOverlay();
// Attempt login
final response = await InvenTreeAPI().fetchToken(widget.profile, username, password);
final response = await InvenTreeAPI().fetchToken(
widget.profile,
username,
password,
);
hideLoadingOverlay();
@@ -75,12 +71,10 @@ class _InvenTreeLoginState extends State<InvenTreeLoginWidget> {
});
}
}
}
@override
Widget build(BuildContext context) {
List<Widget> before = [
ListTile(
title: Text(L10().loginEnter),
@@ -99,11 +93,13 @@ class _InvenTreeLoginState extends State<InvenTreeLoginWidget> {
if (error.isNotEmpty) {
after.add(Divider());
after.add(ListTile(
leading: Icon(TablerIcons.exclamation_circle, color: COLOR_DANGER),
title: Text(L10().error, style: TextStyle(color: COLOR_DANGER)),
subtitle: Text(error, style: TextStyle(color: COLOR_DANGER)),
));
after.add(
ListTile(
leading: Icon(TablerIcons.exclamation_circle, color: COLOR_DANGER),
title: Text(L10().error, style: TextStyle(color: COLOR_DANGER)),
subtitle: Text(error, style: TextStyle(color: COLOR_DANGER)),
),
);
}
return Scaffold(
appBar: AppBar(
@@ -115,8 +111,8 @@ class _InvenTreeLoginState extends State<InvenTreeLoginWidget> {
onPressed: () async {
_doLogin(context);
},
)
]
),
],
),
body: Form(
key: formKey,
@@ -129,9 +125,9 @@ class _InvenTreeLoginState extends State<InvenTreeLoginWidget> {
...before,
TextFormField(
decoration: InputDecoration(
labelText: L10().username,
labelStyle: TextStyle(fontWeight: FontWeight.bold),
hintText: L10().enterUsername
labelText: L10().username,
labelStyle: TextStyle(fontWeight: FontWeight.bold),
hintText: L10().enterUsername,
),
initialValue: "",
keyboardType: TextInputType.text,
@@ -147,41 +143,41 @@ class _InvenTreeLoginState extends State<InvenTreeLoginWidget> {
},
),
TextFormField(
decoration: InputDecoration(
labelText: L10().password,
labelStyle: TextStyle(fontWeight: FontWeight.bold),
hintText: L10().enterPassword,
suffixIcon: IconButton(
icon: _obscured ? Icon(TablerIcons.eye) : Icon(TablerIcons.eye_off),
onPressed: () {
setState(() {
_obscured = !_obscured;
});
},
),
decoration: InputDecoration(
labelText: L10().password,
labelStyle: TextStyle(fontWeight: FontWeight.bold),
hintText: L10().enterPassword,
suffixIcon: IconButton(
icon: _obscured
? Icon(TablerIcons.eye)
: Icon(TablerIcons.eye_off),
onPressed: () {
setState(() {
_obscured = !_obscured;
});
},
),
initialValue: "",
keyboardType: TextInputType.visiblePassword,
obscureText: _obscured,
onSaved: (value) {
password = value?.trim() ?? "";
},
validator: (value) {
if (value == null || value.trim().isEmpty) {
return L10().passwordEmpty;
}
return null;
),
initialValue: "",
keyboardType: TextInputType.visiblePassword,
obscureText: _obscured,
onSaved: (value) {
password = value?.trim() ?? "";
},
validator: (value) {
if (value == null || value.trim().isEmpty) {
return L10().passwordEmpty;
}
return null;
},
),
...after,
],
),
padding: EdgeInsets.all(16),
)
)
),
),
);
}
}
}

View File

@@ -1,4 +1,3 @@
import "package:flutter/material.dart";
import "package:flutter_tabler_icons/flutter_tabler_icons.dart";
@@ -6,15 +5,12 @@ import "package:inventree/l10.dart";
import "package:inventree/app_colors.dart";
import "package:inventree/preferences.dart";
class InvenTreePartSettingsWidget extends StatefulWidget {
@override
_InvenTreePartSettingsState createState() => _InvenTreePartSettingsState();
}
class _InvenTreePartSettingsState extends State<InvenTreePartSettingsWidget> {
_InvenTreePartSettingsState();
bool partShowParameters = true;
@@ -32,16 +28,33 @@ class _InvenTreePartSettingsState extends State<InvenTreePartSettingsWidget> {
}
Future<void> loadSettings() async {
partShowParameters = await InvenTreeSettingsManager().getBool(INV_PART_SHOW_PARAMETERS, true);
partShowBom = await InvenTreeSettingsManager().getBool(INV_PART_SHOW_BOM, true);
partShowPricing = await InvenTreeSettingsManager().getBool(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, false);
partShowParameters = await InvenTreeSettingsManager().getBool(
INV_PART_SHOW_PARAMETERS,
true,
);
partShowBom = await InvenTreeSettingsManager().getBool(
INV_PART_SHOW_BOM,
true,
);
partShowPricing = await InvenTreeSettingsManager().getBool(
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,
false,
);
if (mounted) {
setState(() {
});
setState(() {});
}
}
@@ -50,7 +63,7 @@ class _InvenTreePartSettingsState extends State<InvenTreePartSettingsWidget> {
return Scaffold(
appBar: AppBar(
title: Text(L10().partSettings),
backgroundColor: COLOR_APP_BAR
backgroundColor: COLOR_APP_BAR,
),
body: Container(
child: ListView(
@@ -62,7 +75,10 @@ class _InvenTreePartSettingsState extends State<InvenTreePartSettingsWidget> {
trailing: Switch(
value: partShowParameters,
onChanged: (bool value) {
InvenTreeSettingsManager().setValue(INV_PART_SHOW_PARAMETERS, value);
InvenTreeSettingsManager().setValue(
INV_PART_SHOW_PARAMETERS,
value,
);
setState(() {
partShowParameters = value;
});
@@ -90,7 +106,10 @@ class _InvenTreePartSettingsState extends State<InvenTreePartSettingsWidget> {
trailing: Switch(
value: partShowPricing,
onChanged: (bool value) {
InvenTreeSettingsManager().setValue(INV_PART_SHOW_PRICING, value);
InvenTreeSettingsManager().setValue(
INV_PART_SHOW_PRICING,
value,
);
setState(() {
partShowPricing = value;
});
@@ -105,7 +124,10 @@ class _InvenTreePartSettingsState extends State<InvenTreePartSettingsWidget> {
trailing: Switch(
value: stockShowHistory,
onChanged: (bool value) {
InvenTreeSettingsManager().setValue(INV_STOCK_SHOW_HISTORY, value);
InvenTreeSettingsManager().setValue(
INV_STOCK_SHOW_HISTORY,
value,
);
setState(() {
stockShowHistory = value;
});
@@ -115,11 +137,14 @@ class _InvenTreePartSettingsState extends State<InvenTreePartSettingsWidget> {
ListTile(
title: Text(L10().testResults),
subtitle: Text(L10().testResultsDetail),
leading: Icon(TablerIcons.test_pipe),
leading: Icon(TablerIcons.test_pipe),
trailing: Switch(
value: stockShowTests,
onChanged: (bool value) {
InvenTreeSettingsManager().setValue(INV_STOCK_SHOW_TESTS, value);
InvenTreeSettingsManager().setValue(
INV_STOCK_SHOW_TESTS,
value,
);
setState(() {
stockShowTests = value;
});
@@ -133,16 +158,19 @@ class _InvenTreePartSettingsState extends State<InvenTreePartSettingsWidget> {
trailing: Switch(
value: stockConfirmScan,
onChanged: (bool value) {
InvenTreeSettingsManager().setValue(INV_STOCK_CONFIRM_SCAN, value);
InvenTreeSettingsManager().setValue(
INV_STOCK_CONFIRM_SCAN,
value,
);
setState(() {
stockConfirmScan = value;
});
}
},
),
)
]
)
)
),
],
),
),
);
}
}
}

View File

@@ -1,4 +1,3 @@
import "package:flutter/material.dart";
import "package:flutter_tabler_icons/flutter_tabler_icons.dart";
import "package:inventree/app_colors.dart";
@@ -6,15 +5,14 @@ import "package:inventree/app_colors.dart";
import "package:inventree/l10.dart";
import "package:inventree/preferences.dart";
class InvenTreePurchaseOrderSettingsWidget extends StatefulWidget {
@override
_InvenTreePurchaseOrderSettingsState createState() => _InvenTreePurchaseOrderSettingsState();
_InvenTreePurchaseOrderSettingsState createState() =>
_InvenTreePurchaseOrderSettingsState();
}
class _InvenTreePurchaseOrderSettingsState extends State<InvenTreePurchaseOrderSettingsWidget> {
class _InvenTreePurchaseOrderSettingsState
extends State<InvenTreePurchaseOrderSettingsWidget> {
_InvenTreePurchaseOrderSettingsState();
bool poEnable = true;
@@ -30,70 +28,81 @@ class _InvenTreePurchaseOrderSettingsState extends State<InvenTreePurchaseOrderS
Future<void> loadSettings() async {
poEnable = await InvenTreeSettingsManager().getBool(INV_PO_ENABLE, true);
poShowCamera = await InvenTreeSettingsManager().getBool(INV_PO_SHOW_CAMERA, true);
poConfirmScan = await InvenTreeSettingsManager().getBool(INV_PO_CONFIRM_SCAN, true);
poShowCamera = await InvenTreeSettingsManager().getBool(
INV_PO_SHOW_CAMERA,
true,
);
poConfirmScan = await InvenTreeSettingsManager().getBool(
INV_PO_CONFIRM_SCAN,
true,
);
if (mounted) {
setState(() {
});
setState(() {});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(L10().purchaseOrderSettings),
backgroundColor: COLOR_APP_BAR,
appBar: AppBar(
title: Text(L10().purchaseOrderSettings),
backgroundColor: COLOR_APP_BAR,
),
body: Container(
child: ListView(
children: [
ListTile(
title: Text(L10().purchaseOrderEnable),
subtitle: Text(L10().purchaseOrderEnableDetail),
leading: Icon(TablerIcons.shopping_cart),
trailing: Switch(
value: poEnable,
onChanged: (bool value) {
InvenTreeSettingsManager().setValue(INV_PO_ENABLE, value);
setState(() {
poEnable = value;
});
},
),
),
ListTile(
title: Text(L10().purchaseOrderShowCamera),
subtitle: Text(L10().purchaseOrderShowCameraDetail),
leading: Icon(TablerIcons.camera),
trailing: Switch(
value: poShowCamera,
onChanged: (bool value) {
InvenTreeSettingsManager().setValue(
INV_PO_SHOW_CAMERA,
value,
);
setState(() {
poShowCamera = value;
});
},
),
),
ListTile(
title: Text(L10().purchaseOrderConfirmScan),
subtitle: Text(L10().purchaseOrderConfirmScanDetail),
leading: Icon(TablerIcons.barcode),
trailing: Switch(
value: poConfirmScan,
onChanged: (bool value) {
InvenTreeSettingsManager().setValue(
INV_PO_CONFIRM_SCAN,
value,
);
setState(() {
poConfirmScan = value;
});
},
),
),
],
),
body: Container(
child: ListView(
children: [
ListTile(
title: Text(L10().purchaseOrderEnable),
subtitle: Text(L10().purchaseOrderEnableDetail),
leading: Icon(TablerIcons.shopping_cart),
trailing: Switch(
value: poEnable,
onChanged: (bool value) {
InvenTreeSettingsManager().setValue(INV_PO_ENABLE, value);
setState(() {
poEnable = value;
});
},
),
),
ListTile(
title: Text(L10().purchaseOrderShowCamera),
subtitle: Text(L10().purchaseOrderShowCameraDetail),
leading: Icon(TablerIcons.camera),
trailing: Switch(
value: poShowCamera,
onChanged: (bool value) {
InvenTreeSettingsManager().setValue(INV_PO_SHOW_CAMERA, value);
setState(() {
poShowCamera = value;
});
},
),
),
ListTile(
title: Text(L10().purchaseOrderConfirmScan),
subtitle: Text(L10().purchaseOrderConfirmScanDetail),
leading: Icon(TablerIcons.barcode),
trailing: Switch (
value: poConfirmScan,
onChanged: (bool value) {
InvenTreeSettingsManager().setValue(INV_PO_CONFIRM_SCAN, value);
setState(() {
poConfirmScan = value;
});
},
),
)
]
)
)
),
);
}
}
}

View File

@@ -6,15 +6,13 @@ import "package:url_launcher/url_launcher.dart";
import "package:inventree/l10.dart";
import "package:inventree/helpers.dart";
class ReleaseNotesWidget extends StatelessWidget {
const ReleaseNotesWidget(this.releaseNotes);
final String releaseNotes;
@override
Widget build (BuildContext context) {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(L10().releaseNotes),
@@ -29,21 +27,18 @@ class ReleaseNotesWidget extends StatelessWidget {
openLink(link);
}
},
)
),
);
}
}
class CreditsWidget extends StatelessWidget {
const CreditsWidget(this.credits);
final String credits;
// Callback function when a link is clicked in the markdown
Future<void> openLink(String url) async {
final link = Uri.parse(url);
if (await canLaunchUrl(link)) {
@@ -52,7 +47,7 @@ class CreditsWidget extends StatelessWidget {
}
@override
Widget build (BuildContext context) {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(L10().credits),
@@ -67,7 +62,7 @@ class CreditsWidget extends StatelessWidget {
openLink(link);
}
},
)
),
);
}
}
}

View File

@@ -1,4 +1,3 @@
import "package:flutter/material.dart";
import "package:flutter_tabler_icons/flutter_tabler_icons.dart";
@@ -6,15 +5,14 @@ import "package:inventree/l10.dart";
import "package:inventree/app_colors.dart";
import "package:inventree/preferences.dart";
class InvenTreeSalesOrderSettingsWidget extends StatefulWidget {
@override
_InvenTreeSalesOrderSettingsState createState() => _InvenTreeSalesOrderSettingsState();
_InvenTreeSalesOrderSettingsState createState() =>
_InvenTreeSalesOrderSettingsState();
}
class _InvenTreeSalesOrderSettingsState extends State<InvenTreeSalesOrderSettingsWidget> {
class _InvenTreeSalesOrderSettingsState
extends State<InvenTreeSalesOrderSettingsWidget> {
_InvenTreeSalesOrderSettingsState();
bool soEnable = true;
@@ -29,55 +27,60 @@ class _InvenTreeSalesOrderSettingsState extends State<InvenTreeSalesOrderSetting
Future<void> loadSettings() async {
soEnable = await InvenTreeSettingsManager().getBool(INV_SO_ENABLE, true);
soShowCamera = await InvenTreeSettingsManager().getBool(INV_SO_SHOW_CAMERA, true);
soShowCamera = await InvenTreeSettingsManager().getBool(
INV_SO_SHOW_CAMERA,
true,
);
if (mounted) {
setState(() {
});
setState(() {});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(L10().salesOrderSettings),
backgroundColor: COLOR_APP_BAR,
appBar: AppBar(
title: Text(L10().salesOrderSettings),
backgroundColor: COLOR_APP_BAR,
),
body: Container(
child: ListView(
children: [
ListTile(
title: Text(L10().salesOrderEnable),
subtitle: Text(L10().salesOrderEnableDetail),
leading: Icon(TablerIcons.shopping_cart),
trailing: Switch(
value: soEnable,
onChanged: (bool value) {
InvenTreeSettingsManager().setValue(INV_SO_ENABLE, value);
setState(() {
soEnable = value;
});
},
),
),
ListTile(
title: Text(L10().salesOrderShowCamera),
subtitle: Text(L10().salesOrderShowCameraDetail),
leading: Icon(TablerIcons.camera),
trailing: Switch(
value: soShowCamera,
onChanged: (bool value) {
InvenTreeSettingsManager().setValue(
INV_SO_SHOW_CAMERA,
value,
);
setState(() {
soShowCamera = value;
});
},
),
),
],
),
body: Container(
child: ListView(
children: [
ListTile(
title: Text(L10().salesOrderEnable),
subtitle: Text(L10().salesOrderEnableDetail),
leading: Icon(TablerIcons.shopping_cart),
trailing: Switch(
value: soEnable,
onChanged: (bool value) {
InvenTreeSettingsManager().setValue(INV_SO_ENABLE, value);
setState(() {
soEnable = value;
});
},
),
),
ListTile(
title: Text(L10().salesOrderShowCamera),
subtitle: Text(L10().salesOrderShowCameraDetail),
leading: Icon(TablerIcons.camera),
trailing: Switch(
value: soShowCamera,
onChanged: (bool value) {
InvenTreeSettingsManager().setValue(INV_SO_SHOW_CAMERA, value);
setState(() {
soShowCamera = value;
});
},
),
),
]
)
)
),
);
}
}
}

View File

@@ -12,39 +12,37 @@ import "package:inventree/api.dart";
import "package:inventree/user_profile.dart";
class InvenTreeSelectServerWidget extends StatefulWidget {
@override
_InvenTreeSelectServerState createState() => _InvenTreeSelectServerState();
}
class _InvenTreeSelectServerState extends State<InvenTreeSelectServerWidget> {
_InvenTreeSelectServerState() {
_reload();
}
final GlobalKey<_InvenTreeSelectServerState> _loginKey = GlobalKey<_InvenTreeSelectServerState>();
final GlobalKey<_InvenTreeSelectServerState> _loginKey =
GlobalKey<_InvenTreeSelectServerState>();
List<UserProfile> profiles = [];
Future <void> _reload() async {
Future<void> _reload() async {
profiles = await UserProfileDBManager().getAllProfiles();
if (!mounted) {
return;
}
setState(() {
});
setState(() {});
}
/*
* Logout the selected profile (delete the stored token)
*/
Future<void> _logoutProfile(BuildContext context, {UserProfile? userProfile}) async {
Future<void> _logoutProfile(
BuildContext context, {
UserProfile? userProfile,
}) async {
if (userProfile != null) {
userProfile.token = "";
await UserProfileDBManager().updateProfile(userProfile);
@@ -54,26 +52,25 @@ class _InvenTreeSelectServerState extends State<InvenTreeSelectServerWidget> {
InvenTreeAPI().disconnectFromServer();
_reload();
}
/*
* Edit the selected profile
*/
void _editProfile(BuildContext context, {UserProfile? userProfile, bool createNew = false}) {
void _editProfile(
BuildContext context, {
UserProfile? userProfile,
bool createNew = false,
}) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ProfileEditWidget(userProfile)
)
MaterialPageRoute(builder: (context) => ProfileEditWidget(userProfile)),
).then((context) {
_reload();
});
}
Future <void> _selectProfile(BuildContext context, UserProfile profile) async {
Future<void> _selectProfile(BuildContext context, UserProfile profile) async {
// Disconnect InvenTree
InvenTreeAPI().disconnectFromServer();
@@ -94,8 +91,9 @@ class _InvenTreeSelectServerState extends State<InvenTreeSelectServerWidget> {
// First check if the profile has an associate token
if (!prf.hasToken) {
// Redirect user to login screen
Navigator.push(context,
MaterialPageRoute(builder: (context) => InvenTreeLoginWidget(profile))
Navigator.push(
context,
MaterialPageRoute(builder: (context) => InvenTreeLoginWidget(profile)),
).then((value) async {
_reload();
// Reload profile
@@ -125,8 +123,7 @@ class _InvenTreeSelectServerState extends State<InvenTreeSelectServerWidget> {
_reload();
}
Future <void> _deleteProfile(UserProfile profile) async {
Future<void> _deleteProfile(UserProfile profile) async {
await UserProfileDBManager().deleteProfile(profile);
if (!mounted) {
@@ -135,13 +132,13 @@ class _InvenTreeSelectServerState extends State<InvenTreeSelectServerWidget> {
_reload();
if (InvenTreeAPI().isConnected() && profile.key == (InvenTreeAPI().profile?.key ?? "")) {
if (InvenTreeAPI().isConnected() &&
profile.key == (InvenTreeAPI().profile?.key ?? "")) {
InvenTreeAPI().disconnectFromServer();
}
}
Widget? _getProfileIcon(UserProfile profile) {
// Not selected? No icon for you!
if (!profile.selected) return null;
@@ -152,45 +149,38 @@ class _InvenTreeSelectServerState extends State<InvenTreeSelectServerWidget> {
// Reflect the connection status of the server
if (InvenTreeAPI().isConnected()) {
return Icon(
TablerIcons.circle_check,
color: COLOR_SUCCESS
);
return Icon(TablerIcons.circle_check, color: COLOR_SUCCESS);
} else if (InvenTreeAPI().isConnecting()) {
return Spinner(
icon: TablerIcons.loader_2,
color: COLOR_PROGRESS,
);
return Spinner(icon: TablerIcons.loader_2, color: COLOR_PROGRESS);
} else {
return Icon(
TablerIcons.circle_x,
color: COLOR_DANGER,
);
return Icon(TablerIcons.circle_x, color: COLOR_DANGER);
}
}
@override
Widget build(BuildContext context) {
List<Widget> children = [];
if (profiles.isNotEmpty) {
for (int idx = 0; idx < profiles.length; idx++) {
UserProfile profile = profiles[idx];
children.add(ListTile(
title: Text(
profile.name,
),
tileColor: profile.selected ? Theme.of(context).secondaryHeaderColor : null,
subtitle: Text("${profile.server}"),
leading: profile.hasToken ? Icon(TablerIcons.user_check, color: COLOR_SUCCESS) : Icon(TablerIcons.user_cancel, color: COLOR_WARNING),
trailing: _getProfileIcon(profile),
onTap: () {
_selectProfile(context, profile);
},
onLongPress: () {
OneContext().showDialog(
children.add(
ListTile(
title: Text(profile.name),
tileColor: profile.selected
? Theme.of(context).secondaryHeaderColor
: null,
subtitle: Text("${profile.server}"),
leading: profile.hasToken
? Icon(TablerIcons.user_check, color: COLOR_SUCCESS)
: Icon(TablerIcons.user_cancel, color: COLOR_WARNING),
trailing: _getProfileIcon(profile),
onTap: () {
_selectProfile(context, profile);
},
onLongPress: () {
OneContext().showDialog(
builder: (BuildContext context) {
return SimpleDialog(
title: Text(profile.name),
@@ -204,7 +194,7 @@ class _InvenTreeSelectServerState extends State<InvenTreeSelectServerWidget> {
child: ListTile(
title: Text(L10().profileConnect),
leading: Icon(TablerIcons.server),
)
),
),
SimpleDialogOption(
onPressed: () {
@@ -213,8 +203,8 @@ class _InvenTreeSelectServerState extends State<InvenTreeSelectServerWidget> {
},
child: ListTile(
title: Text(L10().profileEdit),
leading: Icon(TablerIcons.edit)
)
leading: Icon(TablerIcons.edit),
),
),
SimpleDialogOption(
onPressed: () {
@@ -224,7 +214,7 @@ class _InvenTreeSelectServerState extends State<InvenTreeSelectServerWidget> {
child: ListTile(
title: Text(L10().profileLogout),
leading: Icon(TablerIcons.logout),
)
),
),
Divider(),
SimpleDialogOption(
@@ -238,28 +228,28 @@ class _InvenTreeSelectServerState extends State<InvenTreeSelectServerWidget> {
icon: TablerIcons.trash,
onAccept: () {
_deleteProfile(profile);
}
},
);
},
child: ListTile(
title: Text(L10().profileDelete, style: TextStyle(color: Colors.red)),
title: Text(
L10().profileDelete,
style: TextStyle(color: Colors.red),
),
leading: Icon(TablerIcons.trash, color: Colors.red),
)
)
),
),
],
);
}
);
},
));
},
);
},
),
);
}
} else {
// No profile available!
children.add(
ListTile(
title: Text(L10().profileNone),
)
);
children.add(ListTile(title: Text(L10().profileNone)));
}
return Scaffold(
@@ -273,27 +263,25 @@ class _InvenTreeSelectServerState extends State<InvenTreeSelectServerWidget> {
onPressed: () {
_editProfile(context, createNew: true);
},
)
),
],
),
body: Container(
child: ListView(
children: ListTile.divideTiles(
context: context,
tiles: children
tiles: children,
).toList(),
)
),
),
);
}
}
/*
* Widget for editing server details
*/
class ProfileEditWidget extends StatefulWidget {
const ProfileEditWidget(this.profile) : super();
final UserProfile? profile;
@@ -303,7 +291,6 @@ class ProfileEditWidget extends StatefulWidget {
}
class _ProfileEditState extends State<ProfileEditWidget> {
_ProfileEditState() : super();
final formKey = GlobalKey<FormState>();
@@ -316,7 +303,9 @@ class _ProfileEditState extends State<ProfileEditWidget> {
return Scaffold(
appBar: AppBar(
backgroundColor: COLOR_APP_BAR,
title: Text(widget.profile == null ? L10().profileAdd : L10().profileEdit),
title: Text(
widget.profile == null ? L10().profileAdd : L10().profileEdit,
),
actions: [
IconButton(
icon: Icon(TablerIcons.circle_check),
@@ -327,14 +316,10 @@ class _ProfileEditState extends State<ProfileEditWidget> {
UserProfile? prf = widget.profile;
if (prf == null) {
UserProfile profile = UserProfile(
name: name,
server: server,
);
UserProfile profile = UserProfile(name: name, server: server);
await UserProfileDBManager().addProfile(profile);
} else {
prf.name = name;
prf.server = server;
@@ -345,8 +330,8 @@ class _ProfileEditState extends State<ProfileEditWidget> {
Navigator.of(context).pop();
}
},
)
]
),
],
),
body: Form(
key: formKey,
@@ -373,7 +358,7 @@ class _ProfileEditState extends State<ProfileEditWidget> {
}
return null;
}
},
),
TextFormField(
decoration: InputDecoration(
@@ -398,7 +383,8 @@ class _ProfileEditState extends State<ProfileEditWidget> {
return L10().invalidHost;
}
if (!value.startsWith("http:") && !value.startsWith("https:")) {
if (!value.startsWith("http:") &&
!value.startsWith("https:")) {
// return L10().serverStart;
}
@@ -410,7 +396,10 @@ class _ProfileEditState extends State<ProfileEditWidget> {
Uri uri = Uri.parse(value);
if (uri.hasScheme) {
if (!["http", "https"].contains(uri.scheme.toLowerCase())) {
if (![
"http",
"https",
].contains(uri.scheme.toLowerCase())) {
return L10().serverStart;
}
} else {
@@ -422,12 +411,11 @@ class _ProfileEditState extends State<ProfileEditWidget> {
return null;
},
),
]
],
),
padding: EdgeInsets.all(16),
),
)
),
);
}
}
}

View File

@@ -16,15 +16,11 @@ import "package:inventree/settings/sales_order_settings.dart";
// InvenTree settings view
class InvenTreeSettingsWidget extends StatefulWidget {
@override
_InvenTreeSettingsState createState() => _InvenTreeSettingsState();
}
class _InvenTreeSettingsState extends State<InvenTreeSettingsWidget> {
final _scaffoldKey = GlobalKey<ScaffoldState>();
/*
@@ -32,8 +28,10 @@ class _InvenTreeSettingsState extends State<InvenTreeSettingsWidget> {
*/
Future<void> _about() async {
PackageInfo.fromPlatform().then((PackageInfo info) {
Navigator.push(context,
MaterialPageRoute(builder: (context) => InvenTreeAboutWidget(info)));
Navigator.push(
context,
MaterialPageRoute(builder: (context) => InvenTreeAboutWidget(info)),
);
});
}
@@ -48,72 +46,108 @@ class _InvenTreeSettingsState extends State<InvenTreeSettingsWidget> {
body: Center(
child: ListView(
children: [
ListTile(
title: Text(L10().server),
subtitle: Text(L10().configureServer),
leading: Icon(TablerIcons.server, color: COLOR_ACTION),
onTap: () {
Navigator.push(context, MaterialPageRoute(builder: (context) => InvenTreeSelectServerWidget()));
},
),
Divider(),
ListTile(
title: Text(L10().appSettings),
subtitle: Text(L10().appSettingsDetails),
leading: Icon(TablerIcons.settings, color: COLOR_ACTION),
onTap: () {
Navigator.push(context, MaterialPageRoute(builder: (context) => InvenTreeAppSettingsWidget()));
}
),
ListTile(
title: Text(L10().homeScreen),
subtitle: Text(L10().homeScreenSettings),
leading: Icon(TablerIcons.home, color: COLOR_ACTION),
onTap: () {
Navigator.push(context, MaterialPageRoute(builder: (context) => HomeScreenSettingsWidget()));
}
),
ListTile(
title: Text(L10().barcodes),
subtitle: Text(L10().barcodeSettings),
leading: Icon(TablerIcons.barcode, color: COLOR_ACTION),
onTap: () {
Navigator.push(context, MaterialPageRoute(builder: (context) => InvenTreeBarcodeSettingsWidget()));
}
),
ListTile(
title: Text(L10().part),
subtitle: Text(L10().partSettings),
leading: Icon(TablerIcons.box, color: COLOR_ACTION),
onTap: () {
Navigator.push(context, MaterialPageRoute(builder: (context) => InvenTreePartSettingsWidget()));
}
),
ListTile(
title: Text(L10().purchaseOrder),
subtitle: Text(L10().purchaseOrderSettings),
leading: Icon(TablerIcons.shopping_cart, color: COLOR_ACTION),
onTap: () {
Navigator.push(context, MaterialPageRoute(builder: (context) => InvenTreePurchaseOrderSettingsWidget()));
},
),
ListTile(
title: Text(L10().salesOrder),
subtitle: Text(L10().salesOrderSettings),
leading: Icon(TablerIcons.truck, color: COLOR_ACTION),
onTap: () {
Navigator.push(context, MaterialPageRoute(builder: (context) => InvenTreeSalesOrderSettingsWidget()));
},
),
Divider(),
ListTile(
title: Text(L10().about),
leading: Icon(TablerIcons.info_circle, color: COLOR_ACTION),
onTap: _about,
)
]
)
)
ListTile(
title: Text(L10().server),
subtitle: Text(L10().configureServer),
leading: Icon(TablerIcons.server, color: COLOR_ACTION),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => InvenTreeSelectServerWidget(),
),
);
},
),
Divider(),
ListTile(
title: Text(L10().appSettings),
subtitle: Text(L10().appSettingsDetails),
leading: Icon(TablerIcons.settings, color: COLOR_ACTION),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => InvenTreeAppSettingsWidget(),
),
);
},
),
ListTile(
title: Text(L10().homeScreen),
subtitle: Text(L10().homeScreenSettings),
leading: Icon(TablerIcons.home, color: COLOR_ACTION),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => HomeScreenSettingsWidget(),
),
);
},
),
ListTile(
title: Text(L10().barcodes),
subtitle: Text(L10().barcodeSettings),
leading: Icon(TablerIcons.barcode, color: COLOR_ACTION),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => InvenTreeBarcodeSettingsWidget(),
),
);
},
),
ListTile(
title: Text(L10().part),
subtitle: Text(L10().partSettings),
leading: Icon(TablerIcons.box, color: COLOR_ACTION),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => InvenTreePartSettingsWidget(),
),
);
},
),
ListTile(
title: Text(L10().purchaseOrder),
subtitle: Text(L10().purchaseOrderSettings),
leading: Icon(TablerIcons.shopping_cart, color: COLOR_ACTION),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
InvenTreePurchaseOrderSettingsWidget(),
),
);
},
),
ListTile(
title: Text(L10().salesOrder),
subtitle: Text(L10().salesOrderSettings),
leading: Icon(TablerIcons.truck, color: COLOR_ACTION),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => InvenTreeSalesOrderSettingsWidget(),
),
);
},
),
Divider(),
ListTile(
title: Text(L10().about),
leading: Icon(TablerIcons.info_circle, color: COLOR_ACTION),
onTap: _about,
),
],
),
),
);
}
}
}

View File

@@ -1,11 +1,9 @@
import "package:sembast/sembast.dart";
import "package:inventree/helpers.dart";
import "package:inventree/preferences.dart";
class UserProfile {
UserProfile({
this.key,
this.name = "",
@@ -14,7 +12,11 @@ class UserProfile {
this.selected = false,
});
factory UserProfile.fromJson(int key, Map<String, dynamic> json, bool isSelected) => UserProfile(
factory UserProfile.fromJson(
int key,
Map<String, dynamic> json,
bool isSelected,
) => UserProfile(
key: key,
name: (json["name"] ?? "") as String,
server: (json["server"] ?? "") as String,
@@ -58,7 +60,6 @@ class UserProfile {
* Class for storing and managing user (server) profiles
*/
class UserProfileDBManager {
final store = StoreRef("profiles");
Future<Database> get _db async => InvenTreePreferencesDB.instance.database;
@@ -67,7 +68,6 @@ class UserProfileDBManager {
* Check if a profile with the specified name exists in the database
*/
Future<bool> profileNameExists(String name) async {
final profiles = await getAllProfiles();
for (var prf in profiles) {
@@ -84,9 +84,10 @@ class UserProfileDBManager {
* Add a new UserProfile to the profiles database.
*/
Future<bool> addProfile(UserProfile profile) async {
if (profile.name.isEmpty) {
debug("addProfile() : Profile missing required values - not adding to database");
debug(
"addProfile() : Profile missing required values - not adding to database",
);
return false;
}
@@ -113,7 +114,6 @@ class UserProfileDBManager {
* The unique integer <key> is used to determine if the profile already exists.
*/
Future<bool> updateProfile(UserProfile profile) async {
// Prevent invalid profile data from being updated
if (profile.name.isEmpty) {
debug("updateProfile() : Profile missing required values - not updating");
@@ -144,15 +144,15 @@ class UserProfileDBManager {
* The key of the UserProfile should match the "selected" property
*/
Future<UserProfile?> getSelectedProfile() async {
final selected = await store.record("selected").get(await _db);
final profiles = await store.find(await _db);
debug("getSelectedProfile() : ${profiles.length} profiles available - selected = ${selected}");
debug(
"getSelectedProfile() : ${profiles.length} profiles available - selected = ${selected}",
);
for (int idx = 0; idx < profiles.length; idx++) {
if (profiles[idx].key is int && profiles[idx].key == selected) {
return UserProfile.fromJson(
profiles[idx].key! as int,
@@ -169,7 +169,6 @@ class UserProfileDBManager {
* Return all user profile objects
*/
Future<List<UserProfile>> getAllProfiles() async {
final selected = await store.record("selected").get(await _db);
final profiles = await store.find(await _db);
@@ -177,25 +176,26 @@ class UserProfileDBManager {
List<UserProfile> profileList = [];
for (int idx = 0; idx < profiles.length; idx++) {
if (profiles[idx].key is int) {
profileList.add(
UserProfile.fromJson(
profiles[idx].key! as int,
profiles[idx].value! as Map<String, dynamic>,
profiles[idx].key == selected,
)
),
);
}
}
// If there are no available profiles, create a demo profile
if (profileList.isEmpty) {
bool added = await InvenTreeSettingsManager().getBool("demo_profile_added", false);
bool added = await InvenTreeSettingsManager().getBool(
"demo_profile_added",
false,
);
// Don't add a new profile if we have added it previously
if (!added) {
await InvenTreeSettingsManager().setValue("demo_profile_added", true);
UserProfile demoProfile = UserProfile(
@@ -212,7 +212,6 @@ class UserProfileDBManager {
return profileList;
}
/*
* Retrieve a profile by key (or null if no match exists)
*/
@@ -231,7 +230,6 @@ class UserProfileDBManager {
return prf;
}
/*
* Retrieve a profile by name (or null if no match exists)
*/

View File

@@ -1,4 +1,3 @@
import "dart:io";
import "package:flutter/material.dart";
@@ -17,7 +16,6 @@ import "package:inventree/widget/progress.dart";
import "package:inventree/widget/snacks.dart";
import "package:inventree/widget/refreshable_state.dart";
/*
* A generic widget for displaying a list of attachments.
*
@@ -25,8 +23,12 @@ import "package:inventree/widget/refreshable_state.dart";
* we pass a subclassed instance of the InvenTreeAttachment model.
*/
class AttachmentWidget extends StatefulWidget {
const AttachmentWidget(this.attachmentClass, this.modelId, this.imagePrefix, this.hasUploadPermission) : super();
const AttachmentWidget(
this.attachmentClass,
this.modelId,
this.imagePrefix,
this.hasUploadPermission,
) : super();
final InvenTreeAttachment attachmentClass;
final int modelId;
@@ -37,9 +39,7 @@ class AttachmentWidget extends StatefulWidget {
_AttachmentWidgetState createState() => _AttachmentWidgetState();
}
class _AttachmentWidgetState extends RefreshableState<AttachmentWidget> {
_AttachmentWidgetState();
List<InvenTreeAttachment> attachments = [];
@@ -64,7 +64,7 @@ class _AttachmentWidgetState extends RefreshableState<AttachmentWidget> {
refresh(context);
});
});
}
},
),
IconButton(
icon: Icon(TablerIcons.file_upload),
@@ -74,20 +74,19 @@ class _AttachmentWidgetState extends RefreshableState<AttachmentWidget> {
refresh(context);
});
});
}
)
},
),
];
}
Future<void> upload(BuildContext context, File? file) async {
if (file == null) return;
showLoadingOverlay();
final bool result = await widget.attachmentClass.uploadAttachment(
file,
widget.modelId
file,
widget.modelId,
);
hideLoadingOverlay();
@@ -101,35 +100,39 @@ class _AttachmentWidgetState extends RefreshableState<AttachmentWidget> {
refresh(context);
}
Future<void> editAttachment(BuildContext context, InvenTreeAttachment attachment) async
{
attachment.editForm(context, L10().editAttachment).then((result) => {
refresh(context)
});
Future<void> editAttachment(
BuildContext context,
InvenTreeAttachment attachment,
) async {
attachment
.editForm(context, L10().editAttachment)
.then((result) => {refresh(context)});
}
/*
* Delete the specified attachment
*/
Future<void> deleteAttachment(BuildContext context, InvenTreeAttachment attachment) async {
Future<void> deleteAttachment(
BuildContext context,
InvenTreeAttachment attachment,
) async {
final bool result = await attachment.delete();
showSnackIcon(
result ? L10().deleteSuccess : L10().deleteFailed,
success: result
success: result,
);
refresh(context);
}
/*
* Display an option context menu for the selected attachment
*/
Future<void> showOptionsMenu(BuildContext context, InvenTreeAttachment attachment) async {
Future<void> showOptionsMenu(
BuildContext context,
InvenTreeAttachment attachment,
) async {
OneContext().showDialog(
builder: (BuildContext ctx) {
return SimpleDialog(
@@ -144,7 +147,7 @@ class _AttachmentWidgetState extends RefreshableState<AttachmentWidget> {
child: ListTile(
title: Text(L10().edit),
leading: Icon(TablerIcons.edit),
)
),
),
SimpleDialogOption(
onPressed: () async {
@@ -154,29 +157,27 @@ class _AttachmentWidgetState extends RefreshableState<AttachmentWidget> {
child: ListTile(
title: Text(L10().delete),
leading: Icon(TablerIcons.trash, color: COLOR_DANGER),
)
)
]
),
),
],
);
}
},
);
}
@override
Future<void> request(BuildContext context) async {
Map<String, String> filters = {};
if (InvenTreeAPI().supportsModernAttachments) {
filters["model_type"] = widget.attachmentClass.REF_MODEL_TYPE;
filters["model_id"] = widget.modelId.toString();
} else {
filters[widget.attachmentClass.REFERENCE_FIELD] = widget.modelId.toString();
filters[widget.attachmentClass.REFERENCE_FIELD] = widget.modelId
.toString();
}
await widget.attachmentClass.list(
filters: filters
).then((var results) {
await widget.attachmentClass.list(filters: filters).then((var results) {
attachments.clear();
for (var result in results) {
@@ -186,57 +187,58 @@ class _AttachmentWidgetState extends RefreshableState<AttachmentWidget> {
}
});
setState(() {
});
setState(() {});
}
@override
List<Widget> getTiles(BuildContext context) {
List<Widget> tiles = [];
// An "attachment" can either be a file, or a URL
for (var attachment in attachments) {
if (attachment.filename.isNotEmpty) {
tiles.add(ListTile(
title: Text(attachment.filename),
subtitle: Text(attachment.comment),
leading: Icon(attachment.icon, color: COLOR_ACTION),
onTap: () async {
showLoadingOverlay();
await attachment.downloadAttachment();
hideLoadingOverlay();
},
onLongPress: () {
showOptionsMenu(context, attachment);
},
));
}
else if (attachment.link.isNotEmpty) {
tiles.add(ListTile(
title: Text(attachment.link),
subtitle: Text(attachment.comment),
leading: Icon(TablerIcons.link, color: COLOR_ACTION),
onTap: () async {
var uri = Uri.tryParse(attachment.link.trimLeft());
if (uri != null && await canLaunchUrl(uri)) {
await launchUrl(uri);
}
},
onLongPress: () {
showOptionsMenu(context, attachment);
},
));
tiles.add(
ListTile(
title: Text(attachment.filename),
subtitle: Text(attachment.comment),
leading: Icon(attachment.icon, color: COLOR_ACTION),
onTap: () async {
showLoadingOverlay();
await attachment.downloadAttachment();
hideLoadingOverlay();
},
onLongPress: () {
showOptionsMenu(context, attachment);
},
),
);
} else if (attachment.link.isNotEmpty) {
tiles.add(
ListTile(
title: Text(attachment.link),
subtitle: Text(attachment.comment),
leading: Icon(TablerIcons.link, color: COLOR_ACTION),
onTap: () async {
var uri = Uri.tryParse(attachment.link.trimLeft());
if (uri != null && await canLaunchUrl(uri)) {
await launchUrl(uri);
}
},
onLongPress: () {
showOptionsMenu(context, attachment);
},
),
);
}
}
if (tiles.isEmpty) {
tiles.add(ListTile(
leading: Icon(TablerIcons.file_x, color: COLOR_WARNING),
title: Text(L10().attachmentNone),
));
tiles.add(
ListTile(
leading: Icon(TablerIcons.file_x, color: COLOR_WARNING),
title: Text(L10().attachmentNone),
),
);
}
return tiles;

View File

@@ -6,7 +6,6 @@ import "package:flutter/material.dart";
* Long-pressing on this will return the user to the home screen
*/
Widget backButton(BuildContext context, GlobalKey<ScaffoldState> key) {
return GestureDetector(
onLongPress: () {
// Display the menu
@@ -21,4 +20,4 @@ Widget backButton(BuildContext context, GlobalKey<ScaffoldState> key) {
},
),
);
}
}

View File

@@ -16,24 +16,19 @@ import "package:inventree/widget/refreshable_state.dart";
import "package:inventree/widget/snacks.dart";
import "package:inventree/widget/company/supplier_part_list.dart";
/*
* Widget for displaying detail view of a single Company instance
*/
class CompanyDetailWidget extends StatefulWidget {
const CompanyDetailWidget(this.company, {Key? key}) : super(key: key);
final InvenTreeCompany company;
@override
_CompanyDetailState createState() => _CompanyDetailState();
}
class _CompanyDetailState extends RefreshableState<CompanyDetailWidget> {
_CompanyDetailState();
int supplierPartCount = 0;
@@ -61,15 +56,15 @@ class _CompanyDetailState extends RefreshableState<CompanyDetailWidget> {
if (InvenTreeCompany().canEdit) {
actions.add(
IconButton(
icon: Icon(TablerIcons.edit),
tooltip: L10().companyEdit,
onPressed: () {
editCompany(context);
}
)
icon: Icon(TablerIcons.edit),
tooltip: L10().companyEdit,
onPressed: () {
editCompany(context);
},
),
);
}
return actions;
}
@@ -78,23 +73,27 @@ class _CompanyDetailState extends RefreshableState<CompanyDetailWidget> {
List<SpeedDialChild> actions = [];
if (widget.company.isCustomer && InvenTreeSalesOrder().canCreate) {
actions.add(SpeedDialChild(
child: Icon(TablerIcons.truck),
label: L10().salesOrderCreate,
onTap: () async {
_createSalesOrder(context);
}
));
actions.add(
SpeedDialChild(
child: Icon(TablerIcons.truck),
label: L10().salesOrderCreate,
onTap: () async {
_createSalesOrder(context);
},
),
);
}
if (widget.company.isSupplier && InvenTreePurchaseOrder().canCreate) {
actions.add(SpeedDialChild(
child: Icon(TablerIcons.shopping_cart),
label: L10().purchaseOrderCreate,
onTap: () async {
_createPurchaseOrder(context);
}
));
actions.add(
SpeedDialChild(
child: Icon(TablerIcons.shopping_cart),
label: L10().purchaseOrderCreate,
onTap: () async {
_createPurchaseOrder(context);
},
),
);
}
return actions;
@@ -109,17 +108,17 @@ class _CompanyDetailState extends RefreshableState<CompanyDetailWidget> {
fields["customer"]?["value"] = widget.company.pk;
InvenTreeSalesOrder().createForm(
context,
L10().salesOrderCreate,
fields: fields,
onSuccess: (result) async {
Map<String, dynamic> data = result as Map<String, dynamic>;
context,
L10().salesOrderCreate,
fields: fields,
onSuccess: (result) async {
Map<String, dynamic> data = result as Map<String, dynamic>;
if (data.containsKey("pk")) {
var order = InvenTreeSalesOrder.fromJson(data);
order.goToDetailPage(context);
}
if (data.containsKey("pk")) {
var order = InvenTreeSalesOrder.fromJson(data);
order.goToDetailPage(context);
}
},
);
}
@@ -132,17 +131,17 @@ class _CompanyDetailState extends RefreshableState<CompanyDetailWidget> {
fields["supplier"]?["value"] = widget.company.pk;
InvenTreePurchaseOrder().createForm(
context,
L10().purchaseOrderCreate,
fields: fields,
onSuccess: (result) async {
Map<String, dynamic> data = result as Map<String, dynamic>;
context,
L10().purchaseOrderCreate,
fields: fields,
onSuccess: (result) async {
Map<String, dynamic> data = result as Map<String, dynamic>;
if (data.containsKey("pk")) {
var order = InvenTreePurchaseOrder.fromJson(data);
order.goToDetailPage(context);
}
if (data.containsKey("pk")) {
var order = InvenTreePurchaseOrder.fromJson(data);
order.goToDetailPage(context);
}
},
);
}
@@ -156,32 +155,37 @@ class _CompanyDetailState extends RefreshableState<CompanyDetailWidget> {
return;
}
outstandingPurchaseOrders = widget.company.isSupplier ?
await InvenTreePurchaseOrder().count(filters: {
"supplier": widget.company.pk.toString(),
"outstanding": "true"
}) : 0;
outstandingPurchaseOrders = widget.company.isSupplier
? await InvenTreePurchaseOrder().count(
filters: {
"supplier": widget.company.pk.toString(),
"outstanding": "true",
},
)
: 0;
outstandingSalesOrders = widget.company.isCustomer ?
await InvenTreeSalesOrder().count(filters: {
"customer": widget.company.pk.toString(),
"outstanding": "true"
}) : 0;
InvenTreeSupplierPart().count(
filters: {
"supplier": widget.company.pk.toString()
}
).then((value) {
if (mounted) {
setState(() {
supplierPartCount = value;
outstandingSalesOrders = widget.company.isCustomer
? await InvenTreeSalesOrder().count(
filters: {
"customer": widget.company.pk.toString(),
"outstanding": "true",
},
)
: 0;
InvenTreeSupplierPart()
.count(filters: {"supplier": widget.company.pk.toString()})
.then((value) {
if (mounted) {
setState(() {
supplierPartCount = value;
});
}
});
}
});
InvenTreeCompanyAttachment().countAttachments(widget.company.pk)
.then((value) {
InvenTreeCompanyAttachment().countAttachments(widget.company.pk).then((
value,
) {
if (mounted) {
setState(() {
attachmentCount = value;
@@ -190,15 +194,14 @@ class _CompanyDetailState extends RefreshableState<CompanyDetailWidget> {
});
}
Future <void> editCompany(BuildContext context) async {
Future<void> editCompany(BuildContext context) async {
widget.company.editForm(
context,
L10().companyEdit,
onSuccess: (data) async {
refresh(context);
showSnackIcon(L10().companyUpdated, success: true);
}
},
);
}
@@ -207,87 +210,86 @@ class _CompanyDetailState extends RefreshableState<CompanyDetailWidget> {
*/
@override
List<Widget> getTiles(BuildContext context) {
List<Widget> tiles = [];
bool sep = false;
tiles.add(Card(
child: ListTile(
title: Text("${widget.company.name}"),
subtitle: Text("${widget.company.description}"),
leading: InvenTreeAPI().getThumbnail(widget.company.image),
tiles.add(
Card(
child: ListTile(
title: Text("${widget.company.name}"),
subtitle: Text("${widget.company.description}"),
leading: InvenTreeAPI().getThumbnail(widget.company.image),
),
),
));
);
if (!widget.company.active) {
tiles.add(
ListTile(
title: Text(
L10().inactive,
style: TextStyle(
color: COLOR_DANGER
)
),
subtitle: Text(
L10().inactiveCompany,
style: TextStyle(
color: COLOR_DANGER
)
),
leading: Icon(
TablerIcons.exclamation_circle,
color: COLOR_DANGER
),
)
ListTile(
title: Text(L10().inactive, style: TextStyle(color: COLOR_DANGER)),
subtitle: Text(
L10().inactiveCompany,
style: TextStyle(color: COLOR_DANGER),
),
leading: Icon(TablerIcons.exclamation_circle, color: COLOR_DANGER),
),
);
}
if (widget.company.website.isNotEmpty) {
tiles.add(ListTile(
title: Text("${widget.company.website}"),
leading: Icon(TablerIcons.globe, color: COLOR_ACTION),
onTap: () async {
openLink(widget.company.website);
},
));
if (widget.company.website.isNotEmpty) {
tiles.add(
ListTile(
title: Text("${widget.company.website}"),
leading: Icon(TablerIcons.globe, color: COLOR_ACTION),
onTap: () async {
openLink(widget.company.website);
},
),
);
sep = true;
}
sep = true;
}
if (widget.company.email.isNotEmpty) {
tiles.add(ListTile(
title: Text("${widget.company.email}"),
leading: Icon(TablerIcons.at, color: COLOR_ACTION),
onTap: () async {
openLink("mailto:${widget.company.email}");
},
));
if (widget.company.email.isNotEmpty) {
tiles.add(
ListTile(
title: Text("${widget.company.email}"),
leading: Icon(TablerIcons.at, color: COLOR_ACTION),
onTap: () async {
openLink("mailto:${widget.company.email}");
},
),
);
sep = true;
}
sep = true;
}
if (widget.company.phone.isNotEmpty) {
tiles.add(ListTile(
title: Text("${widget.company.phone}"),
leading: Icon(TablerIcons.phone, color: COLOR_ACTION),
onTap: () {
openLink("tel:${widget.company.phone}");
},
));
if (widget.company.phone.isNotEmpty) {
tiles.add(
ListTile(
title: Text("${widget.company.phone}"),
leading: Icon(TablerIcons.phone, color: COLOR_ACTION),
onTap: () {
openLink("tel:${widget.company.phone}");
},
),
);
sep = true;
}
sep = true;
}
// External link
if (widget.company.link.isNotEmpty) {
tiles.add(ListTile(
title: Text("${widget.company.link}"),
leading: Icon(TablerIcons.link, color: COLOR_ACTION),
onTap: () {
widget.company.openLink();
},
));
tiles.add(
ListTile(
title: Text("${widget.company.link}"),
leading: Icon(TablerIcons.link, color: COLOR_ACTION),
onTap: () {
widget.company.openLink();
},
),
);
sep = true;
}
@@ -297,7 +299,6 @@ class _CompanyDetailState extends RefreshableState<CompanyDetailWidget> {
}
if (widget.company.isSupplier) {
if (supplierPartCount > 0) {
tiles.add(
ListTile(
@@ -309,12 +310,12 @@ class _CompanyDetailState extends RefreshableState<CompanyDetailWidget> {
context,
MaterialPageRoute(
builder: (context) => SupplierPartList({
"supplier": widget.company.pk.toString()
})
)
"supplier": widget.company.pk.toString(),
}),
),
);
}
)
},
),
);
}
@@ -328,14 +329,12 @@ class _CompanyDetailState extends RefreshableState<CompanyDetailWidget> {
context,
MaterialPageRoute(
builder: (context) => PurchaseOrderListWidget(
filters: {
"supplier": "${widget.company.pk}"
}
)
)
filters: {"supplier": "${widget.company.pk}"},
),
),
);
}
)
},
),
);
// TODO: Display "supplied parts" count (click through to list of supplier parts)
@@ -365,46 +364,46 @@ class _CompanyDetailState extends RefreshableState<CompanyDetailWidget> {
context,
MaterialPageRoute(
builder: (context) => SalesOrderListWidget(
filters: {
"customer": widget.company.pk.toString()
}
)
)
filters: {"customer": widget.company.pk.toString()},
),
),
);
}
)
},
),
);
}
if (widget.company.notes.isNotEmpty) {
tiles.add(ListTile(
title: Text(L10().notes),
leading: Icon(TablerIcons.note),
onTap: null,
));
tiles.add(
ListTile(
title: Text(L10().notes),
leading: Icon(TablerIcons.note),
onTap: null,
),
);
}
tiles.add(ListTile(
title: Text(L10().attachments),
leading: Icon(TablerIcons.file, color: COLOR_ACTION),
trailing: attachmentCount > 0 ? Text(attachmentCount.toString()) : null,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => AttachmentWidget(
InvenTreeCompanyAttachment(),
widget.company.pk,
widget.company.name,
InvenTreeCompany().canEdit
)
)
);
}
));
tiles.add(
ListTile(
title: Text(L10().attachments),
leading: Icon(TablerIcons.file, color: COLOR_ACTION),
trailing: attachmentCount > 0 ? Text(attachmentCount.toString()) : null,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => AttachmentWidget(
InvenTreeCompanyAttachment(),
widget.company.pk,
widget.company.name,
InvenTreeCompany().canEdit,
),
),
);
},
),
);
return tiles;
}
}
}

View File

@@ -1,4 +1,3 @@
import "package:flutter/material.dart";
import "package:flutter_speed_dial/flutter_speed_dial.dart";
import "package:flutter_tabler_icons/flutter_tabler_icons.dart";
@@ -12,13 +11,12 @@ import "package:inventree/inventree/model.dart";
import "package:inventree/widget/paginator.dart";
import "package:inventree/widget/refreshable_state.dart";
/*
* Widget for displaying a filterable list of Company instances
*/
class CompanyListWidget extends StatefulWidget {
const CompanyListWidget(this.title, this.filters, {Key? key}) : super(key: key);
const CompanyListWidget(this.title, this.filters, {Key? key})
: super(key: key);
final String title;
@@ -28,16 +26,13 @@ class CompanyListWidget extends StatefulWidget {
_CompanyListWidgetState createState() => _CompanyListWidgetState();
}
class _CompanyListWidgetState extends RefreshableState<CompanyListWidget> {
_CompanyListWidgetState();
@override
String getAppBarTitle() => widget.title;
Future<void> _addCompany(BuildContext context) async {
InvenTreeCompany().createForm(
context,
L10().companyAdd,
@@ -49,7 +44,7 @@ class _CompanyListWidgetState extends RefreshableState<CompanyListWidget> {
var company = InvenTreeCompany.fromJson(data);
company.goToDetailPage(context);
}
}
},
);
}
@@ -59,13 +54,13 @@ class _CompanyListWidgetState extends RefreshableState<CompanyListWidget> {
if (InvenTreeAPI().checkPermission("company", "add")) {
actions.add(
SpeedDialChild(
child: Icon(TablerIcons.circle_plus, color: Colors.green),
label: L10().companyAdd,
onTap: () {
_addCompany(context);
}
)
SpeedDialChild(
child: Icon(TablerIcons.circle_plus, color: Colors.green),
label: L10().companyAdd,
onTap: () {
_addCompany(context);
},
),
);
}
@@ -76,12 +71,11 @@ class _CompanyListWidgetState extends RefreshableState<CompanyListWidget> {
Widget getBody(BuildContext context) {
return PaginatedCompanyList(widget.title, widget.filters);
}
}
class PaginatedCompanyList extends PaginatedSearchWidget {
const PaginatedCompanyList(this.companyTitle, Map<String, String> filters) : super(filters: filters);
const PaginatedCompanyList(this.companyTitle, Map<String, String> filters)
: super(filters: filters);
final String companyTitle;
@@ -93,12 +87,10 @@ class PaginatedCompanyList extends PaginatedSearchWidget {
}
class _CompanyListState extends PaginatedSearchState<PaginatedCompanyList> {
_CompanyListState() : super();
@override
Map<String, Map<String, dynamic>> get filterOptions {
Map<String, Map<String, dynamic>> filters = {};
if (InvenTreeAPI().supportsCompanyActiveStatus) {
@@ -113,16 +105,22 @@ class _CompanyListState extends PaginatedSearchState<PaginatedCompanyList> {
}
@override
Future<InvenTreePageResponse?> requestPage(int limit, int offset, Map<String, String> params) async {
final page = await InvenTreeCompany().listPaginated(limit, offset, filters: params);
Future<InvenTreePageResponse?> requestPage(
int limit,
int offset,
Map<String, String> params,
) async {
final page = await InvenTreeCompany().listPaginated(
limit,
offset,
filters: params,
);
return page;
}
@override
Widget buildItem(BuildContext context, InvenTreeModel model) {
InvenTreeCompany company = model as InvenTreeCompany;
return ListTile(

View File

@@ -1,4 +1,3 @@
import "package:flutter/material.dart";
import "package:flutter_speed_dial/flutter_speed_dial.dart";
import "package:flutter_tabler_icons/flutter_tabler_icons.dart";
@@ -19,19 +18,18 @@ import "package:url_launcher/url_launcher.dart";
* Detail widget for viewing a single ManufacturerPart instance
*/
class ManufacturerPartDetailWidget extends StatefulWidget {
const ManufacturerPartDetailWidget(this.manufacturerPart, {Key? key})
: super(key: key);
: super(key: key);
final InvenTreeManufacturerPart manufacturerPart;
@override
_ManufacturerPartDisplayState createState() => _ManufacturerPartDisplayState();
_ManufacturerPartDisplayState createState() =>
_ManufacturerPartDisplayState();
}
class _ManufacturerPartDisplayState extends RefreshableState<ManufacturerPartDetailWidget> {
class _ManufacturerPartDisplayState
extends RefreshableState<ManufacturerPartDetailWidget> {
_ManufacturerPartDisplayState();
@override
@@ -39,7 +37,8 @@ class _ManufacturerPartDisplayState extends RefreshableState<ManufacturerPartDet
@override
Future<void> request(BuildContext context) async {
final bool result = widget.manufacturerPart.pk > 0 &&
final bool result =
widget.manufacturerPart.pk > 0 &&
await widget.manufacturerPart.reload();
if (!result) {
@@ -49,12 +48,12 @@ class _ManufacturerPartDisplayState extends RefreshableState<ManufacturerPartDet
Future<void> editManufacturerPart(BuildContext context) async {
widget.manufacturerPart.editForm(
context,
L10().manufacturerPartEdit,
onSuccess: (data) async {
refresh(context);
showSnackIcon(L10().itemUpdated, success: true);
}
context,
L10().manufacturerPartEdit,
onSuccess: (data) async {
refresh(context);
showSnackIcon(L10().itemUpdated, success: true);
},
);
}
@@ -73,13 +72,13 @@ class _ManufacturerPartDisplayState extends RefreshableState<ManufacturerPartDet
if (widget.manufacturerPart.canEdit) {
actions.add(
IconButton(
icon: Icon(TablerIcons.edit),
tooltip: L10().edit,
onPressed: () {
editManufacturerPart(context);
}
)
IconButton(
icon: Icon(TablerIcons.edit),
tooltip: L10().edit,
onPressed: () {
editManufacturerPart(context);
},
),
);
}
@@ -100,78 +99,85 @@ class _ManufacturerPartDisplayState extends RefreshableState<ManufacturerPartDet
// Internal Part
tiles.add(
ListTile(
title: Text(L10().internalPart),
subtitle: Text(widget.manufacturerPart.partName),
leading: Icon(TablerIcons.box, color: COLOR_ACTION),
trailing: InvenTreeAPI().getThumbnail(widget.manufacturerPart.partImage),
onTap: () async {
showLoadingOverlay();
final part = await InvenTreePart().get(widget.manufacturerPart.partId);
hideLoadingOverlay();
ListTile(
title: Text(L10().internalPart),
subtitle: Text(widget.manufacturerPart.partName),
leading: Icon(TablerIcons.box, color: COLOR_ACTION),
trailing: InvenTreeAPI().getThumbnail(
widget.manufacturerPart.partImage,
),
onTap: () async {
showLoadingOverlay();
final part = await InvenTreePart().get(
widget.manufacturerPart.partId,
);
hideLoadingOverlay();
if (part is InvenTreePart) {
part.goToDetailPage(context);
}
},
)
if (part is InvenTreePart) {
part.goToDetailPage(context);
}
},
),
);
// Manufacturer details
tiles.add(
ListTile(
title: Text(L10().manufacturer),
subtitle: Text(widget.manufacturerPart.manufacturerName),
leading: Icon(TablerIcons.building_factory_2, color: COLOR_ACTION),
trailing: InvenTreeAPI().getThumbnail(widget.manufacturerPart.manufacturerImage),
onTap: () async {
showLoadingOverlay();
var supplier = await InvenTreeCompany().get(widget.manufacturerPart.manufacturerId);
hideLoadingOverlay();
ListTile(
title: Text(L10().manufacturer),
subtitle: Text(widget.manufacturerPart.manufacturerName),
leading: Icon(TablerIcons.building_factory_2, color: COLOR_ACTION),
trailing: InvenTreeAPI().getThumbnail(
widget.manufacturerPart.manufacturerImage,
),
onTap: () async {
showLoadingOverlay();
var supplier = await InvenTreeCompany().get(
widget.manufacturerPart.manufacturerId,
);
hideLoadingOverlay();
if (supplier is InvenTreeCompany) {
supplier.goToDetailPage(context);
}
}
)
if (supplier is InvenTreeCompany) {
supplier.goToDetailPage(context);
}
},
),
);
// MPN (part number)
tiles.add(
ListTile(
title: Text(L10().manufacturerPartNumber),
subtitle: Text(widget.manufacturerPart.MPN),
leading: Icon(TablerIcons.hash),
)
ListTile(
title: Text(L10().manufacturerPartNumber),
subtitle: Text(widget.manufacturerPart.MPN),
leading: Icon(TablerIcons.hash),
),
);
// Description
if (widget.manufacturerPart.description.isNotEmpty) {
tiles.add(
ListTile(
title: Text(L10().description),
subtitle: Text(widget.manufacturerPart.description),
leading: Icon(TablerIcons.info_circle),
)
ListTile(
title: Text(L10().description),
subtitle: Text(widget.manufacturerPart.description),
leading: Icon(TablerIcons.info_circle),
),
);
}
if (widget.manufacturerPart.link.isNotEmpty) {
tiles.add(
ListTile(
title: Text(widget.manufacturerPart.link),
leading: Icon(TablerIcons.link, color: COLOR_ACTION),
onTap: () async {
var uri = Uri.tryParse(widget.manufacturerPart.link);
if (uri != null && await canLaunchUrl(uri)) {
await launchUrl(uri);
}
},
)
ListTile(
title: Text(widget.manufacturerPart.link),
leading: Icon(TablerIcons.link, color: COLOR_ACTION),
onTap: () async {
var uri = Uri.tryParse(widget.manufacturerPart.link);
if (uri != null && await canLaunchUrl(uri)) {
await launchUrl(uri);
}
},
),
);
}
return tiles;
}
}

View File

@@ -17,13 +17,12 @@ import "package:inventree/widget/refreshable_state.dart";
import "package:inventree/widget/snacks.dart";
import "package:inventree/widget/company/manufacturer_part_detail.dart";
/*
* Detail widget for viewing a single SupplierPart instance
*/
class SupplierPartDetailWidget extends StatefulWidget {
const SupplierPartDetailWidget(this.supplierPart, {Key? key}) : super(key: key);
const SupplierPartDetailWidget(this.supplierPart, {Key? key})
: super(key: key);
final InvenTreeSupplierPart supplierPart;
@@ -31,9 +30,8 @@ class SupplierPartDetailWidget extends StatefulWidget {
_SupplierPartDisplayState createState() => _SupplierPartDisplayState();
}
class _SupplierPartDisplayState extends RefreshableState<SupplierPartDetailWidget> {
class _SupplierPartDisplayState
extends RefreshableState<SupplierPartDetailWidget> {
_SupplierPartDisplayState();
@override
@@ -44,12 +42,12 @@ class _SupplierPartDisplayState extends RefreshableState<SupplierPartDetailWidge
*/
Future<void> editSupplierPart(BuildContext context) async {
widget.supplierPart.editForm(
context,
L10().supplierPartEdit,
onSuccess: (data) async {
refresh(context);
showSnackIcon(L10().supplierPartUpdated, success: true);
}
context,
L10().supplierPartEdit,
onSuccess: (data) async {
refresh(context);
showSnackIcon(L10().supplierPartUpdated, success: true);
},
);
}
@@ -60,11 +58,12 @@ class _SupplierPartDisplayState extends RefreshableState<SupplierPartDetailWidge
if (widget.supplierPart.canEdit) {
actions.add(
customBarcodeAction(
context, this,
context,
this,
widget.supplierPart.customBarcode,
"supplierpart",
widget.supplierPart.pk
)
widget.supplierPart.pk,
),
);
}
@@ -77,13 +76,13 @@ class _SupplierPartDisplayState extends RefreshableState<SupplierPartDetailWidge
if (widget.supplierPart.canEdit) {
actions.add(
IconButton(
icon: Icon(TablerIcons.edit),
tooltip: L10().edit,
onPressed: () {
editSupplierPart(context);
}
)
IconButton(
icon: Icon(TablerIcons.edit),
tooltip: L10().edit,
onPressed: () {
editSupplierPart(context);
},
),
);
}
@@ -92,7 +91,8 @@ class _SupplierPartDisplayState extends RefreshableState<SupplierPartDetailWidge
@override
Future<void> request(BuildContext context) async {
final bool result = widget.supplierPart.pk > 0 && await widget.supplierPart.reload();
final bool result =
widget.supplierPart.pk > 0 && await widget.supplierPart.reload();
if (!result) {
Navigator.of(context).pop();
@@ -113,43 +113,33 @@ class _SupplierPartDisplayState extends RefreshableState<SupplierPartDetailWidge
// Internal Part
tiles.add(
ListTile(
title: Text(L10().internalPart),
subtitle: Text(widget.supplierPart.partName),
leading: Icon(TablerIcons.box, color: COLOR_ACTION),
trailing: InvenTreeAPI().getThumbnail(widget.supplierPart.partImage),
onTap: () async {
showLoadingOverlay();
final part = await InvenTreePart().get(widget.supplierPart.partId);
hideLoadingOverlay();
ListTile(
title: Text(L10().internalPart),
subtitle: Text(widget.supplierPart.partName),
leading: Icon(TablerIcons.box, color: COLOR_ACTION),
trailing: InvenTreeAPI().getThumbnail(widget.supplierPart.partImage),
onTap: () async {
showLoadingOverlay();
final part = await InvenTreePart().get(widget.supplierPart.partId);
hideLoadingOverlay();
if (part is InvenTreePart) {
part.goToDetailPage(context);
}
},
)
if (part is InvenTreePart) {
part.goToDetailPage(context);
}
},
),
);
if (!widget.supplierPart.active) {
tiles.add(
ListTile(
title: Text(
L10().inactive,
style: TextStyle(
color: COLOR_DANGER
)
),
subtitle: Text(
L10().inactiveDetail,
style: TextStyle(
color: COLOR_DANGER
)
),
leading: Icon(
TablerIcons.exclamation_circle,
color: COLOR_DANGER
),
)
ListTile(
title: Text(L10().inactive, style: TextStyle(color: COLOR_DANGER)),
subtitle: Text(
L10().inactiveDetail,
style: TextStyle(color: COLOR_DANGER),
),
leading: Icon(TablerIcons.exclamation_circle, color: COLOR_DANGER),
),
);
}
@@ -159,26 +149,30 @@ class _SupplierPartDisplayState extends RefreshableState<SupplierPartDetailWidge
title: Text(L10().supplier),
subtitle: Text(widget.supplierPart.supplierName),
leading: Icon(TablerIcons.building, color: COLOR_ACTION),
trailing: InvenTreeAPI().getThumbnail(widget.supplierPart.supplierImage),
trailing: InvenTreeAPI().getThumbnail(
widget.supplierPart.supplierImage,
),
onTap: () async {
showLoadingOverlay();
var supplier = await InvenTreeCompany().get(widget.supplierPart.supplierId);
var supplier = await InvenTreeCompany().get(
widget.supplierPart.supplierId,
);
hideLoadingOverlay();
if (supplier is InvenTreeCompany) {
supplier.goToDetailPage(context);
}
}
)
},
),
);
// SKU (part number)
tiles.add(
ListTile(
title: Text(L10().supplierPartNumber),
subtitle: Text(widget.supplierPart.SKU),
leading: Icon(TablerIcons.hash),
)
ListTile(
title: Text(L10().supplierPartNumber),
subtitle: Text(widget.supplierPart.SKU),
leading: Icon(TablerIcons.hash),
),
);
// Manufacturer information
@@ -188,17 +182,21 @@ class _SupplierPartDisplayState extends RefreshableState<SupplierPartDetailWidge
title: Text(L10().manufacturer),
subtitle: Text(widget.supplierPart.manufacturerName),
leading: Icon(TablerIcons.building_factory_2, color: COLOR_ACTION),
trailing: InvenTreeAPI().getThumbnail(widget.supplierPart.manufacturerImage),
trailing: InvenTreeAPI().getThumbnail(
widget.supplierPart.manufacturerImage,
),
onTap: () async {
showLoadingOverlay();
var supplier = await InvenTreeCompany().get(widget.supplierPart.manufacturerId);
var supplier = await InvenTreeCompany().get(
widget.supplierPart.manufacturerId,
);
hideLoadingOverlay();
if (supplier is InvenTreeCompany) {
supplier.goToDetailPage(context);
}
}
)
},
),
);
tiles.add(
@@ -208,28 +206,39 @@ class _SupplierPartDisplayState extends RefreshableState<SupplierPartDetailWidge
leading: Icon(TablerIcons.hash, color: COLOR_ACTION),
onTap: () async {
showLoadingOverlay();
var manufacturerPart = await InvenTreeManufacturerPart().get(widget.supplierPart.manufacturerPartId);
var manufacturerPart = await InvenTreeManufacturerPart().get(
widget.supplierPart.manufacturerPartId,
);
hideLoadingOverlay();
if (manufacturerPart is InvenTreeManufacturerPart) {
Navigator.push(context, MaterialPageRoute(
builder: (context) => ManufacturerPartDetailWidget(manufacturerPart)
));
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
ManufacturerPartDetailWidget(manufacturerPart),
),
);
}
},
)
),
);
}
// Packaging
if (widget.supplierPart.packaging.isNotEmpty || widget.supplierPart.pack_quantity.isNotEmpty) {
if (widget.supplierPart.packaging.isNotEmpty ||
widget.supplierPart.pack_quantity.isNotEmpty) {
tiles.add(
ListTile(
title: Text(L10().packaging),
subtitle: widget.supplierPart.packaging.isNotEmpty ? Text(widget.supplierPart.packaging) : null,
subtitle: widget.supplierPart.packaging.isNotEmpty
? Text(widget.supplierPart.packaging)
: null,
leading: Icon(TablerIcons.package),
trailing: widget.supplierPart.pack_quantity.isNotEmpty ? Text(widget.supplierPart.pack_quantity) : null,
)
trailing: widget.supplierPart.pack_quantity.isNotEmpty
? Text(widget.supplierPart.pack_quantity)
: null,
),
);
}
@@ -244,7 +253,7 @@ class _SupplierPartDisplayState extends RefreshableState<SupplierPartDetailWidge
await launchUrl(uri);
}
},
)
),
);
}
@@ -253,11 +262,10 @@ class _SupplierPartDisplayState extends RefreshableState<SupplierPartDetailWidge
ListTile(
title: Text(widget.supplierPart.note),
leading: Icon(TablerIcons.pencil),
)
),
);
}
return tiles;
}
}
}

View File

@@ -10,12 +10,10 @@ import "package:inventree/widget/paginator.dart";
import "package:inventree/widget/refreshable_state.dart";
import "package:inventree/widget/company/supplier_part_detail.dart";
/*
* Widget for displaying a list of Supplier Part instances
*/
class SupplierPartList extends StatefulWidget {
const SupplierPartList(this.filters);
final Map<String, String> filters;
@@ -24,9 +22,7 @@ class SupplierPartList extends StatefulWidget {
_SupplierPartListState createState() => _SupplierPartListState();
}
class _SupplierPartListState extends RefreshableState<SupplierPartList> {
@override
String getAppBarTitle() => L10().supplierParts;
@@ -34,25 +30,22 @@ class _SupplierPartListState extends RefreshableState<SupplierPartList> {
Widget getBody(BuildContext context) {
return PaginatedSupplierPartList(widget.filters);
}
}
class PaginatedSupplierPartList extends PaginatedSearchWidget {
const PaginatedSupplierPartList(Map<String, String> filters) : super(filters: filters);
const PaginatedSupplierPartList(Map<String, String> filters)
: super(filters: filters);
@override
String get searchTitle => L10().supplierParts;
@override
_PaginatedSupplierPartListState createState() => _PaginatedSupplierPartListState();
_PaginatedSupplierPartListState createState() =>
_PaginatedSupplierPartListState();
}
class _PaginatedSupplierPartListState extends PaginatedSearchState<PaginatedSupplierPartList> {
class _PaginatedSupplierPartListState
extends PaginatedSearchState<PaginatedSupplierPartList> {
_PaginatedSupplierPartListState() : super();
@override
@@ -63,7 +56,6 @@ class _PaginatedSupplierPartListState extends PaginatedSearchState<PaginatedSupp
@override
Map<String, Map<String, dynamic>> get filterOptions {
Map<String, Map<String, dynamic>> filters = {};
if (InvenTreeAPI().supportsCompanyActiveStatus) {
@@ -78,8 +70,16 @@ class _PaginatedSupplierPartListState extends PaginatedSearchState<PaginatedSupp
}
@override
Future<InvenTreePageResponse?> requestPage(int limit, int offset, Map<String, String> params) async {
final page = await InvenTreeSupplierPart().listPaginated(limit, offset, filters: params);
Future<InvenTreePageResponse?> requestPage(
int limit,
int offset,
Map<String, String> params,
) async {
final page = await InvenTreeSupplierPart().listPaginated(
limit,
offset,
filters: params,
);
return page;
}
@@ -96,10 +96,10 @@ class _PaginatedSupplierPartListState extends PaginatedSearchState<PaginatedSupp
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SupplierPartDetailWidget(supplierPart)
)
builder: (context) => SupplierPartDetailWidget(supplierPart),
),
);
},
);
}
}
}

View File

@@ -9,12 +9,14 @@ import "package:inventree/l10.dart";
import "package:inventree/preferences.dart";
import "package:inventree/widget/snacks.dart";
/*
* Launch a dialog allowing the user to select from a list of options
*/
Future<void> choiceDialog(String title, List<Widget> items, {Function? onSelected}) async {
Future<void> choiceDialog(
String title,
List<Widget> items, {
Function? onSelected,
}) async {
List<Widget> choices = [];
for (int idx = 0; idx < items.length; idx++) {
@@ -27,7 +29,7 @@ Future<void> choiceDialog(String title, List<Widget> items, {Function? onSelecte
onSelected(idx);
}
},
)
),
);
}
@@ -39,31 +41,33 @@ Future<void> choiceDialog(String title, List<Widget> items, {Function? onSelecte
builder: (BuildContext context) {
return AlertDialog(
title: Text(title),
content: SingleChildScrollView(
child: Column(
children: choices,
)
),
content: SingleChildScrollView(child: Column(children: choices)),
actions: [
TextButton(
child: Text(L10().cancel),
onPressed: () {
Navigator.pop(context);
},
)
),
],
);
}
},
);
}
/*
* Display a "confirmation" dialog allowing the user to accept or reject an action
*/
Future<void> confirmationDialog(String title, String text, {Color? color, IconData icon = TablerIcons.help_circle, String? acceptText, String? rejectText, Function? onAccept, Function? onReject}) async {
Future<void> confirmationDialog(
String title,
String text, {
Color? color,
IconData icon = TablerIcons.help_circle,
String? acceptText,
String? rejectText,
Function? onAccept,
Function? onReject,
}) async {
String _accept = acceptText ?? L10().ok;
String _reject = rejectText ?? L10().cancel;
@@ -90,7 +94,7 @@ Future<void> confirmationDialog(String title, String text, {Color? color, IconDa
if (onReject != null) {
onReject();
}
}
},
),
TextButton(
child: Text(_accept),
@@ -102,14 +106,13 @@ Future<void> confirmationDialog(String title, String text, {Color? color, IconDa
onAccept();
}
},
)
]
),
],
);
}
},
);
}
/*
* Construct an error dialog showing information to the user
*
@@ -117,24 +120,23 @@ Future<void> confirmationDialog(String title, String text, {Color? color, IconDa
* @description = Simple string description of error
* @data = Error response (e.g from server)
*/
Future<void> showErrorDialog(String title, {String description = "", APIResponse? response, IconData icon = TablerIcons.exclamation_circle, Function? onDismissed}) async {
Future<void> showErrorDialog(
String title, {
String description = "",
APIResponse? response,
IconData icon = TablerIcons.exclamation_circle,
Function? onDismissed,
}) async {
List<Widget> children = [];
if (description.isNotEmpty) {
children.add(
ListTile(
title: Text(description),
)
);
children.add(ListTile(title: Text(description)));
} else if (response != null) {
// Look for extra error information in the provided APIResponse object
switch (response.statusCode) {
case 400: // Bad request (typically bad input)
case 400: // Bad request (typically bad input)
if (response.data is Map<String, dynamic>) {
for (String field in response.asMap().keys) {
dynamic error = response.data[field];
if (error is List) {
@@ -143,15 +145,15 @@ Future<void> showErrorDialog(String title, {String description = "", APIResponse
ListTile(
title: Text(field),
subtitle: Text(error[ii].toString()),
)
),
);
}
} else {
children.add(
ListTile(
title: Text(field),
subtitle: Text(response.data[field].toString()),
)
ListTile(
title: Text(field),
subtitle: Text(response.data[field].toString()),
),
);
}
}
@@ -159,8 +161,8 @@ Future<void> showErrorDialog(String title, {String description = "", APIResponse
children.add(
ListTile(
title: Text(L10().responseInvalid),
subtitle: Text(response.data.toString())
)
subtitle: Text(response.data.toString()),
),
);
}
default:
@@ -169,16 +171,15 @@ Future<void> showErrorDialog(String title, {String description = "", APIResponse
ListTile(
title: Text(L10().statusCode),
subtitle: Text(response.statusCode.toString()),
)
),
);
children.add(
ListTile(
title: Text(L10().responseData),
subtitle: Text(response.data.toString()),
)
),
);
}
}
@@ -186,26 +187,28 @@ Future<void> showErrorDialog(String title, {String description = "", APIResponse
return;
}
OneContext().showDialog(
builder: (context) => SimpleDialog(
title: ListTile(
title: Text(title),
leading: Icon(icon),
),
children: children
)
).then((value) {
if (onDismissed != null) {
onDismissed();
}
});
OneContext()
.showDialog(
builder: (context) => SimpleDialog(
title: ListTile(title: Text(title), leading: Icon(icon)),
children: children,
),
)
.then((value) {
if (onDismissed != null) {
onDismissed();
}
});
}
/*
* Display a message indicating the nature of a server / API error
*/
Future<void> showServerError(String url, String title, String description) async {
Future<void> showServerError(
String url,
String title,
String description,
) async {
if (!hasContext()) {
return;
}
@@ -220,7 +223,9 @@ Future<void> showServerError(String url, String title, String description) async
}
// Play a sound
final bool tones = await InvenTreeSettingsManager().getValue(INV_SOUNDS_SERVER, true) as bool;
final bool tones =
await InvenTreeSettingsManager().getValue(INV_SOUNDS_SERVER, true)
as bool;
if (tones) {
playAudioFile("sounds/server_error.mp3");
@@ -234,19 +239,22 @@ Future<void> showServerError(String url, String title, String description) async
actionText: L10().details,
onAction: () {
showErrorDialog(
title,
description: description,
icon: TablerIcons.server
title,
description: description,
icon: TablerIcons.server,
);
}
},
);
}
/*
* Displays an error indicating that the server returned an unexpected status code
*/
Future<void> showStatusCodeError(String url, int status, {String details=""}) async {
Future<void> showStatusCodeError(
String url,
int status, {
String details = "",
}) async {
String msg = statusCodeToString(status);
String extra = url + "\n" + "${L10().statusCode}: ${status}";
@@ -255,14 +263,9 @@ Future<void> showStatusCodeError(String url, int status, {String details=""}) as
extra += details;
}
showServerError(
url,
msg,
extra,
);
showServerError(url, msg, extra);
}
/*
* Provide a human-readable descriptor for a particular error code
*/
@@ -297,7 +300,6 @@ String statusCodeToString(int status) {
}
}
/*
* Displays a message indicating that the server timed out on a certain request
*/

View File

@@ -16,12 +16,10 @@ import "package:inventree/widget/notifications.dart";
import "package:inventree/widget/order/purchase_order_list.dart";
import "package:inventree/widget/stock/location_display.dart";
/*
* Custom "drawer" widget for the InvenTree app.
*/
class InvenTreeDrawer extends StatelessWidget {
const InvenTreeDrawer(this.context);
final BuildContext context;
@@ -53,8 +51,8 @@ class InvenTreeDrawer extends StatelessWidget {
if (_checkConnection()) {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => CategoryDisplayWidget(null))
context,
MaterialPageRoute(builder: (context) => CategoryDisplayWidget(null)),
);
}
}
@@ -65,8 +63,8 @@ class InvenTreeDrawer extends StatelessWidget {
if (_checkConnection()) {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => LocationDisplayWidget(null))
context,
MaterialPageRoute(builder: (context) => LocationDisplayWidget(null)),
);
}
}
@@ -77,24 +75,24 @@ class InvenTreeDrawer extends StatelessWidget {
if (_checkConnection()) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SalesOrderListWidget(filters: {})
)
context,
MaterialPageRoute(
builder: (context) => SalesOrderListWidget(filters: {}),
),
);
}
}
// Load "purchase orders" page
void _purchaseOrders() {
_closeDrawer();
if (_checkConnection()) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => PurchaseOrderListWidget(filters: {})
)
context,
MaterialPageRoute(
builder: (context) => PurchaseOrderListWidget(filters: {}),
),
);
}
}
@@ -104,15 +102,20 @@ class InvenTreeDrawer extends StatelessWidget {
_closeDrawer();
if (_checkConnection()) {
Navigator.push(context,
MaterialPageRoute(builder: (context) => NotificationWidget()));
Navigator.push(
context,
MaterialPageRoute(builder: (context) => NotificationWidget()),
);
}
}
// Load settings widget
void _settings() {
_closeDrawer();
Navigator.push(context, MaterialPageRoute(builder: (context) => InvenTreeSettingsWidget()));
Navigator.push(
context,
MaterialPageRoute(builder: (context) => InvenTreeSettingsWidget()),
);
}
// Construct list of tiles to display in the "drawer" menu
@@ -120,14 +123,16 @@ class InvenTreeDrawer extends StatelessWidget {
List<Widget> tiles = [];
// "Home" access
tiles.add(ListTile(
leading: Icon(TablerIcons.home, color: COLOR_ACTION),
title: Text(
L10().appTitle,
style: TextStyle(fontWeight: FontWeight.bold),
tiles.add(
ListTile(
leading: Icon(TablerIcons.home, color: COLOR_ACTION),
title: Text(
L10().appTitle,
style: TextStyle(fontWeight: FontWeight.bold),
),
onTap: _home,
),
onTap: _home,
));
);
tiles.add(Divider());
@@ -137,7 +142,7 @@ class InvenTreeDrawer extends StatelessWidget {
title: Text(L10().parts),
leading: Icon(TablerIcons.box, color: COLOR_ACTION),
onTap: _parts,
)
),
);
}
@@ -147,7 +152,7 @@ class InvenTreeDrawer extends StatelessWidget {
title: Text(L10().stock),
leading: Icon(TablerIcons.package, color: COLOR_ACTION),
onTap: _stock,
)
),
);
}
@@ -157,7 +162,7 @@ class InvenTreeDrawer extends StatelessWidget {
title: Text(L10().purchaseOrders),
leading: Icon(TablerIcons.shopping_cart, color: COLOR_ACTION),
onTap: _purchaseOrders,
)
),
);
}
@@ -167,7 +172,7 @@ class InvenTreeDrawer extends StatelessWidget {
title: Text(L10().salesOrders),
leading: Icon(TablerIcons.truck_delivery, color: COLOR_ACTION),
onTap: _salesOrders,
)
),
);
}
@@ -180,10 +185,12 @@ class InvenTreeDrawer extends StatelessWidget {
tiles.add(
ListTile(
leading: Icon(TablerIcons.bell, color: COLOR_ACTION),
trailing: notification_count > 0 ? Text(notification_count.toString()) : null,
trailing: notification_count > 0
? Text(notification_count.toString())
: null,
title: Text(L10().notifications),
onTap: _notifications,
)
),
);
tiles.add(Divider());
@@ -198,14 +205,9 @@ class InvenTreeDrawer extends StatelessWidget {
},
title: Text(L10().colorScheme),
subtitle: Text(L10().colorSchemeDetail),
leading: Icon(
TablerIcons.sun_moon,
color: COLOR_ACTION
),
trailing: Icon(
darkMode ? TablerIcons.moon : TablerIcons.sun,
)
)
leading: Icon(TablerIcons.sun_moon, color: COLOR_ACTION),
trailing: Icon(darkMode ? TablerIcons.moon : TablerIcons.sun),
),
);
tiles.add(
@@ -213,7 +215,7 @@ class InvenTreeDrawer extends StatelessWidget {
title: Text(L10().settings),
leading: Icon(Icons.settings, color: COLOR_ACTION),
onTap: _settings,
)
),
);
return tiles;
@@ -221,11 +223,6 @@ class InvenTreeDrawer extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Drawer(
child: ListView(
children: drawerTiles(context),
)
);
return Drawer(child: ListView(children: drawerTiles(context)));
}
}

View File

@@ -9,11 +9,8 @@ import "package:one_context/one_context.dart";
import "package:inventree/l10.dart";
class FilePickerDialog {
static Future<File?> pickImageFromCamera() async {
final picker = ImagePicker();
final pickedImage = await picker.pickImage(source: ImageSource.camera);
@@ -26,7 +23,6 @@ class FilePickerDialog {
}
static Future<File?> pickImageFromGallery() async {
final picker = ImagePicker();
final pickedImage = await picker.pickImage(source: ImageSource.gallery);
@@ -39,7 +35,6 @@ class FilePickerDialog {
}
static Future<File?> pickFileFromDevice() async {
final FilePickerResult? result = await FilePicker.platform.pickFiles();
if (result != null) {
@@ -54,8 +49,12 @@ class FilePickerDialog {
}
// Present a dialog to pick a file, either from local file system or from camera
static Future<void> pickFile({String message = "", bool allowImages = true, bool allowFiles = true, Function(File)? onPicked}) async {
static Future<void> pickFile({
String message = "",
bool allowImages = true,
bool allowFiles = true,
Function(File)? onPicked,
}) async {
String title = "";
if (allowImages && !allowFiles) {
@@ -65,16 +64,10 @@ class FilePickerDialog {
}
// Construct actions
List<Widget> actions = [
];
List<Widget> actions = [];
if (message.isNotEmpty) {
actions.add(
ListTile(
title: Text(message)
)
);
actions.add(ListTile(title: Text(message)));
}
actions.add(
@@ -84,7 +77,6 @@ class FilePickerDialog {
title: Text(allowFiles ? L10().selectFile : L10().selectImage),
),
onPressed: () async {
// Close the dialog
OneContext().popDialog();
@@ -101,7 +93,7 @@ class FilePickerDialog {
}
}
},
)
),
);
if (allowImages) {
@@ -122,100 +114,104 @@ class FilePickerDialog {
onPicked(file);
}
}
}
)
},
),
);
}
OneContext().showDialog(
builder: (context) {
return SimpleDialog(
title: Text(title),
children: actions,
);
}
builder: (context) {
return SimpleDialog(title: Text(title), children: actions);
},
);
}
}
class CheckBoxField extends FormField<bool> {
CheckBoxField({
String? label,
bool? initial = false,
bool tristate = false,
Function(bool?)? onSaved,
TextStyle? labelStyle,
String? helperText,
TextStyle? helperStyle,
}) :
super(
onSaved: onSaved,
initialValue: initial,
builder: (FormFieldState<bool> state) {
return CheckboxListTile(
title: label != null ? Text(label, style: labelStyle) : null,
value: state.value,
tristate: tristate,
onChanged: state.didChange,
subtitle: helperText != null ? Text(helperText, style: helperStyle) : null,
contentPadding: EdgeInsets.zero,
);
}
);
String? label,
bool? initial = false,
bool tristate = false,
Function(bool?)? onSaved,
TextStyle? labelStyle,
String? helperText,
TextStyle? helperStyle,
}) : super(
onSaved: onSaved,
initialValue: initial,
builder: (FormFieldState<bool> state) {
return CheckboxListTile(
title: label != null ? Text(label, style: labelStyle) : null,
value: state.value,
tristate: tristate,
onChanged: state.didChange,
subtitle: helperText != null
? Text(helperText, style: helperStyle)
: null,
contentPadding: EdgeInsets.zero,
);
},
);
}
class StringField extends TextFormField {
StringField({
String label = "",
String? hint,
String? initial,
Function(String?)? onSaved,
Function(String?)? validator,
bool allowEmpty = false,
bool isEnabled = true,
}) : super(
decoration: InputDecoration(
labelText: allowEmpty ? label : label + "*",
hintText: hint,
),
initialValue: initial,
onSaved: onSaved,
enabled: isEnabled,
validator: (value) {
if (!allowEmpty && value != null && value.isEmpty) {
return L10().valueCannotBeEmpty;
}
StringField({String label = "", String? hint, String? initial, Function(String?)? onSaved, Function(String?)? validator, bool allowEmpty = false, bool isEnabled = true}) :
super(
decoration: InputDecoration(
labelText: allowEmpty ? label : label + "*",
hintText: hint
),
initialValue: initial,
onSaved: onSaved,
enabled: isEnabled,
validator: (value) {
if (!allowEmpty && value != null && value.isEmpty) {
return L10().valueCannotBeEmpty;
}
if (validator != null) {
return validator(value) as String?;
}
if (validator != null) {
return validator(value) as String?;
}
return null;
}
);
return null;
},
);
}
/*
* Helper class for quantity values
*/
class QuantityField extends TextFormField {
QuantityField({
String label = "",
String hint = "",
double? max,
TextEditingController? controller,
}) : super(
decoration: InputDecoration(labelText: label, hintText: hint),
controller: controller,
keyboardType: TextInputType.numberWithOptions(
signed: false,
decimal: true,
),
validator: (value) {
if (value != null && value.isEmpty) return L10().quantityEmpty;
QuantityField({String label = "", String hint = "", double? max, TextEditingController? controller}) :
super(
decoration: InputDecoration(
labelText: label,
hintText: hint,
),
controller: controller,
keyboardType: TextInputType.numberWithOptions(signed: false, decimal: true),
validator: (value) {
double quantity = double.tryParse(value.toString()) ?? 0;
if (value != null && value.isEmpty) return L10().quantityEmpty;
if (quantity <= 0) return L10().quantityPositive;
if ((max != null) && (quantity > max)) {
return "Quantity must not exceed ${max}";
}
double quantity = double.tryParse(value.toString()) ?? 0;
if (quantity <= 0) return L10().quantityPositive;
if ((max != null) && (quantity > max)) return "Quantity must not exceed ${max}";
return null;
},
);
}
return null;
},
);
}

View File

@@ -26,18 +26,15 @@ import "package:inventree/widget/snacks.dart";
import "package:inventree/widget/spinner.dart";
import "package:inventree/widget/company/company_list.dart";
class InvenTreeHomePage extends StatefulWidget {
const InvenTreeHomePage({Key? key}) : super(key: key);
@override
_InvenTreeHomePageState createState() => _InvenTreeHomePageState();
}
class _InvenTreeHomePageState extends State<InvenTreeHomePage> with BaseWidgetProperties {
class _InvenTreeHomePageState extends State<InvenTreeHomePage>
with BaseWidgetProperties {
_InvenTreeHomePageState() : super() {
// Load display settings
_loadSettings();
@@ -46,7 +43,6 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage> with BaseWidgetPr
_loadProfile();
InvenTreeAPI().registerCallback(() {
if (mounted) {
setState(() {
// Reload the widget
@@ -70,7 +66,10 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage> with BaseWidgetPr
void _showParts(BuildContext context) {
if (!InvenTreeAPI().checkConnection()) return;
Navigator.push(context, MaterialPageRoute(builder: (context) => CategoryDisplayWidget(null)));
Navigator.push(
context,
MaterialPageRoute(builder: (context) => CategoryDisplayWidget(null)),
);
}
void _showStarredParts(BuildContext context) {
@@ -78,18 +77,17 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage> with BaseWidgetPr
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => PartList({
"starred": "true"
})
)
MaterialPageRoute(builder: (context) => PartList({"starred": "true"})),
);
}
void _showStock(BuildContext context) {
if (!InvenTreeAPI().checkConnection()) return;
Navigator.push(context, MaterialPageRoute(builder: (context) => LocationDisplayWidget(null)));
Navigator.push(
context,
MaterialPageRoute(builder: (context) => LocationDisplayWidget(null)),
);
}
void _showPurchaseOrders(BuildContext context) {
@@ -98,8 +96,8 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage> with BaseWidgetPr
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => PurchaseOrderListWidget(filters: {})
)
builder: (context) => PurchaseOrderListWidget(filters: {}),
),
);
}
@@ -107,17 +105,23 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage> with BaseWidgetPr
if (!InvenTreeAPI().checkConnection()) return;
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SalesOrderListWidget(filters: {})
)
context,
MaterialPageRoute(
builder: (context) => SalesOrderListWidget(filters: {}),
),
);
}
void _showSuppliers(BuildContext context) {
if (!InvenTreeAPI().checkConnection()) return;
Navigator.push(context, MaterialPageRoute(builder: (context) => CompanyListWidget(L10().suppliers, {"is_supplier": "true"})));
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
CompanyListWidget(L10().suppliers, {"is_supplier": "true"}),
),
);
}
/*
@@ -131,39 +135,60 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage> with BaseWidgetPr
void _showCustomers(BuildContext context) {
if (!InvenTreeAPI().checkConnection()) return;
Navigator.push(context, MaterialPageRoute(builder: (context) => CompanyListWidget(L10().customers, {"is_customer": "true"})));
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
CompanyListWidget(L10().customers, {"is_customer": "true"}),
),
);
}
void _selectProfile() {
Navigator.push(
context, MaterialPageRoute(builder: (context) => InvenTreeSelectServerWidget())
context,
MaterialPageRoute(builder: (context) => InvenTreeSelectServerWidget()),
).then((context) {
// Once we return
_loadProfile();
});
}
Future <void> _loadSettings() async {
Future<void> _loadSettings() async {
homeShowSubscribed =
await InvenTreeSettingsManager().getValue(
INV_HOME_SHOW_SUBSCRIBED,
true,
)
as bool;
homeShowPo =
await InvenTreeSettingsManager().getValue(INV_HOME_SHOW_PO, true)
as bool;
homeShowSo =
await InvenTreeSettingsManager().getValue(INV_HOME_SHOW_SO, true)
as bool;
homeShowManufacturers =
await InvenTreeSettingsManager().getValue(
INV_HOME_SHOW_MANUFACTURERS,
true,
)
as bool;
homeShowCustomers =
await InvenTreeSettingsManager().getValue(INV_HOME_SHOW_CUSTOMERS, true)
as bool;
homeShowSuppliers =
await InvenTreeSettingsManager().getValue(INV_HOME_SHOW_SUPPLIERS, true)
as bool;
homeShowSubscribed = await InvenTreeSettingsManager().getValue(INV_HOME_SHOW_SUBSCRIBED, true) as bool;
homeShowPo = await InvenTreeSettingsManager().getValue(INV_HOME_SHOW_PO, true) as bool;
homeShowSo = await InvenTreeSettingsManager().getValue(INV_HOME_SHOW_SO, true) as bool;
homeShowManufacturers = await InvenTreeSettingsManager().getValue(INV_HOME_SHOW_MANUFACTURERS, true) as bool;
homeShowCustomers = await InvenTreeSettingsManager().getValue(INV_HOME_SHOW_CUSTOMERS, true) as bool;
homeShowSuppliers = await InvenTreeSettingsManager().getValue(INV_HOME_SHOW_SUPPLIERS, true) as bool;
setState(() {
});
setState(() {});
}
Future <void> _loadProfile() async {
Future<void> _loadProfile() async {
_profile = await UserProfileDBManager().getSelectedProfile();
// A valid profile was loaded!
if (_profile != null) {
if (!InvenTreeAPI().isConnected() && !InvenTreeAPI().isConnecting()) {
// Attempt server connection
InvenTreeAPI().connectToServer(_profile!).then((result) {
if (mounted) {
@@ -176,8 +201,15 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage> with BaseWidgetPr
setState(() {});
}
Widget _listTile(BuildContext context, String label, IconData icon, {Function()? callback, String role = "", String permission = "", Widget? trailing}) {
Widget _listTile(
BuildContext context,
String label,
IconData icon, {
Function()? callback,
String role = "",
String permission = "",
Widget? trailing,
}) {
bool connected = InvenTreeAPI().isConnected();
bool allowed = true;
@@ -192,20 +224,15 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage> with BaseWidgetPr
child: Align(
child: ListTile(
leading: Icon(
icon,
size: 32,
color: connected && allowed ? COLOR_ACTION : Colors.grey
),
title: Text(
label,
style: TextStyle(
fontSize: 20
),
icon,
size: 32,
color: connected && allowed ? COLOR_ACTION : Colors.grey,
),
title: Text(label, style: TextStyle(fontSize: 20)),
trailing: trailing,
),
alignment: Alignment.center,
)
),
),
onTap: () {
if (!allowed) {
@@ -228,78 +255,89 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage> with BaseWidgetPr
* Constructs a list of tiles for the main screen
*/
List<Widget> getListTiles(BuildContext context) {
List<Widget> tiles = [];
// Parts
if (InvenTreePart().canView) {
tiles.add(_listTile(
context,
L10().parts,
TablerIcons.box,
callback: () {
_showParts(context);
},
));
tiles.add(
_listTile(
context,
L10().parts,
TablerIcons.box,
callback: () {
_showParts(context);
},
),
);
}
// Starred parts
if (homeShowSubscribed && InvenTreePart().canView) {
tiles.add(_listTile(
context,
L10().partsStarred,
TablerIcons.bell,
callback: () {
_showStarredParts(context);
}
));
tiles.add(
_listTile(
context,
L10().partsStarred,
TablerIcons.bell,
callback: () {
_showStarredParts(context);
},
),
);
}
// Stock button
if (InvenTreeStockItem().canView) {
tiles.add(_listTile(
tiles.add(
_listTile(
context,
L10().stock,
TablerIcons.package,
callback: () {
_showStock(context);
}
));
},
),
);
}
// Purchase orders
if (homeShowPo && InvenTreePurchaseOrder().canView) {
tiles.add(_listTile(
tiles.add(
_listTile(
context,
L10().purchaseOrders,
TablerIcons.shopping_cart,
callback: () {
_showPurchaseOrders(context);
}
));
},
),
);
}
if (homeShowSo && InvenTreeSalesOrder().canView) {
tiles.add(_listTile(
context,
L10().salesOrders,
TablerIcons.truck_delivery,
callback: () {
_showSalesOrders(context);
}
));
tiles.add(
_listTile(
context,
L10().salesOrders,
TablerIcons.truck_delivery,
callback: () {
_showSalesOrders(context);
},
),
);
}
// Suppliers
if (homeShowSuppliers && InvenTreePurchaseOrder().canView) {
tiles.add(_listTile(
tiles.add(
_listTile(
context,
L10().suppliers,
TablerIcons.building,
callback: () {
_showSuppliers(context);
}
));
},
),
);
}
// TODO: Add these tiles back in once the features are fleshed out
@@ -320,14 +358,16 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage> with BaseWidgetPr
*/
// Customers
if (homeShowCustomers) {
tiles.add(_listTile(
tiles.add(
_listTile(
context,
L10().customers,
TablerIcons.building_store,
callback: () {
_showCustomers(context);
}
));
},
),
);
}
return tiles;
@@ -338,10 +378,10 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage> with BaseWidgetPr
* display a connection status widget
*/
Widget _connectionStatusWidget(BuildContext context) {
String? serverAddress = InvenTreeAPI().serverAddress;
bool validAddress = serverAddress != null;
bool connecting = !InvenTreeAPI().isConnected() && InvenTreeAPI().isConnecting();
bool connecting =
!InvenTreeAPI().isConnected() && InvenTreeAPI().isConnecting();
Widget leading = Icon(TablerIcons.exclamation_circle, color: COLOR_DANGER);
Widget trailing = Icon(TablerIcons.server, color: COLOR_ACTION);
@@ -373,8 +413,8 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage> with BaseWidgetPr
trailing: trailing,
leading: leading,
onTap: _selectProfile,
)
]
),
],
),
);
}
@@ -384,7 +424,6 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage> with BaseWidgetPr
*/
@override
Widget getBody(BuildContext context) {
if (!InvenTreeAPI().isConnected()) {
return _connectionStatusWidget(context);
}
@@ -398,7 +437,7 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage> with BaseWidgetPr
int hTiles = smallScreen ? 1 : 2;
double aspect = smallScreen ? 5 : 3;
double padding = smallScreen ? 2 : 10;
return GridView.count(
crossAxisCount: w > h ? vTiles : hTiles,
children: getListTiles(context),
@@ -408,12 +447,10 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage> with BaseWidgetPr
mainAxisSpacing: padding,
padding: EdgeInsets.all(padding),
);
}
@override
Widget build(BuildContext context) {
var connected = InvenTreeAPI().isConnected();
var connecting = !connected && InvenTreeAPI().isConnecting();
@@ -426,15 +463,19 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage> with BaseWidgetPr
IconButton(
icon: Icon(
TablerIcons.server,
color: connected ? COLOR_SUCCESS : (connecting ? COLOR_PROGRESS: COLOR_DANGER),
color: connected
? COLOR_SUCCESS
: (connecting ? COLOR_PROGRESS : COLOR_DANGER),
),
onPressed: _selectProfile,
)
),
],
),
drawer: InvenTreeDrawer(context),
body: getBody(context),
bottomNavigationBar: InvenTreeAPI().isConnected() ? buildBottomAppBar(context, homeKey) : null,
bottomNavigationBar: InvenTreeAPI().isConnected()
? buildBottomAppBar(context, homeKey)
: null,
);
}
}

View File

@@ -5,7 +5,6 @@ import "package:inventree/widget/refreshable_state.dart";
import "package:flutter_markdown/flutter_markdown.dart";
import "package:inventree/l10.dart";
/*
* A widget for displaying the notes associated with a given model.
* We need to pass in the following parameters:
@@ -14,7 +13,6 @@ import "package:inventree/l10.dart";
* - Title for the app bar
*/
class NotesWidget extends StatefulWidget {
const NotesWidget(this.model, {Key? key}) : super(key: key);
final InvenTreeModel model;
@@ -23,12 +21,10 @@ class NotesWidget extends StatefulWidget {
_NotesState createState() => _NotesState();
}
/*
* Class representing the state of the NotesWidget
*/
class _NotesState extends RefreshableState<NotesWidget> {
_NotesState();
@override
@@ -41,7 +37,6 @@ class _NotesState extends RefreshableState<NotesWidget> {
@override
List<Widget> appBarActions(BuildContext context) {
List<Widget> actions = [];
if (widget.model.canEdit) {
@@ -54,16 +49,14 @@ class _NotesState extends RefreshableState<NotesWidget> {
context,
L10().editNotes,
fields: {
"notes": {
"multiline": true,
}
"notes": {"multiline": true},
},
onSuccess: (data) async {
refresh(context);
}
},
);
}
)
},
),
);
}
@@ -72,10 +65,6 @@ class _NotesState extends RefreshableState<NotesWidget> {
@override
Widget getBody(BuildContext context) {
return Markdown(
selectable: false,
data: widget.model.notes,
);
return Markdown(selectable: false, data: widget.model.notes);
}
}
}

View File

@@ -1,4 +1,3 @@
import "package:flutter/material.dart";
import "package:flutter_tabler_icons/flutter_tabler_icons.dart";
@@ -8,17 +7,12 @@ import "package:inventree/inventree/model.dart";
import "package:inventree/inventree/notification.dart";
import "package:inventree/widget/refreshable_state.dart";
class NotificationWidget extends StatefulWidget {
@override
_NotificationState createState() => _NotificationState();
}
class _NotificationState extends RefreshableState<NotificationWidget> {
_NotificationState() : super();
List<InvenTreeNotification> notifications = [];
@@ -29,8 +23,7 @@ class _NotificationState extends RefreshableState<NotificationWidget> {
String getAppBarTitle() => L10().notifications;
@override
Future<void> request (BuildContext context) async {
Future<void> request(BuildContext context) async {
final results = await InvenTreeNotification().list();
notifications.clear();
@@ -45,8 +38,10 @@ class _NotificationState extends RefreshableState<NotificationWidget> {
/*
* Dismiss an individual notification entry (mark it as "read")
*/
Future<void> dismissNotification(BuildContext context, InvenTreeNotification notification) async {
Future<void> dismissNotification(
BuildContext context,
InvenTreeNotification notification,
) async {
if (mounted) {
setState(() {
isDismissing = true;
@@ -71,18 +66,17 @@ class _NotificationState extends RefreshableState<NotificationWidget> {
*/
@override
List<Widget> getTiles(BuildContext context) {
List<Widget> tiles = [];
tiles.add(
ListTile(
title: Text(
L10().notifications,
),
title: Text(L10().notifications),
subtitle: notifications.isEmpty ? Text(L10().notificationsEmpty) : null,
leading: notifications.isEmpty ? Icon(TablerIcons.bell_exclamation) : Icon(TablerIcons.bell),
leading: notifications.isEmpty
? Icon(TablerIcons.bell_exclamation)
: Icon(TablerIcons.bell),
trailing: Text("${notifications.length}"),
)
),
);
for (var notification in notifications) {
@@ -92,15 +86,16 @@ class _NotificationState extends RefreshableState<NotificationWidget> {
subtitle: Text(notification.message),
trailing: IconButton(
icon: Icon(TablerIcons.bookmark),
onPressed: isDismissing ? null : () async {
dismissNotification(context, notification);
},
onPressed: isDismissing
? null
: () async {
dismissNotification(context, notification);
},
),
)
),
);
}
return tiles;
}
}

View File

@@ -8,7 +8,6 @@ import "package:inventree/widget/snacks.dart";
import "package:inventree/inventree/orders.dart";
class ExtraLineDetailWidget extends StatefulWidget {
const ExtraLineDetailWidget(this.item, {Key? key}) : super(key: key);
@@ -18,8 +17,8 @@ class ExtraLineDetailWidget extends StatefulWidget {
_ExtraLineDetailWidgetState createState() => _ExtraLineDetailWidgetState();
}
class _ExtraLineDetailWidgetState extends RefreshableState<ExtraLineDetailWidget> {
class _ExtraLineDetailWidgetState
extends RefreshableState<ExtraLineDetailWidget> {
_ExtraLineDetailWidgetState();
@override
@@ -35,8 +34,8 @@ class _ExtraLineDetailWidgetState extends RefreshableState<ExtraLineDetailWidget
icon: Icon(TablerIcons.edit),
onPressed: () {
_editLineItem(context);
}
)
},
),
);
}
@@ -54,13 +53,13 @@ class _ExtraLineDetailWidgetState extends RefreshableState<ExtraLineDetailWidget
var fields = widget.item.formFields();
widget.item.editForm(
context,
L10().editLineItem,
fields: fields,
onSuccess: (data) async {
refresh(context);
showSnackIcon(L10().lineItemUpdated, success: true);
}
context,
L10().editLineItem,
fields: fields,
onSuccess: (data) async {
refresh(context);
showSnackIcon(L10().lineItemUpdated, success: true);
},
);
}
@@ -69,44 +68,41 @@ class _ExtraLineDetailWidgetState extends RefreshableState<ExtraLineDetailWidget
List<Widget> tiles = [];
tiles.add(
ListTile(
title: Text(L10().reference),
trailing: Text(widget.item.reference),
)
ListTile(
title: Text(L10().reference),
trailing: Text(widget.item.reference),
),
);
tiles.add(
ListTile(
title: Text(L10().description),
trailing: Text(widget.item.description),
)
ListTile(
title: Text(L10().description),
trailing: Text(widget.item.description),
),
);
tiles.add(
ListTile(
title: Text(L10().quantity),
trailing: Text(widget.item.quantity.toString()),
)
),
);
tiles.add(
ListTile(
title: Text(L10().unitPrice),
trailing: Text(
renderCurrency(widget.item.price, widget.item.priceCurrency)
)
)
renderCurrency(widget.item.price, widget.item.priceCurrency),
),
),
);
if (widget.item.notes.isNotEmpty) {
tiles.add(
ListTile(
title: Text(L10().notes),
subtitle: Text(widget.item.notes),
)
ListTile(title: Text(L10().notes), subtitle: Text(widget.item.notes)),
);
}
return tiles;
}
}
}

View File

@@ -9,28 +9,27 @@ import "package:inventree/widget/paginator.dart";
import "package:inventree/widget/refreshable_state.dart";
import "package:inventree/widget/snacks.dart";
class POExtraLineListWidget extends StatefulWidget {
const POExtraLineListWidget(this.order, {this.filters = const {}, Key? key}) : super(key: key);
const POExtraLineListWidget(this.order, {this.filters = const {}, Key? key})
: super(key: key);
final InvenTreePurchaseOrder order;
final Map<String, String> filters;
@override
_PurchaseOrderExtraLineListWidgetState createState() => _PurchaseOrderExtraLineListWidgetState();
_PurchaseOrderExtraLineListWidgetState createState() =>
_PurchaseOrderExtraLineListWidgetState();
}
class _PurchaseOrderExtraLineListWidgetState extends RefreshableState<POExtraLineListWidget> {
class _PurchaseOrderExtraLineListWidgetState
extends RefreshableState<POExtraLineListWidget> {
_PurchaseOrderExtraLineListWidgetState();
@override
String getAppBarTitle() => L10().extraLineItems;
Future<void> _addLineItem(BuildContext context) async {
var fields = InvenTreePOExtraLineItem().formFields();
fields["order"]?["value"] = widget.order.pk;
@@ -42,7 +41,7 @@ class _PurchaseOrderExtraLineListWidgetState extends RefreshableState<POExtraLin
onSuccess: (data) async {
refresh(context);
showSnackIcon(L10().lineItemUpdated, success: true);
}
},
);
}
@@ -57,8 +56,8 @@ class _PurchaseOrderExtraLineListWidgetState extends RefreshableState<POExtraLin
label: L10().lineItemAdd,
onTap: () {
_addLineItem(context);
}
)
},
),
);
}
@@ -71,35 +70,41 @@ class _PurchaseOrderExtraLineListWidgetState extends RefreshableState<POExtraLin
}
}
class PaginatedPOExtraLineList extends PaginatedSearchWidget {
const PaginatedPOExtraLineList(Map<String, String> filters) : super(filters: filters);
const PaginatedPOExtraLineList(Map<String, String> filters)
: super(filters: filters);
@override
String get searchTitle => L10().extraLineItems;
@override
_PaginatedPOExtraLineListState createState() => _PaginatedPOExtraLineListState();
_PaginatedPOExtraLineListState createState() =>
_PaginatedPOExtraLineListState();
}
class _PaginatedPOExtraLineListState extends PaginatedSearchState<PaginatedPOExtraLineList> {
class _PaginatedPOExtraLineListState
extends PaginatedSearchState<PaginatedPOExtraLineList> {
_PaginatedPOExtraLineListState() : super();
@override
String get prefix => "po_extra_line_";
@override
Future<InvenTreePageResponse?> requestPage(int limit, int offset, Map<String, String> params) async {
final page = await InvenTreePOExtraLineItem().listPaginated(limit, offset, filters: params);
Future<InvenTreePageResponse?> requestPage(
int limit,
int offset,
Map<String, String> params,
) async {
final page = await InvenTreePOExtraLineItem().listPaginated(
limit,
offset,
filters: params,
);
return page;
}
@override
Widget buildItem(BuildContext context, InvenTreeModel model) {
InvenTreePOExtraLineItem line = model as InvenTreePOExtraLineItem;
return ListTile(
@@ -113,4 +118,4 @@ class _PaginatedPOExtraLineListState extends PaginatedSearchState<PaginatedPOExt
},
);
}
}
}

View File

@@ -21,22 +21,18 @@ import "package:inventree/widget/company/supplier_part_detail.dart";
* Widget for displaying detail view of a single PurchaseOrderLineItem
*/
class POLineDetailWidget extends StatefulWidget {
const POLineDetailWidget(this.item, {Key? key}) : super(key: key);
final InvenTreePOLineItem item;
@override
_POLineDetailWidgetState createState() => _POLineDetailWidgetState();
}
/*
* State for the POLineDetailWidget
*/
class _POLineDetailWidgetState extends RefreshableState<POLineDetailWidget> {
_POLineDetailWidgetState();
InvenTreeStockLocation? destination;
@@ -55,7 +51,7 @@ class _POLineDetailWidgetState extends RefreshableState<POLineDetailWidget> {
onPressed: () {
_editLineItem(context);
},
)
),
);
}
@@ -75,8 +71,8 @@ class _POLineDetailWidgetState extends RefreshableState<POLineDetailWidget> {
label: L10().receiveItem,
onTap: () async {
receiveLineItem(context);
}
)
},
),
);
}
}
@@ -89,7 +85,9 @@ class _POLineDetailWidgetState extends RefreshableState<POLineDetailWidget> {
await widget.item.reload();
if (widget.item.destinationId > 0) {
InvenTreeStockLocation().get(widget.item.destinationId).then((InvenTreeModel? loc) {
InvenTreeStockLocation().get(widget.item.destinationId).then((
InvenTreeModel? loc,
) {
if (mounted) {
if (loc != null && loc is InvenTreeStockLocation) {
setState(() {
@@ -109,7 +107,6 @@ class _POLineDetailWidgetState extends RefreshableState<POLineDetailWidget> {
});
}
}
}
// Callback to edit this line item
@@ -123,21 +120,21 @@ class _POLineDetailWidgetState extends RefreshableState<POLineDetailWidget> {
onSuccess: (data) async {
refresh(context);
showSnackIcon(L10().lineItemUpdated, success: true);
}
},
);
}
// Launch a form to 'receive' this line item
// Launch a form to 'receive' this line item
Future<void> receiveLineItem(BuildContext context) async {
widget.item.receive(
context,
onSuccess: () => {
showSnackIcon(L10().receivedItem, success: true),
refresh(context)
}
refresh(context),
},
);
}
@override
List<Widget> getTiles(BuildContext context) {
List<Widget> tiles = [];
@@ -158,7 +155,7 @@ class _POLineDetailWidgetState extends RefreshableState<POLineDetailWidget> {
part.goToDetailPage(context);
}
},
)
),
);
// Reference to the supplier part
@@ -169,26 +166,33 @@ class _POLineDetailWidgetState extends RefreshableState<POLineDetailWidget> {
leading: Icon(TablerIcons.building, color: COLOR_ACTION),
onTap: () async {
showLoadingOverlay();
var part = await InvenTreeSupplierPart().get(widget.item.supplierPartId);
var part = await InvenTreeSupplierPart().get(
widget.item.supplierPartId,
);
hideLoadingOverlay();
if (part is InvenTreeSupplierPart) {
Navigator.push(context, MaterialPageRoute(builder: (context) => SupplierPartDetailWidget(part)));
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SupplierPartDetailWidget(part),
),
);
}
},
)
),
);
// Destination
if (destination != null) {
tiles.add(ListTile(
tiles.add(
ListTile(
title: Text(L10().destination),
subtitle: Text(destination!.name),
leading: Icon(TablerIcons.map_pin, color: COLOR_ACTION),
onTap: () => {
destination!.goToDetailPage(context)
}
));
onTap: () => {destination!.goToDetailPage(context)},
),
);
}
// Received quantity
@@ -197,23 +201,23 @@ class _POLineDetailWidgetState extends RefreshableState<POLineDetailWidget> {
title: Text(L10().received),
subtitle: ProgressBar(widget.item.progressRatio),
trailing: Text(
widget.item.progressString,
style: TextStyle(
color: widget.item.isComplete ? COLOR_SUCCESS: COLOR_WARNING
)
widget.item.progressString,
style: TextStyle(
color: widget.item.isComplete ? COLOR_SUCCESS : COLOR_WARNING,
),
),
leading: Icon(TablerIcons.progress),
)
),
);
// Reference
if (widget.item.reference.isNotEmpty) {
tiles.add(
ListTile(
title: Text(L10().reference),
subtitle: Text(widget.item.reference),
leading: Icon(TablerIcons.hash),
)
ListTile(
title: Text(L10().reference),
subtitle: Text(widget.item.reference),
leading: Icon(TablerIcons.hash),
),
);
}
@@ -223,9 +227,12 @@ class _POLineDetailWidgetState extends RefreshableState<POLineDetailWidget> {
title: Text(L10().unitPrice),
leading: Icon(TablerIcons.currency_dollar),
trailing: Text(
renderCurrency(widget.item.purchasePrice, widget.item.purchasePriceCurrency)
renderCurrency(
widget.item.purchasePrice,
widget.item.purchasePriceCurrency,
),
),
)
),
);
// Note
@@ -235,7 +242,7 @@ class _POLineDetailWidgetState extends RefreshableState<POLineDetailWidget> {
title: Text(L10().notes),
subtitle: Text(widget.item.notes),
leading: Icon(TablerIcons.note),
)
),
);
}
@@ -249,11 +256,10 @@ class _POLineDetailWidgetState extends RefreshableState<POLineDetailWidget> {
onTap: () async {
await openLink(widget.item.link);
},
)
),
);
}
return tiles;
}
}
}

View File

@@ -16,22 +16,21 @@ import "package:inventree/widget/progress.dart";
* Paginated widget class for displaying a list of purchase order line items
*/
class PaginatedPOLineList extends PaginatedSearchWidget {
const PaginatedPOLineList(Map<String, String> filters) : super(filters: filters);
const PaginatedPOLineList(Map<String, String> filters)
: super(filters: filters);
@override
String get searchTitle => L10().lineItems;
@override
_PaginatedPOLineListState createState() => _PaginatedPOLineListState();
}
/*
* State class for PaginatedPOLineList
*/
class _PaginatedPOLineListState extends PaginatedSearchState<PaginatedPOLineList> {
class _PaginatedPOLineListState
extends PaginatedSearchState<PaginatedPOLineList> {
_PaginatedPOLineListState() : super();
@override
@@ -55,13 +54,20 @@ class _PaginatedPOLineListState extends PaginatedSearchState<PaginatedPOLineList
"label": L10().received,
"help_text": L10().receivedFilterDetail,
"tristate": true,
}
},
};
@override
Future<InvenTreePageResponse?> requestPage(int limit, int offset, Map<String, String> params) async {
final page = await InvenTreePOLineItem().listPaginated(limit, offset, filters: params);
Future<InvenTreePageResponse?> requestPage(
int limit,
int offset,
Map<String, String> params,
) async {
final page = await InvenTreePOLineItem().listPaginated(
limit,
offset,
filters: params,
);
return page;
}
@@ -71,24 +77,34 @@ class _PaginatedPOLineListState extends PaginatedSearchState<PaginatedPOLineList
InvenTreeSupplierPart? supplierPart = item.supplierPart;
if (supplierPart != null) {
return ListTile(
title: Text(supplierPart.SKU),
subtitle: Text(item.partName),
trailing: Text(item.progressString, style: TextStyle(color: item.isComplete ? COLOR_SUCCESS : COLOR_WARNING)),
trailing: Text(
item.progressString,
style: TextStyle(
color: item.isComplete ? COLOR_SUCCESS : COLOR_WARNING,
),
),
leading: InvenTreeAPI().getThumbnail(supplierPart.partImage),
onTap: () async {
showLoadingOverlay();
await item.reload();
hideLoadingOverlay();
Navigator.push(context, MaterialPageRoute(builder: (context) => POLineDetailWidget(item)));
Navigator.push(
context,
MaterialPageRoute(builder: (context) => POLineDetailWidget(item)),
);
},
);
} else {
// Return an error tile
return ListTile(
title: Text(L10().error),
subtitle: Text("supplier part not defined", style: TextStyle(color: COLOR_DANGER)),
subtitle: Text(
"supplier part not defined",
style: TextStyle(color: COLOR_DANGER),
),
);
}
}

View File

@@ -18,7 +18,6 @@ import "package:inventree/widget/order/po_extra_line_list.dart";
import "package:inventree/widget/stock/location_display.dart";
import "package:inventree/widget/order/po_line_list.dart";
import "package:inventree/widget/attachment_widget.dart";
import "package:inventree/widget/notes_widget.dart";
import "package:inventree/widget/progress.dart";
@@ -27,13 +26,11 @@ import "package:inventree/widget/snacks.dart";
import "package:inventree/widget/stock/stock_list.dart";
import "package:inventree/preferences.dart";
/*
* Widget for viewing a single PurchaseOrder instance
*/
class PurchaseOrderDetailWidget extends StatefulWidget {
const PurchaseOrderDetailWidget(this.order, {Key? key}): super(key: key);
const PurchaseOrderDetailWidget(this.order, {Key? key}) : super(key: key);
final InvenTreePurchaseOrder order;
@@ -41,11 +38,10 @@ class PurchaseOrderDetailWidget extends StatefulWidget {
_PurchaseOrderDetailState createState() => _PurchaseOrderDetailState();
}
class _PurchaseOrderDetailState extends RefreshableState<PurchaseOrderDetailWidget> {
class _PurchaseOrderDetailState
extends RefreshableState<PurchaseOrderDetailWidget> {
_PurchaseOrderDetailState();
List<InvenTreePOLineItem> lines = [];
int extraLineCount = 0;
@@ -79,8 +75,8 @@ class _PurchaseOrderDetailState extends RefreshableState<PurchaseOrderDetailWidg
tooltip: L10().purchaseOrderEdit,
onPressed: () {
editOrder(context);
}
)
},
),
);
}
@@ -93,27 +89,26 @@ class _PurchaseOrderDetailState extends RefreshableState<PurchaseOrderDetailWidg
if (showCameraShortcut && widget.order.canEdit) {
actions.add(
SpeedDialChild(
child: Icon(TablerIcons.camera, color: Colors.blue),
label: L10().takePicture,
onTap: () async {
_uploadImage(context);
}
)
SpeedDialChild(
child: Icon(TablerIcons.camera, color: Colors.blue),
label: L10().takePicture,
onTap: () async {
_uploadImage(context);
},
),
);
}
if (widget.order.canCreate) {
if (widget.order.isPending) {
actions.add(
SpeedDialChild(
child: Icon(TablerIcons.circle_plus, color: Colors.green),
label: L10().lineItemAdd,
onTap: () async {
_addLineItem(context);
}
)
},
),
);
actions.add(
@@ -122,8 +117,8 @@ class _PurchaseOrderDetailState extends RefreshableState<PurchaseOrderDetailWidg
label: L10().issueOrder,
onTap: () async {
_issueOrder(context);
}
)
},
),
);
}
@@ -134,8 +129,8 @@ class _PurchaseOrderDetailState extends RefreshableState<PurchaseOrderDetailWidg
label: L10().cancelOrder,
onTap: () async {
_cancelOrder(context);
}
)
},
),
);
}
}
@@ -145,14 +140,11 @@ class _PurchaseOrderDetailState extends RefreshableState<PurchaseOrderDetailWidg
/// Add a new line item to this order
Future<void> _addLineItem(BuildContext context) async {
var fields = InvenTreePOLineItem().formFields();
// Update part field definition
fields["part"]?["hidden"] = false;
fields["part"]?["filters"] = {
"supplier": widget.order.supplierId
};
fields["part"]?["filters"] = {"supplier": widget.order.supplierId};
fields["order"]?["value"] = widget.order.pk;
@@ -163,24 +155,22 @@ class _PurchaseOrderDetailState extends RefreshableState<PurchaseOrderDetailWidg
onSuccess: (data) async {
refresh(context);
showSnackIcon(L10().lineItemUpdated, success: true);
}
},
);
}
/// Upload an image against the current PurchaseOrder
Future<void> _uploadImage(BuildContext context) async {
InvenTreePurchaseOrderAttachment().uploadImage(
widget.order.pk,
prefix: widget.order.reference,
).then((result) => refresh(context));
InvenTreePurchaseOrderAttachment()
.uploadImage(widget.order.pk, prefix: widget.order.reference)
.then((result) => refresh(context));
}
/// Issue this order
Future<void> _issueOrder(BuildContext context) async {
confirmationDialog(
L10().issueOrder, "",
L10().issueOrder,
"",
icon: TablerIcons.send,
color: Colors.blue,
acceptText: L10().issue,
@@ -188,15 +178,15 @@ class _PurchaseOrderDetailState extends RefreshableState<PurchaseOrderDetailWidg
widget.order.issueOrder().then((dynamic) {
refresh(context);
});
}
},
);
}
/// Cancel this order
Future<void> _cancelOrder(BuildContext context) async {
confirmationDialog(
L10().cancelOrder, "",
L10().cancelOrder,
"",
icon: TablerIcons.circle_x,
color: Colors.red,
acceptText: L10().cancel,
@@ -204,7 +194,7 @@ class _PurchaseOrderDetailState extends RefreshableState<PurchaseOrderDetailWidg
widget.order.cancelOrder().then((dynamic) {
refresh(context);
});
}
},
);
}
@@ -217,7 +207,7 @@ class _PurchaseOrderDetailState extends RefreshableState<PurchaseOrderDetailWidg
SpeedDialChild(
child: Icon(Icons.barcode_reader),
label: L10().scanReceivedParts,
onTap:() async {
onTap: () async {
scanBarcode(
context,
handler: POReceiveBarcodeHandler(purchaseOrder: widget.order),
@@ -225,7 +215,7 @@ class _PurchaseOrderDetailState extends RefreshableState<PurchaseOrderDetailWidg
refresh(context);
});
},
)
),
);
}
@@ -239,15 +229,14 @@ class _PurchaseOrderDetailState extends RefreshableState<PurchaseOrderDetailWidg
context,
handler: POAllocateBarcodeHandler(purchaseOrder: widget.order),
);
}
)
},
),
);
}
return actions;
}
@override
Future<void> request(BuildContext context) async {
await widget.order.reload();
@@ -256,8 +245,16 @@ class _PurchaseOrderDetailState extends RefreshableState<PurchaseOrderDetailWidg
lines = await widget.order.getLineItems();
showCameraShortcut = await InvenTreeSettingsManager().getBool(INV_PO_SHOW_CAMERA, true);
supportProjectCodes = api.supportsProjectCodes && await api.getGlobalBooleanSetting("PROJECT_CODES_ENABLED", backup: true);
showCameraShortcut = await InvenTreeSettingsManager().getBool(
INV_PO_SHOW_CAMERA,
true,
);
supportProjectCodes =
api.supportsProjectCodes &&
await api.getGlobalBooleanSetting(
"PROJECT_CODES_ENABLED",
backup: true,
);
completedLines = 0;
@@ -267,7 +264,9 @@ class _PurchaseOrderDetailState extends RefreshableState<PurchaseOrderDetailWidg
}
}
InvenTreePurchaseOrderAttachment().countAttachments(widget.order.pk).then((int value) {
InvenTreePurchaseOrderAttachment().countAttachments(widget.order.pk).then((
int value,
) {
if (mounted) {
setState(() {
attachmentCount = value;
@@ -275,8 +274,11 @@ class _PurchaseOrderDetailState extends RefreshableState<PurchaseOrderDetailWidg
}
});
if (api.supportsPurchaseOrderDestination && widget.order.destinationId > 0) {
InvenTreeStockLocation().get(widget.order.destinationId).then((InvenTreeModel? loc) {
if (api.supportsPurchaseOrderDestination &&
widget.order.destinationId > 0) {
InvenTreeStockLocation().get(widget.order.destinationId).then((
InvenTreeModel? loc,
) {
if (mounted) {
if (loc != null && loc is InvenTreeStockLocation) {
setState(() {
@@ -298,17 +300,19 @@ class _PurchaseOrderDetailState extends RefreshableState<PurchaseOrderDetailWidg
}
// Count number of "extra line items" against this order
InvenTreePOExtraLineItem().count(filters: {"order": widget.order.pk.toString() }).then((int value) {
if (mounted) {
setState(() {
extraLineCount = value;
InvenTreePOExtraLineItem()
.count(filters: {"order": widget.order.pk.toString()})
.then((int value) {
if (mounted) {
setState(() {
extraLineCount = value;
});
}
});
}
});
}
// Edit the currently displayed PurchaseOrder
Future <void> editOrder(BuildContext context) async {
Future<void> editOrder(BuildContext context) async {
var fields = widget.order.formFields();
// Cannot edit supplier field from here
@@ -331,32 +335,29 @@ class _PurchaseOrderDetailState extends RefreshableState<PurchaseOrderDetailWidg
onSuccess: (data) async {
refresh(context);
showSnackIcon(L10().purchaseOrderUpdated, success: true);
}
},
);
}
Widget headerTile(BuildContext context) {
InvenTreeCompany? supplier = widget.order.supplier;
return Card(
child: ListTile(
title: Text(widget.order.reference),
subtitle: Text(widget.order.description),
leading: supplier == null ? null : api.getThumbnail(supplier.thumbnail),
trailing: Text(
api.PurchaseOrderStatus.label(widget.order.status),
style: TextStyle(
color: api.PurchaseOrderStatus.color(widget.order.status)
),
)
)
child: ListTile(
title: Text(widget.order.reference),
subtitle: Text(widget.order.description),
leading: supplier == null ? null : api.getThumbnail(supplier.thumbnail),
trailing: Text(
api.PurchaseOrderStatus.label(widget.order.status),
style: TextStyle(
color: api.PurchaseOrderStatus.color(widget.order.status),
),
),
),
);
}
List<Widget> orderTiles(BuildContext context) {
List<Widget> tiles = [];
InvenTreeCompany? supplier = widget.order.supplier;
@@ -364,165 +365,204 @@ class _PurchaseOrderDetailState extends RefreshableState<PurchaseOrderDetailWidg
tiles.add(headerTile(context));
if (supportProjectCodes && widget.order.hasProjectCode) {
tiles.add(ListTile(
title: Text(L10().projectCode),
subtitle: Text("${widget.order.projectCode} - ${widget.order.projectCodeDescription}"),
leading: Icon(TablerIcons.list),
));
tiles.add(
ListTile(
title: Text(L10().projectCode),
subtitle: Text(
"${widget.order.projectCode} - ${widget.order.projectCodeDescription}",
),
leading: Icon(TablerIcons.list),
),
);
}
if (supplier != null) {
tiles.add(ListTile(
title: Text(L10().supplier),
subtitle: Text(supplier.name),
leading: Icon(TablerIcons.building, color: COLOR_ACTION),
onTap: () {
supplier.goToDetailPage(context);
},
));
tiles.add(
ListTile(
title: Text(L10().supplier),
subtitle: Text(supplier.name),
leading: Icon(TablerIcons.building, color: COLOR_ACTION),
onTap: () {
supplier.goToDetailPage(context);
},
),
);
}
if (widget.order.supplierReference.isNotEmpty) {
tiles.add(ListTile(
title: Text(L10().supplierReference),
subtitle: Text(widget.order.supplierReference),
leading: Icon(TablerIcons.hash),
));
tiles.add(
ListTile(
title: Text(L10().supplierReference),
subtitle: Text(widget.order.supplierReference),
leading: Icon(TablerIcons.hash),
),
);
}
// Order destination
if (destination != null) {
tiles.add(ListTile(
title: Text(L10().destination),
subtitle: Text(destination!.name),
leading: Icon(TablerIcons.map_pin, color: COLOR_ACTION),
tiles.add(
ListTile(
title: Text(L10().destination),
subtitle: Text(destination!.name),
leading: Icon(TablerIcons.map_pin, color: COLOR_ACTION),
onTap: () => {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => LocationDisplayWidget(destination),
),
),
},
),
);
}
Color lineColor = completedLines < widget.order.lineItemCount
? COLOR_WARNING
: COLOR_SUCCESS;
tiles.add(
ListTile(
title: Text(L10().lineItems),
subtitle: ProgressBar(
completedLines.toDouble(),
maximum: widget.order.lineItemCount.toDouble(),
),
leading: Icon(TablerIcons.clipboard_check),
trailing: Text(
"${completedLines} / ${widget.order.lineItemCount}",
style: TextStyle(color: lineColor),
),
),
);
// Extra line items
tiles.add(
ListTile(
title: Text(L10().extraLineItems),
leading: Icon(TablerIcons.clipboard_list, color: COLOR_ACTION),
trailing: Text(extraLineCount.toString()),
onTap: () => {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => LocationDisplayWidget(destination)
)
)
}
));
}
Color lineColor = completedLines < widget.order.lineItemCount ? COLOR_WARNING : COLOR_SUCCESS;
tiles.add(ListTile(
title: Text(L10().lineItems),
subtitle: ProgressBar(
completedLines.toDouble(),
maximum: widget.order.lineItemCount.toDouble(),
builder: (context) => POExtraLineListWidget(
widget.order,
filters: {"order": widget.order.pk.toString()},
),
),
),
},
),
leading: Icon(TablerIcons.clipboard_check),
trailing: Text("${completedLines} / ${widget.order.lineItemCount}", style: TextStyle(color: lineColor)),
));
);
// Extra line items
tiles.add(ListTile(
title: Text(L10().extraLineItems),
leading: Icon(TablerIcons.clipboard_list, color: COLOR_ACTION),
trailing: Text(extraLineCount.toString()),
onTap: () => {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => POExtraLineListWidget(widget.order, filters: {"order": widget.order.pk.toString()})
)
)
},
));
tiles.add(ListTile(
title: Text(L10().totalPrice),
leading: Icon(TablerIcons.currency_dollar),
trailing: Text(
renderCurrency(widget.order.totalPrice, widget.order.totalPriceCurrency)
tiles.add(
ListTile(
title: Text(L10().totalPrice),
leading: Icon(TablerIcons.currency_dollar),
trailing: Text(
renderCurrency(
widget.order.totalPrice,
widget.order.totalPriceCurrency,
),
),
),
));
);
if (widget.order.issueDate.isNotEmpty) {
tiles.add(ListTile(
title: Text(L10().issueDate),
trailing: Text(widget.order.issueDate),
leading: Icon(TablerIcons.calendar),
));
tiles.add(
ListTile(
title: Text(L10().issueDate),
trailing: Text(widget.order.issueDate),
leading: Icon(TablerIcons.calendar),
),
);
}
if (widget.order.startDate.isNotEmpty) {
tiles.add(ListTile(
title: Text(L10().startDate),
trailing: Text(widget.order.startDate),
leading: Icon(TablerIcons.calendar),
));
tiles.add(
ListTile(
title: Text(L10().startDate),
trailing: Text(widget.order.startDate),
leading: Icon(TablerIcons.calendar),
),
);
}
if (widget.order.targetDate.isNotEmpty) {
tiles.add(ListTile(
title: Text(L10().targetDate),
trailing: Text(widget.order.targetDate),
leading: Icon(TablerIcons.calendar),
));
tiles.add(
ListTile(
title: Text(L10().targetDate),
trailing: Text(widget.order.targetDate),
leading: Icon(TablerIcons.calendar),
),
);
}
if (widget.order.completionDate.isNotEmpty) {
tiles.add(ListTile(
title: Text(L10().completionDate),
trailing: Text(widget.order.completionDate),
leading: Icon(TablerIcons.calendar),
));
tiles.add(
ListTile(
title: Text(L10().completionDate),
trailing: Text(widget.order.completionDate),
leading: Icon(TablerIcons.calendar),
),
);
}
// Responsible "owner"
if (widget.order.responsibleName.isNotEmpty && widget.order.responsibleLabel.isNotEmpty) {
tiles.add(ListTile(
title: Text(L10().responsible),
leading: Icon(widget.order.responsibleLabel == "group" ? TablerIcons.users : TablerIcons.user),
trailing: Text(widget.order.responsibleName)
));
if (widget.order.responsibleName.isNotEmpty &&
widget.order.responsibleLabel.isNotEmpty) {
tiles.add(
ListTile(
title: Text(L10().responsible),
leading: Icon(
widget.order.responsibleLabel == "group"
? TablerIcons.users
: TablerIcons.user,
),
trailing: Text(widget.order.responsibleName),
),
);
}
// Notes tile
tiles.add(
ListTile(
title: Text(L10().notes),
leading: Icon(TablerIcons.note, color: COLOR_ACTION),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => NotesWidget(widget.order)
)
);
},
)
ListTile(
title: Text(L10().notes),
leading: Icon(TablerIcons.note, color: COLOR_ACTION),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => NotesWidget(widget.order)),
);
},
),
);
// Attachments
tiles.add(
ListTile(
title: Text(L10().attachments),
leading: Icon(TablerIcons.file, color: COLOR_ACTION),
trailing: attachmentCount > 0 ? Text(attachmentCount.toString()) : null,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => AttachmentWidget(
InvenTreePurchaseOrderAttachment(),
widget.order.pk,
widget.order.reference,
widget.order.canEdit
)
)
);
},
)
ListTile(
title: Text(L10().attachments),
leading: Icon(TablerIcons.file, color: COLOR_ACTION),
trailing: attachmentCount > 0 ? Text(attachmentCount.toString()) : null,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => AttachmentWidget(
InvenTreePurchaseOrderAttachment(),
widget.order.pk,
widget.order.reference,
widget.order.canEdit,
),
),
);
},
),
);
return tiles;
}
@override
@@ -530,10 +570,10 @@ class _PurchaseOrderDetailState extends RefreshableState<PurchaseOrderDetailWidg
return [
Tab(text: L10().details),
Tab(text: L10().lineItems),
Tab(text: L10().received)
Tab(text: L10().received),
];
}
@override
List<Widget> getTabs(BuildContext context) {
return [

View File

@@ -16,18 +16,18 @@ import "package:inventree/inventree/purchase_order.dart";
* Widget class for displaying a list of Purchase Orders
*/
class PurchaseOrderListWidget extends StatefulWidget {
const PurchaseOrderListWidget({this.filters = const {}, Key? key}) : super(key: key);
const PurchaseOrderListWidget({this.filters = const {}, Key? key})
: super(key: key);
final Map<String, String> filters;
@override
_PurchaseOrderListWidgetState createState() => _PurchaseOrderListWidgetState();
_PurchaseOrderListWidgetState createState() =>
_PurchaseOrderListWidgetState();
}
class _PurchaseOrderListWidgetState extends RefreshableState<PurchaseOrderListWidget> {
class _PurchaseOrderListWidgetState
extends RefreshableState<PurchaseOrderListWidget> {
_PurchaseOrderListWidgetState();
@override
@@ -44,8 +44,8 @@ class _PurchaseOrderListWidgetState extends RefreshableState<PurchaseOrderListWi
label: L10().purchaseOrderCreate,
onTap: () {
_createPurchaseOrder(context);
}
)
},
),
);
}
@@ -70,7 +70,7 @@ class _PurchaseOrderListWidgetState extends RefreshableState<PurchaseOrderListWi
var order = InvenTreePurchaseOrder.fromJson(data);
order.goToDetailPage(context);
}
}
},
);
}
@@ -83,13 +83,10 @@ class _PurchaseOrderListWidgetState extends RefreshableState<PurchaseOrderListWi
SpeedDialChild(
child: Icon(Icons.barcode_reader),
label: L10().scanReceivedParts,
onTap:() async {
scanBarcode(
context,
handler: POReceiveBarcodeHandler(),
);
onTap: () async {
scanBarcode(context, handler: POReceiveBarcodeHandler());
},
)
),
);
}
@@ -102,22 +99,20 @@ class _PurchaseOrderListWidgetState extends RefreshableState<PurchaseOrderListWi
}
}
class PaginatedPurchaseOrderList extends PaginatedSearchWidget {
const PaginatedPurchaseOrderList(Map<String, String> filters) : super(filters: filters);
const PaginatedPurchaseOrderList(Map<String, String> filters)
: super(filters: filters);
@override
String get searchTitle => L10().purchaseOrders;
@override
_PaginatedPurchaseOrderListState createState() => _PaginatedPurchaseOrderListState();
_PaginatedPurchaseOrderListState createState() =>
_PaginatedPurchaseOrderListState();
}
class _PaginatedPurchaseOrderListState extends PaginatedSearchState<PaginatedPurchaseOrderList> {
class _PaginatedPurchaseOrderListState
extends PaginatedSearchState<PaginatedPurchaseOrderList> {
_PaginatedPurchaseOrderListState() : super();
@override
@@ -147,29 +142,37 @@ class _PaginatedPurchaseOrderListState extends PaginatedSearchState<PaginatedPur
"label": L10().assignedToMe,
"help_text": L10().assignedToMeDetail,
"tristate": true,
}
},
};
@override
Future<InvenTreePageResponse?> requestPage(int limit, int offset, Map<String, String> params) async {
Future<InvenTreePageResponse?> requestPage(
int limit,
int offset,
Map<String, String> params,
) async {
await InvenTreeAPI().PurchaseOrderStatus.load();
final page = await InvenTreePurchaseOrder().listPaginated(limit, offset, filters: params);
final page = await InvenTreePurchaseOrder().listPaginated(
limit,
offset,
filters: params,
);
return page;
}
@override
Widget buildItem(BuildContext context, InvenTreeModel model) {
InvenTreePurchaseOrder order = model as InvenTreePurchaseOrder;
InvenTreeCompany? supplier = order.supplier;
return ListTile(
title: Text(order.reference),
subtitle: Text(order.description),
leading: supplier == null ? null : InvenTreeAPI().getThumbnail(supplier.thumbnail),
leading: supplier == null
? null
: InvenTreeAPI().getThumbnail(supplier.thumbnail),
trailing: Text(
InvenTreeAPI().PurchaseOrderStatus.label(order.status),
style: TextStyle(
@@ -181,4 +184,4 @@ class _PaginatedPurchaseOrderListState extends PaginatedSearchState<PaginatedPur
},
);
}
}
}

View File

@@ -1,4 +1,3 @@
import "package:flutter/material.dart";
import "package:flutter_speed_dial/flutter_speed_dial.dart";
import "package:flutter_tabler_icons/flutter_tabler_icons.dart";
@@ -25,7 +24,6 @@ import "package:inventree/widget/progress.dart";
* Widget for viewing a single SalesOrder instance
*/
class SalesOrderDetailWidget extends StatefulWidget {
const SalesOrderDetailWidget(this.order, {Key? key}) : super(key: key);
final InvenTreeSalesOrder order;
@@ -34,9 +32,7 @@ class SalesOrderDetailWidget extends StatefulWidget {
_SalesOrderDetailState createState() => _SalesOrderDetailState();
}
class _SalesOrderDetailState extends RefreshableState<SalesOrderDetailWidget> {
_SalesOrderDetailState();
List<InvenTreeSOLineItem> lines = [];
@@ -68,7 +64,7 @@ class _SalesOrderDetailState extends RefreshableState<SalesOrderDetailWidget> {
onPressed: () {
editOrder(context);
},
)
),
);
}
@@ -77,7 +73,6 @@ class _SalesOrderDetailState extends RefreshableState<SalesOrderDetailWidget> {
// Add a new shipment against this sales order
Future<void> _addShipment(BuildContext context) async {
var fields = InvenTreeSalesOrderShipment().formFields();
fields["order"]?["value"] = widget.order.pk;
@@ -89,9 +84,8 @@ class _SalesOrderDetailState extends RefreshableState<SalesOrderDetailWidget> {
fields: fields,
onSuccess: (result) async {
refresh(context);
}
},
);
}
// Add a new line item to this sales order
@@ -102,52 +96,51 @@ class _SalesOrderDetailState extends RefreshableState<SalesOrderDetailWidget> {
fields["order"]?["hidden"] = true;
InvenTreeSOLineItem().createForm(
context,
L10().lineItemAdd,
fields: fields,
onSuccess: (result) async {
refresh(context);
}
context,
L10().lineItemAdd,
fields: fields,
onSuccess: (result) async {
refresh(context);
},
);
}
/// Upload an image for this order
Future<void> _uploadImage(BuildContext context) async {
InvenTreeSalesOrderAttachment().uploadImage(
widget.order.pk,
prefix: widget.order.reference,
).then((result) => refresh(context));
InvenTreeSalesOrderAttachment()
.uploadImage(widget.order.pk, prefix: widget.order.reference)
.then((result) => refresh(context));
}
/// Issue this order
Future<void> _issueOrder(BuildContext context) async {
confirmationDialog(
L10().issueOrder, "",
icon: TablerIcons.send,
color: Colors.blue,
acceptText: L10().issue,
onAccept: () async {
widget.order.issueOrder().then((dynamic) {
refresh(context);
});
}
L10().issueOrder,
"",
icon: TablerIcons.send,
color: Colors.blue,
acceptText: L10().issue,
onAccept: () async {
widget.order.issueOrder().then((dynamic) {
refresh(context);
});
},
);
}
/// Cancel this order
Future<void> _cancelOrder(BuildContext context) async {
confirmationDialog(
L10().cancelOrder, "",
icon: TablerIcons.circle_x,
color: Colors.red,
acceptText: L10().cancel,
onAccept: () async {
await widget.order.cancelOrder().then((dynamic) {
refresh(context);
});
}
L10().cancelOrder,
"",
icon: TablerIcons.circle_x,
color: Colors.red,
acceptText: L10().cancel,
onAccept: () async {
await widget.order.cancelOrder().then((dynamic) {
refresh(context);
});
},
);
}
@@ -157,50 +150,51 @@ class _SalesOrderDetailState extends RefreshableState<SalesOrderDetailWidget> {
if (showCameraShortcut && widget.order.canEdit) {
actions.add(
SpeedDialChild(
child: Icon(TablerIcons.camera, color: Colors.blue),
label: L10().takePicture,
onTap: () async {
_uploadImage(context);
}
)
SpeedDialChild(
child: Icon(TablerIcons.camera, color: Colors.blue),
label: L10().takePicture,
onTap: () async {
_uploadImage(context);
},
),
);
}
if (widget.order.isPending) {
actions.add(
SpeedDialChild(
child: Icon(TablerIcons.send, color: Colors.blue),
label: L10().issueOrder,
onTap: () async {
_issueOrder(context);
}
)
SpeedDialChild(
child: Icon(TablerIcons.send, color: Colors.blue),
label: L10().issueOrder,
onTap: () async {
_issueOrder(context);
},
),
);
}
if (widget.order.isOpen) {
actions.add(
SpeedDialChild(
child: Icon(TablerIcons.circle_x, color: Colors.red),
label: L10().cancelOrder,
onTap: () async {
_cancelOrder(context);
}
)
SpeedDialChild(
child: Icon(TablerIcons.circle_x, color: Colors.red),
label: L10().cancelOrder,
onTap: () async {
_cancelOrder(context);
},
),
);
}
// Add line item
if ((widget.order.isPending || widget.order.isInProgress) && InvenTreeSOLineItem().canCreate) {
if ((widget.order.isPending || widget.order.isInProgress) &&
InvenTreeSOLineItem().canCreate) {
actions.add(
SpeedDialChild(
child: Icon(TablerIcons.circle_plus, color: Colors.green),
label: L10().lineItemAdd,
onTap: () async {
_addLineItem(context);
}
)
},
),
);
actions.add(
@@ -209,8 +203,8 @@ class _SalesOrderDetailState extends RefreshableState<SalesOrderDetailWidget> {
label: L10().shipmentAdd,
onTap: () async {
_addShipment(context);
}
)
},
),
);
}
@@ -221,7 +215,8 @@ class _SalesOrderDetailState extends RefreshableState<SalesOrderDetailWidget> {
List<SpeedDialChild> barcodeButtons(BuildContext context) {
List<SpeedDialChild> actions = [];
if ((widget.order.isInProgress || widget.order.isPending) && InvenTreeSOLineItem().canCreate) {
if ((widget.order.isInProgress || widget.order.isPending) &&
InvenTreeSOLineItem().canCreate) {
actions.add(
SpeedDialChild(
child: Icon(Icons.barcode_reader),
@@ -231,8 +226,8 @@ class _SalesOrderDetailState extends RefreshableState<SalesOrderDetailWidget> {
context,
handler: SOAddItemBarcodeHandler(salesOrder: widget.order),
);
}
)
},
),
);
if (api.supportsBarcodeSOAllocateEndpoint) {
@@ -243,12 +238,10 @@ class _SalesOrderDetailState extends RefreshableState<SalesOrderDetailWidget> {
onTap: () async {
scanBarcode(
context,
handler: SOAllocateStockHandler(
salesOrder: widget.order,
)
handler: SOAllocateStockHandler(salesOrder: widget.order),
);
}
)
},
),
);
}
}
@@ -261,10 +254,20 @@ class _SalesOrderDetailState extends RefreshableState<SalesOrderDetailWidget> {
await widget.order.reload();
await api.SalesOrderStatus.load();
supportsProjectCodes = api.supportsProjectCodes && await api.getGlobalBooleanSetting("PROJECT_CODES_ENABLED", backup: true);
showCameraShortcut = await InvenTreeSettingsManager().getBool(INV_SO_SHOW_CAMERA, true);
supportsProjectCodes =
api.supportsProjectCodes &&
await api.getGlobalBooleanSetting(
"PROJECT_CODES_ENABLED",
backup: true,
);
showCameraShortcut = await InvenTreeSettingsManager().getBool(
INV_SO_SHOW_CAMERA,
true,
);
InvenTreeSalesOrderAttachment().countAttachments(widget.order.pk).then((int value) {
InvenTreeSalesOrderAttachment().countAttachments(widget.order.pk).then((
int value,
) {
if (mounted) {
setState(() {
attachmentCount = value;
@@ -273,13 +276,15 @@ class _SalesOrderDetailState extends RefreshableState<SalesOrderDetailWidget> {
});
// Count number of "extra line items" against this order
InvenTreeSOExtraLineItem().count(filters: {"order": widget.order.pk.toString() }).then((int value) {
if (mounted) {
setState(() {
extraLineCount = value;
InvenTreeSOExtraLineItem()
.count(filters: {"order": widget.order.pk.toString()})
.then((int value) {
if (mounted) {
setState(() {
extraLineCount = value;
});
}
});
}
});
}
// Edit the current SalesOrder instance
@@ -305,7 +310,7 @@ class _SalesOrderDetailState extends RefreshableState<SalesOrderDetailWidget> {
onSuccess: (data) async {
refresh(context);
showSnackIcon(L10().salesOrderUpdated, success: true);
}
},
);
}
@@ -321,121 +326,154 @@ class _SalesOrderDetailState extends RefreshableState<SalesOrderDetailWidget> {
trailing: Text(
api.SalesOrderStatus.label(widget.order.status),
style: TextStyle(
color: api.SalesOrderStatus.color(widget.order.status)
color: api.SalesOrderStatus.color(widget.order.status),
),
),
)
),
);
}
List<Widget> orderTiles(BuildContext context) {
List<Widget> tiles = [
headerTile(context)
];
List<Widget> tiles = [headerTile(context)];
InvenTreeCompany? customer = widget.order.customer;
if (supportsProjectCodes && widget.order.hasProjectCode) {
tiles.add(ListTile(
title: Text(L10().projectCode),
subtitle: Text("${widget.order.projectCode} - ${widget.order.projectCodeDescription}"),
leading: Icon(TablerIcons.list),
));
tiles.add(
ListTile(
title: Text(L10().projectCode),
subtitle: Text(
"${widget.order.projectCode} - ${widget.order.projectCodeDescription}",
),
leading: Icon(TablerIcons.list),
),
);
}
if (customer != null) {
tiles.add(ListTile(
title: Text(L10().customer),
subtitle: Text(customer.name),
leading: Icon(TablerIcons.user, color: COLOR_ACTION),
onTap: () {
customer.goToDetailPage(context);
}
));
tiles.add(
ListTile(
title: Text(L10().customer),
subtitle: Text(customer.name),
leading: Icon(TablerIcons.user, color: COLOR_ACTION),
onTap: () {
customer.goToDetailPage(context);
},
),
);
}
if (widget.order.customerReference.isNotEmpty) {
tiles.add(ListTile(
title: Text(L10().customerReference),
trailing: Text(widget.order.customerReference),
leading: Icon(TablerIcons.hash),
));
tiles.add(
ListTile(
title: Text(L10().customerReference),
trailing: Text(widget.order.customerReference),
leading: Icon(TablerIcons.hash),
),
);
}
Color lineColor = widget.order.complete ? COLOR_SUCCESS : COLOR_WARNING;
tiles.add(ListTile(
title: Text(L10().lineItems),
subtitle: ProgressBar(
widget.order.completedLineItemCount.toDouble(),
maximum: widget.order.lineItemCount.toDouble()
tiles.add(
ListTile(
title: Text(L10().lineItems),
subtitle: ProgressBar(
widget.order.completedLineItemCount.toDouble(),
maximum: widget.order.lineItemCount.toDouble(),
),
leading: Icon(TablerIcons.clipboard_check),
trailing: Text(
"${widget.order.completedLineItemCount} / ${widget.order.lineItemCount}",
style: TextStyle(color: lineColor),
),
),
leading: Icon(TablerIcons.clipboard_check),
trailing: Text("${widget.order.completedLineItemCount} / ${widget.order.lineItemCount}", style: TextStyle(color: lineColor)),
));
);
// Extra line items
tiles.add(ListTile(
title: Text(L10().extraLineItems),
leading: Icon(TablerIcons.clipboard_list, color: COLOR_ACTION),
trailing: Text(extraLineCount.toString()),
onTap: () => {
Navigator.push(
tiles.add(
ListTile(
title: Text(L10().extraLineItems),
leading: Icon(TablerIcons.clipboard_list, color: COLOR_ACTION),
trailing: Text(extraLineCount.toString()),
onTap: () => {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SOExtraLineListWidget(widget.order, filters: {"order": widget.order.pk.toString()})
)
)
},
));
builder: (context) => SOExtraLineListWidget(
widget.order,
filters: {"order": widget.order.pk.toString()},
),
),
),
},
),
);
// Shipment progress
if (widget.order.shipmentCount > 0) {
tiles.add(ListTile(
title: Text(L10().shipments),
subtitle: ProgressBar(
widget.order.completedShipmentCount.toDouble(),
maximum: widget.order.shipmentCount.toDouble()
tiles.add(
ListTile(
title: Text(L10().shipments),
subtitle: ProgressBar(
widget.order.completedShipmentCount.toDouble(),
maximum: widget.order.shipmentCount.toDouble(),
),
leading: Icon(TablerIcons.truck_delivery),
trailing: Text(
"${widget.order.completedShipmentCount} / ${widget.order.shipmentCount}",
style: TextStyle(color: lineColor),
),
),
leading: Icon(TablerIcons.truck_delivery),
trailing: Text("${widget.order.completedShipmentCount} / ${widget.order.shipmentCount}", style: TextStyle(color: lineColor)),
));
);
}
// TODO: total price
if (widget.order.startDate.isNotEmpty) {
tiles.add(ListTile(
title: Text(L10().startDate),
trailing: Text(widget.order.startDate),
leading: Icon(TablerIcons.calendar),
));
tiles.add(
ListTile(
title: Text(L10().startDate),
trailing: Text(widget.order.startDate),
leading: Icon(TablerIcons.calendar),
),
);
}
if (widget.order.targetDate.isNotEmpty) {
tiles.add(ListTile(
title: Text(L10().targetDate),
trailing: Text(widget.order.targetDate),
leading: Icon(TablerIcons.calendar),
));
tiles.add(
ListTile(
title: Text(L10().targetDate),
trailing: Text(widget.order.targetDate),
leading: Icon(TablerIcons.calendar),
),
);
}
if (widget.order.shipmentDate.isNotEmpty) {
tiles.add(ListTile(
title: Text(L10().completionDate),
trailing: Text(widget.order.shipmentDate),
leading: Icon(TablerIcons.calendar),
));
tiles.add(
ListTile(
title: Text(L10().completionDate),
trailing: Text(widget.order.shipmentDate),
leading: Icon(TablerIcons.calendar),
),
);
}
// Responsible "owner"
if (widget.order.responsibleName.isNotEmpty && widget.order.responsibleLabel.isNotEmpty) {
tiles.add(ListTile(
if (widget.order.responsibleName.isNotEmpty &&
widget.order.responsibleLabel.isNotEmpty) {
tiles.add(
ListTile(
title: Text(L10().responsible),
leading: Icon(widget.order.responsibleLabel == "group" ? TablerIcons.users : TablerIcons.user),
trailing: Text(widget.order.responsibleName)
));
leading: Icon(
widget.order.responsibleLabel == "group"
? TablerIcons.users
: TablerIcons.user,
),
trailing: Text(widget.order.responsibleName),
),
);
}
// Notes tile
@@ -446,12 +484,10 @@ class _SalesOrderDetailState extends RefreshableState<SalesOrderDetailWidget> {
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => NotesWidget(widget.order)
)
MaterialPageRoute(builder: (context) => NotesWidget(widget.order)),
);
},
)
),
);
// Attachments
@@ -462,18 +498,18 @@ class _SalesOrderDetailState extends RefreshableState<SalesOrderDetailWidget> {
trailing: attachmentCount > 0 ? Text(attachmentCount.toString()) : null,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => AttachmentWidget(
InvenTreeSalesOrderAttachment(),
widget.order.pk,
widget.order.reference,
widget.order.canEdit
)
)
);
context,
MaterialPageRoute(
builder: (context) => AttachmentWidget(
InvenTreeSalesOrderAttachment(),
widget.order.pk,
widget.order.reference,
widget.order.canEdit,
),
),
);
},
)
),
);
return tiles;
@@ -496,5 +532,4 @@ class _SalesOrderDetailState extends RefreshableState<SalesOrderDetailWidget> {
PaginatedSOShipmentList({"order": widget.order.pk.toString()}),
];
}
}

View File

@@ -1,4 +1,3 @@
import "package:flutter/material.dart";
import "package:flutter_speed_dial/flutter_speed_dial.dart";
import "package:flutter_tabler_icons/flutter_tabler_icons.dart";
@@ -12,20 +11,18 @@ import "package:inventree/api.dart";
import "package:inventree/inventree/company.dart";
import "package:inventree/inventree/model.dart";
class SalesOrderListWidget extends StatefulWidget {
const SalesOrderListWidget({this.filters = const {}, Key? key}) : super(key: key);
const SalesOrderListWidget({this.filters = const {}, Key? key})
: super(key: key);
final Map<String, String> filters;
@override
_SalesOrderListWidgetState createState() => _SalesOrderListWidgetState();
}
class _SalesOrderListWidgetState extends RefreshableState<SalesOrderListWidget> {
class _SalesOrderListWidgetState
extends RefreshableState<SalesOrderListWidget> {
_SalesOrderListWidgetState();
@override
@@ -37,13 +34,13 @@ class _SalesOrderListWidgetState extends RefreshableState<SalesOrderListWidget>
if (InvenTreeSalesOrder().canCreate) {
actions.add(
SpeedDialChild(
child: Icon(TablerIcons.circle_plus),
label: L10().salesOrderCreate,
onTap: () {
_createSalesOrder(context);
}
)
SpeedDialChild(
child: Icon(TablerIcons.circle_plus),
label: L10().salesOrderCreate,
onTap: () {
_createSalesOrder(context);
},
),
);
}
@@ -58,17 +55,17 @@ class _SalesOrderListWidgetState extends RefreshableState<SalesOrderListWidget>
fields.remove("contact");
InvenTreeSalesOrder().createForm(
context,
L10().salesOrderCreate,
fields: fields,
onSuccess: (result) async {
Map<String, dynamic> data = result as Map<String, dynamic>;
context,
L10().salesOrderCreate,
fields: fields,
onSuccess: (result) async {
Map<String, dynamic> data = result as Map<String, dynamic>;
if (data.containsKey("pk")) {
var order = InvenTreeSalesOrder.fromJson(data);
order.goToDetailPage(context);
}
if (data.containsKey("pk")) {
var order = InvenTreeSalesOrder.fromJson(data);
order.goToDetailPage(context);
}
},
);
}
@@ -82,25 +79,22 @@ class _SalesOrderListWidgetState extends RefreshableState<SalesOrderListWidget>
Widget getBody(BuildContext context) {
return PaginatedSalesOrderList(widget.filters);
}
}
class PaginatedSalesOrderList extends PaginatedSearchWidget {
const PaginatedSalesOrderList(Map<String, String> filters) : super(filters: filters);
const PaginatedSalesOrderList(Map<String, String> filters)
: super(filters: filters);
@override
String get searchTitle => L10().salesOrders;
@override
_PaginatedSalesOrderListState createState() => _PaginatedSalesOrderListState();
_PaginatedSalesOrderListState createState() =>
_PaginatedSalesOrderListState();
}
class _PaginatedSalesOrderListState extends PaginatedSearchState<PaginatedSalesOrderList> {
class _PaginatedSalesOrderListState
extends PaginatedSearchState<PaginatedSalesOrderList> {
_PaginatedSalesOrderListState() : super();
@override
@@ -130,21 +124,27 @@ class _PaginatedSalesOrderListState extends PaginatedSearchState<PaginatedSalesO
"label": L10().assignedToMe,
"help_text": L10().assignedToMeDetail,
"tristate": true,
}
},
};
@override
Future<InvenTreePageResponse?> requestPage(int limit, int offset, Map<String, String> params) async {
Future<InvenTreePageResponse?> requestPage(
int limit,
int offset,
Map<String, String> params,
) async {
await InvenTreeAPI().SalesOrderStatus.load();
final page = await InvenTreeSalesOrder().listPaginated(limit, offset, filters: params);
final page = await InvenTreeSalesOrder().listPaginated(
limit,
offset,
filters: params,
);
return page;
}
@override
Widget buildItem(BuildContext context, InvenTreeModel model) {
InvenTreeSalesOrder order = model as InvenTreeSalesOrder;
InvenTreeCompany? customer = order.customer;
@@ -152,18 +152,18 @@ class _PaginatedSalesOrderListState extends PaginatedSearchState<PaginatedSalesO
return ListTile(
title: Text(order.reference),
subtitle: Text(order.description),
leading: customer == null ? null : InvenTreeAPI().getThumbnail(customer.thumbnail),
leading: customer == null
? null
: InvenTreeAPI().getThumbnail(customer.thumbnail),
trailing: Text(
InvenTreeAPI().SalesOrderStatus.label(order.status),
style: TextStyle(
color: InvenTreeAPI().SalesOrderStatus.color(order.status),
)
),
),
onTap: () async {
order.goToDetailPage(context);
}
},
);
}
}
}

View File

@@ -11,40 +11,39 @@ import "package:inventree/widget/paginator.dart";
import "package:inventree/widget/refreshable_state.dart";
import "package:inventree/widget/snacks.dart";
class SOExtraLineListWidget extends StatefulWidget {
const SOExtraLineListWidget(this.order, {this.filters = const {}, Key? key}) : super(key: key);
const SOExtraLineListWidget(this.order, {this.filters = const {}, Key? key})
: super(key: key);
final InvenTreeSalesOrder order;
final Map<String, String> filters;
@override
_SalesOrderExtraLineListWidgetState createState() => _SalesOrderExtraLineListWidgetState();
_SalesOrderExtraLineListWidgetState createState() =>
_SalesOrderExtraLineListWidgetState();
}
class _SalesOrderExtraLineListWidgetState extends RefreshableState<SOExtraLineListWidget> {
class _SalesOrderExtraLineListWidgetState
extends RefreshableState<SOExtraLineListWidget> {
_SalesOrderExtraLineListWidgetState();
@override
String getAppBarTitle() => L10().extraLineItems;
Future<void> _addLineItem(BuildContext context) async {
var fields = InvenTreeSOExtraLineItem().formFields();
fields["order"]?["value"] = widget.order.pk;
InvenTreeSOExtraLineItem().createForm(
context,
L10().lineItemAdd,
fields: fields,
onSuccess: (data) async {
refresh(context);
showSnackIcon(L10().lineItemUpdated, success: true);
}
context,
L10().lineItemAdd,
fields: fields,
onSuccess: (data) async {
refresh(context);
showSnackIcon(L10().lineItemUpdated, success: true);
},
);
}
@@ -54,13 +53,13 @@ class _SalesOrderExtraLineListWidgetState extends RefreshableState<SOExtraLineLi
if (widget.order.canEdit) {
actions.add(
SpeedDialChild(
child: Icon(TablerIcons.circle_plus, color: Colors.green),
label: L10().lineItemAdd,
onTap: () {
_addLineItem(context);
}
)
SpeedDialChild(
child: Icon(TablerIcons.circle_plus, color: Colors.green),
label: L10().lineItemAdd,
onTap: () {
_addLineItem(context);
},
),
);
}
@@ -73,35 +72,41 @@ class _SalesOrderExtraLineListWidgetState extends RefreshableState<SOExtraLineLi
}
}
class PaginatedSOExtraLineList extends PaginatedSearchWidget {
const PaginatedSOExtraLineList(Map<String, String> filters) : super(filters: filters);
const PaginatedSOExtraLineList(Map<String, String> filters)
: super(filters: filters);
@override
String get searchTitle => L10().extraLineItems;
@override
_PaginatedSOExtraLineListState createState() => _PaginatedSOExtraLineListState();
_PaginatedSOExtraLineListState createState() =>
_PaginatedSOExtraLineListState();
}
class _PaginatedSOExtraLineListState extends PaginatedSearchState<PaginatedSOExtraLineList> {
class _PaginatedSOExtraLineListState
extends PaginatedSearchState<PaginatedSOExtraLineList> {
_PaginatedSOExtraLineListState() : super();
@override
String get prefix => "so_extra_line_";
@override
Future<InvenTreePageResponse?> requestPage(int limit, int offset, Map<String, String> params) async {
final page = await InvenTreeSOExtraLineItem().listPaginated(limit, offset, filters: params);
Future<InvenTreePageResponse?> requestPage(
int limit,
int offset,
Map<String, String> params,
) async {
final page = await InvenTreeSOExtraLineItem().listPaginated(
limit,
offset,
filters: params,
);
return page;
}
@override
Widget buildItem(BuildContext context, InvenTreeModel model) {
InvenTreeSOExtraLineItem line = model as InvenTreeSOExtraLineItem;
return ListTile(
@@ -115,4 +120,4 @@ class _PaginatedSOExtraLineListState extends PaginatedSearchState<PaginatedSOExt
},
);
}
}
}

View File

@@ -1,5 +1,3 @@
/*
* Widget for displaying detail view of a single SalesOrderLineItem
*/
@@ -22,21 +20,16 @@ import "package:inventree/l10.dart";
import "package:inventree/helpers.dart";
import "package:inventree/api_form.dart";
class SoLineDetailWidget extends StatefulWidget {
const SoLineDetailWidget(this.item, {Key? key}) : super(key: key);
final InvenTreeSOLineItem item;
@override
_SOLineDetailWidgetState createState() => _SOLineDetailWidgetState();
}
class _SOLineDetailWidgetState extends RefreshableState<SoLineDetailWidget> {
_SOLineDetailWidgetState();
InvenTreeSalesOrder? order;
@@ -51,10 +44,11 @@ class _SOLineDetailWidgetState extends RefreshableState<SoLineDetailWidget> {
if (widget.item.canEdit) {
actions.add(
IconButton(
icon: Icon(TablerIcons.edit),
onPressed: () {
_editLineItem(context);
}),
icon: Icon(TablerIcons.edit),
onPressed: () {
_editLineItem(context);
},
),
);
}
@@ -62,7 +56,6 @@ class _SOLineDetailWidgetState extends RefreshableState<SoLineDetailWidget> {
}
Future<void> _allocateStock(BuildContext context) async {
if (order == null) {
return;
}
@@ -73,12 +66,10 @@ class _SOLineDetailWidgetState extends RefreshableState<SoLineDetailWidget> {
fields["stock_item"]?["filters"] = {
"in_stock": true,
"available": true,
"part": widget.item.partId.toString()
"part": widget.item.partId.toString(),
};
fields["quantity"]?["value"] = widget.item.unallocatedQuantity.toString();
fields["shipment"]?["filters"] = {
"order": order!.pk.toString()
};
fields["shipment"]?["filters"] = {"order": order!.pk.toString()};
launchApiForm(
context,
@@ -89,9 +80,8 @@ class _SOLineDetailWidgetState extends RefreshableState<SoLineDetailWidget> {
icon: TablerIcons.transition_right,
onSuccess: (data) async {
refresh(context);
}
},
);
}
Future<void> _editLineItem(BuildContext context) async {
@@ -109,13 +99,12 @@ class _SOLineDetailWidgetState extends RefreshableState<SoLineDetailWidget> {
onSuccess: (data) async {
refresh(context);
showSnackIcon(L10().lineItemUpdated, success: true);
}
},
);
}
@override
List<SpeedDialChild> actionButtons(BuildContext context) {
List<SpeedDialChild> buttons = [];
if (order != null && order!.isOpen) {
@@ -125,8 +114,8 @@ class _SOLineDetailWidgetState extends RefreshableState<SoLineDetailWidget> {
label: L10().allocateStock,
onTap: () async {
_allocateStock(context);
}
)
},
),
);
}
@@ -138,22 +127,21 @@ class _SOLineDetailWidgetState extends RefreshableState<SoLineDetailWidget> {
List<SpeedDialChild> actions = [];
if (order != null && order!.isOpen && InvenTreeSOLineItem().canCreate) {
if (api.supportsBarcodeSOAllocateEndpoint) {
actions.add(
SpeedDialChild(
child: Icon(TablerIcons.transition_right),
label: L10().allocateStock,
onTap: () async {
scanBarcode(
context,
handler: SOAllocateStockHandler(
salesOrder: order,
lineItem: widget.item
)
);
}
)
SpeedDialChild(
child: Icon(TablerIcons.transition_right),
label: L10().allocateStock,
onTap: () async {
scanBarcode(
context,
handler: SOAllocateStockHandler(
salesOrder: order,
lineItem: widget.item,
),
);
},
),
);
}
}
@@ -193,8 +181,8 @@ class _SOLineDetailWidgetState extends RefreshableState<SoLineDetailWidget> {
if (part is InvenTreePart) {
part.goToDetailPage(context);
}
}
)
},
),
);
// Available quantity
@@ -202,8 +190,8 @@ class _SOLineDetailWidgetState extends RefreshableState<SoLineDetailWidget> {
ListTile(
title: Text(L10().availableStock),
leading: Icon(TablerIcons.packages),
trailing: Text(simpleNumberString(widget.item.availableStock))
)
trailing: Text(simpleNumberString(widget.item.availableStock)),
),
);
// Allocated quantity
@@ -215,10 +203,10 @@ class _SOLineDetailWidgetState extends RefreshableState<SoLineDetailWidget> {
trailing: Text(
widget.item.allocatedString,
style: TextStyle(
color: widget.item.isAllocated ? COLOR_SUCCESS : COLOR_WARNING
)
)
)
color: widget.item.isAllocated ? COLOR_SUCCESS : COLOR_WARNING,
),
),
),
);
// Shipped quantity
@@ -229,11 +217,11 @@ class _SOLineDetailWidgetState extends RefreshableState<SoLineDetailWidget> {
trailing: Text(
widget.item.progressString,
style: TextStyle(
color: widget.item.isComplete ? COLOR_SUCCESS : COLOR_WARNING
color: widget.item.isComplete ? COLOR_SUCCESS : COLOR_WARNING,
),
),
leading: Icon(TablerIcons.truck)
)
leading: Icon(TablerIcons.truck),
),
);
// Reference
@@ -242,36 +230,36 @@ class _SOLineDetailWidgetState extends RefreshableState<SoLineDetailWidget> {
ListTile(
title: Text(L10().reference),
subtitle: Text(widget.item.reference),
leading: Icon(TablerIcons.hash)
)
leading: Icon(TablerIcons.hash),
),
);
}
// Note
if (widget.item.notes.isNotEmpty) {
tiles.add(
ListTile(
title: Text(L10().notes),
subtitle: Text(widget.item.notes),
leading: Icon(TablerIcons.note),
)
ListTile(
title: Text(L10().notes),
subtitle: Text(widget.item.notes),
leading: Icon(TablerIcons.note),
),
);
}
// External link
if (widget.item.link.isNotEmpty) {
tiles.add(
ListTile(
title: Text(L10().link),
subtitle: Text(widget.item.link),
leading: Icon(TablerIcons.link, color: COLOR_ACTION),
onTap: () async {
await openLink(widget.item.link);
},
)
ListTile(
title: Text(L10().link),
subtitle: Text(widget.item.link),
leading: Icon(TablerIcons.link, color: COLOR_ACTION),
onTap: () async {
await openLink(widget.item.link);
},
),
);
}
return tiles;
}
}
}

View File

@@ -9,28 +9,26 @@ import "package:inventree/api.dart";
import "package:inventree/app_colors.dart";
import "package:inventree/widget/progress.dart";
/*
* Paginated widget class for displaying a list of sales order line items
*/
class PaginatedSOLineList extends PaginatedSearchWidget {
const PaginatedSOLineList(Map<String, String> filters) : super(filters: filters);
const PaginatedSOLineList(Map<String, String> filters)
: super(filters: filters);
@override
String get searchTitle => L10().lineItems;
@override
_PaginatedSOLineListState createState() => _PaginatedSOLineListState();
}
/*
* State class for PaginatedSOLineList
*/
class _PaginatedSOLineListState extends PaginatedSearchState<PaginatedSOLineList> {
class _PaginatedSOLineListState
extends PaginatedSearchState<PaginatedSOLineList> {
_PaginatedSOLineListState() : super();
@override
@@ -43,13 +41,19 @@ class _PaginatedSOLineListState extends PaginatedSearchState<PaginatedSOLineList
};
@override
Map<String, Map<String, dynamic>> get filterOptions => {
};
Map<String, Map<String, dynamic>> get filterOptions => {};
@override
Future<InvenTreePageResponse?> requestPage(int limit, int offset, Map<String, String> params) async {
final page = await InvenTreeSOLineItem().listPaginated(limit, offset, filters: params);
Future<InvenTreePageResponse?> requestPage(
int limit,
int offset,
Map<String, String> params,
) async {
final page = await InvenTreeSOLineItem().listPaginated(
limit,
offset,
filters: params,
);
return page;
}
@@ -63,24 +67,30 @@ class _PaginatedSOLineListState extends PaginatedSearchState<PaginatedSOLineList
title: Text(part.name),
subtitle: Text(part.description),
leading: InvenTreeAPI().getThumbnail(part.thumbnail),
trailing: Text(item.progressString, style: TextStyle(color: item.isComplete ? COLOR_SUCCESS : COLOR_WARNING)),
trailing: Text(
item.progressString,
style: TextStyle(
color: item.isComplete ? COLOR_SUCCESS : COLOR_WARNING,
),
),
onTap: () async {
showLoadingOverlay();
await item.reload();
hideLoadingOverlay();
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SoLineDetailWidget(item))
MaterialPageRoute(builder: (context) => SoLineDetailWidget(item)),
);
}
},
);
} else {
return ListTile(
title: Text(L10().error),
subtitle: Text("Missing part detail", style: TextStyle(color: COLOR_DANGER)),
subtitle: Text(
"Missing part detail",
style: TextStyle(color: COLOR_DANGER),
),
);
}
}
}

View File

@@ -1,4 +1,3 @@
import "package:flutter/material.dart";
import "package:flutter_tabler_icons/flutter_tabler_icons.dart";
import "package:inventree/app_colors.dart";
@@ -9,19 +8,19 @@ import "package:inventree/inventree/model.dart";
import "package:inventree/l10.dart";
class PaginatedSOShipmentList extends PaginatedSearchWidget {
const PaginatedSOShipmentList(Map<String, String> filters) : super(filters: filters);
const PaginatedSOShipmentList(Map<String, String> filters)
: super(filters: filters);
@override
String get searchTitle => L10().shipments;
@override
_PaginatedSOShipmentListState createState() => _PaginatedSOShipmentListState();
_PaginatedSOShipmentListState createState() =>
_PaginatedSOShipmentListState();
}
class _PaginatedSOShipmentListState extends PaginatedSearchState<PaginatedSOShipmentList> {
class _PaginatedSOShipmentListState
extends PaginatedSearchState<PaginatedSOShipmentList> {
_PaginatedSOShipmentListState() : super();
@override
@@ -34,22 +33,30 @@ class _PaginatedSOShipmentListState extends PaginatedSearchState<PaginatedSOShip
Map<String, Map<String, dynamic>> get filterOptions => {};
@override
Future<InvenTreePageResponse?> requestPage(int limit, int offset, Map<String, String> params) async {
final page = await InvenTreeSalesOrderShipment().listPaginated(limit, offset, filters: params);
Future<InvenTreePageResponse?> requestPage(
int limit,
int offset,
Map<String, String> params,
) async {
final page = await InvenTreeSalesOrderShipment().listPaginated(
limit,
offset,
filters: params,
);
return page;
}
@override
Widget buildItem(BuildContext context, InvenTreeModel model) {
InvenTreeSalesOrderShipment shipment = model as InvenTreeSalesOrderShipment;
return ListTile(
title: Text(shipment.reference),
subtitle: Text(shipment.tracking_number),
leading: shipment.shipped ? Icon(TablerIcons.calendar_check, color: COLOR_SUCCESS) : Icon(TablerIcons.calendar_cancel, color: COLOR_WARNING),
trailing: shipment.shipped ? Text(shipment.shipment_date ?? "") : null
leading: shipment.shipped
? Icon(TablerIcons.calendar_check, color: COLOR_SUCCESS)
: Icon(TablerIcons.calendar_cancel, color: COLOR_WARNING),
trailing: shipment.shipped ? Text(shipment.shipment_date ?? "") : null,
);
}
}
}

View File

@@ -15,12 +15,10 @@ import "package:inventree/preferences.dart";
import "package:inventree/widget/refreshable_state.dart";
/*
* Abstract base widget class for rendering a PaginatedSearchState
*/
abstract class PaginatedSearchWidget extends StatefulWidget {
const PaginatedSearchWidget({this.filters = const {}, this.title = ""});
final String title;
@@ -30,12 +28,12 @@ abstract class PaginatedSearchWidget extends StatefulWidget {
final Map<String, String> filters;
}
/*
* Generic stateful widget for displaying paginated data retrieved via the API
*/
abstract class PaginatedSearchState<T extends PaginatedSearchWidget> extends State<T> with BaseWidgetProperties {
abstract class PaginatedSearchState<T extends PaginatedSearchWidget>
extends State<T>
with BaseWidgetProperties {
static const _pageSize = 25;
bool showSearchWidget = false;
@@ -73,7 +71,6 @@ abstract class PaginatedSearchState<T extends PaginatedSearchWidget> extends Sta
// Construct the boolean filter options for this list
Future<Map<String, String>> constructFilters() async {
Map<String, String> f = {};
for (String k in filterOptions.keys) {
@@ -95,7 +92,10 @@ abstract class PaginatedSearchState<T extends PaginatedSearchWidget> extends Sta
// Return the selected ordering "field" for this list widget
Future<String> orderingField() async {
dynamic field = await InvenTreeSettingsManager().getValue("${prefix}ordering_field", null);
dynamic field = await InvenTreeSettingsManager().getValue(
"${prefix}ordering_field",
null,
);
if (field != null && orderingOptions.containsKey(field.toString())) {
// A valid ordering field has been found
@@ -110,7 +110,10 @@ abstract class PaginatedSearchState<T extends PaginatedSearchWidget> extends Sta
// Return the selected ordering "order" ("+" or "-") for this list widget
Future<String> orderingOrder() async {
dynamic order = await InvenTreeSettingsManager().getValue("${prefix}ordering_order", "+");
dynamic order = await InvenTreeSettingsManager().getValue(
"${prefix}ordering_order",
"+",
);
return order == "+" ? "+" : "-";
}
@@ -137,10 +140,10 @@ abstract class PaginatedSearchState<T extends PaginatedSearchWidget> extends Sta
// Construct the 'ordering' options
List<Map<String, dynamic>> _opts = [];
orderingOptions.forEach((k, v) => _opts.add({
"value": k.toString(),
"display_name": v.toString()
}));
orderingOptions.forEach(
(k, v) =>
_opts.add({"value": k.toString(), "display_name": v.toString()}),
);
if (_field == null && _opts.isNotEmpty) {
_field = _opts.first["value"];
@@ -160,16 +163,10 @@ abstract class PaginatedSearchState<T extends PaginatedSearchWidget> extends Sta
"required": true,
"value": _order,
"choices": [
{
"value": "+",
"display_name": "Ascending",
},
{
"value": "-",
"display_name": "Descending",
}
]
}
{"value": "+", "display_name": "Ascending"},
{"value": "-", "display_name": "Descending"},
],
},
};
// Add in selected filter options
@@ -219,7 +216,6 @@ abstract class PaginatedSearchState<T extends PaginatedSearchWidget> extends Sta
fields,
icon: TablerIcons.circle_check,
onSuccess: (Map<String, dynamic> data) async {
// Extract data from the processed form
String f = (data["ordering_field"] ?? _field) as String;
String o = (data["ordering_order"] ?? _order) as String;
@@ -235,7 +231,7 @@ abstract class PaginatedSearchState<T extends PaginatedSearchWidget> extends Sta
// Refresh data from the server
_pagingController.refresh();
}
},
);
}
@@ -245,7 +241,6 @@ abstract class PaginatedSearchState<T extends PaginatedSearchWidget> extends Sta
int resultCount = 0;
String resultsString() {
if (resultCount <= 0) {
return noResultsText;
} else {
@@ -260,7 +255,8 @@ abstract class PaginatedSearchState<T extends PaginatedSearchWidget> extends Sta
Timer? _debounceTimer;
// Pagination controller
final PagingController<int, InvenTreeModel> _pagingController = PagingController(firstPageKey: 0);
final PagingController<int, InvenTreeModel> _pagingController =
PagingController(firstPageKey: 0);
void refresh() {
_pagingController.refresh();
@@ -286,8 +282,11 @@ abstract class PaginatedSearchState<T extends PaginatedSearchWidget> extends Sta
* Each implementing class must override this function,
* and return an InvenTreePageResponse object with the correct data format
*/
Future<InvenTreePageResponse?> requestPage(int limit, int offset, Map<String, String> params) async {
Future<InvenTreePageResponse?> requestPage(
int limit,
int offset,
Map<String, String> params,
) async {
// Default implementation returns null - must be overridden
return null;
}
@@ -301,7 +300,6 @@ abstract class PaginatedSearchState<T extends PaginatedSearchWidget> extends Sta
// Include user search term
if (searchTerm.isNotEmpty) {
String _search = searchTerm;
// Include original search in search test
@@ -329,11 +327,7 @@ abstract class PaginatedSearchState<T extends PaginatedSearchWidget> extends Sta
params.addAll(f);
}
final page = await requestPage(
_pageSize,
pageKey,
params
);
final page = await requestPage(_pageSize, pageKey, params);
// We may have disposed of the widget while the request was in progress
// If this is the case, abort
@@ -350,7 +344,7 @@ abstract class PaginatedSearchState<T extends PaginatedSearchWidget> extends Sta
if (page != null) {
for (var result in page.results) {
items.add(result);
items.add(result);
}
}
@@ -367,16 +361,12 @@ abstract class PaginatedSearchState<T extends PaginatedSearchWidget> extends Sta
} catch (error, stackTrace) {
_pagingController.error = error;
sentryReportError(
"paginator.fetchPage",
error, stackTrace,
);
sentryReportError("paginator.fetchPage", error, stackTrace);
}
}
// Callback function when the search term is updated
void updateSearchTerm() {
if (searchTerm == searchController.text) {
// No change
return;
@@ -410,7 +400,6 @@ abstract class PaginatedSearchState<T extends PaginatedSearchWidget> extends Sta
// Function to construct a single paginated item
// Must be overridden in an implementing subclass
Widget buildItem(BuildContext context, InvenTreeModel item) {
// This method must be overridden by the child class
return ListTile(
title: Text("*** UNIMPLEMENTED ***"),
@@ -423,12 +412,8 @@ abstract class PaginatedSearchState<T extends PaginatedSearchWidget> extends Sta
String get noResultsText => L10().noResults;
@override
Widget build (BuildContext context) {
List<Widget> children = [
buildTitleWidget(context),
Divider(),
];
Widget build(BuildContext context) {
List<Widget> children = [buildTitleWidget(context), Divider()];
if (showSearchWidget) {
children.add(buildSearchInput(context));
@@ -436,26 +421,26 @@ abstract class PaginatedSearchState<T extends PaginatedSearchWidget> extends Sta
children.add(
Expanded(
child: CustomScrollView(
shrinkWrap: true,
physics: AlwaysScrollableScrollPhysics(),
scrollDirection: Axis.vertical,
slivers: <Widget>[
PagedSliverList.separated(
pagingController: _pagingController,
builderDelegate: PagedChildBuilderDelegate<InvenTreeModel>(
itemBuilder: (ctx, item, index) {
return buildItem(ctx, item);
},
noItemsFoundIndicatorBuilder: (context) {
return NoResultsWidget(noResultsText);
}
),
separatorBuilder: (context, item) => const Divider(height: 1),
)
]
)
)
child: CustomScrollView(
shrinkWrap: true,
physics: AlwaysScrollableScrollPhysics(),
scrollDirection: Axis.vertical,
slivers: <Widget>[
PagedSliverList.separated(
pagingController: _pagingController,
builderDelegate: PagedChildBuilderDelegate<InvenTreeModel>(
itemBuilder: (ctx, item, index) {
return buildItem(ctx, item);
},
noItemsFoundIndicatorBuilder: (context) {
return NoResultsWidget(noResultsText);
},
),
separatorBuilder: (context, item) => const Divider(height: 1),
),
],
),
),
);
return RefreshIndicator(
@@ -473,28 +458,34 @@ abstract class PaginatedSearchState<T extends PaginatedSearchWidget> extends Sta
* Build the title widget for this list
*/
Widget buildTitleWidget(BuildContext context) {
const double icon_size = 32;
List<Widget> _icons = [];
if (filterOptions.isNotEmpty || orderingOptions.isNotEmpty) {
_icons.add(IconButton(
onPressed: () async {
_setOrderingOptions(context);
},
icon: Icon(Icons.filter_alt, size: icon_size)
));
_icons.add(
IconButton(
onPressed: () async {
_setOrderingOptions(context);
},
icon: Icon(Icons.filter_alt, size: icon_size),
),
);
}
_icons.add(IconButton(
_icons.add(
IconButton(
onPressed: () {
setState(() {
showSearchWidget = !showSearchWidget;
});
},
icon: Icon(showSearchWidget ? Icons.zoom_out : Icons.search, size: icon_size)
));
icon: Icon(
showSearchWidget ? Icons.zoom_out : Icons.search,
size: icon_size,
),
),
);
// _icons.add(IconButton(
// onPressed: () async {
@@ -506,20 +497,13 @@ abstract class PaginatedSearchState<T extends PaginatedSearchWidget> extends Sta
return ListTile(
title: Text(
widget.searchTitle,
style: TextStyle(
fontWeight: FontWeight.bold,
),
style: TextStyle(fontWeight: FontWeight.bold),
),
subtitle: Text(
"${L10().results}: ${resultCount}",
style: TextStyle(
fontStyle: FontStyle.italic
),
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: _icons,
style: TextStyle(fontStyle: FontStyle.italic),
),
trailing: Row(mainAxisSize: MainAxisSize.min, children: _icons),
);
}
@@ -530,7 +514,9 @@ abstract class PaginatedSearchState<T extends PaginatedSearchWidget> extends Sta
return ListTile(
trailing: GestureDetector(
child: Icon(
searchController.text.isEmpty ? TablerIcons.search : TablerIcons.backspace,
searchController.text.isEmpty
? TablerIcons.search
: TablerIcons.backspace,
color: searchController.text.isNotEmpty ? COLOR_DANGER : COLOR_ACTION,
),
onTap: () {
@@ -545,31 +531,22 @@ abstract class PaginatedSearchState<T extends PaginatedSearchWidget> extends Sta
onChanged: (value) {
updateSearchTerm();
},
decoration: InputDecoration(
hintText: L10().search,
),
)
decoration: InputDecoration(hintText: L10().search),
),
);
}
}
class NoResultsWidget extends StatelessWidget {
const NoResultsWidget(this.description);
final String description;
@override
Widget build(BuildContext context) {
return ListTile(
title: Text(
description,
style: TextStyle(fontStyle: FontStyle.italic),
),
title: Text(description, style: TextStyle(fontStyle: FontStyle.italic)),
leading: Icon(TablerIcons.exclamation_circle, color: COLOR_WARNING),
);
}
}

View File

@@ -1,4 +1,3 @@
import "package:flutter/material.dart";
import "package:flutter_tabler_icons/flutter_tabler_icons.dart";
@@ -14,13 +13,15 @@ import "package:inventree/widget/paginator.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);
const BillOfMaterialsWidget(
this.part, {
this.isParentComponent = true,
Key? key,
}) : super(key: key);
final InvenTreePart part;
@@ -53,12 +54,11 @@ class _BillOfMaterialsState extends RefreshableState<BillOfMaterialsWidget> {
showFilterOptions = !showFilterOptions;
});
},
)
),
];
@override
Widget getBody(BuildContext context) {
Map<String, String> filters = {};
if (widget.isParentComponent) {
@@ -72,7 +72,11 @@ class _BillOfMaterialsState extends RefreshableState<BillOfMaterialsWidget> {
ListTile(
leading: InvenTreeAPI().getThumbnail(widget.part.thumbnail),
title: Text(widget.part.fullname),
subtitle: Text(widget.isParentComponent ? L10().billOfMaterials : L10().usedInDetails),
subtitle: Text(
widget.isParentComponent
? L10().billOfMaterials
: L10().usedInDetails,
),
trailing: Text(L10().quantity),
),
Divider(thickness: 1.25),
@@ -87,13 +91,14 @@ class _BillOfMaterialsState extends RefreshableState<BillOfMaterialsWidget> {
}
}
/*
* 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);
const PaginatedBomList(
Map<String, String> filters, {
this.isParentPart = true,
}) : super(filters: filters);
final bool isParentPart;
@@ -104,9 +109,7 @@ class PaginatedBomList extends PaginatedSearchWidget {
_PaginatedBomListState createState() => _PaginatedBomListState();
}
class _PaginatedBomListState extends PaginatedSearchState<PaginatedBomList> {
_PaginatedBomListState() : super();
@override
@@ -123,23 +126,31 @@ class _PaginatedBomListState extends PaginatedSearchState<PaginatedBomList> {
"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);
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;
InvenTreePart? subPart = widget.isParentPart
? bomItem.subPart
: bomItem.part;
String title = subPart?.fullname ?? "error - no name";
@@ -151,16 +162,17 @@ class _PaginatedBomListState extends PaginatedSearchState<PaginatedBomList> {
style: TextStyle(fontWeight: FontWeight.bold),
),
leading: InvenTreeAPI().getThumbnail(subPart?.thumbnail ?? ""),
onTap: subPart == null ? null : () async {
onTap: subPart == null
? null
: () async {
showLoadingOverlay();
var part = await InvenTreePart().get(subPart.pk);
hideLoadingOverlay();
showLoadingOverlay();
var part = await InvenTreePart().get(subPart.pk);
hideLoadingOverlay();
if (part is InvenTreePart) {
part.goToDetailPage(context);
}
},
if (part is InvenTreePart) {
part.goToDetailPage(context);
}
},
);
}
}
}

View File

@@ -13,9 +13,7 @@ import "package:inventree/widget/progress.dart";
import "package:inventree/widget/snacks.dart";
import "package:inventree/widget/refreshable_state.dart";
class CategoryDisplayWidget extends StatefulWidget {
const CategoryDisplayWidget(this.category, {Key? key}) : super(key: key);
final InvenTreePartCategory? category;
@@ -24,9 +22,7 @@ class CategoryDisplayWidget extends StatefulWidget {
_CategoryDisplayState createState() => _CategoryDisplayState();
}
class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
_CategoryDisplayState();
@override
@@ -40,12 +36,12 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
if (InvenTreePartCategory().canEdit) {
actions.add(
IconButton(
icon: Icon(TablerIcons.edit),
icon: Icon(TablerIcons.edit),
tooltip: L10().editCategory,
onPressed: () {
_editCategoryDialog(context);
},
)
),
);
}
}
@@ -58,13 +54,13 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
List<SpeedDialChild> actions = [];
if (InvenTreePart().canCreate) {
actions.add(
SpeedDialChild(
child: Icon(TablerIcons.box),
label: L10().partCreateDetail,
onTap: _newPart,
)
);
actions.add(
SpeedDialChild(
child: Icon(TablerIcons.box),
label: L10().partCreateDetail,
onTap: _newPart,
),
);
}
if (InvenTreePartCategory().canCreate) {
@@ -74,8 +70,8 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
label: L10().categoryCreateDetail,
onTap: () {
_newCategory(context);
}
)
},
),
);
}
@@ -91,12 +87,12 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
}
_cat.editForm(
context,
L10().editCategory,
onSuccess: (data) async {
refresh(context);
showSnackIcon(L10().categoryUpdated, success: true);
}
context,
L10().editCategory,
onSuccess: (data) async {
refresh(context);
showSnackIcon(L10().categoryUpdated, success: true);
},
);
}
@@ -107,7 +103,6 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
@override
Future<void> request(BuildContext context) async {
// Update the category
if (widget.category != null) {
final bool result = await widget.category?.reload() ?? false;
@@ -126,79 +121,69 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
title: Text(
L10().partCategoryTopLevel,
style: TextStyle(fontStyle: FontStyle.italic),
)
)
),
),
);
} else {
List<Widget> children = [
ListTile(
title: Text("${widget.category?.name}",
style: TextStyle(fontWeight: FontWeight.bold)
title: Text(
"${widget.category?.name}",
style: TextStyle(fontWeight: FontWeight.bold),
),
subtitle: Text("${widget.category?.description}"),
leading: widget.category!.customIcon != null ? Icon(widget.category!.customIcon) : Icon(TablerIcons.sitemap)
leading: widget.category!.customIcon != null
? Icon(widget.category!.customIcon)
: Icon(TablerIcons.sitemap),
),
];
if (extra) {
children.add(
ListTile(
title: Text(L10().parentCategory),
subtitle: Text("${widget.category?.parentPathString}"),
leading: Icon(
TablerIcons.arrow_move_up,
color: COLOR_ACTION,
),
onTap: () async {
ListTile(
title: Text(L10().parentCategory),
subtitle: Text("${widget.category?.parentPathString}"),
leading: Icon(TablerIcons.arrow_move_up, color: COLOR_ACTION),
onTap: () async {
int parentId = widget.category?.parentId ?? -1;
int parentId = widget.category?.parentId ?? -1;
if (parentId < 0) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => CategoryDisplayWidget(null),
),
);
} else {
showLoadingOverlay();
var cat = await InvenTreePartCategory().get(parentId);
hideLoadingOverlay();
if (parentId < 0) {
Navigator.push(context, MaterialPageRoute(builder: (context) => CategoryDisplayWidget(null)));
} else {
showLoadingOverlay();
var cat = await InvenTreePartCategory().get(parentId);
hideLoadingOverlay();
if (cat is InvenTreePartCategory) {
cat.goToDetailPage(context);
}
if (cat is InvenTreePartCategory) {
cat.goToDetailPage(context);
}
},
)
}
},
),
);
}
return Card(
child: Column(
children: children
),
);
return Card(child: Column(children: children));
}
}
@override
List<Widget> getTabIcons(BuildContext context) {
return [
Tab(text: L10().details),
Tab(text: L10().parts),
];
return [Tab(text: L10().details), Tab(text: L10().parts)];
}
@override
List<Widget> getTabs(BuildContext context) {
return [
Column(children: detailTiles()),
Column(children: partsTiles()),
];
return [Column(children: detailTiles()), Column(children: partsTiles())];
}
// Construct the "details" panel
List<Widget> detailTiles() {
Map<String, String> filters = {};
int? parent = widget.category?.pk;
@@ -212,12 +197,9 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
List<Widget> tiles = <Widget>[
getCategoryDescriptionCard(),
Expanded(
child: PaginatedPartCategoryList(
filters,
title: L10().subcategories,
),
child: PaginatedPartCategoryList(filters, title: L10().subcategories),
flex: 10,
)
),
];
return tiles;
@@ -225,31 +207,21 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
// Construct the "parts" panel
List<Widget> partsTiles() {
Map<String, String> filters = {
"category": widget.category?.pk.toString() ?? "null",
};
return [
Expanded(
child: PaginatedPartList(filters),
flex: 10,
)
];
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,
},
data: {"parent": (pk > 0) ? pk : null},
onSuccess: (result) async {
Map<String, dynamic> data = result as Map<String, dynamic>;
if (data.containsKey("pk")) {
@@ -260,29 +232,25 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
} else {
refresh(context);
}
}
},
);
}
Future<void> _newPart() async {
int pk = widget.category?.pk ?? -1;
InvenTreePart().createForm(
context,
L10().partCreate,
data: {
"category": (pk > 0) ? pk : null
},
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);
part.goToDetailPage(context);
}
}
},
);
}
}

View File

@@ -9,19 +9,15 @@ 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
@@ -34,19 +30,21 @@ class _PartCategoryListState extends RefreshableState<PartCategoryList> {
}
class PaginatedPartCategoryList extends PaginatedSearchWidget {
const PaginatedPartCategoryList(Map<String, String> filters, {String title = ""}) : super(filters: filters, title: title);
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();
_PaginatedPartCategoryListState createState() =>
_PaginatedPartCategoryListState();
}
class _PaginatedPartCategoryListState extends PaginatedSearchState<PaginatedPartCategoryList> {
class _PaginatedPartCategoryListState
extends PaginatedSearchState<PaginatedPartCategoryList> {
// _PaginatedPartCategoryListState(Map<String, String> filters, bool searchEnabled) : super(filters, searchEnabled);
@override
@@ -59,16 +57,12 @@ class _PaginatedPartCategoryListState extends PaginatedSearchState<PaginatedPart
"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,
};
Map<String, String> options = {"name": L10().name, "level": L10().level};
// Note: API v69 changed 'parts' to 'part_count'
if (InvenTreeAPI().apiVersion >= 69) {
@@ -81,16 +75,22 @@ class _PaginatedPartCategoryListState extends PaginatedSearchState<PaginatedPart
}
@override
Future<InvenTreePageResponse?> requestPage(int limit, int offset, Map<String, String> params) async {
final page = await InvenTreePartCategory().listPaginated(limit, offset, filters: params);
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(
@@ -103,4 +103,4 @@ class _PaginatedPartCategoryListState extends PaginatedSearchState<PaginatedPart
},
);
}
}
}

View File

@@ -27,24 +27,19 @@ import "package:inventree/widget/snacks.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;
@@ -76,13 +71,13 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
if (InvenTreePart().canEdit) {
actions.add(
IconButton(
icon: Icon(TablerIcons.edit),
tooltip: L10().editPart,
onPressed: () {
_editPartDialog(context);
}
)
IconButton(
icon: Icon(TablerIcons.edit),
tooltip: L10().editPart,
onPressed: () {
_editPartDialog(context);
},
),
);
}
return actions;
@@ -94,11 +89,13 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
if (InvenTreePart().canEdit) {
actions.add(
customBarcodeAction(
context, this,
widget.part.customBarcode, "part",
widget.part.pk
)
customBarcodeAction(
context,
this,
widget.part.customBarcode,
"part",
widget.part.pk,
),
);
}
@@ -111,13 +108,13 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
if (InvenTreeStockItem().canCreate) {
actions.add(
SpeedDialChild(
child: Icon(TablerIcons.packages),
label: L10().stockItemCreate,
onTap: () {
_newStockItem(context);
}
)
SpeedDialChild(
child: Icon(TablerIcons.packages),
label: L10().stockItemCreate,
onTap: () {
_newStockItem(context);
},
),
);
}
@@ -132,10 +129,10 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
labels,
widget.part.pk,
"part",
"part=${widget.part.pk}"
"part=${widget.part.pk}",
);
}
)
},
),
);
}
@@ -153,14 +150,22 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
@override
Future<void> request(BuildContext context) async {
final bool result = await part.reload();
// Load page settings from local storage
showPricing = await InvenTreeSettingsManager().getBool(INV_PART_SHOW_PRICING, true);
showParameters = await InvenTreeSettingsManager().getBool(INV_PART_SHOW_PARAMETERS, true);
showPricing = await InvenTreeSettingsManager().getBool(
INV_PART_SHOW_PRICING,
true,
);
showParameters = await InvenTreeSettingsManager().getBool(
INV_PART_SHOW_PARAMETERS,
true,
);
showBom = await InvenTreeSettingsManager().getBool(INV_PART_SHOW_BOM, true);
allowLabelPrinting = await InvenTreeSettingsManager().getBool(INV_ENABLE_LABEL_PRINTING, true);
allowLabelPrinting = await InvenTreeSettingsManager().getBool(
INV_ENABLE_LABEL_PRINTING,
true,
);
if (!result || part.pk == -1) {
// Part could not be loaded, for some reason
@@ -211,11 +216,9 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
}
// Request the number of BOM items
InvenTreePart().count(
filters: {
"in_bom_for": part.pk.toString(),
}
).then((int value) {
InvenTreePart().count(filters: {"in_bom_for": part.pk.toString()}).then((
int value,
) {
if (mounted) {
setState(() {
bomCount = value;
@@ -224,11 +227,9 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
});
// Request number of "used in" parts
InvenTreeBomItem().count(
filters: {
"uses": part.pk.toString(),
}
).then((int value) {
InvenTreeBomItem().count(filters: {"uses": part.pk.toString()}).then((
int value,
) {
if (mounted) {
setState(() {
usedInCount = value;
@@ -237,11 +238,9 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
});
// Request the number of variant items
InvenTreePart().count(
filters: {
"variant_of": part.pk.toString(),
}
).then((int value) {
InvenTreePart().count(filters: {"variant_of": part.pk.toString()}).then((
int value,
) {
if (mounted) {
setState(() {
variantCount = value;
@@ -253,16 +252,14 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
allowLabelPrinting &= api.supportsMixin("labels");
if (allowLabelPrinting) {
String model_type = api.supportsModernLabelPrinting ? InvenTreePart.MODEL_TYPE : "part";
String model_type = api.supportsModernLabelPrinting
? InvenTreePart.MODEL_TYPE
: "part";
String item_key = api.supportsModernLabelPrinting ? "items" : "part";
_labels = await getLabelTemplates(
model_type,
{
item_key: widget.part.pk.toString()
}
);
_labels = await getLabelTemplates(model_type, {
item_key: widget.part.pk.toString(),
});
}
if (mounted) {
@@ -273,41 +270,34 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
}
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);
});
}),
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);
});
},
),
),
);
}
@@ -315,13 +305,10 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
* Build a list of tiles to display under the part description
*/
List<Widget> partTiles() {
List<Widget> tiles = [];
// Image / name / description
tiles.add(
headerTile()
);
tiles.add(headerTile());
if (loading) {
tiles.add(progressIndicator());
@@ -331,23 +318,13 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
if (!part.isActive) {
tiles.add(
ListTile(
title: Text(
L10().inactive,
style: TextStyle(
color: COLOR_DANGER
)
),
title: Text(L10().inactive, style: TextStyle(color: COLOR_DANGER)),
subtitle: Text(
L10().inactiveDetail,
style: TextStyle(
color: COLOR_DANGER
)
style: TextStyle(color: COLOR_DANGER),
),
leading: Icon(
TablerIcons.exclamation_circle,
color: COLOR_DANGER
),
)
leading: Icon(TablerIcons.exclamation_circle, color: COLOR_DANGER),
),
);
}
@@ -356,15 +333,11 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
ListTile(
title: Text(L10().templatePart),
subtitle: Text(parentPart!.fullname),
leading: api.getImage(
parentPart!.thumbnail,
width: 32,
height: 32,
),
leading: api.getImage(parentPart!.thumbnail, width: 32, height: 32),
onTap: () {
parentPart?.goToDetailPage(context);
}
)
},
),
);
}
@@ -372,58 +345,58 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
if (part.categoryName.isNotEmpty) {
tiles.add(
ListTile(
title: Text(L10().partCategory),
subtitle: Text("${part.categoryName}"),
leading: Icon(TablerIcons.sitemap, color: COLOR_ACTION),
onTap: () async {
if (part.categoryId > 0) {
title: Text(L10().partCategory),
subtitle: Text("${part.categoryName}"),
leading: Icon(TablerIcons.sitemap, color: COLOR_ACTION),
onTap: () async {
if (part.categoryId > 0) {
showLoadingOverlay();
var cat = await InvenTreePartCategory().get(part.categoryId);
hideLoadingOverlay();
showLoadingOverlay();
var cat = await InvenTreePartCategory().get(part.categoryId);
hideLoadingOverlay();
if (cat is InvenTreePartCategory) {
cat.goToDetailPage(context);
}
if (cat is InvenTreePartCategory) {
cat.goToDetailPage(context);
}
},
)
}
},
),
);
} else {
tiles.add(
ListTile(
title: Text(L10().partCategory),
subtitle: Text(L10().partCategoryTopLevel),
leading: Icon(TablerIcons.sitemap, color: COLOR_ACTION),
onTap: () {
Navigator.push(context, MaterialPageRoute(
builder: (context) => CategoryDisplayWidget(null)));
},
)
ListTile(
title: Text(L10().partCategory),
subtitle: Text(L10().partCategoryTopLevel),
leading: Icon(TablerIcons.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: Icon(TablerIcons.versions, color: COLOR_ACTION),
trailing: Text(variantCount.toString()),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => PartList(
{
"variant_of": part.pk.toString(),
},
title: L10().variants
)
)
);
},
)
ListTile(
title: Text(L10().variants),
leading: Icon(TablerIcons.versions, color: COLOR_ACTION),
trailing: Text(variantCount.toString()),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => PartList({
"variant_of": part.pk.toString(),
}, title: L10().variants),
),
);
},
),
);
}
@@ -434,19 +407,16 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
leading: Icon(TablerIcons.packages),
trailing: Text(
part.stockString(),
style: TextStyle(
fontWeight: FontWeight.bold,
),
style: TextStyle(fontWeight: FontWeight.bold),
),
),
);
if (showPricing && partPricing != null) {
String pricing = formatPriceRange(
partPricing?.overallMin,
partPricing?.overallMax,
currency: partPricing?.currency
currency: partPricing?.currency,
);
tiles.add(
@@ -455,15 +425,14 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
leading: Icon(TablerIcons.currency_dollar, color: COLOR_ACTION),
trailing: Text(
pricing.isNotEmpty ? pricing : L10().noPricingAvailable,
style: TextStyle(
fontWeight: FontWeight.bold,
),
style: TextStyle(fontWeight: FontWeight.bold),
),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => PartPricingWidget(part: part, partPricing: partPricing),
builder: (context) =>
PartPricingWidget(part: part, partPricing: partPricing),
),
);
},
@@ -473,7 +442,6 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
// Tiles for "purchaseable" parts
if (part.isPurchaseable) {
// On order
tiles.add(
ListTile(
@@ -484,39 +452,41 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
onTap: () {
// TODO - Order views
},
)
),
);
}
// Tiles for an "assembly" part
if (part.isAssembly) {
if (showBom && bomCount > 0) {
tiles.add(
ListTile(
title: Text(L10().billOfMaterials),
leading: Icon(TablerIcons.list_tree, color: COLOR_ACTION),
trailing: Text(bomCount.toString()),
onTap: () {
Navigator.push(context, MaterialPageRoute(
builder: (context) => BillOfMaterialsWidget(part, isParentComponent: true)
));
},
)
ListTile(
title: Text(L10().billOfMaterials),
leading: Icon(TablerIcons.list_tree, 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: Icon(TablerIcons.tools),
trailing: Text("${simpleNumberString(part.building)}"),
onTap: () {
// TODO
},
)
ListTile(
title: Text(L10().building),
leading: Icon(TablerIcons.tools),
trailing: Text("${simpleNumberString(part.building)}"),
onTap: () {
// TODO
},
),
);
}
}
@@ -529,15 +499,16 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
subtitle: Text(L10().usedInDetails),
leading: Icon(TablerIcons.stack_2, color: COLOR_ACTION),
trailing: Text(usedInCount.toString()),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => BillOfMaterialsWidget(part, isParentComponent: false)
)
);
}
)
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
BillOfMaterialsWidget(part, isParentComponent: false),
),
);
},
),
);
}
}
@@ -545,29 +516,28 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
// Keywords?
if (part.keywords.isNotEmpty) {
tiles.add(
ListTile(
title: Text("${part.keywords}"),
leading: Icon(TablerIcons.tags),
)
ListTile(
title: Text("${part.keywords}"),
leading: Icon(TablerIcons.tags),
),
);
}
// External link?
if (part.link.isNotEmpty) {
tiles.add(
ListTile(
title: Text("${part.link}"),
leading: Icon(TablerIcons.link, color: COLOR_ACTION),
onTap: () {
part.openLink();
},
)
ListTile(
title: Text("${part.link}"),
leading: Icon(TablerIcons.link, color: COLOR_ACTION),
onTap: () {
part.openLink();
},
),
);
}
// Tiles for "component" part
if (part.isComponent && part.usedInCount > 0) {
tiles.add(
ListTile(
title: Text(L10().usedIn),
@@ -577,44 +547,44 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
onTap: () {
// TODO
},
)
),
);
}
if (part.isPurchaseable) {
if (part.supplierCount > 0) {
tiles.add(
ListTile(
title: Text(L10().suppliers),
leading: Icon(TablerIcons.building_factory, color: COLOR_ACTION),
trailing: Text("${part.supplierCount}"),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => SupplierPartList({
"part": part.pk.toString()
}))
);
},
)
ListTile(
title: Text(L10().suppliers),
leading: Icon(TablerIcons.building_factory, 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: Icon(TablerIcons.note, color: COLOR_ACTION),
trailing: Text(""),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => NotesWidget(part))
);
},
)
ListTile(
title: Text(L10().notes),
leading: Icon(TablerIcons.note, color: COLOR_ACTION),
trailing: Text(""),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => NotesWidget(part)),
);
},
),
);
tiles.add(
@@ -627,19 +597,18 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
context,
MaterialPageRoute(
builder: (context) => AttachmentWidget(
InvenTreePartAttachment(),
part.pk,
L10().part,
part.canEdit
)
)
InvenTreePartAttachment(),
part.pk,
L10().part,
part.canEdit,
),
),
);
},
)
),
);
return tiles;
}
// Return tiles for each stock item
@@ -654,9 +623,13 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
L10().stockItems,
style: TextStyle(fontWeight: FontWeight.bold),
),
subtitle: part.stockItems.isEmpty ? Text(L10().stockItemsNotAvailable) : null,
trailing: part.stockItems.isNotEmpty ? Text("${part.stockItems.length}") : null,
)
subtitle: part.stockItems.isEmpty
? Text(L10().stockItemsNotAvailable)
: null,
trailing: part.stockItems.isNotEmpty
? Text("${part.stockItems.length}")
: null,
),
);
return tiles;
@@ -666,7 +639,6 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
* 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
@@ -677,9 +649,7 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
int? default_location = part.defaultLocation;
Map<String, dynamic> data = {
"part": part.pk.toString()
};
Map<String, dynamic> data = {"part": part.pk.toString()};
if (default_location != null) {
data["location"] = default_location;
@@ -688,15 +658,22 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
if (part.isTrackable) {
// read the next available serial number
showLoadingOverlay();
var response = await api.get("/api/part/${part.pk}/serial-numbers/", expectedStatusCode: null);
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"];
data["serial_numbers"] =
response.data["next"] ?? response.data["latest"];
}
print("response: " + response.statusCode.toString() + response.data.toString());
print(
"response: " +
response.statusCode.toString() +
response.data.toString(),
);
} else {
// Cannot set serial numbers for non-trackable parts
fields.remove("serial_numbers");
@@ -705,28 +682,24 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
print("data: ${data.toString()}");
InvenTreeStockItem().createForm(
context,
L10().stockItemCreate,
fields: fields,
data: data,
onSuccess: (result) async {
context,
L10().stockItemCreate,
fields: fields,
data: data,
onSuccess: (result) async {
Map<String, dynamic> data = result as Map<String, dynamic>;
Map<String, dynamic> data = result as Map<String, dynamic>;
if (data.containsKey("pk")) {
var item = InvenTreeStockItem.fromJson(data);
item.goToDetailPage(context);
}
if (data.containsKey("pk")) {
var item = InvenTreeStockItem.fromJson(data);
item.goToDetailPage(context);
}
},
);
}
@override
List<Widget> getTabIcons(BuildContext context) {
List<Widget> icons = [
Tab(text: L10().details),
Tab(text: L10().stock)
];
List<Widget> icons = [Tab(text: L10().details), Tab(text: L10().stock)];
if (showParameters) {
icons.add(Tab(text: L10().parameters));
@@ -740,11 +713,9 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
List<Widget> tabs = [
SingleChildScrollView(
physics: AlwaysScrollableScrollPhysics(),
child: Column(
children: partTiles(),
)
child: Column(children: partTiles()),
),
PaginatedStockItemList({"part": part.pk.toString()})
PaginatedStockItemList({"part": part.pk.toString()}),
];
if (showParameters) {
@@ -753,5 +724,4 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
return tabs;
}
}

View File

@@ -11,19 +11,15 @@ 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;
@@ -38,17 +34,14 @@ class _PartImageState extends RefreshableState<PartImageWidget> {
@override
List<Widget> appBarActions(BuildContext context) {
List<Widget> actions = [];
if (part.canEdit) {
// File upload
actions.add(
IconButton(
icon: Icon(TablerIcons.file_upload),
onPressed: () async {
FilePickerDialog.pickFile(
onPicked: (File file) async {
final result = await part.uploadImage(file);
@@ -58,11 +51,10 @@ class _PartImageState extends RefreshableState<PartImageWidget> {
}
refresh(context);
}
},
);
},
)
),
);
}
@@ -73,5 +65,4 @@ class _PartImageState extends RefreshableState<PartImageWidget> {
Widget getBody(BuildContext context) {
return InvenTreeAPI().getImage(part.image);
}
}
}

View File

@@ -9,9 +9,7 @@ import "package:inventree/inventree/part.dart";
import "package:inventree/widget/paginator.dart";
import "package:inventree/widget/refreshable_state.dart";
class PartList extends StatefulWidget {
const PartList(this.filters, {this.title = ""});
final String title;
@@ -22,9 +20,7 @@ class PartList extends StatefulWidget {
_PartListState createState() => _PartListState(filters, title);
}
class _PartListState extends RefreshableState<PartList> {
_PartListState(this.filters, this.title);
final String title;
@@ -40,13 +36,11 @@ class _PartListState extends RefreshableState<PartList> {
Widget getBody(BuildContext context) {
return PaginatedPartList(filters);
}
}
class PaginatedPartList extends PaginatedSearchWidget {
const PaginatedPartList(Map<String, String> filters) : super(filters: filters);
const PaginatedPartList(Map<String, String> filters)
: super(filters: filters);
@override
String get searchTitle => L10().parts;
@@ -55,9 +49,7 @@ class PaginatedPartList extends PaginatedSearchWidget {
_PaginatedPartListState createState() => _PaginatedPartListState();
}
class _PaginatedPartListState extends PaginatedSearchState<PaginatedPartList> {
_PaginatedPartListState() : super();
@override
@@ -84,7 +76,7 @@ class _PaginatedPartListState extends PaginatedSearchState<PaginatedPartList> {
},
"assembly": {
"label": L10().filterAssembly,
"help_text": L10().filterAssemblyDetail
"help_text": L10().filterAssemblyDetail,
},
"component": {
"label": L10().filterComponent,
@@ -92,7 +84,7 @@ class _PaginatedPartListState extends PaginatedSearchState<PaginatedPartList> {
},
"is_template": {
"label": L10().filterTemplate,
"help_text": L10().filterTemplateDetail
"help_text": L10().filterTemplateDetail,
},
"trackable": {
"label": L10().filterTrackable,
@@ -105,18 +97,25 @@ class _PaginatedPartListState extends PaginatedSearchState<PaginatedPartList> {
"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);
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(
@@ -124,10 +123,7 @@ class _PaginatedPartListState extends PaginatedSearchState<PaginatedPartList> {
subtitle: Text(part.description),
trailing: Text(
part.stockString(),
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold
)
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
leading: InvenTreeAPI().getThumbnail(part.thumbnail),
onTap: () {
@@ -135,4 +131,4 @@ class _PaginatedPartListState extends PaginatedSearchState<PaginatedPartList> {
},
);
}
}
}

View File

@@ -11,7 +11,6 @@ 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;
@@ -20,7 +19,6 @@ class PartParameterWidget extends StatefulWidget {
_ParameterWidgetState createState() => _ParameterWidgetState();
}
class _ParameterWidgetState extends RefreshableState<PartParameterWidget> {
_ParameterWidgetState();
@@ -36,28 +34,18 @@ class _ParameterWidgetState extends RefreshableState<PartParameterWidget> {
@override
Widget getBody(BuildContext context) {
Map<String, String> filters = {"part": widget.part.pk.toString()};
Map<String, String> filters = {
"part": widget.part.pk.toString()
};
return Column(
children: [
Expanded(
child: PaginatedParameterList(filters)
)
],
);
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);
const PaginatedParameterList(Map<String, String> filters)
: super(filters: filters);
@override
String get searchTitle => L10().parameters;
@@ -66,18 +54,15 @@ class PaginatedParameterList extends PaginatedSearchWidget {
_PaginatedParameterState createState() => _PaginatedParameterState();
}
class _PaginatedParameterState extends PaginatedSearchState<PaginatedParameterList> {
class _PaginatedParameterState
extends PaginatedSearchState<PaginatedParameterList> {
_PaginatedParameterState() : super();
@override
String get prefix => "parameters_";
@override
Map<String, String> get orderingOptions => {
};
Map<String, String> get orderingOptions => {};
@override
Map<String, Map<String, dynamic>> get filterOptions => {
@@ -85,32 +70,37 @@ class _PaginatedParameterState extends PaginatedSearchState<PaginatedParameterLi
};
@override
Future<InvenTreePageResponse?> requestPage(int limit, int offset, Map<String, String> params) async {
final page = await InvenTreePartParameter().listPaginated(limit, offset, filters: params);
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();
}
context,
L10().editParameter,
onSuccess: (data) async {
updateSearchTerm();
},
);
}
}
@override
Widget buildItem(BuildContext context, InvenTreeModel model) {
InvenTreePartParameter parameter = model as InvenTreePartParameter;
String title = parameter.name;
@@ -123,27 +113,28 @@ class _PaginatedParameterState extends PaginatedSearchState<PaginatedParameterLi
title: Text(title),
subtitle: Text(parameter.description),
trailing: parameter.is_checkbox
? Switch(
value: parameter.as_bool,
onChanged: (bool value) {
if (parameter.canEdit) {
showLoadingOverlay();
parameter.update(
values: {
"data": value.toString()
? Switch(
value: parameter.as_bool,
onChanged: (bool value) {
if (parameter.canEdit) {
showLoadingOverlay();
parameter.update(values: {"data": value.toString()}).then((
value,
) async {
hideLoadingOverlay();
updateSearchTerm();
});
}
).then((value) async{
hideLoadingOverlay();
updateSearchTerm();
});
}
},
) : Text(parameter.value),
onTap: parameter.is_checkbox ? null : () async {
if (parameter.canEdit) {
editParameter(parameter);
}
},
},
)
: Text(parameter.value),
onTap: parameter.is_checkbox
? null
: () async {
if (parameter.canEdit) {
editParameter(parameter);
}
},
);
}
}
}

View File

@@ -7,8 +7,11 @@ import "package:inventree/widget/refreshable_state.dart";
import "package:inventree/helpers.dart";
class PartPricingWidget extends StatefulWidget {
const PartPricingWidget({Key? key, required this.part, required this.partPricing}) : super(key: key);
const PartPricingWidget({
Key? key,
required this.part,
required this.partPricing,
}) : super(key: key);
final InvenTreePart part;
final InvenTreePartPricing? partPricing;
@@ -17,7 +20,6 @@ class PartPricingWidget extends StatefulWidget {
}
class _PartPricingWidgetState extends RefreshableState<PartPricingWidget> {
@override
String getAppBarTitle() {
return L10().partPricing;
@@ -25,14 +27,13 @@ class _PartPricingWidgetState extends RefreshableState<PartPricingWidget> {
@override
List<Widget> getTiles(BuildContext context) {
List<Widget> tiles = [
Card(
child: ListTile(
title: Text(widget.part.fullname),
subtitle: Text(widget.part.description),
leading: api.getThumbnail(widget.part.thumbnail)
)
leading: api.getThumbnail(widget.part.thumbnail),
),
),
];
@@ -41,7 +42,7 @@ class _PartPricingWidgetState extends RefreshableState<PartPricingWidget> {
ListTile(
title: Text(L10().noPricingAvailable),
subtitle: Text(L10().noPricingDataFound),
)
),
);
return tiles;
@@ -50,10 +51,7 @@ class _PartPricingWidgetState extends RefreshableState<PartPricingWidget> {
final pricing = widget.partPricing!;
tiles.add(
ListTile(
title: Text(L10().currency),
trailing: Text(pricing.currency),
)
ListTile(title: Text(L10().currency), trailing: Text(pricing.currency)),
);
tiles.add(
@@ -63,10 +61,10 @@ class _PartPricingWidgetState extends RefreshableState<PartPricingWidget> {
formatPriceRange(
pricing.overallMin,
pricing.overallMax,
currency: pricing.currency
)
currency: pricing.currency,
),
),
)
),
);
if (pricing.overallMin != null) {
@@ -74,9 +72,9 @@ class _PartPricingWidgetState extends RefreshableState<PartPricingWidget> {
ListTile(
title: Text(L10().priceOverrideMin),
trailing: Text(
renderCurrency(pricing.overallMin, pricing.overrideMinCurrency)
)
)
renderCurrency(pricing.overallMin, pricing.overrideMinCurrency),
),
),
);
}
@@ -85,9 +83,9 @@ class _PartPricingWidgetState extends RefreshableState<PartPricingWidget> {
ListTile(
title: Text(L10().priceOverrideMax),
trailing: Text(
renderCurrency(pricing.overallMax, pricing.overrideMaxCurrency)
)
)
renderCurrency(pricing.overallMax, pricing.overrideMaxCurrency),
),
),
);
}
@@ -98,10 +96,10 @@ class _PartPricingWidgetState extends RefreshableState<PartPricingWidget> {
formatPriceRange(
pricing.internalCostMin,
pricing.internalCostMax,
currency: pricing.currency
)
currency: pricing.currency,
),
),
)
),
);
if (widget.part.isTemplate) {
@@ -112,10 +110,10 @@ class _PartPricingWidgetState extends RefreshableState<PartPricingWidget> {
formatPriceRange(
pricing.variantCostMin,
pricing.variantCostMax,
currency: pricing.currency
)
currency: pricing.currency,
),
),
)
),
);
}
@@ -124,13 +122,13 @@ class _PartPricingWidgetState extends RefreshableState<PartPricingWidget> {
ListTile(
title: Text(L10().bomCost),
trailing: Text(
formatPriceRange(
formatPriceRange(
pricing.bomCostMin,
pricing.bomCostMax,
currency: pricing.currency
)
)
)
currency: pricing.currency,
),
),
),
);
}
@@ -142,10 +140,10 @@ class _PartPricingWidgetState extends RefreshableState<PartPricingWidget> {
formatPriceRange(
pricing.purchaseCostMin,
pricing.purchaseCostMax,
currency: pricing.currency
)
currency: pricing.currency,
),
),
)
),
);
tiles.add(
@@ -155,10 +153,10 @@ class _PartPricingWidgetState extends RefreshableState<PartPricingWidget> {
formatPriceRange(
pricing.supplierPriceMin,
pricing.supplierPriceMax,
currency: pricing.currency
)
currency: pricing.currency,
),
),
)
),
);
}
@@ -172,10 +170,10 @@ class _PartPricingWidgetState extends RefreshableState<PartPricingWidget> {
formatPriceRange(
pricing.salePriceMin,
pricing.salePriceMax,
currency: pricing.currency
)
currency: pricing.currency,
),
),
)
),
);
tiles.add(
@@ -185,14 +183,13 @@ class _PartPricingWidgetState extends RefreshableState<PartPricingWidget> {
formatPriceRange(
pricing.saleHistoryMin,
pricing.saleHistoryMax,
currency: pricing.currency
)
currency: pricing.currency,
),
),
)
),
);
}
return tiles;
}
}

View File

@@ -10,19 +10,15 @@ import "package:inventree/inventree/company.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;
@@ -46,7 +42,6 @@ class _PartSupplierState extends RefreshableState<PartSupplierWidget> {
}
Widget _supplierPartTile(BuildContext context, int index) {
InvenTreeSupplierPart _part = _supplierParts[index];
return ListTile(
@@ -73,5 +68,4 @@ class _PartSupplierState extends RefreshableState<PartSupplierWidget> {
itemBuilder: _supplierPartTile,
);
}
}
}

View File

@@ -1,5 +1,3 @@
import "dart:io";
import "package:flutter/material.dart";
@@ -7,17 +5,11 @@ import "package:flutter_overlay_loader/flutter_overlay_loader.dart";
import "package:inventree/app_colors.dart";
import "package:one_context/one_context.dart";
/*
* A simplified linear progress bar widget,
* with standardized color depiction
*/
Widget ProgressBar(
double value,
{
double maximum = 1.0
}) {
Widget ProgressBar(double value, {double maximum = 1.0}) {
double v = 0;
if (value <= 0 || maximum <= 0) {
@@ -33,20 +25,14 @@ Widget ProgressBar(
);
}
/*
* Construct a circular progress indicator
*/
Widget progressIndicator() {
return Center(
child: CircularProgressIndicator()
);
return Center(child: CircularProgressIndicator());
}
void showLoadingOverlay() {
// Do not show overlay if running unit tests
if (Platform.environment.containsKey("FLUTTER_TEST")) {
return;
@@ -60,11 +46,12 @@ void showLoadingOverlay() {
Loader.show(
context,
themeData: Theme.of(context).copyWith(colorScheme: ColorScheme.fromSwatch())
themeData: Theme.of(
context,
).copyWith(colorScheme: ColorScheme.fromSwatch()),
);
}
void hideLoadingOverlay() {
if (Loader.isShown) {
Loader.hide();

View File

@@ -10,12 +10,10 @@ import "package:inventree/widget/back.dart";
import "package:inventree/widget/drawer.dart";
import "package:inventree/widget/search.dart";
/*
* Simple mixin class which defines simple methods for defining widget properties
*/
mixin BaseWidgetProperties {
/*
* Return a list of appBar actions
* By default, no appBar actions are available
@@ -23,7 +21,9 @@ mixin BaseWidgetProperties {
List<Widget> appBarActions(BuildContext context) => [];
// Return a title for the appBar (placeholder)
String getAppBarTitle() { return "--- app bar ---"; }
String getAppBarTitle() {
return "--- app bar ---";
}
// Function to construct a drawer (override if needed)
Widget getDrawer(BuildContext context) {
@@ -38,7 +38,6 @@ mixin BaseWidgetProperties {
// Function to construct a body
Widget getBody(BuildContext context) {
// Default implementation is to return a ListView
// Override getTiles to replace the internal context
return ListView(
@@ -51,7 +50,6 @@ mixin BaseWidgetProperties {
* Construct the top AppBar for this view
*/
AppBar? buildAppBar(BuildContext context, GlobalKey<ScaffoldState> key) {
List<Widget> tabs = getTabIcons(context);
return AppBar(
@@ -70,8 +68,10 @@ mixin BaseWidgetProperties {
* - Button to access global search
* - Button to access barcode scan
*/
BottomAppBar? buildBottomAppBar(BuildContext context, GlobalKey<ScaffoldState> key) {
BottomAppBar? buildBottomAppBar(
BuildContext context,
GlobalKey<ScaffoldState> key,
) {
const double iconSize = 40;
List<Widget> icons = [
@@ -90,10 +90,8 @@ mixin BaseWidgetProperties {
onPressed: () {
if (InvenTreeAPI().checkConnection()) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SearchWidget(true)
)
context,
MaterialPageRoute(builder: (context) => SearchWidget(true)),
);
}
},
@@ -106,27 +104,27 @@ mixin BaseWidgetProperties {
scanBarcode(context);
}
},
)
),
];
return BottomAppBar(
shape: AutomaticNotchedShape(
RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(20)),
),
RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(40)),
),
shape: AutomaticNotchedShape(
RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(20)),
),
notchMargin: 10,
child: IconTheme(
data: IconThemeData(color: Theme.of(context).colorScheme.onPrimary),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.max,
children: icons,
)
)
RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(40)),
),
),
notchMargin: 10,
child: IconTheme(
data: IconThemeData(color: Theme.of(context).colorScheme.onPrimary),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.max,
children: icons,
),
),
);
}
@@ -146,7 +144,6 @@ mixin BaseWidgetProperties {
* Build out action buttons for a given widget
*/
Widget? buildSpeedDial(BuildContext context) {
final actions = actionButtons(context);
final barcodeActions = barcodeButtons(context);
@@ -165,44 +162,38 @@ mixin BaseWidgetProperties {
spacing: 14,
childPadding: const EdgeInsets.all(5),
spaceBetweenChildren: 15,
)
),
);
}
if (actions.isNotEmpty) {
children.add(
SpeedDial(
icon: Icons.more_horiz,
activeIcon: Icons.close,
children: actions,
spacing: 14,
childPadding: const EdgeInsets.all(5),
spaceBetweenChildren: 15,
)
SpeedDial(
icon: Icons.more_horiz,
activeIcon: Icons.close,
children: actions,
spacing: 14,
childPadding: const EdgeInsets.all(5),
spaceBetweenChildren: 15,
),
);
}
return Wrap(
direction: Axis.horizontal,
children: children,
spacing: 15,
);
return Wrap(direction: Axis.horizontal, children: children, spacing: 15);
}
// Return list of "tabs" for this widget
List<Widget> getTabIcons(BuildContext context) => [];
}
/*
* Abstract base class which provides generic "refresh" functionality.
*
* - Drag down and release to 'refresh' the widget
* - Define some method which runs to 'refresh' the widget state
*/
abstract class RefreshableState<T extends StatefulWidget> extends State<T> with BaseWidgetProperties {
abstract class RefreshableState<T extends StatefulWidget> extends State<T>
with BaseWidgetProperties {
final scaffoldKey = GlobalKey<ScaffoldState>();
final refreshKey = GlobalKey<RefreshIndicatorState>();
@@ -235,7 +226,6 @@ abstract class RefreshableState<T extends StatefulWidget> extends State<T> with
// Refresh the widget - handler for custom request() method
Future<void> refresh(BuildContext context) async {
// Escape if the widget is no longer loaded
if (!mounted) {
return;
@@ -259,13 +249,14 @@ abstract class RefreshableState<T extends StatefulWidget> extends State<T> with
@override
Widget build(BuildContext context) {
// Save the context for future use
_context = context;
List<Widget> tabs = getTabIcons(context);
Widget body = tabs.isEmpty ? getBody(context) : TabBarView(children: getTabs(context));
Widget body = tabs.isEmpty
? getBody(context)
: TabBarView(children: getTabs(context));
Scaffold view = Scaffold(
key: scaffoldKey,
@@ -282,19 +273,16 @@ abstract class RefreshableState<T extends StatefulWidget> extends State<T> with
onRefresh: () async {
refresh(context);
},
child: body
child: body,
),
bottomNavigationBar: buildBottomAppBar(context, scaffoldKey),
);
// Default implementation is *not* tabbed
if (tabs.isNotEmpty) {
return DefaultTabController(
length: tabs.length,
child: view,
);
return DefaultTabController(length: tabs.length, child: view);
} else {
return view;
}
}
}
}

View File

@@ -23,21 +23,17 @@ import "package:inventree/widget/order/sales_order_list.dart";
import "package:inventree/widget/company/company_list.dart";
import "package:inventree/widget/company/supplier_part_list.dart";
// Widget for performing database-wide search
class SearchWidget extends StatefulWidget {
const SearchWidget(this.hasAppbar);
final bool hasAppbar;
@override
_SearchDisplayState createState() => _SearchDisplayState(hasAppbar);
}
class _SearchDisplayState extends RefreshableState<SearchWidget> {
_SearchDisplayState(this.hasAppBar) : super();
final _formKey = GlobalKey<FormState>();
@@ -80,7 +76,6 @@ class _SearchDisplayState extends RefreshableState<SearchWidget> {
* Determine if the search is still running
*/
bool isSearching() {
if (searchController.text.isEmpty) {
return false;
}
@@ -128,7 +123,6 @@ class _SearchDisplayState extends RefreshableState<SearchWidget> {
// Callback when the text is being edited
// Incorporates a debounce timer to restrict search frequency
void onSearchTextChanged(String text, {bool immediate = false}) {
if (debounceTimer?.isActive ?? false) {
debounceTimer!.cancel();
}
@@ -151,8 +145,7 @@ class _SearchDisplayState extends RefreshableState<SearchWidget> {
* }
* }
*/
int getSearchResultCount(Map <String, dynamic> results, String key) {
int getSearchResultCount(Map<String, dynamic> results, String key) {
dynamic result = results[key];
if (result == null || result is! Map) {
@@ -170,43 +163,70 @@ class _SearchDisplayState extends RefreshableState<SearchWidget> {
// Actually perform the search query
Future<void> _perform_search(Map<String, dynamic> body) async {
InvenTreeAPI().post(
"search/",
body: body,
expectedStatusCode: 200).then((APIResponse response) {
InvenTreeAPI().post("search/", body: body, expectedStatusCode: 200).then((
APIResponse response,
) {
String searchTerm = (body["search"] ?? "").toString();
String searchTerm = (body["search"] ?? "").toString();
// Only update if the results correspond to the current search term
if (searchTerm == searchController.text && mounted) {
decrementPendingSearches();
// Only update if the results correspond to the current search term
if (searchTerm == searchController.text && mounted) {
Map<String, dynamic> results = {};
decrementPendingSearches();
if (response.isValid() && response.data is Map<String, dynamic>) {
results = response.data as Map<String, dynamic>;
Map<String, dynamic> results = {};
setState(() {
nPartResults = getSearchResultCount(
results,
InvenTreePart.MODEL_TYPE,
);
nCategoryResults = getSearchResultCount(
results,
InvenTreePartCategory.MODEL_TYPE,
);
nStockResults = getSearchResultCount(
results,
InvenTreeStockItem.MODEL_TYPE,
);
nLocationResults = getSearchResultCount(
results,
InvenTreeStockLocation.MODEL_TYPE,
);
nPurchaseOrderResults = getSearchResultCount(
results,
InvenTreePurchaseOrder.MODEL_TYPE,
);
nSalesOrderResults = getSearchResultCount(
results,
InvenTreeSalesOrder.MODEL_TYPE,
);
nSupplierPartResults = getSearchResultCount(
results,
InvenTreeSupplierPart.MODEL_TYPE,
);
nManufacturerPartResults = getSearchResultCount(
results,
InvenTreeManufacturerPart.MODEL_TYPE,
);
nCompanyResults = getSearchResultCount(
results,
InvenTreeCompany.MODEL_TYPE,
);
if (response.isValid() && response.data is Map<String, dynamic>) {
results = response.data as Map<String, dynamic>;
setState(() {
nPartResults = getSearchResultCount(results, InvenTreePart.MODEL_TYPE);
nCategoryResults = getSearchResultCount(results, InvenTreePartCategory.MODEL_TYPE);
nStockResults = getSearchResultCount(results, InvenTreeStockItem.MODEL_TYPE);
nLocationResults = getSearchResultCount(results, InvenTreeStockLocation.MODEL_TYPE);
nPurchaseOrderResults = getSearchResultCount(results, InvenTreePurchaseOrder.MODEL_TYPE);
nSalesOrderResults = getSearchResultCount(results, InvenTreeSalesOrder.MODEL_TYPE);
nSupplierPartResults = getSearchResultCount(results, InvenTreeSupplierPart.MODEL_TYPE);
nManufacturerPartResults = getSearchResultCount(results, InvenTreeManufacturerPart.MODEL_TYPE);
nCompanyResults = getSearchResultCount(results, InvenTreeCompany.MODEL_TYPE);
// Special case for company search results
nCustomerResults = getSearchResultCount(results, "customer");
nManufacturerResults = getSearchResultCount(results, "manufacturer");
nSupplierResults = getSearchResultCount(results, "supplier");
});
} else {
resetSearchResults();
}
// Special case for company search results
nCustomerResults = getSearchResultCount(results, "customer");
nManufacturerResults = getSearchResultCount(
results,
"manufacturer",
);
nSupplierResults = getSearchResultCount(results, "supplier");
});
} else {
resetSearchResults();
}
}
});
}
@@ -237,7 +257,6 @@ class _SearchDisplayState extends RefreshableState<SearchWidget> {
// Consolidated search allows us to perform *all* searches in a single query
if (api.supportsConsolidatedSearch) {
Map<String, dynamic> body = {
"limit": 1,
"search": term,
@@ -262,17 +281,13 @@ class _SearchDisplayState extends RefreshableState<SearchWidget> {
}
if (body.isNotEmpty) {
if (mounted) {
setState(() {
nPendingSearches = 1;
});
_search_query = CancelableOperation.fromFuture(
_perform_search(body),
);
_search_query = CancelableOperation.fromFuture(_perform_search(body));
}
}
} else {
legacySearch(term);
@@ -283,7 +298,6 @@ class _SearchDisplayState extends RefreshableState<SearchWidget> {
* Perform "legacy" search (without consolidated search API endpoint
*/
Future<void> legacySearch(String term) async {
// Search parts
if (InvenTreePart().canView) {
nPendingSearches++;
@@ -302,7 +316,7 @@ class _SearchDisplayState extends RefreshableState<SearchWidget> {
// Search part categories
if (InvenTreePartCategory().canView) {
nPendingSearches++;
InvenTreePartCategory().count(searchQuery: term,).then((int n) {
InvenTreePartCategory().count(searchQuery: term).then((int n) {
if (term == searchController.text) {
if (mounted) {
decrementPendingSearches();
@@ -346,37 +360,31 @@ class _SearchDisplayState extends RefreshableState<SearchWidget> {
// Search purchase orders
if (InvenTreePurchaseOrder().canView) {
nPendingSearches++;
InvenTreePurchaseOrder().count(
searchQuery: term,
filters: {
"outstanding": "true"
}
).then((int n) {
if (term == searchController.text) {
if (mounted) {
decrementPendingSearches();
setState(() {
nPurchaseOrderResults = n;
});
}
}
});
nPendingSearches++;
InvenTreePurchaseOrder()
.count(searchQuery: term, filters: {"outstanding": "true"})
.then((int n) {
if (term == searchController.text) {
if (mounted) {
decrementPendingSearches();
setState(() {
nPurchaseOrderResults = n;
});
}
}
});
}
}
@override
List<Widget> getTiles(BuildContext context) {
List<Widget> tiles = [];
// Search input
tiles.add(
ListTile(
title: TextFormField(
decoration: InputDecoration(
hintText: L10().queryEmpty,
),
decoration: InputDecoration(hintText: L10().queryEmpty),
key: _formKey,
readOnly: false,
autofocus: true,
@@ -385,12 +393,13 @@ class _SearchDisplayState extends RefreshableState<SearchWidget> {
onChanged: (String text) {
onSearchTextChanged(text);
},
onFieldSubmitted: (String text) {
},
onFieldSubmitted: (String text) {},
),
trailing: GestureDetector(
child: Icon(
searchController.text.isEmpty ? TablerIcons.search : TablerIcons.backspace,
searchController.text.isEmpty
? TablerIcons.search
: TablerIcons.backspace,
color: searchController.text.isEmpty ? COLOR_ACTION : COLOR_DANGER,
),
onTap: () {
@@ -398,8 +407,7 @@ class _SearchDisplayState extends RefreshableState<SearchWidget> {
onSearchTextChanged("", immediate: true);
},
),
)
),
);
String query = searchController.text;
@@ -415,17 +423,13 @@ class _SearchDisplayState extends RefreshableState<SearchWidget> {
trailing: Text("${nPartResults}"),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => PartList(
{
"original_search": query
}
)
)
context,
MaterialPageRoute(
builder: (context) => PartList({"original_search": query}),
),
);
}
)
},
),
);
}
@@ -440,15 +444,12 @@ class _SearchDisplayState extends RefreshableState<SearchWidget> {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => PartCategoryList(
{
"original_search": query
}
)
)
builder: (context) =>
PartCategoryList({"original_search": query}),
),
);
},
)
),
);
}
@@ -463,15 +464,11 @@ class _SearchDisplayState extends RefreshableState<SearchWidget> {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => StockItemList(
{
"original_search": query,
}
)
)
builder: (context) => StockItemList({"original_search": query}),
),
);
},
)
),
);
}
@@ -486,15 +483,12 @@ class _SearchDisplayState extends RefreshableState<SearchWidget> {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => StockLocationList(
{
"original_search": query
}
)
)
builder: (context) =>
StockLocationList({"original_search": query}),
),
);
},
)
),
);
}
@@ -510,14 +504,12 @@ class _SearchDisplayState extends RefreshableState<SearchWidget> {
context,
MaterialPageRoute(
builder: (context) => PurchaseOrderListWidget(
filters: {
"original_search": query
}
)
)
filters: {"original_search": query},
),
),
);
},
)
),
);
}
@@ -532,15 +524,12 @@ class _SearchDisplayState extends RefreshableState<SearchWidget> {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SalesOrderListWidget(
filters: {
"original_search": query
}
)
)
builder: (context) =>
SalesOrderListWidget(filters: {"original_search": query}),
),
);
},
)
),
);
}
@@ -555,16 +544,13 @@ class _SearchDisplayState extends RefreshableState<SearchWidget> {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => CompanyListWidget(
L10().companies,
{
"original_search": query
}
)
)
builder: (context) => CompanyListWidget(L10().companies, {
"original_search": query,
}),
),
);
},
)
),
);
}
@@ -579,17 +565,14 @@ class _SearchDisplayState extends RefreshableState<SearchWidget> {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => CompanyListWidget(
L10().customers,
{
"original_search": query,
"is_customer": "true"
}
)
)
builder: (context) => CompanyListWidget(L10().customers, {
"original_search": query,
"is_customer": "true",
}),
),
);
},
)
),
);
}
@@ -604,17 +587,14 @@ class _SearchDisplayState extends RefreshableState<SearchWidget> {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => CompanyListWidget(
L10().manufacturers,
{
"original_search": query,
"is_manufacturer": "true"
}
)
)
builder: (context) => CompanyListWidget(L10().manufacturers, {
"original_search": query,
"is_manufacturer": "true",
}),
),
);
},
)
),
);
}
@@ -629,17 +609,14 @@ class _SearchDisplayState extends RefreshableState<SearchWidget> {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => CompanyListWidget(
L10().suppliers,
{
"original_search": query,
"is_supplier": "true"
}
)
)
builder: (context) => CompanyListWidget(L10().suppliers, {
"original_search": query,
"is_supplier": "true",
}),
),
);
},
)
),
);
}
@@ -654,15 +631,12 @@ class _SearchDisplayState extends RefreshableState<SearchWidget> {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SupplierPartList(
{
"original_search": query
}
)
)
builder: (context) =>
SupplierPartList({"original_search": query}),
),
);
},
)
),
);
}
@@ -672,7 +646,7 @@ class _SearchDisplayState extends RefreshableState<SearchWidget> {
title: Text(L10().searching),
leading: Icon(TablerIcons.search),
trailing: CircularProgressIndicator(),
)
),
);
}
@@ -684,7 +658,7 @@ class _SearchDisplayState extends RefreshableState<SearchWidget> {
style: TextStyle(fontStyle: FontStyle.italic),
),
leading: Icon(TablerIcons.zoom_cancel),
)
),
);
} else {
for (Widget result in results) {
@@ -694,5 +668,4 @@ class _SearchDisplayState extends RefreshableState<SearchWidget> {
return tiles;
}
}

View File

@@ -8,8 +8,13 @@ import "package:inventree/l10.dart";
/*
* Display a configurable 'snackbar' at the bottom of the screen
*/
void showSnackIcon(String text, {IconData? icon, Function()? onAction, bool? success, String? actionText}) {
void showSnackIcon(
String text, {
IconData? icon,
Function()? onAction,
bool? success,
String? actionText,
}) {
debug("showSnackIcon: '${text}'");
// Escape quickly if we do not have context
@@ -34,7 +39,6 @@ void showSnackIcon(String text, {IconData? icon, Function()? onAction, bool? suc
if (icon == null && onAction == null) {
icon = TablerIcons.circle_check;
}
} else if (success != null && success == false) {
backgroundColor = Colors.deepOrange;
@@ -45,35 +49,32 @@ void showSnackIcon(String text, {IconData? icon, Function()? onAction, bool? suc
String _action = actionText ?? L10().details;
List<Widget> childs = [
Text(text),
Spacer(),
];
List<Widget> childs = [Text(text), Spacer()];
if (icon != null) {
childs.add(Icon(icon));
}
OneContext().showSnackBar(builder: (context) => SnackBar(
content: GestureDetector(
child: Row(
children: childs
OneContext().showSnackBar(
builder: (context) => SnackBar(
content: GestureDetector(
child: Row(children: childs),
onTap: () {
ScaffoldMessenger.of(context!).hideCurrentSnackBar();
},
),
onTap: () {
ScaffoldMessenger.of(context!).hideCurrentSnackBar();
},
backgroundColor: backgroundColor,
action: onAction == null
? null
: SnackBarAction(
label: _action,
onPressed: () {
// Immediately dismiss the notification
ScaffoldMessenger.of(context!).hideCurrentSnackBar();
onAction();
},
),
duration: Duration(seconds: onAction == null ? 5 : 10),
),
backgroundColor: backgroundColor,
action: onAction == null ? null : SnackBarAction(
label: _action,
onPressed: () {
// Immediately dismiss the notification
ScaffoldMessenger.of(context!).hideCurrentSnackBar();
onAction();
}
),
duration: Duration(seconds: onAction == null ? 5 : 10),
)
);
}
}

View File

@@ -2,7 +2,6 @@ import "package:flutter/material.dart";
import "package:inventree/app_colors.dart";
class Spinner extends StatefulWidget {
const Spinner({
this.color = COLOR_GRAY_LIGHT,
Key? key,
@@ -27,12 +26,8 @@ class _SpinnerState extends State<Spinner> with SingleTickerProviderStateMixin {
_controller = AnimationController(
vsync: this,
duration: Duration(milliseconds: 2000),
)
..repeat();
_child = Icon(
widget.icon,
color: widget.color
);
)..repeat();
_child = Icon(widget.icon, color: widget.color);
super.initState();
}
@@ -45,9 +40,6 @@ class _SpinnerState extends State<Spinner> with SingleTickerProviderStateMixin {
@override
Widget build(BuildContext context) {
return RotationTransition(
turns: _controller!,
child: _child,
);
return RotationTransition(turns: _controller!, child: _child);
}
}
}

View File

@@ -18,12 +18,10 @@ import "package:inventree/widget/snacks.dart";
import "package:inventree/widget/stock/stock_list.dart";
import "package:inventree/labels.dart";
/*
* Widget for displaying detail view for a single StockLocation instance
*/
class LocationDisplayWidget extends StatefulWidget {
LocationDisplayWidget(this.location, {Key? key}) : super(key: key);
final InvenTreeStockLocation? location;
@@ -35,7 +33,6 @@ class LocationDisplayWidget extends StatefulWidget {
}
class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
_LocationDisplayState(this.location);
final InvenTreeStockLocation? location;
@@ -54,30 +51,29 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
// Add "locate" button
if (location != null && api.supportsMixin("locate")) {
actions.add(
IconButton(
icon: Icon(Icons.travel_explore),
tooltip: L10().locateLocation,
onPressed: () async {
api.locateItemOrLocation(context, location: location!.pk);
}
)
IconButton(
icon: Icon(Icons.travel_explore),
tooltip: L10().locateLocation,
onPressed: () async {
api.locateItemOrLocation(context, location: location!.pk);
},
),
);
}
// Add "edit" button
if (location != null && InvenTreeStockLocation().canEdit) {
actions.add(
IconButton(
icon: Icon(TablerIcons.edit),
tooltip: L10().editLocation,
onPressed: () {
_editLocationDialog(context);
}
)
IconButton(
icon: Icon(TablerIcons.edit),
tooltip: L10().editLocation,
onPressed: () {
_editLocationDialog(context);
},
),
);
}
return actions;
}
@@ -89,18 +85,18 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
// Scan items into this location
if (InvenTreeStockItem().canEdit) {
actions.add(
SpeedDialChild(
child: Icon(TablerIcons.qrcode),
label: L10().barcodeScanItem,
onTap: () {
scanBarcode(
context,
handler: StockLocationScanInItemsHandler(location!),
).then((value) {
refresh(context);
});
}
)
SpeedDialChild(
child: Icon(TablerIcons.qrcode),
label: L10().barcodeScanItem,
onTap: () {
scanBarcode(
context,
handler: StockLocationScanInItemsHandler(location!),
).then((value) {
refresh(context);
});
},
),
);
}
@@ -109,41 +105,43 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
SpeedDialChild(
child: Icon(Icons.barcode_reader),
label: L10().scanReceivedParts,
onTap:() async {
onTap: () async {
scanBarcode(
context,
handler: POReceiveBarcodeHandler(location: location),
);
},
)
),
);
}
// Scan this location into another one
if (InvenTreeStockLocation().canEdit) {
actions.add(
SpeedDialChild(
child: Icon(TablerIcons.qrcode),
label: L10().transferStockLocation,
onTap: () {
scanBarcode(
context,
handler: ScanParentLocationHandler(location!),
).then((value) {
refresh(context);
});
}
)
SpeedDialChild(
child: Icon(TablerIcons.qrcode),
label: L10().transferStockLocation,
onTap: () {
scanBarcode(
context,
handler: ScanParentLocationHandler(location!),
).then((value) {
refresh(context);
});
},
),
);
}
// Assign or un-assign barcodes
actions.add(
customBarcodeAction(
context, this,
location!.customBarcode, "stocklocation",
location!.pk
)
customBarcodeAction(
context,
this,
location!.customBarcode,
"stocklocation",
location!.pk,
),
);
}
@@ -157,44 +155,44 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
// Create new location
if (InvenTreeStockLocation().canCreate) {
actions.add(
SpeedDialChild(
child: Icon(TablerIcons.sitemap),
label: L10().locationCreate,
onTap: () async {
_newLocation(context);
}
)
SpeedDialChild(
child: Icon(TablerIcons.sitemap),
label: L10().locationCreate,
onTap: () async {
_newLocation(context);
},
),
);
}
// Create new item
if (InvenTreeStockItem().canCreate) {
actions.add(
SpeedDialChild(
child: Icon(TablerIcons.packages),
label: L10().stockItemCreate,
onTap: () async {
_newStockItem(context);
}
)
SpeedDialChild(
child: Icon(TablerIcons.packages),
label: L10().stockItemCreate,
onTap: () async {
_newStockItem(context);
},
),
);
}
if (widget.location != null && labels.isNotEmpty) {
actions.add(
SpeedDialChild(
child: Icon(TablerIcons.printer),
label: L10().printLabel,
onTap: () async {
selectAndPrintLabel(
context,
labels,
widget.location!.pk,
"location",
"location=${widget.location!.pk}"
);
}
)
SpeedDialChild(
child: Icon(TablerIcons.printer),
label: L10().printLabel,
onTap: () async {
selectAndPrintLabel(
context,
labels,
widget.location!.pk,
"location",
"location=${widget.location!.pk}",
);
},
),
);
}
@@ -212,12 +210,12 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
}
_loc.editForm(
context,
L10().editLocation,
onSuccess: (data) async {
refresh(context);
showSnackIcon(L10().locationUpdated, success: true);
}
context,
L10().editLocation,
onSuccess: (data) async {
refresh(context);
showSnackIcon(L10().locationUpdated, success: true);
},
);
}
@@ -238,22 +236,24 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
}
List<Map<String, dynamic>> _labels = [];
bool allowLabelPrinting = await InvenTreeSettingsManager().getBool(INV_ENABLE_LABEL_PRINTING, true);
bool allowLabelPrinting = await InvenTreeSettingsManager().getBool(
INV_ENABLE_LABEL_PRINTING,
true,
);
allowLabelPrinting &= api.supportsMixin("labels");
if (allowLabelPrinting) {
if (widget.location != null) {
String model_type = api.supportsModernLabelPrinting
? InvenTreeStockLocation.MODEL_TYPE
: "location";
String item_key = api.supportsModernLabelPrinting
? "items"
: "location";
String model_type = api.supportsModernLabelPrinting ? InvenTreeStockLocation.MODEL_TYPE : "location";
String item_key = api.supportsModernLabelPrinting ? "items" : "location";
_labels = await getLabelTemplates(
model_type,
{
item_key: widget.location!.pk.toString()
}
);
_labels = await getLabelTemplates(model_type, {
item_key: widget.location!.pk.toString(),
});
}
}
@@ -268,19 +268,17 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
int pk = location?.pk ?? -1;
InvenTreeStockLocation().createForm(
context,
L10().locationCreate,
data: {
"parent": (pk > 0) ? pk : null,
},
onSuccess: (result) async {
Map<String, dynamic> data = result as Map<String, dynamic>;
context,
L10().locationCreate,
data: {"parent": (pk > 0) ? pk : null},
onSuccess: (result) async {
Map<String, dynamic> data = result as Map<String, dynamic>;
if (data.containsKey("pk")) {
var loc = InvenTreeStockLocation.fromJson(data);
loc.goToDetailPage(context);
}
if (data.containsKey("pk")) {
var loc = InvenTreeStockLocation.fromJson(data);
loc.goToDetailPage(context);
}
},
);
}
@@ -288,7 +286,6 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
* Launch a dialog form to create a new stock item
*/
Future<void> _newStockItem(BuildContext context) async {
var fields = InvenTreeStockItem().formFields();
// Serial number field is not required here
@@ -301,94 +298,89 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
}
InvenTreeStockItem().createForm(
context,
L10().stockItemCreate,
data: data,
fields: fields,
onSuccess: (result) async {
Map<String, dynamic> data = result as Map<String, dynamic>;
context,
L10().stockItemCreate,
data: data,
fields: fields,
onSuccess: (result) async {
Map<String, dynamic> data = result as Map<String, dynamic>;
if (data.containsKey("pk")) {
var item = InvenTreeStockItem.fromJson(data);
item.goToDetailPage(context);
}
if (data.containsKey("pk")) {
var item = InvenTreeStockItem.fromJson(data);
item.goToDetailPage(context);
}
},
);
}
Widget locationDescriptionCard({bool includeActions = true}) {
if (location == null) {
return Card(
child: ListTile(
title: Text(
L10().stockTopLevel,
style: TextStyle(fontStyle: FontStyle.italic)
),
leading: Icon(TablerIcons.packages),
)
child: ListTile(
title: Text(
L10().stockTopLevel,
style: TextStyle(fontStyle: FontStyle.italic),
),
leading: Icon(TablerIcons.packages),
),
);
} else {
List<Widget> children = [
ListTile(
title: Text("${location!.name}"),
subtitle: Text("${location!.description}"),
leading: location!.customIcon == null ? Icon(TablerIcons.packages) : Icon(location!.customIcon)
leading: location!.customIcon == null
? Icon(TablerIcons.packages)
: Icon(location!.customIcon),
),
];
if (includeActions) {
children.add(
ListTile(
title: Text(L10().parentLocation),
subtitle: Text("${location!.parentPathString}"),
leading: Icon(TablerIcons.arrow_move_up, color: COLOR_ACTION),
onTap: () async {
int parentId = location?.parentId ?? -1;
ListTile(
title: Text(L10().parentLocation),
subtitle: Text("${location!.parentPathString}"),
leading: Icon(TablerIcons.arrow_move_up, color: COLOR_ACTION),
onTap: () async {
int parentId = location?.parentId ?? -1;
if (parentId < 0) {
Navigator.push(context, MaterialPageRoute(
builder: (context) => LocationDisplayWidget(null)));
} else {
showLoadingOverlay();
var loc = await InvenTreeStockLocation().get(parentId);
hideLoadingOverlay();
if (parentId < 0) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => LocationDisplayWidget(null),
),
);
} else {
showLoadingOverlay();
var loc = await InvenTreeStockLocation().get(parentId);
hideLoadingOverlay();
if (loc is InvenTreeStockLocation) {
loc.goToDetailPage(context);
}
if (loc is InvenTreeStockLocation) {
loc.goToDetailPage(context);
}
},
)
}
},
),
);
}
return Card(
child: Column(
children: children,
)
);
return Card(child: Column(children: children));
}
}
@override
List<Widget> getTabIcons(BuildContext context) {
return [
Tab(text: L10().details),
Tab(text: L10().stockItems),
];
return [Tab(text: L10().details), Tab(text: L10().stockItems)];
}
@override
List<Widget> getTabs(BuildContext context) {
return [
Column(children: detailTiles()),
Column(children: stockTiles()),
];
return [Column(children: detailTiles()), Column(children: stockTiles())];
}
// Construct the "details" panel
List<Widget> detailTiles() {
Map<String, String> filters = {};
int? parent = location?.pk;
@@ -402,12 +394,9 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
List<Widget> tiles = [
locationDescriptionCard(),
Expanded(
child: PaginatedStockLocationList(
filters,
title: L10().sublocations,
),
child: PaginatedStockLocationList(filters, title: L10().sublocations),
flex: 10,
)
),
];
return tiles;
@@ -419,11 +408,6 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
"location": location?.pk.toString() ?? "null",
};
return [
Expanded(
child: PaginatedStockItemList(filters),
flex: 10,
)
];
return [Expanded(child: PaginatedStockItemList(filters), flex: 10)];
}
}

View File

@@ -7,9 +7,7 @@ import "package:inventree/widget/paginator.dart";
import "package:inventree/widget/refreshable_state.dart";
import "package:inventree/l10.dart";
class StockLocationList extends StatefulWidget {
const StockLocationList(this.filters);
final Map<String, String> filters;
@@ -18,9 +16,7 @@ class StockLocationList extends StatefulWidget {
_StockLocationListState createState() => _StockLocationListState(filters);
}
class _StockLocationListState extends RefreshableState<StockLocationList> {
_StockLocationListState(this.filters);
final Map<String, String> filters;
@@ -34,21 +30,22 @@ class _StockLocationListState extends RefreshableState<StockLocationList> {
}
}
class PaginatedStockLocationList extends PaginatedSearchWidget {
const PaginatedStockLocationList(Map<String, String> filters, {String title = ""}) : super(filters: filters, title: title);
const PaginatedStockLocationList(
Map<String, String> filters, {
String title = "",
}) : super(filters: filters, title: title);
@override
String get searchTitle => title.isNotEmpty ? title : L10().stockLocations;
@override
_PaginatedStockLocationListState createState() => _PaginatedStockLocationListState();
_PaginatedStockLocationListState createState() =>
_PaginatedStockLocationListState();
}
class _PaginatedStockLocationListState extends PaginatedSearchState<PaginatedStockLocationList> {
class _PaginatedStockLocationListState
extends PaginatedSearchState<PaginatedStockLocationList> {
_PaginatedStockLocationListState() : super();
@override
@@ -64,20 +61,26 @@ class _PaginatedStockLocationListState extends PaginatedSearchState<PaginatedSto
"label": L10().includeSublocations,
"help_text": L10().includeSublocationsDetail,
"tristate": false,
}
},
};
@override
Future<InvenTreePageResponse?> requestPage(int limit, int offset, Map<String, String> params) async {
final page = await InvenTreeStockLocation().listPaginated(limit, offset, filters: params);
Future<InvenTreePageResponse?> requestPage(
int limit,
int offset,
Map<String, String> params,
) async {
final page = await InvenTreeStockLocation().listPaginated(
limit,
offset,
filters: params,
);
return page;
}
@override
Widget buildItem(BuildContext context, InvenTreeModel model) {
InvenTreeStockLocation location = model as InvenTreeStockLocation;
return ListTile(
@@ -90,4 +93,4 @@ class _PaginatedStockLocationListState extends PaginatedSearchState<PaginatedSto
},
);
}
}
}

View File

@@ -28,9 +28,7 @@ import "package:inventree/widget/stock/stock_item_history.dart";
import "package:inventree/widget/stock/stock_item_test_results.dart";
import "package:inventree/widget/notes_widget.dart";
class StockDetailWidget extends StatefulWidget {
const StockDetailWidget(this.item, {Key? key}) : super(key: key);
final InvenTreeStockItem item;
@@ -39,9 +37,7 @@ class StockDetailWidget extends StatefulWidget {
_StockItemDisplayState createState() => _StockItemDisplayState();
}
class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
_StockItemDisplayState();
@override
@@ -62,25 +58,25 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
if (api.supportsMixin("locate")) {
actions.add(
IconButton(
icon: Icon(Icons.travel_explore),
tooltip: L10().locateItem,
onPressed: () async {
api.locateItemOrLocation(context, item: widget.item.pk);
}
)
IconButton(
icon: Icon(Icons.travel_explore),
tooltip: L10().locateItem,
onPressed: () async {
api.locateItemOrLocation(context, item: widget.item.pk);
},
),
);
}
if (widget.item.canEdit) {
actions.add(
IconButton(
icon: Icon(TablerIcons.edit),
tooltip: L10().editItem,
onPressed: () {
_editStockItem(context);
}
)
IconButton(
icon: Icon(TablerIcons.edit),
tooltip: L10().editItem,
onPressed: () {
_editStockItem(context);
},
),
);
}
@@ -89,20 +85,17 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
@override
List<SpeedDialChild> actionButtons(BuildContext context) {
List<SpeedDialChild> actions = [];
if (widget.item.canEdit) {
// Stock adjustment actions available if item is *not* serialized
if (!widget.item.isSerialized()) {
actions.add(
SpeedDialChild(
child: Icon(TablerIcons.circle_check, color: Colors.blue),
label: L10().countStock,
onTap: _countStockDialog,
)
),
);
actions.add(
@@ -110,7 +103,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
child: Icon(TablerIcons.circle_minus, color: Colors.red),
label: L10().removeStock,
onTap: _removeStockDialog,
)
),
);
actions.add(
@@ -118,7 +111,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
child: Icon(TablerIcons.circle_plus, color: Colors.green),
label: L10().addStock,
onTap: _addStockDialog,
)
),
);
}
@@ -129,8 +122,8 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
label: L10().transferStock,
onTap: () {
_transferStockDialog(context);
}
)
},
),
);
}
@@ -141,26 +134,26 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
label: L10().printLabel,
onTap: () async {
selectAndPrintLabel(
context,
labels,
widget.item.pk,
"stock",
"item=${widget.item.pk}"
context,
labels,
widget.item.pk,
"stock",
"item=${widget.item.pk}",
);
}
)
},
),
);
}
if (widget.item.canDelete) {
actions.add(
SpeedDialChild(
child: Icon(TablerIcons.trash, color: Colors.red),
label: L10().stockItemDelete,
onTap: () {
_deleteItem(context);
}
)
SpeedDialChild(
child: Icon(TablerIcons.trash, color: Colors.red),
label: L10().stockItemDelete,
onTap: () {
_deleteItem(context);
},
),
);
}
@@ -174,26 +167,28 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
if (widget.item.canEdit) {
// Scan item into location
actions.add(
SpeedDialChild(
child: Icon(Icons.qr_code_scanner),
label: L10().scanIntoLocation,
onTap: () {
scanBarcode(
context,
handler: StockItemScanIntoLocationHandler(widget.item)
).then((ctx) {
refresh(context);
});
}
)
SpeedDialChild(
child: Icon(Icons.qr_code_scanner),
label: L10().scanIntoLocation,
onTap: () {
scanBarcode(
context,
handler: StockItemScanIntoLocationHandler(widget.item),
).then((ctx) {
refresh(context);
});
},
),
);
actions.add(
customBarcodeAction(
context, this,
widget.item.customBarcode,
"stockitem", widget.item.pk
)
customBarcodeAction(
context,
this,
widget.item.customBarcode,
"stockitem",
widget.item.pk,
),
);
}
@@ -217,8 +212,12 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
@override
Future<void> request(BuildContext context) async {
await api.StockStatus.load();
stockShowHistory = await InvenTreeSettingsManager().getValue(INV_STOCK_SHOW_HISTORY, false) as bool;
stockShowTests = await InvenTreeSettingsManager().getValue(INV_STOCK_SHOW_TESTS, true) as bool;
stockShowHistory =
await InvenTreeSettingsManager().getValue(INV_STOCK_SHOW_HISTORY, false)
as bool;
stockShowTests =
await InvenTreeSettingsManager().getValue(INV_STOCK_SHOW_TESTS, true)
as bool;
final bool result = widget.item.pk > 0 && await widget.item.reload();
@@ -238,7 +237,6 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
// Request test results (async)
if (stockShowTests) {
widget.item.getTestResults().then((value) {
if (mounted) {
setState(() {
// Update
@@ -248,7 +246,9 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
}
// Request the number of attachments
InvenTreeStockItemAttachment().countAttachments(widget.item.pk).then((int value) {
InvenTreeStockItemAttachment().countAttachments(widget.item.pk).then((
int value,
) {
if (mounted) {
setState(() {
attachmentCount = value;
@@ -258,13 +258,18 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
// Request SalesOrder information
if (widget.item.hasSalesOrder) {
InvenTreeSalesOrder().get(widget.item.salesOrderId).then((instance) => {
if (mounted) {
setState(() {
salesOrder = instance as InvenTreeSalesOrder?;
})
}
});
InvenTreeSalesOrder()
.get(widget.item.salesOrderId)
.then(
(instance) => {
if (mounted)
{
setState(() {
salesOrder = instance as InvenTreeSalesOrder?;
}),
},
},
);
} else {
if (mounted) {
setState(() {
@@ -275,13 +280,18 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
// Request Customer information
if (widget.item.hasCustomer) {
InvenTreeCompany().get(widget.item.customerId).then((instance) => {
if (mounted) {
setState(() {
customer = instance as InvenTreeCompany?;
})
}
});
InvenTreeCompany()
.get(widget.item.customerId)
.then(
(instance) => {
if (mounted)
{
setState(() {
customer = instance as InvenTreeCompany?;
}),
},
},
);
} else {
if (mounted) {
setState(() {
@@ -291,22 +301,23 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
}
List<Map<String, dynamic>> _labels = [];
bool allowLabelPrinting = await InvenTreeSettingsManager().getBool(INV_ENABLE_LABEL_PRINTING, true);
bool allowLabelPrinting = await InvenTreeSettingsManager().getBool(
INV_ENABLE_LABEL_PRINTING,
true,
);
allowLabelPrinting &= api.supportsMixin("labels");
// Request information on labels available for this stock item
if (allowLabelPrinting) {
String model_type = api.supportsModernLabelPrinting ? InvenTreeStockItem.MODEL_TYPE : "stock";
String model_type = api.supportsModernLabelPrinting
? InvenTreeStockItem.MODEL_TYPE
: "stock";
String item_key = api.supportsModernLabelPrinting ? "items" : "item";
// Clear the existing labels list
_labels = await getLabelTemplates(
model_type,
{
item_key: widget.item.pk.toString()
}
);
_labels = await getLabelTemplates(model_type, {
item_key: widget.item.pk.toString(),
});
}
if (mounted) {
@@ -318,7 +329,6 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
/// Delete the stock item from the database
Future<void> _deleteItem(BuildContext context) async {
confirmationDialog(
L10().stockItemDelete,
L10().stockItemDeleteConfirm,
@@ -327,7 +337,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
acceptText: L10().delete,
onAccept: () async {
final bool result = await widget.item.delete();
if (result) {
Navigator.of(context).pop();
showSnackIcon(L10().stockItemDeleteSuccess, success: true);
@@ -336,11 +346,9 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
}
},
);
}
Future <void> _editStockItem(BuildContext context) async {
Future<void> _editStockItem(BuildContext context) async {
var fields = InvenTreeStockItem().formFields();
// Some fields we don't want to edit!
@@ -360,16 +368,14 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
onSuccess: (data) async {
refresh(context);
showSnackIcon(L10().stockItemUpdated, success: true);
}
},
);
}
/*
* Launch a dialog to 'add' quantity to this StockItem
*/
Future <void> _addStockDialog() async {
Future<void> _addStockDialog() async {
Map<String, dynamic> fields = {
"pk": {
"parent": "items",
@@ -377,11 +383,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
"hidden": true,
"value": widget.item.pk,
},
"quantity": {
"parent": "items",
"nested": true,
"value": 0,
},
"quantity": {"parent": "items", "nested": true, "value": 0},
"notes": {},
};
@@ -395,12 +397,11 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
onSuccess: (data) async {
_stockUpdateMessage(true);
refresh(context);
}
},
);
}
void _stockUpdateMessage(bool result) {
if (result) {
showSnackIcon(L10().stockItemUpdated, success: true);
}
@@ -410,7 +411,6 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
* Launch a dialog to 'remove' quantity from this StockItem
*/
void _removeStockDialog() {
Map<String, dynamic> fields = {
"pk": {
"parent": "items",
@@ -418,30 +418,25 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
"hidden": true,
"value": widget.item.pk,
},
"quantity": {
"parent": "items",
"nested": true,
"value": 0,
},
"quantity": {"parent": "items", "nested": true, "value": 0},
"notes": {},
};
launchApiForm(
context,
L10().removeStock,
InvenTreeStockItem.removeStockUrl(),
fields,
method: "POST",
icon: TablerIcons.circle_minus,
onSuccess: (data) async {
_stockUpdateMessage(true);
refresh(context);
}
context,
L10().removeStock,
InvenTreeStockItem.removeStockUrl(),
fields,
method: "POST",
icon: TablerIcons.circle_minus,
onSuccess: (data) async {
_stockUpdateMessage(true);
refresh(context);
},
);
}
Future <void> _countStockDialog() async {
Future<void> _countStockDialog() async {
Map<String, dynamic> fields = {
"pk": {
"parent": "items",
@@ -458,58 +453,51 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
};
launchApiForm(
context,
L10().countStock,
InvenTreeStockItem.countStockUrl(),
fields,
method: "POST",
icon: TablerIcons.clipboard_check,
onSuccess: (data) async {
_stockUpdateMessage(true);
refresh(context);
}
context,
L10().countStock,
InvenTreeStockItem.countStockUrl(),
fields,
method: "POST",
icon: TablerIcons.clipboard_check,
onSuccess: (data) async {
_stockUpdateMessage(true);
refresh(context);
},
);
}
/*
* Launches an API Form to transfer this stock item to a new location
*/
Future <void> _transferStockDialog(BuildContext context) async {
Future<void> _transferStockDialog(BuildContext context) async {
Map<String, dynamic> fields = widget.item.transferFields();
launchApiForm(
context,
L10().transferStock,
InvenTreeStockItem.transferStockUrl(),
fields,
method: "POST",
icon: TablerIcons.transfer,
onSuccess: (data) async {
_stockUpdateMessage(true);
refresh(context);
}
context,
L10().transferStock,
InvenTreeStockItem.transferStockUrl(),
fields,
method: "POST",
icon: TablerIcons.transfer,
onSuccess: (data) async {
_stockUpdateMessage(true);
refresh(context);
},
);
}
Widget headerTile() {
Widget? trailing;
if (!widget.item.isInStock) {
trailing = Text(
L10().unavailable,
style: TextStyle(
color: COLOR_DANGER
)
);
trailing = Text(L10().unavailable, style: TextStyle(color: COLOR_DANGER));
} else if (!widget.item.isSerialized()) {
trailing = Text(
widget.item.quantityString(),
style: TextStyle(
fontSize: 20,
color: api.StockStatus.color(widget.item.status),
)
widget.item.quantityString(),
style: TextStyle(
fontSize: 20,
color: api.StockStatus.color(widget.item.status),
),
);
}
@@ -521,7 +509,6 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
trailing: trailing,
onTap: () async {
if (widget.item.partId > 0) {
showLoadingOverlay();
var part = await InvenTreePart().get(widget.item.partId);
hideLoadingOverlay();
@@ -532,7 +519,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
}
},
//trailing: Text(item.serialOrQuantityDisplay()),
)
),
);
}
@@ -558,15 +545,13 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
ListTile(
title: Text(L10().stockLocation),
subtitle: Text("${widget.item.locationPathString}"),
leading: Icon(
TablerIcons.location,
color: COLOR_ACTION,
),
leading: Icon(TablerIcons.location, color: COLOR_ACTION),
onTap: () async {
if (widget.item.locationId > 0) {
showLoadingOverlay();
var loc = await InvenTreeStockLocation().get(widget.item.locationId);
var loc = await InvenTreeStockLocation().get(
widget.item.locationId,
);
hideLoadingOverlay();
if (loc is InvenTreeStockLocation) {
@@ -578,30 +563,32 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
);
} else {
tiles.add(
ListTile(
title: Text(L10().stockLocation),
leading: Icon(TablerIcons.location),
subtitle: Text(L10().locationNotSet),
)
ListTile(
title: Text(L10().stockLocation),
leading: Icon(TablerIcons.location),
subtitle: Text(L10().locationNotSet),
),
);
}
// Quantity information
if (widget.item.isSerialized()) {
tiles.add(
ListTile(
title: Text(L10().serialNumber),
leading: Icon(TablerIcons.hash),
subtitle: Text("${widget.item.serialNumber}"),
)
ListTile(
title: Text(L10().serialNumber),
leading: Icon(TablerIcons.hash),
subtitle: Text("${widget.item.serialNumber}"),
),
);
} else if (widget.item.isInStock) {
tiles.add(
ListTile(
title: widget.item.allocated > 0 ? Text(L10().quantityAvailable) : Text(L10().quantity),
leading: Icon(TablerIcons.packages),
trailing: Text("${widget.item.quantityString()}"),
)
ListTile(
title: widget.item.allocated > 0
? Text(L10().quantityAvailable)
: Text(L10().quantity),
leading: Icon(TablerIcons.packages),
trailing: Text("${widget.item.quantityString()}"),
),
);
}
@@ -611,18 +598,13 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
leading: Icon(TablerIcons.box_off),
title: Text(
L10().unavailable,
style: TextStyle(
color: COLOR_DANGER,
fontWeight: FontWeight.bold,
),
style: TextStyle(color: COLOR_DANGER, fontWeight: FontWeight.bold),
),
subtitle: Text(
L10().unavailableDetail,
style: TextStyle(
color: COLOR_DANGER
)
)
)
style: TextStyle(color: COLOR_DANGER),
),
),
);
}
@@ -633,11 +615,9 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
leading: Icon(TablerIcons.help_circle),
trailing: Text(
api.StockStatus.label(widget.item.status),
style: TextStyle(
color: api.StockStatus.color(widget.item.status),
)
)
)
style: TextStyle(color: api.StockStatus.color(widget.item.status)),
),
),
);
// Supplier part information (if available)
@@ -647,20 +627,26 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
title: Text(L10().supplierPart),
subtitle: Text(widget.item.supplierSKU),
leading: Icon(TablerIcons.building, color: COLOR_ACTION),
trailing: InvenTreeAPI().getThumbnail(widget.item.supplierImage, hideIfNull: true),
trailing: InvenTreeAPI().getThumbnail(
widget.item.supplierImage,
hideIfNull: true,
),
onTap: () async {
showLoadingOverlay();
var sp = await InvenTreeSupplierPart().get(
widget.item.supplierPartId);
widget.item.supplierPartId,
);
hideLoadingOverlay();
if (sp is InvenTreeSupplierPart) {
Navigator.push(
context, MaterialPageRoute(
builder: (context) => SupplierPartDetailWidget(sp))
context,
MaterialPageRoute(
builder: (context) => SupplierPartDetailWidget(sp),
),
);
}
}
)
},
),
);
}
@@ -673,7 +659,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
onTap: () {
// TODO: Click through to the "build order"
},
)
),
);
}
@@ -686,8 +672,8 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
trailing: Text(salesOrder?.reference ?? ""),
onTap: () {
salesOrder?.goToDetailPage(context);
}
)
},
),
);
}
@@ -701,7 +687,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
onTap: () {
customer?.goToDetailPage(context);
},
)
),
);
}
@@ -711,7 +697,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
title: Text(L10().batchCode),
subtitle: Text(widget.item.batch),
leading: Icon(TablerIcons.clipboard_text),
)
),
);
}
@@ -721,18 +707,23 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
title: Text(L10().packaging),
subtitle: Text(widget.item.packaging),
leading: Icon(TablerIcons.package),
)
),
);
}
if (expiryEnabled && widget.item.expiryDate != null) {
Widget? _expiryIcon;
if (widget.item.expired) {
_expiryIcon = Text(L10().expiryExpired, style: TextStyle(color: COLOR_DANGER));
_expiryIcon = Text(
L10().expiryExpired,
style: TextStyle(color: COLOR_DANGER),
);
} else if (widget.item.stale) {
_expiryIcon = Text(L10().expiryStale, style: TextStyle(color: COLOR_WARNING));
_expiryIcon = Text(
L10().expiryStale,
style: TextStyle(color: COLOR_WARNING),
);
}
tiles.add(
@@ -741,19 +732,18 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
subtitle: Text(widget.item.expiryDateString),
leading: Icon(TablerIcons.calendar_x),
trailing: _expiryIcon,
)
),
);
}
// Last update?
if (widget.item.updatedDateString.isNotEmpty) {
tiles.add(
ListTile(
title: Text(L10().lastUpdated),
subtitle: Text(widget.item.updatedDateString),
leading: Icon(TablerIcons.calendar)
)
leading: Icon(TablerIcons.calendar),
),
);
}
@@ -763,8 +753,8 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
ListTile(
title: Text(L10().lastStocktake),
subtitle: Text(widget.item.stocktakeDateString),
leading: Icon(TablerIcons.calendar)
)
leading: Icon(TablerIcons.calendar),
),
);
}
@@ -776,26 +766,27 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
onTap: () {
widget.item.openLink();
},
)
),
);
}
if (stockShowTests || (widget.item.testResultCount > 0)) {
tiles.add(
ListTile(
title: Text(L10().testResults),
leading: Icon(TablerIcons.list_check, color: COLOR_ACTION),
trailing: Text("${widget.item.testResultCount}"),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => StockItemTestResultsWidget(widget.item))
).then((ctx) {
refresh(context);
});
}
)
ListTile(
title: Text(L10().testResults),
leading: Icon(TablerIcons.list_check, color: COLOR_ACTION),
trailing: Text("${widget.item.testResultCount}"),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => StockItemTestResultsWidget(widget.item),
),
).then((ctx) {
refresh(context);
});
},
),
);
}
@@ -805,9 +796,12 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
title: Text(L10().purchasePrice),
leading: Icon(TablerIcons.currency_dollar),
trailing: Text(
renderCurrency(widget.item.purchasePrice, widget.item.purchasePriceCurrency)
)
)
renderCurrency(
widget.item.purchasePrice,
widget.item.purchasePriceCurrency,
),
),
),
);
}
@@ -823,12 +817,13 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => StockItemHistoryWidget(widget.item))
).then((ctx) {
refresh(context);
builder: (context) => StockItemHistoryWidget(widget.item),
),
).then((ctx) {
refresh(context);
});
},
)
),
);
}
@@ -840,34 +835,33 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => NotesWidget(widget.item))
MaterialPageRoute(builder: (context) => NotesWidget(widget.item)),
);
}
)
},
),
);
tiles.add(
ListTile(
title: Text(L10().attachments),
leading: Icon(TablerIcons.file, color: COLOR_ACTION),
trailing: attachmentCount > 0 ? Text(attachmentCount.toString()) : null,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => AttachmentWidget(
InvenTreeStockItemAttachment(),
widget.item.pk,
L10().stockItem,
widget.item.canEdit,
)
)
);
},
)
ListTile(
title: Text(L10().attachments),
leading: Icon(TablerIcons.file, color: COLOR_ACTION),
trailing: attachmentCount > 0 ? Text(attachmentCount.toString()) : null,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => AttachmentWidget(
InvenTreeStockItemAttachment(),
widget.item.pk,
L10().stockItem,
widget.item.canEdit,
),
),
);
},
),
);
return tiles;
}
}
}

View File

@@ -14,10 +14,12 @@ class StockItemHistoryWidget extends StatefulWidget {
final InvenTreeStockItem item;
@override
_StockItemHistoryDisplayState createState() => _StockItemHistoryDisplayState(item);
_StockItemHistoryDisplayState createState() =>
_StockItemHistoryDisplayState(item);
}
class _StockItemHistoryDisplayState extends RefreshableState<StockItemHistoryWidget> {
class _StockItemHistoryDisplayState
extends RefreshableState<StockItemHistoryWidget> {
_StockItemHistoryDisplayState(this.item);
final InvenTreeStockItem item;
@@ -30,20 +32,18 @@ class _StockItemHistoryDisplayState extends RefreshableState<StockItemHistoryWid
@override
Widget getBody(BuildContext context) {
Map<String, String> filters = {
"item": widget.item.pk.toString(),
};
Map<String, String> filters = {"item": widget.item.pk.toString()};
return PaginatedStockHistoryList(filters);
}
}
/*
* Widget which displays a paginated stock history list
*/
class PaginatedStockHistoryList extends PaginatedSearchWidget {
const PaginatedStockHistoryList(Map<String, String> filters) : super(filters: filters);
const PaginatedStockHistoryList(Map<String, String> filters)
: super(filters: filters);
@override
String get searchTitle => L10().stockItemHistory;
@@ -67,15 +67,22 @@ class _PaginatedStockHistoryState
@override
Map<String, Map<String, dynamic>> get filterOptions => {
// TODO: Add filter options
};
// TODO: Add filter options
};
@override
Future<InvenTreePageResponse?> requestPage(
int limit, int offset, Map<String, String> params) async {
int limit,
int offset,
Map<String, String> params,
) async {
await InvenTreeAPI().StockHistoryStatus.load();
final page = await InvenTreeStockItemHistory().listPaginated(limit, offset, filters: params);
final page = await InvenTreeStockItemHistory().listPaginated(
limit,
offset,
filters: params,
);
return page;
}

View File

@@ -13,20 +13,18 @@ import "package:inventree/inventree/model.dart";
import "package:inventree/widget/progress.dart";
import "package:inventree/widget/refreshable_state.dart";
class StockItemTestResultsWidget extends StatefulWidget {
const StockItemTestResultsWidget(this.item, {Key? key}) : super(key: key);
final InvenTreeStockItem item;
@override
_StockItemTestResultDisplayState createState() => _StockItemTestResultDisplayState(item);
_StockItemTestResultDisplayState createState() =>
_StockItemTestResultDisplayState(item);
}
class _StockItemTestResultDisplayState extends RefreshableState<StockItemTestResultsWidget> {
class _StockItemTestResultDisplayState
extends RefreshableState<StockItemTestResultsWidget> {
_StockItemTestResultDisplayState(this.item);
@override
@@ -46,8 +44,8 @@ class _StockItemTestResultDisplayState extends RefreshableState<StockItemTestRes
label: L10().testResultAdd,
onTap: () {
addTestResult(context);
}
)
},
),
);
}
@@ -62,9 +60,18 @@ class _StockItemTestResultDisplayState extends RefreshableState<StockItemTestRes
final InvenTreeStockItem item;
Future <void> addTestResult(BuildContext context, {int templateId = 0, String name = "", bool nameIsEditable = true, bool result = false, String value = "", bool valueRequired = false, bool attachmentRequired = false}) async {
Map<String, Map<String, dynamic>> fields = InvenTreeStockItemTestResult().formFields();
Future<void> addTestResult(
BuildContext context, {
int templateId = 0,
String name = "",
bool nameIsEditable = true,
bool result = false,
String value = "",
bool valueRequired = false,
bool attachmentRequired = false,
}) async {
Map<String, Map<String, dynamic>> fields = InvenTreeStockItemTestResult()
.formFields();
// Add additional filters
fields["template"]?["filters"]?["part"] = "${item.partId}";
@@ -102,7 +109,6 @@ class _StockItemTestResultDisplayState extends RefreshableState<StockItemTestRes
bool match = false;
for (var ii = 0; ii < outputs.length; ii++) {
// Check against templates
if (outputs[ii] is InvenTreePartTestTemplate) {
var template = outputs[ii] as InvenTreePartTestTemplate;
@@ -143,16 +149,17 @@ class _StockItemTestResultDisplayState extends RefreshableState<StockItemTestRes
title: Text(item.partName),
subtitle: Text(item.partDescription),
leading: InvenTreeAPI().getThumbnail(item.partImage),
)
)
),
),
);
tiles.add(
ListTile(
title: Text(L10().testResults,
style: TextStyle(fontWeight: FontWeight.bold)
)
)
title: Text(
L10().testResults,
style: TextStyle(fontWeight: FontWeight.bold),
),
),
);
if (loading) {
@@ -163,16 +170,17 @@ class _StockItemTestResultDisplayState extends RefreshableState<StockItemTestRes
var results = getTestResults();
if (results.isEmpty) {
tiles.add(ListTile(
title: Text(L10().testResultNone),
subtitle: Text(L10().testResultNoneDetail),
));
tiles.add(
ListTile(
title: Text(L10().testResultNone),
subtitle: Text(L10().testResultNoneDetail),
),
);
return tiles;
}
for (var item in results) {
bool _hasResult = false;
bool _required = false;
String _test = "";
@@ -213,35 +221,38 @@ class _StockItemTestResultDisplayState extends RefreshableState<StockItemTestRes
_icon = Icon(TablerIcons.circle_x, color: COLOR_DANGER);
}
tiles.add(ListTile(
title: Text(_test, style: TextStyle(
fontWeight: _required ? FontWeight.bold : FontWeight.normal,
fontStyle: _hasResult ? FontStyle.normal : FontStyle.italic
)),
subtitle: Text(_value),
trailing: Text(_date),
leading: _icon,
onTap: () {
if (InvenTreeStockItemTestResult().canCreate) {
addTestResult(
tiles.add(
ListTile(
title: Text(
_test,
style: TextStyle(
fontWeight: _required ? FontWeight.bold : FontWeight.normal,
fontStyle: _hasResult ? FontStyle.normal : FontStyle.italic,
),
),
subtitle: Text(_value),
trailing: Text(_date),
leading: _icon,
onTap: () {
if (InvenTreeStockItemTestResult().canCreate) {
addTestResult(
context,
name: _test,
templateId: _templateId,
nameIsEditable: !_required,
valueRequired: _valueRequired,
attachmentRequired: _attachmentRequired
);
}
}
));
attachmentRequired: _attachmentRequired,
);
}
},
),
);
}
if (tiles.isEmpty) {
tiles.add(ListTile(
title: Text(L10().testResultNone),
));
tiles.add(ListTile(title: Text(L10().testResultNone)));
}
return tiles;
}
}
}

View File

@@ -7,9 +7,7 @@ import "package:inventree/widget/refreshable_state.dart";
import "package:inventree/l10.dart";
import "package:inventree/api.dart";
class StockItemList extends StatefulWidget {
const StockItemList(this.filters);
final Map<String, String> filters;
@@ -18,9 +16,7 @@ class StockItemList extends StatefulWidget {
_StockListState createState() => _StockListState(filters);
}
class _StockListState extends RefreshableState<StockItemList> {
_StockListState(this.filters);
final Map<String, String> filters;
@@ -35,20 +31,18 @@ class _StockListState extends RefreshableState<StockItemList> {
}
class PaginatedStockItemList extends PaginatedSearchWidget {
const PaginatedStockItemList(Map<String, String> filters) : super(filters: filters);
const PaginatedStockItemList(Map<String, String> filters)
: super(filters: filters);
@override
String get searchTitle => L10().stockItems;
@override
_PaginatedStockItemListState createState() => _PaginatedStockItemListState();
}
class _PaginatedStockItemListState extends PaginatedSearchState<PaginatedStockItemList> {
class _PaginatedStockItemListState
extends PaginatedSearchState<PaginatedStockItemList> {
_PaginatedStockItemListState() : super();
@override
@@ -100,7 +94,7 @@ class _PaginatedStockItemListState extends PaginatedSearchState<PaginatedStockIt
"label": L10().status,
"help_text": L10().statusCode,
"choices": InvenTreeAPI().StockStatus.choices,
}
},
};
if (!InvenTreeAPI().supportsStatusLabelEndpoints) {
@@ -111,15 +105,18 @@ class _PaginatedStockItemListState extends PaginatedSearchState<PaginatedStockIt
}
@override
Future<InvenTreePageResponse?> requestPage(int limit, int offset, Map<String, String> params) async {
Future<InvenTreePageResponse?> requestPage(
int limit,
int offset,
Map<String, String> params,
) async {
// Ensure StockStatus codes are loaded
await InvenTreeAPI().StockStatus.load();
final page = await InvenTreeStockItem().listPaginated(
limit,
offset,
filters: params
filters: params,
);
return page;
@@ -127,7 +124,6 @@ class _PaginatedStockItemListState extends PaginatedSearchState<PaginatedStockIt
@override
Widget buildItem(BuildContext context, InvenTreeModel model) {
InvenTreeStockItem item = model as InvenTreeStockItem;
return ListTile(
@@ -136,17 +132,18 @@ class _PaginatedStockItemListState extends PaginatedSearchState<PaginatedStockIt
leading: InvenTreeAPI().getThumbnail(item.partThumbnail),
trailing: SizedBox(
width: 48,
child: Text("${item.displayQuantity}",
child: Text(
"${item.displayQuantity}",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
color: InvenTreeAPI().StockStatus.color(item.status),
),
)
),
),
onTap: () {
item.goToDetailPage(context);
},
);
}
}
}