working_times 0.3.2 → 0.7.2

Sign up to get free protection for your applications and to get access to all the features.
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