Merge branch 'master' into customers

Conflicts:
	app/assets/javascripts/admin/bulk_order_management.js.coffee
	app/assets/javascripts/admin/bulk_product_update.js.coffee
	app/assets/javascripts/admin/directives/line_item_upd_attr.js.coffee
	app/views/spree/admin/orders/bulk_management.html.haml
	db/schema.rb
	spec/javascripts/unit/bulk_order_management_spec.js.coffee
This commit is contained in:
Rob Harrington
2015-06-03 15:19:40 +08:00
161 changed files with 2496 additions and 1351 deletions

3
.rspec_parallel Normal file
View File

@@ -0,0 +1,3 @@
--format progress
--format ParallelTests::RSpec::SummaryLogger --out tmp/spec_summary.log
--tag ~performance

View File

@@ -113,4 +113,5 @@ group :development do
gem 'guard-rails'
gem 'guard-zeus'
gem 'guard-rspec'
gem 'parallel_tests'
end

View File

@@ -364,6 +364,9 @@ GEM
activesupport (>= 3.0.0)
cocaine (~> 0.5.3)
mime-types
parallel (1.4.1)
parallel_tests (1.3.7)
parallel
paypal-sdk-core (0.2.10)
multi_json (~> 1.0)
xml-simple
@@ -578,6 +581,7 @@ DEPENDENCIES
newrelic_rpm
oj
paperclip
parallel_tests
pg
poltergeist
pry-debugger

View File

