folio-migration-tools 1.2.1__py3-none-any.whl → 1.9.10__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- folio_migration_tools/__init__.py +11 -0
- folio_migration_tools/__main__.py +169 -85
- folio_migration_tools/circulation_helper.py +96 -59
- folio_migration_tools/config_file_load.py +66 -0
- folio_migration_tools/custom_dict.py +6 -4
- folio_migration_tools/custom_exceptions.py +21 -19
- folio_migration_tools/extradata_writer.py +46 -0
- folio_migration_tools/folder_structure.py +63 -66
- folio_migration_tools/helper.py +29 -21
- folio_migration_tools/holdings_helper.py +57 -34
- folio_migration_tools/i18n_config.py +9 -0
- folio_migration_tools/library_configuration.py +173 -13
- folio_migration_tools/mapper_base.py +317 -106
- folio_migration_tools/mapping_file_transformation/courses_mapper.py +203 -0
- folio_migration_tools/mapping_file_transformation/holdings_mapper.py +83 -69
- folio_migration_tools/mapping_file_transformation/item_mapper.py +98 -94
- folio_migration_tools/mapping_file_transformation/manual_fee_fines_mapper.py +352 -0
- folio_migration_tools/mapping_file_transformation/mapping_file_mapper_base.py +702 -223
- folio_migration_tools/mapping_file_transformation/notes_mapper.py +90 -0
- folio_migration_tools/mapping_file_transformation/order_mapper.py +492 -0
- folio_migration_tools/mapping_file_transformation/organization_mapper.py +389 -0
- folio_migration_tools/mapping_file_transformation/ref_data_mapping.py +38 -27
- folio_migration_tools/mapping_file_transformation/user_mapper.py +149 -361
- folio_migration_tools/marc_rules_transformation/conditions.py +650 -246
- folio_migration_tools/marc_rules_transformation/holdings_statementsparser.py +292 -130
- folio_migration_tools/marc_rules_transformation/hrid_handler.py +244 -0
- folio_migration_tools/marc_rules_transformation/loc_language_codes.xml +20846 -0
- folio_migration_tools/marc_rules_transformation/marc_file_processor.py +300 -0
- folio_migration_tools/marc_rules_transformation/marc_reader_wrapper.py +136 -0
- folio_migration_tools/marc_rules_transformation/rules_mapper_authorities.py +241 -0
- folio_migration_tools/marc_rules_transformation/rules_mapper_base.py +681 -201
- folio_migration_tools/marc_rules_transformation/rules_mapper_bibs.py +395 -429
- folio_migration_tools/marc_rules_transformation/rules_mapper_holdings.py +531 -100
- folio_migration_tools/migration_report.py +85 -38
- folio_migration_tools/migration_tasks/__init__.py +1 -3
- folio_migration_tools/migration_tasks/authority_transformer.py +119 -0
- folio_migration_tools/migration_tasks/batch_poster.py +911 -198
- folio_migration_tools/migration_tasks/bibs_transformer.py +121 -116
- folio_migration_tools/migration_tasks/courses_migrator.py +192 -0
- folio_migration_tools/migration_tasks/holdings_csv_transformer.py +252 -247
- folio_migration_tools/migration_tasks/holdings_marc_transformer.py +321 -115
- folio_migration_tools/migration_tasks/items_transformer.py +264 -84
- folio_migration_tools/migration_tasks/loans_migrator.py +506 -195
- folio_migration_tools/migration_tasks/manual_fee_fines_transformer.py +187 -0
- folio_migration_tools/migration_tasks/migration_task_base.py +364 -74
- folio_migration_tools/migration_tasks/orders_transformer.py +373 -0
- folio_migration_tools/migration_tasks/organization_transformer.py +451 -0
- folio_migration_tools/migration_tasks/requests_migrator.py +130 -62
- folio_migration_tools/migration_tasks/reserves_migrator.py +253 -0
- folio_migration_tools/migration_tasks/user_transformer.py +180 -139
- folio_migration_tools/task_configuration.py +46 -0
- folio_migration_tools/test_infrastructure/__init__.py +0 -0
- folio_migration_tools/test_infrastructure/mocked_classes.py +406 -0
- folio_migration_tools/transaction_migration/legacy_loan.py +148 -34
- folio_migration_tools/transaction_migration/legacy_request.py +65 -25
- folio_migration_tools/transaction_migration/legacy_reserve.py +47 -0
- folio_migration_tools/transaction_migration/transaction_result.py +12 -1
- folio_migration_tools/translations/en.json +476 -0
- folio_migration_tools-1.9.10.dist-info/METADATA +169 -0
- folio_migration_tools-1.9.10.dist-info/RECORD +67 -0
- {folio_migration_tools-1.2.1.dist-info → folio_migration_tools-1.9.10.dist-info}/WHEEL +1 -2
- folio_migration_tools-1.9.10.dist-info/entry_points.txt +3 -0
- folio_migration_tools/generate_schemas.py +0 -46
- folio_migration_tools/mapping_file_transformation/mapping_file_mapping_base_impl.py +0 -44
- folio_migration_tools/mapping_file_transformation/user_mapper_base.py +0 -212
- folio_migration_tools/marc_rules_transformation/bibs_processor.py +0 -163
- folio_migration_tools/marc_rules_transformation/holdings_processor.py +0 -284
- folio_migration_tools/report_blurbs.py +0 -219
- folio_migration_tools/transaction_migration/legacy_fee_fine.py +0 -36
- folio_migration_tools-1.2.1.dist-info/METADATA +0 -134
- folio_migration_tools-1.2.1.dist-info/RECORD +0 -50
- folio_migration_tools-1.2.1.dist-info/top_level.txt +0 -1
- {folio_migration_tools-1.2.1.dist-info → folio_migration_tools-1.9.10.dist-info/licenses}/LICENSE +0 -0
|
@@ -1,30 +1,34 @@
|
|
|
1
1
|
import copy
|
|
2
2
|
import csv
|
|
3
|
-
from datetime import datetime, timedelta
|
|
4
3
|
import json
|
|
5
|
-
import sys
|
|
6
|
-
from dateutil import parser as du_parser
|
|
7
|
-
import requests
|
|
8
4
|
import logging
|
|
5
|
+
import sys
|
|
9
6
|
import time
|
|
10
7
|
import traceback
|
|
8
|
+
from datetime import datetime, timedelta
|
|
9
|
+
from typing import Annotated, List, Literal, Optional
|
|
11
10
|
from urllib.error import HTTPError
|
|
12
|
-
from
|
|
13
|
-
from pydantic import
|
|
14
|
-
|
|
11
|
+
from zoneinfo import ZoneInfo
|
|
12
|
+
from pydantic import Field
|
|
13
|
+
|
|
14
|
+
import i18n
|
|
15
|
+
from dateutil import parser as du_parser
|
|
15
16
|
from folio_uuid.folio_namespaces import FOLIONamespaces
|
|
17
|
+
from art import tprint
|
|
18
|
+
|
|
16
19
|
from folio_migration_tools.circulation_helper import CirculationHelper
|
|
17
|
-
from folio_migration_tools.
|
|
20
|
+
from folio_migration_tools.custom_exceptions import TransformationRecordFailedError
|
|
21
|
+
from folio_migration_tools.helper import Helper
|
|
18
22
|
from folio_migration_tools.library_configuration import (
|
|
19
23
|
FileDefinition,
|
|
20
24
|
LibraryConfiguration,
|
|
21
25
|
)
|
|
26
|
+
from folio_migration_tools.mapping_file_transformation.mapping_file_mapper_base import (
|
|
27
|
+
MappingFileMapperBase,
|
|
28
|
+
)
|
|
22
29
|
from folio_migration_tools.migration_report import MigrationReport
|
|
23
30
|
from folio_migration_tools.migration_tasks.migration_task_base import MigrationTaskBase
|
|
24
|
-
|
|
25
|
-
from typing import Optional
|
|
26
|
-
from folio_migration_tools.report_blurbs import Blurbs
|
|
27
|
-
|
|
31
|
+
from folio_migration_tools.task_configuration import AbstractTaskConfiguration
|
|
28
32
|
from folio_migration_tools.transaction_migration.legacy_loan import LegacyLoan
|
|
29
33
|
from folio_migration_tools.transaction_migration.transaction_result import (
|
|
30
34
|
TransactionResult,
|
|
@@ -32,15 +36,60 @@ from folio_migration_tools.transaction_migration.transaction_result import (
|
|
|
32
36
|
|
|
33
37
|
|
|
34
38
|
class LoansMigrator(MigrationTaskBase):
|
|
35
|
-
class TaskConfiguration(
|
|
36
|
-
name:
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
39
|
+
class TaskConfiguration(AbstractTaskConfiguration):
|
|
40
|
+
name: Annotated[
|
|
41
|
+
str,
|
|
42
|
+
Field(
|
|
43
|
+
title="Task name",
|
|
44
|
+
description="The name of the task.",
|
|
45
|
+
),
|
|
46
|
+
]
|
|
47
|
+
migration_task_type: Annotated[
|
|
48
|
+
str,
|
|
49
|
+
Field(
|
|
50
|
+
title="Migration task type",
|
|
51
|
+
description="The type of the migration task.",
|
|
52
|
+
),
|
|
53
|
+
]
|
|
54
|
+
open_loans_files: Annotated[
|
|
55
|
+
Optional[list[FileDefinition]],
|
|
56
|
+
Field(
|
|
57
|
+
title="Open loans files",
|
|
58
|
+
description="List of files containing open loan data.",
|
|
59
|
+
),
|
|
60
|
+
]
|
|
61
|
+
fallback_service_point_id: Annotated[
|
|
62
|
+
str,
|
|
63
|
+
Field(
|
|
64
|
+
title="Fallback service point ID",
|
|
65
|
+
description="Identifier of the fallback service point.",
|
|
66
|
+
),
|
|
67
|
+
]
|
|
68
|
+
starting_row: Annotated[
|
|
69
|
+
Optional[int],
|
|
70
|
+
Field(
|
|
71
|
+
title="Starting row",
|
|
72
|
+
description=("The starting row for data processing. By default is 1."),
|
|
73
|
+
),
|
|
74
|
+
] = 1
|
|
75
|
+
item_files: Annotated[
|
|
76
|
+
Optional[list[FileDefinition]],
|
|
77
|
+
Field(
|
|
78
|
+
title="Item files",
|
|
79
|
+
description=(
|
|
80
|
+
"List of files containing item data. By default is empty list."
|
|
81
|
+
),
|
|
82
|
+
),
|
|
83
|
+
] = []
|
|
84
|
+
patron_files: Annotated[
|
|
85
|
+
Optional[list[FileDefinition]],
|
|
86
|
+
Field(
|
|
87
|
+
title="Patron files",
|
|
88
|
+
description=(
|
|
89
|
+
"List of files containing patron data. By default is empty list."
|
|
90
|
+
),
|
|
91
|
+
),
|
|
92
|
+
] = []
|
|
44
93
|
|
|
45
94
|
@staticmethod
|
|
46
95
|
def get_object_type() -> FOLIONamespaces:
|
|
@@ -50,31 +99,79 @@ class LoansMigrator(MigrationTaskBase):
|
|
|
50
99
|
self,
|
|
51
100
|
task_configuration: TaskConfiguration,
|
|
52
101
|
library_config: LibraryConfiguration,
|
|
102
|
+
folio_client,
|
|
53
103
|
):
|
|
54
104
|
csv.register_dialect("tsv", delimiter="\t")
|
|
105
|
+
self.patron_item_combos: set = set()
|
|
106
|
+
self.t0 = time.time()
|
|
107
|
+
self.num_duplicate_loans = 0
|
|
108
|
+
self.skipped_since_already_added = 0
|
|
109
|
+
self.processed_items: set = set()
|
|
110
|
+
self.failed: dict = {}
|
|
111
|
+
self.failed_and_not_dupe: dict = {}
|
|
55
112
|
self.migration_report = MigrationReport()
|
|
56
|
-
self.valid_legacy_loans = []
|
|
57
|
-
super().__init__(library_config, task_configuration)
|
|
113
|
+
self.valid_legacy_loans: List[LegacyLoan] = []
|
|
114
|
+
super().__init__(library_config, task_configuration, folio_client)
|
|
58
115
|
self.circulation_helper = CirculationHelper(
|
|
59
116
|
self.folio_client,
|
|
60
117
|
task_configuration.fallback_service_point_id,
|
|
61
118
|
self.migration_report,
|
|
62
119
|
)
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
self.
|
|
70
|
-
self.
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
)
|
|
74
|
-
logging.info(
|
|
75
|
-
|
|
76
|
-
|
|
120
|
+
logging.info("Check that SMTP is disabled before migrating loans")
|
|
121
|
+
self.check_smtp_config()
|
|
122
|
+
logging.info("Proceeding with loans migration")
|
|
123
|
+
logging.info("Attempting to retrieve tenant timezone configuration...")
|
|
124
|
+
my_path = "/configurations/entries?query=(module==ORG%20and%20configName==localeSettings)"
|
|
125
|
+
try:
|
|
126
|
+
self.tenant_timezone_str = json.loads(
|
|
127
|
+
self.folio_client.folio_get_single_object(my_path)["configs"][0][
|
|
128
|
+
"value"
|
|
129
|
+
]
|
|
130
|
+
)["timezone"]
|
|
131
|
+
logging.info("Tenant timezone is: %s", self.tenant_timezone_str)
|
|
132
|
+
except Exception:
|
|
133
|
+
logging.info('Tenant locale settings not available. Using "UTC".')
|
|
134
|
+
self.tenant_timezone_str = "UTC"
|
|
135
|
+
self.tenant_timezone = ZoneInfo(self.tenant_timezone_str)
|
|
136
|
+
self.semi_valid_legacy_loans = []
|
|
137
|
+
for file_def in task_configuration.open_loans_files:
|
|
138
|
+
loans_file_path = (
|
|
139
|
+
self.folder_structure.legacy_records_folder / file_def.file_name
|
|
77
140
|
)
|
|
141
|
+
with open(loans_file_path, "r", encoding="utf-8") as loans_file:
|
|
142
|
+
total_rows, empty_rows, reader = (
|
|
143
|
+
MappingFileMapperBase._get_delimited_file_reader(
|
|
144
|
+
loans_file, loans_file_path
|
|
145
|
+
)
|
|
146
|
+
)
|
|
147
|
+
logging.info("Source data file contains %d rows", total_rows)
|
|
148
|
+
logging.info("Source data file contains %d empty rows", empty_rows)
|
|
149
|
+
self.migration_report.set(
|
|
150
|
+
"GeneralStatistics",
|
|
151
|
+
f"Total rows in {loans_file_path.name}",
|
|
152
|
+
total_rows,
|
|
153
|
+
)
|
|
154
|
+
self.migration_report.set(
|
|
155
|
+
"GeneralStatistics",
|
|
156
|
+
f"Empty rows in {loans_file_path.name}",
|
|
157
|
+
empty_rows,
|
|
158
|
+
)
|
|
159
|
+
self.semi_valid_legacy_loans.extend(
|
|
160
|
+
self.load_and_validate_legacy_loans(
|
|
161
|
+
reader,
|
|
162
|
+
file_def.service_point_id
|
|
163
|
+
or task_configuration.fallback_service_point_id,
|
|
164
|
+
)
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
logging.info(
|
|
168
|
+
"Loaded and validated %s loans in file from %s",
|
|
169
|
+
len(self.semi_valid_legacy_loans),
|
|
170
|
+
file_def.file_name,
|
|
171
|
+
)
|
|
172
|
+
logging.info(
|
|
173
|
+
"Loaded and validated %s loans in total", len(self.semi_valid_legacy_loans)
|
|
174
|
+
)
|
|
78
175
|
if any(self.task_configuration.item_files) or any(
|
|
79
176
|
self.task_configuration.patron_files
|
|
80
177
|
):
|
|
@@ -89,34 +186,62 @@ class LoansMigrator(MigrationTaskBase):
|
|
|
89
186
|
"previously migrated objects"
|
|
90
187
|
)
|
|
91
188
|
self.valid_legacy_loans = self.semi_valid_legacy_loans
|
|
92
|
-
|
|
93
|
-
self.t0 = time.time()
|
|
94
|
-
self.num_duplicate_loans = 0
|
|
95
|
-
self.skipped_since_already_added = 0
|
|
96
|
-
self.processed_items = set()
|
|
97
|
-
self.failed = {}
|
|
98
|
-
self.num_legacy_loans_processed = 0
|
|
99
|
-
self.failed_and_not_dupe = {}
|
|
100
|
-
logging.info("Starting row is %s", task_configuration.starting_row)
|
|
189
|
+
logging.info("Starting row number is %s", task_configuration.starting_row)
|
|
101
190
|
logging.info("Init completed")
|
|
102
191
|
|
|
192
|
+
def check_smtp_config(self):
|
|
193
|
+
try:
|
|
194
|
+
smtp_config = self.folio_client.folio_get_single_object(
|
|
195
|
+
"/smtp-configuration"
|
|
196
|
+
)["smtpConfigurations"][0]
|
|
197
|
+
smtp_config_disabled = "disabled" in smtp_config["host"].lower()
|
|
198
|
+
except IndexError:
|
|
199
|
+
smtp_config_disabled = True
|
|
200
|
+
print_smtp_warning()
|
|
201
|
+
if not smtp_config_disabled:
|
|
202
|
+
logging.warn("SMTP connection not disabled...")
|
|
203
|
+
for i in range(10, 0, -1):
|
|
204
|
+
sys.stdout.write(
|
|
205
|
+
"Pausing for {:02d} seconds. Press Ctrl+C to exit...\r".format(i)
|
|
206
|
+
)
|
|
207
|
+
time.sleep(1)
|
|
208
|
+
else:
|
|
209
|
+
logging.info("SMTP connection is disabled...")
|
|
210
|
+
|
|
103
211
|
def do_work(self):
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
self.
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
212
|
+
with self.folio_client.get_folio_http_client() as self.http_client:
|
|
213
|
+
logging.info("Starting")
|
|
214
|
+
starting_index = (
|
|
215
|
+
self.task_configuration.starting_row - 1
|
|
216
|
+
if self.task_configuration.starting_row > 0
|
|
217
|
+
else 0
|
|
218
|
+
)
|
|
219
|
+
if self.task_configuration.starting_row > 1:
|
|
220
|
+
logging.info(f"Skipping {(starting_index)} records")
|
|
221
|
+
for num_loans, legacy_loan in enumerate(
|
|
222
|
+
self.valid_legacy_loans[starting_index:], start=1
|
|
223
|
+
):
|
|
224
|
+
t0_migration = time.time()
|
|
225
|
+
self.migration_report.add_general_statistics(
|
|
226
|
+
i18n.t("Processed pre-validated loans")
|
|
117
227
|
)
|
|
118
|
-
|
|
119
|
-
|
|
228
|
+
try:
|
|
229
|
+
self.checkout_single_loan(legacy_loan)
|
|
230
|
+
except TransformationRecordFailedError as ee:
|
|
231
|
+
logging.error(
|
|
232
|
+
f"Transformation failed in row {num_loans} Item barcode: {legacy_loan.item_barcode} "
|
|
233
|
+
f"Patron barcode: {legacy_loan.patron_barcode}"
|
|
234
|
+
)
|
|
235
|
+
ee.log_it()
|
|
236
|
+
except Exception as ee:
|
|
237
|
+
logging.exception(
|
|
238
|
+
f"Error in row {num_loans} Item barcode: {legacy_loan.item_barcode} "
|
|
239
|
+
f"Patron barcode: {legacy_loan.patron_barcode} {ee}"
|
|
240
|
+
)
|
|
241
|
+
if num_loans % 25 == 0:
|
|
242
|
+
logging.info(
|
|
243
|
+
f"{timings(self.t0, t0_migration, num_loans)} {num_loans}"
|
|
244
|
+
)
|
|
120
245
|
|
|
121
246
|
def checkout_single_loan(self, legacy_loan: LegacyLoan):
|
|
122
247
|
"""Checks a legacy loan out. Retries once if it fails.
|
|
@@ -127,30 +252,65 @@ class LoansMigrator(MigrationTaskBase):
|
|
|
127
252
|
res_checkout = self.circulation_helper.check_out_by_barcode(legacy_loan)
|
|
128
253
|
|
|
129
254
|
if res_checkout.was_successful:
|
|
130
|
-
self.migration_report.add(
|
|
255
|
+
self.migration_report.add("Details", i18n.t("Checked out on first try"))
|
|
256
|
+
self.migration_report.add_general_statistics(
|
|
257
|
+
i18n.t("Successfully checked out")
|
|
258
|
+
)
|
|
131
259
|
self.set_renewal_count(legacy_loan, res_checkout)
|
|
132
260
|
self.set_new_status(legacy_loan, res_checkout)
|
|
133
261
|
elif res_checkout.should_be_retried:
|
|
134
262
|
res_checkout2 = self.handle_checkout_failure(legacy_loan, res_checkout)
|
|
135
263
|
if res_checkout2.was_successful and res_checkout2.folio_loan:
|
|
136
|
-
self.migration_report.add(
|
|
264
|
+
self.migration_report.add(
|
|
265
|
+
"Details", i18n.t("Checked out on second try")
|
|
266
|
+
)
|
|
267
|
+
self.migration_report.add_general_statistics(
|
|
268
|
+
i18n.t("Successfully checked out")
|
|
269
|
+
)
|
|
137
270
|
logging.info("Checked out on second try")
|
|
138
271
|
self.set_renewal_count(legacy_loan, res_checkout2)
|
|
139
272
|
self.set_new_status(legacy_loan, res_checkout2)
|
|
140
273
|
elif legacy_loan.item_barcode not in self.failed:
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
274
|
+
if res_checkout2.error_message == "Aged to lost and checked out":
|
|
275
|
+
self.migration_report.add(
|
|
276
|
+
"Details",
|
|
277
|
+
i18n.t("Second failure")
|
|
278
|
+
+ f": {res_checkout2.migration_report_message}",
|
|
279
|
+
)
|
|
280
|
+
logging.error(
|
|
281
|
+
f"{res_checkout2.error_message}. Item barcode: {legacy_loan.item_barcode}"
|
|
282
|
+
)
|
|
283
|
+
else:
|
|
284
|
+
self.failed[legacy_loan.item_barcode] = legacy_loan
|
|
285
|
+
self.migration_report.add_general_statistics(i18n.t("Failed loans"))
|
|
286
|
+
logging.error(
|
|
287
|
+
"Failed on second try: %s", res_checkout2.error_message
|
|
288
|
+
)
|
|
289
|
+
self.migration_report.add(
|
|
290
|
+
"Details",
|
|
291
|
+
i18n.t("Second failure")
|
|
292
|
+
+ f": {res_checkout2.migration_report_message}",
|
|
293
|
+
)
|
|
294
|
+
raise TransformationRecordFailedError(
|
|
295
|
+
f"Row {legacy_loan.row}",
|
|
296
|
+
i18n.t("Loans failing during checkout, second try"),
|
|
297
|
+
json.dumps(legacy_loan.to_dict()),
|
|
298
|
+
)
|
|
147
299
|
elif not res_checkout.should_be_retried:
|
|
148
300
|
logging.error(
|
|
149
301
|
"Failed first time. No retries: %s", res_checkout.error_message
|
|
150
302
|
)
|
|
303
|
+
self.migration_report.add_general_statistics(i18n.t("Failed loans"))
|
|
151
304
|
self.migration_report.add(
|
|
152
|
-
|
|
153
|
-
|
|
305
|
+
"Details",
|
|
306
|
+
i18n.t("Failed 1st time. No retries")
|
|
307
|
+
+ f": {res_checkout.migration_report_message}",
|
|
308
|
+
)
|
|
309
|
+
self.failed[legacy_loan.item_barcode] = legacy_loan
|
|
310
|
+
raise TransformationRecordFailedError(
|
|
311
|
+
f"Row {legacy_loan.row}",
|
|
312
|
+
i18n.t("Loans failing during checkout"),
|
|
313
|
+
json.dumps(legacy_loan.to_dict()),
|
|
154
314
|
)
|
|
155
315
|
|
|
156
316
|
def set_new_status(self, legacy_loan: LegacyLoan, res_checkout: TransactionResult):
|
|
@@ -174,45 +334,39 @@ class LoansMigrator(MigrationTaskBase):
|
|
|
174
334
|
if legacy_loan.renewal_count > 0:
|
|
175
335
|
self.update_open_loan(res_checkout.folio_loan, legacy_loan)
|
|
176
336
|
self.migration_report.add_general_statistics(
|
|
177
|
-
"Updated renewal count for loan"
|
|
337
|
+
i18n.t("Updated renewal count for loan")
|
|
178
338
|
)
|
|
179
339
|
|
|
180
340
|
def wrap_up(self):
|
|
181
341
|
for k, v in self.failed.items():
|
|
182
|
-
self.failed_and_not_dupe[k] = [v.to_dict()]
|
|
183
|
-
self.
|
|
184
|
-
Blurbs.GeneralStatistics, "Failed loans", len(self.failed_and_not_dupe)
|
|
185
|
-
)
|
|
186
|
-
self.migration_report.set(
|
|
187
|
-
Blurbs.GeneralStatistics,
|
|
188
|
-
"Total Rows in file",
|
|
189
|
-
self.num_legacy_loans_processed,
|
|
190
|
-
)
|
|
342
|
+
self.failed_and_not_dupe[k] = [v if isinstance(v, dict) else v.to_dict()]
|
|
343
|
+
print(f"Wrapping up. Unique loans in failed:{len(self.failed_and_not_dupe)}")
|
|
191
344
|
|
|
192
345
|
self.write_failed_loans_to_file()
|
|
193
346
|
|
|
194
347
|
with open(self.folder_structure.migration_reports_file, "w+") as report_file:
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
f"Time Finished: {datetime.isoformat(datetime.now(timezone.utc))}\n"
|
|
348
|
+
self.migration_report.write_migration_report(
|
|
349
|
+
i18n.t("Loans migration report"), report_file, self.start_datetime
|
|
198
350
|
)
|
|
199
|
-
|
|
351
|
+
self.clean_out_empty_logs()
|
|
200
352
|
|
|
201
353
|
def write_failed_loans_to_file(self):
|
|
202
354
|
csv_columns = [
|
|
203
|
-
"
|
|
355
|
+
"patron_barcode",
|
|
356
|
+
"proxy_patron_barcode",
|
|
204
357
|
"item_barcode",
|
|
205
|
-
"
|
|
358
|
+
"due_date",
|
|
206
359
|
"out_date",
|
|
207
|
-
"
|
|
360
|
+
"next_item_status",
|
|
208
361
|
"renewal_count",
|
|
362
|
+
"service_point_id",
|
|
209
363
|
]
|
|
210
364
|
with open(self.folder_structure.failed_recs_path, "w+") as failed_loans_file:
|
|
211
365
|
writer = csv.DictWriter(
|
|
212
366
|
failed_loans_file, fieldnames=csv_columns, dialect="tsv"
|
|
213
367
|
)
|
|
214
368
|
writer.writeheader()
|
|
215
|
-
for
|
|
369
|
+
for _k, failed_loan in self.failed_and_not_dupe.items():
|
|
216
370
|
writer.writerow(failed_loan[0])
|
|
217
371
|
|
|
218
372
|
def check_barcodes(self):
|
|
@@ -225,21 +379,36 @@ class LoansMigrator(MigrationTaskBase):
|
|
|
225
379
|
user_barcodes, self.task_configuration.patron_files, self.folder_structure
|
|
226
380
|
)
|
|
227
381
|
for loan in self.semi_valid_legacy_loans:
|
|
228
|
-
has_item_barcode = loan.item_barcode in item_barcodes
|
|
229
|
-
|
|
230
|
-
|
|
382
|
+
has_item_barcode = loan.item_barcode in item_barcodes or not any(
|
|
383
|
+
item_barcodes
|
|
384
|
+
)
|
|
385
|
+
has_patron_barcode = loan.patron_barcode in user_barcodes or not any(
|
|
386
|
+
user_barcodes
|
|
387
|
+
)
|
|
388
|
+
has_proxy_barcode = True
|
|
389
|
+
if loan.proxy_patron_barcode:
|
|
390
|
+
has_proxy_barcode = (
|
|
391
|
+
loan.proxy_patron_barcode in user_barcodes or not any(user_barcodes)
|
|
392
|
+
)
|
|
393
|
+
if has_item_barcode and has_patron_barcode and has_proxy_barcode:
|
|
231
394
|
self.migration_report.add_general_statistics(
|
|
232
|
-
"Loans verified against migrated user and item"
|
|
395
|
+
i18n.t("Loans verified against migrated user and item")
|
|
233
396
|
)
|
|
234
397
|
yield loan
|
|
235
398
|
else:
|
|
399
|
+
# Add this loan to failed loans for later correction and re-run.
|
|
400
|
+
self.failed[loan.item_barcode] = loan
|
|
401
|
+
self.migration_report.add_general_statistics(i18n.t("Failed loans"))
|
|
236
402
|
self.migration_report.add(
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
f"
|
|
403
|
+
"DiscardedLoans",
|
|
404
|
+
i18n.t("Loans discarded. Had migrated item barcode")
|
|
405
|
+
+ f": {has_item_barcode}. "
|
|
406
|
+
+ i18n.t("Had migrated user barcode")
|
|
407
|
+
+ f": {has_patron_barcode}"
|
|
408
|
+
+ f": {has_proxy_barcode}",
|
|
240
409
|
)
|
|
241
410
|
if not has_item_barcode:
|
|
242
|
-
Helper.
|
|
411
|
+
Helper.log_data_issue_failed(
|
|
243
412
|
"", "Loan without matched item barcode", json.dumps(loan.to_dict())
|
|
244
413
|
)
|
|
245
414
|
if not has_patron_barcode:
|
|
@@ -248,38 +417,72 @@ class LoansMigrator(MigrationTaskBase):
|
|
|
248
417
|
"Loan without matched patron barcode",
|
|
249
418
|
json.dumps(loan.to_dict()),
|
|
250
419
|
)
|
|
420
|
+
if not has_proxy_barcode:
|
|
421
|
+
Helper.log_data_issue_failed(
|
|
422
|
+
"",
|
|
423
|
+
"Loan without matched proxy patron barcode",
|
|
424
|
+
json.dumps(loan.to_dict()),
|
|
425
|
+
)
|
|
251
426
|
|
|
252
|
-
def load_and_validate_legacy_loans(
|
|
427
|
+
def load_and_validate_legacy_loans(
|
|
428
|
+
self, loans_reader, service_point_id: str
|
|
429
|
+
) -> list:
|
|
430
|
+
results = []
|
|
253
431
|
num_bad = 0
|
|
254
432
|
logging.info("Validating legacy loans in file...")
|
|
255
433
|
for legacy_loan_count, legacy_loan_dict in enumerate(loans_reader):
|
|
256
434
|
try:
|
|
257
435
|
legacy_loan = LegacyLoan(
|
|
258
436
|
legacy_loan_dict,
|
|
259
|
-
|
|
437
|
+
service_point_id,
|
|
438
|
+
self.migration_report,
|
|
439
|
+
self.tenant_timezone,
|
|
260
440
|
legacy_loan_count,
|
|
261
441
|
)
|
|
262
442
|
if any(legacy_loan.errors):
|
|
263
443
|
num_bad += 1
|
|
264
|
-
self.migration_report.add_general_statistics(
|
|
444
|
+
self.migration_report.add_general_statistics(
|
|
445
|
+
i18n.t("Loans failed pre-validation")
|
|
446
|
+
)
|
|
447
|
+
self.migration_report.add_general_statistics(i18n.t("Failed loans"))
|
|
265
448
|
for error in legacy_loan.errors:
|
|
266
449
|
self.migration_report.add(
|
|
267
|
-
|
|
450
|
+
"DiscardedLoans", f"{error[0]} - {error[1]}"
|
|
268
451
|
)
|
|
452
|
+
# Add this loan to failed loans for later correction and re-run.
|
|
453
|
+
self.failed[
|
|
454
|
+
legacy_loan.item_barcode or f"no_barcode_{legacy_loan_count}"
|
|
455
|
+
] = legacy_loan
|
|
269
456
|
else:
|
|
270
|
-
|
|
457
|
+
results.append(legacy_loan)
|
|
458
|
+
except TransformationRecordFailedError as trfe:
|
|
459
|
+
num_bad += 1
|
|
460
|
+
self.migration_report.add_general_statistics(
|
|
461
|
+
i18n.t("Loans failed pre-validation")
|
|
462
|
+
)
|
|
463
|
+
self.migration_report.add(
|
|
464
|
+
"DiscardedLoans",
|
|
465
|
+
f"{trfe.message} - see data issues log",
|
|
466
|
+
)
|
|
467
|
+
trfe.log_it()
|
|
468
|
+
self.failed[
|
|
469
|
+
legacy_loan_dict.get(
|
|
470
|
+
"item_barcode", f"no_barcode_{legacy_loan_count}"
|
|
471
|
+
)
|
|
472
|
+
] = legacy_loan_dict
|
|
271
473
|
except ValueError as ve:
|
|
272
474
|
logging.exception(ve)
|
|
273
475
|
logging.info(
|
|
274
|
-
f"Done validating {legacy_loan_count} "
|
|
275
|
-
f"
|
|
476
|
+
f"Done validating {legacy_loan_count + 1} legacy loans out of which "
|
|
477
|
+
f"{num_bad} where discarded."
|
|
276
478
|
)
|
|
277
|
-
if num_bad / legacy_loan_count > 0.5:
|
|
278
|
-
q = num_bad / legacy_loan_count
|
|
479
|
+
if num_bad / (legacy_loan_count + 1) > 0.5:
|
|
480
|
+
q = num_bad / (legacy_loan_count + 1)
|
|
279
481
|
logging.error("%s percent of loans failed to validate.", (q * 100))
|
|
280
482
|
self.migration_report.log_me()
|
|
281
483
|
logging.critical("Halting...")
|
|
282
484
|
sys.exit(1)
|
|
485
|
+
return results
|
|
283
486
|
|
|
284
487
|
def handle_checkout_failure(
|
|
285
488
|
self, legacy_loan, folio_checkout: TransactionResult
|
|
@@ -306,27 +509,38 @@ class LoansMigrator(MigrationTaskBase):
|
|
|
306
509
|
elif folio_checkout.error_message.startswith(
|
|
307
510
|
"Cannot check out item that already has an open loan"
|
|
308
511
|
):
|
|
309
|
-
return
|
|
310
|
-
elif
|
|
311
|
-
return self.
|
|
512
|
+
return self.handle_checked_out_item(legacy_loan)
|
|
513
|
+
elif "Item is already checked out" in folio_checkout.error_message:
|
|
514
|
+
return self.handle_checked_out_item(legacy_loan)
|
|
515
|
+
elif "Aged to lost" in folio_checkout.error_message:
|
|
516
|
+
return self.handle_lost_item(legacy_loan, "Aged to lost")
|
|
312
517
|
elif folio_checkout.error_message == "Declared lost":
|
|
313
|
-
return
|
|
518
|
+
return self.handle_lost_item(legacy_loan, "Declared lost")
|
|
314
519
|
elif folio_checkout.error_message.startswith(
|
|
315
520
|
"Cannot check out to inactive user"
|
|
316
521
|
):
|
|
317
|
-
return self.
|
|
522
|
+
return self.checkout_to_inactive_user(legacy_loan)
|
|
523
|
+
elif (
|
|
524
|
+
"has the item status Claimed returned and cannot be checked out"
|
|
525
|
+
in folio_checkout.error_message
|
|
526
|
+
):
|
|
527
|
+
return self.handle_claimed_returned_item(legacy_loan)
|
|
318
528
|
else:
|
|
319
529
|
self.migration_report.add(
|
|
320
|
-
|
|
321
|
-
|
|
530
|
+
"Details",
|
|
531
|
+
i18n.t("Other checkout failure") + f": {folio_checkout.error_message}",
|
|
322
532
|
)
|
|
323
533
|
# First failure. Add to list of failed loans
|
|
324
534
|
if legacy_loan.item_barcode not in self.failed:
|
|
325
535
|
self.failed[legacy_loan.item_barcode] = legacy_loan
|
|
326
536
|
else:
|
|
327
|
-
logging.
|
|
328
|
-
|
|
329
|
-
|
|
537
|
+
logging.info(
|
|
538
|
+
i18n.t("Loan already in failed.")
|
|
539
|
+
+ " "
|
|
540
|
+
+ i18n.t("item barcode")
|
|
541
|
+
+ f": {legacy_loan.item_barcode}"
|
|
542
|
+
+ i18n.t("Patron barcode")
|
|
543
|
+
+ f": {legacy_loan.patron_barcode}",
|
|
330
544
|
)
|
|
331
545
|
self.failed_and_not_dupe[legacy_loan.item_barcode] = [
|
|
332
546
|
legacy_loan,
|
|
@@ -337,12 +551,12 @@ class LoansMigrator(MigrationTaskBase):
|
|
|
337
551
|
f"{legacy_loan.item_barcode} Patron barcode: {legacy_loan.patron_barcode}"
|
|
338
552
|
)
|
|
339
553
|
self.migration_report.add(
|
|
340
|
-
|
|
554
|
+
"Details", i18n.t("Duplicate loans (or failed twice)")
|
|
341
555
|
)
|
|
342
556
|
del self.failed[legacy_loan.item_barcode]
|
|
343
557
|
return TransactionResult(False, False, "", "", "")
|
|
344
558
|
|
|
345
|
-
def
|
|
559
|
+
def checkout_to_inactive_user(self, legacy_loan) -> TransactionResult:
|
|
346
560
|
logging.info("Cannot check out to inactive user. Activating and trying again")
|
|
347
561
|
user = self.get_user_by_barcode(legacy_loan.patron_barcode)
|
|
348
562
|
expiration_date = user.get("expirationDate", datetime.isoformat(datetime.now()))
|
|
@@ -352,57 +566,135 @@ class LoansMigrator(MigrationTaskBase):
|
|
|
352
566
|
res = self.circulation_helper.check_out_by_barcode(
|
|
353
567
|
legacy_loan
|
|
354
568
|
) # checkout_and_update
|
|
355
|
-
|
|
569
|
+
if res.should_be_retried:
|
|
570
|
+
res = self.handle_checkout_failure(legacy_loan, res)
|
|
571
|
+
self.migration_report.add("Details", res.migration_report_message)
|
|
356
572
|
self.deactivate_user(user, expiration_date)
|
|
357
573
|
logging.debug("Successfully Deactivated user again")
|
|
358
|
-
self.migration_report.add(
|
|
574
|
+
self.migration_report.add("Details", i18n.t("Handled inactive users"))
|
|
359
575
|
return res
|
|
360
576
|
|
|
361
|
-
def
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
577
|
+
def handle_checked_out_item(self, legacy_loan: LegacyLoan) -> TransactionResult:
|
|
578
|
+
if self.circulation_helper.is_checked_out(legacy_loan):
|
|
579
|
+
return TransactionResult(
|
|
580
|
+
False,
|
|
581
|
+
False,
|
|
582
|
+
legacy_loan,
|
|
583
|
+
i18n.t(
|
|
584
|
+
"Loan already exists for %{item_barcode}",
|
|
585
|
+
item_barcode=legacy_loan.item_barcode,
|
|
586
|
+
),
|
|
587
|
+
i18n.t(
|
|
588
|
+
"Loan already exists for %{item_barcode}",
|
|
589
|
+
item_barcode=legacy_loan.item_barcode,
|
|
590
|
+
),
|
|
591
|
+
)
|
|
592
|
+
else:
|
|
593
|
+
logging.debug(
|
|
594
|
+
i18n.t(
|
|
595
|
+
'Setting item %{item_barcode} to status "Available"',
|
|
596
|
+
item_barcode=legacy_loan.item_barcode,
|
|
597
|
+
)
|
|
598
|
+
)
|
|
599
|
+
legacy_loan.next_item_status = "Available"
|
|
600
|
+
self.set_item_status(legacy_loan)
|
|
601
|
+
res_checkout = self.circulation_helper.check_out_by_barcode(legacy_loan)
|
|
602
|
+
legacy_loan.next_item_status = "Checked out"
|
|
603
|
+
return res_checkout
|
|
604
|
+
|
|
605
|
+
def handle_lost_item(
|
|
606
|
+
self,
|
|
607
|
+
legacy_loan: LegacyLoan,
|
|
608
|
+
lost_type: Literal["Aged to lost", "Declared lost"],
|
|
609
|
+
) -> TransactionResult:
|
|
610
|
+
if self.circulation_helper.is_checked_out(legacy_loan):
|
|
611
|
+
return TransactionResult(
|
|
612
|
+
False,
|
|
613
|
+
False,
|
|
614
|
+
legacy_loan,
|
|
615
|
+
i18n.t("%{lost_type} and checked out", lost_type=lost_type),
|
|
616
|
+
i18n.t("%{lost_type} and checked out", lost_type=lost_type),
|
|
617
|
+
)
|
|
618
|
+
|
|
619
|
+
else:
|
|
620
|
+
logging.debug(
|
|
621
|
+
'Setting item %{item_barcode} to status "Available"',
|
|
622
|
+
item_barcode=legacy_loan.item_barcode,
|
|
623
|
+
)
|
|
624
|
+
legacy_loan.next_item_status = "Available"
|
|
625
|
+
self.set_item_status(legacy_loan)
|
|
626
|
+
res_checkout = self.circulation_helper.check_out_by_barcode(legacy_loan)
|
|
627
|
+
legacy_loan.next_item_status = lost_type
|
|
628
|
+
if lost_type == "Aged to lost":
|
|
629
|
+
self.set_item_status(legacy_loan)
|
|
630
|
+
s = i18n.t(
|
|
631
|
+
"Successfully Checked out %{lost_type} item and put the status back",
|
|
632
|
+
lost_type=lost_type,
|
|
633
|
+
)
|
|
634
|
+
else:
|
|
635
|
+
s = i18n.t(
|
|
636
|
+
"Successfully Checked out %{lost_type} item. Item will be declared lost.",
|
|
637
|
+
lost_type=lost_type,
|
|
638
|
+
)
|
|
639
|
+
logging.info(s)
|
|
640
|
+
self.migration_report.add("Details", s)
|
|
641
|
+
return res_checkout
|
|
642
|
+
|
|
643
|
+
def handle_claimed_returned_item(self, legacy_loan: LegacyLoan):
|
|
644
|
+
if self.circulation_helper.is_checked_out(legacy_loan):
|
|
645
|
+
return TransactionResult(
|
|
646
|
+
False,
|
|
647
|
+
False,
|
|
648
|
+
legacy_loan,
|
|
649
|
+
i18n.t("Claimed returned and checked out"),
|
|
650
|
+
i18n.t("Claimed returned and checked out"),
|
|
651
|
+
)
|
|
652
|
+
else:
|
|
653
|
+
logging.debug(
|
|
654
|
+
'Setting item %{item_barcode} to status "Available"',
|
|
655
|
+
item_barcode=legacy_loan.item_barcode,
|
|
656
|
+
)
|
|
657
|
+
legacy_loan.next_item_status = "Available"
|
|
658
|
+
self.set_item_status(legacy_loan)
|
|
659
|
+
res_checkout = self.circulation_helper.check_out_by_barcode(legacy_loan)
|
|
660
|
+
legacy_loan.next_item_status = "Claimed returned"
|
|
661
|
+
return res_checkout
|
|
372
662
|
|
|
373
663
|
def update_open_loan(self, folio_loan: dict, legacy_loan: LegacyLoan):
|
|
374
664
|
due_date = du_parser.isoparse(str(legacy_loan.due_date))
|
|
375
665
|
out_date = du_parser.isoparse(str(legacy_loan.out_date))
|
|
376
666
|
renewal_count = legacy_loan.renewal_count
|
|
377
|
-
# TODO: add logging instead of print out
|
|
378
667
|
try:
|
|
379
668
|
loan_to_put = copy.deepcopy(folio_loan)
|
|
380
669
|
del loan_to_put["metadata"]
|
|
381
670
|
loan_to_put["dueDate"] = due_date.isoformat()
|
|
382
671
|
loan_to_put["loanDate"] = out_date.isoformat()
|
|
383
672
|
loan_to_put["renewalCount"] = renewal_count
|
|
384
|
-
url =
|
|
385
|
-
|
|
673
|
+
url = (
|
|
674
|
+
f"{self.folio_client.gateway_url}/circulation/loans/{loan_to_put['id']}"
|
|
675
|
+
)
|
|
676
|
+
req = self.http_client.put(
|
|
386
677
|
url,
|
|
387
678
|
headers=self.folio_client.okapi_headers,
|
|
388
|
-
|
|
679
|
+
json=loan_to_put,
|
|
389
680
|
)
|
|
390
681
|
if req.status_code == 422:
|
|
391
682
|
error_message = json.loads(req.text)["errors"][0]["message"]
|
|
392
683
|
s = f"Update open loan error: {error_message} {req.status_code}"
|
|
393
|
-
self.migration_report.add(
|
|
684
|
+
self.migration_report.add("Details", s)
|
|
394
685
|
logging.error(s)
|
|
395
686
|
return False
|
|
396
687
|
elif req.status_code in [201, 204]:
|
|
397
688
|
self.migration_report.add(
|
|
398
|
-
|
|
399
|
-
|
|
689
|
+
"Details",
|
|
690
|
+
i18n.t("Successfully updated open loan") + f" ({req.status_code})",
|
|
400
691
|
)
|
|
401
692
|
return True
|
|
402
693
|
else:
|
|
403
694
|
self.migration_report.add(
|
|
404
|
-
|
|
405
|
-
|
|
695
|
+
"Details",
|
|
696
|
+
i18n.t("Update open loan error http status")
|
|
697
|
+
+ f": {req.status_code}",
|
|
406
698
|
)
|
|
407
699
|
req.raise_for_status()
|
|
408
700
|
logging.debug("Updating open loan was successful")
|
|
@@ -432,16 +724,17 @@ class LoansMigrator(MigrationTaskBase):
|
|
|
432
724
|
"servicePointId": str(self.task_configuration.fallback_service_point_id),
|
|
433
725
|
}
|
|
434
726
|
logging.debug(f"Declare lost data: {json.dumps(data, indent=4)}")
|
|
435
|
-
if self.folio_put_post(
|
|
727
|
+
if self.folio_put_post(
|
|
728
|
+
declare_lost_url, data, "POST", i18n.t("Declare item as lost")
|
|
729
|
+
):
|
|
436
730
|
self.migration_report.add(
|
|
437
|
-
|
|
731
|
+
"Details", i18n.t("Successfully declared loan as lost")
|
|
438
732
|
)
|
|
439
733
|
else:
|
|
440
734
|
logging.error(f"Unsuccessfully declared loan {folio_loan} as lost")
|
|
441
735
|
self.migration_report.add(
|
|
442
|
-
|
|
736
|
+
"Details", i18n.t("Unsuccessfully declared loan as lost")
|
|
443
737
|
)
|
|
444
|
-
# TODO: Exception handling
|
|
445
738
|
|
|
446
739
|
def claim_returned(self, folio_loan):
|
|
447
740
|
claim_returned_url = (
|
|
@@ -457,34 +750,44 @@ class LoansMigrator(MigrationTaskBase):
|
|
|
457
750
|
}
|
|
458
751
|
logging.debug(f"Claim returned data:\t{json.dumps(data)}")
|
|
459
752
|
if self.folio_put_post(
|
|
460
|
-
claim_returned_url, data, "POST", "
|
|
753
|
+
claim_returned_url, data, "POST", i18n.t("Claim item returned")
|
|
461
754
|
):
|
|
462
755
|
self.migration_report.add(
|
|
463
|
-
|
|
756
|
+
"Details", i18n.t("Successfully declared loan as Claimed returned")
|
|
464
757
|
)
|
|
465
758
|
else:
|
|
466
759
|
logging.error(
|
|
467
760
|
f"Unsuccessfully declared loan {folio_loan} as Claimed returned"
|
|
468
761
|
)
|
|
469
762
|
self.migration_report.add(
|
|
470
|
-
|
|
471
|
-
|
|
763
|
+
"Details",
|
|
764
|
+
i18n.t(
|
|
765
|
+
"Unsuccessfully declared loan %{loan} as Claimed returned",
|
|
766
|
+
loan=folio_loan,
|
|
767
|
+
),
|
|
472
768
|
)
|
|
473
|
-
# TODO: Exception handling
|
|
474
769
|
|
|
475
770
|
def set_item_status(self, legacy_loan: LegacyLoan):
|
|
476
771
|
try:
|
|
477
772
|
# Get Item by barcode, update status.
|
|
478
|
-
|
|
479
|
-
|
|
773
|
+
item_path = (
|
|
774
|
+
f'item-storage/items?query=(barcode=="{legacy_loan.item_barcode}")'
|
|
775
|
+
)
|
|
776
|
+
item_url = f"{self.folio_client.gateway_url}/{item_path}"
|
|
777
|
+
resp = self.http_client.get(
|
|
778
|
+
item_url, headers=self.folio_client.okapi_headers
|
|
779
|
+
)
|
|
480
780
|
resp.raise_for_status()
|
|
481
781
|
data = resp.json()
|
|
482
782
|
folio_item = data["items"][0]
|
|
483
783
|
folio_item["status"]["name"] = legacy_loan.next_item_status
|
|
484
784
|
if self.update_item(folio_item):
|
|
485
785
|
self.migration_report.add(
|
|
486
|
-
|
|
487
|
-
|
|
786
|
+
"Details",
|
|
787
|
+
i18n.t(
|
|
788
|
+
"Successfully set item status to %{status}",
|
|
789
|
+
status=legacy_loan.next_item_status,
|
|
790
|
+
),
|
|
488
791
|
)
|
|
489
792
|
logging.debug(
|
|
490
793
|
f"Successfully set item with barcode "
|
|
@@ -498,8 +801,11 @@ class LoansMigrator(MigrationTaskBase):
|
|
|
498
801
|
f"{legacy_loan.item_barcode} to {legacy_loan.next_item_status}"
|
|
499
802
|
)
|
|
500
803
|
self.migration_report.add(
|
|
501
|
-
|
|
502
|
-
|
|
804
|
+
"Details",
|
|
805
|
+
i18n.t(
|
|
806
|
+
"Error setting item status to %{status}",
|
|
807
|
+
status=legacy_loan.next_item_status,
|
|
808
|
+
),
|
|
503
809
|
)
|
|
504
810
|
except Exception as ee:
|
|
505
811
|
logging.error(
|
|
@@ -511,43 +817,43 @@ class LoansMigrator(MigrationTaskBase):
|
|
|
511
817
|
def activate_user(self, user):
|
|
512
818
|
user["active"] = True
|
|
513
819
|
self.update_user(user)
|
|
514
|
-
self.migration_report.add(
|
|
820
|
+
self.migration_report.add("Details", i18n.t("Successfully activated user"))
|
|
515
821
|
|
|
516
822
|
def deactivate_user(self, user, expiration_date):
|
|
517
823
|
user["expirationDate"] = expiration_date
|
|
518
824
|
user["active"] = False
|
|
519
825
|
self.update_user(user)
|
|
520
|
-
self.migration_report.add(
|
|
826
|
+
self.migration_report.add("Details", i18n.t("Successfully deactivated user"))
|
|
521
827
|
|
|
522
828
|
def update_item(self, item):
|
|
523
|
-
url = f
|
|
524
|
-
return self.folio_put_post(url, item, "PUT", "Update item")
|
|
829
|
+
url = f"/item-storage/items/{item['id']}"
|
|
830
|
+
return self.folio_put_post(url, item, "PUT", i18n.t("Update item"))
|
|
525
831
|
|
|
526
832
|
def update_user(self, user):
|
|
527
|
-
url = f
|
|
528
|
-
self.folio_put_post(url, user, "PUT", "Update user")
|
|
833
|
+
url = f"/users/{user['id']}"
|
|
834
|
+
self.folio_put_post(url, user, "PUT", i18n.t("Update user"))
|
|
529
835
|
|
|
530
836
|
def get_user_by_barcode(self, barcode):
|
|
531
|
-
url = f'{self.folio_client.
|
|
532
|
-
resp =
|
|
837
|
+
url = f'{self.folio_client.gateway_url}/users?query=(barcode=="{barcode}")'
|
|
838
|
+
resp = self.http_client.get(url, headers=self.folio_client.okapi_headers)
|
|
533
839
|
resp.raise_for_status()
|
|
534
840
|
data = resp.json()
|
|
535
841
|
return data["users"][0]
|
|
536
842
|
|
|
537
843
|
def folio_put_post(self, url, data_dict, verb, action_description=""):
|
|
538
|
-
full_url = f"{self.folio_client.
|
|
844
|
+
full_url = f"{self.folio_client.gateway_url}{url}"
|
|
539
845
|
try:
|
|
540
846
|
if verb == "PUT":
|
|
541
|
-
resp =
|
|
847
|
+
resp = self.http_client.put(
|
|
542
848
|
full_url,
|
|
543
849
|
headers=self.folio_client.okapi_headers,
|
|
544
|
-
|
|
850
|
+
json=data_dict,
|
|
545
851
|
)
|
|
546
852
|
elif verb == "POST":
|
|
547
|
-
resp =
|
|
853
|
+
resp = self.http_client.post(
|
|
548
854
|
full_url,
|
|
549
855
|
headers=self.folio_client.okapi_headers,
|
|
550
|
-
|
|
856
|
+
json=data_dict,
|
|
551
857
|
)
|
|
552
858
|
else:
|
|
553
859
|
raise Exception("Bad verb")
|
|
@@ -555,18 +861,28 @@ class LoansMigrator(MigrationTaskBase):
|
|
|
555
861
|
error_message = json.loads(resp.text)["errors"][0]["message"]
|
|
556
862
|
logging.error(error_message)
|
|
557
863
|
self.migration_report.add(
|
|
558
|
-
|
|
864
|
+
"Details",
|
|
865
|
+
i18n.t(
|
|
866
|
+
"%{action} error: %{message}",
|
|
867
|
+
action=action_description,
|
|
868
|
+
message=error_message,
|
|
869
|
+
),
|
|
559
870
|
)
|
|
560
871
|
resp.raise_for_status()
|
|
561
872
|
elif resp.status_code in [201, 204]:
|
|
562
873
|
self.migration_report.add(
|
|
563
|
-
|
|
564
|
-
|
|
874
|
+
"Details",
|
|
875
|
+
i18n.t("Successfully %{action}", action=action_description)
|
|
876
|
+
+ f" ({resp.status_code})",
|
|
565
877
|
)
|
|
566
878
|
else:
|
|
567
879
|
self.migration_report.add(
|
|
568
|
-
|
|
569
|
-
|
|
880
|
+
"Details",
|
|
881
|
+
i18n.t(
|
|
882
|
+
"%{action} error. http status: %{status}",
|
|
883
|
+
action=action_description,
|
|
884
|
+
status=resp.status_code,
|
|
885
|
+
),
|
|
570
886
|
)
|
|
571
887
|
|
|
572
888
|
resp.raise_for_status()
|
|
@@ -579,38 +895,41 @@ class LoansMigrator(MigrationTaskBase):
|
|
|
579
895
|
|
|
580
896
|
def change_due_date(self, folio_loan, legacy_loan):
|
|
581
897
|
try:
|
|
582
|
-
|
|
583
|
-
api_url = f"{self.folio_client.
|
|
898
|
+
api_path = f"{folio_loan['id']}/change-due-date"
|
|
899
|
+
api_url = f"{self.folio_client.gateway_url}/circulation/loans/{api_path}"
|
|
584
900
|
body = {
|
|
585
901
|
"dueDate": du_parser.isoparse(str(legacy_loan.due_date)).isoformat()
|
|
586
902
|
}
|
|
587
|
-
req =
|
|
588
|
-
api_url, headers=self.folio_client.okapi_headers,
|
|
903
|
+
req = self.http_client.post(
|
|
904
|
+
api_url, headers=self.folio_client.okapi_headers, json=body
|
|
589
905
|
)
|
|
590
906
|
if req.status_code == 422:
|
|
591
907
|
error_message = json.loads(req.text)["errors"][0]["message"]
|
|
592
908
|
self.migration_report.add(
|
|
593
|
-
|
|
909
|
+
"Details", i18n.t("Change due date error") + f": {error_message}"
|
|
594
910
|
)
|
|
595
911
|
logging.info(
|
|
596
912
|
f"{error_message}\t",
|
|
597
913
|
)
|
|
598
|
-
self.migration_report.add(
|
|
914
|
+
self.migration_report.add("Details", error_message)
|
|
599
915
|
return False
|
|
600
916
|
elif req.status_code == 201:
|
|
601
917
|
self.migration_report.add(
|
|
602
|
-
|
|
918
|
+
"Details",
|
|
919
|
+
i18n.t("Successfully changed due date") + f" ({req.status_code})",
|
|
603
920
|
)
|
|
604
921
|
return True, json.loads(req.text), None
|
|
605
922
|
elif req.status_code == 204:
|
|
606
923
|
self.migration_report.add(
|
|
607
|
-
|
|
924
|
+
"Details",
|
|
925
|
+
i18n.t("Successfully changed due date") + f" ({req.status_code})",
|
|
608
926
|
)
|
|
609
927
|
return True, None, None
|
|
610
928
|
else:
|
|
611
929
|
self.migration_report.add(
|
|
612
|
-
|
|
613
|
-
|
|
930
|
+
"Details",
|
|
931
|
+
i18n.t("Update open loan error http status"),
|
|
932
|
+
f": {req.status_code}",
|
|
614
933
|
)
|
|
615
934
|
req.raise_for_status()
|
|
616
935
|
except HTTPError as exception:
|
|
@@ -621,18 +940,6 @@ class LoansMigrator(MigrationTaskBase):
|
|
|
621
940
|
logging.info(exception)
|
|
622
941
|
return False, None, None
|
|
623
942
|
|
|
624
|
-
def make_loan_utc(self, legacy_loan: LegacyLoan):
|
|
625
|
-
if self.task_configuration.utc_difference != 0:
|
|
626
|
-
legacy_loan.due_date = legacy_loan.due_date + timedelta(
|
|
627
|
-
hours=self.task_configuration.utc_difference
|
|
628
|
-
)
|
|
629
|
-
legacy_loan.out_date = legacy_loan.out_date + timedelta(
|
|
630
|
-
hours=self.task_configuration.utc_difference
|
|
631
|
-
)
|
|
632
|
-
self.migration_report.add_general_statistics(
|
|
633
|
-
"Adjusted out and due dates to UTC"
|
|
634
|
-
)
|
|
635
|
-
|
|
636
943
|
|
|
637
944
|
def timings(t0, t0func, num_objects):
|
|
638
945
|
avg = num_objects / (time.time() - t0)
|
|
@@ -642,3 +949,7 @@ def timings(t0, t0func, num_objects):
|
|
|
642
949
|
f"Total objects: {num_objects}\tTotal elapsed: {elapsed:.2f}\t"
|
|
643
950
|
f"Average per object: {avg:.2f}\tElapsed this time: {elapsed_func:.2f}"
|
|
644
951
|
)
|
|
952
|
+
|
|
953
|
+
|
|
954
|
+
def print_smtp_warning():
|
|
955
|
+
tprint("\nSMTP?\n", space=2)
|