sunat_invoice 0.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 +7 -0
- data/.gitignore +4 -0
- data/.rubocop.yml +11 -0
- data/.rvmrc +1 -0
- data/Gemfile +4 -0
- data/Makefile +8 -0
- data/README.mkd +26 -0
- data/lib/sunat_invoice/catalogs.rb +177 -0
- data/lib/sunat_invoice/client.rb +36 -0
- data/lib/sunat_invoice/clients/consult_client.rb +11 -0
- data/lib/sunat_invoice/clients/invoice_client.rb +40 -0
- data/lib/sunat_invoice/clients/validate_client.rb +18 -0
- data/lib/sunat_invoice/configuration.rb +7 -0
- data/lib/sunat_invoice/customer.rb +18 -0
- data/lib/sunat_invoice/invoice.rb +171 -0
- data/lib/sunat_invoice/item.rb +79 -0
- data/lib/sunat_invoice/model.rb +11 -0
- data/lib/sunat_invoice/provider.rb +52 -0
- data/lib/sunat_invoice/signature.rb +87 -0
- data/lib/sunat_invoice/tax.rb +62 -0
- data/lib/sunat_invoice/tributer.rb +10 -0
- data/lib/sunat_invoice/utils.rb +31 -0
- data/lib/sunat_invoice.rb +21 -0
- data/sunat_invoice.gemspec +25 -0
- data/test/factories.rb +48 -0
- data/test/helper.rb +11 -0
- data/test/invoice_client_test.rb +41 -0
- data/test/invoice_test.rb +145 -0
- data/test/item_test.rb +110 -0
- data/test/support/signature_helper.rb +30 -0
- metadata +199 -0
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require_relative 'tributer'
|
3
|
+
require_relative 'utils'
|
4
|
+
|
5
|
+
module SunatInvoice
|
6
|
+
class Provider < Tributer
|
7
|
+
include Utils
|
8
|
+
|
9
|
+
attr_accessor :signature_id, :signature_location_id, :uri, :pk_file,
|
10
|
+
:cert_file
|
11
|
+
|
12
|
+
def address(xml)
|
13
|
+
xml['cbc'].ID @ubigeo
|
14
|
+
xml['cbc'].StreetName @street
|
15
|
+
xml['cbc'].CitySubdivisionName @zone
|
16
|
+
xml['cbc'].CityName @province
|
17
|
+
xml['cbc'].CountrySubentity @department
|
18
|
+
xml['cbc'].District @district
|
19
|
+
build_country(xml)
|
20
|
+
end
|
21
|
+
|
22
|
+
def build_country(xml)
|
23
|
+
xml['cac'].Country do
|
24
|
+
xml['cbc'].IdentificationCode country_code
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def build_name(xml)
|
29
|
+
xml['cac'].PartyName do
|
30
|
+
xml['cbc'].Name name
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def build_registration_name(xml)
|
35
|
+
xml['cac'].PartyLegalEntity do
|
36
|
+
xml['cbc'].RegistrationName name
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def info(xml)
|
41
|
+
xml['cac'].AccountingSupplierParty do
|
42
|
+
xml['cbc'].CustomerAssignedAccountID ruc
|
43
|
+
xml['cbc'].AdditionalAccountID document_type
|
44
|
+
xml['cac'].Party do
|
45
|
+
build_name(xml)
|
46
|
+
xml['cac'].PostalAddress { address(xml) }
|
47
|
+
build_registration_name(xml)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'model'
|
4
|
+
require_relative 'utils'
|
5
|
+
require 'xmldsig'
|
6
|
+
|
7
|
+
module SunatInvoice
|
8
|
+
class Signature < Model
|
9
|
+
include Utils
|
10
|
+
|
11
|
+
C14N_ALGORITHM = 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315'
|
12
|
+
DIGEST_ALGORITHM = 'http://www.w3.org/2000/09/xmldsig#sha1'
|
13
|
+
SIGNATURE_ALGORITHM = 'http://www.w3.org/2000/09/xmldsig#rsa-sha1'
|
14
|
+
TRANSFORMATION_ALGORITHM = 'http://www.w3.org/2000/09/xmldsig#enveloped-signature'
|
15
|
+
|
16
|
+
attr_accessor :provider
|
17
|
+
|
18
|
+
def signer_data(xml)
|
19
|
+
xml['cac'].Signature do
|
20
|
+
xml['cbc'].ID provider.signature_id
|
21
|
+
xml['cac'].SignatoryParty do
|
22
|
+
xml['cac'].PartyIdentification do
|
23
|
+
xml['cbc'].ID provider.ruc
|
24
|
+
end
|
25
|
+
xml['cac'].PartyName do
|
26
|
+
xml['cbc'].Name provider.name
|
27
|
+
end
|
28
|
+
end
|
29
|
+
xml['cac'].DigitalSignatureAttachment do
|
30
|
+
xml['cac'].ExternalReference do
|
31
|
+
xml['cbc'].URI "##{provider.signature_location_id}"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def sign(invoice_xml)
|
38
|
+
Xmldsig::SignedDocument.new(invoice_xml, id_attr: provider.signature_location_id).sign(private_key)
|
39
|
+
end
|
40
|
+
|
41
|
+
def signature_ext(xml)
|
42
|
+
ubl_ext(xml) do
|
43
|
+
xml['ds'].Signature(Id: provider.signature_location_id) do
|
44
|
+
signed_info xml
|
45
|
+
signature_value xml
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def signed_info(xml)
|
51
|
+
xml['ds'].SignedInfo do
|
52
|
+
xml['ds'].CanonicalizationMethod Algorithm: C14N_ALGORITHM
|
53
|
+
xml['ds'].SignatureMethod Algorithm: SIGNATURE_ALGORITHM
|
54
|
+
xml['ds'].Reference URI: '' do
|
55
|
+
xml['ds'].Transforms do
|
56
|
+
xml['ds'].Transform Algorithm: TRANSFORMATION_ALGORITHM
|
57
|
+
end
|
58
|
+
xml['ds'].DigestMethod Algorithm: DIGEST_ALGORITHM
|
59
|
+
xml['ds'].DigestValue
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def signature_value(xml)
|
65
|
+
xml['ds'].SignatureValue
|
66
|
+
xml['ds'].KeyInfo do
|
67
|
+
xml['ds'].X509Data do
|
68
|
+
xml['ds'].X509Certificate encoded_certificate
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
def encoded_certificate
|
76
|
+
Base64.encode64(certificate.to_der).gsub(/\n/, '')
|
77
|
+
end
|
78
|
+
|
79
|
+
def private_key
|
80
|
+
OpenSSL::PKey::RSA.new(File.read(provider.pk_file))
|
81
|
+
end
|
82
|
+
|
83
|
+
def certificate
|
84
|
+
OpenSSL::X509::Certificate.new(File.read(provider.cert_file))
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'catalogs'
|
4
|
+
require_relative 'utils'
|
5
|
+
|
6
|
+
module SunatInvoice
|
7
|
+
class Tax < Model
|
8
|
+
include Utils
|
9
|
+
|
10
|
+
TAXES = {
|
11
|
+
igv: { id: '1000', name: 'IGV', tax_type_code: 'VAT' },
|
12
|
+
isc: { id: '2000', name: 'ISC', tax_type_code: 'EXC' },
|
13
|
+
other: { id: '9999', name: 'OTROS', tax_type_code: 'OTH' }
|
14
|
+
}.freeze
|
15
|
+
|
16
|
+
attr_accessor :amount, :tax_type, :tax_exemption_reason, :tier_range
|
17
|
+
|
18
|
+
def initialize(*args)
|
19
|
+
super(*args)
|
20
|
+
defaults_for_type(tax_type)
|
21
|
+
end
|
22
|
+
|
23
|
+
def defaults_for_type(type)
|
24
|
+
case type
|
25
|
+
when :igv
|
26
|
+
@tax_exemption_reason ||= Catalogs::CATALOG_07.first
|
27
|
+
when :isc
|
28
|
+
@tier_range ||= Catalogs::CATALOG_08.first
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def xml(xml, currency)
|
33
|
+
xml['cac'].TaxTotal do
|
34
|
+
amount_xml(xml['cbc'], 'TaxAmount', amount, currency)
|
35
|
+
xml['cac'].TaxSubtotal do
|
36
|
+
amount_xml(xml['cbc'], 'TaxAmount', amount, currency)
|
37
|
+
tax_category(xml)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def tax_category(xml)
|
43
|
+
xml['cac'].TaxCategory do
|
44
|
+
xml['cbc'].TaxExemptionReasonCode(tax_exemption_reason) if tax_exemption_reason
|
45
|
+
xml['cbc'].TierRange(tier_range) if tier_range
|
46
|
+
tax_scheme(xml)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def tax_scheme(xml)
|
51
|
+
xml['cac'].TaxScheme do
|
52
|
+
xml['cbc'].ID tax_data(:id)
|
53
|
+
xml['cbc'].Name tax_data(:name)
|
54
|
+
xml['cbc'].TaxTypeCode tax_data(:tax_type_code)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def tax_data(attribute)
|
59
|
+
TAXES[tax_type][attribute]
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: false
|
2
|
+
|
3
|
+
module SunatInvoice
|
4
|
+
module Utils
|
5
|
+
@namespace_path = 'urn:oasis:names:specification:ubl:schema:xsd'
|
6
|
+
@sunat_namespace_path = 'urn:sunat:names:specification:ubl:peru:schema:xsd'
|
7
|
+
|
8
|
+
UBL_NAMESPACES = {
|
9
|
+
'xmlns' => "#{@namespace_path}:Invoice-2",
|
10
|
+
'xmlns:cac' => "#{@namespace_path}:CommonAggregateComponents-2",
|
11
|
+
'xmlns:cbc' => "#{@namespace_path}:CommonBasicComponents-2",
|
12
|
+
'xmlns:ccts' => 'urn:un:unece:uncefact:documentation:2',
|
13
|
+
'xmlns:ds' => 'http://www.w3.org/2000/09/xmldsig#',
|
14
|
+
'xmlns:ext' => "#{@namespace_path}:CommonExtensionComponents-2",
|
15
|
+
'xmlns:qdt' => "#{@namespace_path}:QualifiedDatatypes-2",
|
16
|
+
'xmlns:sac' => "#{@sunat_namespace_path}:SunatAggregateComponents-1",
|
17
|
+
'xmlns:udt' => 'urn:un:unece:uncefact:data:specification:UnqualifiedDataTypesSchemaModule:2',
|
18
|
+
'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance'
|
19
|
+
}.freeze
|
20
|
+
|
21
|
+
def ubl_ext(xml, &block)
|
22
|
+
xml['ext'].UBLExtension do
|
23
|
+
xml['ext'].ExtensionContent(&block)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def amount_xml(xml, tag, price, currency)
|
28
|
+
xml.send(tag, price, currencyID: currency)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'sunat_invoice/invoice'
|
4
|
+
require 'sunat_invoice/provider'
|
5
|
+
require 'sunat_invoice/customer'
|
6
|
+
require 'sunat_invoice/item'
|
7
|
+
require 'sunat_invoice/tax'
|
8
|
+
require 'sunat_invoice/configuration'
|
9
|
+
require 'sunat_invoice/client'
|
10
|
+
require 'sunat_invoice/clients/invoice_client'
|
11
|
+
|
12
|
+
module SunatInvoice
|
13
|
+
class << self
|
14
|
+
attr_accessor :configuration
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.configure
|
18
|
+
self.configuration ||= Configuration.new
|
19
|
+
yield(configuration)
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = 'sunat_invoice'
|
5
|
+
s.version = '0.0.1'
|
6
|
+
s.summary = 'SOAP client to use SUNAT Electronic Invoice API'
|
7
|
+
s.description = s.summary
|
8
|
+
s.authors = ['César Carruitero']
|
9
|
+
s.email = ['cesar@mozilla.pe']
|
10
|
+
s.homepage = 'https://github.com/ccarruitero/sunat_invoice'
|
11
|
+
s.license = 'MPL-2.0'
|
12
|
+
|
13
|
+
s.files = `git ls-files`.split("\n")
|
14
|
+
|
15
|
+
s.add_dependency 'savon', '~> 2.11.2'
|
16
|
+
s.add_dependency 'nokogiri', '~> 1.8'
|
17
|
+
s.add_dependency 'rubyzip', '~> 1.2.1'
|
18
|
+
s.add_dependency 'xmldsig', '~> 0.6.5'
|
19
|
+
|
20
|
+
s.add_development_dependency 'cutest', '~> 1.2'
|
21
|
+
s.add_development_dependency 'factory_bot', '~> 4.8'
|
22
|
+
s.add_development_dependency 'ffaker', '~> 2.7'
|
23
|
+
s.add_development_dependency 'pry', '~> 0.11'
|
24
|
+
s.add_development_dependency 'rubocop', '~> 0.51'
|
25
|
+
end
|
data/test/factories.rb
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: false
|
2
|
+
|
3
|
+
FactoryBot.define do
|
4
|
+
factory :tax, class: 'SunatInvoice::Tax' do
|
5
|
+
amount 20
|
6
|
+
tax_type :igv
|
7
|
+
end
|
8
|
+
|
9
|
+
factory :item, class: 'SunatInvoice::Item' do
|
10
|
+
quantity 10
|
11
|
+
unit_code 'NIU'
|
12
|
+
price 20
|
13
|
+
price_code '01'
|
14
|
+
description 'Grabadora Externo'
|
15
|
+
end
|
16
|
+
|
17
|
+
factory :invoice, class: 'SunatInvoice::Invoice' do
|
18
|
+
provider { build(:provider) }
|
19
|
+
customer { build(:customer) }
|
20
|
+
|
21
|
+
initialize_with { new(attributes) }
|
22
|
+
end
|
23
|
+
|
24
|
+
factory :provider, class: 'SunatInvoice::Provider' do
|
25
|
+
pk_file File.join(File.dirname(__FILE__), 'certs/pk_file')
|
26
|
+
cert_file File.join(File.dirname(__FILE__), 'certs/cert_file')
|
27
|
+
ruc FFaker::IdentificationMX.curp
|
28
|
+
name FFaker::Company.name
|
29
|
+
document_type 6
|
30
|
+
ubigeo '14'
|
31
|
+
street ''
|
32
|
+
zone ''
|
33
|
+
province ''
|
34
|
+
department ''
|
35
|
+
district ''
|
36
|
+
country_code ''
|
37
|
+
|
38
|
+
initialize_with { new(attributes) }
|
39
|
+
end
|
40
|
+
|
41
|
+
factory :customer, class: 'SunatInvoice::Customer' do
|
42
|
+
ruc FFaker::IdentificationMX.curp
|
43
|
+
name FFaker::Company.name
|
44
|
+
document_type 6
|
45
|
+
|
46
|
+
initialize_with { new(attributes) }
|
47
|
+
end
|
48
|
+
end
|
data/test/helper.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'cutest'
|
3
|
+
require 'pry'
|
4
|
+
require 'ffaker'
|
5
|
+
require 'factory_bot'
|
6
|
+
require_relative '../lib/sunat_invoice'
|
7
|
+
require_relative 'factories'
|
8
|
+
|
9
|
+
Dir[File.join(File.dirname(__FILE__), 'support/**/*.rb')].each { |f| require f }
|
10
|
+
|
11
|
+
SignatureHelper.generate_keys
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'helper'
|
4
|
+
|
5
|
+
scope 'SunatInvoice::InvoiceClient' do
|
6
|
+
setup do
|
7
|
+
provider = FactoryBot.build(:provider,
|
8
|
+
signature_id: 'signatureST',
|
9
|
+
signature_location_id: 'signQWI3',
|
10
|
+
ruc: '20100454523',
|
11
|
+
name: 'SOPORTE TECNOLOGICO EIRL')
|
12
|
+
|
13
|
+
customer = FactoryBot.build(:customer,
|
14
|
+
ruc: '20293028401',
|
15
|
+
name: 'SOME BUSINESS')
|
16
|
+
SunatInvoice.configure do |c|
|
17
|
+
c.account_ruc = provider.ruc
|
18
|
+
c.account_user = 'MODDATOS'
|
19
|
+
c.account_password = 'moddatos'
|
20
|
+
c.provider = provider
|
21
|
+
end
|
22
|
+
@client = SunatInvoice::InvoiceClient.new
|
23
|
+
|
24
|
+
tax = SunatInvoice::Tax.new(amount: 3.6, tax_type: :igv)
|
25
|
+
item = FactoryBot.build(:item, taxes: [tax])
|
26
|
+
@invoice = SunatInvoice::Invoice.new(provider: provider,
|
27
|
+
customer: customer,
|
28
|
+
document_number: 'FY02-234')
|
29
|
+
@invoice.items << item
|
30
|
+
end
|
31
|
+
|
32
|
+
test 'should use dev service when not define environment' do
|
33
|
+
assert_equal @client.wsdl, @client.dev_server
|
34
|
+
end
|
35
|
+
|
36
|
+
|
37
|
+
test '#send_invoice success' do
|
38
|
+
response = @client.send_invoice(@invoice.xml, @invoice.document_name)
|
39
|
+
assert_equal response.http.code, 200
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,145 @@
|
|
1
|
+
# frozen_string_literal: false
|
2
|
+
require_relative 'helper'
|
3
|
+
include SunatInvoice
|
4
|
+
|
5
|
+
setup do
|
6
|
+
@provider = FactoryBot.build(:provider)
|
7
|
+
@invoice = FactoryBot.build(:invoice, provider: @provider)
|
8
|
+
tax = SunatInvoice::Tax.new(amount: 3.6, tax_type: :igv)
|
9
|
+
item_attr = { quantity: 10, price: 20, price_code: '01', taxes: [tax] }
|
10
|
+
@invoice.items << SunatInvoice::Item.new(item_attr)
|
11
|
+
@parsed_xml = Nokogiri::XML(@invoice.xml, &:noblanks)
|
12
|
+
end
|
13
|
+
|
14
|
+
test 'is not broken' do
|
15
|
+
assert !@invoice.nil?
|
16
|
+
end
|
17
|
+
|
18
|
+
test 'has namespaces' do
|
19
|
+
cbc = 'urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2'
|
20
|
+
namespaces = @parsed_xml.css('Invoice').first.namespaces
|
21
|
+
assert_equal namespaces['xmlns:cbc'], cbc
|
22
|
+
end
|
23
|
+
|
24
|
+
test 'xml start with invoice tag' do
|
25
|
+
assert_equal @parsed_xml.root.name, 'Invoice'
|
26
|
+
end
|
27
|
+
|
28
|
+
test 'has a date' do
|
29
|
+
date = @parsed_xml.xpath('//cbc:IssueDate')
|
30
|
+
assert_equal date.first.content, DateTime.now.strftime('%Y-%m-%d')
|
31
|
+
end
|
32
|
+
|
33
|
+
test 'has a signature' do
|
34
|
+
# /ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/ds:Signature
|
35
|
+
signature = @parsed_xml.xpath('//ext:UBLExtensions/ext:UBLExtension')
|
36
|
+
assert_equal signature.xpath('//ext:ExtensionContent/ds:Signature').count, 1
|
37
|
+
|
38
|
+
# /cac:Signature
|
39
|
+
signature = @parsed_xml.xpath('//cac:Signature')
|
40
|
+
assert_equal signature.count, 1
|
41
|
+
end
|
42
|
+
|
43
|
+
test 'has a registration name' do
|
44
|
+
provider = @parsed_xml.xpath('//cac:AccountingSupplierParty/cac:Party')
|
45
|
+
name = provider.xpath('//cac:PartyLegalEntity/cbc:RegistrationName')
|
46
|
+
assert_equal name.first.content, @provider.name
|
47
|
+
end
|
48
|
+
|
49
|
+
test 'has a name' do
|
50
|
+
provider = @parsed_xml.xpath('//cac:AccountingSupplierParty/cac:Party')
|
51
|
+
name = provider.xpath('//cac:PartyName/cbc:Name')
|
52
|
+
assert_equal name.first.content, @provider.name
|
53
|
+
end
|
54
|
+
|
55
|
+
test 'has an address' do
|
56
|
+
provider = @parsed_xml.xpath('//cac:AccountingSupplierParty/cac:Party')
|
57
|
+
ubigeo = provider.xpath('//cac:PostalAddress/cbc:ID')
|
58
|
+
assert_equal ubigeo.first.content, @provider.ubigeo
|
59
|
+
|
60
|
+
street = provider.xpath('//cac:PostalAddress/cbc:StreetName')
|
61
|
+
assert_equal street.first.content, @provider.street
|
62
|
+
|
63
|
+
urbanizacion = provider.xpath('//cac:PostalAddress/cbc:CitySubdivisionName')
|
64
|
+
assert_equal urbanizacion.first.content, @provider.zone
|
65
|
+
|
66
|
+
provincia = provider.xpath('//cac:PostalAddress/cbc:CityName')
|
67
|
+
assert_equal provincia.first.content, @provider.province
|
68
|
+
|
69
|
+
# Departamento
|
70
|
+
departamento = provider.xpath('//cac:PostalAddress/cbc:CountrySubentity')
|
71
|
+
assert_equal departamento.first.content, @provider.department
|
72
|
+
|
73
|
+
# Distrito
|
74
|
+
distrito = provider.xpath('//cac:PostalAddress/cbc:District')
|
75
|
+
assert_equal distrito.first.content, @provider.district
|
76
|
+
|
77
|
+
country_code_path = '//cac:PostalAddress/cac:Country/cbc:IdentificationCode'
|
78
|
+
country_code = provider.xpath(country_code_path)
|
79
|
+
assert_equal country_code.first.content, @provider.country_code.to_s
|
80
|
+
end
|
81
|
+
|
82
|
+
test 'has a ruc' do
|
83
|
+
provider = @parsed_xml.xpath('//cac:AccountingSupplierParty')
|
84
|
+
ruc = provider.xpath('//cbc:CustomerAssignedAccountID')
|
85
|
+
assert_equal ruc.first.content, @provider.ruc
|
86
|
+
|
87
|
+
document_type = provider.xpath('//cbc:AdditionalAccountID')
|
88
|
+
assert_equal document_type.first.content, @provider.document_type.to_s
|
89
|
+
end
|
90
|
+
|
91
|
+
test 'has an invoice type' do
|
92
|
+
invoice_type = @parsed_xml.xpath('//cbc:InvoiceTypeCode')
|
93
|
+
assert_equal invoice_type.first.content, @invoice.document_type
|
94
|
+
end
|
95
|
+
|
96
|
+
test 'has a correlative' do
|
97
|
+
tags = @parsed_xml.xpath('//cbc:ID')
|
98
|
+
correlative = tags.select { |tax| tax.parent.name == 'Invoice' }.first
|
99
|
+
assert_equal correlative.content, @invoice.document_number
|
100
|
+
end
|
101
|
+
|
102
|
+
test 'has a customer' do
|
103
|
+
customer = @parsed_xml.xpath('//cac:AccountingCustomerParty')
|
104
|
+
assert_equal customer.count, 1
|
105
|
+
end
|
106
|
+
|
107
|
+
test 'has at least one item' do
|
108
|
+
item = @parsed_xml.xpath('//cac:InvoiceLine')
|
109
|
+
assert item.count.positive?
|
110
|
+
end
|
111
|
+
|
112
|
+
test 'has total by kind of sale' do
|
113
|
+
tag = '//ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sac:AdditionalInformation'
|
114
|
+
additional_info = @parsed_xml.xpath(tag)
|
115
|
+
assert additional_info.count.positive?
|
116
|
+
assert_equal additional_info.first.children.count, 1
|
117
|
+
amount_tag = '//sac:AdditionalMonetaryTotal/cbc:PayableAmount'
|
118
|
+
assert_equal additional_info.xpath(amount_tag).first.content.to_f, 200.to_f
|
119
|
+
end
|
120
|
+
|
121
|
+
test 'has total tag' do
|
122
|
+
total = @parsed_xml.xpath('//cac:LegalMonetaryTotal')
|
123
|
+
assert total.count.positive?
|
124
|
+
end
|
125
|
+
|
126
|
+
test '#calculate_tax_totals' do
|
127
|
+
tax = FactoryBot.build(:tax, amount: 15)
|
128
|
+
invoice = FactoryBot.build(:invoice)
|
129
|
+
2.times do
|
130
|
+
invoice.items << FactoryBot.build(:item, taxes: [tax])
|
131
|
+
end
|
132
|
+
invoice.calculate_tax_totals
|
133
|
+
tax_totals = invoice.instance_variable_get('@tax_totals')
|
134
|
+
assert_equal tax_totals.count, 1
|
135
|
+
assert_equal tax_totals[:igv], 300
|
136
|
+
end
|
137
|
+
|
138
|
+
test '#get_total_igv_code' do
|
139
|
+
invoice = SunatInvoice::Invoice.new
|
140
|
+
assert_equal invoice.get_total_igv_code('11'), '1004'
|
141
|
+
assert_equal invoice.get_total_igv_code('15'), '1004'
|
142
|
+
assert_equal invoice.get_total_igv_code('20'), '1003'
|
143
|
+
assert_equal invoice.get_total_igv_code('32'), '1002'
|
144
|
+
assert invoice.get_total_igv_code('42').nil?
|
145
|
+
end
|
data/test/item_test.rb
ADDED
@@ -0,0 +1,110 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require_relative 'helper'
|
3
|
+
include SunatInvoice
|
4
|
+
|
5
|
+
setup do
|
6
|
+
@item = SunatInvoice::Item.new(quantity: 200, price: 69, price_code: '01',
|
7
|
+
description: 'item description',
|
8
|
+
unit_code: 'NIU')
|
9
|
+
@item.taxes << SunatInvoice::Tax.new(amount: 12.42, tax_type: :igv)
|
10
|
+
invoice_setup
|
11
|
+
end
|
12
|
+
|
13
|
+
def invoice_setup
|
14
|
+
# add item to invoice in order to setup namespaces
|
15
|
+
invoice = FactoryBot.build(:invoice)
|
16
|
+
invoice.items << @item
|
17
|
+
invoice_xml = Nokogiri::XML(invoice.xml, &:noblanks)
|
18
|
+
@item_xml = invoice_xml.xpath('//cac:InvoiceLine')
|
19
|
+
end
|
20
|
+
|
21
|
+
test 'not broken' do
|
22
|
+
assert !@item.nil?
|
23
|
+
end
|
24
|
+
|
25
|
+
# xml
|
26
|
+
test 'has unit code and quantity' do
|
27
|
+
quantity = @item_xml.xpath('//cbc:InvoicedQuantity')
|
28
|
+
assert quantity.count.positive?
|
29
|
+
assert_equal @item.quantity.to_s, quantity.first.content
|
30
|
+
|
31
|
+
# /Invoice/cac:InvoiceLine/cbc:InvoicedQuantity/@unitCode
|
32
|
+
attributes = quantity.first.attributes
|
33
|
+
assert_equal attributes.keys, ['unitCode']
|
34
|
+
assert_equal attributes['unitCode'].value, @item.unit_code
|
35
|
+
end
|
36
|
+
|
37
|
+
test 'has a description' do
|
38
|
+
description = @item_xml.xpath('//cbc:Description')
|
39
|
+
assert_equal @item.description, description.first.content
|
40
|
+
end
|
41
|
+
|
42
|
+
test 'has unit prices' do
|
43
|
+
price = @item_xml.xpath('//cac:Price/cbc:PriceAmount')
|
44
|
+
assert_equal @item.price.to_s, price.first.content
|
45
|
+
# /Invoice/cac:InvoiceLine/cac:Price/cbc:PriceAmount/@currencyID
|
46
|
+
assert_equal price.first.attributes.keys, ['currencyID']
|
47
|
+
assert_equal price.first.attributes['currencyID'].value, 'PEN'
|
48
|
+
|
49
|
+
ref_pricing = @item_xml.xpath('//cac:PricingReference')
|
50
|
+
alt_condition_price = ref_pricing.xpath('//cac:AlternativeConditionPrice')
|
51
|
+
assert_equal 1, alt_condition_price.count
|
52
|
+
|
53
|
+
tag = '//cac:AlternativeConditionPrice/cbc:PriceAmount'
|
54
|
+
alt_price = ref_pricing.xpath(tag)
|
55
|
+
assert_equal @item.sale_price.to_s, alt_price.first.content
|
56
|
+
# //cac:AlternativeConditionPrice/cbc:PriceAmount/@currencyID
|
57
|
+
|
58
|
+
price_type = alt_condition_price.xpath('//cbc:PriceTypeCode')
|
59
|
+
assert_equal @item.price_code, price_type.first.content
|
60
|
+
end
|
61
|
+
|
62
|
+
# taxes
|
63
|
+
test 'has taxes' do
|
64
|
+
taxes = @item_xml.xpath('//cac:InvoiceLine/cac:TaxTotal')
|
65
|
+
assert_equal 1, taxes.count
|
66
|
+
end
|
67
|
+
|
68
|
+
test 'has amount in correct tag' do
|
69
|
+
tag_path = '//cac:InvoiceLine/cac:TaxTotal/cbc:TaxAmount'
|
70
|
+
amount = @item_xml.xpath(tag_path)
|
71
|
+
assert_equal amount.count, 1
|
72
|
+
assert_equal amount.first.content, '12.42'
|
73
|
+
# cac:TaxTotal/cbc:TaxAmount/@currencyID
|
74
|
+
end
|
75
|
+
|
76
|
+
test 'has correct values in TaxScheme' do
|
77
|
+
tag_path = '//cac:TaxTotal/cac:TaxSubtotal/cac:TaxCategory/cac:TaxScheme'
|
78
|
+
id = @item_xml.xpath("#{tag_path}/cbc:ID")
|
79
|
+
assert_equal id.first.content, '1000'
|
80
|
+
|
81
|
+
name = @item_xml.xpath("#{tag_path}/cbc:Name")
|
82
|
+
assert_equal name.first.content, 'IGV'
|
83
|
+
|
84
|
+
tax_code = @item_xml.xpath("#{tag_path}/cbc:TaxTypeCode")
|
85
|
+
assert_equal tax_code.first.content, 'VAT'
|
86
|
+
end
|
87
|
+
|
88
|
+
scope '#sale_taxes' do
|
89
|
+
setup do
|
90
|
+
tax = SunatInvoice::Tax.new(amount: 10.8, tax_type: :igv)
|
91
|
+
@item = SunatInvoice::Item.new(quantity: 2, price: 60, taxes: [tax])
|
92
|
+
end
|
93
|
+
|
94
|
+
test 'return a hash' do
|
95
|
+
assert_equal @item.sale_taxes.class, Hash
|
96
|
+
end
|
97
|
+
|
98
|
+
test 'has tax_type and total tax for item' do
|
99
|
+
assert_equal @item.sale_taxes.count, 1
|
100
|
+
assert_equal @item.sale_taxes.keys, [:igv]
|
101
|
+
assert_equal @item.sale_taxes.values, [21.6]
|
102
|
+
end
|
103
|
+
|
104
|
+
test 'has sum by tax_type' do
|
105
|
+
@item.taxes << SunatInvoice::Tax.new(amount: 5, tax_type: :isc)
|
106
|
+
assert_equal @item.sale_taxes.count, 2
|
107
|
+
assert_equal @item.sale_taxes.keys, [:igv, :isc]
|
108
|
+
assert_equal @item.sale_taxes.values, [21.6, 10]
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module SignatureHelper
|
2
|
+
def self.generate_keys
|
3
|
+
pk = OpenSSL::PKey::RSA.new 2048
|
4
|
+
name = OpenSSL::X509::Name.parse('CN=example.com/C=EE')
|
5
|
+
cert = OpenSSL::X509::Certificate.new
|
6
|
+
cert.version = 2
|
7
|
+
cert.serial = 0
|
8
|
+
cert.not_before = Time.now
|
9
|
+
cert.not_after = cert.not_before + 1 * 365 * 24 * 60 * 60
|
10
|
+
cert.public_key = pk.public_key
|
11
|
+
cert.subject = name
|
12
|
+
cert.issuer = name
|
13
|
+
cert.sign pk, OpenSSL::Digest::SHA1.new
|
14
|
+
|
15
|
+
unless exists?('pk_file')
|
16
|
+
cert_dir = "#{File.dirname(__FILE__)}/../certs"
|
17
|
+
Dir.mkdir(cert_dir) unless Dir.exist?(cert_dir)
|
18
|
+
File.open(file('pk_file'), 'w') { |f| f.puts pk.to_pem }
|
19
|
+
File.open(file('cert_file'), 'w') { |f| f.puts cert.to_pem }
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.file(name)
|
24
|
+
File.join(File.dirname(__FILE__), "../certs/#{name}")
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.exists?(name)
|
28
|
+
File.exists?(file(name))
|
29
|
+
end
|
30
|
+
end
|