uktt 0.2.14

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,6 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task default: :spec
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'uktt'
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require 'irb'
14
+ IRB.start(__FILE__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'uktt/cli'
4
+ Uktt::CLI.start
@@ -0,0 +1,61 @@
1
+ require 'uktt/version'
2
+ require 'uktt/http'
3
+ require 'uktt/section'
4
+ require 'uktt/chapter'
5
+ require 'uktt/heading'
6
+ require 'uktt/commodity'
7
+ require 'uktt/monetary_exchange_rate'
8
+ require 'uktt/quota'
9
+ require 'uktt/pdf'
10
+
11
+ require 'yaml'
12
+ require 'psych'
13
+
14
+ module Uktt
15
+ API_HOST_PROD = 'https://www.trade-tariff.service.gov.uk/api'.freeze
16
+ API_HOST_LOCAL = 'http://localhost:3002/api'.freeze
17
+ API_VERSION = 'v1'.freeze
18
+ SECTION = 'sections'.freeze
19
+ CHAPTER = 'chapters'.freeze
20
+ HEADING = 'headings'.freeze
21
+ COMMODITY = 'commodities'.freeze
22
+ M_X_RATE = 'monetary_exchange_rates'.freeze
23
+ GOODS_NOMENCLATURE = 'goods_nomenclatures'.freeze
24
+ QUOTA = 'quotas'.freeze
25
+ PARENT_CURRENCY = 'EUR'.freeze
26
+
27
+ class Error < StandardError; end
28
+
29
+ # Configuration defaults
30
+ @config = {
31
+ host: Uktt::Http.api_host,
32
+ version: Uktt::Http.spec_version,
33
+ debug: false,
34
+ return_json: false,
35
+ currency: PARENT_CURRENCY
36
+ }
37
+
38
+ @valid_config_keys = @config.keys
39
+
40
+ # Configure through hash
41
+ def self.configure(opts = {})
42
+ opts.each {|k,v| @config[k.to_sym] = v if @valid_config_keys.include? k.to_sym}
43
+ end
44
+
45
+ # Configure through yaml file
46
+ def self.configure_with(path_to_yaml_file)
47
+ begin
48
+ config = YAML::load(IO.read(path_to_yaml_file))
49
+ rescue Errno::ENOENT
50
+ log(:warning, "YAML configuration file couldn't be found. Using defaults."); return
51
+ rescue Psych::SyntaxError
52
+ log(:warning, "YAML configuration file contains invalid syntax. Using defaults."); return
53
+ end
54
+
55
+ configure(config)
56
+ end
57
+
58
+ def self.config
59
+ @config
60
+ end
61
+ end
@@ -0,0 +1,57 @@
1
+ module Uktt
2
+ # A Chapter object for dealing with an API resource
3
+ class Chapter
4
+ attr_accessor :config, :chapter_id
5
+
6
+ def initialize(opts = {})
7
+ @chapter_id = opts[:chapter_id] || nil
8
+ Uktt.configure(opts)
9
+ @config = Uktt.config
10
+ end
11
+
12
+ def retrieve
13
+ return '@chapter_id cannot be nil' if @chapter_id.nil?
14
+
15
+ fetch "#{CHAPTER}/#{@chapter_id}.json"
16
+ end
17
+
18
+ def retrieve_all
19
+ fetch "#{CHAPTER}.json"
20
+ end
21
+
22
+ def goods_nomenclatures
23
+ return '@chapter_id cannot be nil' if @chapter_id.nil?
24
+
25
+ fetch "#{GOODS_NOMENCLATURE}/chapter/#{@chapter_id}.json"
26
+ end
27
+
28
+ def changes
29
+ return '@chapter_id cannot be nil' if @chapter_id.nil?
30
+
31
+ fetch "#{CHAPTER}/#{@chapter_id}/changes.json"
32
+ end
33
+
34
+ def note
35
+ return '@chapter_id cannot be nil' if @chapter_id.nil?
36
+
37
+ fetch "#{CHAPTER}/#{@chapter_id}/chapter_note.json"
38
+ end
39
+
40
+ def config=(new_opts = {})
41
+ merged_opts = Uktt.config.merge(new_opts)
42
+ Uktt.configure merged_opts
43
+ @chapter_id = merged_opts[:chapter_id] || @chapter_id
44
+ @config = Uktt.config
45
+ end
46
+
47
+ private
48
+
49
+ def fetch(resource)
50
+ Uktt::Http.new(@config[:host],
51
+ @config[:version],
52
+ @config[:debug])
53
+ .retrieve(resource,
54
+ @config[:return_json])
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,174 @@
1
+ require 'thor'
2
+ require 'uktt'
3
+
4
+ module Uktt
5
+ # Implemets a CLI using Thor
6
+ class CLI < Thor
7
+ class_option :host,
8
+ aliases: ['-h', '--host'],
9
+ type: :string,
10
+ desc: "Use specified API host, otherwise `#{API_HOST_LOCAL}`",
11
+ banner: 'http://localhost:3002'
12
+ class_option :version,
13
+ aliases: ['-a', '--api-version'],
14
+ type: :string,
15
+ desc: 'Request a specific API version, otherwise `v1`',
16
+ banner: 'v1'
17
+ class_option :debug,
18
+ aliases: ['-d', '--debug'],
19
+ type: :boolean,
20
+ desc: 'Show request and response headers, otherwise not shown',
21
+ banner: true
22
+ class_option :return_json,
23
+ aliases: ['-j', '--json'],
24
+ type: :boolean,
25
+ desc: 'Request JSON response, otherwise OpenStruct',
26
+ banner: true
27
+ class_option :prod,
28
+ aliases: ['-p', '--production'],
29
+ type: :string,
30
+ desc: "Use production API host, otherwise `#{API_HOST_LOCAL}`",
31
+ banner: true
32
+ class_option :goods, aliases: ['-g', '--goods'],
33
+ type: :string,
34
+ desc: 'Retrieve goods nomenclatures in this object',
35
+ banner: false
36
+ class_option :note, aliases: ['-n', '--note'],
37
+ type: :string,
38
+ desc: 'Retrieve a note for this object',
39
+ banner: false
40
+ class_option :changes, aliases: ['-c', '--changes'],
41
+ type: :string,
42
+ desc: 'Retrieve changes for this object',
43
+ banner: false
44
+
45
+ desc 'section', 'Retrieves a section'
46
+ def section(section_id)
47
+ if options[:goods] && options[:version] != 'v2'
48
+ puts 'V2 is required. Use `-a v2`'
49
+ return
50
+ elsif options[:changes]
51
+ puts 'Option not supported for this object'
52
+ return
53
+ end
54
+
55
+ uktt = Uktt::Section.new(options.merge(host: host, section_id: section_id))
56
+ puts uktt.send(action)
57
+ end
58
+
59
+ desc 'sections', 'Retrieves all sections'
60
+ def sections
61
+ puts Uktt::Section.new(options.merge(host: host)).retrieve_all
62
+ end
63
+
64
+ desc 'chapter', 'Retrieves a chapter'
65
+ def chapter(chapter_id)
66
+ if options[:goods] && options[:version] != 'v2'
67
+ puts 'V2 is required. Use `-a v2`'
68
+ return
69
+ end
70
+
71
+ uktt = Uktt::Chapter.new(options.merge(host: host, chapter_id: chapter_id))
72
+ puts uktt.send(action)
73
+ end
74
+
75
+ desc 'chapters', 'Retrieves all chapters'
76
+ def chapters
77
+ puts Uktt::Chapter.new(options.merge(host: host)).retrieve_all
78
+ end
79
+
80
+ desc 'heading', 'Retrieves a heading'
81
+ def heading(heading_id)
82
+ if options[:goods] && options[:version] != 'v2'
83
+ puts 'V2 is required. Use `-a v2`'
84
+ return
85
+ elsif options[:note]
86
+ puts 'Option not supported for this object'
87
+ return
88
+ end
89
+
90
+ uktt = Uktt::Heading.new(options.merge(host: host, heading_id: heading_id))
91
+ puts uktt.send(action)
92
+ end
93
+
94
+ desc 'commodity', 'Retrieves a commodity'
95
+ def commodity(commodity_id)
96
+ if options[:goods] || options[:note]
97
+ puts 'Option not supported for this object'
98
+ return
99
+ end
100
+
101
+ puts Uktt::Commodity.new(options.merge(host: host, commodity_id: commodity_id)).send(action)
102
+ end
103
+
104
+ desc 'monetary_exchange_rates', 'Retrieves monetary exchange rates'
105
+ def monetary_exchange_rates
106
+ puts Uktt::MonetaryExchangeRate.new(options.merge(host: host)).retrieve_all
107
+ end
108
+
109
+ desc 'pdf', 'Makes a PDF of a chapter'
110
+ method_option :filepath, aliases: ['-f', '--filepath'],
111
+ type: :string,
112
+ desc: 'Save PDF to path and name, otherwise saves in `pwd`',
113
+ banner: '`pwd`'
114
+ def pdf(chapter_id)
115
+ puts "Making a PDF for Chapter #{chapter_id}"
116
+ start_time = Time.now
117
+ puts "Finished #{Uktt::Pdf.new(options.merge(chapter_id: chapter_id)).make_chapter} in #{Time.now - start_time}"
118
+ end
119
+
120
+ desc 'test', 'Runs API specs'
121
+ def test
122
+ host, version, _json, _debug, _filepath = handle_class_options(options)
123
+ ver = version ? "VER=#{version} " : ''
124
+ prod = host == API_HOST_PROD ? 'PROD=true ' : ''
125
+ puts `#{ver}#{prod}bundle exec rspec ./spec/uktt_api_spec.rb`
126
+ end
127
+
128
+ desc 'info', 'Prints help for `uktt`'
129
+ method_option :version, aliases: ['-v', '--version']
130
+ def info
131
+ if options[:version]
132
+ puts Uktt::VERSION
133
+ elsif ARGV
134
+ help
135
+ else
136
+ help
137
+ end
138
+ end
139
+ default_task :info
140
+
141
+ no_commands do
142
+ def handle_class_options(options)
143
+ [
144
+ options[:host] || (options[:prod] ? API_HOST_PROD : API_HOST_LOCAL),
145
+ options[:api_version] || 'v1',
146
+ options[:json] || false,
147
+ options[:debug] || false,
148
+ options[:filepath] || nil,
149
+ options[:goods] || false,
150
+ options[:note] || false,
151
+ options[:changes] || false,
152
+ ]
153
+ end
154
+
155
+ def action
156
+ if options[:goods]
157
+ return :goods_nomenclatures
158
+ elsif options[:note]
159
+ return :note
160
+ elsif options[:changes]
161
+ return :changes
162
+ else
163
+ return :retrieve
164
+ end
165
+ end
166
+
167
+ def host
168
+ return ENV['HOST'] if ENV['HOST']
169
+
170
+ options[:prod] ? API_HOST_PROD : Uktt::Http.api_host
171
+ end
172
+ end
173
+ end
174
+ end
@@ -0,0 +1,61 @@
1
+ module Uktt
2
+ # A Commodity object for dealing with an API resource
3
+ class Commodity
4
+ attr_accessor :config, :commodity_id, :response
5
+
6
+ def initialize(opts = {})
7
+ @commodity_id = opts[:commodity_id] || nil
8
+ Uktt.configure(opts)
9
+ @config = Uktt.config
10
+ @response = nil
11
+ end
12
+
13
+ def retrieve
14
+ return '@commodity_id cannot be nil' if @commodity_id.nil?
15
+
16
+ fetch "#{COMMODITY}/#{@commodity_id}.json"
17
+ end
18
+
19
+ def changes
20
+ return '@commodity_id cannot be nil' if @commodity_id.nil?
21
+
22
+ fetch "#{COMMODITY}/#{@commodity_id}/changes.json"
23
+ end
24
+
25
+ def config=(new_opts = {})
26
+ merged_opts = Uktt.config.merge(new_opts)
27
+ Uktt.configure merged_opts
28
+ @commodity_id = merged_opts[:commodity_id] || @commodity_id
29
+ @config = Uktt.config
30
+ end
31
+
32
+ def find(id)
33
+ return '@response is nil, run #retrieve first' unless @response
34
+
35
+ response = @response.included.select do |obj|
36
+ obj.id === id || obj.type === id
37
+ end
38
+ response.length == 1 ? response.first : response
39
+ end
40
+
41
+ def find_in(arr)
42
+ return '@response is nil, run #retrieve first' unless @response
43
+
44
+ response = @response.included.select do |obj|
45
+ arr.include?(obj.id)
46
+ end
47
+ response.length == 1 ? response.first : response
48
+ end
49
+
50
+ private
51
+
52
+ def fetch(resource)
53
+ @response = Uktt::Http.new(
54
+ @config[:host],
55
+ @config[:version],
56
+ @config[:debug])
57
+ .retrieve(resource,
58
+ @config[:return_json])
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,1431 @@
1
+ require 'prawn'
2
+ require 'prawn/table'
3
+ require 'nokogiri'
4
+
5
+ # A class to produce a PDF for a single chapter
6
+ class ExportChapterPdf
7
+ include Prawn::View
8
+
9
+ THIRD_COUNTRY = '103'.freeze
10
+ TARIFF_PREFERENCE = '142'.freeze
11
+ CUSTOM_UNION_DUTY = '106'.freeze
12
+ PREFERENTIAL_MEASURE_TYPE_IDS = [ TARIFF_PREFERENCE, CUSTOM_UNION_DUTY ].freeze
13
+ MEASUREMENT_UNITS = ["% vol", "% vol/hl", "ct/l", "100 p/st", "c/k", "10 000 kg/polar", "kg DHS", "100 kg", "100 kg/net eda", "100 kg common wheat", "100 kg/br", "100 kg live weight", "100 kg/net mas", "100 kg std qual", "100 kg raw sugar", "100 kg/net/%sacchar.", "EUR", "gi F/S", "g", "GT", "hl", "100 m", "kg C₅H₁₄ClNO", "tonne KCl", "kg", "kg/tot/alc", "kg/net eda", "GKG", "kg/lactic matter", "kg/raw sugar", "kg/dry lactic matter", "1000 l", "kg methylamines", "KM", "kg N", "kg H₂O₂", "kg KOH", "kg K₂O", "kg P₂O₅", "kg 90% sdt", "kg NaOH", "kg U", "l alc. 100%", "l", "L total alc.", "1000 p/st", "1000 pa", "m²", "m³", "1000 m³", "m", "1000 kWh", "p/st", "b/f", "ce/el", "pa", "TJ", "1000 kg", "1000 kg/net eda", "1000 kg/biodiesel", "1000 kg/fuel content", "1000 kg/bioethanol", "1000 kg/net mas", "1000 kg std qual", "1000 kg/net/%saccha.", "Watt"].freeze
14
+ P_AND_R_MEASURE_TYPES_IMPORT = %w[277 705 724 745 410 420 465 474 475 707 710 712 714 722 728 730 746 747 748 750 755].freeze
15
+ P_AND_R_MEASURE_TYPES_EXPORT = %w[278 706 740 749 467 473 476 478 479 708 709 715 716 717 718 725 735 751].freeze
16
+ P_AND_R_MEASURE_TYPES_EXIM = %w[760 719].freeze
17
+ P_AND_R_MEASURE_TYPES = (P_AND_R_MEASURE_TYPES_IMPORT + P_AND_R_MEASURE_TYPES_EXIM + P_AND_R_MEASURE_TYPES_EXPORT).freeze
18
+ ANTIDUMPING_MEASURE_TYPES = ().freeze
19
+ SUPPORTED_CURRENCIES = {
20
+ 'BGN' => 'лв',
21
+ 'CZK' => 'Kč',
22
+ 'DKK' => 'kr.',
23
+ 'EUR' => '€',
24
+ 'GBP' => '£',
25
+ 'HRK' => 'kn',
26
+ 'HUF' => 'Ft',
27
+ 'PLN' => 'zł',
28
+ 'RON' => 'lei',
29
+ 'SEK' => 'kr'
30
+ }.freeze
31
+ CURRENCY_REGEX = /([0-9]+\.?[0-9]*)\s€/.freeze
32
+
33
+ CAP_LICENCE_KEY = 'CAP_LICENCE'
34
+ CAP_REFERENCE_TEXT = 'CAP licencing may apply. Specific licence requirements for this commodity can be obtained from the Rural Payment Agency website (www.rpa.gov.uk) under RPA Schemes.'
35
+
36
+ def initialize(opts = {})
37
+ @opts = opts
38
+ @chapter_id = opts[:chapter_id]
39
+
40
+ @margin = [50, 50, 20, 50]
41
+ @footer_height = 30
42
+ @printable_height = 595.28 - (@margin[0] + @margin[2])
43
+ @printable_width = 841.89 - (@margin[1] + @margin[3])
44
+ @base_table_font_size = 8
45
+ @indent_amount = 18
46
+ @document = Prawn::Document.new(
47
+ page_size: 'A4',
48
+ margin: @margin,
49
+ page_layout: :landscape
50
+ )
51
+ @cw = table_column_widths
52
+
53
+ @currency = set_currency
54
+ @currency_exchange_rate = fetch_exchange_rate
55
+
56
+ @footnotes = {}
57
+ @references_lookup = {}
58
+ @quotas = {}
59
+ @prs = {}
60
+ @anti_dumpings = {}
61
+ @pages_headings = {}
62
+
63
+ set_fonts
64
+
65
+ unless @chapter_id.to_s == 'test'
66
+ @chapter = Uktt::Chapter.new(@opts.merge(chapter_id: @chapter_id, version: 'v2')).retrieve
67
+ @section = Uktt::Section.new(@opts.merge(section_id: @chapter.data.relationships.section.data.id, version: 'v2')).retrieve
68
+ @current_heading = @section[:data][:attributes][:position]
69
+ end
70
+
71
+ bounding_box([0, @printable_height],
72
+ width: @printable_width,
73
+ height: @printable_height - @footer_height) do
74
+ if @chapter_id.to_s == 'test'
75
+ test
76
+ return
77
+ else
78
+ build
79
+ end
80
+ end
81
+
82
+ repeat(:all, dynamic: true) do
83
+ # trying to build a hash using page number as the key,
84
+ # but `#curent_heading` returns the last value, not the current value (i.e., when the footer is rendered)
85
+ if @pages_headings[page_number]
86
+ @pages_headings[page_number] << @current_heading
87
+ else
88
+ @pages_headings[page_number] = ['01', @current_heading]
89
+ end
90
+
91
+ page_footer
92
+ end
93
+ end
94
+
95
+ def set_currency
96
+ cur = (SUPPORTED_CURRENCIES.keys & [@opts[:currency]]).first
97
+ if cur = (SUPPORTED_CURRENCIES.keys & [@opts[:currency]]).first
98
+ return cur.upcase
99
+ else
100
+ raise StandardError.new "`#{@opts[:currency]}` is not a supported currency. SUPPORTED_CURRENCIES = [#{SUPPORTED_CURRENCIES.keys.join(', ')}]"
101
+ end
102
+ end
103
+
104
+ def set_fonts
105
+ font_families.update('OpenSans' => {
106
+ normal: 'vendor/assets/Open_Sans/OpenSans-Regular.ttf',
107
+ italic: 'vendor/assets/Open_Sans/OpenSans-RegularItalic.ttf',
108
+ medium: 'vendor/assets/Open_Sans/OpenSans-SemiBold.ttf',
109
+ medium_italic: 'vendor/assets/Open_Sans/OpenSans-SemiBoldItalic.ttf',
110
+ bold: 'vendor/assets/Open_Sans/OpenSans-Bold.ttf',
111
+ bold_italic: 'vendor/assets/Open_Sans/OpenSans-BoldItalic.ttf'
112
+ })
113
+ font_families.update('Monospace' => {
114
+ normal: 'vendor/assets/Overpass_Mono/OverpassMono-Regular.ttf',
115
+ bold: 'vendor/assets/Overpass_Mono/OverpassMono-Bold.ttf'
116
+ })
117
+ font 'OpenSans'
118
+ font_size @base_table_font_size
119
+ end
120
+
121
+ def fetch_exchange_rate(currency = @currency)
122
+ return 1.0 unless currency
123
+
124
+ return 1.0 if currency === Uktt::PARENT_CURRENCY
125
+
126
+ response = ENV.fetch("MX_RATE_EUR_#{currency}") do |_missing_name|
127
+ if currency === 'GBP'
128
+ Uktt::MonetaryExchangeRate.new(version: 'v2').latest(currency)
129
+ else
130
+ raise StandardError.new "Non-GBP currency exchange rates are not available via API and must be manually set with an environment variable, e.g., 'MX_RATE_EUR_#{currency}'"
131
+ end
132
+ end.to_f
133
+
134
+ return response if response > 0.0
135
+
136
+ raise StandardError.new "Currency error. response=#{response.inspect}"
137
+ end
138
+
139
+ def test
140
+ text "Today is #{Date.today}"
141
+ end
142
+
143
+ def build
144
+ if @chapter.data.attributes.goods_nomenclature_item_id[0..1] == @section.data.attributes.chapter_from
145
+ section_info
146
+ pad(16) { stroke_horizontal_rule }
147
+ start_new_page
148
+ end
149
+
150
+ chapter_info
151
+
152
+ move_down(12)
153
+
154
+ commodities_table
155
+
156
+ pad_top(24) do
157
+ font_size(13) do
158
+ pad_bottom(4) { text('<b>Footnotes</b>', inline_format: true) }
159
+ end
160
+ pad_bottom(4) { stroke_horizontal_rule }
161
+ footnotes
162
+ end
163
+
164
+ tariff_quotas
165
+
166
+ prohibitions_and_restrictions
167
+
168
+ anti_dumpings
169
+ end
170
+
171
+ def page_footer
172
+ bounding_box([0, @footer_height],
173
+ width: @printable_width,
174
+ height: @footer_height) do
175
+ table(footer_data, width: @printable_width) do |t|
176
+ t.column(0).align = :left
177
+ t.column(1).align = :center
178
+ t.column(2).align = :right
179
+ t.cells.borders = []
180
+ t.cells.padding = 0
181
+ end
182
+ end
183
+ end
184
+
185
+ def footer_data
186
+ # expecting something like this:
187
+ # `@pages_headings = {1=>["01", "02", "03", "04"], 2=>["04", "05", "06"]}`
188
+ footer_data_array = [[
189
+ format_text("<font size=9>#{Date.today.strftime('%-d %B %Y')}</font>"),
190
+ format_text("<b><font size='15'>#{@chapter.data.attributes.goods_nomenclature_item_id[0..1]}</font>#{Prawn::Text::NBSP * 2}#{page_number}</b>"),
191
+ format_text("<b><font size=9>Customs Tariff</b> Vol 2 Sect #{@section.data.attributes.numeral}#{Prawn::Text::NBSP * 3}<b>#{@chapter.data.attributes.goods_nomenclature_item_id[0..1]} #{@pages_headings[page_number].first.to_s.rjust(2, "0")}-#{@chapter.data.attributes.goods_nomenclature_item_id[0..1]} #{@pages_headings[page_number].last.to_s.rjust(2, "0")}</font></b>")
192
+ ]]
193
+ footer_data_array
194
+ end
195
+
196
+ def format_text(text_in, leading = 0)
197
+ {
198
+ content: text_in,
199
+ kerning: true,
200
+ inline_format: true,
201
+ leading: leading
202
+ }
203
+ end
204
+
205
+ def indents(note)
206
+ @this_indent ||= 0
207
+ @next_indent ||= 0
208
+ @top_pad ||= 0
209
+
210
+ case note
211
+ when /^\d\.\s/
212
+ @this_indent = 0
213
+ @next_indent = 12
214
+ @top_pad = @base_table_font_size / 2
215
+ when /\([a-z]\)\s/
216
+ @this_indent = 12
217
+ @next_indent = 24
218
+ @top_pad = @base_table_font_size / 2
219
+ when /\-\s/
220
+ @this_indent = 36
221
+ @next_indent = 36
222
+ @top_pad = @base_table_font_size / 2
223
+ else
224
+ @this_indent = @next_indent
225
+ @top_pad = 0
226
+ end
227
+ @this_indent
228
+ end
229
+
230
+ def hanging_indent(array, opts = {}, header = nil, leading = 0)
231
+ t = !header.nil? ? [[{ content: header, kerning: true, inline_format: true, colspan: 2, padding_bottom: 0 }, nil]] : []
232
+ make_table(
233
+ t << [
234
+ format_text(array[0], leading),
235
+ format_text(array[1], leading)
236
+ ],
237
+ opts
238
+ ) do |t|
239
+ t.cells.borders = []
240
+ t.column(0).padding_right = 0
241
+ t.row(0).padding_top = 0
242
+ end
243
+ end
244
+
245
+ def text_indent(note, opts)
246
+ if /<table.*/.match?(note)
247
+ indent(0) do
248
+ pad(@base_table_font_size) do
249
+ render_html_table(note)
250
+ end
251
+ end
252
+ else
253
+ indent(indents(note)) do
254
+ pad_top(@top_pad) do
255
+ text("<b>#{note.strip}</b>", opts)
256
+ end
257
+ end
258
+ end
259
+ end
260
+
261
+ def section_info(section = @section)
262
+ section_note = section.data.attributes.section_note || ''
263
+
264
+ if section_note.length > 3200
265
+ opts = {
266
+ width: @printable_width / 3,
267
+ column_widths: [@indent_amount],
268
+ cell_style: {
269
+ padding_bottom: 0
270
+ },
271
+ inline_format: true,
272
+ }
273
+ column_box([0, cursor], columns: 3, width: bounds.width, height: (@printable_height - @footer_height - (@printable_height - cursor)), spacer: (@base_table_font_size * 3)) do
274
+ text("<b><font size='13'>SECTION #{section.data.attributes.numeral}</font>\n<font size='17'>#{section.data.attributes.title}</font></b>", opts)
275
+
276
+ move_down(@base_table_font_size * 1.5)
277
+
278
+ text('<b>Notes</b>', opts.merge(size: 10))
279
+ section_note.split(/\* /).each do |note|
280
+ text_indent(note.gsub(%r{\\.\s}, '. '), opts.merge(size: 10))
281
+ end
282
+ end
283
+ else
284
+ opts = {
285
+ width: @printable_width / 3,
286
+ column_widths: [@indent_amount],
287
+ cell_style: {
288
+ padding_bottom: 0
289
+ }
290
+ }
291
+ column_1 = format_text("<b><font size='13'>SECTION #{section.data.attributes.numeral}</font>\n<font size='17'>#{section.data.attributes.title}</font></b>")
292
+ _column_x, column_2, column_3 = get_notes_columns(section.data.attributes.section_note, opts, 'Notes', 10)
293
+ table(
294
+ [
295
+ [
296
+ column_1,
297
+ column_2,
298
+ column_3
299
+ ]
300
+ ],
301
+ column_widths: [@printable_width / 3, @printable_width / 3, @printable_width / 3]
302
+ ) do |t|
303
+ t.cells.borders = []
304
+ t.column(0).padding_right = 12
305
+ t.row(0).padding_top = 0
306
+ end
307
+ end
308
+ end
309
+
310
+ def chapter_info(chapter = @chapter)
311
+ chapter_note = chapter.data.attributes.chapter_note || ''
312
+ notes, additional_notes, *everything_else = chapter_note.split(/#+\s*[Additional|Subheading]+ Note[s]*\s*#+/i)
313
+ .map do |s|
314
+ s.delete('\\')
315
+ .gsub("\r\n\r\n", "\r\n")
316
+ # .strip
317
+ end
318
+
319
+ notes ||= ''
320
+
321
+ if (additional_notes && chapter_note.length > 2300) || chapter_note.length > 3200
322
+ opts = {
323
+ kerning: true,
324
+ inline_format: true,
325
+ size: @base_table_font_size
326
+ }
327
+
328
+ column_box([0, cursor], columns: 3, width: bounds.width, height: (@printable_height - @footer_height - (@printable_height - cursor) + 20), spacer: (@base_table_font_size * 3)) do
329
+ text("<b><font size='#{@base_table_font_size * 1.5}'>Chapter #{chapter.data.attributes.goods_nomenclature_item_id[0..1].gsub(/^0/, '')}\n#{@chapter.data.attributes.formatted_description}</font></b>", opts)
330
+ move_down(@base_table_font_size * 1.5)
331
+
332
+ text('<b>Chapter notes</b>', opts.merge(size: 9))
333
+ notes.split(/\* /).each do |note|
334
+ text_indent(note, opts.merge(size: 9))
335
+ end
336
+
337
+ move_down(@base_table_font_size)
338
+
339
+ if additional_notes
340
+ text('<b>Subheading notes</b>', opts)
341
+ move_down(@base_table_font_size / 2)
342
+ additional_notes && additional_notes.split(/\* /).each do |note|
343
+ text_indent(note, opts)
344
+ end
345
+ move_down(@base_table_font_size)
346
+ end
347
+
348
+ everything_else.each do |nn|
349
+ text('<b>Additional notes</b>', opts)
350
+ move_down(@base_table_font_size / 2)
351
+ nn.to_s.split(/\* /).each do |note|
352
+ text_indent(note, opts)
353
+ end
354
+ end
355
+ end
356
+ else
357
+ opts = {
358
+ width: @printable_width / 3,
359
+ column_widths: [(@indent_amount + 2)],
360
+ cell_style: {
361
+ padding_bottom: 0
362
+ }
363
+ }
364
+ column_x, column_2, column_3 = get_chapter_notes_columns(chapter.data.attributes.chapter_note, opts, 'Note', @chapter_notes_font_size)
365
+ column_1 = if column_x.empty? || (column_x[0] && column_x[0][0][:content].blank?)
366
+ format_text("<b><font size='#{@base_table_font_size * 1.5}'>Chapter #{chapter.data.attributes.goods_nomenclature_item_id[0..1].gsub(/^0/, '')}\n#{chapter.data.attributes.formatted_description}</font></b>")
367
+ else
368
+ column_x
369
+ end
370
+ table(
371
+ [
372
+ [
373
+ column_1,
374
+ column_2,
375
+ column_3
376
+ ]
377
+ ],
378
+ column_widths: [@printable_width / 3, @printable_width / 3, @printable_width / 3]
379
+ ) do |t|
380
+ t.cells.borders = []
381
+ t.column(0).padding_right = 12
382
+ t.row(0).padding_top = 0
383
+ end
384
+ end
385
+ end
386
+
387
+ def html_table_data(html)
388
+ noko = Nokogiri::HTML(html)
389
+ head = noko.at('th') ? noko.at('th').content : nil
390
+ data = noko.css('tr').map do |tr|
391
+ tr.css('td').map(&:content)
392
+ end
393
+ max_col_count = data.map(&:length).max
394
+ data_normalized = data.reject do |row|
395
+ row.length != max_col_count
396
+ end
397
+ return data_normalized.unshift([{content: head, colspan: max_col_count}]) if head
398
+
399
+ data_normalized
400
+ end
401
+
402
+ def strip_tags(text)
403
+ return if text.nil?
404
+
405
+ noko = Nokogiri::HTML(text)
406
+ noko.css('span', 'abbr').each { |node| node.replace(node.children) }
407
+ noko.content
408
+ end
409
+
410
+ def render_html_table(html)
411
+ html_string = "<table>#{html.gsub("\r\n", '')}</table>"
412
+ table(html_table_data(html_string), cell_style: {
413
+ padding: 2,
414
+ size: 5,
415
+ border_widths: [0.1, 0.1]
416
+ } ) do |t|
417
+ t.width = @printable_width / 3
418
+ end
419
+ end
420
+
421
+ def update_footnotes(v2_commodity)
422
+ measures = commodity_measures(v2_commodity)
423
+
424
+ measure_footnote_ids = measures.map{|m| m.relationships.footnotes.data}.flatten.uniq.map(&:id)
425
+ commodity_footnote_ids = v2_commodity.data.relationships.footnotes.data.flatten.uniq.map(&:id)
426
+ footnotes = (commodity_footnote_ids + measure_footnote_ids).map do |f|
427
+ v2_commodity.included.select{|obj| obj.id == f}
428
+ end.flatten
429
+
430
+ footnotes.each do |fn|
431
+ f = fn.attributes
432
+ next if f.code =~ /0[3,4]./
433
+ if @footnotes[f.code]
434
+ @footnotes[f.code][:refs] << @uktt.response.data.id
435
+ else
436
+ @footnotes[f.code] = {
437
+ text: "#{f.code}-#{render_footnote(f.description)}",
438
+ refs: [@uktt.response.data.id]
439
+ }
440
+ unless @references_lookup[footnote_reference_key(f.code)]
441
+ @references_lookup[footnote_reference_key(f.code)] = {
442
+ index: @references_lookup.length + 1,
443
+ text: replace_html(@footnotes[f.code][:text].delete('|'))
444
+ }
445
+ end
446
+ end
447
+ end
448
+ end
449
+
450
+ def render_footnote(note)
451
+ Nokogiri::HTML(note).css('p').map(&:content).join("\n")
452
+ end
453
+
454
+ def update_quotas(v2_commodity, heading)
455
+ quotas = commodity_measures(v2_commodity).select{|m| measure_is_quota(m)}
456
+ quotas.each do |measure_quota|
457
+ order_number = measure_quota.relationships.order_number.data.id
458
+ if @quotas[order_number]
459
+ @quotas[order_number][:measures] << measure_quota
460
+ @quotas[order_number][:commodities] << v2_commodity.data.attributes.goods_nomenclature_item_id
461
+ else
462
+ duty = v2_commodity.included.select{|obj| measure_quota.relationships.duty_expression.data.id == obj.id}.first.attributes.base
463
+ definition_relation = v2_commodity.included.select{|obj| measure_quota.relationships.order_number.data.id == obj.id}.first.relationships.definition
464
+ return if definition_relation.data.nil?
465
+ definition = v2_commodity.included.select{|obj| definition_relation.data.id == obj.id}.first
466
+ footnotes_ids = measure_quota.relationships.footnotes.data.map(&:id).select{|f| f[0..1] == 'CD'}
467
+ footnotes = v2_commodity.included.select{|obj| footnotes_ids.include?(obj.id)}
468
+
469
+ @quotas[order_number] = {
470
+ commodities: [v2_commodity.data.attributes.goods_nomenclature_item_id],
471
+ descriptions: [[heading.description, v2_commodity.data.attributes.description]],
472
+ measures: [measure_quota],
473
+ duties: [duty],
474
+ definitions: [definition],
475
+ footnotes: footnotes
476
+ }
477
+ end
478
+ end
479
+ end
480
+
481
+ def update_prs(v2_commodity)
482
+ measures = pr_measures(v2_commodity)
483
+ measures.each do |measure|
484
+ document_codes = []
485
+ requirements = []
486
+ id = measure.relationships.measure_type.data.id
487
+
488
+ if @prs[id]
489
+ @prs[id][:commodities] << v2_commodity.data.attributes.goods_nomenclature_item_id
490
+ else
491
+ desc = v2_commodity.included.select{|obj| obj.type = "measure_type" && obj.id == id}.first.attributes.description
492
+ conditions_ids = measure.relationships.measure_conditions.data.map(&:id)
493
+ conditions = v2_commodity.included.select{|obj| obj.type = "measure_condition" && conditions_ids.include?(obj.id) }
494
+ conditions.each do |condition|
495
+ unless condition.nil?
496
+ doc_code = condition.attributes.document_code
497
+ document_codes << condition.attributes.document_code
498
+ requirements << "#{condition.attributes.condition_code}: #{strip_tags(condition.attributes.requirement)}#{" (#{doc_code})" unless doc_code.to_s.empty?}" unless condition.attributes.requirement.nil?
499
+ end
500
+ end
501
+ @prs[id] = {
502
+ measures: measure,
503
+ commodities: [v2_commodity.data.attributes.goods_nomenclature_item_id],
504
+ description: "#{desc} (#{id})",
505
+ conditions: document_codes.reject(&:empty?),
506
+ requirements: requirements.reject(&:nil?),
507
+ }
508
+ end
509
+ end
510
+ end
511
+
512
+ def update_anti_dumpings(v2_commodity)
513
+ anti_dumping_measures(v2_commodity).each do |measure|
514
+ description = ''
515
+ delimiter = ''
516
+
517
+ duty_expression_id = measure.relationships.duty_expression&.data&.id
518
+ if duty_expression_id
519
+ duty_expression = find_duty_expression(duty_expression_id)
520
+ unless duty_expression&.attributes&.base == ''
521
+ measure_type = find_measure_type(measure.relationships.measure_type&.data&.id)
522
+ description += clean_rates(duty_expression&.attributes&.base) + '<br>' + measure_type&.attributes&.description
523
+ delimiter = '<br>'
524
+ end
525
+ end
526
+
527
+ additional_code_id = measure.relationships.additional_code&.data&.id
528
+ if additional_code_id
529
+ additional_code = find_additional_code(additional_code_id)
530
+ description += delimiter + additional_code.attributes.formatted_description
531
+ end
532
+
533
+ unless description == ''
534
+ commodity_item_id = v2_commodity.data.attributes.goods_nomenclature_item_id
535
+ geographical_area_id = measure.relationships.geographical_area.data.id
536
+ @anti_dumpings[commodity_item_id] ||= {}
537
+ @anti_dumpings[commodity_item_id][geographical_area_id] ||= {}
538
+ @anti_dumpings[commodity_item_id][geographical_area_id][additional_code&.attributes&.code || ''] ||= description
539
+ end
540
+ end
541
+ end
542
+
543
+ def find_measure_type(measure_type_id)
544
+ find_included_object(measure_type_id, 'measure_type')
545
+ end
546
+
547
+ def find_duty_expression(duty_expression_id)
548
+ find_included_object(duty_expression_id, 'duty_expression')
549
+ end
550
+
551
+ def find_additional_code(additional_code_id)
552
+ find_included_object(additional_code_id, 'additional_code')
553
+ end
554
+
555
+ def find_included_object(object_id, object_type)
556
+ return nil unless object_id || object_type
557
+ @uktt.response.included.find do |obj|
558
+ obj.id == object_id && obj.type == object_type
559
+ end
560
+ end
561
+
562
+ def commodities_table
563
+ table commodity_table_data, header: true, column_widths: @cw do |t|
564
+ t.cells.border_width = 0.25
565
+ t.cells.borders = %i[left right]
566
+ t.cells.padding_top = 2
567
+ t.cells.padding_bottom = 2
568
+ t.row(0).align = :center
569
+ t.row(0).padding = 2
570
+ t.column(1).align = :center
571
+ t.column(2).align = :center
572
+ t.column(5).align = :center
573
+ t.row(0).borders = %i[top right bottom left]
574
+ t.row(-1).borders = %i[right bottom left]
575
+ end
576
+ end
577
+
578
+ def commodity_table_data(chapter = @chapter)
579
+ result = [] << header_row
580
+ heading_ids = chapter.data.relationships.headings.data.map(&:id)
581
+ heading_objs = chapter.included.select{|obj| heading_ids.include? obj.id}
582
+ heading_gniids = heading_objs.map{|h| h.attributes.goods_nomenclature_item_id}.uniq.sort
583
+
584
+ heading_gniids.each do |heading_gniid|
585
+ @uktt = Uktt::Heading.new(@opts.merge(heading_id: heading_gniid[0..3], version: 'v2'))
586
+ v2_heading = @uktt.retrieve
587
+ heading = v2_heading.data.attributes
588
+ if heading.declarable
589
+ update_footnotes(v2_heading)
590
+ update_quotas(v2_heading, heading)
591
+ update_prs(v2_heading)
592
+ update_anti_dumpings(v2_heading)
593
+ end
594
+
595
+ result << heading_row_head(v2_heading)
596
+ result << heading_row_title(v2_heading)
597
+
598
+ # You'd think this would work, but `page_number` is not updated
599
+ # because we're not inside the `repeat` block
600
+ #
601
+ # if @pages_headings[page_number]
602
+ # @pages_headings[page_number] << @current_heading
603
+ # else
604
+ # @pages_headings[page_number] = [@current_heading]
605
+ # end
606
+ # logger.info @pages_headings.inspect
607
+
608
+ # Same with below, but when trying to get the value of `@current_heading`
609
+ # in the `repeat` block, it always returns the last value, not the current
610
+ @current_heading = heading.goods_nomenclature_item_id[2..3]
611
+
612
+ if v2_heading.data.relationships.commodities
613
+ commodity_ids = v2_heading.data.relationships.commodities.data.map(&:id)
614
+ commodity_objs = v2_heading.included.select{|obj| commodity_ids.include? obj.id}
615
+
616
+ commodity_objs.each do |c|
617
+ if c.attributes.leaf
618
+ @uktt = Uktt::Commodity.new(@opts.merge(commodity_id: c.attributes.goods_nomenclature_item_id, version: 'v2'))
619
+ v2_commodity = @uktt.retrieve
620
+
621
+ if v2_commodity.data
622
+ result << commodity_row(v2_commodity)
623
+ v2_commodity.data.attributes.description = c.attributes.description
624
+
625
+ update_footnotes(v2_commodity) if v2_commodity.data.attributes.declarable
626
+
627
+ update_quotas(v2_commodity, heading)
628
+
629
+ update_prs(v2_commodity)
630
+
631
+ update_anti_dumpings(v2_commodity)
632
+ else
633
+ result << commodity_row_subhead(c)
634
+ end
635
+ else
636
+ result << commodity_row_subhead(c)
637
+ end
638
+ end
639
+ end
640
+ end
641
+ result
642
+ end
643
+
644
+ def header_row
645
+ %w[1 2A 2B 3 4 5 6 7]
646
+ end
647
+
648
+ def heading_row_head(v2_heading)
649
+ heading = v2_heading.data.attributes
650
+ head = {
651
+ content: "<b>#{heading[:goods_nomenclature_item_id][0..1]} #{heading[:goods_nomenclature_item_id][2..3]}</b>",
652
+ kerning: true,
653
+ size: 12,
654
+ borders: [],
655
+ padding_bottom: 0,
656
+ inline_format: true
657
+ }
658
+ [head, '', '', '', '', '', '', '']
659
+ end
660
+
661
+ def heading_row_title(v2_heading)
662
+ heading = v2_heading.data.attributes
663
+ title = {
664
+ content: "<b>#{heading[:description].gsub('|', Prawn::Text::NBSP).upcase}<b>",
665
+ kerning: true,
666
+ size: @base_table_font_size,
667
+ width: @cw[0],
668
+ borders: [],
669
+ padding_top: 0,
670
+ inline_format: true
671
+ }
672
+ if heading.declarable
673
+ heading_data = [
674
+ commodity_code_cell(heading), # Column 2A: Commodity code, 8 digits, center-align
675
+ additional_commodity_code_cell(heading), # Column 2B: Additional commodity code, 2 digits, center-align
676
+ specific_provisions(v2_heading), # Column 3: Specific provisions, left-align
677
+ units_of_quantity_list, # Column 4: Unit of quantity, numbered list, left-align
678
+ third_country_duty_expression, # Column 5: Full tariff rate, percentage, center align
679
+ preferential_tariffs, # Column 6: Preferential tariffs, left align
680
+ formatted_vat_rate_cell # Column 7: VAT Rate: e.g., 'S', 'Z', etc., left align
681
+ ]
682
+ else
683
+ heading_data = ['', '', '', '', '', '', '']
684
+ end
685
+ [[[title]]] + heading_data
686
+ end
687
+
688
+ def commodity_row(v2_commodity)
689
+ commodity = v2_commodity.data.attributes
690
+ [
691
+ formatted_heading_cell(commodity), # Column 1: Heading numbers and descriptions
692
+ commodity_code_cell(commodity), # Column 2A: Commodity code, 8 digits, center-align
693
+ additional_commodity_code_cell(commodity), # Column 2B: Additional commodity code, 2 digits, center-align
694
+ specific_provisions(v2_commodity), # Column 3: Specific provisions, left-align
695
+ units_of_quantity_list, # Column 4: Unit of quantity, numbered list, left-align
696
+ third_country_duty_expression, # Column 5: Full tariff rate, percentage, center align
697
+ preferential_tariffs, # Column 6: Preferential tariffs, left align
698
+ formatted_vat_rate_cell # Column 7: VAT Rate: e.g., 'S', 'Z', etc., left align
699
+ ]
700
+ end
701
+
702
+ def commodity_row_subhead(c)
703
+ commodity = c.attributes
704
+ [
705
+ formatted_heading_cell(commodity),
706
+ commodity_code_cell(commodity),
707
+ additional_commodity_code_cell(commodity),
708
+ '',
709
+ '',
710
+ '',
711
+ '',
712
+ ''
713
+ ]
714
+ end
715
+
716
+ def formatted_heading_cell(commodity)
717
+ indents = (('-' + Prawn::Text::NBSP) * (commodity.number_indents - 1)) # [(commodity.number_indents - 1), 1].max)
718
+ opts = {
719
+ width: @cw[0],
720
+ column_widths: { 0 => ((commodity.number_indents || 1) * 5.1) }
721
+ }
722
+
723
+ footnotes_array = []
724
+ @footnotes.each_pair do |k, v|
725
+ if @uktt.response.data && v[:refs].include?(@uktt.response.data.id) && k[0..1] != 'CD'
726
+ footnotes_array << @references_lookup[footnote_reference_key(k)][:index]
727
+ end
728
+ end
729
+
730
+ if footnotes_array.empty?
731
+ footnote_references = ""
732
+ leading = 0
733
+ else
734
+ footnote_references = " [#{footnotes_array.join(',')}]"
735
+ leading = 4
736
+ end
737
+
738
+ # TODO: implement Commodity#from_harmonized_system? and Commodity#in_combined_nomenclature?
739
+ # i.e.: (see below)
740
+ # if commodity.from_harmonized_system? || commodity[:number_indents] <= 1
741
+ # content = format_text("<b>#{commodity.description}#{footnote_references}</b>")
742
+ # elsif commodity.in_combined_nomenclature?
743
+ # content = hanging_indent(["<i>#{indents}<i>", "<i>#{commodity.description}#{footnote_references}</i>"], opts)
744
+ # else
745
+ # content = hanging_indent([indents, "#{commodity.description}#{footnote_references}"], opts)
746
+ # end
747
+ description = render_special_characters(commodity.description)
748
+ if commodity.number_indents.to_i <= 1 #|| !commodity.declarable
749
+ format_text("<b>#{description}</b><font size='11'><sup><#{footnote_references}</sup></font>", leading)
750
+ elsif commodity.declarable
751
+ hanging_indent(["<i>#{indents}<i>", "<i><u>#{description}</u></i><font size='11'><sup>#{footnote_references}</sup></font>"], opts, nil, leading)
752
+ elsif commodity.number_indents.to_i == 2
753
+ hanging_indent([indents, "<b>#{description}</b><font size='11'><sup>#{footnote_references}</sup></font>"], opts, nil, leading)
754
+ else
755
+ hanging_indent([indents, "#{description}<font size='11'><sup>#{footnote_references}</sup></font>"], opts, nil, leading)
756
+ end
757
+ end
758
+
759
+ def render_special_characters(string)
760
+ string.gsub( /@([2-9])/, '<sub>\1 </sub>' )
761
+ .gsub( /\|/, Prawn::Text::NBSP )
762
+ end
763
+
764
+ def commodity_code_cell(commodity)
765
+ return '' unless commodity.declarable
766
+
767
+ format_text "<font name='Monospace'>#{commodity.goods_nomenclature_item_id[0..5]}#{Prawn::Text::NBSP * 1}#{commodity.goods_nomenclature_item_id[6..7]}</font>"
768
+ end
769
+
770
+ def additional_commodity_code_cell(commodity)
771
+ return '' unless commodity.declarable
772
+
773
+ format_text "<font name='Monospace'>#{(commodity.goods_nomenclature_item_id[8..9]).to_s}</font>"
774
+ end
775
+
776
+ # copied from backend/app/models/measure_type.rb:41
777
+ def measure_type_excise?(measure_type)
778
+ measure_type&.attributes&.measure_type_series_id == 'Q'
779
+ end
780
+
781
+ def measure_type_anti_dumping?(measure_type)
782
+ measure_type&.attributes&.measure_type_series_id == 'D'
783
+ end
784
+
785
+ def anti_dumping_measure_type_ids
786
+ @uktt.response.included.select do |obj|
787
+ obj.type == 'measure_type' && measure_type_anti_dumping?(obj)
788
+ end.map(&:id)
789
+ end
790
+
791
+ def measure_type_tax_code(measure_type)
792
+ measure_type.attributes.description.scan(/\d{3}/).first
793
+ end
794
+
795
+ def measure_type_suspension?(measure_type)
796
+ measure_type&.attributes&.description =~ /suspension/
797
+ end
798
+
799
+ def measure_conditions_has_cap_license?(measure_conditions)
800
+ measure_conditions.any? do |measure_condition|
801
+ measure_condition&.attributes&.document_code == 'L001'
802
+ end
803
+ end
804
+
805
+ def specific_provisions(v2_commodity)
806
+ return '' unless v2_commodity.data.attributes.declarable
807
+
808
+ measures = commodity_measures(v2_commodity)
809
+
810
+ measure_types = measures.map do |measure|
811
+ v2_commodity.included.find {|obj| obj.id == measure.relationships.measure_type.data.id && obj.type == 'measure_type'}
812
+ end
813
+ excise_codes = measure_types.select(&method(:measure_type_excise?)).map(&method(:measure_type_tax_code)).uniq.sort
814
+
815
+ str = excise_codes.length > 0 ? "EXCISE (#{excise_codes.join(', ')})" : ''
816
+ delimiter = str.length > 0 ? "\n" : ''
817
+
818
+ str += measure_types.select(&method(:measure_type_suspension?)).length > 0 ? delimiter + 'S' : ''
819
+ delimiter = str.length > 0 ? "\n" : ''
820
+
821
+ str += (measures.select(&method(:measure_is_quota)).length > 0 ? delimiter + 'TQ' : '')
822
+ delimiter = str.length > 0 ? "\n" : ''
823
+
824
+ measure_conditions = measures.map do |measure|
825
+ v2_commodity.included.find { |obj| measure.relationships.measure_conditions.data.map(&:id).include?(obj.id) && obj.type == 'measure_condition' }
826
+ end.compact.uniq
827
+
828
+ if measure_conditions_has_cap_license?(measure_conditions)
829
+ unless @references_lookup[CAP_LICENCE_KEY]
830
+ @references_lookup[CAP_LICENCE_KEY] = {
831
+ index: @references_lookup.length + 1,
832
+ text: CAP_REFERENCE_TEXT
833
+ }
834
+ end
835
+ str += delimiter + "CAP Lic <font size='11'><sup> [#{@references_lookup[CAP_LICENCE_KEY][:index]}]</sup></font>"
836
+ end
837
+ format_text(str, 0)
838
+ end
839
+
840
+ def units_of_quantity_list
841
+ str = ''
842
+ duties = @uktt.find('duty_expression').map{ |d| d.attributes.base }
843
+ return str if duties.empty?
844
+
845
+ uoq = ['Kg']
846
+ duties.each do |duty|
847
+ uoq << duty if MEASUREMENT_UNITS.include?(duty)
848
+ end
849
+
850
+ uoq.each_with_index do |q, i|
851
+ str << "#{(i + 1).to_s + '. ' if uoq.length > 1}#{q}\n"
852
+ end
853
+
854
+ str
855
+ end
856
+
857
+ def third_country_duty_expression
858
+ measure = @uktt.find('measure').select{|m| m.relationships.measure_type.data.id == THIRD_COUNTRY }.first
859
+ return '' if measure.nil?
860
+
861
+ clean_rates(@uktt.find(measure.relationships.duty_expression.data.id).attributes.base)
862
+ end
863
+
864
+ def preferential_tariffs
865
+ preferential_tariffs = {
866
+ duties: {},
867
+ footnotes: {},
868
+ excluded: {},
869
+ }
870
+ s = []
871
+ @uktt.find('measure').select{|m| PREFERENTIAL_MEASURE_TYPE_IDS.include?(m.relationships.measure_type.data.id) }.each do |t|
872
+ g_id = t.relationships.geographical_area.data.id
873
+ geo = @uktt.response.included.select{|obj| obj.id == g_id}.map{|t| t.id =~ /[A-Z]{2}/ ? t.id : t.attributes.description}.join(', ')
874
+
875
+ d_id = t.relationships.duty_expression.data.id
876
+ duty = @uktt.response.included.select{|obj| obj.id == d_id}.map{|t| t.attributes.base}
877
+
878
+ f_ids = t.relationships.footnotes.data.map(&:id)
879
+ footnotes = @uktt.response.included.select{|obj| f_ids.include? obj.id}.flatten
880
+
881
+ x_ids = t.relationships.excluded_countries.data.map(&:id)
882
+ excluded = @uktt.response.included.select{|obj| x_ids.include? obj.id}
883
+
884
+ footnotes_string = footnotes.map(&:id).map{|fid| "<sup><font size='9'>[#{@references_lookup.dig(footnote_reference_key(fid), :index)}]</font></sup>"}.join(' ')
885
+ excluded_string = excluded.map(&:id).map{|xid| " (Excluding #{xid})"}.join(' ')
886
+ duty_string = clean_rates(duty.join, column: 6)
887
+ s << "#{geo}#{excluded_string}-#{duty_string}#{footnotes_string}"
888
+ end
889
+ { content: s.sort.join(', '), inline_format: true }
890
+ end
891
+
892
+ def formatted_vat_rate_cell
893
+ @uktt.find('measure_type')
894
+ .map(&:id)
895
+ .select{|id| id[0..1] == 'VT'}
896
+ .map{|m| m.chars[2].upcase}
897
+ .join(' ')
898
+ end
899
+
900
+ def footnotes
901
+ return if @footnotes.size == 0
902
+
903
+ cell_style = {
904
+ padding: 0,
905
+ borders: []
906
+ }
907
+ table_opts = {
908
+ column_widths: [25],
909
+ width: @printable_width,
910
+ cell_style: cell_style
911
+ }
912
+ notes_array = @references_lookup.map do |_, reference|
913
+ [ "( #{reference[:index]} )", reference[:text] ]
914
+ end
915
+
916
+ table notes_array, table_opts do |t|
917
+ t.column(1).padding_left = 5
918
+ end
919
+ end
920
+
921
+ def replace_html(raw)
922
+ raw.gsub(/<P>/, "\n")
923
+ .gsub(%r{</P>}, '')
924
+ .gsub('&#38;', '&')
925
+ # .gsub("\n\n", "\n")
926
+ end
927
+
928
+ def tariff_quotas(chapter = @chapter)
929
+ cell_style = {
930
+ padding: 0,
931
+ borders: [],
932
+ inline_format: true
933
+ }
934
+ table_opts = {
935
+ column_widths: quota_table_column_widths,
936
+ width: @printable_width,
937
+ cell_style: cell_style
938
+ }
939
+ quotas_array = quota_header_row
940
+
941
+ @quotas.each do |measure_id, quota|
942
+ commodity_ids = quota[:commodities].uniq
943
+
944
+ while commodity_ids.length > 0
945
+ quotas_array << [
946
+ quota_commodities(commodity_ids.shift(quotas_array.length == 2 ? 42 : 56)),
947
+ quota_description(quota[:descriptions]),
948
+ quota_geo_description(quota[:measures]),
949
+ measure_id,
950
+ quota_rate(quota[:duties]),
951
+ quota_period(quota[:measures]),
952
+ quota_units(quota[:definitions]),
953
+ quota_docs(quota[:footnotes])
954
+ ]
955
+ end
956
+ end
957
+
958
+ unless quotas_array.length <= 2
959
+
960
+ start_new_page
961
+
962
+ font_size(19) do
963
+ text "Chapter #{chapter.data.attributes.goods_nomenclature_item_id[0..1].gsub(/^0/, '')}#{Prawn::Text::NBSP * 4}<b>Additional Information</b>",
964
+ inline_format: true
965
+ end
966
+
967
+ font_size(13) do
968
+ pad_bottom(13) do
969
+ text '<b>Tariff Quotas/Ceilings</b>',
970
+ inline_format: true
971
+ end
972
+ end
973
+
974
+ table quotas_array, table_opts do |t|
975
+ t.cells.border_width = 0.25
976
+ t.cells.borders = %i[top bottom]
977
+ t.cells.padding_top = 2
978
+ t.cells.padding_bottom = 4
979
+ t.cells.padding_right = 9
980
+ t.row(0).border_width = 1
981
+ t.row(0).borders = [:top]
982
+ t.row(1).borders = [:bottom]
983
+ t.row(0).padding_top = 0
984
+ t.row(0).padding_bottom = 0
985
+ t.row(1).padding_top = 0
986
+ t.row(1).padding_bottom = 2
987
+ end
988
+ end
989
+ end
990
+
991
+ def quota_header_row
992
+ [
993
+ [
994
+ format_text('<b>Commodity Code</b>'),
995
+ format_text('<b>Description</b>'),
996
+ format_text('<b>Country of origin</b>'),
997
+ format_text('<b>Tariff Quota Order No.</b>'),
998
+ format_text('<b>Quota rate</b>'),
999
+ format_text('<b>Quota period</b>'),
1000
+ format_text('<b>Quota units</b>'),
1001
+ format_text("<b>Documentary evidence\nrequired</b>")
1002
+ ],
1003
+ (1..8).to_a
1004
+ ]
1005
+ end
1006
+
1007
+ def quota_commodities(commodities)
1008
+ commodities.map do |c|
1009
+ [
1010
+ c[0..3],
1011
+ c[4..5],
1012
+ c[6..7],
1013
+ c[8..-1]
1014
+ ].reject(&:empty?).join(Prawn::Text::NBSP)
1015
+ end.join("\n")
1016
+ end
1017
+
1018
+ def quota_description(descriptions)
1019
+ # descriptions.flatten.join(' - ')
1020
+ descriptions.flatten[1]
1021
+ end
1022
+
1023
+ def quota_geo_description(measures)
1024
+ measures.map do |measure|
1025
+ if @uktt.response.included
1026
+ geos = @uktt.response.included.select{|obj| obj.id == measure.relationships.geographical_area.data.id}
1027
+ geos.first.attributes.description unless geos.first.nil?
1028
+ end
1029
+ end.uniq.join(', ')
1030
+ end
1031
+
1032
+ def quota_rate(duties)
1033
+ clean_rates(duties.uniq.join(', '))
1034
+ end
1035
+
1036
+ def quota_period(measures)
1037
+ formatted_date = '%d/%m/%Y'
1038
+ measures.map do |m|
1039
+ start = m.attributes.effective_start_date ? DateTime.parse(m.attributes.effective_start_date).strftime(formatted_date) : ''
1040
+ ending = m.attributes.effective_end_date ? DateTime.parse(m.attributes.effective_end_date).strftime(formatted_date) : ''
1041
+ "#{start} - #{ending}"
1042
+ end.uniq.join(', ')
1043
+ end
1044
+
1045
+ def quota_units(definitions)
1046
+ definitions.map do |d|
1047
+ d.attributes.measurement_unit
1048
+ end.uniq.join(', ')
1049
+ end
1050
+
1051
+ def quota_docs(footnotes)
1052
+ return '' if footnotes.empty?
1053
+ footnotes.map do |f|
1054
+ f.attributes.description
1055
+ end.uniq.join(', ')
1056
+ end
1057
+
1058
+ def get_chapter_notes_columns(content, opts, header_text = 'Note', _font_size = 9)
1059
+ get_notes_columns(content, opts, header_text, 9, 2)
1060
+ end
1061
+
1062
+ def notes_str_to_note_array(notes_str)
1063
+ notes = []
1064
+ note_tmp = split_note(notes_str)
1065
+ while note_tmp.length >= 2
1066
+ notes << note_tmp[0..1]
1067
+ note_tmp = note_tmp[2..-1]
1068
+ end
1069
+ notes << note_tmp
1070
+ end
1071
+
1072
+ def get_notes_columns(content, opts, header_text = 'Note', font_size = @base_table_font_size, fill_columns = 2)
1073
+ empty_cell = [{ content: '', borders: [] }]
1074
+ return [[empty_cell, empty_cell, empty_cell]] if content.nil?
1075
+
1076
+ column_1 = []
1077
+ column_2 = []
1078
+ column_3 = []
1079
+
1080
+ notes_str = content.delete('\\')
1081
+ notes = notes_str_to_note_array(notes_str)
1082
+
1083
+ title = "<b><font size='#{@base_table_font_size * 1.5}'>Chapter #{@chapter.data.attributes.goods_nomenclature_item_id[0..1].gsub(/^0/, '')}\n#{@chapter[:formatted_description]}</font></b>\n\n"
1084
+ offset = 0
1085
+ notes.each_with_index do |note, i|
1086
+ m = note.join.match(/##\s*(additional|subheading) note[s]*\s*##/i)
1087
+ if m
1088
+ note[0], note[1] = '', ''
1089
+ header = "#{fill_columns == 3 ? title : nil}<b><font size='#{font_size}'>#{"#{m[1]} Note"}</font></b>"
1090
+ offset += 1
1091
+ else
1092
+ header = i.zero? ? "#{fill_columns == 3 ? title : nil}<b><font size='#{font_size}'>#{header_text}</font></b>" : nil
1093
+ end
1094
+ new_note = [
1095
+ {
1096
+ content: hanging_indent([
1097
+ "<b><font size='#{font_size}'>#{note[0]}</font></b>",
1098
+ "<b><font size='#{font_size}'>#{note[1]}</font></b>"
1099
+ ], opts, header),
1100
+ borders: []
1101
+ }
1102
+ ]
1103
+ if fill_columns == 2
1104
+ if i - offset < (notes.length / 2)
1105
+ column_2 << new_note unless new_note == ['', '']
1106
+ else
1107
+ column_3 << new_note
1108
+ end
1109
+ elsif fill_columns == 3
1110
+ if i < (notes.length / 3)
1111
+ column_1 << new_note
1112
+ elsif i < ((notes.length / 3) * 2)
1113
+ column_2 << new_note
1114
+ else
1115
+ column_3 << new_note
1116
+ end
1117
+ end
1118
+ end
1119
+
1120
+ column_2 << empty_cell if column_2.empty?
1121
+ column_3 << empty_cell if column_3.empty?
1122
+ [column_1, column_2, column_3]
1123
+ end
1124
+
1125
+ def split_note(str)
1126
+ arr = str.split(/\* |^([0-9]\.{0,}\s|\([a-z]{1,}\))|(?=##\ )/)
1127
+ .map { |n| n.split(/^([0-9]\.{0,}\s{0,}|\([a-z]{1,}\))/) }
1128
+ .each { |n| n.unshift(Prawn::Text::NBSP) if n.length == 1 }
1129
+ .flatten
1130
+ .reject(&:empty?)
1131
+ .map(&:strip)
1132
+ return arr.unshift((Prawn::Text::NBSP * 2)) if arr.length == 1
1133
+
1134
+ normalize_notes_array(arr)
1135
+ end
1136
+
1137
+ def token?(str)
1138
+ str =~ /^[0-9]\.{0,}\s{0,}|\([a-z]{1,}\)|\s{1,}/
1139
+ end
1140
+
1141
+ def normalize_notes_array(arr)
1142
+ arr.each_with_index do |str, i|
1143
+ if str == Prawn::Text::NBSP && i.odd?
1144
+ arr.delete_at(i)
1145
+ normalize_notes_array(arr)
1146
+ end
1147
+ end
1148
+ end
1149
+
1150
+ def table_column_widths
1151
+ column_ratios = [21, 5, 1.75, 5, 4, 5.25, 19, 2]
1152
+ multiplier = @printable_width / column_ratios.sum
1153
+ column_ratios.map { |n| n * multiplier }
1154
+ end
1155
+
1156
+ def quota_table_column_widths
1157
+ column_ratios = [12, 43, 9, 9, 11, 11, 8, 22]
1158
+ multiplier = 741.89 / column_ratios.sum
1159
+ column_ratios.map { |n| n * multiplier }
1160
+ end
1161
+
1162
+ def pr_table_column_widths
1163
+ column_ratios = [2, 1, 4, 4, 1]
1164
+ multiplier = 741.89 / column_ratios.sum
1165
+ column_ratios.map { |n| n * multiplier }
1166
+ end
1167
+
1168
+ def anti_dumping_table_column_widths
1169
+ column_ratios = [1, 1, 1, 4]
1170
+ multiplier = 741.89 / column_ratios.sum
1171
+ column_ratios.map { |n| n * multiplier }
1172
+ end
1173
+
1174
+ def clean_rates(raw, column: nil)
1175
+ rate = raw
1176
+
1177
+ if column != 6
1178
+ rate = rate.gsub(/^0.00 %/, 'Free')
1179
+ end
1180
+
1181
+ rate = rate.gsub(' EUR ', ' € ')
1182
+ .gsub(' / ', '/')
1183
+ .gsub(/(\.[0-9]{1})0 /, '\1 ')
1184
+ .gsub(/([0-9]{1})\.0 /, '\1 ')
1185
+
1186
+ CURRENCY_REGEX.match(rate) do |m|
1187
+ rate = rate.gsub(m[0], "#{convert_currency(m[1])} #{currency_symbol} ")
1188
+ end
1189
+
1190
+ rate
1191
+ end
1192
+
1193
+ def commodity_measures(commodity)
1194
+ ids = commodity.data.relationships.import_measures.data.map(&:id) + commodity.data.relationships.export_measures.data.map(&:id)
1195
+
1196
+ commodity.included.select{|obj| ids.include? obj.id}
1197
+ end
1198
+
1199
+ def measure_is_quota(measure)
1200
+ !measure.relationships.order_number.data.nil?
1201
+ end
1202
+
1203
+ def measure_footnotes(measure)
1204
+ measure.relationships.footnotes.data.map
1205
+ end
1206
+
1207
+ def measure_duty_expression(measure)
1208
+ measure.relationships.duty_expression.data
1209
+ end
1210
+
1211
+ def pr_measures(v2_commodity)
1212
+ # c = Uktt::Commodity.new(commodity_id: '3403910000')
1213
+ # v2 = c.retrieve
1214
+ v2_commodity.included.select{|obj| obj.type == 'measure' && measure_is_pr(obj)}
1215
+ end
1216
+
1217
+ def anti_dumping_measures(v2_commodity)
1218
+ anti_dumping_ids = anti_dumping_measure_type_ids
1219
+ v2_commodity.included.select{ |obj| obj.type == 'measure' && anti_dumping_ids.include?(obj.relationships.measure_type.data.id) }
1220
+ end
1221
+
1222
+ def measure_is_pr(measure)
1223
+ P_AND_R_MEASURE_TYPES.include?(measure.relationships.measure_type.data.id)
1224
+ end
1225
+
1226
+ def prohibitions_and_restrictions
1227
+ cell_style = {
1228
+ padding: 0,
1229
+ borders: [],
1230
+ inline_format: true
1231
+ }
1232
+ table_opts = {
1233
+ column_widths: pr_table_column_widths,
1234
+ width: @printable_width,
1235
+ cell_style: cell_style
1236
+ }
1237
+ prs_array = pr_header_row
1238
+
1239
+ @prs.each do |id, pr|
1240
+
1241
+ commodity_ids = pr[:commodities].uniq
1242
+
1243
+ while commodity_ids.length > 0
1244
+ prs_array << [
1245
+ quota_commodities(commodity_ids.shift(prs_array.length == 2 ? 46 : 56)),
1246
+ pr[:measures].attributes.import ? "Import" : "Export", # Import/Export
1247
+ pr[:description], # Description, was Measure Type Code
1248
+ pr[:requirements].join("<br/><br/>"), # Requirements, was Measure Group Code
1249
+ pr[:conditions].join("<br/>"), # Document Code/s
1250
+ # '', # Ex-heading Indicator
1251
+ ]
1252
+ end
1253
+ end
1254
+
1255
+ unless prs_array.length <= 2 || false
1256
+
1257
+ start_new_page
1258
+
1259
+ font_size(19) do
1260
+ text "Chapter #{@chapter.data.attributes.goods_nomenclature_item_id[0..1].gsub(/^0/, '')}#{Prawn::Text::NBSP * 4}<b>Additional Information</b>",
1261
+ inline_format: true
1262
+ end
1263
+
1264
+ font_size(13) do
1265
+ pad_bottom(13) do
1266
+ text '<b>Prohibitions and Restrictions</b>',
1267
+ inline_format: true
1268
+ end
1269
+ end
1270
+
1271
+ table prs_array, table_opts do |t|
1272
+ t.cells.border_width = 0.25
1273
+ t.cells.borders = %i[top bottom]
1274
+ t.cells.padding_top = 4
1275
+ t.cells.padding_bottom = 6
1276
+ t.cells.padding_right = 9
1277
+ t.row(0).border_width = 1
1278
+ t.row(0).borders = [:top]
1279
+ t.row(1).borders = [:bottom]
1280
+ t.row(0).padding_top = 0
1281
+ t.row(0).padding_bottom = 0
1282
+ t.row(1).padding_top = 0
1283
+ t.row(1).padding_bottom = 2
1284
+ end
1285
+ end
1286
+ end
1287
+
1288
+ def pr_header_row
1289
+ [
1290
+ [
1291
+ format_text('<b>Commodity Code</b>'),
1292
+ format_text('<b>Import/ Export</b>'),
1293
+ format_text('<b>Description</b>'), # format_text('<b>Measure Type Code</b>'),
1294
+ format_text('<b>Requirements</b>'), # format_text('<b>Measure Group Code</b>'),
1295
+ format_text('<b>Document Code/s</b>'),
1296
+ # format_text('<b>Ex-heading Indicator</b>')
1297
+ ],
1298
+ (1..5).to_a
1299
+ ]
1300
+ end
1301
+
1302
+ def anti_dumpings
1303
+ return if @anti_dumpings.empty?
1304
+
1305
+ # group commodities by goods nomenclature item id and additional codes
1306
+ grouped = @anti_dumpings.group_by do |_, value|
1307
+ value.keys.sort.map do |k|
1308
+ "#{k.to_s}_#{value[k].keys.sort.join('_')}"
1309
+ end.join('_')
1310
+ end.map do |_, value|
1311
+ { value.map(&:first) => value.first.last }
1312
+ end.inject({}, &:merge)
1313
+
1314
+ output = anti_dumping_header_row
1315
+ # represent each line from grouped data as 3+ rows - 1st goods nomenclatures, 2nd geo area id + 1st info row, 3rd and next - rest of the rows with info
1316
+ output += grouped.map do |goods_nomenclature_item_ids, data|
1317
+ [
1318
+ # 1st row
1319
+ [ make_cell(quota_commodities(goods_nomenclature_item_ids), borders: []), make_cell("", borders: []), make_cell("", borders: []), make_cell("", borders: []) ],
1320
+ ].concat(
1321
+ data.map do |geographical_area_id, additional_codes|
1322
+ [
1323
+ # 2nd row
1324
+ [ make_cell("", borders: []), make_cell(geographical_area_id, borders: []), make_cell(additional_codes.first.first, borders: []), make_cell(additional_codes.first.last, borders: []) ]
1325
+ ].concat(
1326
+ # 3rd and next, show additional_code_id only on first line only
1327
+ additional_codes.drop(1).map do |additional_code_id, description|
1328
+ description.split(/<br\/?>/).map do |description_line|
1329
+ borders = description.index(description_line) === 0 ? [:top] : []
1330
+ additional_code_text = description.index(description_line) === 0 ? additional_code_id : ""
1331
+ [ make_cell("", borders: []), make_cell("", borders: []), make_cell(additional_code_text, borders: borders), make_cell(description_line, borders: borders) ]
1332
+ end
1333
+ end.flatten(1)
1334
+ ).push([ make_cell("", borders: []),
1335
+ make_cell("", { borders: %i[bottom] }),
1336
+ make_cell("", { borders: %i[bottom] }),
1337
+ make_cell("", { borders: %i[bottom] }) ]
1338
+ )
1339
+ end.flatten(1)
1340
+ ).tap(&:pop).push([ make_cell("", { borders: %i[bottom] }),
1341
+ make_cell("", { borders: %i[bottom] }),
1342
+ make_cell("", { borders: %i[bottom] }),
1343
+ make_cell("", { borders: %i[bottom] }) ]
1344
+ )
1345
+ end.flatten(1)
1346
+
1347
+ start_new_page
1348
+
1349
+ font_size(19) do
1350
+ text "Chapter #{@chapter.data.attributes.goods_nomenclature_item_id[0..1].gsub(/^0/, '')}#{Prawn::Text::NBSP * 4}<b>Additional Information</b>",
1351
+ inline_format: true
1352
+ end
1353
+
1354
+ font_size(13) do
1355
+ pad_bottom(13) do
1356
+ text '<b>Anti-dumping duties</b>',
1357
+ inline_format: true
1358
+ end
1359
+ end
1360
+
1361
+ cell_style = {
1362
+ padding: 0,
1363
+ inline_format: true
1364
+ }
1365
+ table_opts = {
1366
+ column_widths: anti_dumping_table_column_widths,
1367
+ width: @printable_width,
1368
+ cell_style: cell_style
1369
+ }
1370
+
1371
+ table output, table_opts do |t|
1372
+ t.cells.border_width = 0.25
1373
+ t.cells.padding_right = 9
1374
+ t.row(0).border_width = 1
1375
+ t.row(0).borders = [:top]
1376
+ t.row(1).borders = [:bottom]
1377
+ t.row(0).padding_top = 0
1378
+ t.row(0).padding_bottom = 0
1379
+ t.row(1).padding_top = 0
1380
+ t.row(1).padding_bottom = 2
1381
+ output[2..-1].each_with_index do |line, i|
1382
+ t.row(i + 2).padding_top = "#{line[0]}#{line[1]}#{line[2]}" == '' ? 0 : 6
1383
+ end
1384
+ end
1385
+ end
1386
+
1387
+ def anti_dumping_header_row
1388
+ [
1389
+ [
1390
+ format_text('<b>Commodity Code</b>'),
1391
+ format_text('<b>Country of Origin</b>'),
1392
+ format_text('<b>Additional Code</b>'),
1393
+ format_text('<b>Description/Rate of Duty/Additional Information</b>'),
1394
+ ],
1395
+ (1..4).to_a
1396
+ ]
1397
+ end
1398
+
1399
+ def convert_currency(amount, precision = 1)
1400
+ (amount.to_f * @currency_exchange_rate).round(precision)
1401
+ end
1402
+
1403
+ def currency_symbol
1404
+ return '€' unless @currency
1405
+
1406
+ SUPPORTED_CURRENCIES[@currency]
1407
+ end
1408
+
1409
+ UNIT_ABBREVIATIONS = {
1410
+ 'Number of items'.to_sym => 'Number',
1411
+ 'Hectokilogram'.to_sym => 'Kg'
1412
+ }.freeze
1413
+
1414
+ RECIPIENT_SHORTENER = {
1415
+ # 'EU-Canada agreement: re-imported goods'.to_sym => 'EU-CA',
1416
+ # 'Economic Partnership Agreements'.to_sym => 'EPA',
1417
+ # 'Eastern and Southern Africa States'.to_sym => 'ESAS',
1418
+ # 'GSP (R 12/978) - Annex IV'.to_sym => 'GSP-AX4',
1419
+ # 'OCTs (Overseas Countries and Territories)'.to_sym => 'OCT',
1420
+ # 'GSP+ (incentive arrangement for sustainable development and good governance)'.to_sym => 'GSP+',
1421
+ # 'SADC EPA'.to_sym => 'SADC',
1422
+ # 'GSP (R 12/978) - General arrangements'.to_sym => 'GSP-GA',
1423
+ # 'GSP (R 01/2501) - General arrangements'.to_sym => 'GSP',
1424
+ # 'Central America'.to_sym => 'CEN-AM',
1425
+ }.freeze
1426
+ private
1427
+
1428
+ def footnote_reference_key(footnote_code)
1429
+ "FOOTNOTE-#{footnote_code}"
1430
+ end
1431
+ end