Sterling Rose Design Blog

Calculating Line Item Extensions

0 Comments
Tags: Javascript Rails Prototype AJAX
In my project, I have orders, and each order can have an unlimited number of line_items. Line_items are created by the user clicking on a button, which appends (via RJS) a new row to the line_items tabled form. So far, so good.

But I needed the extended price (quantity * price_per) of each line_item to be calculated every time the user tabbed out or clicked away from the price_per field. Further, I needed the subtotal, tax, total, and balance fields to be automatically re-calculated.

I messed around with it for several hours, trying Javascript, Prototype, and even jQuery, before I finally settled on a Prototype approach that worked. The real struggle was that using Rails 2.3’s nested forms functionality meant that each line_item would have an index key embedded in the middle of the text field’s name and id, and I could not come up with a good way to extract it, to pass it to the Javascript function.

Luckily, the numbering appears to be a 0-based index, so I cheated and used an incrementer.

I know it’s not a perfect solution (it’s obtrusive, it’s probably much more verbose than it needs to be) but it functions.

So, on to the code. Obviously, I’ve snipped out a lot of stuff that doesn’t have anything to do with this example.

/controllers/orders_controller.rb

def new
  @order = Order.new
  @order.line_items.build
  @order.zero_set_amounts
  @colors = Color.unremoved
end

def edit
  @order = Order.new
  @colors = Color.unremoved
end

/models/order.rb

def zero_set_amounts
  self.subtotal = 0
  self.tax = 0
  self.shipping = 0
  self.artwork = 0
  self.setup = 0
  self.printing_charge = 0
  self.misc_charge = 0
  self.total = 0
  self.balance = 0
  self.reorder ||= false
end

/views/layouts/application.html.erb

<head>
  <%= javascript_include_tag :defaults, 'main' %>
</head>

/views/orders/new.html.erb and /views/orders/edit.html.erb

<% form_for(@order) do |f| %>
  <%= render :partial => "form", :locals => {:f => f} %>
<% end %>

/views/orders/_form.html.erb

<div id="order_line_items">
  <%= render :partial => 'line_items', :locals => {:f => f, :order => @order} %>
  <div id="order_summary_fields">
    <div class="left"><%= f.label :subtotal %></div>
    <div class="right"><%= text_field_tag :subtotal, 0, :size => 10, 
      :disabled => true %></div>
    <div class="left"><%= f.label :artwork %></div>
    <div class="right"><%= f.text_field :artwork, :size => 10, :onblur => 
      'recalculateTotals();' %></div>
    <div class="left"><%= f.label :setup %></div>
    <div class="right"><%= f.text_field :setup, :size => 10, :onblur => 
      'recalculateTotals();' %></div>
    <div class="left"><%= f.label :printing_charge, "Printing" %></div>
    <div class="right"><%= f.text_field :printing_charge, :size => 10, :onblur => 
      'recalculateTotals();' %></div>
    <div class="left"><%= f.label :misc_charge, "Miscellaneous" %></div>
    <div class="right"><%= f.text_field :misc_charge, :size => 10, :onblur => 
      'recalculateTotals();' %></div>
    <div class="left"><%= f.label :shipping %></div>
    <div class="right"><%= f.text_field :shipping, :size => 10, :onblur => 
      'recalculateTotals();' %></div>
    <div class="left"><%= f.label :tax %></div>
    <div class="right"><%= text_field_tag :tax, 0, :size => 10, :disabled => true %></div>
    <div class="left"><strong><%= f.label :total %></strong></div>
    <div class="right"><%= text_field_tag :total, 0, :size => 10, 
      :disabled => true %></div>
    <div class="left"><%= f.label :balance %></div>
    <div class="right"><%= text_field_tag :balance, 0, :size => 10, 
      :disabled => true %></div>
    <%= render :partial => "shared/create_or_update", :locals => {:f => f} %>
    <%= f.hidden_field :subtotal %>
    <%= f.hidden_field :tax %>
    <%= f.hidden_field :total %>
    <%= f.hidden_field :balance%>
    <%= f.hidden_field :reorder %>
  </div>  
</div>

/views/order/_line_items.html.erb

