im_onix 1.0.2 → 1.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +4 -0
- data/.yardopts +1 -0
- data/Gemfile +8 -0
- data/LICENSE.md +7 -0
- data/README.md +3 -3
- data/Rakefile +10 -0
- data/bin/html_codelist_to_yml.rb +14 -15
- data/bin/onix_bench.rb +1 -0
- data/bin/onix_pp.rb +4 -8
- data/bin/onix_serialize.rb +27 -0
- data/doc-src/handlers.rb +154 -0
- data/im_onix.gemspec +32 -0
- data/lib/im_onix.rb +0 -1
- data/lib/onix/addressee.rb +10 -0
- data/lib/onix/code.rb +108 -282
- data/lib/onix/collateral_detail.rb +24 -17
- data/lib/onix/collection.rb +38 -0
- data/lib/onix/collection_sequence.rb +7 -0
- data/lib/onix/contributor.rb +40 -39
- data/lib/onix/date.rb +73 -109
- data/lib/onix/descriptive_detail.rb +90 -417
- data/lib/onix/discount_coded.rb +3 -16
- data/lib/onix/entity.rb +28 -62
- data/lib/onix/epub_usage_constraint.rb +7 -0
- data/lib/onix/epub_usage_limit.rb +6 -0
- data/lib/onix/extent.rb +39 -0
- data/lib/onix/helper.rb +25 -25
- data/lib/onix/identifier.rb +13 -54
- data/lib/onix/language.rb +8 -0
- data/lib/onix/market.rb +5 -0
- data/lib/onix/market_publishing_detail.rb +20 -0
- data/lib/onix/onix21.rb +76 -139
- data/lib/onix/onix_message.rb +87 -100
- data/lib/onix/price.rb +19 -39
- data/lib/onix/product.rb +141 -637
- data/lib/onix/product_form_feature.rb +7 -0
- data/lib/onix/product_part.rb +89 -0
- data/lib/onix/product_supplies_extractor.rb +275 -0
- data/lib/onix/product_supply.rb +17 -58
- data/lib/onix/publishing_detail.rb +16 -32
- data/lib/onix/related_material.rb +4 -3
- data/lib/onix/related_product.rb +9 -29
- data/lib/onix/related_work.rb +3 -17
- data/lib/onix/sales_outlet.rb +2 -10
- data/lib/onix/sales_restriction.rb +8 -21
- data/lib/onix/sales_rights.rb +1 -5
- data/lib/onix/sender.rb +12 -0
- data/lib/onix/serializer.rb +156 -0
- data/lib/onix/subject.rb +9 -30
- data/lib/onix/subset.rb +88 -78
- data/lib/onix/supply_detail.rb +42 -0
- data/lib/onix/supporting_resource.rb +29 -86
- data/lib/onix/tax.rb +9 -18
- data/lib/onix/territory.rb +23 -17
- data/lib/onix/title_detail.rb +22 -0
- data/lib/onix/title_element.rb +32 -0
- data/lib/onix/website.rb +3 -16
- 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
|
data/lib/onix/product_supply.rb
CHANGED
@@ -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{|
|
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{|
|
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{|
|
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{|
|
41
|
+
@supply_details.delete_if { |supply_detail| supply_detail.available? }
|
83
42
|
end
|
84
43
|
|
85
44
|
def available?
|