ubl 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
+ SHA256:
3
+ metadata.gz: 1778b4c23b585bf9a2bb5b783aee65d9104d1f9ade0ba7a20fdab7c18aced7de
4
+ data.tar.gz: 7d6c635cc5a0e6fe6e64c9f103705611d6edf9bc7cbebb04222c0c7c37176f02
5
+ SHA512:
6
+ metadata.gz: f6a1306eddf09c6a7003f10c394e8702b95e28af953c4a826ae33dbaac0a33492d07033881898e5b1d9f59577db158802bf466b494c9e4e5bd5bbd82a8ac4ae6
7
+ data.tar.gz: 5b5dcb7717fbacc50e2e1101c786a1d270c1358af163c4ebb694336c0ca3cf05496c49134bbdc88b975f91f738f476477715e0ca792f867955f222f603d7e251
data/.standard.yml ADDED
@@ -0,0 +1,3 @@
1
+ # For available configuration options, see:
2
+ # https://github.com/standardrb/standard
3
+ ruby_version: 3.1
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 roel4d
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,35 @@
1
+ # Ubl
2
+
3
+ Generate UBL invoices and credit notes for Peppol
4
+
5
+ ## installation
6
+
7
+ install the gem and add to the application's gemfile by executing:
8
+
9
+ ```bash
10
+ bundle add ubl
11
+ ```
12
+
13
+ if bundler is not being used to manage dependencies, install the gem by executing:
14
+
15
+ ```bash
16
+ gem install ubl
17
+ ```
18
+
19
+ ## usage
20
+
21
+ todo: write usage instructions here
22
+
23
+ ## development
24
+
25
+ after checking out the repo, run `bin/setup` to install dependencies. then, run `rake test` to run the tests. you can also run `bin/console` for an interactive prompt that will allow you to experiment.
26
+
27
+ to install this gem onto your local machine, run `bundle exec rake install`. to release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
28
+
29
+ ## contributing
30
+
31
+ bug reports and pull requests are welcome on github at https://github.com/roel4d/ubl.
32
+
33
+ ## license
34
+
35
+ the gem is available as open source under the terms of the [mit license](https://opensource.org/licenses/mit).
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "minitest/test_task"
5
+
6
+ Minitest::TestTask.create
7
+
8
+ require "standard/rake"
9
+
10
+ task default: %i[test standard]
@@ -0,0 +1,29 @@
1
+ require_relative "ubl/builder"
2
+
3
+ module Ubl
4
+ class CreditNote < UblBuilder
5
+ def initialize(ubl_be)
6
+ super
7
+ end
8
+
9
+ def build
10
+ builder = Nokogiri::XML::Builder.new(encoding: "UTF-8") do |xml|
11
+ xml.CreditNote(namespaces.merge("xmlns" => "urn:oasis:names:specification:ubl:schema:xsd:CreditNote-2")) do
12
+ build_header(xml) do |xml|
13
+ xml["cbc"].CreditNoteTypeCode "381"
14
+ end
15
+
16
+ build_document_reference(xml, "CreditNote")
17
+
18
+ build_party(xml, @supplier, "AccountingSupplierParty")
19
+ build_party(xml, @customer, "AccountingCustomerParty")
20
+
21
+ build_tax_total(xml)
22
+ build_monetary_total(xml)
23
+ build_invoice_lines(xml)
24
+ end
25
+ end
26
+ builder.to_xml
27
+ end
28
+ end
29
+ end
data/lib/invoice.rb ADDED
@@ -0,0 +1,29 @@
1
+ require_relative "ubl/builder"
2
+
3
+ module Ubl
4
+ class Invoice < UblBuilder
5
+ def initialize(ubl_be)
6
+ super
7
+ end
8
+
9
+ def build
10
+ builder = Nokogiri::XML::Builder.new(encoding: "UTF-8") do |xml|
11
+ xml.Invoice(namespaces) do
12
+ build_header(xml) do |xml|
13
+ xml["cbc"].InvoiceTypeCode "380"
14
+ end
15
+
16
+ build_document_reference(xml, "CommercialInvoice")
17
+
18
+ build_party(xml, @supplier, "AccountingSupplierParty")
19
+ build_party(xml, @customer, "AccountingCustomerParty")
20
+
21
+ build_tax_total(xml)
22
+ build_monetary_total(xml)
23
+ build_invoice_lines(xml)
24
+ end
25
+ end
26
+ builder.to_xml
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,261 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "nokogiri"
4
+ require "date"
5
+ require "base64"
6
+
7
+ module Ubl
8
+ class Error < StandardError; end
9
+
10
+ class UblBuilder
11
+ attr_accessor :invoice_nr, :issue_date, :due_date, :currency, :supplier,
12
+ :customer, :invoice_lines, :tax_total, :legal_monetary_total, :pdffile
13
+
14
+ CUSTOMIZATION_ID = "urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0"
15
+ CUSTOMIZATION_UBL_BE = "urn:cen.eu:en16931:2017#conformant#urn:UBL.BE:1.0.0.20180214"
16
+ PROFILE_ID = "urn:fdc:peppol.eu:2017:poacc:billing:01:1.0"
17
+
18
+ def initialize(ubl_be = false)
19
+ @ubl_be = ubl_be
20
+ @issue_date = Date.today
21
+ @due_date = @issue_date + 30
22
+ @currency = "EUR"
23
+ @attachments = []
24
+ @invoice_lines = []
25
+ @tax_total = 0
26
+ @legal_monetary_total = 0
27
+ end
28
+
29
+ def namespaces
30
+ {
31
+ "xmlns" => "urn:oasis:names:specification:ubl:schema:xsd:Invoice-2",
32
+ "xmlns:cac" => "urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2",
33
+ "xmlns:cbc" => "urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2"
34
+ }
35
+ end
36
+
37
+ def add_supplier(name:, country:, vat_id: nil, address: nil, city: nil, postal_code: nil)
38
+ @supplier = {
39
+ name: name,
40
+ country: country,
41
+ vat_id: vat_id,
42
+ address: address,
43
+ city: city,
44
+ postal_code: postal_code
45
+ }
46
+ end
47
+
48
+ def add_customer(name:, country:, vat_id: nil, address: nil, city: nil, postal_code: nil)
49
+ @customer = {
50
+ name: name,
51
+ country: country,
52
+ vat_id: vat_id,
53
+ address: address,
54
+ city: city,
55
+ postal_code: postal_code
56
+ }
57
+ end
58
+
59
+ def add_line(name:, quantity:, unit_price:, tax_rate: 21.0, unit: "ZZ")
60
+ line_extension_amount = (quantity * unit_price).round(2)
61
+ tax_amount = (line_extension_amount * (tax_rate / 100.0)).round(2)
62
+
63
+ @invoice_lines << {
64
+ id: (@invoice_lines.length + 1).to_s,
65
+ name: name,
66
+ quantity: quantity,
67
+ unit: unit,
68
+ unit_price: unit_price,
69
+ line_extension_amount: line_extension_amount,
70
+ tax_rate: tax_rate,
71
+ tax_amount: tax_amount
72
+ }
73
+
74
+ calculate_totals
75
+ end
76
+
77
+ private
78
+
79
+ def calculate_totals
80
+ line_extension_amount = @invoice_lines.sum { |line| line[:line_extension_amount] }
81
+ @tax_total = @invoice_lines.sum { |line| line[:tax_amount] }
82
+ @legal_monetary_total = line_extension_amount + @tax_total
83
+ end
84
+
85
+ def build_header(xml)
86
+ xml["cbc"].CustomizationID @ubl_be ? CUSTOMIZATION_UBL_BE : CUSTOMIZATION_ID
87
+ xml["cbc"].ProfileID PROFILE_ID
88
+ xml["cbc"].ID @invoice_nr
89
+ xml["cbc"].IssueDate @issue_date.to_s
90
+ xml["cbc"].DueDate @due_date.to_s
91
+ yield xml
92
+ xml["cbc"].DocumentCurrencyCode @currency
93
+ xml["cac"].OrderReference do
94
+ xml["cbc"].ID @invoice_nr
95
+ end
96
+ end
97
+
98
+ def build_document_reference(xml, description)
99
+ if @ubl_be
100
+ xml["cac"].AdditionalDocumentReference do
101
+ xml["cbc"].ID "UBL.BE"
102
+ xml["cbc"].DocumentDescription description
103
+ end
104
+ end
105
+
106
+ if @pdffile
107
+ content = Base64.strict_encode64(File.binread(@pdffile))
108
+
109
+ xml["cac"].AdditionalDocumentReference do
110
+ xml["cbc"].ID @invoice_nr
111
+ xml["cbc"].DocumentDescription "PDF"
112
+ xml["cac"].Attachment do
113
+ xml["cbc"].EmbeddedDocumentBinaryObject(mimeCode: "application/pdf", filename: File.basename(@pdffile)) { xml.text content }
114
+ end
115
+ end
116
+ end
117
+ end
118
+
119
+ def add_attachment(id, filename)
120
+ @attachments << {id: id, content:}
121
+ end
122
+
123
+ def build_party(xml, party_data, party_type)
124
+ return unless party_data
125
+
126
+ xml["cac"].send(party_type) do
127
+ xml["cac"].Party do
128
+ xml["cbc"].EndpointID(schemeID: "0208") { xml.text party_data[:vat_id].gsub(/^[A-Za-z]+/, "") }
129
+
130
+ if party_data[:address]
131
+ xml["cac"].PostalAddress do
132
+ xml["cbc"].StreetName party_data[:address] if party_data[:address]
133
+ xml["cbc"].CityName party_data[:city] if party_data[:city]
134
+ xml["cbc"].PostalZone party_data[:postal_code] if party_data[:postal_code]
135
+ xml["cac"].Country do
136
+ xml["cbc"].IdentificationCode party_data[:country]
137
+ end
138
+ end
139
+ end
140
+
141
+ if party_data[:vat_id]
142
+ xml["cac"].PartyTaxScheme do
143
+ xml["cbc"].CompanyID party_data[:vat_id]
144
+ xml["cac"].TaxScheme do
145
+ xml["cbc"].ID "VAT"
146
+ end
147
+ end
148
+ end
149
+
150
+ xml["cac"].PartyLegalEntity do
151
+ xml["cbc"].RegistrationName party_data[:name]
152
+ end
153
+ end
154
+ end
155
+ end
156
+
157
+ def build_invoice_lines(xml)
158
+ @invoice_lines.each do |line|
159
+ xml["cac"].InvoiceLine do
160
+ xml["cbc"].ID line[:id]
161
+ xml["cbc"].InvoicedQuantity(unitCode: line[:unit]) { xml.text line[:quantity] }
162
+ xml["cbc"].LineExtensionAmount(currencyID: @currency) { xml.text sprintf("%.2f", line[:line_extension_amount]) }
163
+
164
+ if @ubl_be
165
+ xml["cac"].TaxTotal do
166
+ xml["cbc"].TaxAmount(currencyID: @currency) { xml.text line[:tax_amount] }
167
+ end
168
+ end
169
+
170
+ xml["cac"].Item do
171
+ xml["cbc"].Name line[:name]
172
+ xml["cac"].ClassifiedTaxCategory do
173
+ xml["cbc"].ID get_tax_category_id(line[:tax_rate])
174
+ xml["cbc"].Name get_tax_category_name(line[:tax_rate]) if @ubl_be
175
+ xml["cbc"].Percent line[:tax_rate]
176
+ xml["cac"].TaxScheme do
177
+ xml["cbc"].ID "VAT"
178
+ end
179
+ end
180
+ end
181
+
182
+ xml["cac"].Price do
183
+ xml["cbc"].PriceAmount(currencyID: @currency) { xml.text sprintf("%.2f", line[:unit_price]) }
184
+ end
185
+ end
186
+ end
187
+ end
188
+
189
+ def get_tax_category_name(tax_rate)
190
+ case tax_rate
191
+ when 0, "0%"
192
+ "00"
193
+ when 6, "6%"
194
+ "01"
195
+ when 12, "12%"
196
+ "02"
197
+ when 21, "21%"
198
+ "03"
199
+ else
200
+ "00" # Default to 0% category
201
+ end
202
+ end
203
+
204
+ def get_tax_category_id(tax_rate)
205
+ case tax_rate
206
+ when 0, "0%"
207
+ "Z"
208
+ when 6, "6%"
209
+ "S"
210
+ when 12, "12%"
211
+ "S"
212
+ when 21, "21%"
213
+ "S"
214
+ else
215
+ "Z" # Default to 0% category
216
+ end
217
+ end
218
+
219
+ def build_tax_total(xml)
220
+ return if @invoice_lines.empty?
221
+
222
+ xml["cac"].TaxTotal do
223
+ xml["cbc"].TaxAmount(currencyID: @currency) { xml.text sprintf("%.2f", @tax_total) }
224
+
225
+ # Group by tax rate
226
+ tax_groups = @invoice_lines.group_by { |line| line[:tax_rate] }
227
+
228
+ tax_groups.each do |tax_rate, lines|
229
+ taxable_amount = lines.sum { |line| line[:line_extension_amount] }
230
+ tax_amount = lines.sum { |line| line[:tax_amount] }
231
+
232
+ xml["cac"].TaxSubtotal do
233
+ xml["cbc"].TaxableAmount(currencyID: @currency) { xml.text sprintf("%.2f", taxable_amount) }
234
+ xml["cbc"].TaxAmount(currencyID: @currency) { xml.text sprintf("%.2f", tax_amount) }
235
+ xml["cac"].TaxCategory do
236
+ xml["cbc"].ID get_tax_category_id(tax_rate)
237
+ xml["cbc"].Name get_tax_category_name(tax_rate) if @ubl_be
238
+ xml["cbc"].Percent tax_rate
239
+ xml["cac"].TaxScheme do
240
+ xml["cbc"].ID "VAT"
241
+ end
242
+ end
243
+ end
244
+ end
245
+ end
246
+ end
247
+
248
+ def build_monetary_total(xml)
249
+ return if @invoice_lines.empty?
250
+
251
+ line_extension_amount = @invoice_lines.sum { |line| line[:line_extension_amount] }
252
+
253
+ xml["cac"].LegalMonetaryTotal do
254
+ xml["cbc"].LineExtensionAmount(currencyID: @currency) { xml.text sprintf("%.2f", line_extension_amount) }
255
+ xml["cbc"].TaxExclusiveAmount(currencyID: @currency) { xml.text sprintf("%.2f", line_extension_amount) }
256
+ xml["cbc"].TaxInclusiveAmount(currencyID: @currency) { xml.text sprintf("%.2f", @legal_monetary_total) }
257
+ xml["cbc"].PayableAmount(currencyID: @currency) { xml.text sprintf("%.2f", @legal_monetary_total) }
258
+ end
259
+ end
260
+ end
261
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ubl
4
+ VERSION = "0.0.1"
5
+ end
data/sig/ubl.rbs ADDED
@@ -0,0 +1,4 @@
1
+ module Ubl
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,65 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ubl
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - roel4d
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: nokogiri
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '1.18'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '1.18'
26
+ description: Generate UBL documents for Peppol
27
+ email:
28
+ - roel4d@webding.org
29
+ executables: []
30
+ extensions: []
31
+ extra_rdoc_files: []
32
+ files:
33
+ - ".standard.yml"
34
+ - LICENSE.txt
35
+ - README.md
36
+ - Rakefile
37
+ - lib/credit_note.rb
38
+ - lib/invoice.rb
39
+ - lib/ubl/builder.rb
40
+ - lib/ubl/version.rb
41
+ - sig/ubl.rbs
42
+ homepage: https://github.com/roel4d/ubl
43
+ licenses:
44
+ - MIT
45
+ metadata:
46
+ homepage_uri: https://github.com/roel4d/ubl
47
+ source_code_uri: https://github.com/roel4d/invoice
48
+ rdoc_options: []
49
+ require_paths:
50
+ - lib
51
+ required_ruby_version: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: 3.1.0
56
+ required_rubygems_version: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ requirements: []
62
+ rubygems_version: 3.6.7
63
+ specification_version: 4
64
+ summary: Generate UBL documents
65
+ test_files: []