2
0
mirror of https://github.com/inventree/inventree-app.git synced 2025-04-27 21:16:48 +00:00

[refactor] Scan improvements (#577)

* Handle error on unexpected barcode response

* Add ManufacturerPart detail view

* Support barcode scanning for manufacturer part

* Refactoring for null checks

* Ignore selected errors in sentry

* Fix API implementation for ManufacturerPart

* Update release notes

* More error handling

* Decode quantity betterer

* Refactoring

* Add option to confirm checkin details

* Improve response handlign

* Cleanup

* Remove unused imports

* Fix async function

* Fix for assigning custom barcode

* Handle barcode scan result for company

* Fix

* Adjust scan priority

* Refactoring MODEL_TYPE

- Use instead of duplicated const strings

* @override fix
This commit is contained in:
Oliver 2024-12-14 15:24:23 +11:00 committed by GitHub
parent 6b179d108c
commit 524c5469f1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 576 additions and 220 deletions

View File

@ -1,12 +1,15 @@
### 0.17.1 - December 2024 ### 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 - Fixes barcode scanning bug which prevents scanning of DataMatrix codes
- Display "destination" information in PurchaseOrder detail view - Display "destination" information in PurchaseOrder detail view
- Pre-fill "location" field when receiving items against PurchaseOrder - Pre-fill "location" field when receiving items against PurchaseOrder
- Fix display of part name in PurchaseOrderLineItem list - Fix display of part name in PurchaseOrderLineItem list
- Adds "assigned to me" filter for Purchase Order list - Adds "assigned to me" filter for Purchase Order list
- Adds "assigned to me" filter for Sales Order list - Adds "assigned to me" filter for Sales Order list
- Updated translations
### 0.17.0 - December 2024 ### 0.17.0 - December 2024
--- ---

View File

@ -11,15 +11,17 @@ import "package:inventree/api.dart";
import "package:inventree/app_colors.dart"; import "package:inventree/app_colors.dart";
import "package:inventree/barcode/barcode.dart"; import "package:inventree/barcode/barcode.dart";
import "package:inventree/helpers.dart"; import "package:inventree/helpers.dart";
import "package:inventree/inventree/sales_order.dart";
import "package:inventree/l10.dart"; import "package:inventree/l10.dart";
import "package:inventree/inventree/company.dart"; import "package:inventree/inventree/company.dart";
import "package:inventree/inventree/part.dart"; import "package:inventree/inventree/part.dart";
import "package:inventree/inventree/project_code.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/stock.dart";
import "package:inventree/inventree/sentry.dart";
import "package:inventree/widget/dialogs.dart"; import "package:inventree/widget/dialogs.dart";
import "package:inventree/widget/fields.dart"; import "package:inventree/widget/fields.dart";
import "package:inventree/widget/progress.dart"; import "package:inventree/widget/progress.dart";
@ -562,11 +564,17 @@ class APIFormField {
Map<String, dynamic> data = item as Map<String, dynamic>; Map<String, dynamic> data = item as Map<String, dynamic>;
switch (model) { switch (model) {
case "part": case InvenTreePart.MODEL_TYPE:
return InvenTreePart.fromJson(data).fullname; 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; return InvenTreePartCategory.fromJson(data).pathstring;
case "stocklocation": case InvenTreeStockLocation.MODEL_TYPE:
return InvenTreeStockLocation.fromJson(data).pathstring; return InvenTreeStockLocation.fromJson(data).pathstring;
default: default:
return "itemAsString not implemented for '${model}'"; return "itemAsString not implemented for '${model}'";
@ -606,10 +614,12 @@ class APIFormField {
Map<String, String> _relatedFieldFilters() { Map<String, String> _relatedFieldFilters() {
switch (model) { switch (model) {
case "supplierpart": case InvenTreeSupplierPart.MODEL_TYPE:
return InvenTreeSupplierPart().defaultListFilters(); return InvenTreeSupplierPart().defaultListFilters();
case "stockitem": case InvenTreeStockItem.MODEL_TYPE:
return InvenTreeStockItem().defaultListFilters(); return InvenTreeStockItem().defaultListFilters();
default:
break;
} }
return {}; return {};
@ -643,7 +653,7 @@ class APIFormField {
} }
switch (model) { switch (model) {
case "part": case InvenTreePart.MODEL_TYPE:
var part = InvenTreePart.fromJson(data); var part = InvenTreePart.fromJson(data);
return ListTile( return ListTile(
@ -657,14 +667,14 @@ class APIFormField {
) : null, ) : null,
leading: extended ? InvenTreeAPI().getThumbnail(part.thumbnail) : null, leading: extended ? InvenTreeAPI().getThumbnail(part.thumbnail) : null,
); );
case "parttesttemplate": case InvenTreePartTestTemplate.MODEL_TYPE:
var template = InvenTreePartTestTemplate.fromJson(data); var template = InvenTreePartTestTemplate.fromJson(data);
return ListTile( return ListTile(
title: Text(template.testName), title: Text(template.testName),
subtitle: Text(template.description), subtitle: Text(template.description),
); );
case "supplierpart": case InvenTreeSupplierPart.MODEL_TYPE:
var part = InvenTreeSupplierPart.fromJson(data); var part = InvenTreeSupplierPart.fromJson(data);
return ListTile( return ListTile(
@ -673,7 +683,7 @@ class APIFormField {
leading: extended ? InvenTreeAPI().getThumbnail(part.partImage) : null, leading: extended ? InvenTreeAPI().getThumbnail(part.partImage) : null,
trailing: extended && part.supplierImage.isNotEmpty ? InvenTreeAPI().getThumbnail(part.supplierImage) : null, trailing: extended && part.supplierImage.isNotEmpty ? InvenTreeAPI().getThumbnail(part.supplierImage) : null,
); );
case "partcategory": case InvenTreePartCategory.MODEL_TYPE:
var cat = InvenTreePartCategory.fromJson(data); var cat = InvenTreePartCategory.fromJson(data);
@ -687,7 +697,7 @@ class APIFormField {
style: TextStyle(fontWeight: selected ? FontWeight.bold : FontWeight.normal), style: TextStyle(fontWeight: selected ? FontWeight.bold : FontWeight.normal),
) : null, ) : null,
); );
case "stockitem": case InvenTreeStockItem.MODEL_TYPE:
var item = InvenTreeStockItem.fromJson(data); var item = InvenTreeStockItem.fromJson(data);
return ListTile( return ListTile(
@ -697,8 +707,7 @@ class APIFormField {
leading: InvenTreeAPI().getThumbnail(item.partThumbnail), leading: InvenTreeAPI().getThumbnail(item.partThumbnail),
trailing: Text(item.quantityString()), trailing: Text(item.quantityString()),
); );
case "stocklocation": case InvenTreeStockLocation.MODEL_TYPE:
var loc = InvenTreeStockLocation.fromJson(data); var loc = InvenTreeStockLocation.fromJson(data);
return ListTile( return ListTile(
@ -711,7 +720,7 @@ class APIFormField {
style: TextStyle(fontWeight: selected ? FontWeight.bold : FontWeight.normal), style: TextStyle(fontWeight: selected ? FontWeight.bold : FontWeight.normal),
) : null, ) : null,
); );
case "salesordershipment": case InvenTreeSalesOrderShipment.MODEL_TYPE:
var shipment = InvenTreeSalesOrderShipment.fromJson(data); var shipment = InvenTreeSalesOrderShipment.fromJson(data);
return ListTile( return ListTile(
@ -733,14 +742,14 @@ class APIFormField {
title: Text(name), title: Text(name),
subtitle: Text(role), subtitle: Text(role),
); );
case "company": case InvenTreeCompany.MODEL_TYPE:
var company = InvenTreeCompany.fromJson(data); var company = InvenTreeCompany.fromJson(data);
return ListTile( return ListTile(
title: Text(company.name), title: Text(company.name),
subtitle: extended ? Text(company.description) : null, subtitle: extended ? Text(company.description) : null,
leading: InvenTreeAPI().getThumbnail(company.thumbnail) leading: InvenTreeAPI().getThumbnail(company.thumbnail)
); );
case "projectcode": case InvenTreeProjectCode.MODEL_TYPE:
var project_code = InvenTreeProjectCode.fromJson(data); var project_code = InvenTreeProjectCode.fromJson(data);
return ListTile( return ListTile(
title: Text(project_code.code), title: Text(project_code.code),

View File

@ -4,7 +4,9 @@ import "package:flutter_speed_dial/flutter_speed_dial.dart";
import "package:flutter_tabler_icons/flutter_tabler_icons.dart"; import "package:flutter_tabler_icons/flutter_tabler_icons.dart";
import "package:inventree/helpers.dart"; import "package:inventree/helpers.dart";
import "package:inventree/inventree/sales_order.dart"; import "package:inventree/inventree/sales_order.dart";
import "package:inventree/inventree/sentry.dart";
import "package:inventree/preferences.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:inventree/widget/order/sales_order_detail.dart";
import "package:one_context/one_context.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/refreshable_state.dart";
import "package:inventree/widget/snacks.dart"; import "package:inventree/widget/snacks.dart";
import "package:inventree/widget/stock/stock_detail.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"; import "package:inventree/widget/company/supplier_part_detail.dart";
@ -176,15 +179,37 @@ class BarcodeScanHandler extends BarcodeHandler {
*/ */
Future<void> handleSupplierPart(int pk) async { Future<void> 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().pop();
OneContext().push(MaterialPageRoute( OneContext().push(MaterialPageRoute(
builder: (context) => SupplierPartDetailWidget(supplierpart))); builder: (context) => SupplierPartDetailWidget(supplierPart)));
} }
} }
/*
* Response when a "ManufacturerPart" instance is scanned
*/
Future<void> handleManufacturerPart(int pk) async {
var manufacturerPart = await InvenTreeManufacturerPart().get(pk);
if (manufacturerPart is InvenTreeManufacturerPart) {
OneContext().pop();
OneContext().push(MaterialPageRoute(
builder: (context) => ManufacturerPartDetailWidget(manufacturerPart)));
}
}
Future<void> 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 * Response when a "PurchaseOrder" instance is scanned
@ -218,26 +243,32 @@ class BarcodeScanHandler extends BarcodeHandler {
// The following model types can be matched with barcodes // The following model types can be matched with barcodes
List<String> validModels = [ List<String> validModels = [
"part", InvenTreePart.MODEL_TYPE,
"stockitem", InvenTreeCompany.MODEL_TYPE,
"stocklocation", InvenTreeStockItem.MODEL_TYPE,
"supplierpart", InvenTreeStockLocation.MODEL_TYPE,
InvenTreeSupplierPart.MODEL_TYPE,
InvenTreeManufacturerPart.MODEL_TYPE,
]; ];
if (InvenTreeAPI().supportsOrderBarcodes) { if (InvenTreeAPI().supportsOrderBarcodes) {
validModels.add("purchaseorder"); validModels.add(InvenTreePurchaseOrder.MODEL_TYPE);
validModels.add("salesorder"); validModels.add(InvenTreeSalesOrder.MODEL_TYPE);
} }
for (var key in validModels) { for (var key in validModels) {
if (data.containsKey(key)) { 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 // Break on the first valid match found
if (pk > 0) { if (pk > 0) {
model = key; model = key;
break; break;
}
} catch (error, stackTrace) {
sentryReportError("onBarcodeMatched", error, stackTrace);
} }
} }
} }
@ -248,25 +279,30 @@ class BarcodeScanHandler extends BarcodeHandler {
barcodeSuccessTone(); barcodeSuccessTone();
switch (model) { switch (model) {
case "part": case InvenTreeStockItem.MODEL_TYPE:
await handlePart(pk);
return;
case "stockitem":
await handleStockItem(pk); await handleStockItem(pk);
return; return;
case "stocklocation": case InvenTreePurchaseOrder.MODEL_TYPE:
await handleStockLocation(pk);
return;
case "supplierpart":
await handleSupplierPart(pk);
return;
case "purchaseorder":
await handlePurchaseOrder(pk); await handlePurchaseOrder(pk);
return; return;
case "salesorder": case InvenTreeSalesOrder.MODEL_TYPE:
await handleSalesOrder(pk); await handleSalesOrder(pk);
return; return;
// TODO: Handle manufacturer part case InvenTreeStockLocation.MODEL_TYPE:
await handleStockLocation(pk);
return;
case InvenTreeSupplierPart.MODEL_TYPE:
await handleSupplierPart(pk);
return;
case InvenTreeManufacturerPart.MODEL_TYPE:
await handleManufacturerPart(pk);
return;
case InvenTreePart.MODEL_TYPE:
await handlePart(pk);
return;
case InvenTreeCompany.MODEL_TYPE:
await handleCompany(pk);
return;
default: default:
// Fall through to failure state // Fall through to failure state
break; break;
@ -324,21 +360,6 @@ class UniqueBarcodeHandler extends BarcodeHandler {
@override @override
Future<void> onBarcodeMatched(Map<String, dynamic> data) async { Future<void> onBarcodeMatched(Map<String, dynamic> 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<void> onBarcodeUnknown(Map<String, dynamic> data) async {
// If the barcode is unknown, we *can* assign it to the stock item!
if (!data.containsKey("hash") && !data.containsKey("barcode_hash")) { if (!data.containsKey("hash") && !data.containsKey("barcode_hash")) {
showServerError( showServerError(
"barcode/", "barcode/",
@ -370,6 +391,12 @@ class UniqueBarcodeHandler extends BarcodeHandler {
} }
} }
} }
@override
Future<void> onBarcodeUnknown(Map<String, dynamic> data) async {
await onBarcodeMatched(data);
}
} }

View File

@ -93,7 +93,7 @@ class _CameraBarcodeControllerState extends InvenTreeBarcodeControllerState {
/* /*
* Callback function when a barcode is scanned * Callback function when a barcode is scanned
*/ */
void _onScanSuccess(Code? code) { Future<void> onScanSuccess(Code? code) async {
if (scanning_paused) { if (scanning_paused) {
return; return;
@ -122,18 +122,16 @@ class _CameraBarcodeControllerState extends InvenTreeBarcodeControllerState {
if (mounted) { if (mounted) {
setState(() { setState(() {
scanned_code = barcode; scanned_code = barcode;
scanning_paused = barcode.isNotEmpty;
}); });
} }
if (barcode.isNotEmpty) { if (barcode.isNotEmpty) {
handleBarcodeData(barcode).then((_) { pauseScan();
await handleBarcodeData(barcode).then((_) {
if (!single_scanning && mounted) { if (!single_scanning && mounted) {
// Resume next scan resumeScan();
setState(() {
scanning_paused = false;
});
} }
}); });
} }
@ -186,7 +184,7 @@ class _CameraBarcodeControllerState extends InvenTreeBarcodeControllerState {
Widget BarcodeReader(BuildContext context) { Widget BarcodeReader(BuildContext context) {
return ReaderWidget( return ReaderWidget(
onScan: _onScanSuccess, onScan: onScanSuccess,
isMultiScan: false, isMultiScan: false,
tryHarder: true, tryHarder: true,
tryInverted: true, tryInverted: true,

View File

@ -79,14 +79,27 @@ class BarcodeHandler {
return; return;
} }
var response = await InvenTreeAPI().post( APIResponse? response;
try {
response = await InvenTreeAPI().post(
url, url,
body: { body: {
"barcode": barcode, "barcode": barcode,
...extra_data, ...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()); debug("Barcode scan response" + response.data.toString());
@ -94,7 +107,7 @@ class BarcodeHandler {
// Handle strange response from the server // Handle strange response from the server
if (!response.isValid() || !response.isMap()) { if (!response.isValid() || !response.isMap()) {
onBarcodeUnknown({}); await onBarcodeUnknown({});
showSnackIcon(L10().serverError, success: false); showSnackIcon(L10().serverError, success: false);

View File

@ -1,9 +1,7 @@
import "package:flutter/material.dart"; 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:one_context/one_context.dart";
import "package:inventree/l10.dart"; import "package:inventree/l10.dart";
import "package:inventree/api_form.dart";
import "package:inventree/barcode/barcode.dart"; import "package:inventree/barcode/barcode.dart";
import "package:inventree/barcode/handler.dart"; import "package:inventree/barcode/handler.dart";
@ -23,10 +21,11 @@ import "package:inventree/widget/snacks.dart";
*/ */
class POReceiveBarcodeHandler extends BarcodeHandler { class POReceiveBarcodeHandler extends BarcodeHandler {
POReceiveBarcodeHandler({this.purchaseOrder, this.location}); POReceiveBarcodeHandler({this.purchaseOrder, this.location, this.lineItem});
InvenTreePurchaseOrder? purchaseOrder; InvenTreePurchaseOrder? purchaseOrder;
InvenTreeStockLocation? location; InvenTreeStockLocation? location;
InvenTreePOLineItem? lineItem;
@override @override
String getOverlayText(BuildContext context) => L10().barcodeReceivePart; String getOverlayText(BuildContext context) => L10().barcodeReceivePart;
@ -34,11 +33,15 @@ class POReceiveBarcodeHandler extends BarcodeHandler {
@override @override
Future<void> processBarcode(String barcode, Future<void> processBarcode(String barcode,
{String url = "barcode/po-receive/", {String url = "barcode/po-receive/",
Map<String, dynamic> extra_data = const {}}) { Map<String, dynamic> extra_data = const {}}) async {
final bool confirm = await InvenTreeSettingsManager().getBool(INV_PO_CONFIRM_SCAN, true);
final po_extra_data = { final po_extra_data = {
"purchase_order": purchaseOrder?.pk, "purchase_order": purchaseOrder?.pk,
"location": location?.pk, "location": location?.pk,
"line_item": lineItem?.pk,
"auto_allocate": !confirm,
...extra_data, ...extra_data,
}; };
@ -47,11 +50,13 @@ class POReceiveBarcodeHandler extends BarcodeHandler {
@override @override
Future<void> onBarcodeMatched(Map<String, dynamic> data) async { Future<void> onBarcodeMatched(Map<String, dynamic> data) async {
if (!data.containsKey("lineitem")) {
if (data.containsKey("lineitem") || data.containsKey("success")) {
barcodeSuccess(L10().receivedItem);
return;
} else {
return onBarcodeUnknown(data); return onBarcodeUnknown(data);
} }
barcodeSuccess(L10().receivedItem);
} }
@override @override
@ -66,49 +71,41 @@ class POReceiveBarcodeHandler extends BarcodeHandler {
showSnackIcon(L10().missingData, success: false); showSnackIcon(L10().missingData, success: false);
} }
// Construct fields to receive // At minimum, we need the line item ID value
Map<String, dynamic> fields = { final int? lineItemId = lineItemData["pk"] as int?;
"line_item": {
"parent": "items", if (lineItemId == null) {
"nested": true, barcodeFailureTone();
"hidden": true, return;
"value": lineItemData["pk"] as int, }
},
"quantity": { InvenTreePOLineItem? lineItem = await InvenTreePOLineItem().get(lineItemId) as InvenTreePOLineItem?;
"parent": "items",
"nested": true, if (lineItem == null) {
"value": lineItemData["quantity"] as double?, barcodeFailureTone();
}, return;
"status": { }
"parent": "items",
"nested": true, // Next, extract the "optional" fields
},
"location": { // Extract information from the returned server response
"value": lineItemData["location"] as int?, double? quantity = double.tryParse((lineItemData["quantity"] ?? "0").toString());
}, int? destination = lineItemData["location"] as int?;
"barcode": { String? barcode = data["barcode_data"] as String?;
"parent": "items",
"nested": true, // Discard the barcode scanner at this stage
"hidden": true, if (OneContext.hasContext) {
"type": "barcode", OneContext().pop();
"value": data["barcode_data"] as String, }
await lineItem.receive(
OneContext().context!,
destination: destination,
quantity: quantity,
barcode: barcode,
onSuccess: () {
showSnackIcon(L10().receivedItem, success: true);
} }
};
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);
}
); );
} }

View File

@ -33,7 +33,7 @@ class BarcodeScanStockLocationHandler extends BarcodeHandler {
// We expect that the barcode points to a 'stocklocation' // We expect that the barcode points to a 'stocklocation'
if (data.containsKey("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! // A valid stock location!
if (_loc > 0) { if (_loc > 0) {
@ -83,7 +83,7 @@ class BarcodeScanStockItemHandler extends BarcodeHandler {
Future<void> onBarcodeMatched(Map<String, dynamic> data) async { Future<void> onBarcodeMatched(Map<String, dynamic> data) async {
// We expect that the barcode points to a 'stockitem' // We expect that the barcode points to a 'stockitem'
if (data.containsKey("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! // A valid stock location!
if (_item > 0) { if (_item > 0) {

View File

@ -18,6 +18,8 @@ class InvenTreeCompany extends InvenTreeModel {
@override @override
String get URL => "company/"; String get URL => "company/";
static const String MODEL_TYPE = "company";
@override @override
List<String> get rolesRequired => ["purchase_order", "sales_order", "return_order"]; List<String> get rolesRequired => ["purchase_order", "sales_order", "return_order"];
@ -128,6 +130,8 @@ class InvenTreeSupplierPart extends InvenTreeModel {
@override @override
String get URL => "company/part/"; String get URL => "company/part/";
static const String MODEL_TYPE = "supplierpart";
@override @override
List<String> get rolesRequired => ["part", "purchase_order"]; List<String> get rolesRequired => ["part", "purchase_order"];
@ -171,7 +175,7 @@ class InvenTreeSupplierPart extends InvenTreeModel {
String get MPN => getString("MPN", subKey: "manufacturer_part_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 manufacturerPartId => getInt("manufacturer_part");
@ -179,14 +183,14 @@ class InvenTreeSupplierPart extends InvenTreeModel {
String get supplierName => getString("name", subKey: "supplier_detail"); 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"); String get SKU => getString("SKU");
bool get active => getBool("active", backup: true); bool get active => getBool("active", backup: true);
int get partId => getInt("part"); 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"); String get partName => getString("name", subKey: "part_detail");
@ -219,21 +223,52 @@ class InvenTreeManufacturerPart extends InvenTreeModel {
InvenTreeManufacturerPart.fromJson(Map<String, dynamic> json) : super.fromJson(json); InvenTreeManufacturerPart.fromJson(Map<String, dynamic> json) : super.fromJson(json);
@override @override
String url = "company/part/manufacturer/"; String URL = "company/part/manufacturer/";
static const String MODEL_TYPE = "manufacturerpart";
@override @override
Map<String, String> defaultListFilters() { List<String> get rolesRequired => ["part"];
@override
Map<String, Map<String, dynamic>> formFields() {
Map<String, Map<String, dynamic>> fields = {
"manufacturer": {},
"MPN": {},
"link": {},
};
return fields;
}
@override
Map<String, String> defaultFilters() {
return { return {
"manufacturer_detail": "true", "manufacturer_detail": "true",
"part_detail": "true",
}; };
} }
int get partId => getInt("part"); 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"); 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"); String get MPN => getString("MPN");
@override @override
InvenTreeModel createFromJson(Map<String, dynamic> json) => InvenTreeManufacturerPart.fromJson(json); InvenTreeModel createFromJson(Map<String, dynamic> json) => InvenTreeManufacturerPart.fromJson(json);
} }

View File

@ -66,7 +66,7 @@ class InvenTreeModel {
String get WEB_URL => ""; String get WEB_URL => "";
// Return the "model type" of this model // 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 // Helper function to set a value in the JSON data
void setValue(String key, dynamic value) { void setValue(String key, dynamic value) {

View File

@ -23,6 +23,8 @@ class InvenTreePartCategory extends InvenTreeModel {
@override @override
String get URL => "part/category/"; String get URL => "part/category/";
static const String MODEL_TYPE = "partcategory";
@override @override
List<String> get rolesRequired => ["part_category"]; List<String> get rolesRequired => ["part_category"];
@ -79,6 +81,8 @@ class InvenTreePartTestTemplate extends InvenTreeModel {
@override @override
String get URL => "part/test-template/"; String get URL => "part/test-template/";
static const String MODEL_TYPE = "parttesttemplate";
String get key => getString("key"); String get key => getString("key");
String get testName => getString("test_name"); String get testName => getString("test_name");
@ -192,8 +196,7 @@ class InvenTreePart extends InvenTreeModel {
@override @override
String get URL => "part/"; String get URL => "part/";
@override static const String MODEL_TYPE = "part";
String get MODEL_TYPE => "part";
@override @override
List<String> get rolesRequired => ["part"]; List<String> get rolesRequired => ["part"];

View File

@ -16,6 +16,8 @@ class InvenTreeProjectCode extends InvenTreeModel {
@override @override
String get URL => "project-code/"; String get URL => "project-code/";
static const String MODEL_TYPE = "projectcode";
@override @override
Map<String, Map<String, dynamic>> formFields() { Map<String, Map<String, dynamic>> formFields() {
return { return {

View File

@ -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/api.dart";
import "package:inventree/helpers.dart"; import "package:inventree/helpers.dart";
import "package:inventree/inventree/company.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/inventree/orders.dart";
import "package:inventree/widget/progress.dart"; import "package:inventree/widget/progress.dart";
import "package:inventree/api_form.dart";
import "package:inventree/l10.dart";
/* /*
* Class representing an individual PurchaseOrder instance * Class representing an individual PurchaseOrder instance
@ -21,8 +26,7 @@ class InvenTreePurchaseOrder extends InvenTreeOrder {
@override @override
String get URL => "order/po/"; String get URL => "order/po/";
@override static const String MODEL_TYPE = "purchaseorder";
String get MODEL_TYPE => "purchaseorder";
@override @override
List<String> get rolesRequired => ["purchase_order"]; List<String> 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, dynamic>);
}
}
String get SKU => getString("SKU", subKey: "supplier_part_detail"); String get SKU => getString("SKU", subKey: "supplier_part_detail");
double get purchasePrice => getDouble("purchase_price"); double get purchasePrice => getDouble("purchase_price");
@ -223,6 +237,72 @@ class InvenTreePOLineItem extends InvenTreeOrderLine {
Map<String, dynamic> get orderDetail => getMap("order_detail"); Map<String, dynamic> get orderDetail => getMap("order_detail");
Map<String, dynamic> get destinationDetail => getMap("destination_detail"); 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 {
// 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<String, dynamic> 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();
}
}
);
}
}
} }
/* /*

View File

@ -24,8 +24,7 @@ class InvenTreeSalesOrder extends InvenTreeOrder {
@override @override
String get URL => "order/so/"; String get URL => "order/so/";
@override static const String MODEL_TYPE = "salesorder";
String get MODEL_TYPE => "salesorder";
@override @override
List<String> get rolesRequired => ["sales_order"]; List<String> get rolesRequired => ["sales_order"];
@ -250,6 +249,8 @@ class InvenTreeSalesOrderShipment extends InvenTreeModel {
@override @override
String get URL => "/order/so/shipment/"; String get URL => "/order/so/shipment/";
static const String MODEL_TYPE = "salesordershipment";
@override @override
Map<String, Map<String, dynamic>> formFields() { Map<String, Map<String, dynamic>> formFields() {
Map<String, Map<String, dynamic>> fields = { Map<String, Map<String, dynamic>> fields = {

View File

@ -158,6 +158,11 @@ Future<bool> sentryReportMessage(String message, {Map<String, String>? context})
*/ */
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;
}
print("----- Sentry Intercepted error: $error -----"); print("----- Sentry Intercepted error: $error -----");
print(stackTrace); print(stackTrace);
@ -228,3 +233,18 @@ Future<void> sentryReportError(String source, dynamic error, StackTrace? stackTr
print("Uploaded information to Sentry.io : ${response.toString()}"); 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;
}

View File

@ -98,7 +98,7 @@ class InvenTreeStockItemHistory extends InvenTreeModel {
String get URL => "stock/track/"; String get URL => "stock/track/";
@override @override
Map<String, String> defaultListFilters() { Map<String, String> defaultFilters() {
// By default, order by decreasing date // By default, order by decreasing date
return { return {
@ -168,8 +168,7 @@ class InvenTreeStockItem extends InvenTreeModel {
@override @override
String get URL => "stock/"; String get URL => "stock/";
@override static const String MODEL_TYPE = "stockitem";
String get MODEL_TYPE => "stockitem";
@override @override
List<String> get rolesRequired => ["stock"]; List<String> get rolesRequired => ["stock"];
@ -206,7 +205,7 @@ class InvenTreeStockItem extends InvenTreeModel {
if (isSerialized()) { if (isSerialized()) {
// Prevent editing of 'quantity' field if the item is serialized // 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 // Old API does not support these fields
@ -395,7 +394,7 @@ class InvenTreeStockItem extends InvenTreeModel {
// Use the detailed part information as priority // Use the detailed part information as priority
if (jsondata.containsKey("part_detail")) { 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 // Backup if first value fails
@ -411,7 +410,7 @@ class InvenTreeStockItem extends InvenTreeModel {
// Use the detailed part description as priority // Use the detailed part description as priority
if (jsondata.containsKey("part_detail")) { if (jsondata.containsKey("part_detail")) {
desc = (jsondata["part_detail"]["description"] ?? "") as String; desc = (jsondata["part_detail"]?["description"] ?? "") as String;
} }
if (desc.isEmpty) { if (desc.isEmpty) {
@ -425,7 +424,7 @@ class InvenTreeStockItem extends InvenTreeModel {
String img = ""; String img = "";
if (jsondata.containsKey("part_detail")) { if (jsondata.containsKey("part_detail")) {
img = (jsondata["part_detail"]["thumbnail"] ?? "") as String; img = (jsondata["part_detail"]?["thumbnail"] ?? "") as String;
} }
if (img.isEmpty) { if (img.isEmpty) {
@ -468,7 +467,7 @@ class InvenTreeStockItem extends InvenTreeModel {
if (jsondata.containsKey("supplier_part_detail")) { if (jsondata.containsKey("supplier_part_detail")) {
thumb = (jsondata["supplier_part_detail"]?["supplier_detail"]?["image"] ?? "") as String; thumb = (jsondata["supplier_part_detail"]?["supplier_detail"]?["image"] ?? "") as String;
} else if (jsondata.containsKey("supplier_detail")) { } else if (jsondata.containsKey("supplier_detail")) {
thumb = (jsondata["supplier_detail"]["image"] ?? "") as String; thumb = (jsondata["supplier_detail"]?["image"] ?? "") as String;
} }
return thumb; return thumb;
@ -681,8 +680,7 @@ class InvenTreeStockLocation extends InvenTreeModel {
@override @override
String get URL => "stock/location/"; String get URL => "stock/location/";
@override static const String MODEL_TYPE = "stocklocation";
String get MODEL_TYPE => "stocklocation";
@override @override
List<String> get rolesRequired => ["stock_location"]; List<String> get rolesRequired => ["stock_location"];

View File

@ -608,6 +608,9 @@
"itemDeleted": "Item has been removed", "itemDeleted": "Item has been removed",
"@itemDeleted": {}, "@itemDeleted": {},
"itemUpdated": "Item updated",
"@itemUpdated": {},
"keywords": "Keywords", "keywords": "Keywords",
"@keywords": {}, "@keywords": {},
@ -684,6 +687,12 @@
"lost": "Lost", "lost": "Lost",
"@lost": {}, "@lost": {},
"manufacturerPart": "Manufacturer Part",
"@manufacturerPart": {},
"manufacturerPartEdit": "Edit Manufacturer Part",
"@manufacturerPartEdit": {},
"manufacturerPartNumber": "Manufacturer Part Number", "manufacturerPartNumber": "Manufacturer Part Number",
"@manufacturerPartNumber": {}, "@manufacturerPartNumber": {},
@ -915,6 +924,12 @@
"projectCode": "Project Code", "projectCode": "Project Code",
"@projectCode": {}, "@projectCode": {},
"purchaseOrderConfirmScan": "Confirm Scan Data",
"@purchaseOrderConfirmScan": {},
"purchaseOrderConfirmScanDetail": "Confirm details when scanning in items",
"@purchaseOrderConfirmScanDetail": {},
"purchaseOrderEnable": "Enable Purchase Orders", "purchaseOrderEnable": "Enable Purchase Orders",
"@purchaseOrderEnable": {}, "@purchaseOrderEnable": {},

View File

@ -39,6 +39,7 @@ const String INV_STOCK_CONFIRM_SCAN = "stockConfirmScan";
// Purchase order settings // Purchase order settings
const String INV_PO_ENABLE = "poEnable"; const String INV_PO_ENABLE = "poEnable";
const String INV_PO_SHOW_CAMERA = "poShowCamera"; const String INV_PO_SHOW_CAMERA = "poShowCamera";
const String INV_PO_CONFIRM_SCAN = "poConfirmScan";
// Sales order settings // Sales order settings
const String INV_SO_ENABLE = "soEnable"; const String INV_SO_ENABLE = "soEnable";

View File

@ -19,6 +19,7 @@ class _InvenTreePurchaseOrderSettingsState extends State<InvenTreePurchaseOrderS
bool poEnable = true; bool poEnable = true;
bool poShowCamera = true; bool poShowCamera = true;
bool poConfirmScan = true;
@override @override
void initState() { void initState() {
@ -30,6 +31,7 @@ class _InvenTreePurchaseOrderSettingsState extends State<InvenTreePurchaseOrderS
Future<void> loadSettings() async { Future<void> loadSettings() async {
poEnable = await InvenTreeSettingsManager().getBool(INV_PO_ENABLE, true); poEnable = await InvenTreeSettingsManager().getBool(INV_PO_ENABLE, true);
poShowCamera = await InvenTreeSettingsManager().getBool(INV_PO_SHOW_CAMERA, true); poShowCamera = await InvenTreeSettingsManager().getBool(INV_PO_SHOW_CAMERA, true);
poConfirmScan = await InvenTreeSettingsManager().getBool(INV_PO_CONFIRM_SCAN, true);
if (mounted) { if (mounted) {
setState(() { setState(() {
@ -75,6 +77,20 @@ class _InvenTreePurchaseOrderSettingsState extends State<InvenTreePurchaseOrderS
}, },
), ),
), ),
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

@ -0,0 +1,183 @@
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/l10.dart";
import "package:inventree/api.dart";
import "package:inventree/app_colors.dart";
import "package:inventree/inventree/company.dart";
import "package:inventree/inventree/part.dart";
import "package:inventree/widget/refreshable_state.dart";
import "package:inventree/widget/snacks.dart";
import "package:inventree/widget/progress.dart";
import "package:inventree/widget/part/part_detail.dart";
import "package:inventree/widget/company/company_detail.dart";
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);
final InvenTreeManufacturerPart manufacturerPart;
@override
_ManufacturerPartDisplayState createState() => _ManufacturerPartDisplayState();
}
class _ManufacturerPartDisplayState extends RefreshableState<ManufacturerPartDetailWidget> {
_ManufacturerPartDisplayState();
@override
String getAppBarTitle() => L10().manufacturerPart;
@override
Future<void> request(BuildContext context) async {
final bool result = widget.manufacturerPart.pk > 0 &&
await widget.manufacturerPart.reload();
if (!result) {
Navigator.of(context).pop();
}
}
Future<void> editManufacturerPart(BuildContext context) async {
widget.manufacturerPart.editForm(
context,
L10().manufacturerPartEdit,
onSuccess: (data) async {
refresh(context);
showSnackIcon(L10().itemUpdated, success: true);
}
);
}
@override
List<SpeedDialChild> barcodeButtons(BuildContext context) {
List<SpeedDialChild> actions = [];
// TODO: Barcode actions?
return actions;
}
@override
List<Widget> appBarActions(BuildContext context) {
List<Widget> 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<Widget> getTiles(BuildContext context) {
List<Widget> 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;
}
}

View File

@ -1,21 +1,23 @@
import "package:flutter/material.dart"; import "package:flutter/material.dart";
import "package:flutter_speed_dial/flutter_speed_dial.dart"; import "package:flutter_speed_dial/flutter_speed_dial.dart";
import "package:flutter_tabler_icons/flutter_tabler_icons.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/api.dart";
import "package:inventree/app_colors.dart"; import "package:inventree/app_colors.dart";
import "package:inventree/barcode/barcode.dart";
import "package:inventree/l10.dart"; import "package:inventree/l10.dart";
import "package:inventree/barcode/barcode.dart";
import "package:inventree/inventree/part.dart"; import "package:inventree/inventree/part.dart";
import "package:inventree/inventree/company.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/progress.dart";
import "package:inventree/widget/refreshable_state.dart"; import "package:inventree/widget/refreshable_state.dart";
import "package:inventree/widget/snacks.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<SupplierPartDetailWidge
ListTile( ListTile(
title: Text(L10().supplierPartNumber), title: Text(L10().supplierPartNumber),
subtitle: Text(widget.supplierPart.SKU), subtitle: Text(widget.supplierPart.SKU),
leading: Icon(TablerIcons.barcode), leading: Icon(TablerIcons.hash),
) )
); );
@ -210,7 +212,18 @@ class _SupplierPartDisplayState extends RefreshableState<SupplierPartDetailWidge
ListTile( ListTile(
title: Text(L10().manufacturerPartNumber), title: Text(L10().manufacturerPartNumber),
subtitle: Text(widget.supplierPart.MPN), subtitle: Text(widget.supplierPart.MPN),
leading: Icon(TablerIcons.barcode), leading: Icon(TablerIcons.hash, color: COLOR_ACTION),
onTap: () async {
showLoadingOverlay();
var manufacturerPart = await InvenTreeManufacturerPart().get(widget.supplierPart.manufacturerPartId);
hideLoadingOverlay();
if (manufacturerPart is InvenTreeManufacturerPart) {
Navigator.push(context, MaterialPageRoute(
builder: (context) => ManufacturerPartDetailWidget(manufacturerPart)
));
}
},
) )
); );
} }

View File

@ -2,7 +2,6 @@ import "package:flutter/material.dart";
import "package:flutter_speed_dial/flutter_speed_dial.dart"; import "package:flutter_speed_dial/flutter_speed_dial.dart";
import "package:flutter_tabler_icons/flutter_tabler_icons.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/app_colors.dart";
import "package:inventree/helpers.dart"; import "package:inventree/helpers.dart";
import "package:inventree/inventree/model.dart"; import "package:inventree/inventree/model.dart";
@ -132,72 +131,15 @@ class _POLineDetailWidgetState extends RefreshableState<POLineDetailWidget> {
// Launch a form to 'receive' this line item // Launch a form to 'receive' this line item
Future<void> receiveLineItem(BuildContext context) async { Future<void> receiveLineItem(BuildContext context) async {
widget.item.receive(
// 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<String, dynamic> 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(
context, context,
L10().receiveItem, onSuccess: () => {
order.receive_url, showSnackIcon(L10().receivedItem, success: true),
fields, refresh(context)
method: "POST",
icon: TablerIcons.transition_right,
onSuccess: (data) async {
showSnackIcon(L10().receivedItem, success: true);
refresh(context);
} }
); );
} else {
showSnackIcon(L10().error);
return;
}
} }
@override @override
List<Widget> getTiles(BuildContext context) { List<Widget> getTiles(BuildContext context) {
List<Widget> tiles = []; List<Widget> tiles = [];

View File

@ -239,7 +239,7 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
if (allowLabelPrinting) { 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"; String item_key = api.supportsModernLabelPrinting ? "items" : "part";
_labels = await getLabelTemplates( _labels = await getLabelTemplates(

View File

@ -246,7 +246,7 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
if (widget.location != null) { 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"; String item_key = api.supportsModernLabelPrinting ? "items" : "location";
_labels = await getLabelTemplates( _labels = await getLabelTemplates(

View File

@ -298,7 +298,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
// Request information on labels available for this stock item // Request information on labels available for this stock item
if (allowLabelPrinting) { 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"; String item_key = api.supportsModernLabelPrinting ? "items" : "item";
// Clear the existing labels list // Clear the existing labels list