@@ -1,6 +1,6 @@
angular.module("ofn.admin").controller "AdminOrderMgmtCtrl", [
"$scope", "$http", "dataFetcher", "blankOption", "pendingChanges", "VariantUnitManager", "OptionValueNamer", "SpreeApiKey", "Columns"
($scope, $http, dataFetcher, blankOption, pendingChanges, VariantUnitManager, OptionValueNamer, SpreeApiKey, Columns) ->
"$scope", "$http", "$filter", "dataFetcher", "blankOption", "pendingChanges", "VariantUnitManager", "OptionValueNamer", "SpreeApiKey", "Columns"
($scope, $http, $filter, dataFetcher, blankOption, pendingChanges, VariantUnitManager, OptionValueNamer, SpreeApiKey, Columns) ->
$scope.loading = true
$scope.initialiseVariables = ->
@@ -30,7 +30,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 = ""
@@ -40,13 +41,15 @@ angular.module("ofn.admin").controller "AdminOrderMgmtCtrl", [
if $scope.spree_api_key_ok
$http.defaults.headers.common["X-Spree-Token"] = SpreeApiKey
dataFetcher("/api/enterprises/accessible?template=bulk_index&q[is_primary_producer_eq]=true").then (data) ->
$scope.suppliers = data
$scope.suppliers = $filter('orderBy')(data, 'name')
$scope.suppliers.unshift blankOption()
dataFetcher("/api/enterprises/accessible?template=bulk_index&q[is_distributor_eq]=true").then (data) ->
$scope.distributors = data
dataFetcher("/api/enterprises/accessible?template=bulk_index&q[sells_in][]=own&q[sells_in][]=any").then (data) ->
$scope.distributors = $filter('orderBy')(data, 'name')
$scope.distributors.unshift blankOption()
ocFetcher = dataFetcher("/api/order_cycles/accessible").then (data) ->
ocFetcher = dataFetcher("/api/order_cycles/accessible?as=distributor&q[orders_close_at_gt]=#{formatDate(daysFromToday(-90))}").then (data) ->
$scope.orderCycles = data
$scope.orderCyclesByID = []
$scope.orderCyclesByID[oc.id] = oc for oc in $scope.orderCycles
$scope.orderCycles.unshift blankOption()
$scope.fetchOrders()
ocFetcher.then ->
@@ -58,7 +61,7 @@ angular.module("ofn.admin").controller "AdminOrderMgmtCtrl", [
$scope.fetchOrders = ->
$scope.loading = true
dataFetcher("/api/orders/managed?template=bulk_index;page=1;per_page=500;q[completed_at_not_null]=true;q[completed_at_gt]=#{$scope.startDate};q[completed_at_lt]=#{$scope.endDate}").then (data) ->
dataFetcher("/admin/orders/managed?template=bulk_index;page=1;per_page=500;q[state_not_eq]=canceled;q[completed_at_not_null]=true;q[completed_at_gt]=#{$scope.startDate};q[completed_at_lt]=#{$scope.endDate}").then (data) ->
$scope.resetOrders data
$scope.loading = false
@@ -160,6 +163,25 @@ angular.module("ofn.admin").controller "AdminOrderMgmtCtrl", [
$scope.supplierFilter = $scope.suppliers[0].id
$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
$scope.endDate = $scope.orderCyclesByID[$scope.orderCycleFilter].last_order
]
daysFromToday = (days) ->

View File

@@ -1,4 +1,4 @@
angular.module("ofn.admin").controller "AdminProductEditCtrl", ($scope, $timeout, $http, BulkProducts, DisplayProperties, dataFetcher, DirtyProducts, VariantUnitManager, StatusMessage, producers, Taxons, SpreeApiAuth, Columns) ->
angular.module("ofn.admin").controller "AdminProductEditCtrl", ($scope, $timeout, $http, BulkProducts, DisplayProperties, dataFetcher, DirtyProducts, VariantUnitManager, StatusMessage, producers, Taxons, SpreeApiAuth, Columns, tax_categories) ->
$scope.loading = true
$scope.StatusMessage = StatusMessage
@@ -10,7 +10,9 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", ($scope, $timeout
unit: {name: "Unit", visible: true}
price: {name: "Price", visible: true}
on_hand: {name: "On Hand", visible: true}
on_demand: {name: "On Demand", visible: false}
category: {name: "Category", visible: false}
tax_category: {name: "Tax Category", visible: false}
inherits_properties: {name: "Inherits Properties?", visible: false}
available_on: {name: "Available On", visible: false}
@@ -32,6 +34,7 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", ($scope, $timeout
$scope.producers = producers
$scope.taxons = Taxons.taxons
$scope.tax_categories = tax_categories
$scope.filterProducers = [{id: "0", name: ""}].concat $scope.producers
$scope.filterTaxons = [{id: "0", name: ""}].concat $scope.taxons
$scope.producerFilter = "0"
@@ -106,6 +109,12 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", ($scope, $timeout
window.location = "/admin/products/" + product.permalink_live + ((if variant then "/variants/" + variant.id else "")) + "/edit"
$scope.toggleShowAllVariants = ->
showVariants = !DisplayProperties.showVariants 0
$scope.filteredProducts.forEach (product) ->
DisplayProperties.setShowVariants product.id, showVariants
DisplayProperties.setShowVariants 0, showVariants
$scope.addVariant = (product) ->
product.variants.push
id: $scope.nextVariantId()
@@ -137,15 +146,18 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", ($scope, $timeout
$scope.deleteVariant = (product, variant) ->
if !$scope.variantSaved(variant)
$scope.removeVariant(product, variant)
if product.variants.length > 1
if !$scope.variantSaved(variant)
$scope.removeVariant(product, variant)
else
if confirm("Are you sure?")
$http(
method: "DELETE"
url: "/api/products/" + product.permalink_live + "/variants/" + variant.id + "/soft_delete"
).success (data) ->
$scope.removeVariant(product, variant)
else
if confirm("Are you sure?")
$http(
method: "DELETE"
url: "/api/products/" + product.permalink_live + "/variants/" + variant.id + "/soft_delete"
).success (data) ->
$scope.removeVariant(product, variant)
alert("The last variant cannot be deleted!")
$scope.removeVariant = (product, variant) ->
product.variants.splice product.variants.indexOf(variant), 1
@@ -309,9 +321,15 @@ filterSubmitProducts = (productsToFilter) ->
if product.hasOwnProperty("on_hand") and filteredVariants.length == 0 #only update if no variants present
filteredProduct.on_hand = product.on_hand
hasUpdatableProperty = true
if product.hasOwnProperty("on_demand") and filteredVariants.length == 0 #only update if no variants present
filteredProduct.on_demand = product.on_demand
hasUpdatableProperty = true
if product.hasOwnProperty("category_id")
filteredProduct.primary_taxon_id = product.category_id
hasUpdatableProperty = true
if product.hasOwnProperty("tax_category_id")
filteredProduct.tax_category_id = product.tax_category_id
hasUpdatableProperty = true
if product.hasOwnProperty("inherits_properties")
filteredProduct.inherits_properties = product.inherits_properties
hasUpdatableProperty = true
@@ -337,6 +355,9 @@ filterSubmitVariant = (variant) ->
if variant.hasOwnProperty("on_hand")
filteredVariant.on_hand = variant.on_hand
hasUpdatableProperty = true
if variant.hasOwnProperty("on_demand")
filteredVariant.on_demand = variant.on_demand
hasUpdatableProperty = true
if variant.hasOwnProperty("price")
filteredVariant.price = variant.price
hasUpdatableProperty = true

View File

@@ -1,10 +1,8 @@
angular.module("ofn.admin").directive "ofnToggleVariants", (DisplayProperties) ->
link: (scope, element, attrs) ->
if DisplayProperties.showVariants scope.product.id
element.removeClass "icon-chevron-right"
element.addClass "icon-chevron-down"
else
element.removeClass "icon-chevron-down"
element.addClass "icon-chevron-right"
element.on "click", ->
@@ -16,4 +14,4 @@ angular.module("ofn.admin").directive "ofnToggleVariants", (DisplayProperties) -
else
DisplayProperties.setShowVariants scope.product.id, true
element.removeClass "icon-chevron-right"
element.addClass "icon-chevron-down"
element.addClass "icon-chevron-down"

View File

@@ -5,6 +5,13 @@ angular.module("admin.products")
$scope.placeholder_text = ""
$scope.$watchCollection '[product.variant_unit_with_scale, product.master.unit_value_with_description]', ->
$scope.processVariantUnitWithScale()
$scope.processUnitValueWithDescription()
$scope.placeholder_text = new OptionValueNamer($scope.product.master).name()
$scope.variant_unit_options = VariantUnitManager.variantUnitOptions()
$scope.processVariantUnitWithScale = ->
if $scope.product.variant_unit_with_scale
match = $scope.product.variant_unit_with_scale.match(/^([^_]+)_([\d\.]+)$/)
if match
@@ -16,18 +23,15 @@ angular.module("admin.products")
else
$scope.product.variant_unit = $scope.product.variant_unit_scale = null
$scope.processUnitValueWithDescription = ->
if $scope.product.master.hasOwnProperty("unit_value_with_description")
match = $scope.product.master.unit_value_with_description.match(/^([\d\.]+(?= |$)|)( |)(.*)$/)
match = $scope.product.master.unit_value_with_description.match(/^([\d\.]+(?= *|$)|)( *)(.*)$/)
if match
$scope.product.master.unit_value = parseFloat(match[1])
$scope.product.master.unit_value = null if isNaN($scope.product.master.unit_value)
$scope.product.master.unit_value *= $scope.product.variant_unit_scale if $scope.product.master.unit_value && $scope.product.variant_unit_scale
$scope.product.master.unit_description = match[3]
$scope.placeholder_text = new OptionValueNamer($scope.product.master).name()
$scope.variant_unit_options = VariantUnitManager.variantUnitOptions()
$scope.hasVariants = (product) ->
Object.keys(product.variants).length > 0

View File

@@ -19,7 +19,7 @@ angular.module("ofn.admin").factory "BulkProducts", (PagedFetcher, dataFetcher)
# when a respond_overrride for the clone action is used.
id = data.product.id
dataFetcher("/api/products/" + id + "?template=bulk_show").then (newProduct) =>
@addProducts [newProduct]
@insertProductAfter(product, newProduct)
updateVariantLists: (serverProducts, productsWithUnsavedVariants) ->
for product in productsWithUnsavedVariants
@@ -39,6 +39,10 @@ angular.module("ofn.admin").factory "BulkProducts", (PagedFetcher, dataFetcher)
@unpackProduct product
@products.push product
insertProductAfter: (product, newProduct) ->
index = @products.indexOf(product)
@products.splice(index + 1, 0, newProduct)
unpackProduct: (product) ->
#$scope.matchProducer product
@loadVariantUnit product

View File

@@ -3,12 +3,10 @@ angular.module("ofn.admin").factory "DisplayProperties", ->
displayProperties: {}
showVariants: (product_id) ->
@initProduct product_id
@displayProperties[product_id].showVariants
@productProperties(product_id).showVariants
setShowVariants: (product_id, showVariants) ->
@initProduct product_id
@displayProperties[product_id].showVariants = showVariants
@productProperties(product_id).showVariants = showVariants
initProduct: (product_id) ->
productProperties: (product_id) ->
@displayProperties[product_id] ||= {showVariants: false}

View File

@@ -1,9 +1,10 @@
Darkswarm.controller "GroupEnterprisesCtrl", ($scope, Search, FilterSelectorsService) ->
Darkswarm.controller "GroupEnterprisesCtrl", ($scope, Search, FilterSelectorsService, EnterpriseModal) ->
$scope.totalActive = FilterSelectorsService.totalActive
$scope.clearAll = FilterSelectorsService.clearAll
$scope.filterText = FilterSelectorsService.filterText
$scope.FilterSelectorsService = FilterSelectorsService
$scope.query = Search.search()
$scope.openModal = EnterpriseModal.open
$scope.activeTaxons = []
$scope.show_profiles = false
$scope.filtersActive = false

View File

@@ -1,8 +1,3 @@
Darkswarm.controller "GroupsCtrl", ($scope, Groups, $anchorScroll, $rootScope) ->
$scope.Groups = Groups
$scope.order = 'position'
#$rootScope.$on "$locationChangeSuccess", (newRoute, oldRoute) ->
#$anchorScroll()
#
#

View File

@@ -1,6 +1,6 @@
Darkswarm.controller "HubNodeCtrl", ($scope, HashNavigation, Navigation, $location, $templateCache, CurrentHub) ->
$scope.toggle = ->
HashNavigation.toggle $scope.hub.hash
$scope.toggle = (e) ->
HashNavigation.toggle $scope.hub.hash if !angular.element(e.target).inheritedData('is-link')
$scope.open = ->
HashNavigation.active $scope.hub.hash

View File

@@ -32,8 +32,9 @@ Darkswarm.factory 'Products', ($resource, Enterprises, Dereferencer, Taxons, Pro
if product.variants
product.variants = (Variants.register variant for variant in product.variants)
variant.product = product for variant in product.variants
product.master.product = product
product.master = Variants.register product.master if product.master
if product.master
product.master.product = product
product.master = Variants.register product.master
registerVariantsWithCart: ->
for product in @products

View File

@@ -249,3 +249,18 @@ span.required {
color: red;
font-size: 110%;
}
table td.actions {
.icon-trash, .icon-edit, icon-copy {
&.disabled {
border-color: #d0d0d0;
color: #c0c0c0;
background-color: #fafafa;
&:hover {
border-color: #a5a5a5;
color: #a5a5a5;
background-color: #fafafa;
}
}
}
}

View File

@@ -65,6 +65,24 @@
.active_table_row:nth-child(2)
padding-bottom: 0.75rem
.producers-list
li.more-producers-link
.less
display: none
a:hover
text-decoration: underline
li.additional-producer
display: none
&.show-more-producers
li.additional-producer
display: block
li.more-producers-link
.more
display: none
.less
display: block
//CURRENT hub (shows selected hub)
&.current
//overwrites active_table
@@ -83,6 +101,7 @@
.active_table_row:first-child .skinny-head
background-color: rgba(255,255,255,0.85)
//INACTIVE - closed hub
&.inactive
&.closed, &.open

View File

@@ -9,7 +9,7 @@ module Admin
def move_up
EnterpriseGroup.with_isolation_level_serializable do
@enterprise_group = EnterpriseGroup.find params[:enterprise_group_id]
@enterprise_group = EnterpriseGroup.find_by_permalink params[:enterprise_group_id]
@enterprise_group.move_higher
end
redirect_to main_app.admin_enterprise_groups_path
@@ -17,7 +17,7 @@ module Admin
def move_down
EnterpriseGroup.with_isolation_level_serializable do
@enterprise_group = EnterpriseGroup.find params[:enterprise_group_id]
@enterprise_group = EnterpriseGroup.find_by_permalink params[:enterprise_group_id]
@enterprise_group.move_lower
end
redirect_to main_app.admin_enterprise_groups_path
@@ -33,6 +33,12 @@ module Admin
end
alias_method_chain :build_resource, :address
# Overriding method on Spree's resource controller,
# so that resources are found using permalink
def find_resource
EnterpriseGroup.find_by_permalink(params[:id])
end
private
def load_data

View File

@@ -3,6 +3,7 @@ module Admin
before_filter :load_enterprise_set, :only => :index
before_filter :load_countries, :except => [:index, :set_sells, :check_permalink]
before_filter :load_methods_and_fees, :only => [:new, :edit, :update, :create]
before_filter :load_groups, :only => [:new, :edit, :update, :create]
before_filter :load_taxons, :only => [:new, :edit, :update, :create]
before_filter :check_can_change_sells, only: :update
before_filter :check_can_change_bulk_sells, only: :bulk_update
@@ -127,6 +128,10 @@ module Admin
@enterprise_fees = EnterpriseFee.managed_by(spree_current_user).for_enterprise(@enterprise).order(:fee_type, :name).all
end
def load_groups
@groups = EnterpriseGroup.managed_by(spree_current_user) | @enterprise.groups
end
def load_taxons
@taxons = Spree::Taxon.order(:name)
end

View File

@@ -13,7 +13,8 @@ module Api
end
def accessible
@enterprises = Enterprise.ransack(params[:q]).result.accessible_by(current_api_user)
permitted = OpenFoodNetwork::Permissions.new(current_api_user).order_cycle_enterprises
@enterprises = permitted.ransack(params[:q]).result
render params[:template] || :bulk_index
end

View File

@@ -9,7 +9,16 @@ module Api
end
def accessible
@order_cycles = OrderCycle.ransack(params[:q]).result.accessible_by(current_api_user)
@order_cycles = if params[:as] == "distributor"
OrderCycle.ransack(params[:q]).result.
involving_managed_distributors_of(current_api_user).order('updated_at DESC')
elsif params[:as] == "producer"
OrderCycle.ransack(params[:q]).result.
involving_managed_producers_of(current_api_user).order('updated_at DESC')
else
OrderCycle.ransack(params[:q]).result.accessible_by(current_api_user)
end
render params[:template] || :bulk_index
end
end

View File

@@ -12,9 +12,6 @@ class BaseController < ApplicationController
before_filter :check_order_cycle_expiry
def load_active_distributors
@active_distributors ||= Enterprise.distributors_with_active_order_cycles
end
private

View File

@@ -12,9 +12,6 @@ class CheckoutController < Spree::CheckoutController
include EnterprisesHelper
def edit
# Because this controller doesn't inherit from our BaseController
# We need to duplicate the code here
@active_distributors ||= Enterprise.distributors_with_active_order_cycles
end
def update

View File

@@ -4,7 +4,7 @@ class EnterprisesController < BaseController
include OrderCyclesHelper
# These prepended filters are in the reverse order of execution
prepend_before_filter :load_active_distributors, :set_order_cycles, :require_distributor_chosen, :reset_order, only: :shop
prepend_before_filter :set_order_cycles, :require_distributor_chosen, :reset_order, only: :shop
before_filter :clean_permalink, only: :check_permalink
respond_to :js, only: :permalink_checker

View File

@@ -1,12 +1,11 @@
class GroupsController < BaseController
layout 'darkswarm'
before_filter :load_active_distributors
def index
@groups = EnterpriseGroup.on_front_page.by_position
end
def show
@group = EnterpriseGroup.find params[:id]
@group = EnterpriseGroup.find_by_permalink(params[:id]) || EnterpriseGroup.find(params[:id])
end
end

View File

@@ -1,11 +1,9 @@
class HomeController < BaseController
layout 'darkswarm'
before_filter :load_active_distributors
def index
end
def about_us
end
end

View File

@@ -1,6 +1,5 @@
class MapController < BaseController
layout 'darkswarm'
before_filter :load_active_distributors
def index
end

View File

@@ -1,7 +1,6 @@
class ProducersController < BaseController
layout 'darkswarm'
before_filter :load_active_distributors
def index
end
end

View File

@@ -10,12 +10,16 @@ class ShopController < BaseController
end
def products
# Can we make this query less slow?
#
if @products = products_for_shop
render status: 200,
json: ActiveModel::ArraySerializer.new(@products, each_serializer: Api::ProductSerializer,
current_order_cycle: current_order_cycle, current_distributor: current_distributor).to_json
json: ActiveModel::ArraySerializer.new(@products,
each_serializer: Api::ProductSerializer,
current_order_cycle: current_order_cycle,
current_distributor: current_distributor,
variants: variants_for_shop_by_id,
master_variants: master_variants_for_shop_by_id).to_json
else
render json: "", status: 404
end
@@ -56,4 +60,30 @@ class ShopController < BaseController
"name ASC"
end
end
def all_variants_for_shop
# We use the in_stock? method here instead of the in_stock scope because we need to
# look up the stock as overridden by VariantOverrides, and the scope method is not affected
# by them.
Spree::Variant.
for_distribution(current_order_cycle, current_distributor).
each { |v| v.scope_to_hub current_distributor }.
select(&:in_stock?)
end
def variants_for_shop_by_id
index_by_product_id all_variants_for_shop.reject(&:is_master)
end
def master_variants_for_shop_by_id
index_by_product_id all_variants_for_shop.select(&:is_master)
end
def index_by_product_id(variants)
variants.inject({}) do |vs, v|
vs[v.product_id] ||= []
vs[v.product_id] << v
vs
end
end
end

View File

@@ -7,7 +7,7 @@ Spree::Admin::OrdersController.class_eval do
# We need to add expections for collection actions other than :index here
# because spree_auth_devise causes load_order to be called, which results
# in an auth failure as the @order object is nil for collection actions
before_filter :check_authorization, :except => :bulk_management
before_filter :check_authorization, except: [:bulk_management, :managed]
# After updating an order, the fees should be updated as well
# Currently, adding or deleting line items does not trigger updating the
@@ -17,12 +17,19 @@ Spree::Admin::OrdersController.class_eval do
after_filter :update_distribution_charge, :only => :update
respond_override :index => { :html =>
{ :success => lambda {
{ :success => lambda {
# Filter orders to only show those distributed by current user (or all for admin user)
@orders = @search.result.includes([:user, :shipments, :payments]).
distributed_by_user(spree_current_user).
page(params[:page]).
per(params[:per_page] || Spree::Config[:orders_per_page])
# Filter orders by distributor
if params[:distributor_ids]
@orders = @orders.where(distributor_id: params[:distributor_ids])
end
if params[:order_cycle_ids]
@orders = @orders.where(order_cycle_id: params[:order_cycle_ids])
end
} } }
# Overwrite to use confirm_email_for_customer instead of confirm_email.
@@ -37,4 +44,10 @@ Spree::Admin::OrdersController.class_eval do
def update_distribution_charge
@order.update_distribution_charge!
end
def managed
permissions = OpenFoodNetwork::Permissions.new(spree_current_user)
@orders = permissions.editable_orders.ransack(params[:q]).result.page(params[:page]).per(params[:per_page])
render json: @orders, each_serializer: Api::Admin::OrderSerializer
end
end

View File

@@ -406,27 +406,34 @@ Spree::Admin::ReportsController.class_eval do
end
params[:q][:meta_sort] ||= "completed_at.desc"
# -- Search
@search = Spree::Order.complete.not_state(:canceled).managed_by(spree_current_user).search(params[:q])
orders = @search.result
@line_items = orders.map do |o|
lis = o.line_items.managed_by(spree_current_user)
lis = lis.supplied_by_any(params[:supplier_id_in]) if params[:supplier_id_in].present?
lis
end.flatten
#payments = orders.map { |o| o.payments.select { |payment| payment.completed? } }.flatten # Only select completed payments
permissions = OpenFoodNetwork::Permissions.new(spree_current_user)
# -- Prepare form options
my_distributors = Enterprise.is_distributor.managed_by(spree_current_user)
my_suppliers = Enterprise.is_primary_producer.managed_by(spree_current_user)
# -- Search
@search = Spree::Order.complete.not_state(:canceled).search(params[:q])
orders = permissions.visible_orders.merge(@search.result)
@line_items = permissions.visible_line_items.merge(Spree::LineItem.where(order_id: orders))
@line_items = @line_items.supplied_by_any(params[:supplier_id_in]) if params[:supplier_id_in].present?
line_items_with_hidden_details = @line_items.where('"spree_line_items"."id" NOT IN (?)', permissions.editable_line_items)
@line_items.select{ |li| line_items_with_hidden_details.include? li }.each do |line_item|
# TODO We should really be hiding customer code here too, but until we
# have an actual association between order and customer, it's a bit tricky
line_item.order.bill_address.assign_attributes(firstname: "HIDDEN", lastname: "", phone: "", address1: "", address2: "", city: "", zipcode: "", state: nil)
line_item.order.ship_address.assign_attributes(firstname: "HIDDEN", lastname: "", phone: "", address1: "", address2: "", city: "", zipcode: "", state: nil)
line_item.order.assign_attributes(email: "HIDDEN")
end
# My distributors and any distributors distributing products I supply
@distributors = my_distributors | Enterprise.with_distributed_products_outer.merge(Spree::Product.in_any_supplier(my_suppliers))
@distributors = permissions.visible_enterprises_for_order_reports.is_distributor
# My suppliers and any suppliers supplying products I distribute
@suppliers = my_suppliers | my_distributors.map { |d| Spree::Product.in_distributor(d) }.flatten.map(&:supplier).uniq
@suppliers = permissions.visible_enterprises_for_order_reports.is_primary_producer
@order_cycles = OrderCycle.active_or_complete.
involving_managed_distributors_of(spree_current_user).order('orders_close_at DESC')
@order_cycles = OrderCycle.active_or_complete.accessible_by(spree_current_user).order('orders_close_at DESC')
@report_types = REPORT_TYPES[:orders_and_fulfillment]
@report_type = params[:report_type]

View File

@@ -4,7 +4,7 @@ Spree::Admin::VariantsController.class_eval do
def search
search_params = { :product_name_cont => params[:q], :sku_cont => params[:q] }
@variants = Spree::Variant.ransack(search_params.merge(:m => 'or')).result
@variants = Spree::Variant.where(is_master: false).ransack(search_params.merge(:m => 'or')).result
if params[:order_cycle_id].present?
order_cycle = OrderCycle.find params[:order_cycle_id]

View File

@@ -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

View File

@@ -4,12 +4,4 @@ Spree::Api::OrdersController.class_eval do
# because Spree's API controller causes authorize_read! to be called, which
# results in an ActiveRecord::NotFound Exception as the order object is not
# defined for collection actions
before_filter :authorize_read!, :except => [:managed]
def managed
authorize! :admin, Spree::Order
authorize! :read, Spree::Order
@orders = Spree::Order.ransack(params[:q]).result.distributed_by_user(current_api_user).page(params[:page]).per(params[:per_page])
respond_with(@orders, default_template: :index)
end
end

View File

@@ -11,7 +11,7 @@ Spree::Api::ProductsController.class_eval do
# TODO: This should be named 'managed'. Is the action above used? Maybe we should remove it.
def bulk_products
@products = OpenFoodNetwork::Permissions.new(current_api_user).managed_products.
@products = OpenFoodNetwork::Permissions.new(current_api_user).editable_products.
merge(product_scope).
order('created_at DESC').
ransack(params[:q]).result.

View File

@@ -54,6 +54,10 @@ module Admin
admin_inject_json_ams_array "ofn.admin", "products", @products, Api::Admin::ProductSerializer
end
def admin_inject_tax_categories
admin_inject_json_ams_array "ofn.admin", "tax_categories", @tax_categories, Api::Admin::TaxCategorySerializer
end
def admin_inject_taxons
admin_inject_json_ams_array "admin.taxons", "taxons", @taxons, Api::Admin::TaxonSerializer
end

View File

@@ -1,6 +1,16 @@
require 'open_food_network/enterprise_injection_data'
module InjectionHelper
def inject_enterprises
inject_json_ams "enterprises", Enterprise.activated.all, Api::EnterpriseSerializer, active_distributors: @active_distributors
inject_json_ams "enterprises", Enterprise.activated.includes(:address).all, Api::EnterpriseSerializer, enterprise_injection_data
end
def inject_group_enterprises
inject_json_ams "group_enterprises", @group.enterprises, Api::EnterpriseSerializer, enterprise_injection_data
end
def inject_current_hub
inject_json_ams "currentHub", current_distributor, Api::EnterpriseSerializer, enterprise_injection_data
end
def inject_current_order
@@ -53,4 +63,13 @@ module InjectionHelper
end
render partial: "json/injection_ams", locals: {name: name, json: json}
end
private
def enterprise_injection_data
@enterprise_injection_data ||= OpenFoodNetwork::EnterpriseInjectionData.new
{data: @enterprise_injection_data}
end
end

View File

@@ -163,17 +163,6 @@ class Enterprise < ActiveRecord::Base
end
}
# Return enterprises that participate in order cycles that user coordinates, sends to or receives from
scope :accessible_by, lambda { |user|
if user.has_spree_role?('admin')
scoped
else
with_order_cycles_outer.
where('order_cycles.id IN (?)', OrderCycle.accessible_by(user)).
select('DISTINCT enterprises.*')
end
}
def self.find_near(suburb)
enterprises = []
@@ -190,6 +179,10 @@ class Enterprise < ActiveRecord::Base
count(distinct: true)
end
def activated?
confirmed_at.present? && sells != 'unspecified'
end
def set_producer_property(property_name, property_value)
transaction do
property = Spree::Property.where(name: property_name).first_or_create!(presentation: property_name)
@@ -311,7 +304,7 @@ class Enterprise < ActiveRecord::Base
test_permalink = test_permalink.parameterize
test_permalink = "my-enterprise" if test_permalink.blank?
existing = Enterprise.select(:permalink).order(:permalink).where("permalink LIKE ?", "#{test_permalink}%").map(&:permalink)
if existing.empty?
unless existing.include?(test_permalink)
test_permalink
else
used_indices = existing.map do |p|

View File

@@ -16,8 +16,12 @@ class EnterpriseGroup < ActiveRecord::Base
validates :name, presence: true
validates :description, presence: true
before_validation :sanitize_permalink
validates :permalink, uniqueness: true, presence: true
attr_accessible :name, :description, :long_description, :on_front_page, :enterprise_ids
attr_accessible :owner_id
attr_accessible :permalink
attr_accessible :logo, :promo_image
attr_accessible :address_attributes
attr_accessible :email, :website, :facebook, :instagram, :linkedin, :twitter
@@ -71,4 +75,31 @@ class EnterpriseGroup < ActiveRecord::Base
address.zipcode.sub!(/^undefined$/, '')
end
def to_param
permalink
end
private
def self.find_available_value(existing, requested)
return requested unless existing.include?(requested)
used_indices = existing.map do |p|
p.slice!(/^#{requested}/)
p.match(/^\d+$/).to_s.to_i
end
options = (1..used_indices.length + 1).to_a - used_indices
requested + options.first.to_s
end
def find_available_permalink(requested)
existing = self.class.where(id: !id).where("permalink LIKE ?", "#{requested}%").pluck(:permalink)
self.class.find_available_value(existing, requested)
end
def sanitize_permalink
if permalink.blank? || permalink_changed?
requested = permalink.presence || permalink_was.presence || name.presence || 'group'
self.permalink = find_available_permalink(requested.parameterize)
end
end
end

View File

@@ -25,6 +25,32 @@ class EnterpriseRelationship < ActiveRecord::Base
scope :by_name, with_enterprises.order('child_enterprises.name, parent_enterprises.name')
# Load an array of the relatives of each enterprise (ie. any enterprise related to it in
# either direction). This array is split into distributors and producers, and has the format:
# {enterprise_id => {distributors: [id, ...], producers: [id, ...]} }
def self.relatives(activated_only=false)
relationships = EnterpriseRelationship.includes(:child, :parent)
relatives = {}
relationships.each do |r|
relatives[r.parent_id] ||= {distributors: Set.new, producers: Set.new}
relatives[r.child_id] ||= {distributors: Set.new, producers: Set.new}
if !activated_only || r.child.activated?
relatives[r.parent_id][:producers] << r.child_id if r.child.is_primary_producer
relatives[r.parent_id][:distributors] << r.child_id if r.child.is_distributor
end
if !activated_only || r.parent.activated?
relatives[r.child_id][:producers] << r.parent_id if r.parent.is_primary_producer
relatives[r.child_id][:distributors] << r.parent_id if r.parent.is_distributor
end
end
relatives
end
def permissions_list=(perms)
perms.andand.each { |name| permissions.build name: name }
end

View File

@@ -26,7 +26,7 @@ class OrderCycle < ActiveRecord::Base
closed.
where("order_cycles.orders_close_at >= ?", 31.days.ago).
order("order_cycles.orders_close_at DESC") }
scope :soonest_opening, lambda { upcoming.order('order_cycles.orders_open_at ASC') }
scope :distributing_product, lambda { |product|
@@ -64,6 +64,25 @@ class OrderCycle < ActiveRecord::Base
joins('LEFT OUTER JOIN enterprises ON (enterprises.id = exchanges.sender_id OR enterprises.id = exchanges.receiver_id)')
}
scope :involving_managed_distributors_of, lambda { |user|
enterprises = Enterprise.managed_by(user)
# Order cycles where I managed an enterprise at either end of an outgoing exchange
# ie. coordinator or distibutor
joins(:exchanges).merge(Exchange.outgoing).
where('exchanges.receiver_id IN (?) OR exchanges.sender_id IN (?)', enterprises, enterprises).
select('DISTINCT order_cycles.*')
}
scope :involving_managed_producers_of, lambda { |user|
enterprises = Enterprise.managed_by(user)
# Order cycles where I managed an enterprise at either end of an incoming exchange
# ie. coordinator or producer
joins(:exchanges).merge(Exchange.incoming).
where('exchanges.receiver_id IN (?) OR exchanges.sender_id IN (?)', enterprises, enterprises).
select('DISTINCT order_cycles.*')
}
def self.first_opening_for(distributor)
with_distributor(distributor).soonest_opening.first
@@ -73,11 +92,25 @@ class OrderCycle < ActiveRecord::Base
with_distributor(distributor).soonest_closing.first
end
def self.most_recently_closed_for(distributor)
with_distributor(distributor).most_recently_closed.first
end
# Find the earliest closing times for each distributor in an active order cycle, and return
# them in the format {distributor_id => closing_time, ...}
def self.earliest_closing_times
Hash[
Exchange.
outgoing.
joins(:order_cycle).
merge(OrderCycle.active).
group('exchanges.receiver_id').
select('exchanges.receiver_id AS receiver_id, MIN(order_cycles.orders_close_at) AS earliest_close_at').
map { |ex| [ex.receiver_id, ex.earliest_close_at.to_time] }
]
end
def clone!
oc = self.dup
oc.name = "COPY OF #{oc.name}"

View File

@@ -120,7 +120,7 @@ class AbilityDecorator
can [:admin, :index, :read, :create, :edit], Spree::Classification
# Reports page
can [:admin, :index, :customers, :orders_and_distributors, :group_buys, :bulk_coop, :payments, :orders_and_fulfillment, :products_and_inventory], :report
can [:admin, :index, :customers, :orders_and_distributors, :group_buys, :bulk_coop, :payments, :orders_and_fulfillment, :products_and_inventory, :order_cycle_management], :report
end
def add_order_cycle_management_abilities(user)
@@ -142,7 +142,7 @@ class AbilityDecorator
# during the order creation process from the admin backend
order.distributor.nil? || user.enterprises.include?(order.distributor)
end
can [:admin, :bulk_management], Spree::Order if user.admin? || user.enterprises.any?(&:is_distributor)
can [:admin, :bulk_management, :managed], Spree::Order if user.admin? || user.enterprises.any?(&:is_distributor)
can [:admin, :create], Spree::LineItem
can [:destroy], Spree::LineItem do |item|
user.admin? || user.enterprises.include?(order.distributor) || user == order.order_cycle.manager
@@ -154,7 +154,6 @@ class AbilityDecorator
can [:admin, :index, :read, :create, :edit, :update, :fire], Spree::ReturnAuthorization
can [:destroy], Spree::Adjustment do |adjustment|
# Sharing code with destroying a line item. This should be unified and probably applied for other actions as well.
binding.pry
if user.admin?
true
elsif adjustment.adjustable.instance_of? Spree::Order

View File

@@ -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|

View File

@@ -130,6 +130,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

View File

@@ -23,17 +23,18 @@ Spree::Product.class_eval do
attr_accessible :variant_unit, :variant_unit_scale, :variant_unit_name, :unit_value
attr_accessible :inherits_properties, :sku
validates_associated :master, message: "^Price and On Hand must be valid"
# validates_presence_of :variants, unless: :new_record?, message: "Product must have at least one variant"
validates_presence_of :supplier
validates :primary_taxon, presence: { message: "^Product Category can't be blank" }
validates :tax_category_id, presence: { message: "^Tax Category can't be blank" }, if: "Spree::Config.products_require_tax_category"
validates_presence_of :variant_unit, if: :has_variants?
validates_presence_of :variant_unit
validates_presence_of :variant_unit_scale,
if: -> p { %w(weight volume).include? p.variant_unit }
validates_presence_of :variant_unit_name,
if: -> p { p.variant_unit == 'items' }
after_save :ensure_standard_variant
after_initialize :set_available_on_to_now, :if => :new_record?
after_save :update_units
after_touch :touch_distributors
@@ -108,6 +109,12 @@ Spree::Product.class_eval do
# -- Methods
# Called by Spree::Product::duplicate before saving.
def duplicate_extra(parent)
# Spree sets the SKU to "COPY OF #{parent sku}".
self.master.sku = ''
end
def properties_including_inherited
# Product properties override producer properties
ps = product_properties.all
@@ -209,4 +216,31 @@ Spree::Product.class_eval do
Spree::OptionType.where('name LIKE ?', 'unit_%%')
end
def ensure_standard_variant
if master.valid? && variants.empty?
variant = self.master.dup
variant.product = self
variant.is_master = false
self.variants << variant
end
end
# Override Spree's old save_master method and replace it with the most recent method from spree repository
# This fixes any problems arising from failing master saves, without the need for a validates_associated on
# master, while giving us more specific errors as to why saving failed
def save_master
begin
if master && (master.changed? || master.new_record? || (master.default_price && (master.default_price.changed? || master.default_price.new_record?)))
master.save!
end
# If the master cannot be saved, the Product object will get its errors
# and will be destroyed
rescue ActiveRecord::RecordInvalid
master.errors.each do |att, error|
self.errors.add(att, error)
end
raise
end
end
end

View File

@@ -14,7 +14,7 @@ class Spree::ProductSet < ModelSet
if e.nil?
@klass.new(attributes).save unless @reject_if.andand.call(attributes)
else
e.update_attributes(attributes.except(:id, :variants_attributes, :master_attributes)) and
( attributes.except(:id, :variants_attributes, :master_attributes).present? ? e.update_attributes(attributes.except(:id, :variants_attributes, :master_attributes)) : true) and
(attributes[:variants_attributes] ? update_variants_attributes(e, attributes[:variants_attributes]) : true ) and
(attributes[:master_attributes] ? update_variant(e, attributes[:master_attributes]) : true )
end

View File

@@ -25,6 +25,22 @@ Spree::ShippingMethod.class_eval do
scope :by_name, order('spree_shipping_methods.name ASC')
# Return the services (pickup, delivery) that different distributors provide, in the format:
# {distributor_id => {pickup: true, delivery: false}, ...}
def self.services
Hash[
Spree::ShippingMethod.
joins(:distributor_shipping_methods).
group('distributor_id').
select("distributor_id").
select("BOOL_OR(spree_shipping_methods.require_ship_address = 'f') AS pickup").
select("BOOL_OR(spree_shipping_methods.require_ship_address = 't') AS delivery").
map { |sm| [sm.distributor_id.to_i, {pickup: sm.pickup == 't', delivery: sm.delivery == 't'}] }
]
end
def available_to_order_with_distributor_check?(order, display_on=nil)
available_to_order_without_distributor_check?(order, display_on) &&
self.distributors.include?(order.distributor)

View File

@@ -9,4 +9,40 @@ Spree::Taxon.class_eval do
#fs << Spree::ProductFilters.distributor_filter if Spree::ProductFilters.respond_to? :distributor_filter
fs
end
# Find all the taxons of supplied products for each enterprise, indexed by enterprise.
# Format: {enterprise_id => [taxon_id, ...]}
def self.supplied_taxons
taxons = {}
Spree::Taxon.
joins(:products => :supplier).
select('spree_taxons.*, enterprises.id AS enterprise_id').
each do |t|
taxons[t.enterprise_id.to_i] ||= Set.new
taxons[t.enterprise_id.to_i] << t.id
end
taxons
end
# Find all the taxons of distributed products for each enterprise, indexed by enterprise.
# Format: {enterprise_id => [taxon_id, ...]}
def self.distributed_taxons
taxons = {}
Spree::Taxon.
joins(:products).
merge(Spree::Product.with_order_cycles_outer).
where('o_exchanges.incoming = ?', false).
select('spree_taxons.*, o_exchanges.receiver_id AS enterprise_id').
each do |t|
taxons[t.enterprise_id.to_i] ||= Set.new
taxons[t.enterprise_id.to_i] << t.id
end
taxons
end
end

View File

@@ -1,5 +1,9 @@
Spree.user_class.class_eval do
handle_asynchronously :send_reset_password_instructions
if method_defined? :send_reset_password_instructions_with_delay
Bugsnag.notify RuntimeError.new "send_reset_password_instructions already handled asyncronously - double-calling results in infinite job loop"
else
handle_asynchronously :send_reset_password_instructions
end
has_many :enterprise_roles, :dependent => :destroy
has_many :enterprises, through: :enterprise_roles

View File

@@ -7,17 +7,16 @@ Spree::Variant.class_eval do
has_many :exchange_variants, dependent: :destroy
has_many :exchanges, through: :exchange_variants
has_many :variant_overrides
attr_accessible :unit_value, :unit_description, :images_attributes, :display_as, :display_name
accepts_nested_attributes_for :images
validates_presence_of :unit_value,
if: -> v { %w(weight volume).include? v.product.andand.variant_unit },
unless: :is_master
if: -> v { %w(weight volume).include? v.product.andand.variant_unit }
validates_presence_of :unit_description,
if: -> v { v.product.andand.variant_unit.present? && v.unit_value.nil? },
unless: :is_master
if: -> v { v.product.andand.variant_unit.present? && v.unit_value.nil? }
before_validation :update_weight_from_unit_value, if: -> v { v.product.present? }
after_save :update_units
@@ -110,9 +109,15 @@ Spree::Variant.class_eval do
end
def delete
transaction do
self.update_column(:deleted_at, Time.now)
ExchangeVariant.where(variant_id: self).destroy_all
if product.variants == [self] # Only variant left on product
errors.add :product, "must have at least one variant"
false
else
transaction do
self.update_column(:deleted_at, Time.now)
ExchangeVariant.where(variant_id: self).destroy_all
self
end
end
end

View File

@@ -0,0 +1,13 @@
/ insert_before "div.clearfix"
.field-block.alpha.eight.columns
= label_tag nil, t(:distributors)
= select_tag(:distributor_ids,
options_for_select(Enterprise.is_distributor.managed_by(spree_current_user).map {|e| [e.name, e.id]}, params[:distributor_ids]),
{class: "select2 fullwidth", multiple: true})
.field-block.alpha.eight.columns
= label_tag nil, t(:order_cycles)
= select_tag(:order_cycle_ids,
options_for_select(OrderCycle.managed_by(spree_current_user).map {|oc| [oc.name, oc.id]}, params[:order_cycle_ids]),
{class: "select2 fullwidth", multiple: true})

View File

@@ -38,20 +38,26 @@
.twelve.columns.alpha
.six.columns.alpha
= render 'spree/admin/products/primary_taxon_form', f: f
.three.columns
.two.columns
= f.field_container :price do
= f.label :price, t(:price)
%span.required *
%br/
= f.text_field :price, class: 'fullwidth'
= f.error_message_on :price
.three.columns.omega
.two.columns
= f.field_container :on_hand do
= f.label :on_hand, t(:on_hand)
%span.required *
%br/
= f.text_field :on_hand, class: 'fullwidth'
= f.error_message_on :on_hand
.two.columns.omega
= f.field_container :on_demand do
= f.label :on_demand, t(:on_demand)
%br/
= f.check_box :on_demand
= f.error_message_on :on_demand
.twelve.columns.alpha
.six.columns.alpha &nbsp;
.three.columns

View File

@@ -0,0 +1,14 @@
class Api::Admin::BasicOrderCycleSerializer < ActiveModel::Serializer
attributes :id, :name, :first_order, :last_order
has_many :suppliers, serializer: Api::Admin::IdNameSerializer
has_many :distributors, serializer: Api::Admin::IdNameSerializer
def first_order
object.orders_open_at.strftime("%F")
end
def last_order
(object.orders_close_at + 1.day).strftime("%F")
end
end

View File

@@ -0,0 +1,19 @@
class Api::Admin::LineItemSerializer < ActiveModel::Serializer
attributes :id, :quantity, :max_quantity, :supplier, :price, :unit_value, :units_product, :units_variant
def supplier
Api::Admin::IdNameSerializer.new(object.product.supplier).serializable_hash
end
def units_product
Api::Admin::UnitsProductSerializer.new(object.product).serializable_hash
end
def units_variant
Api::Admin::UnitsVariantSerializer.new(object.variant).serializable_hash
end
def unit_value
object.unit_value.to_f
end
end

View File

@@ -0,0 +1,31 @@
class Api::Admin::OrderSerializer < ActiveModel::Serializer
attributes :id, :number, :full_name, :email, :phone, :completed_at, :line_items
has_one :distributor, serializer: Api::Admin::IdNameSerializer
has_one :order_cycle, serializer: Api::Admin::BasicOrderCycleSerializer
def full_name
object.billing_address.nil? ? "" : ( object.billing_address.full_name || "" )
end
def email
object.email || ""
end
def phone
object.billing_address.nil? ? "a" : ( object.billing_address.phone || "" )
end
def completed_at
object.completed_at.blank? ? "" : object.completed_at.strftime("%F %T")
end
def line_items
# we used to have a scope here, but we are at the point where a user which can edit an order
# should be able to edit all of the line_items as well, making the scope redundant
ActiveModel::ArraySerializer.new(
object.line_items.order('id ASC'),
{each_serializer: Api::Admin::LineItemSerializer}
)
end
end

View File

@@ -1,7 +1,7 @@
class Api::Admin::ProductSerializer < ActiveModel::Serializer
attributes :id, :name, :sku, :variant_unit, :variant_unit_scale, :variant_unit_name, :on_demand, :inherits_properties
attributes :on_hand, :price, :available_on, :permalink_live
attributes :on_hand, :price, :available_on, :permalink_live, :tax_category_id
has_one :supplier, key: :producer_id, embed: :id
has_one :primary_taxon, key: :category_id, embed: :id

View File

@@ -0,0 +1,3 @@
class Api::Admin::TaxCategorySerializer < ActiveModel::Serializer
attributes :id, :name
end

View File

@@ -0,0 +1,3 @@
class Api::Admin::UnitsProductSerializer < ActiveModel::Serializer
attributes :id, :name, :group_buy_unit_size, :variant_unit
end

View File

@@ -0,0 +1,8 @@
class Api::Admin::UnitsVariantSerializer < ActiveModel::Serializer
attributes :id, :full_name, :unit_value
def full_name
full_name = object.full_name
object.product.name + (full_name.empty? ? "" : ": #{full_name}")
end
end

View File

@@ -1,6 +1,7 @@
class Api::Admin::VariantSerializer < ActiveModel::Serializer
attributes :id, :options_text, :unit_value, :unit_description, :unit_to_display, :on_demand, :display_as, :display_name, :name_to_display
attributes :on_hand, :price
has_many :variant_overrides
def on_hand
object.on_hand.nil? ? 0 : ( object.on_hand.to_f.finite? ? object.on_hand : "On demand" )

View File

@@ -1,4 +1,7 @@
class Api::EnterpriseSerializer < ActiveModel::Serializer
# We reference this here because otherwise the serializer complains about its absence
Api::IdSerializer
def serializable_hash
cached_serializer_hash.merge uncached_serializer_hash
end
@@ -6,11 +9,11 @@ class Api::EnterpriseSerializer < ActiveModel::Serializer
private
def cached_serializer_hash
Api::CachedEnterpriseSerializer.new(object, @options).serializable_hash
Api::CachedEnterpriseSerializer.new(object, @options).serializable_hash || {}
end
def uncached_serializer_hash
Api::UncachedEnterpriseSerializer.new(object, @options).serializable_hash
Api::UncachedEnterpriseSerializer.new(object, @options).serializable_hash || {}
end
end
@@ -18,19 +21,22 @@ class Api::UncachedEnterpriseSerializer < ActiveModel::Serializer
attributes :orders_close_at, :active
def orders_close_at
OrderCycle.first_closing_for(object).andand.orders_close_at
options[:data].earliest_closing_times[object.id]
end
def active
@options[:active_distributors].andand.include? object
options[:data].active_distributors.andand.include? object
end
end
class Api::CachedEnterpriseSerializer < ActiveModel::Serializer
cached
delegate :cache_key, to: :object
#delegate :cache_key, to: :object
def cache_key
object.andand.cache_key
end
attributes :name, :id, :description, :latitude, :longitude,
:long_description, :website, :instagram, :linkedin, :twitter,
@@ -38,17 +44,27 @@ class Api::CachedEnterpriseSerializer < ActiveModel::Serializer
:email, :hash, :logo, :promo_image, :path, :pickup, :delivery,
:icon, :icon_font, :producer_icon_font, :category, :producers, :hubs
has_many :distributed_taxons, key: :taxons, serializer: Api::IdSerializer
has_many :supplied_taxons, serializer: Api::IdSerializer
attributes :taxons, :supplied_taxons
has_one :address, serializer: Api::AddressSerializer
def taxons
ids_to_objs options[:data].distributed_taxons[object.id]
end
def supplied_taxons
ids_to_objs options[:data].supplied_taxons[object.id]
end
def pickup
object.shipping_methods.where(:require_ship_address => false).present?
services = options[:data].shipping_method_services[object.id]
services ? services[:pickup] : false
end
def delivery
object.shipping_methods.where(:require_ship_address => true).present?
services = options[:data].shipping_method_services[object.id]
services ? services[:delivery] : false
end
def email
@@ -72,11 +88,13 @@ class Api::CachedEnterpriseSerializer < ActiveModel::Serializer
end
def producers
ActiveModel::ArraySerializer.new(object.suppliers.activated, {each_serializer: Api::IdSerializer})
relatives = options[:data].relatives[object.id]
relatives ? ids_to_objs(relatives[:producers]) : []
end
def hubs
ActiveModel::ArraySerializer.new(object.distributors.activated, {each_serializer: Api::IdSerializer})
relatives = options[:data].relatives[object.id]
relatives ? ids_to_objs(relatives[:distributors]) : []
end
# Map svg icons.
@@ -116,4 +134,11 @@ class Api::CachedEnterpriseSerializer < ActiveModel::Serializer
}
icon_fonts[object.category]
end
private
def ids_to_objs(ids)
ids.andand.map { |id| {id: id} }
end
end

View File

@@ -30,8 +30,9 @@ class Api::CachedProductSerializer < ActiveModel::Serializer
#cached
#delegate :cache_key, to: :object
attributes :id, :name, :permalink, :count_on_hand, :on_demand, :group_buy,
:notes, :description, :properties_with_values
attributes :id, :name, :permalink, :count_on_hand
attributes :on_demand, :group_buy, :notes, :description
attributes :properties_with_values
has_many :variants, serializer: Api::VariantSerializer
has_many :taxons, serializer: Api::IdSerializer
@@ -46,13 +47,11 @@ class Api::CachedProductSerializer < ActiveModel::Serializer
end
def variants
# We use the in_stock? method here instead of the in_stock scope because we need to
# look up the stock as overridden by VariantOverrides, and the scope method is not affected
# by them.
object.variants.
for_distribution(options[:current_order_cycle], options[:current_distributor]).
each { |v| v.scope_to_hub options[:current_distributor] }.
select(&:in_stock?)
options[:variants][object.id] || []
end
def master
options[:master_variants][object.id].andand.first
end
end

View File

@@ -19,3 +19,8 @@
= f.label :enterprise_ids, 'Enterprises'
%br/
= f.collection_select :enterprise_ids, @enterprises, :id, :name, {}, {class: "select2 fullwidth", multiple: true}
= f.field_container :permalink do
= f.label :permalink, "Permalink (unique, no spaces)"
%br/
= f.text_field :permalink

View File

@@ -5,15 +5,15 @@
%span.required *
.eight.columns.omega
= f.text_field :name, { placeholder: "eg. Professor Plum's Biodynamic Truffles" }
.row
.alpha.eleven.columns
.three.columns.alpha
= f.label :group_ids, 'Groups'
.with-tip{'data-powertip' => "Select any groups or regions that you are a member of. This will help customers find your enterprise."}
%a What's this?
.eight.columns.omega
= f.collection_select :group_ids, EnterpriseGroup.all, :id, :name, {}, class: "select2 fullwidth", multiple: true, placeholder: "Start typing to search available groups..."
- if @groups.present?
.row
.alpha.eleven.columns
.three.columns.alpha
= f.label :group_ids, 'Groups'
.with-tip{'data-powertip' => "Select any groups or regions that you are a member of. This will help customers find your enterprise."}
%a What's this?
.eight.columns.omega
= f.collection_select :group_ids, @groups, :id, :name, {}, class: "select2 fullwidth", multiple: true, placeholder: "Start typing to search available groups..."
.row
.three.columns.alpha

View File

@@ -1,9 +1,11 @@
object @order_cycle
attributes :id, :name
node( :first_order ) { |order| order.orders_open_at.strftime("%F") }
node( :last_order ) { |order| (order.orders_close_at + 1.day).strftime("%F") }
node( :suppliers ) do |oc|
partial 'api/enterprises/bulk_index', :object => oc.suppliers
end
node( :distributors ) do |oc|
partial 'api/enterprises/bulk_index', :object => oc.distributors
end
end

View File

@@ -3,7 +3,7 @@
%shop.darkswarm
- content_for :order_cycle_form do
%div{"ng-controller" => "OrderCycleChangeCtrl"}
%div{"ng-controller" => "OrderCycleChangeCtrl", "ng-cloak" => true}
%closing{"ng-if" => "OrderCycle.selected()"}
Next order closing
%strong {{ OrderCycle.orders_close_at() | date_in_words }}

View File

@@ -20,7 +20,7 @@
.row.pad-top{bindonce: true}
.small-12.medium-6.columns
.groups-header
%a{"bo-href-i" => "/groups/{{group.id}}"}
%a{"bo-href-i" => "/groups/{{group.permalink}}"}
%i.ofn-i_035-groups
%span.group-name{"bo-text" => "group.name"}
.small-3.medium-2.columns

View File

@@ -3,8 +3,8 @@
= inject_enterprises
-# inject enterprises in this group
-# further hubs and producers of these enterprises can't be resoleved within this small subset
= inject_json_ams "group_enterprises", @group.enterprises, Api::EnterpriseSerializer, active_distributors: @active_distributors
-# further hubs and producers of these enterprises can't be resolved within this small subset
= inject_group_enterprises
#group-page.row.pad-top{"ng-controller" => "GroupPageCtrl"}
.small-12.columns.pad-top
@@ -95,7 +95,7 @@
= render partial: 'home/fat'
= render partial: 'shared/components/enterprise_no_results'
.small-12.medium-12.large-3.columns
= render partial: 'contact'

View File

@@ -1,4 +1,4 @@
.row.active_table_row{"ng-show" => "open()", "ng-click" => "toggle()", "ng-class" => "{'open' : !ofn-i_032-closed-sign()}"}
.row.active_table_row{"ng-show" => "open()", "ng-click" => "toggle($event)", "ng-class" => "{'open' : !ofn-i_032-closed-sign()}"}
.columns.small-12.medium-6.large-5.fat
%div{"bo-if" => "hub.taxons"}
%label Shop for
@@ -21,8 +21,20 @@
.columns.small-12.medium-3.large-5.fat
%div{"bo-if" => "hub.producers"}
%label Our producers
%ul.small-block-grid-2.medium-block-grid-1.large-block-grid-2
%li{"ng-repeat" => "enterprise in hub.producers"}
%ul.small-block-grid-2.medium-block-grid-1.large-block-grid-2{"ng-class" => "{'show-more-producers' : toggleMoreProducers}", "class" => "producers-list"}
%li{"ng-repeat" => "enterprise in hub.producers | limitTo:7"}
%enterprise-modal
%i.ofn-i_036-producers
%span{"bo-text" => "enterprise.name"}
%li{"data-is-link" => "true", "class" => "more-producers-link", "bo-show" => "hub.producers.length>7"}
%a{"ng-click" => "toggleMoreProducers=!toggleMoreProducers"}
.more
+
%span{"bo-text" => "hub.producers.length-7"}
More
.less
Show less
%li{"ng-repeat" => "enterprise in hub.producers.slice(7,hub.producers.length)", "class" => "additional-producer"}
%enterprise-modal
%i.ofn-i_036-producers
%span{"bo-text" => "enterprise.name"}

View File

@@ -1,4 +1,4 @@
.row.active_table_row{"ng-if" => "hub.is_distributor", "ng-click" => "toggle()", "ng-class" => "{'closed' : !open(), 'is_distributor' : producer.is_distributor}", bindonce: true}
.row.active_table_row{"ng-if" => "hub.is_distributor", "ng-click" => "toggle($event)", "ng-class" => "{'closed' : !open(), 'is_distributor' : producer.is_distributor}", bindonce: true}
.columns.small-12.medium-6.large-5.skinny-head
%a.hub{"bo-href" => "hub.path", "ng-class" => "{primary: hub.active, secondary: !hub.active}", "ofn-empties-cart" => "hub"}

View File

@@ -1,6 +0,0 @@
object current_distributor
extends 'json/partials/enterprise'
child suppliers: :producers do
attributes :id
end

View File

@@ -1,5 +1,5 @@
collection @groups
attributes :id, :name, :position, :description, :long_description, :email, :website, :facebook, :instagram, :linkedin, :twitter
attributes :id, :permalink, :name, :position, :description, :long_description, :email, :website, :facebook, :instagram, :linkedin, :twitter
child enterprises: :enterprises do
attributes :id

View File

@@ -24,7 +24,7 @@
= render partial: "shared/ie_warning"
= javascript_include_tag "iehack"
= inject_json "currentHub", "current_hub"
= inject_current_hub
= inject_json "user", "current_user"
= inject_json "railsFlash", "flash"
= inject_taxons
@@ -41,3 +41,4 @@
#footer
%loading
= render 'shared/analytics'

View File

@@ -1,4 +1,4 @@
.row.active_table_row{"ng-if" => "open()", "ng-click" => "toggle()", "ng-class" => "{'open' : !ofn-i_032-closed-sign()}"}
.row.active_table_row{"ng-if" => "open()", "ng-click" => "toggle($event)", "ng-class" => "{'open' : !ofn-i_032-closed-sign()}"}
.columns.small-12.medium-7.large-7.fat
/ Will add in long description available once clean up HTML formatting producer.long_description

View File

@@ -1,4 +1,4 @@
.row.active_table_row{"ng-click" => "toggle()", "ng-class" => "{'closed' : !open(), 'is_distributor' : producer.is_distributor}"}
.row.active_table_row{"ng-click" => "toggle($event)", "ng-class" => "{'closed' : !open(), 'is_distributor' : producer.is_distributor}"}
.columns.small-12.medium-4.large-4.skinny-head
%span{"bo-if" => "producer.is_distributor" }
%a.is_distributor{"bo-href" => "producer.path" }

View File

@@ -0,0 +1,8 @@
:javascript
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
ga('create', 'UA-62912229-1', 'auto');
ga('send', 'pageview');

View File

@@ -2,7 +2,7 @@
%section.left
%a.left-off-canvas-toggle.menu-icon
%span
%section.right
%section.right{"ng-cloak" => true}
.cart
= render partial: "shared/menu/cart"
%a{href: main_app.shop_path}
@@ -11,34 +11,34 @@
%aside.left-off-canvas-menu.show-for-medium-down
%ul.off-canvas-list
%li.ofn-logo
%a{href: root_path}
%a{href: root_path}
%img{src: "/assets/open-food-network-beta.png", srcset: "/assets/open-food-network-beta.svg", width: "110", height: "26"}
- if current_page? root_path
%li.li-menu
%a{"ofn-scroll-to" => "hubs"}
%span.nav-primary
%span.nav-primary
%i.ofn-i_040-hub
Hubs
- else
%li.li-menu
%a{href: root_path + "#/#hubs"}
%span.nav-primary
%span.nav-primary
%i.ofn-i_040-hub
Hubs
%li.li-menu
%a{href: main_app.map_path}
%span.nav-primary
%span.nav-primary
%i.ofn-i_037-map
Map
%li.li-menu
%a{href: main_app.producers_path}
%span.nav-primary
%span.nav-primary
%i.ofn-i_036-producers
Producers
%li.li-menu
%a{href: main_app.groups_path}
%span.nav-primary
%span.nav-primary
%i.ofn-i_035-groups
Groups

View File

@@ -1,4 +1,4 @@
%products.small-12.columns{"ng-controller" => "ProductsCtrl", "ng-show" => "order_cycle.order_cycle_id != null",
%products.small-12.columns{"ng-controller" => "ProductsCtrl", "ng-show" => "order_cycle.order_cycle_id != null", "ng-cloak" => true,
"infinite-scroll" => "incrementLimit()", "infinite-scroll-distance" => "1"}
// TODO: Needs an ng-show to slide content down

View File

@@ -2,7 +2,7 @@
.panel
.row
.small-12.large-4.columns
- if current_distributor.address.address1 || current_distributor.address.address2 || current_distributor.address.city || current_distributor.address.state || current_distributor.address.zipcode
- if current_distributor.address.address1 || current_distributor.address.address2 || current_distributor.address.city || current_distributor.address.state || current_distributor.address.zipcode
%div.center
.header Address
%strong=current_distributor.name
@@ -49,7 +49,7 @@
- unless current_distributor.linkedin.blank?
%span
%a{href: "http://#{current_distributor.linkedin}", target: "_blank" }
%i.ofn-i_042-linkedin
%i.ofn-i_042-linkedin
/ = current_distributor.linkedin
- unless current_distributor.instagram.blank?

View File

@@ -9,5 +9,5 @@
%ul.bullet-list
- for group in current_distributor.groups
%li
%a{href: main_app.groups_path + "/#/#group#{group.id}"}
%a{href: main_app.groups_path + "/#{group.permalink}"}
= group.name

View File

@@ -1,11 +1,11 @@
#tabs{"ng-controller" => "TabsCtrl"}
#tabs{"ng-controller" => "TabsCtrl", "ng-cloak" => true}
.row
%tabset
-# Build all tabs.
- for name, heading_cols in { about: ["About #{current_distributor.name}", 6],
producers: ["Producers",2],
- for name, heading_cols in { about: ["About #{current_distributor.name}", 6],
producers: ["Producers",2],
contact: ["Contact",2],
groups: ["Groups",2]}
groups: ["Groups",2]}
-# tabs take tab path in 'active' and 'select' functions defined in TabsCtrl.
- heading, cols = heading_cols
%tab.columns{heading: heading,

View File

@@ -43,7 +43,7 @@
Shared Resource?
%div{ :class => "eight columns" }
%h6{ :class => "eight columns alpha", 'ng-show' => 'sharedResource', style: 'text-align: center;' } {{ selectedUnitsProduct.name + ": ALL" }}
%h6{ :class => "eight columns alpha", 'ng-hide' => 'sharedResource', style: 'text-align: center;' } {{ selectedUnitsVariant.unit_text }}
%h6{ :class => "eight columns alpha", 'ng-hide' => 'sharedResource', style: 'text-align: center;' } {{ selectedUnitsVariant.full_name }}
%div{ :class => "four columns omega" }
%h6{ :class => "four columns alpha", :style => 'text-align: right;' }
%a{ :href => '#', 'ng-click' => 'selectedUnitsVariant = {};selectedUnitsProduct = {};sharedResource=false;' } Clear
@@ -123,7 +123,7 @@
%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.unit_text'; reverse = !reverse" } Product: Unit
%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
@@ -132,6 +132,7 @@
%th.actions
Ask?&nbsp;
%input{ :type => 'checkbox', 'ng-model' => "confirmDelete" }
<<<<<<< HEAD
%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' }
@@ -144,16 +145,42 @@
%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.unit_text }}
%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", 'obj-for-update' => "line_item", "attr-for-update" => "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-change' => "weightAdjustedPrice(line_item, {{ line_item.unit_value }})", 'obj-for-update' => "line_item", "attr-for-update" => "unit_value" }
%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 }})", 'obj-for-update' => "line_item", "attr-for-update" => "unit_value" }
%td.price{ 'ng-show' => 'columns.price.visible' }
%input{ :type => 'text', :name => 'price', :id => 'price', :value => '{{ line_item.price | currency }}', 'ng-readonly' => "true", 'obj-for-update' => "line_item", "attr-for-update" => "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" }
=======
%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" }
>>>>>>> master
%input{ :type => "button", 'value' => 'Update', 'ng-click' => 'pendingChanges.submitAll()' }

View File

@@ -8,3 +8,4 @@
= render 'spree/admin/products/bulk_edit/actions'
= render 'spree/admin/products/bulk_edit/indicators'
= render 'spree/admin/products/bulk_edit/products'
= render 'spree/admin/products/bulk_edit/save_button_row'

View File

@@ -1,3 +1,4 @@
= admin_inject_producers
= admin_inject_taxons
= admin_inject_tax_categories
= admin_inject_spree_api_key

View File

@@ -7,7 +7,9 @@
%col.display_as{ ng: { show: 'columns.unit.visible' } }
%col.price{ ng: { show: 'columns.price.visible'} }
%col.on_hand{ ng: { show: 'columns.on_hand.visible' } }
%col.on_demand{ ng: { show: 'columns.on_demand.visible' } }
%col.category{ ng: { show: 'columns.category.visible' } }
%col.tax_category{ ng: { show: 'columns.tax_category.visible' } }
%col.inherits_properties{ ng: { show: 'columns.inherits_properties.visible' } }
%col.available_on{ ng: { show: 'columns.available_on.visible' } }
%col.actions
@@ -17,6 +19,8 @@
%thead
%tr{ ng: { controller: "ColumnsCtrl" } }
%th.left-actions
%a{ 'ng-click' => 'toggleShowAllVariants()', :style => 'color: red' }
Expand All
%th.producer{ 'ng-show' => 'columns.producer.visible' } Producer
%th.sku{ 'ng-show' => 'columns.sku.visible' } SKU
%th.name{ 'ng-show' => 'columns.name.visible' } Name
@@ -24,7 +28,9 @@
%th.display_as{ 'ng-show' => 'columns.unit.visible' } Display As
%th.price{ 'ng-show' => 'columns.price.visible' } Price
%th.on_hand{ 'ng-show' => 'columns.on_hand.visible' } On Hand
%th.on_demand{ 'ng-show' => 'columns.on_demand.visible' } On Demand
%th.category{ 'ng-show' => 'columns.category.visible' } Category
%th.tax_category{ 'ng-show' => 'columns.tax_category.visible' } Tax Category
%th.inherits_properties{ 'ng-show' => 'columns.inherits_properties.visible' } Inherits Properties?
%th.available_on{ 'ng-show' => 'columns.available_on.visible' } Av. On
%th.actions

View File

@@ -1,6 +1,6 @@
%tr.product{ :id => "p_{{product.id}}" }
%td.left-actions
%a{ 'ofn-toggle-variants' => 'true', :class => "view-variants icon-chevron-right", 'ng-show' => 'hasVariants(product)' }
%a{ 'ofn-toggle-variants' => 'true', :class => "view-variants", 'ng-show' => 'hasVariants(product)' }
%a{ :class => "add-variant icon-plus-sign", 'ng-click' => "addVariant(product)", 'ng-show' => "!hasVariants(product) && hasUnit(product)" }
%td.producer{ 'ng-show' => 'columns.producer.visible' }
%select.select2.fullwidth{ 'ng-model' => 'product.producer_id', :name => 'producer_id', 'ofn-track-product' => 'producer_id', 'ng-options' => 'producer.id as producer.name for producer in producers' }
@@ -20,8 +20,13 @@
%td.on_hand{ 'ng-show' => 'columns.on_hand.visible' }
%span{ 'ng-bind' => 'product.on_hand', :name => 'on_hand', 'ng-show' => '!hasOnDemandVariants(product) && (hasVariants(product) || product.on_demand)' }
%input.field{ 'ng-model' => 'product.on_hand', :name => 'on_hand', 'ofn-track-product' => 'on_hand', 'ng-hide' => 'hasVariants(product) || product.on_demand', :type => 'number' }
%td.on_demand{ 'ng-show' => 'columns.on_demand.visible' }
%input.field{ 'ng-model' => 'product.on_demand', :name => 'on_demand', 'ofn-track-product' => 'on_demand', :type => 'checkbox', 'ng-hide' => 'hasVariants(product)' }
%td.category{ 'ng-if' => 'columns.category.visible' }
%input.fullwidth{ :type => 'text', id: "p{{product.id}}_category_id", 'ng-model' => 'product.category_id', 'ofn-taxon-autocomplete' => '', 'ofn-track-product' => 'category_id', 'multiple-selection' => 'false', placeholder: 'Category' }
%td.tax_category{ 'ng-if' => 'columns.tax_category.visible' }
%select.select2{ name: 'product_tax_category_id', 'ofn-track-product' => 'tax_category_id', ng: {model: 'product.tax_category_id', options: 'tax_category.id as tax_category.name for tax_category in tax_categories'} }
%option{value: ''} None
%td.inherits_properties{ 'ng-show' => 'columns.inherits_properties.visible' }
%input{ 'ng-model' => 'product.inherits_properties', :name => 'inherits_properties', 'ofn-track-product' => 'inherits_properties', type: "checkbox" }
%td.available_on{ 'ng-show' => 'columns.available_on.visible' }

View File

@@ -15,11 +15,15 @@
%td{ 'ng-show' => 'columns.on_hand.visible' }
%input.field{ 'ng-model' => 'variant.on_hand', 'ng-change' => 'updateOnHand(product)', :name => 'variant_on_hand', 'ng-hide' => 'variant.on_demand', 'ofn-track-variant' => 'on_hand', :type => 'number' }
%span{ 'ng-bind' => 'variant.on_hand', :name => 'variant_on_hand', 'ng-show' => 'variant.on_demand' }
%td{ 'ng-show' => 'columns.on_demand.visible' }
%input.field{ 'ng-model' => 'variant.on_demand', :name => 'variant_on_demand', 'ofn-track-variant' => 'on_demand', :type => 'checkbox' }
%td{ 'ng-show' => 'columns.category.visible' }
%td{ 'ng-show' => 'columns.tax_category.visible' }
%td{ 'ng-show' => 'columns.inherits_properties.visible' }
%td{ 'ng-show' => 'columns.available_on.visible' }
%td.actions
%a{ 'ng-click' => 'editWarn(product,variant)', :class => "edit-variant icon-edit no-text", 'ng-show' => "variantSaved(variant)" }
%td.actions
%span.icon-warning-sign.with-tip{ 'ng-if' => 'variant.variant_overrides', title: "This variant has {{variant.variant_overrides.length}} override(s)" }
%td.actions
%a{ 'ng-click' => 'deleteVariant(product,variant)', :class => "delete-variant icon-trash no-text" }
%a{ 'ng-click' => 'deleteVariant(product,variant)', "ng-class" => '{disabled: product.variants.length < 2}', :class => "delete-variant icon-trash no-text" }

View File

@@ -0,0 +1,3 @@
%div.sixteen.columns.alpha{ 'ng-hide' => 'loading || products.length == 0', style: "margin-bottom: 10px" }
%div.four.columns.alpha
%input.four.columns.alpha{ :type => 'button', :value => 'Save Changes', 'ng-click' => 'submitProducts()'}

View File

@@ -20,6 +20,10 @@
include_blank: true)
%br
%br
= label_tag nil, "Report Type: "
= select_tag(:report_type, options_for_select(@report_types, @report_type))
%br
%br
= check_box_tag :csv
= label_tag :csv, "Download as csv"
%br
@@ -41,4 +45,3 @@
- if @report.table.empty?
%tr
%td{:colspan => "2"}= t(:none)

