folio-migration-tools 1.9.0rc12__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 (28) hide show
  1. folio_migration_tools/library_configuration.py +21 -1
  2. folio_migration_tools/mapper_base.py +78 -4
  3. folio_migration_tools/mapping_file_transformation/courses_mapper.py +2 -1
  4. folio_migration_tools/mapping_file_transformation/holdings_mapper.py +8 -4
  5. folio_migration_tools/mapping_file_transformation/item_mapper.py +4 -11
  6. folio_migration_tools/mapping_file_transformation/manual_fee_fines_mapper.py +1 -0
  7. folio_migration_tools/mapping_file_transformation/mapping_file_mapper_base.py +3 -19
  8. folio_migration_tools/mapping_file_transformation/notes_mapper.py +2 -0
  9. folio_migration_tools/mapping_file_transformation/order_mapper.py +4 -1
  10. folio_migration_tools/mapping_file_transformation/organization_mapper.py +3 -0
  11. folio_migration_tools/mapping_file_transformation/user_mapper.py +3 -1
  12. folio_migration_tools/marc_rules_transformation/rules_mapper_authorities.py +1 -0
  13. folio_migration_tools/marc_rules_transformation/rules_mapper_base.py +83 -4
  14. folio_migration_tools/marc_rules_transformation/rules_mapper_bibs.py +5 -0
  15. folio_migration_tools/marc_rules_transformation/rules_mapper_holdings.py +10 -4
  16. folio_migration_tools/migration_tasks/bibs_transformer.py +13 -3
  17. folio_migration_tools/migration_tasks/holdings_csv_transformer.py +42 -21
  18. folio_migration_tools/migration_tasks/holdings_marc_transformer.py +22 -12
  19. folio_migration_tools/migration_tasks/items_transformer.py +4 -3
  20. folio_migration_tools/migration_tasks/migration_task_base.py +22 -1
  21. folio_migration_tools/migration_tasks/orders_transformer.py +2 -0
  22. folio_migration_tools/migration_tasks/user_transformer.py +1 -0
  23. folio_migration_tools/translations/en.json +12 -3
  24. {folio_migration_tools-1.9.0rc12.dist-info → folio_migration_tools-1.9.0rc13.dist-info}/METADATA +1 -1
  25. {folio_migration_tools-1.9.0rc12.dist-info → folio_migration_tools-1.9.0rc13.dist-info}/RECORD +28 -28
  26. {folio_migration_tools-1.9.0rc12.dist-info → folio_migration_tools-1.9.0rc13.dist-info}/LICENSE +0 -0
  27. {folio_migration_tools-1.9.0rc12.dist-info → folio_migration_tools-1.9.0rc13.dist-info}/WHEEL +0 -0
  28. {folio_migration_tools-1.9.0rc12.dist-info → folio_migration_tools-1.9.0rc13.dist-info}/entry_points.txt +0 -0
@@ -32,7 +32,27 @@ class FileDefinition(BaseModel):
32
32
  ] = ""
33
33
  discovery_suppressed: Annotated[bool, Field(title="Discovery suppressed")] = False
34
34
  staff_suppressed: Annotated[bool, Field(title="Staff suppressed")] = False
35
- service_point_id: Annotated[str, Field(title="Service point ID")] = ""
35
+ service_point_id: Annotated[
36
+ str,
37
+ Field(
38
+ title="Service point ID",
39
+ description=(
40
+ "Service point to be used for "
41
+ "transactions created from this file (Loans-only)."
42
+ ),
43
+ )
44
+ ] = ""
45
+ statistical_code: Annotated[
46
+ str,
47
+ Field(
48
+ title="Statistical code",
49
+ description=(
50
+ "Statistical code (code) to be used inventory records created from "
51
+ "this file (Instances, Holdings, Items). Specify multiple codes using "
52
+ "multi_field_delimiter."
53
+ ),
54
+ )
55
+ ] = ""
36
56
  create_source_records: Annotated[
37
57
  bool,
38
58
  Field(
@@ -6,12 +6,13 @@ import sys
6
6
  import uuid
7
7
  from datetime import datetime, timezone
8
8
  from pathlib import Path
9
- from typing import Dict, List
9
+ from typing import Dict, List, Optional, Tuple, Union
10
10
 
11
11
  import i18n
12
12
  from folio_uuid.folio_namespaces import FOLIONamespaces
13
13
  from folio_uuid.folio_uuid import FolioUUID
14
14
  from folioclient import FolioClient
15
+ from pymarc import Record
15
16
 
16
17
  from folio_migration_tools.custom_exceptions import (
17
18
  TransformationFieldMappingError,
@@ -19,7 +20,8 @@ from folio_migration_tools.custom_exceptions import (
19
20
  TransformationRecordFailedError,
20
21
  )
21
22
  from folio_migration_tools.extradata_writer import ExtradataWriter
22
- from folio_migration_tools.library_configuration import LibraryConfiguration
23
+ from folio_migration_tools.helper import Helper
24
+ from folio_migration_tools.library_configuration import FileDefinition, LibraryConfiguration
23
25
  from folio_migration_tools.mapping_file_transformation.ref_data_mapping import (
24
26
  RefDataMapping,
25
27
  )
@@ -34,8 +36,9 @@ class MapperBase:
34
36
  def __init__(
35
37
  self,
36
38
  library_configuration: LibraryConfiguration,
39
+ task_configuration: AbstractTaskConfiguration,
37
40
  folio_client: FolioClient,
38
- parent_id_map: dict[str, tuple] = {},
41
+ parent_id_map: Dict[str, Tuple] = {},
39
42
  ):
40
43
  logging.info("MapperBase initiating")
41
44
  self.parent_id_map: dict[str, tuple] = parent_id_map
@@ -43,7 +46,7 @@ class MapperBase:
43
46
  self.start_datetime = datetime.now(timezone.utc)
44
47
  self.folio_client: FolioClient = folio_client
45
48
  self.library_configuration: LibraryConfiguration = library_configuration
46
- self.task_configuration: AbstractTaskConfiguration
49
+ self.task_configuration: AbstractTaskConfiguration = task_configuration
47
50
  self.mapped_folio_fields: dict = {}
48
51
  self.migration_report: MigrationReport = MigrationReport()
49
52
  self.num_criticalerrors = 0
@@ -440,6 +443,74 @@ class MapperBase:
440
443
  )
441
444
  )
442
445
 
446
+ def map_statistical_codes(self, folio_record: dict, file_def: FileDefinition, legacy_record: Optional[Union[dict, Record]] = None):
447
+ """Map statistical codes to the folio record.
448
+
449
+ This method checks if the file definition contains statistical codes and
450
+ if so, it splits the codes by the multi-field delimiter and adds them to
451
+ the folio record's 'statisticalCodeIds' field. If the field does not exist,
452
+ it initializes it as an empty list before appending the codes.
453
+
454
+ Args:
455
+ folio_record (dict): The FOLIO record to which the statistical codes will be added.
456
+ file_def (FileDefinition): The file definition containing the statistical codes.
457
+ legacy_record (Optional[Union[dict, Record]]): The legacy record from which the statistical codes are derived.
458
+ """
459
+ if file_def.statistical_code:
460
+ code_strings = file_def.statistical_code.split(
461
+ self.library_configuration.multi_field_delimiter
462
+ )
463
+ folio_record["statisticalCodeIds"] = folio_record.get("statisticalCodeIds", []) + code_strings
464
+
465
+ def setup_statistical_codes_map(self, statistical_codes_map):
466
+ if statistical_codes_map:
467
+ self.statistical_codes_mapping = RefDataMapping(
468
+ self.folio_client,
469
+ "/statistical-codes",
470
+ "statisticalCodes",
471
+ statistical_codes_map,
472
+ "code",
473
+ "StatisticalCodeMapping",
474
+ )
475
+ logging.info(f"Statistical codes mapping set up {self.statistical_codes_mapping.mapped_legacy_keys}")
476
+ else:
477
+ self.statistical_codes_mapping = None
478
+ logging.info("Statistical codes map is not set up")
479
+
480
+ def get_statistical_code(self, legacy_item: dict, folio_prop_name: str, index_or_id):
481
+ if self.statistical_codes_mapping:
482
+ return self.get_mapped_ref_data_value(
483
+ self.statistical_codes_mapping,
484
+ legacy_item,
485
+ index_or_id,
486
+ folio_prop_name,
487
+ True,
488
+ )
489
+ self.migration_report.add(
490
+ "StatisticalCodeMapping",
491
+ i18n.t("Mapping not set up"),
492
+ )
493
+ return ""
494
+
495
+ def map_statistical_code_ids(
496
+ self, legacy_ids, folio_record: dict
497
+ ):
498
+ if stat_codes := {x: None for x in folio_record.pop("statisticalCodeIds", [])}:
499
+ folio_code_ids = set()
500
+ for stat_code in stat_codes:
501
+ if stat_code_id := self.get_statistical_code({"legacy_stat_code": stat_code}, "statisticalCodeId", legacy_ids):
502
+ folio_code_ids.add(stat_code_id)
503
+ else:
504
+ Helper.log_data_issue(
505
+ legacy_ids,
506
+ i18n.t(
507
+ "Statistical code '%{code}' not found in FOLIO",
508
+ code=stat_code,
509
+ ),
510
+ stat_code,
511
+ )
512
+ folio_record["statisticalCodeIds"] = list(folio_code_ids)
513
+
443
514
  @property
444
515
  def base_string_for_folio_uuid(self):
445
516
  if self.library_configuration.use_gateway_url_for_uuids and not self.library_configuration.is_ecs:
@@ -462,6 +533,9 @@ class MapperBase:
462
533
  )
