Fixhancement: include trashed documents in document exporter/importer (#12425)

This commit is contained in:
Jan Kleine
2026-03-30 18:30:22 +02:00
committed by GitHub
parent 5b755528da
commit 0292edbee7
3 changed files with 58 additions and 9 deletions
@@ -385,10 +385,10 @@ class Command(CryptMixin, PaperlessCommand):
"workflow_webhook_actions": WorkflowActionWebhook.objects.all(),
"workflows": Workflow.objects.all(),
"custom_fields": CustomField.objects.all(),
"custom_field_instances": CustomFieldInstance.objects.all(),
"custom_field_instances": CustomFieldInstance.global_objects.all(),
"app_configs": ApplicationConfiguration.objects.all(),
"notes": Note.objects.all(),
"documents": Document.objects.order_by("id").all(),
"notes": Note.global_objects.all(),
"documents": Document.global_objects.order_by("id").all(),
"social_accounts": SocialAccount.objects.all(),
"social_apps": SocialApp.objects.all(),
"social_tokens": SocialToken.objects.all(),
@@ -443,7 +443,7 @@ class Command(CryptMixin, PaperlessCommand):
writer.write_batch(batch)
document_map: dict[int, Document] = {
d.pk: d for d in Document.objects.order_by("id")
d.pk: d for d in Document.global_objects.order_by("id")
}
# 3. Export files from each document
@@ -619,12 +619,15 @@ class Command(CryptMixin, PaperlessCommand):
"""Write per-document manifest file for --split-manifest mode."""
content = [document_dict]
content.extend(
serializers.serialize("python", Note.objects.filter(document=document)),
serializers.serialize(
"python",
Note.global_objects.filter(document=document),
),
)
content.extend(
serializers.serialize(
"python",
CustomFieldInstance.objects.filter(document=document),
CustomFieldInstance.global_objects.filter(document=document),
),
)
manifest_name = base_name.with_name(f"{base_name.stem}-manifest.json")
@@ -125,7 +125,7 @@ class Command(CryptMixin, PaperlessCommand):
"Found existing user(s), this might indicate a non-empty installation",
),
)
if Document.objects.count() != 0:
if Document.global_objects.count() != 0:
self.stdout.write(
self.style.WARNING(
"Found existing documents(s), this might indicate a non-empty installation",
@@ -376,7 +376,7 @@ class Command(CryptMixin, PaperlessCommand):
]
for record in self.track(document_records, description="Copying files..."):
document = Document.objects.get(pk=record["pk"])
document = Document.global_objects.get(pk=record["pk"])
doc_file = record[EXPORTER_FILE_NAME]
document_path = self.source / doc_file
@@ -389,7 +389,7 @@ class TestExportImport(
self.assertIsFile(
str(self.target / doc_from_manifest[EXPORTER_FILE_NAME]),
)
self.d3.delete()
self.d3.hard_delete()
manifest = self._do_export()
self.assertRaises(
@@ -868,6 +868,52 @@ class TestExportImport(
for obj in manifest:
self.assertNotEqual(obj["model"], "auditlog.logentry")
def test_export_import_soft_deleted_document(self) -> None:
"""
GIVEN:
- A document with a note and custom field instance has been soft-deleted
WHEN:
- Export and re-import are performed
THEN:
- The soft-deleted document, note, and custom field instance
survive the round-trip with deleted_at preserved
"""
shutil.rmtree(Path(self.dirs.media_dir) / "documents")
shutil.copytree(
Path(__file__).parent / "samples" / "documents",
Path(self.dirs.media_dir) / "documents",
)
# d1 has self.note and self.cfi1 attached via setUp
self.d1.delete()
self._do_export()
with paperless_environment():
Document.global_objects.all().hard_delete()
Correspondent.objects.all().delete()
DocumentType.objects.all().delete()
Tag.objects.all().delete()
call_command(
"document_importer",
"--no-progress-bar",
self.target,
skip_checks=True,
)
self.assertEqual(Document.global_objects.count(), 4)
reimported_doc = Document.global_objects.get(pk=self.d1.pk)
self.assertIsNotNone(reimported_doc.deleted_at)
self.assertEqual(Note.global_objects.count(), 1)
reimported_note = Note.global_objects.get(pk=self.note.pk)
self.assertIsNotNone(reimported_note.deleted_at)
self.assertEqual(CustomFieldInstance.global_objects.count(), 1)
reimported_cfi = CustomFieldInstance.global_objects.get(pk=self.cfi1.pk)
self.assertIsNotNone(reimported_cfi.deleted_at)
def test_export_data_only(self) -> None:
"""
GIVEN: