invoice_extractor_arca 0.2.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.
Files changed (48) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +49 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +631 -0
  5. data/exe/arca-invoice-extract +6 -0
  6. data/lib/invoice_extractor_arca/catalogs/copy_types.json +5 -0
  7. data/lib/invoice_extractor_arca/catalogs/currencies.json +65 -0
  8. data/lib/invoice_extractor_arca/catalogs/document_types.json +40 -0
  9. data/lib/invoice_extractor_arca/catalogs/iva_types.json +13 -0
  10. data/lib/invoice_extractor_arca/catalogs/payment_methods.json +10 -0
  11. data/lib/invoice_extractor_arca/catalogs/units_measure.json +47 -0
  12. data/lib/invoice_extractor_arca/catalogs/voucher_types.json +99 -0
  13. data/lib/invoice_extractor_arca/catalogs.rb +34 -0
  14. data/lib/invoice_extractor_arca/cli.rb +247 -0
  15. data/lib/invoice_extractor_arca/command_runner.rb +18 -0
  16. data/lib/invoice_extractor_arca/configuration.rb +42 -0
  17. data/lib/invoice_extractor_arca/dependencies/resolver.rb +118 -0
  18. data/lib/invoice_extractor_arca/error.rb +6 -0
  19. data/lib/invoice_extractor_arca/error_record.rb +39 -0
  20. data/lib/invoice_extractor_arca/errors.rb +34 -0
  21. data/lib/invoice_extractor_arca/extractor.rb +381 -0
  22. data/lib/invoice_extractor_arca/final_payload_builder.rb +67 -0
  23. data/lib/invoice_extractor_arca/ocr/rapidocr_extract.py +198 -0
  24. data/lib/invoice_extractor_arca/ocr/rapidocr_text_extractor.rb +117 -0
  25. data/lib/invoice_extractor_arca/pdf/ocr_text_extractor.rb +131 -0
  26. data/lib/invoice_extractor_arca/pdf/renderer.rb +97 -0
  27. data/lib/invoice_extractor_arca/pdf/text_extractor.rb +182 -0
  28. data/lib/invoice_extractor_arca/qr/payload_parser.rb +124 -0
  29. data/lib/invoice_extractor_arca/qr/reader.rb +53 -0
  30. data/lib/invoice_extractor_arca/qr/result_builder.rb +229 -0
  31. data/lib/invoice_extractor_arca/qr_extractor.rb +156 -0
  32. data/lib/invoice_extractor_arca/result.rb +94 -0
  33. data/lib/invoice_extractor_arca/scanners/factory.rb +88 -0
  34. data/lib/invoice_extractor_arca/scanners/qreader_scan.py +102 -0
  35. data/lib/invoice_extractor_arca/scanners/qreader_scanner.rb +131 -0
  36. data/lib/invoice_extractor_arca/scanners/scanner_chain.rb +73 -0
  37. data/lib/invoice_extractor_arca/scanners/zbar_scanner.rb +52 -0
  38. data/lib/invoice_extractor_arca/text/invoice_parser.rb +517 -0
  39. data/lib/invoice_extractor_arca/text/layout_table_parser.rb +218 -0
  40. data/lib/invoice_extractor_arca/text/locale_value_normalizer.rb +53 -0
  41. data/lib/invoice_extractor_arca/text/normalizer.rb +31 -0
  42. data/lib/invoice_extractor_arca/validation/reconciler.rb +53 -0
  43. data/lib/invoice_extractor_arca/version.rb +5 -0
  44. data/lib/invoice_extractor_arca/voucher_validator.rb +169 -0
  45. data/lib/invoice_extractor_arca.rb +117 -0
  46. data/requirements-ocr.txt +2 -0
  47. data/sig/invoice_extractor_arca.rbs +4 -0
  48. metadata +135 -0
