2
0
mirror of https://github.com/inventree/inventree-app.git synced 2025-06-16 20:25:26 +00:00

Order extra lines (#632)

* Define classes for extra line item

* Display PO extra line items

- Also, some refactoring

* Support extra line items for sales order

* linting fixes

* Update release notes
This commit is contained in:
Oliver
2025-04-15 20:49:05 +10:00
committed by GitHub
parent 25d7ac9189
commit 72a78291b2
34 changed files with 642 additions and 193 deletions

View File

@ -0,0 +1,112 @@
import "package:flutter/material.dart";
import "package:flutter_tabler_icons/flutter_tabler_icons.dart";
import "package:inventree/helpers.dart";
import "package:inventree/l10.dart";
import "package:inventree/widget/refreshable_state.dart";
import "package:inventree/widget/snacks.dart";
import "package:inventree/inventree/orders.dart";
class ExtraLineDetailWidget extends StatefulWidget {
const ExtraLineDetailWidget(this.item, {Key? key}) : super(key: key);
final InvenTreeExtraLineItem item;
@override
_ExtraLineDetailWidgetState createState() => _ExtraLineDetailWidgetState();
}
class _ExtraLineDetailWidgetState extends RefreshableState<ExtraLineDetailWidget> {
_ExtraLineDetailWidgetState();
@override
String getAppBarTitle() => L10().extraLineItem;
@override
List<Widget> appBarActions(BuildContext context) {
List<Widget> actions = [];
if (widget.item.canEdit) {
actions.add(
IconButton(
icon: Icon(TablerIcons.edit),
onPressed: () {
_editLineItem(context);
}
)
);
}
return actions;
}
// Function to request data for this page
@override
Future<void> request(BuildContext context) async {
await widget.item.reload();
}
// Callback to edit this line item
Future<void> _editLineItem(BuildContext context) async {
var fields = widget.item.formFields();
widget.item.editForm(
context,
L10().editLineItem,
fields: fields,
onSuccess: (data) async {
refresh(context);
showSnackIcon(L10().lineItemUpdated, success: true);
}
);
}
@override
List<Widget> getTiles(BuildContext context) {
List<Widget> tiles = [];
tiles.add(
ListTile(
title: Text(L10().reference),
trailing: Text(widget.item.reference),
)
);
tiles.add(
ListTile(
title: Text(L10().description),
trailing: Text(widget.item.description),
)
);
tiles.add(
ListTile(
title: Text(L10().quantity),
trailing: Text(widget.item.quantity.toString()),
)
);
tiles.add(
ListTile(
title: Text(L10().unitPrice),
trailing: Text(
renderCurrency(widget.item.price, widget.item.priceCurrency)
)
)
);
if (widget.item.notes.isNotEmpty) {
tiles.add(
ListTile(
title: Text(L10().notes),
subtitle: Text(widget.item.notes),
)
);
}
return tiles;
}
}

View File

@ -0,0 +1,116 @@
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/l10.dart";
import "package:inventree/inventree/model.dart";
import "package:inventree/inventree/purchase_order.dart";
import "package:inventree/widget/paginator.dart";
import "package:inventree/widget/refreshable_state.dart";
import "package:inventree/widget/snacks.dart";
class POExtraLineListWidget extends StatefulWidget {
const POExtraLineListWidget(this.order, {this.filters = const {}, Key? key}) : super(key: key);
final InvenTreePurchaseOrder order;
final Map<String, String> filters;
@override
_PurchaseOrderExtraLineListWidgetState createState() => _PurchaseOrderExtraLineListWidgetState();
}
class _PurchaseOrderExtraLineListWidgetState extends RefreshableState<POExtraLineListWidget> {
_PurchaseOrderExtraLineListWidgetState();
@override
String getAppBarTitle() => L10().extraLineItems;
Future<void> _addLineItem(BuildContext context) async {
var fields = InvenTreePOExtraLineItem().formFields();
fields["order"]?["value"] = widget.order.pk;
InvenTreePOExtraLineItem().createForm(
context,
L10().lineItemAdd,
fields: fields,
onSuccess: (data) async {
refresh(context);
showSnackIcon(L10().lineItemUpdated, success: true);
}
);
}
@override
List<SpeedDialChild> actionButtons(BuildContext context) {
List<SpeedDialChild> actions = [];
if (widget.order.canEdit) {
actions.add(
SpeedDialChild(
child: Icon(TablerIcons.circle_plus, color: Colors.green),
label: L10().lineItemAdd,
onTap: () {
_addLineItem(context);
}
)
);
}
return actions;
}
@override
Widget getBody(BuildContext context) {
return PaginatedPOExtraLineList(widget.filters);
}
}
class PaginatedPOExtraLineList extends PaginatedSearchWidget {
const PaginatedPOExtraLineList(Map<String, String> filters) : super(filters: filters);
@override
String get searchTitle => L10().extraLineItems;
@override
_PaginatedPOExtraLineListState createState() => _PaginatedPOExtraLineListState();
}
class _PaginatedPOExtraLineListState extends PaginatedSearchState<PaginatedPOExtraLineList> {
_PaginatedPOExtraLineListState() : super();
@override
String get prefix => "po_extra_line_";
@override
Future<InvenTreePageResponse?> requestPage(int limit, int offset, Map<String, String> params) async {
final page = await InvenTreePOExtraLineItem().listPaginated(limit, offset, filters: params);
return page;
}
@override
Widget buildItem(BuildContext context, InvenTreeModel model) {
InvenTreePOExtraLineItem line = model as InvenTreePOExtraLineItem;
return ListTile(
title: Text(line.reference),
subtitle: Text(line.description),
trailing: Text(line.quantity.toString()),
onTap: () {
line.goToDetailPage(context).then((_) {
refresh();
});
},
);
}
}

View File

@ -13,8 +13,6 @@ import "package:inventree/inventree/purchase_order.dart";
import "package:inventree/inventree/stock.dart";
import "package:inventree/widget/progress.dart";
import "package:inventree/widget/part/part_detail.dart";
import "package:inventree/widget/stock/location_display.dart";
import "package:inventree/widget/refreshable_state.dart";
import "package:inventree/widget/snacks.dart";
import "package:inventree/widget/company/supplier_part_detail.dart";
@ -157,7 +155,7 @@ class _POLineDetailWidgetState extends RefreshableState<POLineDetailWidget> {
hideLoadingOverlay();
if (part is InvenTreePart) {
Navigator.push(context, MaterialPageRoute(builder: (context) => PartDetailWidget(part)));
part.goToDetailPage(context);
}
},
)
@ -187,14 +185,8 @@ class _POLineDetailWidgetState extends RefreshableState<POLineDetailWidget> {
title: Text(L10().destination),
subtitle: Text(destination!.name),
leading: Icon(TablerIcons.map_pin, color: COLOR_ACTION),
onTap: () =>
{
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => LocationDisplayWidget(destination)
)
)
onTap: () => {
destination!.goToDetailPage(context)
}
));
}

