clevic 0.6.0 → 0.7.0
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.
- data/History.txt +182 -0
- data/Manifest.txt +7 -3
- data/README.txt +36 -23
- data/Rakefile +22 -7
- data/TODO +55 -45
- data/bin/clevic +7 -2
- data/config/hoe.rb +4 -2
- data/lib/clevic.rb +1 -0
- data/lib/clevic/browser.rb +38 -15
- data/lib/clevic/cache_table.rb +24 -8
- data/lib/clevic/db_options.rb +11 -8
- data/lib/clevic/delegates.rb +24 -13
- data/lib/clevic/extensions.rb +6 -2
- data/lib/clevic/field.rb +55 -6
- data/lib/clevic/item_delegate.rb +7 -6
- data/lib/clevic/model_builder.rb +35 -8
- data/lib/clevic/record.rb +28 -0
- data/lib/clevic/table_model.rb +21 -33
- data/lib/clevic/table_view.rb +48 -1
- data/lib/clevic/ui/browser.ui +4 -4
- data/lib/clevic/ui/browser_ui.rb +73 -74
- data/lib/clevic/ui/search_dialog_ui.rb +50 -51
- data/lib/clevic/version.rb +1 -1
- data/{accounts_models.rb → models/accounts_models.rb} +12 -12
- data/models/minimal_models.rb +19 -0
- data/models/times_models.rb +156 -0
- data/{times_models.rb → models/times_sqlite_models.rb} +10 -18
- data/{values_models.rb → models/values_models.rb} +2 -3
- data/script/console +1 -1
- data/sql/accounts.sql +48 -48
- data/sql/times.sql +25 -25
- data/sql/times_sqlite.sql +46 -0
- data/website/index.html +44 -18
- data/website/index.txt +8 -1
- data/website/template.html.erb +2 -1
- metadata +34 -8
@@ -1,8 +1,8 @@
|
|
1
1
|
=begin
|
2
2
|
** Form generated from reading ui file 'search_dialog.ui'
|
3
3
|
**
|
4
|
-
** Created:
|
5
|
-
** by: Qt User Interface Compiler version 4.3.
|
4
|
+
** Created: Wed Jul 23 11:45:29 2008
|
5
|
+
** by: Qt User Interface Compiler version 4.3.3
|
6
6
|
**
|
7
7
|
** WARNING! All changes made in this file will be lost when recompiling ui file!
|
8
8
|
=end
|
@@ -20,58 +20,57 @@ class Ui_SearchDialog
|
|
20
20
|
attr_reader :backwards
|
21
21
|
|
22
22
|
def setupUi(searchDialog)
|
23
|
-
searchDialog.
|
24
|
-
|
25
|
-
|
23
|
+
if searchDialog.objectName.nil?
|
24
|
+
searchDialog.objectName = "searchDialog"
|
25
|
+
end
|
26
|
+
searchDialog.windowModality = Qt::WindowModal
|
27
|
+
searchDialog.resize(307, 400)
|
28
|
+
icon = Qt::Icon.new("../../hilfer/bin/hilfer-icon.png")
|
29
|
+
searchDialog.windowIcon = icon
|
26
30
|
@button_box = Qt::DialogButtonBox.new(searchDialog)
|
27
|
-
@button_box.
|
28
|
-
@button_box.
|
29
|
-
@button_box.
|
30
|
-
@button_box.
|
31
|
+
@button_box.objectName = "button_box"
|
32
|
+
@button_box.geometry = Qt::Rect.new(30, 240, 261, 32)
|
33
|
+
@button_box.orientation = Qt::Horizontal
|
34
|
+
@button_box.standardButtons = Qt::DialogButtonBox::Cancel|Qt::DialogButtonBox::NoButton|Qt::DialogButtonBox::Ok
|
31
35
|
@selected_rows = Qt::CheckBox.new(searchDialog)
|
32
|
-
@selected_rows.
|
33
|
-
@selected_rows.
|
34
|
-
@selected_rows.
|
36
|
+
@selected_rows.objectName = "selected_rows"
|
37
|
+
@selected_rows.enabled = false
|
38
|
+
@selected_rows.geometry = Qt::Rect.new(30, 280, 191, 21)
|
35
39
|
@selected_columns = Qt::CheckBox.new(searchDialog)
|
36
|
-
@selected_columns.
|
37
|
-
@selected_columns.
|
38
|
-
@selected_columns.
|
40
|
+
@selected_columns.objectName = "selected_columns"
|
41
|
+
@selected_columns.enabled = false
|
42
|
+
@selected_columns.geometry = Qt::Rect.new(30, 310, 191, 21)
|
39
43
|
@search_label = Qt::Label.new(searchDialog)
|
40
|
-
@search_label.
|
41
|
-
@search_label.
|
42
|
-
@search_label.
|
44
|
+
@search_label.objectName = "search_label"
|
45
|
+
@search_label.geometry = Qt::Rect.new(20, 10, 51, 21)
|
46
|
+
@search_label.textFormat = Qt::PlainText
|
43
47
|
@search_combo = Qt::ComboBox.new(searchDialog)
|
44
|
-
@search_combo.
|
45
|
-
@search_combo.
|
46
|
-
@search_combo.
|
47
|
-
@search_combo.
|
48
|
+
@search_combo.objectName = "search_combo"
|
49
|
+
@search_combo.geometry = Qt::Rect.new(90, 10, 201, 25)
|
50
|
+
@search_combo.focusPolicy = Qt::StrongFocus
|
51
|
+
@search_combo.editable = true
|
48
52
|
@from_start = Qt::CheckBox.new(searchDialog)
|
49
|
-
@from_start.
|
50
|
-
@from_start.
|
53
|
+
@from_start.objectName = "from_start"
|
54
|
+
@from_start.geometry = Qt::Rect.new(90, 50, 191, 21)
|
51
55
|
@regex = Qt::CheckBox.new(searchDialog)
|
52
|
-
@regex.
|
53
|
-
@regex.
|
56
|
+
@regex.objectName = "regex"
|
57
|
+
@regex.geometry = Qt::Rect.new(90, 80, 191, 21)
|
54
58
|
@whole_words = Qt::CheckBox.new(searchDialog)
|
55
|
-
@whole_words.
|
56
|
-
@whole_words.
|
59
|
+
@whole_words.objectName = "whole_words"
|
60
|
+
@whole_words.geometry = Qt::Rect.new(90, 110, 191, 21)
|
57
61
|
@forwards = Qt::RadioButton.new(searchDialog)
|
58
|
-
@forwards.
|
59
|
-
@forwards.
|
60
|
-
@forwards.
|
62
|
+
@forwards.objectName = "forwards"
|
63
|
+
@forwards.geometry = Qt::Rect.new(90, 140, 171, 26)
|
64
|
+
@forwards.checked = true
|
61
65
|
@backwards = Qt::RadioButton.new(searchDialog)
|
62
|
-
@backwards.
|
63
|
-
@backwards.
|
64
|
-
@search_label.
|
65
|
-
Qt::Widget
|
66
|
-
Qt::Widget
|
67
|
-
Qt::Widget
|
66
|
+
@backwards.objectName = "backwards"
|
67
|
+
@backwards.geometry = Qt::Rect.new(90, 170, 176, 26)
|
68
|
+
@search_label.buddy = @search_combo
|
69
|
+
Qt::Widget.setTabOrder(@search_combo, @selected_columns)
|
70
|
+
Qt::Widget.setTabOrder(@selected_columns, @selected_rows)
|
71
|
+
Qt::Widget.setTabOrder(@selected_rows, @button_box)
|
68
72
|
|
69
73
|
retranslateUi(searchDialog)
|
70
|
-
|
71
|
-
size = Qt::Size.new(307, 400)
|
72
|
-
size = size.expandedTo(searchDialog.minimumSizeHint())
|
73
|
-
searchDialog.resize(size)
|
74
|
-
|
75
74
|
Qt::Object.connect(@button_box, SIGNAL('accepted()'), searchDialog, SLOT('accept()'))
|
76
75
|
Qt::Object.connect(@button_box, SIGNAL('rejected()'), searchDialog, SLOT('reject()'))
|
77
76
|
|
@@ -83,15 +82,15 @@ class Ui_SearchDialog
|
|
83
82
|
end
|
84
83
|
|
85
84
|
def retranslateUi(searchDialog)
|
86
|
-
searchDialog.
|
87
|
-
@selected_rows.
|
88
|
-
@selected_columns.
|
89
|
-
@search_label.
|
90
|
-
@from_start.
|
91
|
-
@regex.
|
92
|
-
@whole_words.
|
93
|
-
@forwards.
|
94
|
-
@backwards.
|
85
|
+
searchDialog.windowTitle = Qt::Application.translate("SearchDialog", "Search", nil, Qt::Application::UnicodeUTF8)
|
86
|
+
@selected_rows.text = Qt::Application.translate("SearchDialog", "Selected Rows", nil, Qt::Application::UnicodeUTF8)
|
87
|
+
@selected_columns.text = Qt::Application.translate("SearchDialog", "Selected Columns", nil, Qt::Application::UnicodeUTF8)
|
88
|
+
@search_label.text = Qt::Application.translate("SearchDialog", "Search", nil, Qt::Application::UnicodeUTF8)
|
89
|
+
@from_start.text = Qt::Application.translate("SearchDialog", "From &Start", nil, Qt::Application::UnicodeUTF8)
|
90
|
+
@regex.text = Qt::Application.translate("SearchDialog", "&Regular Expression", nil, Qt::Application::UnicodeUTF8)
|
91
|
+
@whole_words.text = Qt::Application.translate("SearchDialog", "&Whole Words", nil, Qt::Application::UnicodeUTF8)
|
92
|
+
@forwards.text = Qt::Application.translate("SearchDialog", "&Forwards", nil, Qt::Application::UnicodeUTF8)
|
93
|
+
@backwards.text = Qt::Application.translate("SearchDialog", "&Backwards", nil, Qt::Application::UnicodeUTF8)
|
95
94
|
end # retranslateUi
|
96
95
|
|
97
96
|
def retranslate_ui(searchDialog)
|
data/lib/clevic/version.rb
CHANGED
@@ -2,13 +2,17 @@ require 'clevic.rb'
|
|
2
2
|
|
3
3
|
# db connection
|
4
4
|
Clevic::DbOptions.connect( $options ) do
|
5
|
-
|
5
|
+
# use a different db for testing, so real data doesn't get broken.
|
6
|
+
if options[:database].nil? || options[:database].empty?
|
7
|
+
database( debug? ? :accounts_test : :accounts )
|
8
|
+
else
|
9
|
+
database options[:database]
|
10
|
+
end
|
6
11
|
adapter :postgresql
|
7
|
-
username '
|
12
|
+
username 'accounts'
|
8
13
|
end
|
9
14
|
|
10
|
-
class Entry <
|
11
|
-
include ActiveRecord::Dirty
|
15
|
+
class Entry < Clevic::Record
|
12
16
|
belongs_to :debit, :class_name => 'Account', :foreign_key => 'debit_id'
|
13
17
|
belongs_to :credit, :class_name => 'Account', :foreign_key => 'credit_id'
|
14
18
|
|
@@ -16,9 +20,9 @@ class Entry < ActiveRecord::Base
|
|
16
20
|
def self.ui( parent )
|
17
21
|
Clevic::TableView.new( self, parent ).create_model do
|
18
22
|
plain :date, :sample => '88-WWW-99'
|
19
|
-
distinct :description, :conditions => "now() - date <= '1 year'", :sample => 'm' * 26
|
20
|
-
relational :debit,
|
21
|
-
relational :credit,
|
23
|
+
distinct :description, :conditions => "now() - date <= '1 year'", :sample => 'm' * 26
|
24
|
+
relational :debit, :display => 'name', :conditions => 'active = true', :order => 'lower(name)', :sample => 'Leilani Member Loan'
|
25
|
+
relational :credit, :display => 'name', :conditions => 'active = true', :order => 'lower(name)', :sample => 'Leilani Member Loan'
|
22
26
|
plain :amount, :sample => 999999.99
|
23
27
|
distinct :category
|
24
28
|
plain :cheque_number
|
@@ -73,8 +77,7 @@ class Entry < ActiveRecord::Base
|
|
73
77
|
end
|
74
78
|
end
|
75
79
|
|
76
|
-
class Account <
|
77
|
-
include ActiveRecord::Dirty
|
80
|
+
class Account < Clevic::Record
|
78
81
|
has_many :debits, :class_name => 'Entry', :foreign_key => 'debit_id'
|
79
82
|
has_many :credits, :class_name => 'Entry', :foreign_key => 'credit_id'
|
80
83
|
|
@@ -92,6 +95,3 @@ class Account < ActiveRecord::Base
|
|
92
95
|
end
|
93
96
|
end
|
94
97
|
end
|
95
|
-
|
96
|
-
# order of tab display
|
97
|
-
$options[:models] = [ Entry, Account ]
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'clevic.rb'
|
2
|
+
|
3
|
+
# db connection
|
4
|
+
Clevic::DbOptions.connect do
|
5
|
+
database 'accounts_test'
|
6
|
+
adapter :postgresql
|
7
|
+
username 'accounts'
|
8
|
+
end
|
9
|
+
|
10
|
+
# minimal definition to get combo boxes to show up
|
11
|
+
class Entry < Clevic::Record
|
12
|
+
belongs_to :debit, :class_name => 'Account', :foreign_key => 'debit_id'
|
13
|
+
belongs_to :credit, :class_name => 'Account', :foreign_key => 'credit_id'
|
14
|
+
end
|
15
|
+
|
16
|
+
# minimal definition to get sensible values in combo boxes
|
17
|
+
class Account < Clevic::Record
|
18
|
+
def to_s; name; end
|
19
|
+
end
|
@@ -0,0 +1,156 @@
|
|
1
|
+
require 'clevic.rb'
|
2
|
+
|
3
|
+
# db connection options
|
4
|
+
db = Clevic::DbOptions.connect( $options ) do
|
5
|
+
# use a different db for testing, so real data doesn't get broken.
|
6
|
+
database( debug? ? :times_test : :times )
|
7
|
+
adapter :postgresql
|
8
|
+
username 'times'
|
9
|
+
end
|
10
|
+
|
11
|
+
# model definitions
|
12
|
+
class Entry < Clevic::Record
|
13
|
+
belongs_to :invoice
|
14
|
+
belongs_to :activity
|
15
|
+
belongs_to :project
|
16
|
+
|
17
|
+
# define how fields are displayed
|
18
|
+
def self.ui( parent )
|
19
|
+
Clevic::TableView.new( self, parent ).create_model do
|
20
|
+
plain :date, :sample => '28-Dec-08'
|
21
|
+
relational :project, :display => 'project', :conditions => 'active = true', :order => 'lower(project)'
|
22
|
+
relational :invoice, :display => 'invoice_number', :conditions => "status = 'not sent'", :order => 'invoice_number'
|
23
|
+
plain :start
|
24
|
+
plain :end
|
25
|
+
plain :description, :sample => 'This is a long string designed to hold lots of data and description'
|
26
|
+
relational :activity, :display => 'activity', :order => 'lower(activity)', :sample => 'Troubleshooting', :conditions => 'active = true'
|
27
|
+
distinct :module, :tooltip => 'Module or sub-project'
|
28
|
+
plain :charge, :tooltip => 'Is this time billable?'
|
29
|
+
distinct :person, :tooltip => 'The person who did the work'
|
30
|
+
|
31
|
+
records :order => 'date, start, id'
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# called when a key is pressed in this model's table view
|
36
|
+
def self.key_press_event( event, current_index, view )
|
37
|
+
case
|
38
|
+
# copy almost all of the previous line
|
39
|
+
when event.ctrl? && event.quote_dbl?
|
40
|
+
if current_index.row > 1
|
41
|
+
# fetch previous item
|
42
|
+
model = current_index.model
|
43
|
+
previous_item = model.collection[current_index.row - 1]
|
44
|
+
|
45
|
+
# copy the relevant fields
|
46
|
+
current_index.entity.start = previous_item.end
|
47
|
+
[:date, :project, :invoice, :activity, :module, :charge, :person].each do |attr|
|
48
|
+
current_index.entity.send( "#{attr.to_s}=", previous_item.send( attr ) )
|
49
|
+
end
|
50
|
+
|
51
|
+
# tell view to update
|
52
|
+
top_left_index = model.create_index( current_index.row, 0 )
|
53
|
+
bottom_right_index = model.create_index( current_index.row, current_index.column + view.builder.fields.size )
|
54
|
+
view.dataChanged( top_left_index, bottom_right_index )
|
55
|
+
|
56
|
+
# move to end time field
|
57
|
+
view.override_next_index( model.create_index( current_index.row, view.builder.index( :end ) ) )
|
58
|
+
end
|
59
|
+
# don't let anybody else handle the keypress
|
60
|
+
return true
|
61
|
+
|
62
|
+
when event.ctrl? && event.i?
|
63
|
+
invoice_from_project( current_index, view )
|
64
|
+
# don't let anybody else handle the keypress
|
65
|
+
return true
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# called when data is changed in this model's table view
|
70
|
+
def self.data_changed( top_left, bottom_right, view )
|
71
|
+
invoice_from_project( top_left, view ) if ( top_left == bottom_right )
|
72
|
+
end
|
73
|
+
|
74
|
+
def self.invoice_from_project( current_index, view )
|
75
|
+
# auto-complete invoice number field from project
|
76
|
+
current_field = current_index.attribute
|
77
|
+
if current_field == :project && current_index.entity.project != nil
|
78
|
+
# most recent entry, ordered in reverse
|
79
|
+
invoice = current_index.entity.project.latest_invoice
|
80
|
+
|
81
|
+
unless invoice.nil?
|
82
|
+
# make a reference to the invoice
|
83
|
+
current_index.entity.invoice = invoice
|
84
|
+
|
85
|
+
# update view from top_left to bottom_right
|
86
|
+
model = current_index.model
|
87
|
+
changed_index = model.create_index( current_index.row, view.builder.index( :invoice ) )
|
88
|
+
view.dataChanged( changed_index, changed_index )
|
89
|
+
|
90
|
+
# move edit cursor to start time field
|
91
|
+
view.override_next_index( model.create_index( current_index.row, view.builder.index( :start ) ) )
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
class Invoice < Clevic::Record
|
98
|
+
has_many :entries
|
99
|
+
|
100
|
+
# define how fields are displayed
|
101
|
+
def self.ui( parent )
|
102
|
+
Clevic::TableView.new( self, parent ).create_model do
|
103
|
+
plain :date
|
104
|
+
distinct :client
|
105
|
+
plain :invoice_number
|
106
|
+
restricted :status, :set => ['not sent', 'sent', 'paid', 'debt', 'writeoff', 'internal']
|
107
|
+
restricted :billing, :set => %w{Hours Quote Internal}
|
108
|
+
plain :quote_date
|
109
|
+
plain :quote_amount
|
110
|
+
plain :description
|
111
|
+
|
112
|
+
records :order => 'invoice_number'
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
class Project < Clevic::Record
|
118
|
+
has_many :entries
|
119
|
+
|
120
|
+
def self.ui( parent )
|
121
|
+
Clevic::TableView.new( Project, parent ).create_model do
|
122
|
+
plain :project
|
123
|
+
plain :description
|
124
|
+
distinct :client
|
125
|
+
plain :rate
|
126
|
+
plain :active
|
127
|
+
|
128
|
+
records :order => 'project'
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
# Return the latest invoice for this project
|
133
|
+
# Not part of the UI.
|
134
|
+
def latest_invoice
|
135
|
+
Invoice.find(
|
136
|
+
:first,
|
137
|
+
:conditions => ["client = ? and status = 'not sent'", self.client],
|
138
|
+
:order => 'invoice_number desc'
|
139
|
+
)
|
140
|
+
end
|
141
|
+
|
142
|
+
end
|
143
|
+
|
144
|
+
class Activity < Clevic::Record
|
145
|
+
has_many :entries
|
146
|
+
|
147
|
+
# define how fields are displayed
|
148
|
+
def self.ui( parent )
|
149
|
+
Clevic::TableView.new( Activity, parent ).create_model do
|
150
|
+
plain :activity
|
151
|
+
plain :active
|
152
|
+
|
153
|
+
records :order => 'activity'
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
@@ -2,14 +2,12 @@ require 'clevic.rb'
|
|
2
2
|
|
3
3
|
# db connection options
|
4
4
|
Clevic::DbOptions.connect( $options ) do
|
5
|
-
database
|
6
|
-
adapter :
|
7
|
-
username 'panic'
|
5
|
+
database :times
|
6
|
+
adapter :sqlite3
|
8
7
|
end
|
9
8
|
|
10
9
|
# model definitions
|
11
|
-
class Entry <
|
12
|
-
include ActiveRecord::Dirty
|
10
|
+
class Entry < Clevic::Record
|
13
11
|
belongs_to :invoice
|
14
12
|
belongs_to :activity
|
15
13
|
belongs_to :project
|
@@ -18,12 +16,12 @@ class Entry < ActiveRecord::Base
|
|
18
16
|
def self.ui( parent )
|
19
17
|
Clevic::TableView.new( self, parent ).create_model do
|
20
18
|
plain :date, :sample => '28-Dec-08'
|
21
|
-
relational :project, 'project', :conditions =>
|
22
|
-
relational :invoice, 'invoice_number', :conditions => "status = 'not sent'", :order => 'invoice_number'
|
19
|
+
relational :project, :display => 'project', :conditions => "active = true", :order => 'lower(project)'
|
20
|
+
relational :invoice, :display => 'invoice_number', :conditions => "status = 'not sent'", :order => 'invoice_number'
|
23
21
|
plain :start
|
24
22
|
plain :end
|
25
23
|
plain :description, :sample => 'This is a long string designed to hold lots of data and description'
|
26
|
-
relational :activity, 'activity', :order => 'lower(activity)', :sample => 'Troubleshooting', :conditions => 'active =
|
24
|
+
relational :activity, :display => 'activity', :order => 'lower(activity)', :sample => 'Troubleshooting', :conditions => 'active = #{connection.quoted_true}'
|
27
25
|
distinct :module, :tooltip => 'Module or sub-project'
|
28
26
|
plain :charge, :tooltip => 'Is this time billable?'
|
29
27
|
distinct :person, :tooltip => 'The person who did the work'
|
@@ -94,8 +92,7 @@ class Entry < ActiveRecord::Base
|
|
94
92
|
end
|
95
93
|
end
|
96
94
|
|
97
|
-
class Project <
|
98
|
-
include ActiveRecord::Dirty
|
95
|
+
class Project < Clevic::Record
|
99
96
|
has_many :entries
|
100
97
|
|
101
98
|
def self.ui( parent )
|
@@ -122,8 +119,7 @@ class Project < ActiveRecord::Base
|
|
122
119
|
|
123
120
|
end
|
124
121
|
|
125
|
-
class Activity <
|
126
|
-
include ActiveRecord::Dirty
|
122
|
+
class Activity < Clevic::Record
|
127
123
|
has_many :entries
|
128
124
|
|
129
125
|
# define how fields are displayed
|
@@ -137,15 +133,14 @@ class Activity < ActiveRecord::Base
|
|
137
133
|
end
|
138
134
|
end
|
139
135
|
|
140
|
-
class Invoice <
|
141
|
-
include ActiveRecord::Dirty
|
136
|
+
class Invoice < Clevic::Record
|
142
137
|
has_many :entries
|
143
138
|
|
144
139
|
# define how fields are displayed
|
145
140
|
def self.ui( parent )
|
146
141
|
Clevic::TableView.new( Invoice, parent ).create_model do
|
147
142
|
plain :date
|
148
|
-
distinct :client
|
143
|
+
distinct :client
|
149
144
|
plain :invoice_number
|
150
145
|
restricted :status, :set => ['not sent', 'sent', 'paid', 'debt', 'writeoff', 'internal']
|
151
146
|
restricted :billing, :set => %w{Hours Quote Internal}
|
@@ -157,6 +152,3 @@ class Invoice < ActiveRecord::Base
|
|
157
152
|
end
|
158
153
|
end
|
159
154
|
end
|
160
|
-
|
161
|
-
# tab widget order
|
162
|
-
$options[:models] = [ Entry, Invoice, Project, Activity ]
|