im_onix 1.0.2 → 1.1.1

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 (59) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +4 -0
  3. data/.yardopts +1 -0
  4. data/Gemfile +8 -0
  5. data/LICENSE.md +7 -0
  6. data/README.md +3 -3
  7. data/Rakefile +10 -0
  8. data/bin/html_codelist_to_yml.rb +14 -15
  9. data/bin/onix_bench.rb +1 -0
  10. data/bin/onix_pp.rb +4 -8
  11. data/bin/onix_serialize.rb +27 -0
  12. data/doc-src/handlers.rb +154 -0
  13. data/im_onix.gemspec +32 -0
  14. data/lib/im_onix.rb +0 -1
  15. data/lib/onix/addressee.rb +10 -0
  16. data/lib/onix/code.rb +108 -282
  17. data/lib/onix/collateral_detail.rb +24 -17
  18. data/lib/onix/collection.rb +38 -0
  19. data/lib/onix/collection_sequence.rb +7 -0
  20. data/lib/onix/contributor.rb +40 -39
  21. data/lib/onix/date.rb +73 -109
  22. data/lib/onix/descriptive_detail.rb +90 -417
  23. data/lib/onix/discount_coded.rb +3 -16
  24. data/lib/onix/entity.rb +28 -62
  25. data/lib/onix/epub_usage_constraint.rb +7 -0
  26. data/lib/onix/epub_usage_limit.rb +6 -0
  27. data/lib/onix/extent.rb +39 -0
  28. data/lib/onix/helper.rb +25 -25
  29. data/lib/onix/identifier.rb +13 -54
  30. data/lib/onix/language.rb +8 -0
  31. data/lib/onix/market.rb +5 -0
  32. data/lib/onix/market_publishing_detail.rb +20 -0
  33. data/lib/onix/onix21.rb +76 -139
  34. data/lib/onix/onix_message.rb +87 -100
  35. data/lib/onix/price.rb +19 -39
  36. data/lib/onix/product.rb +141 -637
  37. data/lib/onix/product_form_feature.rb +7 -0
  38. data/lib/onix/product_part.rb +89 -0
  39. data/lib/onix/product_supplies_extractor.rb +275 -0
  40. data/lib/onix/product_supply.rb +17 -58
  41. data/lib/onix/publishing_detail.rb +16 -32
  42. data/lib/onix/related_material.rb +4 -3
  43. data/lib/onix/related_product.rb +9 -29
  44. data/lib/onix/related_work.rb +3 -17
  45. data/lib/onix/sales_outlet.rb +2 -10
  46. data/lib/onix/sales_restriction.rb +8 -21
  47. data/lib/onix/sales_rights.rb +1 -5
  48. data/lib/onix/sender.rb +12 -0
  49. data/lib/onix/serializer.rb +156 -0
  50. data/lib/onix/subject.rb +9 -30
  51. data/lib/onix/subset.rb +88 -78
  52. data/lib/onix/supply_detail.rb +42 -0
  53. data/lib/onix/supporting_resource.rb +29 -86
  54. data/lib/onix/tax.rb +9 -18
  55. data/lib/onix/territory.rb +23 -17
  56. data/lib/onix/title_detail.rb +22 -0
  57. data/lib/onix/title_element.rb +32 -0
  58. data/lib/onix/website.rb +3 -16
  59. metadata +53 -34
