ascii_invoicer 2.5.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.
@@ -0,0 +1,209 @@
1
+ module Generators
2
+ def generate_hours_total full_data
3
+ hours = full_data[:hours]
4
+ hours[:salary] * hours[:time]
5
+ end
6
+
7
+ def generate_hours_time full_data
8
+ hours = full_data[:hours]
9
+ sum = 0
10
+ if hours[:caterers]
11
+ hours[:caterers].values.each{|v| sum += v.rationalize}
12
+ return sum.to_f
13
+ elsif hours[:time]
14
+ fail_at :caterers
15
+ return hours[:time]
16
+ end
17
+ sum
18
+ end
19
+
20
+ def generate_client_fullname full_data
21
+ client = full_data[:client]
22
+ fail_at :client_first_name unless client[:first_name]
23
+ fail_at :client_last_name unless client[:last_name]
24
+ return fail_at :client_fullname unless client[:first_name] and client[:last_name]
25
+ return [client[:first_name], client[:last_name]].join ' '
26
+ end
27
+
28
+ def generate_client_addressing full_data
29
+ return fail_at(:client_addressing) unless full_data[:client]
30
+ return fail_at(:client_title) unless full_data[:client][:title]
31
+ lang = full_data[:lang]
32
+ client = full_data[:client]
33
+ title = client[:title].words.first.downcase
34
+ gender = @settings['gender_matches'][title]
35
+ addressing = @settings['lang_addressing'][lang][gender]
36
+ return "#{addressing} #{client[:title]} #{client[:last_name]}"
37
+ end
38
+
39
+ def generate_caterers full_data
40
+ caterers = []
41
+ full_data[:hours][:caterers].each{|name, time| caterers.push name} if full_data[:hours][:caterers]
42
+ return caterers
43
+ end
44
+
45
+ def generate_event_date full_data
46
+ Date.parse full_data[:event][:dates][0][:begin] unless full_data[:event][:dates].nil?
47
+ end
48
+
49
+ def generate_event_calendaritems full_data
50
+ begin
51
+ events = []
52
+ full_data[:event][:dates].each { |date|
53
+ # TODO event times is not implemented right
54
+ unless date[:times].nil?
55
+
56
+ ## set specific times
57
+ date[:times].each { |time|
58
+ if time[:end]
59
+ dtstart = DateTime.parse( date[:begin].strftime("%d.%m.%Y ") + time[:begin] )
60
+ dtend = DateTime.parse( date[:begin].strftime("%d.%m.%Y ") + time[:end] )
61
+ else
62
+ dtstart = Icalendar::Values::Date.new( date[:begin].strftime "%Y%m%d")
63
+ dtend = Icalendar::Values::Date.new((date[:end]+1).strftime "%Y%m%d")
64
+ end
65
+ event = Icalendar::Event.new
66
+ event.dtstart = dtstart
67
+ event.dtend = dtend
68
+ events.push event unless event.dtstart.nil?
69
+
70
+ }
71
+
72
+ else
73
+ ## set full day event
74
+ event = Icalendar::Event.new
75
+ event.dtstart = Icalendar::Values::Date.new( date[:begin].strftime "%Y%m%d")
76
+ event.dtend = Icalendar::Values::Date.new((date[:end]+1).strftime "%Y%m%d")
77
+ events.push event unless event.dtstart.nil?
78
+
79
+ end
80
+
81
+ events.each{ | event|
82
+
83
+ event.description = ""
84
+ event.summary = full_data[:event][:name]
85
+ event.summary = "CANCELED: #{ event.summary }" if full_data[:canceled]
86
+
87
+ event.description += "Verantwortung: " + full_data[:manager] + "\n" if full_data[:manager]
88
+ if full_data[:hours][:caterers]
89
+ event.description += "Caterer:\n"
90
+ full_data[:caterers].each {|caterer,time| event.description += " - #{ caterer}\n" }
91
+ end
92
+
93
+ event.description += full_data[:description] + "\n" if full_data[:description]
94
+ }
95
+ }
96
+ return events
97
+ rescue
98
+ @errors << :event_dates
99
+ return false
100
+ end
101
+ end
102
+
103
+ def generate_productsbytax full_data
104
+ list = {}
105
+ taxlist = {}
106
+ full_data[:products].each {|product|
107
+ list[product.tax_value] = [] unless list[product.tax_value]
108
+ list[product.tax_value] << product
109
+ }
110
+ list.keys.sort.each{|key| taxlist[key] = list[key] } # sorting a hash by keys
111
+ return taxlist
112
+ end
113
+
114
+ def generate_event_age full_data
115
+ (Date.today - full_data[:event][:date]).to_i
116
+ end
117
+
118
+ def sum_money key
119
+ sum = 0.to_euro
120
+ @data[:products].each{|p| sum += p.hash[key]} if @data[:products].class == Array
121
+ sum.to_euro
122
+ end
123
+
124
+ def generate_event_date full_data
125
+ full_data[:event][:dates][0][:begin] unless full_data[:event][:dates].nil?
126
+ end
127
+
128
+ def generate_event_prettydate full_data
129
+ return fail_at :event_prettydate if full_data[:event][:dates].nil?
130
+ date = full_data[:event][:dates][0]
131
+ first = date[:begin]
132
+ last = full_data[:event][:dates].last[:end]
133
+ last = full_data[:event][:dates].last[:begin] if last.nil?
134
+
135
+ return "#{first.strftime "%d"}-#{last.strftime "%d.%m.%Y"}" if first != last
136
+ return first.strftime "%d.%m.%Y" if first.class == Date
137
+ return first
138
+ end
139
+
140
+ def generate_offer_number full_data
141
+ appendix = full_data[:offer][:appendix]
142
+ full_data[:offer][:date].strftime "A%Y%m%d-#{appendix}"
143
+ end
144
+
145
+
146
+ # costs: price of all products summed up
147
+ # taxes: price of all products taxes summed up ( e.g. price*0.19 )
148
+ # total: costs + taxes
149
+ # final: total + salary * hours
150
+
151
+
152
+ def generate_offer_costs full_data
153
+ sum_money :cost_offer
154
+ end
155
+
156
+ def generate_offer_taxes full_data
157
+ sum_money :tax_offer
158
+ end
159
+
160
+ def generate_offer_total full_data
161
+ sum_money :total_offer
162
+ end
163
+
164
+ def generate_offer_final full_data
165
+ full_data[:offer][:total] + full_data[:hours][:total]
166
+ end
167
+
168
+
169
+
170
+
171
+ def generate_invoice_costs full_data
172
+ sum_money :cost_invoice
173
+ end
174
+
175
+ def generate_invoice_taxes full_data
176
+ sum_money :tax_invoice
177
+ end
178
+
179
+ def generate_invoice_total full_data
180
+ sum_money :total_invoice
181
+ end
182
+
183
+ def generate_invoice_final full_data
184
+ full_data[:invoice][:total] + full_data[:hours][:total]
185
+ end
186
+
187
+ def generate_invoice_delay full_data
188
+ return 0 if full_data[:canceled]
189
+ return -(full_data[:event][:date] - full_data[:invoice][:date] if full_data[:invoice][:date]).to_i
190
+ return -(full_data[:event][:date] - Date.today).to_i
191
+ end
192
+
193
+ def generate_invoice_paydelay full_data
194
+ if full_data[:invoice][:payed_date] and full_data[:invoice][:date]
195
+ delay = full_data[:invoice][:payed_date] - full_data[:invoice][:date]
196
+ fail_at :invoice_payed if delay < 0
197
+ return delay.to_i
198
+ end
199
+ return nil
200
+ end
201
+
202
+ def generate_invoice_longnumber full_data
203
+ if full_data[:invoice][:date]
204
+ year = full_data[:invoice][:date].year
205
+ full_data[:invoice][:number].gsub /^R/, "R#{year}-" if full_data[:invoice][:number]
206
+ end
207
+ end
208
+
209
+ end
@@ -0,0 +1,23 @@
1
+ require 'hash-graft'
2
+
3
+ class HashTransformer
4
+ attr_reader :original_hash, :new_hash
5
+ attr_writer :rules
6
+
7
+ def initialize(hash = {})
8
+ @original_hash = hash[:original_hash]
9
+ @original_hash ||= {}
10
+ @new_hash = {}
11
+ @rules = hash[:rules]
12
+ @rules ||= []
13
+ end
14
+
15
+ def transform
16
+ @new_hash = @original_hash
17
+ @rules.each {|rule|
18
+ @new_hash.set_path(rule[:new], @original_hash.get_path(rule[:old]))
19
+ }
20
+ return @new_hash
21
+ end
22
+
23
+ end
@@ -0,0 +1,268 @@
1
+ require 'icalendar'
2
+ require 'fileutils'
3
+
4
+ module AsciiMixins
5
+
6
+ ## Use Option parser or leave it if only one argument is given
7
+
8
+ def render_project project, choice
9
+ project.validate choice
10
+ if project.valid_for[choice]
11
+ project.create_tex choice, options[:check]
12
+ else
13
+ $logger.error "#{project.name} is not ready for creating an #{choice.to_s}! #{project.data[:valid]} #{project.errors if project.errors.length > 0}"
14
+ end
15
+ end
16
+
17
+ ##TODO turn color_from_date(date) into a loopuk into $SETTINGS
18
+ def color_from_date(date)
19
+ diff = date - Date.today
20
+ return (rand * 256**3).to_i.to_s(16) if Date.today.day == 1 and Date.today.month == 4 #april fools
21
+ return :magenta if diff < -28
22
+ return :cyan if diff < 0
23
+ return [:yellow,:bright] if diff == 0
24
+ return :red if diff < 7
25
+ return :yellow if diff < 14
26
+ return [:green]
27
+ end
28
+
29
+ def print_project_list projects, hash = {}
30
+ table = Textboxes.new
31
+ table.style[:border] = false
32
+ table.style[:column_borders] = false
33
+ table.style[:row_borders] = false
34
+ table.style[:padding_horizontal] = 1
35
+ projects.each_index do |i|
36
+ project = projects[i]
37
+ if !hash[:colors].nil? and hash[:colors]
38
+ color = color_from_date(project.date)
39
+ color = :default if project.validate(:invoice)
40
+ color = [:blue] if project.status == :canceled
41
+ end
42
+ if hash[:verbose]
43
+ row = print_row_verbose project, hash
44
+ else
45
+ row = print_row_simple project, hash
46
+ end
47
+
48
+ row << project.data[:hours][:caterers].keys.join(", ") if hash[:caterers] and project.data[:hours][:caterers]
49
+
50
+ row << project.blockers(:archive) if hash[:blockers]
51
+ if hash[:details]
52
+ hash[:details].each {|detail|
53
+ row << project.data.get_path(detail)
54
+ }
55
+ end
56
+
57
+ row << project.errors if hash[:errors] and project.status == :ok
58
+ row << project.status if hash[:errors] and project.status == :canceled
59
+ row.insert 0, i+1
60
+ table.add_row row, color
61
+ end
62
+ table.set_alignments(:r, :l, :l)
63
+ puts table
64
+ end
65
+
66
+ def print_row_simple(project,hash)
67
+ row = [
68
+ project.pretty_name,
69
+ project.data[:manager],
70
+ project.data[:event][:invoice_number],
71
+ project.data[:event][:date].strftime("%d.%m.%Y"),
72
+ #project.index
73
+ ]
74
+ return row
75
+ end
76
+
77
+ def print_row_verbose (project, hash)
78
+ name = "##{project.data[:name]}#"
79
+ if not project.data[:event][:name].nil? and project.data[:event][:name].size > 0
80
+ name = project.data[:event][:name]
81
+ name = "CANCELED: " + name if project.data :canceled
82
+ end
83
+ row = [
84
+ name,
85
+ project.data[:manager],
86
+ project.data[:invoice][:number],
87
+ project.date.strftime("%d.%m.%Y"),
88
+ project.validate(:offer).print,
89
+ project.validate(:invoice).print,
90
+ project.validate(:payed).print($SETTINGS.currency_symbol),
91
+ # try these: ☑☒✉☕☀☻
92
+ ]
93
+ return row
94
+ end
95
+
96
+ def print_project_list_paths(projects)
97
+ table = Textboxes.new
98
+ projects.each_index do |i|
99
+ p = projects[i]
100
+ table.add_row [
101
+ (i+1).to_s+".",
102
+ p.name.ljust(35),
103
+ p.data[:project_path]
104
+ ]
105
+ end
106
+ table.set_alignments(:r, :l, :l)
107
+ puts table
108
+ end
109
+
110
+ def create_cal_file(projects)
111
+ cal = Icalendar::Calendar.new
112
+ projects.each_index do |i|
113
+ project = projects[i]
114
+ events = project.data[:event][:calendaritems]
115
+ if events
116
+ events.each { |event| cal.add_event event}
117
+ else
118
+ $logger.warn "Calendar can't be parsed. (#{project.data[:name]})", :file
119
+ end
120
+ end
121
+
122
+ cal_file_path = File.join(FileUtils.pwd, $SETTINGS.calendar_file)
123
+ cal_file = File.open(cal_file_path, ?w)
124
+ cal_file.write cal.to_ical
125
+ puts "created #{cal_file_path}"
126
+ end
127
+
128
+ #takes an array of invoices (@plumber.working_projects)
129
+ def print_project_list_yaml(projects)
130
+ projects.each do |p|
131
+ puts p.data.to_yaml
132
+ puts "...\n\n"
133
+ end
134
+ end
135
+
136
+ def caterers_string project, join = ", "
137
+ data = project.data
138
+ data[:hours][:caterers].map{|name, hours| "#{name} (#{hours})" if hours > 0 }.join join if data[:hours][:caterers]
139
+ end
140
+
141
+ #takes an array of invoices (@plumber.working_projects)
142
+ def print_project_list_csv(projects)
143
+ header = [
144
+ 'Rnum',
145
+ 'Bezeichnung',
146
+ 'Datum',
147
+ 'Rechnungsdatum',
148
+ 'Betreuer',
149
+ 'verantwortlich',
150
+ 'Bezahlt am',
151
+ 'Betrag',
152
+ 'Canceled',
153
+ ]
154
+ puts header.to_csv(col_sep:";")
155
+ projects.each do |p|
156
+ canceled = ""
157
+ canceled = "canceled" if p.data[:canceled]
158
+ line = [
159
+ p.data[:invoice][:number],
160
+ p.data[:event][:name],
161
+ p.data[:event][:date],
162
+ p.data[:invoice][:date],
163
+ caterers_string(p),
164
+ p.data[:manager].words[0],
165
+ p.data[:invoice][:payed_date],
166
+ p.data[:invoice][:final],
167
+ canceled,
168
+ # p.valid_for[:invoice]
169
+ ]
170
+ canceled = "canceled" if p.data[:canceled]
171
+ line.map! {|v| v ? v : "" } # wow, that looks cryptic
172
+ puts line.to_csv(col_sep:";")
173
+ end
174
+ end
175
+
176
+ def display_products project, choice = :offer, standalone = true
177
+ table = Textboxes.new
178
+ table.style[:border] = standalone
179
+ table.title = "Project:" + "\"#{project.data[:event][:name]}\"".rjust(25) if standalone
180
+ table.add_row ["#", "name", "price", "cost"]
181
+ table.set_alignments :r, :l, :r, :r
182
+ project.data[:products].each {|product|
183
+ amount = product.amount choice
184
+ price = product.price
185
+ cost = product.cost choice
186
+ table.add_row [amount, product.name, price, cost]
187
+ }
188
+ table.add_row ["#{project.data[:hours][:time]}h", "service" , project.data[:hours][:salary], project.data[:hours][:total]] if project.data.get_path('hours/time').to_i> 0
189
+ table.add_row [nil, caterers_string(project)]
190
+
191
+ return table
192
+ end
193
+
194
+ def display_all project, choice, show_errors = true
195
+ raise "choice must be either :invoice or :offer" unless choice == :invoice or choice == :offer
196
+ data = project.data
197
+
198
+ table = Textboxes.new
199
+ table.style[:border] = true
200
+ table.title = "Project:" + "\"#{data[:event][:name]}\"".rjust(25)
201
+ table.add_row [nil, "name", "amount","price", "cost"]
202
+ table.set_alignments :r, :l, :r, :r, :r
203
+
204
+ i = 0
205
+ data[:products].each {|product|
206
+ amount = product.amount choice
207
+ price = product.price
208
+ cost = product.cost choice
209
+ table.add_row [i+=1,product.name, amount, price, cost]
210
+ }
211
+ table.add_row [i+1,"service", "#{data[:hours][:time]}h", data[:hours][:salary], data[:hours][:total]] if project.data.get_path('hours/time').to_i> 0
212
+
213
+ separator = table.column_widths.map{|w| ?=*w}
214
+ separator[0] = nil
215
+ table.add_row separator
216
+
217
+ table.add_row [nil,"Kosten",nil,nil,"#{data[choice][:costs]}"]
218
+ data[:productsbytax].each {|tax,products|
219
+ tpv = 0.to_euro # tax per value
220
+ tax = (tax.rationalize * 100).to_f
221
+ products.each{|p|
222
+ tpv += p.hash[:tax_offer] if choice == :offer
223
+ tpv += p.hash[:tax_invoice] if choice == :invoice
224
+ }
225
+ table.add_row [nil, "MWST #{tax}%",nil,nil,"#{tpv}"]
226
+ }
227
+ table.add_row [nil, "Final", nil, nil, "#{data[choice][:final]}"]
228
+
229
+ if show_errors
230
+ table.footer = "Errors: #{project.errors.length} (#{ project.errors.join ',' })" if project.errors.length >0
231
+ end
232
+
233
+ return table
234
+ end
235
+
236
+ def display_products_csv project
237
+ puts [['name', 'price', 'amount', 'sold', 'tax_value'].to_csv(col_sep:?;)]+
238
+ project.data[:products].map{|p| p.to_csv(col_sep:?;)}
239
+ end
240
+
241
+ def check_project(path)
242
+ project = InvoiceProject.new $SETTINGS
243
+ project.open path
244
+ unless project.validate(:offer)
245
+ puts "\nWARNING: the file you just edited contains errors! (#{project.errors})"
246
+ unless no? "would you like to edit it again? [y|N]"
247
+ edit_files path
248
+ end
249
+ end
250
+ end
251
+
252
+ ## hand path to editor
253
+ def edit_files(paths, editor = $SETTINGS.editor)
254
+ paths = [paths] if paths.class == String
255
+ paths.select! {|path| path}
256
+ if paths.empty?
257
+ $logger.error "no paths to open"
258
+ return false
259
+ end
260
+ paths.map!{|path| "\"#{path}\"" }
261
+ paths = paths.join ' '
262
+ editor = $SETTINGS.editor unless editor
263
+ $logger.info "Opening #{paths} in #{editor}"
264
+ pid = spawn "#{editor} #{paths}"
265
+ Process.wait pid
266
+ end
267
+
268
+ end