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
@@ -1,54 +1,16 @@
1
1
  require 'nokogiri'
2
- require 'pp'
3
2
  require 'time'
4
- require 'benchmark'
5
3
 
6
4
  require 'onix/subset'
7
-
8
5
  require 'onix/helper'
9
6
  require 'onix/code'
10
- require 'onix/contributor'
11
- require 'onix/product_supply'
7
+ require 'onix/sender'
8
+ require 'onix/addressee'
12
9
  require 'onix/product'
13
10
 
14
11
  require 'onix/onix21'
15
12
 
16
13
  module ONIX
17
-
18
- class Sender < SubsetDSL
19
- include GlnMethods
20
-
21
- elements "SenderIdentifier", :subset
22
- element "SenderName", :text
23
- element "ContactName", :text
24
- element "EmailAddress", :text
25
-
26
- # shortcuts
27
- def identifiers
28
- @sender_identifiers
29
- end
30
-
31
- def name
32
- @sender_name
33
- end
34
- end
35
-
36
- class Addressee < SubsetDSL
37
- include GlnMethods
38
-
39
- elements "AddresseeIdentifier", :subset
40
- element "AddresseeName", :text
41
-
42
- # shortcuts
43
- def identifiers
44
- @addressee_identifiers
45
- end
46
-
47
- def name
48
- @addressee_name
49
- end
50
- end
51
-
52
14
  class ONIXMessage < Subset
53
15
  attr_accessor :sender, :adressee, :sent_date_time,
54
16
  :default_language_of_text, :default_currency_code,
@@ -56,8 +18,8 @@ module ONIX
56
18
  :release
57
19
 
58
20
  def initialize
59
- @products=[]
60
- @vault={}
21
+ @products = []
22
+ @vault = {}
61
23
  end
62
24
 
63
25
  def vault
@@ -71,8 +33,8 @@ module ONIX
71
33
  # merge another message in this one
72
34
  # current object erase other values
73
35
  def merge!(other)
74
- @products+=other.products
75
- @products=@products.uniq { |p| p.ean }
36
+ @products += other.products
37
+ @products = @products.uniq { |p| p.ean }
76
38
  init_vault
77
39
  self
78
40
  end
@@ -86,10 +48,10 @@ module ONIX
86
48
 
87
49
  # initialize hash between ID and product object
88
50
  def init_vault
89
- @vault={}
51
+ @vault = {}
90
52
  @products.each do |product|
91
53
  product.identifiers.each do |ident|
92
- @vault[ident.uniq_id]=product
54
+ @vault[ident.uniq_id] = product
93
55
  end
94
56
  end
95
57
 
@@ -97,7 +59,7 @@ module ONIX
97
59
  product.related.each do |rel|
98
60
  rel.identifiers.each do |ident|
99
61
  if @vault[ident.uniq_id]
100
- rel.product=@vault[ident.uniq_id]
62
+ rel.product = @vault[ident.uniq_id]
101
63
  end
102
64
  end
103
65
  end
@@ -105,7 +67,7 @@ module ONIX
105
67
  product.parts.each do |prt|
106
68
  prt.identifiers.each do |ident|
107
69
  if @vault[ident.uniq_id]
108
- prt.product=@vault[ident.uniq_id]
70
+ prt.product = @vault[ident.uniq_id]
109
71
  end
110
72
  end
111
73
  end
@@ -113,75 +75,100 @@ module ONIX
113
75
  end
114
76
 
115
77
  # open with arg detection
116
- def open(arg, force_encoding=nil)
117
- data=ONIX::Helper.arg_to_data(arg)
78
+ def open(arg, force_encoding = nil)
79
+ data = ONIX::Helper.arg_to_data(arg)
118
80
 
119
- xml=nil
81
+ xml = nil
120
82
  if force_encoding
121
- xml=Nokogiri::XML.parse(data, nil, force_encoding)
83
+ xml = Nokogiri::XML.parse(data, nil, force_encoding)
122
84
  else
123
- xml=Nokogiri::XML.parse(data)
85
+ xml = Nokogiri::XML.parse(data)
124
86
  end
125
87
 
126
88
  xml.remove_namespaces!
127
89
  xml
128
90
  end
129
91
 
130
- # parse filename or file
131
- def parse(arg, force_encoding=nil, force_release=nil)
92
+ # release as an integer eg: 210, 300, 301
93
+ def version
94
+ if @release
95
+ @release.gsub(/\./, "").to_i * 10 ** (3 - @release.scan(".").length - 1)
96
+ end
97
+ end
98
+
99
+ # detect ONIX version from XML tags
100
+ def detect_release(element)
101
+ if element
102
+ return "3.0" if element.search("//DescriptiveDetail").length > 0
103
+ return "3.0" if element.search("//CollateralDetail").length > 0
104
+ return "3.0" if element.search("//ContentDetail").length > 0
105
+ return "3.0" if element.search("//PublishingDetail").length > 0
106
+ end
107
+ "2.1"
108
+ end
132
109
 
133
- xml=open(arg, force_encoding)
134
- @products=[]
110
+ def set_release_from_xml(node, force_release)
111
+ @release = node["release"]
112
+ unless @release
113
+ @release = detect_release(node)
114
+ end
115
+ if force_release
116
+ @release = force_release.to_s
117
+ end
118
+ end
135
119
 
120
+ # parse filename or file
121
+ def parse(arg, force_encoding = nil, force_release = nil)
122
+ xml = open(arg, force_encoding)
123
+ @products = []
136
124
  root = xml.root
125
+ set_release_from_xml(root, force_release)
137
126
  case root
