diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000000..76ce58fccf --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,61 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +## Description + + + +## Expected Behavior + + + +## Actual Behaviour + + + +## Steps to Reproduce + + + +1. +2. +3. +4. + +## Animated Gif/Screenshot + + + +## Context + + + +## Severity + + +## Your Environment + + +* Version used: +* Browser name and version: +* Operating System and version (desktop or mobile): +* OFN Platform instance where you discovered the bug, and which version of the software they are using. + +## Possible Fix + diff --git a/.github/ISSUE_TEMPLATE/feature-template.md b/.github/ISSUE_TEMPLATE/feature-template.md new file mode 100644 index 0000000000..9fb6cc1741 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-template.md @@ -0,0 +1,35 @@ +--- +name: Feature template +about: Create feature epics that detail the larger feature or functionality to be + delivered. +title: '' +labels: '' +assignees: '' + +--- + +## What is the problem we are solving + + +## Success factors = expected outcome + + +## Useful information for inception + + +## Link to the "Product Development - Backlog" item in Discourse + + Add a custom footer + Pages 70 +Home +Development environment setup + +macOS (Sierra, HighSierra and Mojave) +OS X (El Capitan) +OS X (Mavericks) +Ubuntu +On Heroku +Rubocop +General guidelines + +Spree Commerce customisation diff --git a/.github/ISSUE_TEMPLATE/story-template.md b/.github/ISSUE_TEMPLATE/story-template.md new file mode 100644 index 0000000000..48a1c33493 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/story-template.md @@ -0,0 +1,18 @@ +--- +name: Story template +about: Create stories that are small chunks of work that devs will pick up and deliver +title: '' +labels: '' +assignees: '' + +--- + +**## Description** + + +**## Acceptance Criteria** + diff --git a/.travis.yml b/.travis.yml index db818fad23..9fd9d0934c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -39,7 +39,7 @@ before_script: script: - 'if [ "$KARMA" = "true" ]; then bundle exec rake karma:run; else echo "Skipping karma run"; fi' - - 'if [ "$RSPEC_ENGINES" = "true" ]; then bundle exec rake openfoodnetwork:specs:engines:rspec; else echo "Skipping RSpec run in engines"; fi' + - 'if [ "$RSPEC_ENGINES" = "true" ]; then bundle exec rake ofn:specs:engines:rspec; else echo "Skipping RSpec run in engines"; fi' - "bundle exec rake 'knapsack:rspec[--format progress --tag ~performance]'" after_success: diff --git a/GETTING_STARTED.md b/GETTING_STARTED.md index 5a2e7eea54..c30a70c13d 100644 --- a/GETTING_STARTED.md +++ b/GETTING_STARTED.md @@ -75,7 +75,7 @@ Then the main application tests can be run with: The tests of all custom engines can be run with: - bundle exec rake openfoodnetwork:specs:engines:rspec + bundle exec rake ofn:specs:engines:rspec Note: If your OS is not explicitly supported in the setup guides then not all tests may pass. However, you may still be able to develop. Get in touch with the [#dev][slack-dev] channel on Slack to troubleshoot issues and determine if they will preclude you from contributing to OFN. diff --git a/Gemfile b/Gemfile index e36e5cf274..2865321201 100644 --- a/Gemfile +++ b/Gemfile @@ -22,13 +22,13 @@ gem 'spree_auth_devise', github: 'openfoodfoundation/spree_auth_devise', branch: # - Change type of password from string to password to hide it in the form gem 'spree_paypal_express', github: "openfoodfoundation/better_spree_paypal_express", branch: "spree-upgrade-intermediate" #gem 'spree_paypal_express', github: "spree-contrib/better_spree_paypal_express", branch: "1-3-stable" -gem 'stripe', '~> 3.3.2' +gem 'stripe', '~> 4.5.0' # We need at least this version to have Digicert's root certificate # which is needed for Pin Payments (and possibly others). gem 'activemerchant', '~> 1.78' gem 'oauth2', '~> 1.4.1' # Used for Stripe Connect -gem 'jwt', '~> 1.5' +gem 'jwt', '~> 2.1' gem 'delayed_job_active_record' gem 'daemons' diff --git a/Gemfile.lock b/Gemfile.lock index 4af1e20570..be3639fd40 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -252,6 +252,7 @@ GEM compass (~> 1.0.0) sass-rails (< 5.1) sprockets (< 4.0) + connection_pool (2.2.2) crack (0.4.3) safe_yaml (~> 1.0.0) css_parser (1.6.0) @@ -508,7 +509,7 @@ GEM json_spec (1.1.5) multi_json (~> 1.0) rspec (>= 2.0, < 4.0) - jwt (1.5.6) + jwt (2.1.0) kaminari (0.13.0) actionpack (>= 3.0.0) activesupport (>= 3.0.0) @@ -540,6 +541,8 @@ GEM multi_xml (0.6.0) multipart-post (2.0.0) nenv (0.3.0) + net-http-persistent (3.0.0) + connection_pool (~> 2.2) nokogiri (1.6.8.1) mini_portile2 (~> 2.1.0) notiffany (0.1.1) @@ -629,7 +632,7 @@ GEM trollop (~> 2.1) rdoc (3.12.2) json (~> 1.4) - redcarpet (3.2.3) + redcarpet (3.4.0) ref (2.0.0) request_store (1.4.1) rack (>= 1.4) @@ -716,8 +719,9 @@ GEM tilt (~> 1.1, != 1.3.0) state_machine (1.2.0) stringex (1.3.3) - stripe (3.3.2) - faraday (~> 0.9) + stripe (4.5.0) + faraday (~> 0.13) + net-http-persistent (~> 3.0) therubyracer (0.12.0) libv8 (~> 3.16.14.0) ref @@ -813,7 +817,7 @@ DEPENDENCIES jquery-migrate-rails jquery-rails json_spec (~> 1.1.4) - jwt (~> 1.5) + jwt (~> 2.1) knapsack letter_opener (>= 1.4.1) listen (= 3.0.8) @@ -853,7 +857,7 @@ DEPENDENCIES spree_paypal_express! spring (= 1.7.2) spring-commands-rspec - stripe (~> 3.3.2) + stripe (~> 4.5.0) therubyracer (= 0.12.0) timecop truncate_html diff --git a/app/assets/javascripts/admin/product_import/controllers/import_form_controller.js.coffee b/app/assets/javascripts/admin/product_import/controllers/import_form_controller.js.coffee index 95e6a67a5d..174d0049d9 100644 --- a/app/assets/javascripts/admin/product_import/controllers/import_form_controller.js.coffee +++ b/app/assets/javascripts/admin/product_import/controllers/import_form_controller.js.coffee @@ -10,6 +10,7 @@ angular.module("admin.productImport").controller "ImportFormCtrl", ($scope, $htt $scope.updated_ids = [] $scope.update_errors = [] + $scope.batchSize = 50 $scope.step = 'settings' $scope.chunks = 0 $scope.completed = 0 @@ -51,19 +52,28 @@ angular.module("admin.productImport").controller "ImportFormCtrl", ($scope, $htt $scope.start = () -> $scope.started = true total = ams_data.item_count - size = 50 - $scope.chunks = Math.ceil(total / size) + $scope.chunks = Math.ceil(total / $scope.batchSize) - i = 0 + # Process only the first batch. + $scope.processBatch($scope.step, 0, $scope.chunks) - while i < $scope.chunks - start = (i*size)+1 - end = (i+1)*size - if $scope.step == 'import' - $scope.processImport(start, end) - if $scope.step == 'save' - $scope.processSave(start, end) - i++ + $scope.processBatch = (step, batchIndex, batchCount) -> + start = (batchIndex * $scope.batchSize) + 1 + end = (batchIndex + 1) * $scope.batchSize + isLastBatch = batchCount == batchIndex + 1 + + promise = if step == 'import' + $scope.processImport(start, end) + else if step == 'save' + $scope.processSave(start, end) + + return if isLastBatch + + processNextBatch = -> + $scope.processBatch(step, batchIndex + 1, batchCount) + + # Process next batch whether or not processing of the current batch succeeds. + promise.then(processNextBatch, processNextBatch) $scope.processImport = (start, end) -> $http( diff --git a/app/assets/javascripts/admin/subscriptions/controllers/subscription_controller.js.coffee b/app/assets/javascripts/admin/subscriptions/controllers/subscription_controller.js.coffee index 3b26866125..aa635da8cc 100644 --- a/app/assets/javascripts/admin/subscriptions/controllers/subscription_controller.js.coffee +++ b/app/assets/javascripts/admin/subscriptions/controllers/subscription_controller.js.coffee @@ -6,7 +6,11 @@ angular.module("admin.subscriptions").controller "SubscriptionController", ($sco $scope.schedules = Schedules.all $scope.paymentMethods = PaymentMethods.all $scope.shippingMethods = ShippingMethods.all - $scope.distributor_id = $scope.subscription.shop_id # variant selector requires distributor_id + + # Variant selector requires these + $scope.distributor_id = $scope.subscription.shop_id + $scope.eligible_for_subscriptions = true + $scope.view = if $scope.subscription.id? then 'review' else 'details' $scope.nextCallbacks = {} $scope.backCallbacks = {} diff --git a/app/assets/javascripts/admin/utils/directives/variant_autocomplete.js.coffee b/app/assets/javascripts/admin/utils/directives/variant_autocomplete.js.coffee index 9b8f12d3dc..17a467e463 100644 --- a/app/assets/javascripts/admin/utils/directives/variant_autocomplete.js.coffee +++ b/app/assets/javascripts/admin/utils/directives/variant_autocomplete.js.coffee @@ -22,6 +22,7 @@ angular.module("admin.utils").directive "variantAutocomplete", ($timeout) -> q: term distributor_id: scope.distributor_id order_cycle_id: scope.order_cycle_id + eligible_for_subscriptions: scope.eligible_for_subscriptions results: (data, page) -> results: data formatResult: (variant) -> diff --git a/app/assets/javascripts/darkswarm/filters/dates.js.coffee b/app/assets/javascripts/darkswarm/filters/dates.js.coffee index 7b5dd861d5..91efe89d1e 100644 --- a/app/assets/javascripts/darkswarm/filters/dates.js.coffee +++ b/app/assets/javascripts/darkswarm/filters/dates.js.coffee @@ -4,7 +4,7 @@ Darkswarm.filter "date_in_words", -> Darkswarm.filter "sensible_timeframe", (date_in_wordsFilter)-> (date) -> - if moment().add('days', 2) < moment(date) + if moment().add(2, 'days') < moment(date) t 'orders_open' else t('closing') + date_in_wordsFilter(date) diff --git a/app/assets/stylesheets/admin/pages/subscription_form.css.scss b/app/assets/stylesheets/admin/pages/subscription_form.css.scss new file mode 100644 index 0000000000..b892c4f17e --- /dev/null +++ b/app/assets/stylesheets/admin/pages/subscription_form.css.scss @@ -0,0 +1,7 @@ +@import '../variables'; + +.admin-subscription-form-subscription-line-items { + .not-in-open-and-upcoming-order-cycles-warning { + color: $warning-red; + } +} diff --git a/app/assets/stylesheets/admin/pages/subscription_review.css.scss b/app/assets/stylesheets/admin/pages/subscription_review.css.scss new file mode 100644 index 0000000000..76008afc0f --- /dev/null +++ b/app/assets/stylesheets/admin/pages/subscription_review.css.scss @@ -0,0 +1,7 @@ +@import '../variables'; + +.admin-subscription-review-subscription-line-items { + .not-in-open-and-upcoming-order-cycles-warning { + color: $warning-red; + } +} diff --git a/app/controllers/admin/subscription_line_items_controller.rb b/app/controllers/admin/subscription_line_items_controller.rb index d981b71fe1..5267374539 100644 --- a/app/controllers/admin/subscription_line_items_controller.rb +++ b/app/controllers/admin/subscription_line_items_controller.rb @@ -13,7 +13,8 @@ module Admin def build @subscription_line_item.assign_attributes(params[:subscription_line_item]) @subscription_line_item.price_estimate = price_estimate - render json: @subscription_line_item, serializer: Api::Admin::SubscriptionLineItemSerializer + render json: @subscription_line_item, serializer: Api::Admin::SubscriptionLineItemSerializer, + shop: @shop, schedule: @schedule end private @@ -26,7 +27,7 @@ module Admin @shop = Enterprise.managed_by(spree_current_user).find_by_id(params[:shop_id]) @schedule = permissions.editable_schedules.find_by_id(params[:schedule_id]) @order_cycle = @schedule.andand.current_or_next_order_cycle - @variant = Spree::Variant.stockable_by(@shop).find_by_id(params[:subscription_line_item][:variant_id]) + @variant = variant_if_eligible(params[:subscription_line_item][:variant_id]) if @shop.present? end def new_actions @@ -50,5 +51,9 @@ module Admin OpenFoodNetwork::ScopeVariantToHub.new(@shop).scope(@variant) @variant.price + fee_calculator.indexed_fees_for(@variant) end + + def variant_if_eligible(variant_id) + SubscriptionVariantsService.eligible_variants(@shop).find_by_id(variant_id) + end end end diff --git a/app/controllers/spree/admin/invoices_controller.rb b/app/controllers/spree/admin/invoices_controller.rb index 230d01322b..710fda1a3a 100644 --- a/app/controllers/spree/admin/invoices_controller.rb +++ b/app/controllers/spree/admin/invoices_controller.rb @@ -2,6 +2,7 @@ module Spree module Admin class InvoicesController < Spree::Admin::BaseController respond_to :json + authorize_resource class: false def create invoice_service = BulkInvoiceService.new diff --git a/app/models/product_import/entry_validator.rb b/app/models/product_import/entry_validator.rb index 11c0aab7dd..7582330a0a 100644 --- a/app/models/product_import/entry_validator.rb +++ b/app/models/product_import/entry_validator.rb @@ -189,7 +189,7 @@ module ProductImport products.flat_map(&:variants).each do |existing_variant| unit_scale = existing_variant.product.variant_unit_scale unscaled_units = entry.unscaled_units || 0 - entry.unit_value = unscaled_units * unit_scale + entry.unit_value = unscaled_units * unit_scale unless unit_scale.nil? if entry_matches_existing_variant?(entry, existing_variant) variant_override = create_inventory_item(entry, existing_variant) diff --git a/app/models/spree/ability_decorator.rb b/app/models/spree/ability_decorator.rb index 2c68470840..f6f37560ce 100644 --- a/app/models/spree/ability_decorator.rb +++ b/app/models/spree/ability_decorator.rb @@ -210,9 +210,10 @@ class AbilityDecorator # during the order creation process from the admin backend order.distributor.nil? || user.enterprises.include?(order.distributor) || order.order_cycle.andand.coordinated_by?(user) end - can [:admin, :bulk_management, :managed, :bulk_invoice], Spree::Order do + can [:admin, :bulk_management, :managed], Spree::Order do user.admin? || user.enterprises.any?(&:is_distributor) end + can [:admin, :create, :show, :poll], :invoice can [:admin, :visible], Enterprise can [:admin, :index, :create, :update, :destroy], :line_item can [:admin, :index, :create], Spree::LineItem diff --git a/app/serializers/api/admin/subscription_line_item_serializer.rb b/app/serializers/api/admin/subscription_line_item_serializer.rb index 23f4c6bc52..34bc00c6c0 100644 --- a/app/serializers/api/admin/subscription_line_item_serializer.rb +++ b/app/serializers/api/admin/subscription_line_item_serializer.rb @@ -1,7 +1,8 @@ module Api module Admin class SubscriptionLineItemSerializer < ActiveModel::Serializer - attributes :id, :variant_id, :quantity, :description, :price_estimate + attributes :id, :variant_id, :quantity, :description, :price_estimate, + :in_open_and_upcoming_order_cycles def description "#{object.variant.product.name} - #{object.variant.full_name}" @@ -10,6 +11,22 @@ module Api def price_estimate object.price_estimate.andand.to_f || "?" end + + def in_open_and_upcoming_order_cycles + SubscriptionVariantsService.in_open_and_upcoming_order_cycles?(option_or_assigned_shop, + option_or_assigned_schedule, + object.variant) + end + + private + + def option_or_assigned_shop + @options[:shop] || object.subscription.andand.shop + end + + def option_or_assigned_schedule + @options[:schedule] || object.subscription.andand.schedule + end end end end diff --git a/app/services/subscription_validator.rb b/app/services/subscription_validator.rb index 33fc2baf77..051033dd9f 100644 --- a/app/services/subscription_validator.rb +++ b/app/services/subscription_validator.rb @@ -97,15 +97,12 @@ class SubscriptionValidator errors.add(:subscription_line_items, :not_available, name: name) end - # TODO: Extract this into a separate class def available_variant_ids - @available_variant_ids ||= - Spree::Variant.joins(exchanges: { order_cycle: :schedules }) - .where(id: subscription_line_items.map(&:variant_id)) - .where(schedules: { id: schedule }, exchanges: { incoming: false, receiver_id: shop }) - .merge(OrderCycle.not_closed) - .select('DISTINCT spree_variants.id') - .pluck(:id) + return @available_variant_ids if @available_variant_ids.present? + + subscription_variant_ids = subscription_line_items.map(&:variant_id) + @available_variant_ids = SubscriptionVariantsService.eligible_variants(shop) + .where(id: subscription_variant_ids).pluck(:id) end def build_msg_from(k, msg) diff --git a/app/services/subscription_variants_service.rb b/app/services/subscription_variants_service.rb new file mode 100644 index 0000000000..855c200303 --- /dev/null +++ b/app/services/subscription_variants_service.rb @@ -0,0 +1,39 @@ +class SubscriptionVariantsService + # Includes the following variants: + # - Variants of permitted producers + # - Variants of hub + # - Variants that are in outgoing exchanges where the hub is receiver + def self.eligible_variants(distributor) + variant_conditions = ["spree_products.supplier_id IN (?)", permitted_producer_ids(distributor)] + exchange_variant_ids = outgoing_exchange_variant_ids(distributor) + if exchange_variant_ids.present? + variant_conditions[0] << " OR spree_variants.id IN (?)" + variant_conditions << exchange_variant_ids + end + + Spree::Variant.joins(:product).where(is_master: false).where(*variant_conditions) + end + + def self.in_open_and_upcoming_order_cycles?(distributor, schedule, variant) + scope = ExchangeVariant.joins(exchange: { order_cycle: :schedules }) + .where(variant_id: variant, exchanges: { incoming: false, receiver_id: distributor }) + .merge(OrderCycle.not_closed) + scope = scope.where(schedules: { id: schedule }) + scope.any? + end + + def self.permitted_producer_ids(distributor) + other_permitted_producer_ids = EnterpriseRelationship.joins(:parent) + .permitting(distributor).with_permission(:add_to_order_cycle) + .merge(Enterprise.is_primary_producer) + .pluck(:parent_id) + + other_permitted_producer_ids | [distributor.id] + end + + def self.outgoing_exchange_variant_ids(distributor) + ExchangeVariant.select("DISTINCT exchange_variants.variant_id").joins(:exchange) + .where(exchanges: { incoming: false, receiver_id: distributor.id }) + .pluck(:variant_id) + end +end diff --git a/app/views/admin/subscriptions/_review.html.haml b/app/views/admin/subscriptions/_review.html.haml index e1591e6210..7e97bd02aa 100644 --- a/app/views/admin/subscriptions/_review.html.haml +++ b/app/views/admin/subscriptions/_review.html.haml @@ -56,7 +56,7 @@ %input#edit-products{ type: "button", value: t(:edit), ng: { click: "setView('products')" } } .row .seven.columns.alpha.omega - %table#subscription-line-items + %table#subscription-line-items.admin-subscription-review-subscription-line-items %colgroup %col{:style => "width: 62%;"}/ %col{:style => "width: 14%;"}/ @@ -71,7 +71,10 @@ %span= t(:total) %tbody %tr.item{ id: "sli_{{$index}}", ng: { repeat: "item in subscription.subscription_line_items | filter:{ _destroy: '!true' }", class: { even: 'even', odd: 'odd' } } } - %td.description {{ item.description }} + %td + .description {{ item.description }} + .not-in-open-and-upcoming-order-cycles-warning{ ng: { if: '!item.in_open_and_upcoming_order_cycles' } } + = t(".no_open_or_upcoming_order_cycle") %td.price.align-center {{ item.price_estimate | currency }} %td.quantity {{ item.quantity }} %td.total.align-center {{ (item.price_estimate * item.quantity) | currency }} diff --git a/app/views/admin/subscriptions/_subscription_line_items.html.haml b/app/views/admin/subscriptions/_subscription_line_items.html.haml index 0be91f2c93..b80b95a85b 100644 --- a/app/views/admin/subscriptions/_subscription_line_items.html.haml +++ b/app/views/admin/subscriptions/_subscription_line_items.html.haml @@ -1,4 +1,4 @@ -%table#subscription-line-items +%table#subscription-line-items.admin-subscription-form-subscription-line-items %colgroup %col{:style => "width: 49%;"}/ %col{:style => "width: 14%;"}/ @@ -15,7 +15,10 @@ %th.orders-actions.actions %tbody %tr.item{ id: "sli_{{$index}}", ng: { repeat: "item in subscription.subscription_line_items | filter:{ _destroy: '!true' }", class: { even: 'even', odd: 'odd' } } } - %td.description {{ item.description }} + %td + .description {{ item.description }} + .not-in-open-and-upcoming-order-cycles-warning{ ng: { if: '!item.in_open_and_upcoming_order_cycles' } } + = t(".not_in_open_and_upcoming_order_cycles_warning") %td.price.align-center {{ item.price_estimate | currency }} %td.quantity %input{ name: 'quantity', type: 'number', min: 0, ng: { model: 'item.quantity' } } diff --git a/app/views/spree/admin/orders/bulk_management.html.haml b/app/views/spree/admin/orders/bulk_management.html.haml index 50aa10634f..950ec2a45a 100644 --- a/app/views/spree/admin/orders/bulk_management.html.haml +++ b/app/views/spree/admin/orders/bulk_management.html.haml @@ -112,7 +112,7 @@ .margin-bottom-50{ 'ng-hide' => 'RequestMonitor.loading || filteredLineItems.length == 0' } %form{ name: 'bulk_order_form' } - %table.index#listing_orders.bulk{ :class => "sixteen columns alpha" } + %table.index#listing_orders.bulk{ :class => "sixteen columns alpha", ng: { show: "initialized" } } %thead %tr{ ng: { controller: "ColumnsCtrl" } } %th.bulk @@ -157,7 +157,7 @@ = t("admin.orders.bulk_management.ask") %input{ :type => 'checkbox', 'ng-model' => "confirmDelete" } - %tr.line_item{ 'ng-repeat' => "line_item in filteredLineItems = ( lineItems | filter:quickSearch | selectFilter:supplierFilter:distributorFilter:orderCycleFilter | variantFilter:selectedUnitsProduct:selectedUnitsVariant:sharedResource | orderBy:sorting.predicate:sorting.reverse )", 'ng-class-even' => "'even'", 'ng-class-odd' => "'odd'", :id => "li_{{line_item.id}}" } + %tr.line_item{ 'ng-repeat' => "line_item in filteredLineItems = ( lineItems | filter:quickSearch | selectFilter:supplierFilter:distributorFilter:orderCycleFilter | variantFilter:selectedUnitsProduct:selectedUnitsVariant:sharedResource | orderBy:sorting.predicate:sorting.reverse )", 'ng-class-even' => "'even'", 'ng-class-odd' => "'odd'", ng: { attr: { id: "li_{{line_item.id}}" } } } %td.bulk %input{ :type => "checkbox", :name => 'bulk', 'ng-model' => 'line_item.checked', 'ignore-dirty' => true } %td.order_no{ 'ng-show' => 'columns.order_no.visible' } {{ line_item.order.number }} @@ -181,6 +181,6 @@ %input.show-dirty{ :type => 'text', :name => 'price', :id => 'price', :ng => { value: 'line_item.price * line_item.quantity | currency:""', readonly: "true", class: '{"update-error": line_item.errors.price}' } } %span.error{ ng: { bind: 'line_item.errors.price' } } %td.actions - %a{ href: "/admin/orders/{{line_item.order.number}}/edit", :class => "edit-order icon-edit no-text", 'confirm-link-click' => 'confirmRefresh()' } + %a{ ng: { href: "/admin/orders/{{line_item.order.number}}/edit" }, :class => "edit-order icon-edit no-text", 'confirm-link-click' => 'confirmRefresh()' } %td.actions %a{ 'ng-click' => "deleteLineItem(line_item)", :class => "delete-line-item icon-trash no-text" } diff --git a/config/locales/en.yml b/config/locales/en.yml index 4c76f68854..4149a5a5d5 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1088,6 +1088,7 @@ en: this_is_an_estimate: | The displayed prices are only an estimate and calculated at the time the subscription is changed. If you change prices or fees, orders will be updated, but the subscription will still display the old values. + not_in_open_and_upcoming_order_cycles_warning: "There are no open or upcoming order cycles for this product." details: details: Details invalid_error: Oops! Please fill in all of the required fields... @@ -1102,6 +1103,7 @@ en: details: Details address: Address products: Products + no_open_or_upcoming_order_cycle: "No Upcoming Order Cycle" product_already_in_order: This product has already been added to the order. Please edit the quantity directly. orders: number: Number diff --git a/config/locales/fr_BE.yml b/config/locales/fr_BE.yml index c5cfc0054f..94291967f1 100644 --- a/config/locales/fr_BE.yml +++ b/config/locales/fr_BE.yml @@ -1105,7 +1105,7 @@ fr_BE: footer_legal_call: "Lire nos" footer_legal_tos: "Termes et conditions" footer_legal_visit: "Nous trouver sur" - footer_legal_text_html: "Open Food Network est une plateforme logicielle open source, libre et gratuite. Nos données sont protégées sous licence %{content_license} et notre code sous %{code_license}." + footer_legal_text_html: "Open Food Network est une plateforme logicielle open source et libre. Nos données sont protégées sous licence %{content_license} et notre code sous %{code_license}." footer_data_text_with_privacy_policy_html: "Nous prenons soin de vos données. Voir notre %{privacy_policy} et %{cookies_policy}." footer_data_text_without_privacy_policy_html: "Nous prenons soin de vos données. Voir notre %{cookies_policy}." footer_data_privacy_policy: "politique de confidentialité" diff --git a/config/locales/it.yml b/config/locales/it.yml index ddb3185edc..5a123f6480 100644 --- a/config/locales/it.yml +++ b/config/locales/it.yml @@ -602,6 +602,7 @@ it: desc_long_placeholder: Racconta di te ai consumatori. Questa informazione comparirà nel tuo profilo pubblico. business_details: abn: ABN + abn_placeholder: es. 99 123 456 789 acn: ACN acn_placeholder: es. 123 456 789 display_invoice_logo: Mostra logo nelle fatture @@ -1131,6 +1132,7 @@ it: total_excl_tax: "Totale (Tasse escl.):" total_incl_tax: "Totale (Tasse incl.):" abn: "CF:" + acn: "ACN:" invoice_issued_on: "Fattura emessa il" order_number: "Numero fattura:" date_of_transaction: "Data della transazione:" @@ -1149,7 +1151,9 @@ it: menu_5_title: "About" menu_5_url: "http://www.openfoodnetwork.org/" menu_6_title: "Connetti" + menu_6_url: "https://openfoodnetwork.org/au/connect/" menu_7_title: "Impara" + menu_7_url: "https://openfoodnetwork.org/au/learn/" logo: "Logo (640x130)" logo_mobile: "Mobile logo (75x26)" logo_mobile_svg: "Mobile logo (SVG)" @@ -2216,6 +2220,9 @@ it: validation_msg_tax: "^Categoria d'imposta richiesta" validation_msg_tax_category_cant_be_blank: "^Categoria di tassa non può essere vuoto" validation_msg_is_associated_with_an_exising_customer: "è associato con un cliente esistente" + content_configuration_pricing_table: "(TODO: tabella dei prezzi)" + content_configuration_case_studies: "(TODO: Casi studio)" + content_configuration_detail: "(TODO: Dettaglio)" enterprise_name_error: "è già stato utilizzato. Se questo è il nome della tua azienda e vorresti reclamarne la proprietà, o se vuoi contattare questa azienda, puoi contattare l'attuale referente di questo profilo a %{email}." enterprise_owner_error: "^%{email}non può gestire altre aziende (il limite è %{enterprise_limit})." enterprise_role_uniqueness_error: "^Questo ruolo è già presente." @@ -2253,6 +2260,7 @@ it: back_to_orders_list: "Indietro alla lista delle gentili richieste" no_orders_found: "Nessuna gentile richiesta trovata" order_information: "Informazioni Gentile Richiesta" + date_completed: "Data di completamento" amount: "Quantità" state_names: ready: Pronto @@ -2275,6 +2283,7 @@ it: choose: Scegli resolve_errors: 'Per favore risolvi i seguenti errori:' more_items: "+ %{count} ancora" + default_card_updated: Carta predefinita aggiornata admin: enterprise_limit_reached: "Hai raggiunto il limite standard di aziende per account. Scrivi a %{contact_email} se hai bisogno di aumentarlo." modals: @@ -2368,14 +2377,18 @@ it: non_producer_example: es. Botteghe, Food Coop, GAS enterprise_status: status_title: "%{name} è impostato e pronto a partire!" + severity: Gravità description: Descrizione resolve: Risolvi new_tag_rule_dialog: select_rule_type: "Seleziona un tipo di regola:" orders: index: + per_page: "%{results} per pagina" view_file: Vedi file compiling_invoices: Compilazione fatture + bulk_invoice_created: Fattura all'ingrosso creata + bulk_invoice_failed: Creazione fattura all'ingrosso fallita please_wait: Si prega di attendere che il PDF sia pronto prima di chiudere questo modale resend_user_email_confirmation: resend: "Invia nuovamente" @@ -2394,6 +2407,7 @@ it: 'yes': "A richiesta" variant_overrides: on_demand: + use_producer_settings: "Usa le impostazioni dello stock del produttore" 'yes': "Sì" 'no': "No" inventory_products: "Inventario Prodotti" @@ -2401,6 +2415,9 @@ it: new_products: "Nuovi Prodotti" reset_stock_levels: Resetta le quantità disponibili alla quantità predefinita changes_to: Cambia in + one_override: una si è sovrascritta + overrides: sovrascrive + remain_unsaved: restano da memorizzare no_changes_to_save: Nessuna modifica da salvare. no_authorisation: "Non abbiamo l'autorizzazione per salvare queste modifiche. " some_trouble: "Abbiamo avuto problemi durante il salvataggio: %{errors}" @@ -2449,16 +2466,23 @@ it: admin: orders: index: + listing_orders: "Listino Ordini" new_order: "Nuovo ordine" + capture: "Cattura" ship: "Spedizione" edit: "Modifica" + note: "Nota" + first: "Primo" + last: "Ultimo" previous: "Precedente" next: "Prossimo" loading: "Caricamento" no_orders_found: "Nessuna gentile richiesta trovata" results_found: "%{number} Risultati trovati." + viewing: "Guardando da %{start} a %{end}" print_invoices: "Stampa fatture" invoice: + issued_on: Emesso il tax_invoice: FATTURA DELLE TASSE code: Codice from: Da @@ -2490,12 +2514,18 @@ it: stripe_disabled_msg: I pagamenti Stripe sono stati disabilitati dall'amministratore di sistema. request_failed_msg: Spiacenti, qualcosa è andato storto mentre cercavamo di verificare i dettagli dell'account con Stripe... account_missing_msg: Non esiste un account Stripe per questa azienda. + connect_one: Connetti One access_revoked_msg: L'accesso a questo account Stripe è stato revocato, per favore ricollega il tuo account. status: Stato connected: Connesso account_id: Account ID business_name: Ragione sociale charges_enabled: Cambi Consentiti + payments: + source_forms: + stripe: + error_saving_payment: Errore memorizzando il pagamento + submitting_payment: Eseguendo il pagamento products: new: title: 'Nuovo prodotto' @@ -2514,7 +2544,9 @@ it: inherits_properties?: Eredita proprietà? available_on: Disponibile il av_on: "Disp. il" + import_date: "Data di importazione" products_variant: + variant_has_n_overrides: "Questa variante ha %{n} sostituzione/i" new_variant: "Nuova variante" product_name: Nome Prodotto primary_taxon_form: @@ -2523,14 +2555,19 @@ it: group_buy: "Acquisto di gruppo?" display_as: display_as: Visualizza come + reports: + table: + select_and_search: "Seleziona filtri e clicca su %{option} per accedere al tuo dato" users: index: + listing_users: "Elenco Utenti" new_user: "Nuovo utente" user: "Utente" enterprise_limit: "Limite azienda" search: "Cerca" email: "Email" edit: + editing_user: "Modificando Utente" back_to_users_list: "Torna all'elenco degli utenti" general_settings: "Impostazioni generali" form: @@ -2548,6 +2585,7 @@ it: edit: legal_settings: "Impostazioni Legali" cookies_consent_banner_toggle: "Mostra banner di consenso per i cookie" + privacy_policy_url: "Privacy Policy URL" enterprises_require_tos: "Le aziende devono accettare i Termini di Servizio" cookies_policy_matomo_section: "Visualizza la sezione di Matomo nella pagina della cookie policy" cookies_policy_ga_section: "Visualizza la sezione di Google Analytics nella pagina della cookie policy" @@ -2556,7 +2594,12 @@ it: payment: stripe: choose_one: Scegli uno + enter_new_card: Inserire dettagli per una nuova carta + used_saved_card: "usare la carta salvata" + or_enter_new_card: "Oppure, inserire dettagli di una nuova carta" + remember_this_card: Ricordare questa Carta? date_picker: + format: '%Y-%m-%d' js_format: 'aa-mm-gg' inventory: Inventario orders: @@ -2567,6 +2610,7 @@ it: order_mailer: invoice_email: hi: "Ciao %{name}" + invoice_attached_text: 'Aggiunge una fattura per il tuo recente ordine di ' order_state: address: indirizzo adjustments: aggiustamenti @@ -2659,5 +2703,6 @@ it: authorised_shops: Negozi autorizzati authorised_shops: shop_name: "Nome del negozio" + allow_charges?: "Consentire ricarichi" localized_number: invalid_format: 'Formato non valido: inserire un numero.' diff --git a/config/locales/nb.yml b/config/locales/nb.yml index 9a19f953d9..8490c91c8f 100644 --- a/config/locales/nb.yml +++ b/config/locales/nb.yml @@ -1027,6 +1027,7 @@ nb: this_is_an_estimate: | De viste prisene er bare et estimat og beregnet på det tidspunktet abonnementet endres. Hvis du endrer priser eller avgifter, vil ordrer bli oppdatert, men abonnementet vil fortsatt vise de gamle verdiene. + not_in_open_and_upcoming_order_cycles_warning: "Det er ingen åpne eller kommende bestillingsrunder for dette produktet." details: details: Detaljer invalid_error: Oops! Vennligst fyll inn alle obligatoriske felter... @@ -1041,6 +1042,7 @@ nb: details: Detaljer address: Adresse products: Produkter + no_open_or_upcoming_order_cycle: "Ingen kommende bestillingsrunde" product_already_in_order: Dette produktet er allerede lagt til i bestillingen. Vennligst rediger mengden direkte. orders: number: Antall diff --git a/config/schedule.rb b/config/schedule.rb index edec8351ac..4bb20df545 100644 --- a/config/schedule.rb +++ b/config/schedule.rb @@ -13,7 +13,7 @@ job_type :enqueue_job, "cd :path; :environment_variable=:environment bundle exe every 1.hour do - rake 'openfoodnetwork:cache:check_products_integrity' + rake 'ofn:cache:check_products_integrity' end every 1.day, at: '12:05am' do @@ -35,10 +35,10 @@ every 5.minutes do end every 1.day, at: '1:00am' do - rake 'openfoodnetwork:billing:update_account_invoices' + rake 'ofn: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' + rake 'ofn:billing:finalize_account_invoices' end diff --git a/lib/open_food_network/scope_variants_for_search.rb b/lib/open_food_network/scope_variants_for_search.rb index 9befef5304..24b110fb6e 100644 --- a/lib/open_food_network/scope_variants_for_search.rb +++ b/lib/open_food_network/scope_variants_for_search.rb @@ -33,6 +33,10 @@ module OpenFoodNetwork Spree::Variant.where(is_master: false).ransack(search_params.merge(m: 'or')).result end + def distributor + Enterprise.find params[:distributor_id] + end + def scope_to_schedule @variants = @variants.in_schedule(params[:schedule_id]) end @@ -42,12 +46,29 @@ module OpenFoodNetwork end def scope_to_distributor - distributor = Enterprise.find params[:distributor_id] + if params[:eligible_for_subscriptions] + scope_to_eligible_for_subscriptions_in_distributor + else + scope_to_available_for_orders_in_distributor + end + end + + def scope_to_available_for_orders_in_distributor @variants = @variants.in_distributor(distributor) + scope_variants_to_distributor(@variants, distributor) + end + + def scope_to_eligible_for_subscriptions_in_distributor + eligible_variants_scope = SubscriptionVariantsService.eligible_variants(distributor) + @variants = @variants.merge(eligible_variants_scope) + scope_variants_to_distributor(@variants, distributor) + end + + def scope_variants_to_distributor(variants, distributor) scoper = OpenFoodNetwork::ScopeVariantToHub.new(distributor) # Perform scoping after all filtering is done. # Filtering could be a problem on scoped variants. - @variants.each { |v| scoper.scope(v) } + variants.each { |v| scoper.scope(v) } end end end diff --git a/lib/tasks/billing.rake b/lib/tasks/billing.rake index 6bfd0a53e7..4c279bc41e 100644 --- a/lib/tasks/billing.rake +++ b/lib/tasks/billing.rake @@ -1,4 +1,4 @@ -namespace :openfoodnetwork do +namespace :ofn do namespace :billing do desc 'Update enterprise user invoices' task update_account_invoices: :environment do diff --git a/lib/tasks/cache.rake b/lib/tasks/cache.rake index 64a5d3909a..a88e745153 100644 --- a/lib/tasks/cache.rake +++ b/lib/tasks/cache.rake @@ -1,6 +1,6 @@ require 'open_food_network/products_cache_integrity_checker' -namespace :openfoodnetwork do +namespace :ofn do namespace :cache do desc 'check the integrity of the products cache' task :check_products_integrity => :environment do diff --git a/lib/tasks/data.rake b/lib/tasks/data.rake index 643886943c..9ae0984e3a 100644 --- a/lib/tasks/data.rake +++ b/lib/tasks/data.rake @@ -1,4 +1,4 @@ -namespace :openfoodnetwork do +namespace :ofn do namespace :data do desc "Adding relationships based on recent order cycles" task :create_order_cycle_relationships => :environment do diff --git a/lib/tasks/dev.rake b/lib/tasks/dev.rake index 19cc5b7a4b..2eff925395 100644 --- a/lib/tasks/dev.rake +++ b/lib/tasks/dev.rake @@ -1,4 +1,4 @@ -namespace :openfoodnetwork do +namespace :ofn do namespace :dev do desc 'load sample data' task load_sample_data: :environment do diff --git a/lib/tasks/enterprises.rake b/lib/tasks/enterprises.rake index 62d56fe8a5..547e179c2a 100644 --- a/lib/tasks/enterprises.rake +++ b/lib/tasks/enterprises.rake @@ -1,6 +1,6 @@ require 'csv' -namespace :openfoodnetwork do +namespace :ofn do namespace :dev do desc 'export enterprises to CSV' task :export_enterprises => :environment do diff --git a/lib/tasks/specs.rake b/lib/tasks/specs.rake index dddb5ffb54..bd6708dcf6 100644 --- a/lib/tasks/specs.rake +++ b/lib/tasks/specs.rake @@ -1,4 +1,4 @@ -namespace :openfoodnetwork do +namespace :ofn do namespace :specs do namespace :engines do def detect_engine_paths diff --git a/public/inventory_template.csv b/public/inventory_template.csv index d52cecded6..dc2f10e3be 100644 --- a/public/inventory_template.csv +++ b/public/inventory_template.csv @@ -1 +1 @@ -producer,distributor,name,display_name,units,unit_type,price,on_hand +producer,distributor,name,display_name,variant_unit_name,sku,units,unit_type,price,on_hand,on_demand diff --git a/script/setup b/script/setup index 430c07b911..53332df685 100755 --- a/script/setup +++ b/script/setup @@ -52,7 +52,7 @@ printf '\n\n' | bundle exec rake db:setup db:test:prepare printf '\n' # Load some default data for your environment -bundle exec rake openfoodnetwork:dev:load_sample_data +bundle exec rake ofn:dev:load_sample_data printf '\n' printf "${YELLOW}WELCOME TO OPEN FOOD NETWORK!\n" diff --git a/spec/controllers/admin/subscription_line_items_controller_spec.rb b/spec/controllers/admin/subscription_line_items_controller_spec.rb index b2f3a0f432..2b42a20df7 100644 --- a/spec/controllers/admin/subscription_line_items_controller_spec.rb +++ b/spec/controllers/admin/subscription_line_items_controller_spec.rb @@ -10,9 +10,9 @@ describe Admin::SubscriptionLineItemsController, type: :controller do let(:unmanaged_shop) { create(:enterprise) } let!(:product) { create(:product) } let!(:variant) { create(:variant, product: product, unit_value: '100', price: 15.00, option_values: []) } + let!(:outgoing_exchange) { order_cycle.exchanges.create(sender: shop, receiver: shop, variants: [variant], enterprise_fees: [enterprise_fee]) } let!(:enterprise_fee) { create(:enterprise_fee, amount: 3.50) } let!(:order_cycle) { create(:simple_order_cycle, coordinator: shop, orders_open_at: 2.days.from_now, orders_close_at: 7.days.from_now) } - let!(:outgoing_exchange) { order_cycle.exchanges.create(sender: shop, receiver: shop, variants: [variant], enterprise_fees: [enterprise_fee]) } let!(:schedule) { create(:schedule, order_cycles: [order_cycle]) } let(:unmanaged_schedule) { create(:schedule, order_cycles: [create(:simple_order_cycle, coordinator: unmanaged_shop)]) } @@ -42,6 +42,8 @@ describe Admin::SubscriptionLineItemsController, type: :controller do before { params.merge!(shop_id: shop.id) } context "but the shop doesn't have permission to sell product in question" do + let!(:outgoing_exchange) { } + it "returns an error" do spree_post :build, params json_response = JSON.parse(response.body) diff --git a/spec/controllers/admin/subscriptions_controller_spec.rb b/spec/controllers/admin/subscriptions_controller_spec.rb index 5f83bf0529..9385d453f3 100644 --- a/spec/controllers/admin/subscriptions_controller_spec.rb +++ b/spec/controllers/admin/subscriptions_controller_spec.rb @@ -341,7 +341,7 @@ describe Admin::SubscriptionsController, type: :controller do end context 'with subscription_line_items params' do - let!(:product2) { create(:product, supplier: shop) } + let!(:product2) { create(:product) } let!(:variant2) { create(:variant, product: product2, unit_value: '1000', price: 6.00, option_values: []) } before do diff --git a/spec/controllers/api/orders_controller_spec.rb b/spec/controllers/api/orders_controller_spec.rb index 760094570d..134c0cfc5e 100644 --- a/spec/controllers/api/orders_controller_spec.rb +++ b/spec/controllers/api/orders_controller_spec.rb @@ -11,7 +11,9 @@ module Api let!(:distributor2) { create(:distributor_enterprise) } let!(:supplier) { create(:supplier_enterprise) } let!(:coordinator) { create(:distributor_enterprise) } + let!(:coordinator2) { create(:distributor_enterprise) } let!(:order_cycle) { create(:simple_order_cycle, coordinator: coordinator) } + let!(:order_cycle2) { create(:simple_order_cycle, coordinator: coordinator2) } let!(:order1) do create(:order, order_cycle: order_cycle, state: 'complete', completed_at: Time.zone.now, distributor: distributor, billing_address: create(:address) ) @@ -24,7 +26,9 @@ module Api create(:order, order_cycle: order_cycle, state: 'complete', completed_at: Time.zone.now, distributor: distributor, billing_address: create(:address) ) end - let!(:order4) { create(:completed_order_with_fees) } + let!(:order4) do + create(:completed_order_with_fees, order_cycle: order_cycle2, distributor: distributor2) + end let!(:order5) { create(:order, state: 'cart', completed_at: nil) } let!(:line_item1) do create(:line_item, order: order1, @@ -148,7 +152,7 @@ module Api get :index, per_page: 15, page: 1 pagination_data = { - 'results' => 3, + 'results' => 2, 'pages' => 1, 'page' => 1, 'per_page' => 15 diff --git a/spec/controllers/spree/admin/invoices_controller_spec.rb b/spec/controllers/spree/admin/invoices_controller_spec.rb index 1eff65ba2c..9a7453fb70 100644 --- a/spec/controllers/spree/admin/invoices_controller_spec.rb +++ b/spec/controllers/spree/admin/invoices_controller_spec.rb @@ -2,10 +2,11 @@ require 'spec_helper' describe Spree::Admin::InvoicesController, type: :controller do let(:order) { create(:order_with_totals_and_distribution) } - let(:user) { create(:admin_user) } + let(:enterprise_user) { create(:user) } + let!(:enterprise) { create(:enterprise, owner: enterprise_user) } before do - allow(controller).to receive(:spree_current_user) { user } + allow(controller).to receive(:spree_current_user) { enterprise_user } end describe "#create" do diff --git a/spec/features/admin/bulk_order_management_spec.rb b/spec/features/admin/bulk_order_management_spec.rb index d261405a4d..26dbc2aab0 100644 --- a/spec/features/admin/bulk_order_management_spec.rb +++ b/spec/features/admin/bulk_order_management_spec.rb @@ -439,7 +439,7 @@ feature %q{ expect(page).to have_selector "tr#li_#{li3.id}" fill_in "quick_search", :with => o1.email expect(page).to have_selector "tr#li_#{li1.id}" - expect(page).to have_no_selector "tr#li_#{li2.id}", true + expect(page).to have_no_selector "tr#li_#{li2.id}" expect(page).to have_no_selector "tr#li_#{li3.id}" end end @@ -567,6 +567,7 @@ feature %q{ context "when a filter has been applied" do it "only toggles checkboxes which are in filteredLineItems" do fill_in "quick_search", with: o1.number + expect(page).to have_no_selector "tr#li_#{li2.id}" check "toggle_bulk" fill_in "quick_search", with: '' expect(find("tr#li_#{li1.id} input[type='checkbox'][name='bulk']").checked?).to be true @@ -577,11 +578,13 @@ feature %q{ it "only applies the delete action to filteredLineItems" do check "toggle_bulk" fill_in "quick_search", with: o1.number + expect(page).to have_no_selector "tr#li_#{li2.id}" find("div#bulk-actions-dropdown").click find("div#bulk-actions-dropdown div.menu_item", :text => "Delete Selected" ).click - fill_in "quick_search", with: '' expect(page).to have_no_selector "tr#li_#{li1.id}" + fill_in "quick_search", with: '' expect(page).to have_selector "tr#li_#{li2.id}" + expect(page).to have_no_selector "tr#li_#{li1.id}" end end end @@ -740,10 +743,11 @@ feature %q{ end def select_date(date) - current_month = Time.zone.today.strftime("%B") - target_month = date.strftime("%B") + # Wait for datepicker to open and be associated to the datepicker trigger. + expect(page).to have_selector("#ui-datepicker-div") + + navigate_datepicker_to_month date - find('#ui-datepicker-div .ui-datepicker-header .ui-datepicker-prev').click if current_month != target_month find('#ui-datepicker-div .ui-datepicker-calendar .ui-state-default', text: date.strftime("%e").to_s.strip, exact_text: true).click end end diff --git a/spec/features/admin/bulk_product_update_spec.rb b/spec/features/admin/bulk_product_update_spec.rb index 96e3792163..1d9e2fb7b2 100644 --- a/spec/features/admin/bulk_product_update_spec.rb +++ b/spec/features/admin/bulk_product_update_spec.rb @@ -762,9 +762,9 @@ feature %q{ # Shows spinner whilst loading expect(page).to have_css "img.spinner", visible: true - expect(page).to have_no_css "img.spinner", visible: true end + expect(page).to have_no_css "img.spinner", visible: true expect(page).to have_no_selector "div.reveal-modal" within "table#listing_products tr#p_#{product.id}" do diff --git a/spec/features/admin/enterprises/images_spec.rb b/spec/features/admin/enterprises/images_spec.rb index b2f1cb0787..110b8c2664 100644 --- a/spec/features/admin/enterprises/images_spec.rb +++ b/spec/features/admin/enterprises/images_spec.rb @@ -35,8 +35,7 @@ feature "Managing enterprise images" do go_to_images within ".page-admin-enterprises-form__logo-field-group" do - expect(page).to have_selector(".image-field-group__preview-image") - expect(html).to include("logo-white.png") + expect_preview_image "logo-white.png" end # Replacing image @@ -47,8 +46,7 @@ feature "Managing enterprise images" do go_to_images within ".page-admin-enterprises-form__logo-field-group" do - expect(page).to have_selector(".image-field-group__preview-image") - expect(html).to include("logo-black.png") + expect_preview_image "logo-black.png" end # Removing image @@ -60,7 +58,7 @@ feature "Managing enterprise images" do expect(page).to have_content("Logo removed successfully") within ".page-admin-enterprises-form__logo-field-group" do - expect(page).to have_no_selector(".image-field-group__preview-image") + expect_no_preview_image end end @@ -73,8 +71,7 @@ feature "Managing enterprise images" do go_to_images within ".page-admin-enterprises-form__promo-image-field-group" do - expect(page).to have_selector(".image-field-group__preview-image") - expect(html).to include("logo-white.jpg") + expect_preview_image "logo-white.jpg" end # Replacing image @@ -85,8 +82,7 @@ feature "Managing enterprise images" do go_to_images within ".page-admin-enterprises-form__promo-image-field-group" do - expect(page).to have_selector(".image-field-group__preview-image") - expect(html).to include("logo-black.jpg") + expect_preview_image "logo-black.jpg" end # Removing image @@ -98,9 +94,17 @@ feature "Managing enterprise images" do expect(page).to have_content("Promo image removed successfully") within ".page-admin-enterprises-form__promo-image-field-group" do - expect(page).to have_no_selector(".image-field-group__preview-image") + expect_no_preview_image end end end end + + def expect_preview_image(file_name) + expect(page).to have_selector(".image-field-group__preview-image[src*='#{file_name}']") + end + + def expect_no_preview_image + expect(page).to have_no_selector(".image-field-group__preview-image") + end end diff --git a/spec/features/admin/product_import_spec.rb b/spec/features/admin/product_import_spec.rb index d196a686e0..3cff57b16d 100644 --- a/spec/features/admin/product_import_spec.rb +++ b/spec/features/admin/product_import_spec.rb @@ -46,7 +46,7 @@ feature "Product Import", js: true do attach_file 'file', '/tmp/test.csv' click_button 'Upload' - import_data + proceed_to_validation expect(page).to have_selector '.item-count', text: "2" expect(page).to have_no_selector '.invalid-count' @@ -89,7 +89,7 @@ feature "Product Import", js: true do attach_file 'file', '/tmp/test.csv' click_button 'Upload' - import_data + proceed_to_validation expect(page).to have_selector '.item-count', text: "2" expect(page).to have_selector '.invalid-count', text: "2" @@ -112,7 +112,7 @@ feature "Product Import", js: true do attach_file 'file', '/tmp/test.csv' click_button 'Upload' - import_data + proceed_to_validation expect(page).to have_selector '.item-count', text: "1" expect(page).to have_selector '.create-count', text: "1" @@ -142,7 +142,7 @@ feature "Product Import", js: true do attach_file 'file', '/tmp/test.csv' click_button 'Upload' - import_data + proceed_to_validation save_data @@ -164,7 +164,7 @@ feature "Product Import", js: true do end expect(page).to have_selector 'div#s2id_import_date_filter' - import_time = carrots.import_date.to_date.to_formatted_s(:long).gsub(' ', ' ') + import_time = carrots.import_date.to_date.to_formatted_s(:long) select2_select import_time, from: "import_date_filter" expect(page).to have_field "product_name", with: carrots.name @@ -188,7 +188,7 @@ feature "Product Import", js: true do click_button 'Upload' - import_data + proceed_to_validation save_data @@ -213,7 +213,7 @@ feature "Product Import", js: true do attach_file 'file', '/tmp/test.csv' click_button 'Upload' - import_data + proceed_to_validation expect(page).to have_selector '.item-count', text: "3" expect(page).to_not have_selector '.invalid-count' @@ -252,7 +252,7 @@ feature "Product Import", js: true do attach_file 'file', '/tmp/test.csv' click_button 'Upload' - import_data + proceed_to_validation expect(page).to have_selector '.item-count', text: "3" expect(page).to have_no_selector '.invalid-count' @@ -292,6 +292,49 @@ feature "Product Import", js: true do expect(page).to have_content 'Cabbage' end end + + it "handles on_demand and on_hand validations with inventory" do + csv_data = CSV.generate do |csv| + csv << ["name", "distributor", "producer", "category", "on_hand", "price", "units", "on_demand"] + csv << ["Beans", "Another Enterprise", "User Enterprise", "Vegetables", nil, "3.20", "500", "true"] + csv << ["Sprouts", "Another Enterprise", "User Enterprise", "Vegetables", "6", "6.50", "500", "false"] + csv << ["Cabbage", "Another Enterprise", "User Enterprise", "Vegetables", nil, "1.50", "500", nil] + end + File.write('/tmp/test.csv', csv_data) + + visit main_app.admin_product_import_path + select2_select I18n.t('admin.product_import.index.inventories'), from: "settings_import_into" + attach_file 'file', '/tmp/test.csv' + click_button 'Upload' + + import_data + + expect(page).to have_selector '.item-count', text: "3" + expect(page).to have_no_selector '.invalid-count' + expect(page).to have_selector '.inv-create-count', text: '2' + expect(page).to have_selector '.inv-update-count', text: '1' + + save_data + + expect(page).to have_selector '.inv-created-count', text: '2' + expect(page).to have_selector '.inv-updated-count', text: '1' + + beans_override = VariantOverride.where(variant_id: product2.variants.first.id, hub_id: enterprise2.id).first + sprouts_override = VariantOverride.where(variant_id: product3.variants.first.id, hub_id: enterprise2.id).first + cabbage_override = VariantOverride.where(variant_id: product4.variants.first.id, hub_id: enterprise2.id).first + + expect(Float(beans_override.price)).to eq 3.20 + expect(beans_override.count_on_hand).to be_nil + expect(beans_override.on_demand).to be_truthy + + expect(Float(sprouts_override.price)).to eq 6.50 + expect(sprouts_override.count_on_hand).to eq 6 + expect(sprouts_override.on_demand).to eq false + + expect(Float(cabbage_override.price)).to eq 1.50 + expect(cabbage_override.count_on_hand).to be_nil + expect(cabbage_override.on_demand).to be_nil + end end describe "when dealing with uploaded files" do @@ -349,7 +392,7 @@ feature "Product Import", js: true do attach_file 'file', '/tmp/test.csv' click_button 'Upload' - import_data + proceed_to_validation expect(page).to have_content I18n.t('admin.product_import.import.validation_overview') expect(page).to have_selector '.item-count', text: "2" @@ -361,9 +404,61 @@ feature "Product Import", js: true do end end + describe "handling a large file (120 data rows)" do + let!(:producer) { enterprise } + + let(:tmp_csv_path) { "/tmp/test.csv" } + + before do + quick_login_as admin + visit main_app.admin_product_import_path + end + + context "when importing to product list" do + def write_tmp_csv_file + CSV.open(tmp_csv_path, "w") do |csv| + csv << ["name", "producer", "category", "on_hand", "price", "units", "unit_type", + "tax_category", "shipping_category"] + 120.times do |i| + csv << ["Imported Product #{i + 1}", producer.name, category.name, 1, "1.00", "500", + "g", tax_category.name, shipping_category.name] + end + end + end + + before { write_tmp_csv_file } + + it "validates and saves all batches" do + # Upload and validate file. + attach_file "file", tmp_csv_path + click_button I18n.t("admin.product_import.index.upload") + proceed_to_validation + + # Check that all rows are validated. + heading = "120 #{I18n.t("admin.product_import.import.products_to_create")}" + find(".panel-header", text: heading).click + expect(page).to have_content "Imported Product 10" + expect(page).to have_content "Imported Product 60" + expect(page).to have_content "Imported Product 110" + + # Save file. + proceed_with_save + + # Be extra patient. + expect_progress_percentages "33%", "67%", "100%" + expect_import_completed + + # Check that all rows are saved. + expect(producer.supplied_products.find_by_name("Imported Product 10")).to be_present + expect(producer.supplied_products.find_by_name("Imported Product 60")).to be_present + expect(producer.supplied_products.find_by_name("Imported Product 110")).to be_present + end + end + end + private - def import_data + def proceed_to_validation expect(page).to have_selector 'a.button.proceed', visible: true click_link I18n.t('admin.product_import.import.import') expect(page).to have_selector 'form.product-import', visible: true @@ -372,8 +467,22 @@ feature "Product Import", js: true do def save_data expect(page).to have_selector 'a.button.proceed', visible: true - click_link I18n.t('admin.product_import.import.save') + proceed_with_save expect(page).to have_selector 'div.save-results', visible: true + expect_import_completed + end + + def expect_progress_percentages(*percentages) + percentages.each do |percentage| + expect(page).to have_selector ".progress-interface", text: percentage + end + end + + def proceed_with_save + click_link I18n.t("admin.product_import.import.save") + end + + def expect_import_completed expect(page).to have_content I18n.t('admin.product_import.save_results.final_results') end end diff --git a/spec/features/admin/shipping_methods_spec.rb b/spec/features/admin/shipping_methods_spec.rb index f6d4935a55..bb65e70255 100644 --- a/spec/features/admin/shipping_methods_spec.rb +++ b/spec/features/admin/shipping_methods_spec.rb @@ -29,10 +29,13 @@ feature 'shipping methods' do fill_in 'shipping_method_name', with: 'Carrier Pidgeon' check "shipping_method_distributor_ids_#{d1.id}" check "shipping_method_distributor_ids_#{d2.id}" - click_button 'Create' + click_button I18n.t("actions.create") + + expect(page).to have_no_button I18n.t("actions.create") # Then the shipping method should have its distributor set - flash_message.should == 'Shipping method "Carrier Pidgeon" has been successfully created!' + message = "Shipping method \"Carrier Pidgeon\" has been successfully created!" + expect(page).to have_flash_message message sm = Spree::ShippingMethod.last sm.name.should == 'Carrier Pidgeon' @@ -98,9 +101,12 @@ feature 'shipping methods' do expect(page).to have_css '.tag-item' end - click_button 'Create' + click_button I18n.t("actions.create") + + expect(page).to have_no_button I18n.t("actions.create") + message = "Shipping method \"Teleport\" has been successfully created!" + expect(page).to have_flash_message message - flash_message.should == 'Shipping method "Teleport" has been successfully created!' expect(first('tags-input .tag-list ti-tag-item')).to have_content "local" shipping_method = Spree::ShippingMethod.find_by_name('Teleport') diff --git a/spec/features/admin/subscriptions_spec.rb b/spec/features/admin/subscriptions_spec.rb index 27bfaa1bfc..99f32162f6 100644 --- a/spec/features/admin/subscriptions_spec.rb +++ b/spec/features/admin/subscriptions_spec.rb @@ -145,23 +145,25 @@ feature 'Subscriptions' do let!(:customer_user) { create(:user) } let!(:credit_card1) { create(:credit_card, user: customer_user, cc_type: 'visa', last_digits: 1111, month: 10, year: 2030) } let!(:customer) { create(:customer, enterprise: shop, bill_address: address, user: customer_user, allow_charges: true) } - let!(:product1) { create(:product, supplier: shop) } - let!(:product2) { create(:product, supplier: shop) } - let!(:variant1) { create(:variant, product: product1, unit_value: '100', price: 12.00, option_values: []) } - let!(:variant2) { create(:variant, product: product2, unit_value: '1000', price: 6.00, option_values: []) } + let!(:test_product) { create(:product, supplier: shop, distributors: []) } + let!(:test_variant) { create(:variant, product: test_product, unit_value: "100", price: 12.00, option_values: []) } + let!(:shop_product) { create(:product, supplier: shop, distributors: [shop]) } + let!(:shop_variant) { create(:variant, product: shop_product, unit_value: "1000", price: 6.00, option_values: []) } let!(:enterprise_fee) { create(:enterprise_fee, amount: 1.75) } let!(:order_cycle) { create(:simple_order_cycle, coordinator: shop, orders_open_at: 2.days.from_now, orders_close_at: 7.days.from_now) } - let!(:outgoing_exchange) { order_cycle.exchanges.create(sender: shop, receiver: shop, variants: [variant1, variant2], enterprise_fees: [enterprise_fee]) } + let!(:outgoing_exchange) { order_cycle.exchanges.create(sender: shop, receiver: shop, variants: [test_variant, shop_variant], enterprise_fees: [enterprise_fee]) } let!(:schedule) { create(:schedule, order_cycles: [order_cycle]) } let!(:payment_method) { create(:stripe_payment_method, name: 'Credit Card', distributors: [shop], preferred_enterprise_id: shop.id) } let!(:shipping_method) { create(:shipping_method, distributors: [shop]) } - it "passes the smoke test" do + before do visit admin_subscriptions_path - click_link 'New Subscription' - select2_select shop.name, from: 'new_subscription_shop_id' - click_button 'Continue' + click_link "New Subscription" + select2_select shop.name, from: "new_subscription_shop_id" + click_button "Continue" + end + it "passes the smoke test" do select2_select customer.email, from: 'customer_id' select2_select schedule.name, from: 'schedule_id' select2_select payment_method.name, from: 'payment_method_id' @@ -215,11 +217,9 @@ feature 'Subscriptions' do expect(page).to have_content 'Please add at least one product' # Adding a product and getting a price estimate - select2_search_async product1.name, from: I18n.t(:name_or_sku), dropdown_css: '.select2-drop' - fill_in 'add_quantity', with: 2 - click_link 'Add' + add_variant_to_subscription test_variant, 2 within 'table#subscription-line-items tr.item', match: :first do - expect(page).to have_selector 'td.description', text: "#{product1.name} - #{variant1.full_name}" + expect(page).to have_selector '.description', text: "#{test_product.name} - #{test_variant.full_name}" expect(page).to have_selector 'td.price', text: "$13.75" expect(page).to have_input 'quantity', with: "2" expect(page).to have_selector 'td.total', text: "$27.50" @@ -241,11 +241,9 @@ feature 'Subscriptions' do click_button('edit-products') # Adding a new product - select2_search_async product2.name, from: I18n.t(:name_or_sku), dropdown_css: '.select2-drop' - fill_in 'add_quantity', with: 3 - click_link 'Add' + add_variant_to_subscription shop_variant, 3 within 'table#subscription-line-items tr.item', match: :first do - expect(page).to have_selector 'td.description', text: "#{product2.name} - #{variant2.full_name}" + expect(page).to have_selector '.description', text: "#{shop_product.name} - #{shop_variant.full_name}" expect(page).to have_selector 'td.price', text: "$7.75" expect(page).to have_input 'quantity', with: "3" expect(page).to have_selector 'td.total', text: "$23.25" @@ -264,7 +262,7 @@ feature 'Subscriptions' do # Prices are shown in the index within 'table#subscription-line-items tr.item', match: :first do - expect(page).to have_selector 'td.description', text: "#{product2.name} - #{variant2.full_name}" + expect(page).to have_selector '.description', text: "#{shop_product.name} - #{shop_variant.full_name}" expect(page).to have_selector 'td.price', text: "$7.75" expect(page).to have_input 'quantity', with: "3" expect(page).to have_selector 'td.total', text: "$23.25" @@ -282,142 +280,249 @@ feature 'Subscriptions' do # Standing Line Items are created expect(subscription.subscription_line_items.count).to eq 1 subscription_line_item = subscription.subscription_line_items.first - expect(subscription_line_item.variant).to eq variant2 + expect(subscription_line_item.variant).to eq shop_variant expect(subscription_line_item.quantity).to eq 3 end + end - context 'editing an existing subscription' do - let!(:customer) { create(:customer, enterprise: shop) } - let!(:product1) { create(:product, supplier: shop) } - let!(:product2) { create(:product, supplier: shop) } - let!(:product3) { create(:product, supplier: shop) } - let!(:variant1) { create(:variant, product: product1, unit_value: '100', price: 12.00, option_values: []) } - let!(:variant2) { create(:variant, product: product2, unit_value: '1000', price: 6.00, option_values: []) } - let!(:variant3) { create(:variant, product: product3, unit_value: '10000', price: 22.00, option_values: []) } - let!(:enterprise_fee) { create(:enterprise_fee, amount: 1.75) } - let!(:order_cycle) { create(:simple_order_cycle, coordinator: shop, orders_open_at: 2.days.from_now, orders_close_at: 7.days.from_now) } - let!(:outgoing_exchange) { order_cycle.exchanges.create(sender: shop, receiver: shop, variants: [variant1, variant2], enterprise_fees: [enterprise_fee]) } - let!(:schedule) { create(:schedule, order_cycles: [order_cycle]) } - let!(:variant3_oc) { create(:simple_order_cycle, coordinator: shop, orders_open_at: 2.days.from_now, orders_close_at: 7.days.from_now) } - let!(:variant3_ex) { variant3_oc.exchanges.create(sender: shop, receiver: shop, variants: [variant3]) } - let!(:payment_method) { create(:payment_method, distributors: [shop]) } - let!(:stripe_payment_method) { create(:stripe_payment_method, name: 'Credit Card', distributors: [shop], preferred_enterprise_id: shop.id) } - let!(:shipping_method) { create(:shipping_method, distributors: [shop]) } - let!(:subscription) { - create(:subscription, - shop: shop, - customer: customer, - schedule: schedule, - payment_method: payment_method, - shipping_method: shipping_method, - subscription_line_items: [create(:subscription_line_item, variant: variant1, quantity: 2, price_estimate: 13.75)], - with_proxy_orders: true) - } + context 'editing an existing subscription' do + let!(:customer) { create(:customer, enterprise: shop) } + let!(:product1) { create(:product, supplier: shop) } + let!(:product2) { create(:product, supplier: shop) } + let!(:product3) { create(:product, supplier: shop) } + let!(:variant1) { create(:variant, product: product1, unit_value: '100', price: 12.00, option_values: []) } + let!(:variant2) { create(:variant, product: product2, unit_value: '1000', price: 6.00, option_values: []) } + let!(:variant3) { create(:variant, product: product3, unit_value: '10000', price: 22.00, option_values: []) } + let!(:enterprise_fee) { create(:enterprise_fee, amount: 1.75) } + let!(:order_cycle) { create(:simple_order_cycle, coordinator: shop, orders_open_at: 2.days.from_now, orders_close_at: 7.days.from_now) } + let!(:outgoing_exchange) { order_cycle.exchanges.create(sender: shop, receiver: shop, variants: [variant1, variant2], enterprise_fees: [enterprise_fee]) } + let!(:schedule) { create(:schedule, order_cycles: [order_cycle]) } + let!(:variant3_oc) { create(:simple_order_cycle, coordinator: shop, orders_open_at: 2.days.from_now, orders_close_at: 7.days.from_now) } + let!(:variant3_ex) { variant3_oc.exchanges.create(sender: shop, receiver: shop, variants: [variant3]) } + let!(:payment_method) { create(:payment_method, distributors: [shop]) } + let!(:stripe_payment_method) { create(:stripe_payment_method, name: 'Credit Card', distributors: [shop], preferred_enterprise_id: shop.id) } + let!(:shipping_method) { create(:shipping_method, distributors: [shop]) } + let!(:subscription) { + create(:subscription, + shop: shop, + customer: customer, + schedule: schedule, + payment_method: payment_method, + shipping_method: shipping_method, + subscription_line_items: [create(:subscription_line_item, variant: variant1, quantity: 2, price_estimate: 13.75)], + with_proxy_orders: true) + } - it "passes the smoke test" do - visit edit_admin_subscription_path(subscription) + it "passes the smoke test" do + visit edit_admin_subscription_path(subscription) - # Customer and Schedule cannot be edited - click_button 'edit-details' - expect(page).to have_selector '#s2id_customer_id.select2-container-disabled' - expect(page).to have_selector '#s2id_schedule_id.select2-container-disabled' + # Customer and Schedule cannot be edited + click_button 'edit-details' + expect(page).to have_selector '#s2id_customer_id.select2-container-disabled' + expect(page).to have_selector '#s2id_schedule_id.select2-container-disabled' - # Can't use a Stripe payment method because customer does not allow it - select2_select stripe_payment_method.name, from: 'payment_method_id' - expect(page).to have_content I18n.t('admin.subscriptions.details.charges_not_allowed') - click_button 'Save Changes' - expect(page).to have_content 'Credit card charges are not allowed by this customer' - select2_select payment_method.name, from: 'payment_method_id' - click_button 'Review' + # Can't use a Stripe payment method because customer does not allow it + select2_select stripe_payment_method.name, from: 'payment_method_id' + expect(page).to have_content I18n.t('admin.subscriptions.details.charges_not_allowed') + click_button 'Save Changes' + expect(page).to have_content 'Credit card charges are not allowed by this customer' + select2_select payment_method.name, from: 'payment_method_id' + click_button 'Review' - # Existing products should be visible - click_button 'edit-products' - within "#sli_0" do - expect(page).to have_selector 'td.description', text: "#{product1.name} - #{variant1.full_name}" - expect(page).to have_selector 'td.price', text: "$13.75" - expect(page).to have_input 'quantity', with: "2" - expect(page).to have_selector 'td.total', text: "$27.50" + # Existing products should be visible + click_button 'edit-products' + within "#sli_0" do + expect(page).to have_selector '.description', text: "#{product1.name} - #{variant1.full_name}" + expect(page).to have_selector 'td.price', text: "$13.75" + expect(page).to have_input 'quantity', with: "2" + expect(page).to have_selector 'td.total', text: "$27.50" - # Remove variant1 from the subscription - find("a.delete-item").click - end - - # Attempting to submit without a product - click_button 'Save Changes' - expect(page).to have_content 'Please add at least one product' - - # Add variant2 to the subscription - select2_search_async product2.name, from: I18n.t(:name_or_sku), dropdown_css: '.select2-drop' - fill_in 'add_quantity', with: 1 - click_link 'Add' - within "#sli_0" do - expect(page).to have_selector 'td.description', text: "#{product2.name} - #{variant2.full_name}" - expect(page).to have_selector 'td.price', text: "$7.75" - expect(page).to have_input 'quantity', with: "1" - expect(page).to have_selector 'td.total', text: "$7.75" - end - - # Total should be $7.75 - expect(page).to have_selector '#order_form_total', text: "$7.75" - - # Add variant3 to the subscription (even though it is not available) - select2_search_async product3.name, from: I18n.t(:name_or_sku), dropdown_css: '.select2-drop' - fill_in 'add_quantity', with: 1 - click_link 'Add' - within "#sli_1" do - expect(page).to have_selector 'td.description', text: "#{product3.name} - #{variant3.full_name}" - expect(page).to have_selector 'td.price', text: "$22.00" - expect(page).to have_input 'quantity', with: "1" - expect(page).to have_selector 'td.total', text: "$22.00" - end - - # Total should be $29.75 - expect(page).to have_selector '#order_form_total', text: "$29.75" - - click_button 'Save Changes' - expect(page).to have_content "#{product3.name} - #{variant3.full_name} is not available from the selected schedule" - - # Remove variant3 from the subscription - within '#sli_1' do - find("a.delete-item").click - end - - click_button 'Save Changes' - expect(page).to have_current_path admin_subscriptions_path - - select2_select shop.name, from: "shop_id" - expect(page).to have_selector "td.items.panel-toggle" - first("td.items.panel-toggle").click - - # Total should be $7.75 - expect(page).to have_selector '#order_form_total', text: "$7.75" - expect(page).to have_selector 'tr.item', count: 1 - expect(subscription.reload.subscription_line_items.length).to eq 1 - expect(subscription.subscription_line_items.first.variant).to eq variant2 + # Remove variant1 from the subscription + find("a.delete-item").click end - context "with initialised order that has been changed" do - let(:proxy_order) { subscription.proxy_orders.first } - let(:order) { proxy_order.initialise_order! } - let(:line_item) { order.line_items.first } + # Attempting to submit without a product + click_button 'Save Changes' + expect(page).to have_content 'Please add at least one product' - before { line_item.update_attributes(quantity: 3) } + # Add variant2 to the subscription + add_variant_to_subscription(variant2, 1) + within "#sli_0" do + expect(page).to have_selector '.description', text: "#{product2.name} - #{variant2.full_name}" + expect(page).to have_selector 'td.price', text: "$7.75" + expect(page).to have_input 'quantity', with: "1" + expect(page).to have_selector 'td.total', text: "$7.75" + end - it "reports issues encountered during the update" do - visit edit_admin_subscription_path(subscription) - click_button 'edit-products' + # Total should be $7.75 + expect(page).to have_selector '#order_form_total', text: "$7.75" - within "#sli_0" do - fill_in 'quantity', with: "1" - end + # Add variant3 to the subscription (even though it is not available) + add_variant_to_subscription(variant3, 1) + within "#sli_1" do + expect(page).to have_selector '.description', text: "#{product3.name} - #{variant3.full_name}" + expect(page).to have_selector 'td.price', text: "$22.00" + expect(page).to have_input 'quantity', with: "1" + expect(page).to have_selector 'td.total', text: "$22.00" + end - click_button 'Save Changes' - expect(page).to have_content 'Saved' + # Total should be $29.75 + expect(page).to have_selector '#order_form_total', text: "$29.75" - expect(page).to have_selector "#order_update_issues_dialog .message", text: I18n.t("admin.subscriptions.order_update_issues_msg") + # Remove variant3 from the subscription + within '#sli_1' do + find("a.delete-item").click + end + + click_button 'Save Changes' + expect(page).to have_current_path admin_subscriptions_path + + select2_select shop.name, from: "shop_id" + expect(page).to have_selector "td.items.panel-toggle" + first("td.items.panel-toggle").click + + # Total should be $7.75 + expect(page).to have_selector '#order_form_total', text: "$7.75" + expect(page).to have_selector 'tr.item', count: 1 + expect(subscription.reload.subscription_line_items.length).to eq 1 + expect(subscription.subscription_line_items.first.variant).to eq variant2 + end + + context "with initialised order that has been changed" do + let(:proxy_order) { subscription.proxy_orders.first } + let(:order) { proxy_order.initialise_order! } + let(:line_item) { order.line_items.first } + + before { line_item.update_attributes(quantity: 3) } + + it "reports issues encountered during the update" do + visit edit_admin_subscription_path(subscription) + click_button 'edit-products' + + within "#sli_0" do + fill_in 'quantity', with: "1" end + + click_button 'Save Changes' + expect(page).to have_content 'Saved' + + expect(page).to have_selector "#order_update_issues_dialog .message", text: I18n.t("admin.subscriptions.order_update_issues_msg") end end end + + describe "allowed variants" do + let!(:customer) { create(:customer, enterprise: shop, allow_charges: true) } + let!(:credit_card) { create(:credit_card, user: customer.user) } + let!(:shop_product) { create(:product, supplier: shop, distributors: [shop]) } + let!(:shop_variant) { create(:variant, product: shop_product, unit_value: "2000") } + let!(:permitted_supplier) do + create(:supplier_enterprise).tap do |supplier| + create(:enterprise_relationship, child: shop, parent: supplier, permissions_list: [:add_to_order_cycle]) + end + end + let!(:permitted_supplier_product) { create(:product, supplier: permitted_supplier, distributors: [shop]) } + let!(:permitted_supplier_variant) { create(:variant, product: permitted_supplier_product, unit_value: "2000") } + let!(:incoming_exchange_product) { create(:product, distributors: [shop]) } + let!(:incoming_exchange_variant) do + create(:variant, product: incoming_exchange_product, unit_value: "2000").tap do |variant| + create(:exchange, order_cycle: order_cycle, incoming: true, receiver: shop, variants: [variant]) + end + end + let!(:outgoing_exchange_product) { create(:product, distributors: [shop]) } + let!(:outgoing_exchange_variant) do + create(:variant, product: outgoing_exchange_product, unit_value: "2000").tap do |variant| + create(:exchange, order_cycle: order_cycle, incoming: false, receiver: shop, variants: [variant]) + end + end + let!(:enterprise_fee) { create(:enterprise_fee, amount: 1.75) } + let!(:order_cycle) { create(:simple_order_cycle, coordinator: shop) } + let!(:schedule) { create(:schedule, order_cycles: [order_cycle]) } + let!(:payment_method) { create(:stripe_payment_method, distributors: [shop], preferred_enterprise_id: shop.id) } + let!(:shipping_method) { create(:shipping_method, distributors: [shop]) } + + before do + visit admin_subscriptions_path + click_link "New Subscription" + select2_select shop.name, from: "new_subscription_shop_id" + click_button "Continue" + end + + it "permit creating and editing of the subscription" do + # Fill in other details + fill_in_subscription_basic_details + click_button "Next" + expect(page).to have_content "BILLING ADDRESS" + click_button "Next" + + # Add products + expect(page).to have_content "NAME OR SKU" + add_variant_to_subscription shop_variant, 3 + expect_not_in_open_or_upcoming_order_cycle_warning 1 + add_variant_to_subscription permitted_supplier_variant, 4 + expect_not_in_open_or_upcoming_order_cycle_warning 2 + add_variant_to_subscription incoming_exchange_variant, 5 + expect_not_in_open_or_upcoming_order_cycle_warning 3 + add_variant_to_subscription outgoing_exchange_variant, 6 + expect_not_in_open_or_upcoming_order_cycle_warning 3 + click_button "Next" + + # Submit form + expect { + click_button "Create Subscription" + expect(page).to have_current_path admin_subscriptions_path + }.to change(Subscription, :count).by(1) + + # Subscription line items are created + subscription = Subscription.last + expect(subscription.subscription_line_items.count).to eq 4 + + # Edit the subscription + visit edit_admin_subscription_path(subscription) + + # Remove shop_variant from the subscription + click_button "edit-products" + within "#sli_0" do + expect(page).to have_selector ".description", text: shop_variant.name + find("a.delete-item").click + end + + # Submit form + click_button "Save Changes" + expect(page).to have_current_path admin_subscriptions_path + + # Subscription is saved + visit edit_admin_subscription_path(subscription) + expect(page).to have_selector "#subscription-line-items .item", count: 3 + end + end + end + + def fill_in_subscription_basic_details + select2_select customer.email, from: "customer_id" + select2_select schedule.name, from: "schedule_id" + select2_select payment_method.name, from: "payment_method_id" + select2_select shipping_method.name, from: "shipping_method_id" + + find_field("begins_at").click + choose_today_from_datepicker + end + + def expect_not_in_open_or_upcoming_order_cycle_warning(count) + expect(page).to have_content variant_not_in_open_or_upcoming_order_cycle_warning, count: count + end + + def add_variant_to_subscription(variant, quantity) + row_count = all("#subscription-line-items .item").length + variant_name = variant.full_name.present? ? "#{variant.name} - #{variant.full_name}" : variant.name + select2_search variant.name, from: I18n.t(:name_or_sku), dropdown_css: ".select2-drop", select_text: variant_name + fill_in "add_quantity", with: quantity + click_link "Add" + expect(page).to have_selector("#subscription-line-items .item", count: row_count + 1) + end + + def variant_not_in_open_or_upcoming_order_cycle_warning + I18n.t("not_in_open_and_upcoming_order_cycles_warning", + scope: "admin.subscriptions.subscription_line_items") end end diff --git a/spec/models/product_importer_spec.rb b/spec/models/product_importer_spec.rb index f05816e538..7a259f53fe 100644 --- a/spec/models/product_importer_spec.rb +++ b/spec/models/product_importer_spec.rb @@ -26,7 +26,7 @@ describe ProductImport::ProductImporter do let!(:variant) { create(:variant, product_id: product.id, price: '8.50', on_hand: '100', unit_value: '500', display_name: 'Preexisting Banana') } let!(:product2) { create(:simple_product, supplier: enterprise, on_hand: '100', name: 'Beans', unit_value: '500', primary_taxon_id: category.id, description: nil) } let!(:product3) { create(:simple_product, supplier: enterprise, on_hand: '100', name: 'Sprouts', unit_value: '500', primary_taxon_id: category.id) } - let!(:product4) { create(:simple_product, supplier: enterprise, on_hand: '100', name: 'Cabbage', unit_value: '500', primary_taxon_id: category.id) } + let!(:product4) { create(:simple_product, supplier: enterprise, on_hand: '100', name: 'Cabbage', unit_value: '1', variant_unit_scale: nil, variant_unit: "items", variant_unit_name: "Whole", primary_taxon_id: category.id) } let!(:product5) { create(:simple_product, supplier: enterprise2, on_hand: '100', name: 'Lettuce', unit_value: '500', primary_taxon_id: category.id) } let!(:product6) { create(:simple_product, supplier: enterprise3, on_hand: '100', name: 'Beetroot', unit_value: '500', on_demand: true, variant_unit_scale: 1, variant_unit: 'weight', primary_taxon_id: category.id, description: nil) } let!(:product7) { create(:simple_product, supplier: enterprise3, on_hand: '100', name: 'Tomato', unit_value: '500', variant_unit_scale: 1, variant_unit: 'weight', primary_taxon_id: category.id, description: nil) } @@ -465,50 +465,106 @@ describe ProductImport::ProductImporter do end describe "importing items into inventory" do - before do - csv_data = CSV.generate do |csv| - csv << ["name", "distributor", "producer", "on_hand", "price", "units", "unit_type"] - csv << ["Beans", "Another Enterprise", "User Enterprise", "5", "3.20", "500", "g"] - csv << ["Sprouts", "Another Enterprise", "User Enterprise", "6", "6.50", "500", "g"] - csv << ["Cabbage", "Another Enterprise", "User Enterprise", "2001", "1.50", "500", "g"] + describe "creating and updating inventory" do + before do + csv_data = CSV.generate do |csv| + csv << ["name", "distributor", "producer", "on_hand", "price", "units", "unit_type", "variant_unit_name"] + csv << ["Beans", "Another Enterprise", "User Enterprise", "5", "3.20", "500", "g", ""] + csv << ["Sprouts", "Another Enterprise", "User Enterprise", "6", "6.50", "500", "g", ""] + csv << ["Cabbage", "Another Enterprise", "User Enterprise", "2001", "1.50", "1", "", "Whole"] + end + File.write('/tmp/test-m.csv', csv_data) + file = File.new('/tmp/test-m.csv') + settings = {'import_into' => 'inventories'} + @importer = ProductImport::ProductImporter.new(file, admin, start: 1, end: 100, settings: settings) end - File.write('/tmp/test-m.csv', csv_data) - file = File.new('/tmp/test-m.csv') - settings = {'import_into' => 'inventories'} - @importer = ProductImport::ProductImporter.new(file, admin, start: 1, end: 100, settings: settings) - end - after { File.delete('/tmp/test-m.csv') } + after { File.delete('/tmp/test-m.csv') } - it "validates entries" do - @importer.validate_entries - entries = JSON.parse(@importer.entries_json) + it "validates entries" do + @importer.validate_entries + entries = JSON.parse(@importer.entries_json) - expect(filter('valid', entries)).to eq 3 - expect(filter('invalid', entries)).to eq 0 - expect(filter('create_inventory', entries)).to eq 2 - expect(filter('update_inventory', entries)).to eq 1 + expect(filter('valid', entries)).to eq 3 + expect(filter('invalid', entries)).to eq 0 + expect(filter('create_inventory', entries)).to eq 2 + expect(filter('update_inventory', entries)).to eq 1 + end + + it "saves and updates inventory" do + @importer.save_entries + + expect(@importer.inventory_created_count).to eq 2 + expect(@importer.inventory_updated_count).to eq 1 + expect(@importer.updated_ids).to be_a(Array) + expect(@importer.updated_ids.count).to eq 3 + + beans_override = VariantOverride.where(variant_id: product2.variants.first.id, hub_id: enterprise2.id).first + sprouts_override = VariantOverride.where(variant_id: product3.variants.first.id, hub_id: enterprise2.id).first + cabbage_override = VariantOverride.where(variant_id: product4.variants.first.id, hub_id: enterprise2.id).first + + expect(Float(beans_override.price)).to eq 3.20 + expect(beans_override.count_on_hand).to eq 5 + + expect(Float(sprouts_override.price)).to eq 6.50 + expect(sprouts_override.count_on_hand).to eq 6 + + expect(Float(cabbage_override.price)).to eq 1.50 + expect(cabbage_override.count_on_hand).to eq 2001 + end end - it "saves and updates inventory" do - @importer.save_entries + describe "updating existing inventory referenced by display_name" do + before do + csv_data = CSV.generate do |csv| + csv << ["name", "display_name", "distributor", "producer", "on_hand", "price", "units"] + csv << ["Oats", "Porridge Oats", "Another Enterprise", "User Enterprise", "900", "", "500"] + end + File.write('/tmp/test-m.csv', csv_data) + file = File.new('/tmp/test-m.csv') + settings = {'import_into' => 'inventories'} + @importer = ProductImport::ProductImporter.new(file, admin, start: 1, end: 100, settings: settings) + end + after { File.delete('/tmp/test-m.csv') } - expect(@importer.inventory_created_count).to eq 2 - expect(@importer.inventory_updated_count).to eq 1 - expect(@importer.updated_ids).to be_a(Array) - expect(@importer.updated_ids.count).to eq 3 + it "updates inventory item correctly" do + @importer.save_entries - beans_override = VariantOverride.where(variant_id: product2.variants.first.id, hub_id: enterprise2.id).first - sprouts_override = VariantOverride.where(variant_id: product3.variants.first.id, hub_id: enterprise2.id).first - cabbage_override = VariantOverride.where(variant_id: product4.variants.first.id, hub_id: enterprise2.id).first + expect(@importer.inventory_created_count).to eq 1 - expect(Float(beans_override.price)).to eq 3.20 - expect(beans_override.count_on_hand).to eq 5 + override = VariantOverride.where(variant_id: variant2.id, hub_id: enterprise2.id).first + visible = InventoryItem.where(variant_id: variant2.id, enterprise_id: enterprise2.id).first.visible - expect(Float(sprouts_override.price)).to eq 6.50 - expect(sprouts_override.count_on_hand).to eq 6 + expect(override.count_on_hand).to eq 900 + expect(visible).to be_truthy + end + end - expect(Float(cabbage_override.price)).to eq 1.50 - expect(cabbage_override.count_on_hand).to eq 2001 + describe "updating existing item that was set to hidden in inventory" do + before do + InventoryItem.create(variant_id: product4.variants.first.id, enterprise_id: enterprise2.id, visible: false) + + csv_data = CSV.generate do |csv| + csv << ["name", "distributor", "producer", "on_hand", "price", "units", "variant_unit_name"] + csv << ["Cabbage", "Another Enterprise", "User Enterprise", "900", "", "1", "Whole"] + end + File.write('/tmp/test-m.csv', csv_data) + file = File.new('/tmp/test-m.csv') + settings = {'import_into' => 'inventories'} + @importer = ProductImport::ProductImporter.new(file, admin, start: 1, end: 100, settings: settings) + end + after { File.delete('/tmp/test-m.csv') } + + it "sets the item to visible in inventory when the item is updated" do + @importer.save_entries + + expect(@importer.inventory_updated_count).to eq 1 + + override = VariantOverride.where(variant_id: product4.variants.first.id, hub_id: enterprise2.id).first + visible = InventoryItem.where(variant_id: product4.variants.first.id, enterprise_id: enterprise2.id).first.visible + + expect(override.count_on_hand).to eq 900 + expect(visible).to be_truthy + end end end diff --git a/spec/services/subscription_validator_spec.rb b/spec/services/subscription_validator_spec.rb index 6670d14fa4..bdcd14bea5 100644 --- a/spec/services/subscription_validator_spec.rb +++ b/spec/services/subscription_validator_spec.rb @@ -1,8 +1,11 @@ +require "spec_helper" + describe SubscriptionValidator do - let(:shop) { instance_double(Enterprise, name: "Shop") } + let(:owner) { create(:user) } + let(:shop) { create(:enterprise, name: "Shop", owner: owner) } describe "delegation" do - let(:subscription) { create(:subscription) } + let(:subscription) { create(:subscription, shop: shop) } let(:validator) { SubscriptionValidator.new(subscription) } it "delegates to subscription" do @@ -438,6 +441,7 @@ describe SubscriptionValidator do context "but some variants are unavailable" do let(:product) { instance_double(Spree::Product, name: "some_name") } + before do allow(validator).to receive(:available_variant_ids) { [variant2.id] } allow(variant1).to receive(:product) { product } @@ -451,7 +455,9 @@ describe SubscriptionValidator do end context "and all requested variants are available" do - before { allow(validator).to receive(:available_variant_ids) { [variant1.id, variant2.id] } } + before do + allow(validator).to receive(:available_variant_ids) { [variant1.id, variant2.id] } + end it "returns true" do expect(validator.valid?).to be true diff --git a/spec/services/subscription_variants_service_spec.rb b/spec/services/subscription_variants_service_spec.rb new file mode 100644 index 0000000000..31d0ff4ca7 --- /dev/null +++ b/spec/services/subscription_variants_service_spec.rb @@ -0,0 +1,130 @@ +require "spec_helper" + +describe SubscriptionVariantsService do + describe "variant eligibility for subscription" do + let!(:shop) { create(:distributor_enterprise) } + let!(:producer) { create(:supplier_enterprise) } + let!(:product) { create(:product, supplier: producer) } + let!(:variant) { product.variants.first } + + let!(:schedule) { create(:schedule, order_cycles: [order_cycle]) } + let!(:subscription) { create(:subscription, shop: shop, schedule: schedule) } + let!(:subscription_line_item) do + create(:subscription_line_item, subscription: subscription, variant: variant) + end + + let(:current_order_cycle) do + create(:simple_order_cycle, coordinator: shop, orders_open_at: 1.week.ago, + orders_close_at: 1.week.from_now) + end + + let(:future_order_cycle) do + create(:simple_order_cycle, coordinator: shop, orders_open_at: 1.week.from_now, + orders_close_at: 2.weeks.from_now) + end + + let(:past_order_cycle) do + create(:simple_order_cycle, coordinator: shop, orders_open_at: 2.weeks.ago, + orders_close_at: 1.week.ago) + end + + let!(:order_cycle) { current_order_cycle } + + context "if the shop is the supplier for the product" do + let!(:producer) { shop } + + it "is eligible" do + expect(described_class.eligible_variants(shop)).to include(variant) + end + end + + context "if the supplier is permitted for the shop" do + let!(:enterprise_relationship) { create(:enterprise_relationship, child: shop, parent: product.supplier, permissions_list: [:add_to_order_cycle]) } + + it "is eligible" do + expect(described_class.eligible_variants(shop)).to include(variant) + end + end + + context "if the variant is involved in an exchange" do + let!(:order_cycle) { create(:simple_order_cycle, coordinator: shop) } + let!(:schedule) { create(:schedule, order_cycles: [order_cycle]) } + + context "if it is an incoming exchange where the shop is the receiver" do + let!(:incoming_exchange) { order_cycle.exchanges.create(sender: product.supplier, receiver: shop, incoming: true, variants: [variant]) } + + it "is not eligible" do + expect(described_class.eligible_variants(shop)).to_not include(variant) + end + end + + context "if it is an outgoing exchange where the shop is the receiver" do + let!(:outgoing_exchange) { order_cycle.exchanges.create(sender: product.supplier, receiver: shop, incoming: false, variants: [variant]) } + + context "if the order cycle is currently open" do + let!(:order_cycle) { current_order_cycle } + + it "is eligible" do + expect(described_class.eligible_variants(shop)).to include(variant) + end + end + + context "if the order cycle opens in the future" do + let!(:order_cycle) { future_order_cycle } + + it "is eligible" do + expect(described_class.eligible_variants(shop)).to include(variant) + end + end + + context "if the order cycle closed in the past" do + let!(:order_cycle) { past_order_cycle } + + it "is eligible" do + expect(described_class.eligible_variants(shop)).to include(variant) + end + end + end + end + + context "if the variant is unrelated" do + it "is not eligible" do + expect(described_class.eligible_variants(shop)).to_not include(variant) + end + end + end + + describe "checking if variant in open and upcoming order cycles" do + let!(:shop) { create(:enterprise) } + let!(:product) { create(:product) } + let!(:variant) { product.variants.first } + let!(:schedule) { create(:schedule) } + + context "if the variant is involved in an exchange" do + let!(:order_cycle) { create(:simple_order_cycle, coordinator: shop) } + let!(:schedule) { create(:schedule, order_cycles: [order_cycle]) } + + context "if it is an incoming exchange where the shop is the receiver" do + let!(:incoming_exchange) { order_cycle.exchanges.create(sender: product.supplier, receiver: shop, incoming: true, variants: [variant]) } + + it "is is false" do + expect(described_class).not_to be_in_open_and_upcoming_order_cycles(shop, schedule, variant) + end + end + + context "if it is an outgoing exchange where the shop is the receiver" do + let!(:outgoing_exchange) { order_cycle.exchanges.create(sender: product.supplier, receiver: shop, incoming: false, variants: [variant]) } + + it "is true" do + expect(described_class).to be_in_open_and_upcoming_order_cycles(shop, schedule, variant) + end + end + end + + context "if the variant is unrelated" do + it "is false" do + expect(described_class).to_not be_in_open_and_upcoming_order_cycles(shop, schedule, variant) + end + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 25dac67716..f404f4c95a 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -136,6 +136,7 @@ RSpec.configure do |config| config.extend Spree::Api::TestingSupport::Setup, :type => :controller config.include Spree::Api::TestingSupport::Helpers, :type => :controller config.include OpenFoodNetwork::ControllerHelper, :type => :controller + config.include Features::DatepickerHelper, type: :feature config.include OpenFoodNetwork::FeatureToggleHelper config.include OpenFoodNetwork::FiltersHelper config.include OpenFoodNetwork::EnterpriseGroupsHelper diff --git a/spec/support/features/datepicker_helper.rb b/spec/support/features/datepicker_helper.rb new file mode 100644 index 0000000000..60221c6635 --- /dev/null +++ b/spec/support/features/datepicker_helper.rb @@ -0,0 +1,33 @@ +module Features + module DatepickerHelper + def choose_today_from_datepicker + within(".ui-datepicker-calendar") do + find(".ui-datepicker-today").click + end + end + + def navigate_datepicker_to_month(date, reference_date = Time.zone.today) + month_and_year = date.strftime("%B %Y") + + until datepicker_month_and_year == month_and_year.upcase + if date < reference_date + navigate_datepicker_to_previous_month + elsif date > reference_date + navigate_datepicker_to_next_month + end + end + end + + def navigate_datepicker_to_previous_month + find('#ui-datepicker-div .ui-datepicker-header .ui-datepicker-prev').click + end + + def navigate_datepicker_to_next_month + find('#ui-datepicker-div .ui-datepicker-header .ui-datepicker-next').click + end + + def datepicker_month_and_year + find("#ui-datepicker-div .ui-datepicker-title").text + end + end +end diff --git a/spec/support/matchers/flash_message_matchers.rb b/spec/support/matchers/flash_message_matchers.rb new file mode 100644 index 0000000000..b781fc1811 --- /dev/null +++ b/spec/support/matchers/flash_message_matchers.rb @@ -0,0 +1,31 @@ +RSpec::Matchers.define :have_flash_message do |message| + match do |node| + @message, @node = message, node + + # Ignore leading and trailing whitespace. Later versions of Capybara have :exact_text option. + # The :exact option is not supported in has_selector?. + message_substring_regex = substring_match_regex(message) + node.has_selector?(".flash", text: message_substring_regex, visible: false) + end + + failure_message do |actual| + "expected to find flash message ##{@message}" + end + + match_when_negated do |node| + @message, @node = message, node + + # Ignore leading and trailing whitespace. Later versions of Capybara have :exact_text option. + # The :exact option is not supported in has_selector?. + message_substring_regex = substring_match_regex(message) + node.has_no_selector?(".flash", text: message_substring_regex, visible: false) + end + + failure_message_when_negated do |actual| + "expected not to find flash message ##{@message}" + end + + def substring_match_regex(text) + /\A\s*#{Regexp.escape(text)}\s*\Z/ + end +end diff --git a/spec/support/request/web_helper.rb b/spec/support/request/web_helper.rb index c37014e637..912c19e445 100644 --- a/spec/support/request/web_helper.rb +++ b/spec/support/request/web_helper.rb @@ -105,6 +105,16 @@ module WebHelper targetted_select2(value, options) end + # Support having different texts to search for and to click in the select2 + # field. + # + # This overrides the method in Spree. + def targetted_select2_search(value, options) + page.execute_script %Q{$('#{options[:from]}').select2('open')} + page.execute_script "$('#{options[:dropdown_css]} input.select2-input').val('#{value}').trigger('keyup-change');" + select_select2_result(options[:select_text] || value) + end + def multi_select2_select(value, options) find("#s2id_#{options[:from]}").find('ul li.select2-search-field').click select_select2_result(value)