@@ -0,0 +1,65 @@
1
+ {
2
+ "000": "OTRAS MONEDAS",
3
+ "PES": "PESOS",
4
+ "ARS": "PESOS ARGENTINOS",
5
+ "DOL": "Dólar ESTADOUNIDENSE",
6
+ "002": "Dólar EEUU LIBRE",
7
+ "003": "FRANCOS FRANCESES",
8
+ "004": "LIRAS ITALIANAS",
9
+ "005": "PESETAS",
10
+ "006": "MARCOS ALEMANES",
11
+ "007": "FLORINES HOLANDESES",
12
+ "008": "FRANCOS BELGAS",
13
+ "009": "FRANCOS SUIZOS",
14
+ "010": "PESOS MEJICANOS",
15
+ "011": "PESOS URUGUAYOS",
16
+ "012": "REAL",
17
+ "013": "ESCUDOS PORTUGUESES",
18
+ "014": "CORONAS DANESAS",
19
+ "015": "CORONAS NORUEGAS",
20
+ "016": "CORONAS SUECAS",
21
+ "017": "CHELINES AUTRIACOS",
22
+ "018": "Dólar CANADIENSE",
23
+ "019": "YENS",
24
+ "021": "LIBRA ESTERLINA",
25
+ "022": "MARCOS FINLANDESES",
26
+ "023": "BOLIVAR (VENEZOLANO)",
27
+ "024": "CORONA CHECA",
28
+ "025": "DINAR (YUGOSLAVO)",
29
+ "026": "Dólar AUSTRALIANO",
30
+ "027": "DRACMA (GRIEGO)",
31
+ "028": "FLORIN (ANTILLAS HOLA",
32
+ "029": "GUARANI",
33
+ "030": "SHEKEL (ISRAEL)",
34
+ "031": "PESO BOLIVIANO",
35
+ "032": "PESO COLOMBIANO",
36
+ "033": "PESO CHILENO",
37
+ "034": "RAND (SUDAFRICANO)",
38
+ "035": "NUEVO SOL PERUANO",
39
+ "036": "SUCRE (ECUATORIANO)",
40
+ "040": "LEI RUMANOS",
41
+ "041": "DERECHOS ESPECIALES DE GIRO",
42
+ "042": "PESOS DOMINICANOS",
43
+ "043": "BALBOAS PANAMEÑAS",
44
+ "044": "CORDOBAS NICARAGÛENSES",
45
+ "045": "DIRHAM MARROQUÍES",
46
+ "046": "LIBRAS EGIPCIAS",
47
+ "047": "RIYALS SAUDITAS",
48
+ "048": "BRANCOS BELGAS FINANCIERAS",
49
+ "049": "GRAMOS DE ORO FINO",
50
+ "050": "LIBRAS IRLANDESAS",
51
+ "051": "Dólar DE HONG KONG",
52
+ "052": "Dólar DE SINGAPUR",
53
+ "053": "Dólar DE JAMAICA",
54
+ "054": "Dólar DE TAIWAN",
55
+ "055": "QUETZAL (GUATEMALTECOS)",
56
+ "056": "FORINT (HUNGRIA)",
57
+ "057": "BAHT (TAILANDIA)",
58
+ "058": "ECU",
59
+ "059": "DINAR KUWAITI",
60
+ "060": "EURO",
61
+ "061": "ZLTYS POLACOS",
62
+ "062": "RUPIAS HINDÚES",
63
+ "063": "LEMPIRAS HONDUREÑAS",
64
+ "064": "YUAN (Rep. Pop. China)"
65
+ }
@@ -0,0 +1,40 @@
1
+ {
2
+ "0" : "CI Policía Federal",
3
+ "1" : "CI Buenos Aires",
4
+ "2" : "CI Catamarca",
5
+ "3" : "CI Córdoba",
6
+ "4" : "CI Corrientes",
7
+ "5" : "CI Entre Ríos",
8
+ "6" : "CI Jujuy",
9
+ "7" : "CI Mendoza",
10
+ "8" : "CI La Rioja",
11
+ "9" : "CI Salta",
12
+ "10" : "CI San Juan",
13
+ "11" : "CI San Luis",
14
+ "12" : "CI Santa Fe",
15
+ "13" : "CI Santiago del Estero",
16
+ "14" : "CI Tucumán",
17
+ "16" : "CI Chaco",
18
+ "17" : "CI Chubut",
19
+ "18" : "CI Formosa",
20
+ "19" : "CI Misiones",
21
+ "20" : "CI Neuquén",
22
+ "21" : "CI La Pampa",
23
+ "22" : "CI Río Negro",
24
+ "23" : "CI Santa Cruz",
25
+ "24" : "CI Tierra del Fuego",
26
+ "30" : "Certificado de Migración",
27
+ "80" : "CUIT",
28
+ "86" : "CUIL",
29
+ "87" : "CDI",
30
+ "88" : "Usado por Anses para Padrón",
31
+ "89" : "LE",
32
+ "90" : "LC",
33
+ "91" : "CI extranjera",
34
+ "92" : "en trámite",
35
+ "93" : "Acta nacimiento",
36
+ "94" : "Pasaporte",
37
+ "95" : "CI Bs. As. RNP",
38
+ "96" : "DNI",
39
+ "99" : "Sin identificar/venta global diaria"
40
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "01" : "IVA Responsable Inscripto",
3
+ "04" : "IVA Sujeto Exento",
4
+ "05" : "Consumidor Final",
5
+ "06" : "Responsable Monotributo" ,
6
+ "07" : "Sujeto no Categorizado",
7
+ "08" : "Proveedor del Exterior",
8
+ "09" : "Cliente del Exterior",
9
+ "10" : "IVA Liberado – Ley Nº 19.640",
10
+ "13" : "Monotributista Social",
11
+ "15" : "IVA No Alcanzado",
12
+ "16" : "Monotributo Trabajador Independiente Promovido"
13
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "01": "Contado",
3
+ "02": "Tarjeta de Débito",
4
+ "03": "Tarjeta de Crédito",
5
+ "04": "Cuenta Corriente",
6
+ "05": "Cheque",
7
+ "06": "Transferencia Bancaria",
8
+ "07": "Otra",
9
+ "08": "Otros medios de pago electrónico"
10
+ }
@@ -0,0 +1,47 @@
1
+ {
2
+ "01": "kilogramos",
3
+ "02": "metros",
4
+ "03": "metros cuadrados",
5
+ "04": "metros cúbicos",
6
+ "05": "litros",
7
+ "06": "1000 kWh",
8
+ "07": "unidades",
9
+ "08": "pares",
10
+ "09": "docenas",
11
+ "10": "quilates",
12
+ "11": "millares",
13
+ "12": "gramos",
14
+ "13": "milimetros",
15
+ "14": "mm cúbicos",
16
+ "15": "kilómetros",
17
+ "16": "hectolitros",
18
+ "17": "centímetros",
19
+ "18": "jgo. pqt. mazo naipes",
20
+ "19": "cm cúbicos",
21
+ "20": "toneladas",
22
+ "21": "dam cúbicos",
23
+ "22": "hm cúbicos",
24
+ "23": "km cúbicos",
25
+ "24": "microgramos",
26
+ "25": "nanogramos",
27
+ "26": "picogramos",
28
+ "27": "miligramos",
29
+ "28": "mililitros",
30
+ "29": "curie",
31
+ "30": "milicurie",
32
+ "31": "microcurie",
33
+ "32": "uiacthor",
34
+ "33": "muiacthor",
35
+ "34": "kg base",
36
+ "35": "gruesa",
37
+ "36": "kg bruto",
38
+ "37": "uiactant",
39
+ "38": "muiactant",
40
+ "39": "uiactig",
41
+ "40": "muiactig",
42
+ "41": "kg activo",
43
+ "42": "gramo activo",
44
+ "43": "gramo base",
45
+ "44": "packs",
46
+ "45": "otras unidades"
47
+ }
@@ -0,0 +1,99 @@
1
+ {
2
+ "001": "FACTURA A",
3
+ "002": "NOTA DE DEBITO A",
4
+ "003": "NOTA DE CREDITO A",
5
+ "004": "RECIBO A",
6
+ "005": "NOTAS DE VENTA AL CONTADO A",
7
+ "006": "FACTURA B",
8
+ "007": "NOTA DE DEBITO B",
9
+ "008": "NOTA DE CREDITO B",
10
+ "009": "RECIBO B",
11
+ "010": "NOTAS DE VENTA AL CONTADO B",
12
+ "011": "FACTURA C",
13
+ "012": "NOTA DE DEBITO C",
14
+ "013": "NOTA DE CREDITO C",
15
+ "015": "RECIBO C",
16
+ "016": "NOTAS DE VENTA AL CONTADO C",
17
+ "017": "LIQUIDACION DE SERVICIOS PUBLICOS CLASE A",
18
+ "018": "LIQUIDACION DE SERVICIOS PUBLICOS CLASE B",
19
+ "019": "FACTURA POR OPERACIONES CON EL EXTERIOR",
20
+ "020": "NOTA DE DEBITO POR OPERACIONES CON EL EXTERIOR",
21
+ "021": "NOTA DE CREDITO POR OPERACIONES CON EL EXTERIOR",
22
+ "022": "FACTURA PERMISO EXPORTACION SIMPLIFICADO - DTO. 855/97",
23
+ "023": "COMPROBANTES CLASE “A” DE COMPRA PRIMARIA PARA EL SECTOR PESQUERO MARITIMO",
24
+ "024": "COMPROBANTES CLASE “A” DE CONSIGNACION PRIMARIA PARA EL SECTOR PESQUERO MARITIMO",
25
+ "025": "COMPROBANTES CLASE “B” DE COMPRA PRIMARIA PARA EL SECTOR PESQUERO MARITIMO",
26
+ "026": "COMPROBANTES CLASE “B” DE CONSIGNACION PRIMARIA PARA EL SECTOR PESQUERO MARITIMO",
27
+ "027": "LIQUIDACION UNICA COMERCIAL IMPOSITIVA CLASE A",
28
+ "028": "LIQUIDACION UNICA COMERCIAL IMPOSITIVA CLASE B",
29
+ "029": "LIQUIDACION UNICA COMERCIAL IMPOSITIVA CLASE C",
30
+ "030": "COMPROBANTE DE COMPRA DE BIENES USADOS",
31
+ "031": "MANDATO - CONSIGNACION",
32
+ "032": "COMPROBANTE DE COMPRA DE MATERIALES A RECICLAR PROVENIENTE DE RESIDUOS",
33
+ "033": "LIQUIDACION PRIMARIA DE GRANOS",
34
+ "034": "COMPROBANTES A DEL ANEXO I, APARTADO A, INCISO F) R.G. N° 1415",
35
+ "035": "COMPROBANTES B DEL ANEXO I, APARTADO A, INC. F), R.G. N° 1415",
36
+ "036": "COMPROBANTES C DEL ANEXO I, APARTADO A, INC.F), R.G. N° 1415",
37
+ "037": "NOTAS DE DEBITO O DOCUMENTOS EQUIVALENTES QUE CUMPLAN CON LA R.G. N° 1415",
38
+ "038": "NOTAS DE CREDITO O DOCUMENTOS EQUIVALENTES QUE CUMPLAN CON LA R.G. N° 1415",
39
+ "039": "OTROS COMPROBANTES A QUE CUMPLEN CON LA R G N° 1415",
40
+ "040": "OTROS COMPROBANTES B QUE CUMPLAN CON LA R.G. N° 1415",
41
+ "041": "OTROS COMPROBANTES C QUE CUMPLAN CON LA R.G. N° 1415",
42
+ "043": "NOTA DE CREDITO LIQUIDACION UNICA COMERCIAL IMPOSITIVA CLASE B",
43
+ "044": "NOTA DE CREDITO LIQUIDACION UNICA COMERCIAL IMPOSITIVA CLASE C",
44
+ "045": "NOTA DE DEBITO LIQUIDACION UNICA COMERCIAL IMPOSITIVA CLASE A",
45
+ "046": "NOTA DE DEBITO LIQUIDACION UNICA COMERCIAL IMPOSITIVA CLASE B",
46
+ "047": "NOTA DE DEBITO LIQUIDACION UNICA COMERCIAL IMPOSITIVA CLASE C",
47
+ "048": "NOTA DE CREDITO LIQUIDACION UNICA COMERCIAL IMPOSITIVA CLASE A",
48
+ "049": "COMPROBANTES DE COMPRA DE BIENES USADOS NO REGISTRABLES A CONSUMIDORES FINALES",
49
+ "051": "FACTURA A CON LEYENDA “OPERACIÓN SUJETA A RETENCIÓN”",
50
+ "052": "NOTA DE DEBITO A CON LEYENDA “OPERACIÓN SUJETA A RETENCIÓN”",
51
+ "053": "NOTA DE CREDITO A CON LEYENDA “OPERACIÓN SUJETA A RETENCIÓN”",
52
+ "054": "RECIBO A CON LEYENDA “OPERACIÓN SUJETA A RETENCIÓN”",
53
+ "055": "NOTAS DE VENTA AL CONTADO A CON LEYENDA “OPERACIÓN SUJETA A RETENCIÓN”",
54
+ "056": "COMPROBANTES A CON LEYENDA “OPERACIÓN SUJETA A RETENCIÓN” DEL ANEXO I, APARTADO A, INC. F), R.G. N° 1.415",
55
+ "057": "OTROS COMPROBANTES A CON LEYENDA “OPERACIÓN SUJETA A RETENCIÓN”QUE CUMPLAN CON LA R.G. N° 1.415",
56
+ "058": "CUENTA DE VENTA Y LIQUIDO PRODUCTO A CON LEYENDA “OPERACIÓN SUJETA A RETENCIÓN”",
57
+ "059": "LIQUIDACION A CON LEYENDA “OPERACIÓN SUJETA A RETENCIÓN”",
58
+ "060": "CUENTAS DE VENTA Y LIQUIDO PRODUCTO A",
59
+ "061": "CUENTAS DE VENTA Y LIQUIDO PRODUCTO B",
60
+ "063": "LIQUIDACIONES A",
61
+ "064": "LIQUIDACIONES B",
62
+ "066": "DESPACHO DE IMPORTACION",
63
+ "080": "INFORME DIARIO DE CIERRE (ZETA) - CONTROLADORES FISCALES (*)",
64
+ "081": "TIQUE FACTURA A (*)",
65
+ "082": "TIQUE FACTURA B (*)",
66
+ "083": "TIQUE (*)",
67
+ "088": "REMITO ELECTRONICO",
68
+ "089": "RESUMEN DE DATOS",
69
+ "090": "OTROS COMPROBANTES - DOCUMENTOS EXCEPTUADOS - NOTAS DE CREDITO",
70
+ "091": "REMITOS R",
71
+ "099": "OTROS COMPROBANTES QUE NO CUMPLEN O ESTÁN EXCEPTUADOS DE LA R.G. 1415 Y SUS MODIF",
72
+ "109": "TIQUE C",
73
+ "110": "TIQUE NOTA DE CREDITO (*)",
74
+ "111": "TIQUE FACTURA C (*)",
75
+ "112": "TIQUE NOTA DE CREDITO A (*)",
76
+ "113": "TIQUE NOTA DE CREDITO B (*)",
77
+ "114": "TIQUE NOTA DE CREDITO C (*)",
78
+ "115": "TIQUE NOTA DE DEBITO A (*)",
79
+ "116": "TIQUE NOTA DE DEBITO B (*)",
80
+ "117": "TIQUE NOTA DE DEBITO C (*)",
81
+ "201": "FACTURA DE CRÉDITO ELECTRÓNICA MiPyMEs (FCE) A",
82
+ "202": "NOTA DE DEBITO ELECTRÓNICA MiPyMEs (FCE) A",
83
+ "203": "NOTA DE CREDITO ELECTRÓNICA MiPyMEs (FCE) A",
84
+ "206": "FACTURA DE CRÉDITO ELECTRÓNICA MiPyMEs (FCE) B",
85
+ "207": "NOTA DE DEBITO ELECTRÓNICA MiPyMEs (FCE) B",
86
+ "208": "NOTA DE CREDITO ELECTRÓNICA MiPyMEs (FCE) B",
87
+ "211": "FACTURA DE CRÉDITO ELECTRÓNICA MiPyMEs (FCE) C",
88
+ "212": "NOTA DE DEBITO ELECTRÓNICA MiPyMEs (FCE) C",
89
+ "213": "NOTA DE CREDITO ELECTRÓNICA MiPyMEs (FCE) C",
90
+ "331": "LIQUIDACION SECUNDARIA DE GRANOS",
91
+ "332": "CERTIFICACION ELECTRONICA (GRANOS)",
92
+ "991": "REMITO ELECTRÓNICO DE TABACO EN HEBRA",
93
+ "992": "RESUMEN DE DATOS DE TABACO EN HEBRA",
94
+ "993": "REMITO ELECTRÓNICO HARINERO - AUTOMOTOR",
95
+ "994": "REMITO ELECTRÓNICO HARINERO - FERROVIARIO",
96
+ "995": "REMITO ELECTRÓNICO CÁRNICO",
97
+ "997": "Remito Electrónico para Azúcar, Alcohol y Subproductos -Mercado Interno-",
98
+ "998": "Remito Electrónico para Azúcar, Alcohol y Subproductos -Exportación-"
99
+ }
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module InvoiceExtractorArca
6
+ module Catalogs
7
+ module_function
8
+
9
+ def fetch(catalog, code)
10
+ data(catalog).fetch(code)
11
+ end
12
+
13
+ def data(catalog)
14
+ @data ||= {}
15
+
16
+ @data[catalog] ||= begin
17
+ path = File.expand_path(
18
+ "catalogs/#{catalog}.json",
19
+ __dir__
20
+ )
21
+
22
+ JSON.parse(File.read(path))
23
+ end
24
+ end
25
+
26
+ VOUCHER_TYPES = data(:voucher_types)
27
+ CURRENCIES = data(:currencies)
28
+ DOCUMENT_TYPES = data(:document_types)
29
+ IVA_TYPES = data(:iva_types)
30
+ PAYMENT_METHODS = data(:payment_methods)
31
+ UNITS_MEASURE = data(:units_measure)
32
+ COPY_TYPES = data(:copy_types)
33
+ end
34
+ end
@@ -0,0 +1,247 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "optparse"
5
+
6
+ module InvoiceExtractorArca
7
+ class CLI
8
+ EXIT_SUCCESS = 0
9
+ EXIT_EXTRACTION_FAILED = 1
10
+ EXIT_USAGE_ERROR = 2
11
+ EXIT_RUNTIME_ERROR = 3
12
+
13
+ def initialize(
14
+ argv,
15
+ stdout: $stdout,
16
+ stderr: $stderr,
17
+ extractor: InvoiceExtractorArca.method(:extract)
18
+ )
19
+ @argv = argv.dup
20
+ @stdout = stdout
21
+ @stderr = stderr
22
+ @extractor = extractor
23
+ @options = default_options
24
+ end
25
+
26
+ def run
27
+ parser = build_parser
28
+ parser.parse!(@argv)
29
+
30
+ if @options.delete(:help)
31
+ @stdout.puts parser
32
+ return EXIT_SUCCESS
33
+ end
34
+
35
+ input_path = @argv.shift
36
+ raise OptionParser::MissingArgument, "invoice file" unless input_path
37
+
38
+ unless @argv.empty?
39
+ raise OptionParser::InvalidArgument,
40
+ "unexpected arguments: #{@argv.join(" ")}"
41
+ end
42
+
43
+ result = @extractor.call(input_path, **extractor_options)
44
+
45
+ print_result(result)
46
+
47
+ result.success? ? EXIT_SUCCESS : EXIT_EXTRACTION_FAILED
48
+ rescue OptionParser::ParseError => e
49
+ @stderr.puts "Error: #{e.message}"
50
+ @stderr.puts
51
+ @stderr.puts build_parser
52
+ EXIT_USAGE_ERROR
53
+ rescue InvoiceExtractorArca::Error => e
54
+ print_exception(e)
55
+ EXIT_EXTRACTION_FAILED
56
+ rescue SystemCallError => e
57
+ @stderr.puts JSON.generate(
58
+ success: false,
59
+ error: {
60
+ code: "system_error",
61
+ message: e.message
62
+ }
63
+ )
64
+ EXIT_RUNTIME_ERROR
65
+ end
66
+
67
+ private
68
+
69
+ def default_options
70
+ {
71
+ pretty: false,
72
+ strict: false,
73
+ enrichment: true,
74
+ page_range: nil,
75
+ resolution: 300,
76
+ multiple_qr: :first_valid,
77
+ qr_scanner: :zbar_then_qreader,
78
+ dependency_paths: {}
79
+ }
80
+ end
81
+
82
+ def build_parser
83
+ OptionParser.new do |parser|
84
+ parser.banner =
85
+ "Usage: arca-invoice-extract [options] INVOICE_FILE"
86
+
87
+ parser.separator ""
88
+ parser.separator "Extract ARCA invoice data as JSON."
89
+ parser.separator ""
90
+
91
+ parser.on(
92
+ "--pretty",
93
+ "Pretty-print JSON output"
94
+ ) do
95
+ @options[:pretty] = true
96
+ end
97
+
98
+ parser.on(
99
+ "--strict",
100
+ "Raise extraction errors instead of returning them"
101
+ ) do
102
+ @options[:strict] = true
103
+ end
104
+
105
+ parser.on(
106
+ "--[no-]enrichment",
107
+ "Enable or disable text/OCR enrichment"
108
+ ) do |value|
109
+ @options[:enrichment] = value
110
+ end
111
+
112
+ parser.on(
113
+ "--page PAGE",
114
+ Integer,
115
+ "Process one PDF page"
116
+ ) do |page|
117
+ positive_integer!(page, "--page")
118
+ @options[:page_range] = page
119
+ end
120
+
121
+ parser.on(
122
+ "--pages RANGE",
123
+ "Process a page range, for example 1-5"
124
+ ) do |value|
125
+ @options[:page_range] = parse_page_range(value)
126
+ end
127
+
128
+ parser.on(
129
+ "--resolution DPI",
130
+ Integer,
131
+ "PDF rendering resolution, default: 300"
132
+ ) do |resolution|
133
+ positive_integer!(resolution, "--resolution")
134
+ @options[:resolution] = resolution
135
+ end
136
+
137
+ parser.on(
138
+ "--qr-scanner NAME",
139
+ %w[zbar qreader zbar_then_qreader qreader_then_zbar all],
140
+ "QR scanner backend"
141
+ ) do |scanner|
142
+ @options[:qr_scanner] = scanner.to_sym
143
+ end
144
+
145
+ parser.on(
146
+ "--multiple-qr MODE",
147
+ %w[first_valid error],
148
+ "Multiple-QR behavior"
149
+ ) do |mode|
150
+ @options[:multiple_qr] = mode.to_sym
151
+ end
152
+
153
+ parser.on(
154
+ "--dependency NAME=PATH",
155
+ "Set an external dependency path; repeatable"
156
+ ) do |value|
157
+ name, path = value.split("=", 2)
158
+
159
+ if name.to_s.empty? || path.to_s.empty?
160
+ raise OptionParser::InvalidArgument,
161
+ "dependency must use NAME=PATH"
162
+ end
163
+
164
+ @options[:dependency_paths][name.to_sym] = path
165
+ end
166
+
167
+ parser.on(
168
+ "-h",
169
+ "--help",
170
+ "Show this help"
171
+ ) do
172
+ @options[:help] = true
173
+ end
174
+ end
175
+ end
176
+
177
+ def parse_page_range(value)
178
+ match = value.match(/\A(\d+)-(\d+)\z/)
179
+
180
+ unless match
181
+ raise OptionParser::InvalidArgument,
182
+ "page range must use FIRST-LAST"
183
+ end
184
+
185
+ first = Integer(match[1])
186
+ last = Integer(match[2])
187
+
188
+ if first.zero? || last.zero? || first > last
189
+ raise OptionParser::InvalidArgument,
190
+ "page range must be positive and ordered"
191
+ end
192
+
193
+ first..last
194
+ end
195
+
196
+ def positive_integer!(value, option)
197
+ return if value.positive?
198
+
199
+ raise OptionParser::InvalidArgument,
200
+ "#{option} must be positive"
201
+ end
202
+
203
+ def extractor_options
204
+ @options.reject do |name, value|
205
+ name == :pretty || value.nil?
206
+ end
207
+ end
208
+
209
+ def print_result(result)
210
+ payload = result.to_h
211
+
212
+ json =
213
+ if @options[:pretty]
214
+ JSON.pretty_generate(payload)
215
+ else
216
+ JSON.generate(payload)
217
+ end
218
+
219
+ @stdout.puts json
220
+ end
221
+
222
+ def print_exception(exception)
223
+ payload = {
224
+ success: false,
225
+ error: {
226
+ code: underscore(exception.class.name.split("::").last),
227
+ message: exception.message
228
+ }
229
+ }
230
+
231
+ json =
232
+ if @options[:pretty]
233
+ JSON.pretty_generate(payload)
234
+ else
235
+ JSON.generate(payload)
236
+ end
237
+
238
+ @stderr.puts json
239
+ end
240
+
241
+ def underscore(value)
242
+ value
243
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
244
+ .downcase
245
+ end
246
+ end
247
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+
5
+ module InvoiceExtractorArca
6
+ class CommandRunner
7
+ def run(*command)
8
+ stdout, stderr, status = Open3.capture3(*command)
9
+
10
+ {
11
+ stdout: stdout,
12
+ stderr: stderr,
13
+ status: status.exitstatus,
14
+ success: status.success?
15
+ }
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module InvoiceExtractorArca
4
+ class Configuration
5
+ attr_reader :strict,
6
+ :dependency_paths,
7
+ :page_range,
8
+ :resolution,
9
+ :multiple_qr,
10
+ :qr_scanner,
11
+ :python_command,
12
+ :enrichment
13
+
14
+ def initialize(
15
+ strict: false,
16
+ dependency_paths: {},
17
+ page_range: nil,
18
+ resolution: 300,
19
+ multiple_qr: :first_valid,
20
+ qr_scanner: :zbar_then_qreader,
21
+ python_command: :python3,
22
+ enrichment: true
23
+ )
24
+ @strict = strict
25
+ @dependency_paths = dependency_paths || {}
26
+ @page_range = page_range
27
+ @resolution = resolution
28
+ @multiple_qr = multiple_qr
29
+ @qr_scanner = qr_scanner
30
+ @python_command = python_command
31
+ @enrichment = enrichment
32
+ end
33
+
34
+ def strict?
35
+ strict == true
36
+ end
37
+
38
+ def enrichment?
39
+ enrichment == true
40
+ end
41
+ end
42
+ end