138
- when tag_match("ONIXMessage")
139
- @release=root["release"]
140
- if force_release
141
- @release=force_release.to_s
142
- end
143
- root.elements.each do |e|
144
- case e
145
- when tag_match("Header")
146
- e.elements.each do |t|
147
- case t
148
- when tag_match("Sender")
149
- @sender=Sender.parse(t)
150
- when tag_match("Addressee")
151
- @addressee=Addressee.parse(t)
152
- when tag_match("SentDateTime")
153
- tm=t.text
154
- @sent_date_time=Time.strptime(tm, "%Y%m%dT%H%M%S") rescue Time.strptime(tm, "%Y%m%dT%H%M") rescue Time.strptime(tm, "%Y%m%d") rescue nil
155
- when tag_match("DefaultLanguageOfText")
156
- @default_language_of_text=LanguageCode.parse(t)
157
- when tag_match("DefaultCurrencyCode")
158
- @default_currency_code=t.text
159
- else
160
- unsupported(t)
161
- end
162
- end
163
- when tag_match("Product")
164
- product=nil
165
- if @release =~ /^3.0/
166
- product=Product.parse(e)
167
- else
168
- product=ONIX21::Product.parse(e)
169
- end
170
- product.default_language_of_text=@default_language_of_text
171
- product.default_currency_code=@default_currency_code
172
- @products << product
127
+ when tag_match("ONIXMessage")
128
+ root.elements.each do |e|
129
+ case e
130
+ when tag_match("Header")
131
+ e.elements.each do |t|
132
+ case t
133
+ when tag_match("Sender")
134
+ @sender = Sender.parse(t)
135
+ when tag_match("Addressee")
136
+ @addressee = Addressee.parse(t)
137
+ when tag_match("SentDateTime")
138
+ tm = t.text
139
+ @sent_date_time = Time.strptime(tm, "%Y%m%dT%H%M%S") rescue Time.strptime(tm, "%Y%m%dT%H%M") rescue Time.strptime(tm, "%Y%m%d") rescue nil
140
+ when tag_match("DefaultLanguageOfText")
141
+ @default_language_of_text = LanguageCode.parse(t)
142
+ when tag_match("DefaultCurrencyCode")
143
+ @default_currency_code = t.text
144
+ else
145
+ unsupported(t)
146
+ end
173
147
  end
148
+ when tag_match("Product")
149
+ product = nil
150
+ if self.version >= 300
151
+ product = Product.parse(e)
152
+ else
153
+ product = ONIX21::Product.parse(e)
154
+ end
155
+ product.default_language_of_text = @default_language_of_text
156
+ product.default_currency_code = @default_currency_code
157
+ @products << product
174
158
  end
175
-
176
- when tag_match("Product")
177
- product=Product.parse(xml.root)
178
- product.default_language_of_text=@default_language_of_text
179
- product.default_currency_code=@default_currency_code
180
- @products << product
159
+ end
160
+ when tag_match("Product")
161
+ product = nil
162
+ if self.version >= 300
163
+ product = Product.parse(root)
164
+ else
165
+ product = ONIX21::Product.parse(root)
166
+ end
167
+ product.default_language_of_text = @default_language_of_text
168
+ product.default_currency_code = @default_currency_code
169
+ @products << product
181
170
  end
182
-
183
171
  init_vault
184
172
  end
185
173
  end
186
-
187
174
  end
@@ -4,71 +4,51 @@ require 'onix/date'
4
4
 
5
5
  module ONIX
6
6
  class Price < SubsetDSL
7
- element "PriceType", :subset
8
- element "PriceQualifier", :subset
9
- element "DiscountCoded", :subset
7
+ element "PriceType", :subset, :shortcut => :type
8
+ element "PriceQualifier", :subset, :shortcut => :qualifier
9
+ element "DiscountCoded", :subset, :shortcut => :discount
10
10
  element "PriceStatus", :subset
11
- elements "PriceDate", :subset
11
+ elements "PriceDate", :subset, :shortcut => :dates
12
12
  element "PriceAmount", :float,
13
13
  {
14
- :parse_lambda => lambda { |v| (v*100).round },
15
- :serialize_lambda => lambda {|v| v/100.0}
14
+ :shortcut => :amount,
15
+ :parse_lambda => lambda { |v| (v * 100).round },
16
+ :serialize_lambda => lambda { |v| v / 100.0 }
16
17
  }
17
18
  element "Tax", :subset
18
- element "CurrencyCode", :text
19
+ element "CurrencyCode", :text, :shortcut => :currency
19
20
  element "Territory", :subset
20
21
 
21
- # shortcuts
22
- def dates
23
- @price_dates
24
- end
25
-
26
- def amount
27
- @price_amount
28
- end
29
-
30
- def type
31
- @price_type
32
- end
33
-
34
- def currency
35
- @currency_code
36
- end
37
-
38
- def qualifier
39
- @price_qualifier
40
- end
41
-
42
- def discount
43
- @discount_coded
44
- end
22
+ # @!group High level
45
23
 
24
+ # price from date
25
+ # @return [Date]
46
26
  def from_date
47
- dt=@price_dates.from_date.first
27
+ dt = @price_dates.from_date.first
48
28
  if dt
49
29
  dt.date
50
- else
51
- nil
52
30
  end
53
31
  end
54
32
 
33
+ # price until date
34
+ # @return [Date]
55
35
  def until_date
56
- dt=@price_dates.until_date.first
36
+ dt = @price_dates.until_date.first
57
37
  if dt
58
38
  dt.date
59
- else
60
- nil
61
39
  end
62
40
  end
63
41
 
42
+ # does the price include taxes ?
43
+ # @return [Boolean]
64
44
  def including_tax?
65
- if self.type.human=~/IncludingTax/
45
+ if self.type.human =~ /IncludingTax/
66
46
  true
