folio-migration-tools 1.9.0rc11__py3-none-any.whl → 1.9.0rc13__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 (31) hide show
  1. folio_migration_tools/__main__.py +1 -2
  2. folio_migration_tools/library_configuration.py +21 -1
  3. folio_migration_tools/mapper_base.py +78 -4
  4. folio_migration_tools/mapping_file_transformation/courses_mapper.py +2 -1
  5. folio_migration_tools/mapping_file_transformation/holdings_mapper.py +8 -4
  6. folio_migration_tools/mapping_file_transformation/item_mapper.py +4 -11
  7. folio_migration_tools/mapping_file_transformation/manual_fee_fines_mapper.py +1 -0
  8. folio_migration_tools/mapping_file_transformation/mapping_file_mapper_base.py +3 -19
  9. folio_migration_tools/mapping_file_transformation/notes_mapper.py +2 -0
  10. folio_migration_tools/mapping_file_transformation/order_mapper.py +4 -1
  11. folio_migration_tools/mapping_file_transformation/organization_mapper.py +7 -4
  12. folio_migration_tools/mapping_file_transformation/user_mapper.py +3 -1
  13. folio_migration_tools/marc_rules_transformation/holdings_statementsparser.py +7 -14
  14. folio_migration_tools/marc_rules_transformation/rules_mapper_authorities.py +1 -0
  15. folio_migration_tools/marc_rules_transformation/rules_mapper_base.py +83 -4
  16. folio_migration_tools/marc_rules_transformation/rules_mapper_bibs.py +10 -5
  17. folio_migration_tools/marc_rules_transformation/rules_mapper_holdings.py +39 -33
  18. folio_migration_tools/migration_tasks/bibs_transformer.py +13 -3
  19. folio_migration_tools/migration_tasks/holdings_csv_transformer.py +42 -21
  20. folio_migration_tools/migration_tasks/holdings_marc_transformer.py +39 -22
  21. folio_migration_tools/migration_tasks/items_transformer.py +4 -3
  22. folio_migration_tools/migration_tasks/migration_task_base.py +22 -1
  23. folio_migration_tools/migration_tasks/orders_transformer.py +2 -0
  24. folio_migration_tools/migration_tasks/user_transformer.py +1 -0
  25. folio_migration_tools/transaction_migration/legacy_loan.py +2 -1
  26. folio_migration_tools/translations/en.json +12 -1
  27. {folio_migration_tools-1.9.0rc11.dist-info → folio_migration_tools-1.9.0rc13.dist-info}/METADATA +2 -2
  28. {folio_migration_tools-1.9.0rc11.dist-info → folio_migration_tools-1.9.0rc13.dist-info}/RECORD +31 -31
  29. {folio_migration_tools-1.9.0rc11.dist-info → folio_migration_tools-1.9.0rc13.dist-info}/WHEEL +1 -1
  30. {folio_migration_tools-1.9.0rc11.dist-info → folio_migration_tools-1.9.0rc13.dist-info}/LICENSE +0 -0
  31. {folio_migration_tools-1.9.0rc11.dist-info → folio_migration_tools-1.9.0rc13.dist-info}/entry_points.txt +0 -0
@@ -10,7 +10,6 @@ from pathlib import Path
10
10
  from typing import Dict, Generator, List
11
11
 
12
12
  import i18n
13
- import pymarc
14
13
  from defusedxml.ElementTree import fromstring
15
14
  from folio_uuid.folio_namespaces import FOLIONamespaces
16
15
  from folio_uuid.folio_uuid import FolioUUID
@@ -45,11 +44,13 @@ class BibsRulesMapper(RulesMapperBase):
45
44
  folio_client: FolioClient,
46
45
  library_configuration: LibraryConfiguration,
47
46
  task_configuration: MarcTaskConfigurationBase,
47
+ statistical_codes_map: Dict[str, str] = None,
48
48
  ):
49
49
  super().__init__(
50
50
  folio_client,
51
51
  library_configuration,
52
52
  task_configuration,
53
+ statistical_codes_map,
53
54
  self.get_instance_schema(folio_client),
54
55
  Conditions(folio_client, self, "bibs", library_configuration.folio_release),
55
56
  )
@@ -144,7 +145,7 @@ class BibsRulesMapper(RulesMapperBase):
144
145
  self.report_folio_mapping(clean_folio_instance, self.schema)
145
146
  return [clean_folio_instance]
146
147
 
