Attachments refactor (#737)

* refactor attachment code into its own file

* Add getters

* Remove custom models for each type of attachment

* Refactor existing widgets

* Fix double camera open bug

* Remove dead code

* Remove unused imports

* Refactor common code

* format

* Update release notes
This commit is contained in:
Oliver 2025-11-28 23:53:10 +11:00 committed by GitHub
parent bb10117f01
commit 346b1a150f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 381 additions and 519 deletions

View file

@ -0,0 +1,176 @@
import "dart:io";
import "package:flutter/cupertino.dart";
import "package:flutter_tabler_icons/flutter_tabler_icons.dart";
import "package:inventree/api.dart";
import "package:inventree/inventree/model.dart";
import "package:inventree/inventree/sentry.dart";
import "package:inventree/l10.dart";
import "package:inventree/widget/fields.dart";
import "package:inventree/widget/snacks.dart";
import "package:path/path.dart" as path;
class InvenTreeAttachment extends InvenTreeModel {
// Class representing an "attachment" file
InvenTreeAttachment() : super();
InvenTreeAttachment.fromJson(Map<String, dynamic> json)
: super.fromJson(json);
@override
InvenTreeAttachment createFromJson(Map<String, dynamic> json) =>
InvenTreeAttachment.fromJson(json);
@override
String get URL => "attachment/";
@override
Map<String, Map<String, dynamic>> formFields() {
Map<String, Map<String, dynamic>> fields = {"link": {}, "comment": {}};
if (!hasLink) {
fields.remove("link");
}
return fields;
}
// The model type of the instance this attachment is associated with
String get modelType => getString("model_type");
// The ID of the instance this attachment is associated with
int get modelId => getInt("model_id");
String get attachment => getString("attachment");
bool get hasAttachment => attachment.isNotEmpty;
// Return the filename of the attachment
String get filename {
return attachment.split("/").last;
}
IconData get icon {
String fn = filename.toLowerCase();
if (fn.endsWith(".pdf")) {
return TablerIcons.file_type_pdf;
} else if (fn.endsWith(".csv")) {
return TablerIcons.file_type_csv;
} else if (fn.endsWith(".doc") || fn.endsWith(".docx")) {
return TablerIcons.file_type_doc;
} else if (fn.endsWith(".xls") || fn.endsWith(".xlsx")) {
return TablerIcons.file_type_xls;
}
// Image formats
final List<String> img_formats = [".png", ".jpg", ".gif", ".bmp", ".svg"];
for (String fmt in img_formats) {
if (fn.endsWith(fmt)) {
return TablerIcons.file_type_jpg;
}
}
return TablerIcons.file;
}
String get comment => getString("comment");
DateTime? get uploadDate {
if (jsondata.containsKey("upload_date")) {
return DateTime.tryParse((jsondata["upload_date"] ?? "") as String);
} else {
return null;
}
}
// Return a count of how many attachments exist against the specified model ID
Future<int> countAttachments(String modelType, int modelId) async {
Map<String, String> filters = {};
if (!api.supportsModernAttachments) {
return 0;
}
filters["model_type"] = modelType;
filters["model_id"] = modelId.toString();
return count(filters: filters);
}
Future<bool> uploadAttachment(
File attachment,
String modelType,
int modelId, {
String comment = "",
Map<String, String> fields = const {},
}) async {
// Ensure that the correct reference field is set
Map<String, String> data = Map<String, String>.from(fields);
String url = URL;
if (comment.isNotEmpty) {
data["comment"] = comment;
}
data["model_type"] = modelType;
data["model_id"] = modelId.toString();
final APIResponse response = await InvenTreeAPI().uploadFile(
url,
attachment,
method: "POST",
name: "attachment",
fields: data,
);
return response.successful();
}
Future<bool> uploadImage(
String modelType,
int modelId, {
String prefix = "InvenTree",
}) async {
bool result = false;
await FilePickerDialog.pickImageFromCamera().then((File? file) {
if (file != null) {
String dir = path.dirname(file.path);
String ext = path.extension(file.path);
String now = DateTime.now().toIso8601String().replaceAll(":", "-");
// Rename the file with a unique name
String filename = "${dir}/${prefix}_image_${now}${ext}";
try {
return file.rename(filename).then((File renamed) {
return uploadAttachment(renamed, modelType, modelId).then((
success,
) {
result = success;
showSnackIcon(
result ? L10().imageUploadSuccess : L10().imageUploadFailure,
success: result,
);
});
});
} catch (error, stackTrace) {
sentryReportError("uploadImage", error, stackTrace);
showSnackIcon(L10().imageUploadFailure, success: false);
}
}
});
return result;
}
/*
* Download this attachment file
*/
Future<void> downloadAttachment() async {
await InvenTreeAPI().downloadFile(attachment);
}
}

View file

@ -111,31 +111,6 @@ class InvenTreeCompany extends InvenTreeModel {
InvenTreeCompany.fromJson(json);
}
/*
* Class representing an attachment file against a Company object
*/
class InvenTreeCompanyAttachment extends InvenTreeAttachment {
InvenTreeCompanyAttachment() : super();
InvenTreeCompanyAttachment.fromJson(Map<String, dynamic> json)
: super.fromJson(json);
@override
String get REFERENCE_FIELD => "company";
@override
String get REF_MODEL_TYPE => "company";
@override
String get URL => InvenTreeAPI().supportsModernAttachments
? "attachment/"
: "company/attachment/";
@override
InvenTreeModel createFromJson(Map<String, dynamic> json) =>
InvenTreeCompanyAttachment.fromJson(json);
}
/*
* The InvenTreeSupplierPart class represents the SupplierPart model in the InvenTree database
*/

View file

@ -1,5 +1,4 @@
import "dart:async";
import "dart:io";
import "package:flutter_tabler_icons/flutter_tabler_icons.dart";
import "package:flutter/material.dart";
@ -15,7 +14,6 @@ import "package:inventree/helpers.dart";
import "package:inventree/inventree/sentry.dart";
import "package:inventree/widget/dialogs.dart";
import "package:inventree/widget/fields.dart";
// Paginated response object
class InvenTreePageResponse {
@ -934,171 +932,3 @@ class InvenTreeUserSetting extends InvenTreeGlobalSetting {
@override
String get URL => "settings/user/";
}
class InvenTreeAttachment extends InvenTreeModel {
// Class representing an "attachment" file
InvenTreeAttachment() : super();
InvenTreeAttachment.fromJson(Map<String, dynamic> json)
: super.fromJson(json);
@override
String get URL => "attachment/";
@override
Map<String, Map<String, dynamic>> formFields() {
Map<String, Map<String, dynamic>> fields = {"link": {}, "comment": {}};
if (!hasLink) {
fields.remove("link");
}
return fields;
}
// Override this reference field for any subclasses
// Note: This is used for the *legacy* attachment API
String get REFERENCE_FIELD => "";
// Override this reference field for any subclasses
// Note: This is used for the *modern* attachment API
String get REF_MODEL_TYPE => "";
String get attachment => getString("attachment");
bool get hasAttachment => attachment.isNotEmpty;
// Return the filename of the attachment
String get filename {
return attachment.split("/").last;
}
IconData get icon {
String fn = filename.toLowerCase();
if (fn.endsWith(".pdf")) {
return TablerIcons.file_type_pdf;
} else if (fn.endsWith(".csv")) {
return TablerIcons.file_type_csv;
} else if (fn.endsWith(".doc") || fn.endsWith(".docx")) {
return TablerIcons.file_type_doc;
} else if (fn.endsWith(".xls") || fn.endsWith(".xlsx")) {
return TablerIcons.file_type_xls;
}
// Image formats
final List<String> img_formats = [".png", ".jpg", ".gif", ".bmp", ".svg"];
for (String fmt in img_formats) {
if (fn.endsWith(fmt)) {
return TablerIcons.file_type_jpg;
}
}
return TablerIcons.file;
}
String get comment => getString("comment");
DateTime? get uploadDate {
if (jsondata.containsKey("upload_date")) {
return DateTime.tryParse((jsondata["upload_date"] ?? "") as String);
} else {
return null;
}
}
// Return a count of how many attachments exist against the specified model ID
Future<int> countAttachments(int modelId) {
Map<String, String> filters = {};
if (InvenTreeAPI().supportsModernAttachments) {
filters["model_type"] = REF_MODEL_TYPE;
filters["model_id"] = modelId.toString();
} else {
filters[REFERENCE_FIELD] = modelId.toString();
}
return count(filters: filters);
}
Future<bool> uploadAttachment(
File attachment,
int modelId, {
String comment = "",
Map<String, String> fields = const {},
}) async {
// Ensure that the correct reference field is set
Map<String, String> data = Map<String, String>.from(fields);
String url = URL;
if (comment.isNotEmpty) {
data["comment"] = comment;
}
if (InvenTreeAPI().supportsModernAttachments) {
url = "attachment/";
data["model_id"] = modelId.toString();
data["model_type"] = REF_MODEL_TYPE;
} else {
if (REFERENCE_FIELD.isEmpty) {
sentryReportMessage(
"uploadAttachment called with empty 'REFERENCE_FIELD'",
);
return false;
}
data[REFERENCE_FIELD] = modelId.toString();
}
final APIResponse response = await InvenTreeAPI().uploadFile(
url,
attachment,
method: "POST",
name: "attachment",
fields: data,
);
return response.successful();
}
Future<bool> uploadImage(int modelId, {String prefix = "InvenTree"}) async {
bool result = false;
await FilePickerDialog.pickImageFromCamera().then((File? file) {
if (file != null) {
String dir = path.dirname(file.path);
String ext = path.extension(file.path);
String now = DateTime.now().toIso8601String().replaceAll(":", "-");
// Rename the file with a unique name
String filename = "${dir}/${prefix}_image_${now}${ext}";
try {
file.rename(filename).then((File renamed) {
uploadAttachment(renamed, modelId).then((success) {
result = success;
showSnackIcon(
result ? L10().imageUploadSuccess : L10().imageUploadFailure,
success: result,
);
});
});
} catch (error, stackTrace) {
sentryReportError("uploadImage", error, stackTrace);
showSnackIcon(L10().imageUploadFailure, success: false);
}
}
});
return result;
}
/*
* Download this attachment file
*/
Future<void> downloadAttachment() async {
await InvenTreeAPI().downloadFile(attachment);
}
}

View file

@ -547,28 +547,3 @@ class InvenTreePartPricing extends InvenTreeModel {
double? get saleHistoryMin => getDoubleOrNull("sale_history_min");
double? get saleHistoryMax => getDoubleOrNull("sale_history_max");
}
/*
* Class representing an attachment file against a Part object
*/
class InvenTreePartAttachment extends InvenTreeAttachment {
InvenTreePartAttachment() : super();
InvenTreePartAttachment.fromJson(Map<String, dynamic> json)
: super.fromJson(json);
@override
String get REFERENCE_FIELD => "part";
@override
String get REF_MODEL_TYPE => "part";
@override
String get URL => InvenTreeAPI().supportsModernAttachments
? "attachment/"
: "part/attachment/";
@override
InvenTreeModel createFromJson(Map<String, dynamic> json) =>
InvenTreePartAttachment.fromJson(json);
}

View file

@ -336,28 +336,3 @@ class InvenTreePOExtraLineItem extends InvenTreeExtraLineItem {
);
}
}
/*
* Class representing an attachment file against a PurchaseOrder object
*/
class InvenTreePurchaseOrderAttachment extends InvenTreeAttachment {
InvenTreePurchaseOrderAttachment() : super();
InvenTreePurchaseOrderAttachment.fromJson(Map<String, dynamic> json)
: super.fromJson(json);
@override
String get REFERENCE_FIELD => "order";
@override
String get REF_MODEL_TYPE => "purchaseorder";
@override
String get URL => InvenTreeAPI().supportsModernAttachments
? "attachment/"
: "order/po/attachment/";
@override
InvenTreeModel createFromJson(Map<String, dynamic> json) =>
InvenTreePurchaseOrderAttachment.fromJson(json);
}

