ruby_danfe 0.9.0 → 1.11.5
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.
- data/.gitignore +7 -0
- data/.rspec +1 -0
- data/Gemfile +4 -0
- data/README.md +122 -0
- data/Rakefile +28 -0
- data/lib/ruby_danfe.rb +1 -325
- data/lib/ruby_danfe/cst.rb +28 -0
- data/lib/ruby_danfe/dacte_generator.rb +511 -0
- data/lib/ruby_danfe/danfe_generator.rb +287 -0
- data/lib/ruby_danfe/danfe_nfce_generator.rb +237 -0
- data/lib/ruby_danfe/descricao.rb +44 -0
- data/lib/ruby_danfe/document.rb +80 -0
- data/lib/ruby_danfe/helper.rb +68 -0
- data/lib/ruby_danfe/options.rb +24 -0
- data/lib/ruby_danfe/railtie.rb +20 -0
- data/lib/ruby_danfe/ruby_danfe.rb +49 -0
- data/lib/ruby_danfe/version.rb +3 -0
- data/lib/ruby_danfe/xml.rb +58 -0
- data/ruby_danfe.gemspec +26 -11
- data/spec/features/ruby_danfe_spec.rb +59 -0
- data/spec/fixtures/4_decimals_nfe_simples_nacional.xml +1 -0
- data/spec/fixtures/cte.xml +1 -0
- data/spec/fixtures/nfe_date_format_infoadprod_infoadfisco_issues.xml +42 -0
- data/spec/fixtures/nfe_simples_nacional.xml +1 -0
- data/spec/fixtures/nfe_with_fci.xml +1 -0
- data/spec/fixtures/nfe_with_ns.xml +4 -0
- data/spec/fixtures/nfe_without_ns.xml +2 -0
- data/spec/lib/cst_spec.rb +51 -0
- data/spec/lib/descricao_spec.rb +129 -0
- data/spec/lib/helper_spec.rb +88 -0
- data/spec/lib/options_spec.rb +13 -0
- data/spec/lib/ruby_danfe_spec.rb +43 -0
- data/spec/spec_helper.rb +21 -0
- data/spec/support/be_same_file_as.rb +9 -0
- data/test/danfe.xml +42 -0
- data/test/generate.rb +8 -0
- data/test/generate_nfce.rb +8 -0
- data/test/nfce.xml +147 -0
- data/test/nfe_simples_nacional.xml +1 -0
- data/test/nfe_with_fci.xml +1 -0
- data/test/nfe_with_ns.xml +4 -0
- data/test/nfe_without_ns.xml +2 -0
- metadata +167 -75
@@ -0,0 +1,44 @@
|
|
1
|
+
#encoding: utf-8
|
2
|
+
|
3
|
+
module RubyDanfe
|
4
|
+
class Descricao
|
5
|
+
LINEBREAK = "\n"
|
6
|
+
|
7
|
+
def self.generate(det)
|
8
|
+
descricao = "#{det.css('prod/xProd').text}"
|
9
|
+
|
10
|
+
if need_infAdProd(det)
|
11
|
+
descricao += LINEBREAK
|
12
|
+
descricao += det.css('infAdProd').text
|
13
|
+
end
|
14
|
+
|
15
|
+
if need_fci(det)
|
16
|
+
descricao += LINEBREAK
|
17
|
+
descricao += "FCI: #{det.css('prod/nFCI').text}"
|
18
|
+
end
|
19
|
+
|
20
|
+
if need_st(det)
|
21
|
+
descricao += LINEBREAK
|
22
|
+
descricao += "ST: MVA: #{det.css('ICMS/*/pMVAST').text}% "
|
23
|
+
descricao += "* Alíq: #{det.css('ICMS/*/pICMSST').text}% "
|
24
|
+
descricao += "* BC: #{det.css('ICMS/*/vBCST').text} "
|
25
|
+
descricao += "* Vlr: #{det.css('ICMS/*/vICMSST').text}"
|
26
|
+
end
|
27
|
+
|
28
|
+
descricao
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
def self.need_infAdProd(det)
|
33
|
+
!det.css('infAdProd').text.empty?
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.need_fci(det)
|
37
|
+
!det.css('prod/nFCI').text.empty?
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.need_st(det)
|
41
|
+
det.css('ICMS/*/vBCST').text.to_i > 0
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
module RubyDanfe
|
2
|
+
class Document
|
3
|
+
def initialize
|
4
|
+
@document = Prawn::Document.new(
|
5
|
+
:page_size => 'A4',
|
6
|
+
:page_layout => :portrait,
|
7
|
+
:left_margin => 0,
|
8
|
+
:right_margin => 0,
|
9
|
+
:top_margin => 0,
|
10
|
+
:botton_margin => 0
|
11
|
+
)
|
12
|
+
|
13
|
+
@document.font "Times-Roman"
|
14
|
+
end
|
15
|
+
|
16
|
+
def method_missing(method_name, *args, &block)
|
17
|
+
@document.send(method_name, *args, &block)
|
18
|
+
end
|
19
|
+
|
20
|
+
def respond_to_missing?(method_name, include_private = false)
|
21
|
+
@document.respond_to?(method_name, include_private) || super
|
22
|
+
end
|
23
|
+
|
24
|
+
def ititle(h, w, x, y, title)
|
25
|
+
self.text_box title, :size => 10, :at => [x.cm, Helper.invert(y.cm) - 2], :width => w.cm, :height => h.cm, :style => :bold
|
26
|
+
end
|
27
|
+
|
28
|
+
def ibarcode(h, w, x, y, info)
|
29
|
+
Barby::Code128C.new(info).annotate_pdf(self, :x => x.cm, :y => Helper.invert(y.cm), :width => w.cm, :height => h.cm) if info != ''
|
30
|
+
end
|
31
|
+
|
32
|
+
def iqrcode(h, w, x, y, info)
|
33
|
+
Barby::QrCode.new(info, :level => :q).annotate_pdf(self, :x => x.cm, :y => Helper.invert(y.cm), :width => w.cm, :height => h.cm) if info != ''
|
34
|
+
end
|
35
|
+
|
36
|
+
def irectangle(h, w, x, y)
|
37
|
+
self.stroke_rectangle [x.cm, Helper.invert(y.cm)], w.cm, h.cm
|
38
|
+
end
|
39
|
+
|
40
|
+
def ibox(h, w, x, y, title = '', info = '', options = {})
|
41
|
+
box [x.cm, Helper.invert(y.cm)], w.cm, h.cm, title, info, options
|
42
|
+
end
|
43
|
+
|
44
|
+
def idate(h, w, x, y, title = '', info = '', options = {})
|
45
|
+
tt = info.gsub(/T.*/, '').split('-')
|
46
|
+
ibox h, w, x, y, title, "#{tt[2]}/#{tt[1]}/#{tt[0]}", options
|
47
|
+
end
|
48
|
+
|
49
|
+
def box(at, w, h, title = '', info = '', options = {})
|
50
|
+
options = {
|
51
|
+
:align => :left,
|
52
|
+
:size => 10,
|
53
|
+
:style => nil,
|
54
|
+
:valign => :top,
|
55
|
+
:border => 1
|
56
|
+
}.merge(options)
|
57
|
+
self.stroke_rectangle at, w, h if options[:border] == 1
|
58
|
+
self.text_box title, :size => 6, :style => :italic, :at => [at[0] + 2, at[1] - 2], :width => w - 4, :height => 8 if title != ''
|
59
|
+
self.text_box info, :size => options[:size], :at => [at[0] + 2, at[1] - (title != '' ? 14 : 4) ], :width => w - 4, :height => h - (title != '' ? 14 : 4), :align => options[:align], :style => options[:style], :valign => options[:valign]
|
60
|
+
end
|
61
|
+
|
62
|
+
def inumeric(h, w, x, y, title = '', info = '', options = {})
|
63
|
+
numeric [x.cm, Helper.invert(y.cm)], w.cm, h.cm, title, info, options
|
64
|
+
end
|
65
|
+
|
66
|
+
def numeric(at, w, h, title = '', info = '', options = {})
|
67
|
+
options = {:decimals => 2}.merge(options)
|
68
|
+
info = Helper.numerify(info, options[:decimals]) if info != ''
|
69
|
+
box at, w, h, title, info, options.merge({:align => :right})
|
70
|
+
end
|
71
|
+
|
72
|
+
def itable(h, w, x, y, data, options = {}, &block)
|
73
|
+
self.bounding_box [x.cm, Helper.invert(y.cm)], :width => w.cm, :height => h.cm do
|
74
|
+
self.table data, options do |table|
|
75
|
+
yield(table)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module RubyDanfe
|
2
|
+
class Helper
|
3
|
+
def self.numerify(number, decimals = 2)
|
4
|
+
number = number.tr("\n","").delete(" ")
|
5
|
+
return "" if !number || number == ""
|
6
|
+
int, frac = ("%.#{decimals}f" % number).split(".")
|
7
|
+
int.gsub!(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1\.")
|
8
|
+
int + "," + frac
|
9
|
+
rescue
|
10
|
+
number
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.invert(y)
|
14
|
+
28.7.cm - y
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.format_quantity(qty)
|
18
|
+
return Helper.numerify(qty, RubyDanfe.options.quantity_decimals) if RubyDanfe.options.numerify_prod_qcom
|
19
|
+
qty.gsub!(",", ".")
|
20
|
+
qty[qty.rindex('.')] = ',' if qty.rindex('.')
|
21
|
+
qty
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.format_datetime(string)
|
25
|
+
formated_datetime = ""
|
26
|
+
|
27
|
+
if not string.empty?
|
28
|
+
date = DateTime.strptime(string, "%Y-%m-%dT%H:%M:%S")
|
29
|
+
formated_datetime = date.strftime("%d/%m/%Y %H:%M:%S")
|
30
|
+
end
|
31
|
+
|
32
|
+
formated_datetime
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.format_date(string)
|
36
|
+
formated_date = ""
|
37
|
+
|
38
|
+
if not string.empty?
|
39
|
+
date = Date.strptime(string, "%Y-%m-%d")
|
40
|
+
formated_date = date.strftime("%d/%m/%Y")
|
41
|
+
end
|
42
|
+
|
43
|
+
formated_date
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.format_time(string)
|
47
|
+
formated_time = ""
|
48
|
+
|
49
|
+
if not string.empty?
|
50
|
+
time = Time.strptime(string, "%H:%M:%S")
|
51
|
+
formated_time = time.strftime("%H:%M:%S")
|
52
|
+
end
|
53
|
+
|
54
|
+
formated_time
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.extract_time_from_date(string)
|
58
|
+
formated_time = ""
|
59
|
+
|
60
|
+
if not string.empty?
|
61
|
+
date = DateTime.strptime(string, "%Y-%m-%dT%H:%M:%S")
|
62
|
+
formated_time = date.strftime("%H:%M:%S")
|
63
|
+
end
|
64
|
+
|
65
|
+
formated_time
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module RubyDanfe
|
2
|
+
class Options < OpenStruct
|
3
|
+
|
4
|
+
DEFAULTOPTIONS = {
|
5
|
+
quantity_decimals: 2,
|
6
|
+
numerify_prod_qcom: false
|
7
|
+
}
|
8
|
+
|
9
|
+
def initialize(new_options={})
|
10
|
+
options = DEFAULTOPTIONS.merge(config_yaml_load)
|
11
|
+
super options.merge(new_options)
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
def file
|
16
|
+
File.exists?("config/ruby_danfe.yml") ? File.open("config/ruby_danfe.yml").read : ""
|
17
|
+
end
|
18
|
+
|
19
|
+
def config_yaml_load
|
20
|
+
@file_read = YAML.load(file)
|
21
|
+
@file_read ? (@file_read["ruby_danfe"]||{})["options"] : {}
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require "prawn"
|
2
|
+
require "prawn/measurement_extensions"
|
3
|
+
require "barby"
|
4
|
+
require "barby/barcode/code_128"
|
5
|
+
require "barby/outputter/prawn_outputter"
|
6
|
+
require "nokogiri"
|
7
|
+
require 'ostruct'
|
8
|
+
require 'yaml'
|
9
|
+
|
10
|
+
require_relative '../ruby_danfe.rb'
|
11
|
+
require_relative 'options.rb'
|
12
|
+
require_relative 'helper.rb'
|
13
|
+
require_relative 'cst.rb'
|
14
|
+
require_relative 'document.rb'
|
15
|
+
require_relative 'version.rb'
|
16
|
+
require_relative 'dacte_generator.rb'
|
17
|
+
require_relative 'danfe_generator.rb'
|
18
|
+
require_relative 'xml.rb'
|
19
|
+
require_relative 'descricao.rb'
|
20
|
+
require_relative 'ruby_danfe.rb'
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# encoding:utf-8
|
2
|
+
module RubyDanfe
|
3
|
+
def self.generate(pdf_filename, xml_filename, type = :danfe, new_options = {})
|
4
|
+
self.options = new_options if !new_options.empty?
|
5
|
+
|
6
|
+
xml_string = File.new(xml_filename)
|
7
|
+
render_file(pdf_filename, xml_string, type)
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.render(xml_string, type = :danfe, new_options = {})
|
11
|
+
self.options = new_options if !new_options.empty?
|
12
|
+
|
13
|
+
pdf = generatePDF(xml_string, type)
|
14
|
+
pdf.render
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.render_file(pdf_filename, xml_string, type = :danfe, new_options = {})
|
18
|
+
self.options = new_options if !new_options.empty?
|
19
|
+
|
20
|
+
pdf = generatePDF(xml_string, type)
|
21
|
+
pdf.render_file pdf_filename
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.options
|
25
|
+
@options ||= RubyDanfe::Options.new
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.options=(new_options = {})
|
29
|
+
@options = RubyDanfe::Options.new(new_options)
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
def self.generatePDF(xml_string, type = :danfe, new_options = {})
|
34
|
+
self.options = new_options if !new_options.empty?
|
35
|
+
|
36
|
+
xml = XML.new(xml_string)
|
37
|
+
|
38
|
+
if type == :danfe
|
39
|
+
generator = DanfeGenerator.new(xml)
|
40
|
+
elsif type == :danfe_nfce
|
41
|
+
generator = DanfeNfceGenerator.new(xml)
|
42
|
+
else
|
43
|
+
generator = DacteGenerator.new(xml)
|
44
|
+
end
|
45
|
+
|
46
|
+
pdf = generator.generatePDF
|
47
|
+
pdf
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module RubyDanfe
|
2
|
+
class XML
|
3
|
+
def css(xpath)
|
4
|
+
@xml.css(xpath)
|
5
|
+
end
|
6
|
+
|
7
|
+
def xpath(regex)
|
8
|
+
doc = Nokogiri::HTML(@xml.to_s)
|
9
|
+
return doc.xpath(regex)
|
10
|
+
end
|
11
|
+
|
12
|
+
def regex_string(search_string, regex)
|
13
|
+
doc = Nokogiri::HTML(search_string)
|
14
|
+
return doc.xpath(regex)
|
15
|
+
end
|
16
|
+
|
17
|
+
def initialize(xml)
|
18
|
+
@xml = Nokogiri::XML(xml)
|
19
|
+
end
|
20
|
+
|
21
|
+
def [](xpath)
|
22
|
+
node = @xml.css(xpath)
|
23
|
+
return node ? node.text : ""
|
24
|
+
end
|
25
|
+
|
26
|
+
def render
|
27
|
+
if @xml.at_css('infNFe/ide')
|
28
|
+
RubyDanfe.render @xml.to_s, :danfe
|
29
|
+
else
|
30
|
+
RubyDanfe.render @xml.to_s, :dacte
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def collect(ns, tag, &block)
|
35
|
+
result = []
|
36
|
+
# Tenta primeiro com uso de namespace
|
37
|
+
begin
|
38
|
+
@xml.xpath("//#{ns}:#{tag}").each do |det|
|
39
|
+
result << yield(det)
|
40
|
+
end
|
41
|
+
rescue
|
42
|
+
# Caso dê erro, tenta sem
|
43
|
+
@xml.xpath("//#{tag}").each do |det|
|
44
|
+
result << yield(det)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
result
|
48
|
+
end
|
49
|
+
|
50
|
+
def attrib(node, attrib)
|
51
|
+
begin
|
52
|
+
return @xml.css(node).attr(attrib).text
|
53
|
+
rescue
|
54
|
+
""
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
data/ruby_danfe.gemspec
CHANGED
@@ -1,12 +1,27 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
1
|
+
# coding: utf-8
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "ruby_danfe/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "ruby_danfe"
|
7
|
+
spec.version = RubyDanfe::VERSION
|
8
|
+
spec.summary = "DANFE and DACTE pdf generator for Brazilian invoices and transportation docs."
|
9
|
+
spec.author = "Eduardo Reboucas"
|
10
|
+
spec.email = "eduardo.reboucas@gmail.com"
|
11
|
+
spec.homepage = "http://github.com/taxweb/ruby_danfe"
|
12
|
+
spec.license = "MIT"
|
13
|
+
|
14
|
+
spec.files = `git ls-files`.split("\n")
|
15
|
+
spec.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
16
|
+
spec.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
17
|
+
spec.require_paths = ["lib"]
|
18
|
+
|
19
|
+
spec.add_dependency "nokogiri"
|
20
|
+
spec.add_dependency "prawn", '~> 1.0.0'
|
21
|
+
spec.add_dependency "barby"
|
22
|
+
spec.add_dependency "rake"
|
23
|
+
|
24
|
+
spec.add_development_dependency "pry"
|
25
|
+
spec.add_development_dependency "rspec"
|
26
|
+
spec.add_development_dependency "simplecov"
|
12
27
|
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe "RubyDanfe generated pdf files" do
|
4
|
+
let(:base_dir) { "./spec/fixtures/"}
|
5
|
+
let(:output_pdf) { "#{base_dir}output.pdf" }
|
6
|
+
|
7
|
+
before {
|
8
|
+
File.delete(output_pdf) if File.exist?(output_pdf)
|
9
|
+
RubyDanfe.options = {"quantity_decimals" => 2}
|
10
|
+
}
|
11
|
+
|
12
|
+
it "renders a basic NF-e with namespace" do
|
13
|
+
expect(File.exist?(output_pdf)).to be_false
|
14
|
+
|
15
|
+
RubyDanfe.generate(output_pdf, "#{base_dir}nfe_with_ns.xml")
|
16
|
+
|
17
|
+
expect("#{base_dir}nfe_with_ns.xml.fixture.pdf").to be_same_file_as(output_pdf)
|
18
|
+
end
|
19
|
+
|
20
|
+
it "renders another basic NF-e without namespace" do
|
21
|
+
expect(File.exist?(output_pdf)).to be_false
|
22
|
+
|
23
|
+
RubyDanfe.generate(output_pdf, "#{base_dir}nfe_without_ns.xml")
|
24
|
+
|
25
|
+
expect("#{base_dir}nfe_without_ns.xml.fixture.pdf").to be_same_file_as(output_pdf)
|
26
|
+
end
|
27
|
+
|
28
|
+
it "renders a NF-e having FCI in its items" do
|
29
|
+
expect(File.exist?(output_pdf)).to be_false
|
30
|
+
|
31
|
+
RubyDanfe.generate(output_pdf, "#{base_dir}nfe_with_fci.xml")
|
32
|
+
|
33
|
+
expect("#{base_dir}nfe_with_fci.xml.fixture.pdf").to be_same_file_as(output_pdf)
|
34
|
+
end
|
35
|
+
|
36
|
+
it "renders a NF-e of Simples Nacional using CSOSN" do
|
37
|
+
expect(File.exist?(output_pdf)).to be_false
|
38
|
+
|
39
|
+
RubyDanfe.generate(output_pdf, "#{base_dir}nfe_simples_nacional.xml")
|
40
|
+
|
41
|
+
expect("#{base_dir}nfe_simples_nacional.xml.fixture.pdf").to be_same_file_as(output_pdf)
|
42
|
+
end
|
43
|
+
|
44
|
+
it "renders a basic CT-e" do
|
45
|
+
expect(File.exist?(output_pdf)).to be_false
|
46
|
+
|
47
|
+
RubyDanfe.generate(output_pdf, "#{base_dir}cte.xml", :dacte)
|
48
|
+
|
49
|
+
expect("#{base_dir}cte.xml.fixture.pdf").to be_same_file_as(output_pdf)
|
50
|
+
end
|
51
|
+
|
52
|
+
it "renders quantity field with 4 decimals" do
|
53
|
+
expect(File.exist?(output_pdf)).to be_false
|
54
|
+
|
55
|
+
RubyDanfe.generate(output_pdf, "#{base_dir}4_decimals_nfe_simples_nacional.xml", :danfe, {"quantity_decimals" => 4})
|
56
|
+
|
57
|
+
expect("#{base_dir}4_decimals_nfe_simples_nacional.xml.fixture.pdf").to be_same_file_as(output_pdf)
|
58
|
+
end
|
59
|
+
end
|