lewt 0.5.12

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.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.md +22 -0
  3. data/README.md +238 -0
  4. data/bin/lewt +10 -0
  5. data/lib/config/customers.yml +33 -0
  6. data/lib/config/enterprise.yml +54 -0
  7. data/lib/config/settings.yml +10 -0
  8. data/lib/config/templates/invoice.html.liquid +63 -0
  9. data/lib/config/templates/invoice.text.liquid +40 -0
  10. data/lib/config/templates/meta.html.liquid +0 -0
  11. data/lib/config/templates/meta.text.liquid +1 -0
  12. data/lib/config/templates/metastat.html.liquid +2 -0
  13. data/lib/config/templates/metastat.text.liquid +8 -0
  14. data/lib/config/templates/report.html.liquid +29 -0
  15. data/lib/config/templates/report.text.liquid +15 -0
  16. data/lib/config/templates/style.css +461 -0
  17. data/lib/extension.rb +158 -0
  18. data/lib/extensions/calendar-timekeeping/apple_extractor.rb +63 -0
  19. data/lib/extensions/calendar-timekeeping/calendar-timekeeping.rb +65 -0
  20. data/lib/extensions/calendar-timekeeping/extractor.rb +62 -0
  21. data/lib/extensions/calendar-timekeeping/gcal_extractor.rb +61 -0
  22. data/lib/extensions/calendar-timekeeping/ical_extractor.rb +52 -0
  23. data/lib/extensions/liquid-renderer.rb +106 -0
  24. data/lib/extensions/metastat/metamath.rb +108 -0
  25. data/lib/extensions/metastat/metastat.rb +161 -0
  26. data/lib/extensions/simple-expenses.rb +112 -0
  27. data/lib/extensions/simple-invoices.rb +93 -0
  28. data/lib/extensions/simple-milestones.rb +102 -0
  29. data/lib/extensions/simple-reports.rb +81 -0
  30. data/lib/extensions/store.rb +81 -0
  31. data/lib/lewt.rb +233 -0
  32. data/lib/lewt_book.rb +29 -0
  33. data/lib/lewt_ledger.rb +149 -0
  34. data/lib/lewtopts.rb +170 -0
  35. data/tests/LEWT Schedule.ics +614 -0
  36. data/tests/expenses.csv +1 -0
  37. data/tests/milestones.csv +1 -0
  38. data/tests/run_tests.rb +14 -0
  39. data/tests/tc_Billing.rb +29 -0
  40. data/tests/tc_CalExt.rb +44 -0
  41. data/tests/tc_Lewt.rb +37 -0
  42. data/tests/tc_LewtExtension.rb +31 -0
  43. data/tests/tc_LewtLedger.rb +38 -0
  44. data/tests/tc_LewtOpts.rb +26 -0
  45. metadata +158 -0