View file

@ -334,7 +334,7 @@ class InvenTreeSalesOrderShipment extends InvenTreeModel {
/*
* Class representing an allocation of stock against a SalesOrderShipment
*/
class InvenTreeSalesOrderAllocation extends InvenTreeAttachment {
class InvenTreeSalesOrderAllocation extends InvenTreeModel {
InvenTreeSalesOrderAllocation() : super();
InvenTreeSalesOrderAllocation.fromJson(Map<String, dynamic> json)
@ -428,48 +428,3 @@ class InvenTreeSalesOrderAllocation extends InvenTreeAttachment {
}
}
}
/*
* Class representing an attachment file against a SalesOrder object
*/
class InvenTreeSalesOrderAttachment extends InvenTreeAttachment {
InvenTreeSalesOrderAttachment() : super();
InvenTreeSalesOrderAttachment.fromJson(Map<String, dynamic> json)
: super.fromJson(json);
@override
InvenTreeModel createFromJson(Map<String, dynamic> json) =>
InvenTreeSalesOrderAttachment.fromJson(json);
@override
String get REFERENCE_FIELD => "order";
@override
String get REF_MODEL_TYPE => "salesorder";
@override
String get URL => InvenTreeAPI().supportsModernAttachments
? "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/";
}

View file

@ -575,31 +575,6 @@ class InvenTreeStockItem extends InvenTreeModel {
}
}
/*
* Class representing an attachment file against a StockItem object
*/
class InvenTreeStockItemAttachment extends InvenTreeAttachment {
InvenTreeStockItemAttachment() : super();
InvenTreeStockItemAttachment.fromJson(Map<String, dynamic> json)
: super.fromJson(json);
@override
String get REFERENCE_FIELD => "stock_item";
@override
String get REF_MODEL_TYPE => "stockitem";
@override
String get URL => InvenTreeAPI().supportsModernAttachments
? "attachment/"
: "stock/attachment/";
@override
InvenTreeModel createFromJson(Map<String, dynamic> json) =>
InvenTreeStockItemAttachment.fromJson(json);
}
class InvenTreeStockLocation extends InvenTreeModel {
InvenTreeStockLocation() : super();