mirror of
https://github.com/inventree/inventree-app.git
synced 2025-10-29 12:37:37 +00:00
* 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
385 lines
10 KiB
Dart
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(),
|
|
}),
|
|
];
|
|
}
|
|
}
|