@@ -0,0 +1,7 @@
1
+ module ONIX
2
+ class ProductFormFeature < SubsetDSL
3
+ element "ProductFormFeatureType", :subset, :shortcut => :type
4
+ element "ProductFormFeatureValue", :text, :shortcut => :value
5
+ elements "ProductFormFeatureDescription", :text, :shortcut => :descriptions
6
+ end
7
+ end
@@ -0,0 +1,89 @@
1
+ module ONIX
2
+ # product part use full Product to provide file protection and file size
3
+ class ProductPart < SubsetDSL
4
+ include EanMethods
5
+ include ProprietaryIdMethods
6
+
7
+ elements "ProductIdentifier", :subset, :shortcut => :identifiers
8
+ element "ProductForm", :subset, :shortcut => :form
9
+ element "ProductFormDescription", :text, :shortcut => :file_description
10
+ elements "ProductFormDetail", :subset, :shortcut => :form_details
11
+ elements "ProductContentType", :subset, :shortcut => :content_types
12
+ element "NumberOfCopies", :integer
13
+
14
+ def file_formats
15
+ @product_form_details.select { |fd| fd.code =~ /^E1.*/ }
16
+ end
17
+
18
+ # full Product if referenced in ONIXMessage
19
+ attr_accessor :product
20
+
21
+ # this ProductPart is part of Product
22
+ attr_accessor :part_of
23
+
24
+ # @!group High level
25
+
26
+ # digital file format string (Epub,Pdf,AmazonKindle)
27
+ # @return [String]
28
+ def file_format
29
+ file_formats.first.human if file_formats.first
30
+ end
31
+
32
+ # digital file format mimetype
33
+ # @return [String]
34
+ def file_mimetype
35
+ if file_formats.first
36
+ file_formats.first.mimetype
37
+ end
38
+ end
39
+
40
+ # is digital file reflowable ?
41
+ # @return [Boolean]
42
+ def reflowable?
43
+ return true if @product_form_details.select { |fd| fd.code == "E200" }.length > 0
44
+ return false if @product_form_details.select { |fd| fd.code == "E201" }.length > 0
45
+ end
46
+
47
+ # raw part file description string without HTML
48
+ # @return [String]
49
+ def raw_file_description
50
+ if @product_form_description
51
+ Helper.strip_html(@product_form_description).gsub(/\s+/, " ").strip
52
+ end
53
+ end
54
+
55
+ # Protection type string (None, Watermarking, DRM, AdobeDRM)
56
+ # @return [String]
57
+ def protection_type
58
+ if product
59
+ product.protection_type
60
+ else
61
+ if part_of
62
+ part_of.protection_type
63
+ end
64
+ end
65
+ end
66
+
67
+ # List of protections type string (None, Watermarking, DRM, AdobeDRM)
68
+ # @return [Array<String>]
69
+ def protections
70
+ if product
71
+ product.protections
72
+ else
73
+ if part_of
74
+ part_of.protections
75
+ end
76
+ end
77
+ end
78
+
79
+ # digital file filesize in bytes
80
+ # @return [Integer]
81
+ def filesize
82
+ if product
83
+ product.filesize
84
+ end
85
+ end
86
+
87
+ # @!endgroup
88
+ end
89
+ end
@@ -0,0 +1,275 @@
1
+ module ONIX
2
+ # flattened supplies extractor
3
+ module ProductSuppliesExtractor
4
+ # class must define a product_supplies returning an Array of objects responding to :
5
+ # - availability_date (Date)
6
+ # - countries (country code Array)
7
+
8
+ # @!group High level
9
+
10
+ # flattened supplies with prices
11
+ #
12
+ # supplies is a hash symbol array in the form :
13
+ # [{:available=>bool,
14
+ # :availability_date=>date,
15
+ # :including_tax=>bool,
16
+ # :currency=>string,
17
+ # :territory=>string,
18
+ # :suppliers=>[Supplier,...],
19
+ # :prices=>[{:amount=>int,
20
+ # :from_date=>date,
21
+ # :until_date=>date,
22
+ # :tax=>{:amount=>int, :rate_percent=>float}}]}]
23
+ def supplies(keep_all_prices_dates = false)
24
+ supplies = []
25
+
26
+ # add territories if missing
27
+ if self.product_supplies
28
+ self.product_supplies.each do |ps|
29
+ ps.supply_details.each do |sd|
30
+ sd.prices.each do |p|
31
+ supply = {}
32
+ supply[:suppliers] = sd.suppliers
33
+ supply[:available] = sd.available?
34
+ supply[:availability_date] = sd.availability_date
35
+
36
+ unless supply[:availability_date]
37
+ if ps.availability_date
38
+ supply[:availability_date] = ps.market_publishing_detail.availability_date
39
+ end
40
+ end
41
+ supply[:price] = p.amount
42
+ supply[:qualifier] = p.qualifier.human if p.qualifier
43
+ supply[:including_tax] = p.including_tax?
44
+ if !p.territory or p.territory.countries.length == 0
45
+ supply[:territory] = []
46
+ supply[:territory] = ps.countries
47
+
48
+ if supply[:territory].length == 0
49
+ if @publishing_detail
50
+ supply[:territory] = self.countries_rights
51
+ end
52
+ end
53
+ else
54
+ supply[:territory] = p.territory.countries
55
+ end
56
+ supply[:from_date] = p.from_date
57
+ supply[:until_date] = p.until_date
58
+ supply[:currency] = p.currency
59
+ supply[:tax] = p.tax
60
+
61
+ unless supply[:availability_date]
62
+ if @publishing_detail
63
+ supply[:availability_date] = @publishing_detail.publication_date
64
+ end
65
+ end
66
+
67
+ supplies << supply
68
+ end
69
+ end
70
+ end
71
+ end
72
+
73
+ grouped_supplies = {}
74
+ supplies.each do |supply|
75
+ supply[:territory].each do |territory|
76
+ pr_key = "#{supply[:available]}_#{supply[:including_tax]}_#{supply[:currency]}_#{territory}"
77
+ grouped_supplies[pr_key] ||= []
78
+ grouped_supplies[pr_key] << supply
79
+ end
80
+ end
81
+
82
+ nb_suppliers = supplies.map { |s| s[:suppliers][0].name }.uniq.length
83
+ # render prices sequentially with dates
84
+ grouped_supplies.each do |ksup, supply|
85
+ if supply.length > 1
86
+ global_price = supply.select { |p| not p[:from_date] and not p[:until_date] }
87
+ global_price = global_price.first
88
+
89
+ if global_price
90
+ if nb_suppliers > 1
91
+ grouped_supplies[ksup] += self.prices_with_periods(supply, global_price)
92
+ else
93
+ grouped_supplies[ksup] = self.prices_with_periods(supply, global_price)
94
+ end
95
+ grouped_supplies[ksup].uniq!
96
+ else
97
+ # remove explicit from date
98
+ explicit_from = supply.select { |p| p[:from_date] and not supply.select { |sp| sp[:until_date] and sp[:until_date] <= p[:from_date] }.first }.first
99
+ if explicit_from
100
+ explicit_from[:from_date] = nil unless keep_all_prices_dates
101
+ end
102
+ end
103
+ else
104
+ supply.each do |s|
105
+ if s[:from_date] and s[:availability_date] and s[:from_date] >= s[:availability_date]
106
+ s[:availability_date] = s[:from_date]
107
+ end
108
+ s[:from_date] = nil unless keep_all_prices_dates
109
+ end
110
+ end
111
+ end
112
+
113
+ # merge by territories
114
+ grouped_territories_supplies = {}
115
+ grouped_supplies.each do |ksup, supply|
116
+ fsupply = supply.first
117
+ pr_key = "#{fsupply[:available]}_#{fsupply[:including_tax]}_#{fsupply[:currency]}"
118
+ supply.each do |s|
119
+ pr_key += "_#{s[:price]}_#{s[:from_date]}_#{s[:until_date]}"
120
+ end
121
+ grouped_territories_supplies[pr_key] ||= []
122
+ grouped_territories_supplies[pr_key] << supply
123
+ end
124
+
125
+ supplies = []
126
+
127
+ grouped_territories_supplies.each do |ksup, supply|
128
+ fsupply = supply.first.first
129
+ supplies << {:including_tax => fsupply[:including_tax], :currency => fsupply[:currency],
130
+ :territory => supply.map { |fs| fs.map { |s| s[:territory] } }.flatten.uniq,
131
+ :available => fsupply[:available],
132
+ :availability_date => fsupply[:availability_date],
133
+ :suppliers => fsupply[:suppliers],
134
+ :prices => supply.first.map { |s|
135
+
136
+ s[:amount] = s[:price]
137
+ s.delete(:price)
138
+ s.delete(:available)
139
+ s.delete(:currency)
140
+ s.delete(:availability_date)
141
+ s.delete(:including_tax)
142
+ s.delete(:territory)
143
+ s
144
+ }}
145
+ end
146
+
147
+ supplies
148
+ end
149
+
150
+ # add missing periods when they can be guessed
151
+ def prices_with_periods(supplies, global_supply)
152
+ complete_supplies = supplies.select { |supply| supply[:from_date] && supply[:until_date] }.sort_by { |supply| supply[:from_date] }
153
+ missing_start_period_supplies = supplies.select { |supply| supply[:from_date] && !supply[:until_date] }.sort_by { |supply| supply[:from_date] }
154
+ missing_end_period_supplies = supplies.select { |supply| !supply[:from_date] && supply[:until_date] }.sort_by { |supply| supply[:until_date] }
155
+
156
+ return [global_supply] if [complete_supplies, missing_start_period_supplies, missing_end_period_supplies].all? { |supply| supply.empty? }
157
+
158
+ return self.add_missing_periods(complete_supplies, global_supply) unless complete_supplies.empty?
159
+
160
+ without_start = missing_start_period_supplies.length == 1 && complete_supplies.empty? && missing_end_period_supplies.empty?
161
+ without_end = missing_end_period_supplies.length == 1 && complete_supplies.empty? && missing_start_period_supplies.empty?
162
+
163
+ return self.add_starting_period(missing_start_period_supplies.first, global_supply) if without_start
164
+ return self.add_ending_period(missing_end_period_supplies.first, global_supply) if without_end
165
+
166
+ [global_supply]
167
+ end
168
+
169
+ def add_missing_periods(supplies, global_supply)
170
+ new_supplies = []
171
+
172
+ supplies.each.with_index do |supply, index|
173
+ new_supplies << global_supply.dup.tap { |start_sup| start_sup[:until_date] = supply[:from_date] - 1 } if index == 0
174
+
175
+ if index > 0 && index != supplies.length
176
+ new_supplies << global_supply.dup.tap do |missing_supply|
177
+ missing_supply[:from_date] = supplies[index - 1][:until_date] + 1
178
+ missing_supply[:until_date] = supply[:from_date] - 1
179
+ end
180
+ end
181
+
182
+ new_supplies << supply
183
+
184
+ new_supplies << global_supply.dup.tap { |end_sup| end_sup[:from_date] = supply[:until_date] + 1 } if index == supplies.length - 1
185
+ end
186
+
187
+ new_supplies
188
+ end
189
+
190
+ def add_starting_period(supply, global_supply)
191
+ missing_supply = global_supply.dup
192
+ missing_supply[:until_date] = supply[:from_date] - 1
193
+
194
+ [missing_supply, supply]
195
+ end
196
+
197
+ def add_ending_period(supply, global_supply)
198
+ missing_supply = global_supply.dup
199
+ missing_supply[:from_date] = supply[:until_date] + 1
200
+
201
+ [supply, missing_supply]
202
+ end
203
+
204
+ # flattened supplies only including taxes
205
+ def supplies_including_tax
206
+ self.supplies.select { |p| p[:including_tax] }
207
+ end
208
+
209
+ # flattened supplies only excluding taxes
210
+ def supplies_excluding_tax
211
+ self.supplies.select { |p| not p[:including_tax] }
212
+ end
213
+
214
+ # flattened supplies with default tax (excluding tax for US and CA, including otherwise)
215
+ def supplies_with_default_tax
216
+ self.supplies_including_tax + self.supplies_excluding_tax.select { |s| ["CAD", "USD"].include?(s[:currency]) }
217
+ end
218
+
219
+ # flattened supplies for country
220
+ def supplies_for_country(country, currency = nil)
221
+ country_supplies = self.supplies
222
+ if currency
223
+ country_supplies = country_supplies.select { |s| s[:currency] == currency }
224
+ end
225
+ country_supplies.select { |s|
226
+ if s[:territory].include?(country)
227
+ true
228
+ else
229
+ false
230
+ end
231
+ }
232
+ end
233
+
234
+ # price amount for given +currency+ and country at time
235
+ def at_time_price_amount_for(time, currency, country = nil)
236
+ sups = self.supplies_with_default_tax.select { |p| p[:currency] == currency }
237
+ if country
238
+ sups = sups.select { |p| p[:territory].include?(country) }
239
+ end
240
+ if sups.length > 0
241
+ # exclusive
242
+ sup = sups.first[:prices].select { |p|
243
+ (!p[:from_date] or p[:from_date].to_date <= time.to_date) and
244
+ (!p[:until_date] or p[:until_date].to_date > time.to_date)
245
+ }.first
246
+
247
+ if sup
248
+ sup[:amount]
249
+ else
250
+ # or inclusive
251
+ sup = sups.first[:prices].select { |p|
252
+ (!p[:from_date] or p[:from_date].to_date <= time.to_date) and
253
+ (!p[:until_date] or p[:until_date].to_date >= time.to_date)
254
+ }.first
255
+
256
+ if sup
257
+ sup[:amount]
258
+ else
259
+ nil
260
+ end
261
+ end
262
+
263
+ else
264
+ nil
265
+ end
266
+ end
267
+
268
+ # current price amount for given +currency+ and country
269
+ def current_price_amount_for(currency, country = nil)
270
+ at_time_price_amount_for(Time.now, currency, country)
271
+ end
272
+
273
+ # @!endgroup
274
+ end
275
+ end
@@ -1,85 +1,44 @@
1
- require 'onix/price'
2
1
  require 'onix/date'
