uktt 0.2.14

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.
@@ -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