Merge branch 'release/1.6'
This commit is contained in:
commit
5ab26718b3
71
CHANGELOG.rdoc
Normal file
71
CHANGELOG.rdoc
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
= Stop… Camping Time! release news
|
||||||
|
|
||||||
|
== 1.6
|
||||||
|
|
||||||
|
Application:
|
||||||
|
|
||||||
|
* Add support for Ruby 2.x; drop support for Ruby 1.8
|
||||||
|
* Add support for ActiveRecord 4
|
||||||
|
|
||||||
|
Other bugfixes:
|
||||||
|
|
||||||
|
* Round total time of tasks to two decimals
|
||||||
|
* Fix missing doctype in main layout
|
||||||
|
* Fix column cache being out-of-sync after migration
|
||||||
|
* Fix column rename migration
|
||||||
|
* Fix broken migration that cannot access config
|
||||||
|
* Fix broken period calculation initialisation
|
||||||
|
|
||||||
|
== 1.4.1
|
||||||
|
|
||||||
|
Features:
|
||||||
|
|
||||||
|
* Sort invoices in descending order by default
|
||||||
|
* Move the 'Create a new invoice' button to a more consistent location
|
||||||
|
|
||||||
|
== 1.4
|
||||||
|
|
||||||
|
Features:
|
||||||
|
|
||||||
|
* Improvements in IBAN support [#688d33]
|
||||||
|
* Suport alternative invoice templates
|
||||||
|
* Allow time specifications to be added to invoices [#fb896d]
|
||||||
|
* Add a flag for a customer to
|
||||||
|
* Rework the project/task list in the customer view [#9a33e4]
|
||||||
|
* Show billed task instances and fixed costs by linking to
|
||||||
|
the invoice
|
||||||
|
* Add links to billed time entries in the invoice view
|
||||||
|
* Visual tweaks
|
||||||
|
|
||||||
|
Application:
|
||||||
|
|
||||||
|
* Use isodoc 1.00 (needed for IBAN)
|
||||||
|
|
||||||
|
== 1.2
|
||||||
|
|
||||||
|
Features:
|
||||||
|
|
||||||
|
* Default VAT rate set to 21%
|
||||||
|
* Make links on time entry descriptions and tasks more consistent
|
||||||
|
* Color customer names on overview and invoices in invoice
|
||||||
|
lists based on invoice status (yellow: too late, red: far too late)
|
||||||
|
* Check tasks and time entries by default in the invoice create form
|
||||||
|
* Lots of other small view tweaks
|
||||||
|
|
||||||
|
Application:
|
||||||
|
|
||||||
|
* Port to Camping 2.x and isodoc 0.10 [#26e4aa] [#804d96]
|
||||||
|
* Add support for Ruby 1.9
|
||||||
|
* Include jQuery 1.0
|
||||||
|
* Enable response Bootstrap CSS
|
||||||
|
|
||||||
|
Other bug fixes:
|
||||||
|
|
||||||
|
* Redirect back to referer after creating/updating time entries [#f08f36]
|
||||||
|
* Add a day if the end time is before the start time [#d96685]
|
||||||
|
* Check task and time entry checkboxes by default in invoice create form [#4fdf84]
|
||||||
|
* Fix the way the DATE_FORMATS are set to suit AR3.2 [#9dfc93]
|
||||||
|
|
||||||
|
== 1.0
|
||||||
|
|
||||||
|
First release
|
29
Dockerfile
Normal file
29
Dockerfile
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
FROM debian:wheezy
|
||||||
|
MAINTAINER Paul van Tilburg "paul@luon.net"
|
||||||
|
|
||||||
|
ENV DEBIAN_FRONTEND noninteractive
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
camping \
|
||||||
|
ruby-activerecord-3.2 \
|
||||||
|
ruby-sqlite3 \
|
||||||
|
ruby-mab \
|
||||||
|
ruby-actionpack-3.2 \
|
||||||
|
ruby-sass \
|
||||||
|
thin \
|
||||||
|
texlive-latex-base \
|
||||||
|
texlive-latex-extra \
|
||||||
|
rubber
|
||||||
|
|
||||||
|
RUN mkdir -p /home/camping/stoptime
|
||||||
|
ADD . /home/camping/stoptime
|
||||||
|
WORKDIR /home/camping/stoptime
|
||||||
|
ENV HOME /home/camping
|
||||||
|
|
||||||
|
# Ugh, necessary because not available in backports
|
||||||
|
# Before build on Jessie/Sid: apt-get download ruby-mab
|
||||||
|
RUN dpkg -i ruby-mab_0.0.3-1_all.deb
|
||||||
|
|
||||||
|
RUN apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||||
|
|
||||||
|
EXPOSE 3301
|
||||||
|
CMD ["/usr/bin/camping", "stoptime.rb"]
|
|
@ -1,4 +1,4 @@
|
||||||
= Stop… Camping Time!
|
= Stop… Camping Time! documentation
|
||||||
|
|
||||||
A (Camping) web application for task/project time registration and
|
A (Camping) web application for task/project time registration and
|
||||||
invoicing.
|
invoicing.
|
||||||
|
@ -13,12 +13,13 @@ invoicing.
|
||||||
* hourly rates
|
* hourly rates
|
||||||
* Administration of invoices
|
* Administration of invoices
|
||||||
* Invoice generation in PDF/LaTeX format
|
* Invoice generation in PDF/LaTeX format
|
||||||
|
* can include a time specification if required by the customer
|
||||||
|
|
||||||
== Requirements
|
== Requirements
|
||||||
|
|
||||||
Stop… Camping Time! is a Camping application, so you need:
|
Stop… Camping Time! is a Camping application, so you need:
|
||||||
|
|
||||||
* Ruby 1.8 (>= 1.8.7) or 1.9 (>= 1.9.3)
|
* Ruby 1.9 (>= 1.9.3) or 2.x
|
||||||
* Camping (>= 2.1.532) with
|
* Camping (>= 2.1.532) with
|
||||||
* Active Record (>= 3.2)
|
* Active Record (>= 3.2)
|
||||||
* Mab (>= 0.0.3) , and optionally:
|
* Mab (>= 0.0.3) , and optionally:
|
||||||
|
|
76
stoptime.rb
76
stoptime.rb
|
@ -53,6 +53,10 @@ end
|
||||||
# = The main application module
|
# = The main application module
|
||||||
module StopTime
|
module StopTime
|
||||||
|
|
||||||
|
# The version of the application
|
||||||
|
VERSION = '1.6'
|
||||||
|
puts "Starting Stop… Camping Time! version #{VERSION}"
|
||||||
|
|
||||||
# The parsed configuration (Hash).
|
# The parsed configuration (Hash).
|
||||||
attr_reader :config
|
attr_reader :config
|
||||||
|
|
||||||
|
@ -215,7 +219,7 @@ module StopTime::Models
|
||||||
|
|
||||||
# Returns a list of tasks that have not been billed via in invoice.
|
# Returns a list of tasks that have not been billed via in invoice.
|
||||||
def unbilled_tasks
|
def unbilled_tasks
|
||||||
tasks.all(:conditions => ["invoice_id IS NULL"], :order => "name ASC")
|
tasks.where("invoice_id IS NULL").order("name ASC")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -261,7 +265,7 @@ module StopTime::Models
|
||||||
# Returns a list of time entries that should be (and are not yet)
|
# Returns a list of time entries that should be (and are not yet)
|
||||||
# billed.
|
# billed.
|
||||||
def billable_time_entries
|
def billable_time_entries
|
||||||
time_entries.all(:conditions => ["bill = 't'"], :order => "start ASC")
|
time_entries.where("bill = 't'").order("start ASC")
|
||||||
end
|
end
|
||||||
|
|
||||||
# Returns the bill period of the task by means of an Array containing
|
# Returns the bill period of the task by means of an Array containing
|
||||||
|
@ -377,7 +381,7 @@ module StopTime::Models
|
||||||
has_many :time_entries, :through => :tasks
|
has_many :time_entries, :through => :tasks
|
||||||
belongs_to :customer
|
belongs_to :customer
|
||||||
belongs_to :company_info
|
belongs_to :company_info
|
||||||
default_scope order('number DESC')
|
default_scope lambda { order('number DESC') }
|
||||||
|
|
||||||
# Returns a time and cost summary of the contained tasks (Hash of
|
# Returns a time and cost summary of the contained tasks (Hash of
|
||||||
# Task to Array).
|
# Task to Array).
|
||||||
|
@ -402,8 +406,9 @@ module StopTime::Models
|
||||||
# See also Task#bill_period.
|
# See also Task#bill_period.
|
||||||
def period
|
def period
|
||||||
# FIXME: maybe should be updated_at?
|
# FIXME: maybe should be updated_at?
|
||||||
return [created_at, created_at] if tasks.empty?
|
p = [created_at, created_at]
|
||||||
p = tasks.first.bill_period
|
return p if tasks.empty?
|
||||||
|
|
||||||
tasks.each do |task|
|
tasks.each do |task|
|
||||||
tp = task.bill_period
|
tp = task.bill_period
|
||||||
p[0] = tp[0] if tp[0] < p[0]
|
p[0] = tp[0] if tp[0] < p[0]
|
||||||
|
@ -532,9 +537,10 @@ module StopTime::Models
|
||||||
|
|
||||||
class HourlyRateSupport < V 1.3 # :nodoc:
|
class HourlyRateSupport < V 1.3 # :nodoc:
|
||||||
def self.up
|
def self.up
|
||||||
|
config = Config.instance
|
||||||
add_column(Customer.table_name, :hourly_rate, :float,
|
add_column(Customer.table_name, :hourly_rate, :float,
|
||||||
:null => false,
|
:null => false,
|
||||||
:default => @config["hourly_rate"])
|
:default => config["hourly_rate"])
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.down
|
def self.down
|
||||||
|
@ -624,21 +630,11 @@ module StopTime::Models
|
||||||
|
|
||||||
class PaidFlagTypoFix < V 1.9 # :nodoc:
|
class PaidFlagTypoFix < V 1.9 # :nodoc:
|
||||||
def self.up
|
def self.up
|
||||||
add_column(Invoice.table_name, :paid, :boolean)
|
rename_column(Invoice.table_name, :payed, :paid)
|
||||||
Invoice.all.each do |i|
|
|
||||||
i.paid = i.payed unless i.payed.blank?
|
|
||||||
i.save
|
|
||||||
end
|
|
||||||
remove_column(Invoice.table_name, :payed)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.down
|
def self.down
|
||||||
add_column(Invoice.table_name, :payed, :boolean)
|
rename_column(Invoice.table_name, :paid, :payed)
|
||||||
Invoice.all.each do |i|
|
|
||||||
i.payed = i.paid unless i.paid.blank?
|
|
||||||
i.save
|
|
||||||
end
|
|
||||||
remove_column(Invoice.table_name, :paid)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -704,6 +700,9 @@ module StopTime::Models
|
||||||
def self.up
|
def self.up
|
||||||
add_column(Customer.table_name, :time_specification, :boolean)
|
add_column(Customer.table_name, :time_specification, :boolean)
|
||||||
add_column(Invoice.table_name, :include_specification, :boolean)
|
add_column(Invoice.table_name, :include_specification, :boolean)
|
||||||
|
|
||||||
|
Customer.reset_column_information
|
||||||
|
Invoice.reset_column_information
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.down
|
def self.down
|
||||||
|
@ -749,7 +748,7 @@ module StopTime::Controllers
|
||||||
class Customers
|
class Customers
|
||||||
# Gets the list of customers and displays them via Views#customers.
|
# Gets the list of customers and displays them via Views#customers.
|
||||||
def get
|
def get
|
||||||
@customers = Customer.all(:order => "name ASC")
|
@customers = Customer.order("name ASC")
|
||||||
render :customers
|
render :customers
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -812,7 +811,7 @@ module StopTime::Controllers
|
||||||
def get(customer_id)
|
def get(customer_id)
|
||||||
@customer = Customer.find(customer_id)
|
@customer = Customer.find(customer_id)
|
||||||
@input = @customer.attributes
|
@input = @customer.attributes
|
||||||
@tasks = @customer.tasks.all(:order => "name, invoice_id ASC")
|
@tasks = @customer.tasks.order("name ASC, invoice_id ASC")
|
||||||
# FIXME: this dirty hack assumes that tasks have unique names,
|
# FIXME: this dirty hack assumes that tasks have unique names,
|
||||||
# becasue there is no reference from billed tasks to its original
|
# becasue there is no reference from billed tasks to its original
|
||||||
# task.
|
# task.
|
||||||
|
@ -912,7 +911,7 @@ module StopTime::Controllers
|
||||||
@errors = @task.errors
|
@errors = @task.errors
|
||||||
@customer = Customer.find(customer_id)
|
@customer = Customer.find(customer_id)
|
||||||
@customer_list = Customer.all.map { |c| [c.id, c.shortest_name] }
|
@customer_list = Customer.all.map { |c| [c.id, c.shortest_name] }
|
||||||
@time_entries = @task.time_entries.all(:order => "start DESC")
|
@time_entries = @task.time_entries.order("start DESC")
|
||||||
@time_entries.each do |te|
|
@time_entries.each do |te|
|
||||||
@input["bill_#{te.id}"] = true if te.bill?
|
@input["bill_#{te.id}"] = true if te.bill?
|
||||||
end
|
end
|
||||||
|
@ -966,7 +965,7 @@ module StopTime::Controllers
|
||||||
@customer = Customer.find(customer_id)
|
@customer = Customer.find(customer_id)
|
||||||
@customer_list = Customer.all.map { |c| [c.id, c.shortest_name] }
|
@customer_list = Customer.all.map { |c| [c.id, c.shortest_name] }
|
||||||
@task = Task.find(task_id)
|
@task = Task.find(task_id)
|
||||||
@time_entries = @task.time_entries.all(:order => "start DESC")
|
@time_entries = @task.time_entries.order("start DESC")
|
||||||
|
|
||||||
@input = @task.attributes
|
@input = @task.attributes
|
||||||
@input["type"] = @task.type
|
@input["type"] = @task.type
|
||||||
|
@ -1234,7 +1233,7 @@ module StopTime::Controllers
|
||||||
# the timeline using Views#time_entries
|
# the timeline using Views#time_entries
|
||||||
def get
|
def get
|
||||||
if @input["show"] == "all"
|
if @input["show"] == "all"
|
||||||
@time_entries = TimeEntry.all(:order => "start DESC")
|
@time_entries = TimeEntry.order("start DESC")
|
||||||
else
|
else
|
||||||
@time_entries = TimeEntry.joins(:task)\
|
@time_entries = TimeEntry.joins(:task)\
|
||||||
.where("stoptime_tasks.invoice_id" => nil)\
|
.where("stoptime_tasks.invoice_id" => nil)\
|
||||||
|
@ -1323,7 +1322,7 @@ module StopTime::Controllers
|
||||||
@input["end"] = @time_entry.end.to_formatted_s(:time_only)
|
@input["end"] = @time_entry.end.to_formatted_s(:time_only)
|
||||||
@customer_list = Customer.all.map { |c| [c.id, c.shortest_name] }
|
@customer_list = Customer.all.map { |c| [c.id, c.shortest_name] }
|
||||||
@task_list = Hash.new { |h, k| h[k] = Array.new }
|
@task_list = Hash.new { |h, k| h[k] = Array.new }
|
||||||
Task.all(:order => "name, invoice_id ASC").each do |t|
|
Task.order("name ASC, invoice_id ASC").each do |t|
|
||||||
name = t.billed? ? t.name + " (#{t.invoice.number})" : t.name
|
name = t.billed? ? t.name + " (#{t.invoice.number})" : t.name
|
||||||
@task_list[t.customer.shortest_name] << [t.id, name]
|
@task_list[t.customer.shortest_name] << [t.id, name]
|
||||||
end
|
end
|
||||||
|
@ -1418,7 +1417,11 @@ module StopTime::Controllers
|
||||||
# Retrieves the company information and shows a form for updating
|
# Retrieves the company information and shows a form for updating
|
||||||
# via Views#company_form.
|
# via Views#company_form.
|
||||||
def get
|
def get
|
||||||
@company = CompanyInfo.find(@input.revision || :last)
|
@company = if @input.revision.present?
|
||||||
|
CompanyInfo.find(@input.revision)
|
||||||
|
else
|
||||||
|
CompanyInfo.last
|
||||||
|
end
|
||||||
@input = @company.attributes
|
@input = @company.attributes
|
||||||
@history_warn = true if @company != CompanyInfo.last
|
@history_warn = true if @company != CompanyInfo.last
|
||||||
render :company_form
|
render :company_form
|
||||||
|
@ -1428,7 +1431,11 @@ module StopTime::Controllers
|
||||||
# (Views#company_form).
|
# (Views#company_form).
|
||||||
# If the provided information was invalid, the errors are retrieved.
|
# If the provided information was invalid, the errors are retrieved.
|
||||||
def post
|
def post
|
||||||
@company = CompanyInfo.find(@input.revision || :last)
|
@company = if @input.revision.present?
|
||||||
|
CompanyInfo.find(@input.revision)
|
||||||
|
else
|
||||||
|
CompanyInfo.last
|
||||||
|
end
|
||||||
# If we are editing the current info and it is already associated
|
# If we are editing the current info and it is already associated
|
||||||
# with some invoices, create a new revision.
|
# with some invoices, create a new revision.
|
||||||
@history_warn = true if @company != CompanyInfo.last
|
@history_warn = true if @company != CompanyInfo.last
|
||||||
|
@ -1484,6 +1491,7 @@ module StopTime::Views
|
||||||
|
|
||||||
# The main layout used by all views.
|
# The main layout used by all views.
|
||||||
def layout
|
def layout
|
||||||
|
doctype!
|
||||||
html(:lang => "en") do
|
html(:lang => "en") do
|
||||||
head do
|
head do
|
||||||
title "Stop… Camping Time!"
|
title "Stop… Camping Time!"
|
||||||
|
@ -1522,13 +1530,13 @@ module StopTime::Views
|
||||||
small "#{@tasks.count} customers, #{@task_count} active projects/tasks"
|
small "#{@tasks.count} customers, #{@task_count} active projects/tasks"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
div.row do
|
|
||||||
if @tasks.empty?
|
if @tasks.empty?
|
||||||
div.alert.alert_info do
|
div.alert.alert_info do
|
||||||
text! "No customers, projects or tasks found! Set them up " +
|
text! "No customers, projects or tasks found! Set them up " +
|
||||||
"#{a "here", :href => R(CustomersNew)}."
|
"#{a "here", :href => R(CustomersNew)}."
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
|
div.row do
|
||||||
div.span6 do
|
div.span6 do
|
||||||
@tasks.keys.sort_by { |c| c.name }.each do |customer|
|
@tasks.keys.sort_by { |c| c.name }.each do |customer|
|
||||||
inv_klass = "text_info"
|
inv_klass = "text_info"
|
||||||
|
@ -1746,7 +1754,7 @@ module StopTime::Views
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
if @customers.empty?
|
if @customers.empty?
|
||||||
p do
|
div.alert.alert_info do
|
||||||
text! "None found! You can create one " +
|
text! "None found! You can create one " +
|
||||||
"#{a "here", :href => R(CustomersNew)}."
|
"#{a "here", :href => R(CustomersNew)}."
|
||||||
end
|
end
|
||||||
|
@ -1857,6 +1865,9 @@ module StopTime::Views
|
||||||
:href => R(CustomersNTasksNew, @customer.id)
|
:href => R(CustomersNTasksNew, @customer.id)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
if @billed_tasks.empty?
|
||||||
|
p "None found!"
|
||||||
|
else
|
||||||
div.accordion.task_list! do
|
div.accordion.task_list! do
|
||||||
@billed_tasks.keys.sort_by { |task| task.name }.each do |task|
|
@billed_tasks.keys.sort_by { |task| task.name }.each do |task|
|
||||||
div.accordion_group do
|
div.accordion_group do
|
||||||
|
@ -1918,6 +1929,7 @@ module StopTime::Views
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
h2 do
|
h2 do
|
||||||
text! "Invoices"
|
text! "Invoices"
|
||||||
|
@ -2000,14 +2012,14 @@ module StopTime::Views
|
||||||
small "#{@invoices.count} customers, #{@invoice_count} invoices"
|
small "#{@invoices.count} customers, #{@invoice_count} invoices"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
div.row do
|
|
||||||
div.span7 do
|
|
||||||
if @invoices.values.flatten.empty?
|
if @invoices.values.flatten.empty?
|
||||||
p do
|
div.alert.alert_info do
|
||||||
text! "Found none! You can create one by "
|
text! "Found none! You can create one by " +
|
||||||
"#{a "selecting a customer", :href => R(Customers)}."
|
"#{a "selecting a customer", :href => R(Customers)}."
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
|
div.row do
|
||||||
|
div.span7 do
|
||||||
@invoices.keys.sort.each do |key|
|
@invoices.keys.sort.each do |key|
|
||||||
next if @invoices[key].empty?
|
next if @invoices[key].empty?
|
||||||
h3 { key }
|
h3 { key }
|
||||||
|
|
Loading…
Reference in a new issue