folio-migration-tools 1.9.2__tar.gz → 1.9.3__tar.gz

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 (66) hide show
  1. {folio_migration_tools-1.9.2 → folio_migration_tools-1.9.3}/PKG-INFO +1 -1
  2. {folio_migration_tools-1.9.2 → folio_migration_tools-1.9.3}/pyproject.toml +1 -1
  3. {folio_migration_tools-1.9.2 → folio_migration_tools-1.9.3}/src/folio_migration_tools/mapping_file_transformation/user_mapper.py +39 -31
  4. {folio_migration_tools-1.9.2 → folio_migration_tools-1.9.3}/src/folio_migration_tools/marc_rules_transformation/conditions.py +239 -30
  5. {folio_migration_tools-1.9.2 → folio_migration_tools-1.9.3}/src/folio_migration_tools/marc_rules_transformation/holdings_statementsparser.py +99 -65
  6. {folio_migration_tools-1.9.2 → folio_migration_tools-1.9.3}/src/folio_migration_tools/marc_rules_transformation/marc_file_processor.py +6 -1
  7. {folio_migration_tools-1.9.2 → folio_migration_tools-1.9.3}/src/folio_migration_tools/migration_tasks/batch_poster.py +82 -33
  8. {folio_migration_tools-1.9.2 → folio_migration_tools-1.9.3}/src/folio_migration_tools/migration_tasks/holdings_marc_transformer.py +22 -11
  9. {folio_migration_tools-1.9.2 → folio_migration_tools-1.9.3}/src/folio_migration_tools/migration_tasks/items_transformer.py +21 -9
  10. {folio_migration_tools-1.9.2 → folio_migration_tools-1.9.3}/src/folio_migration_tools/migration_tasks/loans_migrator.py +20 -4
  11. {folio_migration_tools-1.9.2 → folio_migration_tools-1.9.3}/src/folio_migration_tools/task_configuration.py +3 -1
  12. {folio_migration_tools-1.9.2 → folio_migration_tools-1.9.3}/src/folio_migration_tools/test_infrastructure/mocked_classes.py +5 -0
  13. {folio_migration_tools-1.9.2 → folio_migration_tools-1.9.3}/src/folio_migration_tools/transaction_migration/legacy_loan.py +8 -2
  14. {folio_migration_tools-1.9.2 → folio_migration_tools-1.9.3}/src/folio_migration_tools/translations/en.json +9 -0
  15. {folio_migration_tools-1.9.2 → folio_migration_tools-1.9.3}/LICENSE +0 -0
  16. {folio_migration_tools-1.9.2 → folio_migration_tools-1.9.3}/README.md +0 -0
  17. {folio_migration_tools-1.9.2 → folio_migration_tools-1.9.3}/src/folio_migration_tools/__init__.py +0 -0
  18. {folio_migration_tools-1.9.2 → folio_migration_tools-1.9.3}/src/folio_migration_tools/__main__.py +0 -0
  19. {folio_migration_tools-1.9.2 → folio_migration_tools-1.9.3}/src/folio_migration_tools/circulation_helper.py +0 -0
  20. {folio_migration_tools-1.9.2 → folio_migration_tools-1.9.3}/src/folio_migration_tools/colors.py +0 -0
  21. {folio_migration_tools-1.9.2 → folio_migration_tools-1.9.3}/src/folio_migration_tools/config_file_load.py +0 -0
  22. {folio_migration_tools-1.9.2 → folio_migration_tools-1.9.3}/src/folio_migration_tools/custom_dict.py +0 -0
  23. {folio_migration_tools-1.9.2 → folio_migration_tools-1.9.3}/src/folio_migration_tools/custom_exceptions.py +0 -0
  24. {folio_migration_tools-1.9.2 → folio_migration_tools-1.9.3}/src/folio_migration_tools/extradata_writer.py +0 -0
  25. {folio_migration_tools-1.9.2 → folio_migration_tools-1.9.3}/src/folio_migration_tools/folder_structure.py +0 -0
  26. {folio_migration_tools-1.9.2 → folio_migration_tools-1.9.3}/src/folio_migration_tools/helper.py +0 -0
  27. {folio_migration_tools-1.9.2 → folio_migration_tools-1.9.3}/src/folio_migration_tools/holdings_helper.py +0 -0
  28. {folio_migration_tools-1.9.2 → folio_migration_tools-1.9.3}/src/folio_migration_tools/i18n_config.py +0 -0
  29. {folio_migration_tools-1.9.2 → folio_migration_tools-1.9.3}/src/folio_migration_tools/library_configuration.py +0 -0
  30. {folio_migration_tools-1.9.2 → folio_migration_tools-1.9.3}/src/folio_migration_tools/mapper_base.py +0 -0
  31. {folio_migration_tools-1.9.2 → folio_migration_tools-1.9.3}/src/folio_migration_tools/mapping_file_transformation/__init__.py +0 -0
  32. {folio_migration_tools-1.9.2 → folio_migration_tools-1.9.3}/src/folio_migration_tools/mapping_file_transformation/courses_mapper.py +0 -0
  33. {folio_migration_tools-1.9.2 → folio_migration_tools-1.9.3}/src/folio_migration_tools/mapping_file_transformation/holdings_mapper.py +0 -0
  34. {folio_migration_tools-1.9.2 → folio_migration_tools-1.9.3}/src/folio_migration_tools/mapping_file_transformation/item_mapper.py +0 -0
  35. {folio_migration_tools-1.9.2 → folio_migration_tools-1.9.3}/src/folio_migration_tools/mapping_file_transformation/manual_fee_fines_mapper.py +0 -0
  36. {folio_migration_tools-1.9.2 → folio_migration_tools-1.9.3}/src/folio_migration_tools/mapping_file_transformation/mapping_file_mapper_base.py +0 -0
  37. {folio_migration_tools-1.9.2 → folio_migration_tools-1.9.3}/src/folio_migration_tools/mapping_file_transformation/notes_mapper.py +0 -0
  38. {folio_migration_tools-1.9.2 → folio_migration_tools-1.9.3}/src/folio_migration_tools/mapping_file_transformation/order_mapper.py +0 -0
  39. {folio_migration_tools-1.9.2 → folio_migration_tools-1.9.3}/src/folio_migration_tools/mapping_file_transformation/organization_mapper.py +0 -0
  40. {folio_migration_tools-1.9.2 → folio_migration_tools-1.9.3}/src/folio_migration_tools/mapping_file_transformation/ref_data_mapping.py +0 -0
  41. {folio_migration_tools-1.9.2 → folio_migration_tools-1.9.3}/src/folio_migration_tools/marc_rules_transformation/__init__.py +0 -0
  42. {folio_migration_tools-1.9.2 → folio_migration_tools-1.9.3}/src/folio_migration_tools/marc_rules_transformation/hrid_handler.py +0 -0
  43. {folio_migration_tools-1.9.2 → folio_migration_tools-1.9.3}/src/folio_migration_tools/marc_rules_transformation/loc_language_codes.xml +0 -0
  44. {folio_migration_tools-1.9.2 → folio_migration_tools-1.9.3}/src/folio_migration_tools/marc_rules_transformation/marc_reader_wrapper.py +0 -0
  45. {folio_migration_tools-1.9.2 → folio_migration_tools-1.9.3}/src/folio_migration_tools/marc_rules_transformation/rules_mapper_authorities.py +0 -0
  46. {folio_migration_tools-1.9.2 → folio_migration_tools-1.9.3}/src/folio_migration_tools/marc_rules_transformation/rules_mapper_base.py +0 -0
  47. {folio_migration_tools-1.9.2 → folio_migration_tools-1.9.3}/src/folio_migration_tools/marc_rules_transformation/rules_mapper_bibs.py +0 -0
  48. {folio_migration_tools-1.9.2 → folio_migration_tools-1.9.3}/src/folio_migration_tools/marc_rules_transformation/rules_mapper_holdings.py +0 -0
  49. {folio_migration_tools-1.9.2 → folio_migration_tools-1.9.3}/src/folio_migration_tools/migration_report.py +0 -0
  50. {folio_migration_tools-1.9.2 → folio_migration_tools-1.9.3}/src/folio_migration_tools/migration_tasks/__init__.py +0 -0
  51. {folio_migration_tools-1.9.2 → folio_migration_tools-1.9.3}/src/folio_migration_tools/migration_tasks/authority_transformer.py +0 -0
  52. {folio_migration_tools-1.9.2 → folio_migration_tools-1.9.3}/src/folio_migration_tools/migration_tasks/bibs_transformer.py +0 -0
  53. {folio_migration_tools-1.9.2 → folio_migration_tools-1.9.3}/src/folio_migration_tools/migration_tasks/courses_migrator.py +0 -0
  54. {folio_migration_tools-1.9.2 → folio_migration_tools-1.9.3}/src/folio_migration_tools/migration_tasks/holdings_csv_transformer.py +0 -0
  55. {folio_migration_tools-1.9.2 → folio_migration_tools-1.9.3}/src/folio_migration_tools/migration_tasks/manual_fee_fines_transformer.py +0 -0
  56. {folio_migration_tools-1.9.2 → folio_migration_tools-1.9.3}/src/folio_migration_tools/migration_tasks/migration_task_base.py +0 -0
  57. {folio_migration_tools-1.9.2 → folio_migration_tools-1.9.3}/src/folio_migration_tools/migration_tasks/orders_transformer.py +0 -0
  58. {folio_migration_tools-1.9.2 → folio_migration_tools-1.9.3}/src/folio_migration_tools/migration_tasks/organization_transformer.py +0 -0
  59. {folio_migration_tools-1.9.2 → folio_migration_tools-1.9.3}/src/folio_migration_tools/migration_tasks/requests_migrator.py +0 -0
  60. {folio_migration_tools-1.9.2 → folio_migration_tools-1.9.3}/src/folio_migration_tools/migration_tasks/reserves_migrator.py +0 -0
  61. {folio_migration_tools-1.9.2 → folio_migration_tools-1.9.3}/src/folio_migration_tools/migration_tasks/user_transformer.py +0 -0
  62. {folio_migration_tools-1.9.2 → folio_migration_tools-1.9.3}/src/folio_migration_tools/test_infrastructure/__init__.py +0 -0
  63. {folio_migration_tools-1.9.2 → folio_migration_tools-1.9.3}/src/folio_migration_tools/transaction_migration/__init__.py +0 -0
  64. {folio_migration_tools-1.9.2 → folio_migration_tools-1.9.3}/src/folio_migration_tools/transaction_migration/legacy_request.py +0 -0
  65. {folio_migration_tools-1.9.2 → folio_migration_tools-1.9.3}/src/folio_migration_tools/transaction_migration/legacy_reserve.py +0 -0
  66. {folio_migration_tools-1.9.2 → folio_migration_tools-1.9.3}/src/folio_migration_tools/transaction_migration/transaction_result.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: folio_migration_tools