463
534
  return location_map
464
535
 
536
+ @staticmethod
537
+ def get_object_type() -> FOLIONamespaces:
538
+ raise NotImplementedError("This method should be overridden in subclasses")
465
539
 
466
540
  def flatten(my_dict: dict, path=""):
467
541
  for k, v in iter(my_dict.items()):
@@ -31,13 +31,13 @@ class CoursesMapper(MappingFileMapperBase):
31
31
  self.user_cache: dict = {}
32
32
  self.notes_mapper: NotesMapper = NotesMapper(
33
33
  library_configuration,
34
+ None,
34
35
  self.folio_client,
35
36
  course_map,
36
37
  FOLIONamespaces.note,
37
38
  True,
38
39
  )
39
40
  self.composite_course_schema = self.get_composite_course_schema()
40
- self.task_configuration = task_configuration
41
41
  super().__init__(
42
42
  folio_client,
43
43
  self.composite_course_schema,
@@ -45,6 +45,7 @@ class CoursesMapper(MappingFileMapperBase):
45
45
  None,
46
46
  FOLIONamespaces.course,
47
47
  library_configuration,
48
+ task_configuration
48
49
  )
49
50
  self.course_map = course_map
50
51
  if terms_map:
@@ -15,7 +15,7 @@ from folio_migration_tools.mapping_file_transformation.mapping_file_mapper_base
15
15
  from folio_migration_tools.mapping_file_transformation.ref_data_mapping import (
16
16
  RefDataMapping,
17
17
  )
18
-
18
+ from folio_migration_tools.task_configuration import AbstractTaskConfiguration
19
19
 
20
20
  class HoldingsMapper(MappingFileMapperBase):
21
21
  def __init__(
@@ -26,6 +26,7 @@ class HoldingsMapper(MappingFileMapperBase):
26
26
  call_number_type_map,
27
27
  instance_id_map,
28
28
  library_configuration: LibraryConfiguration,
29
+ task_config: AbstractTaskConfiguration,
29
30
  statistical_codes_map=None,
30
31
  ):
31
32
  holdings_schema = folio_client.get_holdings_schema()
@@ -37,6 +38,7 @@ class HoldingsMapper(MappingFileMapperBase):
37
38
  statistical_codes_map,
38
39
  FOLIONamespaces.holdings,
39
40
  library_configuration,
41
+ task_config
40
42
  )
41
43
  self.holdings_map = holdings_map
42
44
 
@@ -58,8 +60,10 @@ class HoldingsMapper(MappingFileMapperBase):
58
60
  "CallNumberTypeMapping",
59
61
  )
60
62
 
61
- def perform_additional_mappings(self, folio_rec, file_def):
63
+ def perform_additional_mappings(self, legacy_ids, folio_rec, file_def):
62
64
  self.handle_suppression(folio_rec, file_def)
65
+ self.map_statistical_codes(folio_rec, file_def)
66
+ self.map_statistical_code_ids(legacy_ids, folio_rec)
63
67
 
64
68
  def handle_suppression(self, folio_record, file_def: FileDefinition):
65
69
  folio_record["discoverySuppress"] = file_def.discovery_suppressed
@@ -73,8 +77,8 @@ class HoldingsMapper(MappingFileMapperBase):
73
77
  return self.get_location_id(legacy_item, index_or_id, folio_prop_name)
