zatca 0.1.2 → 1.0.1
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 +4 -4
- data/README.md +11 -2
- data/bin/console +0 -0
- data/bin/setup +0 -0
- data/lib/zatca/client.rb +173 -0
- data/lib/zatca/hacks.rb +45 -0
- data/lib/zatca/hashing.rb +18 -0
- data/lib/zatca/qr_code_extractor.rb +31 -0
- data/lib/zatca/qr_code_generator.rb +9 -2
- data/lib/zatca/signing/certificate.rb +78 -0
- data/lib/zatca/signing/csr.rb +220 -0
- data/lib/zatca/signing/ecdsa.rb +59 -0
- data/lib/zatca/tag.rb +18 -8
- data/lib/zatca/tags.rb +5 -1
- data/lib/zatca/tags_schema.rb +5 -5
- data/lib/zatca/types.rb +7 -0
- data/lib/zatca/ubl/base_component.rb +142 -0
- data/lib/zatca/ubl/builder.rb +166 -0
- data/lib/zatca/ubl/common_aggregate_components/allowance_charge.rb +64 -0
- data/lib/zatca/ubl/common_aggregate_components/classified_tax_category.rb +25 -0
- data/lib/zatca/ubl/common_aggregate_components/delivery.rb +27 -0
- data/lib/zatca/ubl/common_aggregate_components/invoice_line.rb +63 -0
- data/lib/zatca/ubl/common_aggregate_components/item.rb +21 -0
- data/lib/zatca/ubl/common_aggregate_components/legal_monetary_total.rb +59 -0
- data/lib/zatca/ubl/common_aggregate_components/party.rb +28 -0
- data/lib/zatca/ubl/common_aggregate_components/party_identification.rb +25 -0
- data/lib/zatca/ubl/common_aggregate_components/party_legal_entity.rb +19 -0
- data/lib/zatca/ubl/common_aggregate_components/party_tax_scheme.rb +30 -0
- data/lib/zatca/ubl/common_aggregate_components/postal_address.rb +59 -0
- data/lib/zatca/ubl/common_aggregate_components/price.rb +20 -0
- data/lib/zatca/ubl/common_aggregate_components/tax_category.rb +56 -0
- data/lib/zatca/ubl/common_aggregate_components/tax_total.rb +58 -0
- data/lib/zatca/ubl/common_aggregate_components.rb +2 -0
- data/lib/zatca/ubl/invoice.rb +481 -0
- data/lib/zatca/ubl/invoice_subtype_builder.rb +50 -0
- data/lib/zatca/ubl/signing/cert.rb +48 -0
- data/lib/zatca/ubl/signing/invoice_signed_data_reference.rb +44 -0
- data/lib/zatca/ubl/signing/key_info.rb +25 -0
- data/lib/zatca/ubl/signing/object.rb +20 -0
- data/lib/zatca/ubl/signing/qualifying_properties.rb +27 -0
- data/lib/zatca/ubl/signing/signature.rb +50 -0
- data/lib/zatca/ubl/signing/signature_information.rb +19 -0
- data/lib/zatca/ubl/signing/signature_properties_reference.rb +26 -0
- data/lib/zatca/ubl/signing/signed_info.rb +21 -0
- data/lib/zatca/ubl/signing/signed_properties.rb +81 -0
- data/lib/zatca/ubl/signing/signed_signature_properties.rb +23 -0
- data/lib/zatca/ubl/signing/ubl_document_signatures.rb +25 -0
- data/lib/zatca/ubl/signing/ubl_extension.rb +22 -0
- data/lib/zatca/ubl/signing/ubl_extensions.rb +17 -0
- data/lib/zatca/ubl/signing.rb +2 -0
- data/lib/zatca/ubl.rb +2 -0
- data/lib/zatca/version.rb +1 -1
- data/lib/zatca.rb +27 -3
- data/zatca.gemspec +52 -0
- metadata +165 -10
- data/Gemfile.lock +0 -100
@@ -0,0 +1,59 @@
|
|
1
|
+
require "starkbank-ecdsa"
|
2
|
+
|
3
|
+
class ZATCA::Signing::ECDSA
|
4
|
+
def self.sign(content:, private_key: nil, private_key_path: nil, decode_from_base64: false)
|
5
|
+
private_key = parse_private_key(key: private_key, key_path: private_key_path, decode_from_base64: decode_from_base64)
|
6
|
+
|
7
|
+
ecdsa_signature = EllipticCurve::Ecdsa.sign(content, private_key)
|
8
|
+
|
9
|
+
{
|
10
|
+
base64: ecdsa_signature.toBase64,
|
11
|
+
bytes: ecdsa_signature.toDer
|
12
|
+
}
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.add_header_blocks(key_content)
|
16
|
+
# If key is missing header blocks, add them, otherwise return it as is
|
17
|
+
header = "-----BEGIN EC PRIVATE KEY-----"
|
18
|
+
footer = "-----END EC PRIVATE KEY-----"
|
19
|
+
|
20
|
+
unless key_content.include?(header) && key_content.include?(footer)
|
21
|
+
key_content = "#{header}\n#{key_content}\n#{footer}"
|
22
|
+
end
|
23
|
+
|
24
|
+
key_content
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.read_private_key_from_pem(pem)
|
28
|
+
EllipticCurve::PrivateKey.fromPem(add_header_blocks(pem))
|
29
|
+
end
|
30
|
+
|
31
|
+
# Returns a parsed private key
|
32
|
+
# If decode_from_base64 is set to true, the key will be decoded from base64
|
33
|
+
# before passing to OpenSSL to read it. # This is necessary because that's how
|
34
|
+
# ZATCA's sample key is provided.
|
35
|
+
def self.parse_private_key(key: nil, key_path: nil, decode_from_base64: false)
|
36
|
+
parsed_key = if key.is_a?(EllipticCurve::PrivateKey)
|
37
|
+
key
|
38
|
+
elsif key.is_a?(String)
|
39
|
+
key_content = if decode_from_base64
|
40
|
+
Base64.decode64(key)
|
41
|
+
else
|
42
|
+
key
|
43
|
+
end
|
44
|
+
|
45
|
+
read_private_key_from_pem(key_content)
|
46
|
+
elsif key_path.present?
|
47
|
+
key_content = File.read(key_path)
|
48
|
+
key_content = Base64.decode64(key_content) if decode_from_base64
|
49
|
+
|
50
|
+
read_private_key_from_pem(key_content)
|
51
|
+
end
|
52
|
+
|
53
|
+
if parsed_key.blank?
|
54
|
+
raise ArgumentError.new("private_key or private_key_path is required")
|
55
|
+
end
|
56
|
+
|
57
|
+
parsed_key
|
58
|
+
end
|
59
|
+
end
|
data/lib/zatca/tag.rb
CHANGED
@@ -5,9 +5,21 @@ module ZATCA
|
|
5
5
|
vat_registration_number: 2,
|
6
6
|
timestamp: 3,
|
7
7
|
invoice_total: 4,
|
8
|
-
vat_total: 5
|
8
|
+
vat_total: 5,
|
9
|
+
xml_invoice_hash: 6,
|
10
|
+
ecdsa_signature: 7,
|
11
|
+
ecdsa_public_key: 8,
|
12
|
+
ecdsa_stamp_signature: 9 # TODO: is this needed ?
|
9
13
|
}.freeze
|
10
14
|
|
15
|
+
PHASE_1_TAGS = [
|
16
|
+
:seller_name,
|
17
|
+
:vat_registration_number,
|
18
|
+
:timestamp,
|
19
|
+
:invoice_total,
|
20
|
+
:vat_total
|
21
|
+
].freeze
|
22
|
+
|
11
23
|
attr_accessor :id, :key, :value
|
12
24
|
|
13
25
|
def initialize(key:, value:)
|
@@ -20,14 +32,12 @@ module ZATCA
|
|
20
32
|
{id: @id, key: @key, value: @value}
|
21
33
|
end
|
22
34
|
|
23
|
-
def
|
24
|
-
|
25
|
-
|
26
|
-
# All of this should be in 8-bit ASCII.
|
27
|
-
tlv = @id.chr + @value.bytesize.chr + value
|
35
|
+
def should_be_utf8_encoded?
|
36
|
+
PHASE_1_TAGS.include?(key)
|
37
|
+
end
|
28
38
|
|
29
|
-
|
30
|
-
|
39
|
+
def to_tlv
|
40
|
+
tlv = @id.chr + @value.bytesize.chr + @value
|
31
41
|
tlv.force_encoding("ASCII-8BIT")
|
32
42
|
end
|
33
43
|
end
|
data/lib/zatca/tags.rb
CHANGED
@@ -33,8 +33,12 @@ module ZATCA
|
|
33
33
|
Base64.strict_encode64(to_tlv)
|
34
34
|
end
|
35
35
|
|
36
|
-
|
36
|
+
# This is helpful for debugging only, for ZATCA's requirements just call `to_base64`
|
37
|
+
def to_hex_tlv
|
38
|
+
to_tlv.unpack1("H*")
|
39
|
+
end
|
37
40
|
|
41
|
+
# This is helpful for debugging only, for ZATCA's requirements just call `to_base64`
|
38
42
|
def to_tlv
|
39
43
|
@tags.map(&:to_tlv).join("")
|
40
44
|
end
|
data/lib/zatca/tags_schema.rb
CHANGED
@@ -13,10 +13,10 @@ module ZATCA
|
|
13
13
|
required(:invoice_total).filled(:string)
|
14
14
|
required(:vat_total).filled(:string)
|
15
15
|
|
16
|
-
#
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
16
|
+
# Data types required for Phase 2 by 1 January 2023
|
17
|
+
optional(:xml_invoice_hash).filled(:string)
|
18
|
+
optional(:ecdsa_signature).filled(:string)
|
19
|
+
optional(:ecdsa_public_key).filled(:string)
|
20
|
+
optional(:ecdsa_stamp_signature).filled(:string)
|
21
21
|
end
|
22
22
|
end
|
data/lib/zatca/types.rb
ADDED
@@ -0,0 +1,142 @@
|
|
1
|
+
require "dry-initializer"
|
2
|
+
require_relative "../types"
|
3
|
+
|
4
|
+
class ZATCA::UBL::BaseComponent
|
5
|
+
extend Dry::Initializer
|
6
|
+
attr_accessor :elements, :attributes, :name, :value, :index
|
7
|
+
|
8
|
+
# def self.guard_dig(obj)
|
9
|
+
# unless obj.respond_to?(:dig)
|
10
|
+
# raise TypeError, "#{obj.class.name} does not have #dig method"
|
11
|
+
# end
|
12
|
+
# end
|
13
|
+
|
14
|
+
ArrayOfBaseComponentOrNil = ZATCA::Types::Array.of(ZATCA::Types.Instance(ZATCA::UBL::BaseComponent))
|
15
|
+
.default { [] }
|
16
|
+
.constructor { |value| value.blank? ? [] : value.compact }
|
17
|
+
|
18
|
+
option :elements,
|
19
|
+
type: ArrayOfBaseComponentOrNil,
|
20
|
+
default: proc { [] },
|
21
|
+
optional: true
|
22
|
+
|
23
|
+
option :attributes, type: ZATCA::Types::Strict::Hash, default: proc { {} }, optional: true
|
24
|
+
option :value, type: ZATCA::Types::Coercible::String, default: proc { "" }, optional: true
|
25
|
+
option :name, type: ZATCA::Types::Strict::String, default: proc { "" }, optional: true
|
26
|
+
option :index, type: ZATCA::Types::Coercible::Integer.optional, default: proc {}, optional: true
|
27
|
+
|
28
|
+
# def initialize(elements: [], attributes: {}, value: "", name: "", index: nil)
|
29
|
+
# @elements = elements
|
30
|
+
# @attributes = attributes
|
31
|
+
# @value = value
|
32
|
+
# @name = name
|
33
|
+
|
34
|
+
# # HACK: Add a nil index property to be used for cases where we need
|
35
|
+
# # sequential IDs, this list can be populated after the array is built
|
36
|
+
# @index = index
|
37
|
+
# end
|
38
|
+
|
39
|
+
# There are cases where we end up constructing elements with no content
|
40
|
+
# and we don't want to include them in the final XML.
|
41
|
+
#
|
42
|
+
# This method helps us to return nil if the element has no attributes,
|
43
|
+
# elements or value.
|
44
|
+
#
|
45
|
+
# which is then caught in the `build_xml` method (using `elements.compact`)
|
46
|
+
# and ignored.
|
47
|
+
def self.build(elements: [], attributes: {}, value: "", name: "", index: nil)
|
48
|
+
return nil if elements.blank? && attributes.blank? && value.blank?
|
49
|
+
|
50
|
+
new(elements: elements, attributes: attributes, value: value, name: name, index: index)
|
51
|
+
end
|
52
|
+
|
53
|
+
def [](name)
|
54
|
+
elements.find { |element| element.name == name }
|
55
|
+
end
|
56
|
+
|
57
|
+
def dig(key, *args)
|
58
|
+
value = self[key]
|
59
|
+
return value if args.length == 0 || value.nil?
|
60
|
+
# DigRb.guard_dig(value)
|
61
|
+
value.dig(*args)
|
62
|
+
end
|
63
|
+
|
64
|
+
# TODO: Under Active Development
|
65
|
+
def find_nested_element_by_path(path)
|
66
|
+
path_parts = path.split("/")
|
67
|
+
nested_element = self
|
68
|
+
|
69
|
+
path_parts.each_with_index do |path_part, index|
|
70
|
+
# byebug
|
71
|
+
# next_path_part = path_parts[index + 1]
|
72
|
+
# found_element = nested_element[path_part]
|
73
|
+
# found_next_path_part = found_element[next_path_part]
|
74
|
+
|
75
|
+
# element_with_next_path_part = found_element.find do |child_element|
|
76
|
+
# child_element.name == next_path_part
|
77
|
+
# end
|
78
|
+
# byebug
|
79
|
+
# nested_element.elements.each do |element|
|
80
|
+
# byebug
|
81
|
+
# next_element = element[path_part]
|
82
|
+
|
83
|
+
# if next_element && next_element[next_path_part]
|
84
|
+
# nested_element = next_element[next_path_part]
|
85
|
+
# break
|
86
|
+
# elsif next_element.present?
|
87
|
+
# nested_element = next_element
|
88
|
+
# end
|
89
|
+
# end
|
90
|
+
|
91
|
+
# nested_element = found_element if found_element.present?
|
92
|
+
end
|
93
|
+
|
94
|
+
nested_element
|
95
|
+
end
|
96
|
+
|
97
|
+
def schema
|
98
|
+
self.class.schema
|
99
|
+
end
|
100
|
+
|
101
|
+
def to_h
|
102
|
+
{
|
103
|
+
name => {
|
104
|
+
attributes: attributes,
|
105
|
+
**elements.map(&:to_h),
|
106
|
+
value: value
|
107
|
+
}
|
108
|
+
}
|
109
|
+
end
|
110
|
+
|
111
|
+
def to_xml
|
112
|
+
builder = Nokogiri::XML::Builder.new(encoding: "UTF-8") do |xml|
|
113
|
+
xml.root { build_xml(xml) }
|
114
|
+
end
|
115
|
+
|
116
|
+
builder.to_xml
|
117
|
+
end
|
118
|
+
|
119
|
+
def build_xml(xml)
|
120
|
+
xml.send(name, attributes) do
|
121
|
+
if elements.length > 0
|
122
|
+
elements.compact.each { |element| element.build_xml(xml) }
|
123
|
+
elsif value.present?
|
124
|
+
xml.text(value)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def generate_xml(
|
130
|
+
canonicalized: false,
|
131
|
+
spaces: 2,
|
132
|
+
apply_invoice_hacks: false,
|
133
|
+
remove_root_xml_tag: false
|
134
|
+
)
|
135
|
+
ZATCA::UBL::Builder.new(element: self).build(
|
136
|
+
canonicalized: canonicalized,
|
137
|
+
spaces: spaces,
|
138
|
+
apply_invoice_hacks: apply_invoice_hacks,
|
139
|
+
remove_root_xml_tag: remove_root_xml_tag
|
140
|
+
)
|
141
|
+
end
|
142
|
+
end
|
@@ -0,0 +1,166 @@
|
|
1
|
+
class ZATCA::UBL::Builder
|
2
|
+
extend Dry::Initializer
|
3
|
+
|
4
|
+
option :element, type: ZATCA::Types.Instance(ZATCA::UBL::BaseComponent)
|
5
|
+
|
6
|
+
def build(
|
7
|
+
canonicalized: false,
|
8
|
+
spaces: 4,
|
9
|
+
apply_invoice_hacks: false,
|
10
|
+
remove_root_xml_tag: false
|
11
|
+
)
|
12
|
+
@remove_root_xml_tag = remove_root_xml_tag
|
13
|
+
|
14
|
+
builder = Nokogiri::XML::Builder.new(encoding: "UTF-8") do |xml|
|
15
|
+
element.build_xml(xml)
|
16
|
+
end
|
17
|
+
|
18
|
+
xml = if canonicalized
|
19
|
+
canonicalized_xml(builder: builder)
|
20
|
+
else
|
21
|
+
uncanonicalized_xml(builder: builder, spaces: spaces)
|
22
|
+
end
|
23
|
+
|
24
|
+
xml = apply_hacks_to_invoice(element, xml) if apply_invoice_hacks
|
25
|
+
|
26
|
+
xml
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
# ZATCA sadly requires very specific and unconventional indentation in the XML
|
32
|
+
# when it is pretty (uncanonicalized), the only way we can accomplish this is
|
33
|
+
# to find and replace blocks manually.
|
34
|
+
def apply_hacks_to_invoice(element, xml)
|
35
|
+
return xml unless element.is_a?(ZATCA::UBL::Invoice)
|
36
|
+
|
37
|
+
apply_qualifying_properties_hacks(element, xml)
|
38
|
+
end
|
39
|
+
|
40
|
+
def apply_qualifying_properties_hacks(invoice, xml)
|
41
|
+
return xml if invoice.qualifying_properties.blank?
|
42
|
+
|
43
|
+
regex = ZATCA::Hacks.qualifying_properties_regex
|
44
|
+
|
45
|
+
xml.gsub(regex, invoice.qualifying_properties)
|
46
|
+
end
|
47
|
+
|
48
|
+
# This function does not produce canonicalization matching C14N 1.1, it applies
|
49
|
+
# C14N 1.1 then manually adds back the whitespace in the format that ZATCA
|
50
|
+
# expects.
|
51
|
+
def canonicalized_xml(builder:)
|
52
|
+
builder.doc.canonicalize(Nokogiri::XML::XML_C14N_1_1)
|
53
|
+
|
54
|
+
# TODO: In case ZATCA ever asks us to use their whitespace format again.
|
55
|
+
# In some meetings they say we have to use it, in some meetings they say
|
56
|
+
# we don't. The simpler approach is that we don't use it.
|
57
|
+
#
|
58
|
+
# ZATCA's docs specifically state we must use C14N 1.1 canonicalization.
|
59
|
+
# xml = uncanonicalized_xml(builder: builder, spaces: 4)
|
60
|
+
# xml_doc = Nokogiri::XML(xml)
|
61
|
+
|
62
|
+
# canonical_xml = xml_doc.canonicalize(Nokogiri::XML::XML_C14N_1_1)
|
63
|
+
|
64
|
+
# canonical_xml
|
65
|
+
end
|
66
|
+
|
67
|
+
def uncanonicalized_xml(builder:, spaces: 4)
|
68
|
+
builder.to_xml(indent: spaces.to_i)
|
69
|
+
|
70
|
+
# xml = builder.to_xml(indent: spaces.to_i)
|
71
|
+
# xml = match_xml_string_to_zatca_whitespaces(xml)
|
72
|
+
# xml
|
73
|
+
end
|
74
|
+
|
75
|
+
def match_xml_string_to_zatca_whitespaces(xml)
|
76
|
+
# ZATCA has elements that are not spaced by multiples of 4, and random new
|
77
|
+
# lines with trailing whitespaces, so we need to manually adjust our
|
78
|
+
# indentation to match ZATCA's.
|
79
|
+
zatca_weird_whitespaces.each do |whitespace_hash|
|
80
|
+
xml.gsub!(whitespace_hash[:our_version], whitespace_hash[:zatca_version])
|
81
|
+
end
|
82
|
+
|
83
|
+
# Canonicalization already removes the root XML tag for us, but since we had
|
84
|
+
# to create a new uncanonicalized document for ZATCA's invoice hacks, we
|
85
|
+
# have to remove it manually.
|
86
|
+
if @remove_root_xml_tag
|
87
|
+
xml.gsub!("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n", "")
|
88
|
+
end
|
89
|
+
|
90
|
+
# ZATCA Removes the final newline character, so we do the same
|
91
|
+
xml.chomp
|
92
|
+
|
93
|
+
# This part is not clear, ZATCA shared documents with us that use CRLF
|
94
|
+
# but the samples in the SDK use LF, so we're not sure which one is correct.
|
95
|
+
# ZATCA wants CRLF (\r\n) in their canonicalized form instead of just LF (\n)
|
96
|
+
xml.gsub!("\n", "\r\n")
|
97
|
+
|
98
|
+
xml
|
99
|
+
end
|
100
|
+
|
101
|
+
# Not sure if this is needed, in some meetings ZATCA says you have to match
|
102
|
+
# their whitspace exactly and in some meetings they say you don't.
|
103
|
+
# HACK: This is really hacky, using regexes or XPaths would be better, but
|
104
|
+
# that wasn't easy to build and maintain, so we're using this if/until we run
|
105
|
+
# into issues.
|
106
|
+
#
|
107
|
+
# We may eventually go to an approach where we just have hardcoded XML in each
|
108
|
+
# element's file and we just add in the values instead of generating our own
|
109
|
+
# XML if this gets too hard to maintain.
|
110
|
+
def zatca_weird_whitespaces
|
111
|
+
@_zatca_weird_whitespaces ||= [
|
112
|
+
{
|
113
|
+
our_version: "<cbc:ProfileID>",
|
114
|
+
zatca_version: "\n <cbc:ProfileID>"
|
115
|
+
},
|
116
|
+
{
|
117
|
+
our_version: "<cac:AccountingSupplierParty>",
|
118
|
+
zatca_version: "\n \n <cac:AccountingSupplierParty>"
|
119
|
+
},
|
120
|
+
{
|
121
|
+
our_version: " <cbc:CompanyID>",
|
122
|
+
zatca_version: " <cbc:CompanyID>"
|
123
|
+
},
|
124
|
+
{
|
125
|
+
our_version: " <cac:TaxCategory>",
|
126
|
+
zatca_version: " <cac:TaxCategory>"
|
127
|
+
},
|
128
|
+
{
|
129
|
+
our_version: " </cac:TaxCategory>",
|
130
|
+
zatca_version: " </cac:TaxCategory>"
|
131
|
+
},
|
132
|
+
{
|
133
|
+
our_version: ' <cbc:ID schemeAgencyID="6" schemeID="UN/ECE 5305">S</cbc:ID>',
|
134
|
+
zatca_version: ' <cbc:ID schemeAgencyID="6" schemeID="UN/ECE 5305">S</cbc:ID>'
|
135
|
+
},
|
136
|
+
{
|
137
|
+
our_version: "<cac:TaxScheme>\n <cbc:ID schemeAgencyID=\"6\" schemeID=\"UN/ECE 5153\">VAT</cbc:ID>",
|
138
|
+
zatca_version: "<cac:TaxScheme>\n <cbc:ID schemeAgencyID=\"6\" schemeID=\"UN/ECE 5153\">VAT</cbc:ID>"
|
139
|
+
},
|
140
|
+
{
|
141
|
+
our_version: "<cac:TaxTotal>\n <cbc:TaxAmount currencyID=\"SAR\">",
|
142
|
+
zatca_version: "<cac:TaxTotal>\n <cbc:TaxAmount currencyID=\"SAR\">"
|
143
|
+
},
|
144
|
+
{
|
145
|
+
our_version: " <cbc:RoundingAmount currencyID=\"SAR\">",
|
146
|
+
zatca_version: " <cbc:RoundingAmount currencyID=\"SAR\">"
|
147
|
+
},
|
148
|
+
{
|
149
|
+
our_version: " <cbc:ChargeIndicator>",
|
150
|
+
zatca_version: " <cbc:ChargeIndicator>"
|
151
|
+
},
|
152
|
+
{
|
153
|
+
our_version: " <cbc:AllowanceChargeReason>",
|
154
|
+
zatca_version: " <cbc:AllowanceChargeReason>"
|
155
|
+
},
|
156
|
+
{
|
157
|
+
our_version: " <cbc:Amount currencyID=\"SAR\">",
|
158
|
+
zatca_version: " <cbc:Amount currencyID=\"SAR\">"
|
159
|
+
},
|
160
|
+
{
|
161
|
+
our_version: "<cbc:ID schemeAgencyID=\"6\" schemeID=\"UN/ECE 5305\">S</cbc:ID>\n <cbc:Percent>",
|
162
|
+
zatca_version: "<cbc:ID schemeAgencyID=\"6\" schemeID=\"UN/ECE 5305\">S</cbc:ID>\n <cbc:Percent>"
|
163
|
+
}
|
164
|
+
]
|
165
|
+
end
|
166
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
class ZATCA::UBL::CommonAggregateComponents::AllowanceCharge < ZATCA::UBL::BaseComponent
|
2
|
+
attr_accessor :id, :charge_indicator, :allowance_charge_reason, :amount
|
3
|
+
|
4
|
+
# <cac:AllowanceCharge>
|
5
|
+
# <cbc:ID>1</cbc:ID>
|
6
|
+
# <cbc:ChargeIndicator>false</cbc:ChargeIndicator>
|
7
|
+
# <cbc:AllowanceChargeReason>discount</cbc:AllowanceChargeReason>
|
8
|
+
# <cbc:Amount currencyID="SAR">2</cbc:Amount>
|
9
|
+
# <cac:TaxCategory>
|
10
|
+
# <cbc:ID schemeAgencyID="6" schemeID="UN/ECE 5305">S</cbc:ID>
|
11
|
+
# <cbc:Percent>15</cbc:Percent>
|
12
|
+
# <cac:TaxScheme>
|
13
|
+
# <cbc:ID schemeAgencyID="6" schemeID="UN/ECE 5153">VAT</cbc:ID>
|
14
|
+
# </cac:TaxScheme>
|
15
|
+
# </cac:TaxCategory>
|
16
|
+
# </cac:AllowanceCharge>
|
17
|
+
|
18
|
+
def initialize(
|
19
|
+
charge_indicator:, allowance_charge_reason:, amount:, currency_id: "SAR",
|
20
|
+
tax_category: nil, add_tax_category: true, add_id: true, tax_categories: []
|
21
|
+
)
|
22
|
+
super()
|
23
|
+
|
24
|
+
@charge_indicator = charge_indicator.to_s
|
25
|
+
|
26
|
+
@allowance_charge_reason = allowance_charge_reason
|
27
|
+
@amount = amount
|
28
|
+
@currency_id = currency_id
|
29
|
+
|
30
|
+
@add_tax_category = add_tax_category
|
31
|
+
@tax_category = tax_category
|
32
|
+
@add_id = add_id
|
33
|
+
|
34
|
+
if add_tax_category && @tax_category.blank?
|
35
|
+
@tax_category = ZATCA::UBL::CommonAggregateComponents::TaxCategory.new
|
36
|
+
end
|
37
|
+
|
38
|
+
@tax_categories = if @tax_category.present? && tax_categories.blank?
|
39
|
+
[@tax_category]
|
40
|
+
else
|
41
|
+
tax_categories
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def name
|
46
|
+
"cac:AllowanceCharge"
|
47
|
+
end
|
48
|
+
|
49
|
+
def id_element
|
50
|
+
if @add_id && index.present?
|
51
|
+
ZATCA::UBL::BaseComponent.new(name: "cbc:ID", value: index)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def elements
|
56
|
+
[
|
57
|
+
id_element,
|
58
|
+
ZATCA::UBL::BaseComponent.new(name: "cbc:ChargeIndicator", value: @charge_indicator),
|
59
|
+
ZATCA::UBL::BaseComponent.new(name: "cbc:AllowanceChargeReason", value: @allowance_charge_reason),
|
60
|
+
ZATCA::UBL::BaseComponent.new(name: "cbc:Amount", value: @amount, attributes: {"currencyID" => @currency_id}),
|
61
|
+
*@tax_categories
|
62
|
+
]
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
class ZATCA::UBL::CommonAggregateComponents::ClassifiedTaxCategory < ZATCA::UBL::BaseComponent
|
2
|
+
attr_reader :scheme_id, :id
|
3
|
+
|
4
|
+
def initialize(id: "S", percent: "15.00", tax_scheme_id: "VAT")
|
5
|
+
super()
|
6
|
+
|
7
|
+
@id = id
|
8
|
+
@percent = percent
|
9
|
+
@tax_scheme_id = tax_scheme_id
|
10
|
+
end
|
11
|
+
|
12
|
+
def name
|
13
|
+
"cac:ClassifiedTaxCategory"
|
14
|
+
end
|
15
|
+
|
16
|
+
def elements
|
17
|
+
[
|
18
|
+
ZATCA::UBL::BaseComponent.new(name: "cbc:ID", value: @id),
|
19
|
+
ZATCA::UBL::BaseComponent.new(name: "cbc:Percent", value: @percent),
|
20
|
+
ZATCA::UBL::BaseComponent.new(name: "cac:TaxScheme", elements: [
|
21
|
+
ZATCA::UBL::BaseComponent.new(name: "cbc:ID", value: @tax_scheme_id)
|
22
|
+
])
|
23
|
+
]
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
class ZATCA::UBL::CommonAggregateComponents::Delivery < ZATCA::UBL::BaseComponent
|
2
|
+
attr_reader :scheme_id, :id
|
3
|
+
|
4
|
+
def initialize(actual_delivery_date:, latest_delivery_date: nil)
|
5
|
+
super()
|
6
|
+
|
7
|
+
@latest_delivery_date = latest_delivery_date
|
8
|
+
@actual_delivery_date = actual_delivery_date
|
9
|
+
end
|
10
|
+
|
11
|
+
def name
|
12
|
+
"cac:Delivery"
|
13
|
+
end
|
14
|
+
|
15
|
+
def latest_delivery_date_element
|
16
|
+
return nil if @latest_delivery_date.nil?
|
17
|
+
|
18
|
+
ZATCA::UBL::BaseComponent.new(name: "cbc:LatestDeliveryDate", value: @latest_delivery_date)
|
19
|
+
end
|
20
|
+
|
21
|
+
def elements
|
22
|
+
[
|
23
|
+
ZATCA::UBL::BaseComponent.new(name: "cbc:ActualDeliveryDate", value: @actual_delivery_date),
|
24
|
+
latest_delivery_date_element
|
25
|
+
]
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
class ZATCA::UBL::CommonAggregateComponents::InvoiceLine < ZATCA::UBL::BaseComponent
|
2
|
+
# <cac:InvoiceLine>
|
3
|
+
# <cbc:ID>1</cbc:ID>
|
4
|
+
# <cbc:InvoicedQuantity unitCode="PCE">44.000000</cbc:InvoicedQuantity>
|
5
|
+
# <cbc:LineExtensionAmount currencyID="SAR">966.00</cbc:LineExtensionAmount>
|
6
|
+
# <cac:TaxTotal>
|
7
|
+
# <cbc:TaxAmount currencyID="SAR">144.90</cbc:TaxAmount>
|
8
|
+
# <cbc:RoundingAmount currencyID="SAR">1110.90</cbc:RoundingAmount>
|
9
|
+
|
10
|
+
# </cac:TaxTotal>
|
11
|
+
# <cac:Item>
|
12
|
+
# <cbc:Name>dsd</cbc:Name>
|
13
|
+
# <cac:ClassifiedTaxCategory>
|
14
|
+
# <cbc:ID>S</cbc:ID>
|
15
|
+
# <cbc:Percent>15.00</cbc:Percent>
|
16
|
+
# <cac:TaxScheme>
|
17
|
+
# <cbc:ID>VAT</cbc:ID>
|
18
|
+
# </cac:TaxScheme>
|
19
|
+
# </cac:ClassifiedTaxCategory>
|
20
|
+
# </cac:Item>
|
21
|
+
# <cac:Price>
|
22
|
+
# <cbc:PriceAmount currencyID="SAR">22.00</cbc:PriceAmount>
|
23
|
+
# <cac:AllowanceCharge>
|
24
|
+
# <cbc:ChargeIndicator>false</cbc:ChargeIndicator>
|
25
|
+
# <cbc:AllowanceChargeReason>discount</cbc:AllowanceChargeReason>
|
26
|
+
# <cbc:Amount currencyID="SAR">2.00</cbc:Amount>
|
27
|
+
# </cac:AllowanceCharge>
|
28
|
+
# </cac:Price>
|
29
|
+
# </cac:InvoiceLine>
|
30
|
+
|
31
|
+
def initialize(
|
32
|
+
invoiced_quantity:, invoiced_quantity_unit_code:,
|
33
|
+
line_extension_amount:, tax_total:, item:,
|
34
|
+
price:, allowance_charge: nil, currency_id: "SAR"
|
35
|
+
)
|
36
|
+
super()
|
37
|
+
|
38
|
+
@invoiced_quantity = invoiced_quantity
|
39
|
+
@invoiced_quantity_unit_code = invoiced_quantity_unit_code
|
40
|
+
@line_extension_amount = line_extension_amount
|
41
|
+
@tax_total = tax_total
|
42
|
+
@item = item
|
43
|
+
@price = price
|
44
|
+
@allowance_charge = allowance_charge
|
45
|
+
@currency_id = currency_id
|
46
|
+
end
|
47
|
+
|
48
|
+
def name
|
49
|
+
"cac:InvoiceLine"
|
50
|
+
end
|
51
|
+
|
52
|
+
def elements
|
53
|
+
[
|
54
|
+
ZATCA::UBL::BaseComponent.new(name: "cbc:ID", value: index),
|
55
|
+
ZATCA::UBL::BaseComponent.new(name: "cbc:InvoicedQuantity", value: @invoiced_quantity, attributes: {unitCode: @invoiced_quantity_unit_code}),
|
56
|
+
ZATCA::UBL::BaseComponent.new(name: "cbc:LineExtensionAmount", value: @line_extension_amount, attributes: {currencyID: @currency_id}),
|
57
|
+
@tax_total,
|
58
|
+
@item,
|
59
|
+
@price,
|
60
|
+
@allowance_charge
|
61
|
+
]
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
class ZATCA::UBL::CommonAggregateComponents::Item < ZATCA::UBL::BaseComponent
|
2
|
+
attr_reader :scheme_id, :id
|
3
|
+
|
4
|
+
def initialize(name:, classified_tax_category: nil)
|
5
|
+
super()
|
6
|
+
|
7
|
+
@name = name
|
8
|
+
@classified_tax_category = classified_tax_category || ZATCA::UBL::CommonAggregateComponents::ClassifiedTaxCategory.new
|
9
|
+
end
|
10
|
+
|
11
|
+
def name
|
12
|
+
"cac:Item"
|
13
|
+
end
|
14
|
+
|
15
|
+
def elements
|
16
|
+
[
|
17
|
+
ZATCA::UBL::BaseComponent.new(name: "cbc:Name", value: @name),
|
18
|
+
@classified_tax_category
|
19
|
+
]
|
20
|
+
end
|
21
|
+
end
|