folio-migration-tools 1.9.3__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.
@@ -174,11 +174,11 @@ class BibsRulesMapper(RulesMapperBase):
174
174
  self.process_marc_field(folio_instance, main_entry_fields[0], ignored_subsequent_fields, legacy_ids)
175
175
  try:
176
176
  self.process_marc_field(folio_instance, marc_record['245'], ignored_subsequent_fields, legacy_ids)
177
- except KeyError:
177
+ except KeyError as ke:
178
178
  raise TransformationRecordFailedError(
179
179
  legacy_ids,
180
180
  "No 245 field in MARC record"
181
- )
181
+ ) from ke
182
182
 
183
183
  def perform_additional_parsing(
184
184
  self,
@@ -196,9 +196,15 @@ class RulesMapperHoldings(RulesMapperBase):
196
196
  if "004" not in marc_record:
197
197
  raise TransformationProcessError(
198
198
  "",
199
- ("No 004 in record. The tools only support bib-mfhd linking throuh 004"),
199
+ ("No 004 in record. The tools only support bib-mfhd linking through 004"),
200
200
  legacy_ids,
201
201
  )
202
+ if len(marc_record.get_fields("004")) > 1:
203
+ Helper.log_data_issue(
204
+ legacy_ids,
205
+ "More than one linked bib (004) found in record. Using the first one",
206
+ [str(x) for x in marc_record.get_fields("004")],
207
+ )
202
208
  legacy_instance_id = marc_record["004"].data.strip()
203
209
  folio_holding["formerIds"].append(f"{self.bib_id_template}{legacy_instance_id}")
204
210
  if legacy_instance_id in self.parent_id_map:
@@ -131,6 +131,56 @@ class BatchPoster(MigrationTaskBase):
131
131
  ),
132
132
  ),
133
133
  ] = False
134
+ preserve_statistical_codes: Annotated[
135
+ bool,
136
+ Field(
137
+ title="Preserve statistical codes",
138
+ description=(
139
+ "Toggles whether or not to preserve statistical codes "
140
+ "during the upsert process. Defaults to False"
141
+ ),
142
+ ),
143
+ ] = False
144
+ preserve_administrative_notes: Annotated[
145
+ bool,
146
+ Field(
147
+ title="Preserve administrative notes",
148
+ description=(
149
+ "Toggles whether or not to preserve administrative notes "
150
+ "during the upsert process. Defaults to False"
151
+ ),
152
+ ),
153
+ ] = False
154
+ preserve_temporary_locations: Annotated[
155
+ bool,
156
+ Field(
157
+ title="Preserve temporary locations",
158
+ description=(
159
+ "Toggles whether or not to preserve temporary locations "
160
+ "on items during the upsert process. Defaults to False"
161
+ ),
162
+ ),
163
+ ] = False
164
+ preserve_temporary_loan_types: Annotated[
165
+ bool,
166
+ Field(
167
+ title="Preserve temporary loan types",
168
+ description=(
169
+ "Toggles whether or not to preserve temporary loan types "
170
+ "on items during the upsert process. Defaults to False"
171
+ ),
172
+ ),
173
+ ] = False
174
+ preserve_item_status: Annotated[
175
+ bool,
176
+ Field(
177
+ title="Preserve item status",
178
+ description=(
179
+ "Toggles whether or not to preserve item status "
180
+ "on items during the upsert process. Defaults to False"
181
+ ),
182
+ ),
183
+ ] = True
134
184
 
135
185
  task_configuration: TaskConfiguration
136
186
 