74
78
  elif folio_prop_name == "callNumberTypeId":
75
79
  return self.get_call_number_type_id(legacy_item, folio_prop_name, index_or_id)
76
- elif folio_prop_name.startswith("statisticalCodeIds"):
77
- return self.get_statistical_code(legacy_item, folio_prop_name, index_or_id)
80
+ # elif folio_prop_name.startswith("statisticalCodeIds"):
81
+ # return self.get_statistical_code(legacy_item, folio_prop_name, index_or_id)
78
82
 
79
83
  mapped_value = super().get_prop(
80
84
  legacy_item, folio_prop_name, index_or_id, schema_default_value
@@ -52,8 +52,8 @@ class ItemMapper(MappingFileMapperBase):
52
52
  statistical_codes_map,
53
53
  FOLIONamespaces.items,
54
54
  library_configuration,
55
+ task_configuration,
55
56
  )
56
- self.task_configuration = task_configuration
57
57
  self.item_schema = self.folio_client.get_item_schema()
58
58
  self.items_map = items_map
59
59
  self.holdings_id_map = holdings_id_map
@@ -117,8 +117,10 @@ class ItemMapper(MappingFileMapperBase):
117
117
  "LocationMapping",
118
118
  )
119
119
 
120
- def perform_additional_mappings(self, folio_rec, file_def):
120
+ def perform_additional_mappings(self, legacy_ids, folio_rec, file_def):
121
121
  self.handle_suppression(folio_rec, file_def)
122
+ self.map_statistical_codes(folio_rec, file_def)
123
+ self.map_statistical_code_ids(legacy_ids, folio_rec)
122
124
 
123
125
  def handle_suppression(self, folio_record, file_def: FileDefinition):
124
126
  folio_record["discoverySuppress"] = file_def.discovery_suppressed
@@ -219,15 +221,6 @@ class ItemMapper(MappingFileMapperBase):
219
221
  return self.get_mapped_ref_data_value(
220
222
  self.loan_type_mapping, legacy_item, folio_prop_name, index_or_id
221
223
  )
222
- elif folio_prop_name.startswith("statisticalCodeIds"):
223
- statistical_code_id = self.get_statistical_code(
224
- legacy_item, folio_prop_name, index_or_id
225
- )
226
- self.migration_report.add(
227
- "StatisticalCodeMapping",
228
- f"{folio_prop_name} -> {statistical_code_id}",
229
- )
230
- return statistical_code_id
231
224
 
232
225
  mapped_value = super().get_prop(
233
226
  legacy_item, folio_prop_name, index_or_id, schema_default_value
@@ -46,6 +46,7 @@ class ManualFeeFinesMapper(MappingFileMapperBase):
46
46
  None,
47
47
  FOLIONamespaces.fees_fines,
48
48
  library_configuration,
49
+ task_configuration,
49
50
  ignore_legacy_identifier,
50
51
  )
51
52
 
@@ -20,10 +20,8 @@ from folio_migration_tools.custom_exceptions import (
20
20
  )
21
21
  from folio_migration_tools.library_configuration import LibraryConfiguration
22
22
  from folio_migration_tools.mapper_base import MapperBase
23
- from folio_migration_tools.mapping_file_transformation.ref_data_mapping import (
24
- RefDataMapping,
25
- )
26
23
  from folio_migration_tools.migration_report import MigrationReport
24
+ from folio_migration_tools.task_configuration import AbstractTaskConfiguration
27
25
 
28
26
  empty_vals = ["Not mapped", None, ""]
29
27
 
@@ -37,9 +35,10 @@ class MappingFileMapperBase(MapperBase):
37
35
  statistical_codes_map,
38
36
  uuid_namespace: UUID,
39
37
  library_configuration: LibraryConfiguration,
38
+ task_config: AbstractTaskConfiguration,
40
39
  ignore_legacy_identifier=False,
41
40
  ):
42
- super().__init__(library_configuration, folio_client)
41
+ super().__init__(library_configuration, task_config, folio_client)
43
42
  self.uuid_namespace = uuid_namespace
44
43
  self.ignore_legacy_identifier = ignore_legacy_identifier
45
44
  self.schema = schema
@@ -101,21 +100,6 @@ class MappingFileMapperBase(MapperBase):
101
100
  )
102
101
  csv.register_dialect("tsv", delimiter="\t")
103
102
 
104
- def setup_statistical_codes_map(self, statistical_codes_map):
105
- if statistical_codes_map:
106
- self.statistical_codes_mapping = RefDataMapping(
107
- self.folio_client,
108
- "/statistical-codes",
109
- "statisticalCodes",
110
- statistical_codes_map,
111
- "code",
112
- "StatisticalCodeMapping",
113
- )
114
- logging.info("Statistical codes mapping set up")
115
- else:
116
- self.statistical_codes_mapping = None
117
- logging.info("Statistical codes map is not set up")
118
-
119
103
  def setup_field_map(self, ignore_legacy_identifier):
120
104
  field_map = {} # Map of folio_fields and source fields as an array
121
105
  for k in self.record_map["data"]:
@@ -14,6 +14,7 @@ class NotesMapper(MappingFileMapperBase):
14
14
  def __init__(
15
15
  self,
16
16
  library_configuration: LibraryConfiguration,
17
+ task_configuration,
17
18
  folio_client: FolioClient,
18
19
  record_map: dict,
19
20
  object_type: FOLIONamespaces,
@@ -28,6 +29,7 @@ class NotesMapper(MappingFileMapperBase):
28
29
  None,
29
30
  object_type,
30
31
  library_configuration,
32
+ task_configuration,
31
33
  ignore_legacy_identifier,
32
34
  )
33
35
 
@@ -30,6 +30,7 @@ class CompositeOrderMapper(MappingFileMapperBase):
30
30
  self,
31
31
  folio_client: FolioClient,
32
32
  library_configuration: LibraryConfiguration,
33
+ task_configuration,
33
34
  composite_order_map: dict,
34
35
  organizations_id_map: dict,
35
36
  instance_id_map: dict,
@@ -53,6 +54,7 @@ class CompositeOrderMapper(MappingFileMapperBase):
53
54
  None,
54
55
  FOLIONamespaces.orders,
55
56
  library_configuration,
57
+ task_configuration,
56
58
  )
57
59
  logging.info("Loading Instance ID map...")
58
60
  self.instance_id_map = instance_id_map
@@ -80,6 +82,7 @@ class CompositeOrderMapper(MappingFileMapperBase):
80
82
  self.folio_client: FolioClient = folio_client
