2014-11-01 21:37:41 +01:00
|
|
|
|
#!usr/bin/env camping
|
2012-06-06 11:28:59 +02:00
|
|
|
|
# encoding: UTF-8
|
2011-10-31 14:36:01 +01:00
|
|
|
|
#
|
2011-11-15 17:54:09 +01:00
|
|
|
|
# stoptime.rb - The Stop… Camping Time! time registration and invoicing application.
|
2011-10-31 14:36:01 +01:00
|
|
|
|
#
|
|
|
|
|
# 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-09 18:31:23 +01:00
|
|
|
|
require "action_view"
|
2011-11-01 15:27:16 +01:00
|
|
|
|
require "active_support"
|
2011-10-31 14:36:01 +01:00
|
|
|
|
require "camping"
|
2012-01-30 12:57:15 +01:00
|
|
|
|
require "camping/mab"
|
2012-01-25 15:52:46 +01:00
|
|
|
|
require "camping/ar"
|
2011-10-31 14:36:01 +01:00
|
|
|
|
require "pathname"
|
2011-11-09 22:55:59 +01:00
|
|
|
|
require "sass/plugin/rack"
|
2011-10-31 14:36:01 +01:00
|
|
|
|
|
|
|
|
|
Camping.goes :StopTime
|
|
|
|
|
|
2011-11-03 11:39:58 +01:00
|
|
|
|
unless defined? PUBLIC_DIR
|
2011-11-10 18:24:11 +01:00
|
|
|
|
# The directory with public data.
|
2011-11-03 11:39:58 +01:00
|
|
|
|
PUBLIC_DIR = Pathname.new(__FILE__).dirname.expand_path + "public"
|
2011-11-10 18:24:11 +01:00
|
|
|
|
# The directory with template data.
|
2011-11-03 11:39:58 +01:00
|
|
|
|
TEMPLATE_DIR = Pathname.new(__FILE__).dirname.expand_path + "templates"
|
2011-11-03 10:30:02 +01:00
|
|
|
|
|
2011-11-09 18:31:23 +01:00
|
|
|
|
# Set up the locales.
|
|
|
|
|
I18n.load_path += Dir[ File.join('locale', '*.yml') ]
|
|
|
|
|
|
2011-11-09 22:55:59 +01:00
|
|
|
|
# Set up SASS.
|
|
|
|
|
Sass::Plugin.options[:template_location] = "templates/sass"
|
|
|
|
|
|
2012-06-06 11:28:59 +02:00
|
|
|
|
# Set the default encodings.
|
|
|
|
|
if RUBY_VERSION =~ /^1\.9/
|
|
|
|
|
Encoding.default_external = Encoding::UTF_8
|
|
|
|
|
Encoding.default_internal = Encoding::UTF_8
|
|
|
|
|
end
|
|
|
|
|
|
2011-11-01 15:27:16 +01:00
|
|
|
|
# Set the default date(/time) format.
|
2013-06-06 21:49:20 +02:00
|
|
|
|
Time::DATE_FORMATS.merge!(
|
2011-11-02 22:52:47 +01:00
|
|
|
|
:default => "%Y-%m-%d %H:%M",
|
|
|
|
|
:month_and_year => "%B %Y",
|
2011-11-09 15:13:39 +01:00
|
|
|
|
:date_only => "%Y-%m-%d",
|
2011-11-11 14:55:17 +01:00
|
|
|
|
:time_only => "%H:%M",
|
2011-11-03 11:40:58 +01:00
|
|
|
|
:day_code => "%Y%m%d")
|
2013-06-06 21:49:20 +02:00
|
|
|
|
Date::DATE_FORMATS.merge!(
|
2011-11-02 22:52:47 +01:00
|
|
|
|
:default => "%Y-%m-%d",
|
|
|
|
|
:month_and_year => "%B %Y")
|
2011-12-23 21:17:02 +01:00
|
|
|
|
end
|
2011-11-03 10:30:02 +01:00
|
|
|
|
|
2012-01-02 15:59:06 +01:00
|
|
|
|
# = The main application module
|
|
|
|
|
module StopTime
|
2011-11-10 18:24:11 +01:00
|
|
|
|
|
2014-10-19 21:37:20 +02:00
|
|
|
|
# The version of the application
|
2014-10-31 22:06:41 +01:00
|
|
|
|
VERSION = '1.8'
|
2014-10-19 21:37:20 +02:00
|
|
|
|
puts "Starting Stop… Camping Time! version #{VERSION}"
|
|
|
|
|
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# @return [Hash{String=>Object}] The parsed configuration.
|
2011-12-23 21:17:02 +01:00
|
|
|
|
attr_reader :config
|
|
|
|
|
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# Overrides controller call handler so that the configuration is available
|
2011-12-23 21:17:02 +01:00
|
|
|
|
# for all controllers and views.
|
|
|
|
|
def service(*a)
|
|
|
|
|
@config = StopTime::Models::Config.instance
|
2012-01-20 00:36:19 +01:00
|
|
|
|
@format = @request.path_info[/.([^.]+)/, 1];
|
2013-06-20 22:06:52 +02:00
|
|
|
|
@headers["Content-Type"] = "text/html; charset=utf-8"
|
2011-12-23 21:17:02 +01:00
|
|
|
|
super(*a)
|
|
|
|
|
end
|
2011-11-10 18:24:11 +01:00
|
|
|
|
|
2012-01-02 15:59:06 +01:00
|
|
|
|
# Trap the HUP signal and reload the configuration.
|
|
|
|
|
Signal.trap("HUP") do
|
|
|
|
|
$stderr.puts "I: caught signal HUP, reloading config"
|
|
|
|
|
Models::Config.instance.reload
|
|
|
|
|
end
|
2011-10-31 14:36:01 +01:00
|
|
|
|
|
2012-01-20 00:35:44 +01:00
|
|
|
|
# Add support for PUT and DELETE.
|
|
|
|
|
use Rack::MethodOverride
|
|
|
|
|
|
2011-11-10 18:24:11 +01:00
|
|
|
|
# Enable SASS CSS generation from templates/sass.
|
2011-11-09 23:00:13 +01:00
|
|
|
|
use Sass::Plugin::Rack
|
|
|
|
|
|
2011-11-10 18:24:11 +01:00
|
|
|
|
# Create/migrate the database when needed.
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# @return [void]
|
2011-10-31 14:36:01 +01:00
|
|
|
|
def self.create
|
|
|
|
|
StopTime::Models.create_schema
|
|
|
|
|
end
|
|
|
|
|
|
2014-11-01 21:37:41 +01:00
|
|
|
|
end # module StopTime
|
2011-10-31 14:36:01 +01:00
|
|
|
|
|
2012-01-25 11:31:58 +01:00
|
|
|
|
# = The Stop… Camping Time! Markaby extensions
|
2012-01-30 13:57:51 +01:00
|
|
|
|
module StopTime::Mab
|
2013-06-23 22:28:28 +02:00
|
|
|
|
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# List of support methods supported by the server.
|
2012-01-30 13:57:51 +01:00
|
|
|
|
SUPPORTED = [:get, :post]
|
2012-01-30 13:57:51 +01:00
|
|
|
|
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# When a tag is completed (i.e. closed), perform some post-processing
|
|
|
|
|
# on the tag's attributes.
|
|
|
|
|
#
|
|
|
|
|
# * Fix up URLs in attributes by applying +Camping::Helpers#/+ to them.
|
|
|
|
|
# * Change action methods besides the supported methods (see {SUPPORTED})
|
|
|
|
|
# to +:post+ and add a hidden +_method+ input that still contains
|
|
|
|
|
# the original method.
|
|
|
|
|
# * Replace underscores in the class attribute by dashes (needed for
|
|
|
|
|
# Bootstrap).
|
|
|
|
|
#
|
|
|
|
|
# @param [Mab::Mixin::Tag] tag the tag that is completed
|
|
|
|
|
# @return [Mab::Mixin::Tag] the post-processed tag
|
2012-01-30 13:57:51 +01:00
|
|
|
|
def mab_done(tag)
|
2013-06-23 22:28:28 +02:00
|
|
|
|
attrs = tag._attributes
|
|
|
|
|
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# Fix up URLs (normally done by Camping::Mab::mab_done)
|
2013-06-23 22:28:28 +02:00
|
|
|
|
[:href, :action, :src].map { |a| attrs[a] &&= self/attrs[a] }
|
|
|
|
|
|
2013-06-16 20:25:12 +02:00
|
|
|
|
# Transform underscores into dashs in class names
|
2013-06-23 22:28:28 +02:00
|
|
|
|
if attrs.has_key?(:class) and attrs[:class].present?
|
|
|
|
|
attrs[:class] = attrs[:class].gsub('_', '-')
|
2013-06-16 20:25:12 +02:00
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
# The followin method processing is only for form tags.
|
2013-06-16 15:17:13 +02:00
|
|
|
|
return super unless tag._name == :form
|
2012-01-30 13:57:51 +01:00
|
|
|
|
|
2013-06-23 22:28:28 +02:00
|
|
|
|
meth = attrs[:method]
|
|
|
|
|
attrs[:method] = 'post' if override = !SUPPORTED.include?(meth)
|
2012-01-30 13:57:51 +01:00
|
|
|
|
# Inject a hidden input element with the proper method to the tag block
|
|
|
|
|
# if the form method is unsupported.
|
2013-06-16 15:17:13 +02:00
|
|
|
|
tag._block do |orig_blk|
|
2012-01-30 13:57:51 +01:00
|
|
|
|
input :type => 'hidden', :name => '_method', :value => meth
|
|
|
|
|
orig_blk.call
|
|
|
|
|
end if override
|
2013-06-23 22:28:28 +02:00
|
|
|
|
|
|
|
|
|
return super
|
2012-01-30 13:57:51 +01:00
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
include Mab::Indentation
|
2012-01-25 15:53:24 +01:00
|
|
|
|
|
2014-11-01 21:37:41 +01:00
|
|
|
|
end # module StopTime::Mab
|
2012-01-20 00:35:44 +01:00
|
|
|
|
|
2011-11-10 18:24:11 +01:00
|
|
|
|
# = The Stop… Camping Time! models
|
2011-10-31 14:36:01 +01:00
|
|
|
|
module StopTime::Models
|
|
|
|
|
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# == The configuration class
|
2011-12-23 21:17:02 +01:00
|
|
|
|
#
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# This class contains the application configuration constructed by
|
|
|
|
|
# default options (see {DefaultConfig}) merged with the configuration
|
|
|
|
|
# found in the configuration file (see {ConfigFile}).
|
2011-12-23 21:17:02 +01:00
|
|
|
|
class Config
|
|
|
|
|
|
2012-01-02 15:13:18 +01:00
|
|
|
|
# There should only be a single configuration object (for reloading).
|
2011-12-23 21:17:02 +01:00
|
|
|
|
include Singleton
|
|
|
|
|
|
|
|
|
|
# The default configuation file. (FIXME: shouldn't be hardcoded!)
|
2012-01-02 13:03:45 +01:00
|
|
|
|
ConfigFile = File.dirname(__FILE__) + "/config.yaml"
|
2012-01-02 15:59:06 +01:00
|
|
|
|
|
2011-12-23 21:17:02 +01:00
|
|
|
|
# The default configuration. Note that the configuration of the root
|
|
|
|
|
# will be merged with this configuration.
|
2014-02-07 21:04:08 +01:00
|
|
|
|
DefaultConfig = { "invoice_id" => "%Y%N",
|
|
|
|
|
"invoice_template" => "invoice",
|
|
|
|
|
"hourly_rate" => 20.0,
|
2014-10-25 21:35:53 +02:00
|
|
|
|
"time_resolution" => 1,
|
2014-02-07 21:04:08 +01:00
|
|
|
|
"vat_rate" => 21.0 }
|
2011-12-23 21:17:02 +01:00
|
|
|
|
|
|
|
|
|
# Creates a new configuration object and loads the configuation.
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# by reading the file +config.yaml+ on disk (see {ConfigFile}, parsing
|
|
|
|
|
# it, and performing a merge with the default config (see
|
|
|
|
|
# {DefaultConfig}).
|
2011-12-23 21:17:02 +01:00
|
|
|
|
def initialize
|
2012-01-02 15:13:18 +01:00
|
|
|
|
@config = DefaultConfig.dup
|
2011-12-23 21:17:02 +01:00
|
|
|
|
cfg = nil
|
|
|
|
|
# Read and parse the configuration.
|
|
|
|
|
begin
|
|
|
|
|
File.open(ConfigFile, "r") { |file| cfg = YAML.load(file) }
|
|
|
|
|
rescue => e
|
|
|
|
|
$stderr.puts "E: couldn't read configuration file: #{e}"
|
|
|
|
|
end
|
2012-01-02 13:03:45 +01:00
|
|
|
|
# Merge the loaded config with the default config (if it's a Hash)
|
|
|
|
|
case cfg
|
|
|
|
|
when Hash
|
2012-01-02 15:13:18 +01:00
|
|
|
|
@config.merge! cfg if cfg
|
2012-01-02 13:03:45 +01:00
|
|
|
|
when nil, false
|
|
|
|
|
# It's ok, it is empty.
|
|
|
|
|
else
|
|
|
|
|
$stderr.puts "W: wrong format detected in configuration file!"
|
|
|
|
|
end
|
2011-12-23 21:17:02 +01:00
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
# Reloads the configuration file.
|
|
|
|
|
def reload
|
|
|
|
|
load
|
|
|
|
|
end
|
|
|
|
|
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# Returns the value for the given configuration option.
|
|
|
|
|
#
|
|
|
|
|
# @param [String] option a configuration option
|
|
|
|
|
# @return [Object] the value for the given configuration option
|
|
|
|
|
def [](option)
|
|
|
|
|
@config[option]
|
2012-01-02 15:13:33 +01:00
|
|
|
|
end
|
|
|
|
|
|
2011-12-23 21:17:02 +01:00
|
|
|
|
end # class StopTime::Models::Config
|
|
|
|
|
|
2011-11-10 18:24:11 +01:00
|
|
|
|
# == The customer class
|
|
|
|
|
#
|
2011-11-28 12:46:48 +01:00
|
|
|
|
# This class represents a customer that has projects/tasks
|
2011-11-10 18:24:11 +01:00
|
|
|
|
# for which invoices need to be generated.
|
2011-10-31 14:36:01 +01:00
|
|
|
|
class Customer < Base
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# @!attribute id [r]
|
|
|
|
|
# @return [Fixnum] unique identification number
|
|
|
|
|
# @!attribute name
|
|
|
|
|
# @return [String] official (long) name
|
|
|
|
|
# @!attribute short_name
|
|
|
|
|
# @return [String] abbreviated name
|
|
|
|
|
# @!attribute financial_contact
|
|
|
|
|
# @return [String] name of the financial contact person/department
|
|
|
|
|
# @!attribute address_street
|
|
|
|
|
# @return [String] street part of the address
|
|
|
|
|
# @!attribute address_postal_code
|
|
|
|
|
# @return [String] zip/postal code part of the address
|
|
|
|
|
# @!attribute address_city
|
|
|
|
|
# @return [String] city part of the postal code
|
|
|
|
|
# @!attribute email
|
|
|
|
|
# @return [String] email address
|
|
|
|
|
# @!attribute phone
|
|
|
|
|
# @return [String] phone number
|
|
|
|
|
# @!attribute hourly_rate
|
|
|
|
|
# @return [Float] default hourly rate
|
|
|
|
|
# @!attribute time_specification
|
|
|
|
|
# @return [Boolean] flag whether the customer requires time specifications
|
|
|
|
|
# @!attribute created_at
|
|
|
|
|
# @return [Time] time of creation
|
|
|
|
|
# @!attribute updated_at
|
|
|
|
|
# @return [Time] time of last update
|
|
|
|
|
|
|
|
|
|
# @!attribute invoices
|
|
|
|
|
# @return [Array<Invoice>] associated invoices
|
2011-10-31 14:36:01 +01:00
|
|
|
|
has_many :tasks
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# @!attribute tasks
|
|
|
|
|
# @return [Array<Task>] associated tasks
|
2011-11-09 14:02:33 +01:00
|
|
|
|
has_many :invoices
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# @!attribute time_entries
|
|
|
|
|
# @return [Array<TimeEntry>] associated time entries
|
2011-11-02 22:52:47 +01:00
|
|
|
|
has_many :time_entries, :through => :tasks
|
2011-11-09 14:02:33 +01:00
|
|
|
|
|
2011-12-22 16:32:47 +01:00
|
|
|
|
# Returns the short name if set, otherwise the full name.
|
2014-11-01 21:37:41 +01:00
|
|
|
|
#
|
|
|
|
|
# @return [String] the shortest name
|
2011-12-22 16:32:47 +01:00
|
|
|
|
def shortest_name
|
|
|
|
|
short_name.present? ? short_name : name
|
|
|
|
|
end
|
|
|
|
|
|
2011-11-10 18:24:11 +01:00
|
|
|
|
# Returns a list of tasks that have not been billed via in invoice.
|
2014-11-01 21:37:41 +01:00
|
|
|
|
#
|
|
|
|
|
# @return [Array<Task>] associated unbilled tasks
|
2011-11-09 14:02:33 +01:00
|
|
|
|
def unbilled_tasks
|
2014-10-18 21:25:26 +02:00
|
|
|
|
tasks.where("invoice_id IS NULL").order("name ASC")
|
2011-11-09 14:02:33 +01:00
|
|
|
|
end
|
2014-11-01 17:45:16 +01:00
|
|
|
|
|
|
|
|
|
# Returns a list of tasks that are active, i.e. that have not been
|
|
|
|
|
# billed and are either fixed cost or have some registered time.
|
2014-11-01 21:37:41 +01:00
|
|
|
|
#
|
|
|
|
|
# @return [Array<Task>] associated active tasks
|
2014-11-01 17:45:16 +01:00
|
|
|
|
def active_tasks
|
2014-11-01 21:37:41 +01:00
|
|
|
|
unbilled_tasks.select do |task|
|
|
|
|
|
task.fixed_cost? or task.time_entries.present?
|
|
|
|
|
end
|
2014-11-01 17:45:16 +01:00
|
|
|
|
end
|
2014-11-01 21:37:41 +01:00
|
|
|
|
end # class StopTime::Models::Customer
|
2011-10-31 14:36:01 +01:00
|
|
|
|
|
2011-11-10 18:24:11 +01:00
|
|
|
|
# == The task class
|
|
|
|
|
#
|
|
|
|
|
# This class represents a task (or project) of a customer on which time can
|
|
|
|
|
# be registered.
|
|
|
|
|
# There are two types of classes: with an hourly and with a fixed cost.
|
2011-10-31 14:36:01 +01:00
|
|
|
|
class Task < Base
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# @!attribute id [r]
|
|
|
|
|
# @return [Fixnum] unique identification number
|
|
|
|
|
# @!attribute name
|
|
|
|
|
# @return [String] description
|
|
|
|
|
# @!attribute fixed_cost
|
|
|
|
|
# @return [Float] fixed cost of the task
|
|
|
|
|
# @!attribute hourly_rate
|
|
|
|
|
# @return [Float] hourly rate for the task
|
|
|
|
|
# @!attribute vat_rate
|
|
|
|
|
# @return [Float] VAT rate at time of billing
|
|
|
|
|
# @!attribute invoice_comment
|
|
|
|
|
# @return [String] extra comment for the invoice
|
|
|
|
|
# @!attribute created_at
|
|
|
|
|
# @return [Time] time of creation
|
|
|
|
|
# @!attribute updated_at
|
|
|
|
|
# @return [Time] time of last update
|
|
|
|
|
|
|
|
|
|
# @!attribute customer
|
|
|
|
|
# @return [Customer] associated customer
|
2011-11-01 15:29:55 +01:00
|
|
|
|
belongs_to :customer
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# @!attribute time_entries
|
|
|
|
|
# @return [Array<TimeEntry>] associated registered time entries
|
|
|
|
|
has_many :time_entries
|
|
|
|
|
# @!attribute invoice
|
|
|
|
|
# @return [Invoice, nil] associated invoice if the task is billed,
|
|
|
|
|
# +nil+ otherwise
|
2011-11-09 14:02:33 +01:00
|
|
|
|
belongs_to :invoice
|
2011-11-07 10:24:12 +01:00
|
|
|
|
|
2011-11-10 18:24:11 +01:00
|
|
|
|
# Determines whether the task has a fixed cost.
|
|
|
|
|
# When +false+ is returned, one can assume the task has an hourly rate.
|
2014-11-01 21:37:41 +01:00
|
|
|
|
#
|
|
|
|
|
# @return [Boolean] whether the task has a fixed cost
|
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
|
|
|
|
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# Returns the type of the task
|
|
|
|
|
#
|
|
|
|
|
# @return ["fixed_cose", "hourly_rate"] the type of the task
|
2011-11-09 14:07:31 +01:00
|
|
|
|
def type
|
2011-11-07 13:38:07 +01:00
|
|
|
|
fixed_cost? ? "fixed_cost" : "hourly_rate"
|
|
|
|
|
end
|
2011-11-09 14:02:33 +01:00
|
|
|
|
|
2011-11-10 18:24:11 +01:00
|
|
|
|
# Returns a list of time entries that should be (and are not yet)
|
|
|
|
|
# billed.
|
2014-11-01 21:37:41 +01:00
|
|
|
|
#
|
|
|
|
|
# @return [Array<TimeEntry>] associated billable time entries
|
2011-11-09 14:02:33 +01:00
|
|
|
|
def billable_time_entries
|
2014-10-18 21:25:26 +02:00
|
|
|
|
time_entries.where("bill = 't'").order("start ASC")
|
2011-11-09 14:02:33 +01:00
|
|
|
|
end
|
|
|
|
|
|
2011-11-10 18:24:11 +01:00
|
|
|
|
# Returns the bill period of the task by means of an Array containing
|
|
|
|
|
# the first and last Time object found for registered time on this
|
|
|
|
|
# task.
|
|
|
|
|
# If no time is registered, the last time the task has been updated
|
|
|
|
|
# is returned.
|
2014-11-01 21:37:41 +01:00
|
|
|
|
#
|
|
|
|
|
# @return [Array(Time, Time)] the bill period of the task
|
2011-11-09 14:02:33 +01:00
|
|
|
|
def bill_period
|
|
|
|
|
bte = billable_time_entries
|
|
|
|
|
if bte.empty?
|
2011-11-09 16:02:04 +01:00
|
|
|
|
# FIXME: better defaults?
|
2011-11-09 15:14:09 +01:00
|
|
|
|
[updated_at, updated_at]
|
2011-11-09 14:02:33 +01:00
|
|
|
|
else
|
|
|
|
|
[bte.first.start, bte.last.end]
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
2011-11-10 18:24:11 +01:00
|
|
|
|
# Returns whether the task is billed, i.e. included in an invoice.
|
2014-11-01 21:37:41 +01:00
|
|
|
|
#
|
|
|
|
|
# @return [Boolean] whether the task is billed
|
2011-11-09 16:23:48 +01:00
|
|
|
|
def billed?
|
|
|
|
|
not invoice.nil?
|
|
|
|
|
end
|
|
|
|
|
|
2011-11-10 18:24:11 +01:00
|
|
|
|
# Returns a time and cost summary of the registered time on the task
|
2012-09-28 11:56:36 +02:00
|
|
|
|
# by means of Array of four values.
|
|
|
|
|
# In case of a fixed cost task, the first value is the total of time
|
|
|
|
|
# (in hours), the third value is the fixed cost, and the fourth value
|
|
|
|
|
# is the VAT.
|
2011-11-10 18:24:11 +01:00
|
|
|
|
# In case of a task with an hourly rate, the first value is
|
|
|
|
|
# the total of time (in hours), the second value is the hourly rate,
|
2012-09-28 11:56:36 +02:00
|
|
|
|
# the third value is the total amount (time times rate), and the fourth
|
|
|
|
|
# value is the VAT.
|
2014-11-01 21:37:41 +01:00
|
|
|
|
#
|
|
|
|
|
# @return [Array(Float, Float, Float, Float)] the summary of the task
|
2011-11-09 14:02:33 +01:00
|
|
|
|
def summary
|
|
|
|
|
case type
|
|
|
|
|
when "fixed_cost"
|
2011-11-29 16:47:16 +01:00
|
|
|
|
total = time_entries.inject(0.0) { |summ, te| summ + te.hours_total }
|
2012-09-28 11:56:36 +02:00
|
|
|
|
[total, nil, fixed_cost, fixed_cost * (vat_rate/100.0)]
|
2011-11-09 14:02:33 +01:00
|
|
|
|
when "hourly_rate"
|
2012-09-28 11:56:36 +02:00
|
|
|
|
time_entries.inject([0.0, hourly_rate, 0.0, 0.0]) do |summ, te|
|
|
|
|
|
total_cost = te.hours_total * hourly_rate
|
2011-11-09 15:12:29 +01:00
|
|
|
|
summ[0] += te.hours_total
|
2012-09-28 11:56:36 +02:00
|
|
|
|
summ[2] += total_cost
|
|
|
|
|
summ[3] += total_cost * (vat_rate/100.0)
|
2011-11-09 14:02:33 +01:00
|
|
|
|
summ
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
2011-12-02 22:16:27 +01:00
|
|
|
|
|
|
|
|
|
# Returns an invoice comment if the task is billed and if it is
|
|
|
|
|
# set, otherwise the name.
|
2014-11-01 21:37:41 +01:00
|
|
|
|
#
|
|
|
|
|
# @return [String] the invoice comment or task name (if not billed)
|
2011-12-02 22:16:27 +01:00
|
|
|
|
def comment_or_name
|
|
|
|
|
if billed? and self.invoice_comment.present?
|
|
|
|
|
self.invoice_comment
|
|
|
|
|
else
|
|
|
|
|
self.name
|
|
|
|
|
end
|
|
|
|
|
end
|
2014-11-01 21:37:41 +01:00
|
|
|
|
end # class StopTime::Models::Task
|
2011-10-31 14:36:01 +01:00
|
|
|
|
|
2011-11-10 18:24:11 +01:00
|
|
|
|
# == The time entry class
|
|
|
|
|
#
|
|
|
|
|
# This class represents an amount of time that is registered on a certain
|
|
|
|
|
# task.
|
2011-10-31 14:36:01 +01:00
|
|
|
|
class TimeEntry < Base
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# @!attribute id [r]
|
|
|
|
|
# @return [Fixnum] unique identification number
|
|
|
|
|
# @!attribute date
|
|
|
|
|
# @return [Time] date of the entry
|
|
|
|
|
# @!attribute start
|
|
|
|
|
# @return [Time] start time of the entry
|
|
|
|
|
# @!attribute end
|
|
|
|
|
# @return [Time] finish time of the entry
|
|
|
|
|
# @!attribute bill
|
|
|
|
|
# @return [Boolean] flag whether to bill or not
|
|
|
|
|
# @!attribute comment
|
|
|
|
|
# @return [String] additional comment
|
|
|
|
|
# @!attribute created_at
|
|
|
|
|
# @return [Time] time of creation
|
|
|
|
|
# @!attribute updated_at
|
|
|
|
|
# @return [Time] time of last update
|
|
|
|
|
|
|
|
|
|
# @!attribute task
|
|
|
|
|
# @return [Task] associated task the time is registered for
|
2011-10-31 14:36:01 +01:00
|
|
|
|
belongs_to :task
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# @!attribute customer
|
|
|
|
|
# @return [Customer] associated customer
|
2011-11-07 13:38:07 +01:00
|
|
|
|
has_one :customer, :through => :task
|
2011-11-07 17:41:46 +01:00
|
|
|
|
|
2014-10-25 21:55:23 +02:00
|
|
|
|
before_validation :round_start_end
|
|
|
|
|
|
2014-10-12 17:04:18 +02:00
|
|
|
|
# Returns the total amount of time, the duration, in hours (up to
|
|
|
|
|
# 2 decimals only!).
|
2014-11-01 21:37:41 +01:00
|
|
|
|
#
|
|
|
|
|
# @return [Float] the total amount of registered time
|
2011-11-09 15:12:29 +01:00
|
|
|
|
def hours_total
|
2014-10-12 17:04:18 +02:00
|
|
|
|
((self.end - self.start) / 1.hour).round(2)
|
2011-11-07 17:41:46 +01:00
|
|
|
|
end
|
2014-10-25 21:53:30 +02:00
|
|
|
|
|
2014-11-01 22:09:21 +01:00
|
|
|
|
def in_current_month?
|
|
|
|
|
self.end.month == Time.now.month
|
|
|
|
|
end
|
|
|
|
|
|
2014-10-25 21:55:23 +02:00
|
|
|
|
#########
|
|
|
|
|
protected
|
|
|
|
|
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# Rounds the start and end time to the configured resolution using
|
|
|
|
|
# {#round_time).
|
|
|
|
|
#
|
|
|
|
|
# @return [void]
|
2014-10-25 21:55:23 +02:00
|
|
|
|
def round_start_end
|
|
|
|
|
self.start = round_time(self.start)
|
|
|
|
|
self.end = round_time(self.end)
|
|
|
|
|
end
|
|
|
|
|
|
2014-10-25 21:53:30 +02:00
|
|
|
|
#######
|
|
|
|
|
private
|
|
|
|
|
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# Rounds the time using the configured time resolution.
|
|
|
|
|
#
|
|
|
|
|
# @param [Time] t the input time
|
|
|
|
|
# @return [Time] the rounded time
|
2014-10-25 21:53:30 +02:00
|
|
|
|
def round_time(t)
|
|
|
|
|
config = Config.instance
|
|
|
|
|
res = config["time_resolution"]
|
|
|
|
|
down = t - (t.to_i % res.minutes)
|
|
|
|
|
up = down + res.minutes
|
|
|
|
|
|
|
|
|
|
diff_down = t - down
|
|
|
|
|
diff_up = up - t
|
|
|
|
|
|
|
|
|
|
diff_down < diff_up ? down : up
|
|
|
|
|
end
|
2014-11-01 21:37:41 +01:00
|
|
|
|
end # class StopTime::Models::TimeEntry
|
2011-11-07 10:24:12 +01:00
|
|
|
|
|
2011-11-10 18:24:11 +01:00
|
|
|
|
# == The invoice class
|
|
|
|
|
#
|
|
|
|
|
# This class represents an invoice for a customer that contains billed
|
|
|
|
|
# tasks and through the tasks registered time.
|
2011-11-07 10:24:12 +01:00
|
|
|
|
class Invoice < Base
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# @!attribute id [r]
|
|
|
|
|
# @return [Fixnum] unique identification number
|
|
|
|
|
# @!attribute number
|
|
|
|
|
# @return [Fixnum] invoice number
|
|
|
|
|
# @!attribute paid
|
|
|
|
|
# @return [Boolean] flag whether the invoice has been paid
|
|
|
|
|
# @!attribute include_specification
|
|
|
|
|
# @return [Boolean] flag whether the invoice should include a time
|
|
|
|
|
# specification
|
|
|
|
|
# @!attribute created_at
|
|
|
|
|
# @return [Time] time of creation
|
|
|
|
|
# @!attribute updated_at
|
|
|
|
|
# @return [Time] time of last update
|
|
|
|
|
|
|
|
|
|
# @!attribute company_info
|
|
|
|
|
# @return [CompanyInfo] associated company info
|
|
|
|
|
belongs_to :company_info
|
|
|
|
|
# @!attribute customer
|
|
|
|
|
# @return [Customer] associated customer
|
|
|
|
|
belongs_to :customer
|
|
|
|
|
# @!attribute tasks
|
|
|
|
|
# @return [Array<Task>] associated billed tasks
|
2011-11-09 14:02:33 +01:00
|
|
|
|
has_many :tasks
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# @!attribute time_entries
|
|
|
|
|
# @return [Array<TimeEntry>] associated billed time entries
|
2011-11-09 14:02:33 +01:00
|
|
|
|
has_many :time_entries, :through => :tasks
|
2014-11-01 21:37:41 +01:00
|
|
|
|
|
2014-10-18 21:25:26 +02:00
|
|
|
|
default_scope lambda { order('number DESC') }
|
2011-11-07 17:41:46 +01:00
|
|
|
|
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# Returns a time and cost summary for each of the associated tasks.
|
|
|
|
|
# See also {Task#summary} for the specification of the array.
|
|
|
|
|
#
|
|
|
|
|
# @return [Hash{Task=>Array(Float, Float, Float, Float)}] mapping from
|
|
|
|
|
# task to summary
|
2011-11-07 17:41:46 +01:00
|
|
|
|
def summary
|
2011-11-09 14:02:33 +01:00
|
|
|
|
summ = {}
|
2011-12-02 22:17:27 +01:00
|
|
|
|
tasks.each { |task| summ[task] = task.summary }
|
2011-11-09 14:02:33 +01:00
|
|
|
|
return summ
|
|
|
|
|
end
|
2011-11-07 17:41:46 +01:00
|
|
|
|
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# Returns a total per VAT rate for the summary of the associated tasks.
|
|
|
|
|
# See also {#summary}.
|
|
|
|
|
#
|
|
|
|
|
# @return [Hash{Float=>Float}] mapping from VAT rate to total for that
|
|
|
|
|
# rate with respect to the summary
|
2012-09-28 11:57:14 +02:00
|
|
|
|
def vat_summary
|
|
|
|
|
vatsumm = Hash.new(0.0)
|
|
|
|
|
summary.each do |task, summ|
|
|
|
|
|
vatsumm[task.vat_rate] += summ[3]
|
|
|
|
|
end
|
|
|
|
|
return vatsumm
|
|
|
|
|
end
|
|
|
|
|
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# Returns the invoice period based on the associated tasks. This
|
|
|
|
|
# period is a tuple of the earliest of the starting times and the
|
|
|
|
|
# latest of the ending times of all associated tasks.
|
|
|
|
|
# If there are no tasks, the creation time is used.
|
|
|
|
|
#
|
|
|
|
|
# See also {Task#bill_period}.
|
|
|
|
|
#
|
|
|
|
|
# @return [Array(Time, Time)] the invoice period
|
2011-11-09 14:02:33 +01:00
|
|
|
|
def period
|
2014-10-28 10:02:11 +01:00
|
|
|
|
return [created_at, created_at] if tasks.empty?
|
2014-10-18 21:25:51 +02:00
|
|
|
|
|
2014-10-28 10:02:11 +01:00
|
|
|
|
p = [DateTime.now, DateTime.new(0)]
|
2011-11-09 14:02:33 +01:00
|
|
|
|
tasks.each do |task|
|
|
|
|
|
tp = task.bill_period
|
2011-11-09 15:14:09 +01:00
|
|
|
|
p[0] = tp[0] if tp[0] < p[0]
|
|
|
|
|
p[1] = tp[1] if tp[1] > p[1]
|
2011-11-09 14:02:33 +01:00
|
|
|
|
end
|
|
|
|
|
return p
|
2011-11-07 17:41:46 +01:00
|
|
|
|
end
|
2012-01-09 17:38:06 +01:00
|
|
|
|
|
|
|
|
|
# Returns the total amount (including VAT).
|
2014-11-01 21:37:41 +01:00
|
|
|
|
#
|
|
|
|
|
# @note VAT will only be applied if the VAT number is given in the
|
|
|
|
|
# associated company information!
|
|
|
|
|
#
|
|
|
|
|
# @return [Float] the total amount (including VAT)
|
2012-01-09 17:38:06 +01:00
|
|
|
|
def total_amount
|
2012-09-28 11:58:38 +02:00
|
|
|
|
subtotal, vattotal = summary.inject([0.0, 0.0]) do |tot, (task, summ)|
|
|
|
|
|
tot[0] += summ[2]
|
|
|
|
|
tot[1] += summ[3]
|
|
|
|
|
tot
|
|
|
|
|
end
|
|
|
|
|
|
2012-01-09 17:38:06 +01:00
|
|
|
|
if company_info.vatno.blank?
|
|
|
|
|
subtotal
|
|
|
|
|
else
|
2012-09-28 11:58:38 +02:00
|
|
|
|
subtotal + vattotal
|
2012-01-09 17:38:06 +01:00
|
|
|
|
end
|
|
|
|
|
end
|
2013-06-16 23:29:01 +02:00
|
|
|
|
|
|
|
|
|
# Returns if the invoice is past due (i.e. it has not been paid within
|
|
|
|
|
# the required amount of days).
|
2014-11-01 21:37:41 +01:00
|
|
|
|
#
|
|
|
|
|
# @return [Boolean] whether payment of the invoice is past due
|
2013-06-16 23:29:01 +02:00
|
|
|
|
def past_due?
|
|
|
|
|
not paid? and (Time.now - created_at) > 30.days # FIXME: hardcoded!
|
|
|
|
|
end
|
|
|
|
|
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# Returns if the invoice is _way_ past due (i.e. it has not been paid
|
|
|
|
|
# within twice the required amount of days).
|
|
|
|
|
#
|
|
|
|
|
# @return [Boolean] whether payment of the invoice is way past due
|
2013-06-16 23:29:01 +02:00
|
|
|
|
def way_past_due?
|
|
|
|
|
past_due? and (Time.now - created_at) > 2 * 30.days
|
|
|
|
|
end
|
2014-11-01 21:37:41 +01:00
|
|
|
|
end # class StopTime::Models::Invoice
|
2011-10-31 14:36:01 +01:00
|
|
|
|
|
2011-11-10 18:24:11 +01:00
|
|
|
|
# == The company information class
|
|
|
|
|
#
|
|
|
|
|
# This class contains information about the company or sole
|
|
|
|
|
# proprietorship of the user of Stop… Camping Time!
|
2011-11-07 14:54:11 +01:00
|
|
|
|
class CompanyInfo < Base
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# @!attribute id [r]
|
|
|
|
|
# @return [Fixnum] unique identification number
|
|
|
|
|
# @!attribute name
|
|
|
|
|
# @return [String] official company name
|
|
|
|
|
# @!attribute contact_name
|
|
|
|
|
# @return [String] optional personal contact name
|
|
|
|
|
# @!attribute address_street
|
|
|
|
|
# @return [String] street part of the address
|
|
|
|
|
# @!attribute address_postal_code
|
|
|
|
|
# @return [String] zip/postal code part of the address
|
|
|
|
|
# @!attribute address_city
|
|
|
|
|
# @return [String] city part of the postal code
|
|
|
|
|
# @!attribute country
|
|
|
|
|
# @return [String] country of residence
|
|
|
|
|
# @!attribute country_code
|
|
|
|
|
# @return [String] two letter country code
|
|
|
|
|
# @!attribute email
|
|
|
|
|
# @return [String] email address
|
|
|
|
|
# @!attribute phone
|
|
|
|
|
# @return [String] phone number
|
|
|
|
|
# @!attribute cell
|
|
|
|
|
# @return [String] cellular phone number
|
|
|
|
|
# @!attribute website
|
|
|
|
|
# @return [String] web address
|
|
|
|
|
# @!attribute chamber
|
|
|
|
|
# @return [String] optional chamber of commerce ID number
|
|
|
|
|
# @!attribute vatno
|
|
|
|
|
# @return [String] optional VAT number
|
|
|
|
|
# @!attribute bank_name
|
|
|
|
|
# @return [String] name of the bank
|
|
|
|
|
# @!attribute bank_bic
|
|
|
|
|
# @return [String] bank identification code (or: SWIFT code)
|
|
|
|
|
# @!attribute accountname
|
|
|
|
|
# @return [String] name of the bank account holder
|
|
|
|
|
# @!attribute accountno
|
|
|
|
|
# @return [String] number of the bank account
|
|
|
|
|
# @!attribute accountiban
|
|
|
|
|
# @return [String] international bank account number
|
|
|
|
|
# @!attribute created_at
|
|
|
|
|
# @return [Time] time of creation
|
|
|
|
|
# @!attribute updated_at
|
|
|
|
|
# @return [Time] time of last update
|
|
|
|
|
|
|
|
|
|
# @!attribute invoices
|
|
|
|
|
# @return [Array<Invoice>] associated invoices
|
2012-01-09 15:48:20 +01:00
|
|
|
|
has_many :invoices
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# @!attribute original
|
|
|
|
|
# @return [CompanyInfo] original (previous) revision
|
|
|
|
|
belongs_to :original, :class_name => "CompanyInfo"
|
2012-01-09 15:48:20 +01:00
|
|
|
|
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# Returns the revision number.
|
|
|
|
|
# @return [Fixnum] the revision number
|
2012-01-09 15:48:20 +01:00
|
|
|
|
def revision
|
|
|
|
|
id
|
|
|
|
|
end
|
2014-11-01 21:37:41 +01:00
|
|
|
|
end # class StopTime::Models::CompanyInfo
|
2011-11-07 14:54:11 +01:00
|
|
|
|
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# @private
|
|
|
|
|
class StopTimeTables < V 1.0
|
2011-10-31 14:36:01 +01:00
|
|
|
|
def self.up
|
|
|
|
|
create_table Customer.table_name do |t|
|
2011-11-09 15:14:48 +01:00
|
|
|
|
t.string :name, :short_name,
|
2011-10-31 14:36:01 +01:00
|
|
|
|
: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
|
|
|
|
|
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# @private
|
|
|
|
|
class CommentSupport < V 1.1
|
2011-11-03 22:23:50 +01:00
|
|
|
|
def self.up
|
|
|
|
|
add_column(TimeEntry.table_name, :comment, :string)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def self.down
|
|
|
|
|
remove_column(TimeEntry.table_name, :comment)
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# @private
|
|
|
|
|
class BilledFlagSupport < V 1.2
|
2011-11-03 23:07:42 +01:00
|
|
|
|
def self.up
|
|
|
|
|
add_column(TimeEntry.table_name, :bill, :boolean)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def self.down
|
|
|
|
|
remove_column(TimeEntry.table_name, :bill)
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# @private
|
|
|
|
|
class HourlyRateSupport < V 1.3
|
2011-11-03 23:44:06 +01:00
|
|
|
|
def self.up
|
2014-10-18 21:26:14 +02:00
|
|
|
|
config = Config.instance
|
2011-11-03 23:44:06 +01:00
|
|
|
|
add_column(Customer.table_name, :hourly_rate, :float,
|
2011-12-23 21:17:44 +01:00
|
|
|
|
:null => false,
|
2014-10-18 21:26:14 +02:00
|
|
|
|
:default => config["hourly_rate"])
|
2011-11-03 23:44:06 +01:00
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def self.down
|
|
|
|
|
remove_column(Customer.table_name, :hourly_rate)
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# @private
|
|
|
|
|
class FixedCostTaskSupport < V 1.4
|
2011-11-07 10:24:12 +01:00
|
|
|
|
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
|
|
|
|
|
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# @private
|
|
|
|
|
class InvoiceSupport < V 1.5
|
2011-11-07 10:24:12 +01:00
|
|
|
|
def self.up
|
|
|
|
|
create_table Invoice.table_name do |t|
|
|
|
|
|
t.integer :number, :customer_id
|
|
|
|
|
t.boolean :payed
|
|
|
|
|
t.timestamps
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def self.down
|
|
|
|
|
drop_table Invoice.table_name
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# @private
|
|
|
|
|
class CompanyInfoSupport < V 1.6
|
2011-11-07 14:54:11 +01:00
|
|
|
|
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
|
|
|
|
|
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# @private
|
|
|
|
|
class ImprovedInvoiceSupport < V 1.7
|
2011-11-09 14:02:33 +01:00
|
|
|
|
def self.up
|
|
|
|
|
add_column(Task.table_name, :invoice_id, :integer)
|
|
|
|
|
remove_column(Task.table_name, :billed)
|
|
|
|
|
remove_column(TimeEntry.table_name, :invoice_id)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def self.down
|
|
|
|
|
remove_column(Task.table_name, :invoice_id, :integer)
|
|
|
|
|
add_column(Task.table_name, :billed, :boolean)
|
|
|
|
|
add_column(TimeEntry.table_name, :invoice_id)
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# @private
|
|
|
|
|
class TimeEntryDateSupport < V 1.8
|
2011-11-11 14:55:17 +01:00
|
|
|
|
def self.up
|
|
|
|
|
add_column(TimeEntry.table_name, :date, :datetime)
|
|
|
|
|
TimeEntry.all.each do |te|
|
|
|
|
|
te.date = te.start.at_beginning_of_day
|
|
|
|
|
te.save
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def self.down
|
|
|
|
|
remove_column(TimeEntry.table_name, :date)
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# @private
|
|
|
|
|
class PaidFlagTypoFix < V 1.9
|
2011-11-28 12:55:13 +01:00
|
|
|
|
def self.up
|
2014-10-18 21:26:55 +02:00
|
|
|
|
rename_column(Invoice.table_name, :payed, :paid)
|
2011-11-28 12:55:13 +01:00
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def self.down
|
2014-10-18 21:26:55 +02:00
|
|
|
|
rename_column(Invoice.table_name, :paid, :payed)
|
2011-11-28 12:55:13 +01:00
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# @private
|
|
|
|
|
class InvoiceCommentsSupport < V 1.91
|
2011-12-02 22:15:58 +01:00
|
|
|
|
def self.up
|
|
|
|
|
add_column(Task.table_name, :invoice_comment, :string)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def self.down
|
|
|
|
|
remove_column(Task.table_name, :invoice_comment)
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# @private
|
|
|
|
|
class FinancialInfoSupport < V 1.92
|
2012-01-03 16:49:14 +01:00
|
|
|
|
def self.up
|
|
|
|
|
add_column(CompanyInfo.table_name, :bank_name, :string)
|
|
|
|
|
add_column(CompanyInfo.table_name, :bank_bic, :string)
|
|
|
|
|
add_column(CompanyInfo.table_name, :accountiban, :string)
|
|
|
|
|
add_column(Customer.table_name, :financial_contact, :string)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def self.down
|
|
|
|
|
remove_column(CompanyInfo.table_name, :bank_name)
|
|
|
|
|
remove_column(CompanyInfo.table_name, :bank_bic)
|
|
|
|
|
remove_column(CompanyInfo.table_name, :accountiban)
|
|
|
|
|
remove_column(Customer.table_name, :financial_contact)
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# @private
|
|
|
|
|
class CompanyInfoRevisioning < V 1.93
|
2012-01-09 15:48:20 +01:00
|
|
|
|
def self.up
|
|
|
|
|
add_column(CompanyInfo.table_name, :original_id, :integer)
|
|
|
|
|
add_column(Invoice.table_name, :company_info_id, :integer)
|
|
|
|
|
ci = CompanyInfo.last
|
|
|
|
|
Invoice.all.each do |i|
|
|
|
|
|
i.company_info = ci
|
|
|
|
|
i.save
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def self.down
|
|
|
|
|
remove_column(CompanyInfo.table_name, :original_id)
|
|
|
|
|
remove_column(Invoice.table_name, :company_info_id)
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# @private
|
|
|
|
|
class VATRatePerTaskSupport < V 1.94
|
2012-09-28 10:09:42 +02:00
|
|
|
|
def self.up
|
|
|
|
|
add_column(Task.table_name, :vat_rate, :float)
|
|
|
|
|
config = Config.instance
|
|
|
|
|
Task.all.each do |t|
|
|
|
|
|
t.vat_rate = config['vat_rate']
|
|
|
|
|
t.save
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def self.down
|
|
|
|
|
remove_column(Task.table_name, :vat_rate)
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# @private
|
|
|
|
|
class TimeSpecificationSupport < V 1.95
|
2013-07-13 22:31:41 +02:00
|
|
|
|
def self.up
|
|
|
|
|
add_column(Customer.table_name, :time_specification, :boolean)
|
|
|
|
|
add_column(Invoice.table_name, :include_specification, :boolean)
|
2014-10-18 21:27:26 +02:00
|
|
|
|
|
|
|
|
|
Customer.reset_column_information
|
|
|
|
|
Invoice.reset_column_information
|
2013-07-13 22:31:41 +02:00
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def self.down
|
|
|
|
|
remove_column(Customer.table_name, :time_specification)
|
|
|
|
|
remove_column(Invoice.table_name, :include_specification)
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
2011-11-03 22:23:50 +01:00
|
|
|
|
end # StopTime::Models
|
2011-10-31 14:36:01 +01:00
|
|
|
|
|
2011-11-10 18:24:11 +01:00
|
|
|
|
# = The Stop… Camping Time! controllers
|
2011-10-31 14:36:01 +01:00
|
|
|
|
module StopTime::Controllers
|
|
|
|
|
|
2011-11-10 18:24:11 +01:00
|
|
|
|
# == The index controller
|
|
|
|
|
#
|
|
|
|
|
# Controller that presents the overview as the index, listing
|
|
|
|
|
# the running tasks and projects per customer.
|
|
|
|
|
#
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# path:: +/+
|
|
|
|
|
# view:: {Views#overview}
|
2011-10-31 14:36:01 +01:00
|
|
|
|
class Index
|
2011-12-10 21:13:26 +01:00
|
|
|
|
# Shows an overview of all unbilled projects/tasks per customer using
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# {Views#overview}.
|
2011-10-31 14:36:01 +01:00
|
|
|
|
def get
|
2011-11-09 16:02:43 +01:00
|
|
|
|
@tasks = {}
|
2014-11-01 17:47:13 +01:00
|
|
|
|
@tasks_summary = {}
|
2013-06-20 22:06:28 +02:00
|
|
|
|
@task_count = 0
|
2011-11-09 16:02:43 +01:00
|
|
|
|
Customer.all.each do |customer|
|
2014-11-01 17:45:16 +01:00
|
|
|
|
tasks = customer.active_tasks
|
2013-06-20 22:06:28 +02:00
|
|
|
|
@tasks[customer] = tasks
|
2014-11-01 17:47:13 +01:00
|
|
|
|
@tasks_summary[customer] = tasks.inject([0.0, 0.0]) do |summ, task|
|
|
|
|
|
task_summ = task.summary
|
|
|
|
|
summ[0] += task_summ[0]
|
|
|
|
|
summ[1] += task_summ[2]
|
|
|
|
|
summ
|
|
|
|
|
end
|
2013-06-20 22:06:28 +02:00
|
|
|
|
@task_count += tasks.count
|
2011-11-09 16:02:43 +01:00
|
|
|
|
end
|
|
|
|
|
render :overview
|
2011-10-31 14:36:01 +01:00
|
|
|
|
end
|
2014-11-01 21:37:41 +01:00
|
|
|
|
end # class StopTime::Controllers::Index
|
2011-10-31 16:14:54 +01:00
|
|
|
|
|
2011-11-10 18:24:11 +01:00
|
|
|
|
# == The customers controller
|
|
|
|
|
#
|
|
|
|
|
# Controller for viewing a list of existing customers or creating a new
|
|
|
|
|
# one.
|
|
|
|
|
#
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# path:: +/customers+
|
|
|
|
|
# view:: {Views#customers} and {Views#customer_form}
|
2011-10-31 16:14:54 +01:00
|
|
|
|
class Customers
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# Show the list of customers and displays them using {Views#customers}.
|
2011-10-31 16:14:54 +01:00
|
|
|
|
def get
|
2014-10-18 21:25:26 +02:00
|
|
|
|
@customers = Customer.order("name ASC")
|
2011-10-31 16:14:54 +01:00
|
|
|
|
render :customers
|
|
|
|
|
end
|
|
|
|
|
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# Creates a new customer object ({Models::Customer}) if the input is
|
|
|
|
|
# valid and redirects to {CustomersN}.
|
2011-11-10 18:24:11 +01:00
|
|
|
|
# If the provided information is invalid, the errors are retrieved
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# and shown in the initial form ({Views#customer_form}).
|
2011-10-31 16:14:54 +01:00
|
|
|
|
def post
|
|
|
|
|
return redirect R(Customers) if @input.cancel
|
|
|
|
|
@customer = Customer.create(
|
|
|
|
|
:name => @input.name,
|
|
|
|
|
:short_name => @input.short_name,
|
2012-01-03 16:51:07 +01:00
|
|
|
|
:financial_contact => @input.financial_contact,
|
2011-10-31 16:14:54 +01:00
|
|
|
|
: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-09 17:01:13 +01:00
|
|
|
|
@target = [Customer]
|
|
|
|
|
@button = "create"
|
2011-11-03 23:44:06 +01:00
|
|
|
|
return render :customer_form
|
2011-10-31 16:14:54 +01:00
|
|
|
|
end
|
2011-11-10 15:23:09 +01:00
|
|
|
|
redirect R(CustomersN, @customer.id)
|
2011-10-31 16:14:54 +01:00
|
|
|
|
end
|
2014-11-01 21:37:41 +01:00
|
|
|
|
end # class StopTime::Controllers::CustomersN
|
2011-10-31 16:14:54 +01:00
|
|
|
|
|
2011-11-10 18:24:11 +01:00
|
|
|
|
# == The customer creation controller
|
|
|
|
|
#
|
|
|
|
|
# Controller for filling in the information to create a new customer.
|
|
|
|
|
#
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# path:: +/customers/new+
|
|
|
|
|
# view:: {Views#customer_form}
|
2011-11-01 15:29:24 +01:00
|
|
|
|
class CustomersNew
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# Generates the form to create a new customer object ({Models::Customer})
|
|
|
|
|
# using {Views#customer_form}.
|
2011-11-01 15:29:24 +01:00
|
|
|
|
def get
|
2011-12-23 21:17:44 +01:00
|
|
|
|
@customer = Customer.new(:hourly_rate => @config['hourly_rate'])
|
2011-11-09 17:01:13 +01:00
|
|
|
|
@input = @customer.attributes
|
2012-01-06 16:32:19 +01:00
|
|
|
|
@tasks = []
|
2011-11-09 17:01:13 +01:00
|
|
|
|
|
2011-11-07 10:44:35 +01:00
|
|
|
|
@target = [Customers]
|
2011-11-09 17:01:13 +01:00
|
|
|
|
@button = "create"
|
2011-11-01 15:29:24 +01:00
|
|
|
|
render :customer_form
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
2011-11-10 18:24:11 +01:00
|
|
|
|
# == The customer controller
|
|
|
|
|
#
|
|
|
|
|
# Controller for viewing and updating information of a customer.
|
|
|
|
|
#
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# path:: +/customers/+_customer_id_
|
|
|
|
|
# view:: {Views#customer_form}
|
2011-11-01 15:29:24 +01:00
|
|
|
|
class CustomersN
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# Finds the specific customer for the given customer ID and shows
|
|
|
|
|
# a form for updating using {Views#customer_form}.
|
|
|
|
|
#
|
|
|
|
|
# @param [Fixnum] customer_id ID of the customer
|
2011-10-31 16:14:54 +01:00
|
|
|
|
def get(customer_id)
|
|
|
|
|
@customer = Customer.find(customer_id)
|
2011-11-07 17:43:10 +01:00
|
|
|
|
@input = @customer.attributes
|
2014-10-18 21:25:26 +02:00
|
|
|
|
@tasks = @customer.tasks.order("name ASC, invoice_id ASC")
|
2013-06-26 20:23:56 +02:00
|
|
|
|
# FIXME: this dirty hack assumes that tasks have unique names,
|
|
|
|
|
# becasue there is no reference from billed tasks to its original
|
|
|
|
|
# task.
|
|
|
|
|
@billed_tasks = {}
|
|
|
|
|
cur_active_task = nil
|
|
|
|
|
@tasks.each do |task|
|
|
|
|
|
if task.billed?
|
2014-02-07 20:30:38 +01:00
|
|
|
|
if cur_active_task.nil? or
|
|
|
|
|
task.name != cur_active_task.name
|
|
|
|
|
# Apparently, this is billed but it does not belong to the
|
|
|
|
|
# current active task, so probably it was a fixed-cost task
|
|
|
|
|
cur_active_task = task
|
2013-07-14 14:37:40 +02:00
|
|
|
|
@billed_tasks[task] = [task]
|
2013-07-14 14:32:02 +02:00
|
|
|
|
else
|
|
|
|
|
@billed_tasks[cur_active_task] << task
|
|
|
|
|
end
|
2013-06-26 20:23:56 +02:00
|
|
|
|
else
|
|
|
|
|
cur_active_task = task
|
|
|
|
|
@billed_tasks[task] = []
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
2011-12-10 16:54:43 +01:00
|
|
|
|
@invoices = @customer.invoices
|
|
|
|
|
@invoices.each do |i|
|
|
|
|
|
@input["paid_#{i.number}"] = true if i.paid?
|
|
|
|
|
end
|
2011-11-09 15:13:39 +01:00
|
|
|
|
|
|
|
|
|
@target = [CustomersN, @customer.id]
|
2011-11-09 17:01:13 +01:00
|
|
|
|
@button = "update"
|
2011-11-09 15:13:39 +01:00
|
|
|
|
@edit_task = true
|
2011-11-01 15:29:24 +01:00
|
|
|
|
render :customer_form
|
2011-10-31 16:14:54 +01:00
|
|
|
|
end
|
|
|
|
|
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# Updates or deletes the customer with the given customer ID if the
|
|
|
|
|
# input is valid and redirects to {CustomersN}.
|
2011-11-10 18:24:11 +01:00
|
|
|
|
# If the provided information is invalid, the errors are retrieved
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# and shown in the initial form ({Views#customer_form}).
|
|
|
|
|
#
|
|
|
|
|
# @param [Fixnum] customer_id ID of the customer
|
2011-10-31 16:14:54 +01:00
|
|
|
|
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"
|
2012-01-03 16:51:07 +01:00
|
|
|
|
attrs = ["name", "short_name", "financial_contact",
|
2011-11-01 15:29:24 +01:00
|
|
|
|
"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
|
2013-07-13 22:32:27 +02:00
|
|
|
|
@customer.time_specification = @input.has_key? "time_specification"
|
2011-11-01 15:29:24 +01:00
|
|
|
|
@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
|
2014-11-01 21:37:41 +01:00
|
|
|
|
end # class StopTime::Controllers::CustomersN
|
2011-10-31 16:14:54 +01:00
|
|
|
|
|
2011-11-10 18:24:11 +01:00
|
|
|
|
# == The tasks controller for a specific customer
|
|
|
|
|
#
|
|
|
|
|
# Controller for creating, editing and deleting a task for a
|
|
|
|
|
# specific customer.
|
|
|
|
|
#
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# path:: +/customers/+_customer_id_+/tasks+
|
|
|
|
|
# view:: {Views#task_form}
|
2011-11-01 15:29:24 +01:00
|
|
|
|
class CustomersNTasks
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# Creates, updates or deletes a task object ({Models::Task}) for a
|
|
|
|
|
# customer with the given customer ID if the input is valid and
|
|
|
|
|
# redirects to {CustomersN}.
|
2011-11-10 18:24:11 +01:00
|
|
|
|
# If the provided information is invalid, the errors are retrieved and
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# shown in the initial form ({Views#task_form}).
|
|
|
|
|
#
|
|
|
|
|
# @param [Fixnum] customer_id ID of the customer
|
2011-11-01 15:29:24 +01:00
|
|
|
|
def post(customer_id)
|
2011-11-10 12:50:02 +01:00
|
|
|
|
return redirect R(Customers) if @input.cancel
|
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)
|
2011-11-09 14:07:31 +01:00
|
|
|
|
case @input.type
|
2011-11-07 13:40:43 +01:00
|
|
|
|
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
|
2012-09-28 16:08:45 +02:00
|
|
|
|
@task.vat_rate = @input.vat_rate
|
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)
|
2011-12-22 16:33:57 +01:00
|
|
|
|
@customer_list = Customer.all.map { |c| [c.id, c.shortest_name] }
|
2014-10-18 21:25:26 +02:00
|
|
|
|
@time_entries = @task.time_entries.order("start DESC")
|
2011-12-12 17:10:18 +01:00
|
|
|
|
@time_entries.each do |te|
|
|
|
|
|
@input["bill_#{te.id}"] = true if te.bill?
|
|
|
|
|
end
|
|
|
|
|
|
2011-11-07 13:40:43 +01:00
|
|
|
|
@target = [CustomersNTasks, customer_id]
|
|
|
|
|
@method = "create"
|
|
|
|
|
return render :task_form
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
redirect R(CustomersN, customer_id)
|
|
|
|
|
end
|
2014-11-01 21:37:41 +01:00
|
|
|
|
end # class StopTime::Controllers::CustomersNTasks
|
2011-11-07 13:40:43 +01:00
|
|
|
|
|
2011-11-10 18:24:11 +01:00
|
|
|
|
# == The task creation controller for a specific customer
|
|
|
|
|
#
|
|
|
|
|
# Controller for filling in the information to create a new task
|
|
|
|
|
# for a specific customer.
|
|
|
|
|
#
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# path:: +/customers/+_customer_id_+/tasks/new+
|
|
|
|
|
# view:: {Views#task_form}
|
2011-11-07 13:40:43 +01:00
|
|
|
|
class CustomersNTasksNew
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# Generates the form to create a new task object ({Models::Task})
|
|
|
|
|
# for a customer with the given customer ID_ using {Views#task_form}.
|
|
|
|
|
#
|
|
|
|
|
# @param [Fixnum] customer_id ID of the customer
|
2011-11-07 13:40:43 +01:00
|
|
|
|
def get(customer_id)
|
|
|
|
|
@customer = Customer.find(customer_id)
|
2011-12-22 16:33:57 +01:00
|
|
|
|
@customer_list = Customer.all.map { |c| [c.id, c.shortest_name] }
|
2012-09-28 16:08:45 +02:00
|
|
|
|
@task = Task.new(:hourly_rate => @customer.hourly_rate,
|
|
|
|
|
:vat_rate => @config["vat_rate"])
|
2011-11-07 17:43:10 +01:00
|
|
|
|
@input = @task.attributes
|
2011-11-09 14:07:31 +01:00
|
|
|
|
@input["type"] = @task.type # FIXME: find nicer way!
|
2011-11-29 16:21:09 +01:00
|
|
|
|
@input["customer"] = @customer.id
|
2011-11-10 13:07:30 +01:00
|
|
|
|
|
|
|
|
|
@target = [CustomersNTasks, customer_id]
|
|
|
|
|
@method = "create"
|
2011-11-07 13:40:43 +01:00
|
|
|
|
render :task_form
|
|
|
|
|
end
|
2014-11-01 21:37:41 +01:00
|
|
|
|
end # class StopTime::Controllers::CustomersNTasksNew
|
2011-11-07 13:40:43 +01:00
|
|
|
|
|
2011-11-10 18:24:11 +01:00
|
|
|
|
# == The task controller for a specific customer
|
|
|
|
|
#
|
|
|
|
|
# Controller for viewing and updating information of a task for
|
|
|
|
|
# a specific customer.
|
|
|
|
|
#
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# path:: +/customers/+_customer_id_+/tasks/+_task_id_
|
|
|
|
|
# view:: {Views#task_form}
|
2011-11-07 13:40:43 +01:00
|
|
|
|
class CustomersNTasksN
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# Finds the task with the given task ID for the customer with the given
|
|
|
|
|
# customer ID and shows a form for updating using {Views#task_form}.
|
|
|
|
|
#
|
|
|
|
|
# @param [Fixnum] customer_id ID of the customer
|
|
|
|
|
# @param [Fixnum] task_id ID of the task
|
2011-11-07 13:40:43 +01:00
|
|
|
|
def get(customer_id, task_id)
|
|
|
|
|
@customer = Customer.find(customer_id)
|
2011-12-22 16:33:57 +01:00
|
|
|
|
@customer_list = Customer.all.map { |c| [c.id, c.shortest_name] }
|
2011-11-07 13:40:43 +01:00
|
|
|
|
@task = Task.find(task_id)
|
2014-10-18 21:25:26 +02:00
|
|
|
|
@time_entries = @task.time_entries.order("start DESC")
|
2011-12-12 17:10:18 +01:00
|
|
|
|
|
2011-11-07 17:43:10 +01:00
|
|
|
|
@input = @task.attributes
|
2011-11-09 14:07:31 +01:00
|
|
|
|
@input["type"] = @task.type
|
2011-11-29 16:21:09 +01:00
|
|
|
|
@input["customer"] = @customer.id
|
2011-12-12 17:10:18 +01:00
|
|
|
|
@time_entries.each do |te|
|
|
|
|
|
@input["bill_#{te.id}"] = true if te.bill?
|
|
|
|
|
end
|
2013-06-20 22:07:20 +02:00
|
|
|
|
@input["bill"] = true # Bill new entries by default.
|
2011-12-12 17:10:18 +01:00
|
|
|
|
|
2011-11-07 13:40:43 +01:00
|
|
|
|
# FIXME: Check that task is of that customer.
|
2011-12-12 17:10:18 +01:00
|
|
|
|
@target = [CustomersNTasksN, customer_id, task_id]
|
|
|
|
|
@method = "update"
|
2011-11-07 13:40:43 +01:00
|
|
|
|
render :task_form
|
|
|
|
|
end
|
|
|
|
|
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# Updates the task with the given task ID_ for the customer with
|
|
|
|
|
# the given customer ID if the input is valid and redirects to
|
|
|
|
|
# {CustomersN}.
|
2011-11-10 18:24:11 +01:00
|
|
|
|
# If the provided information is invalid, the errors are retrieved
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# and shown in the intial form ({Views#task_form}).
|
|
|
|
|
#
|
|
|
|
|
# @param [Fixnum] customer_id ID of the customer
|
|
|
|
|
# @param [Fixnum] task_id ID of the task
|
2011-11-07 13:40:43 +01:00
|
|
|
|
def post(customer_id, task_id)
|
|
|
|
|
return redirect R(CustomersN, customer_id) if @input.cancel
|
|
|
|
|
@task = Task.find(task_id)
|
|
|
|
|
if @input.has_key? "update"
|
2014-10-25 22:32:57 +02:00
|
|
|
|
@task.customer = Customer.find(@input["customer"])
|
2012-01-02 14:05:03 +01:00
|
|
|
|
@task.name = @input["name"] unless @input["name"].blank?
|
|
|
|
|
if @task.billed? and @input["invoice_comment"].present?
|
|
|
|
|
@task.invoice_comment = @input["invoice_comment"]
|
|
|
|
|
end
|
2011-11-09 14:07:31 +01:00
|
|
|
|
case @input.type
|
2011-11-07 13:40:43 +01:00
|
|
|
|
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.save
|
|
|
|
|
if @task.invalid?
|
|
|
|
|
@errors = @task.errors
|
2011-11-28 13:56:46 +01:00
|
|
|
|
@customer = Customer.find(customer_id)
|
2011-12-22 16:33:57 +01:00
|
|
|
|
@customer_list = Customer.all.map { |c| [c.id, c.shortest_name] }
|
2011-11-07 13:40:43 +01:00
|
|
|
|
@target = [CustomersNTasksN, customer_id, task_id]
|
|
|
|
|
@method = "update"
|
|
|
|
|
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
|
2014-11-01 21:37:41 +01:00
|
|
|
|
end # class StopTime::Controllers::CustomersNTasksN
|
2011-10-31 16:14:54 +01:00
|
|
|
|
|
2011-11-10 18:24:11 +01:00
|
|
|
|
# == The invoices controller for a specific customer
|
|
|
|
|
#
|
|
|
|
|
# Controller for creating and viewing invoices for a specific customer.
|
|
|
|
|
#
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# path:: +/customers/+_customer_id_+/invoices+
|
|
|
|
|
# view:: {Views#invoices}
|
2011-11-07 17:44:20 +01:00
|
|
|
|
class CustomersNInvoices
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# Gets the list of invoices for the customer with the given customer ID
|
|
|
|
|
# and displays them using {Views#invoices}.
|
|
|
|
|
#
|
|
|
|
|
# @param [Fixnum] customer_id ID of the customer
|
2011-11-09 16:03:02 +01:00
|
|
|
|
def get(customer_id)
|
|
|
|
|
# FIXME: quick hack! is this URL even used?
|
|
|
|
|
customer = Customer.find(customer_id)
|
2012-01-03 17:38:14 +01:00
|
|
|
|
customer.invoices.each do |i|
|
|
|
|
|
@input["paid_#{i.number}"] = true if i.paid?
|
|
|
|
|
end
|
|
|
|
|
@invoices = {customer.name => customer.invoices}
|
|
|
|
|
|
2011-11-09 14:02:33 +01:00
|
|
|
|
render :invoices
|
|
|
|
|
end
|
|
|
|
|
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# Creates a new invoice object ({Models::Invoice}) for the customer
|
|
|
|
|
# with the given customer ID if the input is valid and redirects to
|
|
|
|
|
# {CustomersNInvoicesX}.
|
2011-11-10 18:24:11 +01:00
|
|
|
|
#
|
|
|
|
|
# A unique number is generated for the invoice by taking the
|
|
|
|
|
# year and a sequence number.
|
|
|
|
|
#
|
|
|
|
|
# A fixed cost task is directly tied to the invoice.
|
|
|
|
|
#
|
|
|
|
|
# For a task with an hourly rate, a task copy is created with the
|
|
|
|
|
# selected time to bill and put in the invoice; the remaining unbilled
|
|
|
|
|
# time is left in the original task.
|
2014-11-01 21:37:41 +01:00
|
|
|
|
#
|
|
|
|
|
# @param [Fixnum] customer_id ID of the customer
|
2011-11-07 17:44:20 +01:00
|
|
|
|
def post(customer_id)
|
|
|
|
|
return redirect R(CustomersN, customer_id) if @input.cancel
|
|
|
|
|
|
|
|
|
|
# Create the invoice.
|
2014-02-09 14:40:28 +01:00
|
|
|
|
last = Invoice.reorder('number ASC').last
|
2011-11-09 14:02:33 +01:00
|
|
|
|
number = if last
|
|
|
|
|
last_year = last.number.to_s[0..3].to_i
|
|
|
|
|
if Time.now.year > last_year
|
|
|
|
|
number = ("%d%02d" % [Time.now.year, 1])
|
|
|
|
|
else
|
2011-11-09 15:14:48 +01:00
|
|
|
|
number = last.number.succ
|
2011-11-09 14:02:33 +01:00
|
|
|
|
end
|
|
|
|
|
else
|
|
|
|
|
number = ("%d%02d" % [Time.now.year, 1])
|
|
|
|
|
end
|
2011-11-07 17:44:20 +01:00
|
|
|
|
invoice = Invoice.create(:number => number)
|
2011-11-09 14:02:33 +01:00
|
|
|
|
invoice.customer = Customer.find(customer_id)
|
2012-01-09 15:52:09 +01:00
|
|
|
|
invoice.company_info = CompanyInfo.last
|
2013-07-13 22:32:45 +02:00
|
|
|
|
invoice.include_specification = invoice.customer.time_specification
|
2011-11-09 14:02:33 +01:00
|
|
|
|
|
2012-01-02 13:58:47 +01:00
|
|
|
|
# Handle the hourly rated tasks first by looking at the selected time
|
|
|
|
|
# entries.
|
2011-11-09 14:02:33 +01:00
|
|
|
|
tasks = Hash.new { |h, k| h[k] = Array.new }
|
|
|
|
|
@input["time_entries"].each do |entry|
|
|
|
|
|
time_entry = TimeEntry.find(entry)
|
|
|
|
|
tasks[time_entry.task] << time_entry
|
|
|
|
|
end unless @input["time_entries"].blank?
|
|
|
|
|
tasks.each_key do |task|
|
2012-01-02 13:58:47 +01:00
|
|
|
|
# Create a new (billed) task clone that contains the selected time
|
|
|
|
|
# entries, leave the rest unbilled and associated with their task.
|
2013-06-16 22:43:58 +02:00
|
|
|
|
bill_task = task.dup # FIXME: depends on rails version!
|
2011-11-09 14:02:33 +01:00
|
|
|
|
task.time_entries = task.time_entries - tasks[task]
|
2011-11-07 17:44:20 +01:00
|
|
|
|
task.save
|
2011-11-09 14:02:33 +01:00
|
|
|
|
bill_task.time_entries = tasks[task]
|
2011-12-02 22:18:55 +01:00
|
|
|
|
bill_task.invoice_comment = @input["task_#{task.id}_comment"]
|
2011-11-09 14:02:33 +01:00
|
|
|
|
bill_task.save
|
|
|
|
|
invoice.tasks << bill_task
|
2011-11-07 17:44:20 +01:00
|
|
|
|
end
|
2011-11-09 14:02:33 +01:00
|
|
|
|
|
2012-01-02 13:58:47 +01:00
|
|
|
|
# Then, handle the (selected) fixed cost tasks.
|
2011-11-09 14:02:33 +01:00
|
|
|
|
@input["tasks"].each do |task|
|
2011-12-02 22:18:55 +01:00
|
|
|
|
task = Task.find(task)
|
2012-01-02 13:58:47 +01:00
|
|
|
|
next unless task.fixed_cost?
|
2011-12-02 22:18:55 +01:00
|
|
|
|
task.invoice_comment = @input["task_#{task.id}_comment"]
|
|
|
|
|
task.save
|
|
|
|
|
invoice.tasks << task
|
2011-11-09 14:02:33 +01:00
|
|
|
|
end unless @input["tasks"].blank?
|
2011-11-07 17:44:20 +01:00
|
|
|
|
invoice.save
|
|
|
|
|
|
|
|
|
|
redirect R(CustomersNInvoicesX, customer_id, number)
|
|
|
|
|
end
|
2014-11-01 21:37:41 +01:00
|
|
|
|
end # class StopTime::Controllers::CustomersNInvoices
|
2011-11-07 17:44:20 +01:00
|
|
|
|
|
2011-11-10 18:24:11 +01:00
|
|
|
|
# == The invoice controller for a specific customer
|
|
|
|
|
#
|
|
|
|
|
# Controller for viewing and updating information of an invoice for a
|
|
|
|
|
# specific customer.
|
|
|
|
|
#
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# path:: +/customers/+_customer_id_+/invoices/+_invoice_number_
|
|
|
|
|
# view:: {Views#invoice_form}
|
2011-11-16 10:59:18 +01:00
|
|
|
|
class CustomersNInvoicesX < R '/customers/(\d+)/invoices/([^/]+)'
|
2011-11-09 18:31:23 +01:00
|
|
|
|
include ActionView::Helpers::NumberHelper
|
|
|
|
|
include I18n
|
|
|
|
|
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# Finds the invoice with the given invoice number for the customer
|
|
|
|
|
# with the given customer ID and shows a form for updating using
|
|
|
|
|
# {Views#invoice_form}.
|
2011-11-10 18:24:11 +01:00
|
|
|
|
# If the invoice_number has a .pdf or .tex suffix, a PDF or LaTeX
|
|
|
|
|
# source document is generated for the invoice (if not already
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# existing) and served via a redirect to the {Static} controller.
|
|
|
|
|
#
|
|
|
|
|
# @param [Fixnum] customer_id ID of the customer
|
|
|
|
|
# @param [Fixnum] invoice_number number of the invoice
|
2011-11-07 17:44:20 +01:00
|
|
|
|
def get(customer_id, invoice_number)
|
2011-11-03 11:40:58 +01:00
|
|
|
|
# FIXME: make this (much) nicer!
|
2011-11-07 17:44:20 +01:00
|
|
|
|
if m = invoice_number.match(/(\d+)\.(\w+)$/)
|
2011-11-03 11:40:58 +01:00
|
|
|
|
@number = m[1].to_i
|
|
|
|
|
@format = m[2]
|
|
|
|
|
else
|
2011-11-07 17:44:20 +01:00
|
|
|
|
@number = invoice_number.to_i
|
2011-11-03 11:40:58 +01:00
|
|
|
|
@format = "html"
|
|
|
|
|
end
|
2011-11-07 17:44:20 +01:00
|
|
|
|
@invoice = Invoice.find_by_number(@number)
|
2011-11-02 22:52:47 +01:00
|
|
|
|
@customer = Customer.find(customer_id)
|
2012-01-09 15:52:09 +01:00
|
|
|
|
|
|
|
|
|
@company = @invoice.company_info
|
2011-11-07 17:44:20 +01:00
|
|
|
|
@tasks = @invoice.summary
|
2012-09-28 12:00:42 +02:00
|
|
|
|
@vat = @invoice.vat_summary
|
2011-11-09 14:02:33 +01:00
|
|
|
|
@period = @invoice.period
|
2011-11-02 22:52:47 +01:00
|
|
|
|
|
2014-10-31 21:55:04 +01:00
|
|
|
|
tex_file = PUBLIC_DIR + "invoices/#{@number}.tex"
|
|
|
|
|
pdf_file = PUBLIC_DIR + "invoices/#{@number}.pdf"
|
2011-11-03 11:40:58 +01:00
|
|
|
|
if @format == "html"
|
2011-11-09 15:13:39 +01:00
|
|
|
|
@input = @invoice.attributes
|
2014-10-31 21:55:04 +01:00
|
|
|
|
@invoice_file_present = tex_file.exist?
|
2011-12-10 21:08:48 +01:00
|
|
|
|
render :invoice_form
|
2011-11-09 16:11:51 +01:00
|
|
|
|
elsif @format == "tex"
|
|
|
|
|
_generate_invoice_tex(@number) unless tex_file.exist?
|
2011-12-10 16:58:38 +01:00
|
|
|
|
redirect R(Static, "") + "invoices/#{tex_file.basename}"
|
2011-11-03 11:40:58 +01:00
|
|
|
|
elsif @format == "pdf"
|
2011-11-09 16:11:51 +01:00
|
|
|
|
_generate_invoice_pdf(@number) unless pdf_file.exist?
|
2011-12-10 16:58:38 +01:00
|
|
|
|
redirect R(Static, "") + "invoices/#{pdf_file.basename}"
|
2011-11-03 11:40:58 +01:00
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# Updates the invoice with the given invoice number for the customer
|
|
|
|
|
# with the given customer ID and redirects to {CustomersNInvoicesX}.
|
|
|
|
|
#
|
|
|
|
|
# @param [Fixnum] customer_id ID of the customer
|
|
|
|
|
# @param [Fixnum] invoice_number number of the invoice
|
2011-11-09 15:13:39 +01:00
|
|
|
|
def post(customer_id, invoice_number)
|
|
|
|
|
invoice = Invoice.find_by_number(invoice_number)
|
2011-11-28 12:55:13 +01:00
|
|
|
|
invoice.paid = @input.has_key? "paid"
|
2013-07-13 22:32:45 +02:00
|
|
|
|
invoice.include_specification = @input.has_key? "include_specification"
|
2011-11-09 15:13:39 +01:00
|
|
|
|
invoice.save
|
|
|
|
|
|
|
|
|
|
redirect R(CustomersNInvoicesX, customer_id, invoice_number)
|
|
|
|
|
end
|
|
|
|
|
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# Find the invoice with the given invoice number for the customer
|
|
|
|
|
# with the given customer ID and deletes existing invoice files.
|
|
|
|
|
#
|
|
|
|
|
# @param [Fixnum] customer_id ID of the customer
|
|
|
|
|
# @param [Fixnum] invoice_number number of the invoice
|
2014-10-31 21:59:17 +01:00
|
|
|
|
def delete(customer_id, invoice_number)
|
|
|
|
|
@invoice = Invoice.find_by_number(@number)
|
|
|
|
|
@customer = Customer.find(customer_id)
|
|
|
|
|
|
|
|
|
|
tex_file = PUBLIC_DIR + "invoices/#{invoice_number}.tex"
|
|
|
|
|
File.unlink(tex_file) if tex_file.exist?
|
|
|
|
|
|
|
|
|
|
pdf_file = PUBLIC_DIR + "invoices/#{invoice_number}.pdf"
|
|
|
|
|
File.unlink(pdf_file) if pdf_file.exist?
|
|
|
|
|
|
|
|
|
|
redirect R(CustomersNInvoicesX, customer_id, invoice_number)
|
|
|
|
|
end
|
|
|
|
|
|
2014-11-01 21:37:41 +01:00
|
|
|
|
########################
|
2011-12-10 21:05:58 +01:00
|
|
|
|
# Private helper methods
|
|
|
|
|
#
|
2011-11-10 18:24:11 +01:00
|
|
|
|
private
|
|
|
|
|
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# Generates a LaTex document for the invoice with the given number.
|
|
|
|
|
#
|
|
|
|
|
# @param [Fixnum] number number of the invoice
|
2011-11-09 16:11:51 +01:00
|
|
|
|
def _generate_invoice_tex(number)
|
2014-02-07 21:04:08 +01:00
|
|
|
|
template = TEMPLATE_DIR + "#{@config["invoice_template"]}.tex.erb"
|
2011-12-06 14:34:31 +01:00
|
|
|
|
tex_file = PUBLIC_DIR + "invoices/#{number}.tex"
|
2011-11-03 11:40:58 +01:00
|
|
|
|
|
2011-11-09 19:44:59 +01:00
|
|
|
|
I18n.with_locale :nl do
|
2011-11-09 18:31:23 +01:00
|
|
|
|
erb = ERB.new(File.read(template))
|
|
|
|
|
File.open(tex_file, "w") { |f| f.write(erb.result(binding)) }
|
|
|
|
|
end
|
2012-01-03 16:56:51 +01:00
|
|
|
|
rescue Exception => err
|
2014-02-07 21:04:08 +01:00
|
|
|
|
tex_file.delete if File.exist? tex_file
|
2012-01-03 16:56:51 +01:00
|
|
|
|
raise err
|
2011-11-09 16:11:51 +01:00
|
|
|
|
end
|
|
|
|
|
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# Generates a PDF document for the invoice with the given number
|
|
|
|
|
# using {#_generate_invoice_tex}.
|
|
|
|
|
#
|
|
|
|
|
# @param [Fixnum] number number of the invoice
|
2011-11-09 16:11:51 +01:00
|
|
|
|
def _generate_invoice_pdf(number)
|
2011-12-06 14:34:31 +01:00
|
|
|
|
tex_file = PUBLIC_DIR + "invoices/#{@number}.tex"
|
2011-11-09 16:11:51 +01:00
|
|
|
|
_generate_invoice_tex(number) unless tex_file.exist?
|
|
|
|
|
|
|
|
|
|
# FIXME: remove rubber depend, use pdflatex directly
|
2011-11-03 11:40:58 +01:00
|
|
|
|
system("rubber --pdf --inplace #{tex_file}")
|
|
|
|
|
system("rubber --clean --inplace #{tex_file}")
|
2011-11-02 22:52:47 +01:00
|
|
|
|
end
|
2014-11-01 21:37:41 +01:00
|
|
|
|
end # class StopTime::Controllers::CustomerNInvoicesX
|
2011-11-02 22:52:47 +01:00
|
|
|
|
|
2011-11-10 18:24:11 +01:00
|
|
|
|
# == The invoice creating controller for a specifc customer
|
|
|
|
|
#
|
|
|
|
|
# Controller for creating a new invoice for a specific customer.
|
|
|
|
|
#
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# path:: +/customers/+_customer_id_+/invoices/new+
|
|
|
|
|
# view:: {Views#invoice_select_form}
|
2011-11-08 13:08:22 +01:00
|
|
|
|
class CustomersNInvoicesNew
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# Generates the form to create a new invoice object ({Models::Invoice})
|
2011-11-10 18:24:11 +01:00
|
|
|
|
# by listing unbilled fixed cost tasks and unbilled registered time
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# (for tasks with an hourly rate) of the customer with the given
|
|
|
|
|
# customer ID so that it can be individually selected using
|
|
|
|
|
# {Views#invoice_select_form}.
|
|
|
|
|
#
|
|
|
|
|
# @param [Fixnum] customer_id ID of the customer
|
2011-11-08 13:08:22 +01:00
|
|
|
|
def get(customer_id)
|
|
|
|
|
@customer = Customer.find(customer_id)
|
2011-11-09 14:02:33 +01:00
|
|
|
|
@hourly_rate_tasks = {}
|
|
|
|
|
@fixed_cost_tasks = {}
|
2014-11-01 22:08:14 +01:00
|
|
|
|
@customer.active_tasks.each do |task|
|
2011-11-09 14:02:33 +01:00
|
|
|
|
case task.type
|
|
|
|
|
when "fixed_cost"
|
2011-11-09 15:12:29 +01:00
|
|
|
|
total = task.time_entries.inject(0.0) { |s, te| s + te.hours_total }
|
2011-11-09 14:02:33 +01:00
|
|
|
|
@fixed_cost_tasks[task] = total
|
|
|
|
|
when "hourly_rate"
|
|
|
|
|
time_entries = task.billable_time_entries
|
|
|
|
|
@hourly_rate_tasks[task] = time_entries
|
|
|
|
|
end
|
|
|
|
|
end
|
2013-06-20 22:06:28 +02:00
|
|
|
|
@none_found = @hourly_rate_tasks.empty? and @fixed_cost_tasks.empty?
|
2011-11-08 13:08:22 +01:00
|
|
|
|
render :invoice_select_form
|
|
|
|
|
end
|
2014-11-01 21:37:41 +01:00
|
|
|
|
end # class StopTime::Controllers::CustomersNInvoicesNew
|
2011-11-08 13:08:22 +01:00
|
|
|
|
|
2011-11-10 18:24:11 +01:00
|
|
|
|
# == The timeline controller
|
|
|
|
|
#
|
|
|
|
|
# Controller for presenting a timeline of registered time and
|
|
|
|
|
# also for quickly registering time.
|
|
|
|
|
#
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# path:: +/timeline+
|
|
|
|
|
# view:: {Views#time_entries}
|
2011-11-08 13:08:51 +01:00
|
|
|
|
class Timeline
|
2011-11-10 18:24:11 +01:00
|
|
|
|
# Retrieves all registered time in descending order to present
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# the timeline using {Views#time_entries}.
|
2011-10-31 16:14:54 +01:00
|
|
|
|
def get
|
2013-06-20 23:07:24 +02:00
|
|
|
|
if @input["show"] == "all"
|
2014-10-18 21:25:26 +02:00
|
|
|
|
@time_entries = TimeEntry.order("start DESC")
|
2013-06-20 23:07:24 +02:00
|
|
|
|
else
|
2013-06-22 12:58:23 +02:00
|
|
|
|
@time_entries = TimeEntry.joins(:task)\
|
|
|
|
|
.where("stoptime_tasks.invoice_id" => nil)\
|
|
|
|
|
.order("start DESC")
|
2013-06-20 23:07:24 +02:00
|
|
|
|
end
|
2011-12-10 17:07:10 +01:00
|
|
|
|
@time_entries.each do |te|
|
|
|
|
|
@input["bill_#{te.id}"] = true if te.bill?
|
|
|
|
|
end
|
2011-12-22 16:33:57 +01:00
|
|
|
|
@customer_list = Customer.all.map { |c| [c.id, c.shortest_name] }
|
2011-11-29 17:21:51 +01:00
|
|
|
|
@task_list = Hash.new { |h, k| h[k] = Array.new }
|
|
|
|
|
Task.all.reject { |t| t.billed? }.each do |t|
|
2011-12-22 16:33:57 +01:00
|
|
|
|
@task_list[t.customer.shortest_name] << [t.id, t.name]
|
2011-11-09 16:23:48 +01:00
|
|
|
|
end
|
2011-11-07 17:44:58 +01:00
|
|
|
|
@input["bill"] = true # Bill by default.
|
2011-11-29 17:21:51 +01:00
|
|
|
|
@input["task"] = @time_entries.first.task.id if @time_entries.present?
|
2011-11-01 15:29:55 +01:00
|
|
|
|
render :time_entries
|
2011-10-31 16:14:54 +01:00
|
|
|
|
end
|
|
|
|
|
|
2014-10-25 22:33:05 +02:00
|
|
|
|
# Registers a time entry and redirects back to the referer.
|
2011-11-10 18:24:11 +01:00
|
|
|
|
# If the provided information was invalid, the errors are retrieved.
|
2011-11-01 15:29:55 +01:00
|
|
|
|
def post
|
|
|
|
|
if @input.has_key? "enter"
|
2011-11-09 15:12:29 +01:00
|
|
|
|
@time_entry = TimeEntry.create(
|
2011-11-01 15:29:55 +01:00
|
|
|
|
:task_id => @input.task,
|
2011-11-11 14:55:17 +01:00
|
|
|
|
:date => @input.date,
|
2011-11-11 15:18:51 +01:00
|
|
|
|
:start => "#{@input.date} #{@input.start}",
|
|
|
|
|
:end => "#{@input.date} #{@input.end}",
|
2011-11-03 23:07:42 +01:00
|
|
|
|
:comment => @input.comment,
|
|
|
|
|
:bill => @input.has_key?("bill"))
|
2013-06-16 22:58:15 +02:00
|
|
|
|
# Add a day to the end date if the total hours is negative.
|
|
|
|
|
# It means that the end time was before the begin time, i.e.
|
|
|
|
|
# overnight.
|
|
|
|
|
@time_entry.end += 1.day if @time_entry.hours_total < 0
|
2011-11-09 15:12:29 +01:00
|
|
|
|
@time_entry.save
|
|
|
|
|
if @time_entry.invalid?
|
|
|
|
|
@errors = @time_entry.errors
|
2011-11-01 15:29:55 +01:00
|
|
|
|
end
|
|
|
|
|
end
|
2013-06-20 22:39:00 +02:00
|
|
|
|
redirect @request.referer
|
2011-11-09 17:01:13 +01:00
|
|
|
|
end
|
2014-11-01 21:37:41 +01:00
|
|
|
|
end # class StopTime::Controllers::Timeline
|
2011-11-01 15:29:55 +01:00
|
|
|
|
|
2011-11-10 18:24:11 +01:00
|
|
|
|
# == The timeline quick register controller
|
|
|
|
|
#
|
|
|
|
|
# Controller that presents a view for quickly registering time
|
|
|
|
|
# on a task.
|
|
|
|
|
#
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# path:: +/timeline/new+
|
|
|
|
|
# view:: {Views#time_entry_form}
|
2011-11-09 17:01:13 +01:00
|
|
|
|
class TimelineNew
|
2011-11-10 18:24:11 +01:00
|
|
|
|
# Retrieves a list of customers and tasks and the current date
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# and time for prefilling a form ({Views#time_entry_form}) for quickly
|
2011-11-10 18:24:11 +01:00
|
|
|
|
# registering time.
|
2011-11-09 17:01:13 +01:00
|
|
|
|
def get
|
2011-12-22 16:33:57 +01:00
|
|
|
|
@customer_list = Customer.all.map { |c| [c.id, c.shortest_name] }
|
2013-06-26 20:47:09 +02:00
|
|
|
|
@task_list = Hash.new { |h, k| h[k] = Array.new }
|
|
|
|
|
Task.all.reject { |t| t.billed? }.each do |t|
|
|
|
|
|
@task_list[t.customer.shortest_name] << [t.id, t.name]
|
|
|
|
|
end
|
2011-11-09 17:01:13 +01:00
|
|
|
|
@input["bill"] = true
|
2011-11-11 15:18:51 +01:00
|
|
|
|
@input["date"] = DateTime.now.to_date
|
|
|
|
|
@input["start"] = Time.now.to_formatted_s(:time_only)
|
2011-11-09 17:01:13 +01:00
|
|
|
|
|
|
|
|
|
@target = [Timeline]
|
|
|
|
|
@button = "enter"
|
|
|
|
|
render :time_entry_form
|
2011-10-31 16:14:54 +01:00
|
|
|
|
end
|
2014-11-01 21:37:41 +01:00
|
|
|
|
end # class StopTime::Controllers::TimelineNew
|
2011-11-01 15:29:55 +01:00
|
|
|
|
|
2011-11-10 18:24:11 +01:00
|
|
|
|
# == The timeline time entry controller
|
|
|
|
|
#
|
|
|
|
|
# Controller for viewing and updating information of a time entry.
|
|
|
|
|
#
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# path:: +/timeline/+_entry_id_
|
|
|
|
|
# view:: {Views#time_entry_form}
|
2011-11-08 13:08:51 +01:00
|
|
|
|
class TimelineN
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# Finds the time entry with the given time entry ID and shows
|
|
|
|
|
# a form for updating using {Views#time_entry_form}.
|
|
|
|
|
#
|
|
|
|
|
# @param [Fixnum] entry_id ID of the time entry
|
2011-11-07 17:44:58 +01:00
|
|
|
|
def get(entry_id)
|
2011-11-09 15:12:29 +01:00
|
|
|
|
@time_entry = TimeEntry.find(entry_id)
|
|
|
|
|
@input = @time_entry.attributes
|
|
|
|
|
@input["customer"] = @time_entry.task.customer.id
|
|
|
|
|
@input["task"] = @time_entry.task.id
|
2011-11-11 15:18:51 +01:00
|
|
|
|
@input["date"] = @time_entry.date.to_date
|
|
|
|
|
@input["start"] = @time_entry.start.to_formatted_s(:time_only)
|
|
|
|
|
@input["end"] = @time_entry.end.to_formatted_s(:time_only)
|
2011-12-22 16:33:57 +01:00
|
|
|
|
@customer_list = Customer.all.map { |c| [c.id, c.shortest_name] }
|
2013-06-26 20:47:09 +02:00
|
|
|
|
@task_list = Hash.new { |h, k| h[k] = Array.new }
|
2014-10-18 21:25:26 +02:00
|
|
|
|
Task.order("name ASC, invoice_id ASC").each do |t|
|
2012-01-31 22:51:31 +01:00
|
|
|
|
name = t.billed? ? t.name + " (#{t.invoice.number})" : t.name
|
2013-06-26 20:47:09 +02:00
|
|
|
|
@task_list[t.customer.shortest_name] << [t.id, name]
|
2012-01-31 22:51:31 +01:00
|
|
|
|
end
|
2011-11-09 17:01:13 +01:00
|
|
|
|
|
|
|
|
|
@target = [TimelineN, entry_id]
|
|
|
|
|
@button = "update"
|
2011-11-07 17:44:58 +01:00
|
|
|
|
render :time_entry_form
|
|
|
|
|
end
|
|
|
|
|
|
2011-11-10 18:24:11 +01:00
|
|
|
|
# Updates or deletes the time entry if the input is valid and redirects
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# to {Timeline}.
|
2011-11-10 18:24:11 +01:00
|
|
|
|
# If the provided information is invalid, the errors are retrieved
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# and shown in the initial form ({Views#time_entry_form}).
|
|
|
|
|
#
|
|
|
|
|
# @param [Fixnum] entry_id ID of the time entry
|
2011-11-01 15:29:55 +01:00
|
|
|
|
def post(entry_id)
|
2011-11-08 13:08:51 +01:00
|
|
|
|
return redirect R(Timeline) if @input.cancel
|
2011-11-09 15:12:29 +01:00
|
|
|
|
@time_entry = TimeEntry.find(entry_id)
|
2011-11-07 17:44:58 +01:00
|
|
|
|
if @input.has_key? "delete"
|
2011-11-09 15:12:29 +01:00
|
|
|
|
@time_entry.delete
|
2011-11-07 17:44:58 +01:00
|
|
|
|
elsif @input.has_key? "update"
|
2011-11-16 09:47:40 +01:00
|
|
|
|
attrs = ["date", "comment"]
|
2011-11-07 17:44:58 +01:00
|
|
|
|
attrs.each do |attr|
|
2011-11-09 15:12:29 +01:00
|
|
|
|
@time_entry[attr] = @input[attr]
|
2011-11-07 17:44:58 +01:00
|
|
|
|
end
|
2011-11-16 09:47:40 +01:00
|
|
|
|
@time_entry.start = "#{@input["date"]} #{@input["start"]}"
|
|
|
|
|
@time_entry.end = "#{@input["date"]} #{@input["end"]}"
|
2011-11-09 15:12:29 +01:00
|
|
|
|
@time_entry.task = Task.find(@input.task)
|
|
|
|
|
@time_entry.bill = @input.has_key? "bill"
|
2013-06-16 22:58:15 +02:00
|
|
|
|
# Add a day to the end date if the total hours is negative.
|
|
|
|
|
@time_entry.end += 1.day if @time_entry.hours_total < 0
|
2011-11-09 15:12:29 +01:00
|
|
|
|
@time_entry.save
|
|
|
|
|
if @time_entry.invalid?
|
|
|
|
|
@errors = @time_entry.errors
|
2011-11-07 17:44:58 +01:00
|
|
|
|
return render :time_entry_form
|
|
|
|
|
end
|
|
|
|
|
end
|
2013-06-20 22:39:00 +02:00
|
|
|
|
redirect @request.referer
|
2011-11-01 15:29:55 +01:00
|
|
|
|
end
|
2014-11-01 21:37:41 +01:00
|
|
|
|
end # class StopTime::Controllers::TimelineN
|
2011-11-01 15:29:55 +01:00
|
|
|
|
|
2011-11-10 18:24:11 +01:00
|
|
|
|
# == The invoices controller
|
|
|
|
|
#
|
|
|
|
|
# Controller for viewing a list of all invoices.
|
|
|
|
|
#
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# path:: +/invoices+
|
|
|
|
|
# view:: {Views#invoices}
|
2011-11-01 15:29:55 +01:00
|
|
|
|
class Invoices
|
2011-11-10 18:24:11 +01:00
|
|
|
|
# Retrieves the list of invoices, sorted per customer, and displays
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# them using {Views#invoices}.
|
2011-11-01 15:29:55 +01:00
|
|
|
|
def get
|
2011-11-09 16:03:02 +01:00
|
|
|
|
@invoices = {}
|
2013-06-20 22:06:28 +02:00
|
|
|
|
@invoice_count = 0
|
2011-11-09 16:03:02 +01:00
|
|
|
|
Customer.all.each do |customer|
|
2013-06-20 22:06:28 +02:00
|
|
|
|
invoices = customer.invoices
|
|
|
|
|
next if invoices.empty?
|
2014-10-25 18:02:32 +02:00
|
|
|
|
@invoices[customer] = invoices
|
2013-06-20 22:06:28 +02:00
|
|
|
|
invoices.each do |i|
|
2011-12-10 16:54:43 +01:00
|
|
|
|
@input["paid_#{i.number}"] = true if i.paid?
|
|
|
|
|
end
|
2013-06-20 22:06:28 +02:00
|
|
|
|
@invoice_count += invoices.count
|
2011-11-09 16:03:02 +01:00
|
|
|
|
end
|
|
|
|
|
render :invoices
|
|
|
|
|
end
|
2014-11-01 21:37:41 +01:00
|
|
|
|
end # class StopTime::Controllers::Invoices
|
2011-11-09 16:03:02 +01:00
|
|
|
|
|
2011-11-10 18:24:11 +01:00
|
|
|
|
# == The invoices per period controller
|
|
|
|
|
#
|
|
|
|
|
# Controller for viewing a list of all invoices sorted by period.
|
|
|
|
|
#
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# path:: +/invoices/period+
|
|
|
|
|
# view:: {Views#invoices}
|
2011-11-09 16:03:02 +01:00
|
|
|
|
class InvoicesPeriod
|
2011-11-10 18:24:11 +01:00
|
|
|
|
# Retrieves the list of invoices, sorted per period, and displays
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# them using {Views#invoices}.
|
2011-11-09 16:03:02 +01:00
|
|
|
|
def get
|
|
|
|
|
@invoices = Hash.new { |h, k| h[k] = Array.new }
|
|
|
|
|
Invoice.all.each do |invoice|
|
|
|
|
|
# FIXME: this is an unformatted key!
|
|
|
|
|
@invoices[invoice.period.first.at_beginning_of_month] << invoice
|
|
|
|
|
end
|
2011-11-01 15:29:55 +01:00
|
|
|
|
render :invoices
|
|
|
|
|
end
|
|
|
|
|
end
|
2011-11-03 11:00:35 +01:00
|
|
|
|
|
2011-11-10 18:24:11 +01:00
|
|
|
|
# == The company controller
|
|
|
|
|
#
|
|
|
|
|
# Controller for viewing and updating information of the company of
|
|
|
|
|
# the user (stored in Models::CompanyInfo).
|
|
|
|
|
#
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# path:: +/company+
|
|
|
|
|
# view:: {Views#company_form}
|
2011-11-07 14:54:11 +01:00
|
|
|
|
class Company
|
2011-11-10 18:24:11 +01:00
|
|
|
|
# Retrieves the company information and shows a form for updating
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# using {Views#company_form}.
|
2011-11-07 14:54:11 +01:00
|
|
|
|
def get
|
2014-10-18 21:25:26 +02:00
|
|
|
|
@company = if @input.revision.present?
|
|
|
|
|
CompanyInfo.find(@input.revision)
|
|
|
|
|
else
|
|
|
|
|
CompanyInfo.last
|
|
|
|
|
end
|
2011-11-07 17:43:10 +01:00
|
|
|
|
@input = @company.attributes
|
2012-01-09 15:56:40 +01:00
|
|
|
|
@history_warn = true if @company != CompanyInfo.last
|
2011-11-07 14:54:11 +01:00
|
|
|
|
render :company_form
|
|
|
|
|
end
|
|
|
|
|
|
2011-11-10 18:24:11 +01:00
|
|
|
|
# Updates the company information and shows the updated form
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# ({Views#company_form}).
|
2011-11-10 18:24:11 +01:00
|
|
|
|
# If the provided information was invalid, the errors are retrieved.
|
2011-11-07 14:54:11 +01:00
|
|
|
|
def post
|
2014-10-18 21:25:26 +02:00
|
|
|
|
@company = if @input.revision.present?
|
|
|
|
|
CompanyInfo.find(@input.revision)
|
|
|
|
|
else
|
|
|
|
|
CompanyInfo.last
|
|
|
|
|
end
|
2012-01-09 15:52:09 +01:00
|
|
|
|
# If we are editing the current info and it is already associated
|
|
|
|
|
# with some invoices, create a new revision.
|
|
|
|
|
@history_warn = true if @company != CompanyInfo.last
|
|
|
|
|
if @company == CompanyInfo.last and @company.invoices.length > 0
|
|
|
|
|
old_company = @company
|
|
|
|
|
@company = old_company.clone # FIXME: depends on rails versioN!
|
|
|
|
|
@company.original = old_company
|
|
|
|
|
end
|
|
|
|
|
|
2011-11-07 14:54:11 +01:00
|
|
|
|
attrs = ["name", "contact_name",
|
|
|
|
|
"address_street", "address_postal_code", "address_city",
|
|
|
|
|
"country", "country_code",
|
|
|
|
|
"phone", "cell", "email", "website",
|
2012-01-03 16:51:07 +01:00
|
|
|
|
"chamber", "vatno",
|
|
|
|
|
"bank_name", "bank_bic", "accountno", "accountiban"]
|
2011-11-07 14:54:11 +01:00
|
|
|
|
attrs.each do |attr|
|
|
|
|
|
@company[attr] = @input[attr]
|
|
|
|
|
end
|
|
|
|
|
@company.save
|
|
|
|
|
if @company.invalid?
|
|
|
|
|
@errors = @company.errors
|
|
|
|
|
end
|
|
|
|
|
render :company_form
|
|
|
|
|
end
|
2014-11-01 21:37:41 +01:00
|
|
|
|
end # class StopTime::Controllers::Company
|
2011-11-07 14:54:11 +01:00
|
|
|
|
|
2011-11-10 18:24:11 +01:00
|
|
|
|
# == The static data controller
|
|
|
|
|
#
|
|
|
|
|
# Controller for serving static data information available in the
|
|
|
|
|
# +public/+ subdirectory.
|
|
|
|
|
#
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# path:: +/static/+_path_
|
2011-11-10 18:24:11 +01:00
|
|
|
|
# view:: N/A (X-Sendfile)
|
2011-11-15 17:01:52 +01:00
|
|
|
|
class Static < R '/static/(.*?)'
|
2011-11-10 18:24:11 +01:00
|
|
|
|
# Sets the headers such that the web server will fetch and offer
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# the file identified by the path relative to the +public/+ subdirectory.
|
|
|
|
|
#
|
|
|
|
|
# @param [String] path the relative path to a public data file
|
2011-11-03 11:00:35 +01:00
|
|
|
|
def get(path)
|
|
|
|
|
unless path.include? ".."
|
2011-11-15 17:22:57 +01:00
|
|
|
|
full_path = PUBLIC_DIR + path
|
|
|
|
|
@headers['Content-Type'] = Rack::Mime.mime_type(full_path.extname)
|
|
|
|
|
@headers['X-Sendfile'] = full_path.to_s
|
2011-11-03 11:00:35 +01:00
|
|
|
|
else
|
|
|
|
|
@status = "403"
|
|
|
|
|
"Error 403: Invalid path: #{path}"
|
|
|
|
|
end
|
|
|
|
|
end
|
2014-11-01 21:37:41 +01:00
|
|
|
|
end # class StopTime::Controllers::Static
|
2011-11-03 11:00:35 +01:00
|
|
|
|
|
2011-10-31 14:36:01 +01:00
|
|
|
|
end # module StopTime::Controllers
|
|
|
|
|
|
2011-11-10 18:24:11 +01:00
|
|
|
|
# = The Stop… Camping Time! views
|
2011-10-31 14:36:01 +01:00
|
|
|
|
module StopTime::Views
|
|
|
|
|
|
2011-11-10 18:24:11 +01:00
|
|
|
|
# The main layout used by all views.
|
2014-11-01 21:37:41 +01:00
|
|
|
|
#
|
|
|
|
|
# @return [Mab::Mixin::Tag] the main layout
|
2011-10-31 14:36:01 +01:00
|
|
|
|
def layout
|
2014-10-18 21:27:46 +02:00
|
|
|
|
doctype!
|
2013-06-16 20:27:11 +02:00
|
|
|
|
html(:lang => "en") do
|
2011-10-31 14:36:01 +01:00
|
|
|
|
head do
|
|
|
|
|
title "Stop… Camping Time!"
|
2013-06-16 20:27:11 +02:00
|
|
|
|
meta :name => "viewport",
|
|
|
|
|
:content => "width=device-width, initial-scale=1.0"
|
|
|
|
|
# Bootstrap CSS
|
|
|
|
|
link :rel => "stylesheet", :type => "text/css",
|
|
|
|
|
:media => "screen",
|
|
|
|
|
:href => (R(Static, "") + "stylesheets/bootstrap.min.css")
|
2011-11-15 17:01:52 +01:00
|
|
|
|
# FIXME: improve static serving so that the hack below is not needed.
|
2011-11-09 22:55:59 +01:00
|
|
|
|
link :rel => "stylesheet", :type => "text/css",
|
2011-11-15 17:01:52 +01:00
|
|
|
|
:media => "screen",
|
|
|
|
|
:href => (R(Static, "") + "stylesheets/style.css")
|
2013-06-20 21:15:06 +02:00
|
|
|
|
# Responsive bootstrap CSS
|
|
|
|
|
link :rel => "stylesheet", :type => "text/css",
|
|
|
|
|
:href => (R(Static, "") + "stylesheets/bootstrap-responsive.min.css")
|
2011-10-31 14:36:01 +01:00
|
|
|
|
end
|
|
|
|
|
body do
|
2013-06-16 20:27:11 +02:00
|
|
|
|
_menu
|
2013-06-20 21:15:52 +02:00
|
|
|
|
div.container do
|
|
|
|
|
self << yield
|
|
|
|
|
footer { br }
|
|
|
|
|
end
|
2013-06-16 20:27:11 +02:00
|
|
|
|
# JQuery and Bootstrap JavaScript
|
2013-06-23 22:32:21 +02:00
|
|
|
|
script :src => (R(Static, "") + "javascripts/jquery.min.js")
|
2013-06-16 20:27:11 +02:00
|
|
|
|
script :src => (R(Static, "") + "javascripts/bootstrap.min.js")
|
2011-10-31 14:36:01 +01:00
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
2011-11-10 18:24:11 +01:00
|
|
|
|
# The main overview showing accumulated time per task per customer.
|
2014-11-01 21:37:41 +01:00
|
|
|
|
#
|
|
|
|
|
# @return [Mab::Mixin::Tag] the main overview
|
2011-11-09 16:02:43 +01:00
|
|
|
|
def overview
|
2013-06-20 22:06:28 +02:00
|
|
|
|
header.page_header do
|
|
|
|
|
h1 do
|
|
|
|
|
text! "Overview"
|
|
|
|
|
small "#{@tasks.count} customers, #{@task_count} active projects/tasks"
|
2011-11-09 16:02:43 +01:00
|
|
|
|
end
|
2013-06-16 20:27:11 +02:00
|
|
|
|
end
|
2014-10-18 21:28:18 +02:00
|
|
|
|
if @tasks.empty?
|
|
|
|
|
div.alert.alert_info do
|
|
|
|
|
text! "No customers, projects or tasks found! Set them up " +
|
|
|
|
|
"#{a "here", :href => R(CustomersNew)}."
|
|
|
|
|
end
|
|
|
|
|
else
|
|
|
|
|
div.row do
|
2014-11-01 17:47:52 +01:00
|
|
|
|
@tasks.keys.sort_by { |c| c.name }.each do |customer|
|
|
|
|
|
div.span6 do
|
2013-06-20 21:15:52 +02:00
|
|
|
|
inv_klass = "text_info"
|
|
|
|
|
inv_klass = "text_warning" if customer.invoices.any? { |inv| inv.past_due? }
|
|
|
|
|
inv_klass = "text_error" if customer.invoices.any? { |inv| inv.way_past_due? }
|
|
|
|
|
h3 { a customer.name,
|
|
|
|
|
:class => inv_klass,
|
|
|
|
|
:href => R(CustomersN, customer.id) }
|
|
|
|
|
if @tasks[customer].empty?
|
|
|
|
|
p do
|
|
|
|
|
text! "No projects/tasks found! Create one " +
|
|
|
|
|
"#{a "here", :href => R(CustomersNTasksNew, customer.id)}."
|
|
|
|
|
end
|
|
|
|
|
else
|
|
|
|
|
table.table.table_condensed do
|
|
|
|
|
col.task
|
|
|
|
|
col.hours
|
|
|
|
|
col.amount
|
|
|
|
|
@tasks[customer].each do |task|
|
|
|
|
|
tr do
|
|
|
|
|
summary = task.summary
|
|
|
|
|
td do
|
|
|
|
|
a task.name,
|
|
|
|
|
:href => R(CustomersNTasksN, customer.id, task.id)
|
2013-06-16 20:27:11 +02:00
|
|
|
|
end
|
2013-06-20 21:15:52 +02:00
|
|
|
|
summary = task.summary
|
|
|
|
|
td.text_right { "%.2fh" % summary[0] }
|
|
|
|
|
td.text_right { "€ %.2f" % summary[2] }
|
2013-06-16 20:27:11 +02:00
|
|
|
|
end
|
2011-11-09 16:02:43 +01:00
|
|
|
|
end
|
2014-11-01 17:47:13 +01:00
|
|
|
|
tr do
|
|
|
|
|
td { b "Total" }
|
|
|
|
|
td.text_right { "%.2fh" % @tasks_summary[customer][0] }
|
|
|
|
|
td.text_right { "€ %.2f" % @tasks_summary[customer][1] }
|
|
|
|
|
end
|
2011-11-09 16:02:43 +01:00
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
2011-11-10 18:24:11 +01:00
|
|
|
|
# The main overview showing the timeline of registered time.
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# If a task ID is given as an argument, the task column will be hidden
|
|
|
|
|
# and it will be assumed it is used as a partial view.
|
|
|
|
|
#
|
2011-12-12 17:09:35 +01:00
|
|
|
|
# FIXME: This should be done in a nicer way.
|
2014-11-01 21:37:41 +01:00
|
|
|
|
#
|
|
|
|
|
# @param [Fixnum, nil] task_id ID of a task
|
|
|
|
|
# @return [Mab::Mixin::Tag] the main timeline (time entry list) overview
|
2011-12-12 17:09:35 +01:00
|
|
|
|
def time_entries(task_id=nil)
|
2013-06-20 22:06:28 +02:00
|
|
|
|
if task_id.present?
|
|
|
|
|
h2 "Registered #{@task.billed? ? "billed" : "unbilled"} time"
|
|
|
|
|
else
|
|
|
|
|
header.page_header do
|
|
|
|
|
h1 do
|
|
|
|
|
text! "Timeline"
|
|
|
|
|
small "#{@time_entries.count} time entries"
|
2013-06-20 23:07:24 +02:00
|
|
|
|
div.btn_group.pull_right do
|
|
|
|
|
a.btn.btn_small.dropdown_toggle :href => "#", "data-toggle" => "dropdown" do
|
|
|
|
|
text! @input["show"] == "all" ? "All" : "Unbilled"
|
|
|
|
|
span.caret
|
|
|
|
|
end
|
|
|
|
|
ul.dropdown_menu :role => "menu", :aria_labelledby => "dLabel" do
|
|
|
|
|
li { a "All", :href => R(Timeline, :show => "all") }
|
|
|
|
|
li { a "Unbilled", :href => R(Timeline, :show => "unbilled") }
|
|
|
|
|
end
|
|
|
|
|
end
|
2013-06-20 22:06:28 +02:00
|
|
|
|
end
|
2013-06-20 21:15:52 +02:00
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
table.table.table_condensed.table_striped.table_hover do
|
|
|
|
|
unless task_id.present?
|
|
|
|
|
col.customer_short
|
|
|
|
|
col.task
|
|
|
|
|
end
|
|
|
|
|
col.date
|
|
|
|
|
col.start_time
|
|
|
|
|
col.end_time
|
|
|
|
|
col.comment
|
|
|
|
|
col.hours
|
|
|
|
|
col.flag
|
|
|
|
|
thead do
|
|
|
|
|
tr do
|
|
|
|
|
unless task_id.present?
|
|
|
|
|
th "Customer"
|
|
|
|
|
th "Project/Task"
|
|
|
|
|
end
|
|
|
|
|
th "Date"
|
|
|
|
|
th "Start"
|
|
|
|
|
th "End"
|
|
|
|
|
th "Comment"
|
|
|
|
|
th "Total"
|
|
|
|
|
th "Bill?"
|
|
|
|
|
th {}
|
2013-06-16 20:27:11 +02:00
|
|
|
|
end
|
|
|
|
|
end
|
2013-06-20 21:15:52 +02:00
|
|
|
|
tbody do
|
|
|
|
|
form.form_inline :action => R(Timeline), :method => :post do
|
2013-06-16 20:27:11 +02:00
|
|
|
|
tr do
|
2013-06-20 21:15:52 +02:00
|
|
|
|
if task_id.present?
|
|
|
|
|
input :type => :hidden, :name => "task", :value => task_id
|
|
|
|
|
else
|
|
|
|
|
td { }
|
|
|
|
|
td { _form_select_nested("task", @task_list, :class => "task") }
|
|
|
|
|
end
|
|
|
|
|
td { input.date :type => :text, :name => "date",
|
|
|
|
|
:value => DateTime.now.to_date.to_formatted_s }
|
|
|
|
|
td { input.start_time :type => :text, :name => "start",
|
|
|
|
|
:value => DateTime.now.to_time.to_formatted_s(:time_only) }
|
|
|
|
|
td { input.end_time :type => :text, :name => "end" }
|
|
|
|
|
td { input.comment :type => :text, :name => "comment" }
|
|
|
|
|
td { "N/A" }
|
|
|
|
|
td { _form_input_checkbox("bill") }
|
|
|
|
|
td do
|
|
|
|
|
button.btn.btn_small.btn_primary "Enter", :type => :submit, :name => "enter", :value => "Enter"
|
2013-06-16 20:27:11 +02:00
|
|
|
|
end
|
2011-11-01 15:29:55 +01:00
|
|
|
|
end
|
|
|
|
|
end
|
2013-06-20 21:15:52 +02:00
|
|
|
|
@time_entries.each do |entry|
|
|
|
|
|
tr(:class => entry.task.billed? ? "billed" : nil) do
|
|
|
|
|
unless task_id.present?
|
2013-06-16 20:27:11 +02:00
|
|
|
|
td do
|
2013-06-20 21:15:52 +02:00
|
|
|
|
a entry.customer.shortest_name,
|
|
|
|
|
:title => entry.customer.shortest_name,
|
|
|
|
|
:href => R(CustomersN, entry.customer.id)
|
2013-06-16 20:27:11 +02:00
|
|
|
|
end
|
|
|
|
|
td do
|
2013-06-20 21:15:52 +02:00
|
|
|
|
a entry.task.name,
|
|
|
|
|
:title => entry.task.name,
|
|
|
|
|
:href => R(CustomersNTasksN, entry.customer.id, entry.task.id)
|
2013-06-16 20:27:11 +02:00
|
|
|
|
end
|
2013-06-20 21:15:52 +02:00
|
|
|
|
end
|
2013-06-20 22:08:01 +02:00
|
|
|
|
td { entry.date.to_date }
|
2013-06-20 21:15:52 +02:00
|
|
|
|
td { entry.start.to_formatted_s(:time_only) }
|
|
|
|
|
td { entry.end.to_formatted_s(:time_only)}
|
2013-07-13 22:49:41 +02:00
|
|
|
|
if entry.comment.present?
|
2013-06-23 22:33:44 +02:00
|
|
|
|
td { a entry.comment, :href => R(TimelineN, entry.id),
|
|
|
|
|
:title => entry.comment }
|
2013-07-13 22:49:41 +02:00
|
|
|
|
else
|
2014-10-25 18:31:08 +02:00
|
|
|
|
td { a(:href => R(TimelineN, entry.id)) { i "None" } }
|
2013-06-23 22:33:44 +02:00
|
|
|
|
end
|
2013-06-20 21:15:52 +02:00
|
|
|
|
td { "%.2fh" % entry.hours_total }
|
|
|
|
|
td do
|
|
|
|
|
i(:class => "icon-ok") if entry.bill?
|
|
|
|
|
end
|
|
|
|
|
td do
|
|
|
|
|
form.form_inline :action => R(TimelineN, entry.id), :method => :post do
|
|
|
|
|
button.btn.btn_mini.btn_danger "Delete", :type => :submit, :name => "delete", :value => "Delete"
|
2013-06-16 20:27:11 +02:00
|
|
|
|
end
|
2011-11-01 15:29:55 +01:00
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
2011-10-31 16:14:54 +01:00
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# Form for editing a time entry ({Models::TimeEntry}).
|
|
|
|
|
#
|
|
|
|
|
# @return [Mab::Mixin::Tag] the time entry form
|
2011-11-07 17:44:58 +01:00
|
|
|
|
def time_entry_form
|
2013-06-20 22:06:28 +02:00
|
|
|
|
header.page_header do
|
|
|
|
|
h1 do
|
|
|
|
|
text! "Time Entry Information"
|
|
|
|
|
small @input["comment"]
|
|
|
|
|
end
|
2013-06-20 21:15:52 +02:00
|
|
|
|
end
|
|
|
|
|
div.alert do
|
|
|
|
|
button.close(:type => "button", "data-dismiss" => "alert") { "×" }
|
|
|
|
|
strong "Warning!"
|
|
|
|
|
text! "This time entry is already billed! Only make changes if you know " +
|
|
|
|
|
"what you are doing!"
|
|
|
|
|
end if @time_entry.present? and @time_entry.task.billed?
|
|
|
|
|
form.form_horizontal.form_condensed :action => R(*@target), :method => :post do
|
|
|
|
|
div.control_group do
|
|
|
|
|
label.control_label "Customer", :for => "customer"
|
|
|
|
|
div.controls do
|
|
|
|
|
_form_select("customer", @customer_list)
|
2014-10-25 18:30:50 +02:00
|
|
|
|
a.btn "» Go to customer", :href => R(CustomersN, @time_entry.customer.id)
|
2011-11-07 17:44:58 +01:00
|
|
|
|
end
|
2013-06-20 21:15:52 +02:00
|
|
|
|
end
|
|
|
|
|
div.control_group do
|
2014-10-25 18:31:08 +02:00
|
|
|
|
label.control_label "Project/Task", :for => "task"
|
2013-06-20 21:15:52 +02:00
|
|
|
|
div.controls do
|
2013-06-26 20:47:09 +02:00
|
|
|
|
_form_select_nested("task", @task_list)
|
2014-10-25 18:30:50 +02:00
|
|
|
|
a.btn "» Go to project/task", :href => R(CustomersNTasksN,
|
|
|
|
|
@time_entry.customer.id,
|
|
|
|
|
@time_entry.task.id)
|
2012-01-03 17:38:54 +01:00
|
|
|
|
end
|
2013-06-20 21:15:52 +02:00
|
|
|
|
end
|
|
|
|
|
if @time_entry.present? and @time_entry.task.billed?
|
2013-06-16 20:27:11 +02:00
|
|
|
|
div.control_group do
|
2013-06-20 21:15:52 +02:00
|
|
|
|
label.control_label "Billed in invoice"
|
2013-06-16 20:27:11 +02:00
|
|
|
|
div.controls do
|
2013-06-20 21:15:52 +02:00
|
|
|
|
a @time_entry.task.invoice.number,
|
|
|
|
|
:href => R(CustomersNInvoicesX, @time_entry.customer.id,
|
|
|
|
|
@time_entry.task.invoice.number)
|
2013-06-16 20:27:11 +02:00
|
|
|
|
end
|
2011-11-07 17:44:58 +01:00
|
|
|
|
end
|
2013-06-20 21:15:52 +02:00
|
|
|
|
end
|
|
|
|
|
_form_input_with_label("Date", "date", :text, :class => "input-small")
|
|
|
|
|
_form_input_with_label("Start Time", "start", :text, :class => "input-mini")
|
|
|
|
|
_form_input_with_label("End Time", "end", :text, :class => "input-mini")
|
|
|
|
|
_form_input_with_label("Comment", "comment", :text, :class => "input-xxlarge")
|
|
|
|
|
div.control_group do
|
|
|
|
|
label.control_label "Bill?", :for => "bill"
|
|
|
|
|
div.controls do
|
|
|
|
|
_form_input_checkbox("bill")
|
2013-06-16 20:27:11 +02:00
|
|
|
|
end
|
2011-11-07 17:44:58 +01:00
|
|
|
|
end
|
2013-06-20 21:15:52 +02:00
|
|
|
|
# FIXME: link to invoice if any
|
|
|
|
|
div.form_actions do
|
|
|
|
|
button.btn.btn_primary @button.capitalize, :type => "submit",
|
|
|
|
|
:name => @button, :value => @button.capitalize
|
|
|
|
|
button.btn "Cancel", :type => "submit",
|
|
|
|
|
:name => "cancel", :value => "Cancel"
|
|
|
|
|
end
|
2011-11-07 17:44:58 +01:00
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
2011-11-10 18:24:11 +01:00
|
|
|
|
# The main overview of the list of customers.
|
2014-11-01 21:37:41 +01:00
|
|
|
|
#
|
|
|
|
|
# @return [Mab::Mixin::Tag] the customer list overview
|
2011-10-31 16:14:54 +01:00
|
|
|
|
def customers
|
2013-06-20 22:06:28 +02:00
|
|
|
|
header.page_header do
|
2013-06-26 20:23:03 +02:00
|
|
|
|
h1 do
|
|
|
|
|
text! "Customers"
|
|
|
|
|
div.btn_group.pull_right do
|
|
|
|
|
a.btn.btn_small "» Add a new customer", :href=> R(CustomersNew)
|
|
|
|
|
end
|
|
|
|
|
end
|
2013-06-16 20:27:11 +02:00
|
|
|
|
end
|
2013-06-20 21:15:52 +02:00
|
|
|
|
if @customers.empty?
|
2014-10-18 21:28:18 +02:00
|
|
|
|
div.alert.alert_info do
|
2013-06-20 21:15:52 +02:00
|
|
|
|
text! "None found! You can create one " +
|
|
|
|
|
"#{a "here", :href => R(CustomersNew)}."
|
|
|
|
|
end
|
|
|
|
|
else
|
|
|
|
|
table.table.table_striped.table_condensed do
|
|
|
|
|
col.name
|
|
|
|
|
col.short_name
|
|
|
|
|
col.address
|
|
|
|
|
col.email
|
|
|
|
|
col.phone
|
|
|
|
|
thead do
|
|
|
|
|
tr do
|
|
|
|
|
th "Name"
|
|
|
|
|
th "Short name"
|
|
|
|
|
th "Address"
|
|
|
|
|
th "Email"
|
|
|
|
|
th "Phone"
|
|
|
|
|
th {}
|
|
|
|
|
end
|
2013-06-16 20:27:11 +02:00
|
|
|
|
end
|
2013-06-20 21:15:52 +02:00
|
|
|
|
tbody do
|
|
|
|
|
@customers.each do |customer|
|
2013-06-16 20:27:11 +02:00
|
|
|
|
tr do
|
2013-06-20 21:15:52 +02:00
|
|
|
|
td { a customer.name, :href => R(CustomersN, customer.id) }
|
|
|
|
|
td { customer.short_name || "–"}
|
|
|
|
|
td do
|
|
|
|
|
if customer.address_street.present?
|
|
|
|
|
text! customer.address_street
|
|
|
|
|
br
|
|
|
|
|
text! customer.address_postal_code + " " +
|
|
|
|
|
customer.address_city
|
|
|
|
|
else
|
|
|
|
|
"–"
|
2013-06-16 20:27:11 +02:00
|
|
|
|
end
|
2013-06-20 21:15:52 +02:00
|
|
|
|
end
|
|
|
|
|
td do
|
|
|
|
|
if customer.email.present?
|
|
|
|
|
a customer.email, :href => "mailto:#{customer.email}"
|
|
|
|
|
else
|
|
|
|
|
"–"
|
2013-06-16 20:27:11 +02:00
|
|
|
|
end
|
2013-06-20 21:15:52 +02:00
|
|
|
|
end
|
|
|
|
|
td do
|
|
|
|
|
if customer.phone.present?
|
|
|
|
|
# FIXME: hardcoded prefix!
|
|
|
|
|
"0#{customer.phone}"
|
|
|
|
|
else
|
|
|
|
|
"–"
|
2013-06-16 20:27:11 +02:00
|
|
|
|
end
|
2013-06-20 21:15:52 +02:00
|
|
|
|
end
|
|
|
|
|
td do
|
|
|
|
|
form :action => R(CustomersN, customer.id), :method => :post do
|
|
|
|
|
button.btn.btn_mini.btn_danger "Delete", :type => :submit,
|
|
|
|
|
:name => "delete", :value => "Delete"
|
2013-06-16 20:27:11 +02:00
|
|
|
|
end
|
2011-11-10 12:51:12 +01:00
|
|
|
|
end
|
2011-11-01 15:29:24 +01:00
|
|
|
|
end
|
|
|
|
|
end
|
2011-10-31 16:14:54 +01:00
|
|
|
|
end
|
2013-06-16 20:27:11 +02:00
|
|
|
|
end
|
2011-11-10 12:51:12 +01:00
|
|
|
|
end
|
2011-10-31 14:36:01 +01:00
|
|
|
|
end
|
2011-11-09 15:14:48 +01:00
|
|
|
|
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# Form for editing the properties of customer ({Models::Customer}) but also
|
2011-11-10 18:24:11 +01:00
|
|
|
|
# for adding/editing/deleting tasks and showing a list of invoices for
|
|
|
|
|
# the customer.
|
2014-11-01 21:37:41 +01:00
|
|
|
|
#
|
|
|
|
|
# @return [Mab::Mixin::Tag] the customer form
|
2011-11-01 15:29:24 +01:00
|
|
|
|
def customer_form
|
2013-06-20 22:06:28 +02:00
|
|
|
|
header.page_header do
|
|
|
|
|
h1 do
|
|
|
|
|
text! "Customer Information"
|
|
|
|
|
small @input["name"]
|
|
|
|
|
end
|
|
|
|
|
end
|
2013-06-20 21:15:52 +02:00
|
|
|
|
div.row do
|
|
|
|
|
div.span6 do
|
2013-06-20 22:06:28 +02:00
|
|
|
|
h2 "Details"
|
2013-06-20 21:15:52 +02:00
|
|
|
|
form.form_horizontal.form_condensed :action => R(*@target), :method => :post do
|
|
|
|
|
_form_input_with_label("Name", "name", :text)
|
|
|
|
|
_form_input_with_label("Short name", "short_name", :text)
|
|
|
|
|
_form_input_with_label("Street address", "address_street", :text)
|
|
|
|
|
_form_input_with_label("Postal code", "address_postal_code", :text)
|
|
|
|
|
_form_input_with_label("City/town", "address_city", :text)
|
|
|
|
|
_form_input_with_label("Email address", "email", :email)
|
|
|
|
|
_form_input_with_label("Phone number", "phone", :tel)
|
|
|
|
|
_form_input_with_label("Financial contact", "financial_contact", :text)
|
|
|
|
|
_form_input_with_label("Default hourly rate", "hourly_rate", :text)
|
2013-07-13 22:32:27 +02:00
|
|
|
|
div.control_group do
|
|
|
|
|
label.control_label "Time specifications?"
|
|
|
|
|
div.controls do
|
|
|
|
|
_form_input_checkbox("time_specification")
|
|
|
|
|
end
|
|
|
|
|
end
|
2013-06-20 21:15:52 +02:00
|
|
|
|
div.form_actions do
|
|
|
|
|
button.btn.btn_primary @button.capitalize, :type => "submit",
|
|
|
|
|
:name => @button, :value => @button.capitalize
|
|
|
|
|
button.btn "Cancel", :type => "submit",
|
|
|
|
|
:name => "cancel", :value => "Cancel"
|
2011-11-01 15:29:55 +01:00
|
|
|
|
end
|
|
|
|
|
end
|
2013-06-20 21:15:52 +02:00
|
|
|
|
end
|
2011-11-09 15:13:39 +01:00
|
|
|
|
|
2013-06-20 21:15:52 +02:00
|
|
|
|
div.span6 do
|
|
|
|
|
if @edit_task
|
2013-06-26 20:23:56 +02:00
|
|
|
|
h2 do
|
|
|
|
|
text! "Projects & Tasks"
|
|
|
|
|
div.btn_group.pull_right do
|
|
|
|
|
a.btn.btn_small "» Add a new project/task",
|
|
|
|
|
:href => R(CustomersNTasksNew, @customer.id)
|
|
|
|
|
end
|
|
|
|
|
end
|
2014-10-18 21:28:18 +02:00
|
|
|
|
if @billed_tasks.empty?
|
|
|
|
|
p "None found!"
|
|
|
|
|
else
|
|
|
|
|
div.accordion.task_list! do
|
|
|
|
|
@billed_tasks.keys.sort_by { |task| task.name }.each do |task|
|
|
|
|
|
div.accordion_group do
|
|
|
|
|
div.accordion_heading do
|
|
|
|
|
span.accordion_toggle do
|
|
|
|
|
a task.name, "data-toggle" => "collapse",
|
|
|
|
|
"data-parent" => "#task_list",
|
|
|
|
|
:href => "#collapse#{task.id}"
|
|
|
|
|
# FXIME: the following is not very RESTful!
|
|
|
|
|
form.form_inline.pull_right :action => R(CustomersNTasks, @customer.id),
|
|
|
|
|
:method => :post do
|
|
|
|
|
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
|
2013-06-26 20:23:56 +02:00
|
|
|
|
end
|
|
|
|
|
end
|
2014-10-18 21:28:18 +02:00
|
|
|
|
div.accordion_body.collapse :id => "collapse#{task.id}" do
|
|
|
|
|
div.accordion_inner do
|
|
|
|
|
if @billed_tasks[task].empty?
|
|
|
|
|
i { "No billed projects/tasks found" }
|
|
|
|
|
else
|
|
|
|
|
table.table.table_condensed do
|
|
|
|
|
col.task_list
|
|
|
|
|
@billed_tasks[task].sort_by { |t| t.invoice.number }.each do |billed_task|
|
|
|
|
|
tr do
|
|
|
|
|
td do
|
|
|
|
|
a billed_task.comment_or_name,
|
|
|
|
|
:href => R(CustomersNTasksN, @customer.id, billed_task.id)
|
|
|
|
|
small do
|
|
|
|
|
text! "(billed in invoice "
|
|
|
|
|
a billed_task.invoice.number,
|
|
|
|
|
:title => billed_task.invoice.number,
|
|
|
|
|
:href => R(CustomersNInvoicesX, @customer.id,
|
|
|
|
|
billed_task.invoice.number)
|
|
|
|
|
text! ")"
|
|
|
|
|
end
|
2013-07-14 14:33:12 +02:00
|
|
|
|
end
|
2014-10-18 21:28:18 +02:00
|
|
|
|
td do
|
|
|
|
|
# FXIME: the following is not very RESTful!
|
|
|
|
|
form.form_inline.pull_right :action => R(CustomersNTasks, @customer.id),
|
|
|
|
|
:method => :post do
|
|
|
|
|
a.btn.btn_mini "Edit",
|
|
|
|
|
:href => R(CustomersNTasksN, @customer.id,
|
|
|
|
|
billed_task.id)
|
|
|
|
|
input :type => :hidden, :name => "task_id",
|
|
|
|
|
:value => billed_task.id
|
|
|
|
|
button.btn.btn_danger.btn_mini "Delete", :type => :submit,
|
|
|
|
|
:name => "delete", :value => "Delete"
|
|
|
|
|
end
|
2013-07-14 14:33:12 +02:00
|
|
|
|
end
|
|
|
|
|
end
|
2013-06-26 20:23:56 +02:00
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
2013-06-16 20:27:11 +02:00
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
2013-06-26 20:23:56 +02:00
|
|
|
|
|
2014-02-07 21:36:09 +01:00
|
|
|
|
h2 do
|
|
|
|
|
text! "Invoices"
|
|
|
|
|
div.btn_group.pull_right do
|
|
|
|
|
a.btn.btn_small "» Create a new invoice",
|
|
|
|
|
:href => R(CustomersNInvoicesNew, @customer.id)
|
|
|
|
|
end
|
|
|
|
|
end
|
2013-06-20 21:15:52 +02:00
|
|
|
|
_invoice_list(@invoices)
|
2013-06-16 20:27:11 +02:00
|
|
|
|
end
|
2011-11-09 22:55:59 +01:00
|
|
|
|
end
|
2011-11-07 13:40:43 +01:00
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# Form for updating the properties of a task ({Models::Task}).
|
|
|
|
|
#
|
|
|
|
|
# @return [Mab::Mixin::Tag] the task form
|
2011-11-07 13:40:43 +01:00
|
|
|
|
def task_form
|
2013-06-20 22:06:28 +02:00
|
|
|
|
header.page_header do
|
|
|
|
|
h1 do
|
|
|
|
|
text! "Task Information"
|
|
|
|
|
small @task.name
|
|
|
|
|
end
|
2013-06-20 21:15:52 +02:00
|
|
|
|
end
|
|
|
|
|
div.alert do
|
|
|
|
|
button.close(:type => "button", "data-dismiss" => "alert") { "×" }
|
|
|
|
|
strong "Warning!"
|
|
|
|
|
text! "This task is already billed! Only make changes if you know " +
|
|
|
|
|
"what you are doing!"
|
|
|
|
|
end if @task.billed?
|
|
|
|
|
form.form_horizontal.form_condensed :action => R(*@target), :method => :post do
|
|
|
|
|
div.control_group do
|
|
|
|
|
label.control_label "Customer", :for => "customer"
|
|
|
|
|
div.controls do
|
|
|
|
|
_form_select("customer", @customer_list)
|
|
|
|
|
a.btn "» Go to customer", :href => R(CustomersN, @customer.id)
|
|
|
|
|
end
|
2013-06-16 20:27:11 +02:00
|
|
|
|
end
|
2013-06-20 21:15:52 +02:00
|
|
|
|
_form_input_with_label("Name", "name", :text)
|
|
|
|
|
div.control_group do
|
|
|
|
|
label.control_label "Project/Task type"
|
|
|
|
|
div.controls do
|
|
|
|
|
label.radio do
|
|
|
|
|
_form_input_radio("type", "hourly_rate", true)
|
|
|
|
|
text!("Hourly rate: ")
|
|
|
|
|
_form_input("hourly_rate", :number, "Hourly rate", :class => "input-small")
|
|
|
|
|
end
|
|
|
|
|
label.radio do
|
|
|
|
|
_form_input_radio("type", "fixed_cost")
|
|
|
|
|
text!("Fixed cost: ")
|
|
|
|
|
_form_input("fixed_cost", :number, "Fixed cost", :class => "input-small")
|
2013-06-16 20:27:11 +02:00
|
|
|
|
end
|
2011-11-28 13:56:46 +01:00
|
|
|
|
end
|
2013-06-20 21:15:52 +02:00
|
|
|
|
end
|
|
|
|
|
_form_input_with_label("VAT rate", "vat_rate", :number, :class => "input-small")
|
|
|
|
|
if @task.billed?
|
2013-06-16 20:27:11 +02:00
|
|
|
|
div.control_group do
|
2013-06-20 21:15:52 +02:00
|
|
|
|
label.control_label "Billed in invoice"
|
2013-06-16 20:27:11 +02:00
|
|
|
|
div.controls do
|
2013-06-20 21:15:52 +02:00
|
|
|
|
a @task.invoice.number,
|
|
|
|
|
:href => R(CustomersNInvoicesX, @customer.id, @task.invoice.number)
|
2011-11-07 13:40:43 +01:00
|
|
|
|
end
|
2011-11-09 15:14:48 +01:00
|
|
|
|
end
|
2013-06-20 21:15:52 +02:00
|
|
|
|
_form_input_with_label("Invoice comment", "invoice_comment", :text)
|
|
|
|
|
end
|
|
|
|
|
div.form_actions do
|
|
|
|
|
button.btn.btn_primary @method.capitalize, :type => "submit",
|
|
|
|
|
:name => @method, :value => @method.capitalize
|
|
|
|
|
button.btn "Cancel", :type => "submit",
|
|
|
|
|
:name => "cancel", :value => "Cancel"
|
2011-11-07 13:40:43 +01:00
|
|
|
|
end
|
2011-11-01 15:29:55 +01:00
|
|
|
|
end
|
2013-06-20 21:15:52 +02:00
|
|
|
|
# Show registered time (ab)using the time_entries view as partial view.
|
|
|
|
|
time_entries(@task.id) unless @method == "create"
|
2011-11-01 15:29:55 +01:00
|
|
|
|
end
|
|
|
|
|
|
2011-11-10 18:24:11 +01:00
|
|
|
|
# The main overview of the existing invoices.
|
2014-11-01 21:37:41 +01:00
|
|
|
|
#
|
|
|
|
|
# @return [Mab::Mixin::Tag] the invoices list overview
|
2011-11-01 15:29:55 +01:00
|
|
|
|
def invoices
|
2013-06-20 22:06:28 +02:00
|
|
|
|
header.page_header do
|
|
|
|
|
h1 do
|
|
|
|
|
text! "Invoices"
|
|
|
|
|
small "#{@invoices.count} customers, #{@invoice_count} invoices"
|
|
|
|
|
end
|
2013-06-20 21:15:52 +02:00
|
|
|
|
end
|
2014-10-18 21:28:18 +02:00
|
|
|
|
if @invoices.values.flatten.empty?
|
|
|
|
|
div.alert.alert_info do
|
|
|
|
|
text! "Found none! You can create one by " +
|
|
|
|
|
"#{a "selecting a customer", :href => R(Customers)}."
|
|
|
|
|
end
|
|
|
|
|
else
|
|
|
|
|
div.row do
|
|
|
|
|
div.span7 do
|
2014-10-25 18:02:32 +02:00
|
|
|
|
@invoices.keys.sort.each do |customer|
|
|
|
|
|
next if @invoices[customer].empty?
|
2014-10-25 18:03:50 +02:00
|
|
|
|
h2 do
|
|
|
|
|
text! customer.name
|
|
|
|
|
div.btn_group.pull_right do
|
|
|
|
|
a.btn.btn_small "» Create a new invoice",
|
|
|
|
|
:href => R(CustomersNInvoicesNew, customer.id)
|
|
|
|
|
end
|
|
|
|
|
end
|
2014-10-25 18:02:32 +02:00
|
|
|
|
_invoice_list(@invoices[customer])
|
2013-06-16 20:27:11 +02:00
|
|
|
|
end
|
|
|
|
|
end
|
2011-11-10 12:51:12 +01:00
|
|
|
|
end
|
2011-11-09 16:03:02 +01:00
|
|
|
|
end
|
2011-11-02 22:52:47 +01:00
|
|
|
|
end
|
|
|
|
|
|
2011-11-10 18:24:11 +01:00
|
|
|
|
# A view displaying the information (billed tasks and time) of an
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# invoice ({Models::Invoice}) that also allows for updating the "+paid+"
|
2011-11-10 18:24:11 +01:00
|
|
|
|
# property.
|
2014-11-01 21:37:41 +01:00
|
|
|
|
#
|
|
|
|
|
# @return [Mab::Mixin::Tag] the invoice form
|
2011-12-10 21:08:48 +01:00
|
|
|
|
def invoice_form
|
2013-06-20 22:06:28 +02:00
|
|
|
|
header.page_header do
|
|
|
|
|
h1 do
|
2013-06-20 21:15:52 +02:00
|
|
|
|
text! "Invoice for "
|
|
|
|
|
a @customer.name, :href => R(CustomersN, @customer.id)
|
2011-11-09 15:13:39 +01:00
|
|
|
|
end
|
|
|
|
|
end
|
2013-06-20 21:15:52 +02:00
|
|
|
|
div.row do
|
|
|
|
|
div.span6 do
|
|
|
|
|
form.form_horizontal.form_condensed :action => R(CustomersNInvoicesX, @customer.id, @invoice.number),
|
|
|
|
|
:method => :post do
|
|
|
|
|
_form_input_with_label("Number", "number", :text, :disabled => true,
|
|
|
|
|
:class => "input-small")
|
|
|
|
|
div.control_group do
|
|
|
|
|
label.control_label "Date"
|
|
|
|
|
div.controls do
|
|
|
|
|
input.input_medium :type => :text, :name => "created_at",
|
|
|
|
|
:id => "created_at",
|
|
|
|
|
:value => @invoice.created_at.to_formatted_s(:date_only),
|
|
|
|
|
:placeholder => "Date", :disabled => true
|
2013-06-16 20:27:11 +02:00
|
|
|
|
end
|
2013-06-20 21:15:52 +02:00
|
|
|
|
end
|
|
|
|
|
div.control_group do
|
|
|
|
|
label.control_label "Period"
|
|
|
|
|
div.controls do
|
|
|
|
|
input.input_large :type => :text, :name => "period", :id => "period",
|
|
|
|
|
:value => _format_period(@invoice.period),
|
|
|
|
|
:placeholder => "Period", :disabled => true
|
2013-06-16 20:27:11 +02:00
|
|
|
|
end
|
2013-06-20 21:15:52 +02:00
|
|
|
|
end
|
|
|
|
|
div.control_group do
|
|
|
|
|
label.control_label "Paid?"
|
|
|
|
|
div.controls do
|
|
|
|
|
_form_input_checkbox("paid")
|
2013-06-16 20:27:11 +02:00
|
|
|
|
end
|
2011-11-09 14:02:33 +01:00
|
|
|
|
end
|
2013-07-13 22:32:45 +02:00
|
|
|
|
div.control_group do
|
|
|
|
|
label.control_label "Include specification?"
|
|
|
|
|
div.controls do
|
|
|
|
|
_form_input_checkbox("include_specification")
|
|
|
|
|
end
|
|
|
|
|
end
|
2013-06-20 21:15:52 +02:00
|
|
|
|
div.form_actions do
|
|
|
|
|
button.btn.btn_primary "Update", :type => :submit,
|
|
|
|
|
:name => "update", :value => "Update"
|
|
|
|
|
button.btn "Reset", :type => :reset,
|
|
|
|
|
:name => "reset", :value => "Reset"
|
|
|
|
|
end
|
2011-11-02 22:52:47 +01:00
|
|
|
|
end
|
2013-06-20 21:15:52 +02:00
|
|
|
|
end
|
|
|
|
|
div.span6 do
|
|
|
|
|
table.table.table_condensed.table_striped do
|
|
|
|
|
col.task
|
|
|
|
|
col.reg_hours
|
|
|
|
|
col.hourly_rate
|
|
|
|
|
col.amount
|
|
|
|
|
thead do
|
|
|
|
|
tr do
|
|
|
|
|
th { "Project/Task" }
|
|
|
|
|
th.text_right { "Registered" }
|
|
|
|
|
th.text_right { "Hourly rt." }
|
|
|
|
|
th.text_right { "Amount" }
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
tbody do
|
|
|
|
|
subtotal = 0.0
|
|
|
|
|
@tasks.each do |task, line|
|
2013-06-16 20:27:11 +02:00
|
|
|
|
tr do
|
2013-06-20 21:15:52 +02:00
|
|
|
|
td do
|
|
|
|
|
a task.comment_or_name,
|
|
|
|
|
:title => task.comment_or_name,
|
|
|
|
|
:href => R(CustomersNTasksN, task.customer.id, task.id)
|
|
|
|
|
end
|
|
|
|
|
if line[1].blank?
|
|
|
|
|
# FIXME: information of time spent is available in the summary
|
|
|
|
|
# but show it?
|
|
|
|
|
td.text_right { "%.2fh" % line[0] }
|
|
|
|
|
td.text_right "–"
|
|
|
|
|
else
|
|
|
|
|
td.text_right { "%.2fh" % line[0] }
|
|
|
|
|
td.text_right { "€ %.2f" % line[1] }
|
|
|
|
|
end
|
|
|
|
|
td.text_right { "€ %.2f" % line[2] }
|
2013-06-16 20:27:11 +02:00
|
|
|
|
end
|
2013-06-20 21:15:52 +02:00
|
|
|
|
subtotal += line[2]
|
|
|
|
|
task.time_entries.each do |entry|
|
2013-06-16 20:27:11 +02:00
|
|
|
|
tr do
|
2013-06-20 21:15:52 +02:00
|
|
|
|
td.indent do
|
2014-10-25 18:31:48 +02:00
|
|
|
|
time_spec = "from #{entry.start.to_formatted_s(:time_only)} " +
|
|
|
|
|
"until #{entry.end.to_formatted_s(:time_only)} " +
|
|
|
|
|
"on #{entry.date.to_date}"
|
2013-06-20 21:15:52 +02:00
|
|
|
|
if entry.comment.present?
|
2013-07-13 22:49:56 +02:00
|
|
|
|
a "• #{entry.comment}", :href => R(TimelineN, entry.id),
|
2014-10-25 18:31:48 +02:00
|
|
|
|
:title => "#{entry.comment} (#{time_spec})"
|
2013-06-20 21:15:52 +02:00
|
|
|
|
else
|
2014-10-25 18:31:48 +02:00
|
|
|
|
a(:href => R(TimelineN, entry.id),
|
|
|
|
|
:title => time_spec) { i "• None" }
|
2013-06-16 20:27:11 +02:00
|
|
|
|
end
|
|
|
|
|
end
|
2013-06-20 21:15:52 +02:00
|
|
|
|
td.text_right { "%.2fh" % entry.hours_total }
|
|
|
|
|
td.text_right { "–" }
|
|
|
|
|
td.text_right { "–" }
|
2013-06-16 20:27:11 +02:00
|
|
|
|
end
|
2013-06-20 21:15:52 +02:00
|
|
|
|
end unless task.fixed_cost?
|
|
|
|
|
end
|
|
|
|
|
vattotal = 0.0
|
|
|
|
|
if @company.vatno.present?
|
2013-06-16 20:27:11 +02:00
|
|
|
|
tr.total do
|
2013-06-20 21:15:52 +02:00
|
|
|
|
td { i "Sub-total" }
|
2013-06-16 20:27:11 +02:00
|
|
|
|
td ""
|
|
|
|
|
td ""
|
2013-06-20 21:15:52 +02:00
|
|
|
|
td.text_right { "€ %.2f" % subtotal }
|
|
|
|
|
end
|
|
|
|
|
@vat.keys.sort.each do |rate|
|
|
|
|
|
vattotal += @vat[rate]
|
|
|
|
|
tr do
|
|
|
|
|
td { i "VAT %d%%" % rate }
|
|
|
|
|
td ""
|
|
|
|
|
td ""
|
|
|
|
|
td.text_right { "€ %.2f" % @vat[rate] }
|
|
|
|
|
end
|
2012-01-09 17:14:54 +01:00
|
|
|
|
end
|
|
|
|
|
end
|
2013-06-20 21:15:52 +02:00
|
|
|
|
tr.total do
|
|
|
|
|
td { b "Total" }
|
|
|
|
|
td ""
|
|
|
|
|
td ""
|
|
|
|
|
td.text_right { "€ %.2f" % (subtotal + vattotal) }
|
|
|
|
|
end
|
2012-01-02 14:04:43 +01:00
|
|
|
|
end
|
2013-06-20 21:15:52 +02:00
|
|
|
|
end
|
2013-06-16 20:27:11 +02:00
|
|
|
|
|
2013-06-20 21:15:52 +02:00
|
|
|
|
div.btn_group do
|
|
|
|
|
a.btn.btn_primary "» Download PDF",
|
|
|
|
|
:href => R(CustomersNInvoicesX, @customer.id, "#{@invoice.number}.pdf")
|
|
|
|
|
a.btn "» Download LaTeX source",
|
|
|
|
|
:href => R(CustomersNInvoicesX, @customer.id, "#{@invoice.number}.tex")
|
|
|
|
|
a.btn "» View company info",
|
|
|
|
|
:href => R(Company, :revision => @company.revision)
|
2011-11-09 14:07:03 +01:00
|
|
|
|
end
|
2014-10-31 21:55:04 +01:00
|
|
|
|
|
|
|
|
|
div.alert.alert_danger do
|
|
|
|
|
form.form_inline :action => R(CustomersNInvoicesX,
|
|
|
|
|
@customer.id, @invoice.number),
|
|
|
|
|
:method => :delete do
|
|
|
|
|
button.btn.btn_danger "» Remove old", :type => "submit"
|
|
|
|
|
text! "An invoice has already been generated!"
|
|
|
|
|
end
|
|
|
|
|
end if @invoice_file_present
|
2011-11-02 22:52:47 +01:00
|
|
|
|
end
|
|
|
|
|
end
|
2011-10-31 16:14:54 +01:00
|
|
|
|
end
|
|
|
|
|
|
2011-11-10 18:24:11 +01:00
|
|
|
|
# Form for selecting fixed cost tasks and registered time for tasks with
|
|
|
|
|
# an hourly rate that need to be billed.
|
2014-11-01 21:37:41 +01:00
|
|
|
|
#
|
|
|
|
|
# @return [Mab::Mixin::Tag] the invoice construction form
|
2011-11-07 17:44:20 +01:00
|
|
|
|
def invoice_select_form
|
2013-06-20 22:06:28 +02:00
|
|
|
|
header.page_header do
|
|
|
|
|
h1 do
|
|
|
|
|
text! "Create Invoice for "
|
|
|
|
|
a @customer.name, :href => R(CustomersN, @customer.id)
|
|
|
|
|
end
|
2013-06-20 21:15:52 +02:00
|
|
|
|
end
|
|
|
|
|
div.row do
|
|
|
|
|
div.span10 do
|
2013-06-20 22:06:28 +02:00
|
|
|
|
if @none_found
|
|
|
|
|
div.alert.alert_info do
|
|
|
|
|
"No fixed costs tasks or tasks with an hourly rate found!"
|
|
|
|
|
end
|
|
|
|
|
end
|
2013-06-20 21:15:52 +02:00
|
|
|
|
form.form_horizontal :action => R(CustomersNInvoices, @customer.id),
|
|
|
|
|
:method => :post do
|
|
|
|
|
unless @hourly_rate_tasks.empty?
|
2013-06-20 22:06:28 +02:00
|
|
|
|
h3 "Projects/Tasks with an Hourly Rate"
|
2013-06-20 21:15:52 +02:00
|
|
|
|
table.table.table_striped.table_condensed do
|
|
|
|
|
col.flag
|
|
|
|
|
col.date
|
|
|
|
|
col.start_time
|
|
|
|
|
col.end_time
|
|
|
|
|
col.comment
|
|
|
|
|
col.hours
|
|
|
|
|
col.amount
|
|
|
|
|
thead do
|
|
|
|
|
tr do
|
|
|
|
|
th "Bill?"
|
|
|
|
|
th "Date"
|
|
|
|
|
th "Start"
|
|
|
|
|
th "End"
|
|
|
|
|
th "Comment"
|
|
|
|
|
th.text_right "Total"
|
|
|
|
|
th.text_right "Amount"
|
2013-06-16 20:27:11 +02:00
|
|
|
|
end
|
2013-06-20 21:15:52 +02:00
|
|
|
|
end
|
|
|
|
|
tbody do
|
|
|
|
|
@hourly_rate_tasks.keys.each do |task|
|
|
|
|
|
tr.task do
|
|
|
|
|
td { _form_input_checkbox("tasks[]", task.id, true) }
|
|
|
|
|
td task.name, :colspan => 3
|
|
|
|
|
td do
|
|
|
|
|
input :type => :text, :name => "task_#{task.id}_comment",
|
|
|
|
|
:id => "tasks_#{task.id}_comment", :value => task.name
|
|
|
|
|
td {}
|
|
|
|
|
td {}
|
2013-06-16 20:27:11 +02:00
|
|
|
|
end
|
2013-06-20 21:15:52 +02:00
|
|
|
|
end
|
|
|
|
|
@hourly_rate_tasks[task].each do |entry|
|
|
|
|
|
tr do
|
2014-11-01 22:09:44 +01:00
|
|
|
|
td.indent { _form_input_checkbox("time_entries[]", entry.id, !entry.in_current_month?) }
|
2013-06-20 21:15:52 +02:00
|
|
|
|
td { label entry.date.to_date,
|
|
|
|
|
:for => "time_entries[]_#{entry.id}" }
|
|
|
|
|
td { entry.start.to_formatted_s(:time_only) }
|
|
|
|
|
td { entry.end.to_formatted_s(:time_only) }
|
|
|
|
|
td { entry.comment }
|
|
|
|
|
td.text_right { "%.2fh" % entry.hours_total }
|
|
|
|
|
td.text_right { "€ %.2f" % (entry.hours_total * entry.task.hourly_rate) }
|
2013-06-16 20:27:11 +02:00
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
2011-12-02 22:18:55 +01:00
|
|
|
|
end
|
2011-11-09 15:13:39 +01:00
|
|
|
|
end
|
2013-06-20 21:15:52 +02:00
|
|
|
|
end
|
2013-06-16 20:27:11 +02:00
|
|
|
|
|
2013-06-20 21:15:52 +02:00
|
|
|
|
unless @fixed_cost_tasks.empty?
|
|
|
|
|
h3 "Fixed Cost Projects/Tasks"
|
|
|
|
|
table.table.table_striped.table_condensed do
|
|
|
|
|
col.flag
|
|
|
|
|
col.task
|
|
|
|
|
col.comment
|
|
|
|
|
col.hours
|
|
|
|
|
col.amount
|
|
|
|
|
thead do
|
|
|
|
|
tr do
|
|
|
|
|
th "Bill?"
|
|
|
|
|
th "Project/Task"
|
|
|
|
|
th "Comment"
|
|
|
|
|
th.text_right "Registered time"
|
|
|
|
|
th.text_right "Amount"
|
2013-06-16 20:27:11 +02:00
|
|
|
|
end
|
2013-06-20 21:15:52 +02:00
|
|
|
|
end
|
|
|
|
|
tbody do
|
|
|
|
|
@fixed_cost_tasks.keys.each do |task|
|
|
|
|
|
tr do
|
|
|
|
|
td { _form_input_checkbox("tasks[]", task.id, true) }
|
|
|
|
|
td { label task.name, :for => "tasks[]_#{task.id}" }
|
|
|
|
|
td do
|
|
|
|
|
input :type => :text, :name => "task_#{task.id}_comment",
|
|
|
|
|
:id => "tasks_#{task.id}_comment", :value => task.name
|
2013-06-16 20:27:11 +02:00
|
|
|
|
end
|
2013-06-20 21:15:52 +02:00
|
|
|
|
td.text_right { "%.2fh" % @fixed_cost_tasks[task] }
|
|
|
|
|
td.text_right { task.fixed_cost }
|
2013-06-16 20:27:11 +02:00
|
|
|
|
end
|
|
|
|
|
end
|
2011-11-09 15:13:39 +01:00
|
|
|
|
end
|
2011-11-09 14:02:33 +01:00
|
|
|
|
end
|
2013-06-20 21:15:52 +02:00
|
|
|
|
end
|
2011-11-07 17:44:20 +01:00
|
|
|
|
|
2013-06-20 21:15:52 +02:00
|
|
|
|
div.form_actions do
|
|
|
|
|
button.btn.btn_primary "Create invoice", :type => :submit,
|
2013-06-20 22:06:28 +02:00
|
|
|
|
:name => "create", :value => "Create invoice",
|
|
|
|
|
:disabled => @none_found
|
2013-06-20 21:15:52 +02:00
|
|
|
|
button.btn "Cancel", :type => :submit,
|
|
|
|
|
:name => "cancel", :value => "Cancel"
|
2011-11-07 17:44:20 +01:00
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# Form for editing the company information ({Models::CompanyInfo}).
|
|
|
|
|
#
|
|
|
|
|
# @return [Mab::Mixin::Tag] the company information form
|
2011-11-07 14:54:11 +01:00
|
|
|
|
def company_form
|
2013-06-20 22:06:28 +02:00
|
|
|
|
header.page_header do
|
|
|
|
|
h1 do
|
|
|
|
|
text! "Company Information"
|
|
|
|
|
small @company.name
|
|
|
|
|
end
|
2013-06-20 21:15:52 +02:00
|
|
|
|
end
|
|
|
|
|
div.alert.alert_error.alert_block do
|
|
|
|
|
button.close(:type => "button", "data-dismiss" => "alert") { "×" }
|
|
|
|
|
h4 "There were #{@errors.count} errors in the form!"
|
|
|
|
|
ul do
|
|
|
|
|
@errors.each do |attrib, msg|
|
|
|
|
|
li "#{attrib.to_s.capitalize} #{msg}"
|
2013-06-16 20:27:11 +02:00
|
|
|
|
end
|
2011-11-07 14:54:11 +01:00
|
|
|
|
end
|
2013-06-20 21:15:52 +02:00
|
|
|
|
end if @errors
|
|
|
|
|
div.alert.alert_info do
|
|
|
|
|
text! " Viewing revision #{@company.revision}, " +
|
|
|
|
|
" last update at #{@company.updated_at}."
|
|
|
|
|
if @company.original.present?
|
|
|
|
|
a.btn "» View previous revision",
|
|
|
|
|
:href => R(Company, :revision => @company.original.revision)
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
div.alert.alert_block do
|
|
|
|
|
button.close(:type => "button", "data-dismiss" => "alert") { "×" }
|
|
|
|
|
h4 "Warning!"
|
|
|
|
|
text! "This company information is already associated with some invoices! "
|
|
|
|
|
br
|
|
|
|
|
text! "Only make changes if you know what you are doing!"
|
|
|
|
|
end if @history_warn
|
|
|
|
|
form.form_horizontal.form_condensed :action => R(Company, :revision => @company.revision),
|
|
|
|
|
:method => :post do
|
|
|
|
|
_form_input_with_label("Name", "name", :text)
|
|
|
|
|
_form_input_with_label("Contact name", "contact_name", :text)
|
|
|
|
|
_form_input_with_label("Street address", "address_street", :text)
|
|
|
|
|
_form_input_with_label("Postal code", "address_postal_code", :text)
|
|
|
|
|
_form_input_with_label("City/town", "address_city", :text)
|
|
|
|
|
_form_input_with_label("Phone number", "phone", :tel)
|
|
|
|
|
_form_input_with_label("Cellular number", "cell", :tel)
|
|
|
|
|
_form_input_with_label("Email address", "email", :email)
|
|
|
|
|
_form_input_with_label("Web address", "website", :url)
|
|
|
|
|
|
|
|
|
|
h3 "Corporate information"
|
|
|
|
|
_form_input_with_label("Chamber number", "chamber", :text)
|
|
|
|
|
_form_input_with_label("VAT number", "vatno", :text)
|
|
|
|
|
|
|
|
|
|
h3 "Bank information"
|
|
|
|
|
_form_input_with_label("Name", "bank_name", :text)
|
|
|
|
|
_form_input_with_label("Identification code", "bank_bic", :text)
|
|
|
|
|
_form_input_with_label("Account holder", "accountname", :text)
|
|
|
|
|
_form_input_with_label("Account number", "accountno", :text)
|
|
|
|
|
_form_input_with_label("Intl. account number", "accountiban", :text)
|
|
|
|
|
|
|
|
|
|
div.form_actions do
|
|
|
|
|
button.btn.btn_primary "Update", :type => "submit",
|
|
|
|
|
:name => "update", :value => "Update"
|
|
|
|
|
button.tbn "Reset", :type => :reset, :name => "reset",
|
|
|
|
|
:value => "Reset"
|
2013-06-16 20:27:11 +02:00
|
|
|
|
end
|
2011-11-07 14:54:11 +01:00
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
2011-12-10 21:05:58 +01:00
|
|
|
|
###############
|
|
|
|
|
# Partial views
|
|
|
|
|
#
|
|
|
|
|
private
|
|
|
|
|
|
|
|
|
|
# Partial view that generates the menu.
|
2014-11-01 21:37:41 +01:00
|
|
|
|
#
|
|
|
|
|
# @return [Mab::Mixin::Tag] the main menu
|
2011-12-10 21:05:58 +01:00
|
|
|
|
def _menu
|
2013-06-16 20:27:11 +02:00
|
|
|
|
nav.navbar.navbar_fixed_top do
|
|
|
|
|
div.navbar_inner do
|
|
|
|
|
div.container do
|
|
|
|
|
a.brand(:href => R(Index)) { "Stop… Camping Time!" }
|
|
|
|
|
ul.nav do
|
|
|
|
|
[["Overview", Index],
|
|
|
|
|
["Timeline", Timeline],
|
|
|
|
|
["Customers", Customers],
|
|
|
|
|
["Invoices", Invoices],
|
|
|
|
|
["Company", Company]].each { |label, ctrl| _menu_link(label, ctrl) }
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
2011-12-10 21:05:58 +01:00
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
# Partial view that generates the menu link and determines the active
|
|
|
|
|
# menu item.
|
2014-11-01 21:37:41 +01:00
|
|
|
|
#
|
|
|
|
|
# @param [String] label menu item label
|
|
|
|
|
# @param [Object] ctrl menu item target controller
|
|
|
|
|
# @return [Mab::Mixin::Tag] a menu item link tag
|
2011-12-10 21:05:58 +01:00
|
|
|
|
def _menu_link(label, ctrl)
|
|
|
|
|
# FIXME: dirty hack?
|
2012-01-25 15:53:43 +01:00
|
|
|
|
if self.class.to_s.match(/^#{ctrl.to_s}/)
|
2013-06-16 20:27:11 +02:00
|
|
|
|
li.active { a label, :href => R(ctrl) }
|
2011-12-10 21:05:58 +01:00
|
|
|
|
else
|
|
|
|
|
li { a label, :href => R(ctrl) }
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# Partial view that generates a list of invoices.
|
|
|
|
|
#
|
|
|
|
|
# @param [Array<Invoice>] invoices list of invoices
|
|
|
|
|
# @return [Mab::Mixin::Tag] a list of invoices
|
2011-12-10 21:05:58 +01:00
|
|
|
|
def _invoice_list(invoices)
|
|
|
|
|
if invoices.empty?
|
|
|
|
|
p "None found!"
|
|
|
|
|
else
|
2013-06-16 20:27:11 +02:00
|
|
|
|
table.table.table_striped.table_condensed do
|
2013-06-16 15:11:25 +02:00
|
|
|
|
col.number
|
|
|
|
|
col.date
|
|
|
|
|
col.period
|
|
|
|
|
col.amount
|
|
|
|
|
col.flag
|
2013-06-16 20:27:11 +02:00
|
|
|
|
thead do
|
2011-12-10 21:05:58 +01:00
|
|
|
|
tr do
|
2013-06-16 20:27:11 +02:00
|
|
|
|
th "Number"
|
|
|
|
|
th "Date"
|
|
|
|
|
th "Period"
|
|
|
|
|
th.text_right "Amount"
|
|
|
|
|
th "Paid?"
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
tbody do
|
|
|
|
|
invoices.each do |invoice|
|
2013-06-16 23:29:33 +02:00
|
|
|
|
due_class = invoice.past_due? ? "warning" : ""
|
|
|
|
|
due_class = "error" if invoice.way_past_due?
|
|
|
|
|
tr(:class => due_class) do
|
2013-06-16 20:27:11 +02:00
|
|
|
|
td do
|
|
|
|
|
a invoice.number,
|
|
|
|
|
:href => R(CustomersNInvoicesX,
|
|
|
|
|
invoice.customer.id, invoice.number)
|
|
|
|
|
end
|
|
|
|
|
td { invoice.created_at.to_formatted_s(:date_only) }
|
|
|
|
|
td { _format_period(invoice.period) }
|
|
|
|
|
td.text_right { "€ %.2f" % invoice.total_amount }
|
|
|
|
|
td do
|
2013-06-16 21:39:07 +02:00
|
|
|
|
i(:class => "icon-ok") if invoice.paid?
|
2013-06-16 20:27:11 +02:00
|
|
|
|
end
|
2011-12-10 21:05:58 +01:00
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# Partial view for formatting the period of an invoice.
|
|
|
|
|
#
|
|
|
|
|
# @param [Array(Time), Array(Time, Time)] period invoice period
|
|
|
|
|
# @return [String] formatted period of an invoice
|
2011-12-10 21:05:58 +01:00
|
|
|
|
def _format_period(period)
|
|
|
|
|
period = period.map { |m| m.to_formatted_s(:month_and_year) }.uniq
|
|
|
|
|
case period.length
|
2012-06-06 11:28:59 +02:00
|
|
|
|
when 1
|
|
|
|
|
period.first
|
|
|
|
|
when 2
|
|
|
|
|
period.join("–")
|
2011-12-10 21:05:58 +01:00
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# Partial view that generates a form input.
|
2013-06-16 20:27:11 +02:00
|
|
|
|
#
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# @param [String] input_name name of the input
|
|
|
|
|
# @param [String] type type of the input
|
|
|
|
|
# @param [String] placeholder placeholder text
|
|
|
|
|
# @param [Hash] html_options options passed on to the resulting
|
|
|
|
|
# Markaby/Mab tag
|
|
|
|
|
# @return [Mab::Mixin::Tag] a form input tag
|
2013-06-16 20:27:11 +02:00
|
|
|
|
def _form_input(input_name, type, placeholder, html_options={})
|
|
|
|
|
html_options.merge!(:type => type, :name => input_name,
|
|
|
|
|
:id => input_name, :value => @input[input_name],
|
|
|
|
|
:placeholder => placeholder)
|
|
|
|
|
input(html_options)
|
|
|
|
|
end
|
|
|
|
|
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# Partial view that generates a form label and a form input, such that
|
|
|
|
|
# the label is linked to the input.
|
2013-06-16 20:27:11 +02:00
|
|
|
|
#
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# @param [String] label_name label for the input
|
|
|
|
|
# @param [String] input_name name of the input
|
|
|
|
|
# @param [String] type type of the input
|
|
|
|
|
# @param [Hash] html_options options passed on to the resulting
|
|
|
|
|
# Markaby/Mab tag
|
|
|
|
|
# @return [Mab::Mixin::Tag] a form input tag
|
2013-06-16 20:27:11 +02:00
|
|
|
|
def _form_input_with_label(label_name, input_name, type, html_options={})
|
|
|
|
|
div.control_group do
|
|
|
|
|
label.control_label label_name, :for => input_name
|
|
|
|
|
div.controls do
|
|
|
|
|
_form_input(input_name, type, label_name, html_options)
|
|
|
|
|
end
|
|
|
|
|
end
|
2011-10-31 16:14:54 +01:00
|
|
|
|
end
|
|
|
|
|
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# Partial view that generates a form radio button.
|
|
|
|
|
#
|
|
|
|
|
# @param [String] name name of the radio button
|
|
|
|
|
# @param [String] value value of the radio button
|
|
|
|
|
# @param [Boolean] default whether the radio button is initially selected
|
|
|
|
|
# @param [Array] opts additional Markaby/Mab tag options
|
|
|
|
|
# @return [Mab::Mixin::Tag] a form radio button input tag
|
2011-12-10 16:49:28 +01:00
|
|
|
|
def _form_input_radio(name, value, default=false, *opts)
|
2011-11-07 17:43:10 +01:00
|
|
|
|
input_val = @input[name]
|
2011-11-07 15:10:15 +01:00
|
|
|
|
if input_val == value or (input_val.blank? and default)
|
2012-06-06 11:28:59 +02:00
|
|
|
|
input({:type => "radio", :id => "#{name}_#{value}",
|
|
|
|
|
:name => name, :value => value, :checked => true}, *opts)
|
2011-11-07 13:39:24 +01:00
|
|
|
|
else
|
2012-06-06 11:28:59 +02:00
|
|
|
|
input({:type => "radio", :id => "#{name}_#{value}",
|
|
|
|
|
:name => name, :value => value}, *opts)
|
2011-11-07 13:39:24 +01:00
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# Partial view that generates a form checkbox.
|
2013-06-16 22:45:18 +02:00
|
|
|
|
# Whether it is initially checked is determined by the _default_ flag.
|
2011-12-10 16:49:28 +01:00
|
|
|
|
# Additional options can be passed via the collection _opts_.
|
2014-11-01 21:37:41 +01:00
|
|
|
|
#
|
|
|
|
|
# @param [String] name name of the checkbox
|
|
|
|
|
# @param [Boolean] value whether the checkbox is checked
|
|
|
|
|
# @param [Boolean] default whether the checkbox is initially checked
|
|
|
|
|
# @param [Array] opts additional Markaby/Mab tag options
|
|
|
|
|
# @return [Mab::Mixin::Tag] a form checkbox input tag
|
2013-06-16 22:45:18 +02:00
|
|
|
|
def _form_input_checkbox(name, value=true, default=false, *opts)
|
|
|
|
|
if @input[name] == value or default
|
2012-06-06 11:28:59 +02:00
|
|
|
|
input({:type => "checkbox", :id => "#{name}_#{value}", :name => name,
|
|
|
|
|
:value => value, :checked => true}, *opts)
|
2011-11-07 13:39:24 +01:00
|
|
|
|
else
|
2012-06-06 11:28:59 +02:00
|
|
|
|
input({:type => "checkbox", :id => "#{name}_#{value}", :name => name,
|
|
|
|
|
:value => value}, *opts)
|
2011-11-07 13:39:24 +01:00
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# Partial view that generates a form select element.
|
2011-11-10 18:24:11 +01:00
|
|
|
|
#
|
|
|
|
|
# The option list is an Array of a 2-valued array containg a value label
|
|
|
|
|
# and a human readable description for the value.
|
2013-06-16 20:27:11 +02:00
|
|
|
|
#
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# @param [String] name name of the form select element
|
|
|
|
|
# @param [Array(String, String)] opts_list list of options (value and
|
|
|
|
|
# description)
|
|
|
|
|
# @param [Hash] html_options options passed on to the resulting
|
|
|
|
|
# Markaby/Mab tag
|
|
|
|
|
# @return [Mab::Mixin::Tag] a form select input tag
|
2013-06-16 20:27:11 +02:00
|
|
|
|
def _form_select(name, opts_list, html_options={})
|
2011-11-29 11:46:41 +01:00
|
|
|
|
if opts_list.blank?
|
2013-06-16 20:27:11 +02:00
|
|
|
|
html_options.merge!(:name => name, :id => name, :disabled => true)
|
|
|
|
|
select(html_options) do
|
2011-11-10 12:51:12 +01:00
|
|
|
|
option "None found", :value => "none", :selected => true
|
|
|
|
|
end
|
|
|
|
|
else
|
2013-06-16 20:27:11 +02:00
|
|
|
|
html_options.merge!(:name => name, :id => name)
|
|
|
|
|
select(html_options) do
|
2011-11-10 12:51:12 +01:00
|
|
|
|
opts_list.sort_by { |o| o.last }.each do |opt_val, opt_str|
|
|
|
|
|
if @input[name] == opt_val
|
|
|
|
|
option opt_str, :value => opt_val, :selected => true
|
|
|
|
|
else
|
|
|
|
|
option opt_str, :value => opt_val
|
|
|
|
|
end
|
2011-11-01 15:29:24 +01:00
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# Partial view similar to {Views#_form_select} that generates a (nested)
|
|
|
|
|
# select element for a form.
|
|
|
|
|
#
|
|
|
|
|
# In this case, the option lists represents a subdivision of the options,
|
|
|
|
|
# where the key is the name of the subdivision and the value the options
|
|
|
|
|
# list as in {Views#_form_select}.
|
2011-11-29 17:21:51 +01:00
|
|
|
|
#
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# So, the option list is an Hash of Strings mapping to Arrays of a
|
|
|
|
|
# 2-valued array containg a value label and a human readable description
|
|
|
|
|
# for the value.
|
2013-06-16 20:27:11 +02:00
|
|
|
|
#
|
2014-11-01 21:37:41 +01:00
|
|
|
|
# @param [String] name name of the form select element
|
|
|
|
|
# @param [Hash{String=>Array(String, String)}] opts list of options (section
|
|
|
|
|
# name to value and description)
|
|
|
|
|
# @param [Hash] html_options options passed on to the resulting
|
|
|
|
|
# Markaby/Mab tag
|
|
|
|
|
# @return [Mab::Mixin::Tag] a form select input tag
|
2013-06-16 20:27:11 +02:00
|
|
|
|
def _form_select_nested(name, opts, html_options={})
|
2011-11-29 17:21:51 +01:00
|
|
|
|
if opts.blank?
|
2013-06-16 20:27:11 +02:00
|
|
|
|
html_options.merge!(:name => name, :id => name, :disabled => true)
|
|
|
|
|
select(html_options) do
|
2011-11-29 17:21:51 +01:00
|
|
|
|
option "None found", :value => "none", :selected => true
|
|
|
|
|
end
|
|
|
|
|
else
|
2013-06-16 20:27:11 +02:00
|
|
|
|
html_options.merge!(:name => name, :id => name)
|
|
|
|
|
select(html_options) do
|
2011-11-29 17:21:51 +01:00
|
|
|
|
opts.keys.sort.each do |key|
|
2013-06-26 20:46:27 +02:00
|
|
|
|
optgroup :label => key do
|
|
|
|
|
opts[key].sort_by { |o| o.last }.each do |opt_val, opt_str|
|
|
|
|
|
if @input[name] == opt_val
|
|
|
|
|
option(opt_str, {:value => opt_val, :selected => true})
|
|
|
|
|
else
|
|
|
|
|
option(opt_str, {:value => opt_val})
|
|
|
|
|
end
|
2011-11-29 17:21:51 +01:00
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
2011-10-31 14:36:01 +01:00
|
|
|
|
end # module StopTime::Views
|