working_times 0.3.2 → 0.7.2

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d42c1a4ebd7f1f1f281f19acb32a48e1380b98bd3aa3cc2bb1bcd5d56debd527
4
- data.tar.gz: a11317affbb17526880eabedd2d03f696e65e6d9c93d7574c5c623ec40843c6a
3
+ metadata.gz: 7141b461d0bf7215ca3656d259de497ec7062a3639a0945a1fa8dea9deea27ce
4
+ data.tar.gz: ce7ffcd9a86aa7395d15ccab63a44408ddbf5e00aa1ce489bdb0266c2d2170d9
5
5
  SHA512:
6
- metadata.gz: 9fa455a36013da66a73c91bfbabd64a1c3f3fa5b7aef9a7dc52dd782e133ebad74d17a19ed28e86dc6e6f3e4d701f2c87652029ac4ba9c41812d88a683a50813
7
- data.tar.gz: cc4944b5c7c4a957bf5be1567f393902bc848032e08c3dc281f78c0c28333ec87c2d278fa6874e57258ceba687e87a31413fc71279aea2e726a44d1809b38e5e
6
+ metadata.gz: 94cfce8f51ded451931e65fbab2983b0a51a769522c7e762625e739e44484e5f0224ffba32bc9c357f26fc0c28b1d5e5dc641cb452695766198e3f83d239689e
7
+ data.tar.gz: 50d4bab6fa14e2c69eeec085e373694384cd94a72f0647b8212581820b0e019447ffd2603de052de3b13b865a57299731ebb08558dbe46ac1d374fe251a0ae69
@@ -2,4 +2,5 @@ require 'working_times/constants'
2
2
  require 'working_times/config'
3
3
  require 'working_times/state'
4
4
  require 'working_times/record'
5
+ require 'working_times/invoice'
5
6
  require 'working_times/cli'
@@ -1,59 +1,100 @@
1
- # frozen_string_literal: true
2
-
3
1
  require 'thor'
2
+ require 'fileutils'
4
3
 
5
4
  module WorkingTimes
6
5
  class CLI < Thor
7
6
  include State
8
7
 
9
- option :work_on, aliases: ['-w'], desc: 'Specify what group of work on'
10
- desc 'start [COMMENT] <option>', 'Start working with comment.'
11
- def start(comment = nil)
12
- initialize_data_dir
8
+ desc 'init WORKON [TERM] [COMPANY]', 'initialize data directory for your working'
9
+ def init(workon, term = 'default', company = '')
10
+ if Dir.exist?(workon)
11
+ puts "WORKON '#{workon}' is already created. Or name conflicted.'"
12
+ return
13
+ end
14
+
15
+ FileUtils.mkdir_p(File.join(workon, 'terms'))
16
+ FileUtils.mkdir_p(File.join(workon, 'invoices'))
17
+ initialize_wtconf(workon, term, company)
18
+ end
19
+
20
+ desc 'start [COMMENT] ', 'Start working with comment.'
21
+ def start(comment = '')
13
22
  if working?
14
23
  puts "You are already on working at #{current_work}."
15
24
  puts "To finish this, execute 'wt finish'."
16
25
  return
17
26
  end
18
27
 
19
- Record.new(timestamp: Time.now, comment: comment, work_on: options[:work_on]).start
20
- start_work(options[:work_on])
28
+ initialize_term_log
29
+
30
+ Record.new(timestamp: DateTime.now, comment: comment).start
31
+ start_work
21
32
  end
22
33
 
23
- desc 'st [COMMENT] <option>', 'Short hand for *start*'
34
+ desc 'st [COMMENT]', 'Short hand for *start*'
24
35
  alias st start
25
36
 
26
37
  desc 'finish [COMMENT]', 'Finish working on current group.'
27
- def finish(comment = nil)
38
+ def finish(comment = '')
28
39
  unless working?
