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.
Files changed (20) hide show
  1. folio_migration_tools/mapping_file_transformation/user_mapper.py +39 -31
  2. folio_migration_tools/marc_rules_transformation/conditions.py +239 -30
  3. folio_migration_tools/marc_rules_transformation/holdings_statementsparser.py +99 -65
  4. folio_migration_tools/marc_rules_transformation/marc_file_processor.py +6 -1
  5. folio_migration_tools/marc_rules_transformation/rules_mapper_bibs.py +2 -2
  6. folio_migration_tools/marc_rules_transformation/rules_mapper_holdings.py +7 -1
  7. folio_migration_tools/migration_tasks/batch_poster.py +201 -51
  8. folio_migration_tools/migration_tasks/holdings_marc_transformer.py +22 -11
  9. folio_migration_tools/migration_tasks/items_transformer.py +25 -10
  10. folio_migration_tools/migration_tasks/loans_migrator.py +238 -77
  11. folio_migration_tools/migration_tasks/organization_transformer.py +1 -0
  12. folio_migration_tools/task_configuration.py +3 -1
  13. folio_migration_tools/test_infrastructure/mocked_classes.py +5 -0
  14. folio_migration_tools/transaction_migration/legacy_loan.py +30 -10
  15. folio_migration_tools/translations/en.json +19 -1
  16. {folio_migration_tools-1.9.2.dist-info → folio_migration_tools-1.9.4.dist-info}/METADATA +2 -1
  17. {folio_migration_tools-1.9.2.dist-info → folio_migration_tools-1.9.4.dist-info}/RECORD +20 -20
  18. {folio_migration_tools-1.9.2.dist-info → folio_migration_tools-1.9.4.dist-info}/LICENSE +0 -0
  19. {folio_migration_tools-1.9.2.dist-info → folio_migration_tools-1.9.4.dist-info}/WHEEL +0 -0
  20. {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]["value"]
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 = self.folder_structure.legacy_records_folder / file_def.file_name
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 = MappingFileMapperBase._get_delimited_file_reader(
143
- loans_file, loans_file_path
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 or task_configuration.fallback_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("Loaded and validated %s loans in total", len(self.semi_valid_legacy_loans))
170
- if any(self.task_configuration.item_files) or any(self.task_configuration.patron_files):
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("/smtp-configuration")[
188
- "smtpConfigurations"
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("Pausing for {:02d} seconds. Press Ctrl+C to exit...\r".format(i))
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(f"{timings(self.t0, t0_migration, num_loans)} {num_loans}")
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(i18n.t("Successfully checked out"))
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("Details", i18n.t("Checked out on second try"))
246
- self.migration_report.add_general_statistics(i18n.t("Successfully checked out"))
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") + f": {res_checkout2.migration_report_message}",
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
- "", "Loans failing during checkout", json.dumps(legacy_loan.to_dict())
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") + f": {res_checkout2.migration_report_message}",
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("Failed first time. No retries: %s", res_checkout.error_message)
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(self, legacy_loan: LegacyLoan, res_checkout: TransactionResult):
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(i18n.t("Updated renewal count for loan"))
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
- "due_date",
347
+ "patron_barcode",
348
+ "proxy_patron_barcode",
319
349
  "item_barcode",
320
- "next_item_status",
350
+ "due_date",
321
351
  "out_date",
322
- "patron_barcode",
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(failed_loans_file, fieldnames=csv_columns, dialect="tsv")
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(item_barcodes)
342
- has_patron_barcode = loan.patron_barcode in user_barcodes or not any(user_barcodes)
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 = loan.proxy_patron_barcode in user_barcodes or not any(
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
- "", "Loan without matched proxy patron barcode", json.dumps(loan.to_dict())
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(self, loans_reader, service_point_id: str) -> list:
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("DiscardedLoans", f"{error[0]} - {error[1]}")
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 folio_checkout
504
+ return self.handle_checked_out_item(legacy_loan)
447
505
  elif "Aged to lost" in folio_checkout.error_message:
448
- return self.handle_aged_to_lost_item(legacy_loan)
506
+ return self.handle_lost_item(legacy_loan, "Aged to lost")
449
507
  elif folio_checkout.error_message == "Declared lost":
450
- return folio_checkout
451
- elif folio_checkout.error_message.startswith("Cannot check out to inactive user"):
452
- return self.checkout_to_inactice_user(legacy_loan)
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("Details", i18n.t("Duplicate loans (or failed twice)"))
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 checkout_to_inactice_user(self, legacy_loan) -> TransactionResult:
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(legacy_loan) # checkout_and_update
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 handle_aged_to_lost_item(self, legacy_loan: LegacyLoan) -> TransactionResult:
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("Aged to lost and checked out"),
503
- i18n.t("Aged to lost and checked out"),
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("Setting Available")
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 = "Aged to lost"
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
- s = "Successfully Checked out Aged to lost item and put the status back"
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 = f"{self.folio_client.gateway_url}/circulation/loans/{loan_to_put['id']}"
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") + f": {req.status_code}",
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(declare_lost_url, data, "POST", i18n.t("Declare item as lost")):
580
- self.migration_report.add("Details", i18n.t("Successfully declared loan as lost"))
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("Details", i18n.t("Unsuccessfully declared loan as lost"))
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 = f"/circulation/loans/{folio_loan['id']}/claim-item-returned"
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(due_date + timedelta(days=1)),
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(claim_returned_url, data, "POST", i18n.t("Declare item as lost")):
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(f"Unsuccessfully declared loan {folio_loan} as Claimed returned")
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", loan=folio_loan
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 = f'item-storage/items?query=(barcode=="{legacy_loan.item_barcode}")'
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(item_url, headers=self.folio_client.okapi_headers)
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'/item-storage/items/{item["id"]}'
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'/users/{user["id"]}'
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 = {"dueDate": du_parser.isoparse(str(legacy_loan.due_date)).isoformat()}
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", i18n.t("Successfully changed due date") + f" ({req.status_code})"
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", i18n.t("Successfully changed due date") + f" ({req.status_code})"
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=("The type of migration task you want to perform."),
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",