3
- Version: 1.9.2
3
+ Version: 1.9.3
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "folio_migration_tools"
3
- version = "1.9.2"
3
+ version = "1.9.3"
4
4
  description = "A tool allowing you to migrate data from legacy ILS:s (Library systems) into FOLIO LSP"
5
5
  authors = [
6
6
  {name = "Theodor Tolstoy", email = "github.teddes@tolstoy.se"},
@@ -59,8 +59,8 @@ class UserMapper(MappingFileMapperBase):
59
59
  True,
60
60
  )
61
61
  self.notes_mapper.migration_report = self.migration_report
62
- self.setup_departments_mapping(departments_mapping)
63
- self.setup_groups_mapping(groups_map)
62
+ self.departments_mapping = self.setup_departments_mapping(departments_mapping)
63
+ self.groups_mapping = self.setup_groups_mapping(groups_map)
64
64
 
65
65
  for m in self.record_map["data"]:
66
66
  if m["folio_field"].startswith("customFields"):
@@ -120,7 +120,8 @@ class UserMapper(MappingFileMapperBase):
120
120
 
121
121
  return clean_folio_object
122
122
 
123
- def get_users(self, source_file, file_format: str):
123
+ @staticmethod
124
+ def get_users(source_file, file_format: str):
124
125
  csv.register_dialect("tsv", delimiter="\t")
