diff --git a/assets/release_notes.md b/assets/release_notes.md index 1b226592..8b0b61d1 100644 --- a/assets/release_notes.md +++ b/assets/release_notes.md @@ -1,12 +1,15 @@ ### 0.17.1 - December 2024 --- +- Add support for ManufacturerPart model +- Support barcode scanning for ManufacturerPart - Fixes barcode scanning bug which prevents scanning of DataMatrix codes - Display "destination" information in PurchaseOrder detail view - Pre-fill "location" field when receiving items against PurchaseOrder - Fix display of part name in PurchaseOrderLineItem list - Adds "assigned to me" filter for Purchase Order list - Adds "assigned to me" filter for Sales Order list +- Updated translations ### 0.17.0 - December 2024 --- diff --git a/lib/api_form.dart b/lib/api_form.dart index f8e335d9..7b8cb426 100644 --- a/lib/api_form.dart +++ b/lib/api_form.dart @@ -11,15 +11,17 @@ import "package:inventree/api.dart"; import "package:inventree/app_colors.dart"; import "package:inventree/barcode/barcode.dart"; import "package:inventree/helpers.dart"; -import "package:inventree/inventree/sales_order.dart"; import "package:inventree/l10.dart"; import "package:inventree/inventree/company.dart"; import "package:inventree/inventree/part.dart"; import "package:inventree/inventree/project_code.dart"; -import "package:inventree/inventree/sentry.dart"; +import "package:inventree/inventree/purchase_order.dart"; +import "package:inventree/inventree/sales_order.dart"; import "package:inventree/inventree/stock.dart"; +import "package:inventree/inventree/sentry.dart"; + import "package:inventree/widget/dialogs.dart"; import "package:inventree/widget/fields.dart"; import "package:inventree/widget/progress.dart"; @@ -562,11 +564,17 @@ class APIFormField { Map data = item as Map; switch (model) { - case "part": + case InvenTreePart.MODEL_TYPE: return InvenTreePart.fromJson(data).fullname; - case "partcategory": + case InvenTreeCompany.MODEL_TYPE: + return InvenTreeCompany.fromJson(data).name; + case InvenTreePurchaseOrder.MODEL_TYPE: + return InvenTreePurchaseOrder.fromJson(data).reference; + case InvenTreeSalesOrder.MODEL_TYPE: + return InvenTreeSalesOrder.fromJson(data).reference; + case InvenTreePartCategory.MODEL_TYPE: return InvenTreePartCategory.fromJson(data).pathstring; - case "stocklocation": + case InvenTreeStockLocation.MODEL_TYPE: return InvenTreeStockLocation.fromJson(data).pathstring; default: return "itemAsString not implemented for '${model}'"; @@ -606,10 +614,12 @@ class APIFormField { Map _relatedFieldFilters() { switch (model) { - case "supplierpart": + case InvenTreeSupplierPart.MODEL_TYPE: return InvenTreeSupplierPart().defaultListFilters(); - case "stockitem": + case InvenTreeStockItem.MODEL_TYPE: return InvenTreeStockItem().defaultListFilters(); + default: + break; } return {}; @@ -643,7 +653,7 @@ class APIFormField { } switch (model) { - case "part": + case InvenTreePart.MODEL_TYPE: var part = InvenTreePart.fromJson(data); return ListTile( @@ -657,14 +667,14 @@ class APIFormField { ) : null, leading: extended ? InvenTreeAPI().getThumbnail(part.thumbnail) : null, ); - case "parttesttemplate": + case InvenTreePartTestTemplate.MODEL_TYPE: var template = InvenTreePartTestTemplate.fromJson(data); return ListTile( title: Text(template.testName), subtitle: Text(template.description), ); - case "supplierpart": + case InvenTreeSupplierPart.MODEL_TYPE: var part = InvenTreeSupplierPart.fromJson(data); return ListTile( @@ -673,7 +683,7 @@ class APIFormField { leading: extended ? InvenTreeAPI().getThumbnail(part.partImage) : null, trailing: extended && part.supplierImage.isNotEmpty ? InvenTreeAPI().getThumbnail(part.supplierImage) : null, ); - case "partcategory": + case InvenTreePartCategory.MODEL_TYPE: var cat = InvenTreePartCategory.fromJson(data); @@ -687,7 +697,7 @@ class APIFormField { style: TextStyle(fontWeight: selected ? FontWeight.bold : FontWeight.normal), ) : null, ); - case "stockitem": + case InvenTreeStockItem.MODEL_TYPE: var item = InvenTreeStockItem.fromJson(data); return ListTile( @@ -697,8 +707,7 @@ class APIFormField { leading: InvenTreeAPI().getThumbnail(item.partThumbnail), trailing: Text(item.quantityString()), ); - case "stocklocation": - + case InvenTreeStockLocation.MODEL_TYPE: var loc = InvenTreeStockLocation.fromJson(data); return ListTile( @@ -711,7 +720,7 @@ class APIFormField { style: TextStyle(fontWeight: selected ? FontWeight.bold : FontWeight.normal), ) : null, ); - case "salesordershipment": + case InvenTreeSalesOrderShipment.MODEL_TYPE: var shipment = InvenTreeSalesOrderShipment.fromJson(data); return ListTile( @@ -733,14 +742,14 @@ class APIFormField { title: Text(name), subtitle: Text(role), ); - case "company": + case InvenTreeCompany.MODEL_TYPE: var company = InvenTreeCompany.fromJson(data); return ListTile( title: Text(company.name), subtitle: extended ? Text(company.description) : null, leading: InvenTreeAPI().getThumbnail(company.thumbnail) ); - case "projectcode": + case InvenTreeProjectCode.MODEL_TYPE: var project_code = InvenTreeProjectCode.fromJson(data); return ListTile( title: Text(project_code.code), diff --git a/lib/barcode/barcode.dart b/lib/barcode/barcode.dart index dd2a6c8b..0de089e7 100644 --- a/lib/barcode/barcode.dart +++ b/lib/barcode/barcode.dart @@ -4,7 +4,9 @@ import "package:flutter_speed_dial/flutter_speed_dial.dart"; import "package:flutter_tabler_icons/flutter_tabler_icons.dart"; import "package:inventree/helpers.dart"; import "package:inventree/inventree/sales_order.dart"; +import "package:inventree/inventree/sentry.dart"; import "package:inventree/preferences.dart"; +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"; @@ -30,6 +32,7 @@ import "package:inventree/widget/order/purchase_order_detail.dart"; import "package:inventree/widget/refreshable_state.dart"; import "package:inventree/widget/snacks.dart"; 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"; @@ -176,15 +179,37 @@ class BarcodeScanHandler extends BarcodeHandler { */ Future handleSupplierPart(int pk) async { - var supplierpart = await InvenTreeSupplierPart().get(pk); + var supplierPart = await InvenTreeSupplierPart().get(pk); - if (supplierpart is InvenTreeSupplierPart) { + if (supplierPart is InvenTreeSupplierPart) { OneContext().pop(); OneContext().push(MaterialPageRoute( - builder: (context) => SupplierPartDetailWidget(supplierpart))); + builder: (context) => SupplierPartDetailWidget(supplierPart))); } } + /* + * Response when a "ManufacturerPart" instance is scanned + */ + Future handleManufacturerPart(int pk) async { + var manufacturerPart = await InvenTreeManufacturerPart().get(pk); + + if (manufacturerPart is InvenTreeManufacturerPart) { + OneContext().pop(); + OneContext().push(MaterialPageRoute( + builder: (context) => ManufacturerPartDetailWidget(manufacturerPart))); + } + } + + Future handleCompany(int pk) async { + var company = await InvenTreeCompany().get(pk); + + if (company is InvenTreeCompany) { + OneContext().pop(); + OneContext().push(MaterialPageRoute( + builder: (context) => CompanyDetailWidget(company))); + } + } /* * Response when a "PurchaseOrder" instance is scanned @@ -218,26 +243,32 @@ class BarcodeScanHandler extends BarcodeHandler { // The following model types can be matched with barcodes List validModels = [ - "part", - "stockitem", - "stocklocation", - "supplierpart", + InvenTreePart.MODEL_TYPE, + InvenTreeCompany.MODEL_TYPE, + InvenTreeStockItem.MODEL_TYPE, + InvenTreeStockLocation.MODEL_TYPE, + InvenTreeSupplierPart.MODEL_TYPE, + InvenTreeManufacturerPart.MODEL_TYPE, ]; if (InvenTreeAPI().supportsOrderBarcodes) { - validModels.add("purchaseorder"); - validModels.add("salesorder"); + validModels.add(InvenTreePurchaseOrder.MODEL_TYPE); + validModels.add(InvenTreeSalesOrder.MODEL_TYPE); } for (var key in validModels) { if (data.containsKey(key)) { - pk = (data[key]?["pk"] ?? -1) as int; + try { + pk = (data[key]?["pk"] ?? -1) as int; - // Break on the first valid match found - if (pk > 0) { - model = key; - break; + // Break on the first valid match found + if (pk > 0) { + model = key; + break; + } + } catch (error, stackTrace) { + sentryReportError("onBarcodeMatched", error, stackTrace); } } } @@ -248,25 +279,30 @@ class BarcodeScanHandler extends BarcodeHandler { barcodeSuccessTone(); switch (model) { - case "part": - await handlePart(pk); - return; - case "stockitem": + case InvenTreeStockItem.MODEL_TYPE: await handleStockItem(pk); return; - case "stocklocation": + case InvenTreePurchaseOrder.MODEL_TYPE: + await handlePurchaseOrder(pk); + return; + case InvenTreeSalesOrder.MODEL_TYPE: + await handleSalesOrder(pk); + return; + case InvenTreeStockLocation.MODEL_TYPE: await handleStockLocation(pk); return; - case "supplierpart": + case InvenTreeSupplierPart.MODEL_TYPE: await handleSupplierPart(pk); return; - case "purchaseorder": - await handlePurchaseOrder(pk); + case InvenTreeManufacturerPart.MODEL_TYPE: + await handleManufacturerPart(pk); return; - case "salesorder": - await handleSalesOrder(pk); + case InvenTreePart.MODEL_TYPE: + await handlePart(pk); + return; + case InvenTreeCompany.MODEL_TYPE: + await handleCompany(pk); return; - // TODO: Handle manufacturer part default: // Fall through to failure state break; @@ -324,21 +360,6 @@ class UniqueBarcodeHandler extends BarcodeHandler { @override Future onBarcodeMatched(Map data) async { - - barcodeFailureTone(); - - // If the barcode is known, we can"t assign it to the stock item! - showSnackIcon( - L10().barcodeInUse, - icon: Icons.qr_code, - success: false - ); - } - - @override - Future onBarcodeUnknown(Map data) async { - // If the barcode is unknown, we *can* assign it to the stock item! - if (!data.containsKey("hash") && !data.containsKey("barcode_hash")) { showServerError( "barcode/", @@ -370,6 +391,12 @@ class UniqueBarcodeHandler extends BarcodeHandler { } } } + + @override + Future onBarcodeUnknown(Map data) async { + await onBarcodeMatched(data); + } + } diff --git a/lib/barcode/camera_controller.dart b/lib/barcode/camera_controller.dart index 4c6edd7a..ece027c2 100644 --- a/lib/barcode/camera_controller.dart +++ b/lib/barcode/camera_controller.dart @@ -93,7 +93,7 @@ class _CameraBarcodeControllerState extends InvenTreeBarcodeControllerState { /* * Callback function when a barcode is scanned */ - void _onScanSuccess(Code? code) { + Future onScanSuccess(Code? code) async { if (scanning_paused) { return; @@ -122,18 +122,16 @@ class _CameraBarcodeControllerState extends InvenTreeBarcodeControllerState { if (mounted) { setState(() { scanned_code = barcode; - scanning_paused = barcode.isNotEmpty; }); } if (barcode.isNotEmpty) { - handleBarcodeData(barcode).then((_) { + pauseScan(); + + await handleBarcodeData(barcode).then((_) { if (!single_scanning && mounted) { - // Resume next scan - setState(() { - scanning_paused = false; - }); + resumeScan(); } }); } @@ -186,7 +184,7 @@ class _CameraBarcodeControllerState extends InvenTreeBarcodeControllerState { Widget BarcodeReader(BuildContext context) { return ReaderWidget( - onScan: _onScanSuccess, + onScan: onScanSuccess, isMultiScan: false, tryHarder: true, tryInverted: true, diff --git a/lib/barcode/handler.dart b/lib/barcode/handler.dart index 20dc9e92..48831f9a 100644 --- a/lib/barcode/handler.dart +++ b/lib/barcode/handler.dart @@ -79,14 +79,27 @@ class BarcodeHandler { return; } - var response = await InvenTreeAPI().post( + APIResponse? response; + + try { + response = await InvenTreeAPI().post( url, body: { "barcode": barcode, ...extra_data, }, - expectedStatusCode: null, // Do not show an error on "unexpected code" - ); + expectedStatusCode: null, // Do not show an error on "unexpected code" + ); + } catch (error, stackTrace) { + sentryReportError("Barcode.processBarcode", error, stackTrace); + response = null; + } + + if (response == null) { + barcodeFailureTone(); + showSnackIcon(L10().barcodeError, success: false); + return; + } debug("Barcode scan response" + response.data.toString()); @@ -94,7 +107,7 @@ class BarcodeHandler { // Handle strange response from the server if (!response.isValid() || !response.isMap()) { - onBarcodeUnknown({}); + await onBarcodeUnknown({}); showSnackIcon(L10().serverError, success: false); diff --git a/lib/barcode/purchase_order.dart b/lib/barcode/purchase_order.dart index e8bba611..8b2f984b 100644 --- a/lib/barcode/purchase_order.dart +++ b/lib/barcode/purchase_order.dart @@ -1,9 +1,7 @@ import "package:flutter/material.dart"; -import "package:flutter_tabler_icons/flutter_tabler_icons.dart"; +import "package:inventree/preferences.dart"; import "package:one_context/one_context.dart"; - import "package:inventree/l10.dart"; -import "package:inventree/api_form.dart"; import "package:inventree/barcode/barcode.dart"; import "package:inventree/barcode/handler.dart"; @@ -23,10 +21,11 @@ import "package:inventree/widget/snacks.dart"; */ class POReceiveBarcodeHandler extends BarcodeHandler { - POReceiveBarcodeHandler({this.purchaseOrder, this.location}); + POReceiveBarcodeHandler({this.purchaseOrder, this.location, this.lineItem}); InvenTreePurchaseOrder? purchaseOrder; InvenTreeStockLocation? location; + InvenTreePOLineItem? lineItem; @override String getOverlayText(BuildContext context) => L10().barcodeReceivePart; @@ -34,11 +33,15 @@ class POReceiveBarcodeHandler extends BarcodeHandler { @override Future processBarcode(String barcode, {String url = "barcode/po-receive/", - Map extra_data = const {}}) { + Map extra_data = const {}}) async { + + final bool confirm = await InvenTreeSettingsManager().getBool(INV_PO_CONFIRM_SCAN, true); final po_extra_data = { "purchase_order": purchaseOrder?.pk, "location": location?.pk, + "line_item": lineItem?.pk, + "auto_allocate": !confirm, ...extra_data, }; @@ -47,11 +50,13 @@ class POReceiveBarcodeHandler extends BarcodeHandler { @override Future onBarcodeMatched(Map data) async { - if (!data.containsKey("lineitem")) { + + if (data.containsKey("lineitem") || data.containsKey("success")) { + barcodeSuccess(L10().receivedItem); + return; + } else { return onBarcodeUnknown(data); } - - barcodeSuccess(L10().receivedItem); } @override @@ -66,49 +71,41 @@ class POReceiveBarcodeHandler extends BarcodeHandler { showSnackIcon(L10().missingData, success: false); } - // Construct fields to receive - Map fields = { - "line_item": { - "parent": "items", - "nested": true, - "hidden": true, - "value": lineItemData["pk"] as int, - }, - "quantity": { - "parent": "items", - "nested": true, - "value": lineItemData["quantity"] as double?, - }, - "status": { - "parent": "items", - "nested": true, - }, - "location": { - "value": lineItemData["location"] as int?, - }, - "barcode": { - "parent": "items", - "nested": true, - "hidden": true, - "type": "barcode", - "value": data["barcode_data"] as String, - } - }; + // At minimum, we need the line item ID value + final int? lineItemId = lineItemData["pk"] as int?; - final context = OneContext().context!; - final purchase_order_pk = lineItemData["purchase_order"]; - final receive_url = "${InvenTreePurchaseOrder().URL}${purchase_order_pk}/receive/"; - - launchApiForm( - context, - L10().receiveItem, - receive_url, - fields, - method: "POST", - icon: TablerIcons.transition_right, - onSuccess: (data) async { - showSnackIcon(L10().receivedItem, success: true); - } + if (lineItemId == null) { + barcodeFailureTone(); + return; + } + + InvenTreePOLineItem? lineItem = await InvenTreePOLineItem().get(lineItemId) as InvenTreePOLineItem?; + + if (lineItem == null) { + barcodeFailureTone(); + return; + } + + // Next, extract the "optional" fields + + // Extract information from the returned server response + double? quantity = double.tryParse((lineItemData["quantity"] ?? "0").toString()); + int? destination = lineItemData["location"] as int?; + String? barcode = data["barcode_data"] as String?; + + // Discard the barcode scanner at this stage + if (OneContext.hasContext) { + OneContext().pop(); + } + + await lineItem.receive( + OneContext().context!, + destination: destination, + quantity: quantity, + barcode: barcode, + onSuccess: () { + showSnackIcon(L10().receivedItem, success: true); + } ); } diff --git a/lib/barcode/stock.dart b/lib/barcode/stock.dart index d2309010..126d1d06 100644 --- a/lib/barcode/stock.dart +++ b/lib/barcode/stock.dart @@ -33,7 +33,7 @@ class BarcodeScanStockLocationHandler extends BarcodeHandler { // We expect that the barcode points to a 'stocklocation' if (data.containsKey("stocklocation")) { - int _loc = (data["stocklocation"]["pk"] ?? -1) as int; + int _loc = (data["stocklocation"]?["pk"] ?? -1) as int; // A valid stock location! if (_loc > 0) { @@ -83,7 +83,7 @@ class BarcodeScanStockItemHandler extends BarcodeHandler { Future onBarcodeMatched(Map data) async { // We expect that the barcode points to a 'stockitem' if (data.containsKey("stockitem")) { - int _item = (data["stockitem"]["pk"] ?? -1) as int; + int _item = (data["stockitem"]?["pk"] ?? -1) as int; // A valid stock location! if (_item > 0) { diff --git a/lib/inventree/company.dart b/lib/inventree/company.dart index b9bba772..18e543e4 100644 --- a/lib/inventree/company.dart +++ b/lib/inventree/company.dart @@ -18,6 +18,8 @@ class InvenTreeCompany extends InvenTreeModel { @override String get URL => "company/"; + static const String MODEL_TYPE = "company"; + @override List get rolesRequired => ["purchase_order", "sales_order", "return_order"]; @@ -128,6 +130,8 @@ class InvenTreeSupplierPart extends InvenTreeModel { @override String get URL => "company/part/"; + static const String MODEL_TYPE = "supplierpart"; + @override List get rolesRequired => ["part", "purchase_order"]; @@ -171,7 +175,7 @@ class InvenTreeSupplierPart extends InvenTreeModel { 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"); @@ -179,14 +183,14 @@ class InvenTreeSupplierPart extends InvenTreeModel { 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 partName => getString("name", subKey: "part_detail"); @@ -219,21 +223,52 @@ class InvenTreeManufacturerPart extends InvenTreeModel { InvenTreeManufacturerPart.fromJson(Map json) : super.fromJson(json); @override - String url = "company/part/manufacturer/"; + String URL = "company/part/manufacturer/"; + + static const String MODEL_TYPE = "manufacturerpart"; + + @override + List get rolesRequired => ["part"]; + + @override + Map> formFields() { + Map> fields = { + "manufacturer": {}, + "MPN": {}, + "link": {}, + }; + + return fields; + } @override - Map defaultListFilters() { + Map defaultFilters() { return { "manufacturer_detail": "true", + "part_detail": "true", }; } int get partId => getInt("part"); - + + String get partName => getString("name", subKey: "part_detail"); + + String get partDescription => getString("description", subKey: "part_detail"); + + String get partIPN => getString("IPN", subKey: "part_detail"); + + 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 manufacturerDescription => getString("description", subKey: "manufacturer_detail"); + + String get manufacturerImage => (jsondata["manufacturer_detail"]?["image"] ?? jsondata["manufacturer_detail"]?["thumbnail"] ?? InvenTreeAPI.staticThumb) as String; + String get MPN => getString("MPN"); - + @override InvenTreeModel createFromJson(Map json) => InvenTreeManufacturerPart.fromJson(json); } diff --git a/lib/inventree/model.dart b/lib/inventree/model.dart index 6d0e890a..42ced88e 100644 --- a/lib/inventree/model.dart +++ b/lib/inventree/model.dart @@ -66,7 +66,7 @@ class InvenTreeModel { String get WEB_URL => ""; // Return the "model type" of this model - String get MODEL_TYPE => ""; + static const String MODEL_TYPE = ""; // Helper function to set a value in the JSON data void setValue(String key, dynamic value) { diff --git a/lib/inventree/part.dart b/lib/inventree/part.dart index 103fa58c..c803c329 100644 --- a/lib/inventree/part.dart +++ b/lib/inventree/part.dart @@ -23,6 +23,8 @@ class InvenTreePartCategory extends InvenTreeModel { @override String get URL => "part/category/"; + static const String MODEL_TYPE = "partcategory"; + @override List get rolesRequired => ["part_category"]; @@ -79,6 +81,8 @@ class InvenTreePartTestTemplate extends InvenTreeModel { @override String get URL => "part/test-template/"; + static const String MODEL_TYPE = "parttesttemplate"; + String get key => getString("key"); String get testName => getString("test_name"); @@ -192,8 +196,7 @@ class InvenTreePart extends InvenTreeModel { @override String get URL => "part/"; - @override - String get MODEL_TYPE => "part"; + static const String MODEL_TYPE = "part"; @override List get rolesRequired => ["part"]; diff --git a/lib/inventree/project_code.dart b/lib/inventree/project_code.dart index 07a0c19d..e5d45289 100644 --- a/lib/inventree/project_code.dart +++ b/lib/inventree/project_code.dart @@ -16,6 +16,8 @@ class InvenTreeProjectCode extends InvenTreeModel { @override String get URL => "project-code/"; + static const String MODEL_TYPE = "projectcode"; + @override Map> formFields() { return { diff --git a/lib/inventree/purchase_order.dart b/lib/inventree/purchase_order.dart index 82cb0f34..c258c8b6 100644 --- a/lib/inventree/purchase_order.dart +++ b/lib/inventree/purchase_order.dart @@ -1,3 +1,5 @@ +import "package:flutter/cupertino.dart"; +import "package:flutter_tabler_icons/flutter_tabler_icons.dart"; import "package:inventree/api.dart"; import "package:inventree/helpers.dart"; import "package:inventree/inventree/company.dart"; @@ -5,6 +7,9 @@ import "package:inventree/inventree/model.dart"; import "package:inventree/inventree/orders.dart"; import "package:inventree/widget/progress.dart"; +import "package:inventree/api_form.dart"; +import "package:inventree/l10.dart"; + /* * Class representing an individual PurchaseOrder instance @@ -21,8 +26,7 @@ class InvenTreePurchaseOrder extends InvenTreeOrder { @override String get URL => "order/po/"; - @override - String get MODEL_TYPE => "purchaseorder"; + static const String MODEL_TYPE = "purchaseorder"; @override List get rolesRequired => ["purchase_order"]; @@ -212,6 +216,16 @@ class InvenTreePOLineItem extends InvenTreeOrderLine { } } + InvenTreePurchaseOrder? get purchaseOrder { + dynamic detail = jsondata["order_detail"]; + + if (detail == null) { + return null; + } else { + return InvenTreePurchaseOrder.fromJson(detail as Map); + } + } + String get SKU => getString("SKU", subKey: "supplier_part_detail"); double get purchasePrice => getDouble("purchase_price"); @@ -223,6 +237,72 @@ class InvenTreePOLineItem extends InvenTreeOrderLine { Map get orderDetail => getMap("order_detail"); Map get destinationDetail => getMap("destination_detail"); + + // Receive this line item into stock + Future 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; + } + + destination ??= (orderDetail["destination"]) as int?; + + quantity ??= outstanding; + + // Construct form fields + Map fields = { + "line_item": { + "parent": "items", + "nested": true, + "hidden": true, + "value": pk, + }, + "quantity": { + "parent": "items", + "nested": true, + "value": quantity, + }, + "location": {}, + "status": { + "parent": "items", + "nested": true, + }, + "batch_code": { + "parent": "items", + "nested": true, + }, + "barcode": { + "parent": "items", + "nested": true, + "type": "barcode", + "label": L10().barcodeAssign, + "value": barcode, + "required": false, + } + }; + + if (destination != null && destination > 0) { + fields["location"]?["value"] = destination; + } + + InvenTreePurchaseOrder? order = purchaseOrder; + + if (order != null) { + await launchApiForm( + context, + L10().receiveItem, + order.receive_url, + fields, + method: "POST", + icon: TablerIcons.transition_right, + onSuccess: (data) { + if (onSuccess != null) { + onSuccess(); + } + } + ); + } + } } /* diff --git a/lib/inventree/sales_order.dart b/lib/inventree/sales_order.dart index 9acc374b..7372250b 100644 --- a/lib/inventree/sales_order.dart +++ b/lib/inventree/sales_order.dart @@ -24,8 +24,7 @@ class InvenTreeSalesOrder extends InvenTreeOrder { @override String get URL => "order/so/"; - @override - String get MODEL_TYPE => "salesorder"; + static const String MODEL_TYPE = "salesorder"; @override List get rolesRequired => ["sales_order"]; @@ -250,6 +249,8 @@ class InvenTreeSalesOrderShipment extends InvenTreeModel { @override String get URL => "/order/so/shipment/"; + static const String MODEL_TYPE = "salesordershipment"; + @override Map> formFields() { Map> fields = { diff --git a/lib/inventree/sentry.dart b/lib/inventree/sentry.dart index 34894ff4..a052ed57 100644 --- a/lib/inventree/sentry.dart +++ b/lib/inventree/sentry.dart @@ -158,6 +158,11 @@ Future sentryReportMessage(String message, {Map? context}) */ Future sentryReportError(String source, dynamic error, StackTrace? stackTrace, {Map context = const {}}) async { + if (sentryIgnoreError(error)) { + // No action on this error + return; + } + print("----- Sentry Intercepted error: $error -----"); print(stackTrace); @@ -228,3 +233,18 @@ Future sentryReportError(String source, dynamic error, StackTrace? stackTr 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")) { + return true; + } + } + + return false; +} \ No newline at end of file diff --git a/lib/inventree/stock.dart b/lib/inventree/stock.dart index 7b03d18d..1ae5e7ff 100644 --- a/lib/inventree/stock.dart +++ b/lib/inventree/stock.dart @@ -98,7 +98,7 @@ class InvenTreeStockItemHistory extends InvenTreeModel { String get URL => "stock/track/"; @override - Map defaultListFilters() { + Map defaultFilters() { // By default, order by decreasing date return { @@ -168,8 +168,7 @@ class InvenTreeStockItem extends InvenTreeModel { @override String get URL => "stock/"; - @override - String get MODEL_TYPE => "stockitem"; + static const String MODEL_TYPE = "stockitem"; @override List get rolesRequired => ["stock"]; @@ -206,7 +205,7 @@ class InvenTreeStockItem extends InvenTreeModel { if (isSerialized()) { // Prevent editing of 'quantity' field if the item is serialized - fields["quantity"]["hidden"] = true; + fields["quantity"]?["hidden"] = true; } // Old API does not support these fields @@ -395,7 +394,7 @@ class InvenTreeStockItem extends InvenTreeModel { // Use the detailed part information as priority if (jsondata.containsKey("part_detail")) { - nm = (jsondata["part_detail"]["full_name"] ?? "") as String; + nm = (jsondata["part_detail"]?["full_name"] ?? "") as String; } // Backup if first value fails @@ -411,7 +410,7 @@ class InvenTreeStockItem extends InvenTreeModel { // Use the detailed part description as priority if (jsondata.containsKey("part_detail")) { - desc = (jsondata["part_detail"]["description"] ?? "") as String; + desc = (jsondata["part_detail"]?["description"] ?? "") as String; } if (desc.isEmpty) { @@ -425,7 +424,7 @@ class InvenTreeStockItem extends InvenTreeModel { String img = ""; if (jsondata.containsKey("part_detail")) { - img = (jsondata["part_detail"]["thumbnail"] ?? "") as String; + img = (jsondata["part_detail"]?["thumbnail"] ?? "") as String; } if (img.isEmpty) { @@ -468,7 +467,7 @@ class InvenTreeStockItem extends InvenTreeModel { 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; + thumb = (jsondata["supplier_detail"]?["image"] ?? "") as String; } return thumb; @@ -681,8 +680,7 @@ class InvenTreeStockLocation extends InvenTreeModel { @override String get URL => "stock/location/"; - @override - String get MODEL_TYPE => "stocklocation"; + static const String MODEL_TYPE = "stocklocation"; @override List get rolesRequired => ["stock_location"]; diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index d6693bdc..ef3b9d96 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -608,6 +608,9 @@ "itemDeleted": "Item has been removed", "@itemDeleted": {}, + "itemUpdated": "Item updated", + "@itemUpdated": {}, + "keywords": "Keywords", "@keywords": {}, @@ -684,6 +687,12 @@ "lost": "Lost", "@lost": {}, + "manufacturerPart": "Manufacturer Part", + "@manufacturerPart": {}, + + "manufacturerPartEdit": "Edit Manufacturer Part", + "@manufacturerPartEdit": {}, + "manufacturerPartNumber": "Manufacturer Part Number", "@manufacturerPartNumber": {}, @@ -915,6 +924,12 @@ "projectCode": "Project Code", "@projectCode": {}, + "purchaseOrderConfirmScan": "Confirm Scan Data", + "@purchaseOrderConfirmScan": {}, + + "purchaseOrderConfirmScanDetail": "Confirm details when scanning in items", + "@purchaseOrderConfirmScanDetail": {}, + "purchaseOrderEnable": "Enable Purchase Orders", "@purchaseOrderEnable": {}, diff --git a/lib/preferences.dart b/lib/preferences.dart index 57004873..8507cd31 100644 --- a/lib/preferences.dart +++ b/lib/preferences.dart @@ -39,6 +39,7 @@ const String INV_STOCK_CONFIRM_SCAN = "stockConfirmScan"; // Purchase order settings const String INV_PO_ENABLE = "poEnable"; const String INV_PO_SHOW_CAMERA = "poShowCamera"; +const String INV_PO_CONFIRM_SCAN = "poConfirmScan"; // Sales order settings const String INV_SO_ENABLE = "soEnable"; diff --git a/lib/settings/purchase_order_settings.dart b/lib/settings/purchase_order_settings.dart index 5a3b430e..888b89b1 100644 --- a/lib/settings/purchase_order_settings.dart +++ b/lib/settings/purchase_order_settings.dart @@ -19,6 +19,7 @@ class _InvenTreePurchaseOrderSettingsState extends State 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); if (mounted) { setState(() { @@ -75,6 +77,20 @@ class _InvenTreePurchaseOrderSettingsState extends State _ManufacturerPartDisplayState(); +} + + +class _ManufacturerPartDisplayState extends RefreshableState { + + _ManufacturerPartDisplayState(); + + @override + String getAppBarTitle() => L10().manufacturerPart; + + @override + Future request(BuildContext context) async { + final bool result = widget.manufacturerPart.pk > 0 && + await widget.manufacturerPart.reload(); + + if (!result) { + Navigator.of(context).pop(); + } + } + + Future editManufacturerPart(BuildContext context) async { + widget.manufacturerPart.editForm( + context, + L10().manufacturerPartEdit, + onSuccess: (data) async { + refresh(context); + showSnackIcon(L10().itemUpdated, success: true); + } + ); + } + + @override + List barcodeButtons(BuildContext context) { + List actions = []; + + // TODO: Barcode actions? + + return actions; + } + + @override + List appBarActions(BuildContext context) { + List actions = []; + + if (widget.manufacturerPart.canEdit) { + actions.add( + IconButton( + icon: Icon(TablerIcons.edit), + tooltip: L10().edit, + onPressed: () { + editManufacturerPart(context); + } + ) + ); + } + + return actions; + } + + /* + * Build a set of tiles to display for this ManufacturerPart instance + */ + @override + List getTiles(BuildContext context) { + List tiles = []; + + if (loading) { + tiles.add(progressIndicator()); + return tiles; + } + + // 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(); + + if (part is InvenTreePart) { + Navigator.push(context, MaterialPageRoute( + builder: (context) => PartDetailWidget(part))); + } + }, + ) + ); + + // 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(); + + if (supplier is InvenTreeCompany) { + Navigator.push(context, MaterialPageRoute( + builder: (context) => CompanyDetailWidget(supplier) + )); + } + } + ) + ); + + // MPN (part number) + tiles.add( + 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), + ) + ); + } + + 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); + } + }, + ) + ); + } + + return tiles; + } + +} diff --git a/lib/widget/company/supplier_part_detail.dart b/lib/widget/company/supplier_part_detail.dart index c6c50468..c287f0a6 100644 --- a/lib/widget/company/supplier_part_detail.dart +++ b/lib/widget/company/supplier_part_detail.dart @@ -1,21 +1,23 @@ import "package:flutter/material.dart"; import "package:flutter_speed_dial/flutter_speed_dial.dart"; import "package:flutter_tabler_icons/flutter_tabler_icons.dart"; +import "package:url_launcher/url_launcher.dart"; import "package:inventree/api.dart"; import "package:inventree/app_colors.dart"; -import "package:inventree/barcode/barcode.dart"; import "package:inventree/l10.dart"; +import "package:inventree/barcode/barcode.dart"; + import "package:inventree/inventree/part.dart"; import "package:inventree/inventree/company.dart"; -import "package:inventree/widget/company/company_detail.dart"; -import "package:inventree/widget/part/part_detail.dart"; import "package:inventree/widget/progress.dart"; import "package:inventree/widget/refreshable_state.dart"; import "package:inventree/widget/snacks.dart"; -import "package:url_launcher/url_launcher.dart"; +import "package:inventree/widget/company/company_detail.dart"; +import "package:inventree/widget/company/manufacturer_part_detail.dart"; +import "package:inventree/widget/part/part_detail.dart"; /* @@ -180,7 +182,7 @@ class _SupplierPartDisplayState extends RefreshableState ManufacturerPartDetailWidget(manufacturerPart) + )); + } + }, ) ); } diff --git a/lib/widget/order/po_line_detail.dart b/lib/widget/order/po_line_detail.dart index 06e0a735..6ae896e1 100644 --- a/lib/widget/order/po_line_detail.dart +++ b/lib/widget/order/po_line_detail.dart @@ -2,7 +2,6 @@ import "package:flutter/material.dart"; import "package:flutter_speed_dial/flutter_speed_dial.dart"; import "package:flutter_tabler_icons/flutter_tabler_icons.dart"; -import "package:inventree/api_form.dart"; import "package:inventree/app_colors.dart"; import "package:inventree/helpers.dart"; import "package:inventree/inventree/model.dart"; @@ -132,72 +131,15 @@ class _POLineDetailWidgetState extends RefreshableState { // Launch a form to 'receive' this line item Future receiveLineItem(BuildContext context) async { - - // Pre-fill the "destination" to receive into - int destination = widget.item.destinationId; - - if (destination < 0) { - destination = (widget.item.orderDetail["destination"] ?? -1) as int; - } - - // Construct fields to receive - Map fields = { - "line_item": { - "parent": "items", - "nested": true, - "hidden": true, - "value": widget.item.pk, - }, - "quantity": { - "parent": "items", - "nested": true, - "value": widget.item.outstanding, - }, - "status": { - "parent": "items", - "nested": true, - }, - "location": {}, - "batch_code": { - "parent": "items", - "nested": true, - }, - "barcode": { - "parent": "items", - "nested": true, - "type": "barcode", - "label": L10().barcodeAssign, - "required": false, - } - }; - - if (destination > 0) { - fields["location"]?["value"] = destination; - } - - showLoadingOverlay(); - var order = await InvenTreePurchaseOrder().get(widget.item.orderId); - hideLoadingOverlay(); - - if (order is InvenTreePurchaseOrder) { - launchApiForm( + widget.item.receive( context, - L10().receiveItem, - order.receive_url, - fields, - method: "POST", - icon: TablerIcons.transition_right, - onSuccess: (data) async { - showSnackIcon(L10().receivedItem, success: true); - refresh(context); + onSuccess: () => { + showSnackIcon(L10().receivedItem, success: true), + refresh(context) } ); - } else { - showSnackIcon(L10().error); - return; - } } - + @override List getTiles(BuildContext context) { List tiles = []; diff --git a/lib/widget/part/part_detail.dart b/lib/widget/part/part_detail.dart index 0e2f2ca6..a11fe4c2 100644 --- a/lib/widget/part/part_detail.dart +++ b/lib/widget/part/part_detail.dart @@ -239,7 +239,7 @@ class _PartDisplayState extends RefreshableState { 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( diff --git a/lib/widget/stock/location_display.dart b/lib/widget/stock/location_display.dart index 42e8907f..07a8464d 100644 --- a/lib/widget/stock/location_display.dart +++ b/lib/widget/stock/location_display.dart @@ -246,7 +246,7 @@ class _LocationDisplayState extends RefreshableState { if (widget.location != null) { - String model_type = api.supportsModernLabelPrinting ? InvenTreeStockLocation().MODEL_TYPE : "location"; + String model_type = api.supportsModernLabelPrinting ? InvenTreeStockLocation.MODEL_TYPE : "location"; String item_key = api.supportsModernLabelPrinting ? "items" : "location"; _labels = await getLabelTemplates( diff --git a/lib/widget/stock/stock_detail.dart b/lib/widget/stock/stock_detail.dart index 00817089..1b215988 100644 --- a/lib/widget/stock/stock_detail.dart +++ b/lib/widget/stock/stock_detail.dart @@ -298,7 +298,7 @@ class _StockItemDisplayState extends RefreshableState { // 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