Skip to content

Commit b02ecd7

Browse files
Delete documents from Paperless when removed locally
- Add delete_from_paperless function to paperless_service.py - Search by document ID or title to find and delete - Update attachment delete route to remove from Paperless - Update plan year document delete route to remove from Paperless - Gracefully handle cases where document not found in Paperless 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent 93e4561 commit b02ecd7

File tree

3 files changed

+169
-4
lines changed

3 files changed

+169
-4
lines changed

app/routes/attachments.py

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -170,23 +170,55 @@ def add_attachments(expense_id):
170170
def delete_attachment(attachment_id):
171171
"""API endpoint to delete an attachment"""
172172
user_id = session['user_id']
173+
paperless_info = None
173174

174175
with db_connection() as conn:
175-
# Get attachment file path and verify ownership
176+
# Get attachment file path, Paperless info, and verify ownership
176177
attachment = conn.execute('''
177-
SELECT a.file_path FROM attachments a
178+
SELECT a.file_path, a.paperless_doc_id, a.paperless_sync_status,
179+
e.date_of_service, e.category, p.name as provider_name
180+
FROM attachments a
178181
INNER JOIN expenses e ON a.expense_id = e.id
182+
LEFT JOIN providers p ON e.provider_id = p.id
179183
WHERE a.id = ? AND e.user_id = ?
180184
''', (attachment_id, user_id)).fetchone()
181185

182186
if not attachment:
183187
return jsonify({'error': 'Attachment not found or access denied'}), 404
184188

189+
# Store Paperless info before deletion
190+
if attachment['paperless_sync_status'] == 'synced':
191+
paperless_info = {
192+
'doc_id': attachment['paperless_doc_id'],
193+
'date': attachment['date_of_service'],
194+
'category': attachment['category'],
195+
'provider': attachment['provider_name'] or ''
196+
}
197+
185198
# Delete file from filesystem
186199
resolved_path = resolve_attachment_path(attachment['file_path'])
187200
delete_file_safely(resolved_path)
188201

189202
# Delete from database
190203
conn.execute('DELETE FROM attachments WHERE id = ?', (attachment_id,))
191204

205+
# Delete from Paperless (after DB transaction commits)
206+
if paperless_info:
207+
try:
208+
from services.paperless_service import delete_from_paperless
209+
# Build title to search for if no doc_id
210+
title = f"HSA-{paperless_info['date']}"
211+
if paperless_info['provider']:
212+
title += f"-{paperless_info['provider']}"
213+
title += f"-{paperless_info['category']}"
214+
215+
delete_from_paperless(
216+
user_id=user_id,
217+
paperless_doc_id=paperless_info['doc_id'],
218+
title=title
219+
)
220+
except Exception as e:
221+
import logging
222+
logging.getLogger(__name__).warning(f"Paperless delete failed: {e}")
223+
192224
return jsonify({'success': True})

app/routes/plan_year_documents.py

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,22 +160,48 @@ def add_documents(plan_year_id):
160160
def delete_document(document_id):
161161
"""API endpoint to delete a policy document"""
162162
user_id = session['user_id']
163+
paperless_info = None
163164

164165
with db_connection() as conn:
165-
# Get document file path and verify ownership
166+
# Get document file path, Paperless info, and verify ownership
166167
document = conn.execute('''
167-
SELECT d.file_path FROM plan_year_documents d
168+
SELECT d.file_path, d.paperless_doc_id, d.paperless_sync_status,
169+
p.plan_year_start
170+
FROM plan_year_documents d
168171
INNER JOIN plan_years p ON d.plan_year_id = p.id
169172
WHERE d.id = ? AND p.user_id = ?
170173
''', (document_id, user_id)).fetchone()
171174

172175
if not document:
173176
return jsonify({'error': 'Document not found or access denied'}), 404
174177

178+
# Store Paperless info before deletion
179+
if document['paperless_sync_status'] == 'synced':
180+
paperless_info = {
181+
'doc_id': document['paperless_doc_id'],
182+
'date': document['plan_year_start']
183+
}
184+
175185
# Delete file from filesystem
176186
delete_file_safely(document['file_path'])
177187

178188
# Delete from database
179189
conn.execute('DELETE FROM plan_year_documents WHERE id = ?', (document_id,))
180190