@@ -292,7 +342,7 @@ class BatchPoster(MigrationTaskBase):
292
342
  """
293
343
  fetch_batch_size = 90
294
344
  fetch_tasks = []
295
- updates = {}
345
+ existing_records = {}
296
346
  async with httpx.AsyncClient(base_url=self.folio_client.gateway_url) as client:
297
347
  for i in range(0, len(batch), fetch_batch_size):
298
348
  batch_slice = batch[i:i + fetch_batch_size]
@@ -313,31 +363,82 @@ class BatchPoster(MigrationTaskBase):
313
363
  responses = await asyncio.gather(*fetch_tasks)
314
364
 
315
365
  for response in responses:
316
- self.update_record_versions(object_type, updates, response)
366
+ self.collect_existing_records_for_upsert(object_type, response, existing_records)
317
367
  for record in batch:
318
- if record["id"] in updates:
319
- record.update(updates[record["id"]])
368
+ if record["id"] in existing_records:
369
+ self.prepare_record_for_upsert(record, existing_records[record["id"]])
370
+
371
+ def handle_source_marc(self, new_record: dict, existing_record: dict):
372
+ updates = {}
373
+ updates.update(existing_record)
374
+ self.handle_upsert_for_administrative_notes(updates)
375
+ self.handle_upsert_for_statistical_codes(updates)
376
+ keep_new = {k: v for k, v in new_record.items() if k in ["statisticalCodeIds", "administrativeNotes"]}
377
+ if "instanceStatusId" in new_record:
378
+ updates["instanceStatusId"] = new_record["instanceStatusId"]
379
+ for k, v in keep_new.items():
380
+ updates[k] = list(dict.fromkeys(updates.get(k, []) + v))
381
+ new_record.update(updates)
320
382
 
321
383
  @staticmethod
322
- def update_record_versions(object_type, updates, response):
384
+ def collect_existing_records_for_upsert(object_type: str, response: httpx.Response, existing_records: dict):
323
385
  if response.status_code == 200:
324
386
  response_json = response.json()
325
387
  for record in response_json[object_type]:
326
- updates[record["id"]] = {
327
- "_version": record["_version"],
328
- }
329
- if "hrid" in record:
330
- updates[record["id"]]["hrid"] = record["hrid"]
331
- if "status" in record:
332
- updates[record["id"]]["status"] = record["status"]
333
- if "lastCheckIn" in record:
334
- updates[record["id"]]["lastCheckIn"] = record["lastCheckIn"]
388
+ existing_records[record["id"]] = record
335
389
  else:
336
390
  logging.error(
337
- "Failed to fetch current records. HTTP %s\t%s",
338
- response.status_code,
339
- response.text,
340
- )
391
+ "Failed to fetch current records. HTTP %s\t%s",
392
+ response.status_code,
393
+ response.text,
394
+ )
395
+
396
+ def handle_upsert_for_statistical_codes(self, updates: dict):
397
+ if not self.task_configuration.preserve_statistical_codes:
398
+ updates.pop("statisticalCodeIds", None)
399
+
400
+ def handle_upsert_for_administrative_notes(self, updates: dict):
401
+ if not self.task_configuration.preserve_administrative_notes:
402
+ updates.pop("administrativeNotes", None)
403
+
404
+ def handle_upsert_for_temporary_locations(self, updates: dict):
405
+ if not self.task_configuration.preserve_temporary_locations:
406
+ updates.pop("temporaryLocationId", None)
407
+
408
+ def handle_upsert_for_temporary_loan_types(self, updates: dict):
409
+ if not self.task_configuration.preserve_temporary_loan_types:
410
+ updates.pop("temporaryLoanTypeId", None)
411
+
412
+ def keep_existing_fields(self, updates: dict, existing_record: dict):
413
+ keep_existing_fields = ["hrid", "lastCheckIn"]
414
+ if self.task_configuration.preserve_item_status:
415
+ keep_existing_fields.append("status")
416
+ for key in keep_existing_fields:
417
+ if key in existing_record:
418
+ updates[key] = existing_record[key]
419
+
420
+ def prepare_record_for_upsert(self, new_record: dict, existing_record: dict):
421
+ if "source" in existing_record and "MARC" in existing_record["source"]:
422
+ self.handle_source_marc(new_record, existing_record)
423
+ else:
424
+ updates = {
425
+ "_version": existing_record["_version"],
426
+ }
427
+ self.keep_existing_fields(updates, existing_record)
428
+ keep_new = {k: v for k, v in new_record.items() if k in ["statisticalCodeIds", "administrativeNotes"]}
429
+ self.handle_upsert_for_statistical_codes(existing_record)
430
+ self.handle_upsert_for_administrative_notes(existing_record)
431
+ self.handle_upsert_for_temporary_locations(existing_record)
432
+ self.handle_upsert_for_temporary_loan_types(existing_record)
433
+ for k, v in keep_new.items():
434
+ updates[k] = list(dict.fromkeys(existing_record.get(k, []) + v))
435
+ for key in [
436
+ "temporaryLocationId",
437
+ "temporaryLoanTypeId",
438
+ ]:
439
+ if key in existing_record:
440
+ updates[key] = existing_record[key]
441
+ new_record.update(updates)
341
442
 
342
443
  async def get_with_retry(self, client: httpx.AsyncClient, url: str, params=None):
343
444
  if params is None:
@@ -218,7 +218,10 @@ class ItemsTransformer(MigrationTaskBase):
218
218
  self.folio_keys = MappingFileMapperBase.get_mapped_folio_properties_from_map(
219
219
  self.items_map
220
220
  )
221
- if any(k for k in self.folio_keys if k.startswith("statisticalCodeIds")):
221
+ if (
222
+ any(k for k in self.folio_keys if k.startswith("statisticalCodeIds"))
223
+ or any(getattr(k, "statistical_code", "") for k in self.task_configuration.files)
224
+ ):
222
225
  statcode_mapping = self.load_ref_data_mapping_file(
223
226
  "statisticalCodeIds",
224
227
  self.folder_structure.mapping_files_folder
@@ -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
@@ -55,7 +55,7 @@ class LoansMigrator(MigrationTaskBase):
55
55
  Optional[list[FileDefinition]],
56
56
  Field(
57
57
  title="Open loans files",
58
- description="List of files containing open loan data."
58
+ description="List of files containing open loan data.",
59
59
  ),
60
60
  ]
61
61
  fallback_service_point_id: Annotated[
@@ -69,10 +69,7 @@ class LoansMigrator(MigrationTaskBase):
69
69
  Optional[int],
70
70
  Field(
71
71
  title="Starting row",
72
- description=(
73
- "The starting row for data processing. "
74
- "By default is 1."
75
- ),
72
+ description=("The starting row for data processing. By default is 1."),
76
73
  ),
77
74
  ] = 1
78
75
  item_files: Annotated[
@@ -80,8 +77,7 @@ class LoansMigrator(MigrationTaskBase):
80
77
  Field(
81
78
  title="Item files",
82
79
  description=(
83
- "List of files containing item data. "
84
- "By default is empty list."
80
+ "List of files containing item data. By default is empty list."
85
81
  ),
86
82
  ),
87
83
  ] = []
@@ -90,8 +86,7 @@ class LoansMigrator(MigrationTaskBase):
90
86
  Field(
91
87
  title="Patron files",
92
88
  description=(
93
- "List of files containing patron data. "
94
- "By default is empty list."
89
+ "List of files containing patron data. By default is empty list."
95
90
  ),
96
91
  ),
97
92
  ] = []
@@ -104,7 +99,7 @@ class LoansMigrator(MigrationTaskBase):
104
99
  self,
105
100
  task_configuration: TaskConfiguration,
106
101
  library_config: LibraryConfiguration,
107
- folio_client
102
+ folio_client,
108
103
  ):
109
104
  csv.register_dialect("tsv", delimiter="\t")
110
105
  self.patron_item_combos: set = set()
@@ -129,7 +124,9 @@ class LoansMigrator(MigrationTaskBase):
129
124
  my_path = "/configurations/entries?query=(module==ORG%20and%20configName==localeSettings)"
130
125
  try:
131
126
  self.tenant_timezone_str = json.loads(
132
- 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
+ ]
133
130
  )["timezone"]
134
131
  logging.info("Tenant timezone is: %s", self.tenant_timezone_str)
135
132
  except Exception:
@@ -138,10 +135,14 @@ class LoansMigrator(MigrationTaskBase):
138
135
  self.tenant_timezone = ZoneInfo(self.tenant_timezone_str)
139
136
  self.semi_valid_legacy_loans = []
140
137
  for file_def in task_configuration.open_loans_files:
141
- 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
+ )
142
141
  with open(loans_file_path, "r", encoding="utf-8") as loans_file:
143
- total_rows, empty_rows, reader = MappingFileMapperBase._get_delimited_file_reader(
144
- loans_file, loans_file_path
142
+ total_rows, empty_rows, reader = (
143
+ MappingFileMapperBase._get_delimited_file_reader(
144
+ loans_file, loans_file_path
145
+ )
145
146
  )
146
147
  logging.info("Source data file contains %d rows", total_rows)
147
148
  logging.info("Source data file contains %d empty rows", empty_rows)
@@ -158,7 +159,8 @@ class LoansMigrator(MigrationTaskBase):
158
159
  self.semi_valid_legacy_loans.extend(
159
160
  self.load_and_validate_legacy_loans(
160
161
  reader,
161
- 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,
162
164
  )
163
165
  )
164
166
 
@@ -167,8 +169,12 @@ class LoansMigrator(MigrationTaskBase):
167
169
  len(self.semi_valid_legacy_loans),
168
170
  file_def.file_name,
169
171
  )
170
- logging.info("Loaded and validated %s loans in total", len(self.semi_valid_legacy_loans))
171
- 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
+ ):
172
178
  self.valid_legacy_loans = list(self.check_barcodes())
173
179
  logging.info(
174
180
  "Loaded and validated %s loans against barcodes",
@@ -185,9 +191,9 @@ class LoansMigrator(MigrationTaskBase):
185
191
 
186
192
  def check_smtp_config(self):
187
193
  try:
188
- smtp_config = self.folio_client.folio_get_single_object("/smtp-configuration")[
189
- "smtpConfigurations"
190
- ][0]
194
+ smtp_config = self.folio_client.folio_get_single_object(
195
+ "/smtp-configuration"
196
+ )["smtpConfigurations"][0]
191
197
  smtp_config_disabled = "disabled" in smtp_config["host"].lower()
192
198
  except IndexError:
193
199
  smtp_config_disabled = True
@@ -195,7 +201,9 @@ class LoansMigrator(MigrationTaskBase):
195
201
  if not smtp_config_disabled:
196
202
  logging.warn("SMTP connection not disabled...")
197
203
  for i in range(10, 0, -1):
198
- 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
+ )
199
207
  time.sleep(1)
200
208
  else:
201
209
  logging.info("SMTP connection is disabled...")
@@ -225,7 +233,9 @@ class LoansMigrator(MigrationTaskBase):
225
233
  f"Patron barcode: {legacy_loan.patron_barcode} {ee}"
226
234
  )
227
235
  if num_loans % 25 == 0:
228
- 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
+ )
229
239
 
230
240
  def checkout_single_loan(self, legacy_loan: LegacyLoan):
231
241
  """Checks a legacy loan out. Retries once if it fails.
