sunat_invoice 0.0.3 → 0.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: c15a897331d06af16937c9bf1020984592bcd0f3
4
- data.tar.gz: 51cc6c5c299e0d2209a32ccc51be2158b7a4e780
2
+ SHA256:
3
+ metadata.gz: 40396cec4aee7cc4907b9010728d8069d4b53faee0eec724379d652f08c2e0d9
4
+ data.tar.gz: dd6036855facb8d92e78a37e5654569cd7c53e58d8628960d976629e9b9b8049
5
5
  SHA512:
6
- metadata.gz: a5866087a7d0c40c4c88ad32ee94812d7e3dbd75892968ca39df00aa501fcdb9167a0ba06b60c3c38d0095b51c631d6de51055e01dfee4f4db029c473bac4b91
7
- data.tar.gz: 20781a7b8110190a887a7c9740fbfebb4a8fd55893953a07fc12c8da05f549d1fae2630b301bc98329db94dc39c2ca23f2ce017541409774013d034c4626062e
6
+ metadata.gz: 9b937b09684482e2bd3e3b8050891ebbf7bece82648e214afadffbb2a1605a4e01ee0cd243b298fe9e10a17004c8a09c745de8f534803f8fb9d54e5451d63327
7
+ data.tar.gz: 0b6a13a021269f0e7a1147fc5c34f761fb495ad87fc43b5d316fb0004bda9fd13391521492556f540c2ae452fdee261746b1af025313a9fa51c1e9df5069fb1f
data/.gitignore CHANGED
@@ -2,3 +2,4 @@ Gemfile.lock
2
2
  *.swp
3
3
  *.gem
4
4
  test/certs
5
+ doc
data/.rvmrc CHANGED
@@ -1 +1 @@
1
- rvm --create use 2.4.1@sunat_invoice
1
+ rvm --create use 2.6.5@sunat_invoice
data/README.mkd CHANGED
@@ -1,2 +1,44 @@
1
1
  # Sunat Invoice
