From 9485d858eb0cd6a8b0b90b1b3a087bf864c6f13e Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 16 Feb 2023 22:50:32 +1100 Subject: [PATCH] Add support for company attachments (#261) * Add support for company attachments - Add API version check - Add new class - Add link to company detail page - Assorted refactoring * linting fixes --- assets/release_notes.md | 5 ++++ lib/api.dart | 19 +++++++----- lib/inventree/company.dart | 20 +++++++++++++ lib/widget/attachment_widget.dart | 1 - lib/widget/company_detail.dart | 50 ++++++++++++++++++++++++++----- lib/widget/location_display.dart | 15 +++++----- lib/widget/part_detail.dart | 19 ++++++------ lib/widget/refreshable_state.dart | 8 ++++- 8 files changed, 102 insertions(+), 35 deletions(-) diff --git a/assets/release_notes.md b/assets/release_notes.md index dab31188..3e6cee73 100644 --- a/assets/release_notes.md +++ b/assets/release_notes.md @@ -1,6 +1,11 @@ ## InvenTree App Release Notes --- +### 0.11.0 - +--- + +- Add support for attachments on Companies + ### 0.10.0 - February 2023 --- diff --git a/lib/api.dart b/lib/api.dart index 6d06f7f9..d8b012d6 100644 --- a/lib/api.dart +++ b/lib/api.dart @@ -258,17 +258,26 @@ class InvenTreeAPI { int get apiVersion => _apiVersion; + // API endpoint for receiving purchase order line items was introduced in v12 + bool get supportsPoReceive => apiVersion >= 12; + // Notification support requires API v25 or newer bool get supportsNotifications => isConnected() && apiVersion >= 25; + // Return True if the API supports 'settings' (requires API v46) + bool get supportsSettings => isConnected() && apiVersion >= 46; + + // Part parameter support requires API v56 or newer + bool get supportsPartParameters => isConnected() && apiVersion >= 56; + // Supports 'modern' barcode API (v80 or newer) bool get supportModernBarcodes => isConnected() && apiVersion >= 80; // Structural categories requires API v83 or newer bool get supportsStructuralCategories => isConnected() && apiVersion >= 83; - // Part parameter support requires API v56 or newer - bool get supportsPartParameters => isConnected() && apiVersion >= 56; + // Company attachments require API v95 or newer + bool get supportCompanyAttachments => isConnected() && apiVersion >= 95; // Are plugins enabled on the server? bool _pluginsEnabled = false; @@ -322,9 +331,6 @@ class InvenTreeAPI { // Ensure we only ever create a single instance of the API class static final InvenTreeAPI _api = InvenTreeAPI._internal(); - // API endpoint for receiving purchase order line items was introduced in v12 - bool get supportsPoReceive => apiVersion >= 12; - /* * Connect to the remote InvenTree server: * @@ -1280,9 +1286,6 @@ class InvenTreeAPI { ); } - // Return True if the API supports 'settings' (requires API v46) - bool get supportsSettings => isConnected() && apiVersion >= 46; - // Keep a record of which settings we have received from the server Map _globalSettings = {}; Map _userSettings = {}; diff --git a/lib/inventree/company.dart b/lib/inventree/company.dart index 85845541..a0d37a05 100644 --- a/lib/inventree/company.dart +++ b/lib/inventree/company.dart @@ -86,6 +86,26 @@ class InvenTreeCompany extends InvenTreeModel { } +/* + * Class representing an attachment file against a Company object + */ +class InvenTreeCompanyAttachment extends InvenTreeAttachment { + + InvenTreeCompanyAttachment() : super(); + + InvenTreeCompanyAttachment.fromJson(Map json) : super.fromJson(json); + + @override + String get REFERENCE_FIELD => "company"; + + @override + String get URL => "company/attachment/"; + + @override + InvenTreeModel createFromJson(Map json) => InvenTreeCompanyAttachment.fromJson(json); + +} + /* * The InvenTreeSupplierPart class represents the SupplierPart model in the InvenTree database */ diff --git a/lib/widget/attachment_widget.dart b/lib/widget/attachment_widget.dart index 8320d1a2..6ee694b7 100644 --- a/lib/widget/attachment_widget.dart +++ b/lib/widget/attachment_widget.dart @@ -134,7 +134,6 @@ class _AttachmentWidgetState extends RefreshableState { widget.attachment.REFERENCE_FIELD: widget.referenceId.toString() } ).then((var results) { - attachments.clear(); for (var result in results) { diff --git a/lib/widget/company_detail.dart b/lib/widget/company_detail.dart index 093ce07f..7cc528fc 100644 --- a/lib/widget/company_detail.dart +++ b/lib/widget/company_detail.dart @@ -8,12 +8,12 @@ import "package:inventree/app_colors.dart"; import "package:inventree/inventree/company.dart"; import "package:inventree/inventree/purchase_order.dart"; +import "package:inventree/widget/attachment_widget.dart"; import "package:inventree/widget/purchase_order_list.dart"; import "package:inventree/widget/refreshable_state.dart"; import "package:inventree/widget/snacks.dart"; import "package:inventree/widget/supplier_part_list.dart"; - /* * Widget for displaying detail view of a single Company instance */ @@ -37,6 +37,8 @@ class _CompanyDetailState extends RefreshableState { int supplierPartCount = 0; + int attachmentCount = 0; + @override String getAppBarTitle(BuildContext context) => L10().company; @@ -70,7 +72,6 @@ class _CompanyDetailState extends RefreshableState { @override Future request(BuildContext context) async { - final bool result = await widget.company.reload(); if (!result || widget.company.pk <= 0) { @@ -78,15 +79,16 @@ class _CompanyDetailState extends RefreshableState { Navigator.of(context).pop(); return; } - + if (widget.company.isSupplier) { - outstandingOrders = await widget.company.getPurchaseOrders(outstanding: true); + outstandingOrders = + await widget.company.getPurchaseOrders(outstanding: true); } InvenTreeSupplierPart().count( - filters: { - "supplier": widget.company.pk.toString() - } + filters: { + "supplier": widget.company.pk.toString() + } ).then((value) { if (mounted) { setState(() { @@ -94,6 +96,20 @@ class _CompanyDetailState extends RefreshableState { }); } }); + + if (api.supportCompanyAttachments) { + InvenTreeCompanyAttachment().count( + filters: { + "company": widget.company.pk.toString() + } + ).then((value) { + if (mounted) { + setState(() { + attachmentCount = value; + }); + } + }); + } } Future editCompany(BuildContext context) async { @@ -251,6 +267,26 @@ class _CompanyDetailState extends RefreshableState { )); } + if (api.supportCompanyAttachments) { + tiles.add(ListTile( + title: Text(L10().attachments), + leading: FaIcon(FontAwesomeIcons.fileLines, color: COLOR_CLICK), + trailing: attachmentCount > 0 ? Text(attachmentCount.toString()) : null, + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => AttachmentWidget( + InvenTreeCompanyAttachment(), + widget.company.pk, + api.checkPermission("purchase_order", "change") || api.checkPermission("sales_order", "change") + ) + ) + ); + } + )); + } + return tiles; } diff --git a/lib/widget/location_display.dart b/lib/widget/location_display.dart index 95e45aee..7adcc450 100644 --- a/lib/widget/location_display.dart +++ b/lib/widget/location_display.dart @@ -2,7 +2,6 @@ import "package:flutter/material.dart"; import "package:font_awesome_flutter/font_awesome_flutter.dart"; -import "package:inventree/api.dart"; import "package:inventree/app_colors.dart"; import "package:inventree/barcode.dart"; import "package:inventree/l10.dart"; @@ -51,7 +50,7 @@ class _LocationDisplayState extends RefreshableState { if (location != null) { // Add "locate" button - if (InvenTreeAPI().supportsMixin("locate")) { + if (api.supportsMixin("locate")) { actions.add( IconButton( icon: FaIcon(FontAwesomeIcons.magnifyingGlassLocation), @@ -64,7 +63,7 @@ class _LocationDisplayState extends RefreshableState { } // Add "edit" button - if (InvenTreeAPI().checkPermission("stock_location", "change")) { + if (api.checkPermission("stock_location", "change")) { actions.add( IconButton( icon: FaIcon(FontAwesomeIcons.penToSquare), @@ -86,7 +85,7 @@ class _LocationDisplayState extends RefreshableState { final _loc = location; if (_loc != null) { - InvenTreeAPI().locateItemOrLocation(context, location: _loc.pk); + api.locateItemOrLocation(context, location: _loc.pk); } } @@ -372,7 +371,7 @@ class _LocationDisplayState extends RefreshableState { tiles.add(locationDescriptionCard(includeActions: false)); - if (InvenTreeAPI().checkPermission("stock", "add")) { + if (api.checkPermission("stock", "add")) { tiles.add( ListTile( @@ -403,7 +402,7 @@ class _LocationDisplayState extends RefreshableState { if (location != null) { // Scan stock item into location - if (InvenTreeAPI().checkPermission("stock", "change")) { + if (api.checkPermission("stock", "change")) { tiles.add( ListTile( title: Text(L10().barcodeScanItem), @@ -429,7 +428,7 @@ class _LocationDisplayState extends RefreshableState { ); // Scan this location into another one - if (InvenTreeAPI().checkPermission("stock_location", "change")) { + if (api.checkPermission("stock_location", "change")) { tiles.add( ListTile( title: Text(L10().transferStockLocation), @@ -454,7 +453,7 @@ class _LocationDisplayState extends RefreshableState { ); } - if (InvenTreeAPI().supportModernBarcodes) { + if (api.supportModernBarcodes) { tiles.add( customBarcodeActionTile(context, this, location!.customBarcode, "stocklocation", location!.pk) ); diff --git a/lib/widget/part_detail.dart b/lib/widget/part_detail.dart index 3ebab6d5..b410a27e 100644 --- a/lib/widget/part_detail.dart +++ b/lib/widget/part_detail.dart @@ -2,7 +2,6 @@ import "package:flutter/material.dart"; import "package:font_awesome_flutter/font_awesome_flutter.dart"; -import "package:inventree/api.dart"; import "package:inventree/app_colors.dart"; import "package:inventree/barcode.dart"; import "package:inventree/l10.dart"; @@ -73,7 +72,7 @@ class _PartDisplayState extends RefreshableState { List actions = []; - if (InvenTreeAPI().checkPermission("part", "view")) { + if (api.checkPermission("part", "view")) { actions.add( IconButton( icon: FaIcon(FontAwesomeIcons.globe), @@ -82,7 +81,7 @@ class _PartDisplayState extends RefreshableState { ); } - if (InvenTreeAPI().checkPermission("part", "change")) { + if (api.checkPermission("part", "change")) { actions.add( IconButton( icon: FaIcon(FontAwesomeIcons.penToSquare), @@ -144,7 +143,7 @@ class _PartDisplayState extends RefreshableState { }); // Request the number of parameters for this part - if (InvenTreeAPI().supportsPartParameters) { + if (api.supportsPartParameters) { showParameters = await InvenTreeSettingsManager().getValue(INV_PART_SHOW_PARAMETERS, true) as bool; @@ -222,7 +221,7 @@ class _PartDisplayState extends RefreshableState { */ Future _toggleStar(BuildContext context) async { - if (InvenTreeAPI().checkPermission("part", "view")) { + if (api.checkPermission("part", "view")) { showLoadingOverlay(context); await part.update(values: {"starred": "${!part.starred}"}); hideLoadingOverlay(); @@ -256,7 +255,7 @@ class _PartDisplayState extends RefreshableState { }, ), leading: GestureDetector( - child: InvenTreeAPI().getImage(part.thumbnail), + child: api.getImage(part.thumbnail), onTap: () { Navigator.push( context, @@ -316,7 +315,7 @@ class _PartDisplayState extends RefreshableState { ListTile( title: Text(L10().templatePart), subtitle: Text(parentPart!.fullname), - leading: InvenTreeAPI().getImage( + leading: api.getImage( parentPart!.thumbnail, width: 32, height: 32, @@ -589,7 +588,7 @@ class _PartDisplayState extends RefreshableState { builder: (context) => AttachmentWidget( InvenTreePartAttachment(), part.pk, - InvenTreeAPI().checkPermission("part", "change")) + api.checkPermission("part", "change")) ) ); }, @@ -646,7 +645,7 @@ class _PartDisplayState extends RefreshableState { if (part.isTrackable) { // read the next available serial number showLoadingOverlay(context); - var response = await InvenTreeAPI().get("/api/part/${part.pk}/serial-numbers/", expectedStatusCode: null); + var response = await api.get("/api/part/${part.pk}/serial-numbers/", expectedStatusCode: null); hideLoadingOverlay(); if (response.isValid() && response.statusCode == 200) { @@ -701,7 +700,7 @@ class _PartDisplayState extends RefreshableState { ) ); - if (InvenTreeAPI().supportModernBarcodes) { + if (api.supportModernBarcodes) { tiles.add( customBarcodeActionTile(context, this, part.customBarcode, "part", part.pk) ); diff --git a/lib/widget/refreshable_state.dart b/lib/widget/refreshable_state.dart index 6a87dac7..184d8b52 100644 --- a/lib/widget/refreshable_state.dart +++ b/lib/widget/refreshable_state.dart @@ -1,6 +1,9 @@ +import "package:flutter/material.dart"; + +import "package:inventree/api.dart"; + import "package:inventree/widget/back.dart"; import "package:inventree/widget/drawer.dart"; -import "package:flutter/material.dart"; /* @@ -62,6 +65,9 @@ abstract class RefreshableState extends State with bool get loaded => !loading; + // Helper function to return API instance + InvenTreeAPI get api => InvenTreeAPI(); + // Update current tab selection void onTabSelectionChanged(int index) {