@@ -234,17 +244,23 @@ class LoansMigrator(MigrationTaskBase):
234
244
  legacy_loan (LegacyLoan): The Legacy loan
235
245
  """
236
246
  res_checkout = self.circulation_helper.check_out_by_barcode(legacy_loan)
237
-
247
+
238
248
  if res_checkout.was_successful:
239
249
  self.migration_report.add("Details", i18n.t("Checked out on first try"))
240
- 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
+ )
241
253
  self.set_renewal_count(legacy_loan, res_checkout)
242
254
  self.set_new_status(legacy_loan, res_checkout)
243
255
  elif res_checkout.should_be_retried:
244
256
  res_checkout2 = self.handle_checkout_failure(legacy_loan, res_checkout)
245
257
  if res_checkout2.was_successful and res_checkout2.folio_loan:
246
- self.migration_report.add("Details", i18n.t("Checked out on second try"))
247
- 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
+ )
248
264
  logging.info("Checked out on second try")
249
265
  self.set_renewal_count(legacy_loan, res_checkout2)
250
266
  self.set_new_status(legacy_loan, res_checkout2)
@@ -252,7 +268,8 @@ class LoansMigrator(MigrationTaskBase):
252
268
  if res_checkout2.error_message == "Aged to lost and checked out":
253
269
  self.migration_report.add(
254
270
  "Details",
255
- i18n.t("Second failure") + f": {res_checkout2.migration_report_message}",
271
+ i18n.t("Second failure")
272
+ + f": {res_checkout2.migration_report_message}",
256
273
  )
257
274
  logging.error(
258
275
  f"{res_checkout2.error_message}. Item barcode: {legacy_loan.item_barcode}"
@@ -261,15 +278,22 @@ class LoansMigrator(MigrationTaskBase):
261
278
  self.failed[legacy_loan.item_barcode] = legacy_loan
262
279
  self.migration_report.add_general_statistics(i18n.t("Failed loans"))
263
280
  Helper.log_data_issue(
264
- "", "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
265
287
  )
266
- logging.error("Failed on second try: %s", res_checkout2.error_message)
267
288
  self.migration_report.add(
268
289
  "Details",
269
- i18n.t("Second failure") + f": {res_checkout2.migration_report_message}",
290
+ i18n.t("Second failure")
291
+ + f": {res_checkout2.migration_report_message}",
270
292
  )
271
293
  elif not res_checkout.should_be_retried:
272
- 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
+ )
273
297
  self.migration_report.add_general_statistics(i18n.t("Failed loans"))
274
298
  self.migration_report.add(
275
299
  "Details",
@@ -296,10 +320,14 @@ class LoansMigrator(MigrationTaskBase):
296
320
  elif legacy_loan.next_item_status not in ["Available", "", "Checked out"]:
297
321
  self.set_item_status(legacy_loan)
298
322
 
299
- 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
+ ):
300
326
  if legacy_loan.renewal_count > 0:
301
327
  self.update_open_loan(res_checkout.folio_loan, legacy_loan)
302
- 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
+ )
303
331
 
304
332
  def wrap_up(self):
305
333
  for k, v in self.failed.items():
@@ -316,15 +344,19 @@ class LoansMigrator(MigrationTaskBase):
316
344
 
317
345
  def write_failed_loans_to_file(self):
318
346
  csv_columns = [
319
- "due_date",
347
+ "patron_barcode",
348
+ "proxy_patron_barcode",
320
349
  "item_barcode",
321
- "next_item_status",
350
+ "due_date",
322
351
  "out_date",
323
- "patron_barcode",
352
+ "next_item_status",
324
353
  "renewal_count",
354
+ "service_point_id",
325
355
  ]
326
356
  with open(self.folder_structure.failed_recs_path, "w+") as failed_loans_file:
327
- 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
+ )
328
360
  writer.writeheader()
329
361
  for _k, failed_loan in self.failed_and_not_dupe.items():
330
362
  writer.writerow(failed_loan[0])
@@ -339,12 +371,16 @@ class LoansMigrator(MigrationTaskBase):
339
371
  user_barcodes, self.task_configuration.patron_files, self.folder_structure
340
372
  )
341
373
  for loan in self.semi_valid_legacy_loans:
342
- has_item_barcode = loan.item_barcode in item_barcodes or not any(item_barcodes)
343
- 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
+ )
344
380
  has_proxy_barcode = True
345
381
  if loan.proxy_patron_barcode:
346
- has_proxy_barcode = loan.proxy_patron_barcode in user_barcodes or not any(
347
- user_barcodes
382
+ has_proxy_barcode = (
383
+ loan.proxy_patron_barcode in user_barcodes or not any(user_barcodes)
348
384
  )
349
385
  if has_item_barcode and has_patron_barcode and has_proxy_barcode:
350
386
  self.migration_report.add_general_statistics(
@@ -375,10 +411,14 @@ class LoansMigrator(MigrationTaskBase):
375
411
  )
376
412
  if not has_proxy_barcode:
377
413
  Helper.log_data_issue(
378
- "", "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()),
379
417
  )
380
418
 
381
- 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:
382
422
  results = []
383
423
  num_bad = 0
384
424
  logging.info("Validating legacy loans in file...")
@@ -398,7 +438,9 @@ class LoansMigrator(MigrationTaskBase):
398
438
  )
399
439
  self.migration_report.add_general_statistics(i18n.t("Failed loans"))
400
440
  for error in legacy_loan.errors:
401
- self.migration_report.add("DiscardedLoans", f"{error[0]} - {error[1]}")
441
+ self.migration_report.add(
442
+ "DiscardedLoans", f"{error[0]} - {error[1]}"
443
+ )
402
444
  # Add this loan to failed loans for later correction and re-run.
403
445
  self.failed[
404
446
  legacy_loan.item_barcode or f"no_barcode_{legacy_loan_count}"
@@ -416,7 +458,9 @@ class LoansMigrator(MigrationTaskBase):
416
458
  )
417
459
  trfe.log_it()
418
460
  self.failed[
419
- legacy_loan_dict.get("item_barcode", f"no_barcode_{legacy_loan_count}")
461
+ legacy_loan_dict.get(
462
+ "item_barcode", f"no_barcode_{legacy_loan_count}"
463
+ )
420
464
  ] = legacy_loan_dict
421
465
  except ValueError as ve:
422
466
  logging.exception(ve)
@@ -457,13 +501,20 @@ class LoansMigrator(MigrationTaskBase):
457
501
  elif folio_checkout.error_message.startswith(
458
502
  "Cannot check out item that already has an open loan"
459
503
  ):
460
- return folio_checkout
504
+ return self.handle_checked_out_item(legacy_loan)
461
505
  elif "Aged to lost" in folio_checkout.error_message:
462
- return self.handle_aged_to_lost_item(legacy_loan)
506
+ return self.handle_lost_item(legacy_loan, "Aged to lost")
463
507
  elif folio_checkout.error_message == "Declared lost":
464
- return folio_checkout
465
- elif folio_checkout.error_message.startswith("Cannot check out to inactive user"):
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
+ ):
466
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)
467
518
  else:
468
519
  self.migration_report.add(
469
520
  "Details",
@@ -489,7 +540,9 @@ class LoansMigrator(MigrationTaskBase):
489
540
  f"Duplicate loans (or failed twice) Item barcode: "
490
541
  f"{legacy_loan.item_barcode} Patron barcode: {legacy_loan.patron_barcode}"
491
542
  )
492
- 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
+ )
493
546
  del self.failed[legacy_loan.item_barcode]
494
547
  return TransactionResult(False, False, "", "", "")
495
548
 
@@ -500,7 +553,9 @@ class LoansMigrator(MigrationTaskBase):
500
553
  user["expirationDate"] = datetime.isoformat(datetime.now() + timedelta(days=1))
501
554
  self.activate_user(user)
502
555
  logging.debug("Successfully Activated user")
503
- 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
504
559
  if res.should_be_retried:
505
560
  res = self.handle_checkout_failure(legacy_loan, res)
506
561
  self.migration_report.add("Details", res.migration_report_message)
@@ -509,28 +564,92 @@ class LoansMigrator(MigrationTaskBase):
509
564
  self.migration_report.add("Details", i18n.t("Handled inactive users"))
510
565
  return res
511
566
 
512
- def handle_aged_to_lost_item(self, legacy_loan: LegacyLoan) -> TransactionResult:
567
+ def handle_checked_out_item(self, legacy_loan: LegacyLoan) -> TransactionResult:
513
568
  if self.circulation_helper.is_checked_out(legacy_loan):
514
569
  return TransactionResult(
515
570
  False,
516
571
  False,
517
572
  legacy_loan,
518
- i18n.t("Aged to lost and checked out"),
519
- 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
+ ),
520
581
  )
521
-
522
582
  else:
523
- 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
+ )
524
589
  legacy_loan.next_item_status = "Available"
525
590
  self.set_item_status(legacy_loan)
526
591
  res_checkout = self.circulation_helper.check_out_by_barcode(legacy_loan)
527
- 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"
528
615
  self.set_item_status(legacy_loan)
529
- 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
+ )
530
629
  logging.info(s)
531
630
  self.migration_report.add("Details", s)
532
631
  return res_checkout
533
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
+
534
653
  def update_open_loan(self, folio_loan: dict, legacy_loan: LegacyLoan):
535
654
  due_date = du_parser.isoparse(str(legacy_loan.due_date))
536
655
  out_date = du_parser.isoparse(str(legacy_loan.out_date))
@@ -541,7 +660,9 @@ class LoansMigrator(MigrationTaskBase):
541
660
  loan_to_put["dueDate"] = due_date.isoformat()
542
661
  loan_to_put["loanDate"] = out_date.isoformat()
543
662
  loan_to_put["renewalCount"] = renewal_count
544
- 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
+ )
545
666
  req = self.http_client.put(
546
667
  url,
547
668
  headers=self.folio_client.okapi_headers,
@@ -562,7 +683,8 @@ class LoansMigrator(MigrationTaskBase):
562
683
  else:
563
684
  self.migration_report.add(
564
685
  "Details",
565
- 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}",
566
688
  )
567
689
  req.raise_for_status()
568
690
  logging.debug("Updating open loan was successful")
@@ -592,40 +714,59 @@ class LoansMigrator(MigrationTaskBase):
592
714
  "servicePointId": str(self.task_configuration.fallback_service_point_id),
593
715
  }
594
716
  logging.debug(f"Declare lost data: {json.dumps(data, indent=4)}")
595
- if self.folio_put_post(declare_lost_url, data, "POST", i18n.t("Declare item as lost")):
596
- 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
+ )
597
723
  else:
598
724
  logging.error(f"Unsuccessfully declared loan {folio_loan} as lost")
599
- 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
+ )
600
728
 
601
729
  def claim_returned(self, folio_loan):
602
- 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
+ )
603
733
  logging.debug(f"Claim returned url:{claim_returned_url}")
604
734
  due_date = du_parser.isoparse(folio_loan["dueDate"])
605
735
  data = {
606
- "itemClaimedReturnedDateTime": datetime.isoformat(due_date + timedelta(days=1)),
736
+ "itemClaimedReturnedDateTime": datetime.isoformat(
737
+ due_date + timedelta(days=1)
738
+ ),
607
739
  "comment": "Created at migration. Date is due date + 1 day",
608
740
  }
609
741
  logging.debug(f"Claim returned data:\t{json.dumps(data)}")
610
- 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
+ ):
611
745
  self.migration_report.add(
612
746
  "Details", i18n.t("Successfully declared loan as Claimed returned")
613
747
  )
614
748
  else:
615
- 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
+ )
616
752
  self.migration_report.add(
617
753
  "Details",
618
754
  i18n.t(
619
- "Unsuccessfully declared loan %{loan} as Claimed returned", loan=folio_loan
755
+ "Unsuccessfully declared loan %{loan} as Claimed returned",
756
+ loan=folio_loan,
620
757
  ),
621
758
  )
622
759
 
623
760
  def set_item_status(self, legacy_loan: LegacyLoan):
624
761
  try:
625
762
  # Get Item by barcode, update status.
626
- 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
+ )
627
766
  item_url = f"{self.folio_client.gateway_url}/{item_path}"
628
- 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
+ )
629
770
  resp.raise_for_status()
630
771
  data = resp.json()
631
772
  folio_item = data["items"][0]
@@ -675,11 +816,11 @@ class LoansMigrator(MigrationTaskBase):
675
816
  self.migration_report.add("Details", i18n.t("Successfully deactivated user"))
676
817
 
677
818
  def update_item(self, item):
678
- url = f'/item-storage/items/{item["id"]}'
819
+ url = f"/item-storage/items/{item['id']}"
679
820
  return self.folio_put_post(url, item, "PUT", i18n.t("Update item"))
680
821
 
681
822
  def update_user(self, user):
682
- url = f'/users/{user["id"]}'
823
+ url = f"/users/{user['id']}"
683
824
  self.folio_put_post(url, user, "PUT", i18n.t("Update user"))
684
825
 
685
826
  def get_user_by_barcode(self, barcode):
@@ -746,7 +887,9 @@ class LoansMigrator(MigrationTaskBase):
746
887
  try:
747
888
  api_path = f"{folio_loan['id']}/change-due-date"
748
889
  api_url = f"{self.folio_client.gateway_url}/circulation/loans/{api_path}"
749
- 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
+ }
750
893
  req = self.http_client.post(
751
894
  api_url, headers=self.folio_client.okapi_headers, json=body
752
895
  )
@@ -762,12 +905,14 @@ class LoansMigrator(MigrationTaskBase):
762
905
  return False
763
906
  elif req.status_code == 201:
764
907
  self.migration_report.add(
765
- "Details", i18n.t("Successfully changed due date") + f" ({req.status_code})"
908
+ "Details",
909
+ i18n.t("Successfully changed due date") + f" ({req.status_code})",
766
910
  )
767
911
  return True, json.loads(req.text), None
768
912
  elif req.status_code == 204:
769
913
  self.migration_report.add(
770
- "Details", i18n.t("Successfully changed due date") + f" ({req.status_code})"
914
+ "Details",
915
+ i18n.t("Successfully changed due date") + f" ({req.status_code})",
771
916
  )
772
917
  return True, None, None
773
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",
@@ -29,11 +29,13 @@ class LegacyLoan(object):
29
29
  "patron_barcode",
30
30
  "due_date",
31
31
  "out_date",
32
+ ]
33
+ optional_headers = [
34
+ "service_point_id",
35
+ "proxy_patron_barcode",
32
36
  "renewal_count",
33
37
  "next_item_status",
34
- "service_point_id",
35
38
  ]
36
- optional_headers = ["service_point_id", "proxy_patron_barcode"]
37
39
  legal_statuses = [
38
40
  "",
39
41
  "Aged to lost",
@@ -72,7 +74,9 @@ class LegacyLoan(object):
72
74
  )
73
75
  except (ParserError, OverflowError) as ee:
74
76
  logging.error(ee)
75
- self.errors.append((f"Parse date failure in {row=}. Setting UTC NOW", "due_date"))
77
+ self.errors.append(
78
+ (f"Parse date failure in {row=}. Setting UTC NOW", "due_date")
79
+ )
76
80
  temp_date_due = datetime.now(ZoneInfo("UTC"))
77
81
  try:
78
82
  temp_date_out: datetime = parse(self.legacy_loan_dict["out_date"])
@@ -86,18 +90,24 @@ class LegacyLoan(object):
86
90
  temp_date_out = datetime.now(
87
91
  ZoneInfo("UTC")
88
92
  ) # TODO: Consider moving this assignment block above the temp_date_due
89
- self.errors.append((f"Parse date failure in {row=}. Setting UTC NOW", "out_date"))
93
+ self.errors.append(
94
+ (f"Parse date failure in {row=}. Setting UTC NOW", "out_date")
95
+ )
90
96
 
91
97
  # good to go, set properties
92
98
  self.item_barcode: str = self.legacy_loan_dict["item_barcode"].strip()
93
99
  self.patron_barcode: str = self.legacy_loan_dict["patron_barcode"].strip()
94
- self.proxy_patron_barcode: str = self.legacy_loan_dict.get("proxy_patron_barcode", "")
100
+ self.proxy_patron_barcode: str = self.legacy_loan_dict.get(
101
+ "proxy_patron_barcode", ""
102
+ )
95
103
  self.due_date: datetime = temp_date_due
96
104
  self.out_date: datetime = temp_date_out
97
105
  self.correct_for_1_day_loans()
98
106
  self.make_utc()
99
107
  self.renewal_count = self.set_renewal_count(self.legacy_loan_dict)
100
- self.next_item_status = self.legacy_loan_dict.get("next_item_status", "").strip()
108
+ self.next_item_status = self.legacy_loan_dict.get(
109
+ "next_item_status", ""
110
+ ).strip()
101
111
  if self.next_item_status not in legal_statuses:
102
112
  self.errors.append((f"Not an allowed status {row=}", self.next_item_status))
103
113
  self.service_point_id = (
@@ -112,9 +122,11 @@ class LegacyLoan(object):
112
122
  try:
113
123
  return int(renewal_count)
114
124
  except ValueError:
115
- self.report(f"Unresolvable {renewal_count=} was replaced with 0.")
125
+ self.report(
126
+ i18n.t("Unresolvable %{renewal_count=} was replaced with 0.")
127
+ )
116
128
  else:
117
- self.report(f"Missing renewal count was replaced with 0.")
129
+ self.report(i18n.t("Missing renewal count was replaced with 0."))
118
130
  return 0
119
131
 
120
132
  def correct_for_1_day_loans(self):
@@ -129,17 +141,19 @@ class LegacyLoan(object):
129
141
  i18n.t(
130
142
  "Due date is before out date, or date information is missing from both"
131
143
  ),
132
- json.dumps(self.legacy_loan_dict, indent=2)
144
+ json.dumps(self.legacy_loan_dict, indent=2),
133
145
  )
134
146
 
135
147
  def to_dict(self):
136
148
  return {
137
149
  "item_barcode": self.item_barcode,
138
150
  "patron_barcode": self.patron_barcode,
151
+ "proxy_patron_barcode": self.proxy_patron_barcode,
139
152
  "due_date": self.due_date.isoformat(),
140
153
  "out_date": self.out_date.isoformat(),
141
154
  "renewal_count": self.renewal_count,
142
155
  "next_item_status": self.next_item_status,
156
+ "service_point_id": self.service_point_id,
143
157
  }
144
158
 
145
159
  def make_utc(self):
@@ -6,6 +6,7 @@
6
6
  "%{field} a, x and z are missing or empty": "%{field} a, x and z are missing or empty",
7
7
  "%{field} subfields a, x, and z missing from field": "%{field} subfields a, x, and z missing from field",
8
8
  "%{fro} mapped from %{record}": "%{fro} mapped from %{record}",
9
+ "%{lost_type} and checked out": "%{lost_type} and checked out",
9
10
  "%{props} were concatenated": "%{props} were concatenated",
10
11
  "%{schema_value} added to %{prop_name}": "%{schema_value} added to %{prop_name}",
11
12
  "%{tag} subfield %{subfield} not in field": "%{tag} subfield %{subfield} not in field",
@@ -36,6 +37,8 @@
36
37
  "Check mapping file against the schema.": "Check mapping file against the schema.",
37
38
  "Checked out on first try": "Checked out on first try",
38
39
  "Checked out on second try": "Checked out on second try",
40
+ "Claim item returned": "Claim item returned",
41
+ "Claimed returned and checked out": "Claimed returned and checked out",
39
42
  "Click to expand all %{count} things": {
40
43
  "many": "Click to expand all %{count} things",
41
44
  "one": "Click to expand one thing"
@@ -57,6 +60,7 @@
57
60
  "DATA ISSUE Users not in FOLIO": "DATA ISSUE Users not in FOLIO",
58
61
  "Data issue. Consider fixing the record. ": "Data issue. Consider fixing the record. ",
59
62
  "Declare item as lost": "Declare item as lost",
63
+ "Declared lost and checked out": "Declared lost and checked out",
60
64
  "Discarded reserves": "Discarded reserves",
61
65
  "Due date is before out date, or date information is missing from both": "Due date is before out date, or date information is missing from both",
62
66
  "Duplicate 001. Creating HRID instead.\n Previous 001 will be stored in a new 035 field": "Duplicate 001. Creating HRID instead.\n Previous 001 will be stored in a new 035 field",
@@ -88,6 +92,7 @@
88
92
  "Instances HRID starting number": "Instances HRID starting number",
89
93
  "Instances linked using instances_id_map": "Instances linked using instances_id_map",
90
94
  "Interfaces": "Interfaces",
95
+ "Invalid specific retention policy in 008/13-15: %{value}": "Invalid specific retention policy in 008/13-15: %{value}",
91
96
  "Inventory records written to disk": "Inventory records written to disk",
92
97
  "Item lookups performed": "Item lookups performed",
93
98
  "Item transformation report": "Item transformation report",
@@ -95,6 +100,7 @@
95
100
  "Legacy Field": "Legacy Field",
96
101
  "Legacy bib records without 001": "Legacy bib records without 001",
97
102
  "Legacy id is empty": "Legacy id is empty",
103
+ "Loan already exists for %{item_barcode}": "Loan already exists for %{item_barcode}",
98
104
  "Loan already in failed.": "Loan already in failed.",
99
105
  "Loans discarded. Had migrated item barcode": "Loans discarded. Had migrated item barcode",
100
106
  "Loans failed pre-validation": "Loans failed pre-validation",
@@ -176,6 +182,7 @@
176
182
  "Reserve discarded. Could not find migrated barcode": "Reserve discarded. Could not find migrated barcode",
177
183
  "Reserve verified against migrated item": "Reserve verified against migrated item",
178
184
  "Reserves migration report": "Reserves migration report",
185
+ "Retention policy 6 indicates a limited period. Specific retention period will be mapped from 008/13-15": "Retention policy 6 indicates a limited period. Specific retention period will be mapped from 008/13-15",
179
186
  "Rows merged to create Purchase Orders": "Rows merged to create Purchase Orders",
180
187
  "SRS records written to disk": "SRS records written to disk",
181
188
  "Second failure": "Second failure",
@@ -186,6 +193,7 @@
186
193
  "Set leader 11 (Subfield code count) from %{record} to 2": "Set leader 11 (Subfield code count) from %{record} to 2",
187
194
  "Set leader 20-23 from %{field} to 4500": "Set leader 20-23 from %{field} to 4500",
188
195
  "Set up statistical code id mapping...": "Set up statistical code id mapping...",
196
+ "Setting item %{item_barcode} to status \"Available\"": "Setting item %{item_barcode} to status \"Available\"",
189
197
  "Source digits": "Source digits",
190
198
  "Source of heading or term": "Source of heading or term",
191
199
  "Staff suppressed": "Staff suppressed",
@@ -201,6 +209,8 @@
201
209
  "Successful matching on %{criteria}": "Successful matching on %{criteria}",
202
210
  "Successful user transformations": "Successful user transformations",
203
211
  "Successfully %{action}": "Successfully %{action}",
212
+ "Successfully Checked out %{lost_type} item and put the status back": "Successfully Checked out %{lost_type} item and put the status back",
213
+ "Successfully Checked out %{lost_type} item. Item will be declared lost.": "Successfully Checked out %{lost_type} item. Item will be declared lost.",
204
214
  "Successfully activated user": "Successfully activated user",
205
215
  "Successfully changed due date": "Successfully changed due date",
206
216
  "Successfully checked out": "Successfully checked out",
@@ -280,6 +290,8 @@
280
290
  "blurbs.Details.title": "Details",
281
291
  "blurbs.DiffsBetweenOrders.description": "This is a technical report that helps you to identify differences in the mapped order fields. ",
282
292
  "blurbs.DiffsBetweenOrders.title": "Differences between generated orders with same Legacy Identifier",
293
+ "blurbs.DigitizationPolicyMapping.description": "Digitization policies mapped from `008[21]` (LoC documentation)[https://www.loc.gov/marc/holdings/hd008.html]",
294
+ "blurbs.DigitizationPolicyMapping.title": "Digitization policy",
283
295
  "blurbs.DiscardedLoans.description": "List of loans discarded for various resons",
284
296
  "blurbs.DiscardedLoans.title": "Discarded loans",
285
297
  "blurbs.DiscardedRequests.description": "List of requests discarded for various resons",
@@ -320,6 +332,8 @@
320
332
  "blurbs.HoldingsTypeMapping.title": "Holdings type mapping",
321
333
  "blurbs.HridHandling.description": "There are two ways of handling HRIDs. The default behaviour is to take the current 001 and move that to a new 035. This will also emerge as an Identifier on the Inventory Instances. The 001 and Instance HRID will be generated from the HRID settings in FOLIO. The second option is to maintain the 001s in the records, and also add this as the Instance HRID",
322
334
  "blurbs.HridHandling.title": "HRID and 001/035 handling",
335
+ "blurbs.ILLPolicyMapping.description": "ILL policies mapped from `008[20]` (LoC documentation)[https://www.loc.gov/marc/holdings/hd008.html]",
336
+ "blurbs.ILLPolicyMapping.title": "ILL policy",
323
337
  "blurbs.IncompleteEntityMapping.description": "**NO ACTION REQUIRED** <br/>This is a coding anomaly that FSE will look into. <br/>Usually, the library does not have to do anything about it.<br/> One thing to look at is if there are many repeated subfields or unexpected patterns of subfields in the table.",
324
338
  "blurbs.IncompleteEntityMapping.title": "Incomplete entity mapping adding entity",
325
339
  "blurbs.IncompleteSubPropertyRemoved.description": "Add the missing required information to the record in your current ILS to ensure that it can be migrated over.",
@@ -376,6 +390,8 @@
376
390
  "blurbs.MatchedModesOfIssuanceCode.title": "Matched Modes of issuance code",
377
391
  "blurbs.MaterialTypeMapping.description": "",
378
392
  "blurbs.MaterialTypeMapping.title": "Mapped Material Types",
393
+ "blurbs.MethodOfAcquisitionMapping.description": "Acquisition methods mapped from `008[7]` (LoC documentation)[https://www.loc.gov/marc/holdings/hd008.html]",
394
+ "blurbs.MethodOfAcquisitionMapping.title": "Method of acquisition",
379
395
  "blurbs.MissingInstanceTypeIds.description": "**IC ACTION REQUIRED** These reords should get an instance type ID mapped from 336, or a default of Undefined, or they will not be transformed.",
380
396
  "blurbs.MissingInstanceTypeIds.title": "Records without Instance Type Ids",
381
397
  "blurbs.MissingRequiredProperties.description": "",
@@ -408,6 +424,8 @@
408
424
  "blurbs.RecourceTypeMapping.title": "Resource Type Mapping (336)",
409
425
  "blurbs.ReferenceDataMapping.description": "",
410
426
  "blurbs.ReferenceDataMapping.title": "Reference Data Mapping",
427
+ "blurbs.RetentionPolicyMapping.description": "Retention policies mapped from `008[12-15]` (LoC documentation)[https://www.loc.gov/marc/holdings/hd008.html]",
428
+ "blurbs.RetentionPolicyMapping.title": "Retention policy",
411
429
  "blurbs.Section1.description": "This entries below seem to be related to instances",
412
430
  "blurbs.Section1.title": "__Section 1: instances",
413
431
  "blurbs.Section2.description": "The entries below seem to be related to holdings",
@@ -446,15 +464,6 @@
446
464
  "blurbs.ValueSetInMappingFile.title": "Value set in mapping file",
447
465
  "blurbs.ValuesMappedFromLegacyFields.description": "A list fo the values and what they were mapped to",
448
466
  "blurbs.ValuesMappedFromLegacyFields.title": "Values mapped from legacy fields",
449
- "blurbs.MethodOfAcquisitionMapping.title": "Method of acquisition",
450
- "blurbs.MethodOfAcquisitionMapping.description": "Acquisition methods mapped from `008[7]` (LoC documentation)[https://www.loc.gov/marc/holdings/hd008.html]",
451
- "blurbs.RetentionPolicyMapping.title": "Retention policy",
452
- "blurbs.RetentionPolicyMapping.description": "Retention policies mapped from `008[12-15]` (LoC documentation)[https://www.loc.gov/marc/holdings/hd008.html]",
453
- "blurbs.ILLPolicyMapping.title": "ILL policy",
454
- "blurbs.ILLPolicyMapping.description": "ILL policies mapped from `008[20]` (LoC documentation)[https://www.loc.gov/marc/holdings/hd008.html]",
455
- "blurbs.DigitizationPolicyMapping.title": "Digitization policy",
456
- "blurbs.DigitizationPolicyMapping.description": "Digitization policies mapped from `008[21]` (LoC documentation)[https://www.loc.gov/marc/holdings/hd008.html]",
457
- "Invalid specific retention policy in 008/13-15: %{value}": "Invalid specific retention policy in 008/13-15: %{value}",
458
467
  "created": "created",
459
468
  "instance type code (%{code}) not found in FOLIO": "instance type code (%{code}) not found in FOLIO",
460
469
  "item barcode": "item barcode",
@@ -462,4 +471,4 @@
462
471
  "naturalId mapped from %{fro}": "naturalId mapped from %{fro}",
463
472
  "no matching identifier_types in %{names}": "no matching identifier_types in %{names}",
464
473
  "subfield present in %{linked_value_tag} but not in %{pattern_field}": "subfield present in %{linked_value_tag} but not in %{pattern_field}"
465
- }
474
+ }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: folio_migration_tools
3
- Version: 1.9.3
3
+ Version: 1.9.4
4
4
  Summary: A tool allowing you to migrate data from legacy ILS:s (Library systems) into FOLIO LSP
5
5
  License: MIT
6
6
  Keywords: FOLIO,ILS,LSP,Library Systems,MARC21,Library data
@@ -19,6 +19,7 @@ Requires-Dist: argparse-prompt (>=0.0.5,<0.0.6)
19
19
  Requires-Dist: art (>=6.5,<7.0)
20
20
  Requires-Dist: deepdiff (>=6.2.3,<7.0.0)
21
21
  Requires-Dist: defusedxml (>=0.7.1,<0.8.0)
22
+ Requires-Dist: folio-data-import (>=0.3.2,<0.4.0)
22
23
  Requires-Dist: folio-uuid (>=0.2.8,<0.3.0)
23
24
  Requires-Dist: folioclient (>=0.70.1,<0.71.0)
24
25
  Requires-Dist: pyaml (>=21.10.1,<22.0.0)
@@ -32,22 +32,22 @@ folio_migration_tools/marc_rules_transformation/marc_file_processor.py,sha256=Qh
32
32
  folio_migration_tools/marc_rules_transformation/marc_reader_wrapper.py,sha256=9ATjYMRAjy0QcXtmNZaHVhHLJ5hE1WUgOcF6KMJjbgo,5309
33
33
  folio_migration_tools/marc_rules_transformation/rules_mapper_authorities.py,sha256=PGt2w8h2pj8_8sGjQe3L-odFDlquURtKnoNFRWQB3GI,9621
34
34
  folio_migration_tools/marc_rules_transformation/rules_mapper_base.py,sha256=loNZ9gEYaAwjkP2_wLlXGedjWvSdHoGF_oJN9g6gI3s,45928
35
- folio_migration_tools/marc_rules_transformation/rules_mapper_bibs.py,sha256=GYZmVrEKcHkOEH4U3027-vQjS6mfMbk84GJTqiVrD4E,30350
36
- folio_migration_tools/marc_rules_transformation/rules_mapper_holdings.py,sha256=wT9HDodIRYeGbjutVHDHpSBVWrXsuA2LO8e_MmBMmzE,28498
35
+ folio_migration_tools/marc_rules_transformation/rules_mapper_bibs.py,sha256=RD46EzS0NQArn5LCGbrxDm9vbbW9PO_6iNUQwJBAbSg,30364
36
+ folio_migration_tools/marc_rules_transformation/rules_mapper_holdings.py,sha256=ZgyDxmNE7LwW8Cd55wRIEE-u6iyMKCRRXdq2ZRjm2nc,28779
37
37
  folio_migration_tools/migration_report.py,sha256=BkRspM1hwTBnWeqsHamf7yVEofzLj560Q-9G--O00hw,4258
38
38
  folio_migration_tools/migration_tasks/__init__.py,sha256=ZkbY_yGyB84Ke8OMlYUzyyBj4cxxNrhMTwQlu_GbdDs,211
39
39
  folio_migration_tools/migration_tasks/authority_transformer.py,sha256=AoXg9s-GLO3yEEDCrQV7hc4YVXxwxsdxDdpj1zhHydE,4251
40
- folio_migration_tools/migration_tasks/batch_poster.py,sha256=xN1BBZNGW2lZHWPznF6nkYV15XGhwwzcZccCzTbPfA4,40868
40
+ folio_migration_tools/migration_tasks/batch_poster.py,sha256=7gH9KSdtTSbPIS3eXK6_JBi0OshUAupV8AEew9QBSoU,45327
41
41
  folio_migration_tools/migration_tasks/bibs_transformer.py,sha256=46d44pcDAodFXDYbrTCMRASISbDciXmA0CXYfhP2IaE,6298
42
42
  folio_migration_tools/migration_tasks/courses_migrator.py,sha256=CzXnsu-KGP7B4zcINJzLYUqz47D16NuFfzu_DPqRlTQ,7061
43
43
  folio_migration_tools/migration_tasks/holdings_csv_transformer.py,sha256=kMhtHE8DJjA4d6kXBcfflueha3R3nwlBQjdec8CaY8c,21926
44
44
  folio_migration_tools/migration_tasks/holdings_marc_transformer.py,sha256=c_ruhOgidyJdSnnRwWUs3wwFMiLqbVMPOhhCaYuH_TI,14343
45
- folio_migration_tools/migration_tasks/items_transformer.py,sha256=HlTzV7K0AiGBHw56VMascupMKXG0Pv8LS65O9EiQ2VU,19637
46
- folio_migration_tools/migration_tasks/loans_migrator.py,sha256=_7yZH951p5mhLjbyH1r496DG591dD1tg_mmTtHas62o,35316
45
+ folio_migration_tools/migration_tasks/items_transformer.py,sha256=oTbFX2saF7-ZCb1mO3baLvODnBSEbbN5F_GtSth3iG4,19755
46
+ folio_migration_tools/migration_tasks/loans_migrator.py,sha256=9DQf58CZpgTM1m_miZTzsfVOGBWLsHrSFalQBO7g9DE,39406
47
47
  folio_migration_tools/migration_tasks/manual_fee_fines_transformer.py,sha256=CnmlTge7nChUJ10EiUkriQtJlVxWqglgfhjgneh2_yM,7247
48
48
  folio_migration_tools/migration_tasks/migration_task_base.py,sha256=Q-57h6rmt74bC9LidA9ZoagEcwVd_ytq8IUWelVOm2E,22521
49
49
  folio_migration_tools/migration_tasks/orders_transformer.py,sha256=6SnzU_rUTu2B5hQykI2nRA7vI1rg-uxuF9Ncupe0AEY,14302
50
- folio_migration_tools/migration_tasks/organization_transformer.py,sha256=vcCjhN1sS55c_a0LXi1Yw1eq3zpDn5E4BGbm2zDQ_Z4,16885
50
+ folio_migration_tools/migration_tasks/organization_transformer.py,sha256=Kuxkh1sKyUVBqm5qAK1Jrq-4xcyNz2JPZvvFRqfwI8s,16922
51
51
  folio_migration_tools/migration_tasks/requests_migrator.py,sha256=QP9OBezC3FfcKpI78oMmydxcPaUIYAgHyKevyLwC-WQ,14841
52
52
  folio_migration_tools/migration_tasks/reserves_migrator.py,sha256=4sSPer6_6yMwiiY1VYJmYZske_Ah1XG4KAM3NDadPhg,9952
53
53
  folio_migration_tools/migration_tasks/user_transformer.py,sha256=aylrMC9n47fdStgsNfW4ZbJh2E4FDSPypsaNv52ynKc,12330
@@ -55,13 +55,13 @@ folio_migration_tools/task_configuration.py,sha256=73OWc8TX--fwPRptv3eQVEVv0-XmN
55
55
  folio_migration_tools/test_infrastructure/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
56
56
  folio_migration_tools/test_infrastructure/mocked_classes.py,sha256=BurU3NGU_Q8as_BGmW98q9O6bujZDkOfFmvKKdVw9t8,15056
57
57
  folio_migration_tools/transaction_migration/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
58
- folio_migration_tools/transaction_migration/legacy_loan.py,sha256=phd9oO6xd91qC4ilRq3podZ-rKIIwQ01SXe0JxbZAbQ,6339
58
+ folio_migration_tools/transaction_migration/legacy_loan.py,sha256=2GHqHDVShdZuoUlW2yvePcsesJZ5-Huq3leD71tNtaE,6618
59
59
  folio_migration_tools/transaction_migration/legacy_request.py,sha256=1ulyFzPQw_InOjyPzkWpGnNptgXdQ18nmri0J8Nlpkc,6124
60
60
  folio_migration_tools/transaction_migration/legacy_reserve.py,sha256=qzw0okg4axAE_ezXopP9gFsQ_e60o0zh7zqRzFBSWHY,1806
61
61
  folio_migration_tools/transaction_migration/transaction_result.py,sha256=cTdCN0BnlI9_ZJB2Z3Fdkl9gpymIi-9mGZsRFlQcmDk,656
62
- folio_migration_tools/translations/en.json,sha256=TPQRTDdvdkZI2iHczP4hKmFEbd7Hyo5BE37uSo54W_4,40691
63
- folio_migration_tools-1.9.3.dist-info/LICENSE,sha256=PhIEkitVi3ejgq56tt6sWoJIG_zmv82cjjd_aYPPGdI,1072
64
- folio_migration_tools-1.9.3.dist-info/METADATA,sha256=dPvDnsZ0qw3K6pPfHatlCPfSCov_7d8Ll7L2pJYSta8,7444
65
- folio_migration_tools-1.9.3.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
66
- folio_migration_tools-1.9.3.dist-info/entry_points.txt,sha256=Hbe-HjqMcU8FwVshVIkeWyZd9XwgT1CCMNf06EpHQu8,77
67
- folio_migration_tools-1.9.3.dist-info/RECORD,,
62
+ folio_migration_tools/translations/en.json,sha256=6IpYYNFCtQoXACndPM0d1Oa25GYuaF-G-b4YpzTjQH0,41656
63
+ folio_migration_tools-1.9.4.dist-info/LICENSE,sha256=PhIEkitVi3ejgq56tt6sWoJIG_zmv82cjjd_aYPPGdI,1072
64
+ folio_migration_tools-1.9.4.dist-info/METADATA,sha256=L36bpdFWAhsV2r8FBIjpZ-1N4Vl0u4EMV0f7g5ObNgg,7494
65
+ folio_migration_tools-1.9.4.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
66
+ folio_migration_tools-1.9.4.dist-info/entry_points.txt,sha256=Hbe-HjqMcU8FwVshVIkeWyZd9XwgT1CCMNf06EpHQu8,77
67
+ folio_migration_tools-1.9.4.dist-info/RECORD,,