View File

@@ -1,5 +0,0 @@
object @line_item
attributes :id, :quantity, :max_quantity
node( :supplier ) { |li| partial 'api/enterprises/bulk_show', :object => li.product.supplier }
node( :units_product ) { |li| partial 'spree/api/products/units_show', :object => li.product }
node( :units_variant ) { |li| partial 'spree/api/variants/units_show', :object => li.variant }

View File

@@ -1,2 +0,0 @@
collection @orders.order('id ASC')
extends "spree/api/orders/bulk_show"

View File

@@ -1,14 +0,0 @@
object @order
attributes :id, :number
node( :full_name ) { |order| order.billing_address.nil? ? "" : ( order.billing_address.full_name || "" ) }
node( :email ) { |order| order.email || "" }
node( :phone ) { |order| order.billing_address.nil? ? "a" : ( order.billing_address.phone || "" ) }
node( :completed_at ) { |order| order.completed_at.blank? ? "" : order.completed_at.strftime("%F %T") }
node( :distributor ) { |order| partial 'api/enterprises/bulk_show', :object => order.distributor }
node( :order_cycle ) { |order| partial 'api/order_cycles/bulk_show', :object => order.order_cycle }
node( :line_items ) do |order|
order.line_items.managed_by(@current_api_user).order('id ASC').map do |line_item|
partial 'spree/api/line_items/bulk_show', :object => line_item
end
end

