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

686 lines
15 KiB
Dart

import "dart:async";
import "package:inventree/api.dart";
import "package:inventree/helpers.dart";
import "package:inventree/l10.dart";
import "package:inventree/inventree/part.dart";
import "package:inventree/inventree/model.dart";
/*
* Class representing a test result for a single stock item
*/
class InvenTreeStockItemTestResult extends InvenTreeModel {
InvenTreeStockItemTestResult() : super();
InvenTreeStockItemTestResult.fromJson(Map<String, dynamic> json) : super.fromJson(json);
@override
String get URL => "stock/test/";
@override
List<String> get rolesRequired => ["stock"];
@override
Map<String, String> defaultFilters() {
return {
"user_detail": "true",
"template_detail": "true",
};
}
@override
Map<String, Map<String, dynamic>> formFields() {
Map<String, Map<String, dynamic>> fields = {
"stock_item": {"hidden": true},
"test": {},
"template": {
"filters": {
"enabled": "true",
}
},
"result": {},
"value": {},
"notes": {},
"attachment": {},
};
if (InvenTreeAPI().supportsModernTestResults) {
fields.remove("test");
} else {
fields.remove("template");
}
return fields;
}
String get key => getString("key");
int get templateId => getInt("template");
String get testName => getString("test");
bool get result => getBool("result");
String get value => getString("value");
String get attachment => getString("attachment");
String get username => getString("username", subKey: "user_detail");
String get date => getString("date");
@override
InvenTreeStockItemTestResult createFromJson(Map<String, dynamic> json) {
var result = InvenTreeStockItemTestResult.fromJson(json);
return result;
}
}
class InvenTreeStockItemHistory extends InvenTreeModel {
InvenTreeStockItemHistory() : super();
InvenTreeStockItemHistory.fromJson(Map<String, dynamic> json) : super.fromJson(json);
@override
InvenTreeModel createFromJson(Map<String, dynamic> json) => InvenTreeStockItemHistory.fromJson(json);
@override
String get URL => "stock/track/";
@override
Map<String, String> defaultFilters() {
// By default, order by decreasing date
return {
"ordering": "-date",
"user_detail": "true",
};
}
DateTime? get date => getDate("date");
String get dateString => getDateString("date");
String get label => getString("label");
// Return the "deltas" associated with this historical object
Map<String, dynamic> get deltas => getMap("deltas");
// Return the quantity string for this historical object
String get quantityString {
var _deltas = deltas;
if (_deltas.containsKey("quantity")) {
double q = double.tryParse(_deltas["quantity"].toString()) ?? 0;
return simpleNumberString(q);
} else {
return "";
}
}
int? get user => getValue("user") as int?;
String get userString {
if (user != null) {
return getString("username", subKey: "user_detail");
} else {
return "";
}
}
}
/*
* Class representing a StockItem database instance
*/
class InvenTreeStockItem extends InvenTreeModel {
InvenTreeStockItem() : super();
InvenTreeStockItem.fromJson(Map<String, dynamic> json) : super.fromJson(json);
@override
String get URL => "stock/";
static const String MODEL_TYPE = "stockitem";
@override
List<String> get rolesRequired => ["stock"];
// Return a set of fields to transfer this stock item via dialog
Map<String, dynamic> transferFields() {
Map<String, dynamic> fields = {
"pk": {
"parent": "items",
"nested": true,
"hidden": true,
"value": pk,
},
"quantity": {
"parent": "items",
"nested": true,
"value": quantity,
},
"location": {
"value": locationId,
},
"status": {
"parent": "items",
"nested": true,
"value": status,
},
"packaging": {
"parent": "items",
"nested": true,
"value": packaging,
},
"notes": {},
};
if (isSerialized()) {
// Prevent editing of 'quantity' field if the item is serialized
fields["quantity"]?["hidden"] = true;
}
// Old API does not support these fields
if (!api.supportsStockAdjustExtraFields) {
fields.remove("packaging");
fields.remove("status");
}
return fields;
}
// URLs for performing stock actions
static String transferStockUrl() => "stock/transfer/";
static String countStockUrl() => "stock/count/";
static String addStockUrl() => "stock/add/";
static String removeStockUrl() => "stock/remove/";
@override
String get WEB_URL => "stock/item/";
@override
Map<String, Map<String, dynamic>> formFields() {
Map<String, Map<String, dynamic>> fields = {
"part": {},
"location": {},
"quantity": {},
"serial": {},
"serial_numbers": {
"label": L10().serialNumbers,
"type": "string",
},
"status": {},
"batch": {},
"purchase_price": {},
"purchase_price_currency": {},
"packaging": {},
"link": {},
};
return fields;
}
@override
Map<String, String> defaultFilters() {
return {
"part_detail": "true",
"location_detail": "true",
"supplier_detail": "true",
"supplier_part_detail": "true",
"cascade": "false"
};
}
List<InvenTreePartTestTemplate> testTemplates = [];
int get testTemplateCount => testTemplates.length;
// Get all the test templates associated with this StockItem
Future<void> getTestTemplates({bool showDialog=false}) async {
await InvenTreePartTestTemplate().list(
filters: {
"part": "${partId}",
"enabled": "true",
},
).then((var templates) {
testTemplates.clear();
for (var t in templates) {
if (t is InvenTreePartTestTemplate) {
testTemplates.add(t);
}
}
});
}
List<InvenTreeStockItemTestResult> testResults = [];
int get testResultCount => testResults.length;
Future<void> 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);
}
}
});
}
int get status => getInt("status");
bool get isInStock => getBool("in_stock", backup: true);
String get packaging => getString("packaging");
String get batch => getString("batch");
int get partId => getInt("part");
double? get purchasePrice {
String pp = getString("purchase_price");
if (pp.isEmpty) {
return null;
} else {
return double.tryParse(pp);
}
}
String get purchasePriceCurrency => getString("purchase_price_currency");
bool get hasPurchasePrice {
double? pp = purchasePrice;
return pp != null && pp > 0;
}
int get purchaseOrderId => getInt("purchase_order");
int get trackingItemCount => getInt("tracking_items", backup: 0);
bool get isBuilding => getBool("is_building");
int get salesOrderId => getInt("sales_order");
bool get hasSalesOrder => salesOrderId > 0;
int get customerId => getInt("customer");
bool get hasCustomer => customerId > 0;
bool get stale => getBool("stale");
bool get expired => getBool("expired");
DateTime? get expiryDate => getDate("expiry_date");
String get expiryDateString => getDateString("expiry_date");
// Date of last update
DateTime? get updatedDate => getDate("updated");
String get updatedDateString => getDateString("updated");
DateTime? get stocktakeDate => getDate("stocktake_date");
String get stocktakeDateString => getDateString("stocktake_date");
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 = getString("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"] ?? "") as String;
}
if (desc.isEmpty) {
desc = getString("part__description");
}
return desc;
}
String get partImage {
String img = "";
if (jsondata.containsKey("part_detail")) {
img = (jsondata["part_detail"]?["thumbnail"] ?? "") as String;
}
if (img.isEmpty) {
img = getString("part__thumbnail");
}
return img;
}
/*
* Return the Part thumbnail for this stock item.
*/
String get partThumbnail {
String 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 = getString("part__thumbnail");
}
// Still no thumbnail? Use the "no image" image
if (thumb.isEmpty) thumb = InvenTreeAPI.staticThumb;
return thumb;
}
int get supplierPartId => getInt("supplier_part");
String get supplierImage {
String thumb = "";
if (jsondata.containsKey("supplier_part_detail")) {
thumb = (jsondata["supplier_part_detail"]?["supplier_detail"]?["image"] ?? "") as String;
} else if (jsondata.containsKey("supplier_detail")) {
thumb = (jsondata["supplier_detail"]?["image"] ?? "") as String;
}
return thumb;
}
String get supplierName => getString("supplier_name", subKey: "supplier_detail");
String get units => getString("units", subKey: "part_detail");
String get supplierSKU => getString("SKU", subKey: "supplier_part_detail");
String get serialNumber => getString("serial");
double get quantity => getDouble("quantity");
String quantityString({bool includeUnits = true}){
String q = "";
if (allocated > 0) {
q += simpleNumberString(available);
q += " / ";
}
q += simpleNumberString(quantity);
if (includeUnits && units.isNotEmpty) {
q += " ${units}";
}
return q;
}
double get allocated => getDouble("allocated");
double get available => quantity - allocated;
int get locationId => getInt("location");
bool isSerialized() => serialNumber.isNotEmpty && quantity.toInt() == 1;
String serialOrQuantityDisplay() {
if (isSerialized()) {
return "SN ${serialNumber}";
} else if (allocated > 0) {
return "${available} / ${quantity}";
} else {
return simpleNumberString(quantity);
}
}
String get locationName {
if (locationId == -1 || !jsondata.containsKey("location_detail")) return "Unknown Location";
String loc = getString("name", subKey: "location_detail");
// Old-style name
if (loc.isEmpty) {
loc = getString("location__name");
}
return loc;
}
String get locationPathString {
if (locationId == -1 || !jsondata.containsKey("location_detail")) return L10().locationNotSet;
String _loc = getString("pathstring", subKey: "location_detail");
if (_loc.isNotEmpty) {
return _loc;
} else {
return locationName;
}
}
String get displayQuantity {
// Display either quantity or serial number!
if (serialNumber.isNotEmpty) {
return "SN: $serialNumber";
} else {
String q = simpleNumberString(quantity);
if (units.isNotEmpty) {
q += " ${units}";
}
return q;
}
}
@override
InvenTreeModel createFromJson(Map<String, dynamic> json) => InvenTreeStockItem.fromJson(json);
/*
* Perform stocktake action:
*
* - Add
* - Remove
* - Count
*/
Future<bool> adjustStock(String endpoint, double q, {String? notes, int? location}) async {
// Serialized stock cannot be adjusted (unless it is a "transfer")
if (isSerialized() && location == null) {
return false;
}
// Cannot handle negative stock
if (q < 0) {
return false;
}
Map<String, dynamic> data = {};
data = {
"items": [
{
"pk": "${pk}",
"quantity": "${quantity}",
}
],
"notes": notes ?? "",
};
if (location != null) {
data["location"] = location;
}
var response = await api.post(
endpoint,
body: data,
);
return response.isValid() && (response.statusCode == 200 || response.statusCode == 201);
}
Future<bool> countStock(double q, {String? notes}) async {
final bool result = await adjustStock("/stock/count/", q, notes: notes);
return result;
}
Future<bool> addStock(double q, {String? notes}) async {
final bool result = await adjustStock("/stock/add/", q, notes: notes);
return result;
}
Future<bool> removeStock(double q, {String? notes}) async {
final bool result = await adjustStock("/stock/remove/", q, notes: notes);
return result;
}
Future<bool> transferStock(int location, {double? quantity, String? notes}) async {
double q = this.quantity;
if (quantity != null) {
q = quantity;
}
final bool result = await adjustStock(
"/stock/transfer/",
q,
notes: notes,
location: location,
);
return result;
}
}
/*
* Class representing an attachment file against a StockItem object
*/
class InvenTreeStockItemAttachment extends InvenTreeAttachment {
InvenTreeStockItemAttachment() : super();
InvenTreeStockItemAttachment.fromJson(Map<String, dynamic> json) : super.fromJson(json);
@override
String get REFERENCE_FIELD => "stock_item";
@override
String get REF_MODEL_TYPE => "stockitem";
@override
String get URL => InvenTreeAPI().supportsModernAttachments ? "attachment/" : "stock/attachment/";
@override
InvenTreeModel createFromJson(Map<String, dynamic> json) => InvenTreeStockItemAttachment.fromJson(json);
}
class InvenTreeStockLocation extends InvenTreeModel {
InvenTreeStockLocation() : super();
InvenTreeStockLocation.fromJson(Map<String, dynamic> json) : super.fromJson(json);
@override
String get URL => "stock/location/";
static const String MODEL_TYPE = "stocklocation";
@override
List<String> get rolesRequired => ["stock"];
String get pathstring => getString("pathstring");
@override
Map<String, Map<String, dynamic>> formFields() {
Map<String, Map<String, dynamic>> fields = {
"name": {},
"description": {},
"parent": {},
"structural": {},
};
return fields;
}
String get parentPathString {
List<String> psplit = pathstring.split("/");
if (psplit.isNotEmpty) {
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<String, dynamic> json) => InvenTreeStockLocation.fromJson(json);
}