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.
Files changed (45) hide show
  1. data/COPYRIGHT.txt +18 -0
  2. data/CREDITS.rdoc +4 -0
  3. data/GPL.txt +339 -0
  4. data/README.rdoc +57 -0
  5. data/Rakefile +34 -0
  6. data/VERSION +1 -0
  7. data/app/controllers/invoice_controller.rb +94 -0
  8. data/app/controllers/payments_controller.rb +35 -0
  9. data/app/helpers/invoices_helper.rb +52 -0
  10. data/app/models/autofill.rb +69 -0
  11. data/app/models/invoice.rb +70 -0
  12. data/app/models/payment.rb +10 -0
  13. data/app/views/invoice/_form.rhtml +34 -0
  14. data/app/views/invoice/_row.rhtml +17 -0
  15. data/app/views/invoice/_table.rhtml +11 -0
  16. data/app/views/invoice/autocreate.rhtml +48 -0
  17. data/app/views/invoice/autofill.js.rjs +28 -0
  18. data/app/views/invoice/edit.rhtml +17 -0
  19. data/app/views/invoice/index.rhtml +26 -0
  20. data/app/views/invoice/new.rhtml +15 -0
  21. data/app/views/invoice/show.rhtml +63 -0
  22. data/app/views/layouts/print.rhtml +12 -0
  23. data/app/views/payments/_payment.html.erb +4 -0
  24. data/app/views/payments/new.html.erb +38 -0
  25. data/app/views/settings/_invoice_settings.rhtml +37 -0
  26. data/assets/images/creditcards.png +0 -0
  27. data/assets/images/money.png +0 -0
  28. data/assets/images/money_add.png +0 -0
  29. data/assets/images/printer.png +0 -0
  30. data/assets/stylesheets/invoice.css +36 -0
  31. data/assets/stylesheets/invoice_print.css +5 -0
  32. data/config/locales/en.yml +31 -0
  33. data/config/locales/fr.yml +14 -0
  34. data/config/routes.rb +15 -0
  35. data/init.rb +50 -0
  36. data/lang/en.yml +30 -0
  37. data/lang/fr.yml +13 -0
  38. data/lib/invoice_compatibility.rb +16 -0
  39. data/rails/init.rb +1 -0
  40. data/test/functional/invoice_controller_test.rb +25 -0
  41. data/test/integration/access_test.rb +86 -0
  42. data/test/integration/routing_test.rb +31 -0
  43. data/test/test_helper.rb +24 -0
  44. data/test/unit/invoice_test.rb +82 -0
  45. 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,10 @@
1
+ class Payment < ActiveRecord::Base
2
+ belongs_to :invoice
3
+
4
+ validates_presence_of :invoice, :amount, :applied_on
5
+ validates_numericality_of :amount
6
+
7
+ if Rails.env.test?
8
+ generator_for :applied_on => Date.today
9
+ end
10
+ 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 %>