2011-10-31 14:36:01 +01:00
|
|
|
#!/usr/bin/env camping
|
|
|
|
#
|
|
|
|
# stoptime.rb - The Stop… Camping Time! time registration and invoice
|
|
|
|
# application.
|
|
|
|
#
|
|
|
|
# Stop… Camping Time! is Copyright © 2011 Paul van Tilburg <paul@luon.net>
|
|
|
|
#
|
|
|
|
# This program is free software; you can redistribute it and/or modify it
|
|
|
|
# under the terms of the GNU General Public License as published by the
|
|
|
|
# Free Software Foundation; either version 2 of the License, or (at your
|
|
|
|
# option) any later version.
|
|
|
|
|
2011-11-01 15:27:16 +01:00
|
|
|
require "active_support"
|
2011-10-31 14:36:01 +01:00
|
|
|
require "camping"
|
|
|
|
require "markaby"
|
2011-11-03 11:00:35 +01:00
|
|
|
require "mime/types"
|
2011-10-31 14:36:01 +01:00
|
|
|
require "pathname"
|
|
|
|
|
|
|
|
Markaby::Builder.set(:indent, 2)
|
|
|
|
Camping.goes :StopTime
|
|
|
|
|
2011-11-03 11:39:58 +01:00
|
|
|
unless defined? PUBLIC_DIR
|
|
|
|
PUBLIC_DIR = Pathname.new(__FILE__).dirname.expand_path + "public"
|
|
|
|
TEMPLATE_DIR = Pathname.new(__FILE__).dirname.expand_path + "templates"
|
2011-11-03 10:30:02 +01:00
|
|
|
|
2011-11-01 15:27:16 +01:00
|
|
|
# Set the default date(/time) format.
|
|
|
|
ActiveSupport::CoreExtensions::Time::Conversions::DATE_FORMATS.merge!(
|
2011-11-02 22:52:47 +01:00
|
|
|
:default => "%Y-%m-%d %H:%M",
|
|
|
|
:month_and_year => "%B %Y",
|
2011-11-03 11:40:58 +01:00
|
|
|
:month_code => "%Y%m",
|
|
|
|
:day_code => "%Y%m%d")
|
2011-11-01 15:27:16 +01:00
|
|
|
ActiveSupport::CoreExtensions::Date::Conversions::DATE_FORMATS.merge!(
|
2011-11-02 22:52:47 +01:00
|
|
|
:default => "%Y-%m-%d",
|
|
|
|
:month_and_year => "%B %Y")
|
2011-11-03 10:30:02 +01:00
|
|
|
|
|
|
|
# FIXME: this should be configurable.
|
2011-11-03 23:44:06 +01:00
|
|
|
HourlyRate = 20.0
|
2011-11-03 22:17:18 +01:00
|
|
|
VATRate = 19.0
|
2011-11-01 15:27:16 +01:00
|
|
|
end
|
|
|
|
|
2011-10-31 14:36:01 +01:00
|
|
|
module StopTime
|
|
|
|
|
|
|
|
def self.create
|
|
|
|
StopTime::Models.create_schema
|
|
|
|
end
|
|
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
module StopTime::Models
|
|
|
|
|
|
|
|
class Customer < Base
|
|
|
|
has_many :tasks
|
2011-11-02 22:52:47 +01:00
|
|
|
has_many :time_entries, :through => :tasks
|
2011-10-31 14:36:01 +01:00
|
|
|
end
|
|
|
|
|
|
|
|
class Task < Base
|
|
|
|
has_many :time_entries
|
2011-11-01 15:29:55 +01:00
|
|
|
belongs_to :customer
|
2011-11-07 10:24:12 +01:00
|
|
|
|
|
|
|
def fixed_cost?
|
|
|
|
not self.fixed_cost.blank?
|
|
|
|
end
|
2011-11-07 13:38:07 +01:00
|
|
|
|
|
|
|
def task_type
|
|
|
|
fixed_cost? ? "fixed_cost" : "hourly_rate"
|
|
|
|
end
|
2011-10-31 14:36:01 +01:00
|
|
|
end
|
|
|
|
|
|
|
|
class TimeEntry < Base
|
|
|
|
belongs_to :task
|
2011-11-07 17:41:46 +01:00
|
|
|
belongs_to :invoice
|
2011-11-07 13:38:07 +01:00
|
|
|
has_one :customer, :through => :task
|
2011-11-07 17:41:46 +01:00
|
|
|
|
|
|
|
def total
|
|
|
|
(self.end - self.start) / 1.hour
|
|
|
|
end
|
2011-11-07 10:24:12 +01:00
|
|
|
end
|
|
|
|
|
|
|
|
class Invoice < Base
|
|
|
|
has_many :time_entries
|
|
|
|
belongs_to :customer
|
2011-11-07 17:41:46 +01:00
|
|
|
|
|
|
|
def summary
|
|
|
|
# FIXME: ensure that month is a DateTime/Time object.
|
|
|
|
time_entries = self.time_entries.all
|
|
|
|
|
|
|
|
tasks = time_entries.inject({}) do |tasks, entry|
|
|
|
|
time = entry.total
|
|
|
|
if tasks.has_key? entry.task
|
|
|
|
tasks[entry.task][0] += time
|
|
|
|
tasks[entry.task][2] += time * entry.task.hourly_rate
|
|
|
|
else
|
|
|
|
tasks[entry.task] = [time, entry.task.hourly_rate,
|
|
|
|
time * entry.task.hourly_rate]
|
|
|
|
end
|
|
|
|
tasks
|
|
|
|
end
|
|
|
|
|
|
|
|
return tasks
|
|
|
|
end
|
2011-10-31 14:36:01 +01:00
|
|
|
end
|
|
|
|
|
2011-11-07 14:54:11 +01:00
|
|
|
class CompanyInfo < Base
|
|
|
|
end
|
|
|
|
|
2011-10-31 14:36:01 +01:00
|
|
|
class StopTimeTables < V 1.0
|
|
|
|
def self.up
|
|
|
|
create_table Customer.table_name do |t|
|
|
|
|
t.string :name, :short_name,
|
|
|
|
:address_street, :address_postal_code, :address_city,
|
|
|
|
:email, :phone
|
|
|
|
t.timestamps
|
|
|
|
end
|
|
|
|
create_table Task.table_name do |t|
|
|
|
|
t.integer :customer_id
|
|
|
|
t.string :name
|
|
|
|
t.timestamps
|
|
|
|
end
|
|
|
|
create_table TimeEntry.table_name do |t|
|
2011-11-07 10:24:12 +01:00
|
|
|
t.integer :task_id, :invoice_id
|
2011-10-31 14:36:01 +01:00
|
|
|
t.datetime :start, :end
|
|
|
|
t.timestamps
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def self.down
|
|
|
|
drop_table Customer.table_name
|
|
|
|
drop_table Task.table_name
|
|
|
|
drop_table TimeEntry.table_name
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2011-11-03 22:23:50 +01:00
|
|
|
class CommentSupport < V 1.1
|
|
|
|
def self.up
|
|
|
|
add_column(TimeEntry.table_name, :comment, :string)
|
|
|
|
end
|
|
|
|
|
|
|
|
def self.down
|
|
|
|
remove_column(TimeEntry.table_name, :comment)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2011-11-03 23:07:42 +01:00
|
|
|
class BilledFlagSupport < V 1.2
|
|
|
|
def self.up
|
|
|
|
add_column(TimeEntry.table_name, :bill, :boolean)
|
|
|
|
end
|
|
|
|
|
|
|
|
def self.down
|
|
|
|
remove_column(TimeEntry.table_name, :bill)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2011-11-03 23:44:06 +01:00
|
|
|
class HourlyRateSupport < V 1.3
|
|
|
|
def self.up
|
|
|
|
add_column(Customer.table_name, :hourly_rate, :float,
|
|
|
|
:null => false, :default => HourlyRate)
|
|
|
|
end
|
|
|
|
|
|
|
|
def self.down
|
|
|
|
remove_column(Customer.table_name, :hourly_rate)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2011-11-07 10:24:12 +01:00
|
|
|
class FixedCostTaskSupport < V 1.4
|
|
|
|
def self.up
|
|
|
|
add_column(Task.table_name, :billed, :boolean)
|
2011-11-07 10:44:35 +01:00
|
|
|
add_column(Task.table_name, :fixed_cost, :float)
|
2011-11-07 13:38:07 +01:00
|
|
|
add_column(Task.table_name, :hourly_rate, :float)
|
2011-11-07 10:24:12 +01:00
|
|
|
end
|
|
|
|
|
|
|
|
def self.down
|
2011-11-07 10:44:35 +01:00
|
|
|
remove_column(Task.table_name, :billed)
|
|
|
|
remove_column(Task.table_name, :fixed_cost)
|
2011-11-07 13:38:07 +01:00
|
|
|
remove_column(Task.table_name, :hourly_rate, :float)
|
2011-11-07 10:24:12 +01:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
class InvoiceSupport < V 1.5
|
|
|
|
def self.up
|
|
|
|
create_table Invoice.table_name do |t|
|
|
|
|
t.integer :number, :customer_id
|
|
|
|
t.boolean :payed
|
|
|
|
t.timestamps
|
|
|
|
end
|
|
|
|
add_column(TimeEntry.table_name, :invoice_id, :integer)
|
|
|
|
end
|
|
|
|
|
|
|
|
def self.down
|
|
|
|
drop_table Invoice.table_name
|
|
|
|
remove_column(TimeEntry.table_name, :invoice_id)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2011-11-07 14:54:11 +01:00
|
|
|
class CompanyInfoSupport < V 1.6
|
|
|
|
def self.up
|
|
|
|
create_table CompanyInfo.table_name do |t|
|
|
|
|
t.string :name, :contact_name,
|
|
|
|
:address_street, :address_postal_code, :address_city,
|
|
|
|
:country, :country_code,
|
|
|
|
:phone, :cell, :email, :website,
|
|
|
|
:chamber, :vatno, :accountname, :accountno
|
|
|
|
t.timestamps
|
|
|
|
end
|
|
|
|
|
|
|
|
# Add company info record with defaults.
|
|
|
|
cinfo = CompanyInfo.create(:name => "My Company",
|
|
|
|
:contact_name => "Me",
|
|
|
|
:country => "The Netherlands",
|
|
|
|
:country_code => "NL")
|
|
|
|
cinfo.save
|
|
|
|
end
|
|
|
|
|
|
|
|
def self.down
|
|
|
|
drop_table CompanyInfo.table_name
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2011-11-03 22:23:50 +01:00
|
|
|
end # StopTime::Models
|
2011-10-31 14:36:01 +01:00
|
|
|
|
|
|
|
module StopTime::Controllers
|
|
|
|
|
|
|
|
class Index
|
|
|
|
def get
|
2011-11-01 15:29:55 +01:00
|
|
|
redirect R(Timereg)
|
2011-10-31 14:36:01 +01:00
|
|
|
end
|
|
|
|
end
|
2011-10-31 16:14:54 +01:00
|
|
|
|
|
|
|
class Customers
|
|
|
|
def get
|
|
|
|
@customers = Customer.all
|
|
|
|
render :customers
|
|
|
|
end
|
|
|
|
|
|
|
|
def post
|
|
|
|
return redirect R(Customers) if @input.cancel
|
|
|
|
@customer = Customer.create(
|
|
|
|
:name => @input.name,
|
|
|
|
:short_name => @input.short_name,
|
|
|
|
:address_street => @input.address_street,
|
|
|
|
:address_postal_code => @input.address_postal_code,
|
|
|
|
:address_city => @input.address_city,
|
|
|
|
:email => @input.email,
|
2011-11-03 23:44:06 +01:00
|
|
|
:phone => @input.phone,
|
|
|
|
:hourly_rate => @input.hourly_rate)
|
2011-10-31 16:14:54 +01:00
|
|
|
@customer.save
|
|
|
|
if @customer.invalid?
|
|
|
|
@errors = @customer.errors
|
2011-11-03 23:44:06 +01:00
|
|
|
return render :customer_form
|
2011-10-31 16:14:54 +01:00
|
|
|
end
|
|
|
|
redirect R(Customers)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2011-11-01 15:29:24 +01:00
|
|
|
class CustomersNew
|
|
|
|
def get
|
2011-11-07 13:40:24 +01:00
|
|
|
# FIXME: set other defaults?
|
2011-11-07 14:54:11 +01:00
|
|
|
@customer = Customer.new(:hourly_rate => HourlyRate)
|
2011-11-07 10:44:35 +01:00
|
|
|
@target = [Customers]
|
2011-11-01 15:29:24 +01:00
|
|
|
render :customer_form
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
class CustomersN
|
2011-10-31 16:14:54 +01:00
|
|
|
def get(customer_id)
|
|
|
|
@customer = Customer.find(customer_id)
|
2011-11-07 10:44:35 +01:00
|
|
|
@edit_task = true
|
|
|
|
@target = [CustomersN, @customer.id]
|
2011-11-07 13:40:24 +01:00
|
|
|
@input = @customer
|
2011-11-01 15:29:24 +01:00
|
|
|
render :customer_form
|
2011-10-31 16:14:54 +01:00
|
|
|
end
|
|
|
|
|
|
|
|
def post(customer_id)
|
|
|
|
return redirect R(Customers) if @input.cancel
|
|
|
|
@customer = Customer.find(customer_id)
|
2011-11-01 15:29:24 +01:00
|
|
|
if @input.has_key? "delete"
|
|
|
|
@customer.delete
|
2011-11-07 11:12:12 +01:00
|
|
|
elsif @input.has_key? "update"
|
2011-11-01 15:29:24 +01:00
|
|
|
attrs = ["name", "short_name",
|
|
|
|
"address_street", "address_postal_code", "address_city",
|
2011-11-03 23:44:06 +01:00
|
|
|
"email", "phone", "hourly_rate"]
|
2011-11-01 15:29:24 +01:00
|
|
|
attrs.each do |attr|
|
2011-11-07 14:54:11 +01:00
|
|
|
@customer[attr] = @input[attr]
|
2011-11-01 15:29:24 +01:00
|
|
|
end
|
|
|
|
@customer.save
|
|
|
|
if @customer.invalid?
|
|
|
|
@errors = @customer.errors
|
|
|
|
return render :customer_form
|
|
|
|
end
|
|
|
|
end
|
|
|
|
redirect R(Customers)
|
2011-10-31 16:14:54 +01:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2011-11-01 15:29:24 +01:00
|
|
|
class CustomersNTasks
|
|
|
|
def post(customer_id)
|
2011-11-07 13:40:43 +01:00
|
|
|
if @input.has_key? "delete"
|
|
|
|
@task = Task.find(@input.task_id)
|
|
|
|
@task.delete
|
|
|
|
elsif @input.has_key? "edit"
|
|
|
|
return redirect R(CustomersNTasksN, customer_id, @input.task_id)
|
|
|
|
else
|
2011-11-01 15:29:24 +01:00
|
|
|
@task = Task.create(
|
|
|
|
:customer_id => customer_id,
|
2011-11-07 13:40:43 +01:00
|
|
|
:name => @input.name)
|
|
|
|
case @input.task_type
|
|
|
|
when "fixed_cost"
|
|
|
|
@task.fixed_cost = @input.fixed_cost
|
|
|
|
@task.hourly_rate = nil
|
|
|
|
when "hourly_rate"
|
|
|
|
@task.fixed_cost = nil
|
|
|
|
@task.hourly_rate = @input.hourly_rate
|
|
|
|
# FIXME: catch invalid task types!
|
|
|
|
end
|
2011-11-01 15:29:24 +01:00
|
|
|
@task.save
|
|
|
|
if @task.invalid?
|
|
|
|
@errors = @task.errors
|
2011-11-07 13:40:43 +01:00
|
|
|
@customer = Customer.find(customer_id)
|
|
|
|
@target = [CustomersNTasks, customer_id]
|
|
|
|
@method = "create"
|
|
|
|
return render :task_form
|
|
|
|
end
|
|
|
|
end
|
|
|
|
redirect R(CustomersN, customer_id)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
class CustomersNTasksNew
|
|
|
|
def get(customer_id)
|
|
|
|
@customer = Customer.find(customer_id)
|
2011-11-07 14:54:11 +01:00
|
|
|
@task = Task.new(:hourly_rate => @customer.hourly_rate)
|
2011-11-07 13:40:43 +01:00
|
|
|
@target = [CustomersNTasks, customer_id]
|
|
|
|
@method = "create"
|
|
|
|
@input = @task
|
|
|
|
render :task_form
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
class CustomersNTasksN
|
|
|
|
def get(customer_id, task_id)
|
|
|
|
@customer = Customer.find(customer_id)
|
|
|
|
@task = Task.find(task_id)
|
|
|
|
@target = [CustomersNTasksN, customer_id, task_id]
|
|
|
|
@method = "update"
|
|
|
|
@input = @task
|
|
|
|
# FIXME: Check that task is of that customer.
|
|
|
|
render :task_form
|
|
|
|
end
|
|
|
|
|
|
|
|
def post(customer_id, task_id)
|
|
|
|
return redirect R(CustomersN, customer_id) if @input.cancel
|
|
|
|
@customer = Customer.find(customer_id)
|
|
|
|
@task = Task.find(task_id)
|
|
|
|
if @input.has_key? "update"
|
|
|
|
# FIXME: task should be cloned/dupped as to prevent rewriting history!
|
|
|
|
@task["name"] = @input["name"] unless @input["name"].blank?
|
|
|
|
case @input.task_type
|
|
|
|
when "fixed_cost"
|
2011-11-07 15:10:15 +01:00
|
|
|
@task.fixed_cost = @input.fixed_cost
|
|
|
|
@task.hourly_rate = nil
|
2011-11-07 13:40:43 +01:00
|
|
|
when "hourly_rate"
|
2011-11-07 15:10:15 +01:00
|
|
|
@task.fixed_cost = nil
|
|
|
|
@task.hourly_rate = @input.hourly_rate
|
2011-11-07 13:40:43 +01:00
|
|
|
end
|
|
|
|
@task["billed"] = @input.has_key? "billed"
|
|
|
|
@task.save
|
|
|
|
if @task.invalid?
|
|
|
|
@errors = @task.errors
|
|
|
|
@target = [CustomersNTasksN, customer_id, task_id]
|
|
|
|
@method = "update"
|
|
|
|
@input = @task
|
|
|
|
return render :task_form
|
2011-11-01 15:29:24 +01:00
|
|
|
end
|
|
|
|
end
|
|
|
|
redirect R(CustomersN, customer_id)
|
2011-10-31 16:14:54 +01:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2011-11-02 22:52:47 +01:00
|
|
|
class CustomersNInvoicesX
|
2011-11-03 11:40:58 +01:00
|
|
|
def get(customer_id, invoice_id)
|
|
|
|
@month = DateTime.new(invoice_id[0..3].to_i, invoice_id[4..5].to_i, 1)
|
|
|
|
@number = invoice_id[6..-1]
|
|
|
|
# FIXME: make this (much) nicer!
|
|
|
|
invoice_id.gsub!(/\.pdf$/, '')
|
|
|
|
if m = @number.match(/(\d+)\.(\w+)$/)
|
|
|
|
@number = m[1].to_i
|
|
|
|
@format = m[2]
|
|
|
|
else
|
|
|
|
@number = @number.to_i
|
|
|
|
@format = "html"
|
|
|
|
end
|
2011-11-02 22:52:47 +01:00
|
|
|
|
2011-11-07 14:54:11 +01:00
|
|
|
@company = CompanyInfo.first
|
2011-11-02 22:52:47 +01:00
|
|
|
@customer = Customer.find(customer_id)
|
2011-11-03 10:30:02 +01:00
|
|
|
@tasks = @customer.task_summary(@month)
|
2011-11-02 22:52:47 +01:00
|
|
|
|
2011-11-03 11:40:58 +01:00
|
|
|
if @format == "html"
|
|
|
|
render :invoice
|
|
|
|
elsif @format == "pdf"
|
|
|
|
pdf_file = PUBLIC_DIR + "#{invoice_id}.pdf"
|
|
|
|
unless pdf_file.exist?
|
|
|
|
_generate_invoice_pdf(@customer, @tasks, @month, invoice_id)
|
|
|
|
end
|
|
|
|
redirect(StaticX, pdf_file.basename)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def _generate_invoice_pdf(customer, tasks, month, invoice_id)
|
|
|
|
template = TEMPLATE_DIR + "invoice.tex.erb"
|
|
|
|
tex_file = PUBLIC_DIR + "#{invoice_id}.tex"
|
|
|
|
|
|
|
|
erb = ERB.new(File.read(template))
|
|
|
|
File.open(tex_file, "w") { |f| f.write(erb.result(binding)) }
|
|
|
|
system("rubber --pdf --inplace #{tex_file}")
|
|
|
|
system("rubber --clean --inplace #{tex_file}")
|
2011-11-02 22:52:47 +01:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2011-11-01 15:29:55 +01:00
|
|
|
class Timereg
|
2011-10-31 16:14:54 +01:00
|
|
|
def get
|
2011-11-02 22:52:47 +01:00
|
|
|
@time_entries = TimeEntry.all(:order => "start DESC")
|
2011-11-01 15:29:55 +01:00
|
|
|
@customer_list = Customer.all.map { |c| [c.id, c.short_name] }
|
|
|
|
@task_list = Task.all.map { |t| [t.id, t.name] }
|
|
|
|
render :time_entries
|
2011-10-31 16:14:54 +01:00
|
|
|
end
|
|
|
|
|
2011-11-01 15:29:55 +01:00
|
|
|
def post
|
|
|
|
if @input.has_key? "enter"
|
|
|
|
@entry = TimeEntry.create(
|
|
|
|
:task_id => @input.task,
|
|
|
|
:start => @input.start,
|
2011-11-03 22:23:50 +01:00
|
|
|
:end => @input.end,
|
2011-11-03 23:07:42 +01:00
|
|
|
:comment => @input.comment,
|
|
|
|
:bill => @input.has_key?("bill"))
|
2011-11-01 15:29:55 +01:00
|
|
|
@entry.save
|
|
|
|
if @entry.invalid?
|
|
|
|
@errors = @entry.errors
|
|
|
|
end
|
|
|
|
elsif @input.has_key? "delete"
|
|
|
|
end
|
|
|
|
|
2011-11-02 22:52:47 +01:00
|
|
|
@time_entries = TimeEntry.all(:order => "start DESC")
|
2011-11-01 15:29:55 +01:00
|
|
|
@customer_list = Customer.all.map { |c| [c.id, c.short_name] }
|
|
|
|
@task_list = Task.all.map { |t| [t.id, t.name] }
|
2011-10-31 16:14:54 +01:00
|
|
|
render :time_entries
|
|
|
|
end
|
|
|
|
end
|
2011-11-01 15:29:55 +01:00
|
|
|
|
|
|
|
class TimeregN
|
|
|
|
def post(entry_id)
|
|
|
|
TimeEntry.find(entry_id).delete
|
|
|
|
redirect R(Timereg)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
class Invoices
|
|
|
|
def get
|
2011-11-02 22:52:47 +01:00
|
|
|
@time_entries = TimeEntry.all(:order => "start ASC")
|
|
|
|
@customers = Hash.new { |h, k| h[k] = Array.new }
|
|
|
|
|
|
|
|
@time_entries.each do |e|
|
|
|
|
month = e.start.at_beginning_of_month
|
|
|
|
customer = e.task.customer
|
|
|
|
unless @customers[month].include? customer
|
|
|
|
@customers[month] << customer
|
|
|
|
end
|
|
|
|
end
|
2011-11-01 15:29:55 +01:00
|
|
|
render :invoices
|
|
|
|
end
|
|
|
|
end
|
2011-11-03 11:00:35 +01:00
|
|
|
|
2011-11-07 14:54:11 +01:00
|
|
|
class Company
|
|
|
|
def get
|
|
|
|
@company = CompanyInfo.first
|
|
|
|
@input = @company
|
|
|
|
render :company_form
|
|
|
|
end
|
|
|
|
|
|
|
|
def post
|
|
|
|
@company = CompanyInfo.first
|
|
|
|
attrs = ["name", "contact_name",
|
|
|
|
"address_street", "address_postal_code", "address_city",
|
|
|
|
"country", "country_code",
|
|
|
|
"phone", "cell", "email", "website",
|
|
|
|
"chamber", "vatno", "accountname", "accountno"]
|
|
|
|
attrs.each do |attr|
|
|
|
|
@company[attr] = @input[attr]
|
|
|
|
end
|
|
|
|
@company.save
|
|
|
|
if @company.invalid?
|
|
|
|
@errors = @company.errors
|
|
|
|
end
|
|
|
|
render :company_form
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2011-11-03 11:00:35 +01:00
|
|
|
class StaticX
|
|
|
|
def get(path)
|
|
|
|
mime_type = MIME::Types.type_for(path).first
|
|
|
|
@headers['Content-Type'] = mime_type.nil? ? "text/plain" : mime_type.to_s
|
|
|
|
unless path.include? ".."
|
2011-11-03 11:39:58 +01:00
|
|
|
@headers['X-Sendfile'] = (PUBLIC_DIR + path).to_s
|
2011-11-03 11:00:35 +01:00
|
|
|
else
|
|
|
|
@status = "403"
|
|
|
|
"Error 403: Invalid path: #{path}"
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2011-10-31 14:36:01 +01:00
|
|
|
end # module StopTime::Controllers
|
|
|
|
|
|
|
|
module StopTime::Views
|
|
|
|
|
|
|
|
def layout
|
|
|
|
xhtml_strict do
|
|
|
|
head do
|
|
|
|
title "Stop… Camping Time!"
|
|
|
|
end
|
|
|
|
body do
|
|
|
|
div.wrapper! do
|
2011-11-01 15:29:55 +01:00
|
|
|
h1 "Stop… Camping Time!"
|
|
|
|
_menu
|
|
|
|
div.content! do
|
|
|
|
self << yield
|
|
|
|
end
|
2011-10-31 14:36:01 +01:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2011-11-01 15:29:55 +01:00
|
|
|
def _menu
|
|
|
|
ol.menu! do
|
2011-11-07 14:54:11 +01:00
|
|
|
li { a "Overview", :href => R(Index) }
|
2011-11-01 15:29:55 +01:00
|
|
|
li { a "Time Registration", :href => R(Timereg) }
|
|
|
|
li { a "Customers", :href => R(Customers) }
|
|
|
|
li { a "Invoices", :href => R(Invoices) }
|
2011-11-07 14:54:11 +01:00
|
|
|
li { a "Company", :href => R(Company) }
|
2011-11-01 15:29:55 +01:00
|
|
|
end
|
2011-10-31 16:14:54 +01:00
|
|
|
end
|
|
|
|
|
2011-11-01 15:29:55 +01:00
|
|
|
def time_entries
|
|
|
|
h2 "List of time entries"
|
|
|
|
table do
|
|
|
|
tr do
|
|
|
|
th "Customer"
|
|
|
|
th "Project/task"
|
|
|
|
th "Start time"
|
|
|
|
th "End time"
|
2011-11-03 22:23:50 +01:00
|
|
|
th "Comment"
|
|
|
|
th "Total time"
|
2011-11-03 23:07:42 +01:00
|
|
|
th "Bill?"
|
2011-11-01 15:29:55 +01:00
|
|
|
end
|
|
|
|
form :action => R(Timereg), :method => :post do
|
|
|
|
tr do
|
|
|
|
td { _form_select("customer", @customer_list) }
|
|
|
|
td { _form_select("task", @task_list) }
|
2011-11-02 22:52:47 +01:00
|
|
|
td { input :type => :text, :name => "start",
|
|
|
|
:value => DateTime.now.to_date.to_formatted_s + " " }
|
|
|
|
td { input :type => :text, :name => "end",
|
|
|
|
:value => DateTime.now.to_date.to_formatted_s + " " }
|
2011-11-03 22:23:50 +01:00
|
|
|
td { input :type => :text, :name => "comment" }
|
2011-11-01 15:29:55 +01:00
|
|
|
td { "N/A" }
|
2011-11-07 13:40:24 +01:00
|
|
|
td { _form_input_checkbox("bill", "bill") }
|
2011-11-01 15:29:55 +01:00
|
|
|
td do
|
|
|
|
input :type => :submit, :name => "enter", :value => "Enter"
|
|
|
|
input :type => :reset, :name => "clear", :value => "Clear"
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
@time_entries.each do |entry|
|
|
|
|
tr do
|
2011-11-07 13:40:24 +01:00
|
|
|
td { a entry.customer.short_name,
|
|
|
|
:href => R(CustomersN, entry.customer.id) }
|
|
|
|
td { a entry.task.name,
|
|
|
|
:href => R(CustomersNTasksN, entry.customer.id, entry.task.id) }
|
2011-11-01 15:29:55 +01:00
|
|
|
td { entry.start }
|
|
|
|
td { entry.end }
|
2011-11-03 22:23:50 +01:00
|
|
|
td { entry.comment }
|
2011-11-01 15:29:55 +01:00
|
|
|
td { "%.2fh" % ((entry.end - entry.start)/3600.0) }
|
2011-11-07 13:40:24 +01:00
|
|
|
td do
|
|
|
|
if entry.bill
|
|
|
|
input :type => "checkbox", :name => "bill_#{entry.id}",
|
|
|
|
:checked => true, :disabled => true
|
2011-11-03 23:07:42 +01:00
|
|
|
else
|
2011-11-07 13:40:24 +01:00
|
|
|
input :type => "checkbox", :name => "bill_#{entry.id}",
|
|
|
|
:disabled => true
|
2011-11-03 23:07:42 +01:00
|
|
|
end
|
|
|
|
end
|
2011-11-01 15:29:55 +01:00
|
|
|
td do
|
|
|
|
form :action => R(TimeregN, entry.id), :method => :post do
|
|
|
|
input :type => :submit, :name => "delete", :value => "Delete"
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
2011-10-31 16:14:54 +01:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def customers
|
2011-11-01 15:29:24 +01:00
|
|
|
h2 "List of customers"
|
2011-10-31 16:14:54 +01:00
|
|
|
table do
|
|
|
|
tr do
|
|
|
|
th "Name"
|
|
|
|
th "Short name"
|
2011-11-01 15:29:24 +01:00
|
|
|
th "Address"
|
2011-10-31 16:14:54 +01:00
|
|
|
th "Email"
|
|
|
|
th "Phone"
|
|
|
|
end
|
|
|
|
@customers.each do |customer|
|
|
|
|
tr do
|
2011-11-07 13:40:24 +01:00
|
|
|
td { a customer.name, :href => R(CustomersN, customer.id) }
|
2011-10-31 16:14:54 +01:00
|
|
|
td { customer.short_name }
|
|
|
|
td { [customer.address_street,
|
|
|
|
customer.address_postal_code,
|
2011-11-01 15:29:24 +01:00
|
|
|
customer.address_city].join(", ") unless customer.address_street.blank? }
|
2011-11-07 13:40:24 +01:00
|
|
|
td { a customer.email, :href => "mailto:#{customer.email}" }
|
2011-11-01 15:29:24 +01:00
|
|
|
td { customer.phone }
|
|
|
|
td do
|
|
|
|
form :action => R(CustomersN, customer.id), :method => :post do
|
|
|
|
input :type => :submit, :name => "delete", :value => "Delete"
|
|
|
|
end
|
|
|
|
end
|
2011-10-31 16:14:54 +01:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
p do
|
2011-11-01 15:29:24 +01:00
|
|
|
a "Add a new customer", :href=> R(CustomersNew)
|
2011-10-31 16:14:54 +01:00
|
|
|
end
|
2011-10-31 14:36:01 +01:00
|
|
|
end
|
|
|
|
|
2011-11-01 15:29:24 +01:00
|
|
|
def customer_form
|
2011-11-07 10:44:35 +01:00
|
|
|
form :action => R(*@target), :method => :post do
|
2011-10-31 16:14:54 +01:00
|
|
|
ol do
|
2011-11-07 14:54:11 +01:00
|
|
|
li { _form_input_with_label("Name", "name", :text) }
|
|
|
|
li { _form_input_with_label("Short name", "short_name", :text) }
|
|
|
|
li { _form_input_with_label("Street address", "address_street", :text) }
|
|
|
|
li { _form_input_with_label("Postal code", "address_postal_code", :text) }
|
|
|
|
li { _form_input_with_label("City/town", "address_city", :text) }
|
|
|
|
li { _form_input_with_label("Email address", "email", :text) }
|
|
|
|
li { _form_input_with_label("Phone number", "phone", :text) }
|
|
|
|
li { _form_input_with_label("Hourly rate", "hourly_rate", :text) }
|
2011-10-31 16:14:54 +01:00
|
|
|
end
|
2011-11-07 11:12:12 +01:00
|
|
|
input :type => "submit", :name => "update", :value => "Update"
|
2011-10-31 16:14:54 +01:00
|
|
|
input :type => "submit", :name => "cancel", :value => "Cancel"
|
|
|
|
end
|
2011-11-01 15:29:55 +01:00
|
|
|
if @edit_task
|
2011-11-07 13:40:43 +01:00
|
|
|
# FXIME: the following is not very RESTful!
|
2011-11-01 15:29:55 +01:00
|
|
|
form :action => R(CustomersNTasks, @customer.id), :method => :post do
|
|
|
|
h2 "Projects & Tasks"
|
2011-11-07 13:40:43 +01:00
|
|
|
select :name => "task_id", :size => 6 do
|
2011-11-01 15:29:55 +01:00
|
|
|
@customer.tasks.each do |task|
|
|
|
|
option(:value => task.id) { task.name }
|
|
|
|
end
|
|
|
|
end
|
2011-11-07 13:40:24 +01:00
|
|
|
input :type => :submit, :name => "edit", :value => "Edit"
|
2011-11-01 15:29:55 +01:00
|
|
|
input :type => :submit, :name => "delete", :value => "Delete"
|
|
|
|
end
|
2011-11-07 13:40:43 +01:00
|
|
|
a "Add a new project/task", :href => R(CustomersNTasksNew, @customer.id)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def task_form
|
2011-11-07 15:10:15 +01:00
|
|
|
# FIXME: it's not always new
|
2011-11-07 13:40:43 +01:00
|
|
|
h2 "New task for #{@customer.name}"
|
2011-11-07 15:10:15 +01:00
|
|
|
|
2011-11-07 13:40:43 +01:00
|
|
|
form :action => R(*@target), :method => :post do
|
|
|
|
ul do
|
2011-11-07 14:54:11 +01:00
|
|
|
li { _form_input_with_label("Name", "name", :text) }
|
2011-11-07 13:40:43 +01:00
|
|
|
li do
|
|
|
|
ol.radio do
|
|
|
|
li do
|
|
|
|
_form_input_radio("task_type", "hourly_rate", default=true)
|
2011-11-07 14:54:11 +01:00
|
|
|
_form_input_with_label("Hourly rate", "hourly_rate", :text)
|
2011-11-07 13:40:43 +01:00
|
|
|
end
|
|
|
|
li do
|
|
|
|
_form_input_radio("task_type", "fixed_cost")
|
2011-11-07 14:54:11 +01:00
|
|
|
_form_input_with_label("Fixed cost", "fixed_cost", :text)
|
2011-11-07 13:40:43 +01:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
li do
|
|
|
|
if @input.billed?
|
|
|
|
input :type => :checkbox, :name => "billed", :checked => true
|
|
|
|
else
|
|
|
|
input :type => :checkbox, :name => "billed"
|
|
|
|
end
|
|
|
|
span "Billed"
|
|
|
|
end
|
|
|
|
end
|
|
|
|
input :type => "submit", :name => @method, :value => @method.capitalize
|
|
|
|
input :type => "submit", :name => "cancel", :value => "Cancel"
|
2011-11-01 15:29:55 +01:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def invoices
|
|
|
|
h2 "List of invoices"
|
2011-11-02 22:52:47 +01:00
|
|
|
|
|
|
|
cmonth = Time.now
|
2011-11-03 11:00:52 +01:00
|
|
|
ccnt = 1
|
2011-11-02 22:52:47 +01:00
|
|
|
@customers.each do |month, custs|
|
|
|
|
unless month == cmonth
|
|
|
|
h3 { month.to_formatted_s(:month_and_year) }
|
|
|
|
cmonth = month
|
2011-11-03 11:00:52 +01:00
|
|
|
ccnt = 1
|
2011-11-02 22:52:47 +01:00
|
|
|
end
|
|
|
|
ol do
|
|
|
|
custs.each do |cust|
|
|
|
|
li do
|
|
|
|
span { cust.name }
|
|
|
|
a "view", :href => R(CustomersNInvoicesX,
|
2011-11-03 11:00:52 +01:00
|
|
|
cust.id, month.to_formatted_s(:month_code) +
|
|
|
|
"%02d" % ccnt)
|
2011-11-02 22:52:47 +01:00
|
|
|
end
|
2011-11-03 11:00:52 +01:00
|
|
|
ccnt = ccnt + 1
|
2011-11-02 22:52:47 +01:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def invoice
|
|
|
|
h2 { "Invoice for #{@customer.name}, month
|
|
|
|
#{@month.to_formatted_s(:month_and_year)}" }
|
|
|
|
|
|
|
|
table do
|
|
|
|
tr do
|
|
|
|
th { "Description" }
|
|
|
|
th { "Number of hours" }
|
|
|
|
th { "Hourly rate" }
|
|
|
|
th { "Amount" }
|
|
|
|
end
|
2011-11-03 22:17:18 +01:00
|
|
|
subtotal = 0.0
|
2011-11-02 22:52:47 +01:00
|
|
|
@tasks.each do |task, line|
|
|
|
|
tr do
|
|
|
|
td { task.name }
|
|
|
|
td { "%.2fh" % line[0] }
|
|
|
|
td { "€ %.2f" % line[1] }
|
|
|
|
td { "€ %.2f" % line[2] }
|
|
|
|
end
|
2011-11-03 22:17:18 +01:00
|
|
|
subtotal += line[2]
|
|
|
|
end
|
|
|
|
tr do
|
|
|
|
td { i "Sub-total" }
|
|
|
|
td ""
|
|
|
|
td ""
|
|
|
|
td { "€ %.2f" % subtotal }
|
|
|
|
end
|
|
|
|
vat = subtotal * VATRate/100
|
|
|
|
tr do
|
|
|
|
td { i "VAT #{VATRate}%" }
|
|
|
|
td ""
|
|
|
|
td ""
|
|
|
|
td { "€ %.2f" % vat }
|
2011-11-02 22:52:47 +01:00
|
|
|
end
|
|
|
|
tr do
|
|
|
|
td { b "Total amount" }
|
|
|
|
td ""
|
|
|
|
td ""
|
2011-11-03 22:17:18 +01:00
|
|
|
td { "€ %.2f" % (subtotal + vat) }
|
2011-11-02 22:52:47 +01:00
|
|
|
end
|
|
|
|
end
|
2011-10-31 16:14:54 +01:00
|
|
|
end
|
|
|
|
|
2011-11-07 14:54:11 +01:00
|
|
|
def company_form
|
|
|
|
h2 "Company Information"
|
|
|
|
|
|
|
|
if @errors
|
|
|
|
div.form_errors do
|
|
|
|
h3 "There were #{@errors.count} errors in the form!"
|
|
|
|
ul do
|
|
|
|
@errors.each do |attrib, msg|
|
|
|
|
li "#{attrib.to_s.capitalize} #{msg}"
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
form :action => R(Company), :method => :post do
|
|
|
|
ol do
|
|
|
|
li { _form_input_with_label("Name", "name", :text) }
|
|
|
|
li { _form_input_with_label("Contact name", "contact_name", :text) }
|
|
|
|
li { _form_input_with_label("Street address", "address_street", :text) }
|
|
|
|
li { _form_input_with_label("Postal code", "address_postal_code", :text) }
|
|
|
|
li { _form_input_with_label("City/town", "address_city", :text) }
|
|
|
|
li { _form_input_with_label("Phone number", "phone", :text) }
|
|
|
|
li { _form_input_with_label("Cellular number", "cell", :text) }
|
|
|
|
li { _form_input_with_label("Email address", "email", :text) }
|
|
|
|
li { _form_input_with_label("Web address", "website", :text) }
|
|
|
|
li { _form_input_with_label("Chamber number", "chamber", :text) }
|
|
|
|
li { _form_input_with_label("VAT number", "vatno", :text) }
|
|
|
|
li { _form_input_with_label("Account name", "accountname", :text) }
|
|
|
|
li { _form_input_with_label("Account number", "accountno", :text) }
|
|
|
|
end
|
|
|
|
input :type => "submit", :name => "update", :value => "Update"
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def _form_input_with_label(label_name, input_name, type)
|
2011-10-31 16:14:54 +01:00
|
|
|
label label_name, :for => input_name
|
|
|
|
input :type => type, :name => input_name, :id => input_name,
|
2011-11-07 15:10:15 +01:00
|
|
|
:value => @input.send(input_name)
|
2011-10-31 16:14:54 +01:00
|
|
|
end
|
|
|
|
|
2011-11-07 13:39:24 +01:00
|
|
|
def _form_input_radio(name, value, default=false)
|
2011-11-07 15:10:15 +01:00
|
|
|
input_val = @input.send(name)
|
|
|
|
if input_val == value or (input_val.blank? and default)
|
2011-11-07 13:39:24 +01:00
|
|
|
input :type => "radio", :id => "#{name}_#{value}",
|
|
|
|
:name => name, :value => value, :checked => true
|
|
|
|
else
|
|
|
|
input :type => "radio", :id => "#{name}_#{value}",
|
|
|
|
:name => name, :value => value
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def _form_input_checkbox(name, value="true")
|
2011-11-07 15:10:15 +01:00
|
|
|
if @input.send(name) == value
|
2011-11-07 13:39:24 +01:00
|
|
|
input :type => "checkbox", :id => name, :name => name,
|
|
|
|
:value => value, :checked => true
|
|
|
|
else
|
|
|
|
input :type => "checkbox", :id => name, :name => name,
|
|
|
|
:value => value
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def _form_select(name, opts_list)
|
2011-11-01 15:29:24 +01:00
|
|
|
select :name => name, :id => name do
|
2011-11-07 13:39:24 +01:00
|
|
|
opts_list.each do |opt_val, opt_str|
|
2011-11-07 15:10:15 +01:00
|
|
|
if @input.send(name) == opt_val
|
2011-11-07 13:39:24 +01:00
|
|
|
option opt_str, :value => opt_val, :selected => true
|
2011-11-01 15:29:24 +01:00
|
|
|
else
|
2011-11-07 13:39:24 +01:00
|
|
|
option opt_str, :value => opt_val
|
2011-11-01 15:29:24 +01:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2011-10-31 14:36:01 +01:00
|
|
|
end # module StopTime::Views
|