Skip to content

Instantly share code, notes, and snippets.

@psy-q
Created June 6, 2010 06:34
Show Gist options
  • Save psy-q/427373 to your computer and use it in GitHub Desktop.
Save psy-q/427373 to your computer and use it in GitHub Desktop.
class ShippingRate < ActiveRecord::Base
has_many :shipping_costs
has_many :suppliers
validates_presence_of :name
# Input: Weight in grams
# Output: Price for delivery according to associated ShippingCosts
def calculate_for_weight(weight)
@total_cost = 0
@remaining_weight ||= weight # shouldn't really be an instance variable, but scope seems weird below otherwise?
while @remaining_weight > 0.to_f
# The weight is too high to fit into one package, we pick this supplier's highest-weight
# shipping rate, reduce the remaining weight and continue with the rest of the weight (this splits
# orders into multiple packages)
if @remaining_weight > self.maximum_weight
@remaining_weight = (@remaining_weight - self.maximum_weight)
@total_cost += self.shipping_costs.select{|s| s.weight_max == self.maximum_weight}.first.price
add_taxes(self.shipping_costs.select{|s| s.weight_max == self.maximum_weight}.first.taxes)
else
shipping_cost = self.shipping_costs.select{ |sc| @remaining_weight.between?(sc.weight_min, sc.weight_max) }
if shipping_cost.blank?
# No shipping cost was found that matches the weight of this order
# so we assume the order is below the lowest weight and pick the first
# shipping cost of this supplier
@total_cost += self.shipping_costs.first.net_price
add_taxes(self.shipping_costs.first.taxes)
@remaining_weight -= @remaining_weight
else
# A match was found, the weight is between two available shipping rates (shipping_cost.first is this rate)
@total_cost += shipping_cost.first.net_price
add_taxes(shipping_cost.first.taxes)
@remaining_weight -= @remaining_weight
end
end
end
return BigDecimal(@total_cost.to_s)
end
# Input: Weight in grams
# Output: Integer representing the number of packages necessary to ship
# items as heavy as the input weight.
def package_count_for_weight(weight)
(weight / self.maximum_weight).ceil
end
# Calculates incoming shipping cost for all the items in a document. Takes into account
# whether they ship directly from manufacturer to customer or whether they have to be
# shipped twice, once to a workshop and from there to the customer
def calculate_incoming(document)
@total_weight = 0
@incoming_cost = 0
@incoming_package_count = 0
# On direct shipping orders, incoming cost is always 0, we can skip expensive
# calculations on this document.
unless document.direct_shipping?
# We do incoming shipping calculation on a flattened set of all products
# necessary for this order. That includes incoming supply items _and_ complete
# products that we order and ship directly to the customer
supplier_ids = document.existing_incoming_supplier_ids
supplier_ids.each do |sup_id|
this_suppliers_weight = 0
this_suppliers_products_and_supply_items = document.incoming_products_and_supply_items.select{ |p| p.supplier_id == sup_id }
this_suppliers_products_and_supply_items.each do |p|
this_things_weight = document.quantity_of(p) * p.weight
this_suppliers_weight += this_things_weight
@total_weight += this_things_weight
end
@incoming_cost += Supplier.find(sup_id).shipping_rate.calculate_for_weight(this_suppliers_weight * 1000)
@incoming_package_count += Supplier.find(sup_id).shipping_rate.package_count_for_weight(document.weight * 1000)
end
end
end
def calculate_outgoing(document)
if document.direct_shipping?
# This is safe because Document#direct_shipping? checks to make sure there is only one
# supplier.
sr = ShippingRate.find(document.suppliers.first.shipping_rate.id)
else
# We set up an internal shipping rate that is used for outgoing
# calculations from our own store. This should be replaced by a configuration
# or a database setting that guarantees that an outgoing shipping rate is _always_
# available. The database-based one would be more configurable than this,
# but in the first phases, we don't want to make the software dependent
# on specific database entries.
sr = ShippingRate.new
# Source: http://www.post.ch/post-startseite/post-privatkunden/post-versenden/post-pakete-inland/post-pakete-inland-preise.htm
# TODO: Extract to default shipping rate object, change prices to gross (excluding VAT)
sr.shipping_costs << ShippingCost.new(:weight_min => 0, :weight_max => 2000, :price => 8.0)
sr.shipping_costs << ShippingCost.new(:weight_min => 2001, :weight_max => 5000, :price => 10.0)
sr.shipping_costs << ShippingCost.new(:weight_min => 5001, :weight_max => 10000, :price => 13.0)
sr.shipping_costs << ShippingCost.new(:weight_min => 10001, :weight_max => 20000, :price => 19.0)
sr.shipping_costs << ShippingCost.new(:weight_min => 20001, :weight_max => 30000, :price => 26.0)
# This default tax class for shipping would need to be made configurable in the
# final system, especially if it is going to be released as Free Software.
sr.shipping_costs.each do |sc|
sc.tax_class = TaxClass.find_by_percentage("7.6")
end
end
@outgoing_cost = sr.calculate_for_weight(document.weight * 1000)
@outgoing_package_count = sr.package_count_for_weight(document.weight * 1000)
end
# Can calulate shipping for both orders or carts (= documents)
def calculate_for(document)
calculate_incoming(document)
puts "randomly outputting total taxes after inc: #{total_taxes}"
calculate_outgoing(document)
puts "randomly outputting total taxes after out: #{total_taxes}"
end
def incoming_cost
@incoming_cost or BigDecimal.new("0.0")
end
def outgoing_cost
@outgoing_cost or BigDecimal.new("0.0")
end
def incoming_package_count
@incoming_package_count or 0
end
def outgoing_package_count
@outgoing_package_count or 0
end
def total_cost
incoming_cost + outgoing_cost
end
def add_taxes(amount)
@total_taxes ||= 0
@total_taxes += amount
puts "***********************************************"
puts "added #{amount} to total taxes: #{@total_taxes}"
puts "***********************************************"
end
def total_taxes
@total_taxes or BigDecimal.new("0.0")
end
# Maximum weight carried by this ShippingRate's ShippingCosts, in grams
def maximum_weight
self.shipping_costs.collect(&:weight_max).max
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment