uktt 0.2.14
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +12 -0
- data/.rspec +3 -0
- data/.rubocop.yml +21 -0
- data/.travis.yml +7 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +54 -0
- data/LICENSE.txt +21 -0
- data/README.md +307 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/exe/uktt +4 -0
- data/lib/uktt.rb +61 -0
- data/lib/uktt/chapter.rb +57 -0
- data/lib/uktt/cli.rb +174 -0
- data/lib/uktt/commodity.rb +61 -0
- data/lib/uktt/export_chapter_pdf.rb +1431 -0
- data/lib/uktt/export_cover_pdf.rb +225 -0
- data/lib/uktt/heading.rb +62 -0
- data/lib/uktt/http.rb +43 -0
- data/lib/uktt/monetary_exchange_rate.rb +53 -0
- data/lib/uktt/pdf.rb +38 -0
- data/lib/uktt/quota.rb +31 -0
- data/lib/uktt/section.rb +51 -0
- data/lib/uktt/version.rb +3 -0
- data/uktt.gemspec +45 -0
- data/uktt.yaml +5 -0
- data/vendor/assets/HMRC-logo.png +0 -0
- data/vendor/assets/Open_Sans/LICENSE.txt +202 -0
- data/vendor/assets/Open_Sans/OpenSans-Bold.ttf +0 -0
- data/vendor/assets/Open_Sans/OpenSans-BoldItalic.ttf +0 -0
- data/vendor/assets/Open_Sans/OpenSans-Regular.ttf +0 -0
- data/vendor/assets/Open_Sans/OpenSans-RegularItalic.ttf +0 -0
- data/vendor/assets/Open_Sans/OpenSans-SemiBold.ttf +0 -0
- data/vendor/assets/Open_Sans/OpenSans-SemiBoldItalic.ttf +0 -0
- data/vendor/assets/Overpass_Mono/OFL.txt +93 -0
- data/vendor/assets/Overpass_Mono/OverpassMono-Bold.ttf +0 -0
- data/vendor/assets/Overpass_Mono/OverpassMono-Regular.ttf +0 -0
- metadata +198 -0
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -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__)
|
data/bin/setup
ADDED
data/exe/uktt
ADDED
data/lib/uktt.rb
ADDED
@@ -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
|
data/lib/uktt/chapter.rb
ADDED
@@ -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
|
data/lib/uktt/cli.rb
ADDED
@@ -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('&', '&')
|
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
|