67
47
  else
68
48
  false
69
49
  end
70
50
  end
71
51
 
52
+ # @!endgroup
72
53
  end
73
-
74
54
  end
@@ -1,3 +1,4 @@
1
+ require 'forwardable'
1
2
  require 'onix/helper'
2
3
  require 'onix/code'
3
4
  require 'onix/entity'
@@ -6,676 +7,205 @@ require 'onix/publishing_detail'
6
7
  require 'onix/collateral_detail'
7
8
  require 'onix/related_material'
8
9
  require 'onix/supporting_resource'
9
- require 'onix/subject'
10
- require 'onix/contributor'
11
10
  require 'onix/product_supply'
12
11
  require 'onix/territory'
13
12
  require 'onix/error'
13
+ require 'onix/product_supplies_extractor'
14
14
 
15
15
  module ONIX
16
- # flattened supplies extractor
17
- module ProductSuppliesExtractor
18
- # class must define a product_supplies returning an Array of objects responding to :
19
- # - availability_date (Date)
20
- # - countries (country code Array)
21
-
22
- # :category: High level
23
- # flattened supplies with prices
24
- #
25
- # supplies is a hash symbol array in the form :
26
- # [{:available=>bool,
27
- # :availability_date=>date,
28
- # :including_tax=>bool,
29
- # :currency=>string,
30
- # :territory=>string,
31
- # :suppliers=>[Supplier,...],
32
- # :prices=>[{:amount=>int,
33
- # :from_date=>date,
34
- # :until_date=>date,
35
- # :tax=>{:amount=>int, :rate_percent=>float}}]}]
36
- def supplies(keep_all_prices_dates=false)
37
- supplies=[]
38
-
39
- # add territories if missing
40
- if self.product_supplies
41
- self.product_supplies.each do |ps|
42
- ps.supply_details.each do |sd|
43
- sd.prices.each do |p|
44
- supply={}
45
- supply[:suppliers]=sd.suppliers
46
- supply[:available]=sd.available?
47
- supply[:availability_date]=sd.availability_date
48
-
49
- unless supply[:availability_date]
50
- if ps.availability_date
51
- supply[:availability_date]=ps.market_publishing_detail.availability_date
52
- end
53
- end
54
- supply[:price]=p.amount
55
- supply[:qualifier]=p.qualifier.human if p.qualifier
56
- supply[:including_tax]=p.including_tax?
57
- if !p.territory or p.territory.countries.length==0
58
- supply[:territory]=[]
59
- supply[:territory]=ps.countries
60
-
61
- if supply[:territory].length==0
62
- if @publishing_detail
63
- supply[:territory]=self.countries_rights
64
- end
65
- end
66
- else
67
- supply[:territory]=p.territory.countries
68
- end
69
- supply[:from_date]=p.from_date
70
- supply[:until_date]=p.until_date
71
- supply[:currency]=p.currency
72
- supply[:tax]=p.tax
73
-
74
- unless supply[:availability_date]
75
- if @publishing_detail
76
- supply[:availability_date]=@publishing_detail.publication_date
77
- end
78
- end
79
-
80
- supplies << supply
81
- end
82
- end
83
- end
84
- end
85
-
86
- grouped_supplies={}
87
- supplies.each do |supply|
88
- pr_key="#{supply[:available]}_#{supply[:including_tax]}_#{supply[:currency]}_#{supply[:territory].join('_')}"
89
- grouped_supplies[pr_key]||=[]
90
- grouped_supplies[pr_key] << supply
91
- end
92
-
93
- nb_suppliers = supplies.map{|s| s[:suppliers][0].name}.uniq.length
94
- # render prices sequentially with dates
95
- grouped_supplies.each do |ksup, supply|
96
- if supply.length > 1
97
- global_price=supply.select{|p| not p[:from_date] and not p[:until_date]}
98
- global_price=global_price.first
99
-
100
- if global_price
101
- if nb_suppliers > 1
102
- grouped_supplies[ksup] += self.prices_with_periods(supply, global_price)
103
- else
104
- grouped_supplies[ksup] = self.prices_with_periods(supply, global_price)
105
- end
106
- grouped_supplies[ksup].uniq!
107
- else
108
- # remove explicit from date
109
- 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
110
- if explicit_from
111
- explicit_from[:from_date]=nil unless keep_all_prices_dates
112
- end
113
- end
114
-
115
-
116
- else
117
- supply.each do |s|
118
- if s[:from_date] and s[:availability_date] and s[:from_date] >= s[:availability_date]
119
- s[:availability_date]=s[:from_date]
120
- end
121
- s[:from_date]=nil unless keep_all_prices_dates
122
-
123
- end
124
- end
125
- end
126
-
127
- # merge by territories
128
- grouped_territories_supplies={}
129
- grouped_supplies.each do |ksup,supply|
130
- fsupply=supply.first
131
- pr_key="#{fsupply[:available]}_#{fsupply[:including_tax]}_#{fsupply[:currency]}"
132
- supply.each do |s|
133
- pr_key+="_#{s[:price]}_#{s[:from_date]}_#{s[:until_date]}"
134
- end
135
- grouped_territories_supplies[pr_key]||=[]
136
- grouped_territories_supplies[pr_key] << supply
137
- end
138
-
139
- supplies=[]
140
-
141
- grouped_territories_supplies.each do |ksup,supply|
142
- fsupply=supply.first.first
143
- supplies << {:including_tax=>fsupply[:including_tax],:currency=>fsupply[:currency],
144
- :territory=>supply.map{|fs| fs.map{|s| s[:territory]}}.flatten.uniq,
145
- :available=>fsupply[:available],
146
- :availability_date=>fsupply[:availability_date],
147
- :suppliers=>fsupply[:suppliers],
148
- :prices=>supply.first.map{|s|
149
-
150
- s[:amount]=s[:price]
151
- s.delete(:price)
152
- s.delete(:available)
153
- s.delete(:currency)
154
- s.delete(:availability_date)
155
- s.delete(:including_tax)
156
- s.delete(:territory)
157
- s
158
- }}
159
- end
160
-
161
- supplies
162
- end
163
-
164
- # add missing periods when they can be guessed
165
- def prices_with_periods(supplies, global_supply)
166
- complete_supplies = supplies.select{ |supply| supply[:from_date] && supply[:until_date] }.sort_by { |supply| supply[:from_date] }
167
- missing_start_period_supplies = supplies.select{ |supply| supply[:from_date] && !supply[:until_date] }.sort_by { |supply| supply[:from_date] }
168
- missing_end_period_supplies = supplies.select{ |supply| !supply[:from_date] && supply[:until_date] }.sort_by { |supply| supply[:until_date] }
169
-
170
- return [global_supply] if [complete_supplies, missing_start_period_supplies, missing_end_period_supplies].all? {|supply| supply.empty? }
171
-
172
- return self.add_missing_periods(complete_supplies, global_supply) unless complete_supplies.empty?
173
-
174
- without_start = missing_start_period_supplies.length == 1 && complete_supplies.empty? && missing_end_period_supplies.empty?
175
- without_end = missing_end_period_supplies.length == 1 && complete_supplies.empty? && missing_start_period_supplies.empty?
176
-
177
- return self.add_starting_period(missing_start_period_supplies.first, global_supply) if without_start
178
- return self.add_ending_period(missing_end_period_supplies.first, global_supply) if without_end
179
-
180
- [global_supply]
181
- end
182
-
183
- def add_missing_periods(supplies, global_supply)
184
- new_supplies = []
185
-
186
- supplies.each.with_index do |supply, index|
187
- new_supplies << global_supply.dup.tap{ |start_sup| start_sup[:until_date] = supply[:from_date] - 1 } if index == 0
188
-
189
- if index > 0 && index != supplies.length
190
- new_supplies << global_supply.dup.tap do |missing_supply|
191
- missing_supply[:from_date] = supplies[index - 1][:until_date] + 1
192
- missing_supply[:until_date] = supply[:from_date] - 1
193
- end
194
- end
195
-
196
- new_supplies << supply
197
-
198
- new_supplies << global_supply.dup.tap{ |end_sup| end_sup[:from_date] = supply[:until_date] + 1 } if index == supplies.length - 1
199
- end
200
-
201
- new_supplies
202
- end
203
-
204
- def add_starting_period(supply, global_supply)
205
- missing_supply = global_supply.dup
206
- missing_supply[:until_date] = supply[:from_date] - 1
207
-
208
- [missing_supply, supply]
209
- end
210
-
211
- def add_ending_period(supply, global_supply)
212
- missing_supply = global_supply.dup
213
- missing_supply[:from_date] = supply[:until_date] + 1
214
-
215
- [supply, missing_supply]
216
- end
217
-
218
- # :category: High level
219
- # flattened supplies only including taxes
220
- def supplies_including_tax
221
- self.supplies.select{|p| p[:including_tax]}
222
- end
223
-
224
- # :category: High level
225
- # flattened supplies only excluding taxes
226
- def supplies_excluding_tax
227
- self.supplies.select{|p| not p[:including_tax]}
228
- end
229
-
230
- # :category: High level
231
- # flattened supplies with default tax (excluding tax for US and CA, including otherwise)
232
- def supplies_with_default_tax
233
- self.supplies_including_tax + self.supplies_excluding_tax.select{|s| ["CAD","USD"].include?(s[:currency])}
234
- end
235
-
236
- # :category: High level
237
- # flattened supplies for country
238
- def supplies_for_country(country,currency=nil)
239
- country_supplies=self.supplies
240
- if currency
241
- country_supplies=country_supplies.select{|s| s[:currency]==currency}
242
- end
243
- country_supplies.select{|s|
244
- if s[:territory].include?(country)
245
- true
246
- else
247
- false
248
- end
249
- }
250
- end
251
-
252
- # :category: High level
253
- # price amount for given +currency+ and country at time
254
- def at_time_price_amount_for(time,currency,country=nil)
255
- sups=self.supplies_with_default_tax.select { |p| p[:currency]==currency }
256
- if country
257
- sups=sups.select{|p| p[:territory].include?(country)}
258
- end
259
- if sups.length > 0
260
- # exclusive
261
- sup=sups.first[:prices].select { |p|
262
- (!p[:from_date] or p[:from_date].to_date <= time.to_date) and
263
- (!p[:until_date] or p[:until_date].to_date > time.to_date)
264
- }.first
265
-
266
- if sup
267
- sup[:amount]
268
- else
269
- # or inclusive
270
- sup=sups.first[:prices].select { |p|
271
- (!p[:from_date] or p[:from_date].to_date <= time.to_date) and
272
- (!p[:until_date] or p[:until_date].to_date >= time.to_date)
273
- }.first
274
-
275
- if sup
276
- sup[:amount]
277
- else
278
- nil
279
- end
280
- end
281
-
282
- else
283
- nil
284
- end
285
- end
286
-
287
- # :category: High level
288
- # current price amount for given +currency+ and country
289
- def current_price_amount_for(currency,country=nil)
290
- at_time_price_amount_for(Time.now,currency,country)
291
- end
292
- end
293
-
294
16
  class Product < SubsetDSL
