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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: b49429704d8a0fd453f1210ee274c0b111dbb96e
4
+ data.tar.gz: ecae8df2666c3e508abea79a594c375b12b03153
5
+ SHA512:
6
+ metadata.gz: 83d17d8b14f0bfb1bcbdea6e449346d7ac0b819859d434695f93ba306e8f979f0885683454fd655d1bfb54f4ee9f60cbe16e0266e429b1507df75d6745365d82
7
+ data.tar.gz: c1992a23ae4817da9539e980bc3ee745335d70b802d1a18a32793eee7c6918140af4915ae84cd72c6e30a7b8edb2077808f656f596c0e8b4446cdf3eba362865
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ Gemfile.lock
2
+ *.swp
3
+ *.gem
4
+ test/certs
data/.rubocop.yml ADDED
@@ -0,0 +1,11 @@
1
+ AllCops:
2
+ TargetRubyVersion: 2.3
3
+ Style/Documentation:
4
+ Enabled: false
5
+ Style/AsciiComments :
6
+ Enabled: false
7
+ Metrics/BlockLength:
8
+ Exclude:
9
+ - 'test/*.rb'
10
+ Metrics/MethodLength:
11
+ Max: 20
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm --create use 2.4.1@sunat_invoice
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+ source 'https://rubygems.org'
3
+
4
+ gemspec
data/Makefile ADDED
@@ -0,0 +1,8 @@
1
+ .PHONY: test
2
+
3
+ console:
4
+ irb -Ilib -rsunat_invoice
5
+
6
+ test:
7
+ cutest test/**/*_test.rb
8
+ rubocop
data/README.mkd ADDED
@@ -0,0 +1,26 @@
1
+ # Sunat Invoice
2
+ Ruby wrapper to use SUNAT Electronic Invoice API
3
+
4
+ homologacion
5
+ https://www.sunat.gob.pe/ol-ti-itcpgem-sqa/billService?wsdl
6
+
7
+ valid_service
8
+
9
+ http://www.sunat.gob.pe/ol-ti-itconsvalicpe/ConsValiCpe.htm
10
+
11
+ {:validaCDPcriterios=>{:rucEmisor=>"string", :tipoCDP=>"string", :serieCDP=>"string", :numeroCDP=>"string", :tipoDocIdReceptor=>"string", :numeroDocIdReceptor=>"string", :fechaEmision=>"string", :importeTotal=>"double", :nroAutorizacion=>"string"}}
12
+
13
+ http://www.sunat.gob.pe/ol-ti-itconsverixml/ConsVeriXml.htm
14
+
15
+ {:verificaCPEarchivo=>{:nombre=>"string", :archivo=>"string"}}
16
+
17
+ errors
18
+ https://www.facturacionperu.ws/?p=138
19
+
20
+
21
+ To write:
22
+ https://www.xml.com/pub/a/2001/08/08/xmldsig.html
23
+ https://www.di-mgt.com.au/xmldsig.html
24
+
25
+ https://github.com/ebeigarts/signer
26
+ https://github.com/benoist/xmldsig
@@ -0,0 +1,177 @@
1
+ # frozen_string_literal: false
2
+
3
+ module SunatInvoice
4
+ module Catalogs
5
+ # SUNAT codes
6
+
7
+ # Tipo de documento
8
+ CATALOG_01 = [
9
+ '01', # FACTURA
10
+ '03', # BOLETA DE VENTA
11
+ '07', # NOTA DE CREDITO
12
+ '08', # NOTA DE DEBITO 383
13
+ '09', # GUIA DE REMISIÓN REMITENTE
14
+ '12', # TICKET DE MAQUINA REGISTRADORA
15
+ '13', # DOCUMENTO EMITIDO POR BANCOS, INSTITUCIONES FINANCIERAS
16
+ '18', # DOCUMENTOS EMITIDOS POR LAS AFP
17
+ '31', # GUIA DE REMISIÓN TRANSPORTISTA
18
+ ].freeze
19
+
20
+ # Tipo de Tributo
21
+ CATALOG_05 = [
22
+ '1000', # IGV
23
+ '2000', # ISC
24
+ '9999' # Otros
25
+ ].freeze
26
+
27
+ # Tipo de Documentos de Identidad
28
+ CATALOG_06 = [
29
+ '0', # DOC.TRIB.NO.DOM.SIN.RUC
30
+ '1', # DOC. NACIONAL DE IDENTIDAD
31
+ '4', # CARNET DE EXTRANJERIA
32
+ '6', # REG. UNICO DE CONTRIBUYENTES
33
+ '7', # PASAPORTE
34
+ 'A' # CED. DIPLOMATICA DE IDENTIDAD
35
+ ].freeze
36
+
37
+ # Tipo de Afectación del IGV
38
+ CATALOG_07 = [
39
+ '10', # Gravado - Operación Onerosa
40
+ '11', # Gravado – Retiro por premio
41
+ '12', # Gravado – Retiro por donación
42
+ '13', # Gravado – Retiro
43
+ '14', # Gravado – Retiro por publicidad
44
+ '15', # Gravado – Bonificaciones
45
+ '16', # Gravado – Retiro por entrega a trabajadores
46
+ '20', # Exonerado - Operación Onerosa
47
+ '30', # Inafecto - Operación Onerosa
48
+ '31', # Inafecto – Retiro por Bonificación
49
+ '32', # Inafecto – Retiro
50
+ '33', # Inafecto – Retiro por Muestras Médicas
51
+ '34', # Inafecto - Retiro por Convenio Colectivo
52
+ '35', # Inafecto – Retiro por premio
53
+ '36', # Inafecto - Retiro por publicidad
54
+ '40' # Exportación
55
+ ].freeze
56
+
57
+ # Tipo de Sistema de Cáclulo del ISC
58
+ CATALOG_08 = [
59
+ '01', # Sistema al valor
60
+ '02', # Aplicación del Monto Fijo
61
+ '03', # Sistema de Precios de Venta al Público
62
+ ].freeze
63
+
64
+ # Tipo de Nota de Crédito Electrónica
65
+ CATALOG_09 = [
66
+ '01', # Anulación de la operación
67
+ '02', # Anulación por error en el RUC
68
+ '03', # Corrección por error en la descripción
69
+ '04', # Descuento global
70
+ '05', # Descuento por ítem
71
+ '06', # Devolución total
72
+ '07', # Devolución por ítem
73
+ '08', # Bonificación
74
+ '09' # Disminución en el valor
75
+ ].freeze
76
+
77
+ # Tipo de Nota de Débito Electrónica
78
+ CATALOG_10 = [
79
+ '01', # Intereses por mora
80
+ '02' # Aumento en el valor
81
+ ].freeze
82
+
83
+ # Resumen Diario de Boletas de Ventas Electrónicas y Notas Electrónicas
84
+ # Tipo de Valor de Venta
85
+ CATALOG_11 = [
86
+ '01', # Gravado
87
+ '02', # Exonerado
88
+ '03', # Inafecto
89
+ '04', # Exportación
90
+ '05' # Gratuitas
91
+ ].freeze
92
+
93
+ # Documentos Relacionados Tributarios
94
+ CATALOG_12 = [
95
+ '01', # Factura – emitida para corregir error en el RUC
96
+ '04', # Ticket de Salida - ENAPU
97
+ '05', # Código SCOP
98
+ '99' # Otros
99
+ ].freeze
100
+
101
+ # Otros conceptos tributarios
102
+ CATALOG_14 = [
103
+ '1001', # Total valor de venta - operaciones gravadas
104
+ '1002', # Total valor de venta - operaciones inafectas
105
+ '1003', # Total valor de venta - operaciones exoneradas
106
+ '1004', # Total valor de venta - Operaciones gratuitas
107
+ '1005', # Sub total de venta
108
+ '2001', # Percepciones
109
+ '2002', # Retenciones
110
+ '2003', # Detracciones
111
+ '2004', # Bonificaciones
112
+ '2005' # Total descuentos
113
+ ].freeze
114
+
115
+ # Elementos adicionales en la Factura y/o Boleta de Venta Electrónica
116
+ CATALOG_15 = [
117
+ '1000', # Monto en Letras
118
+ '1002',
119
+ # Leyenda “TRANSFERENCIA GRATUITA DE UN BIEN Y/O SERVICIO PRESTADO
120
+ # GRATUITAMENTE”
121
+ '2000', # Leyenda “COMPROBANTE DE PERCEPCIÓN”
122
+ '2001',
123
+ # Leyenda “BIENES TRANSFERIDOS EN LA AMAZONÍA REGIÓN SELVAPARA SER
124
+ # CONSUMIDOS EN LA MISMA”
125
+ '2002',
126
+ # Leyenda “SERVICIOS PRESTADOS EN LA AMAZONÍA REGIÓN SELVA PARA SER
127
+ # CONSUMIDOS EN LA MISMA”
128
+ '2003',
129
+ # Leyenda “CONTRATOS DE CONSTRUCCIÓN EJECUTADOS EN LA AMAZONÍA REGIÓN
130
+ # SELVA”
131
+ '2004', # Leyenda “Agencia de Viaje - Paquete turístico”
132
+ '3000', # Detracciones: CODIGO DE BB Y SS SUJETOS A DETRACCION
133
+ '3001', # Detracciones: NUMERO DE CTA EN EL BN
134
+ '3002',
135
+ # Detracciones: Recursos Hidrobiológicos-Nombre y matrícula de la
136
+ # embarcación
137
+ '3003',
138
+ # Detracciones: Recursos Hidrobiológicos-Tipo y cantidad de especie
139
+ # vendida
140
+ '3004', # Detracciones: Recursos Hidrobiológicos -Lugar de descarga
141
+ '3005', # Detracciones: Recursos Hidrobiológicos -Fecha de descarga
142
+ '3006',
143
+ # Detracciones: Transporte Bienes vía terrestre – Numero Registro MTC
144
+ '3007',
145
+ # Detracciones: Transporte Bienes vía terrestre -configuración vehicular
146
+ '3008', # Detracciones: Transporte Bienes vía terrestre – punto de origen
147
+ '3009', # Detracciones: Transporte Bienes vía terrestre – punto destino
148
+ '3010',
149
+ # Detracciones: Transporte Bienes vía terrestre – valor referencial
150
+ # preliminar
151
+ '4000', # Beneficio hospedajes: Código País de emisión del pasaporte
152
+ '4001',
153
+ # Beneficio hospedajes: Código País de residencia del sujeto no
154
+ # domiciliado
155
+ '4002', # Beneficio Hospedajes: Fecha de ingreso al país
156
+ '4003', # Beneficio Hospedajes: Fecha de ingreso al establecimiento
157
+ '4004', # Beneficio Hospedajes: Fecha de salida del establecimiento
158
+ '4005', # Beneficio Hospedajes: Número de días de permanencia
159
+ '4006', # Beneficio Hospedajes: Fecha de consumo
160
+ '4007',
161
+ # Beneficio Hospedajes: Paquete turístico - Nombres y Apellidos del
162
+ # Huésped
163
+ '4008',
164
+ # Beneficio Hospedajes: Paquete turístico – Tipo documento identidad del
165
+ # huésped
166
+ '4009'
167
+ # Beneficio Hospedajes: Paquete turístico – Numero de documento identidad
168
+ # de huésped
169
+ ].freeze
170
+
171
+ # Tipo de Precio de Venta Unitario
172
+ CATALOG_16 = [
173
+ '01', # Precio unitario (incluye el IGV)
174
+ '02' # Valor referencial unitario en operaciones no onerosas
175
+ ].freeze
176
+ end
177
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'zip'
4
+ require 'savon'
5
+
6
+ module SunatInvoice
7
+ class Client
8
+ def initialize(env = 'dev')
9
+ @env = env
10
+ @soap_client = savon_client
11
+ end
12
+
13
+ def log
14
+ (@env != 'prod')
15
+ end
16
+
17
+ private
18
+
19
+ def config
20
+ SunatInvoice.configuration
21
+ end
22
+
23
+ def authentication
24
+ login = config.account_ruc + config.account_user
25
+ password = config.account_password
26
+ [login, password]
27
+ end
28
+
29
+ def savon_client
30
+ Savon.client(wsdl: wsdl,
31
+ wsse_auth: authentication,
32
+ namespace: 'http://service.sunat.gob.pe',
33
+ log: log)
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SunatInvoice
4
+ class ConsultClient < Client
5
+ # consult CDR and ticket state
6
+
7
+ def wsdl
8
+ 'https://www.sunat.gob.pe/ol-it-wsconscpegem/billConsultService?wsdl'
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SunatInvoice
4
+ class InvoiceClient < Client
5
+ def wsdl
6
+ send("#{@env}_server")
7
+ end
8
+
9
+ def prod_server
10
+ 'https://e-factura.sunat.gob.pe/ol-ti-itcpfegem/billService?wsdl'
11
+ end
12
+
13
+ def dev_server
14
+ 'https://e-beta.sunat.gob.pe/ol-ti-itcpfegem-beta/billService?wsdl'
15
+ end
16
+
17
+ def prepare_zip(invoice, name)
18
+ # * invoice - xml document to zip
19
+ # * name - xml document name
20
+ zip_file = Zip::OutputStream.write_buffer do |zip|
21
+ zip.put_next_entry name
22
+ zip.write invoice
23
+ end
24
+ zip_file.rewind
25
+ encode(zip_file.sysread)
26
+ end
27
+
28
+ def send_invoice(invoice, name)
29
+ zip = prepare_zip(invoice, "#{name}.xml")
30
+ @soap_client.call(:send_bill,
31
+ message: { fileName: "#{name}.zip", contentFile: zip })
32
+ end
33
+
34
+ private
35
+
36
+ def encode(zip_str)
37
+ Base64.encode64(zip_str)
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SunatInvoice
4
+ class ValidateClient < Client
5
+ # verify invoice generated
6
+
7
+ def wsdl
8
+ 'https://www.sunat.gob.pe/ol-it-wsconsvalidcpe/billValidService?wsdl'
9
+ end
10
+
11
+ def verify_file(invoice, name)
12
+ cli = savon_client(valid_server)
13
+ cli.call(:verifica_cp_earchivo, message: {
14
+ nombre: name, archivo: invoice
15
+ })
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SunatInvoice
4
+ class Configuration
5
+ attr_accessor :account_ruc, :account_user, :account_password, :provider
6
+ end
7
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+ require_relative 'tributer'
3
+
4
+ module SunatInvoice
5
+ class Customer < Tributer
6
+ def info(xml)
7
+ xml['cac'].AccountingCustomerParty do
8
+ xml['cbc'].CustomerAssignedAccountID @ruc
9
+ xml['cbc'].AdditionalAccountID @document_type
10
+ xml['cac'].Party do
11
+ xml['cac'].PartyLegalEntity do
12
+ xml['cbc'].RegistrationName @name
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,171 @@
1
+ # frozen_string_literal: false
2
+ require_relative 'utils'
3
+ require_relative 'provider'
4
+ require_relative 'customer'
5
+ require_relative 'signature'
6
+ require_relative 'tax'
7
+ require_relative 'catalogs'
8
+
9
+ module SunatInvoice
10
+ class Invoice < Model
11
+ include Utils
12
+
13
+ attr_accessor :document_type, :document_number, :items
14
+
15
+ UBL_VERSION = '2.0'
16
+ CUSTOMIZATION = '1.0'
17
+
18
+ def initialize(*args)
19
+ super(*args)
20
+ opts = args[0] || {}
21
+ @provider = opts[:provider] || SunatInvoice::Provider.new
22
+ @customer = opts[:customer] || SunatInvoice::Customer.new
23
+ @date = opts[:date] || DateTime.now.strftime('%Y-%m-%d')
24
+ @document_type = opts[:document_type] || '01'
25
+ @document_number = opts[:document_number] || 'F001-1'
26
+ @currency = opts[:currency] || 'PEN'
27
+ @items ||= []
28
+ @signature = SunatInvoice::Signature.new(provider: @provider)
29
+ end
30
+
31
+ def xml
32
+ prepare_totals
33
+
34
+ build = Nokogiri::XML::Builder.new do |xml|
35
+ xml.Invoice(UBL_NAMESPACES) do
36
+ xml['ext'].UBLExtensions do
37
+ build_sale_totals(xml)
38
+ @signature.signature_ext(xml)
39
+ end
40
+
41
+ xml['cbc'].UBLVersionID UBL_VERSION
42
+ xml['cbc'].CustomizationID CUSTOMIZATION
43
+ xml['cbc'].ID @document_number
44
+ xml['cbc'].IssueDate @date
45
+ xml['cbc'].InvoiceTypeCode @document_type
46
+ xml['cbc'].DocumentCurrencyCode @currency
47
+
48
+ @signature.signer_data(xml)
49
+ @provider.info(xml)
50
+ @customer.info(xml)
51
+ build_tax_totals(xml)
52
+ build_total(xml)
53
+ build_items(xml)
54
+ end
55
+ end
56
+ invoice_xml = build.to_xml
57
+ @signature.sign(invoice_xml)
58
+ end
59
+
60
+ def prepare_totals
61
+ calculate_tax_totals
62
+ calculate_sale_totals
63
+ calculate_total
64
+ end
65
+
66
+ def calculate_total
67
+ # calculate invoice total
68
+ @total = 0
69
+ @total += @tax_totals.values.sum
70
+ @total += @sale_totals.values.sum
71
+ end
72
+
73
+ def calculate_sale_totals
74
+ @sale_totals = {}
75
+ # get bi totals according kind of sale (gravado, inafecto, exonerado ..)
76
+ items.each do |item|
77
+ # TODO: I think in most cases only be one tax for item, but should
78
+ # handle more cases
79
+ total_code = get_total_code(item.taxes.first)
80
+ if total_code
81
+ @sale_totals[total_code] = 0 unless @sale_totals[total_code]
82
+ @sale_totals[total_code] += item.bi_value
83
+ end
84
+ end
85
+ end
86
+
87
+ def calculate_tax_totals
88
+ # concat item's sale_taxes
89
+ @tax_totals = {}
90
+ taxes = items.map(&:sale_taxes).flatten
91
+ taxes.each do |tax|
92
+ @tax_totals[tax.keys.first] ||= 0
93
+ @tax_totals[tax.keys.first] += tax.values.sum
94
+ end
95
+ end
96
+
97
+ def build_items(xml)
98
+ items.each_with_index do |item, index|
99
+ item.xml(xml, index, @currency)
100
+ end
101
+ end
102
+
103
+ def document_name
104
+ "#{@provider.ruc}-#{document_type}-#{document_number}"
105
+ end
106
+
107
+ def build_tax_totals(xml)
108
+ @tax_totals.each do |key, value|
109
+ SunatInvoice::Tax.new(tax_type: key, amount: value).xml(xml, @currency)
110
+ end
111
+ end
112
+
113
+ def build_sale_totals(xml)
114
+ ubl_ext(xml) do
115
+ xml['sac'].AdditionalInformation do
116
+ @sale_totals.each do |code, amount|
117
+ xml['sac'].AdditionalMonetaryTotal do
118
+ xml['cbc'].ID code
119
+ amount_xml(xml['cbc'], 'PayableAmount', amount, @currency)
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end
125
+
126
+ def build_total(xml)
127
+ xml['cac'].LegalMonetaryTotal do
128
+ amount_xml(xml['cbc'], 'PayableAmount', @total, @currency)
129
+ end
130
+ end
131
+
132
+ def add_item(item)
133
+ items << item if item.is_a?(SunatInvoice::Item)
134
+ end
135
+
136
+ def get_total_code(tax)
137
+ return unless tax
138
+ case tax.tax_type
139
+ when :igv
140
+ get_total_igv_code(tax.tax_exemption_reason)
141
+ # when :isc
142
+ # tax.tier_range
143
+ end
144
+ end
145
+
146
+ def get_total_igv_code(exemption_reason)
147
+ if Catalogs::CATALOG_07.first == exemption_reason
148
+ Catalogs::CATALOG_14.first
149
+ elsif Catalogs::CATALOG_07[1..6].include?(exemption_reason)
150
+ Catalogs::CATALOG_14[3]
151
+ elsif Catalogs::CATALOG_07[7] == exemption_reason
152
+ Catalogs::CATALOG_14[2]
153
+ elsif Catalogs::CATALOG_07[8..14].include?(exemption_reason)
154
+ Catalogs::CATALOG_14[1]
155
+ end
156
+ end
157
+
158
+ def attributes
159
+ {
160
+ attributes!: {
161
+ 'cbc:InvoicedQuantity' => { '@unitCode' => :unit },
162
+ 'cbc:PriceAmount' => { '@currencyID' => :currency }, # PEN
163
+ 'cbc:TaxAmount' => { '@currencyID' => :currency }, # PEN
164
+ 'cbc:PayableAmount' => { '@currencyID' => :currency }, # PEN
165
+ 'cbc:LineExtensionAmount' => { '@currencyID' => :currency }, # PEN
166
+ 'cbc:ChargeTotalAmount' => { '@currencyID' => :currency }, # PEN
167
+ }
168
+ }
169
+ end
170
+ end
171
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+ require_relative 'utils'
3
+
4
+ module SunatInvoice
5
+ class Item < Model
6
+ include Utils
7
+
8
+ attr_accessor :quantity, :description, :price, :price_code, :unit_code,
9
+ :taxes
10
+
11
+ def initialize(*args)
12
+ # * quantity - quantity of item
13
+ # * description - name or description of product or service
14
+ # * price - unit price without taxes
15
+ # * price_code - type unit price (Catalogs::CATALOG_16)
16
+ # * unit_code - unit of measure
17
+ # UN/ECE rec 20- Unit Of Measure
18
+ # http://www.unece.org/fileadmin/DAM/cefact/recommendations/rec20/rec20_rev3_Annex2e.pdf
19
+ # * taxes - An array of SunatInvoice::Tax
20
+ super(*args)
21
+ @taxes ||= []
22
+ end
23
+
24
+ def bi_value
25
+ # bi of sale = price without taxes * quantity
26
+ (@price.to_f * @quantity.to_f).round(2)
27
+ end
28
+
29
+ def sale_taxes
30
+ # generate and object with taxes sum by type
31
+ sums = {}
32
+ taxes.each do |tax|
33
+ sums[tax.tax_type] ||= 0
34
+ sums[tax.tax_type] = (tax.amount.to_f * quantity.to_f).round(2)
35
+ end
36
+ sums
37
+ end
38
+
39
+ def sale_price
40
+ # unit price with tax
41
+ (@price.to_f + sum_taxes).round(2)
42
+ end
43
+
44
+ def sum_taxes
45
+ taxes.map(&:amount).sum
46
+ end
47
+
48
+ def xml(xml, index, currency)
49
+ xml['cac'].InvoiceLine do
50
+ xml['cbc'].ID(index + 1)
51
+ xml['cbc'].InvoicedQuantity(@quantity, unitCode: unit_code)
52
+ amount_xml(xml['cbc'], 'LineExtensionAmount', bi_value, currency)
53
+ xml['cac'].PricingReference do
54
+ xml['cac'].AlternativeConditionPrice do
55
+ amount_xml(xml['cbc'], 'PriceAmount', sale_price, currency)
56
+ xml['cbc'].PriceTypeCode price_code
57
+ end
58
+ end
59
+ taxes_xml(xml, currency)
60
+ build_item(xml)
61
+ xml['cac'].Price do
62
+ amount_xml(xml['cbc'], 'PriceAmount', price, currency)
63
+ end
64
+ end
65
+ end
66
+
67
+ def build_item(xml)
68
+ xml['cac'].Item do
69
+ xml['cbc'].Description description
70
+ end
71
+ end
72
+
73
+ def taxes_xml(xml, currency)
74
+ taxes&.each do |tax|
75
+ tax.xml(xml, currency)
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SunatInvoice
4
+ class Model
5
+ def initialize(options = {})
6
+ options.each do |key, value|
7
+ send("#{key}=", value) if respond_to?("#{key}=")
8
+ end
9
+ end
10
+ end
11
+ end