Skip to content

Commit c8c7020

Browse files
rerowepPascalRepond
andcommitted
feat(documents): allow deletion when acq order lines are terminal
* Closes rero#3225. * Add module-level constant `ACQ_ORDER_LINE_BLOCKING_STATUSES` (APPROVED, ORDERED, PARTIALLY_RECEIVED) in `documents/api.py` to avoid reallocating the list on every call to `get_links_to_me()`. * Filter the `acq_order_lines` link count by these blocking statuses so that order lines in a terminal state (RECEIVED, CANCELLED) no longer prevent document deletion. * Guard the receipt-line ES dumper against a missing document: when the document has already been deleted, fall back to the pid extracted from the `$ref` instead of raising an error. * Guard the order-line ES dumper the same way, for consistency. * Override `replace_refs()` in `AcqOrderLine` to resolve the document reference manually before delegating to the parent: if the document has been deleted the lazy `JsonRef` proxy would raise a `JsonRefError` during `deepcopy` inside the base dumper, making any edit to a receipt-line (or other resource triggering order-line re-indexation) fail. Follows the same pattern already used in `Template.replace_refs()`. * Extend the reception-workflow test with `get_links_to_me()` assertions at each stage of the workflow, and verify that once all order lines are terminal the document reports no blocking links and no reasons not to delete. Co-Authored-by: Peter Weber <peter.weber@rero.ch> Co-Authored-by: Pascal Repond <pascal.repond@rero.ch>
1 parent d29392b commit c8c7020

File tree

5 files changed

+75
-22
lines changed

5 files changed

+75
-22
lines changed

rero_ils/modules/acquisition/acq_order_lines/api.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# -*- coding: utf-8 -*-
22
#
33
# RERO ILS
4-
# Copyright (C) 2019-2022 RERO
4+
# Copyright (C) 2019-2026 RERO
55
# Copyright (C) 2019-2022 UCLouvain
66
#
77
# This program is free software: you can redistribute it and/or modify
@@ -143,6 +143,29 @@ def _build_total_amount(cls, data):
143143
"""Build total amount for order line."""
144144
data["total_amount"] = data["amount"] * data["quantity"]
145145

146+
def replace_refs(self):
147+
"""Replace $ref with real data, handling deleted documents gracefully.
148+
149+
The document linked to an order line may have been deleted (allowed
150+
when all order lines reach a terminal status). To avoid a
151+
``JsonRefError`` during indexing, we resolve the document ref manually
152+
before delegating to the parent, then restore the original ref.
153+
"""
154+
doc_ref = self.pop("document", None)
155+
try:
156+
data = super().replace_refs()
157+
finally:
158+
if doc_ref:
159+
self["document"] = doc_ref
160+
if doc_ref:
161+
document = extracted_data_from_ref(doc_ref, data="record")
162+
if document:
163+
data["document"] = dict(document)
164+
else:
165+
doc_pid = extracted_data_from_ref(doc_ref)
166+
data["document"] = {"pid": doc_pid}
167+
return data
168+
146169
# GETTER & SETTER =========================================================
147170
# * Define some properties as shortcut to quickly access object attrs.
148171
# * Defines some getter methods to access specific object values.

rero_ils/modules/acquisition/acq_order_lines/dumpers.py

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# -*- coding: utf-8 -*-
22
#
33
# RERO ILS
4-
# Copyright (C) 2019-2022 RERO
4+
# Copyright (C) 2019-2026 RERO
55
# Copyright (C) 2019-2022 UCLouvain
66
#
77
# This program is free software: you can redistribute it and/or modify
@@ -59,15 +59,18 @@ def dump(self, record, data):
5959
# Add document information's: pid, formatted title and ISBN
6060
# identifiers (remove None values from document metadata)
6161
document = record.document
62-
identifiers = document.get_identifiers(filters=[IdentifierType.ISBN], with_alternatives=True)
63-
identifiers = [identifier.normalize() for identifier in identifiers]
64-
65-
data["document"] = {
66-
"pid": document.pid,
67-
"title": TitleExtension.format_text(document.get("title", [])),
68-
"identifiers": identifiers,
69-
}
70-
data["document"] = {k: v for k, v in data["document"].items() if v}
62+
if document:
63+
identifiers = document.get_identifiers(filters=[IdentifierType.ISBN], with_alternatives=True)
64+
identifiers = [identifier.normalize() for identifier in identifiers]
65+
data["document"] = {
66+
"pid": document.pid,
67+
"title": TitleExtension.format_text(document.get("title", [])),
68+
"identifiers": identifiers,
69+
}
70+
data["document"] = {k: v for k, v in data["document"].items() if v}
71+
else:
72+
# Document has been deleted; keep only the pid extracted from the $ref
73+
data["document"] = {"pid": record.document_pid}
7174
return data
7275

7376