147
- def simple_bib_map(self, folio_instnace: dict, marc_record: Record, ignored_subsequent_fields: set, legacy_ids: List[str]):
148
+ def simple_bib_map(self, folio_instance: dict, marc_record: Record, ignored_subsequent_fields: set, legacy_ids: List[str]):
148
149
  """
149
150
  This method applies a much simplified MARC-to-instance
150
151
  mapping to create a minimal FOLIO Instance record to be
@@ -152,7 +153,7 @@ class BibsRulesMapper(RulesMapperBase):
152
153
  than creating SRS records during transformation.
153
154
 
154
155
  Args:
155
- folio_instnace (dict): _description_
156
+ folio_instance (dict): _description_
156
157
  marc_record (Record): _description_
157
158
  legacy_ids (List[str]): _description_
158
159
  file_def (FileDefinition): _description_
@@ -169,9 +170,10 @@ class BibsRulesMapper(RulesMapperBase):
169
170
  if not main_entry_fields:
170
171
  main_entry_fields += marc_record.get_fields("700", "710", "711", "730")
171
172
  main_entry_fields.sort(key=lambda x: int(x.tag))
172
- self.process_marc_field(folio_instnace, main_entry_fields[0], ignored_subsequent_fields, legacy_ids)
173
+ if main_entry_fields:
174
+ self.process_marc_field(folio_instance, main_entry_fields[0], ignored_subsequent_fields, legacy_ids)
173
175
  try:
174
- self.process_marc_field(folio_instnace, marc_record['245'], ignored_subsequent_fields, legacy_ids)
176
+ self.process_marc_field(folio_instance, marc_record['245'], ignored_subsequent_fields, legacy_ids)
175
177
  except KeyError:
176
178
  raise TransformationRecordFailedError(
177
179
  legacy_ids,
@@ -205,6 +207,9 @@ class BibsRulesMapper(RulesMapperBase):
205
207
  folio_instance["modeOfIssuanceId"] = self.get_mode_of_issuance_id(marc_record, legacy_ids)
206
208
  self.handle_languages(folio_instance, marc_record, legacy_ids)
207
209
  self.handle_suppression(folio_instance, file_def)
210
+ # Map statistical codes from MARC and FileDefinition, then map the IDs
211
+ self.map_statistical_codes(folio_instance, file_def, marc_record)
212
+ self.map_statistical_code_ids(legacy_ids, folio_instance)
208
213
  self.handle_holdings(marc_record)
209
214
  if prec_titles := folio_instance.get("precedingTitles", []):
210
215
  self.migration_report.add("PrecedingSuccedingTitles", f"{len(prec_titles)}")
@@ -7,6 +7,7 @@ import i18n
7
7
  from folio_uuid.folio_namespaces import FOLIONamespaces
8
8
  from folio_uuid.folio_uuid import FolioUUID
9
9
  from folioclient import FolioClient
10
+ from pymarc import Optional
10
11
  from pymarc.field import Field
11
12
  from pymarc.record import Record
12
13
 
@@ -39,27 +40,28 @@ class RulesMapperHoldings(RulesMapperBase):
39
40
  task_configuration,
40
41
  library_configuration: LibraryConfiguration,
41
42
  parent_id_map: dict,
42
- boundwith_relationship_map,
43
+ boundwith_relationship_map_rows: List[Dict],
44
+ statistical_codes_map: Optional[Dict] = None,
43
45
  ):
44
- self.task_configuration = task_configuration
45
46
  self.conditions = Conditions(
46
47
  folio_client,
47
48
  self,
48
49
  "holdings",
49
50
  library_configuration.folio_release,
50
- self.task_configuration.default_call_number_type_name,
51
+ task_configuration.default_call_number_type_name,
51
52
  )
52
53
  self.folio = folio_client
53
54
  super().__init__(
54
55
  folio_client,
55
56
  library_configuration,
56
57
  task_configuration,
58
+ statistical_codes_map,
57
59
  self.fetch_holdings_schema(folio_client),
58
60
  self.conditions,
59
61
  parent_id_map,
60
62
  )
61
- self.boundwith_relationship_map = self.setup_boundwith_relationship_map(
62
- boundwith_relationship_map
63
+ self.boundwith_relationship_map: Dict = self.setup_boundwith_relationship_map(
64
+ boundwith_relationship_map_rows
63
65
  )
64
66
  self.location_map = self.validate_location_map(
65
67
  location_map,
@@ -297,6 +299,10 @@ class RulesMapperHoldings(RulesMapperBase):
297
299
  "",
298
300
  )
299
301
  self.handle_suppression(folio_holding, file_def, True)
302
+ # First, map statistical codes from MARC fields and FileDefinitions to FOLIO statistical codes.
303
+ # Then, convert the mapped statistical codes to their corresponding code IDs.
304
+ self.map_statistical_codes(folio_holding, file_def, marc_record)
305
+ self.map_statistical_code_ids(legacy_ids, folio_holding)
300
306
  self.set_source_id(self.create_source_records, folio_holding, self.holdingssources, file_def)
301
307
 
302
308
  def pick_first_location_if_many(self, folio_holding: Dict, legacy_ids: List[str]):
@@ -402,20 +408,20 @@ class RulesMapperHoldings(RulesMapperBase):
402
408
  marc_record (Record): PyMARC record
403
409
  folio_holding (Dict): FOLIO holdings record
404
410
  """
