Merge pull request #5998 from andrewpbrett/shipping-per-pound

Per-pound shipping calculator
This commit is contained in:
Maikel
2020-10-16 15:27:57 +11:00
committed by GitHub
13 changed files with 151 additions and 41 deletions

View File

@@ -84,13 +84,13 @@ module Spree
def permitted_resource_params
params.require(:shipping_method).permit(
:name, :description, :display_on,
:require_ship_address, :tag_list, :calculator_type,
:name, :description, :display_on, :require_ship_address, :tag_list, :calculator_type,
distributor_ids: [],
calculator_attributes: [
:id, :preferred_currency, :preferred_amount, :preferred_per_kg, :preferred_flat_percent,
:preferred_first_item, :preferred_additional_item, :preferred_max_items,
:preferred_minimal_amount, :preferred_normal_amount, :preferred_discount_amount
:id, :preferred_currency, :preferred_amount, :preferred_unit_from_list,
:preferred_per_unit, :preferred_flat_percent, :preferred_first_item,
:preferred_additional_item, :preferred_max_items, :preferred_minimal_amount,
:preferred_normal_amount, :preferred_discount_amount
]
)
end

View File

@@ -45,14 +45,14 @@ module Spree
end
end
def preference_field_for(form, field, options)
def preference_field_for(form, field, options, object)
case options[:type]
when :integer
form.text_field(field, preference_field_options(options))
when :boolean
form.check_box(field, preference_field_options(options))
when :string
form.text_field(field, preference_field_options(options))
preference_field_for_text_field(form, field, options, object)
when :password
form.password_field(field, preference_field_options(options))
when :text
@@ -62,6 +62,21 @@ module Spree
end
end
# Here we show a text field for all string fields except when the field name ends in
# "_from_list", in that case we render a dropdown.
# In this specific case, to render the dropdown, the object provided must have a method named
# like "#{field}_values" that returns an array with the string options to be listed.
def preference_field_for_text_field(form, field, options, object)
if field.end_with?('_from_list') && object.respond_to?("#{field}_values")
list_values = object.__send__("#{field}_values")
selected_value = object.__send__(field)
form.select(field, options_for_select(list_values, selected_value),
preference_field_options(options))
else
form.text_field(field, preference_field_options(options))
end
end
def preference_field_options(options)
field_options = case options[:type]
when :integer
@@ -91,13 +106,21 @@ module Spree
)
end
# maps each preference to a hash containing the label and field html.
# E.g. { :label => "<label>...", :field => "<select>..." }
def preference_fields(object, form)
return unless object.respond_to?(:preferences)
object.preferences.keys.map{ |key|
form.label("preferred_#{key}", Spree.t(key) + ": ") +
preference_field_for(form, "preferred_#{key}", type: object.preference_type(key))
}.join("<br />").html_safe
object.preferences.keys.map { |key|
preference_label = form.label("preferred_#{key}",
Spree.t(key.to_s.gsub("_from_list", "")) + ": ").html_safe
preference_field = preference_field_for(
form,
"preferred_#{key}",
{ type: object.preference_type(key) }, object
).html_safe
{ label: preference_label, field: preference_field }
}
end
def link_to_add_fields(name, target, options = {})

View File

@@ -3,16 +3,30 @@ require 'spree/localized_number'
module Calculator
class Weight < Spree::Calculator
extend Spree::LocalizedNumber
preference :per_kg, :decimal, default: 0.0
localize_number :preferred_per_kg
preference :unit_from_list, :string, default: "kg"
preference :per_unit, :decimal, default: 0.0
localize_number :preferred_per_unit
def self.description
I18n.t('spree.weight')
end
def set_preference(name, value)
if name == :unit_from_list && !["kg", "lb"].include?(value)
calculable.errors.add(:preferred_unit_from_list, I18n.t(:calculator_preferred_unit_error))
else
__send__ self.class.preference_setter_method(name), value
end
end
def compute(object)
line_items = line_items_for object
(total_weight(line_items) * preferred_per_kg).round(2)
(total_weight(line_items) * preferred_per_unit).round(2)
end
def preferred_unit_from_list_values
["kg", "lb"]
end
private
@@ -33,8 +47,8 @@ module Calculator
def weight_per_variant(line_item)
if variant_unit(line_item) == 'weight'
# The calculator price is per_kg so we need to convert unit_value to kg
convert_g_to_kg(line_item.variant.andand.unit_value)
# Convert unit_value to the preferred unit
convert_weight(line_item.variant.andand.unit_value)
else
line_item.variant.andand.weight || 0
end
@@ -42,8 +56,8 @@ module Calculator
def weight_per_final_weight_volume(line_item)
if variant_unit(line_item) == 'weight'
# The calculator price is per_kg so we need to convert final_weight_volume to kg
convert_g_to_kg(line_item.final_weight_volume)
# Convert final_weight_volume to the preferred unit
convert_weight(line_item.final_weight_volume)
else
weight_per_variant(line_item) * quantity_implied_in_final_weight_volume(line_item)
end
@@ -66,10 +80,14 @@ module Calculator
line_item.variant.product.andand.variant_unit
end
def convert_g_to_kg(value)
return 0 unless value
def convert_weight(value)
return 0 unless value && ["kg", "lb"].include?(preferences[:unit_from_list])
value / 1000
if preferences[:unit_from_list] == "kg"
value / 1000
elsif preferences[:unit_from_list] == "lb"
value / 453.6
end
end
end
end

