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,17 @@
1
+ begin
2
+ require "ascii_invoicer/version"
3
+ require "ascii_invoicer/settings_manager"
4
+ require "ascii_invoicer/InvoiceProject"
5
+ require "ascii_invoicer/hash_transformer"
6
+ require "ascii_invoicer/tweaks"
7
+ require "ascii_invoicer/mixins"
8
+ require "ascii_invoicer/ascii_logger"
9
+ rescue LoadError
10
+ require "./lib/ascii_invoicer/version"
11
+ require "./lib/ascii_invoicer/settings_manager"
12
+ require "./lib/ascii_invoicer/InvoiceProject"
13
+ require "./lib/ascii_invoicer/hash_transformer"
14
+ require "./lib/ascii_invoicer/tweaks"
15
+ require "./lib/ascii_invoicer/mixins"
16
+ require "./lib/ascii_invoicer/ascii_logger"
17
+ end
@@ -0,0 +1,356 @@
1
+ require 'yaml'
2
+ require 'csv'
3
+ require 'date'
4
+ require 'euro'
5
+
6
+ require File.join __dir__, 'hash_transformer.rb'
7
+ require File.join __dir__, 'projectFileReader.rb'
8
+ require File.join __dir__, 'rfc5322_regex.rb'
9
+ require File.join __dir__, 'texwriter.rb'
10
+ require File.join __dir__, 'filters.rb'
11
+ require File.join __dir__, 'generators.rb'
12
+
13
+ ## TODO requirements and validity
14
+ ## TODO open, YAML::parse, [transform, ] read_all, generate, validate
15
+ ## TODO statemachine!!
16
+ # http://www.zenspider.com/Languages/Ruby/QuickRef.html
17
+ class InvoiceProject < LuigiProject
18
+ attr_reader :project_path, :project_folder,
19
+ :raw_data,
20
+ :status, :errors,
21
+ :settings, :defaults
22
+
23
+ attr_writer :raw_data, :defaults
24
+
25
+ include TexWriter
26
+ include Filters
27
+ include Generators
28
+ include ProjectFileReader
29
+
30
+ # keys that are in the original file and that will be filtered
31
+ @@filtered_keys=%i[
32
+ format lang created
33
+ client event manager
34
+ offer invoice canceled
35
+ messages products hours
36
+ includes
37
+ ]
38
+
39
+ # keys that are not originally in the file will be generated
40
+ @@generated_keys=%i[
41
+ client_fullname
42
+ client_addressing
43
+ hours_time
44
+ hours_total
45
+ event_date
46
+ event_age
47
+ event_prettydate
48
+ caterers
49
+ offer_number
50
+ offer_costs offer_taxes offer_total offer_final
51
+ invoice_costs invoice_taxes invoice_total invoice_final
52
+ invoice_delay
53
+ invoice_paydelay
54
+ invoice_longnumber
55
+ event_calendaritems
56
+ productsbytax
57
+ ]
58
+
59
+ #def initialize(project_path = nil, template_path = nil, settings = $settings, name = nil)
60
+ def initialize(hash)
61
+ @path = hash[:path]
62
+ @settings = hash[:settings]
63
+ @template_path = hash[:template_path]
64
+ @data = hash[:data]
65
+ @data ||= {}
66
+
67
+ @name = File.basename @path, '.yml'
68
+ @settings = hash[:settings]
69
+ @status = :ok
70
+ @errors = []
71
+ @defaults = {}
72
+ @defaults = @settings[:defaults] unless @settings[:defaults].nil?
73
+
74
+ @defaults['format'] = '1.0.0'
75
+ @logger = $logger
76
+
77
+ unless @template_path.nil?
78
+ create @template_path
79
+ end
80
+
81
+ open(@path) unless @path.nil?
82
+ end
83
+
84
+ ## open given .yml and parse into @raw_data
85
+ def open(project_path)
86
+ #puts "opening \"#{project_path}\""
87
+ raise "already opened another project" unless @project_path.nil?
88
+ @project_path = project_path
89
+ @project_folder = File.split(project_path)[0]
90
+
91
+ error "FILE \"#{project_path}\" does not exist!" unless File.exists?(project_path)
92
+
93
+ ## setting the name
94
+ @data[:name] = File.basename File.split(@project_path)[0]
95
+
96
+ ## opening the project file
97
+ begin
98
+ @raw_data = YAML::load(File.open(project_path))
99
+ rescue SyntaxError => error
100
+ @logger.warn "SyntaxError in #{project_path}, use \"edit\" to correct it.", :both
101
+ @logger.error error, :file
102
+ @status = :unparsable
103
+ return false
104
+ rescue Psych::SyntaxError => error
105
+ @logger.warn "SyntaxError in #{project_path}, use \"edit\" to correct it.", :both
106
+ @logger.error error, :file
107
+ @status = :unparsable
108
+ return false
109
+ else
110
+ @data[:valid] = true # at least for the moment
111
+ @status = :ok
112
+ @data[:project_path] = project_path
113
+ end
114
+
115
+ #load format and transform or not
116
+ @data[:format] = @raw_data['format'] ? @raw_data['format'] : "1.0.0"
117
+ if @data[:format] < "2.4.0"
118
+ begin
119
+ @raw_data = import_100 @raw_data
120
+ rescue =>error
121
+ @status = :unparsable
122
+ @logger.warn "#{error} parsing #{@project_path}"
123
+ puts $@
124
+ return false
125
+ end
126
+ end
127
+
128
+ prepare_data()
129
+ return true
130
+ end
131
+
132
+ # displays "CANCELED: name if canceled"
133
+ def pretty_name
134
+ @data[:canceled] ? "CANCELED: #{@data[:name]}" : @data[:name]
135
+ end
136
+
137
+ # returns the name (LuigiProject Interface)
138
+ def name
139
+ @data[:name]
140
+ end
141
+
142
+ # returns the date (LuigiProject Interface)
143
+ def date
144
+ return @data[:event][:date] if @data[:event][:date]
145
+ return @data[:created] if @data[:created]
146
+ return Date.parse "01.01.0000"
147
+ end
148
+
149
+ # returns the manager
150
+ def manager
151
+ data :manager
152
+ end
153
+
154
+ def path
155
+ @project_path
156
+ end
157
+
158
+ def fill_template
159
+ project_name = @name.sub(?_, " ") #latex build fails if values contain a "_"
160
+ manager_name = @settings.manager_name
161
+ version = @settings.version
162
+ template = File.basename(@template_path, ".yml.erb")
163
+
164
+ return binding()
165
+ end
166
+
167
+ # returns index for sorting (year+invoicenumber)
168
+ def index
169
+ return @data[:invoice][:number] + date.strftime('%Y%m%d') if @data[:invoice][:number]
170
+ return "zzz" + date.strftime('%Y%m%d')
171
+ end
172
+
173
+ ## currently only from 1.0.0 to 2.4.0 Format
174
+ def import_100 hash
175
+ rules = [
176
+ { old:"client", new:"client/fullname" },
177
+ { old:"address", new:"client/address" },
178
+ { old:"email", new:"client/email" },
179
+ { old:"event", new:"event/name" },
180
+ { old:"location", new:"event/location" },
181
+ { old:"description", new:"event/description" }, #trim
182
+ { old:"manumber", new:"offer/number" },
183
+ { old:"anumber", new:"offer/appendix" },
184
+ { old:"rnumber", new:"invoice/number" },
185
+ { old:"payed_date", new:"invoice/payed_date"},
186
+ { old:"invoice_date", new:"invoice/date" },
187
+ { old:"signature", new:"manager" }, #trim
188
+ #{ old:"hours/time", new:"hours/total" },
189
+ ]
190
+ ht = HashTransformer.new :rules => rules, :original_hash => hash
191
+ new_hash = ht.transform()
192
+ new_hash[ 'created' ] = "01.01.0000"
193
+
194
+ date = strpdates(hash['date'])
195
+ new_hash.set_path("event/dates/0/begin", date[0])
196
+ new_hash.set_path("event/dates/0/end", date[1]) unless date[1].nil?
197
+ new_hash.set_path("event/dates/0/time/begin", new_hash.get_path("time")) if date[1].nil?
198
+ new_hash.set_path("event/dates/0/time/end", new_hash.get_path("time_end")) if date[1].nil?
199
+
200
+ new_hash['manager']= new_hash['manager'].lines.to_a[1] if new_hash['manager'].lines.to_a.length > 1
201
+
202
+ if new_hash.get_path("client/fullname").words.class == Array
203
+ new_hash.set_path("client/title", new_hash.get_path("client/fullname").lines.to_a[0].strip)
204
+ new_hash.set_path("client/last_name", new_hash.get_path("client/fullname").lines.to_a[1].strip)
205
+ new_hash.set_path("client/fullname", new_hash.get_path("client/fullname").gsub("\n",' ').strip)
206
+ else
207
+ fail_at :client_fullname
208
+ end
209
+ new_hash.set_path("offer/date", Date.today)
210
+ new_hash.set_path("invoice/date", Date.today) unless new_hash.get_path("invoice/date")
211
+
212
+ return hash
213
+ end
214
+
215
+ def products
216
+ data :products
217
+ end
218
+
219
+
220
+ def prepare_data
221
+ @@filtered_keys.each {|key| read key }
222
+ @@generated_keys.each {|key|
223
+ value = apply_generator key, @data
224
+ @data.set_path key, value, ?_, true # symbols = true
225
+ }
226
+ end
227
+
228
+ def validate choice = :invoice
229
+ blockers(choice).length == 0
230
+ end
231
+
232
+ def blockers choice = :invoice
233
+ inval = {} # invalidators
234
+ inval[ :minimal ] = [:client_last_name, :caterers, :manager, :products, :event_dates]
235
+ inval[ :offer ] = [:offer_number ] + inval[:minimal]
236
+ inval[ :invoice ] = inval[:offer] + [:invoice_number, :invoice_date]
237
+ inval[ :payed ] = inval[:invoice] + [:invoice_payed]
238
+ inval[ :archive ] = inval[:payed]
239
+ inval[ :calendar] = inval[:offer]
240
+ inval[choice] & @errors
241
+ end
242
+
243
+ def to_s
244
+ name
245
+ end
246
+
247
+ def to_yaml
248
+ @raw_data.to_yaml
249
+ end
250
+
251
+
252
+ #getters for path_through_document
253
+ #getting path['through']['document']
254
+ def data key = nil
255
+ return @data if key.nil?
256
+ return @data.get_path key
257
+ end
258
+
259
+ def export_filename choice, ext=""
260
+ offer_number = data 'offer/number'
261
+ invoice_number = data 'invoice/number'
262
+ name = data 'name'
263
+ date = data('event/date').strftime "%Y-%m-%d"
264
+
265
+ ext.prepend '.' unless ext.length > 0 and ext.start_with? '.'
266
+
267
+ if choice == :invoice
268
+ "#{invoice_number} #{name} #{date}#{ext}"
269
+ elsif choice == :offer
270
+ "#{offer_number} #{name}#{ext}"
271
+ else
272
+ return false
273
+ end
274
+ end
275
+ end
276
+
277
+ class InvoiceProduct
278
+ attr_reader :name, :hash, :tax, :valid, :returned,
279
+ :total_invoice, :cost_offer, :cost_invoice, :cost_offer,
280
+ :tax_invoice, :tax_offer, :tax_value,
281
+ :price, :unit
282
+
283
+ def initialize(hash, settings)
284
+ @hash = hash
285
+ @name = hash[:name]
286
+ @price = hash[:price]
287
+ @unit = hash[:unit]
288
+ @amount = hash[:amount]
289
+ @settings = settings
290
+ if hash[:tax]
291
+ @tax_value = hash[:tax]
292
+ else
293
+ @tax_value = @settings[:defaults][:tax]
294
+ end
295
+
296
+ fail "TAX MUST NOT BE > 1" if @tax_value > 1
297
+
298
+ @valid = true
299
+ calculate() unless hash.nil?
300
+ end
301
+
302
+ def to_csv *args
303
+ [@name, @price, @amount, @sold, @tax_value].to_csv(*args)
304
+ end
305
+
306
+ def to_s
307
+ "#{@amount}/#{@sold} #{@name}, #{@price} cost (#{@cost_offer}|#{@cost_invoice}) total(#{@total_offer}|#{@total_invoice} #{@tax_value}) "
308
+ end
309
+
310
+ def calculate()
311
+ return false if @hash.nil?
312
+ @valid = false unless @hash[:sold].nil? or @hash[:returned].nil?
313
+ @valid = false unless @hash[:amount] and @hash[:price]
314
+ @sold = @hash[:sold]
315
+ @price = @hash[:price].to_euro
316
+ @amount = @hash[:amount]
317
+ @returned = @hash[:returned]
318
+
319
+ if @sold
320
+ @returned = @amount - @sold
321
+ elsif @returned
322
+ @sold = @amount - @returned
323
+ else
324
+ @sold = @amount
325
+ @returned = 0
326
+ end
327
+
328
+ @hash[:cost_offer] = @cost_offer = (@price * @amount).to_euro
329
+ @hash[:cost_invoice] = @cost_invoice = (@price * @sold).to_euro
330
+
331
+ @hash[:tax_offer] = @tax_offer = (@cost_offer * @tax_value)
332
+ @hash[:tax_invoice] = @tax_invoice = (@cost_invoice * @tax_value)
333
+
334
+ @hash[:total_offer] = @total_offer = (@cost_offer + @tax_offer)
335
+ @hash[:total_invoice] = @total_invoice = (@cost_invoice + @tax_invoice)
336
+ self.freeze
337
+ end
338
+
339
+ def amount choice
340
+ return @sold if choice == :invoice
341
+ return @amount if choice == :offer
342
+ return -1
343
+ end
344
+
345
+ def cost choice
346
+ return @cost_invoice if choice == :invoice
347
+ return @cost_offer if choice == :offer
348
+ return -1.to_euro
349
+ end
350
+
351
+ def tax choice
352
+ return @tax_invoice if choice == :invoice
353
+ return @tax_offer if choice == :offer
354
+ return -1.to_euro
355
+ end
356
+ end
@@ -0,0 +1,64 @@
1
+ #http://stackoverflow.com/questions/6407141/how-can-i-have-ruby-logger-log-output-to-stdout-as-well-as-file
2
+ require 'logger'
3
+ require 'fileutils'
4
+
5
+ class MultiIO
6
+ def initialize(*targets)
7
+ @targets = targets
8
+ end
9
+
10
+ def write(*args)
11
+ @targets.each {|t| t.write(*args); t.flush}
12
+ end
13
+
14
+ def close
15
+ @targets.each(&:close)
16
+ end
17
+ end
18
+
19
+ class AsciiLogger
20
+
21
+ # logger.log, "message", :stdo
22
+ # logger.log, "message", :file
23
+ # logger.log, "message", :both
24
+ # log, error, info, warn, fatal, unknown
25
+ def initialize name, path
26
+ path = File.expand_path path
27
+ FileUtils.touch path
28
+ @log_file = File.open path, ?a
29
+ @known_loggers = [:file, :stdo, :both]
30
+ @known_methods = [:info, :warn, :error, :fatal, :unknown]
31
+
32
+ @file_logger = Logger.new MultiIO.new @log_file
33
+ @stdo_logger = Logger.new STDOUT
34
+ @both_logger = Logger.new MultiIO.new STDOUT, @log_file
35
+
36
+ @file_logger.progname = name
37
+ @stdo_logger.progname = name
38
+ @both_logger.progname = name
39
+ end
40
+
41
+ def log logger_name, method_name, message
42
+ raise "#{method_name}, unknown method type, use #{@known_methods}" unless @known_methods.include? method_name
43
+ raise "#{logger_name}, unknown logger type, use #{@known_loggers}" unless @known_loggers.include? logger_name
44
+
45
+ logger_name = "@#{(logger_name.to_s)}_logger".to_sym
46
+ if instance_variables.include? logger_name
47
+ logger = instance_variable_get logger_name
48
+ logger.method(method_name).call message
49
+ else
50
+ puts "ERROR: logger not found (#{logger_name})"
51
+ end
52
+ end
53
+
54
+ # imitates stdlib logger interface -> AsciiLogger::error message, {:stdo\:file\:both}
55
+ def method_missing method_name, *stuff
56
+ if @known_methods.include? method_name.to_sym
57
+ message = stuff[0]
58
+ logger = stuff[1].to_sym if stuff[1]
59
+ logger = :both unless @known_loggers.include? logger
60
+ log logger, method_name.to_sym, message
61
+ end
62
+ end
63
+
64
+ end
@@ -0,0 +1,129 @@
1
+ require 'date'
2
+ #require File.join __dir__ + '/rfc5322_regex.rb'
3
+
4
+ module Filters
5
+
6
+
7
+ def strpdates(string,pattern = nil)
8
+ return [Date.today] unless string.class == String
9
+ if pattern
10
+ return [Date.strptime(string, pattern).to_date]
11
+ else
12
+ p = string.split('.')
13
+ p_range = p[0].split('-')
14
+
15
+ if p_range.length == 1
16
+ t = Date.new p[2].to_i, p[1].to_i, p[0].to_i
17
+ return [t]
18
+
19
+ elsif p_range.length == 2
20
+ t1 = Date.new p[2].to_i, p[1].to_i, p_range[0].to_i
21
+ t2 = Date.new p[2].to_i, p[1].to_i, p_range[1].to_i
22
+ return [t1,t2]
23
+
24
+ else
25
+ fail
26
+ end
27
+ end
28
+ end
29
+
30
+ def check_email email
31
+ email =~ $RFC5322
32
+ end
33
+
34
+ def filter_client_email email
35
+ return fail_at :client_email unless check_email email
36
+ email
37
+ end
38
+
39
+ def filter_canceled canceled
40
+ @STATUS = :canceled if @STATUS != :unparsable and canceled
41
+ canceled
42
+ end
43
+
44
+ def filter_event_dates dates
45
+ dates.each {|d|
46
+ unless d[:time].nil? or d[:end].nil? ## time and end is missleading
47
+ @logger.warn "FILTER: #{name} missleading: time and end_date"
48
+ return fail_at :event_dates
49
+ end
50
+
51
+ d[:begin] = Date.parse(d[:begin]) if d[:begin].class == String
52
+
53
+ if not d[:time].nil?
54
+ d[:time][:begin] = DateTime.strptime("#{d[:time][:begin]} #{d[:begin]}", "%H:%M %d.%m.%Y" ) if d[:time][:begin]
55
+ d[:time][:end] = DateTime.strptime("#{d[:time][:end] } #{d[:begin]}", "%H:%M %d.%m.%Y" ) if d[:time][:end]
56
+ end
57
+
58
+ if d[:end].class == String
59
+ d[:end] = Date.parse(d[:end])
60
+ else
61
+ d[:end] = d[:begin]
62
+ end
63
+ }
64
+ dates
65
+ end
66
+
67
+ def filter_event_description string
68
+ return "" unless string
69
+ string.strip
70
+ end
71
+
72
+ def filter_manager string
73
+ string.strip
74
+ end
75
+
76
+ def filter_messages messages
77
+ messages[ @data[:lang].to_sym ]
78
+ end
79
+
80
+ def filter_products products
81
+ new_products = []
82
+ products.each{|k,v|
83
+ if [String, Symbol].include? k.class
84
+ v[:name] = k
85
+ new_products.push InvoiceProduct.new v , @settings
86
+ elsif k.class == Hash
87
+ new_products.push InvoiceProduct.new k.merge(v), @settings
88
+ #new_products.push k.merge(v)
89
+ else
90
+ return k
91
+ throw :filter_error
92
+ end
93
+ }
94
+
95
+ return new_products
96
+ end
97
+
98
+ def filter_created date
99
+ return Date.parse date if date.class == String
100
+ fail_at :created_date
101
+ end
102
+
103
+ def filter_offer_date date
104
+ return Date.parse date if date.class == String
105
+ fail_at :offer_date
106
+ return Date.today
107
+ end
108
+
109
+ def filter_invoice_number number
110
+ return fail_at :invoice_number if number.nil?
111
+ "R#{number.to_s.rjust(3, ?0)}"
112
+ end
113
+
114
+ def filter_invoice_date date
115
+ return Date.parse date if date.class == String
116
+ fail_at :invoice_date
117
+ return Date.today
118
+ end
119
+
120
+ def filter_invoice_payed_date date
121
+ return Date.parse date if date.class == String
122
+ return fail_at :invoice_payed
123
+ end
124
+
125
+ def filter_hours_salary salary
126
+ salary.to_euro
127
+ end
128
+
129
+ end