View File

@@ -1,2 +0,0 @@
object @product
attributes :id, :name, :group_buy_unit_size, :variant_unit

View File

@@ -1,9 +0,0 @@
object @variant
attributes :id
node( :unit_text ) do |v|
options_text = v.options_text
v.product.name + (options_text.empty? ? "" : ": #{options_text}")
end
node( :unit_value ) { |v| v.unit_value }

View File

@@ -10,7 +10,7 @@ development:
test:
adapter: postgresql
encoding: unicode
database: open_food_network_test
database: open_food_network_test<%= ENV['TEST_ENV_NUMBER'] %>
pool: 5
host: localhost
username: ofn

View File

@@ -171,6 +171,10 @@ Spree::Core::Engine.routes.prepend do
post :bulk_update, :on => :collection, :as => :bulk_update
end
resources :orders do
get :managed, on: :collection
end
end
resources :orders do

View File

@@ -0,0 +1,63 @@
class AddStandardVariantToProducts < ActiveRecord::Migration
def up
# Make sure that all products have a variant_unit
Spree::Product.where("variant_unit IS NULL OR variant_unit = ''").update_all(variant_unit: "items", variant_unit_name: "each")
# Find products without any standard variants
products_with_only_master = Spree::Product.includes(:variants).where('spree_variants.id IS NULL').select('DISTINCT spree_products.*')
products_with_only_master.each do |product|
# Add a unit_value to the master variant if it doesn't have one
if product.unit_value.blank?
if product.variant_unit == "weight" && match = product.unit_description.andand.match(/^(\d+(\.\d*)?)(k?g) ?(.*)$/)
scale = (match[3] == "kg" ? 1000 : 1)
product.unit_value = (match[1].to_i*scale)
product.unit_description = match[4]
product.save!
else
unless product.variant_unit == "items" && product.unit_description.present?
product.unit_value = 1
product.save!
end
end
end
# Run the callback to add a copy of the master variant as a standard variant
product.send(:ensure_standard_variant)
existing_master = product.master
new_variant = product.variants.first
# Replace any relevant references to the master variant with the new standard variant
# Inventory Units
# Strategy: do nothing to inventory units pertaining to existing_master,
# new inventory units will be created with reference to new_variant
# Line Items
# Strategy: do nothing to line items pertaining to existing_master,
# new line items will be created with reference to new_variant
# Option Values
# Strategy: add all option values on existing_master to new_variant, and keep on existing_master
option_values = existing_master.option_values
option_values.each do |option_value|
variant_ids = option_value.variant_ids
variant_ids << new_variant.id
option_value.update_attributes(variant_ids: variant_ids)
end
# Prices
# Strategy: duplicate all prices on existing_master and assign them to new_variant
existing_prices = existing_master.prices
existing_prices.each do |price|
new_variant.prices << price.dup
end
# Exchange Variants
# Strategy: Replace all references to existing master in exchanges with new_variant
exchange_variants = ExchangeVariant.where(variant_id: existing_master.id)
exchange_variants.update_all(variant_id: new_variant.id)
end
end
end