125
126
  if file_format == "tsv":
126
127
  reader = csv.DictReader(source_file, dialect="tsv")
@@ -156,12 +157,25 @@ class UserMapper(MappingFileMapperBase):
156
157
  "No Departments mapping set up. Set up a departments mapping file "
157
158
  " or remove the mapping of the Departments field",
158
159
  )
159
- return self.get_mapped_name(
160
- self.departments_mapping,
161
- legacy_user,
162
- index_or_id,
163
- True,
164
- )
160
+ if len(self.departments_mapping.mapped_legacy_keys) == 1 and self.library_configuration.multi_field_delimiter in legacy_user.get(self.departments_mapping.mapped_legacy_keys[0], ""):
161
+ split_departments = legacy_user.get(self.departments_mapping.mapped_legacy_keys[0], "").split(
162
+ self.library_configuration.multi_field_delimiter
163
+ )
164
+ return self.library_configuration.multi_field_delimiter.join([
165
+ self.get_mapped_name(
166
+ self.departments_mapping,
167
+ {self.departments_mapping.mapped_legacy_keys[0]: dept},
168
+ index_or_id,
169
+ True,
170
+ ) for dept in split_departments
171
+ ])
172
+ else:
173
+ return self.get_mapped_name(
174
+ self.departments_mapping,
175
+ legacy_user,
176
+ index_or_id,
177
+ True,
178
+ )
165
179
  elif folio_prop_name in ["expirationDate", "enrollmentDate", "personal.dateOfBirth"]:
166
180
  return self.get_parsed_date(mapped_value, folio_prop_name)
167
181
  return mapped_value
@@ -184,27 +198,21 @@ class UserMapper(MappingFileMapperBase):
184
198
  return ""
185
199
 
186
200
  def setup_groups_mapping(self, groups_map):
187
- if groups_map:
188
- self.groups_mapping = RefDataMapping(
189
- self.folio_client,
190
- "/groups",
191
- "usergroups",
192
- groups_map,
193
- "group",
194
- "UserGroupMapping",
195
- )
196
- else:
197
- self.groups_mapping = None
201
+ return RefDataMapping(
202
+ self.folio_client,
203
+ "/groups",
204
+ "usergroups",
205
+ groups_map,
206
+ "group",
207
+ "UserGroupMapping",
208
+ ) if groups_map else None
198
209
 
199
210
  def setup_departments_mapping(self, departments_mapping):
