mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-01-29 21:17:17 +00:00
Merge branch 'master' into 2-0-stable
* master: (206 commits) embedded groups layout changes embedded groups initial test Allow html requests for OrderCycleController#update Move applicator calls to OrderCycleForm Refactor OrderCycleForm to make logic clearer Extract schedule syncing logic into OrderCycleForm Add basic specs for OrderCyclesController#create Add basic OrderCycleForm to handle create/update logic Remove unnecessary respond_to blocks from OrderCyclesController Include admin users as managers on new enterprises Remove obsolete goWithoutHashFragments Simplify Navigation.go, not preserving hash fragments Only show change warning for open order cycles Use a SubscriptionsCount query object to provide counts to IndexOrderCycleSerializer Preload subscription counts for serialization in order cycle collection actions Request the subscription count for change warning each time, don't cache Ask user to confirm oc date change for open order cycles with subsciptions Fix ordering of Gemfile.lock Add rack-rewrite to handle redirects Renames product bulk edit action to index ...
This commit is contained in:
@@ -2,7 +2,7 @@ version: "2"
|
||||
plugins:
|
||||
rubocop:
|
||||
enabled: true
|
||||
channel: "rubocop-0-48"
|
||||
channel: "rubocop-0-55"
|
||||
scss-lint:
|
||||
enabled: false
|
||||
duplication:
|
||||
@@ -34,3 +34,4 @@ checks:
|
||||
exclude_patterns:
|
||||
- "spec/**/*"
|
||||
- "vendor/**/*"
|
||||
- "app/assets/javascripts/shared/*"
|
||||
|
||||
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -9,6 +9,8 @@ context for others to understand it]
|
||||
|
||||
[List which features should be tested and how]
|
||||
|
||||
[Should we test on mobile?]
|
||||
|
||||
#### Release notes
|
||||
|
||||
[In case this should be present in the release notes, please write them or
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -32,7 +32,6 @@ public/stylesheets
|
||||
public/images
|
||||
public/spree
|
||||
config/abr.yml
|
||||
config/heroku_env.rb
|
||||
config/newrelic.yml
|
||||
config/initializers/feature_toggle.rb
|
||||
config/initializers/db2fog.rb
|
||||
|
||||
@@ -14,6 +14,8 @@ AllCops:
|
||||
- 'vendor/**/*'
|
||||
- 'node_modules/**/*'
|
||||
- !ruby/regexp /old_and_unused\.rb$/
|
||||
# The parser gem fails to parse this file with out current Ruby version.
|
||||
- 'spec/factories.rb'
|
||||
|
||||
# OFN SETTINGS
|
||||
# Cop settings that have been agreed upon by the OFN community
|
||||
@@ -143,7 +145,11 @@ Style/TrailingCommaInArguments:
|
||||
Enabled: false
|
||||
StyleGuide: http://relaxed.ruby.style/#styletrailingcommainarguments
|
||||
|
||||
Style/TrailingCommaInLiteral:
|
||||
Style/TrailingCommaInArrayLiteral:
|
||||
Enabled: false
|
||||
StyleGuide: http://relaxed.ruby.style/#styletrailingcommainliteral
|
||||
|
||||
Style/TrailingCommaInHashLiteral:
|
||||
Enabled: false
|
||||
StyleGuide: http://relaxed.ruby.style/#styletrailingcommainliteral
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
10
Gemfile
10
Gemfile
@@ -2,7 +2,7 @@ source 'https://rubygems.org'
|
||||
ruby "2.1.5"
|
||||
git_source(:github) { |repo_name| "https://github.com/#{repo_name}.git" }
|
||||
|
||||
gem 'rails', '3.2.21'
|
||||
gem 'rails', '~> 3.2.22'
|
||||
gem 'rails-i18n', '~> 3.0.0'
|
||||
gem 'i18n', '~> 0.6.11'
|
||||
gem 'i18n-js', '~> 3.0.0'
|
||||
@@ -25,7 +25,9 @@ gem 'spree_auth_devise', github: 'spree/spree_auth_devise', branch: '2-0-stable'
|
||||
# - Change type of password from string to password to hide it in the form
|
||||
gem 'spree_paypal_express', github: "spree-contrib/better_spree_paypal_express", branch: "2-0-stable"
|
||||
gem 'stripe', '~> 3.3.1'
|
||||
gem 'activemerchant'
|
||||
# We need at least this version to have Digicert's root certificate
|
||||
# which is needed for Pin Payments (and possibly others).
|
||||
gem 'activemerchant', '~> 1.78'
|
||||
|
||||
gem 'oauth2', '~> 1.2.0' # Used for Stripe Connect
|
||||
gem 'jwt', '~> 1.5'
|
||||
@@ -60,6 +62,7 @@ gem 'geocoder'
|
||||
gem 'gmaps4rails'
|
||||
gem 'spinjs-rails'
|
||||
gem 'rack-ssl', require: 'rack/ssl'
|
||||
gem 'rack-rewrite'
|
||||
gem 'custom_error_message', github: 'jeremydurham/custom-err-msg'
|
||||
gem 'angularjs-file-upload-rails', '~> 1.1.6'
|
||||
gem 'roadie-rails', '~> 1.0.3'
|
||||
@@ -76,6 +79,7 @@ gem 'wkhtmltopdf-binary'
|
||||
gem 'foreigner'
|
||||
gem 'immigrant'
|
||||
gem 'roo', '~> 2.7.0'
|
||||
gem 'roo-xls', '~> 1.1.0'
|
||||
|
||||
gem 'whenever', require: false
|
||||
|
||||
@@ -110,7 +114,7 @@ group :test, :development do
|
||||
gem 'fuubar', '~> 2.2.0'
|
||||
gem 'rspec-rails', ">= 3.5.2"
|
||||
gem 'shoulda-matchers'
|
||||
gem 'factory_girl_rails', require: false
|
||||
gem "factory_bot_rails", require: false
|
||||
gem 'capybara', '>= 2.15.4'
|
||||
gem 'database_cleaner', '0.7.1', require: false
|
||||
gem 'awesome_print'
|
||||
|
||||
82
Gemfile.lock
82
Gemfile.lock
@@ -122,12 +122,12 @@ GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
CFPropertyList (2.3.2)
|
||||
actionmailer (3.2.21)
|
||||
actionpack (= 3.2.21)
|
||||
actionmailer (3.2.22.5)
|
||||
actionpack (= 3.2.22.5)
|
||||
mail (~> 2.5.4)
|
||||
actionpack (3.2.21)
|
||||
activemodel (= 3.2.21)
|
||||
activesupport (= 3.2.21)
|
||||
actionpack (3.2.22.5)
|
||||
activemodel (= 3.2.22.5)
|
||||
activesupport (= 3.2.22.5)
|
||||
builder (~> 3.0.0)
|
||||
erubis (~> 2.7.0)
|
||||
journey (~> 1.0.4)
|
||||
@@ -142,18 +142,18 @@ GEM
|
||||
builder (>= 2.1.2, < 4.0.0)
|
||||
i18n (>= 0.6.9)
|
||||
nokogiri (~> 1.4)
|
||||
activemodel (3.2.21)
|
||||
activesupport (= 3.2.21)
|
||||
activemodel (3.2.22.5)
|
||||
activesupport (= 3.2.22.5)
|
||||
builder (~> 3.0.0)
|
||||
activerecord (3.2.21)
|
||||
activemodel (= 3.2.21)
|
||||
activesupport (= 3.2.21)
|
||||
activerecord (3.2.22.5)
|
||||
activemodel (= 3.2.22.5)
|
||||
activesupport (= 3.2.22.5)
|
||||
arel (~> 3.0.2)
|
||||
tzinfo (~> 0.3.29)
|
||||
activeresource (3.2.21)
|
||||
activemodel (= 3.2.21)
|
||||
activesupport (= 3.2.21)
|
||||
activesupport (3.2.21)
|
||||
activeresource (3.2.22.5)
|
||||
activemodel (= 3.2.22.5)
|
||||
activesupport (= 3.2.22.5)
|
||||
activesupport (3.2.22.5)
|
||||
i18n (~> 0.6, >= 0.6.4)
|
||||
multi_json (~> 1.0)
|
||||
acts-as-taggable-on (3.5.0)
|
||||
@@ -267,10 +267,10 @@ GEM
|
||||
eventmachine (1.2.3)
|
||||
excon (0.45.4)
|
||||
execjs (2.6.0)
|
||||
factory_girl (4.9.0)
|
||||
factory_bot (4.10.0)
|
||||
activesupport (>= 3.0.0)
|
||||
factory_girl_rails (4.9.0)
|
||||
factory_girl (~> 4.9.0)
|
||||
factory_bot_rails (4.10.0)
|
||||
factory_bot (~> 4.10.0)
|
||||
railties (>= 3.0.0)
|
||||
faraday (0.9.2)
|
||||
multipart-post (>= 1.2, < 3)
|
||||
@@ -465,7 +465,7 @@ GEM
|
||||
rb-fsevent (~> 0.9, >= 0.9.4)
|
||||
rb-inotify (~> 0.9, >= 0.9.7)
|
||||
lumberjack (1.0.12)
|
||||
mail (2.5.4)
|
||||
mail (2.5.5)
|
||||
mime-types (~> 1.16)
|
||||
treetop (~> 1.4.8)
|
||||
method_source (0.9.0)
|
||||
@@ -476,7 +476,7 @@ GEM
|
||||
railties (>= 3.1)
|
||||
money (5.1.1)
|
||||
i18n (~> 0.6.0)
|
||||
multi_json (1.12.1)
|
||||
multi_json (1.13.1)
|
||||
multi_xml (0.6.0)
|
||||
multipart-post (2.0.0)
|
||||
nenv (0.3.0)
|
||||
@@ -534,28 +534,29 @@ GEM
|
||||
rabl (0.8.4)
|
||||
activesupport (>= 2.3.14)
|
||||
rack (1.4.7)
|
||||
rack-cache (1.7.0)
|
||||
rack-cache (1.8.0)
|
||||
rack (>= 0.4)
|
||||
rack-livereload (0.3.16)
|
||||
rack
|
||||
rack-rewrite (1.5.1)
|
||||
rack-ssl (1.3.4)
|
||||
rack
|
||||
rack-test (0.6.3)
|
||||
rack (>= 1.0)
|
||||
rails (3.2.21)
|
||||
actionmailer (= 3.2.21)
|
||||
actionpack (= 3.2.21)
|
||||
activerecord (= 3.2.21)
|
||||
activeresource (= 3.2.21)
|
||||
activesupport (= 3.2.21)
|
||||
rails (3.2.22.5)
|
||||
actionmailer (= 3.2.22.5)
|
||||
actionpack (= 3.2.22.5)
|
||||
activerecord (= 3.2.22.5)
|
||||
activeresource (= 3.2.22.5)
|
||||
activesupport (= 3.2.22.5)
|
||||
bundler (~> 1.0)
|
||||
railties (= 3.2.21)
|
||||
railties (= 3.2.22.5)
|
||||
rails-i18n (3.0.1)
|
||||
i18n (~> 0.5)
|
||||
rails (>= 3.0.0, < 4.0.0)
|
||||
railties (3.2.21)
|
||||
actionpack (= 3.2.21)
|
||||
activesupport (= 3.2.21)
|
||||
railties (3.2.22.5)
|
||||
actionpack (= 3.2.22.5)
|
||||
activesupport (= 3.2.22.5)
|
||||
rack-ssl (~> 1.3.2)
|
||||
rake (>= 0.8.7)
|
||||
rdoc (~> 3.4)
|
||||
@@ -563,7 +564,7 @@ GEM
|
||||
rainbow (2.2.2)
|
||||
rake
|
||||
raindrops (0.13.0)
|
||||
rake (10.5.0)
|
||||
rake (12.3.1)
|
||||
ransack (0.7.2)
|
||||
actionpack (~> 3.0)
|
||||
activerecord (~> 3.0)
|
||||
@@ -592,6 +593,10 @@ GEM
|
||||
roo (2.7.1)
|
||||
nokogiri (~> 1)
|
||||
rubyzip (~> 1.1, < 2.0.0)
|
||||
roo-xls (1.1.0)
|
||||
nokogiri
|
||||
roo (>= 2.0.0beta1, < 3)
|
||||
spreadsheet (> 0.9.0)
|
||||
rspec (3.7.0)
|
||||
rspec-core (~> 3.7.0)
|
||||
rspec-expectations (~> 3.7.0)
|
||||
@@ -622,6 +627,7 @@ GEM
|
||||
rainbow (>= 1.99.1, < 3.0)
|
||||
ruby-progressbar (~> 1.7)
|
||||
unicode-display_width (~> 1.0, >= 1.0.1)
|
||||
ruby-ole (1.2.12.1)
|
||||
ruby-progressbar (1.8.1)
|
||||
rubyzip (1.2.0)
|
||||
safe_yaml (1.0.4)
|
||||
@@ -640,6 +646,8 @@ GEM
|
||||
activesupport (>= 3.0.0)
|
||||
spinjs-rails (1.3)
|
||||
rails (>= 3.1)
|
||||
spreadsheet (1.1.7)
|
||||
ruby-ole (>= 1.0)
|
||||
sprockets (2.2.3)
|
||||
hike (~> 1.2)
|
||||
multi_json (~> 1.0)
|
||||
@@ -662,7 +670,7 @@ GEM
|
||||
turbo-sprockets-rails3 (0.3.6)
|
||||
railties (> 3.2.8, < 4.0.0)
|
||||
sprockets (>= 2.0.0)
|
||||
tzinfo (0.3.53)
|
||||
tzinfo (0.3.54)
|
||||
uglifier (2.7.1)
|
||||
execjs (>= 0.3.0)
|
||||
json (>= 1.8.0)
|
||||
@@ -701,7 +709,7 @@ PLATFORMS
|
||||
|
||||
DEPENDENCIES
|
||||
active_model_serializers
|
||||
activemerchant
|
||||
activemerchant (~> 1.78)
|
||||
acts-as-taggable-on (~> 3.4)
|
||||
andand
|
||||
angular-rails-templates (~> 0.2.0)
|
||||
@@ -727,7 +735,7 @@ DEPENDENCIES
|
||||
delayed_job_active_record
|
||||
diffy
|
||||
eventmachine (>= 1.2.3)
|
||||
factory_girl_rails
|
||||
factory_bot_rails
|
||||
figaro
|
||||
foreigner
|
||||
foundation-icons-sass-rails
|
||||
@@ -765,13 +773,15 @@ DEPENDENCIES
|
||||
pry-byebug (>= 3.4.3)
|
||||
rabl
|
||||
rack-livereload
|
||||
rack-rewrite
|
||||
rack-ssl
|
||||
rails (= 3.2.21)
|
||||
rails (~> 3.2.22)
|
||||
rails-i18n (~> 3.0.0)
|
||||
redcarpet
|
||||
representative_view
|
||||
roadie-rails (~> 1.0.3)
|
||||
roo (~> 2.7.0)
|
||||
roo-xls (~> 1.1.0)
|
||||
rspec-rails (>= 3.5.2)
|
||||
rspec-retry
|
||||
rubocop (>= 0.49.1)
|
||||
@@ -802,4 +812,4 @@ RUBY VERSION
|
||||
ruby 2.1.5p273
|
||||
|
||||
BUNDLED WITH
|
||||
1.16.1
|
||||
1.16.2
|
||||
|
||||
40
Guardfile
40
Guardfile
@@ -5,45 +5,7 @@ guard 'livereload' do
|
||||
watch(%r{app/views/.+\.(erb|haml|slim)$})
|
||||
watch(%r{app/helpers/.+\.rb})
|
||||
watch(%r{public/.+\.(css|js|html)})
|
||||
#watch(%r{config/locales/.+\.yml})
|
||||
|
||||
# Rails Assets Pipeline
|
||||
watch(%r{(app|vendor)(/assets/\w+/(.+\.(css|js|html|png|jpg))).*}) { |m| "/assets/#{m[3]}" }
|
||||
end
|
||||
|
||||
|
||||
#guard 'rails' do
|
||||
#watch('Gemfile.lock')
|
||||
#watch(%r{^(config|lib)/.*})
|
||||
#end
|
||||
|
||||
|
||||
#guard 'zeus' do
|
||||
## uses the .rspec file
|
||||
## --colour --fail-fast --format documentation --tag ~slow
|
||||
#watch(%r{^spec/.+_spec\.rb$})
|
||||
#watch(%r{^app/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
|
||||
#watch(%r{^app/(.+)\.haml$}) { |m| "spec/#{m[1]}.haml_spec.rb" }
|
||||
#watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
|
||||
#watch(%r{^app/controllers/(.+)_(controller)\.rb$}) { |m| ["spec/routing/#{m[1]}_routing_spec.rb", "spec/#{m[2]}s/#{m[1]}_#{m[2]}_spec.rb", "spec/requests/#{m[1]}_spec.rb"] }
|
||||
#end
|
||||
|
||||
#guard :rspec do
|
||||
#watch(%r{^spec/.+_spec\.rb$})
|
||||
#watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
|
||||
#watch('spec/spec_helper.rb') { "spec" }
|
||||
|
||||
## Rails example
|
||||
#watch(%r{^app/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
|
||||
#watch(%r{^app/(.*)(\.erb|\.haml|\.slim)$}) { |m| "spec/#{m[1]}#{m[2]}_spec.rb" }
|
||||
#watch(%r{^app/controllers/(.+)_(controller)\.rb$}) { |m| ["spec/routing/#{m[1]}_routing_spec.rb", "spec/#{m[2]}s/#{m[1]}_#{m[2]}_spec.rb", "spec/acceptance/#{m[1]}_spec.rb"] }
|
||||
#watch(%r{^spec/support/(.+)\.rb$}) { "spec" }
|
||||
#watch('config/routes.rb') { "spec/routing" }
|
||||
#watch('app/controllers/application_controller.rb') { "spec/controllers" }
|
||||
|
||||
## Capybara features specs
|
||||
#watch(%r{^app/views/(.+)/.*\.(erb|haml|slim)$}) { |m| "spec/features/#{m[1]}_spec.rb" }
|
||||
|
||||
## Turnip features and steps
|
||||
#watch(%r{^spec/acceptance/(.+)\.feature$})
|
||||
#watch(%r{^spec/acceptance/steps/(.+)_steps\.rb$}) { |m| Dir[File.join("**/#{m[1]}.feature")][0] || 'spec/acceptance' }
|
||||
#end
|
||||
|
||||
@@ -103,6 +103,9 @@ Do not forget to run `rake tmp:cache:clear` after locales are updated to reload
|
||||
* Lynne Davis (https://github.com/lin-d-hop)
|
||||
* Paul Mackay (https://github.com/pmackay)
|
||||
* Steve Pettitt (https://github.com/stveep)
|
||||
* Matt Yorkley (https://github.com/Matt-Yorkley)
|
||||
* Pau Pérez (https://github.com/sauloperez)
|
||||
* Enrico Stano (https://github.com/enricostano)
|
||||
|
||||
|
||||
## Licence
|
||||
|
||||
BIN
app/assets/images/home/tagline-bg.jpg
Normal file
BIN
app/assets/images/home/tagline-bg.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 162 KiB |
@@ -36,6 +36,7 @@
|
||||
//= require ./orders/orders
|
||||
//= require ./order_cycles/order_cycles
|
||||
//= require ./payment_methods/payment_methods
|
||||
//= require ./product_import/product_import
|
||||
//= require ./products/products
|
||||
//= require ./resources/resources
|
||||
//= require ./shipping_methods/shipping_methods
|
||||
|
||||
@@ -28,6 +28,7 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", ($scope, $timeout
|
||||
$scope.filterTaxons = [{id: "0", name: ""}].concat $scope.taxons
|
||||
$scope.producerFilter = "0"
|
||||
$scope.categoryFilter = "0"
|
||||
$scope.importDateFilter = "0"
|
||||
$scope.products = BulkProducts.products
|
||||
$scope.filteredProducts = []
|
||||
$scope.currentFilters = []
|
||||
@@ -43,7 +44,7 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", ($scope, $timeout
|
||||
.catch (message) ->
|
||||
$scope.api_error_msg = message
|
||||
|
||||
$scope.$watchCollection '[query, producerFilter, categoryFilter]', ->
|
||||
$scope.$watchCollection '[query, producerFilter, categoryFilter, importDateFilter]', ->
|
||||
$scope.limit = 15 # Reset limit whenever searching
|
||||
|
||||
$scope.fetchProducts = ->
|
||||
@@ -52,6 +53,9 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", ($scope, $timeout
|
||||
$scope.resetProducts()
|
||||
$scope.loading = false
|
||||
|
||||
$timeout ->
|
||||
if $scope.showLatestImport
|
||||
$scope.importDateFilter = $scope.importDates[1].id
|
||||
|
||||
$scope.resetProducts = ->
|
||||
DirtyProducts.clear()
|
||||
@@ -91,6 +95,7 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", ($scope, $timeout
|
||||
$scope.query = ""
|
||||
$scope.producerFilter = "0"
|
||||
$scope.categoryFilter = "0"
|
||||
$scope.importDateFilter = "0"
|
||||
|
||||
$scope.editWarn = (product, variant) ->
|
||||
if (DirtyProducts.count() > 0 and confirm(t("unsaved_changes_confirmation"))) or (DirtyProducts.count() == 0)
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
angular.module("ofn.admin").controller "enterprisesDashboardCtrl", ($scope) ->
|
||||
$scope.activeTab = "hubs"
|
||||
@@ -0,0 +1,6 @@
|
||||
angular.module("ofn.admin").directive "select2NoSearch", ($timeout) ->
|
||||
restrict: 'CA'
|
||||
link: (scope, element, attrs) ->
|
||||
$timeout ->
|
||||
element.select2
|
||||
minimumResultsForSearch: Infinity
|
||||
@@ -0,0 +1,18 @@
|
||||
# Used in enterprise new and edit forms to reset the state when the country is changed
|
||||
angular.module("admin.enterprises").controller 'countryCtrl', ($scope, availableCountries) ->
|
||||
$scope.countries = availableCountries
|
||||
|
||||
$scope.countriesById = $scope.countries.reduce (obj, country) ->
|
||||
obj[country.id] = country
|
||||
obj
|
||||
, {}
|
||||
|
||||
$scope.$watch 'Enterprise.address.country_id', (newID, oldID) ->
|
||||
$scope.clearState() unless $scope.addressStateMatchesCountry()
|
||||
|
||||
$scope.clearState = ->
|
||||
$scope.Enterprise.address.state_id = null
|
||||
|
||||
$scope.addressStateMatchesCountry = ->
|
||||
$scope.countriesById[$scope.Enterprise.address.country_id].states.some (state) ->
|
||||
state.id == $scope.Enterprise.address.state_id
|
||||
@@ -11,6 +11,9 @@ angular.module("admin.enterprises")
|
||||
$scope.$watch 'enterprise_form.$dirty', (newValue) ->
|
||||
StatusMessage.display 'notice', t('admin.unsaved_changes') if newValue
|
||||
|
||||
$scope.$watch 'newManager', (newValue) ->
|
||||
$scope.addManager($scope.newManager) if newValue
|
||||
|
||||
$scope.setFormDirty = ->
|
||||
$scope.$apply ->
|
||||
$scope.enterprise_form.$setDirty()
|
||||
@@ -47,7 +50,7 @@ angular.module("admin.enterprises")
|
||||
email: manager.email
|
||||
confirmed: manager.confirmed
|
||||
if (user for user in $scope.Enterprise.users when user.id == manager.id).length == 0
|
||||
$scope.Enterprise.users.push manager
|
||||
$scope.Enterprise.users.unshift(manager)
|
||||
$scope.enterprise_form?.$setDirty()
|
||||
else
|
||||
alert ("#{manager.email}" + " " + t("is_already_manager"))
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
angular.module("admin.enterprises").controller 'NewEnterpriseController', ($scope, defaultCountryID) ->
|
||||
$scope.Enterprise =
|
||||
address:
|
||||
country_id: defaultCountryID
|
||||
state_id: null
|
||||
@@ -0,0 +1,4 @@
|
||||
angular.module("ofn.admin").filter "importDate", ($filter) ->
|
||||
return (products, importDate) ->
|
||||
return products if importDate == "0"
|
||||
$filter('filter')( products, { import_date: importDate } )
|
||||
@@ -0,0 +1,23 @@
|
||||
angular.module("admin.orderCycles").directive "changeWarning", (ConfirmDialog) ->
|
||||
restrict: "A"
|
||||
scope:
|
||||
orderCycle: '=changeWarning'
|
||||
link: (scope, element, attrs) ->
|
||||
acknowledged = false
|
||||
cancel = 'admin.order_cycles.date_warning.cancel'
|
||||
proceed = 'admin.order_cycles.date_warning.proceed'
|
||||
msg = 'admin.order_cycles.date_warning.msg'
|
||||
options = { cancel: t(cancel), confirm: t(proceed) }
|
||||
|
||||
isOpen = (orderCycle) ->
|
||||
moment(orderCycle.orders_open_at, "YYYY-MM-DD HH:mm:SS Z").isBefore() &&
|
||||
moment(orderCycle.orders_close_at, "YYYY-MM-DD HH:mm:SS Z").isAfter()
|
||||
|
||||
element.focus ->
|
||||
count = scope.orderCycle.subscriptions_count
|
||||
return if acknowledged
|
||||
return unless isOpen(scope.orderCycle)
|
||||
return if count < 1
|
||||
ConfirmDialog.open('info', t(msg, n: count), options).then ->
|
||||
acknowledged = true
|
||||
element.siblings('img').trigger('click')
|
||||
@@ -209,6 +209,7 @@ angular.module('admin.orderCycles').factory 'OrderCycle', ($resource, $window, S
|
||||
delete order_cycle.editable_variants_for_incoming_exchanges
|
||||
delete order_cycle.editable_variants_for_outgoing_exchanges
|
||||
delete order_cycle.visible_variants_for_outgoing_exchanges
|
||||
delete order_cycle.subscriptions_count
|
||||
order_cycle
|
||||
|
||||
removeInactiveExchanges: (order_cycle) ->
|
||||
|
||||
@@ -56,7 +56,4 @@ angular.module("admin.orders").directive 'customerSearchOverride', ->
|
||||
return
|
||||
$('#order_email').val customer.email
|
||||
$('#user_id').val customer.user_id # modified
|
||||
$('#guest_checkout_true').prop 'checked', false
|
||||
$('#guest_checkout_false').prop 'checked', true
|
||||
$('#guest_checkout_false').prop 'disabled', false
|
||||
customer.email
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
angular.module("ofn.admin").controller "DropdownPanelsCtrl", ($scope) ->
|
||||
angular.module("admin.productImport").controller "DropdownPanelsCtrl", ($scope) ->
|
||||
$scope.active = false
|
||||
|
||||
$scope.togglePanel = ->
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
angular.module("admin.productImport").controller "ImportFeedbackCtrl", ($scope) ->
|
||||
|
||||
$scope.count = (items) ->
|
||||
total = 0
|
||||
angular.forEach items, (item) ->
|
||||
total++
|
||||
total
|
||||
|
||||
$scope.attribute_invalid = (attribute, line_number) ->
|
||||
$scope.entries[line_number]['errors'][attribute] != undefined
|
||||
|
||||
$scope.ignore_fields = ['variant_unit', 'variant_unit_scale', 'unit_description']
|
||||
@@ -0,0 +1,161 @@
|
||||
angular.module("admin.productImport").controller "ImportFormCtrl", ($scope, $http, $filter, ProductImportService, $timeout) ->
|
||||
|
||||
$scope.entries = {}
|
||||
$scope.update_counts = {}
|
||||
$scope.reset_counts = {}
|
||||
|
||||
$scope.updates = {}
|
||||
$scope.updated_total = 0
|
||||
$scope.updated_ids = []
|
||||
$scope.update_errors = []
|
||||
|
||||
$scope.chunks = 0
|
||||
$scope.completed = 0
|
||||
$scope.percentage = "0%"
|
||||
$scope.started = false
|
||||
$scope.finished = false
|
||||
|
||||
$scope.countResettable = () ->
|
||||
angular.forEach $scope.supplier_product_counts, (value, key) ->
|
||||
$scope.reset_counts[key] = value
|
||||
if $scope.update_counts[key]
|
||||
$scope.reset_counts[key] -= $scope.update_counts[key]
|
||||
|
||||
$scope.resetProgress = () ->
|
||||
$scope.chunks = 0
|
||||
$scope.completed = 0
|
||||
$scope.percentage = "0%"
|
||||
$scope.started = false
|
||||
$scope.finished = false
|
||||
|
||||
$scope.step = 'settings'
|
||||
|
||||
$scope.confirmSettings = () ->
|
||||
$scope.step = 'import'
|
||||
|
||||
$scope.viewResults = () ->
|
||||
$scope.countResettable()
|
||||
$scope.step = 'results'
|
||||
$scope.resetProgress()
|
||||
|
||||
$scope.acceptResults = () ->
|
||||
$scope.step = 'save'
|
||||
|
||||
$scope.finalResults = () ->
|
||||
$scope.step = 'complete'
|
||||
|
||||
$scope.start = () ->
|
||||
$scope.started = true
|
||||
$scope.percentage = "1%"
|
||||
total = $scope.item_count
|
||||
size = 100
|
||||
$scope.chunks = Math.ceil(total / size)
|
||||
|
||||
i = 0
|
||||
|
||||
while i < $scope.chunks
|
||||
start = (i*size)+1
|
||||
end = (i+1)*size
|
||||
if $scope.step == 'import'
|
||||
$scope.processImport(start, end)
|
||||
if $scope.step == 'save'
|
||||
$scope.processSave(start, end)
|
||||
i++
|
||||
|
||||
$scope.processImport = (start, end) ->
|
||||
$scope.getSettings() if $scope.importSettings == null
|
||||
$http(
|
||||
url: $scope.import_url
|
||||
method: 'POST'
|
||||
data:
|
||||
'start': start
|
||||
'end': end
|
||||
'filepath': $scope.filepath
|
||||
'settings': $scope.importSettings
|
||||
).success((data, status, headers, config) ->
|
||||
angular.merge($scope.entries, angular.fromJson(data['entries']))
|
||||
$scope.sortUpdates(data['reset_counts'])
|
||||
|
||||
$scope.updateProgress()
|
||||
).error((data, status, headers, config) ->
|
||||
$scope.exception = data
|
||||
console.error(data)
|
||||
)
|
||||
|
||||
$scope.importSettings = null
|
||||
|
||||
$scope.getSettings = () ->
|
||||
$scope.importSettings = ProductImportService.getSettings()
|
||||
|
||||
$scope.sortUpdates = (data) ->
|
||||
angular.forEach data, (value, key) ->
|
||||
if (key in $scope.update_counts)
|
||||
$scope.update_counts[key] += value['updates_count']
|
||||
else
|
||||
$scope.update_counts[key] = value['updates_count']
|
||||
|
||||
$scope.processSave = (start, end) ->
|
||||
$scope.getSettings() if $scope.importSettings == null
|
||||
$http(
|
||||
url: $scope.save_url
|
||||
method: 'POST'
|
||||
data:
|
||||
'start': start
|
||||
'end': end
|
||||
'filepath': $scope.filepath
|
||||
'settings': $scope.importSettings
|
||||
).success((data, status, headers, config) ->
|
||||
$scope.sortResults(data['results'])
|
||||
|
||||
angular.forEach data['updated_ids'], (id) ->
|
||||
$scope.updated_ids.push(id)
|
||||
|
||||
angular.forEach data['errors'], (error) ->
|
||||
$scope.update_errors.push(error)
|
||||
|
||||
$scope.updateProgress()
|
||||
).error((data, status, headers, config) ->
|
||||
$scope.exception = data
|
||||
console.error(data)
|
||||
)
|
||||
|
||||
$scope.sortResults = (results) ->
|
||||
angular.forEach results, (value, key) ->
|
||||
if ($scope.updates[key] != undefined)
|
||||
$scope.updates[key] += value
|
||||
else
|
||||
$scope.updates[key] = value
|
||||
|
||||
$scope.updated_total += value
|
||||
|
||||
$scope.resetAbsent = () ->
|
||||
enterprises_to_reset = []
|
||||
angular.forEach $scope.importSettings, (settings, enterprise) ->
|
||||
if settings['reset_all_absent']
|
||||
enterprises_to_reset.push(enterprise)
|
||||
|
||||
if enterprises_to_reset.length && $scope.updated_ids.length
|
||||
$http(
|
||||
url: $scope.reset_url
|
||||
method: 'POST'
|
||||
data:
|
||||
'filepath': $scope.filepath
|
||||
'settings': $scope.importSettings
|
||||
'reset_absent': true,
|
||||
'updated_ids': $scope.updated_ids,
|
||||
'enterprises_to_reset': enterprises_to_reset
|
||||
).success((data, status, headers, config) ->
|
||||
console.log(data)
|
||||
$scope.updates.products_reset = data
|
||||
|
||||
).error((data, status, headers, config) ->
|
||||
console.error(data)
|
||||
)
|
||||
|
||||
$scope.updateProgress = () ->
|
||||
$scope.completed++
|
||||
$scope.percentage = String(Math.round(($scope.completed / $scope.chunks) * 100)) + '%'
|
||||
|
||||
if $scope.completed == $scope.chunks
|
||||
$scope.finished = true
|
||||
$scope.resetAbsent() if $scope.step == 'save'
|
||||
@@ -1,12 +1,38 @@
|
||||
angular.module("ofn.admin").controller "ImportOptionsFormCtrl", ($scope, $rootScope, ProductImportService) ->
|
||||
angular.module("admin.productImport").controller "ImportOptionsFormCtrl", ($scope, $rootScope, ProductImportService) ->
|
||||
|
||||
$scope.toggleResetAbsent = () ->
|
||||
confirmed = confirm t('js.product_import.confirmation') if $scope.resetAbsent
|
||||
$scope.initForm = () ->
|
||||
$scope.settings = {} if $scope.settings == undefined
|
||||
$scope.settings[$scope.supplierId] = {
|
||||
import_into: 'product_list'
|
||||
defaults:
|
||||
count_on_hand:
|
||||
mode: 'overwrite_all'
|
||||
on_hand:
|
||||
mode: 'overwrite_all'
|
||||
tax_category_id:
|
||||
mode: 'overwrite_all'
|
||||
shipping_category_id:
|
||||
mode: 'overwrite_all'
|
||||
available_on:
|
||||
mode: 'overwrite_all'
|
||||
}
|
||||
$scope.import_into = 'product_list'
|
||||
|
||||
if confirmed or !$scope.resetAbsent
|
||||
ProductImportService.updateResetAbsent($scope.supplierId, $scope.resetCount, $scope.resetAbsent)
|
||||
$scope.updateImportInto = () ->
|
||||
$scope.import_into = $scope.settings[$scope.supplierId]['import_into']
|
||||
|
||||
$scope.$watch 'settings', (updated) ->
|
||||
ProductImportService.updateSettings(updated)
|
||||
, true
|
||||
|
||||
$scope.toggleResetAbsent = (id) ->
|
||||
checked = $scope.settings[id]['reset_all_absent']
|
||||
confirmed = confirm t('js.product_import.confirmation') if checked
|
||||
|
||||
if confirmed or !checked
|
||||
ProductImportService.updateResetAbsent($scope.supplierId, $scope.reset_counts[$scope.supplierId], checked)
|
||||
else
|
||||
$scope.resetAbsent = false
|
||||
$scope.settings[id]['reset_all_absent'] = false
|
||||
|
||||
$scope.resetTotal = ProductImportService.resetTotal
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
angular.module("admin.productImport").filter 'entriesFilterValid', ->
|
||||
(entries, type) ->
|
||||
if type == 'all'
|
||||
return entries
|
||||
|
||||
filtered = {}
|
||||
|
||||
angular.forEach entries, (entry, line_number) ->
|
||||
validates_as = entry.validates_as
|
||||
|
||||
if type == 'valid' and validates_as != '' \
|
||||
or type == 'invalid' and validates_as == '' \
|
||||
or type == 'create_product' and validates_as == 'new_product' or validates_as == 'new_variant' \
|
||||
or type == 'update_product' and validates_as == 'existing_variant' \
|
||||
or type == 'create_inventory' and validates_as == 'new_inventory_item' \
|
||||
or type == 'update_inventory' and validates_as == 'existing_inventory_item'
|
||||
filtered[line_number] = entry
|
||||
|
||||
filtered
|
||||
|
||||
angular.module("admin.productImport").filter 'entriesFilterSupplier', ->
|
||||
(entries, supplier) ->
|
||||
if supplier == 'all'
|
||||
return entries
|
||||
|
||||
filtered = {}
|
||||
|
||||
angular.forEach entries, (entry, line_number) ->
|
||||
if supplier == entry.attributes['supplier']
|
||||
filtered[line_number] = entry
|
||||
|
||||
filtered
|
||||
@@ -0,0 +1,3 @@
|
||||
angular.module("admin.productImport", ["ngResource"]).config ($httpProvider) ->
|
||||
$httpProvider.defaults.headers.common["X-CSRF-Token"] = $("meta[name=csrf-token]").attr("content")
|
||||
$httpProvider.defaults.headers.common["Accept"] = "application/json, text/javascript, */*"
|
||||
@@ -1,7 +1,8 @@
|
||||
angular.module("ofn.admin").factory "ProductImportService", ($rootScope) ->
|
||||
angular.module("admin.productImport").factory "ProductImportService", ($rootScope) ->
|
||||
new class ProductImportService
|
||||
suppliers: {}
|
||||
resetTotal: 0
|
||||
settings: {}
|
||||
|
||||
updateResetAbsent: (supplierId, resetCount, resetAbsent) ->
|
||||
if resetAbsent
|
||||
@@ -13,3 +14,8 @@ angular.module("ofn.admin").factory "ProductImportService", ($rootScope) ->
|
||||
|
||||
$rootScope.resetTotal = @resetTotal
|
||||
|
||||
updateSettings: (updated) ->
|
||||
angular.merge(@settings, updated)
|
||||
|
||||
getSettings: () ->
|
||||
@settings
|
||||
@@ -2,5 +2,7 @@ angular.module("ofn.admin").controller "ProductImageCtrl", ($scope, ProductImage
|
||||
$scope.imageUploader = ProductImageService.imageUploader
|
||||
$scope.imagePreview = ProductImageService.imagePreview
|
||||
|
||||
$scope.$watch 'product.image_url', (newValue) ->
|
||||
$scope.imagePreview = newValue if newValue
|
||||
$scope.$watch 'product.image_url', (newValue, oldValue) ->
|
||||
if newValue != oldValue
|
||||
$scope.imagePreview = newValue
|
||||
$scope.uploadModal.close()
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
angular.module("admin.utils").directive "datepicker", ->
|
||||
require: "ngModel"
|
||||
link: (scope, element, attrs, ngModel) ->
|
||||
element.datepicker
|
||||
dateFormat: "yy-mm-dd"
|
||||
onSelect: (dateText, inst) ->
|
||||
scope.$apply (scope) ->
|
||||
# Fires ngModel.$parsers
|
||||
ngModel.$setViewValue dateText
|
||||
@@ -25,7 +25,8 @@ angular.module("admin.utils").directive "tagsWithTranslation", ($timeout) ->
|
||||
scope.object[scope.tagsAttr] ||= []
|
||||
compileTagList()
|
||||
|
||||
scope.tagAdded = ->
|
||||
scope.tagAdded = (tag)->
|
||||
tag.text = tag.text.toLowerCase()
|
||||
scope.onTagAdded()
|
||||
compileTagList()
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ angular.module("admin.variantOverrides").controller "AdminVariantOverridesCtrl",
|
||||
|
||||
$scope.resetSelectFilters = ->
|
||||
$scope.producerFilter = 0
|
||||
$scope.importDateFilter = '0'
|
||||
$scope.query = ''
|
||||
|
||||
$scope.resetSelectFilters()
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
angular.module("admin.variantOverrides").filter "importDate", ($filter, variantOverrides) ->
|
||||
return (products, hub_id, date) ->
|
||||
return [] if !hub_id
|
||||
return $filter('filter')(products, (product) ->
|
||||
return true if date == 0 or date == undefined or date == '0' or date == ''
|
||||
|
||||
angular.forEach product.variants (variant) ->
|
||||
angular.forEach variantOverrides (vo) ->
|
||||
if vo.variant_id == variant.id and vo.import_date == date
|
||||
return true
|
||||
false
|
||||
, true)
|
||||
@@ -1 +1 @@
|
||||
angular.module("admin.variantOverrides", ["admin.indexUtils", "admin.utils", "admin.dropdown", "admin.inventoryItems", 'ngTagsInput'])
|
||||
angular.module("admin.variantOverrides", ["ofn.admin", "admin.indexUtils", "admin.utils", "admin.dropdown", "admin.inventoryItems", 'ngTagsInput'])
|
||||
|
||||
@@ -6,7 +6,14 @@ Darkswarm.controller "ForgotCtrl", ($scope, $http, $location, AuthenticationServ
|
||||
if $scope.spree_user.email != null
|
||||
$http.post("/user/spree_user/password", {spree_user: $scope.spree_user}).success (data)->
|
||||
$scope.sent = true
|
||||
.error (data) ->
|
||||
$scope.errors = t 'email_not_found'
|
||||
.error (data, status) ->
|
||||
$scope.errors = data.error
|
||||
$scope.user_unconfirmed = (status == 401)
|
||||
else
|
||||
$scope.errors = t 'email_required'
|
||||
|
||||
$scope.resend_confirmation = ->
|
||||
$http.post("/user/spree_user/confirmation", {spree_user: $scope.spree_user, return_url: $location.absUrl()}).success (data)->
|
||||
$scope.messages = t('devise.confirmations.send_instructions')
|
||||
.error (data) ->
|
||||
$scope.errors = t('devise.confirmations.failed_to_send')
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
Darkswarm.controller "LoginCtrl", ($scope, $timeout, $location, $http, $window, AuthenticationService, Redirections, Loading) ->
|
||||
$scope.path = "/login"
|
||||
|
||||
$scope.modalMessage = null
|
||||
|
||||
$scope.$watch (->
|
||||
AuthenticationService.modalMessage
|
||||
), (newValue) ->
|
||||
$scope.errors = newValue
|
||||
|
||||
$scope.submit = ->
|
||||
Loading.message = t 'logging_in'
|
||||
$http.post("/user/spree_user/sign_in", {spree_user: $scope.spree_user}).success (data)->
|
||||
@@ -14,7 +21,7 @@ Darkswarm.controller "LoginCtrl", ($scope, $timeout, $location, $http, $window,
|
||||
$scope.user_unconfirmed = (data.error == t('devise.failure.unconfirmed'))
|
||||
|
||||
$scope.resend_confirmation = ->
|
||||
$http.post("/user/spree_user/confirmation", {spree_user: $scope.spree_user}).success (data)->
|
||||
$http.post("/user/spree_user/confirmation", {spree_user: $scope.spree_user, return_url: $location.absUrl()}).success (data)->
|
||||
$scope.messages = t('devise.confirmations.send_instructions')
|
||||
.error (data) ->
|
||||
$scope.errors = t('devise.confirmations.failed_to_send')
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Darkswarm.controller "CheckoutCtrl", ($scope, localStorageService, Checkout, CurrentUser, CurrentHub) ->
|
||||
Darkswarm.controller "CheckoutCtrl", ($scope, localStorageService, Checkout, CurrentUser, CurrentHub, AuthenticationService, SpreeUser, $http) ->
|
||||
$scope.Checkout = Checkout
|
||||
$scope.submitted = false
|
||||
|
||||
@@ -20,8 +20,29 @@ Darkswarm.controller "CheckoutCtrl", ($scope, localStorageService, Checkout, Cur
|
||||
|
||||
$scope.purchase = (event, form) ->
|
||||
event.preventDefault()
|
||||
$scope.formdata = form
|
||||
$scope.submitted = true
|
||||
if form.$valid
|
||||
|
||||
if CurrentUser.id
|
||||
$scope.validateForm(form)
|
||||
else
|
||||
$scope.ensureUserIsGuest()
|
||||
|
||||
$scope.validateForm = ->
|
||||
if $scope.formdata.$valid
|
||||
$scope.Checkout.purchase()
|
||||
else
|
||||
$scope.$broadcast 'purchaseFormInvalid', form
|
||||
$scope.$broadcast 'purchaseFormInvalid', $scope.formdata
|
||||
|
||||
$scope.ensureUserIsGuest = (callback = null) ->
|
||||
$http.post("/user/registered_email", {email: $scope.order.email}).success (data)->
|
||||
if data.registered == true
|
||||
$scope.promptLogin()
|
||||
else
|
||||
$scope.validateForm() if $scope.submitted
|
||||
callback() if callback
|
||||
|
||||
$scope.promptLogin = ->
|
||||
SpreeUser.spree_user.email = $scope.order.email
|
||||
AuthenticationService.pushMessage t('devise.failure.already_registered')
|
||||
AuthenticationService.open '/login'
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
Darkswarm.controller "CountryCtrl", ($scope, availableCountries) ->
|
||||
|
||||
$scope.countries = availableCountries
|
||||
|
||||
$scope.countriesById = $scope.countries.reduce (obj, country) ->
|
||||
obj[country.id] = country
|
||||
obj
|
||||
, {}
|
||||
@@ -1,8 +1,16 @@
|
||||
Darkswarm.controller "DetailsCtrl", ($scope, $timeout) ->
|
||||
Darkswarm.controller "DetailsCtrl", ($scope, $timeout, $http, CurrentUser, AuthenticationService, SpreeUser) ->
|
||||
angular.extend(this, new FieldsetMixin($scope))
|
||||
$scope.name = "details"
|
||||
$scope.nextPanel = "billing"
|
||||
|
||||
$scope.login_or_next = (event) ->
|
||||
event.preventDefault()
|
||||
unless CurrentUser.id
|
||||
$scope.ensureUserIsGuest($scope.next)
|
||||
return
|
||||
|
||||
$scope.next()
|
||||
|
||||
$scope.summary = ->
|
||||
[$scope.fullName(),
|
||||
$scope.order.email,
|
||||
|
||||
@@ -9,6 +9,10 @@ Darkswarm.controller "PaymentCtrl", ($scope, $timeout, savedCreditCards, Dates)
|
||||
$scope.secrets.card_month = "1"
|
||||
$scope.secrets.card_year = moment().year()
|
||||
|
||||
for card in savedCreditCards when card.is_default
|
||||
$scope.secrets.selected_card = card.id
|
||||
break
|
||||
|
||||
$scope.summary = ->
|
||||
[$scope.Checkout.paymentMethod()?.name]
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
Darkswarm.controller "CreditCardsCtrl", ($scope, $timeout, CreditCard, CreditCards, Dates) ->
|
||||
Darkswarm.controller "CreditCardsCtrl", ($scope, CreditCard, CreditCards) ->
|
||||
angular.extend(this, new FieldsetMixin($scope))
|
||||
$scope.savedCreditCards = CreditCards.saved
|
||||
$scope.setDefault = CreditCards.setDefault
|
||||
$scope.CreditCard = CreditCard
|
||||
$scope.secrets = CreditCard.secrets
|
||||
$scope.showForm = CreditCard.show
|
||||
|
||||
@@ -19,3 +19,4 @@ Darkswarm.controller "GroupPageCtrl", ($scope, group_enterprises, Enterprises, M
|
||||
|
||||
$scope.map = angular.copy MapConfiguration.options
|
||||
$scope.mapMarkers = OfnMap.enterprise_markers visible_enterprises
|
||||
$scope.embedded_layout = window.location.search.indexOf("embedded_shopfront=true") != -1
|
||||
@@ -1,12 +0,0 @@
|
||||
Darkswarm.controller "ShoppingTabsCtrl", ($scope, $controller, Navigation, $location) ->
|
||||
angular.extend this, $controller('TabsCtrl', {$scope: $scope})
|
||||
|
||||
$scope.tabs =
|
||||
about: { active: Navigation.isActive('/about') }
|
||||
producers: { active: Navigation.isActive('/producers') }
|
||||
contact: { active: Navigation.isActive('/contact') }
|
||||
groups: { active: Navigation.isActive('/groups') }
|
||||
|
||||
$scope.$on '$locationChangeStart', (event, url) ->
|
||||
tab = $location.path().replace(/^\//, '')
|
||||
$scope.tabs[tab]?.active = true
|
||||
@@ -11,8 +11,10 @@ Darkswarm.directive "shippingTypeSelector", ->
|
||||
scope.selectors =
|
||||
delivery: scope.filterSelectors.new
|
||||
icon: "ofn-i_039-delivery"
|
||||
translation_key: "hubs_delivery"
|
||||
pickup: scope.filterSelectors.new
|
||||
icon: "ofn-i_038-takeaway"
|
||||
translation_key: "hubs_pickup"
|
||||
|
||||
scope.emit = ->
|
||||
scope.shippingTypes =
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
Darkswarm.directive "embeddedTargetBlank", ->
|
||||
restrict: 'A'
|
||||
compile: (element) ->
|
||||
elems = (element.children().find("a"))
|
||||
if window.location.search.indexOf("embedded_shopfront=true") != -1
|
||||
elems.attr("target", "_blank")
|
||||
@@ -1,7 +1,13 @@
|
||||
# This class deals with displaying things in the login modal. It chooses
|
||||
# the modal tab templates and deals with switching tabs and passing data
|
||||
# between the tabs. It has direct access to the instance of the login modal,
|
||||
# and provides that access to other controllers as a service.
|
||||
|
||||
Darkswarm.factory "AuthenticationService", (Navigation, $modal, $location, Redirections, Loading)->
|
||||
|
||||
new class AuthenticationService
|
||||
selectedPath: "/login"
|
||||
modalMessage: null
|
||||
|
||||
constructor: ->
|
||||
if $location.path() in ["/login", "/signup", "/forgot"] || location.pathname is '/register/auth'
|
||||
@@ -32,6 +38,8 @@ Darkswarm.factory "AuthenticationService", (Navigation, $modal, $location, Redir
|
||||
'registration_authentication.html'
|
||||
else
|
||||
'authentication.html'
|
||||
pushMessage: (message) ->
|
||||
@modalMessage = String(message)
|
||||
|
||||
select: (path)=>
|
||||
@selectedPath = path
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
Darkswarm.factory 'CreditCards', (savedCreditCards)->
|
||||
Darkswarm.factory 'CreditCards', ($http, $filter, savedCreditCards, RailsFlashLoader)->
|
||||
new class CreditCard
|
||||
saved: savedCreditCards
|
||||
saved: $filter('orderBy')(savedCreditCards,'-is_default')
|
||||
|
||||
add: (card) ->
|
||||
@saved.push card
|
||||
|
||||
setDefault: (card) =>
|
||||
card.is_default = true
|
||||
for othercard in @saved when othercard != card
|
||||
othercard.is_default = false
|
||||
$http.put("/credit_cards/#{card.id}", is_default: true).then (data) ->
|
||||
RailsFlashLoader.loadFlash({success: t('js.default_card_updated')})
|
||||
, (response) ->
|
||||
RailsFlashLoader.loadFlash({error: response.data.flash.error})
|
||||
|
||||
@@ -3,6 +3,7 @@ Darkswarm.factory "EnterpriseModal", ($modal, $rootScope)->
|
||||
new class EnterpriseModal
|
||||
open: (enterprise)->
|
||||
scope = $rootScope.$new(true) # Spawn an isolate to contain the enterprise
|
||||
scope.embedded_layout = window.location.search.indexOf("embedded_shopfront=true") != -1
|
||||
|
||||
scope.enterprise = enterprise
|
||||
$modal.open(templateUrl: "enterprise_modal.html", scope: scope)
|
||||
|
||||
@@ -17,10 +17,9 @@ Darkswarm.factory 'Navigation', ($location, $window) ->
|
||||
@navigate(path)
|
||||
|
||||
go: (path)->
|
||||
if path.match /^http/
|
||||
$window.location.href = path
|
||||
else
|
||||
$window.location.pathname = path
|
||||
# The browser treats this like clicking on a link.
|
||||
# It works for absolute paths, relative paths and URLs alike.
|
||||
$window.location.href = path
|
||||
|
||||
reload: ->
|
||||
$window.location.reload()
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
%img.spinner{ src: "/assets/spinning-circles.svg", ng: { hide: "!imageUploader.isUploading" }}
|
||||
%img.preview{ng: {src: "{{imagePreview}}", class: "{'faded': imageUploader.isUploading}"}}
|
||||
|
||||
%label{for: 'image-upload', class: 'button'} #{t('admin.products.bulk_edit.upload_an_image')}
|
||||
%label{for: 'image-upload', class: 'button'} {{ 'admin.products.index.upload_an_image' | t }}
|
||||
%input#image-upload{hidden: true, type: 'file', 'nv-file-select' => true, uploader: "imageUploader"}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
%tags-input{ template: 'admin/tag.html',
|
||||
"placeholder" => t('admin.order_cycles.form.add_a_tag'),
|
||||
ng: { model: 'object[tagsAttr]', class: "{'limit-reached': limitReached}"},
|
||||
on: { tag: { added: 'tagAdded()', removed:'tagRemoved()' } } }
|
||||
on: { tag: { added: 'tagAdded($tag)', removed:'tagRemoved()' } } }
|
||||
%auto-complete{ ng: { if: "findTags" }, source: "findTags({query: $query})",
|
||||
template: "admin/tag_autocomplete.html",
|
||||
"min-length" => "0",
|
||||
|
||||
@@ -2,24 +2,28 @@
|
||||
%form{ ng: { controller: "ForgotCtrl", submit: "submit()" } }
|
||||
.row
|
||||
.large-12.columns
|
||||
.alert-box.success.radius{"ng-show" => "sent"}
|
||||
{{'password_reset_sent' | t}}
|
||||
.alert-box.success{"ng-show" => "sent"}
|
||||
{{ 'password_reset_sent' | t }}
|
||||
|
||||
%div{"ng-show" => "!sent"}
|
||||
.alert-box.alert{"ng-show" => "errors != null"}
|
||||
{{ errors }}
|
||||
.alert-box.success{"ng-show" => "messages != null"}
|
||||
{{ messages }}
|
||||
|
||||
.row
|
||||
.large-12.columns
|
||||
%label{for: "email"} {{'signup_email' | t}}
|
||||
%input.title.input-text{name: "email",
|
||||
type: "email",
|
||||
id: "email",
|
||||
tabindex: 1,
|
||||
"ng-model" => "spree_user.email"}
|
||||
.row
|
||||
.large-12.columns
|
||||
%input.button.primary{name: "commit",
|
||||
tabindex: "3",
|
||||
type: "submit",
|
||||
value: "{{'reset_password' | t}}"}
|
||||
.alert-box.alert{"ng-show" => "errors != null"}
|
||||
{{ errors }}
|
||||
%a{ng: {show: 'user_unconfirmed', click: 'resend_confirmation()'}}
|
||||
= t('devise.confirmations.resend_confirmation_email')
|
||||
|
||||
.row
|
||||
.large-12.columns
|
||||
%label{for: "email"} {{'signup_email' | t}}
|
||||
%input.title.input-text{name: "email",
|
||||
type: "email",
|
||||
id: "email",
|
||||
tabindex: 1,
|
||||
"ng-model" => "spree_user.email"}
|
||||
.row
|
||||
.large-12.columns
|
||||
%input.button.primary{name: "commit",
|
||||
tabindex: "3",
|
||||
type: "submit",
|
||||
value: "{{'reset_password' | t}}"}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
.about-container.pad-top
|
||||
%img.enterprise-logo{"ng-src" => "{{::enterprise.logo}}", "ng-if" => "::enterprise.logo"}
|
||||
%p.text-small{"ng-bind-html" => "::enterprise.long_description"}
|
||||
%div{"ng-bind-html" => "::enterprise.long_description"}
|
||||
.small-12.large-4.columns
|
||||
%ng-include{src: "'partials/contact.html'"}
|
||||
%ng-include{src: "'partials/follow.html'"}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
.highlight-top.row
|
||||
.small-12.medium-7.large-8.columns
|
||||
%h3{"ng-if" => "::enterprise.is_distributor"}
|
||||
%a{"ng-href" => "{{::enterprise.path}}", "ofn-change-hub" => "enterprise"}
|
||||
%a{"ng-href" => "{{::enterprise.path}}", "ofn-change-hub" => "enterprise", "ng-attr-target" => "{{ embedded_layout ? '_blank' : undefined}}"}
|
||||
%i{"ng-class" => "::enterprise.icon_font"}
|
||||
%span{"ng-bind" => "::enterprise.name"}
|
||||
%h3{"ng-if" => "::!enterprise.is_distributor", "ng-class" => "::{'is_producer' : enterprise.is_primary_producer}"}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
{{'hubs_delivery' | t}}
|
||||
.row
|
||||
.columns.small-12
|
||||
%a.cta-hub{"ng-href" => "{{::enterprise.path}}",
|
||||
%a.cta-hub{"ng-href" => "{{::enterprise.path}}", "ng-attr-target" => "{{ embedded_layout ? '_blank' : undefined}}",
|
||||
"ng-class" => "{primary: enterprise.active, secondary: !enterprise.active}",
|
||||
"ofn-change-hub" => "enterprise"}
|
||||
.hub-name{"ng-bind" => "::enterprise.name"}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
%ul.small-block-grid-2.medium-block-grid-4.large-block-grid-2
|
||||
%active-selector{"ng-repeat" => "(name, selector) in selectors"}
|
||||
%i{"ng-class" => "selector.icon"}
|
||||
{{ name | capitalize }}
|
||||
{{ selector.translation_key | t | capitalize }}
|
||||
@@ -1,3 +1,38 @@
|
||||
.product-import-introduction {
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
margin: 1.5em 0 1em;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
span.category {
|
||||
display: inline-block;
|
||||
background-color: #f3f3f3;
|
||||
padding: 0.4em 0.8em;
|
||||
margin: 0 0.4em 0.5em 0;
|
||||
}
|
||||
|
||||
table {
|
||||
|
||||
&.product-import-columns tr:hover td {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
thead th {
|
||||
text-transform: none;
|
||||
font-size: 100%;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
div.panel-section {
|
||||
|
||||
.neutral {
|
||||
@@ -15,9 +50,7 @@ div.panel-section {
|
||||
|
||||
div.panel-header {
|
||||
width: 100%;
|
||||
//font-size: 1.5em;
|
||||
clear: both;
|
||||
//border: 1px solid #ccc;
|
||||
float: left;
|
||||
padding: 0.5em;
|
||||
|
||||
@@ -68,7 +101,6 @@ div.panel-section {
|
||||
div.panel-content {
|
||||
width: 100%;
|
||||
clear: both;
|
||||
//border: 1px solid #ccc;
|
||||
margin-bottom: 0.5em;
|
||||
background-color: #f9f9f9;
|
||||
padding: 1.5em;
|
||||
@@ -87,8 +119,6 @@ div.panel-section {
|
||||
white-space: nowrap;
|
||||
}
|
||||
tr.error {
|
||||
//background-color: #ffe6e4;
|
||||
//color: #ee4728;
|
||||
color: #c84C4c;
|
||||
}
|
||||
tr:hover td.invalid {
|
||||
@@ -119,9 +149,7 @@ div.panel-section {
|
||||
margin-bottom: 0.2em;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
br.panels.clearfix {
|
||||
@@ -132,12 +160,6 @@ table.import-settings {
|
||||
background-color: transparent !important;
|
||||
width: auto;
|
||||
|
||||
//select {
|
||||
// width: 100%;
|
||||
//}
|
||||
tr {
|
||||
|
||||
}
|
||||
tbody tr:hover td {
|
||||
background-color: #f3f3f3;
|
||||
}
|
||||
@@ -159,14 +181,25 @@ table.import-settings {
|
||||
padding-right: 2.5em;
|
||||
}
|
||||
tr:first-child td {
|
||||
//border-top: 1px solid #eee;
|
||||
border-top: 0;
|
||||
}
|
||||
tr:last-child td {
|
||||
//border-top: 1px solid #eee;
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
div.select2-container {
|
||||
width: 13.5em;
|
||||
}
|
||||
div.select2-container.select2-container-disabled {
|
||||
a.select2-choice, span.select2-arrow {
|
||||
background-color: #d5d5d5;
|
||||
}
|
||||
}
|
||||
input[disabled], input:disabled {
|
||||
background-color: #d5d5d5;
|
||||
opacity: 1;
|
||||
border-color: transparent;
|
||||
color: white !important;
|
||||
}
|
||||
}
|
||||
|
||||
.panel-section.import-settings {
|
||||
@@ -189,8 +222,6 @@ table.import-settings {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
.post-save-results {
|
||||
p {
|
||||
font-size: 1.25em;
|
||||
@@ -222,4 +253,65 @@ table.import-settings {
|
||||
font-size: 1.05em;
|
||||
margin-top: 0.4em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
form.product-import, div.post-save-results, div.import-wrapper {
|
||||
input[type="submit"] {
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
input[type="submit"], button, a.button {
|
||||
min-width: 8em;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
form.product-import, div.save-results {
|
||||
transition: all linear 0.25s;
|
||||
}
|
||||
|
||||
form.product-import.ng-hide, div.save-results.ng-hide {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
div.import-wrapper {
|
||||
div.progress-interface {
|
||||
text-align: center;
|
||||
transition: all linear 0.25s;
|
||||
|
||||
button:disabled {
|
||||
background: #ccc !important;
|
||||
}
|
||||
|
||||
}
|
||||
div.progress-interface.ng-hide {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
opacity: 0;
|
||||
}
|
||||
.post-save-results {
|
||||
a.button{
|
||||
float: left;
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
div.progress-bar {
|
||||
height: 25px;
|
||||
width: 30em;
|
||||
max-width: 90%;
|
||||
margin: 1em auto;
|
||||
background: #f7f7f7;
|
||||
padding: 3px;
|
||||
border-radius: 0.3em;
|
||||
border: 1px solid #eee;
|
||||
|
||||
span.progress-track{
|
||||
display: block;
|
||||
background: #b7ea53;
|
||||
height: 100%;
|
||||
border-radius: 0.3em;
|
||||
box-shadow: inset 0 0 3px rgba(0,0,0,0.3);
|
||||
transition: width 0.5s ease-in-out;
|
||||
}
|
||||
}
|
||||
|
||||
10
app/assets/stylesheets/admin/reports.css.scss
Normal file
10
app/assets/stylesheets/admin/reports.css.scss
Normal file
@@ -0,0 +1,10 @@
|
||||
.report__table {
|
||||
margin-top: 2em;
|
||||
}
|
||||
.report__message {
|
||||
margin-top: 2em;
|
||||
border: 1px solid #cee1f4;
|
||||
border-radius: .5em;
|
||||
padding: .5em;
|
||||
text-align: center;
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
padding: 4em 2em;
|
||||
|
||||
@include fullbg;
|
||||
|
||||
background-color: black;
|
||||
background-image: url("/assets/home/tagline-bg.jpg");
|
||||
background-repeat: no-repeat;
|
||||
background-position: center center;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
@import "typography";
|
||||
|
||||
body.embedded {
|
||||
nav.top-bar {
|
||||
ul.left, ul.center, ul.right li.current_hub {
|
||||
@@ -28,6 +30,22 @@ body.embedded {
|
||||
footer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.powered-by-embedded {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.contact {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.embedded-fullwidth {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#group-page header {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
nav.top-bar ul.right li.powered-by {
|
||||
@@ -51,6 +69,17 @@ nav.top-bar ul.right li.powered-by {
|
||||
}
|
||||
}
|
||||
|
||||
.powered-by-embedded {
|
||||
opacity: 0.6;
|
||||
@include headingFont;
|
||||
font-size: 1rem;
|
||||
font-weight: 300;
|
||||
color: #555;
|
||||
padding: 0 !important;
|
||||
display: none;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.blocked-cookies {
|
||||
text-align: center;
|
||||
margin-bottom: 0 !important;
|
||||
|
||||
@@ -2,18 +2,8 @@
|
||||
@import "mixins";
|
||||
@import "branding";
|
||||
|
||||
// Foundation overrides
|
||||
#tabs .tabs-content > .content p {
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
.tabs-content > .content {
|
||||
padding-top: 0 !important;
|
||||
}
|
||||
|
||||
// Tabs styling
|
||||
|
||||
#tabs {
|
||||
.tabset-ctrl#shop-tabs {
|
||||
background: url("/assets/gray_jean.png") top left repeat;
|
||||
|
||||
@include box-shadow(inset 0 2px 3px 0 rgba(0, 0, 0, 0.15));
|
||||
@@ -21,29 +11,22 @@
|
||||
display: block;
|
||||
color: $dark-grey;
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
color: $dark-grey;
|
||||
border-bottom: 1px solid $disabled-dark;
|
||||
margin-top: 0.75rem;
|
||||
margin-bottom: 0.75rem;
|
||||
padding-bottom: 0.25rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.panel {
|
||||
border-color: $clr-brick-bright;
|
||||
background-color: rgba(255, 255, 255, 0);
|
||||
}
|
||||
|
||||
dl dd {
|
||||
.tab {
|
||||
text-align: center;
|
||||
border-top: 4px solid transparent;
|
||||
|
||||
@media all and (max-width: 640px) {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
>a {
|
||||
outline: none;
|
||||
display: block;
|
||||
background-color: #efefef;
|
||||
color: #222;
|
||||
font-family: "Oswald", sans-serif;
|
||||
}
|
||||
|
||||
a {
|
||||
@include headingFont;
|
||||
|
||||
@@ -67,15 +50,8 @@
|
||||
padding: 0.35em 0 0.65em 0;
|
||||
text-shadow: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// inactive nav link
|
||||
dl {
|
||||
dd {
|
||||
border-top: 4px solid transparent;
|
||||
|
||||
a:after {
|
||||
&:after {
|
||||
padding-left: 8px;
|
||||
content: "";
|
||||
visibility: hidden;
|
||||
@@ -88,17 +64,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
dd:hover {
|
||||
&:hover {
|
||||
a:after {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// active nav link
|
||||
|
||||
dl {
|
||||
dd.active {
|
||||
&.selected {
|
||||
border-top: 4px solid $clr-brick;
|
||||
|
||||
@media all and (max-width: 640px) {
|
||||
@@ -129,33 +101,44 @@
|
||||
|
||||
// content revealed in accordion
|
||||
|
||||
.tabs-content {
|
||||
.tab-view {
|
||||
margin-bottom: 0;
|
||||
padding: 0;
|
||||
background: none;
|
||||
border: none;
|
||||
|
||||
& > .content {
|
||||
background: none;
|
||||
border: none;
|
||||
img {
|
||||
margin: 0px 0px 0px 40px;
|
||||
}
|
||||
|
||||
img {
|
||||
margin: 0px 0px 0px 40px;
|
||||
p {
|
||||
max-width: 100%;
|
||||
|
||||
@media all and (max-width: 768px) {
|
||||
height: auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
max-width: 555px;
|
||||
ul {
|
||||
list-style-type: none;
|
||||
padding-left: none;
|
||||
}
|
||||
|
||||
@media all and (max-width: 768px) {
|
||||
height: auto !important;
|
||||
}
|
||||
}
|
||||
.header {
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
color: $dark-grey;
|
||||
border-bottom: 1px solid $disabled-dark;
|
||||
margin-top: 0.75rem;
|
||||
margin-bottom: 0.75rem;
|
||||
padding-bottom: 0.25rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style-type: none;
|
||||
padding-left: none;
|
||||
}
|
||||
|
||||
.panel {
|
||||
padding-bottom: 1.25em;
|
||||
}
|
||||
.panel {
|
||||
padding-bottom: 1.25em;
|
||||
border-color: $clr-brick-bright;
|
||||
background-color: rgba(255, 255, 255, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
@import "mixins";
|
||||
@import "branding";
|
||||
|
||||
.tabset-ctrl {
|
||||
.tabset-ctrl:not(#shop-tabs) {
|
||||
.tab-view {
|
||||
padding-top: 30px;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
module Admin
|
||||
class ManagerInvitationsController < Spree::Admin::BaseController
|
||||
authorize_resource class: false
|
||||
|
||||
def create
|
||||
@email = params[:email]
|
||||
@enterprise = Enterprise.find(params[:enterprise_id])
|
||||
@@ -28,6 +30,8 @@ module Admin
|
||||
password = Devise.friendly_token
|
||||
new_user = Spree::User.create(email: @email, unconfirmed_email: @email, password: password)
|
||||
new_user.reset_password_token = Devise.friendly_token
|
||||
# Same time as used in Devise's lib/devise/models/recoverable.rb.
|
||||
new_user.reset_password_sent_at = Time.now.utc
|
||||
new_user.save!
|
||||
|
||||
@enterprise.users << new_user
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
require 'open_food_network/permissions'
|
||||
require 'open_food_network/order_cycle_form_applicator'
|
||||
require 'open_food_network/proxy_order_syncer'
|
||||
|
||||
module Admin
|
||||
class OrderCyclesController < ResourceController
|
||||
include OrderCyclesHelper
|
||||
@@ -9,17 +5,14 @@ module Admin
|
||||
prepend_before_filter :load_data_for_index, :only => :index
|
||||
before_filter :require_coordinator, only: :new
|
||||
before_filter :remove_protected_attrs, only: [:update]
|
||||
before_filter :check_editable_schedule_ids, only: [:create, :update]
|
||||
before_filter :require_order_cycle_set_params, only: [:bulk_update]
|
||||
around_filter :protect_invalid_destroy, only: :destroy
|
||||
create.after :sync_subscriptions
|
||||
update.after :sync_subscriptions
|
||||
|
||||
def index
|
||||
respond_to do |format|
|
||||
format.html
|
||||
format.json do
|
||||
render_as_json @collection, ams_prefix: params[:ams_prefix], current_user: spree_current_user
|
||||
render_as_json @collection, ams_prefix: params[:ams_prefix], current_user: spree_current_user, subscriptions_count: SubscriptionsCount.new(@collection)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -43,51 +36,36 @@ module Admin
|
||||
end
|
||||
|
||||
def create
|
||||
@order_cycle = OrderCycle.new(params[:order_cycle])
|
||||
@order_cycle_form = OrderCycleForm.new(@order_cycle, params, spree_current_user)
|
||||
|
||||
respond_to do |format|
|
||||
if @order_cycle.save
|
||||
OpenFoodNetwork::OrderCycleFormApplicator.new(@order_cycle, spree_current_user).go!
|
||||
invoke_callbacks(:create, :after)
|
||||
flash[:notice] = I18n.t(:order_cycles_create_notice)
|
||||
format.html { redirect_to admin_order_cycles_path }
|
||||
format.json { render :json => { success: true } }
|
||||
else
|
||||
format.html
|
||||
format.json { render :json => { errors: @order_cycle.errors.full_messages }, status: :unprocessable_entity }
|
||||
end
|
||||
if @order_cycle_form.save
|
||||
flash[:notice] = I18n.t(:order_cycles_create_notice)
|
||||
render json: { success: true }
|
||||
else
|
||||
render json: { errors: @order_cycle.errors.full_messages }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
@order_cycle = OrderCycle.find params[:id]
|
||||
@order_cycle_form = OrderCycleForm.new(@order_cycle, params, spree_current_user)
|
||||
|
||||
respond_to do |format|
|
||||
if @order_cycle.update_attributes(params[:order_cycle])
|
||||
unless params[:order_cycle][:incoming_exchanges].nil? && params[:order_cycle][:outgoing_exchanges].nil?
|
||||
# Only update apply exchange information if it is actually submmitted
|
||||
OpenFoodNetwork::OrderCycleFormApplicator.new(@order_cycle, spree_current_user).go!
|
||||
end
|
||||
invoke_callbacks(:update, :after)
|
||||
if @order_cycle_form.save
|
||||
respond_to do |format|
|
||||
flash[:notice] = I18n.t(:order_cycles_update_notice) if params[:reloading] == '1'
|
||||
format.html { redirect_to main_app.edit_admin_order_cycle_path(@order_cycle) }
|
||||
format.json { render :json => { :success => true } }
|
||||
else
|
||||
format.json { render :json => { errors: @order_cycle.errors.full_messages }, status: :unprocessable_entity }
|
||||
format.json { render json: { :success => true } }
|
||||
end
|
||||
else
|
||||
render json: { errors: @order_cycle.errors.full_messages }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def bulk_update
|
||||
if order_cycle_set.andand.save
|
||||
respond_to do |format|
|
||||
format.json { render_as_json @order_cycles, ams_prefix: 'index', current_user: spree_current_user }
|
||||
end
|
||||
render_as_json @order_cycles, ams_prefix: 'index', current_user: spree_current_user, subscriptions_count: SubscriptionsCount.new(@collection)
|
||||
else
|
||||
respond_to do |format|
|
||||
order_cycle = order_cycle_set.collection.find{ |oc| oc.errors.present? }
|
||||
format.json { render :json => { errors: order_cycle.errors.full_messages }, status: :unprocessable_entity }
|
||||
end
|
||||
order_cycle = order_cycle_set.collection.find{ |oc| oc.errors.present? }
|
||||
render json: { errors: order_cycle.errors.full_messages }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
@@ -193,29 +171,6 @@ module Admin
|
||||
end
|
||||
end
|
||||
|
||||
def check_editable_schedule_ids
|
||||
return unless params[:order_cycle][:schedule_ids]
|
||||
requested = params[:order_cycle][:schedule_ids].map(&:to_i)
|
||||
@existing_schedule_ids = @order_cycle.persisted? ? @order_cycle.schedule_ids : []
|
||||
permitted = Schedule.where(id: requested | @existing_schedule_ids).merge(OpenFoodNetwork::Permissions.new(spree_current_user).editable_schedules).pluck(:id)
|
||||
result = @existing_schedule_ids
|
||||
result |= (requested & permitted) # add any requested & permitted ids
|
||||
result -= ((result & permitted) - requested) # remove any existing and permitted ids that were not specifically requested
|
||||
params[:order_cycle][:schedule_ids] = result
|
||||
end
|
||||
|
||||
def sync_subscriptions
|
||||
return unless params[:order_cycle][:schedule_ids]
|
||||
removed_ids = @existing_schedule_ids - @order_cycle.schedule_ids
|
||||
new_ids = @order_cycle.schedule_ids - @existing_schedule_ids
|
||||
if removed_ids.any? || new_ids.any?
|
||||
schedules = Schedule.where(id: removed_ids + new_ids)
|
||||
subscriptions = Subscription.where(schedule_id: schedules)
|
||||
syncer = OpenFoodNetwork::ProxyOrderSyncer.new(subscriptions)
|
||||
syncer.sync!
|
||||
end
|
||||
end
|
||||
|
||||
def order_cycles_from_set
|
||||
remove_unauthorized_bulk_attrs
|
||||
OrderCycle.where(id: params[:order_cycle_set][:collection_attributes].map{ |k,v| v[:id] })
|
||||
|
||||
@@ -1,62 +1,95 @@
|
||||
require 'roo'
|
||||
|
||||
class Admin::ProductImportController < Spree::Admin::BaseController
|
||||
module Admin
|
||||
class ProductImportController < Spree::Admin::BaseController
|
||||
before_filter :validate_upload_presence, except: %i[index guide validate_data]
|
||||
|
||||
before_filter :validate_upload_presence, except: :index
|
||||
|
||||
def import
|
||||
# Save uploaded file to tmp directory
|
||||
@filepath = save_uploaded_file(params[:file])
|
||||
@importer = ProductImporter.new(File.new(@filepath), editable_enterprises)
|
||||
|
||||
check_file_errors @importer
|
||||
check_spreadsheet_has_data @importer
|
||||
|
||||
@tax_categories = Spree::TaxCategory.order('is_default DESC, name ASC')
|
||||
@shipping_categories = Spree::ShippingCategory.order('name ASC')
|
||||
end
|
||||
|
||||
def save
|
||||
@importer = ProductImporter.new(File.new(params[:filepath]), editable_enterprises, params[:settings])
|
||||
@importer.save_all if @importer.has_valid_entries?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def validate_upload_presence
|
||||
unless params[:file] || (params[:filepath] && File.exist?(params[:filepath]))
|
||||
redirect_to '/admin/product_import', notice: I18n.t(:product_import_file_not_found_notice)
|
||||
return
|
||||
def guide
|
||||
@product_categories = Spree::Taxon.order('name ASC').pluck(:name).uniq
|
||||
@tax_categories = Spree::TaxCategory.order('name ASC').pluck(:name)
|
||||
@shipping_categories = Spree::ShippingCategory.order('name ASC').pluck(:name)
|
||||
end
|
||||
end
|
||||
|
||||
def check_file_errors(importer)
|
||||
if importer.errors.present?
|
||||
redirect_to '/admin/product_import', notice: @importer.errors.full_messages.to_sentence
|
||||
return
|
||||
def import
|
||||
# Save uploaded file to tmp directory
|
||||
@filepath = save_uploaded_file(params[:file])
|
||||
@importer = ProductImport::ProductImporter.new(File.new(@filepath), spree_current_user, params[:settings])
|
||||
@original_filename = params[:file].try(:original_filename)
|
||||
|
||||
check_file_errors @importer
|
||||
check_spreadsheet_has_data @importer
|
||||
|
||||
@tax_categories = Spree::TaxCategory.order('is_default DESC, name ASC')
|
||||
@shipping_categories = Spree::ShippingCategory.order('name ASC')
|
||||
end
|
||||
end
|
||||
|
||||
def check_spreadsheet_has_data(importer)
|
||||
unless importer.item_count
|
||||
redirect_to '/admin/product_import', notice: I18n.t(:product_import_no_data_in_spreadsheet_notice)
|
||||
return
|
||||
def validate_data
|
||||
return unless process_data('validate')
|
||||
render json: @importer.import_results, response: 200
|
||||
end
|
||||
end
|
||||
|
||||
def save_uploaded_file(upload)
|
||||
filename = 'import' + Time.now.strftime('%d-%m-%Y-%H-%M-%S')
|
||||
extension = '.' + upload.original_filename.split('.').last
|
||||
directory = 'tmp/product_import'
|
||||
Dir.mkdir(directory) unless File.exists?(directory)
|
||||
File.open(Rails.root.join(directory, filename+extension), 'wb') do |f|
|
||||
f.write(upload.read)
|
||||
f.path
|
||||
def save_data
|
||||
return unless process_data('save')
|
||||
render json: @importer.save_results, response: 200
|
||||
end
|
||||
end
|
||||
|
||||
# Define custom model class for Cancan permissions
|
||||
def model_class
|
||||
ProductImporter
|
||||
def reset_absent_products
|
||||
@importer = ProductImport::ProductImporter.new(File.new(params[:filepath]), spree_current_user, import_into: params[:import_into], enterprises_to_reset: params[:enterprises_to_reset], updated_ids: params[:updated_ids], settings: params[:settings])
|
||||
|
||||
if params.key?(:enterprises_to_reset) && params.key?(:updated_ids)
|
||||
@importer.reset_absent(params[:updated_ids])
|
||||
end
|
||||
|
||||
render json: @importer.products_reset_count
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def validate_upload_presence
|
||||
unless params[:file] || (params[:filepath] && File.exist?(params[:filepath]))
|
||||
redirect_to '/admin/product_import', notice: I18n.t(:product_import_file_not_found_notice)
|
||||
end
|
||||
end
|
||||
|
||||
def process_data(method)
|
||||
@importer = ProductImport::ProductImporter.new(File.new(params[:filepath]), spree_current_user, start: params[:start], end: params[:end], settings: params[:settings])
|
||||
|
||||
begin
|
||||
@importer.send("#{method}_entries")
|
||||
rescue StandardError => e
|
||||
render json: e.message, response: 500
|
||||
return false
|
||||
end
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
def check_file_errors(importer)
|
||||
if importer.errors.present?
|
||||
redirect_to '/admin/product_import', notice: @importer.errors.full_messages.to_sentence
|
||||
end
|
||||
end
|
||||
|
||||
def check_spreadsheet_has_data(importer)
|
||||
unless importer.item_count
|
||||
redirect_to '/admin/product_import', notice: I18n.t(:product_import_no_data_in_spreadsheet_notice)
|
||||
end
|
||||
end
|
||||
|
||||
def save_uploaded_file(upload)
|
||||
filename = 'import' + Time.zone.now.strftime('%d-%m-%Y-%H-%M-%S')
|
||||
extension = '.' + upload.original_filename.split('.').last
|
||||
directory = 'tmp/product_import'
|
||||
Dir.mkdir(directory) unless File.exist?(directory)
|
||||
File.open(Rails.root.join(directory, filename + extension), 'wb') do |f|
|
||||
f.write(upload.read)
|
||||
f.path
|
||||
end
|
||||
end
|
||||
|
||||
# Define custom model class for Cancan permissions
|
||||
def model_class
|
||||
ProductImport::ProductImporter
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -3,6 +3,7 @@ require 'open_food_network/spree_api_key_loader'
|
||||
module Admin
|
||||
class VariantOverridesController < ResourceController
|
||||
include OpenFoodNetwork::SpreeApiKeyLoader
|
||||
include EnterprisesHelper
|
||||
|
||||
prepend_before_filter :load_data
|
||||
before_filter :load_collection, only: [:bulk_update]
|
||||
@@ -55,6 +56,20 @@ module Admin
|
||||
variant_override_enterprises_per_hub
|
||||
|
||||
@inventory_items = InventoryItem.where(enterprise_id: @hubs)
|
||||
@import_dates = inventory_import_dates.uniq.to_json
|
||||
end
|
||||
|
||||
def inventory_import_dates
|
||||
import_dates = VariantOverride.
|
||||
select('DISTINCT variant_overrides.import_date').
|
||||
where('variant_overrides.hub_id IN (?)
|
||||
AND variant_overrides.import_date IS NOT NULL', editable_enterprises.collect(&:id)).
|
||||
order('import_date DESC')
|
||||
|
||||
options = [{ id: '0', name: 'All' }]
|
||||
import_dates.collect(&:import_date).map { |i| options.push(id: i.to_date, name: i.to_date.to_formatted_s(:long)) }
|
||||
|
||||
options
|
||||
end
|
||||
|
||||
def load_collection
|
||||
|
||||
@@ -4,6 +4,7 @@ class ApplicationController < ActionController::Base
|
||||
protect_from_forgery
|
||||
|
||||
prepend_before_filter :restrict_iframes
|
||||
before_filter :set_cache_headers # Issue #1213, prevent cart emptying via cache when using back button
|
||||
|
||||
include EnterprisesHelper
|
||||
helper CssSplitter::ApplicationHelper
|
||||
@@ -59,7 +60,7 @@ class ApplicationController < ActionController::Base
|
||||
return if embedding_without_https?
|
||||
|
||||
response.headers.delete 'X-Frame-Options'
|
||||
response.headers['Content-Security-Policy'] = "frame-ancestors #{embedded_shopfront_referer}"
|
||||
response.headers['Content-Security-Policy'] = "frame-ancestors #{URI(request.referer).host.downcase}"
|
||||
|
||||
check_embedded_request
|
||||
set_embedded_layout
|
||||
@@ -152,4 +153,10 @@ class ApplicationController < ActionController::Base
|
||||
nil
|
||||
end
|
||||
|
||||
def set_cache_headers # https://jacopretorius.net/2014/01/force-page-to-reload-on-browser-back-in-rails.html
|
||||
response.headers["Cache-Control"] = "no-cache, no-store, max-age=0, must-revalidate"
|
||||
response.headers["Pragma"] = "no-cache"
|
||||
response.headers["Expires"] = "Fri, 01 Jan 1990 00:00:00 GMT"
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
@@ -208,7 +208,7 @@ class CheckoutController < Spree::CheckoutController
|
||||
payment_method = Spree::PaymentMethod.find(params[:order][:payments_attributes].first[:payment_method_id])
|
||||
return unless payment_method.kind_of?(Spree::Gateway::PayPalExpress)
|
||||
|
||||
render json: {path: spree.paypal_express_url(payment_method_id: payment_method.id)}, status: 200
|
||||
render json: {path: spree.paypal_express_path(payment_method_id: payment_method.id)}, status: 200
|
||||
true
|
||||
end
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ class ShopController < BaseController
|
||||
if request.post?
|
||||
if oc = OrderCycle.with_distributor(@distributor).active.find_by_id(params[:order_cycle_id])
|
||||
current_order(true).set_order_cycle! oc
|
||||
@current_order_cycle = oc
|
||||
render partial: "json/order_cycle"
|
||||
else
|
||||
render status: 404, json: ""
|
||||
|
||||
@@ -1,7 +1,20 @@
|
||||
Spree::Admin::Orders::CustomerDetailsController.class_eval do
|
||||
before_filter :set_guest_checkout_status, only: :update
|
||||
|
||||
# Inherit CanCan permissions for the current order
|
||||
def model_class
|
||||
load_order unless @order
|
||||
@order
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_guest_checkout_status
|
||||
registered_user = Spree::User.find_by_email(params[:order][:email])
|
||||
|
||||
params[:order][:guest_checkout] = registered_user.nil?
|
||||
|
||||
return unless registered_user
|
||||
@order.user_id = registered_user.id
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,26 +1,74 @@
|
||||
Spree::Admin::OverviewController.class_eval do
|
||||
def index
|
||||
# TODO was sorted with is_distributor DESC as well, not sure why or how we want ot sort this now
|
||||
@enterprises = Enterprise.managed_by(spree_current_user).order('is_primary_producer ASC, name')
|
||||
@enterprises = Enterprise
|
||||
.managed_by(spree_current_user)
|
||||
.order('is_primary_producer ASC, name')
|
||||
@product_count = Spree::Product.active.managed_by(spree_current_user).count
|
||||
@order_cycle_count = OrderCycle.active.managed_by(spree_current_user).count
|
||||
|
||||
unspecified = spree_current_user.owned_enterprises.where(sells: 'unspecified')
|
||||
outside_referral = !URI(request.referer.to_s).path.match(/^\/admin/)
|
||||
|
||||
if OpenFoodNetwork::Permissions.new(spree_current_user).manages_one_enterprise? && !spree_current_user.admin?
|
||||
@enterprise = @enterprises.first
|
||||
if outside_referral && unspecified.any?
|
||||
redirect_to main_app.welcome_admin_enterprise_path(@enterprise)
|
||||
else
|
||||
render "single_enterprise_dashboard"
|
||||
end
|
||||
if first_access
|
||||
redirect_to enterprises_path
|
||||
else
|
||||
if outside_referral && unspecified.any?
|
||||
redirect_to main_app.admin_enterprises_path
|
||||
else
|
||||
render "multi_enterprise_dashboard"
|
||||
end
|
||||
render dashboard_view
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Checks whether the user is accessing the admin for the first time
|
||||
#
|
||||
# @return [Boolean]
|
||||
def first_access
|
||||
outside_referral && incomplete_enterprise_registration?
|
||||
end
|
||||
|
||||
# Checks whether the request comes from another admin page or not
|
||||
#
|
||||
# @return [Boolean]
|
||||
def outside_referral
|
||||
!URI(request.referer.to_s).path.match(%r{/admin})
|
||||
end
|
||||
|
||||
# Checks that all of the enterprises owned by the current user have a 'sells'
|
||||
# property specified, which indicates that the registration process has been
|
||||
# completed
|
||||
#
|
||||
# @return [Boolean]
|
||||
def incomplete_enterprise_registration?
|
||||
@incomplete_enterprise_registration ||= spree_current_user
|
||||
.owned_enterprises
|
||||
.where(sells: 'unspecified')
|
||||
.exists?
|
||||
end
|
||||
|
||||
# Returns the appropriate enterprise path for the current user
|
||||
#
|
||||
# @return [String]
|
||||
def enterprises_path
|
||||
if managed_enterprises.size == 1
|
||||
@enterprise = @enterprises.first
|
||||
main_app.welcome_admin_enterprise_path(@enterprise)
|
||||
else
|
||||
main_app.admin_enterprises_path
|
||||
end
|
||||
end
|
||||
|
||||
# Returns the appropriate dashboard view for the current user
|
||||
#
|
||||
# @return [String]
|
||||
def dashboard_view
|
||||
if managed_enterprises.size == 1
|
||||
@enterprise = @enterprises.first
|
||||
:single_enterprise_dashboard
|
||||
else
|
||||
:multi_enterprise_dashboard
|
||||
end
|
||||
end
|
||||
|
||||
# Returns the list of enterprises the current user is manager of
|
||||
#
|
||||
# @return [ActiveRecord::Relation<Enterprise>]
|
||||
def managed_enterprises
|
||||
spree_current_user.enterprises
|
||||
end
|
||||
end
|
||||
|
||||
@@ -4,8 +4,11 @@ require 'open_food_network/referer_parser'
|
||||
Spree::Admin::ProductsController.class_eval do
|
||||
include OpenFoodNetwork::SpreeApiKeyLoader
|
||||
include OrderCyclesHelper
|
||||
before_filter :load_form_data, :only => [:bulk_edit, :new, :create, :edit, :update]
|
||||
before_filter :load_spree_api_key, :only => [:bulk_edit, :variant_overrides]
|
||||
include EnterprisesHelper
|
||||
|
||||
before_filter :load_data
|
||||
before_filter :load_form_data, :only => [:index, :new, :create, :edit, :update]
|
||||
before_filter :load_spree_api_key, :only => [:index, :variant_overrides]
|
||||
before_filter :strip_new_properties, only: [:create, :update]
|
||||
|
||||
respond_override create: { html: {
|
||||
@@ -13,7 +16,7 @@ Spree::Admin::ProductsController.class_eval do
|
||||
if params[:button] == "add_another"
|
||||
redirect_to new_admin_product_path
|
||||
else
|
||||
redirect_to '/admin/products/bulk_edit'
|
||||
redirect_to admin_products_path
|
||||
end
|
||||
},
|
||||
failure: lambda {
|
||||
@@ -23,6 +26,11 @@ Spree::Admin::ProductsController.class_eval do
|
||||
def product_distributions
|
||||
end
|
||||
|
||||
def index
|
||||
@current_user = spree_current_user
|
||||
@show_latest_import = params[:latest_import] || false
|
||||
end
|
||||
|
||||
def bulk_update
|
||||
collection_hash = Hash[params[:products].each_with_index.map { |p,i| [i,p] }]
|
||||
product_set = Spree::ProductSet.new({:collection_attributes => collection_hash})
|
||||
@@ -49,17 +57,6 @@ Spree::Admin::ProductsController.class_eval do
|
||||
|
||||
protected
|
||||
|
||||
def location_after_save_with_bulk_edit
|
||||
referer_path = OpenFoodNetwork::RefererParser::path(request.referer)
|
||||
|
||||
if referer_path == '/admin/products/bulk_edit'
|
||||
bulk_edit_admin_products_url
|
||||
else
|
||||
location_after_save_without_bulk_edit
|
||||
end
|
||||
end
|
||||
alias_method_chain :location_after_save, :bulk_edit
|
||||
|
||||
def collection
|
||||
# This method is copied directly from the spree product controller, except where we narrow the search below with the managed_by search to support
|
||||
# enterprise users.
|
||||
@@ -86,7 +83,7 @@ Spree::Admin::ProductsController.class_eval do
|
||||
end
|
||||
|
||||
def collection_actions
|
||||
[:index, :bulk_edit, :bulk_update]
|
||||
[:index, :bulk_update]
|
||||
end
|
||||
|
||||
|
||||
@@ -95,6 +92,23 @@ Spree::Admin::ProductsController.class_eval do
|
||||
def load_form_data
|
||||
@producers = OpenFoodNetwork::Permissions.new(spree_current_user).managed_product_enterprises.is_primary_producer.by_name
|
||||
@taxons = Spree::Taxon.order(:name)
|
||||
@import_dates = product_import_dates.uniq.to_json
|
||||
end
|
||||
|
||||
def product_import_dates
|
||||
import_dates = Spree::Variant.
|
||||
select('DISTINCT spree_variants.import_date').
|
||||
joins(:product).
|
||||
where('spree_products.supplier_id IN (?)', editable_enterprises.collect(&:id)).
|
||||
where('spree_variants.import_date IS NOT NULL').
|
||||
where(spree_variants: {is_master: false}).
|
||||
where(spree_variants: {deleted_at: nil}).
|
||||
order('spree_variants.import_date DESC')
|
||||
|
||||
options = [{id: '0', name: ''}]
|
||||
import_dates.collect(&:import_date).map { |i| options.push(id: i.to_date, name: i.to_date.to_formatted_s(:long)) }
|
||||
|
||||
options
|
||||
end
|
||||
|
||||
def strip_new_properties
|
||||
|
||||
@@ -15,9 +15,11 @@ require 'open_food_network/payments_report'
|
||||
require 'open_food_network/orders_and_fulfillments_report'
|
||||
|
||||
Spree::Admin::ReportsController.class_eval do
|
||||
|
||||
include Spree::ReportsHelper
|
||||
|
||||
helper_method :render_content?
|
||||
|
||||
before_filter :cache_search_state
|
||||
# Fetches user's distributors, suppliers and order_cycles
|
||||
before_filter :load_data, only: [:customers, :products_and_inventory, :order_cycle_management, :packing]
|
||||
|
||||
@@ -53,22 +55,21 @@ Spree::Admin::ReportsController.class_eval do
|
||||
}
|
||||
end
|
||||
|
||||
# Overide spree reports list.
|
||||
# Override spree reports list.
|
||||
def index
|
||||
@reports = authorized_reports
|
||||
respond_with(@reports)
|
||||
end
|
||||
|
||||
# This action is short because we refactored it like bosses
|
||||
def customers
|
||||
@report_types = report_types[:customers]
|
||||
@report_type = params[:report_type]
|
||||
@report = OpenFoodNetwork::CustomersReport.new spree_current_user, params
|
||||
@report = OpenFoodNetwork::CustomersReport.new spree_current_user, params, render_content?
|
||||
render_report(@report.header, @report.table, params[:csv], "customers_#{timestamp}.csv")
|
||||
end
|
||||
|
||||
def order_cycle_management
|
||||
prepare_date_params params
|
||||
params[:q] ||= {}
|
||||
|
||||
# -- Prepare form options
|
||||
my_distributors = Enterprise.is_distributor.managed_by(spree_current_user)
|
||||
@@ -84,15 +85,14 @@ Spree::Admin::ReportsController.class_eval do
|
||||
@report_type = params[:report_type]
|
||||
|
||||
# -- Build Report with Order Grouper
|
||||
@report = OpenFoodNetwork::OrderCycleManagementReport.new spree_current_user, params
|
||||
@report = OpenFoodNetwork::OrderCycleManagementReport.new spree_current_user, params, render_content?
|
||||
@table = @report.table_items
|
||||
|
||||
render_report(@report.header, @table, params[:csv], "order_cycle_management_#{timestamp}.csv")
|
||||
end
|
||||
|
||||
def packing
|
||||
# -- Prepare date parameters
|
||||
prepare_date_params params
|
||||
params[:q] ||= {}
|
||||
|
||||
# -- Prepare form options
|
||||
my_distributors = Enterprise.is_distributor.managed_by(spree_current_user)
|
||||
@@ -107,7 +107,7 @@ Spree::Admin::ReportsController.class_eval do
|
||||
@report_type = params[:report_type]
|
||||
|
||||
# -- Build Report with Order Grouper
|
||||
@report = OpenFoodNetwork::PackingReport.new spree_current_user, params
|
||||
@report = OpenFoodNetwork::PackingReport.new spree_current_user, params, render_content?
|
||||
order_grouper = OpenFoodNetwork::OrderGrouper.new @report.rules, @report.columns
|
||||
@table = order_grouper.table(@report.table_items)
|
||||
|
||||
@@ -115,64 +115,26 @@ Spree::Admin::ReportsController.class_eval do
|
||||
end
|
||||
|
||||
def orders_and_distributors
|
||||
prepare_date_params params
|
||||
|
||||
permissions = OpenFoodNetwork::Permissions.new(spree_current_user)
|
||||
@search = permissions.visible_orders.complete.not_state(:canceled).search(params[:q])
|
||||
orders = @search.result
|
||||
|
||||
# If empty array is passed in, the where clause will return all line_items, which is bad
|
||||
orders_with_hidden_details =
|
||||
permissions.editable_orders.empty? ? orders : orders.where('id NOT IN (?)', permissions.editable_orders)
|
||||
|
||||
orders.select{ |order| orders_with_hidden_details.include? order }.each do |order|
|
||||
# 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
|
||||
order.bill_address.andand.assign_attributes(firstname: I18n.t('admin.reports.hidden'), lastname: "", phone: "", address1: "", address2: "", city: "", zipcode: "", state: nil)
|
||||
order.ship_address.andand.assign_attributes(firstname: I18n.t('admin.reports.hidden'), lastname: "", phone: "", address1: "", address2: "", city: "", zipcode: "", state: nil)
|
||||
order.assign_attributes(email: I18n.t('admin.reports.hidden'))
|
||||
end
|
||||
|
||||
@report = OpenFoodNetwork::OrderAndDistributorReport.new orders
|
||||
unless params[:csv]
|
||||
render :html => @report
|
||||
else
|
||||
csv_string = CSV.generate do |csv|
|
||||
csv << @report.header
|
||||
@report.table.each { |row| csv << row }
|
||||
end
|
||||
send_data csv_string, :filename => "orders_and_distributors_#{timestamp}.csv"
|
||||
end
|
||||
@report = OpenFoodNetwork::OrderAndDistributorReport.new spree_current_user, params, render_content?
|
||||
@search = @report.search
|
||||
csv_file_name = "orders_and_distributors_#{timestamp}.csv"
|
||||
render_report(@report.header, @report.table, params[:csv], csv_file_name)
|
||||
end
|
||||
|
||||
def sales_tax
|
||||
prepare_date_params params
|
||||
@distributors = Enterprise.is_distributor.managed_by(spree_current_user)
|
||||
@report_type = params[:report_type]
|
||||
|
||||
@report = OpenFoodNetwork::SalesTaxReport.new spree_current_user, params
|
||||
|
||||
unless params[:csv]
|
||||
render :html => @report
|
||||
else
|
||||
csv_string = CSV.generate do |csv|
|
||||
csv << @report.header
|
||||
@report.table.each { |row| csv << row }
|
||||
end
|
||||
send_data csv_string, :filename => "sales_tax.csv"
|
||||
end
|
||||
@report = OpenFoodNetwork::SalesTaxReport.new spree_current_user, params, render_content?
|
||||
render_report(@report.header, @report.table, params[:csv], "sales_tax.csv")
|
||||
end
|
||||
|
||||
def bulk_coop
|
||||
# -- Prepare date parameters
|
||||
prepare_date_params params
|
||||
|
||||
# -- Prepare form options
|
||||
@distributors = Enterprise.is_distributor.managed_by(spree_current_user)
|
||||
@report_type = params[:report_type]
|
||||
|
||||
# -- Build Report with Order Grouper
|
||||
@report = OpenFoodNetwork::BulkCoopReport.new spree_current_user, params
|
||||
@report = OpenFoodNetwork::BulkCoopReport.new spree_current_user, params, render_content?
|
||||
order_grouper = OpenFoodNetwork::OrderGrouper.new @report.rules, @report.columns
|
||||
@table = order_grouper.table(@report.table_items)
|
||||
csv_file_name = "bulk_coop_#{params[:report_type]}_#{timestamp}.csv"
|
||||
@@ -181,15 +143,12 @@ Spree::Admin::ReportsController.class_eval do
|
||||
end
|
||||
|
||||
def payments
|
||||
# -- Prepare Date Params
|
||||
prepare_date_params params
|
||||
|
||||
# -- Prepare Form Options
|
||||
@distributors = Enterprise.is_distributor.managed_by(spree_current_user)
|
||||
@report_type = params[:report_type]
|
||||
|
||||
# -- Build Report with Order Grouper
|
||||
@report = OpenFoodNetwork::PaymentsReport.new spree_current_user, params
|
||||
@report = OpenFoodNetwork::PaymentsReport.new spree_current_user, params, render_content?
|
||||
order_grouper = OpenFoodNetwork::OrderGrouper.new @report.rules, @report.columns
|
||||
@table = order_grouper.table(@report.table_items)
|
||||
csv_file_name = "payments_#{timestamp}.csv"
|
||||
@@ -198,8 +157,7 @@ Spree::Admin::ReportsController.class_eval do
|
||||
end
|
||||
|
||||
def orders_and_fulfillment
|
||||
# -- Prepare Date Params
|
||||
prepare_date_params params
|
||||
params[:q] ||= {}
|
||||
|
||||
# -- Prepare Form Options
|
||||
permissions = OpenFoodNetwork::Permissions.new(spree_current_user)
|
||||
@@ -216,7 +174,7 @@ Spree::Admin::ReportsController.class_eval do
|
||||
@include_blank = I18n.t(:all)
|
||||
|
||||
# -- Build Report with Order Grouper
|
||||
@report = OpenFoodNetwork::OrdersAndFulfillmentsReport.new spree_current_user, params
|
||||
@report = OpenFoodNetwork::OrdersAndFulfillmentsReport.new spree_current_user, params, render_content?
|
||||
order_grouper = OpenFoodNetwork::OrderGrouper.new @report.rules, @report.columns
|
||||
@table = order_grouper.table(@report.table_items)
|
||||
csv_file_name = "#{params[:report_type]}_#{timestamp}.csv"
|
||||
@@ -226,60 +184,73 @@ Spree::Admin::ReportsController.class_eval do
|
||||
|
||||
def products_and_inventory
|
||||
@report_types = report_types[:products_and_inventory]
|
||||
if params[:report_type] != 'lettuce_share'
|
||||
@report = OpenFoodNetwork::ProductsAndInventoryReport.new spree_current_user, params
|
||||
else
|
||||
@report = OpenFoodNetwork::LettuceShareReport.new spree_current_user, params
|
||||
end
|
||||
@report = if params[:report_type] != 'lettuce_share'
|
||||
OpenFoodNetwork::ProductsAndInventoryReport.new spree_current_user, params, render_content?
|
||||
else
|
||||
OpenFoodNetwork::LettuceShareReport.new spree_current_user, params, render_content?
|
||||
end
|
||||
render_report(@report.header, @report.table, params[:csv], "products_and_inventory_#{timestamp}.csv")
|
||||
end
|
||||
|
||||
def users_and_enterprises
|
||||
# @report_types = report_types[:users_and_enterprises]
|
||||
@report = OpenFoodNetwork::UsersAndEnterprisesReport.new params
|
||||
@report = OpenFoodNetwork::UsersAndEnterprisesReport.new params, render_content?
|
||||
render_report(@report.header, @report.table, params[:csv], "users_and_enterprises_#{timestamp}.csv")
|
||||
end
|
||||
|
||||
def xero_invoices
|
||||
if request.get?
|
||||
params[:q] ||= {}
|
||||
params[:q][:completed_at_gt] = Time.zone.today.beginning_of_month
|
||||
params[:invoice_date] = Time.zone.today
|
||||
params[:due_date] = Time.zone.today + 1.month
|
||||
end
|
||||
params[:q] ||= {}
|
||||
|
||||
@distributors = Enterprise.is_distributor.managed_by(spree_current_user)
|
||||
@order_cycles = OrderCycle.active_or_complete.accessible_by(spree_current_user).order('orders_close_at DESC')
|
||||
|
||||
@report = OpenFoodNetwork::XeroInvoicesReport.new spree_current_user, params
|
||||
@report = OpenFoodNetwork::XeroInvoicesReport.new spree_current_user, params, render_content?
|
||||
render_report(@report.header, @report.table, params[:csv], "xero_invoices_#{timestamp}.csv")
|
||||
end
|
||||
|
||||
def render_report(header, table, create_csv, csv_file_name)
|
||||
unless create_csv
|
||||
render :html => table
|
||||
else
|
||||
csv_string = CSV.generate do |csv|
|
||||
csv << header
|
||||
table.each { |row| csv << row }
|
||||
end
|
||||
send_data csv_string, :filename => csv_file_name
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def prepare_date_params(params)
|
||||
# -- Prepare parameters
|
||||
params[:q] ||= {}
|
||||
if params[:q][:completed_at_gt].blank?
|
||||
params[:q][:completed_at_gt] = Time.zone.now.beginning_of_month
|
||||
else
|
||||
params[:q][:completed_at_gt] = Time.zone.parse(params[:q][:completed_at_gt]) rescue Time.zone.now.beginning_of_month
|
||||
# Some actions are changing the `params` object. That is unfortunate Spree
|
||||
# behavior and we are building on it. So we have to look at `params` early
|
||||
# to check if we are searching or just displaying a report search form.
|
||||
def cache_search_state
|
||||
search_keys = [
|
||||
# search parameter for ransack
|
||||
:q,
|
||||
# common in all reports, only set for CSV rendering
|
||||
:csv,
|
||||
# `button` is included in all forms. It's not important for searching,
|
||||
# but the Users & Enterprises report doesn't have any other parameter
|
||||
# for an empty search. So we use this one to display data.
|
||||
:button,
|
||||
# Some reports use filtering by enterprise or order cycle
|
||||
:distributor_id,
|
||||
:supplier_id,
|
||||
:order_cycle_id,
|
||||
# Xero Invoices can be filtered by date
|
||||
:invoice_date,
|
||||
:due_date
|
||||
]
|
||||
@searching = search_keys.any? { |key| params.key? key }
|
||||
end
|
||||
|
||||
# We don't want to render data unless search params are supplied.
|
||||
# Compiling data can take a long time.
|
||||
def render_content?
|
||||
@searching
|
||||
end
|
||||
|
||||
def render_report(header, table, create_csv, csv_file_name)
|
||||
send_data csv_report(header, table), filename: csv_file_name if create_csv
|
||||
@header = header
|
||||
@table = table
|
||||
# Rendering HTML is the default.
|
||||
end
|
||||
|
||||
def csv_report(header, table)
|
||||
CSV.generate do |csv|
|
||||
csv << header
|
||||
table.each { |row| csv << row }
|
||||
end
|
||||
if params[:q] && !params[:q][:completed_at_lt].blank?
|
||||
params[:q][:completed_at_lt] = Time.zone.parse(params[:q][:completed_at_lt]) rescue ""
|
||||
end
|
||||
params[:q][:meta_sort] ||= "completed_at.desc"
|
||||
end
|
||||
|
||||
def load_data
|
||||
@@ -289,42 +260,42 @@ Spree::Admin::ReportsController.class_eval do
|
||||
distributors_of_my_products = Enterprise.with_distributed_products_outer.merge(Spree::Product.in_any_supplier(my_suppliers))
|
||||
@distributors = my_distributors | distributors_of_my_products
|
||||
# Load suppliers either owned by the user or supplying products their enterprises distribute.
|
||||
suppliers_of_products_I_distribute = my_distributors.map { |d| Spree::Product.in_distributor(d) }.flatten.map(&:supplier).uniq
|
||||
@suppliers = my_suppliers | suppliers_of_products_I_distribute
|
||||
suppliers_of_products_i_distribute = my_distributors.map { |d| Spree::Product.in_distributor(d) }.flatten.map(&:supplier).uniq
|
||||
@suppliers = my_suppliers | suppliers_of_products_i_distribute
|
||||
@order_cycles = OrderCycle.active_or_complete.accessible_by(spree_current_user).order('orders_close_at DESC')
|
||||
end
|
||||
|
||||
def authorized_reports
|
||||
reports = {
|
||||
:orders_and_distributors => {:name => I18n.t('admin.reports.orders_and_distributors.name'), :description => I18n.t('admin.reports.orders_and_distributors.description')},
|
||||
:bulk_coop => {:name => I18n.t('admin.reports.bulk_coop.name'), :description => I18n.t('admin.reports.bulk_coop.description')},
|
||||
:payments => {:name => I18n.t('admin.reports.payments.name'), :description => I18n.t('admin.reports.payments.description')},
|
||||
:orders_and_fulfillment => {:name => I18n.t('admin.reports.orders_and_fulfillment.name'), :description => ''},
|
||||
:customers => {:name => I18n.t('admin.reports.customers.name'), :description => ''},
|
||||
:products_and_inventory => {:name => I18n.t('admin.reports.products_and_inventory.name'), :description => ''},
|
||||
:sales_total => {:name => I18n.t('admin.reports.sales_total.name'), :description => I18n.t('admin.reports.sales_total.description')},
|
||||
:users_and_enterprises => {:name => I18n.t('admin.reports.users_and_enterprises.name'), :description => I18n.t('admin.reports.users_and_enterprises.description')},
|
||||
:order_cycle_management => {:name => I18n.t('admin.reports.order_cycle_management.name'), :description => ''},
|
||||
:sales_tax => {:name => I18n.t('admin.reports.sales_tax.name'), :description => ''},
|
||||
:xero_invoices => {:name => I18n.t('admin.reports.xero_invoices.name'), :description => I18n.t('admin.reports.xero_invoices.description')},
|
||||
:packing => {:name => I18n.t('admin.reports.packing.name'), :description => ''}
|
||||
}
|
||||
all_reports = [
|
||||
:orders_and_distributors,
|
||||
:bulk_coop,
|
||||
:payments,
|
||||
:orders_and_fulfillment,
|
||||
:customers,
|
||||
:products_and_inventory,
|
||||
:sales_total,
|
||||
:users_and_enterprises,
|
||||
:order_cycle_management,
|
||||
:sales_tax,
|
||||
:xero_invoices,
|
||||
:packing
|
||||
]
|
||||
reports = all_reports.select { |action| can? action, :report }
|
||||
reports.map { |report| [report, describe_report(report)] }.to_h
|
||||
end
|
||||
|
||||
reports[:orders_and_fulfillment][:description] =
|
||||
render_to_string(partial: 'orders_and_fulfillment_description', layout: false, locals: {report_types: report_types[:orders_and_fulfillment]}).html_safe
|
||||
reports[:products_and_inventory][:description] =
|
||||
render_to_string(partial: 'products_and_inventory_description', layout: false, locals: {report_types: report_types[:products_and_inventory]}).html_safe
|
||||
reports[:customers][:description] =
|
||||
render_to_string(partial: 'customers_description', layout: false, locals: {report_types: report_types[:customers]}).html_safe
|
||||
reports[:order_cycle_management][:description] =
|
||||
render_to_string(partial: 'order_cycle_management_description', layout: false, locals: {report_types: report_types[:order_cycle_management]}).html_safe
|
||||
reports[:packing][:description] =
|
||||
render_to_string(partial: 'packing_description', layout: false, locals: {report_types: report_types[:packing]}).html_safe
|
||||
reports[:sales_tax][:description] =
|
||||
render_to_string(partial: 'sales_tax_description', layout: false, locals: {report_types: report_types[:sales_tax]}).html_safe
|
||||
|
||||
# Return only reports the user is authorized to view.
|
||||
reports.select { |action| can? action, :report }
|
||||
def describe_report(report)
|
||||
name = I18n.t(:name, scope: [:admin, :reports, report])
|
||||
description = begin
|
||||
I18n.t!(:description, scope: [:admin, :reports, report])
|
||||
rescue I18n::MissingTranslationData
|
||||
render_to_string(
|
||||
partial: "#{report}_description",
|
||||
layout: false,
|
||||
locals: { report_types: report_types[report] }
|
||||
).html_safe
|
||||
end
|
||||
{ name: name, description: description }
|
||||
end
|
||||
|
||||
def timestamp
|
||||
|
||||
@@ -16,6 +16,18 @@ module Spree
|
||||
return render json: { flash: { error: I18n.t(:spree_gateway_error_flash_for_checkout, error: e.message) } }, status: 400
|
||||
end
|
||||
|
||||
def update
|
||||
@credit_card = Spree::CreditCard.find_by_id(params[:id])
|
||||
return update_failed unless @credit_card
|
||||
authorize! :update, @credit_card
|
||||
|
||||
if @credit_card.update_attributes(params[:credit_card])
|
||||
render json: @credit_card, serializer: ::Api::CreditCardSerializer, status: :ok
|
||||
else
|
||||
update_failed
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@credit_card = Spree::CreditCard.find_by_id(params[:id])
|
||||
if @credit_card
|
||||
@@ -65,5 +77,9 @@ module Spree
|
||||
card.user_id = spree_current_user.id
|
||||
card
|
||||
end
|
||||
|
||||
def update_failed
|
||||
render json: { flash: { error: t(:card_could_not_be_updated) } }, status: 400
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -15,4 +15,10 @@ Spree::UsersController.class_eval do
|
||||
|
||||
@orders = @orders.where('distributor_id != ?', Spree::Config.accounts_distributor_id)
|
||||
end
|
||||
|
||||
# Endpoint for queries to check if a user is already registered
|
||||
def registered_email
|
||||
user = Spree.user_class.find_by_email params[:email]
|
||||
render json: { registered: user.present? }
|
||||
end
|
||||
end
|
||||
|
||||
@@ -8,6 +8,7 @@ class UserConfirmationsController < DeviseController
|
||||
|
||||
# POST /resource/confirmation
|
||||
def create
|
||||
set_return_url if params.key? :return_url
|
||||
self.resource = resource_class.send_confirmation_instructions(resource_params)
|
||||
|
||||
if is_navigational_format?
|
||||
@@ -30,6 +31,10 @@ class UserConfirmationsController < DeviseController
|
||||
|
||||
protected
|
||||
|
||||
def set_return_url
|
||||
session[:confirmation_return_url] = params[:return_url]
|
||||
end
|
||||
|
||||
def after_confirmation_path_for(resource)
|
||||
result =
|
||||
if resource.errors.empty?
|
||||
|
||||
@@ -4,6 +4,8 @@ class UserPasswordsController < Spree::UserPasswordsController
|
||||
before_filter :set_admin_redirect, only: :edit
|
||||
|
||||
def create
|
||||
render_unconfirmed_response && return if user_unconfirmed?
|
||||
|
||||
self.resource = resource_class.send_reset_password_instructions(params[resource_name])
|
||||
|
||||
if resource.errors.empty?
|
||||
@@ -15,7 +17,7 @@ class UserPasswordsController < Spree::UserPasswordsController
|
||||
respond_with_navigational(resource) { render :new }
|
||||
end
|
||||
format.js do
|
||||
render json: resource.errors, status: :unauthorized
|
||||
render json: { error: t('email_not_found') }, status: :not_found
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -26,4 +28,13 @@ class UserPasswordsController < Spree::UserPasswordsController
|
||||
def set_admin_redirect
|
||||
session["spree_user_return_to"] = params[:return_to] if params[:return_to]
|
||||
end
|
||||
|
||||
def render_unconfirmed_response
|
||||
render json: { error: t('email_unconfirmed') }, status: :unauthorized
|
||||
end
|
||||
|
||||
def user_unconfirmed?
|
||||
user = Spree::User.find_by_email(params[:spree_user][:email])
|
||||
user && !user.confirmed?
|
||||
end
|
||||
end
|
||||
|
||||
@@ -66,20 +66,6 @@ module CheckoutHelper
|
||||
Spree::Money.new order.total - order.total_tax, currency: order.currency
|
||||
end
|
||||
|
||||
def checkout_state_options(source_address)
|
||||
if source_address == :billing
|
||||
address = @order.billing_address
|
||||
elsif source_address == :shipping
|
||||
address = @order.shipping_address
|
||||
end
|
||||
|
||||
[[]] + address.country.states.map { |c| [c.name, c.id] }
|
||||
end
|
||||
|
||||
def checkout_country_options
|
||||
available_countries.map { |c| [c.name, c.id] }
|
||||
end
|
||||
|
||||
def validated_input(name, path, args = {})
|
||||
attributes = {
|
||||
required: true,
|
||||
|
||||
@@ -74,9 +74,11 @@ module InjectionHelper
|
||||
end
|
||||
|
||||
def inject_saved_credit_cards
|
||||
if spree_current_user
|
||||
data = spree_current_user.credit_cards.with_payment_profile.all
|
||||
end
|
||||
data = if spree_current_user
|
||||
spree_current_user.credit_cards.with_payment_profile.all
|
||||
else
|
||||
[]
|
||||
end
|
||||
|
||||
inject_json_ams "savedCreditCards", data, Api::CreditCardSerializer
|
||||
end
|
||||
|
||||
@@ -19,4 +19,13 @@ module ShopHelper
|
||||
spree_current_user.customer_of(current_distributor)
|
||||
)
|
||||
end
|
||||
|
||||
def shop_tabs
|
||||
[
|
||||
{ name: 'about', title: t(:shopping_tabs_about, distributor: current_distributor.name), cols: 6 },
|
||||
{ name: 'producers', title: t(:label_producers), cols: 2 },
|
||||
{ name: 'contact', title: t(:shopping_tabs_contact), cols: 2 },
|
||||
{ name: 'groups', title: t(:label_groups), cols: 2 },
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
@@ -25,7 +25,6 @@ class SubscriptionConfirmJob
|
||||
def proxy_orders
|
||||
ProxyOrder.not_canceled.where('confirmed_at IS NULL AND placed_at IS NOT NULL')
|
||||
.joins(:order_cycle).merge(recently_closed_order_cycles)
|
||||
.joins(:subscription).merge(Subscription.not_canceled.not_paused)
|
||||
.joins(:order).merge(Spree::Order.complete)
|
||||
end
|
||||
|
||||
|
||||
@@ -34,6 +34,8 @@ class SubscriptionPlacementJob
|
||||
|
||||
changes = cap_quantity_and_store_changes(order)
|
||||
if order.line_items.where('quantity > 0').empty?
|
||||
order.reload.adjustments.destroy_all
|
||||
order.update!
|
||||
return send_empty_email(order, changes)
|
||||
end
|
||||
|
||||
|
||||
@@ -203,14 +203,6 @@ class Enterprise < ActiveRecord::Base
|
||||
self.supplied_products.where('count_on_hand > 0').present?
|
||||
end
|
||||
|
||||
def supplied_and_active_products_on_hand
|
||||
self.supplied_products.where('spree_products.count_on_hand > 0').active
|
||||
end
|
||||
|
||||
def active_products_in_order_cycles
|
||||
self.supplied_and_active_products_on_hand.in_an_active_order_cycle
|
||||
end
|
||||
|
||||
def to_param
|
||||
permalink
|
||||
end
|
||||
@@ -382,7 +374,7 @@ class Enterprise < ActiveRecord::Base
|
||||
end
|
||||
|
||||
def ensure_owner_is_manager
|
||||
users << owner unless users.include?(owner) || owner.admin?
|
||||
users << owner unless users.include?(owner)
|
||||
end
|
||||
|
||||
def enforce_ownership_limit
|
||||
|
||||
28
app/models/feature_flags.rb
Normal file
28
app/models/feature_flags.rb
Normal file
@@ -0,0 +1,28 @@
|
||||
# Tells whether a particular feature is enabled or not
|
||||
class FeatureFlags
|
||||
# Constructor
|
||||
#
|
||||
# @param user [User]
|
||||
def initialize(user)
|
||||
@user = user
|
||||
end
|
||||
|
||||
# Checks whether product import is enabled for the specified user
|
||||
#
|
||||
# @return [Boolean]
|
||||
def product_import_enabled?
|
||||
superadmin?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :user
|
||||
|
||||
# Checks whether the specified user is a superadmin, with full control of the
|
||||
# instance
|
||||
#
|
||||
# @return [Boolean]
|
||||
def superadmin?
|
||||
user.has_spree_role?('admin')
|
||||
end
|
||||
end
|
||||
234
app/models/product_import/entry_processor.rb
Normal file
234
app/models/product_import/entry_processor.rb
Normal file
@@ -0,0 +1,234 @@
|
||||
module ProductImport
|
||||
class EntryProcessor
|
||||
attr_reader :inventory_created, :inventory_updated, :products_created, :variants_created, :variants_updated, :products_reset_count, :supplier_products, :total_supplier_products
|
||||
|
||||
def initialize(importer, validator, import_settings, spreadsheet_data, editable_enterprises, import_time, updated_ids)
|
||||
@importer = importer
|
||||
@validator = validator
|
||||
@import_settings = import_settings
|
||||
@spreadsheet_data = spreadsheet_data
|
||||
@editable_enterprises = editable_enterprises
|
||||
@import_time = import_time
|
||||
@updated_ids = updated_ids
|
||||
|
||||
@inventory_created = 0
|
||||
@inventory_updated = 0
|
||||
@products_created = 0
|
||||
@variants_created = 0
|
||||
@variants_updated = 0
|
||||
@products_reset_count = 0
|
||||
@supplier_products = {}
|
||||
@total_supplier_products = 0
|
||||
end
|
||||
|
||||
def save_all(entries)
|
||||
entries.each do |entry|
|
||||
if import_into_inventory?(entry)
|
||||
save_to_inventory(entry)
|
||||
else
|
||||
save_to_product_list(entry)
|
||||
end
|
||||
end
|
||||
|
||||
@importer.errors.add(:importer, I18n.t(:product_importer_products_save_error)) if total_saved_count.zero?
|
||||
end
|
||||
|
||||
def count_existing_items
|
||||
@spreadsheet_data.suppliers_index.each do |_supplier_name, supplier_id|
|
||||
next unless supplier_id && permission_by_id?(supplier_id)
|
||||
|
||||
products_count =
|
||||
if import_into_inventory_by_supplier?(supplier_id)
|
||||
VariantOverride.where('variant_overrides.hub_id IN (?)', supplier_id).count
|
||||
else
|
||||
Spree::Variant.
|
||||
not_deleted.
|
||||
not_master.
|
||||
joins(:product).
|
||||
where('spree_products.supplier_id IN (?)', supplier_id).
|
||||
count
|
||||
end
|
||||
|
||||
@supplier_products[supplier_id] = products_count
|
||||
@total_supplier_products += products_count
|
||||
end
|
||||
end
|
||||
|
||||
def reset_absent_items
|
||||
# For selected enterprises; set stock to zero for all products/inventory
|
||||
# that were not listed in the newly uploaded spreadsheet
|
||||
return if total_saved_count.zero? || @updated_ids.empty? || !@import_settings.key?(:settings)
|
||||
suppliers_to_reset_products = []
|
||||
suppliers_to_reset_inventories = []
|
||||
|
||||
@import_settings[:settings].each do |enterprise_id, settings|
|
||||
suppliers_to_reset_products.push enterprise_id if settings['reset_all_absent'] && permission_by_id?(enterprise_id) && !import_into_inventory_by_supplier?(enterprise_id)
|
||||
suppliers_to_reset_inventories.push enterprise_id if settings['reset_all_absent'] && permission_by_id?(enterprise_id) && import_into_inventory_by_supplier?(enterprise_id)
|
||||
end
|
||||
|
||||
unless suppliers_to_reset_inventories.empty?
|
||||
@products_reset_count += VariantOverride.
|
||||
where('variant_overrides.hub_id IN (?)
|
||||
AND variant_overrides.id NOT IN (?)', suppliers_to_reset_inventories, @updated_ids).
|
||||
update_all(count_on_hand: 0)
|
||||
end
|
||||
|
||||
return if suppliers_to_reset_products.empty?
|
||||
|
||||
@products_reset_count += Spree::Variant.joins(:product).
|
||||
where('spree_products.supplier_id IN (?)
|
||||
AND spree_variants.id NOT IN (?)
|
||||
AND spree_variants.is_master = false
|
||||
AND spree_variants.deleted_at IS NULL', suppliers_to_reset_products, @updated_ids).
|
||||
update_all(count_on_hand: 0)
|
||||
end
|
||||
|
||||
def total_saved_count
|
||||
@products_created + @variants_created + @variants_updated + @inventory_created + @inventory_updated
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def save_to_inventory(entry)
|
||||
save_new_inventory_item entry if entry.validates_as? 'new_inventory_item'
|
||||
save_existing_inventory_item entry if entry.validates_as? 'existing_inventory_item'
|
||||
end
|
||||
|
||||
def save_to_product_list(entry)
|
||||
save_new_product entry if entry.validates_as? 'new_product'
|
||||
|
||||
if entry.validates_as? 'new_variant'
|
||||
save_variant entry
|
||||
@variants_created += 1
|
||||
end
|
||||
|
||||
return unless entry.validates_as? 'existing_variant'
|
||||
|
||||
save_variant entry
|
||||
@variants_updated += 1
|
||||
end
|
||||
|
||||
def import_into_inventory?(entry)
|
||||
entry.supplier_id && @import_settings[:settings][entry.supplier_id.to_s]['import_into'] == 'inventories'
|
||||
end
|
||||
|
||||
def save_new_inventory_item(entry)
|
||||
new_item = entry.product_object
|
||||
assign_defaults(new_item, entry)
|
||||
new_item.import_date = @import_time
|
||||
|
||||
if new_item.valid? && new_item.save
|
||||
display_in_inventory(new_item, true)
|
||||
@inventory_created += 1
|
||||
@updated_ids.push new_item.id
|
||||
else
|
||||
@importer.errors.add("#{I18n.t('admin.product_import.model.line')} #{line_number}:", new_item.errors.full_messages)
|
||||
end
|
||||
end
|
||||
|
||||
def save_existing_inventory_item(entry)
|
||||
existing_item = entry.product_object
|
||||
assign_defaults(existing_item, entry)
|
||||
existing_item.import_date = @import_time
|
||||
|
||||
if existing_item.valid? && existing_item.save
|
||||
display_in_inventory(existing_item)
|
||||
@inventory_updated += 1
|
||||
@updated_ids.push existing_item.id
|
||||
else
|
||||
@importer.errors.add("#{I18n.t('admin.product_import.model.line')} #{line_number}:", existing_item.errors.full_messages)
|
||||
end
|
||||
end
|
||||
|
||||
def save_new_product(entry)
|
||||
@already_created ||= {}
|
||||
# If we've already added a new product with these attributes
|
||||
# from this spreadsheet, mark this entry as a new variant with
|
||||
# the new product id, as this is a now variant of that product...
|
||||
if @already_created[entry.supplier_id] && @already_created[entry.supplier_id][entry.name]
|
||||
product_id = @already_created[entry.supplier_id][entry.name]
|
||||
@validator.mark_as_new_variant(entry, product_id)
|
||||
return
|
||||
end
|
||||
|
||||
product = Spree::Product.new
|
||||
product.assign_attributes(entry.attributes.except('id'))
|
||||
assign_defaults(product, entry)
|
||||
|
||||
if product.save
|
||||
ensure_variant_updated(product, entry)
|
||||
@products_created += 1
|
||||
@updated_ids.push product.variants.first.id
|
||||
else
|
||||
@importer.errors.add("#{I18n.t('admin.product_import.model.line')} #{line_number}:", product.errors.full_messages)
|
||||
end
|
||||
|
||||
@already_created[entry.supplier_id] = { entry.name => product.id }
|
||||
end
|
||||
|
||||
def save_variant(entry)
|
||||
variant = entry.product_object
|
||||
assign_defaults(variant, entry)
|
||||
variant.import_date = @import_time
|
||||
|
||||
if variant.valid? && variant.save
|
||||
@updated_ids.push variant.id
|
||||
true
|
||||
else
|
||||
@importer.errors.add("#{I18n.t('admin.product_import.model.line')} #{line_number}:", variant.errors.full_messages)
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
def assign_defaults(object, entry)
|
||||
# Assigns a default value for a specified field e.g. category='Vegetables', setting this value
|
||||
# either for all entries (overwrite_all), or only for those entries where the field was blank
|
||||
# in the spreadsheet (overwrite_empty), depending on selected import settings
|
||||
return unless @import_settings.key?(:settings) && @import_settings[:settings][entry.supplier_id.to_s] && @import_settings[:settings][entry.supplier_id.to_s]['defaults']
|
||||
|
||||
@import_settings[:settings][entry.supplier_id.to_s]['defaults'].each do |attribute, setting|
|
||||
next unless setting['active']
|
||||
|
||||
case setting['mode']
|
||||
when 'overwrite_all'
|
||||
object.assign_attributes(attribute => setting['value'])
|
||||
when 'overwrite_empty'
|
||||
if object.send(attribute).blank? || ((attribute == 'on_hand' || attribute == 'count_on_hand') && entry.on_hand_nil)
|
||||
object.assign_attributes(attribute => setting['value'])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def display_in_inventory(variant_override, is_new = false)
|
||||
unless is_new
|
||||
existing_item = InventoryItem.where(variant_id: variant_override.variant_id, enterprise_id: variant_override.hub_id).first
|
||||
|
||||
if existing_item
|
||||
existing_item.assign_attributes(visible: true)
|
||||
existing_item.save
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
InventoryItem.new(variant_id: variant_override.variant_id, enterprise_id: variant_override.hub_id, visible: true).save
|
||||
end
|
||||
|
||||
def ensure_variant_updated(product, entry)
|
||||
# Ensure attributes are correctly copied to a new product's variant
|
||||
variant = product.variants.first
|
||||
variant.display_name = entry.display_name if entry.display_name
|
||||
variant.on_demand = entry.on_demand if entry.on_demand
|
||||
variant.import_date = @import_time
|
||||
variant.save
|
||||
end
|
||||
|
||||
def permission_by_id?(supplier_id)
|
||||
@editable_enterprises.value?(Integer(supplier_id))
|
||||
end
|
||||
|
||||
def import_into_inventory_by_supplier?(supplier_id)
|
||||
@import_settings[:settings] && @import_settings[:settings][supplier_id.to_s] && @import_settings[:settings][supplier_id.to_s]['import_into'] == 'inventories'
|
||||
end
|
||||
end
|
||||
end
|
||||
273
app/models/product_import/entry_validator.rb
Normal file
273
app/models/product_import/entry_validator.rb
Normal file
@@ -0,0 +1,273 @@
|
||||
module ProductImport
|
||||
class EntryValidator
|
||||
def initialize(current_user, import_time, spreadsheet_data, editable_enterprises, inventory_permissions, reset_counts, import_settings)
|
||||
@current_user = current_user
|
||||
@import_time = import_time
|
||||
@spreadsheet_data = spreadsheet_data
|
||||
@editable_enterprises = editable_enterprises
|
||||
@inventory_permissions = inventory_permissions
|
||||
@reset_counts = reset_counts
|
||||
@import_settings = import_settings
|
||||
end
|
||||
|
||||
def validate_all(entries)
|
||||
entries.each do |entry|
|
||||
supplier_validation(entry)
|
||||
unit_fields_validation(entry)
|
||||
|
||||
next if entry.supplier_id.blank?
|
||||
|
||||
if import_into_inventory?(entry)
|
||||
producer_validation(entry)
|
||||
inventory_validation(entry)
|
||||
else
|
||||
category_validation(entry)
|
||||
tax_and_shipping_validation(entry, 'tax', entry.tax_category, @spreadsheet_data.tax_index)
|
||||
tax_and_shipping_validation(entry, 'shipping', entry.shipping_category, @spreadsheet_data.shipping_index)
|
||||
product_validation(entry)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def mark_as_new_variant(entry, product_id)
|
||||
new_variant = Spree::Variant.new(entry.attributes.except('id', 'product_id'))
|
||||
new_variant.product_id = product_id
|
||||
check_on_hand_nil(entry, new_variant)
|
||||
|
||||
if new_variant.valid?
|
||||
entry.product_object = new_variant
|
||||
entry.validates_as = 'new_variant' unless entry.errors?
|
||||
else
|
||||
mark_as_invalid(entry, product_validations: new_variant.errors)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def supplier_validation(entry)
|
||||
supplier_name = entry.supplier
|
||||
|
||||
if supplier_name.blank?
|
||||
mark_as_invalid(entry, attribute: "supplier", error: I18n.t(:error_required))
|
||||
return
|
||||
end
|
||||
|
||||
unless @spreadsheet_data.suppliers_index[supplier_name]
|
||||
mark_as_invalid(entry, attribute: "supplier", error: I18n.t(:error_not_found_in_database, name: supplier_name))
|
||||
return
|
||||
end
|
||||
|
||||
unless permission_by_name?(supplier_name)
|
||||
mark_as_invalid(entry, attribute: "supplier", error: I18n.t(:error_no_permission_for_enterprise, name: supplier_name))
|
||||
return
|
||||
end
|
||||
|
||||
entry.supplier_id = @spreadsheet_data.suppliers_index[supplier_name]
|
||||
end
|
||||
|
||||
def unit_fields_validation(entry)
|
||||
unit_types = ['g', 'kg', 't', 'ml', 'l', 'kl', '']
|
||||
|
||||
unless entry.units && entry.units.present?
|
||||
mark_as_invalid(entry, attribute: 'units', error: I18n.t('admin.product_import.model.blank'))
|
||||
end
|
||||
|
||||
return if import_into_inventory?(entry)
|
||||
|
||||
# unit_type must be valid type
|
||||
if entry.unit_type && entry.unit_type.present?
|
||||
unit_type = entry.unit_type.to_s.strip.downcase
|
||||
mark_as_invalid(entry, attribute: 'unit_type', error: I18n.t('admin.product_import.model.incorrect_value')) unless unit_types.include?(unit_type)
|
||||
return
|
||||
end
|
||||
|
||||
# variant_unit_name must be present if unit_type not present
|
||||
mark_as_invalid(entry, attribute: 'variant_unit_name', error: I18n.t('admin.product_import.model.conditional_blank')) unless entry.variant_unit_name && entry.variant_unit_name.present?
|
||||
end
|
||||
|
||||
def producer_validation(entry)
|
||||
producer_name = entry.producer
|
||||
|
||||
if producer_name.blank?
|
||||
mark_as_invalid(entry, attribute: "producer", error: I18n.t('admin.product_import.model.blank'))
|
||||
return
|
||||
end
|
||||
|
||||
unless @spreadsheet_data.producers_index[producer_name]
|
||||
mark_as_invalid(entry, attribute: "producer", error: "\"#{producer_name}\" #{I18n.t('admin.product_import.model.not_found')}")
|
||||
return
|
||||
end
|
||||
|
||||
unless inventory_permission?(entry.supplier_id, @spreadsheet_data.producers_index[producer_name])
|
||||
mark_as_invalid(entry, attribute: "producer", error: "\"#{producer_name}\": #{I18n.t('admin.product_import.model.inventory_no_permission')}")
|
||||
return
|
||||
end
|
||||
|
||||
entry.producer_id = @spreadsheet_data.producers_index[producer_name]
|
||||
end
|
||||
|
||||
def inventory_validation(entry)
|
||||
# Checks a potential inventory item corresponds to a valid variant
|
||||
match = Spree::Product.where(supplier_id: entry.producer_id, name: entry.name, deleted_at: nil).first
|
||||
|
||||
if match.nil?
|
||||
mark_as_invalid(entry, attribute: 'name', error: I18n.t('admin.product_import.model.no_product'))
|
||||
return
|
||||
end
|
||||
|
||||
match.variants.each do |existing_variant|
|
||||
unit_scale = match.variant_unit_scale
|
||||
unscaled_units = entry.unscaled_units || 0
|
||||
entry.unit_value = unscaled_units * unit_scale
|
||||
|
||||
if entry_matches_existing_variant?(entry, existing_variant)
|
||||
variant_override = create_inventory_item(entry, existing_variant)
|
||||
return validate_inventory_item(entry, variant_override)
|
||||
end
|
||||
end
|
||||
|
||||
mark_as_invalid(entry, attribute: 'product', error: I18n.t('admin.product_import.model.not_found'))
|
||||
end
|
||||
|
||||
def entry_matches_existing_variant?(entry, existing_variant)
|
||||
existing_variant.display_name == entry.display_name && existing_variant.unit_value == entry.unit_value.to_f
|
||||
end
|
||||
|
||||
def category_validation(entry)
|
||||
category_name = entry.category
|
||||
|
||||
if category_name.blank?
|
||||
mark_as_invalid(entry, attribute: "category", error: I18n.t(:error_required))
|
||||
return
|
||||
end
|
||||
|
||||
if @spreadsheet_data.categories_index[category_name]
|
||||
entry.primary_taxon_id = @spreadsheet_data.categories_index[category_name]
|
||||
else
|
||||
mark_as_invalid(entry, attribute: "category", error: I18n.t(:error_not_found_in_database, name: category_name))
|
||||
end
|
||||
end
|
||||
|
||||
def tax_and_shipping_validation(entry, type, category, index)
|
||||
return if category.blank?
|
||||
|
||||
if index.key? category
|
||||
entry.send("#{type}_category_id=", index[category])
|
||||
else
|
||||
mark_as_invalid(entry, attribute: "#{type}_category", error: I18n.t('admin.product_import.model.not_found'))
|
||||
end
|
||||
end
|
||||
|
||||
def product_validation(entry)
|
||||
# Find product with matching supplier and name
|
||||
match = Spree::Product.where(supplier_id: entry.supplier_id, name: entry.name, deleted_at: nil).first
|
||||
|
||||
# If no matching product was found, create a new product
|
||||
if match.nil?
|
||||
mark_as_new_product(entry)
|
||||
return
|
||||
end
|
||||
|
||||
# Otherwise, if a variant exists with matching display_name and unit_value, update it
|
||||
match.variants.each do |existing_variant|
|
||||
if entry_matches_existing_variant?(entry, existing_variant) && existing_variant.deleted_at.nil?
|
||||
return mark_as_existing_variant(entry, existing_variant)
|
||||
end
|
||||
end
|
||||
|
||||
# Otherwise, a variant with sufficiently matching attributes doesn't exist; create a new one
|
||||
mark_as_new_variant(entry, match.id)
|
||||
end
|
||||
|
||||
def mark_as_new_product(entry)
|
||||
new_product = Spree::Product.new
|
||||
new_product.assign_attributes(entry.attributes.except('id'))
|
||||
|
||||
if new_product.valid?
|
||||
entry.validates_as = 'new_product' unless entry.errors?
|
||||
else
|
||||
mark_as_invalid(entry, product_validations: new_product.errors)
|
||||
end
|
||||
end
|
||||
|
||||
def mark_as_existing_variant(entry, existing_variant)
|
||||
existing_variant.assign_attributes(entry.attributes.except('id', 'product_id'))
|
||||
check_on_hand_nil(entry, existing_variant)
|
||||
|
||||
if existing_variant.valid?
|
||||
entry.product_object = existing_variant
|
||||
entry.validates_as = 'existing_variant' unless entry.errors?
|
||||
updates_count_per_supplier(entry.supplier_id) unless entry.errors?
|
||||
else
|
||||
mark_as_invalid(entry, product_validations: existing_variant.errors)
|
||||
end
|
||||
end
|
||||
|
||||
def permission_by_name?(supplier_name)
|
||||
@editable_enterprises.key?(supplier_name)
|
||||
end
|
||||
|
||||
def permission_by_id?(supplier_id)
|
||||
@editable_enterprises.value?(Integer(supplier_id))
|
||||
end
|
||||
|
||||
def inventory_permission?(supplier_id, producer_id)
|
||||
@current_user.admin? || ( @inventory_permissions[supplier_id] && @inventory_permissions[supplier_id].include?(producer_id) )
|
||||
end
|
||||
|
||||
def mark_as_invalid(entry, options = {})
|
||||
entry.errors.add(options[:attribute], options[:error]) if options[:attribute] && options[:error]
|
||||
entry.product_validations = options[:product_validations] if options[:product_validations]
|
||||
end
|
||||
|
||||
def import_into_inventory?(entry)
|
||||
entry.supplier_id && @import_settings[:settings][entry.supplier_id.to_s]['import_into'] == 'inventories'
|
||||
end
|
||||
|
||||
def validate_inventory_item(entry, variant_override)
|
||||
if variant_override.valid? && !entry.errors?
|
||||
mark_as_inventory_item(entry, variant_override)
|
||||
else
|
||||
mark_as_invalid(entry, product_validations: variant_override.errors)
|
||||
end
|
||||
end
|
||||
|
||||
def create_inventory_item(entry, existing_variant)
|
||||
existing_variant_override = VariantOverride.where(variant_id: existing_variant.id, hub_id: entry.supplier_id).first
|
||||
|
||||
variant_override = existing_variant_override || VariantOverride.new(variant_id: existing_variant.id, hub_id: entry.supplier_id)
|
||||
variant_override.assign_attributes(count_on_hand: entry.on_hand, import_date: @import_time)
|
||||
check_on_hand_nil(entry, variant_override)
|
||||
variant_override.assign_attributes(entry.attributes.slice('price', 'on_demand'))
|
||||
|
||||
variant_override
|
||||
end
|
||||
|
||||
def mark_as_inventory_item(entry, variant_override)
|
||||
if variant_override.id
|
||||
entry.validates_as = 'existing_inventory_item'
|
||||
entry.product_object = variant_override
|
||||
updates_count_per_supplier(entry.supplier_id) unless entry.errors?
|
||||
else
|
||||
entry.validates_as = 'new_inventory_item'
|
||||
entry.product_object = variant_override
|
||||
end
|
||||
end
|
||||
|
||||
def updates_count_per_supplier(supplier_id)
|
||||
if @reset_counts[supplier_id] && @reset_counts[supplier_id][:updates_count]
|
||||
@reset_counts[supplier_id][:updates_count] += 1
|
||||
else
|
||||
@reset_counts[supplier_id] = { updates_count: 1 }
|
||||
end
|
||||
end
|
||||
|
||||
def check_on_hand_nil(entry, object)
|
||||
return if entry.on_hand.present?
|
||||
|
||||
object.on_hand = 0 if object.respond_to?(:on_hand)
|
||||
object.count_on_hand = 0 if object.respond_to?(:count_on_hand)
|
||||
entry.on_hand_nil = true
|
||||
end
|
||||
end
|
||||
end
|
||||
252
app/models/product_import/product_importer.rb
Normal file
252
app/models/product_import/product_importer.rb
Normal file
@@ -0,0 +1,252 @@
|
||||
require 'roo'
|
||||
|
||||
module ProductImport
|
||||
class ProductImporter
|
||||
extend ActiveModel::Naming
|
||||
include ActiveModel::Conversion
|
||||
include ActiveModel::Validations
|
||||
|
||||
attr_reader :updated_ids
|
||||
|
||||
def initialize(file, current_user, import_settings = {})
|
||||
unless file.is_a?(File)
|
||||
errors.add(:importer, I18n.t(:product_importer_file_error))
|
||||
return
|
||||
end
|
||||
|
||||
@file = file
|
||||
@sheet = open_spreadsheet
|
||||
@entries = []
|
||||
|
||||
@import_time = Time.zone.now
|
||||
@import_settings = import_settings || {}
|
||||
|
||||
@current_user = current_user
|
||||
@editable_enterprises = {}
|
||||
@inventory_permissions = {}
|
||||
|
||||
@reset_counts = {}
|
||||
@updated_ids = []
|
||||
|
||||
init_product_importer if @sheet
|
||||
end
|
||||
|
||||
def persisted?
|
||||
false # ActiveModel
|
||||
end
|
||||
|
||||
def entries?
|
||||
@entries.count > 0
|
||||
end
|
||||
|
||||
def valid_entries?
|
||||
@entries.each do |entry|
|
||||
return true if entry.validates_as.present?
|
||||
end
|
||||
false
|
||||
end
|
||||
|
||||
def item_count
|
||||
@sheet ? @sheet.last_row - 1 : 0
|
||||
end
|
||||
|
||||
def reset_counts
|
||||
# Return indexed data about existing product count, reset count, and updates count per supplier
|
||||
@reset_counts.each do |supplier_id, values|
|
||||
values[:updates_count] = 0 if values[:updates_count].blank?
|
||||
|
||||
if values[:updates_count] && values[:existing_products]
|
||||
@reset_counts[supplier_id][:reset_count] = values[:existing_products] - values[:updates_count]
|
||||
end
|
||||
end
|
||||
@reset_counts
|
||||
end
|
||||
|
||||
def suppliers_index
|
||||
index = @spreadsheet_data.suppliers_index
|
||||
index.sort_by{ |_k, v| v.to_i }.reverse.to_h
|
||||
end
|
||||
|
||||
def supplier_products
|
||||
@processor.supplier_products
|
||||
end
|
||||
|
||||
def total_supplier_products
|
||||
@processor.total_supplier_products
|
||||
end
|
||||
|
||||
def all_entries
|
||||
@entries
|
||||
end
|
||||
|
||||
def entries_json
|
||||
entries = {}
|
||||
@entries.each do |entry|
|
||||
entries[entry.line_number] = {
|
||||
attributes: entry.displayable_attributes,
|
||||
validates_as: entry.validates_as,
|
||||
errors: entry.invalid_attributes
|
||||
}
|
||||
end
|
||||
entries.to_json
|
||||
end
|
||||
|
||||
def table_headings
|
||||
@entries.first.displayable_attributes.keys.map(&:humanize) if @entries.first
|
||||
end
|
||||
|
||||
def products_created_count
|
||||
@processor.products_created + @processor.variants_created
|
||||
end
|
||||
|
||||
def products_updated_count
|
||||
@processor.variants_updated
|
||||
end
|
||||
|
||||
def inventory_created_count
|
||||
@processor.inventory_created
|
||||
end
|
||||
|
||||
def inventory_updated_count
|
||||
@processor.inventory_updated
|
||||
end
|
||||
|
||||
def products_reset_count
|
||||
@processor.products_reset_count
|
||||
end
|
||||
|
||||
def total_saved_count
|
||||
@processor.total_saved_count
|
||||
end
|
||||
|
||||
def import_results
|
||||
{ entries: entries_json, reset_counts: reset_counts }
|
||||
end
|
||||
|
||||
def save_results
|
||||
{
|
||||
results: {
|
||||
products_created: products_created_count,
|
||||
products_updated: products_updated_count,
|
||||
inventory_created: inventory_created_count,
|
||||
inventory_updated: inventory_updated_count,
|
||||
products_reset: products_reset_count,
|
||||
},
|
||||
updated_ids: updated_ids,
|
||||
errors: errors.full_messages
|
||||
}
|
||||
end
|
||||
|
||||
def validate_entries
|
||||
@validator.validate_all(@entries)
|
||||
end
|
||||
|
||||
def save_entries
|
||||
validate_entries
|
||||
save_all_valid
|
||||
end
|
||||
|
||||
def reset_absent(updated_ids)
|
||||
@products_created = updated_ids.count
|
||||
@updated_ids = updated_ids
|
||||
@processor.reset_absent_items
|
||||
end
|
||||
|
||||
def permission_by_id?(supplier_id)
|
||||
@editable_enterprises.value?(Integer(supplier_id))
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def init_product_importer
|
||||
init_permissions
|
||||
|
||||
if staged_import?
|
||||
build_entries_in_range
|
||||
else
|
||||
build_entries
|
||||
end
|
||||
|
||||
@spreadsheet_data = SpreadsheetData.new(@entries)
|
||||
@validator = EntryValidator.new(@current_user, @import_time, @spreadsheet_data, @editable_enterprises, @inventory_permissions, @reset_counts, @import_settings)
|
||||
@processor = EntryProcessor.new(self, @validator, @import_settings, @spreadsheet_data, @editable_enterprises, @import_time, @updated_ids)
|
||||
|
||||
@processor.count_existing_items unless staged_import?
|
||||
end
|
||||
|
||||
def staged_import?
|
||||
@import_settings && @import_settings.key?(:start) && @import_settings.key?(:end)
|
||||
end
|
||||
|
||||
def init_permissions
|
||||
permissions = OpenFoodNetwork::Permissions.new(@current_user)
|
||||
|
||||
permissions.editable_enterprises.
|
||||
order('is_primary_producer ASC, name').
|
||||
map { |e| @editable_enterprises[e.name] = e.id }
|
||||
|
||||
@inventory_permissions = permissions.variant_override_enterprises_per_hub
|
||||
end
|
||||
|
||||
def open_spreadsheet
|
||||
if accepted_mimetype
|
||||
Roo::Spreadsheet.open(@file, extension: accepted_mimetype)
|
||||
else
|
||||
errors.add(:importer, I18n.t(:product_importer_spreadsheet_error))
|
||||
delete_uploaded_file
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
def accepted_mimetype
|
||||
File.extname(@file.path).in?('.csv', '.xls', '.xlsx', '.ods') ? @file.path.split('.').last.to_sym : false
|
||||
end
|
||||
|
||||
def headers
|
||||
@sheet.row(1)
|
||||
end
|
||||
|
||||
def rows
|
||||
return [] unless @sheet && @sheet.last_row
|
||||
(2..@sheet.last_row).map do |i|
|
||||
@sheet.row(i)
|
||||
end
|
||||
end
|
||||
|
||||
def build_entries_in_range
|
||||
start_line = @import_settings[:start]
|
||||
end_line = @import_settings[:end]
|
||||
|
||||
(start_line..end_line).each do |i|
|
||||
line_number = i + 1
|
||||
row = @sheet.row(line_number)
|
||||
row_data = Hash[[headers, row].transpose]
|
||||
entry = SpreadsheetEntry.new(row_data)
|
||||
entry.line_number = line_number
|
||||
@entries.push entry
|
||||
break if @sheet.last_row == line_number
|
||||
end
|
||||
end
|
||||
|
||||
def build_entries
|
||||
rows.each_with_index do |row, i|
|
||||
row_data = Hash[[headers, row].transpose]
|
||||
entry = SpreadsheetEntry.new(row_data)
|
||||
entry.line_number = i + 2
|
||||
@entries.push entry
|
||||
end
|
||||
@entries
|
||||
end
|
||||
|
||||
def save_all_valid
|
||||
@processor.save_all(@entries)
|
||||
@processor.reset_absent_items unless staged_import?
|
||||
@processor.total_saved_count
|
||||
end
|
||||
|
||||
def delete_uploaded_file
|
||||
return unless @file.path == Rails.root.join('tmp', 'product_import').to_s
|
||||
File.delete(@file)
|
||||
end
|
||||
end
|
||||
end
|
||||
72
app/models/product_import/spreadsheet_data.rb
Normal file
72
app/models/product_import/spreadsheet_data.rb
Normal file
@@ -0,0 +1,72 @@
|
||||
module ProductImport
|
||||
class SpreadsheetData
|
||||
def initialize(entries)
|
||||
@entries = entries
|
||||
end
|
||||
|
||||
def suppliers_index
|
||||
@suppliers_index || create_suppliers_index
|
||||
end
|
||||
|
||||
def producers_index
|
||||
@producers_index = create_producers_index
|
||||
end
|
||||
|
||||
def categories_index
|
||||
@categories_index || create_categories_index
|
||||
end
|
||||
|
||||
def tax_index
|
||||
@tax_index || create_tax_index
|
||||
end
|
||||
|
||||
def shipping_index
|
||||
@shipping_index || create_shipping_index
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def create_suppliers_index
|
||||
@suppliers_index = {}
|
||||
@entries.each do |entry|
|
||||
supplier_name = entry.supplier
|
||||
supplier_id = @suppliers_index[supplier_name] || Enterprise.find_by_name(supplier_name, select: 'id, name').try(:id)
|
||||
@suppliers_index[supplier_name] = supplier_id
|
||||
end
|
||||
@suppliers_index
|
||||
end
|
||||
|
||||
def create_producers_index
|
||||
@producers_index = {}
|
||||
@entries.each do |entry|
|
||||
next unless entry.producer
|
||||
producer_name = entry.producer
|
||||
producer_id = @producers_index[producer_name] || Enterprise.find_by_name(producer_name, select: 'id, name').try(:id)
|
||||
@producers_index[producer_name] = producer_id
|
||||
end
|
||||
@producers_index
|
||||
end
|
||||
|
||||
def create_categories_index
|
||||
@categories_index = {}
|
||||
@entries.each do |entry|
|
||||
category_name = entry.category
|
||||
category_id = @categories_index[category_name] || Spree::Taxon.find_by_name(category_name, select: 'id, name').try(:id)
|
||||
@categories_index[category_name] = category_id
|
||||
end
|
||||
@categories_index
|
||||
end
|
||||
|
||||
def create_tax_index
|
||||
@tax_index = {}
|
||||
Spree::TaxCategory.select([:id, :name]).map { |tc| @tax_index[tc.name] = tc.id }
|
||||
@tax_index
|
||||
end
|
||||
|
||||
def create_shipping_index
|
||||
@shipping_index = {}
|
||||
Spree::ShippingCategory.select([:id, :name]).map { |sc| @shipping_index[sc.name] = sc.id }
|
||||
@shipping_index
|
||||
end
|
||||
end
|
||||
end
|
||||
78
app/models/product_import/spreadsheet_entry.rb
Normal file
78
app/models/product_import/spreadsheet_entry.rb
Normal file
@@ -0,0 +1,78 @@
|
||||
module ProductImport
|
||||
class SpreadsheetEntry
|
||||
extend ActiveModel::Naming
|
||||
include ActiveModel::Conversion
|
||||
include ActiveModel::Validations
|
||||
|
||||
attr_accessor :line_number, :valid, :validates_as, :product_object, :product_validations, :on_hand_nil,
|
||||
:has_overrides, :units, :unscaled_units, :unit_type, :tax_category, :shipping_category
|
||||
|
||||
attr_accessor :id, :product_id, :producer, :producer_id, :supplier, :supplier_id, :name, :display_name, :sku,
|
||||
:unit_value, :unit_description, :variant_unit, :variant_unit_scale, :variant_unit_name,
|
||||
:display_as, :category, :primary_taxon_id, :price, :on_hand, :count_on_hand, :on_demand,
|
||||
:tax_category_id, :shipping_category_id, :description, :import_date
|
||||
|
||||
def initialize(attrs)
|
||||
@validates_as = ''
|
||||
assign_units attrs
|
||||
end
|
||||
|
||||
def persisted?
|
||||
false # ActiveModel
|
||||
end
|
||||
|
||||
def validates_as?(type)
|
||||
@validates_as == type
|
||||
end
|
||||
|
||||
def errors?
|
||||
errors.count > 0 || @product_validations
|
||||
end
|
||||
|
||||
def attributes
|
||||
attrs = {}
|
||||
instance_variables.each do |var|
|
||||
attrs[var.to_s.delete("@")] = instance_variable_get(var)
|
||||
end
|
||||
attrs.except(*non_product_attributes)
|
||||
end
|
||||
|
||||
def displayable_attributes
|
||||
# Modified attributes list for displaying in user feedback
|
||||
attrs = {}
|
||||
instance_variables.each do |var|
|
||||
attrs[var.to_s.delete("@")] = instance_variable_get(var)
|
||||
end
|
||||
attrs.except(*non_product_attributes, *non_display_attributes)
|
||||
end
|
||||
|
||||
def invalid_attributes
|
||||
invalid_attrs = {}
|
||||
errors = @product_validations ? self.errors.messages.merge(@product_validations.messages) : self.errors.messages
|
||||
errors.each do |attr, message|
|
||||
invalid_attrs[attr.to_s] = "#{attr.to_s.capitalize} #{message.first}"
|
||||
end
|
||||
invalid_attrs.except(*non_product_attributes, *non_display_attributes)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def assign_units(attrs)
|
||||
units = UnitConverter.new(attrs)
|
||||
|
||||
units.converted_attributes.each do |attr, value|
|
||||
if respond_to?("#{attr}=")
|
||||
send("#{attr}=", value) unless non_product_attributes.include?(attr)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def non_display_attributes
|
||||
['id', 'product_id', 'unscaled_units', 'variant_id', 'supplier_id', 'primary_taxon', 'primary_taxon_id', 'category_id', 'shipping_category_id', 'tax_category_id', 'variant_unit_scale', 'variant_unit', 'unit_value']
|
||||
end
|
||||
|
||||
def non_product_attributes
|
||||
['line_number', 'valid', 'errors', 'product_object', 'product_validations', 'inventory_validations', 'validates_as', 'save_type', 'on_hand_nil', 'has_overrides']
|
||||
end
|
||||
end
|
||||
end
|
||||
79
app/models/product_import/unit_converter.rb
Normal file
79
app/models/product_import/unit_converter.rb
Normal file
@@ -0,0 +1,79 @@
|
||||
module ProductImport
|
||||
class UnitConverter
|
||||
def initialize(attrs)
|
||||
@attrs = attrs
|
||||
convert_custom_unit_fields
|
||||
end
|
||||
|
||||
def converted_attributes
|
||||
@attrs
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def convert_custom_unit_fields
|
||||
# units unit_type variant_unit_name -> unit_value variant_unit_scale variant_unit
|
||||
# 250 ml nil .... 0.25 0.001 volume
|
||||
# 50 g nil .... 50 1 weight
|
||||
# 2 kg nil .... 2000 1000 weight
|
||||
# 1 nil bunches .... 1 null items
|
||||
|
||||
init_unit_values
|
||||
|
||||
assign_weight_or_volume_attributes if units_and_unit_type_present?
|
||||
assign_item_attributes if units_and_variant_unit_name_present?
|
||||
end
|
||||
|
||||
def unit_scales
|
||||
{
|
||||
'g' => { scale: 1, unit: 'weight' },
|
||||
'kg' => { scale: 1000, unit: 'weight' },
|
||||
't' => { scale: 1000000, unit: 'weight' },
|
||||
'ml' => { scale: 0.001, unit: 'volume' },
|
||||
'l' => { scale: 1, unit: 'volume' },
|
||||
'kl' => { scale: 1000, unit: 'volume' }
|
||||
}
|
||||
end
|
||||
|
||||
def init_unit_values
|
||||
@attrs['variant_unit'] = nil
|
||||
@attrs['variant_unit_scale'] = nil
|
||||
@attrs['unit_value'] = nil
|
||||
|
||||
return unless @attrs.key?('units') && @attrs['units'].present?
|
||||
|
||||
@attrs['unscaled_units'] = @attrs['units']
|
||||
end
|
||||
|
||||
def assign_weight_or_volume_attributes
|
||||
units = @attrs['units'].to_f
|
||||
unit_type = @attrs['unit_type'].to_s.downcase
|
||||
|
||||
return unless valid_unit_type? unit_type
|
||||
|
||||
@attrs['variant_unit'] = unit_scales[unit_type][:unit]
|
||||
@attrs['variant_unit_scale'] = unit_scales[unit_type][:scale]
|
||||
@attrs['unit_value'] = (units || 0) * @attrs['variant_unit_scale']
|
||||
end
|
||||
|
||||
def assign_item_attributes
|
||||
units = @attrs['units'].to_f
|
||||
|
||||
@attrs['variant_unit'] = 'items'
|
||||
@attrs['variant_unit_scale'] = nil
|
||||
@attrs['unit_value'] = units || 1
|
||||
end
|
||||
|
||||
def units_and_unit_type_present?
|
||||
@attrs.key?('units') && @attrs.key?('unit_type') && @attrs['units'].present? && @attrs['unit_type'].present?
|
||||
end
|
||||
|
||||
def units_and_variant_unit_name_present?
|
||||
@attrs.key?('units') && @attrs.key?('variant_unit_name') && @attrs['units'].present? && @attrs['variant_unit_name'].present?
|
||||
end
|
||||
|
||||
def valid_unit_type?(unit_type)
|
||||
unit_scales.key? unit_type
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,464 +0,0 @@
|
||||
require 'roo'
|
||||
|
||||
class ProductImporter
|
||||
extend ActiveModel::Naming
|
||||
include ActiveModel::Conversion
|
||||
include ActiveModel::Validations
|
||||
|
||||
attr_reader :total_supplier_products
|
||||
|
||||
def initialize(file, editable_enterprises, import_settings={})
|
||||
if file.is_a?(File)
|
||||
@file = file
|
||||
@sheet = open_spreadsheet
|
||||
@entries = []
|
||||
@valid_entries = {}
|
||||
@invalid_entries = {}
|
||||
|
||||
@products_to_create = {}
|
||||
@variants_to_create = {}
|
||||
@variants_to_update = {}
|
||||
|
||||
@products_created = 0
|
||||
@variants_created = 0
|
||||
@variants_updated = 0
|
||||
|
||||
@import_settings = import_settings
|
||||
@editable_enterprises = {}
|
||||
editable_enterprises.map { |e| @editable_enterprises[e.name] = e.id }
|
||||
|
||||
@total_supplier_products = 0
|
||||
@products_to_reset = {}
|
||||
@updated_ids = []
|
||||
|
||||
init_product_importer if @sheet
|
||||
else
|
||||
self.errors.add(:importer, I18n.t(:product_importer_file_error))
|
||||
end
|
||||
end
|
||||
|
||||
def persisted?
|
||||
false #ActiveModel, not ActiveRecord
|
||||
end
|
||||
|
||||
def has_valid_entries?
|
||||
valid_count and valid_count > 0
|
||||
end
|
||||
|
||||
def item_count
|
||||
@sheet ? @sheet.last_row - 1 : 0
|
||||
end
|
||||
|
||||
def products_to_reset
|
||||
# Return indexed data about existing product count, reset count, and updates count per supplier
|
||||
@products_to_reset.each do |supplier_id, values|
|
||||
values[:updates_count] = 0 if values[:updates_count].blank?
|
||||
|
||||
if values[:updates_count] and values[:existing_products]
|
||||
@products_to_reset[supplier_id][:reset_count] = values[:existing_products] - values[:updates_count]
|
||||
end
|
||||
end
|
||||
@products_to_reset
|
||||
end
|
||||
|
||||
def valid_count
|
||||
@valid_entries.count
|
||||
end
|
||||
|
||||
def invalid_count
|
||||
@invalid_entries.count
|
||||
end
|
||||
|
||||
def products_create_count
|
||||
@products_to_create.count + @variants_to_create.count
|
||||
end
|
||||
|
||||
def products_update_count
|
||||
@variants_to_update.count
|
||||
end
|
||||
|
||||
def suppliers_index
|
||||
index = @suppliers_index || build_suppliers_index
|
||||
index.sort_by{ |k,v| v.to_i }.reverse.to_h
|
||||
end
|
||||
|
||||
def all_entries
|
||||
invalid_entries.merge(products_to_create).merge(products_to_update).sort.to_h
|
||||
end
|
||||
|
||||
def invalid_entries
|
||||
@invalid_entries
|
||||
end
|
||||
|
||||
def products_to_create
|
||||
@products_to_create.merge(@variants_to_create)
|
||||
end
|
||||
|
||||
def products_to_update
|
||||
@variants_to_update
|
||||
end
|
||||
|
||||
def products_created_count
|
||||
@products_created + @variants_created
|
||||
end
|
||||
|
||||
def products_updated_count
|
||||
@variants_updated
|
||||
end
|
||||
|
||||
def products_reset_count
|
||||
@products_reset_count || 0
|
||||
end
|
||||
|
||||
def total_saved_count
|
||||
@products_created + @variants_created + @variants_updated
|
||||
end
|
||||
|
||||
def save_all
|
||||
save_all_valid
|
||||
delete_uploaded_file
|
||||
end
|
||||
|
||||
def permission_by_name?(supplier_name)
|
||||
@editable_enterprises.has_key?(supplier_name)
|
||||
end
|
||||
|
||||
def permission_by_id?(supplier_id)
|
||||
@editable_enterprises.has_value?(Integer(supplier_id))
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def init_product_importer
|
||||
build_entries
|
||||
build_categories_index
|
||||
build_suppliers_index
|
||||
validate_all
|
||||
end
|
||||
|
||||
def open_spreadsheet
|
||||
if accepted_mimetype
|
||||
Roo::Spreadsheet.open(@file, extension: accepted_mimetype)
|
||||
else
|
||||
self.errors.add(:importer, I18n.t(:product_importer_spreadsheet_error))
|
||||
delete_uploaded_file
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
def accepted_mimetype
|
||||
File.extname(@file.path).in?('.csv', '.xls', '.xlsx', '.ods') ? @file.path.split('.').last.to_sym : false
|
||||
end
|
||||
|
||||
def headers
|
||||
@sheet.row(1)
|
||||
end
|
||||
|
||||
def rows
|
||||
return [] unless @sheet and @sheet.last_row
|
||||
(2..@sheet.last_row).map do |i|
|
||||
@sheet.row(i)
|
||||
end
|
||||
end
|
||||
|
||||
def build_entries
|
||||
rows.each_with_index do |row, i|
|
||||
row_data = Hash[[headers, row].transpose]
|
||||
entry = SpreadsheetEntry.new(row_data)
|
||||
entry.line_number = i+2
|
||||
@entries.push entry
|
||||
end
|
||||
@entries
|
||||
end
|
||||
|
||||
def validate_all
|
||||
@entries.each do |entry|
|
||||
supplier_validation(entry)
|
||||
category_validation(entry)
|
||||
set_update_status(entry)
|
||||
|
||||
mark_as_valid(entry) unless entry_invalid?(entry.line_number)
|
||||
end
|
||||
|
||||
count_existing_products
|
||||
delete_uploaded_file if item_count.zero? or valid_count.zero?
|
||||
end
|
||||
|
||||
def count_existing_products
|
||||
@suppliers_index.each do |supplier_name, supplier_id|
|
||||
if supplier_id and permission_by_id?(supplier_id)
|
||||
products_count = Spree::Variant.joins(:product).
|
||||
where('spree_products.supplier_id IN (?)
|
||||
AND spree_variants.is_master = false
|
||||
AND spree_variants.deleted_at IS NULL', supplier_id).
|
||||
count
|
||||
|
||||
if @products_to_reset[supplier_id]
|
||||
@products_to_reset[supplier_id][:existing_products] = products_count
|
||||
else
|
||||
@products_to_reset[supplier_id] = {existing_products: products_count}
|
||||
end
|
||||
|
||||
@total_supplier_products += products_count
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def entry_invalid?(line_number)
|
||||
!!@invalid_entries[line_number]
|
||||
end
|
||||
|
||||
def supplier_validation(entry)
|
||||
supplier_name = entry.supplier
|
||||
|
||||
if supplier_name.blank?
|
||||
mark_as_invalid(entry, attribute: "supplier", error: I18n.t(:error_required))
|
||||
return
|
||||
end
|
||||
|
||||
unless supplier_exists?(supplier_name)
|
||||
mark_as_invalid(entry, attribute: "supplier", error: I18n.t(:error_not_found_in_database, name: supplier_name))
|
||||
return
|
||||
end
|
||||
|
||||
unless permission_by_name?(supplier_name)
|
||||
mark_as_invalid(entry, attribute: "supplier", error: I18n.t(:error_no_permission_for_enterprise, name: supplier_name))
|
||||
return
|
||||
end
|
||||
|
||||
entry.supplier_id = @suppliers_index[supplier_name]
|
||||
end
|
||||
|
||||
def supplier_exists?(supplier_name)
|
||||
@suppliers_index[supplier_name]
|
||||
end
|
||||
|
||||
def category_validation(entry)
|
||||
category_name = entry.category
|
||||
|
||||
if category_name.blank?
|
||||
mark_as_invalid(entry, attribute: "category", error: I18n.t(:error_required))
|
||||
return
|
||||
end
|
||||
|
||||
if category_exists?(category_name)
|
||||
entry.primary_taxon_id = @categories_index[category_name]
|
||||
else
|
||||
mark_as_invalid(entry, attribute: "category", error: I18n.t(:error_not_found_in_database, name: category_name))
|
||||
end
|
||||
end
|
||||
|
||||
def category_exists?(category_name)
|
||||
@categories_index[category_name]
|
||||
end
|
||||
|
||||
def mark_as_valid(entry)
|
||||
@valid_entries[entry.line_number] = entry
|
||||
end
|
||||
|
||||
def mark_as_invalid(entry, options={})
|
||||
entry.errors.add(options[:attribute], options[:error]) if options[:attribute] and options[:error]
|
||||
entry.product_validations = options[:product_validations] if options[:product_validations]
|
||||
|
||||
@invalid_entries[entry.line_number] = entry
|
||||
end
|
||||
|
||||
# Minimise db queries by getting a list of suppliers to look
|
||||
# up, instead of doing a query for each entry in the spreadsheet
|
||||
def build_suppliers_index
|
||||
@suppliers_index = {}
|
||||
@entries.each do |entry|
|
||||
supplier_name = entry.supplier
|
||||
supplier_id = @suppliers_index[supplier_name] ||
|
||||
Enterprise.find_by_name(supplier_name, :select => 'id, name').try(:id)
|
||||
@suppliers_index[supplier_name] = supplier_id
|
||||
end
|
||||
@suppliers_index
|
||||
end
|
||||
|
||||
def build_categories_index
|
||||
@categories_index = {}
|
||||
@entries.each do |entry|
|
||||
category_name = entry.category
|
||||
category_id = @categories_index[category_name] ||
|
||||
Spree::Taxon.find_by_name(category_name, :select => 'id, name').try(:id)
|
||||
@categories_index[category_name] = category_id
|
||||
end
|
||||
@categories_index
|
||||
end
|
||||
|
||||
def save_all_valid
|
||||
already_created = {}
|
||||
@products_to_create.each do |line_number, entry|
|
||||
# If we've already added a new product with these attributes
|
||||
# from this spreadsheet, mark this entry as a new variant with
|
||||
# the new product id, as this is a now variant of that product...
|
||||
if already_created[entry.supplier_id] and already_created[entry.supplier_id][entry.name]
|
||||
product_id = already_created[entry.supplier_id][entry.name]
|
||||
mark_as_new_variant(entry, product_id)
|
||||
next
|
||||
end
|
||||
|
||||
product = Spree::Product.new()
|
||||
product.assign_attributes(entry.attributes.except('id'))
|
||||
assign_defaults(product, entry.attributes)
|
||||
if product.save
|
||||
ensure_variant_updated(product, entry)
|
||||
@products_created += 1
|
||||
@updated_ids.push product.variants.first.id
|
||||
else
|
||||
self.errors.add("Line #{line_number}:", product.errors.full_messages) #TODO: change
|
||||
end
|
||||
|
||||
already_created[entry.supplier_id] = {entry.name => product.id}
|
||||
end
|
||||
|
||||
@variants_to_update.each do |line_number, entry|
|
||||
variant = entry.product_object
|
||||
assign_defaults(variant, entry.attributes)
|
||||
if variant.valid? and variant.save
|
||||
@variants_updated += 1
|
||||
@updated_ids.push variant.id
|
||||
else
|
||||
self.errors.add("Line #{line_number}:", variant.errors.full_messages) #TODO: change
|
||||
end
|
||||
end
|
||||
|
||||
@variants_to_create.each do |line_number, entry|
|
||||
new_variant = entry.product_object
|
||||
assign_defaults(new_variant, entry.attributes)
|
||||
if new_variant.valid? and new_variant.save
|
||||
@variants_created += 1
|
||||
@updated_ids.push new_variant.id
|
||||
else
|
||||
self.errors.add("Line #{line_number}:", new_variant.errors.full_messages)
|
||||
end
|
||||
end
|
||||
|
||||
self.errors.add(:importer, I18n.t(:product_importer_products_save_error)) if total_saved_count.zero?
|
||||
|
||||
reset_absent_products
|
||||
total_saved_count
|
||||
end
|
||||
|
||||
def reset_absent_products
|
||||
return if total_saved_count.zero?
|
||||
|
||||
enterprises_to_reset = []
|
||||
@import_settings.each do |enterprise_id, settings|
|
||||
enterprises_to_reset.push enterprise_id if settings['reset_all_absent'] and permission_by_id?(enterprise_id)
|
||||
end
|
||||
|
||||
unless enterprises_to_reset.empty? or @updated_ids.empty?
|
||||
# For selected enterprises; set stock to zero for all products
|
||||
# that were not present in the uploaded spreadsheet
|
||||
@products_reset_count = Spree::Variant.joins(:product).
|
||||
where('spree_products.supplier_id IN (?)
|
||||
AND spree_variants.id NOT IN (?)
|
||||
AND spree_variants.is_master = false
|
||||
AND spree_variants.deleted_at IS NULL', enterprises_to_reset, @updated_ids).
|
||||
update_all(count_on_hand: 0)
|
||||
end
|
||||
end
|
||||
|
||||
def assign_defaults(object, entry)
|
||||
@import_settings[entry['supplier_id'].to_s]['defaults'].each do |attribute, setting|
|
||||
case setting['mode']
|
||||
when 'overwrite_all'
|
||||
object.assign_attributes(attribute => setting['value'])
|
||||
when 'overwrite_empty'
|
||||
if object.send(attribute).blank? or (attribute == 'on_hand' and entry['on_hand_nil'])
|
||||
object.assign_attributes(attribute => setting['value'])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def ensure_variant_updated(product, entry)
|
||||
# Ensure display_name and on_demand are copied to new product's variant
|
||||
if entry.display_name || entry.on_demand
|
||||
variant = product.variants.first
|
||||
variant.display_name = entry.display_name if entry.display_name
|
||||
variant.on_demand = entry.on_demand if entry.on_demand
|
||||
variant.save
|
||||
end
|
||||
end
|
||||
|
||||
def set_update_status(entry)
|
||||
# Find product with matching supplier and name
|
||||
match = Spree::Product.where(supplier_id: entry.supplier_id, name: entry.name, deleted_at: nil).first
|
||||
|
||||
# If no matching product was found, create a new product
|
||||
if match.nil?
|
||||
mark_as_new_product(entry)
|
||||
return
|
||||
end
|
||||
|
||||
# Otherwise, if a variant exists with matching display_name and unit_value, update it
|
||||
match.variants.each do |existing_variant|
|
||||
if existing_variant.display_name == entry.display_name && existing_variant.unit_value == Float(entry.unit_value)
|
||||
mark_as_existing_variant(entry, existing_variant)
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
# Otherwise, a variant with sufficiently matching attributes doesn't exist; create a new one
|
||||
mark_as_new_variant(entry, match.id)
|
||||
end
|
||||
|
||||
def mark_as_new_product(entry)
|
||||
new_product = Spree::Product.new()
|
||||
new_product.assign_attributes(entry.attributes.except('id'))
|
||||
if new_product.valid?
|
||||
@products_to_create[entry.line_number] = entry unless entry_invalid?(entry.line_number)
|
||||
else
|
||||
mark_as_invalid(entry, product_validations: new_product.errors)
|
||||
end
|
||||
end
|
||||
|
||||
def mark_as_existing_variant(entry, existing_variant)
|
||||
existing_variant.assign_attributes(entry.attributes.except('id', 'product_id'))
|
||||
check_on_hand_nil(entry, existing_variant)
|
||||
if existing_variant.valid?
|
||||
entry.product_object = existing_variant
|
||||
@variants_to_update[entry.line_number] = entry unless entry_invalid?(entry.line_number)
|
||||
updates_count_per_supplier(entry.supplier_id) unless entry_invalid?(entry.line_number)
|
||||
else
|
||||
mark_as_invalid(entry, product_validations: existing_variant.errors)
|
||||
end
|
||||
end
|
||||
|
||||
def mark_as_new_variant(entry, product_id)
|
||||
new_variant = Spree::Variant.new(entry.attributes.except('id', 'product_id'))
|
||||
new_variant.product_id = product_id
|
||||
check_on_hand_nil(entry, new_variant)
|
||||
if new_variant.valid?
|
||||
entry.product_object = new_variant
|
||||
@variants_to_create[entry.line_number] = entry unless entry_invalid?(entry.line_number)
|
||||
else
|
||||
mark_as_invalid(entry, product_validations: new_variant.errors)
|
||||
end
|
||||
end
|
||||
|
||||
def updates_count_per_supplier(supplier_id)
|
||||
if @products_to_reset[supplier_id] and @products_to_reset[supplier_id][:updates_count]
|
||||
@products_to_reset[supplier_id][:updates_count] += 1
|
||||
else
|
||||
@products_to_reset[supplier_id] = {updates_count: 1}
|
||||
end
|
||||
end
|
||||
|
||||
def check_on_hand_nil(entry, variant)
|
||||
if entry.on_hand.blank?
|
||||
variant.on_hand = 0
|
||||
entry.on_hand_nil = true
|
||||
end
|
||||
end
|
||||
|
||||
def delete_uploaded_file
|
||||
# Only delete if file is in '/tmp/product_import' directory
|
||||
if @file.path == Rails.root.join('tmp', 'product_import').to_s
|
||||
File.delete(@file)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,68 +0,0 @@
|
||||
# Class for defining spreadsheet entry objects for use in ProductImporter
|
||||
class SpreadsheetEntry
|
||||
extend ActiveModel::Naming
|
||||
include ActiveModel::Conversion
|
||||
include ActiveModel::Validations
|
||||
|
||||
attr_accessor :line_number, :valid, :product_object, :product_validations, :save_type, :on_hand_nil
|
||||
|
||||
attr_accessor :id, :product_id, :supplier, :supplier_id, :name, :display_name, :sku,
|
||||
:unit_value, :unit_description, :variant_unit, :variant_unit_scale, :variant_unit_name,
|
||||
:display_as, :category, :primary_taxon_id, :price, :on_hand, :on_demand,
|
||||
:tax_category_id, :shipping_category_id, :description
|
||||
|
||||
def initialize(attrs)
|
||||
@product_validations = {}
|
||||
|
||||
attrs.each do |k, v|
|
||||
if self.respond_to?("#{k}=")
|
||||
send("#{k}=", v) unless non_product_attributes.include?(k)
|
||||
else
|
||||
# Trying to assign unknown attribute. Record this and give feedback or just ignore silently?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def persisted?
|
||||
false #ActiveModel
|
||||
end
|
||||
|
||||
def has_errors?
|
||||
self.errors.count > 0 or @product_validations.count > 0
|
||||
end
|
||||
|
||||
def attributes
|
||||
attrs = {}
|
||||
self.instance_variables.each do |var|
|
||||
attrs[var.to_s.delete("@")] = self.instance_variable_get(var)
|
||||
end
|
||||
attrs.except(*non_product_attributes)
|
||||
end
|
||||
|
||||
def displayable_attributes
|
||||
# Modified attributes list for displaying in user feedback
|
||||
attrs = {}
|
||||
self.instance_variables.each do |var|
|
||||
attrs[var.to_s.delete("@")] = self.instance_variable_get(var)
|
||||
end
|
||||
attrs.except(*non_product_attributes, *non_display_attributes)
|
||||
end
|
||||
|
||||
def invalid_attributes
|
||||
invalid_attrs = {}
|
||||
@product_validations.messages.merge(self.errors.messages).each do |attr, message|
|
||||
invalid_attrs[attr.to_s] = "#{attr.to_s.capitalize} #{message.first}"
|
||||
end
|
||||
invalid_attrs.except(*non_product_attributes, *non_display_attributes)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def non_display_attributes
|
||||
['id', 'product_id', 'variant_id', 'supplier_id', 'primary_taxon', 'primary_taxon_id', 'category_id', 'shipping_category_id', 'tax_category_id']
|
||||
end
|
||||
|
||||
def non_product_attributes
|
||||
['line_number', 'valid', 'errors', 'product_object', 'product_validations', 'save_type', 'on_hand_nil']
|
||||
end
|
||||
end
|
||||
@@ -61,7 +61,7 @@ class AbilityDecorator
|
||||
order.user == user
|
||||
end
|
||||
|
||||
can [:destroy], Spree::CreditCard do |credit_card|
|
||||
can [:update, :destroy], Spree::CreditCard do |credit_card|
|
||||
credit_card.user == user
|
||||
end
|
||||
end
|
||||
@@ -127,12 +127,14 @@ class AbilityDecorator
|
||||
can [:admin, :connect, :status, :destroy], StripeAccount do |stripe_account|
|
||||
user.enterprises.include? stripe_account.enterprise
|
||||
end
|
||||
|
||||
can [:admin, :create], :manager_invitation
|
||||
end
|
||||
|
||||
def add_product_management_abilities(user)
|
||||
# Enterprise User can only access products that they are a supplier for
|
||||
can [:create], Spree::Product
|
||||
can [:admin, :read, :update, :product_distributions, :seo, :group_buy_options, :bulk_edit, :bulk_update, :clone, :delete, :destroy], Spree::Product do |product|
|
||||
can [:admin, :read, :index, :update, :product_distributions, :seo, :group_buy_options, :bulk_update, :clone, :delete, :destroy], Spree::Product do |product|
|
||||
OpenFoodNetwork::Permissions.new(user).managed_product_enterprises.include? product.supplier
|
||||
end
|
||||
|
||||
@@ -175,7 +177,7 @@ class AbilityDecorator
|
||||
can [:admin, :index, :read, :search], Spree::Taxon
|
||||
can [:admin, :index, :read, :create, :edit], Spree::Classification
|
||||
|
||||
can [:admin, :index, :import, :save], ProductImporter
|
||||
can [:admin, :index, :guide, :import, :save, :save_data, :validate_data, :reset_absent_products], ProductImport::ProductImporter
|
||||
|
||||
# Reports page
|
||||
can [:admin, :index, :customers, :orders_and_distributors, :group_buys, :bulk_coop, :payments, :orders_and_fulfillment, :products_and_inventory, :order_cycle_management, :packing], :report
|
||||
|
||||
@@ -5,7 +5,7 @@ Spree::CreditCard.class_eval do
|
||||
attr_accessible :cc_type, :last_digits
|
||||
|
||||
# For holding customer preference in memory
|
||||
attr_accessible :save_requested_by_customer
|
||||
attr_accessible :save_requested_by_customer, :is_default
|
||||
attr_writer :save_requested_by_customer
|
||||
|
||||
# Should be able to remove once we reach Spree v2.2.0
|
||||
@@ -14,15 +14,31 @@ Spree::CreditCard.class_eval do
|
||||
|
||||
belongs_to :user
|
||||
|
||||
after_create :ensure_single_default_card
|
||||
after_save :ensure_single_default_card, if: :is_default_changed?
|
||||
|
||||
# Allows us to use a gateway_payment_profile_id to store Stripe Tokens
|
||||
# Should be able to remove once we reach Spree v2.2.0
|
||||
# Commit: https://github.com/spree/spree/commit/5a4d690ebc64b264bf12904a70187e7a8735ef3f
|
||||
# See also: https://github.com/spree/spree_gateway/issues/111
|
||||
def has_payment_profile? # rubocop:disable Style/PredicateName
|
||||
def has_payment_profile? # rubocop:disable Naming/PredicateName
|
||||
gateway_customer_profile_id.present? || gateway_payment_profile_id.present?
|
||||
end
|
||||
|
||||
def save_requested_by_customer?
|
||||
!!@save_requested_by_customer
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def default_missing?
|
||||
!user.credit_cards.exists?(is_default: true)
|
||||
end
|
||||
|
||||
def ensure_single_default_card
|
||||
return unless user
|
||||
return unless is_default? || default_missing?
|
||||
user.credit_cards.update_all(['is_default=(id=?)', id])
|
||||
self.is_default = true
|
||||
end
|
||||
end
|
||||
|
||||
@@ -17,6 +17,7 @@ Spree::Order.class_eval do
|
||||
|
||||
validates :customer, presence: true, if: :require_customer?
|
||||
validate :products_available_from_new_distribution, :if => lambda { distributor_id_changed? || order_cycle_id_changed? }
|
||||
validate :disallow_guest_order
|
||||
attr_accessible :order_cycle_id, :distributor_id, :customer_id
|
||||
|
||||
before_validation :shipping_address_from_distributor
|
||||
@@ -82,6 +83,20 @@ Spree::Order.class_eval do
|
||||
errors.add(:base, I18n.t(:spree_order_availability_error)) unless DistributionChangeValidator.new(self).can_change_to_distribution?(distributor, order_cycle)
|
||||
end
|
||||
|
||||
def using_guest_checkout?
|
||||
require_email && !user.andand.id
|
||||
end
|
||||
|
||||
def registered_email?
|
||||
Spree.user_class.exists?(email: email)
|
||||
end
|
||||
|
||||
def disallow_guest_order
|
||||
if using_guest_checkout? && registered_email?
|
||||
errors.add(:base, I18n.t('devise.failure.already_registered'))
|
||||
end
|
||||
end
|
||||
|
||||
def empty_with_clear_shipping_and_payments!
|
||||
empty_without_clear_shipping_and_payments!
|
||||
payments.clear
|
||||
|
||||
@@ -174,6 +174,14 @@ Spree::Product.class_eval do
|
||||
order_cycle.variants_distributed_by(distributor).where(product_id: self)
|
||||
end
|
||||
|
||||
# Get the most recent import_date of a product's variants
|
||||
def import_date
|
||||
variants.map do |variant|
|
||||
next if variant.import_date.blank?
|
||||
variant.import_date
|
||||
end.sort.last
|
||||
end
|
||||
|
||||
# Build a product distribution for each distributor
|
||||
def build_product_distributions_for_user user
|
||||
Enterprise.is_distributor.managed_by(user).each do |distributor|
|
||||
|
||||
@@ -10,13 +10,12 @@ Spree::Variant.class_eval do
|
||||
remove_method :options_text if instance_methods(false).include? :options_text
|
||||
include OpenFoodNetwork::VariantAndLineItemNaming
|
||||
|
||||
|
||||
has_many :exchange_variants
|
||||
has_many :exchanges, through: :exchange_variants
|
||||
has_many :variant_overrides
|
||||
has_many :inventory_items
|
||||
|
||||
attr_accessible :unit_value, :unit_description, :images_attributes, :display_as, :display_name
|
||||
attr_accessible :unit_value, :unit_description, :images_attributes, :display_as, :display_name, :import_date
|
||||
accepts_nested_attributes_for :images
|
||||
|
||||
validates_presence_of :unit_value,
|
||||
@@ -30,10 +29,11 @@ Spree::Variant.class_eval do
|
||||
after_save :refresh_products_cache
|
||||
around_destroy :destruction
|
||||
|
||||
|
||||
scope :with_order_cycles_inner, joins(exchanges: :order_cycle)
|
||||
|
||||
scope :not_deleted, where(deleted_at: nil)
|
||||
scope :not_master, where(is_master: false)
|
||||
scope :in_stock, where('spree_variants.count_on_hand > 0 OR spree_variants.on_demand=?', true)
|
||||
scope :in_order_cycle, lambda { |order_cycle|
|
||||
with_order_cycles_inner.
|
||||
merge(Exchange.outgoing).
|
||||
@@ -117,7 +117,6 @@ Spree::Variant.class_eval do
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
private
|
||||
|
||||
def update_weight_from_unit_value
|
||||
|
||||
@@ -3,6 +3,8 @@ class VariantOverride < ActiveRecord::Base
|
||||
|
||||
acts_as_taggable
|
||||
|
||||
attr_accessor :import_date
|
||||
|
||||
belongs_to :hub, class_name: 'Enterprise'
|
||||
belongs_to :variant, class_name: 'Spree::Variant'
|
||||
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
/ set_attributes "div[data-hook='customer_fields'] div.alpha"
|
||||
/ attributes({class: "fullwidth"})
|
||||
@@ -0,0 +1 @@
|
||||
remove "div[data-hook='customer_fields'] div.omega"
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user