diff --git a/app/assets/javascripts/admin/bulk_order_management.js.coffee b/app/assets/javascripts/admin/bulk_order_management.js.coffee index 076dd55e3a..eb99f360af 100644 --- a/app/assets/javascripts/admin/bulk_order_management.js.coffee +++ b/app/assets/javascripts/admin/bulk_order_management.js.coffee @@ -32,7 +32,8 @@ angular.module("ofn.admin").controller "AdminOrderMgmtCtrl", [ variant: { name: "Variant", visible: true } quantity: { name: "Quantity", visible: true } max: { name: "Max", visible: true } - + unit_value: { name: "Weight/Volume", visible: false } + price: { name: "Price", visible: false } $scope.initialise = -> $scope.initialiseVariables() authorise_api_reponse = "" @@ -165,6 +166,20 @@ angular.module("ofn.admin").controller "AdminOrderMgmtCtrl", [ $scope.orderCycleFilter = $scope.orderCycles[0].id $scope.quickSearch = "" + $scope.weightAdjustedPrice = (lineItem, oldValue) -> + if oldValue <= 0 + oldValue = lineItem.units_variant.unit_value + if lineItem.unit_value <= 0 + lineItem.unit_value = lineItem.units_variant.unit_value + lineItem.price = lineItem.price * lineItem.unit_value / oldValue + #$scope.bulk_order_form.line_item.price.$setViewValue($scope.bulk_order_form.line_item.price.$viewValue) + + $scope.unitValueLessThanZero = (lineItem) -> + if lineItem.units_variant.unit_value <= 0 + true + else + false + $scope.$watch "orderCycleFilter", (newVal, oldVal) -> unless $scope.orderCycleFilter == "0" || angular.equals(newVal, oldVal) $scope.startDate = $scope.orderCyclesByID[$scope.orderCycleFilter].first_order diff --git a/app/assets/javascripts/admin/directives/line_item_upd_attr.js.coffee b/app/assets/javascripts/admin/directives/line_item_upd_attr.js.coffee index c83d7fdc0f..c5afce07a5 100644 --- a/app/assets/javascripts/admin/directives/line_item_upd_attr.js.coffee +++ b/app/assets/javascripts/admin/directives/line_item_upd_attr.js.coffee @@ -8,7 +8,9 @@ angular.module("ofn.admin").directive "ofnLineItemUpdAttr", [ scope.$watch -> scope.$eval(attrs.ngModel) , (value) -> - if ngModel.$dirty + #if ngModel.$dirty + # i think i can take this out, this directive is still only called + # on a change and only an updated value will create a db call. if value == element.dbValue pendingChanges.remove(scope.line_item.id, attrName) switchClass( element, "", ["update-pending", "update-error", "update-success"], false ) @@ -20,4 +22,4 @@ angular.module("ofn.admin").directive "ofnLineItemUpdAttr", [ url: "/api/orders/#{scope.line_item.order.number}/line_items/#{scope.line_item.id}?line_item[#{attrName}]=#{value}" pendingChanges.add(scope.line_item.id, attrName, changeObj) switchClass( element, "update-pending", ["update-error", "update-success"], false ) -] \ No newline at end of file +] diff --git a/app/controllers/spree/api/line_items_controller_decorator.rb b/app/controllers/spree/api/line_items_controller_decorator.rb new file mode 100644 index 0000000000..35fca864f4 --- /dev/null +++ b/app/controllers/spree/api/line_items_controller_decorator.rb @@ -0,0 +1,8 @@ +Spree::Api::LineItemsController.class_eval do + after_filter :apply_enterprise_fees, :only => :update + + def apply_enterprise_fees + authorize! :read, order + order.update_distribution_charge! + end +end diff --git a/app/models/spree/line_item_decorator.rb b/app/models/spree/line_item_decorator.rb index b2c0a583fc..4eec0bcd2b 100644 --- a/app/models/spree/line_item_decorator.rb +++ b/app/models/spree/line_item_decorator.rb @@ -1,5 +1,6 @@ Spree::LineItem.class_eval do - attr_accessible :max_quantity + attr_accessible :max_quantity, :unit_value + attr_accessible :unit_value, :price, :as => :api # -- Scopes scope :managed_by, lambda { |user| diff --git a/app/models/spree/order_decorator.rb b/app/models/spree/order_decorator.rb index 0dc6977965..cbc5c0086d 100644 --- a/app/models/spree/order_decorator.rb +++ b/app/models/spree/order_decorator.rb @@ -127,6 +127,7 @@ Spree::Order.class_eval do else current_item = Spree::LineItem.new(:quantity => quantity, max_quantity: max_quantity) current_item.variant = variant + current_item.unit_value = variant.unit_value if currency current_item.currency = currency unless currency.nil? current_item.price = variant.price_in(currency).amount diff --git a/app/serializers/api/admin/line_item_serializer.rb b/app/serializers/api/admin/line_item_serializer.rb index 784e1e8be3..21fde91145 100644 --- a/app/serializers/api/admin/line_item_serializer.rb +++ b/app/serializers/api/admin/line_item_serializer.rb @@ -1,5 +1,5 @@ class Api::Admin::LineItemSerializer < ActiveModel::Serializer - attributes :id, :quantity, :max_quantity, :supplier, :units_product, :units_variant + attributes :id, :quantity, :max_quantity, :supplier, :price, :unit_value, :units_product, :units_variant def supplier Api::Admin::IdNameSerializer.new(object.product.supplier).serializable_hash @@ -12,4 +12,8 @@ class Api::Admin::LineItemSerializer < ActiveModel::Serializer def units_variant Api::Admin::UnitsVariantSerializer.new(object.variant).serializable_hash end + + def unit_value + object.unit_value.to_f + end end diff --git a/app/views/spree/admin/orders/bulk_management.html.haml b/app/views/spree/admin/orders/bulk_management.html.haml index 0f9bd05d04..caf18c6cd3 100644 --- a/app/views/spree/admin/orders/bulk_management.html.haml +++ b/app/views/spree/admin/orders/bulk_management.html.haml @@ -100,53 +100,60 @@ %div{ :class => "sixteen columns alpha", 'ng-show' => '!loading && filteredLineItems.length == 0'} %h1#no_results No orders found. %div{ 'ng-hide' => 'loading || filteredLineItems.length == 0' } - %table.index#listing_orders.bulk{ :class => "sixteen columns alpha" } - %thead - %tr - %th.bulk - %input{ :type => "checkbox", :name => 'toggle_bulk', 'ng-click' => 'toggleAllCheckboxes()', 'ng-checked' => "allBoxesChecked()" } - %th.order_no{ 'ng-show' => 'columns.order_no.visible' } - %a{ :href => '', 'ng-click' => "predicate = 'order.number'; reverse = !reverse" } Order No. - %th.full_name{ 'ng-show' => 'columns.full_name.visible' } - %a{ :href => '', 'ng-click' => "predicate = 'order.full_name'; reverse = !reverse" } Name - %th.email{ 'ng-show' => 'columns.email.visible' } - %a{ :href => '', 'ng-click' => "predicate = 'order.email'; reverse = !reverse" } Email - %th.phone{ 'ng-show' => 'columns.phone.visible' } - %a{ :href => '', 'ng-click' => "predicate = 'order.phone'; reverse = !reverse" } Phone - %th.date{ 'ng-show' => 'columns.order_date.visible' } - %a{ :href => '', 'ng-click' => "predicate = 'order.completed_at'; reverse = !reverse" } Order Date - %th.producer{ 'ng-show' => 'columns.producer.visible' } - %a{ :href => '', 'ng-click' => "predicate = 'supplier.name'; reverse = !reverse" } Producer - %th.order_cycle{ 'ng-show' => 'columns.order_cycle.visible' } - %a{ :href => '', 'ng-click' => "predicate = 'order.order_cycle.name'; reverse = !reverse" } Order Cycle - %th.hub{ 'ng-show' => 'columns.hub.visible' } - %a{ :href => '', 'ng-click' => "predicate = 'order.distributor.name'; reverse = !reverse" } Hub - %th.variant{ 'ng-show' => 'columns.variant.visible' } - %a{ :href => '', 'ng-click' => "predicate = 'units_variant.full_name'; reverse = !reverse" } Product: Unit - %th.quantity{ 'ng-show' => 'columns.quantity.visible' } Quantity - %th.max{ 'ng-show' => 'columns.max.visible' } Max - %th.actions - %th.actions - Ask?  - %input{ :type => 'checkbox', 'ng-model' => "confirmDelete" } - %tr.line_item{ 'ng-repeat' => "line_item in filteredLineItems = ( lineItems | filter:quickSearch | selectFilter:supplierFilter:distributorFilter:orderCycleFilter | variantFilter:selectedUnitsProduct:selectedUnitsVariant:sharedResource | orderBy:predicate:reverse )", 'ng-class-even' => "'even'", 'ng-class-odd' => "'odd'", :id => "li_{{line_item.id}}" } - %td.bulk - %input{ :type => "checkbox", :name => 'bulk', 'ng-model' => 'line_item.checked' } - %td.order_no{ 'ng-show' => 'columns.order_no.visible' } {{ line_item.order.number }} - %td.full_name{ 'ng-show' => 'columns.full_name.visible' } {{ line_item.order.full_name }} - %td.email{ 'ng-show' => 'columns.email.visible' } {{ line_item.order.email }} - %td.phone{ 'ng-show' => 'columns.phone.visible' } {{ line_item.order.phone }} - %td.date{ 'ng-show' => 'columns.order_date.visible' } {{ line_item.order.completed_at }} - %td.producer{ 'ng-show' => 'columns.producer.visible' } {{ line_item.supplier.name }} - %td.order_cycle{ 'ng-show' => 'columns.order_cycle.visible' } {{ line_item.order.order_cycle.name }} - %td.hub{ 'ng-show' => 'columns.hub.visible' } {{ line_item.order.distributor.name }} - %td.variant{ 'ng-show' => 'columns.variant.visible' } - %a{ :href => '#', 'ng-click' => "setSelectedUnitsVariant(line_item.units_product,line_item.units_variant)" } {{ line_item.units_variant.full_name }} - %td.quantity{ 'ng-show' => 'columns.quantity.visible' } - %input{ :type => 'number', :name => 'quantity', 'ng-model' => "line_item.quantity", 'ofn-line-item-upd-attr' => "quantity" } - %td.max{ 'ng-show' => 'columns.max.visible' } {{ line_item.max_quantity }} - %td.actions - %a{ :class => "edit-order icon-edit no-text", 'ofn-confirm-link-path' => "/admin/orders/{{line_item.order.number}}/edit" } - %td.actions - %a{ 'ng-click' => "deleteLineItem(line_item)", :class => "delete-line-item icon-trash no-text" } - %input{ :type => "button", 'value' => 'Update', 'ng-click' => 'pendingChanges.submitAll()' } + %form{ 'ng-model' => "bulk_order_form" } + %table.index#listing_orders.bulk{ :class => "sixteen columns alpha" } + %thead + %tr + %th.bulk + %input{ :type => "checkbox", :name => 'toggle_bulk', 'ng-click' => 'toggleAllCheckboxes()', 'ng-checked' => "allBoxesChecked()" } + %th.order_no{ 'ng-show' => 'columns.order_no.visible' } + %a{ :href => '', 'ng-click' => "predicate = 'order.number'; reverse = !reverse" } Order No. + %th.full_name{ 'ng-show' => 'columns.full_name.visible' } + %a{ :href => '', 'ng-click' => "predicate = 'order.full_name'; reverse = !reverse" } Name + %th.email{ 'ng-show' => 'columns.email.visible' } + %a{ :href => '', 'ng-click' => "predicate = 'order.email'; reverse = !reverse" } Email + %th.phone{ 'ng-show' => 'columns.phone.visible' } + %a{ :href => '', 'ng-click' => "predicate = 'order.phone'; reverse = !reverse" } Phone + %th.date{ 'ng-show' => 'columns.order_date.visible' } + %a{ :href => '', 'ng-click' => "predicate = 'order.completed_at'; reverse = !reverse" } Order Date + %th.producer{ 'ng-show' => 'columns.producer.visible' } + %a{ :href => '', 'ng-click' => "predicate = 'supplier.name'; reverse = !reverse" } Producer + %th.order_cycle{ 'ng-show' => 'columns.order_cycle.visible' } + %a{ :href => '', 'ng-click' => "predicate = 'order.order_cycle.name'; reverse = !reverse" } Order Cycle + %th.hub{ 'ng-show' => 'columns.hub.visible' } + %a{ :href => '', 'ng-click' => "predicate = 'order.distributor.name'; reverse = !reverse" } Hub + %th.variant{ 'ng-show' => 'columns.variant.visible' } + %a{ :href => '', 'ng-click' => "predicate = 'units_variant.full_name'; reverse = !reverse" } Product: Unit + %th.quantity{ 'ng-show' => 'columns.quantity.visible' } Quantity + %th.max{ 'ng-show' => 'columns.max.visible' } Max + %th.unit_value{ 'ng-show' => 'columns.unit_value.visible' } Weight/Volume + %th.price{ 'ng-show' => 'columns.price.visible' } Price + %th.actions + %th.actions + Ask?  + %input{ :type => 'checkbox', 'ng-model' => "confirmDelete" } + %tr.line_item{ 'ng-repeat' => "line_item in filteredLineItems = ( lineItems | filter:quickSearch | selectFilter:supplierFilter:distributorFilter:orderCycleFilter | variantFilter:selectedUnitsProduct:selectedUnitsVariant:sharedResource | orderBy:predicate:reverse )", 'ng-class-even' => "'even'", 'ng-class-odd' => "'odd'", :id => "li_{{line_item.id}}" } + %td.bulk + %input{ :type => "checkbox", :name => 'bulk', 'ng-model' => 'line_item.checked' } + %td.order_no{ 'ng-show' => 'columns.order_no.visible' } {{ line_item.order.number }} + %td.full_name{ 'ng-show' => 'columns.full_name.visible' } {{ line_item.order.full_name }} + %td.email{ 'ng-show' => 'columns.email.visible' } {{ line_item.order.email }} + %td.phone{ 'ng-show' => 'columns.phone.visible' } {{ line_item.order.phone }} + %td.date{ 'ng-show' => 'columns.order_date.visible' } {{ line_item.order.completed_at }} + %td.producer{ 'ng-show' => 'columns.producer.visible' } {{ line_item.supplier.name }} + %td.order_cycle{ 'ng-show' => 'columns.order_cycle.visible' } {{ line_item.order.order_cycle.name }} + %td.hub{ 'ng-show' => 'columns.hub.visible' } {{ line_item.order.distributor.name }} + %td.variant{ 'ng-show' => 'columns.variant.visible' } + %a{ :href => '#', 'ng-click' => "setSelectedUnitsVariant(line_item.units_product,line_item.units_variant)" } {{ line_item.units_variant.full_name }} + %td.quantity{ 'ng-show' => 'columns.quantity.visible' } + %input{ :type => 'number', :name => 'quantity', 'ng-model' => "line_item.quantity", 'ofn-line-item-upd-attr' => "quantity" } + %td.max{ 'ng-show' => 'columns.max.visible' } {{ line_item.max_quantity }} + %td.unit_value{ 'ng-show' => 'columns.unit_value.visible' } + %input{ :type => 'number', :name => 'unit_value', :id => 'unit_value', 'ng-model' => "line_item.unit_value", 'ng-readonly' => "unitValueLessThanZero(line_item)", 'ng-change' => "weightAdjustedPrice(line_item, {{ line_item.unit_value }})", 'ofn-line-item-upd-attr' => "unit_value" } + %td.price{ 'ng-show' => 'columns.price.visible' } + %input{ :type => 'text', :name => 'price', :id => 'price', :value => '{{ line_item.price | currency }}', 'ng-model' => "line_item.price", 'ng-readonly' => "true", 'ofn-line-item-upd-attr' => "price" } + %td.actions + %a{ :class => "edit-order icon-edit no-text", 'ofn-confirm-link-path' => "/admin/orders/{{line_item.order.number}}/edit" } + %td.actions + %a{ 'ng-click' => "deleteLineItem(line_item)", :class => "delete-line-item icon-trash no-text" } + %input{ :type => "button", 'value' => 'Update', 'ng-click' => 'pendingChanges.submitAll()' } diff --git a/db/migrate/20150305004846_add_weight_to_line_items.rb b/db/migrate/20150305004846_add_weight_to_line_items.rb new file mode 100644 index 0000000000..a4df1a12f0 --- /dev/null +++ b/db/migrate/20150305004846_add_weight_to_line_items.rb @@ -0,0 +1,5 @@ +class AddWeightToLineItems < ActiveRecord::Migration + def change + add_column :spree_line_items, :unit_value, :decimal, :precision => 8, :scale => 2 + end +end diff --git a/db/migrate/20150424151117_populate_line_item_unit_value.rb b/db/migrate/20150424151117_populate_line_item_unit_value.rb new file mode 100644 index 0000000000..2122b84472 --- /dev/null +++ b/db/migrate/20150424151117_populate_line_item_unit_value.rb @@ -0,0 +1,9 @@ +class PopulateLineItemUnitValue < ActiveRecord::Migration + def up + execute "UPDATE spree_line_items SET unit_value = spree_variants.unit_value FROM spree_variants WHERE spree_line_items.variant_id = spree_variants.id" + end + + def down + raise ActiveRecord::IrreversibleMigration + end +end diff --git a/db/schema.rb b/db/schema.rb index cb8b503583..f9f32af7d1 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended to check this file into your version control system. -ActiveRecord::Schema.define(:version => 20150424025907) do +ActiveRecord::Schema.define(:version => 20150424151117) do create_table "adjustment_metadata", :force => true do |t| t.integer "adjustment_id" @@ -549,6 +549,7 @@ ActiveRecord::Schema.define(:version => 20150424025907) do t.string "currency" t.decimal "distribution_fee", :precision => 10, :scale => 2 t.string "shipping_method_name" + t.decimal "unit_value", :precision => 8, :scale => 2 end add_index "spree_line_items", ["order_id"], :name => "index_line_items_on_order_id" @@ -618,9 +619,9 @@ ActiveRecord::Schema.define(:version => 20150424025907) do t.string "email" t.text "special_instructions" t.integer "distributor_id" + t.integer "order_cycle_id" t.string "currency" t.string "last_ip_address" - t.integer "order_cycle_id" t.integer "cart_id" end diff --git a/spec/controllers/spree/api/line_items_controller_spec.rb b/spec/controllers/spree/api/line_items_controller_spec.rb new file mode 100644 index 0000000000..37ec50eb7e --- /dev/null +++ b/spec/controllers/spree/api/line_items_controller_spec.rb @@ -0,0 +1,32 @@ +require 'spec_helper' + +module Spree + describe Spree::Api::LineItemsController do + render_views + + before do + stub_authentication! + Spree.user_class.stub :find_by_spree_api_key => current_api_user + end + + def self.make_simple_data! + let!(:order) { FactoryGirl.create(:order, state: 'complete', completed_at: Time.now) } + let!(:line_item) { FactoryGirl.create(:line_item, order: order, unit_value: 500) } + end + + #test that when a line item is updated, an order's fees are updated too + context "as an admin user" do + sign_in_as_admin! + make_simple_data! + + context "as a line item is updated" do + it "update distribution charge on the order" do + line_item_params = { order_id: order.number, id: line_item.id, line_item: { id: line_item.id, unit_value: 520 }, format: :json} + allow(controller).to receive(:order) { order } + expect(order).to receive(:update_distribution_charge!) + spree_post :update, line_item_params + end + end + end + end +end diff --git a/spec/features/admin/bulk_order_management_spec.rb b/spec/features/admin/bulk_order_management_spec.rb index 032acaca4e..3bbb23bb8a 100644 --- a/spec/features/admin/bulk_order_management_spec.rb +++ b/spec/features/admin/bulk_order_management_spec.rb @@ -57,7 +57,7 @@ feature %q{ end it "displays a column for order date" do - page.should have_selector "th,date", text: "ORDER DATE", :visible => true + page.should have_selector "th.date", text: "ORDER DATE", :visible => true page.should have_selector "td.date", text: o1.completed_at.strftime("%F %T"), :visible => true page.should have_selector "td.date", text: o2.completed_at.strftime("%F %T"), :visible => true end @@ -141,8 +141,22 @@ feature %q{ admin_user = quick_login_as_admin end + let!(:p1) { FactoryGirl.create(:product_with_option_types, group_buy: true, group_buy_unit_size: 5000, variant_unit: "weight", variants: [FactoryGirl.create(:variant, unit_value: 1000)] ) } + let!(:v1) { p1.variants.first } let!(:o1) { FactoryGirl.create(:order_with_distributor, state: 'complete', completed_at: Time.now ) } - let!(:li1) { FactoryGirl.create(:line_item, order: o1, :quantity => 5 ) } + let!(:li1) { FactoryGirl.create(:line_item, order: o1, variant: v1, :quantity => 5, :unit_value => 1000 ) } + + context "modifying the weight/volume of a line item" do + it "update-pending is added to variable 'price'" do + visit '/admin/orders/bulk_management' + first("div#columns_dropdown", :text => "COLUMNS").click + first("div#columns_dropdown div.menu div.menu_item", text: "Weight/Volume").click + page.should_not have_css "input[name='price'].update-pending" + li1_unit_value_column = find("tr#li_#{li1.id} td.unit_value") + li1_unit_value_column.fill_in "unit_value", :with => 1200 + page.should have_css "input[name='price'].update-pending", :visible => false + end + end context "using column display toggle" do it "shows a column display toggle button, which shows a list of columns when clicked" do diff --git a/spec/javascripts/unit/bulk_order_management_spec.js.coffee b/spec/javascripts/unit/bulk_order_management_spec.js.coffee index 71ebb9f3b9..122d17f539 100644 --- a/spec/javascripts/unit/bulk_order_management_spec.js.coffee +++ b/spec/javascripts/unit/bulk_order_management_spec.js.coffee @@ -350,6 +350,33 @@ describe "AdminOrderMgmtCtrl", -> spyOn(VariantUnitManager, "getUnitName").andReturn "kg" expect(scope.formattedValueWithUnitName(2000,unitsVariant)).toEqual "2 kg" + describe "updating the price upon updating the weight of a line item", -> + + it "resets the weight if the weight is set to zero", -> + scope.filteredLineItems = [ + { units_variant: { unit_value: 100 }, price: 2, unit_value: 0 } + ] + expect(scope.weightAdjustedPrice(scope.filteredLineItems[0], 100)).toEqual scope.filteredLineItems[0].price + + it "updates the price if the weight is changed", -> + scope.filteredLineItems = [ + { units_variant: { unit_value: 100 }, price: 2, unit_value: 200 } + ] + old_value = scope.filteredLineItems[0].units_variant.unit_value + new_value = scope.filteredLineItems[0].unit_value + sp = scope.filteredLineItems[0].price * new_value / old_value + expect(scope.weightAdjustedPrice(scope.filteredLineItems[0], old_value)).toEqual sp + + it "doesn't update the price if the weight is not changed", -> + scope.filteredLineItems = [ + { units_variant: { unit_value: 100 }, price: 2, unit_value: 100 } + ] + old_value = scope.filteredLineItems[0].unit_value + new_value = scope.filteredLineItems[0].unit_value + sp = scope.filteredLineItems[0].price + expect(scope.weightAdjustedPrice(scope.filteredLineItems[0], old_value)).toEqual sp + + describe "managing pending changes", -> dataSubmitter = pendingChangesService = null