<%= button_to_remote 'Add a New Line Item', {:url => new_line_item_path, :method => :get} %>
...
<% i = 0 %>
<% f.fields_for :line_items do |line_items_forms| %>
  <tr>
    <td><%= line_items_forms.text_field :quantity, :size => 5 %></td>
    <td><%= line_items_forms.text_field :description, :size => 30 %></td>
    <td><%= line_items_forms.select :color_id, @colors.map {|c| [c.name, c.id]} %></td>
    <td><%= line_items_forms.text_field :price_per, :size => 10, :onblur => 
      order.new_record? ? 'updateExtendedPrice("order_line_items_attributes_0_quantity", 
      this.id, "order_line_items_attributes_0_price_extended");' : 
      "updateExtendedPrice('order_line_items_attributes_#{i}_quantity', this.id, 
      'order_line_items_attributes_#{i}_price_extended');" %></td>
    <td><%= line_items_forms.text_field :price_extended, :size => 10, :class => 
      'fluffy_bunny' %></td>
   <% i += 1 %>
  </tr>
<% end %>

Yeah, I called my class “fluffy_bunny.” It’s not a class name I was likely to use anywhere else, and fluffy bunnies are cute. :)
/controllers/line_items_controller.rb

def new    
  begin
    @order = Order.find(params[:order_id])
    @line_item = @order.line_items.build
  rescue ActiveRecord::RecordNotFound
    @line_item = LineItem.new
  ensure
    @item_index = @order.nil? ? Time.now.to_i : (@order.line_items.length - 1)
  end

  respond_to do |format|
    format.html
    format.js do        
      @colors = Color.unremoved.collect {|c| [c.name, c.id]}
      @line_id = "order_line_items_attributes_#{@item_index}"
      @line_name = "order[line_items_attributes][#{@item_index}]"
    end
  end
end

/views/line_items/new.js.erb

$('line_item_table').insert(<%=js render(:partial => "new_line_item") %>, 'bottom');

/views/line_items/_new_line_item.html.erb

<tr> 
  <td><%= text_field_tag "#{@line_id}_quantity", '', :name => "#{@line_name}[quantity]", 
    :size => 5 -%></td> 
  <td><%= text_field_tag "#{@line_id}_description", '', :name => 
    "#{@line_name}[description]", :size => 30 -%></td> 
  <td><%= select_tag "#{@line_id}_color_id", options_for_select(@colors), :name => 
    "#{@line_name}[color_id]" -%></td>
  <td><%= text_field_tag "#{@line_id}_price_per", '', :name => 
    "#{@line_name}[price_per]", :size => 10, :onblur => 
    "updateExtendedPrice('order_line_items_attributes_#{@item_index}_quantity', this.id, 
    'order_line_items_attributes_#{@item_index}_price_extended');" -%></td>
  <td><%= text_field_tag "#{@line_id}_price_extended", '', :name => 
    "#{@line_name}[price_extended]", :size => 10, :class => 'fluffy_bunny' -%></td>
</tr>

/public/javascripts/main.js

function updateExtendedPrice(x,y,z) {
  $(z).value=parseFloat($(x).value) * parseFloat($(y).value);
  updateSubtotal();
  recalculateTotals();
}

function updateSubtotal() {
  var subtotal=0;
  var li;
  var line_item_extensions = $$('.fluffy_bunny');
  line_item_extensions.each(function(li) {subtotal += parseFloat(li.value);})
  $('order_subtotal').value=subtotal;
  $('subtotal').value=subtotal;
}

function updateTax() {
  /* Tax rate is 7% */
  var tax = ((parseFloat($('order_subtotal').value) + 
    parseFloat($('order_artwork').value) + parseFloat($('order_setup').value) + 
    parseFloat($('order_shipping').value) + parseFloat($('order_printing_charge').value) 
    + parseFloat($('order_misc_charge').value)) * 0.07).toFixed(2);
  $('order_tax').value=tax;
  $('tax').value=tax;
}

function updateTotal() {
  var total = (parseFloat($('order_subtotal').value) + 
  parseFloat($('order_artwork').value) + parseFloat($('order_setup').value) + 
  parseFloat($('order_tax').value) + parseFloat($('order_shipping').value) + 
  parseFloat($('order_printing_charge').value) + 
    parseFloat($('order_misc_charge').value)).toFixed(2);
  $('order_total').value=total;
  $('total').value=total;
}

function updateBalance() {
  balance = $('order_total').value;
  $('order_balance').value = balance;
  $('balance').value = balance;
}

function recalculateTotals() {
  updateTax();
  updateTotal();
  updateBalance();
}
Comments

There are no comments.

Leave a Comment











Copyright 2007-2010, Sterling Rose Design. All rights reserved.