Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

added meta files, refactored the Loan, fixed fixtures and worked on t…

…he view a bit
  • Loading branch information...
commit 3ca6552ce189e695e463ae55dc3e44bb04179ebe 1 parent de2d738
cies authored
View
0  LICENSE.txt
No changes.
View
26 README.textile
@@ -0,0 +1,26 @@
+h1. MOSTFIT is an MIS for MFIs.
+
+http://github?
+
+This software is build using the Ruby programming language, on top of the Merb web framework and the DataMapper ORM. MOSTFIT takes a minimal approach, it basically creates the application in the most generic form, yet fully usable for the needs of our clients and beyond.
+
+
+h2. Philosophy
+
+A dead-simple, easy-to-use and easy-to-adapt, webbased MIS for MFIs with a decent feature set.
+
+Many software has been written to accomplish this. Not much was written well. Most of it is closed source, so suffers from limited exposure and often stalled development. We try to do is right by creating just enough.
+
+
+h2. Customizing
+
+Custonmizing MOSTFIT is very simple, in most cases not many modifications will be needed in order to fit your needs. In case you want to create different types of loan (different payment schemes and other acocunting details) just subclass from the Loan class in /app/models/loan.rb. There should be already some specific loan types there. For other modifications, like disabeling some functionality or adding a field, we suggest you just modify the code directly.
+
+Any proper Ruby programmer, or software shop, should be able to help you out customizing this tool to their needs.
+
+We use github, that means you can easily host your own branch of this software. That is good for the following reasons:
+ * keep up with our updates
+ * easily contribute your changes back to the community
+ * incorporate community innovation
+
+Which brings us to our goals of sharing costs, being flexible and delivering the best --free for all-- though open innovation. The license is AGPLv3, which is considered a strong-copyleft (viral) Open and Free Software license.
View
8 TODO.txt
@@ -0,0 +1,8 @@
+
+* write specs (datamodel is nearly complete, no reason not to have tests already)
+* runner creating the loan history
+* extend user system with simple goup based permissions
+* finish the views for users and staff members
+* write a payment generating factory (yaml fixtures are fine for the other resources) for testing, development and demos
+* StaffMember to Staff ??
+* some sort of an action logger like audit trail, preferably user centered (presenting nice overview of all action), easily accomplished with hooks
View
2  app/controllers/payments.rb
@@ -27,7 +27,7 @@ def create(payment)
# we create payment through the loan, so subclasses of the loan can take full responsibility for it (validations and such)
succes, @payment = @loan.repay(amounts, session.user, Date.strptime(payment[:received_on]), payment[:received_by])
if succes # true if saved
- redirect resource(@branch, @center, @client, :loans), :message => {:notice => "Payment ##{@payment.id} has been registered"}
+ redirect resource(@branch, @center, @client, @loan), :message => {:notice => "Payment ##{@payment.id} has been registered"}
else
message[:error] = "Payment failed to be created"
render :new
View
15 app/helpers/global_helpers.rb
@@ -2,24 +2,25 @@ module Merb
module GlobalHelpers
def breadcrums
- crums, strs, url = [], [], ''
+ # breadcrums use the request.uri and the instance vars of the parent
+ # resources (@branch, @center) that are available -- so no db queries
+ crums, url = [], ''
request.uri[1..-1].split('/').each_with_index do |part, index|
url << '/' + part
- if part.to_i.to_s.length == part.length # true when a number
- o = instance_variable_get('@'+strs.last.singular)
+ if part.to_i.to_s.length == part.length # true when a number (id)
+ o = instance_variable_get('@'+url.split('/')[-2].singular) # get the object (@branch)
s = (o.respond_to?(:name) ? link_to(o.name, url) : link_to('#'+o.id.to_s, url))
crums[-1] += ": <b><i>#{s}</i></b>" # merge the instance names (or numbers)
- else
- crums << link_to(part.gsub('_', ' '), url) # add the resource names
+ else # when not a number (id)
+ crums << link_to(part.gsub('_', ' '), url) # add the resource name
end
- strs << part
end
crums.join('&nbsp;<b>&gt;&gt;</b>&nbsp;') # fancy separator
end
def format_currency(i)
# in case of our rupees we do not count with cents, if you want to have cents do that here
- i.to_s + " INR"
+ i.to_i.to_s + " INR"
end
def plurial_nouns(freq)
View
4 app/models/branch.rb
@@ -5,10 +5,10 @@ class Branch
property :name, String, :length => 100, :nullable => false
property :address, Text
- belongs_to :manager, :class_name => 'StaffMember'
+ belongs_to :manager, :child_key => [:manager_staff_id], :class_name => 'StaffMember'
has n, :centers
- validates_present :staff_member_id
+ validates_present :manager_staff_id
def loan_stats
Loan.loan_stats_for self.centers.clients.loans
View
2  app/models/center.rb
@@ -6,8 +6,8 @@ class Center
property :meeting_day, Weekday
property :meeting_time, HoursAndMinutes
- belongs_to :manager, :class_name => 'StaffMember'
belongs_to :branch
+ belongs_to :manager, :child_key => [:manager_staff_id], :class_name => 'StaffMember'
has n, :clients
View
3  app/models/client.rb
@@ -9,8 +9,6 @@ class Client
property :date_of_birth, Date
property :address, Text
- has n, :loans
-
has_attached_file :picture,
:styles => {:medium => "300x300>", :thumb => "60x60#"},
:url => "/uploads/:class/:id/:attachment/:style/:basename.:extension",
@@ -21,6 +19,7 @@ class Client
:url => "/uploads/:class/:id/:attachment/:style/:basename.:extension",
:path => "#{Merb.root}/public/uploads/:class/:id/:attachment/:style/:basename.:extension"
+ has n, :loans
belongs_to :center
def loan_stats
View
59 app/models/loan.rb
@@ -2,20 +2,23 @@ class Loan
include DataMapper::Resource
property :id, Serial
- property :discriminator, Discriminator
- property :amount, Integer # amounts go in as cents: 13.37 => 1337
- property :interest_rate, Float
- property :installment_frequency, Enum[:daily, :weekly, :monthly]
- property :number_of_installments, Integer
+ property :discriminator, Discriminator, :nullable => false
+ property :amount, Integer, :nullable => false # see helper for formatting
+ property :interest_rate, Float, :nullable => false
+ property :installment_frequency, Enum[:daily, :weekly, :monthly], :nullable => false
+ property :number_of_installments, Integer, :nullable => false
property :scheduled_first_payment_date, Date, :nullable => false # arbitrary date for installment number 0
property :scheduled_disbursal_date, Date, :nullable => false
property :disbursal_date, Date # not disbursed when nil
property :created_at, DateTime
property :updated_at, DateTime
+ property :written_off_on, Date
belongs_to :client
- belongs_to :written_off_by, :class_name => 'StaffMember'
+ belongs_to :written_off_by, :child_key => [:written_off_by_staff_id], :class_name => 'StaffMember'
has n, :payments
+ has n, :history, :class_name => 'LoanHistory'
+
# TODO: validations!!!
@@ -39,7 +42,7 @@ def repay(input, user, received_on, received_by) # TODO: some kind of validatio
end
payment = Payment.new(:loan_id => self.id, :user_id => user.id,
- :received_on => received_on, :staff_member_id => received_by,
+ :received_on => received_on, :received_by_staff_id => received_by,
:principal => principal, :interest => interest, :total => total)
[payment.save, payment] # return the success boolean and the payment object itself for further processing
@@ -70,26 +73,37 @@ def total_scheduled_interest_on(date) # typically reimplemented in subclasses
end
- def repaid_principal
- payments.sum(:principal) or 0
+ def total_received_principal_on(date)
+ payments.sum(:principal, :conditions => ['received_on <= ?', date]) or 0
+ end
+
+ def total_received_interest_on(date)
+ payments.sum(:interest, :conditions => ['received_on <= ?', date]) or 0
+ end
+
+ def principle_difference_on(date)
+ total_scheduled_principal_on(date) - total_received_principal_on(date)
end
- def paid_interest
- payments.sum(:interest) or 0
+ def interest_difference_on(date)
+ total_scheduled_interest_on(date) - total_received_interest_on(date)
end
def principal_due_on(date)
- [total_scheduled_principal_on(date) - repaid_principal, 0].max
+ [principle_difference_on(date), 0].max
end
def interest_due_on(date)
- [total_scheduled_interest_on(date) - paid_interest, 0].max
+ [total_scheduled_interest_on(date), 0].max
end
def total_due_on(date)
principal_due_on(date) + interest_due_on(date)
end
+ def total_to_be_received
+ (self.amount.to_f * (1 + self.interest_rate)).round
+ end
def payment_schedule
schedule = []
@@ -131,7 +145,7 @@ def written_off?
def status # returns on of [:open, :closed, :insolvable]
return :written_off if self.written_off?
- self.repaid_principal >= amount ? :repaid : :outstanding # works only if interest gets paid first...
+ self.total_due_on(Date.today) >= self.total_to_be_received ? :repaid : :outstanding
end
def self.loan_stats_for(loans) # the stats for a collection of loans
@@ -142,7 +156,7 @@ def self.loan_stats_for(loans) # the stats for a collection of loans
s = loan.status
stats[s][:number] += 1
stats[s][:total_amount] += loan.amount
- stats[s][:total_repaid] += loan.repaid_principal if [:outstanding, :written_off].include? s
+ stats[s][:total_repaid] += loan.total_received_principal_on(Date.today) if [:outstanding, :written_off].include? s
stats[s][:total_due] += loan.total_due_on(Date.today) if s == :outstanding
end
# calculate percentages
@@ -160,17 +174,16 @@ def number_of_installments_before(date)
return 0 if date < scheduled_first_payment_date
result = case installment_frequency
when :daily
- (date - scheduled_first_payment_date).to_f.floor + 1
+ then (date - scheduled_first_payment_date).to_f.floor + 1
when :weekly
- ((date - scheduled_first_payment_date).to_f / 7).floor + 1
+ then ((date - scheduled_first_payment_date).to_f / 7).floor + 1
when :monthly
- start_day, start_month = scheduled_first_payment_date.day, scheduled_first_payment_date.month
- end_day, end_month = date.day, date.month
- end_month - start_month + (start_day >= end_day ? 0 : 1)
- else
- raise "Strange period you got.."
+ then start_day, start_month = scheduled_first_payment_date.day, scheduled_first_payment_date.month
+ end_day, end_month = date.day, date.month
+ end_month - start_month + (start_day >= end_day ? 0 : 1)
+ else raise "Strange period you got.."
end
- [result, number_of_installments].max # never return more than the number_of_installments
+ [result, number_of_installments].min # never return more than the number_of_installments
end
def shift_date_by_installments(date, number)
View
4 app/models/payment.rb
@@ -11,8 +11,8 @@ class Payment
belongs_to :loan
belongs_to :created_by, :class_name => 'User'
- belongs_to :received_by, :class_name => 'StaffMember'
- belongs_to :deleted_by, :class_name => 'User'
+ belongs_to :received_by, :child_key => [:received_by_staff_id], :class_name => 'StaffMember'
+ belongs_to :deleted_by, :child_key => [:deleted_by_user_id], :class_name => 'User'
validates_present :loan_id, :created_by, :received_by
View
35 app/views/loans/index.html.haml
@@ -6,7 +6,11 @@
%td name
%td
%b= @client.name
- == (id: #{@client.id})
+ == (id: #{@client.id}, ref: #{@client.reference})
+ %tr
+ %td spouse name
+ %td
+ = @client.spouse_name
%tr
%td manager
%td= link_to @branch.manager.name, resource(@branch.manager)
@@ -14,9 +18,10 @@
%td at center
%td
= link_to @center.name, resource(@branch, @center)
+ == (of branch #{link_to @branch.name, resource(@branch)})
%br/
%span.greytext
- managed by:
+ center managed by:
= link_to @center.branch.manager.name, resource(@center.manager)
@@ -48,8 +53,7 @@
%td
%b= format("%.2f%", loan.interest_rate * 100)
%td
- = loan.number_of_installments
- = plurial_nouns loan.installment_frequency
+ == #{loan.number_of_installments}, #{loan.installment_frequency.to_s}
%td
- if loan.disbursal_date
= loan.disbursal_date
@@ -69,11 +73,28 @@
%td
= format("%.2f%", loan.repaid_principal.to_f / loan.amount * 100)
%td
- = loan.status.to_s.gsub('_', ' ')
+ - if loan.status == :written_off
+ written off
+ %br/
+ %span.greytext
+ by
+ = link_to loan.written_off_by.name, resource(loan.written_off_by)
+ on
+ = loan.written_off_on
+ - else
+ = loan.status.to_s
+ %br/
+ %span.greytext
+ - if loan.payments.empty?
+ nothing repaid yet
+ - else
+ last payment on
+ = loan.payments.last(:order => [:received_on]).received_on
+
%td
- = link_to 'edit', resource(@branch, @center, @client, loan, :edit)
+ = link_to 'view details', resource(@branch, @center, @client, loan, :payments)
&nbsp;|&nbsp;
- = link_to 'view payments', resource(@branch, @center, @client, loan, :payments)
+ = link_to 'edit', resource(@branch, @center, @client, loan, :edit)
&nbsp;|&nbsp;
= link_to 'record payment', resource(@branch, @center, @client, loan, :payments, :new)
View
84 app/views/payments/index.html.haml
@@ -4,16 +4,16 @@
by client
= link_to "<i>#{@client.name}</i>", resource(@branch, @center, @client)
-%table.narrow.form{ :width => '100%' }
- %tr
- %td amount
+%table.narrow.form{ :width => '50%' }
+ %tr.odd
+ %td{ :width => '30%' } amount
%td
== <b>#{@loan.amount}</b> @
- %b= format("%.2f%", @loan.interest_rate)
+ %b= format("%.2f%", @loan.interest_rate*100)
%tr
%td type
%td= @loan.discriminator
- %tr
+ %tr.odd
%td installments
%td
= @loan.number_of_installments
@@ -28,7 +28,23 @@
- else
to be disbursed on
= @loan.scheduled_disbursal_date
-
+ %tr.odd
+ %td first payment
+ %td
+ - if @loan.payments.empty?
+ = @loan.scheduled_first_payment_date
+ %br/
+ %span.greytext (scheduled, no payments yet)
+ - else
+ = actual_first_payment = @loan.payments.first(:order => [:received_on.desc]).received_on
+ %br/
+ %span.greytext
+ - if actual_first_payment == @loan.scheduled_first_payment_date
+ as scheduled
+ - else
+ = difference_in_days(@loan.scheduled_first_payment_date, actual_first_payment, ['days earlier', 'days later'])
+ then scheduled
+ == (#{@loan.scheduled_disbursal_date})
%h2== Payments &mdash; (#{link_to 'new', resource(@branch, @center, @client, @loan, :payments, :new)})
@@ -64,4 +80,58 @@
%tr
%td{ :colspan => 7 }
-%p= link_to 'new payment', resource(@branch, @center, @client, @loan, :payments, :new)
+%p= link_to 'new payment', resource(@branch, @center, @client, @loan, :payments, :new)
+
+
+%h2== Payment schedule
+%table.narrow.form{ :width => '100%' }
+ %thead
+ %tr
+ %th
+ %th date due
+ %th scheduled payment
+ %th total scheduled payments
+ %th total paid within date due
+ %th total difference on date due
+ %tbody
+ - first, passed = true, false # these are for printing the "today" row
+ - last_payment = @loan.payments.last( :order => [:received_on.desc])
+ - last_payment_date = last_payment.received_on if last_payment
+ - @loan.payment_schedule.each do |i|
+ - if i[:date] > Date.today and not passed
+ - passed = true
+ - if not first
+ %tr{ :class => cycle('odd','') }
+ %td{ :colspan => 6, :style => "text-align: center;" }
+ %span.greytext== today (#{Date.today})
+ - first = false
+ %tr{ :class => cycle('odd','') }
+ %td &nbsp;
+ %td
+ = i[:date]
+ %td
+ %b= format_currency i[:principal] + i[:interest]
+ %br/
+ %span.greytext== (#{i[:principal].round} principal + #{i[:interest].round} interest)
+ %td
+ - tsp, tsi = @loan.total_scheduled_principal_on(i[:date]), @loan.total_scheduled_interest_on(i[:date])
+ %b= format_currency tsp + tsi
+ %br/
+ %span.greytext== (#{tsp.round} principal + #{tsi.round} interest)
+ %td
+ - if last_payment_date and (i[:date] <= last_payment_date) and (i[:date] <= Date.today)
+ - trp, tri = @loan.total_received_principal_on(i[:date]), @loan.total_received_interest_on(i[:date])
+ %b= format_currency trp + tri
+ %br/
+ %span.greytext== (#{trp} principal + #{tri} interest)
+ %td
+ - if i[:date] <= Date.today
+ - diff = @loan.principle_difference_on(i[:date]) + @loan.interest_difference_on(i[:date])
+ %b
+ %span{ :class => (diff > 0 ? 'red' : 'green') }
+ = format_currency diff.abs
+ %br/
+ %span.greytext
+ %span{ :class => (diff > 0 ? 'red' : 'green') }
+ = format("%.0f%", (diff.abs.to_f / @loan.total_to_be_received) * 100)
+ = diff > 0 ? 'is lacking' : 'is payed in advance'
View
3  public/stylesheets/style.css
@@ -298,6 +298,9 @@ border: solid 1px #33a;
}
+span.red { color: #d33; }
+span.green { color: #3d3; }
+
/**************************************/
/* FOOTER */
/**************************************/
View
6 spec/fixtures/branches.yml
@@ -2,16 +2,16 @@ maladwest:
:id: 1
:name: Mumbai Headquaters
:address: "Link Rd, Opp Offix, Parija Building, no. 203\nMumbai, Maharashtra\nIndia\n+91.9780503352"
- :staff_member_id: 2
+ :manager_staff_id: 2
hyydeeraa:
:id: 2
:name: Hyderabad Outpost
:address: "Road Nr. 2, Banjara Hills, next to Ories\nHyderabad, AP\nIndia\n04040300200"
- :staff_member_id: 4
+ :manager_staff_id: 4
munnar:
:id: 3
:name: Kerela Hills
:address: "Munnar Shanti Station, Hill Rd, Opp. Happy Dhaba\nMunnar, Kerela\nIndia"
- :staff_member_id: 1
+ :manager_staff_id: 1
View
17 spec/fixtures/centers.yml
@@ -3,7 +3,7 @@ mum_center_1:
:name: Oshiwari
:meeting_day: 1
:meeting_time: 1600
- :staff_member_id: 2
+ :manager_staff_id: 2
:branch_id: 1
mum_center_2:
@@ -11,7 +11,7 @@ mum_center_2:
:name: Daravi
:meeting_day: 2
:meeting_time: 1200
- :staff_member_id: 2
+ :manager_staff_id: 2
:branch_id: 1
mum_center_3:
@@ -19,7 +19,7 @@ mum_center_3:
:name: Goregoan
:meeting_day: 5
:meeting_time: 2200
- :staff_member_id: 3
+ :manager_staff_id: 3
:branch_id: 1
hyd_center_1:
@@ -27,7 +27,7 @@ hyd_center_1:
:name: Secudairabad
:meeting_day: 6
:meeting_time: 1200
- :staff_member_id: 1
+ :manager_staff_id: 1
:branch_id: 2
hyd_center_2:
@@ -35,7 +35,7 @@ hyd_center_2:
:name: Koti
:meeting_day: 6
:meeting_time: 1000
- :staff_member_id: 1
+ :manager_staff_id: 1
:branch_id: 2
hyd_center_3:
@@ -43,7 +43,7 @@ hyd_center_3:
:name: Somajiguta
:meeting_day: 1
:meeting_time: 1700
- :staff_member_id: 1
+ :manager_staff_id: 1
:branch_id: 2
kerela_center_1:
@@ -51,14 +51,13 @@ kerela_center_1:
:name: Munnar
:meeting_day: 1
:meeting_time: 1700
- :staff_member_id: 4
+ :manager_staff_id: 4
:branch_id: 3
-
kerela_center_1:
:id: 8
:name: Kochin
:meeting_day: 7
:meeting_time: 1300
- :staff_member_id: 4
+ :manager_staff_id: 4
:branch_id: 3
View
3  spec/fixtures/loans.yml
@@ -62,7 +62,8 @@ l006:
:scheduled_first_payment_date: 2008-12-19
:scheduled_disbursal_date: 2008-12-19
:disbursal_date: 2008-12-19
- :written_off: true
+ :written_off_by_staff_id: 1
+ :written_off_on: 2009-01-19
:client_id: 6
l007:
Please sign in to comment.
Something went wrong with that request. Please try again.