17
+ extend Forwardable
295
18
  include EanMethods
296
19
  include IsbnMethods
297
20
  include ProprietaryIdMethods
21
+ include ProductSuppliesExtractor
298
22
 
299
23
  element "RecordReference", :text
300
24
  element "NotificationType", :subset
301
25
  element "RecordSourceName", :text
302
- elements "ProductIdentifier", :subset
26
+ elements "ProductIdentifier", :subset, :shortcut => :identifiers
303
27
  element "DescriptiveDetail", :subset
304
28
  element "CollateralDetail", :subset
305
29
  element "PublishingDetail", :subset
306
30
  element "RelatedMaterial", :subset
307
31
  elements "ProductSupply", :subset
308
32
 
309
- # shortcuts
310
- def identifiers
311
- @product_identifiers
312
- end
313
-
314
33
  # default LanguageCode from ONIXMessage
315
34
  attr_accessor :default_language_of_text
316
35
  # default code from ONIXMessage
317
36
  attr_accessor :default_currency_code
318
37
 
319
- include ProductSuppliesExtractor
320
-
321
- # :category: High level
322
- # product title string
323
- def title
324
- @descriptive_detail.title
325
- end
326
-
327
- # :category: High level
328
- # product subtitle string
329
- def subtitle
330
- @descriptive_detail.subtitle
331
- end
332
-
333
- # :category: High level
334
- # product description string including HTML
335
- def description
336
- if @collateral_detail
337
- @collateral_detail.description
338
- else
339
- nil
340
- end
341
- end
38
+ # @!group Shortcuts
342
39
 
