import "dart:async"; import "package:intl/intl.dart"; import "package:inventree/inventree/part.dart"; import "package:flutter/cupertino.dart"; import "package:inventree/inventree/model.dart"; import "package:inventree/l10.dart"; import "package:inventree/api.dart"; class InvenTreeStockItemTestResult extends InvenTreeModel { InvenTreeStockItemTestResult() : super(); InvenTreeStockItemTestResult.fromJson(Map json) : super.fromJson(json); @override String get URL => "stock/test/"; @override Map formFields() { return { "stock_item": { "hidden": true }, "test": {}, "result": {}, "value": {}, "notes": {}, "attachment": {}, }; } String get key => (jsondata["key"] ?? "") as String; String get testName => (jsondata["test"] ?? "") as String; bool get result => (jsondata["result"] ?? false) as bool; String get value => (jsondata["value"] ?? "") as String; String get attachment => (jsondata["attachment"] ?? "") as String; String get date => (jsondata["date"] ?? "") as String; @override InvenTreeStockItemTestResult createFromJson(Map json) { var result = InvenTreeStockItemTestResult.fromJson(json); return result; } } class InvenTreeStockItem extends InvenTreeModel { InvenTreeStockItem() : super(); InvenTreeStockItem.fromJson(Map json) : super.fromJson(json); // 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 get URL => "stock/"; @override String WEB_URL = "stock/item/"; @override Map formFields() { return { "part": {}, "location": {}, "quantity": {}, "status": {}, "batch": {}, "packaging": {}, "link": {}, }; } @override Map defaultGetFilters() { return { "part_detail": "true", "location_detail": "true", "supplier_detail": "true", "cascade": "false" }; } @override Map defaultListFilters() { return { "part_detail": "true", "location_detail": "true", "supplier_detail": "true", "cascade": "false" }; } 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); } } }); } String get uid => (jsondata["uid"] ?? "") as String; int get status => (jsondata["status"] ?? -1) as int; String get packaging => (jsondata["packaging"] ?? "") as String; String get batch => (jsondata["batch"] ?? "") as String; int get partId => (jsondata["part"] ?? -1) as int; String get purchasePrice => (jsondata["purchase_price"] ?? "") as String; bool get hasPurchasePrice { String pp = purchasePrice; return pp.isNotEmpty && pp.trim() != "-"; } int get purchaseOrderId => (jsondata["purchase_order"] ?? -1) as int; 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"] ?? "") as String); } else { return null; } } String get updatedDateString { var _updated = updatedDate; if (_updated == null) { return ""; } 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"] ?? "") as String); } else { return null; } } String get stocktakeDateString { var _stocktake = stocktakeDate; if (_stocktake == null) { return ""; } 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"] ?? "") as String; } // Backup if first value fails if (nm.isEmpty) { nm = (jsondata["part__name"] ?? "") as String; } return nm; } String get partDescription { String desc = ""; // Use the detailed part description as priority if (jsondata.containsKey("part_detail")) { desc = (jsondata["part_detail"]["description"] ?? "") as String; } if (desc.isEmpty) { desc = (jsondata["part__description"] ?? "") as String; } return desc; } String get partImage { String img = ""; if (jsondata.containsKey("part_detail")) { img = (jsondata["part_detail"]["thumbnail"] ?? "") as String; } if (img.isEmpty) { img = (jsondata["part__thumbnail"] ?? "") as String; } return img; } /* * Return the Part thumbnail for this stock item. */ String get partThumbnail { String thumb = ""; thumb = (jsondata["part_detail"]?["thumbnail"] ?? "") as String; // Use "image" as a backup if (thumb.isEmpty) { thumb = (jsondata["part_detail"]?["image"] ?? "") as String; } // Try a different approach if (thumb.isEmpty) { thumb = (jsondata["part__thumbnail"] ?? "") as String; } // 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"] ?? "") as String; } return thumb; } String get supplierName { String sname = ""; if (jsondata.containsKey("supplier_detail")) { sname = (jsondata["supplier_detail"]["supplier_name"] ?? "") as String; } return sname; } String get units { return (jsondata["part_detail"]?["units"] ?? "") as String; } String get supplierSKU { String sku = ""; if (jsondata.containsKey("supplier_detail")) { sku = (jsondata["supplier_detail"]["SKU"] ?? "") as String; } return sku; } String get serialNumber => (jsondata["serial"] ?? "") as String; double get quantity => double.tryParse(jsondata["quantity"].toString()) ?? 0; String get quantityString { String q = quantity.toString(); // Simplify integer values e.g. "1.0" becomes "1" if (quantity.toInt() == quantity) { q = quantity.toInt().toString(); } if (units.isNotEmpty) { q += " ${units}"; } return q; } 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"] ?? "") as String; // Old-style name if (loc.isEmpty) { loc = (jsondata["location__name"] ?? "") as String; } return loc; } String get locationPathString { if (locationId == -1 || !jsondata.containsKey("location_detail")) return L10().locationNotSet; String _loc = (jsondata["location_detail"]["pathstring"] ?? "") as String; 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) { return InvenTreeStockItem.fromJson(json); } /* * 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.isValid() && response.statusCode == 200; } } class InvenTreeStockLocation extends InvenTreeModel { InvenTreeStockLocation() : super(); InvenTreeStockLocation.fromJson(Map json) : super.fromJson(json); @override String get URL => "stock/location/"; String get pathstring => (jsondata["pathstring"] ?? "") as String; @override Map formFields() { return { "name": {}, "description": {}, "parent": {}, }; } 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) as int; @override InvenTreeModel createFromJson(Map json) { var loc = InvenTreeStockLocation.fromJson(json); return loc; } }