2
+ require 'onix/market'
3
+ require 'onix/market_publishing_detail'
4
+ require 'onix/supply_detail'
3
5
 
4
6
  module ONIX
5
-
6
- class Market < SubsetDSL
7
- element "Territory", :subset
8
- end
9
-
10
- class MarketPublishingDetail < SubsetDSL
11
- elements "PublisherRepresentative", :subset, {:klass=>"Agent"}
12
- element "MarketPublishingStatus", :subset
13
- elements "MarketDate", :subset
14
-
15
- def availability_date
16
- av=@market_dates.availability.first
17
- if av
18
- av.date
19
- else
20
- nil
21
- end
22
- end
23
- end
24
-
25
- class SupplyDetail < SubsetDSL
26
- elements "Supplier", :subset
27
- element "ProductAvailability", :subset
28
- elements "SupplyDate", :subset
29
- elements "Price", :subset
30
- element "UnpricedItemType", :subset
31
-
32
- def availability
33
- @product_availability
34
- end
35
-
36
- def distributors
37
- @suppliers.select{|s| s.role.human=~/Distributor/}.uniq
38
- end
39
-
40
- def available?
41
- ["Available","NotYetAvailable","InStock","ToOrder","Pod"].include?(@product_availability.human)
42
- end
43
-
44
- def sold_separately?
45
- @product_availability.human!="NotSoldSeparately"
46
- end
47
-
48
- def availability_date
49
- av=@supply_dates.availability.first
50
- if av
51
- av.date
52
- else
53
- nil
54
- end
55
- end
56
- end
57
-
58
7
  class ProductSupply < SubsetDSL
