Files
inventree-app/lib/inventree/build.dart
T
Oliver 2cc5d37f71 Fixes for build orders (#862)
* Fixes for build orders

- Display build description
- Improve status display

* Bump version info

* More fixes
2026-08-11 21:41:55 +10:00

369 lines
9.5 KiB
Dart

/*
* Models representing build orders
*/
import "dart:async";
import "package:flutter/material.dart";
import "package:flutter_tabler_icons/flutter_tabler_icons.dart";
import "package:inventree/api.dart";
import "package:inventree/inventree/model.dart";
import "package:inventree/inventree/orders.dart";
import "package:inventree/inventree/part.dart";
import "package:inventree/inventree/stock.dart";
import "package:inventree/widget/build/build_detail.dart";
/*
* Class representing a Build Order
*/
class InvenTreeBuildOrder extends InvenTreeOrder {
InvenTreeBuildOrder() : super();
InvenTreeBuildOrder.fromJson(Map<String, dynamic> json)
: super.fromJson(json);
@override
InvenTreeModel createFromJson(Map<String, dynamic> json) =>
InvenTreeBuildOrder.fromJson(json);
// API endpoint URL
@override
String get URL => "build/";
// Return the "reference field" for the attachment API
String get REFERENCE_FIELD => "build";
// Return the reference field for the modern attachment API
String get REF_MODEL_TYPE => "build";
static const String MODEL_TYPE = "build";
@override
List<String> get rolesRequired => ["build"];
// Return icon for this model
static IconData get icon => TablerIcons.hammer;
@override
String get description => getString("title");
// Part to be built
int get partId => getInt("part");
// Part detail information
InvenTreePart? get partDetail {
dynamic part_detail = jsondata["part_detail"];
if (part_detail == null) {
return null;
} else {
return InvenTreePart.fromJson(part_detail as Map<String, dynamic>);
}
}
// Build quantity
double get quantity => getDouble("quantity");
// Completed quantity
double get completed => getDouble("completed");
// Progress as a percentage
double get progressPercent {
if (quantity <= 0) return 0.0;
return (completed / quantity * 100).clamp(0.0, 100.0);
}
// Remaining quantity to be built
double get remaining => quantity - completed;
// Is the build order a parent build?
bool get isParentBuild => getInt("parent") > 0;
// Parent build ID
int get parentBuildId => getInt("parent");
// External build
bool get external => getBool("external");
// Return the location where the items will be sourced from
int? get sourceLocationId => getInt("take_from");
// Return the location where the completed items will be stored
int? get destinationId => getInt("destination");
String get destinationName => getString("name", subKey: "destination_detail");
// Line item information
@override
int get lineItemCount => getInt("item_count", backup: 0);
// Allocated line item count
int get allocatedLineItemCount => getInt("allocated_line_count", backup: 0);
// All line items are allocated
bool get areAllLinesAllocated =>
lineItemCount > 0 && lineItemCount == allocatedLineItemCount;
// Output count
int get outputCount => getInt("output_count", backup: 0);
// Status code handling
// Note: These map to BuildStatus in backend/status_codes.py
bool get isWaitingForBuild =>
api.BuildOrderStatus.isNameIn(status, ["PENDING"]);
bool get isInProgress =>
api.BuildOrderStatus.isNameIn(status, ["PRODUCTION"]);
bool get isComplete => api.BuildOrderStatus.isNameIn(status, ["COMPLETE"]);
bool get isCancelled => api.BuildOrderStatus.isNameIn(status, ["CANCELLED"]);
bool get isOnHold => api.BuildOrderStatus.isNameIn(status, ["ON_HOLD"]);
// Can this build order be completed?
bool get canCompleteOrder {
return isInProgress && outputCount > 0;
}
// Can this build order be issued?
bool get canIssue {
return isWaitingForBuild || isOnHold;
}
// Can this build order be put on hold?
bool get canHold {
return isWaitingForBuild || isInProgress;
}
// Can this build order be cancelled?
bool get canCancel {
return !isComplete && !isCancelled;
}
// Override form fields
@override
Map<String, Map<String, dynamic>> formFields() {
return {
"reference": {"required": true},
"part": {"required": true},
"title": {},
"quantity": {"required": true},
"priority": {},
"parent": {},
"sales_order": {},
"batch": {},
"start_date": {},
"target_date": {},
"take_from": {},
"destination": {},
"project_code": {},
"link": {},
"external": {},
"responsible": {},
};
}
// Issue a build order
Future<APIResponse> issue() async {
return await InvenTreeAPI().post(
"${URL}${pk}/issue/",
body: {},
expectedStatusCode: 201,
);
}
// Complete a build order
Future<APIResponse> completeOrder({
bool acceptIncomplete = false,
bool acceptUnallocated = false,
bool acceptOverallocated = false,
}) async {
Map<String, String> data = {
"accept_incomplete": acceptIncomplete.toString(),
"accept_unallocated": acceptUnallocated.toString(),
"accept_overallocated": acceptOverallocated.toString(),
};
return await InvenTreeAPI().post(
"${URL}${pk}/complete/",
body: data,
expectedStatusCode: 201,
);
}
// Put a build order on hold
Future<APIResponse> hold() async {
return await InvenTreeAPI().post(
"${URL}${pk}/hold/",
body: {},
expectedStatusCode: 201,
);
}
// Cancel a build order
Future<APIResponse> cancel() async {
return await InvenTreeAPI().post(
"${URL}${pk}/cancel/",
body: {},
expectedStatusCode: 201,
);
}
// Auto-allocate stock items for this build order
Future<APIResponse> autoAllocate() async {
return await InvenTreeAPI().post(
"${URL}${pk}/auto-allocate/",
body: {},
expectedStatusCode: 201,
);
}
// Unallocate all stock from this build order
Future<APIResponse> unallocateAll() async {
return await InvenTreeAPI().post(
"${URL}${pk}/unallocate/",
body: {},
expectedStatusCode: 201,
);
}
@override
Future<Object?> goToDetailPage(BuildContext context) async {
return Navigator.push(
context,
MaterialPageRoute(builder: (context) => BuildOrderDetailWidget(this)),
);
}
}
/*
* Class representing a build line
*/
class InvenTreeBuildLine extends InvenTreeOrderLine {
InvenTreeBuildLine() : super();
InvenTreeBuildLine.fromJson(Map<String, dynamic> json) : super.fromJson(json);
@override
InvenTreeModel createFromJson(Map<String, dynamic> json) =>
InvenTreeBuildLine.fromJson(json);
// API endpoint URL
@override
String get URL => "build/line/";
@override
Map<String, String> defaultFilters() {
return {"part_detail": "true"};
}
@override
List<String> get rolesRequired => ["build"];
// Build order reference
int get buildId => getInt("build");
// BOM item reference
int get bomItemId => getInt("bom_item");
// Required quantity
double get requiredQuantity => getDouble("quantity");
// Allocated quantity
double get allocatedQuantity => getDouble("allocated");
// Reference the BOM item detail
String get bomReference => getString("reference", subKey: "bom_item_detail");
// Is this line fully allocated?
bool get isFullyAllocated {
// Allow for floating point comparison
return allocatedQuantity >= (requiredQuantity - 0.0001);
}
// Is this line overallocated?
bool get isOverallocated => allocatedQuantity > requiredQuantity;
// Allocation progress as percentage
double get progressPercent {
if (requiredQuantity <= 0) return 0.0;
return (allocatedQuantity / requiredQuantity * 100).clamp(0.0, 100.0);
}
}
/*
* Class representing a build item (stock allocation)
*/
class InvenTreeBuildItem extends InvenTreeModel {
InvenTreeBuildItem() : super();
InvenTreeBuildItem.fromJson(Map<String, dynamic> json) : super.fromJson(json);
@override
InvenTreeModel createFromJson(Map<String, dynamic> json) =>
InvenTreeBuildItem.fromJson(json);
// API endpoint URL
@override
String get URL => "build/item/";
@override
Map<String, String> defaultFilters() {
return {
"part_detail": "true",
"stock_detail": "true",
"location_detail": "true",
};
}
@override
List<String> get rolesRequired => ["build"];
// Build line reference
int get buildLineId => getInt("build_line");
// Stock item being allocated
int get stockItemId => getInt("stock_item");
// Quantity being allocated
double get quantity => getDouble("quantity");
// Stock item to install into
int get installIntoId => getInt("install_into");
// Stock item detail
InvenTreeStockItem? get stockItem {
dynamic stock_item = jsondata["stock_item_detail"];
if (stock_item == null) {
return null;
} else {
return InvenTreeStockItem.fromJson(stock_item as Map<String, dynamic>);
}
}
// Part details
String get partName => getString("name", subKey: "part_detail");
String get partDescription => getString("description", subKey: "part_detail");
String get partThumbnail => getString("thumbnail", subKey: "part_detail");
// Allocation details
String get locationName => getString("name", subKey: "location_detail");
String get locationPath => getString("pathstring", subKey: "location_detail");
String get serialNumber => getString("serial", subKey: "stock_item_detail");
String get batchCode => getString("batch", subKey: "stock_item_detail");
@override
Map<String, Map<String, dynamic>> formFields() {
return {
"build_line": {"required": true, "hidden": true},
"stock_item": {"required": true},
"quantity": {"required": true},
"install_into": {"hidden": true},
};
}
}