343
- # :category: High level
344
- # product larger front cover URL string
345
- def frontcover_url
346
- if @collateral_detail
347
- @collateral_detail.frontcover_url
348
- end
349
- end
350
-
351
- # :category: High level
352
- # product larger front cover last updated date
353
- def frontcover_last_updated
354
- if @collateral_detail
355
- @collateral_detail.frontcover_last_updated
356
- end
357
- end
358
-
359
- # :category: High level
360
- # product larger front cover mimetype
361
- def frontcover_mimetype
362
- if @collateral_detail
363
- @collateral_detail.frontcover_mimetype
364
- end
365
- end
366
-
367
- # :category: High level
368
- # ePub sample URL string
369
- def epub_sample_url
370
- if @collateral_detail
371
- @collateral_detail.epub_sample_url
372
- end
373
- end
374
-
375
- # :category: High level
376
- # ePub sample last updated date
377
- def epub_sample_last_updated
378
- if @collateral_detail
379
- @collateral_detail.epub_sample_last_updated
380
- end
381
- end
382
-
383
- # :category: High level
384
- # ePub sample mimetype
385
- def epub_sample_mimetype
386
- if @collateral_detail
387
- @collateral_detail.epub_sample_mimetype
388
- end
389
- end
390
-
391
- # :category: High level
392
- # product edition number
393
- def edition_number
394
- @descriptive_detail.edition_number
395
- end
40
+ # @return (see PublishingDetail#publishers)
41
+ def publishers
42
+ @publishing_detail ? @publishing_detail.publishers : []
43
+ end
44
+
45
+ # !@endgroup
46
+
47
+ # @!group High level
48
+
49
+ def_delegator :descriptive_detail, :title
50
+ def_delegator :descriptive_detail, :subtitle
51
+ def_delegator :descriptive_detail, :edition_number
52
+ def_delegator :descriptive_detail, :publisher_collection_title
53
+ def_delegator :descriptive_detail, :bisac_categories # deprecated
54
+ def_delegator :descriptive_detail, :clil_categories # deprecated
55
+ def_delegator :descriptive_detail, :bisac_categories_codes
56
+ def_delegator :descriptive_detail, :clil_categories_codes
57
+ def_delegator :descriptive_detail, :keywords
58
+ def_delegator :descriptive_detail, :protection_type
59
+ def_delegator :descriptive_detail, :protections
60
+ def_delegator :descriptive_detail, :drmized?
61
+ def_delegator :descriptive_detail, :digital?
62
+ def_delegator :descriptive_detail, :audio?
63
+ def_delegator :descriptive_detail, :streaming?
64
+ def_delegator :descriptive_detail, :bundle?
65
+ def_delegator :descriptive_detail, :parts
66
+ def_delegator :descriptive_detail, :filesize
67
+ def_delegator :descriptive_detail, :audio_formats
68
+ def_delegator :descriptive_detail, :audio_format
69
+ def_delegator :descriptive_detail, :file_format
70
+ def_delegator :descriptive_detail, :file_mimetype
71
+ def_delegator :descriptive_detail, :file_description
72
+ def_delegator :descriptive_detail, :reflowable?
73
+ def_delegator :descriptive_detail, :pages
74
+ def_delegator :descriptive_detail, :subjects
75
+ def_delegator :descriptive_detail, :form_details
76
+ def_delegator :descriptive_detail, :contributors
77
+
78
+ def_delegator :collateral_detail, :description
79
+ def_delegator :collateral_detail, :frontcover_url
80
+ def_delegator :collateral_detail, :frontcover_last_updated
81
+ def_delegator :collateral_detail, :frontcover_mimetype
82
+ def_delegator :collateral_detail, :epub_sample_url
83
+ def_delegator :collateral_detail, :epub_sample_last_updated
84
+ def_delegator :collateral_detail, :epub_sample_mimetype
85
+
86
+ def_delegator :publishing_detail, :publication_date
87
+ def_delegator :publishing_detail, :embargo_date
88
+ def_delegator :publishing_detail, :preorder_embargo_date
89
+ def_delegator :publishing_detail, :public_announcement_date
90
+ def_delegator :publishing_detail, :sales_restriction
91
+ def_delegator :publishing_detail, :imprint
92
+ def_delegator :publishing_detail, :publisher
396
93
 
