import 'package:intl/intl.dart'; import 'package:inventree/inventree/part.dart'; import 'package:flutter/cupertino.dart'; import 'package:http/http.dart' as http; import 'model.dart'; import 'package:inventree/l10.dart'; import 'dart:async'; import 'dart:io'; import 'package:inventree/api.dart'; class InvenTreeStockItemTestResult extends InvenTreeModel { @override String NAME = "StockItemTestResult"; @override String URL = "stock/test/"; String get key => jsondata['key'] ?? ''; String get testName => jsondata['test'] ?? ''; bool get result => jsondata['result'] ?? false; String get value => jsondata['value'] ?? ''; String get notes => jsondata['notes'] ?? ''; String get attachment => jsondata['attachment'] ?? ''; String get date => jsondata['date'] ?? ''; InvenTreeStockItemTestResult() : super(); InvenTreeStockItemTestResult.fromJson(Map json) : super.fromJson(json); @override InvenTreeStockItemTestResult createFromJson(Map json) { var result = InvenTreeStockItemTestResult.fromJson(json); return result; } } class InvenTreeStockItem extends InvenTreeModel { // Stock status codes static const int OK = 10; static const int ATTENTION = 50; static const int DAMAGED = 55; static const int DESTROYED = 60; static const int REJECTED = 65; static const int LOST = 70; static const int RETURNED = 85; String statusLabel(BuildContext context) { // TODO: Delete me - The translated status values are provided by the API! switch (status) { case OK: return L10().ok; case ATTENTION: return L10().attention; case DAMAGED: return L10().damaged; case DESTROYED: return L10().destroyed; case REJECTED: return L10().rejected; case LOST: return L10().lost; case RETURNED: return L10().returned; default: return status.toString(); } } // Return color associated with stock status Color get statusColor { switch (status) { case OK: return Color(0xFF50aa51); case ATTENTION: return Color(0xFFfdc82a); case DAMAGED: case DESTROYED: case REJECTED: return Color(0xFFe35a57); case LOST: default: return Color(0xFFAAAAAA); } } @override String NAME = "StockItem"; @override String URL = "stock/"; @override String WEB_URL = "stock/item/"; @override Map defaultGetFilters() { var headers = new Map(); headers["part_detail"] = "true"; headers["location_detail"] = "true"; headers["supplier_detail"] = "true"; headers["cascade"] = "false"; return headers; } @override Map defaultListFilters() { var headers = new Map(); headers["part_detail"] = "true"; headers["location_detail"] = "true"; headers["supplier_detail"] = "true"; headers["cascade"] = "false"; return headers; } InvenTreeStockItem() : super(); InvenTreeStockItem.fromJson(Map json) : super.fromJson(json) { // TODO } List testTemplates = []; int get testTemplateCount => testTemplates.length; // Get all the test templates associated with this StockItem Future getTestTemplates({bool showDialog=false}) async { await InvenTreePartTestTemplate().list( filters: { "part": "${partId}", }, ).then((var templates) { testTemplates.clear(); for (var t in templates) { if (t is InvenTreePartTestTemplate) { testTemplates.add(t); } } }); } List testResults = []; int get testResultCount => testResults.length; Future getTestResults() async { await InvenTreeStockItemTestResult().list( filters: { "stock_item": "${pk}", "user_detail": "true", }, ).then((var results) { testResults.clear(); for (var r in results) { if (r is InvenTreeStockItemTestResult) { testResults.add(r); } } }); } Future uploadTestResult(BuildContext context, String testName, bool result, {String? value, String? notes, File? attachment}) async { Map data = { "stock_item": pk.toString(), "test": testName, "result": result.toString(), }; if (value != null && value.isNotEmpty) { data["value"] = value; } if (notes != null && notes.isNotEmpty) { data["notes"] = notes; } /* * Upload is performed in different ways, depending if an attachment is provided. * TODO: Is there a nice way to refactor this one? */ if (attachment == null) { var _result = await InvenTreeStockItemTestResult().create(data); return (_result != null) && (_result is InvenTreeStockItemTestResult); } else { var url = InvenTreeStockItemTestResult().URL; http.StreamedResponse _uploadResponse = await InvenTreeAPI().uploadFile(url, attachment, fields: data); // Check that the HTTP status code is HTTP_201_CREATED return _uploadResponse.statusCode == 201; } } String get uid => jsondata['uid'] ?? ''; int get status => jsondata['status'] ?? -1; int get partId => jsondata['part'] ?? -1; String get purchasePrice => jsondata['purchase_price']; bool get hasPurchasePrice { String pp = purchasePrice; return pp.isNotEmpty && pp.trim() != "-"; } int get trackingItemCount => (jsondata['tracking_items'] ?? 0) as int; // Date of last update DateTime? get updatedDate { if (jsondata.containsKey("updated")) { return DateTime.tryParse(jsondata["updated"] ?? ''); } else { return null; } } String? get updatedDateString { var _updated = updatedDate; if (_updated == null) { return null; } final DateFormat _format = DateFormat("yyyy-MM-dd"); return _format.format(_updated); } DateTime? get stocktakeDate { if (jsondata.containsKey("stocktake_date")) { return DateTime.tryParse(jsondata["stocktake_date"] ?? ''); } else { return null; } } String? get stocktakeDateString { var _stocktake = stocktakeDate; if (_stocktake == null) { return null; } final DateFormat _format = DateFormat("yyyy-MM-dd"); return _format.format(_stocktake); } String get partName { String nm = ''; // Use the detailed part information as priority if (jsondata.containsKey('part_detail')) { nm = jsondata['part_detail']['full_name'] ?? ''; } // Backup if first value fails if (nm.isEmpty) { nm = jsondata['part__name'] ?? ''; } return nm; } String get partDescription { String desc = ''; // Use the detailed part description as priority if (jsondata.containsKey('part_detail')) { desc = jsondata['part_detail']['description'] ?? ''; } if (desc.isEmpty) { desc = jsondata['part__description'] ?? ''; } return desc; } String get partImage { String img = ''; if (jsondata.containsKey('part_detail')) { img = jsondata['part_detail']['thumbnail'] ?? ''; } if (img.isEmpty) { img = jsondata['part__thumbnail'] ?? ''; } return img; } /** * Return the Part thumbnail for this stock item. */ String get partThumbnail { String thumb = ""; thumb = jsondata['part_detail']?['thumbnail'] ?? ''; // Use 'image' as a backup if (thumb.isEmpty) { thumb = jsondata['part_detail']?['image'] ?? ''; } // Try a different approach if (thumb.isEmpty) { thumb = jsondata['part__thumbnail'] ?? ''; } // Still no thumbnail? Use the 'no image' image if (thumb.isEmpty) thumb = InvenTreeAPI.staticThumb; return thumb; } int get supplierPartId => (jsondata['supplier_part'] ?? -1) as int; String get supplierImage { String thumb = ''; if (jsondata.containsKey("supplier_detail")) { thumb = jsondata['supplier_detail']['supplier_logo'] ?? ''; } return thumb; } String get supplierName { String sname = ''; if (jsondata.containsKey("supplier_detail")) { sname = jsondata["supplier_detail"]["supplier_name"] ?? ''; } return sname; } String get supplierSKU { String sku = ''; if (jsondata.containsKey("supplier_detail")) { sku = jsondata["supplier_detail"]["SKU"] ?? ''; } return sku; } String get serialNumber => jsondata['serial'] ?? ""; double get quantity => double.tryParse(jsondata['quantity'].toString()) ?? 0; String get quantityString { if (quantity.toInt() == quantity) { return quantity.toInt().toString(); } else { return quantity.toString(); } } int get locationId => (jsondata['location'] ?? -1) as int; bool isSerialized() => serialNumber.isNotEmpty && quantity.toInt() == 1; String serialOrQuantityDisplay() { if (isSerialized()) { return 'SN ${serialNumber}'; } // Is an integer? if (quantity.toInt() == quantity) { return '${quantity.toInt()}'; } return '${quantity}'; } String get locationName { String loc = ''; if (locationId == -1 || !jsondata.containsKey('location_detail')) return 'Unknown Location'; loc = jsondata['location_detail']['name'] ?? ''; // Old-style name if (loc.isEmpty) { loc = jsondata['location__name'] ?? ''; } return loc; } String get locationPathString { if (locationId == -1 || !jsondata.containsKey('location_detail')) return L10().locationNotSet; String _loc = jsondata['location_detail']['pathstring'] ?? ''; if (_loc.isNotEmpty) { return _loc; } else { return locationName; } } String get displayQuantity { // Display either quantity or serial number! if (serialNumber.isNotEmpty) { return "SN: $serialNumber"; } else { return quantityString; } } @override InvenTreeModel createFromJson(Map json) { var item = InvenTreeStockItem.fromJson(json); // TODO? return item; } /* * Perform stocktake action: * * - Add * - Remove * - Count */ Future adjustStock(BuildContext context, String endpoint, double q, {String? notes}) async { // Serialized stock cannot be adjusted if (isSerialized()) { return false; } // Cannot handle negative stock if (q < 0) { return false; } print("Adjust stock: ${endpoint}"); var response = await api.post( endpoint, body: { "item": { "pk": "${pk}", "quantity": "${q}", }, "notes": notes ?? '', }, expectedStatusCode: 200 ); return response.isValid(); } Future countStock(BuildContext context, double q, {String? notes}) async { final bool result = await adjustStock(context, "/stock/count/", q, notes: notes); return result; } Future addStock(BuildContext context, double q, {String? notes}) async { final bool result = await adjustStock(context, "/stock/add/", q, notes: notes); return result; } Future removeStock(BuildContext context, double q, {String? notes}) async { final bool result = await adjustStock(context, "/stock/remove/", q, notes: notes); return result; } Future transferStock(int location, {double? quantity, String? notes}) async { if ((quantity == null) || (quantity < 0) || (quantity > this.quantity)) { quantity = this.quantity; } final response = await api.post( "/stock/transfer/", body: { "item": { "pk": "${pk}", "quantity": "${quantity}", }, "location": "${location}", "notes": notes ?? "", }, expectedStatusCode: 200 ); return response != null; } } class InvenTreeStockLocation extends InvenTreeModel { @override String NAME = "StockLocation"; @override String URL = "stock/location/"; String get pathstring => jsondata['pathstring'] ?? ''; String get parentpathstring { // TODO - Drive the refactor tractor through this List psplit = pathstring.split('/'); if (psplit.length > 0) { psplit.removeLast(); } String p = psplit.join('/'); if (p.isEmpty) { p = "Top level stock location"; } return p; } int get itemcount => jsondata['items'] ?? 0; InvenTreeStockLocation() : super(); InvenTreeStockLocation.fromJson(Map json) : super.fromJson(json) { // TODO } @override InvenTreeModel createFromJson(Map json) { var loc = InvenTreeStockLocation.fromJson(json); return loc; } }