[Feature] Changelog / Edit History (#13069)

* Initial app scaffolding

* Skeleton for AuditLogTable

* Add UI table

* Add audit history panel to admin center

* Add auditing for models in the 'company' app

* Gate API access of user staff status

* Display item history in UI

* Track 'attachment' history

* Track 'parameter' changes

* Audit support for "common" models

* Track changed for "build" models

* Track history for "order" models:

- PurchaseOrder
- SalesOrder
- SalesOrderShipment
- ReturnOrder

Also track changes against LineItem and ExtraLineItem objects

* Register custom admin class

* Support Part history

* BomItem changes

* Fix for BulkCreateMixin

* Tweak unit tests

* Use Viewset

* Adjust error code behavior

* Setting to keep summary entries

* Refactor helper for middlewares

* Tweak unit tests

* Fixes for ModelInformationDict

* Track pricing changes

* Add model type filter for auditlog table

* Specify filter field type

* Hide HistoryPanel if auding is disabled

* add docs

* Add "--summary" option to export_records task

* Playwright tests

* Track task responsible for data changes

* Updated unit tests

* record changes to global settings

* Update docs

* Ensure values are stringified

* Fix for globalsetting

* Patch unit test

* Tweak playwright tests

* playwright test fix

* use main branch for demo data

* Update CHANGELOG
This commit is contained in:
Oliver authored and GitHub committed 2026-10-25 12:36:13 +11:00
1 parent cdb3b1ecf7
commit c475e9bfcf
91 files changed
+7753 -102

No files matched your search

+40
View File
@@ -1206,9 +1206,11 @@ def update(
'exclude_plugins': 'Exclude plugin data from the output file (default = False)',
'include_sso': 'Include SSO token data in the output file (default = False)',
'include_session': 'Include user session data in the output file (default = False)',
'include_remote_addr': 'Include remote IP addresses for audit log entries in the output file (default = False)',
'prettify': 'Pretty-print the output file with indentation (default = False)',
'bulk': 'Use bulkdumpdata for improved performance on large datasets (default = False)',
'verbose': 'Print verbose output from management commands',
'summary': 'Write a summary of the exported records (record counts and metadata) to the specified JSON file',
}
)
@state_logger
@@ -1222,9 +1224,11 @@ def export_records(
exclude_plugins: bool = False,
include_sso: bool = False,
include_session: bool = False,
include_remote_addr: bool = False,
prettify: bool = False,
bulk: bool = False,
verbose: bool = False,
summary: Optional[str] = None,
):
"""Export all database records to a file."""
# Get an absolute path to the file
@@ -1232,12 +1236,22 @@ def export_records(
if not target.is_absolute():
target = local_dir().joinpath(filename).resolve()
summary_target = None
if summary:
summary_target = Path(summary)
if not summary_target.is_absolute():
summary_target = local_dir().joinpath(summary).resolve()
info(f"Exporting database records to file '{target}'")
wait(c, verbose=verbose)
check_file_existence(target, overwrite)
if summary_target:
check_file_existence(summary_target, overwrite)
excludes = content_excludes(
allow_email=include_email,
allow_tokens=include_tokens,
@@ -1298,10 +1312,17 @@ def export_records(
if model_name == 'auth.user':
entry['fields']['user_permissions'] = []
# Remove remote IP addresses from audit log entries
if include_remote_addr is False and model_name == 'audit.auditlogentry':
entry['fields']['remote_addr'] = None
yield entry
indent = 2 if prettify else None
# Number of exported records, for each model type
record_counts: dict[str, int] = {}
# Write the processed data to file, one entry at a time - avoids ever
# holding a second full copy of the (potentially huge) dataset in memory,
# and avoids a single json.dumps() call across the entire dataset at once
@@ -1313,10 +1334,25 @@ def export_records(
if prettify:
f_out.write('\n')
f_out.write(json.dumps(entry, indent=indent))
if model_name := entry.get('model'):
record_counts[model_name] = record_counts.get(model_name, 0) + 1
if prettify:
f_out.write('\n')
f_out.write(']')
if summary_target:
info(f"Writing export summary to file '{summary_target}'")
summary_data = {
'metadata': metadata_entry,
'total_records': sum(record_counts.values()),
'records': dict(sorted(record_counts.items())),
}
with open(summary_target, 'w', encoding='utf-8') as f_out:
json.dump(summary_data, f_out, indent=2)
success('Data export completed')
@@ -2126,6 +2162,7 @@ def export_definitions(c, basedir: str = ''):
base_path.joinpath('inventree_report_context.json'),
base_path.joinpath('inventree_status_codes.json'),
base_path.joinpath('inventree_roles.json'),
base_path.joinpath('inventree_audited_models.json'),
]
info('Exporting definitions...')
@@ -2146,6 +2183,9 @@ def export_definitions(c, basedir: str = ''):
check_file_existence(filenames[5], overwrite=True)
manage(c, f'export_roles {filenames[5]}', pty=True)
check_file_existence(filenames[6], overwrite=True)
manage(c, f'export_audited_models {filenames[6]}', pty=True)
info('Exporting definitions complete')