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