397
94
  # product LanguageCode of text
398
95
  def language_of_text
399
96
  @descriptive_detail.language_of_text || @default_language_of_text
400
97
  end
401
98
 
402
- # :category: High level
403
99
  # product language code string of text (eg: fre)
100
+ # @return [String]
404
101
  def language_code_of_text
405
102
  if self.language_of_text
406
103
  self.language_of_text.code
407
104
  end
408
105
  end
409
106
 
410
- # :category: High level
411
107
  # product language name string of text (eg: French)
108
+ # @return [String]
412
109
  def language_name_of_text
413
110
  if self.language_of_text
414
111
  self.language_of_text.human
415
112
  end
416
113
  end
417
114
 
418
- # :category: High level
419
- # publisher collection title
420
- def publisher_collection_title
421
- @descriptive_detail.publisher_collection_title
422
- end
423
-
424
- def subjects
425
- @descriptive_detail.subjects
426
- end
427
-
428
- # BISAC categories Subject
429
- def bisac_categories
430
- @descriptive_detail.bisac_categories
431
- end
432
-
433
- # :category: High level
434
- # BISAC categories identifiers string array (eg: FIC000000)
435
- def bisac_categories_codes
436
- self.bisac_categories.map{|c| c.code}.uniq
437
- end
438
-
439
- # CLIL categories Subject
440
- def clil_categories
441
- @descriptive_detail.clil_categories
442
- end
443
-
444
- # :category: High level
445
- # CLIL categories identifier string array
446
- def clil_categories_codes
447
- self.clil_categories.map{|c| c.code}.uniq
448
- end
449
-
450
- # :category: High level
451
- # keywords string array
452
- def keywords
453
- @descriptive_detail.keywords
454
- end
455
-
456
- # :category: High level
457
- # Protection type string (None, Watermarking, Drm, AdobeDrm)
458
- def protection_type
459
- @descriptive_detail.protection_type
460
- end
461
-
462
- # :category: High level
463
- # List of protections type string (None, Watermarking, DRM, AdobeDRM)
464
- def protections
465
- @descriptive_detail.protections
466
- end
467
-
468
- # :category: High level
469
- # product has DRM ?
470
- def drmized?
471
- if @protections.any? {|p| p =~ /Drm/ }
472
- true
473
- else
474
- false
475
- end
476
- end
477
-
478
- # :category: High level
479
- # is product digital ?
480
- def digital?
481
- @descriptive_detail.digital?
482
- end
483
-
484
- # :category: High level
485
- # is product audio ?
486
- def audio?
487
- @descriptive_detail.audio?
488
- end
489
-
490
- # :category: High level
491
- # is product digital ?
492
- def streaming?
493
- @descriptive_detail.streaming?
494
- end
495
-
496
- # :category: High level
497
- # is product a bundle of multiple parts ?
498
- def bundle?
499
- @descriptive_detail.bundle?
500
- end
501
-
115
+ # product can be sold separately ?
116
+ # @return [Boolean]
502
117
  def sold_separately?
503
- @product_supplies.map{|ps| ps.supply_details.map{|sd| sd.sold_separately?}.flatten}.flatten.uniq.first
118
+ @product_supplies.map { |product_supply|
119
+ product_supply.supply_details.map { |supply_detail| supply_detail.sold_separately? }.flatten
120
+ }.flatten.uniq.first
504
121
  end
505
122
 
506
- # :category: High level
507
- # bundle ProductPart array
508
- def parts
509
- @descriptive_detail.parts
510
- end
511
-
512
- # :category: High level
513
- # digital file filesize in bytes
514
- def filesize
515
- @descriptive_detail.filesize
516
- end
517
-
518
- # :category: High level
519
- # audio formats array
520
- def audio_formats
521
- @descriptive_detail.audio_formats
522
- end
523
-
524
- # :category: High level
525
- # audio format string ()
526
- def audio_format
527
- @descriptive_detail.audio_format
528
- end
529
-
530
- # :category: High level
531
- # digital file format string (Epub,Pdf,Mobipocket)
532
- def file_format
533
- @descriptive_detail.file_format
534
- end
535
-
536
- def form_details
537
- @descriptive_detail.form_details
538
- end
539
-
540
- def reflowable?
541
- @descriptive_detail.reflowable?
542
- end
543
-
544
- # :category: High level
545
- # digital file mimetype (Epub,Pdf,Mobipocket)
546
- def file_mimetype
547
- @descriptive_detail.file_mimetype
548
- end
549
-
550
- # :category: High level
551
- # digital file description string
552
- def file_description
553
- @descriptive_detail.file_description
554
- end
555
-
556
- # :category: High level
557
123
  # raw file description string without HTML
124
+ # @return [String]
558
125
  def raw_file_description
559
126
  if @descriptive_detail.file_description
560
- Helper.strip_html(@descriptive_detail.file_description).gsub(/\s+/," ").strip
561
- else
562
- nil
127
+ Helper.strip_html(@descriptive_detail.file_description).gsub(/\s+/, " ").strip
563
128
  end
564
129
  end
565
130
 
566
- # :category: High level
567
- # page count
568
- def pages
569
- @descriptive_detail.pages
570
- end
571
-
572
- # :category: High level
573
131
  # raw book description string without HTML
132
+ # @return [String]
574
133
  def raw_description
575
134
  if self.description
576
- Helper.strip_html(self.description).gsub(/\s+/," ").strip
577
- else
578
- nil
579
- end
580
- end
581
-
582
- def publishers
583
- if @publishing_detail
584
- @publishing_detail.publishers
585
- else
586
- []
135
+ Helper.strip_html(self.description).gsub(/\s+/, " ").strip
587
136
  end
588
137
  end
589
138
 