29
40
  puts 'You are not starting work. Execute "wt start" to start working.'
30
41
  return
31
42
  end
32
43
 
33
- Record.new(timestamp: Time.now, comment: comment).finish
44
+ Record.new(timestamp: DateTime.now, comment: comment).finish
34
45
  finish_work
35
46
  end
36
47
 
37
48
  desc 'fi [COMMENT]', 'Short hand for *finish*'
38
49
  alias fi finish
39
50
 
40
- desc 'rest DURATION', 'Record resting time. e.g. \'wt rest 1h30m\''
41
- def rest(duration = nil)
42
- if duration.nil?
43
- puts <<~MSG
44
- Please specify duration of resting.
45
- e.g. wt rest 1h30m
46
- e.g. wt rest '1 hour 30 minutes'
47
- MSG
48
- return
49
- end
50
-
51
+ desc 'rest DURATION', 'Record resting time. e.g. \'wt rest 1h30m\'\'wt rest 1 hour 30 minutes\''
52
+ def rest(duration)
51
53
  unless working?
52
54
  puts 'You are not starting work. Execute "wt start" to start working.'
53
55
  return
54
56
  end
55
57
 
56
- Record.new(timestamp: Time.now, duration: duration).rest
58
+ Record.new(timestamp: DateTime.now, duration: duration).rest
59
+ end
60
+
61
+ option :build, type: :boolean, aliases: ['-b']
62
+ desc 'invoice', 'Create invoice for current term by TeX template. It will build pdf if option is set'
63
+ def invoice
64
+ Invoice.new.tap do |invoice|
65
+ invoice.generate
66
+ invoice.build if options[:build]
67
+ end
68
+ puts "Invoice created to #{path_invoice_current_term}."
69
+ end
70
+
71
+ desc 'version', 'Show version of working_times'
72
+ def version
73
+ puts 'version: ' + VERSION
74
+ end
75
+
76
+ private
77
+
78
+ def initialize_wtconf(workon, term, company)
79
+ # on initializing, we shouldn't use path helper e.g. Config#data_dir
80
+ data_dir = File.expand_path(workon)
81
+ File.write(File.join(data_dir, 'wtconf.json'), <<~WTCONF)
82
+ {
83
+ "term": "#{term}",
84
+ "invoice": {
85
+ "company": "#{company}",
86
+ "template": "",
87
+ "salaryPerHour": 0,
88
+ "taxRate": 0.0
89
+ }
90
+ }
91
+ WTCONF
92
+ end
93
+
94
+ def initialize_term_log
95
+ return if File.exist?(path_current_term)
96
+
97
+ File.write(path_current_term, SCHEMA.join(',') + "\n")
57
98
  end
58
99
  end
59
100
  end
@@ -1,45 +1,55 @@
1
- # frozen_string_literal: true
1
+ require 'json'
2
2
 
3
3
  module WorkingTimes
4
4
  module Config
5
5
  private
6
6
 
7
- # return where data directory is
8
7
  def data_dir
9
- File.expand_path(wtconf['DATADIR'])
8
+ File.expand_path('.')
10
9
  end
11
10
 
12
- # return default working project/task/etc...
13
- def default_work
14
- wtconf['DEFAULTWORK']
11
+ def path_wtconf
12
+ File.join(data_dir, 'wtconf.json')
13
+ end
14
+
15
+ def term_dir
16
+ File.join(data_dir, 'terms')
17
+ end
18
+
19
+ def path_working_flag
20
+ File.join(data_dir, '.working')
21
+ end
22
+
23
+ def current_term
24
+ wtconf['term']
25
+ end
26
+
27
+ def path_current_term
28
+ File.join(term_dir, current_term)
29
+ end
30
+
31
+ def current_company
32
+ wtconf['company']
33
+ end
34
+
35
+ def invoice_dir
36
+ File.join(data_dir, 'invoices')
37
+ end
38
+
39
+ def invoice_info
40
+ wtconf['invoice']
41
+ end
42
+
43
+ def dir_invoice_current_term
44
+ File.join(invoice_dir, current_term)
45
+ end
46
+
47
+ def path_invoice_current_term
48
+ File.join(dir_invoice_current_term, "#{current_term}.tex")
15
49
  end
