sunat_invoice 0.0.3 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
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