document_types-invoice 0.1.0

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: 2e660ac1529491297fe68d1e079e492bac8da450207ac135ad5c2ec3d9c54ea1
4
+ data.tar.gz: 53ce6371951956104ccea6fdbcfdfdf4440da1b7b93df85e66b0143632a4c2f7
5
+ SHA512:
6
+ metadata.gz: 14758fa7f8e3b6b6f4a553eceda637815c1dc80344f32505fb00c49ca044ec58ee697b77ae7bd440a1d7bf71268e254a1e8e4858f383926fa493048f07e4b575
7
+ data.tar.gz: 0bda661a6725f84624ae0447327cbf686972f50e497d8a3cbd694c04f47a33007c5f5451203e3a9d2cb3a5954dfb6cea746b9d82836c199579dbfcf5870943de
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 plugandwork
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,82 @@
1
+ # DocumentTypes::Invoice
2
+
3
+ A gem that adds invoice document type support to plugandwork application.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'document_types-invoice'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ ```bash
16
+ $ bundle install
17
+ ```
18
+
19
+ ## Prerequisites
20
+
21
+ This gem requires the following infrastructure in your application:
22
+
23
+ 1. A `DocumentTypes::Registry` class for document type registration
24
+ 2. A `DocumentTypable` concern included in your `Doc` model
25
+ 3. Mongoid as the database ORM
26
+
27
+ ## Features
28
+
29
+ - Invoice document type with specific fields (invoice number, dates, amounts, etc.)
30
+ - Automatic detection of invoices from PDF and XML files
31
+ - Support for structured invoice formats (Factur-X, UBL, CII)
32
+ - Invoice-specific search functionality
33
+ - Business logic methods for invoice processing
34
+
35
+ ## Usage
36
+
37
+ ### Search for invoices
38
+
39
+ ```ruby
40
+ # Search invoices by amount
41
+ invoices = DocumentTypes::InvoiceService.search(min_amount: 1000, max_amount: 5000)
42
+
43
+ # Get unpaid invoices
44
+ unpaid = DocumentTypes::InvoiceService.unpaid_invoices
45
+
46
+ # Get overdue invoices
47
+ overdue = DocumentTypes::InvoiceService.overdue_invoices
48
+ ```
49
+
50
+ ### Convert a document to an invoice
51
+
52
+ ```ruby
53
+ # Detect if a document is an invoice
54
+ if DocumentTypes::InvoiceRecord.detect(my_doc)
55
+ # Process as an invoice
56
+ invoice = my_doc.as_invoice
57
+ puts "Invoice: #{invoice.invoice_number}, Amount: #{invoice.total_amount} #{invoice.currency}"
58
+ end
59
+ ```
60
+
61
+ ### Get invoice statistics
62
+
63
+ ```ruby
64
+ # Get statistics for the current month
65
+ stats = DocumentTypes::InvoiceService.stats_for_current_month
66
+
67
+ puts "Total: #{stats[:total]}"
68
+ puts "Average: #{stats[:average]}"
69
+ puts "Count: #{stats[:count]}"
70
+ ```
71
+
72
+ ## Development
73
+
74
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests.
75
+
76
+ ## Contributing
77
+
78
+ Bug reports and pull requests are welcome on GitHub.
79
+
80
+ ## License
81
+
82
+ The gem is available as open source under the terms of the MIT License.
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task default: :spec
@@ -0,0 +1,842 @@
1
+ module DocumentTypes
2
+ class InvoiceRecord
3
+ include Mongoid::Document
4
+ include Mongoid::Timestamps
5
+
6
+ # Indexation of fields for search
7
+ index({ invoice_number: 1 })
8
+ index({ issue_date: 1 })
9
+ index({ total_amount: 1 })
10
+ index({ seller_name: 1 })
11
+ index({ buyer_name: 1 })
12
+ index({ payment_status: 1 })
13
+ index({ payment_due_date: 1 })
14
+ index({ doc_id: 1 })
15
+
16
+ # Invoice-specific fields
17
+ field :invoice_number, type: String
18
+ field :issue_date, type: Date
19
+ field :document_type, type: String # Invoice type: standard, credit note, proforma...
20
+
21
+ # Seller information
22
+ field :seller_name, type: String
23
+ field :seller_tax_id, type: String
24
+ field :seller_address, type: String
25
+
26
+ # Buyer information
27
+ field :buyer_name, type: String
28
+ field :buyer_tax_id, type: String
29
+ field :buyer_address, type: String
30
+
31
+ # Amounts
32
+ field :net_amount, type: Float
33
+ field :tax_amount, type: Float
34
+ field :total_amount, type: Float
35
+ field :currency, type: String, default: "EUR"
36
+
37
+ # Electronic format
38
+ field :electronic_format, type: String # factur_x, ubl, cii, etc.
39
+
40
+ # Payment status
41
+ field :payment_status, type: String, default: "unpaid" # unpaid, partial, paid
42
+ field :payment_due_date, type: Date
43
+ field :payment_method, type: String
44
+
45
+ # Line items (stored as sub-document)
46
+ field :line_items, type: Array, default: []
47
+
48
+ # Relation to original document
49
+ belongs_to :doc, class_name: "Doc", index: true
50
+
51
+ # Validation methods
52
+ validates :doc, presence: true
53
+ validates :invoice_number, presence: true, if: -> { electronic_format.present? }
54
+
55
+ # Callbacks
56
+ after_save :update_doc
57
+
58
+ # Class methods
59
+
60
+ # Detect if a document is an invoice
61
+ def self.detect(doc)
62
+ # Check if type is already defined
63
+ return true if doc.document_type == 'invoice'
64
+
65
+ # Detection based on extension and content
66
+ doc.to_temp_file do |tf|
67
+ file_ext = doc.file_ext.to_s.downcase
68
+ file_path = tf.path
69
+
70
+ case file_ext
71
+ when 'pdf'
72
+ return detect_facturx(file_path)
73
+ when 'xml'
74
+ return detect_ubl(file_path) || detect_cii(file_path)
75
+ end
76
+ end
77
+
78
+ # Detection based on existing superfields
79
+ if doc.superfields.present?
80
+ invoice_indicators = [
81
+ doc.superfields['invoice_number'],
82
+ doc.superfields['document_number'],
83
+ doc.superfields['amounts_total'],
84
+ doc.superfields['amounts_grand_total']
85
+ ]
86
+
87
+ return true if invoice_indicators.compact.any?
88
+ end
89
+
90
+ # Detection based on OCR or text content (for unstructured PDFs)
91
+ if doc.text_parts.present?
92
+ invoice_keywords = %w[facture invoice rechnung factura fattura montant amount total tva vat tax client customer]
93
+
94
+ text_content = doc.text_parts.map(&:text).join(' ').downcase
95
+ matched_keywords = invoice_keywords.count { |keyword| text_content.include?(keyword) }
96
+
97
+ # If at least 3 keywords are found, consider as invoice
98
+ return true if matched_keywords >= 3
99
+ end
100
+
101
+ false
102
+ end
103
+
104
+ # Detect if a PDF is in Factur-X format
105
+ def self.detect_facturx(file_path)
106
+ begin
107
+ require 'pdf-reader'
108
+
109
+ # Method 1: Check XMP metadata
110
+ reader = PDF::Reader.new(file_path)
111
+ if reader.metadata.present?
112
+ metadata_string = reader.metadata.to_s
113
+ metadata_check = metadata_string.include?('urn:factur-x:pdfa:CrossIndustryDocument:invoice:1p0#') ||
114
+ metadata_string.include?('factur-x') ||
115
+ metadata_string.include?('zugferd')
116
+
117
+ return true if metadata_check
118
+ end
119
+
120
+ # Method 2: Check embedded XML files
121
+ io = File.open(file_path, 'rb')
122
+ content = io.read
123
+ io.close
124
+
125
+ facturx_pattern = /\/EmbeddedFile\s+(.+?(factur-x\.xml|zugferd-invoice\.xml|metadata\.xml).+?)\s+endobj/im
126
+ return content.match(facturx_pattern).present?
127
+ rescue => e
128
+ Rails.logger.error "Error detecting Factur-X: #{e.message}"
129
+ return false
130
+ end
131
+ end
132
+
133
+ # Detect if an XML is in UBL format
134
+ def self.detect_ubl(file_path)
135
+ begin
136
+ require 'nokogiri'
137
+
138
+ xml = File.read(file_path)
139
+ doc = Nokogiri::XML(xml)
140
+
141
+ # Check UBL namespaces
142
+ ubl_ns = %w[urn:oasis:names:specification:ubl:schema:xsd:Invoice-2 urn:oasis:names:specification:ubl:schema:xsd:CreditNote-2]
143
+
144
+ ubl_ns.each do |ns|
145
+ return true if doc.xpath("//*[namespace-uri()='#{ns}']").any?
146
+ end
147
+
148
+ false
149
+ rescue => e
150
+ Rails.logger.error "Error detecting UBL: #{e.message}"
151
+ false
152
+ end
153
+ end
154
+
155
+ # Detect if an XML is in CII format
156
+ def self.detect_cii(file_path)
157
+ begin
158
+
159
+ xml = File.read(file_path)
160
+ doc = Nokogiri::XML(xml)
161
+
162
+ # Check CII namespaces
163
+ cii_ns = %w[urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100 urn:un:unece:uncefact:data:standard:CrossIndustryDocument:invoice:1p0#]
164
+
165
+ cii_ns.each do |ns|
166
+ return true if doc.xpath("//*[namespace-uri()='#{ns}']").any?
167
+ end
168
+
169
+ false
170
+ rescue => e
171
+ Rails.logger.error "Error detecting CII: #{e.message}"
172
+ false
173
+ end
174
+ end
175
+
176
+ # Instance methods
177
+
178
+ # Synchronize with the original document
179
+ def sync_with_doc
180
+ # Extract and convert data from the document
181
+ doc.to_temp_file do |tf|
182
+ file_path = tf.path
183
+
184
+ # Based on detected format
185
+ case doc.file_ext.to_s.downcase
186
+ when 'pdf'
187
+ if self.class.detect_facturx(file_path)
188
+ self.electronic_format = 'factur_x'
189
+ extract_facturx_data(file_path)
190
+ else
191
+ # For unstructured PDFs, try to extract via OCR/text analysis
192
+ extract_from_text
193
+ end
194
+ when 'xml'
195
+ if self.class.detect_ubl(file_path)
196
+ self.electronic_format = 'ubl'
197
+ extract_ubl_data(file_path)
198
+ elsif self.class.detect_cii(file_path)
199
+ self.electronic_format = 'cii'
200
+ extract_cii_data(file_path)
201
+ end
202
+ else
203
+ # For other formats, try to extract from superfields
204
+ extract_from_superfields
205
+ end
206
+ end
207
+
208
+ # Save after synchronization
209
+ save
210
+ end
211
+
212
+ # Update the original document
213
+ def update_doc
214
+ # Update superfields in the original document
215
+ sf = doc.superfields || {}
216
+
217
+ # Update key fields in superfields
218
+ sf['invoice_number'] = self.invoice_number
219
+ sf['issue_date'] = self.issue_date&.iso8601
220
+ sf['seller_name'] = self.seller_name
221
+ sf['buyer_name'] = self.buyer_name
222
+ sf['total_amount'] = self.total_amount
223
+ sf['currency'] = self.currency
224
+
225
+ # Update the document
226
+ doc.superfields = sf
227
+ doc.save
228
+ end
229
+
230
+ # Extract data from Factur-X format
231
+ def extract_facturx_data(file_path)
232
+ begin
233
+ # Read PDF content
234
+ io = File.open(file_path, 'rb')
235
+ content = io.read
236
+ io.close
237
+
238
+ # Look for embedded XML files
239
+ facturx_xml = nil
240
+ content.scan(/\/EmbeddedFile\s+(.+?)\s+endobj/m) do |match|
241
+ attachment = match[0]
242
+ if attachment.include?('factur-x.xml') ||
243
+ attachment.include?('zugferd-invoice.xml') ||
244
+ attachment.include?('metadata.xml')
245
+
246
+ xml_match = attachment.match(/stream\s+(.*?)\s+endstream/m)
247
+ if xml_match
248
+ facturx_xml = xml_match[1]
249
+ break
250
+ end
251
+ end
252
+ end
253
+
254
+ return unless facturx_xml
255
+
256
+ # Parse XML content
257
+ doc = Nokogiri::XML(facturx_xml)
258
+
259
+ # Add namespaces for easier navigation
260
+ namespaces = {
261
+ 'fx' => 'urn:factur-x:pdfa:CrossIndustryDocument:invoice:1p0#',
262
+ 'ram' => 'urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100',
263
+ 'rsm' => 'urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100',
264
+ 'udt' => 'urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100'
265
+ }
266
+
267
+ namespaces.each do |prefix, uri|
268
+ doc.root.add_namespace(prefix, uri) if doc.root
269
+ end
270
+
271
+ # Extract invoice data
272
+ self.document_type = extract_text(doc, '//rsm:ExchangedDocument/ram:TypeCode')
273
+ self.invoice_number = extract_text(doc, '//rsm:ExchangedDocument/ram:ID')
274
+
275
+ # Extract date
276
+ date_text = extract_text(doc, '//rsm:ExchangedDocument/ram:IssueDateTime/udt:DateTimeString')
277
+ if date_text.present?
278
+ # Handle different date formats (YYYYMMDD or ISO)
279
+ if date_text.match(/^\d{8}$/)
280
+ self.issue_date = Date.parse("#{date_text[0..3]}-#{date_text[4..5]}-#{date_text[6..7]}")
281
+ else
282
+ self.issue_date = Date.parse(date_text)
283
+ end
284
+ end
285
+
286
+ # Extract seller info
287
+ self.seller_name = extract_text(doc, '//ram:SellerTradeParty/ram:Name')
288
+ self.seller_tax_id = extract_text(doc, '//ram:SellerTradeParty//ram:ID[../ram:TypeCode="VA"]')
289
+
290
+ # Extract buyer info
291
+ self.buyer_name = extract_text(doc, '//ram:BuyerTradeParty/ram:Name')
292
+ self.buyer_tax_id = extract_text(doc, '//ram:BuyerTradeParty//ram:ID[../ram:TypeCode="VA"]')
293
+
294
+ # Extract amounts
295
+ self.net_amount = extract_text(doc, '//ram:LineTotalAmount').to_f
296
+ self.tax_amount = extract_text(doc, '//ram:TaxTotalAmount').to_f
297
+ self.total_amount = extract_text(doc, '//ram:GrandTotalAmount').to_f
298
+ self.currency = extract_text(doc, '//ram:InvoiceCurrencyCode') || 'EUR'
299
+
300
+ # Extract payment info
301
+ due_date_text = extract_text(doc, '//ram:SpecifiedTradePaymentTerms/ram:DueDateDateTime/udt:DateTimeString')
302
+ if due_date_text.present?
303
+ if due_date_text.match(/^\d{8}$/)
304
+ self.payment_due_date = Date.parse("#{due_date_text[0..3]}-#{due_date_text[4..5]}-#{due_date_text[6..7]}")
305
+ else
306
+ self.payment_due_date = Date.parse(due_date_text)
307
+ end
308
+ end
309
+
310
+ # Extract line items if present
311
+ line_items = []
312
+ doc.xpath('//ram:IncludedSupplyChainTradeLineItem').each do |line_node|
313
+ item = {
314
+ description: extract_text(line_node, './/ram:Name'),
315
+ quantity: extract_text(line_node, './/ram:BilledQuantity').to_f,
316
+ unit_price: extract_text(line_node, './/ram:ChargeAmount').to_f,
317
+ line_total: extract_text(line_node, './/ram:LineTotalAmount').to_f
318
+ }
319
+ line_items << item if item[:description].present?
320
+ end
321
+
322
+ self.line_items = line_items if line_items.any?
323
+
324
+ rescue => e
325
+ Rails.logger.error "Error extracting Factur-X data: #{e.message}"
326
+ end
327
+ end
328
+
329
+ # Extract data from UBL format
330
+ def extract_ubl_data(file_path)
331
+ begin
332
+ xml = File.read(file_path)
333
+ doc = Nokogiri::XML(xml)
334
+
335
+ # Add UBL namespaces
336
+ namespaces = {
337
+ 'ubl' => 'urn:oasis:names:specification:ubl:schema:xsd:Invoice-2',
338
+ 'cac' => 'urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2',
339
+ 'cbc' => 'urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2'
340
+ }
341
+
342
+ # Extract invoice data
343
+ self.document_type = extract_text(doc, '//cbc:InvoiceTypeCode', namespaces)
344
+ self.invoice_number = extract_text(doc, '//cbc:ID', namespaces)
345
+
346
+ # Extract date
347
+ date_text = extract_text(doc, '//cbc:IssueDate', namespaces)
348
+ self.issue_date = Date.parse(date_text) if date_text.present?
349
+
350
+ # Extract seller info
351
+ self.seller_name = extract_text(doc, '//cac:AccountingSupplierParty//cac:PartyName/cbc:Name', namespaces)
352
+ self.seller_tax_id = extract_text(doc, '//cac:AccountingSupplierParty//cac:PartyTaxScheme/cbc:CompanyID', namespaces)
353
+
354
+ # Extract buyer info
355
+ self.buyer_name = extract_text(doc, '//cac:AccountingCustomerParty//cac:PartyName/cbc:Name', namespaces)
356
+ self.buyer_tax_id = extract_text(doc, '//cac:AccountingCustomerParty//cac:PartyTaxScheme/cbc:CompanyID', namespaces)
357
+
358
+ # Extract amounts
359
+ self.net_amount = extract_text(doc, '//cac:LegalMonetaryTotal/cbc:LineExtensionAmount', namespaces).to_f
360
+ self.tax_amount = extract_text(doc, '//cac:TaxTotal/cbc:TaxAmount', namespaces).to_f
361
+ self.total_amount = extract_text(doc, '//cac:LegalMonetaryTotal/cbc:PayableAmount', namespaces).to_f
362
+ self.currency = extract_text(doc, '//cbc:DocumentCurrencyCode', namespaces) || 'EUR'
363
+
364
+ # Extract payment info
365
+ due_date_text = extract_text(doc, '//cac:PaymentTerms/cbc:PaymentDueDate', namespaces)
366
+ self.payment_due_date = Date.parse(due_date_text) if due_date_text.present?
367
+
368
+ # Extract line items
369
+ line_items = []
370
+ doc.xpath('//cac:InvoiceLine', namespaces).each do |line_node|
371
+ item = {
372
+ description: extract_text(line_node, './cac:Item/cbc:Description', namespaces),
373
+ quantity: extract_text(line_node, './cbc:InvoicedQuantity', namespaces).to_f,
374
+ unit_price: extract_text(line_node, './cac:Price/cbc:PriceAmount', namespaces).to_f,
375
+ line_total: extract_text(line_node, './cbc:LineExtensionAmount', namespaces).to_f
376
+ }
377
+ line_items << item if item[:description].present?
378
+ end
379
+
380
+ self.line_items = line_items if line_items.any?
381
+
382
+ rescue => e
383
+ Rails.logger.error "Error extracting UBL data: #{e.message}"
384
+ end
385
+ end
386
+
387
+ # Extract data from CII format
388
+ def extract_cii_data(file_path)
389
+ begin
390
+ require 'nokogiri'
391
+
392
+ xml = File.read(file_path)
393
+ doc = Nokogiri::XML(xml)
394
+
395
+ # Add CII namespaces
396
+ namespaces = {
397
+ 'rsm' => 'urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100',
398
+ 'ram' => 'urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100'
399
+ }
400
+
401
+ # Similar extraction logic as Factur-X but with different XPaths
402
+ # ...
403
+
404
+ rescue => e
405
+ Rails.logger.error "Error extracting CII data: #{e.message}"
406
+ end
407
+ end
408
+
409
+ # Extract invoice data from document text using pattern matching
410
+ def extract_from_text
411
+ return unless doc.text_parts.present?
412
+
413
+ text = doc.text_parts.map(&:text).join(' ')
414
+
415
+ # Try to extract invoice number
416
+ invoice_number_patterns = [
417
+ /facture\s+(?:n[°o]|num[ée]ro)\s*[:.: ]*\s*([A-Z0-9\-_\/]+)/i,
418
+ /invoice\s+(?:no|number)\s*[:.: ]*\s*([A-Z0-9\-_\/]+)/i,
419
+ /num[ée]ro\s+de\s+facture\s*[:.: ]*\s*([A-Z0-9\-_\/]+)/i,
420
+ /rechnung\s+(?:nr|nummer)\s*[:.: ]*\s*([A-Z0-9\-_\/]+)/i
421
+ ]
422
+
423
+ invoice_number_patterns.each do |pattern|
424
+ if (match = text.match(pattern))
425
+ self.invoice_number = match[1].strip
426
+ break
427
+ end
428
+ end
429
+
430
+ # Try to extract date
431
+ date_patterns = [
432
+ /date\s+(?:de\s+)?facture\s*[:.: ]*\s*(\d{1,2}[\/-]\d{1,2}[\/-]\d{2,4})/i,
433
+ /invoice\s+date\s*[:.: ]*\s*(\d{1,2}[\/-]\d{1,2}[\/-]\d{2,4})/i,
434
+ /date\s*[:.: ]*\s*(\d{1,2}[\/-]\d{1,2}[\/-]\d{2,4})/i
435
+ ]
436
+
437
+ date_patterns.each do |pattern|
438
+ if (match = text.match(pattern))
439
+ begin
440
+ self.issue_date = Date.parse(match[1])
441
+ break
442
+ rescue
443
+ # Continue if date parsing fails
444
+ end
445
+ end
446
+ end
447
+
448
+ # Try to extract seller and buyer
449
+ seller_patterns = [
450
+ /vendeur\s*[:.: ]*\s*([^\]+)/i,
451
+ /seller\s*[:.: ]*\s*([^\]+)/i,
452
+ /fournisseur\s*[:.: ]*\s*([^\]+)/i,
453
+ /supplier\s*[:.: ]*\s*([^\]+)/i
454
+ ]
455
+
456
+ buyer_patterns = [
457
+ /acheteur\s*[:.: ]*\\s*([^\]+)/i,
458
+ /buyer\s*[:.: ]*\\s*([^\]+)/i,
459
+ /client\s*[:.: ]*\\s*([^\]+)/i,
460
+ /customer\s*[:.: ]*\\s*([^\]+)/i
461
+ ]
462
+
463
+ seller_patterns.each do |pattern|
464
+ if (match = text.match(pattern))
465
+ self.seller_name = match[1].strip
466
+ break
467
+ end
468
+ end
469
+
470
+ buyer_patterns.each do |pattern|
471
+ if (match = text.match(pattern))
472
+ self.buyer_name = match[1].strip
473
+ break
474
+ end
475
+ end
476
+
477
+ # Try to extract amount
478
+ amount_patterns = [
479
+ /montant\s+(?:total|ttc)\s*[:.: ]*\s*(\d+[\s,.]\d+)/i,
480
+ /total\s+amount\s*[:.: ]*\s*(\d+[\s,.]\d+)/i,
481
+ /total\s+(?:ttc|tva incluse)\s*[:.: ]*\s*(\d+[\s,.]\d+)/i,
482
+ /total\s+(?:ht)\s*[:.: ]*\s*(\d+[\s,.]\d+)/i
483
+ ]
484
+
485
+ amount_patterns.each do |pattern|
486
+ if (match = text.match(pattern))
487
+ amount_str = match[1].strip.gsub(/\s/, '').gsub(',', '.')
488
+ self.total_amount = amount_str.to_f
489
+ break
490
+ end
491
+ end
492
+
493
+ # Try to extract currency
494
+ currency_patterns = [
495
+ /([€$£])/,
496
+ /\b(EUR|USD|GBP)\b/i
497
+ ]
498
+
499
+ currency_mapping = {
500
+ '€' => 'EUR',
501
+ '$' => 'USD',
502
+ '£' => 'GBP'
503
+ }
504
+
505
+ currency_patterns.each do |pattern|
506
+ if (match = text.match(pattern))
507
+ symbol = match[1].strip
508
+ self.currency = currency_mapping[symbol] || symbol
509
+ break
510
+ end
511
+ end
512
+ end
513
+
514
+ # Extract invoice data from document superfields
515
+ def extract_from_superfields
516
+ return unless doc.superfields.present?
517
+
518
+ # Map superfields to invoice record fields
519
+ field_mappings = {
520
+ 'invoice_number' => :invoice_number,
521
+ 'document_number' => :invoice_number,
522
+ 'issue_date' => :issue_date,
523
+ 'seller_name' => :seller_name,
524
+ 'buyer_name' => :buyer_name,
525
+ 'amounts_net' => :net_amount,
526
+ 'amounts_tax' => :tax_amount,
527
+ 'amounts_grand_total' => :total_amount,
528
+ 'amounts_total' => :total_amount,
529
+ 'currency' => :currency
530
+ }
531
+
532
+ field_mappings.each do |sf_key, record_field|
533
+ if doc.superfields[sf_key].present?
534
+ value = doc.superfields[sf_key]
535
+
536
+ # Convert to appropriate type
537
+ case record_field
538
+ when :issue_date
539
+ self[record_field] = value.is_a?(Date) ? value : Date.parse(value)
540
+ when :net_amount, :tax_amount, :total_amount
541
+ self[record_field] = value.to_f
542
+ else
543
+ self[record_field] = value
544
+ end
545
+ end
546
+ end
547
+ end
548
+
549
+ # Helper method to extract text from XML node
550
+ def extract_text(doc, xpath, namespaces = nil)
551
+ node = namespaces ? doc.at_xpath(xpath, namespaces) : doc.at_xpath(xpath)
552
+ node ? node.text.strip : nil
553
+ end
554
+
555
+ # Business logic methods
556
+
557
+ # Calculate tax rate
558
+ def tax_rate
559
+ return nil if net_amount.blank? || net_amount.zero? || tax_amount.blank?
560
+ (tax_amount / net_amount * 100).round(2)
561
+ end
562
+
563
+ # Check if the invoice is paid
564
+ def paid?
565
+ payment_status == 'paid'
566
+ end
567
+
568
+ # Check if the invoice is partially paid
569
+ def partially_paid?
570
+ payment_status == 'partial'
571
+ end
572
+
573
+ # Check if the invoice has payment due
574
+ def payment_due?
575
+ !paid? && payment_due_date.present? && payment_due_date < Date.today
576
+ end
577
+
578
+ # Get days until payment is due
579
+ def days_until_due
580
+ return nil unless payment_due_date
581
+ (payment_due_date - Date.today).to_i
582
+ end
583
+
584
+ # Get seller's formatted address
585
+ def formatted_seller_address
586
+ seller_address.present? ? seller_address : "Address not available"
587
+ end
588
+
589
+ # Get buyer's formatted address
590
+ def formatted_buyer_address
591
+ buyer_address.present? ? buyer_address : "Address not available"
592
+ end
593
+
594
+ # Format amount with currency
595
+ def formatted_total_amount
596
+ "#{total_amount} #{currency}"
597
+ end
598
+
599
+ # Get invoice type description
600
+ def document_type_description
601
+ case document_type
602
+ when '380'
603
+ 'Commercial Invoice'
604
+ when '381'
605
+ 'Credit Note'
606
+ when '383'
607
+ 'Debit Note'
608
+ when '386'
609
+ 'Prepayment Invoice'
610
+ when '389'
611
+ 'Self-Billed Invoice'
612
+ else
613
+ document_type || 'Invoice'
614
+ end
615
+ end
616
+ end
617
+ end
618
+ ::XML(xml)
619
+
620
+ # Add CII namespaces
621
+ namespaces = {
622
+ 'rsm' => 'urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100',
623
+ 'ram' => 'urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100'
624
+ }
625
+
626
+ # Similar extraction logic as Factur-X but with different XPaths
627
+ # ...
628
+
629
+ rescue => e
630
+ Rails.logger.error "Error extracting CII data: #{e.message}"
631
+ end
632
+ end
633
+
634
+ # Extract invoice data from document text using pattern matching
635
+ def extract_from_text
636
+ return unless doc.text_parts.present?
637
+
638
+ text = doc.text_parts.map(&:text).join(' ')
639
+
640
+ # Try to extract invoice number
641
+ invoice_number_patterns = [
642
+ /facture\s+(?:n[°o]|num[ée]ro)\s*[:.: ]*\s*([A-Z0-9\-_\/]+)/i,
643
+ /invoice\s+(?:no|number)\s*[:.: ]*\s*([A-Z0-9\-_\/]+)/i,
644
+ /num[ée]ro\s+de\s+facture\s*[:.: ]*\s*([A-Z0-9\-_\/]+)/i,
645
+ /rechnung\s+(?:nr|nummer)\s*[:.: ]*\s*([A-Z0-9\-_\/]+)/i
646
+ ]
647
+
648
+ invoice_number_patterns.each do |pattern|
649
+ if (match = text.match(pattern))
650
+ self.invoice_number = match[1].strip
651
+ break
652
+ end
653
+ end
654
+
655
+ # Try to extract date
656
+ date_patterns = [
657
+ /date\s+(?:de\s+)?facture\s*[:.: ]*\s*(\d{1,2}[\/-]\d{1,2}[\/-]\d{2,4})/i,
658
+ /invoice\s+date\s*[:.: ]*\s*(\d{1,2}[\/-]\d{1,2}[\/-]\d{2,4})/i,
659
+ /date\s*[:.: ]*\s*(\d{1,2}[\/-]\d{1,2}[\/-]\d{2,4})/i
660
+ ]
661
+
662
+ date_patterns.each do |pattern|
663
+ if (match = text.match(pattern))
664
+ begin
665
+ self.issue_date = Date.parse(match[1])
666
+ break
667
+ rescue
668
+ # Continue if date parsing fails
669
+ end
670
+ end
671
+ end
672
+
673
+ # Try to extract seller and buyer
674
+ seller_patterns = [
675
+ /vendeur\s*[:.: ]*\s*([^\n]+)/i,
676
+ /seller\s*[:.: ]*\s*([^\n]+)/i,
677
+ /fournisseur\s*[:.: ]*\s*([^\n]+)/i,
678
+ /supplier\s*[:.: ]*\s*([^\n]+)/i
679
+ ]
680
+
681
+ buyer_patterns = [
682
+ /acheteur\s*[:.: ]*\s*([^\n]+)/i,
683
+ /buyer\s*[:.: ]*\s*([^\n]+)/i,
684
+ /client\s*[:.: ]*\s*([^\n]+)/i,
685
+ /customer\s*[:.: ]*\s*([^\n]+)/i
686
+ ]
687
+
688
+ seller_patterns.each do |pattern|
689
+ if (match = text.match(pattern))
690
+ self.seller_name = match[1].strip
691
+ break
692
+ end
693
+ end
694
+
695
+ buyer_patterns.each do |pattern|
696
+ if (match = text.match(pattern))
697
+ self.buyer_name = match[1].strip
698
+ break
699
+ end
700
+ end
701
+
702
+ # Try to extract amount
703
+ amount_patterns = [
704
+ /montant\s+(?:total|ttc)\s*[:.: ]*\s*(\d+[\s,.]\d+)/i,
705
+ /total\s+amount\s*[:.: ]*\s*(\d+[\s,.]\d+)/i,
706
+ /total\s+(?:ttc|tva incluse)\s*[:.: ]*\s*(\d+[\s,.]\d+)/i,
707
+ /total\s+(?:ht)\s*[:.: ]*\s*(\d+[\s,.]\d+)/i
708
+ ]
709
+
710
+ amount_patterns.each do |pattern|
711
+ if (match = text.match(pattern))
712
+ amount_str = match[1].strip.gsub(/\s/, '').gsub(',', '.')
713
+ self.total_amount = amount_str.to_f
714
+ break
715
+ end
716
+ end
717
+
718
+ # Try to extract currency
719
+ currency_patterns = [
720
+ /([€$£])/,
721
+ /\b(EUR|USD|GBP)\b/i
722
+ ]
723
+
724
+ currency_mapping = {
725
+ '€' => 'EUR',
726
+ '$' => 'USD',
727
+ '£' => 'GBP'
728
+ }
729
+
730
+ currency_patterns.each do |pattern|
731
+ if (match = text.match(pattern))
732
+ symbol = match[1].strip
733
+ self.currency = currency_mapping[symbol] || symbol
734
+ break
735
+ end
736
+ end
737
+ end
738
+
739
+ # Extract invoice data from document superfields
740
+ def extract_from_superfields
741
+ return unless doc.superfields.present?
742
+
743
+ # Map superfields to invoice record fields
744
+ field_mappings = {
745
+ 'invoice_number' => :invoice_number,
746
+ 'document_number' => :invoice_number,
747
+ 'issue_date' => :issue_date,
748
+ 'seller_name' => :seller_name,
749
+ 'buyer_name' => :buyer_name,
750
+ 'amounts_net' => :net_amount,
751
+ 'amounts_tax' => :tax_amount,
752
+ 'amounts_grand_total' => :total_amount,
753
+ 'amounts_total' => :total_amount,
754
+ 'currency' => :currency
755
+ }
756
+
757
+ field_mappings.each do |sf_key, record_field|
758
+ if doc.superfields[sf_key].present?
759
+ value = doc.superfields[sf_key]
760
+
761
+ # Convert to appropriate type
762
+ case record_field
763
+ when :issue_date
764
+ self[record_field] = value.is_a?(Date) ? value : Date.parse(value) rescue nil
765
+ when :net_amount, :tax_amount, :total_amount
766
+ self[record_field] = value.to_f
767
+ else
768
+ self[record_field] = value
769
+ end
770
+ end
771
+ end
772
+ end
773
+
774
+ # Helper method to extract text from XML node
775
+ def extract_text(doc, xpath, namespaces = nil)
776
+ node = namespaces ? doc.at_xpath(xpath, namespaces) : doc.at_xpath(xpath)
777
+ node ? node.text.strip : nil
778
+ end
779
+
780
+ # Business logic methods
781
+
782
+ # Calculate tax rate
783
+ def tax_rate
784
+ return nil if net_amount.blank? || net_amount.zero? || tax_amount.blank?
785
+ (tax_amount / net_amount * 100).round(2)
786
+ end
787
+
788
+ # Check if the invoice is paid
789
+ def paid?
790
+ payment_status == 'paid'
791
+ end
792
+
793
+ # Check if the invoice is partially paid
794
+ def partially_paid?
795
+ payment_status == 'partial'
796
+ end
797
+
798
+ # Check if the invoice has payment due
799
+ def payment_due?
800
+ !paid? && payment_due_date.present? && payment_due_date < Date.today
801
+ end
802
+
803
+ # Get days until payment is due
804
+ def days_until_due
805
+ return nil unless payment_due_date
806
+ (payment_due_date - Date.today).to_i
807
+ end
808
+
809
+ # Get seller's formatted address
810
+ def formatted_seller_address
811
+ seller_address.present? ? seller_address : "Address not available"
812
+ end
813
+
814
+ # Get buyer's formatted address
815
+ def formatted_buyer_address
816
+ buyer_address.present? ? buyer_address : "Address not available"
817
+ end
818
+
819
+ # Format amount with currency
820
+ def formatted_total_amount
821
+ "#{total_amount} #{currency}"
822
+ end
823
+
824
+ # Get invoice type description
825
+ def document_type_description
826
+ case document_type
827
+ when '380'
828
+ 'Commercial Invoice'
829
+ when '381'
830
+ 'Credit Note'
831
+ when '383'
832
+ 'Debit Note'
833
+ when '386'
834
+ 'Prepayment Invoice'
835
+ when '389'
836
+ 'Self-Billed Invoice'
837
+ else
838
+ document_type || 'Invoice'
839
+ end
840
+ end
841
+ end
842
+ end
@@ -0,0 +1,239 @@
1
+ module DocumentTypes
2
+ class InvoiceService
3
+ def initialize(doc)
4
+ @doc = doc
5
+ end
6
+
7
+ # Process document as an invoice
8
+ def process
9
+ # Make sure document is an invoice type
10
+ return false unless @doc.document_type == 'invoice' || DocumentTypes::InvoiceRecord.detect(@doc)
11
+
12
+ # Create or update invoice record
13
+ invoice = @doc.as_invoice
14
+ invoice.sync_with_doc
15
+
16
+ true
17
+ end
18
+
19
+ # Search for invoices based on criteria
20
+ def self.search(criteria = {})
21
+ query = DocumentTypes::InvoiceRecord.all
22
+
23
+ # Filter by amount
24
+ if criteria[:min_amount].present?
25
+ query = query.where(:total_amount.gte => criteria[:min_amount].to_f)
26
+ end
27
+
28
+ if criteria[:max_amount].present?
29
+ query = query.where(:total_amount.lte => criteria[:max_amount].to_f)
30
+ end
31
+
32
+ # Filter by date
33
+ if criteria[:start_date].present?
34
+ start_date = criteria[:start_date].is_a?(Date) ? criteria[:start_date] : Date.parse(criteria[:start_date].to_s)
35
+ query = query.where(:issue_date.gte => start_date)
36
+ end
37
+
38
+ if criteria[:end_date].present?
39
+ end_date = criteria[:end_date].is_a?(Date) ? criteria[:end_date] : Date.parse(criteria[:end_date].to_s)
40
+ query = query.where(:issue_date.lte => end_date)
41
+ end
42
+
43
+ # Filter by invoice number
44
+ if criteria[:invoice_number].present?
45
+ query = query.where(invoice_number: /#{Regexp.escape(criteria[:invoice_number].to_s)}/i)
46
+ end
47
+
48
+ # Filter by seller name
49
+ if criteria[:seller_name].present?
50
+ query = query.where(seller_name: /#{Regexp.escape(criteria[:seller_name].to_s)}/i)
51
+ end
52
+
53
+ # Filter by buyer name
54
+ if criteria[:buyer_name].present?
55
+ query = query.where(buyer_name: /#{Regexp.escape(criteria[:buyer_name].to_s)}/i)
56
+ end
57
+
58
+ # Filter by net amount
59
+ if criteria[:net_amount].present?
60
+ query = query.where(net_amount: criteria[:net_amount].to_f)
61
+ end
62
+
63
+ # Filter by tax amount
64
+ if criteria[:tax_amount].present?
65
+ query = query.where(tax_amount: criteria[:tax_amount].to_f)
66
+ end
67
+
68
+ # Filter by currency
69
+ if criteria[:currency].present?
70
+ query = query.where(currency: criteria[:currency].to_s.upcase)
71
+ end
72
+
73
+ # Filter by payment status
74
+ if criteria[:payment_status].present?
75
+ query = query.where(payment_status: criteria[:payment_status])
76
+ end
77
+
78
+ # Filter by electronic format
79
+ if criteria[:electronic_format].present?
80
+ query = query.where(electronic_format: criteria[:electronic_format])
81
+ end
82
+
83
+ # Return query for additional chaining
84
+ query
85
+ end
86
+
87
+ # Generate reports based on invoices
88
+ def self.generate_report(criteria = {})
89
+ invoices = search(criteria)
90
+
91
+ # Calculate statistics
92
+ total = invoices.sum(:total_amount)
93
+ average = invoices.avg(:total_amount)
94
+ count = invoices.count
95
+
96
+ # Group by month
97
+ by_month = invoices.group_by { |inv| inv.issue_date&.beginning_of_month }
98
+ .transform_values { |invs| invs.sum(&:total_amount) }
99
+
100
+ # Group by seller
101
+ by_seller = invoices.group_by { |inv| inv.seller_name }
102
+ .transform_values { |invs| invs.sum(&:total_amount) }
103
+
104
+ # Group by buyer
105
+ by_buyer = invoices.group_by { |inv| inv.buyer_name }
106
+ .transform_values { |invs| invs.sum(&:total_amount) }
107
+
108
+ # Distribution by payment status
109
+ by_status = invoices.group_by { |inv| inv.payment_status }
110
+ .transform_values { |invs| invs.count }
111
+
112
+ # Return report data
113
+ {
114
+ total: total,
115
+ average: average,
116
+ count: count,
117
+ by_month: by_month,
118
+ by_seller: by_seller,
119
+ by_buyer: by_buyer,
120
+ by_status: by_status
121
+ }
122
+ end
123
+
124
+ # Get unpaid invoices
125
+ def self.unpaid_invoices(user = nil)
126
+ query = search(payment_status: 'unpaid')
127
+
128
+ # Filter by invoices accessible to user if specified
129
+ if user && user.respond_to?(:readable_doc_ids)
130
+ doc_ids = user.readable_doc_ids
131
+ query = query.where(:doc_id.in => doc_ids)
132
+ end
133
+
134
+ query
135
+ end
136
+
137
+ # Get overdue invoices
138
+ def self.overdue_invoices(user = nil)
139
+ today = Date.today
140
+ query = unpaid_invoices(user).where(:payment_due_date.lt => today)
141
+ query
142
+ end
143
+
144
+ # Get upcoming invoices
145
+ def self.upcoming_invoices(days = 7, user = nil)
146
+ today = Date.today
147
+ deadline = today + days.days
148
+ query = unpaid_invoices(user).where(:payment_due_date.gte => today, :payment_due_date.lte => deadline)
149
+ query
150
+ end
151
+
152
+ # Get total by party (seller/buyer)
153
+ def self.total_by_party(party_type, name, criteria = {})
154
+ criteria[:"#{party_type}_name"] = name
155
+ invoices = search(criteria)
156
+
157
+ total = invoices.sum(:total_amount)
158
+ count = invoices.count
159
+
160
+ {
161
+ name: name,
162
+ total: total,
163
+ count: count,
164
+ invoices: invoices
165
+ }
166
+ end
167
+
168
+ # Get total by seller
169
+ def self.total_by_seller(seller_name, criteria = {})
170
+ total_by_party(:seller, seller_name, criteria)
171
+ end
172
+
173
+ # Get total by buyer
174
+ def self.total_by_buyer(buyer_name, criteria = {})
175
+ total_by_party(:buyer, buyer_name, criteria)
176
+ end
177
+
178
+ # Get statistics for a period
179
+ def self.stats_for_period(start_date, end_date, user = nil)
180
+ criteria = {
181
+ start_date: start_date,
182
+ end_date: end_date
183
+ }
184
+
185
+ # Filter by documents accessible to user if specified
186
+ if user && user.respond_to?(:readable_doc_ids)
187
+ doc_ids = user.readable_doc_ids
188
+ invoices = search(criteria).where(:doc_id.in => doc_ids)
189
+ else
190
+ invoices = search(criteria)
191
+ end
192
+
193
+ total = invoices.sum(:total_amount)
194
+ average = invoices.avg(:total_amount)
195
+ count = invoices.count
196
+
197
+ # Distribution by month
198
+ by_month = invoices.group_by { |inv| inv.issue_date&.beginning_of_month }
199
+ .transform_values { |invs| invs.sum(&:total_amount) }
200
+ .sort.to_h
201
+
202
+ {
203
+ start_date: start_date,
204
+ end_date: end_date,
205
+ total: total,
206
+ average: average,
207
+ count: count,
208
+ by_month: by_month
209
+ }
210
+ end
211
+
212
+ # Get statistics for current month
213
+ def self.stats_for_current_month(user = nil)
214
+ today = Date.today
215
+ start_date = today.beginning_of_month
216
+ end_date = today.end_of_month
217
+
218
+ stats_for_period(start_date, end_date, user)
219
+ end
220
+
221
+ # Get statistics for current quarter
222
+ def self.stats_for_current_quarter(user = nil)
223
+ today = Date.today
224
+ start_date = today.beginning_of_quarter
225
+ end_date = today.end_of_quarter
226
+
227
+ stats_for_period(start_date, end_date, user)
228
+ end
229
+
230
+ # Get statistics for current year
231
+ def self.stats_for_current_year(user = nil)
232
+ today = Date.today
233
+ start_date = today.beginning_of_year
234
+ end_date = today.end_of_year
235
+
236
+ stats_for_period(start_date, end_date, user)
237
+ end
238
+ end
239
+ end
@@ -0,0 +1,5 @@
1
+ module DocumentTypes
2
+ module Invoice
3
+ VERSION = "0.1.0"
4
+ end
5
+ end
@@ -0,0 +1,33 @@
1
+ require "document_types/invoice/version"
2
+ require "rails"
3
+
4
+ module DocumentTypes
5
+ module Invoice
6
+ class Engine < ::Rails::Engine
7
+ isolate_namespace DocumentTypes::Invoice
8
+
9
+ initializer "document_types.invoice.register" do |app|
10
+ # S'assurer que les classes de base sont déjà chargées
11
+ if !defined?(DocumentTypes::Registry) || !defined?(DocumentTypes::Base::DocumentTypeRecord)
12
+ Rails.logger.error "DocumentTypes base classes not loaded. Make sure to load document_types/base and document_types/registry before this gem."
13
+ end
14
+
15
+ # Load our models and services
16
+ require_relative "../../app/models/document_types/invoice_record"
17
+ require_relative "../../app/services/document_types/invoice_service"
18
+
19
+ # Register our document type in the registry
20
+ DocumentTypes::InvoiceRecord.register_type if defined?(DocumentTypes::Registry)
21
+ end
22
+
23
+ config.to_prepare do
24
+ # Make sure Doc model has the relationship with InvoiceRecord
25
+ if defined?(Doc) && defined?(DocumentTypable) && Doc.included_modules.include?(DocumentTypable)
26
+ Doc.class_eval do
27
+ has_one :invoice, class_name: "DocumentTypes::InvoiceRecord", dependent: :destroy
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
metadata ADDED
@@ -0,0 +1,122 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: document_types-invoice
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Olivier
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2025-04-16 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 6.0.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 6.0.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: mongoid
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 7.0.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: 7.0.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: nokogiri
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: 1.10.0
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: 1.10.0
55
+ - !ruby/object:Gem::Dependency
56
+ name: pdf-reader
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: 2.4.0
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: 2.4.0
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec-rails
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ description: Modular extension providing invoice document type functionality
84
+ email:
85
+ - olivier.dirrenberger@sinoia.fr
86
+ executables: []
87
+ extensions: []
88
+ extra_rdoc_files: []
89
+ files:
90
+ - LICENSE.txt
91
+ - README.md
92
+ - Rakefile
93
+ - app/models/document_types/invoice_record.rb
94
+ - app/services/document_types/invoice_service.rb
95
+ - lib/document_types/invoice.rb
96
+ - lib/document_types/invoice/version.rb
97
+ homepage: https://code.plugandwork.net/plugandwork/document_types/invoice
98
+ licenses:
99
+ - MIT
100
+ metadata:
101
+ homepage_uri: https://code.plugandwork.net/plugandwork/document_types/invoice
102
+ source_code_uri: https://code.plugandwork.net/plugandwork/document_types/invoice
103
+ post_install_message:
104
+ rdoc_options: []
105
+ require_paths:
106
+ - lib
107
+ required_ruby_version: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - ">="
110
+ - !ruby/object:Gem::Version
111
+ version: 2.6.0
112
+ required_rubygems_version: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: '0'
117
+ requirements: []
118
+ rubygems_version: 3.3.7
119
+ signing_key:
120
+ specification_version: 4
121
+ summary: Invoice document type for Rails application
122
+ test_files: []