590
- def publisher
591
- if @publishing_detail
592
- @publishing_detail.publisher
593
- else
594
- nil
595
- end
596
- end
597
-
598
- # :category: High level
599
139
  # publisher name string, if multiple publishers are found, then they are concatenated with " / "
140
+ # @return [String]
600
141
  def publisher_name
601
142
  if self.publishers.length > 0
602
- self.publishers.map{|p| p.name}.join(" / ")
143
+ self.publishers.map { |p| p.name }.join(" / ")
603
144
  end
604
145
  end
605
146
 
606
- # :category: High level
607
147
  # publisher GLN string, nil if multiple publishers are found
148
+ # @return [String]
608
149
  def publisher_gln
609
- if self.publishers.length==1
150
+ if self.publishers.length == 1
610
151
  self.publisher.gln
611
152
  end
612
153
  end
613
154
 
614
- def imprint
615
- if @publishing_detail
616
- @publishing_detail.imprint
617
- else
618
- nil
619
- end
620
- end
621
-
622
- # :category: High level
623
155
  # imprint name string
156
+ # @return [String]
624
157
  def imprint_name
625
158
  if self.imprint
626
159
  self.imprint.name
627
- else
628
- nil
629
160
  end
630
161
  end
631
162
 
632
- # :category: High level
633
163
  # imprint GLN string
164
+ # @return [String]
634
165
  def imprint_gln
635
166
  if self.imprint
636
167
  self.imprint.gln
637
168
  end
638
169
  end
639
170
 
171
+ # product distributors names
172
+ # @return [Array<String>]
640
173
  def distributors
641
- @product_supplies.map{|ps| ps.distributors}.flatten.uniq{|d| d.name}
174
+ @product_supplies.map { |ps| ps.distributors }.flatten.uniq { |d| d.name }
642
175
  end
643
176
 
644
- # product distributor
177
+ # product distributor name
178
+ # @return [String]
645
179
  def distributor
646
180
  if self.distributors.length > 0
647
- if self.distributors.length==1
648
- self.distributors.first
649
- else
650
- raise ExpectsOneButHasSeveral, self.distributors.map(&:name)
651
- end
181
+ if self.distributors.length == 1
182
+ self.distributors.first
183
+ else
184
+ raise ExpectsOneButHasSeveral, self.distributors.map(&:name)
185
+ end
652
186
  else
653
187
  nil
654
188
  end
655
189
  end
656
190
 
657
- # :category: High level
658
191
  # product distributor name string
192
+ # @return [String]
659
193
  def distributor_name
660
194
  if self.distributor
661
195
  self.distributor.name
662
- else
663
- nil
664
196
  end
665
197
  end
666
198
 
667
- # :category: High level
668
199
  # product distributor GLN string
200
+ # @return [String]
669
201
  def distributor_gln
670
202
  if self.distributor
671
203
  self.distributor.gln
672
- else
673
- nil
674
204
  end
675
205
  end
676
206
 
677
- # :category: High level
678
207
  # return every related subset
208
+ # @return [Array]
679
209
  def related
680
210
  if @related_material
681
211
  (@related_material.related_products + @related_material.related_works)
@@ -684,16 +214,16 @@ module ONIX
684
214
  end
685
215
  end
686
216
 
687
- # :category: High level
688
- # paper linking RelatedProduct
217
+ # first part RelatedProduct
218
+ # @return [RelatedProduct]
689
219
  def part_of_product
690
220
  if @related_material
691
221
  @related_material.part_of_products.first
692
222
  end
693
223
  end
694
224
 
695
- # :category: High level
696
- # paper linking RelatedProduct
225
+ # first paper linking RelatedProduct
226
+ # @return [RelatedProduct]
697
227
  def print_product
698
228
  if @related_material
699
229
  @related_material.print_products.first
@@ -701,146 +231,120 @@ module ONIX
701
231
  end
702
232
 
703
233
  # DEPRECATED see print_product instead
234
+ # @return [RelatedProduct]
704
235
  def paper_linking
705
236
  self.print_product
706
237
  end
707
238
 
708
- # :category: High level
709
- # date of publication
710
- def publication_date
711
- if @publishing_detail
712
- @publishing_detail.publication_date
713
- end
714
- end
715
-
716
- # date of embargo
717
- def embargo_date
718
- if @publishing_detail
719
- @publishing_detail.embargo_date
720
- end
721
- end
722
-
723
- def preorder_embargo_date
724
- if @publishing_detail
725
- @publishing_detail.preorder_embargo_date
726
- end
727
- end
728
-
729
- def public_announcement_date
730
- if @publishing_detail
731
- @publishing_detail.public_announcement_date
732
- end
733
- end
734
-
735
- def sales_restriction
736
- if @publishing_detail
737
- @publishing_detail.sales_restriction
738
- end
739
- end
740
-
741
- # :category: High level
742
239
  # product countries rights string array
240
+ # @return [Array<String>]
743
241
  def countries_rights
744
- countries=[]
242
+ countries = []
745
243
  if @publishing_detail
746
- countries+=@publishing_detail.sales_rights.map{|sr| sr.territory.countries}.flatten.uniq
244
+ countries += @publishing_detail.sales_rights.map { |sale_right| sale_right.territory.countries }.flatten.uniq
747
245
  end
748
246
 
749
247
  if @product_supplies
750
- countries+=@product_supplies.map{|ps| ps.markets.map{|m| m.territory.countries}.flatten}.flatten.uniq
248
+ countries += @product_supplies.map { |product_supply|
249
+ product_supply.markets.map { |market| market.territory.countries }.flatten
250
+ }.flatten.uniq
751
251
  end
752
252
 
753
253
  countries.uniq
754
254
  end