2
2
  Ruby gem to use [SUNAT Electronic Billing](http://cpe.sunat.gob.pe/factura-desde-los-sistemas-del-contribuyente) from your app
3
+
4
+ Allow to generate Electronic Billing Documents and send to [SUNAT](http://www.sunat.gob.pe).
5
+
6
+ ## Usage
7
+
8
+ ### generate document
9
+
10
+ First you have to initialize a kind of document that you want, with your
11
+ desired attributes.
12
+
13
+ ```
14
+ document = SunatInvoice::Invoice.new(invoice_attributes)
15
+ ```
16
+
17
+ ```
18
+ Note: Currently we support this kind of documents:
19
+
20
+ - Invoice for document type `01` and `03`
21
+ - CreditNote for document type `07`
22
+ - DebitNote for document type `08`
23
+ ```
24
+
25
+ ### send document
26
+
27
+ After that, you have to send the document to SUNAT in this way:
28
+
29
+ for document type `01` and related documents
30
+
31
+ ```
32
+ client = SunatInvoice::InvoiceClient.new
33
+ client.dispatch(document)
34
+ ```
35
+
36
+ for document type `03` and related documents, you should send a daily summary
37
+ instead each document
38
+
39
+ ```
40
+ daily_document = SunatInvoice::DailySummary.new
41
+ client.dispatch(daily_document)
42
+ ```
43
+
44
+ ### parse response
@@ -9,7 +9,7 @@ module SunatInvoice
9
9
  '01', # FACTURA
10
10
  '03', # BOLETA DE VENTA
11
11
  '07', # NOTA DE CREDITO
12
- '08', # NOTA DE DEBITO 383
12
+ '08', # NOTA DE DEBITO
13
13
  '09', # GUIA DE REMISIÓN REMITENTE
14
14
  '12', # TICKET DE MAQUINA REGISTRADORA
15
15
  '13', # DOCUMENTO EMITIDO POR BANCOS, INSTITUCIONES FINANCIERAS
@@ -19,7 +19,6 @@ module SunatInvoice
19
19
  @document_number = opts[:document_number] || 'F001-1'
20
20
  @currency = opts[:currency] || 'PEN'
21
21
  @lines ||= []
22
- @signature = SunatInvoice::Signature.new(provider: @provider)
23
22
  end
24
23
 
25
24
  def parties_default(opts)
@@ -4,7 +4,8 @@ require_relative 'line'
4
4
 
5
5
  module SunatInvoice
6
6
  class Item < Line
7
- attr_accessor :quantity, :description, :price, :price_code, :unit_code
7
+ attr_accessor :quantity, :description, :price, :price_code, :unit_code,
8
+ :price_included_tax
8
9
 
9
10
  def initialize(*args)
10
11
  # * quantity - quantity of item
@@ -15,6 +16,7 @@ module SunatInvoice
15
16
  # UN/ECE rec 20- Unit Of Measure
16
17
  # http://www.unece.org/fileadmin/DAM/cefact/recommendations/rec20/rec20_rev3_Annex2e.pdf
17
18
  # * taxes - An array of SunatInvoice::Tax
19
+ # * price_included_tax - price with taxes
18
20
  super(*args)
19
21
  @taxes ||= []
20
22
  end
@@ -38,8 +40,13 @@ module SunatInvoice
38
40
  end
39
41
  end
40
42
 
43
+ def set_price
44
+ @price ||= (@price_included_tax - sum_taxes).round(2) if @price_included_tax
45
+ end
46
+
41
47
  def bi_value
42
48
  # bi of sale = price without taxes * quantity
49
+ set_price
43
50
  (@price.to_f * @quantity.to_f).round(2)
44
51
  end
45
52
 
@@ -55,7 +62,7 @@ module SunatInvoice
55
62
 
56
63
  def sale_price
57
64
  # unit price with tax
58
- (@price.to_f + sum_taxes).round(2)
65
+ @price_included_tax || (@price.to_f + sum_taxes).round(2)
59
66
  end
60
67
 
61
68
  private
@@ -7,7 +7,17 @@ module SunatInvoice
7
7
  class Provider < Tributer
8
8
  include Utils
9
9
 
10
- attr_accessor :signature_id, :signature_location_id, :pk_file, :cert_file
10
+ # signature identification
11
+ attr_accessor :signature_id
12
+
13
+ # Id for signature element in xml document
14
+ attr_accessor :signature_location_id
15
+
16
+ # public key file path
17
+ attr_accessor :pk_file
18
+
19
+ # certificate file path
20
+ attr_accessor :cert_file
11
21
 
12
22
  def info(xml, with_address = true)
13
23
  xml['cac'].AccountingSupplierParty do
@@ -21,7 +31,7 @@ module SunatInvoice
21
31
 
22
32
  def build_party_xml(xml, with_address)
23
33
  xml['cac'].Party do
24
- build_name(xml)
34
+ build_name(xml) if commercial_name
25
35
  xml['cac'].PostalAddress { address(xml) } if with_address
26
36
  build_registration_name(xml)
27
37
  end
@@ -45,7 +55,7 @@ module SunatInvoice
45
55
 
46
56
  def build_name(xml)
47
57
  xml['cac'].PartyName do
48
- xml['cbc'].Name name
58
+ xml['cbc'].Name commercial_name
49
59
  end
50
60
  end
51
61
 
@@ -13,7 +13,8 @@ module SunatInvoice
13
13
  }.freeze
14
14
 
15
15
  ALLOWED_PARSERS = %w[invoice summary status].freeze
16
- VALID_PROCESS = %w[0 99].freeze
16
+ VALID_PROCESS = %w[0].freeze
17
+ IN_PROCESS = %w[99].freeze
17
18
 
18
19
  def initialize(body, parser_type)
19
20
  # body: SOAP body as a Hash. Typically Savon Response body.
@@ -47,6 +48,8 @@ module SunatInvoice
47
48
  if VALID_PROCESS.include?(status_code)
48
49
  encrypted_zip = status_hash[:content]
49
50
  decrypt_zip(encrypted_zip)
51
+ elsif IN_PROCESS.include?(status_code)
52
+ @message = 'Your ticket is still in process'
50
53
  else
51
54
  @message = status_hash[:content]
52
55
  end
@@ -3,6 +3,7 @@
3
3
  require_relative 'model'
4
4
  require_relative 'utils'
5
5
  require 'xmldsig'
6
+ require 'open-uri'
6
7
 
7
8
  module SunatInvoice
8
9
  class Signature < Model
@@ -90,12 +91,16 @@ module SunatInvoice
90
91
  Base64.encode64(certificate.to_der).gsub(/\n/, '')
91
92
  end
92
93
 
94
+ def file_content(file)
95
+ open(file) { |f| f.read }
96
+ end
97
+
93
98
  def private_key
94
- OpenSSL::PKey::RSA.new(File.read(provider.pk_file))
99
+ OpenSSL::PKey::RSA.new(file_content(provider.pk_file))
95
100
  end
96
101
 
97
102
  def certificate
98
- OpenSSL::X509::Certificate.new(File.read(provider.cert_file))
103
+ OpenSSL::X509::Certificate.new(file_content(provider.cert_file))
99
104
  end
100
105
  end
101
106
  end
@@ -1,16 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SignatureHelper
4
- def self.generate_keys
5
- return if exists?('pk_file')
4
+ def self.generate_keys(keys_dir = "#{SunatInvoice.root}/test/certs")
5
+ return if File.exist?("#{keys_dir}/pk_file")
6
6
 
7
7
  pk = OpenSSL::PKey::RSA.new 2048
8
8
  cert = generate_certificate(pk)
9
9
 
10
- cert_dir = "#{File.dirname(__FILE__)}/../certs"
11
- Dir.mkdir(cert_dir) unless Dir.exist?(cert_dir)
12
- File.open(file('pk_file'), 'w') { |f| f.puts pk.to_pem }
13
- File.open(file('cert_file'), 'w') { |f| f.puts cert.to_pem }
10
+ Dir.mkdir(keys_dir) unless Dir.exist?(keys_dir)
11
+ File.open("#{keys_dir}/pk_file", 'w') { |f| f.puts pk.to_pem }
12
+ File.open("#{keys_dir}/cert_file", 'w') { |f| f.puts cert.to_pem }
14
13
  end
15
14
 
16
15
  def self.generate_certificate(pk)
@@ -26,12 +25,4 @@ module SignatureHelper
26
25
  cert.sign pk, OpenSSL::Digest::SHA1.new
27
26
  cert
28
27
  end
29
-
30
- def self.file(name)
31
- File.join(File.dirname(__FILE__), "../certs/#{name}")
32
- end
33
-
34
- def self.exists?(name)
35
- File.exist?(file(name))
36
- end
37
28
  end
@@ -12,9 +12,8 @@ module SunatInvoice
12
12
 
13
13
  def calculate_total
14
14
  # calculate invoice total
15
- @total = 0
16
- @total += @taxes_totals.values.sum
17
- @total += @sale_totals.reject { |k, _v| k == '1004' }.values.sum
15
+ sales_sum = @sale_totals.reject { |k, _v| k == '1004' }.values.sum
16
+ @total = (@taxes_totals.values.sum + sales_sum).round(2)
18
17
  @total -= discount if discount
19
18
  end
20
19
 
@@ -27,7 +26,7 @@ module SunatInvoice
27
26
  total_code = get_total_code(item.taxes.first)
28
27
  if total_code
29
28
  @sale_totals[total_code] = 0 unless @sale_totals[total_code]
30
- @sale_totals[total_code] += item.bi_value
29
+ @sale_totals[total_code] = (@sale_totals[total_code] + item.bi_value).round(2)
31
30
  end
32
31
  end
33
32
  end
@@ -38,7 +37,8 @@ module SunatInvoice
38
37
  taxes = lines&.map(&:sale_taxes)&.flatten
39
38
  taxes&.each do |tax|
40
39
  @taxes_totals[tax.keys.first] ||= 0
41
- @taxes_totals[tax.keys.first] += tax.values.sum
40
+ new_sum = (@taxes_totals[tax.keys.first] + tax.values.sum).round(2)
41
+ @taxes_totals[tax.keys.first] = new_sum
42
42
  end
43
43
  end
44
44
 
@@ -9,8 +9,22 @@ module SunatInvoice
9
9
 
10
10
  attr_accessor :customer, :document_number, :document_type, :discount
11
11
 
12
+ # total for document (sale totals + taxes - discounts)
13
+ attr_reader :total
14
+
15
+ # a hash with taxes totals by tax type
16
+ attr_reader :taxes_totals
17
+
18
+ # a hash with totals without taxes for any kind of sale
19
+ attr_reader :sale_totals
20
+
12
21
  INVOICE_TYPES = %w[01 03].freeze
13
22
 
23
+ def initialize(*args)
24
+ super(*args)
25
+ @signature ||= SunatInvoice::Signature.new(provider: @provider)
26
+ end
27
+
14
28
  def operation
15
29
  :send_bill
16
30
  end
@@ -5,6 +5,7 @@ require_relative 'model'
5
5
  module SunatInvoice
6
6
  class Tributer < Model
7
7
  attr_accessor :ruc, :name, :document_type, :ubigeo, :street, :zone,
8
- :province, :department, :district, :country_code
8
+ :province, :department, :district, :country_code,
9
+ :commercial_name
9
10
  end
10
11
  end
data/lib/sunat_invoice.rb CHANGED
@@ -26,6 +26,10 @@ require 'sunat_invoice/clients/consult_client'
26
26
  module SunatInvoice
27
27
  class << self
28
28
  attr_accessor :configuration
29
+
30
+ def root
31
+ File.dirname(__dir__)
32
+ end
29
33
  end
30
34
 
31
35
  def self.configure
@@ -2,7 +2,7 @@
2
2
 
3
3
  Gem::Specification.new do |s|
4
4
  s.name = 'sunat_invoice'
5
- s.version = '0.0.3'
5
+ s.version = '0.1.1'
6
6
  s.summary = 'Ruby gem to use SUNAT Electronic Billing from your app'
7
7
  s.description = 'Generate and send Electronic Invoices to SUNAT'
8
8
  s.authors = ['César Carruitero']
data/test/factories.rb CHANGED
@@ -2,16 +2,16 @@
2
2
 
3
3
  FactoryBot.define do
4
4
  factory :tax, class: 'SunatInvoice::Tax' do
5
- amount 20
6
- tax_type :igv
5
+ amount { 20 }
6
+ tax_type { :igv }
7
7
  end
8
8
 
9
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'
10
+ quantity { 10 }
11
+ unit_code { 'NIU' }
12
+ price { 20 }
13
+ price_code { '01' }
14
+ description { 'Grabadora Externo' }
15
15
  end
16
16
 
17
17
  factory :invoice, class: 'SunatInvoice::Invoice' do
@@ -22,46 +22,46 @@ FactoryBot.define do
22
22
  end
23
23
 
24
24
  factory :credit_note_line, class: 'SunatInvoice::CreditNoteLine' do
25
- quantity 10
26
- unit_code 'NIU'
27
- price 20
28
- price_code '01'
29
- description 'Grabadora Externo'
25
+ quantity { 10 }
26
+ unit_code { 'NIU' }
27
+ price { 20 }
28
+ price_code { '01' }
29
+ description { 'Grabadora Externo' }
30
30
  taxes { [build(:tax, amount: 3.6)] }
31
31
  end
32
32
 
33
33
  factory :credit_note, class: 'SunatInvoice::CreditNote' do
34
34
  provider { build(:provider) }
35
35
  customer { build(:customer) }
36
- document_number 'F001-211'
37
- currency 'PEN'
38
- ref_document_number 'F001-342'
39
- ref_document_type '01'
40
- response_code '01'
41
- document_type '07'
42
- description 'Some awesome product'
36
+ document_number { 'F001-211' }
37
+ currency { 'PEN' }
38
+ ref_document_number { 'F001-342' }
39
+ ref_document_type { '01' }
40
+ response_code { '01' }
41
+ document_type { '07' }
42
+ description { 'Some awesome product' }
43
43
  lines { [build(:credit_note_line)] }
44
44
  end
45
45
 
46
46
  factory :debit_note_line, class: 'SunatInvoice::DebitNoteLine' do
47
- quantity 10
48
- unit_code 'NIU'
49
- price 20
50
- price_code '01'
51
- description 'Ajuste de precio'
47
+ quantity { 10 }
48
+ unit_code { 'NIU' }
49
+ price { 20 }
50
+ price_code { '01' }
51
+ description { 'Ajuste de precio' }
52
52
  taxes { [build(:tax, amount: 3.6)] }
53
53
  end
54
54
 
55
55
  factory :debit_note, class: 'SunatInvoice::DebitNote' do
56
56
  provider { build(:provider) }
57
57
  customer { build(:customer) }
58
- document_number 'F001-211'
59
- currency 'PEN'
60
- ref_document_number 'F001-342'
61
- ref_document_type '01'
62
- response_code '01'
63
- document_type '08'
64
- description 'Unidades defectuosas'
58
+ document_number { 'F001-211' }
59
+ currency { 'PEN' }
60
+ ref_document_number { 'F001-342' }
61
+ ref_document_type { '01' }
62
+ response_code { '01' }
63
+ document_type { '08' }
64
+ description { 'Unidades defectuosas' }
65
65
  lines { [build(:debit_note_line)] }
66
66
  end
67
67
 
@@ -71,26 +71,26 @@ FactoryBot.define do
71
71
  end
72
72
 
73
73
  factory :provider, class: 'SunatInvoice::Provider' do
74
- pk_file File.join(File.dirname(__FILE__), 'certs/pk_file')
75
- cert_file File.join(File.dirname(__FILE__), 'certs/cert_file')
76
- ruc FFaker::IdentificationMX.curp
77
- name FFaker::Company.name
78
- document_type 6
79
- ubigeo '14'
80
- street ''
81
- zone ''
82
- province ''
83
- department ''
84
- district ''
85
- country_code ''
74
+ pk_file { File.join(File.dirname(__FILE__), 'certs/pk_file') }
75
+ cert_file { File.join(File.dirname(__FILE__), 'certs/cert_file') }
76
+ ruc { FFaker::IdentificationMX.curp }
77
+ name { FFaker::Company.name }
78
+ document_type { 6 }
79
+ ubigeo { '14' }
80
+ street { '' }
81
+ zone { '' }
82
+ province { '' }
83
+ department { '' }
84
+ district { '' }
85
+ country_code { '' }
86
86
 
87
87
  initialize_with { new(attributes) }
88
88
  end
89
89
 
90
90
  factory :customer, class: 'SunatInvoice::Customer' do
91
- ruc FFaker::IdentificationMX.curp
92
- name FFaker::Company.name
93
- document_type 6
91
+ ruc { FFaker::IdentificationMX.curp }
92
+ name { FFaker::Company.name }
93
+ document_type { 6 }
94
94
 
95
95
  initialize_with { new(attributes) }
96
96
  end
@@ -100,19 +100,19 @@ FactoryBot.define do
100
100
  end
101
101
 
102
102
  factory :summary_line, class: 'SunatInvoice::SummaryLine' do
103
- document_type '03'
104
- document_serial 'BD56'
105
- start_document_number 231
106
- end_document_number 239
107
- taxable 2000
103
+ document_type { '03' }
104
+ document_serial { 'BD56' }
105
+ start_document_number { 231 }
106
+ end_document_number { 239 }
107
+ taxable { 2000 }
108
108
  taxes { [build(:tax, amount: 360), build(:tax, amount: 0, tax_type: :isc)] }
109
- total_amount 2360
109
+ total_amount { 2360 }
110
110
  end
111
111
 
112
112
  factory :voided_line, class: 'SunatInvoice::VoidedLine' do
113
- document_type '03'
114
- document_serial 'BD56'
115
- document_number 231
116
- description 'Anulación documento'
113
+ document_type { '03' }
114
+ document_serial { 'BD56' }
115
+ document_number { 231 }
116
+ description { 'Anulación documento' }
117
117
  end
118
118
  end
data/test/helper.rb CHANGED
@@ -6,6 +6,7 @@ require 'ffaker'
6
6
  require 'factory_bot'
7
7
  require_relative '../lib/sunat_invoice'
8
8
  require_relative 'factories'
9
+ require 'sunat_invoice/testing_support/signature_helper'
9
10
 
10
11
  Dir[File.join(File.dirname(__FILE__), 'support/**/*.rb')].each { |f| require f }
11
12
 
data/test/item_test.rb CHANGED
@@ -109,3 +109,14 @@ scope '#sale_taxes' do
109
109
  assert_equal @item.sale_taxes.values, [21.6, 10]
110
110
  end
111
111
  end
112
+
113
+ scope 'allow price_included_tax parameter' do
114
+ item = SunatInvoice::Item.new(quantity: 1,
115
+ price_included_tax: 100,
116
+ price_code: '01',
117
+ description: 'item description',
118
+ unit_code: 'NIU')
119
+ item.taxes << SunatInvoice::Tax.new(amount: 15.25, tax_type: :igv)
120
+ assert_equal item.sale_price.to_s, 100.to_s
121
+ assert_equal item.bi_value.to_s, 84.75.to_s
122
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sunat_invoice
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - César Carruitero
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-12-12 00:00:00.000000000 Z
11
+ date: 2019-11-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: nokogiri
@@ -171,6 +171,7 @@ files:
171
171
  - lib/sunat_invoice/signature.rb
172
172
  - lib/sunat_invoice/summary_line.rb
173
173
  - lib/sunat_invoice/tax.rb
174
+ - lib/sunat_invoice/testing_support/signature_helper.rb
174
175
  - lib/sunat_invoice/trade_calculations.rb
175
176
  - lib/sunat_invoice/trade_document.rb
176
177
  - lib/sunat_invoice/tributer.rb
@@ -192,7 +193,6 @@ files:
192
193
  - test/item_test.rb
193
194
  - test/response_parser_test.rb
194
195
  - test/support/response_helper.rb
195
- - test/support/signature_helper.rb
196
196
  homepage: https://github.com/ccarruitero/sunat_invoice
197
197
  licenses:
198
198
  - MPL-2.0
@@ -212,8 +212,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
212
212
  - !ruby/object:Gem::Version
213
213
  version: '0'
214
214
  requirements: []
215
- rubyforge_project:
216
- rubygems_version: 2.6.13
215
+ rubygems_version: 3.0.6
217
216
  signing_key:
218
217
  specification_version: 4
219
218
  summary: Ruby gem to use SUNAT Electronic Billing from your app