405
- holdings_note_type_tuple = self.conditions.get_ref_data_tuple_by_name(
406
- self.folio.holding_note_types, "holding_note_types", self.task_configuration.mfhd_mrk_note_type
407
- )
408
- try:
409
- holdings_note_type_id = holdings_note_type_tuple[0]
410
- except Exception as ee:
411
- logging.error(ee)
412
- raise TransformationRecordFailedError(
413
- legacy_ids,
414
- f'Holdings note type mapping error.\tNote type name: {self.task_configuration.mfhd_mrk_note_type}\t'
415
- f"Note type not found in FOLIO.",
416
- self.task_configuration.mfhd_mrk_note_type,
417
- ) from ee
418
411
  if self.task_configuration.include_mfhd_mrk_as_note:
412
+ holdings_note_type_tuple = self.conditions.get_ref_data_tuple_by_name(
413
+ self.folio.holding_note_types, "holding_note_types", self.task_configuration.mfhd_mrk_note_type
414
+ )
415
+ try:
416
+ holdings_note_type_id = holdings_note_type_tuple[0]
417
+ except Exception as ee:
418
+ logging.error(ee)
419
+ raise TransformationRecordFailedError(
420
+ legacy_ids,
421
+ f'Holdings note type mapping error.\tNote type name: {self.task_configuration.mfhd_mrk_note_type}\t'
422
+ f"Note type not found in FOLIO.",
423
+ self.task_configuration.mfhd_mrk_note_type,
424
+ ) from ee
419
425
  folio_holding["notes"] = folio_holding.get("notes", []) + [
420
426
  {
421
427
  "note": str(marc_record),
@@ -433,20 +439,20 @@ class RulesMapperHoldings(RulesMapperBase):
433
439
  marc_record (Record): PyMARC record
434
440
  folio_holding (Dict): FOLIO holdings record
435
441
  """
436
- holdings_note_type_tuple = self.conditions.get_ref_data_tuple_by_name(
437
- self.folio.holding_note_types, "holding_note_types", self.task_configuration.mfhd_mrc_note_type
438
- )
439
- try:
440
- holdings_note_type_id = holdings_note_type_tuple[0]
441
- except Exception as ee:
442
- logging.error(ee)
443
- raise TransformationRecordFailedError(
444
- legacy_ids,
445
- f'Holdings note type mapping error.\tNote type name: {self.task_configuration.mfhd_mrc_note_type}\t'
446
- f"Note type not found in FOLIO.",
447
- self.task_configuration.mfhd_mrc_note_type,
448
- ) from ee
449
442
  if self.task_configuration.include_mfhd_mrc_as_note:
443
+ holdings_note_type_tuple = self.conditions.get_ref_data_tuple_by_name(
444
+ self.folio.holding_note_types, "holding_note_types", self.task_configuration.mfhd_mrc_note_type
445
+ )
446
+ try:
447
+ holdings_note_type_id = holdings_note_type_tuple[0]
448
+ except Exception as ee:
449
+ logging.error(ee)
450
+ raise TransformationRecordFailedError(
451
+ legacy_ids,
452
+ f'Holdings note type mapping error.\tNote type name: {self.task_configuration.mfhd_mrc_note_type}\t'
453
+ f"Note type not found in FOLIO.",
454
+ self.task_configuration.mfhd_mrc_note_type,
455
+ ) from ee
450
456
  folio_holding["notes"] = folio_holding.get("notes", []) + [
451
457
  {
452
458
  "note": marc_record.as_marc().decode("utf-8"),
@@ -582,7 +588,7 @@ class RulesMapperHoldings(RulesMapperBase):
582
588
  "", "Column BIB_ID missing from Boundwith relationship map", ""
583
589
  )
584
590
 
585
- def setup_boundwith_relationship_map(self, boundwith_relationship_map: List[Dict]):
591
+ def setup_boundwith_relationship_map(self, boundwith_relationship_map_list: List[Dict]):
586
592
  """
587
593
  Creates a map of MFHD_ID to BIB_ID for boundwith relationships.
588
594
 
@@ -597,7 +603,7 @@ class RulesMapperHoldings(RulesMapperBase):
597
603
  TransformationRecordFailedError: If BIB_ID is not in the instance id map.
598
604
  """
599
605
  new_map = {}
600
- for idx, entry in enumerate(boundwith_relationship_map):
606
+ for idx, entry in enumerate(boundwith_relationship_map_list):
601
607
  self.verity_boundwith_map_entry(entry)
602
608
  mfhd_uuid = str(
603
609
  FolioUUID(
@@ -7,8 +7,6 @@ from pydantic import Field
7
7
 
8
8
  from folio_migration_tools.helper import Helper
9
9
  from folio_migration_tools.library_configuration import (
10
- FileDefinition,
11
- HridHandling,
12
10
  IlsFlavour,
13
11
  LibraryConfiguration,
14
12
  )
@@ -116,11 +114,23 @@ class BibsTransformer(MigrationTaskBase):
116
114
  use_logging: bool = True,
117
115
  ):
118
116
  super().__init__(library_config, task_config, folio_client, use_logging)
117
+ self.task_config = task_config
118
+ self.task_configuration = self.task_config
119
+ if self.task_config.statistical_codes_map_file_name:
120
+ statcode_mapping = self.load_ref_data_mapping_file(
121
+ "statisticalCodeIds",
122
+ self.folder_structure.mapping_files_folder
123
+ / self.task_config.statistical_codes_map_file_name,
124
+ [],
125
+ False,
126
+ )
127
+ else:
128
+ statcode_mapping = None
119
129
  self.processor: MarcFileProcessor
120
130
  self.check_source_files(
121
131
  self.folder_structure.legacy_records_folder, self.task_configuration.files
122
132
  )
123
- self.mapper = BibsRulesMapper(self.folio_client, library_config, self.task_configuration)
133
+ self.mapper = BibsRulesMapper(self.folio_client, library_config, self.task_configuration, statcode_mapping)
124
134
  self.bib_ids: set = set()
125
135
  if (
126
136
  self.task_configuration.reset_hrid_settings
@@ -160,6 +160,16 @@ class HoldingsCsvTransformer(MigrationTaskBase):
160
160
  ),
161
161
  ),
162
162
  ] = True
163
+ statistical_codes_map_file_name: Annotated[
164
+ Optional[str],
165
+ Field(
166
+ title="Statistical code map file name",
167
+ description=(
168
+ "Path to the file containing the mapping of statistical codes. "
169
+ "The file should be in TSV format with legacy_stat_code and folio_code columns."
170
+ ),
171
+ ),
172
+ ] = ""
163
173
 
164
174
  @staticmethod
165
175
  def get_object_type() -> FOLIONamespaces:
@@ -174,16 +184,27 @@ class HoldingsCsvTransformer(MigrationTaskBase):
174
184
  ):
175
185
  super().__init__(library_config, task_config, folio_client, use_logging)
176
186
  self.fallback_holdings_type = None
187
+ self.folio_keys, self.holdings_field_map = self.load_mapped_fields()
188
+ if any(k for k in self.folio_keys if k.startswith("statisticalCodeIds")):
189
+ statcode_mapping = self.load_ref_data_mapping_file(
190
+ "statisticalCodeIds",
191
+ self.folder_structure.mapping_files_folder
192
+ / self.task_configuration.statistical_codes_map_file_name,
193
+ self.folio_keys,
194
+ False,
195
+ )
196
+ else:
197
+ statcode_mapping = None
177
198
  try:
178
- self.task_config = task_config
179
199
  self.bound_with_keys = set()
180
200
  self.mapper = HoldingsMapper(
181
201
  self.folio_client,
182
- self.load_mapped_fields(),
202
+ self.holdings_field_map,
183
203
  self.load_location_map(),
184
204
  self.load_call_number_type_map(),
185
205
  self.load_instance_id_map(True),
186
206
  library_config,
207
+ statcode_mapping,
187
208
  )
188
209
  self.holdings = {}
189
210
  self.total_records = 0
@@ -196,19 +217,19 @@ class HoldingsCsvTransformer(MigrationTaskBase):
196
217
  logging.info("%s\tholdings types in tenant", len(self.holdings_types))
197
218
  self.validate_merge_criterias()
198
219
  self.check_source_files(
199
- self.folder_structure.data_folder / "items", self.task_config.files
220
+ self.folder_structure.data_folder / "items", self.task_configuration.files
200
221
  )
201
222
  self.fallback_holdings_type = next(
202
223
  h
203
224
  for h in self.holdings_types
204
- if h["id"] == self.task_config.fallback_holdings_type_id
225
+ if h["id"] == self.task_configuration.fallback_holdings_type_id
205
226
  )
206
227
  if not self.fallback_holdings_type:
207
228
  raise TransformationProcessError(
208
229
  "",
209
230
  (
210
231
  "Holdings type with ID "
211
- f"{self.task_config.fallback_holdings_type_id} "
232
+ f"{self.task_configuration.fallback_holdings_type_id} "
212
233
  "not found in FOLIO."
213
234
  ),
214
235
  )
@@ -216,15 +237,15 @@ class HoldingsCsvTransformer(MigrationTaskBase):
216
237
  "%s will be used as default holdings type",
217
238
  self.fallback_holdings_type["name"],
218
239
  )
219
- if any(self.task_config.previously_generated_holdings_files):
220
- for file_name in self.task_config.previously_generated_holdings_files:
240
+ if any(self.task_configuration.previously_generated_holdings_files):
241
+ for file_name in self.task_configuration.previously_generated_holdings_files:
221
242
  logging.info("Processing %s", file_name)
222
243
  self.holdings.update(
223
244
  HoldingsHelper.load_previously_generated_holdings(
224
245
  self.folder_structure.results_folder / file_name,
225
- self.task_config.holdings_merge_criteria,
246
+ self.task_configuration.holdings_merge_criteria,
226
247
  self.mapper.migration_report,
227
- self.task_config.holdings_type_uuid_for_boundwiths,
248
+ self.task_configuration.holdings_type_uuid_for_boundwiths,
228
249
  )
229
250
  )
230
251
 
@@ -260,7 +281,7 @@ class HoldingsCsvTransformer(MigrationTaskBase):
260
281
  def load_call_number_type_map(self):
261
282
  with open(
262
283
  self.folder_structure.mapping_files_folder
263
- / self.task_config.call_number_type_map_file_name,
284
+ / self.task_configuration.call_number_type_map_file_name,
264
285
  "r",
265
286
  ) as callnumber_type_map_f:
266
287
  return self.load_ref_data_map_from_file(
@@ -269,7 +290,7 @@ class HoldingsCsvTransformer(MigrationTaskBase):
269
290
 
270
291
  def load_location_map(self):
271
292
  with open(
272
- self.folder_structure.mapping_files_folder / self.task_config.location_map_file_name
293
+ self.folder_structure.mapping_files_folder / self.task_configuration.location_map_file_name
273
294
  ) as location_map_f:
274
295
  return self.load_ref_data_map_from_file(
275
296
  location_map_f, "Found %s rows in location map"
@@ -283,7 +304,7 @@ class HoldingsCsvTransformer(MigrationTaskBase):
283
304
 
284
305
  def load_mapped_fields(self):
285
306
  with open(
286
- self.folder_structure.mapping_files_folder / self.task_config.holdings_map_file_name
307
+ self.folder_structure.mapping_files_folder / self.task_configuration.holdings_map_file_name
287
308
  ) as holdings_mapper_f:
288
309
  holdings_map = json.load(holdings_mapper_f)
289
310
  logging.info("%s fields in holdings mapping file map", len(holdings_map["data"]))
@@ -294,11 +315,11 @@ class HoldingsCsvTransformer(MigrationTaskBase):
294
315
  "%s mapped fields in holdings mapping file map",
295
316
  len(list(mapped_fields)),
296
317
  )
297
- return holdings_map
318
+ return mapped_fields, holdings_map
298
319
 
299
320
  def do_work(self):
300
321
  logging.info("Starting....")
301
- for file_def in self.task_config.files:
322
+ for file_def in self.task_configuration.files:
302
323
  logging.info("Processing %s", file_def.file_name)
303
324
  try:
304
325
  self.process_single_file(file_def)
@@ -311,7 +332,7 @@ class HoldingsCsvTransformer(MigrationTaskBase):
311
332
  print(f"\n{error_str}\nHalting")
312
333
  sys.exit(1)
313
334
  logging.info(
314
- f"processed {self.total_records:,} records in {len(self.task_config.files)} files"
335
+ f"processed {self.total_records:,} records in {len(self.task_configuration.files)} files"
315
336
  )
316
337
 
317
338
  def wrap_up(self):
@@ -357,8 +378,8 @@ class HoldingsCsvTransformer(MigrationTaskBase):
357
378
  holdings_schema = self.folio_client.get_holdings_schema()
358
379
  properties = holdings_schema["properties"].keys()
359
380
  logging.info(properties)
360
- logging.info(self.task_config.holdings_merge_criteria)
361
- res = [mc for mc in self.task_config.holdings_merge_criteria if mc not in properties]
381
+ logging.info(self.task_configuration.holdings_merge_criteria)
382
+ res = [mc for mc in self.task_configuration.holdings_merge_criteria if mc not in properties]
362
383
  if any(res):
363
384
  logging.critical(
364
385
  (
@@ -426,7 +447,7 @@ class HoldingsCsvTransformer(MigrationTaskBase):
426
447
  raise TransformationRecordFailedError(legacy_id, "No instance id in parsed record", "")
427
448
 
428
449
  for folio_holding in holdings_from_row:
429
- self.mapper.perform_additional_mappings(folio_holding, file_def)
450
+ self.mapper.perform_additional_mappings(legacy_id, folio_holding, file_def)
430
451
  self.merge_holding_in(folio_holding, all_instance_ids, legacy_id)
431
452
  self.mapper.report_folio_mapping(folio_holding, self.mapper.schema)
432
453
 
@@ -436,7 +457,7 @@ class HoldingsCsvTransformer(MigrationTaskBase):
436
457
  self.mapper.create_bound_with_holdings(
437
458
  folio_holding,
438
459
  folio_holding["instanceId"],
439
- self.task_config.holdings_type_uuid_for_boundwiths,
460
+ self.task_configuration.holdings_type_uuid_for_boundwiths,
440
461
  )
441
462
  )
442
463
 
@@ -480,9 +501,9 @@ class HoldingsCsvTransformer(MigrationTaskBase):
480
501
  # Regular holding. Merge according to criteria
481
502
  new_holding_key = HoldingsHelper.to_key(
482
503
  incoming_holding,
483
- self.task_config.holdings_merge_criteria,
504
+ self.task_configuration.holdings_merge_criteria,
484
505
  self.mapper.migration_report,
485
- self.task_config.holdings_type_uuid_for_boundwiths,
506
+ self.task_configuration.holdings_type_uuid_for_boundwiths,
486
507
  )
487
508
  if self.holdings.get(new_holding_key, None):
488
509
  self.mapper.migration_report.add_general_statistics(
@@ -216,7 +216,16 @@ class HoldingsMarcTransformer(MigrationTaskBase):
216
216
  ):
217
217
  csv.register_dialect("tsv", delimiter="\t")
218
218
  super().__init__(library_config, task_config, folio_client, use_logging)
219
- self.task_config = task_config
219
+ if self.task_configuration.statistical_codes_map_file_name:
220
+ statcode_mapping = self.load_ref_data_mapping_file(
221
+ "statisticalCodeIds",
222
+ self.folder_structure.mapping_files_folder
223
+ / self.task_configuration.statistical_codes_map_file_name,
224
+ [],
225
+ False,
226
+ )
227
+ else:
228
+ statcode_mapping = None
220
229
  self.holdings_types = list(
221
230
  self.folio_client.folio_get_all("/holdings-types", "holdingsTypes")
222
231
  )
@@ -224,7 +233,7 @@ class HoldingsMarcTransformer(MigrationTaskBase):
224
233
  (
225
234
  h
226
235
  for h in self.holdings_types
227
- if h["id"] == self.task_config.fallback_holdings_type_id
236
+ if h["id"] == self.task_configuration.fallback_holdings_type_id
228
237
  ),
229
238
  {"name": ""},
230
239
  )
@@ -232,7 +241,7 @@ class HoldingsMarcTransformer(MigrationTaskBase):
232
241
  raise TransformationProcessError(
233
242
  "",
234
243
  (
235
- f"Holdings type with ID {self.task_config.fallback_holdings_type_id}"
244
+ f"Holdings type with ID {self.task_configuration.fallback_holdings_type_id}"
236
245
  " not found in FOLIO."
237
246
  ),
238
247
  )
@@ -242,39 +251,47 @@ class HoldingsMarcTransformer(MigrationTaskBase):
242
251
  )
243
252
 
244
253
  # Load Boundwith relationship map
245
- self.boundwith_relationship_map = []
246
- if self.task_config.boundwith_relationship_file_path:
247
- with open(
248
- self.folder_structure.legacy_records_folder
249
- / self.task_config.boundwith_relationship_file_path
250
- ) as boundwith_relationship_file:
251
- self.boundwith_relationship_map = list(
252
- csv.DictReader(boundwith_relationship_file, dialect="tsv")
254
+ self.boundwith_relationship_map_rows = []
255
+ if self.task_configuration.boundwith_relationship_file_path:
256
+ try:
257
+ with open(
258
+ self.folder_structure.legacy_records_folder
259
+ / self.task_configuration.boundwith_relationship_file_path
260
+ ) as boundwith_relationship_file:
261
+ self.boundwith_relationship_map_rows = list(
262
+ csv.DictReader(boundwith_relationship_file, dialect="tsv")
263
+ )
264
+ logging.info(
265
+ "Rows in Bound with relationship map: %s",
266
+ len(self.boundwith_relationship_map_rows),
267
+ )
268
+ except FileNotFoundError:
269
+ raise TransformationProcessError(
270
+ "",
271
+ i18n.t("Provided boundwith relationship file not found"),
272
+ self.task_configuration.boundwith_relationship_file_path,
253
273
  )
254
- logging.info(
255
- "Rows in Bound with relationship map: %s",
256
- len(self.boundwith_relationship_map),
257
- )
258
274
 
259
275
  location_map_path = (
260
276
  self.folder_structure.mapping_files_folder
261
- / self.task_config.location_map_file_name
277
+ / self.task_configuration.location_map_file_name
262
278
  )
263
279
  with open(location_map_path) as location_map_file:
264
280
  self.location_map = list(csv.DictReader(location_map_file, dialect="tsv"))
265
281
  logging.info("Locations in map: %s", len(self.location_map))
266
282
 
267
283
  self.check_source_files(
268
- self.folder_structure.legacy_records_folder, self.task_config.files
284
+ self.folder_structure.legacy_records_folder, self.task_configuration.files
269
285
  )
270
286
  self.instance_id_map = self.load_instance_id_map(True)
271
287
  self.mapper = RulesMapperHoldings(
272
288
  self.folio_client,
273
289
  self.location_map,
274
- self.task_config,
290
+ self.task_configuration,
275
291
  self.library_configuration,
276
292
  self.instance_id_map,
277
- self.boundwith_relationship_map,
293
+ self.boundwith_relationship_map_rows,
294
+ statcode_mapping
278
295
  )
279
296
  self.add_supplemental_mfhd_mappings()
280
297
  if (
@@ -286,12 +303,12 @@ class HoldingsMarcTransformer(MigrationTaskBase):
286
303
  logging.info("Init done")
287
304
 
288
305
  def add_supplemental_mfhd_mappings(self):
289
- if self.task_config.supplemental_mfhd_mapping_rules_file:
306
+ if self.task_configuration.supplemental_mfhd_mapping_rules_file:
290
307
  try:
291
308
  with open(
292
309
  (
293
310
  self.folder_structure.mapping_files_folder
294
- / self.task_config.supplemental_mfhd_mapping_rules_file
311
+ / self.task_configuration.supplemental_mfhd_mapping_rules_file
295
312
  ),
296
313
  "r",
297
314
  ) as new_rules_file:
@@ -306,7 +323,7 @@ class HoldingsMarcTransformer(MigrationTaskBase):
306
323
  raise TransformationProcessError(
307
324
  "",
308
325
  "Provided supplemental MFHD mapping rules file not found",
309
- self.task_config.supplemental_mfhd_mapping_rules_file,
326
+ self.task_configuration.supplemental_mfhd_mapping_rules_file,
310
327
  )
311
328
  else:
312
329
  new_rules = {}
@@ -124,10 +124,10 @@ class ItemsTransformer(MigrationTaskBase):
124
124
  statistical_codes_map_file_name: Annotated[
125
125
  Optional[str],
126
126
  Field(
127
- title="Statistical codes map file name",
127
+ title="Statistical code map file name",
128
128
  description=(
129
- "File name for statistical codes map. "
130
- "Empty string by default."
129
+ "Path to the file containing the mapping of statistical codes. "
130
+ "The file should be in TSV format with legacy_stat_code and folio_code columns."
131
131
  ),
132
132
  ),
133
133
  ] = ""
@@ -205,6 +205,7 @@ class ItemsTransformer(MigrationTaskBase):
205
205
  csv.register_dialect("tsv", delimiter="\t")
206
206
  super().__init__(library_config, task_config, folio_client, use_logging)
207
207
  self.task_config = task_config
208
+ self.task_configuration = self.task_config
208
209
  self.check_source_files(
209
210
  self.folder_structure.legacy_records_folder, self.task_config.files
210
211
  )
@@ -455,7 +455,7 @@ class MigrationTaskBase:
455
455
  logging.info("No mapping setup for %s", folio_property_name)
456
456
  logging.info("%s will have default mapping if any ", folio_property_name)
457
457
  logging.info(
458
- "Add a file named %s and add the field to the item.mapping.json file.",
458
+ "Add a file named %s and add the field to the field mapping json file.",
459
459
  map_file_path,
460
460
  )
461
461
  return None
@@ -522,6 +522,27 @@ class MarcTaskConfigurationBase(task_configuration.AbstractTaskConfiguration):
522
522
  ),
523
523
  ),
524
524
  ] = False
525
+ statistical_codes_map_file_name: Annotated[
526
+ Optional[str],
527
+ Field(
528
+ title="Statistical code map file name",
529
+ description=(
530
+ "Path to the file containing the mapping of statistical codes. "
531
+ "The file should be in TSV format with legacy_stat_code and folio_code columns."
532
+ ),
533
+ ),
534
+ ] = ""
535
+ statistical_code_mapping_fields: Annotated[
536
+ List[str],
537
+ Field(
538
+ title="Statistical code mapping fields",
539
+ description=(
540
+ "List of fields + subfields to be used for mapping statistical codes. "
541
+ "Subfields should be delimited by a \"$\" (eg. 907$a). Single repeating subfields "
542
+ "will be treated as unique values. Multiple subfields will be concatenated together with a space."
543
+ ),
544
+ ),
545
+ ] = []
525
546
 
526
547
  class ExcludeLevelFilter(logging.Filter):
527
548
  def __init__(self, level):
@@ -157,6 +157,7 @@ class OrdersTransformer(MigrationTaskBase):
157
157
  super().__init__(library_config, task_config, folio_client, use_logging)
158
158
  self.object_type_name = self.get_object_type().name
159
159
  self.task_config = task_config
160
+ self.task_configuration = self.task_config
160
161
  self.files = self.list_source_files()
161
162
  self.total_records = 0
162
163
  self.current_folio_record: dict = {}
@@ -175,6 +176,7 @@ class OrdersTransformer(MigrationTaskBase):
175
176
  self.mapper = CompositeOrderMapper(
176
177
  self.folio_client,
177
178
  self.library_configuration,
179
+ self.task_configuration,
178
180
  self.orders_map,
179
181
  self.load_id_map(self.folder_structure.organizations_id_map_path, True),
180
182
  self.load_instance_id_map(True),
@@ -119,6 +119,7 @@ class UserTransformer(MigrationTaskBase):
119
119
  ):
120
120
  super().__init__(library_config, task_config, folio_client, use_logging)
121
121
  self.task_config = task_config
122
+ self.task_configuration = self.task_config
122
123
  self.total_records = 0
123
124
 
124
125
  self.user_map = self.setup_records_map(
@@ -1,5 +1,6 @@
1
1
  import json
2
2
  import logging
3
+ import i18n
3
4
  from datetime import datetime
4
5
  from zoneinfo import ZoneInfo
5
6
 
@@ -123,7 +124,7 @@ class LegacyLoan(object):
123
124
  if self.out_date.hour == 0:
124
125
  self.out_date = self.out_date.replace(hour=0, minute=1)
125
126
  if self.due_date <= self.out_date:
126
- raise TransformationProcessError(self.row, "Due date is before out date, or date information is missing from both", json.dumps(self.legacy_loan_dict, indent=2))
127
+ raise TransformationProcessError(self.row, i18n.t("Due date is before out date, or date information is missing from both"), json.dumps(self.legacy_loan_dict, indent=2))
127
128
 
128
129
  def to_dict(self):
129
130
  return {