59
8
  elements "Market", :subset
60
9
  element "MarketPublishingDetail", :subset
61
10
  elements "SupplyDetail", :subset
62
11
 
12
+ # availability date from market
13
+ # @return [Date]
63
14
  def availability_date
64
15
  if @market_publishing_detail
65
16
  @market_publishing_detail.availability_date
66
17
  end
67
18
  end
68
19
 
20
+ # countries string array
21
+ # @return [Array<String>]
69
22
  def countries
70
- @markets.map{|m| m.territory.countries}.flatten.uniq
23
+ @markets.map { |market| market.territory.countries }.flatten.uniq
71
24
  end
72
25
 
26
+ # distributors string array
27
+ # @return [Array<String>]
73
28
  def distributors
74
- @supply_details.map{|sd| sd.distributors}.flatten.uniq{|d| d.name}
29
+ @supply_details.map { |supply_detail| supply_detail.distributors }.flatten.uniq { |distributor| distributor.name }
75
30
  end
76
31
 
32
+ # available supply details
33
+ # @return [Array<SupplyDetail>]
77
34
  def available_supply_details
78
- @supply_details.select{|sd| sd.available?}
35
+ @supply_details.select { |supply_detail| supply_detail.available? }
79
36
  end
80
37
 
38
+ # unavailable supply details
39
+ # @return [Array<SupplyDetail>]
81
40
  def unavailable_supply_details
82
- @supply_details.delete_if{|sd| sd.available?}
41
+ @supply_details.delete_if { |supply_detail| supply_detail.available? }
83
42
  end
84
43
 
85
44
  def available?