diff --git a/app/controllers/clients.rb b/app/controllers/clients.rb index a41bc6e7..eabb05a6 100644 --- a/app/controllers/clients.rb +++ b/app/controllers/clients.rb @@ -64,7 +64,9 @@ def update(id, client) @client = Client.get(id) raise NotFound unless @client disallow_updation_of_verified_clients - if @client.update_attributes(client) + debugger + @client.update_attributes(client) + if @client.errors.blank? if params[:tags] @client.tags = params[:tags].keys.map{|k| k.to_sym} else diff --git a/app/models/client.rb b/app/models/client.rb index c97ae239..605d2caa 100644 --- a/app/models/client.rb +++ b/app/models/client.rb @@ -11,6 +11,7 @@ class Client before :valid?, :add_created_by_staff_member after :save, :check_client_deceased after :save, :levy_fees + after :save, :update_loan_cache property :id, Serial property :reference, String, :length => 100, :nullable => false, :index => true @@ -127,6 +128,10 @@ class Client validates_with_method :date_joined, :method => :dates_make_sense validates_with_method :inactive_reason, :method => :cannot_have_inactive_reason_if_active + def update_loan_cache + loans.each{|l| l.update_loan_cache(true); l.save} + end + def self.from_csv(row, headers) if center_attr = row[headers[:center]].strip if center = Center.first(:name => center_attr) diff --git a/app/models/data_access_observer.rb b/app/models/data_access_observer.rb index 34fa9d7c..53e691f6 100644 --- a/app/models/data_access_observer.rb +++ b/app/models/data_access_observer.rb @@ -14,11 +14,12 @@ def self.get_object_state(obj, type) def self.log(obj) f = File.open("log/#{obj.class}.log","a") + debugger begin if obj attributes = obj.attributes if @ributes - diff = @ributes.diff(attributes) + diff = @ributes.diff(attributes).reject{|x| x.to_s.match(/^c_/)} # reject the caching properties, defined by c_xxxx diff = diff.map{|k| {k => [@ributes[k],attributes[k]]} if k != :updated_at and not (@ributes[k].nil? and attributes[k].class==String and attributes[k].blank?) } @@ -26,7 +27,6 @@ def self.log(obj) else diff = [attributes.select{|k, v| v and not v.blank? and not v==0}.to_hash] end - if diff.length>0 and diff.find{|x| x.keys.include?(:discriminator)} index = diff.index(diff.find{|x| x.keys.include?(:discriminator)}) diff[index][:discriminator] = diff[index][:discriminator].map{|x| x.to_s if x} diff --git a/app/models/loan.rb b/app/models/loan.rb index aec80e6c..a14dcfaa 100644 --- a/app/models/loan.rb +++ b/app/models/loan.rb @@ -12,6 +12,7 @@ class Loan after :save, :update_history_caller # also seems to do updates after :create, :levy_fees_new after :save, :levy_fees + before :save, :update_loan_cache after :create, :update_cycle_number before :destroy, :verified_cannot_be_deleted # after :destroy, :update_history @@ -170,6 +171,30 @@ class Loan validates_with_method :clients, :method => :check_client_sincerity validates_with_method :insurance_policy, :method => :check_insurance_policy + + def update_loan_cache(force = false) + t = Time.now + self.c_center_id = self.client.center.id if force + self.c_branch_id = self.client.center.branch.id if force + self.c_scheduled_maturity_date = scheduled_maturity_date + # avoid SQL calls + first_payment = payments.select{|p| [:prinicpal, :interest].include?(p.type)}.sort_by{|p| p.received_on}[0] + self.c_actual_first_payment_date = first_payment.received_on if first_payment + st = self.get_status + self.c_last_status = st + self.c_principal_received = payments.select{|p| p.type == :principal}.reduce(0){|s,p| s + p.amount} + self.c_interest_received = payments.select{|p| p.type == :principal}.reduce(0){|s,p| s + p.amount} + last_payment = payments.select{|p| [:prinicpal, :interest].include?(p.type)}.sort_by{|p| p.received_on}.reverse[0] + self.c_last_payment_received_on = last_payment.received_on if last_payment + self.c_maturity_date = c_last_payment_received_on if (STATUSES.index(st) > 5 and last_payment) + self.c_last_payment_id = last_payment.id if last_payment + puts Time.now - t + true + end + + + + def self.display_name "Loan" end @@ -476,6 +501,7 @@ def make_payments(payments, context = :default, defer_update = false) self.reload if payments.map{|p| p.received_on}.map{|d| installment_dates.include?(d)}.include?(false) update_history(true) # update the history if we saved a payment end + update_loan_cache if payments.length > 0 return [true, payments.find{|p| p.type==:principal}, payments.find{|p| p.type==:interest}, payments.find_all{|p| p.type==:fees}] else @@ -490,6 +516,7 @@ def delete_payment(payment, user) payment.deleted_by = user if payment.destroy update_history + update_loan_cache clear_cache return [true, payment] end diff --git a/app/views/branches/_fields.html.haml b/app/views/branches/_fields.html.haml index f28031e7..4d170022 100644 --- a/app/views/branches/_fields.html.haml +++ b/app/views/branches/_fields.html.haml @@ -39,4 +39,6 @@ %button{:type => "submit", :class => "add"} = submit_text    +%b or +   = link_to 'cancel', cancel_url diff --git a/app/views/loans/index.html.haml b/app/views/loans/index.html.haml index 6a7c7b15..4a44d554 100644 --- a/app/views/loans/index.html.haml +++ b/app/views/loans/index.html.haml @@ -41,7 +41,7 @@ %tr %td At center %td - = link_to @center.name, resource(@branch, @center) + = link_to @client.center.name, resource(@client.center) == (of branch #{link_to @branch.name, resource(@branch)}) %br/ %span.greytext diff --git a/app/views/loans/misc.html.haml b/app/views/loans/misc.html.haml index 1c8a3acd..a95da77e 100644 --- a/app/views/loans/misc.html.haml +++ b/app/views/loans/misc.html.haml @@ -1,70 +1,98 @@ -= form_for(:loan, :class => "_remote_", :action => url(:controller => :loans, :action => :update_utilization, :id => @loan.id), :method => "POST") do - %div.box - %h3 Loan Utilization - = select :loan_utilization_id, :collection => LoanUtilization.all.map{|lu| [lu.id, lu.name]}, :include_blank => true - = submit "Save" - -- if @loan.status == :outstanding +%div{:style => "float: left; width: 55%"} .box - %hr - %h3 Prepay this loan - %p - = link_to("Calculate prepayment amount", url(:prepay_loan, @loan.id), :class => "_remote_") + %fieldset + %legend Loan Utilization + = form_for(:loan, :class => "_remote_", :action => url(:controller => :loans, :action => :update_utilization, :id => @loan.id), :method => "POST") do + %h3 Update Loan Utilization + = select :loan_utilization_id, :collection => LoanUtilization.all.map{|lu| [lu.id, lu.name]}, :include_blank => true + = submit "Save" + + + - if @loan.status == :outstanding + .box + %fieldset + %legend Pay + = link_to("Prepay This Loan", url(:prepay_loan, @loan.id), :class => "_remote_") + + - if [:admin, :mis_manager].include?(session.user.role) + .box + %fieldset + %legend Levy Penalty/Fee + = partial "applicable_fees/form", :layout => false, :object => @loan + + - if session.user.role==:admin + .box + %fieldset + %legend Write Off Loan + = form_for(@loan, :action => url(:action => :write_off, :id => @loan.id), :method => "POST", :class => "_disable_button_") do + %table.tall.shaded + %tr + %th + Write off Date : + %td + = date_select_for @loan, :written_off_on + %tr + %th + Select Staff Member : + %td + - center = @client.center + - branch = center.branch + = select :written_off_by_staff_id, :id => "written_off_by_staff_id", :collection => [branch.manager, center.manager].uniq.map{|m| [m.id, m.name]}, :prompt => "select staff member" + %tr + %td{:colspan => "2"} + %span.greytext + Pick the staff member who is writing off the loan + = submit "Write off" + - elsif staff_member = session.user.staff_member + = form_for(@loan, :action => url(:action => :suggest_write_off, :id => @loan.id), :method => "POST", :class => "_disable_button_") do + %table + %tr + %td + %b Suggest to Write Off this Loan + %tr + %td + suggested write off Date : + %td + = date_select_for @loan, :suggested_written_off_on + %tr + %td + Suggested by staff member : + %td + = staff_member.name + %td + %tr + %td + = submit "Send suggestion" -- if [:admin, :mis_manager].include?(session.user.role) - %hr - .box - = partial "applicable_fees/form", :layout => false, :object => @loan - %hr +%div{:style => "float: left; width: 40%"} .box - %h3 Applicable fees - - if @loan.applicable_fees.empty? - = form_for(@loan, :action => url(:controller => :loans, :action => :levy_fees, :id => @loan.id)) do - = submit 'Apply Fees' - = partial "applicable_fees/list", :layout => false, :applicable_fees => @loan.applicable_fees, :return_url => url_for_loan(@loan) + "#misc" -- if session.user.role==:admin - %hr + %fieldset + %legend Applicable fees + - if @loan.applicable_fees.empty? + = form_for(@loan, :action => url(:controller => 'loans', :action => 'levy_fees', :id => @loan.id)) do + = submit 'Apply Fees' + = partial "applicable_fees/list", :layout => false, :applicable_fees => @loan.applicable_fees, :return_url => url_for_loan(@loan) + "#misc" .box - %h3 Write Off Loan - = form_for(@loan, :action => url(:action => :write_off, :id => @loan.id), :method => "POST", :class => "_disable_button_") do - %table + %fieldset + %legend Values + %table.tall %tr + %th + Branch %td - Write off Date : - %td - = date_select_for @loan, :written_off_on + = @loan.c_branch_id %tr + %th + Center %td - Select Staff Member : + = @loan.c_center_id + %tr + %th + Scheduled Maturity Date %td - - center = @client.center - - branch = center.branch - = select :written_off_by_staff_id, :id => "written_off_by_staff_id", :collection => [branch.manager, center.manager].uniq.map{|m| [m.id, m.name]}, :prompt => "select staff member" + = @loan.c_scheduled_maturity_date %tr - %td{:colspan => "2"} - %span.greytext - Pick the staff member who is writing off the loan - %tr - %td - = submit "Write off" -- elsif staff_member = session.user.staff_member - = form_for(@loan, :action => url(:action => :suggest_write_off, :id => @loan.id), :method => "POST", :class => "_disable_button_") do - %table - %tr - %td - %b Suggest to Write Off this Loan - %tr - %td - suggested write off Date : - %td - = date_select_for @loan, :suggested_written_off_on - %tr - %td - Suggested by staff member : - %td - = staff_member.name - %td - %tr - %td - = submit "Send suggestion" - \ No newline at end of file + %th + Branch + %td + = @loan.c_branch_id diff --git a/app/views/payments/index.html.haml b/app/views/payments/index.html.haml index 456fea80..4b8be672 100644 --- a/app/views/payments/index.html.haml +++ b/app/views/payments/index.html.haml @@ -1,6 +1,6 @@ .shaded - if @loan and @loan.disbursal_date - .graph + .graph{:style => "position: relative; top: 60px"} = ofc2(550, 430, 'http://' + (request.env['HTTP_HOST'] or 'example.org') + url(:graph_data, :action => 'loan', :id => @loan.id, :scope_unit => 'months', :scope_size => 3) ) = error_messages_for @payment @@ -144,7 +144,7 @@ .shaded - %div.tab_container + %div.tab_container.clearfix %ul.tabs %li#repayment_schedule Repayment Schedule %li#repayments_made Repayments made diff --git a/public/javascripts/application.js b/public/javascripts/application.js index 7b3800e6..b47b756c 100644 --- a/public/javascripts/application.js +++ b/public/javascripts/application.js @@ -657,7 +657,7 @@ function fillBankAccounts(){ $("#bank_account_selector").html(data); } }); - }); + }); } } @@ -756,7 +756,6 @@ $(document).ready(function(){ fillFundingLines(); fillCashAccounts(); fillBankAccounts(); - $('form').highlight(); //Handling targets form $("select#target_attached_to").change(function(){ $.ajax({ diff --git a/public/stylesheets/style.css b/public/stylesheets/style.css index 39e79cf9..49708186 100644 --- a/public/stylesheets/style.css +++ b/public/stylesheets/style.css @@ -178,8 +178,8 @@ table.form{ } .shaded { - -moz-box-shadow: 2px 2px 7px #999; - box-shadow: 2px 2px 7px #999; + -moz-box-shadow: 2px 2px 7px #ccc; + box-shadow: 2px 2px 7px #ccc; padding: 5px; margin-bottom: 10px; } @@ -302,7 +302,7 @@ margin: 0px; } input[type="text"], input[type="password"], textarea { font-size: 100%; - font-family: sans; + font-family: Arial,Helvetica, FreeSans,LiberationSans,Myriad Pro,Arial,Verdana,sans-serif; background: #fbfbfb; border: 1px solid #E5E5E5; } @@ -943,6 +943,7 @@ table.tall th { background-color: #FBF8F1; padding: 10px 10px 10px 40px; text-align: right; + font-size: 0.9em; } .floating_form{ position: absolute; @@ -1475,6 +1476,12 @@ table.clearform tr td.legend{ .breadcrumb li:nth-child(4) a:after { border-left-color: hsla(85,93%,65%,1); } .breadcrumb li:nth-child(5) a { background: hsla(85,93%,75%,1); } .breadcrumb li:nth-child(5) a:after { border-left-color: hsla(85,93%,75%,1); } +.breadcrumb li:nth-child(6) a { background: hsla(85,93%,85%,1); } +.breadcrumb li:nth-child(6) a:after { border-left-color: hsla(85,93%,85%,1); } +.breadcrumb li:nth-child(7) a { background: hsla(85,93%,90%,1); } +.breadcrumb li:nth-child(7) a:after { border-left-color: hsla(85,93%,90%,1); } +.breadcrumb li:nth-child(8) a { background: hsla(85,93%,95%,1); } +.breadcrumb li:nth-child(8) a:after { border-left-color: hsla(85,93%,95%,1); } /* .breadcrumb li:last-child a { */ /* background: transparent !important; */ /* color: black; */ @@ -1485,27 +1492,170 @@ table.clearform tr td.legend{ .breadcrumb li a:hover { background: hsla(85,93%,25%,1); } .breadcrumb li a:hover:after { border-left-color: hsla(85,93%,25%,1) !important; } -.container a.applied { font-weight: bold; color: blue;} -.container a.approved { font-weight: bold; color: orange;} -.container a.outstanding { font-weight: bold; color: darkgreen;} -.container a.repaid { font-weight: bold; color: brown;} -.container a.arrears { font-weight: bold; color: red;} -.container a.written-off { font-weight: bold; color: black;} -.container a.preclosed { font-weight: bold; color: grey;} +.applied { font-weight: bold; color: blue;} +.approved { font-weight: bold; color: grey;} +.outstanding { font-weight: bold; color: darkgreen;} +.repaid { font-weight: bold; color: brown;} +.arrears { font-weight: bold; color: red;} +.written-off { font-weight: bold; color: black;} +.preclosed { font-weight: bold; color: orange;} -ul.inline { - display: inline; +table.tall { + margin: 14px; + border: solid 1px #c9c9c9; + font-size: 1.3em; +} + +table.tall tr { + border-bottom: solid 1px #cdd3fe; + vertical-align: middle; +} + +table.tall th { + padding: 8px; + background-color: #dadeff; + vertical-align: middle; +} + +table.tall .greytext { + font-weight: normal; + font-size: 0.9em; +} + +table.tall input[type="text"], input[type="password"], textarea { + font-family: Arial,Helvetica, FreeSans,LiberationSans,Myriad Pro,Arial,Verdana,sans-serif; + background: #fbfbfb; + border: 1px solid #E5E5E5; + padding: 3px; +} +table.tall input.disabled[type="submit"] { + color: #ccc; +} +table.tall select { + background: #fbfbfb; + border: 1px solid #E5E5E5; + font-size: 1.1em; } -ul.inline li { - display: inline; - list-style: circle; + +.box fieldset { + background: #fcfcfc; + -moz-border-radius: 10px; + border-radius: 10px; + border: solid 1px #c9c9c9; } +.box legend { + margin: 0px; + padding: 2px 10px; + background: #B7CA79; + color: white; +} -.depth-1 { - margin-left: 2em; +.grey_button { + -moz-box-shadow:inset 0px 1px 0px 0px #ffffff; + -webkit-box-shadow:inset 0px 1px 0px 0px #ffffff; + box-shadow:inset 0px 1px 0px 0px #ffffff; + background:-webkit-gradient( linear, left top, left bottom, color-stop(0.05, #ededed), color-stop(1, #dfdfdf) ); + background:-moz-linear-gradient( center top, #ededed 5%, #dfdfdf 100% ); + filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ededed', endColorstr='#dfdfdf'); + background-color:#ededed; + -moz-border-radius:6px; + -webkit-border-radius:6px; + border-radius:6px; + border:1px solid #dcdcdc; + display:inline-block; + color: #466124; + font-family:arial; + font-size:15px; + font-weight:bold; + padding:6px 24px; + text-decoration:none; + text-shadow:1px 1px 0px #ffffff; + line-height: 1.6em; +}.grey_button:hover { + background:-webkit-gradient( linear, left top, left bottom, color-stop(0.05, #dfdfdf), color-stop(1, #ededed) ); + background:-moz-linear-gradient( center top, #dfdfdf 5%, #ededed 100% ); + filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#dfdfdf', endColorstr='#ededed'); + background-color:#dfdfdf; +}.grey_button:active { + position:relative; + top:1px; +} + +.container input[type="submit"] { + -moz-box-shadow:inset 0px 1px 0px 0px #c1ed9c; + -webkit-box-shadow:inset 0px 1px 0px 0px #c1ed9c; + box-shadow:inset 0px 1px 0px 0px #c1ed9c; + background:-webkit-gradient( linear, left top, left bottom, color-stop(0.05, #9dce2c), color-stop(1, #8cb82b) ); + background:-moz-linear-gradient( center top, #9dce2c 5%, #8cb82b 100% ); + filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#9dce2c', endColorstr='#8cb82b'); + background-color:#9dce2c; + -moz-border-radius:6px; + -webkit-border-radius:6px; + border-radius:6px; + border:1px solid #83c41a; + display:inline-block; + color:#ffffff; + font-family:arial; + font-size:15px; + font-weight:bold; + padding:6px 24px; + text-decoration:none; + text-shadow:1px 1px 0px #689324; +} +.container input[type="submit"] { + background:-webkit-gradient( linear, left top, left bottom, color-stop(0.05, #8cb82b), color-stop(1, #9dce2c) ); + background:-moz-linear-gradient( center top, #8cb82b 5%, #9dce2c 100% ); + filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#8cb82b', endColorstr='#9dce2c'); + background-color:#8cb82b; +} +.container input[type="submit"] { + position:relative; + top:1px; +} + +a.cancelButton a.cancelButton:visited{ + color: white; } -.depth-2 { - margin-left: 4em; -} \ No newline at end of file + +.cancelButton { + -moz-box-shadow:inset 0px 1px 0px 0px #f7c5c0; + -webkit-box-shadow:inset 0px 1px 0px 0px #f7c5c0; + box-shadow:inset 0px 1px 0px 0px #f7c5c0; + background:-webkit-gradient( linear, left top, left bottom, color-stop(0.05, #fc8d83), color-stop(1, #e4685d) ); + background:-moz-linear-gradient( center top, #fc8d83 5%, #e4685d 100% ); + filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fc8d83', endColorstr='#e4685d'); + background-color:#fc8d83; + -moz-border-radius:6px; + -webkit-border-radius:6px; + border-radius:6px; + border:1px solid #d83526; + display:inline-block; + color:#ffffff; + font-family:arial; + font-size:15px; + font-weight:bold; + padding:6px 24px; + text-decoration:none; + text-shadow:1px 1px 0px #b23e35; +}.cancelButton:hover { + background:-webkit-gradient( linear, left top, left bottom, color-stop(0.05, #e4685d), color-stop(1, #fc8d83) ); + background:-moz-linear-gradient( center top, #e4685d 5%, #fc8d83 100% ); + filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#e4685d', endColorstr='#fc8d83'); + background-color:#e4685d; +}.cancelButton:active { + position:relative; + top:1px; +} + +.clearfix:after { + visibility: hidden; + display: block; + font-size: 0; + content: " "; + clear: both; + height: 0; + } +* html .clearfix { zoom: 1; } /* IE6 */ +*:first-child+html .clearfix { zoom: 1; } /* IE7 */