sunat_invoice 0.0.1

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