View File

@ -14,12 +14,12 @@ import "package:inventree/inventree/stock.dart";
import "package:inventree/inventree/purchase_order.dart";
import "package:inventree/widget/dialogs.dart";
import "package:inventree/widget/order/po_extra_line_list.dart";
import "package:inventree/widget/stock/location_display.dart";
import "package:inventree/widget/order/po_line_list.dart";
import "package:inventree/widget/attachment_widget.dart";
import "package:inventree/widget/company/company_detail.dart";
import "package:inventree/widget/notes_widget.dart";
import "package:inventree/widget/progress.dart";
import "package:inventree/widget/refreshable_state.dart";
@ -47,11 +47,11 @@ class _PurchaseOrderDetailState extends RefreshableState<PurchaseOrderDetailWidg
_PurchaseOrderDetailState();
List<InvenTreePOLineItem> lines = [];
int extraLineCount = 0;
InvenTreeStockLocation? destination;
int completedLines = 0;
int attachmentCount = 0;
bool showCameraShortcut = true;
@ -296,6 +296,15 @@ class _PurchaseOrderDetailState extends RefreshableState<PurchaseOrderDetailWidg
});
}
}
// Count number of "extra line items" against this order
InvenTreePOExtraLineItem().count(filters: {"order": widget.order.pk.toString() }).then((int value) {
if (mounted) {
setState(() {
extraLineCount = value;
});
}
});
}
// Edit the currently displayed PurchaseOrder
@ -368,12 +377,7 @@ class _PurchaseOrderDetailState extends RefreshableState<PurchaseOrderDetailWidg
subtitle: Text(supplier.name),
leading: Icon(TablerIcons.building, color: COLOR_ACTION),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => CompanyDetailWidget(supplier)
)
);
supplier.goToDetailPage(context);
},
));
}
@ -415,6 +419,21 @@ class _PurchaseOrderDetailState extends RefreshableState<PurchaseOrderDetailWidg
trailing: Text("${completedLines} / ${widget.order.lineItemCount}", style: TextStyle(color: lineColor)),
));
// Extra line items
tiles.add(ListTile(
title: Text(L10().extraLineItems),
leading: Icon(TablerIcons.clipboard_list, color: COLOR_ACTION),
trailing: Text(extraLineCount.toString()),
onTap: () => {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => POExtraLineListWidget(widget.order, filters: {"order": widget.order.pk.toString()})
)
)
},
));
tiles.add(ListTile(
title: Text(L10().totalPrice),
leading: Icon(TablerIcons.currency_dollar),

View File

@ -5,7 +5,6 @@ import "package:flutter_tabler_icons/flutter_tabler_icons.dart";
import "package:inventree/inventree/company.dart";
import "package:inventree/inventree/model.dart";
import "package:inventree/widget/paginator.dart";
import "package:inventree/widget/order/purchase_order_detail.dart";
import "package:inventree/widget/refreshable_state.dart";
import "package:inventree/l10.dart";
import "package:inventree/api.dart";
@ -69,13 +68,7 @@ class _PurchaseOrderListWidgetState extends RefreshableState<PurchaseOrderListWi
if (data.containsKey("pk")) {
var order = InvenTreePurchaseOrder.fromJson(data);
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => PurchaseOrderDetailWidget(order)
)
);
order.goToDetailPage(context);
}
}
);
@ -184,12 +177,7 @@ class _PaginatedPurchaseOrderListState extends PaginatedSearchState<PaginatedPur
),
),
onTap: () async {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => PurchaseOrderDetailWidget(order)
)
);
order.goToDetailPage(context);
},
);
}