200
- if departments_mapping:
201
- self.departments_mapping = RefDataMapping(
202
- self.folio_client,
203
- "/departments",
204
- "departments",
205
- departments_mapping,
206
- "name",
207
- "DepartmentsMapping",
208
- )
209
- else:
210
- self.departments_mapping = None
211
+ return RefDataMapping(
212
+ self.folio_client,
213
+ "/departments",
214
+ "departments",
215
+ departments_mapping,
216
+ "name",
217
+ "DepartmentsMapping",
218
+ ) if departments_mapping else None
@@ -90,6 +90,7 @@ class Conditions:
90
90
  logging.info("%s\tholding_note_types", len(self.folio.holding_note_types)) # type: ignore
91
91
  logging.info("%s\tcall_number_types", len(self.folio.call_number_types)) # type: ignore
92
92
  self.setup_and_validate_holdings_types()
93
+ self.ill_policies = self.folio.folio_get_all("/ill-policies", "illPolicies")
93
94
  # Raise for empty settings
94
95
  if not self.folio.holding_note_types:
95
96
  raise TransformationProcessError("", "No holding_note_types in FOLIO")
@@ -466,36 +467,6 @@ class Conditions:
466
467
  def condition_char_select(self, legacy_id, value, parameter, marc_field: field.Field):
467
468
  return value[parameter["from"] : parameter["to"]]
468
469
 
469
- def condition_set_receipt_status(self, legacy_id, value, parameter, marc_field: field.Field):
470
- if len(value) < 7:
471
- self.mapper.migration_report.add(
472
- "ReceiptStatusMapping", i18n.t("008 is too short") + f": {value}"
473
- )
474
- return ""
475
- try:
476
- status_map = {
477
- "0": "Unknown",
478
- "1": "Other receipt or acquisition status",
479
- "2": "Received and complete or ceased",
480
- "3": "On order",
481
- "4": "Currently received",
482
- "5": "Not currently received",
483
- "6": "External access",
484
- }
485
- mapped_value = status_map[value[6]]
486
- self.mapper.migration_report.add(
487
- "ReceiptStatusMapping",
488
- i18n.t(
489
- "%{value} mapped to %{mapped_value}", value=value[6], mapped_value=mapped_value
490
- ),
491
- )
492
-
493
- return
494
- except Exception:
495
- self.mapper.migration_report.add(
496
- "ReceiptStatusMapping", i18n.t("%{value} not found in map.", value=value)
497
- )
498
- return "Unknown"
499
470
 
500
471
  def condition_set_identifier_type_id_by_name(
501
472
  self, legacy_id, value, parameter, marc_field: field.Field
@@ -915,3 +886,241 @@ class Conditions:
915
886
  legacy_id,
916
887
  f"Subject source not found for {value} {marc_field}",
917
888
  )