View File

@@ -7,6 +7,10 @@
- if !enterprise_fee.new_record?
.calculator-settings
= f.fields_for :calculator do |calculator_form|
= preference_fields(enterprise_fee.calculator, calculator_form)
- preference_fields(enterprise_fee.calculator, calculator_form).each do |field|
.field.alpha.one.columns
= field[:label]
.field.omega.two.columns
= field[:field]
= calculator_form_string

View File

@@ -10,4 +10,8 @@
= t(:provider_settings)
.preference-settings
= fields_for :payment_method, @payment_method do |payment_method_form|
= preference_fields(@payment_method, payment_method_form)
- preference_fields(@payment_method, payment_method_form).each do |field|
.field.alpha.three.columns
= field[:label]
.field.omega.eight.columns
= field[:field]

View File

@@ -2,12 +2,18 @@
%legend{align: "center"}= t(:calculator)
#preference-settings
.field
= f.label(:calculator_type, t(:calculator), for: 'calc_type')
= f.select(:calculator_type, @calculators.map { |c| [c.description, c.name] }, {}, {id: 'calc_type', class: 'select2 fullwidth'})
.alpha.three.columns
= f.label(:calculator_type, t(:calculator), for: 'calc_type')
.omega.eight.columns
= f.select(:calculator_type, @calculators.map { |c| [c.description, c.name] }, {}, {id: 'calc_type', class: 'select2 fullwidth'})
- if !@object.new_record?
.field
.calculator-settings
= f.fields_for :calculator do |calculator_form|
= preference_fields(@object.calculator, calculator_form)
- preference_fields(@object.calculator, calculator_form).each do |field|
.field.alpha.three.columns
= field[:label]
.field.omega.eight.columns
= field[:field]
- if @object.calculator.respond_to?(:preferences)
%span.calculator-settings-warning.info.warning= t(:calculator_settings_warning)

View File

@@ -44,8 +44,7 @@
%tags-with-translation#something{ object: "shippingMethod", 'find-tags' => 'findTags(query)' }
.row
.alpha.eleven.columns
= render partial: 'spree/admin/shared/calculator_fields', locals: { f: f }
= render partial: 'spree/admin/shared/calculator_fields', locals: { f: f }
.row
.alpha.eleven.columns

View File

@@ -2125,6 +2125,7 @@ See the %{link} to find out more about %{sitename}'s features and to start using
calculator: "Calculator"
calculator_values: "Calculator values"
calculator_settings_warning: "If you are changing the calculator type, you must save first before you can edit the calculator settings"
calculator_preferred_unit_error: "must be kg or lb"
flat_percent_per_item: "Flat Percent (per item)"
flat_rate_per_item: "Flat Rate (per item)"
flat_rate_per_order: "Flat Rate (per order)"
@@ -3146,7 +3147,7 @@ See the %{link} to find out more about %{sitename}'s features and to start using
inventory_error_flash_for_insufficient_quantity: "An item in your cart has become unavailable."
inventory: Inventory
zipcode: Postcode
weight: Weight (per kg)
weight: Weight (per kg or lb)
error_user_destroy_with_orders: "Users with completed orders may not be deleted"
cannot_create_payment_without_payment_methods: "You cannot create a payment for an order without any payment methods defined."
please_define_payment_methods: "Please define some payment methods first."

View File

@@ -0,0 +1,13 @@
class UpdateWeightCalculators < ActiveRecord::Migration
def change
Spree::Calculator.connection.execute(
"UPDATE spree_preferences SET key = replace( key, 'per_kg', 'per_unit') WHERE key ilike '/calculator/weight/per_kg/%'"
)
Calculator::Weight.all.each { |calculator|
calculator.preferred_unit_from_list = 'kg'
Rails.cache.delete("/calculator/weight/per_kg/#{calculator.id}")
calculator.save
}
end
end

View File

@@ -29,13 +29,22 @@ describe Spree::Admin::ShippingMethodsController, type: :controller do
expect(shipping_method.reload.calculator.preferred_currency).to eq "EUR"
end
it "updates preferred_per_kg of a Weight calculator" do
it "updates preferred_per_unit of a Weight calculator" do
shipping_method.calculator = create(:weight_calculator, calculable: shipping_method)
params[:shipping_method][:calculator_attributes][:preferred_per_kg] = 10
params[:shipping_method][:calculator_attributes][:preferred_per_unit] = 10
spree_post :update, params
expect(shipping_method.reload.calculator.preferred_per_kg).to eq 10
expect(shipping_method.reload.calculator.preferred_per_unit).to eq 10
end
it "updates preferred_unit of a Weight calculator" do
shipping_method.calculator = create(:weight_calculator, calculable: shipping_method)
params[:shipping_method][:calculator_attributes][:preferred_unit_from_list] = "kg"
spree_post :update, params
expect(shipping_method.reload.calculator.preferred_unit_from_list).to eq "kg"
end
it "updates preferred_flat_percent of a FlatPercentPerItem calculator" do

