ydim 1.0.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.
- data/History.txt +6 -0
- data/LICENSE.txt +339 -0
- data/Manifest.txt +43 -0
- data/README.txt +21 -0
- data/Rakefile +28 -0
- data/bin/ydim-edit +55 -0
- data/bin/ydim-inject +33 -0
- data/bin/ydimd +55 -0
- data/install.rb +1098 -0
- data/lib/ydim/autoinvoicer.rb +41 -0
- data/lib/ydim/client.rb +29 -0
- data/lib/ydim/config.rb +32 -0
- data/lib/ydim/currency_converter.rb +28 -0
- data/lib/ydim/currency_updater.rb +39 -0
- data/lib/ydim/debitor.rb +86 -0
- data/lib/ydim/factory.rb +58 -0
- data/lib/ydim/invoice.rb +146 -0
- data/lib/ydim/item.rb +36 -0
- data/lib/ydim/mail.rb +95 -0
- data/lib/ydim/odba.rb +39 -0
- data/lib/ydim/root_session.rb +143 -0
- data/lib/ydim/root_user.rb +14 -0
- data/lib/ydim/server.rb +165 -0
- data/lib/ydim/smtp_tls.rb +58 -0
- data/lib/ydim/util.rb +16 -0
- data/test/data/search.html +58 -0
- data/test/stub/odba.rb +21 -0
- data/test/suite.rb +10 -0
- data/test/test_autoinvoicer.rb +51 -0
- data/test/test_currency_converter.rb +37 -0
- data/test/test_currency_updater.rb +45 -0
- data/test/test_debitor.rb +96 -0
- data/test/test_factory.rb +99 -0
- data/test/test_invoice.rb +131 -0
- data/test/test_item.rb +41 -0
- data/test/test_mail.rb +94 -0
- data/test/test_root_session.rb +347 -0
- data/test/test_root_user.rb +20 -0
- data/test/test_server.rb +142 -0
- metadata +137 -0
@@ -0,0 +1,143 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# RootSession -- ydim -- 10.01.2006 -- hwyss@ywesee.com
|
3
|
+
|
4
|
+
require 'drb'
|
5
|
+
require 'encoding/character/utf-8'
|
6
|
+
require 'ydim/autoinvoicer'
|
7
|
+
require 'ydim/debitor'
|
8
|
+
require 'ydim/invoice'
|
9
|
+
require 'ydim/item'
|
10
|
+
require 'ydim/mail'
|
11
|
+
require 'odba'
|
12
|
+
|
13
|
+
module YDIM
|
14
|
+
class RootSession
|
15
|
+
attr_accessor :serv, :client
|
16
|
+
def initialize(user)
|
17
|
+
@user = user
|
18
|
+
end
|
19
|
+
def add_items(invoice_id, items, invoice_key=:invoice)
|
20
|
+
@serv.logger.debug(whoami) {
|
21
|
+
size = (items.respond_to?(:size)) ? items.size : nil
|
22
|
+
"add_items(#{invoice_id}, #{items.class}[#{size}], #{invoice_key})" }
|
23
|
+
invoice = self.send(invoice_key, invoice_id)
|
24
|
+
rate = invoice.suppress_vat ? 0 : @serv.config.vat_rate
|
25
|
+
items.each { |data|
|
26
|
+
item = Item.new({:vat_rate => rate}.update(data))
|
27
|
+
invoice.add_item(item)
|
28
|
+
}
|
29
|
+
invoice.odba_store
|
30
|
+
invoice.items
|
31
|
+
end
|
32
|
+
def autoinvoice(invoice_id)
|
33
|
+
@serv.logger.debug(whoami) { "autoinvoice #{invoice_id}" }
|
34
|
+
AutoInvoice.find_by_unique_id(invoice_id.to_s) \
|
35
|
+
or begin
|
36
|
+
msg = "invalid invoice_id: #{invoice_id}"
|
37
|
+
@serv.logger.error(whoami) { msg }
|
38
|
+
raise IndexError, msg
|
39
|
+
end
|
40
|
+
end
|
41
|
+
def collect_garbage(debitor_id=nil)
|
42
|
+
@serv.logger.info(whoami) { "collect_garbage" }
|
43
|
+
deleted = []
|
44
|
+
Invoice.odba_extent { |inv|
|
45
|
+
if([nil, inv.debitor_id].include?(debitor_id) && inv.deleted)
|
46
|
+
deleted.push(inv.info)
|
47
|
+
inv.odba_delete
|
48
|
+
end
|
49
|
+
}
|
50
|
+
deleted unless(deleted.empty?)
|
51
|
+
end
|
52
|
+
def create_autoinvoice(debitor_id)
|
53
|
+
@serv.logger.debug(whoami) { "create_autoinvoice(#{debitor_id})" }
|
54
|
+
ODBA.transaction {
|
55
|
+
@serv.factory.create_autoinvoice(debitor(debitor_id))
|
56
|
+
}
|
57
|
+
end
|
58
|
+
def create_debitor
|
59
|
+
@serv.logger.info(whoami) { "create_debitor" }
|
60
|
+
ODBA.transaction {
|
61
|
+
id = @serv.id_server.next_id(:debitor)
|
62
|
+
Debitor.new(id).odba_store
|
63
|
+
}
|
64
|
+
end
|
65
|
+
def create_invoice(debitor_id)
|
66
|
+
@serv.logger.debug(whoami) { "create_invoice(#{debitor_id})" }
|
67
|
+
ODBA.transaction {
|
68
|
+
@serv.factory.create_invoice(debitor(debitor_id))
|
69
|
+
}
|
70
|
+
end
|
71
|
+
def currency_converter
|
72
|
+
@serv.logger.debug(whoami) { "currency_converter" }
|
73
|
+
@serv.currency_converter.drb_dup
|
74
|
+
end
|
75
|
+
def debitor(debitor_id)
|
76
|
+
@serv.logger.debug(whoami) { "debitor #{debitor_id}" }
|
77
|
+
Debitor.find_by_unique_id(debitor_id.to_s)\
|
78
|
+
or begin
|
79
|
+
msg = "invalid debitor_id: #{debitor_id}"
|
80
|
+
@serv.logger.error(whoami) { msg }
|
81
|
+
raise IndexError, msg
|
82
|
+
end
|
83
|
+
end
|
84
|
+
def debitors
|
85
|
+
@serv.logger.debug(whoami) { "debitors" }
|
86
|
+
Debitor.odba_extent
|
87
|
+
end
|
88
|
+
def delete_autoinvoice(invoice_id)
|
89
|
+
@serv.logger.debug(whoami) {
|
90
|
+
"delete_autoinvoice(#{invoice_id})" }
|
91
|
+
if(invoice = autoinvoice(invoice_id))
|
92
|
+
invoice.odba_delete
|
93
|
+
end
|
94
|
+
end
|
95
|
+
def delete_item(invoice_id, index, invoice_key=:invoice)
|
96
|
+
@serv.logger.debug(whoami) {
|
97
|
+
"delete_item(#{invoice_id}, #{index}, #{invoice_key})" }
|
98
|
+
invoice = self.send(invoice_key, invoice_id)
|
99
|
+
invoice.items.delete_if { |item| item.index == index }
|
100
|
+
invoice.odba_store
|
101
|
+
invoice.items
|
102
|
+
end
|
103
|
+
def generate_invoice(invoice_id)
|
104
|
+
@serv.logger.info(whoami) { "generate_invoice(#{invoice_id})" }
|
105
|
+
invoice = autoinvoice(invoice_id)
|
106
|
+
AutoInvoicer.new(@serv).generate(invoice)
|
107
|
+
end
|
108
|
+
def invoice(invoice_id)
|
109
|
+
@serv.logger.debug(whoami) { "invoice #{invoice_id}" }
|
110
|
+
Invoice.find_by_unique_id(invoice_id.to_s) \
|
111
|
+
or begin
|
112
|
+
msg = "invalid invoice_id: #{invoice_id}"
|
113
|
+
@serv.logger.error(whoami) { msg }
|
114
|
+
raise IndexError, msg
|
115
|
+
end
|
116
|
+
end
|
117
|
+
def invoice_infos(status=nil)
|
118
|
+
@serv.logger.debug(whoami) { "invoice_infos(#{status})" }
|
119
|
+
Invoice.search_by_status(status).collect { |inv| inv.info }
|
120
|
+
end
|
121
|
+
def search_debitors(email_or_name)
|
122
|
+
@serv.logger.debug(whoami) { "search_debitors(#{email_or_name})" }
|
123
|
+
Debitor.search_by_exact_email(email_or_name) |
|
124
|
+
Debitor.search_by_exact_name(email_or_name)
|
125
|
+
end
|
126
|
+
def send_invoice(invoice_id, sort_args={})
|
127
|
+
@serv.logger.info(whoami) { "send_invoice(#{invoice_id})" }
|
128
|
+
Mail.send_invoice(@serv.config, invoice(invoice_id), sort_args)
|
129
|
+
end
|
130
|
+
def update_item(invoice_id, index, data, invoice_key=:invoice)
|
131
|
+
@serv.logger.debug(whoami) {
|
132
|
+
"update_item(#{invoice_id}, #{index}, #{data.inspect})" }
|
133
|
+
invoice = self.send(invoice_key, invoice_id)
|
134
|
+
item = invoice.item(index)
|
135
|
+
item.update(data)
|
136
|
+
invoice.odba_store
|
137
|
+
item
|
138
|
+
end
|
139
|
+
def whoami
|
140
|
+
@user.unique_id.to_s
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# User -- ydim -- 10.01.2006 -- hwyss@ywesee.com
|
3
|
+
|
4
|
+
require 'rrba/user'
|
5
|
+
require 'odba/drbwrapper'
|
6
|
+
require 'ydim/root_session'
|
7
|
+
|
8
|
+
module YDIM
|
9
|
+
class RootUser < RRBA::User
|
10
|
+
def new_session
|
11
|
+
ODBA::DRbWrapper.new(RootSession.new(self))
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
data/lib/ydim/server.rb
ADDED
@@ -0,0 +1,165 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# Server -- ydim -- 10.01.2006 -- hwyss@ywesee.com
|
3
|
+
|
4
|
+
require 'logger'
|
5
|
+
require 'needle'
|
6
|
+
require 'odba/id_server'
|
7
|
+
require 'rrba/server'
|
8
|
+
require 'ydim/autoinvoicer'
|
9
|
+
require 'ydim/client'
|
10
|
+
require 'ydim/currency_converter'
|
11
|
+
require 'ydim/currency_updater'
|
12
|
+
require 'ydim/factory'
|
13
|
+
require 'ydim/root_user'
|
14
|
+
require 'ydim/util'
|
15
|
+
|
16
|
+
module YDIM
|
17
|
+
class Server
|
18
|
+
ydim_default_dir = File.join(ENV['HOME'], '.ydim')
|
19
|
+
default_config_files = [
|
20
|
+
File.join(ydim_default_dir, 'ydimd.yml'),
|
21
|
+
'/etc/ydim/ydimd.yml',
|
22
|
+
]
|
23
|
+
defaults = {
|
24
|
+
'autoinvoice_hour' => 1,
|
25
|
+
'config' => default_config_files,
|
26
|
+
'conf_dir' => File.join(ydim_default_dir, 'conf'),
|
27
|
+
'currencies' => ['CHF', 'EUR', 'USD'],
|
28
|
+
'currency_update_hour' => 2,
|
29
|
+
'data_dir' => File.join(ydim_default_dir, 'data'),
|
30
|
+
'server_url' => 'druby://localhost:12375',
|
31
|
+
'db_driver_url' => 'DBI:Pg:ydim',
|
32
|
+
'db_user' => 'ydim',
|
33
|
+
'db_auth' => '',
|
34
|
+
'detach' => false,
|
35
|
+
'home_country' => 'Schweiz',
|
36
|
+
'invoice_number_start' => 10000,
|
37
|
+
'log_level' => 'INFO',
|
38
|
+
'log_file' => STDOUT,
|
39
|
+
'mail_body' => "%s %s\n%s",
|
40
|
+
'mail_charset' => 'iso-8859-1',
|
41
|
+
'mail_from' => '',
|
42
|
+
'mail_recipients' => [],
|
43
|
+
'root_name' => 'Root',
|
44
|
+
'root_email' => '',
|
45
|
+
'root_key' => 'root_dsa',
|
46
|
+
'salutation' => {
|
47
|
+
'' => 'Sehr geehrter Herr',
|
48
|
+
'Herr' => 'Sehr geehrter Herr',
|
49
|
+
'Frau' => 'Sehr geehrte Frau',
|
50
|
+
},
|
51
|
+
'smtp_from' => '',
|
52
|
+
'smtp_authtype' => :plain,
|
53
|
+
'smtp_domain' => 'ywesee.com',
|
54
|
+
'smtp_pass' => nil,
|
55
|
+
'smtp_port' => 587,
|
56
|
+
'smtp_server' => 'localhost',
|
57
|
+
'smtp_user' => 'ydim@ywesee.com',
|
58
|
+
'vat_rate' => 7.6,
|
59
|
+
}
|
60
|
+
config = RCLConf::RCLConf.new(ARGV, defaults)
|
61
|
+
config.load(config.config)
|
62
|
+
CONFIG = config
|
63
|
+
SECONDS_IN_DAY = 24*60*60
|
64
|
+
def Server.config
|
65
|
+
CONFIG
|
66
|
+
end
|
67
|
+
def initialize(config, logger)
|
68
|
+
@serv = Needle::Registry.new
|
69
|
+
@serv.register(:auth_server) {
|
70
|
+
auth = RRBA::Server.new
|
71
|
+
root = RootUser.new(:root)
|
72
|
+
root.name = config.root_name
|
73
|
+
root.email = config.root_email
|
74
|
+
root_key = config.root_key
|
75
|
+
path = File.expand_path(root_key, config.conf_dir)
|
76
|
+
path_or_key = File.exist?(path) ? path : root_key
|
77
|
+
root.public_key = Util.load_key(path_or_key)
|
78
|
+
auth.root = root
|
79
|
+
auth
|
80
|
+
}
|
81
|
+
@serv.register(:clients) {
|
82
|
+
ClientHandler.new(@serv)
|
83
|
+
}
|
84
|
+
@serv.register(:config) {
|
85
|
+
config
|
86
|
+
}
|
87
|
+
@serv.register(:currency_converter) {
|
88
|
+
ODBA.cache.fetch_named('currency_converter', self) {
|
89
|
+
CurrencyConverter.new
|
90
|
+
}
|
91
|
+
}
|
92
|
+
@serv.register(:factory) {
|
93
|
+
Factory.new(@serv)
|
94
|
+
}
|
95
|
+
@serv.register(:id_server) {
|
96
|
+
ODBA.cache.fetch_named('id_server', self) {
|
97
|
+
ODBA::IdServer.new
|
98
|
+
}
|
99
|
+
}
|
100
|
+
@serv.register(:logger) {
|
101
|
+
logger
|
102
|
+
}
|
103
|
+
if(hour = config.autoinvoice_hour)
|
104
|
+
@autoinvoicer = repeat_at(hour, 'AutoInvoicer') {
|
105
|
+
AutoInvoicer.new(@serv).run
|
106
|
+
}
|
107
|
+
end
|
108
|
+
if(hour = config.currency_update_hour)
|
109
|
+
if(@serv.currency_converter.known_currencies \
|
110
|
+
< @serv.config.currencies.size)
|
111
|
+
CurrencyUpdater.new(@serv).run
|
112
|
+
end
|
113
|
+
@currency_updater = repeat_at(hour, 'CurrencyUpdater') {
|
114
|
+
CurrencyUpdater.new(@serv).run
|
115
|
+
}
|
116
|
+
end
|
117
|
+
@status_updater = repeat_at(1, 'StatusUpdater') {
|
118
|
+
Invoice.all { |inv| inv.save }
|
119
|
+
}
|
120
|
+
@sessions = []
|
121
|
+
end
|
122
|
+
def login(client, name=nil, &block)
|
123
|
+
@serv.logger.debug(client.__drburi) { 'attempting login' }
|
124
|
+
session = @serv.auth_server.authenticate(name, &block)
|
125
|
+
session.serv = @serv
|
126
|
+
session.client = client
|
127
|
+
@serv.logger.info(session.whoami) { 'login' }
|
128
|
+
@sessions.push(session)
|
129
|
+
session
|
130
|
+
rescue Exception => error
|
131
|
+
@serv.logger.error('unknown user') {
|
132
|
+
[error.class, error.message].join(' - ') }
|
133
|
+
raise
|
134
|
+
end
|
135
|
+
def logout(session)
|
136
|
+
@serv.logger.info(session.whoami) { 'logout' }
|
137
|
+
@sessions.delete(session)
|
138
|
+
nil
|
139
|
+
end
|
140
|
+
def ping
|
141
|
+
true
|
142
|
+
end
|
143
|
+
private
|
144
|
+
def repeat_at(hour, thread_name)
|
145
|
+
Thread.new {
|
146
|
+
Thread.current.abort_on_exception = true
|
147
|
+
loop {
|
148
|
+
now = Time.now
|
149
|
+
next_run = Time.local(now.year, now.month, now.day, hour)
|
150
|
+
sleepy_time = next_run - now
|
151
|
+
if(sleepy_time < 0)
|
152
|
+
sleepy_time += SECONDS_IN_DAY
|
153
|
+
next_run += SECONDS_IN_DAY
|
154
|
+
end
|
155
|
+
@serv.logger.info(thread_name) {
|
156
|
+
sprintf("next run %s, sleeping %i seconds",
|
157
|
+
next_run.strftime("%c"), sleepy_time)
|
158
|
+
}
|
159
|
+
sleep(sleepy_time)
|
160
|
+
yield
|
161
|
+
}
|
162
|
+
}
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require "openssl"
|
2
|
+
require "net/smtp"
|
3
|
+
|
4
|
+
Net::SMTP.class_eval do
|
5
|
+
private
|
6
|
+
def do_start(helodomain, user, secret, authtype)
|
7
|
+
raise IOError, 'SMTP session already started' if @started
|
8
|
+
check_auth_args user, secret, authtype if user or secret
|
9
|
+
|
10
|
+
sock = timeout(@open_timeout) { TCPSocket.open(@address, @port) }
|
11
|
+
@socket = Net::InternetMessageIO.new(sock)
|
12
|
+
@socket.read_timeout = 60 #@read_timeout
|
13
|
+
@socket.debug_output = STDERR #@debug_output
|
14
|
+
|
15
|
+
check_response(critical { recv_response() })
|
16
|
+
do_helo(helodomain)
|
17
|
+
|
18
|
+
raise 'openssl library not installed' unless defined?(OpenSSL)
|
19
|
+
starttls
|
20
|
+
ssl = OpenSSL::SSL::SSLSocket.new(sock)
|
21
|
+
ssl.sync_close = true
|
22
|
+
ssl.connect
|
23
|
+
@socket = Net::InternetMessageIO.new(ssl)
|
24
|
+
@socket.read_timeout = 60 #@read_timeout
|
25
|
+
@socket.debug_output = STDERR #@debug_output
|
26
|
+
do_helo(helodomain)
|
27
|
+
|
28
|
+
authenticate user, secret, authtype if user
|
29
|
+
@started = true
|
30
|
+
ensure
|
31
|
+
unless @started
|
32
|
+
# authentication failed, cancel connection.
|
33
|
+
@socket.close if not @started and @socket and not @socket.closed?
|
34
|
+
@socket = nil
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def do_helo(helodomain)
|
39
|
+
begin
|
40
|
+
if @esmtp
|
41
|
+
ehlo helodomain
|
42
|
+
else
|
43
|
+
helo helodomain
|
44
|
+
end
|
45
|
+
rescue Net::ProtocolError
|
46
|
+
if @esmtp
|
47
|
+
@esmtp = false
|
48
|
+
@error_occured = false
|
49
|
+
retry
|
50
|
+
end
|
51
|
+
raise
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def starttls
|
56
|
+
getok('STARTTLS')
|
57
|
+
end
|
58
|
+
end
|
data/lib/ydim/util.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# Util -- ydim -- 10.01.2006 -- hwyss@ywesee.com
|
3
|
+
|
4
|
+
require 'openssl'
|
5
|
+
require 'fileutils'
|
6
|
+
|
7
|
+
module YDIM
|
8
|
+
module Util
|
9
|
+
def Util.load_key(key)
|
10
|
+
if(File.exist?(key))
|
11
|
+
key = File.read(key)
|
12
|
+
end
|
13
|
+
key = OpenSSL::PKey::DSA.new(key)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
<html><head><meta HTTP-EQUIV="content-type" CONTENT="text/html; charset=UTF-8"><title>1 USD in CHF - Google Search</title><style><!--
|
2
|
+
body,td,div,.p,a{font-family:arial,sans-serif }
|
3
|
+
div,td{color:#000}
|
4
|
+
.f{color:#6f6f6f}
|
5
|
+
.fl:link{color:#77c}
|
6
|
+
a:link,.w,a.w:link,.w a:link{color:#00c}
|
7
|
+
a:visited,.fl:visited{color:#551a8b}
|
8
|
+
a:active,.fl:active{color:#f00}
|
9
|
+
.t a:link,.t a:active,.t a:visited,.t{color:#000}
|
10
|
+
.t{background-color:#e5ecf9}
|
11
|
+
.k{background-color:#36c}
|
12
|
+
.j{width:34em}
|
13
|
+
.h{color:#36c;font-size:14px}
|
14
|
+
.i,.i:link{color:#a90a08}
|
15
|
+
.a,.a:link{color:#008000}
|
16
|
+
.z{display:none}
|
17
|
+
div.n{margin-top:1ex}
|
18
|
+
.n a{font-size:10pt;color:#000}
|
19
|
+
.n .i{font-size:10pt;font-weight:bold}
|
20
|
+
.q:visited,.q:link,.q:active,.q{color:#00c;}
|
21
|
+
.b{font-size:12pt;color:#00c;font-weight:bold}
|
22
|
+
.ch{cursor:pointer;cursor:hand}
|
23
|
+
.sem{display:inline;margin:0;font-size:100%;font-weight:inherit}
|
24
|
+
.e{margin-top:.75em;margin-bottom:.75em}
|
25
|
+
.g{margin-top:1em;margin-bottom:1em}
|
26
|
+
.m{color:#666666;font-size:83%}
|
27
|
+
.ml:link{color:#551a8b;}
|
28
|
+
.sm{display:block;margin-top:0px;margin-bottom:0px;margin-left:40px}
|
29
|
+
.bl{display:none}
|
30
|
+
.fl2,.fl2:link,.fl2:visited{color:#7777CC}
|
31
|
+
.fl2:active{color:#f00}
|
32
|
+
.gh{background-color:#FFFF88;}
|
33
|
+
-->
|
34
|
+
</style>
|
35
|
+
<script>
|
36
|
+
<!--
|
37
|
+
if(document.images){new Image().src="/url?sa=Q&q=1+USD+in+CHF&ct=q&ei=I8_gQ4bMF9DqiwHQ9JHlCQ&sig2=UD3Tzz5it4Ox-8P_bqGNQQ";}
|
38
|
+
function ss(w,id){window.status=w;return true;}
|
39
|
+
function cs(){window.status='';}
|
40
|
+
function rwt(el,ct,cd,sg){el.href="/url?sa=t&ct="+escape(ct)+"&cd="+escape(cd)+"&url="+escape(el.href).replace(/\+/g,"%2B")+"&ei=I8_gQ4bMF9DqiwHQ9JHlCQ"+sg;el.onmousedown="";return true;}
|
41
|
+
var q="1+USD+in+CHF";var eid="I8_gQ4bMF9DqiwHQ9JHlCQ";document.write("<style type=text/css>.bl{ display:inline !important;}</style>");function _inv(){if(window.location.hash=="#invalid"){var a=window.location.href;window.location.replace(a.substring(0,a.indexOf("#")));if(window.location.hash=="#invalid"){window.location.hash=""}if(window.location.hash==""||window.location.hash=="#"){window.location.reload(true)}}}_inv();function ga(o,e) {if (document.getElementById) {var a = o.id.substring(1); var p = "", r = "", t, f, h;var g = e.target;if (g) { t = g.id;f = g.parentNode;if (f) {p = f.id;h = f.parentNode;if (h)r = h.id;}} else {h = e.srcElement;f = h.parentNode;if (f)p = f.id;t = h.id;}if (t==a || p==a || r==a)return true;document.getElementById(a).href += "&ct=bg";top.location.href=document.getElementById(a).href}}
|
42
|
+
//-->
|
43
|
+
</script>
|
44
|
+
</head><body bgcolor=#ffffff onload="document.gs.reset();" topmargin=3 marginheight=3><table border=0 cellspacing=0 cellpadding=0 width=100%><tr><td align=right nowrap><font size=-1><b>hannes.wyss@gmail.com</b> | <a href="/searchhistory/?hl=en">Search History</a> | <a href="https://www.google.com/accounts/ManageAccount">My Account</a> | <a href="http://www.google.com/accounts/Logout?continue=http://www.google.com/search%3Fq%3D1%2BUSD%2Bin%2BCHF">Sign out</a></font></td></tr><tr height=4><td><img alt="" width=1 height=1></td></tr></table><table border=0 cellpadding=0 cellspacing=0 width=100%><tr><form name=gs method=GET action=/search><td valign=top><a href="http://www.google.com/webhp?hl=en"><img src="/images/logo_sm.gif" width=150 height=55 alt="Go to Google Home" border=0 vspace=12></a></td><td> </td><td valign=top width=100%><table cellpadding=0 cellspacing=0 border=0><tr><td height=14 valign=bottom><script><!--
|
45
|
+
function qs(el) {if (window.RegExp && window.encodeURIComponent) {var ue=el.href;var qe=encodeURIComponent(document.gs.q.value);if(ue.indexOf("q=")!=-1){el.href=ue.replace(new RegExp("q=[^&$]*"),"q="+qe);}else{el.href=ue+"&q="+qe;}}return 1;}
|
46
|
+
// -->
|
47
|
+
</script><table border=0 cellpadding=4 cellspacing=0><tr><td><font size=-1><b>Web</b> <a id=t1a class=q href="http://images.google.com/images?q=1+USD+in+CHF&sa=N&tab=wi" onClick="return qs(this);">Images</a> <a id=t2a class=q href="http://groups.google.com/groups?q=1+USD+in+CHF&sa=N&tab=wg" onClick="return qs(this);">Groups</a> <a id=t4a class=q href="http://news.google.com/news?q=1+USD+in+CHF&sa=N&tab=wn" onClick="return qs(this);">News</a> <a id=t5a class=q href="http://froogle.google.com/froogle?q=1+USD+in+CHF&sa=N&tab=wf" onClick="return qs(this);">Froogle</a> <a id=t7a class=q href="http://local.google.com/local?q=1+USD+in+CHF&sa=N&tab=wl" onClick="return qs(this);">Local</a> <b><a href="/intl/en/options/" class=q>more »</a></b></font></td></tr></table></td></tr><tr><td><table border=0 cellpadding=0 cellspacing=0><tr><td nowrap><input type=hidden name=hl value="en"><input type=hidden name=lr value=""><input type=hidden name=c2coff value=1><input type=text name=q size=41 maxlength=2048 value="1 USD in CHF" title="Search"><font size=-1> <input type=submit name="btnG" value="Search"><span id=hf></span></font></td><td nowrap><font size=-2> <a href=/advanced_search?q=1+USD+in+CHF&hl=en&lr=&c2coff=1>Advanced Search</a><br> <a href=/preferences?q=1+USD+in+CHF&hl=en&lr=&c2coff=1>Preferences</a> </font></td></tr></table></td></tr></table><table cellpadding=0 cellspacing=0 border=0><tr><td><font size=-1></font></td></tr><tr><td height=7><img width=1 height=1 alt=""></td></tr></table></td></form></tr></table><table width=100% border=0 cellpadding=0 cellspacing=0><tr><td bgcolor=#3366cc><img width=1 height=1 alt=""></td></tr></table><table width=100% border=0 cellpadding=0 cellspacing=0 bgcolor=#e5ecf9><tr><td bgcolor=#e5ecf9 width=1% nowrap><font size=+1> <b>Web</b></font> </td></tr></table>
|
48
|
+
<script><!--
|
49
|
+
var tt = document.gs.q;
|
50
|
+
tt.focus();
|
51
|
+
var r = tt.createTextRange;
|
52
|
+
if (r) {
|
53
|
+
var s = r();
|
54
|
+
s.collapse(false);
|
55
|
+
s.select(); }
|
56
|
+
// -->
|
57
|
+
</script>
|
58
|
+
<p><table><tr><td><img src=/images/calc_img.gif></td><td> </td><td nowrap><font size=+1><b>1 U.S. dollar = 1.2864003 Swiss francs</b></td></tr><tr><td> </td><td> </td><td><font size=-1><nobr>Rates provided for information only - <a href="/help/currency_disclaimer.html">see disclaimer</a>.</nobr> <a href=/help/features.html#currency>More about currency conversion.</a></td></tr></table><br><table><tr><td><font size=+0>Search for documents containing the terms <a href=/search?hl=en&lr=&c2coff=1&q=%2B1+USD+in+CHF&nocalc=1><b><i>1 USD in CHF</i></b></a>.</td></tr></table><br><br><br clear=all><center><p><hr class=z><table width=100% border=0 cellpadding=0 cellspacing=0><tr><td bgcolor=#3366cc colspan=2><img width=1 height=1 alt=""></td></tr></table><table width=100% cellpadding=2 cellspacing=0 border=0><tr><td align=center class=t><font size=-1><a href="http://www.google.com/">Google Home</a> - <a href="/ads/">Advertising Programs</a> - <a href="/services/">Business Solutions</a> - <a href=/about.html>About Google</a></font></table><br><font size=-1 class=p>©2006 Google</font></center></body></html>
|
data/test/stub/odba.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# ODBA Stub -- ydim -- 24.01.2005 -- hwyss@ywesee.com
|
3
|
+
|
4
|
+
require 'odba/odba'
|
5
|
+
|
6
|
+
module ODBA
|
7
|
+
def ODBA.transaction(&block)
|
8
|
+
block.call
|
9
|
+
end
|
10
|
+
module Persistable
|
11
|
+
attr_reader :odba_stored
|
12
|
+
def odba_store
|
13
|
+
@odba_stored = @odba_stored.to_i.next
|
14
|
+
end
|
15
|
+
end
|
16
|
+
class Cache
|
17
|
+
def fetch_named(*args, &block)
|
18
|
+
block.call
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
data/test/suite.rb
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# TestAutoInvoicer -- ydim -- 01.02.2007 -- hwyss@ywesee.com
|
3
|
+
|
4
|
+
$: << File.expand_path('../lib', File.dirname(__FILE__))
|
5
|
+
|
6
|
+
require 'test/unit'
|
7
|
+
require 'flexmock'
|
8
|
+
require 'ydim/autoinvoicer'
|
9
|
+
require 'ydim/invoice'
|
10
|
+
|
11
|
+
module YDIM
|
12
|
+
class TestAutoInvoicer < Test::Unit::TestCase
|
13
|
+
include FlexMock::TestCase
|
14
|
+
def setup
|
15
|
+
@serv = flexmock('Registry')
|
16
|
+
@autoinvoicer = AutoInvoicer.new(@serv)
|
17
|
+
end
|
18
|
+
def test_run
|
19
|
+
deb1 = flexmock('Debitor1')
|
20
|
+
deb1.should_receive(:autoinvoices).and_return([])
|
21
|
+
deb2 = flexmock('Debitor2')
|
22
|
+
inv1 = flexmock('AutoInvoice1')
|
23
|
+
inv1.should_receive(:total_netto).and_return(1)
|
24
|
+
inv1.should_receive(:date).and_return(Date.today)
|
25
|
+
inv2 = flexmock('AutoInvoice2')
|
26
|
+
inv2.should_receive(:total_netto).and_return(1)
|
27
|
+
inv2.should_receive(:date).and_return(Date.today >> 1)
|
28
|
+
inv3 = flexmock('AutoInvoice3')
|
29
|
+
inv3.should_receive(:total_netto).and_return(0)
|
30
|
+
deb2.should_receive(:autoinvoices).and_return([inv1, inv2, inv3])
|
31
|
+
debitors = { 0 => deb1, 1 => deb2 }
|
32
|
+
flexstub(Debitor).should_receive(:odba_extent).and_return { |blk|
|
33
|
+
debitors.each_value(&blk)
|
34
|
+
}
|
35
|
+
@serv.should_receive(:config).and_return('configuration')
|
36
|
+
factory = flexmock('Factory')
|
37
|
+
factory.should_receive(:generate_invoice).with(inv1)\
|
38
|
+
.times(1).and_return(:generated_invoice)
|
39
|
+
@serv.should_receive(:factory).and_return(factory)
|
40
|
+
ODBA.cache = cache = flexmock('ODBA')
|
41
|
+
cache.should_receive(:transaction).and_return { |bl| bl.call }
|
42
|
+
mail = flexstub(Mail)
|
43
|
+
mail.should_receive(:send_invoice)\
|
44
|
+
.with('configuration', :generated_invoice)\
|
45
|
+
.and_return { assert(true) }
|
46
|
+
mail.should_receive(:send_reminder).with('configuration', inv2)\
|
47
|
+
.and_return { assert(true) }
|
48
|
+
@autoinvoicer.run
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# TestCurrencyConverter -- ydim -- 01.02.2006 -- hwyss@ywesee.com
|
3
|
+
|
4
|
+
$: << File.expand_path('../lib', File.dirname(__FILE__))
|
5
|
+
|
6
|
+
require 'test/unit'
|
7
|
+
require 'ydim/currency_converter'
|
8
|
+
|
9
|
+
module YDIM
|
10
|
+
class TestCurrencyConverter < Test::Unit::TestCase
|
11
|
+
def setup
|
12
|
+
@converter = CurrencyConverter.new
|
13
|
+
end
|
14
|
+
def test_known_currencies
|
15
|
+
assert_equal(0, @converter.known_currencies)
|
16
|
+
@converter.store('EUR', 'CHF', 1.55474)
|
17
|
+
assert_equal(2, @converter.known_currencies)
|
18
|
+
@converter.store('USD', 'CHF', 1.28640)
|
19
|
+
assert_equal(3, @converter.known_currencies)
|
20
|
+
end
|
21
|
+
def test_convert
|
22
|
+
@converter.store('EUR', 'CHF', 1.55474)
|
23
|
+
assert_equal(1.55474, @converter.convert(1, 'EUR', 'CHF'))
|
24
|
+
assert_equal(1, @converter.convert(1, 'CHF', 'CHF'))
|
25
|
+
assert_equal(1/1.55474, @converter.convert(1, 'CHF', 'EUR'))
|
26
|
+
assert_raises(RuntimeError) {
|
27
|
+
@converter.convert(1, 'RND', 'CHF')
|
28
|
+
}
|
29
|
+
end
|
30
|
+
def test_drb_dup
|
31
|
+
@converter.store('EUR', 'CHF', 1.55474)
|
32
|
+
dup = @converter.drb_dup
|
33
|
+
assert_instance_of(MobileCurrencyConverter, dup)
|
34
|
+
assert_equal(1.55474, dup.convert(1, 'EUR', 'CHF'))
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# TestCurrencyUpdater -- ydim -- 01.02.2006 -- hwyss@ywesee.com
|
3
|
+
|
4
|
+
|
5
|
+
$: << File.expand_path('../lib', File.dirname(__FILE__))
|
6
|
+
|
7
|
+
require 'test/unit'
|
8
|
+
require 'ydim/currency_updater'
|
9
|
+
require 'flexmock'
|
10
|
+
|
11
|
+
module YDIM
|
12
|
+
class TestCurrencyUpdater < Test::Unit::TestCase
|
13
|
+
include FlexMock::TestCase
|
14
|
+
def setup
|
15
|
+
@serv = flexmock('Config')
|
16
|
+
@updater = CurrencyUpdater.new(@serv)
|
17
|
+
end
|
18
|
+
def test_extract_conversion
|
19
|
+
html = File.read(File.expand_path('data/search.html',
|
20
|
+
File.dirname(__FILE__)))
|
21
|
+
assert_equal('1.2864003', @updater.extract_conversion(html))
|
22
|
+
end
|
23
|
+
def test_run
|
24
|
+
resp = flexmock('HttpResponse')
|
25
|
+
resp.should_receive(:body)\
|
26
|
+
.and_return(File.read(File.expand_path('data/search.html',
|
27
|
+
File.dirname(__FILE__))))
|
28
|
+
session = flexmock('HttpSession')
|
29
|
+
session.should_receive(:get).with('/search?q=1+CHF+in+EUR')\
|
30
|
+
.and_return(resp)
|
31
|
+
flexstub(Net::HTTP).should_receive(:start)\
|
32
|
+
.times(1).and_return { |host, block|
|
33
|
+
block.call(session)
|
34
|
+
}
|
35
|
+
conv = flexmock('Converter')
|
36
|
+
conv.should_receive(:odba_store).times(1)
|
37
|
+
conv.should_receive(:store).times(1).times(1)
|
38
|
+
config = flexmock('Config')
|
39
|
+
config.should_receive(:currencies).and_return(['CHF', 'EUR'])
|
40
|
+
@serv.should_receive(:currency_converter).and_return(conv)
|
41
|
+
@serv.should_receive(:config).and_return(config)
|
42
|
+
@updater.run
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|