755
255
 
756
- # :category: High level
256
+ # all images
757
257
  def illustrations
758
258
  return [] unless @collateral_detail && @collateral_detail.supporting_resources
759
259
 
760
260
  @collateral_detail.supporting_resources.image.map do |image_resource|
761
261
  {
762
- :url => image_resource.versions.last.links.first.strip,
763
- :type => image_resource.type.human,
764
- :width => image_resource.versions.last.image_width,
765
- :height => image_resource.versions.last.image_height,
766
- :caption => image_resource.caption,
767
- :format_code => image_resource.versions.last.file_format,
768
- :updated_at => image_resource.versions.last.last_updated_utc
262
+ :url => image_resource.versions.last.links.first.strip,
263
+ :type => image_resource.type.human,
264
+ :width => image_resource.versions.last.image_width,
265
+ :height => image_resource.versions.last.image_height,
266
+ :caption => image_resource.caption,
267
+ :format_code => image_resource.versions.last.file_format,
268
+ :updated_at => image_resource.versions.last.last_updated_utc
769
269
  }
770
270
  end
771
271
  end
772
272
 
773
- # :category: High level
273
+ # all excerpts
774
274
  def excerpts
775
275
  return [] unless @collateral_detail && @collateral_detail.supporting_resources
776
276
 
777
- @collateral_detail.supporting_resources.sample_content.human_code_match(:resource_mode,["Text","Multimode"]).map do |resource|
277
+ @collateral_detail.supporting_resources.sample_content.human_code_match(:resource_mode, ["Text", "Multimode"]).map do |resource|
778
278
  {
779
- :url => resource.versions.last.links.first.strip,
780
- :form => resource.versions.last.form.human,
781
- :md5 => resource.versions.last.md5_hash,
782
- :format_code => resource.versions.last.file_format,
783
- :updated_at => resource.versions.last.last_updated_utc
279
+ :url => resource.versions.last.links.first.strip,
280
+ :form => resource.versions.last.form.human,
281
+ :md5 => resource.versions.last.md5_hash,
282
+ :format_code => resource.versions.last.file_format,
283
+ :updated_at => resource.versions.last.last_updated_utc
784
284
  }
785
285
  end
786
286
  end
787
287
 
288
+ # available product supplies
289
+ # @return [Array<ProductSupply>]
788
290
  def available_product_supplies
789
- @product_supplies.select{|ps| ps.available?}
291
+ @product_supplies.select { |product_supply| product_supply.available? }
790
292
  end
791
293
 
792
- # :category: High level
793
294
  # is product available ?
295
+ # @return [Boolean]
794
296
  def available?
795
297
  self.available_product_supplies.length > 0 and not self.delete?
796
298
  end
797
299
 
798
- # :category: High level
799
300
  # is product available for given +country+ ?
301
+ # @return [Boolean]
800
302
  def available_for_country?(country)
801
- self.supplies_for_country(country).select{|s| s[:available]}.length > 0 and self.available?
303
+ self.supplies_for_country(country).select { |s| s[:available] }.length > 0 and self.available?
802
304
  end
803
305
 
804
- # :category: High level
805
306
  # is product price to be announced ?
307
+ # @return [Boolean]
806
308
  def price_to_be_announced?
807
309
  unless self.product_supplies.empty? || self.product_supplies.first.supply_details.empty?
808
310
  unpriced_item_type = self.product_supplies.first.supply_details.first.unpriced_item_type
809
311
  end
810
- unpriced_item_type ? unpriced_item_type.human=="PriceToBeAnnounced" : false
312
+ unpriced_item_type ? unpriced_item_type.human == "PriceToBeAnnounced" : false
811
313
  end
812
314
 
813
- # :category: High level
814
- # is a deletion notification ?
315
+ # is product a deletion notification ?
316
+ # @return [Boolean]
815
317
  def delete?
816
- self.notification_type.human=="Delete"
817
- end
818
-
819
- # :category: High level
820
- # Contributor array
821
- def contributors
822
- @descriptive_detail.contributors
318
+ self.notification_type.human == "Delete"
823
319
  end
824
320
 
825
- # :category: High level
826
321
  # List of ONIX outlets values
322
+ # @return [Array<String>]
827
323
  def onix_outlets_values
828
324
  if @publishing_detail
829
- @publishing_detail.sales_rights.map { |sri|
830
- sri.sales_restrictions.select { |sr| (!sr.start_date or sr.start_date <= Date.today) and (!sr.end_date or sr.end_date >= Date.today) }.map { |sr|
831
- sr.sales_outlets.select { |so|
832
- so.identifier and so.identifier.type.human=="OnixSalesOutletIdCode" }.map { |so| so.identifier.value } } }.flatten
325
+ @publishing_detail.sales_rights.map { |sales_right|
326
+ sales_right.sales_restrictions.select { |sales_restriction|
327
+ (!sales_restriction.start_date or sales_restriction.start_date <= Date.today) and
328
+ (!sales_restriction.end_date or sales_restriction.end_date >= Date.today)
329
+ }.map { |sale_right|
330
+ sale_right.sales_outlets.select { |sale_outlet|
331
+ sale_outlet.identifier and sale_outlet.identifier.type.human == "OnixSalesOutletIdCode" }.map { |sale_outlet|
332
+ sale_outlet.identifier.value
333
+ }
334
+ }
335
+ }.flatten
833
336
  else
834
337
  []
835
338
  end
836
339
  end
837
340
 
341
+ # @!endgroup
342
+
838
343
  def parse(n)
839
344
  super
840
345
  parts.each do |part|
841
- part.part_of=self
346
+ part.part_of = self
842
347
  end
843
348
  end
844
-
845
349
  end
846
350
  end