16
50
 
17
- # parse ~/.wtconf
18
51
  def wtconf
19
- conf = default_conf
20
- begin
21
- File
22
- .readlines(File.expand_path('~/.wtconf'))
23
- .map(&:chomp)
24
- .each do |row|
25
- k, v = row.split('=')
26
- conf[k] = v
27
- end
28
- rescue Errno::ENOENT
29
- puts '~/.wtconf not found, generated.'
30
- generate_wtconf
31
- end
32
- conf
33
- end
34
-
35
- # default configurations of .wtconf
36
- def default_conf
37
- { 'DATADIR' => File.expand_path('~/.wt'), 'DEFAULTWORK' => 'default' }
38
- end
39
-
40
- # generate configuration file to ~/.wtconf when does not exist
41
- def generate_wtconf
42
- File.open(File.expand_path('~/.wtconf'), 'w') { |f| f.puts(default_conf.map { |k, v| "#{k}=#{v}" }) }
52
+ JSON.parse(File.read(path_wtconf))
43
53
  end
44
54
  end
45
55
  end
@@ -1,7 +1,5 @@
1
- # frozen_string_literal: true
2
-
3
1
  module WorkingTimes
4
- VERSION = '0.3.2'
2
+ VERSION = '0.7.2'.freeze
5
3
 
6
4
  START_MSG = [
7
5
  'Have a nice work!',
@@ -12,4 +10,11 @@ module WorkingTimes
12
10
  'Great job!',
13
11
  'Time to beer!'
14
12
  ].freeze
13
+
14
+ SCHEMA = [
15
+ 'started_at', # DateTime#rfc3339
16
+ 'finished_at', # DateTime#rfc3339
17
+ 'rest_sec', # Integer (inidicates second)
18
+ 'comment' # String
19
+ ].freeze
15
20
  end