View File

@@ -0,0 +1,5 @@
class AddWeightToLineItems < ActiveRecord::Migration
def change
add_column :spree_line_items, :unit_value, :decimal, :precision => 8, :scale => 2
end
end

View File

@@ -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

View File

@@ -0,0 +1,26 @@
class AddPermalinkToGroups < ActiveRecord::Migration
def up
add_column :enterprise_groups, :permalink, :string
EnterpriseGroup.reset_column_information
EnterpriseGroup.all.each do |group|
counter = 1
permalink = group.name.parameterize
permalink = "my-group-name" if permalink == ""
while EnterpriseGroup.find_by_permalink(permalink) do
permalink = group.name.parameterize + counter.to_s
counter += 1
end
group.update_column :permalink, permalink
end
change_column :enterprise_groups, :permalink, :string, null: false
add_index :enterprise_groups, :permalink, :unique => true
end
def down
remove_column :enterprise_groups, :permalink
end
end

View File

@@ -11,7 +11,7 @@
#
# It's strongly recommended to check this file into your version control system.
ActiveRecord::Schema.define(:version => 20150508072938) do
ActiveRecord::Schema.define(:version => 20150527004427) do
create_table "adjustment_metadata", :force => true do |t|
t.integer "adjustment_id"
@@ -236,10 +236,12 @@ ActiveRecord::Schema.define(:version => 20150508072938) do
t.string "linkedin", :default => "", :null => false
t.string "twitter", :default => "", :null => false
t.integer "owner_id"
t.string "permalink", :null => false
end
add_index "enterprise_groups", ["address_id"], :name => "index_enterprise_groups_on_address_id"
add_index "enterprise_groups", ["owner_id"], :name => "index_enterprise_groups_on_owner_id"
add_index "enterprise_groups", ["permalink"], :name => "index_enterprise_groups_on_permalink", :unique => true
create_table "enterprise_groups_enterprises", :id => false, :force => true do |t|
t.integer "enterprise_group_id"

View File

@@ -0,0 +1,27 @@
module OpenFoodNetwork
class EnterpriseInjectionData
def active_distributors
@active_distributors ||= Enterprise.distributors_with_active_order_cycles
end
def earliest_closing_times
@earliest_closing_times ||= OrderCycle.earliest_closing_times
end
def shipping_method_services
@shipping_method_services ||= Spree::ShippingMethod.services
end
def relatives
@relatives ||= EnterpriseRelationship.relatives(true)
end
def supplied_taxons
@supplied_taxons ||= Spree::Taxon.supplied_taxons
end
def distributed_taxons
@distributed_taxons ||= Spree::Taxon.distributed_taxons
end
end
end

Some files were not shown because too many files have changed in this diff Show More