From 4d81b145cab596801a6d65cba90cf33f0d70c410 Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou Date: Tue, 18 Jun 2024 11:51:47 +1000 Subject: [PATCH 01/68] Add variant_unit_scale, variant_unit_name to variant --- db/migrate/20240618013850_add_variant_unit_to_variant.rb | 6 ++++++ db/schema.rb | 2 ++ 2 files changed, 8 insertions(+) create mode 100644 db/migrate/20240618013850_add_variant_unit_to_variant.rb diff --git a/db/migrate/20240618013850_add_variant_unit_to_variant.rb b/db/migrate/20240618013850_add_variant_unit_to_variant.rb new file mode 100644 index 00000000000..ec7d7f775ed --- /dev/null +++ b/db/migrate/20240618013850_add_variant_unit_to_variant.rb @@ -0,0 +1,6 @@ +class AddVariantUnitToVariant < ActiveRecord::Migration[7.0] + def change + add_column :spree_variants, :variant_unit_scale, :float + add_column :spree_variants, :variant_unit_name, :string, limit: 255 + end +end diff --git a/db/schema.rb b/db/schema.rb index 9ca97c3eef7..ced400f493c 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -968,6 +968,8 @@ t.bigint "shipping_category_id" t.bigint "primary_taxon_id" t.bigint "supplier_id" + t.float "variant_unit_scale" + t.string "variant_unit_name", limit: 255 t.index ["primary_taxon_id"], name: "index_spree_variants_on_primary_taxon_id" t.index ["product_id"], name: "index_variants_on_product_id" t.index ["shipping_category_id"], name: "index_spree_variants_on_shipping_category_id" From e33ed5141b90c26614ba04ff3815264b0a51447e Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou Date: Tue, 18 Jun 2024 15:40:20 +1000 Subject: [PATCH 02/68] Fix weigths and measures Use variant_unit, variant_unit_scale from the variant --- app/services/weights_and_measures.rb | 10 +-- spec/services/weights_and_measures_spec.rb | 72 +++++++++++----------- 2 files changed, 40 insertions(+), 42 deletions(-) diff --git a/app/services/weights_and_measures.rb b/app/services/weights_and_measures.rb index e285a0baaca..ab436a712e2 100644 --- a/app/services/weights_and_measures.rb +++ b/app/services/weights_and_measures.rb @@ -16,10 +16,10 @@ def scale_for_unit_value def system return "custom" unless scales = scales_for_variant_unit(ignore_available_units: true) - product_scale = @variant.product.variant_unit_scale&.to_f - return "custom" unless product_scale.present? && product_scale.positive? + variant_scale = @variant.variant_unit_scale&.to_f + return "custom" unless variant_scale.present? && variant_scale.positive? - scales[product_scale]['system'] + scales[variant_scale]['system'] end # @returns enumerable with label and value for select @@ -92,9 +92,9 @@ def self.available_units_sorted }.freeze def scales_for_variant_unit(ignore_available_units: false) - return @units[@variant.product.variant_unit] if ignore_available_units + return @units[@variant.variant_unit] if ignore_available_units - @units[@variant.product.variant_unit]&.reject { |_scale, unit_info| + @units[@variant.variant_unit]&.reject { |_scale, unit_info| self.class.available_units.exclude?(unit_info['name']) } end diff --git a/spec/services/weights_and_measures_spec.rb b/spec/services/weights_and_measures_spec.rb index 0992316886c..5b359469e24 100644 --- a/spec/services/weights_and_measures_spec.rb +++ b/spec/services/weights_and_measures_spec.rb @@ -4,57 +4,55 @@ RSpec.describe WeightsAndMeasures do subject { WeightsAndMeasures.new(variant) } - let(:variant) { Spree::Variant.new } - let(:product) { instance_double(Spree::Product) } + let(:variant) { instance_double(Spree::Variant) } let(:available_units) { ["mg", "g", "kg", "T", "oz", "lb", "mL", "cL", "dL", "L", "kL", "gal"].join(",") } before do - allow(variant).to receive(:product) { product } allow(Spree::Config).to receive(:available_units).and_return(available_units) end describe "#system" do context "weight" do before do - allow(product).to receive(:variant_unit) { "weight" } + allow(variant).to receive(:variant_unit) { "weight" } end it "when scale is for a metric unit" do - allow(product).to receive(:variant_unit_scale) { 1.0 } + allow(variant).to receive(:variant_unit_scale) { 1.0 } expect(subject.system).to eq("metric") end it "when scale is for an imperial unit" do - allow(product).to receive(:variant_unit_scale) { 28.35 } + allow(variant).to receive(:variant_unit_scale) { 28.35 } expect(subject.system).to eq("imperial") end it "when precise scale is for an imperial unit" do - allow(product).to receive(:variant_unit_scale) { 28.349523125 } + allow(variant).to receive(:variant_unit_scale) { 28.349523125 } expect(subject.system).to eq("imperial") end end context "volume" do it "when scale is for a metric unit" do - allow(product).to receive(:variant_unit) { "volume" } - allow(product).to receive(:variant_unit_scale) { 1.0 } + allow(variant).to receive(:variant_unit) { "volume" } + allow(variant).to receive(:variant_unit_scale) { 1.0 } expect(subject.system).to eq("metric") end end context "items" do it "when variant unit is items" do - allow(product).to receive(:variant_unit) { "items" } - allow(product).to receive(:variant_unit_scale) { nil } + allow(variant).to receive(:variant_unit) { "items" } + allow(variant).to receive(:variant_unit_scale) { nil } expect(subject.system).to eq("custom") end it "when variant unit is items, even if the scale is present" do - allow(product).to receive(:variant_unit) { "items" } - allow(product).to receive(:variant_unit_scale) { 1.0 } + allow(variant).to receive(:variant_unit) { "items" } + allow(variant).to receive(:variant_unit_scale) { 1.0 } expect(subject.system).to eq("custom") end end @@ -62,38 +60,38 @@ # In the event of corrupt data, we don't want an exception context "corrupt data" do it "when unit is invalid, scale is valid" do - allow(product).to receive(:variant_unit) { "blah" } - allow(product).to receive(:variant_unit_scale) { 1.0 } + allow(variant).to receive(:variant_unit) { "blah" } + allow(variant).to receive(:variant_unit_scale) { 1.0 } expect(subject.system).to eq("custom") end it "when unit is invalid, scale is nil" do - allow(product).to receive(:variant_unit) { "blah" } - allow(product).to receive(:variant_unit_scale) { nil } + allow(variant).to receive(:variant_unit) { "blah" } + allow(variant).to receive(:variant_unit_scale) { nil } expect(subject.system).to eq("custom") end it "when unit is nil, scale is valid" do - allow(product).to receive(:variant_unit) { nil } - allow(product).to receive(:variant_unit_scale) { 1.0 } + allow(variant).to receive(:variant_unit) { nil } + allow(variant).to receive(:variant_unit_scale) { 1.0 } expect(subject.system).to eq("custom") end it "when unit is nil, scale is nil" do - allow(product).to receive(:variant_unit) { nil } - allow(product).to receive(:variant_unit_scale) { nil } + allow(variant).to receive(:variant_unit) { nil } + allow(variant).to receive(:variant_unit_scale) { nil } expect(subject.system).to eq("custom") end it "when unit is valid, but scale is nil" do - allow(product).to receive(:variant_unit) { "weight" } - allow(product).to receive(:variant_unit_scale) { nil } + allow(variant).to receive(:variant_unit) { "weight" } + allow(variant).to receive(:variant_unit_scale) { nil } expect(subject.system).to eq("custom") end it "when unit is valid, but scale is 0" do - allow(product).to receive(:variant_unit) { "weight" } - allow(product).to receive(:variant_unit_scale) { 0.0 } + allow(variant).to receive(:variant_unit) { "weight" } + allow(variant).to receive(:variant_unit_scale) { 0.0 } expect(subject.system).to eq("custom") end end @@ -149,18 +147,18 @@ describe "#scales_for_unit_value" do context "weight" do before do - allow(product).to receive(:variant_unit) { "weight" } + allow(variant).to receive(:variant_unit) { "weight" } end context "metric" do it "for a unit value that should display in grams" do - allow(product).to receive(:variant_unit_scale) { 1.0 } + allow(variant).to receive(:variant_unit_scale) { 1.0 } allow(variant).to receive(:unit_value) { 500 } expect(subject.scale_for_unit_value).to eq([1.0, "g"]) end it "for a unit value that should display in kg" do - allow(product).to receive(:variant_unit_scale) { 1.0 } + allow(variant).to receive(:variant_unit_scale) { 1.0 } allow(variant).to receive(:unit_value) { 1500 } expect(subject.scale_for_unit_value).to eq([1000.0, "kg"]) end @@ -169,7 +167,7 @@ let(:available_units) { ["mg", "g", "T"].join(",") } it "should display in g" do - allow(product).to receive(:variant_unit_scale) { 1.0 } + allow(variant).to receive(:variant_unit_scale) { 1.0 } allow(variant).to receive(:unit_value) { 1500 } expect(subject.scale_for_unit_value).to eq([1.0, "g"]) end @@ -179,15 +177,15 @@ context "volume" do it "for a unit value that should display in kL" do - allow(product).to receive(:variant_unit) { "volume" } - allow(product).to receive(:variant_unit_scale) { 1.0 } + allow(variant).to receive(:variant_unit) { "volume" } + allow(variant).to receive(:variant_unit_scale) { 1.0 } allow(variant).to receive(:unit_value) { 1500 } expect(subject.scale_for_unit_value).to eq([1000, "kL"]) end it "for a unit value that should display in dL" do - allow(product).to receive(:variant_unit) { "volume" } - allow(product).to receive(:variant_unit_scale) { 1.0 } + allow(variant).to receive(:variant_unit) { "volume" } + allow(variant).to receive(:variant_unit_scale) { 1.0 } allow(variant).to receive(:unit_value) { 0.5 } expect(subject.scale_for_unit_value).to eq([0.1, "dL"]) end @@ -195,8 +193,8 @@ context "should not display in dL/cL if those units are not selected" do let(:available_units){ ["mL", "L", "kL", "gal"].join(",") } it "for a unit value that should display in mL" do - allow(product).to receive(:variant_unit) { "volume" } - allow(product).to receive(:variant_unit_scale) { 1.0 } + allow(variant).to receive(:variant_unit) { "volume" } + allow(variant).to receive(:variant_unit_scale) { 1.0 } allow(variant).to receive(:unit_value) { 0.5 } expect(subject.scale_for_unit_value).to eq([0.001, "mL"]) end @@ -205,8 +203,8 @@ context "items" do it "when scale is for items" do - allow(product).to receive(:variant_unit) { "items" } - allow(product).to receive(:variant_unit_scale) { nil } + allow(variant).to receive(:variant_unit) { "items" } + allow(variant).to receive(:variant_unit_scale) { nil } allow(variant).to receive(:unit_value) { 4 } expect(subject.scale_for_unit_value).to eq([nil, nil]) end From 3b89cd59578242448038876ce933245651b182c3 Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou Date: Tue, 18 Jun 2024 15:54:18 +1000 Subject: [PATCH 03/68] Fix option value namer Uses the variant variant_unit, variant_unit_name, variant_unit_scale --- .../variant_units/option_value_namer.rb | 8 +- .../variant_units/option_value_namer_spec.rb | 123 ++++++++---------- 2 files changed, 60 insertions(+), 71 deletions(-) diff --git a/app/services/variant_units/option_value_namer.rb b/app/services/variant_units/option_value_namer.rb index 6a36684fb37..40b1bd04416 100644 --- a/app/services/variant_units/option_value_namer.rb +++ b/app/services/variant_units/option_value_namer.rb @@ -32,16 +32,16 @@ def unit private def value_scaled? - @nameable.product.variant_unit_scale.present? + @nameable.variant_unit_scale.present? end def option_value_value_unit - if @nameable.unit_value.present? && @nameable.product&.persisted? - if %w(weight volume).include? @nameable.product.variant_unit + if @nameable.unit_value.present? + if %w(weight volume).include? @nameable.variant_unit value, unit_name = option_value_value_unit_scaled else value = @nameable.unit_value - unit_name = pluralize(@nameable.product.variant_unit_name, value) + unit_name = pluralize(@nameable.variant_unit_name, value) end value = value.to_i if value == value.to_i diff --git a/spec/services/variant_units/option_value_namer_spec.rb b/spec/services/variant_units/option_value_namer_spec.rb index 58ce97d2461..c3f06746f5f 100644 --- a/spec/services/variant_units/option_value_namer_spec.rb +++ b/spec/services/variant_units/option_value_namer_spec.rb @@ -5,9 +5,8 @@ module VariantUnits RSpec.describe OptionValueNamer do describe "generating option value name" do + subject { OptionValueNamer.new(v) } let(:v) { Spree::Variant.new } - let(:p) { Spree::Product.new } - let(:subject) { OptionValueNamer.new(v) } it "when description is blank" do allow(v).to receive(:unit_description) { nil } @@ -40,18 +39,14 @@ module VariantUnits describe "determining if a variant's value is scaled" do it "returns true when the product has a scale" do - p = Spree::Product.new variant_unit_scale: 1000 - v = Spree::Variant.new - allow(v).to receive(:product) { p } + v = Spree::Variant.new variant_unit_scale: 1000 subject = OptionValueNamer.new v expect(subject.__send__(:value_scaled?)).to be true end it "returns false otherwise" do - p = Spree::Product.new v = Spree::Variant.new - allow(v).to receive(:product) { p } subject = OptionValueNamer.new v expect(subject.__send__(:value_scaled?)).to be false @@ -59,115 +54,109 @@ module VariantUnits end describe "generating option value's value and unit" do - let(:v) { Spree::Variant.new } - let(:subject) { OptionValueNamer.new v } - before do allow(Spree::Config).to receive(:available_units).and_return("g,lb,oz,kg,T,mL,L,kL") end it "generates simple values" do - p = double(:product, variant_unit: 'weight', variant_unit_scale: 1.0) - allow(v).to receive(:product) { p } - allow(p).to receive(:persisted?) { true } - allow(v).to receive(:unit_value) { 100 } + v = instance_double(Spree::Variant, variant_unit: 'weight', variant_unit_scale: 1.0, + unit_value: 100) - expect(subject.__send__(:option_value_value_unit)).to eq [100, 'g'] + option_value_namer = OptionValueNamer.new v + expect(option_value_namer.__send__(:option_value_value_unit)).to eq [100, 'g'] end it "generates values when unit value is non-integer" do - p = double(:product, variant_unit: 'weight', variant_unit_scale: 1.0) - allow(v).to receive(:product) { p } - allow(p).to receive(:persisted?) { true } - allow(v).to receive(:unit_value) { 123.45 } + v = instance_double(Spree::Variant, variant_unit: 'weight', variant_unit_scale: 1.0, + unit_value: 123.45) - expect(subject.__send__(:option_value_value_unit)).to eq [123.45, 'g'] + option_value_namer = OptionValueNamer.new v + expect(option_value_namer.__send__(:option_value_value_unit)).to eq [123.45, 'g'] end it "returns a value of 1 when unit value equals the scale" do - p = double(:product, variant_unit: 'weight', variant_unit_scale: 1000.0) - allow(v).to receive(:product) { p } - allow(p).to receive(:persisted?) { true } - allow(v).to receive(:unit_value) { 1000.0 } + v = instance_double(Spree::Variant, variant_unit: 'weight', variant_unit_scale: 1000.0, + unit_value: 1000.0) - expect(subject.__send__(:option_value_value_unit)).to eq [1, 'kg'] + option_value_namer = OptionValueNamer.new v + expect(option_value_namer.__send__(:option_value_value_unit)).to eq [1, 'kg'] end it "returns only values that are in the same measurement systems" do - p = double(:product, variant_unit: 'weight', variant_unit_scale: 1.0) - allow(v).to receive(:product) { p } - allow(p).to receive(:persisted?) { true } - allow(v).to receive(:unit_value) { 500 } + v = instance_double(Spree::Variant, variant_unit: 'weight', variant_unit_scale: 1.0, + unit_value: 500) + # 500g would convert to > 1 pound, but we don't want the namer to use # pounds since it's in a different measurement system. - expect(subject.__send__(:option_value_value_unit)).to eq [500, 'g'] + option_value_namer = OptionValueNamer.new v + expect(option_value_namer.__send__(:option_value_value_unit)).to eq [500, 'g'] end it "generates values for all weight scales" do [[1.0, 'g'], [28.35, 'oz'], [453.6, 'lb'], [1000.0, 'kg'], [1_000_000.0, 'T']].each do |scale, unit| - p = double(:product, variant_unit: 'weight', variant_unit_scale: scale) - allow(v).to receive(:product) { p } - allow(p).to receive(:persisted?) { true } - allow(v).to receive(:unit_value) { 10.0 * scale } - expect(subject.__send__(:option_value_value_unit)).to eq [10, unit] + v = instance_double(Spree::Variant, variant_unit: 'weight', variant_unit_scale: scale, + unit_value: 10.0 * scale) + + option_value_namer = OptionValueNamer.new v + expect(option_value_namer.__send__(:option_value_value_unit)).to eq [10, unit] end end it "generates values for all volume scales" do [[0.001, 'mL'], [1.0, 'L'], [1000.0, 'kL']].each do |scale, unit| - p = double(:product, variant_unit: 'volume', variant_unit_scale: scale) - allow(v).to receive(:product) { p } - allow(p).to receive(:persisted?) { true } - allow(v).to receive(:unit_value) { 3 * scale } - expect(subject.__send__(:option_value_value_unit)).to eq [3, unit] + v = instance_double(Spree::Variant, variant_unit: 'volume', variant_unit_scale: scale, + unit_value: 3 * scale) + + option_value_namer = OptionValueNamer.new v + expect(option_value_namer.__send__(:option_value_value_unit)).to eq [3, unit] end end it "chooses the correct scale when value is very small" do - p = double(:product, variant_unit: 'volume', variant_unit_scale: 0.001) - allow(v).to receive(:product) { p } - allow(p).to receive(:persisted?) { true } - allow(v).to receive(:unit_value) { 0.0001 } - expect(subject.__send__(:option_value_value_unit)).to eq [0.1, 'mL'] + v = instance_double(Spree::Variant, variant_unit: 'volume', variant_unit_scale: 0.001, + unit_value: 0.0001) + + option_value_namer = OptionValueNamer.new v + expect(option_value_namer.__send__(:option_value_value_unit)).to eq [0.1, 'mL'] end it "generates values for item units" do %w(packet box).each do |unit| - p = double(:product, variant_unit: 'items', variant_unit_scale: nil, - variant_unit_name: unit) - allow(v).to receive(:product) { p } - allow(p).to receive(:persisted?) { true } - allow(v).to receive(:unit_value) { 100 } - expect(subject.__send__(:option_value_value_unit)).to eq [100, unit.pluralize] + v = instance_double(Spree::Variant, variant_unit: 'items', variant_unit_scale: nil, + variant_unit_name: unit, unit_value: 100) + + option_value_namer = OptionValueNamer.new v + expect(option_value_namer.__send__(:option_value_value_unit)).to eq [100, unit.pluralize] end end it "generates singular values for item units when value is 1" do - p = double(:product, variant_unit: 'items', variant_unit_scale: nil, - variant_unit_name: 'packet') - allow(v).to receive(:product) { p } - allow(p).to receive(:persisted?) { true } - allow(v).to receive(:unit_value) { 1 } - expect(subject.__send__(:option_value_value_unit)).to eq [1, 'packet'] + v = instance_double(Spree::Variant, variant_unit: 'items', variant_unit_scale: nil, + variant_unit_name: 'packet', unit_value: 1) + + option_value_namer = OptionValueNamer.new v + expect(option_value_namer.__send__(:option_value_value_unit)).to eq [1, 'packet'] end it "returns [nil, nil] when unit value is not set" do - p = double(:product, variant_unit: 'items', variant_unit_scale: nil, - variant_unit_name: 'foo') - allow(v).to receive(:product) { p } - allow(v).to receive(:unit_value) { nil } - expect(subject.__send__(:option_value_value_unit)).to eq [nil, nil] + v = instance_double(Spree::Variant, variant_unit: 'items', variant_unit_scale: nil, + variant_unit_name: 'foo', unit_value: nil) + + option_value_namer = OptionValueNamer.new v + expect(option_value_namer.__send__(:option_value_value_unit)).to eq [nil, nil] end it "truncates value to 2 decimals maximum" do oz_scale = 28.35 - p = double(:product, variant_unit: 'weight', variant_unit_scale: oz_scale) - allow(v).to receive(:product) { p } - allow(p).to receive(:persisted?) { true } + v = instance_double(Spree::Variant, variant_unit: 'weight', variant_unit_scale: oz_scale, + unit_value: (12.5 * oz_scale).round(2)) + # The unit_value is stored rounded to 2 decimals - allow(v).to receive(:unit_value) { (12.5 * oz_scale).round(2) } - expect(subject.__send__(:option_value_value_unit)).to eq [BigDecimal(12.5, 6), 'oz'] + # allow(v).to receive(:unit_value) { (12.5 * oz_scale).round(2) } + option_value_namer = OptionValueNamer.new v + expect(option_value_namer.__send__(:option_value_value_unit)).to eq [BigDecimal(12.5, 6), + 'oz'] end end end From f58a3a859f2d552457f114d938ad577a2f5132c7 Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou Date: Wed, 26 Jun 2024 21:20:59 +1000 Subject: [PATCH 04/68] Move variant unit attributes to variant 1 Update Spree::Variant model and spec --- app/models/spree/variant.rb | 40 ++- .../variant_and_line_item_naming.rb | 4 +- config/locales/en.yml | 6 +- spec/factories/variant_factory.rb | 9 +- spec/models/spree/variant_spec.rb | 340 +++++++++++------- 5 files changed, 261 insertions(+), 138 deletions(-) diff --git a/app/models/spree/variant.rb b/app/models/spree/variant.rb index 4425c24523a..6ad1cf6d37b 100644 --- a/app/models/spree/variant.rb +++ b/app/models/spree/variant.rb @@ -71,21 +71,25 @@ class Variant < ApplicationRecord validates :tax_category, presence: true, if: proc { Spree::Config.products_require_tax_category } + validates :variant_unit, presence: true validates :unit_value, presence: true, if: ->(variant) { - %w(weight volume).include?(variant.product&.variant_unit) + %w(weight volume).include?(variant.variant_unit) } - validates :unit_value, numericality: { greater_than: 0 }, allow_blank: true - validates :price, numericality: { greater_than_or_equal_to: 0 } - validates :unit_description, presence: true, if: ->(variant) { - variant.product&.variant_unit.present? && variant.unit_value.nil? + variant.variant_unit.present? && variant.unit_value.nil? + } + validates :variant_unit_scale, presence: true, if: ->(variant) { + %w(weight volume).include?(variant.variant_unit) + } + validates :variant_unit_name, presence: true, if: ->(variant) { + variant.variant_unit == 'items' } before_validation :set_cost_currency before_validation :ensure_shipping_category before_validation :ensure_unit_value - before_validation :update_weight_from_unit_value, if: ->(v) { v.product.present? } + before_validation :update_weight_from_unit_value before_validation :convert_variant_weight_to_decimal before_save :assign_units, if: ->(variant) { @@ -95,6 +99,9 @@ class Variant < ApplicationRecord after_create :create_stock_items around_destroy :destruction after_save :save_default_price + after_save :update_units, if: -> { + saved_change_to_variant_unit? || saved_change_to_variant_unit_name? + } # default variant scope only lists non-deleted variants scope :deleted, -> { where.not(deleted_at: nil) } @@ -219,6 +226,23 @@ def total_on_hand Spree::Stock::Quantifier.new(self).total_on_hand end + # Format as per WeightsAndMeasures + # TODO test ? + def variant_unit_with_scale + scale_clean = ActiveSupport::NumberHelper.number_to_rounded(variant_unit_scale, + precision: nil, + strip_insignificant_zeros: true) + [variant_unit, scale_clean].compact_blank.join("_") + end + + def variant_unit_with_scale=(variant_unit_with_scale) + values = variant_unit_with_scale.split("_") + assign_attributes( + variant_unit: values[0], + variant_unit_scale: values[1] || nil + ) + end + private def check_currency @@ -248,7 +272,7 @@ def create_stock_items end def update_weight_from_unit_value - return unless product.variant_unit == 'weight' && unit_value.present? + return unless variant_unit == 'weight' && unit_value.present? self.weight = weight_from_unit_value end @@ -268,7 +292,7 @@ def destruction def ensure_unit_value Bugsnag.notify("Trying to set unit_value to NaN") if unit_value&.nan? - return unless (product&.variant_unit == "items" && unit_value.nil?) || unit_value&.nan? + return unless (variant_unit == "items" && unit_value.nil?) || unit_value&.nan? self.unit_value = 1.0 end diff --git a/app/services/variant_units/variant_and_line_item_naming.rb b/app/services/variant_units/variant_and_line_item_naming.rb index 9b074131554..b9d726b6691 100644 --- a/app/services/variant_units/variant_and_line_item_naming.rb +++ b/app/services/variant_units/variant_and_line_item_naming.rb @@ -64,12 +64,12 @@ def update_units def unit_value_attributes units = { unit_presentation: option_value_name } - units.merge!(variant_unit: product.variant_unit) if has_attribute?(:variant_unit) + units.merge!(variant_unit:) if has_attribute?(:variant_unit) units end def weight_from_unit_value - (unit_value || 0) / 1000 if product.variant_unit == 'weight' + (unit_value || 0) / 1000 if variant_unit == 'weight' end private diff --git a/config/locales/en.yml b/config/locales/en.yml index cba29dc7a3c..b962c1df4db 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -70,13 +70,13 @@ en: price: "Price" primary_taxon_id: "Product Category" shipping_category_id: "Shipping Category" - variant_unit: "Unit Scale" - variant_unit_name: "Variant Unit Name" - unit_value: "Unit value" spree/variant: primary_taxon: "Product Category" shipping_category_id: "Shipping Category" supplier: "Supplier" + variant_unit: "Unit Scale" + variant_unit_name: "Variant Unit Name" + unit_value: "Unit value" spree/credit_card: base: "Credit Card" number: "Number" diff --git a/spec/factories/variant_factory.rb b/spec/factories/variant_factory.rb index 4382af7e0af..1aed03f1833 100644 --- a/spec/factories/variant_factory.rb +++ b/spec/factories/variant_factory.rb @@ -10,6 +10,12 @@ height { generate(:random_float) } width { generate(:random_float) } depth { generate(:random_float) } + unit_value { 1 } + unit_description { '' } + + variant_unit { 'weight' } + variant_unit_scale { 1 } + variant_unit_name { '' } primary_taxon { Spree::Taxon.first || FactoryBot.create(:taxon) } supplier { Enterprise.is_primary_producer.first || FactoryBot.create(:supplier_enterprise) } @@ -31,9 +37,6 @@ on_hand { 5 } end - unit_value { 1 } - unit_description { '' } - after(:create) do |variant, evaluator| variant.on_demand = evaluator.on_demand variant.on_hand = evaluator.on_hand diff --git a/spec/models/spree/variant_spec.rb b/spec/models/spree/variant_spec.rb index 36860d1872a..b8f4828eb09 100644 --- a/spec/models/spree/variant_spec.rb +++ b/spec/models/spree/variant_spec.rb @@ -44,21 +44,164 @@ end end - # add test for the other validation - context "validations" do - it "should validate price is greater than 0" do - variant.price = -1 - expect(variant).not_to be_valid + describe "validations" do + describe "variant_unit" do + subject(:variant) { build(:variant) } + + it { is_expected.to validate_presence_of :variant_unit } + + context "when the product's unit is items" do + subject(:variant) { build(:variant, variant_unit: "items", variant_unit_name: "box") } + + it "is valid with only unit value set" do + variant.unit_value = 1 + variant.unit_description = nil + expect(variant).to be_valid + end + + it "is valid with only unit description set" do + variant.unit_value = nil + variant.unit_description = 'Medium' + expect(variant).to be_valid + end + + it "sets unit_value to 1.0 before validation if it's nil" do + variant.unit_value = nil + variant.unit_description = nil + expect(variant).to be_valid + expect(variant.unit_value).to eq 1.0 + end + end + + context "when the product's unit is non-weight" do + subject(:variant) { build(:variant, variant_unit: "volume") } + + it "sets weight to decimal before save if it's integer" do + variant.weight = 1 + variant.save! + expect(variant.weight).to eq 1.0 + end + + it "sets weight to 0.0 before save if it's nil" do + variant.weight = nil + variant.save! + expect(variant.weight).to eq 0.0 + end + + it "sets weight to 0.0 if input is a non numerical string" do + variant.weight = "BANANAS!" + variant.save! + expect(variant.weight).to eq 0.0 + end + + it "sets weight to correct decimal value if input is numerical string" do + variant.weight = "2" + variant.save! + expect(variant.weight).to eq 2.0 + end + end end - it "should validate price is 0" do - variant.price = 0 - expect(variant).to be_valid + describe "price" do + it { is_expected.to validate_presence_of :price } + + it "should validate price is greater than 0" do + variant.price = -1 + expect(variant).not_to be_valid + end + + it "should validate price is 0" do + variant.price = 0 + expect(variant).to be_valid + end + + it "should validate unit_value is greater than 0" do + variant.unit_value = 0 + + expect(variant).not_to be_valid + end end - it "should validate unit_value is greater than 0" do - variant.unit_value = 0 - expect(variant).not_to be_valid + describe "unit_value" do + subject(:variant) { build(:variant, variant_unit: "item", unit_value: "") } + + it { is_expected.not_to validate_presence_of(:unit_value) } + + %w(weight volume).each do |unit| + context "when variant_unit is #{unit}" do + subject(:variant) { build(:variant, variant_unit: unit) } + + it { is_expected.to validate_presence_of(:unit_value) } + it { is_expected.to validate_numericality_of(:unit_value).is_greater_than(0) } + end + end + end + + describe "unit_description" do + subject(:variant) { build(:variant) } + + it { expect(variant).to be_valid } + it { is_expected.not_to validate_presence_of(:unit_description) } + + context "when variant_unit is set and unit_value is nil" do + subject(:variant) { + build(:variant, variant_unit: "item", unit_value: nil, unit_description: "box") + } + + it { is_expected.to validate_presence_of(:unit_description) } + end + end + + describe "variant_unit_scale" do + subject(:variant) { build(:variant, variant_unit: "box") } + + it { is_expected.not_to validate_presence_of :variant_unit_scale } + + %w(weight volume).each do |unit| + context "when variant_unit is #{unit}" do + subject(:variant) { build(:variant, variant_unit: unit, variant_unit_scale: 1.0) } + + it { is_expected.to validate_presence_of :variant_unit_scale } + end + end + end + + describe "variant_unit_name" do + subject(:variant) { build(:variant) } + + it { is_expected.not_to validate_presence_of :variant_unit_name } + + context "when variant_unit is items" do + subject(:variant) { build(:variant, variant_unit: "items") } + + it { is_expected.to validate_presence_of :variant_unit_name } + end + end + + describe "variant_unit_scale" do + subject(:variant) { build(:variant, variant_unit: "box") } + + it { is_expected.not_to validate_presence_of :variant_unit_scale } + + %w(weight volume).each do |unit| + context "when variant_unit is #{unit}" do + subject(:variant) { build(:variant, variant_unit: unit, variant_unit_scale: 1.0) } + + it { is_expected.to validate_presence_of :variant_unit_scale } + end + end + end + + describe "variant_unit_name" do + subject(:variant) { build(:variant) } + + it { is_expected.not_to validate_presence_of :variant_unit_name } + + context "when variant_unit is items" do + subject(:variant) { build(:variant, variant_unit: "items") } + + it { is_expected.to validate_presence_of :variant_unit_name } + end end describe "tax category" do @@ -529,8 +672,8 @@ context "handling nil values for related naming attributes" do it "returns empty string or product name" do product.name = "Apple" - product.variant_unit = "items" product.display_as = nil + variant.variant_unit = "items" variant.display_as = nil variant.display_name = nil @@ -540,8 +683,8 @@ it "uses the display name correctly" do product.name = "Apple" - product.variant_unit = "items" product.display_as = nil + variant.variant_unit = "items" variant.display_as = nil variant.unit_presentation = nil variant.display_name = "Green" @@ -553,6 +696,7 @@ end describe "calculating the price with enterprise fees" do + # TODO use instance double it "returns the price plus the fees" do distributor = double(:distributor) order_cycle = double(:order_cycle) @@ -590,90 +734,6 @@ end end - context "when the product has variants" do - let!(:product) { create(:simple_product) } - let!(:variant) { create(:variant, product:) } - - %w(weight volume).each do |unit| - context "when the product's unit is #{unit}" do - before do - product.update_attribute :variant_unit, unit - product.reload - end - - it "is valid when unit value is set and unit description is not" do - variant.unit_value = 1 - variant.unit_description = nil - expect(variant).to be_valid - end - - it "is invalid when unit value is not set" do - variant.unit_value = nil - expect(variant).not_to be_valid - end - end - end - - context "when the product's unit is items" do - before do - product.update_attribute :variant_unit, 'items' - product.reload - variant.reload - end - - it "is valid with only unit value set" do - variant.unit_value = 1 - variant.unit_description = nil - expect(variant).to be_valid - end - - it "is valid with only unit description set" do - variant.unit_value = nil - variant.unit_description = 'Medium' - expect(variant).to be_valid - end - - it "sets unit_value to 1.0 before validation if it's nil" do - variant.unit_value = nil - variant.unit_description = nil - expect(variant).to be_valid - expect(variant.unit_value).to eq 1.0 - end - end - - context "when the product's unit is non-weight" do - before do - product.update_attribute :variant_unit, 'volume' - product.reload - variant.reload - end - - it "sets weight to decimal before save if it's integer" do - variant.weight = 1 - variant.save! - expect(variant.weight).to eq 1.0 - end - - it "sets weight to 0.0 before save if it's nil" do - variant.weight = nil - variant.save! - expect(variant.weight).to eq 0.0 - end - - it "sets weight to 0.0 if input is a non numerical string" do - variant.weight = "BANANAS!" - variant.save! - expect(variant.weight).to eq 0.0 - end - - it "sets weight to correct decimal value if input is numerical string" do - variant.weight = "2" - variant.save! - expect(variant.weight).to eq 2.0 - end - end - end - describe "unit value/description" do let(:v) { Spree::Variant.new(unit_presentation: "small" ) } @@ -739,42 +799,41 @@ describe "setting the variant's weight from the unit value" do it "sets the variant's weight when unit is weight" do - p = create(:simple_product, variant_unit: 'volume') - v = create(:variant, product: p, weight: 0) - - p.update! variant_unit: 'weight', variant_unit_scale: 1 - v.update! unit_value: 10, unit_description: 'foo' + v = create(:variant, weight: 0) + v.update!( + variant_unit: 'weight', variant_unit_scale: 1, unit_value: 10, unit_description: 'foo' + ) expect(v.reload.weight).to eq(0.01) end it "does nothing when unit is not weight" do - p = create(:simple_product, variant_unit: 'volume') - v = create(:variant, product: p, weight: 123) - - p.update! variant_unit: 'volume', variant_unit_scale: 1 - v.update! unit_value: 10, unit_description: 'foo' + v = create(:variant, weight: 123, variant_unit: 'volume') + v.update! variant_unit: 'volume', variant_unit_scale: 1, unit_value: 10, + unit_description: 'foo' expect(v.reload.weight).to eq(123) end it "does nothing when unit_value is not set" do - p = create(:simple_product, variant_unit: 'volume') - v = create(:variant, product: p, weight: 123) - - p.update! variant_unit: 'weight', variant_unit_scale: 1 + v = create(:variant, weight: 123, variant_unit: 'volume') # Although invalid, this calls the before_validation callback, which would # error if not handling unit_value == nil case - expect(v.update(unit_value: nil, unit_description: 'foo')).to be false + expect( + v.update(variant_unit: "weight", variant_unit_scale: 1, unit_value: nil, + unit_description: "foo") + ).to be false expect(v.reload.weight).to eq(123) end end context "when the variant already has a value set" do - let!(:p) { create(:simple_product, variant_unit: 'weight', variant_unit_scale: 1) } - let!(:v) { create(:variant, product: p, unit_value: 5, unit_description: 'bar') } + let!(:v) { + create(:variant, variant_unit: 'weight', variant_unit_scale: 1, unit_value: 5, + unit_description: 'bar') + } it "assigns the new option value" do expect(v.unit_presentation).to eq "5g bar" @@ -786,28 +845,30 @@ end context "when the variant does not have a display_as value set" do - let!(:p) { create(:simple_product, variant_unit: 'weight', variant_unit_scale: 1) } let!(:v) { - create(:variant, product: p, unit_value: 5, unit_description: 'bar', display_as: '') + create(:variant, variant_unit: 'weight', variant_unit_scale: 1, unit_value: 5, + unit_description: 'bar', display_as: '') } it "requests the new value from OptionValueName" do expect_any_instance_of(VariantUnits::OptionValueNamer) .to receive(:name).exactly(1).times.and_call_original v.update(unit_value: 10, unit_description: 'foo') + expect(v.unit_presentation).to eq "10g foo" end end context "when the variant has a display_as value set" do - let!(:p) { create(:simple_product, variant_unit: 'weight', variant_unit_scale: 1) } let!(:v) { - create(:variant, product: p, unit_value: 5, unit_description: 'bar', display_as: 'FOOS!') + create(:variant, variant_unit: 'weight', variant_unit_scale: 1, unit_value: 5, + unit_description: 'bar', display_as: 'FOOS!') } it "does not request the new value from OptionValueName" do expect_any_instance_of(VariantUnits::OptionValueNamer).not_to receive(:name) v.update!(unit_value: 10, unit_description: 'foo') + expect(v.unit_presentation).to eq("FOOS!") end end @@ -873,12 +934,11 @@ end describe "#ensure_unit_value" do - let(:product) { create(:product, variant_unit: "weight") } - let(:variant) { create(:variant, product_id: product.id) } + let(:variant) { create(:variant, variant_unit: "weight") } - context "when a product's variant_unit value is changed from weight to items" do + context "when variant_unit value is changed from weight to items" do it "sets the variant's unit_value to 1" do - product.update(variant_unit: "items") + variant.update(variant_unit: "items") expect(variant.unit_value).to eq 1 end @@ -908,4 +968,40 @@ end end end + + describe "after save callback" do + let(:variant) { create(:variant) } + + it "updates units and unit_presenation when saved change to variant unit" do + variant.variant_unit = 'items' + variant.variant_unit_scale = nil + variant.variant_unit_name = 'loaf' + variant.save! + + expect(variant.variant_unit_name).to eq 'loaf' + expect(variant.unit_presentation).to eq "1 loaf" + + variant.update(variant_unit_name: 'bag') + + expect(variant.variant_unit_name).to eq 'bag' + expect(variant.unit_presentation).to eq "1 bag" + + variant.variant_unit = 'weight' + variant.variant_unit_scale = 1 + variant.variant_unit_name = 'g' + variant.save! + + expect(variant.variant_unit).to eq 'weight' + expect(variant.unit_presentation).to eq "1g" + + variant.update(variant_unit: 'volume') + + expect(variant.variant_unit).to eq 'volume' + expect(variant.unit_presentation).to eq "1L" + + variant.update(display_as: 'My display') + + expect(variant.unit_presentation).to eq "My display" + end + end end From d0fe1585d7f65b9df40505a362e9139b7ccd9e87 Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou Date: Wed, 26 Jun 2024 21:50:48 +1000 Subject: [PATCH 05/68] Move variant unit attributes to variant 2 Update Spree::Product and spec --- app/models/spree/product.rb | 53 ++---------- app/models/spree/variant.rb | 5 +- spec/factories/product_factory.rb | 2 - spec/models/spree/product_spec.rb | 135 ++---------------------------- 4 files changed, 20 insertions(+), 175 deletions(-) diff --git a/app/models/spree/product.rb b/app/models/spree/product.rb index d4872b3e848..9232b781202 100755 --- a/app/models/spree/product.rb +++ b/app/models/spree/product.rb @@ -22,7 +22,7 @@ class Product < ApplicationRecord include LogDestroyPerformer self.belongs_to_required_by_default = false - self.ignored_columns += [:supplier_id] + self.ignored_columns += [:supplier_id, :variant_unit_scale, :variant_unit_name] acts_as_paranoid @@ -45,16 +45,6 @@ class Product < ApplicationRecord validates_lengths_from_database validates :name, presence: true - - validates :variant_unit, presence: true - validates :unit_value, numericality: { - greater_than: 0, - if: ->(p) { p.variant_unit.in?(%w(weight volume)) && new_record? } - } - validates :variant_unit_scale, - presence: { if: ->(p) { %w(weight volume).include? p.variant_unit } } - validates :variant_unit_name, - presence: { if: ->(p) { p.variant_unit == 'items' } } validate :validate_image validates :price, numericality: { greater_than_or_equal_to: 0, if: ->{ new_record? } } @@ -66,14 +56,14 @@ class Product < ApplicationRecord # Transient attributes used temporarily when creating a new product, # these values are persisted on the product's variant - attr_accessor :price, :display_as, :unit_value, :unit_description, :tax_category_id, - :shipping_category_id, :primary_taxon_id, :supplier_id + attr_accessor :price, :display_as, :unit_value, :unit_description, :variant_unit, + :variant_unit_name, :variant_unit_scale, :tax_category_id, :shipping_category_id, + :primary_taxon_id, :supplier_id after_validation :validate_variant_attrs, on: :create after_create :ensure_standard_variant after_update :touch_supplier, if: :saved_change_to_primary_taxon_id? around_destroy :destruction - after_save :update_units after_touch :touch_supplier # -- Scopes @@ -254,6 +244,9 @@ def ensure_standard_variant variant.display_as = display_as variant.unit_value = unit_value variant.unit_description = unit_description + variant.variant_unit = variant_unit + variant.variant_unit_name = variant_unit_name + variant.variant_unit_scale = variant_unit_scale variant.tax_category_id = tax_category_id variant.shipping_category_id = shipping_category_id variant.primary_taxon_id = primary_taxon_id @@ -261,25 +254,6 @@ def ensure_standard_variant variants << variant end - # Format as per WeightsAndMeasures (todo: re-orgnaise maybe after product/variant refactor) - def variant_unit_with_scale - # Our code is based upon English based number formatting with a period `.` - scale_clean = ActiveSupport::NumberHelper.number_to_rounded(variant_unit_scale, - precision: nil, - significant: false, - strip_insignificant_zeros: true, - locale: :en) - [variant_unit, scale_clean].compact_blank.join("_") - end - - def variant_unit_with_scale=(variant_unit_with_scale) - values = variant_unit_with_scale.split("_") - assign_attributes( - variant_unit: values[0], - variant_unit_scale: values[1] || nil - ) - end - # Remove any unsupported HTML. def description HtmlSanitizer.sanitize(super) @@ -301,18 +275,6 @@ def validate_variant_attrs errors.add(:supplier_id, :blank) unless Enterprise.find_by(id: supplier_id) end - def update_units - return unless saved_change_to_variant_unit? || saved_change_to_variant_unit_name? - - variants.each do |v| - if v.persisted? - v.update_units - else - v.assign_units - end - end - end - def touch_supplier return if variants.empty? @@ -324,6 +286,7 @@ def touch_supplier # importing product. In this scenario the variant has not been updated with the supplier yet # hence the check. first_variant.supplier.touch if first_variant.supplier.present? + end def validate_image diff --git a/app/models/spree/variant.rb b/app/models/spree/variant.rb index 6ad1cf6d37b..16a555498ef 100644 --- a/app/models/spree/variant.rb +++ b/app/models/spree/variant.rb @@ -229,9 +229,12 @@ def total_on_hand # Format as per WeightsAndMeasures # TODO test ? def variant_unit_with_scale + # Our code is based upon English based number formatting with a period `.` scale_clean = ActiveSupport::NumberHelper.number_to_rounded(variant_unit_scale, precision: nil, - strip_insignificant_zeros: true) + strip_insignificant_zeros: true, + locale: :en + ) [variant_unit, scale_clean].compact_blank.join("_") end diff --git a/spec/factories/product_factory.rb b/spec/factories/product_factory.rb index f2fab0e39f6..453a442be45 100644 --- a/spec/factories/product_factory.rb +++ b/spec/factories/product_factory.rb @@ -20,10 +20,8 @@ unit_value { 1 } unit_description { '' } - variant_unit { 'weight' } variant_unit_scale { 1 } - variant_unit_name { '' } # ensure stock item will be created for this products master before(:create) { DefaultStockLocation.find_or_create } diff --git a/spec/models/spree/product_spec.rb b/spec/models/spree/product_spec.rb index 385588538d3..130ac639a6e 100644 --- a/spec/models/spree/product_spec.rb +++ b/spec/models/spree/product_spec.rb @@ -123,27 +123,7 @@ module Spree it { is_expected.to validate_length_of(:name).is_at_most(255) } it { is_expected.to validate_length_of(:sku).is_at_most(255) } - context "unit value" do - it "requires a unit value when variant unit is weight" do - expect(build(:simple_product, variant_unit: 'weight', variant_unit_name: 'name', - unit_value: nil)).not_to be_valid - expect(build(:simple_product, variant_unit: 'weight', variant_unit_name: 'name', - unit_value: 0)).not_to be_valid - end - - it "requires a unit value when variant unit is volume" do - expect(build(:simple_product, variant_unit: 'volume', variant_unit_name: 'name', - unit_value: nil)).not_to be_valid - expect(build(:simple_product, variant_unit: 'volume', variant_unit_name: 'name', - unit_value: 0)).not_to be_valid - end - - it "does not require a unit value when variant unit is items" do - expect(build(:simple_product, variant_unit: 'items', variant_unit_name: 'name', - unit_value: nil)).to be_valid - end - end - +# context "when the product has variants" do let(:product) do product = create(:simple_product) @@ -151,30 +131,7 @@ module Spree product.reload end - it { is_expected.to validate_numericality_of(:price).is_greater_than_or_equal_to(0) } - - it "requires a unit" do - product.variant_unit = nil - expect(product).not_to be_valid - end - - %w(weight volume).each do |unit| - context "when unit is #{unit}" do - it "is valid when unit scale is set and unit name is not" do - product.variant_unit = unit - product.variant_unit_scale = 1 - product.variant_unit_name = nil - expect(product).to be_valid - end - - it "is invalid when unit scale is not set" do - product.variant_unit = unit - product.variant_unit_scale = nil - product.variant_unit_name = nil - expect(product).not_to be_valid - end - end - end + it { is_expected.to validate_numericality_of(:price).is_greater_than_or_equal_to(0) context "saving a new product" do let!(:product){ Spree::Product.new } @@ -189,6 +146,7 @@ module Spree product.variant_unit = "weight" product.variant_unit_scale = 1000 product.unit_value = 1 + product.unit_description = "some product" product.price = 4.27 product.shipping_category_id = shipping_category.id product.supplier_id = supplier.id @@ -198,49 +156,18 @@ module Spree it "copies properties to the first standard variant" do expect(product.variants.reload.length).to eq 1 standard_variant = product.variants.reload.first + expect(standard_variant).to be_valid + expect(standard_variant.variant_unit).to eq("weight") + expect(standard_variant.variant_unit_scale).to eq(1000) + expect(standard_variant.unit_value).to eq(1) + expect(standard_variant.unit_description).to eq("some product") expect(standard_variant.price).to eq 4.27 expect(standard_variant.shipping_category).to eq shipping_category expect(standard_variant.primary_taxon).to eq taxon expect(standard_variant.supplier).to eq supplier end end - - context "when the unit is items" do - it "is valid when unit name is set and unit scale is not" do - product.variant_unit = 'items' - product.variant_unit_name = 'loaf' - product.variant_unit_scale = nil - expect(product).to be_valid - end - - it "is invalid when unit name is not set" do - product.variant_unit = 'items' - product.variant_unit_name = nil - product.variant_unit_scale = nil - expect(product).not_to be_valid - end - end - end - - context "a basic product" do - let(:product) { build_stubbed(:simple_product) } - - it "requires variant unit fields" do - product.variant_unit = nil - product.variant_unit_name = nil - product.variant_unit_scale = nil - - expect(product).not_to be_valid - end - - it "requires a unit scale when variant unit is weight" do - product.variant_unit = 'weight' - product.variant_unit_scale = nil - product.variant_unit_name = nil - - expect(product).not_to be_valid - end end describe "#validate_image" do @@ -328,30 +255,6 @@ module Spree end end end - - it "updates units when saved change to variant unit" do - product.variant_unit = 'items' - product.variant_unit_scale = nil - product.variant_unit_name = 'loaf' - product.save! - - expect(product.variant_unit_name).to eq 'loaf' - - product.update(variant_unit_name: 'bag') - - expect(product.variant_unit_name).to eq 'bag' - - product.variant_unit = 'weight' - product.variant_unit_scale = 1 - product.variant_unit_name = 'g' - product.save! - - expect(product.variant_unit).to eq 'weight' - - product.update(variant_unit: 'volume') - - expect(product.variant_unit).to eq 'volume' - end end describe "scopes" do @@ -682,28 +585,6 @@ module Spree end end - describe "variant units" do - context "when the product already has a variant unit set" do - let!(:p) { - create(:simple_product, - variant_unit: 'weight', - variant_unit_scale: 1, - variant_unit_name: nil) - } - - it "updates its variants unit values" do - v = create(:variant, unit_value: 1, product: p) - p.reload - - expect(v.unit_presentation).to eq "1g" - - p.update!(variant_unit: 'volume', variant_unit_scale: 0.001) - - expect(v.reload.unit_presentation).to eq "1L" - end - end - end - describe "deletion" do let(:product) { create(:simple_product) } let(:variant) { create(:variant, product:) } From 1793aa3532cc622bb8c2bebd81ffa056b3e6d41a Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou Date: Mon, 19 Aug 2024 15:14:36 +1000 Subject: [PATCH 06/68] Migrate unit sizes to variant --- ...819045115_migrate_unit_size_to_variants.rb | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 db/migrate/20240819045115_migrate_unit_size_to_variants.rb diff --git a/db/migrate/20240819045115_migrate_unit_size_to_variants.rb b/db/migrate/20240819045115_migrate_unit_size_to_variants.rb new file mode 100644 index 00000000000..4747d849397 --- /dev/null +++ b/db/migrate/20240819045115_migrate_unit_size_to_variants.rb @@ -0,0 +1,21 @@ +class MigrateUnitSizeToVariants < ActiveRecord::Migration[7.0] + def up + # Copy variant_unit only if it's empty in the variant + ActiveRecord::Base.connection.execute(<<-SQL + UPDATE spree_variants + SET variant_unit = spree_products.variant_unit + FROM spree_products + WHERE spree_variants.product_id = spree_products.id + AND spree_variants.variant_unit IS NULL + SQL + ) + + ActiveRecord::Base.connection.execute(<<-SQL + UPDATE spree_variants + SET variant_unit_scale = spree_products.variant_unit_scale, variant_unit_name = spree_products.variant_unit_name + FROM spree_products + WHERE spree_variants.product_id = spree_products.id + SQL + ) + end +end From 1ad7123a9dc9a90f2dc7a3841f100f4336f8c1a5 Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou Date: Sun, 30 Jun 2024 14:35:24 +1000 Subject: [PATCH 07/68] Fix Spree::LineItem --- app/models/spree/line_item.rb | 3 ++- spec/models/spree/line_item_spec.rb | 22 ++++++++++++++-------- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/app/models/spree/line_item.rb b/app/models/spree/line_item.rb index 23fed5fbc60..e70834839bd 100644 --- a/app/models/spree/line_item.rb +++ b/app/models/spree/line_item.rb @@ -45,7 +45,8 @@ class LineItem < ApplicationRecord after_destroy :update_order after_save :update_order - delegate :product, :variant_unit, :unit_description, :display_name, :display_as, to: :variant + delegate :product, :variant_unit, :unit_description, :display_name, :display_as, + :variant_unit_scale, :variant_unit_name, to: :variant # Allows manual skipping of Stock::AvailabilityValidator attr_accessor :skip_stock_check, :target_shipment diff --git a/spec/models/spree/line_item_spec.rb b/spec/models/spree/line_item_spec.rb index b6fecf1f33a..37aeaa6fc1b 100644 --- a/spec/models/spree/line_item_spec.rb +++ b/spec/models/spree/line_item_spec.rb @@ -699,11 +699,11 @@ module Spree describe "getting unit for display" do let(:o) { create(:order) } - let(:p1) { create(:product, name: 'Clear Honey', variant_unit_scale: 1) } - let(:v1) { create(:variant, product: p1, unit_value: 500) } + let(:p1) { create(:product, name: 'Clear Honey') } + let(:v1) { create(:variant, product: p1, variant_unit_scale: 1, unit_value: 500) } let(:li1) { create(:line_item, order: o, product: p1, variant: v1) } - let(:p2) { create(:product, name: 'Clear United States Honey', variant_unit_scale: 453.6) } - let(:v2) { create(:variant, product: p2, unit_value: 453.6) } + let(:p2) { create(:product, name: 'Clear United States Honey') } + let(:v2) { create(:variant, product: p2, variant_unit_scale: 453.6, unit_value: 453.6) } let(:li2) { create(:line_item, order: o, product: p2, variant: v2) } before do @@ -723,8 +723,11 @@ module Spree end context "when the line_item has a final_weight_volume set" do - let!(:p0) { create(:simple_product, variant_unit: 'weight', variant_unit_scale: 1) } - let!(:v) { create(:variant, product: p0, unit_value: 10, unit_description: 'bar') } + let!(:p0) { create(:simple_product) } + let!(:v) { + create(:variant, product: p0, variant_unit: 'weight', variant_unit_scale: 1, + unit_value: 10, unit_description: 'bar') + } let!(:p) { create(:simple_product, variant_unit: 'weight', variant_unit_scale: 1) } let!(:li) { create(:line_item, product: p, final_weight_volume: 5) } @@ -742,8 +745,11 @@ module Spree end context "when the variant already has a value set" do - let!(:p0) { create(:simple_product, variant_unit: 'weight', variant_unit_scale: 1) } - let!(:v) { create(:variant, product: p0, unit_value: 10, unit_description: 'bar') } + let!(:p0) { create(:simple_product) } + let!(:v) { + create(:variant, product: p0, variant_unit: 'weight', variant_unit_scale: 1, + unit_value: 10, unit_description: 'bar') + } let!(:p) { create(:simple_product, variant_unit: 'weight', variant_unit_scale: 1) } let!(:li) { create(:line_item, product: p, final_weight_volume: 5) } From e2c762f06be1f6102cef4f53dc1c596466d48d9a Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou Date: Sun, 30 Jun 2024 14:54:14 +1000 Subject: [PATCH 08/68] Refactor, use instance_double in variant spec --- spec/models/spree/variant_spec.rb | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/spec/models/spree/variant_spec.rb b/spec/models/spree/variant_spec.rb index b8f4828eb09..7f3d1559ef7 100644 --- a/spec/models/spree/variant_spec.rb +++ b/spec/models/spree/variant_spec.rb @@ -696,10 +696,9 @@ end describe "calculating the price with enterprise fees" do - # TODO use instance double it "returns the price plus the fees" do - distributor = double(:distributor) - order_cycle = double(:order_cycle) + distributor = instance_double(Enterprise) + order_cycle = instance_double(OrderCycle) variant = Spree::Variant.new price: 100 expect(variant).to receive(:fees_for).with(distributor, order_cycle) { 23 } @@ -709,8 +708,8 @@ describe "calculating the fees" do it "delegates to EnterpriseFeeCalculator" do - distributor = double(:distributor) - order_cycle = double(:order_cycle) + distributor = instance_double(Enterprise) + order_cycle = instance_double(OrderCycle) variant = Spree::Variant.new expect_any_instance_of(OpenFoodNetwork::EnterpriseFeeCalculator) @@ -722,10 +721,10 @@ describe "calculating fees broken down by fee type" do it "delegates to EnterpriseFeeCalculator" do - distributor = double(:distributor) - order_cycle = double(:order_cycle) + distributor = instance_double(Enterprise) + order_cycle = instance_double(OrderCycle) variant = Spree::Variant.new - fees = double(:fees) + fees = instance_double(EnterpriseFee) expect_any_instance_of(OpenFoodNetwork::EnterpriseFeeCalculator) .to receive(:fees_by_type_for).with(variant) { fees } From d7d253e58d72d10c557c2fefec98a27c3b0d3316 Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou Date: Mon, 1 Jul 2024 13:00:30 +1000 Subject: [PATCH 09/68] Fix Unit Price service --- app/services/unit_price.rb | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/app/services/unit_price.rb b/app/services/unit_price.rb index 0b28c973d55..004ec68332a 100644 --- a/app/services/unit_price.rb +++ b/app/services/unit_price.rb @@ -3,12 +3,11 @@ class UnitPrice def initialize(variant) @variant = variant - @product = variant.product end def denominator # catches any case where unit is not kg, lb, or L. - return @variant.unit_value if @product&.variant_unit == "items" + return @variant.unit_value if @variant.variant_unit == "items" case unit when "lb" @@ -23,13 +22,13 @@ def denominator def unit return "lb" if WeightsAndMeasures.new(@variant).system == "imperial" - case @product&.variant_unit + case @variant.variant_unit when "weight" "kg" when "volume" "L" else - @product.variant_unit_name.presence || I18n.t("item") + @variant.variant_unit_name.presence || I18n.t("item") end end end From e22804712e60945f92ec12797d4821c29ecb5f1f Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou Date: Tue, 2 Jul 2024 10:48:34 +1000 Subject: [PATCH 10/68] Fix product importer --- app/models/product_import/entry_processor.rb | 3 ++ app/models/product_import/entry_validator.rb | 45 ++++++++++++++------ spec/models/product_importer_spec.rb | 29 +++++++------ 3 files changed, 52 insertions(+), 25 deletions(-) diff --git a/app/models/product_import/entry_processor.rb b/app/models/product_import/entry_processor.rb index c7596cc1ae3..6393f14578c 100644 --- a/app/models/product_import/entry_processor.rb +++ b/app/models/product_import/entry_processor.rb @@ -224,6 +224,9 @@ def ensure_variant_updated(product, entry) # Ensure attributes are correctly copied to a new product's variant variant = product.variants.first variant.display_name = entry.display_name if entry.display_name + variant.variant_unit = entry.variant_unit if entry.variant_unit + variant.variant_unit_name = entry.variant_unit_name if entry.variant_unit_name + variant.variant_unit_scale = entry.variant_unit_scale if entry.variant_unit_scale variant.import_date = @import_time variant.supplier_id = entry.producer_id variant.save diff --git a/app/models/product_import/entry_validator.rb b/app/models/product_import/entry_validator.rb index 4db5a60d2c1..26233164000 100644 --- a/app/models/product_import/entry_validator.rb +++ b/app/models/product_import/entry_validator.rb @@ -22,9 +22,14 @@ def initialize(current_user, import_time, spreadsheet_data, editable_enterprises end # rubocop:enable Metrics/ParameterLists - def self.non_updatable_fields + def self.non_updatable_product_fields { description: :description, + } + end + + def self.non_updatable_variant_fields + { unit_type: :variant_unit_scale, variant_unit_name: :variant_unit_name, } @@ -67,8 +72,7 @@ def enterprise_field def mark_as_new_variant(entry, product_id) variant_attributes = entry.assignable_attributes.except( - 'id', 'product_id', 'on_hand', 'on_demand', 'variant_unit', 'variant_unit_name', - 'variant_unit_scale' + 'id', 'product_id', 'on_hand', 'on_demand' ) # Variant needs a product. Product needs to be assigned first in order for # delegate to work. name= will fail otherwise. @@ -297,11 +301,11 @@ def inventory_validation(entry) end products.flat_map(&:variants).each do |existing_variant| - unit_scale = existing_variant.product.variant_unit_scale + unit_scale = existing_variant.variant_unit_scale unscaled_units = entry.unscaled_units.to_f || 0 entry.unit_value = unscaled_units * unit_scale unless unit_scale.nil? - if entry_matches_existing_variant?(entry, existing_variant) + if inventory_entry_matches_existing_variant?(entry, existing_variant) variant_override = create_inventory_item(entry, existing_variant) return validate_inventory_item(entry, variant_override) end @@ -312,6 +316,12 @@ def inventory_validation(entry) end def entry_matches_existing_variant?(entry, existing_variant) + # matching on the unscaled unit so we know if we are trying to update an existing variant + display_name_are_the_same?(entry, existing_variant) && + existing_variant.unit_value == entry.unscaled_units.to_f + end + + def inventory_entry_matches_existing_variant?(entry, existing_variant) display_name_are_the_same?(entry, existing_variant) && existing_variant.unit_value == entry.unit_value.to_f end @@ -367,10 +377,12 @@ def product_validation(entry) products.each { |product| product_field_errors(entry, product) } products.flat_map(&:variants).each do |existing_variant| - if entry_matches_existing_variant?(entry, existing_variant) && - existing_variant.deleted_at.nil? - return mark_as_existing_variant(entry, existing_variant) - end + next unless entry_matches_existing_variant?(entry, existing_variant) && + existing_variant.deleted_at.nil? + + variant_field_errors(entry, existing_variant) + + return mark_as_existing_variant(entry, existing_variant) end mark_as_new_variant(entry, products.first.id) @@ -392,8 +404,7 @@ def mark_as_new_product(entry) def mark_as_existing_variant(entry, existing_variant) existing_variant.assign_attributes( - entry.assignable_attributes.except('id', 'product_id', 'variant_unit', 'variant_unit_name', - 'variant_unit_scale') + entry.assignable_attributes.except('id', 'product_id') ) check_on_hand_nil(entry, existing_variant) @@ -406,8 +417,18 @@ def mark_as_existing_variant(entry, existing_variant) end end + def variant_field_errors(entry, existing_variant) + EntryValidator.non_updatable_variant_fields.each do |display_name, attribute| + next if attributes_match?(attribute, existing_variant, entry) || + attributes_blank?(attribute, existing_variant, entry) + + mark_as_invalid(entry, attribute: display_name, + error: I18n.t('admin.product_import.model.not_updatable')) + end + end + def product_field_errors(entry, existing_product) - EntryValidator.non_updatable_fields.each do |display_name, attribute| + EntryValidator.non_updatable_product_fields.each do |display_name, attribute| next if attributes_match?(attribute, existing_product, entry) || attributes_blank?(attribute, existing_product, entry) next if ignore_when_updating_product?(attribute) diff --git a/spec/models/product_importer_spec.rb b/spec/models/product_importer_spec.rb index 4dc3df2199e..d514193bbf6 100644 --- a/spec/models/product_importer_spec.rb +++ b/spec/models/product_importer_spec.rb @@ -160,60 +160,60 @@ carrots = Spree::Product.find_by(name: 'Carrots') carrots_variant = carrots.variants.first expect(carrots.on_hand).to eq 5 - expect(carrots.variant_unit).to eq 'weight' - expect(carrots.variant_unit_scale).to eq 1 expect(carrots_variant.supplier).to eq enterprise expect(carrots_variant.price).to eq 3.20 expect(carrots_variant.unit_value).to eq 500 + expect(carrots_variant_unit).to eq 'weight' + expect(carrots_variant_unit_scale).to eq 1 expect(carrots_variant.on_demand).not_to eq true expect(carrots_variant.import_date).to be_within(1.minute).of Time.zone.now potatoes = Spree::Product.find_by(name: 'Potatoes') potatoes_variant = potatoes.variants.first expect(potatoes.on_hand).to eq 6 - expect(potatoes.variant_unit).to eq 'weight' - expect(potatoes.variant_unit_scale).to eq 1000 expect(potatoes_variant.supplier).to eq enterprise expect(potatoes_variant.price).to eq 6.50 expect(potatoes_variant.unit_value).to eq 2000 + expect(potatoes_variant_unit).to eq 'weight' + expect(potatoes_variant_unit_scale).to eq 1000 expect(potatoes_variant.on_demand).not_to eq true expect(potatoes_variant.import_date).to be_within(1.minute).of Time.zone.now pea_soup = Spree::Product.find_by(name: 'Pea Soup') pea_soup_variant = pea_soup.variants.first expect(pea_soup.on_hand).to eq 8 - expect(pea_soup.variant_unit).to eq 'volume' - expect(pea_soup.variant_unit_scale).to eq 0.001 expect(pea_soup_variant.supplier).to eq enterprise expect(pea_soup_variant.price).to eq 5.50 expect(pea_soup_variant.unit_value).to eq 0.75 + expect(pea_soup_variant_unit).to eq 'volume' + expect(pea_soup_variant_unit_scale).to eq 0.001 expect(pea_soup_variant.on_demand).not_to eq true expect(pea_soup_variant.import_date).to be_within(1.minute).of Time.zone.now salad = Spree::Product.find_by(name: 'Salad') salad_variant = salad.variants.first expect(salad.on_hand).to eq 7 - expect(salad.variant_unit).to eq 'items' - expect(salad.variant_unit_scale).to eq nil expect(salad_variant.supplier).to eq enterprise expect(salad_variant.price).to eq 4.50 expect(salad_variant.unit_value).to eq 1 + expect(salad_variant_unit).to eq 'items' + expect(salad_variant_unit_scale).to eq nil expect(salad_variant.on_demand).not_to eq true expect(salad_variant.import_date).to be_within(1.minute).of Time.zone.now buns = Spree::Product.find_by(name: 'Hot Cross Buns') buns_variant = buns.variants.first expect(buns.on_hand).to eq 7 - expect(buns.variant_unit).to eq 'items' - expect(buns.variant_unit_scale).to eq nil expect(buns_variant.supplier).to eq enterprise expect(buns_variant.price).to eq 3.50 expect(buns_variant.unit_value).to eq 1 + expect(buns_variant_unit).to eq 'items' + expect(buns_variant_unit_scale).to eq nil expect(buns_variant.on_demand).to eq true expect(buns_variant.import_date).to be_within(1.minute).of Time.zone.now end @@ -578,9 +578,12 @@ describe "updating non-updatable fields on existing products" do let(:csv_data) { CSV.generate do |csv| - csv << ["name", "producer", "category", "on_hand", "price", "units", "unit_type"] - csv << ["Beetroot", enterprise3.name, "Vegetables", "5", "3.50", "500", "Kg"] - csv << ["Tomato", enterprise3.name, "Vegetables", "6", "5.50", "500", "Kg"] + csv << ["name", "producer", "category", "on_hand", "price", "units", "unit_type", + "shipping_category"] + csv << ["Beetroot", enterprise3.name, "Vegetables", "5", "3.50", "500", "Kg", + shipping_category.name] + csv << ["Tomato", enterprise3.name, "Vegetables", "6", "5.50", "500", "Kg", + shipping_category.name] end } let(:importer) { import_data csv_data } From 4fd115897a76c1801d15b4f80ed111a04e12dee2 Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou Date: Tue, 2 Jul 2024 13:32:20 +1000 Subject: [PATCH 11/68] Refactor ProductImport::EntryValidator Move comparaison function to ProductImport::SpreadsheetEntry --- app/models/product_import/entry_validator.rb | 21 +---- .../product_import/spreadsheet_entry.rb | 14 +++ .../product_import/spreadsheet_entry_spec.rb | 91 +++++++++++++++++++ 3 files changed, 107 insertions(+), 19 deletions(-) create mode 100644 spec/models/product_import/spreadsheet_entry_spec.rb diff --git a/app/models/product_import/entry_validator.rb b/app/models/product_import/entry_validator.rb index 26233164000..532f0281bf7 100644 --- a/app/models/product_import/entry_validator.rb +++ b/app/models/product_import/entry_validator.rb @@ -305,7 +305,7 @@ def inventory_validation(entry) unscaled_units = entry.unscaled_units.to_f || 0 entry.unit_value = unscaled_units * unit_scale unless unit_scale.nil? - if inventory_entry_matches_existing_variant?(entry, existing_variant) + if entry.match_inventory_variant?(existing_variant) variant_override = create_inventory_item(entry, existing_variant) return validate_inventory_item(entry, variant_override) end @@ -315,23 +315,6 @@ def inventory_validation(entry) error: I18n.t('admin.product_import.model.not_found')) end - def entry_matches_existing_variant?(entry, existing_variant) - # matching on the unscaled unit so we know if we are trying to update an existing variant - display_name_are_the_same?(entry, existing_variant) && - existing_variant.unit_value == entry.unscaled_units.to_f - end - - def inventory_entry_matches_existing_variant?(entry, existing_variant) - display_name_are_the_same?(entry, existing_variant) && - existing_variant.unit_value == entry.unit_value.to_f - end - - def display_name_are_the_same?(entry, existing_variant) - return true if entry.display_name.blank? && existing_variant.display_name.blank? - - existing_variant.display_name == entry.display_name - end - def category_validation(entry) category_name = entry.category @@ -377,7 +360,7 @@ def product_validation(entry) products.each { |product| product_field_errors(entry, product) } products.flat_map(&:variants).each do |existing_variant| - next unless entry_matches_existing_variant?(entry, existing_variant) && + next unless entry.match_variant?(existing_variant) && existing_variant.deleted_at.nil? variant_field_errors(entry, existing_variant) diff --git a/app/models/product_import/spreadsheet_entry.rb b/app/models/product_import/spreadsheet_entry.rb index a46d061ee76..23743e6f90f 100644 --- a/app/models/product_import/spreadsheet_entry.rb +++ b/app/models/product_import/spreadsheet_entry.rb @@ -84,6 +84,14 @@ def invalid_attributes invalid_attrs.except(* NON_PRODUCT_ATTRIBUTES, *NON_DISPLAY_ATTRIBUTES) end + def match_variant?(variant) + match_display_name?(variant) && variant.unit_value.to_d == unscaled_units.to_d + end + + def match_inventory_variant?(variant) + match_display_name?(variant) && variant.unit_value.to_d == unit_value.to_d + end + private def remove_empty_skus(attrs) @@ -99,5 +107,11 @@ def assign_units(attrs) end end end + + def match_display_name?(variant) + return true if display_name.blank? && variant.display_name.blank? + + variant.display_name == display_name + end end end diff --git a/spec/models/product_import/spreadsheet_entry_spec.rb b/spec/models/product_import/spreadsheet_entry_spec.rb new file mode 100644 index 00000000000..f2e98c86c0e --- /dev/null +++ b/spec/models/product_import/spreadsheet_entry_spec.rb @@ -0,0 +1,91 @@ +# frozen_string_literal: false + +require 'spec_helper' +RSpec.describe ProductImport::SpreadsheetEntry do + let(:enterprise) { create(:enterprise) } + let(:entry) { + ProductImport::SpreadsheetEntry.new( + "units" => "500", + "unit_type" => "kg", + "name" => "Tomato", + "enterprise" => enterprise, + "enterprise_id" => enterprise.id, + "producer" => enterprise, + "producer_id" => enterprise.id, + "distributor" => enterprise, + "price" => "1.0", + "on_hand" => "1", + "display_name" => display_name, + ) + } + let(:display_name) { "" } + + # TODO test match on display_name + describe "#match_variant?" do + it "returns true if matching" do + variant = create(:variant, unit_value: 500) + + expect(entry.match_variant?(variant)).to be(true) + end + + it "returns false if not machting" do + variant = create(:variant, unit_value: 250) + + expect(entry.match_variant?(variant)).to be(false) + end + + context "with same display_name" do + let(:display_name) { "Good" } + + it "returns true" do + variant = create(:variant, unit_value: 500, display_name: "Good") + + expect(entry.match_variant?(variant)).to be(true) + end + end + + context "with different display_name" do + let(:display_name) { "Bad" } + + it "returns false" do + variant = create(:variant, unit_value: 500, display_name: "Good") + + expect(entry.match_variant?(variant)).to be(false) + end + end + end + + describe "#match_inventory_variant?" do + it "returns true if matching" do + variant = create(:variant, unit_value: 500_000) + + expect(entry.match_inventory_variant?(variant)).to be(true) + end + + it "returns false if not machting" do + variant = create(:variant, unit_value: 500) + + expect(entry.match_inventory_variant?(variant)).to be(false) + end + + context "with same display_name" do + let(:display_name) { "Good" } + + it "returns true" do + variant = create(:variant, unit_value: 500_000, display_name: "Good") + + expect(entry.match_inventory_variant?(variant)).to be(true) + end + end + + context "with different display_name" do + let(:display_name) { "Bad" } + + it "returns false" do + variant = create(:variant, unit_value: 500_000, display_name: "Good") + + expect(entry.match_inventory_variant?(variant)).to be(false) + end + end + end +end From 37ae217afc377504284b6e9588e2525463ba5d52 Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou Date: Tue, 2 Jul 2024 13:56:48 +1000 Subject: [PATCH 12/68] Fix product set spec --- spec/services/sets/product_set_spec.rb | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/spec/services/sets/product_set_spec.rb b/spec/services/sets/product_set_spec.rb index 35b2d340677..d415f7096de 100644 --- a/spec/services/sets/product_set_spec.rb +++ b/spec/services/sets/product_set_spec.rb @@ -69,22 +69,27 @@ unit_description: 'some description' ) end + let(:variant) { product.variants.first } let(:collection_hash) do { 0 => { id: product.id, - variant_unit: 'weight', - variant_unit_scale: 1 + variants_attributes: [{ + id: variant.id.to_s, + variant_unit: 'weight', + variant_unit_scale: 1 + }] } } end it 'updates the product without error' do expect(product_set.save).to eq true - expect(product_set.saved_count).to eq 1 + # updating variant doesn't increment saved_count + # expect(product_set.saved_count).to eq 1 - expect(product.reload.attributes).to include( + expect(variant.reload.attributes).to include( 'variant_unit' => 'weight' ) @@ -305,8 +310,8 @@ { id: product.variants.first.id.to_s }, # default variant unchanged # omit ID for new variant { - sku: "new sku", price: "5.00", unit_value: "5", - supplier_id: supplier.id, primary_taxon_id: create(:taxon).id + sku: "new sku", price: "5.00", unit_value: "5", variant_unit: "weight", + variant_unit_scale: 1, supplier_id: supplier.id, primary_taxon_id: create(:taxon).id }, ] } @@ -318,9 +323,12 @@ expect(product_set.errors).to be_empty }.to change { product.variants.count }.by(1) - expect(product.variants.last.sku).to eq "new sku" - expect(product.variants.last.price).to eq 5.00 - expect(product.variants.last.unit_value).to eq 5 + variant = product.variants.last + expect(variant.sku).to eq "new sku" + expect(variant.price).to eq 5.00 + expect(variant.unit_value).to eq 5 + expect(variant.variant_unit).to eq "weight" + expect(variant.variant_unit_scale).to eq 1 end context "variant has error" do From 4109fbde7002c872fd34aa8f20665c19f3468e67 Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou Date: Tue, 2 Jul 2024 15:28:31 +1000 Subject: [PATCH 13/68] Fix variant controller spec --- lib/open_food_network/scope_variants_for_search.rb | 2 +- spec/controllers/spree/admin/variants_controller_spec.rb | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/open_food_network/scope_variants_for_search.rb b/lib/open_food_network/scope_variants_for_search.rb index e1540965249..7c51cd8b613 100644 --- a/lib/open_food_network/scope_variants_for_search.rb +++ b/lib/open_food_network/scope_variants_for_search.rb @@ -36,7 +36,7 @@ def query_scope Spree::Variant. ransack(search_params.merge(m: 'or')). result. - order("spree_products.name, display_name, display_as, spree_products.variant_unit_name"). + order("spree_products.name, display_name, display_as, spree_variants.variant_unit_name"). includes(:product). joins(:product) end diff --git a/spec/controllers/spree/admin/variants_controller_spec.rb b/spec/controllers/spree/admin/variants_controller_spec.rb index ba6e1542e5b..a443e4d4491 100644 --- a/spec/controllers/spree/admin/variants_controller_spec.rb +++ b/spec/controllers/spree/admin/variants_controller_spec.rb @@ -12,8 +12,8 @@ module Admin let(:product) { create(:product, name: 'Product A') } let(:deleted_variant) do deleted_variant = product.variants.create( - unit_value: "2", price: 1, primary_taxon: create(:taxon), - supplier: create(:supplier_enterprise) + unit_value: "2", variant_unit: "weight", variant_unit_scale: 1, price: 1, + primary_taxon: create(:taxon), supplier: create(:supplier_enterprise) ) deleted_variant.delete deleted_variant From 8a31153d6dc5678da993c63b2db2c2f8584811fc Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou Date: Tue, 2 Jul 2024 15:45:02 +1000 Subject: [PATCH 14/68] Fix API v0 products controller spec --- spec/controllers/api/v0/products_controller_spec.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/controllers/api/v0/products_controller_spec.rb b/spec/controllers/api/v0/products_controller_spec.rb index 3633dab6ea7..1a6030240cc 100644 --- a/spec/controllers/api/v0/products_controller_spec.rb +++ b/spec/controllers/api/v0/products_controller_spec.rb @@ -35,8 +35,8 @@ it "gets a single product" do product.create_image!(attachment:) - product.variants.create!(unit_value: "1", unit_description: "thing", price: 1, - primary_taxon: taxon, supplier:) + product.variants.create!(unit_value: "1", variant_unit: "weight", variant_unit_scale: 1, + unit_description: "thing", price: 1, primary_taxon: taxon, supplier:) product.variants.first.images.create!(attachment:) product.set_property("spree", "rocks") @@ -121,7 +121,7 @@ expect(json_response["error"]).to eq("Invalid resource. Please fix errors and try again.") errors = json_response["errors"] expect(errors.keys).to match_array([ - "name", "variant_unit", "price", + "name", "price", "primary_taxon_id", "supplier_id" ]) end From 5ec39f994a415a20b7a2bbc1632680fb2f46bd71 Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou Date: Tue, 2 Jul 2024 15:45:46 +1000 Subject: [PATCH 15/68] Fix spree admin products controller spec --- app/services/permitted_attributes/product.rb | 2 -- app/services/permitted_attributes/variant.rb | 9 ++++----- spec/controllers/spree/admin/products_controller_spec.rb | 2 ++ 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/app/services/permitted_attributes/product.rb b/app/services/permitted_attributes/product.rb index 716549724d3..58600e5f267 100644 --- a/app/services/permitted_attributes/product.rb +++ b/app/services/permitted_attributes/product.rb @@ -5,8 +5,6 @@ class Product def self.attributes [ :id, :name, :description, :price, - :variant_unit, :variant_unit_scale, :variant_unit_with_scale, :unit_value, - :unit_description, :variant_unit_name, :display_as, :sku, :group_buy, :group_buy_unit_size, :taxon_ids, :primary_taxon_id, :tax_category_id, :supplier_id, :meta_keywords, :notes, :inherits_properties, :shipping_category_id, diff --git a/app/services/permitted_attributes/variant.rb b/app/services/permitted_attributes/variant.rb index 4d4fdcea38e..9d20975cb0e 100644 --- a/app/services/permitted_attributes/variant.rb +++ b/app/services/permitted_attributes/variant.rb @@ -4,11 +4,10 @@ module PermittedAttributes class Variant def self.attributes [ - :id, :sku, :on_hand, :on_demand, :shipping_category_id, - :price, :unit_value, :unit_description, - :display_name, :display_as, :tax_category_id, - :weight, :height, :width, :depth, :taxon_ids, :primary_taxon_id, - :supplier_id + :id, :sku, :on_hand, :on_demand, :shipping_category_id, :price, :unit_value, + :unit_description, :variant_unit, :variant_unit_name, :variant_unit_scale, :display_name, + :display_as, :tax_category_id, :weight, :height, :width, :depth, :taxon_ids, + :primary_taxon_id, :supplier_id ] end end diff --git a/spec/controllers/spree/admin/products_controller_spec.rb b/spec/controllers/spree/admin/products_controller_spec.rb index f7f81f2cdc0..deae56bd80d 100644 --- a/spec/controllers/spree/admin/products_controller_spec.rb +++ b/spec/controllers/spree/admin/products_controller_spec.rb @@ -111,6 +111,8 @@ "on_hand" => 2, "price" => "5.0", "unit_value" => 4, + "variant_unit" => "weight", + "variant_unit_scale" => "1", "unit_description" => "", "display_name" => "name", "primary_taxon_id" => taxon.id, From df82dd0759f1aa900ff350e4083ea7e8770cede3 Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou Date: Tue, 2 Jul 2024 15:53:10 +1000 Subject: [PATCH 16/68] Fix API v0 variants controller spec --- app/services/permitted_attributes/product.rb | 2 ++ spec/controllers/api/v0/variants_controller_spec.rb | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/app/services/permitted_attributes/product.rb b/app/services/permitted_attributes/product.rb index 58600e5f267..716549724d3 100644 --- a/app/services/permitted_attributes/product.rb +++ b/app/services/permitted_attributes/product.rb @@ -5,6 +5,8 @@ class Product def self.attributes [ :id, :name, :description, :price, + :variant_unit, :variant_unit_scale, :variant_unit_with_scale, :unit_value, + :unit_description, :variant_unit_name, :display_as, :sku, :group_buy, :group_buy_unit_size, :taxon_ids, :primary_taxon_id, :tax_category_id, :supplier_id, :meta_keywords, :notes, :inherits_properties, :shipping_category_id, diff --git a/spec/controllers/api/v0/variants_controller_spec.rb b/spec/controllers/api/v0/variants_controller_spec.rb index ce9f4865126..ec200f13892 100644 --- a/spec/controllers/api/v0/variants_controller_spec.rb +++ b/spec/controllers/api/v0/variants_controller_spec.rb @@ -144,9 +144,9 @@ it "can create a new variant" do original_number_of_variants = variant.product.variants.count - api_post :create, variant: { sku: "12345", unit_value: "1", unit_description: "L", - price: "1", primary_taxon_id: taxon.id, - supplier_id: variant.supplier.id }, + api_post :create, variant: { sku: "12345", unit_value: "1", variant_unit: "weight", + variant_unit_scale: 1, unit_description: "L", price: "1", + primary_taxon_id: taxon.id, supplier_id: variant.supplier.id }, product_id: variant.product.id expect(attributes.all?{ |attr| json_response.include? attr.to_s }).to eq(true) From c8bf23bdc2f71559305ae654af72d091115de783 Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou Date: Tue, 2 Jul 2024 17:02:22 +1000 Subject: [PATCH 17/68] Fix UnitPrice spec --- spec/services/unit_prices_spec.rb | 87 ++++++++++++++----------------- 1 file changed, 39 insertions(+), 48 deletions(-) diff --git a/spec/services/unit_prices_spec.rb b/spec/services/unit_prices_spec.rb index 2219eab6c8e..5166346aa12 100644 --- a/spec/services/unit_prices_spec.rb +++ b/spec/services/unit_prices_spec.rb @@ -3,53 +3,46 @@ require 'spec_helper' RSpec.describe UnitPrice do - subject { UnitPrice.new(variant) } - let(:variant) { Spree::Variant.new } - let(:product) { instance_double(Spree::Product) } - before do - allow(variant).to receive(:product) { product } allow(Spree::Config).to receive(:available_units).and_return("g,lb,oz,kg,T,mL,L,kL") end describe "#unit" do context "metric" do - before do - allow(product).to receive(:variant_unit_scale) { 1.0 } - end - it "returns kg for weight" do - allow(product).to receive(:variant_unit) { "weight" } - expect(subject.unit).to eq("kg") + variant = Spree::Variant.new(variant_unit_scale: 1.0, variant_unit: "weight") + + expect(UnitPrice.new(variant).unit).to eq("kg") end it "returns L for volume" do - allow(product).to receive(:variant_unit) { "volume" } - expect(subject.unit).to eq("L") + variant = Spree::Variant.new(variant_unit_scale: 1.0, variant_unit: "volume") + + expect(UnitPrice.new(variant).unit).to eq("L") end end context "imperial" do it "returns lbs" do - allow(product).to receive(:variant_unit_scale) { 453.6 } - allow(product).to receive(:variant_unit) { "weight" } - expect(subject.unit).to eq("lb") + variant = Spree::Variant.new(variant_unit_scale: 453.6, variant_unit: "weight") + + expect(UnitPrice.new(variant).unit).to eq("lb") end end context "items" do it "returns items if no unit is specified" do - allow(product).to receive(:variant_unit_name) { nil } - allow(product).to receive(:variant_unit_scale) { nil } - allow(product).to receive(:variant_unit) { "items" } - expect(subject.unit).to eq("Item") + variant = Spree::Variant.new(variant_unit_name: nil, variant_unit_scale: nil, + variant_unit: "items") + + expect(UnitPrice.new(variant).unit).to eq("Item") end it "returns the unit if a unit is specified" do - allow(product).to receive(:variant_unit_name) { "bunch" } - allow(product).to receive(:variant_unit_scale) { nil } - allow(product).to receive(:variant_unit) { "items" } - expect(subject.unit).to eq("bunch") + variant = Spree::Variant.new(variant_unit_name: "bunch", variant_unit_scale: nil, + variant_unit: "items") + + expect(UnitPrice.new(variant).unit).to eq("bunch") end end end @@ -57,49 +50,47 @@ describe "#denominator" do context "metric" do it "returns 0.5 for a 500g variant" do - allow(product).to receive(:variant_unit_scale) { 1.0 } - allow(product).to receive(:variant_unit) { "weight" } - variant.unit_value = 500 - expect(subject.denominator).to eq(0.5) + variant = Spree::Variant.new(variant_unit_scale: 1.0, unit_value: 500, + variant_unit: "weight") + + expect(UnitPrice.new(variant).denominator).to eq(0.5) end it "returns 2 for a 2kg variant" do - allow(product).to receive(:variant_unit_scale) { 1000 } - allow(product).to receive(:variant_unit) { "weight" } - variant.unit_value = 2000 - expect(subject.denominator).to eq(2) + variant = Spree::Variant.new(variant_unit_scale: 1000, unit_value: 2000, + variant_unit: "weight") + + expect(UnitPrice.new(variant).denominator).to eq(2) end it "returns 0.5 for a 500mL variant" do - allow(product).to receive(:variant_unit_scale) { 0.001 } - allow(product).to receive(:variant_unit) { "volume" } - variant.unit_value = 0.5 - expect(subject.denominator).to eq(0.5) + variant = Spree::Variant.new(variant_unit_scale: 0.001, unit_value: 0.5, + variant_unit: "volume") + + expect(UnitPrice.new(variant).denominator).to eq(0.5) end end context "imperial" do it "returns 2 for a 2 pound variant" do - allow(product).to receive(:variant_unit_scale) { 453.6 } - allow(product).to receive(:variant_unit) { "weight" } - variant.unit_value = 2 * 453.6 - expect(subject.denominator).to eq(2) + variant = Spree::Variant.new(variant_unit_scale: 453.6, unit_value: 2 * 453.6, + variant_unit: "weight") + + expect(UnitPrice.new(variant).denominator).to eq(2) end end context "items" do it "returns 1 if no unit is specified" do - allow(product).to receive(:variant_unit_scale) { nil } - allow(product).to receive(:variant_unit) { "items" } - variant.unit_value = 1 - expect(subject.denominator).to eq(1) + variant = Spree::Variant.new(variant_unit_scale: nil, unit_value: 1, variant_unit: "items") + + expect(UnitPrice.new(variant).denominator).to eq(1) end it "returns 2 for multi-item units" do - allow(product).to receive(:variant_unit_scale) { nil } - allow(product).to receive(:variant_unit) { "items" } - variant.unit_value = 2 - expect(subject.denominator).to eq(2) + variant = Spree::Variant.new(variant_unit_scale: nil, unit_value: 2, variant_unit: "items") + + expect(UnitPrice.new(variant).denominator).to eq(2) end end end From 9b4cd014bf545e067fb8299d3e63c9cb45045a7d Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou Date: Wed, 3 Jul 2024 14:51:02 +1000 Subject: [PATCH 18/68] Fix DFC supplied product builder --- .../services/quantitative_value_builder.rb | 12 +-- .../app/services/supplied_product_builder.rb | 3 +- .../quantitative_value_builder_spec.rb | 83 +++++++++---------- 3 files changed, 48 insertions(+), 50 deletions(-) diff --git a/engines/dfc_provider/app/services/quantitative_value_builder.rb b/engines/dfc_provider/app/services/quantitative_value_builder.rb index 31401a33695..10896578b50 100644 --- a/engines/dfc_provider/app/services/quantitative_value_builder.rb +++ b/engines/dfc_provider/app/services/quantitative_value_builder.rb @@ -11,7 +11,7 @@ class QuantitativeValueBuilder < DfcBuilder def self.quantity(variant) DataFoodConsortium::Connector::QuantitativeValue.new( - unit: unit(variant.product.variant_unit), + unit: unit(variant.variant_unit), value: variant.unit_value, ) end @@ -27,7 +27,7 @@ def self.unit(unit_name) end end - def self.apply(quantity, product) + def self.apply(quantity, variant) measure, unit_name, unit_scale = map_unit(quantity.unit) value = quantity.value.to_f * unit_scale @@ -37,10 +37,10 @@ def self.apply(quantity, product) value = 1 end - product.variant_unit = measure - product.variant_unit_name = unit_name if measure == "items" - product.variant_unit_scale = unit_scale - product.unit_value = value + variant.variant_unit = measure + variant.variant_unit_name = unit_name if measure == "items" + variant.variant_unit_scale = unit_scale + variant.unit_value = value end # Map DFC units to OFN fields: diff --git a/engines/dfc_provider/app/services/supplied_product_builder.rb b/engines/dfc_provider/app/services/supplied_product_builder.rb index 4eb488c4f23..8707ce04b0f 100644 --- a/engines/dfc_provider/app/services/supplied_product_builder.rb +++ b/engines/dfc_provider/app/services/supplied_product_builder.rb @@ -96,8 +96,7 @@ def self.apply(supplied_product, variant) variant.display_name = supplied_product.name variant.primary_taxon = taxon(supplied_product) - QuantitativeValueBuilder.apply(supplied_product.quantity, variant.product) - variant.unit_value = variant.product.unit_value + QuantitativeValueBuilder.apply(supplied_product.quantity, variant) catalog_item = supplied_product&.catalogItems&.first offer = catalog_item&.offers&.first diff --git a/engines/dfc_provider/spec/services/quantitative_value_builder_spec.rb b/engines/dfc_provider/spec/services/quantitative_value_builder_spec.rb index f18a58b5611..04783becfad 100644 --- a/engines/dfc_provider/spec/services/quantitative_value_builder_spec.rb +++ b/engines/dfc_provider/spec/services/quantitative_value_builder_spec.rb @@ -4,12 +4,11 @@ RSpec.describe QuantitativeValueBuilder do subject(:builder) { described_class } - let(:variant) { build(:variant, product:) } - let(:product) { build(:product, name: "Apple") } + let(:variant) { build(:variant) } describe ".quantity" do it "recognises items" do - product.variant_unit = "item" + variant.variant_unit = "item" variant.unit_value = 1 quantity = builder.quantity(variant) @@ -18,7 +17,7 @@ end it "recognises volume" do - product.variant_unit = "volume" + variant.variant_unit = "volume" variant.unit_value = 2 quantity = builder.quantity(variant) @@ -27,7 +26,7 @@ end it "recognises weight" do - product.variant_unit = "weight" + variant.variant_unit = "weight" variant.unit_value = 1000 # 1kg quantity = builder.quantity(variant) @@ -36,7 +35,7 @@ end it "falls back to items" do - product.variant_unit = nil + variant.variant_unit = nil quantity = builder.quantity(variant) expect(quantity.value).to eq 1.0 @@ -46,7 +45,7 @@ describe ".apply" do let(:quantity_unit) { DfcLoader.connector.MEASURES } - let(:product) { Spree::Product.new } + let(:variant) { Spree::Variant.new } it "uses items for anything unknown" do quantity = DataFoodConsortium::Connector::QuantitativeValue.new( @@ -54,12 +53,12 @@ value: 3, ) - builder.apply(quantity, product) + builder.apply(quantity, variant) - expect(product.variant_unit).to eq "items" - expect(product.variant_unit_name).to eq "Jar" - expect(product.variant_unit_scale).to eq 1 - expect(product.unit_value).to eq 3 + expect(variant.variant_unit).to eq "items" + expect(variant.variant_unit_name).to eq "Jar" + expect(variant.variant_unit_scale).to eq 1 + expect(variant.unit_value).to eq 3 end it "knows metric units" do @@ -68,12 +67,12 @@ value: 2, ) - builder.apply(quantity, product) + builder.apply(quantity, variant) - expect(product.variant_unit).to eq "volume" - expect(product.variant_unit_name).to eq nil - expect(product.variant_unit_scale).to eq 1 - expect(product.unit_value).to eq 2 + expect(variant.variant_unit).to eq "volume" + expect(variant.variant_unit_name).to eq nil + expect(variant.variant_unit_scale).to eq 1 + expect(variant.unit_value).to eq 2 end it "knows metric units with a scale in OFN" do @@ -82,12 +81,12 @@ value: 4, ) - builder.apply(quantity, product) + builder.apply(quantity, variant) - expect(product.variant_unit).to eq "weight" - expect(product.variant_unit_name).to eq nil - expect(product.variant_unit_scale).to eq 1_000 - expect(product.unit_value).to eq 4_000 + expect(variant.variant_unit).to eq "weight" + expect(variant.variant_unit_name).to eq nil + expect(variant.variant_unit_scale).to eq 1_000 + expect(variant.unit_value).to eq 4_000 end it "knows metric units with a small scale" do @@ -96,12 +95,12 @@ value: 5, ) - builder.apply(quantity, product) + builder.apply(quantity, variant) - expect(product.variant_unit).to eq "weight" - expect(product.variant_unit_name).to eq nil - expect(product.variant_unit_scale).to eq 0.001 - expect(product.unit_value).to eq 0.005 + expect(variant.variant_unit).to eq "weight" + expect(variant.variant_unit_name).to eq nil + expect(variant.variant_unit_scale).to eq 0.001 + expect(variant.unit_value).to eq 0.005 end it "interpretes values given as a string" do @@ -110,12 +109,12 @@ value: "0.4", ) - builder.apply(quantity, product) + builder.apply(quantity, variant) - expect(product.variant_unit).to eq "weight" - expect(product.variant_unit_name).to eq nil - expect(product.variant_unit_scale).to eq 1_000 - expect(product.unit_value).to eq 400 + expect(variant.variant_unit).to eq "weight" + expect(variant.variant_unit_name).to eq nil + expect(variant.variant_unit_scale).to eq 1_000 + expect(variant.unit_value).to eq 400 end it "knows imperial units" do @@ -124,12 +123,12 @@ value: 10, ) - builder.apply(quantity, product) + builder.apply(quantity, variant) - expect(product.variant_unit).to eq "weight" - expect(product.variant_unit_name).to eq nil - expect(product.variant_unit_scale).to eq 453.59237 - expect(product.unit_value).to eq 4_535.9237 + expect(variant.variant_unit).to eq "weight" + expect(variant.variant_unit_name).to eq nil + expect(variant.variant_unit_scale).to eq 453.59237 + expect(variant.unit_value).to eq 4_535.9237 end it "knows customary units" do @@ -138,12 +137,12 @@ value: 2, ) - builder.apply(quantity, product) + builder.apply(quantity, variant) - expect(product.variant_unit).to eq "items" - expect(product.variant_unit_name).to eq "dozen" - expect(product.variant_unit_scale).to eq 12 - expect(product.unit_value).to eq 24 + expect(variant.variant_unit).to eq "items" + expect(variant.variant_unit_name).to eq "dozen" + expect(variant.variant_unit_scale).to eq 12 + expect(variant.unit_value).to eq 24 end end end From 36c4d24c936978dd22f07ae2bbff11a4c72c192f Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou Date: Mon, 8 Jul 2024 14:24:22 +1000 Subject: [PATCH 19/68] Fix angular option value namer --- .../services/option_value_namer.js.coffee | 13 +++-- .../option_value_namer_spec.js.coffee | 54 +++++++++---------- 2 files changed, 32 insertions(+), 35 deletions(-) diff --git a/app/assets/javascripts/admin/products/services/option_value_namer.js.coffee b/app/assets/javascripts/admin/products/services/option_value_namer.js.coffee index 0e7c94331d2..2a61b3efc19 100644 --- a/app/assets/javascripts/admin/products/services/option_value_namer.js.coffee +++ b/app/assets/javascripts/admin/products/services/option_value_namer.js.coffee @@ -13,16 +13,16 @@ angular.module("admin.products").factory "OptionValueNamer", (VariantUnitManager name_fields.join ' ' value_scaled: -> - @variant.product.variant_unit_scale? + @variant.variant_unit_scale? option_value_value_unit: -> if @variant.unit_value? - if @variant.product.variant_unit in ["weight", "volume"] + if @variant.variant_unit in ["weight", "volume"] [value, unit_name] = @option_value_value_unit_scaled() else value = @variant.unit_value - unit_name = @pluralize(@variant.product.variant_unit_name, value) + unit_name = @pluralize(@variant.variant_unit_name, value) value = parseInt(value, 10) if value == parseInt(value, 10) @@ -58,14 +58,13 @@ angular.module("admin.products").factory "OptionValueNamer", (VariantUnitManager # to >= 1 when expressed in it. # If there is none available where this is true, use the smallest # available unit. - product = @variant.product - scales = VariantUnitManager.compatibleUnitScales(product.variant_unit_scale, product.variant_unit) + scales = VariantUnitManager.compatibleUnitScales(@variant.variant_unit_scale, @variant.variant_unit) variantUnitValue = @variant.unit_value # sets largestScale = last element in filtered scales array [_, ..., largestScale] = (scales.filter (s) -> variantUnitValue / s >= 1) if (largestScale) - [largestScale, VariantUnitManager.getUnitName(largestScale, product.variant_unit)] + [largestScale, VariantUnitManager.getUnitName(largestScale, @variant.variant_unit)] else - [scales[0], VariantUnitManager.getUnitName(scales[0], product.variant_unit)] + [scales[0], VariantUnitManager.getUnitName(scales[0], @variant.variant_unit)] diff --git a/spec/javascripts/unit/admin/services/option_value_namer_spec.js.coffee b/spec/javascripts/unit/admin/services/option_value_namer_spec.js.coffee index 8eaae5bbeed..6b01dda43eb 100644 --- a/spec/javascripts/unit/admin/services/option_value_namer_spec.js.coffee +++ b/spec/javascripts/unit/admin/services/option_value_namer_spec.js.coffee @@ -42,72 +42,70 @@ describe "Option Value Namer", -> expect(namer.name()).toBe "value unit" describe "determining if a variant's value is scaled", -> - v = p = namer = null + v = namer = null beforeEach -> - p = {} - v = { product: p } + v = {} namer = new OptionValueNamer(v) it "returns true when the product has a scale", -> - p.variant_unit_scale = 1000 + v.variant_unit_scale = 1000 expect(namer.value_scaled()).toBe true it "returns false otherwise", -> expect(namer.value_scaled()).toBe false describe "generating option value's value and unit", -> - v = p = namer = null + v = namer = null beforeEach -> - p = {} - v = { product: p } + v = {} namer = new OptionValueNamer(v) it "generates simple values", -> - p.variant_unit = 'weight' - p.variant_unit_scale = 1.0 + v.variant_unit = 'weight' + v.variant_unit_scale = 1.0 v.unit_value = 100 expect(namer.option_value_value_unit()).toEqual [100, 'g'] it "generates values when unit value is non-integer", -> - p.variant_unit = 'weight' - p.variant_unit_scale = 1.0 + v.variant_unit = 'weight' + v.variant_unit_scale = 1.0 v.unit_value = 123.45 expect(namer.option_value_value_unit()).toEqual [123.45, 'g'] it "returns a value of 1 when unit value equals the scale", -> - p.variant_unit = 'weight' - p.variant_unit_scale = 1000.0 + v.variant_unit = 'weight' + v.variant_unit_scale = 1000.0 v.unit_value = 1000.0 expect(namer.option_value_value_unit()).toEqual [1, 'kg'] it "generates values for all weight scales", -> for units in [[1.0, 'g'], [1000.0, 'kg'], [1000000.0, 'T']] [scale, unit] = units - p.variant_unit = 'weight' - p.variant_unit_scale = scale + v.variant_unit = 'weight' + v.variant_unit_scale = scale v.unit_value = 100 * scale expect(namer.option_value_value_unit()).toEqual [100, unit] it "generates values for all volume scales", -> for units in [[0.001, 'mL'], [1.0, 'L'], [1000.0, 'kL']] [scale, unit] = units - p.variant_unit = 'volume' - p.variant_unit_scale = scale + v.variant_unit = 'volume' + v.variant_unit_scale = scale v.unit_value = 100 * scale expect(namer.option_value_value_unit()).toEqual [100, unit] - + it "generates right values for volume with rounded values", -> unit = 'L' - p.variant_unit = 'volume' - p.variant_unit_scale = 1.0 + v.variant_unit = 'volume' + v.variant_unit_scale = 1.0 v.unit_value = 0.7 expect(namer.option_value_value_unit()).toEqual [700, 'mL'] it "chooses the correct scale when value is very small", -> - p.variant_unit = 'volume' - p.variant_unit_scale = 0.001 + v.variant_unit = 'volume' + v.variant_unit_scale = 0.001 v.unit_value = 0.0001 expect(namer.option_value_value_unit()).toEqual [0.1, 'mL'] @@ -120,15 +118,15 @@ describe "Option Value Namer", -> # subject.option_value_value_unit.should == [100, unit.pluralize] it "generates singular values for item units when value is 1", -> - p.variant_unit = 'items' - p.variant_unit_scale = null - p.variant_unit_name = 'packet' + v.variant_unit = 'items' + v.variant_unit_scale = null + v.variant_unit_name = 'packet' v.unit_value = 1 expect(namer.option_value_value_unit()).toEqual [1, 'packet'] it "returns [nil, nil] when unit value is not set", -> - p.variant_unit = 'items' - p.variant_unit_scale = null - p.variant_unit_name = 'foo' + v.variant_unit = 'items' + v.variant_unit_scale = null + v.variant_unit_name = 'foo' v.unit_value = null expect(namer.option_value_value_unit()).toEqual [null, null] From cd74a73680369e1e63e345f1a5d84d1a0bef2b19 Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou Date: Mon, 8 Jul 2024 14:31:57 +1000 Subject: [PATCH 20/68] Consolidate angular option value namer spec Merge the two spec files into the correct one. --- .../option_value_namer_spec.js.coffee | 147 ++++++++++++++++-- .../option_value_namer_spec.js.coffee | 132 ---------------- 2 files changed, 137 insertions(+), 142 deletions(-) delete mode 100644 spec/javascripts/unit/admin/services/option_value_namer_spec.js.coffee diff --git a/spec/javascripts/unit/admin/products/services/option_value_namer_spec.js.coffee b/spec/javascripts/unit/admin/products/services/option_value_namer_spec.js.coffee index ba03fe30cbd..bfc04b7a759 100644 --- a/spec/javascripts/unit/admin/products/services/option_value_namer_spec.js.coffee +++ b/spec/javascripts/unit/admin/products/services/option_value_namer_spec.js.coffee @@ -1,29 +1,156 @@ describe "OptionValueNamer", -> - subject = null + OptionValueNamer = null beforeEach -> - module('admin.products') + module "ofn.admin" + module "admin.products" module ($provide)-> $provide.value "availableUnits", "g,kg,T,mL,L,kL" null - inject (_OptionValueNamer_) -> - subject = new _OptionValueNamer_ + + beforeEach inject (_OptionValueNamer_) -> + OptionValueNamer = _OptionValueNamer_ describe "pluralize a variant unit name", -> + namer = null + beforeEach -> + namer = new OptionValueNamer({}) + it "returns the same word if no plural is known", -> - expect(subject.pluralize("foo", 2)).toEqual "foo" + expect(namer.pluralize("foo", 2)).toEqual "foo" it "returns the same word if we omit the quantity", -> - expect(subject.pluralize("loaf")).toEqual "loaf" + expect(namer.pluralize("loaf")).toEqual "loaf" it "finds the plural of a word", -> - expect(subject.pluralize("loaf", 2)).toEqual "loaves" + expect(namer.pluralize("loaf", 2)).toEqual "loaves" it "finds the singular of a word", -> - expect(subject.pluralize("loaves", 1)).toEqual "loaf" + expect(namer.pluralize("loaves", 1)).toEqual "loaf" it "finds the zero form of a word", -> - expect(subject.pluralize("loaf", 0)).toEqual "loaves" + expect(namer.pluralize("loaf", 0)).toEqual "loaves" it "ignores upper case", -> - expect(subject.pluralize("Loaf", 2)).toEqual "loaves" + expect(namer.pluralize("Loaf", 2)).toEqual "loaves" + + describe "generating option value name", -> + v = namer = null + beforeEach -> + v = {} + namer = new OptionValueNamer(v) + + it "when description is blank", -> + v.unit_description = null + spyOn(namer, "value_scaled").and.returnValue true + spyOn(namer, "option_value_value_unit").and.returnValue ["value", "unit"] + expect(namer.name()).toBe "valueunit" + + it "when description is present", -> + v.unit_description = 'desc' + spyOn(namer, "option_value_value_unit").and.returnValue ["value", "unit"] + spyOn(namer, "value_scaled").and.returnValue true + expect(namer.name()).toBe "valueunit desc" + + it "when value is blank and description is present", -> + v.unit_description = 'desc' + spyOn(namer, "option_value_value_unit").and.returnValue [null, null] + spyOn(namer, "value_scaled").and.returnValue true + expect(namer.name()).toBe "desc" + + it "spaces value and unit when value is unscaled", -> + v.unit_description = null + spyOn(namer, "option_value_value_unit").and.returnValue ["value", "unit"] + spyOn(namer, "value_scaled").and.returnValue false + expect(namer.name()).toBe "value unit" + + describe "determining if a variant's value is scaled", -> + v = namer = null + + beforeEach -> + v = {} + namer = new OptionValueNamer(v) + + it "returns true when the product has a scale", -> + v.variant_unit_scale = 1000 + expect(namer.value_scaled()).toBe true + + it "returns false otherwise", -> + expect(namer.value_scaled()).toBe false + + describe "generating option value's value and unit", -> + v = namer = null + + beforeEach -> + v = {} + namer = new OptionValueNamer(v) + + it "generates simple values", -> + v.variant_unit = 'weight' + v.variant_unit_scale = 1.0 + v.unit_value = 100 + expect(namer.option_value_value_unit()).toEqual [100, 'g'] + + it "generates values when unit value is non-integer", -> + v.variant_unit = 'weight' + v.variant_unit_scale = 1.0 + v.unit_value = 123.45 + expect(namer.option_value_value_unit()).toEqual [123.45, 'g'] + + it "returns a value of 1 when unit value equals the scale", -> + v.variant_unit = 'weight' + v.variant_unit_scale = 1000.0 + v.unit_value = 1000.0 + expect(namer.option_value_value_unit()).toEqual [1, 'kg'] + + it "generates values for all weight scales", -> + for units in [[1.0, 'g'], [1000.0, 'kg'], [1000000.0, 'T']] + [scale, unit] = units + v.variant_unit = 'weight' + v.variant_unit_scale = scale + v.unit_value = 100 * scale + expect(namer.option_value_value_unit()).toEqual [100, unit] + + it "generates values for all volume scales", -> + for units in [[0.001, 'mL'], [1.0, 'L'], [1000.0, 'kL']] + [scale, unit] = units + v.variant_unit = 'volume' + v.variant_unit_scale = scale + v.unit_value = 100 * scale + expect(namer.option_value_value_unit()).toEqual [100, unit] + + it "generates right values for volume with rounded values", -> + unit = 'L' + v.variant_unit = 'volume' + v.variant_unit_scale = 1.0 + v.unit_value = 0.7 + expect(namer.option_value_value_unit()).toEqual [700, 'mL'] + + it "chooses the correct scale when value is very small", -> + v.variant_unit = 'volume' + v.variant_unit_scale = 0.001 + v.unit_value = 0.0001 + expect(namer.option_value_value_unit()).toEqual [0.1, 'mL'] + + it "generates values for item units", -> + #TODO + # %w(packet box).each do |unit| + # p = double(:product, variant_unit: 'items', variant_unit_scale: nil, variant_unit_name: unit) + # v.stub(:product) { p } + # v.stub(:unit_value) { 100 } + # subject.option_value_value_unit.should == [100, unit.pluralize] + + it "generates singular values for item units when value is 1", -> + v.variant_unit = 'items' + v.variant_unit_scale = null + v.variant_unit_name = 'packet' + v.unit_value = 1 + expect(namer.option_value_value_unit()).toEqual [1, 'packet'] + + it "returns [nil, nil] when unit value is not set", -> + v.variant_unit = 'items' + v.variant_unit_scale = null + v.variant_unit_name = 'foo' + v.unit_value = null + expect(namer.option_value_value_unit()).toEqual [null, null] + diff --git a/spec/javascripts/unit/admin/services/option_value_namer_spec.js.coffee b/spec/javascripts/unit/admin/services/option_value_namer_spec.js.coffee deleted file mode 100644 index 6b01dda43eb..00000000000 --- a/spec/javascripts/unit/admin/services/option_value_namer_spec.js.coffee +++ /dev/null @@ -1,132 +0,0 @@ -describe "Option Value Namer", -> - OptionValueNamer = null - - beforeEach -> - module "ofn.admin" - module "admin.products" - module ($provide)-> - $provide.value "availableUnits", "g,kg,T,mL,L,kL" - null - - beforeEach inject (_OptionValueNamer_) -> - OptionValueNamer = _OptionValueNamer_ - - describe "generating option value name", -> - v = namer = null - beforeEach -> - v = {} - namer = new OptionValueNamer(v) - - it "when description is blank", -> - v.unit_description = null - spyOn(namer, "value_scaled").and.returnValue true - spyOn(namer, "option_value_value_unit").and.returnValue ["value", "unit"] - expect(namer.name()).toBe "valueunit" - - it "when description is present", -> - v.unit_description = 'desc' - spyOn(namer, "option_value_value_unit").and.returnValue ["value", "unit"] - spyOn(namer, "value_scaled").and.returnValue true - expect(namer.name()).toBe "valueunit desc" - - it "when value is blank and description is present", -> - v.unit_description = 'desc' - spyOn(namer, "option_value_value_unit").and.returnValue [null, null] - spyOn(namer, "value_scaled").and.returnValue true - expect(namer.name()).toBe "desc" - - it "spaces value and unit when value is unscaled", -> - v.unit_description = null - spyOn(namer, "option_value_value_unit").and.returnValue ["value", "unit"] - spyOn(namer, "value_scaled").and.returnValue false - expect(namer.name()).toBe "value unit" - - describe "determining if a variant's value is scaled", -> - v = namer = null - - beforeEach -> - v = {} - namer = new OptionValueNamer(v) - - it "returns true when the product has a scale", -> - v.variant_unit_scale = 1000 - expect(namer.value_scaled()).toBe true - - it "returns false otherwise", -> - expect(namer.value_scaled()).toBe false - - describe "generating option value's value and unit", -> - v = namer = null - - beforeEach -> - v = {} - namer = new OptionValueNamer(v) - - it "generates simple values", -> - v.variant_unit = 'weight' - v.variant_unit_scale = 1.0 - v.unit_value = 100 - expect(namer.option_value_value_unit()).toEqual [100, 'g'] - - it "generates values when unit value is non-integer", -> - v.variant_unit = 'weight' - v.variant_unit_scale = 1.0 - v.unit_value = 123.45 - expect(namer.option_value_value_unit()).toEqual [123.45, 'g'] - - it "returns a value of 1 when unit value equals the scale", -> - v.variant_unit = 'weight' - v.variant_unit_scale = 1000.0 - v.unit_value = 1000.0 - expect(namer.option_value_value_unit()).toEqual [1, 'kg'] - - it "generates values for all weight scales", -> - for units in [[1.0, 'g'], [1000.0, 'kg'], [1000000.0, 'T']] - [scale, unit] = units - v.variant_unit = 'weight' - v.variant_unit_scale = scale - v.unit_value = 100 * scale - expect(namer.option_value_value_unit()).toEqual [100, unit] - - it "generates values for all volume scales", -> - for units in [[0.001, 'mL'], [1.0, 'L'], [1000.0, 'kL']] - [scale, unit] = units - v.variant_unit = 'volume' - v.variant_unit_scale = scale - v.unit_value = 100 * scale - expect(namer.option_value_value_unit()).toEqual [100, unit] - - it "generates right values for volume with rounded values", -> - unit = 'L' - v.variant_unit = 'volume' - v.variant_unit_scale = 1.0 - v.unit_value = 0.7 - expect(namer.option_value_value_unit()).toEqual [700, 'mL'] - - it "chooses the correct scale when value is very small", -> - v.variant_unit = 'volume' - v.variant_unit_scale = 0.001 - v.unit_value = 0.0001 - expect(namer.option_value_value_unit()).toEqual [0.1, 'mL'] - - it "generates values for item units", -> - #TODO - # %w(packet box).each do |unit| - # p = double(:product, variant_unit: 'items', variant_unit_scale: nil, variant_unit_name: unit) - # v.stub(:product) { p } - # v.stub(:unit_value) { 100 } - # subject.option_value_value_unit.should == [100, unit.pluralize] - - it "generates singular values for item units when value is 1", -> - v.variant_unit = 'items' - v.variant_unit_scale = null - v.variant_unit_name = 'packet' - v.unit_value = 1 - expect(namer.option_value_value_unit()).toEqual [1, 'packet'] - - it "returns [nil, nil] when unit value is not set", -> - v.variant_unit = 'items' - v.variant_unit_scale = null - v.variant_unit_name = 'foo' - v.unit_value = null - expect(namer.option_value_value_unit()).toEqual [null, null] From b1b534aa1bcbe4ce7ead1d342d540d3185b7d602 Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou Date: Mon, 8 Jul 2024 15:22:08 +1000 Subject: [PATCH 21/68] Fix product and variant api serializer --- app/serializers/api/admin/product_serializer.rb | 3 +-- app/serializers/api/admin/variant_serializer.rb | 3 ++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/serializers/api/admin/product_serializer.rb b/app/serializers/api/admin/product_serializer.rb index 836b78bdaa3..7056c5745cd 100644 --- a/app/serializers/api/admin/product_serializer.rb +++ b/app/serializers/api/admin/product_serializer.rb @@ -3,8 +3,7 @@ module Api module Admin class ProductSerializer < ActiveModel::Serializer - attributes :id, :name, :sku, :variant_unit, :variant_unit_scale, :variant_unit_name, - :inherits_properties, :on_hand, :price, :import_date, :image_url, + attributes :id, :name, :sku, :inherits_properties, :on_hand, :price, :import_date, :image_url, :thumb_url, :variants def variants diff --git a/app/serializers/api/admin/variant_serializer.rb b/app/serializers/api/admin/variant_serializer.rb index 5ccb8347c73..516351a3e40 100644 --- a/app/serializers/api/admin/variant_serializer.rb +++ b/app/serializers/api/admin/variant_serializer.rb @@ -6,7 +6,8 @@ class VariantSerializer < ActiveModel::Serializer attributes :id, :name, :producer_name, :image, :sku, :import_date, :tax_category_id, :options_text, :unit_value, :unit_description, :unit_to_display, :display_as, :display_name, :name_to_display, :variant_overrides_count, - :price, :on_demand, :on_hand, :in_stock, :stock_location_id, :stock_location_name + :price, :on_demand, :on_hand, :in_stock, :stock_location_id, :stock_location_name, + :variant_unit, :variant_unit_scale, :variant_unit_name, :variant_unit_with_scale has_one :primary_taxon, key: :category_id, embed: :id has_one :supplier, key: :producer_id, embed: :id From 6ff9650eaf895be6bcef6ffc705b3b4c09c48b0e Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou Date: Mon, 8 Jul 2024 15:37:45 +1000 Subject: [PATCH 22/68] Fix legacy bulk edit products UI --- .../admin/bulk_product_update.js.coffee | 50 +++-- .../admin/directives/display_as.js.coffee | 21 +- .../directives/maintain_unit_scale.js.coffee | 8 - .../admin/directives/track_master.js.coffee | 8 - .../index/_products_product.html.haml | 3 - .../index/_products_variant.html.haml | 4 +- .../admin/bulk_product_update_spec.js.coffee | 182 +++++++++--------- 7 files changed, 124 insertions(+), 152 deletions(-) delete mode 100644 app/assets/javascripts/admin/directives/maintain_unit_scale.js.coffee delete mode 100644 app/assets/javascripts/admin/directives/track_master.js.coffee diff --git a/app/assets/javascripts/admin/bulk_product_update.js.coffee b/app/assets/javascripts/admin/bulk_product_update.js.coffee index d32977b8c05..eae103015a2 100644 --- a/app/assets/javascripts/admin/bulk_product_update.js.coffee +++ b/app/assets/javascripts/admin/bulk_product_update.js.coffee @@ -187,9 +187,8 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", ($scope, $timeout product.variants.length > 0 - $scope.hasUnit = (product) -> - product.variant_unit_with_scale? - + $scope.hasUnit = (variant) -> + variant.variant_unit_with_scale? $scope.variantSaved = (variant) -> variant.hasOwnProperty('id') && variant.id > 0 @@ -242,32 +241,28 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", ($scope, $timeout $window.location = destination $scope.packProduct = (product) -> - if product.variant_unit_with_scale - match = product.variant_unit_with_scale.match(/^([^_]+)_([\d\.]+)$/) - if match - product.variant_unit = match[1] - product.variant_unit_scale = parseFloat(match[2]) - else - product.variant_unit = product.variant_unit_with_scale - product.variant_unit_scale = null - else - product.variant_unit = product.variant_unit_scale = null - - if product.variants for id, variant of product.variants - $scope.packVariant product, variant + $scope.packVariant variant + + $scope.packVariant = (variant) -> + if variant.variant_unit_with_scale + match = variant.variant_unit_with_scale.match(/^([^_]+)_([\d\.]+)$/) + if match + variant.variant_unit = match[1] + variant.variant_unit_scale = parseFloat(match[2]) + else + variant.variant_unit = variant.variant_unit_with_scale + variant.variant_unit_scale = null - $scope.packVariant = (product, variant) -> if variant.hasOwnProperty("unit_value_with_description") match = variant.unit_value_with_description.match(/^([\d\.\,]+(?= |$)|)( |)(.*)$/) if match - product = BulkProducts.find product.id variant.unit_value = parseFloat(match[1].replace(",", ".")) variant.unit_value = null if isNaN(variant.unit_value) - if variant.unit_value && product.variant_unit_scale - variant.unit_value = parseFloat(window.bigDecimal.multiply(variant.unit_value, product.variant_unit_scale, 2)) + if variant.unit_value && variant.variant_unit_scale + variant.unit_value = parseFloat(window.bigDecimal.multiply(variant.unit_value, variant.variant_unit_scale, 2)) variant.unit_description = match[3] $scope.incrementLimit = -> @@ -321,13 +316,6 @@ filterSubmitProducts = (productsToFilter) -> if product.hasOwnProperty("price") filteredProduct.price = product.price hasUpdatableProperty = true - if product.hasOwnProperty("variant_unit_with_scale") - filteredProduct.variant_unit = product.variant_unit - filteredProduct.variant_unit_scale = product.variant_unit_scale - hasUpdatableProperty = true - if product.hasOwnProperty("variant_unit_name") - filteredProduct.variant_unit_name = product.variant_unit_name - hasUpdatableProperty = true if product.hasOwnProperty("on_hand") and filteredVariants.length == 0 #only update if no variants present filteredProduct.on_hand = product.on_hand hasUpdatableProperty = true @@ -383,6 +371,14 @@ filterSubmitVariant = (variant) -> if variant.hasOwnProperty("producer_id") filteredVariant.supplier_id = variant.producer_id hasUpdatableProperty = true + if variant.hasOwnProperty("variant_unit_with_scale") + filteredVariant.variant_unit = variant.variant_unit + filteredVariant.variant_unit_scale = variant.variant_unit_scale + hasUpdatableProperty = true + if variant.hasOwnProperty("variant_unit_name") + filteredVariant.variant_unit_name = variant.variant_unit_name + hasUpdatableProperty = true + {filteredVariant: filteredVariant, hasUpdatableProperty: hasUpdatableProperty} diff --git a/app/assets/javascripts/admin/directives/display_as.js.coffee b/app/assets/javascripts/admin/directives/display_as.js.coffee index 375f1795eef..bb624113ba8 100644 --- a/app/assets/javascripts/admin/directives/display_as.js.coffee +++ b/app/assets/javascripts/admin/directives/display_as.js.coffee @@ -4,31 +4,30 @@ angular.module("ofn.admin").directive "ofnDisplayAs", (OptionValueNamer) -> scope.$watchCollection -> return [ scope.$eval(attrs.ofnDisplayAs).unit_value_with_description - scope.product.variant_unit_name - scope.product.variant_unit_with_scale + scope.variant.variant_unit_name + scope.variant.variant_unit_with_scale ] , -> [variant_unit, variant_unit_scale] = productUnitProperties() [unit_value, unit_description] = variantUnitProperties(variant_unit_scale) - variant_object = + variant_object = unit_value: unit_value unit_description: unit_description - product: - variant_unit_scale: variant_unit_scale - variant_unit: variant_unit - variant_unit_name: scope.product.variant_unit_name + variant_unit_scale: variant_unit_scale + variant_unit: variant_unit + variant_unit_name: scope.variant.variant_unit_name scope.placeholder_text = new OptionValueNamer(variant_object).name() productUnitProperties = -> # get relevant product properties - if scope.product.variant_unit_with_scale? - match = scope.product.variant_unit_with_scale.match(/^([^_]+)_([\d\.]+)$/) + if scope.variant.variant_unit_with_scale? + match = scope.variant.variant_unit_with_scale.match(/^([^_]+)_([\d\.]+)$/) if match variant_unit = match[1] variant_unit_scale = parseFloat(match[2]) else - variant_unit = scope.product.variant_unit_with_scale + variant_unit = scope.variant.variant_unit_with_scale variant_unit_scale = null else variant_unit = variant_unit_scale = null @@ -45,4 +44,4 @@ angular.module("ofn.admin").directive "ofnDisplayAs", (OptionValueNamer) -> unit_value = null if isNaN(unit_value) unit_value *= variant_unit_scale if unit_value && variant_unit_scale unit_description = match[3] - [unit_value, unit_description] \ No newline at end of file + [unit_value, unit_description] diff --git a/app/assets/javascripts/admin/directives/maintain_unit_scale.js.coffee b/app/assets/javascripts/admin/directives/maintain_unit_scale.js.coffee deleted file mode 100644 index 55306f6d778..00000000000 --- a/app/assets/javascripts/admin/directives/maintain_unit_scale.js.coffee +++ /dev/null @@ -1,8 +0,0 @@ -angular.module("ofn.admin").directive "ofnMaintainUnitScale", -> - require: "ngModel" - link: (scope, element, attrs, ngModel) -> - scope.$watch 'product.variant_unit_with_scale', (newValue, oldValue) -> - if not (oldValue == newValue) - # Triggers track-variant directive to track the unit_value, so that changes to the unit are passed to the server - ngModel.$setViewValue ngModel.$viewValue - \ No newline at end of file diff --git a/app/assets/javascripts/admin/directives/track_master.js.coffee b/app/assets/javascripts/admin/directives/track_master.js.coffee deleted file mode 100644 index be84e0b2046..00000000000 --- a/app/assets/javascripts/admin/directives/track_master.js.coffee +++ /dev/null @@ -1,8 +0,0 @@ -angular.module("ofn.admin").directive "ofnTrackMaster", (DirtyProducts) -> - require: "ngModel" - link: (scope, element, attrs, ngModel) -> - ngModel.$parsers.push (viewValue) -> - if ngModel.$dirty - DirtyProducts.addMasterProperty scope.product.id, scope.product.master.id, attrs.ofnTrackMaster, viewValue - scope.displayDirtyProducts() - viewValue diff --git a/app/views/spree/admin/products/index/_products_product.html.haml b/app/views/spree/admin/products/index/_products_product.html.haml index b5a778a043b..aa10e5e29ad 100644 --- a/app/views/spree/admin/products/index/_products_product.html.haml +++ b/app/views/spree/admin/products/index/_products_product.html.haml @@ -11,9 +11,6 @@ %td.name{ 'ng-show' => 'columns.name.visible' } %input{ 'ng-model' => "product.name", :name => 'product_name', 'ofn-track-product' => 'name', :type => 'text' } %td.unit{ 'ng-show' => 'columns.unit.visible' } - %select.no-search{ "data-controller": "tom-select", 'ng-model' => 'product.variant_unit_with_scale', :name => 'variant_unit_with_scale', 'ofn-track-product' => 'variant_unit_with_scale', 'ng-options' => 'unit[1] as unit[0] for unit in variant_unit_options' } - %input{ 'ng-model' => 'product.master.unit_value_with_description', :name => 'master_unit_value_with_description', 'ofn-track-master' => 'unit_value_with_description', :type => 'text', :placeholder => 'value', 'ng-show' => "!hasVariants(product) && hasUnit(product)", 'ofn-maintain-unit-scale' => true } - %input{ 'ng-model' => 'product.variant_unit_name', :name => 'variant_unit_name', 'ofn-track-product' => 'variant_unit_name', :placeholder => 'unit', 'ng-show' => "product.variant_unit_with_scale == 'items'", :type => 'text' } %td.display_as{ 'ng-show' => 'columns.unit.visible' } %td.price{ 'ng-show' => 'columns.price.visible' } %input{ 'ng-model' => 'product.price', 'ofn-decimal' => :true, :name => 'price', 'ofn-track-product' => 'price', :type => 'text', 'ng-hide' => 'hasVariants(product)' } diff --git a/app/views/spree/admin/products/index/_products_variant.html.haml b/app/views/spree/admin/products/index/_products_variant.html.haml index 8a5c2c03115..4aa5e13f78d 100644 --- a/app/views/spree/admin/products/index/_products_variant.html.haml +++ b/app/views/spree/admin/products/index/_products_variant.html.haml @@ -10,7 +10,9 @@ %td{ 'ng-show' => 'columns.name.visible' } %input{ 'ng-model' => 'variant.display_name', :name => 'variant_display_name', 'ofn-track-variant' => 'display_name', :type => 'text', placeholder: "{{ product.name }}" } %td.unit_value{ 'ng-show' => 'columns.unit.visible' } - %input{ 'ng-model' => 'variant.unit_value_with_description', :name => 'variant_unit_value_with_description', 'ofn-track-variant' => 'unit_value_with_description', :type => 'text', 'ofn-maintain-unit-scale' => true } + %select.no-search{ "data-controller": "tom-select", 'ng-model' => 'variant.variant_unit_with_scale', :name => 'variant_unit_with_scale', 'ofn-track-variant' => 'variant_unit_with_scale', 'ng-options' => 'unit[1] as unit[0] for unit in variant_unit_options' } + %input{ 'ng-model' => 'variant.unit_value_with_description', :name => 'variant_unit_value_with_description', 'ofn-track-variant' => 'unit_value_with_description', :type => 'text', :placeholder => 'value', 'ng-show' => "hasUnit(variant)" } + %input{ 'ng-model' => 'variant.variant_unit_name', :name => 'variant_unit_name', 'ofn-track-variant' => 'variant_unit_name', :placeholder => 'unit', 'ng-show' => "variant.variant_unit_with_scale == 'items'", :type => 'text' } %td.display_as{ 'ng-show' => 'columns.unit.visible' } %input{ 'ofn-display-as' => 'variant', 'ng-model' => 'variant.display_as', name: 'variant_display_as', 'ofn-track-variant' => 'display_as', type: 'text', placeholder: '{{ placeholder_text }}' } %td{ 'ng-show' => 'columns.price.visible' } diff --git a/spec/javascripts/unit/admin/bulk_product_update_spec.js.coffee b/spec/javascripts/unit/admin/bulk_product_update_spec.js.coffee index 70fe6dac589..532f569d69e 100644 --- a/spec/javascripts/unit/admin/bulk_product_update_spec.js.coffee +++ b/spec/javascripts/unit/admin/bulk_product_update_spec.js.coffee @@ -156,14 +156,20 @@ describe "filtering products for submission to database", -> it "returns variant_unit_with_scale as variant_unit and variant_unit_scale", -> testProduct = id: 1 - variant_unit: 'weight' - variant_unit_scale: 1 - variant_unit_with_scale: 'weight_1' + variants: [ + id: 1 + variant_unit: 'weight' + variant_unit_scale: 1 + variant_unit_with_scale: 'weight_1' + ] expect(filterSubmitProducts([testProduct])).toEqual [ id: 1 - variant_unit: 'weight' - variant_unit_scale: 1 + variants_attributes: [ + id: 1 + variant_unit: 'weight' + variant_unit_scale: 1 + ] ] it "returns stock properties of a product if no variant is provided", -> @@ -204,18 +210,15 @@ describe "filtering products for submission to database", -> display_as: "bottle" display_name: "nothing" producer_id: 5 + variant_unit: 'volume' + variant_unit_scale: 1 + variant_unit_name: 'loaf' + variant_unit_with_scale: 'volume_1' ] - variant_unit: 'volume' - variant_unit_scale: 1 - variant_unit_name: 'loaf' - variant_unit_with_scale: 'volume_1' expect(filterSubmitProducts([testProduct])).toEqual [ id: 1 name: "TestProduct" - variant_unit: 'volume' - variant_unit_scale: 1 - variant_unit_name: 'loaf' variants_attributes: [ id: 1 on_hand: 2 @@ -226,6 +229,9 @@ describe "filtering products for submission to database", -> display_as: "bottle" display_name: "nothing" supplier_id: 5 + variant_unit: 'volume' + variant_unit_scale: 1 + variant_unit_name: 'loaf' ] ] @@ -281,7 +287,6 @@ describe "AdminProductEditCtrl", -> $scope.initialise() expect($scope.q.query).toBe query - expect($scope.q.producerFilter).toBe producerFilter expect($scope.q.categoryFilter).toBe categoryFilter expect($scope.q.sorting).toBe sorting expect($scope.q.importDateFilter).toBe importDateFilter @@ -476,13 +481,13 @@ describe "AdminProductEditCtrl", -> describe "determining whether a product has a unit", -> it "returns true when it does", -> - product = - variant_unit_with_scale: 'weight_1000' - expect($scope.hasUnit(product)).toBe(true) + variant ={variant_unit_with_scale: 'weight_1000'} + + expect($scope.hasUnit(variant)).toBe(true) it "returns false when its unit is undefined", -> - product = {} - expect($scope.hasUnit(product)).toBe(false) + variant = {} + expect($scope.hasUnit(variant)).toBe(false) describe "determining whether a variant has been saved", -> @@ -505,174 +510,163 @@ describe "AdminProductEditCtrl", -> window.bigDecimal = jasmine.createSpyObj "bigDecimal", ["multiply"] window.bigDecimal.multiply.and.callFake (a, b, c) -> (a * b).toFixed(c) - it "extracts variant_unit_with_scale into variant_unit and variant_unit_scale", -> + it "packs each variant", -> + spyOn $scope, "packVariant" + testVariant = {id: 1} testProduct = id: 1 - variant_unit: 'weight' - variant_unit_scale: 1 - variant_unit_with_scale: 'volume_1000' + variants: {1: testVariant} $scope.packProduct(testProduct) - expect(testProduct).toEqual - id: 1 - variant_unit: 'volume' - variant_unit_scale: 1000 - variant_unit_with_scale: 'volume_1000' + expect($scope.packVariant).toHaveBeenCalledWith(testVariant) - it "extracts a null value into null variant_unit and variant_unit_scale", -> - testProduct = + describe "packing variants", -> + beforeEach -> + window.bigDecimal = jasmine.createSpyObj "bigDecimal", ["multiply"] + window.bigDecimal.multiply.and.callFake (a, b, c) -> (a * b).toFixed(c) + + it "extracts variant_unit_with_scale into variant_unit and variant_unit_scale", -> + testVariant = id: 1 variant_unit: 'weight' variant_unit_scale: 1 - variant_unit_with_scale: null + variant_unit_with_scale: 'volume_1000' - $scope.packProduct(testProduct) + $scope.packVariant(testVariant) - expect(testProduct).toEqual + expect(testVariant).toEqual id: 1 - variant_unit: null - variant_unit_scale: null - variant_unit_with_scale: null + variant_unit: 'volume' + variant_unit_scale: 1000 + variant_unit_with_scale: 'volume_1000' it "extracts when variant_unit_with_scale is 'items'", -> - testProduct = + testVariant = id: 1 variant_unit: 'weight' variant_unit_scale: 1 variant_unit_with_scale: 'items' - $scope.packProduct(testProduct) + $scope.packVariant(testVariant) - expect(testProduct).toEqual + expect(testVariant).toEqual id: 1 variant_unit: 'items' variant_unit_scale: null variant_unit_with_scale: 'items' - it "packs each variant", -> - spyOn $scope, "packVariant" - testVariant = {id: 1} - testProduct = - id: 1 - variants: {1: testVariant} - - $scope.packProduct(testProduct) - - expect($scope.packVariant).toHaveBeenCalledWith(testProduct, testVariant) - - describe "packing variants", -> - beforeEach -> - window.bigDecimal = jasmine.createSpyObj "bigDecimal", ["multiply"] - window.bigDecimal.multiply.and.callFake (a, b, c) -> (a * b).toFixed(c) - it "extracts unit_value and unit_description from unit_value_with_description", -> - testProduct = {id: 123, variant_unit_scale: 1.0} testVariant = {unit_value_with_description: "250.5 (bottle)"} - BulkProducts.products = [testProduct] - $scope.products = [testProduct] - $scope.packVariant(testProduct, testVariant) + + $scope.packVariant(testVariant) + expect(testVariant).toEqual unit_value: 250.5 unit_description: "(bottle)" unit_value_with_description: "250.5 (bottle)" it "extracts into unit_value when only a number is provided", -> - testProduct = {id: 123, variant_unit_scale: 1.0} - testVariant = {unit_value_with_description: "250.5"} - BulkProducts.products = [testProduct] - $scope.packVariant(testProduct, testVariant) + testVariant = {variant_unit_scale: 1.0, unit_value_with_description: "250.5"} + + $scope.packVariant(testVariant) + expect(testVariant).toEqual unit_value: 250.5 unit_description: '' unit_value_with_description: "250.5" + variant_unit_scale: 1.0 it "extracts into unit_description when only a string is provided", -> - testProduct = {id: 123} testVariant = {unit_value_with_description: "Medium"} - BulkProducts.products = [testProduct] - $scope.packVariant(testProduct, testVariant) + + $scope.packVariant(testVariant) + expect(testVariant).toEqual unit_value: null unit_description: 'Medium' unit_value_with_description: "Medium" it "extracts into unit_description when a string starting with a number is provided", -> - testProduct = {id: 123} testVariant = {unit_value_with_description: "1kg"} - BulkProducts.products = [testProduct] - $scope.packVariant(testProduct, testVariant) + + $scope.packVariant(testVariant) + expect(testVariant).toEqual unit_value: null unit_description: '1kg' unit_value_with_description: "1kg" it "sets blank values when no value provided", -> - testProduct = {id: 123} testVariant = {unit_value_with_description: ""} - BulkProducts.products = [testProduct] - $scope.packVariant(testProduct, testVariant) + + $scope.packVariant(testVariant) + expect(testVariant).toEqual unit_value: null unit_description: '' unit_value_with_description: "" it "sets nothing when the field is undefined", -> - testProduct = {id: 123} testVariant = {} - BulkProducts.products = [testProduct] - $scope.packVariant(testProduct, testVariant) - expect(testVariant).toEqual {} + + $scope.packVariant(testVariant) + + expect(testVariant).toEqual({}) it "sets zero when the field is zero", -> - testProduct = {id: 123, variant_unit_scale: 1.0} - testVariant = {unit_value_with_description: "0"} - BulkProducts.products = [testProduct] - $scope.packVariant(testProduct, testVariant) + testVariant = {variant_unit_scale: 1.0, unit_value_with_description: "0"} + + $scope.packVariant(testVariant) + expect(testVariant).toEqual unit_value: 0 unit_description: '' unit_value_with_description: "0" + variant_unit_scale: 1.0 it "converts value from chosen unit to base unit", -> - testProduct = {id: 123, variant_unit_scale: 1000} - testVariant = {unit_value_with_description: "250.5"} - BulkProducts.products = [testProduct] - $scope.packVariant(testProduct, testVariant) + testVariant = {variant_unit_scale: 1000, unit_value_with_description: "250.5"} + + $scope.packVariant(testVariant) + expect(testVariant).toEqual unit_value: 250500 unit_description: '' unit_value_with_description: "250.5" + variant_unit_scale: 1000 it "does not convert value when using a non-scaled unit", -> - testProduct = {id: 123} testVariant = {unit_value_with_description: "12"} - BulkProducts.products = [testProduct] - $scope.packVariant(testProduct, testVariant) + + $scope.packVariant(testVariant) + expect(testVariant).toEqual unit_value: 12 unit_description: '' unit_value_with_description: "12" it "converts unit_value into a float when a comma separated number is provided", -> - testProduct = {id: 123, variant_unit_scale: 1.0} - testVariant = {unit_value_with_description: "250,5"} - BulkProducts.products = [testProduct] - $scope.packVariant(testProduct, testVariant) + testVariant = {variant_unit_scale: 1.0, unit_value_with_description: "250,5"} + + $scope.packVariant(testVariant) + expect(testVariant).toEqual unit_value: 250.5 unit_description: '' unit_value_with_description: "250,5" + variant_unit_scale: 1.0 it "rounds off the unit_value upto 2 decimal places", -> - testProduct = {id: 123, variant_unit_scale: 1.0} - testVariant = {unit_value_with_description: "1234.567"} - BulkProducts.products = [testProduct] - $scope.packVariant(testProduct, testVariant) + testVariant = {variant_unit_scale: 1.0, unit_value_with_description: "1234.567"} + + $scope.packVariant(testVariant) + expect(testVariant).toEqual unit_value: 1234.57 unit_description: '' unit_value_with_description: "1234.567" + variant_unit_scale: 1.0 describe "filtering products", -> From 94030527a47a05a505f3ca0c85a01d5a4254dc4c Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou Date: Wed, 10 Jul 2024 16:37:47 +1000 Subject: [PATCH 23/68] Update jest configuration to include webpacker dir This allows the test environment to correctly resolve import of services in controller ie: `import OptionValueNamer from "js/services/option_value_namer";` The added benefit is we can now import package to be tested directly without having to specify the whole relative path ie in test file you can do : `import variant_controller from "controllers/variant_controller";` --- jest.config.js | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/jest.config.js b/jest.config.js index 4c914cf1f8d..18863711393 100644 --- a/jest.config.js +++ b/jest.config.js @@ -66,9 +66,7 @@ module.exports = { // maxWorkers: "50%", // An array of directory names to be searched recursively up from the requiring module's location - // moduleDirectories: [ - // "node_modules" - // ], + moduleDirectories: ["node_modules", "app/webpacker"], // An array of file extensions your modules use // moduleFileExtensions: [ @@ -157,9 +155,7 @@ module.exports = { // ], // The regexp pattern or array of patterns that Jest uses to detect test files - "testRegex": [ - "spec/javascripts/.*_test\\.js" - ], + testRegex: ["spec/javascripts/.*_test\\.js"], // This option allows the use of a custom results processor // testResultsProcessor: undefined, @@ -177,9 +173,7 @@ module.exports = { // transform: undefined, // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation - "transformIgnorePatterns": [ - "/node_modules/(?!(stimulus)/)" - ] + transformIgnorePatterns: ["/node_modules/(?!(stimulus)/)"], // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them // unmockedModulePathPatterns: undefined, From e8234ee4a0fc79c455c5c821b5b3208f045244e9 Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou Date: Fri, 12 Jul 2024 11:02:16 +1000 Subject: [PATCH 24/68] Fix Bulk products edit page , part 1 --- .../admin/products_v3/_product_row.html.haml | 14 +-- .../admin/products_v3/_variant_row.html.haml | 14 ++- app/views/admin/products_v3/index.html.haml | 2 +- .../controllers/product_controller.js | 38 -------- .../controllers/variant_controller.js | 42 ++++++--- .../js/services/option_value_namer.js | 17 ++-- .../services/option_value_namer_test.js | 53 ++++++----- .../stimulus/product_controller_test.js | 56 ------------ .../stimulus/variant_controller_test.js | 87 +++++++++++++++++++ 9 files changed, 168 insertions(+), 155 deletions(-) delete mode 100644 app/webpacker/controllers/product_controller.js delete mode 100644 spec/javascripts/stimulus/product_controller_test.js create mode 100644 spec/javascripts/stimulus/variant_controller_test.js diff --git a/app/views/admin/products_v3/_product_row.html.haml b/app/views/admin/products_v3/_product_row.html.haml index edd9d406b67..510ad506055 100644 --- a/app/views/admin/products_v3/_product_row.html.haml +++ b/app/views/admin/products_v3/_product_row.html.haml @@ -7,18 +7,8 @@ %td.col-sku.field.naked_inputs = f.text_field :sku, 'aria-label': t('admin.products_page.columns.sku') = error_message_on product, :sku -%td.col-unit_scale.field.naked_inputs{ 'data-controller': 'toggle-control', 'data-toggle-control-match-value': 'items' } - = f.hidden_field :variant_unit - = f.hidden_field :variant_unit_scale - = f.select :variant_unit_with_scale, - options_for_select(WeightsAndMeasures.variant_unit_options, product.variant_unit_with_scale), - {}, - class: "fullwidth no-input", - 'aria-label': t('admin.products_page.columns.unit_scale'), - data: { "controller": "tom-select", "tom-select-options-value": '{ "plugins": [] }', action: "change->toggle-control#displayIfMatch"} - .field - = f.text_field :variant_unit_name, 'aria-label': t('items'), 'data-toggle-control-target': 'control', style: (product.variant_unit == "items" ? "" : "display: none") - = error_message_on product, :variant_unit_name, 'data-toggle-control-target': 'control' +%td.col-unit_scale.align-right + -# empty %td.col-unit.align-right -# empty %td.col-price.align-right diff --git a/app/views/admin/products_v3/_variant_row.html.haml b/app/views/admin/products_v3/_variant_row.html.haml index 0db0b5873c2..92d6eaaec8a 100644 --- a/app/views/admin/products_v3/_variant_row.html.haml +++ b/app/views/admin/products_v3/_variant_row.html.haml @@ -7,8 +7,18 @@ %td.col-sku.field.naked_inputs = f.text_field :sku, 'aria-label': t('admin.products_page.columns.sku') = error_message_on variant, :sku -%td.col-unit_scale - -# empty +%td.col-unir_scale.field.naked_inputs{ 'data-controller': 'toggle-control', 'data-toggle-control-match-value': 'items' } + = f.hidden_field :variant_unit + = f.hidden_field :variant_unit_scale + = f.select :variant_unit_with_scale, + options_for_select(WeightsAndMeasures.variant_unit_options, variant.variant_unit_with_scale), + {}, + class: "fullwidth no-input", + 'aria-label': t('admin.products_page.columns.unit_scale'), + data: { "controller": "tom-select", "tom-select-options-value": '{ "plugins": [] }', action: "change->toggle-control#displayIfMatch"} + .field + = f.text_field :variant_unit_name, 'aria-label': t('items'), 'data-toggle-control-target': 'control', style: (variant.variant_unit == "items" ? "" : "display: none") + = error_message_on variant, :variant_unit_name, 'data-toggle-control-target': 'control' %td.col-unit.field.popout{'data-controller': "popout", 'data-popout-update-display-value': "false"} = f.button :unit_to_display, class: "popout__button", 'aria-label': t('admin.products_page.columns.unit'), 'data-popout-target': "button" do = variant.unit_to_display # Show the generated summary of unit values diff --git a/app/views/admin/products_v3/index.html.haml b/app/views/admin/products_v3/index.html.haml index fed068a609f..dfa010fdc85 100644 --- a/app/views/admin/products_v3/index.html.haml +++ b/app/views/admin/products_v3/index.html.haml @@ -11,7 +11,7 @@ = render partial: 'spree/admin/shared/product_sub_menu' -#products_v3_page{ "data-controller": "products", 'data-turbo': true } +#products_v3_page{ 'data-turbo': true } = render partial: "content", locals: { products: @products, pagy: @pagy, search_term: @search_term, producer_options: producers, producer_id: @producer_id, category_options: categories, category_id: @category_id, diff --git a/app/webpacker/controllers/product_controller.js b/app/webpacker/controllers/product_controller.js deleted file mode 100644 index 942f3b02e1d..00000000000 --- a/app/webpacker/controllers/product_controller.js +++ /dev/null @@ -1,38 +0,0 @@ -import { Controller } from "stimulus"; - -// Dynamically update related Product unit fields (expected to move to Variant due to Product Refactor) -// -export default class ProductController extends Controller { - connect() { - // idea: create a helper that includes a nice getter/setter for Rails model attr values, just pass it the attribute name. - // It could automatically find (and cache a ref to) each dom element and get/set the values. - this.variantUnit = this.element.querySelector('[name$="[variant_unit]"]'); - this.variantUnitScale = this.element.querySelector('[name$="[variant_unit_scale]"]'); - this.variantUnitWithScale = this.element.querySelector('[name$="[variant_unit_with_scale]"]'); - - // on variant_unit_with_scale changed; update variant_unit and variant_unit_scale - this.variantUnitWithScale.addEventListener("change", this.#updateUnitAndScale.bind(this), { - passive: true, - }); - } - - // private - - // Extract variant_unit and variant_unit_scale from dropdown variant_unit_with_scale, - // and update hidden product fields - #updateUnitAndScale(event) { - const variant_unit_with_scale = this.variantUnitWithScale.value; - const match = variant_unit_with_scale.match(/^([^_]+)_([\d\.]+)$/); // eg "weight_1000" - - if (match) { - this.variantUnit.value = match[1]; - this.variantUnitScale.value = parseFloat(match[2]); - } else { - // "items" - this.variantUnit.value = variant_unit_with_scale; - this.variantUnitScale.value = ""; - } - this.variantUnit.dispatchEvent(new Event("change")); - this.variantUnitScale.dispatchEvent(new Event("change")); - } -} diff --git a/app/webpacker/controllers/variant_controller.js b/app/webpacker/controllers/variant_controller.js index 07780e25319..b39d6f73131 100644 --- a/app/webpacker/controllers/variant_controller.js +++ b/app/webpacker/controllers/variant_controller.js @@ -5,11 +5,17 @@ import OptionValueNamer from "js/services/option_value_namer"; // export default class VariantController extends Controller { connect() { - // Assuming these will be available on the variant soon, just a quick hack to find the product fields: - const product = this.element.closest("[data-record-id]"); - this.variantUnit = product.querySelector('[name$="[variant_unit]"]'); - this.variantUnitScale = product.querySelector('[name$="[variant_unit_scale]"]'); - this.variantUnitName = product.querySelector('[name$="[variant_unit_name]"]'); + // idea: create a helper that includes a nice getter/setter for Rails model attr values, just pass it the attribute name. + // It could automatically find (and cache a ref to) each dom element and get/set the values. + this.variantUnit = this.element.querySelector('[name$="[variant_unit]"]'); + this.variantUnitScale = this.element.querySelector('[name$="[variant_unit_scale]"]'); + this.variantUnitName = this.element.querySelector('[name$="[variant_unit_name]"]'); + this.variantUnitWithScale = this.element.querySelector('[name$="[variant_unit_with_scale]"]'); + + // on variant_unit_with_scale changed; update variant_unit and variant_unit_scale + this.variantUnitWithScale.addEventListener("change", this.#updateUnitAndScale.bind(this), { + passive: true, + }); this.unitValue = this.element.querySelector('[name$="[unit_value]"]'); this.unitDescription = this.element.querySelector('[name$="[unit_description]"]'); @@ -76,11 +82,27 @@ export default class VariantController extends Controller { return { unit_value: parseFloat(this.unitValue.value), unit_description: this.unitDescription.value, - product: { - variant_unit: this.variantUnit.value, - variant_unit_scale: parseFloat(this.variantUnitScale.value), - variant_unit_name: this.variantUnitName.value, - }, + variant_unit: this.variantUnit.value, + variant_unit_scale: parseFloat(this.variantUnitScale.value), + variant_unit_name: this.variantUnitName.value, }; } + + // Extract variant_unit and variant_unit_scale from dropdown variant_unit_with_scale, + // and update hidden product fields + #updateUnitAndScale(event) { + const variant_unit_with_scale = this.variantUnitWithScale.value; + const match = variant_unit_with_scale.match(/^([^_]+)_([\d\.]+)$/); // eg "weight_1000" + + if (match) { + this.variantUnit.value = match[1]; + this.variantUnitScale.value = parseFloat(match[2]); + } else { + // "items" + this.variantUnit.value = variant_unit_with_scale; + this.variantUnitScale.value = ""; + } + this.variantUnit.dispatchEvent(new Event("change")); + this.variantUnitScale.dispatchEvent(new Event("change")); + } } diff --git a/app/webpacker/js/services/option_value_namer.js b/app/webpacker/js/services/option_value_namer.js index ba4b12fd961..e57b869a059 100644 --- a/app/webpacker/js/services/option_value_namer.js +++ b/app/webpacker/js/services/option_value_namer.js @@ -1,4 +1,4 @@ -import VariantUnitManager from "../../js/services/variant_unit_manager"; +import VariantUnitManager from "js/services/variant_unit_manager"; // Javascript clone of VariantUnits::OptionValueNamer, for bulk product editing. export default class OptionValueNamer { @@ -24,17 +24,17 @@ export default class OptionValueNamer { } value_scaled() { - return !!this.variant.product.variant_unit_scale; + return !!this.variant.variant_unit_scale; } option_value_value_unit() { let value, unit_name; if (this.variant.unit_value) { - if (this.variant.product.variant_unit === "weight" || this.variant.product.variant_unit === "volume") { + if (this.variant.variant_unit === "weight" || this.variant.variant_unit === "volume") { [value, unit_name] = this.option_value_value_unit_scaled(); } else { value = this.variant.unit_value; - unit_name = this.pluralize(this.variant.product.variant_unit_name, value); + unit_name = this.pluralize(this.variant.variant_unit_name, value); } if (value == parseInt(value, 10)) { value = parseInt(value, 10); @@ -83,16 +83,17 @@ export default class OptionValueNamer { // to >= 1 when expressed in it. // If there is none available where this is true, use the smallest // available unit. - const product = this.variant.product; - const scales = this.variantUnitManager.compatibleUnitScales(product.variant_unit_scale, product.variant_unit); + const scales = this.variantUnitManager.compatibleUnitScales( + this.variant.variant_unit_scale, this.variant.variant_unit + ); const variantUnitValue = this.variant.unit_value; // sets largestScale = last element in filtered scales array const largestScale = scales.filter(s => variantUnitValue / s >= 1).slice(-1)[0]; if (largestScale) { - return [largestScale, this.variantUnitManager.getUnitName(largestScale, product.variant_unit)]; + return [largestScale, this.variantUnitManager.getUnitName(largestScale, this.variant.variant_unit)]; } else { - return [scales[0], this.variantUnitManager.getUnitName(scales[0], product.variant_unit)]; + return [scales[0], this.variantUnitManager.getUnitName(scales[0], this.variant.variant_unit)]; } } } diff --git a/spec/javascripts/services/option_value_namer_test.js b/spec/javascripts/services/option_value_namer_test.js index e7878adae10..02b3efaeddd 100644 --- a/spec/javascripts/services/option_value_namer_test.js +++ b/spec/javascripts/services/option_value_namer_test.js @@ -2,7 +2,7 @@ * @jest-environment jsdom */ -import OptionValueNamer from "../../../app/webpacker/js/services/option_value_namer"; +import OptionValueNamer from "js/services/option_value_namer"; describe("OptionValueNamer", () => { beforeAll(() => { @@ -53,14 +53,12 @@ describe("OptionValueNamer", () => { }); describe("determining if a variant's value is scaled", function() { - var p; beforeEach(function() { - p = {}; - v = { product: p }; + v = {}; namer = new OptionValueNamer(v); }); it("returns true when the product has a scale", function() { - p.variant_unit_scale = 1000; + v.variant_unit_scale = 1000; expect(namer.value_scaled()).toBe(true); }); it("returns false otherwise", function() { @@ -69,7 +67,7 @@ describe("OptionValueNamer", () => { }); describe("generating option value's value and unit", function() { - var v, p, namer; + var v, namer; // Mock I18n. TODO: moved to a shared helper beforeAll(() => { @@ -84,40 +82,39 @@ describe("OptionValueNamer", () => { }) beforeEach(function() { - p = {}; - v = { product: p }; + v = {}; namer = new OptionValueNamer(v); }); it("generates simple values", function() { - p.variant_unit = 'weight'; - p.variant_unit_scale = 1.0; + v.variant_unit = 'weight'; + v.variant_unit_scale = 1.0; v.unit_value = 100; expect(namer.option_value_value_unit()).toEqual([100, 'g']); }); it("generates values when unit value is non-integer", function() { - p.variant_unit = 'weight'; - p.variant_unit_scale = 1.0; + v.variant_unit = 'weight'; + v.variant_unit_scale = 1.0; v.unit_value = 123.45; expect(namer.option_value_value_unit()).toEqual([123.45, 'g']); }); it("returns a value of 1 when unit value equals the scale", function() { - p.variant_unit = 'weight'; - p.variant_unit_scale = 1000.0; + v.variant_unit = 'weight'; + v.variant_unit_scale = 1000.0; v.unit_value = 1000.0; expect(namer.option_value_value_unit()).toEqual([1, 'kg']); }); it("generates values for all weight scales", function() { [[1.0, 'g'], [1000.0, 'kg'], [1000000.0, 'T']].forEach(([scale, unit]) => { - p.variant_unit = 'weight'; - p.variant_unit_scale = scale; + v.variant_unit = 'weight'; + v.variant_unit_scale = scale; v.unit_value = 100 * scale; expect(namer.option_value_value_unit()).toEqual([100, unit]); }); }); it("generates values for all volume scales", function() { [[0.001, 'mL'], [1.0, 'L'], [1000.0, 'kL']].forEach(([scale, unit]) => { - p.variant_unit = 'volume'; - p.variant_unit_scale = scale; + v.variant_unit = 'volume'; + v.variant_unit_scale = scale; v.unit_value = 100 * scale; expect(namer.option_value_value_unit()).toEqual([100, unit]); }); @@ -125,14 +122,14 @@ describe("OptionValueNamer", () => { it("generates right values for volume with rounded values", function() { var unit; unit = 'L'; - p.variant_unit = 'volume'; - p.variant_unit_scale = 1.0; + v.variant_unit = 'volume'; + v.variant_unit_scale = 1.0; v.unit_value = 0.7; expect(namer.option_value_value_unit()).toEqual([700, 'mL']); }); it("chooses the correct scale when value is very small", function() { - p.variant_unit = 'volume'; - p.variant_unit_scale = 0.001; + v.variant_unit = 'volume'; + v.variant_unit_scale = 0.001; v.unit_value = 0.0001; expect(namer.option_value_value_unit()).toEqual([0.1, 'mL']); }); @@ -145,16 +142,16 @@ describe("OptionValueNamer", () => { // subject.option_value_value_unit.should == [100, unit.pluralize] }); it("generates singular values for item units when value is 1", function() { - p.variant_unit = 'items'; - p.variant_unit_scale = null; - p.variant_unit_name = 'packet'; + v.variant_unit = 'items'; + v.variant_unit_scale = null; + v.variant_unit_name = 'packet'; v.unit_value = 1; expect(namer.option_value_value_unit()).toEqual([1, 'packet']); }); it("returns [null, null] when unit value is not set", function() { - p.variant_unit = 'items'; - p.variant_unit_scale = null; - p.variant_unit_name = 'foo'; + v.variant_unit = 'items'; + v.variant_unit_scale = null; + v.variant_unit_name = 'foo'; v.unit_value = null; expect(namer.option_value_value_unit()).toEqual([null, null]); }); diff --git a/spec/javascripts/stimulus/product_controller_test.js b/spec/javascripts/stimulus/product_controller_test.js deleted file mode 100644 index bcd4f8bdad8..00000000000 --- a/spec/javascripts/stimulus/product_controller_test.js +++ /dev/null @@ -1,56 +0,0 @@ -/** - * @jest-environment jsdom - */ - -import { Application } from "stimulus"; -import product_controller from "../../../app/webpacker/controllers/product_controller"; - -describe("ProductController", () => { - beforeAll(() => { - const application = Application.start(); - application.register("product", product_controller); - }); - - describe("variant_unit_with_scale", () => { - beforeEach(() => { - document.body.innerHTML = ` -
- - - -
- `; - }); - - describe("change", () => { - it("weight_1000", () => { - variant_unit_with_scale.selectedIndex = 1; - variant_unit_with_scale.dispatchEvent(new Event("change")); - - expect(variant_unit.value).toBe("weight"); - expect(variant_unit_scale.value).toBe("1000"); - }); - - it("volume_4.54609", () => { - variant_unit_with_scale.selectedIndex = 2; - variant_unit_with_scale.dispatchEvent(new Event("change")); - - expect(variant_unit.value).toBe("volume"); - expect(variant_unit_scale.value).toBe("4.54609"); - }); - - it("items", () => { - variant_unit_with_scale.selectedIndex = 3; - variant_unit_with_scale.dispatchEvent(new Event("change")); - - expect(variant_unit.value).toBe("items"); - expect(variant_unit_scale.value).toBe(""); - }); - }) - }); -}); diff --git a/spec/javascripts/stimulus/variant_controller_test.js b/spec/javascripts/stimulus/variant_controller_test.js new file mode 100644 index 00000000000..4f40dde81c3 --- /dev/null +++ b/spec/javascripts/stimulus/variant_controller_test.js @@ -0,0 +1,87 @@ +/** + * @jest-environment jsdom + */ + +import { Application } from "stimulus"; +import variant_controller from "controllers/variant_controller"; + + +describe("VariantController", () => { + beforeAll(() => { + // Requires global var from page + global.ofn_available_units_sorted = { + "weight": { + "1.0":{"name":"g","system":"metric"}, + "1000.0":{"name":"kg","system":"metric"}, + "1000000.0":{"name":"T","system":"metric"} + }, + "volume":{ + "0.001":{"name":"mL","system":"metric"}, + "1.0":{"name":"L","system":"metric"}, + "4.54609":{"name":"gal","system":"imperial"}, + "1000.0":{"name":"kL","system":"metric"} + } + }; + + const mockedT = jest.fn(); + mockedT.mockImplementation((string, opts) => (string + ', ' + JSON.stringify(opts))); + + global.I18n = { t: mockedT }; + + const application = Application.start(); + application.register("variant", variant_controller); + }); + + afterAll(() => { + delete global.I18n; + }) + + describe("variant_unit_with_scale", () => { + beforeEach(() => { + document.body.innerHTML = ` +
+ + + + + + + + + +
+ `; + }); + + describe("change", () => { + it("weight_1000", () => { + variant_unit_with_scale.selectedIndex = 1; + variant_unit_with_scale.dispatchEvent(new Event("change")); + + expect(variant_unit.value).toBe("weight"); + expect(variant_unit_scale.value).toBe("1000"); + }); + + it("volume_4.54609", () => { + variant_unit_with_scale.selectedIndex = 2; + variant_unit_with_scale.dispatchEvent(new Event("change")); + + expect(variant_unit.value).toBe("volume"); + expect(variant_unit_scale.value).toBe("4.54609"); + }); + + it("items", () => { + variant_unit_with_scale.selectedIndex = 3; + variant_unit_with_scale.dispatchEvent(new Event("change")); + + expect(variant_unit.value).toBe("items"); + expect(variant_unit_scale.value).toBe(""); + }); + }) + }); +}); From 768825d689f6b0ace4948846982eb9d9d33008c4 Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou Date: Fri, 12 Jul 2024 14:54:12 +1000 Subject: [PATCH 25/68] Fix Bulk Edit product part 2 Note, the empty entry for unit scale need a css fix , currently showing at half height --- .../admin/products_v3/_variant_row.html.haml | 7 ++- spec/system/admin/products_v3/actions_spec.rb | 11 ++--- spec/system/admin/products_v3/create_spec.rb | 5 ++- spec/system/admin/products_v3/update_spec.rb | 43 +++++++++---------- 4 files changed, 32 insertions(+), 34 deletions(-) diff --git a/app/views/admin/products_v3/_variant_row.html.haml b/app/views/admin/products_v3/_variant_row.html.haml index 92d6eaaec8a..25a387674c5 100644 --- a/app/views/admin/products_v3/_variant_row.html.haml +++ b/app/views/admin/products_v3/_variant_row.html.haml @@ -12,10 +12,9 @@ = f.hidden_field :variant_unit_scale = f.select :variant_unit_with_scale, options_for_select(WeightsAndMeasures.variant_unit_options, variant.variant_unit_with_scale), - {}, - class: "fullwidth no-input", - 'aria-label': t('admin.products_page.columns.unit_scale'), - data: { "controller": "tom-select", "tom-select-options-value": '{ "plugins": [] }', action: "change->toggle-control#displayIfMatch"} + { include_blank: true }, + { class: "fullwidth no-input", 'aria-label': t('admin.products_page.columns.unit_scale'), data: { "controller": "tom-select", "tom-select-options-value": '{ "plugins": [] }', action: "change->toggle-control#displayIfMatch" } } + = error_message_on variant, :variant_unit, 'data-toggle-control-target': 'control' .field = f.text_field :variant_unit_name, 'aria-label': t('items'), 'data-toggle-control-target': 'control', style: (variant.variant_unit == "items" ? "" : "display: none") = error_message_on variant, :variant_unit_name, 'data-toggle-control-target': 'control' diff --git a/spec/system/admin/products_v3/actions_spec.rb b/spec/system/admin/products_v3/actions_spec.rb index 368f6d85950..43b776f05f8 100644 --- a/spec/system/admin/products_v3/actions_spec.rb +++ b/spec/system/admin/products_v3/actions_spec.rb @@ -111,13 +111,10 @@ def save_preferences let!(:variant_a1) { product_a.variants.first.tap{ |v| v.update! display_name: "Medium box", sku: "APL-01", price: 5.25, on_hand: 5, - on_demand: false - } - } - let!(:product_a) { - create(:simple_product, name: "Apples", sku: "APL-00", - variant_unit: "weight", variant_unit_scale: 1) # Grams + on_demand: false, variant_unit: "weight", variant_unit_scale: 1 + } # Grams } + let!(:product_a) { create(:simple_product, name: "Apples", sku: "APL-00") } context "when they are under 11" do before do @@ -299,7 +296,7 @@ def save_preferences it "shows error message when cloning invalid record" do # Existing product is invalid: - product_a.update_columns(variant_unit: nil) + product_a.update_columns(name: nil) click_product_clone "Apples" diff --git a/spec/system/admin/products_v3/create_spec.rb b/spec/system/admin/products_v3/create_spec.rb index 9683ebe2a31..d6db98d60c5 100644 --- a/spec/system/admin/products_v3/create_spec.rb +++ b/spec/system/admin/products_v3/create_spec.rb @@ -38,7 +38,7 @@ end describe "creating new variants" do - let!(:product) { create(:product, variant_unit: 'weight', variant_unit_scale: 1000) } + let!(:product) { create(:product) } before { visit_products_page_as_admin } @@ -80,6 +80,7 @@ within page.all("tr.condensed")[1] do # selects second variant row find('input[id$="_sku"]').fill_in with: "345" find('input[id$="_display_name"]').fill_in with: "Small bag" + tomselect_select "Weight (g)", from: "Unit scale" find('button[id$="unit_to_display"]').click # opens the unit value pop out find('input[id$="_unit_value_with_description"]').fill_in with: "0.002" find('input[id$="_display_as"]').fill_in with: "2 grams" @@ -111,6 +112,8 @@ new_variant = Spree::Variant.where(deleted_at: nil).last expect(new_variant.sku).to eq "345" expect(new_variant.display_name).to eq "Small bag" + expect(new_variant.variant_unit).to eq "weight" + expect(new_variant.variant_unit_scale).to eq 1 # g expect(new_variant.unit_value).to eq 2.0 expect(new_variant.display_as).to eq "2 grams" expect(new_variant.unit_presentation).to eq "2 grams" diff --git a/spec/system/admin/products_v3/update_spec.rb b/spec/system/admin/products_v3/update_spec.rb index 57245ecb6b4..73380fbdfff 100644 --- a/spec/system/admin/products_v3/update_spec.rb +++ b/spec/system/admin/products_v3/update_spec.rb @@ -25,22 +25,20 @@ let!(:variant_a1) { product_a.variants.first.tap{ |v| v.update! display_name: "Medium box", sku: "APL-01", price: 5.25, on_hand: 5, - on_demand: false - } + on_demand: false, variant_unit: "weight", variant_unit_scale: 1 + } # Grams } let!(:product_a) { - create(:simple_product, name: "Apples", sku: "APL-00", - variant_unit: "weight", variant_unit_scale: 1) # Grams + create(:simple_product, name: "Apples", sku: "APL-00" ) } let(:variant_b1) { product_b.variants.first.tap{ |v| v.update! display_name: "Medium box", sku: "TMT-01", price: 5, on_hand: 5, - on_demand: false - } + on_demand: false, variant_unit: "weight", variant_unit_scale: 1 + } # Grams } let(:product_b) { - create(:simple_product, name: "Tomatoes", sku: "TMT-01", - variant_unit: "weight", variant_unit_scale: 1) # Grams + create(:simple_product, name: "Tomatoes", sku: "TMT-01") } before do visit admin_products_url @@ -50,11 +48,16 @@ within row_containing_name("Apples") do fill_in "Name", with: "Pommes" fill_in "SKU", with: "POM-00" + end + within row_containing_name("Medium box") do + fill_in "Name", with: "Large box" + fill_in "SKU", with: "POM-01" + tomselect_select "Volume (mL)", from: "Unit scale" + click_on "Unit" # activate popout end # Unit popout - click_on "Unit" # activate popout # have to use below method to trigger the +change+ event, # +fill_in "Unit value", with: ""+ does not trigger +change+ event find_field('Unit value').send_keys(:control, 'a', :backspace) # empty the field @@ -85,13 +88,13 @@ variant_a1.reload }.to change { product_a.name }.to("Pommes") .and change{ product_a.sku }.to("POM-00") - .and change{ product_a.variant_unit }.to("volume") - .and change{ product_a.variant_unit_scale }.to(0.001) .and change{ variant_a1.display_name }.to("Large box") .and change{ variant_a1.sku }.to("POM-01") .and change{ variant_a1.unit_value }.to(0.5001) # volumes are stored in litres .and change{ variant_a1.price }.to(10.25) .and change{ variant_a1.on_hand }.to(6) + .and change{ variant_a1.variant_unit }.to("volume") + .and change{ variant_a1.variant_unit_scale }.to(0.001) within row_containing_name("Pommes") do expect(page).to have_field "Name", with: "Pommes" @@ -130,11 +133,7 @@ it "saves unit values using the new scale" do within row_containing_name("Medium box") do expect(page).to have_button "Unit", text: "1g" - end - within row_containing_name("Apples") do tomselect_select "Weight (kg)", from: "Unit scale" - end - within row_containing_name("Medium box") do # New scale is visible immediately expect(page).to have_button "Unit", text: "1kg" end @@ -142,10 +141,10 @@ click_button "Save changes" expect(page).to have_content "Changes saved" - product_a.reload - expect(product_a.variant_unit).to eq "weight" - expect(product_a.variant_unit_scale).to eq 1000 # kg - expect(variant_a1.reload.unit_value).to eq 1000 # 1kg + variant_a1.reload + expect(variant_a1.variant_unit).to eq "weight" + expect(variant_a1.variant_unit_scale).to eq 1000 # kg + expect(variant_a1.unit_value).to eq 1000 # 1kg within row_containing_name("Medium box") do expect(page).to have_button "Unit", text: "1kg" @@ -153,7 +152,7 @@ end it "saves a custom item unit name" do - within row_containing_name("Apples") do + within row_containing_name("Medium box") do tomselect_select "Items", from: "Unit scale" fill_in "Items", with: "box" end @@ -163,8 +162,8 @@ expect(page).to have_content "Changes saved" product_a.reload - }.to change{ product_a.variant_unit }.to("items") - .and change{ product_a.variant_unit_name }.to("box") + }.to change{ variant_a1.variant_unit }.to("items") + .and change{ variant_a1.variant_unit_name }.to("box") within row_containing_name("Apples") do pending "#12005" From 4cd83d3fd47e2956e86f21213213c888c380c4f8 Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou Date: Mon, 15 Jul 2024 10:07:36 +1000 Subject: [PATCH 26/68] Prettify javascript Also update .prettierignore so that spec files get prettified as well --- .prettierignore | 2 +- .../controllers/variant_controller.js | 4 +- .../js/services/option_value_namer.js | 17 ++- .../services/option_value_namer_test.js | 122 ++++++++++-------- .../stimulus/variant_controller_test.js | 29 ++--- 5 files changed, 98 insertions(+), 76 deletions(-) diff --git a/.prettierignore b/.prettierignore index e6c0ce1c7f5..ed5fb01b740 100644 --- a/.prettierignore +++ b/.prettierignore @@ -5,6 +5,7 @@ *.yaml *.json *.html +**/*.rb # JS # Enabled: app/webpacker/controllers/*.js and app/webpacker/packs/*.js @@ -27,6 +28,5 @@ postcss.config.js /coverage/ /engines/ /public/ -/spec/ /tmp/ /vendor/ diff --git a/app/webpacker/controllers/variant_controller.js b/app/webpacker/controllers/variant_controller.js index b39d6f73131..5a529a338f7 100644 --- a/app/webpacker/controllers/variant_controller.js +++ b/app/webpacker/controllers/variant_controller.js @@ -11,7 +11,7 @@ export default class VariantController extends Controller { this.variantUnitScale = this.element.querySelector('[name$="[variant_unit_scale]"]'); this.variantUnitName = this.element.querySelector('[name$="[variant_unit_name]"]'); this.variantUnitWithScale = this.element.querySelector('[name$="[variant_unit_with_scale]"]'); - + // on variant_unit_with_scale changed; update variant_unit and variant_unit_scale this.variantUnitWithScale.addEventListener("change", this.#updateUnitAndScale.bind(this), { passive: true, @@ -87,7 +87,7 @@ export default class VariantController extends Controller { variant_unit_name: this.variantUnitName.value, }; } - + // Extract variant_unit and variant_unit_scale from dropdown variant_unit_with_scale, // and update hidden product fields #updateUnitAndScale(event) { diff --git a/app/webpacker/js/services/option_value_namer.js b/app/webpacker/js/services/option_value_namer.js index e57b869a059..f0eaf8ea519 100644 --- a/app/webpacker/js/services/option_value_namer.js +++ b/app/webpacker/js/services/option_value_namer.js @@ -9,7 +9,7 @@ export default class OptionValueNamer { name() { const [value, unit] = this.option_value_value_unit(); - const separator = this.value_scaled() ? '' : ' '; + const separator = this.value_scaled() ? "" : " "; const name_fields = []; if (value && unit) { name_fields.push(`${value}${separator}${unit}`); @@ -20,7 +20,7 @@ export default class OptionValueNamer { if (this.variant.unit_description) { name_fields.push(this.variant.unit_description); } - return name_fields.join(' '); + return name_fields.join(" "); } value_scaled() { @@ -55,7 +55,7 @@ export default class OptionValueNamer { } return I18n.t(["inflections", unit_key], { count: count, - defaultValue: unit_name + defaultValue: unit_name, }); } @@ -84,17 +84,20 @@ export default class OptionValueNamer { // If there is none available where this is true, use the smallest // available unit. const scales = this.variantUnitManager.compatibleUnitScales( - this.variant.variant_unit_scale, this.variant.variant_unit + this.variant.variant_unit_scale, + this.variant.variant_unit, ); const variantUnitValue = this.variant.unit_value; // sets largestScale = last element in filtered scales array - const largestScale = scales.filter(s => variantUnitValue / s >= 1).slice(-1)[0]; + const largestScale = scales.filter((s) => variantUnitValue / s >= 1).slice(-1)[0]; if (largestScale) { - return [largestScale, this.variantUnitManager.getUnitName(largestScale, this.variant.variant_unit)]; + return [ + largestScale, + this.variantUnitManager.getUnitName(largestScale, this.variant.variant_unit), + ]; } else { return [scales[0], this.variantUnitManager.getUnitName(scales[0], this.variant.variant_unit)]; } } } - diff --git a/spec/javascripts/services/option_value_namer_test.js b/spec/javascripts/services/option_value_namer_test.js index 02b3efaeddd..61f13ebd970 100644 --- a/spec/javascripts/services/option_value_namer_test.js +++ b/spec/javascripts/services/option_value_namer_test.js @@ -7,133 +7,153 @@ import OptionValueNamer from "js/services/option_value_namer"; describe("OptionValueNamer", () => { beforeAll(() => { // Requires global var from page - global.ofn_available_units_sorted = {"weight":{"1.0":{"name":"g","system":"metric"},"1000.0":{"name":"kg","system":"metric"},"1000000.0":{"name":"T","system":"metric"}},"volume":{"0.001":{"name":"mL","system":"metric"},"1.0":{"name":"L","system":"metric"},"4.54609":{"name":"gal","system":"imperial"},"1000.0":{"name":"kL","system":"metric"}}}; - }) + global.ofn_available_units_sorted = { + weight: { + "1.0": { name: "g", system: "metric" }, + "1000.0": { name: "kg", system: "metric" }, + "1000000.0": { name: "T", system: "metric" }, + }, + volume: { + 0.001: { name: "mL", system: "metric" }, + "1.0": { name: "L", system: "metric" }, + 4.54609: { name: "gal", system: "imperial" }, + "1000.0": { name: "kL", system: "metric" }, + }, + }; + }); - describe("generating option value name", function() { + describe("generating option value name", function () { var v, namer; - beforeEach(function() { + beforeEach(function () { v = {}; var ofn_available_units_sorted = ofn_available_units_sorted; namer = new OptionValueNamer(v); }); - it("when unit is blank (empty items name)", function() { + it("when unit is blank (empty items name)", function () { jest.spyOn(namer, "value_scaled").mockImplementation(() => true); jest.spyOn(namer, "option_value_value_unit").mockImplementation(() => ["value", ""]); expect(namer.name()).toBe("value"); }); - it("when description is blank", function() { + it("when description is blank", function () { v.unit_description = null; jest.spyOn(namer, "value_scaled").mockImplementation(() => true); jest.spyOn(namer, "option_value_value_unit").mockImplementation(() => ["value", "unit"]); expect(namer.name()).toBe("valueunit"); }); - it("when description is present", function() { - v.unit_description = 'desc'; + it("when description is present", function () { + v.unit_description = "desc"; jest.spyOn(namer, "option_value_value_unit").mockImplementation(() => ["value", "unit"]); jest.spyOn(namer, "value_scaled").mockImplementation(() => true); expect(namer.name()).toBe("valueunit desc"); }); - it("when value is blank and description is present", function() { - v.unit_description = 'desc'; + it("when value is blank and description is present", function () { + v.unit_description = "desc"; jest.spyOn(namer, "option_value_value_unit").mockImplementation(() => [null, null]); jest.spyOn(namer, "value_scaled").mockImplementation(() => true); expect(namer.name()).toBe("desc"); }); - it("spaces value and unit when value is unscaled", function() { + it("spaces value and unit when value is unscaled", function () { v.unit_description = null; jest.spyOn(namer, "option_value_value_unit").mockImplementation(() => ["value", "unit"]); jest.spyOn(namer, "value_scaled").mockImplementation(() => false); expect(namer.name()).toBe("value unit"); }); - describe("determining if a variant's value is scaled", function() { - beforeEach(function() { + describe("determining if a variant's value is scaled", function () { + beforeEach(function () { v = {}; namer = new OptionValueNamer(v); }); - it("returns true when the product has a scale", function() { + it("returns true when the product has a scale", function () { v.variant_unit_scale = 1000; expect(namer.value_scaled()).toBe(true); }); - it("returns false otherwise", function() { + it("returns false otherwise", function () { expect(namer.value_scaled()).toBe(false); }); }); - describe("generating option value's value and unit", function() { + describe("generating option value's value and unit", function () { var v, namer; // Mock I18n. TODO: moved to a shared helper beforeAll(() => { const mockedT = jest.fn(); - mockedT.mockImplementation((string, opts) => (string + ', ' + JSON.stringify(opts))); + mockedT.mockImplementation((string, opts) => string + ", " + JSON.stringify(opts)); - global.I18n = { t: mockedT }; - }) + global.I18n = { t: mockedT }; + }); // (jest still doesn't have aroundEach https://github.com/jestjs/jest/issues/4543 ) afterAll(() => { delete global.I18n; - }) + }); - beforeEach(function() { + beforeEach(function () { v = {}; namer = new OptionValueNamer(v); }); - it("generates simple values", function() { - v.variant_unit = 'weight'; + it("generates simple values", function () { + v.variant_unit = "weight"; v.variant_unit_scale = 1.0; v.unit_value = 100; - expect(namer.option_value_value_unit()).toEqual([100, 'g']); + expect(namer.option_value_value_unit()).toEqual([100, "g"]); }); - it("generates values when unit value is non-integer", function() { - v.variant_unit = 'weight'; + it("generates values when unit value is non-integer", function () { + v.variant_unit = "weight"; v.variant_unit_scale = 1.0; v.unit_value = 123.45; - expect(namer.option_value_value_unit()).toEqual([123.45, 'g']); + expect(namer.option_value_value_unit()).toEqual([123.45, "g"]); }); - it("returns a value of 1 when unit value equals the scale", function() { - v.variant_unit = 'weight'; + it("returns a value of 1 when unit value equals the scale", function () { + v.variant_unit = "weight"; v.variant_unit_scale = 1000.0; v.unit_value = 1000.0; - expect(namer.option_value_value_unit()).toEqual([1, 'kg']); + expect(namer.option_value_value_unit()).toEqual([1, "kg"]); }); - it("generates values for all weight scales", function() { - [[1.0, 'g'], [1000.0, 'kg'], [1000000.0, 'T']].forEach(([scale, unit]) => { - v.variant_unit = 'weight'; + it("generates values for all weight scales", function () { + [ + [1.0, "g"], + [1000.0, "kg"], + [1000000.0, "T"], + ].forEach(([scale, unit]) => { + v.variant_unit = "weight"; v.variant_unit_scale = scale; v.unit_value = 100 * scale; expect(namer.option_value_value_unit()).toEqual([100, unit]); }); }); - it("generates values for all volume scales", function() { - [[0.001, 'mL'], [1.0, 'L'], [1000.0, 'kL']].forEach(([scale, unit]) => { - v.variant_unit = 'volume'; + it("generates values for all volume scales", function () { + [ + [0.001, "mL"], + [1.0, "L"], + [1000.0, "kL"], + ].forEach(([scale, unit]) => { + v.variant_unit = "volume"; v.variant_unit_scale = scale; v.unit_value = 100 * scale; expect(namer.option_value_value_unit()).toEqual([100, unit]); }); }); - it("generates right values for volume with rounded values", function() { + it("generates right values for volume with rounded values", function () { var unit; - unit = 'L'; - v.variant_unit = 'volume'; + unit = "L"; + v.variant_unit = "volume"; v.variant_unit_scale = 1.0; v.unit_value = 0.7; - expect(namer.option_value_value_unit()).toEqual([700, 'mL']); + expect(namer.option_value_value_unit()).toEqual([700, "mL"]); }); - it("chooses the correct scale when value is very small", function() { - v.variant_unit = 'volume'; + it("chooses the correct scale when value is very small", function () { + v.variant_unit = "volume"; v.variant_unit_scale = 0.001; v.unit_value = 0.0001; - expect(namer.option_value_value_unit()).toEqual([0.1, 'mL']); + expect(namer.option_value_value_unit()).toEqual([0.1, "mL"]); }); - it("generates values for item units", function() { + it("generates values for item units", function () { //TODO // %w(packet box).each do |unit| // p = double(:product, variant_unit: 'items', variant_unit_scale: nil, variant_unit_name: unit) @@ -141,17 +161,17 @@ describe("OptionValueNamer", () => { // v.stub(:unit_value) { 100 } // subject.option_value_value_unit.should == [100, unit.pluralize] }); - it("generates singular values for item units when value is 1", function() { - v.variant_unit = 'items'; + it("generates singular values for item units when value is 1", function () { + v.variant_unit = "items"; v.variant_unit_scale = null; - v.variant_unit_name = 'packet'; + v.variant_unit_name = "packet"; v.unit_value = 1; - expect(namer.option_value_value_unit()).toEqual([1, 'packet']); + expect(namer.option_value_value_unit()).toEqual([1, "packet"]); }); - it("returns [null, null] when unit value is not set", function() { - v.variant_unit = 'items'; + it("returns [null, null] when unit value is not set", function () { + v.variant_unit = "items"; v.variant_unit_scale = null; - v.variant_unit_name = 'foo'; + v.variant_unit_name = "foo"; v.unit_value = null; expect(namer.option_value_value_unit()).toEqual([null, null]); }); diff --git a/spec/javascripts/stimulus/variant_controller_test.js b/spec/javascripts/stimulus/variant_controller_test.js index 4f40dde81c3..032bcc7d091 100644 --- a/spec/javascripts/stimulus/variant_controller_test.js +++ b/spec/javascripts/stimulus/variant_controller_test.js @@ -5,28 +5,27 @@ import { Application } from "stimulus"; import variant_controller from "controllers/variant_controller"; - describe("VariantController", () => { beforeAll(() => { // Requires global var from page global.ofn_available_units_sorted = { - "weight": { - "1.0":{"name":"g","system":"metric"}, - "1000.0":{"name":"kg","system":"metric"}, - "1000000.0":{"name":"T","system":"metric"} + weight: { + "1.0": { name: "g", system: "metric" }, + "1000.0": { name: "kg", system: "metric" }, + "1000000.0": { name: "T", system: "metric" }, + }, + volume: { + 0.001: { name: "mL", system: "metric" }, + "1.0": { name: "L", system: "metric" }, + 4.54609: { name: "gal", system: "imperial" }, + "1000.0": { name: "kL", system: "metric" }, }, - "volume":{ - "0.001":{"name":"mL","system":"metric"}, - "1.0":{"name":"L","system":"metric"}, - "4.54609":{"name":"gal","system":"imperial"}, - "1000.0":{"name":"kL","system":"metric"} - } }; const mockedT = jest.fn(); - mockedT.mockImplementation((string, opts) => (string + ', ' + JSON.stringify(opts))); + mockedT.mockImplementation((string, opts) => string + ", " + JSON.stringify(opts)); - global.I18n = { t: mockedT }; + global.I18n = { t: mockedT }; const application = Application.start(); application.register("variant", variant_controller); @@ -34,7 +33,7 @@ describe("VariantController", () => { afterAll(() => { delete global.I18n; - }) + }); describe("variant_unit_with_scale", () => { beforeEach(() => { @@ -82,6 +81,6 @@ describe("VariantController", () => { expect(variant_unit.value).toBe("items"); expect(variant_unit_scale.value).toBe(""); }); - }) + }); }); }); From 45b0686130153cd85fca5d6698c3a90d86602316 Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou Date: Tue, 16 Jul 2024 13:31:34 +1000 Subject: [PATCH 27/68] Add PriceParser and UnitPrices and specs This is in preparation for removing angular from the variant update page. Converted using https://www.codeconvert.ai/coffeescript-to-javascript-converter --- app/webpacker/js/services/price_parser.js | 45 +++++ app/webpacker/js/services/unit_prices.js | 51 ++++++ .../javascripts/services/price_parser_test.js | 150 ++++++++++++++++ spec/javascripts/services/unit_prices_test.js | 170 ++++++++++++++++++ 4 files changed, 416 insertions(+) create mode 100644 app/webpacker/js/services/price_parser.js create mode 100644 app/webpacker/js/services/unit_prices.js create mode 100644 spec/javascripts/services/price_parser_test.js create mode 100644 spec/javascripts/services/unit_prices_test.js diff --git a/app/webpacker/js/services/price_parser.js b/app/webpacker/js/services/price_parser.js new file mode 100644 index 00000000000..873f665c9a6 --- /dev/null +++ b/app/webpacker/js/services/price_parser.js @@ -0,0 +1,45 @@ +export default class PriceParser { + parse(price) { + if (!price) { + return null; + } + + // used decimal and thousands separators from currency configuration + const decimalSeparator = I18n.toCurrency(0.1, { precision: 1, unit: "" }).substring(1, 2); + const thousandsSeparator = I18n.toCurrency(1000, { precision: 1, unit: "" }).substring(1, 2); + + // Replace comma used as a decimal separator and remplace by "." + price = this.replaceCommaByFinalPoint(price); + + // Remove configured thousands separator if it is actually a thousands separator + price = this.removeThousandsSeparator(price, thousandsSeparator); + + if (decimalSeparator === ",") { + price = price.replace(",", "."); + } + + price = parseFloat(price); + + if (isNaN(price)) { + return null; + } + + return price; + } + + replaceCommaByFinalPoint(price) { + if (price.match(/^[0-9]*(,{1})[0-9]{1,2}$/g)) { + return price.replace(",", "."); + } else { + return price; + } + } + + removeThousandsSeparator(price, thousandsSeparator) { + if (new RegExp(`^([0-9]*(${thousandsSeparator}{1})[0-9]{3}[0-9\.,]*)*$`, "g").test(price)) { + return price.replaceAll(thousandsSeparator, ""); + } else { + return price; + } + } +} diff --git a/app/webpacker/js/services/unit_prices.js b/app/webpacker/js/services/unit_prices.js new file mode 100644 index 00000000000..6344a2e073d --- /dev/null +++ b/app/webpacker/js/services/unit_prices.js @@ -0,0 +1,51 @@ +import PriceParser from "js/services/price_parser"; +import VariantUnitManager from "js/services/variant_unit_manager"; +import localizeCurrency from "js/services/localize_currency"; + +export default class UnitPrices { + constructor() { + this.variantUnitManager = new VariantUnitManager(); + this.priceParser = new PriceParser(); + } + + displayableUnitPrice(price, scale, unit_type, unit_value, variant_unit_name) { + price = this.priceParser.parse(price); + if (price && !isNaN(price) && unit_type && unit_value) { + const value = localizeCurrency( + this.price(price, scale, unit_type, unit_value, variant_unit_name), + ); + const unit = this.unit(scale, unit_type, variant_unit_name); + return `${value} / ${unit}`; + } + return null; + } + + price(price, scale, unit_type, unit_value) { + return price / this.denominator(scale, unit_type, unit_value); + } + + denominator(scale, unit_type, unit_value) { + const unit = this.unit(scale, unit_type); + if (unit === "lb") { + return unit_value / 453.6; + } else if (unit === "kg") { + return unit_value / 1000; + } else { + return unit_value; + } + } + + unit(scale, unit_type, variant_unit_name = "") { + if (variant_unit_name.length > 0 && unit_type === "items") { + return variant_unit_name; + } else if (unit_type === "items") { + return "item"; + } else if (this.variantUnitManager.systemOfMeasurement(scale, unit_type) === "imperial") { + return "lb"; + } else if (unit_type === "weight") { + return "kg"; + } else if (unit_type === "volume") { + return "L"; + } + } +} diff --git a/spec/javascripts/services/price_parser_test.js b/spec/javascripts/services/price_parser_test.js new file mode 100644 index 00000000000..b8d77948cac --- /dev/null +++ b/spec/javascripts/services/price_parser_test.js @@ -0,0 +1,150 @@ +/** + * @jest-environment jsdom + */ + +import PriceParse from "js/services/price_parser"; + +describe("PriceParser service", function () { + let priceParser = null; + + beforeEach(() => { + priceParser = new PriceParse(); + }); + + describe("test internal method with Regexp", function () { + describe("test replaceCommaByFinalPoint() method", function () { + it("handle the default case (with two numbers after comma)", function () { + expect(priceParser.replaceCommaByFinalPoint("1,00")).toEqual("1.00"); + }); + it("doesn't confuse with thousands separator", function () { + expect(priceParser.replaceCommaByFinalPoint("1,000")).toEqual("1,000"); + }); + it("handle also when there is only one number after the decimal separator", function () { + expect(priceParser.replaceCommaByFinalPoint("1,0")).toEqual("1.0"); + }); + }); + + describe("test removeThousandsSeparator() method", function () { + it("handle the default case", function () { + expect(priceParser.removeThousandsSeparator("1,000", ",")).toEqual("1000"); + expect(priceParser.removeThousandsSeparator("1,000,000", ",")).toEqual("1000000"); + }); + it("handle the case with decimal separator", function () { + expect(priceParser.removeThousandsSeparator("1,000,000.00", ",")).toEqual("1000000.00"); + }); + it("handle the case when it is actually a decimal separator (and not a thousands one)", function () { + expect(priceParser.removeThousandsSeparator("1,00", ",")).toEqual("1,00"); + }); + }); + }); + + describe("with point as decimal separator and comma as thousands separator for I18n service", function () { + beforeAll(() => { + const mockedToCurrency = jest.fn(); + mockedToCurrency.mockImplementation((arg) => { + if (arg == 0.1) { + return "0.1"; + } else if (arg == 1000) { + return "1,000"; + } + }); + + global.I18n = { toCurrency: mockedToCurrency }; + }); + // (jest still doesn't have aroundEach https://github.com/jestjs/jest/issues/4543 ) + afterAll(() => { + delete global.I18n; + }); + + it("handle point as decimal separator", function () { + expect(priceParser.parse("1.00")).toEqual(1.0); + }); + + it("handle point as decimal separator", function () { + expect(priceParser.parse("1.000")).toEqual(1.0); + }); + + it("also handle comma as decimal separator", function () { + expect(priceParser.parse("1,0")).toEqual(1.0); + }); + + it("also handle comma as decimal separator", function () { + expect(priceParser.parse("1,00")).toEqual(1.0); + }); + + it("also handle comma as decimal separator", function () { + expect(priceParser.parse("11,00")).toEqual(11.0); + }); + + it("handle comma as decimal separator but not confusing with thousands separator", function () { + expect(priceParser.parse("11,000")).toEqual(11000); + }); + + it("handle point as decimal separator and comma as thousands separator", function () { + expect(priceParser.parse("1,000,000.00")).toEqual(1000000); + }); + + it("handle integer number", function () { + expect(priceParser.parse("10")).toEqual(10); + }); + + it("handle integer number with comma as thousands separator", function () { + expect(priceParser.parse("1,000")).toEqual(1000); + }); + + it("handle integer number with no thousands separator", function () { + expect(priceParser.parse("1000")).toEqual(1000); + }); + }); + + describe("with comma as decimal separator and final point as thousands separator for I18n service", function () { + beforeAll(() => { + const mockedToCurrency = jest.fn(); + mockedToCurrency.mockImplementation((arg) => { + if (arg == 0.1) { + return "0,1"; + } else if (arg == 1000) { + return "1.000"; + } + }); + + global.I18n = { toCurrency: mockedToCurrency }; + }); + // (jest still doesn't have aroundEach https://github.com/jestjs/jest/issues/4543 ) + afterAll(() => { + delete global.I18n; + }); + + it("handle comma as decimal separator", function () { + expect(priceParser.parse("1,00")).toEqual(1.0); + }); + + it("handle comma as decimal separator with one digit after the comma", function () { + expect(priceParser.parse("11,0")).toEqual(11.0); + }); + + it("handle comma as decimal separator with two digit after the comma", function () { + expect(priceParser.parse("11,00")).toEqual(11.0); + }); + + it("handle comma as decimal separator with three digit after the comma", function () { + expect(priceParser.parse("11,000")).toEqual(11.0); + }); + + it("also handle point as decimal separator", function () { + expect(priceParser.parse("1.00")).toEqual(1.0); + }); + + it("also handle point as decimal separator with integer part with two digits", function () { + expect(priceParser.parse("11.00")).toEqual(11.0); + }); + + it("handle point as decimal separator and final point as thousands separator", function () { + expect(priceParser.parse("1.000.000,00")).toEqual(1000000); + }); + + it("handle integer number", function () { + expect(priceParser.parse("10")).toEqual(10); + }); + }); +}); diff --git a/spec/javascripts/services/unit_prices_test.js b/spec/javascripts/services/unit_prices_test.js new file mode 100644 index 00000000000..6df9eb791f5 --- /dev/null +++ b/spec/javascripts/services/unit_prices_test.js @@ -0,0 +1,170 @@ +/** + * @jest-environment jsdom + */ + +import UnitPrices from "js/services/unit_prices"; + +describe("UnitPrices service", function () { + let unitPrices = null; + + beforeAll(() => { + // Requires global var from page for VariantUnitManager + global.ofn_available_units_sorted = { + weight: { + "1.0": { name: "g", system: "metric" }, + 28.35: { name: "oz", system: "imperial" }, + 453.6: { name: "lb", system: "imperial" }, + "1000.0": { name: "kg", system: "metric" }, + "1000000.0": { name: "T", system: "metric" }, + }, + volume: { + 0.001: { name: "mL", system: "metric" }, + "1.0": { name: "L", system: "metric" }, + "1000.0": { name: "kL", system: "metric" }, + }, + }; + }); + + beforeEach(() => { + unitPrices = new UnitPrices(); + }); + + describe("get correct unit price duo unit/value for weight", function () { + const unit_type = "weight"; + + it("with scale: 1", function () { + const price = 1; + const scale = 1; + const unit_value = 1; + expect(unitPrices.price(price, scale, unit_type, unit_value)).toEqual(1000); + expect(unitPrices.unit(scale, unit_type)).toEqual("kg"); + }); + + it("with scale and unit_value: 1000", function () { + const price = 1; + const scale = 1000; + const unit_value = 1000; + expect(unitPrices.price(price, scale, unit_type, unit_value)).toEqual(1); + expect(unitPrices.unit(scale, unit_type)).toEqual("kg"); + }); + + it("with scale: 1000 and unit_value: 2000", function () { + const price = 1; + const scale = 1000; + const unit_value = 2000; + expect(unitPrices.price(price, scale, unit_type, unit_value)).toEqual(0.5); + expect(unitPrices.unit(scale, unit_type)).toEqual("kg"); + }); + + it("with price: 2", function () { + const price = 2; + const scale = 1; + const unit_value = 1; + expect(unitPrices.price(price, scale, unit_type, unit_value)).toEqual(2000); + expect(unitPrices.unit(scale, unit_type)).toEqual("kg"); + }); + + it("with price: 2, scale and unit_value: 1000", function () { + const price = 2; + const scale = 1000; + const unit_value = 1000; + expect(unitPrices.price(price, scale, unit_type, unit_value)).toEqual(2); + expect(unitPrices.unit(scale, unit_type)).toEqual("kg"); + }); + + it("with price: 2, scale: 1000 and unit_value: 2000", function () { + const price = 2; + const scale = 1000; + const unit_value = 2000; + expect(unitPrices.price(price, scale, unit_type, unit_value)).toEqual(1); + expect(unitPrices.unit(scale, unit_type)).toEqual("kg"); + }); + + it("with price: 2, scale: 1000 and unit_value: 500", function () { + const price = 2; + const scale = 1000; + const unit_value = 500; + expect(unitPrices.price(price, scale, unit_type, unit_value)).toEqual(4); + expect(unitPrices.unit(scale, unit_type)).toEqual("kg"); + }); + }); + + describe("get correct unit price duo unit/value for volume", function () { + const unit_type = "volume"; + + it("with scale: 1", function () { + const price = 1; + const scale = 1; + const unit_value = 1; + expect(unitPrices.price(price, scale, unit_type, unit_value)).toEqual(1); + expect(unitPrices.unit(scale, unit_type)).toEqual("L"); + }); + + it("with price: 2 and unit_value: 0.5", function () { + const price = 2; + const scale = 1; + const unit_value = 0.5; + expect(unitPrices.price(price, scale, unit_type, unit_value)).toEqual(4); + expect(unitPrices.unit(scale, unit_type)).toEqual("L"); + }); + + it("with price: 2, scale: 0.001 and unit_value: 0.01", function () { + const price = 2; + const scale = 0.001; + const unit_value = 0.01; + expect(unitPrices.price(price, scale, unit_type, unit_value)).toEqual(200); + expect(unitPrices.unit(scale, unit_type)).toEqual("L"); + }); + + it("with price: 20000, scale: 1000 and unit_value: 10000", function () { + const price = 20000; + const scale = 1000; + const unit_value = 10000; + expect(unitPrices.price(price, scale, unit_type, unit_value)).toEqual(2); + expect(unitPrices.unit(scale, unit_type)).toEqual("L"); + }); + + it("with price: 2, scale: 1000 and unit_value: 10000 and variant_unit_name: box", function () { + const price = 20000; + const scale = 1000; + const unit_value = 10000; + const variant_unit_name = "Box"; + expect(unitPrices.price(price, scale, unit_type, unit_value, variant_unit_name)).toEqual(2); + expect(unitPrices.unit(scale, unit_type, variant_unit_name)).toEqual("L"); + }); + }); + + describe("get correct unit price duo unit/value for items", function () { + const unit_type = "items"; + const scale = null; + + it("with price: 1 and unit_value: 1", function () { + const price = 1; + const unit_value = 1; + expect(unitPrices.price(price, scale, unit_type, unit_value)).toEqual(1); + expect(unitPrices.unit(scale, unit_type)).toEqual("item"); + }); + + it("with price: 1 and unit_value: 10", function () { + const price = 1; + const unit_value = 10; + expect(unitPrices.price(price, scale, unit_type, unit_value)).toEqual(0.1); + expect(unitPrices.unit(scale, unit_type)).toEqual("item"); + }); + + it("with price: 10 and unit_value: 1", function () { + const price = 10; + const unit_value = 1; + expect(unitPrices.price(price, scale, unit_type, unit_value)).toEqual(10); + expect(unitPrices.unit(scale, unit_type)).toEqual("item"); + }); + + it("with price: 10 and unit_value: 1 and variant_unit_name: box", function () { + const price = 10; + const unit_value = 1; + const variant_unit_name = "Box"; + expect(unitPrices.price(price, scale, unit_type, unit_value, variant_unit_name)).toEqual(10); + expect(unitPrices.unit(scale, unit_type, variant_unit_name)).toEqual("Box"); + }); + }); +}); From cc85fed7cca666592dba4e79f9bdc4d8c33ae9bc Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou Date: Tue, 23 Jul 2024 15:57:17 +1000 Subject: [PATCH 28/68] Add localizeCurrency and specs --- .../js/services/localize_currency.js | 24 +++++++ .../services/localize_currency_test.js | 63 +++++++++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 app/webpacker/js/services/localize_currency.js create mode 100644 spec/javascripts/services/localize_currency_test.js diff --git a/app/webpacker/js/services/localize_currency.js b/app/webpacker/js/services/localize_currency.js new file mode 100644 index 00000000000..c542d195921 --- /dev/null +++ b/app/webpacker/js/services/localize_currency.js @@ -0,0 +1,24 @@ +// Convert number to string currency using injected currency configuration. + +// Requires global variable from page: ofn_currency_config +export default function (amount) { + // Set country code (eg. "US"). + const currency_code = ofn_currency_config.display_currency + ? " " + ofn_currency_config.currency + : ""; + // Set decimal points, 2 or 0 if hide_cents. + const decimals = ofn_currency_config.hide_cents === "true" ? 0 : 2; + // Set format if the currency symbol should come after the number, otherwise (default) use the locale setting. + const format = ofn_currency_config.symbol_position === "after" ? "%n %u" : undefined; + // We need to use parseFloat as the amount should come in as a string. + amount = parseFloat(amount); + + // Build the final price string. + return ( + I18n.toCurrency(amount, { + precision: decimals, + unit: ofn_currency_config.symbol, + format: format, + }) + currency_code + ); +} diff --git a/spec/javascripts/services/localize_currency_test.js b/spec/javascripts/services/localize_currency_test.js new file mode 100644 index 00000000000..fd8a2da3599 --- /dev/null +++ b/spec/javascripts/services/localize_currency_test.js @@ -0,0 +1,63 @@ +/** + * @jest-environment jsdom + */ + +import localizeCurrency from "js/services/localize_currency"; + +describe("convert number to localised currency", function () { + beforeAll(() => { + const mockedToCurrency = jest.fn(); + mockedToCurrency.mockImplementation((amount, options) => { + if (options.format == "%n %u") { + return `${amount.toFixed(options.precision)}${options.unit}`; + } else { + return `${options.unit}${amount.toFixed(options.precision)}`; + } + }); + + global.I18n = { toCurrency: mockedToCurrency }; + + // Requires global var from page + global.ofn_currency_config = { + symbol: "$", + symbol_position: "before", + currency: "D", + hide_cents: "false", + }; + }); + // (jest still doesn't have aroundEach https://github.com/jestjs/jest/issues/4543 ) + afterAll(() => { + delete global.I18n; + }); + + it("adds decimal fraction to an amount", function () { + expect(localizeCurrency(10)).toEqual("$10.00"); + }); + + it("handles an existing fraction", function () { + expect(localizeCurrency(9.9)).toEqual("$9.90"); + }); + + it("can use any currency symbol", function () { + global.ofn_currency_config.symbol = "£"; + expect(localizeCurrency(404.04)).toEqual("£404.04"); + }); + + it("can place symbols after the amount", function () { + global.ofn_currency_config.symbol = "$"; + global.ofn_currency_config.symbol_position = "after"; + expect(localizeCurrency(333.3)).toEqual("333.30$"); + }); + + it("can add a currency string", function () { + global.ofn_currency_config.display_currency = true; + global.ofn_currency_config.symbol_position = "before"; + expect(localizeCurrency(5)).toEqual("$5.00 D"); + }); + + it("can hide cents", function () { + global.ofn_currency_config.display_currency = false; + global.ofn_currency_config.hide_cents = "true"; + expect(localizeCurrency(5)).toEqual("$5"); + }); +}); From ce268ec1758c5b92afed00d4ed963a5f0df12da0 Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou Date: Tue, 23 Jul 2024 16:38:13 +1000 Subject: [PATCH 29/68] Add systemOfMeasurement to VariantUnitManager --- .../js/services/variant_unit_manager.js | 28 ++++++++++++------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/app/webpacker/js/services/variant_unit_manager.js b/app/webpacker/js/services/variant_unit_manager.js index 1711025fcd0..f13af68264c 100644 --- a/app/webpacker/js/services/variant_unit_manager.js +++ b/app/webpacker/js/services/variant_unit_manager.js @@ -7,33 +7,41 @@ export default class VariantUnitManager { getUnitName(scale, unitType) { if (this.units[unitType][scale]) { - return this.units[unitType][scale]['name']; + return this.units[unitType][scale]["name"]; } else { - return ''; + return ""; } - }; + } - // Filter by measurement system + // Filter by measurement system compatibleUnitScales(scale, unitType) { - const scaleSystem = this.units[unitType][scale]['system']; + const scaleSystem = this.units[unitType][scale]["system"]; return Object.entries(this.units[unitType]) .filter(([scale, scaleInfo]) => { - return scaleInfo['system'] == scaleSystem; + return scaleInfo["system"] == scaleSystem; }) .map(([scale, _]) => parseFloat(scale)) .sort(); } + systemOfMeasurement(scale, unitType) { + if (this.units[unitType][scale]) { + return this.units[unitType][scale]["system"]; + } else { + return "custom"; + } + } + // private #loadUnits(units) { // Transform unit scale to a JS Number for compatibility. This would be way simpler in Ruby or Coffeescript!! const unitsTransformed = Object.entries(units).map(([measurement, measurementInfo]) => { - const measurementInfoTransformed = Object.fromEntries(Object.entries(measurementInfo).map(([scale, unitInfo]) => - [ parseFloat(scale), unitInfo ] - )); - return [ measurement, measurementInfoTransformed ]; + const measurementInfoTransformed = Object.fromEntries( + Object.entries(measurementInfo).map(([scale, unitInfo]) => [parseFloat(scale), unitInfo]), + ); + return [measurement, measurementInfoTransformed]; }); return Object.fromEntries(unitsTransformed); } From 7f16b6acde0c8af287a6783bd84a31118eeba6f6 Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou Date: Wed, 24 Jul 2024 12:01:51 +1000 Subject: [PATCH 30/68] Update variant form and rip out angular --- .../variant_units_controller.js.coffee | 32 ---- .../spree/admin/variants/_form.html.haml | 159 +++++++++------- .../controllers/edit_variant_controller.js | 169 ++++++++++++++++++ 3 files changed, 261 insertions(+), 99 deletions(-) delete mode 100644 app/assets/javascripts/admin/products/controllers/variant_units_controller.js.coffee create mode 100644 app/webpacker/controllers/edit_variant_controller.js diff --git a/app/assets/javascripts/admin/products/controllers/variant_units_controller.js.coffee b/app/assets/javascripts/admin/products/controllers/variant_units_controller.js.coffee deleted file mode 100644 index 91549bfc607..00000000000 --- a/app/assets/javascripts/admin/products/controllers/variant_units_controller.js.coffee +++ /dev/null @@ -1,32 +0,0 @@ -angular.module("admin.products").controller "variantUnitsCtrl", ($scope, VariantUnitManager, $timeout, UnitPrices, PriceParser) -> - - $scope.unitName = (scale, type) -> - VariantUnitManager.getUnitName(scale, type) - - $scope.$watchCollection "[unit_value_human, variant.price]", -> - $scope.processUnitPrice() - - $scope.processUnitPrice = -> - if ($scope.variant) - price = $scope.variant.price - scale = $scope.scale - unit_type = angular.element("#product_variant_unit").val() - if (unit_type != "items") - $scope.updateValue() - unit_value = $scope.unit_value - else - unit_value = 1 - variant_unit_name = angular.element("#product_variant_unit_name").val() - $scope.unit_price = UnitPrices.displayableUnitPrice(price, scale, unit_type, unit_value, variant_unit_name) - - $scope.scale = angular.element('#product_variant_unit_scale').val() - - $scope.updateValue = -> - unit_value_human = angular.element('#unit_value_human').val() - $scope.unit_value = bigDecimal.multiply(PriceParser.parse(unit_value_human), $scope.scale, 2) - - variant_unit_value = angular.element('#variant_unit_value').val() - $scope.unit_value_human = parseFloat(bigDecimal.divide(variant_unit_value, $scope.scale, 2)) - - $timeout -> $scope.processUnitPrice() - $timeout -> $scope.updateValue() diff --git a/app/views/spree/admin/variants/_form.html.haml b/app/views/spree/admin/variants/_form.html.haml index dcb4d68b6e4..da6eff5e2bf 100644 --- a/app/views/spree/admin/variants/_form.html.haml +++ b/app/views/spree/admin/variants/_form.html.haml @@ -1,83 +1,108 @@ -.label-block.left.six.columns.alpha{'ng-app' => 'admin.products', 'ng-controller' => 'variantUnitsCtrl'} - .field - = f.label :display_name, t('.display_name') - = f.text_field :display_name, class: "fullwidth", placeholder: t('.display_name_placeholder') - .field - = f.label :display_as, t('.display_as') - = f.text_field :display_as, class: "fullwidth", placeholder: t('.display_as_placeholder') +%div{'data-controller': "edit-variant"} + .label-block.left.six.columns.alpha + %script= render partial: "admin/shared/global_var_ofn", formats: :js, + locals: { name: :available_units_sorted, value: WeightsAndMeasures.available_units_sorted } + + %script= render partial: "admin/shared/global_var_ofn", formats: :js, + locals: { name: :currency_config, value: Api::CurrencyConfigSerializer.new({}) } - - if @product.variant_unit != 'items' .field - = label_tag :unit_value_human, "#{t('admin.'+@product.variant_unit)} ({{unitName(#{@product.variant_unit_scale}, '#{@product.variant_unit}')}})" - = hidden_field_tag 'product_variant_unit_scale', @product.variant_unit_scale - = number_field_tag :unit_value_human, nil, {class: "fullwidth", step: 0.01, 'ng-model' => 'unit_value_human', 'ng-change' => 'updateValue()'} - = f.number_field :unit_value, {hidden: true, 'ng-value' => 'unit_value'} + = f.label :display_name, t('.display_name') + = f.text_field :display_name, class: "fullwidth", placeholder: t('.display_name_placeholder') - .field - = f.label :unit_description, t(:spree_admin_unit_description) - = f.text_field :unit_description, class: "fullwidth", placeholder: t('admin.products.unit_name_placeholder') + .field{ 'data-controller': 'toggle-control', 'data-toggle-control-match-value': 'items' } + -#TODO translation + = f.label :unit_scale, raw(t(:unit_scale) + content_tag(:span, ' *', :class => 'required')) + = f.hidden_field :variant_unit + = f.hidden_field :variant_unit_scale + = f.select :variant_unit_with_scale, + options_for_select(WeightsAndMeasures.variant_unit_options, @variant.variant_unit_with_scale), + { include_blank: true }, + { class: "fullwidth no-input", 'aria-label': t('admin.products_page.columns.unit_scale'), data: { "controller": "tom-select", "tom-select-options-value": '{ "plugins": [] }', action: "change->toggle-control#displayIfMatch" } } + = error_message_on @variant, :variant_unit, 'data-toggle-control-target': 'control' + .field + = f.text_field :variant_unit_name, 'aria-label': t('items'), 'data-toggle-control-target': 'control', style: (@variant.variant_unit == "items" ? "" : "display: none") + = error_message_on @variant, :variant_unit_name, 'data-toggle-control-target': 'control' + + .field.popout{'data-controller': "popout", 'data-popout-update-display-value': "false"} + -#TODO translation + = f.label :unit, raw(t(:unit) + content_tag(:span, ' *', :class => 'required')) + = f.button :unit_to_display, class: "popout__button", 'aria-label': t('admin.products_page.columns.unit'), 'data-popout-target': "button" do + = @variant.unit_to_display # Show the generated summary of unit values + %div.popout__container{ style: 'display: none;', 'data-controller': 'toggle-control', 'data-popout-target': "dialog" } + .field + -# Show a composite field for unit_value and unit_description + = f.hidden_field :unit_value + = f.hidden_field :unit_description + -# todo: create a method for value_with_description + = f.text_field :unit_value_with_description, + value: [number_with_precision((@variant.unit_value || 1) / (@variant.variant_unit_scale || 1), precision: nil, strip_insignificant_zeros: true), @variant.unit_description].compact_blank.join(" "), + 'aria-label': t('admin.products_page.columns.unit_value'), required: true + .field + = f.label :display_as, t('admin.products_page.columns.display_as') + = f.text_field :display_as, placeholder: VariantUnits::OptionValueNamer.new(@variant).name + = error_message_on @variant, :unit_value - %div - .field - = f.label :sku, t('.sku') - = f.text_field :sku, class: 'fullwidth' - .field - = f.label :price, t('.price') - = f.text_field :price, class: 'fullwidth', "ng-model" => "variant.price", "ng-init" => "variant.price = '#{number_to_currency(@variant.price, unit: '')&.strip}'" - .field - = hidden_field_tag 'product_variant_unit', @product.variant_unit - = hidden_field_tag 'product_variant_unit_name', @product.variant_unit_name - = f.field_container :unit_price do - %div{style: "display: flex"} - = f.label :unit_price, t(".unit_price"), {style: "display: inline-block"} - %question-mark-with-tooltip{"question-mark-with-tooltip" => "_", - "question-mark-with-tooltip-append-to-body" => "true", - "question-mark-with-tooltip-placement" => "top", - "question-mark-with-tooltip-animation" => true, - key: "'js.admin.unit_price_tooltip'"} - %input{ "type" => "text", "id" => "variant_unit_price", "name" => "variant[unit_price]", - "class" => 'fullwidth', "disabled" => true, "ng-model" => "unit_price"} - %div{style: "color: black"} - = t("spree.admin.products.new.unit_price_legend") - %div{ 'set-on-demand' => '' } - .field.checkbox - %label - = f.check_box :on_demand - = t(:on_demand) - %div{'ofn-with-tip' => t('admin.products.variants.to_order_tip')} - %a= t('admin.whats_this') + %div + .field + = f.label :sku, t('.sku') + = f.text_field :sku, class: 'fullwidth' + .field + = f.label :price, raw(t(:price) + content_tag(:span, ' *', :class => 'required')) + = f.text_field :price, class: 'fullwidth', value: number_to_currency(@variant.price, unit: '')&.strip .field - = f.label :on_hand, t(:on_hand) - .fullwidth - = f.text_field :on_hand + = hidden_field_tag 'variant_variant_unit', @variant.variant_unit + = hidden_field_tag 'variant_variant_unit_name', @variant.variant_unit_name + = f.field_container :unit_price do + %div{style: "display: flex"} + = f.label :unit_price, t(".unit_price"), {style: "display: inline-block"} + %question-mark-with-tooltip{"question-mark-with-tooltip" => "_", + "question-mark-with-tooltip-append-to-body" => "true", + "question-mark-with-tooltip-placement" => "top", + "question-mark-with-tooltip-animation" => true, + key: "'js.admin.unit_price_tooltip'"} + %input{ "type" => "text", "id" => "variant_unit_price", "name" => "variant[unit_price]", "class" => 'fullwidth', "disabled" => true} + %div{style: "color: black"} + = t("spree.admin.products.new.unit_price_legend") + %div{ 'set-on-demand' => '' } + .field.checkbox + %label + = f.check_box :on_demand + = t(:on_demand) + - #TODO tooltip is broken + %div{'ofn-with-tip' => t('admin.products.variants.to_order_tip')} + %a= t('admin.whats_this') + .field + = f.label :on_hand, t(:on_hand) + .fullwidth + = f.text_field :on_hand -.right.six.columns.omega.label-block - - if @product.variant_unit != 'weight' + .right.six.columns.omega.label-block .field = f.label 'weight', t(:weight)+' (kg)' - value = number_with_precision(@variant.weight, precision: 3) = f.number_field 'weight', value: value, class: 'fullwidth', step: 0.001 - - [:height, :width, :depth].each do |field| - .field - = f.label field, t(field) - - value = number_with_precision(@variant.send(field), precision: 2) - = f.number_field field, value: value, class: 'fullwidth', step: 0.01 + - [:height, :width, :depth].each do |field| + .field + = f.label field, t(field) + - value = number_with_precision(@variant.send(field), precision: 2) + = f.number_field field, value: value, class: 'fullwidth', step: 0.01 - .field - = f.label :tax_category, t(:tax_category), for: :tax_category_id - = f.collection_select(:tax_category_id, @tax_categories, :id, :name, { include_blank: t(:none) }, { class: 'select2 fullwidth' }) + .field + = f.label :tax_category, t(:tax_category), for: :tax_category_id + = f.collection_select(:tax_category_id, @tax_categories, :id, :name, { include_blank: t(:none) }, { class: 'select2 fullwidth' }) - .field - = f.label :shipping_category_id, t(:shipping_categories) - = f.collection_select(:shipping_category_id, @shipping_categories, :id, :name, {}, { class: 'select2 fullwidth' }) + .field + = f.label :shipping_category_id, t(:shipping_categories) + = f.collection_select(:shipping_category_id, @shipping_categories, :id, :name, {}, { class: 'select2 fullwidth' }) - .field - = f.label :primary_taxon, t('spree.admin.products.primary_taxon_form.product_category') - = f.collection_select(:primary_taxon_id, Spree::Taxon.order(:name), :id, :name, { include_blank: true }, { class: "select2 fullwidth" }) + .field + = f.label :primary_taxon, t('spree.admin.products.primary_taxon_form.product_category') + = f.collection_select(:primary_taxon_id, Spree::Taxon.order(:name), :id, :name, { include_blank: true }, { class: "select2 fullwidth" }) - .field - = f.label :supplier, t(:spree_admin_supplier) - = f.collection_select(:supplier_id, @producers, :id, :name, {:include_blank => true}, {:class => "select2 fullwidth"}) + .field + = f.label :supplier, t(:spree_admin_supplier) + = f.collection_select(:supplier_id, @producers, :id, :name, {:include_blank => true}, {:class => "select2 fullwidth"}) -.clear + .clear diff --git a/app/webpacker/controllers/edit_variant_controller.js b/app/webpacker/controllers/edit_variant_controller.js new file mode 100644 index 00000000000..57b8a0ae854 --- /dev/null +++ b/app/webpacker/controllers/edit_variant_controller.js @@ -0,0 +1,169 @@ +import { Controller } from "stimulus"; +import OptionValueNamer from "js/services/option_value_namer"; +import UnitPrices from "js/services/unit_prices"; + +// Dynamically update related variant fields +// +// TODO refactor so we can extract what's common with Bulk product page +export default class EditVariantController extends Controller { + connect() { + this.unitPrices = new UnitPrices(); + // idea: create a helper that includes a nice getter/setter for Rails model attr values, just pass it the attribute name. + // It could automatically find (and cache a ref to) each dom element and get/set the values. + this.variantUnit = this.element.querySelector('[id="variant_variant_unit"]'); + this.variantUnitScale = this.element.querySelector('[id="variant_variant_unit_scale"]'); + this.variantUnitName = this.element.querySelector('[id="variant_variant_unit_name"]'); + this.variantUnitWithScale = this.element.querySelector( + '[id="variant_variant_unit_with_scale"]', + ); + this.variantPrice = this.element.querySelector('[id="variant_price"]'); + + // on variant_unit_with_scale changed; update variant_unit and variant_unit_scale + this.variantUnitWithScale.addEventListener("change", this.#updateUnitAndScale.bind(this), { + passive: true, + }); + + this.unitValue = this.element.querySelector('[id="variant_unit_value"]'); + this.unitDescription = this.element.querySelector('[id="variant_unit_description"]'); + this.unitValueWithDescription = this.element.querySelector( + '[id="variant_unit_value_with_description"]', + ); + this.displayAs = this.element.querySelector('[id="variant_display_as"]'); + this.unitToDisplay = this.element.querySelector('[id="variant_unit_to_display"]'); + + // on unit changed; update display_as:placeholder and unit_to_display + [this.variantUnit, this.variantUnitScale, this.variantUnitName].forEach((element) => { + element.addEventListener("change", this.#unitChanged.bind(this), { passive: true }); + }); + this.variantUnitName.addEventListener("input", this.#unitChanged.bind(this), { passive: true }); + + // on unit_value_with_description changed; update unit_value and unit_description + // on unit_value and/or unit_description changed; update display_as:placeholder and unit_to_display + this.unitValueWithDescription.addEventListener("input", this.#unitChanged.bind(this), { + passive: true, + }); + + // on display_as changed; update unit_to_display + // TODO: optimise to avoid unnecessary OptionValueNamer calc + this.displayAs.addEventListener("input", this.#updateUnitDisplay.bind(this), { passive: true }); + + // update Unit price when variant_unit_with_scale or price changes + [this.variantUnitWithScale, this.variantPrice].forEach((element) => { + element.addEventListener("change", this.#processUnitPrice.bind(this), { passive: true }); + }); + this.unitValueWithDescription.addEventListener("input", this.#processUnitPrice.bind(this), { + passive: true, + }); + + // on variantUnit change we need to check if weight needs to be toggled + this.variantUnit.addEventListener("change", this.#toggleWeight.bind(this), { passive: true }); + + // update unit price on page load + this.#processUnitPrice(); + this.#toggleWeight(); + } + + disconnect() { + // Make sure to clean up anything that happened outside + } + + // private + + // Extract variant_unit and variant_unit_scale from dropdown variant_unit_with_scale, + // and update hidden product fields + #unitChanged(event) { + //Hmm in hindsight the logic in product_controller should be inn this controller already. then we can do everything in one event, and store the generated name in an instance variable. + this.#extractUnitValues(); + this.#updateUnitDisplay(); + } + + // Extract unit_value and unit_description + #extractUnitValues() { + // Extract a number (optional) and text value, separated by a space. + const match = this.unitValueWithDescription.value.match(/^([\d\.\,]+(?= |$)|)( |)(.*)$/); + if (match) { + let unit_value = parseFloat(match[1].replace(",", ".")); + unit_value = isNaN(unit_value) ? null : unit_value; + unit_value *= this.variantUnitScale.value ? this.variantUnitScale.value : 1; // Normalise to default scale + + this.unitValue.value = unit_value; + this.unitDescription.value = match[3]; + } + } + + // Update display_as placeholder and unit_to_display + #updateUnitDisplay() { + const unitDisplay = new OptionValueNamer(this.#variant()).name(); + this.displayAs.placeholder = unitDisplay; + this.unitToDisplay.textContent = this.displayAs.value || unitDisplay; + } + + // A representation of the variant model to satisfy OptionValueNamer. + #variant() { + return { + unit_value: parseFloat(this.unitValue.value), + unit_description: this.unitDescription.value, + variant_unit: this.variantUnit.value, + variant_unit_scale: parseFloat(this.variantUnitScale.value), + variant_unit_name: this.variantUnitName.value, + }; + } + + // Extract variant_unit and variant_unit_scale from dropdown variant_unit_with_scale, + // and update hidden product fields + #updateUnitAndScale(event) { + const variant_unit_with_scale = this.variantUnitWithScale.value; + const match = variant_unit_with_scale.match(/^([^_]+)_([\d\.]+)$/); // eg "weight_1000" + + if (match) { + this.variantUnit.value = match[1]; + this.variantUnitScale.value = parseFloat(match[2]); + } else { + // "items" + this.variantUnit.value = variant_unit_with_scale; + this.variantUnitScale.value = ""; + } + this.variantUnit.dispatchEvent(new Event("change")); + this.variantUnitScale.dispatchEvent(new Event("change")); + } + + #processUnitPrice() { + const unit_type = this.variantUnit.value; + + // TODO double check this + let unit_value = 1; + if (unit_type != "items") { + unit_value = this.unitValue.value; + } + + const unit_price = this.unitPrices.displayableUnitPrice( + this.variantPrice.value, + parseFloat(this.variantUnitScale.value), + unit_type, + unit_value, + this.variantUnitName.value, + ); + + this.element.querySelector('[id="variant_unit_price"]').value = unit_price; + } + + #toggleWeight() { + let display = "block"; + if (this.variantUnit.value === "weight") { + display = "none"; + } + + this.weight = this.element.querySelector('[id="variant_weight"]'); + this.weight.parentElement.style.display = display; + } + + //#showWeight() { + // this.weight = this.element.querySelector('[id="variant_weight"]'); + // this.weight.parentElement.style.display= "block" + //} + + //#hideWeight() { + // this.weight = this.element.querySelector('[id="variant_weight"]'); + // this.weight.parentElement.style.display= "none" + //} +} From 324a4ff59119b439b06a108e2862f3b96990c5cb Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou Date: Mon, 29 Jul 2024 15:21:05 +1000 Subject: [PATCH 31/68] Backport fix for hungarian instance --- app/models/spree/variant.rb | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/models/spree/variant.rb b/app/models/spree/variant.rb index 16a555498ef..fefd41ac0d8 100644 --- a/app/models/spree/variant.rb +++ b/app/models/spree/variant.rb @@ -227,14 +227,13 @@ def total_on_hand end # Format as per WeightsAndMeasures - # TODO test ? def variant_unit_with_scale # Our code is based upon English based number formatting with a period `.` scale_clean = ActiveSupport::NumberHelper.number_to_rounded(variant_unit_scale, precision: nil, + significant: false, strip_insignificant_zeros: true, - locale: :en - ) + locale: :en) [variant_unit, scale_clean].compact_blank.join("_") end From 058d7eeb6906dce9f72184be0a281b66e82a2299 Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou Date: Mon, 29 Jul 2024 15:58:04 +1000 Subject: [PATCH 32/68] Use unit_value_with_description --- app/views/spree/admin/variants/_form.html.haml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/views/spree/admin/variants/_form.html.haml b/app/views/spree/admin/variants/_form.html.haml index da6eff5e2bf..e2306446914 100644 --- a/app/views/spree/admin/variants/_form.html.haml +++ b/app/views/spree/admin/variants/_form.html.haml @@ -34,10 +34,8 @@ -# Show a composite field for unit_value and unit_description = f.hidden_field :unit_value = f.hidden_field :unit_description - -# todo: create a method for value_with_description = f.text_field :unit_value_with_description, - value: [number_with_precision((@variant.unit_value || 1) / (@variant.variant_unit_scale || 1), precision: nil, strip_insignificant_zeros: true), @variant.unit_description].compact_blank.join(" "), - 'aria-label': t('admin.products_page.columns.unit_value'), required: true + value: unit_value_with_description(@variant), 'aria-label': t('admin.products_page.columns.unit_value'), required: true .field = f.label :display_as, t('admin.products_page.columns.display_as') = f.text_field :display_as, placeholder: VariantUnits::OptionValueNamer.new(@variant).name From 00dfe6810fe8c15e1371c3f88ee638b364951cdc Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou Date: Mon, 29 Jul 2024 16:09:36 +1000 Subject: [PATCH 33/68] Fix ProductDuplicator There isn't away I could think of to create an invalid product, so I removed those test --- lib/spree/core/product_duplicator.rb | 1 - spec/lib/spree/core/product_duplicator_spec.rb | 13 ------------- spec/models/spree/product_spec.rb | 10 +--------- 3 files changed, 1 insertion(+), 23 deletions(-) diff --git a/lib/spree/core/product_duplicator.rb b/lib/spree/core/product_duplicator.rb index 82837bf3a61..5000caf7cd4 100644 --- a/lib/spree/core/product_duplicator.rb +++ b/lib/spree/core/product_duplicator.rb @@ -25,7 +25,6 @@ def duplicate_product new_product.deleted_at = nil new_product.updated_at = nil new_product.price = 0 - new_product.unit_value = %w(weight volume).include?(product.variant_unit) ? 1.0 : nil new_product.product_properties = reset_properties new_product.image = duplicate_image(product.image) if product.image new_product.variants = duplicate_variants diff --git a/spec/lib/spree/core/product_duplicator_spec.rb b/spec/lib/spree/core/product_duplicator_spec.rb index 141030048a8..377218211f0 100644 --- a/spec/lib/spree/core/product_duplicator_spec.rb +++ b/spec/lib/spree/core/product_duplicator_spec.rb @@ -73,7 +73,6 @@ expect(new_product).to receive(:product_properties=).with([new_property]) expect(new_product).to receive(:created_at=).with(nil) expect(new_product).to receive(:price=).with(0) - expect(new_product).to receive(:unit_value=).with(nil) expect(new_product).to receive(:updated_at=).with(nil) expect(new_product).to receive(:deleted_at=).with(nil) expect(new_product).to receive(:variants=).with([new_variant]) @@ -99,18 +98,6 @@ end describe "errors" do - context "with invalid product" do - let(:product) { - # name is a required field - create(:product).tap{ |p| p.update_columns(variant_unit: nil) } - } - subject { Spree::Core::ProductDuplicator.new(product).duplicate } - - it "raises RecordInvalid error" do - expect{ subject }.to raise_error(ActiveRecord::RecordInvalid) - end - end - context "invalid variant" do let(:variant) { # tax_category is required when products_require_tax_category diff --git a/spec/models/spree/product_spec.rb b/spec/models/spree/product_spec.rb index 130ac639a6e..91edc40ba0d 100644 --- a/spec/models/spree/product_spec.rb +++ b/spec/models/spree/product_spec.rb @@ -17,13 +17,6 @@ module Spree expect(clone.sku).to eq "" expect(clone.image).to eq product.image end - - it 'fails to duplicate invalid product' do - # Existing product is invalid: - product.update_columns(variant_unit: nil) - - expect{ product.duplicate }.to raise_error(ActiveRecord::ActiveRecordError) - end end context "product has variants" do @@ -123,7 +116,6 @@ module Spree it { is_expected.to validate_length_of(:name).is_at_most(255) } it { is_expected.to validate_length_of(:sku).is_at_most(255) } -# context "when the product has variants" do let(:product) do product = create(:simple_product) @@ -131,7 +123,7 @@ module Spree product.reload end - it { is_expected.to validate_numericality_of(:price).is_greater_than_or_equal_to(0) + it { is_expected.to validate_numericality_of(:price).is_greater_than_or_equal_to(0) } context "saving a new product" do let!(:product){ Spree::Product.new } From 144a09916c50356b0d4514d29a6b6d4bc10f1371 Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou Date: Mon, 29 Jul 2024 16:19:30 +1000 Subject: [PATCH 34/68] Use `instance_double` instead of `double` Instance double, amongst other thing, verifies that any methods being stubbed would be present on an instance of the class --- .../lib/spree/core/product_duplicator_spec.rb | 68 ++++++------------- 1 file changed, 19 insertions(+), 49 deletions(-) diff --git a/spec/lib/spree/core/product_duplicator_spec.rb b/spec/lib/spree/core/product_duplicator_spec.rb index 377218211f0..bab00af4685 100644 --- a/spec/lib/spree/core/product_duplicator_spec.rb +++ b/spec/lib/spree/core/product_duplicator_spec.rb @@ -5,58 +5,28 @@ RSpec.describe Spree::Core::ProductDuplicator do describe "unit" do let(:product) do - double 'Product', - name: "foo", - product_properties: [property], - variants: [variant], - image:, - variant_unit: 'item' + instance_double( + Spree::Product, + name: "foo", + product_properties: [property], + variants: [variant], + image:, + variant_unit: 'item' + ) end - - let(:new_product) do - double 'New Product', - save!: true - end - - let(:property) do - double 'Property' - end - - let(:new_property) do - double 'New Property' - end - + let(:new_product) { instance_double(Spree::Product, save!: true) } + let(:property) { instance_double(Spree::ProductProperty) } + let(:new_property) { instance_double(Spree::ProductProperty) } let(:variant) do - double 'Variant 1', - sku: "67890", - price: 19.50, - currency: "AUD", - images: [image_variant] - end - - let(:new_variant) do - double 'New Variant 1', - sku: "67890" - end - - let(:image) do - double 'Image', - attachment: double('Attachment') - end - - let(:new_image) do - double 'New Image' - end - - let(:image_variant) do - double 'Image Variant', - attachment: double('Attachment') - end - - let(:new_image_variant) do - double 'New Image Variant', - attachment: double('Attachment') + instance_double( + Spree::Variant, sku: "67890", price: 19.50, currency: "AUD", images: [image_variant] + ) end + let(:new_variant) { instance_double(Spree::Variant, sku: "67890") } + let(:image) { instance_double(Spree::Image, attachment: double('Attachment')) } + let(:new_image) { instance_double(Spree::Image) } + let(:image_variant) { instance_double(Spree::Image, attachment: double('Attachment')) } + let(:new_image_variant) { instance_double(Spree::Image, attachment: double('Attachment')) } before do expect(product).to receive(:dup).and_return(new_product) From 45075a0ccd9ddb634f8f620e18549d0c910a9b07 Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou Date: Wed, 31 Jul 2024 11:05:43 +1000 Subject: [PATCH 35/68] Fix rebase typo --- spec/models/product_importer_spec.rb | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/spec/models/product_importer_spec.rb b/spec/models/product_importer_spec.rb index d514193bbf6..3743aaead63 100644 --- a/spec/models/product_importer_spec.rb +++ b/spec/models/product_importer_spec.rb @@ -164,8 +164,8 @@ expect(carrots_variant.supplier).to eq enterprise expect(carrots_variant.price).to eq 3.20 expect(carrots_variant.unit_value).to eq 500 - expect(carrots_variant_unit).to eq 'weight' - expect(carrots_variant_unit_scale).to eq 1 + expect(carrots_variant.variant_unit).to eq 'weight' + expect(carrots_variant.variant_unit_scale).to eq 1 expect(carrots_variant.on_demand).not_to eq true expect(carrots_variant.import_date).to be_within(1.minute).of Time.zone.now @@ -176,8 +176,8 @@ expect(potatoes_variant.supplier).to eq enterprise expect(potatoes_variant.price).to eq 6.50 expect(potatoes_variant.unit_value).to eq 2000 - expect(potatoes_variant_unit).to eq 'weight' - expect(potatoes_variant_unit_scale).to eq 1000 + expect(potatoes_variant.variant_unit).to eq 'weight' + expect(potatoes_variant.variant_unit_scale).to eq 1000 expect(potatoes_variant.on_demand).not_to eq true expect(potatoes_variant.import_date).to be_within(1.minute).of Time.zone.now @@ -188,8 +188,8 @@ expect(pea_soup_variant.supplier).to eq enterprise expect(pea_soup_variant.price).to eq 5.50 expect(pea_soup_variant.unit_value).to eq 0.75 - expect(pea_soup_variant_unit).to eq 'volume' - expect(pea_soup_variant_unit_scale).to eq 0.001 + expect(pea_soup_variant.variant_unit).to eq 'volume' + expect(pea_soup_variant.variant_unit_scale).to eq 0.001 expect(pea_soup_variant.on_demand).not_to eq true expect(pea_soup_variant.import_date).to be_within(1.minute).of Time.zone.now @@ -200,8 +200,8 @@ expect(salad_variant.supplier).to eq enterprise expect(salad_variant.price).to eq 4.50 expect(salad_variant.unit_value).to eq 1 - expect(salad_variant_unit).to eq 'items' - expect(salad_variant_unit_scale).to eq nil + expect(salad_variant.variant_unit).to eq 'items' + expect(salad_variant.variant_unit_scale).to eq nil expect(salad_variant.on_demand).not_to eq true expect(salad_variant.import_date).to be_within(1.minute).of Time.zone.now @@ -212,8 +212,8 @@ expect(buns_variant.supplier).to eq enterprise expect(buns_variant.price).to eq 3.50 expect(buns_variant.unit_value).to eq 1 - expect(buns_variant_unit).to eq 'items' - expect(buns_variant_unit_scale).to eq nil + expect(buns_variant.variant_unit).to eq 'items' + expect(buns_variant.variant_unit_scale).to eq nil expect(buns_variant.on_demand).to eq true expect(buns_variant.import_date).to be_within(1.minute).of Time.zone.now end From d55950a3c5e1f15b01bf8f5e80992e65f9b815ca Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou Date: Wed, 31 Jul 2024 11:13:20 +1000 Subject: [PATCH 36/68] Fix rebase issue --- spec/models/product_import/entry_validator_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/models/product_import/entry_validator_spec.rb b/spec/models/product_import/entry_validator_spec.rb index 47512e5a786..db240b94986 100644 --- a/spec/models/product_import/entry_validator_spec.rb +++ b/spec/models/product_import/entry_validator_spec.rb @@ -124,7 +124,7 @@ unit_value: 500, variant_unit_scale: 1, variant_unit: 'weight', - variants: [create(:variant, supplier: enterprise, unit_value: 500)] + supplier_id: enterprise.id ) } @@ -136,7 +136,7 @@ unit_value: 1000, variant_unit_scale: 1000, variant_unit: 'weight', - variants: [create(:variant, supplier: enterprise, unit_value: 1000)] + supplier_id: enterprise.id ) } From 8f38762393293c5a97802bbfd681fb5b86942986 Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou Date: Tue, 6 Aug 2024 11:04:57 +1000 Subject: [PATCH 37/68] Add missing translations for variant form --- app/views/spree/admin/variants/_form.html.haml | 18 ++++++++---------- config/locales/en.yml | 5 +++++ 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/app/views/spree/admin/variants/_form.html.haml b/app/views/spree/admin/variants/_form.html.haml index e2306446914..fddec7d1c58 100644 --- a/app/views/spree/admin/variants/_form.html.haml +++ b/app/views/spree/admin/variants/_form.html.haml @@ -11,23 +11,21 @@ = f.text_field :display_name, class: "fullwidth", placeholder: t('.display_name_placeholder') .field{ 'data-controller': 'toggle-control', 'data-toggle-control-match-value': 'items' } - -#TODO translation - = f.label :unit_scale, raw(t(:unit_scale) + content_tag(:span, ' *', :class => 'required')) + = f.label :unit_scale, raw(t('.unit_scale') + content_tag(:span, ' *', :class => 'required')) = f.hidden_field :variant_unit = f.hidden_field :variant_unit_scale = f.select :variant_unit_with_scale, options_for_select(WeightsAndMeasures.variant_unit_options, @variant.variant_unit_with_scale), { include_blank: true }, - { class: "fullwidth no-input", 'aria-label': t('admin.products_page.columns.unit_scale'), data: { "controller": "tom-select", "tom-select-options-value": '{ "plugins": [] }', action: "change->toggle-control#displayIfMatch" } } + { class: "fullwidth no-input", 'aria-label': t('.unit_scale'), data: { "controller": "tom-select", "tom-select-options-value": '{ "plugins": [] }', action: "change->toggle-control#displayIfMatch" } } = error_message_on @variant, :variant_unit, 'data-toggle-control-target': 'control' .field = f.text_field :variant_unit_name, 'aria-label': t('items'), 'data-toggle-control-target': 'control', style: (@variant.variant_unit == "items" ? "" : "display: none") = error_message_on @variant, :variant_unit_name, 'data-toggle-control-target': 'control' .field.popout{'data-controller': "popout", 'data-popout-update-display-value': "false"} - -#TODO translation - = f.label :unit, raw(t(:unit) + content_tag(:span, ' *', :class => 'required')) - = f.button :unit_to_display, class: "popout__button", 'aria-label': t('admin.products_page.columns.unit'), 'data-popout-target': "button" do + = f.label :unit, raw(t('.unit') + content_tag(:span, ' *', :class => 'required')) + = f.button :unit_to_display, class: "popout__button", 'aria-label': t('.unit'), 'data-popout-target': "button" do = @variant.unit_to_display # Show the generated summary of unit values %div.popout__container{ style: 'display: none;', 'data-controller': 'toggle-control', 'data-popout-target': "dialog" } .field @@ -35,9 +33,9 @@ = f.hidden_field :unit_value = f.hidden_field :unit_description = f.text_field :unit_value_with_description, - value: unit_value_with_description(@variant), 'aria-label': t('admin.products_page.columns.unit_value'), required: true + value: unit_value_with_description(@variant), 'aria-label': t('.unit_value'), required: true .field - = f.label :display_as, t('admin.products_page.columns.display_as') + = f.label :display_as, t('.display_as') = f.text_field :display_as, placeholder: VariantUnits::OptionValueNamer.new(@variant).name = error_message_on @variant, :unit_value @@ -46,7 +44,7 @@ = f.label :sku, t('.sku') = f.text_field :sku, class: 'fullwidth' .field - = f.label :price, raw(t(:price) + content_tag(:span, ' *', :class => 'required')) + = f.label :price, raw(t('.price') + content_tag(:span, ' *', :class => 'required')) = f.text_field :price, class: 'fullwidth', value: number_to_currency(@variant.price, unit: '')&.strip .field = hidden_field_tag 'variant_variant_unit', @variant.variant_unit @@ -96,7 +94,7 @@ = f.collection_select(:shipping_category_id, @shipping_categories, :id, :name, {}, { class: 'select2 fullwidth' }) .field - = f.label :primary_taxon, t('spree.admin.products.primary_taxon_form.product_category') + = f.label :primary_taxon, t('.variant_category') = f.collection_select(:primary_taxon_id, Spree::Taxon.order(:name), :id, :name, { include_blank: true }, { class: "select2 fullwidth" }) .field diff --git a/config/locales/en.yml b/config/locales/en.yml index b962c1df4db..5b820de04a5 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -4615,6 +4615,11 @@ See the %{link} to find out more about %{sitename}'s features and to start using display_name: "Display Name" display_as_placeholder: 'eg. 2 kg' display_name_placeholder: 'eg. Tomatoes' + unit_scale: "Unit scale" + unit: Unit + price: Price + unit_value: Unit value + variant_category: Category autocomplete: out_of_stock: "Out of Stock" producer_name: "Producer" From 4ad6971121a22bf4388b02fb7503e41de37ff9a0 Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou Date: Tue, 6 Aug 2024 11:23:36 +1000 Subject: [PATCH 38/68] Fix Bulk product edit system spec after rebase --- .../spree/admin/variants_controller.rb | 2 + app/helpers/admin/products_helper.rb | 20 +++--- app/views/admin/products_v3/_table.html.haml | 2 +- .../admin/products_v3/_variant_row.html.haml | 2 +- .../controllers/bulk_form_controller.js | 40 ++++++++---- spec/system/admin/products_v3/actions_spec.rb | 14 ----- spec/system/admin/products_v3/create_spec.rb | 2 +- spec/system/admin/products_v3/update_spec.rb | 61 ++++++++++++++++--- 8 files changed, 95 insertions(+), 48 deletions(-) diff --git a/app/controllers/spree/admin/variants_controller.rb b/app/controllers/spree/admin/variants_controller.rb index 33f29e1b45b..765a011dff3 100644 --- a/app/controllers/spree/admin/variants_controller.rb +++ b/app/controllers/spree/admin/variants_controller.rb @@ -5,6 +5,8 @@ module Spree module Admin class VariantsController < ::Admin::ResourceController + helper ::Admin::ProductsHelper + belongs_to 'spree/product' before_action :load_data, only: [:new, :edit] diff --git a/app/helpers/admin/products_helper.rb b/app/helpers/admin/products_helper.rb index fe00c220022..b67aa57540c 100644 --- a/app/helpers/admin/products_helper.rb +++ b/app/helpers/admin/products_helper.rb @@ -18,17 +18,15 @@ def prepare_new_variant(product, producer_options) end def unit_value_with_description(variant) - precised_unit_value = nil - - if variant.unit_value - scaled_unit_value = variant.unit_value / (variant.product.variant_unit_scale || 1) - precised_unit_value = number_with_precision( - scaled_unit_value, - precision: nil, - strip_insignificant_zeros: true, - significant: false, - ) - end + return "" if variant.unit_value.nil? + + scaled_unit_value = variant.unit_value / (variant.product.variant_unit_scale || 1) + precised_unit_value = number_with_precision( + scaled_unit_value, + precision: nil, + strip_insignificant_zeros: true, + significant: false, + ) [precised_unit_value, variant.unit_description].compact_blank.join(" ") end diff --git a/app/views/admin/products_v3/_table.html.haml b/app/views/admin/products_v3/_table.html.haml index 71e777ba13a..5f302914085 100644 --- a/app/views/admin/products_v3/_table.html.haml +++ b/app/views/admin/products_v3/_table.html.haml @@ -47,7 +47,7 @@ .form-buttons %a.button.reset.medium{ href: admin_products_path(page: @page, per_page: @per_page, search_term: @search_term, producer_id: @producer_id, category_id: @category_id), 'data-turbo': "false" } = t('.reset') - = form.submit t('.save'), class: "medium" + = form.submit t('.save'), { class: "medium", data: { action: "click->bulk-form#popoutEmptyVariantUnit" }} %tr %th.col-image.align-left= # image = render partial: 'spree/admin/shared/stimulus_sortable_header', diff --git a/app/views/admin/products_v3/_variant_row.html.haml b/app/views/admin/products_v3/_variant_row.html.haml index 25a387674c5..2d3fd678967 100644 --- a/app/views/admin/products_v3/_variant_row.html.haml +++ b/app/views/admin/products_v3/_variant_row.html.haml @@ -13,7 +13,7 @@ = f.select :variant_unit_with_scale, options_for_select(WeightsAndMeasures.variant_unit_options, variant.variant_unit_with_scale), { include_blank: true }, - { class: "fullwidth no-input", 'aria-label': t('admin.products_page.columns.unit_scale'), data: { "controller": "tom-select", "tom-select-options-value": '{ "plugins": [] }', action: "change->toggle-control#displayIfMatch" } } + { class: "fullwidth no-input", 'aria-label': t('admin.products_page.columns.unit_scale'), data: { "controller": "tom-select", "tom-select-options-value": '{ "plugins": [] }', action: "change->toggle-control#displayIfMatch" }, required: true } = error_message_on variant, :variant_unit, 'data-toggle-control-target': 'control' .field = f.text_field :variant_unit_name, 'aria-label': t('items'), 'data-toggle-control-target': 'control', style: (variant.variant_unit == "items" ? "" : "display: none") diff --git a/app/webpacker/controllers/bulk_form_controller.js b/app/webpacker/controllers/bulk_form_controller.js index 5558b6d52ad..476bf003918 100644 --- a/app/webpacker/controllers/bulk_form_controller.js +++ b/app/webpacker/controllers/bulk_form_controller.js @@ -93,6 +93,16 @@ export default class BulkFormController extends Controller { } } + // Pop out empty variant unit to allow browser side validation to focus the element + popoutEmptyVariantUnit() { + this.variantUnits = this.element.querySelectorAll("button.popout__button"); + this.variantUnits.forEach((element) => { + if (element.textContent == "") { + element.click(); + } + }); + } + // private #registerSubmit() { @@ -135,7 +145,7 @@ export default class BulkFormController extends Controller { // Check if changed, and mark with class if it is. #checkIsChanged(element) { - if(!element.isConnected) return false; + if (!element.isConnected) return false; const changed = this.#isChanged(element); element.classList.toggle("changed", changed); @@ -143,9 +153,8 @@ export default class BulkFormController extends Controller { } #isChanged(element) { - if (element.type == "checkbox") { + if (element.type == "checkbox") { return element.defaultChecked !== undefined && element.checked != element.defaultChecked; - } else if (element.type == "select-one") { // (weird) Behavior of select element's include_blank option in Rails: // If a select field has include_blank option selected (its value will be ''), @@ -155,42 +164,49 @@ export default class BulkFormController extends Controller { opt.hasAttribute("selected"), ); const selectedOption = element.selectedOptions[0]; - const areBothBlank = selectedOption.value === '' && defaultSelected === undefined + const areBothBlank = selectedOption.value === "" && defaultSelected === undefined; return !areBothBlank && selectedOption !== defaultSelected; - } else { return element.defaultValue !== undefined && element.value != element.defaultValue; } } #removeAnimationClasses(productRowElement) { - productRowElement.classList.remove('slide-in'); - productRowElement.removeEventListener('animationend', this.#removeAnimationClasses.bind(this, productRowElement)); + productRowElement.classList.remove("slide-in"); + productRowElement.removeEventListener( + "animationend", + this.#removeAnimationClasses.bind(this, productRowElement), + ); } #observeProductsTableRows() { this.productsTableObserver = new MutationObserver((mutationList, _observer) => { const mutationRecord = mutationList[0]; - if(mutationRecord) { + if (mutationRecord) { // Right now we are only using it for product clone, so it's always first const productRowElement = mutationRecord.addedNodes[0]; if (productRowElement) { - productRowElement.addEventListener('animationend', this.#removeAnimationClasses.bind(this, productRowElement)); + productRowElement.addEventListener( + "animationend", + this.#removeAnimationClasses.bind(this, productRowElement), + ); // This is equivalent to form.elements. - const productRowFormElements = productRowElement.querySelectorAll('input, select, textarea, button'); + const productRowFormElements = productRowElement.querySelectorAll( + "input, select, textarea, button", + ); this.#registerElements(productRowFormElements); this.toggleFormChanged(); } } }); - const productsTable = document.querySelector('.products'); + const productsTable = document.querySelector(".products"); // Above mutation function will trigger, // whenever +products+ table rows (first level children) are mutated i.e. added or removed - // right now we are using this for product clone + // right now we are using this for product clone this.productsTableObserver.observe(productsTable, { childList: true }); } } diff --git a/spec/system/admin/products_v3/actions_spec.rb b/spec/system/admin/products_v3/actions_spec.rb index 43b776f05f8..9abb75cd56b 100644 --- a/spec/system/admin/products_v3/actions_spec.rb +++ b/spec/system/admin/products_v3/actions_spec.rb @@ -293,20 +293,6 @@ def save_preferences expect(input_content).to match /COPY OF Apples/ end end - - it "shows error message when cloning invalid record" do - # Existing product is invalid: - product_a.update_columns(name: nil) - - click_product_clone "Apples" - - expect(page).to have_content "Unit Scale can't be blank" - - within "table.products" do - # Products does not include the cloned product. - expect(all_input_values).not_to match /COPY OF Apples/ - end - end end end diff --git a/spec/system/admin/products_v3/create_spec.rb b/spec/system/admin/products_v3/create_spec.rb index d6db98d60c5..e767ab38911 100644 --- a/spec/system/admin/products_v3/create_spec.rb +++ b/spec/system/admin/products_v3/create_spec.rb @@ -114,7 +114,7 @@ expect(new_variant.display_name).to eq "Small bag" expect(new_variant.variant_unit).to eq "weight" expect(new_variant.variant_unit_scale).to eq 1 # g - expect(new_variant.unit_value).to eq 2.0 + expect(new_variant.unit_value).to eq 0.002 expect(new_variant.display_as).to eq "2 grams" expect(new_variant.unit_presentation).to eq "2 grams" expect(new_variant.price).to eq 11.1 diff --git a/spec/system/admin/products_v3/update_spec.rb b/spec/system/admin/products_v3/update_spec.rb index 73380fbdfff..98f73fc15e5 100644 --- a/spec/system/admin/products_v3/update_spec.rb +++ b/spec/system/admin/products_v3/update_spec.rb @@ -161,7 +161,7 @@ click_button "Save changes" expect(page).to have_content "Changes saved" - product_a.reload + variant_a1.reload }.to change{ variant_a1.variant_unit }.to("items") .and change{ variant_a1.variant_unit_name }.to("box") @@ -343,8 +343,9 @@ fill_in "Name", with: "Large box" fill_in "SKU", with: "APL-02" + tomselect_select("Weight (kg)", from: "Unit scale") click_on "Unit" # activate popout - fill_in "Unit value", with: "1000" + fill_in "Unit value", with: "1" fill_in "Price", with: 10.25 @@ -366,7 +367,9 @@ expect(new_variant.display_name).to eq "Large box" expect(new_variant.sku).to eq "APL-02" expect(new_variant.price).to eq 10.25 - expect(new_variant.unit_value).to eq 1000 + expect(new_variant.variant_unit).to eq "weight" + expect(new_variant.unit_value).to eq 1 * 1000 + expect(new_variant.variant_unit_scale).to eq 1000 expect(new_variant.on_hand).to eq 3 expect(new_variant.tax_category_id).to be_nil @@ -485,11 +488,12 @@ end context "with invalid data" do + let(:new_variant_row) { find_field("Name", placeholder: "Apples", with: "").ancestor("tr") } + before do click_on "New variant" # find empty row for Apples - new_variant_row = find_field("Name", placeholder: "Apples", with: "").ancestor("tr") expect(new_variant_row).to be_present within new_variant_row do @@ -508,6 +512,30 @@ fill_in "Price", with: "10.25" end + # Client side validation + click_button "Save changes" + within new_variant_row do + expect_browser_validation('select[aria-label="Unit scale"]', + "Please select an item in the list.") + end + + # Fix error + within new_variant_row do + tomselect_select("Weight (kg)", from: "Unit scale") + end + + # Client side validation + click_button "Save changes" + within new_variant_row do + expect_browser_validation('input[aria-label="Unit value"]', + "Please fill in this field.") + end + + # Fix error + within new_variant_row do + fill_in "Unit value", with: "200" + end + expect { click_button "Save changes" @@ -537,6 +565,13 @@ end it "saves changes after fixing errors" do + # Fill value to satisfy client side validation + within new_variant_row do + tomselect_select("Weight (kg)", from: "Unit scale") + click_on "Unit" # activate popout + fill_in "Unit value", with: "200" + end + expect { click_button "Save changes" @@ -547,9 +582,6 @@ fill_in "Name", with: "Nice box" fill_in "SKU", with: "APL-02" - click_on "Unit" # activate popout - fill_in "Unit value", with: "200" - select producer.name, from: 'Producer' select taxon.name, from: 'Category' end @@ -565,10 +597,18 @@ expect(new_variant.display_name).to eq "Nice box" expect(new_variant.sku).to eq "APL-02" expect(new_variant.price).to eq 10.25 - expect(new_variant.unit_value).to eq 200 + expect(new_variant.variant_unit_scale).to eq 1000 + expect(new_variant.unit_value).to eq 200 * 1000 end it "removes unsaved record" do + # Fill value to satisfy client side validation + within new_variant_row do + tomselect_select("Weight (kg)", from: "Unit scale") + click_on "Unit" # activate popout + fill_in "Unit value", with: "200" + end + click_button "Save changes" expect(page).to have_text("1 product could not be saved.") @@ -706,4 +746,9 @@ include_examples "updating image" end end + + def expect_browser_validation(selector, message) + browser_message = page.find(selector)["validationMessage"] + expect(browser_message).to eq message + end end From 25171413efe7cfbb4c25abbd438c594c121a7444 Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou Date: Tue, 6 Aug 2024 15:39:24 +1000 Subject: [PATCH 39/68] Update Spree::Price parsing to match LocalizedNumber.parse Spree::Price parsing was returning 0.0 when given a an empty string as price, resulting in a variant being valid even if no price was given. It only happened if `Spree::LocalizedNumber` wasn't used. Spree::LocalizedNumber` return nil if given a blank number. --- app/models/spree/price.rb | 1 + spec/models/spree/price_spec.rb | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/app/models/spree/price.rb b/app/models/spree/price.rb index 0c2d12c285f..9fcdac64597 100644 --- a/app/models/spree/price.rb +++ b/app/models/spree/price.rb @@ -38,6 +38,7 @@ def check_price # strips all non-price-like characters from the price, taking into account locale settings def parse_price(price) + return nil if price.blank? return price unless price.is_a?(String) separator, _delimiter = I18n.t([:'number.currency.format.separator', diff --git a/spec/models/spree/price_spec.rb b/spec/models/spree/price_spec.rb index 441a7a185b5..047d5da2683 100644 --- a/spec/models/spree/price_spec.rb +++ b/spec/models/spree/price_spec.rb @@ -34,5 +34,25 @@ module Spree expect(variant.reload.price).to eq 10.25 end end + + describe "#price=" do + subject { Spree::Price.new } + + context "with a number" do + it "returns the same number" do + subject.price = 12.5 + + expect(subject.price).to eq(12.5) + end + end + + context "with empty string" do + it "sets the price to nil" do + subject.price = "" + + expect(subject.price).to be_nil + end + end + end end end From cda57fdb44182820d8af7f832f48693a59cede78 Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou Date: Tue, 6 Aug 2024 15:43:42 +1000 Subject: [PATCH 40/68] Add toggleOnHand action It replicate the behavior of setOnDemand angular directive --- .../set_variant_on_demand.js.coffee | 19 --------------- .../controllers/edit_variant_controller.js | 23 +++++++++++-------- 2 files changed, 13 insertions(+), 29 deletions(-) delete mode 100644 app/assets/javascripts/admin/products/directives/set_variant_on_demand.js.coffee diff --git a/app/assets/javascripts/admin/products/directives/set_variant_on_demand.js.coffee b/app/assets/javascripts/admin/products/directives/set_variant_on_demand.js.coffee deleted file mode 100644 index d554a194a43..00000000000 --- a/app/assets/javascripts/admin/products/directives/set_variant_on_demand.js.coffee +++ /dev/null @@ -1,19 +0,0 @@ -angular.module("admin.products").directive "setOnDemand", -> - link: (scope, element, attr) -> - onHand = element.context.querySelector("#variant_on_hand") - onDemand = element.context.querySelector("#variant_on_demand") - - disableOnHandIfOnDemand = -> - if onDemand.checked - onHand.disabled = 'disabled' - onHand.dataStock = onHand.value - onHand.value = t('admin.products.variants.infinity') - - disableOnHandIfOnDemand() - - onDemand.addEventListener 'change', (event) -> - disableOnHandIfOnDemand() - - if !onDemand.checked - onHand.removeAttribute('disabled') - onHand.value = onHand.dataStock diff --git a/app/webpacker/controllers/edit_variant_controller.js b/app/webpacker/controllers/edit_variant_controller.js index 57b8a0ae854..a01fda48e25 100644 --- a/app/webpacker/controllers/edit_variant_controller.js +++ b/app/webpacker/controllers/edit_variant_controller.js @@ -6,6 +6,8 @@ import UnitPrices from "js/services/unit_prices"; // // TODO refactor so we can extract what's common with Bulk product page export default class EditVariantController extends Controller { + static targets = ["onHand"]; + connect() { this.unitPrices = new UnitPrices(); // idea: create a helper that includes a nice getter/setter for Rails model attr values, just pass it the attribute name. @@ -67,6 +69,17 @@ export default class EditVariantController extends Controller { // Make sure to clean up anything that happened outside } + toggleOnHand(event) { + if (event.target.checked === true) { + this.onHandTarget.dataStock = this.onHandTarget.value; + this.onHandTarget.value = I18n.t("admin.products.variants.infinity"); + this.onHandTarget.disabled = "disabled"; + } else { + this.onHandTarget.removeAttribute("disabled"); + this.onHandTarget.value = this.onHandTarget.dataStock; + } + } + // private // Extract variant_unit and variant_unit_scale from dropdown variant_unit_with_scale, @@ -156,14 +169,4 @@ export default class EditVariantController extends Controller { this.weight = this.element.querySelector('[id="variant_weight"]'); this.weight.parentElement.style.display = display; } - - //#showWeight() { - // this.weight = this.element.querySelector('[id="variant_weight"]'); - // this.weight.parentElement.style.display= "block" - //} - - //#hideWeight() { - // this.weight = this.element.querySelector('[id="variant_weight"]'); - // this.weight.parentElement.style.display= "none" - //} } From 4ae392490bed8d3fea0496da8596e57d424a9bc1 Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou Date: Wed, 7 Aug 2024 14:21:29 +1000 Subject: [PATCH 41/68] Fix variant system spec --- .../spree/admin/variants/_form.html.haml | 13 ++- spec/system/admin/variants_spec.rb | 96 ++++++++++++------- 2 files changed, 66 insertions(+), 43 deletions(-) diff --git a/app/views/spree/admin/variants/_form.html.haml b/app/views/spree/admin/variants/_form.html.haml index fddec7d1c58..7f73095cd97 100644 --- a/app/views/spree/admin/variants/_form.html.haml +++ b/app/views/spree/admin/variants/_form.html.haml @@ -60,18 +60,17 @@ %input{ "type" => "text", "id" => "variant_unit_price", "name" => "variant[unit_price]", "class" => 'fullwidth', "disabled" => true} %div{style: "color: black"} = t("spree.admin.products.new.unit_price_legend") - %div{ 'set-on-demand' => '' } + %div .field.checkbox %label - = f.check_box :on_demand + = f.check_box :on_demand, data: { "action": "click->edit-variant#toggleOnHand" } = t(:on_demand) - - #TODO tooltip is broken - %div{'ofn-with-tip' => t('admin.products.variants.to_order_tip')} - %a= t('admin.whats_this') + + = render partial: "admin/shared/tooltip", locals: { tooltip_text: t('admin.products.variants.to_order_tip'), link_text: t('admin.whats_this'), placement: "right" } .field = f.label :on_hand, t(:on_hand) .fullwidth - = f.text_field :on_hand + = f.text_field :on_hand, data: { "edit-variant-target": "onHand" } .right.six.columns.omega.label-block .field @@ -98,7 +97,7 @@ = f.collection_select(:primary_taxon_id, Spree::Taxon.order(:name), :id, :name, { include_blank: true }, { class: "select2 fullwidth" }) .field - = f.label :supplier, t(:spree_admin_supplier) + = f.label :supplier, raw(t(:spree_admin_supplier) + content_tag(:span, ' *', :class => 'required')) = f.collection_select(:supplier_id, @producers, :id, :name, {:include_blank => true}, {:class => "select2 fullwidth"}) .clear diff --git a/spec/system/admin/variants_spec.rb b/spec/system/admin/variants_spec.rb index 7de52b82721..79a49cbccda 100644 --- a/spec/system/admin/variants_spec.rb +++ b/spec/system/admin/variants_spec.rb @@ -13,18 +13,22 @@ describe "new variant" do it "creating a new variant" do - # Given a product with a unit-related option type - product = create(:simple_product, variant_unit: "weight", variant_unit_scale: "1") + # Given a product + product = create(:simple_product) # When I create a variant on the product login_as_admin visit spree.admin_product_variants_path product click_link 'New Variant' - fill_in 'unit_value_human', with: '1' - fill_in 'variant_unit_description', with: 'foo' + tomselect_select("Volume (L)", from: "Unit scale") + click_on "Unit" # activate popout + # Unit popout + fill_in "Unit value", with: "1" + fill_in 'Price', with: 2.5 select taxon.name, from: "variant_primary_taxon_id" select2_select product.variants.first.supplier.name, from: "variant_supplier_id" + click_button 'Create' # Then the variant should have been created @@ -33,7 +37,7 @@ it "creating a new variant from product variant page with filter" do # Given a product with a unit-related option type - product = create(:simple_product, variant_unit: "weight", variant_unit_scale: "1") + product = create(:simple_product) filter = { producerFilter: 2 } # When I create a variant on the product @@ -54,7 +58,7 @@ it "creating a new variant with non-weight unit type" do # Given a product with a unit-related option type - product = create(:simple_product, variant_unit: "volume", variant_unit_scale: "1") + product = create(:simple_product) # When I create a variant on the product login_as_admin @@ -62,11 +66,16 @@ click_link 'New Variant' - # Expect variant_weight to accept 3 decimal places - fill_in 'variant_weight', with: '1.234' - fill_in 'unit_value_human', with: 1 + tomselect_select("Volume (L)", from: "Unit scale") + click_on "Unit" # activate popout + # Unit popout + fill_in "Unit value", with: "1" + fill_in 'Price', with: 2.5 select taxon.name, from: "variant_primary_taxon_id" select2_select product.variants.first.supplier.name, from: "variant_supplier_id" + + # Expect variant_weight to accept 3 decimal places + fill_in 'variant_weight', with: '1.234' click_button 'Create' # Then the variant should have been created @@ -74,12 +83,20 @@ end it "show validation errors if present" do - product = create(:simple_product, variant_unit: "volume", variant_unit_scale: "1") + product = create(:simple_product) login_as_admin visit spree.admin_product_variants_path product click_link 'New Variant' - fill_in 'unit_value_human', with: 0 + tomselect_select("Volume (L)", from: "Unit scale") + fill_in 'Price', with: 2.5 + select taxon.name, from: "variant_primary_taxon_id" + select2_select product.variants.first.supplier.name, from: "variant_supplier_id" + + click_on "Unit" # activate popout + # Unit popout + fill_in "Unit value", with: "0" + # fill_in 'unit_value_human', with: 0 click_button 'Create' expect(page).to have_content "Unit value must be greater than 0" @@ -89,7 +106,7 @@ describe "viewing product variant" do it "when the product page has a product filter" do # Given a product with a unit-related option type - product = create(:simple_product, variant_unit: "weight", variant_unit_scale: "1") + product = create(:simple_product) filter = { producerFilter: 2 } # When I create a variant on the product @@ -123,7 +140,7 @@ describe "editing unit value and description for a variant" do it "when the product variant page has product filter" do - product = create(:simple_product, variant_unit: "weight", variant_unit_scale: "1") + product = create(:simple_product) filter = { producerFilter: 2 } # When I create a variant on the product @@ -140,9 +157,10 @@ it "when variant_unit is weight" do # Given a product with unit-related option types, with a variant - product = create(:simple_product, variant_unit: "weight", variant_unit_scale: "1") + product = create(:simple_product) variant = product.variants.first - variant.update( unit_value: 1, unit_description: 'foo' ) + variant.update( unit_value: 1, unit_description: 'foo', variant_unit: "weight", + variant_unit_scale: "1") # When I view the variant login_as_admin @@ -151,41 +169,47 @@ page.find('table.index .icon-edit').click # And I should see unit value and description fields for the unit-related option value - expect(page).to have_field "unit_value_human", with: "1" - expect(page).to have_field "variant_unit_description", with: "foo" + click_on "Unit" # activate popout + expect(page).to have_field "Unit value", with: "1 foo" # When I update the fields and save the variant - fill_in "unit_value_human", with: "123" - fill_in "variant_unit_description", with: "bar" + click_on "Unit" # activate popout + # Unit popout + fill_in "Unit value", with: "123 bar" + click_button 'Update' expect(page).to have_content %(Variant "#{product.name}" has been successfully updated!) # Then the unit value and description should have been saved - expect(variant.reload.unit_value).to eq(123) + variant.reload + expect(variant.unit_value).to eq(123) expect(variant.unit_description).to eq('bar') end it "can update unit_description when variant_unit is items" do - product = create(:simple_product, variant_unit: "items", variant_unit_name: "bunches") + product = create(:simple_product) variant = product.variants.first - variant.update(unit_description: 'foo') + variant.update(unit_description: 'foo', variant_unit: "items", variant_unit_name: "bunches") login_as_admin visit spree.edit_admin_product_variant_path(product, variant) - expect(page).not_to have_field "unit_value_human" expect(page).to have_field "variant_weight" - expect(page).to have_field "variant_unit_description", with: "foo" + click_on "Unit" # activate popout + expect(page).to have_field "Unit value", with: "1 foo" - fill_in "variant_unit_description", with: "bar" + click_on "Unit" # activate popout + # Unit popout + fill_in "Unit value", with: "123 bar" fill_in "variant_weight", with: "1.234" + click_button 'Update' expect(page).to have_content %(Variant "#{product.name}" has been successfully updated!) expect(variant.reload.unit_description).to eq('bar') end context "with ES as a locale" do - let(:product) { create(:simple_product, variant_unit: "weight", variant_unit_scale: "1") } + let(:product) { create(:simple_product) } let(:variant) { product.variants.first } around do |example| @@ -318,7 +342,7 @@ end describe "editing variant attributes" do - let!(:variant) { create(:variant) } + let!(:variant) { create(:variant, variant_unit: "weight", variant_unit_scale: "1") } let(:product) { variant.product } let!(:tax_category) { create(:tax_category) } @@ -330,34 +354,34 @@ it "editing display name for a variant" do # It should allow the display name to be changed expect(page).to have_field "variant_display_name" + click_on "Unit" # activate popout expect(page).to have_field "variant_display_as" # When I update the fields and save the variant fill_in "variant_display_name", with: "Display Name" + click_on "Unit" # activate popout fill_in "variant_display_as", with: "Display As This" + click_button 'Update' expect(page).to have_content %(Variant "#{product.name}" has been successfully updated!) # Then the displayed values should have been saved - expect(variant.reload.display_name).to eq("Display Name") + variant.reload + expect(variant.display_name).to eq("Display Name") expect(variant.display_as).to eq("Display As This") end it "editing weight for a variant" do # It should allow the weight to be changed - expect(page).to have_field "unit_value_human" + click_on "Unit" # activate popout + expect(page).to have_field "Unit value" # When I update the fields and save the variant with invalid value - fill_in "unit_value_human", with: "1.234" - click_button 'Update' - expect(page).not_to have_content %(Variant "#{product.name}" has been successfully updated!) - - fill_in "unit_value_human", with: "1.23" + fill_in "Unit value", with: "1.234" click_button 'Update' - expect(page).to have_content %(Variant "#{product.name}" has been successfully updated!) # Then the displayed values should have been saved - expect(variant.reload.unit_value).to eq(1.23) + expect(variant.reload.unit_value).to eq(1.234) end context "editing variant tax category" do From 893b541dca6c84d716c3bca84baf49ba1dca7244 Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou Date: Wed, 7 Aug 2024 16:01:58 +1000 Subject: [PATCH 42/68] Fix product system spec The pending spec are to be fix after a rebase, master currently as some changes which will make this easier --- .../edit_units_controller.js.coffee | 24 ------- .../controllers/units_controller.js.coffee | 27 ++++---- .../spree/admin/products/_form.html.haml | 21 +----- app/views/spree/admin/products/new.html.haml | 6 +- .../products/units_controller_spec.js.coffee | 56 +++++++-------- spec/system/admin/products_spec.rb | 69 ++++--------------- 6 files changed, 60 insertions(+), 143 deletions(-) delete mode 100644 app/assets/javascripts/admin/products/controllers/edit_units_controller.js.coffee diff --git a/app/assets/javascripts/admin/products/controllers/edit_units_controller.js.coffee b/app/assets/javascripts/admin/products/controllers/edit_units_controller.js.coffee deleted file mode 100644 index 196ca473193..00000000000 --- a/app/assets/javascripts/admin/products/controllers/edit_units_controller.js.coffee +++ /dev/null @@ -1,24 +0,0 @@ -angular.module("admin.products").controller "editUnitsCtrl", ($scope, VariantUnitManager) -> - - $scope.product = - variant_unit: angular.element('#variant_unit').val() - variant_unit_scale: angular.element('#variant_unit_scale').val() - - $scope.variant_unit_options = VariantUnitManager.variantUnitOptions() - - if $scope.product.variant_unit == 'items' - $scope.variant_unit_with_scale = 'items' - else - $scope.variant_unit_with_scale = $scope.product.variant_unit + '_' + $scope.product.variant_unit_scale.replace(/\.0$/, ''); - - $scope.setFields = -> - if $scope.variant_unit_with_scale == 'items' - variant_unit = 'items' - variant_unit_scale = null - else - options = $scope.variant_unit_with_scale.split('_') - variant_unit = options[0] - variant_unit_scale = options[1] - - $scope.product.variant_unit = variant_unit - $scope.product.variant_unit_scale = variant_unit_scale diff --git a/app/assets/javascripts/admin/products/controllers/units_controller.js.coffee b/app/assets/javascripts/admin/products/controllers/units_controller.js.coffee index a92f4aeadae..446dc1794bd 100644 --- a/app/assets/javascripts/admin/products/controllers/units_controller.js.coffee +++ b/app/assets/javascripts/admin/products/controllers/units_controller.js.coffee @@ -1,15 +1,14 @@ # Controller for "New Products" form (spree/admin/products/new) angular.module("admin.products") .controller "unitsCtrl", ($scope, VariantUnitManager, OptionValueNamer, UnitPrices, PriceParser) -> - $scope.product = { master: {} } - $scope.product.master.product = $scope.product + $scope.product = {} $scope.placeholder_text = "" - $scope.$watchCollection '[product.variant_unit_with_scale, product.master.unit_value_with_description, product.price, product.variant_unit_name]', -> + $scope.$watchCollection '[product.variant_unit_with_scale, product.unit_value_with_description, product.price, product.variant_unit_name]', -> $scope.processVariantUnitWithScale() $scope.processUnitValueWithDescription() $scope.processUnitPrice() - $scope.placeholder_text = new OptionValueNamer($scope.product.master).name() if $scope.product.variant_unit_scale + $scope.placeholder_text = new OptionValueNamer($scope.product).name() if $scope.product.variant_unit_scale $scope.variant_unit_options = VariantUnitManager.variantUnitOptions() @@ -38,24 +37,24 @@ angular.module("admin.products") # Extract unit_value and unit_description from text field unit_value_with_description, # and update hidden variant fields $scope.processUnitValueWithDescription = -> - if $scope.product.master.hasOwnProperty("unit_value_with_description") - match = $scope.product.master.unit_value_with_description.match(/^([\d\.,]+(?= *|$)|)( *)(.*)$/) + if $scope.product.hasOwnProperty("unit_value_with_description") + match = $scope.product.unit_value_with_description.match(/^([\d\.,]+(?= *|$)|)( *)(.*)$/) if match - $scope.product.master.unit_value = PriceParser.parse(match[1]) - $scope.product.master.unit_value = null if isNaN($scope.product.master.unit_value) - $scope.product.master.unit_value = window.bigDecimal.multiply($scope.product.master.unit_value, $scope.product.variant_unit_scale, 2) if $scope.product.master.unit_value && $scope.product.variant_unit_scale - $scope.product.master.unit_description = match[3] + $scope.product.unit_value = PriceParser.parse(match[1]) + $scope.product.unit_value = null if isNaN($scope.product.unit_value) + $scope.product.unit_value = window.bigDecimal.multiply($scope.product.unit_value, $scope.product.variant_unit_scale, 2) if $scope.product.unit_value && $scope.product.variant_unit_scale + $scope.product.unit_description = match[3] else - value = $scope.product.master.unit_value - value = window.bigDecimal.divide(value, $scope.product.variant_unit_scale, 2) if $scope.product.master.unit_value && $scope.product.variant_unit_scale - $scope.product.master.unit_value_with_description = value + " " + $scope.product.master.unit_description + value = $scope.product.unit_value + value = window.bigDecimal.divide(value, $scope.product.variant_unit_scale, 2) if $scope.product.unit_value && $scope.product.variant_unit_scale + $scope.product.unit_value_with_description = value + " " + $scope.product.unit_description # Calculate unit price based on product price and variant_unit_scale $scope.processUnitPrice = -> price = $scope.product.price scale = $scope.product.variant_unit_scale unit_type = $scope.product.variant_unit - unit_value = $scope.product.master.unit_value + unit_value = $scope.product.unit_value variant_unit_name = $scope.product.variant_unit_name $scope.unit_price = UnitPrices.displayableUnitPrice(price, scale, unit_type, unit_value, variant_unit_name) diff --git a/app/views/spree/admin/products/_form.html.haml b/app/views/spree/admin/products/_form.html.haml index 546efeace4f..ab9843b7e22 100644 --- a/app/views/spree/admin/products/_form.html.haml +++ b/app/views/spree/admin/products/_form.html.haml @@ -1,5 +1,5 @@ %div.admin-product-form-fields - .left.twelve.columns.alpha + .left.sixteen.columns.alpha = f.field_container :name do = f.label :name, raw(t(:name) + content_tag(:span, ' *', :class => 'required')) = f.text_field :name, :class => 'fullwidth title' @@ -10,25 +10,6 @@ = f.hidden_field :description, id: "product_description", value: @product.description %trix-editor{ input: "product_description", "data-controller": "trixeditor" } - .right.four.columns.omega - .variant_units_form{ 'ng-app' => 'admin.products', 'ng-controller' => 'editUnitsCtrl' } - - = f.field_container :units do - = f.label :variant_unit_with_scale, t(:spree_admin_variant_unit_scale) - %select.select2.fullwidth{ id: 'product_variant_unit_with_scale', 'ng-model' => 'variant_unit_with_scale', 'ng-change' => 'setFields()', 'ng-options' => 'unit[1] as unit[0] for unit in variant_unit_options' } - %option{'value' => ''} - - = f.text_field :variant_unit, {'id' => 'variant_unit', 'ng-value' => 'product.variant_unit', 'hidden' => true} - = f.text_field :variant_unit_scale, {'id' => 'variant_unit_scale', 'ng-value' => 'product.variant_unit_scale', 'hidden' => true} - - .variant_unit_name{'ng-show' => 'product.variant_unit == "items"'} - = f.field_container :variant_unit_name do - = f.label :variant_unit_name, t(:spree_admin_variant_unit_name) - = f.text_field :variant_unit_name, {placeholder: t('admin.products.unit_name_placeholder')} - = f.error_message_on :variant_unit_name - - .clear - .clear %div diff --git a/app/views/spree/admin/products/new.html.haml b/app/views/spree/admin/products/new.html.haml index a7162157380..d495fc170d9 100644 --- a/app/views/spree/admin/products/new.html.haml +++ b/app/views/spree/admin/products/new.html.haml @@ -48,9 +48,9 @@ = f.field_container :unit_value do = f.label :unit_value, t(".value"), 'ng-disabled' => "!hasUnit(product)" %span.required * - = f.text_field :unit_value, placeholder: "eg. 2", 'ng-model' => 'product.master.unit_value_with_description', class: 'fullwidth', 'ng-disabled' => "!hasUnit(product)" - %input{ type: 'hidden', 'ng-value': 'product.master.unit_value', "ng-init": "product.master.unit_value='#{@product.unit_value}'", name: 'product[unit_value]' } - %input{ type: 'hidden', 'ng-value': 'product.master.unit_description', "ng-init": "product.master.unit_description='#{@product.unit_description}'", name: 'product[unit_description]' } + = f.text_field :unit_value, placeholder: "eg. 2", 'ng-model' => 'product.unit_value_with_description', class: 'fullwidth', 'ng-disabled' => "!hasUnit(product)" + %input{ type: 'hidden', 'ng-value': 'product.unit_value', "ng-init": "product.unit_value='#{@product.unit_value}'", name: 'product[unit_value]' } + %input{ type: 'hidden', 'ng-value': 'product.unit_description', "ng-init": "product.unit_description='#{@product.unit_description}'", name: 'product[unit_description]' } = f.error_message_on :unit_value = render 'display_as', f: f .six.columns.omega{ 'ng-show' => "product.variant_unit_with_scale == 'items'" } diff --git a/spec/javascripts/unit/admin/products/units_controller_spec.js.coffee b/spec/javascripts/unit/admin/products/units_controller_spec.js.coffee index b0e7908b025..4b65a40a241 100644 --- a/spec/javascripts/unit/admin/products/units_controller_spec.js.coffee +++ b/spec/javascripts/unit/admin/products/units_controller_spec.js.coffee @@ -41,64 +41,64 @@ describe "unitsCtrl", -> describe "interpretting unit_value_with_description", -> beforeEach -> - scope.product.master = {} + scope.product = {} describe "when a variant_unit_scale is present", -> beforeEach -> scope.product.variant_unit_scale = 1 it "splits by whitespace in to unit_value and unit_description", -> - scope.product.master.unit_value_with_description = "12 boxes" + scope.product.unit_value_with_description = "12 boxes" scope.processUnitValueWithDescription() - expect(scope.product.master.unit_value).toEqual 12 - expect(scope.product.master.unit_description).toEqual "boxes" + expect(scope.product.unit_value).toEqual 12 + expect(scope.product.unit_description).toEqual "boxes" it "uses whole string as unit_value when only numerical characters are present", -> - scope.product.master.unit_value_with_description = "12345" + scope.product.unit_value_with_description = "12345" scope.processUnitValueWithDescription() - expect(scope.product.master.unit_value).toEqual 12345 - expect(scope.product.master.unit_description).toEqual '' + expect(scope.product.unit_value).toEqual 12345 + expect(scope.product.unit_description).toEqual '' it "uses whole string as description when string does not start with a number", -> - scope.product.master.unit_value_with_description = "boxes 12" + scope.product.unit_value_with_description = "boxes 12" scope.processUnitValueWithDescription() - expect(scope.product.master.unit_value).toEqual null - expect(scope.product.master.unit_description).toEqual "boxes 12" + expect(scope.product.unit_value).toEqual null + expect(scope.product.unit_description).toEqual "boxes 12" it "does not require whitespace to split unit value and description", -> - scope.product.master.unit_value_with_description = "12boxes" + scope.product.unit_value_with_description = "12boxes" scope.processUnitValueWithDescription() - expect(scope.product.master.unit_value).toEqual 12 - expect(scope.product.master.unit_description).toEqual "boxes" + expect(scope.product.unit_value).toEqual 12 + expect(scope.product.unit_description).toEqual "boxes" it "once a whitespace occurs, all subsequent numerical characters are counted as description", -> - scope.product.master.unit_value_with_description = "123 54 boxes" + scope.product.unit_value_with_description = "123 54 boxes" scope.processUnitValueWithDescription() - expect(scope.product.master.unit_value).toEqual 123 - expect(scope.product.master.unit_description).toEqual "54 boxes" + expect(scope.product.unit_value).toEqual 123 + expect(scope.product.unit_description).toEqual "54 boxes" it "handle final point as decimal separator", -> - scope.product.master.unit_value_with_description = "22.22" + scope.product.unit_value_with_description = "22.22" scope.processUnitValueWithDescription() - expect(scope.product.master.unit_value).toEqual 22.22 - expect(scope.product.master.unit_description).toEqual "" + expect(scope.product.unit_value).toEqual 22.22 + expect(scope.product.unit_description).toEqual "" it "handle comma as decimal separator", -> - scope.product.master.unit_value_with_description = "22,22" + scope.product.unit_value_with_description = "22,22" scope.processUnitValueWithDescription() - expect(scope.product.master.unit_value).toEqual 22.22 - expect(scope.product.master.unit_description).toEqual "" - + expect(scope.product.unit_value).toEqual 22.22 + expect(scope.product.unit_description).toEqual "" + it "handle comma as decimal separator with description", -> - scope.product.master.unit_value_with_description = "22,22 things" + scope.product.unit_value_with_description = "22,22 things" scope.processUnitValueWithDescription() - expect(scope.product.master.unit_value).toEqual 22.22 - expect(scope.product.master.unit_description).toEqual "things" + expect(scope.product.unit_value).toEqual 22.22 + expect(scope.product.unit_description).toEqual "things" it "handles nice rounded division", -> # this is a bit absurd, but it assure use that bigDecimal is called window.bigDecimal.multiply.and.returnValue 0.7 - scope.product.master.unit_value_with_description = "700" + scope.product.unit_value_with_description = "700" scope.product.variant_unit_scale = 0.001 scope.processUnitValueWithDescription() - expect(scope.product.master.unit_value).toEqual 0.7 + expect(scope.product.unit_value).toEqual 0.7 diff --git a/spec/system/admin/products_spec.rb b/spec/system/admin/products_spec.rb index 37ad9c967cc..9545f05733b 100644 --- a/spec/system/admin/products_spec.rb +++ b/spec/system/admin/products_spec.rb @@ -50,11 +50,10 @@ expect(page.find("#product_description", visible: false).value).to eq('
A description...
') expect(page.find("#product_variant_unit_field")).to have_content 'Weight (kg)' - - expect(page).to have_content "Name can't be blank" end it "display all attributes when submitting with error: Unit Value must be grater than 0" do + pending "rebase so we can add needed validation" select 'New supplier', from: 'product_supplier_id' fill_in 'product_name', with: "new product name" select "Weight (kg)", from: 'product_variant_unit_with_scale' @@ -111,21 +110,23 @@ expect(current_path).to eq spree.admin_products_path expect(flash_message).to eq('Product "A new product !!!" has been successfully created!') + product = Spree::Product.find_by(name: 'A new product !!!') - expect(product.variant_unit).to eq('weight') - expect(product.variant_unit_scale).to eq(1000) - expect(product.variants.first.unit_value).to eq(5000) - expect(product.variants.first.unit_description).to eq("") - expect(product.variant_unit_name).to eq("") - expect(product.variants.first.primary_taxon_id).to eq(taxon.id) - expect(product.variants.first.price.to_s).to eq('19.99') - expect(product.on_hand).to eq(5) - expect(product.variants.first.tax_category_id).to eq(tax_category.id) - expect(product.variants.first.shipping_category).to eq(shipping_category) + variant = product.variants.first + expect(product.description).to eq("
A description...
") expect(product.group_buy).to be_falsey - variant = product.variants.first + expect(variant.variant_unit).to eq('weight') + expect(variant.variant_unit_scale).to eq(1000) + expect(variant.unit_value).to eq(5000) + expect(variant.unit_description).to eq("") + expect(variant.variant_unit_name).to eq("") + expect(variant.primary_taxon_id).to eq(taxon.id) + expect(variant.price.to_s).to eq('19.99') + expect(variant.on_hand).to eq(5) + expect(variant.tax_category_id).to eq(tax_category.id) + expect(variant.shipping_category).to eq(shipping_category) expect(variant.unit_presentation).to eq("5kg") expect(variant.supplier).to eq(supplier) end @@ -153,6 +154,7 @@ end it "creating product with empty unit value" do + pending "rebase" fill_in 'product_name', with: 'Hot Cakes' select 'New supplier', from: 'product_supplier_id' select "Weight (kg)", from: 'product_variant_unit_with_scale' @@ -640,46 +642,5 @@ expect("#{uri.path}?#{uri.query}").to eq spree.admin_product_images_path(product, filter) end end - - context "editing a product's variant unit scale" do - let(:product) { create(:simple_product, name: 'a product', supplier_id: supplier2.id) } - - before do - allow(Spree::Config).to receive(:available_units).and_return("g,lb,oz,kg,T,mL,L,kL") - visit spree.edit_admin_product_path product - end - - shared_examples 'selecting a unit from dropdown' do |dropdown_option, - var_unit:, var_unit_scale:| - it 'checks if the dropdown selection is persistent' do - select dropdown_option, from: 'product_variant_unit_with_scale' - click_button 'Update' - expect(flash_message).to eq('Product "a product" has been successfully updated!') - product.reload - expect(product.variant_unit).to eq(var_unit) - expect(page).to have_select('product_variant_unit_with_scale', selected: dropdown_option) - expect(product.variant_unit_scale).to eq(var_unit_scale) - end - end - - describe 'a shared example' do - it_behaves_like 'selecting a unit from dropdown', 'Weight (g)', var_unit: 'weight', - var_unit_scale: 1 - it_behaves_like 'selecting a unit from dropdown', 'Weight (kg)', var_unit: 'weight', - var_unit_scale: 1000 - it_behaves_like 'selecting a unit from dropdown', 'Weight (T)', var_unit: 'weight', - var_unit_scale: 1_000_000 - it_behaves_like 'selecting a unit from dropdown', 'Weight (oz)', var_unit: 'weight', - var_unit_scale: 28.35 - it_behaves_like 'selecting a unit from dropdown', 'Weight (lb)', var_unit: 'weight', - var_unit_scale: 453.6 - it_behaves_like 'selecting a unit from dropdown', 'Volume (mL)', var_unit: 'volume', - var_unit_scale: 0.001 - it_behaves_like 'selecting a unit from dropdown', 'Volume (L)', var_unit: 'volume', - var_unit_scale: 1 - it_behaves_like 'selecting a unit from dropdown', 'Volume (kL)', var_unit: 'volume', - var_unit_scale: 1000 - end - end end end From 8ec1f61cd76d3b426f1277fa28cc74dc946c14ce Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou Date: Mon, 12 Aug 2024 14:01:51 +1000 Subject: [PATCH 43/68] Fix legacy bulk edit product system spec --- .../admin/services/bulk_products.js.coffee | 36 +++--- .../services/bulk_products_spec.js.coffee | 120 +++++++++--------- spec/system/admin/bulk_product_update_spec.rb | 77 ++++++----- 3 files changed, 124 insertions(+), 109 deletions(-) diff --git a/app/assets/javascripts/admin/services/bulk_products.js.coffee b/app/assets/javascripts/admin/services/bulk_products.js.coffee index e8b8bb6c3d8..a2823bf9970 100644 --- a/app/assets/javascripts/admin/services/bulk_products.js.coffee +++ b/app/assets/javascripts/admin/services/bulk_products.js.coffee @@ -19,7 +19,7 @@ angular.module("ofn.admin").factory "BulkProducts", (ProductResource, dataFetche for server_product in serverProducts product = @findProductInList(server_product.id, @products) product.variants = server_product.variants - @loadVariantUnitValues product + @loadVariantUnitValues product.variants find: (id) -> @findProductInList id, @products @@ -38,34 +38,32 @@ angular.module("ofn.admin").factory "BulkProducts", (ProductResource, dataFetche @products.splice(index + 1, 0, newProduct) unpackProduct: (product) -> - #$scope.matchProducer product @loadVariantUnit product loadVariantUnit: (product) -> - product.variant_unit_with_scale = - if product.variant_unit && product.variant_unit_scale && product.variant_unit != 'items' - "#{product.variant_unit}_#{product.variant_unit_scale}" - else if product.variant_unit - product.variant_unit - else - null + @loadVariantUnitValues product.variants if product.variants - @loadVariantUnitValues product if product.variants - @loadVariantUnitValue product, product.master if product.master + loadVariantUnitValues: (variants) -> + for variant in variants + @loadVariantUnitValue variant - loadVariantUnitValues: (product) -> - for variant in product.variants - @loadVariantUnitValue product, variant + loadVariantUnitValue: (variant) -> + variant.variant_unit_with_scale = + if variant.variant_unit && variant.variant_unit_scale && variant.variant_unit != 'items' + "#{variant.variant_unit}_#{variant.variant_unit_scale}" + else if variant.variant_unit + variant.variant_unit + else + null - loadVariantUnitValue: (product, variant) -> - unit_value = @variantUnitValue product, variant + unit_value = @variantUnitValue variant unit_value = if unit_value? then unit_value else '' variant.unit_value_with_description = "#{unit_value} #{variant.unit_description || ''}".trim() - variantUnitValue: (product, variant) -> + variantUnitValue: (variant) -> if variant.unit_value? - if product.variant_unit_scale - variant_unit_value = @divideAsInteger variant.unit_value, product.variant_unit_scale + if variant.variant_unit_scale + variant_unit_value = @divideAsInteger variant.unit_value, variant.variant_unit_scale parseFloat(window.bigDecimal.round(variant_unit_value, 2)) else variant.unit_value diff --git a/spec/javascripts/unit/admin/services/bulk_products_spec.js.coffee b/spec/javascripts/unit/admin/services/bulk_products_spec.js.coffee index 466c09109f9..ea741adc35b 100644 --- a/spec/javascripts/unit/admin/services/bulk_products_spec.js.coffee +++ b/spec/javascripts/unit/admin/services/bulk_products_spec.js.coffee @@ -53,126 +53,128 @@ describe "BulkProducts service", -> describe "loading variant unit", -> describe "setting product variant_unit_with_scale field", -> - it "sets by combining variant_unit and variant_unit_scale", -> + it "HERE 2 sets by combining variant_unit and variant_unit_scale", -> product = - variant_unit: "volume" - variant_unit_scale: .001 + variants:[ + id: 10 + variant_unit: "volume" + variant_unit_scale: .001 + ] BulkProducts.loadVariantUnit product - expect(product.variant_unit_with_scale).toEqual "volume_0.001" + expect(product.variants[0].variant_unit_with_scale).toEqual "volume_0.001" it "sets to null when variant_unit is null", -> - product = {variant_unit: null, variant_unit_scale: 1000} + product = + variants: [ + {variant_unit: null, variant_unit_scale: 1000} + ] BulkProducts.loadVariantUnit product - expect(product.variant_unit_with_scale).toBeNull() + + expect(product.variants[0].variant_unit_with_scale).toBeNull() it "sets to variant_unit when variant_unit_scale is null", -> - product = {variant_unit: 'items', variant_unit_scale: null, variant_unit_name: 'foo'} + product = + variants: [ + {variant_unit: 'items', variant_unit_scale: null, variant_unit_name: 'foo'} + ] BulkProducts.loadVariantUnit product - expect(product.variant_unit_with_scale).toEqual "items" + expect(product.variants[0].variant_unit_with_scale).toEqual "items" it "sets to variant_unit when variant_unit is 'items'", -> - product = {variant_unit: 'items', variant_unit_scale: 1000, variant_unit_name: 'foo'} + product = + variants: [ + {variant_unit: 'items', variant_unit_scale: 1000, variant_unit_name: 'foo'} + ] BulkProducts.loadVariantUnit product - expect(product.variant_unit_with_scale).toEqual "items" - - it "loads data for variants (incl. master)", -> - spyOn BulkProducts, "loadVariantUnitValues" - spyOn BulkProducts, "loadVariantUnitValue" - - product = - variant_unit_scale: 1.0 - master: {id: 1, unit_value: 1, unit_description: '(one)'} - variants: [{id: 2, unit_value: 2, unit_description: '(two)'}] - BulkProducts.loadVariantUnit product - - expect(BulkProducts.loadVariantUnitValues).toHaveBeenCalledWith product - expect(BulkProducts.loadVariantUnitValue).toHaveBeenCalledWith product, product.master + expect(product.variants[0].variant_unit_with_scale).toEqual "items" it "loads data for variants (excl. master)", -> spyOn BulkProducts, "loadVariantUnitValue" product = - variant_unit_scale: 1.0 - master: {id: 1, unit_value: 1, unit_description: '(one)'} - variants: [{id: 2, unit_value: 2, unit_description: '(two)'}] - BulkProducts.loadVariantUnitValues product + variants: [ + {id: 2, variant_unit_scale: 1.0, unit_value: 2, unit_description: '(two)'} + ] + BulkProducts.loadVariantUnitValues product.variants - expect(BulkProducts.loadVariantUnitValue).toHaveBeenCalledWith product, product.variants[0] - expect(BulkProducts.loadVariantUnitValue).not.toHaveBeenCalledWith product, product.master + expect(BulkProducts.loadVariantUnitValue).toHaveBeenCalledWith product.variants[0] describe "setting variant unit_value_with_description", -> it "sets by combining unit_value and unit_description", -> product = - variant_unit_scale: 1.0 - variants: [{id: 1, unit_value: 1, unit_description: '(bottle)'}] - BulkProducts.loadVariantUnitValues product, product.variants[0] + variants: [ + {id: 1, variant_unit_scale: 1.0, unit_value: 1, unit_description: '(bottle)'} + ] + BulkProducts.loadVariantUnitValues product.variants expect(product.variants[0]).toEqual id: 1 + variant_unit_scale: 1.0, + variant_unit_with_scale: null, unit_value: 1 unit_description: '(bottle)' unit_value_with_description: '1 (bottle)' it "uses unit_value when description is missing", -> product = - variant_unit_scale: 1.0 - variants: [{id: 1, unit_value: 1}] - BulkProducts.loadVariantUnitValues product, product.variants[0] + variants: [ + {id: 1, variant_unit_scale: 1.0, unit_value: 1} + ] + BulkProducts.loadVariantUnitValues product.variants expect(product.variants[0].unit_value_with_description).toEqual '1' it "uses unit_description when value is missing", -> product = - variant_unit_scale: 1.0 - variants: [{id: 1, unit_description: 'Small'}] - BulkProducts.loadVariantUnitValues product, product.variants[0] + variants: [ + {id: 1, variant_unit_scale: 1.0, unit_description: 'Small'} + ] + BulkProducts.loadVariantUnitValues product.variants expect(product.variants[0].unit_value_with_description).toEqual 'Small' it "converts values from base value to chosen unit", -> product = - variant_unit_scale: 1000.0 - variants: [{id: 1, unit_value: 2500}] - BulkProducts.loadVariantUnitValues product, product.variants[0] + variants: [ + id: 1, variant_unit_scale: 1000.0, unit_value: 2500 + ] + BulkProducts.loadVariantUnitValues product.variants expect(product.variants[0].unit_value_with_description).toEqual '2.5' it "converts values from base value to chosen unit without breaking precision", -> product = - variant_unit_scale: 0.001 - variants: [{id: 1, unit_value: 0.35}] - BulkProducts.loadVariantUnitValues product, product.variants[0] + variants: [ + {id: 1,variant_unit_scale: 0.001, unit_value: 0.35} + ] + BulkProducts.loadVariantUnitValues product.variants expect(product.variants[0].unit_value_with_description).toEqual '350' it "displays a unit_value of zero", -> product = - variant_unit_scale: 1.0 - variants: [{id: 1, unit_value: 0}] - BulkProducts.loadVariantUnitValues product, product.variants[0] + variants: [ + {id: 1, variant_unit_scale: 1.0, unit_value: 0} + ] + BulkProducts.loadVariantUnitValues product.variants expect(product.variants[0].unit_value_with_description).toEqual '0' describe "calculating the scaled unit value for a variant", -> it "returns the scaled value when variant has a unit_value", -> - product = {variant_unit_scale: 0.001} - variant = {unit_value: 5} - expect(BulkProducts.variantUnitValue(product, variant)).toEqual 5000 + variant = {variant_unit_scale: 0.001, unit_value: 5} + expect(BulkProducts.variantUnitValue(variant)).toEqual 5000 it "returns the scaled value rounded off upto 2 decimal points", -> - product = {variant_unit_scale: 28.35} - variant = {unit_value: 1234.5} - expect(BulkProducts.variantUnitValue(product, variant)).toEqual 43.54 + variant = {variant_unit_scale: 28.35, unit_value: 1234.5} + expect(BulkProducts.variantUnitValue(variant)).toEqual 43.54 it "returns the unscaled value when the product has no scale", -> - product = {} variant = {unit_value: 5} - expect(BulkProducts.variantUnitValue(product, variant)).toEqual 5 + expect(BulkProducts.variantUnitValue(variant)).toEqual 5 it "returns zero when the value is zero", -> - product = {} variant = {unit_value: 0} - expect(BulkProducts.variantUnitValue(product, variant)).toEqual 0 + expect(BulkProducts.variantUnitValue(variant)).toEqual 0 it "returns null when the variant has no unit_value", -> - product = {} variant = {} - expect(BulkProducts.variantUnitValue(product, variant)).toEqual null + expect(BulkProducts.variantUnitValue(variant)).toEqual null describe "fetching a product by id", -> diff --git a/spec/system/admin/bulk_product_update_spec.rb b/spec/system/admin/bulk_product_update_spec.rb index e78b5e35050..704cc30407f 100644 --- a/spec/system/admin/bulk_product_update_spec.rb +++ b/spec/system/admin/bulk_product_update_spec.rb @@ -72,19 +72,21 @@ end it "displays a select box for the unit of measure for the product's variants" do - p = FactoryBot.create(:product, variant_unit: 'weight', variant_unit_scale: 1, - variant_unit_name: '') + create(:product, variant_unit: 'weight', variant_unit_scale: 1, + variant_unit_name: '') visit spree.admin_products_path + click_expand_all expect(page).to have_select "variant_unit_with_scale", selected: "Weight (g)" end it "displays a text field for the item name when unit is set to 'Items'" do - p = FactoryBot.create(:product, variant_unit: 'items', variant_unit_scale: nil, - variant_unit_name: 'packet') + create(:product, variant_unit: 'items', variant_unit_scale: nil, + variant_unit_name: 'packet') visit spree.admin_products_path + click_expand_all expect(page).to have_select "variant_unit_with_scale", selected: "Items" expect(page).to have_field "variant_unit_name", with: "packet" @@ -145,17 +147,15 @@ end it "displays a unit value field (for each variant) for each product" do - p1 = FactoryBot.create(:product, price: 2.0, variant_unit: "weight", - variant_unit_scale: "1000") - v1 = FactoryBot.create(:variant, product: p1, price: 12.75, - unit_value: 1200, unit_description: "(small bag)", - display_as: "bag") - v2 = FactoryBot.create(:variant, product: p1, price: 2.50, - unit_value: 4800, unit_description: "(large bag)", - display_as: "bin") + p1 = create(:product, price: 2.0, variant_unit: "weight", variant_unit_scale: "1000") + v1 = create(:variant, product: p1, price: 12.75, unit_value: 1200, variant_unit_scale: "1000", + unit_description: "(small bag)", display_as: "bag") + v2 = create(:variant, product: p1, price: 2.50, unit_value: 4800, variant_unit_scale: "1000", + unit_description: "(large bag)", display_as: "bin") visit spree.admin_products_path expect(page).to have_selector "a.view-variants", count: 1 + all("a.view-variants").each(&:click) expect(page).to have_field "variant_unit_value_with_description", with: "1.2 (small bag)" @@ -220,7 +220,7 @@ let!(:new_supplier) { create(:supplier_enterprise) } let!(:product) { create(:product, variant_unit: 'weight', variant_unit_scale: 1000, supplier_id: supplier.id) - } + } # Weight (kg) before do login_as_admin @@ -249,6 +249,7 @@ # When I fill out variant details and hit update select new_supplier.name, from: 'producer_id' + tomselect_select "Weight (kg)", from: "variant_unit_with_scale" fill_in "variant_display_name", with: "Case of 12 Bottles" fill_in "variant_unit_value_with_description", with: "3 (12x250 mL bottles)" fill_in "variant_display_as", with: "Case" @@ -290,6 +291,7 @@ within "tr#v_-1" do select supplier.name, from: 'producer_id' + tomselect_select "Weight (kg)", from: "variant_unit_with_scale" fill_in "variant_unit_value_with_description", with: "120" fill_in "variant_price", with: "6.66" end @@ -304,6 +306,7 @@ end within "tr#v_-1" do + tomselect_select "Weight (g)", from: "variant_unit_with_scale" fill_in "variant_unit_value_with_description", with: "120g" fill_in "variant_price", with: "6.66" fill_in "variant_on_hand", with: "222" @@ -322,6 +325,7 @@ end within "tr#v_-1" do + tomselect_select "Weight (g)", from: "variant_unit_with_scale" fill_in "variant_unit_value_with_description", with: "120g" fill_in "variant_price", with: "6.66" check "variant_on_demand" @@ -335,12 +339,13 @@ end it "updating product attributes" do - s1 = FactoryBot.create(:supplier_enterprise) - s2 = FactoryBot.create(:supplier_enterprise) - t1 = FactoryBot.create(:taxon) - t2 = FactoryBot.create(:taxon) - p = FactoryBot.create(:product, supplier_id: s1.id, variant_unit: 'volume', - variant_unit_scale: 1, primary_taxon: t2, sku: "OLD SKU") + s1 = create(:supplier_enterprise) + create(:supplier_enterprise) + create(:taxon) + t2 = create(:taxon) + p = create(:product, supplier_id: s1.id, variant_unit: 'volume', variant_unit_scale: 1, + primary_taxon: t2, sku: "OLD SKU") + variant = p.variants.first login_as_admin visit spree.admin_products_path @@ -348,34 +353,43 @@ toggle_columns /^Category?/i, "Inherits Properties?", "SKU" within "tr#p_#{p.id}" do + page.find('a.view-variants').click + expect(page).to have_field "product_name", with: p.name - expect(page).to have_select "variant_unit_with_scale", selected: "Volume (L)" expect(page).to have_checked_field "inherits_properties" expect(page).to have_field "product_sku", with: p.sku fill_in "product_name", with: "Big Bag Of Potatoes" - select "Weight (kg)", from: "variant_unit_with_scale" uncheck "inherits_properties" fill_in "product_sku", with: "NEW SKU" end + within "tr#v_#{variant.id}" do + expect(page).to have_select "variant_unit_with_scale", selected: "Volume (L)" + + tomselect_select "Weight (kg)", from: "variant_unit_with_scale" + end click_button 'Save Changes', match: :first expect(page.find("#status-message")).to have_content "Changes saved." p.reload expect(p.name).to eq "Big Bag Of Potatoes" - expect(p.variant_unit).to eq "weight" - expect(p.variant_unit_scale).to eq 1000 # Kg expect(p.inherits_properties).to be false expect(p.sku).to eq "NEW SKU" + + variant.reload + expect(variant.variant_unit).to eq "weight" + expect(variant.variant_unit_scale).to eq 1000 # Kg end it "updating a product with a variant unit of 'items'" do - p = FactoryBot.create(:product, variant_unit: 'weight', variant_unit_scale: 1000) + p = create(:product, variant_unit: 'weight', variant_unit_scale: 1000) login_as_admin visit spree.admin_products_path + page.find('a.view-variants').click + expect(page).to have_select "variant_unit_with_scale", selected: "Weight (kg)" select "Items", from: "variant_unit_with_scale" @@ -384,10 +398,10 @@ click_button 'Save Changes', match: :first expect(page.find("#status-message")).to have_content "Changes saved." - p.reload - expect(p.variant_unit).to eq "items" - expect(p.variant_unit_scale).to be_nil - expect(p.variant_unit_name).to eq "loaf" + variant = p.variants.first + expect(variant.variant_unit).to eq "items" + expect(variant.variant_unit_scale).to be_nil + expect(variant.variant_unit_name).to eq "loaf" end it "updating a product with variants" do @@ -853,14 +867,15 @@ expect(page).to have_field "product_name", with: p.name fill_in "product_name", with: "Big Bag Of Potatoes" - select "Weight (kg)", from: "variant_unit_with_scale" find("a.view-variants").click end within "#v_#{v.id}" do expect(page).to have_select "producer_id", selected: supplier_permitted.name + select supplier_managed2.name, from: 'producer_id' + select "Weight (kg)", from: "variant_unit_with_scale" fill_in "variant_price", with: "20" fill_in "variant_on_hand", with: "18" fill_in "variant_display_as", with: "Big Bag" @@ -872,8 +887,8 @@ p.reload v.reload expect(p.name).to eq "Big Bag Of Potatoes" - expect(p.variant_unit).to eq "weight" - expect(p.variant_unit_scale).to eq 1000 # Kg + expect(v.variant_unit).to eq "weight" + expect(v.variant_unit_scale).to eq 1000 # Kg expect(v.supplier).to eq supplier_managed2 expect(v.display_as).to eq "Big Bag" expect(v.price).to eq 20.0 From 844cab458e8c6b22840675bd90d2f6ad786b5d78 Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou Date: Mon, 12 Aug 2024 14:14:30 +1000 Subject: [PATCH 44/68] Post rebase fix product import system spec --- spec/system/admin/product_import_spec.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/spec/system/admin/product_import_spec.rb b/spec/system/admin/product_import_spec.rb index 49c90699180..a8095dc3383 100644 --- a/spec/system/admin/product_import_spec.rb +++ b/spec/system/admin/product_import_spec.rb @@ -704,8 +704,9 @@ expect(page).to have_input("[products][2][variants_attributes][0][display_name]", text: "Cupcake") - expect(page).to have_select "_products_2_variant_unit_with_scale", selected: "Items" - expect(page).to have_input("[products][2][variant_unit_name]", + expect(page).to have_select("_products_2_variants_attributes_0_variant_unit_with_scale", + selected: "Items") + expect(page).to have_input("[products][2][variants_attributes][0][variant_unit_name]", text: "Bunch") within(:xpath, '//*[@id="products-form"]/table/tbody[3]/tr[2]/td[7]') do expect(page).to have_content("5") From f7446749ffbf594feb3f94c9adbabadffa6dc03e Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou Date: Mon, 12 Aug 2024 14:37:25 +1000 Subject: [PATCH 45/68] Fix Unit price system spec --- spec/system/admin/unit_price_spec.rb | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/spec/system/admin/unit_price_spec.rb b/spec/system/admin/unit_price_spec.rb index f8682f12b67..6375af57e87 100644 --- a/spec/system/admin/unit_price_spec.rb +++ b/spec/system/admin/unit_price_spec.rb @@ -30,7 +30,10 @@ login_as_admin visit spree.admin_product_variants_path product click_link 'New Variant' - fill_in 'Weight (g)', with: '1' + + tomselect_select "Weight (g)", from: "Unit scale" + click_on "Unit" # activate popout + fill_in "Unit value", with: "1" fill_in 'Price', with: '1' expect(find_field("Unit Price", disabled: true).value).to eq '$1,000.00 / kg' @@ -66,7 +69,10 @@ visit spree.admin_dashboard_path(locale: 'es') visit spree.admin_product_variants_path product click_link 'Nueva Variante' - fill_in 'Peso (g)', with: '1' + + tomselect_select "Peso (g)", from: "Unit scale" + click_on "Unit" # activate popout + fill_in "Unit value", with: "1" fill_in 'Precio', with: '1,5' expect(find_field("Precio por unidad", disabled: true).value).to eq '1.500,00 $ / kg' From 977b6e6c2aeca1be63fbaf539b21eb6bdd7ac5c3 Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou Date: Mon, 12 Aug 2024 14:45:16 +1000 Subject: [PATCH 46/68] Fix minor differences in local env and CI --- spec/system/admin/products_v3/update_spec.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/spec/system/admin/products_v3/update_spec.rb b/spec/system/admin/products_v3/update_spec.rb index 98f73fc15e5..2d6bd09accf 100644 --- a/spec/system/admin/products_v3/update_spec.rb +++ b/spec/system/admin/products_v3/update_spec.rb @@ -527,8 +527,10 @@ # Client side validation click_button "Save changes" within new_variant_row do + # In CI we get "Please fill out this field." and locally we get + # "Please fill in this field." expect_browser_validation('input[aria-label="Unit value"]', - "Please fill in this field.") + /Please fill (in|out) this field./) end # Fix error @@ -749,6 +751,6 @@ def expect_browser_validation(selector, message) browser_message = page.find(selector)["validationMessage"] - expect(browser_message).to eq message + expect(browser_message).to match message end end From fa986f3fc227239eb8c9a70df4b896f0c65854c5 Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou Date: Mon, 12 Aug 2024 15:34:37 +1000 Subject: [PATCH 47/68] Fix orders and fulfillment report --- .../reports/orders_and_fulfillment/base.rb | 7 +++---- .../orders_cycle_supplier_totals_report_spec.rb | 17 +++++++---------- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/lib/reporting/reports/orders_and_fulfillment/base.rb b/lib/reporting/reports/orders_and_fulfillment/base.rb index 36b6ecf325c..0aa6ff3128b 100644 --- a/lib/reporting/reports/orders_and_fulfillment/base.rb +++ b/lib/reporting/reports/orders_and_fulfillment/base.rb @@ -72,8 +72,7 @@ def total_units(line_items) return " " if not_all_have_unit?(line_items) total_units = line_items.sum do |li| - product = li.variant.product - li.quantity * li.unit_value / scale_factor(product) + li.quantity * li.unit_value / scale_factor(li.variant) end total_units.round(3) @@ -92,8 +91,8 @@ def not_all_have_unit?(line_items) line_items.map { |li| li.unit_value.nil? }.any? end - def scale_factor(product) - product.variant_unit == 'weight' ? 1000 : 1 + def scale_factor(variant) + variant.variant_unit == 'weight' ? 1000 : 1 end def report_variant_overrides diff --git a/spec/lib/reports/orders_and_fulfillment/orders_cycle_supplier_totals_report_spec.rb b/spec/lib/reports/orders_and_fulfillment/orders_cycle_supplier_totals_report_spec.rb index e81596cbbd4..38cedaa0153 100644 --- a/spec/lib/reports/orders_and_fulfillment/orders_cycle_supplier_totals_report_spec.rb +++ b/spec/lib/reports/orders_and_fulfillment/orders_cycle_supplier_totals_report_spec.rb @@ -34,8 +34,7 @@ let(:variant) { item.variant } it "contains a sum of total items" do - variant.product.update!(variant_unit: "items", variant_unit_name: "bottle") - variant.update!(unit_value: 6) # six-pack + variant.update!(variant_unit: "items", variant_unit_name: "bottle", unit_value: 6) # six-pack item.update!(final_weight_volume: nil) # reset unit information item.update!(quantity: 3) @@ -44,8 +43,7 @@ end it "contains a sum of total weight" do - variant.product.update!(variant_unit: "weight") - variant.update!(unit_value: 200) # grams + variant.update!(variant_unit: "weight", unit_value: 200) # grams item.update!(final_weight_volume: nil) # reset unit information item.update!(quantity: 3) @@ -57,8 +55,8 @@ # This is not possible with the current code but was possible years ago. # So I'm using `update_columns` to save invalid data. # We still have lots of that data in our databases though. - variant.product.update(variant_unit: "items", variant_unit_name: "container") - variant.update_columns(unit_value: nil, unit_description: "vacuum") + variant.update_columns(variant_unit: "items", variant_unit_name: "container", + unit_value: nil, unit_description: "vacuum") item.update!(final_weight_volume: nil) # reset unit information expect(table_headers[4]).to eq "Total Units" @@ -69,8 +67,7 @@ expect(report).to receive(:display_summary_row?).and_return(true) # assures product appears first on report table variant.product.update!(name: "Alpha-Product #000") - variant.product.update!(variant_unit: "weight") - variant.update!(unit_value: 200) # grams + variant.update!(variant_unit: "weight", unit_value: 200) # grams item.update!(final_weight_volume: nil) # reset unit information item.update!(quantity: 3) @@ -89,8 +86,8 @@ # This is not possible with the current code but was possible years ago. # So I'm using `update_columns` to save invalid data. # We still have lots of that data in our databases though. - variant.product.update(variant_unit: "items", variant_unit_name: "container") - variant.update_columns(unit_value: nil, unit_description: "vacuum") + variant.update_columns(variant_unit: "items", variant_unit_name: "container", + unit_value: nil, unit_description: "vacuum") item.update!(final_weight_volume: nil) # reset unit information # This second line item will have a default a bigint value. From 83a619b0979cae5ca29f91b9e18daa42b4f23715 Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou Date: Tue, 13 Aug 2024 11:20:01 +1000 Subject: [PATCH 48/68] Fix bulk order management page --- .../line_items_controller.js.coffee | 42 +++--- .../api/admin/units_product_serializer.rb | 2 +- .../api/admin/units_variant_serializer.rb | 2 +- .../admin/orders/bulk_management.html.haml | 6 +- .../line_items_controller_spec.js.coffee | 130 +++++++++++------- 5 files changed, 108 insertions(+), 74 deletions(-) diff --git a/app/assets/javascripts/admin/line_items/controllers/line_items_controller.js.coffee b/app/assets/javascripts/admin/line_items/controllers/line_items_controller.js.coffee index 88479620faa..d10de21f262 100644 --- a/app/assets/javascripts/admin/line_items/controllers/line_items_controller.js.coffee +++ b/app/assets/javascripts/admin/line_items/controllers/line_items_controller.js.coffee @@ -199,14 +199,14 @@ angular.module("admin.lineItems").controller 'LineItemsCtrl', ($scope, $timeout, $scope.refreshData() $scope.getLineItemScale = (lineItem) -> - if lineItem.units_product && lineItem.units_variant && (lineItem.units_product.variant_unit == "weight" || lineItem.units_product.variant_unit == "volume") - lineItem.units_product.variant_unit_scale + if lineItem.units_variant && lineItem.units_variant.variant_unit_scale && (lineItem.units_variant.variant_unit == "weight" || lineItem.units_variant.variant_unit == "volume") + lineItem.units_variant.variant_unit_scale else 1 $scope.sumUnitValues = -> sum = $scope.filteredLineItems?.reduce (sum, lineItem) -> - if lineItem.units_product.variant_unit == "items" + if lineItem.units_variant.variant_unit == "items" sum + lineItem.quantity else sum + $scope.roundToThreeDecimals(lineItem.final_weight_volume / $scope.getLineItemScale(lineItem)) @@ -214,7 +214,7 @@ angular.module("admin.lineItems").controller 'LineItemsCtrl', ($scope, $timeout, $scope.sumMaxUnitValues = -> sum = $scope.filteredLineItems?.reduce (sum,lineItem) -> - if lineItem.units_product.variant_unit == "items" + if lineItem.units_variant.variant_unit == "items" sum + lineItem.max_quantity else sum + lineItem.max_quantity * $scope.roundToThreeDecimals(lineItem.units_variant.unit_value / $scope.getLineItemScale(lineItem)) @@ -228,39 +228,41 @@ angular.module("admin.lineItems").controller 'LineItemsCtrl', ($scope, $timeout, return false if !lineItem.hasOwnProperty('final_weight_volume') || !(lineItem.final_weight_volume > 0) true - $scope.getScale = (unitsProduct, unitsVariant) -> - if unitsProduct.hasOwnProperty("variant_unit") && (unitsProduct.variant_unit == "weight" || unitsProduct.variant_unit == "volume") - unitsProduct.variant_unit_scale - else if unitsProduct.hasOwnProperty("variant_unit") && unitsProduct.variant_unit == "items" + $scope.getScale = (unitsVariant) -> + if unitsVariant.hasOwnProperty("variant_unit") && (unitsVariant.variant_unit == "weight" || unitsVariant.variant_unit == "volume") + unitsVariant.variant_unit_scale + else if unitsVariant.hasOwnProperty("variant_unit") && unitsVariant.variant_unit == "items" 1 else null - $scope.getFormattedValueWithUnitName = (value, unitsProduct, unitsVariant, scale) -> - unit_name = VariantUnitManager.getUnitName(scale, unitsProduct.variant_unit) + $scope.getFormattedValueWithUnitName = (value, unitsVariant, scale) -> + unit_name = VariantUnitManager.getUnitName(scale, unitsVariant.variant_unit) $scope.roundToThreeDecimals(value) + " " + unit_name - $scope.getGroupBySizeFormattedValueWithUnitName = (value, unitsProduct, unitsVariant) -> - scale = $scope.getScale(unitsProduct, unitsVariant) + $scope.getGroupBySizeFormattedValueWithUnitName = (value, unitsVariant) -> + scale = $scope.getScale(unitsVariant) if scale && value value = value / scale if scale != 28.35 && scale != 1 && scale != 453.6 # divide by scale if not smallest unit - $scope.getFormattedValueWithUnitName(value, unitsProduct, unitsVariant, scale) + $scope.getFormattedValueWithUnitName(value, unitsVariant, scale) else '' - $scope.formattedValueWithUnitName = (value, unitsProduct, unitsVariant) -> - scale = $scope.getScale(unitsProduct, unitsVariant) + $scope.formattedValueWithUnitName = (value, unitsVariant) -> + scale = $scope.getScale(unitsVariant) if scale - $scope.getFormattedValueWithUnitName(value, unitsProduct, unitsVariant, scale) + $scope.getFormattedValueWithUnitName(value, unitsVariant, scale) else '' $scope.fulfilled = (sumOfUnitValues) -> # A Units Variant is an API object which holds unit properies of a variant - if $scope.selectedUnitsProduct.hasOwnProperty("group_buy_unit_size")&& $scope.selectedUnitsProduct.group_buy_unit_size > 0 && - $scope.selectedUnitsProduct.hasOwnProperty("variant_unit") - if $scope.selectedUnitsProduct.variant_unit == "weight" || $scope.selectedUnitsProduct.variant_unit == "volume" - scale = $scope.selectedUnitsProduct.variant_unit_scale + if $scope.selectedUnitsProduct.hasOwnProperty("group_buy_unit_size") && $scope.selectedUnitsProduct.group_buy_unit_size > 0 && + $scope.selectedUnitsVariant.hasOwnProperty("variant_unit") + + if $scope.selectedUnitsVariant.variant_unit == "weight" || $scope.selectedUnitsVariant.variant_unit == "volume" + + scale = $scope.selectedUnitsVariant.variant_unit_scale sumOfUnitValues = sumOfUnitValues * scale unless scale == 28.35 || scale == 453.6 $scope.roundToThreeDecimals(sumOfUnitValues / $scope.selectedUnitsProduct.group_buy_unit_size) else diff --git a/app/serializers/api/admin/units_product_serializer.rb b/app/serializers/api/admin/units_product_serializer.rb index 60c5f70b970..4d6e83a8fd5 100644 --- a/app/serializers/api/admin/units_product_serializer.rb +++ b/app/serializers/api/admin/units_product_serializer.rb @@ -3,7 +3,7 @@ module Api module Admin class UnitsProductSerializer < ActiveModel::Serializer - attributes :id, :name, :group_buy_unit_size, :variant_unit, :variant_unit_scale + attributes :id, :name, :group_buy_unit_size end end end diff --git a/app/serializers/api/admin/units_variant_serializer.rb b/app/serializers/api/admin/units_variant_serializer.rb index 99423957ffe..6557767afff 100644 --- a/app/serializers/api/admin/units_variant_serializer.rb +++ b/app/serializers/api/admin/units_variant_serializer.rb @@ -3,7 +3,7 @@ module Api module Admin class UnitsVariantSerializer < ActiveModel::Serializer - attributes :id, :full_name, :unit_value + attributes :id, :full_name, :unit_value, :variant_unit, :variant_unit_scale def full_name full_name = object.full_name diff --git a/app/views/spree/admin/orders/bulk_management.html.haml b/app/views/spree/admin/orders/bulk_management.html.haml index 6f27875d8df..f4b57f832fa 100644 --- a/app/views/spree/admin/orders/bulk_management.html.haml +++ b/app/views/spree/admin/orders/bulk_management.html.haml @@ -80,15 +80,15 @@ .three.columns .text-center = t("admin.orders.bulk_management.group_buy_unit_size") - .text-center {{ getGroupBySizeFormattedValueWithUnitName(selectedUnitsProduct.group_buy_unit_size , selectedUnitsProduct, selectedUnitsVariant ) }} + .text-center {{ getGroupBySizeFormattedValueWithUnitName(selectedUnitsProduct.group_buy_unit_size , selectedUnitsVariant ) }} .three.columns .text-center = t("admin.orders.bulk_management.total_qtt_ordered") - .text-center {{ formattedValueWithUnitName( sumUnitValues(), selectedUnitsProduct, selectedUnitsVariant ) }} + .text-center {{ formattedValueWithUnitName( sumUnitValues(), selectedUnitsVariant ) }} .three.columns .text-center = t("admin.orders.bulk_management.max_qtt_ordered") - .text-center {{ formattedValueWithUnitName( sumMaxUnitValues(), selectedUnitsProduct, selectedUnitsVariant ) }} + .text-center {{ formattedValueWithUnitName( sumMaxUnitValues(), selectedUnitsVariant ) }} .three.columns .text-center = t("admin.orders.bulk_management.current_fulfilled_units") diff --git a/spec/javascripts/unit/admin/line_items/controllers/line_items_controller_spec.js.coffee b/spec/javascripts/unit/admin/line_items/controllers/line_items_controller_spec.js.coffee index e77ee3780ea..39797ccc6b6 100644 --- a/spec/javascripts/unit/admin/line_items/controllers/line_items_controller_spec.js.coffee +++ b/spec/javascripts/unit/admin/line_items/controllers/line_items_controller_spec.js.coffee @@ -213,35 +213,67 @@ describe "LineItemsCtrl", -> expect(scope.fulfilled()).toEqual '' it "returns '' if selectedUnitsVariant has no property 'group_buy_unit_size' or group_buy_unit_size is 0", -> - scope.selectedUnitsProduct = { variant_unit: "weight", group_buy_unit_size: 0 } + scope.selectedUnitsProduct = { group_buy_unit_size: 0 } + scope.selectedUnitsVariant = { variant_unit: "weight" } expect(scope.fulfilled()).toEqual '' - scope.selectedUnitsProduct = { variant_unit: "weight" } + scope.selectedUnitsProduct = { } + scope.selectedUnitsVariant = { variant_unit: "weight" } expect(scope.fulfilled()).toEqual '' it "calls Math.round() if variant_unit is 'weight', 'volume', or items", -> spyOn(Math,"round") - scope.selectedUnitsProduct = { variant_unit: "weight", group_buy_unit_size: 10 } + scope.selectedUnitsProduct = { group_buy_unit_size: 10 } + scope.selectedUnitsVariant = { variant_unit: "weight" } + scope.fulfilled() expect(Math.round).toHaveBeenCalled() - scope.selectedUnitsProduct = { variant_unit: "volume", group_buy_unit_size: 10 } + + scope.selectedUnitsProduct = { group_buy_unit_size: 10 } + scope.selectedUnitsVariant = { variant_unit: "volume" } + scope.fulfilled() expect(Math.round).toHaveBeenCalled() - scope.selectedUnitsProduct = { variant_unit: "items", group_buy_unit_size: 10 } + + scope.selectedUnitsProduct = { group_buy_unit_size: 10 } + scope.selectedUnitsVariant = { variant_unit: "items" } + scope.fulfilled() expect(Math.round).toHaveBeenCalled() - describe "returns the quantity of fulfilled group buy units", -> + describe "returns the quantity of fulfilled group buy units", -> runs = [ - { selectedUnitsProduct: { variant_unit: "weight", group_buy_unit_size: 1000, variant_unit_scale: 1 }, arg: 1500, expected: 1.5 }, - { selectedUnitsProduct: { variant_unit: "weight", group_buy_unit_size: 60000, variant_unit_scale: 1000 }, arg: 9, expected: 0.15 }, - { selectedUnitsProduct: { variant_unit: "weight", group_buy_unit_size: 60000, variant_unit_scale: 1 }, arg: 9000, expected: 0.15 } - { selectedUnitsProduct: { variant_unit: "weight", group_buy_unit_size: 5, variant_unit_scale: 28.35 }, arg: 12, expected: 2.4}, - { selectedUnitsProduct: { variant_unit: "volume", group_buy_unit_size: 5000, variant_unit_scale: 1 }, arg: 5, expected: 0.001} - ]; - runs.forEach ({selectedUnitsProduct, arg, expected}) -> - it "returns the quantity of fulfilled group buy units, group_buy_unit_size: " + selectedUnitsProduct.group_buy_unit_size + ", arg: " + arg + ", scale: " + selectedUnitsProduct.variant_unit_scale , -> + { + selectedUnitsProduct: { group_buy_unit_size: 1000 }, + selectedUnitsVariant: { variant_unit: "weight", variant_unit_scale: 1 }, + arg: 1500, + expected: 1.5 + }, { + selectedUnitsProduct: { group_buy_unit_size: 60000 } , + selectedUnitsVariant: { variant_unit: "weight", variant_unit_scale: 1000 } , + arg: 9, + expected: 0.15 + }, { + selectedUnitsProduct: { group_buy_unit_size: 60000 }, + selectedUnitsVariant: { variant_unit: "weight", variant_unit_scale: 1 }, + arg: 9000, + expected: 0.15 + }, { + selectedUnitsProduct: { group_buy_unit_size: 5 }, + selectedUnitsVariant: { variant_unit: "weight", variant_unit_scale: 28.35 }, + arg: 12, + expected: 2.4 + }, { + selectedUnitsProduct: { group_buy_unit_size: 5000 }, + selectedUnitsVariant: { variant_unit: "volume", variant_unit_scale: 1 }, + arg: 5, + expected: 0.001 + } + ] + runs.forEach ({selectedUnitsProduct, selectedUnitsVariant, arg, expected}) -> + it "returns the quantity of fulfilled group buy units, group_buy_unit_size: " + selectedUnitsProduct.group_buy_unit_size + ", arg: " + arg + ", scale: " + selectedUnitsVariant.variant_unit_scale , -> scope.selectedUnitsProduct = selectedUnitsProduct + scope.selectedUnitsVariant = selectedUnitsVariant expect(scope.fulfilled(arg)).toEqual expected describe "allFinalWeightVolumesPresent()", -> @@ -278,44 +310,44 @@ describe "LineItemsCtrl", -> describe "sumUnitValues()", -> it "returns the sum of the final_weight_volumes line_items if volume", -> scope.filteredLineItems = [ - { final_weight_volume: 2, units_product: { variant_unit: "volume" } } - { final_weight_volume: 7, units_product: { variant_unit: "volume" } } - { final_weight_volume: 21, units_product: { variant_unit: "volume" } } + { final_weight_volume: 2, units_variant: { variant_unit: "volume" } } + { final_weight_volume: 7, units_variant: { variant_unit: "volume" } } + { final_weight_volume: 21, units_variant: { variant_unit: "volume" } } ] expect(scope.sumUnitValues()).toEqual 30 it "returns the sum of the quantity line_items if items", -> scope.filteredLineItems = [ - { quantity: 2, units_product: { variant_unit: "items" } } - { quantity: 7, units_product: { variant_unit: "items" } } - { quantity: 21, units_product: { variant_unit: "items" } } + { quantity: 2, units_variant: { variant_unit: "items" } } + { quantity: 7, units_variant: { variant_unit: "items" } } + { quantity: 21, units_variant: { variant_unit: "items" } } ] expect(scope.sumUnitValues()).toEqual 30 it "returns the sum of the final_weight_volumes for line_items with both metric and imperial units", -> scope.filteredLineItems = [ - { final_weight_volume: 907.2, units_product: { variant_unit: "weight", variant_unit_scale: 453.6 }, units_variant: { unit_value: 453.6 } } - { final_weight_volume: 2000, units_product: { variant_unit: "weight", variant_unit_scale: 1000 }, units_variant: { unit_value: 1000 } } - { final_weight_volume: 56.7, units_product: { variant_unit: "weight", variant_unit_scale: 28.35 }, units_variant: { unit_value: 28.35 } } - { final_weight_volume: 2, units_product: { variant_unit: "volume", variant_unit_scale: 1.0 }, units_variant: { unit_value: 1.0 } } + { final_weight_volume: 907.2, units_variant: { variant_unit: "weight", variant_unit_scale: 453.6, unit_value: 453.6 } } + { final_weight_volume: 2000, units_variant: { variant_unit: "weight", variant_unit_scale: 1000, unit_value: 1000 } } + { final_weight_volume: 56.7, units_variant: { variant_unit: "weight", variant_unit_scale: 28.35, unit_value: 28.35 } } + { final_weight_volume: 2, units_variant: { variant_unit: "volume", variant_unit_scale: 1.0, unit_value: 1.0 } } ] expect(scope.sumUnitValues()).toEqual 8 describe "sumMaxUnitValues()", -> it "returns the sum of the product of unit_value and maxOf(max_quantity, pristine quantity) for specified line_items", -> scope.filteredLineItems = [ - { id: 1, units_variant: { unit_value: 1 }, max_quantity: 5, units_product: { variant_unit: "volume", variant_unit_scale: 1 } } - { id: 2, units_variant: { unit_value: 2 }, max_quantity: 1, units_product: { variant_unit: "volume", variant_unit_scale: 1 } } - { id: 3, units_variant: { unit_value: 3 }, max_quantity: 10, units_product: { variant_unit: "volume", variant_unit_scale: 1 } } + { id: 1, units_variant: { variant_unit: "volume", variant_unit_scale: 1, unit_value: 1 }, max_quantity: 5 } + { id: 2, units_variant: { variant_unit: "volume", variant_unit_scale: 1, unit_value: 2 }, max_quantity: 1 } + { id: 3, units_variant: { variant_unit: "volume", variant_unit_scale: 1, unit_value: 3 }, max_quantity: 10 } ] expect(scope.sumMaxUnitValues()).toEqual 37 it "returns the sum of the product of max_quantity for specified line_items if variant_unit is `items`", -> scope.filteredLineItems = [ - { id: 1, units_variant: { unit_value: 1 }, max_quantity: 5, units_product: { variant_unit: "items" } } - { id: 2, units_variant: { unit_value: 2 }, max_quantity: 1, units_product: { variant_unit: "items" } } - { id: 3, units_variant: { unit_value: 3 }, max_quantity: 10, units_product: { variant_unit: "items" } } + { id: 1, units_variant: { variant_unit: "items", unit_value: 1 }, max_quantity: 5 } + { id: 2, units_variant: { variant_unit: "items", unit_value: 2 }, max_quantity: 1 } + { id: 3, units_variant: { variant_unit: "items", unit_value: 3 }, max_quantity: 10 } ] expect(scope.sumMaxUnitValues()).toEqual 16 @@ -331,45 +363,45 @@ describe "LineItemsCtrl", -> expect(scope.formattedValueWithUnitName(1,{})).toEqual '' it "returns the value, and does not call Math.round if variant_unit is 'items'", -> - unitsProduct = { variant_unit: "items" } - expect(scope.formattedValueWithUnitName(1, unitsProduct, unitsVariant)).toEqual "1 items" + unitsVariant.variant_unit= "items" + expect(scope.formattedValueWithUnitName(1, unitsVariant)).toEqual "1 items" it "calls Math.round() if variant_unit is 'weight' or 'volume'", -> - unitsProduct = { variant_unit: "weight", variant_unit_scale: 1 } - scope.formattedValueWithUnitName(1,unitsProduct,unitsVariant) + unitsVariant = { unit_value: "1", variant_unit: "weight", variant_unit_scale: 1 } + scope.formattedValueWithUnitName(1, unitsVariant) expect(Math.round).toHaveBeenCalled() - scope.selectedUnitsVariant = { variant_unit: "volume" } - scope.formattedValueWithUnitName(1,unitsProduct,unitsVariant) + + scope.selectedUnitsVariant = {unit_value: "1", variant_unit: "volume" } + scope.formattedValueWithUnitName(1, unitsVariant) expect(Math.round).toHaveBeenCalled() it "calls Math.round with the value multiplied by 1000", -> - unitsProduct = { variant_unit: "weight", variant_unit_scale: 5 } - scope.formattedValueWithUnitName(10, unitsProduct,unitsVariant) + unitsVariant = { unit_value: 1, variant_unit: "weight", variant_unit_scale: 5 } + scope.formattedValueWithUnitName(10, unitsVariant) expect(Math.round).toHaveBeenCalledWith 10 * 1000 it "returns the result of Math.round divided by 1000, followed by the result of getUnitName", -> - unitsProduct = { variant_unit: "weight", variant_unit_scale: 1000 } + unitsVariant = { unit_value: 1, variant_unit: "weight", variant_unit_scale: 1000 } spyOn(VariantUnitManager, "getUnitName").and.returnValue "kg" - expect(scope.formattedValueWithUnitName(2,unitsProduct,unitsVariant)).toEqual "2 kg" + expect(scope.formattedValueWithUnitName(2, unitsVariant)).toEqual "2 kg" it "handle correclty the imperial units", -> - unitsProduct = { variant_unit: "weight", variant_unit_scale: 1000 } - unitsVariant = { unit_value: "453.6" } + unitsVariant = { variant_unit: "weight", variant_unit_scale: 1000, unit_value: "453.6" } spyOn(VariantUnitManager, "getUnitName").and.returnValue "lb" - expect(scope.formattedValueWithUnitName(2, unitsProduct, unitsVariant)).toEqual "2 lb" + expect(scope.formattedValueWithUnitName(2, unitsVariant)).toEqual "2 lb" describe "get group by size formatted value with unit name", -> beforeEach -> spyOn(VariantUnitManager, "getUnitName").and.returnValue "kg" - - unitsProduct = { variant_unit: "weight", variant_unit_scale: 1000 } - + + unitsVariant = { variant_unit: "weight", variant_unit_scale: 1000 } + it "returns the formatted value with unit name", -> - expect(scope.getGroupBySizeFormattedValueWithUnitName(1000, unitsProduct)).toEqual "1 kg" + expect(scope.getGroupBySizeFormattedValueWithUnitName(1000, unitsVariant)).toEqual "1 kg" it "handle the case when the value is actually null or empty", -> - expect(scope.getGroupBySizeFormattedValueWithUnitName(null, unitsProduct)).toEqual "" - expect(scope.getGroupBySizeFormattedValueWithUnitName("", unitsProduct)).toEqual "" + expect(scope.getGroupBySizeFormattedValueWithUnitName(null, unitsVariant)).toEqual "" + expect(scope.getGroupBySizeFormattedValueWithUnitName("", unitsVariant)).toEqual "" describe "updating the price upon updating the weight of a line item", -> From 218d07c90d4a3fb412c0cd7552432b8dc301fd6b Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou Date: Tue, 13 Aug 2024 12:11:48 +1000 Subject: [PATCH 49/68] Fix product import controller --- app/controllers/admin/product_import_controller.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/controllers/admin/product_import_controller.rb b/app/controllers/admin/product_import_controller.rb index a2dc50ce433..54a9ac84d54 100644 --- a/app/controllers/admin/product_import_controller.rb +++ b/app/controllers/admin/product_import_controller.rb @@ -21,8 +21,9 @@ def import @importer = ProductImport::ProductImporter.new(File.new(@filepath), spree_current_user, params[:settings]) @original_filename = params[:file].try(:original_filename) - @non_updatable_fields = ProductImport::EntryValidator.non_updatable_fields - + @non_updatable_fields = ProductImport::EntryValidator.non_updatable_product_fields.merge( + ProductImport::EntryValidator.non_updatable_variant_fields + ) return if contains_errors? @importer @ams_data = ams_data From 64f60d1c8c8cd16c9ba6d994e1f7aeb126c7450b Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou Date: Tue, 13 Aug 2024 15:44:19 +1000 Subject: [PATCH 50/68] Fix small bug on edit variant page - make sure the weight is only cleared when needed - make sure the displayed unit is up to date --- .../controllers/edit_variant_controller.js | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/app/webpacker/controllers/edit_variant_controller.js b/app/webpacker/controllers/edit_variant_controller.js index a01fda48e25..10e5b742f34 100644 --- a/app/webpacker/controllers/edit_variant_controller.js +++ b/app/webpacker/controllers/edit_variant_controller.js @@ -60,13 +60,22 @@ export default class EditVariantController extends Controller { // on variantUnit change we need to check if weight needs to be toggled this.variantUnit.addEventListener("change", this.#toggleWeight.bind(this), { passive: true }); + // make sure the unit is correct when page is reload after an error + this.#updateUnitDisplay(); // update unit price on page load this.#processUnitPrice(); - this.#toggleWeight(); + + if (this.variantUnit.value === "weight") { + return this.#hideWeight(); + } } disconnect() { // Make sure to clean up anything that happened outside + // TODO remove all added event + this.variantUnit.removeEventListener("change", this.#toggleWeight.bind(this), { + passive: true, + }); } toggleOnHand(event) { @@ -160,13 +169,21 @@ export default class EditVariantController extends Controller { this.element.querySelector('[id="variant_unit_price"]').value = unit_price; } + #hideWeight() { + this.weight = this.element.querySelector('[id="variant_weight"]'); + this.weight.parentElement.style.display = "none"; + } + #toggleWeight() { - let display = "block"; if (this.variantUnit.value === "weight") { - display = "none"; + return this.#hideWeight(); } + // Show weight this.weight = this.element.querySelector('[id="variant_weight"]'); - this.weight.parentElement.style.display = display; + this.weight.parentElement.style.display = "block"; + // Clearing weight value to remove calculated weight for a variant with unit set to "weight" + // See Spree::Variant hook update_weight_from_unit_value + this.weight.value = ""; } } From 630c398b129fbe9cdeac5262a4b176c8d942d1b0 Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou Date: Wed, 14 Aug 2024 11:06:30 +1000 Subject: [PATCH 51/68] Move unit popout css to a partial --- app/webpacker/css/admin/products_v3.scss | 86 +----------------- .../css/admin_v3/pages/_unit_popout.scss | 87 +++++++++++++++++++ 2 files changed, 90 insertions(+), 83 deletions(-) create mode 100644 app/webpacker/css/admin_v3/pages/_unit_popout.scss diff --git a/app/webpacker/css/admin/products_v3.scss b/app/webpacker/css/admin/products_v3.scss index 912e9406086..bb144cd6e87 100644 --- a/app/webpacker/css/admin/products_v3.scss +++ b/app/webpacker/css/admin/products_v3.scss @@ -1,5 +1,7 @@ // Customisations for the new Bulk Edit Products page only // Scoped to containing div, because Turbo messes with body classes +@import "../admin_v3/pages/unit_popout"; + #products_v3_page { #content > .row:first-child > .container:first-child { // Allow table to extend to full width of available screen space @@ -311,89 +313,7 @@ // Popout widget (todo: move to separate fiel) .popout { - position: relative; - - &__button { - // override button styles - &.popout__button { - background: $color-tbl-cell-bg; - color: $color-txt-text; - white-space: nowrap; - border-color: transparent; - font-weight: normal; - padding-left: $border-radius; // Super compact - padding-right: 1rem; // Retain space for arrow - height: auto; - min-width: 2em; - min-height: 1lh; // Line height of parent - - &:hover, - &:active, - &:focus { - background: $color-tbl-cell-bg; - color: $color-txt-text; - position: relative; - } - - &.changed { - border-color: $color-txt-changed-brd; - } - } - - &:hover:not(:active):not(:focus):not(.changed) { - border-color: transparent; - } - - &:hover, - &:active, - &:focus { - // for some reason, sass ignores &:active, &:focus here. we could make this a mixin and include it in multiple rules instead - &:before { - // for some reason, sass seems to extends the selector to include every other :before selector in the app! probably causing the above, and potentially breaking other styles. - // extending .icon-chevron-down causes infinite loop in compilation. does @include work for classes? - font-family: FontAwesome; - text-decoration: inherit; - display: inline-block; - speak: none; - content: "\f078"; - - position: absolute; - top: 0; // Required for empty buttons - right: $border-radius; - font-size: 0.67em; - } - } - } - - &__container { - position: absolute; - top: -0.6em; - left: -0.2em; - z-index: 1; // Cover below row when hover - width: 9em; - - padding: $padding-tbl-cell; - - background: $color-tbl-cell-bg; - border-radius: $border-radius; - box-shadow: 0px 0px 8px 0px rgba($near-black, 0.25); - - .field { - margin-bottom: 0.75em; - - &:last-child { - margin-bottom: 0; - } - } - - input { - height: auto; - - &[disabled] { - color: transparent; // hide value completely - } - } - } + @include unit_popout; } a.image-field { diff --git a/app/webpacker/css/admin_v3/pages/_unit_popout.scss b/app/webpacker/css/admin_v3/pages/_unit_popout.scss new file mode 100644 index 00000000000..475bc9bcd25 --- /dev/null +++ b/app/webpacker/css/admin_v3/pages/_unit_popout.scss @@ -0,0 +1,87 @@ +// Popout widget +@mixin unit_popout { + position: relative; + + &__button { + // override button styles + &.popout__button { + background: $color-tbl-cell-bg; + color: $color-txt-text; + white-space: nowrap; + border-color: transparent; + font-weight: normal; + padding-left: $border-radius; // Super compact + padding-right: 1rem; // Retain space for arrow + height: auto; + min-width: 2em; + min-height: 1lh; // Line height of parent + + &:hover, + &:active, + &:focus { + background: $color-tbl-cell-bg; + color: $color-txt-text; + position: relative; + } + + &.changed { + border-color: $color-txt-changed-brd; + } + } + + &:hover:not(:active):not(:focus):not(.changed) { + border-color: transparent; + } + + &:hover, + &:active, + &:focus { + // for some reason, sass ignores &:active, &:focus here. we could make this a mixin and include it in multiple rules instead + &:before { + // for some reason, sass seems to extends the selector to include every other :before selector in the app! probably causing the above, and potentially breaking other styles. + // extending .icon-chevron-down causes infinite loop in compilation. does @include work for classes? + font-family: FontAwesome; + text-decoration: inherit; + display: inline-block; + speak: none; + content: "\f078"; + + position: absolute; + top: 0; // Required for empty buttons + right: $border-radius; + font-size: 0.67em; + } + } + } + + &__container { + position: absolute; + top: -0.6em; + left: -0.2em; + z-index: 1; // Cover below row when hover + width: 9em; + + padding: $padding-tbl-cell; + + background: $color-tbl-cell-bg; + border-radius: $border-radius; + box-shadow: 0px 0px 8px 0px rgba($near-black, 0.25); + + .field { + margin-bottom: 0.75em; + + &:last-child { + margin-bottom: 0; + } + } + + input { + height: auto; + + &[disabled] { + color: transparent; // hide value completely + } + } + } +} + From a500c75ee9230eca4af9a365511556a101dc8dc9 Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou Date: Wed, 14 Aug 2024 11:07:49 +1000 Subject: [PATCH 52/68] Add stying for the unit pop out --- .../spree/admin/variants/_form.html.haml | 2 +- app/webpacker/css/admin_v3/all.scss | 1 + .../css/admin_v3/pages/edit_variant.scss | 55 +++++++++++++++++++ 3 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 app/webpacker/css/admin_v3/pages/edit_variant.scss diff --git a/app/views/spree/admin/variants/_form.html.haml b/app/views/spree/admin/variants/_form.html.haml index 7f73095cd97..5cd9cf130d9 100644 --- a/app/views/spree/admin/variants/_form.html.haml +++ b/app/views/spree/admin/variants/_form.html.haml @@ -1,4 +1,4 @@ -%div{'data-controller': "edit-variant"} +%div{'data-controller': "edit-variant", id: "edit_variant"} .label-block.left.six.columns.alpha %script= render partial: "admin/shared/global_var_ofn", formats: :js, locals: { name: :available_units_sorted, value: WeightsAndMeasures.available_units_sorted } diff --git a/app/webpacker/css/admin_v3/all.scss b/app/webpacker/css/admin_v3/all.scss index a5c673b9096..32eba7ddde6 100644 --- a/app/webpacker/css/admin_v3/all.scss +++ b/app/webpacker/css/admin_v3/all.scss @@ -91,6 +91,7 @@ @import "../admin/dialog"; @import "../admin/disabled"; @import "components/dropdown"; // admin_v3 +@import "pages/edit_variant"; // admin_v3 @import "pages/enterprise_index_panels"; // admin_v3 @import "../admin/enterprises"; @import "../admin/filters_and_controls"; diff --git a/app/webpacker/css/admin_v3/pages/edit_variant.scss b/app/webpacker/css/admin_v3/pages/edit_variant.scss new file mode 100644 index 00000000000..694c72bd932 --- /dev/null +++ b/app/webpacker/css/admin_v3/pages/edit_variant.scss @@ -0,0 +1,55 @@ +@import "unit_popout"; + +#edit_variant { + + .popout { + @include unit_popout; + + &__button { + // override popout button styles + &.popout__button { + // Reapplying button style from buttons.css + background-color: $color-btn-bg; + border: 1px solid $color-btn-bg; + color: $color-btn-text; + font-weight: bold; + + &:before { + font-family: FontAwesome; + text-decoration: inherit; + display: inline-block; + speak: none; + content: "\f078"; + + position: absolute; + top: 0; // Required for empty buttons + right: $border-radius; + font-size: 0.67em; + } + + // Reapplying button style from buttons.css + &:active, + &:focus { + outline: none; + border: 1px solid $color-btn-hover-border; + } + + &:active:focus { + box-shadow: none; + } + + &:hover { + background-color: $color-btn-hover-bg; + border: 1px solid $color-btn-hover-bg; + color: $color-btn-hover-text; + } + } + } + + &__container { + width: max-content; + top: auto; + left: auto; + } + } +} From 9db417319dd6e710b1877a63bfa4b965aba59f10 Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou Date: Mon, 19 Aug 2024 10:19:22 +1000 Subject: [PATCH 53/68] Improve variant related validation when creating product I disabled Metrics/AbcSize for ensure_standard_variant as I don't think that's hard to understand the code. And utimately it will be removed once product actually becomes optional. --- app/controllers/api/v0/products_controller.rb | 2 +- .../spree/admin/products_controller.rb | 2 +- app/models/spree/product.rb | 37 ++++--- .../api/v0/products_controller_spec.rb | 4 +- spec/models/spree/product_spec.rb | 101 +++++++++++++++++- spec/system/admin/products_spec.rb | 4 +- 6 files changed, 127 insertions(+), 23 deletions(-) diff --git a/app/controllers/api/v0/products_controller.rb b/app/controllers/api/v0/products_controller.rb index 6df0356cf53..801b6a343a1 100644 --- a/app/controllers/api/v0/products_controller.rb +++ b/app/controllers/api/v0/products_controller.rb @@ -21,7 +21,7 @@ def create authorize! :create, Spree::Product @product = Spree::Product.new(product_params) - if @product.save + if @product.save(context: :create_and_create_standard_variant) render json: @product, serializer: Api::Admin::ProductSerializer, status: :created else invalid_resource!(@product) diff --git a/app/controllers/spree/admin/products_controller.rb b/app/controllers/spree/admin/products_controller.rb index 4580a751f01..d383bee4e8e 100644 --- a/app/controllers/spree/admin/products_controller.rb +++ b/app/controllers/spree/admin/products_controller.rb @@ -39,7 +39,7 @@ def edit def create delete_stock_params_and_set_after do @object.attributes = permitted_resource_params - if @object.save + if @object.save(context: :create_and_create_standard_variant) flash[:success] = flash_message_for(@object, :successfully_created) redirect_after_save else diff --git a/app/models/spree/product.rb b/app/models/spree/product.rb index 9232b781202..e1228f56a4e 100755 --- a/app/models/spree/product.rb +++ b/app/models/spree/product.rb @@ -48,7 +48,27 @@ class Product < ApplicationRecord validate :validate_image validates :price, numericality: { greater_than_or_equal_to: 0, if: ->{ new_record? } } - accepts_nested_attributes_for :variants, allow_destroy: true + # These validators are used to make sure the standard variant created via + # `ensure_standard_variant` will be valid. The are only used when creating a new product + with_options on: :create_and_create_standard_variant do + validates :supplier_id, presence: true + validates :primary_taxon_id, presence: true + validates :variant_unit, presence: true + validates :unit_value, presence: true, if: ->(product) { + %w(weight volume).include?(product.variant_unit) + } + validates :unit_value, numericality: { greater_than: 0 }, allow_blank: true + validates :unit_description, presence: true, if: ->(product) { + product.variant_unit.present? && product.unit_value.nil? + } + validates :variant_unit_scale, presence: true, if: ->(product) { + %w(weight volume).include?(product.variant_unit) + } + validates :variant_unit_name, presence: true, if: ->(product) { + product.variant_unit == 'items' + } + end + accepts_nested_attributes_for :image accepts_nested_attributes_for :product_properties, allow_destroy: true, @@ -60,9 +80,8 @@ class Product < ApplicationRecord :variant_unit_name, :variant_unit_scale, :tax_category_id, :shipping_category_id, :primary_taxon_id, :supplier_id - after_validation :validate_variant_attrs, on: :create after_create :ensure_standard_variant - after_update :touch_supplier, if: :saved_change_to_primary_taxon_id? + # after_update :touch_supplier, if: :saved_change_to_primary_taxon_id? around_destroy :destruction after_touch :touch_supplier @@ -235,6 +254,7 @@ def destruction end end + # rubocop:disable Metrics/AbcSize def ensure_standard_variant return unless variants.empty? @@ -253,6 +273,7 @@ def ensure_standard_variant variant.supplier_id = supplier_id variants << variant end + # rubocop:enable Metrics/AbcSize # Remove any unsupported HTML. def description @@ -266,15 +287,6 @@ def description=(html) private - def validate_variant_attrs - # Avoid running validation when we can't set variant attrs - # eg clone product. Will raise error if clonning a product with no variant - return if variants.first&.valid? - - errors.add(:primary_taxon_id, :blank) unless Spree::Taxon.find_by(id: primary_taxon_id) - errors.add(:supplier_id, :blank) unless Enterprise.find_by(id: supplier_id) - end - def touch_supplier return if variants.empty? @@ -286,7 +298,6 @@ def touch_supplier # importing product. In this scenario the variant has not been updated with the supplier yet # hence the check. first_variant.supplier.touch if first_variant.supplier.present? - end def validate_image diff --git a/spec/controllers/api/v0/products_controller_spec.rb b/spec/controllers/api/v0/products_controller_spec.rb index 1a6030240cc..fd3a0ade81a 100644 --- a/spec/controllers/api/v0/products_controller_spec.rb +++ b/spec/controllers/api/v0/products_controller_spec.rb @@ -121,8 +121,8 @@ expect(json_response["error"]).to eq("Invalid resource. Please fix errors and try again.") errors = json_response["errors"] expect(errors.keys).to match_array([ - "name", "price", - "primary_taxon_id", "supplier_id" + "name", "price", "primary_taxon_id", + "supplier_id", "variant_unit" ]) end diff --git a/spec/models/spree/product_spec.rb b/spec/models/spree/product_spec.rb index 91edc40ba0d..e5919a4463a 100644 --- a/spec/models/spree/product_spec.rb +++ b/spec/models/spree/product_spec.rb @@ -133,6 +133,9 @@ module Spree before do create(:stock_location) + end + + it "copies properties to the first standard variant" do product.primary_taxon_id = taxon.id product.name = "Product1" product.variant_unit = "weight" @@ -142,10 +145,8 @@ module Spree product.price = 4.27 product.shipping_category_id = shipping_category.id product.supplier_id = supplier.id - product.save! - end + product.save(context: :create_and_create_standard_variant) - it "copies properties to the first standard variant" do expect(product.variants.reload.length).to eq 1 standard_variant = product.variants.reload.first @@ -159,6 +160,100 @@ module Spree expect(standard_variant.primary_taxon).to eq taxon expect(standard_variant.supplier).to eq supplier end + + context "with variant attributes" do + it { + is_expected.to validate_presence_of(:variant_unit) + .on(:create_and_create_standard_variant) + } + it { + is_expected.to validate_presence_of(:supplier_id) + .on(:create_and_create_standard_variant) + } + it { + is_expected.to validate_presence_of(:primary_taxon_id) + .on(:create_and_create_standard_variant) + } + + describe "unit_value" do + subject { build(:simple_product, variant_unit: "items") } + + it { + is_expected.to validate_numericality_of(:unit_value).is_greater_than(0) + .on(:create_and_create_standard_variant) + } + it { + is_expected.not_to validate_presence_of(:unit_value) + .on(:create_and_create_standard_variant) + } + + ["weight", "volume"].each do |variant_unit| + context "when variant_unit is #{variant_unit}" do + subject { build(:simple_product, variant_unit:) } + + it { + is_expected.to validate_presence_of(:unit_value) + .on(:create_and_create_standard_variant) + } + end + end + + describe "unit_description" do + it { + is_expected.not_to validate_presence_of(:unit_description) + .on(:create_and_create_standard_variant) + } + + context "when variant_unit is et and unit_value is nil" do + subject { + build(:simple_product, variant_unit: "items", unit_value: nil, + unit_description: "box") + } + + it { + is_expected.to validate_presence_of(:unit_description) + .on(:create_and_create_standard_variant) + } + end + end + + describe "variant_unit_scale" do + it { + is_expected.not_to validate_presence_of(:variant_unit_scale) + .on(:create_and_create_standard_variant) + } + + ["weight", "volume"].each do |variant_unit| + context "when variant_unit is #{variant_unit}" do + subject { build(:simple_product, variant_unit:) } + + it { + is_expected.to validate_presence_of(:variant_unit_scale) + .on(:create_and_create_standard_variant) + } + end + end + end + + describe "variant_unit_name" do + subject { build(:simple_product, variant_unit: "volume") } + + it { + is_expected.not_to validate_presence_of(:variant_unit_name) + .on(:create_and_create_standard_variant) + } + + context "when variant_unit is items" do + subject { build(:simple_product, variant_unit: "items") } + + it { + is_expected.to validate_presence_of(:variant_unit_name) + .on(:create_and_create_standard_variant) + } + end + end + end + end end end diff --git a/spec/system/admin/products_spec.rb b/spec/system/admin/products_spec.rb index 9545f05733b..f2a1c322f37 100644 --- a/spec/system/admin/products_spec.rb +++ b/spec/system/admin/products_spec.rb @@ -53,7 +53,6 @@ end it "display all attributes when submitting with error: Unit Value must be grater than 0" do - pending "rebase so we can add needed validation" select 'New supplier', from: 'product_supplier_id' fill_in 'product_name', with: "new product name" select "Weight (kg)", from: 'product_variant_unit_with_scale' @@ -154,7 +153,6 @@ end it "creating product with empty unit value" do - pending "rebase" fill_in 'product_name', with: 'Hot Cakes' select 'New supplier', from: 'product_supplier_id' select "Weight (kg)", from: 'product_variant_unit_with_scale' @@ -169,7 +167,7 @@ click_button 'Create' expect(current_path).to eq spree.admin_products_path - expect(page).to have_content "Unit value is not a number" + expect(page).to have_content "Unit value can't be blank" end it "creating product with empty product category fails" do From 0695b434a2e81a1b8a8b1cd5d701de0cb4b53e49 Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou Date: Mon, 19 Aug 2024 14:12:08 +1000 Subject: [PATCH 54/68] Fix rebase issue --- app/helpers/admin/products_helper.rb | 4 +-- .../admin/products_v3/_variant_row.html.haml | 2 +- spec/helpers/admin/products_helper_spec.rb | 7 ++-- spec/system/admin/products_v3/update_spec.rb | 33 +++++++++---------- 4 files changed, 22 insertions(+), 24 deletions(-) diff --git a/app/helpers/admin/products_helper.rb b/app/helpers/admin/products_helper.rb index b67aa57540c..ea18887e04d 100644 --- a/app/helpers/admin/products_helper.rb +++ b/app/helpers/admin/products_helper.rb @@ -18,9 +18,9 @@ def prepare_new_variant(product, producer_options) end def unit_value_with_description(variant) - return "" if variant.unit_value.nil? + return variant.unit_description.to_s if variant.unit_value.nil? - scaled_unit_value = variant.unit_value / (variant.product.variant_unit_scale || 1) + scaled_unit_value = variant.unit_value / (variant.variant_unit_scale || 1) precised_unit_value = number_with_precision( scaled_unit_value, precision: nil, diff --git a/app/views/admin/products_v3/_variant_row.html.haml b/app/views/admin/products_v3/_variant_row.html.haml index 2d3fd678967..fa423bbce75 100644 --- a/app/views/admin/products_v3/_variant_row.html.haml +++ b/app/views/admin/products_v3/_variant_row.html.haml @@ -27,7 +27,7 @@ = f.hidden_field :unit_value = f.hidden_field :unit_description = f.text_field :unit_value_with_description, - value: unit_value_with_description(variant), 'aria-label': t('admin.products_page.columns.unit_value') + value: unit_value_with_description(variant), 'aria-label': t('admin.products_page.columns.unit_value'), required: true .field = f.label :display_as, t('admin.products_page.columns.display_as') = f.text_field :display_as, placeholder: VariantUnits::OptionValueNamer.new(variant).name diff --git a/spec/helpers/admin/products_helper_spec.rb b/spec/helpers/admin/products_helper_spec.rb index cdd4df9e2a1..cfd04180d99 100644 --- a/spec/helpers/admin/products_helper_spec.rb +++ b/spec/helpers/admin/products_helper_spec.rb @@ -4,8 +4,9 @@ RSpec.describe Admin::ProductsHelper do describe '#unit_value_with_description' do - let(:product) { create(:product, variant_unit_scale: 1000.0) } - let(:variant) { create(:variant, product:, unit_value: 2000.0, unit_description: 'kg') } + let(:variant) { + create(:variant, variant_unit_scale: 1000.0, unit_value: 2000.0, unit_description: 'kg') + } context 'when unit_value and unit_description are present' do it 'returns the scaled unit value with the description' do @@ -30,7 +31,7 @@ end context 'when variant_unit_scale is nil' do - before { product.update_column(:variant_unit_scale, nil) } + before { variant.update_column(:variant_unit_scale, nil) } it 'uses default scale of 1 and returns the unscaled unit value with the description' do expect(helper.unit_value_with_description(variant)).to eq('2000 kg') diff --git a/spec/system/admin/products_v3/update_spec.rb b/spec/system/admin/products_v3/update_spec.rb index 2d6bd09accf..9fa093311a9 100644 --- a/spec/system/admin/products_v3/update_spec.rb +++ b/spec/system/admin/products_v3/update_spec.rb @@ -49,35 +49,34 @@ fill_in "Name", with: "Pommes" fill_in "SKU", with: "POM-00" end + within row_containing_name("Medium box") do fill_in "Name", with: "Large box" fill_in "SKU", with: "POM-01" tomselect_select "Volume (mL)", from: "Unit scale" - click_on "Unit" # activate popout - end - # Unit popout - # have to use below method to trigger the +change+ event, - # +fill_in "Unit value", with: ""+ does not trigger +change+ event - find_field('Unit value').send_keys(:control, 'a', :backspace) # empty the field - click_button "Save changes" # attempt to save and should fail with below error - expect(page).to have_content "must be greater than 0" - click_on "Unit" # activate popout - fill_in "Unit value", with: "500.1" - - within row_containing_name("Medium box") do - fill_in "Name", with: "Large box" - fill_in "SKU", with: "POM-01" + # Unit popout + click_on "Unit" # activate popout + # have to use below method to trigger the +change+ event, + # +fill_in "Unit value", with: ""+ does not trigger +change+ event + find_field('Unit value').send_keys(:control, 'a', :backspace) # empty the field + # In CI we get "Please fill out this field." and locally we get + # "Please fill in this field." + expect_browser_validation('input[aria-label="Unit value"]', + /Please fill (in|out) this field./) + + fill_in "Unit value", with: "500.1" fill_in "Price", with: "10.25" + # Stock popout click_on "On Hand" # activate popout + fill_in "On Hand", with: "-1" end - # Stock popout - fill_in "On Hand", with: "-1" click_button "Save changes" # attempt to save or close the popout expect(page).to have_field "On Hand", with: "-1" # popout is still open + fill_in "On Hand", with: "6" expect { @@ -553,8 +552,6 @@ expect(page).to have_content "is too long" expect(page.find('.col-producer')).to have_content('must exist') expect(page.find('.col-category')).to have_content('must exist') - expect(page.find_button("Unit")).to have_text "" # have_button selector don't work here - expect(page).to have_content "can't be blank" expect(page).to have_field "Price", with: "10.25" # other updated value is retained end From dbca2e2b568e8e18cfefacc73ecace5b07137538 Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou Date: Mon, 19 Aug 2024 14:19:02 +1000 Subject: [PATCH 55/68] Add all columns moved to variant to `ignored_columns` --- app/models/spree/product.rb | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/models/spree/product.rb b/app/models/spree/product.rb index e1228f56a4e..c603541f25d 100755 --- a/app/models/spree/product.rb +++ b/app/models/spree/product.rb @@ -22,7 +22,12 @@ class Product < ApplicationRecord include LogDestroyPerformer self.belongs_to_required_by_default = false - self.ignored_columns += [:supplier_id, :variant_unit_scale, :variant_unit_name] + # These columns have been moved to variant. Currently this is only for documentation purposes, + # because they are declared as attr_accessor below, declaring them as ignored columns has no + # effect + self.ignored_columns += [ + :supplier_id, :primary_taxon_id, :variant_unit, :variant_unit_scale, :variant_unit_name + ] acts_as_paranoid From 377f035ea8a058641723d13565b7bbb4e64af7b0 Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou Date: Wed, 21 Aug 2024 11:37:24 +1000 Subject: [PATCH 56/68] Fix bulk coop report The current spec is useless, but it has been addressed on master --- lib/reporting/reports/bulk_coop/base.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/reporting/reports/bulk_coop/base.rb b/lib/reporting/reports/bulk_coop/base.rb index ce4c154e353..bd6b3d03e7b 100644 --- a/lib/reporting/reports/bulk_coop/base.rb +++ b/lib/reporting/reports/bulk_coop/base.rb @@ -49,7 +49,7 @@ def full_name(line_items) def group_buy_unit_size(line_items) unit_size = line_items.first.variant.product.group_buy_unit_size || 0.0 - unit_size / (line_items.first.product.variant_unit_scale || 1) + unit_size / (line_items.first.variant.variant_unit_scale || 1) end def max_quantity_excess(line_items) @@ -64,7 +64,7 @@ def max_quantity_amount(line_items) end def scaled_unit_value(variant) - (variant.unit_value || 0) / (variant.product.variant_unit_scale || 1) + (variant.unit_value || 0) / (variant.variant_unit_scale || 1) end def option_value_value(line_items) @@ -98,7 +98,7 @@ def total_amount(line_items) end def scaled_final_weight_volume(line_item) - (line_item.final_weight_volume || 0) / (line_item.product.variant_unit_scale || 1) + (line_item.final_weight_volume || 0) / (line_item.variant.variant_unit_scale || 1) end def total_available(line_items) From 3bb2232bc196206c7c63b010f40c8590360cd9e4 Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou Date: Mon, 26 Aug 2024 15:05:25 +1000 Subject: [PATCH 57/68] Remove non updatable check when updating a product After the product redactor it only checked for the "description" on product, which is actually skipped when doing an update. --- .../admin/product_import_controller.rb | 4 +-- app/models/product_import/entry_validator.rb | 25 ------------------- spec/models/product_importer_spec.rb | 2 +- 3 files changed, 2 insertions(+), 29 deletions(-) diff --git a/app/controllers/admin/product_import_controller.rb b/app/controllers/admin/product_import_controller.rb index 54a9ac84d54..9331023aad6 100644 --- a/app/controllers/admin/product_import_controller.rb +++ b/app/controllers/admin/product_import_controller.rb @@ -21,9 +21,7 @@ def import @importer = ProductImport::ProductImporter.new(File.new(@filepath), spree_current_user, params[:settings]) @original_filename = params[:file].try(:original_filename) - @non_updatable_fields = ProductImport::EntryValidator.non_updatable_product_fields.merge( - ProductImport::EntryValidator.non_updatable_variant_fields - ) + @non_updatable_fields = ProductImport::EntryValidator.non_updatable_variant_fields return if contains_errors? @importer @ams_data = ams_data diff --git a/app/models/product_import/entry_validator.rb b/app/models/product_import/entry_validator.rb index 532f0281bf7..a6a2277afda 100644 --- a/app/models/product_import/entry_validator.rb +++ b/app/models/product_import/entry_validator.rb @@ -6,8 +6,6 @@ module ProductImport class EntryValidator - SKIP_VALIDATE_ON_UPDATE = [:description].freeze - # rubocop:disable Metrics/ParameterLists def initialize(current_user, import_time, spreadsheet_data, editable_enterprises, inventory_permissions, reset_counts, import_settings, all_entries) @@ -22,12 +20,6 @@ def initialize(current_user, import_time, spreadsheet_data, editable_enterprises end # rubocop:enable Metrics/ParameterLists - def self.non_updatable_product_fields - { - description: :description, - } - end - def self.non_updatable_variant_fields { unit_type: :variant_unit_scale, @@ -357,8 +349,6 @@ def product_validation(entry) return end - products.each { |product| product_field_errors(entry, product) } - products.flat_map(&:variants).each do |existing_variant| next unless entry.match_variant?(existing_variant) && existing_variant.deleted_at.nil? @@ -410,27 +400,12 @@ def variant_field_errors(entry, existing_variant) end end - def product_field_errors(entry, existing_product) - EntryValidator.non_updatable_product_fields.each do |display_name, attribute| - next if attributes_match?(attribute, existing_product, entry) || - attributes_blank?(attribute, existing_product, entry) - next if ignore_when_updating_product?(attribute) - - mark_as_invalid(entry, attribute: display_name, - error: I18n.t('admin.product_import.model.not_updatable')) - end - end - def attributes_match?(attribute, existing_product, entry) existing_product_value = existing_product.public_send(attribute) entry_value = entry.public_send(attribute) existing_product_value == convert_to_trusted_type(entry_value, existing_product_value) end - def ignore_when_updating_product?(attribute) - SKIP_VALIDATE_ON_UPDATE.include? attribute - end - def convert_to_trusted_type(untrusted_attribute, trusted_attribute) case trusted_attribute when Integer diff --git a/spec/models/product_importer_spec.rb b/spec/models/product_importer_spec.rb index 3743aaead63..60d42c6154b 100644 --- a/spec/models/product_importer_spec.rb +++ b/spec/models/product_importer_spec.rb @@ -575,7 +575,7 @@ end end - describe "updating non-updatable fields on existing products" do + describe "updating non-updatable fields on existing variants" do let(:csv_data) { CSV.generate do |csv| csv << ["name", "producer", "category", "on_hand", "price", "units", "unit_type", From 7c2c614f903399a4ae927ee8aed6b16ac1d47101 Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou <40413322+rioug@users.noreply.github.com> Date: Mon, 26 Aug 2024 13:52:04 +1000 Subject: [PATCH 58/68] Update spec/models/spree/variant_spec.rb Co-authored-by: David Cook --- spec/models/spree/variant_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/models/spree/variant_spec.rb b/spec/models/spree/variant_spec.rb index 7f3d1559ef7..c0974a3baa8 100644 --- a/spec/models/spree/variant_spec.rb +++ b/spec/models/spree/variant_spec.rb @@ -50,7 +50,7 @@ it { is_expected.to validate_presence_of :variant_unit } - context "when the product's unit is items" do + context "when the unit is items" do subject(:variant) { build(:variant, variant_unit: "items", variant_unit_name: "box") } it "is valid with only unit value set" do From 2a671d491d12fcc8b7d74d55d61cf417d164cd65 Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou Date: Mon, 26 Aug 2024 15:14:13 +1000 Subject: [PATCH 59/68] Remove commented out code --- app/models/spree/product.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/app/models/spree/product.rb b/app/models/spree/product.rb index c603541f25d..57f765413ce 100755 --- a/app/models/spree/product.rb +++ b/app/models/spree/product.rb @@ -86,7 +86,6 @@ class Product < ApplicationRecord :primary_taxon_id, :supplier_id after_create :ensure_standard_variant - # after_update :touch_supplier, if: :saved_change_to_primary_taxon_id? around_destroy :destruction after_touch :touch_supplier From ce0c7929a774f85301ef4958f0231271df41e9f0 Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou Date: Mon, 26 Aug 2024 16:17:31 +1000 Subject: [PATCH 60/68] Per review, remove the use of `raw` --- app/views/spree/admin/variants/_form.html.haml | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/app/views/spree/admin/variants/_form.html.haml b/app/views/spree/admin/variants/_form.html.haml index 5cd9cf130d9..1701eed79bb 100644 --- a/app/views/spree/admin/variants/_form.html.haml +++ b/app/views/spree/admin/variants/_form.html.haml @@ -11,7 +11,9 @@ = f.text_field :display_name, class: "fullwidth", placeholder: t('.display_name_placeholder') .field{ 'data-controller': 'toggle-control', 'data-toggle-control-match-value': 'items' } - = f.label :unit_scale, raw(t('.unit_scale') + content_tag(:span, ' *', :class => 'required')) + = f.label :unit_scale do + = t('.unit_scale') + = content_tag(:span, ' *', class: 'required') = f.hidden_field :variant_unit = f.hidden_field :variant_unit_scale = f.select :variant_unit_with_scale, @@ -24,7 +26,9 @@ = error_message_on @variant, :variant_unit_name, 'data-toggle-control-target': 'control' .field.popout{'data-controller': "popout", 'data-popout-update-display-value': "false"} - = f.label :unit, raw(t('.unit') + content_tag(:span, ' *', :class => 'required')) + = f.label :unit do + = t('.unit') + = content_tag(:span, ' *', class: 'required') = f.button :unit_to_display, class: "popout__button", 'aria-label': t('.unit'), 'data-popout-target': "button" do = @variant.unit_to_display # Show the generated summary of unit values %div.popout__container{ style: 'display: none;', 'data-controller': 'toggle-control', 'data-popout-target': "dialog" } @@ -44,7 +48,9 @@ = f.label :sku, t('.sku') = f.text_field :sku, class: 'fullwidth' .field - = f.label :price, raw(t('.price') + content_tag(:span, ' *', :class => 'required')) + = f.label :price do + = t('.price') + = content_tag(:span, ' *', class: 'required') = f.text_field :price, class: 'fullwidth', value: number_to_currency(@variant.price, unit: '')&.strip .field = hidden_field_tag 'variant_variant_unit', @variant.variant_unit @@ -97,7 +103,10 @@ = f.collection_select(:primary_taxon_id, Spree::Taxon.order(:name), :id, :name, { include_blank: true }, { class: "select2 fullwidth" }) .field - = f.label :supplier, raw(t(:spree_admin_supplier) + content_tag(:span, ' *', :class => 'required')) + = f.label :supplier do + = t(:spree_admin_supplier) + = content_tag(:span, ' *', class: 'required') + = f.collection_select(:supplier_id, @producers, :id, :name, {:include_blank => true}, {:class => "select2 fullwidth"}) .clear From 04e14bf38b9428117b851bf1c7eecf318d30708e Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou Date: Mon, 26 Aug 2024 16:32:47 +1000 Subject: [PATCH 61/68] Per review, check value are saved in the database --- spec/system/admin/variants_spec.rb | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/spec/system/admin/variants_spec.rb b/spec/system/admin/variants_spec.rb index 79a49cbccda..2f6280f7783 100644 --- a/spec/system/admin/variants_spec.rb +++ b/spec/system/admin/variants_spec.rb @@ -33,6 +33,14 @@ # Then the variant should have been created expect(page).to have_content "Variant \"#{product.name}\" has been successfully created!" + + new_variant = Spree::Variant.last + expect(new_variant.unit_value).to eq(1) + expect(new_variant.variant_unit).to eq("volume") + expect(new_variant.variant_unit_scale).to eq(1) # Liter + expect(new_variant.price).to eq(2.5) + expect(new_variant.primary_taxon).to eq(taxon) + expect(new_variant.supplier).to eq(product.variants.first.supplier) end it "creating a new variant from product variant page with filter" do From 755a394704fcec7d9f2ffc26d93328b94526dc7d Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou Date: Tue, 27 Aug 2024 10:12:14 +1000 Subject: [PATCH 62/68] Fix spec to remove reliance on browser's message Client side validation messages depend on the browser's locale, which we have no controll over. Now we just check a message is set. --- spec/system/admin/products_v3/update_spec.rb | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/spec/system/admin/products_v3/update_spec.rb b/spec/system/admin/products_v3/update_spec.rb index 9fa093311a9..8fc2cf5ab05 100644 --- a/spec/system/admin/products_v3/update_spec.rb +++ b/spec/system/admin/products_v3/update_spec.rb @@ -61,10 +61,7 @@ # have to use below method to trigger the +change+ event, # +fill_in "Unit value", with: ""+ does not trigger +change+ event find_field('Unit value').send_keys(:control, 'a', :backspace) # empty the field - # In CI we get "Please fill out this field." and locally we get - # "Please fill in this field." - expect_browser_validation('input[aria-label="Unit value"]', - /Please fill (in|out) this field./) + expect_browser_validation('input[aria-label="Unit value"]') fill_in "Unit value", with: "500.1" fill_in "Price", with: "10.25" @@ -514,8 +511,7 @@ # Client side validation click_button "Save changes" within new_variant_row do - expect_browser_validation('select[aria-label="Unit scale"]', - "Please select an item in the list.") + expect_browser_validation('select[aria-label="Unit scale"]') end # Fix error @@ -528,8 +524,7 @@ within new_variant_row do # In CI we get "Please fill out this field." and locally we get # "Please fill in this field." - expect_browser_validation('input[aria-label="Unit value"]', - /Please fill (in|out) this field./) + expect_browser_validation('input[aria-label="Unit value"]') end # Fix error @@ -746,8 +741,10 @@ end end - def expect_browser_validation(selector, message) + # Check a validation message is set, we don't check the message itself because the value is based + # on the browser's locale. + def expect_browser_validation(selector) browser_message = page.find(selector)["validationMessage"] - expect(browser_message).to match message + expect(browser_message.present?).to be(true) end end From b0433bd8f53ff67ba90993f19e18b1727f15e46b Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou Date: Tue, 27 Aug 2024 10:41:33 +1000 Subject: [PATCH 63/68] Re add test for invalid cloned products There is no easy way to make the original product invalid, but we can make sure the cloned product will be invalid. The cloned product add "COPe OF " in front of the product's name, so by starting with a name that's long enough, the cloned product will have a name longer that 255 char and will then be invalid. --- spec/lib/spree/core/product_duplicator_spec.rb | 15 +++++++++++++++ spec/models/spree/product_spec.rb | 7 +++++++ spec/system/admin/products_v3/actions_spec.rb | 15 +++++++++++++++ 3 files changed, 37 insertions(+) diff --git a/spec/lib/spree/core/product_duplicator_spec.rb b/spec/lib/spree/core/product_duplicator_spec.rb index bab00af4685..e8163e961d7 100644 --- a/spec/lib/spree/core/product_duplicator_spec.rb +++ b/spec/lib/spree/core/product_duplicator_spec.rb @@ -68,6 +68,21 @@ end describe "errors" do + context "with invalid product" do + # Name has a max length of 255 char, when cloning a product the cloned product has a name + # starting with "COPY OF ". So we set a name with 254 char to make sure the + # cloned product will be invalid + let(:product) { + create(:product).tap{ |v| v.update_columns(name: "l" * 254) } + } + + subject { Spree::Core::ProductDuplicator.new(product).duplicate } + + it "raises RecordInvalid error" do + expect{ subject }.to raise_error(ActiveRecord::ActiveRecordError) + end + end + context "invalid variant" do let(:variant) { # tax_category is required when products_require_tax_category diff --git a/spec/models/spree/product_spec.rb b/spec/models/spree/product_spec.rb index e5919a4463a..df80c8f0521 100644 --- a/spec/models/spree/product_spec.rb +++ b/spec/models/spree/product_spec.rb @@ -17,6 +17,13 @@ module Spree expect(clone.sku).to eq "" expect(clone.image).to eq product.image end + + it 'fails to duplicate invalid product' do + # cloned product will be invalid + product.update_columns(name: "l" * 254) + + expect{ product.duplicate }.to raise_error(ActiveRecord::ActiveRecordError) + end end context "product has variants" do diff --git a/spec/system/admin/products_v3/actions_spec.rb b/spec/system/admin/products_v3/actions_spec.rb index 9abb75cd56b..872b19ae7db 100644 --- a/spec/system/admin/products_v3/actions_spec.rb +++ b/spec/system/admin/products_v3/actions_spec.rb @@ -294,6 +294,21 @@ def save_preferences end end end + + it "shows error message when cloning invalid record" do + # The cloned product will be invalid + product_a.update_columns(name: "L" * 254) + + # The page has not been reloaded so the product's name is still "Apples" + click_product_clone "Apples" + + expect(page).to have_content "Unable to clone the product" + + within "table.products" do + # Products does not include the cloned product. + expect(all_input_values).not_to match /COPY OF #{('L' * 254)}/ + end + end end describe "delete" do From ef1f3207f70fd0158af2df96273e5ab6edea4830 Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou <40413322+rioug@users.noreply.github.com> Date: Tue, 27 Aug 2024 15:50:14 +1000 Subject: [PATCH 64/68] Update spec/system/admin/products_v3/update_spec.rb Co-authored-by: Maikel --- spec/system/admin/products_v3/update_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/system/admin/products_v3/update_spec.rb b/spec/system/admin/products_v3/update_spec.rb index 8fc2cf5ab05..df298457884 100644 --- a/spec/system/admin/products_v3/update_spec.rb +++ b/spec/system/admin/products_v3/update_spec.rb @@ -745,6 +745,6 @@ # on the browser's locale. def expect_browser_validation(selector) browser_message = page.find(selector)["validationMessage"] - expect(browser_message.present?).to be(true) + expect(browser_message).to be_present end end From 40afe7e0abe401fdbeec69f7f6df7e0d7f0746a9 Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou Date: Tue, 3 Sep 2024 15:50:56 +1000 Subject: [PATCH 65/68] Fix rebase issue --- .../spec/services/affiliate_sales_data_builder_spec.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/engines/dfc_provider/spec/services/affiliate_sales_data_builder_spec.rb b/engines/dfc_provider/spec/services/affiliate_sales_data_builder_spec.rb index 9673b70045f..6d11af2b7cc 100644 --- a/engines/dfc_provider/spec/services/affiliate_sales_data_builder_spec.rb +++ b/engines/dfc_provider/spec/services/affiliate_sales_data_builder_spec.rb @@ -25,7 +25,8 @@ :product, name: "Pomme", supplier_id: supplier.id, - variant_unit: "item", + variant_unit: "items", + variant_unit_name: "bag", ) variant = product.variants.first distributor = create( From 67c11333f3b900bb45ce5b4311aa1a472c0baa33 Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou Date: Mon, 23 Sep 2024 14:46:28 +1000 Subject: [PATCH 66/68] Use AdminTooltipComponent, instead of partial --- app/views/spree/admin/variants/_form.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/spree/admin/variants/_form.html.haml b/app/views/spree/admin/variants/_form.html.haml index 1701eed79bb..25adcabac37 100644 --- a/app/views/spree/admin/variants/_form.html.haml +++ b/app/views/spree/admin/variants/_form.html.haml @@ -72,7 +72,7 @@ = f.check_box :on_demand, data: { "action": "click->edit-variant#toggleOnHand" } = t(:on_demand) - = render partial: "admin/shared/tooltip", locals: { tooltip_text: t('admin.products.variants.to_order_tip'), link_text: t('admin.whats_this'), placement: "right" } + = render AdminTooltipComponent.new(text: t('admin.products.variants.to_order_tip'), link_text: t('admin.whats_this'), placement: "right") .field = f.label :on_hand, t(:on_hand) .fullwidth From f8eeca856ec8ae594070dce285917ff08ac4c4cc Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou Date: Mon, 23 Sep 2024 14:47:15 +1000 Subject: [PATCH 67/68] Fix invoice print specs --- spec/system/admin/invoice_print_spec.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spec/system/admin/invoice_print_spec.rb b/spec/system/admin/invoice_print_spec.rb index 3c9a6adb398..9b5375b4c63 100644 --- a/spec/system/admin/invoice_print_spec.rb +++ b/spec/system/admin/invoice_print_spec.rb @@ -303,7 +303,8 @@ context "Line item with variant having variant_unit as 'items'" do before do - line_item1.variant.update!(variant_unit: "items", display_as: "1 bucket") + line_item1.variant.update!(variant_unit: "items", display_as: "1 bucket", + variant_unit_name: "bucket") login_as_admin visit spree.print_admin_order_path(order1, params: url_params) convert_pdf_to_page From 01337c12f012ceb47bf2c669385ece7f870e20b2 Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou Date: Mon, 30 Sep 2024 12:03:29 +1000 Subject: [PATCH 68/68] Post rebase, fix product cloning spec --- spec/system/admin/products_v3/actions_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/system/admin/products_v3/actions_spec.rb b/spec/system/admin/products_v3/actions_spec.rb index 872b19ae7db..9435ca128ab 100644 --- a/spec/system/admin/products_v3/actions_spec.rb +++ b/spec/system/admin/products_v3/actions_spec.rb @@ -302,7 +302,7 @@ def save_preferences # The page has not been reloaded so the product's name is still "Apples" click_product_clone "Apples" - expect(page).to have_content "Unable to clone the product" + expect(page).to have_content "Product Name is too long (maximum is 255 characters)" within "table.products" do # Products does not include the cloned product.