889
+
890
+ def condition_set_receipt_status(
891
+ self, legacy_id, value, parameter, marc_field: field.Field
892
+ ):
893
+ """
894
+ This method maps the receipt status based on the 008 field.
895
+ This condition is not available in FOLIO's MARC mapping engine and
896
+ will require use of a supplemental mapping rules file in the
897
+ HoldingsMarcTransformer task definition.
898
+ """
899
+ if len(value) < 7:
900
+ self.mapper.migration_report.add(
901
+ "ReceiptStatusMapping", i18n.t("008 is too short") + f": {value}"
902
+ )
903
+ return ""
904
+
905
+ status_map = {
906
+ "0": "Unknown",
907
+ "1": "Other receipt or acquisition status",
908
+ "2": "Received and complete or ceased",
909
+ "3": "On order",
910
+ "4": "Currently received",
911
+ "5": "Not currently received",
912
+ "6": "External access",
913
+ }
914
+
915
+ try:
916
+ mapped_value = status_map[value[6]]
917
+ self.mapper.migration_report.add(
918
+ "ReceiptStatusMapping",
919
+ i18n.t(
920
+ "%{value} mapped to %{mapped_value}",
921
+ value=value[6],
922
+ mapped_value=mapped_value,
923
+ ),
924
+ )
925
+ return mapped_value
926
+ except Exception:
927
+ self.mapper.migration_report.add(
928
+ "ReceiptStatusMapping", i18n.t("%{value} not found in map.", value=value[6])
929
+ )
930
+ return ""
931
+
932
+ def condition_set_acquisition_method(
933
+ self, legacy_id, value, parameter, marc_field: field.Field
934
+ ):
935
+ """
936
+ This method maps the acquisition method based on the 008 field.
937
+ This condition is not available in FOLIO's MARC mapping engine and
938
+ will require use of a supplemental mapping rules file in the
939
+ HoldingsMarcTransformer task definition.
940
+ """
941
+ if len(value) < 8:
942
+ self.mapper.migration_report.add(
943
+ "ReceiptStatusMapping", i18n.t("008 is too short") + f": {value}"
944
+ )
945
+ return ""
946
+
947
+ try:
948
+ acq_methods = {
949
+ "c": "Cooperative or consortial purchase",
950
+ "d": "Deposit",
951
+ "e": "Exchange",
952
+ "f": "Free",
953
+ "g": "Gift",
954
+ "l": "Legal deposit",
955
+ "m": "Membership",
956
+ "n": "Non-library purchase",
957
+ "p": "Purchase",
958
+ "q": "Lease",
959
+ "u": "Unknown",
960
+ "z": "Other method of acquisition",
961
+ }
962
+ mapped_value = acq_methods[value[7]]
963
+ self.mapper.migration_report.add(
964
+ "MethodOfAcquisitionMapping",
965
+ i18n.t(
966
+ "%{value} mapped to %{mapped_value}", value=value[7], mapped_value=mapped_value
967
+ ),
968
+ )
969
+ return mapped_value
970
+ except Exception:
971
+ self.mapper.migration_report.add(
972
+ "MethodOfAcquisitionMapping", i18n.t("%{value} not found in map.", value=value[8])
973
+ )
974
+ return ""
975
+
976
+ def condition_set_retention_policy(
977
+ self, legacy_id, value, parameter, marc_field: field.Field
978
+ ):
979
+ """
980
+ This method maps the retention policy based on the 008 field.
981
+ This condition is not available in FOLIO's MARC mapping engine and
982
+ will require use of a supplemental mapping rules file in the
983
+ HoldingsMarcTransformer task definition.
984
+ """
985
+ if len(value) < 13:
986
+ self.mapper.migration_report.add(
987
+ "RetentionPolicyMapping", i18n.t("008 is too short") + f": {value}"
988
+ )
989
+ return ""
990
+ value = value.replace("|", " ").replace("#", " ") # Replace pipe with space for mapping consistency
991
+ try:
992
+ retention_policies = {
993
+ "0": "Unknown",
994
+ "1": "Other general retention policy",
995
+ "2": "Retained except as replaced by updates",
996
+ "3": "Sample issue retained",
997
+ "4": "Retained until replaced by microform",
998
+ "5": "Retained until replaced by cumulation, replacement volume, or revision",
999
+ "6": "Retained for a limited period",
1000
+ "7": "Not retained",
1001
+ "8": "Permanently retained",
1002
+ }
1003
+ mapped_value = retention_policies[value[12]]
1004
+ self.mapper.migration_report.add(
1005
+ "RetentionPolicyMapping",
1006
+ i18n.t(
1007
+ "%{value} mapped to %{mapped_value}",
1008
+ value=value[12],
1009
+ mapped_value=mapped_value,
1010
+ ),
1011
+ )
1012
+ if value[12] == "6":
1013
+ policy_types = {
1014
+ "l": "Latest",
1015
+ "p": "Previous",
1016
+ }
1017
+ unit_types = {
1018
+ "m": "Day",
1019
+ "w": "Month",
1020
+ "y": "Year",
1021
+ "e": "Edition",
1022
+ "i": "Issue",
1023
+ "s": "Supplement"
1024
+ }
1025
+ try:
1026
+ specific_retention_policy = ""
1027
+ if value[13].strip() or value[15].strip():
1028
+ if value[14].strip() and int(value[14]) > 1:
1029
+ specific_retention_policy = f"{policy_types.get(value[13], '')} {value[14]} {unit_types.get(value[15], '')}s retained".strip()
1030
+ else:
1031
+ specific_retention_policy = f"{policy_types.get(value[13], '')} {unit_types.get(value[15], '')} retained".strip()
1032
+ if specific_retention_policy:
1033
+ self.mapper.migration_report.add(
1034
+ "RetentionPolicyMapping",
1035
+ i18n.t(
1036
+ "Retention policy 6 indicates a limited period. Specific retention period will be mapped from 008/13-15",
1037
+ )
1038
+ )
1039
+ return specific_retention_policy
1040
+ else:
1041
+ raise ValueError(
1042
+ "Specific retention policy is empty or invalid in 008/13-15"
1043
+ )
1044
+ except ValueError:
1045
+ self.mapper.migration_report.add(
1046
+ "RetentionPolicyMapping",
1047
+ i18n.t("Invalid specific retention policy in 008/13-15: %{value}", value=value[13:16]),
1048
+ )
1049
+ return mapped_value
1050
+ except Exception:
1051
+ self.mapper.migration_report.add(
1052
+ "RetentionPolicyMapping", i18n.t("%{value} not found in map.", value=value[12])
1053
+ )
1054
+ return ""
1055
+
1056
+ def condition_set_ill_policy(
1057
+ self, legacy_id, value, parameter, marc_field: field.Field
1058
+ ):
1059
+ """
1060
+ This method maps the ILL policy based on the 008 field.
1061
+ This condition is not available in FOLIO's MARC mapping engine and
1062
+ will require use of a supplemental mapping rules file in the
1063
+ HoldingsMarcTransformer task definition."""
1064
+ if len(value) < 21:
1065
+ self.mapper.migration_report.add(
1066
+ "ILLPolicyMapping", i18n.t("008 is too short") + f": {value}"
1067
+ )
1068
+ return ""
1069
+ try:
1070
+ ill_policies = {
1071
+ "a": "Will lend",
1072
+ "b": "Will not lend",
1073
+ "c": "Will lend hard copy only",
1074
+ "l": "Limited lending policy",
1075
+ "u": "Unknown",
1076
+ }
1077
+ mapped_value = ill_policies[value[20]]
1078
+ self.mapper.migration_report.add(
1079
+ "ILLPolicyMapping",
1080
+ i18n.t("%{value} mapped to %{mapped_value}", value=value[20], mapped_value=mapped_value),
1081
+ )
1082
+ ill_policy_id = self.get_ref_data_tuple_by_name(
1083
+ self.ill_policies, "ill_policies", mapped_value
1084
+ )
1085
+ return ill_policy_id[0] if ill_policy_id else ""
1086
+ except Exception:
1087
+ self.mapper.migration_report.add(
1088
+ "ILLPolicyMapping", i18n.t("%{value} not found in map.", value=value[20])
1089
+ )
1090
+ return ""
1091
+
1092
+ def condition_set_digitization_policy(
1093
+ self, legacy_id, value, parameter, marc_field: field.Field
1094
+ ):
1095
+ """
1096
+ This method maps the digitization policy based on the 008 field.
1097
+ This condition is not available in FOLIO's MARC mapping engine and
1098
+ will require use of a supplemental mapping rules file in the
1099
+ HoldingsMarcTransformer task definition.
1100
+ """
1101
+ if len(value) < 22:
1102
+ self.mapper.migration_report.add(
1103
+ "DigitizationPolicyMapping", i18n.t("008 is too short") + f": {value}"
1104
+ )
1105
+ return ""
1106
+ try:
1107
+ digitization_policies = {
1108
+ "a": "Will reproduce",
1109
+ "b": "Will not reproduce",
1110
+ "u": "Unknown",
1111
+ }
1112
+ mapped_value = digitization_policies[value[21]]
1113
+ self.mapper.migration_report.add(
1114
+ "DigitizationPolicyMapping",
1115
+ i18n.t(
1116
+ "%{value} mapped to %{mapped_value}",
1117
+ value=value[21],
1118
+ mapped_value=mapped_value,
1119
+ ),
1120
+ )
1121
+ return mapped_value
1122
+ except Exception:
1123
+ self.mapper.migration_report.add(
1124
+ "DigitizationPolicyMapping", i18n.t("%{value} not found in map.", value=value[21])
1125
+ )
1126
+ return ""
@@ -23,18 +23,18 @@ class HoldingsStatementsParser:
23
23
  """_summary_
24
24
 
25
25
  Args:
26
- marc_record (Record): _description_
27
- pattern_tag (str): _description_
28
- value_tag (str): _description_
29
- field_textual (str): _description_
30
- legacy_ids (List[str]): _description_
31
- dedupe_results (bool): _description_. Defaults to True.
26
+ marc_record (Record): pymarc Record object
27
+ pattern_tag (str): MARC tag for the pattern field
28
+ value_tag (str): MARC tag for the value field
29
+ field_textual (str): MARC tag for the textual holdings field
30
+ legacy_ids (List[str]): List of legacy IDs associated with the record
31
+ dedupe_results (bool): Whether to deduplicate the results. Defaults to True.
32
32
 
33
33
  Raises:
34
- TransformationFieldMappingError: _description_
34
+ TransformationFieldMappingError: If there is an error in mapping the holdings statements.
35
35
 
36
36
  Returns:
37
- dict: _description_
37
+ dict: A dictionary containing parsed holdings statements and related information.
38
38
  """