View File

@ -7,6 +7,7 @@ import "package:inventree/barcode/sales_order.dart";
import "package:inventree/inventree/company.dart";
import "package:inventree/inventree/sales_order.dart";
import "package:inventree/preferences.dart";
import "package:inventree/widget/order/so_extra_line_list.dart";
import "package:inventree/widget/order/so_line_list.dart";
import "package:inventree/widget/order/so_shipment_list.dart";
import "package:inventree/widget/refreshable_state.dart";
@ -18,7 +19,6 @@ import "package:inventree/widget/attachment_widget.dart";
import "package:inventree/widget/dialogs.dart";
import "package:inventree/widget/notes_widget.dart";
import "package:inventree/widget/snacks.dart";
import "package:inventree/widget/company/company_detail.dart";
import "package:inventree/widget/progress.dart";
/*
@ -40,6 +40,7 @@ class _SalesOrderDetailState extends RefreshableState<SalesOrderDetailWidget> {
_SalesOrderDetailState();
List<InvenTreeSOLineItem> lines = [];
int extraLineCount = 0;
bool showCameraShortcut = true;
bool supportsProjectCodes = false;
@ -270,6 +271,15 @@ class _SalesOrderDetailState extends RefreshableState<SalesOrderDetailWidget> {
});
}
});
// Count number of "extra line items" against this order
InvenTreeSOExtraLineItem().count(filters: {"order": widget.order.pk.toString() }).then((int value) {
if (mounted) {
setState(() {
extraLineCount = value;
});
}
});
}
// Edit the current SalesOrder instance
@ -340,12 +350,7 @@ class _SalesOrderDetailState extends RefreshableState<SalesOrderDetailWidget> {
subtitle: Text(customer.name),
leading: Icon(TablerIcons.user, color: COLOR_ACTION),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => CompanyDetailWidget(customer)
)
);
customer.goToDetailPage(context);
}
));
}
@ -370,6 +375,21 @@ class _SalesOrderDetailState extends RefreshableState<SalesOrderDetailWidget> {
trailing: Text("${widget.order.completedLineItemCount} / ${widget.order.lineItemCount}", style: TextStyle(color: lineColor)),
));
// Extra line items
tiles.add(ListTile(
title: Text(L10().extraLineItems),
leading: Icon(TablerIcons.clipboard_list, color: COLOR_ACTION),
trailing: Text(extraLineCount.toString()),
onTap: () => {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SOExtraLineListWidget(widget.order, filters: {"order": widget.order.pk.toString()})
)
)
},
));
// Shipment progress
if (widget.order.shipmentCount > 0) {
tiles.add(ListTile(

View File

@ -3,7 +3,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/inventree/sales_order.dart";
import "package:inventree/widget/order/sales_order_detail.dart";
import "package:inventree/widget/paginator.dart";
import "package:inventree/widget/refreshable_state.dart";
@ -67,13 +66,7 @@ class _SalesOrderListWidgetState extends RefreshableState<SalesOrderListWidget>
if (data.containsKey("pk")) {
var order = InvenTreeSalesOrder.fromJson(data);
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SalesOrderDetailWidget(order)
)
);
order.goToDetailPage(context);
}
}
);
@ -167,12 +160,7 @@ class _PaginatedSalesOrderListState extends PaginatedSearchState<PaginatedSalesO
)
),
onTap: () async {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SalesOrderDetailWidget(order)
)
);
order.goToDetailPage(context);
}
);

View File

@ -0,0 +1,118 @@
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/l10.dart";
import "package:inventree/inventree/model.dart";
import "package:inventree/inventree/sales_order.dart";
import "package:inventree/widget/paginator.dart";
import "package:inventree/widget/refreshable_state.dart";
import "package:inventree/widget/snacks.dart";
class SOExtraLineListWidget extends StatefulWidget {
const SOExtraLineListWidget(this.order, {this.filters = const {}, Key? key}) : super(key: key);
final InvenTreeSalesOrder order;
final Map<String, String> filters;
@override
_SalesOrderExtraLineListWidgetState createState() => _SalesOrderExtraLineListWidgetState();
}
class _SalesOrderExtraLineListWidgetState extends RefreshableState<SOExtraLineListWidget> {
_SalesOrderExtraLineListWidgetState();
@override
String getAppBarTitle() => L10().extraLineItems;
Future<void> _addLineItem(BuildContext context) async {
var fields = InvenTreeSOExtraLineItem().formFields();
fields["order"]?["value"] = widget.order.pk;
InvenTreeSOExtraLineItem().createForm(
context,
L10().lineItemAdd,
fields: fields,
onSuccess: (data) async {
refresh(context);
showSnackIcon(L10().lineItemUpdated, success: true);
}
);
}
@override
List<SpeedDialChild> actionButtons(BuildContext context) {
List<SpeedDialChild> actions = [];
if (widget.order.canEdit) {
actions.add(
SpeedDialChild(
child: Icon(TablerIcons.circle_plus, color: Colors.green),
label: L10().lineItemAdd,
onTap: () {
_addLineItem(context);
}
)
);
}
return actions;
}
@override
Widget getBody(BuildContext context) {
return PaginatedSOExtraLineList(widget.filters);
}
}
class PaginatedSOExtraLineList extends PaginatedSearchWidget {
const PaginatedSOExtraLineList(Map<String, String> filters) : super(filters: filters);
@override
String get searchTitle => L10().extraLineItems;
@override
_PaginatedSOExtraLineListState createState() => _PaginatedSOExtraLineListState();
}
class _PaginatedSOExtraLineListState extends PaginatedSearchState<PaginatedSOExtraLineList> {
_PaginatedSOExtraLineListState() : super();
@override
String get prefix => "so_extra_line_";
@override
Future<InvenTreePageResponse?> requestPage(int limit, int offset, Map<String, String> params) async {
final page = await InvenTreeSOExtraLineItem().listPaginated(limit, offset, filters: params);
return page;
}
@override
Widget buildItem(BuildContext context, InvenTreeModel model) {
InvenTreeSOExtraLineItem line = model as InvenTreeSOExtraLineItem;
return ListTile(
title: Text(line.reference),
subtitle: Text(line.description),
trailing: Text(line.quantity.toString()),
onTap: () {
line.goToDetailPage(context).then((_) {
refresh();
});
},
);
}
}

View File

@ -15,7 +15,6 @@ import "package:inventree/inventree/sales_order.dart";
import "package:inventree/widget/refreshable_state.dart";
import "package:inventree/widget/progress.dart";
import "package:inventree/widget/part/part_detail.dart";
import "package:inventree/widget/snacks.dart";
import "package:inventree/app_colors.dart";
@ -192,7 +191,7 @@ class _SOLineDetailWidgetState extends RefreshableState<SoLineDetailWidget> {
hideLoadingOverlay();
if (part is InvenTreePart) {
Navigator.push(context, MaterialPageRoute(builder: (context) => PartDetailWidget(part)));
part.goToDetailPage(context);
}
}
)