@@ -0,0 +1,112 @@
1
+ require "csv"
2
+
3
+ # Author:: Jason Wijegooneratne (mailto:code@jwije.com)
4
+ # Copyright:: Copyright (c) 2014 Jason Wijegooneratne
5
+ # License:: MIT. See LICENSE.md distributed with the source code for more information.
6
+
7
+
8
+ module LEWT
9
+
10
+ # The Simple Expenses LEWT Extensions allows you to manage you expenses in a CSV file. The CSV
11
+ # file itself must conform to the following specification:
12
+ #
13
+ # ===Row Indexes:
14
+ # [0] Date
15
+ # [1] Description
16
+ # [2] Context
17
+ # [3] Cost
18
+ #
19
+ # The key <tt>expenses_filepath</tt> can be added to your settings file to change the location where this extension looks for the CSV.
20
+
21
+ class SimpleExpenses < LEWT::Extension
22
+
23
+ # Registers this extension
24
+ def initialize
25
+ options = {
26
+ :include_own => {
27
+ :definition => "toggles including own business expenses from csv file",
28
+ :default => false,
29
+ :short_flag => "-i"
30
+ }
31
+ }
32
+ super({:cmd => "expenses", :options => options })
33
+ end
34
+
35
+ # Extracts data from the expenses CSV file.
36
+ # options [Hash]:: The options hash passed to this function by the Lewt program.
37
+ def extract( options )
38
+ @targets = get_matched_customers( options[:target] )
39
+ @dStart = options[:start]
40
+ @dEnd = options[:end]
41
+ @category = 'Expenses'
42
+ @include_own_expenses = options[:include_own]
43
+ exFile = lewt_settings["expenses_filepath"]
44
+ return get_expenses ( exFile )
45
+ end
46
+
47
+
48
+ # Read file at filepath and parses it expecting the format presented in this classes header.
49
+ # filepath [String]:: The CSV filepath as a string.
50
+ def get_expenses ( filepath )
51
+ # ROWS:
52
+ # [0]Date [1]Description [2]Context [3]Cost
53
+ count = 0
54
+ data = LEWT::LEWTBook.new
55
+ CSV.foreach(filepath) do |row|
56
+ if count > 0
57
+ date = Time.parse(row[0])
58
+ desc = row[1]
59
+ context = row[2]
60
+ cost = row[3].to_f * -1
61
+ if self.is_target_date?( date ) == true && self.is_target_context?(context) == true
62
+ # create ledger entry and append to books
63
+ row_data = LEWT::LEWTLedger.new({
64
+ :date_start => date,
65
+ :date_end => date,
66
+ :category => @category,
67
+ :entity => context,
68
+ :description => desc,
69
+ :quantity => 1,
70
+ :unit_cost => cost
71
+ })
72
+ data.push(row_data)
73
+ end
74
+ end
75
+ # increment our row index counter
76
+ count += 1
77
+ end
78
+
79
+ # return our data as per specification!
80
+ return data
81
+ end
82
+
83
+ # Checks whether event date is within target range
84
+ # date [DateTime]:: The date to check
85
+ # returns: Boolean
86
+ def is_target_date? ( date )
87
+ d = date.to_date
88
+ check = false
89
+ if d >= @dStart.to_date && d <= @dEnd.to_date
90
+ check = true
91
+ end
92
+ return check
93
+ end
94
+
95
+ # Checks if the context field in the CSV matches any of our target clients names or alias'
96
+ # context [String]:: The context field as a string.
97
+ def is_target_context?(context)
98
+ match = false
99
+ @targets.each do |t|
100
+ reg = [ t['alias'], t['name'] ]
101
+ if @include_own_expenses == true
102
+ reg.concat [ @enterprise["alias"], @enterprise["name"] ]
103
+ end
104
+ regex = Regexp.new( reg.join("|"), Regexp::IGNORECASE )
105
+ match = regex.match(context) != nil ? true : false;
106
+ break if match != false
107
+ end
108
+ return match
109
+ end
110
+
111
+ end
112
+ end
@@ -0,0 +1,93 @@
1
+ require "yaml"
2
+ require "securerandom"
3
+
4
+ # Author:: Jason Wijegooneratne (mailto:code@jwije.com)
5
+ # Copyright:: Copyright (c) 2014 Jason Wijegooneratne
6
+ # License:: MIT. See LICENSE.md distributed with the source code for more information.
7
+
8
+ # The Billing LEWT Extension handles processing invoices from extracted data and returns them as a hash
9
+ # for rendering.
10
+
11
+ module LEWT
12
+
13
+ class SimpleInvoices < LEWT::Extension
14
+
15
+ attr_reader :data
16
+
17
+ # Sets up this extensions command
18
+ def initialize
19
+ super({:cmd => "invoice"})
20
+ end
21
+
22
+ # Processes the provided extract data into an invoice for the given targets.
23
+ # options [Hash]:: An hash containing run-time options passed to this extension by LEWT.
24
+ # data [LEWTBook]:: A hash-like object containing all the extracted data in the LEWTLedger format.
25
+ # returns [Array]: The invoice data as an array of hashes.
26
+ def process ( options, data )
27
+ matchData = get_matched_customers( options[:target] )
28
+ bills = Array.new
29
+ matchData.each do |client|
30
+ bills.push( generateBill( client, data) )
31
+ end
32
+ return bills
33
+ end
34
+
35
+ protected
36
+
37
+ # Generates a UID for this invoice based of the customer it is being sent to
38
+ def generate_id
39
+ if !lewt_settings.has_key?("invoice_id_counter")
40
+ self.write_settings("settings.yml", "invoice_id_counter", 0)
41
+ end
42
+ id = lewt_settings["invoice_id_counter"].to_i + 1
43
+ self.write_settings("settings.yml", "invoice_id_counter", id)
44
+ return "#{id.to_s}-" + SecureRandom.hex(4)
45
+ end
46
+
47
+ # Generates a bill for the given client.
48
+ # client [Hash]:: The client to calculate the invoice for.
49
+ # data [LEWTBook]:: The data preloaded into the LEWTBook format.
50
+ # returns [Hash]: The invoice data as a hash.
51
+ def generateBill(client, data)
52
+ bill = {
53
+ "date_created" => DateTime.now.strftime("%d/%m/%y"),
54
+ "id" => generate_id,
55
+ # "date_begin"=> @events.dateBegin.strftime("%d/%m/%y"),
56
+ # "date_end"=> @events.dateEnd.strftime("%d/%m/%y"),
57
+ "billed_to" => client,
58
+ "billed_from" => enterprise,
59
+ "items" => [
60
+ # eg: { description, duration, rate, total
61
+ ],
62
+ "sub-total" => 0,
63
+ "tax" => nil,
64
+ "total" => nil
65
+ }
66
+ # loop events and filter for requested entity (client)
67
+ data.each do |row|
68
+ if row[:entity] == client["name"]
69
+ item = {
70
+ "description" => row[:description],
71
+ "duration" => row[:quantity],
72
+ "rate" => row[:unit_cost],
73
+ "total" => row[:total] < 0 ? row[:total] * -1 : row[:total],
74
+ "start" => row[:date_start].strftime("%d/%m/%y %l:%M%P"),
75
+ "end" => row[:date_end].strftime("%d/%m/%y %l:%M%P")
76
+ }
77
+ bill["items"].push( item );
78
+ bill["sub-total"] += item["total"]
79
+ end
80
+ end
81
+ bill["sub-total"] = bill["sub-total"].round(2)
82
+ bill["tax"] = (bill["sub-total"] * enterprise["invoice-tax"]).round(2)
83
+ bill["total"] = (bill["sub-total"] + bill["tax"]).round(2)
84
+
85
+ return bill;
86
+ end
87
+
88
+ end
89
+
90
+ end
91
+
92
+
93
+
@@ -0,0 +1,102 @@
1
+ require "csv"
2
+
3
+ # Author:: Jason Wijegooneratne (mailto:code@jwije.com)
4
+ # Copyright:: Copyright (c) 2014 Jason Wijegooneratne
5
+ # License:: MIT. See LICENSE.md distributed with the source code for more information.
6
+
7
+
8
+ module LEWT
9
+
10
+ # Extracts milestone payment data from a CSV file.
11
+ #
12
+ # ===Row Indexes:
13
+ # [0] Id
14
+ # [1] Date
15
+ # [2] Description
16
+ # [3] Context
17
+ # [4] Amount
18
+ #
19
+ # The key <tt>milstones_filepath</tt> can be added to your settings file to change the location where this extension looks for the CSV.
20
+
21
+ class SimpleMilestones < LEWT::Extension
22
+
23
+ # Sets up this extension and regsters its run-time options.
24
+ def initialize
25
+ @category = "Milestone Income"
26
+ super({:cmd => "milestones"})
27
+ end
28
+
29
+ # Extracts data from the milestones CSV file.
30
+ # options [Hash]:: The options hash passed to this function by the Lewt program.
31
+ def extract( options )
32
+ matchData = get_matched_customers( options[:target] )
33
+ @dStart = options[:start].to_date
34
+ @dEnd = options[:end].to_date
35
+ @targets = self.get_matched_customers(options[:target])
36
+ exFile = lewt_settings["milestones_filepath"]
37
+ return get_milestones ( exFile )
38
+ end
39
+
40
+ # Read file at filepath and parses it expecting the format presented in this classes header.
41
+ # filepath [String]:: The CSV filepath as a string.
42
+ def get_milestones ( filepath )
43
+ # ROWS:
44
+ # [0]Id [1]Date [2]Description [3]Context [4]Amount
45
+ count = 0
46
+ data = LEWT::LEWTBook.new
47
+
48
+ CSV.foreach(filepath) do |row|
49
+ if count > 0
50
+ id = row[0]
51
+ date = Time.parse( row[1] )
52
+ desc = row[2]
53
+ context = row[3]
54
+ amount = row[4].to_f
55
+
56
+ if self.is_target_date?( date ) == true && self.is_target_context?(context) == true
57
+ # create ledger entry and append to books
58
+ row_data = LEWT::LEWTLedger.new({
59
+ :date_start => date,
60
+ :date_end => date,
61
+ :category => @category,
62
+ :entity => context,
63
+ :description => desc,
64
+ :quantity => 1,
65
+ :unit_cost => amount
66
+ })
67
+ data.push(row_data)
68
+ end
69
+ end
70
+ # increment our row index counter
71
+ count += 1
72
+ end
73
+ return data
74
+ end
75
+
76
+ # Checks if the context field in the CSV matches any of our target clients names or alias'
77
+ # context [String]:: The context field as a string.
78
+ def is_target_context?(context)
79
+ match = false
80
+ @targets.each do |t|
81
+ reg = [ t['alias'], t['name'] ]
82
+ regex = Regexp.new( reg.join("|"), Regexp::IGNORECASE )
83
+ match = regex.match(context) != nil ? true : false;
84
+ break if match != false
85
+ end
86
+ return match
87
+ end
88
+
89
+ # Checks whether event date is within target range
90
+ # date [DateTime]:: The date to check
91
+ # returns: Boolean
92
+ def is_target_date?(date)
93
+ d = date.to_date
94
+ check = false
95
+ if d >= @dStart && d <= @dEnd
96
+ check = true
97
+ end
98
+ return check
99
+ end
100
+
101
+ end
102
+ end
@@ -0,0 +1,81 @@
1
+ # Author:: Jason Wijegooneratne (mailto:code@jwije.com)
2
+ # Copyright:: Copyright (c) 2014 Jason Wijegooneratne
3
+ # License:: MIT. See LICENSE.md distributed with the source code for more information.
4
+
5
+ module LEWT
6
+
7
+ # The Reports LEWT Extension processes ledger data into a brief report.
8
+
9
+ class SimpleReports < LEWT::Extension
10
+
11
+ # Registers this extension.
12
+ def initialize
13
+ super({:cmd => "report"})
14
+ end
15
+
16
+ # Called on Lewt process cycle and uses the ledger data to compose a report.
17
+ # options [Hash]:: The options hash passed to this function by the Lewt program.
18
+ # data [LEWTBook]:: The data in LEWTBook format
19
+ def process ( options, data )
20
+ targets = get_matched_customers( options[:target] )
21
+ return make_report(targets, data)
22
+ end
23
+
24
+ # This method handles the bulk of the calculation required in compiling the report.
25
+ # targets [Hash]:: The target client(s) to operate on
26
+ # data [LEWTBook]:: The data in LEWTBook format
27
+ def make_report ( targets, data )
28
+ report = {
29
+ "date_created" => DateTime.now.strftime("%d/%m/%y"),
30
+ "included_customers" => targets,
31
+ "revenue" => 0,
32
+ "expenses" => 0,
33
+ "income" => 0,
34
+ "taxes" => Array.new,
35
+ "hours" => 0
36
+ }
37
+ data.each do |row|
38
+ if row[:category].downcase.match /income/
39
+ report["revenue"] += row[:total]
40
+ # check if category Hourly Income. If so add quantity to our 'hours' counter.
41
+ if row[:category].downcase.match /hourly/
42
+ report["hours"] += row[:quantity]
43
+ end
44
+ elsif row[:category].downcase.match /expense/
45
+ report["expenses"] += row[:total]
46
+ end
47
+ end
48
+
49
+ # remember expenses is a negative amount to begin with so don't subtract it!
50
+ report["income"] = report["revenue"] + report["expenses"]
51
+ tax_levees = enterprise["tax_levees"]
52
+ tax_total = 0
53
+
54
+ if tax_levees != nil
55
+ tax_levees.each do |tax|
56
+ if tax["applies_to"] == "income"
57
+ # do income tax
58
+ if report["income"] > tax["lower_threshold"]
59
+ taxable = [ report["income"], tax["upper_threshold"]].min - tax["lower_threshold"]
60
+ damage = (taxable * tax["rate"] + ( tax["flatrate"] || 0 )).round(2)
61
+ tax_total += damage
62
+ report["taxes"].push({ "amount" => damage, "name" => tax["name"], "rate" => tax["rate"] })
63
+ end
64
+ elsif tax["applies_to"] == "revenue"
65
+ # do GST's
66
+ damage = report["revenue"] * tax["rate"] + ( tax["flatrate"] || 0 )
67
+ tax_total += damage
68
+ report["taxes"].push({ "amount" => damage, "name" => tax["name"], "rate" => tax["rate"] })
69
+ end
70
+ end
71
+ end
72
+ report["bottom_line"] = (report["income"] - tax_total).round(2)
73
+ return report
74
+ end
75
+
76
+ def calculate_income_tax (income, tax)
77
+
78
+ end
79
+
80
+ end
81
+ end
@@ -0,0 +1,81 @@
1
+ require "yaml"
2
+
3
+ # Author:: Jason Wijegooneratne (mailto:code@jwije.com)
4
+ # Copyright:: Copyright (c) 2014 Jason Wijegooneratne
5
+ # License:: MIT. See LICENSE.md distributed with the source code for more information.
6
+
7
+
8
+ module LEWT
9
+
10
+ # The Store LEWT Extension handles persisting data across sessions. It hooks into the 'process' operation to
11
+ # obtain the raw extract data, likewise it hooks into 'render' to get the process data. Whilst in 'render' it
12
+ # also handles writing the data to the file-system, so to save data you must invoke it with the --render flag!
13
+ # The data is saved to some configurable paths (see source code).
14
+ #
15
+ # Furthermore store can also re-extract previously persisted data from the file system for re-use! This is handy
16
+ # if you need to re-generate some output (ie: an invoice that required a quick edit), or if you would like to use
17
+ # store data built up overtime for bulk operations such as analytics & reporting.
18
+
19
+ class Store < LEWT::Extension
20
+
21
+ # Sets up this extensions command name and run-time options.
22
+ def initialize
23
+ options = {
24
+ :store_filename => {
25
+ :definition => "File name to save as",
26
+ :type => String
27
+ },
28
+ :store_hook => {
29
+ :definition => "Tell store whether to save either the 'extract' or 'process' data",
30
+ :default => "process",
31
+ :type => String
32
+ }
33
+ }
34
+ super({:cmd => "store", :options => options})
35
+ end
36
+
37
+ # This method is not yet implemented. This method (should) extracts previously stored data for reuse.
38
+ # options [Hash]:: A hash that is passed to this extension by the main LEWT program containing ru-time options.
39
+ def extract( options )
40
+
41
+ end
42
+
43
+ # Captures the extract data and converts it to a YML string storing it as a property on this object.
44
+ # Returns an empty array so as not to interupt the process loop.
45
+ # options [Hash]:: A hash that is passed to this extension by the main LEWT program containing ru-time options.
46
+ # data [Hash]:: The extracted data as a hash.
47
+ def process( options, data )
48
+ @extractData = data.to_yaml
49
+ return []
50
+ end
51
+
52
+ # Captures proess data and converts it to a YML string. This method also handles the
53
+ # actual writing of data to the file system. The options 'store_hook' toggles exract or
54
+ # process targeting.
55
+ # options [Hash]:: A hash that is passed to this extension by the main LEWT program containing ru-time options.
56
+ # data [Array]:: The processed data as an array of hashes.
57
+ def render( options, data )
58
+ @processData = data.to_yaml
59
+ name = options[:store_filename]
60
+ yml = options[:store_hook] == "extract" ? @extractData : @processData
61
+ name != nil ? store(yml, name ) : [yml]
62
+ end
63
+
64
+ protected
65
+
66
+ # Writes the given YAML string to a file at path/name.yml, this method will overwrite the
67
+ # file if it already exists.
68
+ # yml [String]:: A YAML string of data.
69
+ # path [String]:: The path to store too.
70
+ # name [String]:: The name of the file to save as.
71
+ def store ( yml, name )
72
+ storefile = File.new( name, "w")
73
+ storefile.puts(yml)
74
+ storefile.close
75
+ return [yml]
76
+ end
77
+
78
+ end
79
+
80
+
81
+ end