39
39
 
40
40
  # Textual holdings statements
@@ -45,21 +45,9 @@ class HoldingsStatementsParser:
45
45
 
46
46
  value_fields = marc_record.get_fields(value_tag)
47
47
  for pattern_field in marc_record.get_fields(pattern_tag):
48
- if "8" not in pattern_field:
49
- raise TransformationFieldMappingError(
50
- legacy_ids,
51
- i18n.t(
52
- "%{tag} subfield %{subfield} not in field",
53
- tag=pattern_tag,
54
- subfield="8",
55
- ),
56
- pattern_field,
57
- )
58
- linked_value_fields = [
59
- value_field
60
- for value_field in value_fields
61
- if "8" in value_field and value_field["8"].split(".")[0] == pattern_field["8"]
62
- ]
48
+ linked_value_fields = HoldingsStatementsParser.get_linked_value_fields(
49
+ pattern_tag, legacy_ids, value_fields, pattern_field
50
+ )
63
51
 
64
52
  if not any(linked_value_fields):
65
53
  return_dict["migration_report"].append(
@@ -75,7 +63,7 @@ class HoldingsStatementsParser:
75
63
  parsed_dict = HoldingsStatementsParser.parse_linked_field(
76
64
  pattern_field, linked_value_field
77
65
  )
78
- except KeyError:
66
+ except KeyError as e:
79
67
  raise TransformationFieldMappingError(
80
68
  legacy_ids,
81
69
  i18n.t(
@@ -84,24 +72,10 @@ class HoldingsStatementsParser:
84
72
  linked_value_tag=linked_value_field,
85
73
  ),
86
74
  pattern_field,
87
- )
88
- if parsed_dict["hlm_stmt"]:
89
- return_dict["hlm_stmts"].append(parsed_dict["hlm_stmt"])
90
- if parsed_dict["statement"]:
91
- logging.info(
92
- f"HOLDINGS STATEMENT PATTERN\t{'-'.join(legacy_ids)}\t{pattern_field}"
93
- f"\t{linked_value_field}"
94
- f"\t{parsed_dict['statement']['statement']}"
95
- f"\t{parsed_dict['statement']['note']}"
96
- f"\t{parsed_dict['statement']['staffNote']}"
97
- )
98
- return_dict["migration_report"].append(
99
- (
100
- "Holdings statements",
101
- f"From {pattern_tag}",
102
- )
103
- )
104
- return_dict["statements"].append(parsed_dict["statement"])
75
+ ) from e
76
+ HoldingsStatementsParser.prepare_return_dict(
77
+ pattern_tag, legacy_ids, return_dict, pattern_field, linked_value_field, parsed_dict
78
+ )
105
79
 
