Merge branch 'release/1.6'

This commit is contained in:
Paul van Tilburg 2014-10-19 21:38:29 +02:00
commit 5ab26718b3
4 changed files with 204 additions and 91 deletions

71
CHANGELOG.rdoc Normal file
View 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
View 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"]

View File

@ -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:

View File

@ -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,56 +1865,60 @@ module StopTime::Views
:href => R(CustomersNTasksNew, @customer.id) :href => R(CustomersNTasksNew, @customer.id)
end end
end end
div.accordion.task_list! do if @billed_tasks.empty?
@billed_tasks.keys.sort_by { |task| task.name }.each do |task| p "None found!"
div.accordion_group do else
div.accordion_heading do div.accordion.task_list! do
span.accordion_toggle do @billed_tasks.keys.sort_by { |task| task.name }.each do |task|
a task.name, "data-toggle" => "collapse", div.accordion_group do
"data-parent" => "#task_list", div.accordion_heading do
:href => "#collapse#{task.id}" span.accordion_toggle do
# FXIME: the following is not very RESTful! a task.name, "data-toggle" => "collapse",
form.form_inline.pull_right :action => R(CustomersNTasks, @customer.id), "data-parent" => "#task_list",
:method => :post do :href => "#collapse#{task.id}"
a.btn.btn_mini "Edit", :href => R(CustomersNTasksN, @customer.id, task.id) # FXIME: the following is not very RESTful!
input :type => :hidden, :name => "task_id", :value => task.id form.form_inline.pull_right :action => R(CustomersNTasks, @customer.id),
button.btn.btn_danger.btn_mini "Delete", :type => :submit, :method => :post do
:name => "delete", :value => "Delete" a.btn.btn_mini "Edit", :href => R(CustomersNTasksN, @customer.id, task.id)
input :type => :hidden, :name => "task_id", :value => task.id
button.btn.btn_danger.btn_mini "Delete", :type => :submit,
:name => "delete", :value => "Delete"
end
end end
end end
end div.accordion_body.collapse :id => "collapse#{task.id}" do
div.accordion_body.collapse :id => "collapse#{task.id}" do div.accordion_inner do
div.accordion_inner do if @billed_tasks[task].empty?
if @billed_tasks[task].empty? i { "No billed projects/tasks found" }
i { "No billed projects/tasks found" } else
else table.table.table_condensed do
table.table.table_condensed do col.task_list
col.task_list @billed_tasks[task].sort_by { |t| t.invoice.number }.each do |billed_task|
@billed_tasks[task].sort_by { |t| t.invoice.number }.each do |billed_task| tr do
tr do td do
td do a billed_task.comment_or_name,
a billed_task.comment_or_name, :href => R(CustomersNTasksN, @customer.id, billed_task.id)
:href => R(CustomersNTasksN, @customer.id, billed_task.id) small do
small do text! "(billed in invoice "
text! "(billed in invoice " a billed_task.invoice.number,
a billed_task.invoice.number, :title => billed_task.invoice.number,
:title => billed_task.invoice.number, :href => R(CustomersNInvoicesX, @customer.id,
:href => R(CustomersNInvoicesX, @customer.id, billed_task.invoice.number)
billed_task.invoice.number) text! ")"
text! ")" end
end end
end td do
td do # FXIME: the following is not very RESTful!
# FXIME: the following is not very RESTful! form.form_inline.pull_right :action => R(CustomersNTasks, @customer.id),
form.form_inline.pull_right :action => R(CustomersNTasks, @customer.id), :method => :post do
:method => :post do a.btn.btn_mini "Edit",
a.btn.btn_mini "Edit", :href => R(CustomersNTasksN, @customer.id,
:href => R(CustomersNTasksN, @customer.id, billed_task.id)
billed_task.id) input :type => :hidden, :name => "task_id",
input :type => :hidden, :name => "task_id", :value => billed_task.id
:value => billed_task.id button.btn.btn_danger.btn_mini "Delete", :type => :submit,
button.btn.btn_danger.btn_mini "Delete", :type => :submit, :name => "delete", :value => "Delete"
:name => "delete", :value => "Delete" end
end end
end end
end end
@ -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 if @invoices.values.flatten.empty?
div.span7 do div.alert.alert_info do
if @invoices.values.flatten.empty? text! "Found none! You can create one by " +
p do "#{a "selecting a customer", :href => R(Customers)}."
text! "Found none! You can create one by " end
"#{a "selecting a customer", :href => R(Customers)}." else
end div.row do
else 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 }