mirror of
https://github.com/inventree/inventree-app.git
synced 2025-10-28 20:17:36 +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
|
||||
---
|
||||
|
||||
|
||||
@@ -279,6 +279,8 @@ class InvenTreeAPI {
|
||||
|
||||
String get username => (userInfo["username"] ?? "") as String;
|
||||
|
||||
int get userId => (userInfo["pk"] ?? -1) as int;
|
||||
|
||||
// Map of server information
|
||||
Map<String, dynamic> serverInfo = {};
|
||||
|
||||
|
||||
@@ -586,6 +586,8 @@ class APIFormField {
|
||||
return InvenTreeSupplierPart().defaultListFilters();
|
||||
case InvenTreeStockItem.MODEL_TYPE:
|
||||
return InvenTreeStockItem().defaultListFilters();
|
||||
case InvenTreeSalesOrder.MODEL_TYPE:
|
||||
return InvenTreeSalesOrder().defaultListFilters();
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -727,7 +729,7 @@ class APIFormField {
|
||||
return ListTile(
|
||||
title: Text(shipment.reference),
|
||||
subtitle: Text(shipment.tracking_number),
|
||||
trailing: shipment.shipped ? Text(shipment.shipment_date!) : null,
|
||||
trailing: shipment.isShipped ? Text(shipment.shipment_date!) : null,
|
||||
);
|
||||
case "owner":
|
||||
String name = (data["name"] ?? "") as String;
|
||||
@@ -754,6 +756,15 @@ class APIFormField {
|
||||
subtitle: Text(project_code.description),
|
||||
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:
|
||||
return ListTile(
|
||||
title: Text(
|
||||
|
||||
@@ -601,7 +601,7 @@ class InvenTreeModel {
|
||||
|
||||
// POST data to update the model
|
||||
Future<APIResponse> update({
|
||||
Map<String, String> values = const {},
|
||||
Map<String, dynamic> values = const {},
|
||||
int? expectedStatusCode = 200,
|
||||
}) async {
|
||||
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/model.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/order/extra_line_detail.dart";
|
||||
import "package:inventree/widget/order/sales_order_detail.dart";
|
||||
@@ -269,6 +272,19 @@ class InvenTreeSalesOrderShipment extends InvenTreeModel {
|
||||
@override
|
||||
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";
|
||||
|
||||
@override
|
||||
@@ -284,6 +300,18 @@ class InvenTreeSalesOrderShipment extends InvenTreeModel {
|
||||
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 tracking_number => getString("tracking_number");
|
||||
@@ -292,7 +320,113 @@ class InvenTreeSalesOrderShipment extends InvenTreeModel {
|
||||
|
||||
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/"
|
||||
: "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": {},
|
||||
|
||||
"allocatedStock": "Allocated Stock",
|
||||
"@allocatedStock": {},
|
||||
|
||||
"appReleaseNotes": "Display app release notes",
|
||||
"@appReleaseNotes": {},
|
||||
|
||||
@@ -331,6 +334,15 @@
|
||||
"deleteFailed": "Delete operation failed",
|
||||
"@deleteFailed": {},
|
||||
|
||||
"deleteImageConfirmation": "Are you sure you want to delete this image?",
|
||||
"@deleteImageConfirmation": {},
|
||||
|
||||
"deleteImageTooltip": "Delete Image",
|
||||
"@deleteImageTooltip": {},
|
||||
|
||||
"deleteImage": "Delete Image",
|
||||
"@deleteImage": {},
|
||||
|
||||
"deletePart": "Delete Part",
|
||||
"@deletePart": {},
|
||||
|
||||
@@ -340,6 +352,9 @@
|
||||
"deleteSuccess": "Delete operation successful",
|
||||
"@deleteSuccess": {},
|
||||
|
||||
"deliveryDate": "Delivery Date",
|
||||
"@deliveryDate": {},
|
||||
|
||||
"description": "Description",
|
||||
"@description": {},
|
||||
|
||||
@@ -548,6 +563,12 @@
|
||||
"homeShowPoDescription": "Show purchase order button on home screen",
|
||||
"@homeShowPoDescription": {},
|
||||
|
||||
"homeShowShipments": "Show Shipments",
|
||||
"@homeShowShipments": {},
|
||||
|
||||
"homeShowShipmentsDescription": "Show pending shipments on the home screen",
|
||||
"@homeShowShipmentsDescription": {},
|
||||
|
||||
"homeShowSo": "Show Sales Orders",
|
||||
"@homeShowSo": {},
|
||||
|
||||
@@ -647,6 +668,12 @@
|
||||
"invalidUsernamePassword": "Invalid username / password combination",
|
||||
"@invalidUsernamePassword": {},
|
||||
|
||||
"invoice": "Invoice",
|
||||
"@invoice": {},
|
||||
|
||||
"invoiceNumber": "Invoice Number",
|
||||
"@invoiceNumber": {},
|
||||
|
||||
"issue": "Issue",
|
||||
"@issue": {},
|
||||
|
||||
@@ -769,6 +796,12 @@
|
||||
"@name": {
|
||||
},
|
||||
|
||||
"no": "No",
|
||||
"@no": {},
|
||||
|
||||
"notApplicable": "N/A",
|
||||
"@notApplicable": {},
|
||||
|
||||
"notConnected": "Not Connected",
|
||||
"@notConnected": {},
|
||||
|
||||
@@ -780,8 +813,8 @@
|
||||
"notifications": "Notifications",
|
||||
"@notifications": {},
|
||||
|
||||
"notificationsEmpty": "No unread notifications",
|
||||
"@notificationsEmpty": {},
|
||||
"notificationsEmpty": "No unread notifications",
|
||||
"@notificationsEmpty": {},
|
||||
|
||||
"noResponse": "No Response from Server",
|
||||
"@noResponse": {},
|
||||
@@ -792,6 +825,12 @@
|
||||
"noImageAvailable": "No image available",
|
||||
"@noImageAvailable": {},
|
||||
|
||||
"noPricingAvailable": "No pricing available",
|
||||
"@noPricingAvailable": {},
|
||||
|
||||
"noPricingDataFound": "No pricing data found for this part",
|
||||
"@noPricingDataFound": {},
|
||||
|
||||
"noSubcategories": "No Subcategories",
|
||||
"@noSubcategories": {},
|
||||
|
||||
@@ -927,6 +966,9 @@
|
||||
"passwordEmpty": "Password cannot be empty",
|
||||
"@passwordEmpty": {},
|
||||
|
||||
"pending": "Pending",
|
||||
"@pending": {},
|
||||
|
||||
"permissionAccountDenied": "Your account does not have the required permissions to perform this action",
|
||||
"@permissionAccountDenied": {},
|
||||
|
||||
@@ -1186,20 +1228,20 @@
|
||||
"salesOrders": "Sales Orders",
|
||||
"@salesOrders": {},
|
||||
|
||||
"salesOrderEnable": "Enable Sales Orders",
|
||||
"@salesOrderEnable": {},
|
||||
"salesOrderEnable": "Enable Sales Orders",
|
||||
"@salesOrderEnable": {},
|
||||
|
||||
"salesOrderEnableDetail": "Enable sales order functionality",
|
||||
"@salesOrderEnableDetail": {},
|
||||
"salesOrderEnableDetail": "Enable sales order functionality",
|
||||
"@salesOrderEnableDetail": {},
|
||||
|
||||
"salesOrderShowCamera": "Camera Shortcut",
|
||||
"@salesOrderShowCamera": {},
|
||||
"salesOrderShowCamera": "Camera Shortcut",
|
||||
"@salesOrderShowCamera": {},
|
||||
|
||||
"salesOrderShowCameraDetail": "Enable image upload shortcut on sales order screen",
|
||||
"@salesOrderShowCameraDetail": {},
|
||||
"salesOrderShowCameraDetail": "Enable image upload shortcut on sales order screen",
|
||||
"@salesOrderShowCameraDetail": {},
|
||||
|
||||
"salesOrderSettings": "Sales order settings",
|
||||
"@salesOrderSettings": {},
|
||||
"@salesOrderSettings": {},
|
||||
|
||||
"salesOrderCreate": "New Sales Order",
|
||||
"@saleOrderCreate": {},
|
||||
@@ -1338,12 +1380,48 @@
|
||||
"serverNotSelected": "Server not selected",
|
||||
"@serverNotSelected": {},
|
||||
|
||||
"shipment": "Shipment",
|
||||
"@shipment": {},
|
||||
|
||||
"shipments": "Shipments",
|
||||
"@shipments": {},
|
||||
|
||||
"shipmentsPending": "Pending Shipments",
|
||||
"@shipmentsPending": {},
|
||||
|
||||
"shipmentAdd": "Add Shipment",
|
||||
"@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": {},
|
||||
|
||||
@@ -1555,6 +1633,9 @@
|
||||
"totalPrice": "Total Price",
|
||||
"@totalPrice": {},
|
||||
|
||||
"trackingNumber": "Tracking Number",
|
||||
"@trackingNumber": {},
|
||||
|
||||
"transfer": "Transfer",
|
||||
"@transfer": {
|
||||
"description": "transfer"
|
||||
@@ -1642,6 +1723,9 @@
|
||||
"website": "Website",
|
||||
"@website": {},
|
||||
|
||||
"yes": "Yes",
|
||||
"@yes": {},
|
||||
|
||||
"price": "Price",
|
||||
"@price": {},
|
||||
|
||||
@@ -1682,20 +1766,5 @@
|
||||
"@currency": {},
|
||||
|
||||
"priceBreaks": "Price Breaks",
|
||||
"@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": {}
|
||||
"@priceBreaks": {}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import "package:path/path.dart";
|
||||
const String INV_HOME_SHOW_SUBSCRIBED = "homeShowSubscribed";
|
||||
const String INV_HOME_SHOW_PO = "homeShowPo";
|
||||
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_CUSTOMERS = "homeShowCustomers";
|
||||
const String INV_HOME_SHOW_SUPPLIERS = "homeShowSuppliers";
|
||||
|
||||
@@ -20,6 +20,7 @@ class _HomeScreenSettingsState extends State<HomeScreenSettingsWidget> {
|
||||
bool homeShowSubscribed = true;
|
||||
bool homeShowPo = true;
|
||||
bool homeShowSo = true;
|
||||
bool homeShowShipments = true;
|
||||
bool homeShowSuppliers = true;
|
||||
bool homeShowManufacturers = true;
|
||||
bool homeShowCustomers = true;
|
||||
@@ -46,6 +47,11 @@ class _HomeScreenSettingsState extends State<HomeScreenSettingsWidget> {
|
||||
homeShowSo =
|
||||
await InvenTreeSettingsManager().getValue(INV_HOME_SHOW_SO, true)
|
||||
as bool;
|
||||
|
||||
homeShowShipments =
|
||||
await InvenTreeSettingsManager().getValue(INV_HOME_SHOW_SHIPMENTS, true)
|
||||
as bool;
|
||||
|
||||
homeShowManufacturers =
|
||||
await InvenTreeSettingsManager().getValue(
|
||||
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(
|
||||
title: Text(L10().homeShowSuppliers),
|
||||
subtitle: Text(L10().homeShowSuppliersDescription),
|
||||
|
||||
@@ -3,7 +3,6 @@ import "dart:io";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_tabler_icons/flutter_tabler_icons.dart";
|
||||
import "package:one_context/one_context.dart";
|
||||
import "package:url_launcher/url_launcher.dart";
|
||||
|
||||
import "package:inventree/api.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(
|
||||
ListTile(
|
||||
title: Text(attachment.link),
|
||||
subtitle: Text(attachment.comment),
|
||||
leading: Icon(TablerIcons.link, color: COLOR_ACTION),
|
||||
onTap: () async {
|
||||
var uri = Uri.tryParse(attachment.link.trimLeft());
|
||||
if (uri != null && await canLaunchUrl(uri)) {
|
||||
await launchUrl(uri);
|
||||
}
|
||||
attachment.openLink();
|
||||
},
|
||||
onLongPress: () {
|
||||
showOptionsMenu(context, attachment);
|
||||
|
||||
@@ -287,7 +287,7 @@ class _CompanyDetailState extends RefreshableState<CompanyDetailWidget> {
|
||||
}
|
||||
|
||||
// External link
|
||||
if (widget.company.link.isNotEmpty) {
|
||||
if (widget.company.hasLink) {
|
||||
tiles.add(
|
||||
ListTile(
|
||||
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/snacks.dart";
|
||||
import "package:inventree/widget/progress.dart";
|
||||
import "package:url_launcher/url_launcher.dart";
|
||||
|
||||
/*
|
||||
* 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(
|
||||
ListTile(
|
||||
title: Text(widget.manufacturerPart.link),
|
||||
leading: Icon(TablerIcons.link, color: COLOR_ACTION),
|
||||
onTap: () async {
|
||||
var uri = Uri.tryParse(widget.manufacturerPart.link);
|
||||
if (uri != null && await canLaunchUrl(uri)) {
|
||||
await launchUrl(uri);
|
||||
}
|
||||
widget.manufacturerPart.openLink();
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
@@ -2,7 +2,6 @@ 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/widget/link_icon.dart";
|
||||
import "package:url_launcher/url_launcher.dart";
|
||||
|
||||
import "package:inventree/app_colors.dart";
|
||||
import "package:inventree/l10.dart";
|
||||
@@ -239,7 +238,7 @@ class _SupplierPartDisplayState
|
||||
);
|
||||
}
|
||||
|
||||
if (widget.supplierPart.link.isNotEmpty) {
|
||||
if (widget.supplierPart.hasLink) {
|
||||
tiles.add(
|
||||
ListTile(
|
||||
title: Text(L10().link),
|
||||
@@ -247,10 +246,7 @@ class _SupplierPartDisplayState
|
||||
leading: Icon(TablerIcons.link, color: COLOR_ACTION),
|
||||
trailing: LinkIcon(external: true),
|
||||
onTap: () async {
|
||||
var uri = Uri.tryParse(widget.supplierPart.link);
|
||||
if (uri != null && await canLaunchUrl(uri)) {
|
||||
await launchUrl(uri);
|
||||
}
|
||||
widget.supplierPart.openLink();
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
@@ -14,6 +14,7 @@ import "package:inventree/preferences.dart";
|
||||
import "package:inventree/l10.dart";
|
||||
import "package:inventree/settings/select_server.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/drawer.dart";
|
||||
@@ -55,6 +56,7 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage>
|
||||
|
||||
bool homeShowPo = false;
|
||||
bool homeShowSo = false;
|
||||
bool homeShowShipments = false;
|
||||
bool homeShowSubscribed = false;
|
||||
bool homeShowManufacturers = 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) {
|
||||
if (!InvenTreeAPI().checkConnection()) return;
|
||||
|
||||
@@ -167,6 +183,11 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage>
|
||||
homeShowSo =
|
||||
await InvenTreeSettingsManager().getValue(INV_HOME_SHOW_SO, true)
|
||||
as bool;
|
||||
|
||||
homeShowShipments =
|
||||
await InvenTreeSettingsManager().getValue(INV_HOME_SHOW_SHIPMENTS, true)
|
||||
as bool;
|
||||
|
||||
homeShowManufacturers =
|
||||
await InvenTreeSettingsManager().getValue(
|
||||
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
|
||||
if (homeShowSuppliers && InvenTreePurchaseOrder().canView) {
|
||||
tiles.add(
|
||||
|
||||
@@ -241,7 +241,7 @@ class _POLineDetailWidgetState extends RefreshableState<POLineDetailWidget> {
|
||||
}
|
||||
|
||||
// External link
|
||||
if (widget.item.link.isNotEmpty) {
|
||||
if (widget.item.hasLink) {
|
||||
tiles.add(
|
||||
ListTile(
|
||||
title: Text(L10().link),
|
||||
|
||||
@@ -375,24 +375,7 @@ class _SalesOrderDetailState extends RefreshableState<SalesOrderDetailWidget> {
|
||||
|
||||
Color lineColor = widget.order.complete ? COLOR_SUCCESS : COLOR_WARNING;
|
||||
|
||||
// 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.truck_delivery),
|
||||
trailing: LargeText(
|
||||
"${widget.order.completedShipmentCount} / ${widget.order.shipmentCount}",
|
||||
color: lineColor,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Line items progress
|
||||
tiles.add(
|
||||
ListTile(
|
||||
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
|
||||
tiles.add(
|
||||
ListTile(
|
||||
@@ -522,8 +523,8 @@ class _SalesOrderDetailState extends RefreshableState<SalesOrderDetailWidget> {
|
||||
List<Widget> getTabIcons(BuildContext context) {
|
||||
return [
|
||||
Tab(text: L10().details),
|
||||
Tab(text: L10().shipments),
|
||||
Tab(text: L10().lineItems),
|
||||
Tab(text: L10().shipments),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -531,8 +532,8 @@ class _SalesOrderDetailState extends RefreshableState<SalesOrderDetailWidget> {
|
||||
List<Widget> getTabs(BuildContext context) {
|
||||
return [
|
||||
ListView(children: orderTiles(context)),
|
||||
PaginatedSOShipmentList({"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
|
||||
if (widget.item.link.isNotEmpty) {
|
||||
if (widget.item.hasLink) {
|
||||
tiles.add(
|
||||
ListTile(
|
||||
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/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 {
|
||||
const PaginatedSOShipmentList(Map<String, String> filters)
|
||||
@@ -51,15 +80,21 @@ class _PaginatedSOShipmentListState
|
||||
Widget buildItem(BuildContext context, InvenTreeModel model) {
|
||||
InvenTreeSalesOrderShipment shipment = model as InvenTreeSalesOrderShipment;
|
||||
|
||||
InvenTreeSalesOrder? order = shipment.order;
|
||||
return ListTile(
|
||||
title: Text(shipment.reference),
|
||||
subtitle: Text(shipment.tracking_number),
|
||||
leading: shipment.shipped
|
||||
title: Text(
|
||||
"${order?.reference ?? L10().salesOrder} - ${shipment.reference}",
|
||||
),
|
||||
subtitle: Text(order?.description ?? L10().description),
|
||||
onTap: () async {
|
||||
shipment.goToDetailPage(context);
|
||||
},
|
||||
leading: shipment.isShipped
|
||||
? Icon(TablerIcons.calendar_check, color: COLOR_SUCCESS)
|
||||
: Icon(TablerIcons.calendar_cancel, color: COLOR_WARNING),
|
||||
trailing: shipment.shipped
|
||||
trailing: shipment.isShipped
|
||||
? LargeText(shipment.shipment_date ?? "")
|
||||
: null,
|
||||
: LargeText(L10().pending),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -524,7 +524,7 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
|
||||
}
|
||||
|
||||
// External link?
|
||||
if (part.link.isNotEmpty) {
|
||||
if (part.hasLink) {
|
||||
tiles.add(
|
||||
ListTile(
|
||||
title: Text("${part.link}"),
|
||||
|
||||
@@ -750,7 +750,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
||||
);
|
||||
}
|
||||
|
||||
if (widget.item.link.isNotEmpty) {
|
||||
if (widget.item.hasLink) {
|
||||
tiles.add(
|
||||
ListTile(
|
||||
title: Text("${widget.item.link}"),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
name: inventree
|
||||
description: InvenTree stock management
|
||||
|
||||
version: 0.19.3+102
|
||||
version: 0.20.0+103
|
||||
|
||||
environment:
|
||||
sdk: ^3.8.1
|
||||
|
||||
Reference in New Issue
Block a user