106
80
  if dedupe_results:
107
81
  return_dict["statements"] = HoldingsStatementsParser.dedupe_list_of_dict(
@@ -109,6 +83,47 @@ class HoldingsStatementsParser:
109
83
  )
110
84
  return return_dict
111
85
 
86
+ @staticmethod
87
+ def prepare_return_dict(
88
+ pattern_tag, legacy_ids, return_dict, pattern_field, linked_value_field, parsed_dict
89
+ ):
90
+ if parsed_dict["hlm_stmt"]:
91
+ return_dict["hlm_stmts"].append(parsed_dict["hlm_stmt"])
92
+ if parsed_dict["statement"]:
93
+ logging.info(
94
+ f"HOLDINGS STATEMENT PATTERN\t{'-'.join(legacy_ids)}\t{pattern_field}"
95
+ f"\t{linked_value_field}"
96
+ f"\t{parsed_dict['statement']['statement']}"
97
+ f"\t{parsed_dict['statement']['note']}"
98
+ f"\t{parsed_dict['statement']['staffNote']}"
99
+ )
100
+ return_dict["migration_report"].append(
101
+ (
102
+ "Holdings statements",
103
+ f"From {pattern_tag}",
104
+ )
105
+ )
106
+ return_dict["statements"].append(parsed_dict["statement"])
107
+
108
+ @staticmethod
109
+ def get_linked_value_fields(pattern_tag, legacy_ids, value_fields, pattern_field):
110
+ if "8" not in pattern_field:
111
+ raise TransformationFieldMappingError(
112
+ legacy_ids,
113
+ i18n.t(
114
+ "%{tag} subfield %{subfield} not in field",
115
+ tag=pattern_tag,
116
+ subfield="8",
117
+ ),
118
+ pattern_field,
119
+ )
120
+ linked_value_fields = [
121
+ value_field
122
+ for value_field in value_fields
123
+ if "8" in value_field and value_field["8"].split(".")[0] == pattern_field["8"]
124
+ ]
125
+ return linked_value_fields
126
+
112
127
  @staticmethod
113
128
  def parse_linked_field(pattern_field: Field, linked_value_fields: Field):
114
129
  break_ind = HoldingsStatementsParser.get_break_indicator(linked_value_fields)
@@ -123,6 +138,20 @@ class HoldingsStatementsParser:
123
138
  "hlm_stmt": hlm_stmt,
124
139
  }
125
140
 
141
+ _from, _to = HoldingsStatementsParser.format_from_to(_from, _to, cron_from, cron_to)
142
+ span = "-" if is_span or is_cron_span else ""
143
+ stmt = f"{_from}{span}{_to}{break_ind}" if _from else ""
144
+ stmt = stmt.strip()
145
+ if "z" in linked_value_fields:
146
+ return_dict["statement"]["note"] = linked_value_fields["z"]
147
+ if "x" in linked_value_fields:
148
+ return_dict["statement"]["staffNote"] = linked_value_fields["x"]
149
+ stmt = re.sub(" +", " ", stmt)
150
+ return_dict["statement"]["statement"] = stmt
151
+ return return_dict
152
+
153
+ @staticmethod
154
+ def format_from_to(_from, _to, cron_from, cron_to):
126
155
  if _from and cron_from:
127
156
  _from = f"{_from} ({cron_from})"
128
157
  if not _from and cron_from:
@@ -137,16 +166,7 @@ class HoldingsStatementsParser:
137
166
  _to = f"({cron_to})"
