sunat_invoice 0.0.1 → 0.0.3
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/Gemfile +1 -0
- data/README.mkd +1 -25
- data/lib/sunat_invoice/client.rb +3 -6
- data/lib/sunat_invoice/clients/consult_client.rb +17 -0
- data/lib/sunat_invoice/clients/invoice_client.rb +11 -3
- data/lib/sunat_invoice/configuration.rb +1 -1
- data/lib/sunat_invoice/credit_note.rb +59 -0
- data/lib/sunat_invoice/credit_note_line.rb +15 -0
- data/lib/sunat_invoice/customer.rb +1 -0
- data/lib/sunat_invoice/daily_summary.rb +46 -0
- data/lib/sunat_invoice/debit_note.rb +33 -0
- data/lib/sunat_invoice/debit_note_line.rb +15 -0
- data/lib/sunat_invoice/invoice.rb +23 -144
- data/lib/sunat_invoice/item.rb +40 -26
- data/lib/sunat_invoice/line.rb +18 -0
- data/lib/sunat_invoice/provider.rb +20 -14
- data/lib/sunat_invoice/response_parser.rb +64 -0
- data/lib/sunat_invoice/signature.rb +29 -15
- data/lib/sunat_invoice/summary_line.rb +75 -0
- data/lib/sunat_invoice/tax.rb +6 -1
- data/lib/sunat_invoice/trade_calculations.rb +68 -0
- data/lib/sunat_invoice/trade_document.rb +85 -0
- data/lib/sunat_invoice/utils.rb +48 -9
- data/lib/sunat_invoice/voided.rb +22 -0
- data/lib/sunat_invoice/voided_line.rb +20 -0
- data/lib/sunat_invoice/xml_document.rb +52 -0
- data/lib/sunat_invoice.rb +14 -0
- data/sunat_invoice.gemspec +5 -5
- data/test/credit_note_test.rb +15 -0
- data/test/daily_summary_test.rb +35 -0
- data/test/debit_note_test.rb +15 -0
- data/test/factories.rb +70 -0
- data/test/fixtures/responses/invoice_success.xml +2 -0
- data/test/fixtures/responses/summary_success.xml +2 -0
- data/test/fixtures/responses/ticket_invalid.xml +2 -0
- data/test/helper.rb +1 -0
- data/test/invoice_client_test.rb +68 -16
- data/test/invoice_test.rb +19 -10
- data/test/item_test.rb +3 -2
- data/test/response_parser_test.rb +28 -0
- data/test/support/response_helper.rb +18 -0
- data/test/support/signature_helper.rb +15 -8
- metadata +34 -13
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: false
|
2
|
+
|
3
|
+
module SunatInvoice
|
4
|
+
class InvalidResponseParser < StandardError; end
|
5
|
+
|
6
|
+
class ResponseParser
|
7
|
+
attr_reader :cdr, :status_code, :document_number, :message, :ticket
|
8
|
+
|
9
|
+
STATUS_CODES = {
|
10
|
+
0 => 'process success',
|
11
|
+
99 => 'in process',
|
12
|
+
98 => 'process with errors'
|
13
|
+
}.freeze
|
14
|
+
|
15
|
+
ALLOWED_PARSERS = %w[invoice summary status].freeze
|
16
|
+
VALID_PROCESS = %w[0 99].freeze
|
17
|
+
|
18
|
+
def initialize(body, parser_type)
|
19
|
+
# body: SOAP body as a Hash. Typically Savon Response body.
|
20
|
+
# parser_type: kind of parser to use.
|
21
|
+
raise InvalidResponseParser unless ALLOWED_PARSERS.include?(parser_type)
|
22
|
+
send("parse_#{parser_type}", body)
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def parse_invoice(body)
|
28
|
+
encrypted_zip = body[:send_bill_response][:application_response]
|
29
|
+
decrypt_zip(encrypted_zip)
|
30
|
+
end
|
31
|
+
|
32
|
+
def parse_xml(cdr_xml)
|
33
|
+
@cdr = Nokogiri::XML(cdr_xml)
|
34
|
+
response_node = @cdr.at('//cac:DocumentResponse/cac:Response')
|
35
|
+
@status_code = response_node.at('//cbc:ResponseCode').content
|
36
|
+
@document_number = response_node.at('//cbc:ReferenceID').content
|
37
|
+
@message = response_node.at('//cbc:Description').content
|
38
|
+
end
|
39
|
+
|
40
|
+
def parse_summary(body)
|
41
|
+
@ticket = body[:send_summary_response][:ticket]
|
42
|
+
end
|
43
|
+
|
44
|
+
def parse_status(body)
|
45
|
+
status_hash = body[:get_status_response][:status]
|
46
|
+
@status_code = status_hash[:status_code]
|
47
|
+
if VALID_PROCESS.include?(status_code)
|
48
|
+
encrypted_zip = status_hash[:content]
|
49
|
+
decrypt_zip(encrypted_zip)
|
50
|
+
else
|
51
|
+
@message = status_hash[:content]
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def decrypt_zip(encrypted_zip)
|
56
|
+
decoded = Base64.decode64(encrypted_zip)
|
57
|
+
Zip::InputStream.open(StringIO.new(decoded)) do |io|
|
58
|
+
while (entry = io.get_next_entry)
|
59
|
+
parse_xml(io.read) if entry.name.include?('.xml')
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -18,24 +18,34 @@ module SunatInvoice
|
|
18
18
|
def signer_data(xml)
|
19
19
|
xml['cac'].Signature do
|
20
20
|
xml['cbc'].ID provider.signature_id
|
21
|
-
xml
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
21
|
+
build_signatory_party(xml)
|
22
|
+
build_digital_attachment(xml)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def build_signatory_party(xml)
|
27
|
+
xml['cac'].SignatoryParty do
|
28
|
+
xml['cac'].PartyIdentification do
|
29
|
+
xml['cbc'].ID provider.ruc
|
28
30
|
end
|
29
|
-
xml['cac'].
|
30
|
-
xml['
|
31
|
-
|
32
|
-
|
31
|
+
xml['cac'].PartyName do
|
32
|
+
xml['cbc'].Name provider.name
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def build_digital_attachment(xml)
|
38
|
+
xml['cac'].DigitalSignatureAttachment do
|
39
|
+
xml['cac'].ExternalReference do
|
40
|
+
xml['cbc'].URI "##{provider.signature_location_id}"
|
33
41
|
end
|
34
42
|
end
|
35
43
|
end
|
36
44
|
|
37
45
|
def sign(invoice_xml)
|
38
|
-
|
46
|
+
options = { id_attr: provider.signature_location_id }
|
47
|
+
doc = Xmldsig::SignedDocument.new(invoice_xml, options)
|
48
|
+
doc.sign(private_key)
|
39
49
|
end
|
40
50
|
|
41
51
|
def signature_ext(xml)
|
@@ -52,15 +62,19 @@ module SunatInvoice
|
|
52
62
|
xml['ds'].CanonicalizationMethod Algorithm: C14N_ALGORITHM
|
53
63
|
xml['ds'].SignatureMethod Algorithm: SIGNATURE_ALGORITHM
|
54
64
|
xml['ds'].Reference URI: '' do
|
55
|
-
xml
|
56
|
-
xml['ds'].Transform Algorithm: TRANSFORMATION_ALGORITHM
|
57
|
-
end
|
65
|
+
build_transforms(xml)
|
58
66
|
xml['ds'].DigestMethod Algorithm: DIGEST_ALGORITHM
|
59
67
|
xml['ds'].DigestValue
|
60
68
|
end
|
61
69
|
end
|
62
70
|
end
|
63
71
|
|
72
|
+
def build_transforms(xml)
|
73
|
+
xml['ds'].Transforms do
|
74
|
+
xml['ds'].Transform Algorithm: TRANSFORMATION_ALGORITHM
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
64
78
|
def signature_value(xml)
|
65
79
|
xml['ds'].SignatureValue
|
66
80
|
xml['ds'].KeyInfo do
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'line'
|
4
|
+
|
5
|
+
module SunatInvoice
|
6
|
+
class SummaryLine < Line
|
7
|
+
attr_accessor :document_type, :document_serial, :start_document_number,
|
8
|
+
:end_document_number, :total_amount, :taxable, :non_taxable,
|
9
|
+
:exempt, :other_charge, :charge_type
|
10
|
+
|
11
|
+
CHARGES = {
|
12
|
+
discount: false,
|
13
|
+
charge: true
|
14
|
+
}.freeze
|
15
|
+
|
16
|
+
def initialize(*args)
|
17
|
+
super(*args)
|
18
|
+
@taxable ||= 0.01
|
19
|
+
@non_taxable ||= 0.01
|
20
|
+
@exempt ||= 0.01
|
21
|
+
@other_charge ||= 0.01
|
22
|
+
end
|
23
|
+
|
24
|
+
def xml(xml, index, currency)
|
25
|
+
xml['sac'].SummaryDocumentsLine do
|
26
|
+
xml['cbc'].LineID(index + 1)
|
27
|
+
build_documents_info(xml)
|
28
|
+
amount_xml(xml['sac'], 'TotalAmount', total_amount, currency)
|
29
|
+
build_payments(xml, currency)
|
30
|
+
build_other_charge(xml, currency)
|
31
|
+
build_taxes_xml(xml, currency)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def payments
|
38
|
+
[{ amount: taxable, code: '01' },
|
39
|
+
{ amount: exempt, code: '02' },
|
40
|
+
{ amount: non_taxable, code: '03' }]
|
41
|
+
end
|
42
|
+
|
43
|
+
def build_documents_info(xml)
|
44
|
+
xml['cbc'].DocumentTypeCode document_type
|
45
|
+
xml['sac'].DocumentSerialID document_serial
|
46
|
+
xml['sac'].StartDocumentNumberID start_document_number
|
47
|
+
xml['sac'].EndDocumentNumberID end_document_number
|
48
|
+
end
|
49
|
+
|
50
|
+
def calculate_total_amount
|
51
|
+
return if total_amount
|
52
|
+
# TODO: sum(billing payments) + allowance charge + sum(taxes)
|
53
|
+
end
|
54
|
+
|
55
|
+
def build_payments(xml, currency)
|
56
|
+
payments.each do |payment|
|
57
|
+
xml['sac'].BillingPayment do
|
58
|
+
amount_xml(xml['cbc'], 'PaidAmount', payment[:amount], currency)
|
59
|
+
xml['cbc'].InstructionID payment[:code]
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def build_other_charge(xml, currency)
|
65
|
+
xml['cac'].AllowanceCharge do
|
66
|
+
xml['cbc'].ChargeIndicator CHARGES.values.last
|
67
|
+
amount_xml(xml['cbc'], 'Amount', other_charge, currency)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def resolve_charge_type
|
72
|
+
charge_type ? CHARGES[charge_type] : CHARGES.values.first
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
data/lib/sunat_invoice/tax.rb
CHANGED
@@ -41,7 +41,7 @@ module SunatInvoice
|
|
41
41
|
|
42
42
|
def tax_category(xml)
|
43
43
|
xml['cac'].TaxCategory do
|
44
|
-
xml
|
44
|
+
tax_exemption(xml)
|
45
45
|
xml['cbc'].TierRange(tier_range) if tier_range
|
46
46
|
tax_scheme(xml)
|
47
47
|
end
|
@@ -58,5 +58,10 @@ module SunatInvoice
|
|
58
58
|
def tax_data(attribute)
|
59
59
|
TAXES[tax_type][attribute]
|
60
60
|
end
|
61
|
+
|
62
|
+
def tax_exemption(xml)
|
63
|
+
return unless tax_exemption_reason
|
64
|
+
xml['cbc'].TaxExemptionReasonCode(tax_exemption_reason)
|
65
|
+
end
|
61
66
|
end
|
62
67
|
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# frozen_string_literal: false
|
2
|
+
|
3
|
+
require_relative 'catalogs'
|
4
|
+
|
5
|
+
module SunatInvoice
|
6
|
+
module TradeCalculations
|
7
|
+
def prepare_totals
|
8
|
+
calculate_taxes_totals
|
9
|
+
calculate_sale_totals
|
10
|
+
calculate_total
|
11
|
+
end
|
12
|
+
|
13
|
+
def calculate_total
|
14
|
+
# calculate invoice total
|
15
|
+
@total = 0
|
16
|
+
@total += @taxes_totals.values.sum
|
17
|
+
@total += @sale_totals.reject { |k, _v| k == '1004' }.values.sum
|
18
|
+
@total -= discount if discount
|
19
|
+
end
|
20
|
+
|
21
|
+
def calculate_sale_totals
|
22
|
+
@sale_totals = {}
|
23
|
+
# get bi totals according kind of sale (gravado, inafecto, exonerado ..)
|
24
|
+
lines&.each do |item|
|
25
|
+
# TODO: I think in most cases only be one tax for item, but should
|
26
|
+
# handle more cases
|
27
|
+
total_code = get_total_code(item.taxes.first)
|
28
|
+
if total_code
|
29
|
+
@sale_totals[total_code] = 0 unless @sale_totals[total_code]
|
30
|
+
@sale_totals[total_code] += item.bi_value
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def calculate_taxes_totals
|
36
|
+
# concat item's sale_taxes
|
37
|
+
@taxes_totals = {}
|
38
|
+
taxes = lines&.map(&:sale_taxes)&.flatten
|
39
|
+
taxes&.each do |tax|
|
40
|
+
@taxes_totals[tax.keys.first] ||= 0
|
41
|
+
@taxes_totals[tax.keys.first] += tax.values.sum
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def get_total_code(tax)
|
46
|
+
return unless tax
|
47
|
+
case tax.tax_type
|
48
|
+
# TODO: :isc
|
49
|
+
when :igv
|
50
|
+
get_total_igv_code(tax.tax_exemption_reason)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def get_total_igv_code(exemption_reason)
|
55
|
+
if Catalogs::CATALOG_07.first == exemption_reason
|
56
|
+
Catalogs::CATALOG_14.first
|
57
|
+
elsif Catalogs::CATALOG_07[1..6].include?(exemption_reason)
|
58
|
+
Catalogs::CATALOG_14[3]
|
59
|
+
elsif Catalogs::CATALOG_07[7] == exemption_reason
|
60
|
+
Catalogs::CATALOG_14[2]
|
61
|
+
elsif Catalogs::CATALOG_07[8] == exemption_reason
|
62
|
+
Catalogs::CATALOG_14[1]
|
63
|
+
elsif Catalogs::CATALOG_07[9..14].include?(exemption_reason)
|
64
|
+
Catalogs::CATALOG_14[3]
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
# frozen_string_literal: false
|
2
|
+
|
3
|
+
require_relative 'tax'
|
4
|
+
require_relative 'trade_calculations'
|
5
|
+
|
6
|
+
module SunatInvoice
|
7
|
+
class TradeDocument < XmlDocument
|
8
|
+
include TradeCalculations
|
9
|
+
|
10
|
+
attr_accessor :customer, :document_number, :document_type, :discount
|
11
|
+
|
12
|
+
INVOICE_TYPES = %w[01 03].freeze
|
13
|
+
|
14
|
+
def operation
|
15
|
+
:send_bill
|
16
|
+
end
|
17
|
+
|
18
|
+
def document_name
|
19
|
+
"#{@provider.ruc}-#{document_type}-#{document_number}"
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def invoice?
|
25
|
+
INVOICE_TYPES.include?(document_type)
|
26
|
+
end
|
27
|
+
|
28
|
+
def build_document_data(xml)
|
29
|
+
build_number(xml)
|
30
|
+
xml['cbc'].IssueDate formatted_date(date)
|
31
|
+
xml['cbc'].InvoiceTypeCode document_type if invoice?
|
32
|
+
xml['cbc'].DocumentCurrencyCode currency
|
33
|
+
end
|
34
|
+
|
35
|
+
def build_ext(xml)
|
36
|
+
super(xml) do |xml_|
|
37
|
+
build_sale_totals(xml_)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def build_sale_totals(xml)
|
42
|
+
prepare_totals
|
43
|
+
ubl_ext(xml) do
|
44
|
+
xml['sac'].AdditionalInformation do
|
45
|
+
@sale_totals&.each do |code, amount|
|
46
|
+
build_monetary_total(xml, code, amount)
|
47
|
+
end
|
48
|
+
build_monetary_total(xml, '2005', discount) if discount
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def build_monetary_total(xml, code, amount)
|
54
|
+
xml['sac'].AdditionalMonetaryTotal do
|
55
|
+
xml['cbc'].ID code
|
56
|
+
amount_xml(xml['cbc'], 'PayableAmount', amount, @currency)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def build_common_content(xml)
|
61
|
+
@signature.signer_data(xml)
|
62
|
+
@provider.info(xml)
|
63
|
+
@customer.info(xml)
|
64
|
+
build_taxes_totals(xml)
|
65
|
+
build_total(xml)
|
66
|
+
build_lines_xml(xml)
|
67
|
+
end
|
68
|
+
|
69
|
+
def build_taxes_totals(xml)
|
70
|
+
@taxes_totals.each do |key, value|
|
71
|
+
SunatInvoice::Tax.new(tax_type: key, amount: value).xml(xml, @currency)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def total_tag
|
76
|
+
'LegalMonetaryTotal'
|
77
|
+
end
|
78
|
+
|
79
|
+
def build_total(xml)
|
80
|
+
xml['cac'].send(total_tag) do
|
81
|
+
amount_xml(xml['cbc'], 'PayableAmount', @total, @currency)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
data/lib/sunat_invoice/utils.rb
CHANGED
@@ -4,18 +4,57 @@ module SunatInvoice
|
|
4
4
|
module Utils
|
5
5
|
@namespace_path = 'urn:oasis:names:specification:ubl:schema:xsd'
|
6
6
|
@sunat_namespace_path = 'urn:sunat:names:specification:ubl:peru:schema:xsd'
|
7
|
+
@un_namespace_path = 'urn:un:unece:uncefact:data:specification'
|
7
8
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
9
|
+
COMMON_NAMESPACES = {
|
10
|
+
cac: "#{@namespace_path}:CommonAggregateComponents-2",
|
11
|
+
cbc: "#{@namespace_path}:CommonBasicComponents-2",
|
12
|
+
ds: 'http://www.w3.org/2000/09/xmldsig#',
|
13
|
+
ext: "#{@namespace_path}:CommonExtensionComponents-2",
|
14
|
+
sac: "#{@sunat_namespace_path}:SunatAggregateComponents-1",
|
15
|
+
xsi: 'http://www.w3.org/2001/XMLSchema-instance'
|
16
|
+
}.freeze
|
17
|
+
|
18
|
+
TRADE_NAMESPACES = {
|
19
|
+
'xmlns:cac' => COMMON_NAMESPACES[:cac],
|
20
|
+
'xmlns:cbc' => COMMON_NAMESPACES[:cbc],
|
12
21
|
'xmlns:ccts' => 'urn:un:unece:uncefact:documentation:2',
|
13
|
-
'xmlns:ds' =>
|
14
|
-
'xmlns:ext' =>
|
22
|
+
'xmlns:ds' => COMMON_NAMESPACES[:ds],
|
23
|
+
'xmlns:ext' => COMMON_NAMESPACES[:ext],
|
15
24
|
'xmlns:qdt' => "#{@namespace_path}:QualifiedDatatypes-2",
|
16
|
-
'xmlns:sac' =>
|
17
|
-
'xmlns:udt' =>
|
18
|
-
'xmlns:xsi' =>
|
25
|
+
'xmlns:sac' => COMMON_NAMESPACES[:sac],
|
26
|
+
'xmlns:udt' => "#{@un_namespace_path}:UnqualifiedDataTypesSchemaModule:2",
|
27
|
+
'xmlns:xsi' => COMMON_NAMESPACES[:xsi]
|
28
|
+
}.freeze
|
29
|
+
|
30
|
+
INVOICE_NAMESPACES = {
|
31
|
+
'xmlns' => "#{@namespace_path}:Invoice-2"
|
32
|
+
}.freeze
|
33
|
+
|
34
|
+
CREDIT_NOTE_NAMESPACES = {
|
35
|
+
'xmlns' => "#{@namespace_path}:CreditNote-2"
|
36
|
+
}.freeze
|
37
|
+
|
38
|
+
DEBIT_NOTE_NAMESPACES = {
|
39
|
+
'xmlns' => "#{@namespace_path}:DebitNote-2"
|
40
|
+
}.freeze
|
41
|
+
|
42
|
+
SUMMARY_NAMESPACES = {
|
43
|
+
'xmlns:cac' => COMMON_NAMESPACES[:cac],
|
44
|
+
'xmlns:cbc' => COMMON_NAMESPACES[:cbc],
|
45
|
+
'xmlns:ds' => COMMON_NAMESPACES[:ds],
|
46
|
+
'xmlns:ext' => COMMON_NAMESPACES[:ext],
|
47
|
+
'xmlns:sac' => COMMON_NAMESPACES[:sac],
|
48
|
+
'xmlns:xsi' => COMMON_NAMESPACES[:xsi]
|
49
|
+
}.freeze
|
50
|
+
|
51
|
+
DAILY_SUMMARY_NAMESPACES = {
|
52
|
+
'xmlns' => "#{@sunat_namespace_path}:SummaryDocuments-1",
|
53
|
+
'xsi:schemaLocation' => 'urn:sunat:names:specification:ubl:peru:schema:xsd:InvoiceSummary-1 D:\UBL_SUNAT\SUNAT_xml_20110112\20110112\xsd\maindoc\UBLPE-InvoiceSummary-1.0.xsd'
|
54
|
+
}.freeze
|
55
|
+
|
56
|
+
VOIDED_NAMESPACES = {
|
57
|
+
'xmlns' => "#{@sunat_namespace_path}:VoidedDocuments-1"
|
19
58
|
}.freeze
|
20
59
|
|
21
60
|
def ubl_ext(xml, &block)
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: false
|
2
|
+
|
3
|
+
require_relative 'daily_summary'
|
4
|
+
|
5
|
+
module SunatInvoice
|
6
|
+
class Voided < DailySummary
|
7
|
+
private
|
8
|
+
|
9
|
+
def namespaces
|
10
|
+
VOIDED_NAMESPACES.merge(SUMMARY_NAMESPACES)
|
11
|
+
end
|
12
|
+
|
13
|
+
def root_name
|
14
|
+
'VoidedDocuments'
|
15
|
+
end
|
16
|
+
|
17
|
+
def document_number
|
18
|
+
formatted = date.strftime('%Y%m%d') # YYYYMMDD
|
19
|
+
"RA-#{formatted}-1"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'line'
|
4
|
+
|
5
|
+
module SunatInvoice
|
6
|
+
class VoidedLine < Line
|
7
|
+
attr_accessor :document_type, :document_serial, :document_number,
|
8
|
+
:description
|
9
|
+
|
10
|
+
def xml(xml, index, _currency)
|
11
|
+
xml['sac'].VoidedDocumentsLine do
|
12
|
+
xml['cbc'].LineID(index + 1)
|
13
|
+
xml['cbc'].DocumentTypeCode document_type
|
14
|
+
xml['sac'].DocumentSerialID document_serial
|
15
|
+
xml['sac'].DocumentNumberID document_number
|
16
|
+
xml['sac'].VoidReasonDescription description
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: false
|
2
|
+
|
3
|
+
require_relative 'model'
|
4
|
+
require_relative 'utils'
|
5
|
+
|
6
|
+
module SunatInvoice
|
7
|
+
class XmlDocument < Model
|
8
|
+
include Utils
|
9
|
+
|
10
|
+
attr_accessor :date, :provider, :signature, :currency, :lines
|
11
|
+
|
12
|
+
UBL_VERSION = '2.0'.freeze
|
13
|
+
CUSTOMIZATION = '1.0'.freeze
|
14
|
+
|
15
|
+
def initialize(*args)
|
16
|
+
super(*args)
|
17
|
+
@date ||= Date.today
|
18
|
+
end
|
19
|
+
|
20
|
+
def build_xml(&block)
|
21
|
+
Nokogiri::XML::Builder.new do |xml|
|
22
|
+
xml.send(root_name, namespaces) do
|
23
|
+
build_ext(xml)
|
24
|
+
xml['cbc'].UBLVersionID UBL_VERSION
|
25
|
+
xml['cbc'].CustomizationID CUSTOMIZATION
|
26
|
+
yield(xml) if block
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def build_ext(xml, &block)
|
32
|
+
xml['ext'].UBLExtensions do
|
33
|
+
yield(xml) if block
|
34
|
+
@signature.signature_ext(xml)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def build_number(xml)
|
39
|
+
xml['cbc'].ID document_number
|
40
|
+
end
|
41
|
+
|
42
|
+
def formatted_date(date)
|
43
|
+
date.strftime('%Y-%m-%d')
|
44
|
+
end
|
45
|
+
|
46
|
+
def build_lines_xml(xml)
|
47
|
+
lines&.each_with_index do |line, index|
|
48
|
+
line.xml(xml, index, currency)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
data/lib/sunat_invoice.rb
CHANGED
@@ -1,13 +1,27 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
# documents
|
4
|
+
require 'sunat_invoice/xml_document'
|
3
5
|
require 'sunat_invoice/invoice'
|
4
6
|
require 'sunat_invoice/provider'
|
5
7
|
require 'sunat_invoice/customer'
|
6
8
|
require 'sunat_invoice/item'
|
7
9
|
require 'sunat_invoice/tax'
|
10
|
+
require 'sunat_invoice/daily_summary'
|
11
|
+
require 'sunat_invoice/summary_line'
|
12
|
+
require 'sunat_invoice/voided'
|
13
|
+
require 'sunat_invoice/voided_line'
|
14
|
+
require 'sunat_invoice/credit_note'
|
15
|
+
require 'sunat_invoice/credit_note_line'
|
16
|
+
require 'sunat_invoice/debit_note'
|
17
|
+
require 'sunat_invoice/debit_note_line'
|
18
|
+
|
19
|
+
# clients
|
8
20
|
require 'sunat_invoice/configuration'
|
21
|
+
require 'sunat_invoice/response_parser'
|
9
22
|
require 'sunat_invoice/client'
|
10
23
|
require 'sunat_invoice/clients/invoice_client'
|
24
|
+
require 'sunat_invoice/clients/consult_client'
|
11
25
|
|
12
26
|
module SunatInvoice
|
13
27
|
class << self
|
data/sunat_invoice.gemspec
CHANGED
@@ -2,9 +2,9 @@
|
|
2
2
|
|
3
3
|
Gem::Specification.new do |s|
|
4
4
|
s.name = 'sunat_invoice'
|
5
|
-
s.version = '0.0.
|
6
|
-
s.summary = '
|
7
|
-
s.description =
|
5
|
+
s.version = '0.0.3'
|
6
|
+
s.summary = 'Ruby gem to use SUNAT Electronic Billing from your app'
|
7
|
+
s.description = 'Generate and send Electronic Invoices to SUNAT'
|
8
8
|
s.authors = ['César Carruitero']
|
9
9
|
s.email = ['cesar@mozilla.pe']
|
10
10
|
s.homepage = 'https://github.com/ccarruitero/sunat_invoice'
|
@@ -12,9 +12,9 @@ Gem::Specification.new do |s|
|
|
12
12
|
|
13
13
|
s.files = `git ls-files`.split("\n")
|
14
14
|
|
15
|
-
s.add_dependency 'savon', '~> 2.11.2'
|
16
15
|
s.add_dependency 'nokogiri', '~> 1.8'
|
17
|
-
s.add_dependency 'rubyzip', '~> 1.2
|
16
|
+
s.add_dependency 'rubyzip', '~> 1.2'
|
17
|
+
s.add_dependency 'savon', '~> 2.11'
|
18
18
|
s.add_dependency 'xmldsig', '~> 0.6.5'
|
19
19
|
|
20
20
|
s.add_development_dependency 'cutest', '~> 1.2'
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: false
|
2
|
+
|
3
|
+
require_relative 'helper'
|
4
|
+
|
5
|
+
setup do
|
6
|
+
provider = FactoryBot.build(:provider)
|
7
|
+
signature = FactoryBot.build(:signature, provider: provider)
|
8
|
+
note = FactoryBot.build(:credit_note, provider: provider,
|
9
|
+
signature: signature)
|
10
|
+
@parsed_xml = Nokogiri::XML(note.xml, &:noblanks)
|
11
|
+
end
|
12
|
+
|
13
|
+
test 'start with CreditNote tag' do
|
14
|
+
assert_equal @parsed_xml.root.name, 'CreditNote'
|
15
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: false
|
2
|
+
|
3
|
+
require_relative 'helper'
|
4
|
+
|
5
|
+
setup do
|
6
|
+
provider = FactoryBot.build(:provider)
|
7
|
+
signature = FactoryBot.build(:signature, provider: provider)
|
8
|
+
ref_date = Date.today - 1
|
9
|
+
@line = FactoryBot.build(:summary_line)
|
10
|
+
summary = SunatInvoice::DailySummary.new(provider: provider,
|
11
|
+
signature: signature,
|
12
|
+
reference_date: ref_date,
|
13
|
+
currency: 'PEN',
|
14
|
+
lines: [@line])
|
15
|
+
@parsed_xml = Nokogiri::XML(summary.xml, &:noblanks)
|
16
|
+
end
|
17
|
+
|
18
|
+
test 'start with SummaryDocuments tag' do
|
19
|
+
assert_equal @parsed_xml.root.name, 'SummaryDocuments'
|
20
|
+
end
|
21
|
+
|
22
|
+
test 'line total amount has correct content' do
|
23
|
+
total = @parsed_xml.at('//sac:SummaryDocumentsLine/sac:TotalAmount').content
|
24
|
+
assert_equal total, @line.total_amount.to_s
|
25
|
+
end
|
26
|
+
|
27
|
+
test 'ChargeIndicator must not be empty when not charge_type' do
|
28
|
+
assert @line.charge_type.nil?
|
29
|
+
assert !@parsed_xml.at('//cbc:ChargeIndicator').content.empty?
|
30
|
+
end
|
31
|
+
|
32
|
+
test 'AllowanceCharge Amount is 0' do
|
33
|
+
amount = @parsed_xml.at('//cac:AllowanceCharge/cbc:Amount').content
|
34
|
+
assert_equal amount, '0.01'
|
35
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: false
|
2
|
+
|
3
|
+
require_relative 'helper'
|
4
|
+
|
5
|
+
setup do
|
6
|
+
provider = FactoryBot.build(:provider)
|
7
|
+
signature = FactoryBot.build(:signature, provider: provider)
|
8
|
+
note = FactoryBot.build(:debit_note, provider: provider,
|
9
|
+
signature: signature)
|
10
|
+
@parsed_xml = Nokogiri::XML(note.xml, &:noblanks)
|
11
|
+
end
|
12
|
+
|
13
|
+
test 'start with DebitNote tag' do
|
14
|
+
assert_equal @parsed_xml.root.name, 'DebitNote'
|
15
|
+
end
|