ydim 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env ruby
2
+ # AutoInvoicer -- ydim -- 13.01.2006 -- hwyss@ywesee.com
3
+
4
+ require 'date'
5
+ require 'pdfinvoice/config'
6
+ require 'pdfinvoice/invoice'
7
+ require 'odba/odba'
8
+ require 'ydim/debitor'
9
+ require 'ydim/mail'
10
+
11
+ module YDIM
12
+ class AutoInvoicer
13
+ def initialize(serv)
14
+ @serv = serv
15
+ end
16
+ def run
17
+ Debitor.odba_extent { |debitor|
18
+ autoinvoice(debitor)
19
+ }
20
+ end
21
+ def autoinvoice(debitor)
22
+ today = Date.today
23
+ next_month = today >> 1
24
+ debitor.autoinvoices.each { |auto|
25
+ if(auto.total_netto > 0)
26
+ case auto.date
27
+ when today
28
+ Mail.send_invoice(@serv.config, generate(auto))
29
+ when next_month
30
+ Mail.send_reminder(@serv.config, auto)
31
+ end
32
+ end
33
+ }
34
+ end
35
+ def generate(autoinvoice)
36
+ ODBA.transaction {
37
+ @serv.factory.generate_invoice(autoinvoice)
38
+ }
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # Client -- ydim -- 12.01.2006 -- hwyss@ywesee.com
3
+
4
+ require 'drb'
5
+
6
+ module YDIM
7
+ class Client
8
+ include DRb::DRbUndumped
9
+ attr_reader :session
10
+ def initialize(config)
11
+ @config = config
12
+ DRb.start_service(config.client_url)
13
+ end
14
+ def login(server, private_key)
15
+ @server = server
16
+ @session = @server.login(self, @config.user) { |challenge|
17
+ if(private_key.respond_to?(:syssign))
18
+ private_key.syssign(challenge)
19
+ end
20
+ }
21
+ end
22
+ def logout
23
+ @server.logout(@session) if(@server)
24
+ end
25
+ def method_missing(meth, *args, &block)
26
+ @session.send(meth, *args, &block)
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env ruby
2
+ # CONFIG -- ydim -- 12.01.2006 -- hwyss@ywesee.com
3
+
4
+ require 'rclconf'
5
+ require 'fileutils'
6
+
7
+ module YDIM
8
+ VERSION = '1.0.0'
9
+ class Client
10
+ home_dir = ENV['HOME'] || '/tmp'
11
+ ydim_default_dir = File.join(home_dir, '.ydim')
12
+ default_config_files = [
13
+ File.join(ydim_default_dir, 'ydim.yml'),
14
+ '/etc/ydim/ydim.yml',
15
+ ]
16
+ defaults = {
17
+ 'client_url' => 'druby://localhost:0',
18
+ 'config' => default_config_files,
19
+ 'private_key' => File.join(home_dir, '.ssh', 'id_dsa'),
20
+ 'user' => nil,
21
+ 'server_url' => 'druby://localhost:12375',
22
+ 'currency' => 'EUR',
23
+ 'invoice_description' => 'Auto-Invoice',
24
+ 'mail_charset' => 'iso-8859-1',
25
+ 'payment_period' => 30,
26
+ }
27
+ config = RCLConf::RCLConf.new(ARGV, defaults)
28
+ config.load(config.config)
29
+
30
+ CONFIG = config
31
+ end
32
+ end
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env ruby
2
+ # CurrencyConverter -- ydim -- 01.02.2006 -- hwyss@ywesee.com
3
+
4
+ module YDIM
5
+ class MobileCurrencyConverter
6
+ def initialize(conversions={})
7
+ @conversions = conversions
8
+ end
9
+ def convert(amount, origin, target)
10
+ return amount if(origin == target)
11
+ amount.to_f * @conversions.fetch([origin, target]) {
12
+ raise "Unknown Conversion '#{origin}' -> '#{target}'"
13
+ }
14
+ end
15
+ end
16
+ class CurrencyConverter < MobileCurrencyConverter
17
+ def drb_dup
18
+ MobileCurrencyConverter.new(@conversions)
19
+ end
20
+ def known_currencies
21
+ @conversions.keys.collect { |origin, target| origin }.uniq.size
22
+ end
23
+ def store(origin, target, rate)
24
+ @conversions.store([target, origin], 1.0/rate)
25
+ @conversions.store([origin, target], rate)
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env ruby
2
+ # CurrencyUpdater -- ydim -- 01.02.2006 -- hwyss@ywesee.com
3
+
4
+ require 'net/http'
5
+
6
+ module YDIM
7
+ class CurrencyUpdater
8
+ def initialize(serv)
9
+ @serv = serv
10
+ end
11
+ def run
12
+ curr = @serv.config.currencies.dup
13
+ while(origin = curr.shift)
14
+ curr.each { |target|
15
+ update_conversion(origin, target)
16
+ }
17
+ end
18
+ @serv.currency_converter.odba_store
19
+ end
20
+ def extract_conversion(html)
21
+ if(match = /1\s+[^<>=]+=\s+(\d+\.\d+)/.match(html))
22
+ match[1]
23
+ end
24
+ end
25
+ def get_conversion(origin, target)
26
+ extract_conversion(get_html(origin, target)).to_f
27
+ end
28
+ def get_html(origin, target)
29
+ ## not in test-suite, test manually when modified
30
+ Net::HTTP.start('www.google.com') { |session|
31
+ session.get("/search?q=1+#{origin.upcase}+in+#{target.upcase}").body
32
+ }
33
+ end
34
+ def update_conversion(origin, target)
35
+ @serv.currency_converter.store(origin, target,
36
+ get_conversion(origin, target))
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env ruby
2
+ # Debitor -- ydim -- 10.01.2006 -- hwyss@ywesee.com
3
+
4
+ require 'ydim/item'
5
+
6
+ module YDIM
7
+ class Debitor
8
+ attr_reader :autoinvoices, :unique_id, :invoices
9
+ attr_accessor :address_lines, :contact, :contact_firstname,
10
+ :contact_title, :country, :debitor_type, :email, :location,
11
+ :name, :salutation, :phone
12
+ def initialize(unique_id)
13
+ @unique_id = unique_id
14
+ @address_lines = []
15
+ @invoices = []
16
+ @autoinvoices = []
17
+ end
18
+ def autoinvoice(unique_id)
19
+ @autoinvoices.find { |invoice| invoice.unique_id == unique_id }
20
+ end
21
+ def autoinvoice_infos
22
+ @autoinvoices.collect { |inv| inv.info }
23
+ end
24
+ def add_autoinvoice(invoice)
25
+ @autoinvoices.push(invoice)
26
+ invoice
27
+ end
28
+ def add_invoice(invoice)
29
+ @invoices.push(invoice)
30
+ invoice
31
+ end
32
+ def address
33
+ lns = [@name]
34
+ lns.push(["z.H.", @salutation,
35
+ @contact_firstname, @contact].compact.join(' '))
36
+ lns.push(@contact_title)
37
+ lns.concat(@address_lines)
38
+ lns.push(@location, @country)
39
+ if @email
40
+ lns.push "To: #@email"
41
+ unless emails_cc.empty?
42
+ lns.push "Cc: #{emails_cc.join(', ')}"
43
+ end
44
+ end
45
+ lns.compact!
46
+ lns
47
+ end
48
+ def delete_autoinvoice(invoice)
49
+ @autoinvoices.delete(invoice)
50
+ end
51
+ def delete_invoice(invoice)
52
+ @invoices.delete(invoice)
53
+ end
54
+ def emails_cc
55
+ @emails_cc ||= []
56
+ end
57
+ def emails_cc=(emails)
58
+ @emails_cc = emails
59
+ end
60
+ def emails
61
+ [ @email ].concat(emails_cc).compact
62
+ end
63
+ def emails=(emails)
64
+ @email = emails.shift
65
+ @emails_cc = emails
66
+ emails
67
+ end
68
+ def foreign?
69
+ @country && !@country.to_s.strip.empty? \
70
+ && @country != Server.config.home_country
71
+ end
72
+ def invoice(unique_id)
73
+ @invoices.find { |invoice| invoice.unique_id == unique_id }
74
+ end
75
+ def invoice_infos(status)
76
+ @invoices.select { |inv|
77
+ inv.status == status
78
+ }.collect { |inv| inv.info }
79
+ end
80
+ def next_invoice_date
81
+ @autoinvoices.collect { |inv| inv.date }.compact.min
82
+ end
83
+ private
84
+ include ItemId
85
+ end
86
+ end
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env ruby
2
+ # Factory -- ydim -- 16.01.2006 -- hwyss@ywesee.com
3
+
4
+ require 'ydim/debitor'
5
+ require 'ydim/invoice'
6
+
7
+ module YDIM
8
+ class Factory
9
+ def initialize(serv)
10
+ @serv = serv
11
+ end
12
+ def create_autoinvoice(debitor)
13
+ id = @serv.id_server.next_id(:autoinvoice,
14
+ @serv.config.invoice_number_start)
15
+ invoice = AutoInvoice.new(id)
16
+ yield(invoice) if(block_given?)
17
+ if debitor.foreign?
18
+ invoice.suppress_vat = true
19
+ end
20
+ invoice.debitor = debitor
21
+ debitor.autoinvoices.odba_store
22
+ invoice
23
+ end
24
+ def create_invoice(debitor)
25
+ id = @serv.id_server.next_id(:invoice, @serv.config.invoice_number_start)
26
+ invoice = Invoice.new(id)
27
+ yield(invoice) if(block_given?)
28
+ invoice.debitor = debitor
29
+ if debitor.foreign?
30
+ invoice.suppress_vat = true
31
+ end
32
+ debitor.invoices.odba_store
33
+ invoice
34
+ end
35
+ def generate_invoice(autoinvoice)
36
+ create_invoice(autoinvoice.debitor) { |inv|
37
+ date = autoinvoice.date || Date.today
38
+ nextdate = autoinvoice.advance(date)
39
+ inv.date = date
40
+ inv.currency = autoinvoice.currency
41
+ inv.description = sprintf("%s %s-%s", autoinvoice.description,
42
+ date.strftime("%d.%m.%Y"),
43
+ (nextdate - 1).strftime("%d.%m.%Y"))
44
+ inv.precision = autoinvoice.precision
45
+ inv.payment_period = autoinvoice.payment_period
46
+ autoinvoice.items.each { |item|
47
+ nitem = item.dup
48
+ nitem.time = Time.now
49
+ nitem.expiry_time = Time.local(nextdate.year, nextdate.month,
50
+ nextdate.day)
51
+ nitem.vat_rate = @serv.config.vat_rate
52
+ inv.add_item(nitem)
53
+ }
54
+ autoinvoice.odba_store
55
+ }
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,146 @@
1
+ #!/usr/bin/env ruby
2
+ # Invoice -- ydim -- 11.01.2006 -- hwyss@ywesee.com
3
+
4
+ require 'pdfinvoice/config'
5
+ require 'pdfinvoice/invoice'
6
+ require 'ydim/item'
7
+ require 'ydim/server'
8
+
9
+ module YDIM
10
+ class Invoice
11
+ class Info
12
+ KEYS = [:unique_id, :date, :description, :payment_received, :currency,
13
+ :status, :debitor_name, :debitor_email, :debitor_id, :due_date,
14
+ :total_netto, :total_brutto, :deleted, :suppress_vat ]
15
+ attr_accessor *KEYS
16
+ def initialize(invoice)
17
+ KEYS.each { |key|
18
+ instance_variable_set("@#{key}", invoice.send(key))
19
+ }
20
+ end
21
+ end
22
+ attr_reader :unique_id, :debitor, :items, :suppress_vat
23
+ attr_accessor :precision, :date, :description, :payment_period,
24
+ :payment_received, :currency, :deleted
25
+ def Invoice.sum(key)
26
+ define_method(key) {
27
+ @items.inject(0.0) { |value, item| value + item.send(key) }
28
+ }
29
+ end
30
+ def initialize(unique_id)
31
+ @unique_id = unique_id
32
+ @items = []
33
+ @precision = 2
34
+ @payment_period = 10
35
+ end
36
+ def invoice_key
37
+ :invoice
38
+ end
39
+ def add_item(item)
40
+ item.index = next_item_id
41
+ @items.push(item)
42
+ item
43
+ end
44
+ def debitor=(debitor)
45
+ if(@debitor)
46
+ @debitor.send("delete_#{invoice_key}", self)
47
+ end
48
+ if(debitor)
49
+ debitor.send("add_#{invoice_key}", self)
50
+ end
51
+ @debitor = debitor
52
+ end
53
+ def debitor_email
54
+ @debitor.email if(@debitor)
55
+ end
56
+ def debitor_id
57
+ @debitor.unique_id if(@debitor)
58
+ end
59
+ def debitor_name
60
+ @debitor.name if(@debitor)
61
+ end
62
+ def due_date
63
+ if(@date && !@payment_received)
64
+ @date + @payment_period.to_i
65
+ end
66
+ end
67
+ def empty?
68
+ @items.empty?
69
+ end
70
+ def info
71
+ Info.new(self)
72
+ end
73
+ def item(index)
74
+ @items.find { |item| item.index == index }
75
+ end
76
+ def status
77
+ if(@deleted)
78
+ 'is_trash'
79
+ elsif(@payment_received)
80
+ 'is_paid'
81
+ elsif(@date && @payment_period \
82
+ && ((@date + @payment_period) < Date.today))
83
+ 'is_due'
84
+ else
85
+ 'is_open'
86
+ end
87
+ end
88
+ def pdf_invoice(sort_args={})
89
+ config = PdfInvoice.config.dup
90
+ config.formats['quantity'] = "%1.#{@precision}f"
91
+ config.formats['total'] = "#{@currency} %1.2f"
92
+ invoice = PdfInvoice::Invoice.new(config)
93
+ invoice.date = @date
94
+ invoice.invoice_number = @unique_id
95
+ invoice.description = @description
96
+ invoice.debitor_address = @debitor.address
97
+ items = @items
98
+ if sort_by = sort_args[:sortby]
99
+ begin
100
+ items = items.sort_by do |item|
101
+ item.send(sort_by)
102
+ end
103
+ if sort_args[:sort_reverse]
104
+ items.reverse!
105
+ end
106
+ rescue StandardError
107
+ end
108
+ end
109
+ invoice.items = items.collect { |item|
110
+ [ item.time, item.text, item.unit, item.quantity.to_f,
111
+ item.price.to_f, item.vat.to_f ]
112
+ }
113
+ invoice
114
+ end
115
+ def suppress_vat= bool
116
+ rate = bool ? 0 : YDIM::Server.config.vat_rate
117
+ @items.each do |item| item.vat_rate = rate end
118
+ @suppress_vat = bool
119
+ end
120
+ def to_pdf(sort_args={})
121
+ pdf_invoice(sort_args).to_pdf
122
+ end
123
+ sum :total_brutto
124
+ sum :total_netto
125
+ sum :vat
126
+ private
127
+ include ItemId
128
+ end
129
+ class AutoInvoice < Invoice
130
+ @@year_ptrn = %r{<year>([^<])*</year>}
131
+ attr_accessor :invoice_interval, :reminder_body, :reminder_subject
132
+ def invoice_key
133
+ :autoinvoice
134
+ end
135
+ def advance(date)
136
+ months = @invoice_interval.to_s[/\d+/].to_i
137
+ if @reminder_subject
138
+ @reminder_subject.gsub!(@@year_ptrn) do |match|
139
+ years = months / 12
140
+ match.gsub(%r{\d+}) do |year| (year.to_i + years).to_s end
141
+ end
142
+ end
143
+ @date = date >> months
144
+ end
145
+ end
146
+ end
data/lib/ydim/item.rb ADDED
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env ruby
2
+ # Item -- ydim -- 11.01.2006 -- hwyss@ywesee.com
3
+
4
+ module YDIM
5
+ class Item
6
+ DATA_KEYS = [ :data, :expiry_time, :item_type, :price, :quantity, :text,
7
+ :time, :unit, :vat_rate ]
8
+ attr_accessor :index, *DATA_KEYS
9
+ def initialize(data={})
10
+ update(data)
11
+ end
12
+ def total_brutto
13
+ total_netto + vat
14
+ end
15
+ def total_netto
16
+ @quantity.to_f * @price.to_f
17
+ end
18
+ def update(data)
19
+ data.each { |key, val|
20
+ if(DATA_KEYS.include?(key.to_sym))
21
+ instance_variable_set("@#{key}", val)
22
+ end
23
+ }
24
+ end
25
+ def vat
26
+ total_netto * (@vat_rate.to_f / 100.0)
27
+ end
28
+ end
29
+ module ItemId
30
+ def next_item_id
31
+ id = @next_item_id.to_i
32
+ @next_item_id = id.next
33
+ id
34
+ end
35
+ end
36
+ end
data/lib/ydim/mail.rb ADDED
@@ -0,0 +1,95 @@
1
+ #!/usr/bin/env ruby
2
+ # Mail -- ydim -- 18.01.2006 -- hwyss@ywesee.com
3
+
4
+ require 'net/smtp'
5
+ require 'rmail'
6
+ require 'ydim/smtp_tls'
7
+
8
+ module YDIM
9
+ module Mail
10
+ def Mail.body(config, debitor, invoice)
11
+ salutation = config.salutation[debitor.salutation.to_s]
12
+ sprintf(config.mail_body, salutation, debitor.contact, invoice.description)
13
+ end
14
+ def Mail.send_invoice(config, invoice, sort_args={})
15
+ debitor = invoice.debitor
16
+ to = debitor.email
17
+ subject = sprintf('Rechnung %s #%i, %s', debitor.name,
18
+ invoice.unique_id, invoice.description)
19
+ invoice_name = sprintf("%s.pdf", subject.tr(' /', '_-'))
20
+ mpart = RMail::Message.new
21
+ header = mpart.header
22
+ header.to = to
23
+ cc = header.cc = debitor.emails_cc
24
+ header.from = config.mail_from
25
+ header.subject = encode_subject config, subject
26
+ header.date = Time.now
27
+ tpart = RMail::Message.new
28
+ mpart.add_part(tpart)
29
+ tpart.header.add('Content-Type', 'text/plain', nil,
30
+ 'charset' => config.mail_charset)
31
+ tpart.body = body(config, debitor, invoice)
32
+ fpart = RMail::Message.new
33
+ mpart.add_part(fpart)
34
+ header = fpart.header
35
+ header.add('Content-Type', 'application/pdf')
36
+ header.add('Content-Disposition', 'attachment', nil,
37
+ {'filename' => invoice_name })
38
+ header.add('Content-Transfer-Encoding', 'base64')
39
+ fpart.body = [invoice.to_pdf(sort_args)].pack('m')
40
+ recipients = config.mail_recipients.dup.push(to).concat(cc).uniq
41
+ Net::SMTP.start(config.smtp_server, config.smtp_port,
42
+ config.smtp_domain, config.smtp_user, config.smtp_pass,
43
+ config.smtp_authtype) { |smtp|
44
+ recipients.each { |recipient|
45
+ smtp.sendmail(mpart.to_s, config.smtp_user, recipient)
46
+ }
47
+ }
48
+ recipients
49
+ rescue Timeout::Error
50
+ retries ||= 3
51
+ if retries > 0
52
+ sleep 3 - retries
53
+ retries -= 1
54
+ retry
55
+ else
56
+ raise
57
+ end
58
+ end
59
+ def Mail.send_reminder(config, autoinvoice)
60
+ subject = autoinvoice.reminder_subject.to_s.strip
61
+ subject.gsub! %r{<year>\s*}, ''
62
+ subject.gsub! %r{\s*</year>}, ''
63
+ body = autoinvoice.reminder_body.to_s.strip
64
+ body.gsub! %r{<invoice>\s*}, ''
65
+ body.gsub! %r{\s*</invoice>}, ''
66
+ unless(subject.empty? || body.empty?)
67
+ debitor = autoinvoice.debitor
68
+ to = debitor.email
69
+ mpart = RMail::Message.new
70
+ header = mpart.header
71
+ header.to = to
72
+ cc = header.cc = debitor.emails_cc
73
+ header.from = config.mail_from
74
+ header.subject = encode_subject config, subject
75
+ header.date = Time.now
76
+ header.add('Content-Type', 'text/plain', nil,
77
+ 'charset' => config.mail_charset)
78
+ mpart.body = body
79
+ recipients = config.mail_recipients.dup.push(to).concat(cc).uniq
80
+ Net::SMTP.start(config.smtp_server, config.smtp_port,
81
+ config.smtp_domain, config.smtp_user, config.smtp_pass,
82
+ config.smtp_authtype) { |smtp|
83
+ recipients.each { |recipient|
84
+ smtp.sendmail(mpart.to_s, config.smtp_user, recipient)
85
+ }
86
+ }
87
+ recipients
88
+ end
89
+ end
90
+ def Mail.encode_subject(config, subject)
91
+ encoded = [subject].pack('M').gsub("=\n", '').gsub(' ', '=20')
92
+ sprintf("=?%s?q?%s?=", config.mail_charset, encoded)
93
+ end
94
+ end
95
+ end
data/lib/ydim/odba.rb ADDED
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env ruby
2
+ # ODBA -- ydim -- 10.01.2006 -- hwyss@ywesee.com
3
+
4
+ require 'odba'
5
+ require 'ydim/currency_converter'
6
+ require 'ydim/debitor'
7
+ require 'ydim/invoice'
8
+
9
+ module ODBA
10
+ module Persistable
11
+ alias :save :odba_store
12
+ end
13
+ end
14
+ module YDIM
15
+ class CurrencyConverter
16
+ include ODBA::Persistable
17
+ ODBA_SERIALIZABLE = ['@conversions']
18
+ end
19
+ class Debitor
20
+ include ODBA::Persistable
21
+ ODBA_SERIALIZABLE = ['@address_lines', '@emails_cc', '@hosting_items']
22
+ odba_index :email
23
+ odba_index :name
24
+ odba_index :unique_id
25
+ end
26
+ class Invoice
27
+ include ODBA::Persistable
28
+ class << self
29
+ alias :all :odba_extent
30
+ end
31
+ alias :save :odba_store
32
+ ODBA_SERIALIZABLE = ['@items']
33
+ odba_index :status
34
+ odba_index :unique_id, :class_filter => :instance_of?
35
+ end
36
+ class AutoInvoice
37
+ odba_index :unique_id, :class_filter => :instance_of?
38
+ end
39
+ end