folio-migration-tools 1.9.2__py3-none-any.whl → 1.9.4__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/mapping_file_transformation/user_mapper.py +39 -31
- folio_migration_tools/marc_rules_transformation/conditions.py +239 -30
- folio_migration_tools/marc_rules_transformation/holdings_statementsparser.py +99 -65
- folio_migration_tools/marc_rules_transformation/marc_file_processor.py +6 -1
- folio_migration_tools/marc_rules_transformation/rules_mapper_bibs.py +2 -2
- folio_migration_tools/marc_rules_transformation/rules_mapper_holdings.py +7 -1
- folio_migration_tools/migration_tasks/batch_poster.py +201 -51
- folio_migration_tools/migration_tasks/holdings_marc_transformer.py +22 -11
- folio_migration_tools/migration_tasks/items_transformer.py +25 -10
- folio_migration_tools/migration_tasks/loans_migrator.py +238 -77
- folio_migration_tools/migration_tasks/organization_transformer.py +1 -0
- folio_migration_tools/task_configuration.py +3 -1
- folio_migration_tools/test_infrastructure/mocked_classes.py +5 -0
- folio_migration_tools/transaction_migration/legacy_loan.py +30 -10
- folio_migration_tools/translations/en.json +19 -1
- {folio_migration_tools-1.9.2.dist-info → folio_migration_tools-1.9.4.dist-info}/METADATA +2 -1
- {folio_migration_tools-1.9.2.dist-info → folio_migration_tools-1.9.4.dist-info}/RECORD +20 -20
- {folio_migration_tools-1.9.2.dist-info → folio_migration_tools-1.9.4.dist-info}/LICENSE +0 -0
- {folio_migration_tools-1.9.2.dist-info → folio_migration_tools-1.9.4.dist-info}/WHEEL +0 -0
- {folio_migration_tools-1.9.2.dist-info → folio_migration_tools-1.9.4.dist-info}/entry_points.txt +0 -0
|
@@ -6,7 +6,7 @@ import sys
|
|
|
6
6
|
import time
|
|
7
7
|
import traceback
|
|
8
8
|
from datetime import datetime, timedelta
|
|
9
|
-
from typing import Annotated, Optional
|
|
9
|
+
from typing import Annotated, Literal, Optional
|
|
10
10
|
from urllib.error import HTTPError
|
|
11
11
|
from zoneinfo import ZoneInfo
|
|
12
12
|
from pydantic import Field
|
|
@@ -17,6 +17,7 @@ from folio_uuid.folio_namespaces import FOLIONamespaces
|
|
|
17
17
|
from art import tprint
|
|
18
18
|
|
|
19
19
|
from folio_migration_tools.circulation_helper import CirculationHelper
|
|
20
|
+
from folio_migration_tools.custom_exceptions import TransformationRecordFailedError
|
|
20
21
|
from folio_migration_tools.helper import Helper
|
|
21
22
|
from folio_migration_tools.library_configuration import (
|
|
22
23
|
FileDefinition,
|
|
@@ -54,7 +55,7 @@ class LoansMigrator(MigrationTaskBase):
|
|
|
54
55
|
Optional[list[FileDefinition]],
|
|
55
56
|
Field(
|
|
56
57
|
title="Open loans files",
|
|
57
|
-
description="List of files containing open loan data."
|
|
58
|
+
description="List of files containing open loan data.",
|
|
58
59
|
),
|
|
59
60
|
]
|
|
60
61
|
fallback_service_point_id: Annotated[
|
|
@@ -68,10 +69,7 @@ class LoansMigrator(MigrationTaskBase):
|
|
|
68
69
|
Optional[int],
|
|
69
70
|
Field(
|
|
70
71
|
title="Starting row",
|
|
71
|
-
description=(
|
|
72
|
-
"The starting row for data processing. "
|
|
73
|
-
"By default is 1."
|
|
74
|
-
),
|
|
72
|
+
description=("The starting row for data processing. By default is 1."),
|
|
75
73
|
),
|
|
76
74
|
] = 1
|
|
77
75
|
item_files: Annotated[
|
|
@@ -79,8 +77,7 @@ class LoansMigrator(MigrationTaskBase):
|
|
|
79
77
|
Field(
|
|
80
78
|
title="Item files",
|
|
81
79
|
description=(
|
|
82
|
-
"List of files containing item data. "
|
|
83
|
-
"By default is empty list."
|
|
80
|
+
"List of files containing item data. By default is empty list."
|
|
84
81
|
),
|
|
85
82
|
),
|
|
86
83
|
] = []
|
|
@@ -89,8 +86,7 @@ class LoansMigrator(MigrationTaskBase):
|
|
|
89
86
|
Field(
|
|
90
87
|
title="Patron files",
|
|
91
88
|
description=(
|
|
92
|
-
"List of files containing patron data. "
|
|
93
|
-
"By default is empty list."
|
|
89
|
+
"List of files containing patron data. By default is empty list."
|
|
94
90
|
),
|
|
95
91
|
),
|
|
96
92
|
] = []
|
|
@@ -103,7 +99,7 @@ class LoansMigrator(MigrationTaskBase):
|
|
|
103
99
|
self,
|
|
104
100
|
task_configuration: TaskConfiguration,
|
|
105
101
|
library_config: LibraryConfiguration,
|
|
106
|
-
folio_client
|
|
102
|
+
folio_client,
|
|
107
103
|
):
|
|
108
104
|
csv.register_dialect("tsv", delimiter="\t")
|
|
109
105
|
self.patron_item_combos: set = set()
|
|
@@ -128,7 +124,9 @@ class LoansMigrator(MigrationTaskBase):
|
|
|
128
124
|
my_path = "/configurations/entries?query=(module==ORG%20and%20configName==localeSettings)"
|
|
129
125
|
try:
|
|
130
126
|
self.tenant_timezone_str = json.loads(
|
|
131
|
-
self.folio_client.folio_get_single_object(my_path)["configs"][0][
|
|
127
|
+
self.folio_client.folio_get_single_object(my_path)["configs"][0][
|
|
128
|
+
"value"
|
|
129
|
+
]
|
|
132
130
|
)["timezone"]
|
|
133
131
|
logging.info("Tenant timezone is: %s", self.tenant_timezone_str)
|
|
134
132
|
except Exception:
|
|
@@ -137,10 +135,14 @@ class LoansMigrator(MigrationTaskBase):
|
|
|
137
135
|
self.tenant_timezone = ZoneInfo(self.tenant_timezone_str)
|
|
138
136
|
self.semi_valid_legacy_loans = []
|
|
139
137
|
for file_def in task_configuration.open_loans_files:
|
|
140
|
-
loans_file_path =
|
|
138
|
+
loans_file_path = (
|
|
139
|
+
self.folder_structure.legacy_records_folder / file_def.file_name
|
|
140
|
+
)
|
|
141
141
|
with open(loans_file_path, "r", encoding="utf-8") as loans_file:
|
|
142
|
-
total_rows, empty_rows, reader =
|
|
143
|
-
|
|
142
|
+
total_rows, empty_rows, reader = (
|
|
143
|
+
MappingFileMapperBase._get_delimited_file_reader(
|
|
144
|
+
loans_file, loans_file_path
|
|
145
|
+
)
|
|
144
146
|
)
|
|
145
147
|
logging.info("Source data file contains %d rows", total_rows)
|
|
146
148
|
logging.info("Source data file contains %d empty rows", empty_rows)
|
|
@@ -157,7 +159,8 @@ class LoansMigrator(MigrationTaskBase):
|
|
|
157
159
|
self.semi_valid_legacy_loans.extend(
|
|
158
160
|
self.load_and_validate_legacy_loans(
|
|
159
161
|
reader,
|
|
160
|
-
file_def.service_point_id
|
|
162
|
+
file_def.service_point_id
|
|
163
|
+
or task_configuration.fallback_service_point_id,
|
|
161
164
|
)
|
|
162
165
|
)
|
|
163
166
|
|
|
@@ -166,8 +169,12 @@ class LoansMigrator(MigrationTaskBase):
|
|
|
166
169
|
len(self.semi_valid_legacy_loans),
|
|
167
170
|
file_def.file_name,
|
|
168
171
|
)
|
|
169
|
-
logging.info(
|
|
170
|
-
|
|
172
|
+
logging.info(
|
|
173
|
+
"Loaded and validated %s loans in total", len(self.semi_valid_legacy_loans)
|
|
174
|
+
)
|
|
175
|
+
if any(self.task_configuration.item_files) or any(
|
|
176
|
+
self.task_configuration.patron_files
|
|
177
|
+
):
|
|
171
178
|
self.valid_legacy_loans = list(self.check_barcodes())
|
|
172
179
|
logging.info(
|
|
173
180
|
"Loaded and validated %s loans against barcodes",
|
|
@@ -184,9 +191,9 @@ class LoansMigrator(MigrationTaskBase):
|
|
|
184
191
|
|
|
185
192
|
def check_smtp_config(self):
|
|
186
193
|
try:
|
|
187
|
-
smtp_config = self.folio_client.folio_get_single_object(
|
|
188
|
-
"
|
|
189
|
-
][0]
|
|
194
|
+
smtp_config = self.folio_client.folio_get_single_object(
|
|
195
|
+
"/smtp-configuration"
|
|
196
|
+
)["smtpConfigurations"][0]
|
|
190
197
|
smtp_config_disabled = "disabled" in smtp_config["host"].lower()
|
|
191
198
|
except IndexError:
|
|
192
199
|
smtp_config_disabled = True
|
|
@@ -194,7 +201,9 @@ class LoansMigrator(MigrationTaskBase):
|
|
|
194
201
|
if not smtp_config_disabled:
|
|
195
202
|
logging.warn("SMTP connection not disabled...")
|
|
196
203
|
for i in range(10, 0, -1):
|
|
197
|
-
sys.stdout.write(
|
|
204
|
+
sys.stdout.write(
|
|
205
|
+
"Pausing for {:02d} seconds. Press Ctrl+C to exit...\r".format(i)
|
|
206
|
+
)
|
|
198
207
|
time.sleep(1)
|
|
199
208
|
else:
|
|
200
209
|
logging.info("SMTP connection is disabled...")
|
|
@@ -224,7 +233,9 @@ class LoansMigrator(MigrationTaskBase):
|
|
|
224
233
|
f"Patron barcode: {legacy_loan.patron_barcode} {ee}"
|
|
225
234
|
)
|
|
226
235
|
if num_loans % 25 == 0:
|
|
227
|
-
logging.info(
|
|
236
|
+
logging.info(
|
|
237
|
+
f"{timings(self.t0, t0_migration, num_loans)} {num_loans}"
|
|
238
|
+
)
|
|
228
239
|
|
|
229
240
|
def checkout_single_loan(self, legacy_loan: LegacyLoan):
|
|
230
241
|
"""Checks a legacy loan out. Retries once if it fails.
|
|
@@ -236,14 +247,20 @@ class LoansMigrator(MigrationTaskBase):
|
|
|
236
247
|
|
|
237
248
|
if res_checkout.was_successful:
|
|
238
249
|
self.migration_report.add("Details", i18n.t("Checked out on first try"))
|
|
239
|
-
self.migration_report.add_general_statistics(
|
|
250
|
+
self.migration_report.add_general_statistics(
|
|
251
|
+
i18n.t("Successfully checked out")
|
|
252
|
+
)
|
|
240
253
|
self.set_renewal_count(legacy_loan, res_checkout)
|
|
241
254
|
self.set_new_status(legacy_loan, res_checkout)
|
|
242
255
|
elif res_checkout.should_be_retried:
|
|
243
256
|
res_checkout2 = self.handle_checkout_failure(legacy_loan, res_checkout)
|
|
244
257
|
if res_checkout2.was_successful and res_checkout2.folio_loan:
|
|
245
|
-
self.migration_report.add(
|
|
246
|
-
|
|
258
|
+
self.migration_report.add(
|
|
259
|
+
"Details", i18n.t("Checked out on second try")
|
|
260
|
+
)
|
|
261
|
+
self.migration_report.add_general_statistics(
|
|
262
|
+
i18n.t("Successfully checked out")
|
|
263
|
+
)
|
|
247
264
|
logging.info("Checked out on second try")
|
|
248
265
|
self.set_renewal_count(legacy_loan, res_checkout2)
|
|
249
266
|
self.set_new_status(legacy_loan, res_checkout2)
|
|
@@ -251,7 +268,8 @@ class LoansMigrator(MigrationTaskBase):
|
|
|
251
268
|
if res_checkout2.error_message == "Aged to lost and checked out":
|
|
252
269
|
self.migration_report.add(
|
|
253
270
|
"Details",
|
|
254
|
-
i18n.t("Second failure")
|
|
271
|
+
i18n.t("Second failure")
|
|
272
|
+
+ f": {res_checkout2.migration_report_message}",
|
|
255
273
|
)
|
|
256
274
|
logging.error(
|
|
257
275
|
f"{res_checkout2.error_message}. Item barcode: {legacy_loan.item_barcode}"
|
|
@@ -260,15 +278,22 @@ class LoansMigrator(MigrationTaskBase):
|
|
|
260
278
|
self.failed[legacy_loan.item_barcode] = legacy_loan
|
|
261
279
|
self.migration_report.add_general_statistics(i18n.t("Failed loans"))
|
|
262
280
|
Helper.log_data_issue(
|
|
263
|
-
"",
|
|
281
|
+
"",
|
|
282
|
+
"Loans failing during checkout",
|
|
283
|
+
json.dumps(legacy_loan.to_dict()),
|
|
284
|
+
)
|
|
285
|
+
logging.error(
|
|
286
|
+
"Failed on second try: %s", res_checkout2.error_message
|
|
264
287
|
)
|
|
265
|
-
logging.error("Failed on second try: %s", res_checkout2.error_message)
|
|
266
288
|
self.migration_report.add(
|
|
267
289
|
"Details",
|
|
268
|
-
i18n.t("Second failure")
|
|
290
|
+
i18n.t("Second failure")
|
|
291
|
+
+ f": {res_checkout2.migration_report_message}",
|
|
269
292
|
)
|
|
270
293
|
elif not res_checkout.should_be_retried:
|
|
271
|
-
logging.error(
|
|
294
|
+
logging.error(
|
|
295
|
+
"Failed first time. No retries: %s", res_checkout.error_message
|
|
296
|
+
)
|
|
272
297
|
self.migration_report.add_general_statistics(i18n.t("Failed loans"))
|
|
273
298
|
self.migration_report.add(
|
|
274
299
|
"Details",
|
|
@@ -295,14 +320,18 @@ class LoansMigrator(MigrationTaskBase):
|
|
|
295
320
|
elif legacy_loan.next_item_status not in ["Available", "", "Checked out"]:
|
|
296
321
|
self.set_item_status(legacy_loan)
|
|
297
322
|
|
|
298
|
-
def set_renewal_count(
|
|
323
|
+
def set_renewal_count(
|
|
324
|
+
self, legacy_loan: LegacyLoan, res_checkout: TransactionResult
|
|
325
|
+
):
|
|
299
326
|
if legacy_loan.renewal_count > 0:
|
|
300
327
|
self.update_open_loan(res_checkout.folio_loan, legacy_loan)
|
|
301
|
-
self.migration_report.add_general_statistics(
|
|
328
|
+
self.migration_report.add_general_statistics(
|
|
329
|
+
i18n.t("Updated renewal count for loan")
|
|
330
|
+
)
|
|
302
331
|
|
|
303
332
|
def wrap_up(self):
|
|
304
333
|
for k, v in self.failed.items():
|
|
305
|
-
self.failed_and_not_dupe[k] = [v.to_dict()]
|
|
334
|
+
self.failed_and_not_dupe[k] = [v if isinstance(v, dict) else v.to_dict()]
|
|
306
335
|
print(f"Wrapping up. Unique loans in failed:{len(self.failed_and_not_dupe)}")
|
|
307
336
|
|
|
308
337
|
self.write_failed_loans_to_file()
|
|
@@ -315,15 +344,19 @@ class LoansMigrator(MigrationTaskBase):
|
|
|
315
344
|
|
|
316
345
|
def write_failed_loans_to_file(self):
|
|
317
346
|
csv_columns = [
|
|
318
|
-
"
|
|
347
|
+
"patron_barcode",
|
|
348
|
+
"proxy_patron_barcode",
|
|
319
349
|
"item_barcode",
|
|
320
|
-
"
|
|
350
|
+
"due_date",
|
|
321
351
|
"out_date",
|
|
322
|
-
"
|
|
352
|
+
"next_item_status",
|
|
323
353
|
"renewal_count",
|
|
354
|
+
"service_point_id",
|
|
324
355
|
]
|
|
325
356
|
with open(self.folder_structure.failed_recs_path, "w+") as failed_loans_file:
|
|
326
|
-
writer = csv.DictWriter(
|
|
357
|
+
writer = csv.DictWriter(
|
|
358
|
+
failed_loans_file, fieldnames=csv_columns, dialect="tsv"
|
|
359
|
+
)
|
|
327
360
|
writer.writeheader()
|
|
328
361
|
for _k, failed_loan in self.failed_and_not_dupe.items():
|
|
329
362
|
writer.writerow(failed_loan[0])
|
|
@@ -338,12 +371,16 @@ class LoansMigrator(MigrationTaskBase):
|
|
|
338
371
|
user_barcodes, self.task_configuration.patron_files, self.folder_structure
|
|
339
372
|
)
|
|
340
373
|
for loan in self.semi_valid_legacy_loans:
|
|
341
|
-
has_item_barcode = loan.item_barcode in item_barcodes or not any(
|
|
342
|
-
|
|
374
|
+
has_item_barcode = loan.item_barcode in item_barcodes or not any(
|
|
375
|
+
item_barcodes
|
|
376
|
+
)
|
|
377
|
+
has_patron_barcode = loan.patron_barcode in user_barcodes or not any(
|
|
378
|
+
user_barcodes
|
|
379
|
+
)
|
|
343
380
|
has_proxy_barcode = True
|
|
344
381
|
if loan.proxy_patron_barcode:
|
|
345
|
-
has_proxy_barcode =
|
|
346
|
-
user_barcodes
|
|
382
|
+
has_proxy_barcode = (
|
|
383
|
+
loan.proxy_patron_barcode in user_barcodes or not any(user_barcodes)
|
|
347
384
|
)
|
|
348
385
|
if has_item_barcode and has_patron_barcode and has_proxy_barcode:
|
|
349
386
|
self.migration_report.add_general_statistics(
|
|
@@ -374,10 +411,14 @@ class LoansMigrator(MigrationTaskBase):
|
|
|
374
411
|
)
|
|
375
412
|
if not has_proxy_barcode:
|
|
376
413
|
Helper.log_data_issue(
|
|
377
|
-
"",
|
|
414
|
+
"",
|
|
415
|
+
"Loan without matched proxy patron barcode",
|
|
416
|
+
json.dumps(loan.to_dict()),
|
|
378
417
|
)
|
|
379
418
|
|
|
380
|
-
def load_and_validate_legacy_loans(
|
|
419
|
+
def load_and_validate_legacy_loans(
|
|
420
|
+
self, loans_reader, service_point_id: str
|
|
421
|
+
) -> list:
|
|
381
422
|
results = []
|
|
382
423
|
num_bad = 0
|
|
383
424
|
logging.info("Validating legacy loans in file...")
|
|
@@ -397,13 +438,30 @@ class LoansMigrator(MigrationTaskBase):
|
|
|
397
438
|
)
|
|
398
439
|
self.migration_report.add_general_statistics(i18n.t("Failed loans"))
|
|
399
440
|
for error in legacy_loan.errors:
|
|
400
|
-
self.migration_report.add(
|
|
441
|
+
self.migration_report.add(
|
|
442
|
+
"DiscardedLoans", f"{error[0]} - {error[1]}"
|
|
443
|
+
)
|
|
401
444
|
# Add this loan to failed loans for later correction and re-run.
|
|
402
445
|
self.failed[
|
|
403
446
|
legacy_loan.item_barcode or f"no_barcode_{legacy_loan_count}"
|
|
404
447
|
] = legacy_loan
|
|
405
448
|
else:
|
|
406
449
|
results.append(legacy_loan)
|
|
450
|
+
except TransformationRecordFailedError as trfe:
|
|
451
|
+
num_bad += 1
|
|
452
|
+
self.migration_report.add_general_statistics(
|
|
453
|
+
i18n.t("Loans failed pre-validation")
|
|
454
|
+
)
|
|
455
|
+
self.migration_report.add(
|
|
456
|
+
"DiscardedLoans",
|
|
457
|
+
f"{trfe.message} - see data issues log",
|
|
458
|
+
)
|
|
459
|
+
trfe.log_it()
|
|
460
|
+
self.failed[
|
|
461
|
+
legacy_loan_dict.get(
|
|
462
|
+
"item_barcode", f"no_barcode_{legacy_loan_count}"
|
|
463
|
+
)
|
|
464
|
+
] = legacy_loan_dict
|
|
407
465
|
except ValueError as ve:
|
|
408
466
|
logging.exception(ve)
|
|
409
467
|
logging.info(
|
|
@@ -443,13 +501,20 @@ class LoansMigrator(MigrationTaskBase):
|
|
|
443
501
|
elif folio_checkout.error_message.startswith(
|
|
444
502
|
"Cannot check out item that already has an open loan"
|
|
445
503
|
):
|
|
446
|
-
return
|
|
504
|
+
return self.handle_checked_out_item(legacy_loan)
|
|
447
505
|
elif "Aged to lost" in folio_checkout.error_message:
|
|
448
|
-
return self.
|
|
506
|
+
return self.handle_lost_item(legacy_loan, "Aged to lost")
|
|
449
507
|
elif folio_checkout.error_message == "Declared lost":
|
|
450
|
-
return
|
|
451
|
-
elif folio_checkout.error_message.startswith(
|
|
452
|
-
|
|
508
|
+
return self.handle_lost_item(legacy_loan, "Declared lost")
|
|
509
|
+
elif folio_checkout.error_message.startswith(
|
|
510
|
+
"Cannot check out to inactive user"
|
|
511
|
+
):
|
|
512
|
+
return self.checkout_to_inactive_user(legacy_loan)
|
|
513
|
+
elif (
|
|
514
|
+
"has the item status Claimed returned and cannot be checked out"
|
|
515
|
+
in folio_checkout.error_message
|
|
516
|
+
):
|
|
517
|
+
return self.handle_claimed_returned_item(legacy_loan)
|
|
453
518
|
else:
|
|
454
519
|
self.migration_report.add(
|
|
455
520
|
"Details",
|
|
@@ -475,46 +540,116 @@ class LoansMigrator(MigrationTaskBase):
|
|
|
475
540
|
f"Duplicate loans (or failed twice) Item barcode: "
|
|
476
541
|
f"{legacy_loan.item_barcode} Patron barcode: {legacy_loan.patron_barcode}"
|
|
477
542
|
)
|
|
478
|
-
self.migration_report.add(
|
|
543
|
+
self.migration_report.add(
|
|
544
|
+
"Details", i18n.t("Duplicate loans (or failed twice)")
|
|
545
|
+
)
|
|
479
546
|
del self.failed[legacy_loan.item_barcode]
|
|
480
547
|
return TransactionResult(False, False, "", "", "")
|
|
481
548
|
|
|
482
|
-
def
|
|
549
|
+
def checkout_to_inactive_user(self, legacy_loan) -> TransactionResult:
|
|
483
550
|
logging.info("Cannot check out to inactive user. Activating and trying again")
|
|
484
551
|
user = self.get_user_by_barcode(legacy_loan.patron_barcode)
|
|
485
552
|
expiration_date = user.get("expirationDate", datetime.isoformat(datetime.now()))
|
|
486
553
|
user["expirationDate"] = datetime.isoformat(datetime.now() + timedelta(days=1))
|
|
487
554
|
self.activate_user(user)
|
|
488
555
|
logging.debug("Successfully Activated user")
|
|
489
|
-
res = self.circulation_helper.check_out_by_barcode(
|
|
556
|
+
res = self.circulation_helper.check_out_by_barcode(
|
|
557
|
+
legacy_loan
|
|
558
|
+
) # checkout_and_update
|
|
559
|
+
if res.should_be_retried:
|
|
560
|
+
res = self.handle_checkout_failure(legacy_loan, res)
|
|
490
561
|
self.migration_report.add("Details", res.migration_report_message)
|
|
491
562
|
self.deactivate_user(user, expiration_date)
|
|
492
563
|
logging.debug("Successfully Deactivated user again")
|
|
493
564
|
self.migration_report.add("Details", i18n.t("Handled inactive users"))
|
|
494
565
|
return res
|
|
495
566
|
|
|
496
|
-
def
|
|
567
|
+
def handle_checked_out_item(self, legacy_loan: LegacyLoan) -> TransactionResult:
|
|
497
568
|
if self.circulation_helper.is_checked_out(legacy_loan):
|
|
498
569
|
return TransactionResult(
|
|
499
570
|
False,
|
|
500
571
|
False,
|
|
501
572
|
legacy_loan,
|
|
502
|
-
i18n.t(
|
|
503
|
-
|
|
573
|
+
i18n.t(
|
|
574
|
+
"Loan already exists for %{item_barcode}",
|
|
575
|
+
item_barcode=legacy_loan.item_barcode,
|
|
576
|
+
),
|
|
577
|
+
i18n.t(
|
|
578
|
+
"Loan already exists for %{item_barcode}",
|
|
579
|
+
item_barcode=legacy_loan.item_barcode,
|
|
580
|
+
),
|
|
504
581
|
)
|
|
505
|
-
|
|
506
582
|
else:
|
|
507
|
-
logging.debug(
|
|
583
|
+
logging.debug(
|
|
584
|
+
i18n.t(
|
|
585
|
+
'Setting item %{item_barcode} to status "Available"',
|
|
586
|
+
item_barcode=legacy_loan.item_barcode,
|
|
587
|
+
)
|
|
588
|
+
)
|
|
508
589
|
legacy_loan.next_item_status = "Available"
|
|
509
590
|
self.set_item_status(legacy_loan)
|
|
510
591
|
res_checkout = self.circulation_helper.check_out_by_barcode(legacy_loan)
|
|
511
|
-
legacy_loan.next_item_status = "
|
|
592
|
+
legacy_loan.next_item_status = "Checked out"
|
|
593
|
+
return res_checkout
|
|
594
|
+
|
|
595
|
+
def handle_lost_item(
|
|
596
|
+
self,
|
|
597
|
+
legacy_loan: LegacyLoan,
|
|
598
|
+
lost_type: Literal["Aged to lost", "Declared lost"],
|
|
599
|
+
) -> TransactionResult:
|
|
600
|
+
if self.circulation_helper.is_checked_out(legacy_loan):
|
|
601
|
+
return TransactionResult(
|
|
602
|
+
False,
|
|
603
|
+
False,
|
|
604
|
+
legacy_loan,
|
|
605
|
+
i18n.t("%{lost_type} and checked out", lost_type=lost_type),
|
|
606
|
+
i18n.t("%{lost_type} and checked out", lost_type=lost_type),
|
|
607
|
+
)
|
|
608
|
+
|
|
609
|
+
else:
|
|
610
|
+
logging.debug(
|
|
611
|
+
'Setting item %{item_barcode} to status "Available"',
|
|
612
|
+
item_barcode=legacy_loan.item_barcode,
|
|
613
|
+
)
|
|
614
|
+
legacy_loan.next_item_status = "Available"
|
|
512
615
|
self.set_item_status(legacy_loan)
|
|
513
|
-
|
|
616
|
+
res_checkout = self.circulation_helper.check_out_by_barcode(legacy_loan)
|
|
617
|
+
legacy_loan.next_item_status = lost_type
|
|
618
|
+
if lost_type == "Aged to lost":
|
|
619
|
+
self.set_item_status(legacy_loan)
|
|
620
|
+
s = i18n.t(
|
|
621
|
+
"Successfully Checked out %{lost_type} item and put the status back",
|
|
622
|
+
lost_type=lost_type,
|
|
623
|
+
)
|
|
624
|
+
else:
|
|
625
|
+
s = i18n.t(
|
|
626
|
+
"Successfully Checked out %{lost_type} item. Item will be declared lost.",
|
|
627
|
+
lost_type=lost_type,
|
|
628
|
+
)
|
|
514
629
|
logging.info(s)
|
|
515
630
|
self.migration_report.add("Details", s)
|
|
516
631
|
return res_checkout
|
|
517
632
|
|
|
633
|
+
def handle_claimed_returned_item(self, legacy_loan: LegacyLoan):
|
|
634
|
+
if self.circulation_helper.is_checked_out(legacy_loan):
|
|
635
|
+
return TransactionResult(
|
|
636
|
+
False,
|
|
637
|
+
False,
|
|
638
|
+
legacy_loan,
|
|
639
|
+
i18n.t("Claimed returned and checked out"),
|
|
640
|
+
i18n.t("Claimed returned and checked out"),
|
|
641
|
+
)
|
|
642
|
+
else:
|
|
643
|
+
logging.debug(
|
|
644
|
+
'Setting item %{item_barcode} to status "Available"',
|
|
645
|
+
item_barcode=legacy_loan.item_barcode,
|
|
646
|
+
)
|
|
647
|
+
legacy_loan.next_item_status = "Available"
|
|
648
|
+
self.set_item_status(legacy_loan)
|
|
649
|
+
res_checkout = self.circulation_helper.check_out_by_barcode(legacy_loan)
|
|
650
|
+
legacy_loan.next_item_status = "Claimed returned"
|
|
651
|
+
return res_checkout
|
|
652
|
+
|
|
518
653
|
def update_open_loan(self, folio_loan: dict, legacy_loan: LegacyLoan):
|
|
519
654
|
due_date = du_parser.isoparse(str(legacy_loan.due_date))
|
|
520
655
|
out_date = du_parser.isoparse(str(legacy_loan.out_date))
|
|
@@ -525,7 +660,9 @@ class LoansMigrator(MigrationTaskBase):
|
|
|
525
660
|
loan_to_put["dueDate"] = due_date.isoformat()
|
|
526
661
|
loan_to_put["loanDate"] = out_date.isoformat()
|
|
527
662
|
loan_to_put["renewalCount"] = renewal_count
|
|
528
|
-
url =
|
|
663
|
+
url = (
|
|
664
|
+
f"{self.folio_client.gateway_url}/circulation/loans/{loan_to_put['id']}"
|
|
665
|
+
)
|
|
529
666
|
req = self.http_client.put(
|
|
530
667
|
url,
|
|
531
668
|
headers=self.folio_client.okapi_headers,
|
|
@@ -546,7 +683,8 @@ class LoansMigrator(MigrationTaskBase):
|
|
|
546
683
|
else:
|
|
547
684
|
self.migration_report.add(
|
|
548
685
|
"Details",
|
|
549
|
-
i18n.t("Update open loan error http status")
|
|
686
|
+
i18n.t("Update open loan error http status")
|
|
687
|
+
+ f": {req.status_code}",
|
|
550
688
|
)
|
|
551
689
|
req.raise_for_status()
|
|
552
690
|
logging.debug("Updating open loan was successful")
|
|
@@ -576,40 +714,59 @@ class LoansMigrator(MigrationTaskBase):
|
|
|
576
714
|
"servicePointId": str(self.task_configuration.fallback_service_point_id),
|
|
577
715
|
}
|
|
578
716
|
logging.debug(f"Declare lost data: {json.dumps(data, indent=4)}")
|
|
579
|
-
if self.folio_put_post(
|
|
580
|
-
|
|
717
|
+
if self.folio_put_post(
|
|
718
|
+
declare_lost_url, data, "POST", i18n.t("Declare item as lost")
|
|
719
|
+
):
|
|
720
|
+
self.migration_report.add(
|
|
721
|
+
"Details", i18n.t("Successfully declared loan as lost")
|
|
722
|
+
)
|
|
581
723
|
else:
|
|
582
724
|
logging.error(f"Unsuccessfully declared loan {folio_loan} as lost")
|
|
583
|
-
self.migration_report.add(
|
|
725
|
+
self.migration_report.add(
|
|
726
|
+
"Details", i18n.t("Unsuccessfully declared loan as lost")
|
|
727
|
+
)
|
|
584
728
|
|
|
585
729
|
def claim_returned(self, folio_loan):
|
|
586
|
-
claim_returned_url =
|
|
730
|
+
claim_returned_url = (
|
|
731
|
+
f"/circulation/loans/{folio_loan['id']}/claim-item-returned"
|
|
732
|
+
)
|
|
587
733
|
logging.debug(f"Claim returned url:{claim_returned_url}")
|
|
588
734
|
due_date = du_parser.isoparse(folio_loan["dueDate"])
|
|
589
735
|
data = {
|
|
590
|
-
"itemClaimedReturnedDateTime": datetime.isoformat(
|
|
736
|
+
"itemClaimedReturnedDateTime": datetime.isoformat(
|
|
737
|
+
due_date + timedelta(days=1)
|
|
738
|
+
),
|
|
591
739
|
"comment": "Created at migration. Date is due date + 1 day",
|
|
592
740
|
}
|
|
593
741
|
logging.debug(f"Claim returned data:\t{json.dumps(data)}")
|
|
594
|
-
if self.folio_put_post(
|
|
742
|
+
if self.folio_put_post(
|
|
743
|
+
claim_returned_url, data, "POST", i18n.t("Claim item returned")
|
|
744
|
+
):
|
|
595
745
|
self.migration_report.add(
|
|
596
746
|
"Details", i18n.t("Successfully declared loan as Claimed returned")
|
|
597
747
|
)
|
|
598
748
|
else:
|
|
599
|
-
logging.error(
|
|
749
|
+
logging.error(
|
|
750
|
+
f"Unsuccessfully declared loan {folio_loan} as Claimed returned"
|
|
751
|
+
)
|
|
600
752
|
self.migration_report.add(
|
|
601
753
|
"Details",
|
|
602
754
|
i18n.t(
|
|
603
|
-
"Unsuccessfully declared loan %{loan} as Claimed returned",
|
|
755
|
+
"Unsuccessfully declared loan %{loan} as Claimed returned",
|
|
756
|
+
loan=folio_loan,
|
|
604
757
|
),
|
|
605
758
|
)
|
|
606
759
|
|
|
607
760
|
def set_item_status(self, legacy_loan: LegacyLoan):
|
|
608
761
|
try:
|
|
609
762
|
# Get Item by barcode, update status.
|
|
610
|
-
item_path =
|
|
763
|
+
item_path = (
|
|
764
|
+
f'item-storage/items?query=(barcode=="{legacy_loan.item_barcode}")'
|
|
765
|
+
)
|
|
611
766
|
item_url = f"{self.folio_client.gateway_url}/{item_path}"
|
|
612
|
-
resp = self.http_client.get(
|
|
767
|
+
resp = self.http_client.get(
|
|
768
|
+
item_url, headers=self.folio_client.okapi_headers
|
|
769
|
+
)
|
|
613
770
|
resp.raise_for_status()
|
|
614
771
|
data = resp.json()
|
|
615
772
|
folio_item = data["items"][0]
|
|
@@ -659,11 +816,11 @@ class LoansMigrator(MigrationTaskBase):
|
|
|
659
816
|
self.migration_report.add("Details", i18n.t("Successfully deactivated user"))
|
|
660
817
|
|
|
661
818
|
def update_item(self, item):
|
|
662
|
-
url = f
|
|
819
|
+
url = f"/item-storage/items/{item['id']}"
|
|
663
820
|
return self.folio_put_post(url, item, "PUT", i18n.t("Update item"))
|
|
664
821
|
|
|
665
822
|
def update_user(self, user):
|
|
666
|
-
url = f
|
|
823
|
+
url = f"/users/{user['id']}"
|
|
667
824
|
self.folio_put_post(url, user, "PUT", i18n.t("Update user"))
|
|
668
825
|
|
|
669
826
|
def get_user_by_barcode(self, barcode):
|
|
@@ -730,7 +887,9 @@ class LoansMigrator(MigrationTaskBase):
|
|
|
730
887
|
try:
|
|
731
888
|
api_path = f"{folio_loan['id']}/change-due-date"
|
|
732
889
|
api_url = f"{self.folio_client.gateway_url}/circulation/loans/{api_path}"
|
|
733
|
-
body = {
|
|
890
|
+
body = {
|
|
891
|
+
"dueDate": du_parser.isoparse(str(legacy_loan.due_date)).isoformat()
|
|
892
|
+
}
|
|
734
893
|
req = self.http_client.post(
|
|
735
894
|
api_url, headers=self.folio_client.okapi_headers, json=body
|
|
736
895
|
)
|
|
@@ -746,12 +905,14 @@ class LoansMigrator(MigrationTaskBase):
|
|
|
746
905
|
return False
|
|
747
906
|
elif req.status_code == 201:
|
|
748
907
|
self.migration_report.add(
|
|
749
|
-
"Details",
|
|
908
|
+
"Details",
|
|
909
|
+
i18n.t("Successfully changed due date") + f" ({req.status_code})",
|
|
750
910
|
)
|
|
751
911
|
return True, json.loads(req.text), None
|
|
752
912
|
elif req.status_code == 204:
|
|
753
913
|
self.migration_report.add(
|
|
754
|
-
"Details",
|
|
914
|
+
"Details",
|
|
915
|
+
i18n.t("Successfully changed due date") + f" ({req.status_code})",
|
|
755
916
|
)
|
|
756
917
|
return True, None, None
|
|
757
918
|
else:
|
|
@@ -148,6 +148,7 @@ class OrganizationTransformer(MigrationTaskBase):
|
|
|
148
148
|
self.mapper = OrganizationMapper(
|
|
149
149
|
self.folio_client,
|
|
150
150
|
self.library_configuration,
|
|
151
|
+
self.task_configuration,
|
|
151
152
|
self.organization_map,
|
|
152
153
|
self.load_ref_data_mapping_file(
|
|
153
154
|
"organizationTypes",
|
|
@@ -25,7 +25,9 @@ class AbstractTaskConfiguration(BaseModel):
|
|
|
25
25
|
str,
|
|
26
26
|
Field(
|
|
27
27
|
title="Migration task type",
|
|
28
|
-
description=(
|
|
28
|
+
description=(
|
|
29
|
+
"The type of migration task you want to perform."
|
|
30
|
+
),
|
|
29
31
|
),
|
|
30
32
|
]
|
|
31
33
|
ecs_tenant_id: Annotated[
|
|
@@ -121,6 +121,11 @@ def folio_get_all_mocked(ref_data_path, array_name, query="", limit=10):
|
|
|
121
121
|
"name": "FOLIO user department name",
|
|
122
122
|
"code": "fdp",
|
|
123
123
|
},
|
|
124
|
+
{
|
|
125
|
+
"id": "12a2ad12-951d-4124-9fb2-58c70f0b7f72",
|
|
126
|
+
"name": "FOLIO user department name 2",
|
|
127
|
+
"code": "fdp2",
|
|
128
|
+
},
|
|
124
129
|
{
|
|
125
130
|
"id": "2f452d21-507d-4b32-a89d-8ea9753cc946",
|
|
126
131
|
"name": "FOLIO fallback user department name",
|