diff --git a/.rspec_parallel b/.rspec_parallel index 867e417e06..cee31855e2 100644 --- a/.rspec_parallel +++ b/.rspec_parallel @@ -1,4 +1,4 @@ ---format progress +--format Fuubar --format ParallelTests::RSpec::SummaryLogger --out tmp/spec_summary.log --format ParallelTests::RSpec::RuntimeLogger --out tmp/parallel_runtime_rspec.log --tag ~performance diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000000..31ccc06991 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,33 @@ +# Contributing + +We love pull requests from everyone. Here are some instructions for +contributing code to Open Food Network. + +Fork, then clone the repo: + + git clone git@github.com:your-username/openfoodnetwork.git + +Follow the instructions in README.markdown to set up your machine. + +Make sure the tests pass: + + rspec spec + +Make your change. Add tests for your change. Make the tests pass: + + rspec spec + +Push to your fork and [submit a pull request][pr]. + +[pr]: https://github.com/openfoodfoundation/openfoodnetwork/compare/ + +At this point you're waiting on us. We may suggest some changes or +improvements or alternatives. + +To increase the chance that your pull request is swiftly accepted: + +* Write tests +* Use a style consistent with the rest of the codebase +* Before submitting, [rebase your work][rebase] on the current master branch + +[rebase]: https://www.atlassian.com/git/tutorials/merging-vs-rebasing/workflow-walkthrough diff --git a/Gemfile b/Gemfile index d8e4c08bc9..6445843148 100644 --- a/Gemfile +++ b/Gemfile @@ -32,6 +32,7 @@ gem 'newrelic_rpm' gem 'haml' gem 'sass', "~> 3.3" gem 'sass-rails', '~> 3.2.3', groups: [:default, :assets] +gem 'redcarpet' gem 'aws-sdk' gem 'db2fog' gem 'andand' @@ -51,7 +52,9 @@ gem 'custom_error_message', :github => 'jeremydurham/custom-err-msg' gem 'angularjs-file-upload-rails', '~> 1.1.0' gem 'roadie-rails', '~> 1.0.3' gem 'figaro' +gem 'blockenspiel' gem 'acts-as-taggable-on', '~> 3.4' +gem 'paper_trail', '~> 3.0.8' gem 'foreigner' gem 'immigrant' @@ -72,13 +75,14 @@ group :assets do gem 'turbo-sprockets-rails3' gem 'foundation-icons-sass-rails' gem 'momentjs-rails' - gem 'angular-rails-templates' + gem 'angular-rails-templates', '~> 0.2.0' end + gem "foundation-rails" gem 'foundation_rails_helper', github: 'willrjmarshall/foundation_rails_helper', branch: "rails3" gem 'jquery-rails' - +gem 'css_splitter' group :test, :development do @@ -95,8 +99,10 @@ group :test, :development do gem 'letter_opener' gem 'timecop' gem 'poltergeist' + gem 'rspec-retry' gem 'json_spec' gem 'unicorn-rails' + gem 'atomic' end group :test do diff --git a/Gemfile.lock b/Gemfile.lock index 244346a15f..333eab0648 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -147,14 +147,15 @@ GEM acts_as_list (0.1.4) addressable (2.3.3) andand (1.3.3) - angular-rails-templates (0.1.1) + angular-rails-templates (0.2.0) railties (>= 3.1) - sprockets + sprockets (~> 2) tilt angularjs-file-upload-rails (1.1.0) angularjs-rails (1.2.13) ansi (1.4.2) arel (3.0.3) + atomic (1.1.99) awesome_nested_set (2.1.5) activerecord (>= 3.0.0) awesome_print (1.0.2) @@ -165,6 +166,7 @@ GEM bcrypt (3.1.7) bcrypt-ruby (3.1.5) bcrypt (>= 3.1.3) + blockenspiel (0.4.5) bugsnag (1.5.2) httparty (>= 0.6, < 1.0) multi_json (~> 1.0) @@ -219,6 +221,8 @@ GEM safe_yaml (~> 0.9.0) css_parser (1.3.5) addressable + css_splitter (0.4.1) + sprockets (>= 2.0.0) daemons (1.2.2) dalli (2.7.2) database_cleaner (0.7.1) @@ -320,7 +324,7 @@ GEM jquery-rails (2.2.2) railties (>= 3.0, < 5.0) thor (>= 0.14, < 2.0) - json (1.8.2) + json (1.8.3) json_spec (1.1.1) multi_json (~> 1.0) rspec (~> 2.0) @@ -353,11 +357,14 @@ GEM net-scp (1.1.2) net-ssh (>= 2.6.5) net-ssh (2.6.8) - newrelic_rpm (3.6.7.152) + newrelic_rpm (3.12.0.288) nokogiri (1.6.6.2) mini_portile (~> 0.6.0) oj (2.1.2) orm_adapter (0.5.0) + paper_trail (3.0.8) + activerecord (>= 3.0, < 5.0) + activesupport (>= 3.0, < 5.0) paperclip (3.5.4) activemodel (>= 3.0.0) activesupport (>= 3.0.0) @@ -428,6 +435,7 @@ GEM ffi (>= 0.5.0) rdoc (3.12.2) json (~> 1.4) + redcarpet (3.2.3) ref (1.0.5) representative (1.0.5) activesupport (>= 2.2.2) @@ -458,6 +466,8 @@ GEM rspec-core (~> 2.14.0) rspec-expectations (~> 2.14.0) rspec-mocks (~> 2.14.0) + rspec-retry (0.4.2) + rspec-core ruby-hmac (0.4.0) ruby-progressbar (1.7.1) safe_yaml (0.9.5) @@ -538,16 +548,19 @@ DEPENDENCIES active_model_serializers acts-as-taggable-on (~> 3.4) andand - angular-rails-templates + angular-rails-templates (~> 0.2.0) angularjs-file-upload-rails (~> 1.1.0) angularjs-rails (= 1.2.13) + atomic awesome_print aws-sdk + blockenspiel bugsnag capybara coffee-rails (~> 3.2.1) comfortable_mexican_sofa compass-rails + css_splitter custom_error_message! daemons dalli @@ -580,6 +593,7 @@ DEPENDENCIES newrelic_rpm nokogiri oj + paper_trail (~> 3.0.8) paperclip parallel_tests pg @@ -590,9 +604,11 @@ DEPENDENCIES rack-ssl rails (= 3.2.21) rails-i18n (~> 3.0.0) + redcarpet representative_view roadie-rails (~> 1.0.3) rspec-rails + rspec-retry sass (~> 3.3) sass-rails (~> 3.2.3) shoulda-matchers diff --git a/README.markdown b/README.markdown index b9aaf8193c..15c6920be1 100644 --- a/README.markdown +++ b/README.markdown @@ -12,10 +12,15 @@ We're part of global movement - get involved! * Find out more and join in the conversation - http://openfoodnetwork.org +## Getting started + +Below are instructions for setting up a development environment for Open Food Network. If you're interested in provisioning a server, see [the project's Ansible playbooks](https://github.com/openfoodfoundation/ofn_deployment). + + ## Dependencies * Rails 3.2.x -* Ruby >= 1.9.3 +* Ruby 1.9.3 * PostgreSQL database * PhantomJS (for testing) * See Gemfile for a list of gems required @@ -32,19 +37,20 @@ You can view the code at: You can download the source with the command: - git clone git@github.com:openfoodfoundation/openfoodnetwork + git clone https://github.com/openfoodfoundation/openfoodnetwork.git ## Get it running For those new to Rails, the following tutorial will help get you up to speed with configuring a Rails environment: http://guides.rubyonrails.org/getting_started.html . -First, check your dependencies: Ensure that you have Ruby 1.9.x installed: +First, check your dependencies: Ensure that you have Ruby >= 1.9.3 installed: ruby --version Install the project's gem dependencies: + cd openfoodnetwork bundle install Configure the site: diff --git a/app/assets/images/case-studies/South_East_Food_Hub.png b/app/assets/images/case-studies/South_East_Food_Hub.png new file mode 100644 index 0000000000..1760a68d72 Binary files /dev/null and b/app/assets/images/case-studies/South_East_Food_Hub.png differ diff --git a/app/assets/images/case-studies/baw-baw.png b/app/assets/images/case-studies/baw-baw.png new file mode 100644 index 0000000000..56dd2c94f1 Binary files /dev/null and b/app/assets/images/case-studies/baw-baw.png differ diff --git a/app/assets/images/case-studies/bfc_logo_square.png b/app/assets/images/case-studies/bfc_logo_square.png new file mode 100644 index 0000000000..90f54df507 Binary files /dev/null and b/app/assets/images/case-studies/bfc_logo_square.png differ diff --git a/app/assets/images/case-studies/bonnie-beef-growers.png b/app/assets/images/case-studies/bonnie-beef-growers.png new file mode 100644 index 0000000000..ad9f5b14ad Binary files /dev/null and b/app/assets/images/case-studies/bonnie-beef-growers.png differ diff --git a/app/assets/images/case-studies/jindivick.jpg b/app/assets/images/case-studies/jindivick.jpg new file mode 100644 index 0000000000..b94ab00166 Binary files /dev/null and b/app/assets/images/case-studies/jindivick.jpg differ diff --git a/app/assets/images/case-studies/jonai.png b/app/assets/images/case-studies/jonai.png new file mode 100644 index 0000000000..6cdf7dd20b Binary files /dev/null and b/app/assets/images/case-studies/jonai.png differ diff --git a/app/assets/images/case-studies/longley.png b/app/assets/images/case-studies/longley.png new file mode 100644 index 0000000000..0d20ae93df Binary files /dev/null and b/app/assets/images/case-studies/longley.png differ diff --git a/app/assets/images/case-studies/mt-alexander.png b/app/assets/images/case-studies/mt-alexander.png new file mode 100644 index 0000000000..efb86d2463 Binary files /dev/null and b/app/assets/images/case-studies/mt-alexander.png differ diff --git a/app/assets/images/case-studies/wandiful.png b/app/assets/images/case-studies/wandiful.png new file mode 100644 index 0000000000..f7c4469022 Binary files /dev/null and b/app/assets/images/case-studies/wandiful.png differ diff --git a/app/assets/images/enterprise-type.png b/app/assets/images/enterprise-type.png new file mode 100644 index 0000000000..0c5741a82f Binary files /dev/null and b/app/assets/images/enterprise-type.png differ diff --git a/app/assets/images/favicon.ico b/app/assets/images/favicon.ico deleted file mode 100644 index 4011e1ba8b..0000000000 Binary files a/app/assets/images/favicon.ico and /dev/null differ diff --git a/app/assets/images/favicon.png b/app/assets/images/favicon.png deleted file mode 100644 index 758c60d7e2..0000000000 Binary files a/app/assets/images/favicon.png and /dev/null differ diff --git a/app/assets/images/groups.svg b/app/assets/images/groups.svg index f4ca32ec27..a2e353d47f 100644 --- a/app/assets/images/groups.svg +++ b/app/assets/images/groups.svg @@ -1,1565 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/app/assets/images/home/background-blurred-oranges.jpg b/app/assets/images/home/background-blurred-oranges.jpg new file mode 100644 index 0000000000..61019198e3 Binary files /dev/null and b/app/assets/images/home/background-blurred-oranges.jpg differ diff --git a/app/assets/images/home/home-apples.jpg b/app/assets/images/home/home-apples.jpg new file mode 100644 index 0000000000..5cf22d4a50 Binary files /dev/null and b/app/assets/images/home/home-apples.jpg differ diff --git a/app/assets/images/home/home-oranges.jpg b/app/assets/images/home/home-oranges.jpg new file mode 100644 index 0000000000..6cd547c97f Binary files /dev/null and b/app/assets/images/home/home-oranges.jpg differ diff --git a/app/assets/images/home/home-strawberries.jpg b/app/assets/images/home/home-strawberries.jpg new file mode 100644 index 0000000000..e837c69065 Binary files /dev/null and b/app/assets/images/home/home-strawberries.jpg differ diff --git a/app/assets/images/home/home.jpg b/app/assets/images/home/home.jpg new file mode 100644 index 0000000000..cb59fbef31 Binary files /dev/null and b/app/assets/images/home/home.jpg differ diff --git a/app/assets/images/home/home1.jpg b/app/assets/images/home/home1.jpg new file mode 100644 index 0000000000..0e37397127 Binary files /dev/null and b/app/assets/images/home/home1.jpg differ diff --git a/app/assets/images/home/home2.jpg b/app/assets/images/home/home2.jpg new file mode 100644 index 0000000000..fa548cb519 Binary files /dev/null and b/app/assets/images/home/home2.jpg differ diff --git a/app/assets/images/home/home3.jpg b/app/assets/images/home/home3.jpg new file mode 100644 index 0000000000..26fdf1037c Binary files /dev/null and b/app/assets/images/home/home3.jpg differ diff --git a/app/assets/images/home/ofn_bg_1.jpg b/app/assets/images/home/ofn_bg_1.jpg deleted file mode 100644 index 416ebf35fb..0000000000 Binary files a/app/assets/images/home/ofn_bg_1.jpg and /dev/null differ diff --git a/app/assets/images/home/shopping-bg.jpg b/app/assets/images/home/shopping-bg.jpg deleted file mode 100644 index 0a02b0f6f4..0000000000 Binary files a/app/assets/images/home/shopping-bg.jpg and /dev/null differ diff --git a/app/assets/images/home/tagline-bg.jpg b/app/assets/images/home/tagline-bg.jpg deleted file mode 100644 index 68366a9569..0000000000 Binary files a/app/assets/images/home/tagline-bg.jpg and /dev/null differ diff --git a/app/assets/images/hubs-bg.jpg b/app/assets/images/hubs-bg.jpg new file mode 100644 index 0000000000..2d1c49e230 Binary files /dev/null and b/app/assets/images/hubs-bg.jpg differ diff --git a/app/assets/images/icon-mask-apple.png b/app/assets/images/icon-mask-apple.png new file mode 100644 index 0000000000..87bfa91bb8 Binary files /dev/null and b/app/assets/images/icon-mask-apple.png differ diff --git a/app/assets/images/icon-mask-bread.png b/app/assets/images/icon-mask-bread.png new file mode 100644 index 0000000000..d5c29d96ff Binary files /dev/null and b/app/assets/images/icon-mask-bread.png differ diff --git a/app/assets/images/icon-mask-magnifier.png b/app/assets/images/icon-mask-magnifier.png new file mode 100644 index 0000000000..7f53b7db9d Binary files /dev/null and b/app/assets/images/icon-mask-magnifier.png differ diff --git a/app/assets/images/icon-mask-truck.png b/app/assets/images/icon-mask-truck.png new file mode 100644 index 0000000000..40550f98c2 Binary files /dev/null and b/app/assets/images/icon-mask-truck.png differ diff --git a/app/assets/images/logo-australia.png b/app/assets/images/logo-australia.png new file mode 100644 index 0000000000..46b8ef071a Binary files /dev/null and b/app/assets/images/logo-australia.png differ diff --git a/app/assets/images/logo-black.png b/app/assets/images/logo-black.png new file mode 100644 index 0000000000..31ec1422f1 Binary files /dev/null and b/app/assets/images/logo-black.png differ diff --git a/app/assets/images/logo-black.svg b/app/assets/images/logo-black.svg new file mode 100644 index 0000000000..5f69f364c4 --- /dev/null +++ b/app/assets/images/logo-black.svg @@ -0,0 +1,82 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + diff --git a/app/assets/images/logo-color.png b/app/assets/images/logo-color.png new file mode 100644 index 0000000000..e464781a54 Binary files /dev/null and b/app/assets/images/logo-color.png differ diff --git a/app/assets/images/logo-color.svg b/app/assets/images/logo-color.svg new file mode 100644 index 0000000000..84de1b4ceb --- /dev/null +++ b/app/assets/images/logo-color.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/assets/images/logo-global-white.png b/app/assets/images/logo-global-white.png new file mode 100644 index 0000000000..8761bafe9d Binary files /dev/null and b/app/assets/images/logo-global-white.png differ diff --git a/app/assets/images/logo-white-notext.png b/app/assets/images/logo-white-notext.png new file mode 100644 index 0000000000..bfd590c621 Binary files /dev/null and b/app/assets/images/logo-white-notext.png differ diff --git a/app/assets/images/logo-white.png b/app/assets/images/logo-white.png new file mode 100644 index 0000000000..ffd1c6f73a Binary files /dev/null and b/app/assets/images/logo-white.png differ diff --git a/app/assets/images/logo-white.svg b/app/assets/images/logo-white.svg new file mode 100644 index 0000000000..c2c22bab40 --- /dev/null +++ b/app/assets/images/logo-white.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/assets/images/logo.jpg b/app/assets/images/logo.jpg deleted file mode 100644 index 49e232eb55..0000000000 Binary files a/app/assets/images/logo.jpg and /dev/null differ diff --git a/app/assets/images/logo.png b/app/assets/images/logo.png deleted file mode 100644 index aca7aac1dc..0000000000 Binary files a/app/assets/images/logo.png and /dev/null differ diff --git a/app/assets/images/noimage/large.png b/app/assets/images/noimage/large.png index 29dcff5ea9..166a74488a 100644 Binary files a/app/assets/images/noimage/large.png and b/app/assets/images/noimage/large.png differ diff --git a/app/assets/images/noimage/mini.png b/app/assets/images/noimage/mini.png index 5094c92a18..db94c3ce7f 100644 Binary files a/app/assets/images/noimage/mini.png and b/app/assets/images/noimage/mini.png differ diff --git a/app/assets/images/noimage/product.png b/app/assets/images/noimage/product.png index ca06da639b..6ca94eadb2 100644 Binary files a/app/assets/images/noimage/product.png and b/app/assets/images/noimage/product.png differ diff --git a/app/assets/images/noimage/small.png b/app/assets/images/noimage/small.png index ca06da639b..02088d8da0 100644 Binary files a/app/assets/images/noimage/small.png and b/app/assets/images/noimage/small.png differ diff --git a/app/assets/images/ofn-o.png b/app/assets/images/ofn-o.png new file mode 100644 index 0000000000..d6498ddb4d Binary files /dev/null and b/app/assets/images/ofn-o.png differ diff --git a/app/assets/images/ofn-o.svg b/app/assets/images/ofn-o.svg new file mode 100644 index 0000000000..2082ae6176 --- /dev/null +++ b/app/assets/images/ofn-o.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/assets/images/ofn_logo.png b/app/assets/images/ofn_logo.png deleted file mode 100644 index cdf690d997..0000000000 Binary files a/app/assets/images/ofn_logo.png and /dev/null differ diff --git a/app/assets/images/ofn_logo_beta.png b/app/assets/images/ofn_logo_beta.png deleted file mode 100644 index 420a51768d..0000000000 Binary files a/app/assets/images/ofn_logo_beta.png and /dev/null differ diff --git a/app/assets/images/ofn_logo_black.png b/app/assets/images/ofn_logo_black.png deleted file mode 100644 index d5f250daba..0000000000 Binary files a/app/assets/images/ofn_logo_black.png and /dev/null differ diff --git a/app/assets/images/ofn_logo_small.png b/app/assets/images/ofn_logo_small.png deleted file mode 100644 index 90277ab384..0000000000 Binary files a/app/assets/images/ofn_logo_small.png and /dev/null differ diff --git a/app/assets/images/ofw.png b/app/assets/images/ofw.png deleted file mode 100644 index ac571bacd0..0000000000 Binary files a/app/assets/images/ofw.png and /dev/null differ diff --git a/app/assets/images/open-food-network-beta-black.png b/app/assets/images/open-food-network-beta-black.png deleted file mode 100644 index 8e5f810023..0000000000 Binary files a/app/assets/images/open-food-network-beta-black.png and /dev/null differ diff --git a/app/assets/images/open-food-network-beta.png b/app/assets/images/open-food-network-beta.png deleted file mode 100644 index 965a248162..0000000000 Binary files a/app/assets/images/open-food-network-beta.png and /dev/null differ diff --git a/app/assets/images/open-food-network-beta.svg b/app/assets/images/open-food-network-beta.svg deleted file mode 100644 index eb882701d4..0000000000 --- a/app/assets/images/open-food-network-beta.svg +++ /dev/null @@ -1,840 +0,0 @@ - - - - - - - - BETA - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - BETA - - > - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/assets/images/pickup.png b/app/assets/images/pickup.png deleted file mode 100644 index aabceb508e..0000000000 Binary files a/app/assets/images/pickup.png and /dev/null differ diff --git a/app/assets/images/pin_bg.png b/app/assets/images/pin_bg.png deleted file mode 100644 index cfdd66162d..0000000000 Binary files a/app/assets/images/pin_bg.png and /dev/null differ diff --git a/app/assets/images/producers.svg b/app/assets/images/producers.svg new file mode 100644 index 0000000000..9804557f4b --- /dev/null +++ b/app/assets/images/producers.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/assets/images/store/cart.png b/app/assets/images/store/cart.png deleted file mode 100755 index 8650c39936..0000000000 Binary files a/app/assets/images/store/cart.png and /dev/null differ diff --git a/app/assets/images/subtle_white_feathers.png b/app/assets/images/subtle_white_feathers.png deleted file mode 100644 index dd699f66ba..0000000000 Binary files a/app/assets/images/subtle_white_feathers.png and /dev/null differ diff --git a/app/assets/images/tile-wide.png b/app/assets/images/tile-wide.png new file mode 100644 index 0000000000..f4e348b73c Binary files /dev/null and b/app/assets/images/tile-wide.png differ diff --git a/app/assets/javascripts/admin/accounts_and_billing_settings/accounts_and_billing_settings.js.coffee b/app/assets/javascripts/admin/accounts_and_billing_settings/accounts_and_billing_settings.js.coffee new file mode 100644 index 0000000000..d4f544e300 --- /dev/null +++ b/app/assets/javascripts/admin/accounts_and_billing_settings/accounts_and_billing_settings.js.coffee @@ -0,0 +1 @@ +angular.module("admin.accounts_and_billing_settings", []) diff --git a/app/assets/javascripts/admin/accounts_and_billing_settings/directives/method_settings.js.coffee b/app/assets/javascripts/admin/accounts_and_billing_settings/directives/method_settings.js.coffee new file mode 100644 index 0000000000..32ef50bb64 --- /dev/null +++ b/app/assets/javascripts/admin/accounts_and_billing_settings/directives/method_settings.js.coffee @@ -0,0 +1,14 @@ +angular.module("admin.accounts_and_billing_settings").directive "methodSettingsFor", -> + template: "
" + restrict: 'A' + scope: { + enterprise_id: '=methodSettingsFor' + } + link: (scope, element, attrs) -> + scope.include_html = "" + + scope.$watch "enterprise_id", (newVal, oldVal)-> + if !newVal? || newVal == "" + scope.include_html = "" + else + scope.include_html = "/admin/accounts_and_billing_settings/show_methods?enterprise_id=#{newVal};" diff --git a/app/assets/javascripts/admin/accounts_and_billing_settings/directives/watchValueAs.js.coffee b/app/assets/javascripts/admin/accounts_and_billing_settings/directives/watchValueAs.js.coffee new file mode 100644 index 0000000000..a14288db55 --- /dev/null +++ b/app/assets/javascripts/admin/accounts_and_billing_settings/directives/watchValueAs.js.coffee @@ -0,0 +1,11 @@ +angular.module("admin.accounts_and_billing_settings").directive "watchValueAs", -> + restrict: 'A' + scope: { + value: "=watchValueAs" + } + link: (scope, element, attrs) -> + scope.value = element.val() + + element.on "change blur load", -> + scope.$apply -> + scope.value = element.val() diff --git a/app/assets/javascripts/admin/all.js b/app/assets/javascripts/admin/all.js index e44e6ab96d..cbaccac66d 100644 --- a/app/assets/javascripts/admin/all.js +++ b/app/assets/javascripts/admin/all.js @@ -12,13 +12,17 @@ //= require angular //= require angular-resource //= require angular-animate +//= require angular-sanitize //= require admin/spree_core //= require admin/spree_auth //= require admin/spree_promo //= require admin/spree_paypal_express //= require ../shared/ng-infinite-scroll.min.js //= require ../shared/ng-tags-input.min.js +//= require angular-rails-templates +//= require_tree ../templates/admin //= require ./admin +//= require ./accounts_and_billing_settings/accounts_and_billing_settings //= require ./customers/customers //= require ./dropdown/dropdown //= require ./enterprises/enterprises @@ -33,5 +37,6 @@ //= require ./users/users //= require textAngular.min.js //= require textAngular-sanitize.min.js +//= require ../shared/bindonce.min.js //= require_tree . diff --git a/app/assets/javascripts/admin/bulk_order_management.js.coffee b/app/assets/javascripts/admin/bulk_order_management.js.coffee index 5c2b5b8984..8cd3d5b204 100644 --- a/app/assets/javascripts/admin/bulk_order_management.js.coffee +++ b/app/assets/javascripts/admin/bulk_order_management.js.coffee @@ -30,7 +30,7 @@ angular.module("ofn.admin").controller "AdminOrderMgmtCtrl", [ variant: { name: "Variant", visible: true } quantity: { name: "Quantity", visible: true } max: { name: "Max", visible: true } - unit_value: { name: "Weight/Volume", visible: false } + final_weight_volume: { name: "Weight/Volume", visible: false } price: { name: "Price", visible: false } $scope.initialise = -> $scope.initialiseVariables() @@ -166,10 +166,10 @@ angular.module("ofn.admin").controller "AdminOrderMgmtCtrl", [ $scope.weightAdjustedPrice = (lineItem, oldValue) -> if oldValue <= 0 - oldValue = lineItem.units_variant.unit_value - if lineItem.unit_value <= 0 - lineItem.unit_value = lineItem.units_variant.unit_value - lineItem.price = lineItem.price * lineItem.unit_value / oldValue + oldValue = lineItem.units_variant.unit_value * line_item.quantity + if lineItem.final_weight_volume <= 0 + lineItem.final_weight_volume = lineItem.units_variant.unit_value * lineItem.quantity + lineItem.price = lineItem.price * lineItem.final_weight_volume / oldValue #$scope.bulk_order_form.line_item.price.$setViewValue($scope.bulk_order_form.line_item.price.$viewValue) $scope.unitValueLessThanZero = (lineItem) -> @@ -178,6 +178,13 @@ angular.module("ofn.admin").controller "AdminOrderMgmtCtrl", [ else false + $scope.updateOnQuantity = (lineItem, oldQuantity) -> + if lineItem.quantity <= 0 + lineItem.quantity = 1 + # reset price to original unit value + lineItem.price = lineItem.price * (oldQuantity * lineItem.units_variant.unit_value) / lineItem.final_weight_volume + lineItem.final_weight_volume = lineItem.units_variant.unit_value * lineItem.quantity + $scope.$watch "orderCycleFilter", (newVal, oldVal) -> unless $scope.orderCycleFilter == "0" || angular.equals(newVal, oldVal) $scope.startDate = $scope.orderCyclesByID[$scope.orderCycleFilter].first_order diff --git a/app/assets/javascripts/admin/controllers/enterprise_relationships_controller.js.coffee b/app/assets/javascripts/admin/controllers/enterprise_relationships_controller.js.coffee index 88524fb330..47083d443a 100644 --- a/app/assets/javascripts/admin/controllers/enterprise_relationships_controller.js.coffee +++ b/app/assets/javascripts/admin/controllers/enterprise_relationships_controller.js.coffee @@ -9,3 +9,26 @@ angular.module("ofn.admin").controller "AdminEnterpriseRelationshipsCtrl", ($sco $scope.delete = (enterprise_relationship) -> if confirm("Are you sure?") $scope.EnterpriseRelationships.delete enterprise_relationship + + $scope.toggleKeyword = (string, key) -> + string = '' unless string + words = string.split ' ' + words = words.filter (s) -> + s + index = words.indexOf key + if index > -1 + words.splice index, 1 + else + words.push key + words.join ' ' + + $scope.allPermissionsChecked = -> + for i in EnterpriseRelationships.all_permissions + if !$scope.permissions[i] + return false + return true + + $scope.checkAllPermissions = -> + newValue = !$scope.allPermissionsChecked() + EnterpriseRelationships.all_permissions.forEach (p) -> + $scope.permissions[p] = newValue diff --git a/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee b/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee index 03cd7d4943..d48f2a42f8 100644 --- a/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee +++ b/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee @@ -1,6 +1,6 @@ angular.module("admin.enterprises") - .controller "enterpriseCtrl", ($scope, NavigationCheck, Enterprise, EnterprisePaymentMethods, EnterpriseShippingMethods, SideMenu) -> - $scope.Enterprise = Enterprise.enterprise + .controller "enterpriseCtrl", ($scope, NavigationCheck, enterprise, EnterprisePaymentMethods, EnterpriseShippingMethods, SideMenu) -> + $scope.Enterprise = enterprise $scope.PaymentMethods = EnterprisePaymentMethods.paymentMethods $scope.ShippingMethods = EnterpriseShippingMethods.shippingMethods $scope.navClear = NavigationCheck.clear diff --git a/app/assets/javascripts/admin/enterprises/controllers/enterprise_index_row_controller.js.coffee b/app/assets/javascripts/admin/enterprises/controllers/enterprise_index_row_controller.js.coffee new file mode 100644 index 0000000000..63b8daf07c --- /dev/null +++ b/app/assets/javascripts/admin/enterprises/controllers/enterprise_index_row_controller.js.coffee @@ -0,0 +1,49 @@ +angular.module("admin.enterprises").controller "EnterpriseIndexRowCtrl", ($scope) -> + $scope.status = -> + if $scope.enterprise.issues.length > 0 + "issue" + else if $scope.enterprise.warnings.length > 0 + "warning" + else + "ok" + + + $scope.producerText = -> + switch $scope.enterprise.is_primary_producer + when true + "Producer" + else + "Non-Producer" + + $scope.packageText = -> + switch $scope.enterprise.is_primary_producer + when true + switch $scope.enterprise.sells + when "none" + "Profile" + when "own" + "Shop" + when "any" + "Hub" + else + "Choose" + else + switch $scope.enterprise.sells + when "none" + "Profile" + when "any" + "Hub" + else + "Choose" + + $scope.updateRowText = -> + $scope.producer = $scope.producerText() + $scope.package = $scope.packageText() + $scope.producerError = ($scope.producer == "Choose") + $scope.packageError = ($scope.package == "Choose") + + + $scope.updateRowText() + + $scope.$on "enterprise:updated", -> + $scope.updateRowText() diff --git a/app/assets/javascripts/admin/enterprises/controllers/enterprises_controller.js.coffee b/app/assets/javascripts/admin/enterprises/controllers/enterprises_controller.js.coffee new file mode 100644 index 0000000000..dca4e65800 --- /dev/null +++ b/app/assets/javascripts/admin/enterprises/controllers/enterprises_controller.js.coffee @@ -0,0 +1,13 @@ +angular.module("admin.enterprises").controller 'enterprisesCtrl', ($scope, Enterprises, Columns) -> + Enterprises.loaded = false + $scope.allEnterprises = Enterprises.index() + + $scope.loaded = -> + Enterprises.loaded + + $scope.columns = Columns.setColumns + name: { name: "Name", visible: true } + producer: { name: "Producer", visible: true } + package: { name: "Package", visible: true } + status: { name: "Status", visible: true } + manage: { name: "Manage", visible: true } diff --git a/app/assets/javascripts/admin/enterprises/controllers/index_package_panel_controller.js.coffee b/app/assets/javascripts/admin/enterprises/controllers/index_package_panel_controller.js.coffee new file mode 100644 index 0000000000..f191dc9e7a --- /dev/null +++ b/app/assets/javascripts/admin/enterprises/controllers/index_package_panel_controller.js.coffee @@ -0,0 +1,2 @@ +angular.module("admin.enterprises").controller 'indexPackagePanelCtrl', ($scope, $controller) -> + angular.extend this, $controller('indexPanelCtrl', {$scope: $scope}) diff --git a/app/assets/javascripts/admin/enterprises/controllers/index_panel_controller.js.coffee b/app/assets/javascripts/admin/enterprises/controllers/index_panel_controller.js.coffee new file mode 100644 index 0000000000..6f568ca5ea --- /dev/null +++ b/app/assets/javascripts/admin/enterprises/controllers/index_panel_controller.js.coffee @@ -0,0 +1,23 @@ +angular.module("admin.enterprises").controller 'indexPanelCtrl', ($scope, Enterprises) -> + $scope.enterprise = $scope.object + $scope.saving = false + + $scope.saved = -> + Enterprises.saved($scope.enterprise) + + $scope.save = -> + unless $scope.saved() + $scope.saving = true + Enterprises.save($scope.enterprise).then (data) -> + $scope.$emit("enterprise:updated") + $scope.saving = false + , (response) -> + $scope.saving = false + if response.status == 422 && response.data.errors? + message = 'Please resolve the following errors:\n' + for attr, msg of response.data.errors + message += "#{attr} #{msg}\n" + alert(message) + + $scope.resetAttribute = (attribute) -> + Enterprises.resetAttribute($scope.enterprise, attribute) diff --git a/app/assets/javascripts/admin/enterprises/controllers/index_producer_panel_controller.js.coffee b/app/assets/javascripts/admin/enterprises/controllers/index_producer_panel_controller.js.coffee new file mode 100644 index 0000000000..75fd9f1ec5 --- /dev/null +++ b/app/assets/javascripts/admin/enterprises/controllers/index_producer_panel_controller.js.coffee @@ -0,0 +1,14 @@ +angular.module("admin.enterprises").controller 'indexProducerPanelCtrl', ($scope, $controller) -> + angular.extend this, $controller('indexPanelCtrl', {$scope: $scope}) + + $scope.changeToProducer = -> + $scope.resetAttribute('sells') + $scope.resetAttribute('producer_profile_only') + $scope.enterprise.is_primary_producer = true + + $scope.changeToNonProducer = -> + if $scope.enterprise.sells == 'own' + $scope.enterprise.sells = 'any' + if $scope.enterprise.producer_profile_only = true + $scope.enterprise.producer_profile_only = false + $scope.enterprise.is_primary_producer = false diff --git a/app/assets/javascripts/admin/enterprises/controllers/index_status_panel_controller.js.coffee b/app/assets/javascripts/admin/enterprises/controllers/index_status_panel_controller.js.coffee new file mode 100644 index 0000000000..987a206bd0 --- /dev/null +++ b/app/assets/javascripts/admin/enterprises/controllers/index_status_panel_controller.js.coffee @@ -0,0 +1,3 @@ +angular.module("admin.enterprises").controller 'indexStatusPanelCtrl', ($scope, $filter) -> + $scope.issues = $scope.object.issues + $scope.warnings = $scope.object.warnings diff --git a/app/assets/javascripts/admin/enterprises/controllers/side_menu_controller.js.coffee b/app/assets/javascripts/admin/enterprises/controllers/side_menu_controller.js.coffee index d5d0e1681a..45a5d068a4 100644 --- a/app/assets/javascripts/admin/enterprises/controllers/side_menu_controller.js.coffee +++ b/app/assets/javascripts/admin/enterprises/controllers/side_menu_controller.js.coffee @@ -1,6 +1,6 @@ angular.module("admin.enterprises") - .controller "sideMenuCtrl", ($scope, $parse, Enterprise, SideMenu, enterprisePermissions) -> - $scope.Enterprise = Enterprise.enterprise + .controller "sideMenuCtrl", ($scope, $parse, enterprise, SideMenu, enterprisePermissions) -> + $scope.Enterprise = enterprise $scope.menu = SideMenu $scope.select = SideMenu.select diff --git a/app/assets/javascripts/admin/enterprises/enterprises.js.coffee b/app/assets/javascripts/admin/enterprises/enterprises.js.coffee index 5a7942d639..6be7e00ffa 100644 --- a/app/assets/javascripts/admin/enterprises/enterprises.js.coffee +++ b/app/assets/javascripts/admin/enterprises/enterprises.js.coffee @@ -1 +1 @@ -angular.module("admin.enterprises", [ "admin.payment_methods", "admin.utils", "admin.shipping_methods", "admin.users", "textAngular", "admin.side_menu", "admin.taxons"] ) \ No newline at end of file +angular.module("admin.enterprises", [ "admin.payment_methods", "admin.utils", "admin.shipping_methods", "admin.users", "textAngular", "admin.side_menu", "admin.taxons", 'admin.indexUtils', 'admin.dropdown', 'pasvaz.bindonce', 'ngSanitize'] ) \ No newline at end of file diff --git a/app/assets/javascripts/admin/enterprises/services/enterprise_payment_methods.js.coffee b/app/assets/javascripts/admin/enterprises/services/enterprise_payment_methods.js.coffee index b1a88f82fb..d1808e5001 100644 --- a/app/assets/javascripts/admin/enterprises/services/enterprise_payment_methods.js.coffee +++ b/app/assets/javascripts/admin/enterprises/services/enterprise_payment_methods.js.coffee @@ -1,11 +1,11 @@ angular.module("admin.enterprises") - .factory "EnterprisePaymentMethods", (Enterprise, PaymentMethods) -> + .factory "EnterprisePaymentMethods", (enterprise, PaymentMethods) -> new class EnterprisePaymentMethods paymentMethods: PaymentMethods.paymentMethods constructor: -> for payment_method in @paymentMethods - payment_method.selected = payment_method.id in Enterprise.enterprise.payment_method_ids + payment_method.selected = payment_method.id in enterprise.payment_method_ids displayColor: -> if @paymentMethods.length > 0 && @selectedCount() > 0 diff --git a/app/assets/javascripts/admin/enterprises/services/enterprise_resource.js.coffee b/app/assets/javascripts/admin/enterprises/services/enterprise_resource.js.coffee new file mode 100644 index 0000000000..357302d2b8 --- /dev/null +++ b/app/assets/javascripts/admin/enterprises/services/enterprise_resource.js.coffee @@ -0,0 +1,8 @@ +angular.module("admin.enterprises").factory 'EnterpriseResource', ($resource) -> + $resource('/admin/enterprises/:id.json', {}, { + 'index': + method: 'GET' + isArray: true + 'update': + method: 'PUT' + }) diff --git a/app/assets/javascripts/admin/enterprises/services/enterprise_shipping_methods.js.coffee b/app/assets/javascripts/admin/enterprises/services/enterprise_shipping_methods.js.coffee index 3f64c07442..50d15ff002 100644 --- a/app/assets/javascripts/admin/enterprises/services/enterprise_shipping_methods.js.coffee +++ b/app/assets/javascripts/admin/enterprises/services/enterprise_shipping_methods.js.coffee @@ -1,11 +1,11 @@ angular.module("admin.enterprises") - .factory "EnterpriseShippingMethods", (Enterprise, ShippingMethods) -> + .factory "EnterpriseShippingMethods", (enterprise, ShippingMethods) -> new class EnterpriseShippingMethods shippingMethods: ShippingMethods.shippingMethods constructor: -> for shipping_method in @shippingMethods - shipping_method.selected = shipping_method.id in Enterprise.enterprise.shipping_method_ids + shipping_method.selected = shipping_method.id in enterprise.shipping_method_ids displayColor: -> if @shippingMethods.length > 0 && @selectedCount() > 0 diff --git a/app/assets/javascripts/admin/enterprises/services/enterprises.js.coffee b/app/assets/javascripts/admin/enterprises/services/enterprises.js.coffee new file mode 100644 index 0000000000..3411c4a6c4 --- /dev/null +++ b/app/assets/javascripts/admin/enterprises/services/enterprises.js.coffee @@ -0,0 +1,39 @@ +angular.module("admin.enterprises").factory 'Enterprises', ($q, EnterpriseResource) -> + new class Enterprises + enterprises: [] + enterprises_by_id: {} + pristine_by_id: {} + loaded: false + + index: (params={}, callback=null) -> + EnterpriseResource.index params, (data) => + for enterprise in data + @enterprises.push enterprise + @pristine_by_id[enterprise.id] = angular.copy(enterprise) + + @loaded = true + (callback || angular.noop)(@enterprises) + + @enterprises + + save: (enterprise) -> + deferred = $q.defer() + enterprise.$update({id: enterprise.permalink}) + .then( (data) => + @pristine_by_id[enterprise.id] = angular.copy(enterprise) + deferred.resolve(data) + ).catch (response) -> + deferred.reject(response) + deferred.promise + + saved: (enterprise) -> + @diff(enterprise).length == 0 + + diff: (enterprise) -> + changed = [] + for attr, value of enterprise when not angular.equals(value, @pristine_by_id[enterprise.id][attr]) + changed.push attr unless attr is "$$hashKey" + changed + + resetAttribute: (enterprise, attribute) -> + enterprise[attribute] = @pristine_by_id[enterprise.id][attribute] diff --git a/app/assets/javascripts/admin/enterprises/services/permalink_checker.js.coffee b/app/assets/javascripts/admin/enterprises/services/permalink_checker.js.coffee index cb3fde9324..e1b62e6f6e 100644 --- a/app/assets/javascripts/admin/enterprises/services/permalink_checker.js.coffee +++ b/app/assets/javascripts/admin/enterprises/services/permalink_checker.js.coffee @@ -2,6 +2,7 @@ angular.module("admin.enterprises").factory 'PermalinkChecker', ($q, $http) -> new class PermalinkChecker deferredRequest: null deferredAbort: null + MAX_PERMALINK_LENGTH: 255 check: (permalink) => @abort(@deferredAbort) if @deferredRequest && @deferredRequest.promise @@ -15,9 +16,14 @@ angular.module("admin.enterprises").factory 'PermalinkChecker', ($q, $http) -> timeout: deferredAbort.promise ) .success( (data) => - deferredRequest.resolve - permalink: data - available: "Available" + if data.length > @MAX_PERMALINK_LENGTH || !data.match(/^[\w-]+$/) + deferredRequest.resolve + permalink: permalink + available: "Error" + else + deferredRequest.resolve + permalink: data + available: "Available" ).error (data,status) => if status == 409 deferredRequest.resolve diff --git a/app/assets/javascripts/admin/filters/keywords_filter.js.coffee b/app/assets/javascripts/admin/filters/keywords_filter.js.coffee new file mode 100644 index 0000000000..7d207518f0 --- /dev/null +++ b/app/assets/javascripts/admin/filters/keywords_filter.js.coffee @@ -0,0 +1,7 @@ +angular.module("ofn.admin").filter "keywords", ($filter) -> + return (array, query) -> + return array unless query + keywords = query.split ' ' + keywords.forEach (key) -> + array = $filter('filter')(array, key) + array diff --git a/app/assets/javascripts/admin/index_utils/directives/panel_row.js.coffee b/app/assets/javascripts/admin/index_utils/directives/panel_row.js.coffee new file mode 100644 index 0000000000..eb5a4171f4 --- /dev/null +++ b/app/assets/javascripts/admin/index_utils/directives/panel_row.js.coffee @@ -0,0 +1,37 @@ +angular.module("admin.indexUtils").directive "panelRow", (Panels, Columns) -> + restrict: "C" + templateUrl: "admin/panel.html" + scope: + object: "=" + panels: "=" + link: (scope, element, attrs) -> + scope.template = "" + selected = null + scope.columnCount = Columns.visibleCount + + scope.$on "columnCount:changed", (event, count) -> + scope.columnCount = count + + setTemplate = -> + if selected? + scope.template = 'admin/panels/' + scope.panels[selected] + '.html' + else + scope.template = "" + + scope.getSelected = -> + selected + + scope.setSelected = (name) -> + scope.$apply -> + selected = name + setTemplate() + + scope.open = (name) -> + element.show 0, -> + scope.setSelected name + + scope.close = -> + element.hide 0, -> + scope.setSelected null + + Panels.register(scope.object.id, scope) diff --git a/app/assets/javascripts/admin/index_utils/directives/panel_toggle.js.coffee b/app/assets/javascripts/admin/index_utils/directives/panel_toggle.js.coffee new file mode 100644 index 0000000000..df81328905 --- /dev/null +++ b/app/assets/javascripts/admin/index_utils/directives/panel_toggle.js.coffee @@ -0,0 +1,12 @@ +angular.module("admin.indexUtils").directive "panelToggle", -> + restrict: "C" + transclude: true + template: '
' + require: "^panelToggleRow" + scope: + name: "@" + link: (scope, element, attrs, ctrl) -> + scope.selected = ctrl.register(scope.name, element) + + element.on "click", -> + scope.selected = ctrl.select(scope.name) diff --git a/app/assets/javascripts/admin/index_utils/directives/panel_toggle_row.js.coffee b/app/assets/javascripts/admin/index_utils/directives/panel_toggle_row.js.coffee new file mode 100644 index 0000000000..d2d9c90ff8 --- /dev/null +++ b/app/assets/javascripts/admin/index_utils/directives/panel_toggle_row.js.coffee @@ -0,0 +1,29 @@ +angular.module("admin.indexUtils").directive "panelToggleRow", (Panels) -> + restrict: "C" + scope: + object: "=" + selected: "@?" + controller: ($scope) -> + panelToggles = {} + + this.register = (name, element) -> + panelToggles[name] = element + panelToggles[name].addClass("selected") if $scope.selected == name + $scope.selected == name + + this.select = (name) -> + panelToggle.removeClass("selected") for panelName, panelToggle of panelToggles + + switch $scope.selected = Panels.toggle($scope.object.id, name) + when null + panelToggles[name].parent(".panel-toggle-row").removeClass("expanded") + else + panelToggles[$scope.selected].addClass("selected") + panelToggles[$scope.selected].parent(".panel-toggle-row").addClass("expanded") + + $scope.selected == name + + this + # + # link: (scope, element, attrs) -> + # Panels.registerInitialSelection(scope.object.id, scope.selected) diff --git a/app/assets/javascripts/admin/index_utils/directives/toggle_column.js.coffee b/app/assets/javascripts/admin/index_utils/directives/toggle_column.js.coffee index d6239ff47f..2910e9a7a1 100644 --- a/app/assets/javascripts/admin/index_utils/directives/toggle_column.js.coffee +++ b/app/assets/javascripts/admin/index_utils/directives/toggle_column.js.coffee @@ -1,11 +1,8 @@ -angular.module("admin.indexUtils").directive "ofnToggleColumn", -> +angular.module("admin.indexUtils").directive "ofnToggleColumn", (Columns) -> link: (scope, element, attrs) -> element.addClass "selected" if scope.column.visible + element.click "click", -> scope.$apply -> - if scope.column.visible - scope.column.visible = false - element.removeClass "selected" - else - scope.column.visible = true - element.addClass "selected" + Columns.toggleColumn(scope.column) + element.toggleClass "selected" diff --git a/app/assets/javascripts/admin/index_utils/index_utils.js.coffee b/app/assets/javascripts/admin/index_utils/index_utils.js.coffee index 0925bf45ed..adcd68e3c5 100644 --- a/app/assets/javascripts/admin/index_utils/index_utils.js.coffee +++ b/app/assets/javascripts/admin/index_utils/index_utils.js.coffee @@ -1 +1 @@ -angular.module("admin.indexUtils", ['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, */*"; \ No newline at end of file +angular.module("admin.indexUtils", ['ngResource', 'templates']).config ($httpProvider) -> $httpProvider.defaults.headers.common["X-CSRF-Token"] = $("meta[name=csrf-token]").attr("content"); $httpProvider.defaults.headers.common["Accept"] = "application/json, text/javascript, */*"; \ No newline at end of file diff --git a/app/assets/javascripts/admin/index_utils/services/columns.js.coffee b/app/assets/javascripts/admin/index_utils/services/columns.js.coffee index 46e7ccd3b8..8bd99bf2f2 100644 --- a/app/assets/javascripts/admin/index_utils/services/columns.js.coffee +++ b/app/assets/javascripts/admin/index_utils/services/columns.js.coffee @@ -1,8 +1,18 @@ -angular.module("admin.indexUtils").factory 'Columns', -> +angular.module("admin.indexUtils").factory 'Columns', ($rootScope) -> new class Columns columns: {} + visibleCount: 0 - setColumns: (columns) -> + setColumns: (columns) => @columns = {} @columns[name] = column for name, column of columns + @calculateVisibleCount() @columns + + toggleColumn: (column) => + column.visible = !column.visible + @calculateVisibleCount() + + calculateVisibleCount: => + @visibleCount = (column for name, column of @columns when column.visible).length + $rootScope.$broadcast "columnCount:changed", @visibleCount diff --git a/app/assets/javascripts/admin/index_utils/services/panels.js.coffee b/app/assets/javascripts/admin/index_utils/services/panels.js.coffee new file mode 100644 index 0000000000..27852bed12 --- /dev/null +++ b/app/assets/javascripts/admin/index_utils/services/panels.js.coffee @@ -0,0 +1,19 @@ +angular.module("admin.indexUtils").factory 'Panels', -> + new class Panels + panels: {} + + register: (id, scope) -> + if id? && scope? + @panels[id] = scope + + toggle: (id, name) -> + scope = @panels[id] + selected = scope.getSelected() + switch selected + when name + scope.close() + when null + scope.open(name) + else + scope.setSelected(name) + scope.getSelected() diff --git a/app/assets/javascripts/admin/services/bulk_products.js.coffee b/app/assets/javascripts/admin/services/bulk_products.js.coffee index 6085d48fcc..4e06a87bd2 100644 --- a/app/assets/javascripts/admin/services/bulk_products.js.coffee +++ b/app/assets/javascripts/admin/services/bulk_products.js.coffee @@ -19,6 +19,7 @@ angular.module("ofn.admin").factory "BulkProducts", (PagedFetcher, dataFetcher) # when a respond_overrride for the clone action is used. id = data.product.id dataFetcher("/api/products/" + id + "?template=bulk_show").then (newProduct) => + @unpackProduct newProduct @insertProductAfter(product, newProduct) updateVariantLists: (serverProducts, productsWithUnsavedVariants) -> diff --git a/app/assets/javascripts/admin/services/enterprise_relationships.js.coffee b/app/assets/javascripts/admin/services/enterprise_relationships.js.coffee index e6dcbf15df..8c7d798138 100644 --- a/app/assets/javascripts/admin/services/enterprise_relationships.js.coffee +++ b/app/assets/javascripts/admin/services/enterprise_relationships.js.coffee @@ -26,7 +26,7 @@ angular.module("ofn.admin").factory 'EnterpriseRelationships', ($http, enterpris permission_presentation: (permission) -> switch permission - when "add_to_order_cycle" then "to add to order cycle" - when "manage_products" then "to manage products" - when "edit_profile" then "to edit profile" - when "create_variant_overrides" then "to override variant details" + when "add_to_order_cycle" then "add to order cycle" + when "manage_products" then "manage products" + when "edit_profile" then "edit profile" + when "create_variant_overrides" then "override variant details" diff --git a/app/assets/javascripts/darkswarm/all.js.coffee b/app/assets/javascripts/darkswarm/all.js.coffee index 68bc01b6c1..f4303f8037 100644 --- a/app/assets/javascripts/darkswarm/all.js.coffee +++ b/app/assets/javascripts/darkswarm/all.js.coffee @@ -15,6 +15,7 @@ #= require ../shared/bindonce.min.js #= require ../shared/ng-infinite-scroll.min.js #= require ../shared/angular-local-storage.js +#= require ../shared/angular-slideables.js #= require angularjs-file-upload diff --git a/app/assets/javascripts/darkswarm/controllers/enterprises_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/enterprises_controller.js.coffee index 1e43c17465..3883eca8d5 100644 --- a/app/assets/javascripts/darkswarm/controllers/enterprises_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/enterprises_controller.js.coffee @@ -1,18 +1,69 @@ -Darkswarm.controller "EnterprisesCtrl", ($scope, Enterprises, Search, $document, $rootScope, HashNavigation, FilterSelectorsService, EnterpriseModal) -> +Darkswarm.controller "EnterprisesCtrl", ($scope, $rootScope, $timeout, Enterprises, Search, $document, HashNavigation, FilterSelectorsService, EnterpriseModal, enterpriseMatchesNameQueryFilter, distanceWithinKmFilter) -> $scope.Enterprises = Enterprises - $scope.totalActive = FilterSelectorsService.totalActive - $scope.clearAll = FilterSelectorsService.clearAll - $scope.filterText = FilterSelectorsService.filterText - $scope.FilterSelectorsService = FilterSelectorsService + $scope.totalActive = FilterSelectorsService.totalActive + $scope.clearAll = FilterSelectorsService.clearAll + $scope.filterText = FilterSelectorsService.filterText + $scope.FilterSelectorsService = FilterSelectorsService $scope.query = Search.search() $scope.openModal = EnterpriseModal.open $scope.activeTaxons = [] $scope.show_profiles = false $scope.filtersActive = false + $scope.distanceMatchesShown = false + $scope.$watch "query", (query)-> + Enterprises.flagMatching query Search.search query + $rootScope.$broadcast 'enterprisesChanged' + $scope.distanceMatchesShown = false + + $timeout -> + Enterprises.calculateDistance query, $scope.firstNameMatch() + $rootScope.$broadcast 'enterprisesChanged' + + + $rootScope.$on "enterprisesChanged", -> + $scope.filterEnterprises() + $scope.updateVisibleMatches() + + + # When filter settings change, this could change which name match is at the top, or even + # result in no matches. This affects the reference point that the distance matches are + # calculated from, so we need to recalculate distances. + $scope.$watch '[activeTaxons, shippingTypes, show_profiles]', -> + $timeout -> + Enterprises.calculateDistance $scope.query, $scope.firstNameMatch() + $rootScope.$broadcast 'enterprisesChanged' + , true + $rootScope.$on "$locationChangeSuccess", (newRoute, oldRoute) -> if HashNavigation.active "hubs" $document.scrollTo $("#hubs"), 100, 200 + + + $scope.filterEnterprises = -> + es = Enterprises.hubs + $scope.nameMatches = enterpriseMatchesNameQueryFilter(es, true) + $scope.distanceMatches = enterpriseMatchesNameQueryFilter(es, false) + $scope.distanceMatches = distanceWithinKmFilter($scope.distanceMatches, 50) + + + $scope.updateVisibleMatches = -> + $scope.visibleMatches = if $scope.nameMatches.length == 0 || $scope.distanceMatchesShown + $scope.nameMatches.concat $scope.distanceMatches + else + $scope.nameMatches + + + $scope.showDistanceMatches = -> + $scope.distanceMatchesShown = true + $scope.updateVisibleMatches() + + + $scope.firstNameMatch = -> + if $scope.nameMatchesFiltered? + $scope.nameMatchesFiltered[0] + else + undefined diff --git a/app/assets/javascripts/darkswarm/controllers/home_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/home_controller.js.coffee new file mode 100644 index 0000000000..928f9a4fa1 --- /dev/null +++ b/app/assets/javascripts/darkswarm/controllers/home_controller.js.coffee @@ -0,0 +1,5 @@ +Darkswarm.controller "HomeCtrl", ($scope) -> + $scope.brandStoryExpanded = false + + $scope.toggleBrandStory = -> + $scope.brandStoryExpanded = !$scope.brandStoryExpanded diff --git a/app/assets/javascripts/darkswarm/controllers/line_item_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/line_item_controller.js.coffee index ea62163868..864f25177a 100644 --- a/app/assets/javascripts/darkswarm/controllers/line_item_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/line_item_controller.js.coffee @@ -1,4 +1,5 @@ Darkswarm.controller "LineItemCtrl", ($scope)-> - $scope.$watch "line_item.quantity", (newValue, oldValue)-> + $scope.$watch '[line_item.quantity, line_item.max_quantity]', (newValue, oldValue)-> if newValue != oldValue $scope.Cart.orderChanged() + , true \ No newline at end of file diff --git a/app/assets/javascripts/darkswarm/controllers/order_cycle_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/order_cycle_controller.js.coffee index 2f1394e90a..42d4c1c44d 100644 --- a/app/assets/javascripts/darkswarm/controllers/order_cycle_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/order_cycle_controller.js.coffee @@ -1,6 +1,4 @@ -# TODO this SUCKS. Fix it - -Darkswarm.controller "OrderCycleCtrl", ($scope, OrderCycle, $timeout) -> +Darkswarm.controller "OrderCycleCtrl", ($scope, $timeout, OrderCycle) -> $scope.order_cycle = OrderCycle.order_cycle $scope.OrderCycle = OrderCycle @@ -9,11 +7,26 @@ Darkswarm.controller "OrderCycleCtrl", ($scope, OrderCycle, $timeout) -> # That takes an expression instead of a trigger, and binds to that $timeout => if !$scope.OrderCycle.selected() - $("#order_cycle_id").trigger("openTrigger") + $("#order_cycle_id").trigger("openTrigger") -Darkswarm.controller "OrderCycleChangeCtrl", ($scope, OrderCycle, Products, $timeout) -> +Darkswarm.controller "OrderCycleChangeCtrl", ($scope, $timeout, OrderCycle, Products, Variants, Cart) -> + # Track previous order cycle id for use with revertOrderCycle() + $scope.previous_order_cycle_id = OrderCycle.order_cycle.order_cycle_id + $scope.$watch 'order_cycle.order_cycle_id', (newValue, oldValue)-> + $scope.previous_order_cycle_id = oldValue + $scope.changeOrderCycle = -> - OrderCycle.push_order_cycle Products.update + OrderCycle.push_order_cycle $scope.orderCycleChanged $timeout -> - $("#order_cycle_id").trigger("closeTrigger") + $("#order_cycle_id").trigger("closeTrigger") + + $scope.revertOrderCycle = -> + $scope.order_cycle.order_cycle_id = $scope.previous_order_cycle_id + + $scope.orderCycleChanged = -> + # push_order_cycle clears the cart server-side. Here we call Cart.clear() to clear the + # client-side cart. + Variants.clear() + Cart.clear() + Products.update() diff --git a/app/assets/javascripts/darkswarm/darkswarm.js.coffee b/app/assets/javascripts/darkswarm/darkswarm.js.coffee index e8ea9dce3c..a062ead058 100644 --- a/app/assets/javascripts/darkswarm/darkswarm.js.coffee +++ b/app/assets/javascripts/darkswarm/darkswarm.js.coffee @@ -10,6 +10,7 @@ window.Darkswarm = angular.module("Darkswarm", ["ngResource", 'google-maps', 'duScroll', 'angularFileUpload', + 'angularSlideables' ]).config ($httpProvider, $tooltipProvider, $locationProvider, $anchorScrollProvider) -> $httpProvider.defaults.headers.post['X-CSRF-Token'] = $('meta[name="csrf-token"]').attr('content') $httpProvider.defaults.headers.put['X-CSRF-Token'] = $('meta[name="csrf-token"]').attr('content') diff --git a/app/assets/javascripts/darkswarm/directives/change_hub.js.coffee b/app/assets/javascripts/darkswarm/directives/change_hub.js.coffee new file mode 100644 index 0000000000..a699e18902 --- /dev/null +++ b/app/assets/javascripts/darkswarm/directives/change_hub.js.coffee @@ -0,0 +1,16 @@ +Darkswarm.directive "ofnChangeHub", (CurrentHub, Cart) -> + # Compares scope.hub with CurrentHub. Will trigger an confirmation if they are different, + # and Cart isn't empty + restrict: "A" + scope: + hub: "=ofnChangeHub" + link: (scope, elm, attr)-> + cart_will_need_emptying = -> + CurrentHub.hub?.id and CurrentHub.hub.id isnt scope.hub.id and !Cart.empty() + + if cart_will_need_emptying() + elm.bind 'click', (ev)-> + if confirm "Are you sure? This will change your selected hub and remove any items in your shopping cart." + Cart.clear() + else + ev.preventDefault() diff --git a/app/assets/javascripts/darkswarm/directives/change_order_cycle.js.coffee b/app/assets/javascripts/darkswarm/directives/change_order_cycle.js.coffee new file mode 100644 index 0000000000..a747e5c219 --- /dev/null +++ b/app/assets/javascripts/darkswarm/directives/change_order_cycle.js.coffee @@ -0,0 +1,22 @@ +Darkswarm.directive "ofnChangeOrderCycle", (OrderCycle, Cart, storage) -> + # Compares chosen order cycle with pre-set OrderCycle. Will trigger + # a confirmation if they are different, and Cart isn't empty + restrict: "A" + scope: true + link: (scope, elm, attr)-> + order_cycle_id = -> + parseInt elm.val() + + cart_needs_emptying = -> + OrderCycle.order_cycle?.order_cycle_id && OrderCycle.order_cycle.order_cycle_id != order_cycle_id() && !Cart.empty() + + elm.bind 'change', (ev)-> + if cart_needs_emptying() + if confirm "Are you sure? This will change your selected order cycle and remove any items in your shopping cart." + Cart.clear() + scope.changeOrderCycle() + else + scope.$apply -> + scope.revertOrderCycle() + else + scope.changeOrderCycle() diff --git a/app/assets/javascripts/darkswarm/directives/empties_cart.js.coffee b/app/assets/javascripts/darkswarm/directives/empties_cart.js.coffee deleted file mode 100644 index 51e960867b..0000000000 --- a/app/assets/javascripts/darkswarm/directives/empties_cart.js.coffee +++ /dev/null @@ -1,13 +0,0 @@ -Darkswarm.directive "ofnEmptiesCart", (CurrentHub, Cart, Navigation, storage) -> - # Compares scope.hub with CurrentHub. Will trigger an confirmation if they are different, - # and Cart isn't empty - restrict: "A" - scope: - hub: "=ofnEmptiesCart" - link: (scope, elm, attr)-> - if CurrentHub.hub?.id and CurrentHub.hub.id isnt scope.hub.id and !Cart.empty() - elm.bind 'click', (ev)-> - ev.preventDefault() - if confirm "Are you sure? This will change your selected Hub and remove any items in your shopping cart." - storage.clearAll() # One day this will have to be moar GRANULAR - Navigation.go scope.hub.path diff --git a/app/assets/javascripts/darkswarm/directives/filter_selector.js.coffee b/app/assets/javascripts/darkswarm/directives/filter_selector.js.coffee index 95aee1b413..818af3b785 100644 --- a/app/assets/javascripts/darkswarm/directives/filter_selector.js.coffee +++ b/app/assets/javascripts/darkswarm/directives/filter_selector.js.coffee @@ -22,17 +22,11 @@ Darkswarm.directive "filterSelector", (FilterSelectorsService)-> .map (selector)-> selector.object.id - # This can be called from a parent scope - # when data has been loaded, in order to pass - # selectors up - scope.$on 'loadFilterSelectors', -> - scope.allSelectors = scope.selectors() if attr.allSelectors? - - scope.$watchCollection "selectors()", (newValue, oldValue) -> - scope.allSelectors = scope.selectors() if attr.allSelectors? + scope.$watchCollection "objects()", (newValue, oldValue) -> + scope.allSelectors = scope.buildSelectors() # Build a list of selectors - scope.selectors = -> + scope.buildSelectors = -> # Generate a selector for each object. # NOTE: THESE ARE MEMOIZED to stop new selectors from being created constantly, otherwise function always returns non-identical results # This means the $digest cycle can never close and times out diff --git a/app/assets/javascripts/darkswarm/directives/integer.js.coffee b/app/assets/javascripts/darkswarm/directives/integer.js.coffee new file mode 100644 index 0000000000..e162246122 --- /dev/null +++ b/app/assets/javascripts/darkswarm/directives/integer.js.coffee @@ -0,0 +1,5 @@ +Darkswarm.directive "integer", -> + restrict: 'A' + link: (scope, elem, attr) -> + elem.bind 'input', -> + elem.val Math.round(elem.val()) diff --git a/app/assets/javascripts/darkswarm/directives/page_alert.js.coffee b/app/assets/javascripts/darkswarm/directives/page_alert.js.coffee new file mode 100644 index 0000000000..659bd227e9 --- /dev/null +++ b/app/assets/javascripts/darkswarm/directives/page_alert.js.coffee @@ -0,0 +1,14 @@ +Darkswarm.directive "ofnPageAlert", ($timeout) -> + restrict: 'A' + scope: true + link: (scope, elem, attrs) -> + container_elems = $(".off-canvas-wrap .inner-wrap, .off-canvas-wrap .inner-wrap .fixed, .page-alert") + + # Wait a moment after page load before showing the alert. Otherwise we often miss the + # start of the animation. + $timeout -> + container_elems.addClass("move-down") + , 1000 + + scope.close = -> + container_elems.removeClass("move-down") diff --git a/app/assets/javascripts/darkswarm/directives/scroll_after_load.js.coffee b/app/assets/javascripts/darkswarm/directives/scroll_after_load.js.coffee index 55c5a311da..e29dcbb8f8 100644 --- a/app/assets/javascripts/darkswarm/directives/scroll_after_load.js.coffee +++ b/app/assets/javascripts/darkswarm/directives/scroll_after_load.js.coffee @@ -2,10 +2,9 @@ Darkswarm.directive 'scrollAfterLoad', ($timeout, $location, $document)-> # Scroll to an element on page load restrict: "A" link: (scope, element, attr) -> - if scope.$last is true - $(window).load -> - $timeout -> - elem = $("##{$location.hash()}") - if elem.length > 0 - $document.scrollTo elem , 100, 200, (x)-> - x * (2 - x) + elem = element + $(window).load -> + $timeout -> + if elem? + $document.scrollTo elem, 100, 200, (x) -> + x * (2 - x) diff --git a/app/assets/javascripts/darkswarm/filters/distance_within_km.js.coffee b/app/assets/javascripts/darkswarm/filters/distance_within_km.js.coffee new file mode 100644 index 0000000000..c6a37a2b40 --- /dev/null +++ b/app/assets/javascripts/darkswarm/filters/distance_within_km.js.coffee @@ -0,0 +1,5 @@ +Darkswarm.filter 'distanceWithinKm', -> + (enterprises, range) -> + enterprises ||= [] + enterprises.filter (enterprise) -> + enterprise.distance / 1000 <= range diff --git a/app/assets/javascripts/darkswarm/filters/enterpriseMatchesNameQuery.js.coffee b/app/assets/javascripts/darkswarm/filters/enterpriseMatchesNameQuery.js.coffee new file mode 100644 index 0000000000..6e786a9f62 --- /dev/null +++ b/app/assets/javascripts/darkswarm/filters/enterpriseMatchesNameQuery.js.coffee @@ -0,0 +1,4 @@ +Darkswarm.filter 'enterpriseMatchesNameQuery', -> + (enterprises, matches_name_query) -> + enterprises.filter (enterprise) -> + enterprise.matches_name_query == matches_name_query diff --git a/app/assets/javascripts/darkswarm/services/cart.js.coffee b/app/assets/javascripts/darkswarm/services/cart.js.coffee index bbb0b5fab4..7550b151f0 100644 --- a/app/assets/javascripts/darkswarm/services/cart.js.coffee +++ b/app/assets/javascripts/darkswarm/services/cart.js.coffee @@ -1,4 +1,4 @@ -Darkswarm.factory 'Cart', (CurrentOrder, Variants, $timeout, $http)-> +Darkswarm.factory 'Cart', (CurrentOrder, Variants, $timeout, $http, storage)-> # Handles syncing of current cart/order state to server new class Cart dirty: false @@ -20,7 +20,7 @@ Darkswarm.factory 'Cart', (CurrentOrder, Variants, $timeout, $http)-> $http.post('/orders/populate', @data()).success (data, status)=> @saved() .error (response, status)=> - # TODO what shall we do here? + @scheduleRetry() data: => variants = {} @@ -30,6 +30,12 @@ Darkswarm.factory 'Cart', (CurrentOrder, Variants, $timeout, $http)-> max_quantity: li.max_quantity {variants: variants} + scheduleRetry: => + console.log "Error updating cart: #{status}. Retrying in 3 seconds..." + $timeout => + console.log "Retrying cart update" + @orderChanged() + , 3000 saved: => @dirty = false @@ -63,6 +69,10 @@ Darkswarm.factory 'Cart', (CurrentOrder, Variants, $timeout, $http)-> exists = @line_items.some (li)-> li.variant == variant @create_line_item(variant) unless exists + clear: -> + @line_items = [] + storage.clearAll() # One day this will have to be moar GRANULAR + create_line_item: (variant)-> variant.extended_name = @extendedVariantName(variant) variant.line_item = diff --git a/app/assets/javascripts/darkswarm/services/enterprise_registration_service.js.coffee b/app/assets/javascripts/darkswarm/services/enterprise_registration_service.js.coffee index 77b2204316..702a382eb0 100644 --- a/app/assets/javascripts/darkswarm/services/enterprise_registration_service.js.coffee +++ b/app/assets/javascripts/darkswarm/services/enterprise_registration_service.js.coffee @@ -26,7 +26,11 @@ Darkswarm.factory "EnterpriseRegistrationService", ($http, RegistrationService, RegistrationService.select('about') ).error((data) => Loading.clear() - alert('Failed to create your enterprise.\nPlease ensure all fields are completely filled out.') + if data?.errors? + errors = ("#{k.capitalize()} #{v[0]}" for k, v of data.errors when v.length > 0) + alert "Failed to create your enterprise.\n" + errors.join('\n') + else + alert('Failed to create your enterprise.\nPlease ensure all fields are completely filled out.') ) # RegistrationService.select('about') diff --git a/app/assets/javascripts/darkswarm/services/enterprises.js.coffee b/app/assets/javascripts/darkswarm/services/enterprises.js.coffee index 6040f1d150..2dddfbc7ef 100644 --- a/app/assets/javascripts/darkswarm/services/enterprises.js.coffee +++ b/app/assets/javascripts/darkswarm/services/enterprises.js.coffee @@ -1,4 +1,4 @@ -Darkswarm.factory 'Enterprises', (enterprises, CurrentHub, Taxons, Dereferencer, visibleFilter)-> +Darkswarm.factory 'Enterprises', (enterprises, CurrentHub, Taxons, Dereferencer, visibleFilter, Matcher, Geo, $rootScope)-> new class Enterprises enterprises_by_id: {} constructor: -> @@ -28,3 +28,36 @@ Darkswarm.factory 'Enterprises', (enterprises, CurrentHub, Taxons, Dereferencer, Dereferencer.dereference enterprise.taxons, Taxons.taxons_by_id Dereferencer.dereference enterprise.supplied_taxons, Taxons.taxons_by_id + flagMatching: (query) -> + for enterprise in @enterprises + enterprise.matches_name_query = if query? && query.length > 0 + Matcher.match([enterprise.name], query) + else + false + + calculateDistance: (query, firstMatching) -> + if query?.length > 0 + if firstMatching? + @setDistanceFrom firstMatching + else + @calculateDistanceGeo query + else + @resetDistance() + + calculateDistanceGeo: (query) -> + Geo.geocode query, (results, status) => + $rootScope.$apply => + if status == Geo.OK + #console.log "Geocoded #{query} -> #{results[0].geometry.location}." + @setDistanceFrom results[0].geometry.location + else + console.log "Geocoding failed for the following reason: #{status}" + @resetDistance() + + setDistanceFrom: (locatable) -> + for enterprise in @enterprises + enterprise.distance = Geo.distanceBetween enterprise, locatable + $rootScope.$broadcast 'enterprisesChanged' + + resetDistance: -> + enterprise.distance = null for enterprise in @enterprises diff --git a/app/assets/javascripts/darkswarm/services/geo.js.erb.coffee b/app/assets/javascripts/darkswarm/services/geo.js.erb.coffee new file mode 100644 index 0000000000..2f96722b08 --- /dev/null +++ b/app/assets/javascripts/darkswarm/services/geo.js.erb.coffee @@ -0,0 +1,23 @@ +Darkswarm.service "Geo", -> + new class Geo + OK: google.maps.GeocoderStatus.OK + + # Usage: + # Geo.geocode address, (results, status) -> + # if status == Geo.OK + # console.log results[0].geometry.location + # else + # console.log "Error: #{status}" + geocode: (address, callback) -> + geocoder = new google.maps.Geocoder() + geocoder.geocode {'address': address, 'region': "<%= Spree::Country.find_by_id(Spree::Config[:default_country_id]).iso %>"}, callback + + distanceBetween: (src, dst) -> + google.maps.geometry.spherical.computeDistanceBetween @toLatLng(src), @toLatLng(dst) + + # Wrap an object in a google.maps.LatLng if it has not been already + toLatLng: (locatable) -> + if locatable.lat? + locatable + else + new google.maps.LatLng locatable.latitude, locatable.longitude \ No newline at end of file diff --git a/app/assets/javascripts/darkswarm/services/map.js.coffee b/app/assets/javascripts/darkswarm/services/map.js.coffee index 7ff9f553f2..96768e8379 100644 --- a/app/assets/javascripts/darkswarm/services/map.js.coffee +++ b/app/assets/javascripts/darkswarm/services/map.js.coffee @@ -9,8 +9,8 @@ Darkswarm.factory "OfnMap", (Enterprises, EnterpriseModal, visibleFilter) -> # Adding methods to each enterprise extend: (enterprise) -> new class MapMarker - # We're whitelisting attributes because GMaps tries to crawl - # our data, and our data is recursive, so it breaks + # We cherry-pick attributes because GMaps tries to crawl + # our data, and our data is cyclic, so it breaks latitude: enterprise.latitude longitude: enterprise.longitude icon: enterprise.icon diff --git a/app/assets/javascripts/darkswarm/services/map_configuration.js.coffee b/app/assets/javascripts/darkswarm/services/map_configuration.js.coffee index 8f1d735357..9c5a375d8c 100644 --- a/app/assets/javascripts/darkswarm/services/map_configuration.js.coffee +++ b/app/assets/javascripts/darkswarm/services/map_configuration.js.coffee @@ -7,5 +7,5 @@ Darkswarm.factory "MapConfiguration", -> zoom: 12 additional_options: {} #mapTypeId: 'satellite' - styles: [{"featureType":"landscape","stylers":[{"saturation":-100},{"lightness":65},{"visibility":"on"}]},{"featureType":"poi","stylers":[{"saturation":-100},{"lightness":51},{"visibility":"simplified"}]},{"featureType":"road.highway","stylers":[{"saturation":-100},{"visibility":"simplified"}]},{"featureType":"road.arterial","stylers":[{"saturation":-100},{"lightness":30},{"visibility":"on"}]},{"featureType":"road.local","stylers":[{"saturation":-100},{"lightness":40},{"visibility":"on"}]},{"featureType":"transit","stylers":[{"saturation":-100},{"visibility":"simplified"}]},{"featureType":"administrative.province","stylers":[{"visibility":"off"}]},{"featureType":"water","elementType":"labels","stylers":[{"visibility":"on"},{"lightness":-25},{"saturation":-100}]},{"featureType":"water","elementType":"geometry","stylers":[{"hue":"#ffff00"},{"lightness":-25},{"saturation":-97}]}] + styles: [{"featureType":"landscape","stylers":[{"saturation":-100},{"lightness":65},{"visibility":"on"}]},{"featureType":"poi","stylers":[{"saturation":-100},{"lightness":51},{"visibility":"simplified"}]},{"featureType":"road.highway","stylers":[{"saturation":-100},{"visibility":"simplified"}]},{"featureType":"road.arterial","stylers":[{"saturation":-100},{"lightness":30},{"visibility":"on"}]},{"featureType":"road.local","stylers":[{"saturation":-100},{"lightness":40},{"visibility":"on"}]},{"featureType":"transit","stylers":[{"saturation":-100},{"visibility":"simplified"}]},{"featureType":"administrative.province","stylers":[{"visibility":"off"}]},{"featureType":"water","elementType":"labels","stylers":[{"visibility":"on"},{"lightness":-25},{"saturation":-100}]},{"featureType":"water","elementType":"geometry","stylers":[{"hue":"#ffff00"},{"lightness":-25},{"saturation":-97}]},{"featureType":"road","elementType": "labels.icon","stylers":[{"visibility":"off"}]}] diff --git a/app/assets/javascripts/darkswarm/services/products.js.coffee b/app/assets/javascripts/darkswarm/services/products.js.coffee index 4785adae85..475ed5be0b 100644 --- a/app/assets/javascripts/darkswarm/services/products.js.coffee +++ b/app/assets/javascripts/darkswarm/services/products.js.coffee @@ -10,12 +10,26 @@ Darkswarm.factory 'Products', ($resource, Enterprises, Dereferencer, Taxons, Pro update: => @loading = true - @products = $resource("/shop/products").query (products)=> - @extend() && @dereference() + @products = [] + $resource("/shop/products").query (products)=> + @products = products + + @extend() + @dereference() @registerVariants() @registerVariantsWithCart() @loading = false - @ + + extend: -> + for product in @products + if product.variants?.length > 0 + prices = (v.price for v in product.variants) + product.price = Math.min.apply(null, prices) + product.hasVariants = product.variants?.length > 0 + + product.primaryImage = product.images[0]?.small_url if product.images + product.primaryImageOrMissing = product.primaryImage || "/assets/noimage/small.png" + product.largeImage = product.images[0]?.large_url if product.images dereference: -> for product in @products @@ -42,14 +56,3 @@ Darkswarm.factory 'Products', ($resource, Enterprises, Dereferencer, Taxons, Pro for variant in product.variants Cart.register_variant variant Cart.register_variant product.master if product.master - - extend: -> - for product in @products - if product.variants?.length > 0 - prices = (v.price for v in product.variants) - product.price = Math.min.apply(null, prices) - product.hasVariants = product.variants?.length > 0 - - product.primaryImage = product.images[0]?.small_url if product.images - product.primaryImageOrMissing = product.primaryImage || "/assets/noimage/small.png" - product.largeImage = product.images[0]?.large_url if product.images diff --git a/app/assets/javascripts/darkswarm/services/variants.js.coffee b/app/assets/javascripts/darkswarm/services/variants.js.coffee index 6562bd9e0b..3048049ad5 100644 --- a/app/assets/javascripts/darkswarm/services/variants.js.coffee +++ b/app/assets/javascripts/darkswarm/services/variants.js.coffee @@ -1,6 +1,10 @@ Darkswarm.factory 'Variants', -> new class Variants variants: {} + + clear: -> + @variants = {} + register: (variant)-> @variants[variant.id] ||= @extend variant diff --git a/app/assets/javascripts/darkswarm/util.js.coffee b/app/assets/javascripts/darkswarm/util.js.coffee new file mode 100644 index 0000000000..e6485284b3 --- /dev/null +++ b/app/assets/javascripts/darkswarm/util.js.coffee @@ -0,0 +1,2 @@ +String.prototype.capitalize = -> + this.charAt(0).toUpperCase() + this.slice(1) diff --git a/app/assets/javascripts/shared/angular-slideables.js b/app/assets/javascripts/shared/angular-slideables.js new file mode 100644 index 0000000000..0279c0f4f2 --- /dev/null +++ b/app/assets/javascripts/shared/angular-slideables.js @@ -0,0 +1,55 @@ +/* + * Angular Slideables - A "pure" Angular implementation of jQuery-style slideToggle() + * Source: https://github.com/EricWVGG/AngularSlideables + * By Eric Jacobsen, used under MIT licence + */ + +angular.module('angularSlideables', []) +.directive('slideable', function () { + return { + restrict:'C', + compile: function (element, attr) { + // wrap tag + var contents = element.html(); + element.html('
' + contents + '
'); + + return function postLink(scope, element, attrs) { + // default properties + attrs.duration = (!attrs.duration) ? '1s' : attrs.duration; + attrs.easing = (!attrs.easing) ? 'ease-in-out' : attrs.easing; + element.css({ + 'overflow': 'hidden', + 'height': '0px', + 'transitionProperty': 'height', + 'transitionDuration': attrs.duration, + 'transitionTimingFunction': attrs.easing + }); + }; + } + }; +}) +.directive('slideToggle', function() { + return { + restrict: 'A', + link: function(scope, element, attrs) { + var target, content; + + attrs.expanded = false; + + element.bind('click', function() { + if (!target) target = document.querySelector(attrs.slideToggle); + if (!content) content = target.querySelector('.slideable_content'); + + if(!attrs.expanded) { + content.style.border = '1px solid rgba(0,0,0,0)'; + var y = content.clientHeight; + content.style.border = 0; + target.style.height = y + 'px'; + } else { + target.style.height = '0px'; + } + attrs.expanded = !attrs.expanded; + }); + } + } +}); diff --git a/app/assets/javascripts/shared/ng-tags-input.min.js b/app/assets/javascripts/shared/ng-tags-input.min.js old mode 100755 new mode 100644 diff --git a/app/assets/javascripts/templates/admin/panel.html.haml b/app/assets/javascripts/templates/admin/panel.html.haml new file mode 100644 index 0000000000..be0c98109f --- /dev/null +++ b/app/assets/javascripts/templates/admin/panel.html.haml @@ -0,0 +1,2 @@ +%td{ colspan: "{{columnCount}}" } + .panel{ ng: { include: "template" } } diff --git a/app/assets/javascripts/templates/admin/panels/enterprise_package.html.haml b/app/assets/javascripts/templates/admin/panels/enterprise_package.html.haml new file mode 100644 index 0000000000..c42d8910e1 --- /dev/null +++ b/app/assets/javascripts/templates/admin/panels/enterprise_package.html.haml @@ -0,0 +1,133 @@ +.row.enterprise_package_panel{ ng: { controller: 'indexPackagePanelCtrl' } } + .alpha.eight.columns + %div{ ng: { if: "!enterprise.is_primary_producer", switch: "enterprise.sells" } } + .info{ ng: { switch: { when: "none" } } } + %h3 Hub Profile + + %p + %strong COST: ALWAYS FREE + + %p People can find and contact you on the Open Food Network. Your enterprise will be visible on the map, and will be searchable in listings. + + %p Having a profile, and making connections within your local food system through the Open Food Network will always be free. + + .info{ ng: { switch: { when: "any" } } } + %h3 Hub Shop + + %p + %strong COST: 2% OF SALES, CAPPED AT $50 PER MONTH + + %p Your enterprise is the backbone of your local food system. You aggregate produce from other enterprises and can sell it through your shop on the Open Food Network. + + %p Hubs can take many forms, whether they be a food co-op, a buying group, a veggie-box program, or a local grocery store. + + %p If you also want to sell your own products, you will need to switch this enterprise to be a producer. + + .info{ ng: { switch: { default: true } } } + %h3 + Please Choose a Package + %i.icon-arrow-right + + %p + %strong Your enterprise will not be fully activated until a package is selected from the options on the left. + + %p + Click on an option to see more detailed information about each package, and hit the red SAVE button when you are done! + + + + %div{ ng: { if: "enterprise.is_primary_producer", switch: "enterprise.sells" } } + .info{ ng: { switch: { when: "none" } } } + %h3 Profile Only + + %p + %strong COST: ALWAYS FREE + + %p A profile makes you visible and contactable to others and is a way to share your story. + + %p If you prefer to focus on producing food, and want to leave the work of selling it to someone else, you won't require a shop on the Open Food Network. + + %p Add your products to Open Food Network, allowing hubs to stock your products in their stores. + + .info{ ng: { switch: { when: "own" } } } + %h3 Producer Shop + + %p + %strong COST: 2% OF SALES, CAPPED AT $50 PER MONTH + + %p Sell your products directly to customers through your very own Open Food Network shopfront. + + %p A Producer Shop is for your produce only, if you want to sell produce grown/produced off site, please select 'Producer Hub'. + + .info{ ng: { switch: { when: "any" } } } + %h3 Producer Hub + + %p + %strong COST: 2% OF SALES, CAPPED AT $50 PER MONTH + + %p Your enterprise is the backbone of your local food system. You can sell your own produce as well as produce aggregated from other enterprises through your shopfront on the Open Food Network. + + %p Producer Hubs can take many forms, whether they be a CSA, a veggie-box program, or a food co-op with a rooftop garden. + + %p The Open Food Network aims to support as many hub models as possible, so no matter your situation, we want to provide the tools you need to run your organisation or local food business. + + .info{ ng: { switch: { default: true } } } + %h3 + Please Choose a Package + %i.icon-arrow-right + + %p + %strong Your producer enterprise will not be fully activated until a package is selected from the options on the left. + + %p + Click on an option to see more detailed information about each package, and hit the red SAVE button when you are done! + + .omega.eight.columns{ ng: { switch: "enterprise.is_primary_producer" } } + %div{ ng: { switch: { when: "false" } } } + %a.button.selector.hub-profile{ ng: { click: "enterprise.owned && (enterprise.sells='none')", class: "{selected: enterprise.sells=='none', disabled: !enterprise.owned}" } } + .top + %h3 Profile Only + %p Get a listing + .bottom ALWAYS FREE + %a.button.selector.hub{ ng: { click: "enterprise.owned && (enterprise.sells='any')", class: "{selected: enterprise.sells=='any', disabled: !enterprise.owned}" } } + .top + %h3 Hub Shop + %p Sell produce from others + .bottom + \2% OF SALES + %br + CAPPED AT $50 PER MONTH + + %div{ ng: { switch: { when: "true" } } } + %a.button.selector.producer-profile{ ng: { click: "enterprise.owned && (enterprise.sells='none')", class: "{selected: enterprise.sells=='none', disabled: !enterprise.owned}" } } + .top + %h3 Profile Only + %p Get a listing + .bottom ALWAYS FREE + %a.button.selector.producer-shop{ ng: { click: "enterprise.owned && (enterprise.sells='own')", class: "{selected: enterprise.sells=='own', disabled: !enterprise.owned}" } } + .top + %h3 Producer Shop + %p Sell your own produce + .bottom + \2% OF SALES + %br + CAPPED AT $50 PER MONTH + %a.button.selector.producer-hub{ ng: { click: "enterprise.owned && (enterprise.sells='any')", class: "{selected: enterprise.sells=='any', disabled: !enterprise.owned}" } } + .top + %h3 Producer Hub + %p Sell produce from self and others + .bottom + \2% OF SALES + %br + CAPPED AT $50 PER MONTH + + %a.button.update.fullwidth{ ng: { show: "enterprise.owned", class: "{disabled: saved() && !saving, saving: saving}", click: "save()" } } + %span{ ng: {hide: "saved() || saving" } } + SAVE + %i.icon-save + %span{ ng: {show: "saved() && !saving" } } + SAVED + %i.icon-ok-sign + %span{ ng: {show: "saving" } } + SAVING + %i.icon-refresh diff --git a/app/assets/javascripts/templates/admin/panels/enterprise_producer.html.haml b/app/assets/javascripts/templates/admin/panels/enterprise_producer.html.haml new file mode 100644 index 0000000000..ea8554892f --- /dev/null +++ b/app/assets/javascripts/templates/admin/panels/enterprise_producer.html.haml @@ -0,0 +1,39 @@ +.row.enterprise_producer_panel{ ng: { controller: 'indexProducerPanelCtrl' } } + + .alpha.eight.columns + .info{ ng: { show: "enterprise.is_primary_producer==true" } } + %h3 Producer + %p Producers make yummy things to eat &/or drink. You're a producer if you grow it, raise it, brew it, bake it, ferment it, milk it or mould it. + + %p Producers can also perform other functions, such as aggregating food from other enterprises and selling it through a shop on the Open Food Network. + + + .info{ ng: { show: "enterprise.is_primary_producer==false" } } + %h3 Non-Producer + %p Non-producers do not produce any food themselves, meaning that they cannot create their own products for sale through the Open Food Network. + + %p Instead, non-producers specialise in linking producers to the end eater, whether it be by aggregating, grading, packing, selling or delivering food. + + .omega.eight.columns + %a.button.selector.producer{ ng: { click: 'enterprise.owned && changeToProducer()', class: "{selected: enterprise.is_primary_producer==true, disabled: !enterprise.owned}" } } + .top + %h3 PRODUCER + %p Producers of food + .bottom eg. GROWERS, BAKERS, BREWERS, MAKERS + + %a.button.selector.non-producer{ ng: { click: 'enterprise.owned && changeToNonProducer()', class: "{selected: enterprise.is_primary_producer==false, disabled: !enterprise.owned}" } } + .top + %h3 Non-Producer + %p All other food enterprises + .bottom eg. Grocery stores, Food co-ops, Buying groups + + %a.button.update.fullwidth{ ng: { show: "enterprise.owned", class: "{disabled: saved() && !saving, saving: saving}", click: "save()" } } + %span{ ng: {hide: "saved() || saving" } } + SAVE + %i.icon-save + %span{ ng: {show: "saved() && !saving" } } + SAVED + %i.icon-ok-sign + %span{ ng: {show: "saving" } } + SAVING + %i.icon-refresh diff --git a/app/assets/javascripts/templates/admin/panels/enterprise_status.html.haml b/app/assets/javascripts/templates/admin/panels/enterprise_status.html.haml new file mode 100644 index 0000000000..39392a497d --- /dev/null +++ b/app/assets/javascripts/templates/admin/panels/enterprise_status.html.haml @@ -0,0 +1,29 @@ +.row.enterprise_status_panel{ ng: { controller: 'indexStatusPanelCtrl' } } + .alpha.omega.sixteen.columns + + %h4.status-ok.text-center{ ng: { show: "issues.length == 0 && warnings.length == 0" } } + %i.icon-ok-sign + {{ object.name }} is set up and ready to go! + + %table{ ng: { show: "issues.length > 0 || warnings.length > 0" } } + %thead + %th.severity + Severity + %th.description + Description + %th.resolve + Resolve + %tr{ ng: { repeat: "issue in issues"} } + %td.severity + %i.icon-warning-sign.issue + %td.description + %span{ bo: { bind: "issue.description" } } + %td.resolve + %div{ ng: { bind: { html: "issue.link" } } } + %tr{ ng: { repeat: "warning in warnings"} } + %td.severity + %i.icon-warning-sign.warning + %td.description + %span{ bo: { bind: "warning.description" } } + %td.resolve + %div{ ng: { bind: { html: "warning.link" } } } diff --git a/app/assets/javascripts/templates/enterprise_modal.html.haml b/app/assets/javascripts/templates/enterprise_modal.html.haml index 6b13f6ee78..350cd6ed80 100644 --- a/app/assets/javascripts/templates/enterprise_modal.html.haml +++ b/app/assets/javascripts/templates/enterprise_modal.html.haml @@ -1,4 +1,5 @@ %ng-include{src: "'partials/enterprise_header.html'"} %ng-include{src: "'partials/enterprise_details.html'"} %ng-include{src: "'partials/hub_details.html'"} +%ng-include{src: "'partials/producer_details.html'"} %ng-include{src: "'partials/close.html'"} diff --git a/app/assets/javascripts/templates/filter_selector.html.haml b/app/assets/javascripts/templates/filter_selector.html.haml index c6990c369f..a53c4f44db 100644 --- a/app/assets/javascripts/templates/filter_selector.html.haml +++ b/app/assets/javascripts/templates/filter_selector.html.haml @@ -1,4 +1,4 @@ -%div{bindonce:true, style: "display: inline-block" } +%ul{ bindonce: true } %active-selector{ ng: { repeat: "selector in allSelectors", show: "ifDefined(selector.fits, true)" } } %render-svg{path: "{{selector.object.icon}}", ng: { if: "selector.object.icon"} } %span{"bo-text" => "selector.object.name"} diff --git a/app/assets/javascripts/templates/partials/enterprise_header.html.haml b/app/assets/javascripts/templates/partials/enterprise_header.html.haml index c0d9d5d9e0..44175a57fe 100644 --- a/app/assets/javascripts/templates/partials/enterprise_header.html.haml +++ b/app/assets/javascripts/templates/partials/enterprise_header.html.haml @@ -2,7 +2,7 @@ .highlight-top.row .small-12.medium-7.large-8.columns %h3{"ng-if" => "enterprise.is_distributor"} - %a{"bo-href" => "enterprise.path", "ofn-empties-cart" => "enterprise"} + %a{"bo-href" => "enterprise.path", "ofn-change-hub" => "enterprise"} %i{"ng-class" => "enterprise.icon_font"} %span{"bo-text" => "enterprise.name"} %h3{"ng-if" => "!enterprise.is_distributor", "ng-class" => "{'is_producer' : enterprise.is_primary_producer}"} @@ -10,4 +10,4 @@ %span{"bo-text" => "enterprise.name"} .small-12.medium-5.large-4.columns.text-right.small-only-text-left %p{"bo-bind" => "[enterprise.address.city, enterprise.address.state_name] | printArray"} - %img.hero-img{"bo-src" => "enterprise.promo_image"} + %img.hero-img{"bo-src" => "enterprise.promo_image"} diff --git a/app/assets/javascripts/templates/partials/hub_actions.html.haml b/app/assets/javascripts/templates/partials/hub_actions.html.haml index f5b451ef3c..c17f9a86f5 100644 --- a/app/assets/javascripts/templates/partials/hub_actions.html.haml +++ b/app/assets/javascripts/templates/partials/hub_actions.html.haml @@ -1,16 +1,15 @@ .row.pad-top{bindonce: true, "ng-if" => "enterprise.hubs.length > 0 && enterprise.is_distributor"} .cta-container.small-12.columns - %label - Shop for - %strong{"bo-text" => "enterprise.name"} + %label + Shop for + %strong{"bo-text" => "enterprise.name"} products at: - %a.cta-hub{"ng-repeat" => "hub in enterprise.hubs", - "bo-href" => "hub.path", + %a.cta-hub{"ng-repeat" => "hub in enterprise.hubs", + "bo-href" => "hub.path", "bo-class" => "{primary: hub.active, secondary: !hub.active}", - "ofn-empties-cart" => "hub"} + "ofn-change-hub" => "hub"} %i.ofn-i_033-open-sign{"bo-if" => "hub.active"} %i.ofn-i_032-closed-sign{"bo-if" => "!hub.active"} - .hub-name{"bo-text" => "hub.name"} + .hub-name{"bo-text" => "hub.name"} .button-address{"bo-bind" => "[hub.address.city, hub.address.state_name] | printArray"} / %i.ofn-i_007-caret-right - diff --git a/app/assets/javascripts/templates/partials/hub_details.html.haml b/app/assets/javascripts/templates/partials/hub_details.html.haml index 8be5ceddac..17fabe846a 100644 --- a/app/assets/javascripts/templates/partials/hub_details.html.haml +++ b/app/assets/javascripts/templates/partials/hub_details.html.haml @@ -5,18 +5,18 @@ %label{"active-table-hub-link" => "enterprise", change: "Change shop to:", shop: "Shop now at:"} .small-8.columns.right %label.right{"bo-if" => "enterprise.pickup || enterprise.delivery"} - Delivery options: - %span{"bo-if" => "enterprise.pickup"} + Delivery options: + %span{"bo-if" => "enterprise.pickup"} %i.ofn-i_038-takeaway Pickup - %span{"bo-if" => "enterprise.delivery"} + %span{"bo-if" => "enterprise.delivery"} %i.ofn-i_039-delivery Delivery .row .columns.small-12 - %a.cta-hub{"bo-href" => "enterprise.path", + %a.cta-hub{"bo-href" => "enterprise.path", "ng-class" => "{primary: enterprise.active, secondary: !enterprise.active}", - "ofn-empties-cart" => "enterprise"} + "ofn-change-hub" => "enterprise"} %i.ofn-i_033-open-sign{"bo-if" => "enterprise.active"} %i.ofn-i_032-closed-sign{"bo-if" => "!enterprise.active"} .hub-name{"bo-text" => "enterprise.name"} diff --git a/app/assets/javascripts/templates/partials/producer_details.html.haml b/app/assets/javascripts/templates/partials/producer_details.html.haml new file mode 100644 index 0000000000..08ac6ea1f1 --- /dev/null +++ b/app/assets/javascripts/templates/partials/producer_details.html.haml @@ -0,0 +1,23 @@ +-# Show places to buy products from this producer, when there are any +-# Do not show this for producer shops selling only their own produce, +-# Since a shopping link will already have been displayed in hub_details.html.haml +.row.active_table_row.pad-top{bindonce: true, "ng-if" => "enterprise.is_primary_producer && enterprise.hubs.length > 0 && !(enterprise.hubs.length == 1 && enterprise.hubs[0] == enterprise)"} + .columns.small-12 + .row + .columns.small-12.fat + %div{"bo-if" => "enterprise.name"} + %label + Shop for + %span.turquoise{"bo-text" => "enterprise.name"} + products at: + %div.show-for-medium-up{"bo-if" => "!enterprise.name"} +   + .row.cta-container + .columns.small-12 + %a.cta-hub{"ng-repeat" => "hub in enterprise.hubs | filter:{id: '!'+enterprise.id} | orderBy:'-active'", + "bo-href" => "hub.path", "ofn-empties-cart" => "hub", + "bo-class" => "{primary: hub.active, secondary: !hub.active}"} + %i.ofn-i_033-open-sign{"bo-if" => "hub.active"} + %i.ofn-i_032-closed-sign{"bo-if" => "!hub.active"} + .hub-name{"bo-text" => "hub.name"} + .button-address{"bo-bind" => "[hub.address.city, hub.address.state_name] | printArray"} diff --git a/app/assets/javascripts/templates/product_modal.html.haml b/app/assets/javascripts/templates/product_modal.html.haml index 3db8035acc..481fa67c20 100644 --- a/app/assets/javascripts/templates/product_modal.html.haml +++ b/app/assets/javascripts/templates/product_modal.html.haml @@ -4,17 +4,15 @@ %h3{"bo-text" => "product.name"} %span %em from - %span.avenir{"bo-text" => "enterprise.name"} + %span{"bo-text" => "enterprise.name"} %br .filter-shopfront.taxon-selectors.inline-block - %ul - %filter-selector{ objects: "[product] | taxonsOf" } + %filter-selector{ objects: "[product] | taxonsOf" } .filter-shopfront.property-selectors.inline-block - %ul - %filter-selector{ objects: "[product] | propertiesWithValuesOf" } + %filter-selector{ objects: "[product] | propertiesWithValuesOf" } %div{"ng-if" => "product.description"} %hr diff --git a/app/assets/javascripts/templates/shop_variant.html.haml b/app/assets/javascripts/templates/shop_variant.html.haml index 6d050d0bde..04048f3cd0 100644 --- a/app/assets/javascripts/templates/shop_variant.html.haml +++ b/app/assets/javascripts/templates/shop_variant.html.haml @@ -11,6 +11,7 @@ .small-5.medium-3.large-3.columns.text-right{"bo-if" => "!variant.product.group_buy"} %input{type: :number, + integer: true, value: nil, min: 0, placeholder: "0", @@ -26,14 +27,17 @@ %span.bulk-input %input.bulk.first{type: :number, value: nil, + integer: true, min: 0, "ng-model" => "variant.line_item.quantity", placeholder: "min", "ofn-disable-scroll" => true, max: "{{variant.on_demand && 9999 || variant.count_on_hand }}", name: "variants[{{variant.id}}]", id: "variants_{{variant.id}}"} - %span.bulk-input{"bo-if" => "variant.product.group_buy"} + %span.bulk-input %input.bulk.second{type: :number, + "ng-disabled" => "!variant.line_item.quantity", + integer: true, min: 0, "ng-model" => "variant.line_item.max_quantity", placeholder: "max", diff --git a/app/assets/javascripts/templates/single_line_selectors.html.haml b/app/assets/javascripts/templates/single_line_selectors.html.haml index d54ce57bbb..d8d5ae6f3a 100644 --- a/app/assets/javascripts/templates/single_line_selectors.html.haml +++ b/app/assets/javascripts/templates/single_line_selectors.html.haml @@ -1,8 +1,8 @@ -%ul - -# In order for the single-line-selector scope to have access to the available selectors, - %filter-selector{objects: "objects()", "active-selectors" => "activeSelectors", "all-selectors" => "allSelectors" } +-# In order for the single-line-selector scope to have access to the available selectors, +%filter-selector{objects: "objects()", "active-selectors" => "activeSelectors", "all-selectors" => "allSelectors" } - %li.more{ ng: { show: "overFlowSelectors().length > 0 || fitting" } } +%ul{ ng: { if: "overFlowSelectors().length > 0 || fitting" } } + %li.more %a.dropdown{ data: { dropdown: "{{ 'show-more-' + selectorName }}" }, ng: { class: "{active: selectedOverFlowSelectors().length > 0}" } } %span + {{ overFlowSelectors().length }} more diff --git a/app/assets/stylesheets/admin/account.css.scss b/app/assets/stylesheets/admin/account.css.scss new file mode 100644 index 0000000000..7d58147d91 --- /dev/null +++ b/app/assets/stylesheets/admin/account.css.scss @@ -0,0 +1,17 @@ +.row.invoice_title { + margin-bottom: 0px; +} + +table.invoice_summary { + margin-bottom: 70px; + + tr.total { + font-weight: bold; + } +} + +.invoice_title { + .balance { + color: #9fc820; + } +} diff --git a/app/assets/stylesheets/admin/enterprise_index_panels.css.scss b/app/assets/stylesheets/admin/enterprise_index_panels.css.scss new file mode 100644 index 0000000000..2ce2916497 --- /dev/null +++ b/app/assets/stylesheets/admin/enterprise_index_panels.css.scss @@ -0,0 +1,113 @@ +.enterprise_package_panel, .enterprise_producer_panel { + .info { + p { + font-size: 1rem; + margin: 10px 0px; + } + } + + a.update { + cursor: pointer; + margin-bottom: 10px; + font-size: 1.3rem; + background-color: #DA5354; + &:hover { + background-color: #CD4E4F; + } + &.disabled { + background-color: #C1C1C1; + } + &.saving { + background-color: #FF9848; + i.icon-refresh { + -webkit-animation: spin 2s infinite linear; + animation: spin 2s infinite linear; + } + } + span{ + i{ + font-size: 1.5rem; + margin-left: 10px; + } + } + } + + a.selector { + display: block; + position: relative; + margin-bottom: 20px; + border: 2px solid black; + text-align: center; + // width: 100%; + cursor: pointer; + &, & * { + color: white; + } + &:hover { + &:after { + border-top-color: #9fc820; + } + } + &.disabled{ + background-color: #C1C1C1; + } + .bottom { + background: repeating-linear-gradient(60deg, rgba(84, 152, 218, 0), rgba(84, 152, 218, 0) 5px, rgba(255, 255, 255, 0.25) 5px, rgba(255, 255, 255, 0.25) 10px); + margin-top: 1em; + margin-left: -15px; + margin-right: -15px; + padding: 5px; + text-transform: uppercase; + } + &.selected { + background-color: #000000; + + &:after { + top: 50%; + left: 0; + border: solid transparent; + content: " "; + height: 0; + width: 0; + position: absolute; + pointer-events: none; + + border-top: 20px solid transparent; + border-bottom: 20px solid transparent; + border-right: 20px solid #000000; + margin-top: -20px; + margin-left: -20px; + } + } + } +} + +.enterprise_status_panel { + .status-ok { + margin: 30px 0px; + i.icon-ok-sign { + color: #9fc820; + font-size: 1.5rem; + } + } + + td.description{ + font-size: 0.9rem; + } + + td.severity { + text-align: center; + + i { + font-size: 1.5rem; + + &.issue{ + color: #da5354; + } + + &.warning{ + color: #ff9848; + } + } + } +} diff --git a/app/assets/stylesheets/admin/index_panels.css.scss b/app/assets/stylesheets/admin/index_panels.css.scss new file mode 100644 index 0000000000..a910e41d14 --- /dev/null +++ b/app/assets/stylesheets/admin/index_panels.css.scss @@ -0,0 +1,130 @@ +tr.panel-toggle-row { + td.panel-toggle{ + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + + position: relative; + + i.icon-chevron::before { + font-size: 1.2rem; + content: "\f078"; + } + + &.error::before { + font-family: FontAwesome; + font-weight: normal; + font-style: normal; + display: inline-block; + text-decoration: inherit; + position: absolute; + top: 5px; + right: 5px; + font-size: 2rem; + -webkit-font-smoothing: antialiased; + content: "\f071"; + color: #da5354; + } + + &.status { + i.icon-status::before { + font-size: 1.5rem; + opacity: 0.5; + } + + i.issue::before { + content: "\f071"; + color: #da5354; + } + + i.warning::before { + content: "\f071"; + color: #ff9848; + } + + i.ok::before { + content: "\f058"; + color: #9fc820; + } + } + + &:hover { + cursor: pointer; + background-color: #d0e2f6; + * { + color: #1b3c56; + } + + i.icon-status::before { + opacity: 1.0; + } + } + } + + &.expanded{ + td { + border-bottom: 2px solid #444444; + + &.selected { + background-color: #ffffff; + border-left: 2px solid #444444; + border-right: 2px solid #444444; + border-top: 2px solid #444444; + border-bottom: none; + + &:hover { + background-color: #ffffff; + } + + * { + color: #1b3c56; + } + + i.icon-status::before { + opacity: 1.0; + } + + i.icon-chevron::before { + content: "\f077"; + } + } + } + } +} + +tr.panel-row { + display: none; + + &:hover { + td { + background-color: #ffffff; + } + } + + >td { + border-color: #444444; + padding: 0; + .panel { + border-left: 1px solid #444444; + border-right: 1px solid #444444; + border-bottom: 1px solid #444444; + + .row{ + margin: 0px -4px; + + padding: 20px 0px; + + .column.alpha, .columns.alpha { + padding-left: 20px; + } + + .column.omega, .columns.omega { + padding-right: 20px; + } + } + } + } +} diff --git a/app/assets/stylesheets/darkswarm/_shop-filters.css.sass b/app/assets/stylesheets/darkswarm/_shop-filters.css.sass index 3b31481ee1..cf80c63035 100644 --- a/app/assets/stylesheets/darkswarm/_shop-filters.css.sass +++ b/app/assets/stylesheets/darkswarm/_shop-filters.css.sass @@ -92,24 +92,23 @@ span.filter-label opacity: 0.75 -.filter-shopfront.taxon-selectors, .filter-shopfront.property-selectors - background: transparent - - single-line-selectors - overflow-x: hidden - white-space: nowrap - - .f-dropdown - overflow-x: auto - white-space: normal - - ul - margin: 0 - ul, ul li - list-style: none - - .filter-shopfront + &.taxon-selectors, &.property-selectors + background: transparent + + single-line-selectors + overflow-x: hidden + white-space: nowrap + + .f-dropdown + overflow-x: auto + white-space: normal + + ul + margin: 0 + display: inline-block + ul, ul li + list-style: none // Shopfront taxons &.taxon-selectors diff --git a/app/assets/stylesheets/darkswarm/_shop-navigation.css.sass b/app/assets/stylesheets/darkswarm/_shop-navigation.css.sass index 17b68a3e46..a2f2304d3e 100644 --- a/app/assets/stylesheets/darkswarm/_shop-navigation.css.sass +++ b/app/assets/stylesheets/darkswarm/_shop-navigation.css.sass @@ -1,3 +1,4 @@ +@import typography .darkswarm @@ -20,7 +21,7 @@ margin-right: 12px location - font-family: "AvenirBla_IE", "AvenirBla" + @include headingFont @media all and (max-width: 768px) location, location + small display: block @@ -63,6 +64,7 @@ @media all and (max-width: 768px) font-size: 0.875em closing + @include headingFont @media all and (max-width: 768px) font-size: 1.2em padding-bottom: 10px diff --git a/app/assets/stylesheets/darkswarm/all.scss b/app/assets/stylesheets/darkswarm/all.scss index 80e43cdc8b..6cfc32e605 100644 --- a/app/assets/stylesheets/darkswarm/all.scss +++ b/app/assets/stylesheets/darkswarm/all.scss @@ -2,13 +2,15 @@ * This is a manifest file that'll automatically include all the stylesheets available in this directory * and any sub-directories. You're free to add application-wide styles to this file and they'll appear at * the top of the compiled file, but it's generally better to create a new file per style scope. - + *= require_self - *= require foundation - *= require_tree . */ +@import 'variables'; +@import 'foundation'; @import 'foundation-icons'; +@import '*'; + ofn-modal { display: block; } \ No newline at end of file diff --git a/app/assets/stylesheets/darkswarm/all_split2.css b/app/assets/stylesheets/darkswarm/all_split2.css new file mode 100644 index 0000000000..e231ca5d02 --- /dev/null +++ b/app/assets/stylesheets/darkswarm/all_split2.css @@ -0,0 +1,3 @@ +/* + *= require 'darkswarm/all' + */ diff --git a/app/assets/stylesheets/darkswarm/animations.sass b/app/assets/stylesheets/darkswarm/animations.sass index 62e87e7224..d452b69d39 100644 --- a/app/assets/stylesheets/darkswarm/animations.sass +++ b/app/assets/stylesheets/darkswarm/animations.sass @@ -55,7 +55,7 @@ 100% opacity: 1 -@-webkit-keyframes spin +@-webkit-keyframes spin 0% -webkit-transform: rotate(0deg) transform: rotate(0deg) @@ -104,12 +104,10 @@ .animate-repeat - -webkit-transform: translateZ(0) - transform: translateZ(0) &.ng-move, &.ng-enter, &.ng-leave - -webkit-transition: all 300ms linear - transition: all 300ms linear - + -webkit-transition: all 300ms linear + transition: all 300ms linear + &.ng-leave opacity: 1 &.ng-leave-active @@ -178,7 +176,7 @@ product.animate-repeat overflow: hidden max-height: 0 opacity: 0 !important - + // &.ng-hide-add-active, &.ng-hide-remove-active &.ng-hide-add, &.ng-hide-remove @@ -197,7 +195,7 @@ product.animate-repeat &.ng-hide opacity: 0 !important - + // &.ng-hide-add-active, &.ng-hide-remove-active &.ng-hide-add, &.ng-hide-remove @@ -206,8 +204,8 @@ product.animate-repeat it as hidden. */ display: block !important - - + + @mixin csstrans @@ -217,7 +215,3 @@ product.animate-repeat -o-transition: all 300ms ease transition: all 300ms ease -webkit-transform-style: preserve-3d - - - - diff --git a/app/assets/stylesheets/darkswarm/big-input.sass b/app/assets/stylesheets/darkswarm/big-input.sass index 165404fff3..0660fda460 100644 --- a/app/assets/stylesheets/darkswarm/big-input.sass +++ b/app/assets/stylesheets/darkswarm/big-input.sass @@ -6,14 +6,14 @@ //Big search used in active table search \\ @mixin big-input($input, $inputhvr, $inputactv) - @include avenir + @include headingFont @include csstrans @include border-radius(0.5rem) background: rgba(255,255,255,0.1) border: 2px solid $input font-size: 2rem box-shadow: 0 - padding: 0.75rem 1rem 0.35rem + padding: 0.5rem 1rem height: auto width: 100% margin-bottom: 0.5rem @@ -33,7 +33,7 @@ background: white background: rgba(255,255,255,0.5) text-shadow: 0 0 10px #ffffff - padding: 1.5rem 1rem 1rem + padding: 1rem letter-spacing: 0.02rem outline: none @@ -44,7 +44,7 @@ letter-spacing: 0 @mixin medium-input($input, $inputhvr, $inputactv) - @include avenir + @include headingFont @include csstrans @include border-radius(0.5rem) background: rgba(255,255,255,0.1) diff --git a/app/assets/stylesheets/darkswarm/branding.css.sass b/app/assets/stylesheets/darkswarm/branding.css.sass index da2600818e..76614599cf 100644 --- a/app/assets/stylesheets/darkswarm/branding.css.sass +++ b/app/assets/stylesheets/darkswarm/branding.css.sass @@ -1,3 +1,11 @@ +$ofn-brand: #f27052 +// e.g. australia, uk, norway specific color + +$ofn-grey: #808184 + + +// old colors: + $clr-brick: #c1122b $clr-brick-light: #f5e6e7 $clr-brick-light-trans: rgba(245, 230, 231, 0.9) diff --git a/app/assets/stylesheets/darkswarm/footer.sass b/app/assets/stylesheets/darkswarm/footer.sass index 9e3f9dac55..76dd0f4384 100644 --- a/app/assets/stylesheets/darkswarm/footer.sass +++ b/app/assets/stylesheets/darkswarm/footer.sass @@ -1,15 +1,78 @@ @import branding @import mixins +@import animations footer - background: $dark-grey - border-top: 1px dotted white - @include panepadding .row - &, & * + p a + font-size: 0.875rem + a, a * + @include csstrans color: white - a, a * - color: $clr-brick-light-bright &:hover, &:active, &:focus - color: $clr-brick-bright - @include textsoftpress + color: rgba(white, 1) + text-decoration: underline + + .footer-global + background-color: $ofn-grey + padding-top: 60px + padding-bottom: 40px + .logo + width: 200px + height: 200px + background: $ofn-grey + @include border-radius(120px) + margin: -140px auto 0 auto + img + margin-top: 36px + + .alert-box + background-color: transparent + border: none + padding: 0 + a.big-alert + @include csstrans + width: 100% + border: 1px solid rgba($dark-grey, 0.35) + background-image: url("/assets/tile-wide.png") + background-position: center center + background-color: #bbb + padding: 12px 0 8px 0 + display: block + &, & * + @include csstrans + color: #333 + strong + letter-spacing: 0.5px + &:hover, &:active, &:focus + text-decoration: none + border-color: white + &, & * + color: rgba(white, 1) + .row + &, p, h1, h2, h3, h4, h5, h6 + color: $disabled-bright + + .footer-local + background: lighten($dark-grey, 3%) + @include panepadding + .row + &, p, h1, h2, h3, h4, h5, h6 + color: $disabled-med + p.secure-icon i + font-size: 10rem + color: rgba(white, 0.1) + p.secure-text + color: rgba($disabled-med, 0.35) + .social-icons + margin-bottom: 0.25rem + margin-top: 0.75rem + a + i + font-size: 1.5rem + color: white + &:hover, &:active, &:focus + text-decoration: none + i + color: lighten($dark-grey, 60%) + text-shadow: 2px 2px 0 black diff --git a/app/assets/stylesheets/darkswarm/groups.css.sass b/app/assets/stylesheets/darkswarm/groups.css.sass index 7f70bf7211..97b984eb34 100644 --- a/app/assets/stylesheets/darkswarm/groups.css.sass +++ b/app/assets/stylesheets/darkswarm/groups.css.sass @@ -1,14 +1,16 @@ @import branding @import mixins +@import typography // Search page #groups - background-color: $clr-brick-light + background-color: lighten($clr-brick, 56%) background-image: url("/assets/groups.svg") - background-position: center 15px + background-position: center 50px background-repeat: no-repeat - padding-bottom: 20px - + background-size: 922px 922px + @include sidepaddingSm + @include panepadding a > .group-name &:hover, &:focus, &:active text-decoration: underline @@ -65,7 +67,7 @@ margin-bottom: -2px margin-right: 2px text-transform: capitalize - @include avenir + @include headingFont @include border-radius(1em 0.25em 0 0) @include gradient($disabled-light, $disabled-bright) @media screen and (min-width: 768px) diff --git a/app/assets/stylesheets/darkswarm/header.css.sass b/app/assets/stylesheets/darkswarm/header.css.sass index f0bf6072d6..523afca824 100644 --- a/app/assets/stylesheets/darkswarm/header.css.sass +++ b/app/assets/stylesheets/darkswarm/header.css.sass @@ -1,18 +1,20 @@ +@import variables + nav.top-bar margin-bottom: 0px a.icon &:hover text-decoration: none - height: 45px + height: $topbar-height color: white i font-size: 29px - line-height: 45px + line-height: $topbar-height span font-size: 13px display: inline-block - line-height: 45px - height: 45px + line-height: $topbar-height + height: $topbar-height vertical-align: top body > section[role='main'] diff --git a/app/assets/stylesheets/darkswarm/home.css.sass b/app/assets/stylesheets/darkswarm/home.css.sass deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/app/assets/stylesheets/darkswarm/home_panes.css.sass b/app/assets/stylesheets/darkswarm/home_panes.css.sass index 568b6ebc66..ed74450317 100644 --- a/app/assets/stylesheets/darkswarm/home_panes.css.sass +++ b/app/assets/stylesheets/darkswarm/home_panes.css.sass @@ -1,57 +1,149 @@ @import branding @import mixins - -// Styling for big panes on homepage - -#beta.pane - .row - @include panepadding - background-image: url("/assets/home/macbook.png") - background-repeat: no-repeat - background-position: center bottom - padding-bottom: 280px - &, & * - max-width: 610px +@import typography +@import animations +@import variables -#map.pane - @include darkbg - .row - @include panepadding - background-image: url("/assets/home/maps-bg.svg") - background-repeat: no-repeat - background-position: left center - -#groups.pane - @include darkbg - .row - @include panepadding - background-image: url("/assets/home/groups-bg.svg") - background-repeat: no-repeat - background-position: left center - -#producers.pane - @include turqbg - @include panepadding - background-image: url("/assets/home/producers-bg.svg") - background-repeat: no-repeat - background-position: center center - background-size: 80% 80% - - @media all and (max-width: 768px) - background-position: center top - background-size: 100% 100% - .row - .pricing-table - .title - color: $clr-turquoise-light - .price - background-color: rgba(240, 240, 240, 0.6) - .description, .bullet-item, .cta-button - background-color: rgba(255, 255, 255, 0.8) - -// Responsive -@media all and (max-width: 768px) - #map.pane, #groups.pane, #producers.pane +// Styling for big panes on homepage \\ +#panes + .pane .row - background-position: center center \ No newline at end of file + @include panepadding + padding-top: 75px + padding-bottom: 75px + &.header + padding-bottom: 0 + &.content + padding-top: 0 + + +// Background styles \\ +#system.pane + background-color: white + +#brand-story.pane, #cta.pane, #shops.pane + @include tiledPane + +#stats.pane + background-image: url("/assets/home/background-blurred-oranges.jpg") + background-position: center center + background-color: $ofn-grey + @include fullbg + @include paneWhiteText + + +// Content styles \\ +#brand-story.pane + .row + h2 + font-weight: 300 + font-size: 88px + p + @include bodyFont + font-size: 1.5rem + font-weight: 300 + @media all and (max-width: 768px) + h2 + font-size: 52px + p + font-size: 1.3rem + + a.text-vbig i + font-size: 75px + +#system.pane + .row .row + padding-bottom: 0 + @media all and (max-width: 640px) + .row .row + padding: 0 + + .home-icon-box + background-image: url("/assets/ofn-o.png") + background-position: center center + background-repeat: no-repeat + background-size: auto 100% + padding: 3rem 0 + text-align: center + margin-top: 2rem + @media all and (min-width: 642px) + margin-top: 0 + i + font-size: 4rem + a + display: block + width: 64px + height: 64px + margin: 0 auto + background-color: $brand-colour + background-position: center center + background-repeat: no-repeat + background-size: auto 100% + &.search + background-image: url("/assets/icon-mask-magnifier.png") + &.shop + background-image: url("/assets/icon-mask-apple.png") + &.pick-up-delivery + background-image: url("/assets/icon-mask-truck.png") + + h2 + font-size: 70px + font-weight: 300 + color: $brand-colour + @media all and (max-width: 640px) + font-size: 45px + + + a + color: $brand-colour + + .home-icon-box-bottom + margin-top: 1rem + width: 100% + padding-left: 1rem + padding-right: 1rem + @media all and (min-width: 480px) + padding-left: 3rem + padding-right: 3rem + @media all and (min-width: 642px) + padding-left: 1rem + padding-right: 1rem + h4 + color: $brand-colour + border-bottom: 2px solid lighten($brand-colour, 20%) + text-align: center + padding: 1rem 0 + margin: 1.5rem 0 + +#cta.pane, #stats.pane + h2 + font-weight: 300 + font-size: 45px + margin-bottom: 2rem + @media all and (max-width: 830px) + font-size: 35px + +#stats.pane + .row.header + padding-bottom: 2rem + + h4 + font-weight: 300 + text-transform: uppercase + margin: 1.5rem 0 + display: inline-block + strong + display: block + font-weight: normal + font-size: 75px + +#shops.pane + @include paneWhiteText + h2 + margin-bottom: 2rem + font-size: 4.4rem + font-weight: 300 + +#shops-signup.pane + background-color: $brand-colour diff --git a/app/assets/stylesheets/darkswarm/home_tagline.css.sass b/app/assets/stylesheets/darkswarm/home_tagline.css.sass index ed85e07d60..fadaae70b0 100644 --- a/app/assets/stylesheets/darkswarm/home_tagline.css.sass +++ b/app/assets/stylesheets/darkswarm/home_tagline.css.sass @@ -1,25 +1,33 @@ @import branding @import mixins +@import variables // Styling for brand intro / tagline on homepage #tagline - background-color: black - background-image: url("/assets/home/ofn_bg_1.jpg") - @include fullbg - height: 400px - padding: 40px 0px - h1, h2, p - color: white - h1 - margin-bottom: 1em - h2 - font-size: 1.6875rem - max-width: 610px - margin: 0 auto + width: 100% + &:before + content: "" + @include fullbg + background-color: $ofn-grey + background-image: url("/assets/home/home.jpg") + position: fixed + left: 0 + right: 0 + bottom: 0 + z-index: -1 + width: 100% + height: 100% - a - color: white - &:hover, &:active, &:focus - color: $clr-brick-light-bright - @include textsoftpress + h1 + margin-top: 2rem + @media all and (min-width: 768px) + margin-top: 10rem + img + max-width: 45% + @media all and (min-height: 500px) + max-width: 80% + + margin-bottom: 2rem + @media all and (min-width: 768px) + margin-bottom: 5rem diff --git a/app/assets/stylesheets/darkswarm/hub_node.css.sass b/app/assets/stylesheets/darkswarm/hub_node.css.sass index 94950fa22c..4d175681f2 100644 --- a/app/assets/stylesheets/darkswarm/hub_node.css.sass +++ b/app/assets/stylesheets/darkswarm/hub_node.css.sass @@ -30,9 +30,18 @@ float: right margin-left: 0.5rem + //Hub Link + @media all and (max-width: 640px) + a.hub + display: block + //Hub Name span.hub-name-listing font-weight: 700 + &:after + content: ">>" + display: inline-block + margin-left: 5px //CLOSED row &.closed diff --git a/app/assets/stylesheets/darkswarm/hubs.css.sass b/app/assets/stylesheets/darkswarm/hubs.css.sass index 43f2b8cbea..a351170d99 100644 --- a/app/assets/stylesheets/darkswarm/hubs.css.sass +++ b/app/assets/stylesheets/darkswarm/hubs.css.sass @@ -2,8 +2,9 @@ @import mixins #hubs - background-repeat: repeat - background-image: url("/assets/subtle_white_feathers.png") - // background: $clr-brick-ultra-light url("/assets/home/shopping-bg.jpg") - // @include fullwidthbg + background-color: lighten($ofn-grey, 43%) @include panepadding + @include sidepaddingSm + + .name-matches, .distance-matches + margin-top: 4em \ No newline at end of file diff --git a/app/assets/stylesheets/darkswarm/menu.css.sass b/app/assets/stylesheets/darkswarm/menu.css.sass index b256c6165c..41b86f72ff 100644 --- a/app/assets/stylesheets/darkswarm/menu.css.sass +++ b/app/assets/stylesheets/darkswarm/menu.css.sass @@ -1,49 +1,75 @@ +@import compass @import branding @import mixins @import typography +@import variables -.fixed .top-bar - @include box-shadow(0 2px 3px 0 rgba(0,0,0,0.25)) - nav @include textpress + text-shadow: none + + // Create center style for nav ul (foundation provides left and right) + text-align: center + .top-bar-section + // Avoid menu items blocking logo + li:not(.has-form), li:not(.has-form) a:not(.button), li:not(.has-form) a:not(.button):hover + background-color: transparent + + ul.center + display: inline-block + // By default, we center between the left and right uls, but we want to be centered + // relative to the whole page. The difference in width between the other uls is 74px, + // so we offset by that amount here. + margin-left: -74px + .joyride-tip-guide .button text-shadow: none // Default overrides - big menu - .top-bar-section ul li.ofn-logo > a - display: table-cell - vertical-align: middle - .top-bar-section .has-dropdown > a - padding-right: 15px !important - + padding-right: $topbar-height / 3 !important + i.ofn-i_022-cog font-size: 24px - line-height: 45px - + line-height: $topbar-height + .top-bar-section .has-dropdown > a:after display: none .top-bar-section ul li > a font-size: 0.75rem - height: 45px + height: $topbar-height opacity: 0.8 &:hover, &:focus, &:active opacity: 1 + @include transition(all 0.3s ease-in-out) + + .top-bar-section ul li.ofn-logo > a + display: table-cell + vertical-align: middle + opacity: 1 .nav-branded - color: $clr-brick-light-bright + color: $brand-colour span font-size: 13px .nav-primary - @include avenir + @include headingFont font-size: 0.875rem + font-weight: 300 + ul .nav-primary + text-transform: uppercase + ul.dropdown + border: 1px solid $smoke + border-top: none // Mobile Menu +.tab-bar + background-color: white + .off-canvas-wrap.move-right .tab-bar .menu-icon @include box-shadow(inset 0 0 6px 2px rgba(0,0,0,0.5)) @@ -55,29 +81,57 @@ nav -webkit-box-shadow: 0 0px 0 1px #666, 0 7px 0 1px #666, 0 14px 0 1px #666 box-shadow: 0 0px 0 1px #666, 0 7px 0 1px #666, 0 14px 0 1px #666 +.tab-bar .menu-icon span::after + box-shadow: 0 0 0 1px black, 0 7px 0 1px black, 0 14px 0 1px black + +.tab-bar .ofn-logo + padding: 9px 0 0 9px + +.left-off-canvas-menu + background-color: white + .off-canvas-wrap.move-right ul.off-canvas-list font-size: 0.875rem .li-menu - @include avenir + @include headingFont font-size: 1rem a - color: rgba(255, 255, 255, 0.9) + color: rgba(0, 0, 0, 0.9) + li a + color: rgba(0, 0, 0, 0.9) + &:hover + background-color: transparent + color: $brand-colour + @include transition(all 0.3s ease-in-out) .off-canvas-wrap.move-right ul.off-canvas-list i font-size: 1.5rem margin-right: 0.25rem -// Responsive + +// Responsive + +@media screen and (max-width: 1450px) + nav .top-bar-section + ul li a, .has-dropdown > a + padding: 0 $topbar-height / 8 !important + + ul.center + margin-left: -24px + @media screen and (min-width: 1025px) body.off-canvas // padding required to placehold for fixed menu bar - padding-top: 45px + padding-top: $topbar-height + + @media screen and (max-width: 1025px) - section.right + body.off-canvas + // padding required to placehold for fixed menu bar + padding-top: 0 + section.right .nav-branded padding: 0 1em - - \ No newline at end of file diff --git a/app/assets/stylesheets/darkswarm/mixins.sass b/app/assets/stylesheets/darkswarm/mixins.sass index 6925e84f72..4e779bd2a5 100644 --- a/app/assets/stylesheets/darkswarm/mixins.sass +++ b/app/assets/stylesheets/darkswarm/mixins.sass @@ -5,10 +5,46 @@ // Generic \\ +@mixin tiledPane + background-image: url("/assets/tile-wide.png") + background-color: $brand-colour + background-position: center center + @include paneWhiteText + @mixin panepadding padding-top: 100px padding-bottom: 100px +@mixin paneWhiteText + &, & * + color: white + +@mixin sidepaddingSm + padding-left: 10px + padding-right: 10px + @media all and (min-width: 768px) + padding-left: 20px + padding-right: 20px + @media all and (min-width: 1024px) + padding-left: 50px + padding-right: 50px + @media all and (min-width: 1200px) + padding-left: 100px + padding-right: 100px + +@mixin sidepaddingBg + padding-left: 20px + padding-right: 20px + @media all and (min-width: 768px) + padding-left: 40px + padding-right: 40px + @media all and (min-width: 1024px) + padding-left: 100px + padding-right: 100px + @media all and (min-width: 1200px) + padding-left: 200px + padding-right: 200px + @mixin disabled color: $disabled-bright @@ -53,9 +89,6 @@ // Typography \\ -@mixin avenir - font-family: "AvenirBla_IE", "AvenirBla" - @mixin textpress text-shadow: 0 -1px 1px #111111, 0 1px 2px #222222 @@ -108,7 +141,7 @@ color: $clr-turquoise-bright @mixin fullbg - background-position: center center + background-position: center center background-repeat: no-repeat -webkit-background-size: cover -moz-background-size: cover @@ -116,7 +149,7 @@ background-size: cover @mixin fullwidthbg - background-position: center top + background-position: center top background-repeat: no-repeat background-size: 100% auto @@ -137,4 +170,3 @@ // W3C filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='$gradient-clr1', endColorstr='$gradient-clr2',GradientType=0 ) // IE6-8 - diff --git a/app/assets/stylesheets/darkswarm/page_alert.css.sass b/app/assets/stylesheets/darkswarm/page_alert.css.sass new file mode 100644 index 0000000000..b7d6ea643e --- /dev/null +++ b/app/assets/stylesheets/darkswarm/page_alert.css.sass @@ -0,0 +1,58 @@ +@import branding +@import animations +@import "compass/css3/transition" + +// Basic style \\ +.page-alert + .alert-box + height: 55px + overflow: hidden + border: 1px solid rgba($dark-grey, 0.35) + border-left: none + border-right: none + background-color: #bbb + background-image: url("/assets/tile-wide.png") + background-position: center center + padding: 12px 0 8px 0 + margin: 0 + + h6 + @media all and (max-width: 480px) + font-size: 10px + line-height: 24px + + a.alert-cta + &, & * + @include csstrans + color: #333 + strong + letter-spacing: 0.5px + &:hover, &:active, &:focus + &, & * + text-decoration: none + color: white + + +// Show-hide animation \\ +.off-canvas-wrap .inner-wrap, .off-canvas-wrap .inner-wrap .fixed, nav.tab-bar + @include transition(all, 1000ms, ease-in-out) + + &.move-down + margin-top: 55px + @include transition(all, 1000ms, ease-in-out) + + +.off-canvas-wrap .inner-wrap .page-alert.fixed + top: -55px + z-index: 1 + // TODO: Compass to disable transition + -moz-transition: none + -webkit-transition: none + -o-transition: color 0 ease-in + transition: none + +.off-canvas-wrap.move-right .inner-wrap.move-down + .page-alert + top: -55px * 2 + .left-off-canvas-menu + top: -55px diff --git a/app/assets/stylesheets/darkswarm/producers.css.sass b/app/assets/stylesheets/darkswarm/producers.css.sass index 8f0e7edfcb..2055a45035 100644 --- a/app/assets/stylesheets/darkswarm/producers.css.sass +++ b/app/assets/stylesheets/darkswarm/producers.css.sass @@ -2,9 +2,13 @@ @import mixins .producers - @include fullwidthbg - background-image: url("/assets/producers/producers-pg-bg.jpg") + background-color: lighten($clr-turquoise, 68%) + background-image: url("/assets/producers.svg") + background-position: center 50px background-repeat: no-repeat + background-size: 922px 763px + @include sidepaddingSm + @include panepadding a color: $clr-turquoise &:hover, &:active, &:focus diff --git a/app/assets/stylesheets/darkswarm/registration.css.sass b/app/assets/stylesheets/darkswarm/registration.css.sass index f63eb30421..ba56701600 100644 --- a/app/assets/stylesheets/darkswarm/registration.css.sass +++ b/app/assets/stylesheets/darkswarm/registration.css.sass @@ -4,7 +4,6 @@ #registration-modal header text-align: center - // background-color: #efefef @media all and (max-width: 64em) text-align: left .container diff --git a/app/assets/stylesheets/darkswarm/shop.css.sass b/app/assets/stylesheets/darkswarm/shop.css.sass index 9d38398f36..5422c4463a 100644 --- a/app/assets/stylesheets/darkswarm/shop.css.sass +++ b/app/assets/stylesheets/darkswarm/shop.css.sass @@ -12,7 +12,6 @@ @import shop-popovers .darkswarm - products display: block padding-top: 20px diff --git a/app/assets/stylesheets/darkswarm/shopping-cart.css.sass b/app/assets/stylesheets/darkswarm/shopping-cart.css.sass index cb71913831..74cec02ce0 100644 --- a/app/assets/stylesheets/darkswarm/shopping-cart.css.sass +++ b/app/assets/stylesheets/darkswarm/shopping-cart.css.sass @@ -38,6 +38,7 @@ padding: 4px 12px color: #fff .buttons + margin-bottom: 0.1em .button height: auto top: 0px diff --git a/app/assets/stylesheets/darkswarm/signup.css.sass b/app/assets/stylesheets/darkswarm/signup.css.sass new file mode 100644 index 0000000000..cd6ebf458a --- /dev/null +++ b/app/assets/stylesheets/darkswarm/signup.css.sass @@ -0,0 +1,121 @@ +@import branding +@import mixins +@import typography +@import animations +@import variables + + +#producer-signup.pane, #shops-signup.pane + @include tiledPane + + h2 + margin-bottom: 2rem + font-size: 4.4rem + font-weight: 300 + +#producer-details.pane, #hub-details.pane, .groups-details.pane + background-color: lighten($ofn-grey, 44%) + + +#producer-case-studies, #shops-case-studies + padding-top: 100px + padding-bottom: 100px + background-color: $brand-colour + background-image: url("/assets/hubs-bg.jpg") + background-position: center center + -webkit-filter: brightness(1.1) + filter: brightness(1.1) + h2 + color: $brand-colour + font-size: 3rem + .case-study + background-color: rgba(255, 255, 255, 0.5) + padding: 1rem + margin-top: 2rem + text-align: center + .case-study-img + background-color: white + margin-bottom: 1rem + @media all and (min-width: 768px) + float: right + margin-left: 2rem + @media all and (min-width: 640px) + text-align: left + h4, a + color: $brand-colour + a + &, & * + @include csstrans + opacity: 1 + &:hover, &:focus, &:active + &, & * + opacity: 0.75 + + +// Signup tables \\ +table.signup-table + width: 100% + border: 0 + +table.signup-table.hubs-table, table.signup-table.producers-table + tr + td + background-color: white + border-bottom: 1px solid rgba($ofn-grey, 0.3) + td:nth-child(2) + background-color: lighten($ofn-grey, 46%) + td:nth-child(3) + background-color: lighten($ofn-grey, 41%) + td:last-child + &, & i + color: $brand-colour + border-bottom: 1px solid rgba($brand-colour, 0.3) + background-color: lighten($brand-colour, 48%) + thead + background-color: transparent + tr + td + border-bottom: 1px solid transparent + td:nth-child(1) + background-color: transparent + td:nth-child(2) + background: lighten($ofn-grey, 44%) + td:nth-child(3) + background: lighten($ofn-grey, 38%) + td:last-child + &, & * + color: white + background: $brand-colour + h5 + text-transform: uppercase + color: $ofn-grey + font-weight: 400 + font-size: 0.875rem + margin-bottom: 0.25em + + tfoot + background-color: transparent + tr + td + border-bottom: 1px solid transparent + td:nth-child(1) + background-color: transparent + td:nth-child(2) + background: lighten($ofn-grey, 44%) + td:nth-child(3) + background: lighten($ofn-grey, 38%) + td:last-child + &, & * + color: white + background: $brand-colour + h2 + .text-small + text-transform: uppercase + display: inline-block + font-weight: 400 + line-height: 1.5 + @include headingFont + +// Detail \\ +.enterprise-type-flowchart + float: right diff --git a/app/assets/stylesheets/darkswarm/style.css b/app/assets/stylesheets/darkswarm/style.css.scss old mode 100755 new mode 100644 similarity index 100% rename from app/assets/stylesheets/darkswarm/style.css rename to app/assets/stylesheets/darkswarm/style.css.scss diff --git a/app/assets/stylesheets/darkswarm/tabs.css.sass b/app/assets/stylesheets/darkswarm/tabs.css.sass index 0c911eb66a..12134a19e1 100644 --- a/app/assets/stylesheets/darkswarm/tabs.css.sass +++ b/app/assets/stylesheets/darkswarm/tabs.css.sass @@ -37,7 +37,7 @@ text-align: left a - @include avenir + @include headingFont background: transparent text-transform: uppercase line-height: 1 diff --git a/app/assets/stylesheets/darkswarm/typography.css.sass b/app/assets/stylesheets/darkswarm/typography.css.sass index f549420d83..97230d5b0a 100644 --- a/app/assets/stylesheets/darkswarm/typography.css.sass +++ b/app/assets/stylesheets/darkswarm/typography.css.sass @@ -1,30 +1,27 @@ @import branding -//@import mixins -@font-face - font-family: 'AvenirBla_IE' - src: url("/AveniBla.eot") format("opentype") +@mixin headingFont + font-family: 'Oswald', sans-serif -@font-face - font-family: 'AvenirBla' - src: url("/AvenirLTStd-Black.otf") format("opentype") +@mixin bodyFont + font-family: 'Roboto', Arial, sans-serif -@font-face - font-family: 'AvenirMed_IE' - src: url("/AveniMed.eot") format("opentype") +$headingFont: 'Oswald' +$bodyFont: 'Roboto' -@font-face - font-family: 'AvenirMed' - src: url("/AvenirLTStd-Medium.otf") format("opentype") - -$font-helvetica: "Helvetica Neue", "HelveticaNeue", "Helvetica", Helvetica, Arial, sans-serif - +body + @include bodyFont + font-weight: 400 a color: $clr-brick &:hover, &:focus, &:active text-decoration: none color: $clr-brick-bright +.text-vbig + font-size: 2rem + font-weight: 300 + .text-big font-size: 1.5rem font-weight: 300 @@ -35,17 +32,17 @@ small, .small .text-small font-size: 0.875rem margin-bottom: 0.5rem - font-family: $font-helvetica + font-family: $bodyFont &, & * font-size: 0.875rem .text-normal font-weight: 400 - font-family: $font-helvetica + font-family: $bodyFont .text-skinny font-weight: 300 - font-family: $font-helvetica + font-family: $bodyFont .word-wrap word-wrap: break-word @@ -69,11 +66,11 @@ small, .small .brick color: $clr-brick -@mixin avenir - font-family: "AvenirBla_IE", "AvenirBla" +.hr-light + border-color: rgba(#ddd, 0.25) -h1, h2, h3, h4, h5, h6, .avenir - @include avenir +h1, h2, h3, h4, h5, h6 + @include headingFont padding: 0px ul.bullet-list, ul.check-list @@ -108,8 +105,8 @@ ul.check-list .not-bold font-weight: normal -strong.avenir - font-weight: normal // Avenir is basically bold anyway +.footer-pad + padding-bottom: 100px // These selectors match the default Foundation selectors diff --git a/app/assets/stylesheets/darkswarm/ui.css.sass b/app/assets/stylesheets/darkswarm/ui.css.sass index d18adae1b4..5c049a2bba 100644 --- a/app/assets/stylesheets/darkswarm/ui.css.sass +++ b/app/assets/stylesheets/darkswarm/ui.css.sass @@ -1,18 +1,19 @@ @import foundation/components/buttons @import branding @import mixins +@import typography // Button class extensions .neutral-btn @include button @include border-radius(0.5em) - font-family: 'Open Sans', Calibri, Candara, Segoe, "Segoe UI", Optima, Arial, sans-serif + font-family: $bodyFont background-color: transparent border: 2px solid rgba(200, 200, 200, 1) color: #999 -.neutral-btn:hover, .neutral-btn:active, .neutral-btn:focus +.neutral-btn:hover, .neutral-btn:active, .neutral-btn:focus background-color: rgba(200, 200, 200, 0.2) border: 2px solid rgba(200, 200, 200, 0.8) @@ -20,7 +21,7 @@ border-color: #000 color: #000 -.neutral-btn.dark:hover, .neutral-btn.dark:active, .neutral-btn.dark:focus +.neutral-btn.dark:hover, .neutral-btn.dark:active, .neutral-btn.dark:focus background-color: rgba(0, 0, 0, 0.1) border: 2px solid rgba(0, 0, 0, 0.8) text-shadow: 0 1px 0 #fff @@ -29,18 +30,18 @@ border-color: #fff color: #fff -.neutral-btn.light:hover, .neutral-btn.light:active, .neutral-btn.light:focus +.neutral-btn.light:hover, .neutral-btn.light:active, .neutral-btn.light:focus background-color: rgba(255, 255, 255, 0.2) border: 2px solid rgba(255, 255, 255, 0.8) - text-shadow: 0 1px 0 $clr-brick + text-shadow: 0 1px 0 rgba(0,0,0,0.2) .neutral-btn.turquoise border-color: $clr-turquoise color: $clr-turquoise -.neutral-btn.turquoise:hover, .neutral-btn.turquoise:active, .neutral-btn.turquoise:focus +.neutral-btn.turquoise:hover, .neutral-btn.turquoise:active, .neutral-btn.turquoise:focus background-color: rgba(0, 0, 0, 0.1) - text-shadow: 0 1px 0 #fff + // text-shadow: 0 1px 0 #fff // Rewrite foundation's .primary button style @@ -49,7 +50,7 @@ outline: none // Turn off blue highlight on chrome .button.primary, button.primary - font-family: 'Open Sans', Calibri, Candara, Segoe, "Segoe UI", Optima, Arial, sans-serif + font-family: $bodyFont background: $clr-brick color: white @@ -58,7 +59,7 @@ text-shadow: 0 1px 0 $clr-brick button.success, .button.success - background: #0096ad + background: #0096ad .button.success:hover, .button.success:active, .button.success:focus, button.success:hover, button.success:active, button.success:focus background: #14b6cc @@ -77,11 +78,20 @@ button.success, .button.success label margin: 0 0.2rem float: right - -// Responsive +// Transparent button +.button.transparent + @include headingFont + font-size: 20px + text-transform: uppercase + background: rgba(0, 0, 0, 0.3) + border: 3px solid #fff + text-shadow: none + &:hover + background: rgba(0, 0, 0, 0.6) + color: #fff + +// Responsive @media screen and (min-width: 768px) [role="main"] padding: 0 - - diff --git a/app/assets/stylesheets/darkswarm/variables.css.sass b/app/assets/stylesheets/darkswarm/variables.css.sass index 1020f7cbaa..ea87d11fc4 100644 --- a/app/assets/stylesheets/darkswarm/variables.css.sass +++ b/app/assets/stylesheets/darkswarm/variables.css.sass @@ -1 +1,32 @@ -// necessary; user to overwrite Foundation variables css \ No newline at end of file +@import "foundation/functions" +@import "foundation/components/global" + +// Brand guide colours: +// International: #81c26e +// Australia: #f35746 +// Africa: #f35e32 +// South Africa: #f9a72b +// Norway: #4b83cc +// Scandanavia: #0c8bbc +// UK: #e6373f + +$brand-colour: #f27052 + + +// Topbar +$topbar-height: rem-calc(75) +$topbar-link-padding: $topbar-height / 3 + +$topbar-bg: $white +$topbar-bg-color: $topbar-bg + +$topbar-link-color: $black +$topbar-link-color-hover: $brand-colour +$topbar-link-color-active: $black +$topbar-link-color-active-hover: $white +$topbar-link-bg-hover: $white + +$topbar-dropdown-link-color: $black +$topbar-dropdown-bg: $white +$topbar-dropdown-link-bg: $white +$topbar-dropdown-link-bg-hover: $white diff --git a/app/controllers/admin/account_controller.rb b/app/controllers/admin/account_controller.rb new file mode 100644 index 0000000000..a752b2cfdd --- /dev/null +++ b/app/controllers/admin/account_controller.rb @@ -0,0 +1,9 @@ +class Admin::AccountController < Spree::Admin::BaseController + + def show + @invoices = spree_current_user.account_invoices + # @enterprises = Enterprise.where(id: BillablePeriod.where(owner_id: spree_current_user).map(&:enterprise_id)) + # .group_by('enterprise.id').joins(:billable_periods) + # .select('SUM(billable_periods.turnover) AS turnover').order('turnover DESC') + end +end diff --git a/app/controllers/admin/accounts_and_billing_settings_controller.rb b/app/controllers/admin/accounts_and_billing_settings_controller.rb new file mode 100644 index 0000000000..0f3b986cef --- /dev/null +++ b/app/controllers/admin/accounts_and_billing_settings_controller.rb @@ -0,0 +1,74 @@ +require 'open_food_network/accounts_and_billing_settings_validator' + +class Admin::AccountsAndBillingSettingsController < Spree::Admin::BaseController + before_filter :load_distributors, only: [:edit, :update, :start_job] + before_filter :load_jobs, only: [:edit, :update, :start_job] + before_filter :load_settings, only: [:edit, :update, :start_job] + before_filter :require_valid_settings, only: [:update, :start_job] + before_filter :require_known_job, only: [:start_job] + + def update + Spree::Config.set(params[:settings]) + flash[:success] = t(:successfully_updated, :resource => t(:billing_and_account_settings)) + redirect_to_edit + end + + def start_job + if @update_account_invoices_job || @finalize_account_invoices_job + flash[:error] = "A task is already running, please wait until it has finished" + else + new_job = "#{params[:job][:name]}".camelize.constantize.new + Delayed::Job.enqueue new_job + flash[:success] = "Task Queued" + end + + redirect_to_edit + end + + def show_methods + @enterprise = Enterprise.find_by_id(params[:enterprise_id]) + @shipping_methods = @enterprise.shipping_methods + @payment_methods = @enterprise.payment_methods + render partial: 'method_settings' + end + + private + + def redirect_to_edit + redirect_to main_app.edit_admin_accounts_and_billing_settings_path + end + + def require_valid_settings + render :edit unless @settings.valid? + end + + def known_jobs + ['update_account_invoices', 'finalize_account_invoices'] + end + + def require_known_job + unless known_jobs.include?(params[:job][:name]) + flash[:error] = "Unknown Task: #{params[:job][:name].to_s}" + redirect_to_edit + end + end + + def load_settings + @settings = OpenFoodNetwork::AccountsAndBillingSettingsValidator.new(params[:settings] || { + accounts_distributor_id: Spree::Config[:accounts_distributor_id], + default_accounts_payment_method_id: Spree::Config[:default_accounts_payment_method_id], + default_accounts_shipping_method_id: Spree::Config[:default_accounts_shipping_method_id], + auto_update_invoices: Spree::Config[:auto_update_invoices], + auto_finalize_invoices: Spree::Config[:auto_finalize_invoices] + }) + end + + def load_distributors + @distributors = Enterprise.is_distributor.select([:id, :name]) + end + + def load_jobs + @update_account_invoices_job = Delayed::Job.where("handler LIKE (?)", "%UpdateAccountInvoices%").last + @finalize_account_invoices_job = Delayed::Job.where("handler LIKE (?)", "%FinalizeAccountInvoices%").last + end +end diff --git a/app/controllers/admin/contents_controller.rb b/app/controllers/admin/contents_controller.rb new file mode 100644 index 0000000000..87c86997d9 --- /dev/null +++ b/app/controllers/admin/contents_controller.rb @@ -0,0 +1,29 @@ +module Admin + class ContentsController < Spree::Admin::BaseController + def edit + @preference_sections = [{name: 'Header', preferences: [:logo, :logo_mobile, :logo_mobile_svg]}, + {name: 'Home page', preferences: [:home_hero, :home_show_stats]}, + {name: 'Producer signup page', preferences: [:producer_signup_pricing_table_html, :producer_signup_case_studies_html, :producer_signup_detail_html]}, + {name: 'Hub signup page', preferences: [:hub_signup_pricing_table_html, :hub_signup_case_studies_html, :hub_signup_detail_html]}, + {name: 'Group signup page', preferences: [:group_signup_pricing_table_html, :group_signup_case_studies_html, :group_signup_detail_html]}, + {name: 'Footer', preferences: [:footer_logo, + :footer_facebook_url, :footer_twitter_url, :footer_instagram_url, :footer_linkedin_url, :footer_googleplus_url, :footer_pinterest_url, + :footer_email, :footer_links_md, :footer_about_url, :footer_tos_url]}] + end + + def update + params.each do |name, value| + if ContentConfig.has_preference?(name) || ContentConfig.has_attachment?(name) + ContentConfig.send("#{name}=", value) + end + end + + # Save any uploaded images + ContentConfig.save + + flash[:success] = t(:successfully_updated, :resource => "Your content") + + redirect_to main_app.edit_admin_content_path + end + end +end diff --git a/app/controllers/admin/customers_controller.rb b/app/controllers/admin/customers_controller.rb index 27b5b1380d..b1ceb88c2f 100644 --- a/app/controllers/admin/customers_controller.rb +++ b/app/controllers/admin/customers_controller.rb @@ -17,7 +17,7 @@ module Admin private def collection - return Customer.where("1=0") if html_request? || params[:enterprise_id].nil? + return Customer.where("1=0") unless json_request? && params[:enterprise_id].present? enterprise = Enterprise.managed_by(spree_current_user).find_by_id(params[:enterprise_id]) Customer.of(enterprise) end diff --git a/app/controllers/admin/enterprises_controller.rb b/app/controllers/admin/enterprises_controller.rb index e50779c3ff..5ccc9b9c92 100644 --- a/app/controllers/admin/enterprises_controller.rb +++ b/app/controllers/admin/enterprises_controller.rb @@ -3,7 +3,7 @@ require 'open_food_network/referer_parser' module Admin class EnterprisesController < ResourceController before_filter :load_enterprise_set, :only => :index - before_filter :load_countries, :except => [:index, :set_sells, :check_permalink] + before_filter :load_countries, :except => [:index, :register, :check_permalink] before_filter :load_methods_and_fees, :only => [:new, :edit, :update, :create] before_filter :load_groups, :only => [:new, :edit, :update, :create] before_filter :load_taxons, :only => [:new, :edit, :update, :create] @@ -18,34 +18,61 @@ module Admin before_filter :load_properties, only: [:edit, :update] before_filter :setup_property, only: [:edit] - helper 'spree/products' include ActionView::Helpers::TextHelper include OrderCyclesHelper - def set_sells - enterprise = Enterprise.find_by_permalink(params[:id]) || Enterprise.find(params[:id]) - attributes = { sells: params[:sells] } - attributes[:producer_profile_only] = params[:sells] == "none" && !!params[:producer_profile_only] - attributes[:shop_trial_start_date] = Time.now if params[:sells] == "own" + def index + respond_to do |format| + format.html + format.json do + render json: @collection, each_serializer: Api::Admin::IndexEnterpriseSerializer, spree_current_user: spree_current_user + end + end + end - if %w(none own).include?(params[:sells]) - if params[:sells] == 'own' && enterprise.shop_trial_start_date - expiry = enterprise.shop_trial_start_date + Enterprise::SHOP_TRIAL_LENGTH.days - if Time.now > expiry - flash[:error] = "Sorry, but you've already had a trial. Expired on: #{expiry.strftime('%Y-%m-%d')}" - else - attributes.delete :shop_trial_start_date - enterprise.update_attributes(attributes) - flash[:notice] = "Welcome back! Your trial expires on: #{expiry.strftime('%Y-%m-%d')}" - end - elsif enterprise.update_attributes(attributes) - flash[:success] = "Congratulations! Registration for #{enterprise.name} is complete!" + def welcome + render layout: "spree/layouts/bare_admin" + end + + + def update + invoke_callbacks(:update, :before) + if @object.update_attributes(params[object_name]) + invoke_callbacks(:update, :after) + flash[:success] = flash_message_for(@object, :successfully_updated) + respond_with(@object) do |format| + format.html { redirect_to location_after_save } + format.js { render :layout => false } + format.json { render json: @object, serializer: Api::Admin::IndexEnterpriseSerializer, spree_current_user: spree_current_user } end else - flash[:error] = "Unauthorised" + invoke_callbacks(:update, :fails) + respond_with(@object) do |format| + format.json { render json: { errors: @object.errors.messages }, status: :unprocessable_entity } + end + end + end + + def register + if params[:sells] == 'unspecified' + flash[:error] = "Please select a package" + return render :welcome, layout: "spree/layouts/bare_admin" + end + + attributes = { sells: params[:sells], visible: true } + + if ['own', 'any'].include? params[:sells] + attributes[:shop_trial_start_date] = @enterprise.shop_trial_start_date || Time.now + end + + if @enterprise.update_attributes(attributes) + flash[:success] = "Congratulations! Registration for #{@enterprise.name} is complete!" + redirect_to admin_path + else + flash[:error] = "Could not complete registration for #{@enterprise.name}" + render :welcome, layout: "spree/layouts/bare_admin" end - redirect_to admin_path end def bulk_update @@ -98,7 +125,7 @@ module Admin private def load_enterprise_set - @enterprise_set = EnterpriseSet.new collection + @enterprise_set = EnterpriseSet.new(collection) if spree_current_user.admin? end def load_countries @@ -112,6 +139,16 @@ module Admin coordinator = Enterprise.find_by_id(params[:coordinator_id]) if params[:coordinator_id] order_cycle = OrderCycle.new(coordinator: coordinator) if order_cycle.nil? && coordinator.present? return OpenFoodNetwork::OrderCyclePermissions.new(spree_current_user, order_cycle).visible_enterprises + when :index + if spree_current_user.admin? + OpenFoodNetwork::Permissions.new(spree_current_user). + editable_enterprises. + order('is_primary_producer ASC, name') + elsif json_request? + OpenFoodNetwork::Permissions.new(spree_current_user).editable_enterprises + else + Enterprise.where("1=0") unless json_request? + end else # TODO was ordered with is_distributor DESC as well, not sure why or how we want to sort this now OpenFoodNetwork::Permissions.new(spree_current_user). @@ -141,13 +178,15 @@ module Admin def check_can_change_bulk_sells unless spree_current_user.admin? params[:enterprise_set][:collection_attributes].each do |i, enterprise_params| - enterprise_params.delete :sells + enterprise_params.delete :sells unless spree_current_user == Enterprise.find_by_id(enterprise_params[:id]).owner end end end def check_can_change_sells - params[:enterprise].delete :sells unless spree_current_user.admin? + unless spree_current_user.admin? || spree_current_user == @enterprise.owner + params[:enterprise].delete :sells + end end def override_owner @@ -202,7 +241,7 @@ module Admin # Overriding method on Spree's resource controller def location_after_save referer_path = OpenFoodNetwork::RefererParser::path(request.referer) - refered_from_edit = referer_path == main_app.edit_admin_enterprise_path(@enterprise) + refered_from_edit = referer_path =~ /\/edit$/ if params[:enterprise].key?(:producer_properties_attributes) && !refered_from_edit main_app.admin_enterprises_path else diff --git a/app/controllers/admin/order_cycles_controller.rb b/app/controllers/admin/order_cycles_controller.rb index 8cc0aef9b4..9e1155d6b7 100644 --- a/app/controllers/admin/order_cycles_controller.rb +++ b/app/controllers/admin/order_cycles_controller.rb @@ -64,8 +64,8 @@ module Admin end def bulk_update - @order_cycle_set = OrderCycleSet.new(params[:order_cycle_set]) - if @order_cycle_set.save + @order_cycle_set = params[:order_cycle_set] && OrderCycleSet.new(params[:order_cycle_set]) + if @order_cycle_set.andand.save redirect_to main_app.admin_order_cycles_path, :notice => 'Order cycles have been updated.' else render :index @@ -138,10 +138,12 @@ module Admin end def remove_unauthorized_bulk_attrs - params[:order_cycle_set][:collection_attributes].each do |i, hash| - order_cycle = OrderCycle.find(hash[:id]) - unless Enterprise.managed_by(spree_current_user).include?(order_cycle.andand.coordinator) - params[:order_cycle_set][:collection_attributes].delete i + if params.key? :order_cycle_set + params[:order_cycle_set][:collection_attributes].each do |i, hash| + order_cycle = OrderCycle.find(hash[:id]) + unless Enterprise.managed_by(spree_current_user).include?(order_cycle.andand.coordinator) + params[:order_cycle_set][:collection_attributes].delete i + end end end end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index aaa7d0bb06..7dfd404b69 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -4,6 +4,7 @@ class ApplicationController < ActionController::Base protect_from_forgery include EnterprisesHelper + helper CssSplitter::ApplicationHelper def redirect_to(options = {}, response_status = {}) ::Rails.logger.error("Redirected by #{caller(1).first rescue "unknown"}") diff --git a/app/controllers/checkout_controller.rb b/app/controllers/checkout_controller.rb index 11be28ef04..b23a57ebde 100644 --- a/app/controllers/checkout_controller.rb +++ b/app/controllers/checkout_controller.rb @@ -23,7 +23,7 @@ class CheckoutController < Spree::CheckoutController return if redirect_to_paypal_express_form_if_needed end - if @order.next + if advance_order_state(@order) state_callback(:after) else if @order.errors.present? @@ -83,6 +83,16 @@ class CheckoutController < Spree::CheckoutController params[:order] end + # Perform order.next, guarding against StaleObjectErrors + def advance_order_state(order) + tries ||= 3 + order.next + + rescue ActiveRecord::StaleObjectError + retry unless (tries -= 1).zero? + false + end + def update_failed clear_ship_address diff --git a/app/controllers/enterprise_confirmations_controller.rb b/app/controllers/enterprise_confirmations_controller.rb index d275260599..0375a04d49 100644 --- a/app/controllers/enterprise_confirmations_controller.rb +++ b/app/controllers/enterprise_confirmations_controller.rb @@ -40,7 +40,7 @@ class EnterpriseConfirmationsController < DeviseController def new_user_reset_path(resource) password = Devise.friendly_token.first(8) user = Spree::User.create(email: resource.email, password: password, password_confirmation: password) - user.send_reset_password_instructions + user.send_reset_password_instructions_without_delay resource.users << user spree.edit_spree_user_password_path(user, :reset_password_token => user.reset_password_token, return_to: spree.admin_path) end diff --git a/app/controllers/enterprises_controller.rb b/app/controllers/enterprises_controller.rb index 75ad5c475b..8a875c76bf 100644 --- a/app/controllers/enterprises_controller.rb +++ b/app/controllers/enterprises_controller.rb @@ -9,55 +9,6 @@ class EnterprisesController < BaseController respond_to :js, only: :permalink_checker - def index - @enterprises = Enterprise.all - end - - def suppliers - @suppliers = Enterprise.is_primary_producer - end - - def distributors - @distributors = Enterprise.is_distributor - - respond_to do |format| - format.js do - @distributor_details = Hash[@distributors.map { |d| [d.id, render_to_string(:partial => 'enterprises/distributor_details', :locals => {:distributor => d})] }] - end - format.html do - @distributors - end - end - end - - def show - @enterprise = Enterprise.find_by_permalink(params[:id]) || Enterprise.find(params[:id]) - - # User can view this page if they've already chosen their distributor, or if this page - # is for a supplier, they may use it to select a distributor that sells this supplier's - # products. - unless current_distributor || @enterprise.is_primary_producer - redirect_to spree.root_path and return - end - - - options = {:enterprise_id => params[:id]} - options.merge(params.reject { |k,v| k == :id }) - - @products = [] - - if @enterprise.is_primary_producer - @distributors = Enterprise.distributing_any_product_of(@enterprise.supplied_products).by_name.all - end - - if current_order_cycle - @searcher = Spree::Config.searcher_class.new(options) - @products = @searcher.retrieve_products - - order_cycle_products = current_order_cycle.products_distributed_by(current_distributor) - @products = @products & order_cycle_products - end - end def check_permalink return render text: params[:permalink], status: 409 if Enterprise.find_by_permalink params[:permalink] diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb index 3bb7a68538..07ba097f85 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -2,8 +2,11 @@ class HomeController < BaseController layout 'darkswarm' def index - end - - def about_us + if ContentConfig.home_show_stats + @num_distributors = Enterprise.is_distributor.activated.visible.count + @num_producers = Enterprise.is_primary_producer.activated.visible.count + @num_users = Spree::User.joins(:orders).merge(Spree::Order.complete).count('DISTINCT spree_users.*') + @num_orders = Spree::Order.complete.count + end end end diff --git a/app/controllers/shop_controller.rb b/app/controllers/shop_controller.rb index 0655624a0b..ef78605b41 100644 --- a/app/controllers/shop_controller.rb +++ b/app/controllers/shop_controller.rb @@ -12,13 +12,17 @@ class ShopController < BaseController def products if @products = products_for_shop + enterprise_fee_calculator = OpenFoodNetwork::EnterpriseFeeCalculator.new current_distributor, current_order_cycle + render status: 200, json: ActiveModel::ArraySerializer.new(@products, each_serializer: Api::ProductSerializer, current_order_cycle: current_order_cycle, current_distributor: current_distributor, variants: variants_for_shop_by_id, - master_variants: master_variants_for_shop_by_id).to_json + master_variants: master_variants_for_shop_by_id, + enterprise_fee_calculator: enterprise_fee_calculator, + ).to_json else render json: "", status: 404 @@ -42,10 +46,12 @@ class ShopController < BaseController def products_for_shop if current_order_cycle + scoper = OpenFoodNetwork::ScopeProductToHub.new(current_distributor) + current_order_cycle. valid_products_distributed_by(current_distributor). order(taxon_order). - each { |p| p.scope_to_hub current_distributor }. + each { |p| scoper.scope(p) }. select { |p| !p.deleted? && p.has_stock_for_distribution?(current_order_cycle, current_distributor) } end end @@ -65,9 +71,10 @@ class ShopController < BaseController # We use the in_stock? method here instead of the in_stock scope because we need to # look up the stock as overridden by VariantOverrides, and the scope method is not affected # by them. + scoper = OpenFoodNetwork::ScopeVariantToHub.new(current_distributor) Spree::Variant. for_distribution(current_order_cycle, current_distributor). - each { |v| v.scope_to_hub current_distributor }. + each { |v| scoper.scope(v) }. select(&:in_stock?) end diff --git a/app/controllers/shops_controller.rb b/app/controllers/shops_controller.rb new file mode 100644 index 0000000000..3305811e07 --- /dev/null +++ b/app/controllers/shops_controller.rb @@ -0,0 +1,6 @@ +class ShopsController < BaseController + layout 'darkswarm' + + def index + end +end diff --git a/app/controllers/spree/admin/base_controller_decorator.rb b/app/controllers/spree/admin/base_controller_decorator.rb index 3fa6a5c5e1..4579e905e7 100644 --- a/app/controllers/spree/admin/base_controller_decorator.rb +++ b/app/controllers/spree/admin/base_controller_decorator.rb @@ -62,4 +62,8 @@ Spree::Admin::BaseController.class_eval do def html_request? request.format.html? end + + def json_request? + request.format.json? + end end diff --git a/app/controllers/spree/admin/overview_controller_decorator.rb b/app/controllers/spree/admin/overview_controller_decorator.rb index 6eea9c6ea4..ffd2569c1f 100644 --- a/app/controllers/spree/admin/overview_controller_decorator.rb +++ b/app/controllers/spree/admin/overview_controller_decorator.rb @@ -5,16 +5,22 @@ Spree::Admin::OverviewController.class_eval do @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 @enterprise.sells == "unspecified" - render "welcome", layout: "spree/layouts/bare_admin" + if outside_referral && unspecified.any? + redirect_to main_app.welcome_admin_enterprise_path(@enterprise) else render "single_enterprise_dashboard" end else - render "multi_enterprise_dashboard" + if outside_referral && unspecified.any? + redirect_to main_app.admin_enterprises_path + else + render "multi_enterprise_dashboard" + end end end end - diff --git a/app/controllers/spree/admin/reports_controller_decorator.rb b/app/controllers/spree/admin/reports_controller_decorator.rb index 81975bb250..50762a97e1 100644 --- a/app/controllers/spree/admin/reports_controller_decorator.rb +++ b/app/controllers/spree/admin/reports_controller_decorator.rb @@ -1,13 +1,18 @@ require 'csv' require 'open_food_network/order_and_distributor_report' require 'open_food_network/products_and_inventory_report' +require 'open_food_network/lettuce_share_report' require 'open_food_network/group_buy_report' require 'open_food_network/order_grouper' require 'open_food_network/customers_report' require 'open_food_network/users_and_enterprises_report' require 'open_food_network/order_cycle_management_report' +require 'open_food_network/packing_report' require 'open_food_network/sales_tax_report' require 'open_food_network/xero_invoices_report' +require 'open_food_network/bulk_coop_report' +require 'open_food_network/payments_report' +require 'open_food_network/orders_and_fulfillments_report' Spree::Admin::ReportsController.class_eval do @@ -22,7 +27,8 @@ Spree::Admin::ReportsController.class_eval do ], products_and_inventory: [ ['All products', :all_products], - ['Inventory (on hand)', :inventory] + ['Inventory (on hand)', :inventory], + ['LettuceShare', :lettuce_share] ], customers: [ ["Mailing List", :mailing_list], @@ -31,11 +37,15 @@ Spree::Admin::ReportsController.class_eval do order_cycle_management: [ ["Payment Methods Report", :payment_methods], ["Delivery Report", :delivery] + ], + packing: [ + ["Pack By Customer", :pack_by_customer], + ["Pack By Supplier", :pack_by_supplier] ] } # Fetches user's distributors, suppliers and order_cycles - before_filter :load_data, only: [:customers, :products_and_inventory, :order_cycle_management] + before_filter :load_data, only: [:customers, :products_and_inventory, :order_cycle_management, :packing] # Render a partial for orders and fulfillment description respond_override :index => { :html => { :success => lambda { @@ -47,7 +57,9 @@ Spree::Admin::ReportsController.class_eval do 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 +} } } # Overide spree reports list. @@ -75,19 +87,33 @@ Spree::Admin::ReportsController.class_eval do render_report(@report.header, @report.table, params[:csv], "order_cycle_management_#{timestamp}.csv") end + def packing + # -- Prepare date parameters + prepare_date_params params + + # -- Prepare form options + my_distributors = Enterprise.is_distributor.managed_by(spree_current_user) + my_suppliers = Enterprise.is_primary_producer.managed_by(spree_current_user) + + # My distributors and any distributors distributing products I supply + @distributors = my_distributors | Enterprise.with_distributed_products_outer.merge(Spree::Product.in_any_supplier(my_suppliers)) + # My suppliers and any suppliers supplying products I distribute + @suppliers = my_suppliers | my_distributors.map { |d| Spree::Product.in_distributor(d) }.flatten.map(&:supplier).uniq + @order_cycles = OrderCycle.active_or_complete.accessible_by(spree_current_user).order('orders_close_at DESC') + @report_types = REPORT_TYPES[:packing] + @report_type = params[:report_type] + + # -- Build Report with Order Grouper + @report = OpenFoodNetwork::PackingReport.new spree_current_user, params + order_grouper = OpenFoodNetwork::OrderGrouper.new @report.rules, @report.columns + @table = order_grouper.table(@report.table_items) + csv_file_name = "#{params[:report_type]}_#{timestamp}.csv" + + render_report(@report.header, @table, params[:csv], "packing_#{timestamp}.csv") + end + def orders_and_distributors - 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]).beginning_of_day rescue Time.zone.now.beginning_of_month - end - - if params[:q] && !params[:q][:completed_at_lt].blank? - params[:q][:completed_at_lt] = Time.zone.parse(params[:q][:completed_at_lt]).end_of_day rescue "" - end - params[:q][:meta_sort] ||= "completed_at.desc" + prepare_date_params params @search = Spree::Order.complete.not_state(:canceled).managed_by(spree_current_user).search(params[:q]) orders = @search.result @@ -105,18 +131,7 @@ Spree::Admin::ReportsController.class_eval do end def sales_tax - 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]).beginning_of_day rescue Time.zone.now.beginning_of_month - end - - if params[:q] && !params[:q][:completed_at_lt].blank? - params[:q][:completed_at_lt] = Time.zone.parse(params[:q][:completed_at_lt]).end_of_day rescue "" - end - params[:q][:meta_sort] ||= "completed_at.desc" + prepare_date_params params @search = Spree::Order.complete.not_state(:canceled).managed_by(spree_current_user).search(params[:q]) orders = @search.result @@ -135,300 +150,47 @@ Spree::Admin::ReportsController.class_eval do end def bulk_coop - 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]).beginning_of_day rescue Time.zone.now.beginning_of_month - end - - if params[:q] && !params[:q][:completed_at_lt].blank? - params[:q][:completed_at_lt] = Time.zone.parse(params[:q][:completed_at_lt]).end_of_day rescue "" - end - params[:q][:meta_sort] ||= "completed_at.desc" - - @search = Spree::Order.complete.not_state(:canceled).managed_by(spree_current_user).search(params[:q]) - - orders = @search.result - @line_items = orders.map { |o| o.line_items.managed_by(spree_current_user) }.flatten + # -- Prepare date parameters + prepare_date_params params + # -- Prepare form options @distributors = Enterprise.is_distributor.managed_by(spree_current_user) @report_type = params[:report_type] - case params[:report_type] - when "bulk_coop_supplier_report" + # -- Build Report with Order Grouper + @report = OpenFoodNetwork::BulkCoopReport.new spree_current_user, params + 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" - header = ["Supplier", "Product", "Unit Size", "Variant", "Weight", "Sum Total", "Sum Max Total", "Units Required", "Remainder"] - - columns = [ proc { |lis| lis.first.variant.product.supplier.name }, - proc { |lis| lis.first.variant.product.name }, - proc { |lis| lis.first.variant.product.group_buy ? (lis.first.variant.product.group_buy_unit_size || 0.0) : "" }, - proc { |lis| lis.first.variant.full_name }, - proc { |lis| lis.first.variant.weight || 0 }, - proc { |lis| lis.sum { |li| li.quantity } }, - proc { |lis| lis.sum { |li| li.max_quantity || 0 } }, - proc { |lis| "" }, - proc { |lis| "" } ] - - rules = [ { group_by: proc { |li| li.variant.product.supplier }, - sort_by: proc { |supplier| supplier.name } }, - { group_by: proc { |li| li.variant.product }, - sort_by: proc { |product| product.name }, - summary_columns: [ proc { |lis| lis.first.variant.product.supplier.name }, - proc { |lis| lis.first.variant.product.name }, - proc { |lis| lis.first.variant.product.group_buy ? (lis.first.variant.product.group_buy_unit_size || 0.0) : "" }, - proc { |lis| "" }, - proc { |lis| "" }, - proc { |lis| lis.sum { |li| (li.quantity || 0) * (li.variant.weight || 0) } }, - proc { |lis| lis.sum { |li| (li.max_quantity || 0) * (li.variant.weight || 0) } }, - proc { |lis| ( (lis.first.variant.product.group_buy_unit_size || 0).zero? ? 0 : ( lis.sum { |li| ( [li.max_quantity || 0, li.quantity || 0].max ) * (li.variant.weight || 0) } / lis.first.variant.product.group_buy_unit_size ) ).floor }, - proc { |lis| lis.sum { |li| ( [li.max_quantity || 0, li.quantity || 0].max) * (li.variant.weight || 0) } - ( ( (lis.first.variant.product.group_buy_unit_size || 0).zero? ? 0 : ( lis.sum { |li| ( [li.max_quantity || 0, li.quantity || 0].max) * (li.variant.weight || 0) } / lis.first.variant.product.group_buy_unit_size ) ).floor * (lis.first.variant.product.group_buy_unit_size || 0) ) } ] }, - { group_by: proc { |li| li.variant }, - sort_by: proc { |variant| variant.full_name } } ] - - when "bulk_coop_allocation" - - header = ["Customer", "Product", "Unit Size", "Variant", "Weight", "Sum Total", "Sum Max Total", "Total Allocated", "Remainder"] - - columns = [ proc { |lis| lis.first.order.bill_address.firstname + " " + lis.first.order.bill_address.lastname }, - proc { |lis| lis.first.variant.product.name }, - proc { |lis| lis.first.variant.product.group_buy ? (lis.first.variant.product.group_buy_unit_size || 0.0) : "" }, - proc { |lis| lis.first.variant.full_name }, - proc { |lis| lis.first.variant.weight || 0 }, - proc { |lis| lis.sum { |li| li.quantity } }, - proc { |lis| lis.sum { |li| li.max_quantity || 0 } }, - proc { |lis| "" }, - proc { |lis| "" } ] - - rules = [ { group_by: proc { |li| li.variant.product }, - sort_by: proc { |product| product.name }, - summary_columns: [ proc { |lis| "TOTAL" }, - proc { |lis| lis.first.variant.product.name }, - proc { |lis| lis.first.variant.product.group_buy ? (lis.first.variant.product.group_buy_unit_size || 0.0) : "" }, - proc { |lis| "" }, - proc { |lis| "" }, - proc { |lis| lis.sum { |li| li.quantity * (li.variant.weight || 0) } }, - proc { |lis| lis.sum { |li| (li.max_quantity || 0) * (li.variant.weight || 0) } }, - proc { |lis| ( (lis.first.variant.product.group_buy_unit_size || 0).zero? ? 0 : ( lis.sum { |li| ( [li.max_quantity || 0, li.quantity || 0].max ) * (li.variant.weight || 0) } / lis.first.variant.product.group_buy_unit_size ) ).floor * (lis.first.variant.product.group_buy_unit_size || 0) }, - proc { |lis| lis.sum { |li| ( [li.max_quantity || 0, li.quantity || 0].max ) * (li.variant.weight || 0) } - ( ( (lis.first.variant.product.group_buy_unit_size || 0).zero? ? 0 : ( lis.sum { |li| ( [li.max_quantity || 0, li.quantity || 0].max ) * (li.variant.weight || 0) } / lis.first.variant.product.group_buy_unit_size ) ).floor * (lis.first.variant.product.group_buy_unit_size || 0) ) } ] }, - { group_by: proc { |li| li.variant }, - sort_by: proc { |variant| variant.full_name } }, - { group_by: proc { |li| li.order }, - sort_by: proc { |order| order.to_s } } ] - - when "bulk_coop_packing_sheets" - - header = ["Customer", "Product", "Variant", "Sum Total"] - - columns = [ proc { |lis| lis.first.order.bill_address.firstname + " " + lis.first.order.bill_address.lastname }, - proc { |lis| lis.first.variant.product.name }, - proc { |lis| lis.first.variant.full_name }, - proc { |lis| lis.sum { |li| li.quantity } } ] - - rules = [ { group_by: proc { |li| li.variant.product }, - sort_by: proc { |product| product.name } }, - { group_by: proc { |li| li.variant }, - sort_by: proc { |variant| variant.full_name } }, - { group_by: proc { |li| li.order }, - sort_by: proc { |order| order.to_s } } ] - - when "bulk_coop_customer_payments" - - header = ["Customer", "Date of Order", "Total Cost", "Amount Owing", "Amount Paid"] - - columns = [ proc { |lis| lis.first.order.bill_address.firstname + " " + lis.first.order.bill_address.lastname }, - proc { |lis| lis.first.order.completed_at.to_s }, - proc { |lis| lis.map { |li| li.order }.uniq.sum { |o| o.total } }, - proc { |lis| lis.map { |li| li.order }.uniq.sum { |o| o.outstanding_balance } }, - proc { |lis| lis.map { |li| li.order }.uniq.sum { |o| o.payment_total } } ] - - rules = [ { group_by: proc { |li| li.order }, - sort_by: proc { |order| order.completed_at } } ] - - else # List all line items - - header = ["Supplier", "Product", "Unit Size", "Variant", "Weight", "Sum Total", "Sum Max Total", "Units Required", "Remainder"] - - columns = [ proc { |lis| lis.first.variant.product.supplier.name }, - proc { |lis| lis.first.variant.product.name }, - proc { |lis| lis.first.variant.product.group_buy ? (lis.first.variant.product.group_buy_unit_size || 0.0) : "" }, - proc { |lis| lis.first.variant.full_name }, - proc { |lis| lis.first.variant.weight || 0 }, - proc { |lis| lis.sum { |li| li.quantity } }, - proc { |lis| lis.sum { |li| li.max_quantity || 0 } }, - proc { |lis| "" }, - proc { |lis| "" } ] - - rules = [ { group_by: proc { |li| li.variant.product.supplier }, - sort_by: proc { |supplier| supplier.name } }, - { group_by: proc { |li| li.variant.product }, - sort_by: proc { |product| product.name }, - summary_columns: [ proc { |lis| lis.first.variant.product.supplier.name }, - proc { |lis| lis.first.variant.product.name }, - proc { |lis| lis.first.variant.product.group_buy ? (lis.first.variant.product.group_buy_unit_size || 0.0) : "" }, - proc { |lis| "" }, - proc { |lis| "" }, - proc { |lis| lis.sum { |li| li.quantity * (li.variant.weight || 0) } }, - proc { |lis| lis.sum { |li| (li.max_quantity || 0) * (li.variant.weight || 0) } }, - proc { |lis| ( (lis.first.variant.product.group_buy_unit_size || 0).zero? ? 0 : ( lis.sum { |li| ( [li.max_quantity || 0, li.quantity || 0].max ) * (li.variant.weight || 0) } / lis.first.variant.product.group_buy_unit_size ) ).floor }, - proc { |lis| lis.sum { |li| ( [li.max_quantity || 0, li.quantity || 0].max ) * (li.variant.weight || 0) } - ( ( (lis.first.variant.product.group_buy_unit_size || 0).zero? ? 0 : ( lis.sum { |li| ( [li.max_quantity || 0, li.quantity || 0].max ) * (li.variant.weight || 0) } / lis.first.variant.product.group_buy_unit_size ) ).floor * (lis.first.variant.product.group_buy_unit_size || 0) ) } ] }, - { group_by: proc { |li| li.variant }, - sort_by: proc { |variant| variant.full_name } } ] - - end - - order_grouper = OpenFoodNetwork::OrderGrouper.new rules, columns - - @header = header - @table = order_grouper.table(@line_items) - csv_file_name = "bulk_coop_#{timestamp}.csv" - - render_report(@header, @table, params[:csv], csv_file_name) + render_report(@report.header, @table, params[:csv], csv_file_name) end def payments - 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]).beginning_of_day rescue Time.zone.now.beginning_of_month - end - - if params[:q] && !params[:q][:completed_at_lt].blank? - params[:q][:completed_at_lt] = Time.zone.parse(params[:q][:completed_at_lt]).end_of_day rescue "" - end - params[:q][:meta_sort] ||= "completed_at.desc" - - @search = Spree::Order.complete.not_state(:canceled).managed_by(spree_current_user).search(params[:q]) - - orders = @search.result - payments = orders.map { |o| o.payments.select { |payment| payment.completed? } }.flatten # Only select completed 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] - case params[:report_type] - when "payments_by_payment_type" - table_items = payments - - header = ["Payment State", "Distributor", "Payment Type", "Total (#{currency_symbol})"] - - columns = [ proc { |payments| payments.first.order.payment_state }, - proc { |payments| payments.first.order.distributor.name }, - proc { |payments| payments.first.payment_method.name }, - proc { |payments| payments.sum { |payment| payment.amount } } ] - - rules = [ { group_by: proc { |payment| payment.order.payment_state }, - sort_by: proc { |payment_state| payment_state } }, - { group_by: proc { |payment| payment.order.distributor }, - sort_by: proc { |distributor| distributor.name } }, - { group_by: proc { |payment| Spree::PaymentMethod.unscoped { payment.payment_method } }, - sort_by: proc { |method| method.name } } ] - - when "itemised_payment_totals" - table_items = orders - - header = ["Payment State", "Distributor", "Product Total (#{currency_symbol})", "Shipping Total (#{currency_symbol})", "Outstanding Balance (#{currency_symbol})", "Total (#{currency_symbol})"] - - columns = [ proc { |orders| orders.first.payment_state }, - proc { |orders| orders.first.distributor.name }, - proc { |orders| orders.sum { |o| o.item_total } }, - proc { |orders| orders.sum { |o| o.ship_total } }, - proc { |orders| orders.sum { |o| o.outstanding_balance } }, - proc { |orders| orders.sum { |o| o.total } } ] - - rules = [ { group_by: proc { |order| order.payment_state }, - sort_by: proc { |payment_state| payment_state } }, - { group_by: proc { |order| order.distributor }, - sort_by: proc { |distributor| distributor.name } } ] - - when "payment_totals" - table_items = orders - - header = ["Payment State", "Distributor", "Product Total (#{currency_symbol})", "Shipping Total (#{currency_symbol})", "Total (#{currency_symbol})", "EFT (#{currency_symbol})", "PayPal (#{currency_symbol})", "Outstanding Balance (#{currency_symbol})"] - - columns = [ proc { |orders| orders.first.payment_state }, - proc { |orders| orders.first.distributor.name }, - proc { |orders| orders.sum { |o| o.item_total } }, - proc { |orders| orders.sum { |o| o.ship_total } }, - proc { |orders| orders.sum { |o| o.total } }, - proc { |orders| orders.sum { |o| o.payments.select { |payment| payment.completed? && (payment.payment_method.name.to_s.include? "EFT") }.sum { |payment| payment.amount } } }, - proc { |orders| orders.sum { |o| o.payments.select { |payment| payment.completed? && (payment.payment_method.name.to_s.include? "PayPal") }.sum{ |payment| payment.amount } } }, - proc { |orders| orders.sum { |o| o.outstanding_balance } } ] - - rules = [ { group_by: proc { |order| order.payment_state }, - sort_by: proc { |payment_state| payment_state } }, - { group_by: proc { |order| order.distributor }, - sort_by: proc { |distributor| distributor.name } } ] - - else - table_items = payments - - header = ["Payment State", "Distributor", "Payment Type", "Total (#{currency_symbol})"] - - columns = [ proc { |payments| payments.first.order.payment_state }, - proc { |payments| payments.first.order.distributor.name }, - proc { |payments| payments.first.payment_method.name }, - proc { |payments| payments.sum { |payment| payment.amount } } ] - - rules = [ { group_by: proc { |payment| payment.order.payment_state }, - sort_by: proc { |payment_state| payment_state } }, - { group_by: proc { |payment| payment.order.distributor }, - sort_by: proc { |distributor| distributor.name } }, - { group_by: proc { |payment| payment.payment_method }, - sort_by: proc { |method| method.name } } ] - - end - - order_grouper = OpenFoodNetwork::OrderGrouper.new rules, columns - - @header = header - @table = order_grouper.table(table_items) + # -- Build Report with Order Grouper + @report = OpenFoodNetwork::PaymentsReport.new spree_current_user, params + order_grouper = OpenFoodNetwork::OrderGrouper.new @report.rules, @report.columns + @table = order_grouper.table(@report.table_items) csv_file_name = "payments_#{timestamp}.csv" - render_report(@header, @table, params[:csv], csv_file_name) - + render_report(@report.header, @table, params[:csv], csv_file_name) end def orders_and_fulfillment - # -- 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 - 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" + # -- Prepare Date Params + prepare_date_params params + # -- Prepare Form Options permissions = OpenFoodNetwork::Permissions.new(spree_current_user) - - # -- Search - - @search = Spree::Order.complete.not_state(:canceled).search(params[:q]) - orders = permissions.visible_orders.merge(@search.result) - - @line_items = permissions.visible_line_items.merge(Spree::LineItem.where(order_id: orders)) - @line_items = @line_items.supplied_by_any(params[:supplier_id_in]) if params[:supplier_id_in].present? - - line_items_with_hidden_details = @line_items.where('"spree_line_items"."id" NOT IN (?)', permissions.editable_line_items) - @line_items.select{ |li| line_items_with_hidden_details.include? li }.each do |line_item| - # TODO We should really be hiding customer code here too, but until we - # have an actual association between order and customer, it's a bit tricky - line_item.order.bill_address.assign_attributes(firstname: "HIDDEN", lastname: "", phone: "", address1: "", address2: "", city: "", zipcode: "", state: nil) - line_item.order.ship_address.assign_attributes(firstname: "HIDDEN", lastname: "", phone: "", address1: "", address2: "", city: "", zipcode: "", state: nil) - line_item.order.assign_attributes(email: "HIDDEN") - end - # My distributors and any distributors distributing products I supply @distributors = permissions.visible_enterprises_for_order_reports.is_distributor - # My suppliers and any suppliers supplying products I distribute @suppliers = permissions.visible_enterprises_for_order_reports.is_primary_producer @@ -438,239 +200,25 @@ Spree::Admin::ReportsController.class_eval do @report_types = REPORT_TYPES[:orders_and_fulfillment] @report_type = params[:report_type] - # -- Format according to report type - case params[:report_type] - when "order_cycle_supplier_totals" - table_items = @line_items - @include_blank = 'All' + @include_blank = 'All' - header = ["Producer", "Product", "Variant", "Amount", "Total Units", "Curr. Cost per Unit", "Total Cost", "Status", "Incoming Transport"] - - columns = [ proc { |line_items| line_items.first.variant.product.supplier.name }, - proc { |line_items| line_items.first.variant.product.name }, - proc { |line_items| line_items.first.variant.full_name }, - proc { |line_items| line_items.sum { |li| li.quantity } }, - proc { |line_items| total_units(line_items) }, - proc { |line_items| line_items.first.price }, - proc { |line_items| line_items.sum { |li| li.amount } }, - proc { |line_items| "" }, - proc { |line_items| "incoming transport" } ] - - rules = [ { group_by: proc { |line_item| line_item.variant.product.supplier }, - sort_by: proc { |supplier| supplier.name } }, - { group_by: proc { |line_item| line_item.variant.product }, - sort_by: proc { |product| product.name } }, - { group_by: proc { |line_item| line_item.variant }, - sort_by: proc { |variant| variant.full_name } } ] - - when "order_cycle_supplier_totals_by_distributor" - table_items = @line_items - @include_blank = 'All' - - header = ["Producer", "Product", "Variant", "To Hub", "Amount", "Curr. Cost per Unit", "Total Cost", "Shipping Method"] - - columns = [ proc { |line_items| line_items.first.variant.product.supplier.name }, - proc { |line_items| line_items.first.variant.product.name }, - proc { |line_items| line_items.first.variant.full_name }, - proc { |line_items| line_items.first.order.distributor.name }, - proc { |line_items| line_items.sum { |li| li.quantity } }, - proc { |line_items| line_items.first.price }, - proc { |line_items| line_items.sum { |li| li.amount } }, - proc { |line_items| "shipping method" } ] - - rules = [ { group_by: proc { |line_item| line_item.variant.product.supplier }, - sort_by: proc { |supplier| supplier.name } }, - { group_by: proc { |line_item| line_item.variant.product }, - sort_by: proc { |product| product.name } }, - { group_by: proc { |line_item| line_item.variant }, - sort_by: proc { |variant| variant.full_name }, - summary_columns: [ proc { |line_items| "" }, - proc { |line_items| "" }, - proc { |line_items| "" }, - proc { |line_items| "TOTAL" }, - proc { |line_items| "" }, - proc { |line_items| "" }, - proc { |line_items| line_items.sum { |li| li.amount } }, - proc { |line_items| "" } ] }, - { group_by: proc { |line_item| line_item.order.distributor }, - sort_by: proc { |distributor| distributor.name } } ] - - when "order_cycle_distributor_totals_by_supplier" - table_items = @line_items - @include_blank = 'All' - - header = ["Hub", "Producer", "Product", "Variant", "Amount", "Curr. Cost per Unit", "Total Cost", "Total Shipping Cost", "Shipping Method"] - - columns = [ proc { |line_items| line_items.first.order.distributor.name }, - proc { |line_items| line_items.first.variant.product.supplier.name }, - proc { |line_items| line_items.first.variant.product.name }, - proc { |line_items| line_items.first.variant.full_name }, - proc { |line_items| line_items.sum { |li| li.quantity } }, - proc { |line_items| line_items.first.price }, - proc { |line_items| line_items.sum { |li| li.amount } }, - proc { |line_items| "" }, - proc { |line_items| "shipping method" } ] - - rules = [ { group_by: proc { |line_item| line_item.order.distributor }, - sort_by: proc { |distributor| distributor.name }, - summary_columns: [ proc { |line_items| "" }, - proc { |line_items| "TOTAL" }, - proc { |line_items| "" }, - proc { |line_items| "" }, - proc { |line_items| "" }, - proc { |line_items| "" }, - proc { |line_items| line_items.sum { |li| li.amount } }, - proc { |line_items| line_items.map { |li| li.order }.uniq.sum { |o| o.ship_total } }, - proc { |line_items| "" } ] }, - { group_by: proc { |line_item| line_item.variant.product.supplier }, - sort_by: proc { |supplier| supplier.name } }, - { group_by: proc { |line_item| line_item.variant.product }, - sort_by: proc { |product| product.name } }, - { group_by: proc { |line_item| line_item.variant }, - sort_by: proc { |variant| variant.full_name } } ] - - when "order_cycle_customer_totals" - table_items = @line_items - @include_blank = 'All' - - header = ["Hub", "Customer", "Email", "Phone", "Producer", "Product", "Variant", - "Amount", "Item (#{currency_symbol})", "Item + Fees (#{currency_symbol})", "Admin & Handling (#{currency_symbol})", "Ship (#{currency_symbol})", "Total (#{currency_symbol})", "Paid?", - "Shipping", "Delivery?", - "Ship Street", "Ship Street 2", "Ship City", "Ship Postcode", "Ship State", - "Comments", "SKU", - "Order Cycle", "Payment Method", "Customer Code", "Tags", - "Billing Street 1", "Billing Street 2", "Billing City", "Billing Postcode", "Billing State" - ] - - rsa = proc { |line_items| line_items.first.order.shipping_method.andand.require_ship_address } - - columns = [ - proc { |line_items| line_items.first.order.distributor.name }, - proc { |line_items| line_items.first.order.bill_address.firstname + " " + line_items.first.order.bill_address.lastname }, - proc { |line_items| line_items.first.order.email }, - proc { |line_items| line_items.first.order.bill_address.phone }, - proc { |line_items| line_items.first.variant.product.supplier.name }, - proc { |line_items| line_items.first.variant.product.name }, - proc { |line_items| line_items.first.variant.full_name }, - - proc { |line_items| line_items.sum { |li| li.quantity } }, - proc { |line_items| line_items.sum { |li| li.amount } }, - proc { |line_items| line_items.sum { |li| li.amount_with_adjustments } }, - proc { |line_items| "" }, - proc { |line_items| "" }, - proc { |line_items| "" }, - proc { |line_items| "" }, - - proc { |line_items| line_items.first.order.shipping_method.andand.name }, - proc { |line_items| rsa.call(line_items) ? 'Y' : 'N' }, - - proc { |line_items| line_items.first.order.ship_address.andand.address1 if rsa.call(line_items) }, - proc { |line_items| line_items.first.order.ship_address.andand.address2 if rsa.call(line_items) }, - proc { |line_items| line_items.first.order.ship_address.andand.city if rsa.call(line_items) }, - proc { |line_items| line_items.first.order.ship_address.andand.zipcode if rsa.call(line_items) }, - proc { |line_items| line_items.first.order.ship_address.andand.state if rsa.call(line_items) }, - - proc { |line_items| "" }, - proc { |line_items| line_items.first.variant.product.sku }, - - proc { |line_items| line_items.first.order.order_cycle.andand.name }, - proc { |line_items| line_items.first.order.payments.first.andand.payment_method.andand.name }, - proc { |line_items| line_items.first.order.user.andand.customer_of(line_items.first.order.distributor).andand.code }, - proc { |line_items| "" }, - - proc { |line_items| line_items.first.order.bill_address.andand.address1 }, - proc { |line_items| line_items.first.order.bill_address.andand.address2 }, - proc { |line_items| line_items.first.order.bill_address.andand.city }, - proc { |line_items| line_items.first.order.bill_address.andand.zipcode }, - proc { |line_items| line_items.first.order.bill_address.andand.state } ] - - rules = [ { group_by: proc { |line_item| line_item.order.distributor }, - sort_by: proc { |distributor| distributor.name } }, - { group_by: proc { |line_item| line_item.order }, - sort_by: proc { |order| order.bill_address.lastname + " " + order.bill_address.firstname }, - summary_columns: [ - proc { |line_items| line_items.first.order.distributor.name }, - proc { |line_items| line_items.first.order.bill_address.firstname + " " + line_items.first.order.bill_address.lastname }, - proc { |line_items| "" }, - proc { |line_items| "" }, - proc { |line_items| "" }, - proc { |line_items| "TOTAL" }, - proc { |line_items| "" }, - - proc { |line_items| "" }, - proc { |line_items| line_items.sum { |li| li.amount } }, - proc { |line_items| line_items.sum { |li| li.amount_with_adjustments } }, - proc { |line_items| line_items.map { |li| li.order }.uniq.sum { |o| o.admin_and_handling_total } }, - proc { |line_items| line_items.map { |li| li.order }.uniq.sum { |o| o.ship_total } }, - proc { |line_items| line_items.map { |li| li.order }.uniq.sum { |o| o.total } }, - proc { |line_items| line_items.all? { |li| li.order.paid? } ? "Yes" : "No" }, - - proc { |line_items| "" }, - proc { |line_items| "" }, - - proc { |line_items| "" }, - proc { |line_items| "" }, - proc { |line_items| "" }, - proc { |line_items| "" }, - proc { |line_items| "" }, - - proc { |line_items| line_items.first.order.special_instructions } , - proc { |line_items| "" }, - - proc { |line_items| line_items.first.order.order_cycle.andand.name }, - proc { |line_items| line_items.first.order.payments.first.andand.payment_method.andand.name }, - proc { |line_items| "" }, - proc { |line_items| "" }, - - proc { |line_items| "" }, - proc { |line_items| "" }, - proc { |line_items| "" }, - proc { |line_items| "" }, - proc { |line_items| "" } - ] }, - - { group_by: proc { |line_item| line_item.variant.product }, - sort_by: proc { |product| product.name } }, - { group_by: proc { |line_item| line_item.variant }, - sort_by: proc { |variant| variant.full_name } } ] - - else - table_items = @line_items - @include_blank = 'All' - - header = ["Producer", "Product", "Variant", "Amount", "Curr. Cost per Unit", "Total Cost", "Status", "Incoming Transport"] - - columns = [ proc { |line_items| line_items.first.variant.product.supplier.name }, - proc { |line_items| line_items.first.variant.product.name }, - proc { |line_items| line_items.first.variant.full_name }, - proc { |line_items| line_items.sum { |li| li.quantity } }, - proc { |line_items| line_items.first.price }, - proc { |line_items| line_items.sum { |li| li.quantity * li.price } }, - proc { |line_items| "" }, - proc { |line_items| "incoming transport" } ] - - rules = [ { group_by: proc { |line_item| line_item.variant.product.supplier }, - sort_by: proc { |supplier| supplier.name } }, - { group_by: proc { |line_item| line_item.variant.product }, - sort_by: proc { |product| product.name } }, - { group_by: proc { |line_item| line_item.variant }, - sort_by: proc { |variant| variant.full_name } } ] - - end - - order_grouper = OpenFoodNetwork::OrderGrouper.new rules, columns - - @header = header - @table = order_grouper.table(table_items) + # -- Build Report with Order Grouper + @report = OpenFoodNetwork::OrdersAndFulfillmentsReport.new spree_current_user, params + order_grouper = OpenFoodNetwork::OrderGrouper.new @report.rules, @report.columns + @table = order_grouper.table(@report.table_items) csv_file_name = "#{params[:report_type]}_#{timestamp}.csv" - render_report(@header, @table, params[:csv], csv_file_name) + render_report(@report.header, @table, params[:csv], csv_file_name) end def products_and_inventory @report_types = REPORT_TYPES[:products_and_inventory] - @report = OpenFoodNetwork::ProductsAndInventoryReport.new spree_current_user, params + if params[:report_type] != 'lettuce_share' + @report = OpenFoodNetwork::ProductsAndInventoryReport.new spree_current_user, params + else + @report = OpenFoodNetwork::LettuceShareReport.new spree_current_user, params + end render_report(@report.header, @report.table, params[:csv], "products_and_inventory_#{timestamp}.csv") end @@ -709,6 +257,20 @@ Spree::Admin::ReportsController.class_eval do 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 + 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 # Load distributors either owned by the user or selling their enterprises products. my_distributors = Enterprise.is_distributor.managed_by(spree_current_user) @@ -732,23 +294,14 @@ Spree::Admin::ReportsController.class_eval do :sales_total => { :name => "Sales Total", :description => "Sales Total For All Orders" }, :users_and_enterprises => { :name => "Users & Enterprises", :description => "Enterprise Ownership & Status" }, :order_cycle_management => {:name => "Order Cycle Management", :description => ''}, + :packing => {:name => "Packing Reports", :description => ''}, :sales_tax => { :name => "Sales Tax", :description => "Sales Tax For Orders" }, :xero_invoices => { :name => "Xero Invoices", :description => 'Invoices for import into Xero' } - } # Return only reports the user is authorized to view. reports.select { |action| can? action, :report } end - def total_units(line_items) - return " " if line_items.map{ |li| li.variant.unit_value.nil? }.any? - total_units = line_items.sum do |li| - scale_factor = ( li.product.variant_unit == 'weight' ? 1000 : 1 ) - li.quantity * li.variant.unit_value / scale_factor - end - total_units.round(3) - end - def timestamp Time.now.strftime("%Y%m%d") end diff --git a/app/controllers/spree/admin/variants_controller_decorator.rb b/app/controllers/spree/admin/variants_controller_decorator.rb index 2308d2a923..814cfb12f8 100644 --- a/app/controllers/spree/admin/variants_controller_decorator.rb +++ b/app/controllers/spree/admin/variants_controller_decorator.rb @@ -14,9 +14,10 @@ Spree::Admin::VariantsController.class_eval do if params[:distributor_id].present? distributor = Enterprise.find params[:distributor_id] @variants = @variants.in_distributor(distributor) + scoper = OpenFoodNetwork::ScopeVariantToHub.new(distributor) # Perform scoping after all filtering is done. # Filtering could be a problem on scoped variants. - @variants.each { |v| v.scope_to_hub(distributor) } + @variants.each { |v| scoper.scope(v) } end end diff --git a/app/controllers/spree/api/line_items_controller_decorator.rb b/app/controllers/spree/api/line_items_controller_decorator.rb index 35fca864f4..5493c74043 100644 --- a/app/controllers/spree/api/line_items_controller_decorator.rb +++ b/app/controllers/spree/api/line_items_controller_decorator.rb @@ -1,5 +1,8 @@ Spree::Api::LineItemsController.class_eval do - after_filter :apply_enterprise_fees, :only => :update + after_filter :apply_enterprise_fees, only: :update + + + private def apply_enterprise_fees authorize! :read, order diff --git a/app/helpers/markdown_helper.rb b/app/helpers/markdown_helper.rb new file mode 100644 index 0000000000..bd45e31124 --- /dev/null +++ b/app/helpers/markdown_helper.rb @@ -0,0 +1,6 @@ +module MarkdownHelper + def render_markdown(markdown) + md ||= Redcarpet::Markdown.new(Redcarpet::Render::HTML, no_intra_emphasis: true, tables: true, autolink: true, superscript: true) + md.render markdown + end +end diff --git a/app/helpers/spree/admin/base_helper_decorator.rb b/app/helpers/spree/admin/base_helper_decorator.rb index e278626c5c..82bf794073 100644 --- a/app/helpers/spree/admin/base_helper_decorator.rb +++ b/app/helpers/spree/admin/base_helper_decorator.rb @@ -15,6 +15,16 @@ module Spree link_to_with_icon('icon-trash', name, url, :class => "remove_fields #{options[:class]}", :data => {:action => 'remove'}, :title => t(:remove)) + f.hidden_field(:_destroy) end + + + def preference_field_tag_with_files(name, value, options) + if options[:type] == :file + file_field_tag name, preference_field_options(options) + else + preference_field_tag_without_files name, value, options + end + end + alias_method_chain :preference_field_tag, :files end end end diff --git a/app/helpers/spree/reports_helper.rb b/app/helpers/spree/reports_helper.rb index f4ecdcea6f..bbc184d800 100644 --- a/app/helpers/spree/reports_helper.rb +++ b/app/helpers/spree/reports_helper.rb @@ -18,6 +18,11 @@ module Spree orders.map { |o| o.shipping_method.andand.name }.uniq end + def xero_report_types + [['Summary', 'summary'], + ['Detailed', 'detailed']] + end + def currency_symbol Spree::Money.currency_symbol end diff --git a/app/jobs/finalize_account_invoices.rb b/app/jobs/finalize_account_invoices.rb new file mode 100644 index 0000000000..fcf872e1c3 --- /dev/null +++ b/app/jobs/finalize_account_invoices.rb @@ -0,0 +1,96 @@ +class FinalizeAccountInvoices + attr_reader :year, :month, :start_date, :end_date + + def initialize(year = nil, month = nil) + ref_point = Time.now - 1.month + @year = year || ref_point.year + @month = month || ref_point.month + @start_date = Time.new(@year, @month) + @end_date = Time.new(@year, @month) + 1.month + end + + def before(job) + UpdateBillablePeriods.new(year, month).perform + UpdateAccountInvoices.new(year, month).perform + end + + def perform + return unless settings_are_valid? + + + invoice_orders = AccountInvoice.where(year: year, month: month).map(&:order) + invoice_orders.select{ |order| order.present? && order.completed_at.nil? }.each{ |order| finalize(order) } + end + + def finalize(invoice_order) + # TODO: When we implement per-customer and/or per-user preferences around shipping and payment methods + # we can update these to read from those preferences + invoice_order.payments.create(payment_method_id: Spree::Config.default_accounts_payment_method_id, amount: invoice_order.total) + invoice_order.update_attribute(:shipping_method_id, Spree::Config.default_accounts_shipping_method_id) + while invoice_order.state != "complete" + if invoice_order.errors.any? + Bugsnag.notify(RuntimeError.new("FinalizeInvoiceError"), { + job: "FinalizeAccountInvoices", + error: "Cannot finalize invoice due to errors", + data: { + errors: invoice_order.errors.full_messages + } + }) + break + else + invoice_order.next + end + end + end + + private + + def settings_are_valid? + unless end_date <= Time.now + Bugsnag.notify(RuntimeError.new("InvalidJobSettings"), { + job: "FinalizeAccountInvoices", + error: "end_date is in the future", + data: { + end_date: end_date.localtime.strftime("%F %T"), + now: Time.now.strftime("%F %T") + } + }) + return false + end + + unless @accounts_distributor = Enterprise.find_by_id(Spree::Config.accounts_distributor_id) + Bugsnag.notify(RuntimeError.new("InvalidJobSettings"), { + job: "FinalizeAccountInvoices", + error: "accounts_distributor_id is invalid", + data: { + accounts_distributor_id: Spree::Config.accounts_distributor_id + } + }) + return false + end + + unless @accounts_distributor.payment_methods.find_by_id(Spree::Config.default_accounts_payment_method_id) + Bugsnag.notify(RuntimeError.new("InvalidJobSettings"), { + job: "FinalizeAccountInvoices", + error: "default_accounts_payment_method_id is invalid", + data: { + default_accounts_payment_method_id: Spree::Config.default_accounts_payment_method_id + } + }) + return false + end + + unless @accounts_distributor.shipping_methods.find_by_id(Spree::Config.default_accounts_shipping_method_id) + Bugsnag.notify(RuntimeError.new("InvalidJobSettings"), { + job: "FinalizeAccountInvoices", + error: "default_accounts_shipping_method_id is invalid", + data: { + default_accounts_shipping_method_id: Spree::Config.default_accounts_shipping_method_id + } + }) + return false + end + + true + end +end diff --git a/app/jobs/update_account_invoices.rb b/app/jobs/update_account_invoices.rb new file mode 100644 index 0000000000..6f43a10e31 --- /dev/null +++ b/app/jobs/update_account_invoices.rb @@ -0,0 +1,104 @@ +class UpdateAccountInvoices + attr_reader :year, :month, :start_date, :end_date + + def initialize(year = nil, month = nil) + ref_point = Time.now - 1.day + @year = year || ref_point.year + @month = month || ref_point.month + @start_date = Time.new(@year, @month) + @end_date = Time.new(@year, @month) + 1.month + @end_date = Time.now.beginning_of_day if start_date == Time.now.beginning_of_month + end + + def before(job) + UpdateBillablePeriods.new(year, month).perform + end + + def perform + return unless settings_are_valid? + + account_invoices = AccountInvoice.where(year: year, month: month) + account_invoices.each { |account_invoice| update(account_invoice) } + end + + def update(account_invoice) + current_adjustments = [] + unless account_invoice.order + account_invoice.order = account_invoice.user.orders.new(distributor_id: Spree::Config[:accounts_distributor_id]) + end + + if account_invoice.order.complete? + Bugsnag.notify(RuntimeError.new("InvoiceAlreadyFinalized"), { + invoice_order: account_invoice.order.as_json + }) + else + billable_periods = account_invoice.billable_periods.order(:enterprise_id, :begins_at).reject{ |bp| bp.turnover == 0 } + + if billable_periods.any? + address = billable_periods.first.enterprise.address + account_invoice.order.update_attributes(bill_address: address, ship_address: address) + end + + billable_periods.each do |billable_period| + current_adjustments << billable_period.ensure_correct_adjustment_for(account_invoice.order) + end + + account_invoice.save if current_adjustments.any? + + clean_up(account_invoice.order, current_adjustments) + end + end + + def clean_up(invoice_order, current_adjustments) + # Snag and then delete any obsolete adjustments + obsolete_adjustments = invoice_order.adjustments.where('source_type = (?) AND id NOT IN (?)', "BillablePeriod", current_adjustments) + + if obsolete_adjustments.any? + Bugsnag.notify(RuntimeError.new("Obsolete Adjustments"), { + current: current_adjustments.map(&:as_json), + obsolete: obsolete_adjustments.map(&:as_json) + }) + + obsolete_adjustments.destroy_all + end + + if current_adjustments.empty? + if invoice_order.persisted? + Bugsnag.notify(RuntimeError.new("Empty Persisted Invoice"), { + invoice_order: invoice_order.as_json + }) + else + invoice_order.destroy + end + end + end + + private + + def settings_are_valid? + unless end_date <= Time.now + Bugsnag.notify(RuntimeError.new("InvalidJobSettings"), { + job: "UpdateAccountInvoices", + error: "end_date is in the future", + data: { + end_date: end_date.localtime.strftime("%F %T"), + now: Time.now.strftime("%F %T") + } + }) + return false + end + + unless Enterprise.find_by_id(Spree::Config.accounts_distributor_id) + Bugsnag.notify(RuntimeError.new("InvalidJobSettings"), { + job: "UpdateAccountInvoices", + error: "accounts_distributor_id is invalid", + data: { + accounts_distributor_id: Spree::Config.accounts_distributor_id + } + }) + return false + end + + true + end +end diff --git a/app/jobs/update_billable_periods.rb b/app/jobs/update_billable_periods.rb new file mode 100644 index 0000000000..35daf48dbc --- /dev/null +++ b/app/jobs/update_billable_periods.rb @@ -0,0 +1,128 @@ +class UpdateBillablePeriods + attr_reader :year, :month, :start_date, :end_date + + def initialize(year = nil, month = nil) + ref_point = Time.now - 1.day + @year = year || ref_point.year + @month = month || ref_point.month + @start_date = Time.new(@year, @month) + @end_date = Time.new(@year, @month) + 1.month + @end_date = Time.now.beginning_of_day if start_date == Time.now.beginning_of_month + end + + def perform + return unless settings_are_valid? + + job_start_time = Time.now + + enterprises = Enterprise.where('created_at < (?)', end_date).select([:id, :name, :owner_id, :sells, :shop_trial_start_date, :created_at]) + + # Cycle through enterprises + enterprises.each do |enterprise| + start_for_enterprise = [start_date, enterprise.created_at].max + end_for_enterprise = [end_date].min # [end_date, enterprise.deleted_at].min + + # Cycle through previous versions of this enterprise + versions = enterprise.versions.where('created_at >= (?) AND created_at < (?)', start_for_enterprise, end_for_enterprise).order(:created_at) + + trial_start = enterprise.shop_trial_start_date + trial_expiry = enterprise.shop_trial_expiry + + versions.each do |version| + begins_at = version.previous.andand.created_at || start_for_enterprise + ends_at = version.created_at + + split_for_trial(version.reify, begins_at, ends_at, trial_start, trial_expiry) + end + + # Update / create billable_period for current start + begins_at = versions.last.andand.created_at || start_for_enterprise + ends_at = end_date + + split_for_trial(enterprise, begins_at, ends_at, trial_start, trial_expiry) + + clean_up_untouched_billable_periods_for(enterprise, job_start_time) + end + end + + def split_for_trial(enterprise, begins_at, ends_at, trial_start, trial_expiry) + trial_start = trial_expiry = begins_at-1.day if trial_start.nil? || trial_expiry.nil? + + # If the trial begins after ends_at, create a bill for the entire period + # Otherwise, create a normal billable_period from the begins_at until the start of the trial + if trial_start > begins_at + update_billable_period(enterprise, begins_at, [ends_at, trial_start].min, false) + end + + # If all or some of the trial occurs between begins_at and ends_at + # Create a trial billable_period from the from begins_at or trial_start, whichever occurs last, until ends_at, or trial_expiry whichever occurs first + if trial_expiry >= begins_at && trial_start <= ends_at + update_billable_period(enterprise, [trial_start, begins_at].max, [ends_at, trial_expiry].min, true) + end + + # If the trial finishes before begins_at, or trial has not been set, create a bill for the entire period + # Otherwise, create a normal billable_period from the end of the trial until ends_at + if trial_expiry < ends_at + update_billable_period(enterprise, [trial_expiry, begins_at].max, ends_at, false) + end + end + + def update_billable_period(enterprise, begins_at, ends_at, trial) + owner_id = enterprise.owner_id + sells = enterprise.sells + orders = Spree::Order.where('distributor_id = (?) AND completed_at >= (?) AND completed_at < (?)', enterprise.id, begins_at, ends_at) + account_invoice = AccountInvoice.find_or_create_by_user_id_and_year_and_month(owner_id, begins_at.year, begins_at.month) + + billable_period = BillablePeriod.where(account_invoice_id: account_invoice.id, begins_at: begins_at, enterprise_id: enterprise.id).first + + unless account_invoice.order.andand.complete? + billable_period ||= BillablePeriod.new(account_invoice_id: account_invoice.id, begins_at: begins_at, enterprise_id: enterprise.id) + billable_period.update_attributes({ + ends_at: ends_at, + sells: sells, + trial: trial, + owner_id: owner_id, + turnover: orders.sum(&:total) + }) + end + + billable_period.touch + end + + def clean_up_untouched_billable_periods_for(enterprise, job_start_time) + # Snag and then delete any BillablePeriods which overlap + obsolete_billable_periods = enterprise.billable_periods.where('ends_at > (?) AND begins_at < (?) AND updated_at < (?)', start_date, end_date, job_start_time) + + if obsolete_billable_periods.any? + current_billable_periods = enterprise.billable_periods.where('ends_at >= (?) AND begins_at <= (?) AND updated_at > (?)', start_date, end_date, job_start_time) + + Delayed::Worker.logger.info "#{enterprise.name} #{start_date.strftime("%F %T")} #{job_start_time.strftime("%F %T")}" + Delayed::Worker.logger.info "#{obsolete_billable_periods.first.updated_at.strftime("%F %T")}" + + Bugsnag.notify(RuntimeError.new("Obsolete BillablePeriods"), { + current: current_billable_periods.map(&:as_json), + obsolete: obsolete_billable_periods.map(&:as_json) + }) + end + + obsolete_billable_periods.each(&:delete) + end + + private + + def settings_are_valid? + unless end_date <= Time.now + Bugsnag.notify(RuntimeError.new("InvalidJobSettings"), { + job: "UpdateBillablePeriods", + error: "end_date is in the future", + data: { + end_date: end_date.localtime.strftime("%F %T"), + now: Time.now.strftime("%F %T") + } + }) + return false + end + + true + end +end diff --git a/app/models/account_invoice.rb b/app/models/account_invoice.rb new file mode 100644 index 0000000000..6d381565bb --- /dev/null +++ b/app/models/account_invoice.rb @@ -0,0 +1,6 @@ +class AccountInvoice < ActiveRecord::Base + belongs_to :user, class_name: "Spree::User" + belongs_to :order, class_name: "Spree::Order" + attr_accessible :user_id, :order_id, :issued_at, :month, :year + has_many :billable_periods +end diff --git a/app/models/billable_period.rb b/app/models/billable_period.rb new file mode 100644 index 0000000000..8ea3829440 --- /dev/null +++ b/app/models/billable_period.rb @@ -0,0 +1,79 @@ +class BillablePeriod < ActiveRecord::Base + belongs_to :enterprise + belongs_to :owner, class_name: 'Spree::User' + belongs_to :account_invoice + has_one :adjustment, :as => :source, class_name: "Spree::Adjustment" #, :dependent => :destroy + + default_scope where(deleted_at: nil) + + def display_turnover + Spree::Money.new(turnover, {currency: Spree::Config[:currency]}) + end + + def display_bill + Spree::Money.new(bill, {currency: Spree::Config[:currency]}) + end + + def bill + # Will make this more sophisicated in the future in that it will use global config variables to calculate + return 0 if trial? + if ['own', 'any'].include? sells + bill = (turnover * 0.02).round(2) + bill > 50 ? 50 : bill + else + 0 + end + end + + def label + enterprise_version = enterprise.version_at(begins_at) + category = enterprise_version.category.to_s.titleize + category += (trial ? " Trial" : "") + + "#{enterprise_version.name} (#{category})" + end + + def adjustment_label + begins = begins_at.localtime.strftime("%d/%m/%y") + ends = ends_at.localtime.strftime("%d/%m/%y") + + "#{label} [#{begins} - #{ends}]" + end + + def delete + self.update_column(:deleted_at, Time.now) + end + + def ensure_correct_adjustment_for(invoice) + if adjustment + # adjustment.originator = enterprise.package + adjustment.update_attributes( label: adjustment_label, amount: bill ) + else + self.adjustment = invoice.adjustments.new( adjustment_attrs, :without_protection => true ) + end + + if Spree::Config.account_bill_inc_tax + adjustment.set_included_tax! Spree::Config.account_bill_tax_rate + else + adjustment.set_included_tax! 0 + end + + adjustment + end + + private + + def adjustment_attrs + # We should ultimately have an EnterprisePackage model, which holds all info about shop type, producer, trials, etc. + # It should also implement a calculator that we can use here by specifying the package as the originator of the + # adjustment, meaning that adjustments are created and updated using Spree's existing architecture. + + { label: adjustment_label, + amount: bill, + source: self, + originator: nil, # enterprise.package + mandatory: true, + locked: false + } + end +end diff --git a/app/models/content_configuration.rb b/app/models/content_configuration.rb new file mode 100644 index 0000000000..2357f8dfa3 --- /dev/null +++ b/app/models/content_configuration.rb @@ -0,0 +1,54 @@ +require 'open_food_network/paperclippable' + +class ContentConfiguration < Spree::Preferences::FileConfiguration + include OpenFoodNetwork::Paperclippable + + # Header + preference :logo, :file + preference :logo_mobile, :file + preference :logo_mobile_svg, :file + has_attached_file :logo + has_attached_file :logo_mobile + has_attached_file :logo_mobile_svg + + # Home page + preference :home_hero, :file + preference :home_show_stats, :boolean, default: true + has_attached_file :home_hero + + # Producer sign-up page + preference :producer_signup_pricing_table_html, :text, default: "(TODO: Pricing table)" + preference :producer_signup_case_studies_html, :text, default: "(TODO: Case studies)" + preference :producer_signup_detail_html, :text, default: "(TODO: Detail)" + + # Hubs sign-up page + preference :hub_signup_pricing_table_html, :text, default: "(TODO: Pricing table)" + preference :hub_signup_case_studies_html, :text, default: "(TODO: Case studies)" + preference :hub_signup_detail_html, :text, default: "(TODO: Detail)" + + # Groups sign-up page + preference :group_signup_pricing_table_html, :text, default: "(TODO: Pricing table)" + preference :group_signup_case_studies_html, :text, default: "(TODO: Case studies)" + preference :group_signup_detail_html, :text, default: "(TODO: Detail)" + + # Footer + preference :footer_logo, :file + has_attached_file :footer_logo + preference :footer_facebook_url, :string, default: "https://www.facebook.com/OpenFoodNet" + preference :footer_twitter_url, :string, default: "https://twitter.com/OpenFoodNet" + preference :footer_instagram_url, :string, default: "" + preference :footer_linkedin_url, :string, default: "http://www.linkedin.com/groups/Open-Food-Foundation-4743336" + preference :footer_googleplus_url, :string, default: "" + preference :footer_pinterest_url, :string, default: "" + preference :footer_email, :string, default: "hello@openfoodnetwork.org" + preference :footer_links_md, :text, default: <<-EOS +[Newsletter sign-up](/) + +[News](/) + +[Calendar](/) +EOS + + preference :footer_about_url, :string, default: "http://www.openfoodnetwork.org/ofn-local/open-food-network-australia/" + preference :footer_tos_url, :string, default: "/Terms-of-service.pdf" +end diff --git a/app/models/enterprise.rb b/app/models/enterprise.rb index 4adfb05309..a0b8e91c40 100644 --- a/app/models/enterprise.rb +++ b/app/models/enterprise.rb @@ -10,11 +10,14 @@ class Enterprise < ActiveRecord::Base devise :confirmable, reconfirmable: true, confirmation_keys: [ :id, :email ] handle_asynchronously :send_confirmation_instructions handle_asynchronously :send_on_create_confirmation_instructions + has_paper_trail only: [:owner_id, :sells], on: [:update] self.inheritance_column = nil acts_as_gmappable :process_geocoding => false + has_many :relationships_as_parent, class_name: 'EnterpriseRelationship', foreign_key: 'parent_id', dependent: :destroy + has_many :relationships_as_child, class_name: 'EnterpriseRelationship', foreign_key: 'child_id', dependent: :destroy has_and_belongs_to_many :groups, class_name: 'EnterpriseGroup' has_many :producer_properties, foreign_key: 'producer_id' has_many :properties, through: :producer_properties @@ -31,6 +34,7 @@ class Enterprise < ActiveRecord::Base has_many :distributor_shipping_methods, foreign_key: :distributor_id has_many :shipping_methods, through: :distributor_shipping_methods has_many :customers + has_many :billable_periods delegate :latitude, :longitude, :city, :state_name, :to => :address @@ -56,6 +60,7 @@ class Enterprise < ActiveRecord::Base validates :name, presence: true + validate :name_is_unique validates :sells, presence: true, inclusion: {in: SELLS} validates :address, presence: true, associated: true validates :email, presence: true @@ -325,6 +330,10 @@ class Enterprise < ActiveRecord::Base !confirmed? || pending_reconfirmation? end + def shop_trial_expiry + shop_trial_start_date.andand + Enterprise::SHOP_TRIAL_LENGTH.days + end + protected def devise_mailer @@ -333,6 +342,15 @@ class Enterprise < ActiveRecord::Base private + def name_is_unique + dups = Enterprise.where(name: name) + dups = dups.where('id != ?', id) unless new_record? + + if dups.any? + errors.add :name, "has already been taken. If this is your enterprise and you would like to claim ownership, please contact the current manager of this profile at #{dups.first.owner.email}." + end + end + def email_is_known? owner.enterprises.confirmed.map(&:email).include?(email) end diff --git a/app/models/enterprise_group.rb b/app/models/enterprise_group.rb index 986dc07d22..63eb63f0b3 100644 --- a/app/models/enterprise_group.rb +++ b/app/models/enterprise_group.rb @@ -1,6 +1,8 @@ require 'open_food_network/locking' +require 'open_food_network/permalink_generator' class EnterpriseGroup < ActiveRecord::Base + include PermalinkGenerator acts_as_list has_and_belongs_to_many :enterprises @@ -81,25 +83,10 @@ class EnterpriseGroup < ActiveRecord::Base private - def self.find_available_value(existing, requested) - return requested unless existing.include?(requested) - used_indices = existing.map do |p| - p.slice!(/^#{requested}/) - p.match(/^\d+$/).to_s.to_i - end - options = (1..used_indices.length + 1).to_a - used_indices - requested + options.first.to_s - end - - def find_available_permalink(requested) - existing = self.class.where(id: !id).where("permalink LIKE ?", "#{requested}%").pluck(:permalink) - self.class.find_available_value(existing, requested) - end - def sanitize_permalink if permalink.blank? || permalink_changed? requested = permalink.presence || permalink_was.presence || name.presence || 'group' - self.permalink = find_available_permalink(requested.parameterize) + self.permalink = create_unique_permalink(requested.parameterize) end end end diff --git a/app/models/enterprise_relationship.rb b/app/models/enterprise_relationship.rb index de06a0578e..8d279b2a20 100644 --- a/app/models/enterprise_relationship.rb +++ b/app/models/enterprise_relationship.rb @@ -32,6 +32,15 @@ class EnterpriseRelationship < ActiveRecord::Base relationships = EnterpriseRelationship.includes(:child, :parent) relatives = {} + Enterprise.is_primary_producer.pluck(:id).each do |enterprise_id| + relatives[enterprise_id] ||= { distributors: Set.new, producers: Set.new } + relatives[enterprise_id][:producers] << enterprise_id + end + Enterprise.is_distributor.pluck(:id).each do |enterprise_id| + relatives[enterprise_id] ||= { distributors: Set.new, producers: Set.new } + relatives[enterprise_id][:distributors] << enterprise_id + end + relationships.each do |r| relatives[r.parent_id] ||= {distributors: Set.new, producers: Set.new} relatives[r.child_id] ||= {distributors: Set.new, producers: Set.new} diff --git a/app/models/order_cycle.rb b/app/models/order_cycle.rb index 515f26621d..44b482c1c4 100644 --- a/app/models/order_cycle.rb +++ b/app/models/order_cycle.rb @@ -132,7 +132,12 @@ class OrderCycle < ActiveRecord::Base end def variants - self.exchanges.map(&:variants).flatten.uniq.reject(&:deleted?) + Spree::Variant. + joins(:exchanges). + merge(Exchange.in_order_cycle(self)). + not_deleted. + select('DISTINCT spree_variants.*'). + to_a # http://stackoverflow.com/q/15110166 end def distributed_variants diff --git a/app/models/spree/ability_decorator.rb b/app/models/spree/ability_decorator.rb index 56478f99c7..95b509ee33 100644 --- a/app/models/spree/ability_decorator.rb +++ b/app/models/spree/ability_decorator.rb @@ -73,9 +73,12 @@ class AbilityDecorator can [:admin, :index, :read, :create, :edit, :update_positions, :destroy], ProducerProperty can [:admin, :index, :create], Enterprise - can [:read, :edit, :update, :bulk_update, :set_sells, :resend_confirmation], Enterprise do |enterprise| + can [:read, :edit, :update, :bulk_update, :resend_confirmation], Enterprise do |enterprise| OpenFoodNetwork::Permissions.new(user).editable_enterprises.include? enterprise end + can [:welcome, :register], Enterprise do |enterprise| + enterprise.owner == user + end can [:manage_payment_methods, :manage_shipping_methods, :manage_enterprise_fees], Enterprise do |enterprise| user.enterprises.include? enterprise end @@ -87,6 +90,8 @@ class AbilityDecorator end can [:admin, :known_users], :search + + can [:admin, :show], :account end def add_product_management_abilities(user) @@ -120,7 +125,7 @@ class AbilityDecorator can [:admin, :index, :read, :create, :edit], Spree::Classification # Reports page - can [:admin, :index, :customers, :orders_and_distributors, :group_buys, :bulk_coop, :payments, :orders_and_fulfillment, :products_and_inventory, :order_cycle_management], :report + can [:admin, :index, :customers, :orders_and_distributors, :group_buys, :bulk_coop, :payments, :orders_and_fulfillment, :products_and_inventory, :order_cycle_management, :packing], :report end def add_order_cycle_management_abilities(user) @@ -183,7 +188,7 @@ class AbilityDecorator end # Reports page - can [:admin, :index, :customers, :group_buys, :bulk_coop, :sales_tax, :payments, :orders_and_distributors, :orders_and_fulfillment, :products_and_inventory, :order_cycle_management], :report + can [:admin, :index, :customers, :group_buys, :bulk_coop, :sales_tax, :payments, :orders_and_distributors, :orders_and_fulfillment, :products_and_inventory, :order_cycle_management, :xero_invoices], :report can [:admin, :index, :update], Customer, enterprise_id: Enterprise.managed_by(user).pluck(:id) end diff --git a/app/models/spree/adjustment_decorator.rb b/app/models/spree/adjustment_decorator.rb index 8447d24046..d27b5d760b 100644 --- a/app/models/spree/adjustment_decorator.rb +++ b/app/models/spree/adjustment_decorator.rb @@ -6,6 +6,7 @@ module Spree has_one :metadata, class_name: 'AdjustmentMetadata' scope :enterprise_fee, where(originator_type: 'EnterpriseFee') + scope :billable_period, where(source_type: 'BillablePeriod') scope :included_tax, where(originator_type: 'Spree::TaxRate', adjustable_type: 'Spree::LineItem') scope :with_tax, where('spree_adjustments.included_tax > 0') scope :without_tax, where('spree_adjustments.included_tax = 0') @@ -20,5 +21,9 @@ module Spree def set_absolute_included_tax!(tax) update_attributes! included_tax: tax.round(2) end + + def has_tax? + included_tax > 0 + end end end diff --git a/app/models/spree/app_configuration_decorator.rb b/app/models/spree/app_configuration_decorator.rb index 5ad4a9a1c5..2e1c838e95 100644 --- a/app/models/spree/app_configuration_decorator.rb +++ b/app/models/spree/app_configuration_decorator.rb @@ -7,4 +7,13 @@ Spree::AppConfiguration.class_eval do # Tax Preferences preference :products_require_tax_category, :boolean, default: false preference :shipping_tax_rate, :decimal, default: 0 + preference :account_bill_inc_tax, :boolean, default: false + preference :account_bill_tax_rate, :decimal, default: 0 + + # Accounts & Billing Preferences + preference :accounts_distributor_id, :integer, default: nil + preference :default_accounts_payment_method_id, :integer, default: nil + preference :default_accounts_shipping_method_id, :integer, default: nil + preference :auto_update_invoices, :boolean, default: false + preference :auto_finalize_invoices, :boolean, default: false end diff --git a/app/models/spree/inventory_unit_decorator.rb b/app/models/spree/inventory_unit_decorator.rb index 939a97996f..9868596b41 100644 --- a/app/models/spree/inventory_unit_decorator.rb +++ b/app/models/spree/inventory_unit_decorator.rb @@ -4,9 +4,10 @@ module Spree return [] unless order.completed? #increase inventory to meet initial requirements + scoper = OpenFoodNetwork::ScopeVariantToHub.new(order.distributor) order.line_items.each do |line_item| # Scope variant to hub so that stock levels may be subtracted from VariantOverride. - line_item.variant.scope_to_hub order.distributor + scoper.scope(line_item.variant) increase(order, line_item.variant, line_item.quantity) end diff --git a/app/models/spree/line_item_decorator.rb b/app/models/spree/line_item_decorator.rb index f64f197d56..689cabcc6c 100644 --- a/app/models/spree/line_item_decorator.rb +++ b/app/models/spree/line_item_decorator.rb @@ -1,6 +1,6 @@ Spree::LineItem.class_eval do - attr_accessible :max_quantity, :unit_value - attr_accessible :unit_value, :price, :as => :api + attr_accessible :max_quantity, :final_weight_volume + attr_accessible :final_weight_volume, :price, :as => :api # -- Scopes scope :managed_by, lambda { |user| @@ -33,6 +33,10 @@ Spree::LineItem.class_eval do where('spree_adjustments.id IS NULL') + def has_tax? + adjustments.included_tax.any? + end + def price_with_adjustments # EnterpriseFee#create_locked_adjustment applies adjustments on line items to their parent order, # so line_item.adjustments returns an empty array diff --git a/app/models/spree/order_decorator.rb b/app/models/spree/order_decorator.rb index 3c0f212945..4ca328a5b1 100644 --- a/app/models/spree/order_decorator.rb +++ b/app/models/spree/order_decorator.rb @@ -130,7 +130,11 @@ Spree::Order.class_eval do else current_item = Spree::LineItem.new(:quantity => quantity, max_quantity: max_quantity) current_item.variant = variant - current_item.unit_value = variant.unit_value + if variant.unit_value + current_item.final_weight_volume = variant.unit_value * quantity + else + current_item.final_weight_volume = 0 + end if currency current_item.currency = currency unless currency.nil? current_item.price = variant.price_in(currency).amount @@ -157,20 +161,22 @@ Spree::Order.class_eval do end def update_distribution_charge! - EnterpriseFee.clear_all_adjustments_on_order self + with_lock do + EnterpriseFee.clear_all_adjustments_on_order self - line_items.each do |line_item| - if provided_by_order_cycle? line_item - OpenFoodNetwork::EnterpriseFeeCalculator.new.create_line_item_adjustments_for line_item + line_items.each do |line_item| + if provided_by_order_cycle? line_item + OpenFoodNetwork::EnterpriseFeeCalculator.new.create_line_item_adjustments_for line_item - else - pd = product_distribution_for line_item - pd.create_adjustment_for line_item if pd + else + pd = product_distribution_for line_item + pd.create_adjustment_for line_item if pd + end end - end - if order_cycle - OpenFoodNetwork::EnterpriseFeeCalculator.new.create_order_adjustments_for self + if order_cycle + OpenFoodNetwork::EnterpriseFeeCalculator.new.create_order_adjustments_for self + end end end @@ -230,9 +236,16 @@ Spree::Order.class_eval do (adjustments + price_adjustments).sum &:included_tax end + def account_invoice? + distributor_id == Spree::Config.accounts_distributor_id + end + # Overrride of Spree method, that allows us to send separate confirmation emails to user and shop owners + # And separately, to skip sending confirmation email completely for user invoice orders def deliver_order_confirmation_email - Delayed::Job.enqueue ConfirmOrderJob.new(id) + unless account_invoice? + Delayed::Job.enqueue ConfirmOrderJob.new(id) + end end diff --git a/app/models/spree/order_populator_decorator.rb b/app/models/spree/order_populator_decorator.rb index 3759866236..cd80d17b97 100644 --- a/app/models/spree/order_populator_decorator.rb +++ b/app/models/spree/order_populator_decorator.rb @@ -9,7 +9,7 @@ Spree::OrderPopulator.class_eval do errors.add(:base, "That distributor or order cycle can't supply all the products in your cart. Please choose another.") end - if valid? + if valid? @order.with_lock do @order.empty! if overwrite @@ -33,7 +33,7 @@ Spree::OrderPopulator.class_eval do def attempt_cart_add(variant_id, quantity, max_quantity = nil) quantity = quantity.to_i variant = Spree::Variant.find(variant_id) - variant.scope_to_hub @distributor + OpenFoodNetwork::ScopeVariantToHub.new(@distributor).scope(variant) if quantity > 0 if check_stock_levels(variant, quantity) && check_order_cycle_provided_for(variant) && diff --git a/app/models/spree/preferences/file_configuration.rb b/app/models/spree/preferences/file_configuration.rb new file mode 100644 index 0000000000..fa1838a778 --- /dev/null +++ b/app/models/spree/preferences/file_configuration.rb @@ -0,0 +1,50 @@ +module Spree::Preferences + class FileConfiguration < Configuration + + def self.preference(name, type, *args) + if type == :file + super "#{name}_file_name", :string, *args + super "#{name}_content_type", :string, *args + super "#{name}_file_size", :integer, *args + super "#{name}_updated_at", :string, *args + + else + super name, type, *args + end + end + + + def get_preference(key) + if !has_preference?(key) && has_attachment?(key) + send key + else + super key + end + end + alias :[] :get_preference + + + def preference_type(name) + if has_attachment? name + :file + else + super name + end + end + + + # Spree's Configuration responds to preference methods via method_missing, but doesn't + # override respond_to?, which consequently reports those methods as unavailable. Paperclip + # errors if respond_to? isn't correct, so we override it here. + def respond_to?(method, include_all=false) + name = method.to_s.gsub('=', '') + super(self.class.preference_getter_method(name), include_all) || super(method, include_all) + end + + + def has_attachment?(name) + self.class.respond_to?(:attachment_definitions) && + self.class.attachment_definitions.keys.include?(name.to_sym) + end + end +end diff --git a/app/models/spree/product_decorator.rb b/app/models/spree/product_decorator.rb index c789d81963..53f960ba63 100644 --- a/app/models/spree/product_decorator.rb +++ b/app/models/spree/product_decorator.rb @@ -1,8 +1,7 @@ -require 'open_food_network/scope_product_to_hub' +require 'open_food_network/permalink_generator' Spree::Product.class_eval do - include OpenFoodNetwork::ProductScopableToHub - + include PermalinkGenerator # We have an after_destroy callback on Spree::ProductOptionType. However, if we # don't specify dependent => destroy on this association, it is not called. See: # https://github.com/rails/rails/issues/7618 @@ -23,6 +22,8 @@ Spree::Product.class_eval do attr_accessible :variant_unit, :variant_unit_scale, :variant_unit_name, :unit_value attr_accessible :inherits_properties, :sku + before_validation :sanitize_permalink + # validates_presence_of :variants, unless: :new_record?, message: "Product must have at least one variant" validates_presence_of :supplier validates :primary_taxon, presence: { message: "^Product Category can't be blank" } @@ -243,4 +244,11 @@ Spree::Product.class_eval do raise end end + + def sanitize_permalink + if permalink.blank? || permalink_changed? + requested = permalink.presence || permalink_was.presence || name.presence || 'product' + self.permalink = create_unique_permalink(requested.parameterize) + end + end end diff --git a/app/models/spree/shipment_decorator.rb b/app/models/spree/shipment_decorator.rb index 460fae4985..f544d95517 100644 --- a/app/models/spree/shipment_decorator.rb +++ b/app/models/spree/shipment_decorator.rb @@ -11,5 +11,14 @@ module Spree end alias_method_chain :ensure_correct_adjustment, :included_tax + + private + + # NOTE: This is an override of spree's method, needed to allow orders + # without line items (ie. user invoices) to not have inventory units + def require_inventory + return false unless Spree::Config[:track_inventory_levels] && line_items.count > 0 # This line altered + order.completed? && !order.canceled? + end end end diff --git a/app/models/spree/user_decorator.rb b/app/models/spree/user_decorator.rb index 13ab56c129..b724a41d1b 100644 --- a/app/models/spree/user_decorator.rb +++ b/app/models/spree/user_decorator.rb @@ -7,6 +7,8 @@ Spree.user_class.class_eval do has_many :enterprises, through: :enterprise_roles has_many :owned_enterprises, class_name: 'Enterprise', foreign_key: :owner_id, inverse_of: :owner has_many :owned_groups, class_name: 'EnterpriseGroup', foreign_key: :owner_id, inverse_of: :owner + has_many :account_invoices + has_many :billable_periods, foreign_key: :owner_id, inverse_of: :owner has_one :cart has_many :customers @@ -17,7 +19,6 @@ Spree.user_class.class_eval do validate :limit_owned_enterprises - def known_users if admin? Spree::User.scoped @@ -48,7 +49,6 @@ Spree.user_class.class_eval do owned_enterprises(:reload).size < enterprise_limit end - private def limit_owned_enterprises diff --git a/app/models/spree/variant_decorator.rb b/app/models/spree/variant_decorator.rb index 72a7fb4dec..5ac89c1430 100644 --- a/app/models/spree/variant_decorator.rb +++ b/app/models/spree/variant_decorator.rb @@ -1,10 +1,7 @@ -require 'open_food_network/scope_variant_to_hub' require 'open_food_network/enterprise_fee_calculator' require 'open_food_network/option_value_namer' Spree::Variant.class_eval do - include OpenFoodNetwork::VariantScopableToHub - has_many :exchange_variants, dependent: :destroy has_many :exchanges, through: :exchange_variants has_many :variant_overrides @@ -46,6 +43,13 @@ Spree::Variant.class_eval do } + def self.indexed + Hash[ + scoped.map { |v| [v.id, v] } + ] + end + + def price_with_fees(distributor, order_cycle) price + fees_for(distributor, order_cycle) end @@ -96,6 +100,14 @@ Spree::Variant.class_eval do display_as end + def product_and_variant_name + name = product.name + + name += " - #{name_to_display}" if name_to_display != product.name + name += " (#{options_text})" if options_text + + name + end def update_units delete_unit_option_values diff --git a/app/models/variant_override.rb b/app/models/variant_override.rb index 51bb1468f7..55afd99321 100644 --- a/app/models/variant_override.rb +++ b/app/models/variant_override.rb @@ -8,6 +8,12 @@ class VariantOverride < ActiveRecord::Base where(hub_id: hubs) } + def self.indexed(hub) + Hash[ + for_hubs(hub).map { |vo| [vo.variant, vo] } + ] + end + def self.price_for(hub, variant) self.for(hub, variant).andand.price end @@ -25,12 +31,21 @@ class VariantOverride < ActiveRecord::Base if vo.nil? Bugsnag.notify RuntimeError.new "Attempting to decrement stock level for a variant without a VariantOverride." - - elsif vo.count_on_hand.blank? - Bugsnag.notify RuntimeError.new "Attempting to decrement stock level on a VariantOverride without a count_on_hand specified." - else - vo.decrement! :count_on_hand, quantity + vo.decrement_stock! quantity + end + end + + + def stock_overridden? + count_on_hand.present? + end + + def decrement_stock!(quantity) + if stock_overridden? + decrement! :count_on_hand, quantity + else + Bugsnag.notify RuntimeError.new "Attempting to decrement stock level on a VariantOverride without a count_on_hand specified." end end diff --git a/app/overrides/spree/admin/shared/_configuration_menu/add_accounts_and_billing.html.haml.deface b/app/overrides/spree/admin/shared/_configuration_menu/add_accounts_and_billing.html.haml.deface new file mode 100644 index 0000000000..14f4925206 --- /dev/null +++ b/app/overrides/spree/admin/shared/_configuration_menu/add_accounts_and_billing.html.haml.deface @@ -0,0 +1,4 @@ +// insert_bottom "[data-hook='admin_configurations_sidebar_menu']" + +%li + = link_to 'Accounts & Billing', main_app.edit_admin_accounts_and_billing_settings_path diff --git a/app/overrides/spree/admin/shared/_configuration_menu/add_content.html.haml.deface b/app/overrides/spree/admin/shared/_configuration_menu/add_content.html.haml.deface new file mode 100644 index 0000000000..0709cb9451 --- /dev/null +++ b/app/overrides/spree/admin/shared/_configuration_menu/add_content.html.haml.deface @@ -0,0 +1,3 @@ +/ insert_bottom "[data-hook='admin_configurations_sidebar_menu']" + +%li= link_to 'Content', main_app.edit_admin_content_path diff --git a/app/overrides/spree/admin/tax_settings/edit/add_products_require_tax_category.html.haml.deface b/app/overrides/spree/admin/tax_settings/edit/add_products_require_tax_category.html.haml.deface deleted file mode 100644 index 588669f005..0000000000 --- a/app/overrides/spree/admin/tax_settings/edit/add_products_require_tax_category.html.haml.deface +++ /dev/null @@ -1,6 +0,0 @@ -/ insert_before "[data-hook='shipment_vat']" - -%div.field.align-center{ "data-hook" => "products_require_tax_category" } - = hidden_field_tag 'preferences[products_require_tax_category]', '0' - = check_box_tag 'preferences[products_require_tax_category]', '1', Spree::Config[:products_require_tax_category] - = label_tag nil, t(:products_require_tax_category) \ No newline at end of file diff --git a/app/overrides/spree/admin/tax_settings/edit/shipping_tax_rate.html.haml.deface b/app/overrides/spree/admin/tax_settings/edit/shipping_tax_rate.html.haml.deface deleted file mode 100644 index b378ba84a6..0000000000 --- a/app/overrides/spree/admin/tax_settings/edit/shipping_tax_rate.html.haml.deface +++ /dev/null @@ -1,5 +0,0 @@ -/ insert_after "[data-hook='shipment_vat']" - -.field.align-center{ "data-hook" => "shipping_tax_rate" } - = number_field_tag "preferences[shipping_tax_rate]", Spree::Config[:shipping_tax_rate].to_f, in: 0.0..1.0, step: 0.01 - = label_tag nil, t(:shipping_tax_rate) \ No newline at end of file diff --git a/app/overrides/spree/layouts/admin/add_customers_admin_tab.html.haml.deface b/app/overrides/spree/layouts/admin/add_customers_admin_tab.html.haml.deface new file mode 100644 index 0000000000..7b7534b23f --- /dev/null +++ b/app/overrides/spree/layouts/admin/add_customers_admin_tab.html.haml.deface @@ -0,0 +1,2 @@ +/ insert_bottom "[data-hook='admin_tabs'], #admin_tabs[data-hook]" += tab :customers, :url => main_app.admin_customers_path diff --git a/app/serializers/api/admin/index_enterprise_serializer.rb b/app/serializers/api/admin/index_enterprise_serializer.rb new file mode 100644 index 0000000000..0f9166f4f8 --- /dev/null +++ b/app/serializers/api/admin/index_enterprise_serializer.rb @@ -0,0 +1,56 @@ +class Api::Admin::IndexEnterpriseSerializer < ActiveModel::Serializer + attributes :name, :id, :permalink, :is_primary_producer, :sells, :producer_profile_only, :owned, :edit_path + + attributes :issues, :warnings + + def owned + return true if options[:spree_current_user].admin? + object.owner == options[:spree_current_user] + end + + def edit_path + edit_admin_enterprise_path(object) + end + + def shipping_methods_ok? + return true unless object.is_distributor + object.shipping_methods.any? + end + + def payment_methods_ok? + return true unless object.is_distributor + object.payment_methods.any? + end + + def issues + issues = [] + + issues << { + description: "#{object.name} currently has no shipping methods.", + link: "Create New" + } unless shipping_methods_ok? + + issues << { + description: "#{object.name} currently has no payment methods.", + link: "Create New" + } unless payment_methods_ok? + + issues << { + description: "Email confirmation is pending. We've sent a confirmation email to #{object.email}.", + link: "Resend Email" + } unless object.confirmed? + + issues + end + + def warnings + warnings = [] + + warnings << { + description: "#{object.name} is not visible and so cannot be found on the map or in searches", + link: "Edit" + } unless object.visible + + warnings + end +end diff --git a/app/serializers/api/admin/line_item_serializer.rb b/app/serializers/api/admin/line_item_serializer.rb index 21fde91145..d12e3b291a 100644 --- a/app/serializers/api/admin/line_item_serializer.rb +++ b/app/serializers/api/admin/line_item_serializer.rb @@ -1,5 +1,5 @@ class Api::Admin::LineItemSerializer < ActiveModel::Serializer - attributes :id, :quantity, :max_quantity, :supplier, :price, :unit_value, :units_product, :units_variant + attributes :id, :quantity, :max_quantity, :supplier, :price, :final_weight_volume, :units_product, :units_variant def supplier Api::Admin::IdNameSerializer.new(object.product.supplier).serializable_hash @@ -13,7 +13,7 @@ class Api::Admin::LineItemSerializer < ActiveModel::Serializer Api::Admin::UnitsVariantSerializer.new(object.variant).serializable_hash end - def unit_value - object.unit_value.to_f + def final_weight_volume + object.final_weight_volume.to_f end end diff --git a/app/serializers/api/enterprise_serializer.rb b/app/serializers/api/enterprise_serializer.rb index 44364c4dbc..201757314f 100644 --- a/app/serializers/api/enterprise_serializer.rb +++ b/app/serializers/api/enterprise_serializer.rb @@ -89,12 +89,12 @@ class Api::CachedEnterpriseSerializer < ActiveModel::Serializer def producers relatives = options[:data].relatives[object.id] - relatives ? ids_to_objs(relatives[:producers]) : [] + ids_to_objs(relatives.andand[:producers]) end def hubs relatives = options[:data].relatives[object.id] - relatives ? ids_to_objs(relatives[:distributors]) : [] + ids_to_objs(relatives.andand[:distributors]) end # Map svg icons. @@ -139,6 +139,7 @@ class Api::CachedEnterpriseSerializer < ActiveModel::Serializer private def ids_to_objs(ids) - ids.andand.map { |id| {id: id} } + return [] if ids.blank? + ids.map { |id| {id: id} } end end diff --git a/app/serializers/api/product_serializer.rb b/app/serializers/api/product_serializer.rb index 4c4aec2310..5a1d1b5c86 100644 --- a/app/serializers/api/product_serializer.rb +++ b/app/serializers/api/product_serializer.rb @@ -22,7 +22,12 @@ class Api::UncachedProductSerializer < ActiveModel::Serializer attributes :price def price - object.master.price_with_fees(options[:current_distributor], options[:current_order_cycle]) + if options[:enterprise_fee_calculator] + object.master.price + options[:enterprise_fee_calculator].indexed_fees_for(object.master) + else + object.master.price_with_fees(options[:current_distributor], options[:current_order_cycle]) + end + end end @@ -35,13 +40,14 @@ class Api::CachedProductSerializer < ActiveModel::Serializer attributes :properties_with_values has_many :variants, serializer: Api::VariantSerializer - has_many :taxons, serializer: Api::IdSerializer - has_many :images, serializer: Api::ImageSerializer - - has_one :supplier, serializer: Api::IdSerializer - has_one :primary_taxon, serializer: Api::TaxonSerializer has_one :master, serializer: Api::VariantSerializer + has_one :primary_taxon, serializer: Api::TaxonSerializer + has_many :taxons, serializer: Api::IdSerializer + + has_many :images, serializer: Api::ImageSerializer + has_one :supplier, serializer: Api::IdSerializer + def properties_with_values object.properties_including_inherited end diff --git a/app/serializers/api/variant_serializer.rb b/app/serializers/api/variant_serializer.rb index f995fa4345..6908eaf84f 100644 --- a/app/serializers/api/variant_serializer.rb +++ b/app/serializers/api/variant_serializer.rb @@ -1,21 +1,25 @@ class Api::VariantSerializer < ActiveModel::Serializer - attributes :id, :is_master, :count_on_hand, :name_to_display, :unit_to_display, + attributes :id, :is_master, :count_on_hand, :name_to_display, :unit_to_display, :options_text, :on_demand, :price, :fees, :price_with_fees, :product_name - def price_with_fees - object.price_with_fees(options[:current_distributor], options[:current_order_cycle]) - end - def price object.price end def fees - object.fees_by_type_for(options[:current_distributor], options[:current_order_cycle]) + options[:enterprise_fee_calculator].andand.indexed_fees_by_type_for(object) || + object.fees_by_type_for(options[:current_distributor], options[:current_order_cycle]) + end + + def price_with_fees + if options[:enterprise_fee_calculator] + object.price + options[:enterprise_fee_calculator].indexed_fees_for(object) + else + object.price_with_fees(options[:current_distributor], options[:current_order_cycle]) + end end def product_name object.product.name end - end diff --git a/app/views/admin/account/show.html.haml b/app/views/admin/account/show.html.haml new file mode 100644 index 0000000000..809d52eb2f --- /dev/null +++ b/app/views/admin/account/show.html.haml @@ -0,0 +1,59 @@ + +- content_for :page_title do + = t(:account) + + +- if @invoices.empty? + %h4= t(:no_invoices_to_display) + +- @invoices.order('year DESC, month DESC').each do |invoice| + - order = invoice.order + .row.invoice_title + .eight.columns.alpha + %h4= "#{t(:abbr_month_names, :scope => :date)[invoice.month]} #{invoice.year}#{invoice.order.completed? ? "" : "*"}" + .eight.columns.omega.text-right + %h4.balance= invoice.order.display_total + %table.invoice_summary + %col{ width: '20%' } + %col{ width: '60%' } + %col{ width: '20%' } + %thead + %th Date + %th= t(:description) + %th= t(:charge) + - invoice.billable_periods.select{ |bp| bp.bill > 0}.each do |billable_period| + %tr + %td.text-center= "#{billable_period.begins_at.strftime("%d/%m/%Y")}" + %td= billable_period.label + %td.text-right= billable_period.display_bill + - order.adjustments.where('source_type <> (?)', "BillablePeriod").each do |adjustment| + %tr + %td.text-center   + %td= adjustment.label + %td.text-right= adjustment.display_amount + %tr.total + %td.text-center   + %td= t(:total).upcase + %td.text-right= order.display_total + +-# - if @enterprises.empty? +-# %h4 No enterprises to display +-# +-# - @enterprises.each do |enterprise| +-# %h2= enterprise.name +-# %table +-# %thead +-# %th Begins +-# %th Ends +-# %th Sells +-# %th Trial? +-# %th Turnover +-# %th Bill +-# - enterprise.billable_periods.each do |billable_period| +-# %tr +-# %td= billable_period.begins_at.localtime.strftime("%F %T") +-# %td= billable_period.ends_at.localtime.strftime("%F %T") +-# %td= billable_period.sells +-# %td= billable_period.trial? +-# %td= billable_period.display_turnover +-# %td= billable_period.display_bill diff --git a/app/views/admin/accounts_and_billing_settings/_method_settings.html.haml b/app/views/admin/accounts_and_billing_settings/_method_settings.html.haml new file mode 100644 index 0000000000..17037d47e9 --- /dev/null +++ b/app/views/admin/accounts_and_billing_settings/_method_settings.html.haml @@ -0,0 +1,10 @@ +.row + .six.columns.alpha + .field + = label :settings, :default_accounts_payment_method_id, t(:default_accounts_payment_method) + = collection_select(:settings, :default_accounts_payment_method_id, @payment_methods, :id, :name, { include_blank: true, selected: Spree::Config.default_accounts_payment_method_id}, { class: "select2 fullwidth" }) + + .six.columns.omega + .field + = label :settings, :default_accounts_shipping_method_id, t(:default_accounts_shipping_method) + = collection_select(:settings, :default_accounts_shipping_method_id, @shipping_methods, :id, :name, { include_blank: true, selected: Spree::Config.default_accounts_shipping_method_id}, { class: "select2 fullwidth" }) diff --git a/app/views/admin/accounts_and_billing_settings/edit.html.haml b/app/views/admin/accounts_and_billing_settings/edit.html.haml new file mode 100644 index 0000000000..d13c81dbed --- /dev/null +++ b/app/views/admin/accounts_and_billing_settings/edit.html.haml @@ -0,0 +1,88 @@ += render :partial => 'spree/admin/shared/configuration_menu' + +- content_for :page_title do + = t(:accounts_and_billing_settings) + += render 'spree/shared/error_messages', target: @settings + +-# - month_options = (0...12).map { |i| Time.now.beginning_of_month - i.months }.map{ |t| [t.strftime("%b %Y"), t.strftime("%b %Y %z")]} + +%fieldset.no-border-bottom + %legend Settings + = form_for @settings, as: :settings, url: main_app.admin_accounts_and_billing_settings_path, :method => :put do |f| + .row{ ng: { app: 'admin.accounts_and_billing_settings' } } + .twelve.columns.alpha.omega + .field + = f.label :accounts_distributor_id, t(:accounts_administration_distributor) + = f.collection_select(:accounts_distributor_id, @distributors, :id, :name, { include_blank: true }, { class: "select2 fullwidth", 'watch-value-as' => "enterprise_id"}) + + = f.hidden_field :default_accounts_payment_method_id, value: '' + = f.hidden_field :default_accounts_shipping_method_id, value: '' + %div{ 'method-settings-for' => 'enterprise_id' } + + .row + .six.columns.alpha + %fieldset.no-border-bottom + %legend Update Invoices + = f.check_box :auto_update_invoices + = f.label :auto_update_invoices, "Auto-update invoices nightly at 1:00am" + + .six.columns.omega + %fieldset.no-border-bottom + %legend Finalise Invoices + = f.check_box :auto_finalize_invoices + = f.label :auto_finalize_invoices, "Auto-finalise invoices monthly on the 2nd at 1:30am" + + .row + .twelve.columns.alpha.omega.form-buttons{"data-hook" => "buttons"} + = button t(:update), 'icon-refresh', value: "update" + +%fieldset.no-border-bottom + %legend Manually Run Tasks + .row + .six.columns.alpha.step.text-center + .form-buttons{"data-hook" => "buttons"} + =link_to_with_icon "icon-undo", "Update User Invoices", + main_app.start_job_admin_accounts_and_billing_settings_path(job: { name: "update_account_invoices" }), + class: "button fullwidth" + + %br + + - if @update_account_invoices_job + %p.text-center + - if @update_account_invoices_job.run_at < Time.now + %strong In Progress + %br + Started at: + - else + %strong Queued + %br + Scheduled for: + = @update_account_invoices_job.run_at + - else + %p.explanation + Use this button to immediately update invoices for the month to date for each enterprise user in the system. This task can be set up to run automatically every night. + + + .six.columns.omega.step.text-center + .form-buttons{"data-hook" => "buttons"} + =link_to_with_icon "icon-ok-sign", "Finalise User Invoices", + main_app.start_job_admin_accounts_and_billing_settings_path(job: { name: "finalize_account_invoices" }), + class: "button fullwidth" + + %br + + - if @finalize_account_invoices_job + %p.text-center + - if @finalize_account_invoices_job.run_at < Time.now + %strong In Progress + %br + Started at: + - else + %strong Queued + %br + Scheduled for: + = @finalize_account_invoices_job.run_at + - else + %p.explanation + Use this button to finalize all invoices in the system for the previous calendar month. This task can be set up to run automatically once a month. diff --git a/app/views/admin/contents/_fieldset.html.haml b/app/views/admin/contents/_fieldset.html.haml new file mode 100644 index 0000000000..709f3d5ec8 --- /dev/null +++ b/app/views/admin/contents/_fieldset.html.haml @@ -0,0 +1,8 @@ +%fieldset.no-border-bottom + %legend{align: "center"}= name + - preferences.each do |key| + - type = ContentConfig.preference_type(key) + .field + = label_tag(key, t(key) + ': ') + tag(:br) if type != :boolean + = preference_field_tag(key, ContentConfig[key], :type => type) + = label_tag(key, t(key)) + tag(:br) if type == :boolean diff --git a/app/views/admin/contents/edit.html.haml b/app/views/admin/contents/edit.html.haml new file mode 100644 index 0000000000..82751abebc --- /dev/null +++ b/app/views/admin/contents/edit.html.haml @@ -0,0 +1,15 @@ += render 'spree/admin/shared/configuration_menu' + +- content_for :page_title do + Content + + += form_tag main_app.admin_content_path, method: :put, multipart: true do + #preferences + - @preference_sections.each do |preference_section| + = render 'fieldset', name: preference_section[:name], preferences: preference_section[:preferences] + + .form-buttons.filter-actions.actions{"data-hook" => "buttons"} + = button t(:update), 'icon-refresh' + %span.or= t(:or) + = link_to_with_icon 'icon-remove', t(:cancel), main_app.edit_admin_content_path, class: 'button' diff --git a/app/views/admin/enterprise_relationships/_enterprise_relationship.html.haml b/app/views/admin/enterprise_relationships/_enterprise_relationship.html.haml index 45e79b1877..996bc487d8 100644 --- a/app/views/admin/enterprise_relationships/_enterprise_relationship.html.haml +++ b/app/views/admin/enterprise_relationships/_enterprise_relationship.html.haml @@ -1,10 +1,10 @@ -%tr{"ng-repeat" => "enterprise_relationship in EnterpriseRelationships.enterprise_relationships | filter:query"} +%tr{"ng-repeat" => "enterprise_relationship in EnterpriseRelationships.enterprise_relationships | keywords:query"} %td {{ enterprise_relationship.parent_name }} %td permits %td {{ enterprise_relationship.child_name }} %td %ul %li{"ng-repeat" => "permission in enterprise_relationship.permissions"} - {{ EnterpriseRelationships.permission_presentation(permission.name) }} + to {{ EnterpriseRelationships.permission_presentation(permission.name) }} %td.actions %a.delete-enterprise-relationship.icon-trash.no-text{'ng-click' => 'delete(enterprise_relationship)'} diff --git a/app/views/admin/enterprise_relationships/_form.html.haml b/app/views/admin/enterprise_relationships/_form.html.haml index 9d031a57c5..433f9f4ca0 100644 --- a/app/views/admin/enterprise_relationships/_form.html.haml +++ b/app/views/admin/enterprise_relationships/_form.html.haml @@ -7,10 +7,13 @@ %td %select{name: "enterprise_relationship_child_id", "ng-model" => "child_id", "ng-options" => "e.id as e.name for e in Enterprises.all_enterprises"} %td + %label + %input{type: "checkbox", ng: {checked: "allPermissionsChecked()", click: "checkAllPermissions()"}} + Everything %div{"ng-repeat" => "permission in EnterpriseRelationships.all_permissions"} %label %input{type: "checkbox", "ng-model" => "permissions[permission]"} - {{ EnterpriseRelationships.permission_presentation(permission) }} + to {{ EnterpriseRelationships.permission_presentation(permission) }} %td.actions %input{type: "button", value: "Create", "ng-click" => "create()"} .errors {{ EnterpriseRelationships.create_errors }} diff --git a/app/views/admin/enterprise_relationships/_search_input.html.haml b/app/views/admin/enterprise_relationships/_search_input.html.haml new file mode 100644 index 0000000000..b8bcbc62c6 --- /dev/null +++ b/app/views/admin/enterprise_relationships/_search_input.html.haml @@ -0,0 +1,5 @@ +%input.search{"ng-model" => "query", "placeholder" => "Search"} + +%label{ng: {repeat: "permission in EnterpriseRelationships.all_permissions"}} + %input{type: "checkbox", ng: {click: "$parent.query = toggleKeyword($parent.query, permission)"}} + {{ EnterpriseRelationships.permission_presentation(permission) }} diff --git a/app/views/admin/enterprise_relationships/index.html.haml b/app/views/admin/enterprise_relationships/index.html.haml index 0807a37825..40fbdbc415 100644 --- a/app/views/admin/enterprise_relationships/index.html.haml +++ b/app/views/admin/enterprise_relationships/index.html.haml @@ -6,7 +6,7 @@ %div{"ng-app" => "ofn.admin", "ng-controller" => "AdminEnterpriseRelationshipsCtrl"} = render 'data' - %input.search{"ng-model" => "query", "placeholder" => "Search"} + = render 'search_input' %table#enterprise-relationships %tbody diff --git a/app/views/admin/enterprises/_admin_index.html.haml b/app/views/admin/enterprises/_admin_index.html.haml new file mode 100644 index 0000000000..68c74a783c --- /dev/null +++ b/app/views/admin/enterprises/_admin_index.html.haml @@ -0,0 +1,44 @@ +-# For purposes of debugging bulk_update. See Admin/Enterprises#bulk_update. +- if flash[:action] + %p= flash[:action] + += form_for @enterprise_set, url: main_app.bulk_update_admin_enterprises_path do |f| + %table#listing_enterprises.index + %colgroup + %col{style: "width: 25%;"}/ + %col{style: "width: 15%;"}/ + %col{style: "width: 5%;"}/ + - if spree_current_user.admin? + %col{style: "width: 12%;"}/ + - if spree_current_user.admin? + %col{style: "width: 18%;"}/ + %col{style: "width: 25%;"}/ + %thead + %tr{"data-hook" => "enterprises_header"} + %th Name + %th Role + - if spree_current_user.admin? + %th Sells + %th Visible? + - if spree_current_user.admin? + %th Owner + %th + %tbody + = f.fields_for :collection do |enterprise_form| + - enterprise = enterprise_form.object + %tr{class: "enterprise-#{enterprise.id}"} + %td= link_to enterprise.name, main_app.edit_admin_enterprise_path(enterprise) + %td + = enterprise_form.check_box :is_primary_producer + Producer + - if spree_current_user.admin? + %td= enterprise_form.select :sells, Enterprise::SELLS, {}, class: 'select2 fullwidth' + %td= enterprise_form.check_box :visible + - if spree_current_user.admin? + %td= enterprise_form.select :owner_id, enterprise.users.map{ |e| [ e.email, e.id ] }, {}, class: "select2 fullwidth" + %td{"data-hook" => "admin_users_index_row_actions"} + = render 'actions', enterprise: enterprise + - if @enterprises.empty? + %tr + %td{colspan: "4"}= t(:none) + = f.submit 'Update' diff --git a/app/views/admin/enterprises/_change_type_form.html.haml b/app/views/admin/enterprises/_change_type_form.html.haml new file mode 100644 index 0000000000..740857e381 --- /dev/null +++ b/app/views/admin/enterprises/_change_type_form.html.haml @@ -0,0 +1,104 @@ += admin_inject_enterprise + += form_for @enterprise, url: main_app.register_admin_enterprise_path(@enterprise), + html: { name: "change_type", id: "change_type", novalidate: true, "ng-app" => "admin.enterprises", "ng-controller"=> 'changeTypeFormCtrl' } do |change_type_form| + -# Have to use hidden:'true' on this input rather than type:'hidden' as the latter seems to break ngPattern and therefore validation + %input{ hidden: "true", name: "sells", ng: { required: true, pattern: "/^(none|own|any)$/", model: 'sells', value: "sells"} } + + .row + .options.sixteen.columns.alpha + - if @enterprise.is_primary_producer + .basic_producer.option.one-third.column.alpha + %a.full-width.button.selector{ ng: { click: "sells='none'", class: "{selected: sells=='none'}" } } + .top + %h3 Producer Profile + %p Connect through OFN + .bottom ALWAYS FREE + %p.description + Add your products to Open Food Network, allowing hubs to stock your products in their stores. + %br + %br + Having a profile, and making connections within your local food system through the Open Food Network will always be free. + + .producer_shop.option.one-third.column + %a.full-width.button.selector{ ng: { click: "sells='own'", class: "{selected: sells=='own'}" } } + .top + %h3 Producer Shop + %p Sell your own produce + .bottom + \%2 OF SALES + %br + CAPPED AT $50 PER MONTH + %p.description + Sell your products directly to customers through your very own Open Food Network shopfront. + %br + %br + A Producer Shop is for your produce only, if you want to sell produce grown/produced off site, select 'Producer Hub'. + %br + %br + You will be billed for 2% of your actual transactions, capped at $50 a month (so if you don’t sell anything you don’t pay anything, but you never pay more than $50 a month). + + .full_hub.option.one-third.column.omega + %a.full-width.button.selector{ ng: { click: "sells='any'", class: "{selected: sells=='any'}" } } + .top + %h3 Producer Hub + %p Sell produce from self and others + .bottom + \%2 OF SALES + %br + CAPPED AT $50 PER MONTH + %p.description + Your enterprise is the backbone of your local food system. You can sell your own produce as well as produce aggregated from other enterprises through your shopfront on the Open Food Network. + %br + %br + You will be billed for 2% of your actual transactions, capped at $50 a month (so if you don’t sell anything you don’t pay anything, but you never pay more than $50 a month). + + + -# %p.description + -# Test out having your own shopfront with full access to all Shopfront features for 30 days. + -# %br + -# %br + -# At the end of your trial, there is a one-off $200 fee to fully activate your account. Then you will be billed for 2% of your actual transactions, capped at $50 a month (so if you don’t sell anything you don’t pay anything, but you never pay more than $50 a month). + + - else + .shop_profile.option.one-third.column.alpha + %a.full-width.button.selector{ ng: { click: "sells='none'", class: "{selected: sells=='none'}" } } + .top + %h3 Profile Only + %p Get a listing + .bottom ALWAYS FREE + %p.description + People can find and contact you on the Open Food Network. Your enterprise will be visible on the map, and will be searchable in listings. + %br + %br + Having a profile, and making connections within your local food system through the Open Food Network will always be free. + + .full_hub.option.one-third.column + %a.full-width.button.selector{ ng: { click: "sells='any'", class: "{selected: sells=='any'}" } } + .top + %h3 Hub Shop + %p Sell produce from others + .bottom + \%2 OF SALES + %br + CAPPED AT $50 PER MONTH + %p.description + Your enterprise is the backbone of your local food system. You aggregate produce from other enterprises and can sell it through your shop on the Open Food Network. + %br + %br + You will be billed for 2% of your actual transactions, capped at $50 a month (so if you don’t sell anything you don’t pay anything, but you never pay more than $50 a month). + + + .row + .sixteen.columns.alpha + %span.error{ ng: { show: "(change_type.sells.$error.required || change_type.sells.$error.pattern) && submitted" } } + Please choose one of the options above. + - if @enterprise.sells == 'unspecified' && @enterprise.shop_trial_start_date.nil? + %input.button.big{ type: 'submit', value: 'Start 30 Day Trial', ng: { click: "submit(change_type)", show: "sells=='own' || sells=='any'" } } + %input.button.big{ type: 'submit', value: 'Select and continue', ng: { click: "submit(change_type)", hide: "sells=='own' || sells=='any'" } } + - elsif @enterprise.sells == 'unspecified' + %input.button.big{ type: 'submit', value: 'Select and continue', ng: { click: "submit(change_type)" } } + - else + %input.button.big{ type: 'submit', value: 'Change now', ng: { click: "submit(change_type)" } } + %br   + %hr diff --git a/app/views/admin/enterprises/_enterprise_user_index.html.haml b/app/views/admin/enterprises/_enterprise_user_index.html.haml new file mode 100644 index 0000000000..5f4ad6cca9 --- /dev/null +++ b/app/views/admin/enterprises/_enterprise_user_index.html.haml @@ -0,0 +1,60 @@ +%div{ ng: { app: 'admin.enterprises', controller: 'enterprisesCtrl' } } + .row{ 'ng-hide' => '!loaded()' } + .controls{ :class => "sixteen columns alpha", :style => "margin-bottom: 15px;" } + .four.columns.alpha + %input{ :class => "fullwidth", :type => "text", :id => 'quick_search', 'ng-model' => 'quickSearch', :placeholder => 'Search By Name' } + .six.columns   + -# %div.ofn_drop_down{ 'ng-controller' => "DropDownCtrl", :id => "bulk_actions_dropdown", 'ofn-drop-down' => true } + -# %span{ :class => 'icon-check' }   Actions + -# %span{ 'ng-class' => "expanded && 'icon-caret-up' || !expanded && 'icon-caret-down'" } + -# %div.menu{ 'ng-show' => "expanded" } + -# %div.menu_item{ :class => "three columns alpha", 'ng-repeat' => "action in bulkActions", 'ng-click' => "selectedBulkAction.callback(filteredEnterprises)", 'ofn-close-on-click' => true } + -# %span{ :class => 'three columns omega' } {{action.name }} + .three.columns   + .three.columns.omega + %div.ofn_drop_down{ 'ng-controller' => "DropDownCtrl", :id => "columns_dropdown", 'ofn-drop-down' => true, :style => 'float:right;' } + %span{ :class => 'icon-reorder' }   Columns + %span{ 'ng-class' => "expanded && 'icon-caret-up' || !expanded && 'icon-caret-down'" } + %div.menu{ 'ng-show' => "expanded" } + %div.menu_item{ :class => "three columns alpha", 'ng-repeat' => "column in columns", 'ofn-toggle-column' => true } + %span{ :class => 'one column alpha', :style => 'text-align: center'} {{ column.visible && "✓" || !column.visible && " " }} + %span{ :class => 'two columns omega' } {{column.name }} + .row{ 'ng-if' => '!loaded()' } + .sixteen.columns.alpha#loading + %img.spinner{ src: "/assets/spinning-circles.svg" } + %h1 LOADING ENTERPRISES + .row{ :class => "sixteen columns alpha", 'ng-show' => 'loaded() && filteredEnterprises.length == 0'} + %h1#no_results No enterprises found. + + + .row{ ng: { show: "loaded() && filteredEnterprises.length > 0" }, bindonce: true } + %table.index#enterprises + %col.name{ width: "28%", ng: { show: 'columns.name.visible' } } + %col.producer{ width: "18%", ng: { show: 'columns.producer.visible' }} + %col.package{ width: "18%", ng: { show: 'columns.package.visible' }} + %col.status{ width: "18%", ng: { show: 'columns.status.visible' }} + %col.manage{ width: "18%", ng: { show: 'columns.manage.visible' }} + %thead + %tr{ ng: { controller: "ColumnsCtrl" } } + %th.name{ ng: { show: 'columns.name.visible' } } + Name + %th.producer{ ng: { show: 'columns.producer.visible' } } Producer? + %th.package{ ng: { show: 'columns.package.visible' } } Package + %th.status{ ng: { show: 'columns.status.visible' } } Status + %th.manage{ ng: { show: 'columns.manage.visible' } } Manage + %tbody{ :id => "e_{{enterprise.id}}", ng: { repeat: "enterprise in filteredEnterprises = ( allEnterprises | filter:{ name: quickSearch } )", controller: 'EnterpriseIndexRowCtrl' } } + %tr.enterprise.panel-toggle-row{ object: "enterprise", ng: { class: { even: "'even'", odd: "'odd'"} } } + %td.name{ ng: { show: 'columns.name.visible' } } + %span{ bo: { bind: "enterprise.name" } } + %td.producer.panel-toggle.text-center{ ng: { show: 'columns.producer.visible', class: "{error: producerError}" }, name: "producer" } + %h5{ ng: { bind: "producer" } } + %td.package.panel-toggle.text-center{ ng: { show: 'columns.package.visible', class: "{error: packageError}" }, name: "package" } + %h5{ ng: { bind: "package" } } + %td.status.panel-toggle.text-center{ ng: { show: 'columns.status.visible' }, name: "status" } + %i.icon-status{ bo: { class: "status" } } + %td.manage{ ng: { show: 'columns.manage.visible' } } + %a.button.fullwidth{ bo: { href: 'enterprise.edit_path' } } + Manage + %i.icon-arrow-right + + %tr.panel-row{ object: "enterprise", panels: "{producer: 'enterprise_producer', package: 'enterprise_package', status: 'enterprise_status'}" } diff --git a/app/views/admin/enterprises/index.html.haml b/app/views/admin/enterprises/index.html.haml index 8269cd3699..826d09336c 100644 --- a/app/views/admin/enterprises/index.html.haml +++ b/app/views/admin/enterprises/index.html.haml @@ -2,6 +2,8 @@ Enterprises - content_for :page_actions do + = render 'admin/shared/user_guide_link' + - if spree_current_user.can_own_more_enterprises? %li#new_product_link = button_link_to "New Enterprise", main_app.new_admin_enterprise_path, :icon => 'icon-plus', :id => 'admin_new_enterprise_link' @@ -10,47 +12,7 @@ = render :partial => 'spree/shared/error_messages', :locals => { :target => @enterprise_set } --# For purposes of debugging bulk_update. See Admin/Enterprises#bulk_update. -- if flash[:action] - %p= flash[:action] - -= form_for @enterprise_set, url: main_app.bulk_update_admin_enterprises_path do |f| - %table#listing_enterprises.index - %colgroup - %col{style: "width: 25%;"}/ - %col{style: "width: 15%;"}/ - %col{style: "width: 5%;"}/ - - if spree_current_user.admin? - %col{style: "width: 12%;"}/ - - if spree_current_user.admin? - %col{style: "width: 18%;"}/ - %col{style: "width: 25%;"}/ - %thead - %tr{"data-hook" => "enterprises_header"} - %th Name - %th Role - - if spree_current_user.admin? - %th Sells - %th Visible? - - if spree_current_user.admin? - %th Owner - %th - %tbody - = f.fields_for :collection do |enterprise_form| - - enterprise = enterprise_form.object - %tr{class: "enterprise-#{enterprise.id}"} - %td= link_to enterprise.name, main_app.edit_admin_enterprise_path(enterprise) - %td - = enterprise_form.check_box :is_primary_producer - Producer - - if spree_current_user.admin? - %td= enterprise_form.select :sells, Enterprise::SELLS, {}, class: 'select2 fullwidth' - %td= enterprise_form.check_box :visible - - if spree_current_user.admin? - %td= enterprise_form.select :owner_id, enterprise.users.map{ |e| [ e.email, e.id ] }, {}, class: "select2 fullwidth" - %td{"data-hook" => "admin_users_index_row_actions"} - = render 'actions', enterprise: enterprise - - if @enterprises.empty? - %tr - %td{colspan: "4"}= t(:none) - = f.submit 'Update' +- if spree_current_user.admin? + = render 'admin_index' +- else + = render 'enterprise_user_index' diff --git a/app/views/spree/admin/overview/welcome.html.haml b/app/views/admin/enterprises/welcome.html.haml similarity index 100% rename from app/views/spree/admin/overview/welcome.html.haml rename to app/views/admin/enterprises/welcome.html.haml diff --git a/app/views/admin/shared/_enterprises_sub_menu.html.haml b/app/views/admin/shared/_enterprises_sub_menu.html.haml index 31366091ef..3088b7e73e 100644 --- a/app/views/admin/shared/_enterprises_sub_menu.html.haml +++ b/app/views/admin/shared/_enterprises_sub_menu.html.haml @@ -1,4 +1,4 @@ = content_for :sub_menu do %ul#sub_nav.inline-menu{"data-hook" => "admin_enterprise_sub_tabs"} = tab :enterprises, url: main_app.admin_enterprises_path - = tab :relationships, url: main_app.admin_enterprise_relationships_path, match_path: '/enterprise_relationships' + = tab :enterprise_relationships, url: main_app.admin_enterprise_relationships_path diff --git a/app/views/admin/shared/_user_guide_link.html.haml b/app/views/admin/shared/_user_guide_link.html.haml new file mode 100644 index 0000000000..bfae6002e7 --- /dev/null +++ b/app/views/admin/shared/_user_guide_link.html.haml @@ -0,0 +1 @@ += button_link_to "User Guide", "http://www.openfoodnetwork.org/platform/user-guide/", :icon => 'icon-external-link', :id => 'user_guide_link', target: '_blank' diff --git a/app/views/checkout/edit.html.haml b/app/views/checkout/edit.html.haml index 2f30c93ba5..c2cc0aaa9d 100644 --- a/app/views/checkout/edit.html.haml +++ b/app/views/checkout/edit.html.haml @@ -1,6 +1,9 @@ +- content_for(:title) do + Checkout + = inject_enterprises -.darkswarm +.darkswarm.footer-pad - content_for :order_cycle_form do %closing Checkout now @@ -23,4 +26,3 @@ = render partial: "shared/footer" - diff --git a/app/views/enterprise_mailer/confirmation_instructions.html.haml b/app/views/enterprise_mailer/confirmation_instructions.html.haml index 3fe7ba09e9..3ee6e8fd12 100644 --- a/app/views/enterprise_mailer/confirmation_instructions.html.haml +++ b/app/views/enterprise_mailer/confirmation_instructions.html.haml @@ -15,7 +15,7 @@ %p After confirming your email you can access your administration account for this enterprise. See the - = link_to 'User Guide', 'http://global.openfoodnetwork.org/platform/user-guide/' + = link_to 'User Guide', 'http://www.openfoodnetwork.org/platform/user-guide/' = "to find out more about #{ Spree::Config[:site_name] }'s features and to start using your profile or online store." = render 'shared/mailers/signoff' diff --git a/app/views/enterprise_mailer/welcome.html.haml b/app/views/enterprise_mailer/welcome.html.haml index 3cd9d14034..ea939dbc6f 100644 --- a/app/views/enterprise_mailer/welcome.html.haml +++ b/app/views/enterprise_mailer/welcome.html.haml @@ -8,7 +8,7 @@ %p The User Guide with detailed support for setting up your Producer or Hub is here: - = link_to 'Open Food Network User Guide', 'http://global.openfoodnetwork.org/platform/user-guide/' + = link_to 'Open Food Network User Guide', 'http://www.openfoodnetwork.org/platform/user-guide/' %p You can manage your account by logging into the diff --git a/app/views/enterprises/_distributor_details.html.haml b/app/views/enterprises/_distributor_details.html.haml deleted file mode 100644 index a3f070b451..0000000000 --- a/app/views/enterprises/_distributor_details.html.haml +++ /dev/null @@ -1,24 +0,0 @@ -.distributor-details{'data-hook' => 'distributor-details'} - %h2= distributor.name - %p - %strong Address: - %br/ - = render 'spree/shared/address', :address => distributor.address - %p - %strong Next collection time: - %br/ - = distributor.next_collection_at - %p - %strong Regular collection times: - %br/ - = distributor.pickup_times - %p - %strong Contact: - %br/ - = distributor.contact - %br/ - = "Phone: #{distributor.phone}" - %br/ - = "Email: #{distributor.email}" - %p= distributor.description - %p= link_to distributor.website, distributor.website if distributor.website diff --git a/app/views/enterprises/distributors.html.haml b/app/views/enterprises/distributors.html.haml deleted file mode 100644 index 941b65237b..0000000000 --- a/app/views/enterprises/distributors.html.haml +++ /dev/null @@ -1,12 +0,0 @@ -- content_for :sidebar do - %div{'data-hook' => "homepage_sidebar_navigation"} - = render 'spree/sidebar' - - -%h1 Distributors - -= cms_page_content(:content, Cms::Page.find_by_full_path('/enterprises/distributors')) - -%ul.enterprises - - @distributors.each do |distributor| - %li= link_to distributor.name, distributor diff --git a/app/views/enterprises/distributors.js.erb b/app/views/enterprises/distributors.js.erb deleted file mode 100644 index aed01164da..0000000000 --- a/app/views/enterprises/distributors.js.erb +++ /dev/null @@ -1 +0,0 @@ -distributors = <%= @distributor_details.to_json.html_safe %>; \ No newline at end of file diff --git a/app/views/enterprises/index.html.haml b/app/views/enterprises/index.html.haml deleted file mode 100644 index a1e07dfc04..0000000000 --- a/app/views/enterprises/index.html.haml +++ /dev/null @@ -1,12 +0,0 @@ -- content_for :sidebar do - %div{'data-hook' => "homepage_sidebar_navigation"} - = render 'spree/sidebar' - - -%h1 Enterprises - -= cms_page_content(:content, Cms::Page.find_by_full_path('/enterprises')) - -%ul.enterprises - - @enterprises.each do |enterprise| - %li= link_to enterprise.name, enterprise diff --git a/app/views/enterprises/shop.html.haml b/app/views/enterprises/shop.html.haml index 08e25417cb..7ad617a4a1 100644 --- a/app/views/enterprises/shop.html.haml +++ b/app/views/enterprises/shop.html.haml @@ -1,3 +1,6 @@ +- content_for(:title) do + = current_distributor.name + = inject_enterprises %shop.darkswarm @@ -13,7 +16,7 @@ / Will this label should be a variable to reflect 'Ready for pickup / delivery' as appropriate %select.avenir#order_cycle_id{"ng-model" => "order_cycle.order_cycle_id", - "ng-change" => "changeOrderCycle()", + "ofn-change-order-cycle" => true, "ng-options" => "oc.id as oc.time for oc in #{@order_cycles.map {|oc| {time: pickup_time(oc), id: oc.id}}.to_json}", "popover-placement" => "left", "popover" => "Choose when you want your order:", "popover-trigger" => "openTrigger"} diff --git a/app/views/enterprises/shop_front.html.haml b/app/views/enterprises/shop_front.html.haml deleted file mode 100644 index 79364d368a..0000000000 --- a/app/views/enterprises/shop_front.html.haml +++ /dev/null @@ -1,15 +0,0 @@ -.row - .large-12.columns - %h2= @enterprise.name -.row - .large-12.columns= @enterprise.long_description.andand.html_safe - -.row - .large-12.columns - .products - - @products.each_slice(4).to_a.each do |products_row| - .row - - products_row.each do |product| - .large-4.columns.centered - .clearfix= link_to small_image(product), product - = link_to product.name, product diff --git a/app/views/enterprises/show.html.haml b/app/views/enterprises/show.html.haml deleted file mode 100644 index 8e5035dbcc..0000000000 --- a/app/views/enterprises/show.html.haml +++ /dev/null @@ -1,18 +0,0 @@ -- if @enterprise != current_distributor - %h2= @enterprise.name - -.enterprise-description= @enterprise.long_description.andand.html_safe - -- if current_distributor - = render :template => 'spree/products/index' - -- else - %h3 Hubs that distribute our products - %p.hint Select a hub to start shopping: - - %ul#supplier-distributors - - if @distributors.delete @enterprise - %li= link_to "Buy direct from the farm", enterprise_shop_path(@enterprise), {class: distributor_link_class(@enterprise)} - - - @distributors.each do |distributor| - %li= render partial: "shared/distributor", object: distributor diff --git a/app/views/enterprises/suppliers.html.haml b/app/views/enterprises/suppliers.html.haml deleted file mode 100644 index c67a7bc987..0000000000 --- a/app/views/enterprises/suppliers.html.haml +++ /dev/null @@ -1,12 +0,0 @@ -- content_for :sidebar do - %div{'data-hook' => "homepage_sidebar_navigation"} - = render 'spree/sidebar' - - -%h1 Suppliers - -= cms_page_content(:content, Cms::Page.find_by_full_path('/enterprises/suppliers')) - -%ul.enterprises - - @suppliers.each do |supplier| - %li= link_to supplier.name, supplier diff --git a/app/views/groups/index.html.haml b/app/views/groups/index.html.haml index 710039c7fb..537a81cc26 100644 --- a/app/views/groups/index.html.haml +++ b/app/views/groups/index.html.haml @@ -1,20 +1,23 @@ -= inject_enterprises +- content_for(:title) do + Groups + += inject_enterprises :javascript angular.module('Darkswarm').value('groups', #{render partial: "json/groups", object: @groups}) -#groups.pad-top{"ng-controller" => "GroupsCtrl"} +#groups.pad-top.footer-pad{"ng-controller" => "GroupsCtrl"} #active-table-search.row.pad-top .small-12.columns %h1 Groups / regions %p - %input.animate-show{type: :text, + %input{type: :text, "ng-model" => "query", placeholder: "Search name or keyword", "ng-debounce" => "150", "ofn-disable-enter" => true} - .group{"ng-repeat" => "group in groups = (Groups.groups | groups:query | orderBy:order)", + .group.animate-repeat{"ng-repeat" => "group in groups = (Groups.groups | groups:query | orderBy:order)", name: "group{{group.id}}", id: "group{{group.id}}"} .row.pad-top{bindonce: true} diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index 80101d9e8c..907926f8b7 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -1,3 +1,6 @@ +- content_for(:title) do + = @group.name + -# inject all enterprises as "enterprises" -# it could be more efficient to inject only the enterprises that are related to the group = inject_enterprises @@ -6,7 +9,7 @@ -# further hubs and producers of these enterprises can't be resolved within this small subset = inject_group_enterprises -#group-page.row.pad-top{"ng-controller" => "GroupPageCtrl"} +#group-page.row.pad-top.footer-pad{"ng-controller" => "GroupPageCtrl"} .small-12.columns.pad-top %header .row diff --git a/app/views/groups/signup.html.haml b/app/views/groups/signup.html.haml new file mode 100644 index 0000000000..c1c885d0f0 --- /dev/null +++ b/app/views/groups/signup.html.haml @@ -0,0 +1,61 @@ +- content_for(:title) do + Sign up as a group + +#panes + #shops-signup.pane + .row.header + .small-12.medium-12.columns.text-center + %h2 Groups sign up + .row.content + .small-12.medium-6.medium-offset-3.columns.text-center + %p.text-big We're an amazing platform for collaborative marketing, the easiest way for your members and stakeholders to reach new markets. We're non-profit, affordable, and simple. + %br + %a.button.transparent{href: "hello@openfoodnetwork.org?subject=I'd%20like%20to%20talk%20to%20you%20about%20groups%20on%20the%20Open%20Food%20Network".reverse, target: '_blank', mailto: true} + Email us + + .groups-details.pane + .row + .small-12.medium-8.medium-offset-2.columns + %h3.text-center We transform food systems fairly. + %p.text-big It's why we get out of bed every day. We're a global non-profit, based on open source code. We play fair. You can always trust us. + %p.text-big We know you have big ideas, and we want to help. We'll share our knowledge, networks and resources. We know that isolation doesn't create change, so we'll partner with you. + %br + %h3.text-center We meet you where you are. + %p.text-big You might be an alliance of food hubs, producers, or distributors, and an industry body, or a local government. + %p.text-big Whatever your role in your local food movement, we're ready to help. However you come to wonder what Open Food Network would look like or is doing in your part of the world, let's start the conversation. + %br + %h3.text-center We make food movements make more sense. + %p.text-big You need to activate and enable your networks, we offer a platform for conversation and action. You need real engagement. We’ll help reach all the players, all the stakeholders, all the sectors. + %p.text-big You need resourcing. We’ll bring all our experience to bear. You need cooperation. We’ll better connect you to a global network of peers. + .pane + .row + .small-12.medium-10.medium-offset-1.columns.text-center + %h2 Group Account + -# %p.text-big + -# / If there is a time-sensitive offer you can write it here, e.g. + -# Time-sensitive offer goes here! + %br + = ContentConfig.group_signup_pricing_table_html.html_safe + + #shops-case-studies + .row + .small-12.medium-10.medium-offset-1.columns + %h2.text-center Case studies + %br + = ContentConfig.group_signup_case_studies_html.html_safe + + .pane#cta + .row + .small-12.medium-6.medium-offset-3.columns.text-center + %h2 Ready to discuss? + %p.text-big Get in touch to discover what OFN can do for you: + %a.button.transparent{href: "hello@openfoodnetwork.org?subject=I'd%20like%20to%20talk%20to%20you%20about%20groups%20on%20the%20Open%20Food%20Network".reverse, target: '_blank', mailto: true} + Email us + + #hub-details.pane.footer-pad + .row + .small-12.medium-10.medium-offset-1.columns + %h2.text-center Here's the detail. + = ContentConfig.group_signup_detail_html.html_safe + += render partial: "shared/footer" diff --git a/app/views/home/_beta.en-GB.html.haml b/app/views/home/_beta.en-GB.html.haml deleted file mode 100644 index 2badc7c826..0000000000 --- a/app/views/home/_beta.en-GB.html.haml +++ /dev/null @@ -1,13 +0,0 @@ -#beta.pane - - .row - .small-12.columns.text-center - %h2 S'cuse us - %h5 while we get (more) awesome - %p Open Food Network UK is a new service that’s being piloted right now! - %p Want to help? Or find out when OFN is coming to you? - %strong We’d love to hear from you: - %p - %a{href: "hello@openfoodnetwork.org".reverse, target: '_blank', mailto: true} Food buyers - | - %a{href: "hello@openfoodnetwork.org".reverse, target: '_blank', mailto: true} Food producers & farmers diff --git a/app/views/home/_beta.html.haml b/app/views/home/_beta.html.haml deleted file mode 100644 index af72211862..0000000000 --- a/app/views/home/_beta.html.haml +++ /dev/null @@ -1,13 +0,0 @@ -#beta.pane - - .row - .small-12.columns.text-center - %h2 S'cuse us - %h5 while we get (more) awesome - %p Open Food Network (beta) is a new service that’s being built right now! Our food producers are currently based around Melbourne and Victoria, and we hope to expand OFN nationally very soon. - %p Want to help? Or find out when OFN is coming to you? - %strong We’d love to hear from you: - %p - %a{href: "hello@openfoodnetwork.org".reverse, target: '_blank', mailto: true} Food buyers - | - %a{href: "hello@openfoodnetwork.org".reverse, target: '_blank', mailto: true} Food producers & farmers \ No newline at end of file diff --git a/app/views/home/_brandstory.html.haml b/app/views/home/_brandstory.html.haml new file mode 100644 index 0000000000..40e5b77bf4 --- /dev/null +++ b/app/views/home/_brandstory.html.haml @@ -0,0 +1,18 @@ +#brand-story.pane + .row + .small-12.medium-8.medium-offset-2.columns.text-center + %h2 Food, unincorporated. + %p Sometimes the best way to fix the system is to start a new one… + + #brand-story-text.hide-show.slideable + %p We begin from the ground up. With farmers and growers ready to tell their stories proudly and truly. With distributors ready to connect people with products fairly and honestly. With buyers who believe that better weekly shopping decisions can seriously change the world. + %p Then we need a way to make it real. A way to empower everyone who grows, sells and buys food. A way to tell all the stories, to handle all the logistics. A way to turn transaction into transformation every day. + %p So we build an online marketplace that levels the playing field. It’s transparent, so it creates real relationships. It’s open source, so it’s owned by everyone. It scales to regions and nations, so people start versions across the world. + %p It works everywhere. It changes everything. + %p + %strong We call it Open Food Network. + %p We all love food. Now we can love our food system too. + + %a.text-vbig{"slide-toggle" => "#brand-story-text", "ng-click" => "toggleBrandStory()"} + %i.ofn-i_005-caret-down{"ng-hide" => "brandStoryExpanded"} + %i.ofn-i_006-caret-up{ "ng-show" => "brandStoryExpanded"} diff --git a/app/views/home/_cta.html.haml b/app/views/home/_cta.html.haml new file mode 100644 index 0000000000..ab95bae95d --- /dev/null +++ b/app/views/home/_cta.html.haml @@ -0,0 +1,7 @@ +#cta.pane + .row + .small-12.columns.text-center + %h2 Shopping that makes the world a better place. + %br + %a.button.transparent{href: "/shops"} + I'm Ready diff --git a/app/views/home/_fat.html.haml b/app/views/home/_fat.html.haml index a5543fa2a3..8d857d03f7 100644 --- a/app/views/home/_fat.html.haml +++ b/app/views/home/_fat.html.haml @@ -1,21 +1,21 @@ -.row.active_table_row{"ng-show" => "open()", "ng-click" => "toggle($event)", "ng-class" => "{'open' : !ofn-i_032-closed-sign()}"} +.row.active_table_row{"ng-show" => "open()", "ng-click" => "toggle($event)", "ng-class" => "{'open' : !ofn-i_032-closed-sign()}", bindonce: true} .columns.small-12.medium-6.large-5.fat %div{"bo-if" => "hub.taxons"} %label Shop for .trans-sentence %span.fat-taxons{"ng-repeat" => "taxon in hub.taxons"} %render-svg{path: "{{taxon.icon}}"} - %span{"bo-text" => "taxon.name"} + %span{"bo-text" => "taxon.name"} %div.show-for-medium-up{"bo-if" => "hub.taxons.length==0"}   .columns.small-12.medium-3.large-2.fat %div{"bo-if" => "hub.pickup || hub.delivery"} %label Delivery options %ul.small-block-grid-2.medium-block-grid-1.large-block-grid-1 - %li.pickup{"bo-if" => "hub.pickup"} + %li.pickup{"bo-if" => "hub.pickup"} %i.ofn-i_038-takeaway Pickup - %li.delivery{"bo-if" => "hub.delivery"} + %li.delivery{"bo-if" => "hub.delivery"} %i.ofn-i_039-delivery Delivery .columns.small-12.medium-3.large-5.fat diff --git a/app/views/home/_filters.html.haml b/app/views/home/_filters.html.haml index 6b339c13c9..21719b04b7 100644 --- a/app/views/home/_filters.html.haml +++ b/app/views/home/_filters.html.haml @@ -1,22 +1,21 @@ .row - -# = render partial: 'shared/components/filter_controls' - .small-12.medium-6.columns   + = render partial: 'shared/components/filter_controls' + -# .small-12.medium-6.columns   = render partial: 'shared/components/show_profiles' --# .row.animate-show{"ng-show" => "filtersActive"} --# .small-12.columns --# .row.filter-box --# .small-12.large-9.columns --# %h5.tdhead --# .light Filter by --# Type --# %ul.small-block-grid-2.medium-block-grid-4.large-block-grid-5 --# %filter-selector{objects: "Enterprises.hubs | searchEnterprises:query | taxonsOf", "active-selectors" => "activeTaxons"} --# .small-12.large-3.columns --# %h5.tdhead --# .light Filter by --# Delivery --# %ul.small-block-grid-2.medium-block-grid-4.large-block-grid-2 --# %shipping-type-selector{results: "shippingTypes"} --# --# = render partial: 'shared/components/filter_box' +.row.animate-show{"ng-show" => "filtersActive"} + .small-12.columns + .row.filter-box + .small-12.large-9.columns + %h5.tdhead + .light Filter by + Type + %filter-selector.small-block-grid-2.medium-block-grid-4.large-block-grid-5{ objects: "visibleMatches | visible | taxonsOf", "active-selectors" => "activeTaxons" } + .small-12.large-3.columns + %h5.tdhead + .light Filter by + Delivery + %ul.small-block-grid-2.medium-block-grid-4.large-block-grid-2 + %shipping-type-selector{results: "shippingTypes"} + += render partial: 'shared/components/filter_box' diff --git a/app/views/home/_groups.html.haml b/app/views/home/_groups.html.haml deleted file mode 100644 index 5d76dd1e2d..0000000000 --- a/app/views/home/_groups.html.haml +++ /dev/null @@ -1,10 +0,0 @@ -#groups.pane - - .row - .small-12.columns.text-center - %h2 Groups / Regions - %h5 See all the groups & regions on the Open Food Network - %p - %a.neutral-btn.light{href: "/groups"} - %i.ofn-i_035-groups - View groups & regions \ No newline at end of file diff --git a/app/views/home/_hubs.html.haml b/app/views/home/_hubs.html.haml index 1e5151469e..40074ce0bb 100644 --- a/app/views/home/_hubs.html.haml +++ b/app/views/home/_hubs.html.haml @@ -1,22 +1,26 @@ -= inject_enterprises -#hubs.hubs{"ng-controller" => "EnterprisesCtrl"} += inject_enterprises + +#hubs.hubs{"ng-controller" => "EnterprisesCtrl", "ng-cloak" => true} .row .small-12.columns - %h1 Shop in your local area + %h1{"scroll-after-load" => (spree_current_user ? true : nil)} Shop in your local area - = render partial: "shared/components/enterprise_search" - = render partial: "home/filters" + = render "shared/components/enterprise_search" + = render "home/filters" - .row{bindonce: true} + .row .small-12.columns - .active_table - %hub.active_table_node.row.animate-repeat{"ng-repeat" => "hub in filteredEnterprises = (Enterprises.hubs | visible | searchEnterprises:query | taxons:activeTaxons | shipping:shippingTypes | showHubProfiles:show_profiles | orderBy:['-active', '+orders_close_at'])", - "ng-class" => "{'is_profile' : hub.category == 'hub_profile', 'closed' : !open(), 'open' : open(), 'inactive' : !hub.active, 'current' : current()}", - "scroll-after-load" => true, - "ng-controller" => "HubNodeCtrl", - id: "{{hub.hash}}"} - .small-12.columns - = render partial: 'home/skinny' - = render partial: 'home/fat' + .name-matches{"ng-show" => "nameMatchesFiltered.length > 0"} + %h2 Did you mean? + = render "home/hubs_table", enterprises: "nameMatches" - = render partial: 'shared/components/enterprise_no_results' + .distance-matches{"ng-if" => "nameMatchesFiltered.length == 0 || distanceMatchesShown"} + %h2{"ng-show" => "nameMatchesFiltered.length > 0 || query.length > 0"} + Closest to + %span{"ng-show" => "nameMatchesFiltered.length > 0"} {{ nameMatchesFiltered[0].name }}... + %span{"ng-hide" => "nameMatchesFiltered.length > 0"} {{ query }}... + + = render "home/hubs_table", enterprises: "distanceMatches" + + .show-distance-matches{"ng-show" => "nameMatchesFiltered.length > 0 && !distanceMatchesShown"} + %a{href: "", "ng-click" => "showDistanceMatches()"} Show me shops near {{ nameMatchesFiltered[0].name }} diff --git a/app/views/home/_hubs_table.html.haml b/app/views/home/_hubs_table.html.haml new file mode 100644 index 0000000000..8842079f56 --- /dev/null +++ b/app/views/home/_hubs_table.html.haml @@ -0,0 +1,10 @@ +.active_table + %hub.active_table_node.row{"ng-repeat" => "hub in #{enterprises}Filtered = (#{enterprises} | visible | taxons:activeTaxons | shipping:shippingTypes | showHubProfiles:show_profiles | orderBy:['-active', '+distance', '+orders_close_at'])", + "ng-class" => "{'is_profile' : hub.category == 'hub_profile', 'closed' : !open(), 'open' : open(), 'inactive' : !hub.active, 'current' : current()}", + "ng-controller" => "HubNodeCtrl", + id: "{{hub.hash}}"} + .small-12.columns + = render 'home/skinny' + = render 'home/fat' + + = render 'shared/components/enterprise_no_results', enterprises: "#{enterprises}Filtered" diff --git a/app/views/home/_map.html.haml b/app/views/home/_map.html.haml deleted file mode 100644 index 5a89dd181e..0000000000 --- a/app/views/home/_map.html.haml +++ /dev/null @@ -1,10 +0,0 @@ -#map.pane - - .row - .small-12.columns.text-center - %h2 Map - %h5 of all our food hubs and producers - %p - %a.neutral-btn.light{href: "/map"} - %i.ofn-i_037-map - View map \ No newline at end of file diff --git a/app/views/home/_producer-register.html.haml b/app/views/home/_producer-register.html.haml deleted file mode 100644 index 62845bd9ea..0000000000 --- a/app/views/home/_producer-register.html.haml +++ /dev/null @@ -1,75 +0,0 @@ -#producers.pane - - .row - .small-12.columns.text-center - %h2 - = t :producers - %h5 Want to join the Open Food Network? - %br - %a.neutral-btn.turquoise{href: "/register"} - Register now - %i.ofn-i_007-caret-right - / .row - / .small-12.medium-4.columns.text-center - / %ul.pricing-table - / %li.title Profile - / %li.price Always free - / %li.description Help people find and contact you on OFN - / %li.bullet-item - / %i.ofn-i_019-map-pin - / Pin on OFN Map - / %li.bullet-item - / %i.ofn-i_044-facebook - / Share your contact and social info - / %li.cta-button - / %a.neutral-btn.turquoise{:href => "/register"} - / Register now - / %i.ofn-i_007-caret-right - - / .small-12.medium-4.columns.text-center - / %ul.pricing-table - / %li.title Supplier - / %li.price Always free - / %li.description Sell your products through existing OFN shopfronts - / %li.bullet-item - / %i.ofn-i_019-map-pin - / Pin on OFN Map - / %li.bullet-item - / %i.ofn-i_044-facebook - / Share your contact and social info - / %li.bullet-item - / %i.ofn-i_067-shop - / Create and manage products - / %li.cta-button - / %a.neutral-btn.turquoise{:href => "/register"} - / Register now - / %i.ofn-i_007-caret-right - - / .small-12.medium-4.columns.text-center - / %ul.pricing-table - / %li.title Shopfront - / %li.price $200 setup fee - / %li.description + Sliding monthly fee of $5-$50/month - / %li.bullet-item - / %i.ofn-i_019-map-pin - / Pin on OFN Map - / %li.bullet-item - / %i.ofn-i_044-facebook - / Share your contact and social info - / %li.bullet-item - / %i.ofn-i_067-shop - / Create and manage products - / %li.bullet-item - / %i.ofn-i_051-check-big - / Create and manage order cycles - / %li.bullet-item - / %i.ofn-i_051-check-big - / Sell your products on Shopfront - / %li.cta-button - / %a.neutral-btn.turquoise{:href => "/register"} - / Register now - / %i.ofn-i_007-caret-right - - - - \ No newline at end of file diff --git a/app/views/home/_producers.html.haml b/app/views/home/_producers.html.haml deleted file mode 100644 index 3d4fcfbd68..0000000000 --- a/app/views/home/_producers.html.haml +++ /dev/null @@ -1,10 +0,0 @@ -#producers.pane - - .row - .small-12.columns.text-center - %h2 Producers - %h5 Looking for a specific producer or farmer? - %p - %a.neutral-btn.turquoise{href: "/producers"} - %i.ofn-i_036-producers - View all producers \ No newline at end of file diff --git a/app/views/home/_skinny.html.haml b/app/views/home/_skinny.html.haml index 9cf7a01c77..c7d91b676b 100644 --- a/app/views/home/_skinny.html.haml +++ b/app/views/home/_skinny.html.haml @@ -1,7 +1,6 @@ .row.active_table_row{"ng-if" => "hub.is_distributor", "ng-click" => "toggle($event)", "ng-class" => "{'closed' : !open(), 'is_distributor' : producer.is_distributor}", bindonce: true} - - .columns.small-12.medium-6.large-5.skinny-head - %a.hub{"bo-href" => "hub.path", "ng-class" => "{primary: hub.active, secondary: !hub.active}", "ofn-empties-cart" => "hub"} + .columns.small-12.medium-5.large-5.skinny-head + %a.hub{"bo-href" => "hub.path", "ng-class" => "{primary: hub.active, secondary: !hub.active}", "ofn-change-hub" => "hub", "data-is-link" => "true"} %i{bo: {class: "hub.icon_font"}} %span.margin-top.hub-name-listing{"bo-bind" => "hub.name | truncate:40"} @@ -9,23 +8,28 @@ %span.margin-top{"bo-text" => "hub.address.city"} .columns.small-2.medium-1.large-1 %span.margin-top{"bo-bind" => "hub.address.state_name | uppercase"} + %span.margin-top{"ng-if" => "hub.distance != null && hub.distance > 0"} ({{ hub.distance / 1000 | number:0 }} km) - .columns.small-6.medium-3.large-4.text-right{"bo-if" => "hub.active"} - %a.hub.open_closed{"bo-href" => "hub.path", "ng-class" => "{primary: hub.active, secondary: !hub.active}", "ofn-empties-cart" => "hub"} + .columns.small-4.medium-3.large-3.text-right{"bo-if" => "hub.active"} + %a.hub.open_closed{"bo-href" => "hub.path", "ng-class" => "{primary: hub.active, secondary: !hub.active}", "ofn-change-hub" => "hub"} %i.ofn-i_033-open-sign %span.margin-top{ bo: { if: "current()" } } %em Shopping here %span.margin-top{ bo: { if: "!current()" } } %span{"bo-bind" => "hub.orders_close_at | sensible_timeframe"} - .columns.small-6.medium-3.large-4.text-right{"bo-if" => "!hub.active"} - %a.hub.open_closed{"bo-href" => "hub.path", "ng-class" => "{primary: hub.active, secondary: !hub.active}", "ofn-empties-cart" => "hub"} + .columns.small-4.medium-3.large-3.text-right{"bo-if" => "!hub.active"} + %a.hub.open_closed{"bo-href" => "hub.path", "ng-class" => "{primary: hub.active, secondary: !hub.active}", "ofn-change-hub" => "hub"} %i.ofn-i_032-closed-sign %span.margin-top{ bo: { if: "current()" } } %em Shopping here %span.margin-top{ bo: { if: "!current()" } } Orders closed -.row.active_table_row{"ng-if" => "!hub.is_distributor", "ng-class" => "closed"} + .columns.small-2.medium-1.large-1.text-right + %span.margin-top + %i{"ng-class" => "{'ofn-i_005-caret-down' : !open(), 'ofn-i_006-caret-up' : open()}"} + +.row.active_table_row{"ng-if" => "!hub.is_distributor", "ng-class" => "closed", bindonce: true} .columns.small-12.medium-6.large-5.skinny-head %a.hub{"ng-click" => "openModal(hub)", "ng-class" => "{primary: hub.active, secondary: !hub.active}"} %i{ng: {class: "hub.icon_font"}} diff --git a/app/views/home/_stats.html.haml b/app/views/home/_stats.html.haml new file mode 100644 index 0000000000..6c686a22b7 --- /dev/null +++ b/app/views/home/_stats.html.haml @@ -0,0 +1,23 @@ +#stats.pane + .row.header + .small-12.medium-8.medium-offset-2.columns.text-center + %h2 We're creating a new food system. + + .row.content + - if ContentConfig.home_show_stats + .small-12.medium-3.columns.text-center + %h4 + %strong= number_with_delimiter @num_producers + food producers + .small-12.medium-3.columns.text-center + %h4 + %strong= number_with_delimiter @num_distributors + food shops + .small-12.medium-3.columns.text-center + %h4 + %strong= number_with_delimiter @num_users + food shoppers + .small-12.medium-3.columns.text-center + %h4 + %strong= number_with_delimiter @num_orders + food orders diff --git a/app/views/home/_system.html.haml b/app/views/home/_system.html.haml new file mode 100644 index 0000000000..ed32e305f4 --- /dev/null +++ b/app/views/home/_system.html.haml @@ -0,0 +1,23 @@ +#system.pane + .row + .small-12.medium-12.large-8.large-offset-2.columns.text-center + %h2 Here's how it works. + .row + .small-12.medium-4.columns.text-left + .home-icon-box + %a.search{href: "/shops"} + .home-icon-box-bottom + %h4 1. Search + %p.text-normal Search our diverse, independent shops for seasonal local food. Search by neighbourhood and food category, or whether you prefer delivery or pickup. + .small-12.medium-4.columns.text-left + .home-icon-box + %a.shop{href: "/shops"} + .home-icon-box-bottom + %h4 2. Shop + %p.text-normal Transform your transactions with affordable local food from diverse producers and hubs. Know the stories behind your food and the people who make it! + .small-12.medium-4.columns.text-left + .home-icon-box + %a.pick-up-delivery{href: "/shops"} + .home-icon-box-bottom + %h4 3. Pick-up / Delivery + %p.text-normal Hang on for your delivery, or visit your producer or hub for a more personal connection with your food. Food shopping as diverse as nature intended it. diff --git a/app/views/home/about_us.html.haml b/app/views/home/about_us.html.haml deleted file mode 100644 index 79e4a657a7..0000000000 --- a/app/views/home/about_us.html.haml +++ /dev/null @@ -1,8 +0,0 @@ -.row - .large-12.columns - %h2 What is open food network - %p - Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna - aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. - Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint - occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum \ No newline at end of file diff --git a/app/views/home/index.html.haml b/app/views/home/index.html.haml index a15442e794..ee4fca7b27 100644 --- a/app/views/home/index.html.haml +++ b/app/views/home/index.html.haml @@ -1,24 +1,25 @@ -#tagline - .row - .small-12.text-center.columns - %h1 - %img{src: "/assets/open-food-network-beta.png", srcset: "/assets/open-food-network-beta.svg", width: "550", height: "134", title: "Open Food Network (beta)"} - %h2 An open marketplace that makes it easy to find, buy, sell and move sustainable local food. - - %ofn-modal{title: "Learn more", "ng-cloak" => true} - = render partial: "modals/learn_more" +:css + #tagline:before { background-image: url("#{ContentConfig.home_hero.url}") } -.ng-cloak - = render partial: "home/hubs" - / = render partial: "home/map" +%div{"ng-controller" => "HomeCtrl"} + = render partial: "shared/menu/alert" - / = render partial: "home/producers" + #tagline + .row + .small-12.text-center.columns + %h1 + / TODO: Rohan - logo asset & width is content manageable: + %img{src: "/assets/logo-white-notext.png", width: "250", title: Spree::Config.site_name} + %br/ + %a.button.transparent{href: "/shops"} + Shop Now - / = render partial: "home/groups" -= render partial: "home/producer-register" + #panes + = render partial: "home/brandstory" + = render partial: "home/system" + = render partial: "home/cta" + = render partial: "home/stats" -= render partial: "home/beta" - -= render partial: "shared/footer" + = render partial: "shared/footer" diff --git a/app/views/layouts/darkswarm.html.haml b/app/views/layouts/darkswarm.html.haml index 5169efa942..33a3d6148a 100644 --- a/app/views/layouts/darkswarm.html.haml +++ b/app/views/layouts/darkswarm.html.haml @@ -3,16 +3,16 @@ %meta{charset: 'utf-8'}/ %meta{name: 'viewport', content: "width=device-width,initial-scale=1.0"}/ - %title= content_for?(:title) ? yield(:title) : 'Welcome to Open Food Network' + %title= content_for?(:title) ? "#{yield(:title)} - Open Food Network".html_safe : 'Welcome to Open Food Network' - if Rails.env.production? = favicon_link_tag - else = favicon_link_tag "/favicon-staging.ico" - %link{href: "https://fonts.googleapis.com/css?family=Open+Sans:400,700", rel: "stylesheet", type: "text/css"}/ + %link{href: "https://fonts.googleapis.com/css?family=Roboto:400,300italic,400italic,300,700,700italic|Oswald:300,400,700", rel: "stylesheet", type: "text/css"} = yield :scripts - %script{src: "//maps.googleapis.com/maps/api/js?libraries=places&sensor=false"} - = stylesheet_link_tag "darkswarm/all" + %script{src: "//maps.googleapis.com/maps/api/js?libraries=places,geometry&sensor=false"} + = split_stylesheet_link_tag "darkswarm/all" = javascript_include_tag "darkswarm/all" @@ -34,7 +34,7 @@ .off-canvas-wrap{offcanvas: true} .inner-wrap - = render partial: "shared/menu/menu" + = render "shared/menu/menu" %section{ role: "main" } = yield diff --git a/app/views/layouts/mailer.html.haml b/app/views/layouts/mailer.html.haml index 9042c91213..39f677b890 100644 --- a/app/views/layouts/mailer.html.haml +++ b/app/views/layouts/mailer.html.haml @@ -15,7 +15,7 @@ %table{:bgcolor => "#f2f2f2"} %tr %td - %img{:src => "#{ asset_path 'open-food-network-beta-black.png' }", :width => "200", :height => "49"}/ + %img{:src => "#{ asset_path 'logo-color.png' }", :width => "144", :height => "50"}/ %td{:align => "right"} %h6.collapse Open Food Network diff --git a/app/views/layouts/registration.html.haml b/app/views/layouts/registration.html.haml index d6122cdc3b..c0e0c4b7ec 100644 --- a/app/views/layouts/registration.html.haml +++ b/app/views/layouts/registration.html.haml @@ -3,12 +3,12 @@ %meta{charset: 'utf-8'}/ %meta{name: 'viewport', content: "width=device-width,initial-scale=1.0"}/ - %title= content_for?(:title) ? yield(:title) : 'Welcome to Open Food Network' + %title= content_for?(:title) ? "#{yield(:title)} - Open Food Network" : 'Welcome to Open Food Network' - if Rails.env.production? = favicon_link_tag - else = favicon_link_tag "/favicon-staging.ico" - %link{href: "https://fonts.googleapis.com/css?family=Open+Sans:400,700", rel: "stylesheet", type: "text/css"}/ + %link{href: "https://fonts.googleapis.com/css?family=Roboto:400,300italic,400italic,300,700,700italic|Oswald:300,400,700", rel: "stylesheet", type: "text/css"} = yield :scripts %script{src: "//maps.googleapis.com/maps/api/js?libraries=places&sensor=false"} @@ -19,7 +19,7 @@ = render "layouts/bugherd_script" = csrf_meta_tags - %body.off-canvas{"ng-app" => "Darkswarm", style: 'background-image: url("/assets/home/ofn_bg_1.jpg")' } + %body.off-canvas{"ng-app" => "Darkswarm", style: 'background-image: url("/assets/tile-wide.png")' } / [if lte IE 8] = render partial: "shared/ie_warning" = javascript_include_tag "iehack" diff --git a/app/views/map/index.html.haml b/app/views/map/index.html.haml index ce4052be57..30df47e28c 100644 --- a/app/views/map/index.html.haml +++ b/app/views/map/index.html.haml @@ -1,8 +1,11 @@ -= inject_enterprises +- content_for(:title) do + Map + += inject_enterprises .map-container{"fill-vertical" => true} %map{"ng-controller" => "MapCtrl"} %google-map{options: "map.additional_options", center: "map.center", zoom: "map.zoom", styles: "map.styles", draggable: "true"} %map-search - %markers{models: "OfnMap.enterprises", fit: "true", - coords: "'self'", icon: "'icon'", click: "'reveal'"} + %markers{models: "OfnMap.enterprises", fit: "true", + coords: "'self'", icon: "'icon'", click: "'reveal'"} diff --git a/app/views/producers/_fat.html.haml b/app/views/producers/_fat.html.haml index 99099a31f8..06d73666af 100644 --- a/app/views/producers/_fat.html.haml +++ b/app/views/producers/_fat.html.haml @@ -1,5 +1,5 @@ .row.active_table_row{"ng-if" => "open()", "ng-click" => "toggle($event)", "ng-class" => "{'open' : !ofn-i_032-closed-sign()}"} - + .columns.small-12.medium-7.large-7.fat / Will add in long description available once clean up HTML formatting producer.long_description %div{"bo-if" => "producer.description"} @@ -10,23 +10,23 @@ %label   .columns.small-12.medium-5.large-5.fat - + %div{"bo-if" => "producer.supplied_taxons"} %label Shop for %p.trans-sentence %span.fat-taxons{"ng-repeat" => "taxon in producer.supplied_taxons"} %render-svg{path: "{{taxon.icon}}"} %span{"bo-text" => "taxon.name"} - + %div.show-for-medium-up{"ng-if" => "producer.supplied_taxons.length==0"}   %div{"bo-if" => "producer.email || producer.website || producer.phone"} %label Contact - + %p.word-wrap{"bo-if" => "producer.phone"} - Call - %span{"bo-text" => "producer.phone"} + Call + %span{"bo-text" => "producer.phone"} %p.word-wrap{"bo-if" => "producer.email"} %a{"bo-href" => "producer.email | stripUrl", target: "_blank", mailto: true} @@ -39,20 +39,20 @@ %div{"bo-if" => "producer.twitter || producer.facebook || producer.linkedin || producer.instagram"} %label Follow .follow-icons{bindonce: true} - %span{"bo-if" => "producer.twitter"} + %span{"bo-if" => "producer.twitter"} %a{"bo-href-i" => "http://twitter.com/{{producer.twitter}}", target: "_blank"} %i.ofn-i_041-twitter - + %span{"bo-if" => "producer.facebook"} %a{"bo-href-i" => "http://{{producer.facebook | stripUrl}}", target: "_blank"} %i.ofn-i_044-facebook - + %span{"bo-if" => "producer.linkedin"} %a{"bo-href-i" => "http://{{producer.linkedin | stripUrl}}", target: "_blank"} %i.ofn-i_042-linkedin - + %span{"bo-if" => "producer.instagram"} - %a{"bo-href-i" => "http://instagram.com/{{producer.instagram}}", target: "_blank"} + %a{"bo-href-i" => "http://instagram.com/{{producer.instagram}}", target: "_blank"} %i.ofn-i_043-instagram .row.active_table_row.pad-top{"ng-if" => "open()", "bo-if" => "producer.hubs"} @@ -60,19 +60,18 @@ .row .columns.small-12.fat %div{"bo-if" => "producer.name"} - %label + %label Shop for - %span.turquoise{"bo-text" => "producer.name"} + %span.turquoise{"bo-text" => "producer.name"} products at: %div.show-for-medium-up{"bo-if" => "!producer.name"}   .row.cta-container .columns.small-12 - %a.cta-hub{"ng-repeat" => "hub in producer.hubs | orderBy:'-active'", - "bo-href" => "hub.path", "ofn-empties-cart" => "hub", + %a.cta-hub{"ng-repeat" => "hub in producer.hubs | visible | orderBy:'-active'", + "bo-href" => "hub.path", "ofn-change-hub" => "hub", "bo-class" => "{primary: hub.active, secondary: !hub.active}"} %i.ofn-i_033-open-sign{"bo-if" => "hub.active"} %i.ofn-i_032-closed-sign{"bo-if" => "!hub.active"} .hub-name{"bo-text" => "hub.name"} .button-address{"bo-bind" => "[hub.address.city, hub.address.state_name] | printArray"} - diff --git a/app/views/producers/_filters.html.haml b/app/views/producers/_filters.html.haml index ec23c05b92..103b31d531 100644 --- a/app/views/producers/_filters.html.haml +++ b/app/views/producers/_filters.html.haml @@ -1,15 +1,14 @@ --# .row --# = render partial: 'shared/components/filter_controls' --# .small-12.medium-6.columns.text-right --#   --# --# .row.animate-show{"ng-show" => "filtersActive"} --# .small-12.columns --# .row.filter-box --# .small-12.columns --# %h5.tdhead --# .light Filter by --# Type --# %ul.small-block-grid-2.medium-block-grid-4.large-block-grid-6 --# %filter-selector{objects: "Enterprises.producers | searchEnterprises:query | taxonsOf", "active-selectors" => "activeTaxons"} --# = render partial: 'shared/components/filter_box' +.row + = render partial: 'shared/components/filter_controls' + .small-12.medium-6.columns.text-right +   + +.row.animate-show{"ng-show" => "filtersActive"} + .small-12.columns + .row.filter-box + .small-12.columns + %h5.tdhead + .light Filter by + Type + %filter-selector.small-block-grid-2.medium-block-grid-4.large-block-grid-6{objects: "Enterprises.producers | searchEnterprises:query | taxonsOf", "active-selectors" => "activeTaxons"} + = render partial: 'shared/components/filter_box' diff --git a/app/views/producers/index.html.haml b/app/views/producers/index.html.haml index e3ae9815e2..cb9c3d3826 100644 --- a/app/views/producers/index.html.haml +++ b/app/views/producers/index.html.haml @@ -1,6 +1,9 @@ +- content_for(:title) do + Producers + = inject_enterprises -.producers.pad-top{"ng-controller" => "EnterprisesCtrl", "ng-cloak" => true} +.producers{"ng-controller" => "EnterprisesCtrl", "ng-cloak" => true} .row .small-12.columns.pad-top %h1 Find local producers @@ -12,7 +15,6 @@ .small-12.columns .active_table %producer.active_table_node.row.animate-repeat{id: "{{producer.path}}", - "scroll-after-load" => true, "ng-repeat" => "producer in filteredEnterprises = (Enterprises.producers | visible | searchEnterprises:query | taxons:activeTaxons)", "ng-controller" => "ProducerNodeCtrl", "ng-class" => "{'closed' : !open(), 'open' : open(), 'inactive' : !producer.active}", diff --git a/app/views/producers/signup.html.haml b/app/views/producers/signup.html.haml new file mode 100644 index 0000000000..169d4b179f --- /dev/null +++ b/app/views/producers/signup.html.haml @@ -0,0 +1,46 @@ +- content_for(:title) do + Sign up as a producer + +#panes + #producer-signup.pane + .row.header + .small-12.medium-12.columns.text-center + %h2 Food producers, empowered. + .row.content + .small-12.medium-6.medium-offset-3.columns.text-center + %p.text-big Sell your food and tell your stories to diverse new markets. Save time and money on every overhead. We support innovation without the risk. We've levelled the playing field. + %br + %a.button.transparent{href: "/register"} + Join now + .pane + .row + .small-12.medium-10.medium-offset-1.columns.text-center + %h2 Enterprise Accounts + -# %p.text-big + -# If there is a time-sensitive offer you can write it here, e.g. + -# Sign up before 30th June for an extra month free! + %br + = ContentConfig.producer_signup_pricing_table_html.html_safe + + #producer-case-studies + .row + .small-12.medium-10.medium-offset-1.columns + %h2.text-center Stories from our producers. + %br + = ContentConfig.producer_signup_case_studies_html.html_safe + + .pane#cta + .row + .small-12.medium-6.medium-offset-3.columns.text-center + %h2 Join now! + %p.text-big Start with a free profile, and expand when you're ready! + %a.button.transparent{href: "/register"} + Join now + + #producer-details.pane.footer-pad + .row + .small-12.medium-10.medium-offset-1.columns + %h2.text-center Here's the detail. + = ContentConfig.producer_signup_detail_html.html_safe + += render partial: "shared/footer" diff --git a/app/views/registration/index.html.haml b/app/views/registration/index.html.haml index d110e98657..a05c383395 100644 --- a/app/views/registration/index.html.haml +++ b/app/views/registration/index.html.haml @@ -1,3 +1,6 @@ +- content_for(:title) do + Register + = inject_spree_api_key = inject_available_countries = inject_enterprise_attributes diff --git a/app/views/shared/_case_study.html.haml b/app/views/shared/_case_study.html.haml new file mode 100644 index 0000000000..12e59c6b6c --- /dev/null +++ b/app/views/shared/_case_study.html.haml @@ -0,0 +1,6 @@ +.case-study + %img.case-study-img{src: img_src, width: "100", height: "100", title: title} + %h4= title + %p.text-small= description + %a{href: link, target: "_blank"} + %strong More diff --git a/app/views/shared/_footer.html.haml b/app/views/shared/_footer.html.haml index e06ae0f0aa..7450966e5f 100644 --- a/app/views/shared/_footer.html.haml +++ b/app/views/shared/_footer.html.haml @@ -1,60 +1,136 @@ %footer - .row.landing-page-row - .small-12.medium-4.columns.text-left - %h4 Email us - %p - %a{href: "hello@openfoodnetwork.org".reverse, target: '_blank', mailto: true} - %span.email - = "hello@openfoodnetwork.org".reverse - %h4 Follow us - %p - %a{title:'Follow us on Facebook', href: 'https://www.facebook.com/OpenFoodNet', target: '_blank'} - %i.ofn-i_044-facebook - Facebook    - %a{title:'Follow us on Twitter', href: 'https://twitter.com/OpenFoodNet', target: '_blank'} - %i.ofn-i_041-twitter - Twitter    - %a{title:'Join our group on LinkedIn', href: 'http://www.linkedin.com/groups/Open-Food-Foundation-4743336', target: '_blank'} - %i.ofn-i_042-linkedin - LinkedIn - .small-12.medium-3.columns.text-left - %h4 Getting around - %ul.bullet-list - %li - %a{href: "/shop"} Shop - %li - %a{href: "/map"} Map - %li - %a{href: "/producers"} Producers - %li - %a{href: "/groups"} Groups - .small-12.medium-2.columns.text-left - %h4 Producers - %p - = t :producers_join - %p - %a{href: "/register"} Register now - .small-12.medium-3.columns.text-left - %h4 About us - %p OFN is a network of independent online food stores that connect farmers and food hubs with individuals and local businesses. It gives farmers and food hubs an easier and fairer way to distribute their food. - .row.landing-page-row - .small-12.columns.text-center.pad-top - %hr - %h5.pad-top - %a{title: 'Open Food Network', href:'http://www.openfoodnetwork.org', target: '_blank' } openfoodnetwork.org - %br - © Copyright - = Date.today.year - Open Food Foundation - %p - %small - %a{href:"https://creativecommons.org/licenses/by-sa/3.0/", target: "_blank" } Creative Commons Attribution-ShareAlike 3.0 Unported (CC BY-SA 3.0) - %p - %small - %a{href:"/Terms-of-service.pdf", target: "_blank" } Site terms & conditions - | - %a{href:"https://github.com/openfoodfoundation/openfoodnetwork", target: "_blank" } Open Source & developer info on GitHub + .footer-global + .row + .small-12.columns.text-center + .logo + %img{src: "/assets/logo-white-notext.png", width: "120px"} + .row + .small-12.medium-8.medium-offset-2.columns.text-center + .alert-box + %a.big-alert{href: "http://www.openfoodnetwork.org", target: "_blank"} + %h6 + Interested in selling food on the Open Food Network?   + %strong Start here + %i.ofn-i_054-point-right + .row + .small-12.medium-4.medium-offset-2.columns.text-center + %h6 OFN Global + %p + %a{href: "http://www.openfoodnetwork.org", target: "_blank"} Home + %span | + %a{href: "http://www.openfoodnetwork.org/news/", target: "_blank"} News + %span | + %a{href: "http://www.openfoodnetwork.org/about/history-team/", target: "_blank"} About + %span | + %a{href: "http://www.openfoodnetwork.org/contact/", target: "_blank"} Contact - // To be added when Guy's pretty landing page is up: - //| - //%a{href:'' } Developers + + .small-12.medium-4.columns.text-center + %h6 OFN Sites + %p + %a{href: "http://dev.openfoodnetwork.org", target: "_blank"} Developer + %span | + %a{href: "http://community.openfoodnetwork.org", target: "_blank"} Community + %span | + %a{href: "http://www.openfoodnetwork.org/platform/user-guide/", target: "_blank"} User Guide + + .medium-2.columns.text-center + / Placeholder + + .footer-local + .row + .small-12.medium-2.medium-offset-2.columns.text-center + %p.secure-icon + %i.ofn-i_017-locked + .small-12.medium-6.columns.text-center + %p.text-big.secure-text Secure and trusted. + %p.secure-text Open Food Network uses SSL encryption (2048 bit RSA) everywhere to keep your shopping and payment information private. Our servers do not store your credit card details and payments are processed by PCI-compliant services. + .small-12.medium-2.columns + + .row + .small-12.medium-8.medium-offset-2.columns.text-center + %hr.hr-light + %br + + .row + .small-6.medium-3.medium-offset-2.columns.text-left + // This is the instance-managed set of links: + %h4 Keep in touch + %p.social-icons + - if ContentConfig.footer_facebook_url.present? + %a{href: ContentConfig.footer_facebook_url} + %i.ofn-i_044-facebook + - if ContentConfig.footer_twitter_url.present? + %a{href: ContentConfig.footer_twitter_url} + %i.ofn-i_041-twitter + - if ContentConfig.footer_instagram_url.present? + %a{href: ContentConfig.footer_instagram_url} + %i.ofn-i_043-instagram + - if ContentConfig.footer_linkedin_url.present? + %a{href: ContentConfig.footer_linkedin_url} + %i.ofn-i_042-linkedin + - if ContentConfig.footer_googleplus_url.present? + %a{href: ContentConfig.footer_googleplus_url} + %i.ofn-i_046-g + - if ContentConfig.footer_pinterest_url.present? + %a{href: ContentConfig.footer_pinterest_url} + %i.ofn-i_045-pintrest + - if ContentConfig.footer_email.present? + %p + %a{href: ContentConfig.footer_email.reverse, mailto: true, target: '_blank'} Email us + = render_markdown(ContentConfig.footer_links_md).html_safe + + + .small-6.medium-3.columns.text-left + %h4 Navigate + %p + %a{href: "/shops"} Shops + %p + %a{href: "/map"} Map + %p + %a{href: "/producers"} Producers + %p + %a{href: "/groups"} Groups + %p + %a{href: ContentConfig.footer_about_url} About + + .small-12.medium-2.columns.text-left + %h4 Join us + %p + %a{href: "/producers/signup"} Producers sign-up + %p + %a{href: "/shops/signup"} Hubs sign-up + %p + %a{href: "/groups/signup"} Groups sign-up + %p + %a{href: "http://www.openfoodnetwork.org/platform/food-system-partners/", target: "_blank"} Food systems partners + + .medium-2.columns.text-center + / Placeholder + + .row + .small-12.medium-8.medium-offset-2.columns.text-center + %hr.hr-light + %br + + .row + .small-12.medium-3.medium-offset-2.columns.text-left + %a{href: root_path} + %img{src: ContentConfig.footer_logo.url, width: "220"} + .small-12.medium-5.columns.text-left + %p.text-small + Read our + %a{href: ContentConfig.footer_tos_url} Terms & conditions + | + Find us on + %a{href:"https://github.com/openfoodfoundation/openfoodnetwork", target: "_blank"} Github + %p.text-small + Open Food Network is a free and open source software platform. Our content is licensed with + = succeed ',' do + %a{href:"https://creativecommons.org/licenses/by-sa/3.0/", target: "_blank" } CC BY-SA 3.0 + and our code with + = succeed '.' do + %a{href:"https://tldrlegal.com/license/gnu-affero-general-public-license-v3-(agpl-3.0)", target: "_blank" } AGPL 3 + + .medium-2.columns.text-center + / Placeholder diff --git a/app/views/shared/components/_enterprise_no_results.html.haml b/app/views/shared/components/_enterprise_no_results.html.haml index 2e6edd0875..1c8af802a2 100644 --- a/app/views/shared/components/_enterprise_no_results.html.haml +++ b/app/views/shared/components/_enterprise_no_results.html.haml @@ -1,4 +1,5 @@ -%producer.row{"ng-show" => "filteredEnterprises.length == 0"} +- enterprises ||= 'filteredEnterprises' +%producer.row{"ng-show" => "#{enterprises}.length == 0"} %p.no-results Sorry, no results found for %strong {{query}}. diff --git a/app/views/shared/components/_enterprise_search.html.haml b/app/views/shared/components/_enterprise_search.html.haml index 62dcec3ea6..4bb7ca446b 100644 --- a/app/views/shared/components/_enterprise_search.html.haml +++ b/app/views/shared/components/_enterprise_search.html.haml @@ -3,5 +3,5 @@ %input{type: :text, "ng-model" => "query", placeholder: t('search_by_name'), - "ng-debounce" => "150", + "ng-debounce" => "500", "ofn-disable-enter" => true} diff --git a/app/views/shared/menu/_alert.html.haml b/app/views/shared/menu/_alert.html.haml new file mode 100644 index 0000000000..3ce7da91f1 --- /dev/null +++ b/app/views/shared/menu/_alert.html.haml @@ -0,0 +1,9 @@ +.text-center.page-alert.fixed{ "ofn-page-alert" => true } + .alert-box + %a.alert-cta{href: "http://www.openfoodnetwork.org", target: "_blank"} + %h6 + Interested in selling food on the Open Food Network?   + %strong + Start here + %i.ofn-i_054-point-right + %a.close{ ng: { click: "close()" } } × diff --git a/app/views/shared/menu/_cart.html.haml b/app/views/shared/menu/_cart.html.haml index f8287c8f61..f4d3e00c67 100644 --- a/app/views/shared/menu/_cart.html.haml +++ b/app/views/shared/menu/_cart.html.haml @@ -9,7 +9,11 @@ .joyride-tip-guide{"ng-class" => "{ in: open }", "ng-show" => "open"} %span.joyride-nub.top .joyride-content-wrapper - %h5 Your shopping cart + %h5.text-left Your shopping cart + .buttons.text-right + %a.button.secondary.tiny.add_to_cart{ href: cart_path, type: :submit, "ng-disabled" => "Cart.dirty || Cart.empty()", "ng-class" => "{ dirty: Cart.dirty }" } + {{ Cart.dirty ? 'Updating cart...' : (Cart.empty() ? 'Cart empty' : 'Edit your cart' ) }} + %a.button.primary.tiny{href: checkout_path, "ng-disabled" => "Cart.dirty || Cart.empty()"} Checkout now %table %tr.product-cart{"ng-repeat" => "line_item in Cart.line_items_present()", "ng-controller" => "LineItemCtrl", "id" => "cart-variant-{{ line_item.variant.id }}"} diff --git a/app/views/shared/menu/_large_menu.html.haml b/app/views/shared/menu/_large_menu.html.haml index a12d0030e0..4f71e1c93e 100644 --- a/app/views/shared/menu/_large_menu.html.haml +++ b/app/views/shared/menu/_large_menu.html.haml @@ -1,43 +1,33 @@ %nav.top-bar.show-for-large-up{'data-topbar' => true} %section.top-bar-section - %ul.left{} + %ul.left %li.ofn-logo - %a{href: root_path} - %img{src: "/assets/open-food-network-beta.png", srcset: "/assets/open-food-network-beta.svg", width: "110", height: "26"} - %li.divider - - if current_page? root_path - %li - %a{"ofn-scroll-to" => "hubs"} - %span.nav-primary Hubs - - else - %li - %a{href: root_path + "#/#hubs"} - %span.nav-primary Hubs - %li.divider + %a{href: root_path} + %img{src: ContentConfig.logo.url, width: "250", height: "51"} + %ul.center + %li + %a{href: main_app.shops_path} + %span.nav-primary Shops %li %a{href: main_app.map_path} %span.nav-primary Map - %li.divider %li %a{href: main_app.producers_path} %span.nav-primary Producers - %li.divider %li %a{href: main_app.groups_path} %span.nav-primary Groups - %li.divider - %section.top-bar-section + %li + %a{href: ContentConfig.footer_about_url} + %span.nav-primary About %ul.right - %li.divider - if spree_current_user.nil? = render 'shared/signed_out' - else = render 'shared/signed_in' - %li.divider %li.current_hub{"ng-controller" => "CurrentHubCtrl", "ng-show" => "CurrentHub.hub.id", "ng-cloak" => true} %a{href: main_app.shop_path} - %em Shopping @ - %span.nav-primary.nav-branded {{ CurrentHub.hub.name }} - %li.divider + %em Shopping @ + %span.nav-primary.nav-branded {{ CurrentHub.hub.name | truncate:25 }} %li.cart{"ng-cloak" => true} = render partial: "shared/menu/cart" diff --git a/app/views/shared/menu/_mobile_menu.html.haml b/app/views/shared/menu/_mobile_menu.html.haml index e5ba9af83b..d43c95ca46 100644 --- a/app/views/shared/menu/_mobile_menu.html.haml +++ b/app/views/shared/menu/_mobile_menu.html.haml @@ -2,6 +2,12 @@ %section.left %a.left-off-canvas-toggle.menu-icon %span + + %section.left + .ofn-logo + %a{href: root_path} + %img{src: ContentConfig.logo_mobile.url, srcset: ContentConfig.logo_mobile_svg.url, width: "75", height: "26"} + %section.right{"ng-cloak" => true} .cart = render partial: "shared/menu/cart" @@ -12,20 +18,18 @@ %ul.off-canvas-list %li.ofn-logo %a{href: root_path} - %img{src: "/assets/open-food-network-beta.png", srcset: "/assets/open-food-network-beta.svg", width: "110", height: "26"} - - - if current_page? root_path - %li.li-menu + %img{src: ContentConfig.logo_mobile.url, srcset: ContentConfig.logo_mobile_svg.url, width: "75", height: "26"} + %li.li-menu + - if current_page? main_app.shops_path %a{"ofn-scroll-to" => "hubs"} %span.nav-primary - %i.ofn-i_040-hub - Hubs - - else - %li.li-menu - %a{href: root_path + "#/#hubs"} + %i.ofn-i_019-map-pin + Shops + - else + %a{href: main_app.shops_path} %span.nav-primary - %i.ofn-i_040-hub - Hubs + %i.ofn-i_019-map-pin + Shops %li.li-menu %a{href: main_app.map_path} %span.nav-primary @@ -41,6 +45,11 @@ %span.nav-primary %i.ofn-i_035-groups Groups + %li.li-menu + %a{href: ContentConfig.footer_about_url} + %span.nav-primary + %i.ofn-i_013-help + About %li - if spree_current_user.nil? diff --git a/app/views/shop/products/_form.html.haml b/app/views/shop/products/_form.html.haml index bdaded0f0d..527ecbdd94 100644 --- a/app/views/shop/products/_form.html.haml +++ b/app/views/shop/products/_form.html.haml @@ -1,57 +1,58 @@ -%products.small-12.columns{"ng-controller" => "ProductsCtrl", "ng-show" => "order_cycle.order_cycle_id != null", "ng-cloak" => true, -"infinite-scroll" => "incrementLimit()", "infinite-scroll-distance" => "1"} +.footer-pad.small-12.columns + %products{"ng-controller" => "ProductsCtrl", "ng-show" => "order_cycle.order_cycle_id != null", "ng-cloak" => true, + "infinite-scroll" => "incrementLimit()", "infinite-scroll-distance" => "1"} - // TODO: Needs an ng-show to slide content down - .row.animate-slide{ "ng-show" => "query || appliedPropertiesList() || appliedTaxonsList()" } - .small-12.columns - .alert-box.search-alert.ng-scope - %a.right{"ng-click" => "clearAll()"} - Clear all - %i.ofn-i_009-close - %span.filter-label - Showing: - %span.applied-properties - {{ appliedPropertiesList() }} - %span.applied-taxons - {{ appliedTaxonsList() }} - %span{ ng: { hide: "!query"} } - %span{ "ng-show" => "appliedPropertiesList() || appliedTaxonsList()" } - with - %span.applied-search "{{ query }}" - .row - .small-12.medium-6.large-5.columns - %input#search.text{"ng-model" => "query", - placeholder: "Search by product or producer", - "ng-debounce" => "100", - "ofn-disable-enter" => true} + // TODO: Needs an ng-show to slide content down + .row.animate-slide{ "ng-show" => "query || appliedPropertiesList() || appliedTaxonsList()" } + .small-12.columns + .alert-box.search-alert.ng-scope + %a.right{"ng-click" => "clearAll()"} + Clear all + %i.ofn-i_009-close + %span.filter-label + Showing: + %span.applied-properties + {{ appliedPropertiesList() }} + %span.applied-taxons + {{ appliedTaxonsList() }} + %span{ ng: { hide: "!query"} } + %span{ "ng-show" => "appliedPropertiesList() || appliedTaxonsList()" } + with + %span.applied-search "{{ query }}" + .row + .small-12.medium-6.large-5.columns + %input#search.text{"ng-model" => "query", + placeholder: "Search by product or producer", + "ng-debounce" => "100", + "ofn-disable-enter" => true} - .small-12.medium-6.large-6.large-offset-1.columns - = render partial: "shop/products/filters" + .small-12.medium-6.large-6.large-offset-1.columns + = render partial: "shop/products/filters" - %div.pad-top{bindonce: true} - %product.animate-repeat{"ng-controller" => "ProductNodeCtrl", - "ng-repeat" => "product in filteredProducts = (Products.products | products:query | taxons:activeTaxons | properties: activeProperties) track by product.id ", "id" => "product-{{ product.id }}"} - = render partial: "shop/products/summary" - %shop-variant{variant: 'product.master', "bo-if" => "!product.hasVariants", "id" => "variant-{{ product.master.id }}"} - %shop-variant{variant: 'variant', "ng-repeat" => "variant in product.variants track by variant.id", "id" => "variant-{{ variant.id }}"} + %div.pad-top{bindonce: true} + %product.animate-repeat{"ng-controller" => "ProductNodeCtrl", + "ng-repeat" => "product in filteredProducts = (Products.products | products:query | taxons:activeTaxons | properties: activeProperties) track by product.id ", "id" => "product-{{ product.id }}"} + = render partial: "shop/products/summary" + %shop-variant{variant: 'product.master', "bo-if" => "!product.hasVariants", "id" => "variant-{{ product.master.id }}"} + %shop-variant{variant: 'variant', "ng-repeat" => "variant in product.variants track by variant.id", "id" => "variant-{{ variant.id }}"} - %product{"ng-show" => "Products.loading"} - .row.summary - .small-12.columns.text-center - Loading products... - .row - .small-12.columns.text-center - %img.spinner{ src: "/assets/spinning-circles.svg" } + %product{"ng-show" => "Products.loading"} + .row.summary + .small-12.columns.text-center + Loading products... + .row + .small-12.columns.text-center + %img.spinner{ src: "/assets/spinning-circles.svg" } - %div{"ng-show" => "filteredProducts.length == 0 && !Products.loading"} - .row.summary - .small-12.columns - %p.no-results - Sorry, no results found for - %strong {{query}}. - Try another search? - .row - .small-12.columns - %form{action: cart_path} - %i.ofn-i_011-spinner.cart-spinner{"ng-show" => "Cart.dirty"} - %input.small.button.primary.right.add_to_cart{type: :submit, value: "{{ Cart.dirty ? 'Updating cart...' : (Cart.empty() ? 'Cart empty' : 'Edit your cart' ) }}", "ng-disabled" => "Cart.dirty || Cart.empty()", "ng-class" => "{ dirty: Cart.dirty }" } + %div{"ng-show" => "filteredProducts.length == 0 && !Products.loading"} + .row.summary + .small-12.columns + %p.no-results + Sorry, no results found for + %strong {{query}}. + Try another search? + .row + .small-12.columns + %form{action: cart_path} + %i.ofn-i_011-spinner.cart-spinner{"ng-show" => "Cart.dirty"} + %input.small.button.primary.right.add_to_cart{type: :submit, value: "{{ Cart.dirty ? 'Updating cart...' : (Cart.empty() ? 'Cart empty' : 'Edit your cart' ) }}", "ng-disabled" => "Cart.dirty || Cart.empty()", "ng-class" => "{ dirty: Cart.dirty }" } diff --git a/app/views/shops/index.html.haml b/app/views/shops/index.html.haml new file mode 100644 index 0000000000..40a42da1ea --- /dev/null +++ b/app/views/shops/index.html.haml @@ -0,0 +1,12 @@ +- content_for(:title) do + Shops + +#panes + #shops.pane + .row + .small-12.medium-6.medium-offset-3.columns.text-center + %h2 Shopping, transformed. + %p.text-big Food grows in cycles, farmers harvest in cycles, and we order food in cycles. If you find an order cycle closed, check back soon. + += render partial: "home/hubs" += render partial: "shared/footer" diff --git a/app/views/shops/signup.html.haml b/app/views/shops/signup.html.haml new file mode 100644 index 0000000000..a04f7d82e2 --- /dev/null +++ b/app/views/shops/signup.html.haml @@ -0,0 +1,48 @@ +- content_for(:title) do + Sign up as a hub + +#panes + #shops-signup.pane + .row.header + .small-12.medium-12.columns.text-center + %h2 Food hubs, unlimited. + .row.content + .small-12.medium-6.medium-offset-3.columns.text-center + %p.text-big Whatever your model, we support you. However you change, we're with you. We're non-profit, independent, and open-sourced. We're the software partners you've dreamed of. + %br + %a.button.transparent{href: "/register"} + Join now + .pane + .row + .small-12.medium-10.medium-offset-1.columns.text-center + %h2 Enterprise Accounts + -# %p.text-big + -# If there is a time-sensitive offer you can write it here, e.g. + -# Sign up before 30th June for an extra month free! + %br + = ContentConfig.hub_signup_pricing_table_html.html_safe + + #shops-case-studies + .row + .small-12.medium-10.medium-offset-1.columns + %h2.text-center Stories from our hubs. + %br + = ContentConfig.hub_signup_case_studies_html.html_safe + + .pane#cta + .row + .small-12.medium-6.medium-offset-3.columns.text-center + %h2 We're ready to help. + %p.text-big You need a better return. You need new buyers and logistics partners. You need your story told across wholesale, retail, and the kitchen table. + %br + %a.button.transparent{href: "/register"} + Join now + + #hub-details.pane.footer-pad + .row + .small-12.medium-10.medium-offset-1.columns + %h2.text-center Here's the detail. + = ContentConfig.hub_signup_detail_html.html_safe + + += render partial: "shared/footer" diff --git a/app/views/spree/admin/orders/bulk_management.html.haml b/app/views/spree/admin/orders/bulk_management.html.haml index 71134e38a9..0e4c69b695 100644 --- a/app/views/spree/admin/orders/bulk_management.html.haml +++ b/app/views/spree/admin/orders/bulk_management.html.haml @@ -126,7 +126,7 @@ %a{ :href => '', 'ng-click' => "predicate = 'units_variant.full_name'; reverse = !reverse" } Product: Unit %th.quantity{ 'ng-show' => 'columns.quantity.visible' } Quantity %th.max{ 'ng-show' => 'columns.max.visible' } Max - %th.unit_value{ 'ng-show' => 'columns.unit_value.visible' } Weight/Volume + %th.final_weight_volume{ 'ng-show' => 'columns.final_weight_volume.visible' } Weight/Volume %th.price{ 'ng-show' => 'columns.price.visible' } Price %th.actions %th.actions @@ -146,12 +146,12 @@ %td.variant{ 'ng-show' => 'columns.variant.visible' } %a{ :href => '#', 'ng-click' => "setSelectedUnitsVariant(line_item.units_product,line_item.units_variant)" } {{ line_item.units_variant.full_name }} %td.quantity{ 'ng-show' => 'columns.quantity.visible' } - %input{ :type => 'number', :name => 'quantity', 'ng-model' => "line_item.quantity", 'obj-for-update' => "line_item", "attr-for-update" => "quantity" } + %input{ :type => 'number', :name => 'quantity', :id => 'quantity', :value => 'line_item.quantity', 'ng-model' => "line_item.quantity", 'ng-change' => "updateOnQuantity(line_item, {{ line_item.quantity }})", 'obj-for-update' => "line_item", "attr-for-update" => "quantity" } %td.max{ 'ng-show' => 'columns.max.visible' } {{ line_item.max_quantity }} - %td.unit_value{ 'ng-show' => 'columns.unit_value.visible' } - %input{ :type => 'number', :name => 'unit_value', :id => 'unit_value', 'ng-model' => "line_item.unit_value", 'ng-readonly' => "unitValueLessThanZero(line_item)", 'ng-change' => "weightAdjustedPrice(line_item, {{ line_item.unit_value }})", 'obj-for-update' => "line_item", "attr-for-update" => "unit_value" } + %td.final_weight_volume{ 'ng-show' => 'columns.final_weight_volume.visible' } + %input{ :type => 'text', :name => 'final_weight_volume', :id => 'final_weight_volume', :value => 'line_item.final_weight_volume', 'ng-model' => "line_item.final_weight_volume", 'ng-readonly' => "unitValueLessThanZero(line_item)", 'ng-change' => "weightAdjustedPrice(line_item, {{ line_item.final_weight_volume }})", 'obj-for-update' => "line_item", "attr-for-update" => "final_weight_volume" } %td.price{ 'ng-show' => 'columns.price.visible' } - %input{ :type => 'text', :name => 'price', :id => 'price', :value => '{{ line_item.price | currency }}', 'ng-readonly' => "true", 'obj-for-update' => "line_item", "attr-for-update" => "price" } + %input{ :type => 'text', :name => 'price', :id => 'price', :value => '{{ line_item.price * line_item.quantity | currency }}', 'ng-readonly' => "true", 'obj-for-update' => "line_item", "attr-for-update" => "price" } %td.actions %a{ :class => "edit-order icon-edit no-text", 'ofn-confirm-link-path' => "/admin/orders/{{line_item.order.number}}/edit" } %td.actions diff --git a/app/views/spree/admin/overview/_change_type_form.html.haml b/app/views/spree/admin/overview/_change_type_form.html.haml deleted file mode 100644 index 530d870307..0000000000 --- a/app/views/spree/admin/overview/_change_type_form.html.haml +++ /dev/null @@ -1,60 +0,0 @@ -= admin_inject_enterprise - -= form_for @enterprise, url: main_app.set_sells_admin_enterprise_path(@enterprise), - html: { name: "change_type", id: "change_type", novalidate: true, "ng-app" => "admin.enterprises", "ng-controller"=> 'changeTypeFormCtrl' } do |change_type_form| - -# Have to use hidden:'true' on this input rather than type:'hidden' as the latter seems to break ngPattern and therefore validation - %input{ hidden: "true", name: "sells", ng: { required: true, pattern: "/^(none|own)$/", model: 'sells', value: "sells"} } - - .row - .options.sixteen.columns.alpha - - if @enterprise.is_primary_producer - %input{ type: 'checkbox', hidden: true, name: "producer_profile_only", ng: { required: true, model: 'producer_profile_only', value: "producer_profile_only"} } - .basic_producer.option.one-third.column.alpha - %a.full-width.button.selector{ ng: { click: "sells='none';producer_profile_only=true;", class: "{selected: sells=='none' && producer_profile_only==true}" } } - .top - %h3 Producer Profile - %p Connect through OFN - .bottom ALWAYS FREE - %p.description - You want to use Open Food Network as a place for people to find and contact you. - - .producer_shop.option.one-third.column - %a.full-width.button.selector{ ng: { click: "sells='none';producer_profile_only=false;", class: "{selected: sells=='none' && producer_profile_only==false}" } } - .top - %h3 Sell products - %p As a supplier - .bottom ALWAYS FREE - %p.description - Add your products to Open Food Network, allowing customers to see your product range, and allowing you to act as a supplier to other shopfronts. - - .full_hub.option.one-third.column.omega.disabled - %a.full-width.button.selector{ ng: { click: "sells='own';producer_profile_only=false;", class: "{selected: sells=='own' && producer_profile_only==false}" } } - .top - %h3 Sell products - %p Through an OFN shopfront - .bottom 30 DAY TRIAL - %p.description - Test out having your own shopfront with full access to all Shopfront features for 30 days. - %br - %br - At the end of your trial, there is a one-off $200 fee to fully activate your account. Then you will be billed for 2% of your actual transactions, capped at $50 a month (so if you don’t sell anything you don’t pay anything, but you never pay more than $50 a month). - - - else - .shop_profile.option.one-third.column.alpha - %a.full-width.button.selector{ ng: { click: "sells='none'", class: "{selected: sells=='none'}" } } - .top - %h3 Shop Profile - %p Get a listing - .bottom ALWAYS FREE - %p.description - You want to use OFN as a place for people to find and contact you. - .row - .sixteen.columns.alpha - %span.error{ ng: { show: "(change_type.sells.$error.required || change_type.sells.$error.pattern) && submitted" } } - Please choose one of the options above. - - if @enterprise.sells == 'unspecified' - %input.button.big{ type: 'submit', value: 'Select and continue', ng: { click: "submit(change_type)" } } - - else - %input.button.big{ type: 'submit', value: 'Change now', ng: { click: "submit(change_type)" } } - %br   - %hr diff --git a/app/views/spree/admin/overview/multi_enterprise_dashboard.html.haml b/app/views/spree/admin/overview/multi_enterprise_dashboard.html.haml index f230d7e44e..420196d3ce 100644 --- a/app/views/spree/admin/overview/multi_enterprise_dashboard.html.haml +++ b/app/views/spree/admin/overview/multi_enterprise_dashboard.html.haml @@ -1,3 +1,7 @@ +- content_for :page_actions do + = render 'admin/shared/user_guide_link' + + %h1{ :style => 'margin-bottom: 30px'} Dashboard - if @enterprises.unconfirmed.any? @@ -21,4 +25,4 @@ - if can? :admin, OrderCycle = render partial: "order_cycles" - = render partial: "enterprises" \ No newline at end of file + = render partial: "enterprises" diff --git a/app/views/spree/admin/overview/single_enterprise_dashboard.html.haml b/app/views/spree/admin/overview/single_enterprise_dashboard.html.haml index f8cb1fb5d9..3998f736de 100644 --- a/app/views/spree/admin/overview/single_enterprise_dashboard.html.haml +++ b/app/views/spree/admin/overview/single_enterprise_dashboard.html.haml @@ -5,24 +5,26 @@ = "(#{enterprise_type_name(@enterprise)})" - content_for :page_actions do + = render 'admin/shared/user_guide_link' + :javascript function toggleType(){ - if( $('#type_selection').is(":visible") ){ + if( $('#package_selection').is(":visible") ){ $('button#toggle_type i').switchClass("icon-chevron-up","icon-chevron-down") } else { $('button#toggle_type i').switchClass("icon-chevron-down","icon-chevron-up") } - $("#type_selection").slideToggle() + $("#package_selection").slideToggle() } - #type_button + #package_button %button#toggle_type{ onClick: 'toggleType()' } - Change type + Change Package %i.icon-chevron-down -#type_selection{ hidden: true } - = render partial: "change_type_form" +#package_selection{ hidden: true } + = render partial: "/admin/enterprises/change_type_form" - if @enterprise.confirmed_at.nil? @@ -94,7 +96,3 @@ %a.button.bottom{href: main_app.admin_order_cycles_path} Manage order cycles %span.icon-arrow-right - - - - diff --git a/app/views/spree/admin/products/_group_buy_form.html.haml b/app/views/spree/admin/products/_group_buy_form.html.haml index 6669563d15..8f1de2a884 100644 --- a/app/views/spree/admin/products/_group_buy_form.html.haml +++ b/app/views/spree/admin/products/_group_buy_form.html.haml @@ -9,6 +9,6 @@ = f.label :group_buy_0, 'No' %br.clear = f.field_container :group_buy_unit_size do - = f.label :group_buy_unit_size + = f.label :group_buy_unit_size, "Bulk unit size" %br = f.text_field :group_buy_unit_size diff --git a/app/views/spree/admin/products/_supplier_and_group_buy_for_new.html.haml b/app/views/spree/admin/products/_supplier_and_group_buy_for_new.html.haml deleted file mode 100644 index d17a45fa6c..0000000000 --- a/app/views/spree/admin/products/_supplier_and_group_buy_for_new.html.haml +++ /dev/null @@ -1,20 +0,0 @@ -.row - .alpha.six.columns - = f.field_container :supplier do - = f.label :supplier - = f.collection_select(:supplier_id, Enterprise.is_primary_producer.managed_by(spree_current_user).by_name, :id, :name, {:include_blank => true}, {:class => "select2 fullwidth"}) - = f.error_message_on :supplier - .four.columns - = f.field_container :group_buy do - = f.label :group_buy, 'Group buy?' - %br - .alpha.two.columns - = f.radio_button :group_buy, '1', :checked => f.object.group_buy - = f.label :group_buy_1, 'Yes' - .omega.two.columns - = f.radio_button :group_buy, '0', :checked => !f.object.group_buy - = f.label :group_buy_0, 'No' - .omega.six.columns - = f.field_container :group_buy_unit_size do - = f.label :group_buy_unit_size - = f.text_field :group_buy_unit_size, :class => "fullwidth" diff --git a/app/views/spree/admin/reports/_packing_description.html.haml b/app/views/spree/admin/reports/_packing_description.html.haml new file mode 100644 index 0000000000..3d7855cb89 --- /dev/null +++ b/app/views/spree/admin/reports/_packing_description.html.haml @@ -0,0 +1,4 @@ +%ul{style: "margin-left: 12pt"} + - report_types.each do |report_type| + %li + = link_to report_type[0], "#{packing_admin_reports_url}?report_type=#{report_type[1]}" diff --git a/app/views/spree/admin/reports/bulk_coop.html.haml b/app/views/spree/admin/reports/bulk_coop.html.haml index d80a449bb4..8d9a1234ce 100644 --- a/app/views/spree/admin/reports/bulk_coop.html.haml +++ b/app/views/spree/admin/reports/bulk_coop.html.haml @@ -1,4 +1,4 @@ -= form_for @search, :url => spree.bulk_coop_admin_reports_path do |f| += form_for @report.search, :url => spree.bulk_coop_admin_reports_path do |f| = render 'date_range_form', f: f .row @@ -20,7 +20,7 @@ %table#listing_orders.index %thead %tr{'data-hook' => "orders_header"} - - @header.each do |heading| + - @report.header.each do |heading| %th=heading %tbody - @table.each do |row| diff --git a/app/views/spree/admin/reports/orders_and_fulfillment.html.haml b/app/views/spree/admin/reports/orders_and_fulfillment.html.haml index 081cb43384..2465fe0ff2 100644 --- a/app/views/spree/admin/reports/orders_and_fulfillment.html.haml +++ b/app/views/spree/admin/reports/orders_and_fulfillment.html.haml @@ -1,4 +1,4 @@ -= form_for @search, :url => spree.orders_and_fulfillment_admin_reports_path do |f| += form_for @report.search, :url => spree.orders_and_fulfillment_admin_reports_path do |f| = render 'date_range_form', f: f .row @@ -30,7 +30,7 @@ %table#listing_orders.index %thead %tr{'data-hook' => "orders_header"} - - @header.each do |heading| + - @report.header.each do |heading| %th=heading %tbody - @table.each do |row| diff --git a/app/views/spree/admin/reports/packing.html.haml b/app/views/spree/admin/reports/packing.html.haml new file mode 100644 index 0000000000..2aa8d8c8b9 --- /dev/null +++ b/app/views/spree/admin/reports/packing.html.haml @@ -0,0 +1,42 @@ += form_for @report.search, :url => spree.packing_admin_reports_path do |f| + = render 'date_range_form', f: f + + .row + .alpha.two.columns= label_tag nil, "Hubs: " + .omega.fourteen.columns= f.collection_select(:distributor_id_in, @distributors, :id, :name, {}, {class: "select2 fullwidth", multiple: true}) + + .row + .alpha.two.columns= label_tag nil, "Producers: " + .omega.fourteen.columns= select_tag(:supplier_id_in, options_from_collection_for_select(@suppliers, :id, :name, params[:supplier_id_in]), {class: "select2 fullwidth", multiple: true}) + + .row + .alpha.two.columns= label_tag nil, "Order Cycles: " + .omega.fourteen.columns + = f.select(:order_cycle_id_in, report_order_cycle_options(@order_cycles), {selected: params[:q][:order_cycle_id_in]}, {class: "select2 fullwidth", multiple: true}) + + .row + .alpha.two.columns= label_tag nil, "Report Type: " + .omega.fourteen.columns= select_tag(:report_type, options_for_select(@report_types, @report_type)) + + .row + = check_box_tag :csv + = label_tag :csv, "Download as csv" + + .row + = button t(:search) + +%br +%br +%table#listing_orders.index + %thead + %tr{'data-hook' => "orders_header"} + - @report.header.each do |heading| + %th=heading + %tbody + - @table.each do |row| + %tr + - row.each do |column| + %td= column + - if @table.empty? + %tr + %td{:colspan => "2"}= t(:none) diff --git a/app/views/spree/admin/reports/payments.html.haml b/app/views/spree/admin/reports/payments.html.haml index 1a92d507db..909846f47f 100644 --- a/app/views/spree/admin/reports/payments.html.haml +++ b/app/views/spree/admin/reports/payments.html.haml @@ -1,4 +1,4 @@ -= form_for @search, :url => spree.payments_admin_reports_path do |f| += form_for @report.search, :url => spree.payments_admin_reports_path do |f| = render 'date_range_form', f: f .row @@ -20,7 +20,7 @@ %table#listing_orders.index %thead %tr{'data-hook' => "orders_header"} - - @header.each do |heading| + - @report.header.each do |heading| %th=heading %tbody - @table.each do |row| diff --git a/app/views/spree/admin/reports/xero_invoices.html.haml b/app/views/spree/admin/reports/xero_invoices.html.haml index 1ae4e3b279..be58420e13 100644 --- a/app/views/spree/admin/reports/xero_invoices.html.haml +++ b/app/views/spree/admin/reports/xero_invoices.html.haml @@ -1,6 +1,9 @@ = form_for @search, url: spree.xero_invoices_admin_reports_path do |f| = render 'date_range_form', f: f + .row + .four.columns.alpha= label_tag :report_type, "Report Type: " + .four.columns.omega= select_tag :report_type, options_for_select(xero_report_types, params[:report_type]), {include_blank: false, class: "select2 fullwidth"} .row .four.columns.alpha= label_tag nil, "Hub: " .four.columns.omega= f.collection_select(:distributor_id_eq, @distributors, :id, :name, {:include_blank => 'All'}, {:class => "select2 fullwidth"}) diff --git a/app/views/spree/admin/tax_settings/edit.html.haml b/app/views/spree/admin/tax_settings/edit.html.haml new file mode 100644 index 0000000000..102ec0609e --- /dev/null +++ b/app/views/spree/admin/tax_settings/edit.html.haml @@ -0,0 +1,32 @@ += render :partial => 'spree/admin/shared/configuration_menu' + +- content_for :page_title do + = t(:tax_settings) + += form_tag admin_tax_settings_path, :method => :put do + + .field.align-center{ "data-hook" => "products_require_tax_category" } + = hidden_field_tag 'preferences[products_require_tax_category]', '0' + = check_box_tag 'preferences[products_require_tax_category]', '1', Spree::Config[:products_require_tax_category] + = label_tag nil, t(:products_require_tax_category) + + .field.align-center{"data-hook" => "shipment_vat"} + = hidden_field_tag 'preferences[shipment_inc_vat]', '0' + = check_box_tag 'preferences[shipment_inc_vat]', '1', Spree::Config[:shipment_inc_vat] + = label_tag nil, t(:shipment_inc_vat) + + .field.align-center{ "data-hook" => "shipping_tax_rate" } + = number_field_tag "preferences[shipping_tax_rate]", Spree::Config[:shipping_tax_rate].to_f, in: 0.0..1.0, step: 0.01 + = label_tag nil, t(:shipping_tax_rate) + + .field.align-center{"data-hook" => "billing_tax"} + = hidden_field_tag 'preferences[account_bill_inc_tax]', '0' + = check_box_tag 'preferences[account_bill_inc_tax]', '1', Spree::Config[:account_bill_inc_tax] + = label_tag nil, t(:account_bill_inc_tax) + + .field.align-center{ "data-hook" => "account_bill_tax_rate" } + = number_field_tag "preferences[account_bill_tax_rate]", Spree::Config[:account_bill_tax_rate].to_f, in: 0.0..1.0, step: 0.01 + = label_tag nil, t(:account_bill_tax_rate) + + .form-buttons{"data-hook" => "buttons"} + = button t(:update), 'icon-refresh' diff --git a/app/views/spree/api/products/bulk_show.v1.rabl b/app/views/spree/api/products/bulk_show.v1.rabl index d2cd23bd03..c336d72530 100644 --- a/app/views/spree/api/products/bulk_show.v1.rabl +++ b/app/views/spree/api/products/bulk_show.v1.rabl @@ -1,5 +1,11 @@ object @product -attributes :id, :name, :variant_unit, :variant_unit_scale, :variant_unit_name, :on_demand + +# TODO: This is used by bulk product edit when a product is cloned. +# But the list of products is serialized by Api::Admin::ProductSerializer. +# This should probably be unified. + +attributes :id, :name, :sku, :variant_unit, :variant_unit_scale, :variant_unit_name, :on_demand, :inherits_properties +attributes :on_hand, :price, :available_on, :permalink_live, :tax_category_id # Infinity is not a valid JSON object, but Rails encodes it anyway node( :taxon_ids ) { |p| p.taxons.map{ |t| t.id }.join(",") } @@ -8,6 +14,8 @@ node( :price ) { |p| p.price.nil? ? '0.0' : p.price } node( :available_on ) { |p| p.available_on.blank? ? "" : p.available_on.strftime("%F %T") } node( :permalink_live ) { |p| p.permalink } +node( :producer_id ) { |p| p.supplier_id } +node( :category_id ) { |p| p.primary_taxon_id } node( :supplier ) do |p| partial 'api/enterprises/bulk_show', :object => p.supplier end diff --git a/app/views/spree/order_mailer/_shipping.html.haml b/app/views/spree/order_mailer/_shipping.html.haml index cb9cde72ca..b50a03323c 100644 --- a/app/views/spree/order_mailer/_shipping.html.haml +++ b/app/views/spree/order_mailer/_shipping.html.haml @@ -8,14 +8,14 @@ Delivery details - if @order.order_cycle.andand.pickup_time_for(@order.distributor) - %h4 + %h4 Delivery on: %strong #{@order.order_cycle.pickup_time_for(@order.distributor)} - if @order.shipping_method.andand.description - %p + %p %em #{@order.shipping_method.description.html_safe} %br   - + - if @order.ship_address %h4 Delivery address: %p @@ -37,21 +37,15 @@ Collection details - if @order.order_cycle.andand.pickup_time_for(@order.distributor).present? - %h4 + %h4 Ready for collection: %strong #{@order.order_cycle.pickup_time_for(@order.distributor)} - + - if @order.shipping_method.andand.description.present? %p %em #{@order.shipping_method.description.html_safe} %br   - - if @order.ship_address.full_address - %p - %strong Collecting from: - %br - #{@order.ship_address.full_address} - - if @order.order_cycle.andand.pickup_instructions_for(@order.distributor).present? %p %strong Collection instructions: diff --git a/app/views/spree/order_mailer/confirm_email_for_shop.html.haml b/app/views/spree/order_mailer/confirm_email_for_shop.html.haml index f62b3cdfb4..ac5d45d893 100644 --- a/app/views/spree/order_mailer/confirm_email_for_shop.html.haml +++ b/app/views/spree/order_mailer/confirm_email_for_shop.html.haml @@ -19,9 +19,10 @@ %h4 Order confirmation %strong ##{@order.number} -%p +%h5 %strong= "#{@order.bill_address.firstname} #{@order.bill_address.lastname}" - completed the following order at your shopfront: + = " <#{@order.email}>" if @order.email + = @order.bill_address.phone if @order.bill_address.phone = render 'order_summary' = render 'payment' diff --git a/app/views/spree/orders/edit.html.haml b/app/views/spree/orders/edit.html.haml index 5f8186e568..3a00558a52 100644 --- a/app/views/spree/orders/edit.html.haml +++ b/app/views/spree/orders/edit.html.haml @@ -1,19 +1,22 @@ -= inject_enterprises +- content_for(:title) do + Shopping Cart + += inject_enterprises .darkswarm - content_for :order_cycle_form do %closing Your shopping cart - %p + %p Order ready for %strong - if @order.order_cycle - = pickup_time @order.order_cycle + = pickup_time @order.order_cycle - else = @order.distributor.next_collection_at = render partial: "shopping_shared/details" - - %fieldset + + %fieldset.footer-pad - if @order.line_items.empty? %div.row{"data-hook" => "empty_cart"} %p= t(:your_cart_is_empty) @@ -31,9 +34,9 @@ .links{'data-hook' => "cart_buttons"} .row .columns.large-8{"data-hook" => ""} - + %a.button.large.secondary{href: main_app.shop_path} - %i.ofn-i_008-caret-left + %i.ofn-i_008-caret-left Continue shopping .columns.large-4.text-right %a#checkout-link.button.large.primary{href: main_app.checkout_path} @@ -41,4 +44,3 @@ %i.ofn-i_007-caret-right = render partial: "shared/footer" - diff --git a/app/views/spree/orders/show.html.haml b/app/views/spree/orders/show.html.haml index de142e3cac..7533ae981a 100644 --- a/app/views/spree/orders/show.html.haml +++ b/app/views/spree/orders/show.html.haml @@ -1,3 +1,6 @@ +- content_for(:title) do + Order Confirmation + = inject_enterprises .darkswarm @@ -11,7 +14,7 @@ = render "shopping_shared/details" if current_distributor.present? - %fieldset#order_summary{"data-hook" => ""} + %fieldset#order_summary.footer-pad{"data-hook" => ""} .row .columns.large-12.text-center %h2 @@ -30,3 +33,6 @@ - unless params.has_key? :checkout_complete - if try_spree_current_user && respond_to?(:spree_account_path) = link_to t(:my_account), spree_account_path, :class => "button" + + += render partial: "shared/footer" diff --git a/app/views/spree/shared/_order_details.html.haml b/app/views/spree/shared/_order_details.html.haml index 4fe07f07aa..e8315a00e9 100644 --- a/app/views/spree/shared/_order_details.html.haml +++ b/app/views/spree/shared/_order_details.html.haml @@ -65,12 +65,6 @@ %strong #{order.order_cycle.pickup_time_for(order.distributor)} %p.text-small.text-skinny.pre-line %em= order.shipping_method.description.andand.html_safe || "" - .order-summary.text-small - %strong - Collection Address - .pad - %p.text-small - = order.ship_address.full_address - if order.order_cycle.pickup_instructions_for(order.distributor).present? %br diff --git a/config/application.rb b/config/application.rb index 2461f9b72d..8a492f5121 100644 --- a/config/application.rb +++ b/config/application.rb @@ -82,7 +82,7 @@ module Openfoodnetwork config.assets.enabled = true # Version of your assets, change this if you want to expire all your assets - config.assets.version = '1.1' + config.assets.version = '1.2' config.sass.load_paths += [ "#{Gem.loaded_specs['foundation-rails'].full_gem_path}/vendor/assets/stylesheets/foundation/components", @@ -95,7 +95,7 @@ module Openfoodnetwork config.assets.initialize_on_precompile = true config.assets.precompile += ['store/all.css', 'store/all.js', 'store/shop_front.js', 'iehack.js'] config.assets.precompile += ['admin/all.css', 'admin/restore_spree_from_cms.css', 'admin/*.js', 'admin/**/*.js'] - config.assets.precompile += ['darkswarm/all.css', 'darkswarm/all.js'] + config.assets.precompile += ['darkswarm/all.css', 'darkswarm/all_split2.css', 'darkswarm/all.js'] config.assets.precompile += ['mail/all.css'] config.assets.precompile += ['comfortable_mexican_sofa/*'] config.assets.precompile += ['search/all.css', 'search/*.js'] diff --git a/config/environments/test.rb b/config/environments/test.rb index dd3413c8f0..67131b6489 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -11,6 +11,10 @@ Openfoodnetwork::Application.configure do config.serve_static_assets = true config.static_cache_control = "public, max-age=3600" + # Separate cache stores when running in parallel + config.cache_store = :file_store, Rails.root.join("tmp", "cache", "paralleltests#{ENV['TEST_ENV_NUMBER']}") + + # Log error messages when you accidentally call methods on nil config.whiny_nils = true @@ -37,6 +41,11 @@ Openfoodnetwork::Application.configure do # Print deprecation notices to the stderr config.active_support.deprecation = :stderr config.action_mailer.default_url_options = { :host => "test.host" } + + # To block requests before running the database cleaner + require 'open_food_network/rack_request_blocker' + # Make sure the middleware is inserted first in middleware chain + config.middleware.insert_before('ActionDispatch::Static', 'RackRequestBlocker') end # Allows us to use _url helpers in Rspec diff --git a/config/initializers/content_config.rb b/config/initializers/content_config.rb new file mode 100644 index 0000000000..abbd2c70e9 --- /dev/null +++ b/config/initializers/content_config.rb @@ -0,0 +1 @@ +ContentConfig = ContentConfiguration.new diff --git a/config/initializers/user_class_extensions.rb b/config/initializers/user_class_extensions.rb new file mode 100644 index 0000000000..cd9ff60a0c --- /dev/null +++ b/config/initializers/user_class_extensions.rb @@ -0,0 +1,11 @@ +Spree::Core::Engine.config.to_prepare do + if Spree.user_class + Spree.user_class.class_eval do + + # Override of spree method to ignore orders associated with account_invoices + def last_incomplete_spree_order + spree_orders.incomplete.where("id NOT IN (?)", account_invoices.map(&:order_id)).order('created_at DESC').first + end + end + end +end diff --git a/config/locales/en.yml b/config/locales/en.yml index fe7d3c53aa..3149620eab 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -19,3 +19,20 @@ en: producers: Aussie Producers producers_join: Australian producers are now welcome to join the Open Food Network. charges_sales_tax: Charges GST? + + logo: "Logo (640x130)" + logo_mobile: "Mobile logo (75x26)" + logo_mobile_svg: "Mobile logo (SVG)" + home_hero: "Hero image" + home_show_stats: "Show statistics" + footer_logo: "Logo (220x76)" + footer_facebook_url: "Facebook URL" + footer_twitter_url: "Twitter URL" + footer_instagram_url: "Instagram URL" + footer_linkedin_url: "LinkedIn URL" + footer_googleplus_url: "Google Plus URL" + footer_pinterest_url: "Pinterest URL" + footer_email: "Email" + footer_links_md: "Links" + footer_about_url: "About URL" + footer_tos_url: "Terms of Service URL" \ No newline at end of file diff --git a/config/ng-test.conf.js b/config/ng-test.conf.js index 0456d5eb88..56df589f0c 100644 --- a/config/ng-test.conf.js +++ b/config/ng-test.conf.js @@ -11,6 +11,7 @@ module.exports = function(config) { 'app/assets/javascripts/shared/angular-local-storage.js', 'app/assets/javascripts/shared/bindonce.min.js', 'app/assets/javascripts/shared/ng-infinite-scroll.min.js', + 'app/assets/javascripts/shared/angular-slideables.js', 'app/assets/javascripts/admin/*.js*', 'app/assets/javascripts/admin/*/*.js*', // Pull in top level files in each folder first (often these are module declarations) diff --git a/config/routes.rb b/config/routes.rb index 8e62cc3193..a21e3187a2 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,6 +1,12 @@ Openfoodnetwork::Application.routes.draw do root :to => 'home#index' + # Redirects from old URLs avoid server errors and helps search engines + get "/enterprises", to: redirect("/") + get "/products", to: redirect("/") + get "/products/:id", to: redirect("/") + get "/t/products/:id", to: redirect("/") + get "/about_us", to: redirect(ContentConfig.footer_about_url) get "/#/login", to: "home#index", as: :spree_login get "/login", to: redirect("/#/login") @@ -16,8 +22,23 @@ Openfoodnetwork::Application.routes.draw do get :order_cycle end - resources :groups - resources :producers + resources :producers, only: [:index] do + collection do + get :signup + end + end + + resources :shops, only: [:index] do + collection do + get :signup + end + end + + resources :groups, only: [:index, :show] do + collection do + get :signup + end + end get '/checkout', :to => 'checkout#edit' , :as => :checkout put '/checkout', :to => 'checkout#update' , :as => :update_checkout @@ -25,18 +46,16 @@ Openfoodnetwork::Application.routes.draw do resources :enterprises do collection do - get :suppliers - get :distributors post :search get :check_permalink end member do - get :shop_front # new world - get :shop # old world + get :shop end end get '/:id/shop', to: 'enterprises#shop', as: 'enterprise_shop' + get "/enterprises/:permalink", to: redirect("/") # Legacy enterprise URL devise_for :enterprise, controllers: { confirmations: 'enterprise_confirmations' } @@ -57,7 +76,8 @@ Openfoodnetwork::Application.routes.draw do end member do - put :set_sells + get :welcome + put :register end resources :producer_properties do @@ -85,6 +105,17 @@ Openfoodnetwork::Application.routes.draw do end resources :customers, only: [:index, :update] + + resource :content + + resource :accounts_and_billing_settings, only: [:edit, :update] do + collection do + get :show_methods + get :start_job + end + end + + resource :account, only: [:show], controller: 'account' end namespace :api do @@ -99,8 +130,6 @@ Openfoodnetwork::Application.routes.draw do end end - get "about_us", :controller => 'home', :action => "about_us" - namespace :open_food_network do resources :cart do post :add_variant @@ -130,6 +159,7 @@ end Spree::Core::Engine.routes.prepend do match '/admin/reports/orders_and_distributors' => 'admin/reports#orders_and_distributors', :as => "orders_and_distributors_admin_reports", :via => [:get, :post] match '/admin/reports/order_cycle_management' => 'admin/reports#order_cycle_management', :as => "order_cycle_management_admin_reports", :via => [:get, :post] + match '/admin/reports/packing' => 'admin/reports#packing', :as => "packing_admin_reports", :via => [:get, :post] match '/admin/reports/group_buys' => 'admin/reports#group_buys', :as => "group_buys_admin_reports", :via => [:get, :post] match '/admin/reports/bulk_coop' => 'admin/reports#bulk_coop', :as => "bulk_coop_admin_reports", :via => [:get, :post] match '/admin/reports/payments' => 'admin/reports#payments', :as => "payments_admin_reports", :via => [:get, :post] diff --git a/config/schedule.rb b/config/schedule.rb index 0c98f72dc6..a09ca55dce 100644 --- a/config/schedule.rb +++ b/config/schedule.rb @@ -18,3 +18,12 @@ end every 4.hours do rake 'db2fog:backup' end + +every 1.day, at: '1:00am' do + rake 'openfoodnetwork:billing:update_account_invoices' +end + +# On the 2nd of every month at 1:30am +every '30 1 2 * *' do + rake 'openfoodnetwork:billing:finalize_account_invoices' +end diff --git a/config/unicorn.rb b/config/unicorn.rb index e5d7317940..22e954b4d0 100644 --- a/config/unicorn.rb +++ b/config/unicorn.rb @@ -1,27 +1,2 @@ -preload_app true # https://newrelic.com/docs/ruby/no-data-with-unicorn -worker_processes 4 # amount of unicorn workers to spin up -timeout 60 # restarts workers that hang for 30 seconds - - -# https://devcenter.heroku.com/articles/forked-pg-connections -before_fork do |server, worker| - - Signal.trap 'TERM' do - puts 'Unicorn master intercepting TERM and sending myself QUIT instead' - Process.kill 'QUIT', Process.pid - end - - defined?(ActiveRecord::Base) and - ActiveRecord::Base.connection.disconnect! -end - -after_fork do |server, worker| - - Signal.trap 'TERM' do - puts 'Unicorn worker intercepting TERM and doing nothing. Wait for master to sent QUIT' - end - - defined?(ActiveRecord::Base) and - ActiveRecord::Base.establish_connection - -end +worker_processes 2 # amount of unicorn workers to spin up +timeout 120 # restarts workers that hang for 30 seconds diff --git a/db/migrate/20150612045544_make_enterprises_name_unique.rb b/db/migrate/20150612045544_make_enterprises_name_unique.rb new file mode 100644 index 0000000000..5722284d94 --- /dev/null +++ b/db/migrate/20150612045544_make_enterprises_name_unique.rb @@ -0,0 +1,20 @@ +class MakeEnterprisesNameUnique < ActiveRecord::Migration + def up + dup_names = Enterprise.group('name').select('name, COUNT(*) AS num_enterprises') + + dup_names.each do |data| + (data.num_enterprises.to_i - 1).times do |i| + e = Enterprise.find_by_name data.name + new_name = "#{data.name}-#{i+1}" + e.update_column :name, new_name + say "Renamed enterprise #{data.name} to #{new_name}" + end + end + + add_index :enterprises, :name, unique: true + end + + def down + remove_index :enterprises, :name + end +end diff --git a/db/migrate/20150619020711_create_versions.rb b/db/migrate/20150619020711_create_versions.rb new file mode 100644 index 0000000000..23be970c66 --- /dev/null +++ b/db/migrate/20150619020711_create_versions.rb @@ -0,0 +1,13 @@ +class CreateVersions < ActiveRecord::Migration + def change + create_table :versions do |t| + t.string :item_type, :null => false + t.integer :item_id, :null => false + t.string :event, :null => false + t.string :whodunnit + t.text :object + t.datetime :created_at + end + add_index :versions, [:item_type, :item_id] + end +end diff --git a/db/migrate/20150619100137_create_bill_items.rb b/db/migrate/20150619100137_create_bill_items.rb new file mode 100644 index 0000000000..fec80692b1 --- /dev/null +++ b/db/migrate/20150619100137_create_bill_items.rb @@ -0,0 +1,15 @@ +class CreateBillItems < ActiveRecord::Migration + def change + create_table :bill_items do |t| + t.references :enterprise, nil: false + t.references :owner, nil: false + t.datetime :begins_at, default: nil + t.datetime :ends_at, default: nil + t.string :sells, default: nil + t.boolean :trial, default: false + t.decimal :turnover, default: 0.0 + t.foreign_key :enterprises + t.foreign_key :spree_users, column: :owner_id + end + end +end diff --git a/db/migrate/20150626090338_rename_bill_items_to_billable_periods.rb b/db/migrate/20150626090338_rename_bill_items_to_billable_periods.rb new file mode 100644 index 0000000000..caa2fb9a4d --- /dev/null +++ b/db/migrate/20150626090338_rename_bill_items_to_billable_periods.rb @@ -0,0 +1,9 @@ +class RenameBillItemsToBillablePeriods < ActiveRecord::Migration + def up + rename_table :bill_items, :billable_periods + end + + def down + rename_table :billable_periods, :bill_items + end +end diff --git a/db/migrate/20150701034055_add_timestamps_to_billable_periods.rb b/db/migrate/20150701034055_add_timestamps_to_billable_periods.rb new file mode 100644 index 0000000000..6c3ea52356 --- /dev/null +++ b/db/migrate/20150701034055_add_timestamps_to_billable_periods.rb @@ -0,0 +1,8 @@ +class AddTimestampsToBillablePeriods < ActiveRecord::Migration + def change + change_table(:billable_periods) do |t| + t.datetime :deleted_at, default: nil + t.timestamps + end + end +end diff --git a/db/migrate/20150719153136_rename_line_item_unit_value.rb b/db/migrate/20150719153136_rename_line_item_unit_value.rb new file mode 100644 index 0000000000..9dbdce75c5 --- /dev/null +++ b/db/migrate/20150719153136_rename_line_item_unit_value.rb @@ -0,0 +1,5 @@ +class RenameLineItemUnitValue < ActiveRecord::Migration + def change + rename_column :spree_line_items, :unit_value, :final_weight_volume + end +end diff --git a/db/migrate/20150719153732_update_precision_on_line_item_final_weight_volume.rb b/db/migrate/20150719153732_update_precision_on_line_item_final_weight_volume.rb new file mode 100644 index 0000000000..c1b3482c0f --- /dev/null +++ b/db/migrate/20150719153732_update_precision_on_line_item_final_weight_volume.rb @@ -0,0 +1,11 @@ +class UpdatePrecisionOnLineItemFinalWeightVolume < ActiveRecord::Migration + def up + change_column :spree_line_items, :final_weight_volume, :decimal, :precision => 10, :scale => 2 + end + + def down + raise ActiveRecord::IrreversibleMigration + end +end + + diff --git a/db/migrate/20150730160010_update_spree_line_item_final_weight_volume.rb b/db/migrate/20150730160010_update_spree_line_item_final_weight_volume.rb new file mode 100644 index 0000000000..4d51a7160b --- /dev/null +++ b/db/migrate/20150730160010_update_spree_line_item_final_weight_volume.rb @@ -0,0 +1,9 @@ +class UpdateSpreeLineItemFinalWeightVolume < ActiveRecord::Migration + def up + execute "UPDATE spree_line_items SET final_weight_volume = final_weight_volume * quantity" + end + + def down + raise ActiveRecord::IrreversibleMigration + end +end diff --git a/db/migrate/20150916012814_create_account_invoices.rb b/db/migrate/20150916012814_create_account_invoices.rb new file mode 100644 index 0000000000..7e92a53593 --- /dev/null +++ b/db/migrate/20150916012814_create_account_invoices.rb @@ -0,0 +1,18 @@ +class CreateAccountInvoices < ActiveRecord::Migration + def change + create_table :account_invoices do |t| + t.references :user, null: false + t.references :order + t.integer :year, null: false + t.integer :month, null: false + t.datetime :issued_at + + t.timestamps + end + add_index :account_invoices, :user_id + add_index :account_invoices, :order_id + + add_foreign_key :account_invoices, :spree_orders, column: :order_id + add_foreign_key :account_invoices, :spree_users, column: :user_id + end +end diff --git a/db/migrate/20150916061809_add_account_invoice_to_billable_periods.rb b/db/migrate/20150916061809_add_account_invoice_to_billable_periods.rb new file mode 100644 index 0000000000..ee1d9d5d93 --- /dev/null +++ b/db/migrate/20150916061809_add_account_invoice_to_billable_periods.rb @@ -0,0 +1,7 @@ +class AddAccountInvoiceToBillablePeriods < ActiveRecord::Migration + def change + add_column :billable_periods, :account_invoice_id, :integer, null: false + add_index :billable_periods, :account_invoice_id + add_foreign_key :billable_periods, :account_invoices, column: :account_invoice_id + end +end diff --git a/db/migrate/20151002020537_ensure_address_for_account_invoice_orders.rb b/db/migrate/20151002020537_ensure_address_for_account_invoice_orders.rb new file mode 100644 index 0000000000..6d2c7d45c5 --- /dev/null +++ b/db/migrate/20151002020537_ensure_address_for_account_invoice_orders.rb @@ -0,0 +1,15 @@ +class EnsureAddressForAccountInvoiceOrders < ActiveRecord::Migration + def up + AccountInvoice.where('order_id IS NOT NULL').each do |account_invoice| + billable_periods = account_invoice.billable_periods.order(:enterprise_id).reject{ |bp| bp.turnover == 0 } + + if billable_periods.any? + address = billable_periods.first.enterprise.address + account_invoice.order.update_attributes(bill_address: address, ship_address: address) + end + end + end + + def down + end +end diff --git a/db/schema.rb b/db/schema.rb index 48309e3fd7..37ad6f1a44 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,20 @@ # # It's strongly recommended to check this file into your version control system. -ActiveRecord::Schema.define(:version => 20150605052516) do +ActiveRecord::Schema.define(:version => 20151002020537) do + + create_table "account_invoices", :force => true do |t| + t.integer "user_id", :null => false + t.integer "order_id" + t.integer "year", :null => false + t.integer "month", :null => false + t.datetime "issued_at" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + end + + add_index "account_invoices", ["order_id"], :name => "index_account_invoices_on_order_id" + add_index "account_invoices", ["user_id"], :name => "index_account_invoices_on_user_id" create_table "adjustment_metadata", :force => true do |t| t.integer "adjustment_id" @@ -24,6 +37,22 @@ ActiveRecord::Schema.define(:version => 20150605052516) do add_index "adjustment_metadata", ["adjustment_id"], :name => "index_adjustment_metadata_on_adjustment_id" add_index "adjustment_metadata", ["enterprise_id"], :name => "index_adjustment_metadata_on_enterprise_id" + create_table "billable_periods", :force => true do |t| + t.integer "enterprise_id" + t.integer "owner_id" + t.datetime "begins_at" + t.datetime "ends_at" + t.string "sells" + t.boolean "trial", :default => false + t.decimal "turnover", :default => 0.0 + t.datetime "deleted_at" + t.datetime "created_at" + t.datetime "updated_at" + t.integer "account_invoice_id", :null => false + end + + add_index "billable_periods", ["account_invoice_id"], :name => "index_billable_periods_on_account_invoice_id" + create_table "carts", :force => true do |t| t.integer "user_id" end @@ -322,6 +351,7 @@ ActiveRecord::Schema.define(:version => 20150605052516) do add_index "enterprises", ["address_id"], :name => "index_enterprises_on_address_id" add_index "enterprises", ["confirmation_token"], :name => "index_enterprises_on_confirmation_token", :unique => true add_index "enterprises", ["is_primary_producer", "sells"], :name => "index_enterprises_on_is_primary_producer_and_sells" + add_index "enterprises", ["name"], :name => "index_enterprises_on_name", :unique => true add_index "enterprises", ["owner_id"], :name => "index_enterprises_on_owner_id" add_index "enterprises", ["permalink"], :name => "index_enterprises_on_permalink", :unique => true add_index "enterprises", ["sells"], :name => "index_enterprises_on_sells" @@ -564,7 +594,7 @@ ActiveRecord::Schema.define(:version => 20150605052516) do t.string "currency" t.decimal "distribution_fee", :precision => 10, :scale => 2 t.string "shipping_method_name" - t.decimal "unit_value", :precision => 8, :scale => 2 + t.decimal "final_weight_volume", :precision => 10, :scale => 2 end add_index "spree_line_items", ["order_id"], :name => "index_line_items_on_order_id" @@ -1127,9 +1157,27 @@ ActiveRecord::Schema.define(:version => 20150605052516) do add_index "variant_overrides", ["variant_id", "hub_id"], :name => "index_variant_overrides_on_variant_id_and_hub_id" + create_table "versions", :force => true do |t| + t.string "item_type", :null => false + t.integer "item_id", :null => false + t.string "event", :null => false + t.string "whodunnit" + t.text "object" + t.datetime "created_at" + end + + add_index "versions", ["item_type", "item_id"], :name => "index_versions_on_item_type_and_item_id" + + add_foreign_key "account_invoices", "spree_orders", name: "account_invoices_order_id_fk", column: "order_id" + add_foreign_key "account_invoices", "spree_users", name: "account_invoices_user_id_fk", column: "user_id" + add_foreign_key "adjustment_metadata", "enterprises", name: "adjustment_metadata_enterprise_id_fk" add_foreign_key "adjustment_metadata", "spree_adjustments", name: "adjustment_metadata_adjustment_id_fk", column: "adjustment_id", dependent: :delete + add_foreign_key "billable_periods", "account_invoices", name: "billable_periods_account_invoice_id_fk" + add_foreign_key "billable_periods", "enterprises", name: "bill_items_enterprise_id_fk" + add_foreign_key "billable_periods", "spree_users", name: "bill_items_owner_id_fk", column: "owner_id" + add_foreign_key "carts", "spree_users", name: "carts_user_id_fk", column: "user_id" add_foreign_key "cms_blocks", "cms_pages", name: "cms_blocks_page_id_fk", column: "page_id" diff --git a/lib/open_food_network/accounts_and_billing_settings_validator.rb b/lib/open_food_network/accounts_and_billing_settings_validator.rb new file mode 100644 index 0000000000..0269f9ff43 --- /dev/null +++ b/lib/open_food_network/accounts_and_billing_settings_validator.rb @@ -0,0 +1,41 @@ +# This class is a lightweight model used to validate preferences for accounts and billing settings +# when they are submitted to the AccountsAndBillingSettingsController + +module OpenFoodNetwork + class AccountsAndBillingSettingsValidator + include ActiveModel::Validations + + attr_accessor :accounts_distributor_id, :default_accounts_payment_method_id, :default_accounts_shipping_method_id + attr_accessor :auto_update_invoices, :auto_finalize_invoices + + validate :ensure_accounts_distributor_set + validate :ensure_default_payment_method_set + validate :ensure_default_shipping_method_set + # validate :ensure_billing_info_collected, unless: lambda { create_invoices_for_enterprise_users == '0' } + + def initialize(attr, button=nil) + attr.each { |k,v| instance_variable_set("@#{k}", v) } + @button = button + end + + def ensure_accounts_distributor_set + unless Enterprise.find_by_id(accounts_distributor_id) + errors.add(:accounts_distributor, "must be set if you wish to create invoices for enterprise users.") + end + end + + def ensure_default_payment_method_set + unless Enterprise.find_by_id(accounts_distributor_id) && + Enterprise.find_by_id(accounts_distributor_id).payment_methods.find_by_id(default_accounts_payment_method_id) + errors.add(:default_payment_method, "must be set if you wish to create invoices for enterprise users.") + end + end + + def ensure_default_shipping_method_set + unless Enterprise.find_by_id(accounts_distributor_id) && + Enterprise.find_by_id(accounts_distributor_id).shipping_methods.find_by_id(default_accounts_shipping_method_id) + errors.add(:default_shipping_method, "must be set if you wish to create invoices for enterprise users.") + end + end + end +end diff --git a/lib/open_food_network/bulk_coop_report.rb b/lib/open_food_network/bulk_coop_report.rb new file mode 100644 index 0000000000..57f34d9edc --- /dev/null +++ b/lib/open_food_network/bulk_coop_report.rb @@ -0,0 +1,104 @@ +require 'open_food_network/reports/bulk_coop_supplier_report' +require 'open_food_network/reports/bulk_coop_allocation_report' + +module OpenFoodNetwork + class BulkCoopReport + attr_reader :params + def initialize(user, params = {}) + @params = params + @user = user + + @supplier_report = OpenFoodNetwork::Reports::BulkCoopSupplierReport.new + @allocation_report = OpenFoodNetwork::Reports::BulkCoopAllocationReport.new + end + + def header + case params[:report_type] + when "bulk_coop_supplier_report" + @supplier_report.header + when "bulk_coop_allocation" + @allocation_report.header + when "bulk_coop_packing_sheets" + ["Customer", "Product", "Variant", "Sum Total"] + when "bulk_coop_customer_payments" + ["Customer", "Date of Order", "Total Cost", "Amount Owing", "Amount Paid"] + else + ["Supplier", "Product", "Bulk Unit Size", "Variant", "Weight", "Sum Total", "Sum Max Total", "Units Required", "Remainder"] + end + end + + def search + Spree::Order.complete.not_state(:canceled).managed_by(@user).search(params[:q]) + end + + def table_items + orders = search.result + orders.map { |o| o.line_items.managed_by(@user) }.flatten + end + + def rules + case params[:report_type] + when "bulk_coop_supplier_report" + @supplier_report.rules + when "bulk_coop_allocation" + @allocation_report.rules + when "bulk_coop_packing_sheets" + [ { group_by: proc { |li| li.variant.product }, + sort_by: proc { |product| product.name } }, + { group_by: proc { |li| li.variant }, + sort_by: proc { |variant| variant.full_name } }, + { group_by: proc { |li| li.order }, + sort_by: proc { |order| order.to_s } } ] + when "bulk_coop_customer_payments" + [ { group_by: proc { |li| li.order }, + sort_by: proc { |order| order.completed_at } } ] + else + [ { group_by: proc { |li| li.variant.product.supplier }, + sort_by: proc { |supplier| supplier.name } }, + { group_by: proc { |li| li.variant.product }, + sort_by: proc { |product| product.name }, + summary_columns: [ proc { |lis| lis.first.variant.product.supplier.name }, + proc { |lis| lis.first.variant.product.name }, + proc { |lis| lis.first.variant.product.group_buy_unit_size || 0.0 }, + proc { |lis| "" }, + proc { |lis| "" }, + proc { |lis| lis.sum { |li| li.quantity * (li.variant.weight || 0) } }, + proc { |lis| lis.sum { |li| (li.max_quantity || 0) * (li.variant.weight || 0) } }, + proc { |lis| ( (lis.first.variant.product.group_buy_unit_size || 0).zero? ? 0 : ( lis.sum { |li| ( [li.max_quantity || 0, li.quantity || 0].max ) * (li.variant.weight || 0) } / lis.first.variant.product.group_buy_unit_size ) ).floor }, + proc { |lis| lis.sum { |li| ( [li.max_quantity || 0, li.quantity || 0].max ) * (li.variant.weight || 0) } - ( ( (lis.first.variant.product.group_buy_unit_size || 0).zero? ? 0 : ( lis.sum { |li| ( [li.max_quantity || 0, li.quantity || 0].max ) * (li.variant.weight || 0) } / lis.first.variant.product.group_buy_unit_size ) ).floor * (lis.first.variant.product.group_buy_unit_size || 0) ) } ] }, + { group_by: proc { |li| li.variant }, + sort_by: proc { |variant| variant.full_name } } ] + end + end + + def columns + case params[:report_type] + when "bulk_coop_supplier_report" + @supplier_report.columns + when "bulk_coop_allocation" + @allocation_report.columns + when "bulk_coop_packing_sheets" + [ proc { |lis| lis.first.order.bill_address.firstname + " " + lis.first.order.bill_address.lastname }, + proc { |lis| lis.first.variant.product.name }, + proc { |lis| lis.first.variant.full_name }, + proc { |lis| lis.sum { |li| li.quantity } } ] + when "bulk_coop_customer_payments" + [ proc { |lis| lis.first.order.bill_address.firstname + " " + lis.first.order.bill_address.lastname }, + proc { |lis| lis.first.order.completed_at.to_s }, + proc { |lis| lis.map { |li| li.order }.uniq.sum { |o| o.total } }, + proc { |lis| lis.map { |li| li.order }.uniq.sum { |o| o.outstanding_balance } }, + proc { |lis| lis.map { |li| li.order }.uniq.sum { |o| o.payment_total } } ] + else + [ proc { |lis| lis.first.variant.product.supplier.name }, + proc { |lis| lis.first.variant.product.name }, + proc { |lis| lis.first.variant.product.group_buy_unit_size || 0.0 }, + proc { |lis| lis.first.variant.full_name }, + proc { |lis| lis.first.variant.weight || 0 }, + proc { |lis| lis.sum { |li| li.quantity } }, + proc { |lis| lis.sum { |li| li.max_quantity || 0 } }, + proc { |lis| "" }, + proc { |lis| "" } ] + end + end + end +end diff --git a/lib/open_food_network/enterprise_fee_calculator.rb b/lib/open_food_network/enterprise_fee_calculator.rb index c8070f2ab2..d65a80c0dc 100644 --- a/lib/open_food_network/enterprise_fee_calculator.rb +++ b/lib/open_food_network/enterprise_fee_calculator.rb @@ -7,17 +7,35 @@ module OpenFoodNetwork @order_cycle = order_cycle end + def indexed_fees_for(variant) + load_enterprise_fees unless @indexed_enterprise_fees + + indexed_enterprise_fees_for(variant).sum do |enterprise_fee| + calculate_fee_for variant, enterprise_fee + end + end + + def indexed_fees_by_type_for(variant) + load_enterprise_fees unless @indexed_enterprise_fees + + indexed_enterprise_fees_for(variant).inject({}) do |fees, enterprise_fee| + fees[enterprise_fee.fee_type.to_sym] ||= 0 + fees[enterprise_fee.fee_type.to_sym] += calculate_fee_for variant, enterprise_fee + fees + end.select { |fee_type, amount| amount > 0 } + end + def fees_for(variant) per_item_enterprise_fee_applicators_for(variant).sum do |applicator| - calculate_fee_for variant, applicator + calculate_fee_for variant, applicator.enterprise_fee end end def fees_by_type_for(variant) per_item_enterprise_fee_applicators_for(variant).inject({}) do |fees, applicator| fees[applicator.enterprise_fee.fee_type.to_sym] ||= 0 - fees[applicator.enterprise_fee.fee_type.to_sym] += calculate_fee_for variant, applicator + fees[applicator.enterprise_fee.fee_type.to_sym] += calculate_fee_for variant, applicator.enterprise_fee fees end.select { |fee_type, amount| amount > 0 } end @@ -45,12 +63,50 @@ module OpenFoodNetwork private - def calculate_fee_for(variant, applicator) + def load_enterprise_fees + @indexed_enterprise_fees = {} + + exchange_fees = per_item_enterprise_fees_with_exchange_details + load_exchange_fees exchange_fees + load_coordinator_fees + end + + def per_item_enterprise_fees_with_exchange_details + EnterpriseFee. + per_item. + joins(:exchanges => :exchange_variants). + where('exchanges.order_cycle_id = ?', @order_cycle.id). + merge(Exchange.supplying_to(@distributor)). + select('enterprise_fees.*, exchange_variants.variant_id AS variant_id') + end + + def load_exchange_fees(exchange_fees) + exchange_fees.each do |enterprise_fee| + @indexed_enterprise_fees[enterprise_fee.variant_id.to_i] ||= [] + @indexed_enterprise_fees[enterprise_fee.variant_id.to_i] << enterprise_fee + end + end + + def load_coordinator_fees + @order_cycle.coordinator_fees.per_item.each do |enterprise_fee| + @order_cycle.variants.map(&:id).each do |variant_id| + @indexed_enterprise_fees[variant_id] ||= [] + @indexed_enterprise_fees[variant_id] << enterprise_fee + end + end + end + + def indexed_enterprise_fees_for(variant) + @indexed_enterprise_fees[variant.id] || [] + end + + + def calculate_fee_for(variant, enterprise_fee) # Spree's Calculator interface accepts Orders or LineItems, # so we meet that interface with a struct. # Amount is faked, this is a method on LineItem line_item = OpenStruct.new variant: variant, quantity: 1, amount: variant.price - applicator.enterprise_fee.compute_amount(line_item) + enterprise_fee.compute_amount(line_item) end def per_item_enterprise_fee_applicators_for(variant) diff --git a/lib/open_food_network/lettuce_share_report.rb b/lib/open_food_network/lettuce_share_report.rb new file mode 100644 index 0000000000..1dc283eba8 --- /dev/null +++ b/lib/open_food_network/lettuce_share_report.rb @@ -0,0 +1,78 @@ +require 'open_food_network/products_and_inventory_report_base' + +module OpenFoodNetwork + class LettuceShareReport < ProductsAndInventoryReportBase + def header + [ + "PRODUCT", + "Description", + "Qty", + "Pack Size", + "Unit", + "Unit Price", + "Total", + "GST incl.", + "Grower and growing method", + "Taxon" + ] + end + + def table + variants.map do |variant| + [ + variant.product.name, + variant.full_name, + '', + OptionValueNamer.new(variant).value, + OptionValueNamer.new(variant).unit, + variant.price - gst(variant), + variant.price, + gst(variant), + grower_and_method(variant), + variant.product.primary_taxon.name + ] + end + end + + + private + + def gst(variant) + tax_category = variant.product.tax_category + if tax_category && tax_category.tax_rates.present? + tax_rate = tax_category.tax_rates.first + line_item = mock_line_item(variant, tax_category) + tax_rate.calculator.compute line_item + else + 0 + end + end + + def mock_line_item(variant, tax_category) + product = OpenStruct.new tax_category: tax_category + line_item = Spree::LineItem.new quantity: 1 + line_item.define_singleton_method(:product) { variant.product } + line_item.define_singleton_method(:price) { variant.price } + line_item + end + + def grower_and_method(variant) + cert = certification(variant) + + result = producer_name(variant) + result += " (#{cert})" if cert.present? + result + end + + def producer_name(variant) + variant.product.supplier.name + end + + def certification(variant) + variant.product.properties_including_inherited.map do |p| + "#{p[:name]} - #{p[:value]}" + end.join(', ') + end + + end +end diff --git a/lib/open_food_network/option_value_namer.rb b/lib/open_food_network/option_value_namer.rb index 2c2383906d..6a77d9cb35 100644 --- a/lib/open_food_network/option_value_namer.rb +++ b/lib/open_food_network/option_value_namer.rb @@ -15,6 +15,17 @@ module OpenFoodNetwork name_fields.join ' ' end + def value + value, _ = option_value_value_unit + value + end + + def unit + _, unit = option_value_value_unit + unit + end + + private def value_scaled? diff --git a/lib/open_food_network/order_grouper.rb b/lib/open_food_network/order_grouper.rb index c0a0fc51ea..c24d1069b8 100644 --- a/lib/open_food_network/order_grouper.rb +++ b/lib/open_food_network/order_grouper.rb @@ -8,7 +8,7 @@ module OpenFoodNetwork def build_tree(items, remaining_rules) rules = remaining_rules.clone - unless rules.empty? + if rules.any? rule = rules.delete_at(0) # Remove current rule for subsequent groupings group_and_sort(rule, rules, items) else @@ -20,7 +20,7 @@ module OpenFoodNetwork branch = {} groups = items.group_by { |item| rule[:group_by].call(item) } sorted_groups = groups.sort_by { |key, value| rule[:sort_by].call(key) } - sorted_groups.each do |property,items_by_property| + sorted_groups.each do |property, items_by_property| branch[property] = build_tree(items_by_property, remaining_rules) branch[property][:summary_row] = { items: items_by_property, columns: rule[:summary_columns] } unless rule[:summary_columns] == nil || is_leaf_node(branch[property]) end @@ -44,15 +44,15 @@ module OpenFoodNetwork end def table(items) - tree = build_tree(items,@rules) + tree = build_tree(items, @rules) table = build_table(tree) table end - + private - + def is_leaf_node(node) node.is_a? Array end end -end \ No newline at end of file +end diff --git a/lib/open_food_network/orders_and_fulfillments_report.rb b/lib/open_food_network/orders_and_fulfillments_report.rb new file mode 100644 index 0000000000..03c4ec801a --- /dev/null +++ b/lib/open_food_network/orders_and_fulfillments_report.rb @@ -0,0 +1,256 @@ +include Spree::ReportsHelper + +module OpenFoodNetwork + class OrdersAndFulfillmentsReport + attr_reader :params + def initialize(user, params = {}) + @params = params + @user = user + end + + def header + case params[:report_type] + when "order_cycle_supplier_totals" + ["Producer", "Product", "Variant", "Amount", "Total Units", "Curr. Cost per Unit", "Total Cost", "Status", "Incoming Transport"] + when "order_cycle_supplier_totals_by_distributor" + ["Producer", "Product", "Variant", "To Hub", "Amount", "Curr. Cost per Unit", "Total Cost", "Shipping Method"] + when "order_cycle_distributor_totals_by_supplier" + ["Hub", "Producer", "Product", "Variant", "Amount", "Curr. Cost per Unit", "Total Cost", "Total Shipping Cost", "Shipping Method"] + when "order_cycle_customer_totals" + ["Hub", "Customer", "Email", "Phone", "Producer", "Product", "Variant", + "Amount", "Item (#{currency_symbol})", "Item + Fees (#{currency_symbol})", "Admin & Handling (#{currency_symbol})", "Ship (#{currency_symbol})", "Total (#{currency_symbol})", "Paid?", + "Shipping", "Delivery?", + "Ship Street", "Ship Street 2", "Ship City", "Ship Postcode", "Ship State", + "Comments", "SKU", + "Order Cycle", "Payment Method", "Customer Code", "Tags", + "Billing Street 1", "Billing Street 2", "Billing City", "Billing Postcode", "Billing State" + ] + else + ["Producer", "Product", "Variant", "Amount", "Curr. Cost per Unit", "Total Cost", "Status", "Incoming Transport"] + end + + end + + def search + Spree::Order.complete.not_state(:canceled).search(params[:q]) + end + + def table_items + permissions = OpenFoodNetwork::Permissions.new(@user) + orders = permissions.visible_orders.merge(search.result) + + line_items = permissions.visible_line_items.merge(Spree::LineItem.where(order_id: orders)) + line_items = line_items.supplied_by_any(params[:supplier_id_in]) if params[:supplier_id_in].present? + + line_items_with_hidden_details = line_items.where('"spree_line_items"."id" NOT IN (?)', permissions.editable_line_items) + line_items.select{ |li| line_items_with_hidden_details.include? li }.each do |line_item| + # TODO We should really be hiding customer code here too, but until we + # have an actual association between order and customer, it's a bit tricky + line_item.order.bill_address.assign_attributes(firstname: "HIDDEN", lastname: "", phone: "", address1: "", address2: "", city: "", zipcode: "", state: nil) + line_item.order.ship_address.assign_attributes(firstname: "HIDDEN", lastname: "", phone: "", address1: "", address2: "", city: "", zipcode: "", state: nil) + line_item.order.assign_attributes(email: "HIDDEN") + end + line_items + end + + def rules + case params[:report_type] + when "order_cycle_supplier_totals" + [ { group_by: proc { |line_item| line_item.variant.product.supplier }, + sort_by: proc { |supplier| supplier.name } }, + { group_by: proc { |line_item| line_item.variant.product }, + sort_by: proc { |product| product.name } }, + { group_by: proc { |line_item| line_item.variant }, + sort_by: proc { |variant| variant.full_name } } ] + when "order_cycle_supplier_totals_by_distributor" + [ { group_by: proc { |line_item| line_item.variant.product.supplier }, + sort_by: proc { |supplier| supplier.name } }, + { group_by: proc { |line_item| line_item.variant.product }, + sort_by: proc { |product| product.name } }, + { group_by: proc { |line_item| line_item.variant }, + sort_by: proc { |variant| variant.full_name }, + summary_columns: [ proc { |line_items| "" }, + proc { |line_items| "" }, + proc { |line_items| "" }, + proc { |line_items| "TOTAL" }, + proc { |line_items| "" }, + proc { |line_items| "" }, + proc { |line_items| line_items.sum { |li| li.amount } }, + proc { |line_items| "" } ] }, + { group_by: proc { |line_item| line_item.order.distributor }, + sort_by: proc { |distributor| distributor.name } } ] + when "order_cycle_distributor_totals_by_supplier" + [ { group_by: proc { |line_item| line_item.order.distributor }, + sort_by: proc { |distributor| distributor.name }, + summary_columns: [ proc { |line_items| "" }, + proc { |line_items| "TOTAL" }, + proc { |line_items| "" }, + proc { |line_items| "" }, + proc { |line_items| "" }, + proc { |line_items| "" }, + proc { |line_items| line_items.sum { |li| li.amount } }, + proc { |line_items| line_items.map { |li| li.order }.uniq.sum { |o| o.ship_total } }, + proc { |line_items| "" } ] }, + { group_by: proc { |line_item| line_item.variant.product.supplier }, + sort_by: proc { |supplier| supplier.name } }, + { group_by: proc { |line_item| line_item.variant.product }, + sort_by: proc { |product| product.name } }, + { group_by: proc { |line_item| line_item.variant }, + sort_by: proc { |variant| variant.full_name } } ] + when "order_cycle_customer_totals" + [ { group_by: proc { |line_item| line_item.order.distributor }, + sort_by: proc { |distributor| distributor.name } }, + { group_by: proc { |line_item| line_item.order }, + sort_by: proc { |order| order.bill_address.lastname + " " + order.bill_address.firstname }, + summary_columns: [ + proc { |line_items| line_items.first.order.distributor.name }, + proc { |line_items| line_items.first.order.bill_address.firstname + " " + line_items.first.order.bill_address.lastname }, + proc { |line_items| "" }, + proc { |line_items| "" }, + proc { |line_items| "" }, + proc { |line_items| "TOTAL" }, + proc { |line_items| "" }, + + proc { |line_items| "" }, + proc { |line_items| line_items.sum { |li| li.amount } }, + proc { |line_items| line_items.sum { |li| li.amount_with_adjustments } }, + proc { |line_items| line_items.map { |li| li.order }.uniq.sum { |o| o.admin_and_handling_total } }, + proc { |line_items| line_items.map { |li| li.order }.uniq.sum { |o| o.ship_total } }, + proc { |line_items| line_items.map { |li| li.order }.uniq.sum { |o| o.total } }, + proc { |line_items| line_items.all? { |li| li.order.paid? } ? "Yes" : "No" }, + + proc { |line_items| "" }, + proc { |line_items| "" }, + + proc { |line_items| "" }, + proc { |line_items| "" }, + proc { |line_items| "" }, + proc { |line_items| "" }, + proc { |line_items| "" }, + + proc { |line_items| line_items.first.order.special_instructions } , + proc { |line_items| "" }, + + proc { |line_items| line_items.first.order.order_cycle.andand.name }, + proc { |line_items| line_items.first.order.payments.first.andand.payment_method.andand.name }, + proc { |line_items| "" }, + proc { |line_items| "" }, + + proc { |line_items| "" }, + proc { |line_items| "" }, + proc { |line_items| "" }, + proc { |line_items| "" }, + proc { |line_items| "" } + ] }, + + { group_by: proc { |line_item| line_item.variant.product }, + sort_by: proc { |product| product.name } }, + { group_by: proc { |line_item| line_item.variant }, + sort_by: proc { |variant| variant.full_name } } ] + else + [ { group_by: proc { |line_item| line_item.variant.product.supplier }, + sort_by: proc { |supplier| supplier.name } }, + { group_by: proc { |line_item| line_item.variant.product }, + sort_by: proc { |product| product.name } }, + { group_by: proc { |line_item| line_item.variant }, + sort_by: proc { |variant| variant.full_name } } ] + end + end + + def columns + case params[:report_type] + when "order_cycle_supplier_totals" + [ proc { |line_items| line_items.first.variant.product.supplier.name }, + proc { |line_items| line_items.first.variant.product.name }, + proc { |line_items| line_items.first.variant.full_name }, + proc { |line_items| line_items.sum { |li| li.quantity } }, + proc { |line_items| total_units(line_items) }, + proc { |line_items| line_items.first.price }, + proc { |line_items| line_items.sum { |li| li.amount } }, + proc { |line_items| "" }, + proc { |line_items| "incoming transport" } ] + when "order_cycle_supplier_totals_by_distributor" + [ proc { |line_items| line_items.first.variant.product.supplier.name }, + proc { |line_items| line_items.first.variant.product.name }, + proc { |line_items| line_items.first.variant.full_name }, + proc { |line_items| line_items.first.order.distributor.name }, + proc { |line_items| line_items.sum { |li| li.quantity } }, + proc { |line_items| line_items.first.price }, + proc { |line_items| line_items.sum { |li| li.amount } }, + proc { |line_items| "shipping method" } ] + when "order_cycle_distributor_totals_by_supplier" + [ proc { |line_items| line_items.first.order.distributor.name }, + proc { |line_items| line_items.first.variant.product.supplier.name }, + proc { |line_items| line_items.first.variant.product.name }, + proc { |line_items| line_items.first.variant.full_name }, + proc { |line_items| line_items.sum { |li| li.quantity } }, + proc { |line_items| line_items.first.price }, + proc { |line_items| line_items.sum { |li| li.amount } }, + proc { |line_items| "" }, + proc { |line_items| "shipping method" } ] + when "order_cycle_customer_totals" + rsa = proc { |line_items| line_items.first.order.shipping_method.andand.require_ship_address } + [ + proc { |line_items| line_items.first.order.distributor.name }, + proc { |line_items| line_items.first.order.bill_address.firstname + " " + line_items.first.order.bill_address.lastname }, + proc { |line_items| line_items.first.order.email }, + proc { |line_items| line_items.first.order.bill_address.phone }, + proc { |line_items| line_items.first.variant.product.supplier.name }, + proc { |line_items| line_items.first.variant.product.name }, + proc { |line_items| line_items.first.variant.full_name }, + + proc { |line_items| line_items.sum { |li| li.quantity } }, + proc { |line_items| line_items.sum { |li| li.amount } }, + proc { |line_items| line_items.sum { |li| li.amount_with_adjustments } }, + proc { |line_items| "" }, + proc { |line_items| "" }, + proc { |line_items| "" }, + proc { |line_items| "" }, + + proc { |line_items| line_items.first.order.shipping_method.andand.name }, + proc { |line_items| rsa.call(line_items) ? 'Y' : 'N' }, + + proc { |line_items| line_items.first.order.ship_address.andand.address1 if rsa.call(line_items) }, + proc { |line_items| line_items.first.order.ship_address.andand.address2 if rsa.call(line_items) }, + proc { |line_items| line_items.first.order.ship_address.andand.city if rsa.call(line_items) }, + proc { |line_items| line_items.first.order.ship_address.andand.zipcode if rsa.call(line_items) }, + proc { |line_items| line_items.first.order.ship_address.andand.state if rsa.call(line_items) }, + + proc { |line_items| "" }, + proc { |line_items| line_items.first.variant.product.sku }, + + proc { |line_items| line_items.first.order.order_cycle.andand.name }, + proc { |line_items| line_items.first.order.payments.first.andand.payment_method.andand.name }, + proc { |line_items| line_items.first.order.user.andand.customer_of(line_items.first.order.distributor).andand.code }, + proc { |line_items| line_items.first.order.user.andand.customer_of(line_items.first.order.distributor).andand.tags.andand.join(', ') }, + + proc { |line_items| line_items.first.order.bill_address.andand.address1 }, + proc { |line_items| line_items.first.order.bill_address.andand.address2 }, + proc { |line_items| line_items.first.order.bill_address.andand.city }, + proc { |line_items| line_items.first.order.bill_address.andand.zipcode }, + proc { |line_items| line_items.first.order.bill_address.andand.state } ] + else + [ proc { |line_items| line_items.first.variant.product.supplier.name }, + proc { |line_items| line_items.first.variant.product.name }, + proc { |line_items| line_items.first.variant.full_name }, + proc { |line_items| line_items.sum { |li| li.quantity } }, + proc { |line_items| line_items.first.price }, + proc { |line_items| line_items.sum { |li| li.quantity * li.price } }, + proc { |line_items| "" }, + proc { |line_items| "incoming transport" } ] + end + end + + private + + def total_units(line_items) + return " " if line_items.map{ |li| li.variant.unit_value.nil? }.any? + total_units = line_items.sum do |li| + scale_factor = ( li.product.variant_unit == 'weight' ? 1000 : 1 ) + li.quantity * li.variant.unit_value / scale_factor + end + total_units.round(3) + end + + end +end diff --git a/lib/open_food_network/packing_report.rb b/lib/open_food_network/packing_report.rb new file mode 100644 index 0000000000..fcc5df5e88 --- /dev/null +++ b/lib/open_food_network/packing_report.rb @@ -0,0 +1,124 @@ +module OpenFoodNetwork + class PackingReport + attr_reader :params + def initialize(user, params = {}) + @params = params + @user = user + end + + def header + if is_by_customer? + ["Hub", "Code", "First Name", "Last Name", "Supplier", "Product", "Variant", "Quantity", "TempControlled?"] + else + ["Hub", "Supplier", "Code", "First Name", "Last Name", "Product", "Variant", "Quantity", "TempControlled?"] + end + end + + def search + Spree::Order.complete.not_state(:canceled).managed_by(@user).search(params[:q]) + end + + def orders + search.result + end + + def table_items + @line_items = orders.map do |o| + lis = o.line_items.managed_by(@user) + lis = lis.supplied_by_any(params[:supplier_id_in]) if params[:supplier_id_in].present? + lis + end.flatten + end + + def rules + if is_by_customer? +# customer_rows orders +# table_items = @line_items + + [ + { group_by: proc { |line_item| line_item.order.distributor }, + sort_by: proc { |distributor| distributor.name } }, + { group_by: proc { |line_item| line_item.order }, + sort_by: proc { |order| order.bill_address.lastname }, + summary_columns: [ proc { |line_items| "" }, + proc { |line_items| "" }, + proc { |line_items| "" }, + proc { |line_items| "" }, + proc { |line_items| "" }, + proc { |line_items| "TOTAL ITEMS" }, + proc { |line_items| "" }, + proc { |line_items| line_items.sum { |li| li.quantity } }, + proc { |line_items| "" } ] }, + { group_by: proc { |line_item| line_item.variant.product.supplier }, + sort_by: proc { |supplier| supplier.name } }, + { group_by: proc { |line_item| line_item.variant }, + sort_by: proc { |variant| variant.product.name } } ] + else +# supplier_rows orders +# table_items = supplier_rows orders +# + [ { group_by: proc { |line_item| line_item.order.distributor }, + sort_by: proc { |distributor| distributor.name } }, + { group_by: proc { |line_item| line_item.variant.product.supplier }, + sort_by: proc { |supplier| supplier.name }, + summary_columns: [ proc { |line_items| "" }, + proc { |line_items| "" }, + proc { |line_items| "" }, + proc { |line_items| "" }, + proc { |line_items| "" }, + proc { |line_items| "TOTAL ITEMS" }, + proc { |line_items| "" }, + proc { |line_items| line_items.sum { |li| li.quantity } }, + proc { |line_items| "" } ] }, + { group_by: proc { |line_item| line_item.variant }, + sort_by: proc { |variant| variant.product.name } } ] + end + end + + def columns + if is_by_customer? + [ proc { |line_items| line_items.first.order.distributor.name }, + proc { |line_items| customer_code(line_items.first.order.email) }, + proc { |line_items| line_items.first.order.bill_address.firstname }, + proc { |line_items| line_items.first.order.bill_address.lastname }, + proc { |line_items| line_items.first.variant.product.supplier.name }, + proc { |line_items| line_items.first.variant.product.name }, + proc { |line_items| line_items.first.variant.full_name }, + proc { |line_items| line_items.sum { |li| li.quantity } }, + proc { |line_items| is_temperature_controlled?(line_items.first) } + ] + else + [ + proc { |line_items| line_items.first.order.distributor.name }, + proc { |line_items| line_items.first.variant.product.supplier.name }, + proc { |line_items| customer_code(line_items.first.order.email) }, + proc { |line_items| line_items.first.order.bill_address.firstname }, + proc { |line_items| line_items.first.order.bill_address.lastname }, + proc { |line_items| line_items.first.variant.product.name }, + proc { |line_items| line_items.first.variant.full_name }, + proc { |line_items| line_items.sum { |li| li.quantity } }, + proc { |line_items| is_temperature_controlled?(line_items.first) } + ] + end + end + + private + + def is_temperature_controlled?(line_item) + if line_item.product.shipping_category.andand.temperature_controlled + "Yes" + else + "No" + end + end + + def is_by_customer? + params[:report_type] == "pack_by_customer" + end + + def customer_code(email) + customer = Customer.where(email: email).first + customer.nil? ? "" : customer.code + end + end +end diff --git a/lib/open_food_network/paperclippable.rb b/lib/open_food_network/paperclippable.rb new file mode 100644 index 0000000000..78b4d1a100 --- /dev/null +++ b/lib/open_food_network/paperclippable.rb @@ -0,0 +1,45 @@ +# Allow use of Paperclip's has_attached_file on non-ActiveRecord classes +# https://gist.github.com/basgys/5712426 + +module OpenFoodNetwork + module Paperclippable + def self.included(base) + base.send :extend, ActiveModel::Naming + base.send :extend, ActiveModel::Callbacks + base.send :include, ActiveModel::Validations + base.send :include, Paperclip::Glue + + # Paperclip required callbacks + base.send :define_model_callbacks, :save, only: [:after] + base.send :define_model_callbacks, :commit, only: [:after] + base.send :define_model_callbacks, :destroy, only: [:before, :after] + + # Initialise an ID + base.send :attr_accessor, :id + base.instance_variable_set :@id, 1 + end + + # ActiveModel requirements + def to_model + self + end + + def valid?() true end + def new_record?() true end + def destroyed?() true end + + def save + run_callbacks :save do + end + true + end + + def errors + obj = Object.new + def obj.[](key) [] end + def obj.full_messages() [] end + def obj.any?() false end + obj + end + end +end diff --git a/lib/open_food_network/payments_report.rb b/lib/open_food_network/payments_report.rb new file mode 100644 index 0000000000..4872da6b2f --- /dev/null +++ b/lib/open_food_network/payments_report.rb @@ -0,0 +1,101 @@ +module OpenFoodNetwork + class PaymentsReport + attr_reader :params + def initialize(user, params = {}) + @params = params + @user = user + end + + def header + case params[:report_type] + when "payments_by_payment_type" + ["Payment State", "Distributor", "Payment Type", "Total (#{currency_symbol})"] + when "itemised_payment_totals" + ["Payment State", "Distributor", "Product Total (#{currency_symbol})", "Shipping Total (#{currency_symbol})", "Outstanding Balance (#{currency_symbol})", "Total (#{currency_symbol})"] + when "payment_totals" + ["Payment State", "Distributor", "Product Total (#{currency_symbol})", "Shipping Total (#{currency_symbol})", "Total (#{currency_symbol})", "EFT (#{currency_symbol})", "PayPal (#{currency_symbol})", "Outstanding Balance (#{currency_symbol})"] + else + ["Payment State", "Distributor", "Payment Type", "Total (#{currency_symbol})"] + end + end + + def search + Spree::Order.complete.not_state(:canceled).managed_by(@user).search(params[:q]) + end + + def table_items + orders = search.result + payments = orders.map { |o| o.payments.select { |payment| payment.completed? } }.flatten # Only select completed payments + case params[:report_type] + when "payments_by_payment_type" + payments + when "itemised_payment_totals" + orders + when "payment_totals" + orders + else + payments + end + end + + def rules + case params[:report_type] + when "payments_by_payment_type" + [ { group_by: proc { |payment| payment.order.payment_state }, + sort_by: proc { |payment_state| payment_state } }, + { group_by: proc { |payment| payment.order.distributor }, + sort_by: proc { |distributor| distributor.name } }, + { group_by: proc { |payment| Spree::PaymentMethod.unscoped { payment.payment_method } }, + sort_by: proc { |method| method.name } } ] + when "itemised_payment_totals" + [ { group_by: proc { |order| order.payment_state }, + sort_by: proc { |payment_state| payment_state } }, + { group_by: proc { |order| order.distributor }, + sort_by: proc { |distributor| distributor.name } } ] + when "payment_totals" + [ { group_by: proc { |order| order.payment_state }, + sort_by: proc { |payment_state| payment_state } }, + { group_by: proc { |order| order.distributor }, + sort_by: proc { |distributor| distributor.name } } ] + else + [ { group_by: proc { |payment| payment.order.payment_state }, + sort_by: proc { |payment_state| payment_state } }, + { group_by: proc { |payment| payment.order.distributor }, + sort_by: proc { |distributor| distributor.name } }, + { group_by: proc { |payment| payment.payment_method }, + sort_by: proc { |method| method.name } } ] + end + end + + def columns + case params[:report_type] + when "payments_by_payment_type" + [ proc { |payments| payments.first.order.payment_state }, + proc { |payments| payments.first.order.distributor.name }, + proc { |payments| payments.first.payment_method.name }, + proc { |payments| payments.sum { |payment| payment.amount } } ] + when "itemised_payment_totals" + [ proc { |orders| orders.first.payment_state }, + proc { |orders| orders.first.distributor.name }, + proc { |orders| orders.sum { |o| o.item_total } }, + proc { |orders| orders.sum { |o| o.ship_total } }, + proc { |orders| orders.sum { |o| o.outstanding_balance } }, + proc { |orders| orders.sum { |o| o.total } } ] + when "payment_totals" + [ proc { |orders| orders.first.payment_state }, + proc { |orders| orders.first.distributor.name }, + proc { |orders| orders.sum { |o| o.item_total } }, + proc { |orders| orders.sum { |o| o.ship_total } }, + proc { |orders| orders.sum { |o| o.total } }, + proc { |orders| orders.sum { |o| o.payments.select { |payment| payment.completed? && (payment.payment_method.name.to_s.include? "EFT") }.sum { |payment| payment.amount } } }, + proc { |orders| orders.sum { |o| o.payments.select { |payment| payment.completed? && (payment.payment_method.name.to_s.include? "PayPal") }.sum{ |payment| payment.amount } } }, + proc { |orders| orders.sum { |o| o.outstanding_balance } } ] + else + [ proc { |payments| payments.first.order.payment_state }, + proc { |payments| payments.first.order.distributor.name }, + proc { |payments| payments.first.payment_method.name }, + proc { |payments| payments.sum { |payment| payment.amount } } ] + end + end + end +end diff --git a/lib/open_food_network/permalink_generator.rb b/lib/open_food_network/permalink_generator.rb new file mode 100644 index 0000000000..99d923fd1e --- /dev/null +++ b/lib/open_food_network/permalink_generator.rb @@ -0,0 +1,22 @@ +module PermalinkGenerator + def self.included(base) + base.extend(ClassMethods) + end + + module ClassMethods + def find_available_value(existing, requested) + return requested unless existing.include?(requested) + used_indices = existing.map do |p| + p.slice!(/^#{requested}/) + p.match(/^\d+$/).to_s.to_i + end + options = (1..used_indices.length + 1).to_a - used_indices + requested + options.first.to_s + end + end + + def create_unique_permalink(requested) + existing = self.class.where('id != ?', id).where("permalink LIKE ?", "#{requested}%").pluck(:permalink) + self.class.find_available_value(existing, requested) + end +end diff --git a/lib/open_food_network/products_and_inventory_report.rb b/lib/open_food_network/products_and_inventory_report.rb index 2b797bd944..39109cb104 100644 --- a/lib/open_food_network/products_and_inventory_report.rb +++ b/lib/open_food_network/products_and_inventory_report.rb @@ -1,106 +1,36 @@ +require 'open_food_network/products_and_inventory_report_base' + module OpenFoodNetwork - - class ProductsAndInventoryReport - attr_reader :params - def initialize(user, params = {}) - @user = user - @params = params - end - + class ProductsAndInventoryReport < ProductsAndInventoryReportBase def header [ - "Supplier", - "Producer Suburb", - "Product", - "Product Properties", - "Taxons", - "Variant Value", - "Price", - "Group Buy Unit Quantity", - "Amount" - ] + "Supplier", + "Producer Suburb", + "Product", + "Product Properties", + "Taxons", + "Variant Value", + "Price", + "Group Buy Unit Quantity", + "Amount" + ] end def table variants.map do |variant| - [variant.product.supplier.name, - variant.product.supplier.address.city, - variant.product.name, - variant.product.properties.map(&:name).join(", "), - variant.product.taxons.map(&:name).join(", "), - variant.full_name, - variant.price, - variant.product.group_buy_unit_size, - "" + [ + variant.product.supplier.name, + variant.product.supplier.address.city, + variant.product.name, + variant.product.properties.map(&:name).join(", "), + variant.product.taxons.map(&:name).join(", "), + variant.full_name, + variant.price, + variant.product.group_buy_unit_size, + "" ] end end - def permissions - return @permissions unless @permissions.nil? - @permissions = OpenFoodNetwork::Permissions.new(@user) - end - - def visible_products - return @visible_products unless @visible_products.nil? - @visible_products = permissions.visible_products - end - - def variants - filter(child_variants) - end - - def child_variants - Spree::Variant.where(:is_master => false) - .joins(:product) - .merge(visible_products) - .order("spree_products.name") - end - - def filter(variants) - # NOTE: Ordering matters. - # filter_to_order_cycle and filter_to_distributor return Arrays not Arel - filter_to_distributor filter_to_order_cycle filter_on_hand filter_to_supplier filter_not_deleted variants - end - - def filter_not_deleted(variants) - variants.not_deleted - end - - def filter_on_hand(variants) - if params[:report_type] == "inventory" - variants.where("spree_variants.count_on_hand > 0") - else - variants - end - end - - def filter_to_supplier(variants) - if params[:supplier_id].to_i > 0 - variants.where("spree_products.supplier_id = ?", params[:supplier_id]) - else - variants - end - end - - def filter_to_distributor(variants) - if params[:distributor_id].to_i > 0 - distributor = Enterprise.find params[:distributor_id] - variants.select do |v| - Enterprise.distributing_product(v.product_id).include? distributor - end - else - variants - end - end - - def filter_to_order_cycle(variants) - if params[:order_cycle_id].to_i > 0 - order_cycle = OrderCycle.find params[:order_cycle_id] - variants.select { |v| order_cycle.variants.include? v } - else - variants - end - end end end diff --git a/lib/open_food_network/products_and_inventory_report_base.rb b/lib/open_food_network/products_and_inventory_report_base.rb new file mode 100644 index 0000000000..df5b656848 --- /dev/null +++ b/lib/open_food_network/products_and_inventory_report_base.rb @@ -0,0 +1,76 @@ +module OpenFoodNetwork + class ProductsAndInventoryReportBase + attr_reader :params + + def initialize(user, params = {}) + @user = user + @params = params + end + + def permissions + @permissions ||= OpenFoodNetwork::Permissions.new(@user) + end + + def visible_products + @visible_products ||= permissions.visible_products + end + + def variants + filter(child_variants) + end + + def child_variants + Spree::Variant. + where(is_master: false). + joins(:product). + merge(visible_products). + order('spree_products.name') + end + + def filter(variants) + # NOTE: Ordering matters. + # filter_to_order_cycle and filter_to_distributor return arrays not relations + filter_to_distributor filter_to_order_cycle filter_on_hand filter_to_supplier filter_not_deleted variants + end + + def filter_not_deleted(variants) + variants.not_deleted + end + + def filter_on_hand(variants) + if params[:report_type] == 'inventory' + variants.where('spree_variants.count_on_hand > 0') + else + variants + end + end + + def filter_to_supplier(variants) + if params[:supplier_id].to_i > 0 + variants.where("spree_products.supplier_id = ?", params[:supplier_id]) + else + variants + end + end + + def filter_to_distributor(variants) + if params[:distributor_id].to_i > 0 + distributor = Enterprise.find params[:distributor_id] + variants.select do |v| + Enterprise.distributing_product(v.product_id).include? distributor + end + else + variants + end + end + + def filter_to_order_cycle(variants) + if params[:order_cycle_id].to_i > 0 + order_cycle = OrderCycle.find params[:order_cycle_id] + variants.select { |v| order_cycle.variants.include? v } + else + variants + end + end + end +end diff --git a/lib/open_food_network/rack_request_blocker.rb b/lib/open_food_network/rack_request_blocker.rb new file mode 100644 index 0000000000..71f1c450e2 --- /dev/null +++ b/lib/open_food_network/rack_request_blocker.rb @@ -0,0 +1,77 @@ +# Copied from http://blog.salsify.com/engineering/tearing-capybara-ajax-tests +# https://gist.github.com/jturkel/9317269/raw/ff7838684370fd8a468ffe1e5ce1f3e46ba39951/rack_request_blocker.rb + +require 'atomic' + +# Rack middleware that keeps track of the number of active requests and can block new requests. +class RackRequestBlocker + + @@num_active_requests = Atomic.new(0) + @@block_requests = Atomic.new(false) + + # Returns the number of requests the server is currently processing. + def self.num_active_requests + @@num_active_requests.value + end + + # Prevents the server from accepting new requests. Any new requests will return an HTTP + # 503 status. + def self.block_requests! + @@block_requests.value = true + end + + # Allows the server to accept requests again. + def self.allow_requests! + @@block_requests.value = false + end + + def initialize(app) + @app = app + end + + def call(env) + increment_active_requests + if block_requests? + block_request(env) + else + @app.call(env) + end + ensure + decrement_active_requests + end + + def self.wait_for_requests_complete + self.block_requests! + max_wait_time = 30 + polling_interval = 0.01 + wait_until = Time.now + max_wait_time.seconds + while true + return if self.num_active_requests == 0 + if Time.now > wait_until + raise "Failed waiting for completing requests, #{self.num_active_requests} running." + else + sleep(polling_interval) + end + end + ensure + self.allow_requests! + end + + private + + def block_requests? + @@block_requests.value + end + + def block_request(env) + [503, {}, []] + end + + def increment_active_requests + @@num_active_requests.update { |v| v + 1 } + end + + def decrement_active_requests + @@num_active_requests.update { |v| v - 1 } + end +end diff --git a/lib/open_food_network/reports/bulk_coop_allocation_report.rb b/lib/open_food_network/reports/bulk_coop_allocation_report.rb new file mode 100644 index 0000000000..2bd19ea66a --- /dev/null +++ b/lib/open_food_network/reports/bulk_coop_allocation_report.rb @@ -0,0 +1,50 @@ +require 'open_food_network/reports/bulk_coop_report' + +module OpenFoodNetwork::Reports + class BulkCoopAllocationReport < BulkCoopReport + header "Customer", "Product", "Bulk Unit Size", "Variant", "Variant value", "Variant unit", "Weight", "Sum Total", "Total Available", "Unallocated", "Max quantity excess" + + organise do + group { |li| li.variant.product } + sort &:name + + summary_row do + column { |lis| "TOTAL" } + column { |lis| product_name(lis) } + column { |lis| group_buy_unit_size_f(lis) } + column { |lis| "" } + column { |lis| "" } + column { |lis| "" } + column { |lis| "" } + column { |lis| total_amount(lis) } + column { |lis| total_available(lis) } + column { |lis| remainder(lis) } + column { |lis| max_quantity_excess(lis) } + end + + organise do + group { |li| li.variant } + sort &:full_name + + organise do + group { |li| li.order } + sort { |order| order.to_s } + end + end + end + + columns do + column { |lis| lis.first.order.bill_address.firstname + " " + lis.first.order.bill_address.lastname } + column { |lis| lis.first.variant.product.name } + column { |lis| lis.first.variant.product.group_buy_unit_size || 0.0 } + column { |lis| lis.first.variant.full_name } + column { |lis| OpenFoodNetwork::OptionValueNamer.new(lis.first.variant).value } + column { |lis| OpenFoodNetwork::OptionValueNamer.new(lis.first.variant).unit } + column { |lis| lis.first.variant.weight || 0 } + column { |lis| total_amount(lis) } + column { |lis| "" } + column { |lis| "" } + column { |lis| "" } + end + end +end diff --git a/lib/open_food_network/reports/bulk_coop_report.rb b/lib/open_food_network/reports/bulk_coop_report.rb new file mode 100644 index 0000000000..dbce047963 --- /dev/null +++ b/lib/open_food_network/reports/bulk_coop_report.rb @@ -0,0 +1,66 @@ +require 'open_food_network/reports/report' + +module OpenFoodNetwork::Reports + class BulkCoopReport < Report + + + private + + class << self + + def supplier_name(lis) + lis.first.variant.product.supplier.name + end + + def product_name(lis) + lis.first.variant.product.name + end + + def group_buy_unit_size(lis) + (lis.first.variant.product.group_buy_unit_size || 0.0) / + (lis.first.product.variant_unit_scale || 1) + end + + def group_buy_unit_size_f(lis) + group_buy_unit_size(lis) + end + + def total_amount(lis) + lis.sum { |li| (li.quantity || 0) * scaled_amount(li) } + end + + def units_required(lis) + if group_buy_unit_size(lis).zero? + 0 + else + ( total_amount(lis) / group_buy_unit_size(lis) ).ceil + end + end + + def total_available(lis) + units_required(lis) * group_buy_unit_size(lis) + end + + def remainder(lis) + remainder = total_available(lis) - total_amount(lis) + remainder >= 0 ? remainder : '' + end + + def max_quantity_excess(lis) + max_quantity_amount(lis) - total_amount(lis) + end + + def max_quantity_amount(lis) + lis.sum do |li| + max_quantity = [li.max_quantity || 0, li.quantity || 0].max + max_quantity * scaled_amount(li) + end + end + + def scaled_amount(li) + (li.variant.unit_value || 0) / (li.product.variant_unit_scale || 1) + end + + end + end +end diff --git a/lib/open_food_network/reports/bulk_coop_supplier_report.rb b/lib/open_food_network/reports/bulk_coop_supplier_report.rb new file mode 100644 index 0000000000..df3b40c519 --- /dev/null +++ b/lib/open_food_network/reports/bulk_coop_supplier_report.rb @@ -0,0 +1,50 @@ +require 'open_food_network/reports/bulk_coop_report' + +module OpenFoodNetwork::Reports + class BulkCoopSupplierReport < BulkCoopReport + header "Supplier", "Product", "Bulk Unit Size", "Variant", "Variant value", "Variant unit", "Weight", "Sum Total", "Units Required", "Unallocated", "Max quantity excess" + + organise do + group { |li| li.variant.product.supplier } + sort &:name + + organise do + group { |li| li.variant.product } + sort &:name + + summary_row do + column { |lis| supplier_name(lis) } + column { |lis| product_name(lis) } + column { |lis| group_buy_unit_size_f(lis) } + column { |lis| "" } + column { |lis| "" } + column { |lis| "" } + column { |lis| "" } + column { |lis| total_amount(lis) } + column { |lis| units_required(lis) } + column { |lis| remainder(lis) } + column { |lis| max_quantity_excess(lis) } + end + + organise do + group { |li| li.variant } + sort &:full_name + end + end + end + + columns do + column { |lis| supplier_name(lis) } + column { |lis| product_name(lis) } + column { |lis| group_buy_unit_size_f(lis) } + column { |lis| lis.first.variant.full_name } + column { |lis| OpenFoodNetwork::OptionValueNamer.new(lis.first.variant).value } + column { |lis| OpenFoodNetwork::OptionValueNamer.new(lis.first.variant).unit } + column { |lis| lis.first.variant.weight || 0 } + column { |lis| total_amount(lis) } + column { |lis| '' } + column { |lis| '' } + column { |lis| '' } + end + end +end diff --git a/lib/open_food_network/reports/report.rb b/lib/open_food_network/reports/report.rb new file mode 100644 index 0000000000..d575fcd014 --- /dev/null +++ b/lib/open_food_network/reports/report.rb @@ -0,0 +1,46 @@ +require 'open_food_network/reports/row' +require 'open_food_network/reports/rule' + +module OpenFoodNetwork::Reports + class Report + class_attribute :_header, :_columns, :_rules_head + + # -- API + def header + self._header + end + + def columns + self._columns.to_a + end + + def rules + # Flatten linked list and return as hashes + rules = [] + + rule = self._rules_head + while rule + rules << rule + rule = rule.next + end + + rules.map &:to_h + end + + + # -- DSL + def self.header(*columns) + self._header = columns + end + + def self.columns(&block) + self._columns = Row.new + Blockenspiel.invoke block, self._columns + end + + def self.organise(&block) + self._rules_head = Rule.new + Blockenspiel.invoke block, self._rules_head + end + end +end diff --git a/lib/open_food_network/reports/row.rb b/lib/open_food_network/reports/row.rb new file mode 100644 index 0000000000..01c8dd192e --- /dev/null +++ b/lib/open_food_network/reports/row.rb @@ -0,0 +1,17 @@ +module OpenFoodNetwork::Reports + class Row + include Blockenspiel::DSL + + def initialize + @columns = [] + end + + def column(&block) + @columns << block + end + + def to_a + @columns + end + end +end diff --git a/lib/open_food_network/reports/rule.rb b/lib/open_food_network/reports/rule.rb new file mode 100644 index 0000000000..e2f94f32a2 --- /dev/null +++ b/lib/open_food_network/reports/rule.rb @@ -0,0 +1,32 @@ +require 'open_food_network/reports/row' + +module OpenFoodNetwork::Reports + class Rule + include Blockenspiel::DSL + attr_reader :next + + def group(&block) + @group = block + end + + def sort(&block) + @sort = block + end + + def summary_row(&block) + @summary_row = Row.new + Blockenspiel.invoke block, @summary_row + end + + def organise(&block) + @next = Rule.new + Blockenspiel.invoke block, @next + end + + def to_h + h = {group_by: @group, sort_by: @sort} + h.merge!({summary_columns: @summary_row.to_a}) if @summary_row + h + end + end +end diff --git a/lib/open_food_network/scope_product_to_hub.rb b/lib/open_food_network/scope_product_to_hub.rb index b214abc0d8..d3e018f295 100644 --- a/lib/open_food_network/scope_product_to_hub.rb +++ b/lib/open_food_network/scope_product_to_hub.rb @@ -1,16 +1,23 @@ require 'open_food_network/scope_variant_to_hub' module OpenFoodNetwork - module ScopeProductToHub - def variants_distributed_by(order_cycle, distributor) - super.each { |v| v.scope_to_hub @hub } - end - end - - module ProductScopableToHub - def scope_to_hub(hub) - extend OpenFoodNetwork::ScopeProductToHub + class ScopeProductToHub + def initialize(hub) @hub = hub + @variant_overrides = VariantOverride.indexed @hub + end + + def scope(product) + product.send :extend, OpenFoodNetwork::ScopeProductToHub::ScopeProductToHub + product.instance_variable_set :@hub, @hub + product.instance_variable_set :@variant_overrides, @variant_overrides + end + + + module ScopeProductToHub + def variants_distributed_by(order_cycle, distributor) + super.each { |v| ScopeVariantToHub.new(@hub, @variant_overrides).scope(v) } + end end end end diff --git a/lib/open_food_network/scope_variant_to_hub.rb b/lib/open_food_network/scope_variant_to_hub.rb index 91ed634043..80396ffa17 100644 --- a/lib/open_food_network/scope_variant_to_hub.rb +++ b/lib/open_food_network/scope_variant_to_hub.rb @@ -1,30 +1,48 @@ module OpenFoodNetwork - module ScopeVariantToHub - def price - VariantOverride.price_for(@hub, self) || super + class ScopeVariantToHub + def initialize(hub, variant_overrides=nil) + @hub = hub + @variant_overrides = variant_overrides || VariantOverride.indexed(@hub) end - def price_in(currency) - Spree::Price.new(amount: price, currency: currency) + def scope(variant) + variant.send :extend, OpenFoodNetwork::ScopeVariantToHub::ScopeVariantToHub + variant.instance_variable_set :@hub, @hub + variant.instance_variable_set :@variant_override, @variant_overrides[variant] end - def count_on_hand - VariantOverride.count_on_hand_for(@hub, self) || super - end - def decrement!(attribute, by=1) - if attribute == :count_on_hand && VariantOverride.stock_overridden?(@hub, self) - VariantOverride.decrement_stock! @hub, self, by - else - super + module ScopeVariantToHub + def price + @variant_override.andand.price || super + end + + def price_in(currency) + Spree::Price.new(amount: price, currency: currency) + end + + def count_on_hand + @variant_override.andand.count_on_hand || super + end + + def on_demand + if @variant_override.andand.count_on_hand.present? + # If we're overriding the stock level of an on_demand variant, show it as not + # on_demand, so our stock control can take effect. + false + else + super + end + end + + def decrement!(attribute, by=1) + if attribute == :count_on_hand && @variant_override.andand.stock_overridden? + @variant_override.decrement_stock! by + else + super + end end end - end - module VariantScopableToHub - def scope_to_hub(hub) - extend OpenFoodNetwork::ScopeVariantToHub - @hub = hub - end end end diff --git a/lib/open_food_network/xero_invoices_report.rb b/lib/open_food_network/xero_invoices_report.rb index 1a7f7bd636..cf546efc90 100644 --- a/lib/open_food_network/xero_invoices_report.rb +++ b/lib/open_food_network/xero_invoices_report.rb @@ -5,7 +5,8 @@ module OpenFoodNetwork @opts = opts. reject { |k, v| v.blank? }. - reverse_merge({invoice_date: Date.today, + reverse_merge({report_type: 'summary', + invoice_date: Date.today, due_date: 2.weeks.from_now.to_date, account_code: 'food sales'}) end @@ -19,49 +20,110 @@ module OpenFoodNetwork @orders.each_with_index do |order, i| invoice_number = invoice_number_for(order, i) - rows += rows_for_order(order, invoice_number, @opts) + rows += detail_rows_for_order(order, invoice_number, @opts) if detail? + rows += summary_rows_for_order(order, invoice_number, @opts) end - rows + rows.compact end private - def invoice_number_for(order, i) - @opts[:initial_invoice_number] ? @opts[:initial_invoice_number].to_i+i : order.number + def detail_rows_for_order(order, invoice_number, opts) + rows = [] + + rows += line_item_detail_rows(order, invoice_number, opts) + + if order.account_invoice? + rows += adjustment_detail_rows(order, invoice_number, opts) + end + + rows end - def rows_for_order(order, invoice_number, opts) - [ - summary_row(order, 'Total untaxable produce (no tax)', total_untaxable_products(order), invoice_number, 'GST Free Income', opts), - summary_row(order, 'Total taxable produce (tax inclusive)', total_taxable_products(order), invoice_number, 'GST on Income', opts), - summary_row(order, 'Total untaxable fees (no tax)', total_untaxable_fees(order), invoice_number, 'GST Free Income', opts), - summary_row(order, 'Total taxable fees (tax inclusive)', total_taxable_fees(order), invoice_number, 'GST on Income', opts), - summary_row(order, 'Delivery Shipping Cost (tax inclusive)', total_shipping(order), invoice_number, tax_on_shipping_s(order), opts) - ].compact + def line_item_detail_rows(order, invoice_number, opts) + order.line_items.map do |line_item| + line_item_detail_row(line_item, invoice_number, opts) + end + end + + def line_item_detail_row(line_item, invoice_number, opts) + row(line_item.order, + line_item.product.sku, + line_item.variant.product_and_variant_name, + line_item.quantity.to_s, + line_item.price.to_s, + invoice_number, + tax_type(line_item), + opts) + end + + def adjustment_detail_rows(order, invoice_number, opts) + account_invoice_adjustments(order).map do |adjustment| + adjustment_detail_row(adjustment, invoice_number, opts) + end + end + + def adjustment_detail_row(adjustment, invoice_number, opts) + row(adjustment.source.andand.account_invoice.andand.order, + '', + adjustment.label, + 1, + adjustment.amount, + invoice_number, + tax_type(adjustment), + opts) + end + + def summary_rows_for_order(order, invoice_number, opts) + rows = [] + + rows += produce_summary_rows(order, invoice_number, opts) unless detail? + rows += fee_summary_rows(order, invoice_number, opts) unless detail? && order.account_invoice? + rows += shipping_summary_rows(order, invoice_number, opts) + + rows + end + + def produce_summary_rows(order, invoice_number, opts) + [summary_row(order, 'Total untaxable produce (no tax)', total_untaxable_products(order), invoice_number, 'GST Free Income', opts), + summary_row(order, 'Total taxable produce (tax inclusive)', total_taxable_products(order), invoice_number, 'GST on Income', opts)] + end + + def fee_summary_rows(order, invoice_number, opts) + [summary_row(order, 'Total untaxable fees (no tax)', total_untaxable_fees(order), invoice_number, 'GST Free Income', opts), + summary_row(order, 'Total taxable fees (tax inclusive)', total_taxable_fees(order), invoice_number, 'GST on Income', opts)] + end + + def shipping_summary_rows(order, invoice_number, opts) + [summary_row(order, 'Delivery Shipping Cost (tax inclusive)', total_shipping(order), invoice_number, tax_on_shipping_s(order), opts)] end def summary_row(order, description, amount, invoice_number, tax_type, opts={}) + row order, '', description, '1', amount, invoice_number, tax_type, opts + end + + def row(order, sku, description, quantity, amount, invoice_number, tax_type, opts={}) return nil if amount == 0 - [order.bill_address.full_name, + [order.bill_address.andand.full_name, order.email, - order.bill_address.address1, - order.bill_address.address2, + order.bill_address.andand.address1, + order.bill_address.andand.address2, '', '', - order.bill_address.city, - order.bill_address.state, - order.bill_address.zipcode, - order.bill_address.country.andand.name, + order.bill_address.andand.city, + order.bill_address.andand.state, + order.bill_address.andand.zipcode, + order.bill_address.andand.country.andand.name, invoice_number, order.number, opts[:invoice_date], opts[:due_date], - '', + sku, description, - '1', + quantity, amount, '', opts[:account_code], @@ -76,6 +138,16 @@ module OpenFoodNetwork ] end + def account_invoice_adjustments(order) + order.adjustments. + billable_period. + select { |a| a.source.present? } + end + + def invoice_number_for(order, i) + @opts[:initial_invoice_number] ? @opts[:initial_invoice_number].to_i+i : order.number + end + def total_untaxable_products(order) order.line_items.without_tax.sum &:amount end @@ -100,5 +172,13 @@ module OpenFoodNetwork tax_on_shipping = order.adjustments.shipping.sum(&:included_tax) > 0 tax_on_shipping ? 'GST on Income' : 'GST Free Income' end + + def detail? + @opts[:report_type] == 'detailed' + end + + def tax_type(taxable) + taxable.has_tax? ? 'GST on Income' : 'GST Free Income' + end end end diff --git a/lib/spree/core/controller_helpers/order_decorator.rb b/lib/spree/core/controller_helpers/order_decorator.rb index 88ae412ec2..dc38e861e6 100644 --- a/lib/spree/core/controller_helpers/order_decorator.rb +++ b/lib/spree/core/controller_helpers/order_decorator.rb @@ -3,8 +3,9 @@ Spree::Core::ControllerHelpers::Order.class_eval do order = current_order_without_scoped_variants(create_order_if_necessary) if order + scoper = OpenFoodNetwork::ScopeVariantToHub.new(order.distributor) order.line_items.each do |li| - li.variant.scope_to_hub order.distributor + scoper.scope(li.variant) end end diff --git a/lib/tasks/billing.rake b/lib/tasks/billing.rake new file mode 100644 index 0000000000..6bfd0a53e7 --- /dev/null +++ b/lib/tasks/billing.rake @@ -0,0 +1,13 @@ +namespace :openfoodnetwork do + namespace :billing do + desc 'Update enterprise user invoices' + task update_account_invoices: :environment do + Delayed::Job.enqueue(UpdateAccountInvoices.new) if Spree::Config[:auto_update_invoices] + end + + desc 'Finalize enterprise user invoices' + task finalize_account_invoices: :environment do + Delayed::Job.enqueue(FinalizeAccountInvoices.new) if Spree::Config[:auto_finalize_invoices] + end + end +end diff --git a/public/AveniBla.eot b/public/AveniBla.eot deleted file mode 100644 index 1ae59a3381..0000000000 Binary files a/public/AveniBla.eot and /dev/null differ diff --git a/public/AveniMed.eot b/public/AveniMed.eot deleted file mode 100644 index 61c35da54b..0000000000 Binary files a/public/AveniMed.eot and /dev/null differ diff --git a/public/AvenirLTStd-Black.otf b/public/AvenirLTStd-Black.otf deleted file mode 100644 index 1a934a2af3..0000000000 Binary files a/public/AvenirLTStd-Black.otf and /dev/null differ diff --git a/public/AvenirLTStd-Medium.otf b/public/AvenirLTStd-Medium.otf deleted file mode 100644 index e902718152..0000000000 Binary files a/public/AvenirLTStd-Medium.otf and /dev/null differ diff --git a/public/favicon-staging.ico b/public/favicon-staging.ico index c54a7c4528..91eac8cce8 100644 Binary files a/public/favicon-staging.ico and b/public/favicon-staging.ico differ diff --git a/public/favicon.ico b/public/favicon.ico index 9e0f9a2681..c62c6eeb38 100644 Binary files a/public/favicon.ico and b/public/favicon.ico differ diff --git a/script/archive_branch.sh b/script/archive_branch.sh new file mode 100755 index 0000000000..0177592a54 --- /dev/null +++ b/script/archive_branch.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +# Archive an old branch as a tagged named archive/branch-name to declutter the branch list + +BRANCH_NAME=$1 + +git tag archive/$BRANCH_NAME $BRANCH_NAME +git checkout -q archive/$BRANCH_NAME +git branch -d $BRANCH_NAME +git checkout -q master diff --git a/script/ci/includes.sh b/script/ci/includes.sh index d7619f7d23..619d03153e 100644 --- a/script/ci/includes.sh +++ b/script/ci/includes.sh @@ -5,20 +5,60 @@ function load_environment { fi } +function master_merged { + if [[ `git tag -l "$BUILDKITE_BRANCH"` != '' ]]; then + echo "'$BUILDKITE_BRANCH' is a tag." + if [[ `git tag -l --contains origin/master "$BUILDKITE_BRANCH"` != '' ]]; then + echo "This tag contains the current master." + return 0 + else + echo "This tag does not contain the current master." + return 1 + fi + fi + if [[ `git branch -r --merged origin/$BUILDKITE_BRANCH` == *origin/master* ]]; then + echo "This branch already has the current master merged." + return 0 + fi + return 1 +} + function exit_unless_master_merged { - if [[ `git branch -a --merged origin/$BUILDKITE_BRANCH` != *origin/master* ]]; then + if ! master_merged; then echo "This branch does not have the current master merged. Please merge master and push again." exit 1 fi } function succeed_if_master_merged { - if [[ `git branch -a --merged origin/$BUILDKITE_BRANCH` == *origin/master* ]]; then - echo "This branch already has the current master merged." - exit 0 + master_merged && exit 0 +} + +function set_ofn_commit { + echo "Setting commit to $1" + buildkite-agent meta-data set "openfoodnetwork:git:commit" $1 +} + +function get_ofn_commit { + OFN_COMMIT=`buildkite-agent meta-data get "openfoodnetwork:git:commit"` + + # If we don't catch this failure case, push will execute: + # git push remote :master --force + # Which will delete the master branch on the server + + if [[ `expr length "$OFN_COMMIT"` == 0 ]]; then + echo 'OFN_COMMIT_NOT_FOUND' + else + echo $OFN_COMMIT fi } +function checkout_ofn_commit { + OFN_COMMIT=`buildkite-agent meta-data get "openfoodnetwork:git:commit"` + echo "Checking out stored commit $OFN_COMMIT" + git checkout -qf "$OFN_COMMIT" +} + function drop_and_recreate_database { # Adapted from: http://stackoverflow.com/questions/12924466/capistrano-with-postgresql-error-database-is-being-accessed-by-other-users psql -U openfoodweb postgres <&1) =~ "Done" ]] +exec 5>&1 +OUTPUT=$(git push production `get_ofn_commit`:master --force 2>&1 |tee /dev/fd/5) +[[ $OUTPUT =~ "Done" ]] diff --git a/script/ci/push_to_staging.sh b/script/ci/push_to_staging.sh index c634ca53a4..7ace392276 100755 --- a/script/ci/push_to_staging.sh +++ b/script/ci/push_to_staging.sh @@ -1,6 +1,6 @@ #!/bin/bash -set -ex +set -e source ./script/ci/includes.sh # Add staging git remote if required @@ -16,4 +16,6 @@ echo "--- Loading baseline data" ssh ofn-staging2 "/home/openfoodweb/apps/openfoodweb/current/script/ci/load_staging_baseline.sh" echo "--- Pushing to staging" -[[ $(git push staging2 $BUILDKITE_COMMIT:master --force 2>&1) =~ "Done" ]] +exec 5>&1 +OUTPUT=$(git push staging2 `get_ofn_commit`:master --force 2>&1 |tee /dev/fd/5) +[[ $OUTPUT =~ "Done" ]] diff --git a/script/ci/run_js_tests.sh b/script/ci/run_js_tests.sh index 91a4be09d9..c4a03403b9 100755 --- a/script/ci/run_js_tests.sh +++ b/script/ci/run_js_tests.sh @@ -5,6 +5,7 @@ set -e echo "--- Loading environment" source ./script/ci/includes.sh load_environment +checkout_ofn_commit echo "--- Verifying branch is based on current master" exit_unless_master_merged diff --git a/script/ci/run_tests.sh b/script/ci/run_tests.sh index 9f9cdfa30d..efae0805a6 100755 --- a/script/ci/run_tests.sh +++ b/script/ci/run_tests.sh @@ -5,6 +5,7 @@ set -e echo "--- Loading environment" source ./script/ci/includes.sh load_environment +checkout_ofn_commit echo "--- Verifying branch is based on current master" exit_unless_master_merged diff --git a/spec/controllers/admin/accounts_and_billing_settings_controller_spec.rb b/spec/controllers/admin/accounts_and_billing_settings_controller_spec.rb new file mode 100644 index 0000000000..7ec9eb1ca0 --- /dev/null +++ b/spec/controllers/admin/accounts_and_billing_settings_controller_spec.rb @@ -0,0 +1,241 @@ +require 'spec_helper' + +describe Admin::AccountsAndBillingSettingsController, type: :controller do + let!(:pm1) { create(:payment_method) } + let!(:sm1) { create(:shipping_method) } + let!(:pm2) { create(:payment_method) } + let!(:sm2) { create(:shipping_method) } + let!(:accounts_distributor) { create(:distributor_enterprise, payment_methods: [pm1], shipping_methods: [sm1]) } + let!(:new_distributor) { create(:distributor_enterprise, payment_methods: [pm2], shipping_methods: [sm2]) } + let(:user) { create(:user) } + let(:admin) { create(:admin_user) } + + before do + Spree::Config.set({ + accounts_distributor_id: accounts_distributor.id, + default_accounts_payment_method_id: pm1.id, + default_accounts_shipping_method_id: sm1.id, + auto_update_invoices: true, + auto_finalize_invoices: false + }) + end + + describe "edit" do + context "as an enterprise user" do + before { allow(controller).to receive(:spree_current_user) { user } } + + it "does not allow access" do + spree_get :edit + expect(response).to redirect_to spree.unauthorized_path + end + end + + context "as super admin" do + before { allow(controller).to receive(:spree_current_user) { admin } } + + it "loads relevant global settings into a locally dummy class" do + spree_get :edit + settings = assigns(:settings) + + expect(settings.accounts_distributor_id).to eq accounts_distributor.id + expect(settings.default_accounts_payment_method_id).to eq pm1.id + expect(settings.default_accounts_shipping_method_id).to eq sm1.id + expect(settings.auto_update_invoices).to eq true + expect(settings.auto_finalize_invoices).to eq false + end + end + end + + describe "update" do + context "as an enterprise user" do + before { allow(controller).to receive(:spree_current_user) { user } } + + it "does not allow access" do + spree_get :update + expect(response).to redirect_to spree.unauthorized_path + end + end + + context "as super admin" do + before {allow(controller).to receive(:spree_current_user) { admin } } + let(:params) { { settings: { } } } + + context "when required settings have no values" do + before do + params[:settings][:accounts_distributor_id] = '' + params[:settings][:default_accounts_payment_method_id] = '0' + params[:settings][:default_accounts_shipping_method_id] = '0' + params[:settings][:auto_update_invoices] = '0' + params[:settings][:auto_finalize_invoices] = '0' + spree_get :update, params + end + + it "does not allow them to be empty/false" do + expect(response).to render_template :edit + expect(assigns(:settings).errors.count).to be 3 + expect(Spree::Config.accounts_distributor_id).to eq accounts_distributor.id + expect(Spree::Config.default_accounts_payment_method_id).to eq pm1.id + expect(Spree::Config.default_accounts_shipping_method_id).to eq sm1.id + expect(Spree::Config.auto_update_invoices).to be true + expect(Spree::Config.auto_finalize_invoices).to be false + end + end + + context "when required settings have values" do + before do + params[:settings][:accounts_distributor_id] = new_distributor.id + params[:settings][:default_accounts_payment_method_id] = pm2.id + params[:settings][:default_accounts_shipping_method_id] = sm2.id + params[:settings][:auto_update_invoices] = '0' + params[:settings][:auto_finalize_invoices] = '0' + end + + it "sets global config to the specified values" do + spree_get :update, params + expect(Spree::Config.accounts_distributor_id).to eq new_distributor.id + expect(Spree::Config.default_accounts_payment_method_id).to eq pm2.id + expect(Spree::Config.default_accounts_shipping_method_id).to eq sm2.id + expect(Spree::Config.auto_update_invoices).to be false + expect(Spree::Config.auto_finalize_invoices).to be false + end + end + end + end + + describe "start_job" do + context "as an enterprise user" do + before do + allow(controller).to receive(:spree_current_user) { user } + spree_post :start_job, enterprise_id: accounts_distributor.id + end + + it "does not allow access" do + expect(response).to redirect_to spree.unauthorized_path + end + end + + context "as super admin" do + before do + allow(controller).to receive(:spree_current_user) { admin } + end + + context "when settings are not valid" do + before do + Spree::Config.set({ accounts_distributor_id: "" }) + Spree::Config.set({ default_accounts_payment_method_id: "" }) + Spree::Config.set({ default_accounts_shipping_method_id: "" }) + spree_post :start_job, job: { name: "" } + end + + it "returns immediately and renders :edit" do + expect(assigns(:settings).errors.count).to eq 3 + expect(response).to render_template :edit + end + end + + context "when settings are valid" do + before do + Spree::Config.set({ accounts_distributor_id: accounts_distributor.id }) + Spree::Config.set({ default_accounts_payment_method_id: pm1.id }) + Spree::Config.set({ default_accounts_shipping_method_id: sm1.id }) + end + + context "and job_name is not on the known_jobs list" do + before do + spree_post :start_job, job: { name: "" } + end + + it "returns immediately with an error" do + expect(flash[:error]).to eq "Unknown Task: " + expect(response).to redirect_to edit_admin_accounts_and_billing_settings_path + end + end + + context "and job_name is update_account_invoices" do + let!(:params) { { job: { name: "update_account_invoices" } } } + + context "and no jobs are currently running" do + before do + allow(controller).to receive(:load_jobs) + end + + it "runs the job" do + expect{spree_post :start_job, params}.to enqueue_job UpdateAccountInvoices + expect(flash[:success]).to eq "Task Queued" + expect(response).to redirect_to edit_admin_accounts_and_billing_settings_path + end + end + + context "and there are jobs currently running" do + before do + allow(controller).to receive(:load_jobs) + controller.instance_variable_set("@update_account_invoices_job", double(:update_account_invoices_job)) + end + + it "does not run the job" do + expect{spree_post :start_job, params}.to_not enqueue_job UpdateAccountInvoices + expect(flash[:error]).to eq "A task is already running, please wait until it has finished" + expect(response).to redirect_to edit_admin_accounts_and_billing_settings_path + end + end + end + + context "and job_name is finalize_account_invoices" do + let!(:params) { { job: { name: "finalize_account_invoices" } } } + + context "and no jobs are currently running" do + before do + allow(controller).to receive(:load_jobs) + end + + it "runs the job" do + expect{spree_post :start_job, params}.to enqueue_job FinalizeAccountInvoices + expect(flash[:success]).to eq "Task Queued" + expect(response).to redirect_to edit_admin_accounts_and_billing_settings_path + end + end + + context "and there are jobs currently running" do + before do + allow(controller).to receive(:load_jobs) + controller.instance_variable_set("@finalize_account_invoices_job", double(:finalize_account_invoices_job)) + end + + it "does not run the job" do + expect{spree_post :start_job, params}.to_not enqueue_job FinalizeAccountInvoices + expect(flash[:error]).to eq "A task is already running, please wait until it has finished" + expect(response).to redirect_to edit_admin_accounts_and_billing_settings_path + end + end + end + end + end + end + + describe "show_methods" do + context "as an enterprise user" do + before do + allow(controller).to receive(:spree_current_user) { user } + spree_get :show_methods, enterprise_id: accounts_distributor.id + end + + it "does not allow access" do + expect(response).to redirect_to spree.unauthorized_path + end + end + + context "as super admin" do + before do + allow(controller).to receive(:spree_current_user) { admin } + spree_get :show_methods, enterprise_id: accounts_distributor.id + end + + it "renders the method_settings template" do + expect(assigns(:payment_methods)).to eq [pm1] + expect(assigns(:shipping_methods)).to eq [sm1] + expect(assigns(:enterprise)).to eq accounts_distributor + expect(response).to render_template :method_settings + end + end + end +end diff --git a/spec/controllers/admin/enterprises_controller_spec.rb b/spec/controllers/admin/enterprises_controller_spec.rb index 79f72cfac3..ef31b779c9 100644 --- a/spec/controllers/admin/enterprises_controller_spec.rb +++ b/spec/controllers/admin/enterprises_controller_spec.rb @@ -184,6 +184,15 @@ module Admin end context "as owner" do + it "allows 'sells' to be changed" do + controller.stub spree_current_user: profile_enterprise.owner + enterprise_params = { id: profile_enterprise, enterprise: { sells: 'any' } } + + spree_put :update, enterprise_params + profile_enterprise.reload + expect(profile_enterprise.sells).to eq 'any' + end + it "allows owner to be changed" do controller.stub spree_current_user: distributor_owner update_params = { id: distributor, enterprise: { owner_id: distributor_manager } } @@ -234,39 +243,51 @@ module Admin end end - describe "set_sells" do + describe "register" do let(:enterprise) { create(:enterprise, sells: 'none') } - before do - controller.stub spree_current_user: distributor_manager - end - context "as a normal user" do - it "does not allow 'sells' to be set" do - spree_post :set_sells, { id: enterprise.id, sells: 'none' } + before do + controller.stub spree_current_user: distributor_manager + end + + it "does not allow access" do + spree_post :register, { id: enterprise.id, sells: 'none' } expect(response).to redirect_to spree.unauthorized_path end end context "as a manager" do before do + controller.stub spree_current_user: distributor_manager enterprise.enterprise_roles.build(user: distributor_manager).save end - context "allows setting 'sells' to 'none'" do + it "does not allow access" do + spree_post :register, { id: enterprise.id, sells: 'none' } + expect(response).to redirect_to spree.unauthorized_path + end + end + + context "as an owner" do + before do + controller.stub spree_current_user: enterprise.owner + end + + context "setting 'sells' to 'none'" do it "is allowed" do - spree_post :set_sells, { id: enterprise, sells: 'none' } + spree_post :register, { id: enterprise, sells: 'none' } expect(response).to redirect_to spree.admin_path expect(flash[:success]).to eq "Congratulations! Registration for #{enterprise.name} is complete!" expect(enterprise.reload.sells).to eq 'none' end + end - context "setting producer_profile_only to true" do - it "is allowed" do - spree_post :set_sells, { id: enterprise, sells: 'none', producer_profile_only: true } - expect(response).to redirect_to spree.admin_path - expect(enterprise.reload.producer_profile_only).to eq true - end + context "setting producer_profile_only" do + it "is ignored" do + spree_post :register, { id: enterprise, sells: 'none', producer_profile_only: true } + expect(response).to redirect_to spree.admin_path + expect(enterprise.reload.producer_profile_only).to be false end end @@ -277,68 +298,97 @@ module Admin end context "if the trial has finished" do - it "is disallowed" do + let(:trial_start) { 30.days.ago.beginning_of_day } + + before do + enterprise.update_attribute(:shop_trial_start_date, trial_start) + end + + it "is allowed" do Timecop.freeze(Time.zone.local(2015, 4, 16, 14, 0, 0)) do - enterprise.update_attribute(:shop_trial_start_date, 30.days.ago.beginning_of_day) - spree_post :set_sells, { id: enterprise, sells: 'own' } + spree_post :register, { id: enterprise, sells: 'own' } expect(response).to redirect_to spree.admin_path - trial_expiry = Date.today.strftime("%Y-%m-%d") - expect(flash[:error]).to eq "Sorry, but you've already had a trial. Expired on: #{trial_expiry}" - expect(enterprise.reload.sells).to eq 'none' + expect(enterprise.reload.sells).to eq 'own' + expect(enterprise.shop_trial_start_date).to eq trial_start end end end context "if the trial has not finished" do + let(:trial_start) { Date.today.to_time } + before do - enterprise.shop_trial_start_date = Date.today.to_time - enterprise.save! + enterprise.update_attribute(:shop_trial_start_date, trial_start) end it "is allowed, but trial start date is not reset" do - spree_post :set_sells, { id: enterprise, sells: 'own' } + spree_post :register, { id: enterprise, sells: 'own' } expect(response).to redirect_to spree.admin_path - trial_expiry = (Date.today + 30.days).strftime("%Y-%m-%d") - expect(flash[:notice]).to eq "Welcome back! Your trial expires on: #{trial_expiry}" expect(enterprise.reload.sells).to eq 'own' - expect(enterprise.reload.shop_trial_start_date).to eq Date.today.to_time + expect(enterprise.shop_trial_start_date).to eq trial_start end end context "if a trial has not started" do it "is allowed" do - spree_post :set_sells, { id: enterprise, sells: 'own' } + spree_post :register, { id: enterprise, sells: 'own' } expect(response).to redirect_to spree.admin_path expect(flash[:success]).to eq "Congratulations! Registration for #{enterprise.name} is complete!" expect(enterprise.reload.sells).to eq 'own' expect(enterprise.reload.shop_trial_start_date).to be > Time.now-(1.minute) end end - - context "setting producer_profile_only to true" do - it "is ignored" do - spree_post :set_sells, { id: enterprise, sells: 'own', producer_profile_only: true } - expect(response).to redirect_to spree.admin_path - expect(enterprise.reload.producer_profile_only).to be false - end - end end context "setting 'sells' to any" do - it "is not allowed" do - spree_post :set_sells, { id: enterprise, sells: 'any' } - expect(response).to redirect_to spree.admin_path - expect(flash[:error]).to eq "Unauthorised" - expect(enterprise.reload.sells).to eq 'none' + context "if the trial has finished" do + let(:trial_start) { 30.days.ago.beginning_of_day } + + before do + enterprise.update_attribute(:shop_trial_start_date, trial_start) + end + + it "is allowed" do + Timecop.freeze(Time.zone.local(2015, 4, 16, 14, 0, 0)) do + spree_post :register, { id: enterprise, sells: 'any' } + expect(response).to redirect_to spree.admin_path + expect(enterprise.reload.sells).to eq 'any' + expect(enterprise.shop_trial_start_date).to eq trial_start + end + end + end + + context "if the trial has not finished" do + let(:trial_start) { Date.today.to_time } + + before do + enterprise.update_attribute(:shop_trial_start_date, trial_start) + end + + it "is allowed, but trial start date is not reset" do + spree_post :register, { id: enterprise, sells: 'any' } + expect(response).to redirect_to spree.admin_path + expect(enterprise.reload.sells).to eq 'any' + expect(enterprise.shop_trial_start_date).to eq trial_start + end + end + + context "if a trial has not started" do + it "is allowed" do + spree_post :register, { id: enterprise, sells: 'any' } + expect(response).to redirect_to spree.admin_path + expect(flash[:success]).to eq "Congratulations! Registration for #{enterprise.name} is complete!" + expect(enterprise.reload.sells).to eq 'any' + expect(enterprise.reload.shop_trial_start_date).to be > Time.now-(1.minute) + end end end context "settiing 'sells' to 'unspecified'" do it "is not allowed" do - spree_post :set_sells, { id: enterprise, sells: 'unspecified' } - expect(response).to redirect_to spree.admin_path - expect(flash[:error]).to eq "Unauthorised" - expect(enterprise.reload.sells).to eq 'none' + spree_post :register, { id: enterprise, sells: 'unspecified' } + expect(response).to render_template :welcome + expect(flash[:error]).to eq "Please select a package" end end end @@ -386,6 +436,21 @@ module Admin end end + context "as the owner of an enterprise" do + it "allows 'sells' and 'owner' to be changed" do + controller.stub spree_current_user: original_owner + bulk_enterprise_params = { enterprise_set: { collection_attributes: { '0' => { id: profile_enterprise1.id, sells: 'any', owner_id: new_owner.id }, '1' => { id: profile_enterprise2.id, sells: 'any', owner_id: new_owner.id } } } } + + spree_put :bulk_update, bulk_enterprise_params + profile_enterprise1.reload + profile_enterprise2.reload + expect(profile_enterprise1.sells).to eq 'any' + expect(profile_enterprise2.sells).to eq 'any' + expect(profile_enterprise1.owner).to eq original_owner + expect(profile_enterprise2.owner).to eq original_owner + end + end + context "as super admin" do it "allows 'sells' and 'owner' to be changed" do profile_enterprise1.enterprise_roles.build(user: new_owner).save @@ -449,5 +514,59 @@ module Admin end end end + + describe "index" do + context "as super admin" do + let(:super_admin) { create(:admin_user) } + let!(:user) { create_enterprise_user(enterprise_limit: 10) } + let!(:enterprise1) { create(:enterprise, sells: 'any', owner: user) } + let!(:enterprise2) { create(:enterprise, sells: 'own', owner: user) } + let!(:enterprise3) { create(:enterprise, sells: 'any', owner: create_enterprise_user ) } + + before do + controller.stub spree_current_user: super_admin + end + + context "html" do + it "returns all enterprises" do + spree_get :index, format: :html + expect(assigns(:collection)).to include enterprise1, enterprise2, enterprise3 + end + end + + context "json" do + it "returns all enterprises" do + spree_get :index, format: :json + expect(assigns(:collection)).to include enterprise1, enterprise2, enterprise3 + end + end + end + + context "as an enterprise user" do + let!(:user) { create_enterprise_user(enterprise_limit: 10) } + let!(:enterprise1) { create(:enterprise, sells: 'any', owner: user) } + let!(:enterprise2) { create(:enterprise, sells: 'own', owner: user) } + let!(:enterprise3) { create(:enterprise, sells: 'any', owner: create_enterprise_user ) } + + before do + controller.stub spree_current_user: user + end + + context "html" do + it "returns an empty @collection" do + spree_get :index, format: :html + expect(assigns(:collection)).to eq [] + end + end + + context "json" do + it "scopes @collection to enterprises editable by the user" do + spree_get :index, format: :json + expect(assigns(:collection)).to include enterprise1, enterprise2 + expect(assigns(:collection)).to_not include enterprise3 + end + end + end + end end end diff --git a/spec/controllers/admin/order_cycles_controller_spec.rb b/spec/controllers/admin/order_cycles_controller_spec.rb index e9b0f4ea95..0483c43714 100644 --- a/spec/controllers/admin/order_cycles_controller_spec.rb +++ b/spec/controllers/admin/order_cycles_controller_spec.rb @@ -76,6 +76,12 @@ module Admin expect(oc.orders_open_at.to_date).to eq Date.today - 21.days expect(oc.orders_close_at.to_date).to eq Date.today + 21.days end + + it "does nothing when no data is supplied" do + expect do + spree_put :bulk_update + end.to change(oc, :orders_open_at).by(0) + end end context "when I do not manage the coordinator of an order cycle" do diff --git a/spec/controllers/checkout_controller_spec.rb b/spec/controllers/checkout_controller_spec.rb index 0499febfc9..dd9e44c5be 100644 --- a/spec/controllers/checkout_controller_spec.rb +++ b/spec/controllers/checkout_controller_spec.rb @@ -55,7 +55,7 @@ describe CheckoutController do it "doesn't copy the previous shipping address from a pickup order" do old_order = create(:order, bill_address: create(:address), ship_address: create(:address)) Spree::Order.stub_chain(:order, :where, :where, :limit, :detect).and_return(old_order) - controller.send(:find_last_used_addresses, "email").last.should == nil + controller.send(:find_last_used_addresses, "email").last.should == nil end describe "building the order" do @@ -69,7 +69,7 @@ describe CheckoutController do get :edit assigns[:order].ship_address.address1.should be_nil end - + it "clears the ship address when re-rendering edit" do controller.should_receive(:clear_ship_address).and_return true order.stub(:update_attributes).and_return false @@ -119,6 +119,32 @@ describe CheckoutController do response.status.should == 200 response.body.should == {path: spree.order_path(order)}.to_json end + + describe "stale object handling" do + it "retries when a stale object error is encountered" do + order.stub(:update_attributes).and_return true + controller.stub(:state_callback) + + # The first time, raise a StaleObjectError. The second time, succeed. + order.stub(:next).once. + and_raise(ActiveRecord::StaleObjectError.new(Spree::Variant.new, 'update')) + order.stub(:next).once do + order.update_column :state, 'complete' + true + end + + xhr :post, :update, order: {}, use_route: :spree + response.status.should == 200 + end + + it "tries a maximum of 3 times before giving up and returning an error" do + order.stub(:update_attributes).and_return true + order.stub(:next) { raise ActiveRecord::StaleObjectError.new(Spree::Variant.new, 'update') } + + xhr :post, :update, order: {}, use_route: :spree + response.status.should == 400 + end + end end describe "Paypal routing" do diff --git a/spec/controllers/enterprise_confirmations_controller_spec.rb b/spec/controllers/enterprise_confirmations_controller_spec.rb index 6fdab61be2..49e7ffe6e1 100644 --- a/spec/controllers/enterprise_confirmations_controller_spec.rb +++ b/spec/controllers/enterprise_confirmations_controller_spec.rb @@ -50,6 +50,7 @@ describe EnterpriseConfirmationsController do end it "redirects to the user to reset their password" do + expect(new_user).to receive(:send_reset_password_instructions_without_delay).and_call_original spree_get :show, confirmation_token: unconfirmed_enterprise.confirmation_token expect(response).to redirect_to spree.edit_spree_user_password_path(new_user, :reset_password_token => "token", return_to: spree.admin_path) expect(flash[:success]).to eq I18n.t('devise.enterprise_confirmations.enterprise.confirmed') diff --git a/spec/controllers/enterprises_controller_spec.rb b/spec/controllers/enterprises_controller_spec.rb index 34d095c29a..1d9cb2eb9c 100644 --- a/spec/controllers/enterprises_controller_spec.rb +++ b/spec/controllers/enterprises_controller_spec.rb @@ -1,56 +1,6 @@ require 'spec_helper' describe EnterprisesController do - it "displays suppliers" do - s = create(:supplier_enterprise) - d = create(:distributor_enterprise) - - spree_get :suppliers - - assigns(:suppliers).should == [s] - end - - describe "displaying an enterprise and its products" do - let(:p) { create(:simple_product, supplier: s) } - let(:s) { create(:supplier_enterprise) } - let!(:c) { create(:distributor_enterprise) } - let(:d1) { create(:distributor_enterprise) } - let(:d2) { create(:distributor_enterprise) } - let(:oc1) { create(:simple_order_cycle) } - let(:oc2) { create(:simple_order_cycle) } - - it "displays products for the selected (order_cycle -> outgoing exchange)" do - create(:exchange, order_cycle: oc1, sender: s, receiver: c, incoming: true, variants: [p.master]) - create(:exchange, order_cycle: oc1, sender: c, receiver: d1, incoming: false, variants: [p.master]) - - controller.stub(:current_distributor) { d1 } - controller.stub(:current_order_cycle) { oc1 } - - spree_get :show, {id: d1} - - assigns(:products).should include p - end - - it "does not display other products in the order cycle or in the distributor" do - # Given a product that is in this order cycle on a different distributor - create(:exchange, order_cycle: oc1, sender: s, receiver: c, incoming: true, variants: [p.master]) - create(:exchange, order_cycle: oc1, sender: c, receiver: d2, incoming: false, variants: [p.master]) - - # And is also in this distributor in a different order cycle - create(:exchange, order_cycle: oc2, sender: s, receiver: c, incoming: true, variants: [p.master]) - create(:exchange, order_cycle: oc2, sender: c, receiver: d1, incoming: false, variants: [p.master]) - - # When I view the enterprise page for d1 x oc1 - controller.stub(:current_distributor) { d1 } - controller.stub(:current_order_cycle) { oc1 } - spree_get :show, {id: d1} - - # Then I should not see the product - assigns(:products).should_not include p - end - - end - describe "shopping for a distributor" do before(:each) do @@ -102,14 +52,6 @@ describe EnterprisesController do end end - context "when a distributor has not been chosen" do - it "redirects #show to distributor selection" do - @distributor = create(:distributor_enterprise) - spree_get :show, {id: @distributor} - response.should redirect_to spree.root_path - end - end - context "checking permalink availability" do # let(:enterprise) { create(:enterprise, permalink: 'enterprise_permalink') } diff --git a/spec/controllers/shop_controller_spec.rb b/spec/controllers/shop_controller_spec.rb index 68f09b78de..2c7105b356 100644 --- a/spec/controllers/shop_controller_spec.rb +++ b/spec/controllers/shop_controller_spec.rb @@ -162,7 +162,10 @@ describe ShopController do end it "returns price including fees" do - Spree::Variant.any_instance.stub(:price_with_fees).and_return 998.00 + # Price is 19.99 + OpenFoodNetwork::EnterpriseFeeCalculator.any_instance. + stub(:indexed_fees_for).and_return 978.01 + xhr :get, :products response.body.should have_content "998.0" end diff --git a/spec/controllers/home_controller_spec.rb b/spec/controllers/shops_controller_spec.rb similarity index 94% rename from spec/controllers/home_controller_spec.rb rename to spec/controllers/shops_controller_spec.rb index 924462741f..a4c66ea3a7 100644 --- a/spec/controllers/home_controller_spec.rb +++ b/spec/controllers/shops_controller_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe HomeController do +describe ShopsController do render_views let!(:distributor) { create(:distributor_enterprise) } let!(:invisible_distributor) { create(:distributor_enterprise, visible: false) } diff --git a/spec/controllers/spree/admin/overview_controller_spec.rb b/spec/controllers/spree/admin/overview_controller_spec.rb index d38d4b3b2e..868a779d4a 100644 --- a/spec/controllers/spree/admin/overview_controller_spec.rb +++ b/spec/controllers/spree/admin/overview_controller_spec.rb @@ -9,23 +9,37 @@ describe Spree::Admin::OverviewController do controller.stub spree_current_user: user end - context "when user own only one enterprise" do + context "when user owns only one enterprise" do let!(:enterprise) { create(:distributor_enterprise, owner: user) } - it "renders the single enterprise dashboard" do - spree_get :index - response.should render_template "single_enterprise_dashboard" - end + context "when the referer is not an admin page" do + before { @request.env['HTTP_REFERER'] = 'http://test.com/some_other_path' } - context "when the enterprise sells property has not been set" do - before do - enterprise.sells = "unspecified" - enterprise.save + context "and the enterprise has sells='unspecified'" do + before do + enterprise.update_attribute(:sells, "unspecified") + end + + it "redirects to the welcome page for the enterprise" do + spree_get :index + response.should redirect_to welcome_admin_enterprise_path(enterprise) + end end - it "renders the welcome page" do + context "and the enterprise does not have sells='unspecified'" do + it "renders the single enterprise dashboard" do + spree_get :index + response.should render_template "single_enterprise_dashboard" + end + end + end + + context "when the refer is an admin page" do + before { @request.env['HTTP_REFERER'] = 'http://test.com/admin' } + + it "renders the single enterprise dashboard" do spree_get :index - response.should render_template "welcome" + response.should render_template "single_enterprise_dashboard" end end end @@ -34,10 +48,36 @@ describe Spree::Admin::OverviewController do let!(:enterprise1) { create(:distributor_enterprise, owner: user) } let!(:enterprise2) { create(:distributor_enterprise, owner: user) } - it "renders the multi enterprise dashboard" do - spree_get :index - response.should render_template "multi_enterprise_dashboard" + context "when the referer is not an admin page" do + before { @request.env['HTTP_REFERER'] = 'http://test.com/some_other_path' } + + context "and at least one owned enterprise has sells='unspecified'" do + before do + enterprise1.update_attribute(:sells, "unspecified") + end + + it "redirects to the enterprises index" do + spree_get :index + response.should redirect_to admin_enterprises_path + end + end + + context "and no owned enterprises have sells='unspecified'" do + it "renders the multiple enterprise dashboard" do + spree_get :index + response.should render_template "multi_enterprise_dashboard" + end + end + end + + context "when the refer is an admin page" do + before { @request.env['HTTP_REFERER'] = 'http://test.com/admin' } + + it "renders the multiple enterprise dashboard" do + spree_get :index + response.should render_template "multi_enterprise_dashboard" + end end end end -end \ No newline at end of file +end diff --git a/spec/controllers/spree/admin/reports_controller_spec.rb b/spec/controllers/spree/admin/reports_controller_spec.rb index e930c1e339..b333e9b14e 100644 --- a/spec/controllers/spree/admin/reports_controller_spec.rb +++ b/spec/controllers/spree/admin/reports_controller_spec.rb @@ -56,6 +56,11 @@ describe Spree::Admin::ReportsController do order end + # Results + let(:resulting_orders_prelim) { assigns(:report).search.result } + let(:resulting_orders) { assigns(:report).table_items.map(&:order) } + let(:resulting_products) { assigns(:report).table_items.map(&:product) } + # As manager of a coordinator (c1) context "Coordinator Enterprise User" do before { login_as_enterprise_user [c1] } @@ -64,8 +69,8 @@ describe Spree::Admin::ReportsController do it "shows all orders in order cycles I coordinate" do spree_get :orders_and_fulfillment - assigns(:line_items).map(&:order).should include orderA1, orderA2 - assigns(:line_items).map(&:order).should_not include orderB1, orderB2 + resulting_orders.should include orderA1, orderA2 + resulting_orders.should_not include orderB1, orderB2 end end end @@ -88,9 +93,9 @@ describe Spree::Admin::ReportsController do it "only shows orders that I have access to" do spree_get :bulk_coop - assigns(:search).result.should include(orderA1, orderB1) - assigns(:search).result.should_not include(orderA2) - assigns(:search).result.should_not include(orderB2) + resulting_orders.should include(orderA1, orderB1) + resulting_orders.should_not include(orderA2) + resulting_orders.should_not include(orderB2) end end @@ -98,9 +103,9 @@ describe Spree::Admin::ReportsController do it "only shows orders that I have access to" do spree_get :payments - assigns(:search).result.should include(orderA1, orderB1) - assigns(:search).result.should_not include(orderA2) - assigns(:search).result.should_not include(orderB2) + resulting_orders_prelim.should include(orderA1, orderB1) + resulting_orders_prelim.should_not include(orderA2) + resulting_orders_prelim.should_not include(orderB2) end end @@ -108,15 +113,15 @@ describe Spree::Admin::ReportsController do it "only shows orders that I distribute" do spree_get :orders_and_fulfillment - assigns(:line_items).map(&:order).should include orderA1, orderB1 - assigns(:line_items).map(&:order).should_not include orderA2, orderB2 + resulting_orders.should include orderA1, orderB1 + resulting_orders.should_not include orderA2, orderB2 end it "only shows the selected order cycle" do spree_get :orders_and_fulfillment, q: {order_cycle_id_in: [ocA.id.to_s]} - assigns(:search).result.should include(orderA1) - assigns(:search).result.should_not include(orderB1) + resulting_orders.should include(orderA1) + resulting_orders.should_not include(orderB1) end end end @@ -129,8 +134,8 @@ describe Spree::Admin::ReportsController do it "only shows product line items that I am supplying" do spree_get :bulk_coop - assigns(:line_items).map(&:product).should include p1 - assigns(:line_items).map(&:product).should_not include p2, p3 + resulting_products.should include p1 + resulting_products.should_not include p2, p3 end end @@ -143,8 +148,8 @@ describe Spree::Admin::ReportsController do it "only shows product line items that I am supplying" do spree_get :orders_and_fulfillment - assigns(:line_items).map(&:product).should include p1 - assigns(:line_items).map(&:product).should_not include p2, p3 + resulting_products.should include p1 + resulting_products.should_not include p2, p3 end end @@ -152,15 +157,15 @@ describe Spree::Admin::ReportsController do it "does not show me line_items I supply" do spree_get :orders_and_fulfillment - assigns(:line_items).map(&:product).should_not include p1, p2, p3 + resulting_products.should_not include p1, p2, p3 end end it "only shows the selected order cycle" do spree_get :orders_and_fulfillment, q: {order_cycle_id_eq: ocA.id} - assigns(:search).result.should include(orderA1) - assigns(:search).result.should_not include(orderB1) + resulting_orders_prelim.should include(orderA1) + resulting_orders_prelim.should_not include(orderB1) end end end diff --git a/spec/controllers/spree/api/line_items_controller_spec.rb b/spec/controllers/spree/api/line_items_controller_spec.rb index 37ec50eb7e..abb355f0f3 100644 --- a/spec/controllers/spree/api/line_items_controller_spec.rb +++ b/spec/controllers/spree/api/line_items_controller_spec.rb @@ -11,7 +11,7 @@ module Spree def self.make_simple_data! let!(:order) { FactoryGirl.create(:order, state: 'complete', completed_at: Time.now) } - let!(:line_item) { FactoryGirl.create(:line_item, order: order, unit_value: 500) } + let!(:line_item) { FactoryGirl.create(:line_item, order: order, final_weight_volume: 500) } end #test that when a line item is updated, an order's fees are updated too @@ -21,7 +21,7 @@ module Spree context "as a line item is updated" do it "update distribution charge on the order" do - line_item_params = { order_id: order.number, id: line_item.id, line_item: { id: line_item.id, unit_value: 520 }, format: :json} + line_item_params = { order_id: order.number, id: line_item.id, line_item: { id: line_item.id, final_weight_volume: 520 }, format: :json} allow(controller).to receive(:order) { order } expect(order).to receive(:update_distribution_charge!) spree_post :update, line_item_params diff --git a/spec/controllers/spree/orders_controller_spec.rb b/spec/controllers/spree/orders_controller_spec.rb index 52bd21b500..d95012a6fb 100644 --- a/spec/controllers/spree/orders_controller_spec.rb +++ b/spec/controllers/spree/orders_controller_spec.rb @@ -27,6 +27,8 @@ describe Spree::OrdersController do end it "redirects home with message if hub is not ready for checkout" do + VariantOverride.stub(:indexed).and_return({}) + order = subject.current_order(true) distributor.stub(:ready_for_checkout?) { false } order.stub(distributor: distributor, order_cycle: order_cycle) diff --git a/spec/factories.rb b/spec/factories.rb index db2b3be299..0db34ab78c 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -25,7 +25,7 @@ FactoryGirl.define do ExchangeFee.create!(exchange: ex2, enterprise_fee: create(:enterprise_fee, enterprise: ex2.sender)) - #Distributors + # Distributors distributor1 = create(:distributor_enterprise) distributor2 = create(:distributor_enterprise) @@ -44,7 +44,7 @@ FactoryGirl.define do # Products with images [ex1, ex2].each do |exchange| product = create(:product, supplier: exchange.sender) - image = File.open(File.expand_path('../../app/assets/images/logo.jpg', __FILE__)) + image = File.open(File.expand_path('../../app/assets/images/logo-white.png', __FILE__)) Spree::Image.create({:viewable_id => product.master.id, :viewable_type => 'Spree::Variant', :alt => "position 1", :attachment => image, :position => 1}) exchange.variants << product.variants.first @@ -214,9 +214,29 @@ FactoryGirl.define do factory :customer, :class => Customer do email { Faker::Internet.email } enterprise - code { Faker::Lorem.word } + code { SecureRandom.base64(150) } user end + + factory :billable_period do + begins_at { Time.now.beginning_of_month } + ends_at { Time.now.beginning_of_month + 1.month } + sells { 'any' } + trial { false } + enterprise + owner { enterprise.owner } + turnover { rand(100000).to_f/100 } + account_invoice do + AccountInvoice.where(user_id: owner_id, year: begins_at.year, month: begins_at.month).first || + FactoryGirl.create(:account_invoice, user: owner, year: begins_at.year, month: begins_at.month) + end + end + + factory :account_invoice do + user { FactoryGirl.create :user } + year { 2000 + rand(100) } + month { 1 + rand(12) } + end end diff --git a/spec/features/admin/account_spec.rb b/spec/features/admin/account_spec.rb new file mode 100644 index 0000000000..f2be99c3a3 --- /dev/null +++ b/spec/features/admin/account_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +feature 'Account Page' do + include AuthenticationWorkflow + + describe "updating" do + let!(:user) { create(:user) } + let!(:enterprise) { create(:distributor_enterprise, owner: user) } + + before do + quick_login_as user + end + + context "as an enterprise user" do + it "loads the page" do + visit admin_account_path + expect(page).to have_content "Account" + end + end + end +end diff --git a/spec/features/admin/accounts_and_billing_settings_spec.rb b/spec/features/admin/accounts_and_billing_settings_spec.rb new file mode 100644 index 0000000000..b5316f6b21 --- /dev/null +++ b/spec/features/admin/accounts_and_billing_settings_spec.rb @@ -0,0 +1,60 @@ +require 'spec_helper' + +feature 'Account and Billing Settings' do + include AuthenticationWorkflow + include WebHelper + + describe "updating" do + let!(:admin) { create(:admin_user) } + let!(:pm1) { create(:payment_method) } + let!(:sm1) { create(:shipping_method) } + let!(:accounts_distributor) { create(:distributor_enterprise, payment_methods: [pm1], shipping_methods: [sm1]) } + + before do + Spree::Config.set({ + accounts_distributor_id: 0, + default_accounts_payment_method_id: 0, + default_accounts_shipping_method_id: 0, + auto_update_invoices: false, + auto_finalize_invoices: false + }) + end + + before do + quick_login_as_admin + end + + context "as an admin user", js: true do + it "loads the page" do + visit spree.admin_path + click_link "Configuration" + click_link "Accounts & Billing" + + expect(page).to have_select2 "settings_accounts_distributor_id" + select2_select accounts_distributor.name, from: "settings_accounts_distributor_id" + expect(page).to have_select "settings_default_accounts_payment_method_id" + expect(page).to have_select "settings_default_accounts_shipping_method_id" + expect(page).to have_link "Update User Invoices", href: start_job_admin_accounts_and_billing_settings_path(job: { name: 'update_account_invoices'}) + expect(page).to have_link "Finalise User Invoices", href: start_job_admin_accounts_and_billing_settings_path(job: { name: 'finalize_account_invoices'}) + end + + it "attributes can be changed", js: true do + visit edit_admin_accounts_and_billing_settings_path + + select2_select accounts_distributor.name, from: "settings_accounts_distributor_id" + select pm1.name, from: "settings_default_accounts_payment_method_id" + select sm1.name, from: "settings_default_accounts_shipping_method_id" + check "settings_auto_update_invoices" + check "settings_auto_finalize_invoices" + + click_button "Update" + + expect(Spree::Config.accounts_distributor_id).to eq accounts_distributor.id + expect(Spree::Config.default_accounts_payment_method_id).to eq pm1.id + expect(Spree::Config.default_accounts_shipping_method_id).to eq sm1.id + expect(Spree::Config.auto_update_invoices).to be true + expect(Spree::Config.auto_finalize_invoices).to be true + end + end + end +end diff --git a/spec/features/admin/bulk_order_management_spec.rb b/spec/features/admin/bulk_order_management_spec.rb index 3bbb23bb8a..8d52f21b48 100644 --- a/spec/features/admin/bulk_order_management_spec.rb +++ b/spec/features/admin/bulk_order_management_spec.rb @@ -132,6 +132,8 @@ feature %q{ click_button "Update" page.should_not have_selector "input[name='quantity'].update-pending" page.should have_selector "input[name='quantity'].update-success" + page.should have_selector "input[name='final_weight_volume'].update-success", visible: false + page.should have_selector "input[name='price'].update-success", visible: false end end end @@ -144,7 +146,7 @@ feature %q{ let!(:p1) { FactoryGirl.create(:product_with_option_types, group_buy: true, group_buy_unit_size: 5000, variant_unit: "weight", variants: [FactoryGirl.create(:variant, unit_value: 1000)] ) } let!(:v1) { p1.variants.first } let!(:o1) { FactoryGirl.create(:order_with_distributor, state: 'complete', completed_at: Time.now ) } - let!(:li1) { FactoryGirl.create(:line_item, order: o1, variant: v1, :quantity => 5, :unit_value => 1000 ) } + let!(:li1) { FactoryGirl.create(:line_item, order: o1, variant: v1, :quantity => 5, :final_weight_volume => 1000 ) } context "modifying the weight/volume of a line item" do it "update-pending is added to variable 'price'" do @@ -152,12 +154,36 @@ feature %q{ first("div#columns_dropdown", :text => "COLUMNS").click first("div#columns_dropdown div.menu div.menu_item", text: "Weight/Volume").click page.should_not have_css "input[name='price'].update-pending" - li1_unit_value_column = find("tr#li_#{li1.id} td.unit_value") - li1_unit_value_column.fill_in "unit_value", :with => 1200 + li1_final_weight_volume_column = find("tr#li_#{li1.id} td.final_weight_volume") + li1_final_weight_volume_column.fill_in "final_weight_volume", :with => 1200 page.should have_css "input[name='price'].update-pending", :visible => false end end + context "modifying the quantity of a line item" do + it "update-pending is added to variable 'price'" do + visit '/admin/orders/bulk_management' + #first("div#columns_dropdown", :text => "COLUMNS").click + #first("div#columns_dropdown div.menu div.menu_item", text: "Quantity").click + page.should_not have_css "input[name='price'].update-pending" + li1_quantity_column = find("tr#li_#{li1.id} td.quantity") + li1_quantity_column.fill_in "quantity", :with => 6 + page.should have_css "input[name='price'].update-pending", :visible => false + end + end + + context "modifying the quantity of a line item" do + it "update-pending is added to variable 'weight/volume'" do + visit '/admin/orders/bulk_management' + first("div#columns_dropdown", :text => "COLUMNS").click + first("div#columns_dropdown div.menu div.menu_item", text: "Weight/Volume").click + page.should_not have_css "input[name='price'].update-pending" + li1_quantity_column = find("tr#li_#{li1.id} td.quantity") + li1_quantity_column.fill_in "quantity", :with => 6 + page.should have_css "input[name='final_weight_volume'].update-pending", :visible => false + end + end + context "using column display toggle" do it "shows a column display toggle button, which shows a list of columns when clicked" do visit '/admin/orders/bulk_management' @@ -265,8 +291,7 @@ feature %q{ end it "displays a select box for order cycles, which filters line items by the selected order cycle" do - order_cycle_names = ["All"] - OrderCycle.all.each{ |oc| order_cycle_names << oc.name } + order_cycle_names = OrderCycle.pluck(:name).push "All" find("div.select2-container#s2id_order_cycle_filter").click order_cycle_names.each { |ocn| page.should have_selector "div.select2-drop-active ul.select2-results li", text: ocn } find("div.select2-container#s2id_order_cycle_filter").click @@ -421,8 +446,10 @@ feature %q{ page.fill_in "quantity", :with => (li2.quantity + 1).to_s end fill_in "start_date_filter", :with => (Date.today - 9).strftime("%F %T") + page.should have_selector "input[name='quantity'].update-pending" click_button "SAVE" - page.should_not have_selector "input[name='quantity'].update-pending" + page.should have_no_selector "input.update-pending" + page.should have_selector "input[name='quantity'].update-success" within("tr#li_#{li2.id} td.quantity") do page.should have_field "quantity", :with => ( li2.quantity + 1 ).to_s end diff --git a/spec/features/admin/content_spec.rb b/spec/features/admin/content_spec.rb new file mode 100644 index 0000000000..073ae35e3c --- /dev/null +++ b/spec/features/admin/content_spec.rb @@ -0,0 +1,40 @@ +require 'spec_helper' + +feature %q{ + As a site administrator + I want to configure the site content +} do + include AuthenticationWorkflow + include WebHelper + + before do + login_to_admin_section + click_link 'Configuration' + click_link 'Content' + end + + scenario "filling in a setting shows the result on the home page" do + fill_in 'footer_facebook_url', with: '' + fill_in 'footer_twitter_url', with: 'http://twitter.com/me' + fill_in 'footer_links_md', with: '[markdown link](/)' + click_button 'Update' + page.should have_content 'Your content has been successfully updated!' + + visit root_path + + # Then social media icons are only shown if they have a value + page.should_not have_selector 'i.ofn-i_044-facebook' + page.should have_selector 'i.ofn-i_041-twitter' + + # And markdown is rendered + page.should have_link 'markdown link' + end + + scenario "uploading logos" do + attach_file 'logo', "#{Rails.root}/app/assets/images/logo-white.png" + click_button 'Update' + page.should have_content 'Your content has been successfully updated!' + + ContentConfig.logo.to_s.should include "logo-white" + end +end diff --git a/spec/features/admin/enterprise_fees_spec.rb b/spec/features/admin/enterprise_fees_spec.rb index fe55d8e90b..d462a3d809 100644 --- a/spec/features/admin/enterprise_fees_spec.rb +++ b/spec/features/admin/enterprise_fees_spec.rb @@ -139,7 +139,9 @@ feature %q{ ef2 click_link 'Enterprises' - within(".enterprise-#{distributor1.id}") { click_link 'Enterprise Fees' } + within("#e_#{distributor1.id}") { click_link 'Manage' } + within(".side_menu") { click_link 'Enterprise Fees' } + click_link "Create One Now" select distributor1.name, :from => 'enterprise_fee_set_collection_attributes_0_enterprise_id' fill_in 'enterprise_fee_set_collection_attributes_0_name', :with => 'foo' @@ -156,17 +158,21 @@ feature %q{ enterprise_fee.enterprise.should == distributor1 end - it "shows me only enterprise fees for the enterprise I select" do + pending "shows me only enterprise fees for the enterprise I select" do ef1 ef2 click_link 'Enterprises' - within(".enterprise-#{distributor1.id}") { click_link 'Enterprise Fees' } + within("#e_#{distributor1.id}") { click_link 'Manage' } + within(".side_menu") { click_link 'Enterprise Fees' } + click_link "Manage Enterprise Fees" page.should have_field 'enterprise_fee_set_collection_attributes_0_name', with: 'One' page.should_not have_field 'enterprise_fee_set_collection_attributes_1_name', with: 'Two' click_link 'Enterprises' - within(".enterprise-#{distributor2.id}") { click_link 'Enterprise Fees' } + within("#e_#{distributor2.id}") { click_link 'Manage' } + within(".side_menu") { click_link 'Enterprise Fees' } + click_link "Manage Enterprise Fees" page.should_not have_field 'enterprise_fee_set_collection_attributes_0_name', with: 'One' page.should have_field 'enterprise_fee_set_collection_attributes_0_name', with: 'Two' end @@ -177,7 +183,9 @@ feature %q{ distributor3 click_link 'Enterprises' - within(".enterprise-#{distributor2.id}") { click_link 'Enterprise Fees' } + within("#e_#{distributor2.id}") { click_link 'Manage' } + within(".side_menu") { click_link 'Enterprise Fees' } + click_link "Manage Enterprise Fees" page.should have_select('enterprise_fee_set_collection_attributes_1_enterprise_id', selected: 'Second Distributor', options: ['', 'First Distributor', 'Second Distributor']) diff --git a/spec/features/admin/enterprises/index_spec.rb b/spec/features/admin/enterprises/index_spec.rb new file mode 100644 index 0000000000..5786e7a75b --- /dev/null +++ b/spec/features/admin/enterprises/index_spec.rb @@ -0,0 +1,214 @@ +require 'spec_helper' + +feature 'Enterprises Index' do + include AuthenticationWorkflow + include WebHelper + + context "as an admin user" do + scenario "listing enterprises" do + s = create(:supplier_enterprise) + d = create(:distributor_enterprise) + + login_to_admin_section + click_link 'Enterprises' + + within("tr.enterprise-#{s.id}") do + expect(page).to have_content s.name + expect(page).to have_select "enterprise_set_collection_attributes_1_sells" + expect(page).to have_content "Edit Profile" + expect(page).to have_content "Delete" + expect(page).to_not have_content "Payment Methods" + expect(page).to_not have_content "Shipping Methods" + expect(page).to have_content "Enterprise Fees" + end + + within("tr.enterprise-#{d.id}") do + expect(page).to have_content d.name + expect(page).to have_select "enterprise_set_collection_attributes_0_sells" + expect(page).to have_content "Edit Profile" + expect(page).to have_content "Delete" + expect(page).to have_content "Payment Methods" + expect(page).to have_content "Shipping Methods" + expect(page).to have_content "Enterprise Fees" + end + end + + context "editing enterprises in bulk" do + let!(:s){ create(:supplier_enterprise) } + let!(:d){ create(:distributor_enterprise, sells: 'none') } + let!(:d_manager) { create_enterprise_user(enterprise_limit: 1) } + + before do + d_manager.enterprise_roles.build(enterprise: d).save + expect(d.owner).to_not eq d_manager + end + + context "without violating rules" do + before do + login_to_admin_section + click_link 'Enterprises' + end + + it "updates the enterprises" do + within("tr.enterprise-#{d.id}") do + expect(page).to have_checked_field "enterprise_set_collection_attributes_0_visible" + uncheck "enterprise_set_collection_attributes_0_visible" + select 'any', from: "enterprise_set_collection_attributes_0_sells" + select d_manager.email, from: 'enterprise_set_collection_attributes_0_owner_id' + end + click_button "Update" + flash_message.should == 'Enterprises updated successfully' + distributor = Enterprise.find(d.id) + expect(distributor.visible).to eq false + expect(distributor.sells).to eq 'any' + expect(distributor.owner).to eq d_manager + end + end + + context "with data that violates rules" do + let!(:second_distributor) { create(:distributor_enterprise, sells: 'none') } + + before do + d_manager.enterprise_roles.build(enterprise: second_distributor).save + expect(d.owner).to_not eq d_manager + + login_to_admin_section + click_link 'Enterprises' + end + + it "does not update the enterprises and displays errors" do + within("tr.enterprise-#{d.id}") do + select d_manager.email, from: 'enterprise_set_collection_attributes_0_owner_id' + end + within("tr.enterprise-#{second_distributor.id}") do + select d_manager.email, from: 'enterprise_set_collection_attributes_1_owner_id' + end + click_button "Update" + flash_message.should == 'Update failed' + expect(page).to have_content "#{d_manager.email} is not permitted to own any more enterprises (limit is 1)." + second_distributor.reload + expect(second_distributor.owner).to_not eq d_manager + end + end + end + end + + describe "as the manager of an enterprise" do + let(:supplier1) { create(:supplier_enterprise, name: 'First Supplier') } + let(:supplier2) { create(:supplier_enterprise, name: 'Another Supplier') } + let(:distributor1) { create(:distributor_enterprise, name: 'First Distributor') } + let(:distributor2) { create(:distributor_enterprise, name: 'Another Distributor') } + let(:distributor3) { create(:distributor_enterprise, name: 'Yet Another Distributor') } + let(:enterprise_manager) { create_enterprise_user } + let!(:er) { create(:enterprise_relationship, parent: distributor3, child: distributor1, permissions_list: [:edit_profile]) } + + before(:each) do + enterprise_manager.enterprise_roles.build(enterprise: supplier1).save + enterprise_manager.enterprise_roles.build(enterprise: distributor1).save + + login_to_admin_as enterprise_manager + end + + context "listing enterprises", js: true do + it "displays enterprises I have permission to manage" do + click_link "Enterprises" + + within("tbody#e_#{distributor1.id}") do + expect(page).to have_content distributor1.name + expect(page).to have_selector "td.producer", text: 'Non-Producer' + expect(page).to have_selector "td.package", text: 'Hub' + end + + within("tbody#e_#{distributor3.id}") do + expect(page).to have_content distributor3.name + expect(page).to have_selector "td.producer", text: 'Non-Producer' + expect(page).to have_selector "td.package", text: 'Hub' + end + + within("tbody#e_#{supplier1.id}") do + expect(page).to have_content supplier1.name + expect(page).to have_selector "td.producer", text: 'Producer' + expect(page).to have_selector "td.package", text: 'Profile' + end + + expect(page).to_not have_content "supplier2.name" + expect(page).to_not have_content "distributor2.name" + + expect(find("#content-header")).to have_link "New Enterprise" + end + + + it "does not give me an option to change or update the package and producer properties of enterprises I manage" do + click_link "Enterprises" + + within("tbody#e_#{distributor1.id}") do + find("td.producer").click + expect(page).to have_selector "a.selector.producer.disabled" + find("a.selector.producer.disabled").click + expect(page).to have_selector "a.selector.non-producer.selected.disabled" + expect(page).to_not have_selector "a.update" + find("td.package").click + expect(page).to have_selector "a.selector.hub-profile.disabled" + find("a.selector.hub-profile.disabled").click + expect(page).to have_selector "a.selector.hub.selected.disabled" + expect(page).to_not have_selector "a.update" + end + end + end + end + + describe "as the owner of an enterprise" do + let!(:user) { create_enterprise_user } + let!(:owned_distributor) { create(:distributor_enterprise, name: 'Owned Distributor', owner: user) } + + before do + login_to_admin_as user + end + + context "listing enterprises", js: true do + it "allows me to change or update the package and producer properties of enterprises I manage" do + click_link "Enterprises" + + within("tbody#e_#{owned_distributor.id}") do + # Open the producer panel + find("td.producer").click + + expect(page).to_not have_selector "a.selector.producer.selected" + expect(page).to have_selector "a.selector.non-producer.selected" + + # Change to a producer + find("a.selector.producer").click + + expect(page).to_not have_selector "a.selector.non-producer.selected" + expect(page).to have_selector "a.selector.producer.selected" + expect(page).to have_selector "a.update", text: "SAVE" + + # Save selection + find('a.update').click + expect(page).to have_selector "a.update", text: "SAVED" + expect(owned_distributor.reload.is_primary_producer).to eq true + + # Open the package panel + find("td.package").click + + expect(page).to_not have_selector "a.selector.producer-profile.selected" + expect(page).to_not have_selector "a.selector.producer-shop.selected" + expect(page).to have_selector "a.selector.producer-hub.selected" + + # Change to a producer-shop + find("a.selector.producer-shop").click + + expect(page).to_not have_selector "a.selector.producer-profile.selected" + expect(page).to have_selector "a.selector.producer-shop.selected" + expect(page).to_not have_selector "a.selector.producer-hub.selected" + expect(page).to have_selector "a.update", text: "SAVE" + + # Save selection + find('a.update').click + expect(page).to have_selector "a.update", text: "SAVED" + expect(owned_distributor.reload.sells).to eq "own" + end + end + end + end +end diff --git a/spec/features/admin/enterprises_spec.rb b/spec/features/admin/enterprises_spec.rb index 395f882b51..43c6942bcf 100644 --- a/spec/features/admin/enterprises_spec.rb +++ b/spec/features/admin/enterprises_spec.rb @@ -7,93 +7,6 @@ feature %q{ include AuthenticationWorkflow include WebHelper - scenario "listing enterprises" do - s = create(:supplier_enterprise) - d = create(:distributor_enterprise) - - login_to_admin_section - click_link 'Enterprises' - - within("tr.enterprise-#{s.id}") do - expect(page).to have_content s.name - expect(page).to have_select "enterprise_set_collection_attributes_1_sells" - expect(page).to have_content "Edit Profile" - expect(page).to have_content "Delete" - expect(page).to_not have_content "Payment Methods" - expect(page).to_not have_content "Shipping Methods" - expect(page).to have_content "Enterprise Fees" - end - - within("tr.enterprise-#{d.id}") do - expect(page).to have_content d.name - expect(page).to have_select "enterprise_set_collection_attributes_0_sells" - expect(page).to have_content "Edit Profile" - expect(page).to have_content "Delete" - expect(page).to have_content "Payment Methods" - expect(page).to have_content "Shipping Methods" - expect(page).to have_content "Enterprise Fees" - end - end - - context "editing enterprises in bulk" do - let!(:s){ create(:supplier_enterprise) } - let!(:d){ create(:distributor_enterprise, sells: 'none') } - let!(:d_manager) { create_enterprise_user(enterprise_limit: 1) } - - before do - d_manager.enterprise_roles.build(enterprise: d).save - expect(d.owner).to_not eq d_manager - end - - context "without violating rules" do - before do - login_to_admin_section - click_link 'Enterprises' - end - - it "updates the enterprises" do - within("tr.enterprise-#{d.id}") do - expect(page).to have_checked_field "enterprise_set_collection_attributes_0_visible" - uncheck "enterprise_set_collection_attributes_0_visible" - select 'any', from: "enterprise_set_collection_attributes_0_sells" - select d_manager.email, from: 'enterprise_set_collection_attributes_0_owner_id' - end - click_button "Update" - flash_message.should == 'Enterprises updated successfully' - distributor = Enterprise.find(d.id) - expect(distributor.visible).to eq false - expect(distributor.sells).to eq 'any' - expect(distributor.owner).to eq d_manager - end - end - - context "with data that violates rules" do - let!(:second_distributor) { create(:distributor_enterprise, sells: 'none') } - - before do - d_manager.enterprise_roles.build(enterprise: second_distributor).save - expect(d.owner).to_not eq d_manager - - login_to_admin_section - click_link 'Enterprises' - end - - it "does not update the enterprises and displays errors" do - within("tr.enterprise-#{d.id}") do - select d_manager.email, from: 'enterprise_set_collection_attributes_0_owner_id' - end - within("tr.enterprise-#{second_distributor.id}") do - select d_manager.email, from: 'enterprise_set_collection_attributes_1_owner_id' - end - click_button "Update" - flash_message.should == 'Update failed' - expect(page).to have_content "#{d_manager.email} is not permitted to own any more enterprises (limit is 1)." - second_distributor.reload - expect(second_distributor.owner).to_not eq d_manager - end - end - end - scenario "viewing an enterprise" do e = create(:enterprise) @@ -165,6 +78,8 @@ feature %q{ end fill_in 'enterprise_name', :with => 'Eaterprises' + fill_in 'enterprise_permalink', with: 'eaterprises-permalink' + page.should have_selector '.available' choose 'Own' within (".side_menu") { click_link "Users" } @@ -325,56 +240,23 @@ feature %q{ end - context "as an Enterprise user" do + context "as an Enterprise user", js: true do let(:supplier1) { create(:supplier_enterprise, name: 'First Supplier') } let(:supplier2) { create(:supplier_enterprise, name: 'Another Supplier') } let(:distributor1) { create(:distributor_enterprise, name: 'First Distributor') } let(:distributor2) { create(:distributor_enterprise, name: 'Another Distributor') } let(:distributor3) { create(:distributor_enterprise, name: 'Yet Another Distributor') } let(:enterprise_user) { create_enterprise_user } - let(:er) { create(:enterprise_relationship, parent: distributor3, child: distributor1, permissions_list: [:edit_profile]) } + let!(:er) { create(:enterprise_relationship, parent: distributor3, child: distributor1, permissions_list: [:edit_profile]) } before(:each) do enterprise_user.enterprise_roles.build(enterprise: supplier1).save enterprise_user.enterprise_roles.build(enterprise: distributor1).save - er login_to_admin_as enterprise_user end - context "listing enterprises" do - scenario "displays enterprises I have permission to manage" do - oc_user_coordinating = create(:simple_order_cycle, { coordinator: supplier1, name: 'Order Cycle 1' } ) - oc_for_other_user = create(:simple_order_cycle, { coordinator: supplier2, name: 'Order Cycle 2' } ) - - click_link "Enterprises" - - within("tr.enterprise-#{distributor1.id}") do - expect(page).to have_content distributor1.name - expect(page).to have_unchecked_field "enterprise_set_collection_attributes_0_is_primary_producer" - expect(page).to_not have_select "enterprise_set_collection_attributes_0_sells" - end - - within("tr.enterprise-#{distributor3.id}") do - expect(page).to have_content distributor3.name - expect(page).to have_unchecked_field "enterprise_set_collection_attributes_1_is_primary_producer" - expect(page).to_not have_select "enterprise_set_collection_attributes_1_sells" - end - - within("tr.enterprise-#{supplier1.id}") do - expect(page).to have_content supplier1.name - expect(page).to have_checked_field "enterprise_set_collection_attributes_2_is_primary_producer" - expect(page).to_not have_select "enterprise_set_collection_attributes_2_sells" - end - - expect(page).to_not have_content "supplier2.name" - expect(page).to_not have_content "distributor2.name" - - expect(find("#content-header")).to have_link "New Enterprise" - end - end - - context "when I have reached my enterprise ownership limit", js: true do + context "when I have reached my enterprise ownership limit" do it "does not display the link to create a new enterprise" do supplier1.reload enterprise_user.owned_enterprises.push [supplier1] @@ -428,7 +310,7 @@ feature %q{ scenario "editing enterprises I manage" do click_link 'Enterprises' - within("#listing_enterprises tr.enterprise-#{distributor1.id}") { click_link 'Edit Profile' } + within("tbody#e_#{distributor1.id}") { click_link 'Manage' } fill_in 'enterprise_name', :with => 'Eaterprises' click_button 'Update' @@ -440,7 +322,7 @@ feature %q{ describe "enterprises I have edit permission for, but do not manage" do it "allows me to edit them" do click_link 'Enterprises' - within("#listing_enterprises tr.enterprise-#{distributor3.id}") { click_link 'Edit Profile' } + within("tbody#e_#{distributor3.id}") { click_link 'Manage' } fill_in 'enterprise_name', :with => 'Eaterprises' click_button 'Update' @@ -449,18 +331,9 @@ feature %q{ distributor3.reload.name.should == 'Eaterprises' end - it "does not show links to manage shipping methods, payment methods or enterprise fees" do + it "does not show links to manage shipping methods, payment methods or enterprise fees on the edit page" do click_link 'Enterprises' - within("#listing_enterprises tr.enterprise-#{distributor3.id}") do - page.should_not have_link 'Shipping Methods' - page.should_not have_link 'Payment Methods' - page.should_not have_link 'Enterprise Fees' - end - end - - it "does not show links to manage shipping methods, payment methods or enterprise fees on the edit page", js: true do - click_link 'Enterprises' - within("#listing_enterprises tr.enterprise-#{distributor3.id}") { click_link 'Edit Profile' } + within("tbody#e_#{distributor3.id}") { click_link 'Manage' } within(".side_menu") do page.should_not have_link 'Shipping Methods' @@ -472,26 +345,35 @@ feature %q{ scenario "editing images for an enterprise" do click_link 'Enterprises' - first(".edit").click - page.should have_content "Logo" - page.should have_content "Promo" + within("tbody#e_#{distributor1.id}") { click_link 'Manage' } + + within(".side_menu") do + click_link "Images" + end + + page.should have_content "LOGO" + page.should have_content "PROMO" end - scenario "managing producer properties", js: true do + scenario "managing producer properties" do create(:property, name: "Certified Organic") click_link 'Enterprises' - within(".enterprise-#{supplier1.id}") { click_link 'Properties' } + within("#e_#{supplier1.id}") { click_link 'Manage' } + within(".side_menu") do + click_link "Properties" + end # -- Update only select2_select "Certified Organic", from: 'enterprise_producer_properties_attributes_0_property_name' fill_in 'enterprise_producer_properties_attributes_0_value', with: "NASAA 12345" click_button 'Update' - page.should have_selector '#listing_enterprises a', text: supplier1.name supplier1.producer_properties(true).count.should == 1 # -- Destroy pp = supplier1.producer_properties.first - within(".enterprise-#{supplier1.id}") { click_link 'Properties' } + within(".side_menu") do + click_link "Properties" + end within("#spree_producer_property_#{pp.id}") { page.find('a.remove_fields').click } page.should_not have_selector '#progress' diff --git a/spec/features/admin/orders_spec.rb b/spec/features/admin/orders_spec.rb index c2a302fcbb..4a30c3a60c 100644 --- a/spec/features/admin/orders_spec.rb +++ b/spec/features/admin/orders_spec.rb @@ -21,7 +21,7 @@ feature %q{ create :check_payment, order: @order, amount: @order.total end - scenario "creating an order with distributor and order cycle", js: true do + scenario "creating an order with distributor and order cycle", js: true, retry: 3 do order_cycle = create(:order_cycle) distributor = order_cycle.distributors.first product = order_cycle.products.first @@ -50,11 +50,12 @@ feature %q{ scenario "can add a product to an existing order", js: true do login_to_admin_section visit '/admin/orders' - page.find('td.actions a.icon-edit').click - targetted_select2_search @product.name, from: ".variant_autocomplete", dropdown_css: ".select2-search" + click_edit - click_icon :plus + targetted_select2_search @product.name, from: '#add_variant_id', dropdown_css: '.select2-drop' + + click_link 'Add' page.should have_selector 'td', text: @product.name @order.line_items(true).map(&:product).should include @product @@ -153,4 +154,21 @@ feature %q{ end end + + # Working around intermittent click failing + # Possible causes of failure: + # - the link moves + # - the missing content (font icon only) + # - the screen is not big enough + # However, some operations before the click or a second click on failure work. + # + # A lot of people had similar problems: + # https://github.com/teampoltergeist/poltergeist/issues/520 + # https://github.com/thoughtbot/capybara-webkit/issues/494 + def click_edit + click_result = click_icon :edit + unless click_result['status'] == 'success' + click_icon :edit + end + end end diff --git a/spec/features/admin/payment_method_spec.rb b/spec/features/admin/payment_method_spec.rb index 36fe344930..aa7caa2e0b 100644 --- a/spec/features/admin/payment_method_spec.rb +++ b/spec/features/admin/payment_method_spec.rb @@ -20,12 +20,12 @@ feature %q{ click_link 'New Payment Method' fill_in 'payment_method_name', :with => 'Cheque payment method' - + check "payment_method_distributor_ids_#{@distributors[0].id}" click_button 'Create' flash_message.should == 'Payment Method has been successfully created!' - + payment_method = Spree::PaymentMethod.find_by_name('Cheque payment method') payment_method.distributors.should == [@distributors[0]] end @@ -53,7 +53,7 @@ feature %q{ end end - context "as an enterprise user" do + context "as an enterprise user", js: true do let(:enterprise_user) { create_enterprise_user } let(:distributor1) { create(:distributor_enterprise, name: 'First Distributor') } let(:distributor2) { create(:distributor_enterprise, name: 'Second Distributor') } @@ -70,8 +70,11 @@ feature %q{ it "I can get to the new enterprise page" do click_link 'Enterprises' - within(".enterprise-#{distributor1.id}") { click_link 'Payment Methods' } - click_link 'New Payment Method' + within("#e_#{distributor1.id}") { click_link 'Manage' } + within(".side_menu") do + click_link "Payment Methods" + end + click_link 'Create One Now' current_path.should == spree.new_admin_payment_method_path end @@ -109,17 +112,25 @@ feature %q{ end - it "shows me only payment methods for the enterprise I select" do + pending "shows me only payment methods for the enterprise I select" do pm1 pm2 click_link 'Enterprises' - within(".enterprise-#{distributor1.id}") { click_link 'Payment Methods' } + within("#e_#{distributor1.id}") { click_link 'Manage' } + within(".side_menu") do + click_link "Payment Methods" + end + page.should have_content pm1.name page.should have_content pm2.name click_link 'Enterprises' - within(".enterprise-#{distributor2.id}") { click_link 'Payment Methods' } + within("#e_#{distributor2.id}") { click_link 'Manage' } + within(".side_menu") do + click_link "Payment Methods" + end + page.should_not have_content pm1.name page.should have_content pm2.name end diff --git a/spec/features/admin/products_spec.rb b/spec/features/admin/products_spec.rb index a82674ddaf..2aafc82b02 100644 --- a/spec/features/admin/products_spec.rb +++ b/spec/features/admin/products_spec.rb @@ -82,7 +82,7 @@ feature %q{ visit spree.edit_admin_product_path(product) choose 'product_group_buy_1' - fill_in 'Group buy unit size', :with => '10' + fill_in 'Bulk unit size', :with => '10' click_button 'Update' @@ -117,31 +117,34 @@ feature %q{ end end - scenario "creating a new product", js: true do - Spree::Config.products_require_tax_category = false - click_link 'Products' - click_link 'New Product' + context "products do not require a tax category" do + scenario "creating a new product", js: true do + with_products_require_tax_category(false) do + click_link 'Products' + click_link 'New Product' - fill_in 'product_name', :with => 'A new product !!!' - fill_in 'product_price', :with => '19.99' + fill_in 'product_name', :with => 'A new product !!!' + fill_in 'product_price', :with => '19.99' - page.should have_selector('#product_supplier_id') - select 'Another Supplier', :from => 'product_supplier_id' - select 'Weight (g)', from: 'product_variant_unit_with_scale' - fill_in 'product_unit_value_with_description', with: '500' - select taxon.name, from: "product_primary_taxon_id" - select 'None', from: "product_tax_category_id" + page.should have_selector('#product_supplier_id') + select 'Another Supplier', :from => 'product_supplier_id' + select 'Weight (g)', from: 'product_variant_unit_with_scale' + fill_in 'product_unit_value_with_description', with: '500' + select taxon.name, from: "product_primary_taxon_id" + select 'None', from: "product_tax_category_id" - # Should only have suppliers listed which the user can manage - page.should have_select 'product_supplier_id', with_options: [@supplier2.name, @supplier_permitted.name] - page.should_not have_select 'product_supplier_id', with_options: [@supplier.name] + # Should only have suppliers listed which the user can manage + page.should have_select 'product_supplier_id', with_options: [@supplier2.name, @supplier_permitted.name] + page.should_not have_select 'product_supplier_id', with_options: [@supplier.name] - click_button 'Create' + click_button 'Create' - flash_message.should == 'Product "A new product !!!" has been successfully created!' - product = Spree::Product.find_by_name('A new product !!!') - product.supplier.should == @supplier2 - product.tax_category.should be_nil + flash_message.should == 'Product "A new product !!!" has been successfully created!' + product = Spree::Product.find_by_name('A new product !!!') + product.supplier.should == @supplier2 + product.tax_category.should be_nil + end + end end scenario "editing a product" do @@ -202,7 +205,7 @@ feature %q{ scenario "deleting product images", js: true do product = create(:simple_product, supplier: @supplier2) - image = File.open(File.expand_path('../../../../app/assets/images/logo.jpg', __FILE__)) + image = File.open(File.expand_path('../../../../app/assets/images/logo-white.png', __FILE__)) Spree::Image.create({:viewable_id => product.master.id, :viewable_type => 'Spree::Variant', :alt => "position 1", :attachment => image, :position => 1}) visit spree.admin_product_images_path(product) diff --git a/spec/features/admin/reports_spec.rb b/spec/features/admin/reports_spec.rb index 4f820bb004..9cefc7ba26 100644 --- a/spec/features/admin/reports_spec.rb +++ b/spec/features/admin/reports_spec.rb @@ -84,6 +84,66 @@ feature %q{ end end + describe "Packing reports" do + before do + login_to_admin_section + click_link "Reports" + end + + let(:bill_address1) { create(:address, lastname: "Aman") } + let(:bill_address2) { create(:address, lastname: "Bman") } + let(:distributor_address) { create(:address, :address1 => "distributor address", :city => 'The Shire', :zipcode => "1234") } + let(:distributor) { create(:distributor_enterprise, :address => distributor_address) } + let(:order1) { create(:order, distributor: distributor, bill_address: bill_address1) } + let(:order2) { create(:order, distributor: distributor, bill_address: bill_address2) } + let(:supplier) { create(:supplier_enterprise, name: "Supplier") } + let(:product_1) { create(:simple_product, name: "Product 1", supplier: supplier ) } + let(:variant_1) { create(:variant, product: product_1, unit_description: "Big") } + let(:variant_2) { create(:variant, product: product_1, unit_description: "Small") } + let(:product_2) { create(:simple_product, name: "Product 2", supplier: supplier) } + + before do + Timecop.travel(Time.zone.local(2013, 4, 25, 14, 0, 0)) { order1.finalize! } + Timecop.travel(Time.zone.local(2013, 4, 25, 15, 0, 0)) { order2.finalize! } + + create(:line_item, variant: variant_1, quantity: 1, order: order1) + create(:line_item, variant: variant_2, quantity: 3, order: order1) + create(:line_item, variant: product_2.master, quantity: 3, order: order2) + + end + + scenario "Pack By Customer" do + click_link "Pack By Customer" + fill_in 'q_completed_at_gt', with: '2013-04-25 13:00:00' + fill_in 'q_completed_at_lt', with: '2013-04-25 16:00:00' + #select 'Pack By Customer', from: 'report_type' + click_button 'Search' + + rows = find("table#listing_orders.index").all("thead tr") + table = rows.map { |r| r.all("th").map { |c| c.text.strip } } + table.sort.should == [ + ["Hub", "Code", "First Name", "Last Name", "Supplier", "Product", "Variant", "Quantity", "TempControlled?"] + ].sort + all('table#listing_orders tbody tr').count.should == 5 # Totals row per order + end + + scenario "Pack By Supplier" do + click_link "Pack By Supplier" + fill_in 'q_completed_at_gt', with: '2013-04-25 13:00:00' + fill_in 'q_completed_at_lt', with: '2013-04-25 16:00:00' + #select 'Pack By Customer', from: 'report_type' + click_button 'Search' + + rows = find("table#listing_orders").all("thead tr") + table = rows.map { |r| r.all("th").map { |c| c.text.strip } } + table.sort.should == [ + ["Hub", "Supplier", "Code", "First Name", "Last Name", "Product", "Variant", "Quantity", "TempControlled?"] + ].sort + all('table#listing_orders tbody tr').count.should == 4 # Totals row per supplier + end + end + + scenario "orders and distributors report" do login_to_admin_section click_link 'Reports' @@ -223,18 +283,27 @@ feature %q{ end describe "products and inventory report" do - it "shows products and inventory report" do - product1 = create(:simple_product, name: "Product Name", price: 100) - variant1 = product1.variants.first - variant2 = create(:variant, product: product1, price: 80.0) - product2 = create(:simple_product, name: "Product 2", price: 99.0, variant_unit: 'weight', variant_unit_scale: 1, unit_value: '100') - variant3 = product2.variants.first + let(:supplier) { create(:supplier_enterprise, name: 'Supplier Name') } + let(:taxon) { create(:taxon, name: 'Taxon Name') } + let(:product1) { create(:simple_product, name: "Product Name", price: 100, supplier: supplier, primary_taxon: taxon) } + let(:product2) { create(:simple_product, name: "Product 2", price: 99.0, variant_unit: 'weight', variant_unit_scale: 1, unit_value: '100', supplier: supplier, primary_taxon: taxon) } + let(:variant1) { product1.variants.first } + let(:variant2) { create(:variant, product: product1, price: 80.0) } + let(:variant3) { product2.variants.first } + + before do + product1.set_property 'Organic', 'NASAA 12345' + product2.set_property 'Organic', 'NASAA 12345' + product1.taxons = [taxon] + product2.taxons = [taxon] variant1.update_column(:count_on_hand, 10) variant2.update_column(:count_on_hand, 20) variant3.update_column(:count_on_hand, 9) variant1.option_values = [create(:option_value, :presentation => "Test")] variant2.option_values = [create(:option_value, :presentation => "Something")] + end + it "shows products and inventory report" do login_to_admin_section click_link 'Reports' @@ -243,15 +312,19 @@ feature %q{ click_link 'Products & Inventory' page.should have_content "Supplier" - rows = find("table#listing_products").all("tr") - table = rows.map { |r| r.all("th,td").map { |c| c.text.strip } } + page.should have_table_row ["Supplier", "Producer Suburb", "Product", "Product Properties", "Taxons", "Variant Value", "Price", "Group Buy Unit Quantity", "Amount"] + page.should have_table_row [product1.supplier.name, product1.supplier.address.city, "Product Name", product1.properties.map(&:presentation).join(", "), product1.primary_taxon.name, "Test", "100.0", product1.group_buy_unit_size.to_s, ""] + page.should have_table_row [product1.supplier.name, product1.supplier.address.city, "Product Name", product1.properties.map(&:presentation).join(", "), product1.primary_taxon.name, "Something", "80.0", product1.group_buy_unit_size.to_s, ""] + page.should have_table_row [product2.supplier.name, product1.supplier.address.city, "Product 2", product1.properties.map(&:presentation).join(", "), product2.primary_taxon.name, "100g", "99.0", product1.group_buy_unit_size.to_s, ""] + end - table.sort.should == [ - ["Supplier", "Producer Suburb", "Product", "Product Properties", "Taxons", "Variant Value", "Price", "Group Buy Unit Quantity", "Amount"], - [product1.supplier.name, product1.supplier.address.city, "Product Name", product1.properties.join(", "), product1.primary_taxon.name, "Test", "100.0", product1.group_buy_unit_size.to_s, ""], - [product1.supplier.name, product1.supplier.address.city, "Product Name", product1.properties.join(", "), product1.primary_taxon.name, "Something", "80.0", product1.group_buy_unit_size.to_s, ""], - [product2.supplier.name, product1.supplier.address.city, "Product 2", product1.properties.join(", "), product2.primary_taxon.name, "100g", "99.0", product1.group_buy_unit_size.to_s, ""] - ].sort + it "shows the LettuceShare report" do + login_to_admin_section + click_link 'Reports' + click_link 'LettuceShare' + + page.should have_table_row ['PRODUCT', 'Description', 'Qty', 'Pack Size', 'Unit', 'Unit Price', 'Total', 'GST incl.', 'Grower and growing method', 'Taxon'] + page.should have_table_row ['Product 2', '100g', '', '100', 'g', '99.0', '99.0', '0', 'Supplier Name (Organic - NASAA 12345)', 'Taxon Name'] end end @@ -315,8 +388,8 @@ feature %q{ let(:country) { Spree::Country.find Spree::Config.default_country_id } let(:bill_address) { create(:address, firstname: 'Customer', lastname: 'Name', address1: 'customer l1', address2: '', city: 'customer city', zipcode: 1234, country: country) } let(:order1) { create(:order, order_cycle: order_cycle, distributor: user1.enterprises.first, shipping_method: shipping_method, bill_address: bill_address) } - let(:product1) { create(:taxed_product, zone: zone, price: 12.54, tax_rate_amount: 0) } - let(:product2) { create(:taxed_product, zone: zone, price: 500.15, tax_rate_amount: 0.2) } + let(:product1) { create(:taxed_product, zone: zone, price: 12.54, tax_rate_amount: 0, sku: 'sku1') } + let(:product2) { create(:taxed_product, zone: zone, price: 500.15, tax_rate_amount: 0.2, sku: 'sku2') } let!(:line_item1) { create(:line_item, variant: product1.master, price: 12.54, quantity: 1, order: order1) } let!(:line_item2) { create(:line_item, variant: product2.master, price: 500.15, quantity: 3, order: order1) } @@ -345,11 +418,11 @@ feature %q{ it "shows Xero invoices report" do xero_invoice_table.should match_table [ xero_invoice_header, - xero_invoice_row('Total untaxable produce (no tax)', 12.54, 'GST Free Income'), - xero_invoice_row('Total taxable produce (tax inclusive)', 1500.45, 'GST on Income'), - xero_invoice_row('Total untaxable fees (no tax)', 10.0, 'GST Free Income'), - xero_invoice_row('Total taxable fees (tax inclusive)', 20.0, 'GST on Income'), - xero_invoice_row('Delivery Shipping Cost (tax inclusive)', 100.55, 'GST on Income') + xero_invoice_summary_row('Total untaxable produce (no tax)', 12.54, 'GST Free Income'), + xero_invoice_summary_row('Total taxable produce (tax inclusive)', 1500.45, 'GST on Income'), + xero_invoice_summary_row('Total untaxable fees (no tax)', 10.0, 'GST Free Income'), + xero_invoice_summary_row('Total taxable fees (tax inclusive)', 20.0, 'GST on Income'), + xero_invoice_summary_row('Delivery Shipping Cost (tax inclusive)', 100.55, 'GST on Income') ] end @@ -364,14 +437,60 @@ feature %q{ xero_invoice_table.should match_table [ xero_invoice_header, - xero_invoice_row('Total untaxable produce (no tax)', 12.54, 'GST Free Income', opts), - xero_invoice_row('Total taxable produce (tax inclusive)', 1500.45, 'GST on Income', opts), - xero_invoice_row('Total untaxable fees (no tax)', 10.0, 'GST Free Income', opts), - xero_invoice_row('Total taxable fees (tax inclusive)', 20.0, 'GST on Income', opts), - xero_invoice_row('Delivery Shipping Cost (tax inclusive)', 100.55, 'GST on Income', opts) + xero_invoice_summary_row('Total untaxable produce (no tax)', 12.54, 'GST Free Income', opts), + xero_invoice_summary_row('Total taxable produce (tax inclusive)', 1500.45, 'GST on Income', opts), + xero_invoice_summary_row('Total untaxable fees (no tax)', 10.0, 'GST Free Income', opts), + xero_invoice_summary_row('Total taxable fees (tax inclusive)', 20.0, 'GST on Income', opts), + xero_invoice_summary_row('Delivery Shipping Cost (tax inclusive)', 100.55, 'GST on Income', opts) ] end + it "generates a detailed report" do + select 'Detailed', from: 'report_type' + click_button 'Search' + + opts = {} + + xero_invoice_table.should match_table [ + xero_invoice_header, + xero_invoice_li_row(line_item1), + xero_invoice_li_row(line_item2), + xero_invoice_summary_row('Total untaxable fees (no tax)', 10.0, 'GST Free Income', opts), + xero_invoice_summary_row('Total taxable fees (tax inclusive)', 20.0, 'GST on Income', opts), + xero_invoice_summary_row('Delivery Shipping Cost (tax inclusive)', 100.55, 'GST on Income', opts) + ] + end + + describe "account invoices" do + let(:accounts_distributor) { create(:distributor_enterprise) } + let(:billable_period) { create(:billable_period, account_invoice: account_invoice) } + let(:account_invoice) { create(:account_invoice, order: account_invoice_order) } + let!(:account_invoice_order) { create(:order, order_cycle: order_cycle, distributor: accounts_distributor) } + let!(:adjustment) { create(:adjustment, adjustable: account_invoice_order, source: billable_period, label: 'Account invoice item', amount: 12.34) } # Tax? + + before do + Spree::Config.accounts_distributor_id = accounts_distributor.id + + account_invoice_order.update_attribute :email, 'customer@email.com' + Timecop.travel(Time.zone.local(2015, 4, 25, 14, 0, 0)) { account_invoice_order.finalize! } + + visit current_path + end + + it "generates a detailed report for account invoices" do + select 'Detailed', from: 'report_type' + select accounts_distributor.name, from: 'q_distributor_id_eq' + click_button 'Search' + + opts = {} + + xero_invoice_table.should match_table [ + xero_invoice_header, + xero_invoice_account_invoice_row(adjustment) + ] + end + end + private @@ -383,11 +502,30 @@ feature %q{ %w(*ContactName EmailAddress POAddressLine1 POAddressLine2 POAddressLine3 POAddressLine4 POCity PORegion POPostalCode POCountry *InvoiceNumber Reference *InvoiceDate *DueDate InventoryItemCode *Description *Quantity *UnitAmount Discount *AccountCode *TaxType TrackingName1 TrackingOption1 TrackingName2 TrackingOption2 Currency BrandingTheme Paid?) end - def xero_invoice_row(description, amount, tax_type, opts={}) - opts.reverse_merge!({invoice_number: order1.number, invoice_date: '2015-04-26', due_date: '2015-05-10', account_code: 'food sales'}) + def xero_invoice_summary_row(description, amount, tax_type, opts={}) + xero_invoice_row '', description, amount, '1', tax_type, opts + end - ['Customer Name', 'customer@email.com', 'customer l1', '', '', '', 'customer city', 'Victoria', '1234', country.name, opts[:invoice_number], order1.number, opts[:invoice_date], opts[:due_date], '', description, '1', amount.to_s, '', opts[:account_code], tax_type, '', '', '', '', Spree::Config.currency, '', 'N'] + def xero_invoice_li_row(line_item, opts={}) + tax_type = line_item.has_tax? ? 'GST on Income' : 'GST Free Income' + xero_invoice_row line_item.product.sku, line_item.variant.product_and_variant_name, line_item.price.to_s, line_item.quantity.to_s, tax_type, opts + end + def xero_invoice_account_invoice_row(adjustment, opts={}) + opts.reverse_merge!({customer_name: '', address1: '', city: '', state: '', zipcode: '', country: '', invoice_number: account_invoice_order.number, order_number: account_invoice_order.number}) + tax_type = adjustment.has_tax? ? 'GST on Income' : 'GST Free Income' + xero_invoice_row('', adjustment.label, adjustment.amount, '1', tax_type, opts) + end + + def xero_invoice_row(sku, description, amount, quantity, tax_type, opts={}) + opts.reverse_merge!({customer_name: 'Customer Name', address1: 'customer l1', city: 'customer city', state: 'Victoria', zipcode: '1234', country: country.name, invoice_number: order1.number, order_number: order1.number, invoice_date: '2015-04-26', due_date: '2015-05-10', account_code: 'food sales'}) + + [opts[:customer_name], 'customer@email.com', opts[:address1], '', '', '', opts[:city], opts[:state], opts[:zipcode], opts[:country], opts[:invoice_number], opts[:order_number], opts[:invoice_date], opts[:due_date], + + sku, + description, + quantity, + amount.to_s, '', opts[:account_code], tax_type, '', '', '', '', Spree::Config.currency, '', 'N'] end end end diff --git a/spec/features/admin/shipping_methods_spec.rb b/spec/features/admin/shipping_methods_spec.rb index 2857fa6502..9f04b6707b 100644 --- a/spec/features/admin/shipping_methods_spec.rb +++ b/spec/features/admin/shipping_methods_spec.rb @@ -61,7 +61,7 @@ feature 'shipping methods' do end end - context "as an enterprise user" do + context "as an enterprise user", js: true do let(:enterprise_user) { create_enterprise_user } let(:distributor1) { create(:distributor_enterprise, name: 'First Distributor') } let(:distributor2) { create(:distributor_enterprise, name: 'Second Distributor') } @@ -78,8 +78,11 @@ feature 'shipping methods' do it "creating a shipping method" do click_link 'Enterprises' - within(".enterprise-#{distributor1.id}") { click_link 'Shipping Methods' } - click_link 'New Shipping Method' + within("#e_#{distributor1.id}") { click_link 'Manage' } + within(".side_menu") do + click_link "Shipping Methods" + end + click_link 'Create One Now' # Show the correct fields page.should have_field 'shipping_method_name' @@ -119,17 +122,24 @@ feature 'shipping methods' do page.all('td', text: 'Two').count.should == 1 end - it "shows me only shipping methods for the enterprise I select" do + pending "shows me only shipping methods for the enterprise I select" do sm1 sm2 click_link 'Enterprises' - within(".enterprise-#{distributor1.id}") { click_link 'Shipping Methods' } + within("#e_#{distributor1.id}") { click_link 'Manage' } + within(".side_menu") do + click_link "Shipping Methods" + end page.should have_content sm1.name page.should have_content sm2.name click_link 'Enterprises' - within(".enterprise-#{distributor2.id}") { click_link 'Shipping Methods' } + within("#e_#{distributor2.id}") { click_link 'Manage' } + within(".side_menu") do + click_link "Shipping Methods" + end + page.should_not have_content sm1.name page.should have_content sm2.name end diff --git a/spec/features/admin/tax_settings_spec.rb b/spec/features/admin/tax_settings_spec.rb new file mode 100644 index 0000000000..9894ee8a72 --- /dev/null +++ b/spec/features/admin/tax_settings_spec.rb @@ -0,0 +1,56 @@ +require 'spec_helper' + +feature 'Account and Billing Settings' do + include AuthenticationWorkflow + include WebHelper + + describe "updating" do + let!(:admin) { create(:admin_user) } + + before do + Spree::Config.set({ + products_require_tax_category: false, + shipment_inc_vat: false, + shipping_tax_rate: 0, + account_bill_inc_tax: false, + account_bill_tax_rate: 0 + }) + end + + before do + quick_login_as_admin + end + + context "as an admin user" do + it "loads the page" do + visit spree.admin_path + click_link "Configuration" + click_link "Tax Settings" + + expect(page).to have_unchecked_field 'preferences_products_require_tax_category' + expect(page).to have_unchecked_field 'preferences_shipment_inc_vat' + expect(page).to have_field 'preferences_shipping_tax_rate' + expect(page).to have_unchecked_field 'preferences_account_bill_inc_tax' + expect(page).to have_field 'preferences_account_bill_tax_rate' + end + + it "attributes can be changed" do + visit spree.edit_admin_tax_settings_path + + check 'preferences_products_require_tax_category' + check 'preferences_shipment_inc_vat' + fill_in 'preferences_shipping_tax_rate', with: '0.12' + check 'preferences_account_bill_inc_tax' + fill_in 'preferences_account_bill_tax_rate', with: '0.05' + + click_button "Update" + + expect(Spree::Config.products_require_tax_category).to be true + expect(Spree::Config.shipment_inc_vat).to be true + expect(Spree::Config.shipping_tax_rate).to eq 0.12 + expect(Spree::Config.account_bill_inc_tax).to be true + expect(Spree::Config.account_bill_tax_rate).to eq 0.05 + end + end + end +end diff --git a/spec/features/consumer/authentication_spec.rb b/spec/features/consumer/authentication_spec.rb index bfa9f141fe..b01b04fe84 100644 --- a/spec/features/consumer/authentication_spec.rb +++ b/spec/features/consumer/authentication_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -feature "Authentication", js: true do +feature "Authentication", js: true, retry: 3 do include UIComponentHelper # Attempt to address intermittent failures in these specs diff --git a/spec/features/consumer/producers_spec.rb b/spec/features/consumer/producers_spec.rb index 6e09365195..8a26eaf026 100644 --- a/spec/features/consumer/producers_spec.rb +++ b/spec/features/consumer/producers_spec.rb @@ -10,6 +10,8 @@ feature %q{ let!(:invisible_producer) { create(:supplier_enterprise, visible: false) } let(:taxon) { create(:taxon) } let!(:product) { create(:simple_product, supplier: producer, taxons: [taxon]) } + let(:shop) { create(:distributor_enterprise) } + let!(:er) { create(:enterprise_relationship, parent: shop, child: producer) } before do visit producers_path @@ -24,4 +26,9 @@ feature %q{ it "doesn't show invisible producers" do page.should_not have_content invisible_producer.name end + + it "links to places to buy produce" do + expand_active_table_node producer.name + page.should have_link shop.name + end end diff --git a/spec/features/consumer/shopping/cart_spec.rb b/spec/features/consumer/shopping/cart_spec.rb index 7ec4c75004..58c570735d 100644 --- a/spec/features/consumer/shopping/cart_spec.rb +++ b/spec/features/consumer/shopping/cart_spec.rb @@ -24,7 +24,6 @@ feature "full-page cart", js: true do end it "shows the total tax for the order, including product tax and tax on fees" do - save_screenshot '/home/rohan/ss.png', full: true page.should have_selector '.tax-total', text: '11.00' # 10 + 1 end end diff --git a/spec/features/consumer/shopping/shopping_spec.rb b/spec/features/consumer/shopping/shopping_spec.rb index 248826ea5a..f1bed3a276 100644 --- a/spec/features/consumer/shopping/shopping_spec.rb +++ b/spec/features/consumer/shopping/shopping_spec.rb @@ -22,7 +22,7 @@ feature "As a consumer I want to shop with a distributor", js: true do it "shows a distributor with images" do # Given the distributor has a logo - distributor.logo = File.new(Rails.root + 'app/assets/images/logo.jpg') + distributor.logo = File.new(Rails.root + 'app/assets/images/logo-white.png') distributor.save! # Then we should see the distributor and its logo @@ -42,7 +42,7 @@ feature "As a consumer I want to shop with a distributor", js: true do end describe "selecting an order cycle" do - let(:exchange1) { Exchange.find(oc1.exchanges.to_enterprises(distributor).outgoing.first.id) } + let(:exchange1) { oc1.exchanges.to_enterprises(distributor).outgoing.first } it "selects an order cycle if only one is open" do exchange1.update_attribute :pickup_time, "turtles" @@ -51,7 +51,8 @@ feature "As a consumer I want to shop with a distributor", js: true do end describe "with multiple order cycles" do - let(:exchange2) { Exchange.find(oc2.exchanges.to_enterprises(distributor).outgoing.first.id) } + let(:exchange2) { oc2.exchanges.to_enterprises(distributor).outgoing.first } + before do exchange1.update_attribute :pickup_time, "frogs" exchange2.update_attribute :pickup_time, "turtles" @@ -83,6 +84,59 @@ feature "As a consumer I want to shop with a distributor", js: true do open_product_modal product modal_should_be_open_for product end + + describe "changing order cycle" do + it "shows the correct fees after selecting and changing an order cycle" do + enterprise_fee = create(:enterprise_fee, amount: 1001) + exchange2.enterprise_fees << enterprise_fee + exchange2.variants << variant + exchange1.variants << variant + + # -- Selecting an order cycle + visit shop_path + select "turtles", from: "order_cycle_id" + page.should have_content "$1020.99" + + # -- Cart shows correct price + fill_in "variants[#{variant.id}]", with: 1 + show_cart + within("li.cart") { page.should have_content "$1020.99" } + + # -- Changing order cycle + select "frogs", from: "order_cycle_id" + page.should have_content "$19.99" + + # -- Cart should be cleared + # ng-animate means that the old product row is likely to be present, so we explicitly + # fill in the quantity in the incoming row + page.should_not have_selector "tr.product-cart" + within('product.ng-enter') { fill_in "variants[#{variant.id}]", with: 1 } + within("li.cart") { page.should have_content "$19.99" } + end + + describe "declining to clear the cart" do + before do + exchange2.variants << variant + exchange1.variants << variant + + visit shop_path + select "turtles", from: "order_cycle_id" + fill_in "variants[#{variant.id}]", with: 1 + end + + it "leaves the cart untouched when the user declines" do + handle_js_confirm(false) do + select "frogs", from: "order_cycle_id" + show_cart + page.should have_selector "tr.product-cart" + page.should have_selector 'li.cart', text: '1 item' + + # The order cycle choice should not have changed + page.should have_select 'order_cycle_id', selected: 'turtles' + end + end + end + end end end @@ -134,15 +188,20 @@ feature "As a consumer I want to shop with a distributor", js: true do end it "should save group buy data to the cart" do + # -- Quantity fill_in "variants[#{variant.id}]", with: 6 - fill_in "variant_attributes[#{variant.id}][max_quantity]", with: 7 page.should have_in_cart product.name + wait_until { !cart_dirty } + li = Spree::Order.order(:created_at).last.line_items.order(:created_at).last + li.quantity.should == 6 + + # -- Max quantity + fill_in "variant_attributes[#{variant.id}][max_quantity]", with: 7 wait_until { !cart_dirty } li = Spree::Order.order(:created_at).last.line_items.order(:created_at).last li.max_quantity.should == 7 - li.quantity.should == 6 end end end diff --git a/spec/features/consumer/shopping/variant_overrides_spec.rb b/spec/features/consumer/shopping/variant_overrides_spec.rb index 98d58e174f..f850436e52 100644 --- a/spec/features/consumer/shopping/variant_overrides_spec.rb +++ b/spec/features/consumer/shopping/variant_overrides_spec.rb @@ -15,21 +15,26 @@ feature "shopping with variant overrides defined", js: true do let(:pm) { hub.payment_methods.first } let(:p1) { create(:simple_product, supplier: producer) } let(:p2) { create(:simple_product, supplier: producer) } + let(:p3) { create(:simple_product, supplier: producer, on_demand: true) } let(:v1) { create(:variant, product: p1, price: 11.11, unit_value: 1) } let(:v2) { create(:variant, product: p1, price: 22.22, unit_value: 2) } let(:v3) { create(:variant, product: p2, price: 33.33, unit_value: 3) } let(:v4) { create(:variant, product: p1, price: 44.44, unit_value: 4) } + let(:v5) { create(:variant, product: p3, price: 55.55, unit_value: 5, on_demand: true) } + let(:v6) { create(:variant, product: p3, price: 66.66, unit_value: 6, on_demand: true) } let!(:vo1) { create(:variant_override, hub: hub, variant: v1, price: 55.55, count_on_hand: nil) } let!(:vo2) { create(:variant_override, hub: hub, variant: v2, count_on_hand: 0) } let!(:vo3) { create(:variant_override, hub: hub, variant: v3, count_on_hand: 0) } let!(:vo4) { create(:variant_override, hub: hub, variant: v4, count_on_hand: 3) } + let!(:vo5) { create(:variant_override, hub: hub, variant: v5, count_on_hand: 0) } + let!(:vo6) { create(:variant_override, hub: hub, variant: v6, count_on_hand: 6) } let(:ef) { create(:enterprise_fee, enterprise: hub, fee_type: 'packing', calculator: Spree::Calculator::FlatPercentItemTotal.new(preferred_flat_percent: 10)) } before do - outgoing_exchange.variants = [v1, v2, v3, v4] + outgoing_exchange.variants = [v1, v2, v3, v4, v5, v6] outgoing_exchange.enterprise_fees << ef sm.calculator.preferred_amount = 0 - visit shop_path + visit shops_path click_link hub.name end @@ -46,6 +51,9 @@ feature "shopping with variant overrides defined", js: true do # Entire product should not appear - no stock page.should_not have_content p2.name page.should_not have_content v3.options_text + + # On-demand product with VO of no stock should NOT appear + page.should_not have_content v5.options_text end it "calculates fees correctly" do @@ -89,9 +97,7 @@ feature "shopping with variant overrides defined", js: true do it "shows the correct prices in the checkout" do fill_in "variants[#{v1.id}]", with: "2" - show_cart - wait_until_enabled 'li.cart a.button' - click_link 'Checkout now' + click_checkout page.should have_selector 'form.edit_order .cart-total', text: '$122.21' page.should have_selector 'form.edit_order .shipping', text: '$0.00' @@ -103,9 +109,7 @@ feature "shopping with variant overrides defined", js: true do describe "creating orders" do it "creates the order with the correct prices" do fill_in "variants[#{v1.id}]", with: "2" - show_cart - wait_until_enabled 'li.cart a.button' - click_link 'Checkout now' + click_checkout complete_checkout @@ -116,9 +120,7 @@ feature "shopping with variant overrides defined", js: true do it "subtracts stock from the override" do fill_in "variants[#{v4.id}]", with: "2" - show_cart - wait_until_enabled 'li.cart a.button' - click_link 'Checkout now' + click_checkout expect do expect do @@ -127,11 +129,20 @@ feature "shopping with variant overrides defined", js: true do end.to change { vo4.reload.count_on_hand }.by(-2) end + it "subtracts stock from stock-overridden on_demand variants" do + fill_in "variants[#{v6.id}]", with: "2" + click_checkout + + expect do + expect do + complete_checkout + end.to change { v6.reload.count_on_hand }.by(0) + end.to change { vo6.reload.count_on_hand }.by(-2) + end + it "does not subtract stock from overrides that do not override count_on_hand" do fill_in "variants[#{v1.id}]", with: "2" - show_cart - wait_until_enabled 'li.cart a.button' - click_link 'Checkout now' + click_checkout expect do complete_checkout @@ -142,9 +153,7 @@ feature "shopping with variant overrides defined", js: true do it "does not show out of stock flags on order confirmation page" do v4.update_attribute :count_on_hand, 0 fill_in "variants[#{v4.id}]", with: "2" - show_cart - wait_until_enabled 'li.cart a.button' - click_link 'Checkout now' + click_checkout complete_checkout @@ -187,4 +196,11 @@ feature "shopping with variant overrides defined", js: true do place_order page.should have_content "Your order has been processed successfully" end + + def click_checkout + show_cart + wait_until_enabled 'li.cart a.button' + click_link 'Checkout now', match: :first + end + end diff --git a/spec/features/consumer/home_spec.rb b/spec/features/consumer/shops_spec.rb similarity index 95% rename from spec/features/consumer/home_spec.rb rename to spec/features/consumer/shops_spec.rb index 128b1f734f..faeff9c37d 100644 --- a/spec/features/consumer/home_spec.rb +++ b/spec/features/consumer/shops_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -feature 'Home', js: true do +feature 'Shops', js: true do include AuthenticationWorkflow include UIComponentHelper @@ -13,7 +13,7 @@ feature 'Home', js: true do let!(:er) { create(:enterprise_relationship, parent: distributor, child: producer) } before do - visit "/" + visit shops_path end it "shows hubs" do @@ -28,7 +28,7 @@ feature 'Home', js: true do it "should grey out hubs that are not in an order cycle" do create(:simple_product, distributors: [d1, d2]) - visit root_path + visit shops_path page.should have_selector 'hub.inactive' page.should have_selector 'hub.inactive', text: d2.name end diff --git a/spec/features/consumer/suppliers_spec.rb b/spec/features/consumer/suppliers_spec.rb deleted file mode 100644 index d110e89ca4..0000000000 --- a/spec/features/consumer/suppliers_spec.rb +++ /dev/null @@ -1,34 +0,0 @@ -require 'spec_helper' - -feature %q{ - As a consumer - I want to see a list of products from a supplier - So that I can connect with them (and maybe buy stuff too) -} do - include AuthenticationWorkflow - include WebHelper - - background do - create(:distributor_enterprise, :name => "Edible garden") - end - - scenario "entering the site via a supplier's page" do - # Given a supplier with some distributed products - s = create(:supplier_enterprise) - d = create(:distributor_enterprise, with_payment_and_shipping: true) - p = create(:simple_product, supplier: s) - oc = create(:simple_order_cycle, suppliers: [s], distributors: [d], variants: [p.master]) - - # When I visit a supplier page - visit enterprise_path(s) - - # Then I should see a list of hubs that distribute the suppliers products - page.should have_link d.name - - # When I click on a hub - click_link d.name - - # Then that hub should be selected - page.should have_content d.name - end -end diff --git a/spec/javascripts/unit/admin/enterprises/controllers/enterprise_controller_spec.js.coffee b/spec/javascripts/unit/admin/enterprises/controllers/enterprise_controller_spec.js.coffee index 71e4c37aee..b504ff8f90 100644 --- a/spec/javascripts/unit/admin/enterprises/controllers/enterprise_controller_spec.js.coffee +++ b/spec/javascripts/unit/admin/enterprises/controllers/enterprise_controller_spec.js.coffee @@ -1,16 +1,15 @@ describe "enterpriseCtrl", -> ctrl = null scope = null - Enterprise = null + enterprise = null PaymentMethods = null ShippingMethods = null beforeEach -> module('admin.enterprises') - Enterprise = - enterprise: - is_primary_producer: true - sells: "none" + enterprise = + is_primary_producer: true + sells: "none" PaymentMethods = paymentMethods: "payment methods" ShippingMethods = @@ -18,11 +17,11 @@ describe "enterpriseCtrl", -> inject ($rootScope, $controller) -> scope = $rootScope - ctrl = $controller 'enterpriseCtrl', {$scope: scope, Enterprise: Enterprise, EnterprisePaymentMethods: PaymentMethods, EnterpriseShippingMethods: ShippingMethods} + ctrl = $controller 'enterpriseCtrl', {$scope: scope, enterprise: enterprise, EnterprisePaymentMethods: PaymentMethods, EnterpriseShippingMethods: ShippingMethods} describe "initialisation", -> it "stores enterprise", -> - expect(scope.Enterprise).toEqual Enterprise.enterprise + expect(scope.Enterprise).toEqual enterprise it "stores payment methods", -> expect(scope.PaymentMethods).toBe PaymentMethods.paymentMethods @@ -36,28 +35,28 @@ describe "enterpriseCtrl", -> u1 = { id: 1, email: 'name1@email.com' } u2 = { id: 2, email: 'name2@email.com' } u3 = { id: 3, email: 'name3@email.com' } - Enterprise.enterprise.users = [u1, u2 ,u3] + enterprise.users = [u1, u2 ,u3] it "adds a user to the list", -> u4 = { id: 4, email: "name4@email.com" } scope.addManager u4 - expect(Enterprise.enterprise.users).toContain u4 + expect(enterprise.users).toContain u4 it "ignores object without an id", -> u4 = { not_id: 4, email: "name4@email.com" } scope.addManager u4 - expect(Enterprise.enterprise.users).not.toContain u4 + expect(enterprise.users).not.toContain u4 it "it ignores objects without an email", -> u4 = { id: 4, not_email: "name4@email.com" } scope.addManager u4 - expect(Enterprise.enterprise.users).not.toContain u4 + expect(enterprise.users).not.toContain u4 it "ignores objects that are already in the list, and alerts the user", -> spyOn(window, "alert").andCallThrough() u4 = { id: 3, email: "email-doesn't-matter.com" } scope.addManager u4 - expect(Enterprise.enterprise.users).not.toContain u4 + expect(enterprise.users).not.toContain u4 expect(window.alert).toHaveBeenCalledWith "email-doesn't-matter.com is already a manager!" @@ -67,13 +66,13 @@ describe "enterpriseCtrl", -> u1 = { id: 1, email: 'name1@email.com' } u2 = { id: 2, email: 'name2@email.com' } u3 = { id: 3, email: 'name3@email.com' } - Enterprise.enterprise.users = [u1, u2 ,u3] + enterprise.users = [u1, u2 ,u3] it "removes a user with the given id", -> scope.removeManager {id: 2} - expect(Enterprise.enterprise.users).not.toContain u2 + expect(enterprise.users).not.toContain u2 it "does nothing when given object has no id attribute", -> scope.removeManager {not_id: 2} - expect(Enterprise.enterprise.users).toEqual [u1,u2,u3] + expect(enterprise.users).toEqual [u1,u2,u3] diff --git a/spec/javascripts/unit/admin/enterprises/controllers/enterprises_controller_spec.js.coffee b/spec/javascripts/unit/admin/enterprises/controllers/enterprises_controller_spec.js.coffee new file mode 100644 index 0000000000..6e8bdd8040 --- /dev/null +++ b/spec/javascripts/unit/admin/enterprises/controllers/enterprises_controller_spec.js.coffee @@ -0,0 +1,19 @@ +describe "EnterprisesCtrl", -> + ctrl = null + scope = null + Enterprises = null + + beforeEach -> + module('admin.enterprises') + inject ($controller, $rootScope, _Enterprises_) -> + scope = $rootScope + Enterprises = _Enterprises_ + spyOn(Enterprises, "index").andReturn "list of enterprises" + ctrl = $controller 'enterprisesCtrl', {$scope: scope, Enterprises: Enterprises} + + describe "setting the shop on scope", -> + it "calls Enterprises#index with the correct params", -> + expect(Enterprises.index).toHaveBeenCalled() + + it "resets $scope.allEnterprises with the result of Enterprises#index", -> + expect(scope.allEnterprises).toEqual "list of enterprises" diff --git a/spec/javascripts/unit/admin/enterprises/controllers/index_panel_controller_spec.js.coffee b/spec/javascripts/unit/admin/enterprises/controllers/index_panel_controller_spec.js.coffee new file mode 100644 index 0000000000..b74595b775 --- /dev/null +++ b/spec/javascripts/unit/admin/enterprises/controllers/index_panel_controller_spec.js.coffee @@ -0,0 +1,52 @@ +describe "indexPanelCtrl", -> + ctrl = null + scope = null + Enterprises = null + + beforeEach -> + module('admin.enterprises') + inject ($controller, $rootScope, _Enterprises_) -> + scope = $rootScope.$new() + $rootScope.object = { some: "object" } + Enterprises = _Enterprises_ + ctrl = $controller 'indexPanelCtrl', {$scope: scope, Enterprises: Enterprises} + + describe "initialisation", -> + it "pulls object from the parent scope and points the 'enterprise' on the current scope to it", inject ($rootScope) -> + expect(scope.enterprise).toBe $rootScope.object + + describe "saving changes on an enterprise", -> + describe "when changes have been made", -> + deferred = null + + beforeEach inject ($q) -> + spyOn(scope, "saved").andReturn false + spyOn(scope, "$emit") + deferred = $q.defer() + spyOn(Enterprises, "save").andReturn(deferred.promise) + scope.save() + + it "sets scope.saving to true", -> + expect(scope.saving).toBe true + + describe "when the save is successful", -> + beforeEach inject ($rootScope) -> + deferred.resolve() + $rootScope.$digest() + + it "sets scope.saving to false", -> + expect(scope.saving).toBe false + + it "emits an 'enterprise:updated' event", -> + expect(scope.$emit).toHaveBeenCalledWith("enterprise:updated") + + describe "when the save is unsuccessful", -> + beforeEach inject ($rootScope) -> + deferred.reject({ status: 404 }) + $rootScope.$digest() + + it "sets scope.saving to false", -> + expect(scope.saving).toBe false + + it "does not emit an 'enterprise:updated' event", -> + expect(scope.$emit).not.toHaveBeenCalled() diff --git a/spec/javascripts/unit/admin/enterprises/controllers/side_menu_controller_spec.js.coffee b/spec/javascripts/unit/admin/enterprises/controllers/side_menu_controller_spec.js.coffee index ebed830c6b..e88aeb44f2 100644 --- a/spec/javascripts/unit/admin/enterprises/controllers/side_menu_controller_spec.js.coffee +++ b/spec/javascripts/unit/admin/enterprises/controllers/side_menu_controller_spec.js.coffee @@ -1,15 +1,14 @@ describe "menuCtrl", -> ctrl = null scope = null - Enterprise = null + enterprise = null SideMenu = SideMenu beforeEach -> module('admin.enterprises') - Enterprise = - enterprise: - payment_method_ids: [ 1, 3 ] - shipping_method_ids: [ 2, 4 ] + enterprise = + payment_method_ids: [ 1, 3 ] + shipping_method_ids: [ 2, 4 ] # PaymentMethods = # paymentMethods: [ { id: 1 }, { id: 2 }, { id: 3 }, { id: 4 } ] # ShippingMethods = @@ -20,11 +19,11 @@ describe "menuCtrl", -> SideMenu = _SideMenu_ spyOn(SideMenu, "select").andCallThrough() spyOn(SideMenu, "setItems").andCallThrough() - ctrl = $controller 'sideMenuCtrl', {$scope: scope, Enterprise: Enterprise, SideMenu: SideMenu, enterprisePermissions: {}} + ctrl = $controller 'sideMenuCtrl', {$scope: scope, enterprise: enterprise, SideMenu: SideMenu, enterprisePermissions: {}} describe "initialisation", -> it "stores enterprise", -> - expect(scope.Enterprise).toEqual Enterprise.enterprise + expect(scope.Enterprise).toEqual enterprise it "sets the item list", -> expect(SideMenu.setItems).toHaveBeenCalled diff --git a/spec/javascripts/unit/admin/enterprises/services/enterprise_payment_methods_spec.js.coffee b/spec/javascripts/unit/admin/enterprises/services/enterprise_payment_methods_spec.js.coffee index 28052c59f9..0a719b203a 100644 --- a/spec/javascripts/unit/admin/enterprises/services/enterprise_payment_methods_spec.js.coffee +++ b/spec/javascripts/unit/admin/enterprises/services/enterprise_payment_methods_spec.js.coffee @@ -1,19 +1,18 @@ describe "EnterprisePaymentMethods service", -> - Enterprise = null + enterprise = null PaymentMethods = null EnterprisePaymentMethods = null beforeEach -> - Enterprise = - enterprise: - payment_method_ids: [ 1, 3 ] + enterprise = + payment_method_ids: [ 1, 3 ] PaymentMethods = paymentMethods: [ { id: 1 }, { id: 2 }, { id: 3 }, { id: 4 } ] module 'admin.enterprises' module ($provide) -> $provide.value 'PaymentMethods', PaymentMethods - $provide.value 'Enterprise', Enterprise + $provide.value 'enterprise', enterprise null inject (_EnterprisePaymentMethods_) -> diff --git a/spec/javascripts/unit/admin/enterprises/services/enterprise_shipping_methods_spec.js.coffee b/spec/javascripts/unit/admin/enterprises/services/enterprise_shipping_methods_spec.js.coffee index 4cbcf9ab25..4b857023b8 100644 --- a/spec/javascripts/unit/admin/enterprises/services/enterprise_shipping_methods_spec.js.coffee +++ b/spec/javascripts/unit/admin/enterprises/services/enterprise_shipping_methods_spec.js.coffee @@ -1,19 +1,18 @@ describe "EnterpriseShippingMethods service", -> - Enterprise = null + enterprise = null ShippingMethods = null EnterpriseShippingMethods = null beforeEach -> - Enterprise = - enterprise: - shipping_method_ids: [ 1, 3 ] + enterprise = + shipping_method_ids: [ 1, 3 ] ShippingMethods = shippingMethods: [ { id: 1 }, { id: 2 }, { id: 3 }, { id: 4 } ] module 'admin.enterprises' module ($provide) -> $provide.value 'ShippingMethods', ShippingMethods - $provide.value 'Enterprise', Enterprise + $provide.value 'enterprise', enterprise null inject (_EnterpriseShippingMethods_) -> diff --git a/spec/javascripts/unit/admin/enterprises/services/enterprises_spec.js.coffee b/spec/javascripts/unit/admin/enterprises/services/enterprises_spec.js.coffee new file mode 100644 index 0000000000..ba184045d0 --- /dev/null +++ b/spec/javascripts/unit/admin/enterprises/services/enterprises_spec.js.coffee @@ -0,0 +1,105 @@ +describe "Enterprises service", -> + Enterprises = EnterpriseResource = enterprises = $httpBackend = null + + beforeEach -> + module 'admin.enterprises' + + inject ($q, _$httpBackend_, _Enterprises_, _EnterpriseResource_) -> + Enterprises = _Enterprises_ + EnterpriseResource = _EnterpriseResource_ + $httpBackend = _$httpBackend_ + + + describe "#index", -> + result = null + + beforeEach -> + $httpBackend.expectGET('/admin/enterprises.json').respond 200, [{ id: 5, name: 'Enterprise 1'}] + expect(Enterprises.loaded).toBe false + result = Enterprises.index() + $httpBackend.flush() + + it "stores returned data in @enterprises, with ids as keys", -> + # This is super weird and freaking annoying. I think resource results have extra + # properties ($then, $promise) that cause them to not be equal to the reponse object + # provided to the expectGET clause above. + expect(Enterprises.enterprises).toEqual [ new EnterpriseResource({ id: 5, name: 'Enterprise 1'}) ] + + it "returns @enterprises", -> + expect(result).toEqual Enterprises.enterprises + + it "sets @loaded to true", -> + expect(Enterprises.loaded).toBe true + + + describe "#save", -> + result = null + + describe "success", -> + enterprise = null + resolved = false + + beforeEach -> + enterprise = new EnterpriseResource( { id: 15, permalink: 'enterprise1', name: 'Enterprise 1' } ) + $httpBackend.expectPUT('/admin/enterprises/enterprise1.json').respond 200, { id: 15, name: 'Enterprise 1'} + Enterprises.save(enterprise).then( -> resolved = true) + $httpBackend.flush() + + it "updates the pristine copy of the enterprise", -> + # Resource results have extra properties ($then, $promise) that cause them to not + # be exactly equal to the response object provided to the expectPUT clause above. + expect(Enterprises.pristine_by_id[15]).toEqual enterprise + + it "resolves the promise", -> + expect(resolved).toBe(true); + + + describe "failure", -> + enterprise = null + rejected = false + + beforeEach -> + enterprise = new EnterpriseResource( { id: 15, permalink: 'permalink', name: 'Enterprise 1' } ) + $httpBackend.expectPUT('/admin/enterprises/permalink.json').respond 422, { error: 'obj' } + Enterprises.save(enterprise).catch( -> rejected = true) + $httpBackend.flush() + + it "does not update the pristine copy of the enterprise", -> + expect(Enterprises.pristine_by_id[15]).toBeUndefined() + + it "rejects the promise", -> + expect(rejected).toBe(true); + + describe "#saved", -> + describe "when attributes of the object have been altered", -> + beforeEach -> + spyOn(Enterprises, "diff").andReturn ["attr1", "attr2"] + + it "returns false", -> + expect(Enterprises.saved({})).toBe false + + describe "when attributes of the object have not been altered", -> + beforeEach -> + spyOn(Enterprises, "diff").andReturn [] + + it "returns false", -> + expect(Enterprises.saved({})).toBe true + + + describe "diff", -> + beforeEach -> + Enterprises.pristine_by_id = { 23: { id: 23, name: "ent1", is_primary_producer: true } } + + it "returns a list of properties that have been altered", -> + expect(Enterprises.diff({ id: 23, name: "enterprise123", is_primary_producer: true })).toEqual ["name"] + + + describe "resetAttribute", -> + enterprise = { id: 23, name: "ent1", is_primary_producer: true } + + beforeEach -> + Enterprises.pristine_by_id = { 23: { id: 23, name: "enterprise1", is_primary_producer: true } } + + it "resets the specified value according to the pristine record", -> + Enterprises.resetAttribute(enterprise, "name") + expect(enterprise.name).toEqual "enterprise1" diff --git a/spec/javascripts/unit/admin/enterprises/services/permalink_checker_spec.js.coffee b/spec/javascripts/unit/admin/enterprises/services/permalink_checker_spec.js.coffee index 6eef847858..3814d74cbc 100644 --- a/spec/javascripts/unit/admin/enterprises/services/permalink_checker_spec.js.coffee +++ b/spec/javascripts/unit/admin/enterprises/services/permalink_checker_spec.js.coffee @@ -1,6 +1,10 @@ describe "Permalink Checker service", -> PermalinkChecker = null $httpBackend = null + permalink = "this-is-a-permalink" + permalink_too_long = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + permalink_invalid_chars = "" + beforeEach -> module 'admin.enterprises' @@ -8,7 +12,31 @@ describe "Permalink Checker service", -> $httpBackend = _$httpBackend_ PermalinkChecker = $injector.get("PermalinkChecker") - it "sends an http request to check the permalink", -> - permalink = "this-is-a-permalink" - $httpBackend.expectGET "/enterprises/check_permalink?permalink=#{permalink}" - PermalinkChecker.check(permalink) \ No newline at end of file + it "responds to available permalinks", -> + $httpBackend.expectGET("/enterprises/check_permalink?permalink=#{permalink}").respond permalink + PermalinkChecker.check(permalink).then (data) -> + expect(data.permalink).toEqual permalink + expect(data.available).toEqual "Available" + $httpBackend.flush() + + it "responds to unavailable permalinks", -> + $httpBackend.expectGET("/enterprises/check_permalink?permalink=#{permalink}").respond 409, permalink + PermalinkChecker.check(permalink).then (data) -> + expect(data.permalink).toEqual permalink + expect(data.available).toEqual "Unavailable" + $httpBackend.flush() + + describe "invalid data", -> + it "errors for permalinks that are too long", -> + $httpBackend.expectGET("/enterprises/check_permalink?permalink=#{permalink}").respond permalink_too_long + PermalinkChecker.check(permalink).then (data) -> + expect(data.permalink).toEqual permalink + expect(data.available).toEqual "Error" + $httpBackend.flush() + + it "errors for permalinks that contain invalid characters", -> + $httpBackend.expectGET("/enterprises/check_permalink?permalink=#{permalink}").respond permalink_invalid_chars + PermalinkChecker.check(permalink).then (data) -> + expect(data.permalink).toEqual permalink + expect(data.available).toEqual "Error" + $httpBackend.flush() diff --git a/spec/javascripts/unit/admin/index_utils/directives/panel_row_spec.js.coffee b/spec/javascripts/unit/admin/index_utils/directives/panel_row_spec.js.coffee new file mode 100644 index 0000000000..2161afe7de --- /dev/null +++ b/spec/javascripts/unit/admin/index_utils/directives/panel_row_spec.js.coffee @@ -0,0 +1,33 @@ +describe "PanelRow directive", -> + Panels = null + element = null + directiveScope = null + + beforeEach -> + module 'admin.indexUtils' + + beforeEach inject ($rootScope, $compile, $injector, $templateCache, _Panels_) -> + Panels = _Panels_ + $templateCache.put 'admin/panel.html', '{{ template }}' + # Declare the directive HTML. + element = angular.element('
') + # Define the root scope. + scope = $rootScope + # Compile and digest the directive. + $compile(element) scope + scope.$digest() + + directiveScope = element.find('span').scope() + return + + describe "initialisation", -> + it "registers the scope with the panels service", -> + expect(Panels.panels[12]).toEqual directiveScope + + describe "setting the selected panel", -> + beforeEach -> + directiveScope.setSelected('panel1') + + it 'updates the active template on the scope', -> + expect(element.find('span').html()).toEqual "admin/panels/template.html" + return diff --git a/spec/javascripts/unit/admin/index_utils/services/columns_spec.js.coffee b/spec/javascripts/unit/admin/index_utils/services/columns_spec.js.coffee index 0b6093f31d..2bff5e5a73 100644 --- a/spec/javascripts/unit/admin/index_utils/services/columns_spec.js.coffee +++ b/spec/javascripts/unit/admin/index_utils/services/columns_spec.js.coffee @@ -7,9 +7,34 @@ describe "Columns service", -> inject (_Columns_) -> Columns = _Columns_ - Columns.columns = ["something"] - describe "setting columns", -> it "sets resets @columns and copies each column of the provided object across", -> Columns.setColumns({ name: { visible: true } }) expect(Columns.columns).toEqual { name: { visible: true } } + + it "calls calculateVisibleCount", -> + spyOn(Columns, "calculateVisibleCount") + Columns.setColumns({ name: { visible: true } }) + expect(Columns.calculateVisibleCount).toHaveBeenCalled() + + describe "toggling a column", -> + it "switches the visibility of the given column", -> + column = { visible: false } + Columns.toggleColumn(column) + expect(column.visible).toBe true + + it "calls calculateVisibleCount", -> + spyOn(Columns, "calculateVisibleCount") + Columns.toggleColumn({ visible: false }) + expect(Columns.calculateVisibleCount).toHaveBeenCalled() + + describe "calculating visibleCount", -> + it "counts the number of columns ", -> + Columns.columns = { col1: { visible: false }, col2: { visible: true }, col3: { visible: true }, col4: { visible: false } } + Columns.calculateVisibleCount() + expect(Columns.visibleCount).toBe 2 + + it "$broadcasts the updated visible count to $rootScope", inject ($rootScope) -> + spyOn($rootScope, "$broadcast") + Columns.calculateVisibleCount() + expect($rootScope.$broadcast).toHaveBeenCalled() diff --git a/spec/javascripts/unit/admin/index_utils/services/panels_spec.js.coffee b/spec/javascripts/unit/admin/index_utils/services/panels_spec.js.coffee new file mode 100644 index 0000000000..a55d9ffa61 --- /dev/null +++ b/spec/javascripts/unit/admin/index_utils/services/panels_spec.js.coffee @@ -0,0 +1,52 @@ +describe "Panels service", -> + Panels = null + + beforeEach -> + module 'admin.indexUtils' + + inject (_Panels_) -> + Panels = _Panels_ + + describe "registering panels", -> + it "adds the panel provided scope to @panelsm indexed by the provided id", -> + Panels.register(23, { some: 'scope'} ) + expect(Panels.panels[23]).toEqual { some: 'scope' } + + it "ignores the input if id or scope are null", -> + Panels.register(null, { some: 'scope'} ) + Panels.register(23, null) + expect(Panels.panels).toEqual { } + + describe "toggling a panel", -> + scopeMock = null + + beforeEach -> + scopeMock = + open: jasmine.createSpy('open') + close: jasmine.createSpy('close') + setSelected: jasmine.createSpy('setSelected') + Panels.panels = { '12': scopeMock } + + describe "when no panel is currently selected", -> + beforeEach -> + scopeMock.getSelected = jasmine.createSpy('getSelected').andReturn(null) + Panels.toggle(12, 'panel_name') + + it "calls #open on the scope", -> + expect(scopeMock.open).toHaveBeenCalledWith('panel_name') + + describe "when #toggle is called for the currently selected panel", -> + beforeEach -> + scopeMock.getSelected = jasmine.createSpy('getSelected').andReturn('panel_name') + Panels.toggle(12, 'panel_name') + + it "calls #close on the scope", -> + expect(scopeMock.close).toHaveBeenCalled() + + describe "when #toggle is called for a different panel", -> + beforeEach -> + scopeMock.getSelected = jasmine.createSpy('getSelected').andReturn('some_other_panel_name') + Panels.toggle(12, 'panel_name') + + it "calls #setSelected on the scope", -> + expect(scopeMock.setSelected).toHaveBeenCalledWith('panel_name') diff --git a/spec/javascripts/unit/admin/services/bulk_products_spec.js.coffee b/spec/javascripts/unit/admin/services/bulk_products_spec.js.coffee index 54b57aed48..7d4c5d1d7c 100644 --- a/spec/javascripts/unit/admin/services/bulk_products_spec.js.coffee +++ b/spec/javascripts/unit/admin/services/bulk_products_spec.js.coffee @@ -62,12 +62,15 @@ describe "BulkProducts service", -> id: 17 spyOn(BulkProducts, "insertProductAfter") + spyOn(BulkProducts, "unpackProduct") BulkProducts.products = [originalProduct] $httpBackend.expectGET("/admin/products/oranges/clone.json").respond 200, product: clonedProduct $httpBackend.expectGET("/api/products/17?template=bulk_show").respond 200, clonedProduct BulkProducts.cloneProduct BulkProducts.products[0] $httpBackend.flush() + expect(BulkProducts.unpackProduct).toHaveBeenCalledWith clonedProduct + BulkProducts.unpackProduct(clonedProduct) expect(BulkProducts.insertProductAfter).toHaveBeenCalledWith originalProduct, clonedProduct diff --git a/spec/javascripts/unit/admin/services/enterprise_relationships_spec.js.coffee b/spec/javascripts/unit/admin/services/enterprise_relationships_spec.js.coffee index 0d8e50fc5a..0d0a01215d 100644 --- a/spec/javascripts/unit/admin/services/enterprise_relationships_spec.js.coffee +++ b/spec/javascripts/unit/admin/services/enterprise_relationships_spec.js.coffee @@ -12,7 +12,7 @@ describe "enterprise relationships", -> EnterpriseRelationships = _EnterpriseRelationships_ it "presents permission names", -> - expect(EnterpriseRelationships.permission_presentation("add_to_order_cycle")).toEqual "to add to order cycle" - expect(EnterpriseRelationships.permission_presentation("manage_products")).toEqual "to manage products" - expect(EnterpriseRelationships.permission_presentation("edit_profile")).toEqual "to edit profile" - expect(EnterpriseRelationships.permission_presentation("create_variant_overrides")).toEqual "to override variant details" + expect(EnterpriseRelationships.permission_presentation("add_to_order_cycle")).toEqual "add to order cycle" + expect(EnterpriseRelationships.permission_presentation("manage_products")).toEqual "manage products" + expect(EnterpriseRelationships.permission_presentation("edit_profile")).toEqual "edit profile" + expect(EnterpriseRelationships.permission_presentation("create_variant_overrides")).toEqual "override variant details" diff --git a/spec/javascripts/unit/bulk_order_management_spec.js.coffee b/spec/javascripts/unit/bulk_order_management_spec.js.coffee index 7c1fe5da23..4fb3072f8e 100644 --- a/spec/javascripts/unit/bulk_order_management_spec.js.coffee +++ b/spec/javascripts/unit/bulk_order_management_spec.js.coffee @@ -354,25 +354,44 @@ describe "AdminOrderMgmtCtrl", -> it "resets the weight if the weight is set to zero", -> scope.filteredLineItems = [ - { units_variant: { unit_value: 100 }, price: 2, unit_value: 0 } + { units_variant: { unit_value: 100 }, price: 2, quantity: 1, final_weight_volume: 0 } ] expect(scope.weightAdjustedPrice(scope.filteredLineItems[0], 100)).toEqual scope.filteredLineItems[0].price it "updates the price if the weight is changed", -> scope.filteredLineItems = [ - { units_variant: { unit_value: 100 }, price: 2, unit_value: 200 } + { units_variant: { unit_value: 100 }, price: 2, final_weight_volume: 200 } ] old_value = scope.filteredLineItems[0].units_variant.unit_value - new_value = scope.filteredLineItems[0].unit_value + new_value = scope.filteredLineItems[0].final_weight_volume sp = scope.filteredLineItems[0].price * new_value / old_value expect(scope.weightAdjustedPrice(scope.filteredLineItems[0], old_value)).toEqual sp + it "updates the weight if the quantity is changed", -> + scope.filteredLineItems = [ + { units_variant: { unit_value: 150 }, price: 1, final_weight_volume: 100, quantity: 2 } + ] + old_value = 1 + nw = scope.filteredLineItems[0].units_variant.unit_value * scope.filteredLineItems[0].quantity + scope.updateOnQuantity(scope.filteredLineItems[0], old_value) + expect(scope.filteredLineItems[0].final_weight_volume).toEqual nw + + it "updates the price if the quantity is changed", -> + scope.filteredLineItems = [ + { units_variant: { unit_value: 150 }, price: 21, final_weight_volume: 100, quantity: 2 } + ] + old_value = 1 + np = scope.filteredLineItems[0].price * (old_value * scope.filteredLineItems[0].units_variant.unit_value) / scope.filteredLineItems[0].final_weight_volume + scope.updateOnQuantity(scope.filteredLineItems[0], old_value) + expect(scope.filteredLineItems[0].price).toEqual np + + it "doesn't update the price if the weight is not changed", -> scope.filteredLineItems = [ - { units_variant: { unit_value: 100 }, price: 2, unit_value: 100 } + { units_variant: { unit_value: 100 }, price: 2, final_weight_volume: 100 } ] - old_value = scope.filteredLineItems[0].unit_value - new_value = scope.filteredLineItems[0].unit_value + old_value = scope.filteredLineItems[0].final_weight_volume + new_value = scope.filteredLineItems[0].final_weight_volume sp = scope.filteredLineItems[0].price expect(scope.weightAdjustedPrice(scope.filteredLineItems[0], old_value)).toEqual sp @@ -394,14 +413,14 @@ describe "Auxiliary functions", -> beforeEach -> date = new Date date.setYear(2010) - date.setMonth(5) # Zero indexed, so 5 is June + date.setMonth(4) # Zero indexed, so 4 is May date.setDate(15) date.setHours(5) date.setMinutes(10) date.setSeconds(30) it "returns a date formatted as yyyy-mm-dd", -> - expect(formatDate(date)).toEqual "2010-06-15" + expect(formatDate(date)).toEqual "2010-05-15" it "returns a time formatted as hh-MM:ss", -> expect(formatTime(date)).toEqual "05:10:30" diff --git a/spec/javascripts/unit/darkswarm/controllers/home_controller_spec.js.coffee b/spec/javascripts/unit/darkswarm/controllers/home_controller_spec.js.coffee new file mode 100644 index 0000000000..8ba07e2f77 --- /dev/null +++ b/spec/javascripts/unit/darkswarm/controllers/home_controller_spec.js.coffee @@ -0,0 +1,19 @@ +describe "HomeCtrl", -> + ctrl = null + scope = null + + beforeEach -> + module 'Darkswarm' + scope = {} + + inject ($controller) -> + ctrl = $controller 'HomeCtrl', {$scope: scope} + + it "starts with the brand story contracted", -> + expect(scope.brandStoryExpanded).toBe false + + it "toggles the brand story", -> + scope.toggleBrandStory() + expect(scope.brandStoryExpanded).toBe true + scope.toggleBrandStory() + expect(scope.brandStoryExpanded).toBe false diff --git a/spec/javascripts/unit/darkswarm/controllers/order_cycle_controller_spec.js.coffee b/spec/javascripts/unit/darkswarm/controllers/order_cycle_controller_spec.js.coffee index a9746f2d39..deb7aea8b1 100644 --- a/spec/javascripts/unit/darkswarm/controllers/order_cycle_controller_spec.js.coffee +++ b/spec/javascripts/unit/darkswarm/controllers/order_cycle_controller_spec.js.coffee @@ -1,19 +1,17 @@ describe 'OrderCycleCtrl', -> ctrl = null scope = null - event = null - product_ctrl = null OrderCycle = null beforeEach -> module 'Darkswarm' scope = {} - OrderCycle = - order_cycle: "test" + OrderCycle = + order_cycle: + id: 123 inject ($controller) -> scope = {} ctrl = $controller 'OrderCycleCtrl', {$scope: scope, OrderCycle: OrderCycle} it "puts the order cycle in scope", -> - expect(scope.order_cycle).toEqual "test" - + expect(scope.order_cycle).toEqual {id: 123} diff --git a/spec/javascripts/unit/darkswarm/filters/active_spec.js.coffee b/spec/javascripts/unit/darkswarm/filters/active_spec.js.coffee index 337121b1ae..25c6c1d003 100644 --- a/spec/javascripts/unit/darkswarm/filters/active_spec.js.coffee +++ b/spec/javascripts/unit/darkswarm/filters/active_spec.js.coffee @@ -1,12 +1,8 @@ describe 'filtering by active', -> filterByActive = null objects = [ - { - active: true - } - { - active: false - } + {active: true} + {active: false} ] diff --git a/spec/javascripts/unit/darkswarm/filters/distance_within_km_spec.js.coffee b/spec/javascripts/unit/darkswarm/filters/distance_within_km_spec.js.coffee new file mode 100644 index 0000000000..afbc31ee84 --- /dev/null +++ b/spec/javascripts/unit/darkswarm/filters/distance_within_km_spec.js.coffee @@ -0,0 +1,17 @@ +describe "filtering enterprises to those within a certain radius", -> + filter = null + enterprises = [ + {distance: 25000} + {distance: 75000} + ] + + beforeEach -> + module 'Darkswarm' + inject ($filter) -> + filter = $filter('distanceWithinKm') + + it "filters to those enterprises within a distance", -> + expect(filter(enterprises, 50)).toEqual [enterprises[0]] + + it "returns empty array when enterprises array is null", -> + expect(filter(null, 50)).toEqual [] diff --git a/spec/javascripts/unit/darkswarm/services/cart_spec.js.coffee b/spec/javascripts/unit/darkswarm/services/cart_spec.js.coffee index 4be1a13dd8..0519b59763 100644 --- a/spec/javascripts/unit/darkswarm/services/cart_spec.js.coffee +++ b/spec/javascripts/unit/darkswarm/services/cart_spec.js.coffee @@ -3,6 +3,8 @@ describe 'Cart service', -> Variants = null variant = null order = null + $httpBackend = null + $timeout = null beforeEach -> module 'Darkswarm' @@ -16,9 +18,11 @@ describe 'Cart service', -> ] } angular.module('Darkswarm').value('currentOrder', order) - inject ($injector)-> + inject ($injector, _$httpBackend_, _$timeout_)-> Variants = $injector.get("Variants") Cart = $injector.get("Cart") + $httpBackend = _$httpBackend_ + $timeout = _$timeout_ it "backreferences line items", -> expect(Cart.line_items[0].variant.line_item).toBe Cart.line_items[0] @@ -44,6 +48,34 @@ describe 'Cart service', -> order.line_items[0].quantity = 2 expect(Cart.total_item_count()).toEqual 2 + describe "updating the cart", -> + data = {variants: {}} + + it "marks the form as saved on success", -> + spyOn(Cart, 'saved') + $httpBackend.expectPOST("/orders/populate", data).respond 200, {} + Cart.update() + $httpBackend.flush() + expect(Cart.saved).toHaveBeenCalled() + + it "retries the update on failure", -> + spyOn(Cart, 'scheduleRetry') + $httpBackend.expectPOST("/orders/populate", data).respond 404, {} + Cart.update() + $httpBackend.flush() + expect(Cart.scheduleRetry).toHaveBeenCalled() + + it "schedules retries of updates", -> + spyOn(Cart, 'orderChanged') + Cart.scheduleRetry() + $timeout.flush() + expect(Cart.orderChanged).toHaveBeenCalled() + + it "clears the cart", -> + expect(Cart.line_items).not.toEqual [] + Cart.clear() + expect(Cart.line_items).toEqual [] + describe "generating an extended variant name", -> it "returns the product name when it is the same as the variant name", -> variant = {product_name: 'product_name', name_to_display: 'product_name'} diff --git a/spec/javascripts/unit/darkswarm/services/enterprise_registration_spec.js.coffee b/spec/javascripts/unit/darkswarm/services/enterprise_registration_spec.js.coffee index 1852dbd16c..4952c3c4fc 100644 --- a/spec/javascripts/unit/darkswarm/services/enterprise_registration_spec.js.coffee +++ b/spec/javascripts/unit/darkswarm/services/enterprise_registration_spec.js.coffee @@ -56,6 +56,20 @@ describe "EnterpriseRegistrationService", -> it "does not move the user to the about page", -> expect(RegistrationServiceMock.select).not.toHaveBeenCalled + describe "failure due to duplicate name", -> + beforeEach -> + spyOn(RegistrationServiceMock, "select") + spyOn(window, "alert") + $httpBackend.expectPOST("/api/enterprises?token=keykeykeykey").respond 400, {"error": "Invalid resource. Please fix errors and try again.", "errors": {"name": ["has already been taken. If this is your enterprise and you would like to claim ownership, please contact the current manager of this profile at owner@example.com."], "permalink": [] }} + EnterpriseRegistrationService.create() + $httpBackend.flush() + + it "alerts the user to failure", -> + expect(window.alert).toHaveBeenCalledWith 'Failed to create your enterprise.\nName has already been taken. If this is your enterprise and you would like to claim ownership, please contact the current manager of this profile at owner@example.com.' + + it "does not move the user to the about page", -> + expect(RegistrationServiceMock.select).not.toHaveBeenCalled + describe "updating an enterprise", -> beforeEach -> diff --git a/spec/javascripts/unit/darkswarm/services/enterprise_spec.js.coffee b/spec/javascripts/unit/darkswarm/services/enterprise_spec.js.coffee index 94dd7d39d2..67774aecbd 100644 --- a/spec/javascripts/unit/darkswarm/services/enterprise_spec.js.coffee +++ b/spec/javascripts/unit/darkswarm/services/enterprise_spec.js.coffee @@ -1,24 +1,37 @@ describe "Enterprises service", -> Enterprises = null CurrentHubMock = {} + Geo = + OK: 'ok' + succeed: true + geocode: (query, callback) -> + if @succeed + results = [{geometry: {location: "location"}}] + callback(results, @OK) + else + callback(results, 'Oops') + distanceBetween: (locatable, location) -> + 123 + taxons = [ {id: 1, name: "test"} ] enterprises = [ - {id: 1, visible: true, category: "hub", producers: [{id: 5}], taxons: [{id: 1}]}, - {id: 2, visible: true, category: "hub", producers: [{id: 6}]} - {id: 3, visible: true, category: "hub_profile"} - {id: 4, visible: false, category: "hub", producers: [{id: 7}]} - {id: 5, visible: true, category: "producer_hub", hubs: [{id: 1}]}, - {id: 6, visible: true, category: "producer_shop", hubs: [{id: 2}]}, - {id: 7, visible: true, category: "producer", hubs: [{id: 2}]} - {id: 8, visible: false, category: "producer", hubs: [{id: 2}]} + {id: 1, visible: true, name: 'a', category: "hub", producers: [{id: 5}], taxons: [{id: 1}]}, + {id: 2, visible: true, name: 'b', category: "hub", producers: [{id: 6}]} + {id: 3, visible: true, name: 'c', category: "hub_profile"} + {id: 4, visible: false,name: 'd', category: "hub", producers: [{id: 7}]} + {id: 5, visible: true, name: 'e', category: "producer_hub", hubs: [{id: 1}]}, + {id: 6, visible: true, name: 'f', category: "producer_shop", hubs: [{id: 2}]}, + {id: 7, visible: true, name: 'g', category: "producer", hubs: [{id: 2}]} + {id: 8, visible: false,name: 'h', category: "producer", hubs: [{id: 2}]} ] H1: 0 beforeEach -> module 'Darkswarm' module ($provide)-> $provide.value "CurrentHub", CurrentHubMock + $provide.value "Geo", Geo null angular.module('Darkswarm').value('enterprises', enterprises) angular.module('Darkswarm').value('taxons', taxons) @@ -73,3 +86,70 @@ describe "Enterprises service", -> expect(Enterprises.producers).toContain Enterprises.enterprises[4] expect(Enterprises.producers).toContain Enterprises.enterprises[5] expect(Enterprises.producers).toContain Enterprises.enterprises[6] + + describe "flagging enterprises with names matching a query", -> + it "flags enterprises when a query is provided", -> + Enterprises.flagMatching 'c' + expect(e.matches_name_query).toBe true for e in enterprises when e.name == 'c' + expect(e.matches_name_query).toBe false for e in enterprises when e.name != 'c' + + it "clears flags when query is null", -> + Enterprises.flagMatching null + expect(e.matches_name_query).toBe false for e in enterprises + + it "clears flags when query is blank", -> + Enterprises.flagMatching '' + expect(e.matches_name_query).toBe false for e in enterprises + + describe "calculating the distance of enterprises from a location", -> + describe "when a query is provided", -> + it "sets the distance from the enterprise when a name match is available", -> + spyOn(Enterprises, "setDistanceFrom") + Enterprises.calculateDistance "asdf", 'match' + expect(Enterprises.setDistanceFrom).toHaveBeenCalledWith('match') + + it "calculates the distance from the geocoded query otherwise", -> + spyOn(Enterprises, "calculateDistanceGeo") + Enterprises.calculateDistance "asdf", undefined + expect(Enterprises.calculateDistanceGeo).toHaveBeenCalledWith("asdf") + + it "resets the distance when query is null", -> + spyOn(Enterprises, "resetDistance") + Enterprises.calculateDistance null + expect(Enterprises.resetDistance).toHaveBeenCalled() + + it "resets the distance when query is blank", -> + spyOn(Enterprises, "resetDistance") + Enterprises.calculateDistance "" + expect(Enterprises.resetDistance).toHaveBeenCalled() + + describe "calculating the distance of enterprises from a location by geocoding", -> + beforeEach -> + spyOn(Enterprises, "setDistanceFrom") + + it "calculates distance for all enterprises when geocoding succeeds", -> + Geo.succeed = true + Enterprises.calculateDistanceGeo('query') + expect(Enterprises.setDistanceFrom).toHaveBeenCalledWith("location") + + it "resets distance when geocoding fails", -> + Geo.succeed = false + spyOn(Enterprises, "resetDistance") + Enterprises.calculateDistanceGeo('query') + expect(Enterprises.setDistanceFrom).not.toHaveBeenCalled() + expect(Enterprises.resetDistance).toHaveBeenCalled() + + describe "setting the distance of each enterprise from a central location", -> + it "sets the distances", -> + Enterprises.setDistanceFrom 'location' + for e in Enterprises.enterprises + expect(e.distance).toEqual 123 + + describe "resetting the distance measurement of all enterprises", -> + beforeEach -> + e.distance = 123 for e in Enterprises.enterprises + + it "resets the distance", -> + Enterprises.resetDistance() + for e in Enterprises.enterprises + expect(e.distance).toBeNull() \ No newline at end of file diff --git a/spec/javascripts/unit/darkswarm/services/groups_spec.js.coffee b/spec/javascripts/unit/darkswarm/services/groups_spec.js.coffee index abd9c5c617..50e2159338 100644 --- a/spec/javascripts/unit/darkswarm/services/groups_spec.js.coffee +++ b/spec/javascripts/unit/darkswarm/services/groups_spec.js.coffee @@ -1,7 +1,8 @@ describe "Groups service", -> Groups = null Enterprises = null - CurrentHubMock = {} + CurrentHubMock = {} + Geo = {} groups = [{ id: 1 name: "Test Group" @@ -17,17 +18,18 @@ describe "Groups service", -> beforeEach -> module 'Darkswarm' - angular.module('Darkswarm').value('groups', groups) - angular.module('Darkswarm').value('enterprises', enterprises) + angular.module('Darkswarm').value('groups', groups) + angular.module('Darkswarm').value('enterprises', enterprises) module ($provide)-> - $provide.value "CurrentHub", CurrentHubMock + $provide.value "CurrentHub", CurrentHubMock + $provide.value "Geo", Geo null inject (_Groups_, _Enterprises_)-> - Groups = _Groups_ - Enterprises = _Enterprises_ + Groups = _Groups_ + Enterprises = _Enterprises_ it "dereferences group enterprises", -> expect(Groups.groups[0].enterprises[0]).toBe enterprises[0] - + it "dereferences enterprise groups", -> expect(Enterprises.enterprises[0].groups[0]).toBe groups[0] diff --git a/spec/javascripts/unit/darkswarm/services/map_spec.js.coffee b/spec/javascripts/unit/darkswarm/services/map_spec.js.coffee index 3ef21705d3..4252000460 100644 --- a/spec/javascripts/unit/darkswarm/services/map_spec.js.coffee +++ b/spec/javascripts/unit/darkswarm/services/map_spec.js.coffee @@ -1,6 +1,7 @@ describe "Hubs service", -> OfnMap = null - CurrentHubMock = {} + CurrentHubMock = {} + Geo = {} enterprises = [ { id: 2 @@ -13,12 +14,13 @@ describe "Hubs service", -> beforeEach -> module 'Darkswarm' - angular.module('Darkswarm').value('enterprises', enterprises) + angular.module('Darkswarm').value('enterprises', enterprises) module ($provide)-> - $provide.value "CurrentHub", CurrentHubMock + $provide.value "CurrentHub", CurrentHubMock + $provide.value "Geo", Geo null inject ($injector)-> - OfnMap = $injector.get("OfnMap") + OfnMap = $injector.get("OfnMap") it "builds MapMarkers from enterprises", -> expect(OfnMap.enterprises[0].id).toBe enterprises[0].id diff --git a/spec/javascripts/unit/darkswarm/services/products_spec.js.coffee b/spec/javascripts/unit/darkswarm/services/products_spec.js.coffee index 590156260d..778e701c7d 100644 --- a/spec/javascripts/unit/darkswarm/services/products_spec.js.coffee +++ b/spec/javascripts/unit/darkswarm/services/products_spec.js.coffee @@ -10,6 +10,7 @@ describe 'Products service', -> productWithImage = null properties = null taxons = null + Geo = {} beforeEach -> product = @@ -40,6 +41,7 @@ describe 'Products service', -> $provide.value "currentOrder", currentOrder $provide.value "taxons", taxons $provide.value "properties", properties + $provide.value "Geo", Geo null inject ($injector, _$httpBackend_)-> diff --git a/spec/javascripts/unit/darkswarm/services/variants_spec.js.coffee b/spec/javascripts/unit/darkswarm/services/variants_spec.js.coffee index 5c6e138e0d..a235bafaee 100644 --- a/spec/javascripts/unit/darkswarm/services/variants_spec.js.coffee +++ b/spec/javascripts/unit/darkswarm/services/variants_spec.js.coffee @@ -23,3 +23,9 @@ describe 'Variants service', -> it "initialises base price percentage", -> expect(Variants.register(variant).basePricePercentage).toEqual 81 + + it "clears registered variants", -> + Variants.register(variant) + expect(Variants.variants[variant.id]).toBe variant + Variants.clear() + expect(Variants.variants[variant.id]).toBeUndefined() \ No newline at end of file diff --git a/spec/jobs/finalize_account_invoices_spec.rb b/spec/jobs/finalize_account_invoices_spec.rb new file mode 100644 index 0000000000..8ba3c66f24 --- /dev/null +++ b/spec/jobs/finalize_account_invoices_spec.rb @@ -0,0 +1,209 @@ +require 'spec_helper' + +def travel_to(time) + around { |example| Timecop.travel(start_of_july + time) { example.run } } +end + + +describe FinalizeAccountInvoices do + describe "unit specs" do + let!(:finalizer) { FinalizeAccountInvoices.new } + let!(:start_of_july) { Time.now.beginning_of_year + 6.months } + let!(:year) { Time.now.year } + + describe "perform" do + let!(:accounts_distributor) { create(:distributor_enterprise) } + + #Invoice from June + let!(:account_invoice1) { create(:account_invoice, year: year, month: 6, order: create(:order, completed_at: nil))} + + # We don't care when it was completed, in the future or past + let!(:account_invoice2) { create(:account_invoice, year: year, month: 6, order: create(:order, completed_at: start_of_july - 10.days))} + let!(:account_invoice3) { create(:account_invoice, year: year, month: 6, order: create(:order, completed_at: start_of_july + 10.days))} + + # Invoices from July + let!(:account_invoice4) { create(:account_invoice, year: year, month: 7, order: create(:order, completed_at: nil))} + let!(:account_invoice5) { create(:account_invoice, year: year, month: 7, order: create(:order, completed_at: start_of_july + 10.days))} + + before do + allow(Enterprise).to receive(:find_by_id) { accounts_distributor } + allow(accounts_distributor).to receive(:payment_methods) { double(:payment_methods, find_by_id: true) } + allow(accounts_distributor).to receive(:shipping_methods) { double(:shipping_methods, find_by_id: true) } + allow(finalizer).to receive(:finalize) + allow(Bugsnag).to receive(:notify) + end + + context "when necessary global config setting have not been set" do + travel_to(20.days) + + context "when accounts_distributor has been set" do + before do + allow(Enterprise).to receive(:find_by_id) { false } + finalizer.perform + end + + it "snags errors and doesn't run" do + expect(Bugsnag).to have_received(:notify).with(RuntimeError.new("InvalidJobSettings"), anything) + expect(finalizer).to_not have_received(:finalize) + end + end + + context "when default payment method has been set" do + before do + allow(accounts_distributor).to receive(:payment_methods) { double(:payment_methods, find_by_id: false) } + finalizer.perform + end + + it "snags errors and doesn't run" do + expect(Bugsnag).to have_received(:notify).with(RuntimeError.new("InvalidJobSettings"), anything) + expect(finalizer).to_not have_received(:finalize) + end + end + + context "when default shipping method has been set" do + before do + allow(accounts_distributor).to receive(:shipping_methods) { double(:shipping_methods, find_by_id: false) } + finalizer.perform + end + + it "snags errors and doesn't run" do + expect(Bugsnag).to have_received(:notify).with(RuntimeError.new("InvalidJobSettings"), anything) + expect(finalizer).to_not have_received(:finalize) + end + end + end + + context "when necessary global config setting have been set" do + context "and no date arguments are passed to the job" do + travel_to(3.days) + + it "finalizes the uncompleted orders from account_invoices for the previous calendar month" do + finalizer.perform + expect(finalizer).to have_received(:finalize).with(account_invoice1.order) + expect(finalizer).to_not have_received(:finalize).with(account_invoice2.order) + expect(finalizer).to_not have_received(:finalize).with(account_invoice3.order) + expect(finalizer).to_not have_received(:finalize).with(account_invoice4.order) + expect(finalizer).to_not have_received(:finalize).with(account_invoice5.order) + end + end + + context "an a specific year and month are passed as arguments" do + let!(:finalizer) { FinalizeAccountInvoices.new(Time.now.year, 7) } + + before do + allow(finalizer).to receive(:finalizer) + end + + context "that ends in the past" do + travel_to(1.month + 3.hours) + + it "finalizes the uncompleted orders from account_invoices for the specified calendar month" do + finalizer.perform + expect(finalizer).to_not have_received(:finalize).with(account_invoice1.order) + expect(finalizer).to_not have_received(:finalize).with(account_invoice2.order) + expect(finalizer).to_not have_received(:finalize).with(account_invoice3.order) + expect(finalizer).to have_received(:finalize).with(account_invoice4.order) + expect(finalizer).to_not have_received(:finalize).with(account_invoice5.order) + end + end + + context "that ends in the future" do + travel_to 3.days + + it "does not finalize any orders" do + finalizer.perform + expect(Bugsnag).to have_received(:notify).with(RuntimeError.new("InvalidJobSettings"), anything) + expect(finalizer).to_not have_received(:finalize) + end + end + end + end + end + + describe "finalize" do + let!(:pm) { create(:payment_method, name: "PM1") } + let!(:sm) { create(:shipping_method, name: "ship1") } + let!(:accounts_distributor) { create(:distributor_enterprise, payment_methods: [pm], shipping_methods: [sm]) } + let!(:invoice_order) { create(:order, distributor: accounts_distributor) } + + before do + Spree::Config.set({ accounts_distributor_id: accounts_distributor.id }) + Spree::Config.set({ default_accounts_payment_method_id: pm.id }) + Spree::Config.set({ default_accounts_shipping_method_id: sm.id }) + invoice_order.line_items.clear + end + + it "creates payment, assigns shipping method and finalizes the order" do + expect(invoice_order.completed_at).to be nil + finalizer.finalize(invoice_order) + expect(invoice_order.completed_at).to_not be nil + expect(invoice_order.payments.count).to eq 1 + expect(invoice_order.payments.first.payment_method).to eq pm + expect(invoice_order.shipping_method).to eq sm + end + + it "does not send a confirmation email" do + expect(invoice_order).to receive(:deliver_order_confirmation_email).and_call_original + expect{finalizer.finalize(invoice_order)}.to_not enqueue_job ConfirmOrderJob + end + + context "when errors exist on the order" do + before do + allow(invoice_order).to receive(:errors) { double(:errors, any?: true, full_messages: ["Error message 1", "Error message 2"]) } + allow(Bugsnag).to receive(:notify) + end + + it "Snags a bug and does not finalize the order" do + finalizer.finalize(invoice_order) + expect(Bugsnag).to have_received(:notify).with(RuntimeError.new("FinalizeInvoiceError"), anything) + expect(invoice_order).to_not be_completed + end + end + end + end + + describe "validation spec" do + let!(:start_of_july) { Time.now.beginning_of_year + 6.months } + + let!(:updater) { UpdateAccountInvoices.new } + let!(:finalizer) { FinalizeAccountInvoices.new } + + let!(:pm) { create(:payment_method, name: "Default Payment Method") } + let!(:sm) { create(:shipping_method, name: "Default Shipping Method") } + let!(:accounts_distributor) { create(:distributor_enterprise, payment_methods: [pm], shipping_methods: [sm]) } + + let!(:user) { create(:user) } + let!(:billable_period1) { create(:billable_period, sells: 'any', owner: user, begins_at: start_of_july - 1.month, ends_at: start_of_july) } + let!(:billable_period2) { create(:billable_period, owner: user, begins_at: start_of_july, ends_at: start_of_july + 10.days) } + let!(:billable_period3) { create(:billable_period, owner: user, begins_at: start_of_july + 12.days, ends_at: start_of_july + 20.days) } + + before do + sm.calculator.set_preference(:amount, 0); sm.calculator.save! + + Spree::Config.set({ accounts_distributor_id: accounts_distributor.id }) + Spree::Config.set({ default_accounts_payment_method_id: pm.id }) + Spree::Config.set({ default_accounts_shipping_method_id: sm.id }) + end + + context "finalizing an invoice" do + travel_to(3.hours) + + it "finalizes it" do + # Create an invoice using the updater, to make sure we are using + # an order as it would be when generated this way + expect{updater.perform}.to change{Spree::Order.count}.from(0).to(1) + invoice = user.orders.first + + # Finalize invoices + finalizer.perform + invoice.reload + + expect(invoice.completed_at).to_not be_nil + expect(invoice.total).to eq billable_period1.bill + expect(invoice.payments.count).to eq 1 + expect(invoice.payments.first.amount).to eq billable_period1.bill + expect(invoice.state).to eq 'complete' + end + end + end +end diff --git a/spec/jobs/update_account_invoices_spec.rb b/spec/jobs/update_account_invoices_spec.rb new file mode 100644 index 0000000000..fd84a22e1e --- /dev/null +++ b/spec/jobs/update_account_invoices_spec.rb @@ -0,0 +1,417 @@ +require 'spec_helper' + +def travel_to(time) + around { |example| Timecop.travel(start_of_july + time) { example.run } } +end + +describe UpdateAccountInvoices do + describe "units specs" do + let!(:start_of_july) { Time.now.beginning_of_year + 6.months } + + let!(:updater) { UpdateAccountInvoices.new } + + let!(:user) { create(:user) } + let!(:old_billable_period) { create(:billable_period, owner: user, begins_at: start_of_july - 1.month, ends_at: start_of_july) } + let!(:billable_period1) { create(:billable_period, owner: user, begins_at: start_of_july, ends_at: start_of_july + 12.days) } + let!(:billable_period2) { create(:billable_period, owner: user, begins_at: start_of_july + 12.days, ends_at: start_of_july + 20.days) } + let(:june_account_invoice) { old_billable_period.account_invoice } + let(:july_account_invoice) { billable_period1.account_invoice } + + describe "perform" do + let(:accounts_distributor) { double(:accounts_distributor) } + before do + allow(Enterprise).to receive(:find_by_id) { accounts_distributor } + allow(updater).to receive(:update) + allow(Bugsnag).to receive(:notify) + end + + context "when necessary global config setting have not been set" do + travel_to(20.days) + + context "when accounts_distributor has been set" do + before do + allow(Enterprise).to receive(:find_by_id) { false } + updater.perform + end + + it "snags errors and doesn't run" do + expect(Bugsnag).to have_received(:notify).with(RuntimeError.new("InvalidJobSettings"), anything) + expect(updater).to_not have_received(:update) + end + end + end + + context "when necessary global config setting have been set" do + context "on the first of the month" do + travel_to(3.hours) + + it "updates invoices from the previous month" do + updater.perform + expect(updater).to have_received(:update).once + .with(june_account_invoice) + expect(updater).to_not have_received(:update) + .with(july_account_invoice) + end + end + + context "on other days" do + travel_to(20.days) + + it "updates invoices from the current month" do + updater.perform + expect(updater).to have_received(:update).once + .with(july_account_invoice) + end + end + + context "when specfic a specific month (and year) are passed as arguments" do + let!(:updater) { UpdateAccountInvoices.new(Time.now.year, 7) } + + before do + allow(updater).to receive(:update) + end + + context "that just ended (in the past)" do + travel_to(1.month) + + it "updates invoices from the previous month" do + updater.perform + expect(updater).to have_received(:update).once + .with(july_account_invoice) + end + end + + context "that starts in the past and ends in the future (ie. current_month)" do + travel_to 30.days + + it "updates invoices from that current month" do + updater.perform + expect(updater).to have_received(:update).once + .with(july_account_invoice) + end + end + + context "that starts in the future" do + travel_to -1.days + + it "snags an error and does not update invoices" do + updater.perform + expect(Bugsnag).to have_received(:notify).with(RuntimeError.new("InvalidJobSettings"), anything) + expect(updater).to_not have_received(:update) + end + end + end + end + end + + describe "update" do + before do + allow(june_account_invoice).to receive(:save).and_call_original + allow(july_account_invoice).to receive(:save).and_call_original + allow(updater).to receive(:clean_up) + allow(updater).to receive(:finalize) + allow(Bugsnag).to receive(:notify) + end + + context "where an order for the invoice already exists" do + let!(:invoice_order) { create(:order, user: user) } + + before do + expect(Spree::Order).to_not receive(:new) + allow(june_account_invoice).to receive(:order) { invoice_order } + end + + context "where the order is already complete" do + before do + allow(invoice_order).to receive(:complete?) { true } + updater.update(june_account_invoice) + end + + it "snags a bug" do + expect(Bugsnag).to have_received(:notify) + end + + it "does not save the order" do + expect(june_account_invoice).to_not have_received(:save) + end + + it "does not clean up the order" do + expect(updater).to_not have_received(:clean_up).with(invoice_order, anything) + end + end + + context "where the order is not complete" do + before do + allow(invoice_order).to receive(:complete?) { false } + updater.update(june_account_invoice) + end + + it "creates adjustments for each billing item" do + adjustments = invoice_order.adjustments + expect(adjustments.map(&:source_id)).to eq [old_billable_period.id] + expect(adjustments.map(&:amount)).to eq [old_billable_period.bill] + expect(adjustments.map(&:label)).to eq [old_billable_period.adjustment_label] + end + + it "assigns a addresses to the order" do + expect(invoice_order.billing_address).to be_a Spree::Address + expect(invoice_order.shipping_address).to be_a Spree::Address + expect(invoice_order.billing_address).to eq old_billable_period.enterprise.address + expect(invoice_order.shipping_address).to eq old_billable_period.enterprise.address + end + + it "saves the order" do + expect(june_account_invoice).to have_received(:save) + expect(june_account_invoice.order).to be_persisted + end + + it "cleans up the order" do + expect(updater).to have_received(:clean_up).with(invoice_order, anything).once + end + end + end + + context "where an order for the invoice does not already exist" do + let!(:accounts_distributor) { create(:distributor_enterprise) } + before do + Spree::Config.set({ accounts_distributor_id: accounts_distributor.id }) + updater.update(july_account_invoice) + end + + it "creates adjustments for each billing item" do + adjustments = july_account_invoice.order.adjustments + expect(adjustments.map(&:source_id)).to eq [billable_period1.id, billable_period2.id] + expect(adjustments.map(&:amount)).to eq [billable_period1.bill, billable_period2.bill] + expect(adjustments.map(&:label)).to eq [billable_period1.adjustment_label, billable_period2.adjustment_label] + end + + it "saves the order" do + expect(july_account_invoice).to have_received(:save) + expect(july_account_invoice.order).to be_persisted + end + + it "cleans up order" do + expect(updater).to have_received(:clean_up).with(july_account_invoice.order, anything).once + end + end + end + + describe "clean_up" do + let!(:invoice_order) { create(:order) } + let!(:obsolete1) { create(:adjustment, adjustable: invoice_order) } + let!(:obsolete2) { create(:adjustment, adjustable: invoice_order) } + let!(:current1) { create(:adjustment, adjustable: invoice_order) } + let!(:current2) { create(:adjustment, adjustable: invoice_order) } + + before do + allow(invoice_order).to receive(:save) + allow(invoice_order).to receive(:destroy) + allow(Bugsnag).to receive(:notify) + end + + context "when current adjustments are present" do + let!(:current_adjustments) { [current1, current2] } + + context "and obsolete adjustments are present" do + let!(:obsolete_adjustments) { [obsolete1, obsolete2] } + + before do + allow(obsolete_adjustments).to receive(:destroy_all) + allow(invoice_order).to receive(:adjustments) { double(:adjustments, where: obsolete_adjustments) } + updater.clean_up(invoice_order, current_adjustments) + end + + it "destroys obsolete adjustments and snags a bug" do + expect(obsolete_adjustments).to have_received(:destroy_all) + expect(Bugsnag).to have_received(:notify).with(RuntimeError.new("Obsolete Adjustments"), anything) + end + end + + context "and obsolete adjustments are not present" do + let!(:obsolete_adjustments) { [] } + + before do + allow(invoice_order).to receive(:adjustments) { double(:adjustments, where: obsolete_adjustments) } + updater.clean_up(invoice_order, current_adjustments) + end + + it "has no bugs to snag" do + expect(Bugsnag).to_not have_received(:notify) + end + end + end + + context "when current adjustments are not present" do + let!(:current_adjustments) { [] } + + context "and obsolete adjustments are present" do + let!(:obsolete_adjustments) { [obsolete1, obsolete2] } + + before do + allow(obsolete_adjustments).to receive(:destroy_all) + allow(invoice_order).to receive(:adjustments) { double(:adjustments, where: obsolete_adjustments) } + end + + it "destroys obsolete adjustments and snags a bug" do + updater.clean_up(invoice_order, current_adjustments) + expect(obsolete_adjustments).to have_received(:destroy_all) + expect(Bugsnag).to have_received(:notify).with(RuntimeError.new("Obsolete Adjustments"), anything) + end + + context "when the order is not persisted" do + before do + allow(invoice_order).to receive(:persisted?) { false } + end + + it "destroys the order" do + updater.clean_up(invoice_order, current_adjustments) + expect(invoice_order).to have_received(:destroy) + end + end + + context "when the order is persisted" do + before do + allow(invoice_order).to receive(:persisted?) { true } + end + + it "snags a bug" do + updater.clean_up(invoice_order, current_adjustments) + expect(Bugsnag).to have_received(:notify).with(RuntimeError.new("Empty Persisted Invoice"), anything) + end + end + end + + context "and obsolete adjustments are not present" do + let!(:obsolete_adjustments) { [] } + + before do + allow(invoice_order).to receive(:adjustments) { double(:adjustments, where: obsolete_adjustments) } + end + + it "has no bugs to snag" do + expect(Bugsnag).to_not have_received(:notify).with(RuntimeError.new("Obsolete Adjustments"), anything) + end + + context "when the order is not persisted" do + before do + allow(invoice_order).to receive(:persisted?) { false } + end + + it "destroys the order" do + updater.clean_up(invoice_order, current_adjustments) + expect(invoice_order).to have_received(:destroy) + end + end + + context "when the order is persisted" do + before do + allow(invoice_order).to receive(:persisted?) { true } + end + + it "snags a bug" do + updater.clean_up(invoice_order, current_adjustments) + expect(Bugsnag).to have_received(:notify).with(RuntimeError.new("Empty Persisted Invoice"), anything) + end + end + end + end + end + end + + describe "validation spec" do + let!(:start_of_july) { Time.now.beginning_of_year + 6.months } + + let!(:updater) { UpdateAccountInvoices.new } + + let!(:accounts_distributor) { create(:distributor_enterprise) } + + let!(:user) { create(:user) } + let!(:billable_period1) { create(:billable_period, sells: 'any', owner: user, begins_at: start_of_july - 1.month, ends_at: start_of_july) } + let!(:billable_period2) { create(:billable_period, owner: user, begins_at: start_of_july, ends_at: start_of_july + 10.days) } + let!(:billable_period3) { create(:billable_period, owner: user, begins_at: start_of_july + 12.days, ends_at: start_of_july + 20.days) } + let!(:july_account_invoice) { billable_period2.account_invoice } + let!(:august_account_invoice) { create(:account_invoice, user: user, year: july_account_invoice.year, month: 8)} + + before do + Spree::Config.set({ accounts_distributor_id: accounts_distributor.id }) + end + + context "when no invoice_order currently exists" do + context "when relevant billable periods exist" do + travel_to(20.days) + + it "creates an invoice_order" do + expect{updater.perform}.to change{Spree::Order.count}.from(0).to(1) + invoice_order = july_account_invoice.reload.order + expect(user.orders.first).to eq invoice_order + expect(invoice_order.completed_at).to be_nil + billable_adjustments = invoice_order.adjustments.where('source_type = (?)', 'BillablePeriod') + expect(billable_adjustments.map(&:amount)).to eq [billable_period2.bill, billable_period3.bill] + expect(invoice_order.total).to eq billable_period2.bill + billable_period3.bill + expect(invoice_order.payments.count).to eq 0 + expect(invoice_order.state).to eq 'cart' + expect(invoice_order.bill_address).to be_a Spree::Address + expect(invoice_order.ship_address).to be_a Spree::Address + expect(invoice_order.bill_address).to eq billable_period2.enterprise.address + expect(invoice_order.ship_address).to eq billable_period2.enterprise.address + end + end + + context "when no relevant billable periods exist" do + travel_to(1.month + 5.days) + + it "does not create an order" do + expect(updater).to receive(:update).with(august_account_invoice).and_call_original + expect{updater.perform}.to_not change{Spree::Order.count}.from(0) + end + end + end + + context "when an order already exists" do + context "when relevant billable periods exist" do + let!(:invoice_order) { create(:order, user: user, distributor: accounts_distributor, created_at: start_of_july) } + let!(:billable_adjustment) { create(:adjustment, adjustable: invoice_order, source_type: 'BillablePeriod') } + + before do + invoice_order.line_items.clear + july_account_invoice.update_attribute(:order, invoice_order) + end + + travel_to(20.days) + + it "updates the order, and clears any obsolete invoices" do + expect{updater.perform}.to_not change{Spree::Order.count} + invoice_order = user.orders.first + expect(invoice_order.completed_at).to be_nil + billable_adjustments = invoice_order.adjustments.where('source_type = (?)', 'BillablePeriod') + expect(billable_adjustments).to_not include billable_adjustment + expect(billable_adjustments.map(&:amount)).to eq [billable_period2.bill, billable_period3.bill] + expect(invoice_order.total).to eq billable_period2.bill + billable_period3.bill + expect(invoice_order.payments.count).to eq 0 + expect(invoice_order.state).to eq 'cart' + expect(invoice_order.bill_address).to be_a Spree::Address + expect(invoice_order.ship_address).to be_a Spree::Address + expect(invoice_order.bill_address).to eq billable_period2.enterprise.address + expect(invoice_order.ship_address).to eq billable_period2.enterprise.address + end + end + + context "when no relevant billable periods exist" do + let!(:invoice_order) { create(:order, user: user, distributor: accounts_distributor) } + + before do + invoice_order.line_items.clear + august_account_invoice.update_attribute(:order, invoice_order) + end + + travel_to(1.month + 5.days) + + it "snags a bug" do + expect(updater).to receive(:update).with(august_account_invoice).and_call_original + expect(Bugsnag).to receive(:notify).with(RuntimeError.new("Empty Persisted Invoice"), anything) + expect{updater.perform}.to_not change{Spree::Order.count} + end + end + end + end +end diff --git a/spec/jobs/update_billable_periods_spec.rb b/spec/jobs/update_billable_periods_spec.rb new file mode 100644 index 0000000000..ef6d2b12ad --- /dev/null +++ b/spec/jobs/update_billable_periods_spec.rb @@ -0,0 +1,721 @@ +require 'spec_helper' + +def travel_to(time) + around { |example| Timecop.travel(start_of_july + time) { example.run } } +end + +describe UpdateBillablePeriods do + describe "unit specs" do + let!(:start_of_july) { Time.now.beginning_of_year + 6.months } + let!(:year) { Time.now.year } + + let!(:updater) { UpdateBillablePeriods.new } + + describe "perform", versioning: true do + let!(:enterprise) { create(:supplier_enterprise, created_at: start_of_july - 1.month, sells: 'any') } + + context "when no date arguments are passed to the job" do + before do + expect(updater).to receive(:clean_up_untouched_billable_periods_for).once + end + + context "on the first of the month" do + travel_to(3.hours) + + it "processes the previous month" do + expect(updater).to receive(:split_for_trial) + .with(enterprise, start_of_july - 1.month, start_of_july, nil, nil) + updater.perform + end + end + + context "on all other days" do + travel_to(1.day + 3.hours) + + it "processes the current month up until previous midnight" do + expect(updater).to receive(:split_for_trial) + .with(enterprise, start_of_july, start_of_july + 1.day, nil, nil) + updater.perform + end + end + end + + context "when a specfic year and month are passed as arguments" do + let!(:updater) { UpdateBillablePeriods.new(year, 6) } + + before do + allow(updater).to receive(:split_for_trial) + end + + context "that ends in the past" do + travel_to(3.hours) + + it "processes the month" do + expect(updater).to receive(:split_for_trial) + .with(enterprise, start_of_july - 1.month, start_of_july, nil, nil) + updater.perform + end + end + + context "that starts in the past and ends in the future (ie. current month)" do + travel_to(-3.days) + + it "processes the current month up to the previous midnight" do + expect(updater).to receive(:split_for_trial) + .with(enterprise, start_of_july - 1.month, start_of_july-3.days, nil, nil) + updater.perform + end + end + + context "that starts in the future" do + travel_to(-31.days) + + it "does not run" do + expect(updater).to_not receive(:split_for_trial) + updater.perform + end + end + end + + context "when an enterprise is created before the beginning of the current month" do + before do + expect(updater).to receive(:clean_up_untouched_billable_periods_for).once + end + + travel_to(28.days) + + context "when no alterations to sells or owner have been made during the current month" do + + it "begins at the start of the month" do + expect(updater).to receive(:split_for_trial) + .with(enterprise, start_of_july, start_of_july + 28.days, nil, nil) + updater.perform + end + end + + context "when sells has been changed within the current month" do + before do + Timecop.freeze(start_of_july + 10.days) do + # NOTE: Sells is changed between when order1 and order2 are placed + enterprise.update_attribute(:sells, 'own') + end + end + + travel_to(28.days) + + it "processes each sells period separately" do + allow(updater).to receive(:split_for_trial).twice + updater.perform + + expect(updater).to have_received(:split_for_trial) + .with(enterprise.versions.first.reify, start_of_july, start_of_july + 10.days, nil, nil) + + expect(updater).to have_received(:split_for_trial) + .with(enterprise, start_of_july + 10.days, start_of_july + 28.days, nil, nil) + end + end + + context "when owner has been changed within the current month" do + let!(:new_owner) { create(:user) } + + before do + Timecop.freeze(start_of_july + 10.days) do + # NOTE: Sells is changed between when order1 and order2 are placed + enterprise.update_attribute(:owner, new_owner) + end + end + + travel_to(28.days) + + it "processes each ownership period separately" do + allow(updater).to receive(:split_for_trial).twice + updater.perform + + expect(updater).to have_received(:split_for_trial) + .with(enterprise.versions.first.reify, start_of_july, start_of_july + 10.days, nil, nil) + + expect(updater).to have_received(:split_for_trial) + .with(enterprise, start_of_july + 10.days, start_of_july + 28.days, nil, nil) + end + end + + context "when some other attribute has been changed within the current month" do + before do + Timecop.freeze(start_of_july + 10.days) do + # NOTE: Sells is changed between when order1 and order2 are placed + enterprise.update_attribute(:name, 'Some New Name') + end + end + + travel_to(28.days) + + it "does not create a version, and so does not split the period" do + expect(enterprise.versions).to eq [] + allow(updater).to receive(:split_for_trial).once + updater.perform + expect(updater).to have_received(:split_for_trial) + .with(enterprise, start_of_july, start_of_july + 28.days, nil, nil) + end + end + + context "where sells or owner_id were altered during the previous month (ie. June)" do + let!(:new_owner) { create(:user) } + + before do + Timecop.freeze(start_of_july - 20.days) do + # NOTE: Sells is changed between when order1 and order2 are placed + enterprise.update_attribute(:sells, 'own') + end + Timecop.freeze(start_of_july - 10.days) do + # NOTE: Sells is changed between when order1 and order2 are placed + enterprise.update_attribute(:owner, new_owner) + end + end + + travel_to(28.days) + + it "ignores those verions" do + allow(updater).to receive(:split_for_trial).once + updater.perform + expect(updater).to have_received(:split_for_trial) + .with(enterprise, start_of_july, start_of_july + 28.days, nil, nil) + end + end + + context "where sells or owner_id were altered in the future" do + let!(:new_owner) { create(:user) } + + before do + Timecop.freeze(start_of_july + 17.days) do + enterprise.update_attribute(:sells, 'own') + end + Timecop.freeze(start_of_july + 35.days) do + enterprise.update_attribute(:owner, new_owner) + end + end + + travel_to(15.days) + + it "ignores those verions" do + allow(updater).to receive(:split_for_trial).once + updater.perform + expect(updater).to have_received(:split_for_trial) + .with(enterprise, start_of_july, start_of_july + 15.days, nil, nil) + end + end + end + + context "when an enterprise is created during the current month" do + before do + expect(updater).to receive(:clean_up_untouched_billable_periods_for).once + enterprise.update_attribute(:created_at, start_of_july + 10.days) + end + + travel_to(28.days) + + it "begins at the date the enterprise was created" do + allow(updater).to receive(:split_for_trial).once + updater.perform + expect(updater).to have_received(:split_for_trial) + .with(enterprise, start_of_july + 10.days, start_of_july + 28.days, nil, nil) + end + end + + context "when an enterprise is created after the previous midnight" do + before do + expect(updater).to_not receive(:clean_up_untouched_billable_periods_for) + enterprise.update_attribute(:created_at, start_of_july + 29.days) + end + + travel_to(28.days) + + it "ignores the enterprise" do + allow(updater).to receive(:split_for_trial) + updater.perform + expect(updater).to_not have_received(:split_for_trial) + end + end + + pending "when an enterprise is deleted during the current month" do + before do + expect(updater).to receive(:clean_up_untouched_billable_periods_for).once + enterprise.update_attribute(:deleted_at, start_of_july + 20.days) + end + + travel_to(28.days) + + it "ends at the date the enterprise was deleted" do + allow(updater).to receive(:split_for_trial) + updater.perform + expect(updater).to have_received(:split_for_trial) + .with(enterprise, start_of_july, start_of_july + 20.days, nil, nil) + end + end + end + + describe "split_for_trial" do + let!(:enterprise) { double(:enterprise) } + let(:begins_at) { start_of_july } + let(:ends_at) { begins_at + 30.days } + + context "when trial_start is nil" do + let(:trial_start) { nil } + let(:trial_expiry) { begins_at + 3.days } + + before do + allow(updater).to receive(:update_billable_period).once + updater.split_for_trial(enterprise, begins_at, ends_at, trial_start, trial_expiry) + end + + it "calls update_billable_period once for the entire period" do + expect(updater).to have_received(:update_billable_period) + .with(enterprise, begins_at, ends_at, false) + end + end + + context "when trial_expiry is nil" do + let(:trial_start) { begins_at + 3.days } + let(:trial_expiry) { nil } + + before do + allow(updater).to receive(:update_billable_period).once + updater.split_for_trial(enterprise, begins_at, ends_at, trial_start, trial_expiry) + end + + it "calls update_billable_period once for the entire period" do + expect(updater).to have_received(:update_billable_period) + .with(enterprise, begins_at, ends_at, false) + end + end + + context "when the trial begins before begins_at" do + let(:trial_start) { begins_at - 10.days } + + context "and the trial ends before begins_at" do + let(:trial_expiry) { begins_at - 5.days } + + before do + allow(updater).to receive(:update_billable_period).once + updater.split_for_trial(enterprise, begins_at, ends_at, trial_start, trial_expiry) + end + + it "calls update_billable_period once for the entire period" do + expect(updater).to have_received(:update_billable_period) + .with(enterprise, begins_at, ends_at, false) + end + end + + context "and the trial ends after begins_at" do + let(:trial_expiry) { begins_at + 5.days } + + before do + allow(updater).to receive(:update_billable_period).twice + updater.split_for_trial(enterprise, begins_at, ends_at, trial_start, trial_expiry) + end + + it "calls update_billable_period once for the trial period" do + expect(updater).to have_received(:update_billable_period) + .with(enterprise, begins_at, trial_expiry, true) + end + + it "calls update_billable_period once for the non-trial period" do + expect(updater).to have_received(:update_billable_period) + .with(enterprise, trial_expiry, ends_at, false) + end + end + + context "and the trial ends after ends_at" do + let(:trial_expiry) { ends_at + 5.days } + + before do + allow(updater).to receive(:update_billable_period).once + updater.split_for_trial(enterprise, begins_at, ends_at, trial_start, trial_expiry) + end + + it "calls update_billable_period once for the entire (trial) period" do + expect(updater).to have_received(:update_billable_period) + .with(enterprise, begins_at, ends_at, true) + end + end + end + + context "when the trial begins after begins_at" do + let(:trial_start) { begins_at + 5.days } + + context "and the trial begins after ends_at" do + let(:trial_start) { ends_at + 5.days } + let(:trial_expiry) { ends_at + 10.days } + + before do + allow(updater).to receive(:update_billable_period).once + updater.split_for_trial(enterprise, begins_at, ends_at, trial_start, trial_expiry) + end + + it "calls update_billable_period once for the entire period" do + expect(updater).to have_received(:update_billable_period) + .with(enterprise, begins_at, ends_at, false) + end + end + + context "and the trial ends before ends_at" do + let(:trial_expiry) { ends_at - 2.days } + + before do + allow(updater).to receive(:update_billable_period).exactly(3).times + updater.split_for_trial(enterprise, begins_at, ends_at, trial_start, trial_expiry) + end + + it "calls update_billable_period once for the non-trial period before the trial" do + expect(updater).to have_received(:update_billable_period) + .with(enterprise, begins_at, trial_start, false) + end + + it "calls update_billable_period once for the trial period" do + expect(updater).to have_received(:update_billable_period) + .with(enterprise, trial_start, trial_expiry, true) + end + + it "calls update_billable_period once for the non-trial period after the trial" do + expect(updater).to have_received(:update_billable_period) + .with(enterprise, trial_expiry, ends_at, false) + end + end + + context "and the trial ends after ends_at" do + let(:trial_expiry) { ends_at + 5.days } + + before do + allow(updater).to receive(:update_billable_period).twice + updater.split_for_trial(enterprise, begins_at, ends_at, trial_start, trial_expiry) + end + + it "calls update_billable_period once for the non-trial period" do + expect(updater).to have_received(:update_billable_period) + .with(enterprise, begins_at, trial_start, false) + end + + it "calls update_billable_period once for the trial period" do + expect(updater).to have_received(:update_billable_period) + .with(enterprise, trial_start, ends_at, true) + end + end + end + end + + describe "update_billable_period" do + let!(:enterprise) { create(:enterprise, sells: 'any') } + + let!(:existing) { create(:billable_period, enterprise: enterprise, begins_at: start_of_july) } + + before do + allow(Spree::Order).to receive(:where) { [ + double(:order, total: 10), + double(:order, total: 20), + double(:order, total: 30) + ]} + end + + context "when the account invoice is already_complete" do + before do + allow(BillablePeriod).to receive(:where) { [existing] } + allow(existing.account_invoice).to receive(:order) { double(:order, complete?: true ) } + allow(AccountInvoice).to receive(:find_or_create_by_user_id_and_year_and_month) { existing.account_invoice } + end + + it "does not update the billing period, but changes updated_at by touching the billable period " do + expect(existing).to_not receive(:update_attributes) + expect(existing).to receive(:touch) + expect(Bugsnag).to_not receive(:notify) + expect{ + updater.update_billable_period(enterprise, start_of_july, start_of_july + 20.days, false) + }.to_not change{ BillablePeriod.count } + end + end + + context "when arguments match both 'begins_at' and 'enterprise_id' of an existing billable period" do + it "updates the existing billable period" do + expect{ + updater.update_billable_period(enterprise, start_of_july, start_of_july + 20.days, false) + }.to_not change{ BillablePeriod.count } + existing.reload + expect(existing.owner_id).to eq enterprise.owner_id + expect(existing.ends_at).to eq start_of_july + 20.days + expect(existing.sells).to eq enterprise.sells + expect(existing.trial).to eq false + expect(existing.turnover).to eq 60 + end + + context "when there is nothing to update" do + before do + Timecop.freeze(start_of_july + 3.days) { + existing.update_attributes( + begins_at: start_of_july, + ends_at: start_of_july + 20.days, + trial: false, + sells: enterprise.sells, + turnover: 60 + ) + } + end + + it "changes updated_at anyway by touching the billable period" do + Timecop.freeze(start_of_july + 10.days) { + expect{ + updater.update_billable_period(enterprise, start_of_july, start_of_july + 20.days, false) + }.to change{ existing.reload.updated_at } + .from(start_of_july + 3.days) + .to(start_of_july + 10.days) + } + end + end + end + + context "when 'begins_at' does not match an existing billable period" do + before do + expect{ + updater.update_billable_period(enterprise, start_of_july + 20.days, start_of_july + 30.days, false) + }.to change{ BillablePeriod.count }.from(1).to(2) + end + + it "creates a new existing billable period" do + billable_period = BillablePeriod.last + expect(billable_period.owner_id).to eq enterprise.owner_id + expect(billable_period.ends_at).to eq start_of_july + 30.days + expect(billable_period.sells).to eq enterprise.sells + expect(billable_period.trial).to eq false + expect(billable_period.turnover).to eq 60 + end + end + + context "when 'enterprise_id' does not match an existing billable period" do + let!(:new_enterprise) { create(:enterprise, sells: 'own') } + + before do + expect{ + updater.update_billable_period(new_enterprise, start_of_july, start_of_july + 20.days, false) + }.to change{ BillablePeriod.count }.from(1).to(2) + end + + it "creates a new existing billable period" do + billable_period = BillablePeriod.last + expect(billable_period.owner_id).to eq new_enterprise.owner_id + expect(billable_period.ends_at).to eq start_of_july + 20.days + expect(billable_period.sells).to eq new_enterprise.sells + expect(billable_period.trial).to eq false + expect(billable_period.turnover).to eq 60 + end + end + end + + context "cleaning up untouched billable periods" do + let(:job_start_time) { Time.now } + let(:enterprise) { create(:enterprise) } + # Updated after start + let!(:bp1) { create(:billable_period, enterprise: enterprise, updated_at: job_start_time + 2.seconds, begins_at: start_of_july, ends_at: start_of_july + 5.days ) } + let!(:bp2) { create(:billable_period, enterprise: enterprise, updated_at: job_start_time + 2.seconds, begins_at: start_of_july + 5.days, ends_at: start_of_july + 10.days ) } + # Updated before start + let!(:bp3) { create(:billable_period, enterprise: enterprise, updated_at: job_start_time - 5.seconds, begins_at: start_of_july, ends_at: start_of_july + 10.days ) } + # Updated before start but begins after end_date + let!(:bp4) { create(:billable_period, enterprise: enterprise, updated_at: job_start_time - 5.seconds, begins_at: start_of_july + 10.days, ends_at: start_of_july + 15.days ) } + # Updated before start but begins at end_date (ie. not before end_date, so should be ignored) EDGE CASE + let!(:bp5) { create(:billable_period, enterprise: enterprise, updated_at: job_start_time - 5.seconds, begins_at: start_of_july + 8.days, ends_at: start_of_july + 10.days ) } + # Updated before start but ends before start_date + let!(:bp6) { create(:billable_period, enterprise: enterprise, updated_at: job_start_time - 5.seconds, begins_at: start_of_july - 10.days, ends_at: start_of_july - 5.days ) } + # Updated before start but ends at start_date (ie. not after start_date, so should be ignored) EDGE CASE + let!(:bp7) { create(:billable_period, enterprise: enterprise, updated_at: job_start_time - 5.seconds, begins_at: start_of_july - 5.days, ends_at: start_of_july ) } + + before do + allow(Bugsnag).to receive(:notify) + allow(updater).to receive(:start_date) { start_of_july } + allow(updater).to receive(:end_date) { start_of_july + 8.days } + updater.clean_up_untouched_billable_periods_for(enterprise, job_start_time) + end + + it "soft deletes untouched billable_periods" do + expect(bp1.reload.deleted_at).to be_nil + expect(bp2.reload.deleted_at).to be_nil + expect(bp3.reload.deleted_at).to_not be_nil + expect(bp4.reload.deleted_at).to be_nil + expect(bp5.reload.deleted_at).to be_nil + expect(bp6.reload.deleted_at).to be_nil + expect(bp7.reload.deleted_at).to be_nil + end + + it "notifies bugsnag" do + expect(Bugsnag).to have_received(:notify).once + end + end + end + + describe "validation spec" do + # Chose july to test with because June has 30 days and so is easy to calculate end date for shop trial + let!(:start_of_july) { Time.now.beginning_of_year + 6.months } + + let!(:year) { Time.now.year } + + let!(:enterprise) { create(:supplier_enterprise, sells: 'any') } + + let!(:original_owner) { enterprise.owner } + + let!(:new_owner) { create(:user) } + + let!(:account_invoice1) { create(:account_invoice, user: original_owner, year: year, month: 7)} + let!(:account_invoice2) { create(:account_invoice, user: new_owner, year: year, month: 7)} + + # This BP was updated before the current run and so should be marked for deletion at the end of the run + let!(:obsolete_bp) { create(:billable_period, enterprise: enterprise, updated_at: start_of_july + 10.days, begins_at: start_of_july + 6.5.days, ends_at: start_of_july + 10.days ) } + + # This one has an updated_at in the future (so that it doesn't get deleted) + # It also has a begins_at date which matches a period that would otherwise be created, + # and so it should be picked up and overwritten + let!(:bp_to_overwrite) { create(:billable_period, enterprise: enterprise, updated_at: start_of_july + 21.days, begins_at: start_of_july + 10.days, ends_at: start_of_july + 15.days ) } + + let!(:order1) { create(:order, completed_at: start_of_july + 1.days, distributor: enterprise) } + let!(:order2) { create(:order, completed_at: start_of_july + 3.days, distributor: enterprise) } + let!(:order3) { create(:order, completed_at: start_of_july + 5.days, distributor: enterprise) } + let!(:order4) { create(:order, completed_at: start_of_july + 7.days, distributor: enterprise) } + let!(:order5) { create(:order, completed_at: start_of_july + 9.days, distributor: enterprise) } + let!(:order6) { create(:order, completed_at: start_of_july + 11.days, distributor: enterprise) } + let!(:order7) { create(:order, completed_at: start_of_july + 13.days, distributor: enterprise) } + let!(:order8) { create(:order, completed_at: start_of_july + 15.days, distributor: enterprise) } + let!(:order9) { create(:order, completed_at: start_of_july + 17.days, distributor: enterprise) } + let!(:order10) { create(:order, completed_at: start_of_july + 19.days, distributor: enterprise) } + + before do + order1.line_items = [ create(:line_item, price: 12.56, order: order1) ] + order2.line_items = [ create(:line_item, price: 87.44, order: order2) ] + order3.line_items = [ create(:line_item, price: 50.00, order: order3) ] + order4.line_items = [ create(:line_item, price: 73.37, order: order4) ] + order5.line_items = [ create(:line_item, price: 22.46, order: order5) ] + order6.line_items = [ create(:line_item, price: 44.85, order: order6) ] + order7.line_items = [ create(:line_item, price: 93.45, order: order7) ] + order8.line_items = [ create(:line_item, price: 59.38, order: order8) ] + order9.line_items = [ create(:line_item, price: 47.23, order: order9) ] + order10.line_items = [ create(:line_item, price: 2.35, order: order10) ] + [order1, order2, order3, order4, order5, order6, order7, order8, order9, order10].each(&:update!) + + allow(Enterprise).to receive(:where) { double(:enterprises, select: [enterprise]) } + end + + context "super complex example", versioning: true do + before do + enterprise.update_attribute(:created_at, start_of_july + 2.days) + + Timecop.freeze(start_of_july + 4.days) { enterprise.update_attribute(:sells, 'own') } + + Timecop.freeze(start_of_july + 6.days) { enterprise.update_attribute(:owner, new_owner) } + + enterprise.update_attribute(:shop_trial_start_date, start_of_july + 8.days) + + Timecop.freeze(start_of_july + 10.days) { enterprise.update_attribute(:owner, original_owner) } + + Timecop.freeze(start_of_july + 12.days) { enterprise.update_attribute(:sells, 'any') } + + allow(enterprise).to receive(:shop_trial_expiry) { start_of_july + 14.days } + + Timecop.freeze(start_of_july + 16.days) { enterprise.update_attribute(:sells, 'own') } + + Timecop.freeze(start_of_july + 18.days) { enterprise.update_attribute(:owner, new_owner) } + end + + travel_to(20.days) + + before do + UpdateBillablePeriods.new.perform + end + + let(:billable_periods) { BillablePeriod.order(:updated_at) } + + it "creates the correct billable periods and deleted obsolete ones" do + expect(obsolete_bp.reload.deleted_at).to_not be_nil + + bp_to_overwrite.reload + + expect(bp_to_overwrite.sells).to eq 'own' + expect(bp_to_overwrite.trial).to be true + expect(bp_to_overwrite.owner).to eq original_owner + expect(bp_to_overwrite.begins_at).to eq start_of_july + 10.days + expect(bp_to_overwrite.ends_at).to eq start_of_july + 12.days + expect(bp_to_overwrite.turnover).to eq order6.total + expect(bp_to_overwrite.account_invoice).to eq account_invoice1 + + expect(billable_periods.count).to eq 9 + + expect(account_invoice1.billable_periods.sort).to eq billable_periods.sort.select{ |bp| bp.owner == original_owner } + expect(account_invoice2.billable_periods.sort).to eq billable_periods.sort.select{ |bp| bp.owner == new_owner } + + expect(billable_periods.map(&:begins_at)).to eq [ + start_of_july + 2.days, + start_of_july + 4.days, + start_of_july + 6.days, + start_of_july + 8.days, + start_of_july + 10.days, + start_of_july + 12.days, + start_of_july + 14.days, + start_of_july + 16.days, + start_of_july + 18.days + ] + + expect(billable_periods.map(&:ends_at)).to eq [ + start_of_july + 4.days, + start_of_july + 6.days, + start_of_july + 8.days, + start_of_july + 10.days, + start_of_july + 12.days, + start_of_july + 14.days, + start_of_july + 16.days, + start_of_july + 18.days, + start_of_july + 20.days + ] + + expect(billable_periods.map(&:owner)).to eq [ + original_owner, + original_owner, + new_owner, + new_owner, + original_owner, + original_owner, + original_owner, + original_owner, + new_owner + ] + + expect(billable_periods.map(&:sells)).to eq [ + 'any', + 'own', + 'own', + 'own', + 'own', + 'any', + 'any', + 'own', + 'own' + ] + + expect(billable_periods.map(&:trial)).to eq [ + false, + false, + false, + true, + true, + true, + false, + false, + false + ] + + expect(billable_periods.map(&:turnover)).to eq [ + order2.total, + order3.total, + order4.total, + order5.total, + order6.total, + order7.total, + order8.total, + order9.total, + order10.total + ] + end + end + end +end diff --git a/spec/lib/open_food_network/enterprise_fee_calculator_spec.rb b/spec/lib/open_food_network/enterprise_fee_calculator_spec.rb index 9c950234cd..d2d673e5ab 100644 --- a/spec/lib/open_food_network/enterprise_fee_calculator_spec.rb +++ b/spec/lib/open_food_network/enterprise_fee_calculator_spec.rb @@ -3,32 +3,80 @@ require 'open_food_network/enterprise_fee_calculator' module OpenFoodNetwork describe EnterpriseFeeCalculator do describe "integration" do + let(:supplier1) { create(:supplier_enterprise) } + let(:supplier2) { create(:supplier_enterprise) } let(:coordinator) { create(:distributor_enterprise) } let(:distributor) { create(:distributor_enterprise) } let(:order_cycle) { create(:simple_order_cycle) } - let(:product) { create(:simple_product, price: 10.00) } + let(:product1) { create(:simple_product, supplier: supplier1, price: 10.00) } + let(:product2) { create(:simple_product, supplier: supplier2, price: 20.00) } describe "calculating fees for a variant" do - it "sums all the per-item fees for the variant in the specified hub + order cycle" do - enterprise_fee1 = create(:enterprise_fee, amount: 20) - enterprise_fee2 = create(:enterprise_fee, amount: 3) - enterprise_fee3 = create(:enterprise_fee, - calculator: Spree::Calculator::FlatRate.new(preferred_amount: 2)) + describe "summing all the per-item fees for the variant in the specified hub + order cycle" do + let(:enterprise_fee1) { create(:enterprise_fee, amount: 20) } + let(:enterprise_fee2) { create(:enterprise_fee, amount: 3) } + let(:enterprise_fee3) { create(:enterprise_fee, calculator: Spree::Calculator::FlatRate.new(preferred_amount: 2)) } - create(:exchange, order_cycle: order_cycle, sender: coordinator, receiver: distributor, incoming: false, - enterprise_fees: [enterprise_fee1, enterprise_fee2, enterprise_fee3], variants: [product.master]) + describe "supplier fees" do + let!(:exchange1) { create(:exchange, order_cycle: order_cycle, sender: supplier1, receiver: coordinator, incoming: true, + enterprise_fees: [enterprise_fee1], variants: [product1.master]) } + let!(:exchange2) { create(:exchange, order_cycle: order_cycle, sender: supplier2, receiver: coordinator, incoming: true, + enterprise_fees: [enterprise_fee2], variants: [product2.master]) } - EnterpriseFeeCalculator.new(distributor, order_cycle).fees_for(product.master).should == 23 + it "calculates via regular computation" do + EnterpriseFeeCalculator.new(distributor, order_cycle).fees_for(product1.master).should == 20 + EnterpriseFeeCalculator.new(distributor, order_cycle).fees_for(product2.master).should == 3 + end + + it "calculates via indexed computation" do + EnterpriseFeeCalculator.new(distributor, order_cycle).indexed_fees_for(product1.master).should == 20 + EnterpriseFeeCalculator.new(distributor, order_cycle).indexed_fees_for(product2.master).should == 3 + end + end + + describe "coordinator fees" do + let!(:exchange) { create(:exchange, order_cycle: order_cycle, sender: coordinator, receiver: distributor, incoming: false, + enterprise_fees: [], variants: [product1.master]) } + + before do + order_cycle.coordinator_fees = [enterprise_fee1, enterprise_fee2, enterprise_fee3] + end + + it "sums via regular computation" do + EnterpriseFeeCalculator.new(distributor, order_cycle).fees_for(product1.master).should == 23 + end + + it "sums via indexed computation" do + EnterpriseFeeCalculator.new(distributor, order_cycle).indexed_fees_for(product1.master).should == 23 + end + end + + describe "distributor fees" do + let!(:exchange) { create(:exchange, order_cycle: order_cycle, sender: coordinator, receiver: distributor, incoming: false, + enterprise_fees: [enterprise_fee1, enterprise_fee2, enterprise_fee3], variants: [product1.master]) } + + it "sums via regular computation" do + EnterpriseFeeCalculator.new(distributor, order_cycle).fees_for(product1.master).should == 23 + end + + it "sums via indexed computation" do + EnterpriseFeeCalculator.new(distributor, order_cycle).indexed_fees_for(product1.master).should == 23 + end + end end - it "sums percentage fees for the variant" do - enterprise_fee1 = create(:enterprise_fee, amount: 20, fee_type: "admin", calculator: Spree::Calculator::FlatPercentItemTotal.new(preferred_flat_percent: 20)) + describe "summing percentage fees for the variant" do + let!(:enterprise_fee1) { create(:enterprise_fee, amount: 20, fee_type: "admin", calculator: Spree::Calculator::FlatPercentItemTotal.new(preferred_flat_percent: 20)) } + let!(:exchange) { create(:exchange, order_cycle: order_cycle, sender: coordinator, receiver: distributor, incoming: false, + enterprise_fees: [enterprise_fee1], variants: [product1.master]) } - create(:exchange, order_cycle: order_cycle, sender: coordinator, receiver: distributor, incoming: false, - enterprise_fees: [enterprise_fee1], variants: [product.master]) + it "sums via regular computation" do + EnterpriseFeeCalculator.new(distributor, order_cycle).fees_for(product1.master).should == 2.00 + end - product.master.price.should == 10.00 - EnterpriseFeeCalculator.new(distributor, order_cycle).fees_for(product.master).should == 2.00 + it "sums via indexed computation" do + EnterpriseFeeCalculator.new(distributor, order_cycle).indexed_fees_for(product1.master).should == 2.00 + end end end @@ -41,25 +89,37 @@ module OpenFoodNetwork let!(:exchange) { create(:exchange, order_cycle: order_cycle, sender: coordinator, receiver: distributor, incoming: false, enterprise_fees: [ef_admin, ef_sales, ef_packing, ef_transport, ef_fundraising], - variants: [product.master]) } + variants: [product1.master]) } - it "returns a breakdown of fees" do - EnterpriseFeeCalculator.new(distributor, order_cycle).fees_by_type_for(product.master).should == {admin: 1.23, sales: 4.56, packing: 7.89, transport: 0.12, fundraising: 3.45} + describe "regular computation" do + it "returns a breakdown of fees" do + EnterpriseFeeCalculator.new(distributor, order_cycle).fees_by_type_for(product1.master).should == {admin: 1.23, sales: 4.56, packing: 7.89, transport: 0.12, fundraising: 3.45} + end + + it "filters out zero fees" do + ef_admin.calculator.update_attribute :preferred_amount, 0 + EnterpriseFeeCalculator.new(distributor, order_cycle).fees_by_type_for(product1.master).should == {sales: 4.56, packing: 7.89, transport: 0.12, fundraising: 3.45} + end end - it "filters out zero fees" do - ef_admin.calculator.update_attribute :preferred_amount, 0 + describe "indexed computation" do + it "returns a breakdown of fees" do + EnterpriseFeeCalculator.new(distributor, order_cycle).indexed_fees_by_type_for(product1.master).should == {admin: 1.23, sales: 4.56, packing: 7.89, transport: 0.12, fundraising: 3.45} + end - EnterpriseFeeCalculator.new(distributor, order_cycle).fees_by_type_for(product.master).should == {sales: 4.56, packing: 7.89, transport: 0.12, fundraising: 3.45} + it "filters out zero fees" do + ef_admin.calculator.update_attribute :preferred_amount, 0 + EnterpriseFeeCalculator.new(distributor, order_cycle).indexed_fees_by_type_for(product1.master).should == {sales: 4.56, packing: 7.89, transport: 0.12, fundraising: 3.45} + end end end describe "creating adjustments" do let(:order) { create(:order, distributor: distributor, order_cycle: order_cycle) } - let!(:line_item) { create(:line_item, order: order, variant: product.master) } + let!(:line_item) { create(:line_item, order: order, variant: product1.master) } let(:enterprise_fee_line_item) { create(:enterprise_fee) } let(:enterprise_fee_order) { create(:enterprise_fee, calculator: Spree::Calculator::FlatRate.new(preferred_amount: 2)) } - let!(:exchange) { create(:exchange, order_cycle: order_cycle, sender: coordinator, receiver: distributor, incoming: false, variants: [product.master]) } + let!(:exchange) { create(:exchange, order_cycle: order_cycle, sender: coordinator, receiver: distributor, incoming: false, variants: [product1.master]) } before { order.reload } @@ -83,6 +143,57 @@ module OpenFoodNetwork end end + + describe "indexed fee retrieval" do + subject { EnterpriseFeeCalculator.new distributor, order_cycle } + let(:order_cycle) { create(:simple_order_cycle, coordinator_fees: [ef_coordinator]) } + let(:distributor) { create(:distributor_enterprise) } + let(:distributor_other) { create(:distributor_enterprise) } + let!(:ef_absent) { create(:enterprise_fee) } + let!(:ef_exchange) { create(:enterprise_fee) } + let!(:ef_coordinator) { create(:enterprise_fee) } + let!(:ef_other_distributor) { create(:enterprise_fee) } + let!(:exchange) { create(:exchange, sender: order_cycle.coordinator, receiver: distributor, order_cycle: order_cycle, enterprise_fees: [ef_exchange], variants: [v]) } + let(:v) { create(:variant) } + let(:indexed_variants) { {v.id => v} } + let(:indexed_enterprise_fees) { subject.instance_variable_get(:@indexed_enterprise_fees) } + + before { subject.instance_variable_set(:@indexed_enterprise_fees, {}) } + + describe "fetching enterprise fees with pre-loaded exchange details" do + it "scopes enterprise fees to those on exchanges for the current order cycle" do + subject.send(:per_item_enterprise_fees_with_exchange_details).should == [ef_exchange] + end + + it "includes the exchange variant id" do + subject.send(:per_item_enterprise_fees_with_exchange_details).first.variant_id.to_i.should == + v.id + end + + it "does not include outgoing exchanges to other distributors" do + create(:exchange, order_cycle: order_cycle, sender: order_cycle.coordinator, receiver: distributor_other, enterprise_fees: [ef_other_distributor], variants: [v]) + + subject.send(:per_item_enterprise_fees_with_exchange_details).should == [ef_exchange] + end + end + + describe "loading exchange fees" do + let(:exchange_fees) { subject.send(:per_item_enterprise_fees_with_exchange_details) } + + it "loads exchange fees" do + subject.send(:load_exchange_fees, exchange_fees) + indexed_enterprise_fees.should == {v.id => [ef_exchange]} + end + end + + describe "loading coordinator fees" do + it "loads coordinator fees" do + subject.send(:load_coordinator_fees) + indexed_enterprise_fees.should == {v.id => [ef_coordinator]} + end + end + end + describe "creating adjustments for a line item" do let(:oc) { OrderCycle.new } let(:variant) { double(:variant) } diff --git a/spec/lib/open_food_network/enterprise_injection_data_spec.rb b/spec/lib/open_food_network/enterprise_injection_data_spec.rb index cb94f2374a..ded13c9a6b 100644 --- a/spec/lib/open_food_network/enterprise_injection_data_spec.rb +++ b/spec/lib/open_food_network/enterprise_injection_data_spec.rb @@ -12,6 +12,11 @@ module OpenFoodNetwork it "only loads activated relatives" do subject.relatives[enterprise.id][:producers].should_not include producer_inactive.id end + + it "loads self where appropiate" do + subject.relatives[producer.id][:producers].should include producer.id + subject.relatives[enterprise.id][:distributors].should include enterprise.id + end end end end diff --git a/spec/lib/open_food_network/lettuce_share_report_spec.rb b/spec/lib/open_food_network/lettuce_share_report_spec.rb new file mode 100644 index 0000000000..a3a82b2f00 --- /dev/null +++ b/spec/lib/open_food_network/lettuce_share_report_spec.rb @@ -0,0 +1,31 @@ +require 'open_food_network/lettuce_share_report' + +module OpenFoodNetwork + describe LettuceShareReport do + let(:user) { create(:user) } + let(:report) { LettuceShareReport.new user } + let(:v) { create(:variant) } + + describe "grower and method" do + it "shows just the producer when there is no certification" do + report.stub(:producer_name) { "Producer" } + report.stub(:certification) { "" } + + report.send(:grower_and_method, v).should == "Producer" + end + + it "shows producer and certification when a certification is present" do + report.stub(:producer_name) { "Producer" } + report.stub(:certification) { "Method" } + + report.send(:grower_and_method, v).should == "Producer (Method)" + end + end + + describe "gst" do + it "handles tax category without rates" do + report.send(:gst, v).should == 0 + end + end + end +end diff --git a/spec/lib/open_food_network/packing_report_spec.rb b/spec/lib/open_food_network/packing_report_spec.rb new file mode 100644 index 0000000000..40e3d44c9e --- /dev/null +++ b/spec/lib/open_food_network/packing_report_spec.rb @@ -0,0 +1,81 @@ +require 'spec_helper' + +include AuthenticationWorkflow + +module OpenFoodNetwork + describe PackingReport do + context "as a site admin" do + let(:user) do + user = create(:user) + user.spree_roles << Spree::Role.find_or_create_by_name!("admin") + user + end + subject { PackingReport.new user } + + describe "fetching orders" do + it "fetches completed orders" do + o1 = create(:order) + o2 = create(:order, completed_at: 1.day.ago) + subject.orders.should == [o2] + end + + it "does not show cancelled orders" do + o1 = create(:order, state: "canceled", completed_at: 1.day.ago) + o2 = create(:order, completed_at: 1.day.ago) + subject.orders.should == [o2] + end + end + end + + context "as an enterprise user" do + let!(:user) { create_enterprise_user } + + subject { PackingReport.new user } + + describe "fetching orders" do + let(:supplier) { create(:supplier_enterprise) } + let(:product) { create(:simple_product, supplier: supplier) } + let(:d1) { create(:distributor_enterprise) } + let(:oc1) { create(:simple_order_cycle) } + let(:order) { create(:order, completed_at: 1.day.ago, order_cycle: oc1, distributor: d1) } + + before do + d1.enterprise_roles.create!(user: user) + end + + it "only shows orders managed by the current user" do + d2 = create(:distributor_enterprise) + d2.enterprise_roles.create!(user: create(:user)) + o2 = create(:order, distributor: d2, completed_at: 1.day.ago) + + subject.orders.should == [order] + end + + it "only shows the selected order cycle" do + oc2 = create(:simple_order_cycle) + order2 = create(:order, order_cycle: oc2) + subject.stub(:params).and_return(order_cycle_id_in: oc1.id) + subject.orders.should == [order] + end + + it "only shows product line items that I am supplying" do + d2 = create(:distributor_enterprise) + create(:enterprise_relationship, parent: supplier, child: d1, permissions_list: [:add_to_order_cycle]) + d2.enterprise_roles.create!(user: create(:user)) + + s2 = create(:supplier_enterprise) + p2 = create(:simple_product, supplier: s2) + + li1 = create(:line_item, product: product) + li2 = create(:line_item, product: p2) + o1 = create(:order, distributor: d1, completed_at: 1.day.ago) + o1.line_items << li1 + o2 = create(:order, distributor: d2, completed_at: 1.day.ago) + o2.line_items << li2 + subject.orders.map{ |o| o.line_items}.flatten.should include li1 + subject.orders.map{ |o| o.line_items}.flatten.should_not include li2 + end + end + end + end +end diff --git a/spec/lib/open_food_network/reports/report_spec.rb b/spec/lib/open_food_network/reports/report_spec.rb new file mode 100644 index 0000000000..0421320119 --- /dev/null +++ b/spec/lib/open_food_network/reports/report_spec.rb @@ -0,0 +1,100 @@ +require 'open_food_network/reports/report' + +module OpenFoodNetwork::Reports + P1 = Proc.new { |o| o[:one] } + P2 = Proc.new { |o| o[:two] } + P3 = Proc.new { |o| o[:three] } + P4 = Proc.new { |o| o[:four] } + + class TestReport < Report + header 'One', 'Two', 'Three', 'Four' + + columns do + column &P1 + column &P2 + column &P3 + column &P4 + end + + organise do + group &P1 + sort &P2 + + organise do + group &P3 + sort &P4 + + summary_row do + column &P1 + column &P4 + end + end + end + end + + class HelperReport < Report + columns do + column { |o| my_helper(o) } + end + + + private + + def self.my_helper(o) + o[:one] + end + end + + + describe Report do + let(:report) { TestReport.new } + let(:helper_report) { HelperReport.new } + let(:rules_head) { TestReport._rules_head } + let(:data) { {one: 1, two: 2, three: 3, four: 4} } + + it "returns the header" do + report.header.should == %w(One Two Three Four) + end + + it "returns columns as an array of procs" do + report.columns[0].call(data).should == 1 + report.columns[1].call(data).should == 2 + report.columns[2].call(data).should == 3 + report.columns[3].call(data).should == 4 + end + + it "supports helpers when outputting columns" do + helper_report.columns[0].call(data).should == 1 + end + + describe "rules" do + let(:group_by) { rules_head.to_h[:group_by] } + let(:sort_by) { rules_head.to_h[:sort_by] } + let(:next_group_by) { rules_head.next.to_h[:group_by] } + let(:next_sort_by) { rules_head.next.to_h[:sort_by] } + let(:next_summary_columns) { rules_head.next.to_h[:summary_columns] } + + it "constructs the head of the rules list" do + group_by.call(data).should == 1 + sort_by.call(data).should == 2 + end + + it "constructs nested rules" do + next_group_by.call(data).should == 3 + next_sort_by.call(data).should == 4 + end + + it "constructs summary columns for rules" do + next_summary_columns[0].call(data).should == 1 + next_summary_columns[1].call(data).should == 4 + end + end + + describe "outputting rules" do + it "outputs the rules" do + report.rules.should == [{group_by: P1, sort_by: P2}, + {group_by: P3, sort_by: P4, summary_columns: [P1, P4]}] + end + end + end +end diff --git a/spec/lib/open_food_network/reports/row_spec.rb b/spec/lib/open_food_network/reports/row_spec.rb new file mode 100644 index 0000000000..deaa84e491 --- /dev/null +++ b/spec/lib/open_food_network/reports/row_spec.rb @@ -0,0 +1,16 @@ +require 'open_food_network/reports/row' + +module OpenFoodNetwork::Reports + describe Row do + let(:row) { Row.new } + let(:proc) { Proc.new {} } + + it "can define a number of columns and return them as an array" do + row.column &proc + row.column &proc + row.column &proc + + row.to_a.should == [proc, proc, proc] + end + end +end diff --git a/spec/lib/open_food_network/reports/rule_spec.rb b/spec/lib/open_food_network/reports/rule_spec.rb new file mode 100644 index 0000000000..0e7a1d979b --- /dev/null +++ b/spec/lib/open_food_network/reports/rule_spec.rb @@ -0,0 +1,36 @@ +require 'open_food_network/reports/rule' + +module OpenFoodNetwork::Reports + describe Rule do + let(:rule) { Rule.new } + let(:proc) { Proc.new {} } + + it "can define a group proc and return it in a hash" do + rule.group &proc + rule.to_h.should == {group_by: proc, sort_by: nil} + end + + it "can define a sort proc and return it in a hash" do + rule.sort &proc + rule.to_h.should == {group_by: nil, sort_by: proc} + end + + it "can define a nested rule" do + rule.organise &proc + rule.next.should be_a Rule + end + + it "can define a summary row and return it in a hash" do + rule.summary_row do + column {} + column {} + column {} + end + + rule.to_h[:summary_columns].count.should == 3 + rule.to_h[:summary_columns][0].should be_a Proc + rule.to_h[:summary_columns][1].should be_a Proc + rule.to_h[:summary_columns][2].should be_a Proc + end + end +end diff --git a/spec/lib/open_food_network/scope_variant_to_hub_spec.rb b/spec/lib/open_food_network/scope_variant_to_hub_spec.rb index 429bfbe082..2a249f9c7a 100644 --- a/spec/lib/open_food_network/scope_variant_to_hub_spec.rb +++ b/spec/lib/open_food_network/scope_variant_to_hub_spec.rb @@ -5,16 +5,18 @@ module OpenFoodNetwork let(:hub) { create(:distributor_enterprise) } let(:v) { create(:variant, price: 11.11, count_on_hand: 1) } let(:vo) { create(:variant_override, hub: hub, variant: v, price: 22.22, count_on_hand: 2) } + let(:vo_price_only) { create(:variant_override, hub: hub, variant: v, price: 22.22, count_on_hand: nil) } + let(:scoper) { ScopeVariantToHub.new(hub) } describe "overriding price" do it "returns the overridden price when one is present" do vo - v.scope_to_hub hub + scoper.scope v v.price.should == 22.22 end it "returns the variant's price otherwise" do - v.scope_to_hub hub + scoper.scope v v.price.should == 11.11 end end @@ -22,12 +24,12 @@ module OpenFoodNetwork describe "overriding price_in" do it "returns the overridden price when one is present" do vo - v.scope_to_hub hub + scoper.scope v v.price_in('AUD').amount.should == 22.22 end it "returns the variant's price otherwise" do - v.scope_to_hub hub + scoper.scope v v.price_in('AUD').amount.should == 11.11 end end @@ -35,14 +37,35 @@ module OpenFoodNetwork describe "overriding stock levels" do it "returns the overridden stock level when one is present" do vo - v.scope_to_hub hub + scoper.scope v v.count_on_hand.should == 2 end it "returns the variant's stock level otherwise" do - v.scope_to_hub hub + scoper.scope v v.count_on_hand.should == 1 end + + describe "overriding stock on an on_demand variant" do + let(:v) { create(:variant, price: 11.11, on_demand: true) } + + it "clears on_demand when the stock is overridden" do + vo + scoper.scope v + v.on_demand.should be_false + end + + it "does not clear on_demand when only the price is overridden" do + vo_price_only + scoper.scope v + v.on_demand.should be_true + end + + it "does not clear on_demand when there is no override" do + scoper.scope v + v.on_demand.should be_true + end + end end end end diff --git a/spec/lib/open_food_network/xero_invoices_report_spec.rb b/spec/lib/open_food_network/xero_invoices_report_spec.rb index 8551d663a8..5326d31149 100644 --- a/spec/lib/open_food_network/xero_invoices_report_spec.rb +++ b/spec/lib/open_food_network/xero_invoices_report_spec.rb @@ -12,7 +12,71 @@ module OpenFoodNetwork it "uses defaults when blank params are passed" do report.instance_variable_get(:@opts).should == {invoice_date: Date.civil(2015, 5, 5), due_date: Date.civil(2015, 5, 19), - account_code: 'food sales'} + account_code: 'food sales', + report_type: 'summary'} + end + end + + describe "summary rows" do + let(:report) { XeroInvoicesReport.new [], {initial_invoice_number: '', invoice_date: '', due_date: '', account_code: ''} } + let(:order) { double(:order) } + let(:summary_rows) { report.send(:summary_rows_for_order, order, 1, {}) } + + before do + report.stub(:produce_summary_rows) { ['produce'] } + report.stub(:fee_summary_rows) { ['fee'] } + report.stub(:shipping_summary_rows) { ['shipping'] } + order.stub(:account_invoice?) { false } + end + + it "displays produce summary rows when summary report" do + report.stub(:detail?) { false } + summary_rows.should include 'produce' + end + + it "does not display produce summary rows when detail report" do + report.stub(:detail?) { true } + summary_rows.should_not include 'produce' + end + + it "displays fee summary rows when summary report" do + report.stub(:detail?) { false } + order.stub(:account_invoice?) { true } + summary_rows.should include 'fee' + end + + it "displays fee summary rows when this is not an account invoice" do + report.stub(:detail?) { true } + order.stub(:account_invoice?) { false } + summary_rows.should include 'fee' + end + + it "does not display fee summary rows when this is a detail report for an account invoice" do + report.stub(:detail?) { true } + order.stub(:account_invoice?) { true } + summary_rows.should_not include 'fee' + end + + it "always displays shipping summary rows" do + summary_rows.should include 'shipping' + end + end + + describe "finding account invoice adjustments" do + let(:report) { XeroInvoicesReport.new [], {initial_invoice_number: '', invoice_date: '', due_date: '', account_code: ''} } + let!(:order) { create(:order) } + let(:billable_period) { create(:billable_period) } + let(:shipping_method) { create(:shipping_method) } + let!(:adj_invoice) { create(:adjustment, adjustable: order, label: 'Account invoice item', source: billable_period) } + let!(:adj_shipping) { create(:adjustment, adjustable: order, label: "Shipping", originator: shipping_method) } + + it "returns BillablePeriod adjustments only" do + report.send(:account_invoice_adjustments, order).should == [adj_invoice] + end + + it "excludes adjustments where the source is missing" do + billable_period.destroy + report.send(:account_invoice_adjustments, order).should be_empty end end diff --git a/spec/models/account_invoice_spec.rb b/spec/models/account_invoice_spec.rb new file mode 100644 index 0000000000..0473d1d455 --- /dev/null +++ b/spec/models/account_invoice_spec.rb @@ -0,0 +1,5 @@ +require 'spec_helper' + +describe AccountInvoice do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/models/billable_period_spec.rb b/spec/models/billable_period_spec.rb new file mode 100644 index 0000000000..1d5cd19507 --- /dev/null +++ b/spec/models/billable_period_spec.rb @@ -0,0 +1,27 @@ +require 'spec_helper' + +describe Customer, type: :model do + describe 'ensure_correct_adjustment' do + let!(:start_of_july) { Time.now.beginning_of_year + 6.months } + let!(:user) { create(:user) } + let!(:invoice) { create(:order, user: user) } + let!(:billable_period) { create(:billable_period, owner: user, begins_at: start_of_july, ends_at: start_of_july + 12.days) } + + before do + allow(billable_period).to receive(:bill) { 99 } + allow(billable_period).to receive(:adjustment_label) { "Label for adjustment" } + Spree::Config.set({ account_bill_inc_tax: true }) + Spree::Config.set({ account_bill_tax_rate: 0.1 }) + end + + context "when no adjustment currently exists" do + it "creates an adjustment on the given order" do + expect(invoice.total_tax).to eq 0.0 + expect(billable_period.adjustment).to be nil + billable_period.ensure_correct_adjustment_for(invoice) + expect(billable_period.adjustment).to be_a Spree::Adjustment + expect(invoice.total_tax).to eq 9.0 + end + end + end +end diff --git a/spec/models/enterprise_group_spec.rb b/spec/models/enterprise_group_spec.rb index 45e7b0692c..63b3ef58ae 100644 --- a/spec/models/enterprise_group_spec.rb +++ b/spec/models/enterprise_group_spec.rb @@ -51,7 +51,7 @@ describe EnterpriseGroup do # it "can have an image" do # eg = create(:enterprise_group) - # image_file = File.open(File.expand_path('../../../app/assets/images/logo.jpg', __FILE__)) + # image_file = File.open(File.expand_path('../../../app/assets/images/logo-white.png', __FILE__)) # image = Spree::Image.create(viewable_id: eg.id, viewable_type: 'EnterpriseGroup', attachment: image_file) # eg.reload.image.should == image # end diff --git a/spec/models/enterprise_relationship_spec.rb b/spec/models/enterprise_relationship_spec.rb index c732d50849..fcdfa8b250 100644 --- a/spec/models/enterprise_relationship_spec.rb +++ b/spec/models/enterprise_relationship_spec.rb @@ -72,14 +72,20 @@ describe EnterpriseRelationship do describe "finding relatives" do let(:e1) { create(:supplier_enterprise) } - let(:e2) { create(:supplier_enterprise, sells: 'any') } + let(:e2) { create(:distributor_enterprise) } let!(:er) { create(:enterprise_relationship, parent: e1, child: e2) } let(:er_reverse) { create(:enterprise_relationship, parent: e2, child: e1) } + it "includes self where appropriate" do + EnterpriseRelationship.relatives[e2.id][:distributors].should include e2.id + EnterpriseRelationship.relatives[e2.id][:producers].should_not include e2.id + end + it "categorises enterprises into distributors and producers" do + e2.update_attribute :is_primary_producer, true EnterpriseRelationship.relatives.should == - {e1.id => {distributors: Set.new([e2.id]), producers: Set.new([e2.id])}, - e2.id => {distributors: Set.new([]), producers: Set.new([e1.id])}} + {e1.id => {distributors: Set.new([e2.id]), producers: Set.new([e1.id, e2.id])}, + e2.id => {distributors: Set.new([e2.id]), producers: Set.new([e2.id, e1.id])}} end it "finds inactive enterprises by default" do diff --git a/spec/models/enterprise_spec.rb b/spec/models/enterprise_spec.rb index 388d80258e..f90cebaae4 100644 --- a/spec/models/enterprise_spec.rb +++ b/spec/models/enterprise_spec.rb @@ -109,6 +109,17 @@ describe Enterprise do Spree::Product.where(id: p.id).should be_empty end + it "destroys relationships upon destroy" do + e = create(:enterprise) + e_other = create(:enterprise) + er1 = create(:enterprise_relationship, parent: e, child: e_other) + er2 = create(:enterprise_relationship, child: e, parent: e_other) + + e.destroy + + EnterpriseRelationship.where(id: [er1, er2]).should be_empty + end + describe "relationships to other enterprises" do let(:e) { create(:distributor_enterprise) } let(:p) { create(:supplier_enterprise) } @@ -179,6 +190,31 @@ describe Enterprise do }.to raise_error ActiveRecord::RecordInvalid, "Validation failed: Owner can't be blank" end + describe "name uniqueness" do + let(:owner) { create(:user, email: 'owner@example.com') } + let!(:enterprise) { create(:enterprise, name: 'Enterprise', owner: owner) } + + it "prevents duplicate names for new records" do + e = Enterprise.new name: enterprise.name + e.should_not be_valid + e.errors[:name].first.should == + "has already been taken. If this is your enterprise and you would like to claim ownership, please contact the current manager of this profile at owner@example.com." + end + + it "prevents duplicate names for existing records" do + e = create(:enterprise, name: 'foo') + e.name = enterprise.name + e.should_not be_valid + e.errors[:name].first.should == + "has already been taken. If this is your enterprise and you would like to claim ownership, please contact the current manager of this profile at owner@example.com." + end + + it "does not prohibit the saving of an enterprise with no name clash" do + enterprise.email = 'new@email.com' + enterprise.should be_valid + end + end + describe "preferred_shopfront_taxon_order" do it "empty strings are valid" do enterprise = build(:enterprise, preferred_shopfront_taxon_order: "") diff --git a/spec/models/order_cycle_spec.rb b/spec/models/order_cycle_spec.rb index c05d3d00d5..f87872a44d 100644 --- a/spec/models/order_cycle_spec.rb +++ b/spec/models/order_cycle_spec.rb @@ -218,6 +218,10 @@ describe OrderCycle do @oc.variants.should match_array [@p0.master, @p1.master, @p2.master, @p2_v] end + it "returns the correct count of variants" do + @oc.variants.count.should == 4 + end + it "reports on the variants distributed" do @oc.distributed_variants.should match_array [@p1.master, @p2.master, @p2_v] end diff --git a/spec/models/spree/ability_spec.rb b/spec/models/spree/ability_spec.rb index 0ea8c820f1..6b60707775 100644 --- a/spec/models/spree/ability_spec.rb +++ b/spec/models/spree/ability_spec.rb @@ -217,7 +217,7 @@ module Spree end it "should not be able to read other reports" do - should_not have_ability([:sales_total, :group_buys, :payments, :orders_and_distributors, :users_and_enterprises], for: :report) + should_not have_ability([:sales_total, :group_buys, :payments, :orders_and_distributors, :users_and_enterprises, :xero_invoices], for: :report) end it "should not be able to access customer actions" do @@ -298,11 +298,11 @@ module Spree let!(:er_pd) { create(:enterprise_relationship, parent: d_related, child: d1, permissions_list: [:edit_profile]) } it "should be able to edit enterprises it manages" do - should have_ability([:read, :edit, :update, :bulk_update, :set_sells, :resend_confirmation], for: d1) + should have_ability([:read, :edit, :update, :bulk_update, :resend_confirmation], for: d1) end it "should be able to edit enterprises it has permission to" do - should have_ability([:read, :edit, :update, :bulk_update, :set_sells, :resend_confirmation], for: d_related) + should have_ability([:read, :edit, :update, :bulk_update, :resend_confirmation], for: d_related) end it "should be able to manage shipping methods, payment methods and enterprise fees for enterprises it manages" do @@ -404,7 +404,7 @@ module Spree end it "should be able to read some reports" do - should have_ability([:admin, :index, :customers, :sales_tax, :group_buys, :bulk_coop, :payments, :orders_and_distributors, :orders_and_fulfillment, :products_and_inventory, :order_cycle_management], for: :report) + should have_ability([:admin, :index, :customers, :sales_tax, :group_buys, :bulk_coop, :payments, :orders_and_distributors, :orders_and_fulfillment, :products_and_inventory, :order_cycle_management, :xero_invoices], for: :report) end it "should not be able to read other reports" do @@ -477,12 +477,20 @@ module Spree user end + it 'should have the ability to view the admin account page' do + should have_ability([:admin, :show], for: :account) + end + it 'should have the ability to read and edit enterprises that I manage' do - should have_ability([:read, :edit, :update, :bulk_update, :set_sells], for: s1) + should have_ability([:read, :edit, :update, :bulk_update], for: s1) end it 'should not have the ability to read and edit enterprises that I do not manage' do - should_not have_ability([:read, :edit, :update, :bulk_update, :set_sells], for: s2) + should_not have_ability([:read, :edit, :update, :bulk_update], for: s2) + end + + it 'should not have the ability to welcome and register enterprises that I do not own' do + should_not have_ability([:welcome, :register], for: s1) end it 'should have the ability administrate and create enterpises' do @@ -494,6 +502,18 @@ module Spree should_not have_ability([:users], for: :search) end end + + context 'enterprise owner' do + let (:user) { s1.owner } + + it 'should have the ability to welcome and register enterprises that I own' do + should have_ability([:welcome, :register], for: s1) + end + + it 'should have the ability to view the admin account page' do + should have_ability([:admin, :show], for: :account) + end + end end end end diff --git a/spec/models/spree/adjustment_spec.rb b/spec/models/spree/adjustment_spec.rb index bd952f2e9c..512e720f54 100644 --- a/spec/models/spree/adjustment_spec.rb +++ b/spec/models/spree/adjustment_spec.rb @@ -5,18 +5,30 @@ module Spree adjustment.metadata.should be end - describe "finding adjustments with and without tax included" do + describe "querying included tax" do let!(:adjustment_with_tax) { create(:adjustment, included_tax: 123) } let!(:adjustment_without_tax) { create(:adjustment, included_tax: 0) } - it "finds adjustments with tax" do - Adjustment.with_tax.should include adjustment_with_tax - Adjustment.with_tax.should_not include adjustment_without_tax + describe "finding adjustments with and without tax included" do + it "finds adjustments with tax" do + Adjustment.with_tax.should include adjustment_with_tax + Adjustment.with_tax.should_not include adjustment_without_tax + end + + it "finds adjustments without tax" do + Adjustment.without_tax.should include adjustment_without_tax + Adjustment.without_tax.should_not include adjustment_with_tax + end end - it "finds adjustments without tax" do - Adjustment.without_tax.should include adjustment_without_tax - Adjustment.without_tax.should_not include adjustment_with_tax + describe "checking if an adjustment includes tax" do + it "returns true when it has > 0 tax" do + adjustment_with_tax.should have_tax + end + + it "returns false when it has 0 tax" do + adjustment_without_tax.should_not have_tax + end end end diff --git a/spec/models/spree/line_item_spec.rb b/spec/models/spree/line_item_spec.rb index a61f4e67fc..f9c0e97f20 100644 --- a/spec/models/spree/line_item_spec.rb +++ b/spec/models/spree/line_item_spec.rb @@ -63,5 +63,20 @@ module Spree li.amount_with_adjustments.should == 122.22 end end + + describe "checking if a line item has tax included" do + let(:li_no_tax) { create(:line_item) } + let(:li_tax) { create(:line_item) } + let(:tax_rate) { create(:tax_rate, calculator: Spree::Calculator::DefaultTax.new) } + let!(:adjustment) { create(:adjustment, adjustable: li_tax, originator: tax_rate, label: "TR", amount: 123, included_tax: 10.00) } + + it "returns true when it does" do + li_tax.should have_tax + end + + it "returns false otherwise" do + li_no_tax.should_not have_tax + end + end end end diff --git a/spec/models/spree/order_populator_spec.rb b/spec/models/spree/order_populator_spec.rb index 5602e46da5..ef72106e16 100644 --- a/spec/models/spree/order_populator_spec.rb +++ b/spec/models/spree/order_populator_spec.rb @@ -21,7 +21,7 @@ module Spree op.populate(params).should be_false op.errors.to_a.should == ["That distributor or order cycle can't supply all the products in your cart. Please choose another."] end - + it "empties the order if override is true" do op.stub(:distribution_can_supply_products_in_cart).and_return true order.stub(:with_lock).and_yield @@ -38,7 +38,7 @@ module Spree it "attempts cart add with max_quantity" do op.stub(:distribution_can_supply_products_in_cart).and_return true order.should_receive(:empty!) - params = {variants: {"1" => {quantity: 1, max_quantity: 2}}} + params = {variants: {"1" => {quantity: 1, max_quantity: 2}}} order.stub(:with_lock).and_yield op.should_receive(:attempt_cart_add).with("1", 1, 2).and_return true op.populate(params, true) @@ -48,9 +48,9 @@ module Spree describe "attempt_cart_add" do it "performs additional validations" do variant = double(:variant) - variant.stub(:scope_to_hub) quantity = 123 Spree::Variant.stub(:find).and_return(variant) + VariantOverride.stub(:for).and_return(nil) op.should_receive(:check_stock_levels).with(variant, quantity).and_return(true) op.should_receive(:check_order_cycle_provided_for).with(variant).and_return(true) diff --git a/spec/models/spree/order_spec.rb b/spec/models/spree/order_spec.rb index dddaf554a2..d86916e24e 100644 --- a/spec/models/spree/order_spec.rb +++ b/spec/models/spree/order_spec.rb @@ -504,12 +504,41 @@ describe Spree::Order do end end + describe "checking if an order is an account invoice" do + let(:accounts_distributor) { create(:distributor_enterprise) } + let(:order_account_invoice) { create(:order, distributor: accounts_distributor) } + let(:order_general) { create(:order, distributor: create(:distributor_enterprise)) } + + before do + Spree::Config.accounts_distributor_id = accounts_distributor.id + end + + it "returns true when the order is distributed by the accounts distributor" do + order_account_invoice.should be_account_invoice + end + + it "returns false otherwise" do + order_general.should_not be_account_invoice + end + end + describe "sending confirmation emails" do + let!(:distributor) { create(:distributor_enterprise) } + let!(:order) { create(:order, distributor: distributor) } + it "sends confirmation emails" do expect do - create(:order).deliver_order_confirmation_email + order.deliver_order_confirmation_email end.to enqueue_job ConfirmOrderJob end + + it "does not send confirmation emails when distributor is the accounts_distributor" do + Spree::Config.set({ accounts_distributor_id: distributor.id }) + + expect do + order.deliver_order_confirmation_email + end.to_not enqueue_job ConfirmOrderJob + end end describe "associating a customer" do diff --git a/spec/models/spree/preferences/file_configuration_spec.rb b/spec/models/spree/preferences/file_configuration_spec.rb new file mode 100644 index 0000000000..eb18b23e12 --- /dev/null +++ b/spec/models/spree/preferences/file_configuration_spec.rb @@ -0,0 +1,57 @@ +require 'spec_helper' + +module Spree + module Preferences + class TestConfiguration < FileConfiguration + preference :name, :string + + include OpenFoodNetwork::Paperclippable + preference :logo, :file + has_attached_file :logo + end + + describe FileConfiguration do + let(:c) { TestConfiguration.new } + + describe "getting preferences" do + it "returns regular preferences" do + c.name = 'foo' + c.get_preference(:name).should == 'foo' + end + + it "returns file preferences" do + c.get_preference(:logo).should be_a Paperclip::Attachment + end + + it "returns regular preferences via []" do + c.name = 'foo' + c[:name].should == 'foo' + end + + it "returns file preferences via []" do + c[:logo].should be_a Paperclip::Attachment + end + end + + describe "getting preference types" do + it "returns regular preference types" do + c.preference_type(:name).should == :string + end + + it "returns file preference types" do + c.preference_type(:logo).should == :file + end + end + + describe "respond_to?" do + it "responds to preference getters" do + c.respond_to?(:name).should be_true + end + + it "responds to preference setters" do + c.respond_to?(:name=).should be_true + end + end + end + end +end diff --git a/spec/models/spree/product_spec.rb b/spec/models/spree/product_spec.rb index 505ac8ee2e..335caca4ad 100644 --- a/spec/models/spree/product_spec.rb +++ b/spec/models/spree/product_spec.rb @@ -40,18 +40,18 @@ module Spree describe "tax category" do context "when a tax category is required" do - before { Spree::Config.products_require_tax_category = true } - it "is invalid when a tax category is not provided" do - build(:product, tax_category_id: nil).should_not be_valid + with_products_require_tax_category(true) do + build(:product, tax_category_id: nil).should_not be_valid + end end end context "when a tax category is not required" do - before { Spree::Config.products_require_tax_category = false } - it "is valid when a tax category is not provided" do - build(:product, tax_category_id: nil).should be_valid + with_products_require_tax_category(false) do + build(:product, tax_category_id: nil).should be_valid + end end end end @@ -205,7 +205,7 @@ module Spree Product.in_distributor(d1).should == [p1] end - it "doesn't show products listed in the incoming exchange only", :future => true do + it "doesn't show products listed in the incoming exchange only" do s = create(:supplier_enterprise) c = create(:distributor_enterprise) d = create(:distributor_enterprise) diff --git a/spec/models/spree/user_spec.rb b/spec/models/spree/user_spec.rb index 848febb9c5..7fefd56afb 100644 --- a/spec/models/spree/user_spec.rb +++ b/spec/models/spree/user_spec.rb @@ -56,9 +56,9 @@ describe Spree.user_class do end describe "known_users" do - let!(:u1) { create_enterprise_user } - let!(:u2) { create_enterprise_user } - let!(:u3) { create_enterprise_user } + let!(:u1) { create(:user) } + let!(:u2) { create(:user) } + let!(:u3) { create(:user) } let!(:e1) { create(:enterprise, owner: u1, users: [u1, u2]) } describe "as an enterprise user" do @@ -73,6 +73,7 @@ describe Spree.user_class do describe "as admin" do let(:admin) { quick_login_as_admin } + it "returns all users" do expect(admin.known_users).to include u1, u2, u3 end diff --git a/spec/models/spree/variant_spec.rb b/spec/models/spree/variant_spec.rb index 73aa8900d1..69dda85ff5 100644 --- a/spec/models/spree/variant_spec.rb +++ b/spec/models/spree/variant_spec.rb @@ -104,6 +104,17 @@ module Spree end end + describe "indexing variants by id" do + let!(:v1) { create(:variant) } + let!(:v2) { create(:variant) } + let!(:v3) { create(:variant) } + + it "indexes variants by id" do + Variant.where(id: [v1, v2, v3]).indexed.should == + {v1.id => v1, v2.id => v2, v3.id => v3} + end + end + describe "generating the full name" do let(:v) { Variant.new } @@ -142,6 +153,37 @@ module Spree end end + describe "generating the product and variant name" do + let(:v) { Variant.new } + let(:p) { double(:product, name: 'product') } + + before do + v.stub(:product) { p } + v.stub(:name_to_display) { p.name } + v.stub(:options_text) { nil } + end + + it "returns the product name only when there's no extra info" do + v.product_and_variant_name.should == 'product' + end + + it "also shows the name to display when different to the product name" do + v.stub(:name_to_display) { 'NTD' } + v.product_and_variant_name.should == 'product - NTD' + end + + it "shows the options text when present" do + v.stub(:options_text) { 'OT' } + v.product_and_variant_name.should == 'product (OT)' + end + + it "displays all attributes" do + v.stub(:name_to_display) { 'NTD' } + v.stub(:options_text) { 'OT' } + v.product_and_variant_name.should == 'product - NTD (OT)' + end + end + describe "calculating the price with enterprise fees" do it "returns the price plus the fees" do distributor = double(:distributor) diff --git a/spec/models/variant_override_spec.rb b/spec/models/variant_override_spec.rb index 3a6bd9b1df..e9f22a33be 100644 --- a/spec/models/variant_override_spec.rb +++ b/spec/models/variant_override_spec.rb @@ -14,8 +14,16 @@ describe VariantOverride do it "finds variant overrides for a set of hubs" do VariantOverride.for_hubs([hub1, hub2]).should match_array [vo1, vo2] end + + describe "fetching variant overrides indexed by variant" do + it "gets indexed variant overrides for one hub" do + VariantOverride.indexed(hub1).should == {v => vo1} + VariantOverride.indexed(hub2).should == {v => vo2} + end + end end + describe "looking up prices" do it "returns the numeric price when present" do VariantOverride.create!(variant: variant, hub: hub, price: 12.34) diff --git a/spec/performance/shop_controller_spec.rb b/spec/performance/shop_controller_spec.rb index 984581a2ab..6794c70da5 100644 --- a/spec/performance/shop_controller_spec.rb +++ b/spec/performance/shop_controller_spec.rb @@ -8,11 +8,12 @@ describe ShopController, type: :controller, performance: true do before do controller.stub(:current_distributor) { d } controller.stub(:current_order_cycle) { order_cycle } + Spree::Config.currency = 'AUD' end describe "fetching products" do let(:exchange) { order_cycle.exchanges.to_enterprises(d).outgoing.first } - let(:image) { File.open(File.expand_path('../../../app/assets/images/logo.jpg', __FILE__)) } + let(:image) { File.open(File.expand_path('../../../app/assets/images/logo-white.png', __FILE__)) } before do 11.times do diff --git a/spec/serializers/admin/index_enterprise_serializer_spec.rb b/spec/serializers/admin/index_enterprise_serializer_spec.rb new file mode 100644 index 0000000000..3651f53f8d --- /dev/null +++ b/spec/serializers/admin/index_enterprise_serializer_spec.rb @@ -0,0 +1,34 @@ +describe Api::Admin::IndexEnterpriseSerializer do + include AuthenticationWorkflow + + let(:enterprise) { create(:distributor_enterprise) } + context "when spree_current_user is a manager" do + let(:user) { create_enterprise_user } + before do + user.enterprise_roles.create(enterprise: enterprise) + end + + it "sets 'owned' to false" do + serializer = Api::Admin::IndexEnterpriseSerializer.new enterprise, spree_current_user: user + serializer.to_json.should match "\"owned\":false" + end + end + + context "when spree_current_user is " do + let(:user) { enterprise.owner } + + it "sets 'owned' to true" do + serializer = Api::Admin::IndexEnterpriseSerializer.new enterprise, spree_current_user: user + serializer.to_json.should match "\"owned\":true" + end + end + + context "when spree_current_user is the owner" do + let(:user) { create(:admin_user) } + + it "sets 'owned' to true" do + serializer = Api::Admin::IndexEnterpriseSerializer.new enterprise, spree_current_user: user + serializer.to_json.should match "\"owned\":true" + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index a911fe7742..a309037ccb 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -12,6 +12,8 @@ require File.expand_path("../../config/environment", __FILE__) require 'rspec/rails' require 'capybara' require 'database_cleaner' +require 'rspec/retry' +require 'paper_trail/frameworks/rspec' # Allow connections to phantomjs/selenium whilst raising errors # when connecting to external sites @@ -33,7 +35,7 @@ require 'capybara/poltergeist' Capybara.javascript_driver = :poltergeist Capybara.register_driver :poltergeist do |app| - options = {phantomjs_options: ['--load-images=no'], window_size: [1280, 800], timeout: 1.minute} + options = {phantomjs_options: ['--load-images=no'], window_size: [1280, 800], timeout: 2.minutes} # Extend poltergeist's timeout to allow ample time to use pry in browser thread #options.merge! {timeout: 5.minutes} # Enable the remote inspector: Use page.driver.debug to open a remote debugger in chrome @@ -70,16 +72,27 @@ RSpec.configure do |config| # Filters config.filter_run_excluding :skip => true, :future => true, :to_figure_out => true + # Retry + config.verbose_retry = true + # DatabaseCleaner config.before(:suite) { DatabaseCleaner.clean_with :deletion, {except: ['spree_countries', 'spree_states']} } config.before(:each) { DatabaseCleaner.strategy = :transaction } config.before(:each, js: true) { DatabaseCleaner.strategy = :deletion, {except: ['spree_countries', 'spree_states']} } config.before(:each) { DatabaseCleaner.start } config.after(:each) { DatabaseCleaner.clean } + config.after(:each, js:true) do + Capybara.reset_sessions! + RackRequestBlocker.wait_for_requests_complete + DatabaseCleaner.clean + end # Geocoding config.before(:each) { Spree::Address.any_instance.stub(:geocode).and_return([1,1]) } + # Ensure we start with consistent config settings + config.before(:each) { Spree::Config.products_require_tax_category = false } + # Helpers config.include Rails.application.routes.url_helpers config.include Spree::UrlHelpers @@ -91,6 +104,7 @@ RSpec.configure do |config| config.include OpenFoodNetwork::ControllerHelper, :type => :controller config.include OpenFoodNetwork::FeatureToggleHelper config.include OpenFoodNetwork::EnterpriseGroupsHelper + config.include OpenFoodNetwork::ProductsHelper config.include OpenFoodNetwork::DistributionHelper config.include OpenFoodNetwork::HtmlHelper config.include ActionView::Helpers::DateHelper diff --git a/spec/support/products_helper.rb b/spec/support/products_helper.rb new file mode 100644 index 0000000000..8e9ebc1c98 --- /dev/null +++ b/spec/support/products_helper.rb @@ -0,0 +1,12 @@ +module OpenFoodNetwork + module ProductsHelper + def with_products_require_tax_category(value) + original_value = Spree::Config.products_require_tax_category + + Spree::Config.products_require_tax_category = value + yield + ensure + Spree::Config.products_require_tax_category = original_value + end + end +end diff --git a/spec/support/request/web_helper.rb b/spec/support/request/web_helper.rb index db8b3af51c..15c586561c 100644 --- a/spec/support/request/web_helper.rb +++ b/spec/support/request/web_helper.rb @@ -93,19 +93,11 @@ module WebHelper errors.map(&:text) end - def handle_js_confirm(accept=true, debug=false) + def handle_js_confirm(accept=true) page.evaluate_script "window.confirm = function(msg) { return #{!!accept }; }" yield end - def handle_webdriver_random_failure(retry_times = 3) - begin - yield - rescue Selenium::WebDriver::Error::InvalidSelectorError => e - e.message =~ /nsIDOMXPathEvaluator.createNSResolver/ ? (retry if (retry_times -= 1 ) > 0) : raise - end - end - def click_dialog_button(button_content) page.find(:xpath, "//div[@class=\"ui-dialog-buttonset\"]//span[contains(text(),\"#{button_content}\")]").click end @@ -158,4 +150,3 @@ module WebHelper wait_until { page.evaluate_script("$.active") == 0 } end end -