chiliproject_invoice 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/COPYRIGHT.txt +18 -0
- data/CREDITS.rdoc +4 -0
- data/GPL.txt +339 -0
- data/README.rdoc +57 -0
- data/Rakefile +34 -0
- data/VERSION +1 -0
- data/app/controllers/invoice_controller.rb +94 -0
- data/app/controllers/payments_controller.rb +35 -0
- data/app/helpers/invoices_helper.rb +52 -0
- data/app/models/autofill.rb +69 -0
- data/app/models/invoice.rb +70 -0
- data/app/models/payment.rb +10 -0
- data/app/views/invoice/_form.rhtml +34 -0
- data/app/views/invoice/_row.rhtml +17 -0
- data/app/views/invoice/_table.rhtml +11 -0
- data/app/views/invoice/autocreate.rhtml +48 -0
- data/app/views/invoice/autofill.js.rjs +28 -0
- data/app/views/invoice/edit.rhtml +17 -0
- data/app/views/invoice/index.rhtml +26 -0
- data/app/views/invoice/new.rhtml +15 -0
- data/app/views/invoice/show.rhtml +63 -0
- data/app/views/layouts/print.rhtml +12 -0
- data/app/views/payments/_payment.html.erb +4 -0
- data/app/views/payments/new.html.erb +38 -0
- data/app/views/settings/_invoice_settings.rhtml +37 -0
- data/assets/images/creditcards.png +0 -0
- data/assets/images/money.png +0 -0
- data/assets/images/money_add.png +0 -0
- data/assets/images/printer.png +0 -0
- data/assets/stylesheets/invoice.css +36 -0
- data/assets/stylesheets/invoice_print.css +5 -0
- data/config/locales/en.yml +31 -0
- data/config/locales/fr.yml +14 -0
- data/config/routes.rb +15 -0
- data/init.rb +50 -0
- data/lang/en.yml +30 -0
- data/lang/fr.yml +13 -0
- data/lib/invoice_compatibility.rb +16 -0
- data/rails/init.rb +1 -0
- data/test/functional/invoice_controller_test.rb +25 -0
- data/test/integration/access_test.rb +86 -0
- data/test/integration/routing_test.rb +31 -0
- data/test/test_helper.rb +24 -0
- data/test/unit/invoice_test.rb +82 -0
- metadata +114 -0
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.0
|
@@ -0,0 +1,94 @@
|
|
1
|
+
class InvoiceController < ApplicationController
|
2
|
+
unloadable
|
3
|
+
layout 'base'
|
4
|
+
before_filter :authorize_global, :get_settings
|
5
|
+
before_filter :find_invoice, :only => [:show, :edit, :update, :destroy]
|
6
|
+
before_filter :default_invoice, :only => [:new, :autocreate]
|
7
|
+
before_filter :last_invoice_number, :only => [:new, :autocreate, :edit]
|
8
|
+
|
9
|
+
helper :invoices
|
10
|
+
|
11
|
+
def index
|
12
|
+
@open_invoices = Invoice.open
|
13
|
+
@late_invoices = Invoice.late
|
14
|
+
@closed_invoices = Invoice.closed
|
15
|
+
end
|
16
|
+
|
17
|
+
def new
|
18
|
+
end
|
19
|
+
|
20
|
+
def autocreate
|
21
|
+
@autofill = Autofill.new
|
22
|
+
end
|
23
|
+
|
24
|
+
def show
|
25
|
+
@payments = @invoice.payments.find(:all, :order => 'applied_on DESC')
|
26
|
+
render :layout => 'print' if params[:print]
|
27
|
+
end
|
28
|
+
|
29
|
+
def edit
|
30
|
+
end
|
31
|
+
|
32
|
+
def create
|
33
|
+
@invoice = Invoice.new(params[:invoice])
|
34
|
+
if @invoice.save
|
35
|
+
flash[:notice] = "Invoice created"
|
36
|
+
redirect_to invoice_path(@invoice)
|
37
|
+
else
|
38
|
+
render :action => 'new'
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
|
43
|
+
def update
|
44
|
+
if @invoice.update_attributes(params[:invoice])
|
45
|
+
flash[:notice] = "Invoice saved"
|
46
|
+
redirect_to invoice_path(@invoice)
|
47
|
+
else
|
48
|
+
render :action => 'edit'
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def destroy
|
53
|
+
if @invoice.destroy
|
54
|
+
flash[:notice] = "Invoice deleted"
|
55
|
+
redirect_to invoice_index_path
|
56
|
+
else
|
57
|
+
flash[:notice] = "Error"
|
58
|
+
redirect_to invoice_index_path
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def autofill
|
63
|
+
@autofill = Autofill.new_from_params(params[:autofill])
|
64
|
+
|
65
|
+
respond_to do |format|
|
66
|
+
format.js
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def outstanding
|
71
|
+
@invoice = Invoice.find_by_id(params[:invoice_id])
|
72
|
+
@invoice ||= Invoice.find_by_id(params[:id])
|
73
|
+
render :text => @invoice.outstanding
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
def find_invoice
|
78
|
+
@invoice = Invoice.find(params[:invoice][:id]) if params[:invoice]
|
79
|
+
@invoice ||= Invoice.find(params[:id])
|
80
|
+
end
|
81
|
+
|
82
|
+
def get_settings
|
83
|
+
@settings = Setting.plugin_chiliproject_invoice
|
84
|
+
end
|
85
|
+
|
86
|
+
def default_invoice
|
87
|
+
@invoice = Invoice.default
|
88
|
+
end
|
89
|
+
|
90
|
+
def last_invoice_number
|
91
|
+
@last_number = Invoice.last_invoice_number
|
92
|
+
end
|
93
|
+
|
94
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
class PaymentsController < ApplicationController
|
2
|
+
unloadable
|
3
|
+
layout 'base'
|
4
|
+
before_filter :authorize_global, :get_settings
|
5
|
+
|
6
|
+
helper :invoices
|
7
|
+
|
8
|
+
def new
|
9
|
+
@payment = Payment.new(payment_params)
|
10
|
+
end
|
11
|
+
|
12
|
+
def create
|
13
|
+
@payment = Payment.new(params[:payment])
|
14
|
+
if @payment.save
|
15
|
+
flash[:notice] = "Payment created"
|
16
|
+
redirect_to :controller => 'invoice', :action => "show", :id => params[:id], :invoice => { :id => @payment.invoice }
|
17
|
+
else
|
18
|
+
render :action => 'new', :id => params[:id]
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
def get_settings
|
25
|
+
@settings = Setting.plugin_chiliproject_invoice
|
26
|
+
end
|
27
|
+
|
28
|
+
def payment_params
|
29
|
+
routing_params = {:invoice_id => params[:invoice_id]}
|
30
|
+
standard_params = params[:payment] || {}
|
31
|
+
|
32
|
+
routing_params.merge(standard_params)
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module InvoicesHelper
|
2
|
+
|
3
|
+
def invoice_list_tabs(invoices = { })
|
4
|
+
tabs = [{:name => 'open', :label => "label_open_invoices", :items => invoices[:open]},
|
5
|
+
{:name => 'late', :label => "label_late_invoices", :items => invoices[:late]},
|
6
|
+
{:name => 'closed', :label => "label_closed_invoices", :items => invoices[:closed]}
|
7
|
+
]
|
8
|
+
end
|
9
|
+
|
10
|
+
def invoice_status(invoice)
|
11
|
+
case true
|
12
|
+
when invoice.fully_paid?
|
13
|
+
return content_tag(:div,
|
14
|
+
content_tag(:p, l(:label_paid_invoice)),
|
15
|
+
:class => "invoice-message fully-paid nonprinting")
|
16
|
+
when invoice.late?
|
17
|
+
return content_tag(:div,
|
18
|
+
content_tag(:p, l(:label_late_invoices)),
|
19
|
+
:class => "invoice-message late nonprinting")
|
20
|
+
|
21
|
+
else
|
22
|
+
return content_tag(:div,
|
23
|
+
content_tag(:p, l(:label_open_invoices)),
|
24
|
+
:class => "invoice-message pending nonprinting")
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def invoice_menu(invoice=nil, &block)
|
30
|
+
menu_items = []
|
31
|
+
menu_items << link_to(l(:label_invoice_list), invoice_index_path, :class => 'icon icon-invoice-list')
|
32
|
+
menu_items << link_to(l(:label_new_invoice), new_invoice_path, :class => 'icon icon-invoice-new')
|
33
|
+
menu_items << link_to(l(:label_new_autofilled_invoice), autocreate_invoice_path, :class => 'icon icon-invoice-new')
|
34
|
+
|
35
|
+
if invoice.nil?
|
36
|
+
menu_items << link_to(l(:label_new_payment), new_payment_path, :class => 'icon icon-payment-new')
|
37
|
+
else
|
38
|
+
menu_items << link_to(l(:label_new_payment), new_invoice_payment_path(invoice), :class => 'icon icon-payment-new')
|
39
|
+
end
|
40
|
+
|
41
|
+
yield menu_items if block_given?
|
42
|
+
|
43
|
+
return content_tag(:div, menu_items.join(' '), :class => "contextual nonprinting", :id => "invoice-menu") +
|
44
|
+
content_tag(:div, '', :style => 'clear: both')
|
45
|
+
|
46
|
+
end
|
47
|
+
|
48
|
+
def header_tags
|
49
|
+
return stylesheet_link_tag("invoice.css", :plugin => "chiliproject_invoice", :media => 'all') +
|
50
|
+
stylesheet_link_tag("invoice_print.css", :plugin => "chiliproject_invoice", :media => 'print')
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# Mock class to help build forms
|
2
|
+
class Autofill
|
3
|
+
# "Properties"
|
4
|
+
attr_accessor :project_id
|
5
|
+
attr_accessor :date_from
|
6
|
+
attr_accessor :date_to
|
7
|
+
attr_accessor :activities
|
8
|
+
attr_accessor :rate
|
9
|
+
attr_accessor :project
|
10
|
+
attr_accessor :customer
|
11
|
+
attr_accessor :issues
|
12
|
+
attr_accessor :total_time
|
13
|
+
attr_accessor :time_entries
|
14
|
+
attr_accessor :total
|
15
|
+
|
16
|
+
# Fake out an AR object
|
17
|
+
def errors
|
18
|
+
return { }
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.new_from_params(params)
|
22
|
+
autofill = Autofill.new
|
23
|
+
return autofill if params.blank?
|
24
|
+
|
25
|
+
# Get project
|
26
|
+
autofill.project = Project.find_by_id(params[:project_id])
|
27
|
+
|
28
|
+
# Get customer
|
29
|
+
autofill.customer = Customer.find_by_id(autofill.project.customer_id) # Customer plugin only has a 1-way relationship
|
30
|
+
|
31
|
+
# Build date range
|
32
|
+
autofill.date_from = params[:date_from]
|
33
|
+
autofill.date_to = params[:date_to]
|
34
|
+
|
35
|
+
# Build activities
|
36
|
+
if params[:activities]
|
37
|
+
autofill.activities = params[:activities].collect {|p| p.to_i }
|
38
|
+
end
|
39
|
+
|
40
|
+
autofill.activities ||= []
|
41
|
+
|
42
|
+
# Fetch issues
|
43
|
+
autofill.issues = autofill.project.issues.find(:all,
|
44
|
+
:conditions => ['time_entries.spent_on >= :from AND time_entries.spent_on <= :to AND time_entries.activity_id IN (:activities)',
|
45
|
+
{
|
46
|
+
:from => autofill.date_from,
|
47
|
+
:to => autofill.date_to,
|
48
|
+
:activities => autofill.activities
|
49
|
+
}],
|
50
|
+
:include => [:time_entries])
|
51
|
+
|
52
|
+
autofill.total_time = autofill.issues.collect(&:time_entries).flatten.collect(&:hours).sum
|
53
|
+
|
54
|
+
# Time logged without an issue
|
55
|
+
autofill.time_entries = autofill.project.time_entries.find(:all,
|
56
|
+
:conditions => ['issue_id IS NULL AND spent_on >= :from AND spent_on <= :to AND activity_id IN (:activities)',
|
57
|
+
{
|
58
|
+
:from => autofill.date_from,
|
59
|
+
:to => autofill.date_to,
|
60
|
+
:activities => autofill.activities
|
61
|
+
}])
|
62
|
+
|
63
|
+
autofill.total_time += autofill.time_entries.collect(&:hours).sum
|
64
|
+
|
65
|
+
autofill.total = autofill.total_time.to_f * params[:rate].to_f
|
66
|
+
|
67
|
+
autofill
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
class Invoice < ActiveRecord::Base
|
2
|
+
belongs_to :customer
|
3
|
+
belongs_to :project
|
4
|
+
has_many :payments
|
5
|
+
before_save :textilize
|
6
|
+
|
7
|
+
validates_presence_of :invoice_number, :customer, :amount, :description
|
8
|
+
validates_uniqueness_of :invoice_number
|
9
|
+
|
10
|
+
def self.default
|
11
|
+
return Invoice.new({ :due_date => Date.today + Setting.plugin_chiliproject_invoice['invoice_payment_terms'].to_i.days })
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.open
|
15
|
+
invoices = self.find(:all)
|
16
|
+
return invoices.select { |invoice| invoice.open? }
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.late
|
20
|
+
invoices = self.find(:all)
|
21
|
+
return invoices.select { |invoice| invoice.late? }
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.closed
|
25
|
+
invoices = self.find(:all)
|
26
|
+
return invoices.select { |invoice| invoice.fully_paid? }
|
27
|
+
end
|
28
|
+
|
29
|
+
def textilize
|
30
|
+
self.description_html = RedCloth3.new(self.description).to_html
|
31
|
+
end
|
32
|
+
|
33
|
+
# Is this invoice current but not fully paid?
|
34
|
+
def open?
|
35
|
+
!fully_paid? && !late?
|
36
|
+
end
|
37
|
+
|
38
|
+
def fully_paid?
|
39
|
+
outstanding <= 0
|
40
|
+
end
|
41
|
+
|
42
|
+
def late?
|
43
|
+
return false if fully_paid?
|
44
|
+
return Time.now > self.due_date
|
45
|
+
end
|
46
|
+
|
47
|
+
def outstanding
|
48
|
+
(total = amount - payments.sum('amount')) > 0 ? total : 0.0
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.last_invoice_number
|
52
|
+
last_invoice = first(:order => 'id DESC')
|
53
|
+
if last_invoice.present?
|
54
|
+
last_invoice.invoice_number
|
55
|
+
else
|
56
|
+
'-'
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
|
61
|
+
if Rails.env.test?
|
62
|
+
generator_for :invoice_number, :start => '10000' do |prev|
|
63
|
+
prev.succ
|
64
|
+
end
|
65
|
+
|
66
|
+
generator_for :amount => 100.0
|
67
|
+
generator_for :description => 'This is your test invoice.'
|
68
|
+
generator_for :due_date => 1.month.from_now
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
<fieldset>
|
2
|
+
<legend><%= l(:label_invoice) %></legend>
|
3
|
+
<%= error_messages_for 'invoice' %>
|
4
|
+
|
5
|
+
<p>
|
6
|
+
<%= form.text_field :invoice_number %> (<%= l(:label_last_number) %> #<%= @last_number %>)
|
7
|
+
</p>
|
8
|
+
|
9
|
+
<p>
|
10
|
+
<%= form.select :customer_id, Customer.find(:all).collect { |c| ["#{c.company} - #{c.name}", c.id]} %>
|
11
|
+
</p>
|
12
|
+
|
13
|
+
<p>
|
14
|
+
<%= form.select :project_id, Project.find(:all, :order => 'name ASC').collect { |p| [h(p.name), p.id]}, { :include_blank => true} %>
|
15
|
+
</p>
|
16
|
+
|
17
|
+
<p>
|
18
|
+
<%= form.text_field :invoiced_on, :size => 10 %><%= calendar_for('invoice_invoiced_on') %>
|
19
|
+
</p>
|
20
|
+
|
21
|
+
<p>
|
22
|
+
<%= form.text_field :due_date, :size => 10 %><%= calendar_for('invoice_due_date') %>
|
23
|
+
</p>
|
24
|
+
|
25
|
+
<p>
|
26
|
+
<%= form.text_field :amount %>
|
27
|
+
</p>
|
28
|
+
|
29
|
+
<p>
|
30
|
+
<%= form.text_area :description, :cols => 60 %>
|
31
|
+
<%= wikitoolbar_for 'invoice_description' %>
|
32
|
+
</p>
|
33
|
+
|
34
|
+
</fieldset>
|
@@ -0,0 +1,17 @@
|
|
1
|
+
<tr>
|
2
|
+
<td>
|
3
|
+
<%= link_to h(row.invoice_number), :action => 'show', :id => @project, :invoice => { :id => row } %>
|
4
|
+
</td>
|
5
|
+
<td>
|
6
|
+
<%= h(row.invoiced_on.strftime('%B %d, %Y')) if row.invoiced_on.present? %>
|
7
|
+
</td>
|
8
|
+
<td>
|
9
|
+
<%= h row.customer.company %> - <%= h row.customer.name %>
|
10
|
+
</td>
|
11
|
+
<td>
|
12
|
+
<%= link_to(h(row.project.name), :controller => 'projects', :action => 'show', :id => row.project) unless row.project.nil? %>
|
13
|
+
</td>
|
14
|
+
<td>
|
15
|
+
<%= h number_to_currency(row.amount, :unit => @settings['invoice_currency_symbol']) %>
|
16
|
+
</td>
|
17
|
+
</tr>
|
@@ -0,0 +1,11 @@
|
|
1
|
+
<table class="invoices">
|
2
|
+
<tr>
|
3
|
+
<th><%= l(:field_invoice_number) %></th>
|
4
|
+
<th><%= l(:field_date_issued) %></th>
|
5
|
+
<th><%= l(:field_customer) %></th>
|
6
|
+
<th><%= l(:field_project) %></th>
|
7
|
+
<th><%= l(:field_amount) %></th>
|
8
|
+
</tr>
|
9
|
+
<%= render :partial => 'row', :collection => invoices %>
|
10
|
+
</table>
|
11
|
+
|
@@ -0,0 +1,48 @@
|
|
1
|
+
<% content_for :header_tags do %>
|
2
|
+
<%= header_tags %>
|
3
|
+
<% end %>
|
4
|
+
|
5
|
+
<%= invoice_menu %>
|
6
|
+
|
7
|
+
<h1><%= l(:label_new_autofilled_invoice) %></h1>
|
8
|
+
|
9
|
+
<% labelled_tabular_form_for(:autofill, @autofill, :url => { :action => 'autofill', :id => @project}) do |form| %>
|
10
|
+
|
11
|
+
<fieldset>
|
12
|
+
<legend><%= l(:label_auto_fill) %></legend>
|
13
|
+
<p>
|
14
|
+
<%= form.label :project_id %>
|
15
|
+
<%= form.collection_select :project_id, (Project.find(:all, :order => 'name ASC')), :id, :name %>
|
16
|
+
</p>
|
17
|
+
|
18
|
+
<p>
|
19
|
+
<%= form.text_field "date_from", :size => 10 %><%= calendar_for('autofill_date_from') %>
|
20
|
+
</p>
|
21
|
+
|
22
|
+
<p>
|
23
|
+
<%= form.text_field "date_to", :size => 10 %><%= calendar_for('autofill_date_to') %>
|
24
|
+
</p>
|
25
|
+
|
26
|
+
<p>
|
27
|
+
<%= label :autofill, :activities %>
|
28
|
+
<%= select_tag 'autofill[activities][]',
|
29
|
+
options_from_collection_for_select(InvoiceCompatibility::Enumeration.activities, :id, :name),
|
30
|
+
{ :multiple => true, :size => 5 } %>
|
31
|
+
</p>
|
32
|
+
|
33
|
+
<p>
|
34
|
+
<%= form.text_field 'rate', :size => 6, :value => @settings['invoice_default_rate'] %>
|
35
|
+
</p>
|
36
|
+
|
37
|
+
<p><%= submit_to_remote 'submit', 'Autofill', :url => { :action => 'autofill', :id => @project } %></p>
|
38
|
+
|
39
|
+
</fieldset>
|
40
|
+
<% end %>
|
41
|
+
|
42
|
+
<% labelled_tabular_form_for(:invoice, @invoice, :url => { :action => 'create', :id => @project}) do |form| %>
|
43
|
+
|
44
|
+
<%= render(:partial => 'form', :object => form) %>
|
45
|
+
|
46
|
+
<p><%= form.submit l(:button_create) %></p>
|
47
|
+
|
48
|
+
<% end %>
|