191+
# Delete from Paperless (after DB transaction commits)
192+
if paperless_info:
193+
try:
194+
from services.paperless_service import delete_from_paperless
195+
# Build title to search for if no doc_id
196+
title = f"HSA-Policy-{paperless_info['date']}"
197+
198+
delete_from_paperless(
199+
user_id=user_id,
200+
paperless_doc_id=paperless_info['doc_id'],
201+
title=title
202+
)
203+
except Exception as e:
204+
import logging
205+
logging.getLogger(__name__).warning(f"Paperless delete failed: {e}")
206+
181207
return jsonify({'success': True})

app/services/paperless_service.py

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,113 @@ def get_or_create_correspondent(url: str, token: str, name: str) -> Optional[int
199199
return None
200200

201201

202+
def find_document_by_title(url: str, token: str, title: str) -> Optional[int]:
203+
"""Find a document by its title.
204+
205+
Args:
206+
url: Paperless base URL
207+
token: API token
208+
title: Document title to search for
209+
210+
Returns:
211+
Document ID if found, None otherwise
212+
"""
213+
headers = {'Authorization': f'Token {token}'}
214+
215+
try:
216+
response = requests.get(
217+
f"{url}/api/documents/",
218+
headers=headers,
219+
params={'query': f'title:"{title}"'},
220+
timeout=PAPERLESS_TIMEOUT
221+
)
222+
223+
if response.status_code == 200:
224+
data = response.json()
225+
results = data.get('results', [])
226+
# Find exact match
227+
for doc in results:
228+
if doc.get('title') == title:
229+
return doc.get('id')
230+
# Fallback to first result if close match
231+
if results:
232+
return results[0].get('id')
233+
234+
except Exception as e:
235+
logger.error(f"Error searching for document '{title}': {e}")
236+
237+
return None
238+
239+
240+
def delete_document(url: str, token: str, doc_id: int) -> tuple[bool, str]:
241+
"""Delete a document from Paperless-ngx.
242+
243+
Args:
244+
url: Paperless base URL
245+
token: API token
246+
doc_id: Document ID to delete
247+
248+
Returns:
249+
Tuple of (success, message)
250+
"""
251+
headers = {'Authorization': f'Token {token}'}
252+
253+
try:
254+
response = requests.delete(
255+
f"{url}/api/documents/{doc_id}/",
256+
headers=headers,
257+
timeout=PAPERLESS_TIMEOUT
258+
)
259+
260+
if response.status_code == 204:
261+
return True, "Document deleted"
262+
elif response.status_code == 404:
263+
return True, "Document not found (may have been deleted)"
264+
else:
265+
return False, f"Delete failed: {response.status_code}"
266+
267+
except requests.exceptions.Timeout:
268+
return False, "Request timed out"
269+
except requests.exceptions.ConnectionError:
270+
return False, "Could not connect to server"
271+
except Exception as e:
272+
logger.error(f"Paperless delete error: {e}")
273+
return False, f"Error: {str(e)}"
274+
275+
276+
def delete_from_paperless(
277+
user_id: int,
278+
paperless_doc_id: Optional[int] = None,
279+
title: Optional[str] = None
280+
) -> tuple[bool, str]:
281+
"""Delete a document from Paperless-ngx.
282+
283+
Args:
284+
user_id: User ID
285+
paperless_doc_id: Known Paperless document ID (preferred)
286+
title: Document title to search for (fallback)
287+
288+
Returns:
289+
Tuple of (success, message)
290+
"""
291+
config = get_user_paperless_config(user_id)
292+
if not config:
293+
return True, "Paperless not configured" # Not an error
294+
295+
# If we have a document ID, use it directly
296+
if paperless_doc_id:
297+
return delete_document(config['url'], config['token'], paperless_doc_id)
298+
299+
# Otherwise, try to find by title
300+
if title:
301+
doc_id = find_document_by_title(config['url'], config['token'], title)
302+
if doc_id:
303+
return delete_document(config['url'], config['token'], doc_id)
304+
return True, "Document not found in Paperless" # Not necessarily an error
305+
306+
return True, "No document ID or title provided"
307+
308+
202309
def sync_document(
203310
url: str,
204311
token: str,

0 commit comments

Comments
 (0)