81
83
  self.notes_mapper: NotesMapper = NotesMapper(
82
84
  library_configuration,
85
+ None,
83
86
  self.folio_client,
84
87
  composite_order_map,
85
88
  FOLIONamespaces.note,
@@ -366,7 +369,7 @@ class CompositeOrderMapper(MappingFileMapperBase):
366
369
 
367
370
  def perform_additional_mapping(self, index_or_id, composite_order):
368
371
  self.validate_po_number(index_or_id, composite_order.get("poNumber"))
369
-
372
+
370
373
  # Get organization UUID from FOLIO
371
374
  composite_order["vendor"] = self.get_folio_organization_uuid(
372
375
  index_or_id, composite_order.get("vendor")
@@ -23,6 +23,7 @@ class OrganizationMapper(MappingFileMapperBase):
23
23
  self,
24
24
  folio_client: FolioClient,
25
25
  library_configuration: LibraryConfiguration,
26
+ task_config,
26
27
  organization_map: dict,
27
28
  organization_types_map,
28
29
  address_categories_map,
@@ -43,6 +44,7 @@ class OrganizationMapper(MappingFileMapperBase):
43
44
  None,
44
45
  FOLIONamespaces.organizations,
45
46
  library_configuration,
47
+ task_config,
46
48
  )
47
49
  self.organization_schema = organization_schema
48
50
  # Set up reference data maps
@@ -56,6 +58,7 @@ class OrganizationMapper(MappingFileMapperBase):
56
58
  self.folio_client: FolioClient = folio_client
57
59
  self.notes_mapper: NotesMapper = NotesMapper(
58
60
  library_configuration,
61
+ None,
59
62
  self.folio_client,
60
63
  organization_map,
61
64
  FOLIONamespaces.note,
@@ -47,10 +47,12 @@ class UserMapper(MappingFileMapperBase):
47
47
  None,
48
48
  FOLIONamespaces.users,
49
49
  library_config,
50
+ task_config
50
51
  )
51
- self.task_config = task_config
52
+ self.task_config = self.task_configuration
52
53
  self.notes_mapper: NotesMapper = NotesMapper(
53
54
  self.library_configuration,
55
+ None,
54
56
  self.folio_client,
55
57
  self.record_map,
56
58
  FOLIONamespaces.users,
@@ -62,6 +62,7 @@ class AuthorityMapper(RulesMapperBase):
62
62
  folio_client,
63
63
  library_configuration,
64
64
  task_configuration,
65
+ None,
65
66
  self.get_authority_json_schema(folio_client, library_configuration),
66
67
  Conditions(folio_client, self, "auth", library_configuration.folio_release),
67
68
  )
@@ -6,14 +6,14 @@ import urllib.parse
6
6
  import uuid
7
7
  from abc import abstractmethod
8
8
  from textwrap import wrap
9
- from typing import List, Tuple
9
+ from typing import Dict, List, Tuple
10
10
 
11
11
  import i18n
12
12
  import pymarc
13
13
  from dateutil.parser import parse
14
14
  from folio_uuid.folio_uuid import FOLIONamespaces, FolioUUID
15
15
  from folioclient import FolioClient
16
- from pymarc import Field, Record, Subfield
16
+ from pymarc import Field, Optional, Record, Subfield
17
17
 
18
18
  from folio_migration_tools.custom_exceptions import (
19
19
  TransformationFieldMappingError,
@@ -35,18 +35,18 @@ class RulesMapperBase(MapperBase):
35
35
  folio_client: FolioClient,
36
36
  library_configuration: LibraryConfiguration,
37
37
  task_configuration,
38
+ statistical_codes_map: Optional[Dict],
38
39
  schema: dict,
39
40
  conditions=None,
40
41
  parent_id_map: dict[str, tuple] = None,
41
42
  ):
42
- super().__init__(library_configuration, folio_client, parent_id_map)
43
+ super().__init__(library_configuration, task_configuration, folio_client, parent_id_map)
43
44
  self.parsed_records = 0
44
45
  self.id_map: dict[str, tuple] = {}
45
46
  self.start = time.time()
46
47
  self.last_batch_time = time.time()
47
48
  self.folio_client: FolioClient = folio_client
48
49
  self.schema: dict = schema
49
- self.task_configuration = task_configuration
50
50
  self.conditions = conditions
51
51
  self.item_json_schema = ""
52
52
  self.mappings: dict = {}
@@ -61,6 +61,8 @@ class RulesMapperBase(MapperBase):
61
61
  self.migration_report,
62
62
  self.task_configuration.deactivate035_from001,
63
63
  )
64
+
65
+ self.setup_statistical_codes_map(statistical_codes_map)
64
66
  logging.info("Current user id is %s", self.folio_client.current_user)
65
67
 
66
68
  def print_progress(self):
@@ -813,6 +815,83 @@ class RulesMapperBase(MapperBase):
813
815
  )
814
816
  data_import_marc_file.write(marc_record.as_marc())
815
817
 
