2
0
mirror of https://github.com/inventree/inventree-app.git synced 2025-10-29 04:27:36 +00:00
Files
inventree-app/lib/widget/order/so_shipment_detail.dart
Oliver 624655ec6b 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
2025-10-24 13:36:10 +11:00

385 lines
10 KiB
Dart

/*
* 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(),
}),
];
}
}