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.
@@ -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