818
+
819
+ def map_statistical_codes(
820
+ self,
821
+ folio_record: dict,
822
+ file_def: FileDefinition,
823
+ marc_record: Record,
824
+ ):
825
+ """Map statistical codes to FOLIO instance
826
+
827
+ This method first calls the base class method to map statistical codes
828
+ from the file_def. Then, it checks to see if there are any MARC field
829
+ mappings defined in the task configuration. If so, it creates a list
830
+ of lists where the first element is the MARC field tag, and the remaining
831
+ elements are the subfields to be used for mapping. It then iterates
832
+ through the MARC fields, retrieves the values based on the subfields.
833
+ Finally, it adds the mapped codes to the folio_record's statisticalCodeIds.
834
+
835
+ Args:
836
+ legacy_ids (List[str]): The legacy IDs of the folio record
837
+ folio_record (dict): The Dictionary representation of the FOLIO record
838
+ marc_record (Record): The pymarc Record object
839
+ file_def (FileDefinition): The file definition object from which marc_record was read
840
+ """
841
+ super().map_statistical_codes(folio_record, file_def)
842
+ if self.task_configuration.statistical_code_mapping_fields:
843
+ stat_code_marc_fields = []
844
+ for mapping in self.task_configuration.statistical_code_mapping_fields:
845
+ stat_code_marc_fields.append(mapping.split("$"))
846
+ for field_map in stat_code_marc_fields:
847
+ mapped_codes = self.map_stat_codes_from_marc_field(field_map, marc_record, self.library_configuration.multi_field_delimiter)
848
+ folio_record['statisticalCodeIds'] = folio_record.get("statisticalCodeIds", []) + mapped_codes
849
+
850
+ @staticmethod
851
+ def map_stat_codes_from_marc_field(field_map: List[str], marc_record: Record, multi_field_delimiter: str="<delimiter>") -> List[str]:
852
+ """Map statistical codes from MARC field to FOLIO instance.
853
+
854
+ This function extracts statistical codes from a MARC field based on the provided field map.
855
+ It supports multiple subfields and uses a delimiter to handle concatenated values.
856
+
857
+ Args:
858
+ field_map (List[str]): A list where the first element is the MARC field tag, and the remaining elements are subfields to extract values from.
859
+ marc_record (Record): The MARC record to process.
860
+ multi_field_delimiter (str): A delimiter used to concatenate multiple subfield values that should be individual mapped values.
861
+
862
+ Returns:
863
+ str: A string of statistical codes extracted from the MARC field, formatted as "<field>_<subfield>:<value>".
864
+ """
865
+ field_values = []
866
+ if len(field_map) == 2:
867
+ subfields = []
868
+ for mf in marc_record.get_fields(field_map[0]):
869
+ subfields.extend(
870
+ multi_field_delimiter.join(
871
+ mf.get_subfields(field_map[1])
872
+ ).split(multi_field_delimiter)
873
+ )
874
+ field_values.extend(
875
+ [
876
+ f"{field_map[0]}_{field_map[1]}:{x}" for
877
+ x in subfields
878
+ ]
879
+ )
880
+ elif len(field_map) > 2:
881
+ for mf in marc_record.get_fields(field_map[0]):
882
+ for sf in field_map[1:]:
883
+ field_values.extend(
884
+ [
885
+ f"{field_map[0]}_{sf}:{x}" for x in multi_field_delimiter.join(
886
+ mf.get_subfields(sf)
887
+ ).split(multi_field_delimiter)
888
+ ]
889
+ )
890
+ elif field_map:
891
+ for mf in marc_record.get_fields(field_map[0]):
892
+ field_values.append(f"{field_map[0]}:{mf.value()}")
893
+ return field_values
894
+
816
895
  def save_source_record(
817
896
  self,
818
897
  srs_records_file,
@@ -44,11 +44,13 @@ class BibsRulesMapper(RulesMapperBase):
44
44
  folio_client: FolioClient,
45
45
  library_configuration: LibraryConfiguration,
46
46
  task_configuration: MarcTaskConfigurationBase,
47
+ statistical_codes_map: Dict[str, str] = None,
47
48
  ):
48
49
  super().__init__(
49
50
  folio_client,
50
51
  library_configuration,
51
52
  task_configuration,
53
+ statistical_codes_map,
52
54
  self.get_instance_schema(folio_client),
53
55
  Conditions(folio_client, self, "bibs", library_configuration.folio_release),
54
56
  )
@@ -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
 
@@ -40,20 +41,21 @@ class RulesMapperHoldings(RulesMapperBase):
40
41
  library_configuration: LibraryConfiguration,
41
42
  parent_id_map: dict,
42
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,
@@ -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]):
@@ -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
  )
@@ -243,11 +252,11 @@ class HoldingsMarcTransformer(MigrationTaskBase):
243
252
 
244
253
  # Load Boundwith relationship map
245
254
  self.boundwith_relationship_map_rows = []
246
- if self.task_config.boundwith_relationship_file_path:
255
+ if self.task_configuration.boundwith_relationship_file_path:
247
256
  try:
248
257
  with open(
249
258
  self.folder_structure.legacy_records_folder
250
- / self.task_config.boundwith_relationship_file_path
259
+ / self.task_configuration.boundwith_relationship_file_path
251
260
  ) as boundwith_relationship_file:
252
261
  self.boundwith_relationship_map_rows = list(
253
262
  csv.DictReader(boundwith_relationship_file, dialect="tsv")
@@ -260,28 +269,29 @@ class HoldingsMarcTransformer(MigrationTaskBase):
260
269
  raise TransformationProcessError(
261
270
  "",
262
271
  i18n.t("Provided boundwith relationship file not found"),
263
- self.task_config.boundwith_relationship_file_path,
272
+ self.task_configuration.boundwith_relationship_file_path,
264
273
  )
265
274
 
266
275
  location_map_path = (
267
276
  self.folder_structure.mapping_files_folder
268
- / self.task_config.location_map_file_name
277
+ / self.task_configuration.location_map_file_name
269
278
  )
270
279
  with open(location_map_path) as location_map_file:
271
280
  self.location_map = list(csv.DictReader(location_map_file, dialect="tsv"))
272
281
  logging.info("Locations in map: %s", len(self.location_map))
273
282
 
274
283
  self.check_source_files(
275
- self.folder_structure.legacy_records_folder, self.task_config.files
284
+ self.folder_structure.legacy_records_folder, self.task_configuration.files
276
285
  )
277
286
  self.instance_id_map = self.load_instance_id_map(True)
278
287
  self.mapper = RulesMapperHoldings(
279
288
  self.folio_client,
280
289
  self.location_map,
281
- self.task_config,
290
+ self.task_configuration,
282
291
  self.library_configuration,
283
292
  self.instance_id_map,
284
293
  self.boundwith_relationship_map_rows,
294
+ statcode_mapping
285
295
  )
286
296
  self.add_supplemental_mfhd_mappings()
287
297
  if (
@@ -293,12 +303,12 @@ class HoldingsMarcTransformer(MigrationTaskBase):
293
303
  logging.info("Init done")
294
304
 
295
305
  def add_supplemental_mfhd_mappings(self):
296
- if self.task_config.supplemental_mfhd_mapping_rules_file:
306
+ if self.task_configuration.supplemental_mfhd_mapping_rules_file:
297
307
  try:
298
308
  with open(
299
309
  (
300
310
  self.folder_structure.mapping_files_folder
301
- / self.task_config.supplemental_mfhd_mapping_rules_file
311
+ / self.task_configuration.supplemental_mfhd_mapping_rules_file
302
312
  ),
303
313
  "r",
304
314
  ) as new_rules_file:
@@ -313,7 +323,7 @@ class HoldingsMarcTransformer(MigrationTaskBase):
313
323
  raise TransformationProcessError(
314
324
  "",
315
325
  "Provided supplemental MFHD mapping rules file not found",
316
- self.task_config.supplemental_mfhd_mapping_rules_file,
326
+ self.task_configuration.supplemental_mfhd_mapping_rules_file,
317
327
  )
318
328
  else:
319
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(
@@ -12,6 +12,7 @@
12
12
  "%{value} added to %{entry}": "%{value} added to %{entry}",
13
13
  "%{value} mapped to %{mapped_value}": "%{value} mapped to %{mapped_value}",
14
14
  "%{value} not found in map.": "%{value} not found in map.",
15
+ "0 is staff-only, all other values are public": "0 is staff-only, all other values are public",
15
16
  "008 is too short": "008 is too short",
16
17
  "008 length invalid. '%{rest}' was stripped out": "008 length invalid. '%{rest}' was stripped out",
17
18
  "035 generation from 001 turned off": "035 generation from 001 turned off",
@@ -57,6 +58,7 @@
57
58
  "Data issue. Consider fixing the record. ": "Data issue. Consider fixing the record. ",
58
59
  "Declare item as lost": "Declare item as lost",
59
60
  "Discarded reserves": "Discarded reserves",
61
+ "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",
60
62
  "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",
61
63
  "Duplicate MARC record identifiers ": "Duplicate MARC record identifiers ",
62
64
  "Duplicate barcodes": "Duplicate barcodes",
@@ -106,6 +108,7 @@
106
108
  "Mapped from Indicator 1": "Mapped from Indicator 1",
107
109
  "Mapping failed for %{tag} \"%{subfield}\" (%{normalized_subfield})": "Mapping failed for %{tag} \"%{subfield}\" (%{normalized_subfield})",
108
110
  "Mapping failed for %{tag} \"%{subfield}\" (Normalized: %{normalized_subfield})": "Mapping failed for %{tag} \"%{subfield}\" (Normalized: %{normalized_subfield})",
111
+ "Mapping not set up": "Mapping not set up",
109
112
  "Mapping not set up for target field": "Mapping not set up for target field",
110
113
  "Mapping not setup": "Mapping not setup",
111
114
  "Measure": "Measure",
@@ -128,7 +131,9 @@
128
131
  "Number of organizations created": "Number of organizations created",
129
132
  "Number of records written to disk": "Number of records written to disk",
130
133
  "Number of reoccuring identical %{type}": "Number of reoccuring identical %{type}",
134
+ "Number of reoccurring identical %{type}": "Number of reoccurring identical %{type}",
131
135
  "Ogranization transformation report": "Ogranization transformation report",
136
+ "Organization transformation report": "Organization transformation report",
132
137
  "Organizations linked using organizations_id_map": "Organizations linked using organizations_id_map",
133
138
  "Organizations not in ID map, linked using FOLIO lookup": "Organizations not in ID map, linked using FOLIO lookup",
134
139
  "Original value": "Original value",
@@ -143,8 +148,10 @@
143
148
  "Previously transformed holdings record loaded": "Previously transformed holdings record loaded",
144
149
  "Processed pre-validated loans": "Processed pre-validated loans",
145
150
  "Processed reserves": "Processed reserves",
151
+ "Provided boundwith relationship file not found": "Provided boundwith relationship file not found",
146
152
  "Pruchase Orders and Purchase Order Lines Transformation Report": "Pruchase Orders and Purchase Order Lines Transformation Report",
147
153
  "RECORD FAILED Organization identifier not in ID map/FOLIO": "RECORD FAILED Organization identifier not in ID map/FOLIO",
154
+ "RECORD FAILED: PO number has invalid character(s)": "RECORD FAILED: PO number has invalid character(s)",
148
155
  "Records failed": "Records failed",
149
156
  "Records failed because of failed holdings": "Records failed because of failed holdings",
150
157
  "Records failed due to an error. See data issues log for details": "Records failed due to an error. See data issues log for details",
@@ -178,9 +185,13 @@
178
185
  "Set leader 10 (Indicator count) from %{field} to 2": "Set leader 10 (Indicator count) from %{field} to 2",
179
186
  "Set leader 11 (Subfield code count) from %{record} to 2": "Set leader 11 (Subfield code count) from %{record} to 2",
180
187
  "Set leader 20-23 from %{field} to 4500": "Set leader 20-23 from %{field} to 4500",
188
+ "Set up statistical code id mapping...": "Set up statistical code id mapping...",
181
189
  "Source digits": "Source digits",
182
190
  "Source of heading or term": "Source of heading or term",
183
191
  "Staff suppressed": "Staff suppressed",
192
+ "Statistical code": "Statistical code",
193
+ "Statistical code '%{code}' not found in FOLIO": "Statistical code '%{code}' not found in FOLIO",
194
+ "Statistical codes not supported for this object type": "Statistical codes not supported for this object type",
184
195
  "Stored courselistings": "Stored courselistings",
185
196
  "Stored courses": "Stored courses",
186
197
  "Stored instructors": "Stored instructors",
@@ -441,7 +452,5 @@
441
452
  "legacy id from %{fro}": "legacy id from %{fro}",
442
453
  "naturalId mapped from %{fro}": "naturalId mapped from %{fro}",
443
454
  "no matching identifier_types in %{names}": "no matching identifier_types in %{names}",
444
- "subfield present in %{linked_value_tag} but not in %{pattern_field}": "subfield present in %{linked_value_tag} but not in %{pattern_field}",
445
- "Provided boundwith relationship file not found": "Provided boundwith relationship file not found",
446
- "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"
455
+ "subfield present in %{linked_value_tag} but not in %{pattern_field}": "subfield present in %{linked_value_tag} but not in %{pattern_field}"
447
456
  }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: folio_migration_tools
3
- Version: 1.9.0rc12
3
+ Version: 1.9.0rc13
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
@@ -10,19 +10,19 @@ folio_migration_tools/folder_structure.py,sha256=bZlmKGtxdytWcqjnM2lE4Vpx4nHyYRk
10
10
  folio_migration_tools/helper.py,sha256=KkOkNAGO_fuYqxdLrsbLzCJLQHUrFZG1NzD4RmpQ-KM,2804
11
11
  folio_migration_tools/holdings_helper.py,sha256=yJpz6aJrKRBiJ1MtT5bs2vXAc88uJuGh2_KDuCySOKc,7559
12
12
  folio_migration_tools/i18n_config.py,sha256=3AH_2b9zTsxE4XTe4isM_zYtPJSlK0ix6eBmV7kAYUM,228
13
- folio_migration_tools/library_configuration.py,sha256=I2Pj2hrjbhbc3HNwzQ-LTPM4IyJyeqAXsb2aA9TvX3s,6469
14
- folio_migration_tools/mapper_base.py,sha256=H2QiNZZzDKxuCxSdXaa-1BRatAUArpr3Te1QsAscMog,20122
13
+ folio_migration_tools/library_configuration.py,sha256=LzICsZQdOkXwIqdDfh59x0-Cx77Lb18qVnWroNqekS8,7046
14
+ folio_migration_tools/mapper_base.py,sha256=IYER8Dq-4qLq3qiAvUpnzc33usUbfZtNKzyZJD6DNds,23567
15
15
  folio_migration_tools/mapping_file_transformation/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
- folio_migration_tools/mapping_file_transformation/courses_mapper.py,sha256=RuNkdG9XumpgPO3Zvcx_JYzZ598Xle_AMNf18zLR2UM,8095
17
- folio_migration_tools/mapping_file_transformation/holdings_mapper.py,sha256=nJS-xx1LszvbYfw0qdTUHX9xXHlxS7wP5mYmixFMh8A,7221
18
- folio_migration_tools/mapping_file_transformation/item_mapper.py,sha256=YYZFgNoDVuSO_mRuaDNZ6-6bYbEtYFtfbIZ1MFPBAgc,10687
19
- folio_migration_tools/mapping_file_transformation/manual_fee_fines_mapper.py,sha256=nCkqbxaDHKxMuqQHh_afxQp48YrVD-SeCZ0L1iGvnkk,13402
20
- folio_migration_tools/mapping_file_transformation/mapping_file_mapper_base.py,sha256=bLL6tTqqv2MOjZlowjL8lngYP09F_iwfFikEpjB4nmI,37816
21
- folio_migration_tools/mapping_file_transformation/notes_mapper.py,sha256=auLQZqa4rSJo_MIV4Lc5-LG8RcBpp2bnKH243qNYq_0,3470
22
- folio_migration_tools/mapping_file_transformation/order_mapper.py,sha256=k-kIuf2ceXrPWe3oVnfhuQlE7eglcx6PDLVJtddkeiM,17680
23
- folio_migration_tools/mapping_file_transformation/organization_mapper.py,sha256=78-r1dGP2Vd49bsf5dJVk__gzNgI6MoSTA9xuRiHR-4,14575
16
+ folio_migration_tools/mapping_file_transformation/courses_mapper.py,sha256=yX8yCCw6B54r7JUn5IPhMYKBgImiagUzfwBxNVRf5MQ,8091
17
+ folio_migration_tools/mapping_file_transformation/holdings_mapper.py,sha256=DY4cpSWaJWJo-8ESpO4PSf_raTIRAVSt9N0-aV1ShGc,7504
18
+ folio_migration_tools/mapping_file_transformation/item_mapper.py,sha256=wYEnSsV7U7YE2Df2sld_qJsbPtV9YoEV42iAbatjN8U,10402
19
+ folio_migration_tools/mapping_file_transformation/manual_fee_fines_mapper.py,sha256=2-W2Z8hwAhWT77zfDWuwWqm20j4w1mfzeAXWiyssQ8I,13434
20
+ folio_migration_tools/mapping_file_transformation/mapping_file_mapper_base.py,sha256=EHeeC-Yr4Cy9OhoTcteJAtMiG_yJu8pq1NqikkVCmMc,37274
21
+ folio_migration_tools/mapping_file_transformation/notes_mapper.py,sha256=vCmZmjrjyYtXeFCyVqvWfnP8y1jGGu15RXzXIHh12xY,3530
22
+ folio_migration_tools/mapping_file_transformation/order_mapper.py,sha256=-JEBEeOntNPE9-NYhWAJ1hpQI03ZzMv-_mkyLzSa9x4,17750
23
+ folio_migration_tools/mapping_file_transformation/organization_mapper.py,sha256=u1Lb6tApn-nVLqbbJV38BuipKL3OK8Y2uQ4ogoyGQaI,14639
24
24
  folio_migration_tools/mapping_file_transformation/ref_data_mapping.py,sha256=qFsn_LwKZeKFdOudfEQnNA3DEHOdNQVKzTPdZAlDPX0,8864
25
- folio_migration_tools/mapping_file_transformation/user_mapper.py,sha256=Q8418BdXdCuEDxfoXqLCjWy1lUxhQNLRwSE5Gi1lqoA,7805
25
+ folio_migration_tools/mapping_file_transformation/user_mapper.py,sha256=GnePkCK2k8VSd_wAUNlLwTLLB-WwpxZLBC-kDXtONBU,7867
26
26
  folio_migration_tools/marc_rules_transformation/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
27
27
  folio_migration_tools/marc_rules_transformation/conditions.py,sha256=ttTZISieqveu3YpvpnawHh3In1_DNQMTziI5yasfmWU,39142
28
28
  folio_migration_tools/marc_rules_transformation/holdings_statementsparser.py,sha256=Rvdub7RdtR1BWnZij_Ejgp7cG9Kz58i_tXwXJbhaMtU,12101
@@ -30,27 +30,27 @@ folio_migration_tools/marc_rules_transformation/hrid_handler.py,sha256=SgnSYeNR0
30
30
  folio_migration_tools/marc_rules_transformation/loc_language_codes.xml,sha256=ztn2_yKws6qySL4oSsZh7sOjxq5bCC1PhAnXJdtgmJ0,382912
31
31
  folio_migration_tools/marc_rules_transformation/marc_file_processor.py,sha256=M-PHduzMYmZnrMwOSlwnWQ5bT-566gVRFSMo-JgS2d4,12346
32
32
  folio_migration_tools/marc_rules_transformation/marc_reader_wrapper.py,sha256=9ATjYMRAjy0QcXtmNZaHVhHLJ5hE1WUgOcF6KMJjbgo,5309
33
- folio_migration_tools/marc_rules_transformation/rules_mapper_authorities.py,sha256=e-wwJs8s8qEgIp8NvQgjx9lEyv7uvt08Fp6fPsy1GK8,9603
34
- folio_migration_tools/marc_rules_transformation/rules_mapper_base.py,sha256=6EcOQSDG3wgW7S7re2uWiLkbup9Flg4ZqhtX6cCLnbk,41402
35
- folio_migration_tools/marc_rules_transformation/rules_mapper_bibs.py,sha256=b4HO10sYMk4mHhM25TaDez472AtJi0AknWsBhCapNnA,30016
36
- folio_migration_tools/marc_rules_transformation/rules_mapper_holdings.py,sha256=Cx9X9ANP5dlZyRAy5IQH8tnbhURTOQTZpAFpKuIkUm4,26542
33
+ folio_migration_tools/marc_rules_transformation/rules_mapper_authorities.py,sha256=PGt2w8h2pj8_8sGjQe3L-odFDlquURtKnoNFRWQB3GI,9621
34
+ folio_migration_tools/marc_rules_transformation/rules_mapper_base.py,sha256=6vVvrNoViZZ1g2piKtOPMlqYspej-2eei8HIwM-K_Lg,45309
35
+ folio_migration_tools/marc_rules_transformation/rules_mapper_bibs.py,sha256=H7RrU1KfFlTj6M0l7HXD3vOtxKHY4FD5xrXmGjCjcRc,30324
36
+ folio_migration_tools/marc_rules_transformation/rules_mapper_holdings.py,sha256=dOqje_UVxTQDgi2CMNjjzZRKOHU99bR0ik6sYuAzibo,26939
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
40
  folio_migration_tools/migration_tasks/batch_poster.py,sha256=BcbNz2z5bGcbXmNvjnLd3IVHb0RSuagiVPvm77GUzwU,36402
41
- folio_migration_tools/migration_tasks/bibs_transformer.py,sha256=uOIfKVfkUdSr0RMbLpC4KbwbU16VKkXRJY8ULbdKzQk,5831
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
- folio_migration_tools/migration_tasks/holdings_csv_transformer.py,sha256=NtysoayEIqQ8c_GNcRi6LXDYR-7OLmqFCfciMwzsyT4,21668
44
- folio_migration_tools/migration_tasks/holdings_marc_transformer.py,sha256=gCa5hwTG7px6JvHQQs7YIWP12eAlS1uwuzFyeu-M4qE,13665
45
- folio_migration_tools/migration_tasks/items_transformer.py,sha256=qk0sLPBxE5MtPnpLzO_gEhVVe1BqHHnpn2Zaz_vo1RY,19083
43
+ folio_migration_tools/migration_tasks/holdings_csv_transformer.py,sha256=g3r4J1Y18vU18BjUmye7oSANR_TLTrP95la41flqj5M,22736
44
+ folio_migration_tools/migration_tasks/holdings_marc_transformer.py,sha256=DVYdSNUPmdTv6GfLJdyT806dZV6UQHH_T8gkqtPgXaU,14143
45
+ folio_migration_tools/migration_tasks/items_transformer.py,sha256=uBcVv-t_4N9j1pNyJPt-wFkKbNQLTe4_NFvOXMUcBjA,19212
46
46
  folio_migration_tools/migration_tasks/loans_migrator.py,sha256=CPsin9XLzHwNrpKHPMHAvgRvpoH8QvAfYZYr1FSxAN4,34520
47
47
  folio_migration_tools/migration_tasks/manual_fee_fines_transformer.py,sha256=CnmlTge7nChUJ10EiUkriQtJlVxWqglgfhjgneh2_yM,7247
48
- folio_migration_tools/migration_tasks/migration_task_base.py,sha256=cZVPqWMuaNJYsnqiiFlyjiRSQ4trQyaSDwykDHfbkyg,21640
49
- folio_migration_tools/migration_tasks/orders_transformer.py,sha256=ry3oUUVQTFKCDUbGF5Zjo5ppa6AseKQwpF-wb1sb5UY,14214
48
+ folio_migration_tools/migration_tasks/migration_task_base.py,sha256=Q-57h6rmt74bC9LidA9ZoagEcwVd_ytq8IUWelVOm2E,22521
49
+ folio_migration_tools/migration_tasks/orders_transformer.py,sha256=6SnzU_rUTu2B5hQykI2nRA7vI1rg-uxuF9Ncupe0AEY,14302
50
50
  folio_migration_tools/migration_tasks/organization_transformer.py,sha256=vcCjhN1sS55c_a0LXi1Yw1eq3zpDn5E4BGbm2zDQ_Z4,16885
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
- folio_migration_tools/migration_tasks/user_transformer.py,sha256=cNBT-wn_xx1OQXiB-vMLZmvyzkg1X562AJXUcYfThaE,12279
53
+ folio_migration_tools/migration_tasks/user_transformer.py,sha256=aylrMC9n47fdStgsNfW4ZbJh2E4FDSPypsaNv52ynKc,12330
54
54
  folio_migration_tools/task_configuration.py,sha256=2GXVog0-_cFybqsU2WFcxnTNGDhvDzqb7gYyIimdPAk,1131
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=lpohRMqKnGmsoyspUQFXuyrGXRoIQZu2Dq7Q5WLQJw8,14877
@@ -59,9 +59,9 @@ folio_migration_tools/transaction_migration/legacy_loan.py,sha256=zablUc_xKI2ah1
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=Y9x7JbeC2N-yFvv5fWmWzupbzJBQ7_lgCmsDsGa2OUE,38935
63
- folio_migration_tools-1.9.0rc12.dist-info/LICENSE,sha256=PhIEkitVi3ejgq56tt6sWoJIG_zmv82cjjd_aYPPGdI,1072
64
- folio_migration_tools-1.9.0rc12.dist-info/METADATA,sha256=8jaDR0QeEvZuAEn-K-C08JMx1BaWPEEBp3U4HvOCPxE,7448
65
- folio_migration_tools-1.9.0rc12.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
66
- folio_migration_tools-1.9.0rc12.dist-info/entry_points.txt,sha256=Hbe-HjqMcU8FwVshVIkeWyZd9XwgT1CCMNf06EpHQu8,77
67
- folio_migration_tools-1.9.0rc12.dist-info/RECORD,,
62
+ folio_migration_tools/translations/en.json,sha256=FeoaN3INfim4_-l3DSamHo2hn1SUJr5DsgDgsV4XUek,39693
63
+ folio_migration_tools-1.9.0rc13.dist-info/LICENSE,sha256=PhIEkitVi3ejgq56tt6sWoJIG_zmv82cjjd_aYPPGdI,1072
64
+ folio_migration_tools-1.9.0rc13.dist-info/METADATA,sha256=Oo3lLbvSLtsgtpkihR2fKc_IJmXR4LXcDUSDLNGjOzU,7448
65
+ folio_migration_tools-1.9.0rc13.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
66
+ folio_migration_tools-1.9.0rc13.dist-info/entry_points.txt,sha256=Hbe-HjqMcU8FwVshVIkeWyZd9XwgT1CCMNf06EpHQu8,77
67
+ folio_migration_tools-1.9.0rc13.dist-info/RECORD,,