rero_ils/modules/acquisition/acq_receipt_lines/dumpers.py

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,16 @@ def dump(self, record, data):
5050
# Add document information's: pid, formatted title and ISBN identifiers
5151
# (remove None values from document metadata)
5252
document = order_line.document
53-
identifiers = document.get_identifiers(filters=[IdentifierType.ISBN], with_alternatives=True)
54-
identifiers = [identifier.normalize() for identifier in identifiers]
55-
data["document"] = {
56-
"pid": document.pid,
57-
"title": TitleExtension.format_text(document.get("title", [])),
58-
"identifiers": identifiers,
59-
}
60-
data["document"] = {k: v for k, v in data["document"].items() if v}
53+
if document:
54+
identifiers = document.get_identifiers(filters=[IdentifierType.ISBN], with_alternatives=True)
55+
identifiers = [identifier.normalize() for identifier in identifiers]
56+
data["document"] = {
57+
"pid": document.pid,
58+
"title": TitleExtension.format_text(document.get("title", [])),
59+
"identifiers": identifiers,
60+
}
61+
data["document"] = {k: v for k, v in data["document"].items() if v}
62+
else:
63+
# Document has been deleted; keep only the pid extracted from the $ref
64+
data["document"] = {"pid": order_line.document_pid}
6165
return data

rero_ils/modules/documents/api.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# -*- coding: utf-8 -*-
22
#
33
# RERO ILS
4-
# Copyright (C) 2019-2023 RERO
4+
# Copyright (C) 2019-2026 RERO
55
# Copyright (C) 2019-2023 UCLouvain
66
#
77
# This program is free software: you can redistribute it and/or modify
@@ -29,6 +29,7 @@
2929
from jsonschema.exceptions import ValidationError
3030

3131
from rero_ils.modules.acquisition.acq_order_lines.api import AcqOrderLinesSearch
32+
from rero_ils.modules.acquisition.acq_order_lines.models import AcqOrderLineStatus
3233
from rero_ils.modules.api import IlsRecord, IlsRecordsIndexer, IlsRecordsSearch
3334
from rero_ils.modules.commons.identifiers import IdentifierFactory, IdentifierType
3435
from rero_ils.modules.documents.tasks import reindex_document_items
@@ -57,6 +58,12 @@
5758
# fetcher
5859
document_id_fetcher = partial(id_fetcher, provider=DocumentProvider)
5960

61+
ACQ_ORDER_LINE_BLOCKING_STATUSES = [
62+
AcqOrderLineStatus.APPROVED,
63+
AcqOrderLineStatus.ORDERED,
64+
AcqOrderLineStatus.PARTIALLY_RECEIVED,
65+
]
66+
6067

6168
class DocumentsSearch(IlsRecordsSearch):
6269
"""DocumentsSearch."""
@@ -313,8 +320,14 @@ def get_links_to_me(self, get_pids=False):
313320
exclude_states=[LoanState.CANCELLED, LoanState.ITEM_RETURNED],
314321
)
315322
file_query = self.get_records_files_query().source()
316-
acq_order_lines_query = AcqOrderLinesSearch().filter("term", document__pid=self.pid)
323+
324+
acq_order_lines_query = (
325+
AcqOrderLinesSearch()
326+
.filter("term", document__pid=self.pid)
327+
.filter("terms", status=ACQ_ORDER_LINE_BLOCKING_STATUSES)
328+
)
317329
local_fields_query = LocalFieldsSearch().get_local_fields(self.provider.pid_type, self.pid)
330+
318331
relation_types = {
319332
"partOf": "partOf.document.pid",
320333
"supplement": "supplement.pid",

tests/api/acquisition/test_acquisition_reception_workflow.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# -*- coding: utf-8 -*-
22
#
33
# RERO ILS
4-
# Copyright (C) 2021 RERO
4+
# Copyright (C) 2021-2026 RERO
55
#
66
# This program is free software: you can redistribute it and/or modify
77
# it under the terms of the GNU Affero General Public License as published by
@@ -64,6 +64,7 @@ def test_acquisition_reception_workflow(
6464
document,
6565
):
6666
"""Test complete acquisition workflow."""
67+
assert "acq_order_lines" not in document.get_links_to_me()
6768

6869
def assert_account_data(accounts):
6970
"""Assert account informations."""
@@ -286,6 +287,8 @@ def assert_account_data(accounts):
286287
}
287288
assert_account_data(manual_controls)
288289

290+
assert document.get_links_to_me() == {"acq_order_lines": 6}
291+
289292
# STEP 3 :: UPDATE ORDER LINES
290293
# * Cancel some order lines and change some quantities --> make sure
291294
# calculations still good
@@ -363,6 +366,8 @@ def assert_account_data(accounts):
363366
assert order_line_1.unreceived_quantity == 5
364367
assert order_line_1.status == AcqOrderLineStatus.APPROVED
365368

369+
assert document.get_links_to_me() == {"acq_order_lines": 4}
370+
366371
# STEP 4 :: SEND THE ORDER
367372
# * Test send order and make sure statuses are up to date.
368373
# - check order lines (status, order-date)
@@ -611,6 +616,11 @@ def assert_account_data(accounts):
611616
}
612617
assert_account_data(manual_controls)
613618

619+
# All order lines are now RECEIVED or CANCELLED (no blocking statuses remain),
620+
# so the document should have no links preventing deletion.
621+
assert "acq_order_lines" not in document.get_links_to_me()
622+
assert document.reasons_not_to_delete() == {}
623+
614624
# TEST 8: DELETE RECEIPTS
615625
# * Delete the second receipt. This will also delete the related receipt
616626
# lines. The order status must remain to PARTIALLY_RECEIVED.

0 commit comments

Comments
 (0)