@@ -0,0 +1,135 @@
1
+ module WorkingTimes
2
+ # Class about creating invoice
3
+ # This class is designed to build data_dir/invoices/current_term/invoice.tex
4
+ # from data_dir/invoices/template.tex
5
+ class Invoice
6
+ include Config
7
+ attr_reader :path_template, :salary_per_hour, :tax_rate, :company
8
+
9
+ WDAYS = %w[日 月 火 水 木 金 土].freeze
10
+ Date::DATE_FORMATS[:jp_date] = '%m月%d日'
11
+ Time::DATE_FORMATS[:only_hm] = '%H:%M'
12
+
13
+ # path_template : String
14
+ # salary_per_hour : Integer
15
+ # tax_rate : Float
16
+ # company : String
17
+ def initialize
18
+ h_invoice_info = invoice_info
19
+
20
+ @path_template = h_invoice_info['template']
21
+ @salary_per_hour = h_invoice_info['salaryPerHour']
22
+ @tax_rate = h_invoice_info['taxRate']
23
+ @company = h_invoice_info['company']
24
+ end
25
+
26
+ def generate
27
+ create_dir_invoice_current_term
28
+ makeup_worktable
29
+ generate_invoice_from_template
30
+ end
31
+
32
+ def build
33
+ puts 'Currently, it is not available to build pdf with latexmk.'
34
+ puts 'Wait for new version!'
35
+ end
36
+
37
+ private
38
+
39
+ def create_dir_invoice_current_term
40
+ FileUtils.mkdir_p(dir_invoice_current_term)
41
+ end
42
+
43
+ def makeup_worktable
44
+ @worktable = [
45
+ '\hline',
46
+ '日付 & 曜日 & 内容 & 出勤 & 退勤 & 休憩 & 労働時間 \\\\ \hline\hline'
47
+ ]
48
+ @allworktime_on_sec = 0.0
49
+ working_times = CSV.open(path_current_term, headers: true)
50
+ row = working_times.first
51
+ date_itr = beginning_of_month(row['started_at'])
52
+ date_itr.upto(date_itr.end_of_month) do |date|
53
+ if row.nil?
54
+ @worktable << format_worktable_row(date)
55
+ elsif same_day?(row['started_at'], date.rfc3339)
56
+ worktime = calculate_worktime(row['started_at'], row['finished_at'], row['rest_sec'])
57
+ @allworktime_on_sec += worktime
58
+ @worktable <<
59
+ format_worktable_row(date, row['comment'], row['started_at'], row['finished_at'], row['rest_sec'], worktime)
60
+ row = working_times.readline
61
+ else
62
+ @worktable << format_worktable_row(date)
63
+ end
64
+ end
65
+ end
66
+
67
+ def generate_invoice_from_template
68
+ template = File.readlines(path_template)
69
+ File.open(path_invoice_current_term, 'w') do |f|
70
+ template.each do |t|
71
+ t.gsub!(/##COMPANY##/, company)
72
+ t.gsub!(/##WORKTIME##/, parse_second_to_hh_str(@allworktime_on_sec))
73
+ t.gsub!(/##ACTUALWORKTIME##/, parse_second_to_hm_str(@allworktime_on_sec))
74
+ t.gsub!(/##SALARYPERHOUR##/, salary_per_hour.to_s)
75
+ t.gsub!(/##SALARY##/, salary.to_s)
76
+ t.gsub!(/##TAXRATE##/, "#{(tax_rate * 100).to_i}\\%")
77
+ t.gsub!(/##TAX##/, tax.to_s)
78
+ t.gsub!(/##SALARYWITHTAX##/, (salary + tax).to_s)
79
+ # on gsub, '\' means 'replace sub string'.
80
+ # so we should use block if work as expected
81
+ t.gsub!(/##WORKTABLE##/) { @worktable.join("\n") }
82
+
83
+ f.write(t)
84
+ end
85
+ end
86
+ end
87
+
88
+ def beginning_of_month(rfc3339)
89
+ Date.parse(rfc3339).beginning_of_month
90
+ end
91
+
92
+ def same_day?(one_rfc3339, another_rfc3339)
93
+ Date.parse(one_rfc3339) == Date.parse(another_rfc3339)
94
+ end
95
+
96
+ def calculate_worktime(st_rfc3339, fi_rfc3339, rest_sec)
97
+ Time.parse(fi_rfc3339) - Time.parse(st_rfc3339) - rest_sec.to_i
98
+ end
99
+
100
+ # date : Date
101
+ # comment : String
102
+ # started_at, finished_at : String (RFC3339)
103
+ # rest, worktime : Integer
104
+ def format_worktable_row(date, comment = nil, started_at = nil, finished_at = nil, rest = nil, worktime = nil)
105
+ "#{date.to_s(:jp_date)} & " \
106
+ "#{WDAYS[date.wday]} & " \
107
+ "#{comment || '-'} & " \
108
+ "#{started_at.nil? ? '-' : Time.parse(started_at).to_s(:only_hm)} & " \
109
+ "#{finished_at.nil? ? '-' : Time.parse(finished_at).to_s(:only_hm)} & " \
110
+ "#{rest.nil? ? '-' : parse_second_to_hm_str(rest.to_i)} & " \
111
+ "#{worktime.nil? ? '-' : parse_second_to_hm_str(worktime)} \\\\ \\hline"
112
+ end
113
+
114
+ # second : Float
115
+ def parse_second_to_hm_str(second)
116
+ second = second.to_i
117
+ h = second / 3600
118
+ m = (second - 3600 * h) / 60
119
+ "#{h}:#{m.to_s.rjust(2, '0')}"
120
+ end
121
+
122
+ def parse_second_to_hh_str(second)
123
+ (second / 3600).to_i.to_s
124
+ end
125
+
126
+ # calculate with 'hour' only
127
+ def salary
128
+ @salary ||= @allworktime_on_sec.to_i / 3600 * salary_per_hour
129
+ end
130
+
131
+ def tax
132
+ @tax ||= (salary * tax_rate).to_i
133
+ end
134
+ end
135
+ end
@@ -1,57 +1,62 @@
1
- # frozen_string_literal: true
2
-
3
1
  require 'active_support/time'
2
+ require 'csv'
4
3
 
5
4
  module WorkingTimes
6
5
  class Record
7
6
  include State
8
7
 
9
- attr_reader :timestamp, :comment, :duration, :work_on
8
+ OPTIONS = { headers: true, return_headers: true, write_headers: true }.freeze
9
+
10
+ attr_reader :timestamp, :comment, :duration
10
11
 
11
- def initialize(timestamp:, comment: nil, duration: nil, work_on: nil)
12
+ def initialize(timestamp:, comment: nil, duration: nil)
12
13
  @timestamp = timestamp
13
14
  @comment = comment
14
15
  @duration = duration
15
- @work_on = work_on.nil? ? default_work : work_on
16
16
  end
17
17
 
18
18
  def start
19
- File.open("#{data_dir}/#{work_on}", 'a+') do |f|
20
- f.puts "#{timestamp.rfc3339},,#{comment},start"
19
+ CSV.open(path_current_term, 'a+', OPTIONS) do |csv|
20
+ csv.puts([timestamp.rfc3339, '', 0, comment])
21
21
  end
22
22
  end
23
23
 
24
24
  def finish
25
- File.open("#{data_dir}/#{current_work}", 'a+') do |f|
26
- f.puts ",#{timestamp.rfc3339},#{comment},finish"
25
+ updated_csv = ''
26
+ CSV.filter(File.open(path_current_term), updated_csv, OPTIONS) do |row|
27
+ next if row.header_row?
28
+ next unless row['finished_at'].empty?
29
+
30
+ row['finished_at'] = timestamp.rfc3339
31
+ row['comment'] = comment
27
32
  end
33
+ File.write(path_current_term, updated_csv)
28
34
  end
29
35
 
30
36
  def rest
31
- parse_rest_finished_at
32
- File.open("#{data_dir}/#{current_work}", 'a+') do |f|
33
- f.puts "#{timestamp.rfc3339},#{@finished_at.rfc3339},#{comment},rest"
34
- end
37
+ parse_rest_sec
35
38
 
36
- show_rest_msg
39
+ updated_csv = ''
40
+ CSV.filter(File.open(path_current_term), updated_csv, OPTIONS) do |row|
41
+ next if row.header_row?
42
+ next unless row['finished_at'].empty?
43
+
44
+ row['rest_sec'] = @rest_sec
45
+ end
46
+ File.write(path_current_term, updated_csv)
37
47
  end
38
48
 
39
49
  private
40
50
 
41
- def parse_rest_finished_at
42
- @finished_at = timestamp
51
+ def parse_rest_sec
52
+ @rest_sec = 0
43
53
  if /(?<hour>\d+)\s*h/ =~ duration
44
- @finished_at += hour.to_i.hour
54
+ @rest_sec += hour.to_i * 3600
45
55
  end
46
56
 
47
57
  if /(?<minute>\d+)\s*m/ =~ duration
48
- @finished_at += minute.to_i.minute
58
+ @rest_sec += minute.to_i * 60
49
59
  end
50
60
  end
51
-
52
- def show_rest_msg
53
- Time::DATE_FORMATS[:rest_finished_at] = '%H:%M:%S'
54
- puts "You can rest until #{@finished_at.to_s(:rest_finished_at)}."
55
- end
56
61
  end
57
62
  end
@@ -1,46 +1,37 @@
1
- # frozen_string_literal: true
2
-
3
1
  module WorkingTimes
4
2
  module State
5
3
  private
6
4
 
7
5
  include Config
8
6
 
9
- # generate data directory when does not exist
10
- # it is usually called by 'start' command
11
- def initialize_data_dir
12
- return if exist_data_dir?
13
-
14
- puts 'data directory .wt not found, generated.'
15
- Dir.mkdir(data_dir)
16
- end
17
-
18
- def exist_data_dir?
19
- File.exist?(data_dir)
20
- end
21
-
22
7
  def working?
23
- File.exist?("#{data_dir}/.working")
8
+ File.exist?(path_working_flag)
24
9
  end
25
10
 
26
11
  # return what kind of working on
27
12
  def current_work
28
- File.readlines("#{data_dir}/.working").last.chomp
13
+ File.readlines(path_working_flag).last.chomp
29
14
  end
30
15
 
31
- # create ~/.wt/.working include what you working on
32
- # and show 'started' message
33
- def start_work(work_on)
34
- work_on = work_on.nil? ? default_work : work_on
35
- File.open("#{data_dir}/.working", 'w+') { |f| f.puts work_on }
16
+ def start_work
17
+ File.write(path_working_flag, current_term)
36
18
  puts START_MSG.sample
37
19
  end
38
20
 
39
- # delete 'working' flag
40
- # and show 'finished' message
41
21
  def finish_work
42
- File.delete("#{data_dir}/.working")
22
+ puts "You were working about #{worked_time}."
23
+ File.delete(path_working_flag)
43
24
  puts FINISH_MSG.sample
44
25
  end
26
+
27
+ def worked_time
28
+ last_record = CSV.read(path_current_term).last
29
+ started_at = Time.parse(last_record[0])
30
+ finished_at = Time.parse(last_record[1])
31
+ duration = (finished_at - started_at).to_i
32
+ hour = duration / 3600
33
+ min = (duration - 3600 * hour) / 60
34
+ "#{hour.to_s.rjust(2, '0')} hour #{min.to_s.rjust(2, '0')} min"
35
+ end
45
36
  end
46
37
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: working_times
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.2
4
+ version: 0.7.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Aoshi Fujioka
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2019-10-08 00:00:00.000000000 Z
11
+ date: 2020-06-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '5'
19
+ version: 5.2.4.3
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '5'
26
+ version: 5.2.4.3
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: thor
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -44,28 +44,28 @@ dependencies:
44
44
  requirements:
45
45
  - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: '1.17'
47
+ version: '2.1'
48
48
  type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: '1.17'
54
+ version: '2.1'
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: rake
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
- - - "~>"
59
+ - - ">="
60
60
  - !ruby/object:Gem::Version
61
- version: '10.0'
61
+ version: 12.3.3
62
62
  type: :development
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
- - - "~>"
66
+ - - ">="
67
67
  - !ruby/object:Gem::Version
68
- version: '10.0'
68
+ version: 12.3.3
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: rspec
71
71
  requirement: !ruby/object:Gem::Requirement
@@ -94,6 +94,34 @@ dependencies:
94
94
  - - ">="
95
95
  - !ruby/object:Gem::Version
96
96
  version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: pry-byebug
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: rubocop
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
97
125
  description: Store your working/worked time simply. This gem gives simple CLI tool.
98
126
  email:
99
127
  - blue20will@gmail.com
@@ -107,6 +135,7 @@ files:
107
135
  - lib/working_times/cli.rb
108
136
  - lib/working_times/config.rb
109
137
  - lib/working_times/constants.rb
138
+ - lib/working_times/invoice.rb
110
139
  - lib/working_times/record.rb
111
140
  - lib/working_times/state.rb
112
141
  homepage: https://github.com/arsley/working_times