butterware-kashflow 0.0.9

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/kashflow.gemspec ADDED
@@ -0,0 +1,72 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = "kashflow"
8
+ s.version = "0.0.8"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Pogodan"]
12
+ s.date = "2012-03-30"
13
+ s.description = "A Ruby wrapper for the Kashflow Accouting SOAP API"
14
+ s.email = "dev@pogodan.com"
15
+ s.extra_rdoc_files = [
16
+ "LICENSE.txt",
17
+ "README.mkd"
18
+ ]
19
+ s.files = [
20
+ ".document",
21
+ "Gemfile",
22
+ "Gemfile.lock",
23
+ "LICENSE.txt",
24
+ "README.mkd",
25
+ "Rakefile",
26
+ "VERSION",
27
+ "config/kf_api_methods.yml",
28
+ "kashflow.gemspec",
29
+ "lib/kashflow.rb",
30
+ "lib/kashflow/api.rb",
31
+ "lib/kashflow/api_method.rb",
32
+ "lib/kashflow/client.rb",
33
+ "test/helper.rb",
34
+ "test/test_kashflow.rb"
35
+ ]
36
+ s.homepage = "http://www.pogodan.com/projects"
37
+ s.licenses = ["MIT"]
38
+ s.require_paths = ["lib"]
39
+ s.rubygems_version = "1.8.19"
40
+ s.summary = "A Ruby wrapper for the Kashflow SOAP API"
41
+
42
+ if s.respond_to? :specification_version then
43
+ s.specification_version = 3
44
+
45
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
46
+ s.add_runtime_dependency(%q<savon>, ["~> 0.9.9"])
47
+ s.add_runtime_dependency(%q<nokogiri>, [">= 0"])
48
+ s.add_development_dependency(%q<shoulda>, [">= 0"])
49
+ s.add_development_dependency(%q<bundler>, [">= 0"])
50
+ s.add_development_dependency(%q<jeweler>, [">= 0"])
51
+ s.add_development_dependency(%q<i18n>, [">= 0"])
52
+ s.add_development_dependency(%q<activesupport>, [">= 0"])
53
+ else
54
+ s.add_dependency(%q<savon>, ["~> 0.9.9"])
55
+ s.add_dependency(%q<nokogiri>, [">= 0"])
56
+ s.add_dependency(%q<shoulda>, [">= 0"])
57
+ s.add_dependency(%q<bundler>, [">= 0"])
58
+ s.add_dependency(%q<jeweler>, [">= 0"])
59
+ s.add_dependency(%q<i18n>, [">= 0"])
60
+ s.add_dependency(%q<activesupport>, [">= 0"])
61
+ end
62
+ else
63
+ s.add_dependency(%q<savon>, ["~> 0.9.9"])
64
+ s.add_dependency(%q<nokogiri>, [">= 0"])
65
+ s.add_dependency(%q<shoulda>, [">= 0"])
66
+ s.add_dependency(%q<bundler>, [">= 0"])
67
+ s.add_dependency(%q<jeweler>, [">= 0"])
68
+ s.add_dependency(%q<i18n>, [">= 0"])
69
+ s.add_dependency(%q<activesupport>, [">= 0"])
70
+ end
71
+ end
72
+
@@ -0,0 +1 @@
1
+ require 'kashflow'
data/lib/kashflow.rb ADDED
@@ -0,0 +1,21 @@
1
+ require 'ostruct'
2
+ require 'savon'
3
+
4
+ require File.expand_path('../kashflow/client', __FILE__)
5
+ require File.expand_path('../kashflow/api_method', __FILE__)
6
+ require File.expand_path('../kashflow/api', __FILE__)
7
+
8
+ module Kashflow
9
+ def self.client(login, password)
10
+ @client ||= Kashflow::Client.new(login, password)
11
+ end
12
+
13
+ def self.method_missing(method, *args, &block)
14
+ return super unless client.respond_to?(method)
15
+ client.send(method, *args, &block)
16
+ end
17
+
18
+ def self.root
19
+ File.dirname(File.expand_path('..', __FILE__))
20
+ end
21
+ end
@@ -0,0 +1,64 @@
1
+ require 'open-uri'
2
+ require 'nokogiri'
3
+ require 'active_support/core_ext/object'
4
+ require 'active_support/core_ext/hash'
5
+
6
+ ## Kashflow.export_yaml will scrape the public API site for methods and
7
+ ## dump them into the YAML config we use. we probably can/should just get this
8
+ ## from the WSDL directly, but Savon wasn't working right, so this is the stopgap measure
9
+ module Kashflow
10
+ class Api
11
+ class << self
12
+
13
+ def api_page(path)
14
+ cache_path = File.join(Kashflow.root, 'tmp', 'kf_api_pages', path)
15
+
16
+ unless File.exists?(cache_path)
17
+ File.open(cache_path, 'w'){ |f| f << open("http://accountingapi.com/#{path}").read }
18
+ end
19
+
20
+ Nokogiri::HTML(open(cache_path))
21
+ end
22
+
23
+ def apis_from_web
24
+ main_page = api_page('manual_class_customer.asp')
25
+
26
+ main_page.search('#mr_manual_inner a').select do |node|
27
+ node['href'] =~ /manual_methods/ and
28
+ node['href'] != 'manual_methods_overview.asp'
29
+
30
+ end.map{|n| new(n.content, n['href']) }
31
+ end
32
+
33
+ def export_yaml
34
+ api_methods = apis_from_web.map { |a| ApiMethod.new(a.name, a.import) }
35
+
36
+ File.open(Client.yaml_path, 'w') do |f|
37
+ YAML.dump(api_methods, f)
38
+ end
39
+ end
40
+
41
+ end
42
+
43
+ attr_reader :name, :page
44
+ def initialize(name, page)
45
+ @name = name
46
+ @page = page
47
+ end
48
+
49
+ def api_page
50
+ @api_page ||= self.class.api_page(page)
51
+ end
52
+
53
+ def import
54
+ @fields ||= api_page.search('.mnl_tbl1 tr').map do |row|
55
+ {
56
+ :name => row.search('span.mnl_varName').first.try(:content),
57
+ :type => row.search('span.mnl_varType').first.try(:content),
58
+ :direction => row.search('td strong').first.try(:content),
59
+ :desc => row.search('td').last.try(:content)
60
+ }
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,19 @@
1
+ module Kashflow
2
+ class ApiMethod
3
+ attr_accessor :name, :request_attrs, :response_attrs
4
+
5
+ def initialize(name, fields)
6
+ @name = name
7
+
8
+ # split into request/response attrs
9
+ @request_attrs, @response_attrs = fields.partition{|f| f[:direction] == 'IN' }.map do |arr|
10
+ arr.map do |fields|
11
+ # get rid of the :direction and cleanup the description text
12
+ fields.slice!(:type, :desc, :name)
13
+ fields[:desc].try(:strip!)
14
+ fields
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,133 @@
1
+ module Kashflow
2
+ class Client
3
+ attr_reader :service
4
+
5
+ def self.yaml_path
6
+ File.join(Kashflow.root, 'config', 'kf_api_methods.yml')
7
+ end
8
+
9
+ def self.api_methods
10
+ @@api_methods ||= YAML.load_file(yaml_path)
11
+ end
12
+
13
+ def initialize(login, password)
14
+ raise "missing login/password" unless login and password
15
+
16
+ @login, @password = login, password
17
+ @service = Savon::Client.new do |wsdl, http|
18
+ wsdl.document = "https://securedwebapp.com/api/service.asmx?wsdl"
19
+ http.auth.ssl.verify_mode = :none
20
+ end
21
+ end
22
+
23
+ def lookup_api_method(name)
24
+ self.class.api_methods.detect{ |api_method| api_method.name.underscore == name.to_s }
25
+ end
26
+
27
+ def method_missing(m, *args, &block)
28
+ api_method = lookup_api_method(m)
29
+
30
+ if api_method
31
+ # puts "found api_method #{api_method.name} for #{m}: #{api_method.request_attrs.inspect}"
32
+ # puts "you're calling with #{args.inspect}"
33
+
34
+ api_call(m, api_method.name, args)
35
+ else
36
+ raise "method_missing: No method for #{m}"
37
+ end
38
+ end
39
+
40
+ def api_call(name, method, args)
41
+ soap_return = soap_call(name, method, args)
42
+ response = soap_return["#{name}_response".to_sym]
43
+ # puts "got response: " + response.inspect
44
+
45
+ raise "API call failed: [#{response[:status_detail]}]\n\n #{response.inspect}" unless response[:status] == 'OK'
46
+
47
+ r = response["#{name}_result".to_sym]
48
+ if r.is_a?(String)
49
+ r
50
+ else
51
+ if r.is_a?(Enumerable)
52
+ if r.values.all?{|v| v.is_a?(Array) }# || r.keys.size == 1
53
+ object_type, attrs = r.first
54
+ else
55
+ # puts "arrayifying #{r.inspect}"
56
+ object_type = lookup_api_method(name).response_attrs.first[:type]
57
+ attrs = r.first.last.is_a?(Hash) ? [r.first.last] : [r]
58
+ end
59
+
60
+ # puts "it's an enumerable... #{object_type} | #{attrs.inspect}"
61
+
62
+ ostructs = attrs.map do |record_attrs|
63
+ # puts "making new ostruct with #{record_attrs.inspect}"
64
+ OpenStruct.new(record_attrs.merge(:object_type => object_type.to_s))
65
+ end
66
+ #r.first.last
67
+ else
68
+ #puts "it's a #{r.class}"
69
+ r
70
+ end
71
+ end
72
+ end
73
+
74
+ def object_wrapper(name, params_xml)
75
+ object_alias = {:customer => "custr", :quote => "quote", :invoice => "Inv", :supplier => "supl", :receipt => "Inv", :line => "InvLine"}
76
+ needs_object = [ "insert", "update" ]
77
+ operation, object, line = name.to_s.split("_")
78
+ if needs_object.include? operation
79
+ text = object_alias[object.to_sym]
80
+ text = "sup" if operation == "update" and object == "supplier"
81
+ if line
82
+ text = object_alias[:line]
83
+ case name.to_s
84
+ when "insert_invoice_line_with_invoice_number"
85
+ line_id = "<InvoiceNumber>#{params_xml.match(/<InvoiceNumber>(.*?)<\/InvoiceNumber>/)[1]}</InvoiceNumber>\n\t\t"
86
+ else
87
+ line_id = "<ReceiptID>#{params_xml.match(/<ReceiptID>(.*?)<\/ReceiptID>/)[1]}</ReceiptID>\n\t\t" if object == "receipt"
88
+ line_id = "<InvoiceID>#{params_xml.match(/<InvoiceID>(.*?)<\/InvoiceID>/)[1]}</InvoiceID>\n\t\t" if object == "invoice"
89
+ end
90
+ end
91
+ return ["#{line_id}<#{text}>", "</#{text}>"]
92
+ else
93
+ return ["",""]
94
+ end
95
+ end
96
+
97
+ # called with CamelCase version of method name
98
+ def soap_call(name, method, params = {})
99
+ # puts "name = #{name}, method = #{method}, params = #{params.inspect}"
100
+ begin
101
+ result = @service.request(name) do |soap|
102
+ # soap.action = "KashFlow/#{method}"
103
+
104
+ params = params.pop if params.is_a?(Array)
105
+ params_xml = params.map do |field, value|
106
+ xml_tag = field.to_s.camelize
107
+ "<#{xml_tag}>#{value}</#{xml_tag}>"
108
+ end.join("\n") unless params.blank?
109
+
110
+ params_xml = params_xml.gsub(/Id>/,"ID>") if params_xml
111
+ params_xml = params_xml.gsub(/Dbid>/,"DBID>") if params_xml
112
+ pretext, posttext = object_wrapper(name, params_xml)
113
+
114
+ soap.xml = %[<?xml version="1.0" encoding="utf-8"?>
115
+ <soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
116
+ <soap:Body>
117
+ <#{method} xmlns="KashFlow">
118
+ <UserName>#{@login}</UserName>
119
+ <Password>#{@password}</Password>
120
+ #{pretext}
121
+ #{params_xml}
122
+ #{posttext}
123
+ </#{method}>
124
+ </soap:Body>
125
+ </soap:Envelope>]
126
+ end.to_hash
127
+ rescue Savon::SOAP::Fault => e
128
+ puts "soap fault:" + e.inspect
129
+ return false
130
+ end
131
+ end
132
+ end
133
+ end
data/test/helper.rb ADDED
@@ -0,0 +1,18 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ begin
4
+ Bundler.setup(:default, :development)
5
+ rescue Bundler::BundlerError => e
6
+ $stderr.puts e.message
7
+ $stderr.puts "Run `bundle install` to install missing gems"
8
+ exit e.status_code
9
+ end
10
+ require 'test/unit'
11
+ require 'shoulda'
12
+
13
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
14
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
15
+ require 'kashflow'
16
+
17
+ class Test::Unit::TestCase
18
+ end
@@ -0,0 +1,7 @@
1
+ require 'helper'
2
+
3
+ class TestKashflow < Test::Unit::TestCase
4
+ should "probably rename this file and start testing for real" do
5
+ flunk "hey buddy, you should probably rename this file and start testing for real"
6
+ end
7
+ end
metadata ADDED
@@ -0,0 +1,180 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: butterware-kashflow
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.9
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Pogodan
9
+ - David Bourguignon
10
+ autorequire:
11
+ bindir: bin
12
+ cert_chain: []
13
+ date: 2012-08-31 00:00:00.000000000 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: savon
17
+ requirement: !ruby/object:Gem::Requirement
18
+ none: false
19
+ requirements:
20
+ - - ~>
21
+ - !ruby/object:Gem::Version
22
+ version: 0.9.9
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ none: false
27
+ requirements:
28
+ - - ~>
29
+ - !ruby/object:Gem::Version
30
+ version: 0.9.9
31
+ - !ruby/object:Gem::Dependency
32
+ name: nokogiri
33
+ requirement: !ruby/object:Gem::Requirement
34
+ none: false
35
+ requirements:
36
+ - - ! '>='
37
+ - !ruby/object:Gem::Version
38
+ version: '0'
39
+ type: :runtime
40
+ prerelease: false
41
+ version_requirements: !ruby/object:Gem::Requirement
42
+ none: false
43
+ requirements:
44
+ - - ! '>='
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ - !ruby/object:Gem::Dependency
48
+ name: shoulda
49
+ requirement: !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ type: :development
56
+ prerelease: false
57
+ version_requirements: !ruby/object:Gem::Requirement
58
+ none: false
59
+ requirements:
60
+ - - ! '>='
61
+ - !ruby/object:Gem::Version
62
+ version: '0'
63
+ - !ruby/object:Gem::Dependency
64
+ name: bundler
65
+ requirement: !ruby/object:Gem::Requirement
66
+ none: false
67
+ requirements:
68
+ - - ! '>='
69
+ - !ruby/object:Gem::Version
70
+ version: '0'
71
+ type: :development
72
+ prerelease: false
73
+ version_requirements: !ruby/object:Gem::Requirement
74
+ none: false
75
+ requirements:
76
+ - - ! '>='
77
+ - !ruby/object:Gem::Version
78
+ version: '0'
79
+ - !ruby/object:Gem::Dependency
80
+ name: jeweler
81
+ requirement: !ruby/object:Gem::Requirement
82
+ none: false
83
+ requirements:
84
+ - - ! '>='
85
+ - !ruby/object:Gem::Version
86
+ version: '0'
87
+ type: :development
88
+ prerelease: false
89
+ version_requirements: !ruby/object:Gem::Requirement
90
+ none: false
91
+ requirements:
92
+ - - ! '>='
93
+ - !ruby/object:Gem::Version
94
+ version: '0'
95
+ - !ruby/object:Gem::Dependency
96
+ name: i18n
97
+ requirement: !ruby/object:Gem::Requirement
98
+ none: false
99
+ requirements:
100
+ - - ! '>='
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
103
+ type: :development
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ none: false
107
+ requirements:
108
+ - - ! '>='
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: activesupport
113
+ requirement: !ruby/object:Gem::Requirement
114
+ none: false
115
+ requirements:
116
+ - - ! '>='
117
+ - !ruby/object:Gem::Version
118
+ version: '0'
119
+ type: :development
120
+ prerelease: false
121
+ version_requirements: !ruby/object:Gem::Requirement
122
+ none: false
123
+ requirements:
124
+ - - ! '>='
125
+ - !ruby/object:Gem::Version
126
+ version: '0'
127
+ description: A Ruby wrapper for the Kashflow Accouting SOAP API
128
+ email: david@butterware.co.uk
129
+ executables: []
130
+ extensions: []
131
+ extra_rdoc_files:
132
+ - LICENSE.txt
133
+ - README.mkd
134
+ files:
135
+ - .document
136
+ - Gemfile
137
+ - Gemfile.lock
138
+ - LICENSE.txt
139
+ - README.mkd
140
+ - Rakefile
141
+ - VERSION
142
+ - butterware-kashflow.gemspec
143
+ - config/kf_api_methods.yml
144
+ - kashflow.gemspec
145
+ - lib/butterware-kashflow.rb
146
+ - lib/kashflow.rb
147
+ - lib/kashflow/api.rb
148
+ - lib/kashflow/api_method.rb
149
+ - lib/kashflow/client.rb
150
+ - test/helper.rb
151
+ - test/test_kashflow.rb
152
+ homepage: https://github.com/butterware/kashflow
153
+ licenses:
154
+ - MIT
155
+ post_install_message:
156
+ rdoc_options: []
157
+ require_paths:
158
+ - lib
159
+ required_ruby_version: !ruby/object:Gem::Requirement
160
+ none: false
161
+ requirements:
162
+ - - ! '>='
163
+ - !ruby/object:Gem::Version
164
+ version: '0'
165
+ segments:
166
+ - 0
167
+ hash: -2842399016050066755
168
+ required_rubygems_version: !ruby/object:Gem::Requirement
169
+ none: false
170
+ requirements:
171
+ - - ! '>='
172
+ - !ruby/object:Gem::Version
173
+ version: '0'
174
+ requirements: []
175
+ rubyforge_project:
176
+ rubygems_version: 1.8.19
177
+ signing_key:
178
+ specification_version: 3
179
+ summary: A Ruby wrapper for the Kashflow SOAP API
180
+ test_files: []