mirror of
https://github.com/inventree/inventree-app.git
synced 2025-10-29 12:37:37 +00:00
SalesOrderShipment (#697)
* Add detail widget for SalesOrderShipment * Support editing of shipment details * Rearrange SalesOrderDetail page * Add support for attachments against shipment model * refactoring * Add shipment details page * Add user actions for shipments: - Check / uncheck - Take photo * Placeholder action to send shipment * Send shipment from app * Display pending shipments on the home screen * Improve rending for shipments list * Add class definition for SalesOrderAllocation * Display list of items allocated against SalesOrderShipment * Bump release notse * Click through to stock item * Bump version number * dart format * cleanup * Remove unused imports
This commit is contained in:
@@ -1,3 +1,11 @@
|
|||||||
|
### 0.20.0 - October 2025
|
||||||
|
---
|
||||||
|
|
||||||
|
- View pending shipments from the home screen
|
||||||
|
- Display detail view for shipments
|
||||||
|
- Adds ability to ship pending outgoing shipments
|
||||||
|
- Adds ability to mark outgoing shipments as "checked" or "unchecked"
|
||||||
|
|
||||||
### 0.19.3 - September 2025
|
### 0.19.3 - September 2025
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -279,6 +279,8 @@ class InvenTreeAPI {
|
|||||||
|
|
||||||
String get username => (userInfo["username"] ?? "") as String;
|
String get username => (userInfo["username"] ?? "") as String;
|
||||||
|
|
||||||
|
int get userId => (userInfo["pk"] ?? -1) as int;
|
||||||
|
|
||||||
// Map of server information
|
// Map of server information
|
||||||
Map<String, dynamic> serverInfo = {};
|
Map<String, dynamic> serverInfo = {};
|
||||||
|
|
||||||
|
|||||||
@@ -586,6 +586,8 @@ class APIFormField {
|
|||||||
return InvenTreeSupplierPart().defaultListFilters();
|
return InvenTreeSupplierPart().defaultListFilters();
|
||||||
case InvenTreeStockItem.MODEL_TYPE:
|
case InvenTreeStockItem.MODEL_TYPE:
|
||||||
return InvenTreeStockItem().defaultListFilters();
|
return InvenTreeStockItem().defaultListFilters();
|
||||||
|
case InvenTreeSalesOrder.MODEL_TYPE:
|
||||||
|
return InvenTreeSalesOrder().defaultListFilters();
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -727,7 +729,7 @@ class APIFormField {
|
|||||||
return ListTile(
|
return ListTile(
|
||||||
title: Text(shipment.reference),
|
title: Text(shipment.reference),
|
||||||
subtitle: Text(shipment.tracking_number),
|
subtitle: Text(shipment.tracking_number),
|
||||||
trailing: shipment.shipped ? Text(shipment.shipment_date!) : null,
|
trailing: shipment.isShipped ? Text(shipment.shipment_date!) : null,
|
||||||
);
|
);
|
||||||
case "owner":
|
case "owner":
|
||||||
String name = (data["name"] ?? "") as String;
|
String name = (data["name"] ?? "") as String;
|
||||||
@@ -754,6 +756,15 @@ class APIFormField {
|
|||||||
subtitle: Text(project_code.description),
|
subtitle: Text(project_code.description),
|
||||||
leading: Icon(TablerIcons.list),
|
leading: Icon(TablerIcons.list),
|
||||||
);
|
);
|
||||||
|
case InvenTreeSalesOrder.MODEL_TYPE:
|
||||||
|
var so = InvenTreeSalesOrder.fromJson(data);
|
||||||
|
return ListTile(
|
||||||
|
title: Text(so.reference),
|
||||||
|
subtitle: Text(so.description),
|
||||||
|
leading: InvenTreeAPI().getThumbnail(
|
||||||
|
so.customer?.thumbnail ?? so.customer?.image ?? "",
|
||||||
|
),
|
||||||
|
);
|
||||||
default:
|
default:
|
||||||
return ListTile(
|
return ListTile(
|
||||||
title: Text(
|
title: Text(
|
||||||
|
|||||||
@@ -601,7 +601,7 @@ class InvenTreeModel {
|
|||||||
|
|
||||||
// POST data to update the model
|
// POST data to update the model
|
||||||
Future<APIResponse> update({
|
Future<APIResponse> update({
|
||||||
Map<String, String> values = const {},
|
Map<String, dynamic> values = const {},
|
||||||
int? expectedStatusCode = 200,
|
int? expectedStatusCode = 200,
|
||||||
}) async {
|
}) async {
|
||||||
var url = path.join(URL, pk.toString());
|
var url = path.join(URL, pk.toString());
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ import "package:inventree/helpers.dart";
|
|||||||
import "package:inventree/inventree/company.dart";
|
import "package:inventree/inventree/company.dart";
|
||||||
import "package:inventree/inventree/model.dart";
|
import "package:inventree/inventree/model.dart";
|
||||||
import "package:inventree/inventree/orders.dart";
|
import "package:inventree/inventree/orders.dart";
|
||||||
|
import "package:inventree/inventree/part.dart";
|
||||||
|
import "package:inventree/inventree/stock.dart";
|
||||||
|
import "package:inventree/widget/order/so_shipment_detail.dart";
|
||||||
import "package:inventree/widget/progress.dart";
|
import "package:inventree/widget/progress.dart";
|
||||||
import "package:inventree/widget/order/extra_line_detail.dart";
|
import "package:inventree/widget/order/extra_line_detail.dart";
|
||||||
import "package:inventree/widget/order/sales_order_detail.dart";
|
import "package:inventree/widget/order/sales_order_detail.dart";
|
||||||
@@ -269,6 +272,19 @@ class InvenTreeSalesOrderShipment extends InvenTreeModel {
|
|||||||
@override
|
@override
|
||||||
String get URL => "/order/so/shipment/";
|
String get URL => "/order/so/shipment/";
|
||||||
|
|
||||||
|
String get SHIP_SHIPMENT_URL => "/order/so/shipment/${pk}/ship/";
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Object?> goToDetailPage(BuildContext context) async {
|
||||||
|
return Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(builder: (context) => SOShipmentDetailWidget(this)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<String> get rolesRequired => ["sales_order"];
|
||||||
|
|
||||||
static const String MODEL_TYPE = "salesordershipment";
|
static const String MODEL_TYPE = "salesordershipment";
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -284,6 +300,18 @@ class InvenTreeSalesOrderShipment extends InvenTreeModel {
|
|||||||
return fields;
|
return fields;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int get orderId => getInt("order");
|
||||||
|
|
||||||
|
InvenTreeSalesOrder? get order {
|
||||||
|
dynamic order_detail = jsondata["order_detail"];
|
||||||
|
|
||||||
|
if (order_detail == null) {
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
return InvenTreeSalesOrder.fromJson(order_detail as Map<String, dynamic>);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
String get reference => getString("reference");
|
String get reference => getString("reference");
|
||||||
|
|
||||||
String get tracking_number => getString("tracking_number");
|
String get tracking_number => getString("tracking_number");
|
||||||
@@ -292,7 +320,113 @@ class InvenTreeSalesOrderShipment extends InvenTreeModel {
|
|||||||
|
|
||||||
String? get shipment_date => getString("shipment_date");
|
String? get shipment_date => getString("shipment_date");
|
||||||
|
|
||||||
bool get shipped => shipment_date != null && shipment_date!.isNotEmpty;
|
String? get delivery_date => getString("delivery_date");
|
||||||
|
|
||||||
|
int? get checked_by_id => getInt("checked_by");
|
||||||
|
|
||||||
|
bool get isChecked => checked_by_id != null && checked_by_id! > 0;
|
||||||
|
|
||||||
|
bool get isShipped => shipment_date != null && shipment_date!.isNotEmpty;
|
||||||
|
|
||||||
|
bool get isDelivered => delivery_date != null && delivery_date!.isNotEmpty;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Class representing an allocation of stock against a SalesOrderShipment
|
||||||
|
*/
|
||||||
|
class InvenTreeSalesOrderAllocation extends InvenTreeAttachment {
|
||||||
|
InvenTreeSalesOrderAllocation() : super();
|
||||||
|
|
||||||
|
InvenTreeSalesOrderAllocation.fromJson(Map<String, dynamic> json)
|
||||||
|
: super.fromJson(json);
|
||||||
|
|
||||||
|
@override
|
||||||
|
InvenTreeModel createFromJson(Map<String, dynamic> json) =>
|
||||||
|
InvenTreeSalesOrderAllocation.fromJson(json);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get URL => "/order/so-allocation/";
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<String> get rolesRequired => ["sales_order"];
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, String> defaultFilters() {
|
||||||
|
return {
|
||||||
|
"part_detail": "true",
|
||||||
|
"order_detail": "true",
|
||||||
|
"item_detail": "true",
|
||||||
|
"location_detail": "true",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static const String MODEL_TYPE = "salesorderallocation";
|
||||||
|
|
||||||
|
int get orderId => getInt("order");
|
||||||
|
|
||||||
|
InvenTreeSalesOrder? get order {
|
||||||
|
dynamic order_detail = jsondata["order_detail"];
|
||||||
|
|
||||||
|
if (order_detail == null) {
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
return InvenTreeSalesOrder.fromJson(order_detail as Map<String, dynamic>);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int get stockItemId => getInt("item");
|
||||||
|
|
||||||
|
InvenTreeStockItem? get stockItem {
|
||||||
|
dynamic item_detail = jsondata["item_detail"];
|
||||||
|
|
||||||
|
if (item_detail == null) {
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
return InvenTreeStockItem.fromJson(item_detail as Map<String, dynamic>);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int get partId => getInt("part");
|
||||||
|
|
||||||
|
InvenTreePart? get part {
|
||||||
|
dynamic part_detail = jsondata["part_detail"];
|
||||||
|
|
||||||
|
if (part_detail == null) {
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
return InvenTreePart.fromJson(part_detail as Map<String, dynamic>);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int get shipmentId => getInt("shipment");
|
||||||
|
|
||||||
|
bool get hasShipment => shipmentId > 0;
|
||||||
|
|
||||||
|
InvenTreeSalesOrderShipment? get shipment {
|
||||||
|
dynamic shipment_detail = jsondata["shipment_detail"];
|
||||||
|
|
||||||
|
if (shipment_detail == null) {
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
return InvenTreeSalesOrderShipment.fromJson(
|
||||||
|
shipment_detail as Map<String, dynamic>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int get locationId => getInt("location");
|
||||||
|
|
||||||
|
InvenTreeStockLocation? get location {
|
||||||
|
dynamic location_detail = jsondata["location_detail"];
|
||||||
|
|
||||||
|
if (location_detail == null) {
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
return InvenTreeStockLocation.fromJson(
|
||||||
|
location_detail as Map<String, dynamic>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -319,3 +453,23 @@ class InvenTreeSalesOrderAttachment extends InvenTreeAttachment {
|
|||||||
? "attachment/"
|
? "attachment/"
|
||||||
: "order/so/attachment/";
|
: "order/so/attachment/";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class InvenTreeSalesOrderShipmentAttachment extends InvenTreeAttachment {
|
||||||
|
InvenTreeSalesOrderShipmentAttachment() : super();
|
||||||
|
|
||||||
|
InvenTreeSalesOrderShipmentAttachment.fromJson(Map<String, dynamic> json)
|
||||||
|
: super.fromJson(json);
|
||||||
|
|
||||||
|
@override
|
||||||
|
InvenTreeModel createFromJson(Map<String, dynamic> json) =>
|
||||||
|
InvenTreeSalesOrderShipmentAttachment.fromJson(json);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get REFERENCE_FIELD => "shipment";
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get REF_MODEL_TYPE => "salesordershipment";
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get URL => "attachment/";
|
||||||
|
}
|
||||||
|
|||||||
@@ -65,6 +65,9 @@
|
|||||||
"allocateStock": "Allocate Stock",
|
"allocateStock": "Allocate Stock",
|
||||||
"@allocateStock": {},
|
"@allocateStock": {},
|
||||||
|
|
||||||
|
"allocatedStock": "Allocated Stock",
|
||||||
|
"@allocatedStock": {},
|
||||||
|
|
||||||
"appReleaseNotes": "Display app release notes",
|
"appReleaseNotes": "Display app release notes",
|
||||||
"@appReleaseNotes": {},
|
"@appReleaseNotes": {},
|
||||||
|
|
||||||
@@ -331,6 +334,15 @@
|
|||||||
"deleteFailed": "Delete operation failed",
|
"deleteFailed": "Delete operation failed",
|
||||||
"@deleteFailed": {},
|
"@deleteFailed": {},
|
||||||
|
|
||||||
|
"deleteImageConfirmation": "Are you sure you want to delete this image?",
|
||||||
|
"@deleteImageConfirmation": {},
|
||||||
|
|
||||||
|
"deleteImageTooltip": "Delete Image",
|
||||||
|
"@deleteImageTooltip": {},
|
||||||
|
|
||||||
|
"deleteImage": "Delete Image",
|
||||||
|
"@deleteImage": {},
|
||||||
|
|
||||||
"deletePart": "Delete Part",
|
"deletePart": "Delete Part",
|
||||||
"@deletePart": {},
|
"@deletePart": {},
|
||||||
|
|
||||||
@@ -340,6 +352,9 @@
|
|||||||
"deleteSuccess": "Delete operation successful",
|
"deleteSuccess": "Delete operation successful",
|
||||||
"@deleteSuccess": {},
|
"@deleteSuccess": {},
|
||||||
|
|
||||||
|
"deliveryDate": "Delivery Date",
|
||||||
|
"@deliveryDate": {},
|
||||||
|
|
||||||
"description": "Description",
|
"description": "Description",
|
||||||
"@description": {},
|
"@description": {},
|
||||||
|
|
||||||
@@ -548,6 +563,12 @@
|
|||||||
"homeShowPoDescription": "Show purchase order button on home screen",
|
"homeShowPoDescription": "Show purchase order button on home screen",
|
||||||
"@homeShowPoDescription": {},
|
"@homeShowPoDescription": {},
|
||||||
|
|
||||||
|
"homeShowShipments": "Show Shipments",
|
||||||
|
"@homeShowShipments": {},
|
||||||
|
|
||||||
|
"homeShowShipmentsDescription": "Show pending shipments on the home screen",
|
||||||
|
"@homeShowShipmentsDescription": {},
|
||||||
|
|
||||||
"homeShowSo": "Show Sales Orders",
|
"homeShowSo": "Show Sales Orders",
|
||||||
"@homeShowSo": {},
|
"@homeShowSo": {},
|
||||||
|
|
||||||
@@ -647,6 +668,12 @@
|
|||||||
"invalidUsernamePassword": "Invalid username / password combination",
|
"invalidUsernamePassword": "Invalid username / password combination",
|
||||||
"@invalidUsernamePassword": {},
|
"@invalidUsernamePassword": {},
|
||||||
|
|
||||||
|
"invoice": "Invoice",
|
||||||
|
"@invoice": {},
|
||||||
|
|
||||||
|
"invoiceNumber": "Invoice Number",
|
||||||
|
"@invoiceNumber": {},
|
||||||
|
|
||||||
"issue": "Issue",
|
"issue": "Issue",
|
||||||
"@issue": {},
|
"@issue": {},
|
||||||
|
|
||||||
@@ -769,6 +796,12 @@
|
|||||||
"@name": {
|
"@name": {
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"no": "No",
|
||||||
|
"@no": {},
|
||||||
|
|
||||||
|
"notApplicable": "N/A",
|
||||||
|
"@notApplicable": {},
|
||||||
|
|
||||||
"notConnected": "Not Connected",
|
"notConnected": "Not Connected",
|
||||||
"@notConnected": {},
|
"@notConnected": {},
|
||||||
|
|
||||||
@@ -780,8 +813,8 @@
|
|||||||
"notifications": "Notifications",
|
"notifications": "Notifications",
|
||||||
"@notifications": {},
|
"@notifications": {},
|
||||||
|
|
||||||
"notificationsEmpty": "No unread notifications",
|
"notificationsEmpty": "No unread notifications",
|
||||||
"@notificationsEmpty": {},
|
"@notificationsEmpty": {},
|
||||||
|
|
||||||
"noResponse": "No Response from Server",
|
"noResponse": "No Response from Server",
|
||||||
"@noResponse": {},
|
"@noResponse": {},
|
||||||
@@ -792,6 +825,12 @@
|
|||||||
"noImageAvailable": "No image available",
|
"noImageAvailable": "No image available",
|
||||||
"@noImageAvailable": {},
|
"@noImageAvailable": {},
|
||||||
|
|
||||||
|
"noPricingAvailable": "No pricing available",
|
||||||
|
"@noPricingAvailable": {},
|
||||||
|
|
||||||
|
"noPricingDataFound": "No pricing data found for this part",
|
||||||
|
"@noPricingDataFound": {},
|
||||||
|
|
||||||
"noSubcategories": "No Subcategories",
|
"noSubcategories": "No Subcategories",
|
||||||
"@noSubcategories": {},
|
"@noSubcategories": {},
|
||||||
|
|
||||||
@@ -927,6 +966,9 @@
|
|||||||
"passwordEmpty": "Password cannot be empty",
|
"passwordEmpty": "Password cannot be empty",
|
||||||
"@passwordEmpty": {},
|
"@passwordEmpty": {},
|
||||||
|
|
||||||
|
"pending": "Pending",
|
||||||
|
"@pending": {},
|
||||||
|
|
||||||
"permissionAccountDenied": "Your account does not have the required permissions to perform this action",
|
"permissionAccountDenied": "Your account does not have the required permissions to perform this action",
|
||||||
"@permissionAccountDenied": {},
|
"@permissionAccountDenied": {},
|
||||||
|
|
||||||
@@ -1186,20 +1228,20 @@
|
|||||||
"salesOrders": "Sales Orders",
|
"salesOrders": "Sales Orders",
|
||||||
"@salesOrders": {},
|
"@salesOrders": {},
|
||||||
|
|
||||||
"salesOrderEnable": "Enable Sales Orders",
|
"salesOrderEnable": "Enable Sales Orders",
|
||||||
"@salesOrderEnable": {},
|
"@salesOrderEnable": {},
|
||||||
|
|
||||||
"salesOrderEnableDetail": "Enable sales order functionality",
|
"salesOrderEnableDetail": "Enable sales order functionality",
|
||||||
"@salesOrderEnableDetail": {},
|
"@salesOrderEnableDetail": {},
|
||||||
|
|
||||||
"salesOrderShowCamera": "Camera Shortcut",
|
"salesOrderShowCamera": "Camera Shortcut",
|
||||||
"@salesOrderShowCamera": {},
|
"@salesOrderShowCamera": {},
|
||||||
|
|
||||||
"salesOrderShowCameraDetail": "Enable image upload shortcut on sales order screen",
|
"salesOrderShowCameraDetail": "Enable image upload shortcut on sales order screen",
|
||||||
"@salesOrderShowCameraDetail": {},
|
"@salesOrderShowCameraDetail": {},
|
||||||
|
|
||||||
"salesOrderSettings": "Sales order settings",
|
"salesOrderSettings": "Sales order settings",
|
||||||
"@salesOrderSettings": {},
|
"@salesOrderSettings": {},
|
||||||
|
|
||||||
"salesOrderCreate": "New Sales Order",
|
"salesOrderCreate": "New Sales Order",
|
||||||
"@saleOrderCreate": {},
|
"@saleOrderCreate": {},
|
||||||
@@ -1338,12 +1380,48 @@
|
|||||||
"serverNotSelected": "Server not selected",
|
"serverNotSelected": "Server not selected",
|
||||||
"@serverNotSelected": {},
|
"@serverNotSelected": {},
|
||||||
|
|
||||||
|
"shipment": "Shipment",
|
||||||
|
"@shipment": {},
|
||||||
|
|
||||||
"shipments": "Shipments",
|
"shipments": "Shipments",
|
||||||
"@shipments": {},
|
"@shipments": {},
|
||||||
|
|
||||||
|
"shipmentsPending": "Pending Shipments",
|
||||||
|
"@shipmentsPending": {},
|
||||||
|
|
||||||
"shipmentAdd": "Add Shipment",
|
"shipmentAdd": "Add Shipment",
|
||||||
"@shipmentAdd": {},
|
"@shipmentAdd": {},
|
||||||
|
|
||||||
|
"shipmentCheck": "Check Shipment",
|
||||||
|
"@shipmentCheck": {},
|
||||||
|
|
||||||
|
"shipmentCheckDetail": "Mark this shipment as checked",
|
||||||
|
"@shipmentCheckDetail": {},
|
||||||
|
|
||||||
|
"shipmentChecked": "Shipment Checked",
|
||||||
|
"@shipmentChecked": {},
|
||||||
|
|
||||||
|
"shipmentDate": "Shipment Date",
|
||||||
|
"@shipmentDate": {},
|
||||||
|
|
||||||
|
"shipmentEdit": "Edit Shipment",
|
||||||
|
"@shipmentEdit": {},
|
||||||
|
|
||||||
|
"shipmentReference": "Shipment Reference",
|
||||||
|
"@shipmentReference": {},
|
||||||
|
|
||||||
|
"shipmentSend": "Send Shipment",
|
||||||
|
"@shipmentSend": {},
|
||||||
|
|
||||||
|
"shipmentUncheck": "Uncheck Shipment",
|
||||||
|
"@shipmentUncheck": {},
|
||||||
|
|
||||||
|
"shipmentUncheckDetail": "Mark this shipment as unchecked",
|
||||||
|
"@shipmentUncheckDetail": {},
|
||||||
|
|
||||||
|
"shipmentUpdated": "Shipment Updated",
|
||||||
|
"@shipmentUpdated": {},
|
||||||
|
|
||||||
"shipped": "Shipped",
|
"shipped": "Shipped",
|
||||||
"@shipped": {},
|
"@shipped": {},
|
||||||
|
|
||||||
@@ -1555,6 +1633,9 @@
|
|||||||
"totalPrice": "Total Price",
|
"totalPrice": "Total Price",
|
||||||
"@totalPrice": {},
|
"@totalPrice": {},
|
||||||
|
|
||||||
|
"trackingNumber": "Tracking Number",
|
||||||
|
"@trackingNumber": {},
|
||||||
|
|
||||||
"transfer": "Transfer",
|
"transfer": "Transfer",
|
||||||
"@transfer": {
|
"@transfer": {
|
||||||
"description": "transfer"
|
"description": "transfer"
|
||||||
@@ -1642,6 +1723,9 @@
|
|||||||
"website": "Website",
|
"website": "Website",
|
||||||
"@website": {},
|
"@website": {},
|
||||||
|
|
||||||
|
"yes": "Yes",
|
||||||
|
"@yes": {},
|
||||||
|
|
||||||
"price": "Price",
|
"price": "Price",
|
||||||
"@price": {},
|
"@price": {},
|
||||||
|
|
||||||
@@ -1682,20 +1766,5 @@
|
|||||||
"@currency": {},
|
"@currency": {},
|
||||||
|
|
||||||
"priceBreaks": "Price Breaks",
|
"priceBreaks": "Price Breaks",
|
||||||
"@priceBreaks": {},
|
"@priceBreaks": {}
|
||||||
|
|
||||||
"noPricingAvailable": "No pricing available",
|
|
||||||
"@noPricingAvailable": {},
|
|
||||||
|
|
||||||
"noPricingDataFound": "No pricing data found for this part",
|
|
||||||
"@noPricingDataFound": {},
|
|
||||||
|
|
||||||
"deleteImageConfirmation": "Are you sure you want to delete this image?",
|
|
||||||
"@deleteImageConfirmation": {},
|
|
||||||
|
|
||||||
"deleteImageTooltip": "Delete Image",
|
|
||||||
"@deleteImageTooltip": {},
|
|
||||||
|
|
||||||
"deleteImage": "Delete Image",
|
|
||||||
"@deleteImage": {}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import "package:path/path.dart";
|
|||||||
const String INV_HOME_SHOW_SUBSCRIBED = "homeShowSubscribed";
|
const String INV_HOME_SHOW_SUBSCRIBED = "homeShowSubscribed";
|
||||||
const String INV_HOME_SHOW_PO = "homeShowPo";
|
const String INV_HOME_SHOW_PO = "homeShowPo";
|
||||||
const String INV_HOME_SHOW_SO = "homeShowSo";
|
const String INV_HOME_SHOW_SO = "homeShowSo";
|
||||||
|
const String INV_HOME_SHOW_SHIPMENTS = "homeShowShipments";
|
||||||
const String INV_HOME_SHOW_MANUFACTURERS = "homeShowManufacturers";
|
const String INV_HOME_SHOW_MANUFACTURERS = "homeShowManufacturers";
|
||||||
const String INV_HOME_SHOW_CUSTOMERS = "homeShowCustomers";
|
const String INV_HOME_SHOW_CUSTOMERS = "homeShowCustomers";
|
||||||
const String INV_HOME_SHOW_SUPPLIERS = "homeShowSuppliers";
|
const String INV_HOME_SHOW_SUPPLIERS = "homeShowSuppliers";
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ class _HomeScreenSettingsState extends State<HomeScreenSettingsWidget> {
|
|||||||
bool homeShowSubscribed = true;
|
bool homeShowSubscribed = true;
|
||||||
bool homeShowPo = true;
|
bool homeShowPo = true;
|
||||||
bool homeShowSo = true;
|
bool homeShowSo = true;
|
||||||
|
bool homeShowShipments = true;
|
||||||
bool homeShowSuppliers = true;
|
bool homeShowSuppliers = true;
|
||||||
bool homeShowManufacturers = true;
|
bool homeShowManufacturers = true;
|
||||||
bool homeShowCustomers = true;
|
bool homeShowCustomers = true;
|
||||||
@@ -46,6 +47,11 @@ class _HomeScreenSettingsState extends State<HomeScreenSettingsWidget> {
|
|||||||
homeShowSo =
|
homeShowSo =
|
||||||
await InvenTreeSettingsManager().getValue(INV_HOME_SHOW_SO, true)
|
await InvenTreeSettingsManager().getValue(INV_HOME_SHOW_SO, true)
|
||||||
as bool;
|
as bool;
|
||||||
|
|
||||||
|
homeShowShipments =
|
||||||
|
await InvenTreeSettingsManager().getValue(INV_HOME_SHOW_SHIPMENTS, true)
|
||||||
|
as bool;
|
||||||
|
|
||||||
homeShowManufacturers =
|
homeShowManufacturers =
|
||||||
await InvenTreeSettingsManager().getValue(
|
await InvenTreeSettingsManager().getValue(
|
||||||
INV_HOME_SHOW_MANUFACTURERS,
|
INV_HOME_SHOW_MANUFACTURERS,
|
||||||
@@ -118,6 +124,23 @@ class _HomeScreenSettingsState extends State<HomeScreenSettingsWidget> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
ListTile(
|
||||||
|
title: Text(L10().homeShowShipments),
|
||||||
|
subtitle: Text(L10().homeShowShipmentsDescription),
|
||||||
|
leading: Icon(TablerIcons.cube_send),
|
||||||
|
trailing: Switch(
|
||||||
|
value: homeShowShipments,
|
||||||
|
onChanged: (bool value) {
|
||||||
|
InvenTreeSettingsManager().setValue(
|
||||||
|
INV_HOME_SHOW_SHIPMENTS,
|
||||||
|
value,
|
||||||
|
);
|
||||||
|
setState(() {
|
||||||
|
homeShowShipments = value;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(L10().homeShowSuppliers),
|
title: Text(L10().homeShowSuppliers),
|
||||||
subtitle: Text(L10().homeShowSuppliersDescription),
|
subtitle: Text(L10().homeShowSuppliersDescription),
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import "dart:io";
|
|||||||
import "package:flutter/material.dart";
|
import "package:flutter/material.dart";
|
||||||
import "package:flutter_tabler_icons/flutter_tabler_icons.dart";
|
import "package:flutter_tabler_icons/flutter_tabler_icons.dart";
|
||||||
import "package:one_context/one_context.dart";
|
import "package:one_context/one_context.dart";
|
||||||
import "package:url_launcher/url_launcher.dart";
|
|
||||||
|
|
||||||
import "package:inventree/api.dart";
|
import "package:inventree/api.dart";
|
||||||
import "package:inventree/l10.dart";
|
import "package:inventree/l10.dart";
|
||||||
@@ -212,17 +211,14 @@ class _AttachmentWidgetState extends RefreshableState<AttachmentWidget> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else if (attachment.link.isNotEmpty) {
|
} else if (attachment.hasLink) {
|
||||||
tiles.add(
|
tiles.add(
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(attachment.link),
|
title: Text(attachment.link),
|
||||||
subtitle: Text(attachment.comment),
|
subtitle: Text(attachment.comment),
|
||||||
leading: Icon(TablerIcons.link, color: COLOR_ACTION),
|
leading: Icon(TablerIcons.link, color: COLOR_ACTION),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
var uri = Uri.tryParse(attachment.link.trimLeft());
|
attachment.openLink();
|
||||||
if (uri != null && await canLaunchUrl(uri)) {
|
|
||||||
await launchUrl(uri);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
onLongPress: () {
|
onLongPress: () {
|
||||||
showOptionsMenu(context, attachment);
|
showOptionsMenu(context, attachment);
|
||||||
|
|||||||
@@ -287,7 +287,7 @@ class _CompanyDetailState extends RefreshableState<CompanyDetailWidget> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// External link
|
// External link
|
||||||
if (widget.company.link.isNotEmpty) {
|
if (widget.company.hasLink) {
|
||||||
tiles.add(
|
tiles.add(
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(L10().link),
|
title: Text(L10().link),
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import "package:inventree/inventree/part.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/progress.dart";
|
import "package:inventree/widget/progress.dart";
|
||||||
import "package:url_launcher/url_launcher.dart";
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Detail widget for viewing a single ManufacturerPart instance
|
* Detail widget for viewing a single ManufacturerPart instance
|
||||||
@@ -163,16 +162,13 @@ class _ManufacturerPartDisplayState
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (widget.manufacturerPart.link.isNotEmpty) {
|
if (widget.manufacturerPart.hasLink) {
|
||||||
tiles.add(
|
tiles.add(
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(widget.manufacturerPart.link),
|
title: Text(widget.manufacturerPart.link),
|
||||||
leading: Icon(TablerIcons.link, color: COLOR_ACTION),
|
leading: Icon(TablerIcons.link, color: COLOR_ACTION),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
var uri = Uri.tryParse(widget.manufacturerPart.link);
|
widget.manufacturerPart.openLink();
|
||||||
if (uri != null && await canLaunchUrl(uri)) {
|
|
||||||
await launchUrl(uri);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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/widget/link_icon.dart";
|
import "package:inventree/widget/link_icon.dart";
|
||||||
import "package:url_launcher/url_launcher.dart";
|
|
||||||
|
|
||||||
import "package:inventree/app_colors.dart";
|
import "package:inventree/app_colors.dart";
|
||||||
import "package:inventree/l10.dart";
|
import "package:inventree/l10.dart";
|
||||||
@@ -239,7 +238,7 @@ class _SupplierPartDisplayState
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (widget.supplierPart.link.isNotEmpty) {
|
if (widget.supplierPart.hasLink) {
|
||||||
tiles.add(
|
tiles.add(
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(L10().link),
|
title: Text(L10().link),
|
||||||
@@ -247,10 +246,7 @@ class _SupplierPartDisplayState
|
|||||||
leading: Icon(TablerIcons.link, color: COLOR_ACTION),
|
leading: Icon(TablerIcons.link, color: COLOR_ACTION),
|
||||||
trailing: LinkIcon(external: true),
|
trailing: LinkIcon(external: true),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
var uri = Uri.tryParse(widget.supplierPart.link);
|
widget.supplierPart.openLink();
|
||||||
if (uri != null && await canLaunchUrl(uri)) {
|
|
||||||
await launchUrl(uri);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import "package:inventree/preferences.dart";
|
|||||||
import "package:inventree/l10.dart";
|
import "package:inventree/l10.dart";
|
||||||
import "package:inventree/settings/select_server.dart";
|
import "package:inventree/settings/select_server.dart";
|
||||||
import "package:inventree/user_profile.dart";
|
import "package:inventree/user_profile.dart";
|
||||||
|
import "package:inventree/widget/order/so_shipment_list.dart";
|
||||||
|
|
||||||
import "package:inventree/widget/part/category_display.dart";
|
import "package:inventree/widget/part/category_display.dart";
|
||||||
import "package:inventree/widget/drawer.dart";
|
import "package:inventree/widget/drawer.dart";
|
||||||
@@ -55,6 +56,7 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage>
|
|||||||
|
|
||||||
bool homeShowPo = false;
|
bool homeShowPo = false;
|
||||||
bool homeShowSo = false;
|
bool homeShowSo = false;
|
||||||
|
bool homeShowShipments = false;
|
||||||
bool homeShowSubscribed = false;
|
bool homeShowSubscribed = false;
|
||||||
bool homeShowManufacturers = false;
|
bool homeShowManufacturers = false;
|
||||||
bool homeShowCustomers = false;
|
bool homeShowCustomers = false;
|
||||||
@@ -112,6 +114,20 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _showPendingShipments(BuildContext context) {
|
||||||
|
if (!InvenTreeAPI().checkConnection()) return;
|
||||||
|
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => SOShipmentListWidget(
|
||||||
|
title: L10().shipmentsPending,
|
||||||
|
filters: {"order_outstanding": "true", "shipped": "false"},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
void _showSuppliers(BuildContext context) {
|
void _showSuppliers(BuildContext context) {
|
||||||
if (!InvenTreeAPI().checkConnection()) return;
|
if (!InvenTreeAPI().checkConnection()) return;
|
||||||
|
|
||||||
@@ -167,6 +183,11 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage>
|
|||||||
homeShowSo =
|
homeShowSo =
|
||||||
await InvenTreeSettingsManager().getValue(INV_HOME_SHOW_SO, true)
|
await InvenTreeSettingsManager().getValue(INV_HOME_SHOW_SO, true)
|
||||||
as bool;
|
as bool;
|
||||||
|
|
||||||
|
homeShowShipments =
|
||||||
|
await InvenTreeSettingsManager().getValue(INV_HOME_SHOW_SHIPMENTS, true)
|
||||||
|
as bool;
|
||||||
|
|
||||||
homeShowManufacturers =
|
homeShowManufacturers =
|
||||||
await InvenTreeSettingsManager().getValue(
|
await InvenTreeSettingsManager().getValue(
|
||||||
INV_HOME_SHOW_MANUFACTURERS,
|
INV_HOME_SHOW_MANUFACTURERS,
|
||||||
@@ -325,6 +346,19 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (homeShowShipments && InvenTreeSalesOrderShipment().canView) {
|
||||||
|
tiles.add(
|
||||||
|
_listTile(
|
||||||
|
context,
|
||||||
|
L10().shipmentsPending,
|
||||||
|
TablerIcons.cube_send,
|
||||||
|
callback: () {
|
||||||
|
_showPendingShipments(context);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Suppliers
|
// Suppliers
|
||||||
if (homeShowSuppliers && InvenTreePurchaseOrder().canView) {
|
if (homeShowSuppliers && InvenTreePurchaseOrder().canView) {
|
||||||
tiles.add(
|
tiles.add(
|
||||||
|
|||||||
@@ -241,7 +241,7 @@ class _POLineDetailWidgetState extends RefreshableState<POLineDetailWidget> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// External link
|
// External link
|
||||||
if (widget.item.link.isNotEmpty) {
|
if (widget.item.hasLink) {
|
||||||
tiles.add(
|
tiles.add(
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(L10().link),
|
title: Text(L10().link),
|
||||||
|
|||||||
@@ -375,24 +375,7 @@ class _SalesOrderDetailState extends RefreshableState<SalesOrderDetailWidget> {
|
|||||||
|
|
||||||
Color lineColor = widget.order.complete ? COLOR_SUCCESS : COLOR_WARNING;
|
Color lineColor = widget.order.complete ? COLOR_SUCCESS : COLOR_WARNING;
|
||||||
|
|
||||||
// Shipment progress
|
// Line items progress
|
||||||
if (widget.order.shipmentCount > 0) {
|
|
||||||
tiles.add(
|
|
||||||
ListTile(
|
|
||||||
title: Text(L10().shipments),
|
|
||||||
subtitle: ProgressBar(
|
|
||||||
widget.order.completedShipmentCount.toDouble(),
|
|
||||||
maximum: widget.order.shipmentCount.toDouble(),
|
|
||||||
),
|
|
||||||
leading: Icon(TablerIcons.truck_delivery),
|
|
||||||
trailing: LargeText(
|
|
||||||
"${widget.order.completedShipmentCount} / ${widget.order.shipmentCount}",
|
|
||||||
color: lineColor,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
tiles.add(
|
tiles.add(
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(L10().lineItems),
|
title: Text(L10().lineItems),
|
||||||
@@ -408,6 +391,24 @@ class _SalesOrderDetailState extends RefreshableState<SalesOrderDetailWidget> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Shipment progress
|
||||||
|
if (widget.order.shipmentCount > 0) {
|
||||||
|
tiles.add(
|
||||||
|
ListTile(
|
||||||
|
title: Text(L10().shipments),
|
||||||
|
subtitle: ProgressBar(
|
||||||
|
widget.order.completedShipmentCount.toDouble(),
|
||||||
|
maximum: widget.order.shipmentCount.toDouble(),
|
||||||
|
),
|
||||||
|
leading: Icon(TablerIcons.cube_send),
|
||||||
|
trailing: LargeText(
|
||||||
|
"${widget.order.completedShipmentCount} / ${widget.order.shipmentCount}",
|
||||||
|
color: lineColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Extra line items
|
// Extra line items
|
||||||
tiles.add(
|
tiles.add(
|
||||||
ListTile(
|
ListTile(
|
||||||
@@ -522,8 +523,8 @@ class _SalesOrderDetailState extends RefreshableState<SalesOrderDetailWidget> {
|
|||||||
List<Widget> getTabIcons(BuildContext context) {
|
List<Widget> getTabIcons(BuildContext context) {
|
||||||
return [
|
return [
|
||||||
Tab(text: L10().details),
|
Tab(text: L10().details),
|
||||||
Tab(text: L10().shipments),
|
|
||||||
Tab(text: L10().lineItems),
|
Tab(text: L10().lineItems),
|
||||||
|
Tab(text: L10().shipments),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -531,8 +532,8 @@ class _SalesOrderDetailState extends RefreshableState<SalesOrderDetailWidget> {
|
|||||||
List<Widget> getTabs(BuildContext context) {
|
List<Widget> getTabs(BuildContext context) {
|
||||||
return [
|
return [
|
||||||
ListView(children: orderTiles(context)),
|
ListView(children: orderTiles(context)),
|
||||||
PaginatedSOShipmentList({"order": widget.order.pk.toString()}),
|
|
||||||
PaginatedSOLineList({"order": widget.order.pk.toString()}),
|
PaginatedSOLineList({"order": widget.order.pk.toString()}),
|
||||||
|
PaginatedSOShipmentList({"order": widget.order.pk.toString()}),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
69
lib/widget/order/so_allocation_list.dart
Normal file
69
lib/widget/order/so_allocation_list.dart
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import "package:flutter/material.dart";
|
||||||
|
import "package:inventree/api.dart";
|
||||||
|
import "package:inventree/inventree/model.dart";
|
||||||
|
import "package:inventree/inventree/part.dart";
|
||||||
|
import "package:inventree/inventree/sales_order.dart";
|
||||||
|
import "package:inventree/inventree/stock.dart";
|
||||||
|
import "package:inventree/l10.dart";
|
||||||
|
import "package:inventree/widget/link_icon.dart";
|
||||||
|
import "package:inventree/widget/paginator.dart";
|
||||||
|
|
||||||
|
class PaginatedSOAllocationList extends PaginatedSearchWidget {
|
||||||
|
const PaginatedSOAllocationList(Map<String, String> filters)
|
||||||
|
: super(filters: filters);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get searchTitle => L10().allocatedStock;
|
||||||
|
|
||||||
|
@override
|
||||||
|
_PaginatedSOAllocationListState createState() =>
|
||||||
|
_PaginatedSOAllocationListState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PaginatedSOAllocationListState
|
||||||
|
extends PaginatedSearchState<PaginatedSOAllocationList> {
|
||||||
|
_PaginatedSOAllocationListState() : super();
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get prefix => "so_allocation_";
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, String> get orderingOptions => {};
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, Map<String, dynamic>> get filterOptions => {};
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<InvenTreePageResponse?> requestPage(
|
||||||
|
int limit,
|
||||||
|
int offset,
|
||||||
|
Map<String, String> params,
|
||||||
|
) async {
|
||||||
|
final page = await InvenTreeSalesOrderAllocation().listPaginated(
|
||||||
|
limit,
|
||||||
|
offset,
|
||||||
|
filters: params,
|
||||||
|
);
|
||||||
|
|
||||||
|
return page;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget buildItem(BuildContext context, InvenTreeModel model) {
|
||||||
|
InvenTreeSalesOrderAllocation allocation =
|
||||||
|
model as InvenTreeSalesOrderAllocation;
|
||||||
|
|
||||||
|
InvenTreePart? part = allocation.part;
|
||||||
|
InvenTreeStockItem? stockItem = allocation.stockItem;
|
||||||
|
|
||||||
|
return ListTile(
|
||||||
|
title: Text(part?.fullname ?? ""),
|
||||||
|
subtitle: Text(part?.description ?? ""),
|
||||||
|
onTap: () async {
|
||||||
|
stockItem?.goToDetailPage(context);
|
||||||
|
},
|
||||||
|
leading: InvenTreeAPI().getThumbnail(allocation.part?.thumbnail ?? ""),
|
||||||
|
trailing: LargeText(stockItem?.serialOrQuantityDisplay() ?? ""),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -244,7 +244,7 @@ class _SOLineDetailWidgetState extends RefreshableState<SoLineDetailWidget> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// External link
|
// External link
|
||||||
if (widget.item.link.isNotEmpty) {
|
if (widget.item.hasLink) {
|
||||||
tiles.add(
|
tiles.add(
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(L10().link),
|
title: Text(L10().link),
|
||||||
|
|||||||
384
lib/widget/order/so_shipment_detail.dart
Normal file
384
lib/widget/order/so_shipment_detail.dart
Normal file
@@ -0,0 +1,384 @@
|
|||||||
|
/*
|
||||||
|
* Widget for displaying detail view of a single SalesOrderShipment
|
||||||
|
*/
|
||||||
|
|
||||||
|
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/api.dart";
|
||||||
|
import "package:inventree/api_form.dart";
|
||||||
|
import "package:inventree/app_colors.dart";
|
||||||
|
import "package:inventree/inventree/sales_order.dart";
|
||||||
|
import "package:inventree/l10.dart";
|
||||||
|
import "package:inventree/preferences.dart";
|
||||||
|
import "package:inventree/widget/attachment_widget.dart";
|
||||||
|
import "package:inventree/widget/link_icon.dart";
|
||||||
|
import "package:inventree/widget/notes_widget.dart";
|
||||||
|
import "package:inventree/widget/order/so_allocation_list.dart";
|
||||||
|
import "package:inventree/widget/refreshable_state.dart";
|
||||||
|
import "package:inventree/widget/snacks.dart";
|
||||||
|
|
||||||
|
class SOShipmentDetailWidget extends StatefulWidget {
|
||||||
|
const SOShipmentDetailWidget(this.shipment, {Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
final InvenTreeSalesOrderShipment shipment;
|
||||||
|
|
||||||
|
@override
|
||||||
|
_SOShipmentDetailWidgetState createState() => _SOShipmentDetailWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SOShipmentDetailWidgetState
|
||||||
|
extends RefreshableState<SOShipmentDetailWidget> {
|
||||||
|
_SOShipmentDetailWidgetState();
|
||||||
|
|
||||||
|
// The SalesOrder associated with this shipment
|
||||||
|
InvenTreeSalesOrder? order;
|
||||||
|
|
||||||
|
int attachmentCount = 0;
|
||||||
|
bool showCameraShortcut = true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String getAppBarTitle() => L10().shipment;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Widget> appBarActions(BuildContext context) {
|
||||||
|
List<Widget> actions = [];
|
||||||
|
|
||||||
|
if (widget.shipment.canEdit) {
|
||||||
|
actions.add(
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(TablerIcons.edit),
|
||||||
|
onPressed: () {
|
||||||
|
_editShipment(context);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return actions;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _editShipment(BuildContext context) async {
|
||||||
|
var fields = widget.shipment.formFields();
|
||||||
|
|
||||||
|
fields["order"]?["hidden"] = true;
|
||||||
|
|
||||||
|
widget.shipment.editForm(
|
||||||
|
context,
|
||||||
|
L10().shipmentEdit,
|
||||||
|
fields: fields,
|
||||||
|
onSuccess: (data) async {
|
||||||
|
refresh(context);
|
||||||
|
showSnackIcon(L10().shipmentUpdated, success: true);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> request(BuildContext context) async {
|
||||||
|
await widget.shipment.reload();
|
||||||
|
|
||||||
|
showCameraShortcut = await InvenTreeSettingsManager().getBool(
|
||||||
|
INV_SO_SHOW_CAMERA,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
final so = await InvenTreeSalesOrder().get(widget.shipment.orderId);
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
order = (so is InvenTreeSalesOrder ? so : null);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
InvenTreeSalesOrderShipmentAttachment()
|
||||||
|
.countAttachments(widget.shipment.pk)
|
||||||
|
.then((int value) {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
attachmentCount = value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Upload an image for this shipment
|
||||||
|
Future<void> _uploadImage(BuildContext context) async {
|
||||||
|
InvenTreeSalesOrderShipmentAttachment()
|
||||||
|
.uploadImage(widget.shipment.pk, prefix: widget.shipment.reference)
|
||||||
|
.then((result) => refresh(context));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mark this shipment as shipped
|
||||||
|
Future<void> _sendShipment(BuildContext context) async {
|
||||||
|
Map<String, dynamic> fields = {
|
||||||
|
"shipment_date": {
|
||||||
|
"value": widget.shipment.isShipped
|
||||||
|
? widget.shipment.shipment_date!
|
||||||
|
: DateTime.now().toIso8601String().split("T").first,
|
||||||
|
},
|
||||||
|
"tracking_number": {"value": widget.shipment.tracking_number},
|
||||||
|
"invoice_number": {"value": widget.shipment.invoice_number},
|
||||||
|
};
|
||||||
|
|
||||||
|
launchApiForm(
|
||||||
|
context,
|
||||||
|
L10().shipmentSend,
|
||||||
|
widget.shipment.SHIP_SHIPMENT_URL,
|
||||||
|
fields,
|
||||||
|
method: "POST",
|
||||||
|
onSuccess: (data) {
|
||||||
|
refresh(context);
|
||||||
|
showSnackIcon(L10().shipmentUpdated, success: true);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<SpeedDialChild> actionButtons(BuildContext context) {
|
||||||
|
List<SpeedDialChild> actions = [];
|
||||||
|
|
||||||
|
if (!widget.shipment.canEdit) {
|
||||||
|
// Exit early if we do not have edit permissions
|
||||||
|
return actions;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showCameraShortcut) {
|
||||||
|
actions.add(
|
||||||
|
SpeedDialChild(
|
||||||
|
child: Icon(TablerIcons.camera, color: Colors.blue),
|
||||||
|
label: L10().takePicture,
|
||||||
|
onTap: () async {
|
||||||
|
_uploadImage(context);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check shipment
|
||||||
|
if (!widget.shipment.isChecked && !widget.shipment.isShipped) {
|
||||||
|
actions.add(
|
||||||
|
SpeedDialChild(
|
||||||
|
child: Icon(TablerIcons.check, color: Colors.green),
|
||||||
|
label: L10().shipmentCheck,
|
||||||
|
onTap: () async {
|
||||||
|
widget.shipment
|
||||||
|
.update(values: {"checked_by": InvenTreeAPI().userId})
|
||||||
|
.then((_) {
|
||||||
|
showSnackIcon(L10().shipmentUpdated, success: true);
|
||||||
|
refresh(context);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uncheck shipment
|
||||||
|
if (widget.shipment.isChecked && !widget.shipment.isShipped) {
|
||||||
|
actions.add(
|
||||||
|
SpeedDialChild(
|
||||||
|
child: Icon(TablerIcons.x, color: Colors.red),
|
||||||
|
label: L10().shipmentUncheck,
|
||||||
|
onTap: () async {
|
||||||
|
widget.shipment.update(values: {"checked_by": null}).then((_) {
|
||||||
|
showSnackIcon(L10().shipmentUpdated, success: true);
|
||||||
|
refresh(context);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send shipment
|
||||||
|
if (!widget.shipment.isShipped) {
|
||||||
|
actions.add(
|
||||||
|
SpeedDialChild(
|
||||||
|
child: Icon(TablerIcons.truck_delivery, color: Colors.green),
|
||||||
|
label: L10().shipmentSend,
|
||||||
|
onTap: () async {
|
||||||
|
_sendShipment(context);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Cancel shipment
|
||||||
|
|
||||||
|
return actions;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Widget> shipmentTiles(BuildContext context) {
|
||||||
|
List<Widget> tiles = [];
|
||||||
|
|
||||||
|
final bool checked = widget.shipment.isChecked;
|
||||||
|
final bool shipped = widget.shipment.isShipped;
|
||||||
|
final bool delivered = widget.shipment.isDelivered;
|
||||||
|
|
||||||
|
// Order information
|
||||||
|
if (order != null) {
|
||||||
|
// Add SalesOrder information
|
||||||
|
|
||||||
|
tiles.add(
|
||||||
|
Card(
|
||||||
|
child: ListTile(
|
||||||
|
title: Text(order!.reference),
|
||||||
|
subtitle: Text(order!.description),
|
||||||
|
leading: api.getThumbnail(order!.customer?.thumbnail ?? ""),
|
||||||
|
trailing: LargeText(
|
||||||
|
api.SalesOrderStatus.label(order!.status),
|
||||||
|
color: api.SalesOrderStatus.color(order!.status),
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
order!.goToDetailPage(context);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shipment reference number
|
||||||
|
tiles.add(
|
||||||
|
ListTile(
|
||||||
|
title: Text(L10().shipmentReference),
|
||||||
|
trailing: LargeText(widget.shipment.reference),
|
||||||
|
leading: Icon(TablerIcons.hash),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (widget.shipment.invoice_number.isNotEmpty) {
|
||||||
|
tiles.add(
|
||||||
|
ListTile(
|
||||||
|
title: Text(L10().invoiceNumber),
|
||||||
|
trailing: LargeText(widget.shipment.invoice_number),
|
||||||
|
leading: Icon(TablerIcons.invoice),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tracking Number
|
||||||
|
if (widget.shipment.tracking_number.isNotEmpty) {
|
||||||
|
tiles.add(
|
||||||
|
ListTile(
|
||||||
|
title: Text(L10().trackingNumber),
|
||||||
|
trailing: LargeText(widget.shipment.tracking_number),
|
||||||
|
leading: Icon(TablerIcons.truck_delivery),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (checked || !shipped) {
|
||||||
|
tiles.add(
|
||||||
|
ListTile(
|
||||||
|
title: Text(L10().shipmentChecked),
|
||||||
|
trailing: LargeText(
|
||||||
|
checked ? L10().yes : L10().no,
|
||||||
|
color: checked ? COLOR_SUCCESS : COLOR_WARNING,
|
||||||
|
),
|
||||||
|
leading: Icon(
|
||||||
|
checked ? TablerIcons.circle_check : TablerIcons.circle_x,
|
||||||
|
color: checked ? COLOR_SUCCESS : COLOR_WARNING,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
tiles.add(
|
||||||
|
ListTile(
|
||||||
|
title: Text(L10().shipmentDate),
|
||||||
|
trailing: LargeText(
|
||||||
|
shipped ? widget.shipment.shipment_date! : L10().notApplicable,
|
||||||
|
),
|
||||||
|
leading: Icon(
|
||||||
|
shipped ? TablerIcons.calendar_check : TablerIcons.calendar_cancel,
|
||||||
|
color: shipped ? COLOR_SUCCESS : COLOR_WARNING,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
tiles.add(
|
||||||
|
ListTile(
|
||||||
|
title: Text(L10().deliveryDate),
|
||||||
|
trailing: LargeText(
|
||||||
|
delivered ? widget.shipment.delivery_date! : L10().notApplicable,
|
||||||
|
),
|
||||||
|
leading: Icon(
|
||||||
|
delivered ? TablerIcons.calendar_check : TablerIcons.calendar_cancel,
|
||||||
|
color: delivered ? COLOR_SUCCESS : COLOR_WARNING,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// External link
|
||||||
|
if (widget.shipment.hasLink) {
|
||||||
|
tiles.add(
|
||||||
|
ListTile(
|
||||||
|
title: Text(L10().link),
|
||||||
|
leading: Icon(TablerIcons.link, color: COLOR_ACTION),
|
||||||
|
trailing: LinkIcon(),
|
||||||
|
onTap: () async {
|
||||||
|
widget.shipment.openLink();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notes tile
|
||||||
|
tiles.add(
|
||||||
|
ListTile(
|
||||||
|
title: Text(L10().notes),
|
||||||
|
leading: Icon(TablerIcons.note, color: COLOR_ACTION),
|
||||||
|
trailing: LinkIcon(),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => NotesWidget(widget.shipment),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Attachments
|
||||||
|
tiles.add(
|
||||||
|
ListTile(
|
||||||
|
title: Text(L10().attachments),
|
||||||
|
leading: Icon(TablerIcons.file, color: COLOR_ACTION),
|
||||||
|
trailing: LinkIcon(
|
||||||
|
text: attachmentCount > 0 ? attachmentCount.toString() : null,
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => AttachmentWidget(
|
||||||
|
InvenTreeSalesOrderShipmentAttachment(),
|
||||||
|
widget.shipment.pk,
|
||||||
|
widget.shipment.reference,
|
||||||
|
widget.shipment.canEdit,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return tiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Widget> getTabIcons(BuildContext context) {
|
||||||
|
return [Tab(text: L10().details), Tab(text: L10().allocatedStock)];
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Widget> getTabs(BuildContext context) {
|
||||||
|
return [
|
||||||
|
ListView(children: shipmentTiles(context)),
|
||||||
|
PaginatedSOAllocationList({
|
||||||
|
"order": widget.shipment.orderId.toString(),
|
||||||
|
"shipment": widget.shipment.pk.toString(),
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,35 @@ import "package:inventree/widget/paginator.dart";
|
|||||||
|
|
||||||
import "package:inventree/inventree/model.dart";
|
import "package:inventree/inventree/model.dart";
|
||||||
import "package:inventree/l10.dart";
|
import "package:inventree/l10.dart";
|
||||||
|
import "package:inventree/widget/refreshable_state.dart";
|
||||||
|
|
||||||
|
class SOShipmentListWidget extends StatefulWidget {
|
||||||
|
const SOShipmentListWidget({
|
||||||
|
this.title = "",
|
||||||
|
this.filters = const {},
|
||||||
|
Key? key,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
final Map<String, String> filters;
|
||||||
|
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
@override
|
||||||
|
_SOShipmentListWidgetState createState() => _SOShipmentListWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SOShipmentListWidgetState
|
||||||
|
extends RefreshableState<SOShipmentListWidget> {
|
||||||
|
_SOShipmentListWidgetState();
|
||||||
|
|
||||||
|
@override
|
||||||
|
String getAppBarTitle() => widget.title;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget getBody(BuildContext context) {
|
||||||
|
return PaginatedSOShipmentList(widget.filters);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class PaginatedSOShipmentList extends PaginatedSearchWidget {
|
class PaginatedSOShipmentList extends PaginatedSearchWidget {
|
||||||
const PaginatedSOShipmentList(Map<String, String> filters)
|
const PaginatedSOShipmentList(Map<String, String> filters)
|
||||||
@@ -51,15 +80,21 @@ class _PaginatedSOShipmentListState
|
|||||||
Widget buildItem(BuildContext context, InvenTreeModel model) {
|
Widget buildItem(BuildContext context, InvenTreeModel model) {
|
||||||
InvenTreeSalesOrderShipment shipment = model as InvenTreeSalesOrderShipment;
|
InvenTreeSalesOrderShipment shipment = model as InvenTreeSalesOrderShipment;
|
||||||
|
|
||||||
|
InvenTreeSalesOrder? order = shipment.order;
|
||||||
return ListTile(
|
return ListTile(
|
||||||
title: Text(shipment.reference),
|
title: Text(
|
||||||
subtitle: Text(shipment.tracking_number),
|
"${order?.reference ?? L10().salesOrder} - ${shipment.reference}",
|
||||||
leading: shipment.shipped
|
),
|
||||||
|
subtitle: Text(order?.description ?? L10().description),
|
||||||
|
onTap: () async {
|
||||||
|
shipment.goToDetailPage(context);
|
||||||
|
},
|
||||||
|
leading: shipment.isShipped
|
||||||
? Icon(TablerIcons.calendar_check, color: COLOR_SUCCESS)
|
? Icon(TablerIcons.calendar_check, color: COLOR_SUCCESS)
|
||||||
: Icon(TablerIcons.calendar_cancel, color: COLOR_WARNING),
|
: Icon(TablerIcons.calendar_cancel, color: COLOR_WARNING),
|
||||||
trailing: shipment.shipped
|
trailing: shipment.isShipped
|
||||||
? LargeText(shipment.shipment_date ?? "")
|
? LargeText(shipment.shipment_date ?? "")
|
||||||
: null,
|
: LargeText(L10().pending),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -524,7 +524,7 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// External link?
|
// External link?
|
||||||
if (part.link.isNotEmpty) {
|
if (part.hasLink) {
|
||||||
tiles.add(
|
tiles.add(
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text("${part.link}"),
|
title: Text("${part.link}"),
|
||||||
|
|||||||
@@ -750,7 +750,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (widget.item.link.isNotEmpty) {
|
if (widget.item.hasLink) {
|
||||||
tiles.add(
|
tiles.add(
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text("${widget.item.link}"),
|
title: Text("${widget.item.link}"),
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
name: inventree
|
name: inventree
|
||||||
description: InvenTree stock management
|
description: InvenTree stock management
|
||||||
|
|
||||||
version: 0.19.3+102
|
version: 0.20.0+103
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.8.1
|
sdk: ^3.8.1
|
||||||
|
|||||||
Reference in New Issue
Block a user