View File

@@ -13,7 +13,14 @@ FactoryBot.define do
end
factory :weight_calculator, class: Calculator::Weight do
after(:build) { |c| c.set_preference(:per_kg, 0.5) }
after(:create) { |c| c.set_preference(:per_kg, 0.5); c.save! }
after(:build) { |c|
c.set_preference(:per_unit, 0.5)
c.set_preference(:unit_from_list, "kg")
}
after(:create) { |c|
c.set_preference(:per_unit, 0.5)
c.set_preference(:unit_from_list, "kg")
c.save!
}
end
end

View File

@@ -118,7 +118,7 @@ feature "full-page cart", js: true do
describe "admin weight calculated fees" do
context "order with 2 line items" do
let(:admin_fee) {
create(:enterprise_fee, calculator: Calculator::Weight.new(preferred_per_kg: 1),
create(:enterprise_fee, calculator: Calculator::Weight.new(preferred_per_unit: 1, preferred_unit_from_list: "kg"),
enterprise: order_cycle.coordinator, fee_type: 'admin')
}

View File

@@ -1,7 +1,7 @@
require 'spec_helper'
describe Calculator::Weight do
it_behaves_like "a model using the LocalizedNumber module", [:preferred_per_kg]
it_behaves_like "a model using the LocalizedNumber module", [:preferred_per_unit]
it "computes shipping cost for an order by total weight" do
variant1 = build_stubbed(:variant, unit_value: 10_000)
@@ -14,7 +14,8 @@ describe Calculator::Weight do
order = double(:order, line_items: [line_item1, line_item2, line_item3])
subject.set_preference(:per_kg, 5)
subject.set_preference(:per_unit, 5)
subject.set_preference(:unit_from_list, "kg")
expect(subject.compute(order)).to eq(350) # (10 * 1 + 20 * 3) * 5
end
@@ -22,7 +23,10 @@ describe Calculator::Weight do
let(:variant) { build_stubbed(:variant, unit_value: 10_000) }
let(:line_item) { build_stubbed(:line_item, variant: variant, quantity: 2) }
before { subject.set_preference(:per_kg, 5) }
before {
subject.set_preference(:per_unit, 5)
subject.set_preference(:unit_from_list, "kg")
}
it "computes shipping cost for a line item" do
expect(subject.compute(line_item)).to eq(100) # 10 * 2 * 5
@@ -57,15 +61,18 @@ describe Calculator::Weight do
order = double(:order, line_items: [line_item1, line_item2])
object_with_order = double(:object_with_order, order: order)
subject.set_preference(:per_kg, 5)
subject.set_preference(:per_unit, 5)
subject.set_preference(:unit_from_list, "kg")
expect(subject.compute(object_with_order)).to eq(250) # (10 * 1 + 20 * 2) * 5
subject.set_preference(:unit_from_list, "lb")
expect(subject.compute(object_with_order)).to eq(551.15) # (10 * 1 + 20 * 2) * 5 * 2.2
end
context "when line item final_weight_volume is set" do
let!(:product) { build_stubbed(:product, product_attributes) }
let!(:variant) { build_stubbed(:variant, variant_attributes.merge(product: product)) }
let(:calculator) { described_class.new(preferred_per_kg: 6) }
let(:calculator) { described_class.new(preferred_per_unit: 6, preferred_unit_from_list: "kg") }
let(:line_item) do
build_stubbed(:line_item, variant: variant, quantity: 2).tap do |object|
object.send(:calculate_final_weight_volume)
@@ -182,7 +189,10 @@ describe Calculator::Weight do
}
let(:line_item) { build_stubbed(:line_item, variant: variant, quantity: 1) }
before { subject.set_preference(:per_kg, 5) }
before {
subject.set_preference(:per_unit, 5)
subject.set_preference(:unit_from_list, "kg")
}
context "when unit_value is zero variant.weight is present" do
let(:variant) { build_stubbed(:variant, product: product, unit_value: 0, weight: 10.0) }
@@ -224,4 +234,20 @@ describe Calculator::Weight do
end
end
end
it "allows a preferred_unit of 'kg' and 'lb'" do
subject.calculable = build(:shipping_method)
subject.set_preference(:per_unit, 5)
subject.set_preference(:unit_from_list, "kg")
expect(subject.calculable.errors.count).to eq(0)
subject.set_preference(:unit_from_list, "lb")
expect(subject.calculable.errors.count).to eq(0)
end
it "does not allow a preferred_unit of anything but 'kg' or 'lb'" do
subject.calculable = build(:shipping_method)
subject.set_preference(:per_unit, 5)
subject.set_preference(:unit_from_list, "kb")
expect(subject.calculable.errors.count).to eq(1)
end
end