138
167
  if _from and _from == cron_from:
139
168
  _from = f"({cron_from})"
140
- span = " - " if is_span or is_cron_span else ""
141
- stmt = f"{_from}{span}{_to}{break_ind}" if _from else ""
142
- stmt = stmt.strip()
143
- if "z" in linked_value_fields:
144
- return_dict["statement"]["note"] = linked_value_fields["z"]
145
- if "x" in linked_value_fields:
146
- return_dict["statement"]["staffNote"] = linked_value_fields["x"]
147
- stmt = re.sub(" +", " ", stmt)
148
- return_dict["statement"]["statement"] = stmt
149
- return return_dict
169
+ return _from, _to
150
170
 
151
171
  @staticmethod
152
172
  def get_textual_statements(
@@ -276,12 +296,9 @@ class HoldingsStatementsParser:
276
296
  elif cron_to.strip() and val:
277
297
  val_rest = val
278
298
  if year:
279
- spill_year = f"{hlm_stmt}:" if "-" not in hlm_stmt else ""
280
- cron_from = f"{cron_from.strip()}:{val} "
281
- if cron_to and "".join(val_rest):
282
- cron_to = f"{cron_to}:{''.join(val_rest)} "
283
- elif not cron_to and "".join(val_rest):
284
- cron_to = f"{spill_year}{''.join(val_rest)}"
299
+ cron_from, cron_to = HoldingsStatementsParser.format_year_cron_from_cron_to(
300
+ cron_from, cron_to, hlm_stmt, val, val_rest
301
+ )
285
302
 
286
303
  else:
287
304
  if "season" in desc:
@@ -292,6 +309,16 @@ class HoldingsStatementsParser:
292
309
  cron_to = f"{cron_to} {''.join(val_rest)}".strip()
293
310
  return (f"{cron_from.strip()}", cron_to.strip(), hlm_stmt, is_span)
294
311
 
312
+ @staticmethod
313
+ def format_year_cron_from_cron_to(cron_from, cron_to, hlm_stmt, val, val_rest):
314
+ spill_year = f"{hlm_stmt}:" if "-" not in hlm_stmt else ""
315
+ cron_from = f"{cron_from.strip()}:{val}"
316
+ if cron_to and "".join(val_rest):
317
+ cron_to = f"{cron_to}:{''.join(val_rest)}"
318
+ elif not cron_to and "".join(val_rest):
319
+ cron_to = f"{spill_year}{''.join(val_rest)}"
320
+ return cron_from, cron_to
321
+
295
322
  @staticmethod
296
323
  def get_from_to(pattern_field: Field, linked_value_field: Field):
297
324
  _from = ""
@@ -300,11 +327,18 @@ class HoldingsStatementsParser:
300
327
  for enum_level in [el for el in "abcdef" if el in linked_value_field]:
301
328
  desc = pattern_field.get(enum_level, "")
302
329
  desc = desc.strip() if "(" not in desc else ""
303
- if linked_value_field.get(enum_level):
304
- val, *val_rest = linked_value_field[enum_level].split("-")
305
- is_span = "-" in linked_value_field[enum_level] or is_span
306
- _from = f"{_from}{(':' if _from else '')}{desc}{val}"
307
- temp_to = "".join(val_rest)
308
- if temp_to.strip():
309
- _to = f"{_to}{(':' if _to else '')}{desc}{temp_to}"
330
+ _from, _to, is_span = HoldingsStatementsParser.format_enum_parts(
331
+ linked_value_field, _from, _to, is_span, enum_level, desc
332
+ )
310
333
  return (f"{_from.strip()}", _to.strip(), is_span)
334
+
335
+ @staticmethod
336
+ def format_enum_parts(linked_value_field, _from, _to, is_span, enum_level, desc):
337
+ if linked_value_field.get(enum_level):
338
+ val, *val_rest = linked_value_field[enum_level].split("-")
339
+ is_span = "-" in linked_value_field[enum_level] or is_span
340
+ _from = f"{_from}{(':' if _from else '')}{desc}{val}"
341
+ temp_to = "".join(val_rest)
342
+ if temp_to.strip():
343
+ _to = f"{_to}{(':' if _to else '')}{desc}{temp_to}"
344
+ return _from, _to, is_span
@@ -65,7 +65,12 @@ class MarcFileProcessor:
65
65
  self.records_count += 1
66
66
  try:
67
67
  # Transform the MARC21 to a FOLIO record
68
- legacy_ids = self.mapper.get_legacy_ids(marc_record, idx)
68
+ try:
69
+ legacy_ids = self.mapper.get_legacy_ids(marc_record, idx)
70
+ except ValueError as e:
71
+ raise TransformationRecordFailedError(
72
+ f"{idx} in {file_def.file_name}", str(e), idx
73
+ ) from e
69
74
  if not legacy_ids:
70
75
  raise TransformationRecordFailedError(
71
76
  f"Index in file: {idx}", "No legacy id found", idx