diff --git a/.codeclimate.yml b/.codeclimate.yml index c126a85843..14569f6b0f 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -2,7 +2,7 @@ version: "2" plugins: rubocop: enabled: true - channel: "rubocop-0-48" + channel: "rubocop-0-55" scss-lint: enabled: false duplication: @@ -34,3 +34,4 @@ checks: exclude_patterns: - "spec/**/*" - "vendor/**/*" +- "app/assets/javascripts/shared/*" diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 7c620167cc..7468fd5248 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -9,6 +9,8 @@ context for others to understand it] [List which features should be tested and how] +[Should we test on mobile?] + #### Release notes [In case this should be present in the release notes, please write them or diff --git a/.gitignore b/.gitignore index 65877ba8fb..f295094890 100644 --- a/.gitignore +++ b/.gitignore @@ -32,7 +32,6 @@ public/stylesheets public/images public/spree config/abr.yml -config/heroku_env.rb config/newrelic.yml config/initializers/feature_toggle.rb config/initializers/db2fog.rb diff --git a/.rubocop.yml b/.rubocop.yml index 1bd857d1d0..25acf35f57 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -14,6 +14,8 @@ AllCops: - 'vendor/**/*' - 'node_modules/**/*' - !ruby/regexp /old_and_unused\.rb$/ + # The parser gem fails to parse this file with out current Ruby version. + - 'spec/factories.rb' # OFN SETTINGS # Cop settings that have been agreed upon by the OFN community @@ -143,7 +145,11 @@ Style/TrailingCommaInArguments: Enabled: false StyleGuide: http://relaxed.ruby.style/#styletrailingcommainarguments -Style/TrailingCommaInLiteral: +Style/TrailingCommaInArrayLiteral: + Enabled: false + StyleGuide: http://relaxed.ruby.style/#styletrailingcommainliteral + +Style/TrailingCommaInHashLiteral: Enabled: false StyleGuide: http://relaxed.ruby.style/#styletrailingcommainliteral diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 72bef1c992..445f0dbc35 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,20 +1,20 @@ # This configuration was generated by # `rubocop --auto-gen-config --exclude-limit 1400` -# on 2018-02-01 09:48:08 +0100 using RuboCop version 0.49.1. +# on 2018-05-08 14:46:01 +1000 using RuboCop version 0.55.0. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new # versions of RuboCop, may require this file to be generated again. -# Offense count: 37 +# Offense count: 36 # Cop supports --auto-correct. # Configuration parameters: Include, TreatCommentsAsGroupSeparators. -# Include: **/Gemfile, **/gems.rb +# Include: **/*.gemfile, **/Gemfile, **/gems.rb Bundler/OrderedGems: Exclude: - 'Gemfile' -# Offense count: 128 +# Offense count: 124 # Cop supports --auto-correct. Layout/AlignArray: Exclude: @@ -25,12 +25,11 @@ Layout/AlignArray: - 'lib/open_food_network/orders_and_fulfillments_report.rb' - 'lib/open_food_network/packing_report.rb' - 'spec/controllers/spree/orders_controller_spec.rb' - - 'spec/lib/open_food_network/order_and_distributor_report_spec.rb' - 'spec/lib/open_food_network/order_grouper_spec.rb' -# Offense count: 133 +# Offense count: 127 # Cop supports --auto-correct. -# Configuration parameters: EnforcedHashRocketStyle, SupportedHashRocketStyles, EnforcedColonStyle, SupportedColonStyles, EnforcedLastArgumentHashStyle, SupportedLastArgumentHashStyles. +# Configuration parameters: EnforcedHashRocketStyle, EnforcedColonStyle, EnforcedLastArgumentHashStyle. # SupportedHashRocketStyles: key, separator, table # SupportedColonStyles: key, separator, table # SupportedLastArgumentHashStyles: always_inspect, always_ignore, ignore_implicit, ignore_explicit @@ -43,11 +42,11 @@ Layout/AlignHash: - 'lib/open_food_network/packing_report.rb' - 'lib/open_food_network/payments_report.rb' - 'spec/archive/features/consumer/checkout_spec.rb' - - 'spec/factories.rb' - 'spec/features/admin/bulk_product_update_spec.rb' - 'spec/features/admin/order_cycles_spec.rb' - 'spec/features/admin/products_spec.rb' - 'spec/features/admin/variant_overrides_spec.rb' + - 'spec/features/consumer/shopping/cart_spec.rb' - 'spec/lib/open_food_network/customers_report_spec.rb' - 'spec/lib/open_food_network/enterprise_fee_calculator_spec.rb' - 'spec/lib/open_food_network/permissions_spec.rb' @@ -59,9 +58,9 @@ Layout/AlignHash: - 'spec/models/spree/shipping_method_spec.rb' - 'spec/models/spree/variant_spec.rb' -# Offense count: 44 +# Offense count: 62 # Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, SupportedStyles, IndentationWidth. +# Configuration parameters: EnforcedStyle, IndentationWidth. # SupportedStyles: with_first_parameter, with_fixed_indentation Layout/AlignParameters: Exclude: @@ -79,17 +78,35 @@ Layout/AlignParameters: - 'spec/controllers/enterprises_controller_spec.rb' - 'spec/controllers/shop_controller_spec.rb' - 'spec/features/admin/enterprise_relationships_spec.rb' + - 'spec/features/admin/order_cycles_spec.rb' - 'spec/features/consumer/shopping/checkout_spec.rb' - 'spec/helpers/enterprises_helper_spec.rb' - 'spec/lib/open_food_network/user_balance_calculator_spec.rb' + - 'spec/serializers/variant_serializer_spec.rb' - 'spec/support/request/authentication_workflow.rb' -# Offense count: 8 +# Offense count: 5 # Cop supports --auto-correct. -Layout/CommentIndentation: +# Configuration parameters: EnforcedStyleAlignWith. +# SupportedStylesAlignWith: either, start_of_block, start_of_line +Layout/BlockAlignment: Exclude: - - 'Guardfile' - - 'lib/open_food_network/packing_report.rb' + - 'spec/lib/open_food_network/enterprise_fee_calculator_spec.rb' + - 'spec/models/enterprise_spec.rb' + - 'spec/models/spree/line_item_spec.rb' + - 'spec/models/spree/product_spec.rb' + +# Offense count: 1 +# Cop supports --auto-correct. +Layout/BlockEndNewline: + Exclude: + - 'spec/features/consumer/shopping/cart_spec.rb' + +# Offense count: 1 +# Cop supports --auto-correct. +Layout/ClosingParenthesisIndentation: + Exclude: + - 'spec/features/admin/order_cycles_spec.rb' # Offense count: 8 # Cop supports --auto-correct. @@ -102,11 +119,10 @@ Layout/ElseAlignment: - 'app/serializers/api/admin/order_cycle_serializer.rb' - 'lib/open_food_network/sales_tax_report.rb' -# Offense count: 210 +# Offense count: 209 # Cop supports --auto-correct. Layout/EmptyLines: Exclude: - - 'Guardfile' - 'app/controllers/admin/enterprise_fees_controller.rb' - 'app/controllers/admin/order_cycles_controller.rb' - 'app/controllers/admin/producer_properties_controller.rb' @@ -164,12 +180,10 @@ Layout/EmptyLines: - 'lib/open_food_network/enterprise_fee_calculator.rb' - 'lib/open_food_network/enterprise_issue_validator.rb' - 'lib/open_food_network/integrity_checker.rb' - - 'lib/open_food_network/last_used_address.rb' - 'lib/open_food_network/lettuce_share_report.rb' - 'lib/open_food_network/option_value_namer.rb' - 'lib/open_food_network/order_cycle_form_applicator.rb' - 'lib/open_food_network/order_cycle_permissions.rb' - - 'lib/open_food_network/permissions.rb' - 'lib/open_food_network/products_cache.rb' - 'lib/open_food_network/products_cache_integrity_checker.rb' - 'lib/open_food_network/products_cache_refreshment.rb' @@ -179,7 +193,6 @@ Layout/EmptyLines: - 'lib/open_food_network/sales_tax_report.rb' - 'lib/open_food_network/scope_product_to_hub.rb' - 'lib/open_food_network/scope_variant_to_hub.rb' - - 'lib/open_food_network/xero_invoices_report.rb' - 'lib/spree/core/controller_helpers/order_decorator.rb' - 'lib/tasks/cache.rake' - 'lib/tasks/dev.rake' @@ -189,13 +202,13 @@ Layout/EmptyLines: - 'spec/controllers/admin/column_preferences_controller_spec.rb' - 'spec/controllers/admin/enterprises_controller_spec.rb' - 'spec/controllers/admin/order_cycles_controller_spec.rb' + - 'spec/controllers/admin/subscription_line_items_controller_spec.rb' - 'spec/controllers/enterprises_controller_spec.rb' - 'spec/controllers/shop_controller_spec.rb' - 'spec/controllers/spree/admin/payments_controller_spec.rb' - 'spec/controllers/spree/api/products_controller_spec.rb' - 'spec/controllers/spree/checkout_controller_spec.rb' - 'spec/controllers/spree/orders_controller_spec.rb' - - 'spec/factories.rb' - 'spec/features/admin/bulk_product_update_spec.rb' - 'spec/features/admin/enterprise_relationships_spec.rb' - 'spec/features/admin/enterprise_roles_spec.rb' @@ -219,7 +232,6 @@ Layout/EmptyLines: - 'spec/jobs/heartbeat_job_spec.rb' - 'spec/lib/open_food_network/enterprise_fee_calculator_spec.rb' - 'spec/lib/open_food_network/option_value_namer_spec.rb' - - 'spec/lib/open_food_network/order_and_distributor_report_spec.rb' - 'spec/lib/open_food_network/order_cycle_form_applicator_spec.rb' - 'spec/lib/open_food_network/order_cycle_management_report_spec.rb' - 'spec/lib/open_food_network/order_cycle_permissions_spec.rb' @@ -232,6 +244,7 @@ Layout/EmptyLines: - 'spec/models/model_set_spec.rb' - 'spec/models/product_distribution_spec.rb' - 'spec/models/spree/adjustment_spec.rb' + - 'spec/models/spree/line_item_spec.rb' - 'spec/models/spree/order_populator_spec.rb' - 'spec/models/spree/order_spec.rb' - 'spec/models/spree/product_spec.rb' @@ -245,15 +258,19 @@ Layout/EmptyLines: - 'spec/support/delayed_job_helper.rb' - 'spec/support/matchers/table_matchers.rb' -# Offense count: 66 +# Offense count: 1 # Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, SupportedStyles. +Layout/EmptyLinesAroundArguments: + Exclude: + - 'spec/archive/features/consumer/checkout_spec.rb' + +# Offense count: 65 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle. # SupportedStyles: empty_lines, no_empty_lines Layout/EmptyLinesAroundBlockBody: Exclude: - 'app/controllers/spree/admin/orders_controller_decorator.rb' - - 'app/controllers/spree/admin/reports_controller_decorator.rb' - - 'app/controllers/spree/admin/variants_controller_decorator.rb' - 'app/controllers/spree/api/orders_controller_decorator.rb' - 'app/controllers/spree/api/products_controller_decorator.rb' - 'app/controllers/spree/checkout_controller_decorator.rb' @@ -267,11 +284,11 @@ Layout/EmptyLinesAroundBlockBody: - 'lib/tasks/users.rake' - 'spec/controllers/admin/tag_rules_controller_spec.rb' - 'spec/controllers/cart_controller_spec.rb' - - 'spec/controllers/enterprise_confirmations_controller_spec.rb' - 'spec/controllers/spree/admin/orders_controller_spec.rb' - 'spec/controllers/spree/admin/reports_controller_spec.rb' - 'spec/controllers/spree/api/orders_controller_spec.rb' - 'spec/controllers/spree/orders_controller_spec.rb' + - 'spec/controllers/user_confirmations_controller_spec.rb' - 'spec/controllers/user_registrations_controller_spec.rb' - 'spec/features/admin/caching_spec.rb' - 'spec/features/admin/orders_spec.rb' @@ -287,7 +304,6 @@ Layout/EmptyLinesAroundBlockBody: - 'spec/jobs/update_billable_periods_spec.rb' - 'spec/lib/open_food_network/group_buy_report_spec.rb' - 'spec/lib/open_food_network/lettuce_share_report_spec.rb' - - 'spec/lib/open_food_network/order_and_distributor_report_spec.rb' - 'spec/lib/open_food_network/order_grouper_spec.rb' - 'spec/lib/open_food_network/referer_parser_spec.rb' - 'spec/lib/open_food_network/user_balance_calculator_spec.rb' @@ -307,8 +323,8 @@ Layout/EmptyLinesAroundBlockBody: # Offense count: 27 # Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, SupportedStyles. -# SupportedStyles: empty_lines, empty_lines_except_namespace, empty_lines_special, no_empty_lines +# Configuration parameters: EnforcedStyle. +# SupportedStyles: empty_lines, empty_lines_except_namespace, empty_lines_special, no_empty_lines, beginning_only, ending_only Layout/EmptyLinesAroundClassBody: Exclude: - 'app/controllers/admin/account_controller.rb' @@ -336,7 +352,19 @@ Layout/EmptyLinesAroundClassBody: - 'lib/open_food_network/rack_request_blocker.rb' - 'lib/open_food_network/reports/bulk_coop_report.rb' -# Offense count: 55 +# Offense count: 5 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyleAlignWith, AutoCorrect, Severity. +# SupportedStylesAlignWith: keyword, variable, start_of_line +Layout/EndAlignment: + Exclude: + - 'app/controllers/admin/order_cycles_controller.rb' + - 'app/controllers/api/order_cycles_controller.rb' + - 'app/serializers/api/admin/for_order_cycle/enterprise_serializer.rb' + - 'app/serializers/api/admin/for_order_cycle/supplied_product_serializer.rb' + - 'app/serializers/api/admin/order_cycle_serializer.rb' + +# Offense count: 53 # Cop supports --auto-correct. # Configuration parameters: AllowForAlignment, ForceEqualSignAlignment. Layout/ExtraSpacing: @@ -356,7 +384,6 @@ Layout/ExtraSpacing: - 'lib/spree/product_filters.rb' - 'lib/tasks/karma.rake' - 'spec/controllers/admin/accounts_and_billing_settings_controller_spec.rb' - - 'spec/factories.rb' - 'spec/features/admin/bulk_order_management_spec.rb' - 'spec/features/admin/bulk_product_update_spec.rb' - 'spec/features/admin/orders_spec.rb' @@ -366,7 +393,6 @@ Layout/ExtraSpacing: - 'spec/features/consumer/shopping/shopping_spec.rb' - 'spec/lib/open_food_network/enterprise_fee_calculator_spec.rb' - 'spec/lib/open_food_network/reports/rule_spec.rb' - - 'spec/lib/open_food_network/xero_invoices_report_spec.rb' - 'spec/models/enterprise_fee_spec.rb' - 'spec/models/enterprise_spec.rb' - 'spec/models/order_cycle_spec.rb' @@ -379,16 +405,16 @@ Layout/ExtraSpacing: - 'spec/spec_helper.rb' - 'spec/support/request/web_helper.rb' -# Offense count: 3 +# Offense count: 2 # Cop supports --auto-correct. -# Configuration parameters: SupportedStyles, IndentationWidth. +# Configuration parameters: IndentationWidth. # SupportedStyles: special_inside_parentheses, consistent, align_brackets Layout/IndentArray: EnforcedStyle: consistent # Offense count: 52 # Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, SupportedStyles, IndentationWidth. +# Configuration parameters: EnforcedStyle, IndentationWidth. # SupportedStyles: special_inside_parentheses, consistent, align_braces Layout/IndentHash: Exclude: @@ -409,22 +435,20 @@ Layout/IndentHash: - 'spec/lib/open_food_network/order_cycle_form_applicator_spec.rb' - 'spec/support/request/authentication_workflow.rb' -# Offense count: 21 +# Offense count: 20 # Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, SupportedStyles. +# Configuration parameters: EnforcedStyle. # SupportedStyles: normal, rails Layout/IndentationConsistency: Exclude: - - 'app/controllers/spree/admin/reports_controller_decorator.rb' - 'lib/open_food_network/permissions.rb' - - 'spec/controllers/admin/order_cycles_controller_spec.rb' - 'spec/controllers/admin/tag_rules_controller_spec.rb' - 'spec/features/consumer/shopping/checkout_spec.rb' - 'spec/helpers/admin/business_model_configuration_helper_spec.rb' - 'spec/models/spree/line_item_spec.rb' - 'spec/models/spree/product_spec.rb' -# Offense count: 18 +# Offense count: 21 # Cop supports --auto-correct. # Configuration parameters: Width, IgnoredPatterns. Layout/IndentationWidth: @@ -443,14 +467,15 @@ Layout/IndentationWidth: - 'spec/lib/open_food_network/enterprise_fee_calculator_spec.rb' - 'spec/lib/open_food_network/order_grouper_spec.rb' - 'spec/lib/spree/product_filters_spec.rb' + - 'spec/mailers/enterprise_mailer_spec.rb' - 'spec/models/enterprise_spec.rb' + - 'spec/models/spree/calculator/flexi_rate_spec.rb' -# Offense count: 78 +# Offense count: 51 # Cop supports --auto-correct. Layout/LeadingCommentSpace: Exclude: - 'Gemfile' - - 'Guardfile' - 'app/models/billable_period.rb' - 'app/models/content_configuration.rb' - 'app/models/product_importer.rb' @@ -464,11 +489,9 @@ Layout/LeadingCommentSpace: - 'lib/tasks/users.rake' - 'spec/archive/features/consumer/checkout_spec.rb' - 'spec/controllers/spree/api/line_items_controller_spec.rb' - - 'spec/factories.rb' - 'spec/features/admin/products_spec.rb' - 'spec/features/admin/reports_spec.rb' - 'spec/jobs/finalize_account_invoices_spec.rb' - - 'spec/lib/open_food_network/order_and_distributor_report_spec.rb' - 'spec/lib/open_food_network/order_grouper_spec.rb' - 'spec/lib/open_food_network/user_balance_calculator_spec.rb' - 'spec/models/enterprise_spec.rb' @@ -484,11 +507,11 @@ Layout/MultilineBlockLayout: Exclude: - 'app/models/spree/calculator/default_tax_decorator.rb' - 'app/models/spree/product_decorator.rb' - - 'lib/open_food_network/users_and_enterprises_report.rb' - 'spec/controllers/admin/column_preferences_controller_spec.rb' - 'spec/controllers/shop_controller_spec.rb' - 'spec/controllers/spree/orders_controller_spec.rb' - 'spec/features/admin/variant_overrides_spec.rb' + - 'spec/features/consumer/shopping/cart_spec.rb' - 'spec/helpers/enterprises_helper_spec.rb' - 'spec/jobs/update_billable_periods_spec.rb' - 'spec/lib/open_food_network/enterprise_fee_calculator_spec.rb' @@ -502,7 +525,7 @@ Layout/MultilineBlockLayout: # Offense count: 6 # Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, SupportedStyles. +# Configuration parameters: EnforcedStyle. # SupportedStyles: symmetrical, new_line, same_line Layout/MultilineHashBraceLayout: Exclude: @@ -512,9 +535,9 @@ Layout/MultilineHashBraceLayout: - 'spec/controllers/admin/order_cycles_controller_spec.rb' - 'spec/support/request/authentication_workflow.rb' -# Offense count: 6 +# Offense count: 7 # Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, SupportedStyles. +# Configuration parameters: EnforcedStyle. # SupportedStyles: symmetrical, new_line, same_line Layout/MultilineMethodCallBraceLayout: Exclude: @@ -522,22 +545,21 @@ Layout/MultilineMethodCallBraceLayout: - 'app/models/spree/variant_decorator.rb' - 'app/overrides/add_capture_order_shortcut.rb' - 'lib/open_food_network/products_renderer.rb' + - 'spec/features/admin/order_cycles_spec.rb' - 'spec/lib/open_food_network/products_and_inventory_report_spec.rb' -# Offense count: 7 +# Offense count: 4 # Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, SupportedStyles, IndentationWidth. +# Configuration parameters: EnforcedStyle, IndentationWidth. # SupportedStyles: aligned, indented, indented_relative_to_receiver Layout/MultilineMethodCallIndentation: Exclude: - - 'spec/controllers/spree/admin/payments_controller_spec.rb' - 'spec/lib/open_food_network/cached_products_renderer_spec.rb' - - 'spec/requests/checkout/paypal_spec.rb' - 'spec/serializers/variant_serializer_spec.rb' -# Offense count: 33 +# Offense count: 32 # Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, SupportedStyles, IndentationWidth. +# Configuration parameters: EnforcedStyle, IndentationWidth. # SupportedStyles: aligned, indented Layout/MultilineOperationIndentation: Exclude: @@ -555,7 +577,6 @@ Layout/MultilineOperationIndentation: - 'lib/open_food_network/products_cache_refreshment.rb' - 'lib/open_food_network/sales_tax_report.rb' - 'lib/open_food_network/users_and_enterprises_report.rb' - - 'spec/factories.rb' # Offense count: 7 # Cop supports --auto-correct. @@ -567,7 +588,7 @@ Layout/SpaceAfterColon: - 'spec/models/variant_override_spec.rb' - 'spec/spec_helper.rb' -# Offense count: 53 +# Offense count: 85 # Cop supports --auto-correct. Layout/SpaceAfterComma: Exclude: @@ -589,8 +610,9 @@ Layout/SpaceAfterComma: - 'spec/features/admin/variant_overrides_spec.rb' - 'spec/jobs/update_account_invoices_spec.rb' - 'spec/lib/open_food_network/group_buy_report_spec.rb' - - 'spec/lib/open_food_network/order_and_distributor_report_spec.rb' + - 'spec/lib/open_food_network/subscription_summary_spec.rb' - 'spec/models/content_configuration_spec.rb' + - 'spec/models/spree/line_item_spec.rb' - 'spec/models/spree/order_spec.rb' - 'spec/models/tag_rule/discount_order_spec.rb' - 'spec/models/tag_rule/filter_order_cycles_spec.rb' @@ -600,16 +622,15 @@ Layout/SpaceAfterComma: - 'spec/spec_helper.rb' - 'spec/support/cancan_helper.rb' -# Offense count: 3 +# Offense count: 2 # Cop supports --auto-correct. Layout/SpaceAfterSemicolon: Exclude: - 'spec/controllers/spree/admin/base_controller_spec.rb' - - 'spec/models/enterprise_spec.rb' # Offense count: 65 # Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, SupportedStyles. +# Configuration parameters: EnforcedStyle. # SupportedStyles: space, no_space Layout/SpaceAroundEqualsInParameterDefault: Exclude: @@ -644,7 +665,6 @@ Layout/SpaceAroundEqualsInParameterDefault: - 'lib/open_food_network/permissions.rb' - 'lib/open_food_network/scope_variant_to_hub.rb' - 'lib/open_food_network/tag_rule_applicator.rb' - - 'lib/open_food_network/xero_invoices_report.rb' - 'lib/spree/money_decorator.rb' - 'spec/features/admin/enterprise_relationships_spec.rb' - 'spec/features/admin/reports_spec.rb' @@ -653,7 +673,7 @@ Layout/SpaceAroundEqualsInParameterDefault: - 'spec/support/request/distribution_helper.rb' - 'spec/support/request/web_helper.rb' -# Offense count: 61 +# Offense count: 59 # Cop supports --auto-correct. # Configuration parameters: AllowForAlignment. Layout/SpaceAroundOperators: @@ -668,13 +688,10 @@ Layout/SpaceAroundOperators: - 'app/overrides/remove_side_bar.rb' - 'app/overrides/replace_shipping_address_form_with_distributor_details.rb' - 'app/serializers/api/enterprise_serializer.rb' - - 'lib/open_food_network/xero_invoices_report.rb' - 'lib/spree/product_filters.rb' - 'spec/controllers/admin/enterprises_controller_spec.rb' - 'spec/controllers/cart_controller_spec.rb' - 'spec/controllers/spree/orders_controller_spec.rb' - - 'spec/controllers/user_passwords_controller_spec.rb' - - 'spec/factories.rb' - 'spec/features/admin/bulk_order_management_spec.rb' - 'spec/features/admin/bulk_product_update_spec.rb' - 'spec/features/consumer/shopping/checkout_spec.rb' @@ -688,45 +705,57 @@ Layout/SpaceAroundOperators: - 'spec/support/cancan_helper.rb' - 'spec/support/seeds.rb' -# Offense count: 6 +# Offense count: 5 # Cop supports --auto-correct. Layout/SpaceBeforeComma: Exclude: - 'app/helpers/checkout_helper.rb' - - 'app/models/spree/ability_decorator.rb' - 'lib/open_food_network/orders_and_fulfillments_report.rb' - 'spec/features/admin/bulk_order_management_spec.rb' - 'spec/features/admin/bulk_product_update_spec.rb' -# Offense count: 6 +# Offense count: 4 # Cop supports --auto-correct. # Configuration parameters: AllowForAlignment. Layout/SpaceBeforeFirstArg: Exclude: - - 'spec/factories.rb' - 'spec/features/admin/bulk_product_update_spec.rb' - - 'spec/features/admin/product_import_spec.rb' - 'spec/features/consumer/multilingual_spec.rb' - 'spec/models/enterprise_fee_spec.rb' -# Offense count: 1 -# Cop supports --auto-correct. -Layout/SpaceBeforeSemicolon: - Exclude: - - 'spec/models/enterprise_spec.rb' - # Offense count: 5 # Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, SupportedStyles. +# Configuration parameters: EnforcedStyle. # SupportedStyles: require_no_space, require_space Layout/SpaceInLambdaLiteral: Exclude: - 'app/models/spree/product_decorator.rb' - 'app/models/spree/variant_decorator.rb' -# Offense count: 187 +# Offense count: 129 # Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, SupportedStyles, EnforcedStyleForEmptyBraces, SupportedStylesForEmptyBraces, SpaceBeforeBlockParameters. +# Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBrackets. +# SupportedStyles: space, no_space, compact +# SupportedStylesForEmptyBrackets: space, no_space +Layout/SpaceInsideArrayLiteralBrackets: + Exclude: + - 'app/controllers/admin/order_cycles_controller.rb' + - 'app/helpers/spree/reports_helper.rb' + - 'lib/open_food_network/bulk_coop_report.rb' + - 'lib/open_food_network/orders_and_fulfillments_report.rb' + - 'lib/open_food_network/packing_report.rb' + - 'lib/open_food_network/payments_report.rb' + - 'lib/open_food_network/users_and_enterprises_report.rb' + - 'spec/controllers/admin/variant_overrides_controller_spec.rb' + - 'spec/controllers/cart_controller_spec.rb' + - 'spec/features/admin/reports_spec.rb' + - 'spec/jobs/update_billable_periods_spec.rb' + - 'spec/lib/open_food_network/order_grouper_spec.rb' + - 'spec/lib/open_food_network/users_and_enterprises_report_spec.rb' + +# Offense count: 192 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBraces, SpaceBeforeBlockParameters. # SupportedStyles: space, no_space # SupportedStylesForEmptyBraces: space, no_space Layout/SpaceInsideBlockBraces: @@ -749,7 +778,6 @@ Layout/SpaceInsideBlockBraces: - 'spec/controllers/spree/admin/base_controller_spec.rb' - 'spec/controllers/spree/admin/orders_controller_spec.rb' - 'spec/controllers/spree/admin/search_controller_spec.rb' - - 'spec/factories.rb' - 'spec/features/admin/bulk_order_management_spec.rb' - 'spec/features/admin/customers_spec.rb' - 'spec/features/admin/tag_rules_spec.rb' @@ -769,7 +797,6 @@ Layout/SpaceInsideBlockBraces: - 'spec/lib/open_food_network/tag_rule_applicator_spec.rb' - 'spec/models/column_preference_spec.rb' - 'spec/models/enterprise_relationship_spec.rb' - - 'spec/models/enterprise_spec.rb' - 'spec/models/spree/line_item_spec.rb' - 'spec/models/spree/order_spec.rb' - 'spec/models/spree/payment_spec.rb' @@ -780,32 +807,9 @@ Layout/SpaceInsideBlockBraces: - 'spec/spec_helper.rb' - 'spec/support/cancan_helper.rb' -# Offense count: 140 +# Offense count: 786 # Cop supports --auto-correct. -Layout/SpaceInsideBrackets: - Exclude: - - 'app/controllers/admin/order_cycles_controller.rb' - - 'app/helpers/spree/reports_helper.rb' - - 'app/models/enterprise.rb' - - 'app/serializers/api/admin/exchange_serializer.rb' - - 'lib/open_food_network/bulk_coop_report.rb' - - 'lib/open_food_network/order_and_distributor_report.rb' - - 'lib/open_food_network/orders_and_fulfillments_report.rb' - - 'lib/open_food_network/packing_report.rb' - - 'lib/open_food_network/payments_report.rb' - - 'lib/open_food_network/users_and_enterprises_report.rb' - - 'spec/controllers/admin/variant_overrides_controller_spec.rb' - - 'spec/controllers/cart_controller_spec.rb' - - 'spec/features/admin/reports_spec.rb' - - 'spec/jobs/update_billable_periods_spec.rb' - - 'spec/lib/open_food_network/order_and_distributor_report_spec.rb' - - 'spec/lib/open_food_network/order_grouper_spec.rb' - - 'spec/lib/open_food_network/users_and_enterprises_report_spec.rb' - - 'spec/performance/orders_controller_spec.rb' - -# Offense count: 778 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, SupportedStyles, EnforcedStyleForEmptyBraces, SupportedStylesForEmptyBraces. +# Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBraces. # SupportedStyles: space, no_space, compact # SupportedStylesForEmptyBraces: space, no_space Layout/SpaceInsideHashLiteralBraces: @@ -814,12 +818,10 @@ Layout/SpaceInsideHashLiteralBraces: - 'app/controllers/admin/contents_controller.rb' - 'app/controllers/admin/enterprise_relationships_controller.rb' - 'app/controllers/admin/enterprise_roles_controller.rb' - - 'app/controllers/admin/order_cycles_controller.rb' - 'app/controllers/api/statuses_controller.rb' - 'app/controllers/checkout_controller.rb' - 'app/controllers/spree/admin/line_items_controller_decorator.rb' - 'app/controllers/spree/admin/products_controller_decorator.rb' - - 'app/controllers/spree/admin/reports_controller_decorator.rb' - 'app/controllers/spree/admin/search_controller_decorator.rb' - 'app/controllers/spree/orders_controller_decorator.rb' - 'app/helpers/admin/business_model_configuration_helper.rb' @@ -853,12 +855,13 @@ Layout/SpaceInsideHashLiteralBraces: - 'lib/open_food_network/reports/rule.rb' - 'lib/open_food_network/sales_tax_report.rb' - 'lib/open_food_network/variant_and_line_item_naming.rb' - - 'lib/open_food_network/xero_invoices_report.rb' - 'lib/tasks/users.rake' - 'spec/controllers/admin/accounts_and_billing_settings_controller_spec.rb' - 'spec/controllers/admin/business_model_configuration_controller_spec.rb' - 'spec/controllers/admin/enterprises_controller_spec.rb' + - 'spec/controllers/admin/manager_invitations_controller_spec.rb' - 'spec/controllers/admin/order_cycles_controller_spec.rb' + - 'spec/controllers/admin/subscriptions_controller_spec.rb' - 'spec/controllers/api/statuses_controller_spec.rb' - 'spec/controllers/cart_controller_spec.rb' - 'spec/controllers/checkout_controller_spec.rb' @@ -874,7 +877,6 @@ Layout/SpaceInsideHashLiteralBraces: - 'spec/controllers/spree/user_sessions_controller_spec.rb' - 'spec/controllers/user_passwords_controller_spec.rb' - 'spec/controllers/user_registrations_controller_spec.rb' - - 'spec/factories.rb' - 'spec/features/admin/accounts_and_billing_settings_spec.rb' - 'spec/features/admin/image_settings_spec.rb' - 'spec/features/admin/products_spec.rb' @@ -891,7 +893,6 @@ Layout/SpaceInsideHashLiteralBraces: - 'spec/lib/open_food_network/reports/report_spec.rb' - 'spec/lib/open_food_network/reports/rule_spec.rb' - 'spec/lib/open_food_network/tag_rule_applicator_spec.rb' - - 'spec/lib/open_food_network/xero_invoices_report_spec.rb' - 'spec/lib/stripe/account_connector_spec.rb' - 'spec/models/customer_spec.rb' - 'spec/models/enterprise_fee_spec.rb' @@ -914,15 +915,27 @@ Layout/SpaceInsideHashLiteralBraces: - 'spec/requests/checkout/failed_checkout_spec.rb' - 'spec/requests/checkout/stripe_connect_spec.rb' - 'spec/serializers/enterprise_serializer_spec.rb' + - 'spec/services/order_syncer_spec.rb' + - 'spec/services/subscription_form_spec.rb' - 'spec/spec_helper.rb' - 'spec/support/cancan_helper.rb' - 'spec/support/request/authentication_workflow.rb' - 'spec/support/request/shop_workflow.rb' - 'spec/support/seeds.rb' +# Offense count: 4 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBrackets. +# SupportedStyles: space, no_space +# SupportedStylesForEmptyBrackets: space, no_space +Layout/SpaceInsideReferenceBrackets: + Exclude: + - 'app/serializers/api/admin/exchange_serializer.rb' + - 'spec/performance/orders_controller_spec.rb' + # Offense count: 10 # Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, SupportedStyles. +# Configuration parameters: EnforcedStyle. # SupportedStyles: space, no_space Layout/SpaceInsideStringInterpolation: Exclude: @@ -930,8 +943,9 @@ Layout/SpaceInsideStringInterpolation: - 'lib/open_food_network/users_and_enterprises_report.rb' - 'spec/support/request/web_helper.rb' -# Offense count: 6 +# Offense count: 5 # Cop supports --auto-correct. +# Configuration parameters: IndentationWidth. Layout/Tab: Exclude: - 'app/controllers/admin/invoice_settings_controller.rb' @@ -939,8 +953,9 @@ Layout/Tab: - 'spec/lib/spree/product_filters_spec.rb' - 'spec/models/spree/line_item_spec.rb' -# Offense count: 67 +# Offense count: 62 # Cop supports --auto-correct. +# Configuration parameters: AllowInHeredoc. Layout/TrailingWhitespace: Exclude: - 'app/models/distributor_shipping_method.rb' @@ -953,7 +968,6 @@ Layout/TrailingWhitespace: - 'app/views/json/_enterprises.rabl' - 'app/views/json/_producer.rabl' - 'app/views/json/partials/_producer.rabl' - - 'lib/tasks/dev.rake' - 'spec/controllers/admin/column_preferences_controller_spec.rb' - 'spec/controllers/shop_controller_spec.rb' - 'spec/features/admin/enterprise_user_spec.rb' @@ -964,52 +978,25 @@ Layout/TrailingWhitespace: - 'spec/lib/open_food_network/order_grouper_spec.rb' - 'spec/lib/open_food_network/permissions_spec.rb' - 'spec/lib/open_food_network/user_balance_calculator_spec.rb' - - 'spec/models/order_cycle_spec.rb' - - 'spec/models/spree/product_spec.rb' - 'spec/models/spree/variant_spec.rb' - 'spec/serializers/admin/enterprise_serializer_spec.rb' - 'spec/serializers/enterprise_serializer_spec.rb' - 'spec/support/request/menu_helper.rb' - 'spec/views/json/producers.json.rabl_spec.rb' -# Offense count: 5 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyleAlignWith, SupportedStylesAlignWith. -# SupportedStylesAlignWith: either, start_of_block, start_of_line -Lint/BlockAlignment: - Exclude: - - 'spec/lib/open_food_network/enterprise_fee_calculator_spec.rb' - - 'spec/models/enterprise_spec.rb' - - 'spec/models/spree/line_item_spec.rb' - - 'spec/models/spree/product_spec.rb' - -# Offense count: 1 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyleAlignWith, SupportedStylesAlignWith, AutoCorrect. -# SupportedStylesAlignWith: start_of_line, def -Lint/DefEndAlignment: - Exclude: - - 'app/models/spree/line_item_decorator.rb' - # Offense count: 1 # Cop supports --auto-correct. Lint/DeprecatedClassMethods: Exclude: - 'app/controllers/admin/product_import_controller.rb' -# Offense count: 5 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyleAlignWith, SupportedStylesAlignWith, AutoCorrect. -# SupportedStylesAlignWith: keyword, variable, start_of_line -Lint/EndAlignment: +# Offense count: 4 +Lint/DuplicateMethods: Exclude: - - 'app/controllers/admin/order_cycles_controller.rb' - - 'app/controllers/api/order_cycles_controller.rb' - - 'app/serializers/api/admin/for_order_cycle/enterprise_serializer.rb' - - 'app/serializers/api/admin/for_order_cycle/supplied_product_serializer.rb' - - 'app/serializers/api/admin/order_cycle_serializer.rb' + - 'lib/discourse/single_sign_on.rb' + - 'lib/open_food_network/subscription_summary.rb' -# Offense count: 17 +# Offense count: 18 Lint/IneffectiveAccessModifier: Exclude: - 'app/models/column_preference.rb' @@ -1022,7 +1009,7 @@ Lint/IneffectiveAccessModifier: # Offense count: 2 # Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, SupportedStyles. +# Configuration parameters: EnforcedStyle. # SupportedStyles: runtime_error, standard_error Lint/InheritException: Exclude: @@ -1030,7 +1017,7 @@ Lint/InheritException: - 'lib/open_food_network/products_renderer.rb' # Offense count: 1 -Lint/LiteralInCondition: +Lint/LiteralAsCondition: Exclude: - 'lib/open_food_network/rack_request_blocker.rb' @@ -1040,6 +1027,7 @@ Lint/NonLocalExitFromIterator: - 'app/models/product_importer.rb' # Offense count: 1 +# Cop supports --auto-correct. Lint/ScriptPermission: Exclude: - 'Rakefile' @@ -1067,7 +1055,7 @@ Lint/UnderscorePrefixedVariableName: Exclude: - 'spec/support/cancan_helper.rb' -# Offense count: 126 +# Offense count: 125 # Cop supports --auto-correct. # Configuration parameters: IgnoreEmptyBlocks, AllowUnusedKeywordArguments. Lint/UnusedBlockArgument: @@ -1092,8 +1080,6 @@ Lint/UnusedBlockArgument: - 'lib/open_food_network/reports/bulk_coop_allocation_report.rb' - 'lib/open_food_network/reports/bulk_coop_supplier_report.rb' - 'lib/open_food_network/sales_tax_report.rb' - - 'lib/open_food_network/xero_invoices_report.rb' - - 'spec/factories.rb' - 'spec/lib/open_food_network/order_grouper_spec.rb' - 'spec/support/cancan_helper.rb' - 'spec/support/delayed_job_helper.rb' @@ -1102,12 +1088,11 @@ Lint/UnusedBlockArgument: - 'spec/support/matchers/table_matchers.rb' - 'spec/support/performance_helper.rb' -# Offense count: 16 +# Offense count: 15 # Cop supports --auto-correct. # Configuration parameters: AllowUnusedKeywordArguments, IgnoreEmptyMethods. Lint/UnusedMethodArgument: Exclude: - - 'app/helpers/admin/injection_helper.rb' - 'app/helpers/angular_form_builder.rb' - 'app/helpers/angular_form_helper.rb' - 'app/helpers/order_cycles_helper.rb' @@ -1133,12 +1118,12 @@ Lint/UselessAccessModifier: - 'lib/open_food_network/reports/bulk_coop_report.rb' - 'spec/lib/open_food_network/reports/report_spec.rb' -# Offense count: 325 +# Offense count: 315 +# Configuration parameters: CheckForMethodsWithNoSideEffects. Lint/Void: Exclude: - 'app/serializers/api/enterprise_serializer.rb' - 'spec/archive/features/consumer/checkout_spec.rb' - - 'spec/controllers/admin/bulk_line_items_controller_spec.rb' - 'spec/controllers/api/order_cycles_controller_spec.rb' - 'spec/controllers/cart_controller_spec.rb' - 'spec/controllers/checkout_controller_spec.rb' @@ -1150,15 +1135,13 @@ Lint/Void: - 'spec/controllers/spree/api/products_controller_spec.rb' - 'spec/controllers/spree/api/variants_controller_spec.rb' - 'spec/controllers/spree/orders_controller_spec.rb' - - 'spec/controllers/user_passwords_controller_spec.rb' - 'spec/controllers/user_registrations_controller_spec.rb' + - 'spec/features/admin/bulk_product_update_spec.rb' - 'spec/features/admin/enterprise_fees_spec.rb' - 'spec/features/admin/enterprise_groups_spec.rb' - - 'spec/features/admin/enterprise_user_spec.rb' - 'spec/features/admin/enterprises/index_spec.rb' - 'spec/features/admin/enterprises_spec.rb' - 'spec/features/admin/order_cycles_spec.rb' - - 'spec/features/admin/orders_spec.rb' - 'spec/features/admin/payment_method_spec.rb' - 'spec/features/admin/product_import_spec.rb' - 'spec/features/admin/products_spec.rb' @@ -1180,7 +1163,6 @@ Lint/Void: - 'spec/lib/open_food_network/packing_report_spec.rb' - 'spec/lib/open_food_network/reports/report_spec.rb' - 'spec/lib/open_food_network/reports/rule_spec.rb' - - 'spec/mailers/enterprise_mailer_spec.rb' - 'spec/mailers/order_mailer_spec.rb' - 'spec/models/cart_spec.rb' - 'spec/models/enterprise_relationship_spec.rb' @@ -1199,12 +1181,106 @@ Lint/Void: - 'spec/serializers/enterprise_serializer_spec.rb' - 'spec/support/request/web_helper.rb' -# Offense count: 794 +# Offense count: 945 # Configuration parameters: CountComments, ExcludedMethods. Metrics/BlockLength: - Max: 672 + Max: 773 + +# Offense count: 8 +Naming/AccessorMethodName: + Exclude: + - 'app/models/product_importer.rb' + - 'app/models/spree/adjustment_decorator.rb' + - 'app/models/spree/order_decorator.rb' + - 'spec/support/request/shop_workflow.rb' + - 'spec/support/request/web_helper.rb' + +# Offense count: 1 +Naming/BinaryOperatorParameterName: + Exclude: + - 'app/models/exchange.rb' + +# Offense count: 2 +# Configuration parameters: ExpectMatchingDefinition, Regex, IgnoreExecutableScripts, AllowedAcronyms. +# AllowedAcronyms: CLI, DSL, ACL, API, ASCII, CPU, CSS, DNS, EOF, GUID, HTML, HTTP, HTTPS, ID, IP, JSON, LHS, QPS, RAM, RHS, RPC, SLA, SMTP, SQL, SSH, TCP, TLS, TTL, UDP, UI, UID, UUID, URI, URL, UTF8, VM, XML, XMPP, XSRF, XSS +Naming/FileName: + Exclude: + - 'Gemfile' + - 'Guardfile' + +# Offense count: 1 +# Configuration parameters: Blacklist. +# Blacklist: (?-mix:(^|\s)(EO[A-Z]{1}|END)(\s|$)) +Naming/HeredocDelimiterNaming: + Exclude: + - 'app/models/content_configuration.rb' + +# Offense count: 4 +Naming/MemoizedInstanceVariableName: + Exclude: + - 'app/controllers/spree/admin/payments_controller_decorator.rb' + - 'lib/open_food_network/address_finder.rb' + +# Offense count: 25 +# Configuration parameters: NamePrefix, NamePrefixBlacklist, NameWhitelist, MethodDefinitionMacros. +# NamePrefix: is_, has_, have_ +# NamePrefixBlacklist: is_, has_, have_ +# NameWhitelist: is_a? +# MethodDefinitionMacros: define_method, define_singleton_method +Naming/PredicateName: Exclude: - 'spec/**/*' + - 'app/mailers/producer_mailer.rb' + - 'app/models/enterprise.rb' + - 'app/models/enterprise_relationship.rb' + - 'app/models/order_cycle.rb' + - 'app/models/product_importer.rb' + - 'app/models/spreadsheet_entry.rb' + - 'app/models/spree/ability_decorator.rb' + - 'app/models/spree/adjustment_decorator.rb' + - 'app/models/spree/line_item_decorator.rb' + - 'app/models/spree/order_decorator.rb' + - 'app/models/spree/payment_method_decorator.rb' + - 'app/models/spree/preferences/file_configuration.rb' + - 'app/models/spree/product_decorator.rb' + - 'app/models/spree/shipping_method_decorator.rb' + - 'lib/open_food_network/customers_report.rb' + - 'lib/open_food_network/order_cycle_management_report.rb' + - 'lib/open_food_network/order_grouper.rb' + - 'lib/open_food_network/packing_report.rb' + - 'lib/tasks/data.rake' + +# Offense count: 14 +# Configuration parameters: MinNameLength, AllowNamesEndingInNumbers, AllowedNames, ForbiddenNames. +# AllowedNames: io, id, to, by, on, in, at +Naming/UncommunicativeMethodParamName: + Exclude: + - 'app/controllers/spree/orders_controller_decorator.rb' + - 'app/helpers/admin/injection_helper.rb' + - 'app/helpers/spree/admin/base_helper_decorator.rb' + - 'app/helpers/spree/base_helper_decorator.rb' + - 'app/models/exchange.rb' + - 'app/services/subscription_validator.rb' + - 'lib/open_food_network/property_merge.rb' + - 'lib/open_food_network/reports/bulk_coop_report.rb' + - 'spec/lib/open_food_network/reports/report_spec.rb' + - 'spec/mailers/producer_mailer_spec.rb' + +# Offense count: 4 +# Configuration parameters: EnforcedStyle. +# SupportedStyles: snake_case, camelCase +Naming/VariableName: + Exclude: + - 'app/helpers/admin/injection_helper.rb' + +# Offense count: 16 +# Configuration parameters: EnforcedStyle. +# SupportedStyles: snake_case, normalcase, non_integer +Naming/VariableNumber: + Exclude: + - 'spec/archive/features/consumer/checkout_spec.rb' + - 'spec/lib/open_food_network/products_and_inventory_report_spec.rb' + - 'spec/models/calculator/weight_spec.rb' # Offense count: 1 Performance/Caller: @@ -1255,7 +1331,81 @@ Performance/StringReplacement: - 'app/helpers/spree/admin/navigation_helper_decorator.rb' - 'app/models/spree/preferences/file_configuration.rb' -# Offense count: 10 +# Offense count: 4 +# Cop supports --auto-correct. +Performance/UnneededSort: + Exclude: + - 'spec/features/admin/order_cycles_spec.rb' + +# Offense count: 203 +# Cop supports --auto-correct. +Rails/ActiveRecordAliases: + Exclude: + - 'app/controllers/admin/bulk_line_items_controller.rb' + - 'app/controllers/admin/enterprises_controller.rb' + - 'app/controllers/admin/order_cycles_controller.rb' + - 'app/controllers/admin/subscriptions_controller.rb' + - 'app/controllers/api/enterprises_controller.rb' + - 'app/controllers/api/product_images_controller.rb' + - 'app/controllers/checkout_controller.rb' + - 'app/controllers/spree/admin/line_items_controller_decorator.rb' + - 'app/controllers/spree/orders_controller_decorator.rb' + - 'app/helpers/i18n_helper.rb' + - 'app/jobs/subscription_placement_job.rb' + - 'app/jobs/update_account_invoices.rb' + - 'app/jobs/update_billable_periods.rb' + - 'app/models/billable_period.rb' + - 'app/models/spree/adjustment_decorator.rb' + - 'app/models/spree/line_item_decorator.rb' + - 'app/models/spree/order_decorator.rb' + - 'app/models/spree/product_set.rb' + - 'app/services/create_mail_method.rb' + - 'app/services/line_item_syncer.rb' + - 'app/services/order_factory.rb' + - 'app/services/order_syncer.rb' + - 'lib/open_food_network/order_cycle_form_applicator.rb' + - 'lib/open_food_network/subscription_payment_updater.rb' + - 'lib/stripe/profile_storer.rb' + - 'spec/controllers/admin/customers_controller_spec.rb' + - 'spec/controllers/admin/proxy_orders_controller_spec.rb' + - 'spec/controllers/admin/subscriptions_controller_spec.rb' + - 'spec/controllers/line_items_controller_spec.rb' + - 'spec/controllers/spree/admin/payment_methods_controller_spec.rb' + - 'spec/controllers/spree/orders_controller_spec.rb' + - 'spec/features/admin/bulk_order_management_spec.rb' + - 'spec/features/admin/order_cycles_spec.rb' + - 'spec/features/admin/orders_spec.rb' + - 'spec/features/admin/subscriptions_spec.rb' + - 'spec/features/admin/variants_spec.rb' + - 'spec/features/consumer/account_spec.rb' + - 'spec/features/consumer/registration_spec.rb' + - 'spec/features/consumer/shopping/cart_spec.rb' + - 'spec/features/consumer/shopping/orders_spec.rb' + - 'spec/features/consumer/shopping/shopping_spec.rb' + - 'spec/jobs/subscription_confirm_job_spec.rb' + - 'spec/jobs/subscription_placement_job_spec.rb' + - 'spec/jobs/update_account_invoices_spec.rb' + - 'spec/jobs/update_billable_periods_spec.rb' + - 'spec/lib/open_food_network/products_cache_refreshment_spec.rb' + - 'spec/lib/open_food_network/products_cache_spec.rb' + - 'spec/models/customer_spec.rb' + - 'spec/models/enterprise_caching_spec.rb' + - 'spec/models/exchange_spec.rb' + - 'spec/models/order_cycle_spec.rb' + - 'spec/models/producer_property_spec.rb' + - 'spec/models/proxy_order_spec.rb' + - 'spec/models/spree/adjustment_spec.rb' + - 'spec/models/spree/line_item_spec.rb' + - 'spec/models/spree/order_spec.rb' + - 'spec/models/spree/product_spec.rb' + - 'spec/models/spree/user_spec.rb' + - 'spec/models/spree/variant_spec.rb' + - 'spec/models/variant_override_spec.rb' + - 'spec/requests/checkout/stripe_connect_spec.rb' + - 'spec/services/order_syncer_spec.rb' + - 'spec/services/subscription_estimator_spec.rb' + +# Offense count: 11 # Cop supports --auto-correct. # Configuration parameters: NilOrEmpty, NotPresent, UnlessPresent. Rails/Blank: @@ -1269,7 +1419,7 @@ Rails/Blank: - 'lib/tasks/data.rake' # Offense count: 3 -# Configuration parameters: EnforcedStyle, SupportedStyles. +# Configuration parameters: EnforcedStyle. # SupportedStyles: strict, flexible Rails/Date: Exclude: @@ -1278,6 +1428,7 @@ Rails/Date: # Offense count: 6 # Cop supports --auto-correct. +# Configuration parameters: EnforceForPrefixed. Rails/Delegate: Exclude: - 'app/models/spree/line_item_decorator.rb' @@ -1286,10 +1437,11 @@ Rails/Delegate: - 'app/serializers/api/admin/tag_rule_serializer.rb' - 'app/serializers/api/variant_serializer.rb' -# Offense count: 7 +# Offense count: 8 Rails/FilePath: Exclude: - 'lib/tasks/karma.rake' + - 'spec/features/admin/bulk_product_update_spec.rb' - 'spec/features/admin/content_spec.rb' - 'spec/models/content_configuration_spec.rb' - 'spec/models/spree/image_spec.rb' @@ -1305,7 +1457,7 @@ Rails/FindEach: - 'app/models/enterprise.rb' - 'app/models/spree/user_decorator.rb' -# Offense count: 5 +# Offense count: 7 # Configuration parameters: Include. # Include: app/models/**/*.rb Rails/HasAndBelongsToMany: @@ -1317,6 +1469,56 @@ Rails/HasAndBelongsToMany: - 'app/models/spree/line_item_decorator.rb' - 'app/models/spree/payment_method_decorator.rb' +# Offense count: 31 +# Configuration parameters: Include. +# Include: app/models/**/*.rb +Rails/HasManyOrHasOneDependent: + Exclude: + - 'app/models/account_invoice.rb' + - 'app/models/billable_period.rb' + - 'app/models/cart.rb' + - 'app/models/customer.rb' + - 'app/models/enterprise.rb' + - 'app/models/order_cycle.rb' + - 'app/models/spree/address_decorator.rb' + - 'app/models/spree/adjustment_decorator.rb' + - 'app/models/spree/order_decorator.rb' + - 'app/models/spree/payment_method_decorator.rb' + - 'app/models/spree/property_decorator.rb' + - 'app/models/spree/shipping_method_decorator.rb' + - 'app/models/spree/user_decorator.rb' + - 'app/models/spree/variant_decorator.rb' + - 'app/models/subscription.rb' + +# Offense count: 43 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle. +# SupportedStyles: numeric, symbolic +Rails/HttpStatus: + Exclude: + - 'app/controllers/admin/bulk_line_items_controller.rb' + - 'app/controllers/admin/column_preferences_controller.rb' + - 'app/controllers/admin/customers_controller.rb' + - 'app/controllers/admin/enterprise_fees_controller.rb' + - 'app/controllers/admin/enterprise_relationships_controller.rb' + - 'app/controllers/admin/enterprise_roles_controller.rb' + - 'app/controllers/admin/manager_invitations_controller.rb' + - 'app/controllers/admin/tag_rules_controller.rb' + - 'app/controllers/admin/variant_overrides_controller.rb' + - 'app/controllers/api/enterprises_controller.rb' + - 'app/controllers/application_controller.rb' + - 'app/controllers/checkout_controller.rb' + - 'app/controllers/enterprises_controller.rb' + - 'app/controllers/line_items_controller.rb' + - 'app/controllers/shop_controller.rb' + - 'app/controllers/spree/admin/line_items_controller_decorator.rb' + - 'app/controllers/spree/admin/products_controller_decorator.rb' + - 'app/controllers/spree/credit_cards_controller.rb' + - 'app/controllers/spree/orders_controller_decorator.rb' + - 'app/controllers/spree/store_controller_decorator.rb' + - 'app/controllers/stripe/callbacks_controller.rb' + - 'app/controllers/stripe/webhooks_controller.rb' + # Offense count: 11 Rails/OutputSafety: Exclude: @@ -1327,7 +1529,7 @@ Rails/OutputSafety: - 'lib/spree/money_decorator.rb' - 'spec/features/admin/orders_spec.rb' -# Offense count: 6 +# Offense count: 7 # Cop supports --auto-correct. Rails/PluralizationGrammar: Exclude: @@ -1336,13 +1538,18 @@ Rails/PluralizationGrammar: - 'spec/jobs/update_billable_periods_spec.rb' - 'spec/models/order_cycle_spec.rb' +# Offense count: 1 +# Cop supports --auto-correct. +Rails/Presence: + Exclude: + - 'app/serializers/api/admin/customer_serializer.rb' + # Offense count: 5 # Cop supports --auto-correct. # Configuration parameters: NotNilAndNotEmpty, NotBlank, UnlessBlank. Rails/Present: Exclude: - 'app/controllers/spree/admin/orders_controller_decorator.rb' - - 'app/controllers/spree/admin/reports_controller_decorator.rb' - 'app/models/producer_property.rb' - 'lib/open_food_network/products_and_inventory_report.rb' @@ -1354,7 +1561,7 @@ Rails/ReadWriteAttribute: Exclude: - 'app/models/enterprise.rb' -# Offense count: 46 +# Offense count: 45 # Configuration parameters: Include. # Include: app/models/**/*.rb Rails/ScopeArgs: @@ -1374,7 +1581,7 @@ Rails/ScopeArgs: - 'app/models/spree/variant_decorator.rb' # Offense count: 18 -# Configuration parameters: EnforcedStyle, SupportedStyles. +# Configuration parameters: EnforcedStyle. # SupportedStyles: strict, flexible Rails/TimeZone: Exclude: @@ -1392,12 +1599,11 @@ Rails/TimeZone: - 'spec/models/variant_override_spec.rb' # Offense count: 1 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, SupportedStyles, AutoCorrect. -# SupportedStyles: conservative, aggressive -Rails/UniqBeforePluck: +# Configuration parameters: Environments. +# Environments: development, test, production +Rails/UnknownEnv: Exclude: - - 'lib/open_food_network/sales_tax_report.rb' + - 'lib/open_food_network/cached_products_renderer.rb' # Offense count: 21 # Cop supports --auto-correct. @@ -1416,18 +1622,9 @@ Rails/Validation: - 'app/models/spree/variant_decorator.rb' - 'app/models/variant_override.rb' -# Offense count: 8 -Style/AccessorMethodName: - Exclude: - - 'app/models/product_importer.rb' - - 'app/models/spree/adjustment_decorator.rb' - - 'app/models/spree/order_decorator.rb' - - 'spec/support/request/shop_workflow.rb' - - 'spec/support/request/web_helper.rb' - # Offense count: 35 # Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, SupportedStyles. +# Configuration parameters: EnforcedStyle. # SupportedStyles: always, conditionals Style/AndOr: Exclude: @@ -1448,16 +1645,16 @@ Style/AndOr: # Offense count: 2 # Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, SupportedStyles. +# Configuration parameters: EnforcedStyle. # SupportedStyles: percent_q, bare_percent Style/BarePercentLiterals: Exclude: - 'spec/features/admin/variants_spec.rb' - 'spec/support/request/web_helper.rb' -# Offense count: 207 +# Offense count: 210 # Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, SupportedStyles. +# Configuration parameters: EnforcedStyle. # SupportedStyles: braces, no_braces, context_dependent Style/BracesAroundHashParameters: Exclude: @@ -1485,14 +1682,13 @@ Style/BracesAroundHashParameters: - 'lib/open_food_network/order_cycle_form_applicator.rb' - 'lib/open_food_network/reports/rule.rb' - 'lib/open_food_network/variant_and_line_item_naming.rb' - - 'lib/open_food_network/xero_invoices_report.rb' - 'spec/controllers/admin/accounts_and_billing_settings_controller_spec.rb' - 'spec/controllers/admin/business_model_configuration_controller_spec.rb' - 'spec/controllers/admin/enterprises_controller_spec.rb' + - 'spec/controllers/admin/manager_invitations_controller_spec.rb' - 'spec/controllers/admin/order_cycles_controller_spec.rb' - 'spec/controllers/api/order_cycles_controller_spec.rb' - 'spec/controllers/cart_controller_spec.rb' - - 'spec/controllers/enterprise_confirmations_controller_spec.rb' - 'spec/controllers/enterprises_controller_spec.rb' - 'spec/controllers/line_items_controller_spec.rb' - 'spec/controllers/spree/admin/adjustments_controller_spec.rb' @@ -1504,7 +1700,7 @@ Style/BracesAroundHashParameters: - 'spec/controllers/spree/api/products_controller_spec.rb' - 'spec/controllers/spree/api/variants_controller_spec.rb' - 'spec/controllers/spree/orders_controller_spec.rb' - - 'spec/factories.rb' + - 'spec/controllers/user_confirmations_controller_spec.rb' - 'spec/features/admin/accounts_and_billing_settings_spec.rb' - 'spec/features/admin/business_model_configuration_spec.rb' - 'spec/features/admin/order_cycles_spec.rb' @@ -1516,7 +1712,7 @@ Style/BracesAroundHashParameters: - 'spec/jobs/update_account_invoices_spec.rb' - 'spec/lib/open_food_network/feature_toggle_spec.rb' - 'spec/lib/open_food_network/order_cycle_form_applicator_spec.rb' - - 'spec/lib/open_food_network/xero_invoices_report_spec.rb' + - 'spec/lib/open_food_network/subscription_summarizer_spec.rb' - 'spec/models/billable_period_spec.rb' - 'spec/models/product_distribution_spec.rb' - 'spec/models/spree/ability_spec.rb' @@ -1537,7 +1733,8 @@ Style/CaseEquality: - 'spec/models/spree/payment_spec.rb' # Offense count: 87 -# Configuration parameters: EnforcedStyle, SupportedStyles. +# Cop supports --auto-correct. +# Configuration parameters: AutoCorrect, EnforcedStyle. # SupportedStyles: nested, compact Style/ClassAndModuleChildren: Exclude: @@ -1624,7 +1821,7 @@ Style/ClassAndModuleChildren: # Offense count: 3 # Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, SupportedStyles. +# Configuration parameters: EnforcedStyle. # SupportedStyles: is_a?, kind_of? Style/ClassCheck: Exclude: @@ -1647,21 +1844,25 @@ Style/ColonMethodCall: # Offense count: 12 # Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, SupportedStyles, SingleLineConditionsOnly, IncludeTernaryExpressions. +# Configuration parameters: EnforcedStyle, SingleLineConditionsOnly, IncludeTernaryExpressions. # SupportedStyles: assign_to_condition, assign_inside_condition Style/ConditionalAssignment: Exclude: - 'app/controllers/checkout_controller.rb' - 'app/controllers/spree/admin/base_controller_decorator.rb' - 'app/controllers/spree/admin/payment_methods_controller_decorator.rb' - - 'app/controllers/spree/admin/reports_controller_decorator.rb' - 'app/controllers/spree/admin/search_controller_decorator.rb' - 'app/helpers/spree/admin/orders_helper_decorator.rb' - - 'app/models/spree/calculator/flexi_rate_decorator.rb' - 'app/models/spree/calculator/per_item_decorator.rb' + - 'app/models/spree/line_item_decorator.rb' - 'app/models/spree/payment_decorator.rb' - 'spec/lib/open_food_network/order_grouper_spec.rb' +# Offense count: 2 +Style/DateTime: + Exclude: + - 'lib/open_food_network/users_and_enterprises_report.rb' + # Offense count: 5 # Cop supports --auto-correct. Style/EachWithObject: @@ -1671,14 +1872,13 @@ Style/EachWithObject: - 'lib/open_food_network/enterprise_fee_calculator.rb' - 'lib/open_food_network/products_renderer.rb' -# Offense count: 2 +# Offense count: 1 # Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, SupportedStyles. +# Configuration parameters: EnforcedStyle. # SupportedStyles: empty, nil, both Style/EmptyElse: Exclude: - 'app/models/spreadsheet_entry.rb' - - 'app/serializers/api/admin/basic_order_cycle_serializer.rb' # Offense count: 2 # Cop supports --auto-correct. @@ -1689,7 +1889,7 @@ Style/EmptyLiteral: # Offense count: 6 # Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, SupportedStyles. +# Configuration parameters: EnforcedStyle. # SupportedStyles: compact, expanded Style/EmptyMethod: Exclude: @@ -1701,20 +1901,22 @@ Style/EmptyMethod: - 'app/controllers/spree/admin/products_controller_decorator.rb' # Offense count: 2 -# Configuration parameters: ExpectMatchingDefinition, Regex, IgnoreExecutableScripts, AllowedAcronyms. -# AllowedAcronyms: CLI, DSL, ACL, API, ASCII, CPU, CSS, DNS, EOF, GUID, HTML, HTTP, HTTPS, ID, IP, JSON, LHS, QPS, RAM, RHS, RPC, SLA, SMTP, SQL, SSH, TCP, TLS, TTL, UDP, UI, UID, UUID, URI, URL, UTF8, VM, XML, XMPP, XSRF, XSS -Style/FileName: +# Cop supports --auto-correct. +Style/ExpandPathArguments: Exclude: - - 'Gemfile' - - 'Guardfile' + - 'spec/features/admin/products_spec.rb' + - 'spec/performance/shop_controller_spec.rb' -# Offense count: 1 -# Configuration parameters: SupportedStyles. -# SupportedStyles: annotated, template +# Offense count: 5 +# Configuration parameters: EnforcedStyle. +# SupportedStyles: annotated, template, unannotated Style/FormatStringToken: - EnforcedStyle: template + Exclude: + - 'app/helpers/order_cycles_helper.rb' + - 'lib/open_food_network/sales_tax_report.rb' + - 'spec/models/enterprise_spec.rb' -# Offense count: 87 +# Offense count: 88 # Configuration parameters: MinBodyLength. Style/GuardClause: Exclude: @@ -1734,7 +1936,6 @@ Style/GuardClause: - 'app/controllers/spree/admin/products_controller_decorator.rb' - 'app/controllers/spree/admin/resource_controller_decorator.rb' - 'app/controllers/spree/admin/shipping_methods_controller_decorator.rb' - - 'app/controllers/spree/admin/variants_controller_decorator.rb' - 'app/controllers/spree/orders_controller_decorator.rb' - 'app/controllers/spree/paypal_controller_decorator.rb' - 'app/jobs/products_cache_integrity_checker_job.rb' @@ -1766,9 +1967,9 @@ Style/GuardClause: - 'spec/support/request/distribution_helper.rb' - 'spec/support/request/shop_workflow.rb' -# Offense count: 1219 +# Offense count: 1040 # Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, SupportedStyles, UseHashRocketsWithSymbolValues, PreferHashRocketsForNonAlnumEndingSymbols. +# Configuration parameters: EnforcedStyle, UseHashRocketsWithSymbolValues, PreferHashRocketsForNonAlnumEndingSymbols. # SupportedStyles: ruby19, hash_rockets, no_mixed_keys, ruby19_no_mixed_keys Style/HashSyntax: Exclude: @@ -1781,15 +1982,12 @@ Style/HashSyntax: - 'app/controllers/admin/tag_rules_controller.rb' - 'app/controllers/api/enterprises_controller.rb' - 'app/controllers/checkout_controller.rb' - - 'app/controllers/enterprise_confirmations_controller.rb' - 'app/controllers/open_food_network/cart_controller.rb' - 'app/controllers/spree/admin/line_items_controller_decorator.rb' - 'app/controllers/spree/admin/orders_controller_decorator.rb' - 'app/controllers/spree/admin/products_controller_decorator.rb' - - 'app/controllers/spree/admin/reports_controller_decorator.rb' - 'app/controllers/spree/admin/search_controller_decorator.rb' - 'app/controllers/spree/admin/shipping_methods_controller_decorator.rb' - - 'app/controllers/spree/admin/variants_controller_decorator.rb' - 'app/controllers/spree/api/products_controller_decorator.rb' - 'app/controllers/spree/orders_controller_decorator.rb' - 'app/controllers/spree/paypal_controller_decorator.rb' @@ -1803,7 +2001,6 @@ Style/HashSyntax: - 'app/helpers/spree/admin/navigation_helper_decorator.rb' - 'app/helpers/spree/admin/orders_helper_decorator.rb' - 'app/mailers/enterprise_mailer.rb' - - 'app/mailers/producer_mailer.rb' - 'app/mailers/spree/order_mailer_decorator.rb' - 'app/mailers/spree/user_mailer_decorator.rb' - 'app/models/billable_period.rb' @@ -1867,7 +2064,6 @@ Style/HashSyntax: - 'lib/spree/product_filters.rb' - 'lib/tasks/cache.rake' - 'lib/tasks/data.rake' - - 'lib/tasks/dev.rake' - 'lib/tasks/enterprises.rake' - 'lib/tasks/karma.rake' - 'spec/archive/features/consumer/checkout_spec.rb' @@ -1876,7 +2072,6 @@ Style/HashSyntax: - 'spec/controllers/api/order_cycles_controller_spec.rb' - 'spec/controllers/base_controller_spec.rb' - 'spec/controllers/cart_controller_spec.rb' - - 'spec/controllers/enterprise_confirmations_controller_spec.rb' - 'spec/controllers/spree/admin/orders_controller_spec.rb' - 'spec/controllers/spree/admin/payment_methods_controller_spec.rb' - 'spec/controllers/spree/admin/payments_controller_spec.rb' @@ -1886,7 +2081,6 @@ Style/HashSyntax: - 'spec/controllers/spree/orders_controller_spec.rb' - 'spec/controllers/spree/user_sessions_controller_spec.rb' - 'spec/controllers/user_registrations_controller_spec.rb' - - 'spec/factories.rb' - 'spec/features/admin/bulk_order_management_spec.rb' - 'spec/features/admin/bulk_product_update_spec.rb' - 'spec/features/admin/customers_spec.rb' @@ -1900,13 +2094,14 @@ Style/HashSyntax: - 'spec/features/admin/products_spec.rb' - 'spec/features/admin/reports_spec.rb' - 'spec/features/admin/shipping_methods_spec.rb' + - 'spec/features/admin/subscriptions_spec.rb' - 'spec/features/admin/variant_overrides_spec.rb' - 'spec/features/consumer/account/cards_spec.rb' - 'spec/features/consumer/shopping/products_spec.rb' - 'spec/features/consumer/shopping/shopping_spec.rb' + - 'spec/jobs/subscription_placement_job_spec.rb' - 'spec/lib/open_food_network/group_buy_report_spec.rb' - 'spec/lib/open_food_network/lettuce_share_report_spec.rb' - - 'spec/lib/open_food_network/order_and_distributor_report_spec.rb' - 'spec/lib/open_food_network/order_cycle_form_applicator_spec.rb' - 'spec/lib/open_food_network/order_grouper_spec.rb' - 'spec/lib/open_food_network/tag_rule_applicator_spec.rb' @@ -1932,13 +2127,11 @@ Style/HashSyntax: - 'spec/support/request/web_helper.rb' - 'spec/support/seeds.rb' -# Offense count: 6 +# Offense count: 4 Style/IfInsideElse: Exclude: - 'app/controllers/admin/column_preferences_controller.rb' - 'app/controllers/admin/variant_overrides_controller.rb' - - 'app/controllers/enterprise_confirmations_controller.rb' - - 'app/controllers/spree/admin/overview_controller_decorator.rb' - 'app/controllers/spree/admin/products_controller_decorator.rb' # Offense count: 1 @@ -1964,6 +2157,7 @@ Style/LineEndConcatenation: # Offense count: 11 # Cop supports --auto-correct. +# Configuration parameters: IgnoredMethods. Style/MethodCallWithoutArgsParentheses: Exclude: - 'app/controllers/spree/admin/payment_methods_controller_decorator.rb' @@ -1976,7 +2170,7 @@ Style/MethodCallWithoutArgsParentheses: # Offense count: 14 # Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, SupportedStyles. +# Configuration parameters: EnforcedStyle. # SupportedStyles: require_parentheses, require_no_parentheses, require_no_parentheses_except_multiline Style/MethodDefParentheses: Exclude: @@ -1997,24 +2191,35 @@ Style/MethodMissing: Exclude: - 'app/helpers/application_helper.rb' -# Offense count: 5 +# Offense count: 6 +Style/MixinUsage: + Exclude: + - 'lib/open_food_network/orders_and_fulfillments_report.rb' + - 'spec/features/admin/orders_spec.rb' + - 'spec/lib/open_food_network/bulk_coop_report_spec.rb' + - 'spec/lib/open_food_network/order_cycle_management_report_spec.rb' + - 'spec/lib/open_food_network/orders_and_fulfillments_report_spec.rb' + - 'spec/lib/open_food_network/packing_report_spec.rb' + +# Offense count: 4 # Cop supports --auto-correct. Style/MultilineIfModifier: Exclude: - 'lib/open_food_network/enterprise_issue_validator.rb' - 'lib/spree/core/controller_helpers/respond_with_decorator.rb' -# Offense count: 7 +# Offense count: 6 # Cop supports --auto-correct. Style/MutableConstant: Exclude: - 'app/models/enterprise.rb' - 'app/models/enterprise_fee.rb' - - 'app/models/spree/payment_method_decorator.rb' - 'lib/discourse/single_sign_on.rb' -# Offense count: 8 +# Offense count: 7 # Cop supports --auto-correct. +# Configuration parameters: Whitelist. +# Whitelist: be, be_a, be_an, be_between, be_falsey, be_kind_of, be_instance_of, be_truthy, be_within, eq, eql, end_with, include, match, raise_error, respond_to, start_with Style/NestedParenthesizedCalls: Exclude: - 'app/controllers/admin/enterprises_controller.rb' @@ -2032,7 +2237,7 @@ Style/NestedTernaryOperator: # Offense count: 3 # Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, MinBodyLength, SupportedStyles. +# Configuration parameters: EnforcedStyle, MinBodyLength. # SupportedStyles: skip_modifier_ifs, always Style/Next: Exclude: @@ -2059,21 +2264,21 @@ Style/Not: # Offense count: 16 # Cop supports --auto-correct. -# Configuration parameters: EnforcedOctalStyle, SupportedOctalStyles. +# Configuration parameters: EnforcedOctalStyle. # SupportedOctalStyles: zero_with_o, zero_only Style/NumericLiteralPrefix: Exclude: - 'spec/features/admin/order_cycles_spec.rb' -# Offense count: 15 +# Offense count: 12 # Cop supports --auto-correct. # Configuration parameters: Strict. Style/NumericLiterals: MinDigits: 11 -# Offense count: 15 +# Offense count: 14 # Cop supports --auto-correct. -# Configuration parameters: AutoCorrect, EnforcedStyle, SupportedStyles. +# Configuration parameters: AutoCorrect, EnforcedStyle. # SupportedStyles: predicate, comparison Style/NumericPredicate: Exclude: @@ -2086,7 +2291,6 @@ Style/NumericPredicate: - 'app/models/spree/order_decorator.rb' - 'lib/open_food_network/integrity_checker.rb' - 'lib/open_food_network/rack_request_blocker.rb' - - 'lib/open_food_network/xero_invoices_report.rb' - 'lib/spree/money_decorator.rb' # Offense count: 2 @@ -2095,11 +2299,6 @@ Style/OneLineConditional: Exclude: - 'app/controllers/spree/admin/orders_controller_decorator.rb' -# Offense count: 1 -Style/OpMethod: - Exclude: - - 'app/models/exchange.rb' - # Offense count: 1 # Cop supports --auto-correct. # Configuration parameters: AllowSafeAssignment. @@ -2107,37 +2306,9 @@ Style/ParenthesesAroundCondition: Exclude: - 'app/controllers/checkout_controller.rb' -# Offense count: 25 -# Configuration parameters: NamePrefix, NamePrefixBlacklist, NameWhitelist. -# NamePrefix: is_, has_, have_ -# NamePrefixBlacklist: is_, has_, have_ -# NameWhitelist: is_a? -Style/PredicateName: - Exclude: - - 'spec/**/*' - - 'app/mailers/producer_mailer.rb' - - 'app/models/enterprise.rb' - - 'app/models/enterprise_relationship.rb' - - 'app/models/order_cycle.rb' - - 'app/models/product_importer.rb' - - 'app/models/spreadsheet_entry.rb' - - 'app/models/spree/ability_decorator.rb' - - 'app/models/spree/adjustment_decorator.rb' - - 'app/models/spree/line_item_decorator.rb' - - 'app/models/spree/order_decorator.rb' - - 'app/models/spree/payment_method_decorator.rb' - - 'app/models/spree/preferences/file_configuration.rb' - - 'app/models/spree/product_decorator.rb' - - 'app/models/spree/shipping_method_decorator.rb' - - 'lib/open_food_network/customers_report.rb' - - 'lib/open_food_network/order_cycle_management_report.rb' - - 'lib/open_food_network/order_grouper.rb' - - 'lib/open_food_network/packing_report.rb' - - 'lib/tasks/data.rake' - # Offense count: 4 # Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, SupportedStyles. +# Configuration parameters: EnforcedStyle. # SupportedStyles: short, verbose Style/PreferredHashMethods: Exclude: @@ -2156,7 +2327,7 @@ Style/Proc: # Offense count: 5 # Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, SupportedStyles. +# Configuration parameters: EnforcedStyle. # SupportedStyles: compact, exploded Style/RaiseArgs: Exclude: @@ -2165,11 +2336,10 @@ Style/RaiseArgs: - 'lib/open_food_network/products_renderer.rb' - 'spec/models/spree/tax_rate_spec.rb' -# Offense count: 3 +# Offense count: 2 # Cop supports --auto-correct. Style/RedundantBegin: Exclude: - - 'app/controllers/admin/order_cycles_controller.rb' - 'app/controllers/shop_controller.rb' - 'app/models/spree/product_decorator.rb' @@ -2189,7 +2359,7 @@ Style/RedundantParentheses: - 'spec/controllers/admin/enterprises_controller_spec.rb' - 'spec/features/admin/bulk_product_update_spec.rb' -# Offense count: 12 +# Offense count: 13 # Cop supports --auto-correct. # Configuration parameters: AllowMultipleReturnValues. Style/RedundantReturn: @@ -2197,13 +2367,14 @@ Style/RedundantReturn: - 'app/controllers/admin/enterprise_fees_controller.rb' - 'app/controllers/admin/enterprises_controller.rb' - 'app/controllers/admin/product_import_controller.rb' + - 'app/controllers/spree/credit_cards_controller.rb' - 'app/models/enterprise_fee.rb' - 'app/models/spree/adjustment_decorator.rb' - 'app/models/spree/classification_decorator.rb' - 'app/models/spree/order_populator_decorator.rb' - 'app/serializers/api/admin/enterprise_serializer.rb' -# Offense count: 110 +# Offense count: 114 # Cop supports --auto-correct. Style/RedundantSelf: Exclude: @@ -2240,12 +2411,11 @@ Style/RedundantSelf: # Offense count: 13 # Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, SupportedStyles, AllowInnerSlashes. +# Configuration parameters: EnforcedStyle, AllowInnerSlashes. # SupportedStyles: slashes, percent_r, mixed Style/RegexpLiteral: Exclude: - 'app/controllers/admin/enterprises_controller.rb' - - 'app/controllers/spree/admin/overview_controller_decorator.rb' - 'app/helpers/groups_helper.rb' - 'app/helpers/html_helper.rb' - 'app/models/enterprise.rb' @@ -2260,12 +2430,11 @@ Style/RescueModifier: Exclude: - 'app/controllers/application_controller.rb' - 'app/controllers/spree/admin/orders_controller_decorator.rb' - - 'app/controllers/spree/admin/reports_controller_decorator.rb' - 'lib/tasks/data.rake' # Offense count: 5 # Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, SupportedStyles. +# Configuration parameters: EnforcedStyle. # SupportedStyles: require_parentheses, require_no_parentheses Style/StabbyLambdaParentheses: Exclude: @@ -2274,7 +2443,7 @@ Style/StabbyLambdaParentheses: # Offense count: 14 # Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, SupportedStyles. +# Configuration parameters: EnforcedStyle. # SupportedStyles: single_quotes, double_quotes Style/StringLiteralsInInterpolation: Exclude: @@ -2324,7 +2493,7 @@ Style/SymbolProc: # Offense count: 5 # Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, SupportedStyles, AllowSafeAssignment. +# Configuration parameters: EnforcedStyle, AllowSafeAssignment. # SupportedStyles: require_parentheses, require_no_parentheses, require_parentheses_when_complex Style/TernaryParentheses: Exclude: @@ -2351,7 +2520,6 @@ Style/TrivialAccessors: # Cop supports --auto-correct. Style/UnlessElse: Exclude: - - 'app/controllers/spree/admin/reports_controller_decorator.rb' - 'app/models/enterprise.rb' - 'lib/open_food_network/order_grouper.rb' @@ -2401,19 +2569,8 @@ Style/UnneededPercentQ: - 'spec/features/consumer/producers_spec.rb' - 'spec/support/request/web_helper.rb' -# Offense count: 4 -# Configuration parameters: EnforcedStyle, SupportedStyles. -# SupportedStyles: snake_case, camelCase -Style/VariableName: - Exclude: - - 'app/controllers/spree/admin/reports_controller_decorator.rb' - - 'app/helpers/admin/injection_helper.rb' - -# Offense count: 16 -# Configuration parameters: EnforcedStyle, SupportedStyles. -# SupportedStyles: snake_case, normalcase, non_integer -Style/VariableNumber: - Exclude: - - 'spec/archive/features/consumer/checkout_spec.rb' - - 'spec/lib/open_food_network/products_and_inventory_report_spec.rb' - - 'spec/models/calculator/weight_spec.rb' +# Offense count: 6392 +# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns. +# URISchemes: http, https +Metrics/LineLength: + Max: 623 diff --git a/Gemfile b/Gemfile index c587209866..ec397895a9 100644 --- a/Gemfile +++ b/Gemfile @@ -2,7 +2,7 @@ source 'https://rubygems.org' ruby "2.1.5" git_source(:github) { |repo_name| "https://github.com/#{repo_name}.git" } -gem 'rails', '3.2.21' +gem 'rails', '~> 3.2.22' gem 'rails-i18n', '~> 3.0.0' gem 'i18n', '~> 0.6.11' gem 'i18n-js', '~> 3.0.0' @@ -25,7 +25,9 @@ gem 'spree_auth_devise', github: 'spree/spree_auth_devise', branch: '2-0-stable' # - Change type of password from string to password to hide it in the form gem 'spree_paypal_express', github: "spree-contrib/better_spree_paypal_express", branch: "2-0-stable" gem 'stripe', '~> 3.3.1' -gem 'activemerchant' +# We need at least this version to have Digicert's root certificate +# which is needed for Pin Payments (and possibly others). +gem 'activemerchant', '~> 1.78' gem 'oauth2', '~> 1.2.0' # Used for Stripe Connect gem 'jwt', '~> 1.5' @@ -60,6 +62,7 @@ gem 'geocoder' gem 'gmaps4rails' gem 'spinjs-rails' gem 'rack-ssl', require: 'rack/ssl' +gem 'rack-rewrite' gem 'custom_error_message', github: 'jeremydurham/custom-err-msg' gem 'angularjs-file-upload-rails', '~> 1.1.6' gem 'roadie-rails', '~> 1.0.3' @@ -76,6 +79,7 @@ gem 'wkhtmltopdf-binary' gem 'foreigner' gem 'immigrant' gem 'roo', '~> 2.7.0' +gem 'roo-xls', '~> 1.1.0' gem 'whenever', require: false @@ -110,7 +114,7 @@ group :test, :development do gem 'fuubar', '~> 2.2.0' gem 'rspec-rails', ">= 3.5.2" gem 'shoulda-matchers' - gem 'factory_girl_rails', require: false + gem "factory_bot_rails", require: false gem 'capybara', '>= 2.15.4' gem 'database_cleaner', '0.7.1', require: false gem 'awesome_print' diff --git a/Gemfile.lock b/Gemfile.lock index eb41eed5b0..4ca060e0b0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -122,12 +122,12 @@ GEM remote: https://rubygems.org/ specs: CFPropertyList (2.3.2) - actionmailer (3.2.21) - actionpack (= 3.2.21) + actionmailer (3.2.22.5) + actionpack (= 3.2.22.5) mail (~> 2.5.4) - actionpack (3.2.21) - activemodel (= 3.2.21) - activesupport (= 3.2.21) + actionpack (3.2.22.5) + activemodel (= 3.2.22.5) + activesupport (= 3.2.22.5) builder (~> 3.0.0) erubis (~> 2.7.0) journey (~> 1.0.4) @@ -142,18 +142,18 @@ GEM builder (>= 2.1.2, < 4.0.0) i18n (>= 0.6.9) nokogiri (~> 1.4) - activemodel (3.2.21) - activesupport (= 3.2.21) + activemodel (3.2.22.5) + activesupport (= 3.2.22.5) builder (~> 3.0.0) - activerecord (3.2.21) - activemodel (= 3.2.21) - activesupport (= 3.2.21) + activerecord (3.2.22.5) + activemodel (= 3.2.22.5) + activesupport (= 3.2.22.5) arel (~> 3.0.2) tzinfo (~> 0.3.29) - activeresource (3.2.21) - activemodel (= 3.2.21) - activesupport (= 3.2.21) - activesupport (3.2.21) + activeresource (3.2.22.5) + activemodel (= 3.2.22.5) + activesupport (= 3.2.22.5) + activesupport (3.2.22.5) i18n (~> 0.6, >= 0.6.4) multi_json (~> 1.0) acts-as-taggable-on (3.5.0) @@ -267,10 +267,10 @@ GEM eventmachine (1.2.3) excon (0.45.4) execjs (2.6.0) - factory_girl (4.9.0) + factory_bot (4.10.0) activesupport (>= 3.0.0) - factory_girl_rails (4.9.0) - factory_girl (~> 4.9.0) + factory_bot_rails (4.10.0) + factory_bot (~> 4.10.0) railties (>= 3.0.0) faraday (0.9.2) multipart-post (>= 1.2, < 3) @@ -465,7 +465,7 @@ GEM rb-fsevent (~> 0.9, >= 0.9.4) rb-inotify (~> 0.9, >= 0.9.7) lumberjack (1.0.12) - mail (2.5.4) + mail (2.5.5) mime-types (~> 1.16) treetop (~> 1.4.8) method_source (0.9.0) @@ -476,7 +476,7 @@ GEM railties (>= 3.1) money (5.1.1) i18n (~> 0.6.0) - multi_json (1.12.1) + multi_json (1.13.1) multi_xml (0.6.0) multipart-post (2.0.0) nenv (0.3.0) @@ -534,28 +534,29 @@ GEM rabl (0.8.4) activesupport (>= 2.3.14) rack (1.4.7) - rack-cache (1.7.0) + rack-cache (1.8.0) rack (>= 0.4) rack-livereload (0.3.16) rack + rack-rewrite (1.5.1) rack-ssl (1.3.4) rack rack-test (0.6.3) rack (>= 1.0) - rails (3.2.21) - actionmailer (= 3.2.21) - actionpack (= 3.2.21) - activerecord (= 3.2.21) - activeresource (= 3.2.21) - activesupport (= 3.2.21) + rails (3.2.22.5) + actionmailer (= 3.2.22.5) + actionpack (= 3.2.22.5) + activerecord (= 3.2.22.5) + activeresource (= 3.2.22.5) + activesupport (= 3.2.22.5) bundler (~> 1.0) - railties (= 3.2.21) + railties (= 3.2.22.5) rails-i18n (3.0.1) i18n (~> 0.5) rails (>= 3.0.0, < 4.0.0) - railties (3.2.21) - actionpack (= 3.2.21) - activesupport (= 3.2.21) + railties (3.2.22.5) + actionpack (= 3.2.22.5) + activesupport (= 3.2.22.5) rack-ssl (~> 1.3.2) rake (>= 0.8.7) rdoc (~> 3.4) @@ -563,7 +564,7 @@ GEM rainbow (2.2.2) rake raindrops (0.13.0) - rake (10.5.0) + rake (12.3.1) ransack (0.7.2) actionpack (~> 3.0) activerecord (~> 3.0) @@ -592,6 +593,10 @@ GEM roo (2.7.1) nokogiri (~> 1) rubyzip (~> 1.1, < 2.0.0) + roo-xls (1.1.0) + nokogiri + roo (>= 2.0.0beta1, < 3) + spreadsheet (> 0.9.0) rspec (3.7.0) rspec-core (~> 3.7.0) rspec-expectations (~> 3.7.0) @@ -622,6 +627,7 @@ GEM rainbow (>= 1.99.1, < 3.0) ruby-progressbar (~> 1.7) unicode-display_width (~> 1.0, >= 1.0.1) + ruby-ole (1.2.12.1) ruby-progressbar (1.8.1) rubyzip (1.2.0) safe_yaml (1.0.4) @@ -640,6 +646,8 @@ GEM activesupport (>= 3.0.0) spinjs-rails (1.3) rails (>= 3.1) + spreadsheet (1.1.7) + ruby-ole (>= 1.0) sprockets (2.2.3) hike (~> 1.2) multi_json (~> 1.0) @@ -662,7 +670,7 @@ GEM turbo-sprockets-rails3 (0.3.6) railties (> 3.2.8, < 4.0.0) sprockets (>= 2.0.0) - tzinfo (0.3.53) + tzinfo (0.3.54) uglifier (2.7.1) execjs (>= 0.3.0) json (>= 1.8.0) @@ -701,7 +709,7 @@ PLATFORMS DEPENDENCIES active_model_serializers - activemerchant + activemerchant (~> 1.78) acts-as-taggable-on (~> 3.4) andand angular-rails-templates (~> 0.2.0) @@ -727,7 +735,7 @@ DEPENDENCIES delayed_job_active_record diffy eventmachine (>= 1.2.3) - factory_girl_rails + factory_bot_rails figaro foreigner foundation-icons-sass-rails @@ -765,13 +773,15 @@ DEPENDENCIES pry-byebug (>= 3.4.3) rabl rack-livereload + rack-rewrite rack-ssl - rails (= 3.2.21) + rails (~> 3.2.22) rails-i18n (~> 3.0.0) redcarpet representative_view roadie-rails (~> 1.0.3) roo (~> 2.7.0) + roo-xls (~> 1.1.0) rspec-rails (>= 3.5.2) rspec-retry rubocop (>= 0.49.1) @@ -802,4 +812,4 @@ RUBY VERSION ruby 2.1.5p273 BUNDLED WITH - 1.16.1 + 1.16.2 diff --git a/Guardfile b/Guardfile index 9dc491669e..4ae1efda60 100644 --- a/Guardfile +++ b/Guardfile @@ -5,45 +5,7 @@ guard 'livereload' do watch(%r{app/views/.+\.(erb|haml|slim)$}) watch(%r{app/helpers/.+\.rb}) watch(%r{public/.+\.(css|js|html)}) - #watch(%r{config/locales/.+\.yml}) + # Rails Assets Pipeline watch(%r{(app|vendor)(/assets/\w+/(.+\.(css|js|html|png|jpg))).*}) { |m| "/assets/#{m[3]}" } end - - -#guard 'rails' do - #watch('Gemfile.lock') - #watch(%r{^(config|lib)/.*}) -#end - - -#guard 'zeus' do - ## uses the .rspec file - ## --colour --fail-fast --format documentation --tag ~slow - #watch(%r{^spec/.+_spec\.rb$}) - #watch(%r{^app/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" } - #watch(%r{^app/(.+)\.haml$}) { |m| "spec/#{m[1]}.haml_spec.rb" } - #watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" } - #watch(%r{^app/controllers/(.+)_(controller)\.rb$}) { |m| ["spec/routing/#{m[1]}_routing_spec.rb", "spec/#{m[2]}s/#{m[1]}_#{m[2]}_spec.rb", "spec/requests/#{m[1]}_spec.rb"] } -#end - -#guard :rspec do - #watch(%r{^spec/.+_spec\.rb$}) - #watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" } - #watch('spec/spec_helper.rb') { "spec" } - - ## Rails example - #watch(%r{^app/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" } - #watch(%r{^app/(.*)(\.erb|\.haml|\.slim)$}) { |m| "spec/#{m[1]}#{m[2]}_spec.rb" } - #watch(%r{^app/controllers/(.+)_(controller)\.rb$}) { |m| ["spec/routing/#{m[1]}_routing_spec.rb", "spec/#{m[2]}s/#{m[1]}_#{m[2]}_spec.rb", "spec/acceptance/#{m[1]}_spec.rb"] } - #watch(%r{^spec/support/(.+)\.rb$}) { "spec" } - #watch('config/routes.rb') { "spec/routing" } - #watch('app/controllers/application_controller.rb') { "spec/controllers" } - - ## Capybara features specs - #watch(%r{^app/views/(.+)/.*\.(erb|haml|slim)$}) { |m| "spec/features/#{m[1]}_spec.rb" } - - ## Turnip features and steps - #watch(%r{^spec/acceptance/(.+)\.feature$}) - #watch(%r{^spec/acceptance/steps/(.+)_steps\.rb$}) { |m| Dir[File.join("**/#{m[1]}.feature")][0] || 'spec/acceptance' } -#end diff --git a/README.md b/README.md index 47cd122f84..018e605ad7 100644 --- a/README.md +++ b/README.md @@ -103,6 +103,9 @@ Do not forget to run `rake tmp:cache:clear` after locales are updated to reload * Lynne Davis (https://github.com/lin-d-hop) * Paul Mackay (https://github.com/pmackay) * Steve Pettitt (https://github.com/stveep) +* Matt Yorkley (https://github.com/Matt-Yorkley) +* Pau Pérez (https://github.com/sauloperez) +* Enrico Stano (https://github.com/enricostano) ## Licence diff --git a/app/assets/images/home/tagline-bg.jpg b/app/assets/images/home/tagline-bg.jpg new file mode 100644 index 0000000000..68366a9569 Binary files /dev/null and b/app/assets/images/home/tagline-bg.jpg differ diff --git a/app/assets/javascripts/admin/all.js b/app/assets/javascripts/admin/all.js index bc5cfea4a3..7c6839ea54 100644 --- a/app/assets/javascripts/admin/all.js +++ b/app/assets/javascripts/admin/all.js @@ -36,6 +36,7 @@ //= require ./orders/orders //= require ./order_cycles/order_cycles //= require ./payment_methods/payment_methods +//= require ./product_import/product_import //= require ./products/products //= require ./resources/resources //= require ./shipping_methods/shipping_methods diff --git a/app/assets/javascripts/admin/bulk_product_update.js.coffee b/app/assets/javascripts/admin/bulk_product_update.js.coffee index 3e5f388a5f..a9f2f20d14 100644 --- a/app/assets/javascripts/admin/bulk_product_update.js.coffee +++ b/app/assets/javascripts/admin/bulk_product_update.js.coffee @@ -28,6 +28,7 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", ($scope, $timeout $scope.filterTaxons = [{id: "0", name: ""}].concat $scope.taxons $scope.producerFilter = "0" $scope.categoryFilter = "0" + $scope.importDateFilter = "0" $scope.products = BulkProducts.products $scope.filteredProducts = [] $scope.currentFilters = [] @@ -43,7 +44,7 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", ($scope, $timeout .catch (message) -> $scope.api_error_msg = message - $scope.$watchCollection '[query, producerFilter, categoryFilter]', -> + $scope.$watchCollection '[query, producerFilter, categoryFilter, importDateFilter]', -> $scope.limit = 15 # Reset limit whenever searching $scope.fetchProducts = -> @@ -52,6 +53,9 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", ($scope, $timeout $scope.resetProducts() $scope.loading = false + $timeout -> + if $scope.showLatestImport + $scope.importDateFilter = $scope.importDates[1].id $scope.resetProducts = -> DirtyProducts.clear() @@ -91,6 +95,7 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", ($scope, $timeout $scope.query = "" $scope.producerFilter = "0" $scope.categoryFilter = "0" + $scope.importDateFilter = "0" $scope.editWarn = (product, variant) -> if (DirtyProducts.count() > 0 and confirm(t("unsaved_changes_confirmation"))) or (DirtyProducts.count() == 0) diff --git a/app/assets/javascripts/admin/controllers/enterprises_dashboard_controller.js.coffee b/app/assets/javascripts/admin/controllers/enterprises_dashboard_controller.js.coffee deleted file mode 100644 index 60315d30c1..0000000000 --- a/app/assets/javascripts/admin/controllers/enterprises_dashboard_controller.js.coffee +++ /dev/null @@ -1,2 +0,0 @@ -angular.module("ofn.admin").controller "enterprisesDashboardCtrl", ($scope) -> - $scope.activeTab = "hubs" diff --git a/app/assets/javascripts/admin/directives/select2_no_search.js.coffee b/app/assets/javascripts/admin/directives/select2_no_search.js.coffee new file mode 100644 index 0000000000..64ded4fcc8 --- /dev/null +++ b/app/assets/javascripts/admin/directives/select2_no_search.js.coffee @@ -0,0 +1,6 @@ +angular.module("ofn.admin").directive "select2NoSearch", ($timeout) -> + restrict: 'CA' + link: (scope, element, attrs) -> + $timeout -> + element.select2 + minimumResultsForSearch: Infinity \ No newline at end of file diff --git a/app/assets/javascripts/admin/enterprises/controllers/country_controller.js.coffee b/app/assets/javascripts/admin/enterprises/controllers/country_controller.js.coffee new file mode 100644 index 0000000000..085a992a36 --- /dev/null +++ b/app/assets/javascripts/admin/enterprises/controllers/country_controller.js.coffee @@ -0,0 +1,18 @@ +# Used in enterprise new and edit forms to reset the state when the country is changed +angular.module("admin.enterprises").controller 'countryCtrl', ($scope, availableCountries) -> + $scope.countries = availableCountries + + $scope.countriesById = $scope.countries.reduce (obj, country) -> + obj[country.id] = country + obj + , {} + + $scope.$watch 'Enterprise.address.country_id', (newID, oldID) -> + $scope.clearState() unless $scope.addressStateMatchesCountry() + + $scope.clearState = -> + $scope.Enterprise.address.state_id = null + + $scope.addressStateMatchesCountry = -> + $scope.countriesById[$scope.Enterprise.address.country_id].states.some (state) -> + state.id == $scope.Enterprise.address.state_id diff --git a/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee b/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee index 2d1dc494f4..bb1ae6b31a 100644 --- a/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee +++ b/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee @@ -11,6 +11,9 @@ angular.module("admin.enterprises") $scope.$watch 'enterprise_form.$dirty', (newValue) -> StatusMessage.display 'notice', t('admin.unsaved_changes') if newValue + $scope.$watch 'newManager', (newValue) -> + $scope.addManager($scope.newManager) if newValue + $scope.setFormDirty = -> $scope.$apply -> $scope.enterprise_form.$setDirty() @@ -47,7 +50,7 @@ angular.module("admin.enterprises") email: manager.email confirmed: manager.confirmed if (user for user in $scope.Enterprise.users when user.id == manager.id).length == 0 - $scope.Enterprise.users.push manager + $scope.Enterprise.users.unshift(manager) $scope.enterprise_form?.$setDirty() else alert ("#{manager.email}" + " " + t("is_already_manager")) diff --git a/app/assets/javascripts/admin/enterprises/controllers/new_enterprise_controller.js.coffee b/app/assets/javascripts/admin/enterprises/controllers/new_enterprise_controller.js.coffee new file mode 100644 index 0000000000..06677f5184 --- /dev/null +++ b/app/assets/javascripts/admin/enterprises/controllers/new_enterprise_controller.js.coffee @@ -0,0 +1,5 @@ +angular.module("admin.enterprises").controller 'NewEnterpriseController', ($scope, defaultCountryID) -> + $scope.Enterprise = + address: + country_id: defaultCountryID + state_id: null diff --git a/app/assets/javascripts/admin/filters/import_date_filter.js.coffee b/app/assets/javascripts/admin/filters/import_date_filter.js.coffee new file mode 100644 index 0000000000..768d305afd --- /dev/null +++ b/app/assets/javascripts/admin/filters/import_date_filter.js.coffee @@ -0,0 +1,4 @@ +angular.module("ofn.admin").filter "importDate", ($filter) -> + return (products, importDate) -> + return products if importDate == "0" + $filter('filter')( products, { import_date: importDate } ) diff --git a/app/assets/javascripts/admin/order_cycles/directives/change-warning.js.coffee b/app/assets/javascripts/admin/order_cycles/directives/change-warning.js.coffee new file mode 100644 index 0000000000..c1cd159807 --- /dev/null +++ b/app/assets/javascripts/admin/order_cycles/directives/change-warning.js.coffee @@ -0,0 +1,23 @@ +angular.module("admin.orderCycles").directive "changeWarning", (ConfirmDialog) -> + restrict: "A" + scope: + orderCycle: '=changeWarning' + link: (scope, element, attrs) -> + acknowledged = false + cancel = 'admin.order_cycles.date_warning.cancel' + proceed = 'admin.order_cycles.date_warning.proceed' + msg = 'admin.order_cycles.date_warning.msg' + options = { cancel: t(cancel), confirm: t(proceed) } + + isOpen = (orderCycle) -> + moment(orderCycle.orders_open_at, "YYYY-MM-DD HH:mm:SS Z").isBefore() && + moment(orderCycle.orders_close_at, "YYYY-MM-DD HH:mm:SS Z").isAfter() + + element.focus -> + count = scope.orderCycle.subscriptions_count + return if acknowledged + return unless isOpen(scope.orderCycle) + return if count < 1 + ConfirmDialog.open('info', t(msg, n: count), options).then -> + acknowledged = true + element.siblings('img').trigger('click') diff --git a/app/assets/javascripts/admin/order_cycles/services/order_cycle.js.coffee b/app/assets/javascripts/admin/order_cycles/services/order_cycle.js.coffee index f35db6e3b5..59d9b4f994 100644 --- a/app/assets/javascripts/admin/order_cycles/services/order_cycle.js.coffee +++ b/app/assets/javascripts/admin/order_cycles/services/order_cycle.js.coffee @@ -209,6 +209,7 @@ angular.module('admin.orderCycles').factory 'OrderCycle', ($resource, $window, S delete order_cycle.editable_variants_for_incoming_exchanges delete order_cycle.editable_variants_for_outgoing_exchanges delete order_cycle.visible_variants_for_outgoing_exchanges + delete order_cycle.subscriptions_count order_cycle removeInactiveExchanges: (order_cycle) -> diff --git a/app/assets/javascripts/admin/orders/directives/customer_search_override.js.coffee b/app/assets/javascripts/admin/orders/directives/customer_search_override.js.coffee index 08d4ceeb5d..76200d92ff 100644 --- a/app/assets/javascripts/admin/orders/directives/customer_search_override.js.coffee +++ b/app/assets/javascripts/admin/orders/directives/customer_search_override.js.coffee @@ -56,7 +56,4 @@ angular.module("admin.orders").directive 'customerSearchOverride', -> return $('#order_email').val customer.email $('#user_id').val customer.user_id # modified - $('#guest_checkout_true').prop 'checked', false - $('#guest_checkout_false').prop 'checked', true - $('#guest_checkout_false').prop 'disabled', false customer.email diff --git a/app/assets/javascripts/admin/product_import/controllers/dropdown_panels.js.coffee b/app/assets/javascripts/admin/product_import/controllers/dropdown_panels.js.coffee index 1403611168..91b0c23a53 100644 --- a/app/assets/javascripts/admin/product_import/controllers/dropdown_panels.js.coffee +++ b/app/assets/javascripts/admin/product_import/controllers/dropdown_panels.js.coffee @@ -1,4 +1,4 @@ -angular.module("ofn.admin").controller "DropdownPanelsCtrl", ($scope) -> +angular.module("admin.productImport").controller "DropdownPanelsCtrl", ($scope) -> $scope.active = false $scope.togglePanel = -> diff --git a/app/assets/javascripts/admin/product_import/controllers/import_feedback.js.coffee b/app/assets/javascripts/admin/product_import/controllers/import_feedback.js.coffee new file mode 100644 index 0000000000..f914b0bb57 --- /dev/null +++ b/app/assets/javascripts/admin/product_import/controllers/import_feedback.js.coffee @@ -0,0 +1,12 @@ +angular.module("admin.productImport").controller "ImportFeedbackCtrl", ($scope) -> + + $scope.count = (items) -> + total = 0 + angular.forEach items, (item) -> + total++ + total + + $scope.attribute_invalid = (attribute, line_number) -> + $scope.entries[line_number]['errors'][attribute] != undefined + + $scope.ignore_fields = ['variant_unit', 'variant_unit_scale', 'unit_description'] 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 new file mode 100644 index 0000000000..b3d3e2d824 --- /dev/null +++ b/app/assets/javascripts/admin/product_import/controllers/import_form_controller.js.coffee @@ -0,0 +1,161 @@ +angular.module("admin.productImport").controller "ImportFormCtrl", ($scope, $http, $filter, ProductImportService, $timeout) -> + + $scope.entries = {} + $scope.update_counts = {} + $scope.reset_counts = {} + + $scope.updates = {} + $scope.updated_total = 0 + $scope.updated_ids = [] + $scope.update_errors = [] + + $scope.chunks = 0 + $scope.completed = 0 + $scope.percentage = "0%" + $scope.started = false + $scope.finished = false + + $scope.countResettable = () -> + angular.forEach $scope.supplier_product_counts, (value, key) -> + $scope.reset_counts[key] = value + if $scope.update_counts[key] + $scope.reset_counts[key] -= $scope.update_counts[key] + + $scope.resetProgress = () -> + $scope.chunks = 0 + $scope.completed = 0 + $scope.percentage = "0%" + $scope.started = false + $scope.finished = false + + $scope.step = 'settings' + + $scope.confirmSettings = () -> + $scope.step = 'import' + + $scope.viewResults = () -> + $scope.countResettable() + $scope.step = 'results' + $scope.resetProgress() + + $scope.acceptResults = () -> + $scope.step = 'save' + + $scope.finalResults = () -> + $scope.step = 'complete' + + $scope.start = () -> + $scope.started = true + $scope.percentage = "1%" + total = $scope.item_count + size = 100 + $scope.chunks = Math.ceil(total / size) + + i = 0 + + while i < $scope.chunks + start = (i*size)+1 + end = (i+1)*size + if $scope.step == 'import' + $scope.processImport(start, end) + if $scope.step == 'save' + $scope.processSave(start, end) + i++ + + $scope.processImport = (start, end) -> + $scope.getSettings() if $scope.importSettings == null + $http( + url: $scope.import_url + method: 'POST' + data: + 'start': start + 'end': end + 'filepath': $scope.filepath + 'settings': $scope.importSettings + ).success((data, status, headers, config) -> + angular.merge($scope.entries, angular.fromJson(data['entries'])) + $scope.sortUpdates(data['reset_counts']) + + $scope.updateProgress() + ).error((data, status, headers, config) -> + $scope.exception = data + console.error(data) + ) + + $scope.importSettings = null + + $scope.getSettings = () -> + $scope.importSettings = ProductImportService.getSettings() + + $scope.sortUpdates = (data) -> + angular.forEach data, (value, key) -> + if (key in $scope.update_counts) + $scope.update_counts[key] += value['updates_count'] + else + $scope.update_counts[key] = value['updates_count'] + + $scope.processSave = (start, end) -> + $scope.getSettings() if $scope.importSettings == null + $http( + url: $scope.save_url + method: 'POST' + data: + 'start': start + 'end': end + 'filepath': $scope.filepath + 'settings': $scope.importSettings + ).success((data, status, headers, config) -> + $scope.sortResults(data['results']) + + angular.forEach data['updated_ids'], (id) -> + $scope.updated_ids.push(id) + + angular.forEach data['errors'], (error) -> + $scope.update_errors.push(error) + + $scope.updateProgress() + ).error((data, status, headers, config) -> + $scope.exception = data + console.error(data) + ) + + $scope.sortResults = (results) -> + angular.forEach results, (value, key) -> + if ($scope.updates[key] != undefined) + $scope.updates[key] += value + else + $scope.updates[key] = value + + $scope.updated_total += value + + $scope.resetAbsent = () -> + enterprises_to_reset = [] + angular.forEach $scope.importSettings, (settings, enterprise) -> + if settings['reset_all_absent'] + enterprises_to_reset.push(enterprise) + + if enterprises_to_reset.length && $scope.updated_ids.length + $http( + url: $scope.reset_url + method: 'POST' + data: + 'filepath': $scope.filepath + 'settings': $scope.importSettings + 'reset_absent': true, + 'updated_ids': $scope.updated_ids, + 'enterprises_to_reset': enterprises_to_reset + ).success((data, status, headers, config) -> + console.log(data) + $scope.updates.products_reset = data + + ).error((data, status, headers, config) -> + console.error(data) + ) + + $scope.updateProgress = () -> + $scope.completed++ + $scope.percentage = String(Math.round(($scope.completed / $scope.chunks) * 100)) + '%' + + if $scope.completed == $scope.chunks + $scope.finished = true + $scope.resetAbsent() if $scope.step == 'save' diff --git a/app/assets/javascripts/admin/product_import/controllers/import_options_form.js.coffee b/app/assets/javascripts/admin/product_import/controllers/import_options_form.js.coffee index 21c08b9ae1..965692c86d 100644 --- a/app/assets/javascripts/admin/product_import/controllers/import_options_form.js.coffee +++ b/app/assets/javascripts/admin/product_import/controllers/import_options_form.js.coffee @@ -1,12 +1,38 @@ -angular.module("ofn.admin").controller "ImportOptionsFormCtrl", ($scope, $rootScope, ProductImportService) -> +angular.module("admin.productImport").controller "ImportOptionsFormCtrl", ($scope, $rootScope, ProductImportService) -> - $scope.toggleResetAbsent = () -> - confirmed = confirm t('js.product_import.confirmation') if $scope.resetAbsent + $scope.initForm = () -> + $scope.settings = {} if $scope.settings == undefined + $scope.settings[$scope.supplierId] = { + import_into: 'product_list' + defaults: + count_on_hand: + mode: 'overwrite_all' + on_hand: + mode: 'overwrite_all' + tax_category_id: + mode: 'overwrite_all' + shipping_category_id: + mode: 'overwrite_all' + available_on: + mode: 'overwrite_all' + } + $scope.import_into = 'product_list' - if confirmed or !$scope.resetAbsent - ProductImportService.updateResetAbsent($scope.supplierId, $scope.resetCount, $scope.resetAbsent) + $scope.updateImportInto = () -> + $scope.import_into = $scope.settings[$scope.supplierId]['import_into'] + + $scope.$watch 'settings', (updated) -> + ProductImportService.updateSettings(updated) + , true + + $scope.toggleResetAbsent = (id) -> + checked = $scope.settings[id]['reset_all_absent'] + confirmed = confirm t('js.product_import.confirmation') if checked + + if confirmed or !checked + ProductImportService.updateResetAbsent($scope.supplierId, $scope.reset_counts[$scope.supplierId], checked) else - $scope.resetAbsent = false + $scope.settings[id]['reset_all_absent'] = false $scope.resetTotal = ProductImportService.resetTotal diff --git a/app/assets/javascripts/admin/product_import/filters/filter_entries.js.coffee b/app/assets/javascripts/admin/product_import/filters/filter_entries.js.coffee new file mode 100644 index 0000000000..4a1007b3a9 --- /dev/null +++ b/app/assets/javascripts/admin/product_import/filters/filter_entries.js.coffee @@ -0,0 +1,32 @@ +angular.module("admin.productImport").filter 'entriesFilterValid', -> + (entries, type) -> + if type == 'all' + return entries + + filtered = {} + + angular.forEach entries, (entry, line_number) -> + validates_as = entry.validates_as + + if type == 'valid' and validates_as != '' \ + or type == 'invalid' and validates_as == '' \ + or type == 'create_product' and validates_as == 'new_product' or validates_as == 'new_variant' \ + or type == 'update_product' and validates_as == 'existing_variant' \ + or type == 'create_inventory' and validates_as == 'new_inventory_item' \ + or type == 'update_inventory' and validates_as == 'existing_inventory_item' + filtered[line_number] = entry + + filtered + +angular.module("admin.productImport").filter 'entriesFilterSupplier', -> + (entries, supplier) -> + if supplier == 'all' + return entries + + filtered = {} + + angular.forEach entries, (entry, line_number) -> + if supplier == entry.attributes['supplier'] + filtered[line_number] = entry + + filtered diff --git a/app/assets/javascripts/admin/product_import/product_import.js.coffee b/app/assets/javascripts/admin/product_import/product_import.js.coffee new file mode 100644 index 0000000000..5eb83204e1 --- /dev/null +++ b/app/assets/javascripts/admin/product_import/product_import.js.coffee @@ -0,0 +1,3 @@ +angular.module("admin.productImport", ["ngResource"]).config ($httpProvider) -> + $httpProvider.defaults.headers.common["X-CSRF-Token"] = $("meta[name=csrf-token]").attr("content") + $httpProvider.defaults.headers.common["Accept"] = "application/json, text/javascript, */*" diff --git a/app/assets/javascripts/admin/product_import/services/product_import_service.js.coffee b/app/assets/javascripts/admin/product_import/services/product_import_service.js.coffee index 330e7b6cad..af0f464df1 100644 --- a/app/assets/javascripts/admin/product_import/services/product_import_service.js.coffee +++ b/app/assets/javascripts/admin/product_import/services/product_import_service.js.coffee @@ -1,7 +1,8 @@ -angular.module("ofn.admin").factory "ProductImportService", ($rootScope) -> +angular.module("admin.productImport").factory "ProductImportService", ($rootScope) -> new class ProductImportService suppliers: {} resetTotal: 0 + settings: {} updateResetAbsent: (supplierId, resetCount, resetAbsent) -> if resetAbsent @@ -13,3 +14,8 @@ angular.module("ofn.admin").factory "ProductImportService", ($rootScope) -> $rootScope.resetTotal = @resetTotal + updateSettings: (updated) -> + angular.merge(@settings, updated) + + getSettings: () -> + @settings \ No newline at end of file diff --git a/app/assets/javascripts/admin/products/controllers/product_image_controller.js.coffee b/app/assets/javascripts/admin/products/controllers/product_image_controller.js.coffee index 1fb2ceea06..fb447208e2 100644 --- a/app/assets/javascripts/admin/products/controllers/product_image_controller.js.coffee +++ b/app/assets/javascripts/admin/products/controllers/product_image_controller.js.coffee @@ -2,5 +2,7 @@ angular.module("ofn.admin").controller "ProductImageCtrl", ($scope, ProductImage $scope.imageUploader = ProductImageService.imageUploader $scope.imagePreview = ProductImageService.imagePreview - $scope.$watch 'product.image_url', (newValue) -> - $scope.imagePreview = newValue if newValue + $scope.$watch 'product.image_url', (newValue, oldValue) -> + if newValue != oldValue + $scope.imagePreview = newValue + $scope.uploadModal.close() diff --git a/app/assets/javascripts/admin/utils/directives/date_picker.js.coffee b/app/assets/javascripts/admin/utils/directives/date_picker.js.coffee new file mode 100644 index 0000000000..c6c8b4fba6 --- /dev/null +++ b/app/assets/javascripts/admin/utils/directives/date_picker.js.coffee @@ -0,0 +1,9 @@ +angular.module("admin.utils").directive "datepicker", -> + require: "ngModel" + link: (scope, element, attrs, ngModel) -> + element.datepicker + dateFormat: "yy-mm-dd" + onSelect: (dateText, inst) -> + scope.$apply (scope) -> + # Fires ngModel.$parsers + ngModel.$setViewValue dateText diff --git a/app/assets/javascripts/admin/utils/directives/tags_with_translation.js.coffee b/app/assets/javascripts/admin/utils/directives/tags_with_translation.js.coffee index 9722ab26af..ef84570f5e 100644 --- a/app/assets/javascripts/admin/utils/directives/tags_with_translation.js.coffee +++ b/app/assets/javascripts/admin/utils/directives/tags_with_translation.js.coffee @@ -25,7 +25,8 @@ angular.module("admin.utils").directive "tagsWithTranslation", ($timeout) -> scope.object[scope.tagsAttr] ||= [] compileTagList() - scope.tagAdded = -> + scope.tagAdded = (tag)-> + tag.text = tag.text.toLowerCase() scope.onTagAdded() compileTagList() diff --git a/app/assets/javascripts/admin/variant_overrides/controllers/variant_overrides_controller.js.coffee b/app/assets/javascripts/admin/variant_overrides/controllers/variant_overrides_controller.js.coffee index ac6c993fef..0fe051f496 100644 --- a/app/assets/javascripts/admin/variant_overrides/controllers/variant_overrides_controller.js.coffee +++ b/app/assets/javascripts/admin/variant_overrides/controllers/variant_overrides_controller.js.coffee @@ -25,6 +25,7 @@ angular.module("admin.variantOverrides").controller "AdminVariantOverridesCtrl", $scope.resetSelectFilters = -> $scope.producerFilter = 0 + $scope.importDateFilter = '0' $scope.query = '' $scope.resetSelectFilters() diff --git a/app/assets/javascripts/admin/variant_overrides/filters/import_date_filter.js.coffee b/app/assets/javascripts/admin/variant_overrides/filters/import_date_filter.js.coffee new file mode 100644 index 0000000000..b46b996128 --- /dev/null +++ b/app/assets/javascripts/admin/variant_overrides/filters/import_date_filter.js.coffee @@ -0,0 +1,12 @@ +angular.module("admin.variantOverrides").filter "importDate", ($filter, variantOverrides) -> + return (products, hub_id, date) -> + return [] if !hub_id + return $filter('filter')(products, (product) -> + return true if date == 0 or date == undefined or date == '0' or date == '' + + angular.forEach product.variants (variant) -> + angular.forEach variantOverrides (vo) -> + if vo.variant_id == variant.id and vo.import_date == date + return true + false + , true) diff --git a/app/assets/javascripts/admin/variant_overrides/variant_overrides.js.coffee b/app/assets/javascripts/admin/variant_overrides/variant_overrides.js.coffee index 4d875cbd33..84e6537a64 100644 --- a/app/assets/javascripts/admin/variant_overrides/variant_overrides.js.coffee +++ b/app/assets/javascripts/admin/variant_overrides/variant_overrides.js.coffee @@ -1 +1 @@ -angular.module("admin.variantOverrides", ["admin.indexUtils", "admin.utils", "admin.dropdown", "admin.inventoryItems", 'ngTagsInput']) +angular.module("admin.variantOverrides", ["ofn.admin", "admin.indexUtils", "admin.utils", "admin.dropdown", "admin.inventoryItems", 'ngTagsInput']) diff --git a/app/assets/javascripts/darkswarm/controllers/authentication/forgot_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/authentication/forgot_controller.js.coffee index 85920de958..7c3b67d823 100644 --- a/app/assets/javascripts/darkswarm/controllers/authentication/forgot_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/authentication/forgot_controller.js.coffee @@ -6,7 +6,14 @@ Darkswarm.controller "ForgotCtrl", ($scope, $http, $location, AuthenticationServ if $scope.spree_user.email != null $http.post("/user/spree_user/password", {spree_user: $scope.spree_user}).success (data)-> $scope.sent = true - .error (data) -> - $scope.errors = t 'email_not_found' + .error (data, status) -> + $scope.errors = data.error + $scope.user_unconfirmed = (status == 401) else $scope.errors = t 'email_required' + + $scope.resend_confirmation = -> + $http.post("/user/spree_user/confirmation", {spree_user: $scope.spree_user, return_url: $location.absUrl()}).success (data)-> + $scope.messages = t('devise.confirmations.send_instructions') + .error (data) -> + $scope.errors = t('devise.confirmations.failed_to_send') diff --git a/app/assets/javascripts/darkswarm/controllers/authentication/login_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/authentication/login_controller.js.coffee index 016d60d9d2..c6c54e38d1 100644 --- a/app/assets/javascripts/darkswarm/controllers/authentication/login_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/authentication/login_controller.js.coffee @@ -1,6 +1,13 @@ Darkswarm.controller "LoginCtrl", ($scope, $timeout, $location, $http, $window, AuthenticationService, Redirections, Loading) -> $scope.path = "/login" + $scope.modalMessage = null + + $scope.$watch (-> + AuthenticationService.modalMessage + ), (newValue) -> + $scope.errors = newValue + $scope.submit = -> Loading.message = t 'logging_in' $http.post("/user/spree_user/sign_in", {spree_user: $scope.spree_user}).success (data)-> @@ -14,7 +21,7 @@ Darkswarm.controller "LoginCtrl", ($scope, $timeout, $location, $http, $window, $scope.user_unconfirmed = (data.error == t('devise.failure.unconfirmed')) $scope.resend_confirmation = -> - $http.post("/user/spree_user/confirmation", {spree_user: $scope.spree_user}).success (data)-> + $http.post("/user/spree_user/confirmation", {spree_user: $scope.spree_user, return_url: $location.absUrl()}).success (data)-> $scope.messages = t('devise.confirmations.send_instructions') .error (data) -> $scope.errors = t('devise.confirmations.failed_to_send') diff --git a/app/assets/javascripts/darkswarm/controllers/checkout/checkout_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/checkout/checkout_controller.js.coffee index 3397077a00..67ac51293c 100644 --- a/app/assets/javascripts/darkswarm/controllers/checkout/checkout_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/checkout/checkout_controller.js.coffee @@ -1,4 +1,4 @@ -Darkswarm.controller "CheckoutCtrl", ($scope, localStorageService, Checkout, CurrentUser, CurrentHub) -> +Darkswarm.controller "CheckoutCtrl", ($scope, localStorageService, Checkout, CurrentUser, CurrentHub, AuthenticationService, SpreeUser, $http) -> $scope.Checkout = Checkout $scope.submitted = false @@ -20,8 +20,29 @@ Darkswarm.controller "CheckoutCtrl", ($scope, localStorageService, Checkout, Cur $scope.purchase = (event, form) -> event.preventDefault() + $scope.formdata = form $scope.submitted = true - if form.$valid + + if CurrentUser.id + $scope.validateForm(form) + else + $scope.ensureUserIsGuest() + + $scope.validateForm = -> + if $scope.formdata.$valid $scope.Checkout.purchase() else - $scope.$broadcast 'purchaseFormInvalid', form + $scope.$broadcast 'purchaseFormInvalid', $scope.formdata + + $scope.ensureUserIsGuest = (callback = null) -> + $http.post("/user/registered_email", {email: $scope.order.email}).success (data)-> + if data.registered == true + $scope.promptLogin() + else + $scope.validateForm() if $scope.submitted + callback() if callback + + $scope.promptLogin = -> + SpreeUser.spree_user.email = $scope.order.email + AuthenticationService.pushMessage t('devise.failure.already_registered') + AuthenticationService.open '/login' diff --git a/app/assets/javascripts/darkswarm/controllers/checkout/country_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/checkout/country_controller.js.coffee new file mode 100644 index 0000000000..347fa367ad --- /dev/null +++ b/app/assets/javascripts/darkswarm/controllers/checkout/country_controller.js.coffee @@ -0,0 +1,8 @@ +Darkswarm.controller "CountryCtrl", ($scope, availableCountries) -> + + $scope.countries = availableCountries + + $scope.countriesById = $scope.countries.reduce (obj, country) -> + obj[country.id] = country + obj + , {} diff --git a/app/assets/javascripts/darkswarm/controllers/checkout/details_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/checkout/details_controller.js.coffee index e0fef8e343..e8a6b1ff23 100644 --- a/app/assets/javascripts/darkswarm/controllers/checkout/details_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/checkout/details_controller.js.coffee @@ -1,8 +1,16 @@ -Darkswarm.controller "DetailsCtrl", ($scope, $timeout) -> +Darkswarm.controller "DetailsCtrl", ($scope, $timeout, $http, CurrentUser, AuthenticationService, SpreeUser) -> angular.extend(this, new FieldsetMixin($scope)) $scope.name = "details" $scope.nextPanel = "billing" + $scope.login_or_next = (event) -> + event.preventDefault() + unless CurrentUser.id + $scope.ensureUserIsGuest($scope.next) + return + + $scope.next() + $scope.summary = -> [$scope.fullName(), $scope.order.email, diff --git a/app/assets/javascripts/darkswarm/controllers/checkout/payment_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/checkout/payment_controller.js.coffee index 04ff6ab45e..261bf9759a 100644 --- a/app/assets/javascripts/darkswarm/controllers/checkout/payment_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/checkout/payment_controller.js.coffee @@ -9,6 +9,10 @@ Darkswarm.controller "PaymentCtrl", ($scope, $timeout, savedCreditCards, Dates) $scope.secrets.card_month = "1" $scope.secrets.card_year = moment().year() + for card in savedCreditCards when card.is_default + $scope.secrets.selected_card = card.id + break + $scope.summary = -> [$scope.Checkout.paymentMethod()?.name] diff --git a/app/assets/javascripts/darkswarm/controllers/credit_cards_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/credit_cards_controller.js.coffee index 7fa3e4f8c4..868057b22c 100644 --- a/app/assets/javascripts/darkswarm/controllers/credit_cards_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/credit_cards_controller.js.coffee @@ -1,6 +1,7 @@ -Darkswarm.controller "CreditCardsCtrl", ($scope, $timeout, CreditCard, CreditCards, Dates) -> +Darkswarm.controller "CreditCardsCtrl", ($scope, CreditCard, CreditCards) -> angular.extend(this, new FieldsetMixin($scope)) $scope.savedCreditCards = CreditCards.saved + $scope.setDefault = CreditCards.setDefault $scope.CreditCard = CreditCard $scope.secrets = CreditCard.secrets $scope.showForm = CreditCard.show diff --git a/app/assets/javascripts/darkswarm/controllers/group_page_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/group_page_controller.js.coffee index 5d9bb0aa02..da67840752 100644 --- a/app/assets/javascripts/darkswarm/controllers/group_page_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/group_page_controller.js.coffee @@ -19,3 +19,4 @@ Darkswarm.controller "GroupPageCtrl", ($scope, group_enterprises, Enterprises, M $scope.map = angular.copy MapConfiguration.options $scope.mapMarkers = OfnMap.enterprise_markers visible_enterprises + $scope.embedded_layout = window.location.search.indexOf("embedded_shopfront=true") != -1 \ No newline at end of file diff --git a/app/assets/javascripts/darkswarm/controllers/shopping_tabs_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/shopping_tabs_controller.js.coffee deleted file mode 100644 index b99d169c5b..0000000000 --- a/app/assets/javascripts/darkswarm/controllers/shopping_tabs_controller.js.coffee +++ /dev/null @@ -1,12 +0,0 @@ -Darkswarm.controller "ShoppingTabsCtrl", ($scope, $controller, Navigation, $location) -> - angular.extend this, $controller('TabsCtrl', {$scope: $scope}) - - $scope.tabs = - about: { active: Navigation.isActive('/about') } - producers: { active: Navigation.isActive('/producers') } - contact: { active: Navigation.isActive('/contact') } - groups: { active: Navigation.isActive('/groups') } - - $scope.$on '$locationChangeStart', (event, url) -> - tab = $location.path().replace(/^\//, '') - $scope.tabs[tab]?.active = true diff --git a/app/assets/javascripts/darkswarm/directives/shipping_type_selector.js.coffee b/app/assets/javascripts/darkswarm/directives/shipping_type_selector.js.coffee index 15fda75820..1f0196f9fe 100644 --- a/app/assets/javascripts/darkswarm/directives/shipping_type_selector.js.coffee +++ b/app/assets/javascripts/darkswarm/directives/shipping_type_selector.js.coffee @@ -11,8 +11,10 @@ Darkswarm.directive "shippingTypeSelector", -> scope.selectors = delivery: scope.filterSelectors.new icon: "ofn-i_039-delivery" + translation_key: "hubs_delivery" pickup: scope.filterSelectors.new icon: "ofn-i_038-takeaway" + translation_key: "hubs_pickup" scope.emit = -> scope.shippingTypes = diff --git a/app/assets/javascripts/darkswarm/directives/target_blank.js.coffee b/app/assets/javascripts/darkswarm/directives/target_blank.js.coffee new file mode 100644 index 0000000000..f7d52f07bf --- /dev/null +++ b/app/assets/javascripts/darkswarm/directives/target_blank.js.coffee @@ -0,0 +1,6 @@ +Darkswarm.directive "embeddedTargetBlank", -> + restrict: 'A' + compile: (element) -> + elems = (element.children().find("a")) + if window.location.search.indexOf("embedded_shopfront=true") != -1 + elems.attr("target", "_blank") \ No newline at end of file diff --git a/app/assets/javascripts/darkswarm/services/authentication_service.js.coffee b/app/assets/javascripts/darkswarm/services/authentication_service.js.coffee index b504d92c76..4b53ee2230 100644 --- a/app/assets/javascripts/darkswarm/services/authentication_service.js.coffee +++ b/app/assets/javascripts/darkswarm/services/authentication_service.js.coffee @@ -1,7 +1,13 @@ +# This class deals with displaying things in the login modal. It chooses +# the modal tab templates and deals with switching tabs and passing data +# between the tabs. It has direct access to the instance of the login modal, +# and provides that access to other controllers as a service. + Darkswarm.factory "AuthenticationService", (Navigation, $modal, $location, Redirections, Loading)-> new class AuthenticationService selectedPath: "/login" + modalMessage: null constructor: -> if $location.path() in ["/login", "/signup", "/forgot"] || location.pathname is '/register/auth' @@ -32,6 +38,8 @@ Darkswarm.factory "AuthenticationService", (Navigation, $modal, $location, Redir 'registration_authentication.html' else 'authentication.html' + pushMessage: (message) -> + @modalMessage = String(message) select: (path)=> @selectedPath = path diff --git a/app/assets/javascripts/darkswarm/services/credit_cards.js.coffee b/app/assets/javascripts/darkswarm/services/credit_cards.js.coffee index c588ef681c..d217a7a7bd 100644 --- a/app/assets/javascripts/darkswarm/services/credit_cards.js.coffee +++ b/app/assets/javascripts/darkswarm/services/credit_cards.js.coffee @@ -1,6 +1,15 @@ -Darkswarm.factory 'CreditCards', (savedCreditCards)-> +Darkswarm.factory 'CreditCards', ($http, $filter, savedCreditCards, RailsFlashLoader)-> new class CreditCard - saved: savedCreditCards + saved: $filter('orderBy')(savedCreditCards,'-is_default') add: (card) -> @saved.push card + + setDefault: (card) => + card.is_default = true + for othercard in @saved when othercard != card + othercard.is_default = false + $http.put("/credit_cards/#{card.id}", is_default: true).then (data) -> + RailsFlashLoader.loadFlash({success: t('js.default_card_updated')}) + , (response) -> + RailsFlashLoader.loadFlash({error: response.data.flash.error}) diff --git a/app/assets/javascripts/darkswarm/services/enterprise_modal.js.coffee b/app/assets/javascripts/darkswarm/services/enterprise_modal.js.coffee index a0fb3ce5c5..ae43c85090 100644 --- a/app/assets/javascripts/darkswarm/services/enterprise_modal.js.coffee +++ b/app/assets/javascripts/darkswarm/services/enterprise_modal.js.coffee @@ -3,6 +3,7 @@ Darkswarm.factory "EnterpriseModal", ($modal, $rootScope)-> new class EnterpriseModal open: (enterprise)-> scope = $rootScope.$new(true) # Spawn an isolate to contain the enterprise + scope.embedded_layout = window.location.search.indexOf("embedded_shopfront=true") != -1 scope.enterprise = enterprise $modal.open(templateUrl: "enterprise_modal.html", scope: scope) diff --git a/app/assets/javascripts/darkswarm/services/navigation.js.coffee b/app/assets/javascripts/darkswarm/services/navigation.js.coffee index f511e29e0c..b58786e930 100644 --- a/app/assets/javascripts/darkswarm/services/navigation.js.coffee +++ b/app/assets/javascripts/darkswarm/services/navigation.js.coffee @@ -17,10 +17,9 @@ Darkswarm.factory 'Navigation', ($location, $window) -> @navigate(path) go: (path)-> - if path.match /^http/ - $window.location.href = path - else - $window.location.pathname = path + # The browser treats this like clicking on a link. + # It works for absolute paths, relative paths and URLs alike. + $window.location.href = path reload: -> $window.location.reload() diff --git a/app/assets/javascripts/templates/admin/modals/image_upload.html.haml b/app/assets/javascripts/templates/admin/modals/image_upload.html.haml index 76a96805ef..f864bbee5f 100644 --- a/app/assets/javascripts/templates/admin/modals/image_upload.html.haml +++ b/app/assets/javascripts/templates/admin/modals/image_upload.html.haml @@ -6,5 +6,5 @@ %img.spinner{ src: "/assets/spinning-circles.svg", ng: { hide: "!imageUploader.isUploading" }} %img.preview{ng: {src: "{{imagePreview}}", class: "{'faded': imageUploader.isUploading}"}} - %label{for: 'image-upload', class: 'button'} #{t('admin.products.bulk_edit.upload_an_image')} + %label{for: 'image-upload', class: 'button'} {{ 'admin.products.index.upload_an_image' | t }} %input#image-upload{hidden: true, type: 'file', 'nv-file-select' => true, uploader: "imageUploader"} diff --git a/app/assets/javascripts/templates/admin/tags_input.html.haml b/app/assets/javascripts/templates/admin/tags_input.html.haml index f08c0cc6f6..c652aea610 100644 --- a/app/assets/javascripts/templates/admin/tags_input.html.haml +++ b/app/assets/javascripts/templates/admin/tags_input.html.haml @@ -1,7 +1,7 @@ %tags-input{ template: 'admin/tag.html', "placeholder" => t('admin.order_cycles.form.add_a_tag'), ng: { model: 'object[tagsAttr]', class: "{'limit-reached': limitReached}"}, - on: { tag: { added: 'tagAdded()', removed:'tagRemoved()' } } } + on: { tag: { added: 'tagAdded($tag)', removed:'tagRemoved()' } } } %auto-complete{ ng: { if: "findTags" }, source: "findTags({query: $query})", template: "admin/tag_autocomplete.html", "min-length" => "0", diff --git a/app/assets/javascripts/templates/forgot.html.haml b/app/assets/javascripts/templates/forgot.html.haml index 958f1daa39..0157b6fc36 100644 --- a/app/assets/javascripts/templates/forgot.html.haml +++ b/app/assets/javascripts/templates/forgot.html.haml @@ -2,24 +2,28 @@ %form{ ng: { controller: "ForgotCtrl", submit: "submit()" } } .row .large-12.columns - .alert-box.success.radius{"ng-show" => "sent"} - {{'password_reset_sent' | t}} + .alert-box.success{"ng-show" => "sent"} + {{ 'password_reset_sent' | t }} - %div{"ng-show" => "!sent"} - .alert-box.alert{"ng-show" => "errors != null"} - {{ errors }} + .alert-box.success{"ng-show" => "messages != null"} + {{ messages }} - .row - .large-12.columns - %label{for: "email"} {{'signup_email' | t}} - %input.title.input-text{name: "email", - type: "email", - id: "email", - tabindex: 1, - "ng-model" => "spree_user.email"} - .row - .large-12.columns - %input.button.primary{name: "commit", - tabindex: "3", - type: "submit", - value: "{{'reset_password' | t}}"} + .alert-box.alert{"ng-show" => "errors != null"} + {{ errors }} + %a{ng: {show: 'user_unconfirmed', click: 'resend_confirmation()'}} + = t('devise.confirmations.resend_confirmation_email') + + .row + .large-12.columns + %label{for: "email"} {{'signup_email' | t}} + %input.title.input-text{name: "email", + type: "email", + id: "email", + tabindex: 1, + "ng-model" => "spree_user.email"} + .row + .large-12.columns + %input.button.primary{name: "commit", + tabindex: "3", + type: "submit", + value: "{{'reset_password' | t}}"} diff --git a/app/assets/javascripts/templates/partials/enterprise_details.html.haml b/app/assets/javascripts/templates/partials/enterprise_details.html.haml index 35a47b3f11..609ac155b6 100644 --- a/app/assets/javascripts/templates/partials/enterprise_details.html.haml +++ b/app/assets/javascripts/templates/partials/enterprise_details.html.haml @@ -15,7 +15,7 @@ .about-container.pad-top %img.enterprise-logo{"ng-src" => "{{::enterprise.logo}}", "ng-if" => "::enterprise.logo"} - %p.text-small{"ng-bind-html" => "::enterprise.long_description"} + %div{"ng-bind-html" => "::enterprise.long_description"} .small-12.large-4.columns %ng-include{src: "'partials/contact.html'"} %ng-include{src: "'partials/follow.html'"} diff --git a/app/assets/javascripts/templates/partials/enterprise_header.html.haml b/app/assets/javascripts/templates/partials/enterprise_header.html.haml index 066ebdd957..bb07fdd019 100644 --- a/app/assets/javascripts/templates/partials/enterprise_header.html.haml +++ b/app/assets/javascripts/templates/partials/enterprise_header.html.haml @@ -2,7 +2,7 @@ .highlight-top.row .small-12.medium-7.large-8.columns %h3{"ng-if" => "::enterprise.is_distributor"} - %a{"ng-href" => "{{::enterprise.path}}", "ofn-change-hub" => "enterprise"} + %a{"ng-href" => "{{::enterprise.path}}", "ofn-change-hub" => "enterprise", "ng-attr-target" => "{{ embedded_layout ? '_blank' : undefined}}"} %i{"ng-class" => "::enterprise.icon_font"} %span{"ng-bind" => "::enterprise.name"} %h3{"ng-if" => "::!enterprise.is_distributor", "ng-class" => "::{'is_producer' : enterprise.is_primary_producer}"} diff --git a/app/assets/javascripts/templates/partials/hub_details.html.haml b/app/assets/javascripts/templates/partials/hub_details.html.haml index 5fb0c93696..baa9055305 100644 --- a/app/assets/javascripts/templates/partials/hub_details.html.haml +++ b/app/assets/javascripts/templates/partials/hub_details.html.haml @@ -14,7 +14,7 @@ {{'hubs_delivery' | t}} .row .columns.small-12 - %a.cta-hub{"ng-href" => "{{::enterprise.path}}", + %a.cta-hub{"ng-href" => "{{::enterprise.path}}", "ng-attr-target" => "{{ embedded_layout ? '_blank' : undefined}}", "ng-class" => "{primary: enterprise.active, secondary: !enterprise.active}", "ofn-change-hub" => "enterprise"} .hub-name{"ng-bind" => "::enterprise.name"} diff --git a/app/assets/javascripts/templates/shipping_type_selector.html.haml b/app/assets/javascripts/templates/shipping_type_selector.html.haml index 7774ab62c0..45f35911d4 100644 --- a/app/assets/javascripts/templates/shipping_type_selector.html.haml +++ b/app/assets/javascripts/templates/shipping_type_selector.html.haml @@ -1,4 +1,4 @@ %ul.small-block-grid-2.medium-block-grid-4.large-block-grid-2 %active-selector{"ng-repeat" => "(name, selector) in selectors"} %i{"ng-class" => "selector.icon"} - {{ name | capitalize }} + {{ selector.translation_key | t | capitalize }} \ No newline at end of file diff --git a/app/assets/stylesheets/admin/product_import.css.scss b/app/assets/stylesheets/admin/product_import.css.scss index 907e8b1a5c..4210439b78 100644 --- a/app/assets/stylesheets/admin/product_import.css.scss +++ b/app/assets/stylesheets/admin/product_import.css.scss @@ -1,3 +1,38 @@ +.product-import-introduction { + + h1, h2, h3, h4, h5, h6 { + margin: 1.5em 0 1em; + } + + h6 { + font-size: 1em; + } + + p { + margin-bottom: 1em; + } + + span.category { + display: inline-block; + background-color: #f3f3f3; + padding: 0.4em 0.8em; + margin: 0 0.4em 0.5em 0; + } + + table { + + &.product-import-columns tr:hover td { + background-color: transparent; + } + + thead th { + text-transform: none; + font-size: 100%; + text-align: left; + } + } +} + div.panel-section { .neutral { @@ -15,9 +50,7 @@ div.panel-section { div.panel-header { width: 100%; - //font-size: 1.5em; clear: both; - //border: 1px solid #ccc; float: left; padding: 0.5em; @@ -68,7 +101,6 @@ div.panel-section { div.panel-content { width: 100%; clear: both; - //border: 1px solid #ccc; margin-bottom: 0.5em; background-color: #f9f9f9; padding: 1.5em; @@ -87,8 +119,6 @@ div.panel-section { white-space: nowrap; } tr.error { - //background-color: #ffe6e4; - //color: #ee4728; color: #c84C4c; } tr:hover td.invalid { @@ -119,9 +149,7 @@ div.panel-section { margin-bottom: 0.2em; } } - } - } br.panels.clearfix { @@ -132,12 +160,6 @@ table.import-settings { background-color: transparent !important; width: auto; - //select { - // width: 100%; - //} - tr { - - } tbody tr:hover td { background-color: #f3f3f3; } @@ -159,14 +181,25 @@ table.import-settings { padding-right: 2.5em; } tr:first-child td { - //border-top: 1px solid #eee; border-top: 0; } tr:last-child td { - //border-top: 1px solid #eee; border-bottom: 0; } - + div.select2-container { + width: 13.5em; + } + div.select2-container.select2-container-disabled { + a.select2-choice, span.select2-arrow { + background-color: #d5d5d5; + } + } + input[disabled], input:disabled { + background-color: #d5d5d5; + opacity: 1; + border-color: transparent; + color: white !important; + } } .panel-section.import-settings { @@ -189,8 +222,6 @@ table.import-settings { } } - - .post-save-results { p { font-size: 1.25em; @@ -222,4 +253,65 @@ table.import-settings { font-size: 1.05em; margin-top: 0.4em; } -} \ No newline at end of file +} + +form.product-import, div.post-save-results, div.import-wrapper { + input[type="submit"] { + margin-right: 0.5em; + } + input[type="submit"], button, a.button { + min-width: 8em; + text-align: center; + } +} + +form.product-import, div.save-results { + transition: all linear 0.25s; +} + +form.product-import.ng-hide, div.save-results.ng-hide { + opacity: 0; +} + +div.import-wrapper { + div.progress-interface { + text-align: center; + transition: all linear 0.25s; + + button:disabled { + background: #ccc !important; + } + + } + div.progress-interface.ng-hide { + position: absolute; + width: 100%; + opacity: 0; + } + .post-save-results { + a.button{ + float: left; + margin-right: 0.5em; + } + } +} + +div.progress-bar { + height: 25px; + width: 30em; + max-width: 90%; + margin: 1em auto; + background: #f7f7f7; + padding: 3px; + border-radius: 0.3em; + border: 1px solid #eee; + + span.progress-track{ + display: block; + background: #b7ea53; + height: 100%; + border-radius: 0.3em; + box-shadow: inset 0 0 3px rgba(0,0,0,0.3); + transition: width 0.5s ease-in-out; + } +} diff --git a/app/assets/stylesheets/admin/reports.css.scss b/app/assets/stylesheets/admin/reports.css.scss new file mode 100644 index 0000000000..13df70de8b --- /dev/null +++ b/app/assets/stylesheets/admin/reports.css.scss @@ -0,0 +1,10 @@ +.report__table { + margin-top: 2em; +} +.report__message { + margin-top: 2em; + border: 1px solid #cee1f4; + border-radius: .5em; + padding: .5em; + text-align: center; +} diff --git a/app/assets/stylesheets/admin/welcome.css.scss b/app/assets/stylesheets/admin/welcome.css.scss index fe31a07396..77cd21e134 100644 --- a/app/assets/stylesheets/admin/welcome.css.scss +++ b/app/assets/stylesheets/admin/welcome.css.scss @@ -6,7 +6,7 @@ padding: 4em 2em; @include fullbg; - + background-color: black; background-image: url("/assets/home/tagline-bg.jpg"); background-repeat: no-repeat; background-position: center center; diff --git a/app/assets/stylesheets/darkswarm/embedded_shopfront.css.scss b/app/assets/stylesheets/darkswarm/embedded_shopfront.css.scss index b29acab34e..bc02417b55 100644 --- a/app/assets/stylesheets/darkswarm/embedded_shopfront.css.scss +++ b/app/assets/stylesheets/darkswarm/embedded_shopfront.css.scss @@ -1,3 +1,5 @@ +@import "typography"; + body.embedded { nav.top-bar { ul.left, ul.center, ul.right li.current_hub { @@ -28,6 +30,22 @@ body.embedded { footer { display: none; } + + .powered-by-embedded { + display: block; + } + + .contact { + display: none; + } + + .embedded-fullwidth { + width: 100%; + } + + #group-page header { + display: none; + } } nav.top-bar ul.right li.powered-by { @@ -51,6 +69,17 @@ nav.top-bar ul.right li.powered-by { } } +.powered-by-embedded { + opacity: 0.6; + @include headingFont; + font-size: 1rem; + font-weight: 300; + color: #555; + padding: 0 !important; + display: none; + margin-top: 6px; + } + .blocked-cookies { text-align: center; margin-bottom: 0 !important; diff --git a/app/assets/stylesheets/darkswarm/tabs.css.scss b/app/assets/stylesheets/darkswarm/shop_tabs.css.scss similarity index 60% rename from app/assets/stylesheets/darkswarm/tabs.css.scss rename to app/assets/stylesheets/darkswarm/shop_tabs.css.scss index 55b55f1189..c1777ec851 100644 --- a/app/assets/stylesheets/darkswarm/tabs.css.scss +++ b/app/assets/stylesheets/darkswarm/shop_tabs.css.scss @@ -2,18 +2,8 @@ @import "mixins"; @import "branding"; -// Foundation overrides -#tabs .tabs-content > .content p { - max-width: 100% !important; -} - -.tabs-content > .content { - padding-top: 0 !important; -} - // Tabs styling - -#tabs { +.tabset-ctrl#shop-tabs { background: url("/assets/gray_jean.png") top left repeat; @include box-shadow(inset 0 2px 3px 0 rgba(0, 0, 0, 0.15)); @@ -21,29 +11,22 @@ display: block; color: $dark-grey; - .header { - text-align: center; - text-transform: uppercase; - color: $dark-grey; - border-bottom: 1px solid $disabled-dark; - margin-top: 0.75rem; - margin-bottom: 0.75rem; - padding-bottom: 0.25rem; - font-size: 0.875rem; - } - - .panel { - border-color: $clr-brick-bright; - background-color: rgba(255, 255, 255, 0); - } - - dl dd { + .tab { text-align: center; + border-top: 4px solid transparent; @media all and (max-width: 640px) { text-align: left; } + >a { + outline: none; + display: block; + background-color: #efefef; + color: #222; + font-family: "Oswald", sans-serif; + } + a { @include headingFont; @@ -67,15 +50,8 @@ padding: 0.35em 0 0.65em 0; text-shadow: none; } - } - } - // inactive nav link - dl { - dd { - border-top: 4px solid transparent; - - a:after { + &:after { padding-left: 8px; content: ""; visibility: hidden; @@ -88,17 +64,13 @@ } } - dd:hover { + &:hover { a:after { visibility: visible; } } - } - // active nav link - - dl { - dd.active { + &.selected { border-top: 4px solid $clr-brick; @media all and (max-width: 640px) { @@ -129,33 +101,44 @@ // content revealed in accordion - .tabs-content { + .tab-view { margin-bottom: 0; + padding: 0; + background: none; + border: none; - & > .content { - background: none; - border: none; + img { + margin: 0px 0px 0px 40px; + } - img { - margin: 0px 0px 0px 40px; + p { + max-width: 100%; + + @media all and (max-width: 768px) { + height: auto !important; } + } - p { - max-width: 555px; + ul { + list-style-type: none; + padding-left: none; + } - @media all and (max-width: 768px) { - height: auto !important; - } - } + .header { + text-align: center; + text-transform: uppercase; + color: $dark-grey; + border-bottom: 1px solid $disabled-dark; + margin-top: 0.75rem; + margin-bottom: 0.75rem; + padding-bottom: 0.25rem; + font-size: 0.875rem; + } - ul { - list-style-type: none; - padding-left: none; - } - - .panel { - padding-bottom: 1.25em; - } + .panel { + padding-bottom: 1.25em; + border-color: $clr-brick-bright; + background-color: rgba(255, 255, 255, 0); } } } diff --git a/app/assets/stylesheets/darkswarm/tabset.css.scss b/app/assets/stylesheets/darkswarm/tabset.css.scss index eaefe188a8..c100bff672 100644 --- a/app/assets/stylesheets/darkswarm/tabset.css.scss +++ b/app/assets/stylesheets/darkswarm/tabset.css.scss @@ -2,7 +2,7 @@ @import "mixins"; @import "branding"; -.tabset-ctrl { +.tabset-ctrl:not(#shop-tabs) { .tab-view { padding-top: 30px; } diff --git a/app/controllers/admin/manager_invitations_controller.rb b/app/controllers/admin/manager_invitations_controller.rb index ed70322f34..5849aa2880 100644 --- a/app/controllers/admin/manager_invitations_controller.rb +++ b/app/controllers/admin/manager_invitations_controller.rb @@ -1,5 +1,7 @@ module Admin class ManagerInvitationsController < Spree::Admin::BaseController + authorize_resource class: false + def create @email = params[:email] @enterprise = Enterprise.find(params[:enterprise_id]) @@ -28,6 +30,8 @@ module Admin password = Devise.friendly_token new_user = Spree::User.create(email: @email, unconfirmed_email: @email, password: password) new_user.reset_password_token = Devise.friendly_token + # Same time as used in Devise's lib/devise/models/recoverable.rb. + new_user.reset_password_sent_at = Time.now.utc new_user.save! @enterprise.users << new_user diff --git a/app/controllers/admin/order_cycles_controller.rb b/app/controllers/admin/order_cycles_controller.rb index 445e268085..ad728c2185 100644 --- a/app/controllers/admin/order_cycles_controller.rb +++ b/app/controllers/admin/order_cycles_controller.rb @@ -1,7 +1,3 @@ -require 'open_food_network/permissions' -require 'open_food_network/order_cycle_form_applicator' -require 'open_food_network/proxy_order_syncer' - module Admin class OrderCyclesController < ResourceController include OrderCyclesHelper @@ -9,17 +5,14 @@ module Admin prepend_before_filter :load_data_for_index, :only => :index before_filter :require_coordinator, only: :new before_filter :remove_protected_attrs, only: [:update] - before_filter :check_editable_schedule_ids, only: [:create, :update] before_filter :require_order_cycle_set_params, only: [:bulk_update] around_filter :protect_invalid_destroy, only: :destroy - create.after :sync_subscriptions - update.after :sync_subscriptions def index respond_to do |format| format.html format.json do - render_as_json @collection, ams_prefix: params[:ams_prefix], current_user: spree_current_user + render_as_json @collection, ams_prefix: params[:ams_prefix], current_user: spree_current_user, subscriptions_count: SubscriptionsCount.new(@collection) end end end @@ -43,51 +36,36 @@ module Admin end def create - @order_cycle = OrderCycle.new(params[:order_cycle]) + @order_cycle_form = OrderCycleForm.new(@order_cycle, params, spree_current_user) - respond_to do |format| - if @order_cycle.save - OpenFoodNetwork::OrderCycleFormApplicator.new(@order_cycle, spree_current_user).go! - invoke_callbacks(:create, :after) - flash[:notice] = I18n.t(:order_cycles_create_notice) - format.html { redirect_to admin_order_cycles_path } - format.json { render :json => { success: true } } - else - format.html - format.json { render :json => { errors: @order_cycle.errors.full_messages }, status: :unprocessable_entity } - end + if @order_cycle_form.save + flash[:notice] = I18n.t(:order_cycles_create_notice) + render json: { success: true } + else + render json: { errors: @order_cycle.errors.full_messages }, status: :unprocessable_entity end end def update - @order_cycle = OrderCycle.find params[:id] + @order_cycle_form = OrderCycleForm.new(@order_cycle, params, spree_current_user) - respond_to do |format| - if @order_cycle.update_attributes(params[:order_cycle]) - unless params[:order_cycle][:incoming_exchanges].nil? && params[:order_cycle][:outgoing_exchanges].nil? - # Only update apply exchange information if it is actually submmitted - OpenFoodNetwork::OrderCycleFormApplicator.new(@order_cycle, spree_current_user).go! - end - invoke_callbacks(:update, :after) + if @order_cycle_form.save + respond_to do |format| flash[:notice] = I18n.t(:order_cycles_update_notice) if params[:reloading] == '1' format.html { redirect_to main_app.edit_admin_order_cycle_path(@order_cycle) } - format.json { render :json => { :success => true } } - else - format.json { render :json => { errors: @order_cycle.errors.full_messages }, status: :unprocessable_entity } + format.json { render json: { :success => true } } end + else + render json: { errors: @order_cycle.errors.full_messages }, status: :unprocessable_entity end end def bulk_update if order_cycle_set.andand.save - respond_to do |format| - format.json { render_as_json @order_cycles, ams_prefix: 'index', current_user: spree_current_user } - end + render_as_json @order_cycles, ams_prefix: 'index', current_user: spree_current_user, subscriptions_count: SubscriptionsCount.new(@collection) else - respond_to do |format| - order_cycle = order_cycle_set.collection.find{ |oc| oc.errors.present? } - format.json { render :json => { errors: order_cycle.errors.full_messages }, status: :unprocessable_entity } - end + order_cycle = order_cycle_set.collection.find{ |oc| oc.errors.present? } + render json: { errors: order_cycle.errors.full_messages }, status: :unprocessable_entity end end @@ -193,29 +171,6 @@ module Admin end end - def check_editable_schedule_ids - return unless params[:order_cycle][:schedule_ids] - requested = params[:order_cycle][:schedule_ids].map(&:to_i) - @existing_schedule_ids = @order_cycle.persisted? ? @order_cycle.schedule_ids : [] - permitted = Schedule.where(id: requested | @existing_schedule_ids).merge(OpenFoodNetwork::Permissions.new(spree_current_user).editable_schedules).pluck(:id) - result = @existing_schedule_ids - result |= (requested & permitted) # add any requested & permitted ids - result -= ((result & permitted) - requested) # remove any existing and permitted ids that were not specifically requested - params[:order_cycle][:schedule_ids] = result - end - - def sync_subscriptions - return unless params[:order_cycle][:schedule_ids] - removed_ids = @existing_schedule_ids - @order_cycle.schedule_ids - new_ids = @order_cycle.schedule_ids - @existing_schedule_ids - if removed_ids.any? || new_ids.any? - schedules = Schedule.where(id: removed_ids + new_ids) - subscriptions = Subscription.where(schedule_id: schedules) - syncer = OpenFoodNetwork::ProxyOrderSyncer.new(subscriptions) - syncer.sync! - end - end - def order_cycles_from_set remove_unauthorized_bulk_attrs OrderCycle.where(id: params[:order_cycle_set][:collection_attributes].map{ |k,v| v[:id] }) diff --git a/app/controllers/admin/product_import_controller.rb b/app/controllers/admin/product_import_controller.rb index 83d476bc6c..4037bcd85b 100644 --- a/app/controllers/admin/product_import_controller.rb +++ b/app/controllers/admin/product_import_controller.rb @@ -1,62 +1,95 @@ require 'roo' -class Admin::ProductImportController < Spree::Admin::BaseController +module Admin + class ProductImportController < Spree::Admin::BaseController + before_filter :validate_upload_presence, except: %i[index guide validate_data] - before_filter :validate_upload_presence, except: :index - - def import - # Save uploaded file to tmp directory - @filepath = save_uploaded_file(params[:file]) - @importer = ProductImporter.new(File.new(@filepath), editable_enterprises) - - check_file_errors @importer - check_spreadsheet_has_data @importer - - @tax_categories = Spree::TaxCategory.order('is_default DESC, name ASC') - @shipping_categories = Spree::ShippingCategory.order('name ASC') - end - - def save - @importer = ProductImporter.new(File.new(params[:filepath]), editable_enterprises, params[:settings]) - @importer.save_all if @importer.has_valid_entries? - end - - private - - def validate_upload_presence - unless params[:file] || (params[:filepath] && File.exist?(params[:filepath])) - redirect_to '/admin/product_import', notice: I18n.t(:product_import_file_not_found_notice) - return + def guide + @product_categories = Spree::Taxon.order('name ASC').pluck(:name).uniq + @tax_categories = Spree::TaxCategory.order('name ASC').pluck(:name) + @shipping_categories = Spree::ShippingCategory.order('name ASC').pluck(:name) end - end - def check_file_errors(importer) - if importer.errors.present? - redirect_to '/admin/product_import', notice: @importer.errors.full_messages.to_sentence - return + def import + # Save uploaded file to tmp directory + @filepath = save_uploaded_file(params[:file]) + @importer = ProductImport::ProductImporter.new(File.new(@filepath), spree_current_user, params[:settings]) + @original_filename = params[:file].try(:original_filename) + + check_file_errors @importer + check_spreadsheet_has_data @importer + + @tax_categories = Spree::TaxCategory.order('is_default DESC, name ASC') + @shipping_categories = Spree::ShippingCategory.order('name ASC') end - end - def check_spreadsheet_has_data(importer) - unless importer.item_count - redirect_to '/admin/product_import', notice: I18n.t(:product_import_no_data_in_spreadsheet_notice) - return + def validate_data + return unless process_data('validate') + render json: @importer.import_results, response: 200 end - end - def save_uploaded_file(upload) - filename = 'import' + Time.now.strftime('%d-%m-%Y-%H-%M-%S') - extension = '.' + upload.original_filename.split('.').last - directory = 'tmp/product_import' - Dir.mkdir(directory) unless File.exists?(directory) - File.open(Rails.root.join(directory, filename+extension), 'wb') do |f| - f.write(upload.read) - f.path + def save_data + return unless process_data('save') + render json: @importer.save_results, response: 200 end - end - # Define custom model class for Cancan permissions - def model_class - ProductImporter + def reset_absent_products + @importer = ProductImport::ProductImporter.new(File.new(params[:filepath]), spree_current_user, import_into: params[:import_into], enterprises_to_reset: params[:enterprises_to_reset], updated_ids: params[:updated_ids], settings: params[:settings]) + + if params.key?(:enterprises_to_reset) && params.key?(:updated_ids) + @importer.reset_absent(params[:updated_ids]) + end + + render json: @importer.products_reset_count + end + + private + + def validate_upload_presence + unless params[:file] || (params[:filepath] && File.exist?(params[:filepath])) + redirect_to '/admin/product_import', notice: I18n.t(:product_import_file_not_found_notice) + end + end + + def process_data(method) + @importer = ProductImport::ProductImporter.new(File.new(params[:filepath]), spree_current_user, start: params[:start], end: params[:end], settings: params[:settings]) + + begin + @importer.send("#{method}_entries") + rescue StandardError => e + render json: e.message, response: 500 + return false + end + + true + end + + def check_file_errors(importer) + if importer.errors.present? + redirect_to '/admin/product_import', notice: @importer.errors.full_messages.to_sentence + end + end + + def check_spreadsheet_has_data(importer) + unless importer.item_count + redirect_to '/admin/product_import', notice: I18n.t(:product_import_no_data_in_spreadsheet_notice) + end + end + + def save_uploaded_file(upload) + filename = 'import' + Time.zone.now.strftime('%d-%m-%Y-%H-%M-%S') + extension = '.' + upload.original_filename.split('.').last + directory = 'tmp/product_import' + Dir.mkdir(directory) unless File.exist?(directory) + File.open(Rails.root.join(directory, filename + extension), 'wb') do |f| + f.write(upload.read) + f.path + end + end + + # Define custom model class for Cancan permissions + def model_class + ProductImport::ProductImporter + end end end diff --git a/app/controllers/admin/variant_overrides_controller.rb b/app/controllers/admin/variant_overrides_controller.rb index 5aa1975e8d..4a49f53c7c 100644 --- a/app/controllers/admin/variant_overrides_controller.rb +++ b/app/controllers/admin/variant_overrides_controller.rb @@ -3,6 +3,7 @@ require 'open_food_network/spree_api_key_loader' module Admin class VariantOverridesController < ResourceController include OpenFoodNetwork::SpreeApiKeyLoader + include EnterprisesHelper prepend_before_filter :load_data before_filter :load_collection, only: [:bulk_update] @@ -55,6 +56,20 @@ module Admin variant_override_enterprises_per_hub @inventory_items = InventoryItem.where(enterprise_id: @hubs) + @import_dates = inventory_import_dates.uniq.to_json + end + + def inventory_import_dates + import_dates = VariantOverride. + select('DISTINCT variant_overrides.import_date'). + where('variant_overrides.hub_id IN (?) + AND variant_overrides.import_date IS NOT NULL', editable_enterprises.collect(&:id)). + order('import_date DESC') + + options = [{ id: '0', name: 'All' }] + import_dates.collect(&:import_date).map { |i| options.push(id: i.to_date, name: i.to_date.to_formatted_s(:long)) } + + options end def load_collection diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index e19c9bb26b..d22e8a6ba4 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -4,6 +4,7 @@ class ApplicationController < ActionController::Base protect_from_forgery prepend_before_filter :restrict_iframes + before_filter :set_cache_headers # Issue #1213, prevent cart emptying via cache when using back button include EnterprisesHelper helper CssSplitter::ApplicationHelper @@ -59,7 +60,7 @@ class ApplicationController < ActionController::Base return if embedding_without_https? response.headers.delete 'X-Frame-Options' - response.headers['Content-Security-Policy'] = "frame-ancestors #{embedded_shopfront_referer}" + response.headers['Content-Security-Policy'] = "frame-ancestors #{URI(request.referer).host.downcase}" check_embedded_request set_embedded_layout @@ -152,4 +153,10 @@ class ApplicationController < ActionController::Base nil end + def set_cache_headers # https://jacopretorius.net/2014/01/force-page-to-reload-on-browser-back-in-rails.html + response.headers["Cache-Control"] = "no-cache, no-store, max-age=0, must-revalidate" + response.headers["Pragma"] = "no-cache" + response.headers["Expires"] = "Fri, 01 Jan 1990 00:00:00 GMT" + end + end diff --git a/app/controllers/checkout_controller.rb b/app/controllers/checkout_controller.rb index 16b5134b11..8cba0d8a0e 100644 --- a/app/controllers/checkout_controller.rb +++ b/app/controllers/checkout_controller.rb @@ -208,7 +208,7 @@ class CheckoutController < Spree::CheckoutController payment_method = Spree::PaymentMethod.find(params[:order][:payments_attributes].first[:payment_method_id]) return unless payment_method.kind_of?(Spree::Gateway::PayPalExpress) - render json: {path: spree.paypal_express_url(payment_method_id: payment_method.id)}, status: 200 + render json: {path: spree.paypal_express_path(payment_method_id: payment_method.id)}, status: 200 true end diff --git a/app/controllers/shop_controller.rb b/app/controllers/shop_controller.rb index a32d5d0b85..8cac75eca2 100644 --- a/app/controllers/shop_controller.rb +++ b/app/controllers/shop_controller.rb @@ -27,6 +27,7 @@ class ShopController < BaseController if request.post? if oc = OrderCycle.with_distributor(@distributor).active.find_by_id(params[:order_cycle_id]) current_order(true).set_order_cycle! oc + @current_order_cycle = oc render partial: "json/order_cycle" else render status: 404, json: "" diff --git a/app/controllers/spree/admin/orders/customer_details_controller_decorator.rb b/app/controllers/spree/admin/orders/customer_details_controller_decorator.rb index 19dd3aae65..a8ce4dad59 100644 --- a/app/controllers/spree/admin/orders/customer_details_controller_decorator.rb +++ b/app/controllers/spree/admin/orders/customer_details_controller_decorator.rb @@ -1,7 +1,20 @@ Spree::Admin::Orders::CustomerDetailsController.class_eval do + before_filter :set_guest_checkout_status, only: :update + # Inherit CanCan permissions for the current order def model_class load_order unless @order @order end + + private + + def set_guest_checkout_status + registered_user = Spree::User.find_by_email(params[:order][:email]) + + params[:order][:guest_checkout] = registered_user.nil? + + return unless registered_user + @order.user_id = registered_user.id + end end diff --git a/app/controllers/spree/admin/overview_controller_decorator.rb b/app/controllers/spree/admin/overview_controller_decorator.rb index ffd2569c1f..548631aaf4 100644 --- a/app/controllers/spree/admin/overview_controller_decorator.rb +++ b/app/controllers/spree/admin/overview_controller_decorator.rb @@ -1,26 +1,74 @@ Spree::Admin::OverviewController.class_eval do def index - # TODO was sorted with is_distributor DESC as well, not sure why or how we want ot sort this now - @enterprises = Enterprise.managed_by(spree_current_user).order('is_primary_producer ASC, name') + @enterprises = Enterprise + .managed_by(spree_current_user) + .order('is_primary_producer ASC, name') @product_count = Spree::Product.active.managed_by(spree_current_user).count @order_cycle_count = OrderCycle.active.managed_by(spree_current_user).count - unspecified = spree_current_user.owned_enterprises.where(sells: 'unspecified') - outside_referral = !URI(request.referer.to_s).path.match(/^\/admin/) - - if OpenFoodNetwork::Permissions.new(spree_current_user).manages_one_enterprise? && !spree_current_user.admin? - @enterprise = @enterprises.first - if outside_referral && unspecified.any? - redirect_to main_app.welcome_admin_enterprise_path(@enterprise) - else - render "single_enterprise_dashboard" - end + if first_access + redirect_to enterprises_path else - if outside_referral && unspecified.any? - redirect_to main_app.admin_enterprises_path - else - render "multi_enterprise_dashboard" - end + render dashboard_view end end + + private + + # Checks whether the user is accessing the admin for the first time + # + # @return [Boolean] + def first_access + outside_referral && incomplete_enterprise_registration? + end + + # Checks whether the request comes from another admin page or not + # + # @return [Boolean] + def outside_referral + !URI(request.referer.to_s).path.match(%r{/admin}) + end + + # Checks that all of the enterprises owned by the current user have a 'sells' + # property specified, which indicates that the registration process has been + # completed + # + # @return [Boolean] + def incomplete_enterprise_registration? + @incomplete_enterprise_registration ||= spree_current_user + .owned_enterprises + .where(sells: 'unspecified') + .exists? + end + + # Returns the appropriate enterprise path for the current user + # + # @return [String] + def enterprises_path + if managed_enterprises.size == 1 + @enterprise = @enterprises.first + main_app.welcome_admin_enterprise_path(@enterprise) + else + main_app.admin_enterprises_path + end + end + + # Returns the appropriate dashboard view for the current user + # + # @return [String] + def dashboard_view + if managed_enterprises.size == 1 + @enterprise = @enterprises.first + :single_enterprise_dashboard + else + :multi_enterprise_dashboard + end + end + + # Returns the list of enterprises the current user is manager of + # + # @return [ActiveRecord::Relation] + def managed_enterprises + spree_current_user.enterprises + end end diff --git a/app/controllers/spree/admin/products_controller_decorator.rb b/app/controllers/spree/admin/products_controller_decorator.rb index 59c4cd3069..3ee88fc18d 100644 --- a/app/controllers/spree/admin/products_controller_decorator.rb +++ b/app/controllers/spree/admin/products_controller_decorator.rb @@ -4,8 +4,11 @@ require 'open_food_network/referer_parser' Spree::Admin::ProductsController.class_eval do include OpenFoodNetwork::SpreeApiKeyLoader include OrderCyclesHelper - before_filter :load_form_data, :only => [:bulk_edit, :new, :create, :edit, :update] - before_filter :load_spree_api_key, :only => [:bulk_edit, :variant_overrides] + include EnterprisesHelper + + before_filter :load_data + before_filter :load_form_data, :only => [:index, :new, :create, :edit, :update] + before_filter :load_spree_api_key, :only => [:index, :variant_overrides] before_filter :strip_new_properties, only: [:create, :update] respond_override create: { html: { @@ -13,7 +16,7 @@ Spree::Admin::ProductsController.class_eval do if params[:button] == "add_another" redirect_to new_admin_product_path else - redirect_to '/admin/products/bulk_edit' + redirect_to admin_products_path end }, failure: lambda { @@ -23,6 +26,11 @@ Spree::Admin::ProductsController.class_eval do def product_distributions end + def index + @current_user = spree_current_user + @show_latest_import = params[:latest_import] || false + end + def bulk_update collection_hash = Hash[params[:products].each_with_index.map { |p,i| [i,p] }] product_set = Spree::ProductSet.new({:collection_attributes => collection_hash}) @@ -49,17 +57,6 @@ Spree::Admin::ProductsController.class_eval do protected - def location_after_save_with_bulk_edit - referer_path = OpenFoodNetwork::RefererParser::path(request.referer) - - if referer_path == '/admin/products/bulk_edit' - bulk_edit_admin_products_url - else - location_after_save_without_bulk_edit - end - end - alias_method_chain :location_after_save, :bulk_edit - def collection # This method is copied directly from the spree product controller, except where we narrow the search below with the managed_by search to support # enterprise users. @@ -86,7 +83,7 @@ Spree::Admin::ProductsController.class_eval do end def collection_actions - [:index, :bulk_edit, :bulk_update] + [:index, :bulk_update] end @@ -95,6 +92,23 @@ Spree::Admin::ProductsController.class_eval do def load_form_data @producers = OpenFoodNetwork::Permissions.new(spree_current_user).managed_product_enterprises.is_primary_producer.by_name @taxons = Spree::Taxon.order(:name) + @import_dates = product_import_dates.uniq.to_json + end + + def product_import_dates + import_dates = Spree::Variant. + select('DISTINCT spree_variants.import_date'). + joins(:product). + where('spree_products.supplier_id IN (?)', editable_enterprises.collect(&:id)). + where('spree_variants.import_date IS NOT NULL'). + where(spree_variants: {is_master: false}). + where(spree_variants: {deleted_at: nil}). + order('spree_variants.import_date DESC') + + options = [{id: '0', name: ''}] + import_dates.collect(&:import_date).map { |i| options.push(id: i.to_date, name: i.to_date.to_formatted_s(:long)) } + + options end def strip_new_properties diff --git a/app/controllers/spree/admin/reports_controller_decorator.rb b/app/controllers/spree/admin/reports_controller_decorator.rb index 6c16031c0b..657c6b6bb8 100644 --- a/app/controllers/spree/admin/reports_controller_decorator.rb +++ b/app/controllers/spree/admin/reports_controller_decorator.rb @@ -15,9 +15,11 @@ require 'open_food_network/payments_report' require 'open_food_network/orders_and_fulfillments_report' Spree::Admin::ReportsController.class_eval do - include Spree::ReportsHelper + helper_method :render_content? + + before_filter :cache_search_state # Fetches user's distributors, suppliers and order_cycles before_filter :load_data, only: [:customers, :products_and_inventory, :order_cycle_management, :packing] @@ -53,22 +55,21 @@ Spree::Admin::ReportsController.class_eval do } end - # Overide spree reports list. + # Override spree reports list. def index @reports = authorized_reports respond_with(@reports) end - # This action is short because we refactored it like bosses def customers @report_types = report_types[:customers] @report_type = params[:report_type] - @report = OpenFoodNetwork::CustomersReport.new spree_current_user, params + @report = OpenFoodNetwork::CustomersReport.new spree_current_user, params, render_content? render_report(@report.header, @report.table, params[:csv], "customers_#{timestamp}.csv") end def order_cycle_management - prepare_date_params params + params[:q] ||= {} # -- Prepare form options my_distributors = Enterprise.is_distributor.managed_by(spree_current_user) @@ -84,15 +85,14 @@ Spree::Admin::ReportsController.class_eval do @report_type = params[:report_type] # -- Build Report with Order Grouper - @report = OpenFoodNetwork::OrderCycleManagementReport.new spree_current_user, params + @report = OpenFoodNetwork::OrderCycleManagementReport.new spree_current_user, params, render_content? @table = @report.table_items render_report(@report.header, @table, params[:csv], "order_cycle_management_#{timestamp}.csv") end def packing - # -- Prepare date parameters - prepare_date_params params + params[:q] ||= {} # -- Prepare form options my_distributors = Enterprise.is_distributor.managed_by(spree_current_user) @@ -107,7 +107,7 @@ Spree::Admin::ReportsController.class_eval do @report_type = params[:report_type] # -- Build Report with Order Grouper - @report = OpenFoodNetwork::PackingReport.new spree_current_user, params + @report = OpenFoodNetwork::PackingReport.new spree_current_user, params, render_content? order_grouper = OpenFoodNetwork::OrderGrouper.new @report.rules, @report.columns @table = order_grouper.table(@report.table_items) @@ -115,64 +115,26 @@ Spree::Admin::ReportsController.class_eval do end def orders_and_distributors - prepare_date_params params - - permissions = OpenFoodNetwork::Permissions.new(spree_current_user) - @search = permissions.visible_orders.complete.not_state(:canceled).search(params[:q]) - orders = @search.result - - # If empty array is passed in, the where clause will return all line_items, which is bad - orders_with_hidden_details = - permissions.editable_orders.empty? ? orders : orders.where('id NOT IN (?)', permissions.editable_orders) - - orders.select{ |order| orders_with_hidden_details.include? order }.each do |order| - # TODO We should really be hiding customer code here too, but until we - # have an actual association between order and customer, it's a bit tricky - order.bill_address.andand.assign_attributes(firstname: I18n.t('admin.reports.hidden'), lastname: "", phone: "", address1: "", address2: "", city: "", zipcode: "", state: nil) - order.ship_address.andand.assign_attributes(firstname: I18n.t('admin.reports.hidden'), lastname: "", phone: "", address1: "", address2: "", city: "", zipcode: "", state: nil) - order.assign_attributes(email: I18n.t('admin.reports.hidden')) - end - - @report = OpenFoodNetwork::OrderAndDistributorReport.new orders - unless params[:csv] - render :html => @report - else - csv_string = CSV.generate do |csv| - csv << @report.header - @report.table.each { |row| csv << row } - end - send_data csv_string, :filename => "orders_and_distributors_#{timestamp}.csv" - end + @report = OpenFoodNetwork::OrderAndDistributorReport.new spree_current_user, params, render_content? + @search = @report.search + csv_file_name = "orders_and_distributors_#{timestamp}.csv" + render_report(@report.header, @report.table, params[:csv], csv_file_name) end def sales_tax - prepare_date_params params @distributors = Enterprise.is_distributor.managed_by(spree_current_user) @report_type = params[:report_type] - - @report = OpenFoodNetwork::SalesTaxReport.new spree_current_user, params - - unless params[:csv] - render :html => @report - else - csv_string = CSV.generate do |csv| - csv << @report.header - @report.table.each { |row| csv << row } - end - send_data csv_string, :filename => "sales_tax.csv" - end + @report = OpenFoodNetwork::SalesTaxReport.new spree_current_user, params, render_content? + render_report(@report.header, @report.table, params[:csv], "sales_tax.csv") end def bulk_coop - # -- Prepare date parameters - prepare_date_params params - # -- Prepare form options @distributors = Enterprise.is_distributor.managed_by(spree_current_user) @report_type = params[:report_type] # -- Build Report with Order Grouper - @report = OpenFoodNetwork::BulkCoopReport.new spree_current_user, params + @report = OpenFoodNetwork::BulkCoopReport.new spree_current_user, params, render_content? order_grouper = OpenFoodNetwork::OrderGrouper.new @report.rules, @report.columns @table = order_grouper.table(@report.table_items) csv_file_name = "bulk_coop_#{params[:report_type]}_#{timestamp}.csv" @@ -181,15 +143,12 @@ Spree::Admin::ReportsController.class_eval do end def payments - # -- Prepare Date Params - prepare_date_params params - # -- Prepare Form Options @distributors = Enterprise.is_distributor.managed_by(spree_current_user) @report_type = params[:report_type] # -- Build Report with Order Grouper - @report = OpenFoodNetwork::PaymentsReport.new spree_current_user, params + @report = OpenFoodNetwork::PaymentsReport.new spree_current_user, params, render_content? order_grouper = OpenFoodNetwork::OrderGrouper.new @report.rules, @report.columns @table = order_grouper.table(@report.table_items) csv_file_name = "payments_#{timestamp}.csv" @@ -198,8 +157,7 @@ Spree::Admin::ReportsController.class_eval do end def orders_and_fulfillment - # -- Prepare Date Params - prepare_date_params params + params[:q] ||= {} # -- Prepare Form Options permissions = OpenFoodNetwork::Permissions.new(spree_current_user) @@ -216,7 +174,7 @@ Spree::Admin::ReportsController.class_eval do @include_blank = I18n.t(:all) # -- Build Report with Order Grouper - @report = OpenFoodNetwork::OrdersAndFulfillmentsReport.new spree_current_user, params + @report = OpenFoodNetwork::OrdersAndFulfillmentsReport.new spree_current_user, params, render_content? order_grouper = OpenFoodNetwork::OrderGrouper.new @report.rules, @report.columns @table = order_grouper.table(@report.table_items) csv_file_name = "#{params[:report_type]}_#{timestamp}.csv" @@ -226,60 +184,73 @@ Spree::Admin::ReportsController.class_eval do def products_and_inventory @report_types = report_types[:products_and_inventory] - if params[:report_type] != 'lettuce_share' - @report = OpenFoodNetwork::ProductsAndInventoryReport.new spree_current_user, params - else - @report = OpenFoodNetwork::LettuceShareReport.new spree_current_user, params - end + @report = if params[:report_type] != 'lettuce_share' + OpenFoodNetwork::ProductsAndInventoryReport.new spree_current_user, params, render_content? + else + OpenFoodNetwork::LettuceShareReport.new spree_current_user, params, render_content? + end render_report(@report.header, @report.table, params[:csv], "products_and_inventory_#{timestamp}.csv") end def users_and_enterprises - # @report_types = report_types[:users_and_enterprises] - @report = OpenFoodNetwork::UsersAndEnterprisesReport.new params + @report = OpenFoodNetwork::UsersAndEnterprisesReport.new params, render_content? render_report(@report.header, @report.table, params[:csv], "users_and_enterprises_#{timestamp}.csv") end def xero_invoices - if request.get? - params[:q] ||= {} - params[:q][:completed_at_gt] = Time.zone.today.beginning_of_month - params[:invoice_date] = Time.zone.today - params[:due_date] = Time.zone.today + 1.month - end + params[:q] ||= {} + @distributors = Enterprise.is_distributor.managed_by(spree_current_user) @order_cycles = OrderCycle.active_or_complete.accessible_by(spree_current_user).order('orders_close_at DESC') - @report = OpenFoodNetwork::XeroInvoicesReport.new spree_current_user, params + @report = OpenFoodNetwork::XeroInvoicesReport.new spree_current_user, params, render_content? render_report(@report.header, @report.table, params[:csv], "xero_invoices_#{timestamp}.csv") end - def render_report(header, table, create_csv, csv_file_name) - unless create_csv - render :html => table - else - csv_string = CSV.generate do |csv| - csv << header - table.each { |row| csv << row } - end - send_data csv_string, :filename => csv_file_name - end - end - private - def prepare_date_params(params) - # -- Prepare parameters - params[:q] ||= {} - if params[:q][:completed_at_gt].blank? - params[:q][:completed_at_gt] = Time.zone.now.beginning_of_month - else - params[:q][:completed_at_gt] = Time.zone.parse(params[:q][:completed_at_gt]) rescue Time.zone.now.beginning_of_month + # Some actions are changing the `params` object. That is unfortunate Spree + # behavior and we are building on it. So we have to look at `params` early + # to check if we are searching or just displaying a report search form. + def cache_search_state + search_keys = [ + # search parameter for ransack + :q, + # common in all reports, only set for CSV rendering + :csv, + # `button` is included in all forms. It's not important for searching, + # but the Users & Enterprises report doesn't have any other parameter + # for an empty search. So we use this one to display data. + :button, + # Some reports use filtering by enterprise or order cycle + :distributor_id, + :supplier_id, + :order_cycle_id, + # Xero Invoices can be filtered by date + :invoice_date, + :due_date + ] + @searching = search_keys.any? { |key| params.key? key } + end + + # We don't want to render data unless search params are supplied. + # Compiling data can take a long time. + def render_content? + @searching + end + + def render_report(header, table, create_csv, csv_file_name) + send_data csv_report(header, table), filename: csv_file_name if create_csv + @header = header + @table = table + # Rendering HTML is the default. + end + + def csv_report(header, table) + CSV.generate do |csv| + csv << header + table.each { |row| csv << row } end - if params[:q] && !params[:q][:completed_at_lt].blank? - params[:q][:completed_at_lt] = Time.zone.parse(params[:q][:completed_at_lt]) rescue "" - end - params[:q][:meta_sort] ||= "completed_at.desc" end def load_data @@ -289,42 +260,42 @@ Spree::Admin::ReportsController.class_eval do distributors_of_my_products = Enterprise.with_distributed_products_outer.merge(Spree::Product.in_any_supplier(my_suppliers)) @distributors = my_distributors | distributors_of_my_products # Load suppliers either owned by the user or supplying products their enterprises distribute. - suppliers_of_products_I_distribute = my_distributors.map { |d| Spree::Product.in_distributor(d) }.flatten.map(&:supplier).uniq - @suppliers = my_suppliers | suppliers_of_products_I_distribute + suppliers_of_products_i_distribute = my_distributors.map { |d| Spree::Product.in_distributor(d) }.flatten.map(&:supplier).uniq + @suppliers = my_suppliers | suppliers_of_products_i_distribute @order_cycles = OrderCycle.active_or_complete.accessible_by(spree_current_user).order('orders_close_at DESC') end def authorized_reports - reports = { - :orders_and_distributors => {:name => I18n.t('admin.reports.orders_and_distributors.name'), :description => I18n.t('admin.reports.orders_and_distributors.description')}, - :bulk_coop => {:name => I18n.t('admin.reports.bulk_coop.name'), :description => I18n.t('admin.reports.bulk_coop.description')}, - :payments => {:name => I18n.t('admin.reports.payments.name'), :description => I18n.t('admin.reports.payments.description')}, - :orders_and_fulfillment => {:name => I18n.t('admin.reports.orders_and_fulfillment.name'), :description => ''}, - :customers => {:name => I18n.t('admin.reports.customers.name'), :description => ''}, - :products_and_inventory => {:name => I18n.t('admin.reports.products_and_inventory.name'), :description => ''}, - :sales_total => {:name => I18n.t('admin.reports.sales_total.name'), :description => I18n.t('admin.reports.sales_total.description')}, - :users_and_enterprises => {:name => I18n.t('admin.reports.users_and_enterprises.name'), :description => I18n.t('admin.reports.users_and_enterprises.description')}, - :order_cycle_management => {:name => I18n.t('admin.reports.order_cycle_management.name'), :description => ''}, - :sales_tax => {:name => I18n.t('admin.reports.sales_tax.name'), :description => ''}, - :xero_invoices => {:name => I18n.t('admin.reports.xero_invoices.name'), :description => I18n.t('admin.reports.xero_invoices.description')}, - :packing => {:name => I18n.t('admin.reports.packing.name'), :description => ''} - } + all_reports = [ + :orders_and_distributors, + :bulk_coop, + :payments, + :orders_and_fulfillment, + :customers, + :products_and_inventory, + :sales_total, + :users_and_enterprises, + :order_cycle_management, + :sales_tax, + :xero_invoices, + :packing + ] + reports = all_reports.select { |action| can? action, :report } + reports.map { |report| [report, describe_report(report)] }.to_h + end - reports[:orders_and_fulfillment][:description] = - render_to_string(partial: 'orders_and_fulfillment_description', layout: false, locals: {report_types: report_types[:orders_and_fulfillment]}).html_safe - reports[:products_and_inventory][:description] = - render_to_string(partial: 'products_and_inventory_description', layout: false, locals: {report_types: report_types[:products_and_inventory]}).html_safe - reports[:customers][:description] = - render_to_string(partial: 'customers_description', layout: false, locals: {report_types: report_types[:customers]}).html_safe - reports[:order_cycle_management][:description] = - render_to_string(partial: 'order_cycle_management_description', layout: false, locals: {report_types: report_types[:order_cycle_management]}).html_safe - reports[:packing][:description] = - render_to_string(partial: 'packing_description', layout: false, locals: {report_types: report_types[:packing]}).html_safe - reports[:sales_tax][:description] = - render_to_string(partial: 'sales_tax_description', layout: false, locals: {report_types: report_types[:sales_tax]}).html_safe - - # Return only reports the user is authorized to view. - reports.select { |action| can? action, :report } + def describe_report(report) + name = I18n.t(:name, scope: [:admin, :reports, report]) + description = begin + I18n.t!(:description, scope: [:admin, :reports, report]) + rescue I18n::MissingTranslationData + render_to_string( + partial: "#{report}_description", + layout: false, + locals: { report_types: report_types[report] } + ).html_safe + end + { name: name, description: description } end def timestamp diff --git a/app/controllers/spree/credit_cards_controller.rb b/app/controllers/spree/credit_cards_controller.rb index 4582931dd0..9dd129bacf 100644 --- a/app/controllers/spree/credit_cards_controller.rb +++ b/app/controllers/spree/credit_cards_controller.rb @@ -16,6 +16,18 @@ module Spree return render json: { flash: { error: I18n.t(:spree_gateway_error_flash_for_checkout, error: e.message) } }, status: 400 end + def update + @credit_card = Spree::CreditCard.find_by_id(params[:id]) + return update_failed unless @credit_card + authorize! :update, @credit_card + + if @credit_card.update_attributes(params[:credit_card]) + render json: @credit_card, serializer: ::Api::CreditCardSerializer, status: :ok + else + update_failed + end + end + def destroy @credit_card = Spree::CreditCard.find_by_id(params[:id]) if @credit_card @@ -65,5 +77,9 @@ module Spree card.user_id = spree_current_user.id card end + + def update_failed + render json: { flash: { error: t(:card_could_not_be_updated) } }, status: 400 + end end end diff --git a/app/controllers/spree/users_controller_decorator.rb b/app/controllers/spree/users_controller_decorator.rb index aca371c75b..53cfd09324 100644 --- a/app/controllers/spree/users_controller_decorator.rb +++ b/app/controllers/spree/users_controller_decorator.rb @@ -15,4 +15,10 @@ Spree::UsersController.class_eval do @orders = @orders.where('distributor_id != ?', Spree::Config.accounts_distributor_id) end + + # Endpoint for queries to check if a user is already registered + def registered_email + user = Spree.user_class.find_by_email params[:email] + render json: { registered: user.present? } + end end diff --git a/app/controllers/user_confirmations_controller.rb b/app/controllers/user_confirmations_controller.rb index 8e3e504eaa..e564da0b64 100644 --- a/app/controllers/user_confirmations_controller.rb +++ b/app/controllers/user_confirmations_controller.rb @@ -8,6 +8,7 @@ class UserConfirmationsController < DeviseController # POST /resource/confirmation def create + set_return_url if params.key? :return_url self.resource = resource_class.send_confirmation_instructions(resource_params) if is_navigational_format? @@ -30,6 +31,10 @@ class UserConfirmationsController < DeviseController protected + def set_return_url + session[:confirmation_return_url] = params[:return_url] + end + def after_confirmation_path_for(resource) result = if resource.errors.empty? diff --git a/app/controllers/user_passwords_controller.rb b/app/controllers/user_passwords_controller.rb index 1870cc8859..e6b7610a13 100644 --- a/app/controllers/user_passwords_controller.rb +++ b/app/controllers/user_passwords_controller.rb @@ -4,6 +4,8 @@ class UserPasswordsController < Spree::UserPasswordsController before_filter :set_admin_redirect, only: :edit def create + render_unconfirmed_response && return if user_unconfirmed? + self.resource = resource_class.send_reset_password_instructions(params[resource_name]) if resource.errors.empty? @@ -15,7 +17,7 @@ class UserPasswordsController < Spree::UserPasswordsController respond_with_navigational(resource) { render :new } end format.js do - render json: resource.errors, status: :unauthorized + render json: { error: t('email_not_found') }, status: :not_found end end end @@ -26,4 +28,13 @@ class UserPasswordsController < Spree::UserPasswordsController def set_admin_redirect session["spree_user_return_to"] = params[:return_to] if params[:return_to] end + + def render_unconfirmed_response + render json: { error: t('email_unconfirmed') }, status: :unauthorized + end + + def user_unconfirmed? + user = Spree::User.find_by_email(params[:spree_user][:email]) + user && !user.confirmed? + end end diff --git a/app/helpers/checkout_helper.rb b/app/helpers/checkout_helper.rb index a874d996d9..bc885bd5dc 100644 --- a/app/helpers/checkout_helper.rb +++ b/app/helpers/checkout_helper.rb @@ -66,20 +66,6 @@ module CheckoutHelper Spree::Money.new order.total - order.total_tax, currency: order.currency end - def checkout_state_options(source_address) - if source_address == :billing - address = @order.billing_address - elsif source_address == :shipping - address = @order.shipping_address - end - - [[]] + address.country.states.map { |c| [c.name, c.id] } - end - - def checkout_country_options - available_countries.map { |c| [c.name, c.id] } - end - def validated_input(name, path, args = {}) attributes = { required: true, diff --git a/app/helpers/injection_helper.rb b/app/helpers/injection_helper.rb index 5b6186ff3e..ea5589abe6 100644 --- a/app/helpers/injection_helper.rb +++ b/app/helpers/injection_helper.rb @@ -74,9 +74,11 @@ module InjectionHelper end def inject_saved_credit_cards - if spree_current_user - data = spree_current_user.credit_cards.with_payment_profile.all - end + data = if spree_current_user + spree_current_user.credit_cards.with_payment_profile.all + else + [] + end inject_json_ams "savedCreditCards", data, Api::CreditCardSerializer end diff --git a/app/helpers/shop_helper.rb b/app/helpers/shop_helper.rb index 920d41d65e..0ca473e44f 100644 --- a/app/helpers/shop_helper.rb +++ b/app/helpers/shop_helper.rb @@ -19,4 +19,13 @@ module ShopHelper spree_current_user.customer_of(current_distributor) ) end + + def shop_tabs + [ + { name: 'about', title: t(:shopping_tabs_about, distributor: current_distributor.name), cols: 6 }, + { name: 'producers', title: t(:label_producers), cols: 2 }, + { name: 'contact', title: t(:shopping_tabs_contact), cols: 2 }, + { name: 'groups', title: t(:label_groups), cols: 2 }, + ] + end end diff --git a/app/jobs/subscription_confirm_job.rb b/app/jobs/subscription_confirm_job.rb index f2788288ff..d9c284d7f8 100644 --- a/app/jobs/subscription_confirm_job.rb +++ b/app/jobs/subscription_confirm_job.rb @@ -25,7 +25,6 @@ class SubscriptionConfirmJob def proxy_orders ProxyOrder.not_canceled.where('confirmed_at IS NULL AND placed_at IS NOT NULL') .joins(:order_cycle).merge(recently_closed_order_cycles) - .joins(:subscription).merge(Subscription.not_canceled.not_paused) .joins(:order).merge(Spree::Order.complete) end diff --git a/app/jobs/subscription_placement_job.rb b/app/jobs/subscription_placement_job.rb index 91d395157f..348420c0f8 100644 --- a/app/jobs/subscription_placement_job.rb +++ b/app/jobs/subscription_placement_job.rb @@ -34,6 +34,8 @@ class SubscriptionPlacementJob changes = cap_quantity_and_store_changes(order) if order.line_items.where('quantity > 0').empty? + order.reload.adjustments.destroy_all + order.update! return send_empty_email(order, changes) end diff --git a/app/models/enterprise.rb b/app/models/enterprise.rb index 82618e4e2e..97855812ef 100644 --- a/app/models/enterprise.rb +++ b/app/models/enterprise.rb @@ -203,14 +203,6 @@ class Enterprise < ActiveRecord::Base self.supplied_products.where('count_on_hand > 0').present? end - def supplied_and_active_products_on_hand - self.supplied_products.where('spree_products.count_on_hand > 0').active - end - - def active_products_in_order_cycles - self.supplied_and_active_products_on_hand.in_an_active_order_cycle - end - def to_param permalink end @@ -382,7 +374,7 @@ class Enterprise < ActiveRecord::Base end def ensure_owner_is_manager - users << owner unless users.include?(owner) || owner.admin? + users << owner unless users.include?(owner) end def enforce_ownership_limit diff --git a/app/models/feature_flags.rb b/app/models/feature_flags.rb new file mode 100644 index 0000000000..aaba0f4472 --- /dev/null +++ b/app/models/feature_flags.rb @@ -0,0 +1,28 @@ +# Tells whether a particular feature is enabled or not +class FeatureFlags + # Constructor + # + # @param user [User] + def initialize(user) + @user = user + end + + # Checks whether product import is enabled for the specified user + # + # @return [Boolean] + def product_import_enabled? + superadmin? + end + + private + + attr_reader :user + + # Checks whether the specified user is a superadmin, with full control of the + # instance + # + # @return [Boolean] + def superadmin? + user.has_spree_role?('admin') + end +end diff --git a/app/models/product_import/entry_processor.rb b/app/models/product_import/entry_processor.rb new file mode 100644 index 0000000000..509a21fd43 --- /dev/null +++ b/app/models/product_import/entry_processor.rb @@ -0,0 +1,234 @@ +module ProductImport + class EntryProcessor + attr_reader :inventory_created, :inventory_updated, :products_created, :variants_created, :variants_updated, :products_reset_count, :supplier_products, :total_supplier_products + + def initialize(importer, validator, import_settings, spreadsheet_data, editable_enterprises, import_time, updated_ids) + @importer = importer + @validator = validator + @import_settings = import_settings + @spreadsheet_data = spreadsheet_data + @editable_enterprises = editable_enterprises + @import_time = import_time + @updated_ids = updated_ids + + @inventory_created = 0 + @inventory_updated = 0 + @products_created = 0 + @variants_created = 0 + @variants_updated = 0 + @products_reset_count = 0 + @supplier_products = {} + @total_supplier_products = 0 + end + + def save_all(entries) + entries.each do |entry| + if import_into_inventory?(entry) + save_to_inventory(entry) + else + save_to_product_list(entry) + end + end + + @importer.errors.add(:importer, I18n.t(:product_importer_products_save_error)) if total_saved_count.zero? + end + + def count_existing_items + @spreadsheet_data.suppliers_index.each do |_supplier_name, supplier_id| + next unless supplier_id && permission_by_id?(supplier_id) + + products_count = + if import_into_inventory_by_supplier?(supplier_id) + VariantOverride.where('variant_overrides.hub_id IN (?)', supplier_id).count + else + Spree::Variant. + not_deleted. + not_master. + joins(:product). + where('spree_products.supplier_id IN (?)', supplier_id). + count + end + + @supplier_products[supplier_id] = products_count + @total_supplier_products += products_count + end + end + + def reset_absent_items + # For selected enterprises; set stock to zero for all products/inventory + # that were not listed in the newly uploaded spreadsheet + return if total_saved_count.zero? || @updated_ids.empty? || !@import_settings.key?(:settings) + suppliers_to_reset_products = [] + suppliers_to_reset_inventories = [] + + @import_settings[:settings].each do |enterprise_id, settings| + suppliers_to_reset_products.push enterprise_id if settings['reset_all_absent'] && permission_by_id?(enterprise_id) && !import_into_inventory_by_supplier?(enterprise_id) + suppliers_to_reset_inventories.push enterprise_id if settings['reset_all_absent'] && permission_by_id?(enterprise_id) && import_into_inventory_by_supplier?(enterprise_id) + end + + unless suppliers_to_reset_inventories.empty? + @products_reset_count += VariantOverride. + where('variant_overrides.hub_id IN (?) + AND variant_overrides.id NOT IN (?)', suppliers_to_reset_inventories, @updated_ids). + update_all(count_on_hand: 0) + end + + return if suppliers_to_reset_products.empty? + + @products_reset_count += Spree::Variant.joins(:product). + where('spree_products.supplier_id IN (?) + AND spree_variants.id NOT IN (?) + AND spree_variants.is_master = false + AND spree_variants.deleted_at IS NULL', suppliers_to_reset_products, @updated_ids). + update_all(count_on_hand: 0) + end + + def total_saved_count + @products_created + @variants_created + @variants_updated + @inventory_created + @inventory_updated + end + + private + + def save_to_inventory(entry) + save_new_inventory_item entry if entry.validates_as? 'new_inventory_item' + save_existing_inventory_item entry if entry.validates_as? 'existing_inventory_item' + end + + def save_to_product_list(entry) + save_new_product entry if entry.validates_as? 'new_product' + + if entry.validates_as? 'new_variant' + save_variant entry + @variants_created += 1 + end + + return unless entry.validates_as? 'existing_variant' + + save_variant entry + @variants_updated += 1 + end + + def import_into_inventory?(entry) + entry.supplier_id && @import_settings[:settings][entry.supplier_id.to_s]['import_into'] == 'inventories' + end + + def save_new_inventory_item(entry) + new_item = entry.product_object + assign_defaults(new_item, entry) + new_item.import_date = @import_time + + if new_item.valid? && new_item.save + display_in_inventory(new_item, true) + @inventory_created += 1 + @updated_ids.push new_item.id + else + @importer.errors.add("#{I18n.t('admin.product_import.model.line')} #{line_number}:", new_item.errors.full_messages) + end + end + + def save_existing_inventory_item(entry) + existing_item = entry.product_object + assign_defaults(existing_item, entry) + existing_item.import_date = @import_time + + if existing_item.valid? && existing_item.save + display_in_inventory(existing_item) + @inventory_updated += 1 + @updated_ids.push existing_item.id + else + @importer.errors.add("#{I18n.t('admin.product_import.model.line')} #{line_number}:", existing_item.errors.full_messages) + end + end + + def save_new_product(entry) + @already_created ||= {} + # If we've already added a new product with these attributes + # from this spreadsheet, mark this entry as a new variant with + # the new product id, as this is a now variant of that product... + if @already_created[entry.supplier_id] && @already_created[entry.supplier_id][entry.name] + product_id = @already_created[entry.supplier_id][entry.name] + @validator.mark_as_new_variant(entry, product_id) + return + end + + product = Spree::Product.new + product.assign_attributes(entry.attributes.except('id')) + assign_defaults(product, entry) + + if product.save + ensure_variant_updated(product, entry) + @products_created += 1 + @updated_ids.push product.variants.first.id + else + @importer.errors.add("#{I18n.t('admin.product_import.model.line')} #{line_number}:", product.errors.full_messages) + end + + @already_created[entry.supplier_id] = { entry.name => product.id } + end + + def save_variant(entry) + variant = entry.product_object + assign_defaults(variant, entry) + variant.import_date = @import_time + + if variant.valid? && variant.save + @updated_ids.push variant.id + true + else + @importer.errors.add("#{I18n.t('admin.product_import.model.line')} #{line_number}:", variant.errors.full_messages) + false + end + end + + def assign_defaults(object, entry) + # Assigns a default value for a specified field e.g. category='Vegetables', setting this value + # either for all entries (overwrite_all), or only for those entries where the field was blank + # in the spreadsheet (overwrite_empty), depending on selected import settings + return unless @import_settings.key?(:settings) && @import_settings[:settings][entry.supplier_id.to_s] && @import_settings[:settings][entry.supplier_id.to_s]['defaults'] + + @import_settings[:settings][entry.supplier_id.to_s]['defaults'].each do |attribute, setting| + next unless setting['active'] + + case setting['mode'] + when 'overwrite_all' + object.assign_attributes(attribute => setting['value']) + when 'overwrite_empty' + if object.send(attribute).blank? || ((attribute == 'on_hand' || attribute == 'count_on_hand') && entry.on_hand_nil) + object.assign_attributes(attribute => setting['value']) + end + end + end + end + + def display_in_inventory(variant_override, is_new = false) + unless is_new + existing_item = InventoryItem.where(variant_id: variant_override.variant_id, enterprise_id: variant_override.hub_id).first + + if existing_item + existing_item.assign_attributes(visible: true) + existing_item.save + return + end + end + + InventoryItem.new(variant_id: variant_override.variant_id, enterprise_id: variant_override.hub_id, visible: true).save + end + + def ensure_variant_updated(product, entry) + # Ensure attributes are correctly copied to a new product's variant + variant = product.variants.first + variant.display_name = entry.display_name if entry.display_name + variant.on_demand = entry.on_demand if entry.on_demand + variant.import_date = @import_time + variant.save + end + + def permission_by_id?(supplier_id) + @editable_enterprises.value?(Integer(supplier_id)) + end + + def import_into_inventory_by_supplier?(supplier_id) + @import_settings[:settings] && @import_settings[:settings][supplier_id.to_s] && @import_settings[:settings][supplier_id.to_s]['import_into'] == 'inventories' + end + end +end diff --git a/app/models/product_import/entry_validator.rb b/app/models/product_import/entry_validator.rb new file mode 100644 index 0000000000..8ffb2edf2c --- /dev/null +++ b/app/models/product_import/entry_validator.rb @@ -0,0 +1,273 @@ +module ProductImport + class EntryValidator + def initialize(current_user, import_time, spreadsheet_data, editable_enterprises, inventory_permissions, reset_counts, import_settings) + @current_user = current_user + @import_time = import_time + @spreadsheet_data = spreadsheet_data + @editable_enterprises = editable_enterprises + @inventory_permissions = inventory_permissions + @reset_counts = reset_counts + @import_settings = import_settings + end + + def validate_all(entries) + entries.each do |entry| + supplier_validation(entry) + unit_fields_validation(entry) + + next if entry.supplier_id.blank? + + if import_into_inventory?(entry) + producer_validation(entry) + inventory_validation(entry) + else + category_validation(entry) + tax_and_shipping_validation(entry, 'tax', entry.tax_category, @spreadsheet_data.tax_index) + tax_and_shipping_validation(entry, 'shipping', entry.shipping_category, @spreadsheet_data.shipping_index) + product_validation(entry) + end + end + end + + def mark_as_new_variant(entry, product_id) + new_variant = Spree::Variant.new(entry.attributes.except('id', 'product_id')) + new_variant.product_id = product_id + check_on_hand_nil(entry, new_variant) + + if new_variant.valid? + entry.product_object = new_variant + entry.validates_as = 'new_variant' unless entry.errors? + else + mark_as_invalid(entry, product_validations: new_variant.errors) + end + end + + private + + def supplier_validation(entry) + supplier_name = entry.supplier + + if supplier_name.blank? + mark_as_invalid(entry, attribute: "supplier", error: I18n.t(:error_required)) + return + end + + unless @spreadsheet_data.suppliers_index[supplier_name] + mark_as_invalid(entry, attribute: "supplier", error: I18n.t(:error_not_found_in_database, name: supplier_name)) + return + end + + unless permission_by_name?(supplier_name) + mark_as_invalid(entry, attribute: "supplier", error: I18n.t(:error_no_permission_for_enterprise, name: supplier_name)) + return + end + + entry.supplier_id = @spreadsheet_data.suppliers_index[supplier_name] + end + + def unit_fields_validation(entry) + unit_types = ['g', 'kg', 't', 'ml', 'l', 'kl', ''] + + unless entry.units && entry.units.present? + mark_as_invalid(entry, attribute: 'units', error: I18n.t('admin.product_import.model.blank')) + end + + return if import_into_inventory?(entry) + + # unit_type must be valid type + if entry.unit_type && entry.unit_type.present? + unit_type = entry.unit_type.to_s.strip.downcase + mark_as_invalid(entry, attribute: 'unit_type', error: I18n.t('admin.product_import.model.incorrect_value')) unless unit_types.include?(unit_type) + return + end + + # variant_unit_name must be present if unit_type not present + mark_as_invalid(entry, attribute: 'variant_unit_name', error: I18n.t('admin.product_import.model.conditional_blank')) unless entry.variant_unit_name && entry.variant_unit_name.present? + end + + def producer_validation(entry) + producer_name = entry.producer + + if producer_name.blank? + mark_as_invalid(entry, attribute: "producer", error: I18n.t('admin.product_import.model.blank')) + return + end + + unless @spreadsheet_data.producers_index[producer_name] + mark_as_invalid(entry, attribute: "producer", error: "\"#{producer_name}\" #{I18n.t('admin.product_import.model.not_found')}") + return + end + + unless inventory_permission?(entry.supplier_id, @spreadsheet_data.producers_index[producer_name]) + mark_as_invalid(entry, attribute: "producer", error: "\"#{producer_name}\": #{I18n.t('admin.product_import.model.inventory_no_permission')}") + return + end + + entry.producer_id = @spreadsheet_data.producers_index[producer_name] + end + + def inventory_validation(entry) + # Checks a potential inventory item corresponds to a valid variant + match = Spree::Product.where(supplier_id: entry.producer_id, name: entry.name, deleted_at: nil).first + + if match.nil? + mark_as_invalid(entry, attribute: 'name', error: I18n.t('admin.product_import.model.no_product')) + return + end + + match.variants.each do |existing_variant| + unit_scale = match.variant_unit_scale + unscaled_units = entry.unscaled_units || 0 + entry.unit_value = unscaled_units * unit_scale + + if entry_matches_existing_variant?(entry, existing_variant) + variant_override = create_inventory_item(entry, existing_variant) + return validate_inventory_item(entry, variant_override) + end + end + + mark_as_invalid(entry, attribute: 'product', error: I18n.t('admin.product_import.model.not_found')) + end + + def entry_matches_existing_variant?(entry, existing_variant) + existing_variant.display_name == entry.display_name && existing_variant.unit_value == entry.unit_value.to_f + end + + def category_validation(entry) + category_name = entry.category + + if category_name.blank? + mark_as_invalid(entry, attribute: "category", error: I18n.t(:error_required)) + return + end + + if @spreadsheet_data.categories_index[category_name] + entry.primary_taxon_id = @spreadsheet_data.categories_index[category_name] + else + mark_as_invalid(entry, attribute: "category", error: I18n.t(:error_not_found_in_database, name: category_name)) + end + end + + def tax_and_shipping_validation(entry, type, category, index) + return if category.blank? + + if index.key? category + entry.send("#{type}_category_id=", index[category]) + else + mark_as_invalid(entry, attribute: "#{type}_category", error: I18n.t('admin.product_import.model.not_found')) + end + end + + def product_validation(entry) + # Find product with matching supplier and name + match = Spree::Product.where(supplier_id: entry.supplier_id, name: entry.name, deleted_at: nil).first + + # If no matching product was found, create a new product + if match.nil? + mark_as_new_product(entry) + return + end + + # Otherwise, if a variant exists with matching display_name and unit_value, update it + match.variants.each do |existing_variant| + if entry_matches_existing_variant?(entry, existing_variant) && existing_variant.deleted_at.nil? + return mark_as_existing_variant(entry, existing_variant) + end + end + + # Otherwise, a variant with sufficiently matching attributes doesn't exist; create a new one + mark_as_new_variant(entry, match.id) + end + + def mark_as_new_product(entry) + new_product = Spree::Product.new + new_product.assign_attributes(entry.attributes.except('id')) + + if new_product.valid? + entry.validates_as = 'new_product' unless entry.errors? + else + mark_as_invalid(entry, product_validations: new_product.errors) + end + end + + def mark_as_existing_variant(entry, existing_variant) + existing_variant.assign_attributes(entry.attributes.except('id', 'product_id')) + check_on_hand_nil(entry, existing_variant) + + if existing_variant.valid? + entry.product_object = existing_variant + entry.validates_as = 'existing_variant' unless entry.errors? + updates_count_per_supplier(entry.supplier_id) unless entry.errors? + else + mark_as_invalid(entry, product_validations: existing_variant.errors) + end + end + + def permission_by_name?(supplier_name) + @editable_enterprises.key?(supplier_name) + end + + def permission_by_id?(supplier_id) + @editable_enterprises.value?(Integer(supplier_id)) + end + + def inventory_permission?(supplier_id, producer_id) + @current_user.admin? || ( @inventory_permissions[supplier_id] && @inventory_permissions[supplier_id].include?(producer_id) ) + end + + def mark_as_invalid(entry, options = {}) + entry.errors.add(options[:attribute], options[:error]) if options[:attribute] && options[:error] + entry.product_validations = options[:product_validations] if options[:product_validations] + end + + def import_into_inventory?(entry) + entry.supplier_id && @import_settings[:settings][entry.supplier_id.to_s]['import_into'] == 'inventories' + end + + def validate_inventory_item(entry, variant_override) + if variant_override.valid? && !entry.errors? + mark_as_inventory_item(entry, variant_override) + else + mark_as_invalid(entry, product_validations: variant_override.errors) + end + end + + def create_inventory_item(entry, existing_variant) + existing_variant_override = VariantOverride.where(variant_id: existing_variant.id, hub_id: entry.supplier_id).first + + variant_override = existing_variant_override || VariantOverride.new(variant_id: existing_variant.id, hub_id: entry.supplier_id) + variant_override.assign_attributes(count_on_hand: entry.on_hand, import_date: @import_time) + check_on_hand_nil(entry, variant_override) + variant_override.assign_attributes(entry.attributes.slice('price', 'on_demand')) + + variant_override + end + + def mark_as_inventory_item(entry, variant_override) + if variant_override.id + entry.validates_as = 'existing_inventory_item' + entry.product_object = variant_override + updates_count_per_supplier(entry.supplier_id) unless entry.errors? + else + entry.validates_as = 'new_inventory_item' + entry.product_object = variant_override + end + end + + def updates_count_per_supplier(supplier_id) + if @reset_counts[supplier_id] && @reset_counts[supplier_id][:updates_count] + @reset_counts[supplier_id][:updates_count] += 1 + else + @reset_counts[supplier_id] = { updates_count: 1 } + end + end + + def check_on_hand_nil(entry, object) + return if entry.on_hand.present? + + object.on_hand = 0 if object.respond_to?(:on_hand) + object.count_on_hand = 0 if object.respond_to?(:count_on_hand) + entry.on_hand_nil = true + end + end +end diff --git a/app/models/product_import/product_importer.rb b/app/models/product_import/product_importer.rb new file mode 100644 index 0000000000..24618cfd05 --- /dev/null +++ b/app/models/product_import/product_importer.rb @@ -0,0 +1,252 @@ +require 'roo' + +module ProductImport + class ProductImporter + extend ActiveModel::Naming + include ActiveModel::Conversion + include ActiveModel::Validations + + attr_reader :updated_ids + + def initialize(file, current_user, import_settings = {}) + unless file.is_a?(File) + errors.add(:importer, I18n.t(:product_importer_file_error)) + return + end + + @file = file + @sheet = open_spreadsheet + @entries = [] + + @import_time = Time.zone.now + @import_settings = import_settings || {} + + @current_user = current_user + @editable_enterprises = {} + @inventory_permissions = {} + + @reset_counts = {} + @updated_ids = [] + + init_product_importer if @sheet + end + + def persisted? + false # ActiveModel + end + + def entries? + @entries.count > 0 + end + + def valid_entries? + @entries.each do |entry| + return true if entry.validates_as.present? + end + false + end + + def item_count + @sheet ? @sheet.last_row - 1 : 0 + end + + def reset_counts + # Return indexed data about existing product count, reset count, and updates count per supplier + @reset_counts.each do |supplier_id, values| + values[:updates_count] = 0 if values[:updates_count].blank? + + if values[:updates_count] && values[:existing_products] + @reset_counts[supplier_id][:reset_count] = values[:existing_products] - values[:updates_count] + end + end + @reset_counts + end + + def suppliers_index + index = @spreadsheet_data.suppliers_index + index.sort_by{ |_k, v| v.to_i }.reverse.to_h + end + + def supplier_products + @processor.supplier_products + end + + def total_supplier_products + @processor.total_supplier_products + end + + def all_entries + @entries + end + + def entries_json + entries = {} + @entries.each do |entry| + entries[entry.line_number] = { + attributes: entry.displayable_attributes, + validates_as: entry.validates_as, + errors: entry.invalid_attributes + } + end + entries.to_json + end + + def table_headings + @entries.first.displayable_attributes.keys.map(&:humanize) if @entries.first + end + + def products_created_count + @processor.products_created + @processor.variants_created + end + + def products_updated_count + @processor.variants_updated + end + + def inventory_created_count + @processor.inventory_created + end + + def inventory_updated_count + @processor.inventory_updated + end + + def products_reset_count + @processor.products_reset_count + end + + def total_saved_count + @processor.total_saved_count + end + + def import_results + { entries: entries_json, reset_counts: reset_counts } + end + + def save_results + { + results: { + products_created: products_created_count, + products_updated: products_updated_count, + inventory_created: inventory_created_count, + inventory_updated: inventory_updated_count, + products_reset: products_reset_count, + }, + updated_ids: updated_ids, + errors: errors.full_messages + } + end + + def validate_entries + @validator.validate_all(@entries) + end + + def save_entries + validate_entries + save_all_valid + end + + def reset_absent(updated_ids) + @products_created = updated_ids.count + @updated_ids = updated_ids + @processor.reset_absent_items + end + + def permission_by_id?(supplier_id) + @editable_enterprises.value?(Integer(supplier_id)) + end + + private + + def init_product_importer + init_permissions + + if staged_import? + build_entries_in_range + else + build_entries + end + + @spreadsheet_data = SpreadsheetData.new(@entries) + @validator = EntryValidator.new(@current_user, @import_time, @spreadsheet_data, @editable_enterprises, @inventory_permissions, @reset_counts, @import_settings) + @processor = EntryProcessor.new(self, @validator, @import_settings, @spreadsheet_data, @editable_enterprises, @import_time, @updated_ids) + + @processor.count_existing_items unless staged_import? + end + + def staged_import? + @import_settings && @import_settings.key?(:start) && @import_settings.key?(:end) + end + + def init_permissions + permissions = OpenFoodNetwork::Permissions.new(@current_user) + + permissions.editable_enterprises. + order('is_primary_producer ASC, name'). + map { |e| @editable_enterprises[e.name] = e.id } + + @inventory_permissions = permissions.variant_override_enterprises_per_hub + end + + def open_spreadsheet + if accepted_mimetype + Roo::Spreadsheet.open(@file, extension: accepted_mimetype) + else + errors.add(:importer, I18n.t(:product_importer_spreadsheet_error)) + delete_uploaded_file + nil + end + end + + def accepted_mimetype + File.extname(@file.path).in?('.csv', '.xls', '.xlsx', '.ods') ? @file.path.split('.').last.to_sym : false + end + + def headers + @sheet.row(1) + end + + def rows + return [] unless @sheet && @sheet.last_row + (2..@sheet.last_row).map do |i| + @sheet.row(i) + end + end + + def build_entries_in_range + start_line = @import_settings[:start] + end_line = @import_settings[:end] + + (start_line..end_line).each do |i| + line_number = i + 1 + row = @sheet.row(line_number) + row_data = Hash[[headers, row].transpose] + entry = SpreadsheetEntry.new(row_data) + entry.line_number = line_number + @entries.push entry + break if @sheet.last_row == line_number + end + end + + def build_entries + rows.each_with_index do |row, i| + row_data = Hash[[headers, row].transpose] + entry = SpreadsheetEntry.new(row_data) + entry.line_number = i + 2 + @entries.push entry + end + @entries + end + + def save_all_valid + @processor.save_all(@entries) + @processor.reset_absent_items unless staged_import? + @processor.total_saved_count + end + + def delete_uploaded_file + return unless @file.path == Rails.root.join('tmp', 'product_import').to_s + File.delete(@file) + end + end +end diff --git a/app/models/product_import/spreadsheet_data.rb b/app/models/product_import/spreadsheet_data.rb new file mode 100644 index 0000000000..303cf1a93b --- /dev/null +++ b/app/models/product_import/spreadsheet_data.rb @@ -0,0 +1,72 @@ +module ProductImport + class SpreadsheetData + def initialize(entries) + @entries = entries + end + + def suppliers_index + @suppliers_index || create_suppliers_index + end + + def producers_index + @producers_index = create_producers_index + end + + def categories_index + @categories_index || create_categories_index + end + + def tax_index + @tax_index || create_tax_index + end + + def shipping_index + @shipping_index || create_shipping_index + end + + private + + def create_suppliers_index + @suppliers_index = {} + @entries.each do |entry| + supplier_name = entry.supplier + supplier_id = @suppliers_index[supplier_name] || Enterprise.find_by_name(supplier_name, select: 'id, name').try(:id) + @suppliers_index[supplier_name] = supplier_id + end + @suppliers_index + end + + def create_producers_index + @producers_index = {} + @entries.each do |entry| + next unless entry.producer + producer_name = entry.producer + producer_id = @producers_index[producer_name] || Enterprise.find_by_name(producer_name, select: 'id, name').try(:id) + @producers_index[producer_name] = producer_id + end + @producers_index + end + + def create_categories_index + @categories_index = {} + @entries.each do |entry| + category_name = entry.category + category_id = @categories_index[category_name] || Spree::Taxon.find_by_name(category_name, select: 'id, name').try(:id) + @categories_index[category_name] = category_id + end + @categories_index + end + + def create_tax_index + @tax_index = {} + Spree::TaxCategory.select([:id, :name]).map { |tc| @tax_index[tc.name] = tc.id } + @tax_index + end + + def create_shipping_index + @shipping_index = {} + Spree::ShippingCategory.select([:id, :name]).map { |sc| @shipping_index[sc.name] = sc.id } + @shipping_index + end + end +end diff --git a/app/models/product_import/spreadsheet_entry.rb b/app/models/product_import/spreadsheet_entry.rb new file mode 100644 index 0000000000..b32c96a811 --- /dev/null +++ b/app/models/product_import/spreadsheet_entry.rb @@ -0,0 +1,78 @@ +module ProductImport + class SpreadsheetEntry + extend ActiveModel::Naming + include ActiveModel::Conversion + include ActiveModel::Validations + + attr_accessor :line_number, :valid, :validates_as, :product_object, :product_validations, :on_hand_nil, + :has_overrides, :units, :unscaled_units, :unit_type, :tax_category, :shipping_category + + attr_accessor :id, :product_id, :producer, :producer_id, :supplier, :supplier_id, :name, :display_name, :sku, + :unit_value, :unit_description, :variant_unit, :variant_unit_scale, :variant_unit_name, + :display_as, :category, :primary_taxon_id, :price, :on_hand, :count_on_hand, :on_demand, + :tax_category_id, :shipping_category_id, :description, :import_date + + def initialize(attrs) + @validates_as = '' + assign_units attrs + end + + def persisted? + false # ActiveModel + end + + def validates_as?(type) + @validates_as == type + end + + def errors? + errors.count > 0 || @product_validations + end + + def attributes + attrs = {} + instance_variables.each do |var| + attrs[var.to_s.delete("@")] = instance_variable_get(var) + end + attrs.except(*non_product_attributes) + end + + def displayable_attributes + # Modified attributes list for displaying in user feedback + attrs = {} + instance_variables.each do |var| + attrs[var.to_s.delete("@")] = instance_variable_get(var) + end + attrs.except(*non_product_attributes, *non_display_attributes) + end + + def invalid_attributes + invalid_attrs = {} + errors = @product_validations ? self.errors.messages.merge(@product_validations.messages) : self.errors.messages + errors.each do |attr, message| + invalid_attrs[attr.to_s] = "#{attr.to_s.capitalize} #{message.first}" + end + invalid_attrs.except(*non_product_attributes, *non_display_attributes) + end + + private + + def assign_units(attrs) + units = UnitConverter.new(attrs) + + units.converted_attributes.each do |attr, value| + if respond_to?("#{attr}=") + send("#{attr}=", value) unless non_product_attributes.include?(attr) + end + end + end + + def non_display_attributes + ['id', 'product_id', 'unscaled_units', 'variant_id', 'supplier_id', 'primary_taxon', 'primary_taxon_id', 'category_id', 'shipping_category_id', 'tax_category_id', 'variant_unit_scale', 'variant_unit', 'unit_value'] + end + + def non_product_attributes + ['line_number', 'valid', 'errors', 'product_object', 'product_validations', 'inventory_validations', 'validates_as', 'save_type', 'on_hand_nil', 'has_overrides'] + end + end +end diff --git a/app/models/product_import/unit_converter.rb b/app/models/product_import/unit_converter.rb new file mode 100644 index 0000000000..d7f9fdc3c0 --- /dev/null +++ b/app/models/product_import/unit_converter.rb @@ -0,0 +1,79 @@ +module ProductImport + class UnitConverter + def initialize(attrs) + @attrs = attrs + convert_custom_unit_fields + end + + def converted_attributes + @attrs + end + + private + + def convert_custom_unit_fields + # units unit_type variant_unit_name -> unit_value variant_unit_scale variant_unit + # 250 ml nil .... 0.25 0.001 volume + # 50 g nil .... 50 1 weight + # 2 kg nil .... 2000 1000 weight + # 1 nil bunches .... 1 null items + + init_unit_values + + assign_weight_or_volume_attributes if units_and_unit_type_present? + assign_item_attributes if units_and_variant_unit_name_present? + end + + def unit_scales + { + 'g' => { scale: 1, unit: 'weight' }, + 'kg' => { scale: 1000, unit: 'weight' }, + 't' => { scale: 1000000, unit: 'weight' }, + 'ml' => { scale: 0.001, unit: 'volume' }, + 'l' => { scale: 1, unit: 'volume' }, + 'kl' => { scale: 1000, unit: 'volume' } + } + end + + def init_unit_values + @attrs['variant_unit'] = nil + @attrs['variant_unit_scale'] = nil + @attrs['unit_value'] = nil + + return unless @attrs.key?('units') && @attrs['units'].present? + + @attrs['unscaled_units'] = @attrs['units'] + end + + def assign_weight_or_volume_attributes + units = @attrs['units'].to_f + unit_type = @attrs['unit_type'].to_s.downcase + + return unless valid_unit_type? unit_type + + @attrs['variant_unit'] = unit_scales[unit_type][:unit] + @attrs['variant_unit_scale'] = unit_scales[unit_type][:scale] + @attrs['unit_value'] = (units || 0) * @attrs['variant_unit_scale'] + end + + def assign_item_attributes + units = @attrs['units'].to_f + + @attrs['variant_unit'] = 'items' + @attrs['variant_unit_scale'] = nil + @attrs['unit_value'] = units || 1 + end + + def units_and_unit_type_present? + @attrs.key?('units') && @attrs.key?('unit_type') && @attrs['units'].present? && @attrs['unit_type'].present? + end + + def units_and_variant_unit_name_present? + @attrs.key?('units') && @attrs.key?('variant_unit_name') && @attrs['units'].present? && @attrs['variant_unit_name'].present? + end + + def valid_unit_type?(unit_type) + unit_scales.key? unit_type + end + end +end diff --git a/app/models/product_importer.rb b/app/models/product_importer.rb deleted file mode 100644 index 13fc0152da..0000000000 --- a/app/models/product_importer.rb +++ /dev/null @@ -1,464 +0,0 @@ -require 'roo' - -class ProductImporter - extend ActiveModel::Naming - include ActiveModel::Conversion - include ActiveModel::Validations - - attr_reader :total_supplier_products - - def initialize(file, editable_enterprises, import_settings={}) - if file.is_a?(File) - @file = file - @sheet = open_spreadsheet - @entries = [] - @valid_entries = {} - @invalid_entries = {} - - @products_to_create = {} - @variants_to_create = {} - @variants_to_update = {} - - @products_created = 0 - @variants_created = 0 - @variants_updated = 0 - - @import_settings = import_settings - @editable_enterprises = {} - editable_enterprises.map { |e| @editable_enterprises[e.name] = e.id } - - @total_supplier_products = 0 - @products_to_reset = {} - @updated_ids = [] - - init_product_importer if @sheet - else - self.errors.add(:importer, I18n.t(:product_importer_file_error)) - end - end - - def persisted? - false #ActiveModel, not ActiveRecord - end - - def has_valid_entries? - valid_count and valid_count > 0 - end - - def item_count - @sheet ? @sheet.last_row - 1 : 0 - end - - def products_to_reset - # Return indexed data about existing product count, reset count, and updates count per supplier - @products_to_reset.each do |supplier_id, values| - values[:updates_count] = 0 if values[:updates_count].blank? - - if values[:updates_count] and values[:existing_products] - @products_to_reset[supplier_id][:reset_count] = values[:existing_products] - values[:updates_count] - end - end - @products_to_reset - end - - def valid_count - @valid_entries.count - end - - def invalid_count - @invalid_entries.count - end - - def products_create_count - @products_to_create.count + @variants_to_create.count - end - - def products_update_count - @variants_to_update.count - end - - def suppliers_index - index = @suppliers_index || build_suppliers_index - index.sort_by{ |k,v| v.to_i }.reverse.to_h - end - - def all_entries - invalid_entries.merge(products_to_create).merge(products_to_update).sort.to_h - end - - def invalid_entries - @invalid_entries - end - - def products_to_create - @products_to_create.merge(@variants_to_create) - end - - def products_to_update - @variants_to_update - end - - def products_created_count - @products_created + @variants_created - end - - def products_updated_count - @variants_updated - end - - def products_reset_count - @products_reset_count || 0 - end - - def total_saved_count - @products_created + @variants_created + @variants_updated - end - - def save_all - save_all_valid - delete_uploaded_file - end - - def permission_by_name?(supplier_name) - @editable_enterprises.has_key?(supplier_name) - end - - def permission_by_id?(supplier_id) - @editable_enterprises.has_value?(Integer(supplier_id)) - end - - private - - def init_product_importer - build_entries - build_categories_index - build_suppliers_index - validate_all - end - - def open_spreadsheet - if accepted_mimetype - Roo::Spreadsheet.open(@file, extension: accepted_mimetype) - else - self.errors.add(:importer, I18n.t(:product_importer_spreadsheet_error)) - delete_uploaded_file - nil - end - end - - def accepted_mimetype - File.extname(@file.path).in?('.csv', '.xls', '.xlsx', '.ods') ? @file.path.split('.').last.to_sym : false - end - - def headers - @sheet.row(1) - end - - def rows - return [] unless @sheet and @sheet.last_row - (2..@sheet.last_row).map do |i| - @sheet.row(i) - end - end - - def build_entries - rows.each_with_index do |row, i| - row_data = Hash[[headers, row].transpose] - entry = SpreadsheetEntry.new(row_data) - entry.line_number = i+2 - @entries.push entry - end - @entries - end - - def validate_all - @entries.each do |entry| - supplier_validation(entry) - category_validation(entry) - set_update_status(entry) - - mark_as_valid(entry) unless entry_invalid?(entry.line_number) - end - - count_existing_products - delete_uploaded_file if item_count.zero? or valid_count.zero? - end - - def count_existing_products - @suppliers_index.each do |supplier_name, supplier_id| - if supplier_id and permission_by_id?(supplier_id) - products_count = Spree::Variant.joins(:product). - where('spree_products.supplier_id IN (?) - AND spree_variants.is_master = false - AND spree_variants.deleted_at IS NULL', supplier_id). - count - - if @products_to_reset[supplier_id] - @products_to_reset[supplier_id][:existing_products] = products_count - else - @products_to_reset[supplier_id] = {existing_products: products_count} - end - - @total_supplier_products += products_count - end - end - end - - def entry_invalid?(line_number) - !!@invalid_entries[line_number] - end - - def supplier_validation(entry) - supplier_name = entry.supplier - - if supplier_name.blank? - mark_as_invalid(entry, attribute: "supplier", error: I18n.t(:error_required)) - return - end - - unless supplier_exists?(supplier_name) - mark_as_invalid(entry, attribute: "supplier", error: I18n.t(:error_not_found_in_database, name: supplier_name)) - return - end - - unless permission_by_name?(supplier_name) - mark_as_invalid(entry, attribute: "supplier", error: I18n.t(:error_no_permission_for_enterprise, name: supplier_name)) - return - end - - entry.supplier_id = @suppliers_index[supplier_name] - end - - def supplier_exists?(supplier_name) - @suppliers_index[supplier_name] - end - - def category_validation(entry) - category_name = entry.category - - if category_name.blank? - mark_as_invalid(entry, attribute: "category", error: I18n.t(:error_required)) - return - end - - if category_exists?(category_name) - entry.primary_taxon_id = @categories_index[category_name] - else - mark_as_invalid(entry, attribute: "category", error: I18n.t(:error_not_found_in_database, name: category_name)) - end - end - - def category_exists?(category_name) - @categories_index[category_name] - end - - def mark_as_valid(entry) - @valid_entries[entry.line_number] = entry - end - - def mark_as_invalid(entry, options={}) - entry.errors.add(options[:attribute], options[:error]) if options[:attribute] and options[:error] - entry.product_validations = options[:product_validations] if options[:product_validations] - - @invalid_entries[entry.line_number] = entry - end - - # Minimise db queries by getting a list of suppliers to look - # up, instead of doing a query for each entry in the spreadsheet - def build_suppliers_index - @suppliers_index = {} - @entries.each do |entry| - supplier_name = entry.supplier - supplier_id = @suppliers_index[supplier_name] || - Enterprise.find_by_name(supplier_name, :select => 'id, name').try(:id) - @suppliers_index[supplier_name] = supplier_id - end - @suppliers_index - end - - def build_categories_index - @categories_index = {} - @entries.each do |entry| - category_name = entry.category - category_id = @categories_index[category_name] || - Spree::Taxon.find_by_name(category_name, :select => 'id, name').try(:id) - @categories_index[category_name] = category_id - end - @categories_index - end - - def save_all_valid - already_created = {} - @products_to_create.each do |line_number, entry| - # If we've already added a new product with these attributes - # from this spreadsheet, mark this entry as a new variant with - # the new product id, as this is a now variant of that product... - if already_created[entry.supplier_id] and already_created[entry.supplier_id][entry.name] - product_id = already_created[entry.supplier_id][entry.name] - mark_as_new_variant(entry, product_id) - next - end - - product = Spree::Product.new() - product.assign_attributes(entry.attributes.except('id')) - assign_defaults(product, entry.attributes) - if product.save - ensure_variant_updated(product, entry) - @products_created += 1 - @updated_ids.push product.variants.first.id - else - self.errors.add("Line #{line_number}:", product.errors.full_messages) #TODO: change - end - - already_created[entry.supplier_id] = {entry.name => product.id} - end - - @variants_to_update.each do |line_number, entry| - variant = entry.product_object - assign_defaults(variant, entry.attributes) - if variant.valid? and variant.save - @variants_updated += 1 - @updated_ids.push variant.id - else - self.errors.add("Line #{line_number}:", variant.errors.full_messages) #TODO: change - end - end - - @variants_to_create.each do |line_number, entry| - new_variant = entry.product_object - assign_defaults(new_variant, entry.attributes) - if new_variant.valid? and new_variant.save - @variants_created += 1 - @updated_ids.push new_variant.id - else - self.errors.add("Line #{line_number}:", new_variant.errors.full_messages) - end - end - - self.errors.add(:importer, I18n.t(:product_importer_products_save_error)) if total_saved_count.zero? - - reset_absent_products - total_saved_count - end - - def reset_absent_products - return if total_saved_count.zero? - - enterprises_to_reset = [] - @import_settings.each do |enterprise_id, settings| - enterprises_to_reset.push enterprise_id if settings['reset_all_absent'] and permission_by_id?(enterprise_id) - end - - unless enterprises_to_reset.empty? or @updated_ids.empty? - # For selected enterprises; set stock to zero for all products - # that were not present in the uploaded spreadsheet - @products_reset_count = Spree::Variant.joins(:product). - where('spree_products.supplier_id IN (?) - AND spree_variants.id NOT IN (?) - AND spree_variants.is_master = false - AND spree_variants.deleted_at IS NULL', enterprises_to_reset, @updated_ids). - update_all(count_on_hand: 0) - end - end - - def assign_defaults(object, entry) - @import_settings[entry['supplier_id'].to_s]['defaults'].each do |attribute, setting| - case setting['mode'] - when 'overwrite_all' - object.assign_attributes(attribute => setting['value']) - when 'overwrite_empty' - if object.send(attribute).blank? or (attribute == 'on_hand' and entry['on_hand_nil']) - object.assign_attributes(attribute => setting['value']) - end - end - end - end - - def ensure_variant_updated(product, entry) - # Ensure display_name and on_demand are copied to new product's variant - if entry.display_name || entry.on_demand - variant = product.variants.first - variant.display_name = entry.display_name if entry.display_name - variant.on_demand = entry.on_demand if entry.on_demand - variant.save - end - end - - def set_update_status(entry) - # Find product with matching supplier and name - match = Spree::Product.where(supplier_id: entry.supplier_id, name: entry.name, deleted_at: nil).first - - # If no matching product was found, create a new product - if match.nil? - mark_as_new_product(entry) - return - end - - # Otherwise, if a variant exists with matching display_name and unit_value, update it - match.variants.each do |existing_variant| - if existing_variant.display_name == entry.display_name && existing_variant.unit_value == Float(entry.unit_value) - mark_as_existing_variant(entry, existing_variant) - return - end - end - - # Otherwise, a variant with sufficiently matching attributes doesn't exist; create a new one - mark_as_new_variant(entry, match.id) - end - - def mark_as_new_product(entry) - new_product = Spree::Product.new() - new_product.assign_attributes(entry.attributes.except('id')) - if new_product.valid? - @products_to_create[entry.line_number] = entry unless entry_invalid?(entry.line_number) - else - mark_as_invalid(entry, product_validations: new_product.errors) - end - end - - def mark_as_existing_variant(entry, existing_variant) - existing_variant.assign_attributes(entry.attributes.except('id', 'product_id')) - check_on_hand_nil(entry, existing_variant) - if existing_variant.valid? - entry.product_object = existing_variant - @variants_to_update[entry.line_number] = entry unless entry_invalid?(entry.line_number) - updates_count_per_supplier(entry.supplier_id) unless entry_invalid?(entry.line_number) - else - mark_as_invalid(entry, product_validations: existing_variant.errors) - end - end - - def mark_as_new_variant(entry, product_id) - new_variant = Spree::Variant.new(entry.attributes.except('id', 'product_id')) - new_variant.product_id = product_id - check_on_hand_nil(entry, new_variant) - if new_variant.valid? - entry.product_object = new_variant - @variants_to_create[entry.line_number] = entry unless entry_invalid?(entry.line_number) - else - mark_as_invalid(entry, product_validations: new_variant.errors) - end - end - - def updates_count_per_supplier(supplier_id) - if @products_to_reset[supplier_id] and @products_to_reset[supplier_id][:updates_count] - @products_to_reset[supplier_id][:updates_count] += 1 - else - @products_to_reset[supplier_id] = {updates_count: 1} - end - end - - def check_on_hand_nil(entry, variant) - if entry.on_hand.blank? - variant.on_hand = 0 - entry.on_hand_nil = true - end - end - - def delete_uploaded_file - # Only delete if file is in '/tmp/product_import' directory - if @file.path == Rails.root.join('tmp', 'product_import').to_s - File.delete(@file) - end - end -end diff --git a/app/models/spreadsheet_entry.rb b/app/models/spreadsheet_entry.rb deleted file mode 100644 index 1db6d43879..0000000000 --- a/app/models/spreadsheet_entry.rb +++ /dev/null @@ -1,68 +0,0 @@ -# Class for defining spreadsheet entry objects for use in ProductImporter -class SpreadsheetEntry - extend ActiveModel::Naming - include ActiveModel::Conversion - include ActiveModel::Validations - - attr_accessor :line_number, :valid, :product_object, :product_validations, :save_type, :on_hand_nil - - attr_accessor :id, :product_id, :supplier, :supplier_id, :name, :display_name, :sku, - :unit_value, :unit_description, :variant_unit, :variant_unit_scale, :variant_unit_name, - :display_as, :category, :primary_taxon_id, :price, :on_hand, :on_demand, - :tax_category_id, :shipping_category_id, :description - - def initialize(attrs) - @product_validations = {} - - attrs.each do |k, v| - if self.respond_to?("#{k}=") - send("#{k}=", v) unless non_product_attributes.include?(k) - else - # Trying to assign unknown attribute. Record this and give feedback or just ignore silently? - end - end - end - - def persisted? - false #ActiveModel - end - - def has_errors? - self.errors.count > 0 or @product_validations.count > 0 - end - - def attributes - attrs = {} - self.instance_variables.each do |var| - attrs[var.to_s.delete("@")] = self.instance_variable_get(var) - end - attrs.except(*non_product_attributes) - end - - def displayable_attributes - # Modified attributes list for displaying in user feedback - attrs = {} - self.instance_variables.each do |var| - attrs[var.to_s.delete("@")] = self.instance_variable_get(var) - end - attrs.except(*non_product_attributes, *non_display_attributes) - end - - def invalid_attributes - invalid_attrs = {} - @product_validations.messages.merge(self.errors.messages).each do |attr, message| - invalid_attrs[attr.to_s] = "#{attr.to_s.capitalize} #{message.first}" - end - invalid_attrs.except(*non_product_attributes, *non_display_attributes) - end - - private - - def non_display_attributes - ['id', 'product_id', 'variant_id', 'supplier_id', 'primary_taxon', 'primary_taxon_id', 'category_id', 'shipping_category_id', 'tax_category_id'] - end - - def non_product_attributes - ['line_number', 'valid', 'errors', 'product_object', 'product_validations', 'save_type', 'on_hand_nil'] - end -end diff --git a/app/models/spree/ability_decorator.rb b/app/models/spree/ability_decorator.rb index dc00ec1871..385b9be032 100644 --- a/app/models/spree/ability_decorator.rb +++ b/app/models/spree/ability_decorator.rb @@ -61,7 +61,7 @@ class AbilityDecorator order.user == user end - can [:destroy], Spree::CreditCard do |credit_card| + can [:update, :destroy], Spree::CreditCard do |credit_card| credit_card.user == user end end @@ -127,12 +127,14 @@ class AbilityDecorator can [:admin, :connect, :status, :destroy], StripeAccount do |stripe_account| user.enterprises.include? stripe_account.enterprise end + + can [:admin, :create], :manager_invitation end def add_product_management_abilities(user) # Enterprise User can only access products that they are a supplier for can [:create], Spree::Product - can [:admin, :read, :update, :product_distributions, :seo, :group_buy_options, :bulk_edit, :bulk_update, :clone, :delete, :destroy], Spree::Product do |product| + can [:admin, :read, :index, :update, :product_distributions, :seo, :group_buy_options, :bulk_update, :clone, :delete, :destroy], Spree::Product do |product| OpenFoodNetwork::Permissions.new(user).managed_product_enterprises.include? product.supplier end @@ -175,7 +177,7 @@ class AbilityDecorator can [:admin, :index, :read, :search], Spree::Taxon can [:admin, :index, :read, :create, :edit], Spree::Classification - can [:admin, :index, :import, :save], ProductImporter + can [:admin, :index, :guide, :import, :save, :save_data, :validate_data, :reset_absent_products], ProductImport::ProductImporter # Reports page can [:admin, :index, :customers, :orders_and_distributors, :group_buys, :bulk_coop, :payments, :orders_and_fulfillment, :products_and_inventory, :order_cycle_management, :packing], :report diff --git a/app/models/spree/credit_card_decorator.rb b/app/models/spree/credit_card_decorator.rb index b9c0fca337..e82dea3443 100644 --- a/app/models/spree/credit_card_decorator.rb +++ b/app/models/spree/credit_card_decorator.rb @@ -5,7 +5,7 @@ Spree::CreditCard.class_eval do attr_accessible :cc_type, :last_digits # For holding customer preference in memory - attr_accessible :save_requested_by_customer + attr_accessible :save_requested_by_customer, :is_default attr_writer :save_requested_by_customer # Should be able to remove once we reach Spree v2.2.0 @@ -14,15 +14,31 @@ Spree::CreditCard.class_eval do belongs_to :user + after_create :ensure_single_default_card + after_save :ensure_single_default_card, if: :is_default_changed? + # Allows us to use a gateway_payment_profile_id to store Stripe Tokens # Should be able to remove once we reach Spree v2.2.0 # Commit: https://github.com/spree/spree/commit/5a4d690ebc64b264bf12904a70187e7a8735ef3f # See also: https://github.com/spree/spree_gateway/issues/111 - def has_payment_profile? # rubocop:disable Style/PredicateName + def has_payment_profile? # rubocop:disable Naming/PredicateName gateway_customer_profile_id.present? || gateway_payment_profile_id.present? end def save_requested_by_customer? !!@save_requested_by_customer end + + private + + def default_missing? + !user.credit_cards.exists?(is_default: true) + end + + def ensure_single_default_card + return unless user + return unless is_default? || default_missing? + user.credit_cards.update_all(['is_default=(id=?)', id]) + self.is_default = true + end end diff --git a/app/models/spree/order_decorator.rb b/app/models/spree/order_decorator.rb index 0228586327..c6def73d68 100644 --- a/app/models/spree/order_decorator.rb +++ b/app/models/spree/order_decorator.rb @@ -17,6 +17,7 @@ Spree::Order.class_eval do validates :customer, presence: true, if: :require_customer? validate :products_available_from_new_distribution, :if => lambda { distributor_id_changed? || order_cycle_id_changed? } + validate :disallow_guest_order attr_accessible :order_cycle_id, :distributor_id, :customer_id before_validation :shipping_address_from_distributor @@ -82,6 +83,20 @@ Spree::Order.class_eval do errors.add(:base, I18n.t(:spree_order_availability_error)) unless DistributionChangeValidator.new(self).can_change_to_distribution?(distributor, order_cycle) end + def using_guest_checkout? + require_email && !user.andand.id + end + + def registered_email? + Spree.user_class.exists?(email: email) + end + + def disallow_guest_order + if using_guest_checkout? && registered_email? + errors.add(:base, I18n.t('devise.failure.already_registered')) + end + end + def empty_with_clear_shipping_and_payments! empty_without_clear_shipping_and_payments! payments.clear diff --git a/app/models/spree/product_decorator.rb b/app/models/spree/product_decorator.rb index 06970f0a13..8186f99473 100644 --- a/app/models/spree/product_decorator.rb +++ b/app/models/spree/product_decorator.rb @@ -174,6 +174,14 @@ Spree::Product.class_eval do order_cycle.variants_distributed_by(distributor).where(product_id: self) end + # Get the most recent import_date of a product's variants + def import_date + variants.map do |variant| + next if variant.import_date.blank? + variant.import_date + end.sort.last + end + # Build a product distribution for each distributor def build_product_distributions_for_user user Enterprise.is_distributor.managed_by(user).each do |distributor| diff --git a/app/models/spree/variant_decorator.rb b/app/models/spree/variant_decorator.rb index 592c93b437..2947efc282 100644 --- a/app/models/spree/variant_decorator.rb +++ b/app/models/spree/variant_decorator.rb @@ -10,13 +10,12 @@ Spree::Variant.class_eval do remove_method :options_text if instance_methods(false).include? :options_text include OpenFoodNetwork::VariantAndLineItemNaming - has_many :exchange_variants has_many :exchanges, through: :exchange_variants has_many :variant_overrides has_many :inventory_items - attr_accessible :unit_value, :unit_description, :images_attributes, :display_as, :display_name + attr_accessible :unit_value, :unit_description, :images_attributes, :display_as, :display_name, :import_date accepts_nested_attributes_for :images validates_presence_of :unit_value, @@ -30,10 +29,11 @@ Spree::Variant.class_eval do after_save :refresh_products_cache around_destroy :destruction - scope :with_order_cycles_inner, joins(exchanges: :order_cycle) scope :not_deleted, where(deleted_at: nil) + scope :not_master, where(is_master: false) + scope :in_stock, where('spree_variants.count_on_hand > 0 OR spree_variants.on_demand=?', true) scope :in_order_cycle, lambda { |order_cycle| with_order_cycles_inner. merge(Exchange.outgoing). @@ -117,7 +117,6 @@ Spree::Variant.class_eval do end end - private def update_weight_from_unit_value diff --git a/app/models/variant_override.rb b/app/models/variant_override.rb index 453a93f7b4..a6a5dfe8ed 100644 --- a/app/models/variant_override.rb +++ b/app/models/variant_override.rb @@ -3,6 +3,8 @@ class VariantOverride < ActiveRecord::Base acts_as_taggable + attr_accessor :import_date + belongs_to :hub, class_name: 'Enterprise' belongs_to :variant, class_name: 'Spree::Variant' diff --git a/app/overrides/spree/admin/orders/customer_details/_form/make_email_fullwidth.html.haml.deface b/app/overrides/spree/admin/orders/customer_details/_form/make_email_fullwidth.html.haml.deface new file mode 100644 index 0000000000..409c3c90ab --- /dev/null +++ b/app/overrides/spree/admin/orders/customer_details/_form/make_email_fullwidth.html.haml.deface @@ -0,0 +1,2 @@ +/ set_attributes "div[data-hook='customer_fields'] div.alpha" +/ attributes({class: "fullwidth"}) diff --git a/app/overrides/spree/admin/orders/customer_details/_form/remove_guest_order_buttons.deface b/app/overrides/spree/admin/orders/customer_details/_form/remove_guest_order_buttons.deface new file mode 100644 index 0000000000..f94267be6f --- /dev/null +++ b/app/overrides/spree/admin/orders/customer_details/_form/remove_guest_order_buttons.deface @@ -0,0 +1 @@ +remove "div[data-hook='customer_fields'] div.omega" diff --git a/app/overrides/spree/admin/products/edit/return_to_bulk_product_edit.html.haml.deface b/app/overrides/spree/admin/products/edit/return_to_bulk_product_edit.html.haml.deface deleted file mode 100644 index 8557143ae0..0000000000 --- a/app/overrides/spree/admin/products/edit/return_to_bulk_product_edit.html.haml.deface +++ /dev/null @@ -1,3 +0,0 @@ -/ replace "code[erb-loud]:contains('button_link_to t(:back_to_products_list)')" - -= button_link_to t('admin.products.back_to_products_list'), bulk_edit_admin_products_path, :icon => 'icon-arrow-left' \ No newline at end of file diff --git a/app/overrides/spree/admin/products/edit/return_to_products.html.haml.deface b/app/overrides/spree/admin/products/edit/return_to_products.html.haml.deface new file mode 100644 index 0000000000..1865179b4c --- /dev/null +++ b/app/overrides/spree/admin/products/edit/return_to_products.html.haml.deface @@ -0,0 +1,3 @@ +/ replace "code[erb-loud]:contains('button_link_to t(:back_to_products_list)')" + += button_link_to t('admin.products.back_to_products_list'), admin_products_path, :icon => 'icon-arrow-left' diff --git a/app/overrides/spree/admin/products/new/replace_form.html.haml.deface b/app/overrides/spree/admin/products/new/replace_form.html.haml.deface index efcd0f1d3d..538ca95fbf 100644 --- a/app/overrides/spree/admin/products/new/replace_form.html.haml.deface +++ b/app/overrides/spree/admin/products/new/replace_form.html.haml.deface @@ -90,7 +90,7 @@ = button t('actions.create_and_add_another'), 'icon-repeat', :submit, value: 'add_another' %span.or = t(:or) - = link_to_with_icon 'icon-remove', t('actions.cancel'), bulk_edit_admin_products_path, :class => 'button' + = link_to_with_icon 'icon-remove', t('actions.cancel'), admin_products_path, :class => 'button' :javascript diff --git a/app/overrides/spree/admin/shared/_product_sub_menu/replace_products_tab.html.haml.deface b/app/overrides/spree/admin/shared/_product_sub_menu/replace_products_tab.html.haml.deface deleted file mode 100644 index ec97ae63fe..0000000000 --- a/app/overrides/spree/admin/shared/_product_sub_menu/replace_products_tab.html.haml.deface +++ /dev/null @@ -1,3 +0,0 @@ -/ replace "code[erb-loud]:contains('tab :products')" - -= tab :products, url: bulk_edit_admin_products_path, :match_path => '/products' \ No newline at end of file diff --git a/app/serializers/api/admin/enterprise_serializer.rb b/app/serializers/api/admin/enterprise_serializer.rb index 2429ce21a9..e29954992f 100644 --- a/app/serializers/api/admin/enterprise_serializer.rb +++ b/app/serializers/api/admin/enterprise_serializer.rb @@ -8,6 +8,7 @@ class Api::Admin::EnterpriseSerializer < ActiveModel::Serializer has_one :owner, serializer: Api::Admin::UserSerializer has_many :users, serializer: Api::Admin::UserSerializer + has_one :address, serializer: Api::AddressSerializer def tag_groups object.tag_rules.prioritised.reject(&:is_default).each_with_object([]) do |tag_rule, tag_groups| diff --git a/app/serializers/api/admin/index_order_cycle_serializer.rb b/app/serializers/api/admin/index_order_cycle_serializer.rb index 22d6d4445e..144bc38a7c 100644 --- a/app/serializers/api/admin/index_order_cycle_serializer.rb +++ b/app/serializers/api/admin/index_order_cycle_serializer.rb @@ -7,7 +7,7 @@ module Api attributes :id, :name, :orders_open_at, :orders_close_at, :status, :variant_count, :deletable attributes :coordinator, :producers, :shops, :viewing_as_coordinator - attributes :edit_path, :clone_path, :delete_path + attributes :edit_path, :clone_path, :delete_path, :subscriptions_count has_many :schedules, serializer: Api::Admin::IdNameSerializer @@ -61,6 +61,10 @@ module Api admin_order_cycle_path(object) end + def subscriptions_count + options[:subscriptions_count].for(object.id) + end + private def visible_enterprises diff --git a/app/serializers/api/admin/order_cycle_serializer.rb b/app/serializers/api/admin/order_cycle_serializer.rb index 9b5ff7a7cb..919ab9de1f 100644 --- a/app/serializers/api/admin/order_cycle_serializer.rb +++ b/app/serializers/api/admin/order_cycle_serializer.rb @@ -4,7 +4,7 @@ class Api::Admin::OrderCycleSerializer < ActiveModel::Serializer attributes :id, :name, :orders_open_at, :orders_close_at, :coordinator_id, :exchanges attributes :editable_variants_for_incoming_exchanges, :editable_variants_for_outgoing_exchanges attributes :visible_variants_for_outgoing_exchanges - attributes :viewing_as_coordinator, :schedule_ids + attributes :viewing_as_coordinator, :schedule_ids, :subscriptions_count has_many :coordinator_fees, serializer: Api::IdSerializer @@ -20,6 +20,10 @@ class Api::Admin::OrderCycleSerializer < ActiveModel::Serializer Enterprise.managed_by(options[:current_user]).include? object.coordinator end + def subscriptions_count + ProxyOrder.not_canceled.where(order_cycle_id: object.id).count + end + def exchanges scoped_exchanges = OpenFoodNetwork::OrderCyclePermissions.new(options[:current_user], object).visible_exchanges.by_enterprise_name ActiveModel::ArraySerializer.new(scoped_exchanges, {each_serializer: Api::Admin::ExchangeSerializer, current_user: options[:current_user] }) diff --git a/app/serializers/api/admin/product_serializer.rb b/app/serializers/api/admin/product_serializer.rb index dc88a03448..8af5701b94 100644 --- a/app/serializers/api/admin/product_serializer.rb +++ b/app/serializers/api/admin/product_serializer.rb @@ -1,7 +1,7 @@ class Api::Admin::ProductSerializer < ActiveModel::Serializer attributes :id, :name, :sku, :variant_unit, :variant_unit_scale, :variant_unit_name, :on_demand, :inherits_properties - attributes :on_hand, :price, :available_on, :permalink_live, :tax_category_id, :image_url, :thumb_url + attributes :on_hand, :price, :available_on, :permalink_live, :tax_category_id, :import_date, :image_url, :thumb_url has_one :supplier, key: :producer_id, embed: :id has_one :primary_taxon, key: :category_id, embed: :id diff --git a/app/serializers/api/admin/variant_override_serializer.rb b/app/serializers/api/admin/variant_override_serializer.rb index 68b86818a1..c63e77c068 100644 --- a/app/serializers/api/admin/variant_override_serializer.rb +++ b/app/serializers/api/admin/variant_override_serializer.rb @@ -1,6 +1,6 @@ class Api::Admin::VariantOverrideSerializer < ActiveModel::Serializer attributes :id, :hub_id, :variant_id, :sku, :price, :count_on_hand, :on_demand, :default_stock, :resettable - attributes :tag_list, :tags + attributes :tag_list, :tags, :import_date def tag_list object.tag_list.join(",") diff --git a/app/serializers/api/admin/variant_serializer.rb b/app/serializers/api/admin/variant_serializer.rb index f27528e1c7..3fc22b98d4 100644 --- a/app/serializers/api/admin/variant_serializer.rb +++ b/app/serializers/api/admin/variant_serializer.rb @@ -1,6 +1,6 @@ class Api::Admin::VariantSerializer < ActiveModel::Serializer attributes :id, :options_text, :unit_value, :unit_description, :unit_to_display, :on_demand, :display_as, :display_name, :name_to_display, :sku - attributes :on_hand, :price + attributes :on_hand, :price, :import_date has_many :variant_overrides def on_hand diff --git a/app/serializers/api/credit_card_serializer.rb b/app/serializers/api/credit_card_serializer.rb index 1ea2da07d5..bab0f5bc8d 100644 --- a/app/serializers/api/credit_card_serializer.rb +++ b/app/serializers/api/credit_card_serializer.rb @@ -1,6 +1,6 @@ module Api class CreditCardSerializer < ActiveModel::Serializer - attributes :id, :brand, :number, :expiry, :formatted, :delete_link + attributes :id, :brand, :number, :expiry, :formatted, :delete_link, :is_default def brand object.cc_type.capitalize diff --git a/app/serializers/api/state_serializer.rb b/app/serializers/api/state_serializer.rb index a43e5665b3..38ad444701 100644 --- a/app/serializers/api/state_serializer.rb +++ b/app/serializers/api/state_serializer.rb @@ -1,7 +1,3 @@ class Api::StateSerializer < ActiveModel::Serializer attributes :id, :name, :abbr - - def abbr - object.abbr.upcase - end end diff --git a/app/services/order_cycle_form.rb b/app/services/order_cycle_form.rb new file mode 100644 index 0000000000..d463badfa4 --- /dev/null +++ b/app/services/order_cycle_form.rb @@ -0,0 +1,88 @@ +require 'open_food_network/permissions' +require 'open_food_network/proxy_order_syncer' +require 'open_food_network/order_cycle_form_applicator' + +class OrderCycleForm + def initialize(order_cycle, params, user) + @order_cycle = order_cycle + @params = params + @user = user + @permissions = OpenFoodNetwork::Permissions.new(user) + end + + def save + build_schedule_ids + order_cycle.assign_attributes(params[:order_cycle]) + return false unless order_cycle.valid? + order_cycle.transaction do + order_cycle.save! + apply_exchange_changes + sync_subscriptions + true + end + rescue ActiveRecord::RecordInvalid + false + end + + private + + attr_accessor :order_cycle, :params, :user, :permissions + + def apply_exchange_changes + return if exchanges_unchanged? + OpenFoodNetwork::OrderCycleFormApplicator.new(order_cycle, user).go! + end + + def exchanges_unchanged? + [:incoming_exchanges, :outgoing_exchanges].all? do |direction| + params[:order_cycle][direction].nil? + end + end + + def schedule_ids? + params[:order_cycle][:schedule_ids].present? + end + + def build_schedule_ids + return unless schedule_ids? + result = existing_schedule_ids + result |= (requested_schedule_ids & permitted_schedule_ids) # Add permitted and requested + result -= ((result & permitted_schedule_ids) - requested_schedule_ids) # Remove permitted but not requested + params[:order_cycle][:schedule_ids] = result + end + + def sync_subscriptions + return unless schedule_ids? + return unless schedule_sync_required? + OpenFoodNetwork::ProxyOrderSyncer.new(subscriptions_to_sync).sync! + end + + def schedule_sync_required? + removed_schedule_ids.any? || new_schedule_ids.any? + end + + def subscriptions_to_sync + Subscription.where(schedule_id: removed_schedule_ids + new_schedule_ids) + end + + def requested_schedule_ids + params[:order_cycle][:schedule_ids].map(&:to_i) + end + + def permitted_schedule_ids + Schedule.where(id: requested_schedule_ids | existing_schedule_ids) + .merge(permissions.editable_schedules).pluck(:id) + end + + def existing_schedule_ids + @existing_schedule_ids ||= order_cycle.persisted? ? order_cycle.schedule_ids : [] + end + + def removed_schedule_ids + existing_schedule_ids - order_cycle.schedule_ids + end + + def new_schedule_ids + @order_cycle.schedule_ids - existing_schedule_ids + end +end diff --git a/app/services/subscriptions_count.rb b/app/services/subscriptions_count.rb new file mode 100644 index 0000000000..6afbe15dae --- /dev/null +++ b/app/services/subscriptions_count.rb @@ -0,0 +1,19 @@ +class SubscriptionsCount + def initialize(order_cycles) + @order_cycles = order_cycles + end + + def for(order_cycle_id) + active[order_cycle_id] || 0 + end + + private + + attr_accessor :order_cycles + + def active + return @active unless @active.nil? + return @active = [] if order_cycles.blank? + @active ||= ProxyOrder.not_canceled.group(:order_cycle_id).where(order_cycle_id: order_cycles).count + end +end diff --git a/app/views/admin/enterprises/_form_data.html.haml b/app/views/admin/enterprises/_form_data.html.haml index ee3d17d4aa..14f83b79bf 100644 --- a/app/views/admin/enterprises/_form_data.html.haml +++ b/app/views/admin/enterprises/_form_data.html.haml @@ -3,3 +3,4 @@ = admin_inject_payment_methods = admin_inject_shipping_methods = admin_inject_enterprise_permissions += admin_inject_available_countries(module: 'admin.enterprises') diff --git a/app/views/admin/enterprises/_new_form.html.haml b/app/views/admin/enterprises/_new_form.html.haml index 9d25fbff7a..84102ce63b 100644 --- a/app/views/admin/enterprises/_new_form.html.haml +++ b/app/views/admin/enterprises/_new_form.html.haml @@ -87,15 +87,17 @@ = af.text_field :city, { placeholder: t(:city_placeholder)} .five.columns.omega = af.text_field :zipcode, { placeholder: t(:postcode_placeholder)} - .row - .three.columns.alpha - = af.label :state_id, t(:state) - \/ - = af.label :country_id, t(:country) - .four.columns - = af.collection_select :state_id, af.object.country.states, :id, :name, {}, :class => "select2 fullwidth" - .five.columns.omega - = af.collection_select :country_id, available_countries, :id, :name, {}, :class => "select2 fullwidth" + %div{ "ng-controller" => "countryCtrl" } + .row + .three.columns.alpha + = af.label :state_id, t(:state) + \/ + = af.label :country_id, t(:country) + .four.columns + %input.ofn-select2.fullwidth#enterprise_address_attributes_state_id{ name: 'enterprise[address_attributes][state_id]', type: 'number', data: 'countriesById[Enterprise.address.country_id].states', placeholder: t('admin.choose'), ng: { model: 'Enterprise.address.state_id' } } + .five.columns.omega + %input.ofn-select2.fullwidth#enterprise_address_attributes_country_id{ name: 'enterprise[address_attributes][country_id]', type: 'number', data: 'countries', placeholder: t('admin.choose'), ng: { model: 'Enterprise.address.country_id' } } + .row .twelve.columns.alpha   diff --git a/app/views/admin/enterprises/form/_address.html.haml b/app/views/admin/enterprises/form/_address.html.haml index a1485d485f..cb278df5e0 100644 --- a/app/views/admin/enterprises/form/_address.html.haml +++ b/app/views/admin/enterprises/form/_address.html.haml @@ -31,7 +31,8 @@ \/ = af.label :country_id, t(:country) %span.required * - .four.columns - = af.collection_select :state_id, af.object.country.states, :id, :name, {}, :class => "select2 fullwidth" - .four.columns.omega - = af.collection_select :country_id, available_countries, :id, :name, {}, :class => "select2 fullwidth" + %div{ "ng-controller" => "countryCtrl" } + .four.columns + %input.ofn-select2.fullwidth#enterprise_address_attributes_state_id{ name: 'enterprise[address_attributes][state_id]', type: 'number', data: 'countriesById[Enterprise.address.country_id].states', placeholder: t('admin.choose'), ng: { model: 'Enterprise.address.state_id' } } + .four.columns.omega + %input.ofn-select2.fullwidth#enterprise_address_attributes_country_id{ name: 'enterprise[address_attributes][country_id]', type: 'number', data: 'countries', placeholder: t('admin.choose'), ng: { model: 'Enterprise.address.country_id' } } diff --git a/app/views/admin/enterprises/form/_shop_preferences.html.haml b/app/views/admin/enterprises/form/_shop_preferences.html.haml index 2b7e916eeb..fceac454aa 100644 --- a/app/views/admin/enterprises/form/_shop_preferences.html.haml +++ b/app/views/admin/enterprises/form/_shop_preferences.html.haml @@ -77,16 +77,16 @@ = f.radio_button :allow_order_changes, true, "ng-model" => "Enterprise.allow_order_changes", "ng-value" => "true" = f.label :allow_order_changes, t('.allow_order_changes_true'), value: :true - --# .row --# .alpha.eleven.columns --# .three.columns.alpha --# %label= t '.enable_subscriptions' --# %div{'ofn-with-tip' => t('.enable_subscriptions_tip')} --# %a= t 'admin.whats_this' --# .three.columns --# = f.radio_button :enable_subscriptions, true --# = f.label :enable_subscriptions, t('.enable_subscriptions_true'), value: :true --# .five.columns.omega --# = f.radio_button :enable_subscriptions, false --# = f.label :enable_subscriptions, t('.enable_subscriptions_false'), value: :false +- if spree_current_user.admin? + .row + .alpha.eleven.columns + .three.columns.alpha + %label= t '.enable_subscriptions' + %div{'ofn-with-tip' => t('.enable_subscriptions_tip')} + %a= t 'admin.whats_this' + .three.columns + = f.radio_button :enable_subscriptions, true + = f.label :enable_subscriptions, t('.enable_subscriptions_true'), value: :true + .five.columns.omega + = f.radio_button :enable_subscriptions, false + = f.label :enable_subscriptions, t('.enable_subscriptions_false'), value: :false diff --git a/app/views/admin/enterprises/form/_users.html.haml b/app/views/admin/enterprises/form/_users.html.haml index adc6d4ba67..89696d112b 100644 --- a/app/views/admin/enterprises/form/_users.html.haml +++ b/app/views/admin/enterprises/form/_users.html.haml @@ -44,7 +44,6 @@ - # Ignore this input in the submit = hidden_field_tag :ignored, nil, class: "select2 fullwidth", 'user-select' => 'newManager', 'ng-model' => 'newManager' %td.actions - %a{ 'ng-click' => 'addManager(newManager)', :class => "icon-plus no-text" } %tr.animate-repeat{ id: "manager-{{manager.id}}", ng: { repeat: 'manager in Enterprise.users' }} %td = hidden_field_tag "enterprise[user_ids][]", nil, multiple: true, 'ng-value' => 'manager.id' diff --git a/app/views/admin/enterprises/new.html.haml b/app/views/admin/enterprises/new.html.haml index 4676d117fb..dd830054f5 100644 --- a/app/views/admin/enterprises/new.html.haml +++ b/app/views/admin/enterprises/new.html.haml @@ -6,10 +6,15 @@ - content_for :page_actions do %li= button_link_to t('.back_link'), main_app.admin_enterprises_path, icon: 'icon-arrow-left' +- content_for :app_wrapper_attrs do + = "ng-app='admin.enterprises'" + += admin_inject_available_countries(module: 'admin.enterprises') += admin_inject_json "admin.enterprises", "defaultCountryID", Spree::Config[:default_country_id] -# Form = form_for [main_app, :admin, @enterprise], html: { "nav-check" => '', "nav-callback" => '' } do |f| .row - .twelve.columns.fullwidth_inputs{ ng: { app: "admin.users" } } + .twelve.columns.fullwidth_inputs{ ng: { controller: "NewEnterpriseController" } } = render 'new_form', f: f diff --git a/app/views/admin/order_cycles/_name_and_timing_form.html.haml b/app/views/admin/order_cycles/_name_and_timing_form.html.haml index 11fc0a7a6a..842b12d5c3 100644 --- a/app/views/admin/order_cycles/_name_and_timing_form.html.haml +++ b/app/views/admin/order_cycles/_name_and_timing_form.html.haml @@ -10,7 +10,7 @@ = f.label :orders_open_at, t('.orders_open') .omega.six.columns - if viewing_as_coordinator_of?(@order_cycle) - = f.text_field :orders_open_at, 'datetimepicker' => 'order_cycle.orders_open_at', 'ng-model' => 'order_cycle.orders_open_at', 'ng-disabled' => '!loaded()' + = f.text_field :orders_open_at, 'datetimepicker' => 'order_cycle.orders_open_at', 'ng-model' => 'order_cycle.orders_open_at', 'ng-disabled' => '!loaded()', 'change-warning' => 'order_cycle' - else {{ order_cycle.orders_open_at }} @@ -23,7 +23,7 @@ = f.label :orders_close, t('.orders_close') .six.columns.omega - if viewing_as_coordinator_of?(@order_cycle) - = f.text_field :orders_close_at, 'datetimepicker' => 'order_cycle.orders_close_at', 'ng-model' => 'order_cycle.orders_close_at', 'ng-disabled' => '!loaded()' + = f.text_field :orders_close_at, 'datetimepicker' => 'order_cycle.orders_close_at', 'ng-model' => 'order_cycle.orders_close_at', 'ng-disabled' => '!loaded()', 'change-warning' => 'order_cycle' - else {{ order_cycle.orders_close_at }} @@ -31,7 +31,7 @@ - if subscriptions_enabled? .row .two.columns.alpha - = f.label :schedule_ids, t('admin.order_cycles.schedules') + = f.label :schedule_ids, t('admin.order_cycles.index.schedules') .twelve.columns - if viewing_as_coordinator_of?(@order_cycle) %input.fullwidth.ofn-select2#schedule_ids{ name: 'order_cycle[schedule_ids]', diff --git a/app/views/admin/order_cycles/_row.html.haml b/app/views/admin/order_cycles/_row.html.haml index a2fb90df3b..40f40e5126 100644 --- a/app/views/admin/order_cycles/_row.html.haml +++ b/app/views/admin/order_cycles/_row.html.haml @@ -8,10 +8,10 @@ {{ schedule.name + ($last ? '' : ',') }} %span{ ng: { show: 'orderCycle.schedules.length == 0'}} None %td.orders_open_at{ ng: { show: 'columns.open.visible' } } - %input.datetimepicker{ id: 'oc{{::orderCycle.id}}_orders_open_at', name: 'oc{{::orderCycle.id}}[orders_open_at]', type: 'text', ng: { if: 'orderCycle.viewing_as_coordinator', model: 'orderCycle.orders_open_at' }, datetimepicker: 'orderCycle.orders_open_at' } + %input.datetimepicker{ id: 'oc{{::orderCycle.id}}_orders_open_at', name: 'oc{{::orderCycle.id}}[orders_open_at]', type: 'text', ng: { if: 'orderCycle.viewing_as_coordinator', model: 'orderCycle.orders_open_at' }, datetimepicker: 'orderCycle.orders_open_at', 'change-warning' => 'orderCycle' } %input{ id: 'oc{{::orderCycle.id}}_orders_open_at', name: 'oc{{::orderCycle.id}}[orders_open_at]', type: 'text', ng: { if: '!orderCycle.viewing_as_coordinator', model: 'orderCycle.orders_open_at'}, disabled: true } %td.orders_close_at{ ng: { show: 'columns.close.visible' } } - %input.datetimepicker{ id: 'oc{{::orderCycle.id}}_orders_close_at', name: 'oc{{::orderCycle.id}}[orders_close_at]', type: 'text', ng: { if: 'orderCycle.viewing_as_coordinator', model: 'orderCycle.orders_close_at' }, datetimepicker: 'orderCycle.orders_close_at' } + %input.datetimepicker{ id: 'oc{{::orderCycle.id}}_orders_close_at', name: 'oc{{::orderCycle.id}}[orders_close_at]', type: 'text', ng: { if: 'orderCycle.viewing_as_coordinator', model: 'orderCycle.orders_close_at' }, datetimepicker: 'orderCycle.orders_close_at', 'change-warning' => 'orderCycle' } %input{ id: 'oc{{::orderCycle.id}}_orders_close_at', name: 'oc{{::orderCycle.id}}[orders_close_at]', type: 'text', ng: { if: '!orderCycle.viewing_as_coordinator', model: 'orderCycle.orders_close_at'}, disabled: true } - unless simple_index diff --git a/app/views/admin/product_import/_entries_table.html.haml b/app/views/admin/product_import/_entries_table.html.haml index cd293eea05..8ad958b1c2 100644 --- a/app/views/admin/product_import/_entries_table.html.haml +++ b/app/views/admin/product_import/_entries_table.html.haml @@ -1,15 +1,14 @@ -- if entries && entries.count > 0 - %div.table-wrap - %table - %thead - %th - %th Line - - entries.values.first.displayable_attributes.each do |key, value| - %th= key - - entries.each do |line_number, entry| - %tr{class: ('error' if entry.has_errors?)} - %td - %i{class: (entry.has_errors? ? 'fa fa-warning warning' : 'fa fa-check-circle success')} - %td= line_number - - entry.displayable_attributes.each do |key, value| - %td{class: ('invalid' if entry.has_errors? and entry.invalid_attributes[key])}= value +%div.table-wrap + %table + %thead + %th + %th #{t('admin.product_import.import.line')} + - @importer.table_headings.each do |heading| + %th= heading + %tr{ng: {repeat: "(line_number, entry) in (entries | entriesFilterValid:'#{entries}')"}} + %td + %i{ng: {class: "{'fa fa-warning warning': (count(entry.errors) > 0), 'fa fa-check-circle success': (count(entry.errors) == 0)}"}} + %td + {{line_number}} + %td{ng: {repeat: "(attribute, value) in entry.attributes", class: "{'invalid': attribute_invalid(attribute, line_number)}"}} + {{value}} diff --git a/app/views/admin/product_import/_errors_list.html.haml b/app/views/admin/product_import/_errors_list.html.haml index e7203a7ddd..787aabe76b 100644 --- a/app/views/admin/product_import/_errors_list.html.haml +++ b/app/views/admin/product_import/_errors_list.html.haml @@ -1,11 +1,9 @@ -- @importer.invalid_entries.each do |line_number, entry| - %div.import-errors - %p.line - %strong - Item line #{line_number}: - %span= entry.name - - if entry.display_name - ( #{entry.display_name} ) - - entry.invalid_attributes.each do |attr, error| - %p.error -  -  #{error} \ No newline at end of file +%div.import-errors{ng: {controller: 'ImportFeedbackCtrl', repeat: "(line_number, entry ) in (entries | entriesFilterValid:'invalid')"}} + %p.line + %strong + #{t('admin.product_import.import.item_line')} {{line_number}}: + %span {{entry.attributes.name}} + %span{ng: {if: "entry.attributes.display_name"}} + ( {{entry.attributes.display_name}} ) + %p.error{ng: {repeat: "(attribute, error) in entry.errors", show: "ignore_fields.indexOf(attribute) < 0" }} +  -  {{error}} diff --git a/app/views/admin/product_import/_import_options.html.haml b/app/views/admin/product_import/_import_options.html.haml index f5733c4f80..3c79357e46 100644 --- a/app/views/admin/product_import/_import_options.html.haml +++ b/app/views/admin/product_import/_import_options.html.haml @@ -1,4 +1,4 @@ -%h5 Import options and defaults +%h5= t('admin.product_import.import.options_and_defaults') %br - @importer.suppliers_index.each do |name, supplier_id| @@ -9,8 +9,6 @@ %i{ng: {class: "{'icon-chevron-down': active, 'icon-chevron-right': !active}"}} %div.header-icon.neutral %i.fa.fa-edit - -#%div.header-count - -# %strong.invalid-count= @importer.invalid_count %div.header-description = name %div.panel-content{ng: {hide: '!active'}} @@ -19,31 +17,29 @@ %div.panel-section.import-settings{ng: {controller: 'DropdownPanelsCtrl'}} %div.panel-header %div.header-caret - -#%i{ng: {class: "{'icon-chevron-down': active, 'icon-chevron-right': !active}"}} %div.header-icon.error %i.fa.fa-warning - -#%div.header-count - -# %strong.invalid-count= @importer.invalid_count %div.header-description = name - %span.header-error= " - you do not have permission to manage this enterprise" - -#%div.panel-content{ng: {hide: '!active'}} - -# = render 'options_form', supplier_id: supplier_id, name: name + %span.header-error= " - #{t('admin.product_import.import.no_permission')}" - elsif name %div.panel-section.import-settings{ng: {controller: 'DropdownPanelsCtrl'}} %div.panel-header %div.header-caret - -#%i{ng: {class: "{'icon-chevron-down': active, 'icon-chevron-right': !active}"}} %div.header-icon.error %i.fa.fa-warning - -#%div.header-count - -# %strong.invalid-count= @importer.invalid_count %div.header-description = name - %span.header-error= " - enterprise could not be found in database" - -#%div.panel-content{ng: {hide: '!active'}} - -# = render 'options_form', supplier_id: supplier_id, name: name - + %span.header-error= " - #{t('admin.product_import.import.not_found')}" + - else + %div.panel-section.import-settings{ng: {controller: 'DropdownPanelsCtrl'}} + %div.panel-header + %div.header-caret + %div.header-icon.error + %i.fa.fa-warning + %div.header-description + = t('admin.product_import.import.no_name') + %span.header-error= " - #{t('admin.product_import.import.blank_supplier')}" %br.panels.clearfix %br diff --git a/app/views/admin/product_import/_import_review.html.haml b/app/views/admin/product_import/_import_review.html.haml index 20cb48b769..9015c31bb8 100644 --- a/app/views/admin/product_import/_import_review.html.haml +++ b/app/views/admin/product_import/_import_review.html.haml @@ -1,83 +1,106 @@ -%h5 Import validation overview +%h5= t('admin.product_import.import.validation_overview') %br --#%div.panel-section --# %div.panel-header --# %div.header-caret --# %div.header-icon.info --# %i.fa.fa-info-circle --# %div.header-count --# %strong.existing-count= @importer.total_supplier_products --# %div.header-description --# Existing products in referenced enterprise(s) --# -#%div.panel-content{ng: {hide: '!active'}} --# -# Content goes here +%div{ng: {controller: 'ImportFeedbackCtrl'}} -%div.panel-section{ng: {controller: 'DropdownPanelsCtrl', init: "count = #{@importer.item_count}"}} - %div.panel-header{ng: {click: 'togglePanel()', class: '{active: active && count}'}} - %div.header-caret - %i{ng: {class: "{'icon-chevron-down': active, 'icon-chevron-right': !active}", hide: 'count == 0'}} - %div.header-icon.success - %i.fa.fa-info-circle.info - %div.header-count - %strong.item-count= @importer.item_count - %div.header-description - Entries found in imported file - %div.panel-content{ng: {hide: '!active || count == 0'}} - = render 'entries_table', entries: @importer.all_entries + %div.panel-section{ng: {controller: 'DropdownPanelsCtrl'}} + %div.panel-header{ng: {click: 'togglePanel()', class: '{active: active && count((entries | entriesFilterValid:"all"))}'}} + %div.header-caret + %i{ng: {class: "{'icon-chevron-down': active, 'icon-chevron-right': !active}", hide: 'count((entries | entriesFilterValid:"all")) == 0'}} + %div.header-icon.success + %i.fa.fa-info-circle.info + %div.header-count + %strong.item-count + {{ count((entries | entriesFilterValid:"all")) }} + %div.header-description + = t('admin.product_import.import.entries_found') + %div.panel-content{ng: {hide: '!active || count((entries | entriesFilterValid:"all")) == 0'}} + = render 'entries_table', entries: 'all' -%div.panel-section{ng: {controller: 'DropdownPanelsCtrl', init: "count = #{@importer.invalid_count}"}} - %div.panel-header{ng: {click: 'togglePanel()', class: '{active: active && count}'}} - %div.header-caret - %i{ng: {class: "{'icon-chevron-down': active, 'icon-chevron-right': !active}", hide: 'count == 0'}} - %div.header-icon.warning - %i.fa.fa-warning - %div.header-count - %strong.invalid-count= @importer.invalid_count - %div.header-description - Items contain errors and will not be imported - %div.panel-content{ng: {hide: '!active || count == 0'}} - = render 'errors_list' - %br - = render 'entries_table', entries: @importer.invalid_entries + %div.panel-section{ng: {controller: 'DropdownPanelsCtrl', hide: 'count((entries | entriesFilterValid:"invalid")) == 0'}} + %div.panel-header{ng: {click: 'togglePanel()', class: '{active: active && count((entries | entriesFilterValid:"invalid"))}'}} + %div.header-caret + %i{ng: {class: "{'icon-chevron-down': active, 'icon-chevron-right': !active}", hide: 'count((entries | entriesFilterValid:"invalid")) == 0'}} + %div.header-icon.warning + %i.fa.fa-warning + %div.header-count + %strong.invalid-count + {{ count((entries | entriesFilterValid:"invalid")) }} + %div.header-description + = t('admin.product_import.import.entries_with_errors') + %div.panel-content{ng: {hide: '!active || count((entries | entriesFilterValid:"invalid")) == 0'}} + = render 'errors_list' + %br + = render 'entries_table', entries: 'invalid' -%div.panel-section{ng: {controller: 'DropdownPanelsCtrl', init: "count = #{@importer.products_create_count}"}} - %div.panel-header{ng: {click: 'togglePanel()', class: '{active: active && count}'}} - %div.header-caret - %i{ng: {class: "{'icon-chevron-down': active, 'icon-chevron-right': !active}", hide: 'count == 0'}} - %div.header-icon.success - %i.fa.fa-check-circle - %div.header-count - %strong.create-count= @importer.products_create_count - %div.header-description - Products will be created - %div.panel-content{ng: {hide: '!active || count == 0'}} - = render 'entries_table', entries: @importer.products_to_create + %div.panel-section{ng: {controller: 'DropdownPanelsCtrl', hide: 'count((entries | entriesFilterValid:"create_product")) == 0'}} + %div.panel-header{ng: {click: 'togglePanel()', class: '{active: active && count((entries | entriesFilterValid:"create_product"))}'}} + %div.header-caret + %i{ng: {class: "{'icon-chevron-down': active, 'icon-chevron-right': !active}", hide: 'count((entries | entriesFilterValid:"create_product")) == 0'}} + %div.header-icon.success + %i.fa.fa-check-circle + %div.header-count + %strong.create-count + {{ count((entries | entriesFilterValid:"create_product")) }} + %div.header-description + = t('admin.product_import.import.products_to_create') + %div.panel-content{ng: {hide: '!active || count((entries | entriesFilterValid:"create_product")) == 0'}} + = render 'entries_table', entries: 'create_product' -%div.panel-section{ng: {controller: 'DropdownPanelsCtrl', init: "count = #{@importer.products_update_count}"}} - %div.panel-header{ng: {click: 'togglePanel()', class: '{active: active && count}'}} - %div.header-caret - %i{ng: {class: "{'icon-chevron-down': active, 'icon-chevron-right': !active}", hide: 'count == 0'}} - %div.header-icon.success - %i.fa.fa-check-circle - %div.header-count - %strong.update-count= @importer.products_update_count - %div.header-description - Products will be updated - %div.panel-content{ng: {hide: '!active || count == 0'}} - = render 'entries_table', entries: @importer.products_to_update + %div.panel-section{ng: {controller: 'DropdownPanelsCtrl', hide: 'count((entries | entriesFilterValid:"update_product")) == 0'}} + %div.panel-header{ng: {click: 'togglePanel()', class: '{active: active && count((entries | entriesFilterValid:"update_product"))}'}} + %div.header-caret + %i{ng: {class: "{'icon-chevron-down': active, 'icon-chevron-right': !active}", hide: 'count((entries | entriesFilterValid:"update_product")) == 0'}} + %div.header-icon.success + %i.fa.fa-check-circle + %div.header-count + %strong.update-count + {{ count((entries | entriesFilterValid:"update_product")) }} + %div.header-description + = t('admin.product_import.import.products_to_update') + %div.panel-content{ng: {hide: '!active || count((entries | entriesFilterValid:"update_product")) == 0'}} + = render 'entries_table', entries: 'update_product' -%div.panel-section{ng: {controller: 'ImportOptionsFormCtrl', hide: 'resetTotal == 0'}} - %div.panel-header - %div.header-caret - %div.header-icon.info - %i.fa.fa-info-circle - %div.header-count - %strong.reset-count - {{resetTotal}} - %div.header-description - Existing products will have their stock reset to zero - -#%div.panel-content{ng: {hide: '!active'}} - -# Content goes here + %div.panel-section{ng: {controller: 'DropdownPanelsCtrl', hide: 'count((entries | entriesFilterValid:"create_inventory")) == 0'}} + %div.panel-header{ng: {click: 'togglePanel()', class: '{active: active && count((entries | entriesFilterValid:"create_inventory"))}'}} + %div.header-caret + %i{ng: {class: "{'icon-chevron-down': active, 'icon-chevron-right': !active}", hide: 'count((entries | entriesFilterValid:"create_inventory")) == 0'}} + %div.header-icon.success + %i.fa.fa-check-circle + %div.header-count + %strong.inv-create-count + {{ count((entries | entriesFilterValid:"create_inventory")) }} + %div.header-description + = t('admin.product_import.import.inventory_to_create') + %div.panel-content{ng: {hide: '!active || count((entries | entriesFilterValid:"create_inventory")) == 0'}} + = render 'entries_table', entries: 'create_inventory' -%br.panels.clearfix \ No newline at end of file + %div.panel-section{ng: {controller: 'DropdownPanelsCtrl', hide: 'count((entries | entriesFilterValid:"update_inventory")) == 0'}} + %div.panel-header{ng: {click: 'togglePanel()', class: '{active: active && count((entries | entriesFilterValid:"update_inventory"))}'}} + %div.header-caret + %i{ng: {class: "{'icon-chevron-down': active, 'icon-chevron-right': !active}", hide: 'count((entries | entriesFilterValid:"update_inventory")) == 0'}} + %div.header-icon.success + %i.fa.fa-check-circle + %div.header-count + %strong.inv-update-count + {{ count((entries | entriesFilterValid:"update_inventory")) }} + %div.header-description + = t('admin.product_import.import.inventory_to_update') + %div.panel-content{ng: {hide: '!active || count((entries | entriesFilterValid:"update_inventory")) == 0'}} + = render 'entries_table', entries: 'update_inventory' + + %div.panel-section{ng: {controller: 'ImportOptionsFormCtrl', hide: 'resetTotal == 0'}} + %div.panel-header + %div.header-caret + %div.header-icon.info + %i.fa.fa-info-circle + %div.header-count + %strong.reset-count + {{ resetTotal }} + %div.header-description + -if @import_into == 'inventories' + = t('admin.product_import.import.inventory_to_reset') + - else + = t('admin.product_import.import.products_to_reset') + + %br.panels.clearfix diff --git a/app/views/admin/product_import/_options_form.html.haml b/app/views/admin/product_import/_options_form.html.haml index da0e2df055..5e4383de35 100644 --- a/app/views/admin/product_import/_options_form.html.haml +++ b/app/views/admin/product_import/_options_form.html.haml @@ -1,35 +1,66 @@ -%table.import-settings{ng: {controller: 'ImportOptionsFormCtrl', init: "supplierId = #{supplier_id}; resetCount = #{@importer.products_to_reset[supplier_id][:reset_count]}"}} - %tr +%table.import-settings{ng: {controller: 'ImportOptionsFormCtrl', init: "supplierId = #{supplier_id}; initForm()"}} + %tr.import-into %td.description - Remove absent products? + Import Into: %td - = check_box_tag "settings[#{supplier_id}][reset_all_absent]", 1, false, :'ng-model' => 'resetAbsent', :'ng-change' => 'toggleResetAbsent()' %td - %tr + = select_tag "settings[#{supplier_id}][import_into]", options_for_select({"Product List" => :product_list, "Inventories" => :inventories}), {class: 'select2 fullwidth select2-no-search', 'ng-model' => "settings[#{supplier_id}]['import_into']", 'ng-change' => "updateImportInto()"} + %td + + %tr.stock-level.inventory{ng: {show: 'import_into == "inventories"'}} %td.description - Set default stock level + = t('admin.product_import.import.default_stock') %td - = select_tag "settings[#{supplier_id}][defaults][on_hand][mode]", options_for_select({"Don't overwrite" => :overwrite_none, "Overwrite all" => :overwrite_all, "Overwrite if empty" => :overwrite_empty}), {class: 'select2 fullwidth'} + = check_box_tag "settings[#{supplier_id}][defaults][count_on_hand][active]", 1, false, 'ng-model' => "settings[#{supplier_id}]['defaults']['count_on_hand']['active']" %td - = number_field_tag "settings[#{supplier_id}][defaults][on_hand][value]", 0 - %tr + = select_tag "settings[#{supplier_id}][defaults][count_on_hand][mode]", options_for_select({"#{t('admin.product_import.import.overwrite_all')}" => :overwrite_all, "#{t('admin.product_import.import.overwrite_empty')}" => :overwrite_empty}), {class: 'select2 fullwidth select2-no-search', 'ng-model' => "settings[#{supplier_id}]['defaults']['count_on_hand']['mode']", 'ng-disabled' => "!settings[#{supplier_id}]['defaults']['count_on_hand']['active']"} + %td + = number_field_tag "settings[#{supplier_id}][defaults][count_on_hand][value]", 0, 'ng-disabled' => "!settings[#{supplier_id}]['defaults']['count_on_hand']['active']", 'ng-model' => "settings[#{supplier_id}]['defaults']['count_on_hand']['value']" + + %tr.stock-level.productlist{ng: {show: 'import_into == "product_list"'}} %td.description - Set default tax category + = t('admin.product_import.import.default_stock') %td - = select_tag "settings[#{supplier_id}][defaults][tax_category_id][mode]", options_for_select({"Don't overwrite" => :overwrite_none, "Overwrite all" => :overwrite_all, "Overwrite if empty" => :overwrite_empty}), {class: 'select2 fullwidth'} + = check_box_tag "settings[#{supplier_id}][defaults][on_hand][active]", 1, false, 'ng-model' => "settings[#{supplier_id}]['defaults']['on_hand']['active']" %td - = select_tag "settings[#{supplier_id}][defaults][tax_category_id][value]", options_for_select(@tax_categories.map {|tc| [tc.name, tc.id]}), {prompt: 'None', class: 'select2 fullwidth'} - %tr + = select_tag "settings[#{supplier_id}][defaults][on_hand][mode]", options_for_select({"#{t('admin.product_import.import.overwrite_all')}" => :overwrite_all, "#{t('admin.product_import.import.overwrite_empty')}" => :overwrite_empty}), {class: 'select2 fullwidth select2-no-search', 'ng-model' => "settings[#{supplier_id}]['defaults']['on_hand']['mode']", 'ng-disabled' => "!settings[#{supplier_id}]['defaults']['on_hand']['active']"} + %td + = number_field_tag "settings[#{supplier_id}][defaults][on_hand][value]", 0, 'ng-model' => "settings[#{supplier_id}]['defaults']['on_hand']['value']", 'ng-disabled' => "!settings[#{supplier_id}]['defaults']['on_hand']['active']" + + %tr.tax-category{ng: {show: 'import_into == "product_list"'}} %td.description - Set default shipping category + = t('admin.product_import.import.default_tax_cat') %td - = select_tag "settings[#{supplier_id}][defaults][shipping_category_id][mode]", options_for_select({"Don't overwrite" => :overwrite_none, "Overwrite all" => :overwrite_all, "Overwrite if empty" => :overwrite_empty}), {class: 'select2 fullwidth'} + = check_box_tag "settings[#{supplier_id}][defaults][tax_category_id][active]", 1, false, 'ng-model' => "settings[#{supplier_id}]['defaults']['tax_category_id']['active']" %td - = select_tag "settings[#{supplier_id}][defaults][shipping_category_id][value]", options_for_select(@shipping_categories.map {|sc| [sc.name, sc.id]}), {prompt: 'None', class: 'select2 fullwidth'} - %tr + = select_tag "settings[#{supplier_id}][defaults][tax_category_id][mode]", options_for_select({"#{t('admin.product_import.import.overwrite_all')}" => :overwrite_all, "#{t('admin.product_import.import.overwrite_empty')}" => :overwrite_empty}), {class: 'select2 fullwidth select2-no-search', 'ng-model' => "settings[#{supplier_id}]['defaults']['tax_category_id']['mode']", 'ng-disabled' => "!settings[#{supplier_id}]['defaults']['tax_category_id']['active']"} + %td + = select_tag "settings[#{supplier_id}][defaults][tax_category_id][value]", options_for_select(@tax_categories.map {|tc| [tc.name, tc.id]}), {prompt: 'None', class: 'select2 fullwidth select2-no-search', 'ng-model' => "settings[#{supplier_id}]['defaults']['tax_category_id']['value']", 'ng-disabled' => "!settings[#{supplier_id}]['defaults']['tax_category_id']['active']"} + + %tr.shipping-category{ng: {show: 'import_into == "product_list"'}} %td.description - Set default available date + = t('admin.product_import.import.default_shipping_cat') + %td + = check_box_tag "settings[#{supplier_id}][defaults][shipping_category_id][active]", 1, false, 'ng-model' => "settings[#{supplier_id}]['defaults']['shipping_category_id']['active']" + %td + = select_tag "settings[#{supplier_id}][defaults][shipping_category_id][mode]", options_for_select({"#{t('admin.product_import.import.overwrite_all')}" => :overwrite_all, "#{t('admin.product_import.import.overwrite_empty')}" => :overwrite_empty}), {class: 'select2 fullwidth select2-no-search', 'ng-model' => "settings[#{supplier_id}]['defaults']['shipping_category_id']['mode']", 'ng-disabled' => "!settings[#{supplier_id}]['defaults']['shipping_category_id']['active']"} + %td + = select_tag "settings[#{supplier_id}][defaults][shipping_category_id][value]", options_for_select(@shipping_categories.map {|sc| [sc.name, sc.id]}), {prompt: 'None', class: 'select2 fullwidth select2-no-search', 'ng-model' => "settings[#{supplier_id}]['defaults']['shipping_category_id']['value']", 'ng-disabled' => "!settings[#{supplier_id}]['defaults']['shipping_category_id']['active']"} + + %tr.available-date{ng: {show: 'import_into == "product_list"'}} + %td.description + = t('admin.product_import.import.default_available_date') + %td + = check_box_tag "settings[#{supplier_id}][defaults][available_on][active]", 1, false, 'ng-model' => "settings[#{supplier_id}]['defaults']['available_on']['active']" + %td + = select_tag "settings[#{supplier_id}][defaults][available_on][mode]", options_for_select({"#{t('admin.product_import.import.overwrite_all')}" => :overwrite_all, "#{t('admin.product_import.import.overwrite_empty')}" => :overwrite_empty}), {class: 'select2 fullwidth select2-no-search', 'ng-model' => "settings[#{supplier_id}]['defaults']['available_on']['mode']", 'ng-disabled' => "!settings[#{supplier_id}]['defaults']['available_on']['active']"} + %td + = text_field_tag "settings[#{supplier_id}][defaults][available_on][value]", nil, {class: 'datepicker', placeholder: 'Today', 'ng-model' => "settings[#{supplier_id}]['defaults']['available_on']['value']", 'ng-disabled' => "!settings[#{supplier_id}]['defaults']['available_on']['active']"} + + %tr.reset-absent + %td.description + = t('admin.product_import.import.reset_absent?') + %td + = check_box_tag "settings[#{supplier_id}][reset_all_absent]", 1, false, :'ng-model' => "settings[#{supplier_id}]['reset_all_absent']", :'ng-change' => "toggleResetAbsent('#{supplier_id}')" %td - = select_tag "settings[#{supplier_id}][defaults][available_on][mode]", options_for_select({"Don't overwrite" => :overwrite_none, "Overwrite all" => :overwrite_all, "Overwrite if empty" => :overwrite_empty}), {class: 'select2 fullwidth'} %td - = text_field_tag "settings[#{supplier_id}][defaults][available_on][value]", nil, {class: 'datepicker', placeholder: 'Today'} diff --git a/app/views/admin/product_import/_save_results.html.haml b/app/views/admin/product_import/_save_results.html.haml new file mode 100644 index 0000000000..9ab9652ac1 --- /dev/null +++ b/app/views/admin/product_import/_save_results.html.haml @@ -0,0 +1,63 @@ + +%h5= t('admin.product_import.save.final_results') +%br + +%div.post-save-results + + %p{ng: {show: 'updates.products_created'}} + %i.fa{ng: {class: "{'fa-info-circle': updates.products_created == 0, 'fa-check-circle': updates.products_created > 0}"}} + %strong.created-count + {{ updates.products_created }} + = t('admin.product_import.save.products_created') + + %p{ng: {show: 'updates.products_updated'}} + %i.fa{ng: {class: "{'fa-info-circle': updates.products_updated == 0, 'fa-check-circle': updates.products_updated > 0}"}} + %strong.updated-count + {{ updates.products_updated }} + = t('admin.product_import.save.products_updated') + + %p{ng: {show: 'updates.inventory_created'}} + %i.fa{ng: {class: "{'fa-info-circle': updates.inventory_created == 0, 'fa-check-circle': updates.inventory_created > 0}"}} + %strong.inv-created-count + {{ updates.inventory_created }} + = t('admin.product_import.save.inventory_created') + + %p{ng: {show: 'updates.inventory_updated'}} + %i.fa{ng: {class: "{'fa-info-circle': updates.inventory_updated == 0, 'fa-check-circle': updates.inventory_updated > 0}"}} + %strong.inv-updated-count + {{ updates.inventory_updated }} + = t('admin.product_import.save.inventory_updated') + + %p{ng: {show: 'updates.products_reset'}} + %i.fa.fa-info-circle + %strong.reset-count + {{ updates.products_reset }} + - if @import_into == 'inventories' + = t('admin.product_import.save.inventory_reset') + - else + = t('admin.product_import.save.products_reset') + + %br + + %p{ng: {show: 'update_errors.length == 0'}} + = t('admin.product_import.save.all_saved') + + %div{ng: {show: 'update_errors.length > 0'}} + %p {{ updated_total }} #{t('admin.product_import.save.some_saved')} + %br + + %h5= t('admin.product_import.save.save_errors') + + %p.save-error{ng: {repeat: 'error in update_errors'}} +  -  {{ error }} + + %br + %div{ng: {show: 'updated_total > 0'}} + %a.button.view{href: main_app.admin_inventory_path, ng: {show: 'updates.inventory_created > 0 || updates.inventory_updated > 0'}} + = t('admin.product_import.save.view_inventory') + + %a.button.view{href: admin_products_path + '?latest_import=true', ng: {show: 'updates.products_created > 0 || updates.products_updated > 0'}} + = t('admin.product_import.save.view_products') + + %a.button{href: main_app.admin_product_import_path} + = t('admin.back') diff --git a/app/views/admin/product_import/_upload_form.html.haml b/app/views/admin/product_import/_upload_form.html.haml index 46e05adfdb..008f027139 100644 --- a/app/views/admin/product_import/_upload_form.html.haml +++ b/app/views/admin/product_import/_upload_form.html.haml @@ -1,9 +1,11 @@ -%h5 Select a spreadsheet to upload -%br -= form_tag main_app.admin_product_import_path, multipart: true do - = file_field_tag :file +%div{ng: {app: 'admin.productImport'}} + + %h5= t('admin.product_import.index.select_file') %br - %br - = submit_tag "Import" - %br - %br \ No newline at end of file + = form_tag main_app.admin_product_import_path, multipart: true, class: 'product-import' do + %label #{t('admin.product_import.index.spreadsheet')} + %br + = file_field_tag :file + %br + %br + = submit_tag "#{t('admin.product_import.index.upload')}" diff --git a/app/views/admin/product_import/guide.html.haml b/app/views/admin/product_import/guide.html.haml new file mode 100644 index 0000000000..aab0350606 --- /dev/null +++ b/app/views/admin/product_import/guide.html.haml @@ -0,0 +1,69 @@ +- content_for :page_title do + Product Import Guide + +- content_for :page_actions do + %div.toolbar{ 'data-hook' => "toolbar" } + %ul.actions.header-action-links.inline-menu + %li + = button_link_to 'Back to Import', main_app.admin_product_import_path + += render partial: 'spree/admin/shared/product_sub_menu' + +.product-import-introduction + + %h5 Spreadsheet Columns + + = render 'admin/product_import/guide/columns' + + %h5 Weight, volume, or single items + + %p + When creating a product you can specify a number of options. If the product is measured + by weight, unit_value should be set to a number and unit_type should be set to either + g for grams, Kg for kilograms, or T for tonnes. + + %p + For items such as liquids, the unit_type can be ml for millilitres, L for litres or + Kl for kilolitres. + + %p + For other items that are sold in different units such as "a bunch of carrots", you can + enter 1 for units, leave unit_type blank, and enter a name in the variant_unit_name + column such as "loaf" or "bunch". + + %h6 Example spreadsheet data: + + = render 'admin/product_import/guide/units' + + + %h5 Product Variants + + %p + Variants are distinguished by the units and display_name fields and grouped by product name. + The example below shows a salad that comes in 500g and 750g variants, and a cake that comes in + multiple flavours. + + %h6 Example spreadsheet data: + + = render 'admin/product_import/guide/variants' + + + %h5 Category Values + + %p + Listed below are the available Product categories, as well as Tax and Shipping categories. + + %h6 Product Categories + + - @product_categories.each do |pc| + %span.category= pc + + %h6 Tax Categories + + - @tax_categories.each do |tc| + %span.category= tc + + %h6 Shipping Categories + + - @shipping_categories.each do |sc| + %span.category= sc diff --git a/app/views/admin/product_import/guide/_columns.html.haml b/app/views/admin/product_import/guide/_columns.html.haml new file mode 100644 index 0000000000..d5aa454670 --- /dev/null +++ b/app/views/admin/product_import/guide/_columns.html.haml @@ -0,0 +1,93 @@ +%table.product-import-columns + %thead + %tr + %th Column Title + %th Required + %th Examples + %th Description + %th Notes + %tbody + %tr + %td + %strong name + %td Yes + %td Potatoes + %td The name of the product + %td + %tr + %td + %strong supplier + %td Yes + %td Pennylane Farm + %td The name of the product's supplier + %td + %tr + %td + %strong category + %td Yes + %td Vegetables + %td The product category + %td See below for a list of available categories + %tr + %td + %strong on_hand + %td Yes + %td 15 + %td The amount currently in stock + %td + %tr + %td + %strong price + %td Yes + %td 2.50 + %td The price of the product + %td + %tr + %td + %strong units + %td Yes + %td 750 + %td The weight or volume value. + %td + %tr + %td + %strong unit_type + %td Yes + %td g + %td The unit type, i.e. g for grams, Kg for Kilograms, ml for millilitres. Can be blank (see notes). + %td If unit_type is left blank, a variant_unit_name must be given. + %tr + %td + %strong variant_unit_name + %td Maybe + %td bunch + %td If the product is sold as an item such as "bunch", "loaf" or "case", that goes here. + %td Used for products that don't use a unit_type for weight or volume. + %tr + %td + %strong display_name + %td No + %td Orange + %td Used to name product variants, if they have a distinct name. + %td + %tr + %td + %strong on_demand + %td No + %td 1 + %td Flag the product as "made on demand". The product will not use stock levels. + %td 1 for active, 0 for disabled, or leave blank to ignore. + %tr + %td + %strong tax_category + %td No + %td (Various, see notes) + %td Sets the product tax category + %td See below for a list of available categories + %tr + %td + %strong shipping_category + %td No + %td (Various, see notes) + %td Sets the product shipping category + %td See below for a list of available categories diff --git a/app/views/admin/product_import/guide/_units.html.haml b/app/views/admin/product_import/guide/_units.html.haml new file mode 100644 index 0000000000..07161bdb1c --- /dev/null +++ b/app/views/admin/product_import/guide/_units.html.haml @@ -0,0 +1,54 @@ +%table + %thead + %tr + %th + %th name + %th category + %th supplier + %th on_hand + %th price + %th units + %th unit_type + %th variant_unit_name + %tbody + %tr + %td 1 + %td Salad Bag + %td Salads + %td Sue's Salads + %td 26 + %td 3.50 + %td 500 + %td g + %td + %tr + %td 2 + %td Fruit Juice + %td Drinks + %td Country Juices + %td 12 + %td 3.50 + %td 300 + %td ml + %td + %tr + %td 3 + %td Potatoes + %td Vegetables + %td Fernwell Farm + %td 67 + %td 4.20 + %td 1 + %td kg + %td + + %tr + %td 3 + %td Wholemeal Bread + %td Baked goods + %td Tim's Bakery + %td 66 + %td 3.00 + %td 1 + %td + %td loaf diff --git a/app/views/admin/product_import/guide/_variants.html.haml b/app/views/admin/product_import/guide/_variants.html.haml new file mode 100644 index 0000000000..dfd95879c1 --- /dev/null +++ b/app/views/admin/product_import/guide/_variants.html.haml @@ -0,0 +1,54 @@ +%table + %thead + %tr + %th + %th name + %th category + %th supplier + %th on_hand + %th price + %th units + %th unit_type + %th display_name + %tbody + %tr + %td 1 + %td Salad Bag + %td Salads + %td Sue's Salads + %td 26 + %td 3.50 + %td 500 + %td g + %td + %tr + %td 2 + %td Salad Bag + %td Salads + %td Sue's Salads + %td 44 + %td 5.50 + %td 750 + %td g + %td + %tr + %td 3 + %td Cake + %td Baked goods + %td Tim's Cakes + %td 10 + %td 4 + %td 500 + %td g + %td Banana and Walnut + %tr + %td 4 + %td Cake + %td Baked goods + %td Tim's Cakes + %td 18 + %td 4 + %td 500 + %td g + %td Carrot + diff --git a/app/views/admin/product_import/import.html.haml b/app/views/admin/product_import/import.html.haml index 74422c162f..bb8c85b8e4 100644 --- a/app/views/admin/product_import/import.html.haml +++ b/app/views/admin/product_import/import.html.haml @@ -1,34 +1,74 @@ - content_for :page_title do - Product Import + #{t('admin.product_import.title')} = render partial: 'spree/admin/shared/product_sub_menu' -= form_tag main_app.admin_product_import_save_path, {'ng-app' => 'ofn.admin'} do +.import-wrapper{ng: {app: 'admin.productImport', controller: 'ImportFormCtrl', init: "supplier_product_counts = #{@importer.supplier_products.to_json}"}} - - if @importer.invalid_count && !@importer.has_valid_entries? - %h5 No valid entries found - %p There are no entries that can be saved + - if @importer.item_count == 0 #and @importer.invalid_count + %h5 + = t('admin.product_import.import.no_valid_entries') + %p + = t('admin.product_import.import.none_to_save') %br - - = render 'import_options' if @importer.has_valid_entries? - - = render 'import_review' - - - if @importer.has_valid_entries? - - if @importer.invalid_count > 0 - %br - %h5 Imported file contains some invalid entries - %p Save valid entries for now and discard the others? - - else - %h5 No errors detected! - %p Save all imported products? - %br - = hidden_field_tag :filepath, @filepath - = submit_tag "Save" - %a.button{href: main_app.admin_product_import_path} Cancel - - else - %br - %a.button{href: main_app.admin_product_import_path} Cancel + .settings-section{ng: {show: 'step == "settings"'}} + = render 'import_options' if @importer.table_headings + %br + %a.button.proceed{href: '', ng: {click: 'confirmSettings()'}} + = t('admin.product_import.import.proceed') + %a.button{href: main_app.admin_product_import_path} #{t('admin.cancel')} + .progress-interface{ng: {show: 'step == "import"'}} + %span.filename + = @original_filename + %span.percentage + ({{ percentage }}) + .progress-bar + %span.progress-track{class: 'ng-binding', style: "width:{{percentage}}"} + %button.start_import{ng: {click: 'start()', disabled: 'started', init: "item_count = #{@importer.item_count}; import_url = '#{main_app.admin_product_import_process_async_path}'; filepath = '#{@filepath}'; import_into = '#{@import_into}'"}} + = t('admin.product_import.index.import') + %button.review{ng: {click: 'viewResults()', disabled: '!finished'}} + = t('admin.product_import.import.review') + %p.red + {{ exception }} + = form_tag false, {class: 'product-import', name: 'importForm', 'ng-show' => 'step == "results"'} do + + = render 'import_review' if @importer.table_headings + + %div{ng: {controller: 'ImportFeedbackCtrl', show: 'count((entries | entriesFilterValid:"valid")) > 0'}} + %div{ng: {if: 'count((entries | entriesFilterValid:"invalid")) > 0'}} + %br + %h5= t('admin.product_import.import.some_invalid_entries') + %p= t('admin.product_import.import.save_valid?') + %div{ng: {show: 'count((entries | entriesFilterValid:"invalid")) == 0'}} + %br + %h5= t('admin.product_import.import.no_errors') + %p= t('admin.product_import.import.save_all_imported?') + %br + = hidden_field_tag :filepath, @filepath + = hidden_field_tag "settings[import_into]", @import_into + + %a.button.proceed{href: '', ng: {click: 'acceptResults()'}}= t('admin.product_import.import.proceed') + + %a.button{href: main_app.admin_product_import_path}= t('admin.cancel') + + %div{ng: {controller: 'ImportFeedbackCtrl', show: 'count((entries | entriesFilterValid:"valid")) == 0'}} + %br + %a.button{href: main_app.admin_product_import_path}= t('admin.cancel') + + .progress-interface{ng: {show: 'step == "save"'}} + %span.filename + #{t('admin.product_import.import.save_imported')} ({{ percentage }}) + .progress-bar{} + %span.progress-track{ng: {style: "{'width':percentage}"}} + %button.start_save{ng: {click: 'start()', disabled: 'started', init: "item_count = #{@importer.item_count}; save_url = '#{main_app.admin_product_import_save_async_path}'; reset_url = '#{main_app.admin_product_import_reset_async_path}'; filepath = '#{@filepath}'; import_into = '#{@import_into}'"}} + = t('admin.product_import.import.save') + %button.view_results{ng: {click: 'finalResults()', disabled: '!finished'}} + = t('admin.product_import.import.results') + %p.red + {{ exception }} + + .save-results{ng: {show: 'step == "complete"'}} + = render 'save_results' diff --git a/app/views/admin/product_import/index.html.haml b/app/views/admin/product_import/index.html.haml index c904b6a70f..f2e8e5208b 100644 --- a/app/views/admin/product_import/index.html.haml +++ b/app/views/admin/product_import/index.html.haml @@ -1,6 +1,12 @@ - content_for :page_title do - Product Import + #{t('admin.product_import.title')} -= render :partial => 'spree/admin/shared/product_sub_menu' +- content_for :page_actions do + %div.toolbar{ 'data-hook' => "toolbar" } + %ul.actions.header-action-links.inline-menu + %li + = button_link_to 'View Guide', main_app.admin_product_import_guide_path + += render partial: 'spree/admin/shared/product_sub_menu' = render 'upload_form' diff --git a/app/views/admin/product_import/save.html.haml b/app/views/admin/product_import/save.html.haml index 06d11e5f35..4d88bef979 100644 --- a/app/views/admin/product_import/save.html.haml +++ b/app/views/admin/product_import/save.html.haml @@ -1,38 +1,63 @@ - content_for :page_title do - Product Import + #{t('admin.product_import.title')} -= render :partial => 'spree/admin/shared/product_sub_menu' += render partial: 'spree/admin/shared/product_sub_menu' -%h5 Import final results +%h5= t('admin.product_import.save.final_results') %br -%div.post-save-results{ng: {app: 'ofn.admin'}} +%div.post-save-results{ng: {app: 'admin.productImport'}} - %p - %i.fa{ng: {class: "{'fa-info-circle': #{@importer.products_created_count} == 0, 'fa-check-circle': #{@importer.products_created_count} != 0}"}} - %strong.created-count= @importer.products_created_count - Products created + - if @importer.products_created_count > 0 + %p + %i.fa{ng: {class: "{'fa-info-circle': #{@importer.products_created_count} == 0, 'fa-check-circle': #{@importer.products_created_count} != 0}"}} + %strong.created-count= @importer.products_created_count + = t('admin.product_import.save.products_created') - %p - %i.fa{ng: {class: "{'fa-info-circle': #{@importer.products_updated_count} == 0, 'fa-check-circle': #{@importer.products_updated_count} != 0}"}} - %strong.updated-count= @importer.products_updated_count - Products updated + - if @importer.products_updated_count > 0 + %p + %i.fa{ng: {class: "{'fa-info-circle': #{@importer.products_updated_count} == 0, 'fa-check-circle': #{@importer.products_updated_count} != 0}"}} + %strong.updated-count= @importer.products_updated_count + = t('admin.product_import.save.products_updated') + + - if @importer.inventory_created_count > 0 + %p + %i.fa{ng: {class: "{'fa-info-circle': #{@importer.inventory_created_count} == 0, 'fa-check-circle': #{@importer.inventory_created_count} != 0}"}} + %strong.inv-created-count= @importer.inventory_created_count + = t('admin.product_import.save.inventory_created') + + - if @importer.inventory_updated_count > 0 + %p + %i.fa{ng: {class: "{'fa-info-circle': #{@importer.inventory_updated_count} == 0, 'fa-check-circle': #{@importer.inventory_updated_count} != 0}"}} + %strong.inv-updated-count= @importer.inventory_updated_count + = t('admin.product_import.save.inventory_updated') - if @importer.products_reset_count > 0 %p - %i.fa.fa-check-circle + %i.fa.fa-info-circle %strong.reset-count= @importer.products_reset_count - Products had stock level reset to zero + - if @import_into == 'inventories' + = t('admin.product_import.save.inventory_reset') + - else + = t('admin.product_import.save.products_reset') %br - if @importer.errors.count == 0 - %p All #{@importer.total_saved_count} items saved successfully + %p= t('admin.product_import.save.all_saved', { num: "#{@importer.total_saved_count}" }) - else - %h5 Save errors + %p= t('admin.product_import.save.total_saved', { num: "#{@importer.total_saved_count}" }) + %br + %h5= t('admin.product_import.save.save_errors') - @importer.errors.full_messages.each do |error| %p.save-error  -  #{error} %br - %a.button{href: main_app.admin_product_import_path} Back + - if @importer.total_saved_count > 0 + - if @import_into == 'inventories' + %a.button{href: main_app.admin_inventory_path}= t('admin.product_import.save.view_inventory') + - else + %a.button{href: admin_products_path + '?latest_import=true'}= t('admin.product_import.save.view_products') + + %a.button{href: main_app.admin_product_import_path}= t('admin.back') diff --git a/app/views/admin/variant_overrides/_filters.html.haml b/app/views/admin/variant_overrides/_filters.html.haml index b7a1c948ba..4126be9317 100644 --- a/app/views/admin/variant_overrides/_filters.html.haml +++ b/app/views/admin/variant_overrides/_filters.html.haml @@ -1,17 +1,22 @@ .filters.sixteen.columns.alpha.omega .filter.four.columns.alpha - %label{ :for => 'query', ng: {class: '{disabled: !hub_id}'} }=t('admin.quick_search') + %label{for: 'query', ng: {class: '{disabled: !hub_id}'} }=t('admin.quick_search') %br - %input.fullwidth{ :type => "text", :id => 'query', ng: { model: 'query', disabled: '!hub_id'} } - .two.columns   - .filter_select.four.columns - %label{ :for => 'hub_id', ng: { bind: "hub_id ? '#{t('admin.shop')}' : '#{t('admin.variant_overrides.index.select_a_shop')}'" } } + %input.fullwidth{type: "text", id: 'query', ng: {model: 'query', disabled: '!hub_id'} } + .one.columns   + .filter_select.three.columns + %label{for: 'hub_id', ng: {bind: "hub_id ? '#{t('admin.shop')}' : '#{t('admin.variant_overrides.index.select_a_shop')}'" } } %br - %select.select2.fullwidth#hub_id{ 'ng-model' => 'hub_id', name: 'hub_id', ng: { options: 'hub.id as hub.name for (id, hub) in hubs' } } - .filter_select.four.columns - %label{ :for => 'producer_filter', ng: {class: '{disabled: !hub_id}'} }=t('admin.producer') + %select.select2.fullwidth#hub_id{name: 'hub_id', ng: {model: 'hub_id', options: 'hub.id as hub.name for (id, hub) in hubs' } } + .filter_select.three.columns + %label{for: 'producer_filter', ng: {class: '{disabled: !hub_id}'} }=t('admin.producer') %br - %input.ofn-select2.fullwidth{ :id => 'producer_filter', type: 'number', data: 'producers', blank: "{id: 0, name: '#{t(:all)}'}", ng: { model: 'producerFilter', disabled: '!hub_id' } } + %input.ofn-select2.fullwidth{id: 'producer_filter', type: 'number', data: 'producers', blank: "{id: 0, name: '#{t(:all)}'}", ng: {model: 'producerFilter', disabled: '!hub_id' } } + .filter_select.three.columns + %label{ :for => 'import_date_filter', ng: {class: '{disabled: !hub_id}'} } #{t('admin.variant_overrides.index.import_date')} + %br + %select.fullwidth{id: 'import_date_filter', 'ofn-select2-min-search' => 5, ng: {model: 'importDateFilter', options: 'date.id as date.name for date in import_dates', disabled: '!hub_id', init: "import_dates = #{@import_dates}"} } + %options{value: '0', selected: 'selected'} #{t(:all)} -# .filter_select{ :class => "three columns" } -# %label{ :for => 'distributor_filter' }Hub -# %br diff --git a/app/views/admin/variant_overrides/_products.html.haml b/app/views/admin/variant_overrides/_products.html.haml index 711c52f6cf..1acbfd2924 100644 --- a/app/views/admin/variant_overrides/_products.html.haml +++ b/app/views/admin/variant_overrides/_products.html.haml @@ -13,6 +13,7 @@ %col.inheritance{ width: "5%", ng: { show: 'columns.inheritance.visible' } } %col.tags{ width: "30%", ng: { show: 'columns.tags.visible' } } %col.visibility{ width: "10%", ng: { show: 'columns.visibility.visible' } } + %col.visibility{ width: "10%", ng: { show: 'columns.import_date.visible' } } %thead %tr{ ng: { controller: "ColumnsCtrl" } } %th.producer{ ng: { show: 'columns.producer.visible' } }=t('admin.producer') @@ -25,6 +26,7 @@ %th.inheritance{ ng: { show: 'columns.inheritance.visible' } }=t('admin.variant_overrides.index.inherit?') %th.tags{ ng: { show: 'columns.tags.visible' } }=t('admin.tags') %th.visibility{ ng: { show: 'columns.visibility.visible' } }=t('admin.variant_overrides.index.hide') - %tbody{ ng: {repeat: 'product in filteredProducts = (products | hubPermissions:hubPermissions:hub_id | inventoryProducts:hub_id:views | attrFilter:{producer_id:producerFilter} | filter:query) | limitTo:productLimit' } } + %th.import_date{ ng: { show: 'columns.import_date.visible' } }=t('admin.variant_overrides.index.import_date') + %tbody{ ng: {repeat: 'product in filteredProducts = (products | hubPermissions:hubPermissions:hub_id | inventoryProducts:hub_id:views | attrFilter:{producer_id:producerFilter} | importDate:hub_id:importDateFilter | filter:query) | limitTo:productLimit' } } = render 'admin/variant_overrides/products_product' = render 'admin/variant_overrides/products_variants' diff --git a/app/views/admin/variant_overrides/_products_product.html.haml b/app/views/admin/variant_overrides/_products_product.html.haml index 0427333790..5f101e91a6 100644 --- a/app/views/admin/variant_overrides/_products_product.html.haml +++ b/app/views/admin/variant_overrides/_products_product.html.haml @@ -9,3 +9,4 @@ %td.inheritance{ ng: { show: 'columns.inheritance.visible' } } %td.tags{ ng: { show: 'columns.tags.visible' } } %td.visibility{ ng: { show: 'columns.visibility.visible' } } + %td.import_date{ ng: { show: 'columns.import_date.visible' } } diff --git a/app/views/admin/variant_overrides/_products_variants.html.haml b/app/views/admin/variant_overrides/_products_variants.html.haml index 94fc309798..7c530a1cb2 100644 --- a/app/views/admin/variant_overrides/_products_variants.html.haml +++ b/app/views/admin/variant_overrides/_products_variants.html.haml @@ -23,3 +23,5 @@ %td.visibility{ ng: { show: 'columns.visibility.visible' } } %button.icon-remove.hide.fullwidth{ :type => 'button', ng: { click: "setVisibility(hub_id,variant.id,false)" } } = t('admin.variant_overrides.index.hide') + %td.import_date{ ng: { show: 'columns.import_date.visible' } } + %span {{variantOverrides[hub_id][variant.id].import_date | date:"MMMM dd, yyyy HH:mm"}} \ No newline at end of file diff --git a/app/views/checkout/_billing.html.haml b/app/views/checkout/_billing.html.haml index bd590d63a5..07b0d0f251 100644 --- a/app/views/checkout/_billing.html.haml +++ b/app/views/checkout/_billing.html.haml @@ -19,25 +19,26 @@ %input{type: :checkbox, "ng-model" => "Checkout.default_bill_address"} = t :checkout_default_bill_address - = f.fields_for :bill_address, @order.bill_address do |ba| - .row - .small-12.columns - = validated_input t(:address), "order.bill_address.address1", "ofn-focus" => "accordion['billing']" - .row - .small-12.columns - = validated_input t(:address2), "order.bill_address.address2", required: false - .row - .small-6.columns - = validated_input t(:city), "order.bill_address.city" + %div{ "ng-controller" => "CountryCtrl" } + = f.fields_for :bill_address, @order.bill_address do |ba| + .row + .small-12.columns + = validated_input t(:address), "order.bill_address.address1", "ofn-focus" => "accordion['billing']" + .row + .small-12.columns + = validated_input t(:address2), "order.bill_address.address2", required: false + .row + .small-6.columns + = validated_input t(:city), "order.bill_address.city" - .small-6.columns - = validated_select t(:state), "order.bill_address.state_id", checkout_state_options(:billing) - .row - .small-6.columns - = validated_input t(:postcode), "order.bill_address.zipcode" + .small-6.columns + = validated_select t(:state), "order.bill_address.state_id", {}, {"ng-options" => "s.id as s.name for s in countriesById[order.bill_address.country_id].states"} + .row + .small-6.columns + = validated_input t(:postcode), "order.bill_address.zipcode" - .small-6.columns.right - = validated_select t(:country), "order.bill_address.country_id", checkout_country_options + .small-6.columns.right + = validated_select t(:country), "order.bill_address.country_id", {}, {"ng-init" => "order.bill_address.country_id = order.bill_address.country_id || #{Spree::Config[:default_country_id]}", "ng-options" => "c.id as c.name for c in countries"} .row .small-12.columns.text-right diff --git a/app/views/checkout/_details.html.haml b/app/views/checkout/_details.html.haml index 788996be37..775c00c670 100644 --- a/app/views/checkout/_details.html.haml +++ b/app/views/checkout/_details.html.haml @@ -28,5 +28,5 @@ .row .small-12.columns.text-right - %button.primary{"ng-disabled" => "details.$invalid", "ng-click" => "next($event)"} + %button.primary{"ng-disabled" => "details.$invalid", "ng-click" => "login_or_next($event)"} = t :next diff --git a/app/views/checkout/_shipping_ship_address.html.haml b/app/views/checkout/_shipping_ship_address.html.haml index 7b4db0d54c..1119886a47 100644 --- a/app/views/checkout/_shipping_ship_address.html.haml +++ b/app/views/checkout/_shipping_ship_address.html.haml @@ -1,28 +1,28 @@ .small-12.columns #ship_address{"ng-if" => "Checkout.requireShipAddress()"} %div.visible{"ng-if" => "!Checkout.ship_address_same_as_billing"} - .row - .small-6.columns - = validated_input t(:first_name), "order.ship_address.firstname", "ofn-focus" => "accordion['shipping']" - .small-6.columns - = validated_input t(:last_name), "order.ship_address.lastname" - .row - .small-12.columns - = validated_input t(:address), "order.ship_address.address1" - .row - .small-12.columns - = validated_input t(:address2), "order.ship_address.address2", required: false - .row - .small-6.columns - = validated_input t(:city), "order.ship_address.city" - .small-6.columns - = validated_select t(:state), "order.ship_address.state_id", checkout_state_options(:shipping) - .row - .small-6.columns - = validated_input t(:postcode), "order.ship_address.zipcode" - .small-6.columns.right - = validated_select t(:country), "order.ship_address.country_id", checkout_country_options - - .row - .small-6.columns - = validated_input t(:phone), "order.ship_address.phone" + %div{ "ng-controller" => "CountryCtrl" } + .row + .small-6.columns + = validated_input t(:first_name), "order.ship_address.firstname", "ofn-focus" => "accordion['shipping']" + .small-6.columns + = validated_input t(:last_name), "order.ship_address.lastname" + .row + .small-12.columns + = validated_input t(:address), "order.ship_address.address1" + .row + .small-12.columns + = validated_input t(:address2), "order.ship_address.address2", required: false + .row + .small-6.columns + = validated_input t(:city), "order.ship_address.city" + .small-6.columns + = validated_select t(:state), "order.ship_address.state_id", {}, {"ng-options" => "s.id as s.name for s in countriesById[order.ship_address.country_id].states"} + .row + .small-6.columns + = validated_input t(:postcode), "order.ship_address.zipcode" + .small-6.columns.right + = validated_select t(:country), "order.ship_address.country_id", {}, {"ng-init" => "order.ship_address.country_id = order.ship_address.country_id || #{Spree::Config[:default_country_id]}", "ng-options" => "c.id as c.name for c in countries"} + .row + .small-6.columns + = validated_input t(:phone), "order.ship_address.phone" diff --git a/app/views/checkout/edit.html.haml b/app/views/checkout/edit.html.haml index b3d30ed442..75c9eff08f 100644 --- a/app/views/checkout/edit.html.haml +++ b/app/views/checkout/edit.html.haml @@ -2,6 +2,7 @@ = t :checkout_title = inject_enterprise_and_relatives += inject_available_countries .darkswarm.footer-pad - content_for :order_cycle_form do diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index 90b74c0b42..bd15ce9a76 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -31,7 +31,7 @@ .small-12.columns.pad-top .row - .small-12.medium-12.large-9.columns + .small-12.medium-12.large-9.embedded-fullwidth.columns %div{"ng-controller" => "GroupTabsCtrl"} %tabset %tab{heading: t(:label_map), @@ -48,9 +48,10 @@ %tab{heading: t(:groups_about), active: "tabs.about.active", select: "select(\'about\')"} - %h1 - = t :groups_about - %p!= @group.long_description + .about{ "embedded_target_blank" => true } + %h1 + = t :groups_about + %p!= @group.long_description %tab{heading: t(:groups_producers), active: "tabs.producers.active", @@ -103,7 +104,7 @@ = render 'shared/components/enterprise_no_results' - .small-12.medium-12.large-3.columns + .small-12.medium-12.large-3.columns.contact = render 'contact' .small-12.columns.pad-top @@ -119,7 +120,11 @@ %i.ofn-i_050-mail-circle =link_to_service "http://", @group.website, title: t(:groups_contact_website) do %i.ofn-i_049-web - %p -   + .powered-by-embedded + %img{src: '/favicon.ico'} + %span + = t 'powered_by' + %span + = t 'title' -= render "shared/footer" += render "shared/footer" \ No newline at end of file diff --git a/app/views/producers/_fat.html.haml b/app/views/producers/_fat.html.haml index 8ef6282cb7..190e244bb5 100644 --- a/app/views/producers/_fat.html.haml +++ b/app/views/producers/_fat.html.haml @@ -76,7 +76,7 @@ .row.cta-container .columns.small-12 %a.cta-hub{"ng-repeat" => "hub in producer.hubs | visible | orderBy:'-active'", - "ng-href" => "{{::hub.path}}", "ofn-change-hub" => "hub", + "ng-href" => "{{::hub.path}}", "ng-attr-target" => "{{ embedded_layout ? '_blank' : undefined }}", "ofn-change-hub" => "hub", "ng-class" => "::{primary: hub.active, secondary: !hub.active}"} %i.ofn-i_068-shop-reversed{"ng-if" => "::hub.active"} %i.ofn-i_068-shop-reversed{"ng-if" => "::!hub.active"} diff --git a/app/views/producers/_skinny.html.haml b/app/views/producers/_skinny.html.haml index 28fbd5439f..9771576842 100644 --- a/app/views/producers/_skinny.html.haml +++ b/app/views/producers/_skinny.html.haml @@ -1,7 +1,7 @@ .row.active_table_row{"ng-click" => "toggle($event)", "ng-class" => "{'closed' : !open(), 'is_distributor' : producer.is_distributor}"} .columns.small-12.medium-8.large-8.skinny-head %span{"ng-if" => "::producer.is_distributor" } - %a.is_distributor{"ng-href" => "{{::producer.path}}"} + %a.is_distributor{"ng-href" => "{{::producer.path}}", "ng-attr-target" => "{{ embedded_layout ? '_blank' : undefined}}"} .row.vertical-align-middle .columns.small-2.medium-2.large-2 %i{ng: {class: "::producer.producer_icon_font"}} @@ -18,7 +18,7 @@ .columns.small-6.medium-2.large-2 %span.margin-top{"ng-bind" => "::producer.address.city"} .columns.small-4.medium-1.large-1 - %span.margin-top{"ng-bind" => "::producer.address.state_name | uppercase"} + %span.margin-top{"ng-bind" => "::producer.address.state_name"} .columns.small-2.medium-1.large-1.text-right %span.margin-top %i{"ng-class" => "{'ofn-i_005-caret-down' : !open(), 'ofn-i_006-caret-up' : open()}"} diff --git a/app/views/shared/_footer.html.haml b/app/views/shared/_footer.html.haml index de352b444a..6b04cedb5a 100644 --- a/app/views/shared/_footer.html.haml +++ b/app/views/shared/_footer.html.haml @@ -142,3 +142,13 @@ .medium-2.columns.text-center / Placeholder + -if ENV['SKYLIGHT_PUBLIC_DASHBOARD_URL'].present? + .row + .small-12.medium-8.medium-offset-2.columns.text-center + %hr.hr-light + %br + + .row + .small-12.medium-8.medium-offset-2.columns.text-center + .text.small + = t :footer_skylight_dashboard_html, {dashboard: link_to('Skylight', ENV['SKYLIGHT_PUBLIC_DASHBOARD_URL'], target: "_blank")} diff --git a/app/views/shopping_shared/_about.html.haml b/app/views/shopping_shared/_about.html.haml index eac2fe5658..e4d03a1d2c 100644 --- a/app/views/shopping_shared/_about.html.haml +++ b/app/views/shopping_shared/_about.html.haml @@ -1,8 +1,9 @@ -.content#about{"ng-controller" => "AboutUsCtrl"} - .panel - .row - .small-12.large-8.columns - %img.hero-img-small{"ng-src" => "{{::CurrentHub.hub.promo_image}}", "ng-if" => "::CurrentHub.hub.promo_image"} - %p{"ng-bind-html" => "::CurrentHub.hub.long_description"} - .small-12.large-4.columns -   +%script{ type: "text/ng-template", id: "shop/about.html" } + .content#about{"ng-controller" => "AboutUsCtrl"} + .panel + .row + .small-12.large-8.columns + %img.hero-img-small{"ng-src" => "{{::CurrentHub.hub.promo_image}}", "ng-if" => "::CurrentHub.hub.promo_image"} + %p{"ng-bind-html" => "::CurrentHub.hub.long_description"} + .small-12.large-4.columns +   diff --git a/app/views/shopping_shared/_contact.html.haml b/app/views/shopping_shared/_contact.html.haml index 405c958768..94c0bd3b52 100644 --- a/app/views/shopping_shared/_contact.html.haml +++ b/app/views/shopping_shared/_contact.html.haml @@ -1,62 +1,66 @@ -.content#contact - .panel - .row - .small-12.large-4.columns - - if current_distributor.address.address1 || current_distributor.address.address2 || current_distributor.address.city || current_distributor.address.state || current_distributor.address.zipcode - %div.center - .header - = t :shopping_contact_address - %strong=current_distributor.name - %p - = current_distributor.address.address1 - - unless current_distributor.address.address2.blank? - %br - = current_distributor.address.address2 - %br - = current_distributor.address.city - = current_distributor.address.state - = current_distributor.address.zipcode - - .small-12.large-4.columns - - if current_distributor.website || current_distributor.email_address - %div.center - .header - = t :shopping_contact_web - %p - - unless current_distributor.website.blank? - %a{href: "http://#{current_distributor.website}", target: "_blank" } - = current_distributor.website +%script{ type: "text/ng-template", id: "shop/contact.html" } + .content#contact + .panel + .row + .small-12.large-4.columns + - if current_distributor.address.address1 || current_distributor.address.address2 || current_distributor.address.city || current_distributor.address.state || current_distributor.address.zipcode + %div.center + .header + = t :shopping_contact_address + %strong=current_distributor.name + %p + = current_distributor.address.address1 + - unless current_distributor.address.address2.blank? %br - - unless current_distributor.email_address.blank? - %a{href: current_distributor.email_address.reverse, mailto: true} - %span.email - = current_distributor.email_address.reverse + = current_distributor.address.address2 + %br + = current_distributor.address.city + = current_distributor.address.state + = current_distributor.address.zipcode - .small-12.large-4.columns - - if current_distributor.twitter.present? || current_distributor.facebook.present? || current_distributor.linkedin.present? || current_distributor.instagram.present? - %div.center - .header - = t :shopping_contact_social - %div.follow-icons - - unless current_distributor.twitter.blank? - %span - %a{href: "http://twitter.com/#{current_distributor.twitter}", target: "_blank" } - %i.ofn-i_041-twitter + .small-12.large-4.columns + - if current_distributor.website || current_distributor.email_address || current_distributor.phone + %div.center + .header + = t :shopping_contact_web + %p + - unless current_distributor.phone.blank? + = current_distributor.phone + %br + - unless current_distributor.website.blank? + %a{href: "http://#{current_distributor.website}", target: "_blank" } + = current_distributor.website + %br + - unless current_distributor.email_address.blank? + %a{href: current_distributor.email_address.reverse, mailto: true} + %span.email + = current_distributor.email_address.reverse - - unless current_distributor.facebook.blank? - %span - %a{href: "http://#{current_distributor.facebook}", target: "_blank" } - %i.ofn-i_044-facebook - / = current_distributor.facebook + .small-12.large-4.columns + - if current_distributor.twitter.present? || current_distributor.facebook.present? || current_distributor.linkedin.present? || current_distributor.instagram.present? + %div.center + .header + = t :shopping_contact_social + %div.follow-icons + - unless current_distributor.twitter.blank? + %span + %a{href: "http://twitter.com/#{current_distributor.twitter}", target: "_blank" } + %i.ofn-i_041-twitter - - unless current_distributor.linkedin.blank? - %span - %a{href: "http://#{current_distributor.linkedin}", target: "_blank" } - %i.ofn-i_042-linkedin - / = current_distributor.linkedin + - unless current_distributor.facebook.blank? + %span + %a{href: "http://#{current_distributor.facebook}", target: "_blank" } + %i.ofn-i_044-facebook + / = current_distributor.facebook - - unless current_distributor.instagram.blank? - %span - %a{href: "http://instagram.com/#{current_distributor.instagram}", target: "_blank" } - %i.ofn-i_043-instagram - / = current_distributor.instagram + - unless current_distributor.linkedin.blank? + %span + %a{href: "http://#{current_distributor.linkedin}", target: "_blank" } + %i.ofn-i_042-linkedin + / = current_distributor.linkedin + + - unless current_distributor.instagram.blank? + %span + %a{href: "http://instagram.com/#{current_distributor.instagram}", target: "_blank" } + %i.ofn-i_043-instagram + / = current_distributor.instagram diff --git a/app/views/shopping_shared/_groups.html.haml b/app/views/shopping_shared/_groups.html.haml index 6278f44c77..5e04f3d43c 100644 --- a/app/views/shopping_shared/_groups.html.haml +++ b/app/views/shopping_shared/_groups.html.haml @@ -1,13 +1,14 @@ -.content - .panel - .row - .small-12.large-4.columns - - if current_distributor.groups.length > 0 - %h5 - =current_distributor.name - = t :shopping_groups_part_of - %ul.bullet-list - - for group in current_distributor.groups - %li - %a{href: main_app.groups_path + "/#{group.permalink}"} - = group.name +%script{ type: "text/ng-template", id: "shop/groups.html" } + .content + .panel + .row + .small-12.large-4.columns + - if current_distributor.groups.length > 0 + %h5 + =current_distributor.name + = t :shopping_groups_part_of + %ul.bullet-list + - for group in current_distributor.groups + %li + %a{href: main_app.groups_path + "/#{group.permalink}"} + = group.name diff --git a/app/views/shopping_shared/_producers.html.haml b/app/views/shopping_shared/_producers.html.haml index 3c9e9982db..754a26b265 100644 --- a/app/views/shopping_shared/_producers.html.haml +++ b/app/views/shopping_shared/_producers.html.haml @@ -1,11 +1,12 @@ -.content#producers{"ng-controller" => "ProducersTabCtrl"} - .panel - .row - .small-12.columns - %h5 - = t :shopping_producers_of_hub, hub: '{{CurrentHub.hub.name}}' - %ul.small-block-grid-2.large-block-grid-4 - %li{"ng-repeat" => "enterprise in CurrentHub.hub.producers"} - %enterprise-modal - %i.ofn-i_036-producers - {{ enterprise.name }} +%script{ type: "text/ng-template", id: "shop/producers.html" } + .content#producers{"ng-controller" => "ProducersTabCtrl"} + .panel + .row + .small-12.columns + %h5 + = t :shopping_producers_of_hub, hub: '{{CurrentHub.hub.name}}' + %ul.small-block-grid-2.large-block-grid-4 + %li{"ng-repeat" => "enterprise in CurrentHub.hub.producers"} + %enterprise-modal + %i.ofn-i_036-producers + {{ enterprise.name }} diff --git a/app/views/shopping_shared/_tabs.html.haml b/app/views/shopping_shared/_tabs.html.haml index 3bcd2aa37e..3961cf9bfa 100644 --- a/app/views/shopping_shared/_tabs.html.haml +++ b/app/views/shopping_shared/_tabs.html.haml @@ -1,15 +1,9 @@ -#tabs{"ng-controller" => "ShoppingTabsCtrl", "ng-cloak" => true} +- shop_tabs.each do |tab| + = render "shopping_shared/#{tab[:name]}" + +.tabset-ctrl#shop-tabs{ navigate: 'true', prefix: 'shop', ng: { cloak: true } } .row - %tabset{ 'open-on-load' => 'false' } - -# Build all tabs. - - for name, heading_cols in { about: [t(:shopping_tabs_about, distributor: current_distributor.name), 6], - producers: [t(:label_producers),2], - contact: [t(:shopping_tabs_contact),2], - groups: [t(:label_groups),2]} - - heading, cols = heading_cols - %tab.columns{heading: heading, - id: "tab_#{name}", - active: "tabs.#{name}.active", - select: "select(\'#{name}\')", - class: "small-12 medium-#{cols}" } - = render "shopping_shared/#{name}" + - shop_tabs.each do |tab| + .small-12.columns.tab{ id: "tab_#{tab[:name]}", name: tab[:name], class: "medium-#{tab[:cols]}" } + %a{ href: 'javascript:void(0)' }=tab[:title] + .small-12.columns.tab-view diff --git a/app/views/shops/_fat.html.haml b/app/views/shops/_fat.html.haml index beb882c4d4..66752acefd 100644 --- a/app/views/shops/_fat.html.haml +++ b/app/views/shops/_fat.html.haml @@ -33,17 +33,18 @@ %enterprise-modal %i.ofn-i_036-producers %span{"ng-bind" => "::enterprise.name"} + %li{"ng-repeat" => "enterprise in hub.producers.slice(7,hub.producers.length)", "class" => "additional-producer"} + %enterprise-modal + %i.ofn-i_036-producers + %span{"ng-bind" => "::enterprise.name"} %li{"data-is-link" => "true", "class" => "more-producers-link", "ng-show" => "::hub.producers.length>7"} - %a{"ng-click" => "toggleMoreProducers=!toggleMoreProducers"} + %a{"ng-click" => "toggleMoreProducers=!toggleMoreProducers; $event.stopPropagation()"} .more + %span{"ng-bind" => "::hub.producers.length-7"} = t :label_more .less = t :label_less - %li{"ng-repeat" => "enterprise in hub.producers.slice(7,hub.producers.length)", "class" => "additional-producer"} - %enterprise-modal - %i.ofn-i_036-producers - %span{"ng-bind" => "::enterprise.name"} + %div.show-for-medium-up{"ng-if" => "::hub.producers.length==0"}   diff --git a/app/views/shops/_skinny.html.haml b/app/views/shops/_skinny.html.haml index a6603236e1..6a67f4eed9 100644 --- a/app/views/shops/_skinny.html.haml +++ b/app/views/shops/_skinny.html.haml @@ -1,17 +1,17 @@ .row.active_table_row{"ng-if" => "hub.is_distributor", "ng-click" => "toggle($event)", "ng-class" => "{'closed' : !open(), 'is_distributor' : producer.is_distributor}"} .columns.small-12.medium-5.large-5.skinny-head - %a.hub{"ng-href" => "{{::hub.path}}", "ng-class" => "{primary: hub.active, secondary: !hub.active}", "ofn-change-hub" => "hub", "data-is-link" => "true"} + %a.hub{"ng-href" => "{{::hub.path}}", "ng-attr-target" => "{{ embedded_layout ? '_blank' : undefined}}", "ng-class" => "{primary: hub.active, secondary: !hub.active}", "ofn-change-hub" => "hub", "data-is-link" => "true"} %i{ng: {class: "::hub.icon_font"}} %span.margin-top.hub-name-listing{"ng-bind" => "::hub.name | truncate:40"} .columns.small-4.medium-2.large-2 %span.margin-top{"ng-bind" => "::hub.address.city"} .columns.small-2.medium-1.large-1 - %span.margin-top{"ng-bind" => "::hub.address.state_name | uppercase"} + %span.margin-top{"ng-bind" => "::hub.address.state_name"} %span.margin-top{"ng-if" => "hub.distance != null && hub.distance > 0"} ({{ hub.distance / 1000 | number:0 }} km) .columns.small-4.medium-3.large-3.text-right{"ng-if" => "::hub.active"} - %a.hub.open_closed{"ng-href" => "{{::hub.path}}", "ng-class" => "{primary: hub.active, secondary: !hub.active}", "ofn-change-hub" => "hub"} + %a.hub.open_closed{"ng-href" => "{{::hub.path}}", "ng-attr-target" => "{{ embedded_layout ? '_blank' : undefined}}", "ng-class" => "{primary: hub.active, secondary: !hub.active}", "ofn-change-hub" => "hub"} %i.ofn-i_068-shop-reversed %span.margin-top{ ng: { if: "::current()" } } %em= t :hubs_shopping_here @@ -19,7 +19,7 @@ %span{"ng-bind" => "::hub.orders_close_at | sensible_timeframe"} .columns.small-4.medium-3.large-3.text-right{"ng-if" => "::!hub.active"} - %a.hub.open_closed{"ng-href" => "{{::hub.path}}", "ng-class" => "{primary: hub.active, secondary: !hub.active}", "ofn-change-hub" => "hub"} + %a.hub.open_closed{"ng-href" => "{{::hub.path}}", "ng-attr-target" => "{{ embedded_layout ? '_blank' : undefined}}", "ng-class" => "{primary: hub.active, secondary: !hub.active}", "ofn-change-hub" => "hub"} %span.margin-top{ ng: { if: "::current()" } } %em= t :hubs_shopping_here %span.margin-top{ ng: { if: "::!current()" } } @@ -39,7 +39,7 @@ .columns.small-4.medium-2.large-2 %span.margin-top{"ng-bind" => "::hub.address.city"} .columns.small-2.medium-1.large-1 - %span.margin-top{"ng-bind" => "::hub.address.state_name | uppercase"} + %span.margin-top{"ng-bind" => "::hub.address.state_name"} .columns.small-6.medium-3.large-4.text-right %span.margin-top{ ng: { if: "::!current()" } } diff --git a/app/views/spree/admin/overview/_enterprise_row.html.haml b/app/views/spree/admin/overview/_enterprise_row.html.haml new file mode 100644 index 0000000000..682a50b65c --- /dev/null +++ b/app/views/spree/admin/overview/_enterprise_row.html.haml @@ -0,0 +1,33 @@ +%a.sixteen.columns.alpha.list-item{ class: "#{cycle('odd','even')}", href: "#{main_app.edit_admin_enterprise_path(enterprise)}" } + %span.five.columns.alpha + = enterprise.name + %span.symbol.three.columns.centered + - if can?(:admin, Spree::PaymentMethod) && enterprise.is_distributor + - payment_method_count = enterprise.payment_methods.count + - if payment_method_count > 0 + %span.icon-ok-sign{ 'ofn-with-tip' => "#{pluralize payment_method_count, 'payment method'}" } + - else + %span.icon-remove-sign{ 'ofn-with-tip' => t('.has_no_payment_methods', enterprise: enterprise.name) } + - else +   + %span.symbol.three.columns.centered + - if can?(:admin, Spree::ShippingMethod) && enterprise.is_distributor + - shipping_method_count = enterprise.shipping_methods.count + - if shipping_method_count > 0 + %span.icon-ok-sign{ 'ofn-with-tip' => "#{pluralize shipping_method_count, 'shipping method'}" } + - else + %span.icon-remove-sign{ 'ofn-with-tip' => t('.has_no_shipping_methods', enterprise: enterprise.name) } + - else +   + %span.symbol.three.columns.centered + - if can?(:admin, EnterpriseFee) && enterprise.is_distributor + - fee_count = enterprise.enterprise_fees.count + - if fee_count > 0 + %span.icon-ok-sign{ 'ofn-with-tip' => "#{pluralize fee_count, 'fee'}" } + - else + %span.icon-warning-sign{ 'ofn-with-tip' => t('.has_no_enterprise_fees', enterprise: enterprise.name) } + - else +   + %span.two.columns.omega.right + %span.icon-arrow-right + diff --git a/app/views/spree/admin/overview/_enterprises.html.haml b/app/views/spree/admin/overview/_enterprises.html.haml index fbf19fbd43..1ffb68e8cb 100644 --- a/app/views/spree/admin/overview/_enterprises.html.haml +++ b/app/views/spree/admin/overview/_enterprises.html.haml @@ -1,11 +1,33 @@ -%div.dashboard_item.sixteen.columns.alpha#enterprises{ 'ng-controller' => "enterprisesDashboardCtrl" } +%div.dashboard_item.sixteen.columns.alpha#enterprises = render 'enterprises_header' - if @enterprises.empty? - = render 'enterprises_none' + %div.sixteen.columns.alpha.list-item.red + %span.text.fifteen.columns.alpha + = t "spree_admin_enterprises_none_text" + %span.one.columns.omega + %span.icon-remove-sign + %a.sixteen.columns.alpha.button.bottom.red{ href: "#{main_app.new_admin_enterprise_path}" } + = t "spree_admin_enterprises_none_create_a_new_enterprise" + %span.icon-arrow-right - else - = render 'enterprises_tabs' - = render 'enterprises_hubs_tab' - = render 'enterprises_producers_tab' - = render 'enterprises_footer' + %div.sixteen.columns.alpha.list-title + %span.five.columns.alpha + = t "spree_admin_enterprises_hubs_name" + - if can? :admin, Spree::PaymentMethod + %span.centered.three.columns + = t(:payment_methods) + - if can? :admin, Spree::ShippingMethod + %span.centered.three.columns + = t "spree_admin_enterprises_shipping_methods" + - if can? :admin, EnterpriseFee + %span.centered.three.columns + = t "spree_admin_enterprises_fees" + %div.sixteen.columns.alpha.list + - @enterprises.each do |enterprise| + = render 'enterprise_row', { enterprise: enterprise } + + %a.sixteen.columns.alpha.button.bottom.blue{ href: "#{main_app.admin_enterprises_path}" } + = t "spree_admin_overview_enterprises_footer" + %span.icon-arrow-right diff --git a/app/views/spree/admin/overview/_enterprises_footer.html.haml b/app/views/spree/admin/overview/_enterprises_footer.html.haml deleted file mode 100644 index c704dfef64..0000000000 --- a/app/views/spree/admin/overview/_enterprises_footer.html.haml +++ /dev/null @@ -1,3 +0,0 @@ -%a.sixteen.columns.alpha.button.bottom.blue{ href: "#{main_app.admin_enterprises_path}" } - = t "spree_admin_overview_enterprises_footer" - %span.icon-arrow-right diff --git a/app/views/spree/admin/overview/_enterprises_hubs_tab.html.haml b/app/views/spree/admin/overview/_enterprises_hubs_tab.html.haml deleted file mode 100644 index dbaff71cfa..0000000000 --- a/app/views/spree/admin/overview/_enterprises_hubs_tab.html.haml +++ /dev/null @@ -1,47 +0,0 @@ -%div.hubs_tab{ ng: { show: "activeTab == 'hubs'"} } - %div.sixteen.columns.alpha.list-title - %span.five.columns.alpha - = t "spree_admin_enterprises_hubs_name" - - if can? :admin, Spree::PaymentMethod - %span.centered.three.columns - = t(:payment_methods) - - if can? :admin, Spree::ShippingMethod - %span.centered.three.columns - = t "spree_admin_enterprises_shipping_methods" - - if can? :admin, EnterpriseFee - %span.centered.three.columns - = t "spree_admin_enterprises_fees" - %div.sixteen.columns.alpha.list - - @enterprises.is_distributor.each do |enterprise| - %a.sixteen.columns.alpha.list-item{ class: "#{cycle('odd','even')}", href: "#{main_app.edit_admin_enterprise_path(enterprise)}" } - %span.five.columns.alpha - = enterprise.name - %span.symbol.three.columns.centered - - if can? :admin, Spree::PaymentMethod - - payment_method_count = enterprise.payment_methods.count - - if payment_method_count > 0 - %span.icon-ok-sign{ 'ofn-with-tip' => "#{pluralize payment_method_count, 'payment method'}" } - - else - %span.icon-remove-sign{ 'ofn-with-tip' => t('.has_no_payment_methods', enterprise: enterprise.name) } - - else -   - %span.symbol.three.columns.centered - - if can? :admin, Spree::ShippingMethod - - shipping_method_count = enterprise.shipping_methods.count - - if shipping_method_count > 0 - %span.icon-ok-sign{ 'ofn-with-tip' => "#{pluralize shipping_method_count, 'shipping method'}" } - - else - %span.icon-remove-sign{ 'ofn-with-tip' => t('.has_no_shipping_methods', enterprise: enterprise.name) } - - else -   - %span.symbol.three.columns.centered - - if can? :admin, EnterpriseFee - - fee_count = enterprise.enterprise_fees.count - - if fee_count > 0 - %span.icon-ok-sign{ 'ofn-with-tip' => "#{pluralize fee_count, 'fee'}" } - - else - %span.icon-warning-sign{ 'ofn-with-tip' => t('.has_no_enterprise_fees', enterprise: enterprise.name) } - - else -   - %span.two.columns.omega.right - %span.icon-arrow-right diff --git a/app/views/spree/admin/overview/_enterprises_none.html.haml b/app/views/spree/admin/overview/_enterprises_none.html.haml deleted file mode 100644 index 72cd90df1a..0000000000 --- a/app/views/spree/admin/overview/_enterprises_none.html.haml +++ /dev/null @@ -1,8 +0,0 @@ -%div.sixteen.columns.alpha.list-item.red - %span.text.fifteen.columns.alpha - = t "spree_admin_enterprises_none_text" - %span.one.columns.omega - %span.icon-remove-sign -%a.sixteen.columns.alpha.button.bottom.red{ href: "#{main_app.new_admin_enterprise_path}" } - = t "spree_admin_enterprises_none_create_a_new_enterprise" - %span.icon-arrow-right diff --git a/app/views/spree/admin/overview/_enterprises_producers_tab.html.haml b/app/views/spree/admin/overview/_enterprises_producers_tab.html.haml deleted file mode 100644 index 19cae1e6f8..0000000000 --- a/app/views/spree/admin/overview/_enterprises_producers_tab.html.haml +++ /dev/null @@ -1,47 +0,0 @@ -%div.producers_tab{ ng: { show: "activeTab == 'producers'"} } - %div.list-title.sixteen.columns.alpha - %span.five.columns.alpha - = t "spree_admin_enterprises_producers_name" - - if can? :admin, Spree::Product - %span.centered.three.columns - = t "spree_admin_enterprises_producers_total_products" - %span.centered.three.columns - = t "spree_admin_enterprises_producers_active_products" - - if can? :admin, OrderCycle - %span.centered.three.columns - = t "spree_admin_enterprises_producers_order_cycles" - %div.sixteen.columns.alpha.list - - @enterprises.is_primary_producer.each do |enterprise| - %a.sixteen.columns.alpha.list-item{ class: "#{cycle('odd','even')}", href: "#{main_app.edit_admin_enterprise_path(enterprise)}" } - - %span.five.columns.alpha - = enterprise.name - - %span.symbol.three.columns.centered - - if can? :admin, Spree::Product - %span.one.column.alpha   - %span.text-icon.one.column.centered{ class: "#{enterprise.supplied_products.not_deleted.any? ? "green" : "red" }" } - = enterprise.supplied_products.not_deleted.count - %span.one.column.omega   - - else -   - %span.symbol.three.columns.centered - - if can? :admin, Spree::Product - %span.one.column.alpha   - %span.text-icon.one.column.centered{ class: "#{enterprise.supplied_and_active_products_on_hand.any? ? "green" : "red" }" } - = enterprise.supplied_and_active_products_on_hand.count - %span.one.column.omega   - - else -   - - %span.symbol.three.columns.centered - - if can? :admin, OrderCycle - %span.one.column.alpha   - %span.text-icon.one.column.centered{ class: "#{enterprise.active_products_in_order_cycles.any? ? "green" : "orange" }" } - = enterprise.active_products_in_order_cycles.count - %span.one.column.omega   - - else -   - - %span.two.columns.omega.right - %span.icon-arrow-right diff --git a/app/views/spree/admin/overview/_enterprises_tabs.html.haml b/app/views/spree/admin/overview/_enterprises_tabs.html.haml deleted file mode 100644 index 5f8eb9e3c2..0000000000 --- a/app/views/spree/admin/overview/_enterprises_tabs.html.haml +++ /dev/null @@ -1,5 +0,0 @@ -%div.sixteen.columns.alpha.tabs - %div.dashboard_tab.eight.columns.alpha.blue{ ng: { class: "{selected: activeTab == 'hubs'}", click: "activeTab = 'hubs'" } } - = t "spree_admin_enterprises_tabs_hubs" - %div.dashboard_tab.eight.columns.omega.blue{ ng: { class: "{selected: activeTab == 'producers'}", click: "activeTab = 'producers'" } } - = t "spree_admin_enterprises_tabs_producers" diff --git a/app/views/spree/admin/overview/_products.html.haml b/app/views/spree/admin/overview/_products.html.haml index c017cd11f9..37de2f11d4 100644 --- a/app/views/spree/admin/overview/_products.html.haml +++ b/app/views/spree/admin/overview/_products.html.haml @@ -15,7 +15,7 @@ = "You have #{@product_count} active product#{@product_count > 1 ? "s" : ""}." %span.one.column.omega %span.icon-ok-sign - %a.seven.columns.alpha.button.bottom.blue{ href: "#{bulk_edit_admin_products_path}" } + %a.seven.columns.alpha.button.bottom.blue{ href: "#{admin_products_path}" } = t "spree_admin_enterprises_producers_manage_products" %span.icon-arrow-right - else diff --git a/app/views/spree/admin/overview/single_enterprise_dashboard.html.haml b/app/views/spree/admin/overview/single_enterprise_dashboard.html.haml index dd7322b5b9..dac3df1b8f 100644 --- a/app/views/spree/admin/overview/single_enterprise_dashboard.html.haml +++ b/app/views/spree/admin/overview/single_enterprise_dashboard.html.haml @@ -72,7 +72,7 @@ %span.icon-th-large = t "add_and_manage_products" .list - %a.button.bottom{href: bulk_edit_admin_products_path} + %a.button.bottom{href: admin_products_path} = t "manage_products" %span.icon-arrow-right diff --git a/app/views/spree/admin/products/bulk_edit.html.haml b/app/views/spree/admin/products/bulk_edit.html.haml deleted file mode 100644 index 4e6cf8c704..0000000000 --- a/app/views/spree/admin/products/bulk_edit.html.haml +++ /dev/null @@ -1,10 +0,0 @@ -= render 'spree/admin/products/bulk_edit/header' -= render 'spree/admin/products/bulk_edit/data' - -%div{ ng: { app: 'ofn.admin', controller: 'AdminProductEditCtrl', init: 'initialise()' } } - - = render 'spree/admin/products/bulk_edit/filters' - %hr.divider.sixteen.columns.alpha.omega - = render 'spree/admin/products/bulk_edit/actions' - = render 'spree/admin/products/bulk_edit/indicators' - = render 'spree/admin/products/bulk_edit/products' diff --git a/app/views/spree/admin/products/bulk_edit/_filters.html.haml b/app/views/spree/admin/products/bulk_edit/_filters.html.haml deleted file mode 100644 index a0130615bb..0000000000 --- a/app/views/spree/admin/products/bulk_edit/_filters.html.haml +++ /dev/null @@ -1,18 +0,0 @@ -.filters.sixteen.columns.alpha.omega - .quick_search.four.columns.alpha - %label{ :for => 'quick_filter' } - %br - %input.quick-search.fullwidth{ 'ng-model' => 'query', :name => "quick_filter", :type => 'text', 'placeholder' => t('admin.quick_search') } - .filter_select.four.columns - %label{ :for => 'producer_filter' }= t 'producer' - %br - %select.fullwidth{ :id => 'producer_filter', 'ofn-select2-min-search' => 5, 'ng-model' => 'producerFilter', 'ng-options' => 'producer.id as producer.name for producer in filterProducers' } - .filter_select.four.columns - %label{ :for => 'category_filter' }= t 'category' - %br - %select.fullwidth{ :id => 'category_filter', 'ofn-select2-min-search' => 5, 'ng-model' => 'categoryFilter', 'ng-options' => 'taxon.id as taxon.name for taxon in filterTaxons'} - %div{ :class => "one column" }   - .filter_clear.three.columns.omega - %label{ :for => 'clear_all_filters' } - %br - %input.fullwidth.red{ :type => 'button', :id => 'clear_all_filters', :value => t('admin.clear_filters'), 'ng-click' => "resetSelectFilters()" } diff --git a/app/views/spree/admin/products/index.html.haml b/app/views/spree/admin/products/index.html.haml new file mode 100644 index 0000000000..50927a1ff9 --- /dev/null +++ b/app/views/spree/admin/products/index.html.haml @@ -0,0 +1,10 @@ += render 'spree/admin/products/index/header' += render 'spree/admin/products/index/data' + +%div{ ng: { app: 'ofn.admin', controller: 'AdminProductEditCtrl', init: 'initialise()' } } + + = render 'spree/admin/products/index/filters' + %hr.divider.sixteen.columns.alpha.omega + = render 'spree/admin/products/index/actions' + = render 'spree/admin/products/index/indicators' + = render 'spree/admin/products/index/products' diff --git a/app/views/spree/admin/products/bulk_edit/_actions.html.haml b/app/views/spree/admin/products/index/_actions.html.haml similarity index 100% rename from app/views/spree/admin/products/bulk_edit/_actions.html.haml rename to app/views/spree/admin/products/index/_actions.html.haml diff --git a/app/views/spree/admin/products/bulk_edit/_data.html.haml b/app/views/spree/admin/products/index/_data.html.haml similarity index 100% rename from app/views/spree/admin/products/bulk_edit/_data.html.haml rename to app/views/spree/admin/products/index/_data.html.haml diff --git a/app/views/spree/admin/products/index/_filters.html.haml b/app/views/spree/admin/products/index/_filters.html.haml new file mode 100644 index 0000000000..9c08c9adda --- /dev/null +++ b/app/views/spree/admin/products/index/_filters.html.haml @@ -0,0 +1,26 @@ +.filters.sixteen.columns.alpha.omega + .quick_search.three.columns.alpha + %label{ for: 'quick_filter' } + %br + %input.quick-search.fullwidth{ ng: {model: 'query'}, name: "quick_filter", type: 'text', placeholder: t('admin.quick_search') } + .one.columns   + .filter_select.three.columns + %label{ for: 'producer_filter' }= t 'producer' + %br + %select.fullwidth{ id: 'producer_filter', 'ofn-select2-min-search' => 5, ng: {model: 'producerFilter', options: 'producer.id as producer.name for producer in filterProducers'} } + .filter_select.three.columns + %label{ for: 'category_filter' }= t 'category' + %br + %select.fullwidth{ id: 'category_filter', 'ofn-select2-min-search' => 5, ng: {model: 'categoryFilter', options: 'taxon.id as taxon.name for taxon in filterTaxons'} } + - if FeatureFlags.new(@current_user).product_import_enabled? + .filter_select.three.columns + %label{ for: 'import_filter' } Import Date + %br + %select.fullwidth{ id: 'import_date_filter', 'ofn-select2-min-search' => 5, ng: {model: 'importDateFilter', init: "importDates = #{@import_dates}; showLatestImport = #{@show_latest_import}"}} + %option{value: "{{date.id}}", ng: {repeat: "date in importDates track by date.id" }} + {{date.name}} + + .filter_clear.three.columns.omega + %label{ for: 'clear_all_filters' } + %br + %input.fullwidth.red{ :type => 'button', :id => 'clear_all_filters', :value => t('admin.clear_filters'), 'ng-click' => "resetSelectFilters()" } diff --git a/app/views/spree/admin/products/bulk_edit/_header.html.haml b/app/views/spree/admin/products/index/_header.html.haml similarity index 100% rename from app/views/spree/admin/products/bulk_edit/_header.html.haml rename to app/views/spree/admin/products/index/_header.html.haml diff --git a/app/views/spree/admin/products/bulk_edit/_indicators.html.haml b/app/views/spree/admin/products/index/_indicators.html.haml similarity index 100% rename from app/views/spree/admin/products/bulk_edit/_indicators.html.haml rename to app/views/spree/admin/products/index/_indicators.html.haml diff --git a/app/views/spree/admin/products/bulk_edit/_products.html.haml b/app/views/spree/admin/products/index/_products.html.haml similarity index 65% rename from app/views/spree/admin/products/bulk_edit/_products.html.haml rename to app/views/spree/admin/products/index/_products.html.haml index 04dee1c2df..c122896e48 100644 --- a/app/views/spree/admin/products/bulk_edit/_products.html.haml +++ b/app/views/spree/admin/products/index/_products.html.haml @@ -2,13 +2,13 @@ %form{ name: 'bulk_product_form' } %save-bar{ dirty: "bulk_product_form.$dirty", persist: "false" } %input.red{ type: "button", value: t(:save_changes), ng: { click: "submitProducts()", disabled: "!bulk_product_form.$dirty" } } - %input{ type: "button", value: t(:close), 'ng-click' => "cancel('#{bulk_edit_admin_products_path}')" } + %input{ type: "button", value: t(:close), 'ng-click' => "cancel('#{admin_products_path}')" } %table.index#listing_products.bulk{ "infinite-scroll" => "incrementLimit()", "infinite-scroll-distance" => "1" } - = render 'spree/admin/products/bulk_edit/products_head' + = render 'spree/admin/products/index/products_head' - %tbody{ 'ng-repeat' => 'product in filteredProducts = ( products | filter:query | producer: producerFilter | category: categoryFilter | limitTo:limit )', 'ng-class-even' => "'even'", 'ng-class-odd' => "'odd'" } + %tbody{ 'ng-repeat' => 'product in filteredProducts = ( products | filter:query | producer: producerFilter | category: categoryFilter | importDate: importDateFilter | limitTo:limit )', 'ng-class-even' => "'even'", 'ng-class-odd' => "'odd'" } - = render 'spree/admin/products/bulk_edit/products_product' - = render 'spree/admin/products/bulk_edit/products_variant' + = render 'spree/admin/products/index/products_product' + = render 'spree/admin/products/index/products_variant' diff --git a/app/views/spree/admin/products/bulk_edit/_products_head.html.haml b/app/views/spree/admin/products/index/_products_head.html.haml similarity index 93% rename from app/views/spree/admin/products/bulk_edit/_products_head.html.haml rename to app/views/spree/admin/products/index/_products_head.html.haml index 1ed74174f1..e5aa68f38d 100644 --- a/app/views/spree/admin/products/bulk_edit/_products_head.html.haml +++ b/app/views/spree/admin/products/index/_products_head.html.haml @@ -13,6 +13,7 @@ %col.tax_category{ ng: { show: 'columns.tax_category.visible' } } %col.inherits_properties{ ng: { show: 'columns.inherits_properties.visible' } } %col.available_on{ ng: { show: 'columns.available_on.visible' } } + %col.import_date{ ng: { show: 'columns.import_date.visible' } } %col.actions %col.actions %col.actions @@ -35,6 +36,7 @@ %th.tax_category{ 'ng-show' => 'columns.tax_category.visible' }=t('.tax_category') %th.inherits_properties{ 'ng-show' => 'columns.inherits_properties.visible' }=t('.inherits_properties?') %th.available_on{ 'ng-show' => 'columns.available_on.visible' }=t('.av_on') + %th.import_date{ 'ng-show' => 'columns.import_date.visible' }=t('.import_date') %th.actions %th.actions %th.actions diff --git a/app/views/spree/admin/products/bulk_edit/_products_product.html.haml b/app/views/spree/admin/products/index/_products_product.html.haml similarity index 97% rename from app/views/spree/admin/products/bulk_edit/_products_product.html.haml rename to app/views/spree/admin/products/index/_products_product.html.haml index e947f2eba2..d365212568 100644 --- a/app/views/spree/admin/products/bulk_edit/_products_product.html.haml +++ b/app/views/spree/admin/products/index/_products_product.html.haml @@ -34,6 +34,8 @@ %input{ 'ng-model' => 'product.inherits_properties', :name => 'inherits_properties', 'ofn-track-product' => 'inherits_properties', type: "checkbox" } %td.available_on{ 'ng-show' => 'columns.available_on.visible' } %input{ 'ng-model' => 'product.available_on', :name => 'available_on', 'ofn-track-product' => 'available_on', 'datetimepicker' => 'product.available_on', type: "text" } + %td.import_date{ 'ng-show' => 'columns.import_date.visible' } + %span {{(product.import_date | date:"MMMM dd, yyyy HH:mm") || ""}} %td.actions %a{ 'ng-click' => 'editWarn(product)', :class => "edit-product icon-edit no-text", 'ofn-with-tip' => t(:edit) } %td.actions diff --git a/app/views/spree/admin/products/bulk_edit/_products_variant.html.haml b/app/views/spree/admin/products/index/_products_variant.html.haml similarity index 91% rename from app/views/spree/admin/products/bulk_edit/_products_variant.html.haml rename to app/views/spree/admin/products/index/_products_variant.html.haml index eb87039ff0..c4c0412eef 100644 --- a/app/views/spree/admin/products/bulk_edit/_products_variant.html.haml +++ b/app/views/spree/admin/products/index/_products_variant.html.haml @@ -23,9 +23,11 @@ %td{ 'ng-show' => 'columns.tax_category.visible' } %td{ 'ng-show' => 'columns.inherits_properties.visible' } %td{ 'ng-show' => 'columns.available_on.visible' } + %td{ 'ng-show' => 'columns.import_date.visible' } + %span {{variant.import_date | date:"MMMM dd, yyyy HH:mm"}} %td.actions %a{ 'ng-click' => 'editWarn(product,variant)', :class => "edit-variant icon-edit no-text", 'ng-show' => "variantSaved(variant)", 'ofn-with-tip' => t(:edit) } %td.actions - %span.icon-warning-sign{ 'ng-if' => 'variant.variant_overrides', 'ofn-with-tip' => "{{ 'spree.admin.products.bulk_edit.products_variant.variant_has_n_overrides' | t:{n: variant.variant_overrides.length} }}" } + %span.icon-warning-sign{ 'ng-if' => 'variant.variant_overrides', 'ofn-with-tip' => "{{ 'spree.admin.products.index.products_variant.variant_has_n_overrides' | t:{n: variant.variant_overrides.length} }}" } %td.actions %a{ 'ng-click' => 'deleteVariant(product,variant)', "ng-class" => '{disabled: product.variants.length < 2}', :class => "delete-variant icon-trash no-text", 'ofn-with-tip' => t(:remove) } diff --git a/app/views/spree/admin/products/bulk_edit/_save_button_row.html.haml b/app/views/spree/admin/products/index/_save_button_row.html.haml similarity index 100% rename from app/views/spree/admin/products/bulk_edit/_save_button_row.html.haml rename to app/views/spree/admin/products/index/_save_button_row.html.haml diff --git a/app/views/spree/admin/reports/_link_order.html.haml b/app/views/spree/admin/reports/_link_order.html.haml new file mode 100644 index 0000000000..9f0b72b4f7 --- /dev/null +++ b/app/views/spree/admin/reports/_link_order.html.haml @@ -0,0 +1 @@ +%a.edit-order{href: "/admin/orders/#{value}"}= value diff --git a/app/views/spree/admin/reports/_table.html.haml b/app/views/spree/admin/reports/_table.html.haml new file mode 100644 index 0000000000..7cc612a82d --- /dev/null +++ b/app/views/spree/admin/reports/_table.html.haml @@ -0,0 +1,23 @@ +- column_partials ||= {} +- if render_content? + %table.report__table{id: id} + %thead + %tr + - @header.each do |heading| + %th= heading + %tbody + - @table.each do |row| + %tr + - row.each_with_index do |cell_value, column_index| + %td + - partial = column_partials[column_index] + - if partial + = render partial, value: cell_value + - else + = cell_value + - if @table.empty? + %tr + %td{colspan: @header.count}= t(:none) +- else + %p.report__message + = t(".select_and_search") diff --git a/app/views/spree/admin/reports/bulk_coop.html.haml b/app/views/spree/admin/reports/bulk_coop.html.haml index 71bd570658..a3a3b8e367 100644 --- a/app/views/spree/admin/reports/bulk_coop.html.haml +++ b/app/views/spree/admin/reports/bulk_coop.html.haml @@ -15,18 +15,5 @@ %br %br = button t(:search) -%br -%br -%table#listing_orders.index - %thead - %tr{'data-hook' => "orders_header"} - - @report.header.each do |heading| - %th=heading - %tbody - - @table.each do |row| - %tr - - row.each do |column| - %td= column - - if @table.empty? - %tr - %td{:colspan => "2"}= t(:none) + += render "table", id: "listing_orders" diff --git a/app/views/spree/admin/reports/customers.html.haml b/app/views/spree/admin/reports/customers.html.haml index 3ab25e5c6c..63d084f016 100644 --- a/app/views/spree/admin/reports/customers.html.haml +++ b/app/views/spree/admin/reports/customers.html.haml @@ -29,18 +29,4 @@ %br = button t(:search) -%br -%br -%table#listing_customers.index - %thead - %tr{'data-hook' => "orders_header"} - - @report.header.each do |heading| - %th=heading - %tbody - - @report.table.each do |row| - %tr - - row.each do |column| - %td= column - - if @report.table.empty? - %tr - %td{:colspan => "2"}= t(:none) += render "table", id: "listing_customers" diff --git a/app/views/spree/admin/reports/order_cycle_management.html.haml b/app/views/spree/admin/reports/order_cycle_management.html.haml index bf4a1ed2e8..54d9eca68a 100644 --- a/app/views/spree/admin/reports/order_cycle_management.html.haml +++ b/app/views/spree/admin/reports/order_cycle_management.html.haml @@ -29,18 +29,4 @@ .row = button t(:search) -%br -%br -%table#listing_ocm_orders.index - %thead - %tr{'data-hook' => "orders_header"} - - @report.header.each do |heading| - %th=heading - %tbody - - @table.each do |row| - %tr - - row.each do |column| - %td= column - - if @table.empty? - %tr - %td{:colspan => "2"}= t(:none) += render "table", id: "listing_ocm_orders" diff --git a/app/views/spree/admin/reports/orders_and_distributors.html.haml b/app/views/spree/admin/reports/orders_and_distributors.html.haml index aa710b1db6..2279438c25 100644 --- a/app/views/spree/admin/reports/orders_and_distributors.html.haml +++ b/app/views/spree/admin/reports/orders_and_distributors.html.haml @@ -6,18 +6,4 @@ %br = button t(:search) -%br -%br -%table#listing_orders.index - %thead - %tr{'data-hook' => 'orders_header'} - - @report.header.each do |heading| - %th=heading - %tbody - - @report.table.each do |row| - %tr - - row.each do |column| - %td= column - - if @report.table.empty? - %tr - %td{:colspan => "2"}= t(:none) += render "table", id: "listing_orders" diff --git a/app/views/spree/admin/reports/orders_and_fulfillment.html.haml b/app/views/spree/admin/reports/orders_and_fulfillment.html.haml index 5bda882108..9bf5b6bd24 100644 --- a/app/views/spree/admin/reports/orders_and_fulfillment.html.haml +++ b/app/views/spree/admin/reports/orders_and_fulfillment.html.haml @@ -25,18 +25,4 @@ .row = button t(:search) -%br -%br -%table#listing_orders.index - %thead - %tr{'data-hook' => "orders_header"} - - @report.header.each do |heading| - %th=heading - %tbody - - @table.each do |row| - %tr - - row.each do |column| - %td= column - - if @table.empty? - %tr - %td{:colspan => "2"}= t(:none) += render "table", id: "listing_orders" diff --git a/app/views/spree/admin/reports/packing.html.haml b/app/views/spree/admin/reports/packing.html.haml index fa9d22eb70..742520d715 100644 --- a/app/views/spree/admin/reports/packing.html.haml +++ b/app/views/spree/admin/reports/packing.html.haml @@ -25,18 +25,4 @@ .row = button t(:search) -%br -%br -%table#listing_orders.index - %thead - %tr{'data-hook' => "orders_header"} - - @report.header.each do |heading| - %th=heading - %tbody - - @table.each do |row| - %tr - - row.each do |column| - %td= column - - if @table.empty? - %tr - %td{:colspan => "2"}= t(:none) += render "table", id: "listing_orders" diff --git a/app/views/spree/admin/reports/payments.html.haml b/app/views/spree/admin/reports/payments.html.haml index b69708fbd1..23b046e7c0 100644 --- a/app/views/spree/admin/reports/payments.html.haml +++ b/app/views/spree/admin/reports/payments.html.haml @@ -15,18 +15,5 @@ %br %br = button t(:search) -%br -%br -%table#listing_orders.index - %thead - %tr{'data-hook' => "orders_header"} - - @report.header.each do |heading| - %th=heading - %tbody - - @table.each do |row| - %tr - - row.each do |column| - %td= column - - if @table.empty? - %tr - %td{:colspan => "2"}= t(:none) + += render "table", id: "listing_orders" diff --git a/app/views/spree/admin/reports/products_and_inventory.html.haml b/app/views/spree/admin/reports/products_and_inventory.html.haml index 40c4fbfccf..3d552b81be 100644 --- a/app/views/spree/admin/reports/products_and_inventory.html.haml +++ b/app/views/spree/admin/reports/products_and_inventory.html.haml @@ -31,18 +31,4 @@ = label_tag :csv, t(:report_customers_csv) %br = button t(:search) -%br -%br -%table#listing_products.index - %thead - %tr{'data-hook' => "products_header"} - - @report.header.each do |heading| - %th=heading - %tbody - - @report.table.each do |row| - %tr - - row.each do |column| - %td= column - - if @report.table.empty? - %tr - %td{:colspan => "2"}= t(:none) += render "table", id: "listing_products" diff --git a/app/views/spree/admin/reports/sales_tax.html.haml b/app/views/spree/admin/reports/sales_tax.html.haml index 9971b2c5cb..d64f73ec4d 100644 --- a/app/views/spree/admin/reports/sales_tax.html.haml +++ b/app/views/spree/admin/reports/sales_tax.html.haml @@ -16,22 +16,4 @@ %br = button t(:search) -%br -%br -%table#listing_orders.index - %thead - %tr{'data-hook' => "orders_header"} - - @report.header.each do |heading| - %th= heading - %tbody - - @report.table.each do |row| - %tr - - row.each_with_index do |column, i| - - if i == 0 - %td - %a.edit-order{'href' => "/admin/orders/#{column}"}= column - - else - %td= column - - if @report.table.empty? - %tr - %td{:colspan => @report.header.count}= t(:none) += render "table", id: "listing_orders", column_partials: {0 => "link_order"} diff --git a/app/views/spree/admin/reports/users_and_enterprises.html.haml b/app/views/spree/admin/reports/users_and_enterprises.html.haml index 1b57373c38..d7a521911f 100644 --- a/app/views/spree/admin/reports/users_and_enterprises.html.haml +++ b/app/views/spree/admin/reports/users_and_enterprises.html.haml @@ -17,18 +17,5 @@ = label_tag :csv, t(:report_customers_csv) .row = button t(:search) -%br -%br -%table#users_and_enterprises - %thead - %tr - - @report.header.each do |heading| - %th=heading - %tbody - - @report.table.each do |row| - %tr - - row.each do |column| - %td= column - - if @report.table.empty? - %tr - %td{:colspan => "2"}= t(:none) \ No newline at end of file + += render "table", id: "users_and_enterprises" diff --git a/app/views/spree/admin/reports/xero_invoices.html.haml b/app/views/spree/admin/reports/xero_invoices.html.haml index 8669089f81..0139b42a68 100644 --- a/app/views/spree/admin/reports/xero_invoices.html.haml +++ b/app/views/spree/admin/reports/xero_invoices.html.haml @@ -32,16 +32,4 @@ .four.columns.alpha= button t(:search) -%table#listing_invoices.index - %thead - %tr - - @report.header.each do |header| - %th= header - %tbody - - @report.table.each do |row| - %tr - - row.each do |column| - %td= column - - if @report.table.empty? - %tr - %td{:colspan => "2"}= t(:none) += render "table", id: "listing_invoices" diff --git a/app/views/spree/admin/shared/_tabs.html.erb b/app/views/spree/admin/shared/_tabs.html.erb index 2228d4ac7b..eb76d8b98f 100644 --- a/app/views/spree/admin/shared/_tabs.html.erb +++ b/app/views/spree/admin/shared/_tabs.html.erb @@ -1,5 +1,5 @@ <%= tab :dashboard, :route => :admin, :icon => 'icon-dashboard' %> <%= tab :orders, :payments, :creditcard_payments, :shipments, :credit_cards, :return_authorizations, :url => admin_orders_path('q[s]' => 'completed_at desc'), :icon => 'icon-shopping-cart' %> -<%= tab :products , :option_types, :properties, :prototypes, :variants, :product_properties, :taxons, :url => bulk_edit_admin_products_path, :icon => 'icon-th-large' %> +<%= tab :products , :option_types, :properties, :prototypes, :variants, :product_properties, :taxons, :url => admin_products_path, :icon => 'icon-th-large' %> <%= tab :reports, :icon => 'icon-file' %> <%= tab :configurations, :general_settings, :mail_methods, :tax_categories, :zones, :states, :payment_methods, :inventory_settings, :taxonomies, :shipping_methods, :trackers, :label => 'configuration', :icon => 'icon-wrench', :url => edit_admin_general_settings_path %> diff --git a/app/views/spree/checkout/payment/_stripe.html.haml b/app/views/spree/checkout/payment/_stripe.html.haml index 3c1bc0c35e..eace9f00a7 100644 --- a/app/views/spree/checkout/payment/_stripe.html.haml +++ b/app/views/spree/checkout/payment/_stripe.html.haml @@ -1,4 +1,4 @@ -.row{ "ng-show" => "savedCreditCards != null && savedCreditCards.length > 0" } +.row{ "ng-show" => "savedCreditCards.length > 0" } .small-12.columns %h6= t('.used_saved_card') %select{ name: "selected_card", required: false, ng: { model: "secrets.selected_card", options: "card.id as card.formatted for card in savedCreditCards" } } diff --git a/app/views/spree/users/_saved_cards.html.haml b/app/views/spree/users/_saved_cards.html.haml index d8c710607b..d37d6c6a86 100644 --- a/app/views/spree/users/_saved_cards.html.haml +++ b/app/views/spree/users/_saved_cards.html.haml @@ -3,11 +3,14 @@ %th= t(:card_type) %th= t(:card_number) %th= t(:card_expiry_date) - %th= t(:delete?) + %th= t('.default?') + %th= t('.delete?') %tr.card{ id: "card{{ card.id }}", ng: { repeat: "card in savedCreditCards" } } %td.brand{ ng: { bind: '::card.brand' } } %td.number{ ng: { bind: '::card.number' } } %td.expiry{ ng: { bind: '::card.expiry' } } + %td.is-default + %input{ type: 'radio', name: 'default_card', ng: { model: 'card.is_default', change: 'setDefault(card)', value: "true"} } %td.actions %a{"rel" => "nofollow", "data-method" => "delete", "ng-href" => "{{card.delete_link}}" } = t(:delete) diff --git a/config/application.yml.example b/config/application.yml.example index a554404a45..03b33f9f11 100644 --- a/config/application.yml.example +++ b/config/application.yml.example @@ -43,6 +43,9 @@ SMTP_PASSWORD: 'f00d' # optional, see: https://www.skylight.io/oss # SKYLIGHT_AUTHENTICATION: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +# Should be set if using Skylight, adds a link to Skylight dashboard to our footer +# SKYLIGHT_PUBLIC_DASHBOARD_URL: "https://oss.skylight.io/app/applications/xxxxxxxxxx" + # Stripe Connect details for instance account # Find these under 'API keys' and 'Connect' in your Stripe account dashboard -> Account Settings # Under 'Connect', the Redirect URI should be set to https://YOUR_SERVER_URL/stripe/callbacks (e.g. https://openfoodnetwork.org.uk/stripe/callbacks) diff --git a/config/database.yml b/config/database.yml index 7eef2396f9..7f55b6c499 100644 --- a/config/database.yml +++ b/config/database.yml @@ -16,7 +16,6 @@ test: username: ofn password: f00d -#not used with heroku production: adapter: postgresql encoding: unicode diff --git a/config/environments/development.rb b/config/environments/development.rb index f8a177076f..22bf2a1945 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -35,8 +35,3 @@ Openfoodnetwork::Application.configure do config.action_mailer.delivery_method = :letter_opener config.action_mailer.default_url_options = { host: "0.0.0.0:3000" } end - - -# Load heroku vars from local file -heroku_env = File.join(Rails.root, 'config', 'heroku_env.rb') -load(heroku_env) if File.exists?(heroku_env) diff --git a/config/initializers/rack_rewrite.rb b/config/initializers/rack_rewrite.rb new file mode 100644 index 0000000000..98c93a79f3 --- /dev/null +++ b/config/initializers/rack_rewrite.rb @@ -0,0 +1,7 @@ +module Openfoodnetwork + class Application < Rails::Application + config.middleware.insert_before(Rack::Lock, Rack::Rewrite) do + r301 '/admin/products/bulk_edit', '/admin/products' # TODO: Date added 15/06/2018 + end + end +end diff --git a/config/locales/de_DE.yml b/config/locales/de_DE.yml index 27b3bf40f4..73551f49fd 100644 --- a/config/locales/de_DE.yml +++ b/config/locales/de_DE.yml @@ -10,14 +10,20 @@ de_DE: email: E-Mail des Kunden spree/payment: amount: Menge + order_cycle: + orders_close_at: Schlussdatum errors: models: spree/user: attributes: email: - taken: "Es gibt bereits einen Account für diese Email-Adresse. Bitte versuche Dich einzuloggen oder setze Dein Passwort zurück." + taken: "Es gibt bereits ein Konto für diese E-Mail-Adresse. Bitte versuchen Sie sich einzuloggen oder setzen Sie Ihr Passwort zurück." spree/order: no_card: Es sind keine gültigen Kreditkarten verfügbar + order_cycle: + attributes: + orders_close_at: + after_orders_open_at: muss nach dem Öffnungsdatum sein activemodel: errors: models: @@ -25,21 +31,21 @@ de_DE: attributes: subscription_line_items: at_least_one_product: "^ Bitte fügen Sie mindestens ein Produkt hinzu" - not_available: "^ %{name} ist im ausgewählten seitplan nicht verfügbar" + not_available: "^ %{name} ist im ausgewählten Plan nicht verfügbar" ends_at: - after_begins_at: "Muss danach sein beginnt um" + after_begins_at: "Muss nach \"beginnt um\" sein " customer: does_not_belong_to_shop: "gehört nicht zu %{shop}" schedule: not_coordinated_by_shop: "wird nicht von %{shop} koordiniert" payment_method: not_available_to_shop: "ist nicht verfügbar für %{shop}" - invalid_type: "muss eine Cash- oder Stripe-Methode sein" + invalid_type: "muss eine Bar- oder Stripe-Methode sein" shipping_method: not_available_to_shop: "ist nicht verfügbar für %{shop}" credit_card: not_available: "ist nicht verfügbar" - blank: "Wird benötigt" + blank: "wird benötigt" devise: confirmations: send_instructions: "Sie erhalten in einigen Minuten eine E-Mail mit Anweisungen zur Bestätigung Ihres Kontos." @@ -53,11 +59,15 @@ de_DE: failure: invalid: | Ungültige E-Mail-Adresse oder Passwort. - Hast du als Gast bestellt? Vielleicht brauchst du ein neues Konto oder musst dein Passwort zurücksetzen. + Haben Sie als Gast bestellt? Vielleicht brauchen Sie ein neues Konto oder müssen Ihr Passwort zurücksetzen. unconfirmed: "Sie müssen Ihr Konto bestätigen, bevor Sie fortfahren." + already_registered: "Diese E-Mail ist bereits registriert. Bitte loggen Sie sich ein oder verwenden Sie eine andere E-Mail-Adresse." + user_passwords: + spree_user: + updated_not_active: "Ihr Passwort wurde zurückgesetzt, aber ihre E-Mail muss noch bestätigt werden." enterprise_mailer: confirmation_instructions: - subject: "Bitte bestätige die E-Mail-Adresse für %{enterprise}" + subject: "Bitte bestätigen Sie die E-Mail-Adresse für %{enterprise}" welcome: subject: "%{enterprise} ist jetzt auf %{sitename}" invite_manager: @@ -69,13 +79,13 @@ de_DE: placement_summary_email: subject: Eine Zusammenfassung der kürzlich aufgegebenen Abonnementbestellungen greeting: "Hallo %{name}," - intro: "Im Folgenden finden Sie eine Zusammenfassung der Abonnementaufträge, die gerade für %{shop} erteilt wurden." + intro: "Im Folgenden finden Sie eine Zusammenfassung der Abonnementbestellungen, die gerade für %{shop} erteilt wurden." confirmation_summary_email: subject: Eine Zusammenfassung der kürzlich bestätigten Abonnementbestellungen greeting: "Hallo %{name}," - intro: "Im Folgenden finden Sie eine Zusammenfassung der Abonnementaufträge, die gerade für %{shop} abgeschlossen wurden." + intro: "Im Folgenden finden Sie eine Zusammenfassung der Abonnementbestellungen, die gerade für %{shop} abgeschlossen wurden." summary_overview: - total: Insgesamt wurden die Abonnements %{count} für die automatische Verarbeitung markiert. + total: Insgesamt wurden %{count} Abonnements für die automatische Verarbeitung markiert. success_zero: Von diesen wurde keine erfolgreich verarbeitet. success_some: Von diesen wurde %{count} erfolgreich verarbeitet. success_all: Alle wurden erfolgreich verarbeitet. @@ -87,7 +97,7 @@ de_DE: explainer: Diese Bestellungen wurden bearbeitet, aber für einige angeforderte Artikel war nicht genügend Lagerbestand verfügbar empty: title: Keine Lagerbestellung (%{count} Bestellungen) - explainer: Diese Bestellungen konnten nicht bearbeitet werden, da für angeforderte Artikel kein Bestand verfügbar war + explainer: Diese Bestellungen konnten nicht bearbeitet werden, da für die angeforderten Artikel kein Bestand verfügbar war complete: title: Bereits verarbeitet (%{count} Bestellungen) explainer: Diese Bestellungen wurden bereits als vollständig markiert und daher nicht verändert @@ -106,10 +116,10 @@ de_DE: site_meta_description: "Wir starten von Grund auf, in dem die Landwirte und Landwirtinnen sowie die Erzeuger und Erzeugerinnen Ihre Geschichten voll Stolz und wahrhaftig erzählen können. Und indem die Verteilenden uns Konsumierende gerecht und ehrlich Zugang zu den Produkten bieten können. Mit Konsumierenden, die glauben, dass dies die bessere Entscheidung für den Wocheneinkauf darstellen kann." search_by_name: Suche nach Name oder Ort... producers_join: 'Wir laden Deutsche Produzenten ein, jetzt dem Open Food Network beizutreten. ' - charges_sales_tax: Gebühren GST? + charges_sales_tax: Berechnet Steuern? print_invoice: "Rechnung drucken" print_ticket: "Ticket drucken" - select_ticket_printer: "Wähle einen Drucker für die Tickets" + select_ticket_printer: "Wählen Sie einen Drucker für die Tickets" send_invoice: "Rechnung senden" resend_confirmation: "Bestätigung erneut senden" view_order: "Bestellung zeigen" @@ -143,13 +153,13 @@ de_DE: loading: Laden... show_more: Mehr anzeigen show_all: Alles anzeigen - show_all_with_more: "Alles anzeigen (%{num} More)" + show_all_with_more: "Alles anzeigen (%{num} mehr)" cancel: Abbrechen edit: Bearbeite clone: Klonen distributors: Verteilende distribution: Verteilung - bulk_order_management: Sammelbestellung + bulk_order_management: Massenbearbeitung von Bestellungen enterprise_groups: Gruppen reports: Berichte variant_overrides: Inventar @@ -157,12 +167,12 @@ de_DE: all: Alle current: Aktuell available: Verfügbar - dashboard: Instrumententafel + dashboard: Übersicht undefined: undefiniert unused: ungebraucht admin_and_handling: Admin & Handhabung profile: Profil - supplier_only: Nur für Versorgende + supplier_only: Nur Anbieter weight: Gewicht volume: Menge items: Artikel @@ -185,19 +195,19 @@ de_DE: pick_up: Abholen copy: Kopieren actions: - create_and_add_another: "Erstellen und Hinzufügen eines anderen" + create_and_add_another: "Erstellen und weitere hinzufügen" admin: begins_at: Beginnt um - begins_on: Beginnt an + begins_on: Beginnt am customer: Kunde date: Datum - email: 'E-Mail:' - ends_at: Endet am + email: E-Mail + ends_at: Endet um ends_on: Endet am name: Name on_hand: verfügbar on_demand: Auf Anfrage - on_demand?: Auf Anfrage? + on_demand?: Unbegrenzt? order_cycle: Bestellungszyklus payment: Zahlung payment_method: Bezahlverfahren @@ -207,7 +217,7 @@ de_DE: image: Bild product: Produkt quantity: Menge - schedule: Zeitplan + schedule: Plan shipping: Versand shipping_method: Versandart shop: Shop @@ -227,7 +237,10 @@ de_DE: form_invalid: "Das Formular beinhaltet fehlende oder ungültige Felder" clear_filters: Filter löschen clear: klar - show_more: Zeig mehr + save: Speichern + cancel: Abrechen + back: Zurück + show_more: Mehr zeigen show_n_more: Zeige %{num} mehr choose: "Wählen..." please_select: Bitte auswählen... @@ -238,7 +251,7 @@ de_DE: whats_this: Was ist das? tag_has_rules: "Vorhandene Regeln für dieses Tag: %{num}" has_one_rule: "hat eine Regel" - has_n_rules: "hat %{num} Regel" + has_n_rules: "hat %{num} Regel(n)" unsaved_confirm_leave: "Es gibt ungespeicherte Änderungen auf dieser Seite. Möchten Sie ohne Speichern fortfahren?" unsaved_changes: "Sie haben ungespeicherte Änderungen" accounts_and_billing_settings: @@ -246,25 +259,25 @@ de_DE: default_accounts_payment_method: "Zahlungsart des Standardkontos" default_accounts_shipping_method: "Lieferart des Standardkontos" edit: - accounts_and_billing: "Konto und Rechung" + accounts_and_billing: "Konten und Abrechung" accounts_administration_distributor: "Kontenadministrator" admin_settings: "Einstellungen" update_invoice: "Rechnungen aktualisieren" auto_update_invoices: "Rechnungen automatisch jede Nacht um 1:00 Uhr aktualisieren" finalise_invoice: "Rechnungen fertigstellen" auto_finalise_invoices: "Rechnungen automatisch am 2. jedes Monats um 1.30 Uhr fertigstellen" - manually_run_task: "Task manuell ausführen" - update_user_invoice_explained: "Verwenden Sie diese Schaltfläche, um die Rechnungen für den jeweiligen Monat für jeden Unternehmensbenutzer im System sofort zu aktualisieren. Diese Aufgabe kann so eingerichtet werden, dass sie jede Nacht automatisch ausgeführt wird." + manually_run_task: "Vorgang manuell ausführen" + update_user_invoice_explained: "Verwenden Sie diesen Button, um die Rechnungen für den jeweiligen Monat für jeden Unternehmensbenutzer im System sofort zu aktualisieren. Diese Aufgabe kann so eingerichtet werden, dass sie jede Nacht automatisch ausgeführt wird." finalise_user_invoices: "Benutzerrechnungen fertigstellen" - finalise_user_invoice_explained: "Nütze diesen Button, um alle Rechnungen des vergangenen Kalendermonats in diesem System fertig zu machen." + finalise_user_invoice_explained: "Verwenden Sie diesen Button, um die Rechnungen für den Vormonat zu aktualisieren. Diese Aufgabe kann so eingerichtet werden, dass sie einmal im Monat automatisch abläuft." update_user_invoices: "Benutzerrechungen aktualisieren" errors: - accounts_distributor: Bitte wählen, wenn Du eine Rechnung für Unternehmen erstellen möchtest. - default_payment_method: Bitte wählen, wenn Du eine Rechnung für Unternehmen erstellen möchtest. - default_shipping_method: Bitte wählen, wenn Du eine Rechnung für Unternehmen erstellen möchtest. + accounts_distributor: Muss gewählt werden, wenn Sie Rechnungen für Unternehmen erstellen möchten. + default_payment_method: Muss gewählt werden, wenn Sie Rechnungen für Unternehmen erstellen möchten. + default_shipping_method: Muss gewählt werden, wenn Sie Rechnungen für Unternehmen erstellen möchten. shopfront_settings: embedded_shopfront_settings: "Eingebettete Shopfront-Einstellungen" - enable_embedded_shopfronts: "Aktivieren Sie eingebettete Shopfronts" + enable_embedded_shopfronts: "Eingebettete Shopfronts erlauben" embedded_shopfronts_whitelist: "Externe Domains Whitelist" number_localization: number_localization_settings: "Nummernlokalisierungseinstellungen" @@ -274,23 +287,23 @@ de_DE: business_model_configuration: "Geschäftsmodel" business_model_configuration_tip: "Konfigurieren Sie die Rate, mit der die Geschäfte jeden Monat für die Nutzung des Open Food Network berechnet werden." bill_calculation_settings: "Rechnungsberechnungseinstellungen" - bill_calculation_settings_tip: "Passen Sie den Betrag an, den Unternehmen jeden Monat für die Nutzung des OFN in Rechnung stellen." - shop_trial_length: "Shop Testlänge (Tage)" + bill_calculation_settings_tip: "Passen Sie den Betrag an, der Unternehmen jeden Monat für die Nutzung des OFN in Rechnung gestellt wird." + shop_trial_length: "Laden Testlänge (Tage)" shop_trial_length_tip: "Die Zeitdauer (in Tagen), die Unternehmen, die als Geschäfte eingerichtet sind, als Testphase ausführen können." - fixed_monthly_charge: "Monatliche Fixkosten" - fixed_monthly_charge_tip: "Die monatlichen Fixkosten aller Unternehmen, die als Laden laufen und ein Minimum an rechnungsfähigem Umsatz erreicht haben (falls gemacht)" + fixed_monthly_charge: "feste Monatsgebühren" + fixed_monthly_charge_tip: "Die monatliche Gebühr für Unternehmen, die als Laden laufen und ein Minimum an rechnungsfähigem Umsatz erreicht haben (falls verwendet)." percentage_of_turnover: "Prozentsatz des Umsatzes" - percentage_of_turnover_tip: "Falls die größer als Null" - monthly_cap_excl_tax: "Monatliche Obergrenze (exkl. GST)" - monthly_cap_excl_tax_tip: "Falls er größer als Null ist, wird dieser Wert als Obergrenze für den Betrag, den die Läden jeden Monat zahlen müssen, festgelegt." + percentage_of_turnover_tip: "Falls nicht null, wird dieser Prozentsatz (0.0 - 1.0) dem Umsatz eines Ladens angerechnet und zu jeglichen Festgebühren (links) addiert, um daraus die monatliche Rechnung zu erstellen. " + monthly_cap_excl_tax: "Monatliche Obergrenze (exkl. Steuer)" + monthly_cap_excl_tax_tip: "Falls größer als Null, wird dieser Wert als Obergrenze für den Betrag, den die Läden jeden Monat zahlen müssen, festgelegt." tax_rate: "Steuerrate" tax_rate_tip: "Steuersatz, der für die monatliche Rechnung gilt, die den Unternehmen für die Nutzung des Systems in Rechnung gestellt wird." - minimum_monthly_billable_turnover: "Minimaler monatlicher abrechenbarer Umsatz" - minimum_monthly_billable_turnover_tip: "Der monatliche Mindestumsatz vor einer Ladenfront wird für die Nutzung von OFN berechnet. Unternehmen, die in einem Monat weniger als diesen Betrag abwickeln, werden weder als Prozentsatz noch als Festsatz berechnet." - example_bill_calculator: "Beispiel Rechnungsrechner" + minimum_monthly_billable_turnover: "Monatlicher Mindestumsatz" + minimum_monthly_billable_turnover_tip: "Der monatliche Mindestumsatz unter dem ein Laden nicht berechnet wird. Geschäften die weniger umsetzen wird weder eine Festgebühr noch ein Prozentsatz in Rechnung gestellt. " + example_bill_calculator: "Beispielrechner" example_bill_calculator_legend: "Ändern Sie den Beispielumsatz, um den Effekt der Einstellungen auf der linken Seite zu visualisieren." example_monthly_turnover: "Beispiel monatlicher Umsatz" - example_monthly_turnover_tip: "Ein beispielhafter monatlicher Umsatz für ein Unternehmen, der zum Generieren verwendet wird, berechnet eine beispielhafte monatliche Rechnung unten." + example_monthly_turnover_tip: "Ein beispielhafter monatlicher Umsatz eines Unternehmens, mit dem unten eine Beispiel-Monatsrechnung erstellt wird." cap_reached?: "Limit erreicht ?" cap_reached?_tip: "Ob die Kappe (links angegeben) erreicht wurde, unter Berücksichtigung der Einstellungen und des Umsatzes." included_tax: "inkl. Steuer" @@ -303,27 +316,27 @@ de_DE: new_customer: "Neuer Kunde" customer_placeholder: "Kunde@beispiel.org" valid_email_error: Bitte geben Sie eine gültige E-Mail-Adresse ein - add_a_new_customer_for: Fügen Sie einen neuen Kunden für %{shop_name} hinzu - code: Code - duplicate_code: "Dieser Code wird bereits verwendet." + add_a_new_customer_for: Neuer Kunde für %{shop_name} hinzufügen + code: Kode + duplicate_code: "Dieser Kode wird bereits verwendet." bill_address: "Rechnungsadresse" ship_address: "Lieferadresse" update_address_success: 'Adresse wurde erfolgreich aktualisiert.' - update_address_error: 'Es tut uns leid! Bitte fülle alle erforderlichen Felder aus!' - edit_bill_address: 'Bearbeite die Rechnungsadresse.' - edit_ship_address: 'Bearbeite die Lieferadresse.' + update_address_error: 'Es tut uns leid! Bitte füllen Sie alle erforderlichen Felder aus!' + edit_bill_address: 'Rechnungsadresse bearbeiten' + edit_ship_address: 'Lieferadresse bearbeiten' required_fileds: 'Die erforderlichen Felder sind mit einem Sternchen gekennzeichnet.' - select_country: 'Wähle das Bundesland/Land.' - select_state: 'Wähle das Land/den Staat.' - edit: 'Bearbeite' - update_address: 'Aktualisiere die Adresse.' - confirm_delete: 'Sicher oder löschen?' - search_by_email: "Suche mittels Emailadresse/Code" + select_country: 'Land wählen' + select_state: 'Bundesland wählen' + edit: 'Bearbeiten' + update_address: 'Adresse aktualisieren' + confirm_delete: 'Sicher zu löschen?' + search_by_email: "Suche nach Email/Kode" destroy: - has_associated_orders: 'Löschen fehlgeschlagen: Kunde hat Bestellungen mit seinem Shop verknüpft' + has_associated_orders: 'Löschen fehlgeschlagen: Kunde hat Bestellungen mit diesem Laden' cache_settings: show: - title: Zwischenspeichern + title: Cachen distributor: Verteiler order_cycle: Bestellungszyklus status: Status @@ -335,10 +348,10 @@ de_DE: header: Header home_page: Homepage producer_signup_page: Hersteller-Anmeldeseite - hub_signup_page: Hub Anmeldeseite - group_signup_page: Gruppe Anmeldeseite + hub_signup_page: Hub-Anmeldeseite + group_signup_page: Gruppen-Anmeldeseite footer_and_external_links: Fußzeile und externe Links - your_content: Dein Inhalt + your_content: Ihr Inhalt enterprise_fees: index: title: Unternehmensgebühren @@ -357,25 +370,103 @@ de_DE: enterprise_role: manages: verwaltet products: - unit_name_placeholder: 'z.B. Trauben' - bulk_edit: + unit_name_placeholder: 'z.B. Bündel' + index: unit: Einheit - display_as: Darstellen als + display_as: Angezeigt als category: Kategorie tax_category: Steuerkategorie inherits_properties?: Vererbt Eigenschaften? - available_on: Verfügbar auf - av_on: "Ein V. Auf" - upload_an_image: Lade ein Bild hoch + available_on: Verfügbar am + av_on: "Verfüg. am" + import_date: Importiert + upload_an_image: Bild hochladen + product_search_keywords: Keywords für die Produktsuche + product_search_tip: Geben Sie Wörter ein, um Ihre Produkte in den Geschäften zu suchen. Verwenden Sie Leerzeichen, um jedes Keyword zu trennen. + SEO_keywords: SEO Schlüsselwörter + seo_tip: Geben Sie Wörter ein, um Ihre Produkte im Internet zu durchsuchen. Verwenden Sie Leerzeichen, um jedes Keyword zu trennen. + Search: Suche properties: - property_name: Name des Anwesens + property_name: Name der Eigenschaft inherited_property: Vererbte Eigenschaft variants: - to_order_tip: "Artikel, die auf Bestellung hergestellt werden, haben keinen festgelegten Lagerbestand, wie zum Beispiel frisch gebackene Brote." + to_order_tip: "Artikel, die auf Bestellung hergestellt werden, haben keinen festgelegten Lagerbestand." product_distributions: "Produktverteilungen" group_buy_options: "Gruppenkaufoptionen" - seo: "SEO" back_to_products_list: "Zurück zur Produktliste" + product_import: + title: Produkt importieren + file_not_found: Datei nicht gefunden oder konnte nicht geöffnet werden + no_data: Keine Daten in der Tabelle gefunden + confirm_reset: "Dadurch wird der Lagerbestand für alle Produkte auf Null gesetzt\n Unternehmen, die in der hochgeladenen Datei nicht vorhanden sind" + model: + no_file: "Fehler: keine Datei hochgeladen" + could_not_process: "Datei konnte nicht verarbeitet werden: ungültiger Dateityp" + incorrect_value: falscher Wert + conditional_blank: Kann nicht leer sein, wenn unit_type leer ist + no_product: hat keine Produkte in der Datenbank gefunden + not_found: nicht in der Datenbank gefunden + blank: kann nicht leer sein + products_no_permission: Sie sind nicht berechtigt, Produkte für dieses Unternehmen zu verwalten + inventory_no_permission: Sie sind nicht berechtigt, Inventar für diesen Produzenten zu erstellen + none_saved: hat keine Produkte erfolgreich gespeichert + line: Linie + index: + select_file: Wählen Sie eine Tabelle zum Hochladen aus + spreadsheet: Kalkulationstabelle + import_into: "Importieren in:" + product_list: Produktliste + inventories: Vorräte + import: Einführen + upload: Hochladen + import: + review: Rezension + proceed: Vorgehen + save: Speichern + results: Ergebnisse + save_imported: Speichern Sie importierte Produkte + no_valid_entries: Keine gültigen Einträge gefunden + none_to_save: Es gibt keine Einträge, die gespeichert werden können + some_invalid_entries: Importierte Datei enthält einige ungültige Einträge + save_valid?: Gültige Einträge für jetzt speichern und die anderen verwerfen? + no_errors: Keine Fehler gefunden! + save_all_imported?: Alle importierten Produkte speichern? + options_and_defaults: Importoptionen und Standardwerte + no_permission: Sie sind nicht berechtigt, dieses Unternehmen zu verwalten + not_found: Unternehmen konnte nicht in der Datenbank gefunden werden + no_name: Kein Name + blank_supplier: Einige Produkte haben einen leeren Lieferantennamen + reset_absent?: Fehlende Produkte zurücksetzen? + overwrite_all: Alles überschreiben + overwrite_empty: Überschreiben, wenn leer + default_stock: Stellen Sie den Lagerbestand ein + default_tax_cat: Stellen Sie die Steuerkategorie ein + default_shipping_cat: Legen Sie die Versandkategorie fest + default_available_date: Stellen Sie das verfügbare Datum ein + validation_overview: Validierungsübersicht importieren + entries_found: Einträge in der importierten Datei gefunden + entries_with_errors: Artikel enthalten Fehler und werden nicht importiert + products_to_create: Produkte werden erstellt + products_to_update: Produkte werden aktualisiert + inventory_to_create: Inventargegenstände werden erstellt + inventory_to_update: Inventargegenstände werden aktualisiert + products_to_reset: Bestehende Produkte werden auf Null zurückgesetzt + inventory_to_reset: Bestehende Inventarartikel werden auf Null zurückgesetzt + line: Linie + item_line: Artikelzeile + save: + final_results: Importieren Sie die endgültigen Ergebnisse + products_created: Produkte erstellt + products_updated: Produkte aktualisiert + inventory_created: Inventarelemente erstellt + inventory_updated: Inventargegenstände aktualisiert + products_reset: Bei den Produkten wurde der Lagerbestand auf Null zurückgesetzt + inventory_reset: Bei Inventarartikeln wurde der Lagerbestand auf Null zurückgesetzt + all_saved: "Alle Artikel wurden erfolgreich gespeichert" + some_saved: "Elemente wurden erfolgreich gespeichert" + save_errors: Fehler speichern + view_products: Produkte anzeigen + view_inventory: Inventar anzeigen variant_overrides: loading_flash: loading_inventory: LADEN INVENTAR @@ -386,6 +477,7 @@ de_DE: inherit?: Übernehme add: Füge hinzu hide: Verstecke + import_date: Importiert select_a_shop: Wähle einen Laden review_now: Überprüfe jetzt new_products_alert_message: Es sind %{new_product_count} neue Produkte verfügbar, die ins Sortiment mit aufgenommen werden können @@ -415,7 +507,7 @@ de_DE: product_unit: "Produkt: Einheit" weight_volume: "Gewicht / Volumen" ask: "Fragen?" - page_title: "Sammelbestellung" + page_title: "Massenbearbeitung von Bestellungen" actions_delete: "Ausgewählte löschen" loading: "Bestellungen werden geladen" no_results: "Keine Bestellungen gefunden." @@ -549,19 +641,19 @@ de_DE: es wieder geöffnet wird. Dies wird in Ihrem Shop nur angezeigt, wenn Sie keine aktiven Bestellzyklen haben (dh Shop ist geschlossen). shopfront_category_ordering: Shopfront Kategorie Bestellung - open_date: Offenes Datum + open_date: Öffnungsdatum close_date: Abschlussdatum social: twitter_placeholder: z.B. @the_prof stripe_connect: connect_with_stripe: "Verbinde dich mit Streifen" stripe_connect_intro: "Um Zahlungen mit Kreditkarte zu akzeptieren, müssen Sie Ihr Stripe-Konto mit dem Open Food Network verbinden. Verwenden Sie den Knopf rechts, um loszulegen." - stripe_account_connected: "Streifenkonto verbunden." + stripe_account_connected: "Stripe-Konto verbunden." disconnect: "Trennen Sie das Konto" confirm_modal: title: Verbinde dich mit Streifen part1: Stripe ist ein Zahlungsverarbeitungsdienst, der es Geschäften im OFN ermöglicht, Kreditkartenzahlungen von Kunden zu akzeptieren. - part2: Um diese Funktion zu verwenden, müssen Sie Ihr Stripe-Konto mit dem OFN verbinden. Wenn Sie unten auf "Ich stimme zu" klicken, wird die Stripe-Website an Sie weitergeleitet, wo Sie ein bestehendes Stripe-Konto verbinden oder ein neues erstellen können, falls Sie noch kein Konto haben. + part2: Um diese Funktion zu verwenden, müssen Sie Ihr Stripe-Konto mit dem OFN verbinden. Wenn Sie unten auf "Ich stimme zu" klicken, wird die Stripe-Website an Sie weitergeleitet, wo Sie ein bestehendes Stripe-Konto verbinden oder ein neues erstellen können. part3: Dadurch kann das Open Food Network Kreditkartenzahlungen von Kunden in Ihrem Namen akzeptieren. Bitte beachten Sie, dass Sie ein eigenes Stripe-Konto unterhalten müssen, die Gebühren für Stripe-Gebühren bezahlen und etwaige Rückbuchungen und Kundenservice selbst vornehmen müssen. i_agree: Ich stimme zu cancel: Stornieren @@ -649,7 +741,7 @@ de_DE: welcome_title: Willkommen im Open Food Netzwerk! welcome_text: Sie haben erfolgreich eine erstellt next_step: Nächster Schritt - choose_starting_point: 'Wählen Sie Ihren Ausgangspunkt:' + choose_starting_point: 'Wählen Sie Ihr Paket:' invite_manager: user_already_exists: "Benutzer existiert bereits" error: "Etwas ist schief gelaufen" @@ -709,7 +801,7 @@ de_DE: name: Name orders_open: Bestellungen öffnen um coordinator: Koordinator - order_closes: Bestellungen schließen + orders_close: Bestellungen schließen row: suppliers: Lieferanten distributors: Händler @@ -724,6 +816,8 @@ de_DE: destroy_errors: orders_present: Dieser Bestellzyklus wurde von einem Kunden ausgewählt und kann nicht gelöscht werden. Um zu verhindern, dass Kunden darauf zugreifen, schließen Sie es stattdessen. schedule_present: Dieser Bestellzyklus ist mit einem Zeitplan verknüpft und kann nicht gelöscht werden. Bitte heben Sie die Verknüpfung auf oder löschen Sie den Zeitplan zuerst. + bulk_update: + no_data: Hm, etwas ist schief gelaufen. Keine Bestellzyklusdaten gefunden. producer_properties: index: title: Herstellereigenschaften @@ -820,7 +914,7 @@ de_DE: enable_subscriptions_step_1_html: 1. Gehen Sie zur Seite %{enterprises_link}, suchen Sie Ihren Shop und klicken Sie auf "Verwalten" enable_subscriptions_step_2: 2. Aktivieren Sie unter "Shop-Einstellungen" die Option Abonnements set_up_shipping_and_payment_methods_html: Richten Sie die Methoden %{shipping_link} und %{payment_link} ein - set_up_shipping_and_payment_methods_note_html: Beachten Sie, dass nur Cash- und Stripe-Zahlungsmethoden für Abonnements verwendet werden dürfen + set_up_shipping_and_payment_methods_note_html: Beachten Sie, dass nur Bar- und Stripe-Zahlungsmethoden für Abonnements verwendet werden dürfen ensure_at_least_one_customer_html: Stellen Sie sicher, dass mindestens eine %{customer_link} vorhanden ist create_at_least_one_schedule: Erstellen Sie mindestens einen Zeitplan create_at_least_one_schedule_step_1_html: 1. Gehen Sie auf die Seite %{order_cycles_link} @@ -836,7 +930,7 @@ de_DE: details: details: Einzelheiten invalid_error: Hoppla! Bitte füllen Sie alle erforderlichen Felder aus ... - allowed_payment_method_types_tip: Zurzeit können nur Cash- und Stripe-Zahlungsmethoden verwendet werden + allowed_payment_method_types_tip: Zurzeit können nur Bar- und Stripe-Zahlungsmethoden verwendet werden credit_card: Kreditkarte no_cards_available: Keine Karten verfügbar loading_flash: @@ -865,12 +959,15 @@ de_DE: no_subscriptions: Noch keine Abonnements why_dont_you_add_one: Warum fügst du keinen hinzu? :) no_matching_subscriptions: Keine passenden Abonnements gefunden + schedules: + destroy: + associated_subscriptions_error: Dieser Zeitplan kann nicht gelöscht werden, da ihm Subskriptionen zugeordnet sind stripe_connect_settings: edit: title: "Streifen verbinden" settings: "die Einstellungen" stripe_connect_enabled: Shops aktivieren, um Zahlungen über Stripe Connect zu akzeptieren? - no_api_key_msg: Für dieses Unternehmen existiert kein Streifenkonto. + no_api_key_msg: Für dieses Unternehmen existiert kein Stripe-Konto. configuration_explanation_html: Detaillierte Anweisungen zur Konfiguration der Stripe Connect-Integration finden Sie unter konsultieren Sie diese Anleitung . status: Status ok: OK @@ -911,8 +1008,9 @@ de_DE: register: "registrieren" contact: "Kontakt" require_customer_login: "Dieser Shop ist nur für Kunden." - require_login_html: "Bitte %{login}, wenn Sie bereits ein Konto haben. Andernfalls, %{register}, ein Kunde zu werden." + require_login_html: "Bitte %{login}, wenn Sie bereits ein Konto haben. Andernfalls, %{register}, um Kunde zu werden." require_customer_html: "Bitte %{contact} %{enterprise}, um Kunde zu werden." + card_could_not_be_updated: Die Karte konnte nicht aktualisiert werden card_could_not_be_saved: Karte konnte nicht gespeichert werden spree_gateway_error_flash_for_checkout: "Bei den Zahlungsinformationen ist ein Problem aufgetreten: %{error}" invoice_billing_address: "Rechnungsadresse:" @@ -1049,6 +1147,7 @@ de_DE: footer_legal_tos: "Geschäftsbedingungen" footer_legal_visit: "Finden Sie uns auf" footer_legal_text_html: "Open Food Network ist eine freie und Open-Source-Software-Plattform. Unser Inhalt ist mit %{content_license} und unserem Code mit %{code_license} lizenziert." + footer_skylight_dashboard_html: Leistungsdaten sind unter %{dashboard} verfügbar. home_shop: Jetzt einkaufen brandstory_headline: "Essen, ohne eigene Rechtspersönlichkeit." brandstory_intro: "Manchmal ist der beste Weg, das System zu reparieren, ein neues zu starten ..." @@ -1132,7 +1231,6 @@ de_DE: email_admin_html: "Sie können Ihr Konto verwalten, indem Sie sich bei %{link} anmelden oder indem Sie oben rechts auf der Startseite auf das Zahnrad klicken und Administration auswählen." email_community_html: "Wir haben auch ein Online-Forum für Community-Diskussionen in Bezug auf OFN-Software und die einzigartigen Herausforderungen eines Lebensmittelunternehmens. Sie werden ermutigt mitzumachen. Wir entwickeln uns ständig weiter und Ihr Beitrag in diesem Forum wird prägen, was als nächstes passiert. %{link}" join_community: "Trete der Community bei" - email_help: "Wenn Sie irgendwelche Schwierigkeiten haben, lesen Sie unsere FAQs, durchsuchen Sie das Forum oder posten Sie ein \"Support\" -Thema und jemand wird Ihnen helfen!" email_confirmation_activate_account: "Bevor wir Ihr neues Konto aktivieren können, müssen wir Ihre E-Mail-Adresse bestätigen." email_confirmation_greeting: "Hallo, %{contact}!" email_confirmation_profile_created: "Ein Profil für %{name} wurde erfolgreich erstellt! Um Ihr Profil zu aktivieren, müssen wir diese E-Mail-Adresse bestätigen." @@ -1194,9 +1292,9 @@ de_DE: invite_email: greeting: "Hallo!" invited_to_manage: "Sie wurden eingeladen, %{enterprise} auf %{instance} zu verwalten." - confirm_your_email: "Sie werden in Kürze eine E-Mail erhalten, um Ihre Registrierung zu bestätigen." + confirm_your_email: "Sie sollten eine E-Mail mit einem Bestätigungslink erhalten haben oder in Kürze erhalten. Sie können nicht auf das Profil von %{enterprise} zugreifen, bis Sie Ihre E-Mail-Adresse bestätigt haben." set_a_password: "Sie werden dann aufgefordert, ein Kennwort festzulegen, bevor Sie das Unternehmen verwalten können." - mistakenly_sent: "Nicht sicher, warum Sie diese E-Mail erhalten haben? Bitte kontaktieren Sie %{owner_email} für weitere Informationen, oder kontaktieren Sie %{instance} unter %{instance_email}." + mistakenly_sent: "Nicht sicher, warum Sie diese E-Mail erhalten haben? Bitte kontaktieren Sie %{owner_email} für weitere Informationen." producer_mail_greeting: "Liebe/r" producer_mail_text_before: "Wir haben jetzt alle Verbraucherbestellungen für den nächsten Essenstropfen." producer_mail_order_text: "Hier finden Sie eine Zusammenfassung der Bestellungen für Ihre Produkte:" @@ -1445,6 +1543,7 @@ de_DE: november: "November" december: "Dezember" email_not_found: "Emailadresse wurde nicht gefunden" + email_unconfirmed: "Sie müssen Ihre E-Mail-Adresse bestätigen, bevor Sie Ihr Passwort zurücksetzen können." email_required: "Sie müssen eine E-Mail-Adresse angeben" logging_in: "Moment, wir melden uns an" signup_email: "Deine E-Mail-Adresse" @@ -1696,6 +1795,8 @@ de_DE: calculator: "Rechner" calculator_values: "Rechnerwerte" flat_percent_per_item: "Flache Prozent (pro Artikel)" + flat_rate_per_item: "Pauschale (pro Stück)" + flat_rate_per_order: "Pauschalpreis pro Bestellung)" new_order_cycles: "Neue Bestellzyklen" new_order_cycle: "Neuer Bestellzyklus" select_a_coordinator_for_your_order_cycle: "Wählen Sie einen Koordinator für Ihren Bestellzyklus" @@ -1730,12 +1831,7 @@ de_DE: spree_admin_enterprises_fees: "Unternehmensgebühren" spree_admin_enterprises_none_create_a_new_enterprise: "ERSTELLEN SIE EIN NEUES UNTERNEHMEN" spree_admin_enterprises_none_text: "Sie haben noch keine Unternehmen" - spree_admin_enterprises_producers_name: "Name" - spree_admin_enterprises_producers_total_products: "Produkte insgesamt" - spree_admin_enterprises_producers_active_products: "Aktive Produkte" - spree_admin_enterprises_producers_order_cycles: "Produkte in OCs" spree_admin_enterprises_tabs_hubs: "HUBS" - spree_admin_enterprises_tabs_producers: "ERZEUGER" spree_admin_enterprises_producers_manage_products: "PRODUKTE VERWALTEN" spree_admin_enterprises_any_active_products_text: "Sie haben keine aktiven Produkte." spree_admin_enterprises_create_new_product: "NEUES PRODUKT" @@ -2000,7 +2096,6 @@ de_DE: content_configuration_pricing_table: "(TODO: Preistabelle)" content_configuration_case_studies: "(TODO: Fallstudien)" content_configuration_detail: "(Todo: Detail)" - enterprise_name_error: "wurde bereits genommen. Wenn dies Ihr Unternehmen ist und Sie die Eigentumsrechte beanspruchen möchten, wenden Sie sich bitte an den aktuellen Manager dieses Profils unter %{email}." enterprise_owner_error: "^ %{email} darf keine weiteren Unternehmen besitzen (Limit ist %{enterprise_limit})." enterprise_role_uniqueness_error: "^ Diese Rolle ist bereits vorhanden." inventory_item_visibility_error: muss wahr oder falsch sein @@ -2035,7 +2130,13 @@ de_DE: order_cycles_no_permission_to_coordinate_error: "Keines Ihrer Unternehmen ist berechtigt, einen Bestellzyklus zu koordinieren" order_cycles_no_permission_to_create_error: "Sie sind nicht berechtigt, einen von diesem Unternehmen koordinierten Bestellzyklus zu erstellen" back_to_orders_list: "Zurück zur Bestellliste" + order_information: "Bestellinformationen" + date_completed: "Datum abgeschlossen" amount: "Menge" + state_names: + ready: Bereit + pending: steht aus + shipped: Wird versendet js: saving: 'Speichern ...' changes_saved: 'Änderungen gespeichert' @@ -2057,6 +2158,9 @@ de_DE: enterprise_limit_reached: "Sie haben die Standardgrenze für Unternehmen pro Konto erreicht. Schreiben Sie an %{contact_email}, wenn Sie es erhöhen müssen." modals: got_it: Ich habs + close: "Schließen" + invite: "Einladen" + invite_title: "Laden Sie einen nicht registrierten Benutzer ein" tag_rule_help: title: Tag-Regeln overview: Überblick @@ -2237,6 +2341,8 @@ de_DE: email: Email account_updated: "Konto aktualisiert!" my_account: "Mein Konto" + date: "Datum" + time: "Zeit" admin: orders: invoice: @@ -2262,10 +2368,10 @@ de_DE: payment_methods: stripe_connect: enterprise_select_placeholder: Wählen... - loading_account_information_msg: Kontoinformationen von Stripe laden, bitte warten ... + loading_account_information_msg: Kontoinformationen von Stripe werden geladen, bitte warten ... stripe_disabled_msg: Streifenzahlungen wurden vom Systemadministrator deaktiviert. request_failed_msg: Es tut uns leid. Beim Versuch, Kontodaten mit Stripe zu überprüfen, ist ein Fehler aufgetreten. - account_missing_msg: Für dieses Unternehmen existiert kein Streifenkonto. + account_missing_msg: Für dieses Unternehmen existiert kein Stripe-Konto. connect_one: Verbinde eins access_revoked_msg: Der Zugriff auf dieses Stripe-Konto wurde widerrufen. Bitte verbinden Sie Ihr Konto erneut. status: Status @@ -2281,7 +2387,7 @@ de_DE: new: title: 'Neues Produkt' unit_name_placeholder: 'z.B. Trauben' - bulk_edit: + index: header: title: Massenbearbeitung von Produkten indicators: @@ -2386,18 +2492,18 @@ de_DE: issue_text: | Falls die URL unten nicht funktionieren sollte, versuche die Adresse mit Hilfe von "copy and paste" in dein Browserfenster zu übertragen. confirmation_instructions: - subject: Bitte bestätigen Sie Ihren OFN-Account + subject: Bitte bestätigen Sie Ihr OFN-Konto weight: Gewicht (pro kg) zipcode: Postleitzahl users: form: - account_settings: Account Einstellungen + account_settings: Konto Einstellungen show: tabs: orders: Aufträge cards: Kreditkarten transactions: Transaktionen - settings: Account Einstellungen + settings: Konto Einstellungen unconfirmed_email: "Ausstehende E-Mail-Bestätigung für: %{unconfirmed_email}. Ihre E-Mail-Adresse wird aktualisiert, sobald die neue E-Mail bestätigt wurde." orders: open_orders: Offene Bestellungen diff --git a/config/locales/en.yml b/config/locales/en.yml index 315ee8960c..35754e0bc2 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -117,6 +117,10 @@ en: Invalid email or password. Were you a guest last time? Perhaps you need to create an account or reset your password. unconfirmed: "You have to confirm your account before continuing." + already_registered: "This email address is already registered. Please log in to continue, or go back and use another email address." + user_passwords: + spree_user: + updated_not_active: "Your password has been reset, but your email has not been confirmed yet." enterprise_mailer: confirmation_instructions: subject: "Please confirm the email address for %{enterprise}" @@ -301,6 +305,9 @@ en: form_invalid: "Form contains missing or invalid fields" clear_filters: Clear Filters clear: Clear + save: Save + cancel: Cancel + back: Back show_more: Show more show_n_more: Show %{num} more choose: "Choose..." @@ -447,7 +454,7 @@ en: products: unit_name_placeholder: 'eg. bunches' - bulk_edit: + index: unit: Unit display_as: Display As category: Category @@ -455,6 +462,7 @@ en: inherits_properties?: Inherits Properties? available_on: Available On av_on: "Av. On" + import_date: Imported upload_an_image: Upload an image product_search_keywords: Product Search Keywords product_search_tip: Type words to help search your products in the shops. Use space to separate each keyword. @@ -470,6 +478,80 @@ en: group_buy_options: "Group Buy Options" back_to_products_list: "Back to products list" + product_import: + title: Product Import + file_not_found: File not found or could not be opened + no_data: No data found in spreadsheet + confirm_reset: "This will set stock level to zero on all products for this \n enterprise that are not present in the uploaded file" + model: + no_file: "error: no file uploaded" + could_not_process: "could not process file: invalid filetype" + incorrect_value: incorrect value + conditional_blank: can't be blank if unit_type is blank + no_product: did not match any products in the database + not_found: not found in database + blank: can't be blank + products_no_permission: you do not have permission to manage products for this enterprise + inventory_no_permission: you do not have permission to create inventory for this producer + none_saved: did not save any products successfully + line: Line + index: + select_file: Select a spreadsheet to upload + spreadsheet: Spreadsheet + import_into: "Import into:" + product_list: Product list + inventories: Inventories + import: Import + upload: Upload + import: + review: Review + proceed: Proceed + save: Save + results: Results + save_imported: Save imported products + no_valid_entries: No valid entries found + none_to_save: There are no entries that can be saved + some_invalid_entries: Imported file contains some invalid entries + save_valid?: Save valid entries for now and discard the others? + no_errors: No errors detected! + save_all_imported?: Save all imported products? + options_and_defaults: Import options and defaults + no_permission: you do not have permission to manage this enterprise + not_found: enterprise could not be found in database + no_name: No name + blank_supplier: some products have blank supplier name + reset_absent?: Reset absent products? + overwrite_all: Overwrite all + overwrite_empty: Overwrite if empty + default_stock: Set stock level + default_tax_cat: Set tax category + default_shipping_cat: Set shipping category + default_available_date: Set available date + validation_overview: Import validation overview + entries_found: Entries found in imported file + entries_with_errors: Items contain errors and will not be imported + products_to_create: Products will be created + products_to_update: Products will be updated + inventory_to_create: Inventory items will be created + inventory_to_update: Inventory items will be updated + products_to_reset: Existing products will have their stock reset to zero + inventory_to_reset: Existing inventory items will have their stock reset to zero + line: Line + item_line: Item line + save: + final_results: Import final results + products_created: Products created + products_updated: Products updated + inventory_created: Inventory items created + inventory_updated: Inventory items updated + products_reset: Products had stock level reset to zero + inventory_reset: Inventory items had stock level reset to zero + all_saved: "All items saved successfully" + some_saved: "items saved successfully" + save_errors: Save errors + view_products: View Products + view_inventory: View Inventory + variant_overrides: loading_flash: loading_inventory: LOADING INVENTORY @@ -480,6 +562,7 @@ en: inherit?: Inherit? add: Add hide: Hide + import_date: Imported select_a_shop: Select A Shop review_now: Review Now new_products_alert_message: There are %{new_product_count} new products available to add to your inventory. @@ -741,7 +824,7 @@ en: welcome_title: Welcome to the Open Food Network! welcome_text: You have successfully created a next_step: Next step - choose_starting_point: 'Choose your starting point:' + choose_starting_point: 'Choose your package:' invite_manager: user_already_exists: "User already exists" error: "Something went wrong" @@ -802,7 +885,7 @@ en: name: Name orders_open: Orders open at coordinator: Coordinator - order_closes: Orders close + orders_close: Orders close row: suppliers: suppliers distributors: distributors @@ -819,6 +902,10 @@ en: schedule_present: That order cycle is linked to a schedule and cannot be deleted. Please unlink or delete the schedule first. bulk_update: no_data: Hm, something went wrong. No order cycle data found. + date_warning: + msg: This order cycle is linked to %{n} open subscription orders. Changing this date now will not affect any orders which have already been placed, but should be avoided if possible. Are you sure you want to proceed? + cancel: Cancel + proceed: Proceed producer_properties: index: title: Producer Properties @@ -1029,6 +1116,7 @@ en: # Front-end controller translations + card_could_not_be_updated: Card could not be updated card_could_not_be_saved: card could not be saved spree_gateway_error_flash_for_checkout: "There was a problem with your payment information: %{error}" @@ -1183,6 +1271,8 @@ en: footer_legal_visit: "Find us on" footer_legal_text_html: "Open Food Network is a free and open source software platform. Our content is licensed with %{content_license} and our code with %{code_license}." + footer_skylight_dashboard_html: Performance data is available on %{dashboard}. + home_shop: Shop Now brandstory_headline: "Food, unincorporated." @@ -1628,6 +1718,7 @@ See the %{link} to find out more about %{sitename}'s features and to start using november: "November" december: "December" email_not_found: "Email address not found" + email_unconfirmed: "You must confirm your email address before you can reset your password." email_required: "You must provide an email address" logging_in: "Hold on a moment, we're logging you in" signup_email: "Your email" @@ -1930,13 +2021,7 @@ See the %{link} to find out more about %{sitename}'s features and to start using spree_admin_enterprises_fees: "Enterprise Fees" spree_admin_enterprises_none_create_a_new_enterprise: "CREATE A NEW ENTERPRISE" spree_admin_enterprises_none_text: "You don't have any enterprises yet" - spree_admin_enterprises_producers_name: "Name" - spree_admin_enterprises_producers_total_products: "Total Products" - spree_admin_enterprises_producers_active_products: "Active Products" - spree_admin_enterprises_producers_order_cycles: "Products in OCs" - spree_admin_enterprises_producers_order_cycles_title: "" spree_admin_enterprises_tabs_hubs: "HUBS" - spree_admin_enterprises_tabs_producers: "PRODUCERS" spree_admin_enterprises_producers_manage_products: "MANAGE PRODUCTS" spree_admin_enterprises_any_active_products_text: "You don't have any active products." spree_admin_enterprises_create_new_product: "CREATE A NEW PRODUCT" @@ -2203,7 +2288,7 @@ See the %{link} to find out more about %{sitename}'s features and to start using content_configuration_pricing_table: "(TODO: Pricing table)" content_configuration_case_studies: "(TODO: Case studies)" content_configuration_detail: "(TODO: Detail)" - enterprise_name_error: "has already been taken. If this is your enterprise and you would like to claim ownership, please contact the current manager of this profile at %{email}." + enterprise_name_error: "has already been taken. If this is your enterprise and you would like to claim ownership, or if you would like to trade with this enterprise please contact the current manager of this profile at %{email}." enterprise_owner_error: "^%{email} is not permitted to own any more enterprises (limit is %{enterprise_limit})." enterprise_role_uniqueness_error: "^That role is already present." inventory_item_visibility_error: must be true or false @@ -2238,6 +2323,7 @@ See the %{link} to find out more about %{sitename}'s features and to start using order_cycles_no_permission_to_coordinate_error: "None of your enterprises have permission to coordinate an order cycle" order_cycles_no_permission_to_create_error: "You don't have permission to create an order cycle coordinated by that enterprise" back_to_orders_list: "Back to order list" + no_orders_found: "No Orders Found" order_information: "Order Information" date_completed: "Date Completed" amount: "Amount" @@ -2262,6 +2348,7 @@ See the %{link} to find out more about %{sitename}'s features and to start using choose: Choose resolve_errors: Please resolve the following errors more_items: "+ %{count} More" + default_card_updated: Default Card Updated admin: enterprise_limit_reached: "You have reached the standard limit of enterprises per account. Write to %{contact_email} if you need to increase it." modals: @@ -2486,7 +2573,7 @@ See the %{link} to find out more about %{sitename}'s features and to start using new: title: 'New Product' unit_name_placeholder: 'eg. bunches' - bulk_edit: + index: header: title: Bulk Edit Products indicators: @@ -2502,6 +2589,7 @@ See the %{link} to find out more about %{sitename}'s features and to start using inherits_properties?: Inherits Properties? available_on: Available On av_on: "Av. On" + import_date: "Import Date" products_variant: variant_has_n_overrides: "This variant has %{n} override(s)" new_variant: "New variant" @@ -2514,6 +2602,8 @@ See the %{link} to find out more about %{sitename}'s features and to start using display_as: display_as: Display As reports: + table: + select_and_search: "Select filters and click on SEARCH to access your data." bulk_coop: bulk_coop_supplier_report: 'Bulk Co-op - Totals by Supplier' bulk_coop_allocation: 'Bulk Co-op - Allocation' @@ -2647,5 +2737,8 @@ See the %{link} to find out more about %{sitename}'s features and to start using total: Total paid?: Paid? view: View + saved_cards: + default?: Default? + delete?: Delete? localized_number: invalid_format: has an invalid format. Please enter a number. diff --git a/config/locales/en_GB.yml b/config/locales/en_GB.yml index 15bb74b37f..eec1f844f6 100644 --- a/config/locales/en_GB.yml +++ b/config/locales/en_GB.yml @@ -137,6 +137,9 @@ en_GB: form_invalid: "Form contains missing or invalid fields" clear_filters: Clear Filters clear: Clear + save: Save + cancel: Cancel + back: Back columns: Columns actions: Actions viewing: "Viewing: %{current_view_name}" @@ -264,11 +267,86 @@ en_GB: manages: manages products: unit_name_placeholder: 'eg. bunches' + index: + unit: Unit + display_as: Display As + category: Category + tax_category: Tax Category + inherits_properties?: Inherits Properties? + available_on: Available On + av_on: "Av. On" + import_date: Imported + Search: Search properties: property_name: Property Name inherited_property: Inherited Property variants: to_order_tip: "Items made to order do not have a set stock level, such as loaves of bread made fresh to order." + product_import: + title: Product Import + file_not_found: File not found or could not be opened + no_data: No data found in spreadsheet + confirm_reset: "This will set stock level to zero on all products for this \n enterprise that are not present in the uploaded file" + model: + no_file: "error: no file uploaded" + could_not_process: "could not process file: invalid filetype" + no_product: did not match any products in the database + not_found: not found in database + blank: can't be blank + products_no_permission: you do not have permission to manage products for this enterprise + inventory_no_permission: you do not have permission to create inventory for this producer + none_saved: did not save any products successfully + line: Line + index: + select_file: Select a spreadsheet to upload + spreadsheet: Spreadsheet + import_into: "Import into:" + product_list: Product list + inventories: Inventories + import: Import + import: + no_valid_entries: No valid entries found + none_to_save: There are no entries that can be saved + some_invalid_entries: Imported file contains some invalid entries + save_valid?: Save valid entries for now and discard the others? + no_errors: No errors detected! + save_all_imported?: Save all imported products? + options_and_defaults: Import options and defaults + no_permission: you do not have permission to manage this enterprise + not_found: enterprise could not be found in database + no_name: No name + blank_supplier: some products have blank supplier name + reset_absent?: Reset absent products? + overwrite_all: Overwrite all + overwrite_empty: Overwrite if empty + default_stock: Set default stock level + default_tax_cat: Set default tax category + default_shipping_cat: Set default shipping category + default_available_date: Set default available date + validation_overview: Import validation overview + entries_found: Entries found in imported file + entries_with_errors: Items contain errors and will not be imported + products_to_create: Products will be created + products_to_update: Products will be updated + inventory_to_create: Inventory items will be created + inventory_to_update: Inventory items will be updated + products_to_reset: Existing products will have their stock reset to zero + inventory_to_reset: Existing inventory items will have their stock reset to zero + line: Line + item_line: Item line + save: + final_results: Import final results + products_created: Products created + products_updated: Products updated + inventory_created: Inventory items created + inventory_updated: Inventory items updated + products_reset: Products had stock level reset to zero + inventory_reset: Inventory items had stock level reset to zero + all_saved: "All %{num} items saved successfully" + total_saved: "%{num} items saved successfully" + save_errors: Save errors + view_products: View Products + view_inventory: View Inventory variant_overrides: loading_flash: loading_inventory: LOADING INVENTORY @@ -279,6 +357,7 @@ en_GB: inherit?: Inherit? add: Add hide: Hide + import_date: Imported select_a_shop: Select A Shop review_now: Review Now new_products_alert_message: There are %{new_product_count} new products available to add to your inventory. @@ -923,7 +1002,6 @@ en_GB: email_admin_html: "You can manage your account by logging into the %{link} or by clicking on the cog in the top right hand side of the homepage, and selecting Administration." email_community_html: "We also have an online forum for community discussion related to OFN software and the unique challenges of running a food enterprise. You are encouraged to join in. We are constantly evolving and your input into this forum will shape what happens next. %{link}" join_community: "Join the community" - email_help: "If you have any difficulties, check out our FAQs, browse the forum or post a 'Support' topic and someone will help you out!" email_confirmation_greeting: "Hi, %{contact}!" email_confirmation_profile_created: "A profile for %{name} has been successfully created! To activate your Profile we need to confirm this email address." email_confirmation_click_link: "Please click the link below to confirm your email and to continue setting up your profile." @@ -1734,7 +1812,6 @@ en_GB: products_unsaved: "Changes to %{n} products remain unsaved." is_already_manager: "is already a manager!" no_change_to_save: "No change to save" - add_manager: "Add a manager" users: "Users" about: "About" images: "Images" @@ -2029,7 +2106,7 @@ en_GB: new: title: 'New Product' unit_name_placeholder: 'eg. bunches' - bulk_edit: + index: header: title: Bulk Edit Products indicators: diff --git a/config/locales/en_US.yml b/config/locales/en_US.yml index 65d0155eb2..0da03df1c6 100644 --- a/config/locales/en_US.yml +++ b/config/locales/en_US.yml @@ -364,7 +364,7 @@ en_US: manages: manages products: unit_name_placeholder: 'eg. bunches' - bulk_edit: + index: unit: Unit display_as: Display As category: Category @@ -849,10 +849,13 @@ en_US: allowed_payment_method_types_tip: Only Cash and Stripe payment methods may be used at the moment credit_card: Credit Card no_cards_available: No cards available + loading_flash: + loading: LOADING SUBSCRIPTIONS review: details: Details address: Address products: Products + product_already_in_order: This product has already been added to the order. Please edit the quantity directly. orders: number: Number confirm_edit: Are you sure you want to edit this order? Doing so may make it more difficult to automatically sync changes to the subscription in the future. @@ -2293,7 +2296,7 @@ en_US: new: title: 'New Product' unit_name_placeholder: 'eg. bunches' - bulk_edit: + index: header: title: Bulk Edit Products indicators: @@ -2369,6 +2372,7 @@ en_US: subscription_state: active: active pending: pending + ended: ended paused: paused canceled: cancelled payment_states: diff --git a/config/locales/es.yml b/config/locales/es.yml index 7d6e146b7a..e990f79e55 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -10,6 +10,8 @@ es: email: E-mail del consumidor spree/payment: amount: Cantidad + order_cycle: + orders_close_at: Fecha de cierre errors: models: spree/user: @@ -18,6 +20,10 @@ es: taken: "Ya existe una cuenta con este email. Inicie sesión o restablezca tu contraseña." spree/order: no_card: No hay tarjetas de crédito válidas disponibles + order_cycle: + attributes: + orders_close_at: + after_orders_open_at: debe ser después de la fecha de apertura activemodel: errors: models: @@ -358,14 +364,20 @@ es: manages: Gestionan products: unit_name_placeholder: 'ej. manojos' - bulk_edit: + index: unit: Unidad display_as: Mostrar como category: Categoría tax_category: Categoría de impuestos inherits_properties?: ¿Hereda propiedades? available_on: Disponible en + av_on: "Av. En" upload_an_image: Subir una imagen + product_search_keywords: Palabras clave de búsqueda de productos + product_search_tip: Escriba palabras para ayudar a buscar sus productos en las tiendas. Use espacio para separar cada palabra clave. + SEO_keywords: Palabras clave de SEO + seo_tip: Escriba palabras para ayudar a buscar sus productos en la web. Use espacio para separar cada palabra clave. + Search: Buscar properties: property_name: Nombre de la Propiedad inherited_property: Propiedad Heredada @@ -373,7 +385,6 @@ es: to_order_tip: "Los artículos hechos según demanda no tienen un nivel de stock, como por ejemplo panes hechos según demanda." product_distributions: "Distribuciones de productos" group_buy_options: "Opciones de compra grupales" - seo: "SEO" back_to_products_list: "Volver a la lista de productos" variant_overrides: loading_flash: @@ -723,6 +734,8 @@ es: destroy_errors: orders_present: Ese ciclo de pedido ha sido seleccionado por un cliente y no puede ser eliminado. Para evitar que los clientes accedan a él, ciérrelo. schedule_present: Ese ciclo de pedido está vinculado a un horario y no puede ser eliminado. Desvincula o elimina el calendario primero. + bulk_update: + no_data: Hm, algo salió mal. No se encontraron datos de ciclo de pedido. producer_properties: index: title: Propiedades de la Productora @@ -864,6 +877,9 @@ es: no_subscriptions: Aún no hay suscripciones ... why_dont_you_add_one: ¿Por qué no agregas una? :) no_matching_subscriptions: No se encontraron suscripciones coincidentes + schedules: + destroy: + associated_subscriptions_error: Este horario no se puede eliminar porque tiene suscripciones asociadas stripe_connect_settings: edit: title: "Stripe Connect" @@ -1131,7 +1147,6 @@ es: email_admin_html: "Puede administrar su cuenta iniciando sesión en %{link} o haciendo clic en el engrane arriba a la derecha de la página de inicio, y seleccionando Administración." email_community_html: "También tenemos un foro en líea para la discusión comunal relacionada con el programa OFN y los retos únicos del funcionamiento de una organización de alimentación. Lo invitamos a unirse. Estamos evolucionando de forma constante y su aporte en este formo le dará forma a lo que pase luego. %{link}" join_community: "Unirse a la comunidad" - email_help: "Si tiene dificultades, revise nuestras preguntas frecuentes, navegue el foro o haga una entrada de con tema de 'Soporte' y ¡alguien le ayudará!" email_confirmation_activate_account: "Antes de que podamos activar su nueva cuenta, necesitamos confirmar su dirección de correo electrónico." email_confirmation_greeting: "Hola, %{contact}!" email_confirmation_profile_created: "¡Se creó un un perfil para %{name} con éxito! Para activar su Perfil necesitamos que confirme esta dirección de correos." @@ -1193,9 +1208,9 @@ es: invite_email: greeting: "¡Hola!" invited_to_manage: "Ha sido invitado a administrar %{enterprise} en %{instance}." - confirm_your_email: "En breve recibirá un correo electrónico para confirmar su registro." + confirm_your_email: "Debería haber recibido o recibirá pronto un correo electrónico con un enlace de confirmación. No podrá acceder al perfil de %{enterprise} hasta que haya confirmado su correo electrónico." set_a_password: "Luego se le pedirá que establezca una contraseña antes de poder administrar la organización." - mistakenly_sent: "¿No está seguro de por qué ha recibido este correo electrónico? Por favor, póngase en contacto con %{owner_email} para obtener más información, o puede ponerse en contacto con %{instance} en %{instance_email}." + mistakenly_sent: "¿No está seguro de por qué ha recibido este correo electrónico? Por favor, póngase en contacto con %{owner_email} para más información." producer_mail_greeting: "Estimada" producer_mail_text_before: "Ahora tenemos todas los pedidos de las consumidoras para la siguiente ronda." producer_mail_order_text: "Se muestra un resumen de los pedidos de tus productos:" @@ -1444,6 +1459,7 @@ es: november: "Noviembre" december: "Diciembre" email_not_found: "Dirección de correo electrónico no encontrada" + email_unconfirmed: "Debe confirmar su dirección de correo electrónico antes de poder restablecer su contraseña." email_required: "Debe brindar una dirección de correo electrónico" logging_in: "Espere un momento, le vamos a iniciar una sesión" signup_email: "Tu correo electrónico" @@ -1695,6 +1711,10 @@ es: calculator: "Calculadora" calculator_values: "Calculadora de valores" flat_percent_per_item: "Porcentaje fijo (por artículo)" + flat_rate_per_item: "Tarifa plana (por artículo)" + flat_rate_per_order: "Tarifa plana (por pedido)" + flexible_rate: "Tarifa flexible" + price_sack: "Precio saco" new_order_cycles: "Nuevos Ciclos de Pedidos" new_order_cycle: "Nuevo Ciclo de Pedido" select_a_coordinator_for_your_order_cycle: "Selecciona un coordinador para vuestro ciclo de pedido" @@ -2035,7 +2055,12 @@ es: order_cycles_no_permission_to_create_error: "No tienes permiso para crear un ciclo de pedido coordinado por esta empresa." back_to_orders_list: "Volver a la lista de pedidos" order_information: "información del pedido" + date_completed: "Fecha de finalización" amount: "Cantidad" + state_names: + ready: Listo + pending: Pendiente + shipped: Enviado js: saving: 'Guardando...' changes_saved: 'Cambios guardados.' @@ -2057,6 +2082,9 @@ es: enterprise_limit_reached: "Has alcanzado el límite estándar de organizaciones por cuenta. Escriba a %{contact_email} si necesita aumentarlo." modals: got_it: Lo entiendo + close: "Cerrar" + invite: "Invitar" + invite_title: "Invitar a un usuario no registrado" tag_rule_help: title: Reglas de las Etiquetas overview: Visión general @@ -2235,6 +2263,8 @@ es: email: Email account_updated: "Cuenta actualizada!" my_account: "Mi cuenta" + date: "Fecha" + time: "Hora" admin: orders: invoice: @@ -2279,7 +2309,7 @@ es: new: title: 'Nuevo producto' unit_name_placeholder: 'ej. manojos' - bulk_edit: + index: header: title: Editar varios Productos indicators: diff --git a/config/locales/fr.yml b/config/locales/fr.yml index c054a485c1..3f9b4d7548 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -10,6 +10,8 @@ fr: email: Email acheteur spree/payment: amount: Montant + order_cycle: + orders_close_at: Date de fermeture errors: models: spree/user: @@ -18,6 +20,10 @@ fr: taken: "Un compte existe déjà pour cet e-mail. Connectez-vous ou demandez un nouveau mot de passe." spree/order: no_card: Aucune carte de crédit valide trouvée + order_cycle: + attributes: + orders_close_at: + after_orders_open_at: doit être postérieure à Date d'ouverture activemodel: errors: models: @@ -55,6 +61,10 @@ fr: Email / mot de passe incorrect. Créez votre compte ou réinitialisez votre mot de passe. unconfirmed: "Veuillez valider le lien envoyé par email pour pouvoir continuer." + already_registered: "Cet email est déjà associé à un utilisateur et a déjà été validé. Veuillez vous connecter pour continuer, ou utiliser un autre email." + user_passwords: + spree_user: + updated_not_active: "Votre mot de passe a bien été réinitialisé, mais votre email n'a pas encore été confirmé." enterprise_mailer: confirmation_instructions: subject: "Confirmez l'adresse email pour %{enterprise}" @@ -101,7 +111,7 @@ fr: title: Autre échec (%{count} commandes) explainer: Le traitement automatique de ces commandes a échoué pour une raison inconnue. Cela n'aurait pas dû arriver, veuillez nous contacter si vous constatez quelque chose d'anormal. home: "OFF" - title: Open Food Network France + title: Open Food France welcome_to: 'Bienvenue sur ' site_meta_description: "Tout commence dans le sol. Avec ces paysans, agriculteurs, producteurs, engagés pour une agriculture durable et régénératrice, et désireux de partager leur histoire et leur passion avec fierté. Avec ces distributeurs souhaitant reconnecter les individus à leurs aliments et aux gens qui les produisent, soutenir les prises de conscience, dans une démarche de transparence, d'honnêteté, en assurant une juste rémunération des producteurs. Avec ces acheteurs qui croient que de meilleures décisions d'achats peuvent ..." search_by_name: Recherche par nom ou département... @@ -227,8 +237,11 @@ fr: form_invalid: "Le formulaire contient des champs manquants ou invalides" clear_filters: Annuler les filtres clear: Annuler + save: Sauvergarder + cancel: Annuler + back: Retour show_more: Afficher plus - show_n_more: Montrer %{num} supplémentaires + show_n_more: Montrer + %{num} choose: "Choisir..." please_select: Veuillez choisir... columns: Colonnes @@ -358,7 +371,7 @@ fr: manages: gère products: unit_name_placeholder: 'ex: botte' - bulk_edit: + index: unit: Unité display_as: Unité affichéé category: Catégorie @@ -366,8 +379,12 @@ fr: inherits_properties?: Hériter des propriétés? available_on: Disponible via av_on: "Disp. via" + import_date: Importé upload_an_image: Importer une image product_search_keywords: Mots-clés de recherche produits + product_search_tip: Saisissez des mots qui peuvent simplifier la recherche de vo produits dans les boutiques. Laissez un espace entre chaque mot-clé. + SEO_keywords: Mot-clés de référencement web + seo_tip: Saisissez des mots qui peuvent simplifier la recherche de vos produits sur le web. Laissez un espace entre chaque mot-clé. Search: Rechercher properties: property_name: Nom du label @@ -377,6 +394,76 @@ fr: product_distributions: "Lieux de distribution" group_buy_options: "Options d'achat par lot" back_to_products_list: "Retour à la liste produits" + product_import: + title: Import liste produits + file_not_found: Fichier non trouvé ou impossible à ouvrir + no_data: Aucune donnée trouvée dans le tableau + confirm_reset: "Cette action remettra tous les niveaux de stock à zero pour cette\nentreprise pour les produits non présents dans ce fichier." + model: + no_file: "erreur : aucun document importé" + could_not_process: "impossible de traiter le fichier : type de fichier invalide" + incorrect_value: Valeur non conforme + conditional_blank: ne peut pas être vide si unit_type est vide + no_product: n'a pu être associé à aucun produit existant dans la base de données + not_found: non trouvé dans le base de données + blank: Champ obligatoire + products_no_permission: vous n'avez pas l'autorisation de gérer les produits de cette entreprise + inventory_no_permission: vous n'avez pas l'autorisation d'ajouter les produits de ce producteur à votre catalogue boutique + none_saved: n'a pu sauvegarder aucun produit :-( + line: Ligne + index: + select_file: Sélectionner le fichier (tableur sous format csv) à importer + spreadsheet: Tableur csv + import_into: "Importer dans:" + product_list: Catalogues produits des producteurs + inventories: Catalogues boutiques des hubs distributeurs + import: Importer + upload: Télécharger + import: + review: Vérifier + proceed: Continuer + save: Sauvergarder + results: Résultats + save_imported: Sauvegarder les produits importés + no_valid_entries: Aucune entrée valide trouvée + none_to_save: Il n'y a pas aucune information pouvant être sauvegardée + some_invalid_entries: 'Le fichier importé contient des données non-conformes ' + save_valid?: Sauvegarder les informations valides et détruire les autres? + no_errors: Aucune erreur détectée! + save_all_imported?: Sauvegarder tous les produits importés? + options_and_defaults: Options d'import + no_permission: vous n'avez pas l'autorisation de gérer cette entreprise + not_found: l'entreprise n'a pas été trouvée dans la base de données + no_name: Pas de nom + blank_supplier: certains produits ne sont associés à aucun fournisseur + reset_absent?: Mettre à zéro le produits absents du fichier? + overwrite_all: Modifier pour tous + overwrite_empty: Modifier si vide + default_stock: Indiquer niveau de stock + default_tax_cat: Indiquer taux de taxe + default_shipping_cat: Indiquer condition de transport + default_available_date: Indiquer date de disponibilité + validation_overview: Aperçu des entrées produits créées/modifiées + entries_found: Informations trouvées dans le fichier importé + entries_with_errors: Certaines lignes contiennent des erreurs et les produits correspondant ne seront pas importés + products_to_create: Ces produits vont être crées + products_to_update: Ces produits vont être mis à jour + inventory_to_create: Ces produits vont être ajoutés au catalogue boutique + inventory_to_update: Les informations de ces produits dans le catalogue boutique vont être mises à jour + products_to_reset: Le stock des produits existants va être remis à zero + inventory_to_reset: Dans le catalogue boutique, le stock des produits existants va être remis à zéro + line: Ligne + item_line: Ligne produit concernée + save: + final_results: Importer les informations produits confirmées + products_created: produits crées + products_updated: produits mis à jour + inventory_created: produits ajoutés dans le catalogue boutique + inventory_updated: produits mis à jour dans le catalogue boutique + products_reset: produits ont vu leur niveau de stock remis à zéro + inventory_reset: produits ont vu leur niveau de stock remis à zéro dans le catalogue boutique + all_saved: "Tous les produits ont été sauvegardés avec succès" + some_saved: "produits sauvegardés avec succès" variant_overrides: loading_flash: loading_inventory: Catalogue boutique en cours de chargement... @@ -387,6 +474,7 @@ fr: inherit?: Hériter? add: Ajouter hide: Masquer + import_date: Importé select_a_shop: Choisir une boutique review_now: Vérifier maintenant new_products_alert_message: Il y a %{new_product_count} nouveaux produits disponibles pouvant être ajoutés à votre catalogue. @@ -649,7 +737,7 @@ fr: welcome_title: Bienvenue sur Open Food France ! welcome_text: 'Vous avez créé avec succès ' next_step: Etape suivante - choose_starting_point: 'Choisissez par où commencer:' + choose_starting_point: 'Choisir votre type d''entreprise:' invite_manager: user_already_exists: "Le compte existe déjà" error: "Un problème est survenu" @@ -709,7 +797,7 @@ fr: name: Nom orders_open: Commandes à partir de coordinator: Coordinateur - order_closes: Commandes jusqu'au + orders_close: Commandes jusqu'au row: suppliers: fournisseurs distributors: hubs-distributeurs @@ -724,6 +812,8 @@ fr: destroy_errors: orders_present: Ce cycle de vente a déjà été utilisé par un acheteur et ne peut être supprimé. Pour empêcher aux acheteurs d'y accéder, veuillez plutôt le fermer. schedule_present: Ce cycle de vente est lié à un rythme d'abonnement et ne peut pas être supprimé. Veuillez d'abord supprimer ce lien ou supprimer le rythme d'abonnement. + bulk_update: + no_data: Une erreur s'est produite. Aucune donnée trouvée. producer_properties: index: title: Propriétés / labels du producteur @@ -816,12 +906,12 @@ fr: cancel_subscription: Annuler Abonnement setup_explanation: just_a_few_more_steps: 'Encore quelques étapes avant de pouvoir commencer:' - enable_subscriptions: "Activer la fonction abonnements pour au moins une de vos boutiques" + enable_subscriptions: "Activez la fonction abonnements pour au moins une de vos boutiques" enable_subscriptions_step_1_html: 1. Allez à %{enterprises_link}, trouvez votre boutique, et cliquez sur "Gérer" enable_subscriptions_step_2: 2. Sous "Préférences boutiques", activez la fonction Abonnements - set_up_shipping_and_payment_methods_html: Paramétrez des méthodes de %{shipping_link} et %{payment_link} + set_up_shipping_and_payment_methods_html: Paramétrez au moins une méthode d'%{shipping_link} et une méthode de %{payment_link} set_up_shipping_and_payment_methods_note_html: Notez bien que seules des méthodes de paiement de type "cash" ou "Stripe" pourront
être utilisées pour les Abonnements - ensure_at_least_one_customer_html: Assurez-vous qu'au moins une %{customer_link} existe + ensure_at_least_one_customer_html: Assurez-vous qu'au moins un %{customer_link} est enregistré dans votre liste d'acheteurs. create_at_least_one_schedule: Créez au moins un rythme d'abonnement create_at_least_one_schedule_step_1_html: 1. Allez à la page %{order_cycles_link} create_at_least_one_schedule_step_2: 2. Créez un cycle de vente si ce n'est pas encore fait @@ -865,6 +955,9 @@ fr: no_subscriptions: Pas encore d'abonnements... why_dont_you_add_one: Pourquoi ne pas en créer un? :) no_matching_subscriptions: Aucun abonnement correspondant trouvé + schedules: + destroy: + associated_subscriptions_error: Ce rythme d'abonnement ne peut pas être supprimé car il est associé à des abonnements. stripe_connect_settings: edit: title: "Stripe Connect" @@ -907,9 +1000,9 @@ fr: register: "Démarrez ici" shop: messages: - login: "se connecter" + login: "Se connecter" register: "s'inscrire" - contact: "contact" + contact: "contacter" require_customer_login: "La boutique est réservée aux membres." require_login_html: "Déjà inscrit? %{login}. Sinon, %{register} pour pouvoir faire vos achats." require_customer_html: "Veuillez %{contact} %{enterprise} pour devenir membre." @@ -1193,7 +1286,9 @@ fr: invite_email: greeting: "Bonjour!" invited_to_manage: "Vous avez été invité(e) à gérer %{enterprise} sur %{instance}." + confirm_your_email: "Vous avez reçu ou allez recevoir prochainement un email avec un lien de validation. Vous n'aurez pas accès au profil de l'entreprise %{enterprise} avant d'avoir cliqué sur ce lien." set_a_password: "Vous serez ensuite invité(e) à choisir un mot de passe avant de pouvoir accéder et gérer le profil de l'entreprise." + mistakenly_sent: "Vous ne savez pas pourquoi vous recevez cet email? Veuillez contacter %{owner_email} pour plus d'informations." producer_mail_greeting: "Cher(ère)" producer_mail_text_before: "Nous avons reçu toutes les commandes pour la prochaine livraison." producer_mail_order_text: "Voilà la liste et les quantités des produits commandés vous concernant:" @@ -1442,6 +1537,7 @@ fr: november: "Novembre" december: "Décembre" email_not_found: "Adresse email non trouvée" + email_unconfirmed: "Vous devez confirmer votre adresse email avant de pouvoir réinitiatliser votre mot de passe." email_required: "Vous devez saisir une adresse email" logging_in: "Veuillez patienter, connexion en cours" signup_email: "Votre email" @@ -1693,6 +1789,10 @@ fr: calculator: "Calculateur" calculator_values: "Valeurs applicables" flat_percent_per_item: "Pourcentage net" + flat_rate_per_item: "Montant fixe par article (hors articles au poids/volume)" + flat_rate_per_order: "Montant fixe par commande" + flexible_rate: "Montant variable selon nb articles" + price_sack: "Montant variable selon total commande" new_order_cycles: "Nouveau cycle de vente" new_order_cycle: "Nouveau Cycle de Vente" select_a_coordinator_for_your_order_cycle: "Choisissez un coordinateur pour votre cycle de vente" @@ -1727,12 +1827,7 @@ fr: spree_admin_enterprises_fees: "Marges et commissions" spree_admin_enterprises_none_create_a_new_enterprise: "CRÉER UNE NOUVELLE ENTREPRISE" spree_admin_enterprises_none_text: "Vous n'avez pas encore d'entreprise" - spree_admin_enterprises_producers_name: "Nom" - spree_admin_enterprises_producers_total_products: "Total produits " - spree_admin_enterprises_producers_active_products: "Produits actifs" - spree_admin_enterprises_producers_order_cycles: "Produits dans le cycle de vente" spree_admin_enterprises_tabs_hubs: "HUBS" - spree_admin_enterprises_tabs_producers: "PRODUCTEURS" spree_admin_enterprises_producers_manage_products: "GÉRER LES PRODUITS" spree_admin_enterprises_any_active_products_text: "Vous n'avez aucun produit actif." spree_admin_enterprises_create_new_product: "CRÉER UN NOUVEAU PRODUIT" @@ -2227,7 +2322,7 @@ fr: product_import: confirmation: | Cette action remettra tous les niveaux de stock à zero pour cette - entreprises pour les produits non présents dans ce fichier. + entreprise pour les produits non présents dans ce fichier. order_cycles: update_success: 'Votre cycle de vente a été mis à jour.' no_distributors: Il n'y a pas de distributeur pour ce cycle de vente. Il ne sera pas visible aux acheteurs tant qu'il n'y aura pas de distributeur. Voulez-vous tout de même sauvegarder ce cycle de vente ? @@ -2294,7 +2389,7 @@ fr: new: title: 'Nouveau Produit' unit_name_placeholder: 'ex: botte' - bulk_edit: + index: header: title: Gestion du catalogue produits indicators: diff --git a/config/locales/fr_CA.yml b/config/locales/fr_CA.yml new file mode 100644 index 0000000000..d1e68d1a75 --- /dev/null +++ b/config/locales/fr_CA.yml @@ -0,0 +1,2540 @@ +fr_CA: + language_name: "Français" + activerecord: + attributes: + spree/order: + payment_state: Statut du paiement + shipment_state: Statut de la livraison + completed_at: 'Passée à ' + number: N° commande + email: Email acheteur + spree/payment: + amount: Montant + order_cycle: + orders_close_at: Date de fermeture + errors: + models: + spree/user: + attributes: + email: + taken: "Un compte existe déjà pour cet e-mail. Connectez-vous ou demandez un nouveau mot de passe." + spree/order: + no_card: Aucune carte de crédit valide trouvée + order_cycle: + attributes: + orders_close_at: + after_orders_open_at: doit être après la date d'ouverture + activemodel: + errors: + models: + subscription_validator: + attributes: + subscription_line_items: + at_least_one_product: "Veuillez ajouter au moins un produit" + not_available: "^%{name} n'est pas disponible pour le rythme d'abonnement sélectionné" + ends_at: + after_begins_at: "doit être après le démarrage" + customer: + does_not_belong_to_shop: "n'appartient pas à %{shop}" + schedule: + not_coordinated_by_shop: "n'est pas coordonné par %{shop}" + payment_method: + not_available_to_shop: "n'est pas disponible pour %{shop}" + invalid_type: "doit être une méthode de paiement de type \"cash\" ou \"Stripe\"" + shipping_method: + not_available_to_shop: "n'est pas disponible pour %{shop}" + credit_card: + not_available: "n'est pas disponible" + blank: "est requis" + devise: + confirmations: + send_instructions: "Un email a été envoyé avec des instructions pour confirmer votre adresse email. Vérifiez votre boite mail!" + failed_to_send: "Une erreur est survenue lors de l'envoi de l'email de confirmation." + resend_confirmation_email: "Renvoyer l'email de confirmation." + confirmed: "Merci d'avoir confirmé votre adresse email. Vous pouvez maintenant vous connecter." + not_confirmed: "Votre adresse email n'a pas pu être confirmée. Peut-être avez-vous déjà confirmé cette adresse email?" + user_registrations: + spree_user: + signed_up_but_unconfirmed: "Un message avec un lien de confirmation a été envoyé à l'adresse email indiquée. Veuillez cliquer sur ce lien pour activer votre compte." + failure: + invalid: | + Email / mot de passe incorrect. + Étiez-vous invité la dernière fois? + Créez votre compte ou réinitialisez votre mot de passe. + unconfirmed: "Veuillez valider le lien envoyé par email pour pouvoir continuer." + already_registered: "Cet email existe déjà. Veuillez vous connecter ou utiliser une autre adresse email." + user_passwords: + spree_user: + updated_not_active: "Votre mot de passe a été mis à jour, mais votre email n'a pas encore été confirmé." + enterprise_mailer: + confirmation_instructions: + subject: "Confirmez l'adresse email pour %{enterprise}" + welcome: + subject: "%{enterprise} est maintenant sur %{sitename}" + invite_manager: + subject: "%{enterprise} vous a invité comme manager" + producer_mailer: + order_cycle: + subject: "Rapport de cycle de vente pour %{producer}" + subscription_mailer: + placement_summary_email: + subject: Un résumé des dernières commandes récemment passées + greeting: "Bonjour %{name}," + intro: "Voici le résumé des commandes qui viennent d'être passées pour la boutique %{shop}." + confirmation_summary_email: + subject: Un résumé des dernières commandes confirmées + greeting: "Bonjour %{name}," + intro: "Voici le résumé des commandes qui viennent d'être finalisées pour %{shop}." + summary_overview: + total: Un total de %{count} commandes ont été paramétrées pour traitement automatique. + success_zero: Sur celles-ci, aucune n'a été traitée avec succès. + success_some: Sur celles-ci, %{count} ont été traitées avec succès. + success_all: Toutes ont été traitées avec succès. + issues: Les détails sur les problèmes rencontrés sont affichés ci-dessous. + summary_detail: + no_message_provided: Aucun message d'erreur à afficher + changes: + title: Stock insuffisant (%{count} commandes) + explainer: Ces commandes ont été traitées mais pour certains produits, le stock était insuffisant + empty: + title: Pas de stock (%{count} commandes) + explainer: Ces commandes n'ont pas pu être traitées car les produits souhaités étaient en rupture de stok + complete: + title: Déjà traité (%{count} commandes) + explainer: Ces commandes étaient déjà marquées comme passées, et n'ont donc pas été modifiées + processing: + title: Erreur rencontrée (%{count} commandes) + explainer: Le traitement automatique de ces commandes a échoué. L'erreur a été affichée à l'endroit pertinent. + failed_payment: + title: Le paiement a échoué (%{count} commandes) + explainer: Le traitement automatique des paiements pour ces commandes a échoué. L'erreur a été affichée à l'endroit pertinent. + other: + title: Autre échec (%{count} commandes) + explainer: Le traitement automatique de ces commandes a échoué pour une raison inconnue. Cela n'aurait pas dû arriver, veuillez nous contacter si vous lisez ce message. + home: "OFN" + title: 'Open Food Network ' + welcome_to: 'Bienvenue sur ' + site_meta_description: "Tout commence dans le sol. Avec ces paysans, agriculteurs, producteurs, engagés pour une agriculture durable et régénératrice, et désireux de partager leur histoire et leur passion avec fierté. Avec ces distributeurs souhaitant reconnecter les individus à leurs aliments et aux gens qui les produisent, soutenir les prises de conscience, dans une démarche de transparence, d'honnêteté, en assurant une juste rémunération des producteurs. Avec ces acheteurs qui croient que de meilleures décisions d'achats peuvent ..." + search_by_name: Recherche par nom ou département... + producers_join: Les producteurs et autres hubs basés au Québec sont invités à rejoindre Open Food Network Canada. + charges_sales_tax: Soumis à la TVA? + print_invoice: "Imprimer la facture" + print_ticket: "Imprimer ticket de caisse" + select_ticket_printer: "Choisir l'imprimante tickets" + send_invoice: "Envoyer la facture" + resend_confirmation: "Renvoyer la confirmation" + view_order: "Voir la commande" + edit_order: "Editer la commande" + ship_order: "Envoyer la commande" + cancel_order: "Annuler la commande" + confirm_send_invoice: "La facture de cette commande va être transmise à l'acheteur. Etes-vous sûr de vouloir continuer ?" + confirm_resend_order_confirmation: "Etes-vous sûr de vouloir renvoyer le mail de confirmation de commande ?" + must_have_valid_business_number: "%{enterprise_name} doit avoir un SIRET valide avant que les factures puissent être envoyées." + invoice: "Facture" + percentage_of_sales: "%{percentage} des ventes" + capped_at_cap: "plafonné à %{cap}" + per_month: "par mois" + free: "gratuit" + free_trial: "essai gratuit" + plus_tax: "plus TVA" + min_bill_turnover_desc: "Quand le chiffre d'affaire dépasse %{mbt_amount}" + say_no: "Non" + say_yes: "Oui" + then: puis + ongoing: En cours + bill_address: Adresse de facturation + ship_address: Adresse de livraison + sort_order_cycles_on_shopfront_by: "Trier les cycles de vente par" + required_fields: Les champs obligatoires sont mentionnés par un asterisk + select_continue: Choisir et continuer + remove: Supprimer + or: ou + collapse_all: Tout masquer + expand_all: Tout afficher + loading: Chargement en cours... + show_more: Voir plus + show_all: Tout voir + show_all_with_more: "Voir tous (%{num} en plus)" + cancel: Annuler + edit: Modifier + clone: Dupliquer + distributors: Distributeurs + distribution: Distribution + bulk_order_management: Gestion des commandes par lot + enterprise_groups: Groupes + reports: Rapports + variant_overrides: Catalogue de produits + spree_products: Produits + all: Tous + current: Actuel + available: Disponible + dashboard: Tableau de bord + undefined: indéfini + unused: inutilisé + admin_and_handling: Admin et gestion + profile: Profil + supplier_only: Uniquement Fournisseur + weight: Poids + volume: Volume + items: Pièces + summary: Résumé + detailed: Détaillé + updated: Mis à jour + 'yes': "Oui" + 'no': "Non" + y: 'O' + n: 'N' + powered_by: Exécuté par + blocked_cookies_alert: "Votre navigateur semble bloquer des cookies nécessaires à l'utilisation de ce site. Cliquez ci-dessous pour autoriser les cookies et rechargez la page." + allow_cookies: "Autoriser les cookies" + notes: Commentaires + error: Erreur + processing_payment: Paiement en cours... + show_only_unfulfilled_orders: Ne montrer que les commandes non finalisées + filter_results: Filtrer les résultats + quantity: Quantité + pick_up: Retrait + copy: Copier + actions: + create_and_add_another: "Créer et ajouter nouveau" + admin: + begins_at: Commence à + begins_on: Commence le + customer: Acheteur + date: Date + email: Email + ends_at: Termine à + ends_on: Termine le + name: Nom + on_hand: En stock + on_demand: A volonté + on_demand?: A volonté? + order_cycle: Cycle de vente + payment: Paiement + payment_method: Méthode de paiement + phone: N° tel + price: Prix + producer: Producteur + image: Image + product: Produit + quantity: Quantité + schedule: Rythme d'abonnement + shipping: Expédition + shipping_method: Option d'expédition + shop: Boutique + sku: Référence produit + status_state: Département + tags: Tags + variant: Variante + weight: Poids + volume: Volume + items: Pièce + select_all: Tout sélectionner + obsolete_master: Master obsolète + quick_search: Recherche rapide + clear_all: Vider + start_date: "Date de début" + end_date: "Date de fin" + form_invalid: "Le formulaire contient des champs manquants ou invalides" + clear_filters: Annuler les filtres + clear: Effacer + save: Enregistrer + cancel: Annuler + back: Retour + show_more: Afficher plus + show_n_more: Montrer %{num} supplémentaires + choose: "Choisir..." + please_select: Veuillez choisir... + columns: Colonnes + actions: Actions + viewing: "Vous regardez: %{current_view_name}" + description: Description + whats_this: Qu'est-ce que c'est? + tag_has_rules: "Règles existantes pour ce tag: %{num}" + has_one_rule: "a une règle" + has_n_rules: "a %{num} règles" + unsaved_confirm_leave: "Des modifications n'ont pas été sauvegardées et seront perdues si vous quittez la page. Souhaitez-vous quitter la page?" + unsaved_changes: "Des modifications n'ont pas été sauvegardées." + accounts_and_billing_settings: + method_settings: + default_accounts_payment_method: "Méthode de paiement par défaut" + default_accounts_shipping_method: "Méthode d'envoi par défaut" + edit: + accounts_and_billing: "Comptes & Factures" + accounts_administration_distributor: "Entreprise d'administration des comptes (facturation des hubs)" + admin_settings: "Paramètres" + update_invoice: "Mettre à jour les factures" + auto_update_invoices: "Mettre à jour automatiquement les factures chaque nuit à 01:00" + finalise_invoice: "Finaliser les factures" + auto_finalise_invoices: "Finaliser automatiquement les factures le 2 de chaque mois à 01:30" + manually_run_task: "Tâche exécutée manuellement" + update_user_invoice_explained: "Cliquez ici pour mettre à jour immédiatement les factures pour le mois en cours pour toutes les entreprises utilisant le système. Cette tache peut être définie pour s'effectuer automatiquement chaque nuit." + finalise_user_invoices: "Finaliser les factures utilisateurs" + finalise_user_invoice_explained: "Cliquez ici pour finaliser toutes les factures pour le mois calendaire précédent. Cette tâche peut-être définie pour être opérée automatiquement une fois par mois." + update_user_invoices: "Mettre à jour les factures utilisateurs" + errors: + accounts_distributor: 'doit être défini si vous souhaitez générer des factures pour les utilisateurs entreprises. ' + default_payment_method: 'doit être défini si vous souhaitez générer des factures pour les utilisateurs entreprises. ' + default_shipping_method: doit être défini si vous souhaitez générer des factures pour les utilisateurs entreprises. + shopfront_settings: + embedded_shopfront_settings: "Paramètres Boutiques Intégrées" + enable_embedded_shopfronts: "Autoriser l'intégration des boutiques" + embedded_shopfronts_whitelist: "Liste blanche des Domaines Externes" + number_localization: + number_localization_settings: "Gestion localisation des nombres" + enable_localized_number: "Utiliser le traitement international des séparateurs de milliers/centimes" + business_model_configuration: + edit: + business_model_configuration: "Modèle économique" + business_model_configuration_tip: "Configurer la fréquence à laquelle les boutiques seront facturées chaque mois pour l'utilisation d'Open Food Network" + bill_calculation_settings: "Paramètres du calcul des frais" + bill_calculation_settings_tip: "Définir le montant qui sera facturé aux hubs tous les mois pour leur utilisation d'Open Food Network." + shop_trial_length: "Durée de la période de test (jours)" + shop_trial_length_tip: "La durée (en jours) de la période d'essai." + fixed_monthly_charge: "Charge mensuelle fixe" + fixed_monthly_charge_tip: "Le montant fixe mensuel facturé pour tous les hubs qui dépassent le seuil de chiffre d'affaire facturable (si défini)." + percentage_of_turnover: "Pourcentage du chiffre d'affaire" + percentage_of_turnover_tip: "Quand supérieur à zéro, ce taux (0.0 - 1.0) sera appliqué au chiffre d'affaire du hub pour déterminer la commission à facturer, qui sera ajoutée aux autres charges (à gauche) pour calculer le montant à facturer pour le mois." + monthly_cap_excl_tax: "plafond mensuel (sans TVA)" + monthly_cap_excl_tax_tip: "Quand supérieure à zéro, cette valeur sert de limite supérieure facturable pour un mois." + tax_rate: "TVA applicable" + tax_rate_tip: "TVA applicable sur le service facturé par Open Food Network." + minimum_monthly_billable_turnover: "Chiffre d'affaire minimum facturable (mensuel)" + minimum_monthly_billable_turnover_tip: "Chiffre d'affaire mensuel au delà duquel le hub devra payer le service Open Food Network. Les hubs n'atteignant pas ce chiffre d'affaire mensuel ne seront pas facturés, ni sur le montant fixe ni sur la commission variable." + example_bill_calculator: "Exemple de calcul de facture" + example_bill_calculator_legend: "Changer le chiffre d'affaire pour voir l'impact des paramètres définis à gauche." + example_monthly_turnover: "Exemple de CA mensuel" + example_monthly_turnover_tip: "Exemple de chiffre d'affaire mensuel qui sert de base de calcul pour voir quel est le montant qui sera facturé au hub concerné." + cap_reached?: "Seuil atteint?" + cap_reached?_tip: "On voit ici si le seuil (défini à gauche) a été atteint, en fonction du chiffre d'affaire et du paramétrage du seuil." + included_tax: "Inclut TVA" + included_tax_tip: "TVA inclue dans l'exemple en cours, dépend du chiffre d'affaire et des paramétrages à gauche." + total_monthly_bill_incl_tax: "Facture mensuelle totale (taxes incluses)" + total_monthly_bill_incl_tax_tip: "Exemple du total TTC facturé pour le mois, selon paramétrages et chiffre d'affaire du mois." + customers: + index: + add_customer: "Ajouter un acheteur" + new_customer: "Nouveau client" + customer_placeholder: "acheteur@exemple.org" + valid_email_error: Veuillez entrer un email valide + add_a_new_customer_for: Ajouter un nouvel acheteur pour %{shop_name} + code: Code + duplicate_code: "Ce code est déjà utilisé." + bill_address: "Adresse de facturation" + ship_address: "Adresse de livraison" + update_address_success: 'Adresse mise à jour avec succès.' + update_address_error: 'Oups! Veuillez remplir tous les champs obligatoires!' + edit_bill_address: 'Modifier l''adresse de facturation' + edit_ship_address: 'Modifier l''adresse de livraison' + required_fileds: 'Les champs obligatoires sont indiqués avec un astérisque *' + select_country: 'Choisir le pays' + select_state: 'Choisir le département' + edit: 'Modifier' + update_address: 'Mettre à jour l''adresse' + confirm_delete: 'Confirmer suppression?' + search_by_email: "Recherche par email/code..." + destroy: + has_associated_orders: 'Suppression impossible: des commandes sont associées à cette boutique' + cache_settings: + show: + title: Mise en cache + distributor: Hub-distributeur + order_cycle: Cycle de vente + status: Statut + diff: Diff + error: Erreur + contents: + edit: + title: Contenu + header: Titre + home_page: Page d'accueil + producer_signup_page: Page d'inscription Producteur + hub_signup_page: Page d'inscription Hub + group_signup_page: Page d'inscription Groupe + footer_and_external_links: Pied de page et Liens Externes + your_content: Votre contenu + enterprise_fees: + index: + title: Marges et Commissions + enterprise: Entreprise + fee_type: Type de commissions + name: Nom + tax_category: TVA applicable + calculator: Calculateur + calculator_values: Valeurs applicables + enterprise_groups: + index: + new_button: Nouveau groupe d'entreprises + enterprise_roles: + form: + manages: gère + enterprise_role: + manages: gère + products: + unit_name_placeholder: 'ex: bottes' + bulk_edit: + unit: Unité + display_as: Unité affichée + category: Catégorie + tax_category: TVA applicable + inherits_properties?: Hériter des propriétés? + available_on: Disponible via + av_on: "Disp. via" + import_date: importé + upload_an_image: Importer une image + product_search_keywords: Mots-clés de recherche produits + product_search_tip: Saisissez des mots qui peuvent simplifier la recherche de vos produits dans les boutiques. Laissez un espace entre chaque mot-clé. + SEO_keywords: Mot-clés de référencement web + seo_tip: Saisissez des mots qui peuvent simplifier la recherche de vos produits sur le web. Laissez un espace entre chaque mot-clé. + Search: Rechercher + properties: + property_name: Nom du label + inherited_property: Label producteur appliqué par défaut + variants: + to_order_tip: "Les articles fabriqués sur commande n'ont pas un niveau de stock défini, comme des pains faits à la main." + product_distributions: "Lieux de distribution" + group_buy_options: "Options d'achat par lot" + back_to_products_list: "Retour à la liste produits" + product_import: + title: import produit + file_not_found: Fichier non trouvé ou impossible à ouvrir + no_data: Aucune donnée trouvée dans le tableau + confirm_reset: "Cela va définir le stock à zero sur tous les produits des entreprises non présentes dans le fichier téléchargé." + model: + no_file: "erreur : aucun document importé" + could_not_process: "impossible de traiter le fichier : type de fichier invalide" + incorrect_value: valeur incorrecte + conditional_blank: Champ obligatoire si le type d'unité est vide + no_product: 'aucun produit trouvé ' + not_found: n'a pas été trouvé dans la base de donnée + blank: Champ obligatoire + products_no_permission: vous n'avez pas les droits requis pour gérer les produits de cette entreprise + inventory_no_permission: Vous n'avez pas la permission de créer un catalogue de produits pour ce producteur + none_saved: n'a pu sauvegarder aucun produit :-( + line: Ligne + index: + select_file: Sélectionner une feuille de calcul à uploader + spreadsheet: Feuille de calcul + import_into: "Importer" + product_list: Liste produit + inventories: Catalogues produits + import: Importer + upload: Télécharger + import: + review: Vérifier + proceed: Procéder + save: Enregistrer + results: Résultats + save_imported: Sauvegarder les produits importés + no_valid_entries: Pas d'entrées valides trouvées + none_to_save: Aucune donnée n'a pu être sauvegardée + some_invalid_entries: Le fichier importé contient des entrées invalides + save_valid?: Sauvegarder les entrées valides et supprimer les autres? + no_errors: Aucune erreur détectée ! + save_all_imported?: Sauvegarder les produits importés? + options_and_defaults: Options d'import + no_permission: vous n'avez pas les droits requis pour gérer les produits de cette entreprise + not_found: entreprise non trouvée dans la base de donnée + no_name: Pas de nom + blank_supplier: certains produits ne sont associés à aucun fournisseur + reset_absent?: Mettre à zéro le produits absents ? + overwrite_all: Modifier tous + overwrite_empty: Modifier si vide + default_stock: Indiquer niveau de stock + default_tax_cat: Indiquer le type de taxe + default_shipping_cat: Indiquer condition de transport + default_available_date: Indiquer date de disponibilité + validation_overview: 'Aperçu des entrées produits ' + entries_found: Informations trouvées dans le fichier importé + entries_with_errors: Certaines lignes contiennent des erreurs et les produits correspondant ne seront pas importés + products_to_create: Ces produits vont être crées + products_to_update: Ces produits vont être mis à jour + inventory_to_create: Les produits du catalogue seront crées + inventory_to_update: Les produits du catalogue seront mis à jour + products_to_reset: Le stock des produits existants va être remis à zero + inventory_to_reset: Les produits du catalogue auront leurs stocks remis à zéro + line: Ligne + item_line: Ligne produit concernée + save: + inventory_created: Les produits du catalogue ont été crées + inventory_updated: Les produits du catalogue ont été mis à jour + inventory_reset: Les stocks des produits du catalogue ont été remis à zéro + view_inventory: voir catalogue + variant_overrides: + loading_flash: + loading_inventory: Catalogue de produits en cours de chargement... + index: + title: Catalogue de produits + description: Utilisez cette page pour gérer le catalogue de votre entreprise. Les détails produits saisis ici remplaceront ceux de la page "Produit" pour votre entreprise uniquement. + enable_reset?: Autoriser réinitialisation du stock (retour configurations par défaut)? + inherit?: Hériter? + add: Ajouter + hide: Masquer + import_date: importé + select_a_shop: Choisir une boutique + review_now: Vérifier maintenant + new_products_alert_message: Il y a %{new_product_count} nouveaux produits disponibles pouvant être ajoutés à votre catalogue. + currently_empty: Votre catalogue est actuellement vide + no_matching_products: Pas de produits correspondants dans votre catalogue + no_hidden_products: Aucun produit masqué dans ce catalogue + no_matching_hidden_products: Aucune produit masqué ne répond à la recherche + no_new_products: Pas de nouveaux produits à ajouter à ce catalogue + no_matching_new_products: Pas de nouveaux produits répondant à la recherche + inventory_powertip: Ceci est votre catalogue produits. Pour ajouter des produits à votre catalogue, sélectionnez "Nouveaux Produits" dans le menu déroulant. + hidden_powertip: Ces produits ont été masqués de votre catalogue, vous ne pourrez pas les proposer dans votre boutique. Vous pouvez cliquer sur "Ajouter" pour ajouter un produit à votre catalogue. + new_powertip: Ces produits peuvent être ajoutés à votre catalogue. Cliquez sur "Ajouter" pour ajouter un produit à votre catalogue, ou 'Masquer" pour ne plus l'afficher. Vous pourrez changer d'avis plus tard! + controls: + back_to_my_inventory: Retour à mon catalogue de produits + orders: + index: + capture: "Payée" + ship: "Expédier" + invoice_email_sent: 'L''email de facturation a bien été envoyé' + order_email_resent: 'L''email de commande a de nouveau été envoyé' + bulk_management: + tip: "Utilisez cette page pour changer les quantités d'un produit sur plusieurs commandes. Les produits peuvent aussi être supprimés de toutes les commandes, si nécessaire." + shared: "Ressource partagée?" + order_no: "N° commande" + order_date: "Date commande" + max: "Max" + product_unit: "Produit: Unité" + weight_volume: "Poids/Volume" + ask: "Demander?" + page_title: "Gestion des commandes" + actions_delete: "Supprimer la sélection" + loading: "Commandes en cours de chargement" + no_results: "Aucune commande trouvée." + group_buy_unit_size: "Quantité totale du lot" + total_qtt_ordered: "Quantité totale commandée" + max_qtt_ordered: "Quantité max commandée" + current_fulfilled_units: "Nombre d'unités commandées" + max_fulfilled_units: "Nombre max d'unités commandées" + order_error: "Des erreurs doivent être résolues avant de pouvoir mettre à jour les commandes.\nLes champs entourés en rouge contiennent des erreurs." + variants_without_unit_value: "ATTENTION: certaines variantes n'ont pas de nombre d'unités" + select_variant: "Choisir une variante" + enterprise: + select_outgoing_oc_products_from: Sélectionner les produits sortants pour le cycle de vente parmi + enterprises: + index: + title: Entreprises + new_enterprise: Nouvelle entreprise + producer?: "Producteur?" + package: Pack + status: Statut + manage: Gérer + form: + about_us: + desc_short: Description courte + desc_short_placeholder: Parlez de votre entreprise en une ou deux phrases + desc_long: A propos + desc_long_placeholder: Parlez de vous à vos acheteurs ! Ces informations seront visibles sur votre profil public. + business_details: + abn: SIRET + abn_placeholder: 'ex: 404 833 048 00022' + acn: n° TVA intracommunautaire + acn_placeholder: 'ex: 404 833 048' + display_invoice_logo: Afficher le logo sur la facture + invoice_text: Ajouter une mention spécifique en bas des factures + contact: + name: Nom + name_placeholder: 'ex: Bernard Michelet' + email_address: Adresse email publique + email_address_placeholder: 'ex: labelleferme@maferme.fr' + email_address_tip: "Cette adresse email sera affichée sur votre profil public" + phone: n° téléphone + phone_placeholder: 'ex: 06 13 24 35 46' + website: Site internet + website_placeholder: 'ex: www.maferme.fr' + enterprise_fees: + name: Nom + fee_type: Type de commissions + manage_fees: Gérer les marges et commissions + no_fees_yet: Vous n'avez pas encore défini de commissions + create_button: Créer une commission + images: + logo: Logo + promo_image_placeholder: 'Cette image est affichée dans "A propos"' + promo_image_note1: 'ATTENTION:' + promo_image_note2: Votre bannière doit mesurer 1200 x 260, toute image non conforme sera rognée. + promo_image_note3: La bannière est affichée en haut de la page de votre entreprise et dans sa version condensée (pop-up). + inventory_settings: + text1: Vous pouvez choisir de gérer vos stocks et prix via votre + inventory: catalogue de produits + text2: > + Si vous utilisez l'outil "catalogue de produits", vous pouvez choisir + si les nouveaux produits ajoutés par vos fournisseurs doivent être référencés + dans votre catalogue de produits avant qu'ils puissent mis en vente + dans votre boutique. Si vous n'utilisez pas cet outil, choisissez l'option + indiquant "recommandé" ci-dessous: + preferred_product_selection_from_inventory_only_yes: Les nouveaux produits des producteurs peuvent être ajoutés à ma boutique en ligne (recommandé) + preferred_product_selection_from_inventory_only_no: Les nouveaux produits des producteurs doivent être ajoutés à mon catalogue de produits avant de pouvoir être ajoutés à ma boutique en ligne + payment_methods: + name: Nom + applies: Active? + manage: Gérer les méthodes de paiement + not_method_yet: Vous n'avez pas encore de méthode de paiement. + create_button: Créer une nouvelle méthode de paiement + create_one_button: En créer une maintenant + primary_details: + name: Nom + name_placeholder: 'ex: La ferme bio de Bernard' + groups: Groupes + groups_tip: Sélectionnez les groupes desquels vous êtes membres. Cela améliorera votre visibilité et permettra aux acheteurs de vous trouver plus facilement. + groups_placeholder: Commencer à taper pour voir les groupes disponibles... + primary_producer: Producteur? + primary_producer_tip: Cochez "producteur" si vous vendez des aliments que vous produisez vous-même (bruts ou transformés) + producer: Producteur + any: Tous + none: Aucun + own: Les siens + sells: Produits vendus + sells_tip: "Aucun - l'entreprise ne vend pas en direct aux acheteurs.
Les miens - l'entreprise vend ses propres produits aux acheteurs.
Tous - l'entreprise vend ses propres produits et/ou les produits d'autres entreprises.
" + visible_in_search: Apparaît dans la recherche? + visible_in_search_tip: Indiquez si vous souhaitez ou ne souhaitez pas que votre entreprise apparaisse sur la carte et dans la liste des boutiques. + visible: Visible + not_visible: Invisible + permalink: Nom pour URL (sans espace) + permalink_tip: "Ce nom permanent est utilisé pour créer l'url de votre boutique: %{link}ma-boutique/shop" + link_to_front: Lien URL de la boutique + link_to_front_tip: C'est le lien qui permet d'accéder en direct à votre boutique sur Open Food Network. + shipping_methods: + name: Nom + applies: Active? + manage: Gérer les méthodes de livraison + create_button: Créer nouvelle méthode de livraison + create_one_button: En créer une maintenant + no_method_yet: Vous n'avez pas encore paramétré de méthode de livraison. + shop_preferences: + shopfront_requires_login: "Boutique visible par tous?" + shopfront_requires_login_tip: "Choisissez si les acheteurs doivent être logués pour voir la boutique ou si la boutique est visible par tout le monde." + shopfront_requires_login_false: "Visible par tous" + shopfront_requires_login_true: "Visible uniquement pour les acheteurs logués" + recommend_require_login: "Nous recommandons de demander aux utilisateurs de se connecter si vous souhaitez leur permettre de modifier leur commande." + allow_guest_orders: "Commandes des invités" + allow_guest_orders_tip: "Autoriser la commande en tant qu'invité ou demander que l'acheteur soit logué." + allow_guest_orders_false: "Demander que l'acheteur se logue pour pouvoir commander" + allow_guest_orders_true: "Autoriser les commandes en mode invité" + allow_order_changes: "Modifier la commande" + allow_order_changes_tip: "Permettre aux acheteurs de modifier leur commande tant que le cycle de vente est ouvert." + allow_order_changes_false: "Les commandes validées ne peuvent plus être modifiées / annulées" + allow_order_changes_true: "Les acheteurs peuvent modifier / valider leurs commandes tant que le cycle de vente est ouvert" + enable_subscriptions: "Abonnements" + enable_subscriptions_tip: "Activer la fonction abonnements?" + enable_subscriptions_false: "Désactivée" + enable_subscriptions_true: "Activée" + shopfront_message: Message d'accueil de la boutique ouverte + shopfront_message_placeholder: > + Vous pouvez ici expliquer à vos acheteurs comment votre boutique fonctionne. + Ce texte s'affiche dans votre boutique, au-dessus de la liste de produits. + shopfront_closed_message: Message d'accueil de la boutique fermée + shopfront_closed_message_placeholder: > + Vous pouvez ici expliquer à vos acheteurs pourquoi votre boutique est + fermée et/ou quand elle ouvrira. Ce texte s'affiche uniquement quand + il n'y a pas de cycle de vente en cours (donc quand votre boutique est + fermée). + shopfront_category_ordering: Ordre d'affichage des catégories + open_date: Date d'ouverture + close_date: Date de fermeture + social: + twitter_placeholder: ex. @OpenFoodNet_fr + stripe_connect: + connect_with_stripe: "Connecter avec Stripe" + stripe_connect_intro: "Pour accepter des paiements utilisant la carte bancaire, vous devez connecter votre compte Stripe à Open Food Network. Cliquez sur le bouton à droite pour commencer." + stripe_account_connected: "Compte Stripe connecté." + disconnect: "Déconnecter le compte" + confirm_modal: + title: Connecter avec Stripe + part1: Stripe est un système de paiement qui permet aux boutiques sur Open Food Network d'accepter des paiements par carte bancaire de leurs acheteurs. + part2: Pour utiliser cette fonctionnalité, vous devez connecter votre compte Stripe à Open Food Network. En cliquant sur "J'accepte" ci-dessous, vous serez redirigé vers le site internet de Stripe, où vous pourrez connecter votre compte existant ou en créer un si vous n'en avez pas encore. + part3: Cela permettra à Open Food Network d'accepter en votre nom les paiements par carte de crédit en provenance de vos acheteurs. Veuillez noter que c'est à vous de gérer votre compte Stripe, de payer les frais dus à Stripe et de gérer les éventuels remboursements et le service après vente. + i_agree: J'accepte + cancel: Annuler + tag_rules: + default_rules: + by_default: Règles à appliquer "par défaut" + no_rules_yet: Aucune règle par défaut + add_new_button: '+ Ajouter une règle par défaut' + no_tags_yet: Aucun tag défini par cette entreprise pour le moment + no_rules_yet: Aucune règle ne concerne ce tag pour le moment + for_customers_tagged: 'Pour les acheteurs avec le tag:' + add_new_rule: '+ Ajouter une nouvelle règle' + add_new_tag: '+ Ajouter un nouveau tag' + users: + email_confirmation_notice_html: "L'email de confirmation n'a pas encore été validé. Il a été envoyé à %{email}." + resend: Renvoyer + owner: 'Manager principal' + contact: "Contact" + contact_tip: "Le manager qui recevra les emails de confirmation de commande et autres notifications de l'entreprise. Il doit avoir confirmé son adresse email pour pouvoir être sélectionné." + owner_tip: Manager principal de cette entreprise. + notifications: Notifications + notifications_tip: Une notification de commande sera envoyée à cette adresse email pour chaque commande passée dans votre boutique. + notifications_placeholder: 'ex: bernard@maferme.fr' + notifications_note: 'A noter: si vous saisissez une nouvelle adresse, un email de confirmation sera envoyé à cette adresse avec un lien de validation à cliquer.' + managers: Managers + managers_tip: 'Sélectionner ici les utilisateurs ayant la permission de gérer cette entreprise. ' + invite_manager: "Inviter un manager" + invite_manager_tip: "Inviter un nouvel utilisateur à créer son compte et le nommer comme manager de cette entreprise." + add_unregistered_user: "Ajouter un nouvel utilisateur" + email_confirmed: "Email confirmé" + email_not_confirmed: "Email non confirmé" + actions: + edit_profile: Modifier le profil + properties: Labels / propriétés + payment_methods: Méthodes de paiement + payment_methods_tip: Cette entreprise n'a pas paramétré de méthode de paiement + shipping_methods: Méthodes de livraison + shipping_methods_tip: Cette entreprise a paramétré des méthodes de paiement + enterprise_fees: Marges et commissions + enterprise_fees_tip: Cette entreprise n'a pas paramétré de marges et commissions + admin_index: + name: Nom + role: Role + sells: Produits vendus + visible: Visible? + owner: Gérant + producer: Producteur + change_type_form: + producer_profile: Profil producteur + connect_ofn: Gagnez en visibilité via OFN + always_free: GRATUIT + producer_description_text: Saisissez votre catalogue produits sur Open Food Network, ce qui permettra aux hubs-distributeurs utilisant la plateforme de les proposer dans leurs boutiques (sur votre autorisation). + producer_shop: Boutique Producteur + sell_your_produce: Vendez vos propres produits + producer_shop_description_text: Vendez vos produits en direct aux mangeurs/restaurateurs/etc. via votre propre Boutique Producteur sur Open Food Network. + producer_shop_description_text2: Une Boutique Producteur vous permet de vendre uniquement vos propres produits. Si vous voulez vendre d'autres produits, sélectionnez "Hub Producteur" + producer_hub: Hub Producteur + producer_hub_text: Vendez vos produits et ceux d'autres fournisseurs + producer_hub_description_text: Vous pouvez vendre non seulement vos produits, mais aussi des produits d'autres producteurs de votre région, artisans, ou distributeurs afin de proposer une offre complète dans votre boutique. Vous soutenez ainsi le développement de votre système alimentaire territorial ! + profile: Profil uniquement + get_listing: Référencez votre hub/point de vente + profile_description_text: Les visiteurs peuvent vous trouver sur Open Food Network et vous contacter. Votre entreprise sera visible sur la carte. + hub_shop: Boutique Hub + hub_shop_text: Vendez des produits de multiples fournisseurs + hub_shop_description_text: Vous proposez des produits de différents producteurs de votre région, artisans, ou distributeurs afin de proposer une offre complète dans votre boutique. Vous soutenez ainsi le développement de votre système alimentaire territorial ! + choose_option: Veuilliez choisir l'une des options ci-dessus. + change_now: Changer + enterprise_user_index: + loading_enterprises: CHARGEMENT DES ENTREPRISES + no_enterprises_found: Aucune entreprise trouvée. + search_placeholder: Recherche par nom + manage: Gérer + new_form: + owner: Gérant + owner_tip: L'utilisateur principal est l'individu qui porte la responsabilité principale de l'entreprise dans le contexte de l'utilisation d'Open Food Network. + i_am_producer: Je suis un producteur + contact_name: Nom du contact principal + edit: + editing: 'En modification:' + back_link: Revenir à la liste des entreprises + new: + title: Nouvelle entreprise + back_link: Revenir à la liste des entreprises + welcome: + welcome_title: Bienvenue sur Open Food Network ! + welcome_text: 'Vous avez créé avec succès ' + next_step: Etape suivante + choose_starting_point: 'Choisir le type de compte souhaité' + invite_manager: + user_already_exists: "Le compte existe déjà" + error: "Un problème est survenu" + order_cycles: + edit: + advanced_settings: Paramétrages avancés + update_and_close: Mettre à jour et fermer + choose_products_from: 'Choisir produits depuis :' + exchange_form: + pickup_time_tip: Quand des commandes liées à ce cycle de vente seront prêtes à être soumises à l'acheteur + pickup_instructions_placeholder: "Modalités de retrait/livraison" + pickup_instructions_tip: Ces instructions sont affichées aux acheteurs après passage d'une commande + pickup_time_placeholder: "Prêt pour (ex : jour + créneau horaire)" + receival_instructions_placeholder: "Modalités de livraison" + add_fee: 'Ajouter une commission' + selected: 'sélectionné' + add_exchange_form: + add_supplier: 'Ajouter un fournisseur' + add_distributor: 'Ajouter un distributeur' + advanced_settings: + title: Paramétrages avancés + choose_product_tip: Vous pouvez choisir de limiter le choix des produits pouvant être mis en vente dans votre boutique à ceux figurant dans le catalogue de produits de %{inventory}. + preferred_product_selection_from_coordinator_inventory_only_here: Uniquement les produits du catalogue du coordinateur + preferred_product_selection_from_coordinator_inventory_only_all: Tous les produits disponibles dans les catalogues producteurs + save_reload: Sauvegarder et rafraichir la page + coordinator_fees: + add: Ajouter commission coordinateur + form: + incoming: Produits entrants (pouvant être mis en vente par les hubs) + supplier: Fournisseur + receival_details: Détails livraison produits + fees: Commissions + outgoing: Produits sortants (mis en vente par/via un ou plusieurs hubs) + distributor: Hub (distributeur) + products: Produits + tags: Tags + add_a_tag: Ajouter un tag + delivery_details: Précisions retrait / livraison + debug_info: Informations de débogage + index: + involving: Concernant + schedule: Rythme d'abonnement + schedules: Rythmes d'abonnement + adding_a_new_schedule: Ajouter un nouveau rythme d'abonnement + updating_a_schedule: Mettre à jour un rythme d'abonnement + new_schedule: Nouveau rythme d'abonnement + create_schedule: Créer rythme d'abonnement + update_schedule: Mettre à jour rythme d'abonnement + delete_schedule: Supprimer rythme d'abonnement + created_schedule: Créer rythme d'abonnement + updated_schedule: Mettre à jour rythme d'abonnement + deleted_schedule: Supprimer rythme d'abonnement + schedule_name_placeholder: Nom du rythme d'abonnement + name_required_error: Veuillez saisir un nom pour ce rythme d'abonnement + no_order_cycles_error: Veuillez saisir au moins un cycle de vente (glisser déposer) + name_and_timing_form: + name: Nom + orders_open: Commandes à partir de + coordinator: Coordinateur + orders_close: Commandes jusqu'au + row: + suppliers: fournisseurs + distributors: Distributeurs + variants: variantes + simple_form: + ready_for: 'Prêt pour ' + ready_for_placeholder: Date / Heure + customer_instructions: Précisions pour l'acheteur + customer_instructions_placeholder: Commentaires pour le retrait / la livraison + products: Produits + fees: Commissions + destroy_errors: + orders_present: Ce cycle de vente a déjà été utilisé par un acheteur et ne peut être supprimé. Pour empêcher aux acheteurs d'y accéder, veuillez plutôt le fermer. + schedule_present: Ce cycle de vente est lié à un rythme d'abonnement et ne peut pas être supprimé. Veuillez d'abord supprimer ce lien ou supprimer le rythme d'abonnement. + producer_properties: + index: + title: Propriétés / labels du producteur + proxy_orders: + cancel: + could_not_cancel_the_order: La commande n'a pas pu être supprimée + resume: + could_not_resume_the_order: La commande n'a pas pu être reprise + shared: + user_guide_link: + user_guide: Guide utilisateur + invoice_settings: + edit: + title: Paramètres de facturation + invoice_style2?: Utiliser le modèle de facture alternatif qui détaille le montant de TVA agrégé par taux et l'information du taux de TVA par produit (pas adapté pour les instances affichant les prix HT) + enable_receipt_printing?: Afficher les options d'impression de tickets de caisse dans le menu déroulant des commandes? + overview: + enterprises_header: + ofn_with_tip: Les Entreprises sont des Producteurs et/ou Hubs distributeurs, et sont donc les organisations de base qui utilisent Open Food Network. + enterprises_hubs_tabs: + has_no_payment_methods: "%{enterprise} n'a pas défini de méthode de paiement" + has_no_shipping_methods: "%{enterprise} n'a pas défini de méthode de livraison" + has_no_enterprise_fees: "%{enterprise} n'a pas défini de marges et commissions" + enterprise_issues: + create_new: Créer Nouveau + resend_email: Renvoyer l'email + has_no_payment_methods: "%{enterprise} n'a pas de méthode de paiement active" + has_no_shipping_methods: "%{enterprise} n'a pas de méthode de livraison active" + email_confirmation: "L'adresse e-mail doit être confirmée. Nous avons envoyé un lien de confirmation à %{email}." + not_visible: "%{enterprise}n'est pas visible et ne peut être trouvé sur la carte ou dans les recherches sur le site." + reports: + hidden: Masqué + unitsize: Unité de mesure + total: Total + total_items: Nb Articles + supplier_totals: Totaux Cycle de Vente par Producteur + supplier_totals_by_distributor: Totaux Cycle de Vente par Producteur pour chaque Hub Distributeur + totals_by_supplier: Totaux Cycle de Vente par Hub Distributeur pour chaque Producteur + customer_totals: Totaux Cycle de Vente par Acheteur + all_products: Tous les produits + inventory: Catalogue produits (en stock) + lettuce_share: LettuceShare + mailing_list: Liste de mails + addresses: Adresses + payment_methods: Rapport Méthodes de Paiement + delivery: Rapport de Livraison + tax_types: Par type de taxe + tax_rates: Par taux de taxe + pack_by_customer: Préparation des commandes par Acheteur + pack_by_supplier: Préparation des commandes par Producteur + orders_and_distributors: + name: Commandes et Hubs Distributeurs + description: Liste des Commandes avec les détails des Hubs Ditributeurs + bulk_coop: + name: Achat groupés en vrac + description: Rapports achats groupés vrac + payments: + name: Rapports des paiements + description: Rapports des paiements reçus + orders_and_fulfillment: + name: Rapports des commandes + customers: + name: Acheteurs + products_and_inventory: + name: Produits et Catalogues + sales_total: + name: Total des Ventes + description: Total des Ventes pour toutes les Commandes + users_and_enterprises: + name: Utilisateurs & Entreprises + description: Gérance de l'Entreprise & Droits + order_cycle_management: + name: Gestion des Cycles de Vente + sales_tax: + name: TVA + xero_invoices: + name: Facture Xero + description: Factures pour import dans Xero + packing: + name: Rapports de préparation des paniers + subscriptions: + subscriptions: Abonnements + new: Nouvel abonnement + create: Créer abonnement + index: + please_select_a_shop: Veuillez choisir une boutique + edit_subscription: Mettre à jour Abonnement + pause_subscription: Mettre en pause Abonnement + unpause_subscription: Reprendre Abonnement + cancel_subscription: Annuler Abonnement + setup_explanation: + just_a_few_more_steps: 'Encore quelques étapes avant de pouvoir commencer:' + enable_subscriptions: "Activez la fonction abonnements pour au moins une de vos boutiques" + enable_subscriptions_step_1_html: 1. Allez à %{enterprises_link}, trouvez votre boutique, et cliquez sur "Gérer" + enable_subscriptions_step_2: 2. Sous "Préférences boutiques", activez la fonction Abonnements + set_up_shipping_and_payment_methods_html: Paramétrez au moins une méthode d'%{shipping_link} et une méthode de %{payment_link} + set_up_shipping_and_payment_methods_note_html: Notez bien que seules des méthodes de paiement de type "cash" ou "Stripe" pourront
être utilisées pour les Abonnements + ensure_at_least_one_customer_html: Assurez-vous qu'au moins un %{customer_link} est enregistré dans votre liste d'acheteurs. + create_at_least_one_schedule: Créez au moins un rythme d'abonnement + create_at_least_one_schedule_step_1_html: 1. Allez à la page %{order_cycles_link} + create_at_least_one_schedule_step_2: 2. Créez un cycle de vente si ce n'est pas encore fait + create_at_least_one_schedule_step_3: 3. Cliquez sur "+ Nouveau Rythme d'abonnement", et remplissez le formulaire + once_you_are_done_you_can_html: Une fois que c'est fait, vous pouvez %{reload_this_page_link} + reload_this_page: recharger cette page + steps: + details: 1. Informations de base + address: 2. Adresse + products: 3. Ajouter des produits + review: 4. Vérifier et Enregistrer + details: + details: Informations + invalid_error: Oups! Veuillez remplir tous les champs obligatoires... + allowed_payment_method_types_tip: Seules des méthodes de paiement de type "cash" ou "Stripe" peuvent être utilisées pour le moment + credit_card: Carte de crédit + no_cards_available: Pas de carte disponible + loading_flash: + loading: Abonnements en cours de chargement + review: + details: Informations + address: Adresse + products: Produits + product_already_in_order: Ce produit a déjà été ajouté à la commande. Veuillez directement modifier la quantité. + orders: + number: Nombre + confirm_edit: Voulez-vous vraiment modifier cette commande? Si vous poursuivez, la synchronisation automatique des modifications de l'abonnement pourrait être plus difficile à l'avenir. + confirm_cancel_msg: Voulez-vous vraiment annuler cet abonnement? Cette action sera irréversible. + cancel_failure_msg: 'Désolé, l''annulation a échoué!' + confirm_pause_msg: Voulez-vous vraiment mettre en pause cet abonnement? + pause_failure_msg: 'Désolé, la mise en pause a échoué!' + confirm_unpause_msg: Voulez-vous vraiment annuler la mise en pause de cet abonnement? + unpause_failure_msg: 'Désolé, l''annulation de la mise en pause a échoué!' + confirm_cancel_open_orders_msg: "Cet abonnement a des commandes ouvertes. Les acheteurs ont été notifiés que leur commande allait être passée. Voulez-vous annulez ces commandes ou les conserver?" + resume_canceled_orders_msg: "Certaines commandes pour cet abonnement peuvent être réouvertes dès maintenant. Vous pouvez les réouvrir depuis la liste des commandes." + yes_cancel_them: Les annuler + no_keep_them: Les conserver + yes_i_am_sure: Oui, je confirme + order_update_issues_msg: Certaines commandes n'ont pas pu être mises à jour automatiquement, probablement car elles ont été manuellement modifiées. Veuillez revoir les erreurs listées ci-dessous et effectuer si nécessaire les ajustements nécessaires sur les commandes individuelles. + no_results: + no_subscriptions: Pas encore d'abonnements... + why_dont_you_add_one: Pourquoi ne pas en créer un? :) + no_matching_subscriptions: Aucun abonnement correspondant trouvé + schedules: + destroy: + associated_subscriptions_error: Ce rythme d'abonnement ne peut pas être supprimé car il est associé à des abonnements. + stripe_connect_settings: + edit: + title: "Stripe Connect" + settings: "Paramètres" + stripe_connect_enabled: Permettre aux boutiques d'accepter les paiements via Stripe Connect ? + no_api_key_msg: Aucun compte Stripe n'existe pour cette entreprise. + configuration_explanation_html: Pour des instructions précises sur comment configurer Stripe Connect, veuillez consulter ce guide. + status: Statut + ok: Ok + instance_secret_key: Clé Secrète de l'Instance + account_id: Identifiant Compte + business_name: Nom de l'entreprise + charges_enabled: Frais activés + charges_enabled_warning: "Attention : les Frais ne sont pas activés pour votre compte" + auth_fail_error: La clé de l'API est invalide + empty_api_key_error_html: Aucune clé d'API Stripe n'a été fournie. Pour mettre en place votre clé d'API, veuillez suivre ces instructions + controllers: + enterprises: + stripe_connect_cancelled: "La connexion avec Stripe a été annulée" + stripe_connect_success: "Compte Stripe connecté avec succès" + stripe_connect_fail: Désolé, la connexion de votre compte Stripe a échoué :-( + stripe_connect_settings: + resource: Configuration de Stripe Connect + checkout: + already_ordered: + cart: "panier" + message_html: "Vous avez déjà passé une commande pour ce cycle de vente. Vérifiez votre %{cart} pour voir les produits commandés. Vous pouvez annuler ou modifier votre commande jusqu'à la fermeture du cycle de vente." + shops: + hubs: + show_closed_shops: "Afficher les boutiques fermées" + hide_closed_shops: "Masquer les boutiques fermées" + show_on_map: "Tout afficher sur la carte" + shared: + menu: + cart: + checkout: "Poursuivre la commande" + already_ordered_products: "Déjà commandé dans ce cycle de vente" + register_call: + selling_on_ofn: "Vous souhaitez proposer vos produits sur Open Food Network?" + register: "Démarrez ici" + shop: + messages: + login: "se connecter" + register: "s'inscrire" + contact: "contact" + require_customer_login: "La boutique est réservée aux membres." + require_login_html: "Déjà inscrit? %{login}. Sinon, %{register} pour pouvoir faire vos achats." + require_customer_html: "Veuillez %{contact} %{enterprise} pour devenir membre." + card_could_not_be_updated: La carte n'a pu être mise à jour + card_could_not_be_saved: la carte n'a pas pu être sauvegardée + spree_gateway_error_flash_for_checkout: "Il y a eu un problème avec vos informations de paiement : %{error}" + invoice_billing_address: "Adresse de facturation :" + invoice_column_tax: "TVA" + invoice_column_price: "Prix" + invoice_column_item: "Produit" + invoice_column_qty: "Qté" + invoice_column_unit_price_with_taxes: "Prix unitaire TTC" + invoice_column_unit_price_without_taxes: "Prix unitaire HT" + invoice_column_price_with_taxes: "Prix total TTC" + invoice_column_price_without_taxes: "Prix total HT" + invoice_column_tax_rate: "TVA applicable" + invoice_tax_total: "Total TVA :" + tax_invoice: "FACTURE" + tax_total: "Total taxe (%{rate}) :" + total_excl_tax: "Total HT :" + total_incl_tax: "Total TTC :" + abn: "SIRET" + acn: "n° TVA intracommunautaire" + invoice_issued_on: "Date de facture :" + order_number: "N° de facture :" + date_of_transaction: "Date de la transaction :" + ticket_column_qty: "Qté" + ticket_column_item: "Produit" + ticket_column_unit_price: "Prix unitaire" + ticket_column_total_price: "Prix total" + logo: "Logo (640x130)" + logo_mobile: "Logo smartphone (75x26)" + logo_mobile_svg: "Logo smartphone (SVG)" + home_hero: "Bannière" + home_show_stats: "Afficher statistiques" + footer_logo: "Logo (220x76)" + footer_facebook_url: "Facebook URL" + footer_twitter_url: "Twitter URL" + footer_instagram_url: "Instagram URL" + footer_linkedin_url: "LinkedIn URL" + footer_googleplus_url: "Google Plus URL" + footer_pinterest_url: "Pinterest URL" + footer_email: "Email" + footer_links_md: "Liens" + footer_about_url: "A propos URL" + footer_tos_url: "Conditions d'utilisation URL" + name: Nom + first_name: Prénom + last_name: Nom de famille + email: Email + phone: Téléphone + next: Suivant + address: Adresse + address_placeholder: 'ex: 24 rue de la croix verte' + address2: Adresse (suite) + city: Ville + city_placeholder: 'ex: Nantes' + postcode: Code postal + postcode_placeholder: 'ex: 44000' + state: Département + country: Pays + unauthorized: Non authorisé + terms_of_service: "Conditions d'utilisation" + on_demand: A volonté + none: Aucun + not_allowed: Non autorisé + no_shipping: pas de méthode de livraison + no_payment: pas de méthode de paiement + no_shipping_or_payment: pas de méthode de livraison ou de paiement + unconfirmed: non confirmé + days: jours + label_shop: "Boutique" + label_shops: "Boutiques" + label_map: "Carte" + label_producer: "Producteur" + label_producers: "Producteurs" + label_groups: "Groupes" + label_about: "A propos" + label_connect: "Se connecter" + label_learn: "Apprendre" + label_blog: "Blog" + label_support: "Soutien" + label_shopping: "Achats" + label_login: "Se connecter" + label_logout: "Déconnexion" + label_signup: "Inscription" + label_administration: "Administration" + label_admin: "Admin" + label_account: "Compte" + label_more: "Afficher plus" + label_less: "Masquer" + label_notices: "Informations" + cart_items: "Produits" + cart_headline: "Votre panier" + total: "Total" + cart_updating: "Mettre à jour le panier" + cart_empty: "Panier vide" + cart_edit: "Modifier votre panier" + card_number: Numéro de carte + card_securitycode: "Cryptogramme visuel" + card_expiry_date: Date d'expiration + card_masked_digit: "X" + card_expiry_abbreviation: "Exp" + new_credit_card: "Nouvelle carte de crédit" + my_credit_cards: Mes cartes bancaires + add_new_credit_card: Ajouter nouvelle carte de crédit + saved_cards: Sauvegarder cartes + add_a_card: Ajouter une Carte + add_card: Ajouter Carte + you_have_no_saved_cards: Vous n'avez pas encore sauvegardé de carte + saving_credit_card: Enregistrement de la carte de crédit... + card_has_been_removed: "Votre carte a été supprimée (numéro : %{number})" + card_could_not_be_removed: Désolée, la carte n'a pas pu être supprimée :-( + ie_warning_headline: "Votre navigateur n'est pas à jour :-(" + ie_warning_text: "Pour une expérience optimale sur Open Food Network, nous vous recommandons fortement de mettre à jour votre navigateur:" + ie_warning_chrome: Télécharger Chrome + ie_warning_firefox: Télécharger Firefox + ie_warning_ie: Mettre à jour Internet Explorer + ie_warning_other: "Impossible de mettre à jour votre navigateur? Essayez Open Food Network sur votre smartphone :-)" + footer_global_headline: "OFN Global" + footer_global_home: "Accueil" + footer_global_news: "News" + footer_global_about: "A propos" + footer_global_contact: "Contact" + footer_sites_headline: "Sites OFN" + footer_sites_developer: "Developpeur" + footer_sites_community: "Communauté" + footer_sites_userguide: "Guide utilisateur" + footer_secure: "Fiable et sécurisé." + footer_secure_text: "Open Food Network utilise un certificat type SSL (2048 bit RSA) pour garantir la confidentialité de votre commandes et données bancaires. Nos serveurs ne conservent pas vos données bancaires et les paiements sont effectués conformément aux normes de sécurité PCI." + footer_contact_headline: "Restez en contact" + footer_contact_email: "Nous écrire" + footer_nav_headline: "Naviguer" + footer_join_headline: "Nous rejoindre" + footer_join_body: "Créer un profil, une boutique ou un groupe sur Open Food Network." + footer_join_cta: "Je veux en savoir plus!" + 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}." + home_shop: Faire mes courses + brandstory_headline: "Des aliments porteurs de sens." + brandstory_intro: "Parfois, le meilleur moyen de réparer le système, c'est d'en inventer un autre..." + brandstory_part1: "Tout commence dans le sol. Avec ces paysans, agriculteurs, producteurs, engagés pour une agriculture durable et régénératrice, et désireux de partager leur histoire et leur passion avec fierté. Avec ces distributeurs souhaitant reconnecter les individus à leurs aliments et aux gens qui les produisent, soutenir les prises de conscience, dans une démarche de transparence, d'honnêteté, en assurant une juste rémunération des producteurs. Avec ces acheteurs qui croient que de meilleures décisions d'achats peuvent véritablement changer le monde." + brandstory_part2: "Nous avons besoin d'un outil pour rendre tout ça réel. Un moyen de redonner le pouvoir à ceux qui cultivent, vendent et achètent la nourriture. Un moyen de raconter les histoires, de gérer la logistique. Un moyen de transformer chaque jour les transactions en actions porteuses de changement." + brandstory_part3: "C'est pour cela que nous construisons cette plateforme, ce \"marché en ligne\", afin de rééquilibrer les échanges et redistribuer le pouvoir. Elle est transparente, pour assurer des relations équitables et favoriser les prises de conscience. Elle est open source, donc possédée par tout le monde. Elle se déploie aux échelles régionales et nationales, et des gens lancent de multiples versions à travers le monde." + brandstory_part4: "Elle fonctionne partout. Elle change tout." + brandstory_part5_strong: "Cette plateforme s'appelle Open Food Network." + brandstory_part6: "Nous aimons notre nourriture. Maintenant nous pouvons aussi aimer notre système alimentaire." + learn_body: "Explorer les modèles, les histoires et les ressources disponibles pour vous aider à développer votre propre initiative de commerce/organisation oeuvrant pour un système alimentaire équitable et juste. Trouver des outils pour vous former, des événements et autres opportunités d'apprendre de vos pairs." + learn_cta: "Découvrir " + connect_body: "Rechercher dans le répertoire des producteurs, hubs et groupes pour trouver des commerçants éthiques à côté de chez vous. Inscrivez votre commerce ou organisation sur OFN pour que les acheteurs puissent vous trouver. Rejoignez la communauté pour recevoir du soutien et résoudre ensemble les problèmes." + connect_cta: "Explorer" + system_headline: "Faire mes courses - comment ça marche?" + system_step1: "1. Recherche" + system_step1_text: "Recherchez des produits locaux, de saison, parmi nos multiples boutiques indépendantes. Filtrez par localisation ou catégorie de produits, livraison en point retrait ou à domicile." + system_step2: "2. Achat" + system_step2_text: "Transformez vos achats en choisissant des produits locaux et abordables, proposés par les divers producteurs et hubs. Découvrez les histoires et les personnes qui se cachent derrière les produits!" + system_step3: "3. Retrait / Livraison" + system_step3_text: "Réceptionnez vos produits à domicile, ou rendez vous chez votre producteur ou hub pour rencontrer les gens qui se cachent derrière les produits. Au delà de la bio-diversité, nous cultivons l'éco-diversité: vivez des expériences d'achat de nourriture uniques et humaines." + cta_headline: "Des achats qui rendent le monde un peu meilleur." + cta_label: "Je vote avec mes achats" + stats_headline: "Nous créons un nouveau système alimentaire." + stats_producers: "agriculteurs et producteurs" + stats_shops: "boutiques" + stats_shoppers: "acheteurs" + stats_orders: "commandes" + checkout_title: Finalisation commande + checkout_now: Passer la commande + checkout_order_ready: Commande prête pour + checkout_hide: Masquer + checkout_expand: Afficher + checkout_headline: "Ok, prêt à finaliser la commande?" + checkout_as_guest: "Passer commande en mode invité" + checkout_details: "Vos informations" + checkout_billing: "Informations de facturation" + checkout_default_bill_address: "Sauvegarder comme adresse de facturation par défaut" + checkout_shipping: Informations de livraison + checkout_default_ship_address: "Sauvegarder comme adresse de livraison par défaut" + checkout_method_free: Pas de frais supplémentaires + checkout_address_same: Adresse de livraison identique à l'adresse de facturation? + checkout_ready_for: "Prêt pour:" + checkout_instructions: "Commentaires ou demandes spécifiques?" + checkout_payment: Paiement + checkout_send: Passer la commande + checkout_your_order: Votre commande + checkout_cart_total: Panier total + checkout_shipping_price: Livraison + checkout_total_price: Total + checkout_back_to_cart: "Retour au Panier" + cost_currency: "Coût de la devise" + order_paid: RÉGLÉ + order_not_paid: NON RÉGLÉ + order_total: Total commande + order_payment: "Payer via:" + order_billing_address: Adresse de facturation + order_delivery_on: Livraison prévue + order_delivery_address: Adresse de livraison + order_delivery_time: Créneau de livraison/retrait + order_special_instructions: "Vos commentaires:" + order_pickup_time: Prêt à être retiré + order_pickup_instructions: Instructions de retrait + order_produce: Produit + order_total_price: Total + order_includes_tax: (dont TVA) + order_payment_paypal_successful: Votre paiement via PayPal a été réalisé avec succès. + order_hub_info: Hub Info + order_back_to_store: Retour à la boutique + order_back_to_cart: Retour au panier + bom_tip: "Utilisez cette page pour modifier les quantités sur plusieurs commandes à la fois. Les produits peuvent aussi être supprimés des commandes si nécessaire." + unsaved_changes_warning: "Des modifications n'ont pas été enregistrées et seront perdues si vous continuez." + unsaved_changes_error: "Les champs entourés en rouge contiennent des erreurs." + products: "Produits" + products_in: "dans %{oc}" + products_at: "à %{distributor}" + products_elsewhere: "Produits trouvés ailleurs" + email_welcome: "Bienvenue" + email_confirmed: "Veuillez confirmer votre adresse email." + email_registered: "fait maintenant partie de" + email_userguide_html: "Le Guide Utilisateur expliquant comment mettre en place son profil producteur ou son hub est accessible ici: %{link}" + email_admin_html: "Vous pouvez gérer votre compte en vous connectant ici %{link} ou en cliquant sur la roue en haut à droite de la page d'accueil et en sélectionnant Administration." + email_community_html: "Nous avons aussi un forum de discussion en ligne (en anglais) pour échanger avec la communauté sur des questions liées au logiciel OFN et aux défis de la gestion d'un food hub. Nous vous invitons à y participer. Nous sommes en constante évolution et vos contributions à ce forum vont façonner les prochaines étapes. %{link}" + join_community: "Rejoindre la communauté" + email_confirmation_activate_account: "Avant de pouvoir activer votre compte, nous devons nous assurer de la validité de votre adresse email." + email_confirmation_greeting: "Bonjour %{contact}!" + email_confirmation_profile_created: "Le profil pour %{name} a été créé avec succès! Pour activer votre Profil nous devons vérifier cette adresse email." + email_confirmation_click_link: "Veuillez cliquer sur le lien ci-dessous pour confirmer votre email et continuer la configuration de votre compte." + email_confirmation_link_label: "Confirmer cette adresse email »" + email_confirmation_help_html: "Après confirmation de votre email, vous pourrez accéder au compte d'administration de cette entreprise. Voir %{link} pour en savoir plus à propos de %{sitename} et commencer à utiliser votre profil et/ou boutique en ligne." + email_social: "Nous suivre:" + email_contact: "Nous écrire:" + email_signoff: "Cordialement," + email_signature: "L'équipe %{sitename}" + email_confirm_customer_greeting: "Bonjour %{name}," + email_confirm_customer_intro_html: "Merci d'avoir passé commande chez %{distributor}!" + email_confirm_customer_number_html: "Confirmation de commande #%{number}" + email_confirm_customer_details_html: "Détails de votre commande chez %{distributor}:" + email_confirm_customer_signoff: "Cordialement," + email_confirm_shop_greeting: "Bonjour %{name}," + email_confirm_shop_order_html: "Bravo! Vous avez reçu une nouvelle commande pour %{distributor}!" + email_confirm_shop_number_html: "Confirmation de commande #%{number}" + email_order_summary_item: "Produit" + email_order_summary_quantity: "Qté" + email_order_summary_price: "Prix" + email_order_summary_subtotal: "Sous-total:" + email_order_summary_total: "Total:" + email_order_summary_includes_tax: "(dont TVA)" + email_payment_paid: RÉGLÉ + email_payment_not_paid: NON RÉGLÉ + email_payment_summary: Résumé du paiement + email_payment_method: "Payer via :" + email_so_placement_intro_html: "Vous avez une nouvelle commande pour %{distributor}" + email_so_placement_details_html: "Voici les détails de votre commande pour %{distributor}:" + email_so_placement_changes: "Malheureusement, certains produits demandés n'étaient pas disponibles. Les quantités d'origine demandées apparaissent barrées ci-dessous." + email_so_payment_success_intro_html: "Un paiement automatique a été effectué pour votre commande auprès de %{distributor}." + email_so_placement_explainer_html: "Cette commande a été créée automatiquement dans le cadre de votre abonnement." + email_so_edit_true_html: "Vous pouvez effectuer des modifications jusqu'à la fermeture de la période de commande le %{orders_close_at}." + email_so_edit_false_html: "Vous pouvez consulter les détails de cette commande à tout moment." + email_so_contact_distributor_html: "Pour toute question contactez %{distributor} via %{email}." + email_so_confirmation_intro_html: "Votre commande auprès de %{distributor} est maintenant confirmée" + email_so_confirmation_explainer_html: "Cette commande a été automatiquement passée pour vous dans le cadre de votre abonnement, et a maintenant été confirmée." + email_so_confirmation_details_html: "Voici les détails concernant cette commande auprès de %{distributor}:" + email_so_empty_intro_html: "Nous avons essayé de passer votre commande auprès de %{distributor}, mais une erreur est survenue..." + email_so_empty_explainer_html: "Malheureusement, aucun des produits demandés n'étaient disponibles, nous n'avons donc pas pu passer votre commande. Les quantités d'origine demandées apparaissent barrées ci-dessous." + email_so_empty_details_html: "Voici les détails concernant la commande qui n'a pas pu être passée auprès de %{distributor}:" + email_so_failed_payment_intro_html: "Nous avons essayé d'effectuer un paiement, mais une erreur est survenue..." + email_so_failed_payment_explainer_html: "Le paiement pour l'abonnement auprès de %{distributor} a échoué à cause d'un problème avec votre carte de crédit. %{distributor} a été notifié de ce problème de paiement." + email_so_failed_payment_details_html: "Voici les détails concernant l'erreur fournis par la passerelle de paiement:" + email_shipping_delivery_details: Détails de livraison + email_shipping_delivery_time: "Livré le:" + email_shipping_delivery_address: "Adresse de livraison:" + email_shipping_collection_details: Détails de retrait + email_shipping_collection_time: "Prêt pour retrait:" + email_shipping_collection_instructions: "Instructions de retrait:" + email_special_instructions: "Vos commentaires:" + email_signup_greeting: Bonjour! + email_signup_welcome: "Bienvenue sur %{sitename}!" + email_signup_confirmed_email: "Merci d'avoir confirmé votre email." + email_signup_shop_html: "Vous pouvez maintenant vous connecter sur %{link}." + email_signup_text: "Merci d'avoir rejoint le réseau. Si vous êtes un client, nous sommes impatients de vous faire découvrir de nombreux agriculteurs fantastiques, de merveilleux hubs de distribution et des plats délicieux! Si vous êtes un producteur ou autre entreprise alimentaire, nous sommes ravis de vous compter parmi les membres du réseau." + email_signup_help_html: "Vos questions et feedbacks sont les bienvenus! Cliquez sur le bouton Envoyer un commentaire sur le site ou envoyez-nous un email à %{email}" + invite_email: + greeting: "Bonjour!" + invited_to_manage: "Vous avez été invité(e) à gérer %{enterprise} sur %{instance}." + confirm_your_email: "Vous avez reçu ou allez recevoir prochainement un email avec un lien de validation. Vous n'aurez pas accès au profil de l'entreprise %{enterprise} avant d'avoir cliqué sur ce lien." + set_a_password: "Vous serez ensuite invité(e) à choisir un mot de passe avant de pouvoir accéder et gérer le profil de l'entreprise." + mistakenly_sent: "Vous ne savez pas pourquoi vous avez reçu cet email? Veuillez contacter %{owner_email} pour plus d'informations." + producer_mail_greeting: "Cher(ère)" + producer_mail_text_before: "Nous avons reçu toutes les commandes pour la prochaine livraison." + producer_mail_order_text: "Voilà la liste et les quantités des produits commandés vous concernant:" + producer_mail_delivery_instructions: "Modalités de livraison des produits:" + producer_mail_signoff: "Merci et belle fin de journée!" + shopping_oc_closed: La boutique est actuellement fermée + shopping_oc_closed_description: "Veuillez attendre l'ouverture du prochain cycle de vente (ou contactez-nous directement pour voir si nous pouvons accepter une commande tardive)" + shopping_oc_last_closed: "Le dernier cycle de vente s'est terminé il y a %{distance_of_time}" + shopping_oc_next_open: "Le prochain cycle de vente ouvrira dans %{distance_of_time}" + shopping_tabs_about: "A propos de %{distributor}" + shopping_tabs_contact: "Contact" + shopping_contact_address: "Adresse" + shopping_contact_web: "Contact" + shopping_contact_social: "Suivre" + shopping_groups_part_of: "fait partie de:" + shopping_producers_of_hub: "Les producteurs de %{hub}:" + enterprises_next_closing: "Clôture des commandes pour ce cycle" + enterprises_ready_for: "Prêt pour" + enterprises_choose: "Choisissez votre option:" + maps_open: "Ouvre" + maps_closed: "Fermée" + hubs_buy: "Acheter:" + hubs_shopping_here: "Achats en cours" + hubs_orders_closed: "Boutique fermée" + hubs_profile_only: "Fiche profil" + hubs_delivery_options: "Options de livraison" + hubs_pickup: "Retrait" + hubs_delivery: "Livraison" + hubs_producers: "Nos producteurs" + hubs_filter_by: "Filtrer par" + hubs_filter_type: "Catégorie" + hubs_filter_delivery: "Livraison" + hubs_filter_property: "Propriétés / labels" + hubs_matches: "Vous voulez dire?" + hubs_intro: Passez commande près de chez vous + hubs_distance: Le plus près de + hubs_distance_filter: "Afficher les boutiques près de %{location}" + shop_changeable_orders_alert_html: + one: Votre commande avec %{shop} / %{order} est ouverte pour vérification. Vous pouvez effectuer des modification jusqu'à %{oc_close}. + other: Vous avez %{count} commandes avec %{shop}ouvertes à la vérification. Vous pouvez effectuer des modifications jusqu'à %{oc_close}. + orders_changeable_orders_alert_html: Cette commande a été confirmée, mais vous pouvez effectuer des modifications jusqu'à %{oc_close}. + products_clear_all: Vider + products_showing: "Afficher:" + products_with: avec + products_search: "Recherche par produit ou producteur" + products_loading: "Produits en cours de chargement..." + products_updating_cart: "Actualisation du panier..." + products_cart_empty: "Panier vide" + products_edit_cart: "Valider votre panier" + products_from: de + products_change: "Aucun changement à sauvegarder." + products_update_error: "Échec de l'enregistrement dû à:" + products_update_error_msg: "Échec de l'enregistrement." + products_update_error_data: "Échec de l'enregistrement dû à des données non valides." + products_changes_saved: "Modifications enregistrées." + search_no_results_html: "Désolé, aucun résultat pour %{query}. Autre recherche?" + components_profiles_popover: "Certaines entreprises ont juste créé leur profil sur Open Food Network mais ne vendent pas via la plateforme. Elles ont peut-être une boutique physique, ou une boutique en ligne sur une autre plateforme." + components_profiles_show: "Afficher aussi les profils" + components_filters_nofilters: "Pas de filtre" + components_filters_clearfilters: "Vider les filtres" + groups_title: Groupes + groups_headline: Groupes / réseaux territoriaux + groups_text: "Chaque producteur est unique. Chaque entreprise peut offrir quelque chose de différent. Nos groupes sont des collectifs de producteurs, des plateformes et des distributeurs qui partagent une proximité géographique, un marché fermier ou des valeurs. C'est ce qui rend votre expérience d'achat plus simple. Explorez donc ces groupes sélectionnés." + groups_search: "Recherche par nom ou mot-clé" + groups_no_groups: "Aucun groupe trouvé" + groups_about: "A propos" + groups_producers: "Nos producteurs" + groups_hubs: "Nos hubs" + groups_contact_web: Contact + groups_contact_social: Suivre + groups_contact_address: Adresse + groups_contact_email: Nous écrire + groups_contact_website: Visiter notre site web + groups_contact_facebook: Nous suivre sur Facebook + groups_signup_title: S'inscrire en tant que groupe + groups_signup_headline: Inscription groupe + groups_signup_intro: "Nous sommes une plate-forme très efficace pour le marketing collaboratif, une excellente manière pour vos membres et parties prenantes d'atteindre de nouveaux marchés. Nous sommes à but non lucratif, abordable et simple." + groups_signup_email: Nous écrire + groups_signup_motivation1: Nous transformons les systèmes alimentaires pour remettre de l'équité dans les échanges. + groups_signup_motivation2: C'est pourquoi nous sortons du lit chaque matin. Nous sommes une organisation à but non lucratif, basée sur un code source ouvert. Nous opérons en toute transparence. + groups_signup_motivation3: Vous avez de belles idées, et nous voulons vous aider. Nous partageons nos connaissances, réseaux et ressources. Nous savons que l'isolement ne crée pas le changement, alors coopérons. + groups_signup_motivation4: Nous venons à votre rencontrer. + groups_signup_motivation5: Vous êtes un réseau de circuits de distribution alternatifs, de producteurs, de distributeurs, une administration liée à l'industrie alimentaire ou une autorité locale? + groups_signup_motivation6: Quel que soit votre rôle dans la relocalisation des systèmes alimentaires, nous sommes prêts à vous soutenir. Si vous vous demandez à quoi Open Food Network ressemble / pourrait ressembler dans votre coin du monde, contactez-nous. + groups_signup_motivation7: Nous contribuons à remettre du sens dans les systèmes alimentaires. + groups_signup_motivation8: Vous avez besoin de connecter et d'outiller vos réseaux, nous offrons une plate-forme pour la coopération et l'action. Vous souhaitez de l'engagement. Nous vous aidons à atteindre les acteurs, les parties-prenantes, les secteurs. + groups_signup_motivation9: Vous avez besoin de ressources. Nous mettons à votre service notre expérience. Vous avez besoin de coopération. Nous vous connectons à un large réseau d'acteurs et d'organisations soeurs partout dans le monde. + groups_signup_pricing: Compte groupe + groups_signup_studies: Etudes de cas + groups_signup_contact: Vous voulez discuter? + groups_signup_contact_text: "Prenez contact et découvrez ce qu'Open Food Network peut faire pour vous:" + groups_signup_detail: "Plus de précisions." + login_invalid: "Email ou mot de passe erroné" + modal_hubs: "Food Hubs" + modal_hubs_abstract: Nos food hubs sont les points de contact entre vous et les personnes qui produisent votre nourriture! + modal_hubs_content1: Vous pouvez chercher le hub qui vous convient par localisation ou par nom. Certains hubs ont de multiples points de retrait de vos achats, et certains proposent également la livraison à domicile. Chaque food hub est un point de vente et gère de façon indépendante ses opérations et sa logistique - attendez-vous donc à des disparités de fonctionnement entre les hubs. + modal_hubs_content2: Vous pouvez uniquement faire vos courses dans un hub à la fois. + modal_groups: "Groupes / réseaux territoriaux" + modal_groups_content1: Voilà les organisations et les relations inter-hubs qui constituent l'Open Food Network. + modal_groups_content2: Certains groupes sont regroupés pas localisation ou région, d'autres sur des smilitudes non géographiques. + modal_how: "Comment ça marche" + modal_how_shop: Faire vos courses sur Open Food Network + modal_how_shop_explained: Recherchez un food hub près de chez vous et commencez vos achats! Vous pouvez afficher plus d'infos sur chaque food hub pour voir le type de produits qu'il propose, et cliquer sur le hub pour commencer vos achats. (Vous ne pouvez faire vos courses que dans un food hub à la fois.) + modal_how_pickup: Frais de retrait, livraison et transport + modal_how_pickup_explained: Certains food hubs livrent à domicile, d'autres vous demandent de venir récupérer vos achats dans un point de retrait. Vous pouvez voir quelle options sont proposées sur la page d'accueil du hub, et sélectionner votre choix au moment de la validation de la commande. La livraison à domicile coûtera souvent plus cher, et les prix diffèrent selon le hub. Chaque food hub est un point de vente et gère de façon indépendante ses opérations et sa logistique - attendez-vous donc à des disparités de fonctionnement entre les hubs. + modal_how_more: En savoir plus + modal_how_more_explained: "Pour en savoir plus sur Open Food Network, comment ça marche, et contribuer, allez voir:" + modal_producers: "Producteurs" + modal_producers_explained: "Nos producteurs font pousser et fabriquent tous les délicieux produits que vous pouvez acheter sur Open Food Network." + producers_about: A propos + producers_buy: Acheter + producers_contact: Contact + producers_contact_phone: Appeler + producers_contact_social: Suivre + producers_buy_at_html: "Acheter les produits de %{enterprise} dans les boutiques suivantes:" + producers_filter: Filtrer par + producers_filter_type: Catégorie + producers_filter_property: Propriété + producers_title: Producteurs + producers_headline: Trouvez un producteur local + producers_signup_title: S'inscrire en tant que producteur + producers_signup_headline: Des producteurs, indépendants + producers_signup_motivation: Vendez vos produits et racontez vos histoires pour toucher de nouveaux marchés. Gagnez du temps et de l'argent sur la gestion des opérations courantes. Vous pouvez innover sans prendre de risque. Nous nivellons le terrain de jeu pour des échanges plus équitables. + producers_signup_send: Rejoindre le réseau + producers_signup_enterprise: Comptes entreprises + producers_signup_studies: Les histoires de nos producteurs. + producers_signup_cta_headline: Rejoindre le réseau! + producers_signup_cta_action: Rejoindre le réseau + producers_signup_detail: Comment ça marche. + products_item: Produit + products_description: Description + products_variant: Variante + products_quantity: Quantité + products_available: Disponible? + products_producer: "Producteur" + products_price: "Prix" + register_title: S'inscrire + sell_title: "S'inscrire" + sell_headline: "Aller sur Open Food France!" + sell_motivation: "Mettez en avant vos beaux aliments." + sell_producers: "Producteurs" + sell_hubs: "Hubs" + sell_groups: "Groupes" + sell_producers_detail: "Créer un profil pour votre entreprise sur OFNetwork en quelques minutes. A tout moment vous pourrez créer une boutique en ligne pour vendre vos produits en direct aux acheteurs." + sell_hubs_detail: "Créer un profil pour votre entreprise de distribution ou organisation sur OFN. A tout moment vous pourrez créer une boutique multi-fournisseurs." + sell_groups_detail: "Créer un répertoire sur mesure (regroupant différents producteurs et hubs de distribution) pour votre région ou votre organisation." + sell_user_guide: "En savoir plus en explorant le guide utilisateur." + sell_listing_price: "La création d'un profil sur OFN est entièrement libre. Si vous ouvrez et gérez une boutique sur OFN, ou créez un groupe pour votre organisation ou réseau régional, nous vous invitons à contribuer au commun Open Food Network que vous utilisez. En effet, faire tourner la plateforme Open Food Network a un coût, et nous comptons sur VOUS pour contribuer à couvrir ces frais de fonctionnement (location et maintenance des serveurs, support utilisateur, nouveaux développements...). Par exemple, en reversant sous forme de don à l'association 2% de votre chiffre d'affaire, et/ou un montant fixe tous les mois. Vous pouvez aussi contribuer au commun \"en compétences\" (développement de fonctionnalités, recherche de financement, support utilisateur, etc.)." + sell_embed: "Nous pouvons aussi intégrer votre boutique OFN dans votre propre site web ou construire un site web d'alimentation locale sur mesure pour votre région." + sell_ask_services: "Nous consulter sur les services de OFN." + shops_title: Boutiques + shops_headline: Des achats qui transforment. + shops_text: Les aliments poussent selon des cycles naturels, les fermiers récoltent en cycles. Alors ici, nous achetons aussi en cycles. Si un cycle de vente est terminé, attendez le suivant ou demandez des infos au hub ! + shops_signup_title: S'inscrire en tant que hub + shops_signup_headline: Des hubs divers et variés + shops_signup_motivation: Quel que soit votre modèle, vous pouvez vous appuyer sur Open Food Network. Si vous voulez le faire évoluer, nous sommes là pour vous aider. Nous agissons selon des principes de non-lucrativité, d'indépendance, et de transparence. Et nous faisons tout notre possible pour répondre à vos besoins et vous accompagner en toute circonstance. + shops_signup_action: Rejoindre le réseau + shops_signup_pricing: Comptes entreprises + shops_signup_stories: Histoires de hubs. + shops_signup_help: Nous sommes là pour vous aider. + shops_signup_help_text: Vous avez besoin de pouvoir travailler de manière efficace. Vous avez besoin de nouveaux acheteurs et de partenaires logistiques. Vous souhaitez que votre histoire soit racontée tout au long du circuit, que l'acheteur final sache qui se trouve derrière les produits. + shops_signup_detail: Comment ça marche. + orders: Commandes + orders_fees: Frais... + orders_edit_title: Panier + orders_edit_headline: Votre panier + orders_edit_time: Commande prête pour + orders_edit_continue: Retour à la boutique + orders_edit_checkout: Finalisation commande + orders_form_empty_cart: "Vider le panier" + orders_form_subtotal: Sous-total + orders_form_admin: Admin & gestion + orders_form_total: Total + orders_oc_expired_headline: Les commandes ne sont plus possibles pour ce cycle de vente. + orders_oc_expired_text: "Désolé, les commandes pour ce cycle de vente ont été clôturées il y a %{time}! Veuillez contacter directement le hub pour voir s'il accepte les commandes tardives." + orders_oc_expired_text_others_html: "Désolé, les commandes pour ce cycle de vente ont été clôturées il y a %{time}! Veuillez contacter directement le hub pour voir s'il accepte les commandes tardives %{link}." + orders_oc_expired_text_link: "ou voir si d'autres cycles de vente sont ouverts pour ce hub" + orders_oc_expired_email: "Email:" + orders_oc_expired_phone: "Téléphone:" + orders_show_title: Confirmation de commande + orders_show_time: Commande prête pour + orders_show_order_number: "Commande #%{number}" + orders_show_cancelled: Annulée + orders_show_confirmed: Confirmée + orders_your_order_has_been_cancelled: "Votre commande a été annulée" + orders_could_not_cancel: "Désolé, la commande n'a pas pu être annulée" + orders_cannot_remove_the_final_item: "Impossible de supprimer le dernier produit d'une commande, si vous souhaitez supprimer l'ensemble des produits, veuillez annuler la commande." + orders_bought_items_notice: + one: "Un produit ajouté a bien été confirmé pour ce cycle de vente" + other: "%{count} produits ajoutés ont déjà été confirmés pour ce cycle de vente." + orders_bought_edit_button: Modifier les produits confirmés + orders_bought_already_confirmed: "* déjà confirmé" + orders_confirm_cancel: Voulez-vous vraiment annuler cette commande ? + products_cart_distributor_choice: "Distributeur pour votre commande:" + products_cart_distributor_change: "Vore distributeur pour cette commande sera dorénavant %{name} si vous ajoutez ce produit à votre panier." + products_cart_distributor_is: "Votre distributeur pour cette commande est %{name}." + products_distributor_error: "Terminez votre commande chez %{link} avant de faire vos courses chez un autre distributeur." + products_oc: "Cycle de vente pour votre commande:" + products_oc_change: "Votre cycle de vente pour cette commande sera dorénavant %{name} si vous ajoutez ce produit à votre panier." + products_oc_is: "Votre cycle de vente pour cette commande est %{name}." + products_oc_error: "Veuillez terminer votre commande pour %{link} avant de faire vos courses pour un autre cycle de vente." + products_oc_current: "votre cycle de vente actuel" + products_max_quantity: Quantité max + products_distributor: Distributeur + products_distributor_info: Quand vous choisissez un distributeur pour votre commande, les adresse et date de retrait seront affichées ici. + products_distribution_adjustment_label: "Distribution par %{distributor}du produit %{product}" + shop_trial_expires_in: "Votre période d'essai se termine dans" + shop_trial_expired_notice: "Bonne nouvelle ! Nous avons décidé d'étendre votre période d'essai jusqu'à nouvel ordre." + password: Mot de passe + remember_me: Se souvenir de moi + are_you_sure: "Confirmer?" + orders_open: Boutique ouverte + closing: "Fermeture " + going_back_to_home_page: "Retour à la page d'accueil" + creating: Création + updating: Mettre à jour + failed_to_create_enterprise: "Impossible de créer votre entreprise" + failed_to_create_enterprise_unknown: "Impossible de créer votre entreprise.\nVérifiez que tous les champs sont remplis." + failed_to_update_enterprise_unknown: "Impossible de mettre à jour votre entreprise.\nVérifiez que tous les champs sont remplis." + enterprise_confirm_delete_message: "Cette action supprimera également le produit %{product} que cette entreprise distibue. Voulez-vous vraiment continuer ?" + order_not_saved_yet: "Votre commande n'a pas encore été enregistrée. Attendez quelques secondes!" + filter_by: "Filtrer par" + hide_filters: "Masquer les filtres" + one_filter_applied: "1 filtre appliqué" + x_filters_applied: "filtres appliqués" + submitting_order: "Votre commande est en cours d'envoi : veuillez patienter" + confirm_hub_change: "Confirmer? Cette action modifiera la boutique sélectionnée et tous les articles de votre panier seront effacés." + confirm_oc_change: "Confirmer? Cette action modifiera le cycle de vente sélectionné et tous les articles de votre panier seront effacés." + location_placeholder: "Saisissez une localisation..." + error_required: "Champ obligatoire" + error_number: "saisir un nombre" + error_email: "saisir une adresse email" + error_not_found_in_database: "%{name} n'a pas été trouvé dans la base de donnée" + error_no_permission_for_enterprise: "\"%{name}\" : vous n'avez pas les droits requis pour gérer les produits de cette entreprise" + item_handling_fees: "Frais logistiques (inclus dans le prix affiché)" + january: "Janvier" + february: "Février" + march: "Mars" + april: "Avril" + may: "Mai" + june: "Juin" + july: "Juillet" + august: "Août" + september: "Septembre" + october: "Octobre" + november: "Novembre" + december: "Décembre" + email_not_found: "Adresse email non trouvée" + email_unconfirmed: "Vous devez confirmer votre email avant de mettre à jour votre mot de passe." + email_required: "Vous devez saisir une adresse email" + logging_in: "Veuillez patienter, connexion en cours" + signup_email: "Votre email" + choose_password: "Choisissez un mot de passe" + confirm_password: "Confirmez votre mot de passe" + action_signup: "S'inscrire" + welcome_to_ofn: "Bienvenue sur Open Food Network" + signup_or_login: "Commencez par vous inscrire (ou vous connecter)" + have_an_account: "Déjà inscrit?" + action_login: "Se connecter." + forgot_password: "Mot de passe oublié?" + password_reset_sent: "Un email contenant les instructions pour changer votre mot de passe a été envoyé!" + reset_password: "Changer de mot de passe" + who_is_managing_enterprise: "Qui gère %{enterprise}?" + update_and_recalculate_fees: "Mettre à jour et recalculer les frais" + enterprise: + registration: + modal: + steps: + details: + title: 'Détails' + headline: "Commençons !" + enterprise: "Hey ! Nous avons d'abord besoin de quelques informations sur votre entreprise :" + producer: "Hey ! Nous avons d'abord besoin de quelques informations sur votre ferme :" + enterprise_name_field: "Nom de l'entreprise :" + producer_name_field: "Nom de la ferme :" + producer_name_field_placeholder: "ex: La Ferme du Marais" + producer_name_field_error: "Veuillez choisir le nom de votre entreprise" + address1_field: "Adresse ligne 1" + address1_field_placeholder: "ex : 35 rue du bac" + address1_field_error: "Veuillez saisir une adresse" + address2_field: "Adresse ligne 2" + suburb_field: "Ville :" + suburb_field_placeholder: "ex : Nantes" + suburb_field_error: "Veuillez saisir une ville" + postcode_field: "Code postal :" + postcode_field_placeholder: "ex : 44000" + postcode_field_error: "Veuillez saisir le code postal" + state_field: "Département :" + state_field_error: "Veuillez saisir un Département" + country_field: "Pays :" + country_field_error: "Veuillez saisir une Pays" + contact: + title: 'Contact' + contact_field: 'Personne référente' + contact_field_placeholder: 'Nom du contact principal' + contact_field_required: "Vous devez saisir une personne référente" + email_field: 'Adresse email' + email_field_placeholder: 'ex : robert@mabelleferme.fr' + phone_field: 'Numéro de téléphone' + phone_field_placeholder: 'ex : 06 24 53 26 53' + type: + title: 'Catégorie' + headline: "Dernière étape pour ajouter %{enterprise} !" + question: "Etes-vous un producteur ?" + yes_producer: "Oui, je suis un producteur" + no_producer: "Non, je ne suis pas un producteur" + producer_field_error: "Veuillez faire un choix. Etes vous un producteur?" + yes_producer_help: "Un producteur fabrique de bonnes choses à boire et à manger. Vous êtes un producteur si vous les faites pousser, les élevez, les pétrissez, transformez, fermentez, les réduisez en grains, etc." + no_producer_help: "Si vous n'êtes pas un producteur, vous êtes probablement un revendeur ou distributeur alimentaire : un \"hub\", une coopérative, un groupement d'achat, un revendeur, un grossiste, ou autre." + about: + title: 'A propos' + images: + title: 'Images' + social: + title: 'Réseaux sociaux' + enterprise_contact: "Personne référente" + enterprise_contact_placeholder: "Nom du contact principal" + enterprise_contact_required: "Vous devez saisir une personne référente" + enterprise_email_address: "Adresse email" + enterprise_email_placeholder: "ex : robert@mabelleferme.fr" + enterprise_phone: "Numéro de téléphone" + enterprise_phone_placeholder: "ex : 06 24 53 26 53" + back: "Retour" + continue: "Suivant" + limit_reached_headline: "Oh non!" + limit_reached_message: "Vous avez atteint la limite!" + limit_reached_text: "Vous avez atteint la limite du nombre d'entreprises que vous êtes autorisés à gérer sur" + limit_reached_action: "Retour sur la page d'accueil" + select_promo_image: "Etape 3. Sélectionnez une image promotionnelle" + promo_image_tip: "Conseil: affichée en format bannière, taille optimale 1200×260px" + promo_image_label: "Choisissez une image promotionnelle" + action_or: "OU" + promo_image_drag: "Glissez déplacez votre image promotionnelle ici" + review_promo_image: "Etape 4. Validez votre bannière promotionnelle" + review_promo_image_tip: "Conseil: pour un résultat optimal, votre image promotionnelle doit être adaptée à l'espace disponible" + promo_image_placeholder: "Votre logo apparaîtra ici pour validation une fois uploadé" + uploading: "Upload en cours..." + select_logo: "Etape 1. Insérez votre logo" + logo_tip: "Conseil: utilisez un format d'image carré de préférence, min 300×300px" + logo_label: "Insérez votre logo" + logo_drag: "Glissez déplacez votre logo ici" + review_logo: "Etape 2: Validez votre logo" + review_logo_tip: "Conseil: pour un résultat optimal, votre logo doit être adapté à l'espace disponible" + logo_placeholder: "Votre logo apparaîtra ici pour validation une fois uploadé" + enterprise_about_headline: "Bien joué!" + enterprise_about_message: "A présent, allons un peu plus dans les détails concernant" + enterprise_success: "Opération réussie! %{enterprise} a été ajoutée à Open Food Network" + enterprise_description: "Description courte" + enterprise_description_placeholder: "Une phrase pour décrire votre organisation" + enterprise_long_desc: "Description longue" + enterprise_long_desc_placeholder: "Vous pouvez ici raconter l'histoire de votre organisation - votre projet, les valeurs que vous défendez. Nous vous conseillons de ne pas dépasser 600 caractères ou 150 mots." + enterprise_long_desc_length: "%{num} caractères / inférieur à 600 recommandé" + enterprise_limit: Nombre max d'entreprises + enterprise_abn: "SIRET" + enterprise_abn_placeholder: "ex: 404 833 048 00022" + enterprise_acn: "n° TVA intracommunautaire" + enterprise_acn_placeholder: "ex: 404 833 048" + enterprise_tax_required: "Merci de choisir." + enterprise_final_step: "Dernière étape!" + enterprise_social_text: "Comment trouver la boutique en ligne %{enterprise} ?" + website: "Site internet" + website_placeholder: "ex: openfoodnetwork.ca" + facebook: "Facebook" + facebook_placeholder: "ex: www.facebook.com/NomDeLaPage" + linkedin: "LinkedIn" + linkedin_placeholder: "ex: www.linkedin.com/VotreNom" + twitter: "Twitter" + twitter_placeholder: "ex: @twitter_pseudo" + instagram: "Instagram" + instagram_placeholder: "ex: @instagram_pseudo" + registration_greeting: "Bonjour!" + registration_intro: "Vous pouvez maintenant créer votre profil \"Producteur\" ou \"Hub\"" + registration_action: "Démarrons!" + registration_checklist: "Vous aurez besoin de" + registration_time: "5-10 minutes" + registration_enterprise_address: "L'adresse de l'entreprise" + registration_contact_details: "Les détails du contact référent" + registration_logo: "Votre logo" + registration_promo_image: "Une image bannière pour votre profil" + registration_about_us: "Un texte \"A propos\"" + registration_outcome_headline: "Qu'est-ce que ça m'apporte?" + registration_outcome1_html: "Votre profil permet aux gens de vous trouver et de vous contacter via Open Food Network." + registration_outcome2: "Utilisez cet espace pour raconter l'histoire de votre entreprise, et stimuler les visites vers vos points de présence en ligne." + registration_outcome3: "C'est aussi le premier pas vers la vente via Open Food Network, ou l'ouverture de votre boutique en ligne." + registration_finished_headline: "C'est terminé!" + registration_finished_thanks: "Merci d'avoir complété le profil de %{enterprise}" + registration_finished_login: "Vous pouvez modifier ou mettre à jour les détails de votre entreprise à tout moment en vous connectant sur Open Food Network, rubrique Admin." + registration_finished_action: "Accueil Open Food Network" + registration_contact_name: 'Nom du contact principal' + registration_type_headline: "Dernière étape pour ajouter %{enterprise}!" + registration_type_question: "Etes-vous un producteur?" + registration_type_producer: "Oui, je suis un producteur" + registration_type_no_producer: "Non, je ne suis pas un producteur" + registration_type_error: "Veuillez faire un choix. Etes vous un producteur?" + registration_type_producer_help: "Un producteur fabrique de bonnes choses à boire et à manger. Vous êtes un producteur si vous les faites pousser, les élevez, les pétrissez, transformez, fermentez, les réduisez en grains, etc." + registration_type_no_producer_help: "Si vous n'êtes pas un producteur, vous êtes probablement un revendeur ou distributeur alimentaire: un \"hub\", une coopérative, un groupement d'achat, un revendeur, un grossiste, ou autre." + create_profile: "Créer votre profil" + registration_images_headline: "Merci!" + registration_images_description: "Ajoutez maintenant de jolies photos pour que votre profil soit attractif! :)" + registration_detail_headline: "Commençons" + registration_detail_enterprise: "Woohoo! Dites-nous déjà quelques mots à propos de votre entreprise:" + registration_detail_producer: "Woohoo! Dites-nous déjà quelques mots à propos de votre ferme:" + registration_detail_name_enterprise: "Nom de l'entreprise:" + registration_detail_name_producer: "Nom de la ferme:" + registration_detail_name_placeholder: "ex: La super ferme de Charlie" + registration_detail_name_error: "Veuillez choisir le nom de votre entreprise" + registration_detail_address1: "Adresse ligne 1" + registration_detail_address1_placeholder: "ex: 123 rue des étangs" + registration_detail_address1_error: "Veuillez saisir une adresse" + registration_detail_address2: "Adresse ligne 2" + registration_detail_suburb: "Ville:" + registration_detail_suburb_placeholder: "ex: Montréal" + registration_detail_suburb_error: "Veuillez saisir une ville" + registration_detail_postcode: "Code postal:" + registration_detail_postcode_placeholder: "ex: J3H 3K5" + registration_detail_postcode_error: "Veuillez saisir le code postal" + registration_detail_state: "Région:" + registration_detail_state_error: "Veuillez saisir une Région" + registration_detail_country: "Pays:" + registration_detail_country_error: "Veuillez saisir un Pays" + shipping_method_destroy_error: "Cette méthode de livraison ne peut pas être supprimée car elle est référencée dans une commande : %{number}." + accounts_and_billing_task_already_running_error: "Une autre tache est en cours, merci de patienter un instant..." + accounts_and_billing_start_task_notice: "Tache mise en file d'attente" + fees: "Frais" + item_cost: "Coût du produit" + bulk: "Vrac" + shop_variant_quantity_min: "min" + shop_variant_quantity_max: "max" + follow: "Suivre" + shop_for_products_html: "Acheter les produits de %{enterprise} dans les boutiques suivantes:" + change_shop: "Changer de boutique pour:" + shop_at: "Acheter maintenant :" + price_breakdown: "Détail du prix:" + admin_fee: "Frais de gestion admin" + sales_fee: "Frais de ventes/marketing" + packing_fee: "Frais de conditionnement" + transport_fee: "Frais logistiques" + fundraising_fee: "Frais recherche de financement" + price_graph: "Légende détail du prix" + included_tax: "Inclut TVA" + balance: "Solde" + transaction: "Transaction" + transaction_date: "Date" + payment_state: "Statut du paiement" + shipping_state: "Statut de la livraison" + value: "Nb unités" + balance_due: "Montant dû" + credit: "Crédit" + Paid: "Payé" + Ready: "Prêt" + ok: OK + not_visible: invisible + you_have_no_orders_yet: "Vous n'avez pas encore de commande" + running_balance: "Solde courant" + outstanding_balance: "Solde restant" + admin_entreprise_relationships: "Permissions inter-entreprises" + admin_entreprise_relationships_everything: "Tout" + admin_entreprise_relationships_permits: "autorise" + admin_entreprise_relationships_seach_placeholder: "Chercher" + admin_entreprise_relationships_button_create: "Créer" + admin_entreprise_groups: "Groupes d'entreprises" + admin_entreprise_groups_name: "Nom" + admin_entreprise_groups_owner: "Gérant" + admin_entreprise_groups_on_front_page: "Sur la page d'accueil?" + admin_entreprise_groups_entreprise: "Entreprises" + admin_entreprise_groups_data_powertip: "L'utilisateur principal en charge de ce groupe." + admin_entreprise_groups_data_powertip_logo: "Il s'agit du logo du groupe" + admin_entreprise_groups_data_powertip_promo_image: "Cette image est affichée en haut du profil Groupe." + admin_entreprise_groups_contact: "Contact" + admin_entreprise_groups_contact_phone_placeholder: "ex: 98 7654 3210" + admin_entreprise_groups_contact_address1_placeholder: "ex: 24 rue de la croix verte" + admin_entreprise_groups_contact_city: "Ville" + admin_entreprise_groups_contact_city_placeholder: "ex: Bordeaux" + admin_entreprise_groups_contact_zipcode: "Code postal" + admin_entreprise_groups_contact_zipcode_placeholder: "ex: 14120" + admin_entreprise_groups_contact_state_id: "Département" + admin_entreprise_groups_contact_country_id: "Pays" + admin_entreprise_groups_web: "Liens web" + admin_entreprise_groups_web_twitter: "ex: @OpenFoodNet_fr" + admin_entreprise_groups_web_website_placeholder: "ex: www.monepicerieenligne.fr" + admin_order_cycles: "Gérer les cycles de vente" + open: "Ouvre" + close: "Ferme" + create: "Créer" + search: "Rechercher" + supplier: "Fournisseurs" + product_name: "Nom du Produit" + product_description: "Description du Produit" + units: "Unité de mesure" + coordinator: "Coordinateur" + distributor: "Distributeur" + enterprise_fees: "Marges et commissions" + process_my_order: "Valider ma Commande" + delivery_instructions: Instructions de Livraison + delivery_method: Méthode de Livraison + fee_type: "Type de marge" + tax_category: "TVA applicable" + calculator: "Calculateur" + calculator_values: "Valeurs applicables" + flat_percent_per_item: "Pourcentage net" + flat_rate_per_item: "Montant fixe par article (hors articles au poids/volume)" + flat_rate_per_order: "Montant fixe par commande" + flexible_rate: "Montant variable selon nb articles" + price_sack: "Montant variable selon total commande" + new_order_cycles: "Nouveaux cycles de vente" + new_order_cycle: "Nouveau Cycle de Vente" + select_a_coordinator_for_your_order_cycle: "Choisissez un coordinateur pour votre cycle de vente" + notify_producers: 'Notifier les producteurs' + edit_order_cycle: "Modifier le cycle de vente" + roles: "Roles" + update: "Mettre à jour" + delete: Supprimer + add_producer_property: "Ajouter une propriété" + in_progress: "En cours" + started_at: "Commencé à" + queued: "En attente" + scheduled_for: "Prévu pour" + customers: "Acheteurs" + please_select_hub: "Veuillez sélectionner un Hub" + loading_customers: "Chargement de la liste des acheteurs" + no_customers_found: "Aucun acheteur trouvé" + go: "Lancer" + hub: "Hub" + producer: "Producteur" + product: "Produit" + price: "Prix" + on_hand: "En stock" + save_changes: "Sauvegarder les modifications" + order_saved: "Commande Sauvegardée" + no_products: Pas de Produits + spree_admin_overview_enterprises_header: "Mes entreprises" + spree_admin_overview_enterprises_footer: "GÉRER MES ENTREPRISES" + spree_admin_enterprises_hubs_name: "Nom" + spree_admin_enterprises_create_new: "CRÉER NOUVELLE" + spree_admin_enterprises_shipping_methods: "Méthodes de livraison" + spree_admin_enterprises_fees: "Marges et commissions" + spree_admin_enterprises_none_create_a_new_enterprise: "CRÉER UNE NOUVELLE ENTREPRISE" + spree_admin_enterprises_none_text: "Vous n'avez pas encore d'entreprise" + spree_admin_enterprises_tabs_hubs: "HUBS" + spree_admin_enterprises_producers_manage_products: "GÉRER LES PRODUITS" + spree_admin_enterprises_any_active_products_text: "Vous n'avez aucun produit actif." + spree_admin_enterprises_create_new_product: "CRÉER UN NOUVEAU PRODUIT" + spree_admin_single_enterprise_alert_mail_confirmation: "Veuillez confirmer l'adresse mail pour" + spree_admin_single_enterprise_alert_mail_sent: "Email envoyé à " + spree_admin_overview_action_required: "Action requise" + spree_admin_overview_check_your_inbox: "Veuillez vérifier votre boîte mail pour les prochaines étapes. Merci!" + spree_admin_unit_value: Nb Unités + spree_admin_unit_description: 'Description complémentaire (ex: "(vrac)")' + spree_admin_variant_unit: Unité + spree_admin_variant_unit_scale: Echelle unitaire (en g ou L) + spree_admin_supplier: Fournisseur + spree_admin_product_category: Catégorie Produit + spree_admin_variant_unit_name: Nom de la pièce (si vendu à la pièce) + change_package: "Changer de type de compte" + spree_admin_single_enterprise_hint: "Astuce: Pour permettre aux gens de vous trouver, activez votre visibilité " + spree_admin_eg_pickup_from_school: "ex : \"Retrait des produits à l'Ecole Marimati / Au Café du coin / chez Babette / ...\"" + spree_admin_eg_collect_your_order: "ex : \"Veuillez récupérer votre commande au 123 Parliament Street, Toronto, Ontario 3070 \"" + spree_classification_primary_taxon_error: "L'intitulé %{taxon}est l'intitulé de base pour %{product} et ne peut être supprimé" + spree_order_availability_error: "Le distributeur ne peut fournir les produits de votre panier pour ce cycle de vente." + spree_order_populator_error: "Le distributeur ne peut fournir tous les produits de votre panier pour ce cycle de vente. Merci de choisir un autre distributeur ou un autre cycle de vente." + spree_order_populator_availability_error: "Ce produit n'est pas disponible pour ce cycle de vente / distributeur." + spree_distributors_error: "Veuillez sélectionner au moins un hub" + spree_user_enterprise_limit_error: "^ %{email} ne peut pas créer de nouvelles entreprises (limite actuelle : %{enterprise_limit} entreprises )." + spree_variant_product_error: doit avoir au moins une variante + your_profil_live: "Votre profil en ligne" + on_ofn_map: "sur la carte Open Food Network" + see: "Voir" + live: "en ligne" + manage: "Gérer" + resend: "Renvoyer" + trial: Découverte + add_and_manage_products: "Ajouter & gérer des produits" + add_and_manage_order_cycles: "Ajouter & gérer des cycles de vente" + manage_order_cycles: "Gérer les cycles de vente" + manage_products: "Gérer les produits" + edit_profile_details: "Modifier les informations du profil" + edit_profile_details_etc: "Modifier la description, les images, etc." + order_cycle: "Cycle de vente" + order_cycles: "Cycles de Vente" + enterprises: "Entreprises" + enterprise_relationships: "Permissions inter-entreprises" + remove_tax: "Retirer TVA" + enterprise_terms_of_service: "Conditions Générales d'Utilisation" + enterprises_require_tos: "Les entreprises doivent accepter les Conditions Générales d'Utilisation" + enterprise_tos_link: "Lien vers les Conditions Générales d'Utilisation" + enterprise_tos_message: "Nous soutenons la mise en place d'un système alimentaire résilient et durable, et souhaitons œuvrer avec des entreprises qui partagent nos valeurs et notre vision. Ainsi, nous demandons aux entreprises s'enregistrant sur Open Food Network de valider nos " + enterprise_tos_link_text: "Conditions d'utilisation" + enterprise_tos_agree: "J'adhère aux valeurs d'Open Food Network et valide les Conditions Générales d'Utilisation." + tax_settings: "Paramètres TVA" + products_require_tax_category: "vous devez choisir la TVA applicable" + admin_shared_address_1: "Adresse" + admin_shared_address_2: "Adresse (suite)" + admin_share_city: "Ville" + admin_share_zipcode: "Code postal" + admin_share_country: "Pays" + admin_share_state: "Département" + hub_sidebar_hubs: "Hubs" + hub_sidebar_none_available: "Aucun disponible" + hub_sidebar_manage: "Gérer" + hub_sidebar_at_least: "Sélectionnez un/des hubs" + hub_sidebar_blue: "bleu" + hub_sidebar_red: "rouge" + shop_trial_in_progress: "Votre période de test se termine dans %{days}." + report_customers_distributor: "Distributeur" + report_customers_supplier: "Fournisseurs" + report_customers_cycle: "Cycle de vente" + report_customers_type: "Type de rapport" + report_customers_csv: "Télécharger en csv" + report_producers: "Producteurs:" + report_type: "Type de rapport: " + report_hubs: "Hubs:" + report_payment: "Méthodes de paiement:" + report_distributor: "Distributeurs:" + report_payment_by: 'Paiements par type' + report_itemised_payment: 'Détail du paiement' + report_payment_totals: 'Total des paiements' + report_all: 'tous' + report_order_cycle: "Cycle de vente:" + report_entreprises: "Entreprises:" + report_users: "Utilisateurs" + report_tax_rates: TVA par taux + report_tax_types: TVA par type de produit/service + report_header_order_cycle: Cycle de Vente + report_header_user: Utilisateur + report_header_email: Email + report_header_status: Statut + report_header_comments: Commentaire + report_header_first_name: Prénom + report_header_last_name: Nom + report_header_phone: n° tel + report_header_suburb: Ville + report_header_address: Adresse + report_header_billing_address: Adresse de facturation + report_header_relationship: Droits + report_header_hub: Hub + report_header_hub_address: Adresse du Hub + report_header_to_hub: Distributeur + report_header_hub_code: Code du Hub + report_header_code: Code + report_header_paid: Payé ? + report_header_delivery: Livré ? + report_header_shipping: Livraison + report_header_shipping_method: Méthode de Livraison + report_header_shipping_instructions: Instructions de Livraison + report_header_ship_street: Rue Livraison + report_header_ship_street_2: ' Rue (2) Livraison' + report_header_ship_city: Ville Livraison + report_header_ship_postcode: Code Postal Livraison + report_header_ship_state: Département Livraison + report_header_billing_street: Rue Facturation + report_header_billing_street_2: Rue (2) Facturation + report_header_billing_street_3: Rue (3) Facturation + report_header_billing_street_4: Rue (4) Facturation + report_header_billing_city: Ville Facturation + report_header_billing_postcode: Code Postal Facturation + report_header_billing_state: Département Facturation + report_header_incoming_transport: Transport réception + report_header_special_instructions: Note au producteur + report_header_order_number: N° commande + report_header_date: Date + report_header_confirmation_date: Date de confirmation + report_header_tags: Tags + report_header_items: Produits + report_header_items_total: "Montant total des produits %{currency_symbol}" + report_header_taxable_items_total: "Montant produits soumis à TVA (%{currency_symbol})" + report_header_sales_tax: "TVA sur produits (%{currency_symbol})" + report_header_delivery_charge: "Frais de livraison (%{currency_symbol})" + report_header_tax_on_delivery: "TVA sur livraison (%{currency_symbol})" + report_header_tax_on_fees: "TVA sur commission hub (%{currency_symbol})" + report_header_total_tax: "Total TVA (%{currency_symbol})" + report_header_enterprise: Entreprise + report_header_customer: Acheteur + report_header_customer_code: Code acheteur + report_header_product: Produit + report_header_product_properties: Propriétés / labels Produits + report_header_quantity: Nb commandé + report_header_max_quantity: Quantité Max + report_header_variant: Variante + report_header_variant_value: Nb Unités Variante + report_header_variant_unit: Unité + report_header_total_available: Total disponible + report_header_unallocated: Non alloué + report_header_max_quantity_excess: Dépassement Qté Max + report_header_taxons: Intitulés + report_header_supplier: Fournisseur + report_header_producer: Producteur + report_header_producer_suburb: Ville Producteur + report_header_unit: Unité + report_header_group_buy_unit_quantity: Nb d'unités achetées (vente par lots) + report_header_cost: Coût + report_header_shipping_cost: Coût de livraison + report_header_curr_cost_per_unit: Prix prod unitaire + report_header_total_shipping_cost: Total coût de livraison + report_header_payment_method: Méthode de paiement + report_header_sells: Vend + report_header_visible: Visible + report_header_price: Prix + report_header_unit_size: Unité de mesure + report_header_distributor: Distributeur + report_header_distributor_address: Adresse Hub Distributeur + report_header_distributor_city: Ville Distributeur + report_header_distributor_postcode: Code Postal Distributeur + report_header_delivery_address: Adresse Livraison + report_header_delivery_postcode: Code Postal Livraison + report_header_bulk_unit_size: Quantité totale du lot + report_header_weight: Poids + report_header_sum_total: Somme Totale + report_header_date_of_order: Date de Commande + report_header_amount_owing: Montant dû + report_header_amount_paid: Montant payé + report_header_units_required: Nb Unités Requises + report_header_remainder: Reste à payer + report_header_order_date: Date de commande + report_header_order_id: N° Commande + report_header_item_name: Nom de la pièce + report_header_temp_controlled_items: Article à température contrôlée ? + report_header_customer_name: Nom Acheteur + report_header_customer_email: E-mail Acheteur + report_header_customer_phone: Tel Acheteur + report_header_customer_city: Ville Acheteur + report_header_payment_state: Statut du Paiement + report_header_payment_type: Type de Paiement + report_header_item_price: "Coût produits (%{currency})" + report_header_item_fees_price: "Coût produits + Marge (%{currency})" + report_header_admin_handling_fees: "Admin et gestion (%{currency})" + report_header_ship_price: "Frais de livraison (%{currency})" + report_header_pay_fee_price: "Frais de Transaction (%{currency})" + report_header_total_price: "Total (%{currency})" + report_header_product_total_price: "Total Produit (%{currency})" + report_header_shipping_total_price: "Total Livaison (%{currency})" + report_header_outstanding_balance_price: "Solde (%{currency})" + report_header_eft_price: "TEF / Transfert Electronique (%{currency})" + report_header_paypal_price: "Paypal (%{currency})" + report_header_sku: Référence Produit + report_header_amount: Quantité + report_header_balance: Solde + report_header_total_cost: "Coût Total" + report_header_total_ordered: Total Commandé + report_header_total_max: Max Total + report_header_total_units: Vol. total + report_header_sum_max_total: "Somme Max Total" + report_header_total_excl_vat: "Total HT (%{currency_symbol})" + report_header_total_incl_vat: "Total TTC (%{currency_symbol})" + report_header_temp_controlled: Temp Contrôlée ? + report_header_is_producer: Producteur ? + report_header_not_confirmed: Non confirmé + report_header_gst_on_income: TVA sur revenu + report_header_gst_free_income: Revenu TVA déduite + report_header_total_untaxable_produce: Total produits non taxable + report_header_total_taxable_produce: Total produits soumis à TVA (inclut TVA) + report_header_total_untaxable_fees: Total marges et frais annexes non taxables + report_header_total_taxable_fees: Total marges et frais annexes soumis à TVA (inclut TVA) + report_header_delivery_shipping_cost: Coût de Livraison (incl. TVA) + report_header_transaction_fee: Frais de Transaction (TVA non incluse) + report_header_total_untaxable_admin: Total ajustements non taxables + report_header_total_taxable_admin: Total ajustments soumis à TVA (inclut TVA) + initial_invoice_number: "N° de facture initial:" + invoice_date: "Date de facture:" + due_date: "Date d'échéance:" + account_code: "Code compte:" + equals: "Egal" + contains: "contient" + discount: "Réduction" + filter_products: "Filtrer les produits" + delete_product_variant: "La variante ne peut pas être supprimée!" + progress: "en cours" + saving: "Enregistrement..." + success: "succès" + failure: "échec" + unsaved_changes_confirmation: "Les changements non sauvegardés seront perdus. Continuer?" + one_product_unsaved: "Des changements sur un produit n'ont pas été sauvegardés." + products_unsaved: "Des changements sur %{n} produits n'ont pas été sauvegardés." + is_already_manager: "est déjà manager!" + no_change_to_save: "Pas de changement à sauvegarder" + user_invited: "%{email}a été invité à gérer cette entreprise" + add_manager: "Ajouter un utilisateur existant" + users: "Utilisateurs" + about: "A propos" + images: "Images" + web: "Web" + primary_details: "Informations de base" + adrdress: "Adresse" + contact: "Contact" + social: "Réseaux sociaux" + business_details: "Informations juridiques" + properties: "Propriétés / labels" + shipping: "Expédition" + shipping_methods: "Méthodes de livraison" + payment_methods: "Méthodes de paiement" + payment_method_fee: "Frais de transaction" + inventory_settings: "paramètres catalogue de produits" + tag_rules: "Règles de tag" + shop_preferences: "Préférences boutique" + enterprise_fee_whole_order: Commande totale + enterprise_fee_by: "%{type}marges/frais par %{role} %{enterprise_name}" + validation_msg_relationship_already_established: "^Un lien est déjà établi." + validation_msg_at_least_one_hub: "^Sélectionnez au moins un hub" + validation_msg_product_category_cant_be_blank: "^Veuillez sélectionner la catégorie produit" + validation_msg_tax_category_cant_be_blank: "^Veuillez sélectionner la TVA applicable" + validation_msg_is_associated_with_an_exising_customer: "est associé à un acheteur existant" + content_configuration_pricing_table: "(A FAIRE : Tableau des tarifs)" + content_configuration_case_studies: "(A FAIRE : Etudes de Cas)" + content_configuration_detail: "(A FAIRE : Détails)" + enterprise_name_error: "Est déjà prit. Si c'est votre entreprise et que vous souhaitez revendiquer la propriété, ou si vous souhaitez échanger avec elle, veuillez contacter le manager actuel de ce profil " + enterprise_owner_error: "^ %{email} ne peut pas créer de nouvelles entreprises (limite actuelle : %{enterprise_limit} entreprises )." + enterprise_role_uniqueness_error: "^Ce rôle existe déjà." + inventory_item_visibility_error: doit être vrai ou faux + product_importer_file_error: "erreur : aucun document importé" + product_importer_spreadsheet_error: "impossible de traiter le fichier : type de fichier invalide" + product_importer_products_save_error: n'a pu sauvegarder aucun produit :-( + product_import_file_not_found_notice: 'Fichier non trouvé ou impossible à ouvrir' + product_import_no_data_in_spreadsheet_notice: 'Aucune donnée trouvée dans le tableau' + order_choosing_hub_notice: Votre hub a été sélectionné. + order_cycle_selecting_notice: Votre cycle de vente a été sélectionné. + adjustments_tax_rate_error: "^Veuillez vérifier la TVA applicable pour cet ajustement." + active_distributors_not_ready_for_checkout_message_singular: >- + Le hub %{distributor_names} est sélectionné dans un cycle de vente actif, mais + n'a pas paramétré de méthode de livraison et/ou de paiement. La boutique de + ce hub restera inaccessible jusqu'à ce qu'une méthode de livraison et une méthode + de paiement aient été paramétrées. + active_distributors_not_ready_for_checkout_message_plural: >- + Les hubs %{distributor_names} sont sélectionnés dans un cycle de vente actif, + mais n'ont pas paramétré de méthode de livraison et/ou de paiement. Les boutiques + de ces hubs resteront inaccessibles jusqu'à ce qu'une méthode de livraison et + une méthode de paiement aient été paramétrées. + enterprise_fees_update_notice: Les marges et commissions de votre entreprise ont été mises à jour. + enterprise_fees_destroy_error: "Cette marge ou commission ne peut être supprimée car elle est utilisée par la vente suivante : %{id} - %{name}." + enterprise_register_package_error: "Veuillez choisir une option" + enterprise_register_error: "L'inscription a échoué pour %{enterprise}" + enterprise_register_success_notice: "Bravo ! L'entreprise %{enterprise} est maintenant inscrite sur Open Food Network :-)" + enterprise_bulk_update_success_notice: "Entreprises mises à jour avec succès" + enterprise_bulk_update_error: 'Echec dans la mise à jour' + order_cycles_create_notice: 'Votre cycle de vente a été créé.' + order_cycles_update_notice: 'Votre cycle de vente a été mis à jour.' + order_cycles_bulk_update_notice: 'Des cycles de vente ont été mis à jour.' + order_cycles_clone_notice: "Votre cycle de vente %{name} a été dupliqué." + order_cycles_email_to_producers_notice: 'Les emails à destination des producteurs ont été mis en file d''attente pour envoi.' + order_cycles_no_permission_to_coordinate_error: "Aucune de vos entreprises n'a les droits requis pour coordonner un cycle de vente" + order_cycles_no_permission_to_create_error: "Vous n'avez pas les droits requis pour créer un cycle de vente coordonné par cette entreprise" + back_to_orders_list: "Retour à la liste des commandes" + no_orders_found: "Aucune commande trouvée" + order_information: "Info commande" + date_completed: "Date d'opération" + amount: "Montant" + state_names: + ready: Prêt + pending: En attente + shipped: Expédié + js: + saving: 'Enregistrement en cours...' + changes_saved: 'Modifications sauvegardées.' + save_changes_first: Veuillez d'abord sauvegarder les modifications. + all_changes_saved: Toutes les modifications ont été sauvegardées. + unsaved_changes: Des modifications n'ont pas été sauvegardées + all_changes_saved_successfully: Toutes les modifications ont été sauvegardées avec succès + oh_no: "Oups ! Nous n'avons pas réussi à sauvegarder vos modification :-(" + unauthorized: "Vous n'avez pas les droits d'accès à cette page." + error: Erreur + unavailable: Non disponible + profile: Profil + hub: Hub + shop: Boutique + choose: Choisir + resolve_errors: Veuillez corriger les erreurs suivantes + more_items: "+ %{count} en plus" + default_card_updated: La carte bancaire par défaut a été mise à jour + admin: + enterprise_limit_reached: "Vous avez atteint le nombre limite d'entreprises autorisées par défaut. Ecrivez à %{contact_email}si vous avez besoin d'augmenter cette limite." + modals: + got_it: J'ai compris + close: "Fermer" + invite: "Inviter" + invite_title: "Inviter un nouvel utilisateur" + tag_rule_help: + title: Règles de tag + overview: Aperçu + overview_text: > + Les règles de tag vous permettent de paramétrer ce qui est vu ou pas + par tel ou tel type d'acheteur. Par exemple des options de livraison, + des méthodes de paiement, des produits, ou des cycles de vente. + by_default_rules: "Règles à appliquer \"par défaut\"" + by_default_rules_text: > + Les règles de tag par défaut vous permettent de masquer des éléments + par défaut. Vous pouvez ensuite permettre à certains acheteurs, selon + les tags attribués, de voir ces éléments. + customer_tagged_rules: "Règles pour les acheteur avec un tag" + customer_tagged_rules_text: > + En créant une règle spécifique à un tag, vous pouvez modifier le contenu + vu par défaut (afficher ou masquer) par les acheteurs associés à ce + tag. + panels: + save: Enregistrer + saved: Enregistré + saving: En cours d'enregistrement + enterprise_package: + hub_profile: Profil Hub + hub_profile_cost: "COÛT: CONTRIBUTION LIBRE" + hub_profile_text1: > + Les visiteurs voient votre profil sur la carte, et peuvent vous contacter. + Vous augmentez ainsi votre visibilité. + hub_profile_text2: > + Créez votre profil et utilisez le réseau Open Food Network pour vous + connecter à votre système alimentaire territorial, sera toujours gratuit. + hub_shop: Boutique Hub + hub_shop_text1: > + Vous proposez des produits de différents producteurs de votre région, + artisans, ou distributeurs afin de proposer une offre complète dans + votre boutique. Vous soutenez ainsi le développement de votre système + alimentaire territorial ! + hub_shop_text2: > + Un hub n'a pas de modèle figé, il peut s'agir d'un groupement d'achat, + d'une épicerie coopérative, d'une épicerie locale de quartier ou épicerie + en circuit court en ligne, etc. + hub_shop_text3: > + Si vous produisez et voulez également vendre vos propres produits, vous + devez modifier le statut de votre entreprise, elle doit apparaitre en + tant que "producteur". + choose_package: Choisir le type de compte souhaité + choose_package_text1: > + Votre entreprise ne sera activée et visible que lorsque vous aurez choisi + le type de compte souhaité parmi les options à gauche. + choose_package_text2: > + Cliquez sur une option pour voir le détail du compte proposé, puis une + fois votre choix fait, cliquez sur le bouton rouge ENREGISTRER ! + profile_only: Profil uniquement + profile_only_cost: "COÛT: CONTRIBUTION LIBRE" + profile_only_text1: > + Gagnez en visibilité, racontez votre histoire, et affichez vos coordonnées + pour pouvoir être contactés. + profile_only_text2: > + Si vous souhaitez vous concentrer sur votre activité de production, + et laisser à d'autre le soin de distribuer vos produits, vous n'avez + pas besoin d'une boutique sur Open Food Network. + profile_only_text3: > + Saisissez votre catalogue produits sur Open Food Network, ce qui permettra + aux hubs-distributeurs utilisant la plateforme de les proposer dans + leurs boutiques (sur votre autorisation). + producer_shop: Boutique Producteur + producer_shop_text1: > + Vendez vos produits en direct aux clients finaux. via votre propre Boutique + Producteur sur Open Food Network. + producer_shop_text2: > + Une Boutique Producteur vous permet de vendre uniquement vos propres + produits. Si vous voulez vendre d'autres produits, sélectionnez "Hub + Producteur" + producer_hub: Hub Producteur + producer_hub_text1: > + Vous pouvez vendre non seulement vos produits, mais aussi des produits + d'autres producteurs de votre région, artisans, ou distributeurs afin + de proposer une offre complète dans votre boutique. Vous soutenez ainsi + le développement de votre système alimentaire territorial ! + producer_hub_text2: > + Un hub producteur peut prendre différentes formes, une boutique de vente + directe, un magasin de producteurs en ligne, un drive fermier, etc. + producer_hub_text3: > + Open Food Network soutient tous les modèles de hubs alimentaires, nous + pensons que la résilience du système viendra de la diversité des modèles. + Donc quel que soit votre modèle, nous souhaitons vous apporter les outils + de gestion donc vous avez besoin pour opérer votre circuit court. + get_listing: Référencez votre entreprise + always_free: GRATUIT + sell_produce_others: 'Vendez des produits de multiples fournisseurs ' + sell_own_produce: Vendez vos propres produits + sell_both: Vendez vos produits et ceux d'autres fournisseurs + enterprise_producer: + producer: Producteur + producer_text1: > + Un producteur fabrique de bonnes choses à boire et à manger. Vous êtes + un producteur si vous les faites pousser, les élevez, les pétrissez, + transformez, fermentez, les réduisez en grains, etc. + producer_text2: > + Un producteur peut aussi avoir d'autres rôles, comme par exemple stocker + et distribuer des produits d'autres producteurs à travers une boutique + sur Open Food Network. + non_producer: Non-producteur + non_producer_text1: > + Les entreprises qui ne produisent pas ne peuvent pas créer leur propre + catalogue produits pour les vendre sur Open Food Network. + non_producer_text2: > + Ces entreprises vont plutôt faire le lien entre des producteurs et des + clients finaux, en proposant un modèle opérationnel pour agréger, préparer + les commandes, ou encore livrer les produits. + producer_desc: 'Producteurs:' + producer_example: 'ex: maraichers, boulangers, brasseurs, artisans' + non_producer_desc: Autres entreprises de distribution alimentaire + non_producer_example: 'ex: épiceries, coopératives, groupements d''achats' + enterprise_status: + status_title: "%{name} est en place et prêt à démarrer!" + severity: Rigueur + description: Description + resolve: Résoudre + new_tag_rule_dialog: + select_rule_type: "Choisir le type de règle:" + out_of_stock: + reduced_stock_available: Stock disponible + out_of_stock_text: > + Pendant que vous faisiez vos achats, le niveau de stock disponible pour + un ou plusieurs produits dans votre panier est devenu insuffisant pour répondre + à votre demande. Voilà les modifications opérées: + now_out_of_stock: est maintenant en rupture de stock. + only_n_remainging: "plus que %{num} en stock." + variant_overrides: + inventory_products: "Produits du Catalogue " + hidden_products: "Produits Masqués" + new_products: "Nouveaux Produits" + reset_stock_levels: Réinitialiser les niveaux de stock (par défaut) + changes_to: Devient + one_override: une modification + overrides: modifications + remain_unsaved: n'a pas encore été sauvegardé. + no_changes_to_save: Aucune modification à sauvegarder.' + no_authorisation: "Nous n'avons pas pu sauvegarder ces modifications, elles ne sont donc pas enregistrées." + some_trouble: "Nous n'avons pas pu sauvegarder : %{errors}" + changing_on_hand_stock: Modification des niveaux de stock en cours... + stock_reset: Les niveaux de stock ont été réinitiatlisés (valeurs par défaut) + tag_rules: + show_hide_variants: 'Afficher ou Masquer les variantes dans ma boutique' + show_hide_shipping: 'Afficher ou Montrer les méthodes de livraison lors de la finalisation de commande' + show_hide_payment: 'Afficher ou Montrer les méthodes de paiement lors de la finalisation de commande' + show_hide_order_cycles: 'Afficher ou Masquer les cycles de vente de ma boutique' + visible: VISIBLE + not_visible: INVISIBLE + services: + unsaved_changes_message: Des modifications n'ont pas encore été sauvegardées, sauvegarder maintenant ou ignorer ? + save: SAUVEGARDER + ignore: IGNORER + add_to_order_cycle: "vendre les produits (ajouter au cycle de vente)" + manage_products: "Gérer les produits" + edit_profile: "modifier le profil" + add_products_to_inventory: "ajouter les produits au catalogue de produits" + resources: + could_not_delete_customer: 'L''acheteur n''a pas pu être supprimé' + product_import: + confirmation: | + Cette action remettra tous les niveaux de stock à zero pour cette + entreprises pour les produits non présents dans ce fichier. + order_cycles: + update_success: 'Votre cycle de vente a été mis à jour.' + no_distributors: Il n'y a pas de distributeur pour ce cycle de vente. Il ne sera pas visible aux acheteurs tant qu'il n'y aura pas de distributeur. Voulez-vous tout de même sauvegarder ce cycle de vente ? + enterprises: + producer: "Producteur" + non_producer: "Non-producteur" + customers: + select_shop: 'Veuillez d''abord choisir une boutique' + could_not_create: Oups ! Création impossible... + subscriptions: + closes: ferme + closed: fermé + close_date_not_set: Date de fin non renseignée + producers: + signup: + start_free_profile: "Commencez par créer votre profil entreprise, et changez de formule quand vous êtes prêt !" + spree: + email: Email + account_updated: "Compte mis à jour!" + my_account: "Mon compte" + date: "Date" + time: "Heure" + admin: + orders: + invoice: + issued_on: Editée le + tax_invoice: FACTURE + code: Code + from: De + to: A + form: + distribution_fields: + title: Distribution + distributor: "Distributeur : " + order_cycle: "Cycle de vente : " + overview: + order_cycles: + order_cycles: "Cycles de vente" + order_cycles_tip: "Les cycles de vente définissent quand et où vos produits peuvent être commandés par vos acheteurs." + you_have_active: + zero: "Vous n'avez aucun cycle de vente actif." + one: "Vous avez un cycle de vente actif." + other: "Vous avez %{count} cycles de vente actifs." + manage_order_cycles: "GERER LES CYCLES DE VENTE" + payment_methods: + stripe_connect: + enterprise_select_placeholder: Choisir... + loading_account_information_msg: Informations de compte en cours de chargement depuis Stripe, veuillez patienter... + stripe_disabled_msg: Les paiements via Stripe ont été désactivés par l'administrateur système. + request_failed_msg: Désolé, une erreur est survenue lors de la vérification du compte par Stripe... + account_missing_msg: Aucun compte Stripe n'existe pour cette entreprise. + connect_one: En connecter un + access_revoked_msg: L'accès à ce compte Stripe a été révoqué, veuillez reconnecter votre compte. + status: Statut + connected: Connecté + account_id: Identifiant Compte + business_name: Nom de l'entreprise + charges_enabled: Frais activés + payments: + source_forms: + stripe: + no_payment_via_admin_backend: La création de paiements via Stripe depuis le back office d'administration n'est pas possible pour le moment + products: + new: + title: 'Nouveau Produit' + unit_name_placeholder: 'ex: botte' + bulk_edit: + header: + title: Gestion du catalogue produits + indicators: + title: CHARGEMENT DES PRODUITS + no_products: "Aucun produit trouvé. Ajouter un produit ?" + no_results: "Désolé, aucun résultat trouvé" + products_head: + name: Produit/Variante + unit: Unité + display_as: Unité affichéé + category: Catégorie + tax_category: TVA applicable + inherits_properties?: Hériter des propriétés producteur? + available_on: Disponible via + av_on: "Disp. via" + import_date: "Date d'import" + products_variant: + variant_has_n_overrides: "Cette variante a été modifiée %{n} fois dans des catalogues boutiques" + new_variant: "Nouvelle variante" + product_name: Nom du Produit + primary_taxon_form: + product_category: Catégorie Produit + group_buy_form: + group_buy: "Achat groupé de lots fixes ?" + bulk_unit_size: Quantité totale du lot + display_as: + display_as: Unité affichéé + reports: + table: + select_and_search: "Sélectionner les filtres et cliquez sur RECHERCHER pour accéder aux données." + bulk_coop: + bulk_coop_supplier_report: 'Achats groupés - Totaux par Producteur' + bulk_coop_allocation: 'Achats groupés - Allocation' + bulk_coop_packing_sheets: 'Achats groupés - Feuilles de préparation des paniers' + bulk_coop_customer_payments: 'Achats groupés - Paiement des acheteurs' + shared: + configuration_menu: + stripe_connect: Stripe Connect + variants: + autocomplete: + producer_name: Producteur + checkout: + payment: + stripe: + choose_one: En choisir un + enter_new_card: Entrer les informations pour la nouvelle carte + used_saved_card: "Utiliser une carte sauvegardée :" + or_enter_new_card: "Ou entrez les informations pour utiliser une nouvelle carte :" + remember_this_card: Se souvenir de cette carte ? + date_picker: + format: '%Y-%m-%d' + js_format: 'yy-mm-dd' + inventory: Catalogue de produits + orders: + bought: + item: "Déjà commandé dans ce cycle de vente" + order_mailer: + invoice_email: + hi: "Bonjour %{name}" + invoice_attached_text: 'Veuillez trouver ci-joint la facture pour votre récente commande auprès de ' + order_state: + address: adresse + adjustments: ajustements + awaiting_return: attente du retour + canceled: annulé + cart: panier + complete: terminer + confirm: confirmer + delivery: livraison + paused: mis en pause + payment: paiement + pending: en attente + resumed: recommencé + returned: retourné + skrill: cash + subscription_state: + active: actif + pending: en attente + ended: terminé + paused: mis en pause + canceled: annulé + payment_states: + balance_due: solde dû + completed: effectué + checkout: passer commande + credit_owed: crédit acheteur + failed: échec + paid: payé + pending: en attente + processing: en traitement + void: faire un avoir + invalid: invalide + shipment_states: + backorder: réapprovisionnement + partial: partiel + pending: en attente + ready: prêt + shipped: envoyé + user_mailer: + reset_password_instructions: + request_sent_text: | + Votre demande de nouveau mot de passe a bien été prise en compte. + Si vous n'avez pas demandé de nouveau mot de passe, veuillez ignorer cet e-mail. + link_text: > + Si vous êtes bien à l'origine de cette demande, veuillez cliquer sur le + lien ci-dessous : + issue_text: | + Si le lien ne fonctionne pas, essayez de le copier - coller dans la barre d'adresse de votre navigateur. + Si le problème persiste, n'hésitez pas à nous contacter. + confirmation_instructions: + subject: Veuillez confirmer votre compte + weight: Poids (au kg) + zipcode: Code postal + users: + form: + account_settings: Paramètres du Compte + show: + tabs: + orders: Commandes + cards: Cartes bancaires + transactions: Achats + settings: Paramètres du Compte + unconfirmed_email: "Attente de validation pour l'email: %{unconfirmed_email}. Votre adresse email sera mise à jour quand le nouvel email aura été confirmé." + orders: + open_orders: Commandes Ouvertes + past_orders: Commandes Passées + transactions: + transaction_history: Historique des Transactions + open_orders: + order: Commander + shop: Faire mes courses + changes_allowed_until: Modifications permises jusqu'à + items: Pièce + total: Total + edit: Modifier + cancel: Annuler + closed: Fermée + until: Jusqu'à + past_orders: + order: Commande + shop: Boutique + completed_at: 'Passée à :' + items: Produits + total: Total + paid?: Payé ? + view: Afficher + saved_cards: + default?: Par default? + delete?: Effacer? + localized_number: + invalid_format: n'est pas un format valide. Veuillez entrer un nombre. diff --git a/config/locales/it.yml b/config/locales/it.yml index fbef1581d7..fe81ff7f31 100644 --- a/config/locales/it.yml +++ b/config/locales/it.yml @@ -125,6 +125,8 @@ it: cache_settings: show: error: Eorrore + products: + Search: Cerca variant_overrides: index: title: Inventario @@ -363,7 +365,6 @@ it: email_admin_html: "Puoi gestire il tuo profilo facendo il log in al link %{link} o cliccando sull'ingranaggio in alto a destra della homepage, e selezionando Amministrazione" email_community_html: "Abbiamo anche un forum on-line per le discussioni della comunità sul software OFN e le sfide uniche legate all'avere un'impresa del cibo. Sei invitato ad unirti. Ci evolviamo in continuo e il tuo contributo in questo forum plasmerà ciò che sarà. %{link}" join_community: "Unisciti alla community" - email_help: "Se hai difficoltà, controlla le nostre FAQ, naviga nel forum o crea una discussione di 'Supporto' e qualcuno ti aiuterà!" email_confirmation_greeting: "Ciao, %{contact}!" email_confirmation_profile_created: "Un profilo per %{name} è stato creato con successo! Per attivare il tuo Profilo abbiamo bisogno di confermare questo indirizzo email." email_confirmation_click_link: "Per favore clicca il link di seguito per confermare la tua email e continuare l'impostazione del tuo profilo" @@ -914,7 +915,6 @@ it: products_unsaved: "Modifiche a %{n} prodotti rimangono non salvate." is_already_manager: "è già un gestore!" no_change_to_save: "Nessuna modifica da salvare" - add_manager: "Aggiungi un gestore" users: "Utenti" about: "About" images: "Immagini" diff --git a/config/locales/nb.yml b/config/locales/nb.yml index cbd91b72fd..ae77ae6d18 100644 --- a/config/locales/nb.yml +++ b/config/locales/nb.yml @@ -8,6 +8,10 @@ nb: completed_at: Fullført på number: Nummer email: Epost Kunde + spree/payment: + amount: Beløp + order_cycle: + orders_close_at: Lukkedato errors: models: spree/user: @@ -16,6 +20,10 @@ nb: taken: "Det finnes allerede en konto for denne eposten. Vennligst logg inn eller tilbakestill passordet ditt." spree/order: no_card: Det er ingen gyldige kredittkort tilgjengelig + order_cycle: + attributes: + orders_close_at: + after_orders_open_at: må være etter åpningsdato activemodel: errors: models: @@ -53,11 +61,17 @@ nb: Ugyldig epost eller passord. Var du gjest forrige gang? Kanskje du må opprette en konto eller nullstille passordet. unconfirmed: "Du må bekrefte kontoen din før du fortsetter." + already_registered: "Denne epostadressen er allerede registrert. Vennligst logg inn for å fortsette, eller gå tilbake og bruk en annen epostadresse." + user_passwords: + spree_user: + updated_not_active: "Ditt passord har blitt tilbakestilt, men epostadressen din er ikke bekreftet enda." enterprise_mailer: confirmation_instructions: subject: "Vennligst bekreft epostadressen til %{enterprise}" welcome: subject: "%{enterprise} er nå på %{sitename}" + invite_manager: + subject: "%{enterprise} har invitert deg til å være en administrator" producer_mailer: order_cycle: subject: "Bestillingsrunderapport for %{producer}" @@ -200,6 +214,7 @@ nb: phone: Telefon price: Pris producer: Produsent + image: Bilde product: Produkt quantity: Mengde schedule: Tidsplan @@ -222,6 +237,9 @@ nb: form_invalid: "Skjemaet inneholder manglende eller ugyldige felt" clear_filters: Fjern filtre clear: Fjern + save: Lagre + cancel: Avbryt + back: Tilbake show_more: Vis mer show_n_more: Vis %{num} flere choose: "Velg..." @@ -353,6 +371,21 @@ nb: manages: administrerer products: unit_name_placeholder: 'f.eks. bunter' + index: + unit: Enhet + display_as: Vis som + category: Kategori + tax_category: Avgiftskategori + inherits_properties?: Arver egenskaper? + available_on: Tilgjengelig på + av_on: "Tilgj. På" + import_date: Importert + upload_an_image: Last opp et bilde + product_search_keywords: Nøkkelord for produktsøk + product_search_tip: Skriv ord for å søke etter dine produkter i butikkene. Bruk mellomrom for å skille mellom hvert nøkkelord. + SEO_keywords: SEO Nøkkelord + seo_tip: Skriv ord for å søke etter produktene dine på nettet. Bruk mellomrom for å skille mellom hvert søkeord. + Search: Søk properties: property_name: Navn på egenskap inherited_property: Arvet egenskap @@ -360,8 +393,80 @@ nb: to_order_tip: "Varer laget for bestilling har ikke et lagernivå, slik som ferske skiver brød laget for bestilling." product_distributions: "Produktdistribusjoner" group_buy_options: "Gruppekjøpsalternativer" - seo: "SEO" back_to_products_list: "Tilbake til produktlisten" + product_import: + title: Produktimport + file_not_found: Filen ble ikke funnet eller kunne ikke åpnes + no_data: Ingen data funnet i regnearket + confirm_reset: "Dette vil stille lagernivå til null på alle produkter for denne\n bedriften som ikke er til stede i den opplastede filen" + model: + no_file: "feil: ingen fil lastet opp" + could_not_process: "kunne ikke behandle filen: ugyldig filtype" + incorrect_value: feil verdi + conditional_blank: kan ikke være tom hvis unit_type er tom + no_product: samsvarte ikke med noen produkter i databasen + not_found: ikke funnet i databasen + blank: kan ikke være tomt + products_no_permission: du har ikke tillatelse til å administrere produkter for denne bedriften + inventory_no_permission: du har ikke tillatelse til å opprette lager for denne produsenten + none_saved: kunne ikke lagre noen produkter + line: Linje + index: + select_file: Velg et regneark for å laste opp + spreadsheet: Regneark + import_into: "Importer inn i:" + product_list: Produktliste + inventories: Varelagre + import: Import + upload: Last opp + import: + review: Anmeldelse + proceed: Fortsett + save: Lagre + results: Resultater + save_imported: Lagre importerte produkter + no_valid_entries: Ingen gyldige oppføringer funnet + none_to_save: Det er ingen oppføringer som kan lagres + some_invalid_entries: Importert fil inneholder noen ugyldige oppføringer + save_valid?: Lagre gyldige oppføringer for nå og forkast de andre? + no_errors: Ingen feil oppdaget! + save_all_imported?: Lagre alle importerte produkter? + options_and_defaults: Importalternativer og standardinnstillinger + no_permission: du har ikke tillatelse til å administrere denne bedriften + not_found: bedriften kunne ikke bli funnet i databasen + no_name: Ingen navn + blank_supplier: noen produkter har tomt leverandørnavn + reset_absent?: Tilbakestill fraværende produkter? + overwrite_all: Overskrive alt + overwrite_empty: Overskriv hvis tomt + default_stock: Sett lagernivå + default_tax_cat: Angi avgiftskategori + default_shipping_cat: Angi fraktkategori + default_available_date: Angi tilgjengelig dato + validation_overview: Importvalidering oversikt + entries_found: Oppføringer funnet i importert fil + entries_with_errors: Elementene inneholder feil og vil ikke bli importert + products_to_create: Produkter vil bli opprettet + products_to_update: Produktene vil bli oppdatert + inventory_to_create: Lagerelementer vil bli opprettet + inventory_to_update: Lagerelementer blir oppdatert + products_to_reset: Eksisterende produkter vil få lager satt til null + inventory_to_reset: Eksisterende vareobjekter vil få lager satt til null + line: Linje + item_line: Artikkellinje + save: + final_results: Importer endelige resultater + products_created: Produkter opprettet + products_updated: Produkter oppdatert + inventory_created: Lagerelementer opprettet + inventory_updated: Lagerelementer oppdatert + products_reset: Produktene hadde lagernivå satt til null + inventory_reset: Lagerelementer hadde lagernivå satt til null + all_saved: "Alle elementer lagret vellykket" + some_saved: "elementer lagret vellykket" + save_errors: Lagre feil + view_products: Se produkter + view_inventory: Se beholdning variant_overrides: loading_flash: loading_inventory: LASTER VARELAGER @@ -372,6 +477,7 @@ nb: inherit?: Arve? add: Legg til hide: Skjul + import_date: Importert select_a_shop: Velg en butikk review_now: Sjekk nå new_products_alert_message: Det er %{new_product_count} nye produkter tilgjengelig for å legge til ditt varelager. @@ -412,6 +518,7 @@ nb: max_fulfilled_units: "Max Oppfylte Enheter" order_error: "Noen feil må løses før du kan oppdatere bestillinger.\nAlle felt med røde kanter inneholder feil." variants_without_unit_value: "ADVARSEL: Noen varianter mangler enhetsverdi" + select_variant: "Velg en variant" enterprise: select_outgoing_oc_products_from: Velg utgående bestillingsrundeprodukter fra enterprises: @@ -571,6 +678,9 @@ nb: notifications_note: 'Merk: En ny epostadresse må kanskje bekreftes før bruk' managers: Administratorer managers_tip: Andre brukere med tilgang til å administrere denne bedriften. + invite_manager: "Inviter Administrator" + invite_manager_tip: "Inviter en uregistrert bruker til å registrere seg og bli administrator av denne bedriften." + add_unregistered_user: "Legg til en uregistrert bruker" email_confirmed: "Epost bekreftet" email_not_confirmed: "Epost ikke bekreftet" actions: @@ -629,7 +739,10 @@ nb: welcome_title: Velkommen til Open Food Network! welcome_text: Du har opprettet en next_step: Neste steg - choose_starting_point: 'Velg ditt startpunkt:' + choose_starting_point: 'Velg din pakke:' + invite_manager: + user_already_exists: "Brukeren eksisterer allerede" + error: "Noe gikk galt" order_cycles: edit: advanced_settings: Avanserte Innstillinger @@ -686,7 +799,7 @@ nb: name: Navn orders_open: Bestillinger åpner coordinator: Koordinator - order_closes: Bestillinger stenger + orders_close: Bestillinger stenger row: suppliers: 'leverandører ' distributors: distributører @@ -701,6 +814,8 @@ nb: destroy_errors: orders_present: Denne bestillingsrunden er valgt av en kunde og kan ikke slettes. For å hindre at kundene får tilgang til det, må du lukke den i stedet. schedule_present: Denne bestillingsrunden er knyttet til en tidsplan og kan ikke slettes. Vennligst fjern koblingen eller slett tidsplanen først. + bulk_update: + no_data: Hm, noe gikk galt. Ingen data for bestillingsrunde funnet. producer_properties: index: title: Produsentegenskaper @@ -842,6 +957,9 @@ nb: no_subscriptions: Ingen abonnement ennå... why_dont_you_add_one: Hvorfor legger du ikke til en? :) no_matching_subscriptions: Ingen tilsvarende abonnement funnet + schedules: + destroy: + associated_subscriptions_error: Denne tidsplanen kan ikke slettes fordi den har tilknyttede abonnementer stripe_connect_settings: edit: title: "Stripe Connect" @@ -890,6 +1008,7 @@ nb: require_customer_login: "Denne butikken er kun for kunder." require_login_html: "Vennligst %{login} hvis du allerede har en konto. Hvis ikke, %{register} for å bli kunde." require_customer_html: "Vennligst %{contact} %{enterprise} for å bli kunde." + card_could_not_be_updated: Kortet kunne ikke oppdateres card_could_not_be_saved: kort kunne ikke lagres spree_gateway_error_flash_for_checkout: "Det oppstod et problem med betalingsinformasjonen din: %{error}" invoice_billing_address: "Fakturaadresse:" @@ -1026,6 +1145,7 @@ nb: footer_legal_tos: "Vilkår og betingelser" footer_legal_visit: "Finn oss på" footer_legal_text_html: "Open Food Network er en plattform med fri og åpen kildekode. Vårt innhold er lisensiert med %{content_license} og vår kode med %{code_license}." + footer_skylight_dashboard_html: Ytelsesdata er tilgjengelig på %{dashboard}. home_shop: Handle nå brandstory_headline: "Food, unincorporated." brandstory_intro: "Noen ganger er det best å fikse systemet ved å starte et nytt..." @@ -1109,7 +1229,6 @@ nb: email_admin_html: "Du kan administrere din konto ved å logge inn på %{link} eller ved klikke på tannhjulet øverst til høyre på hjemmesiden og velge Administrasjon." email_community_html: "Vi har også et online forum for diskusjon relatert til OFN programvaren og de forskjellige utfordringene med å drive et matfirma. Vi oppfordrer deg til å bli med. Vi utvikler oss hele tiden og dine innspill til dette forumet vil forme det som skjer videre. %{link}" join_community: "Bli med" - email_help: "Hvis du har problemer, sjekk vår FAQ, utforsk forumet eller skriv et 'Support'-emne og noen vil hjelpe deg!" email_confirmation_activate_account: "Før vi kan aktivere den nye kontoen din, må vi bekrefte epostadressen din." email_confirmation_greeting: "Hei, %{contact}!" email_confirmation_profile_created: "En profil for %{name} har blitt opprettet! For å aktivere din profil må du bekrefte denne epostadressen." @@ -1168,6 +1287,12 @@ nb: email_signup_shop_html: "Du kan nå logge inn på %{link}." email_signup_text: "Takk for at du ble med i nettverket. Hvis du er kunde ser vi frem til å vise deg mange fantastiske bønder, flotte mathubs og deilig mat! Hvis du er produsent eller selskap er vi glade for å ha deg som en del av nettverket." email_signup_help_html: "Vi tar imot alle dine spørsmål og tilbakemeldinger; du kan bruke Send tilbakemelding på nettside eller email oss på %{email}" + invite_email: + greeting: "Hallo!" + invited_to_manage: "Du har blitt invitert til å administrere %{enterprise} på %{instance}." + confirm_your_email: "Du burde ha mottatt eller vil snart motta en epost med en bekreftelseslenke. Du vil ikke kunne få tilgang til %{enterprise}s profil før du har bekreftet din epost." + set_a_password: "Du blir da bedt om å angi et passord før du kan administrere bedriften." + mistakenly_sent: "Ikke sikker på hvorfor du har mottatt denne e-posten? Kontakt %{owner_email} for mer informasjon." producer_mail_greeting: "Kjære" producer_mail_text_before: "Alle dine kunderbestillinger er klar." producer_mail_order_text: "Her er en oppsummering av bestillingene:" @@ -1416,6 +1541,7 @@ nb: november: "november" december: "desember" email_not_found: "epostadresse ikke funnet" + email_unconfirmed: "Du må bekrefte epostadressen din før du kan tilbakestille passordet ditt." email_required: "Du må oppgi en epostadresse" logging_in: "Et øyeblikk, vi logger deg inn" signup_email: "Din epost" @@ -1430,6 +1556,7 @@ nb: password_reset_sent: "En epost med instruksjoner om å nullstille passordet har blitt sendt!" reset_password: "Tilbakestill passord" who_is_managing_enterprise: "Hvem er ansvarlig for å administrere %{enterprise}?" + update_and_recalculate_fees: "Oppdater og regn avgifter på nytt" enterprise: registration: modal: @@ -1666,6 +1793,10 @@ nb: calculator: "Kalkulator" calculator_values: "Kalkulatorverdier" flat_percent_per_item: "Flat prosent (per stk)" + flat_rate_per_item: "Flat rate (per vare)" + flat_rate_per_order: "Flat Rate (per bestilling)" + flexible_rate: "Fleksibel Rate" + price_sack: "Prissekk" new_order_cycles: "Nye Bestillingsrunder" new_order_cycle: "Ny bestillingsrunde" select_a_coordinator_for_your_order_cycle: "Velg en koordinator for bestillingsrunde" @@ -1700,12 +1831,7 @@ nb: spree_admin_enterprises_fees: "Bedriftsavgift" spree_admin_enterprises_none_create_a_new_enterprise: "OPPRETT NY BEDRIFT" spree_admin_enterprises_none_text: "Du har ingen bedrifter ennå" - spree_admin_enterprises_producers_name: "Navn" - spree_admin_enterprises_producers_total_products: "Alle Produkter" - spree_admin_enterprises_producers_active_products: "Aktive Produkter" - spree_admin_enterprises_producers_order_cycles: "Produkter i Bestillingsrunder" spree_admin_enterprises_tabs_hubs: "HUBS" - spree_admin_enterprises_tabs_producers: "PRODUSENTER" spree_admin_enterprises_producers_manage_products: "ADMINISTRER PRODUKTER" spree_admin_enterprises_any_active_products_text: "Du har ingen aktive produkter." spree_admin_enterprises_create_new_product: "OPPRETT NYTT PRODUKT" @@ -1941,7 +2067,8 @@ nb: products_unsaved: "Endringer i %{n} produkter er fortsatt ulagret." is_already_manager: "er allerede administrator!" no_change_to_save: "Ingen endring å lagre" - add_manager: "Legg til administrator" + user_invited: "%{email} har blitt invitert til å administrere denne bedriften" + add_manager: "Legg til en eksisterende bruker" users: "Brukere" about: "Om" images: "Bilder" @@ -1952,6 +2079,7 @@ nb: social: "Sosial" business_details: "Forretningsdetaljer" properties: "Egenskaper" + shipping: "Levering" shipping_methods: "Leveringsmetoder" payment_methods: "Betalingsmetoder" payment_method_fee: "Transaksjongebyr" @@ -1968,7 +2096,7 @@ nb: content_configuration_pricing_table: "(TODO: Pristabell)" content_configuration_case_studies: "(TODO: Casestudier)" content_configuration_detail: "(TODO: Detalj)" - enterprise_name_error: "har allerede blitt tatt. Hvis dette er din bedrift og du ønsker å kreve eierskap, vennligst kontakt gjeldende leder av denne profilen på %{email}." + enterprise_name_error: "har allerede blitt tatt. Hvis dette er din bedrift og du ønsker å kreve eierskap, eller hvis du ønsker å handle med denne bedriften, vennligst kontakt gjeldende administrator av denne profilen på %{email}." enterprise_owner_error: "^ %{email} kan ikke eie flere bedrifter (grense er %{enterprise_limit})." enterprise_role_uniqueness_error: "^Den rollen finnes allerede." inventory_item_visibility_error: må være true eller false @@ -2002,6 +2130,14 @@ nb: order_cycles_email_to_producers_notice: 'E-poster som skal sendes til produsentene har blitt satt i kø for sending.' order_cycles_no_permission_to_coordinate_error: "Ingen av bedriftene dine har tillatelse til å koordinere en bestillingsrunde" order_cycles_no_permission_to_create_error: "Du har ikke rettigheter til å opprette en bestillingsrunde som er koordinert av den bedriften" + back_to_orders_list: "Tilbake til bestillingslisten" + no_orders_found: "Ingen bestillinger funnet" + order_information: "Bestillingsinformasjon" + date_completed: "Dato Fullført" + amount: "Beløp" + state_names: + ready: Klar + pending: I påvente av js: saving: 'Lagrer...' changes_saved: 'Endringene er lagret.' @@ -2240,7 +2376,7 @@ nb: new: title: 'Nytt produkt' unit_name_placeholder: 'f.eks. bunter' - bulk_edit: + index: header: title: Endre produkter i bulk indicators: diff --git a/config/locales/pt-BR.yml b/config/locales/pt-BR.yml index 37a0c05dd4..54cb35e468 100644 --- a/config/locales/pt-BR.yml +++ b/config/locales/pt-BR.yml @@ -141,7 +141,7 @@ pt-BR: update_address: 'Atualizar Endereço' confirm_delete: 'Certeza que quer excluir?' products: - bulk_edit: + index: unit: Unidade display_as: Mostrar Como category: Categoria diff --git a/config/locales/pt.yml b/config/locales/pt.yml index 25603b30ab..9dea0d380d 100644 --- a/config/locales/pt.yml +++ b/config/locales/pt.yml @@ -3,21 +3,27 @@ pt: activerecord: attributes: spree/order: - payment_state: Estado do pagamento + payment_state: Estado do Pagamento shipment_state: Estado do envio - completed_at: Completado em + completed_at: Concluído Em number: Número - email: Email do/a consumidor/a + email: Email do/a Consumidor/a spree/payment: amount: Quantia + order_cycle: + orders_close_at: Data de fecho errors: models: spree/user: attributes: email: - taken: "Já existe uma conta associada a este email. Por favor faça login ou defina uma nova senha." + taken: "Já existe uma conta associada a este email. Por favor faça login ou defina uma nova palavra-passe." spree/order: - no_card: Não estão disponíveis cartões de crédito válidos + no_card: Não há cartões de crédito válidos disponíveis + order_cycle: + attributes: + orders_close_at: + after_orders_open_at: tem de ser após data de abertura activemodel: errors: models: @@ -25,7 +31,7 @@ pt: attributes: subscription_line_items: at_least_one_product: "^Por favor adicione pelo menos um produto" - not_available: "^%{name} não está disponível no calendário selecionado" + not_available: "^%{name} não está disponível no horário selecionado" ends_at: after_begins_at: "tem de ser depois de começar às" customer: @@ -33,53 +39,57 @@ pt: schedule: not_coordinated_by_shop: "não é coordenado por %{shop}" payment_method: - not_available_to_shop: "não está disponível para a %{shop}" + not_available_to_shop: "não está disponível para %{shop}" invalid_type: "tem de ser em dinheiro ou método Stripe" shipping_method: - not_available_to_shop: "não está disponível para a %{shop}" + not_available_to_shop: "não está disponível para %{shop}" credit_card: not_available: "não está disponível" blank: "é obrigatório" devise: confirmations: - send_instructions: "Receberá um email com instruções sobre como confirmar a sua conta em alguns minutos." - failed_to_send: "Ocorreu um erro no envio do email de confirmação" + send_instructions: "Daqui a uns minutos irá receber um email com instruções sobre como confirmar a sua conta." + failed_to_send: "Ocorreu um erro enquanto enviávamos o seu email de confirmação" resend_confirmation_email: "Reenviar email de confirmação." confirmed: "Obrigada por confirmar o seu email! Agora já pode fazer login." - not_confirmed: "O seu email não pôde ser confirmado. Talvez já tenha completado este passo?" + not_confirmed: "O seu email não pôde ser confirmado. Talvez já tenha concluído este passo?" user_registrations: spree_user: signed_up_but_unconfirmed: "Foi enviada uma mensagem para o seu endereço de email com um link de confirmação. Por favor clique nesse link para activar a sua conta." failure: invalid: | - Email ou senha incorrecto. - Entrou como visitante da última vez? Talvez precise de criar uma conta ou de redefinir a sua senha. + Email ou palavra-passe incorrectos. + Entrou como visitante da última vez? Talvez precise de criar uma conta ou de redefinir a sua palavra-passe. unconfirmed: "Tem de confirmar a sua conta antes de continuar." + already_registered: "Este endereço de email já está registado. Por favor faça login para continuar, ou volte atrás e use outro endereço de email." + user_passwords: + spree_user: + updated_not_active: "A sua palavra-passe foi redefinida, mas o seu email ainda não foi confirmado." enterprise_mailer: confirmation_instructions: - subject: "Por favor confirme o endereço de email da %{enterprise}" + subject: "Por favor confirme o endereço de email de %{enterprise}" welcome: subject: "%{enterprise} está agora em %{sitename}" invite_manager: - subject: "%{enterprise} enviou-lhe um convite para ser gestora/a" + subject: "%{enterprise} convidou-o/a para gestor/a" producer_mailer: order_cycle: subject: "Relatório de ciclo de encomendas de %{producer}" subscription_mailer: placement_summary_email: - subject: Um resumo das encomendas de subscrição recentes + subject: Um resumo das encomendas de subscrição feitas recentemente greeting: "Olá %{name}," - intro: "Abaixo está um resumo das encomendas de subscrição que acabam de ser feitas na %{shop}." + intro: "Abaixo está um resumo das encomendas de subscrição que acabam de ser feitas em %{shop}." confirmation_summary_email: subject: Um resumo das encomendas de subscrição confirmadas recentemente greeting: "Olá %{name}," - intro: "Abaixo está um resumo das encomendas de subscrição que acabam de ser finalizadas na %{shop}." + intro: "Abaixo está um resumo das encomendas de subscrição que acabam de ser finalizadas em %{shop}." summary_overview: total: 'Um total de %{count} subscrições foram marcadas para processamento automático. ' success_zero: Destas, nenhuma foi processada com sucesso. success_some: 'Destas, %{count} foram processadas com sucesso. ' success_all: Todas foram processadas com sucesso. - issues: Detalhes dos problemas encontrados estão listados abaixo. + issues: Abaixo estão listados os detalhes dos problemas encontrados. summary_detail: no_message_provided: Não foi fornecida nenhuma mensagem de erro changes: @@ -87,10 +97,10 @@ pt: explainer: Estas encomendas foram processadas mas não existe stock suficiente para alguns dos itens requisitados empty: title: Sem Stock (%{count} encomendas) - explainer: Não foi possível processar estas encomendas porque não existe stock disponível de nenhum dos produtos requisitados + explainer: Não foi possível processar estas encomendas porque não existe stock disponível para nenhum dos produtos requisitados complete: - title: Já foi processada (%{count} encomendas) - explainer: Estas encomendas já foram marcadas como completas, e portanto foram deixadas como estão + title: Já foram processadas (%{count} encomendas) + explainer: Estas encomendas já foram marcadas como concluídas, e portanto deixámo-las como estão processing: title: Encontrado Erro (%{count} encomendas) explainer: O processamento automático destas encomendas falhou devido a um erro. O erro foi listado quando possível. @@ -99,42 +109,42 @@ pt: explainer: O processamento automático do pagamento destas encomendas falhou devido a um erro. O erro foi listado quando possível. other: title: Outra Falha (%{count} encomendas) - explainer: 'O processamento automático destas encomendas falhou devido a razão desconhecida. Isto não deveria estar a acontecer, por favor contacte-nos se estiver a ver esta mensagem. ' + explainer: 'O processamento automático destas encomendas falhou devido a uma razão desconhecida. Isto não deveria estar a acontecer, por favor contacte-nos se estiver a ver esta mensagem. ' home: "OFN" title: Open Food Network - welcome_to: 'Bem vindo a' + welcome_to: 'Bem-vindo à' site_meta_description: "Começamos a partir da terra. Com agricultores e produtores prontos a contarem as suas histórias com um brilho nos olhos. Com distribuidores prontos a estabelecerem ligações entre pessoas e produtos de forma justa e honesta. Com consumidores que acreditam que melhores decisões no momento da compra..." - search_by_name: Procurar por nome ou localidade - producers_join: Produtores e produtoras nacionais, estão convidados a juntarem-se à Open Food Network! - charges_sales_tax: Cobra GST? + search_by_name: Procurar por nome ou localidade... + producers_join: Produtores e produtoras de proximidade, estão convidados a juntarem-se à Open Food Network! + charges_sales_tax: Cobra IVA? print_invoice: "Imprimir factura" - print_ticket: "Imprimir bilhetes" + print_ticket: "Imprimir bilhete" select_ticket_printer: "Seleccionar impressora" send_invoice: "Enviar factura" resend_confirmation: "Reenviar confirmação" - view_order: "Ver encomenda" - edit_order: "Editar encomenda" - ship_order: "Enviar encomenda" - cancel_order: "Cancelar encomenda" - confirm_send_invoice: "Vai ser enviada ao cliente uma factura desta encomenda. Tem a certeza que deseja continuar?" + view_order: "Ver Encomenda" + edit_order: "Editar Encomenda" + ship_order: "Enviar Encomenda" + cancel_order: "Cancelar Encomenda" + confirm_send_invoice: "Vai ser enviada uma factura desta encomenda ao consumidor. Tem a certeza que deseja continuar?" confirm_resend_order_confirmation: "Tem a certeza que deseja enviar novamente o email de confirmação de encomenda? " - must_have_valid_business_number: "%{enterprise_name}tem de ter um ABN válido antes de se poder enviar facturas." + must_have_valid_business_number: "%{enterprise_name}tem de ter um NIPC/NIF válido antes de se poder enviar facturas." invoice: "Factura" percentage_of_sales: "%{percentage} de vendas" capped_at_cap: "limitado em %{cap}" per_month: "por mês" - free: "grátis" - free_trial: "faça um teste gratuitamente" - plus_tax: "mais taxas" + free: "gratuito" + free_trial: "experimentar sem pagar" + plus_tax: "mais IVA" min_bill_turnover_desc: "assim que o volume de negócios exceder %{mbt_amount}" say_no: "Não" say_yes: "Sim" then: então ongoing: A decorrer - bill_address: Endereço de Facturação - ship_address: Endereço de Envio - sort_order_cycles_on_shopfront_by: "Ordenar Ciclos de Encomendas no Mercado Por" - required_fields: Os campos obrigatórios são indicados com um asterisco + bill_address: Morada de Faturação + ship_address: Morada de Envio + sort_order_cycles_on_shopfront_by: "Ordenar Ciclos de Encomendas na Loja Por" + required_fields: Os campos obrigatórios estão indicados com um asterisco select_continue: Seleccionar e continuar remove: Remover or: ou @@ -143,13 +153,13 @@ pt: loading: A carregar... show_more: Mostrar mais show_all: Mostrar tudo - show_all_with_more: "Mostrar tudo (%{num} Mais)" + show_all_with_more: "Mostrar tudo (Mais %{num})" cancel: Cancelar edit: Editar clone: Clonar distributors: Distribuidores distribution: Distribuição - bulk_order_management: Gestão de encomendas por atacado + bulk_order_management: Gestão de Encomendas por Atacado enterprise_groups: Grupos reports: Relatórios variant_overrides: Inventário @@ -173,7 +183,7 @@ pt: 'no': "Não" y: 'S' n: 'N' - powered_by: Distribuído por + powered_by: Impulsionado por blocked_cookies_alert: "O seu browser pode estar a bloquear cookies necessárias para o correcto funcionamento deste mercado. Clique abaixo para autorizar cookies e recarregue a página." allow_cookies: "Autorizar cookies" notes: Notas @@ -183,13 +193,13 @@ pt: filter_results: Filtrar resultados quantity: Quantidade pick_up: Levantamento - copy: Cópia + copy: Copiar actions: create_and_add_another: "Criar e acrescentar outro" admin: begins_at: Começa às begins_on: Começa em - customer: Cliente + customer: Consumidor/a date: Data email: Email ends_at: 'Termina às ' @@ -198,12 +208,12 @@ pt: on_hand: Disponível on_demand: Sob Encomenda on_demand?: Sob Encomenda? - order_cycle: Ciclo de encomendas + order_cycle: Ciclo de Encomendas payment: Pagamento payment_method: Método de Pagamento phone: Telefone price: Preço - producer: Produtor + producer: Produtor/a image: Imagem product: Produto quantity: Quantidade @@ -212,41 +222,44 @@ pt: shipping_method: Método de Envio shop: Loja sku: SKU - status_state: Estado + status_state: Região tags: Etiquetas variant: Variante weight: Peso - volume: Tamanho + volume: Volume items: Itens - select_all: Selecionar tudo + select_all: Seleccionar tudo obsolete_master: Mestre obsoleto quick_search: Pesquisa Rápida clear_all: Limpar Tudo start_date: "Data de Início" - end_date: "Data de Término" + end_date: "Data de Fim" form_invalid: "O formulário contém campos incompletos ou inválidos" clear_filters: Limpar filtros clear: Limpar + save: Guardar + cancel: Cancelar + back: Voltar show_more: Mostrar mais show_n_more: Mostrar mais %{num} choose: "Escolher..." - please_select: Por favor selecione... + please_select: Por favor seleccione... columns: Colunas actions: Acções viewing: "Visualizando: %{current_view_name}" description: Descrição whats_this: O que é isto? tag_has_rules: "Regras existentes para esta etiqueta: %{num}" - has_one_rule: "possui uma regra" + has_one_rule: "tem uma regra" has_n_rules: "tem %{num} regras" unsaved_confirm_leave: "Existem alterações por guardar nesta página. Continuar sem guardar? " - unsaved_changes: "Algumas alterações não foram guardadas" + unsaved_changes: "Tem alterações por guardar" accounts_and_billing_settings: method_settings: default_accounts_payment_method: "Método de pagamento da conta por defeito" default_accounts_shipping_method: "Método de envio da conta por defeito" edit: - accounts_and_billing: "Contas e facturação" + accounts_and_billing: "Contabilidade & Facturação" accounts_administration_distributor: "Administração de contas de distribuidor" admin_settings: "Configurações" update_invoice: "Actualizar facturas" @@ -263,25 +276,25 @@ pt: default_payment_method: deve estar definido se desejar criar facturas para organizações default_shipping_method: deve estar definido se desejar criar facturas para organizações registadas shopfront_settings: - embedded_shopfront_settings: "Definições de loja incorporadas" - enable_embedded_shopfronts: "Permitir lojas incorporadas" - embedded_shopfronts_whitelist: "Whitelist de domínios externos" + embedded_shopfront_settings: "Definições de Loja Embutida" + enable_embedded_shopfronts: "Permitir Lojas Embutidas" + embedded_shopfronts_whitelist: "Whitelist de Domínios Externos" number_localization: number_localization_settings: "Definições de Localização de Números" enable_localized_number: "Utilizar a lógica do separador de milhares/décimas internacional" business_model_configuration: edit: - business_model_configuration: "Modelo de negócio" - business_model_configuration_tip: "Configure a taxa de quais lojas serão cobradas mensalmente por usar o Open Food Network" - bill_calculation_settings: "Configurações de cálculo de conta" + business_model_configuration: "Modelo de Negócio" + business_model_configuration_tip: "Configure a taxa a que as lojas serão cobradas mensalmente por usar o Open Food Network" + bill_calculation_settings: "Configurações de Cálculo de Conta" bill_calculation_settings_tip: "Configure a quantia que será cobrada mensalmente às organizações pela utilização da OFN" shop_trial_length: "Duração da Loja Experimental (Dias)" shop_trial_length_tip: "Duração (em número de dias) do período experimental para as organizações definidas como lojas." - fixed_monthly_charge: "Taxa mensal fixa" + fixed_monthly_charge: "Taxa Mensal Fixa" fixed_monthly_charge_tip: "Uma taxa mensal fixa para todas as organizações definidas como loja e que tenham excedido o volume de negócios mínimo facturável (se definido)." percentage_of_turnover: "Percentagem do volume de negócios" percentage_of_turnover_tip: "Quando maior que zero, esta taxa (0,0 - 1,0) será aplicada ao volume de negócios total de cada loja e somada a qualquer taxa fixa (à esquerda) para calcular a conta mensal." - monthly_cap_excl_tax: "tecto mensal (excluindo GST)" + monthly_cap_excl_tax: "tecto mensal (excluindo IVA)" monthly_cap_excl_tax_tip: "Quando maior do que zero, este valor será usado como o limite na quantia cobrada às lojas em cada mês." tax_rate: "Taxa de Impostos" tax_rate_tip: "Taxa de impostos que se aplica à conta mensal que é cobrada às organizações por usarem o sistema." @@ -295,37 +308,37 @@ pt: cap_reached?_tip: "Se o \"tecto\" (limite definido à esquerda) foi atingido, dadas as definições e o volume de negócios indicado." included_tax: "Taxa incluída" included_tax_tip: "A taxa total incluída no exemplo de conta mensal, dadas as definições e o volume de negócios indicado." - total_monthly_bill_incl_tax: "Total de conta mensal (taxa incluída)" + total_monthly_bill_incl_tax: "Total da Conta Mensal (taxa incluída)" total_monthly_bill_incl_tax_tip: "O exemplo de conta mensal total incluindo taxas, dadas as definições e o volume de negócios indicado." customers: index: - add_customer: "Adicionar Cliente" - new_customer: "Novo Cliente" - customer_placeholder: "cliente@exemplo.org" + add_customer: "Adicionar Consumidor/a" + new_customer: "Novo/a Consumidor/a" + customer_placeholder: "consumidora@exemplo.org" valid_email_error: Por favor usar um endereço de email válido - add_a_new_customer_for: Adicionar novo cliente para %{shop_name} + add_a_new_customer_for: Adicionar novo/a consumidor/a a %{shop_name} code: Código duplicate_code: "Este código já está a ser usado." - bill_address: "Endereço de Factura" - ship_address: "Endereço de Entrega" - update_address_success: 'Endereço actualizado com sucesso' - update_address_error: 'Por favor preencher todos os campos obrigatórios' - edit_bill_address: 'Editar Endereço de Factura' - edit_ship_address: 'Editar Endereço de Entrega' + bill_address: "Morada de Faturação" + ship_address: "Morada de Envio" + update_address_success: 'Morada actualizada com sucesso' + update_address_error: 'Perdão! Por favor preencha todos os campos obrigatórios!' + edit_bill_address: 'Editar Morada de Faturação' + edit_ship_address: 'Editar Morada de Envio' required_fileds: 'Os campos obrigatórios estão marcados com um asterisco' - select_country: 'Selecionar País' - select_state: 'Selecionar Estado' + select_country: 'Seleccionar País' + select_state: 'Seleccionar Região' edit: 'Editar' - update_address: 'Actualizar Endereço' + update_address: 'Actualizar Morada' confirm_delete: 'De certeza que é para apagar? ' search_by_email: "Pesquisar por e-mail/código" destroy: - has_associated_orders: 'Não foi possível apagar: o cliente tem encomendas associadas a esta loja.' + has_associated_orders: 'Não foi possível apagar: o/a consumidor/a tem encomendas associadas a esta loja.' cache_settings: show: title: A carregar distributor: Distribuidor - order_cycle: Ciclo de Pedidos + order_cycle: Ciclo de Encomendas status: Status diff: Diff error: Erro @@ -343,9 +356,9 @@ pt: index: title: Taxas de Organização enterprise: Organização - fee_type: Tipo de taxa + fee_type: Tipo de Taxa name: Nome - tax_category: Categoria de taxa + tax_category: Categoria de Imposto calculator: Calculadora calculator_values: Valores da calculadora enterprise_groups: @@ -358,18 +371,19 @@ pt: manages: gere products: unit_name_placeholder: 'ex: molho, ramo, etc.' - bulk_edit: + index: unit: Unidade display_as: Mostrar como category: Categoria - tax_category: Categoria de imposto + tax_category: Categoria de Imposto inherits_properties?: Herda Propriedades? available_on: Disponível em av_on: "Disp. em" + import_date: Importado upload_an_image: Carregar uma imagem - product_search_keywords: Palavras-chave de Pesquisa de Produto + product_search_keywords: Palavras-chave para Pesquisa de Produto product_search_tip: Insira palavras que ajudem a encontrar os seus produtos nas lojas. Use um espaço para separar cada palavra-chave - SEO_keywords: Palavras-chave SEO + SEO_keywords: Palavras-chave para fins de SEO seo_tip: Insira palavras que ajudem a encontrar os seus produtos na web. Use um espaço para separar cada palavra-chave Search: Procurar properties: @@ -380,28 +394,98 @@ pt: product_distributions: "Distribuições de Produto" group_buy_options: "Opções de Compra Colectiva" back_to_products_list: "Voltar à lista de produtos" + product_import: + title: Importação de Produtos + file_not_found: O ficheiro não foi encontrado ou não pôde ser aberto + no_data: Não foram encontrados dados na tabela + confirm_reset: "Isto colocará o nível de stock a zero em todos os produtos desta \n organização que não estão presentes no ficheiro carregado" + model: + no_file: "erro: nenhum ficheiro foi carregado" + could_not_process: "não foi possível processar o ficheiro: tipo de ficheiro inválido" + incorrect_value: valor incorrecto + conditional_blank: não pode ser vazio se tipo de unidade é vazio + no_product: não foram encontrados produtos + not_found: não encontrado + blank: não pode ser vazio + products_no_permission: não tem permissões para gerir produtos desta organização + inventory_no_permission: não tem permissões para criar inventário para este produtor + none_saved: não gravou nenhum produto com sucesso + line: Linha + index: + select_file: Selecione uma folha de cálculo para carregar + spreadsheet: Folha de cálculo + import_into: "Importar para:" + product_list: Lista de produtos + inventories: Inventários + import: Importar + upload: Carregar + import: + review: Rever + proceed: Continuar + save: Guardar + results: Resultados + save_imported: Guardar produtos importados + no_valid_entries: Não foram encontradas entradas válidas + none_to_save: Não foram encontradas entradas que possam ser guardadas + some_invalid_entries: Ficheiro importado contém algumas entradas inválidas + save_valid?: Guardar entradas válidas e descartar as outras? + no_errors: Nenhum erro detectado. + save_all_imported?: Guardar todos os produtos importados? + options_and_defaults: Importar opções e valores por defeito + no_permission: não tem permissões para gerir esta organização + not_found: organização não encontrada + no_name: Sem nome + blank_supplier: alguns produtos têm o nome do fornecedor vazio + reset_absent?: Restabelecer produtos ausentes? + overwrite_all: Substituir todos + overwrite_empty: Substituir se vazio + default_stock: Definir nível de stock + default_tax_cat: Definir categoria de imposto + default_shipping_cat: Definir categoria de envio + default_available_date: Definir data de disponibilidade + validation_overview: Visão geral da validação da importação + entries_found: Entradas encontradas no ficheiro importado + entries_with_errors: Entradas contêm erros e não serão importados + products_to_create: Produtos serão criados + products_to_update: Produtos serão actualizados + inventory_to_create: Itens de inventário serão criados + inventory_to_update: Itens de inventário serão actualizados + products_to_reset: Produtos existentes terão o nível de stock restabelecido a zero + inventory_to_reset: Itens de inventário existentes terão o nível de stock restabelecido a zero + line: Linha + item_line: Linha de item + save: + final_results: Importar resultados finais + products_created: Produtos criados + products_updated: Produtos actualizados + inventory_created: Itens de inventário criados + inventory_updated: Itens de inventário actualizados + products_reset: Os produtos tiveram o nível de stock restabelecido a zero + inventory_reset: Itens de inventário tiveram o nível de stock restabelecido a zero + all_saved: "Todos os itens guardados com sucesso" variant_overrides: loading_flash: - loading_inventory: CARREGANDO INVENTÁRIO + loading_inventory: A CARREGAR INVENTÁRIO... index: title: Inventário description: Utilize esta página para gerir os inventários das suas organizações. Qualquer detalhe de produto que seja introduzido aqui, irá substituir o que foi especificado na página de 'Produtos' enable_reset?: Activar Reposição do Stock? inherit?: Herdar? add: Adicionar - hide: Esconder - select_a_shop: Selecionar Uma Loja - review_now: Avaliar Agora - new_products_alert_message: Há %{new_product_count} novos produtos disponíveis para serem adicionados ao seu inventário. - currently_empty: Actualmente, o seu inventário está vazio - no_matching_products: Nenhum produto correspondente encontrado no seu inventário - no_hidden_products: Nenhum produto foi ocultado neste inventário + hide: Ocultar + import_date: Importado + select_a_shop: Seleccionar Uma Loja + review_now: Rever Agora + new_products_alert_message: Há %{new_product_count} novos produtos disponíveis para adicionar ao seu inventário. + currently_empty: Actualmente o seu inventário está vazio + no_matching_products: Não foi encontrado nenhum produto correspondente no seu inventário + no_hidden_products: Não foi ocultado nenhum produto deste inventário no_matching_hidden_products: Nenhum produto oculto corresponde à sua pesquisa no_new_products: Nenhum novo produto está disponível para ser adicionado a este inventário - no_matching_new_products: Nenhum novo produto corresponde à sua busca - inventory_powertip: Este é o seu inventário de produtos. Para adicionar produtos ao seu inventário, selecione 'Novos Produtos' no menu Visualizar + no_matching_new_products: Nenhum produto novo corresponde à sua pesquisa + inventory_powertip: Este é o seu inventário de produtos. Para adicionar produtos ao seu inventário, seleccione 'Novos Produtos' no menu Visualizar hidden_powertip: Estes produtos foram escondidos no seu inventário e não estarão disponíveis para serem adicionados à sua loja. Pode clicar em 'Adicionar' para adicionar um produto ao seu inventário. - new_powertip: 'Estes produtos estão disponíveis para serem adicionados ao seu inventário. Clique em ''Adicionar'' para adicionar um produto em seu inventário, ou ''Ocultar'' para escondê-lo. Pode voltar atrás quando quiser! ' + new_powertip: 'Estes produtos estão disponíveis para serem adicionados ao seu inventário. Clique em ''Adicionar'' para adicionar um produto ao seu inventário, ou ''Ocultar'' para escondê-lo. Pode voltar atrás quando quiser! ' controls: back_to_my_inventory: Voltar ao inventário orders: @@ -411,28 +495,28 @@ pt: invoice_email_sent: 'Email de facturação enviado' order_email_resent: 'Email de encomenda reenviado' bulk_management: - tip: "Use esta página para alterar a quantidade de produtos entre múltiplos pedidos. Produtos podem ser removidos completamente dos pedidos, se necessário" + tip: "Use esta página para alterar a quantidade de produtos entre múltiplas encomendas. Os produtos podem ser removidos completamente das encomendas, se necessário" shared: "Recurso Compartilhado?" - order_no: "Pedido nº" - order_date: "Data do Pedido" + order_no: "Encomenda Nº" + order_date: "Data da Encomenda" max: "Máximo" product_unit: "Produto: Unidade" weight_volume: "Peso/Volume" ask: "Perguntar?" - page_title: "Gestão de Pedidos a Granel" - actions_delete: "Apagar Selecionado" + page_title: "Gestão de Encomendas por Atacado" + actions_delete: "Apagar o Selecionado" loading: "Carregando encomendas" no_results: "Nenhuma encomenda encontrada. " group_buy_unit_size: "Tamanho de Unidade para Compras de Grupo" - total_qtt_ordered: "Quantidade Total do Pedido" - max_qtt_ordered: "Quantidade Máxima do Pedido" + total_qtt_ordered: "Quantidade Total da Encomenda" + max_qtt_ordered: "Quantidade Máxima da Encomenda" current_fulfilled_units: "Unidades Completas no Momento" max_fulfilled_units: "Máximo de Unidades Completas " - order_error: "Alguns erros devem ser corrigidos antes de actualizar os pedidos.\nOs campos com bordas vermelhas contêm erros." - variants_without_unit_value: "AVISO: Algumas variantes não possuem unidade de valor" + order_error: "Alguns erros devem ser corrigidos antes de actualizar encomendas.\nOs campos com bordas vermelhas contêm erros." + variants_without_unit_value: "AVISO: Algumas variantes não possuem valor unitário" select_variant: "Selecionar uma variante" enterprise: - select_outgoing_oc_products_from: 'Selecione a saída do ciclo de pedidos ' + select_outgoing_oc_products_from: Selecione produtos de saída do ciclo de encomendas enterprises: index: title: Organizações @@ -443,14 +527,14 @@ pt: manage: Gerir form: about_us: - desc_short: Descrição curta + desc_short: Descrição Curta desc_short_placeholder: Conte-nos sobre a sua organização em uma ou duas frases desc_long: Sobre nós desc_long_placeholder: Conte um pouco de si aos consumidores. Esta informação aparece no seu perfil público. business_details: - abn: ABN + abn: NIPC abn_placeholder: 'ex: 99 123 456 789' - acn: ACN + acn: NIF acn_placeholder: 'ex: 123 456 789' display_invoice_logo: Exibir logotipo nas facturas invoice_text: 'Adicionar texto customizado no final das facturas ' @@ -462,17 +546,17 @@ pt: email_address_tip: "Este endereço de email será apresentado no seu perfil público" phone: Telefone phone_placeholder: 'ex: 987654321' - website: Site - website_placeholder: 'ex: www.trufas.com.br' + website: Website + website_placeholder: 'ex: www.cogumelos.pt' enterprise_fees: name: Nome - fee_type: Tipo de taxa - manage_fees: Gerir taxas da organização + fee_type: Tipo de Taxa + manage_fees: Gerir Taxas da Organização no_fees_yet: Ainda não tem nenhuma taxa de organização definida. create_button: Criar uma agora images: logo: Logo - promo_image_placeholder: 'Essa imagem aparece em "Sobre nós"' + promo_image_placeholder: 'Esta imagem aparece em "Sobre nós"' promo_image_note1: 'POR FAVOR OBSERVE:' promo_image_note2: Qualquer imagem promocional carregada aqui será cortada para 1200 x 260. promo_image_note3: A imagem promocional é exibida no topo da página de perfil da organização e em janelas pop-up. @@ -484,14 +568,14 @@ pt: produtos adicionados pelos seus fornecedores precisam de ser adicionados ao seu inventário antes de entrarem em stock. Se não está a usar o inventário para gerir os seus produtos, deve selecionar a opção 'recomendado' abaixo. - preferred_product_selection_from_inventory_only_yes: Novos produtos podem ser colocados na minha montra (recomendado) - preferred_product_selection_from_inventory_only_no: Novos produtos devem ser adicionados ao meu inventário antes de serem colocados na minha montra + preferred_product_selection_from_inventory_only_yes: Novos produtos podem ser colocados na minha loja (recomendado) + preferred_product_selection_from_inventory_only_no: Novos produtos têm de ser adicionados ao meu inventário antes de serem colocados na minha loja payment_methods: name: Nome applies: Aplica? manage: Gerir métodos de pagamento - not_method_yet: Ainda não tem nenhum método de pagamento. - create_button: Criar novo método de pagamento + not_method_yet: Não tem nenhum método de pagamento. + create_button: Criar Novo Método de Pagamento create_one_button: 'Criar um agora ' primary_details: name: 'Nome ' @@ -518,53 +602,54 @@ pt: shipping_methods: name: Nome applies: Aplica? - manage: Gerir formas de envio - create_button: Criar nova forma de envio + manage: Gerir Métodos de Envio + create_button: Criar Nova Forma de Envio create_one_button: Criar um agora no_method_yet: Ainda não tem nenhuma forma de envio. shop_preferences: shopfront_requires_login: "Mercado visível publicamente?" - shopfront_requires_login_tip: "Escolha se os consumidores vão precisar de fazer login para ver a montra da loja, ou se esta estará visível para toda a gente." + shopfront_requires_login_tip: "Escolha se os consumidores vão precisar de fazer login para ver a loja, ou se esta estará visível para toda a gente." shopfront_requires_login_false: "Público" - shopfront_requires_login_true: "Disponível somente para clientes registados" + shopfront_requires_login_true: "Visível somente para consumidores registados" recommend_require_login: "Recomendamos que peça login dos utilizadores quando é permitido que as encomendas sejam alteradas. " allow_guest_orders: "Encomendas de convidados" allow_guest_orders_tip: "Permitir o checkout como convidado, ou requisitar um utilizador registado. " - allow_guest_orders_false: "Requisitar o registo para fazer encomendas" + allow_guest_orders_false: "Exigir o registo para fazer encomendas" allow_guest_orders_true: "Permitir checkout de convidados" allow_order_changes: "Alterar encomendas" - allow_order_changes_tip: "Permitir que os consumidores alterem os seus pedidos enquanto o ciclo de encomendas estiver aberto." + allow_order_changes_tip: "Permitir que os consumidores alterem as suas encomendas enquanto o ciclo de encomendas estiver aberto." allow_order_changes_false: "Encomendas submetidas não podem ser alteradas / canceladas." - allow_order_changes_true: "Os consumidores pode alterar / cancelar encomendas enquanto o ciclo de encomendas estiver aberto." + allow_order_changes_true: "Os consumidores podem alterar / cancelar encomendas enquanto o ciclo de encomendas estiver aberto." enable_subscriptions: "Subscrições" - enable_subscriptions_tip: "Activar funcionalidade das subscrições?" + enable_subscriptions_tip: "Activar a funcionalidade das subscrições?" enable_subscriptions_false: "Desactivado" enable_subscriptions_true: "Activo" - shopfront_message: Mensagem da montra da loja + shopfront_message: Mensagem da Loja shopfront_message_placeholder: > Uma explicação breve e opcional para os consumidores a explicar como - funciona a loja, a ser exibida acima da lista de produtos na sua montra. - shopfront_closed_message: Mensagem de loja fechada + funciona a sua loja, a ser exibida acima da lista de produtos na sua + loja. + shopfront_closed_message: Mensagem de Loja Fechada shopfront_closed_message_placeholder: > - Uma mensagem que forneça uma explicação detalhada sobre o motivo da - loja estar fechada e quando é que os consumidores podem esperar que - abra novamente, a ser exibida quando não houver nenhum ciclo de encomendas - activo. + Uma mensagem que forneça uma explicação mais detalhada sobre o motivo + da sua loja estar fechada e quando é que os consumidores podem esperar + que abra novamente. A ser exibida quando não houver nenhum ciclo de + encomendas activo. shopfront_category_ordering: Categoria de pedidos da loja - open_date: Dia de abertura - close_date: Dia de fecho + open_date: Dia de Abertura + close_date: Data de fecho social: twitter_placeholder: 'ex: @o_prof' stripe_connect: connect_with_stripe: "Conectar com o Stripe" stripe_connect_intro: "Para aceitar pagamentos com cartão de crédito, vai ser necessário ligar a sua conta Stripe à Open Food Network. Use o botão à direita para começar." stripe_account_connected: "Conta Stripe conectada" - disconnect: "Desligar conta" + disconnect: "Desconectar conta" confirm_modal: title: Conectar com o Stripe - part1: O Stripe é um serviço de processamento de pagamentos que permite às lojas OFN aceitarem pagamentos dos seus clientes com cartão de crédito. + part1: O Stripe é um serviço de processamento de pagamentos que permite às lojas OFN aceitarem pagamentos dos seus consumidores com cartão de crédito. part2: Para usar esta funcionalidade, tem de ligar a sua conta Stripe à OFN. Ao clicar em 'Concordo' abaixo vai ser redirecionado para o website Stripe onde pode estabelecer ligação com uma conta existente, ou criar uma conta nova caso ainda não tenha uma. - part3: Isto vai permitir à Open Food Network aceitar pagamentos dos seus clientes com cartão de crédito em seu nome. Tenha em conta que do seu lado vai ser necessário manter a sua própria conta Stripe, pagar as taxas à Stripe e lidar com quaisquer rejeições ou serviços ao cliente. + part3: Isto vai permitir à Open Food Network aceitar pagamentos dos seus consumidores com cartão de crédito em seu nome. Tenha em conta que do seu lado vai ser necessário manter a sua própria conta Stripe, pagar as taxas à Stripe e lidar com quaisquer rejeições ou serviços ao cliente. i_agree: Concordo cancel: Cancelar tag_rules: @@ -578,47 +663,47 @@ pt: add_new_rule: '+ Adicionar nova regra' add_new_tag: '+ Adicionar nova etiqueta' users: - email_confirmation_notice_html: "A confirmação de e-mail está pendente. Enviamos um e-mail de confirmação para %{email}." + email_confirmation_notice_html: "A confirmação do email está pendente. Enviamos um email de confirmação para %{email}." resend: Reenviar owner: 'Proprietário' contact: "Contacto" - contact_tip: "O coordenador que vai receber emails da organização, como encomendas e notificações. Tem de ter um endereço de email confirmado." + contact_tip: "O gestor que vai receber emails da organização, como encomendas e notificações. Tem de ter um endereço de email confirmado." owner_tip: O utilizador principal responsável por esta organização. notifications: Notificações - notifications_tip: Notificações sobre pedidos serão enviadas para este endereço de e-mail. - notifications_placeholder: 'ex: gustavo@trufas.com.br' - notifications_note: 'Nota: um novo endereço de e-mail pode precisar ser confirmado antes do uso' - managers: Administradores + notifications_tip: Notificações sobre encomendas serão enviadas para este email. + notifications_placeholder: 'ex: gustavo@trufas.pt' + notifications_note: 'Nota: um novo endereço de email pode precisar de ser confirmado antes do uso' + managers: Gestores managers_tip: Os outros utilizadores com permissão para gerir esta organização. invite_manager: "Convidar Gestor/a" - invite_manager_tip: "Convidar um utilizador não-registado a inscrever-se e tornar-se gestor desta organização." - add_unregistered_user: "Adicionar um utilizador não-registado" + invite_manager_tip: "Convidar um/a utilizador/a não-registado/a a inscrever-se e tornar-se gestor/a desta organização." + add_unregistered_user: "Adicionar um/a utilizador/a não-registado/a" email_confirmed: "Email confirmado" email_not_confirmed: "Email não confirmado" actions: - edit_profile: Editar perfil + edit_profile: Editar Perfil properties: Propriedades - payment_methods: Formas de pagamento + payment_methods: Métodos de pagamento payment_methods_tip: Esta organização não tem formas de pagamento definidas - shipping_methods: Formas de entrega + shipping_methods: Métodos de Envio shipping_methods_tip: Esta organização tem formas de entrega definidas - enterprise_fees: Taxas da organização + enterprise_fees: Taxas da Organização enterprise_fees_tip: Esta organização não cobra taxas admin_index: name: Nome - role: Cargo + role: Papeis sells: Vende visible: Visível? owner: Proprietário producer: Produtor change_type_form: - producer_profile: Perfil do produtor + producer_profile: Perfil do Produtor connect_ofn: Conectar através da Open Food Network always_free: SEMPRE GRATUITO producer_description_text: Adicione os seus produtos à Open Food Network, permitindo que os hubs os comercializem nas suas lojas. - producer_shop: Loja de produtor + producer_shop: Loja de Produtor sell_your_produce: 'Venda os seus próprios produtos ' - producer_shop_description_text: Venda os seus produtos directamente aos seus clientes através da sua própria montra Open Food Network. + producer_shop_description_text: Venda os seus produtos directamente aos seus consumidores através da sua própria montra Open Food Network. producer_shop_description_text2: Uma Loja de Produtor/a é somente para os seus produtos, se quiser vender produtos de outro local, selecione Hub de Produtores. producer_hub: Hub de Produtores producer_hub_text: Venda os seus próprios produtos e também os de outros produtores @@ -626,7 +711,7 @@ pt: profile: Somente perfil get_listing: Obter uma listagem profile_description_text: As pessoas podem encontrá-lo e contactá-lo na Open Food Network. A sua organização ficará visível no mapa e poderá ser encontrada através do motor de pesquisa. - hub_shop: Loja do hub + hub_shop: Loja do distribuidor hub_shop_text: Comercialize produtos de outros hub_shop_description_text: A sua organização é a espinha dorsal de um sistema de produção e consumo local. Através dela, pode agregar produtos de outras organizações e produtores, e comercializá-los na sua loja na Open Food Network. choose_option: Por favor escolha uma das opções acima. @@ -634,13 +719,13 @@ pt: enterprise_user_index: loading_enterprises: CARREGANDO ORGANIZAÇÕES no_enterprises_found: Nenhuma organização encontrada. - search_placeholder: Busca por nome - manage: Administrar + search_placeholder: Procurar por nome + manage: Gerir new_form: owner: Proprietário owner_tip: O utilizador principal responsável por esta organização. i_am_producer: Sou um produtor - contact_name: Nome para contato + contact_name: Nome do contacto edit: editing: 'Edição:' back_link: Voltar à lista de organizações @@ -648,35 +733,35 @@ pt: title: Nova Organização back_link: Voltar à lista de organizações welcome: - welcome_title: Bem-vindo ao Open Food Network! + welcome_title: Bem-vindo à Open Food Network! welcome_text: Você criou com sucesso uma - next_step: Próximo passo - choose_starting_point: 'Escolha o seu ponto de partida:' + next_step: Passo seguinte + choose_starting_point: 'Escolha o seu pacote:' invite_manager: - user_already_exists: "O utilizador já existe" + user_already_exists: "O/A utilizador/a já existe" error: "Algo correu mal" order_cycles: edit: - advanced_settings: Configurações avançadas + advanced_settings: Configurações Avançadas update_and_close: Atualizar e fechar choose_products_from: 'Escolha produtos de:' exchange_form: - pickup_time_tip: Quando é que as encomendas deste OC ficam prontas para o cliente + pickup_time_tip: Quando as encomendas deste ciclo ficam prontas para o consumidor pickup_instructions_placeholder: "Instruções para levantamento" - pickup_instructions_tip: Estas instruções são mostradas aos clientes depois de finalizarem as suas encomendas + pickup_instructions_tip: Estas instruções são mostradas aos consumidores depois de finalizarem as suas encomendas pickup_time_placeholder: "Pronto para (dia/hora)" - receival_instructions_placeholder: "Instruções de recepção" + receival_instructions_placeholder: "Instruções de recebimento" add_fee: 'Acrescentar taxa' selected: 'selecionado' add_exchange_form: add_supplier: 'Acrescentar fornecedor' - add_distributor: 'Acrescentar distribuidor' + add_distributor: 'Adicionar distribuidor' advanced_settings: - title: Configurações avançadas - choose_product_tip: 'Você pode optar por restringir todos os produtos disponíveis (tanto de entrada, quanto de saída) somente para aqueles do inventário %{inventory}. ' + title: Configurações Avançadas + choose_product_tip: 'Pode optar por restringir todos os produtos disponíveis (tanto de entrada como de saída) apenas aos produtos do inventário %{inventory}. ' preferred_product_selection_from_coordinator_inventory_only_here: Apenas inventário do coordenador - preferred_product_selection_from_coordinator_inventory_only_all: Todos os produtos disponíveis - save_reload: Salvar e atualizar página + preferred_product_selection_from_coordinator_inventory_only_all: Todos os Produtos Disponíveis + save_reload: Guardar e recarregar página coordinator_fees: add: Adicionar taxa de coordenador form: @@ -689,46 +774,48 @@ pt: products: Produtos tags: Etiquetas add_a_tag: Acrescentar uma etiqueta - delivery_details: Detalhes de entrega/retirada + delivery_details: Detalhes de entrega/levantamento debug_info: Informação de depuração index: involving: Envolvendo schedule: Horário schedules: Horários - adding_a_new_schedule: Adicionar um novo horário - updating_a_schedule: Actualizar um horário - new_schedule: Novo horário - create_schedule: Criar horário - update_schedule: Actualizar horário - delete_schedule: Apagar horário + adding_a_new_schedule: Adicionar um novo Horário + updating_a_schedule: Actualizar um Horário + new_schedule: Novo Horário + create_schedule: Criar Horário + update_schedule: Actualizar Horário + delete_schedule: Apagar Horário created_schedule: Horário criado updated_schedule: Horário actualizado deleted_schedule: Horário apagado - schedule_name_placeholder: Nome do horário + schedule_name_placeholder: Nome do Horário name_required_error: Por favor atribua um nome a este horário no_order_cycles_error: Por favor selecione pelo menos um ciclo de encomendas (arrastar e largar) name_and_timing_form: name: Nome - orders_open: Pedidos abrem às + orders_open: Encomendas abrem às coordinator: Coordenador - order_closes: Pedidos fecham + orders_close: Encomendas fecham row: suppliers: fornecedores distributors: distribuidores variants: variantes simple_form: ready_for: Pronto para - ready_for_placeholder: Dia/hora + ready_for_placeholder: Data / hora customer_instructions: Instruções para o consumidor - customer_instructions_placeholder: Notas de entrega ou retirada + customer_instructions_placeholder: Notas de entrega ou levantamento products: Produtos fees: Taxas destroy_errors: - orders_present: Esse ciclo de encomendas foi selecionado por um/a consumidor/a e não pode ser apagado. Para evitar que os clientes acedam, por favor feche-o. + orders_present: Esse ciclo de encomendas foi selecionado por um/a consumidor/a e não pode ser apagado. Para evitar que os consumidores acedam, por favor feche-o. schedule_present: Esse ciclo de encomendas está ligado a um horário e não pode ser apagado. Por favor elimine a ligação ou apague primeiro o horário. + bulk_update: + no_data: Hmmm, algo correu mal. Não foram encontrados dados do ciclo de encomendas. producer_properties: index: - title: Propriedades do produtor + title: Propriedades do Produtor proxy_orders: cancel: could_not_cancel_the_order: Não foi possível cancelar a encomenda @@ -736,12 +823,12 @@ pt: could_not_resume_the_order: Não foi possível prosseguir com a encomenda shared: user_guide_link: - user_guide: Guia do usuário + user_guide: Manual do Utilizador invoice_settings: edit: - title: Configuração da fatura + title: Configuração de Faturas invoice_style2?: Use o modelo alternativo de fatura que inclui o total de impostos dividido por taxa e taxa de imposto por item (ainda não disponível para países que exibem preços sem taxas) - enable_receipt_printing?: Mostrar opções para imprimir recibos usando impressoras térmicas em pedidos suspensos? + enable_receipt_printing?: Mostrar opções para imprimir recibos usando impressoras térmicas no selector de encomendas? overview: enterprises_header: ofn_with_tip: As Organizações são Produtores e/ou Hubs e representam a unidade básica de organização dentro da Open Food Network. @@ -754,7 +841,7 @@ pt: resend_email: Reenviar Email has_no_payment_methods: "%{enterprise} actualmente não tem métodos de pagamento" has_no_shipping_methods: "%{enterprise} actualmente não tem métodos de envio" - email_confirmation: "O email de confirmação está pendente. Enviámos um email de confirmação para %{email}." + email_confirmation: "A confirmação do email está pendente. Enviámos um email de confirmação para %{email}." not_visible: "%{enterprise} não está visível portanto não pode ser encontrada nem no mapa nem nas pesquisas." reports: hidden: ESCONDIDA @@ -764,31 +851,31 @@ pt: supplier_totals: Totais do Fornecedor no Ciclo de Encomendas supplier_totals_by_distributor: Totais do Fornecedor no Ciclo de Encomendas por Distribuidor totals_by_supplier: Totais do Distribuidor no Ciclo de Encomendas por Fornecedor - customer_totals: Totais do Cliente no Ciclo de Encomendas + customer_totals: Totais do Consumidor no Ciclo de Encomendas all_products: Todos os produtos - inventory: Inventário (em mãos) + inventory: Inventário (disponível) lettuce_share: LettuceShare - mailing_list: Mailing List + mailing_list: Listas de Email addresses: Moradas - payment_methods: Relatório dos Métodos de Pagamento + payment_methods: Relatório de Métodos de Pagamento delivery: Relatório de Entregas tax_types: Tipos de Imposto tax_rates: Taxas de Imposto - pack_by_customer: Pacote por Cliente - pack_by_supplier: Pacote por Fornecedor + pack_by_customer: Embalar por Consumidor + pack_by_supplier: Embalar por Fornecedor orders_and_distributors: name: Encomendas e Distribuidores description: Encomendas com detalhes do distribuidor bulk_coop: name: Cooperativa por Atacado - description: Relatórios de encomendas de Cooperativa por Atacado + description: Relatórios de Encomendas de Cooperativas por Atacado payments: - name: Relatórios de Pagamento - description: Relatórios para Pagamentos + name: Relatórios de Pagamentos + description: Relatórios de Pagamentos orders_and_fulfillment: - name: Relatórios de Encomendas & Desempenho + name: Relatórios de Encomendas & Cumprimento customers: - name: Clientes + name: Consumidores/as products_and_inventory: name: Produtos & Inventário sales_total: @@ -800,16 +887,16 @@ pt: order_cycle_management: name: Gestão de Ciclo de Encomendas sales_tax: - name: Imposto de vendas + name: Imposto sobre Vendas xero_invoices: name: Facturas Xero - description: Facturas a importar no Xero + description: Facturas para importar para o Xero packing: - name: Relatórios de embalamento + name: Relatórios de Embalamento subscriptions: subscriptions: Subscrições - new: Nova subscrição - create: Criar subscrição + new: Nova Subscrição + create: Criar Subscrição index: please_select_a_shop: Por favor selecione uma loja edit_subscription: Editar Subscrição @@ -817,14 +904,14 @@ pt: unpause_subscription: Parar pausa da Subscrição cancel_subscription: Cancelar Subscrição setup_explanation: - just_a_few_more_steps: 'Só mais uns passos para poder começar:' + just_a_few_more_steps: 'Só mais uns passos antes de poder começar:' enable_subscriptions: "Activar subscrições para pelo menos uma das suas lojas" enable_subscriptions_step_1_html: 1. Vá à página %{enterprises_link}, encontre a sua loja, e clique em "Gerir" enable_subscriptions_step_2: 2. Em "Preferências da Loja", active a opção de Subscrições set_up_shipping_and_payment_methods_html: 'Definir métodos de %{shipping_link} e %{payment_link} ' - set_up_shipping_and_payment_methods_note_html: Repare que somente pagamentos em Dinheiro ou usando o método Stripe é que podem
ser usados com as subscrições + set_up_shipping_and_payment_methods_note_html: Note que apenas pagamentos em Dinheiro ou usando o método Stripe podem
ser usados com as subscrições ensure_at_least_one_customer_html: Garanta que existe pelo menos um %{customer_link} - create_at_least_one_schedule: Crie pelo menos um horário + create_at_least_one_schedule: Crie pelo menos um Horário create_at_least_one_schedule_step_1_html: 1. Vá à página %{order_cycles_link} create_at_least_one_schedule_step_2: 2. Crie um ciclo de encomendas se ainda não o fez create_at_least_one_schedule_step_3: 3. Clique em '+ Novo Horário', e preencha o formulário @@ -850,7 +937,7 @@ pt: product_already_in_order: Este produto já foi adicionado à encomenda. Por favor edite a quantidade directamente. orders: number: Número - confirm_edit: Tem a certeza que quer editar esta encomenda? Ao fazê-lo pode tornar-se mais difícil sincronizar alterações automaticamente à subscrição no futuro. + confirm_edit: Tem a certeza que quer editar esta encomenda? Ao fazê-lo pode tornar-se mais difícil sincronizar alterações automaticamente com a subscrição no futuro. confirm_cancel_msg: Tem a certeza que pretende cancelar esta subscrição? Esta acção não pode ser desfeita. cancel_failure_msg: 'Desculpe, o cancelamento falhou!' confirm_pause_msg: Tem a certeza que deseja pausar esta subscrição? @@ -858,7 +945,7 @@ pt: confirm_unpause_msg: Tem a certeza que pretende parar a pausa desta subscrição? unpause_failure_msg: 'Desculpe, não foi possível parar a pausa!' confirm_cancel_open_orders_msg: "Algumas encomendas para esta subscrição estão actualmente abertas. O/a consumidor/a foi notificado que a encomenda será processada. Quer cancelar esta(s) encomenda(s) ou mantê-las?" - resume_canceled_orders_msg: "Algumas encomendas desta subscrição podem ser retomadas neste momento. Para retomá-las, selecione no menú dropdown de encomendas." + resume_canceled_orders_msg: "Algumas encomendas desta subscrição podem ser retomadas neste momento. Para retomá-las, selecione no menu dropdown de encomendas." yes_cancel_them: Cancelá-las no_keep_them: Mantê-las yes_i_am_sure: Sim, tenho a certeza @@ -866,7 +953,7 @@ pt: no_results: no_subscriptions: Ainda não existem subscrições.... why_dont_you_add_one: Porque não acrescentar uma? :) - no_matching_subscriptions: Não foram encontradas subscrições a condizer + no_matching_subscriptions: Não foram encontradas subscrições correspondentes schedules: destroy: associated_subscriptions_error: Este horário não pode ser eliminado porque tem subscrições associadas @@ -879,13 +966,13 @@ pt: configuration_explanation_html: Para instruções detalhadas sobre como configurar a integração com Stripe Connect, por favor consulte este guia. status: Estado ok: Ok - instance_secret_key: Chave Secreta da instância - account_id: ID de conta - business_name: Nome do negócio + instance_secret_key: Chave Secreta da Instância + account_id: ID de Conta + business_name: Nome do Negócio charges_enabled: Taxas activas charges_enabled_warning: "Aviso: As taxas não estão activas para a sua conta" - auth_fail_error: A chave API que indicou não é válida - empty_api_key_error_html: Não foi fornecida nenhuma chave API. Para definir a sua chave API, por favor siga estas instruções + auth_fail_error: A chave da API que indicou não é válida + empty_api_key_error_html: Não foi fornecida nenhuma chave de API Stripe. Para definir a sua chave de API, por favor siga estas instruções controllers: enterprises: stripe_connect_cancelled: "A ligação ao Stripe foi cancelada" @@ -895,53 +982,53 @@ pt: resource: Configuração Stripe Connect checkout: already_ordered: - cart: "Carrinho" - message_html: "Você já possui um pedido para esta compra. Verifique o %{cart} para ver os itens já encomendados. Você também pode cancelar itens enquanto o ciclo estiver aberto." + cart: "carrinho" + message_html: "Já tem uma encomenda para este ciclo de encomendas. Verifique o %{cart} para ver os itens já encomendados. Também pode cancelar itens enquanto o ciclo estiver aberto." shops: hubs: show_closed_shops: "Mostrar lojas fechadas" - hide_closed_shops: "Esconder lojas fechadas" - show_on_map: "Mostrar tudo no mapa" + hide_closed_shops: "Ocultar lojas fechadas" + show_on_map: "Mostrar todas as lojas no mapa" shared: menu: cart: checkout: "Finalizar compra agora" - already_ordered_products: "Já encomendado neste ciclo de pedidos" + already_ordered_products: "Já encomendado neste ciclo de encomendas" register_call: - selling_on_ofn: "Interessado em participar da Open Food Network?" - register: "Registre-se aqui" + selling_on_ofn: "Tem interesse em participar na Open Food Network?" + register: "Registe-se aqui" shop: messages: login: "Entrar" - register: "registro" - contact: "contato" + register: "registo" + contact: "contacto" require_customer_login: "Essa loja é somente para clientes." - require_login_html: "Fazer o %{login} se você já possui uma conta. Caso contrário, %{register} para se tornar cliente. " + require_login_html: "Por favor %{login} se já tem uma conta. Caso contrário, %{register} para se tornar consumidor." require_customer_html: "Por favor %{contact} a %{enterprise} para se tornar consumidor/a. " card_could_not_be_saved: o cartão não pode ser guardado spree_gateway_error_flash_for_checkout: "Houve um problema com a sua informação de pagamento: %{error}" - invoice_billing_address: "Endereço de cobrança:" - invoice_column_tax: "GST" + invoice_billing_address: "Morada de faturação:" + invoice_column_tax: "IVA" invoice_column_price: "Preço" invoice_column_item: "Item" - invoice_column_qty: "Quantidade" - invoice_column_unit_price_with_taxes: "Preço unitário (com taxa)" + invoice_column_qty: "Qtd" + invoice_column_unit_price_with_taxes: "Preço unitário (incl. taxa)" invoice_column_unit_price_without_taxes: "Preço unitário (sem taxa)" - invoice_column_price_with_taxes: "Preço total (com taxas)" + invoice_column_price_with_taxes: "Preço total (incl. taxas)" invoice_column_price_without_taxes: "Preço total (sem taxas)" invoice_column_tax_rate: "Taxa de imposto" - invoice_tax_total: "Total GST:" - tax_invoice: "NOTA FISCAL" - tax_total: "Total de imposto (%{rate}):" - total_excl_tax: "Total (sem imposto):" - total_incl_tax: "Total (com imposto):" - abn: "ABN:" - acn: "ACN:" - invoice_issued_on: "Nota fiscal emitida em:" - order_number: "Número da nota fiscal:" - date_of_transaction: "Dia da transação:" + invoice_tax_total: "Total IVA:" + tax_invoice: "FACTURA FISCAL" + tax_total: "Total de Impostos (%{rate}):" + total_excl_tax: "Total (excl. imposto):" + total_incl_tax: "Total (incl. imposto):" + abn: "NIPC:" + acn: "NIF:" + invoice_issued_on: "Fatura emitida em:" + order_number: "Número da fatura:" + date_of_transaction: "Data da transação:" ticket_column_qty: "Qtd" - ticket_column_item: "Ítem" + ticket_column_item: "Item" ticket_column_unit_price: "Preço Unitário" ticket_column_total_price: "Preço Total" logo: "Logo (640x130)" @@ -959,24 +1046,24 @@ pt: footer_email: "Email" footer_links_md: "Links" footer_about_url: "URL Sobre" - footer_tos_url: "URL Termos de Serviço" + footer_tos_url: "URL dos Termos de Serviço" name: Nome first_name: Primeiro Nome last_name: Último Nome email: Email phone: Telefone - next: Próximo - address: Endereço + next: Seguinte + address: Morada address_placeholder: 'ex: Rua Alta, 123' - address2: Complemento + address2: Morada (cont.) city: Cidade - city_placeholder: 'ex: Porto' + city_placeholder: ex. Famalicão postcode: Código postal - postcode_placeholder: 'ex: 3070' - state: Estado + postcode_placeholder: 'ex: 4000-125' + state: Região country: País unauthorized: Não autorizado - terms_of_service: "Termos de Serviço" + terms_of_service: "Termos de serviço" on_demand: Sob encomenda none: Nenhum not_allowed: Não permitido @@ -996,9 +1083,9 @@ pt: label_learn: "Aprender" label_blog: "Blog" label_support: "Assistência" - label_shopping: "Compras" + label_shopping: "A comprar" label_login: "Entrar" - label_logout: "Sair" + label_logout: "Terminar sessão" label_signup: "Registe-se" label_administration: "Administração" label_admin: "Admin" @@ -1007,7 +1094,7 @@ pt: label_less: "Mostrar menos" label_notices: "Avisos" cart_items: "itens" - cart_headline: "o seu carrinho de compras" + cart_headline: "O seu carrinho de compras" total: "Total" cart_updating: "Atualizando carrinho" cart_empty: "Carrinho vazio" @@ -1029,132 +1116,132 @@ pt: card_could_not_be_removed: Pedimos desculpa, o cartão não pôde ser removido. ie_warning_headline: "O seu navegador está desactualizado :-(" ie_warning_text: "Para ter uma melhor experiência com a Open Food Network, recomendamos vivamente que actualize o seu navegador:" - ie_warning_chrome: Baixar Chrome - ie_warning_firefox: Baixar Firefox + ie_warning_chrome: Descarregar Chrome + ie_warning_firefox: Descarregar Firefox ie_warning_ie: Actualizar Internet Explorer ie_warning_other: "Não consegue actualizar o navegador? Tente aceder à OFN pelo smartphone :-)" footer_global_headline: "OFN Global" footer_global_home: "Início" - footer_global_news: "Novidades" + footer_global_news: "Notícias" footer_global_about: "Sobre" footer_global_contact: "Contacto" footer_sites_headline: "Páginas OFN" footer_sites_developer: "Desenvolvimento" footer_sites_community: "Comunidade" - footer_sites_userguide: "Manual de Utilizador" - footer_secure: "Seguro e confiável." + footer_sites_userguide: "Manual do Utilizador" + footer_secure: "Seguro e de confiança." footer_secure_text: "A Open Food Network utiliza a criptografia SSL (2048 bit RSA) para manter as suas informações em segurança. Os nossos servidores não guardam os detalhes do seu cartão de crédito e os pagamentos são processados por serviços compatíveis com PCI." - footer_contact_headline: "Mantenha-se em contacto" + footer_contact_headline: "Ficamos em contacto" footer_contact_email: "Envie-nos um email" footer_nav_headline: "Navegar" footer_join_headline: "Junte-se a nós" - footer_join_body: "Criar uma lista de ofertas, loja ou cooperativa na Open Food Network" + footer_join_body: "Crie uma lista de ofertas, uma loja ou um grupo de consumo na Open Food Network" footer_join_cta: "Quero saber mais!" - footer_legal_call: "Leia o nosso" + footer_legal_call: "Leia os nossos" footer_legal_tos: "Termos e condições" - footer_legal_visit: "Encontre-nos em " - footer_legal_text_html: "A Open Food Network é uma plataforma livre e open-source. O nosso conteúdo é disponibilizado sob uma licença %{content_license} e o nosso código %{code_license}." - home_shop: Compre Agora - brandstory_headline: "Alimentos, com liberdade" - brandstory_intro: "Às vezes, a melhor maneira de consertar o sistema é construir um novo..." + footer_legal_visit: "Encontre-nos no" + footer_legal_text_html: "A Open Food Network é uma plataforma livre e de código aberto. O nosso conteúdo tem uma licença %{content_license} e o nosso código %{code_license}." + home_shop: Ir às compras + brandstory_headline: "Para quem consome com princípios" + brandstory_intro: "Às vezes a melhor forma de consertar o sistema é construir um novo..." brandstory_part1: "Começamos a partir da terra. Com agricultores e produtores prontos a contarem as suas histórias com um brilho nos olhos. Com distribuidores prontos a estabelecerem ligações entre pessoas e produtos de forma justa e honesta. Com consumidores que acreditam que melhores decisões no momento da compra podem mudar o mundo. " - brandstory_part2: "Precisamos de uma ferramenta para empoderar a todos que produzem, vendem e compram comida. Uma maneira de contar histórias, e controlar toda a logística. " - brandstory_part3: "Por isso construímos um mercado online, transparente, capaz de criar conexões verdadeiras. O código é aberto, e pode ser modificado para melhor se adaptar as particularidades de cada canto do planeta. " - brandstory_part4: "Queremos de volta o controle sobre os alimentos que consumimos." - brandstory_part5_strong: "Bem vindos à Open Food Network" - brandstory_part6: "Todos amamos comida. Agora a gente também pode amar nosso sistema alimentar. " - learn_body: "Conheça novos modelos, histórias e fornecedores para dar suporte à sua iniciativa, mercado ou organização. Encontre oportunidades para aprender com quem faz parte do seu setor. " + brandstory_part2: "Depois precisamos de uma forma de torná-lo real. Uma forma de dar poder a todos e todas que cultivam, produzem, vendem e compram alimentos. Uma forma de contar todas as histórias, e de lidar com toda a logística. Una forma de transformar transacção em transformação todos os dias. " + brandstory_part3: "Por isso construímos um mercado online para quem quer jogar noutro campeonato. É transparente, e portanto promove relações verdadeiras. É de código aberto, e portanto somos todos donos e donas, podendo modificá-lo para melhor se adaptar às particularidades de cada lugar. " + brandstory_part4: "Funciona em todo o lado e muda tudo. " + brandstory_part5_strong: "Chamamos-lhe Open Food Network" + brandstory_part6: "Todos nós adoramos comer bem. Agora também podemos amar o nosso sistema alimentar. " + learn_body: "Explore novos modelos, histórias e recursos para apoiar a sua iniciativa agroalimentar. Encontre acções de formação, eventos e outras oportunidades para aprender com os seus pares. " learn_cta: "Inspire-se" - connect_body: "Procure em nossa lista por produtores, distribuidores e cooperativas para encontrar um comércio justo, perto de você. Registre seu negócio na OFN para que os consumidores possam te encontrar. Junte-se à comunidade para trocar experiências e resolver problemas, juntos. " + connect_body: "Procure no nosso directório de produtores, centrais de abastecimento e grupos de consumo para encontrar um comércio justo perto de si. Registe o seu negócio ou organização na OFN para que os consumidores possam encontrá-lo. Junte-se à comunidade para trocar experiências e resolver problemas em conjunto." connect_cta: "Explore" - system_headline: "A feira funciona assim:" - system_step1: "1. Busca" - system_step1_text: "Escolha entre diversos mercados independentes por alimentos locais, da estação. Procure por região, tipo de alimentos, ou se você prefere retirar ou receber em casa. " - system_step2: "2. Compra" - system_step2_text: "Transforme seu comércio com fornecedores de alimentos locais. Conheça as histórias por trás do seu produto, e daqueles que o fazem!" - system_step3: "3. Coleta / Entrega" - system_step3_text: "Espere que sua compre chegue até você, ou retire nos pontos de entrega determinados pelo seu fornecedor. Simples assim!" - cta_headline: "A feira que incentiva a economia local." - cta_label: "Estou pronto" - stats_headline: "Estamos criando um novo sistema alimentar" - stats_producers: "produtores" + system_headline: "As Compras — eis como funcionam:" + system_step1: "1. Procurar" + system_step1_text: "Escolha produtos sazonais de proximidade entre diversos pontos de venda independentes. Procure por região, tipo de alimento, e participe num grupo de consumo. " + system_step2: "2. Comprar" + system_step2_text: "Transforme o seu consumo, adquirindo produtos locais acessíveis de diversos produtores e produtoras. Conheça as histórias por trás da sua comida e quem a produz!" + system_step3: "3. Levantar" + system_step3_text: "Há a possibilidade de entrega ao domicílio ou de levantamento no ponto de distribuição definido por cada fornecedor. Visite o seu Grupo de Consumo para um vínculo mais directo com os produtores e com a vizinhança. " + cta_headline: "Incentivar a economia local, fazer do mundo um lugar melhor. " + cta_label: "Eu voto com o meu garfo" + stats_headline: "Estamos a criar um novo sistema alimentar" + stats_producers: "produtores/as" stats_shops: "lojas" - stats_shoppers: "compradores" - stats_orders: "pedidos" - checkout_title: Fechar pedido - checkout_now: Fechar pedido agora - checkout_order_ready: Pedido pronto para + stats_shoppers: "consumidores/as" + stats_orders: "encomendas" + checkout_title: Finalizar compra + checkout_now: Finalizar compra agora + checkout_order_ready: Encomenda pronta para checkout_hide: Ocultar checkout_expand: Expandir - checkout_headline: "Ok, pronto para fechar o pedido?" - checkout_as_guest: "Fechar pedido como convidado" - checkout_details: "Seus detalhes" + checkout_headline: "Ok, pronto para finalizar a encomenda?" + checkout_as_guest: "Finalizar encomenda como convidado" + checkout_details: "Os seus detalhes" checkout_billing: "Informações para fatura" - checkout_default_bill_address: "Salvar como endereço de faturamento padrão" + checkout_default_bill_address: "Guardar como morada de faturação por defeito" checkout_shipping: Informações para envio - checkout_default_ship_address: "Salvar como endereço para entrega padrão" + checkout_default_ship_address: "Guardar como morada para entrega por defeito" checkout_method_free: Não custa nada - checkout_address_same: O endereço de entrega é o mesmo endereço da fatura? + checkout_address_same: A morada de entrega é a mesma que a morada de faturação? checkout_ready_for: "Pronto para" checkout_instructions: "Algum comentário ou instruções especiais?" checkout_payment: Pagamento - checkout_send: Fechar pedido agora - checkout_your_order: Seu pedido + checkout_send: Finalizar encomenda agora + checkout_your_order: A sua encomenda checkout_cart_total: Total do carrinho checkout_shipping_price: Envio checkout_total_price: Total - checkout_back_to_cart: "De volta para o Carrinho" - cost_currency: "Moeda de custo" + checkout_back_to_cart: "Voltar ao Carrinho" + cost_currency: "Moeda de Custo" order_paid: PAGO order_not_paid: NÃO PAGO - order_total: Total do pedido + order_total: Total da encomenda order_payment: "Pagando com:" - order_billing_address: Endereço de fatura + order_billing_address: Morada de faturação order_delivery_on: Entrega em - order_delivery_address: Endereço para entrega + order_delivery_address: Morada de entrega order_delivery_time: Tempo de entrega - order_special_instructions: "Suas anotações" - order_pickup_time: Pronto para retirada - order_pickup_instructions: Instruções para retirada + order_special_instructions: "As suas notas" + order_pickup_time: Pronto para levantamento + order_pickup_instructions: Instruções para levantamento order_produce: Produtos order_total_price: Total - order_includes_tax: (inclui taxas) - order_payment_paypal_successful: Seu pagamento via Paypal foi processado com sucesso. - order_hub_info: Informações do distribuidor + order_includes_tax: (inclui impostos) + order_payment_paypal_successful: O seu pagamento via Paypal foi processado com sucesso. + order_hub_info: Informações da Central order_back_to_store: Voltar à loja - order_back_to_cart: Voltar ao carrinho - bom_tip: "Use esta página para alterar as quantidades de produtos em vários pedidos. Os produtos também podem ser inteiramente removidos dos pedidos, se necessário." - unsaved_changes_warning: "Existem modificações não salvas que serão perdidas se você continuar" - unsaved_changes_error: "Campos com bordas vermelhas contem erros. " + order_back_to_cart: Voltar ao Carrinho + bom_tip: "Use esta página para alterar as quantidades de produtos em várias encomendas. Os produtos também podem ser removidos das encomendas, se necessário." + unsaved_changes_warning: "Existem modificações não guardadas que serão perdidas se continuar." + unsaved_changes_error: "Campos com contornos vermelhos contêm erros. " products: "Produtos" products_in: "em %{oc}" products_at: "em %{distributor}" products_elsewhere: "Produtos encontrados em outros lugares" - email_welcome: "Bem vindo" - email_confirmed: "Obrigado por confirmar seu endereço" + email_welcome: "Bem-vindo" + email_confirmed: "Obrigado por confirmar o seu endereço de email." email_registered: "é agora parte de" - email_userguide_html: "O Guia de Usuário com suporte detalhado para gerenciar sua loja está aqui:" - email_admin_html: "Você pode gerenciar sua conta entrando no %{link} ou clicando na engrenagem no canto superior direito da página, e selecionando Administração." + email_userguide_html: "O Manual do Utilizador com suporte detalhado para gerir a sua loja está aqui: %{link}" + email_admin_html: "Pode gerir a sua conta entrando no %{link} ou clicando na roda dentada no canto superior direito da página, e selecionando Administração." email_community_html: "Também temos um fórum online para discussão sobre a plataforma e os desafios de se manter uma iniciativa de produção e consumo local. Está convidado/a a participar! Estamos constantemente a evoluir e as suas ideias vão ajudar-nos a melhorar.\n%{link}" - join_community: "Faça parte da comunidade" + join_community: "Junte-se à comunidade" email_confirmation_activate_account: "Antes de podermos activar a sua conta, precisamos de confirmar o seu endereço de email." email_confirmation_greeting: "Olá, %{contact}!" - email_confirmation_profile_created: "Um perfil para %{name} foi criado com sucesso!\nPara ativar seu Perfil precisamos que você confirme seu endereço de email." - email_confirmation_click_link: "Clique no link abaixo para confirma seu email e continuar criando seu perfil." - email_confirmation_link_label: "Confirme esse endereço de email »" + email_confirmation_profile_created: "Um perfil para %{name} foi criado com sucesso!\nPara ativar o seu Perfil precisamos que confirme este endereço de email." + email_confirmation_click_link: "Clique no link abaixo para confirmar o seu email e continuar a criar o seu perfil." + email_confirmation_link_label: "Confirme este endereço de email »" email_confirmation_help_html: "Depois de confirmar o seu email, pode aceder à conta de administração desta organização. Veja o %{link} para saber mais sobre a %{sitename} e para começar a utilizar o seu perfil ou loja online." - email_social: "Conecte-se com a gente:" + email_social: "Conecte-se connosco:" email_contact: "Envie-nos um email:" - email_signoff: "Olá," - email_signature: "%{sitename} Equipe" + email_signoff: "Obrigado," + email_signature: "Equipa %{sitename} " email_confirm_customer_greeting: "Olá %{name}, " email_confirm_customer_intro_html: "Obrigado por comprar com %{distributor}!" - email_confirm_customer_number_html: "Confirmação de pedido #%{number}" - email_confirm_customer_details_html: "Aqui estão os detalhes de pedido de %{distributor}:" + email_confirm_customer_number_html: "Confirmação de encomenda #%{number}" + email_confirm_customer_details_html: "Aqui estão os detalhes da sua encomenda com %{distributor}:" email_confirm_customer_signoff: "Atenciosamente," email_confirm_shop_greeting: "Olá %{name}, " - email_confirm_shop_order_html: "Parabéns! Você tem um novo pedido para %{distributor}:" - email_confirm_shop_number_html: "Confirmação de pedido #%{number}" + email_confirm_shop_order_html: "Parabéns! Tem uma nova encomenda para %{distributor}:" + email_confirm_shop_number_html: "Confirmação de encomenda #%{number}" email_order_summary_item: "Item" email_order_summary_quantity: "Qtd" email_order_summary_price: "Preço" @@ -1167,65 +1254,65 @@ pt: email_payment_method: "Pagando com:" email_so_placement_intro_html: "Tem uma nova encomenda com %{distributor}" email_so_placement_details_html: "Aqui estão os detalhes da sua encomenda com %{distributor}:" - email_so_placement_changes: "Infelizmente, nem todos os produtos que requisitou estão disponíveis. As quantidades originais que requisitou aparecem abaixo." + email_so_placement_changes: "Infelizmente, nem todos os produtos que requisitou estão disponíveis. As quantidades originais que requisitou aparecem riscadas abaixo." email_so_payment_success_intro_html: "Um pagamento automático foi processado para a sua encomenda com %{distributor}." email_so_placement_explainer_html: "Esta encomenda foi criada automaticamente para si." email_so_edit_true_html: "Pode fazer alterações até ao fecho das encomendas a %{orders_close_at}." email_so_edit_false_html: "Pode ver detalhes desta encomenda em qualquer momento. " email_so_contact_distributor_html: "Se tiver alguma questão pode contactar %{distributor}através de %{email}." email_so_confirmation_intro_html: "A sua encomenda com %{distributor} está agora confirmada" - email_so_confirmation_explainer_html: "Esta encomenda foi feita automaticamente para si, e já está finalizada." + email_so_confirmation_explainer_html: "Esta encomenda foi feita automaticamente para si, e está agora finalizada." email_so_confirmation_details_html: "Aqui está tudo o que precisa de saber sobre a sua encomenda com %{distributor}:" email_so_empty_intro_html: "Tentámos fazer uma nova encomenda com %{distributor}, mas tivemos alguns problemas..." - email_so_empty_explainer_html: "Infelizmente, nenhum dos produtos que requisitou estava disponível, portanto a encomenda não prosseguiu. As quantidades originais que requisitou aparecem abaixo. " + email_so_empty_explainer_html: "Infelizmente, nenhum dos produtos que requisitou estava disponível, portanto a encomenda não prosseguiu. As quantidades originais que requisitou aparecem riscadas abaixo. " email_so_empty_details_html: "Aqui estão os detalhes da encomenda que não prosseguiu com %{distributor}:" email_so_failed_payment_intro_html: "Tentámos processar um pagamento, mas tivemos alguns problemas..." email_so_failed_payment_explainer_html: "O pagamento para a sua subscrição com %{distributor} falhou devido a um problema no seu cartão de crédito. %{distributor} foi notificado desta falha no pagamento. " email_so_failed_payment_details_html: "Aqui estão os detalhes da falha fornecidos pelo portal de pagamento:" email_shipping_delivery_details: Detalhes da entrega - email_shipping_delivery_time: "Entrega " - email_shipping_delivery_address: "Endereço de entrega:" - email_shipping_collection_details: Detalhes para retirada - email_shipping_collection_time: "Pronto para retirada" - email_shipping_collection_instructions: "Instruções para retirada:" - email_special_instructions: "Suas anotações" + email_shipping_delivery_time: "Entrega em:" + email_shipping_delivery_address: "Morada de entrega:" + email_shipping_collection_details: Detalhes para levantamento + email_shipping_collection_time: "Pronto para levantamento" + email_shipping_collection_instructions: "Instruções para levantamento:" + email_special_instructions: "As suas notas:" email_signup_greeting: Olá! - email_signup_welcome: "Bem vindo a %{sitename}!" + email_signup_welcome: "Bem-vindo a %{sitename}!" email_signup_confirmed_email: "Obrigada por confirmar o seu email." - email_signup_shop_html: "Já pode fazer log in em %{link}." + email_signup_shop_html: "Agora pode fazer log in em %{link}." email_signup_text: "Obrigada por juntar-se à rede. Se é consumidor/a, vai ficar a conhecer vários produtores fantásticos, pontos de venda incríveis e comidas deliciosas! Se é produtor/a ou uma organização de consumo local, estamos felizes por tê-lo na nossa rede" - email_signup_help_html: "Dúvidas e comentários são sempre benvindos; você pode usar o botão Enviar Comentário no site, ou enviar um e-mail para %{email}" + email_signup_help_html: "Questões e comentários são sempre bem-vindos; pode usar o botão Enviar Comentário no site, ou enviar um email para %{email}" invite_email: greeting: "Olá!" invited_to_manage: "Foste convidado/a para gerir %{enterprise} em %{instance}." - confirm_your_email: "Deves ter recebido, ou receberás em breve, um email com um link de confirmação. Não vais conseguir aceder ao perfil de %{enterprise} enquanto não tiveres confirmado o teu email. " + confirm_your_email: "Deve ter recebido, ou receberá em breve, um email com um link de confirmação. Não vai conseguir aceder ao perfil de %{enterprise} enquanto não tiver confirmado o seu email. " set_a_password: "Depois ser-te-á pedido que definas uma palavra-passe antes de poderes administrar a organização." mistakenly_sent: "Não tens a certeza porque é que recebeste este email? Por favor contacta %{owner_email}para mais informação. " - producer_mail_greeting: "Querido" - producer_mail_text_before: "Agora temos todos os pedidos do cliente para a próxima entrega." - producer_mail_order_text: "Aqui está um resumo de pedidos para seus produtos:" - producer_mail_delivery_instructions: "Instruções para retirada/entrega do estoque:" + producer_mail_greeting: "Caro/a" + producer_mail_text_before: "Agora temos todos as encomendas para a próxima entrega." + producer_mail_order_text: "Aqui está um resumo das encomendas para os seus produtos:" + producer_mail_delivery_instructions: "Instruções para levantamento/entrega dos produtos:" producer_mail_signoff: "Obrigado e até breve" - shopping_oc_closed: Fechado para pedidos - shopping_oc_closed_description: "Favor aguardar até que o próximo ciclo seja aberto (ou entre em contato diretamente para saber se podemos aceitar pedidos atrasados)" - shopping_oc_last_closed: "A último ciclo fechou a %{distance_of_time} atrás" - shopping_oc_next_open: "A próximo ciclo abre em %{distance_of_time}" + shopping_oc_closed: Fechado para encomendas + shopping_oc_closed_description: "Por favor aguarde até que o próximo ciclo seja aberto (ou entre em contato connosco para saber se podemos aceitar encomendas tardias)" + shopping_oc_last_closed: "O último ciclo fechou à %{distance_of_time} atrás" + shopping_oc_next_open: "O próximo ciclo abre em %{distance_of_time}" shopping_tabs_about: "Sobre %{distributor}" - shopping_tabs_contact: "Contato" - shopping_contact_address: "Endereço" - shopping_contact_web: "Contato" + shopping_tabs_contact: "Contacto" + shopping_contact_address: "Morada" + shopping_contact_web: "Contacto" shopping_contact_social: "Seguir" shopping_groups_part_of: "é parte de:" - shopping_producers_of_hub: "produtores de %{hub}:" - enterprises_next_closing: "Próximo fechamento de pedido" + shopping_producers_of_hub: "Produtores de %{hub}:" + enterprises_next_closing: "As encomendas fecham em" enterprises_ready_for: "Pronto para" - enterprises_choose: "Escolha para quando você quer seu pedido:" + enterprises_choose: "Escolha para quando quer a sua encomenda:" maps_open: "Aberto" maps_closed: "Fechado" - hubs_buy: "Compre por:" - hubs_shopping_here: "Comprando aqui" - hubs_orders_closed: "Fechado para pedidos" - hubs_profile_only: "Somente perfil" + hubs_buy: "Comprar:" + hubs_shopping_here: "Comprar aqui" + hubs_orders_closed: "Fechado para encomendas" + hubs_profile_only: "Apenas perfil" hubs_delivery_options: "Opções de entrega" hubs_pickup: "Levantamento" hubs_delivery: "Entrega" @@ -1234,7 +1321,7 @@ pt: hubs_filter_type: "Tipo" hubs_filter_delivery: "Entrega" hubs_filter_property: "Propriedade" - hubs_matches: "Você quis dizer?" + hubs_matches: "Quis dizer?" hubs_intro: Compre na sua região hubs_distance: Mais próximo a hubs_distance_filter: "Mostrar lojas próximas a %{location}" @@ -1242,94 +1329,94 @@ pt: one: Tem encomendas com actualmente abertas para revisão. Pode fazer alterações até . other: 'Tem %{count}encomendas com %{shop} actualmente abertas para revisão. Pode fazer alterações até %{oc_close}. ' orders_changeable_orders_alert_html: Esta encomenda está confirmada, mas pode fazer alterações até %{oc_close}. - products_clear_all: Apagar tudo + products_clear_all: Limpar tudo products_showing: "Mostrando:" products_with: com products_search: "Procurar por produto ou produtor" products_loading: "Carregando produtos..." products_updating_cart: "Atualizando carrinho..." products_cart_empty: "Carrinho vazio" - products_edit_cart: "Edite seu carrinho" + products_edit_cart: "Edite o seu carrinho" products_from: de - products_change: "Nenhuma modificação a ser salva." - products_update_error: "Falha ao salvar, com os seguintes erros:" - products_update_error_msg: "Falha ao salvar." - products_update_error_data: "Falha no salvamento devido a dados inválidos:" - products_changes_saved: "Modificações salvas." - search_no_results_html: "Desculpe, nenhum resultado encontrado para %{query}. Que tal tentar outra busca?" - components_profiles_popover: "Perfis não possuem mercaods na Open Food Network, mas pode ter suas próprias lojas físicas ou online em outro endereço" + products_change: "Nenhuma modificação a ser guardada." + products_update_error: "Falha ao guardar, com os seguintes erros:" + products_update_error_msg: "Falha ao guardar." + products_update_error_data: "Falha ao guardar devido a dados inválidos:" + products_changes_saved: "Alterações gravadas." + search_no_results_html: "Desculpe, nenhum resultado encontrado para %{query}. Tente fazer outra pesquisa." + components_profiles_popover: "Os Perfis não possuem loja na Open Food Network, mas podem ter as suas próprias lojas físicas ou online noutro endereço." components_profiles_show: "Mostrar perfis" components_filters_nofilters: "Nenhum filtro" - components_filters_clearfilters: "Eliminar filtros" + components_filters_clearfilters: "Limpar filtros" groups_title: Grupos groups_headline: Grupos / Regiões - groups_text: "Todo produtor é único. Todo negócio tem algo de diferente para oferecer. Nossos grupos são coletivos de produtores, armazéns e distribuidores que compartilham algo em comum, como localização, mercado ou filosofia. Isso faz com que sua experiência de compra fique mais fácil. Explore a curadoria feita por cada um de nosso grupos. " - groups_search: "Procurar por nome ou localidade" - groups_no_groups: "Nenhum grupo encontrado. " + groups_text: "Cada produtor é único. Cada negócio tem algo de diferente para oferecer. Os nossos grupos são coletivos de produtores, centrais e distribuidores que partilham algo em comum, como localização, mercado ou filosofia. Isso faz com que a sua experiência de compra seja mais fácil. Explore a curadoria feita por cada um dos nosso grupos. " + groups_search: "Procurar por nome ou termo" + groups_no_groups: "Nenhum grupo encontrado." groups_about: "Sobre nós" - groups_producers: "Nosso produtores" - groups_hubs: "Nossas" - groups_contact_web: Contato + groups_producers: "Os nossos produtores" + groups_hubs: "Nossas centrais" + groups_contact_web: Contacto groups_contact_social: Seguir - groups_contact_address: Endereço + groups_contact_address: Morada groups_contact_email: Envie-nos um email - groups_contact_website: Visite nosso website - groups_contact_facebook: Siga + groups_contact_website: Visite o nosso website + groups_contact_facebook: Siga-nos no Facebook groups_signup_title: Inscreva-se como grupo groups_signup_headline: Inscrição de grupos groups_signup_intro: "Somos uma plataforma incrível para marketing colaborativo: a maneira mais fácil para que seus membros alcancem novos mercados. Não temos fins lucrativos, somos simples e acessíveis." groups_signup_email: Envie-nos um email groups_signup_motivation1: Transformamos o sistema alimentar de maneira justa. - groups_signup_motivation2: 'Trabalhos para isso. Somos uma organização global, sem fins lucrativos, baseada em código open source. Jogamos limpo, sem mistério. ' - groups_signup_motivation3: 'Sabemos que você tem grandes planos, e queremos ajudar. Podemos compartilhar conhecimento, redes e recurso. Sabemos que ninguém muda nada sozinho, por isso queremos você como parceiro. ' + groups_signup_motivation2: Trabalhos para isso. Somos uma organização global sem fins lucrativos, baseada em código open source. Jogamos limpo. + groups_signup_motivation3: 'Sabemos que tem grandes ideias, e queremos ajudar. Podemos partilhar conhecimento, redes e recursos. Sabemos que ninguém muda nada sozinho, por isso queremos esta parceria consigo. ' groups_signup_motivation4: Vamos até onde você está. - groups_signup_motivation5: 'Seja uma cooperativa de produtores, distribuidores, indústria ou governo local. ' - groups_signup_motivation6: 'Seja qual for o seu papel, estamos aqui para ajudar. Entre em contato com a gente e conte-nos sobre suas ideias e projetos. ' - groups_signup_motivation7: 'Queremos dar sentido para os movimentos por boa comida. ' - groups_signup_motivation8: 'Se você precisa engajar sua rede de contatos, nós oferecemos a plataforma para isso. Conectamos todos os agentes e setores envolvidos no sistema alimentar. ' - groups_signup_motivation9: 'Se você precisa de recursos, nós te conectamos a uma rede global de parceiros. ' - groups_signup_pricing: Conta de Grupos + groups_signup_motivation5: 'Pode ser uma cooperativa de produtores, distribuidores, indústria ou governo local. ' + groups_signup_motivation6: Seja qual for o seu papel, estamos prontos para ajudar. Entre em contato connosco para saber como seria ou como está a ser a implementação da Open Food Network na sua região. + groups_signup_motivation7: 'Queremos dar mais sentido aos movimentos por boa comida. ' + groups_signup_motivation8: 'É necessário envolver a sua rede, nós oferecemos uma plataforma para contacto e ação. Nós ajudamos a chegar a todos os agentes, interessados e setores. ' + groups_signup_motivation9: 'Se precisa de recursos, nós traremos a nossa epxeriência. Se precisa de cooperação, nós conectamos a uma rede global de parceiros. ' + groups_signup_pricing: Conta de Grupo groups_signup_studies: Estudos de Caso groups_signup_contact: Pronto para discutir? - groups_signup_contact_text: "Entre em contato para descobrir o que a OFN pode fazer por você" + groups_signup_contact_text: "Entre em contato para descobrir o que a OFN pode fazer por si:" groups_signup_detail: "Aqui está o detalhe. " - login_invalid: "Email ou senha inválidos" + login_invalid: "Email ou palavra-passe inválidos" modal_hubs: "Centrais de Alimentos" - modal_hubs_abstract: Nossas centrais de alimentos são o ponto de contato entre você e as pessoas que produzem sua comida! - modal_hubs_content1: 'Você pode procurar pelo mercado mais próximo, por localização ou por nome. Alguns distribuidores possuem múltiplos pontos de entrega, onde você pode retirar suas compras, e outros ainda entregam na sua casa. Cada mercado é um ponto de venda independente, e por isso as ofertas e maneira de operar podem variar de um para outro. ' - modal_hubs_content2: Você só pode comprar em uma central de alimentos por vez. + modal_hubs_abstract: As nossas centrais de alimentos são o ponto de contato entre si e as pessoas que produzem a sua comida! + modal_hubs_content1: 'Pode procurar por uma central conveniente por localização ou por nome. Algumas centrais têm múltiplos pontos de entrega, onde pode levantar as suas compras, e outros ainda entregam na sua casa. Cada central é um ponto de venda independente, e por isso as ofertas e maneira de operar podem variar de um para outro. ' + modal_hubs_content2: Só pode comprar numa central de alimentos de cada vez. modal_groups: "Grupos / Regiões" - modal_groups_content1: Essas são as organizações e relações entre as centrais que constroem a Open Food Network + modal_groups_content1: Estas são as organizações e relações entre as centrais que constroem a Open Food Network modal_groups_content2: Alguns grupos estão organizados por localização, outros por similaridades não geográficas. modal_how: "Como funciona" - modal_how_shop: Compra na Open Food Network - modal_how_shop_explained: Procure por um mercado próximo e comece suas compras! Em cada mercado você pode ver, em detalhe, quais produtos são oferecidos (você só pode comprar em um mercado de cada vez). + modal_how_shop: Compre na Open Food Network + modal_how_shop_explained: Procure por um mercado próximo e comece as suas compras! Em cada mercado pode ver, em detalhe, quais os produtos que são oferecidos (só pode comprar num mercado de cada vez). modal_how_pickup: 'Custos de coleta e entrega. ' - modal_how_pickup_explained: 'Alguns mercados entregam na sua casa, outros oferecem um local para que você mesmo retire os produtos. É possível ver quais opções estão disponíveis no perfil individual de cada um, e fazer sua escolha no momento do checkout. Provavelmente será cobrada uma taxa de entrega, que pode variar de mercado para mercado. ' + modal_how_pickup_explained: Alguns mercados entregam em sua casa, outros oferecem um local para que levante os produtos. É possível ver quais as opções que estão disponíveis no perfil individual de cada um, e fazer a sua escolha no momento do checkout. Provavelmente será cobrada uma taxa de entrega, que pode variar de mercado para mercado. modal_how_more: Saiba mais - modal_how_more_explained: "Para saber mais sobre a Open Food Network, como funciona, e se envolver:" + modal_how_more_explained: "Para saber mais sobre a Open Food Network, como funciona, e participar, visite:" modal_producers: "Produtores" - modal_producers_explained: "Nosso produtores são quem disponibilizam toda a oferta da Open Food Network" + modal_producers_explained: "Os nosso produtores são quem disponibilizam toda a comida que pode comprar na Open Food Network." producers_about: Sobre nós - producers_buy: Compre por - producers_contact: Contato + producers_buy: Comprar + producers_contact: Contacto producers_contact_phone: Ligue producers_contact_social: Seguir - producers_buy_at_html: "Compre por produtos oferecidos por %{enterprise} em:" + producers_buy_at_html: "Compre produtos oferecidos por %{enterprise} em:" producers_filter: Filtrar por producers_filter_type: Tipo - producers_filter_property: Propriedades + producers_filter_property: Propriedade producers_title: Produtores producers_headline: Encontre produtores locais producers_signup_title: Inscreva-se como produtor producers_signup_headline: Mais liberdade para quem produz comida. - producers_signup_motivation: Comercialize seus produtos e conte sua história para um mercado novo e diferenciado. Economize tempo e dinheiro em comunicação e logística. - producers_signup_send: Cadastre-se agora + producers_signup_motivation: Comercialize seus produtos e conte a sua história num mercado novo e diferenciado. Economize tempo e dinheiro em comunicação e logística. + producers_signup_send: Registe-se agora producers_signup_enterprise: Contas da Organização - producers_signup_studies: Histórias de nossos produtores - producers_signup_cta_headline: Cadastre-se agora! - producers_signup_cta_action: Cadastre-se agora - producers_signup_detail: Aqui está o detalhe/ + producers_signup_studies: Histórias do nossos produtores + producers_signup_cta_headline: Registe-se agora! + producers_signup_cta_action: Registe-se agora + producers_signup_detail: Aqui está o detalhe. products_item: Ítem products_description: Descrição products_variant: Variante @@ -1337,46 +1424,46 @@ pt: products_available: Disponível? products_producer: "Produtor" products_price: "Preço" - register_title: Registro - sell_title: "\bRegistrar" - sell_headline: "Fazer parte da Open Food Network!" - sell_motivation: "Mostre seus produtos deliciosos." + register_title: Registo + sell_title: "\bRegistar" + sell_headline: "Junte-se à Open Food Network!" + sell_motivation: "Mostre os seus produtos deliciosos." sell_producers: "Produtores" sell_hubs: "Centrais" sell_groups: "Grupos" - sell_producers_detail: "Crie um perfil para seu negócio em apenas alguns minutos. A qualquer momento você poderá fazer se tornar um mercado online e vender seus produtos diretamente ao consumidor." + sell_producers_detail: "Crie um perfil para o seu negócio em apenas alguns minutos. A qualquer momento poderá passar o seu perfil a um mercado online e vender os seus produtos diretamente ao consumidor." sell_hubs_detail: "Crie um perfil para a sua organização na OFN. A qualquer momento poderá actualizar o seu perfil para passar a incluir vários outros produtores. " sell_groups_detail: "Configure uma lista personalizada de organizações (produtores, cooperativas, lojas, etc.) para a sua região ou organização. " - sell_user_guide: "Saiba mais acessando nosso guia. " + sell_user_guide: "Saiba mais através do nosso manual de utilizador. " sell_listing_price: "Criar um perfil na OFN não custa nada. Abrir e gerir um ponto de vendas na OFN também não. Organizar um grupo de consumo na OFN é gratuito." - sell_embed: "Você também pode embutir um mercado da OFN no seu próprio site, ou construir um site específico para a sua região. " - sell_ask_services: "Pergunte-nos sobre nossos serviços." + sell_embed: "Também podemos embutir um mercado da OFN no seu próprio site, ou construir um site específico para a sua região. " + sell_ask_services: "Pergunte-nos sobre os nossos serviços." shops_title: Lojas shops_headline: A feira, transformada - shops_text: 'A colheita é feita em ciclos, a comida é produzida em ciclos, e nós fazemos nossos pedidos em ciclos. Se você encontrar um ciclo de pedidos fechado, volte em breve para tentar novamente. ' - shops_signup_title: Registre-se como uma central - shops_signup_headline: 'Um mercado de alimentos sem tamanho. ' + shops_text: A comida é produzida em ciclos, a colheita é feita em ciclos e nós fazemos encomendas em ciclos. Se encontrar um ciclo de encomendas fechado, volte em breve para tentar novamente. + shops_signup_title: Registe-se como uma central + shops_signup_headline: 'Um mercado de alimentos, não corporativo. ' shops_signup_motivation: Seja qual for o seu modelo, nós ajudamos. Se houver mudanças, estamos consigo. Somos sem fins lucrativos, independentes, e open source. Somos os parceiros de software com os quais sonhou. shops_signup_action: Junte-se agora shops_signup_pricing: Contas da Organização shops_signup_stories: Histórias dos nossos membros. - shops_signup_help: Estamos prontos para ajudar + shops_signup_help: Estamos prontos para ajudar. shops_signup_help_text: Você está a precisar de melhor retorno, de novos clientes e parceiros de logística. Está a precisar que a sua história seja contada em grupos de consumo, em retalho e à mesa de jantar. shops_signup_detail: Aqui está o detalhe. orders: Encomendas orders_fees: Taxas... - orders_edit_title: Carrinho de compras + orders_edit_title: Carrinho de Compras orders_edit_headline: O seu carrinho de compras - orders_edit_time: Pedido pronto para + orders_edit_time: Encomenda pronta para orders_edit_continue: Continuar a comprar - orders_edit_checkout: Fechar pedido + orders_edit_checkout: Finalizar compra orders_form_empty_cart: "Carrinho vazio" orders_form_subtotal: Subtotal dos produtos orders_form_admin: Administração & Logística orders_form_total: Total - orders_oc_expired_headline: Este ciclo de encomendas está fechado para pedidos - orders_oc_expired_text: "Desculpe, este ciclo de encomendas fechou há %{time} atrás. Por favor entre em contacto directamente com a sua central para saber se podem aceitar pedidos tardios." - orders_oc_expired_text_others_html: "Desculpe, este ciclo de encomendas fechou há %{time} atrás. Por favor entre em contacto directamente com a sua central para saber se podem aceitar pedidos tardios %{link}." + orders_oc_expired_headline: Este ciclo de encomendas está fechado + orders_oc_expired_text: "Desculpe, este ciclo de encomendas fechou há %{time} atrás. Por favor entre em contacto directamente com a sua central para saber se podem aceitar encomendas tardias." + orders_oc_expired_text_others_html: "Desculpe, este ciclo de encomendas fechou há %{time} atrás. Por favor entre em contacto directamente com a sua central para saber se podem aceitar encomendas tardias %{link}." orders_oc_expired_text_link: "ou veja os outros ciclos de encomendas que estão disponíveis nesta central" orders_oc_expired_email: "Email:" orders_oc_expired_phone: "Telefone:" @@ -1394,46 +1481,46 @@ pt: orders_bought_edit_button: Editar itens confirmados orders_bought_already_confirmed: "* já confirmado" orders_confirm_cancel: Tem a certeza que quer cancelar esta encomenda? - products_cart_distributor_choice: "Distribuidor para seu pedido:" + products_cart_distributor_choice: "Distribuidor para a sua encomenda:" products_cart_distributor_change: "O distribuidor para esta encomenda será alterado para %{name} se adicionar este produto ao carrinho." - products_cart_distributor_is: "O distribuidor para este pedido é %{name}." - products_distributor_error: "Favor completar seu pedido no %{link} antes de comprar com outro distribuidor." - products_oc: "Ciclo de pedido para seu pedido:" - products_oc_change: "O ciclo de pedido para esse pedido será trocada para %{name} se você adicionar este produto ao carrinho." - products_oc_is: "O ciclo de pedido para este pedido é %{name}." - products_oc_error: "Favor completar seu pedido no %{link} antes de comprar em outro ciclo de pedido." + products_cart_distributor_is: "O distribuidor para esta encomenda é %{name}." + products_distributor_error: "Por favor complete a sua encomenda em %{link} antes de comprar com outro distribuidor." + products_oc: "Ciclo de encomendas para a sua encomenda:" + products_oc_change: "O ciclo de encomendas para esta encomenda será trocado para %{name} se adicionar este produto ao carrinho." + products_oc_is: "O ciclo de encomendas para esta encomenda é %{name}." + products_oc_error: "Por favor complete a sua encomenda em %{link} antes de comprar noutro ciclo de encomendas." products_oc_current: "o seu actual ciclo de encomendas" products_max_quantity: Quantidade máxima products_distributor: Distribuidor - products_distributor_info: Quando seleccionar um distribuidor para a sua encomenda, o endereço e data de levantamento serão exibidos aqui. + products_distributor_info: Quando seleccionar um distribuidor para a sua encomenda, a morada e data de levantamento serão exibidos aqui. products_distribution_adjustment_label: "Distribuição de produto por %{distributor} para %{product}." shop_trial_expires_in: "O período de avaliação do mercado termina em " - shop_trial_expired_notice: "Boa notícia! Decidimos extender o período avaliação do mercado até segunda ordem. " - password: Senha - remember_me: Lembre-me - are_you_sure: "Tem certeza?" + shop_trial_expired_notice: "Boa notícia! Decidimos extender o período avaliação do mercado. " + password: Palavra-passe + remember_me: Lembrar meu login + are_you_sure: "Tem a certeza?" orders_open: Encomendas abertas closing: "Fechando" going_back_to_home_page: "Voltando à pagina inicial" creating: Criando updating: Atualizando - failed_to_create_enterprise: "Falha ao criar a sua organização" + failed_to_create_enterprise: "Falha ao criar a sua organização." failed_to_create_enterprise_unknown: "Falha ao criar a sua organização. \nPor favor verifique se todos os campos foram preenchidos correctamente." failed_to_update_enterprise_unknown: "Falha ao actualizar a sua organização. \nPor favor verifique se todos os campos foram preenchidos correctamente." enterprise_confirm_delete_message: "Isto também vai apagar o %{product} que esta organização fornece. Tem a certeza que deseja continuar?" order_not_saved_yet: "A sua encomenda ainda não foi guardada. Só mais uns minutinhos para teminar! " filter_by: "Filtrar por" - hide_filters: "Esconder filtros" + hide_filters: "Ocultar filtros" one_filter_applied: "1 filtro aplicado" x_filters_applied: "filtros aplicados" - submitting_order: "A processar o seu pedido: favor aguarde" - confirm_hub_change: "Tem certeza? Isso irá mudar a central selecionada e remover todos os ítens do carrinho de compras." + submitting_order: "A processar a sua encomenda: por favor aguarde" + confirm_hub_change: "Tem a certeza? Isto mudará a central selecionada e removerá todos os ítens do seu carrinho de compras." confirm_oc_change: "Tem a certeza? Isto mudará o ciclo de encomendas seleccionado e removerá todos os itens do carrinho de compras." location_placeholder: "Digite uma localidade..." - error_required: "Não pode ser vazio" - error_number: "Precisa ser um número" - error_email: "Precisa ser um endereço de email" - error_not_found_in_database: "%{name} não foi encontrado na base de dados." + error_required: "não pode ser vazio" + error_number: "precisa ser um número" + error_email: "precisa ser um endereço de email" + error_not_found_in_database: "%{name} não foi encontrado na base de dados" error_no_permission_for_enterprise: "\"%{name}\": não tem permissão para gerir produtos desta organização" item_handling_fees: "Taxas de Manejo do Produto (incluídas no total do produto)" january: "Janeiro" @@ -1449,19 +1536,20 @@ pt: november: "Novembro" december: "Dezembro" email_not_found: "Endereço de email não encontrado" - email_required: "Você precisa providenciar um endereço de email" + email_unconfirmed: "Tem de confirmar o seu endereço de email antes de poder redefinir a sua palavra-passe." + email_required: "Precisa definir um endereço de email" logging_in: "Fazendo o login, aguarde um momento" - signup_email: "Seu email" - choose_password: "Escolha uma senha" - confirm_password: "Confirme a senha" - action_signup: "Cadastre-se agora" + signup_email: "Email" + choose_password: "Escolha uma palavra-passe" + confirm_password: "Confirme a palavra-passe" + action_signup: "Registe-se agora" welcome_to_ofn: "Bem-vindo à Open Food Network!" - signup_or_login: "Faça seu cadastro ou login para começar" - have_an_account: "Já possui um conta?" - action_login: "Entrar agora" - forgot_password: "Esqueceu sua senha?" - password_reset_sent: "Um email foi enviado com instruções para resetar sua senha!" - reset_password: "Resetar password" + signup_or_login: "Comece por fazer login ou registar-se" + have_an_account: "Já possui uma conta?" + action_login: "Entrar agora." + forgot_password: "Esqueceu-se da sua palavra-passe?" + password_reset_sent: "Foi enviado um email com instruções para redefinir a sua palavra-passe!" + reset_password: "Redefinir palavra-passe" who_is_managing_enterprise: "Quem é responsável por gerir %{enterprise}? " update_and_recalculate_fees: "Actualizar e Recalcular Taxas" enterprise: @@ -1470,32 +1558,32 @@ pt: steps: details: title: 'Detalhes' - headline: "Vamos lá começar" + headline: "Vamos começar" enterprise: "Primeiro precisamos de saber um pouco sobre a sua organização:" producer: "Primeiro precisamos de saber um pouco sobre a sua quinta:" enterprise_name_field: "Nome da organização:" - producer_name_field: "Nome da quinta:" + producer_name_field: "Nome da Quinta:" producer_name_field_placeholder: "ex: Quinta da Liliana Espectacular" producer_name_field_error: "Por favor escolha um nome único para a sua organização" - address1_field: "Endereço (primeira linha):" + address1_field: "Morada linha 1:" address1_field_placeholder: "ex: Rua das Framboesas 123" - address1_field_error: "Por favor indique um endereço" - address2_field: "Endereço (segunda linha):" + address1_field_error: "Por favor indique uma morada" + address2_field: "Morada linha 2:" suburb_field: "Localidade:" - suburb_field_placeholder: "ex: Lagarteiro" + suburb_field_placeholder: "ex: Famalicão" suburb_field_error: "Por favor indique uma localidade" postcode_field: "Código postal:" - postcode_field_placeholder: "ex: 3070" + postcode_field_placeholder: "ex: 4000-125" postcode_field_error: "Código postal obrigatório" - state_field: "Região" - state_field_error: "Estado obrigatório" + state_field: "Região:" + state_field_error: "Campo obrigatório" country_field: "País:" country_field_error: "Por favor selecione um país" contact: title: 'Contacto' - contact_field: 'Contacto principal' + contact_field: 'Contacto Principal' contact_field_placeholder: 'Nome do contacto' - contact_field_required: "É obrigatório indicar um contacto principal" + contact_field_required: "É obrigatório indicar um contacto principal." email_field: 'Endereço de email' email_field_placeholder: 'ex: liliana@daquinta.com' phone_field: 'Número de telefone' @@ -1507,8 +1595,8 @@ pt: yes_producer: "Sim, sou produtor/a" no_producer: "Não, não sou produtor/a" producer_field_error: "Por favor escolha uma opção. É produtor/a?" - yes_producer_help: "Produtores/as são quem faz coisas deliciosas para comer e/ou beber. És produtor/a se plantas, crias, fermentas, amassas, munges ou moldas." - no_producer_help: "Se não és produtor/a, provavelmente és alguém que vende e distribui alimentos. Podes ser uma cooperativa, um grupo de consumo, um hub, do retalho, ou outros." + yes_producer_help: "Produtores/as são quem faz coisas deliciosas para comer e/ou beber. É produtor/a se planta, cria, fermenta, amassa, munge ou molda algo." + no_producer_help: "Se não é produtor/a, é provavelmente alguém que vende e distribui alimentos. Pode ser uma cooperativa, um grupo de consumo, um distribuidor, um retalhista, ou outro." about: title: 'Sobre' images: @@ -1517,8 +1605,8 @@ pt: title: 'Social' enterprise_contact: "Contacto Principal" enterprise_contact_placeholder: "Nome do contacto" - enterprise_contact_required: "É preciso adicionar um contacto principal" - enterprise_email_address: "Endereço de e-mail" + enterprise_contact_required: "É obrigatório indicar um contacto principal." + enterprise_email_address: "Endereço de email" enterprise_email_placeholder: "ex: liliana@daquinta.com" enterprise_phone: "Número de telefone" enterprise_phone_placeholder: "ex: 97 1234 5678" @@ -1529,103 +1617,103 @@ pt: limit_reached_text: "Chegou ao número limite de organizações que pode ter na " limit_reached_action: "Voltar à página principal" select_promo_image: "Passo 3. Selecionar Imagem Promocional" - promo_image_tip: "Tamanho preferencial: 1200x260px" + promo_image_tip: "Dica: mostrada como banner, o tamanho preferido é 1200x260px" promo_image_label: "Escolher uma imagem promocional" action_or: "OU" - promo_image_drag: "Arraste e solte sua imagem aqui" - review_promo_image: "Passo 4. Avalie Sua Imagem Promocional" - review_promo_image_tip: "Dica: para melhores resultados, sua imagem deve preencher o espaço disponível" - promo_image_placeholder: "Seu logo aparecerá aqui para avaliação assim que for carregado" + promo_image_drag: "Arraste e solte a sua imagem promocional aqui" + review_promo_image: "Passo 4. Reveja a sua Imagem Promocional" + review_promo_image_tip: "Dica: para melhores resultados, a sua imagem promocional deve preencher o espaço disponível" + promo_image_placeholder: "A sua imagem de perfil aparecerá aqui para revisão assim que for carregada" uploading: "Carregando..." select_logo: "Passo 1. Selecionar imagem de perfil" - logo_tip: "Dica: Imagens quadradas funcionam melhor, com mínimo de 300x300px" + logo_tip: "Dica: Imagens quadradas funcionam melhor, de preferência com pelo menos 300x300px" logo_label: "Escolha uma imagem de perfil" - logo_drag: "Arraste e solte sua imagem aqui" - review_logo: "Passo 2. Avalie sua imagem de perfil" - review_logo_tip: "Dica: para melhores resultados, sua imagem deve preencher o espaço disponível" - logo_placeholder: "Seu logo aparecerá aqui para avaliação assim que for carregado`" + logo_drag: "Arraste e solte a sua imagem de perfil aqui" + review_logo: "Passo 2. Reveja a sua imagem de perfil" + review_logo_tip: "Dica: para melhores resultados, a sua imagem deve preencher o espaço disponível" + logo_placeholder: "A sua imagem de perfil aparecerá aqui para revisão assim que for carregada" enterprise_about_headline: "Boa!" - enterprise_about_message: "Vamos inserir mais detalhes sobre" - enterprise_success: "Sucesso! %{enterprise} foi adicionada a Open Food Network" + enterprise_about_message: "Agora vamos inserir os detalhes sobre" + enterprise_success: "Sucesso! %{enterprise} foi adicionada à Open Food Network" enterprise_description: "Descrição Curta" enterprise_description_placeholder: "Uma frase curta que descreva a sua organização " - enterprise_long_desc: "Descrição completa" + enterprise_long_desc: "Descrição Longa" enterprise_long_desc_placeholder: "Esta é a oportunidade para contar a história da sua organização - o quê que a torna diferente e maravilhosa? Sugerimos um parágrafo com 600 caracteres ou 150 palavras, no máximo. " enterprise_long_desc_length: "%{num} caracteres / recomendamos até 600" enterprise_limit: Limite da Organização - enterprise_abn: "ABN" + enterprise_abn: "NIPC" enterprise_abn_placeholder: "ex: 99 123 456 789" - enterprise_acn: "ACN" + enterprise_acn: "NIF" enterprise_acn_placeholder: "ex: 123 456 789" - enterprise_tax_required: "Você precisa fazer uma seleção." + enterprise_tax_required: "É obrigatório fazer uma seleção." enterprise_final_step: "Último passo!" enterprise_social_text: "Como é que as pessoas podem encontrar a %{enterprise} online?" website: "Website" - website_placeholder: "eg. openfoodnetwork.com.br" + website_placeholder: "eg. openfoodnetwork.com" facebook: "Facebook" - facebook_placeholder: "ex. www.facebook.com/suapagina" + facebook_placeholder: "ex. www.facebook.com/asuapagina" linkedin: "LinkedIn" - linkedin_placeholder: "ex. www.linkedin.com/seunome" + linkedin_placeholder: "ex. www.linkedin.com/oseunome" twitter: "Twitter" twitter_placeholder: "ex. @conta_twitter" instagram: "Instagram" instagram_placeholder: "ex. @conta_instagram" registration_greeting: "Olá!" - registration_intro: "Você pode criar um perfil para seu Produtor ou Distribuidor" + registration_intro: "Agora pode criar um perfil para seu Produtor ou Distribuidor" registration_action: "Vamos começar!" - registration_checklist: "Você vai precisar" + registration_checklist: "Vai precisar de" registration_time: "5-10 minutos" registration_enterprise_address: "Morada da organização" - registration_contact_details: "Informações para contato" - registration_logo: "Sua imagem de perfil" - registration_promo_image: "Imagem horizontal para seu perfil" - registration_about_us: "'Sobre Nós'" - registration_outcome_headline: "O que eu ganho?" - registration_outcome1_html: "Seu perfil ajuda as pessoas a te encontrarem e entrarem em contato com você na Open Food Network" + registration_contact_details: "Informações para contacto" + registration_logo: "Imagem de perfil" + registration_promo_image: "Imagem horizontal para o seu perfil" + registration_about_us: "Texto 'Sobre Nós'" + registration_outcome_headline: "O que ganho?" + registration_outcome1_html: "O seu perfil ajuda as pessoas a o encontrarem e entrarem em contacto consigo na Open Food Network" registration_outcome2: "Use este espaço para contar a história da sua organização, de forma a gerar ligações à sua presença social e online. " - registration_outcome3: "Esse é também o primeiro passo para começar a comercializar na Open Food Network, ou abrir uma loja online" - registration_finished_headline: "Pronto!" + registration_outcome3: "É também o primeiro passo para comercializar na Open Food Network, ou abrir uma loja online." + registration_finished_headline: "Terminado!" registration_finished_thanks: "Obrigado por preencher os detalhes de %{enterprise}." - registration_finished_login: "Pode alterar ou actualizar as informações da sua organização a qualquer momento fazendo login na Open Food Network e entrando na secção Admin." + registration_finished_login: "Pode alterar ou actualizar as informações da sua organização a qualquer momento fazendo login na Open Food Network e entrando na secção de Administração." registration_finished_action: "Página principal" registration_contact_name: 'Nome do contacto' registration_type_headline: "Último passo para adicionar %{enterprise}!" - registration_type_question: "Você é um produtor?" - registration_type_producer: "Sim, sou um produtor" - registration_type_no_producer: "Não, não sou um produtor" - registration_type_error: "Favor escolher um. Você é um produtor?" - registration_type_producer_help: "Produtores fazem coisas deliciosas de comer e beber. " - registration_type_no_producer_help: "Se você não é um produtor, você provavelmente é alguém que vende e distribui comida. Você pode ser uma central, cooperativa, grupo de compras, lojista, etc." - create_profile: "Crir perfil" + registration_type_question: "É produtor/a?" + registration_type_producer: "Sim, sou produtor/a" + registration_type_no_producer: "Não, não sou produtor/a" + registration_type_error: "Por favor escolha uma opção. É produtor/a?" + registration_type_producer_help: "Produtores/as são quem faz coisas deliciosas para comer e/ou beber. É produtor/a se planta, cria, fermenta, amassa, munge ou molda algo." + registration_type_no_producer_help: "Se não é produtor/a, é provavelmente alguém que vende e distribui alimentos. Pode ser uma cooperativa, um grupo de consumo, um distribuidor, um retalhista, ou outro." + create_profile: "Criar perfil" registration_images_headline: "Obrigado!" - registration_images_description: "Vamos adicionar umas belas imagens para seu perfil ficar lindão!" + registration_images_description: "Vamos adicionar umas boas imagens para o seu perfil ficar impecável!" registration_detail_headline: "Vamos Começar" registration_detail_enterprise: "Primeiro precisamos saber mais sobre a sua organização:" registration_detail_producer: "Primeiro precisamos saber mais sobre sua produção:" registration_detail_name_enterprise: "Nome da Organização:" - registration_detail_name_producer: "Nome da Produção" - registration_detail_name_placeholder: "ex. Fazenda da Nina" + registration_detail_name_producer: "Nome da Quinta" + registration_detail_name_placeholder: "ex: Quinta da Liliana Espectacular" registration_detail_name_error: "Escolha um nome único para a sua organização" - registration_detail_address1: "Linha de endereço 1:" - registration_detail_address1_placeholder: "ex. Rua Mármore, 123" - registration_detail_address1_error: "Favor inserir um endereço" - registration_detail_address2: "Linha de endereço 2:" - registration_detail_suburb: "Localidade" - registration_detail_suburb_placeholder: "ex. Contagem" - registration_detail_suburb_error: "Favor inserir uma localidade" + registration_detail_address1: "Morada linha 1:" + registration_detail_address1_placeholder: "ex: Rua das Framboesas 123" + registration_detail_address1_error: "Por favor indique uma morada" + registration_detail_address2: "Morada linha 2:" + registration_detail_suburb: "Localidade:" + registration_detail_suburb_placeholder: "ex. Famalicão" + registration_detail_suburb_error: "Por favor indique uma localidade" registration_detail_postcode: "Código postal" - registration_detail_postcode_placeholder: "ex. 3070" - registration_detail_postcode_error: "Código postal requisitado" - registration_detail_state: "Estado" - registration_detail_state_error: "É obrigatório inserir o Estado" - registration_detail_country: "País" - registration_detail_country_error: "Favor inserir um país" + registration_detail_postcode_placeholder: "ex: 4000-125" + registration_detail_postcode_error: "Código postal obrigatório" + registration_detail_state: "Região:" + registration_detail_state_error: "Campo obrigatório" + registration_detail_country: "País:" + registration_detail_country_error: "Por favor selecione um país" shipping_method_destroy_error: "Este método de envio não pode ser apagado porque está referenciado por uma encomenda: %{number}." accounts_and_billing_task_already_running_error: "Já está a decorrer uma tarefa, por favor aguarde que esteja terminada" - accounts_and_billing_start_task_notice: "Tarefa em fila de espera" + accounts_and_billing_start_task_notice: "Tarefa colocada em fila" fees: "Taxas" item_cost: "Custo da unidade" - bulk: "Atacado" + bulk: "Por Atacado" shop_variant_quantity_min: "mín." shop_variant_quantity_max: "max." follow: "Seguir" @@ -1633,61 +1721,61 @@ pt: change_shop: "Mudar loja para:" shop_at: "Compre agora em:" price_breakdown: "Preço detalhado" - admin_fee: "Taxa de manejo" + admin_fee: "Taxa de administração" sales_fee: "Taxa de venda" packing_fee: "Taxa de embalagem" transport_fee: "Taxa de transporte" - fundraising_fee: "Taxa de poupança" + fundraising_fee: "Taxa de financiamento" price_graph: "Gráfico de preços" included_tax: "Taxas incluídas" - balance: "Balanço" + balance: "Saldo" transaction: "Transação" - transaction_date: "DataData" - payment_state: "Status do Pagamento" - shipping_state: "Status da entrega" + transaction_date: "Data" + payment_state: "Estado do Pagamento" + shipping_state: "Estado do envio" value: "Valor" - balance_due: "saldo devedor" + balance_due: "Saldo pendente" credit: "Crédito" Paid: "Pago" Ready: "Pronto" ok: OK not_visible: não visível you_have_no_orders_yet: "Ainda não tem encomendas" - running_balance: "Balanço corrente" - outstanding_balance: "Saldo devedor" + running_balance: "Saldo corrente" + outstanding_balance: "Saldo pendente" admin_entreprise_relationships: "Relações da Organização" admin_entreprise_relationships_everything: "Tudo" admin_entreprise_relationships_permits: "permite" - admin_entreprise_relationships_seach_placeholder: "Busca" + admin_entreprise_relationships_seach_placeholder: "Procurar" admin_entreprise_relationships_button_create: "Criar" admin_entreprise_groups: "Grupos da Organização" admin_entreprise_groups_name: "Nome" - admin_entreprise_groups_owner: "Dono" + admin_entreprise_groups_owner: "Proprietário" admin_entreprise_groups_on_front_page: "Na página inicial?" admin_entreprise_groups_entreprise: "Organizações" - admin_entreprise_groups_data_powertip: "Usuário responsável pelo grupo" + admin_entreprise_groups_data_powertip: "O utilizador responsável por este grupo." admin_entreprise_groups_data_powertip_logo: "Esse é o logo do grupo" - admin_entreprise_groups_data_powertip_promo_image: "Essa é a imagem que aparecerá no topo do perfil do Grupo" - admin_entreprise_groups_contact: "Contato" + admin_entreprise_groups_data_powertip_promo_image: "Esta imagem aparecerá no topo do perfil do Grupo" + admin_entreprise_groups_contact: "Contacto" admin_entreprise_groups_contact_phone_placeholder: "ex: 987654321" admin_entreprise_groups_contact_address1_placeholder: "ex: Rua Alta, 123" admin_entreprise_groups_contact_city: "Localidade" - admin_entreprise_groups_contact_city_placeholder: "ex. Contagem" + admin_entreprise_groups_contact_city_placeholder: "ex. Famalicão" admin_entreprise_groups_contact_zipcode: "Código Postal " - admin_entreprise_groups_contact_zipcode_placeholder: "ex. 3078" - admin_entreprise_groups_contact_state_id: "Estadi" + admin_entreprise_groups_contact_zipcode_placeholder: "ex: 4000-125" + admin_entreprise_groups_contact_state_id: "Região" admin_entreprise_groups_contact_country_id: "País" admin_entreprise_groups_web: "Recursos Web" admin_entreprise_groups_web_twitter: "ex. @nome_perfil" - admin_entreprise_groups_web_website_placeholder: "ex. www.cogumelos.com.br" + admin_entreprise_groups_web_website_placeholder: "ex. www.cogumelos.pt" admin_order_cycles: "Ciclos de Encomendas do Administrador" open: "Aberto" close: "Fechado" create: "Criar" search: "Procurar" supplier: "Fornecedor" - product_name: "Nome do produto" - product_description: "Descrição do produto" + product_name: "Nome do Produto" + product_description: "Descrição do Produto" units: "Tamanho unitário" coordinator: "Coordenador" distributor: "Distribuidor" @@ -1696,13 +1784,17 @@ pt: delivery_instructions: Instruções de Entrega delivery_method: Método de Entrega fee_type: "Tipo de Taxa" - tax_category: "Categoria de taxa" + tax_category: "Categoria de Imposto" calculator: "Calculadora" calculator_values: "Valores da calculadora" flat_percent_per_item: "Percentual (por unidade)" - new_order_cycles: "Novos Ciclo de Encomendas" - new_order_cycle: "Novo ciclo de encomendas" - select_a_coordinator_for_your_order_cycle: "Escolher um coordenador para o seu ciclo de encomendas" + flat_rate_per_item: "Taxa Fixa (por item)" + flat_rate_per_order: "Taxa fixa (por encomenda)" + flexible_rate: "Taxa flexível" + price_sack: "Saco de Preços" + new_order_cycles: "Novos Ciclos de Encomendas" + new_order_cycle: "Novo Ciclo de Encomendas" + select_a_coordinator_for_your_order_cycle: "Escolha um coordenador para o seu ciclo de encomendas" notify_producers: 'Notificar produtores' edit_order_cycle: "Editar Ciclo de Encomendas" roles: "Papeis" @@ -1711,48 +1803,43 @@ pt: add_producer_property: "Adicionar produtor" in_progress: "Em andamento" started_at: "Começou em " - queued: "Aguardando" + queued: "Em fila" scheduled_for: "Agendado para" - customers: "Clientes" - please_select_hub: "Favor selecionar uma Central" - loading_customers: "Carregando Clientes" - no_customers_found: "Nenhum cliente encontrado" + customers: "Consumidores/as" + please_select_hub: "Por favor selecione uma Central" + loading_customers: "Carregando Consumidores" + no_customers_found: "Nenhum consumidor encontrado" go: "Ir" hub: "Central" producer: "Produtor" product: "Produto" price: "Preço" on_hand: "Disponível" - save_changes: "Salvar Modificações" - order_saved: "Pedido guardado" + save_changes: "Guardar Modificações" + order_saved: "Encomenda Guardada" no_products: Sem Produtos spree_admin_overview_enterprises_header: "As minhas Organizações" spree_admin_overview_enterprises_footer: "GERIR AS MINHAS ORGANIZAÇÕES" spree_admin_enterprises_hubs_name: "Nome" spree_admin_enterprises_create_new: "CRIAR NOVA" - spree_admin_enterprises_shipping_methods: "Métodos de Entrega" + spree_admin_enterprises_shipping_methods: "Métodos de Envio" spree_admin_enterprises_fees: "Taxas da Organização" spree_admin_enterprises_none_create_a_new_enterprise: "CRIAR UMA NOVA ORGANIZAÇÃO" spree_admin_enterprises_none_text: "Ainda não tem nenhuma organização" - spree_admin_enterprises_producers_name: "Nome" - spree_admin_enterprises_producers_total_products: "Total de Produtos" - spree_admin_enterprises_producers_active_products: "Produtos Ativos" - spree_admin_enterprises_producers_order_cycles: "Produtos em ciclos de pedidos" spree_admin_enterprises_tabs_hubs: "CENTRAIS" - spree_admin_enterprises_tabs_producers: "PRODUTORES" - spree_admin_enterprises_producers_manage_products: "GERENCIAR PRODUTOS" - spree_admin_enterprises_any_active_products_text: "Você ainda não tem nenhum produto ativo." + spree_admin_enterprises_producers_manage_products: "GERIR PRODUTOS" + spree_admin_enterprises_any_active_products_text: "Não tem nenhum produto ativo." spree_admin_enterprises_create_new_product: "CRIAR UM NOVO PRODUTO" - spree_admin_single_enterprise_alert_mail_confirmation: "Favor confirmar o endereço de email para" - spree_admin_single_enterprise_alert_mail_sent: "Enviamos um e-mail para" - spree_admin_overview_action_required: "Medida Solicitada" - spree_admin_overview_check_your_inbox: "Por favor, cheque sua caixa de entrada para obter mais instruções. Obrigada!" - spree_admin_unit_value: Valor unitário + spree_admin_single_enterprise_alert_mail_confirmation: "Por favor confirme o endereço de email para" + spree_admin_single_enterprise_alert_mail_sent: "Enviamos um email para" + spree_admin_overview_action_required: "Ação Requerida" + spree_admin_overview_check_your_inbox: "Por favor, verifique a sua caixa de correio para mais instruções. Obrigada!" + spree_admin_unit_value: Valor Unitário spree_admin_unit_description: Descrição Unitária spree_admin_variant_unit: Unidade variante spree_admin_variant_unit_scale: Escala de unidade variante spree_admin_supplier: Fornecedor - spree_admin_product_category: Categoria de produto + spree_admin_product_category: Categoria de Produto spree_admin_variant_unit_name: Nome de unidade variante change_package: "Modificar Embalagem" spree_admin_single_enterprise_hint: "Dica: Para permitir que as pessoas te encontrem, ative sua visibilidade em" @@ -1762,55 +1849,55 @@ pt: spree_order_availability_error: "O distribuidor ou ciclo de encomendas não pode fornecer os produtos do seu carrinho" spree_order_populator_error: "Esse distribuidor ou ciclo de encomendas não pode fornecer todos os produtos do seu carrinho. Por favor escolha outro." spree_order_populator_availability_error: "Esse produto não está disponível no distribuidor ou ciclo de encomendas selecionado." - spree_distributors_error: "Tem de selecionar pelo menos um Hub." + spree_distributors_error: "Tem de selecionar pelo menos uma Central" spree_user_enterprise_limit_error: "^%{email} não tem autorização para ter mais organizações (o limite é %{enterprise_limit})." spree_variant_product_error: tem de ter pelo menos uma variante - your_profil_live: "Seu perfil online" - on_ofn_map: "O mapa da Open Food Network" + your_profil_live: "O seu perfil online" + on_ofn_map: "no mapa da Open Food Network" see: "Ver" live: "online" - manage: "Gerenciar" - resend: "Re-enviar" + manage: "Gerir" + resend: "Reenviar" trial: Experiência - add_and_manage_products: "Adicionar e gerenciar produtos" + add_and_manage_products: "Adicionar e gerir produtos" add_and_manage_order_cycles: "Adicionar e gerir ciclos de encomendas" manage_order_cycles: "Gerir ciclos de encomendas" - manage_products: "Gerenciar produtos" + manage_products: "Gerir produtos" edit_profile_details: "Editar detalhes de perfil " - edit_profile_details_etc: "Modificar a descrição dos seu perfil, imagem, etc." - order_cycle: "Ciclo de Pedidos" - order_cycles: "Ciclos de pedidos" + edit_profile_details_etc: "Modificar o seu perfil: descrição, imagem, etc." + order_cycle: "Ciclo de Encomendas" + order_cycles: "Ciclos de Encomendas" enterprises: "Organizações" enterprise_relationships: "Relações da Organização" - remove_tax: "Remover taxa" + remove_tax: "Remover imposto" enterprise_terms_of_service: "Termos de Serviço da Organização" enterprises_require_tos: "As organizações têm de aceitar os Termos de Serviço" enterprise_tos_link: "Ligação para Termos de Serviço da Organização" enterprise_tos_message: "Queremos trabalhar com pessoas que partilham os nossos objectivos e valores. Por isso pedimos às organizações novas que concordem com os nossos" enterprise_tos_link_text: "Termos de Serviço." enterprise_tos_agree: "Concordo com os Termos e Serviço acima" - tax_settings: "Configurações de Taxas" - products_require_tax_category: "produtos necessitam uma categoria de taxa" - admin_shared_address_1: "Endereço" - admin_shared_address_2: "Endereço (cont.)" + tax_settings: "Configurações de Impostos" + products_require_tax_category: "produtos necessitam uma categoria de imposto" + admin_shared_address_1: "Morada" + admin_shared_address_2: "Morada (cont.)" admin_share_city: "Cidade" admin_share_zipcode: "Código Postal " admin_share_country: "País" - admin_share_state: "Estadi" + admin_share_state: "Região" hub_sidebar_hubs: "Centrais" hub_sidebar_none_available: "Nada Disponível" hub_sidebar_manage: "Gerir" - hub_sidebar_at_least: "Ao menos uma central deve ser selecionada" + hub_sidebar_at_least: "Tem de selecionar pelo menos uma central" hub_sidebar_blue: "azul" hub_sidebar_red: "vermelho" shop_trial_in_progress: "O período de avaliação do mercado termina em %{days}." report_customers_distributor: "Distribuidor" report_customers_supplier: "Fornecedor" - report_customers_cycle: "Ciclo de Pedidos" - report_customers_type: "Relatar Tipo" - report_customers_csv: "Fazer download como csv" + report_customers_cycle: "Ciclo de Encomendas" + report_customers_type: "Tipo de Relatório" + report_customers_csv: "Descarregar como csv" report_producers: "Produtores:" - report_type: "Relatar Tipo:" + report_type: "Tipo de Relatório" report_hubs: "Centrais:" report_payment: "Métodos de Pagamento" report_distributor: "Distribuidor:" @@ -1818,12 +1905,12 @@ pt: report_itemised_payment: 'Totais dos Pagamentos Discriminados' report_payment_totals: 'Totais dos Pagamanetos' report_all: 'todos' - report_order_cycle: "Ciclo de Pedidos:" + report_order_cycle: "Ciclo de Encomendas:" report_entreprises: "Organizações:" - report_users: "Usuários:" + report_users: "Utilizadores:" report_tax_rates: Taxas de imposto - report_tax_types: Tipos e imposto - report_header_order_cycle: Ciclo de encomendas + report_tax_types: Tipos de imposto + report_header_order_cycle: Ciclo de Encomendas report_header_user: Utilizador report_header_email: Email report_header_status: Status @@ -1833,13 +1920,13 @@ pt: report_header_phone: Telefone report_header_suburb: Localidade report_header_address: Morada - report_header_billing_address: Morada de facturação + report_header_billing_address: Morada de faturação report_header_relationship: Relação report_header_hub: Hub - report_header_hub_address: Morada do Hub - report_header_to_hub: Para o Hub - report_header_hub_code: Código do Hub - report_header_code: CódigoCódigo + report_header_hub_address: Morada da Central + report_header_to_hub: Para a Central + report_header_hub_code: Código da Central + report_header_code: Código report_header_paid: Pago? report_header_delivery: Entrega? report_header_shipping: Envio @@ -1849,35 +1936,35 @@ pt: report_header_ship_street_2: Rua de Envio 2 report_header_ship_city: Cidade de Envio report_header_ship_postcode: Código postal de Envio - report_header_ship_state: Estado de Envio + report_header_ship_state: Região de Envio report_header_billing_street: Rua de Facturação report_header_billing_street_2: Rua de Facturação 2 report_header_billing_street_3: Rua de Facturação 3 report_header_billing_street_4: Rua de Facturação 4 report_header_billing_city: Cidade de Facturação - report_header_billing_postcode: Código postal de facturação - report_header_billing_state: Estado de Facturação + report_header_billing_postcode: Código Postal de Facturação + report_header_billing_state: Região de Facturação report_header_incoming_transport: Transporte vindouro - report_header_special_instructions: Instruções especiais - report_header_order_number: Número de encomenda + report_header_special_instructions: Instruções Especiais + report_header_order_number: Número da encomenda report_header_date: Data - report_header_confirmation_date: Data de confirmação + report_header_confirmation_date: Data de Confirmação report_header_tags: Etiquetas report_header_items: Itens report_header_items_total: "Total de itens %{currency_symbol}" - report_header_taxable_items_total: "Total de itens taxáveis (%{currency_symbol})" - report_header_sales_tax: "Taxa de Vendas (%{currency_symbol})" - report_header_delivery_charge: "Cobrança de entrega (%{currency_symbol})" - report_header_tax_on_delivery: "Taxa sobre a entrega (%{currency_symbol})" - report_header_tax_on_fees: "Taxa sobre tarifas (%{currency_symbol})" - report_header_total_tax: "Taxa total (%{currency_symbol})" + report_header_taxable_items_total: "Total de itens tributáveis (%{currency_symbol})" + report_header_sales_tax: "Imposto sobre Vendas (%{currency_symbol})" + report_header_delivery_charge: "Taxa de Entrega (%{currency_symbol})" + report_header_tax_on_delivery: "Imposto sobre a Entrega (%{currency_symbol})" + report_header_tax_on_fees: "Imposto sobre honorários (%{currency_symbol})" + report_header_total_tax: "Total de Impostos (%{currency_symbol})" report_header_enterprise: Organização - report_header_customer: Cliente - report_header_customer_code: Código de Cliente + report_header_customer: Consumidor + report_header_customer_code: Código de Consumidor report_header_product: Produto report_header_product_properties: Propriedades do Produto report_header_quantity: Quantidade - report_header_max_quantity: Quantidade máxima + report_header_max_quantity: Quantidade Máxima report_header_variant: Variante report_header_variant_value: Valor da Variante report_header_variant_unit: Unidade da Variante @@ -1891,102 +1978,104 @@ pt: report_header_unit: Unidade report_header_group_buy_unit_quantity: Quantidade unitária da Compra em Grupo report_header_cost: Custo - report_header_shipping_cost: Custo de envio + report_header_shipping_cost: Custo de Envio report_header_curr_cost_per_unit: Custo por Unidade Act. - report_header_total_shipping_cost: Custo total de envio + report_header_total_shipping_cost: Custo Total de Envio report_header_payment_method: Método de Pagamento report_header_sells: Vende report_header_visible: Visível report_header_price: Preço report_header_unit_size: Tamanho unitário report_header_distributor: Distribuidor - report_header_distributor_address: Endereço do distribuidor + report_header_distributor_address: Morada do distribuidor report_header_distributor_city: Cidade do distribuidor report_header_distributor_postcode: Código postal do distribuidor - report_header_delivery_address: Endereço de entrega - report_header_delivery_postcode: Código postal da entrega - report_header_bulk_unit_size: Tamanho unitário por atacado + report_header_delivery_address: Morada de Entrega + report_header_delivery_postcode: Código Postal da Entrega + report_header_bulk_unit_size: Tamanho Unitário por Atacado report_header_weight: Peso - report_header_sum_total: Soma total - report_header_date_of_order: Data de encomenda + report_header_sum_total: Soma Total + report_header_date_of_order: Data da encomenda report_header_amount_owing: Quantia em dívida - report_header_amount_paid: Quantia paga - report_header_units_required: Unidades requisitadas + report_header_amount_paid: Quantia Paga + report_header_units_required: Unidades Requisitadas report_header_remainder: Restante - report_header_order_date: Data de encomenda - report_header_order_id: ID de encomenda + report_header_order_date: Data da encomenda + report_header_order_id: Id de Encomenda report_header_item_name: Nome do item - report_header_temp_controlled_items: Itens de temperatura controlada? - report_header_customer_name: Nome do cliente - report_header_customer_email: Email do cliente - report_header_customer_phone: Telefone do cliente - report_header_customer_city: Cidade do cliente - report_header_payment_state: Estado do pagamento - report_header_payment_type: Tipo de pagamento + report_header_temp_controlled_items: Itens de Temperatura Controlada? + report_header_customer_name: Nome do Consumidor + report_header_customer_email: Email do Consumidor + report_header_customer_phone: Telefone do Consumidor + report_header_customer_city: Cidade do Consumidor + report_header_payment_state: Estado do Pagamento + report_header_payment_type: Tipo de Pagamento report_header_item_price: "Item (%{currency})" report_header_item_fees_price: "Item + Taxas (%{currency})" report_header_admin_handling_fees: "Administração & Handling (%{currency})" report_header_ship_price: "Enviar (%{currency})" report_header_pay_fee_price: "Pagar taxa (%{currency})" report_header_total_price: "Total (%{currency})" - report_header_product_total_price: "Total do produto (%{currency})" - report_header_shipping_total_price: "Total do envio (%{currency})" + report_header_product_total_price: "Total do Produto (%{currency})" + report_header_shipping_total_price: "Total do Envio (%{currency})" report_header_outstanding_balance_price: "Saldo pendente (%{currency})" report_header_eft_price: "EFT (%{currency})" report_header_paypal_price: "Paypal (%{currency})" report_header_sku: SKU report_header_amount: Quantia report_header_balance: Saldo - report_header_total_cost: "Custo total" - report_header_total_ordered: Total encomendado + report_header_total_cost: "Custo Total" + report_header_total_ordered: Total Encomendado report_header_total_max: Máx. Total report_header_total_units: Unidades Totais - report_header_sum_max_total: "Somar Máximo Total" + report_header_sum_max_total: "Soma do Máximo Total" report_header_total_excl_vat: "Total excl. impostos (%{currency_symbol})" report_header_total_incl_vat: "Total incl. impostos (%{currency_symbol})" report_header_temp_controlled: TempControlada? report_header_is_producer: Produtor/a? - report_header_not_confirmed: Não confirmado - report_header_gst_on_income: GST sobre rendimentos - report_header_gst_free_income: Rendimentos Livres de GST - report_header_total_untaxable_produce: Total de produtos não taxáveis (sem impostos) - report_header_total_taxable_produce: Total de produtos taxáveis (inclui impostos) - report_header_total_untaxable_fees: Total de taxas não taxáveis (sem impostos) - report_header_total_taxable_fees: Total de taxas taxáveis (com impostos) + report_header_not_confirmed: Não Confirmado + report_header_gst_on_income: IVA sobre rendimentos + report_header_gst_free_income: Rendimentos Livres de IVA + report_header_total_untaxable_produce: Total não tributável de produtos (sem impostos) + report_header_total_taxable_produce: Total tributável de produtos (inclui impostos) + report_header_total_untaxable_fees: Total não tributável de taxas (sem impostos) + report_header_total_taxable_fees: Total tributável de taxas (com impostos) report_header_delivery_shipping_cost: Custo de Envio e Entrega (inclui impostos) report_header_transaction_fee: Taxa de transacção (sem impostos) - report_header_total_untaxable_admin: Total de ajustamentos de administração não taxáveis (sem impostos) - report_header_total_taxable_admin: Total de ajustamentos de administração taxáveis (inclui impostos) - initial_invoice_number: "Número de factura inicial:" - invoice_date: "Data de factura:" - due_date: "Data limite:" + report_header_total_untaxable_admin: Total de ajustes de administração não taxáveis (sem impostos) + report_header_total_taxable_admin: Total de ajustes de administração taxáveis (inclui impostos) + initial_invoice_number: "Número da factura inicial:" + invoice_date: "Data da factura:" + due_date: "Data de vencimento:" account_code: "Código de conta:" - equals: "Igual a:" - contains: "contém:" + equals: "Igual a" + contains: "contém" discount: "Desconto" filter_products: "Filtrar Produtos" - delete_product_variant: "A última variante não pode ser deletada!" + delete_product_variant: "A última variante não pode ser apagada!" progress: "progresso" - saving: "Salvando.." - success: "Sucesso" + saving: "A guardar.." + success: "sucesso" failure: "falha" - unsaved_changes_confirmation: "Modificações não salvas serão perdidas. Continuar mesmo assim?" - one_product_unsaved: "Modificações para um produto permanecem não salvas." - products_unsaved: "Modificações para %{n} produtos permanecem não salvas." + unsaved_changes_confirmation: "Modificações não guardadas serão perdidas. Continuar mesmo assim?" + one_product_unsaved: "Modificações para um produto permanecem não guardadas." + products_unsaved: "Modificações para %{n} produtos permanecem não guardadas." is_already_manager: "já é um gestor!" - no_change_to_save: "Nenhuma modificação a ser salva" - users: "Usuários" + no_change_to_save: "Nenhuma modificação a ser guardada" + user_invited: "%{email} foi convidado/a para gerir esta organização" + add_manager: "Adicionar um/a utilizador/a existente" + users: "Utilizadores" about: "Sobre" images: "Imagens" web: "Web" - primary_details: "Detalhes principais" - adrdress: "Endereço" - contact: "Contato" + primary_details: "Detalhes Principais" + adrdress: "Morada" + contact: "Contacto" social: "Social" - business_details: "Detalhes do negócio" + business_details: "Detalhes do Negócio" properties: "Propriedades" shipping: "Envio" - shipping_methods: "Métodos de Entrega" + shipping_methods: "Métodos de Envio" payment_methods: "Métodos de Pagamento" payment_method_fee: "Taxa de transação" inventory_settings: "Configurações de Inventário" @@ -1997,12 +2086,12 @@ pt: validation_msg_relationship_already_established: "^Essa relação já foi estabelecida." validation_msg_at_least_one_hub: "^Pelo menos uma central deve ser selecionada" validation_msg_product_category_cant_be_blank: "^A Categoria do Produto deve ser preenchida" - validation_msg_tax_category_cant_be_blank: "^A Categoria da taxa deve ser preenchida" - validation_msg_is_associated_with_an_exising_customer: "está associado com um cliente existente" + validation_msg_tax_category_cant_be_blank: "^A Categoria de Imposto deve ser preenchida" + validation_msg_is_associated_with_an_exising_customer: "está associado com um consumidor existente" content_configuration_pricing_table: "(AFAZER: Tabela de preços)" content_configuration_case_studies: "(AFAZER: Casos de estudo)" content_configuration_detail: "(AFAZER: Detalhe)" - enterprise_name_error: "já está a ser usado. Se essa for a sua organização e quiser reclamar propriedade, por favor contacte o actual gestor de perfil em %{email}." + enterprise_name_error: "já está tomado. Se esta organização for sua e quiser solicitar direito de propriedade, ou se quiser estabelecer uma colaboração com esta organização, por favor contacte quem actualmente gere o perfil: %{email}." enterprise_owner_error: "^%{email} não tem autorização para ter mais organizações (o limite é %{enterprise_limit})." enterprise_role_uniqueness_error: "^Essa função já está presente." inventory_item_visibility_error: tem de ser verdadeiro ou falso @@ -2011,17 +2100,17 @@ pt: product_importer_products_save_error: não gravou nenhum produto com sucesso product_import_file_not_found_notice: 'O ficheiro não foi encontrado ou não pôde ser aberto' product_import_no_data_in_spreadsheet_notice: 'Não foram encontrados dados na tabela' - order_choosing_hub_notice: O seu hub foi selecionado + order_choosing_hub_notice: A sua central foi selecionada order_cycle_selecting_notice: O seu ciclo de encomendas foi selecionado. - adjustments_tax_rate_error: "^Por favor verifique se a taxa de impostos para este ajustamento está correcta." + adjustments_tax_rate_error: "^Por favor verifique se a taxa de impostos para este ajuste está correcta." active_distributors_not_ready_for_checkout_message_singular: >- O hub %{distributor_names} está listado num ciclo de encomendas activo, mas não tem métodos de envio e pagamento válidos. Enquanto isto não for definido, - os clientes não conseguirão fazer compras neste hub. + os consumidores não conseguirão fazer compras neste hub. active_distributors_not_ready_for_checkout_message_plural: >- Os hubs %{distributor_names} estão listados num ciclo de encomendas activo, mas não tem métodos de envio e pagamento válidos. Enquanto isto não for definido, - os clientes não conseguirão fazer compras nestes hubs. + os consumidores não conseguirão fazer compras nestes hubs. enterprise_fees_update_notice: As suas taxas de organização foram actualizadas. enterprise_fees_destroy_error: "Essa taxa de organização não pode ser apagada porque está referenciada numa distribuição de produto: %{id} - %{name}." enterprise_register_package_error: "Por favor selecione um pacote" @@ -2033,16 +2122,17 @@ pt: order_cycles_update_notice: 'O seu ciclo de encomendas foi actualizado.' order_cycles_bulk_update_notice: 'Os ciclos de encomendas foram actualizados.' order_cycles_clone_notice: "O seu ciclo de encomendas %{name} foi clonado." - order_cycles_email_to_producers_notice: 'Os email a enviar aos produtores foram postos na fila de espera para envio.' + order_cycles_email_to_producers_notice: 'Os email a enviar aos produtores foram postos na fila para envio.' order_cycles_no_permission_to_coordinate_error: "Nenhuma das suas organizações tem permissão para coordenar um ciclo de encomendas." order_cycles_no_permission_to_create_error: "Não tem permissão para criar um ciclo de encomendas coordenado por essa organização." back_to_orders_list: "Voltar à lista de encomendas" order_information: "Informação da Encomenda" - date_completed: "Data Conclusão" + date_completed: "Data de Conclusão" amount: "Quantia" state_names: - ready: Pronto + ready: Pronta pending: Pendente + shipped: Enviado js: saving: 'A guardar....' changes_saved: 'Alterações guardadas.' @@ -2055,7 +2145,7 @@ pt: error: Erro unavailable: Indisponível profile: Perfil - hub: Hub + hub: Central shop: Loja choose: Escolha resolve_errors: Por favor resolva os seguintes erros @@ -2064,6 +2154,9 @@ pt: enterprise_limit_reached: "Atingiu o número limite de organizações por conta. Escreva para %{contact_email} se precisar de aumentá-lo." modals: got_it: Percebi + close: "Fechar" + invite: "Convidar" + invite_title: "Convidar um/a utilizador/a não-registado/a" tag_rule_help: title: Regras de Etiquetas overview: Visão geral @@ -2086,7 +2179,7 @@ pt: saved: GUARDADO saving: A GUARDAR enterprise_package: - hub_profile: Perfil de Hub + hub_profile: Perfil de Central hub_profile_cost: "CUSTO: SEMPRE GRATUITO" hub_profile_text1: > As pessoas podem encontrá-lo e contactá-lo através da Open Food Network. @@ -2094,7 +2187,7 @@ pt: hub_profile_text2: > Ter um perfil e estabelecer ligações com o seu sistema alimentar local através da Open Food Network não custa nada. - hub_shop: Loja do hub + hub_shop: Loja da Central hub_shop_text1: > A sua organização é a espinha dorsal do seu sistema de produção e consumo local. Pode agregar produtos de outras organizações e vendê-los através @@ -2124,7 +2217,7 @@ pt: profile_only_text3: > Adicione os seus produtos na Open Food Network, permitindo assim que os hubs os associem aos seus pontos de venda. - producer_shop: Loja do produtor + producer_shop: Loja do Produtor producer_shop_text1: > Venda os seus produtos diretamente aos consumidores através da sua própria loja no Open Food Network. @@ -2132,7 +2225,7 @@ pt: Uma loja de produtor é individual, somente para comercialização de seus produtos. Se quiser vender bens produzidos em outros lugares e/ou pessoas, por favor selecione hub de produtor. - producer_hub: Hub de produtor + producer_hub: Hub de Produtor producer_hub_text1: > A sua organização é a espinha dorsal do seu sistema de produção e consumo local. Através da sua loja no Open Food Network, poderá vender os seus @@ -2148,62 +2241,62 @@ pt: get_listing: Obter uma listagem always_free: SEMPRE GRATUITO sell_produce_others: Comercialize produtos de outros - sell_own_produce: 'Venda seus próprios produtos ' - sell_both: Venda seus próprios produtos e de outros produtores + sell_own_produce: 'Venda os seus próprios produtos ' + sell_both: Venda os seus próprios produtos e de outros produtores enterprise_producer: producer: Produtor producer_text1: > Produtores/as são quem faz coisas deliciosas para comer e/ou beber. - És produtor/a se plantas, crias, fermentas, amassas, munges ou moldas. + É produtor/a se planta, cria, fermenta, amassa, munge ou molda algo. producer_text2: > Os Produtores também podem ter outras funções, como agregar alimentos de outras organizações e vendê-los através de uma loja na Open Food Network. - non_producer: Não produtor + non_producer: Não-produtor non_producer_text1: > Os Não-Produtores não produzem alimentos, o que quer dizer que não podem criar os seus próprios produtos para venda através da Open Food Network. non_producer_text2: > - Por outro lado, os não-produtores são especializados em estabelecer - ligações entre produtores e "comedores" finais, através da agregação, + Em vez disso, os não-produtores são especializados em estabelecer ligações + entre produtores e consumidores finais, através da agregação, classificação, empacotamento, venda ou distribuição de alimentos. producer_desc: Produtores/as de alimentos producer_example: 'ex: AGRICULTORES, PADEIRAS, ALQUIMISTAS, FAZEDORES' non_producer_desc: Todas as outras organizações de produção e consumo local de alimentos - non_producer_example: 'ex: Mercearias, cooperativas de consumo, grupos de compras' + non_producer_example: 'ex: mercearias, cooperativas de consumo, grupos de compras' enterprise_status: status_title: "%{name} está configurado e pronto para a acção!" severity: Severidade description: Descrição resolve: Resolver new_tag_rule_dialog: - select_rule_type: "Selecionar um tipo d regra:" + select_rule_type: "Selecionar um tipo de regra:" out_of_stock: reduced_stock_available: Stock reduzido disponível out_of_stock_text: > - Enquanto andavas às compras, o nível de stock de um ou mais produtos no - teu carrinho baixou. Aqui está o que mudou: - now_out_of_stock: está agora sem stock + Enquanto estava a comprar, o nível de stock de um ou mais produtos no seu + carrinho baixou. Aqui está o que mudou: + now_out_of_stock: está agora sem stock. only_n_remainging: "agora só tem %{num}restantes." variant_overrides: inventory_products: "Produtos de Inventário" - hidden_products: "Produtos escondidos" - new_products: "Novos produtos" + hidden_products: "Produtos Escondidos" + new_products: "Novos Produtos" reset_stock_levels: Voltar a definir os níveis de stock para os valores por defeito. changes_to: Muda para one_override: uma substituição overrides: substituições remain_unsaved: está por guardar. no_changes_to_save: Sem alterações a guardar.' - no_authorisation: "Não consegui autorização para guardar essas alterações, portanto mantêm-se por guardar." - some_trouble: "Tive alguns problemas a guardar: %{errors}" - changing_on_hand_stock: A alterar níveis de stock à mão... + no_authorisation: "Sem autorização para guardar as alterações, mantêm-se por guardar." + some_trouble: "Alguns problemas a guardar: %{errors}" + changing_on_hand_stock: A alterar níveis de stock disponíveis... stock_reset: Redefinir os stocks para valores por defeito. tag_rules: - show_hide_variants: 'Mostrar ou esconder variantes na minha montra' - show_hide_shipping: 'Mostrar ou Esconder métodos de envio à saída' - show_hide_payment: 'Mostrar ou Esconder métodos de pagamento à saída' - show_hide_order_cycles: 'Mostrar ou Esconder ciclos de encomendas na minha montra' + show_hide_variants: 'Mostrar ou esconder variantes na minha loja' + show_hide_shipping: 'Mostrar ou Esconder métodos de envio na finalização das encomendas' + show_hide_payment: 'Mostrar ou Esconder métodos de pagamento na finalização das encomendas' + show_hide_order_cycles: 'Mostrar ou Esconder ciclos de encomendas na minha loja' visible: VISÍVEL not_visible: NÃO VISÍVEL services: @@ -2215,13 +2308,13 @@ pt: edit_profile: "editar perfil" add_products_to_inventory: "acrescente produtos ao inventário" resources: - could_not_delete_customer: 'Não foi possível eliminar o cliente' + could_not_delete_customer: 'Não foi possível eliminar o consumidor' product_import: confirmation: | Isto colocará a zero o stock de todos os produtos desta organização que não estejam presentes no ficheiro carregado. order_cycles: update_success: 'O seu ciclo de encomendas foi actualizado.' - no_distributors: Não existem distribuidores neste ciclo. Este ciclo de encomendas só ficará visível para os clientes quando um distribuidor for adicionado. Gostaria de continuar a guardar este ciclo de encomendas? + no_distributors: Não existem distribuidores neste ciclo de encomendas. Este ciclo de encomendas só ficará visível para os consumidores quando um distribuidor for adicionado. Gostaria de continuar a guardar este ciclo de encomendas? enterprises: producer: "Produtor" non_producer: "Não-Produtor" @@ -2234,7 +2327,7 @@ pt: close_date_not_set: Data de fecho não definida producers: signup: - start_free_profile: "Começa com um perfil gratuito e expande quando estiveres pronto/a!" + start_free_profile: "Comece com um perfil gratuito e expanda quando estivere pronto/a!" spree: email: Email account_updated: "Conta actualizada!" @@ -2245,7 +2338,7 @@ pt: orders: invoice: issued_on: Emitido em - tax_invoice: FACTURA COM IMPOSTOS + tax_invoice: FACTURA FISCAL code: Código from: De to: Para @@ -2253,16 +2346,16 @@ pt: distribution_fields: title: Distribuição distributor: "Distribuidor:" - order_cycle: "Ciclo de encomenda:" + order_cycle: "Ciclo de encomendas:" overview: order_cycles: - order_cycles: "Ciclos de encomenda" - order_cycles_tip: "Os ciclos de encomenda determinam quando e onde é que os seus produtos estão disponíveis para os consumidores." + order_cycles: "Ciclos de Encomendas" + order_cycles_tip: "Os ciclos de encomendas determinam quando e onde é que os seus produtos estão disponíveis para os consumidores." you_have_active: zero: "Não tem nenhum ciclo de encomendas activo." one: "Tem um ciclo de encomendas activo." - other: "Tem %{count}ciclos de encomenda activos." - manage_order_cycles: "GERIR CICLOS DE ENCOMENDA" + other: "Tem %{count}ciclos de encomendas activos." + manage_order_cycles: "GERIR CICLOS DE ENCOMENDAS" payment_methods: stripe_connect: enterprise_select_placeholder: Escolher... @@ -2272,20 +2365,20 @@ pt: account_missing_msg: 'Não existem contas Stripe associadas a esta organização. ' connect_one: Ligar Uma access_revoked_msg: O acesso a esta conta Stripe foi revogado, por favor volte a ligar a sua conta. - status: EstadoEstado + status: Estado connected: Ligado - account_id: ID de conta - business_name: Nome do negócio + account_id: ID de Conta + business_name: Nome do Negócio charges_enabled: Taxas activas payments: source_forms: stripe: - no_payment_via_admin_backend: Neste momento não é possível criar pagamentos baseados em Stripe a partir do painel de administrador. + no_payment_via_admin_backend: Neste momento não é possível criar pagamentos baseados em Stripe a partir do painel de administrador products: new: title: 'Novo Produto' unit_name_placeholder: 'ex: ramos' - bulk_edit: + index: header: title: Editar Produtos por Atacado indicators: @@ -2297,16 +2390,16 @@ pt: unit: Unidade display_as: Mostrar como category: Categoria - tax_category: Categoria de Taxa + tax_category: Categoria de Imposto inherits_properties?: Herda Propriedades? available_on: Disponível em av_on: "Disp. em" products_variant: variant_has_n_overrides: "Esta variante tem %{n}substituição(ões)" new_variant: "Nova variante" - product_name: Nome do produto + product_name: Nome do Produto primary_taxon_form: - product_category: Categoria de produto + product_category: Categoria de Produto group_buy_form: group_buy: "Compra em grupo?" bulk_unit_size: Tamanho unitário por atacado @@ -2317,7 +2410,7 @@ pt: bulk_coop_supplier_report: 'Cooperativa por Atacado - Totais por Fornecedor' bulk_coop_allocation: 'Cooperativa por Atacado - Alocação' bulk_coop_packing_sheets: 'Cooperativa por Atacado - Folhas de Empacotamento' - bulk_coop_customer_payments: 'Cooperativa por Atacado - Pagamentos do Cliente' + bulk_coop_customer_payments: 'Cooperativa por Atacado - Pagamentos do Consumidor' shared: configuration_menu: stripe_connect: Ligar ao Stripe @@ -2344,7 +2437,7 @@ pt: hi: "Olá %{name}" invoice_attached_text: Pode encontrar em anexo um recibo da encomenda que fez recentemente a order_state: - address: endereço + address: morada adjustments: ajustes awaiting_return: aguardando retorno canceled: cancelado @@ -2365,18 +2458,18 @@ pt: paused: em pausa canceled: cancelado payment_states: - balance_due: saldo devedor + balance_due: saldo pendente completed: completado - checkout: fechar pedido + checkout: finalizar compra credit_owed: crédito devido - failed: falha + failed: falhou paid: pago pending: pendente processing: em processamento void: vazio invalid: inválido shipment_states: - backorder: atrasos + backorder: rutura de stock partial: parcial pending: pendente ready: pronto @@ -2384,12 +2477,12 @@ pt: user_mailer: reset_password_instructions: request_sent_text: | - Recebemos um pedido para redefinir a sua senha. + Recebemos um pedido para redefinir a sua palavra-passe. Se não fez este pedido, simplesmente ignore esta mensagem. link_text: > - Se fez este pedido, clique na ligação abaixo: + Se fez este pedido, clique no link abaixo: issue_text: | - Se o endereço URL acima não funcionar, tente copiá-lo e colá-lo directamente no browser. Se continuar a ter problemas por favor entre em contacto. + Se o endereço URL acima não funcionar, tente copiá-lo e colá-lo directamente no browser. Se continuar a ter problemas por favor entre em contacto connosco. confirmation_instructions: subject: Por favor confirme a sua conta OFN weight: Peso (por kg) @@ -2420,12 +2513,14 @@ pt: closed: Fechado until: Até past_orders: - order: Encomendar - shop: Comprar - completed_at: Completado em + order: Encomenda + shop: Loja + completed_at: Concluído Em items: Itens total: Total paid?: Pago? view: Ver + saved_cards: + delete?: Apagar? localized_number: invalid_format: tem um formato inválido. Por favor introduza um número. diff --git a/config/locales/sv.yml b/config/locales/sv.yml index 7c549a2ac3..6b8ff19728 100644 --- a/config/locales/sv.yml +++ b/config/locales/sv.yml @@ -247,6 +247,7 @@ sv: manages: hanterar products: unit_name_placeholder: 't.ex. klasar' + Search: Sök properties: property_name: Egendomens Namn inherited_property: Ärvd Egendom @@ -843,7 +844,6 @@ sv: email_admin_html: "Du kan sköta ditt konto genom att logga in %{link} eller att klicka på kuggen i det övre högra hörnet på hemsidan och välja Administration." email_community_html: "Vi har också ett internetforum för gemensamma diskussioner som är relaterade till OFN programvara och den unika utmaningen att använda ett matföretag. Vi utvecklas ständigt och dina inlägg till detta forum påverkar vad som händer i fortsättningen. %{link}" join_community: "Gå med i gemenskapen" - email_help: "Om du hamnar i svårigheter, titta i vårt FAQ, sök igenom vårt forum eller skicka en fråga till Support och du kommer att få hjälp." email_confirmation_greeting: "Hej, %{contact}!" email_confirmation_profile_created: "En profil för %{name} har skapats! För att aktivera din profil behöver vi få en bekräftelse på din e-postadress." email_confirmation_click_link: "Var vänlig att klicka på länken nedan för att bekräfta din e-postadress och fortsätt att skapa din profil." @@ -1590,7 +1590,6 @@ sv: products_unsaved: "Ändringar till %{n} produkter är fortfarande osparade." is_already_manager: "är allaredan chef!" no_change_to_save: "Ingen ändring att spara" - add_manager: "Utse en chef" users: "Användare" about: "Om oss" images: "Bilder" @@ -1856,7 +1855,7 @@ sv: other: "Du har %{count} aktiva beställningsomgångar." manage_order_cycles: "HANTERA BESTÄLLNINGSOMGÅNGAR" products: - bulk_edit: + index: header: title: 'Redigera Omfång av Produkter ' indicators: diff --git a/config/routes.rb b/config/routes.rb index 2062a1d60f..53356e2f52 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -19,6 +19,7 @@ Openfoodnetwork::Application.routes.draw do get "/register", to: "registration#index", as: :registration get "/register/auth", to: "registration#authenticate", as: :registration_auth + post "/user/registered_email", to: "spree/users#registered_email" # Redirects to global website get "/connect", to: redirect("https://openfoodnetwork.org/#{ENV['DEFAULT_COUNTRY_CODE'].andand.downcase}/connect/") @@ -133,8 +134,11 @@ Openfoodnetwork::Application.routes.draw do get '/inventory', to: 'variant_overrides#index' get '/product_import', to: 'product_import#index' + get '/product_import/guide', to: 'product_import#guide', as: 'product_import_guide' post '/product_import', to: 'product_import#import' - post '/product_import/save', to: 'product_import#save', as: 'product_import_save' + post '/product_import/validate_data', to: 'product_import#validate_data', as: 'product_import_process_async' + post '/product_import/save_data', to: 'product_import#save_data', as: 'product_import_save_async' + post '/product_import/reset_absent', to: 'product_import#reset_absent_products', as: 'product_import_reset_async' resources :variant_overrides do post :bulk_update, on: :collection @@ -255,7 +259,6 @@ Spree::Core::Engine.routes.prepend do match '/admin/reports/orders_and_fulfillment' => 'admin/reports#orders_and_fulfillment', :as => "orders_and_fulfillment_admin_reports", :via => [:get, :post] match '/admin/reports/users_and_enterprises' => 'admin/reports#users_and_enterprises', :as => "users_and_enterprises_admin_reports", :via => [:get, :post] match '/admin/reports/sales_tax' => 'admin/reports#sales_tax', :as => "sales_tax_admin_reports", :via => [:get, :post] - match '/admin/products/bulk_edit' => 'admin/products#bulk_edit', :as => "bulk_edit_admin_products" match '/admin/orders/bulk_management' => 'admin/orders#bulk_management', :as => "admin_bulk_order_management" match '/admin/reports/products_and_inventory' => 'admin/reports#products_and_inventory', :as => "products_and_inventory_admin_reports", :via => [:get, :post] match '/admin/reports/customers' => 'admin/reports#customers', :as => "customers_admin_reports", :via => [:get, :post] diff --git a/db/migrate/20170310231746_add_import_date_to_spree_variants.rb b/db/migrate/20170310231746_add_import_date_to_spree_variants.rb new file mode 100644 index 0000000000..d5acbf882e --- /dev/null +++ b/db/migrate/20170310231746_add_import_date_to_spree_variants.rb @@ -0,0 +1,5 @@ +class AddImportDateToSpreeVariants < ActiveRecord::Migration + def change + add_column :spree_variants, :import_date, :datetime + end +end diff --git a/db/migrate/20170314132401_add_import_date_to_variant_overrides.rb b/db/migrate/20170314132401_add_import_date_to_variant_overrides.rb new file mode 100644 index 0000000000..daef32ec61 --- /dev/null +++ b/db/migrate/20170314132401_add_import_date_to_variant_overrides.rb @@ -0,0 +1,5 @@ +class AddImportDateToVariantOverrides < ActiveRecord::Migration + def change + add_column :variant_overrides, :import_date, :datetime + end +end diff --git a/db/migrate/20180418025217_add_is_default_to_credit_card.rb b/db/migrate/20180418025217_add_is_default_to_credit_card.rb new file mode 100644 index 0000000000..c39b3e44dc --- /dev/null +++ b/db/migrate/20180418025217_add_is_default_to_credit_card.rb @@ -0,0 +1,5 @@ +class AddIsDefaultToCreditCard < ActiveRecord::Migration + def change + add_column :spree_credit_cards, :is_default, :boolean, default: false + end +end diff --git a/db/schema.rb b/db/schema.rb index 8ce0862f94..aff5e278c6 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -490,12 +490,13 @@ ActiveRecord::Schema.define(:version => 20180426145669) do t.string "start_year" t.string "issue_number" t.integer "address_id" - t.datetime "created_at", :null => false - t.datetime "updated_at", :null => false + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false t.string "gateway_customer_profile_id" t.string "gateway_payment_profile_id" t.integer "user_id" t.integer "payment_method_id" + t.boolean "is_default", :default => false end add_index "spree_credit_cards", ["payment_method_id"], :name => "index_spree_credit_cards_on_payment_method_id" @@ -1135,6 +1136,7 @@ ActiveRecord::Schema.define(:version => 20180426145669) do t.string "unit_description", :default => "" t.string "display_name" t.string "display_as" + t.datetime "import_date" end add_index "spree_variants", ["product_id"], :name => "index_variants_on_product_id" @@ -1254,6 +1256,7 @@ ActiveRecord::Schema.define(:version => 20180426145669) do t.string "sku" t.boolean "on_demand" t.datetime "permission_revoked_at" + t.datetime "import_date" end add_index "variant_overrides", ["variant_id", "hub_id"], :name => "index_variant_overrides_on_variant_id_and_hub_id" diff --git a/db/seeds.rb b/db/seeds.rb index 34e812799b..d54987f68f 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -50,3 +50,6 @@ def create_mail_method end create_mail_method + +spree_user = Spree::User.first +spree_user && spree_user.confirm! diff --git a/doc/img/spree_upgrade_branches.jpg b/doc/img/spree_upgrade_branches.jpg new file mode 100755 index 0000000000..f633de8c49 Binary files /dev/null and b/doc/img/spree_upgrade_branches.jpg differ diff --git a/doc/img/spree_upgrade_epics.jpg b/doc/img/spree_upgrade_epics.jpg new file mode 100755 index 0000000000..38096c0bdd Binary files /dev/null and b/doc/img/spree_upgrade_epics.jpg differ diff --git a/lib/open_food_network/bulk_coop_report.rb b/lib/open_food_network/bulk_coop_report.rb index a0437e7d42..1e0cf5e69d 100644 --- a/lib/open_food_network/bulk_coop_report.rb +++ b/lib/open_food_network/bulk_coop_report.rb @@ -1,15 +1,17 @@ require 'open_food_network/reports/bulk_coop_supplier_report' require 'open_food_network/reports/bulk_coop_allocation_report' +require "open_food_network/reports/line_items" module OpenFoodNetwork class BulkCoopReport attr_reader :params - def initialize(user, params = {}) + def initialize(user, params = {}, render_table = false) @params = params @user = user + @render_table = render_table - @supplier_report = OpenFoodNetwork::Reports::BulkCoopSupplierReport.new - @allocation_report = OpenFoodNetwork::Reports::BulkCoopAllocationReport.new + @supplier_report = Reports::BulkCoopSupplierReport.new + @allocation_report = Reports::BulkCoopAllocationReport.new end def header @@ -44,25 +46,12 @@ module OpenFoodNetwork end def search - permissions.visible_orders.complete.not_state(:canceled).search(params[:q]) + Reports::LineItems.search_orders(permissions, params) end def table_items - orders = search.result - - line_items = permissions.visible_line_items.merge(Spree::LineItem.where(order_id: orders)) - - line_items_with_hidden_details = - permissions.editable_line_items.empty? ? line_items : line_items.where('"spree_line_items"."id" NOT IN (?)', permissions.editable_line_items) - - line_items.select{ |li| line_items_with_hidden_details.include? li }.each do |line_item| - # TODO We should really be hiding customer code here too, but until we - # have an actual association between order and customer, it's a bit tricky - line_item.order.bill_address.andand.assign_attributes(firstname: I18n.t('admin.reports.hidden'), lastname: "", phone: "", address1: "", address2: "", city: "", zipcode: "", state: nil) - line_item.order.ship_address.andand.assign_attributes(firstname: I18n.t('admin.reports.hidden'), lastname: "", phone: "", address1: "", address2: "", city: "", zipcode: "", state: nil) - line_item.order.assign_attributes(email: I18n.t('admin.reports.hidden')) - end - line_items + return [] unless @render_table + Reports::LineItems.list(permissions, params) end def rules diff --git a/lib/open_food_network/column_preference_defaults.rb b/lib/open_food_network/column_preference_defaults.rb index ee28e4b2a5..3b28d7e9ce 100644 --- a/lib/open_food_network/column_preference_defaults.rb +++ b/lib/open_food_network/column_preference_defaults.rb @@ -19,7 +19,8 @@ module OpenFoodNetwork reset: { name: I18n.t("#{node}.enable_reset?"), visible: false }, inheritance: { name: I18n.t("#{node}.inherit?"), visible: false }, tags: { name: I18n.t("admin.tags"), visible: false }, - visibility: { name: I18n.t("#{node}.hide"), visible: false } + visibility: { name: I18n.t("#{node}.hide"), visible: false }, + import_date: { name: I18n.t("#{node}.import_date"), visible: false } } end @@ -54,8 +55,8 @@ module OpenFoodNetwork } end - def products_bulk_edit_columns - node = "spree.admin.products.bulk_edit.products_head" + def products_index_columns + node = "spree.admin.products.index.products_head" { image: { name: I18n.t("admin.image"), visible: true }, producer: { name: I18n.t("admin.producer"), visible: true }, @@ -68,7 +69,8 @@ module OpenFoodNetwork category: { name: I18n.t("#{node}.category"), visible: false }, tax_category: { name: I18n.t("#{node}.tax_category"), visible: false }, inherits_properties: { name: I18n.t("#{node}.inherits_properties?"), visible: false }, - available_on: { name: I18n.t("#{node}.available_on"), visible: false } + available_on: { name: I18n.t("#{node}.available_on"), visible: false }, + import_date: { name: I18n.t("#{node}.import_date"), visible: false } } end diff --git a/lib/open_food_network/customers_report.rb b/lib/open_food_network/customers_report.rb index a84e1ce416..0a605b3596 100644 --- a/lib/open_food_network/customers_report.rb +++ b/lib/open_food_network/customers_report.rb @@ -1,9 +1,10 @@ module OpenFoodNetwork class CustomersReport attr_reader :params - def initialize(user, params = {}) + def initialize(user, params = {}, compile_table = false) @params = params @user = user + @compile_table = compile_table end def header @@ -25,6 +26,7 @@ module OpenFoodNetwork end def table + return [] unless @compile_table orders.map do |order| if is_mailing_list? [order.email, diff --git a/lib/open_food_network/lettuce_share_report.rb b/lib/open_food_network/lettuce_share_report.rb index a1a157c3a3..7bfcf26ec4 100644 --- a/lib/open_food_network/lettuce_share_report.rb +++ b/lib/open_food_network/lettuce_share_report.rb @@ -19,6 +19,7 @@ module OpenFoodNetwork end def table + return [] unless @render_table variants.select { |v| v.in_stock? } .map do |variant| [ diff --git a/lib/open_food_network/order_and_distributor_report.rb b/lib/open_food_network/order_and_distributor_report.rb index dec58d7aeb..1663d483a4 100644 --- a/lib/open_food_network/order_and_distributor_report.rb +++ b/lib/open_food_network/order_and_distributor_report.rb @@ -1,8 +1,12 @@ module OpenFoodNetwork class OrderAndDistributorReport - def initialize orders - @orders = orders + def initialize(user, params = {}, render_table = false) + @params = params + @user = user + @render_table = render_table + + @permissions = OpenFoodNetwork::Permissions.new(user) end def header @@ -27,20 +31,72 @@ module OpenFoodNetwork I18n.t(:report_header_shipping_instructions)] end + def search + @permissions.visible_orders.complete.not_state(:canceled).search(@params[:q]) + end + def table + return [] unless @render_table + + orders = search.result + + # If empty array is passed in, the where clause will return all line_items, which is bad + orders_with_hidden_details = + @permissions.editable_orders.empty? ? orders : orders.where('id NOT IN (?)', @permissions.editable_orders) + + orders.select{ |order| orders_with_hidden_details.include? order }.each do |order| + # TODO We should really be hiding customer code here too, but until we + # have an actual association between order and customer, it's a bit tricky + order.bill_address.andand.assign_attributes(firstname: I18n.t('admin.reports.hidden'), lastname: "", phone: "", address1: "", address2: "", city: "", zipcode: "", state: nil) + order.ship_address.andand.assign_attributes(firstname: I18n.t('admin.reports.hidden'), lastname: "", phone: "", address1: "", address2: "", city: "", zipcode: "", state: nil) + order.assign_attributes(email: I18n.t('admin.reports.hidden')) + end + + line_item_details orders + end + + private + + def line_item_details(orders) order_and_distributor_details = [] - @orders.each do |order| + orders.each do |order| order.line_items.each do |line_item| - order_and_distributor_details << [order.created_at, order.id, - order.bill_address.full_name, order.email, order.bill_address.phone, order.bill_address.city, - line_item.product.sku, line_item.product.name, line_item.options_text, line_item.quantity, line_item.max_quantity, line_item.price * line_item.quantity, line_item.distribution_fee, - order.payments.first.andand.payment_method.andand.name, - order.distributor.andand.name, order.distributor.address.address1, order.distributor.address.city, order.distributor.address.zipcode, order.special_instructions ] + order_and_distributor_details << row_for(line_item, order) end end order_and_distributor_details end + + # Returns a row with the data to display for the specified line_item and + # its order + # + # @param line_item [Spree::LineItem] + # @param order [Spree::Order] + # @return [Array] + def row_for(line_item, order) + [ + order.created_at, + order.id, + order.bill_address.full_name, + order.email, + order.bill_address.phone, + order.bill_address.city, + line_item.product.sku, + line_item.product.name, + line_item.options_text, + line_item.quantity, + line_item.max_quantity, + line_item.price * line_item.quantity, + line_item.distribution_fee, + order.payments.first.andand.payment_method.andand.name, + order.distributor.andand.name, + order.distributor.address.address1, + order.distributor.address.city, + order.distributor.address.zipcode, + order.special_instructions + ] + end end end diff --git a/lib/open_food_network/order_cycle_management_report.rb b/lib/open_food_network/order_cycle_management_report.rb index 53310e6fe1..d9921d15c2 100644 --- a/lib/open_food_network/order_cycle_management_report.rb +++ b/lib/open_food_network/order_cycle_management_report.rb @@ -3,9 +3,10 @@ require 'open_food_network/user_balance_calculator' module OpenFoodNetwork class OrderCycleManagementReport attr_reader :params - def initialize(user, params = {}) + def initialize(user, params = {}, render_table = false) @params = params @user = user + @render_table = render_table end def header @@ -50,6 +51,8 @@ module OpenFoodNetwork end def table_items + return [] unless @render_table + if is_payment_methods? orders.map { |o| payment_method_row o } else diff --git a/lib/open_food_network/orders_and_fulfillments_report.rb b/lib/open_food_network/orders_and_fulfillments_report.rb index 1de8c3ff40..eabdbc6c5f 100644 --- a/lib/open_food_network/orders_and_fulfillments_report.rb +++ b/lib/open_food_network/orders_and_fulfillments_report.rb @@ -1,11 +1,14 @@ +require "open_food_network/reports/line_items" + include Spree::ReportsHelper module OpenFoodNetwork class OrdersAndFulfillmentsReport attr_reader :params - def initialize(user, params = {}) + def initialize(user, params = {}, render_table = false) @params = params @user = user + @render_table = render_table end def header @@ -45,27 +48,12 @@ module OpenFoodNetwork end def search - permissions.visible_orders.complete.not_state(:canceled).search(params[:q]) + Reports::LineItems.search_orders(permissions, params) end def table_items - orders = search.result - - line_items = permissions.visible_line_items.merge(Spree::LineItem.where(order_id: orders)) - line_items = line_items.supplied_by_any(params[:supplier_id_in]) if params[:supplier_id_in].present? - - # If empty array is passed in, the where clause will return all line_items, which is bad - line_items_with_hidden_details = - permissions.editable_line_items.empty? ? line_items : line_items.where('"spree_line_items"."id" NOT IN (?)', permissions.editable_line_items) - - line_items.select{ |li| line_items_with_hidden_details.include? li }.each do |line_item| - # TODO We should really be hiding customer code here too, but until we - # have an actual association between order and customer, it's a bit tricky - line_item.order.bill_address.andand.assign_attributes(firstname: I18n.t('admin.reports.hidden'), lastname: "", phone: "", address1: "", address2: "", city: "", zipcode: "", state: nil) - line_item.order.ship_address.andand.assign_attributes(firstname: I18n.t('admin.reports.hidden'), lastname: "", phone: "", address1: "", address2: "", city: "", zipcode: "", state: nil) - line_item.order.assign_attributes(email: I18n.t('admin.reports.hidden')) - end - line_items + return [] unless @render_table + Reports::LineItems.list(permissions, params) end def rules diff --git a/lib/open_food_network/packing_report.rb b/lib/open_food_network/packing_report.rb index 4c9eb783ec..6f29fa7bc9 100644 --- a/lib/open_food_network/packing_report.rb +++ b/lib/open_food_network/packing_report.rb @@ -1,9 +1,12 @@ +require "open_food_network/reports/line_items" + module OpenFoodNetwork class PackingReport attr_reader :params - def initialize(user, params = {}) + def initialize(user, params = {}, render_table = false) @params = params @user = user + @render_table = render_table end def header @@ -35,33 +38,16 @@ module OpenFoodNetwork end def search - permissions.visible_orders.complete.not_state(:canceled).search(params[:q]) + Reports::LineItems.search_orders(permissions, params) end def table_items - orders = search.result - - line_items = permissions.visible_line_items.merge(Spree::LineItem.where(order_id: orders)) - line_items = line_items.supplied_by_any(params[:supplier_id_in]) if params[:supplier_id_in].present? - - line_items_with_hidden_details = - permissions.editable_line_items.empty? ? line_items : line_items.where('"spree_line_items"."id" NOT IN (?)', permissions.editable_line_items) - - line_items.select{ |li| line_items_with_hidden_details.include? li }.each do |line_item| - # TODO We should really be hiding customer code here too, but until we - # have an actual association between order and customer, it's a bit tricky - line_item.order.bill_address.andand.assign_attributes(firstname: I18n.t('admin.reports.hidden'), lastname: "", phone: "", address1: "", address2: "", city: "", zipcode: "", state: nil) - line_item.order.ship_address.andand.assign_attributes(firstname: I18n.t('admin.reports.hidden'), lastname: "", phone: "", address1: "", address2: "", city: "", zipcode: "", state: nil) - line_item.order.assign_attributes(email: I18n.t('admin.reports.hidden')) - end - line_items + return [] unless @render_table + Reports::LineItems.list(permissions, params) end def rules if is_by_customer? -# customer_rows orders -# table_items = @line_items - [ { group_by: proc { |line_item| line_item.order.distributor }, sort_by: proc { |distributor| distributor.name } }, @@ -84,9 +70,6 @@ module OpenFoodNetwork sort_by: proc { |full_name| full_name } } ] else -# supplier_rows orders -# table_items = supplier_rows orders -# [ { group_by: proc { |line_item| line_item.order.distributor }, sort_by: proc { |distributor| distributor.name } }, { group_by: proc { |line_item| line_item.product.supplier }, diff --git a/lib/open_food_network/payments_report.rb b/lib/open_food_network/payments_report.rb index 015b8af247..fc46422011 100644 --- a/lib/open_food_network/payments_report.rb +++ b/lib/open_food_network/payments_report.rb @@ -1,9 +1,10 @@ module OpenFoodNetwork class PaymentsReport attr_reader :params - def initialize(user, params = {}) + def initialize(user, params = {}, render_table = false) @params = params @user = user + @render_table = render_table end def header @@ -37,6 +38,7 @@ module OpenFoodNetwork end def table_items + return [] unless @render_table orders = search.result payments = orders.map { |o| o.payments.select { |payment| payment.completed? } }.flatten # Only select completed payments case params[:report_type] diff --git a/lib/open_food_network/products_and_inventory_report.rb b/lib/open_food_network/products_and_inventory_report.rb index 6f6ac4a44d..16d94dafb7 100644 --- a/lib/open_food_network/products_and_inventory_report.rb +++ b/lib/open_food_network/products_and_inventory_report.rb @@ -18,6 +18,7 @@ module OpenFoodNetwork end def table + return [] unless @render_table variants.map do |variant| [ variant.product.supplier.name, diff --git a/lib/open_food_network/products_and_inventory_report_base.rb b/lib/open_food_network/products_and_inventory_report_base.rb index 7641c056ea..dff53fdb9a 100644 --- a/lib/open_food_network/products_and_inventory_report_base.rb +++ b/lib/open_food_network/products_and_inventory_report_base.rb @@ -2,9 +2,10 @@ module OpenFoodNetwork class ProductsAndInventoryReportBase attr_reader :params - def initialize(user, params = {}) + def initialize(user, params = {}, render_table = false) @user = user @params = params + @render_table = render_table end def permissions diff --git a/lib/open_food_network/proxy_order_syncer.rb b/lib/open_food_network/proxy_order_syncer.rb index 0b52ec1971..10cbaa12b0 100644 --- a/lib/open_food_network/proxy_order_syncer.rb +++ b/lib/open_food_network/proxy_order_syncer.rb @@ -19,7 +19,7 @@ module OpenFoodNetwork return sync_all! if @subscriptions return initialise_proxy_orders! unless @subscription.id create_proxy_orders! - remove_obsolete_proxy_orders! + remove_orphaned_proxy_orders! end private @@ -28,7 +28,7 @@ module OpenFoodNetwork @subscriptions.each do |subscription| @subscription = subscription create_proxy_orders! - remove_obsolete_proxy_orders! + remove_orphaned_proxy_orders! end end @@ -51,14 +51,15 @@ module OpenFoodNetwork not_closed_in_range_order_cycles.pluck(:id) - proxy_orders.map(&:order_cycle_id) end - def remove_obsolete_proxy_orders! - obsolete_proxy_orders.scoped.delete_all + def remove_orphaned_proxy_orders! + orphaned_proxy_orders.scoped.delete_all end - def obsolete_proxy_orders - in_range_order_cycle_ids = in_range_order_cycles.pluck(:id) - return proxy_orders unless in_range_order_cycle_ids.any? - proxy_orders.where('order_cycle_id NOT IN (?)', in_range_order_cycle_ids) + def orphaned_proxy_orders + orphaned = proxy_orders.where(placed_at: nil) + order_cycle_ids = in_range_order_cycles.pluck(:id) + return orphaned unless order_cycle_ids.any? + orphaned.where('order_cycle_id NOT IN (?)', order_cycle_ids) end def insert_values diff --git a/lib/open_food_network/reports/line_items.rb b/lib/open_food_network/reports/line_items.rb new file mode 100644 index 0000000000..86350a95e3 --- /dev/null +++ b/lib/open_food_network/reports/line_items.rb @@ -0,0 +1,30 @@ +module OpenFoodNetwork + module Reports + # shared code to search and list line items + module LineItems + def self.search_orders(permissions, params) + permissions.visible_orders.complete.not_state(:canceled).search(params[:q]) + end + + def self.list(permissions, params) + orders = search_orders(permissions, params).result + + line_items = permissions.visible_line_items.merge(Spree::LineItem.where(order_id: orders)) + line_items = line_items.supplied_by_any(params[:supplier_id_in]) if params[:supplier_id_in].present? + + # If empty array is passed in, the where clause will return all line_items, which is bad + line_items_with_hidden_details = + permissions.editable_line_items.empty? ? line_items : line_items.where('"spree_line_items"."id" NOT IN (?)', permissions.editable_line_items) + + line_items.select{ |li| line_items_with_hidden_details.include? li }.each do |line_item| + # TODO We should really be hiding customer code here too, but until we + # have an actual association between order and customer, it's a bit tricky + line_item.order.bill_address.andand.assign_attributes(firstname: I18n.t('admin.reports.hidden'), lastname: "", phone: "", address1: "", address2: "", city: "", zipcode: "", state: nil) + line_item.order.ship_address.andand.assign_attributes(firstname: I18n.t('admin.reports.hidden'), lastname: "", phone: "", address1: "", address2: "", city: "", zipcode: "", state: nil) + line_item.order.assign_attributes(email: I18n.t('admin.reports.hidden')) + end + line_items + end + end + end +end diff --git a/lib/open_food_network/sales_tax_report.rb b/lib/open_food_network/sales_tax_report.rb index 061fd4b64a..02f0651ecf 100644 --- a/lib/open_food_network/sales_tax_report.rb +++ b/lib/open_food_network/sales_tax_report.rb @@ -3,9 +3,10 @@ module OpenFoodNetwork include Spree::ReportsHelper attr_accessor :user, :params - def initialize(user, params) + def initialize(user, params, render_table) @user = user @params = params + @render_table = render_table end def header @@ -42,6 +43,7 @@ module OpenFoodNetwork end def table + return [] unless @render_table case params[:report_type] when "tax_rates" orders.map do |order| diff --git a/lib/open_food_network/users_and_enterprises_report.rb b/lib/open_food_network/users_and_enterprises_report.rb index 4f816af8bd..89dba0b73a 100644 --- a/lib/open_food_network/users_and_enterprises_report.rb +++ b/lib/open_food_network/users_and_enterprises_report.rb @@ -1,8 +1,9 @@ module OpenFoodNetwork class UsersAndEnterprisesReport attr_reader :params - def initialize(params = {}) + def initialize(params = {}, compile_table = false) @params = params + @compile_table = compile_table # Convert arrays of ids to comma delimited strings @params[:enterprise_id_in] = @params[:enterprise_id_in].join(',') if @params[:enterprise_id_in].kind_of? Array @@ -22,6 +23,7 @@ module OpenFoodNetwork end def table + return [] unless @compile_table users_and_enterprises.map do |uae| [ uae["user_email"], diff --git a/lib/open_food_network/xero_invoices_report.rb b/lib/open_food_network/xero_invoices_report.rb index 43f278c73e..5e351871e8 100644 --- a/lib/open_food_network/xero_invoices_report.rb +++ b/lib/open_food_network/xero_invoices_report.rb @@ -1,6 +1,6 @@ module OpenFoodNetwork class XeroInvoicesReport - def initialize(user, opts={}) + def initialize(user, opts = {}, compile_table = false) @user = user @opts = opts. @@ -9,6 +9,7 @@ module OpenFoodNetwork invoice_date: Time.zone.today, due_date: Time.zone.today + 1.month, account_code: 'food sales'}) + @compile_table = compile_table end def header @@ -26,6 +27,7 @@ module OpenFoodNetwork end def table + return [] unless @compile_table rows = [] orders.each_with_index do |order, i| diff --git a/lib/tasks/dev.rake b/lib/tasks/dev.rake index 4bf1c58c3c..6fa9af6f3e 100644 --- a/lib/tasks/dev.rake +++ b/lib/tasks/dev.rake @@ -9,7 +9,6 @@ namespace :openfoodnetwork do require_relative '../../spec/support/spree/init' task_name = "openfoodnetwork:dev:load_sample_data" - spree_user = Spree::User.find_by_email('spree@example.com') country = Spree::Country.find_by_iso(ENV.fetch('DEFAULT_COUNTRY_CODE')) state = country.states.first @@ -17,9 +16,9 @@ namespace :openfoodnetwork do unless Spree::Zone.find_by_name 'Australia' puts "[#{task_name}] Seeding shipping / payment information" - zone = FactoryGirl.create(:zone, name: 'Australia', zone_members: []) + zone = FactoryBot.create(:zone, name: 'Australia', zone_members: []) Spree::ZoneMember.create(zone: zone, zoneable: country) - address = FactoryGirl.create( + address = FactoryBot.create( :address, address1: "15/1 Ballantyne Street", zipcode: "3153", @@ -27,19 +26,19 @@ namespace :openfoodnetwork do country: country, state: state ) - enterprise = FactoryGirl.create(:enterprise, address: address) + enterprise = FactoryBot.create(:enterprise, address: address) - FactoryGirl.create(:shipping_method, zone: zone, distributors: [enterprise]) + FactoryBot.create(:shipping_method, zone: zone, distributors: [enterprise]) end # -- Taxonomies unless Spree::Taxonomy.find_by_name 'Products' puts "[#{task_name}] Seeding taxonomies" - taxonomy = Spree::Taxonomy.find_by_name('Products') || FactoryGirl.create(:taxonomy, name: 'Products') + taxonomy = Spree::Taxonomy.find_by_name('Products') || FactoryBot.create(:taxonomy, name: 'Products') taxonomy_root = taxonomy.root ['Vegetables', 'Fruit', 'Oils', 'Preserves and Sauces', 'Dairy', 'Meat and Fish'].each do |taxon_name| - FactoryGirl.create(:taxon, name: taxon_name, parent_id: taxonomy_root.id) + FactoryBot.create(:taxon, name: taxon_name, parent_id: taxonomy_root.id) end end @@ -47,39 +46,39 @@ namespace :openfoodnetwork do unless Spree::Address.find_by_zipcode "3160" puts "[#{task_name}] Seeding addresses" - FactoryGirl.create(:address, address1: "25 Myrtle Street", zipcode: "3153", city: "Bayswater", country: country, state: state) - FactoryGirl.create(:address, address1: "6 Rollings Road", zipcode: "3156", city: "Upper Ferntree Gully", country: country, state: state) - FactoryGirl.create(:address, address1: "72 Lake Road", zipcode: "3130", city: "Blackburn", country: country, state: state) - FactoryGirl.create(:address, address1: "7 Verbena Street", zipcode: "3195", city: "Mordialloc", country: country, state: state) - FactoryGirl.create(:address, address1: "20 Galvin Street", zipcode: "3018", city: "Altona", country: country, state: state) - FactoryGirl.create(:address, address1: "59 Websters Road", zipcode: "3106", city: "Templestowe", country: country, state: state) - FactoryGirl.create(:address, address1: "17 Torresdale Drive", zipcode: "3155", city: "Boronia", country: country, state: state) - FactoryGirl.create(:address, address1: "21 Robina CRT", zipcode: "3764", city: "Kilmore", country: country, state: state) - FactoryGirl.create(:address, address1: "25 Kendall Street", zipcode: "3134", city: "Ringwood", country: country, state: state) - FactoryGirl.create(:address, address1: "2 Mines Road", zipcode: "3135", city: "Ringwood East", country: country, state: state) - FactoryGirl.create(:address, address1: "183 Millers Road", zipcode: "3025", city: "Altona North", country: country, state: state) - FactoryGirl.create(:address, address1: "310 Pascoe Vale Road", zipcode: "3040", city: "Essendon", country: country, state: state) - FactoryGirl.create(:address, address1: "6 Martin Street", zipcode: "3160", city: "Belgrave", country: country, state: state) + FactoryBot.create(:address, address1: "25 Myrtle Street", zipcode: "3153", city: "Bayswater", country: country, state: state) + FactoryBot.create(:address, address1: "6 Rollings Road", zipcode: "3156", city: "Upper Ferntree Gully", country: country, state: state) + FactoryBot.create(:address, address1: "72 Lake Road", zipcode: "3130", city: "Blackburn", country: country, state: state) + FactoryBot.create(:address, address1: "7 Verbena Street", zipcode: "3195", city: "Mordialloc", country: country, state: state) + FactoryBot.create(:address, address1: "20 Galvin Street", zipcode: "3018", city: "Altona", country: country, state: state) + FactoryBot.create(:address, address1: "59 Websters Road", zipcode: "3106", city: "Templestowe", country: country, state: state) + FactoryBot.create(:address, address1: "17 Torresdale Drive", zipcode: "3155", city: "Boronia", country: country, state: state) + FactoryBot.create(:address, address1: "21 Robina CRT", zipcode: "3764", city: "Kilmore", country: country, state: state) + FactoryBot.create(:address, address1: "25 Kendall Street", zipcode: "3134", city: "Ringwood", country: country, state: state) + FactoryBot.create(:address, address1: "2 Mines Road", zipcode: "3135", city: "Ringwood East", country: country, state: state) + FactoryBot.create(:address, address1: "183 Millers Road", zipcode: "3025", city: "Altona North", country: country, state: state) + FactoryBot.create(:address, address1: "310 Pascoe Vale Road", zipcode: "3040", city: "Essendon", country: country, state: state) + FactoryBot.create(:address, address1: "6 Martin Street", zipcode: "3160", city: "Belgrave", country: country, state: state) end # -- Enterprises unless Enterprise.count > 1 puts "[#{task_name}] Seeding enterprises" - 3.times { FactoryGirl.create(:supplier_enterprise, address: Spree::Address.find_by_zipcode("3160")) } + 3.times { FactoryBot.create(:supplier_enterprise, address: Spree::Address.find_by_zipcode("3160")) } - FactoryGirl.create(:distributor_enterprise, name: "Green Grass", address: Spree::Address.find_by_zipcode("3153")) - FactoryGirl.create(:distributor_enterprise, name: "AusFarmers United", address: Spree::Address.find_by_zipcode("3156")) - FactoryGirl.create(:distributor_enterprise, name: "Blackburn FreeGrossers", address: Spree::Address.find_by_zipcode("3130")) - FactoryGirl.create(:distributor_enterprise, name: "MegaFoods", address: Spree::Address.find_by_zipcode("3195")) - FactoryGirl.create(:distributor_enterprise, name: "Eco Butchers", address: Spree::Address.find_by_zipcode("3018")) - FactoryGirl.create(:distributor_enterprise, name: "Western Wines", address: Spree::Address.find_by_zipcode("3106")) - FactoryGirl.create(:distributor_enterprise, name: "QuickFresh", address: Spree::Address.find_by_zipcode("3155")) - FactoryGirl.create(:distributor_enterprise, name: "Fooderers", address: Spree::Address.find_by_zipcode("3764")) - FactoryGirl.create(:distributor_enterprise, name: "Food Local", address: Spree::Address.find_by_zipcode("3134")) - FactoryGirl.create(:distributor_enterprise, name: "Green Food Trading Corporation", address: Spree::Address.find_by_zipcode("3135")) - FactoryGirl.create(:distributor_enterprise, name: "Better Food", address: Spree::Address.find_by_zipcode("3025")) - FactoryGirl.create(:distributor_enterprise, name: "Gippsland Poultry", address: Spree::Address.find_by_zipcode("3040")) + FactoryBot.create(:distributor_enterprise, name: "Green Grass", address: Spree::Address.find_by_zipcode("3153")) + FactoryBot.create(:distributor_enterprise, name: "AusFarmers United", address: Spree::Address.find_by_zipcode("3156")) + FactoryBot.create(:distributor_enterprise, name: "Blackburn FreeGrossers", address: Spree::Address.find_by_zipcode("3130")) + FactoryBot.create(:distributor_enterprise, name: "MegaFoods", address: Spree::Address.find_by_zipcode("3195")) + FactoryBot.create(:distributor_enterprise, name: "Eco Butchers", address: Spree::Address.find_by_zipcode("3018")) + FactoryBot.create(:distributor_enterprise, name: "Western Wines", address: Spree::Address.find_by_zipcode("3106")) + FactoryBot.create(:distributor_enterprise, name: "QuickFresh", address: Spree::Address.find_by_zipcode("3155")) + FactoryBot.create(:distributor_enterprise, name: "Fooderers", address: Spree::Address.find_by_zipcode("3764")) + FactoryBot.create(:distributor_enterprise, name: "Food Local", address: Spree::Address.find_by_zipcode("3134")) + FactoryBot.create(:distributor_enterprise, name: "Green Food Trading Corporation", address: Spree::Address.find_by_zipcode("3135")) + FactoryBot.create(:distributor_enterprise, name: "Better Food", address: Spree::Address.find_by_zipcode("3025")) + FactoryBot.create(:distributor_enterprise, name: "Gippsland Poultry", address: Spree::Address.find_by_zipcode("3040")) end # -- Enterprise users @@ -88,12 +87,12 @@ namespace :openfoodnetwork do pw = "spree123" - u = FactoryGirl.create(:user, email: "sup@example.com", password: pw, password_confirmation: pw) + u = FactoryBot.create(:user, email: "sup@example.com", password: pw, password_confirmation: pw) u.enterprises << Enterprise.is_primary_producer.first u.enterprises << Enterprise.is_primary_producer.second puts " Supplier User created: #{u.email}/#{pw} (" + u.enterprise_roles.map{ |er| er.enterprise.name}.join(", ") + ")" - u = FactoryGirl.create(:user, email: "dist@example.com", password: pw, password_confirmation: pw) + u = FactoryBot.create(:user, email: "dist@example.com", password: pw, password_confirmation: pw) u.enterprises << Enterprise.is_distributor.first u.enterprises << Enterprise.is_distributor.second puts " Distributor User created: #{u.email}/#{pw} (" + u.enterprise_roles.map{ |er| er.enterprise.name}.join(", ") + ")" @@ -102,14 +101,14 @@ namespace :openfoodnetwork do # -- Enterprise fees unless EnterpriseFee.count > 1 Enterprise.is_distributor.each do |distributor| - FactoryGirl.create(:enterprise_fee, enterprise: distributor) + FactoryBot.create(:enterprise_fee, enterprise: distributor) end end # -- Enterprise Payment Methods unless Spree::PaymentMethod.count > 1 Enterprise.is_distributor.each do |distributor| - FactoryGirl.create(:payment_method, distributors: [distributor], name: "Cheque (#{distributor.name})", environment: 'development') + FactoryBot.create(:payment_method, distributors: [distributor], name: "Cheque (#{distributor.name})", environment: 'development') end end @@ -117,7 +116,7 @@ namespace :openfoodnetwork do unless Spree::Product.count > 0 puts "[#{task_name}] Seeding products" - prod1 = FactoryGirl.create(:product, + prod1 = FactoryBot.create(:product, name: 'Garlic', price: 20.00, supplier: Enterprise.is_primary_producer[0], taxons: [Spree::Taxon.find_by_name('Vegetables')]) @@ -127,7 +126,7 @@ namespace :openfoodnetwork do enterprise_fee: Enterprise.is_distributor[0].enterprise_fees.first) - prod2 = FactoryGirl.create(:product, + prod2 = FactoryBot.create(:product, name: 'Fuji Apple', price: 5.00, supplier: Enterprise.is_primary_producer[1], taxons: [Spree::Taxon.find_by_name('Fruit')]) @@ -136,7 +135,7 @@ namespace :openfoodnetwork do distributor: Enterprise.is_distributor[1], enterprise_fee: Enterprise.is_distributor[1].enterprise_fees.first) - prod3 = FactoryGirl.create(:product, + prod3 = FactoryBot.create(:product, name: 'Beef - 5kg Trays', price: 50.00, supplier: Enterprise.is_primary_producer[2], taxons: [Spree::Taxon.find_by_name('Meat and Fish')]) @@ -145,7 +144,7 @@ namespace :openfoodnetwork do distributor: Enterprise.is_distributor[2], enterprise_fee: Enterprise.is_distributor[2].enterprise_fees.first) - prod4 = FactoryGirl.create(:product, + prod4 = FactoryBot.create(:product, name: 'Carrots', price: 3.00, supplier: Enterprise.is_primary_producer[2], taxons: [Spree::Taxon.find_by_name('Meat and Fish')]) @@ -154,7 +153,7 @@ namespace :openfoodnetwork do distributor: Enterprise.is_distributor[2], enterprise_fee: Enterprise.is_distributor[2].enterprise_fees.first) - prod5 = FactoryGirl.create(:product, + prod5 = FactoryBot.create(:product, name: 'Potatoes', price: 2.00, supplier: Enterprise.is_primary_producer[2], taxons: [Spree::Taxon.find_by_name('Meat and Fish')]) @@ -163,7 +162,7 @@ namespace :openfoodnetwork do distributor: Enterprise.is_distributor[2], enterprise_fee: Enterprise.is_distributor[2].enterprise_fees.first) - prod6 = FactoryGirl.create(:product, + prod6 = FactoryBot.create(:product, name: 'Tomatoes', price: 2.00, supplier: Enterprise.is_primary_producer[2], taxons: [Spree::Taxon.find_by_name('Meat and Fish')]) @@ -172,7 +171,7 @@ namespace :openfoodnetwork do distributor: Enterprise.is_distributor[2], enterprise_fee: Enterprise.is_distributor[2].enterprise_fees.first) - prod7 = FactoryGirl.create(:product, + prod7 = FactoryBot.create(:product, name: 'Potatoes', price: 2.00, supplier: Enterprise.is_primary_producer[2], taxons: [Spree::Taxon.find_by_name('Meat and Fish')]) @@ -202,8 +201,6 @@ namespace :openfoodnetwork do CreateOrderCycle.new(enterprise2, variants).call EnterpriseRole.create!(user: Spree::User.first, enterprise: enterprise2) - - spree_user.confirm! end end end diff --git a/script/rubocop_autocorrect b/script/rubocop_autocorrect index 1ca248ddfa..f9e7cac2fc 100755 --- a/script/rubocop_autocorrect +++ b/script/rubocop_autocorrect @@ -1,5 +1,15 @@ #!/bin/sh +# Usage +# +# 1. Clean any git unstagged or untracked changes. Consider creating a new branch +# 2. Remove a cop's exclusion paragraph from the .rubocop_todo.yml +# 3. Run: +# +# $ ./script/rubocop_autocorrect +# +# This will commit all the changes. + set -e COP="$1" diff --git a/spec/archive/features/consumer/checkout_spec.rb b/spec/archive/features/consumer/checkout_spec.rb index da147e7b2b..918391abef 100644 --- a/spec/archive/features/consumer/checkout_spec.rb +++ b/spec/archive/features/consumer/checkout_spec.rb @@ -487,9 +487,9 @@ feature %q{ ExchangeFee.create!(exchange: ex2, enterprise_fee: supplier_fee4) # Distributors - distributor1 = FactoryGirl.create(:distributor_enterprise, name: "FruitAndVeg") + distributor1 = FactoryBot.create(:distributor_enterprise, name: "FruitAndVeg") @distributor1 = distributor1 - distributor2 = FactoryGirl.create(:distributor_enterprise, name: "MoreFreshStuff") + distributor2 = FactoryBot.create(:distributor_enterprise, name: "MoreFreshStuff") create_enterprise_group_for distributor1 distributor_fee1 = create(:enterprise_fee, enterprise: distributor1, fee_type: 'packing', amount: 7) distributor_fee2 = create(:enterprise_fee, enterprise: distributor1, fee_type: 'transport', amount: 8) diff --git a/spec/controllers/admin/bulk_line_items_controller_spec.rb b/spec/controllers/admin/bulk_line_items_controller_spec.rb index ecc6c37f5c..dd5c1c6326 100644 --- a/spec/controllers/admin/bulk_line_items_controller_spec.rb +++ b/spec/controllers/admin/bulk_line_items_controller_spec.rb @@ -7,14 +7,14 @@ describe Admin::BulkLineItemsController, type: :controller do render_views let(:line_item_attributes) { %i[id quantity max_quantity price supplier final_weight_volume units_product units_variant order] } - let!(:dist1) { FactoryGirl.create(:distributor_enterprise) } - let!(:order1) { FactoryGirl.create(:order, state: 'complete', completed_at: 1.day.ago, distributor: dist1, billing_address: FactoryGirl.create(:address) ) } - let!(:order2) { FactoryGirl.create(:order, state: 'complete', completed_at: Time.zone.now, distributor: dist1, billing_address: FactoryGirl.create(:address) ) } - let!(:order3) { FactoryGirl.create(:order, state: 'complete', completed_at: Time.zone.now, distributor: dist1, billing_address: FactoryGirl.create(:address) ) } - let!(:line_item1) { FactoryGirl.create(:line_item, order: order1) } - let!(:line_item2) { FactoryGirl.create(:line_item, order: order2) } - let!(:line_item3) { FactoryGirl.create(:line_item, order: order2) } - let!(:line_item4) { FactoryGirl.create(:line_item, order: order3) } + let!(:dist1) { FactoryBot.create(:distributor_enterprise) } + let!(:order1) { FactoryBot.create(:order, state: 'complete', completed_at: 1.day.ago, distributor: dist1, billing_address: FactoryBot.create(:address) ) } + let!(:order2) { FactoryBot.create(:order, state: 'complete', completed_at: Time.zone.now, distributor: dist1, billing_address: FactoryBot.create(:address) ) } + let!(:order3) { FactoryBot.create(:order, state: 'complete', completed_at: Time.zone.now, distributor: dist1, billing_address: FactoryBot.create(:address) ) } + let!(:line_item1) { FactoryBot.create(:line_item, order: order1) } + let!(:line_item2) { FactoryBot.create(:line_item, order: order2) } + let!(:line_item3) { FactoryBot.create(:line_item, order: order2) } + let!(:line_item4) { FactoryBot.create(:line_item, order: order3) } context "as a normal user" do before { controller.stub spree_current_user: create_enterprise_user } @@ -82,11 +82,11 @@ describe Admin::BulkLineItemsController, type: :controller do let(:distributor2) { create(:distributor_enterprise) } let(:coordinator) { create(:distributor_enterprise) } let(:order_cycle) { create(:simple_order_cycle, coordinator: coordinator) } - let!(:order1) { FactoryGirl.create(:order, order_cycle: order_cycle, state: 'complete', completed_at: Time.zone.now, distributor: distributor1, billing_address: FactoryGirl.create(:address) ) } - let!(:line_item1) { FactoryGirl.create(:line_item, order: order1, product: FactoryGirl.create(:product, supplier: supplier)) } - let!(:line_item2) { FactoryGirl.create(:line_item, order: order1, product: FactoryGirl.create(:product, supplier: supplier)) } - let!(:order2) { FactoryGirl.create(:order, order_cycle: order_cycle, state: 'complete', completed_at: Time.zone.now, distributor: distributor2, billing_address: FactoryGirl.create(:address) ) } - let!(:line_item3) { FactoryGirl.create(:line_item, order: order2, product: FactoryGirl.create(:product, supplier: supplier)) } + let!(:order1) { FactoryBot.create(:order, order_cycle: order_cycle, state: 'complete', completed_at: Time.zone.now, distributor: distributor1, billing_address: FactoryBot.create(:address) ) } + let!(:line_item1) { FactoryBot.create(:line_item, order: order1, product: FactoryBot.create(:product, supplier: supplier)) } + let!(:line_item2) { FactoryBot.create(:line_item, order: order1, product: FactoryBot.create(:product, supplier: supplier)) } + let!(:order2) { FactoryBot.create(:order, order_cycle: order_cycle, state: 'complete', completed_at: Time.zone.now, distributor: distributor2, billing_address: FactoryBot.create(:address) ) } + let!(:line_item3) { FactoryBot.create(:line_item, order: order2, product: FactoryBot.create(:product, supplier: supplier)) } context "producer enterprise" do before do @@ -130,8 +130,8 @@ describe Admin::BulkLineItemsController, type: :controller do let(:distributor1) { create(:distributor_enterprise) } let(:coordinator) { create(:distributor_enterprise) } let(:order_cycle) { create(:simple_order_cycle, coordinator: coordinator) } - let!(:order1) { FactoryGirl.create(:order, order_cycle: order_cycle, state: 'complete', completed_at: Time.zone.now, distributor: distributor1, billing_address: FactoryGirl.create(:address) ) } - let!(:line_item1) { FactoryGirl.create(:line_item, order: order1, product: FactoryGirl.create(:product, supplier: supplier)) } + let!(:order1) { FactoryBot.create(:order, order_cycle: order_cycle, state: 'complete', completed_at: Time.zone.now, distributor: distributor1, billing_address: FactoryBot.create(:address) ) } + let!(:line_item1) { FactoryBot.create(:line_item, order: order1, product: FactoryBot.create(:product, supplier: supplier)) } let(:line_item_params) { { quantity: 3, final_weight_volume: 3000, price: 3.00 } } let(:params) { { id: line_item1.id, order_id: order1.number, line_item: line_item_params } } @@ -226,8 +226,8 @@ describe Admin::BulkLineItemsController, type: :controller do let(:distributor1) { create(:distributor_enterprise) } let(:coordinator) { create(:distributor_enterprise) } let(:order_cycle) { create(:simple_order_cycle, coordinator: coordinator) } - let!(:order1) { FactoryGirl.create(:order, order_cycle: order_cycle, state: 'complete', completed_at: Time.zone.now, distributor: distributor1, billing_address: FactoryGirl.create(:address) ) } - let!(:line_item1) { FactoryGirl.create(:line_item, order: order1, product: FactoryGirl.create(:product, supplier: supplier)) } + let!(:order1) { FactoryBot.create(:order, order_cycle: order_cycle, state: 'complete', completed_at: Time.zone.now, distributor: distributor1, billing_address: FactoryBot.create(:address) ) } + let!(:line_item1) { FactoryBot.create(:line_item, order: order1, product: FactoryBot.create(:product, supplier: supplier)) } let(:params) { { id: line_item1.id, order_id: order1.number } } before do diff --git a/spec/controllers/admin/manager_invitations_controller_spec.rb b/spec/controllers/admin/manager_invitations_controller_spec.rb index 01b7c984d1..c5e6bf35f8 100644 --- a/spec/controllers/admin/manager_invitations_controller_spec.rb +++ b/spec/controllers/admin/manager_invitations_controller_spec.rb @@ -2,8 +2,11 @@ require 'spec_helper' module Admin describe ManagerInvitationsController, type: :controller do + let!(:enterprise_owner) { create(:user) } + let!(:other_enterprise_user) { create(:user) } let!(:existing_user) { create(:user) } - let!(:enterprise) { create(:enterprise) } + let!(:enterprise) { create(:enterprise, owner: enterprise_owner ) } + let!(:enterprise2) { create(:enterprise, owner: other_enterprise_user ) } let(:admin) { create(:admin_user) } describe "#create" do @@ -38,5 +41,38 @@ module Admin end end end + + describe "with enterprise permissions" do + context "as user with proper enterprise permissions" do + before do + controller.stub spree_current_user: enterprise_owner + end + + it "returns success code" do + spree_post :create, {email: 'an@email.com', enterprise_id: enterprise.id} + + new_user = Spree::User.find_by_email('an@email.com') + + expect(new_user.reset_password_token).to_not be_nil + expect(json_response['user']).to eq new_user.id + expect(response.status).to eq 200 + end + end + + context "as another enterprise user without permissions for this enterprise" do + before do + controller.stub spree_current_user: other_enterprise_user + end + + it "returns unauthorized response" do + spree_post :create, {email: 'another@email.com', enterprise_id: enterprise.id} + + new_user = Spree::User.find_by_email('another@email.com') + + expect(new_user).to be_nil + expect(response.status).to eq 302 + end + end + end end end diff --git a/spec/controllers/admin/order_cycles_controller_spec.rb b/spec/controllers/admin/order_cycles_controller_spec.rb index fa290c3703..853ae6a174 100644 --- a/spec/controllers/admin/order_cycles_controller_spec.rb +++ b/spec/controllers/admin/order_cycles_controller_spec.rb @@ -96,7 +96,86 @@ module Admin end end + describe "create" do + let(:shop) { create(:distributor_enterprise) } + + context "as a manager of a shop" do + let(:form_mock) { instance_double(OrderCycleForm) } + let(:params) { { format: :json, order_cycle: {} } } + + before do + login_as_enterprise_user([shop]) + allow(OrderCycleForm).to receive(:new) { form_mock } + end + + context "when creation is successful" do + before { allow(form_mock).to receive(:save) { true } } + + it "returns success: true" do + spree_post :create, params + json_response = JSON.parse(response.body) + expect(json_response['success']).to be true + end + end + + context "when an error occurs" do + before { allow(form_mock).to receive(:save) { false } } + + it "returns an errors hash" do + spree_post :create, params + json_response = JSON.parse(response.body) + expect(json_response['errors']).to be + end + end + end + end + describe "update" do + let(:order_cycle) { create(:simple_order_cycle) } + let(:coordinator) { order_cycle.coordinator } + let(:form_mock) { instance_double(OrderCycleForm) } + + before do + allow(OrderCycleForm).to receive(:new) { form_mock } + end + + context "as a manager of the coordinator" do + before { login_as_enterprise_user([coordinator]) } + let(:params) { { format: :json, id: order_cycle.id, order_cycle: {} } } + + context "when updating succeeds" do + before { allow(form_mock).to receive(:save) { true } } + + context "when the page is reloading" do + before { params[:reloading] = '1' } + + it "sets flash message" do + spree_put :update, params + flash[:notice].should == 'Your order cycle has been updated.' + end + end + + context "when the page is not reloading" do + it "does not set flash message" do + spree_put :update, params + flash[:notice].should be nil + end + end + end + + context "when a validation error occurs" do + before { allow(form_mock).to receive(:save) { false } } + + it "returns an error message" do + spree_put :update, params + json_response = JSON.parse(response.body) + expect(json_response['errors']).to be + end + end + end + end + + describe "limiting update scope" do let(:order_cycle) { create(:simple_order_cycle) } let(:producer) { create(:supplier_enterprise) } let(:coordinator) { order_cycle.coordinator } @@ -105,114 +184,30 @@ module Admin let!(:incoming_exchange) { create(:exchange, order_cycle: order_cycle, sender: producer, receiver: coordinator, incoming: true, variants: [v]) } let!(:outgoing_exchange) { create(:exchange, order_cycle: order_cycle, sender: coordinator, receiver: hub, incoming: false, variants: [v]) } + let(:allowed) { { incoming_exchanges: [], outgoing_exchanges: [] } } + let(:restricted) { { name: 'some name', orders_open_at: 1.day.from_now, orders_close_at: 1.day.ago } } + let(:params) { { format: :json, id: order_cycle.id, order_cycle: allowed.merge(restricted) } } + let(:form_mock) { instance_double(OrderCycleForm, save: true) } + + before { allow(controller).to receive(:spree_current_user) { user } } + context "as a manager of the coordinator" do - before { login_as_enterprise_user([coordinator]) } + let(:user) { coordinator.owner } + let(:expected) { [order_cycle, hash_including(order_cycle: allowed.merge(restricted)), user] } - it "sets flash message when page is reloading" do - spree_put :update, id: order_cycle.id, reloading: '1', order_cycle: {} - flash[:notice].should == 'Your order cycle has been updated.' - end - - it "does not set flash message otherwise" do - flash[:notice].should be_nil - end - - context "when updating without explicitly submitting exchanges" do - let(:form_applicator_mock) { double(:form_applicator) } - - before do - allow(OpenFoodNetwork::OrderCycleFormApplicator).to receive(:new) { form_applicator_mock } - allow(form_applicator_mock).to receive(:go!) { nil } - end - - it "does not run the OrderCycleFormApplicator" do - expect(order_cycle.exchanges.incoming).to eq [incoming_exchange] - expect(order_cycle.exchanges.outgoing).to eq [outgoing_exchange] - expect(order_cycle.prefers_product_selection_from_coordinator_inventory_only?).to be false - spree_put :update, id: order_cycle.id, order_cycle: { name: 'Some new name', preferred_product_selection_from_coordinator_inventory_only: true } - expect(form_applicator_mock).to_not have_received(:go!) - order_cycle.reload - expect(order_cycle.exchanges.incoming).to eq [incoming_exchange] - expect(order_cycle.exchanges.outgoing).to eq [outgoing_exchange] - expect(order_cycle.name).to eq 'Some new name' - expect(order_cycle.prefers_product_selection_from_coordinator_inventory_only?).to be true - end - end - - context "when a validation error occurs" do - let(:params) { - { - format: :json, - id: order_cycle.id, - order_cycle: { orders_open_at: order_cycle.orders_close_at + 1.day } - } - } - - it "returns an error message" do - spree_put :update, params - json_response = JSON.parse(response.body) - expect(json_response['errors']).to be_present - end + it "allows me to update exchange information for exchanges, name and dates" do + expect(OrderCycleForm).to receive(:new).with(*expected) { form_mock } + spree_put :update, params end end context "as a producer supplying to an order cycle" do - before do - login_as_enterprise_user [producer] - end + let(:user) { producer.owner } + let(:expected) { [order_cycle, hash_including(order_cycle: allowed), user] } - describe "removing a variant from incoming" do - let(:params) do - {order_cycle: { - incoming_exchanges: [{id: incoming_exchange.id, enterprise_id: producer.id, sender_id: producer.id, variants: {v.id => false}}], - outgoing_exchanges: [{id: outgoing_exchange.id, enterprise_id: hub.id, receiver_id: hub.id, variants: {v.id => false}}] } - } - end - - it "removes the variant from outgoing also" do - spree_put :update, {id: order_cycle.id}.merge(params) - Exchange.where(order_cycle_id: order_cycle).with_variant(v).should be_empty - end - end - end - end - - describe "updating schedules" do - let(:user) { create(:user, enterprise_limit: 10) } - let!(:managed_coordinator) { create(:enterprise, owner: user) } - let!(:managed_enterprise) { create(:enterprise, owner: user) } - let!(:coordinated_order_cycle) { create(:simple_order_cycle, coordinator: managed_coordinator ) } - let!(:coordinated_order_cycle2) { create(:simple_order_cycle, coordinator: managed_enterprise ) } - let!(:uncoordinated_order_cycle) { create(:simple_order_cycle, coordinator: create(:enterprise) ) } - let!(:coordinated_schedule) { create(:schedule, order_cycles: [coordinated_order_cycle] ) } - let!(:coordinated_schedule2) { create(:schedule, order_cycles: [coordinated_order_cycle2] ) } - let!(:uncoordinated_schedule) { create(:schedule, order_cycles: [uncoordinated_order_cycle] ) } - - context "where I manage the order_cycle's coordinator" do - render_views - - before do - controller.stub spree_current_user: user - end - - it "allows me to assign only schedules that already I coordinate to the order cycle" do - schedule_ids = [coordinated_schedule2.id, uncoordinated_schedule.id] - spree_put :update, format: :json, id: coordinated_order_cycle.id, order_cycle: { schedule_ids: schedule_ids } - expect(assigns(:order_cycle)).to eq coordinated_order_cycle - # coordinated_order_cycle2 is added - expect(coordinated_order_cycle.reload.schedules).to include coordinated_schedule2 - # coordinated_order_cycle is removed, uncoordinated_order_cycle is NOT added - expect(coordinated_order_cycle.reload.schedules).to_not include coordinated_schedule, uncoordinated_schedule - end - - it "syncs proxy orders when schedule_ids change" do - syncer_mock = double(:syncer) - allow(OpenFoodNetwork::ProxyOrderSyncer).to receive(:new) { syncer_mock } - expect(syncer_mock).to receive(:sync!).exactly(2).times - - spree_put :update, format: :json, id: coordinated_order_cycle.id, order_cycle: { schedule_ids: [coordinated_schedule.id, coordinated_schedule2.id] } - spree_put :update, format: :json, id: coordinated_order_cycle.id, order_cycle: { schedule_ids: [coordinated_schedule.id] } - spree_put :update, format: :json, id: coordinated_order_cycle.id, order_cycle: { schedule_ids: [coordinated_schedule.id] } + it "allows me to update exchange information for exchanges, but not name or dates" do + expect(OrderCycleForm).to receive(:new).with(*expected) { form_mock } + spree_put :update, params end end end @@ -337,5 +332,6 @@ module Admin end end end + end end diff --git a/spec/controllers/api/order_cycles_controller_spec.rb b/spec/controllers/api/order_cycles_controller_spec.rb index 0802bc1d58..48b47fcb9c 100644 --- a/spec/controllers/api/order_cycles_controller_spec.rb +++ b/spec/controllers/api/order_cycles_controller_spec.rb @@ -8,8 +8,8 @@ module Api render_views describe "managed" do - let!(:oc1) { FactoryGirl.create(:simple_order_cycle) } - let!(:oc2) { FactoryGirl.create(:simple_order_cycle) } + let!(:oc1) { FactoryBot.create(:simple_order_cycle) } + let!(:oc2) { FactoryBot.create(:simple_order_cycle) } let(:coordinator) { oc1.coordinator } let(:attributes) { [:id, :name, :suppliers, :distributors] } diff --git a/spec/controllers/cart_controller_spec.rb b/spec/controllers/cart_controller_spec.rb index b014232c7f..f7f48c3a06 100644 --- a/spec/controllers/cart_controller_spec.rb +++ b/spec/controllers/cart_controller_spec.rb @@ -5,14 +5,14 @@ module OpenFoodNetwork describe CartController, type: :controller do render_views - let(:user) { FactoryGirl.create(:user) } + let(:user) { FactoryBot.create(:user) } let(:product1) do - p1 = FactoryGirl.create(:product) + p1 = FactoryBot.create(:product) p1.update_column(:count_on_hand, 10) p1 end let(:cart) { Cart.create(user: user) } - let(:distributor) { FactoryGirl.create(:distributor_enterprise) } + let(:distributor) { FactoryBot.create(:distributor_enterprise) } before do end @@ -30,7 +30,7 @@ module OpenFoodNetwork end context 'with an empty order' do - let(:order) { FactoryGirl.create(:order, distributor: distributor) } + let(:order) { FactoryBot.create(:order, distributor: distributor) } before(:each) do cart.orders << order @@ -48,9 +48,9 @@ module OpenFoodNetwork end context 'an order with line items' do - let(:product) { FactoryGirl.create(:product, distributors: [ distributor ]) } - let(:order) { FactoryGirl.create(:order, { distributor: distributor } ) } - let(:line_item) { FactoryGirl.create(:line_item, { variant: product.master }) } + let(:product) { FactoryBot.create(:product, distributors: [ distributor ]) } + let(:order) { FactoryBot.create(:order, { distributor: distributor } ) } + let(:line_item) { FactoryBot.create(:line_item, { variant: product.master }) } before(:each) do order.line_items << line_item diff --git a/spec/controllers/shop_controller_spec.rb b/spec/controllers/shop_controller_spec.rb index 52c0bd4174..12b63d2ff8 100644 --- a/spec/controllers/shop_controller_spec.rb +++ b/spec/controllers/shop_controller_spec.rb @@ -56,6 +56,20 @@ describe ShopController, type: :controller do spree_get :order_cycle response.body.should have_content oc1.id end + + context "when the order cycle has already been set" do + let(:oc1) { create(:simple_order_cycle, distributors: [distributor]) } + let(:oc2) { create(:simple_order_cycle, distributors: [distributor]) } + let(:order) { create(:order, order_cycle: oc1) } + + before { allow(controller).to receive(:current_order) { order } } + + it "returns the new order cycle details" do + spree_post :order_cycle, order_cycle_id: oc2.id + expect(response).to be_success + expect(response.body).to have_content oc2.id + end + end end it "should not allow the user to select an invalid order cycle" do @@ -143,12 +157,12 @@ describe ShopController, type: :controller do let!(:tag_rule) { create(:filter_products_tag_rule, enterprise: distributor, preferred_customer_tags: "member", - preferred_variant_tags: "members-only") + preferred_variant_tags: "members-only") } let!(:default_tag_rule) { create(:filter_products_tag_rule, enterprise: distributor, is_default: true, - preferred_variant_tags: "members-only") + preferred_variant_tags: "members-only") } let(:product1) { { "id" => 1, "name" => 'product 1', "variants" => [{ "id" => 4, "tag_list" => ["members-only"] }] } } let(:product2) { { "id" => 2, "name" => 'product 2', "variants" => [{ "id" => 5, "tag_list" => ["members-only"] }, {"id" => 9, "tag_list" => ["something"]}] } } diff --git a/spec/controllers/spree/admin/line_items_controller_spec.rb b/spec/controllers/spree/admin/line_items_controller_spec.rb index 1420d4910d..5b7575e7e5 100644 --- a/spec/controllers/spree/admin/line_items_controller_spec.rb +++ b/spec/controllers/spree/admin/line_items_controller_spec.rb @@ -25,8 +25,8 @@ describe Spree::Admin::LineItemsController, type: :controller do let(:distributor1) { create(:distributor_enterprise) } let(:coordinator) { create(:distributor_enterprise) } let(:order_cycle) { create(:simple_order_cycle, coordinator: coordinator) } - let!(:order1) { FactoryGirl.create(:order, order_cycle: order_cycle, state: 'complete', completed_at: Time.zone.now, distributor: distributor1, billing_address: FactoryGirl.create(:address) ) } - let!(:line_item1) { FactoryGirl.create(:line_item, order: order1, product: FactoryGirl.create(:product, supplier: supplier)) } + let!(:order1) { FactoryBot.create(:order, order_cycle: order_cycle, state: 'complete', completed_at: Time.zone.now, distributor: distributor1, billing_address: FactoryBot.create(:address) ) } + let!(:line_item1) { FactoryBot.create(:line_item, order: order1, product: FactoryBot.create(:product, supplier: supplier)) } let(:line_item_params) { { quantity: 3, final_weight_volume: 3000, price: 3.00 } } let(:params) { { id: line_item1.id, order_id: order1.number, line_item: line_item_params } } diff --git a/spec/controllers/spree/admin/orders/customer_details_controller_spec.rb b/spec/controllers/spree/admin/orders/customer_details_controller_spec.rb new file mode 100644 index 0000000000..bd78bc2fd9 --- /dev/null +++ b/spec/controllers/spree/admin/orders/customer_details_controller_spec.rb @@ -0,0 +1,63 @@ +require 'spec_helper' + +describe Spree::Admin::Orders::CustomerDetailsController, type: :controller do + include AuthenticationWorkflow + + describe "#update" do + context "adding customer details via newly created admin order" do + let!(:user) { create(:user) } + let(:address) { create(:address) } + let!(:distributor) { create(:distributor_enterprise) } + let!(:shipping_method) { create(:shipping_method) } + let!(:order) { + create( + :order_with_totals_and_distribution, + state: 'cart', + shipping_method: shipping_method, + distributor: distributor, + user: nil, + email: nil, + bill_address: nil, + ship_address: nil, + ) + } + let(:address_params) { + { + firstname: address.firstname, + lastname: address.lastname, + address1: address.address1, + address2: address.address2, + city: address.city, + zipcode: address.zipcode, + country_id: address.country_id, + state_id: address.state_id, + phone: address.phone + } + } + + before do + login_as_enterprise_user [order.distributor] + end + + context "when adding details of a registered user" do + it "redirects to shipments on success" do + spree_post :update, order: { email: user.email, bill_address_attributes: address_params, ship_address_attributes: address_params }, order_id: order.number + + order.reload + + expect(response).to redirect_to spree.edit_admin_order_shipment_path(order, order.shipment) + end + end + + context "when adding details of an unregistered user" do + it "redirects to shipments on success" do + spree_post :update, order: { email: 'unregistered@email.com', bill_address_attributes: address_params, ship_address_attributes: address_params }, order_id: order.number + + order.reload + + expect(response).to redirect_to spree.edit_admin_order_shipment_path(order, order.shipment) + end + end + end + end +end diff --git a/spec/controllers/spree/admin/orders_controller_spec.rb b/spec/controllers/spree/admin/orders_controller_spec.rb index 752c7f1dc8..25c2683d4d 100644 --- a/spec/controllers/spree/admin/orders_controller_spec.rb +++ b/spec/controllers/spree/admin/orders_controller_spec.rb @@ -35,14 +35,14 @@ describe Spree::Admin::OrdersController, type: :controller do let(:order_attributes) { [:id, :full_name, :email, :phone, :completed_at, :distributor, :order_cycle, :number] } def self.make_simple_data! - let!(:dist1) { FactoryGirl.create(:distributor_enterprise) } - let!(:order1) { FactoryGirl.create(:order, state: 'complete', completed_at: Time.zone.now, distributor: dist1, billing_address: FactoryGirl.create(:address) ) } - let!(:order2) { FactoryGirl.create(:order, state: 'complete', completed_at: Time.zone.now, distributor: dist1, billing_address: FactoryGirl.create(:address) ) } - let!(:order3) { FactoryGirl.create(:order, state: 'complete', completed_at: Time.zone.now, distributor: dist1, billing_address: FactoryGirl.create(:address) ) } - let!(:line_item1) { FactoryGirl.create(:line_item, order: order1) } - let!(:line_item2) { FactoryGirl.create(:line_item, order: order2) } - let!(:line_item3) { FactoryGirl.create(:line_item, order: order2) } - let!(:line_item4) { FactoryGirl.create(:line_item, order: order3) } + let!(:dist1) { FactoryBot.create(:distributor_enterprise) } + let!(:order1) { FactoryBot.create(:order, state: 'complete', completed_at: Time.zone.now, distributor: dist1, billing_address: FactoryBot.create(:address) ) } + let!(:order2) { FactoryBot.create(:order, state: 'complete', completed_at: Time.zone.now, distributor: dist1, billing_address: FactoryBot.create(:address) ) } + let!(:order3) { FactoryBot.create(:order, state: 'complete', completed_at: Time.zone.now, distributor: dist1, billing_address: FactoryBot.create(:address) ) } + let!(:line_item1) { FactoryBot.create(:line_item, order: order1) } + let!(:line_item2) { FactoryBot.create(:line_item, order: order2) } + let!(:line_item3) { FactoryBot.create(:line_item, order: order2) } + let!(:line_item4) { FactoryBot.create(:line_item, order: order3) } let(:line_item_attributes) { [:id, :quantity, :max_quantity, :supplier, :units_product, :units_variant] } end @@ -95,11 +95,11 @@ describe Spree::Admin::OrdersController, type: :controller do let(:distributor2) { create(:distributor_enterprise) } let(:coordinator) { create(:distributor_enterprise) } let(:order_cycle) { create(:simple_order_cycle, coordinator: coordinator) } - let!(:order1) { FactoryGirl.create(:order, order_cycle: order_cycle, state: 'complete', completed_at: Time.zone.now, distributor: distributor1, billing_address: FactoryGirl.create(:address) ) } - let!(:line_item1) { FactoryGirl.create(:line_item, order: order1, product: FactoryGirl.create(:product, supplier: supplier)) } - let!(:line_item2) { FactoryGirl.create(:line_item, order: order1, product: FactoryGirl.create(:product, supplier: supplier)) } - let!(:order2) { FactoryGirl.create(:order, order_cycle: order_cycle, state: 'complete', completed_at: Time.zone.now, distributor: distributor2, billing_address: FactoryGirl.create(:address) ) } - let!(:line_item3) { FactoryGirl.create(:line_item, order: order2, product: FactoryGirl.create(:product, supplier: supplier)) } + let!(:order1) { FactoryBot.create(:order, order_cycle: order_cycle, state: 'complete', completed_at: Time.zone.now, distributor: distributor1, billing_address: FactoryBot.create(:address) ) } + let!(:line_item1) { FactoryBot.create(:line_item, order: order1, product: FactoryBot.create(:product, supplier: supplier)) } + let!(:line_item2) { FactoryBot.create(:line_item, order: order1, product: FactoryBot.create(:product, supplier: supplier)) } + let!(:order2) { FactoryBot.create(:order, order_cycle: order_cycle, state: 'complete', completed_at: Time.zone.now, distributor: distributor2, billing_address: FactoryBot.create(:address) ) } + let!(:line_item3) { FactoryBot.create(:line_item, order: order2, product: FactoryBot.create(:product, supplier: supplier)) } context "producer enterprise" do diff --git a/spec/controllers/spree/admin/overview_controller_spec.rb b/spec/controllers/spree/admin/overview_controller_spec.rb index 7b024f10bf..e445932555 100644 --- a/spec/controllers/spree/admin/overview_controller_spec.rb +++ b/spec/controllers/spree/admin/overview_controller_spec.rb @@ -2,18 +2,20 @@ require 'spec_helper' describe Spree::Admin::OverviewController, type: :controller do include AuthenticationWorkflow - context "loading overview" do - let(:user) { create_enterprise_user(enterprise_limit: 2) } + describe "#index" do before do - controller.stub spree_current_user: user + allow(controller).to receive(:spree_current_user).and_return(user) end context "when user owns only one enterprise" do + let(:user) { create_enterprise_user } let!(:enterprise) { create(:distributor_enterprise, owner: user) } context "when the referer is not an admin page" do - before { @request.env['HTTP_REFERER'] = 'http://test.com/some_other_path' } + before do + @request.env['HTTP_REFERER'] = 'http://test.com/not_admin_path' + end context "and the enterprise has sells='unspecified'" do before do @@ -22,14 +24,15 @@ describe Spree::Admin::OverviewController, type: :controller do it "redirects to the welcome page for the enterprise" do spree_get :index - response.should redirect_to welcome_admin_enterprise_path(enterprise) + expect(response) + .to redirect_to welcome_admin_enterprise_path(enterprise) end end context "and the enterprise does not have sells='unspecified'" do it "renders the single enterprise dashboard" do spree_get :index - response.should render_template "single_enterprise_dashboard" + expect(response).to render_template :single_enterprise_dashboard end end end @@ -39,17 +42,21 @@ describe Spree::Admin::OverviewController, type: :controller do it "renders the single enterprise dashboard" do spree_get :index - response.should render_template "single_enterprise_dashboard" + expect(response).to render_template :single_enterprise_dashboard end end end context "when user owns multiple enterprises" do + let(:user) { create_enterprise_user(enterprise_limit: 2) } + let!(:enterprise1) { create(:distributor_enterprise, owner: user) } - let!(:enterprise2) { create(:distributor_enterprise, owner: user) } + before { create(:distributor_enterprise, owner: user) } context "when the referer is not an admin page" do - before { @request.env['HTTP_REFERER'] = 'http://test.com/some_other_path' } + before do + @request.env['HTTP_REFERER'] = 'http://test.com/not_admin_path' + end context "and at least one owned enterprise has sells='unspecified'" do before do @@ -58,14 +65,14 @@ describe Spree::Admin::OverviewController, type: :controller do it "redirects to the enterprises index" do spree_get :index - response.should redirect_to admin_enterprises_path + expect(response).to redirect_to admin_enterprises_path end end context "and no owned enterprises have sells='unspecified'" do it "renders the multiple enterprise dashboard" do spree_get :index - response.should render_template "multi_enterprise_dashboard" + expect(response).to render_template :multi_enterprise_dashboard end end end @@ -75,7 +82,7 @@ describe Spree::Admin::OverviewController, type: :controller do it "renders the multiple enterprise dashboard" do spree_get :index - response.should render_template "multi_enterprise_dashboard" + expect(response).to render_template :multi_enterprise_dashboard end end end diff --git a/spec/controllers/spree/admin/products_controller_spec.rb b/spec/controllers/spree/admin/products_controller_spec.rb index 8632105b3e..d5b1f861b5 100644 --- a/spec/controllers/spree/admin/products_controller_spec.rb +++ b/spec/controllers/spree/admin/products_controller_spec.rb @@ -23,7 +23,7 @@ describe Spree::Admin::ProductsController, type: :controller do context "creating a new product" do before { login_as_admin } - it "redirects to bulk_edit when the user hits 'create'" do + it "redirects to products when the user hits 'create'" do s = create(:supplier_enterprise) t = create(:taxon) spree_post :create, { @@ -40,7 +40,7 @@ describe Spree::Admin::ProductsController, type: :controller do }, button: 'create' } - response.should redirect_to "/admin/products/bulk_edit" + response.should redirect_to spree.admin_products_path end it "redirects to new when the user hits 'add_another'" do diff --git a/spec/controllers/spree/admin/reports_controller_spec.rb b/spec/controllers/spree/admin/reports_controller_spec.rb index 82186b9b9d..3c884967c9 100644 --- a/spec/controllers/spree/admin/reports_controller_spec.rb +++ b/spec/controllers/spree/admin/reports_controller_spec.rb @@ -3,55 +3,55 @@ require 'spec_helper' describe Spree::Admin::ReportsController, type: :controller do # Given two distributors and two suppliers - let(:ba) { create(:address) } - let(:sa) { create(:address) } - let(:si) { "pick up on thursday please" } - let(:c1) { create(:distributor_enterprise) } - let(:c2) { create(:distributor_enterprise) } - let(:s1) { create(:supplier_enterprise) } - let(:s2) { create(:supplier_enterprise) } - let(:s3) { create(:supplier_enterprise) } - let(:d1) { create(:distributor_enterprise) } - let(:d2) { create(:distributor_enterprise) } - let(:d3) { create(:distributor_enterprise) } - let(:p1) { create(:product, price: 12.34, distributors: [d1], supplier: s1) } - let(:p2) { create(:product, price: 23.45, distributors: [d2], supplier: s2) } - let(:p3) { create(:product, price: 34.56, distributors: [d3], supplier: s3) } + let(:bill_address) { create(:address) } + let(:ship_address) { create(:address) } + let(:instructions) { "pick up on thursday please" } + let(:coordinator1) { create(:distributor_enterprise) } + let(:coordinator2) { create(:distributor_enterprise) } + let(:supplier1) { create(:supplier_enterprise) } + let(:supplier2) { create(:supplier_enterprise) } + let(:supplier3) { create(:supplier_enterprise) } + let(:distributor1) { create(:distributor_enterprise) } + let(:distributor2) { create(:distributor_enterprise) } + let(:distributor3) { create(:distributor_enterprise) } + let(:product1) { create(:product, price: 12.34, distributors: [distributor1], supplier: supplier1) } + let(:product2) { create(:product, price: 23.45, distributors: [distributor2], supplier: supplier2) } + let(:product3) { create(:product, price: 34.56, distributors: [distributor3], supplier: supplier3) } # Given two order cycles with both distributors - let(:ocA) { create(:simple_order_cycle, coordinator: c1, distributors: [d1, d2], suppliers: [s1, s2, s3], variants: [p1.master, p3.master]) } - let(:ocB) { create(:simple_order_cycle, coordinator: c2, distributors: [d1, d2], suppliers: [s1, s2, s3], variants: [p2.master]) } + let(:ocA) { create(:simple_order_cycle, coordinator: coordinator1, distributors: [distributor1, distributor2], suppliers: [supplier1, supplier2, supplier3], variants: [product1.master, product3.master]) } + let(:ocB) { create(:simple_order_cycle, coordinator: coordinator2, distributors: [distributor1, distributor2], suppliers: [supplier1, supplier2, supplier3], variants: [product2.master]) } - # orderA1 can only be accessed by s1, s3 and d1 - let!(:orderA1) do - order = create(:order, distributor: d1, bill_address: ba, ship_address: sa, special_instructions: si, order_cycle: ocA) - order.line_items << create(:line_item, variant: p1.master) - order.line_items << create(:line_item, variant: p3.master) + # orderA1 can only be accessed by supplier1, supplier3 and distributor1 + let(:orderA1) do + order = create(:order, distributor: distributor1, bill_address: bill_address, ship_address: ship_address, special_instructions: instructions, order_cycle: ocA) + order.line_items << create(:line_item, variant: product1.master) + order.line_items << create(:line_item, variant: product3.master) order.finalize! order.save order end - # orderA2 can only be accessed by s2 and d2 - let!(:orderA2) do - order = create(:order, distributor: d2, bill_address: ba, ship_address: sa, special_instructions: si, order_cycle: ocA) - order.line_items << create(:line_item, variant: p2.master) + # orderA2 can only be accessed by supplier2 and distributor2 + let(:orderA2) do + order = create(:order, distributor: distributor2, bill_address: bill_address, ship_address: ship_address, special_instructions: instructions, order_cycle: ocA) + order.line_items << create(:line_item, variant: product2.master) order.finalize! order.save order end - # orderB1 can only be accessed by s1, s3 and d1 - let!(:orderB1) do - order = create(:order, distributor: d1, bill_address: ba, ship_address: sa, special_instructions: si, order_cycle: ocB) - order.line_items << create(:line_item, variant: p1.master) - order.line_items << create(:line_item, variant: p3.master) + # orderB1 can only be accessed by supplier1, supplier3 and distributor1 + let(:orderB1) do + order = create(:order, distributor: distributor1, bill_address: bill_address, ship_address: ship_address, special_instructions: instructions, order_cycle: ocB) + order.line_items << create(:line_item, variant: product1.master) + order.line_items << create(:line_item, variant: product3.master) order.finalize! order.save order end - # orderB2 can only be accessed by s2 and d2 - let!(:orderB2) do - order = create(:order, distributor: d2, bill_address: ba, ship_address: sa, special_instructions: si, order_cycle: ocB) - order.line_items << create(:line_item, variant: p2.master) + # orderB2 can only be accessed by supplier2 and distributor2 + let(:orderB2) do + order = create(:order, distributor: distributor2, bill_address: bill_address, ship_address: ship_address, special_instructions: instructions, order_cycle: ocB) + order.line_items << create(:line_item, variant: product2.master) order.finalize! order.save order @@ -62,74 +62,90 @@ describe Spree::Admin::ReportsController, type: :controller do let(:resulting_orders) { assigns(:report).table_items.map(&:order) } let(:resulting_products) { assigns(:report).table_items.map(&:product) } - # As manager of a coordinator (c1) + # As manager of a coordinator (coordinator1) context "Coordinator Enterprise User" do - before { login_as_enterprise_user [c1] } + let!(:present_objects) { [orderA1, orderA2, orderB1, orderB2] } + + before { login_as_enterprise_user [coordinator1] } describe 'Orders & Fulfillment' do it "shows all orders in order cycles I coordinate" do - spree_get :orders_and_fulfillment + spree_post :orders_and_fulfillment, {q: {}} - resulting_orders.should include orderA1, orderA2 - resulting_orders.should_not include orderB1, orderB2 + expect(resulting_orders).to include orderA1, orderA2 + expect(resulting_orders).not_to include orderB1, orderB2 end end end - # As a Distributor Enterprise user for d1 + # As a Distributor Enterprise user for distributor1 context "Distributor Enterprise User" do - before { login_as_enterprise_user [d1] } + before { login_as_enterprise_user [distributor1] } describe 'Orders and Distributors' do - it "only shows orders that I have access to" do - spree_get :orders_and_distributors + let!(:present_objects) { [orderA1, orderA2, orderB1, orderB2] } - assigns(:search).result.should include(orderA1, orderB1) - assigns(:search).result.should_not include(orderA2) - assigns(:search).result.should_not include(orderB2) + it "only shows orders that I have access to" do + spree_post :orders_and_distributors + + expect(assigns(:search).result).to include(orderA1, orderB1) + expect(assigns(:search).result).not_to include(orderA2) + expect(assigns(:search).result).not_to include(orderB2) end end describe 'Bulk Coop' do - it "only shows orders that I have access to" do - spree_get :bulk_coop + let!(:present_objects) { [orderA1, orderA2, orderB1, orderB2] } - resulting_orders.should include(orderA1, orderB1) - resulting_orders.should_not include(orderA2) - resulting_orders.should_not include(orderB2) + it "only shows orders that I have access to" do + spree_post :bulk_coop, {q: {}} + + expect(resulting_orders).to include(orderA1, orderB1) + expect(resulting_orders).not_to include(orderA2) + expect(resulting_orders).not_to include(orderB2) end end describe 'Payments' do - it "only shows orders that I have access to" do - spree_get :payments + let!(:present_objects) { [orderA1, orderA2, orderB1, orderB2] } - resulting_orders_prelim.should include(orderA1, orderB1) - resulting_orders_prelim.should_not include(orderA2) - resulting_orders_prelim.should_not include(orderB2) + it "only shows orders that I have access to" do + spree_post :payments + + expect(resulting_orders_prelim).to include(orderA1, orderB1) + expect(resulting_orders_prelim).not_to include(orderA2) + expect(resulting_orders_prelim).not_to include(orderB2) end end describe 'Orders & Fulfillment' do - it "only shows orders that I distribute" do - spree_get :orders_and_fulfillment + context "with four orders" do + let!(:present_objects) { [orderA1, orderA2, orderB1, orderB2] } - resulting_orders.should include orderA1, orderB1 - resulting_orders.should_not include orderA2, orderB2 + it "only shows orders that I distribute" do + spree_post :orders_and_fulfillment, {q: {}} + + expect(resulting_orders).to include orderA1, orderB1 + expect(resulting_orders).not_to include orderA2, orderB2 + end end - it "only shows the selected order cycle" do - spree_get :orders_and_fulfillment, q: {order_cycle_id_in: [ocA.id.to_s]} + context "with two orders" do + let!(:present_objects) { [orderA1, orderB1] } - resulting_orders.should include(orderA1) - resulting_orders.should_not include(orderB1) + it "only shows the selected order cycle" do + spree_post :orders_and_fulfillment, q: {order_cycle_id_in: [ocA.id.to_s]} + + expect(resulting_orders).to include(orderA1) + expect(resulting_orders).not_to include(orderB1) + end end end end - # As a Supplier Enterprise user for s1 + # As a Supplier Enterprise user for supplier1 context "Supplier" do - before { login_as_enterprise_user [s1] } + before { login_as_enterprise_user [supplier1] } describe 'index' do it "loads reports relevant to producers" do @@ -143,53 +159,57 @@ describe Spree::Admin::ReportsController, type: :controller do describe 'Bulk Coop' do context "where I have granted P-OC to the distributor" do + let!(:present_objects) { [orderA1, orderA2] } + before do - create(:enterprise_relationship, parent: s1, child: d1, permissions_list: [:add_to_order_cycle]) + create(:enterprise_relationship, parent: supplier1, child: distributor1, permissions_list: [:add_to_order_cycle]) end it "only shows product line items that I am supplying" do - spree_get :bulk_coop + spree_post :bulk_coop, {q: {}} - resulting_products.should include p1 - resulting_products.should_not include p2, p3 + expect(resulting_products).to include product1 + expect(resulting_products).not_to include product2, product3 end end context "where I have not granted P-OC to the distributor" do it "shows product line items that I am supplying" do - spree_get :bulk_coop + spree_post :bulk_coop - resulting_products.should_not include p1, p2, p3 + expect(resulting_products).not_to include product1, product2, product3 end end end describe 'Orders & Fulfillment' do + let!(:present_objects) { [orderA1, orderA2] } + context "where I have granted P-OC to the distributor" do before do - create(:enterprise_relationship, parent: s1, child: d1, permissions_list: [:add_to_order_cycle]) + create(:enterprise_relationship, parent: supplier1, child: distributor1, permissions_list: [:add_to_order_cycle]) end it "only shows product line items that I am supplying" do - spree_get :orders_and_fulfillment + spree_post :orders_and_fulfillment, {q: {}} - resulting_products.should include p1 - resulting_products.should_not include p2, p3 + expect(resulting_products).to include product1 + expect(resulting_products).not_to include product2, product3 end it "only shows the selected order cycle" do - spree_get :orders_and_fulfillment, q: {order_cycle_id_eq: ocA.id} + spree_post :orders_and_fulfillment, q: {order_cycle_id_eq: ocA.id} - resulting_orders_prelim.should include(orderA1) - resulting_orders_prelim.should_not include(orderB1) + expect(resulting_orders_prelim).to include(orderA1) + expect(resulting_orders_prelim).not_to include(orderB1) end end context "where I have not granted P-OC to the distributor" do it "does not show me line_items I supply" do - spree_get :orders_and_fulfillment + spree_post :orders_and_fulfillment - resulting_products.should_not include p1, p2, p3 + expect(resulting_products).not_to include product1, product2, product3 end end end @@ -198,34 +218,44 @@ describe Spree::Admin::ReportsController, type: :controller do context "Products & Inventory" do before { login_as_admin } - it "should build distributors for the current user" do - spree_get :products_and_inventory - assigns(:distributors).should match_array [c1, c2, d1, d2, d3] + context "with distributors and suppliers" do + let(:distributors) { [coordinator1, distributor1, distributor2] } + let(:suppliers) { [supplier1, supplier2] } + let!(:present_objects) { [distributors, suppliers] } + + it "should build distributors for the current user" do + spree_get :products_and_inventory + expect(assigns(:distributors)).to match_array distributors + end + + it "builds suppliers for the current user" do + spree_get :products_and_inventory + expect(assigns(:suppliers)).to match_array suppliers + end end - it "builds suppliers for the current user" do - spree_get :products_and_inventory - assigns(:suppliers).should match_array [s1, s2, s3] - end + context "with order cycles" do + let!(:order_cycles) { [ocA, ocB] } - it "builds order cycles for the current user" do - spree_get :products_and_inventory - assigns(:order_cycles).should match_array [ocB, ocA] + it "builds order cycles for the current user" do + spree_get :products_and_inventory + expect(assigns(:order_cycles)).to match_array order_cycles + end end it "assigns report types" do spree_get :products_and_inventory - assigns(:report_types).should == subject.report_types[:products_and_inventory] + expect(assigns(:report_types)).to eq(subject.report_types[:products_and_inventory]) end it "creates a ProductAndInventoryReport" do - OpenFoodNetwork::ProductsAndInventoryReport.should_receive(:new) - .with(@admin_user, {"test" => "foo", "controller" => "spree/admin/reports", "action" => "products_and_inventory"}) + expect(OpenFoodNetwork::ProductsAndInventoryReport).to receive(:new) + .with(@admin_user, {"test" => "foo", "controller" => "spree/admin/reports", "action" => "products_and_inventory"}, false) .and_return(report = double(:report)) - report.stub(:header).and_return [] - report.stub(:table).and_return [] + allow(report).to receive(:header).and_return [] + allow(report).to receive(:table).and_return [] spree_get :products_and_inventory, test: "foo" - assigns(:report).should == report + expect(assigns(:report)).to eq(report) end end @@ -233,40 +263,75 @@ describe Spree::Admin::ReportsController, type: :controller do before { login_as_admin } it "should have report types for customers" do - subject.report_types[:customers].should == [ + expect(subject.report_types[:customers]).to eq([ ["Mailing List", :mailing_list], ["Addresses", :addresses] - ] + ]) end - it "should build distributors for the current user" do - spree_get :customers - assigns(:distributors).should match_array [c1, c2, d1, d2, d3] + context "with distributors and suppliers" do + let(:distributors) { [coordinator1, distributor1, distributor2] } + let(:suppliers) { [supplier1, supplier2] } + let!(:present_objects) { [distributors, suppliers] } + + it "should build distributors for the current user" do + spree_get :customers + expect(assigns(:distributors)).to match_array distributors + end + + it "builds suppliers for the current user" do + spree_get :customers + expect(assigns(:suppliers)).to match_array suppliers + end end - it "builds suppliers for the current user" do - spree_get :customers - assigns(:suppliers).should match_array [s1, s2, s3] - end + context "with order cycles" do + let!(:order_cycles) { [ocA, ocB] } - it "builds order cycles for the current user" do - spree_get :customers - assigns(:order_cycles).should match_array [ocB, ocA] + it "builds order cycles for the current user" do + spree_get :customers + expect(assigns(:order_cycles)).to match_array order_cycles + end end it "assigns report types" do spree_get :customers - assigns(:report_types).should == subject.report_types[:customers] + expect(assigns(:report_types)).to eq(subject.report_types[:customers]) end it "creates a CustomersReport" do - OpenFoodNetwork::CustomersReport.should_receive(:new) - .with(@admin_user, {"test" => "foo", "controller" => "spree/admin/reports", "action" => "customers"}) + expect(OpenFoodNetwork::CustomersReport).to receive(:new) + .with(@admin_user, {"test" => "foo", "controller" => "spree/admin/reports", "action" => "customers"}, false) .and_return(report = double(:report)) - report.stub(:header).and_return [] - report.stub(:table).and_return [] + allow(report).to receive(:header).and_return [] + allow(report).to receive(:table).and_return [] spree_get :customers, test: "foo" - assigns(:report).should == report + expect(assigns(:report)).to eq(report) + end + end + + context "Admin" do + before { login_as_admin } + + describe "users_and_enterprises" do + let!(:present_objects) { [coordinator1] } + + it "shows report search forms" do + spree_get :users_and_enterprises + expect(assigns(:report).table).to eq [] + end + + it "shows report data" do + spree_post :users_and_enterprises, {q: {}} + expect(assigns(:report).table.empty?).to be false + end + end + + describe "sales_tax" do + it "shows report search forms" do + spree_get :sales_tax + expect(assigns(:report).table).to eq [] + end end end end diff --git a/spec/controllers/spree/api/line_items_controller_spec.rb b/spec/controllers/spree/api/line_items_controller_spec.rb index cf864aabc5..47c09f36b6 100644 --- a/spec/controllers/spree/api/line_items_controller_spec.rb +++ b/spec/controllers/spree/api/line_items_controller_spec.rb @@ -12,8 +12,8 @@ module Spree context "as an admin user" do sign_in_as_admin! - let(:order) { FactoryGirl.create(:order, state: 'complete', completed_at: Time.zone.now) } - let(:line_item) { FactoryGirl.create(:line_item, order: order, final_weight_volume: 500) } + let(:order) { FactoryBot.create(:order, state: 'complete', completed_at: Time.zone.now) } + let(:line_item) { FactoryBot.create(:line_item, order: order, final_weight_volume: 500) } context "as a line item is updated" do before { allow(controller).to receive(:order) { order } } diff --git a/spec/controllers/spree/api/products_controller_spec.rb b/spec/controllers/spree/api/products_controller_spec.rb index d952c42537..b3d518a9da 100644 --- a/spec/controllers/spree/api/products_controller_spec.rb +++ b/spec/controllers/spree/api/products_controller_spec.rb @@ -75,8 +75,8 @@ module Spree end it "sorts products in ascending id order" do - FactoryGirl.create(:product, supplier: supplier) - FactoryGirl.create(:product, supplier: supplier) + FactoryBot.create(:product, supplier: supplier) + FactoryBot.create(:product, supplier: supplier) spree_get :index, { :template => 'bulk_index', :format => :json } @@ -99,7 +99,7 @@ module Spree spree_get :index, { :template => 'bulk_index', :format => :json } json_response.size.should == 1 - product5 = FactoryGirl.create(:product) + product5 = FactoryBot.create(:product) product5.available_on = nil product5.save! diff --git a/spec/controllers/spree/api/variants_controller_spec.rb b/spec/controllers/spree/api/variants_controller_spec.rb index 06f6185675..698a3e7926 100644 --- a/spec/controllers/spree/api/variants_controller_spec.rb +++ b/spec/controllers/spree/api/variants_controller_spec.rb @@ -4,10 +4,10 @@ module Spree describe Spree::Api::VariantsController, type: :controller do render_views - let(:supplier) { FactoryGirl.create(:supplier_enterprise) } - let!(:variant1) { FactoryGirl.create(:variant) } - let!(:variant2) { FactoryGirl.create(:variant) } - let!(:variant3) { FactoryGirl.create(:variant) } + let(:supplier) { FactoryBot.create(:supplier_enterprise) } + let!(:variant1) { FactoryBot.create(:variant) } + let!(:variant2) { FactoryBot.create(:variant) } + let!(:variant3) { FactoryBot.create(:variant) } let(:attributes) { [:id, :options_text, :price, :on_hand, :unit_value, :unit_description, :on_demand, :display_as, :display_name] } before do diff --git a/spec/controllers/spree/credit_cards_controller_spec.rb b/spec/controllers/spree/credit_cards_controller_spec.rb index 7806f51d5c..1a907a4fda 100644 --- a/spec/controllers/spree/credit_cards_controller_spec.rb +++ b/spec/controllers/spree/credit_cards_controller_spec.rb @@ -69,6 +69,53 @@ describe Spree::CreditCardsController, type: :controller do end end + describe "#update" do + let(:params) { { format: :json, credit_card: { is_default: true } } } + context "when the specified credit card is not found" do + before { params[:id] = 123 } + + it "renders a flash error" do + put :update, params + json_response = JSON.parse(response.body) + expect(json_response['flash']['error']).to eq I18n.t(:card_could_not_be_updated) + end + end + + context "when the specified credit card is found" do + let!(:card) { create(:credit_card, gateway_customer_profile_id: 'cus_AZNMJ') } + before { params[:id] = card.id } + + context "but the card is not owned by the user" do + it "redirects to unauthorized" do + put :update, params + expect(response).to redirect_to spree.unauthorized_path + end + end + + context "and the card is owned by the user" do + before { card.update_attribute(:user_id, user.id) } + + context "when the update completes successfully" do + it "renders a serialized copy of the updated card" do + expect{ put :update, params }.to change { card.reload.is_default }.to(true) + json_response = JSON.parse(response.body) + expect(json_response['id']).to eq card.id + expect(json_response['is_default']).to eq true + end + end + + context "when the update fails" do + before { params[:credit_card][:month] = 'some illegal month' } + it "renders an error" do + put :update, params + json_response = JSON.parse(response.body) + expect(json_response['flash']['error']).to eq I18n.t(:card_could_not_be_updated) + end + end + end + end + end + describe "#destroy" do context "when the specified credit card is not found" do let(:params) { { id: 123 } } diff --git a/spec/controllers/spree/users_controller_spec.rb b/spec/controllers/spree/users_controller_spec.rb index f361cba0b5..35e0e0f6a3 100644 --- a/spec/controllers/spree/users_controller_spec.rb +++ b/spec/controllers/spree/users_controller_spec.rb @@ -1,4 +1,5 @@ require 'spec_helper' +require 'spree/api/testing_support/helpers' describe Spree::UsersController, type: :controller do include AuthenticationWorkflow @@ -46,4 +47,18 @@ describe Spree::UsersController, type: :controller do expect(orders).not_to include d1o3 end end + + describe "registered_email" do + let!(:user) { create(:user) } + + it "returns true if email corresponds to a registered user" do + spree_post :registered_email, email: user.email + expect(json_response['registered']).to eq true + end + + it "returns false if email does not correspond to a registered user" do + spree_post :registered_email, email: 'nonregistereduser@example.com' + expect(json_response['registered']).to eq false + end + end end diff --git a/spec/controllers/user_passwords_controller_spec.rb b/spec/controllers/user_passwords_controller_spec.rb index 59a2f16280..42e505e172 100644 --- a/spec/controllers/user_passwords_controller_spec.rb +++ b/spec/controllers/user_passwords_controller_spec.rb @@ -3,6 +3,7 @@ require 'spree/api/testing_support/helpers' describe UserPasswordsController, type: :controller do let(:user) { create(:user) } + let(:unconfirmed_user) { create(:user, confirmed_at: nil) } before do @request.env["devise.mapping"] = Devise.mappings[:spree_user] @@ -44,11 +45,16 @@ describe UserPasswordsController, type: :controller do end describe "via ajax" do - it "returns errors" do + it "returns error when email not found" do xhr :post, :create, spree_user: {}, use_route: :spree - json = JSON.parse(response.body) - response.status.should == 401 - json.should == {"email"=>["can't be blank"]} + expect(response.status).to eq 404 + expect(json_response).to eq 'error' => I18n.t('email_not_found') + end + + it "returns error when user is unconfirmed" do + xhr :post, :create, spree_user: {email: unconfirmed_user.email}, use_route: :spree + expect(response.status).to eq 401 + expect(json_response).to eq 'error' => I18n.t('email_unconfirmed') end end end diff --git a/spec/factories.rb b/spec/factories.rb index 4731967055..215d9e0635 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -1,7 +1,7 @@ require 'ffaker' require 'spree/testing_support/factories' -# http://www.rubydoc.info/gems/factory_girl/file/GETTING_STARTED.md +# http://www.rubydoc.info/gems/factory_bot/file/GETTING_STARTED.md # # The spree_core gem defines factories in several files. For example: # @@ -15,7 +15,7 @@ require 'spree/testing_support/factories' # * order_with_inventory_unit_shipped # * completed_order_with_totals # -FactoryGirl.define do +FactoryBot.define do factory :classification, class: Spree::Classification do end @@ -85,7 +85,7 @@ FactoryGirl.define do orders_open_at { 1.day.ago } orders_close_at { 1.week.from_now } - coordinator { Enterprise.is_distributor.first || FactoryGirl.create(:distributor_enterprise) } + coordinator { Enterprise.is_distributor.first || FactoryBot.create(:distributor_enterprise) } transient do suppliers [] @@ -128,14 +128,14 @@ FactoryGirl.define do factory :exchange, :class => Exchange do incoming false - order_cycle { OrderCycle.first || FactoryGirl.create(:simple_order_cycle) } - sender { incoming ? FactoryGirl.create(:enterprise) : order_cycle.coordinator } - receiver { incoming ? order_cycle.coordinator : FactoryGirl.create(:enterprise) } + order_cycle { OrderCycle.first || FactoryBot.create(:simple_order_cycle) } + sender { incoming ? FactoryBot.create(:enterprise) : order_cycle.coordinator } + receiver { incoming ? order_cycle.coordinator : FactoryBot.create(:enterprise) } end factory :schedule, class: Schedule do sequence(:name) { |n| "Schedule #{n}" } - order_cycles { [OrderCycle.first || FactoryGirl.create(:simple_order_cycle)] } + order_cycles { [OrderCycle.first || FactoryBot.create(:simple_order_cycle)] } end factory :subscription, :class => Subscription do @@ -201,12 +201,12 @@ FactoryGirl.define do end factory :enterprise, :class => Enterprise do - owner { FactoryGirl.create :user } + owner { FactoryBot.create :user } sequence(:name) { |n| "Enterprise #{n}" } sells 'any' description 'enterprise' long_description '

Hello, world!

This is a paragraph.

' - address { FactoryGirl.create(:address) } + address { FactoryBot.create(:address) } end factory :supplier_enterprise, :parent => :enterprise do @@ -241,7 +241,7 @@ FactoryGirl.define do sequence(:permalink) { |n| "group#{n}" } description 'this is a group' on_front_page false - address { FactoryGirl.build(:address) } + address { FactoryBot.build(:address) } end sequence(:calculator_amount) @@ -255,21 +255,21 @@ FactoryGirl.define do sequence(:name) { |n| "Enterprise fee #{n}" } sequence(:fee_type) { |n| EnterpriseFee::FEE_TYPES[n % EnterpriseFee::FEE_TYPES.count] } - enterprise { Enterprise.first || FactoryGirl.create(:supplier_enterprise) } + enterprise { Enterprise.first || FactoryBot.create(:supplier_enterprise) } calculator { build(:calculator_per_item, preferred_amount: amount) } after(:create) { |ef| ef.calculator.save! } end factory :product_distribution, :class => ProductDistribution do - product { |pd| Spree::Product.first || FactoryGirl.create(:product) } - distributor { |pd| Enterprise.is_distributor.first || FactoryGirl.create(:distributor_enterprise) } - enterprise_fee { |pd| FactoryGirl.create(:enterprise_fee, enterprise: pd.distributor) } + product { |pd| Spree::Product.first || FactoryBot.create(:product) } + distributor { |pd| Enterprise.is_distributor.first || FactoryBot.create(:distributor_enterprise) } + enterprise_fee { |pd| FactoryBot.create(:enterprise_fee, enterprise: pd.distributor) } end factory :adjustment_metadata, :class => AdjustmentMetadata do - adjustment { FactoryGirl.create(:adjustment) } - enterprise { FactoryGirl.create(:distributor_enterprise) } + adjustment { FactoryBot.create(:adjustment) } + enterprise { FactoryBot.create(:distributor_enterprise) } fee_name 'fee' fee_type 'packing' enterprise_role 'distributor' @@ -286,7 +286,7 @@ FactoryGirl.define do after(:create) do |order| p = create(:simple_product, :distributors => [order.distributor]) - FactoryGirl.create(:line_item, :order => order, :product => p) + FactoryBot.create(:line_item, :order => order, :product => p) order.reload end end @@ -309,8 +309,8 @@ FactoryGirl.define do order.distributor.update_attribute(:charges_sales_tax, true) Spree::Zone.global.update_attribute(:default_tax, true) - p = FactoryGirl.create(:taxed_product, zone: Spree::Zone.global, price: proxy.product_price, tax_rate_amount: proxy.tax_rate_amount, tax_rate_name: proxy.tax_rate_name, distributors: [order.distributor]) - FactoryGirl.create(:line_item, order: order, product: p, price: p.price) + p = FactoryBot.create(:taxed_product, zone: Spree::Zone.global, price: proxy.product_price, tax_rate_amount: proxy.tax_rate_amount, tax_rate_name: proxy.tax_rate_name, distributors: [order.distributor]) + FactoryBot.create(:line_item, order: order, product: p, price: p.price) order.reload end end @@ -403,34 +403,34 @@ FactoryGirl.define do turnover { rand(100000).to_f/100 } account_invoice do AccountInvoice.where(user_id: owner_id, year: begins_at.year, month: begins_at.month).first || - FactoryGirl.create(:account_invoice, user: owner, year: begins_at.year, month: begins_at.month) + FactoryBot.create(:account_invoice, user: owner, year: begins_at.year, month: begins_at.month) end end factory :account_invoice do - user { FactoryGirl.create :user } + user { FactoryBot.create :user } year { 2000 + rand(100) } month { 1 + rand(12) } end factory :filter_order_cycles_tag_rule, class: TagRule::FilterOrderCycles do - enterprise { FactoryGirl.create :distributor_enterprise } + enterprise { FactoryBot.create :distributor_enterprise } end factory :filter_shipping_methods_tag_rule, class: TagRule::FilterShippingMethods do - enterprise { FactoryGirl.create :distributor_enterprise } + enterprise { FactoryBot.create :distributor_enterprise } end factory :filter_products_tag_rule, class: TagRule::FilterProducts do - enterprise { FactoryGirl.create :distributor_enterprise } + enterprise { FactoryBot.create :distributor_enterprise } end factory :filter_payment_methods_tag_rule, class: TagRule::FilterPaymentMethods do - enterprise { FactoryGirl.create :distributor_enterprise } + enterprise { FactoryBot.create :distributor_enterprise } end factory :tag_rule, class: TagRule::DiscountOrder do - enterprise { FactoryGirl.create :distributor_enterprise } + enterprise { FactoryBot.create :distributor_enterprise } before(:create) do |tr| tr.calculator = Spree::Calculator::FlatPercentItemTotal.new(calculable: tr) end @@ -442,7 +442,7 @@ FactoryGirl.define do end factory :stripe_account do - enterprise { FactoryGirl.create :distributor_enterprise } + enterprise { FactoryBot.create :distributor_enterprise } stripe_user_id "abc123" stripe_publishable_key "xyz456" end @@ -456,9 +456,9 @@ FactoryGirl.define do end -FactoryGirl.modify do +FactoryBot.modify do factory :product do - primary_taxon { Spree::Taxon.first || FactoryGirl.create(:taxon) } + primary_taxon { Spree::Taxon.first || FactoryBot.create(:taxon) } end factory :base_product do @@ -467,8 +467,8 @@ FactoryGirl.modify do # When this fix has been merged into a version of Spree that we're using, this line can be removed. sequence(:name) { |n| "Product ##{n} - #{Kernel.rand(9999)}" } - supplier { Enterprise.is_primary_producer.first || FactoryGirl.create(:supplier_enterprise) } - primary_taxon { Spree::Taxon.first || FactoryGirl.create(:taxon) } + supplier { Enterprise.is_primary_producer.first || FactoryBot.create(:supplier_enterprise) } + primary_taxon { Spree::Taxon.first || FactoryBot.create(:taxon) } unit_value 1 unit_description '' @@ -484,7 +484,7 @@ FactoryGirl.modify do end factory :shipping_method do - distributors { [Enterprise.is_distributor.first || FactoryGirl.create(:distributor_enterprise)] } + distributors { [Enterprise.is_distributor.first || FactoryBot.create(:distributor_enterprise)] } display_on '' end @@ -495,13 +495,13 @@ FactoryGirl.modify do factory :payment do transient do - distributor { order.distributor || Enterprise.is_distributor.first || FactoryGirl.create(:distributor_enterprise) } + distributor { order.distributor || Enterprise.is_distributor.first || FactoryBot.create(:distributor_enterprise) } end - payment_method { FactoryGirl.create(:payment_method, distributors: [distributor]) } + payment_method { FactoryBot.create(:payment_method, distributors: [distributor]) } end factory :payment_method do - distributors { [Enterprise.is_distributor.first || FactoryGirl.create(:distributor_enterprise)] } + distributors { [Enterprise.is_distributor.first || FactoryBot.create(:distributor_enterprise)] } end factory :option_type do diff --git a/spec/features/admin/bulk_product_update_spec.rb b/spec/features/admin/bulk_product_update_spec.rb index 91f28d5498..26090d4330 100644 --- a/spec/features/admin/bulk_product_update_spec.rb +++ b/spec/features/admin/bulk_product_update_spec.rb @@ -13,39 +13,39 @@ feature %q{ end it "displays a list of products" do - p1 = FactoryGirl.create(:product) - p2 = FactoryGirl.create(:product) + p1 = FactoryBot.create(:product) + p2 = FactoryBot.create(:product) - visit '/admin/products/bulk_edit' + visit spree.admin_products_path expect(page).to have_field "product_name", with: p1.name, :visible => true expect(page).to have_field "product_name", with: p2.name, :visible => true end it "displays a message when number of products is zero" do - visit '/admin/products/bulk_edit' + visit spree.admin_products_path expect(page).to have_text "No products yet. Why don't you add some?" end it "displays a select box for suppliers, with the appropriate supplier selected" do - s1 = FactoryGirl.create(:supplier_enterprise) - s2 = FactoryGirl.create(:supplier_enterprise) - s3 = FactoryGirl.create(:supplier_enterprise) - p1 = FactoryGirl.create(:product, supplier: s2) - p2 = FactoryGirl.create(:product, supplier: s3) + s1 = FactoryBot.create(:supplier_enterprise) + s2 = FactoryBot.create(:supplier_enterprise) + s3 = FactoryBot.create(:supplier_enterprise) + p1 = FactoryBot.create(:product, supplier: s2) + p2 = FactoryBot.create(:product, supplier: s3) - visit '/admin/products/bulk_edit' + visit spree.admin_products_path expect(page).to have_select "producer_id", with_options: [s1.name,s2.name,s3.name], selected: s2.name expect(page).to have_select "producer_id", with_options: [s1.name,s2.name,s3.name], selected: s3.name end it "displays a date input for available_on for each product, formatted to yyyy-mm-dd hh:mm:ss" do - p1 = FactoryGirl.create(:product, available_on: Date.current) - p2 = FactoryGirl.create(:product, available_on: Date.current-1) + p1 = FactoryBot.create(:product, available_on: Date.current) + p2 = FactoryBot.create(:product, available_on: Date.current-1) - visit '/admin/products/bulk_edit' + visit spree.admin_products_path find("div#columns-dropdown", :text => "COLUMNS").click find("div#columns-dropdown div.menu div.menu_item", text: "Available On").click find("div#columns-dropdown", :text => "COLUMNS").click @@ -55,12 +55,12 @@ feature %q{ end it "displays an on hand count in a span for each product" do - p1 = FactoryGirl.create(:product, on_hand: 15) + p1 = FactoryBot.create(:product, on_hand: 15) v1 = p1.variants.first v1.on_hand = 4 v1.save! - visit '/admin/products/bulk_edit' + visit spree.admin_products_path within "#p_#{p1.id}" do expect(page).to have_no_field "on_hand", with: "15" @@ -69,11 +69,11 @@ feature %q{ end it "displays 'on demand' for any variant that is available on demand" do - p1 = FactoryGirl.create(:product) - v1 = FactoryGirl.create(:variant, product: p1, is_master: false, on_hand: 4) - v2 = FactoryGirl.create(:variant, product: p1, is_master: false, on_hand: 0, on_demand: true) + p1 = FactoryBot.create(:product) + v1 = FactoryBot.create(:variant, product: p1, is_master: false, on_hand: 4) + v2 = FactoryBot.create(:variant, product: p1, is_master: false, on_hand: 0, on_demand: true) - visit '/admin/products/bulk_edit' + visit spree.admin_products_path expect(page).to have_selector "a.view-variants", count: 1 find("a.view-variants").trigger('click') @@ -84,17 +84,17 @@ feature %q{ end it "displays a select box for the unit of measure for the product's variants" do - p = FactoryGirl.create(:product, variant_unit: 'weight', variant_unit_scale: 1, variant_unit_name: '') + p = FactoryBot.create(:product, variant_unit: 'weight', variant_unit_scale: 1, variant_unit_name: '') - visit '/admin/products/bulk_edit' + visit spree.admin_products_path expect(page).to have_select "variant_unit_with_scale", selected: "Weight (g)" end it "displays a text field for the item name when unit is set to 'Items'" do - p = FactoryGirl.create(:product, variant_unit: 'items', variant_unit_scale: nil, variant_unit_name: 'packet') + p = FactoryBot.create(:product, variant_unit: 'items', variant_unit_scale: nil, variant_unit_name: 'packet') - visit '/admin/products/bulk_edit' + visit spree.admin_products_path expect(page).to have_select "variant_unit_with_scale", selected: "Items" expect(page).to have_field "variant_unit_name", with: "packet" @@ -107,10 +107,10 @@ feature %q{ end it "displays a list of variants for each product" do - v1 = FactoryGirl.create(:variant, display_name: "something1" ) - v2 = FactoryGirl.create(:variant, display_name: "something2" ) + v1 = FactoryBot.create(:variant, display_name: "something1" ) + v2 = FactoryBot.create(:variant, display_name: "something2" ) - visit '/admin/products/bulk_edit' + visit spree.admin_products_path expect(page).to have_selector "a.view-variants", count: 2 all("a.view-variants").each { |e| e.trigger('click') } @@ -121,11 +121,11 @@ feature %q{ end it "displays an on_hand input (for each variant) for each product" do - p1 = FactoryGirl.create(:product) - v1 = FactoryGirl.create(:variant, product: p1, is_master: false, on_hand: 15) - v2 = FactoryGirl.create(:variant, product: p1, is_master: false, on_hand: 6) + p1 = FactoryBot.create(:product) + v1 = FactoryBot.create(:variant, product: p1, is_master: false, on_hand: 15) + v2 = FactoryBot.create(:variant, product: p1, is_master: false, on_hand: 6) - visit '/admin/products/bulk_edit' + visit spree.admin_products_path expect(page).to have_selector "a.view-variants", count: 1 all("a.view-variants").each { |e| e.trigger('click') } @@ -136,11 +136,11 @@ feature %q{ it "displays a price input (for each variant) for each product" do - p1 = FactoryGirl.create(:product, price: 2.0) - v1 = FactoryGirl.create(:variant, product: p1, is_master: false, price: 12.75) - v2 = FactoryGirl.create(:variant, product: p1, is_master: false, price: 2.50) + p1 = FactoryBot.create(:product, price: 2.0) + v1 = FactoryBot.create(:variant, product: p1, is_master: false, price: 12.75) + v2 = FactoryBot.create(:variant, product: p1, is_master: false, price: 2.50) - visit '/admin/products/bulk_edit' + visit spree.admin_products_path expect(page).to have_selector "a.view-variants", count: 1 all("a.view-variants").each { |e| e.trigger('click') } @@ -150,11 +150,11 @@ feature %q{ end it "displays a unit value field (for each variant) for each product" do - p1 = FactoryGirl.create(:product, price: 2.0, variant_unit: "weight", variant_unit_scale: "1000") - v1 = FactoryGirl.create(:variant, product: p1, is_master: false, price: 12.75, unit_value: 1200, unit_description: "(small bag)", display_as: "bag") - v2 = FactoryGirl.create(:variant, product: p1, is_master: false, price: 2.50, unit_value: 4800, unit_description: "(large bag)", display_as: "bin") + p1 = FactoryBot.create(:product, price: 2.0, variant_unit: "weight", variant_unit_scale: "1000") + v1 = FactoryBot.create(:variant, product: p1, is_master: false, price: 12.75, unit_value: 1200, unit_description: "(small bag)", display_as: "bag") + v2 = FactoryBot.create(:variant, product: p1, is_master: false, price: 2.50, unit_value: 4800, unit_description: "(large bag)", display_as: "bin") - visit '/admin/products/bulk_edit' + visit spree.admin_products_path expect(page).to have_selector "a.view-variants", count: 1 all("a.view-variants").each { |e| e.trigger('click') } @@ -167,13 +167,13 @@ feature %q{ scenario "creating a new product" do - s = FactoryGirl.create(:supplier_enterprise) - d = FactoryGirl.create(:distributor_enterprise) + s = FactoryBot.create(:supplier_enterprise) + d = FactoryBot.create(:distributor_enterprise) taxon = create(:taxon) login_to_admin_section - visit '/admin/products/bulk_edit' + visit spree.admin_products_path find("a", text: "NEW PRODUCT").click expect(page).to have_content 'NEW PRODUCT' @@ -186,7 +186,7 @@ feature %q{ select taxon.name, from: 'product_primary_taxon_id' click_button 'Create' - expect(URI.parse(current_url).path).to eq '/admin/products/bulk_edit' + expect(URI.parse(current_url).path).to eq spree.admin_products_path expect(flash_message).to eq 'Product "Big Bag Of Apples" has been successfully created!' expect(page).to have_field "product_name", with: 'Big Bag Of Apples' end @@ -194,9 +194,9 @@ feature %q{ scenario "creating new variants" do # Given a product without variants or a unit - p = FactoryGirl.create(:product, variant_unit: 'weight', variant_unit_scale: 1000) + p = FactoryBot.create(:product, variant_unit: 'weight', variant_unit_scale: 1000) login_to_admin_section - visit '/admin/products/bulk_edit' + visit spree.admin_products_path # I should see an add variant button page.find('a.view-variants').trigger('click') @@ -238,15 +238,15 @@ feature %q{ end scenario "updating product attributes" do - s1 = FactoryGirl.create(:supplier_enterprise) - s2 = FactoryGirl.create(:supplier_enterprise) - t1 = FactoryGirl.create(:taxon) - t2 = FactoryGirl.create(:taxon) - p = FactoryGirl.create(:product, supplier: s1, available_on: Date.current, variant_unit: 'volume', variant_unit_scale: 1, primary_taxon: t2, sku: "OLD SKU") + s1 = FactoryBot.create(:supplier_enterprise) + s2 = FactoryBot.create(:supplier_enterprise) + t1 = FactoryBot.create(:taxon) + t2 = FactoryBot.create(:taxon) + p = FactoryBot.create(:product, supplier: s1, available_on: Date.current, variant_unit: 'volume', variant_unit_scale: 1, primary_taxon: t2, sku: "OLD SKU") login_to_admin_section - visit '/admin/products/bulk_edit' + visit spree.admin_products_path find("div#columns-dropdown", :text => "COLUMNS").click find("div#columns-dropdown div.menu div.menu_item", text: "Available On").click @@ -288,11 +288,11 @@ feature %q{ end scenario "updating a product with a variant unit of 'items'" do - p = FactoryGirl.create(:product, variant_unit: 'weight', variant_unit_scale: 1000) + p = FactoryBot.create(:product, variant_unit: 'weight', variant_unit_scale: 1000) login_to_admin_section - visit '/admin/products/bulk_edit' + visit spree.admin_products_path expect(page).to have_select "variant_unit_with_scale", selected: "Weight (kg)" @@ -309,16 +309,16 @@ feature %q{ end scenario "updating a product with variants" do - s1 = FactoryGirl.create(:supplier_enterprise) - s2 = FactoryGirl.create(:supplier_enterprise) - p = FactoryGirl.create(:product, supplier: s1, available_on: Date.current, variant_unit: 'volume', variant_unit_scale: 0.001, + s1 = FactoryBot.create(:supplier_enterprise) + s2 = FactoryBot.create(:supplier_enterprise) + p = FactoryBot.create(:product, supplier: s1, available_on: Date.current, variant_unit: 'volume', variant_unit_scale: 0.001, price: 3.0, on_hand: 9, unit_value: 0.25, unit_description: '(bottle)' ) v = p.variants.first v.update_column(:sku, "VARIANTSKU") login_to_admin_section - visit '/admin/products/bulk_edit' + visit spree.admin_products_path expect(page).to have_selector "a.view-variants", count: 1 find("a.view-variants").trigger('click') @@ -352,12 +352,12 @@ feature %q{ end scenario "updating delegated attributes of variants in isolation" do - p = FactoryGirl.create(:product) - v = FactoryGirl.create(:variant, product: p, price: 3.0) + p = FactoryBot.create(:product) + v = FactoryBot.create(:variant, product: p, price: 3.0) login_to_admin_section - visit '/admin/products/bulk_edit' + visit spree.admin_products_path expect(page).to have_selector "a.view-variants", count: 1 find("a.view-variants").trigger('click') @@ -378,10 +378,10 @@ feature %q{ end scenario "updating a product mutiple times without refresh" do - p = FactoryGirl.create(:product, name: 'original name') + p = FactoryBot.create(:product, name: 'original name') login_to_admin_section - visit '/admin/products/bulk_edit' + visit spree.admin_products_path expect(page).to have_field "product_name", with: "original name" @@ -411,10 +411,10 @@ feature %q{ end scenario "updating a product after cloning a product" do - p = FactoryGirl.create(:product, :name => "product 1") + p = FactoryBot.create(:product, :name => "product 1") login_to_admin_section - visit '/admin/products/bulk_edit' + visit spree.admin_products_path expect(page).to have_selector "a.clone-product", count: 1 find("a.clone-product").click @@ -433,11 +433,11 @@ feature %q{ scenario "updating when a filter has been applied" do s1 = create(:supplier_enterprise) s2 = create(:supplier_enterprise) - p1 = FactoryGirl.create(:simple_product, :name => "product1", supplier: s1) - p2 = FactoryGirl.create(:simple_product, :name => "product2", supplier: s2) + p1 = FactoryBot.create(:simple_product, :name => "product1", supplier: s1) + p2 = FactoryBot.create(:simple_product, :name => "product2", supplier: s2) login_to_admin_section - visit '/admin/products/bulk_edit' + visit spree.admin_products_path select2_select s1.name, from: "producer_filter" @@ -455,16 +455,16 @@ feature %q{ describe "using action buttons" do describe "using delete buttons" do - let!(:p1) { FactoryGirl.create(:product) } - let!(:p2) { FactoryGirl.create(:product) } + let!(:p1) { FactoryBot.create(:product) } + let!(:p2) { FactoryBot.create(:product) } let!(:v1) { p1.variants.first } let!(:v2) { p2.variants.first } - let!(:v3) { FactoryGirl.create(:variant, product: p2 ) } + let!(:v3) { FactoryBot.create(:variant, product: p2 ) } before do quick_login_as_admin - visit '/admin/products/bulk_edit' + visit spree.admin_products_path end it "shows a delete button for products, which deletes the appropriate product when clicked" do @@ -476,7 +476,7 @@ feature %q{ expect(page).to have_selector "a.delete-product", :count => 1 - visit '/admin/products/bulk_edit' + visit spree.admin_products_path expect(page).to have_selector "a.delete-product", :count => 1 end @@ -493,7 +493,7 @@ feature %q{ expect(page).to have_selector "a.delete-variant", :count => 2 - visit '/admin/products/bulk_edit' + visit spree.admin_products_path expect(page).to have_selector "a.view-variants" all("a.view-variants").select { |e| e.visible? }.each { |e| e.trigger('click') } @@ -502,14 +502,14 @@ feature %q{ end describe "using edit buttons" do - let!(:p1) { FactoryGirl.create(:product) } - let!(:p2) { FactoryGirl.create(:product) } + let!(:p1) { FactoryBot.create(:product) } + let!(:p2) { FactoryBot.create(:product) } let!(:v1) { p1.variants.first } let!(:v2) { p2.variants.first } before do quick_login_as_admin - visit '/admin/products/bulk_edit' + visit spree.admin_products_path end it "shows an edit button for products, which takes the user to the standard edit page for that product" do @@ -538,12 +538,12 @@ feature %q{ describe "using clone buttons" do it "shows a clone button for products, which duplicates the product and adds it to the page when clicked" do - p1 = FactoryGirl.create(:product, :name => "P1") - p2 = FactoryGirl.create(:product, :name => "P2") - p3 = FactoryGirl.create(:product, :name => "P3") + p1 = FactoryBot.create(:product, :name => "P1") + p2 = FactoryBot.create(:product, :name => "P2") + p3 = FactoryBot.create(:product, :name => "P3") login_to_admin_section - visit '/admin/products/bulk_edit' + visit spree.admin_products_path expect(page).to have_selector "a.clone-product", :count => 3 @@ -554,7 +554,7 @@ feature %q{ expect(page).to have_field "product_name", with: "COPY OF #{p1.name}" expect(page).to have_select "producer_id", selected: "#{p1.supplier.name}" - visit '/admin/products/bulk_edit' + visit spree.admin_products_path expect(page).to have_selector "a.clone-product", :count => 4 expect(page).to have_field "product_name", with: "COPY OF #{p1.name}" @@ -566,10 +566,10 @@ feature %q{ describe "using the page" do describe "using column display dropdown" do it "shows a column display dropdown, which shows a list of columns when clicked" do - FactoryGirl.create(:simple_product) + FactoryBot.create(:simple_product) login_to_admin_section - visit '/admin/products/bulk_edit' + visit spree.admin_products_path find("div#columns-dropdown", :text => "COLUMNS").click find("div#columns-dropdown div.menu div.menu_item", text: "Available On").click @@ -597,11 +597,11 @@ feature %q{ it "displays basic filtering controls which filter the product list" do s1 = create(:supplier_enterprise) s2 = create(:supplier_enterprise) - p1 = FactoryGirl.create(:simple_product, :name => "product1", supplier: s1) - p2 = FactoryGirl.create(:simple_product, :name => "product2", supplier: s2) + p1 = FactoryBot.create(:simple_product, :name => "product1", supplier: s1) + p2 = FactoryBot.create(:simple_product, :name => "product2", supplier: s2) login_to_admin_section - visit '/admin/products/bulk_edit' + visit spree.admin_products_path # Page shows the filter controls expect(page).to have_select "producer_filter", visible: false @@ -655,7 +655,7 @@ feature %q{ end it "shows only products that I supply" do - visit '/admin/products/bulk_edit' + visit spree.admin_products_path expect(page).to have_field 'product_name', with: product_supplied.name expect(page).to have_field 'product_name', with: product_supplied_permitted.name @@ -663,7 +663,7 @@ feature %q{ end it "shows only suppliers that I manage or have permission to" do - visit '/admin/products/bulk_edit' + visit spree.admin_products_path expect(page).to have_select 'producer_id', with_options: [supplier_managed1.name, supplier_managed2.name, supplier_permitted.name], selected: supplier_managed1.name expect(page).to have_no_select 'producer_id', with_options: [supplier_unmanaged.name] @@ -672,7 +672,7 @@ feature %q{ it "shows inactive products that I supply" do product_supplied_inactive - visit '/admin/products/bulk_edit' + visit spree.admin_products_path expect(page).to have_field 'product_name', with: product_supplied_inactive.name end @@ -680,7 +680,7 @@ feature %q{ it "allows me to create a product" do taxon = create(:taxon, name: 'Fruit') - visit '/admin/products/bulk_edit' + visit spree.admin_products_path find("a", text: "NEW PRODUCT").click expect(page).to have_content 'NEW PRODUCT' @@ -696,7 +696,7 @@ feature %q{ end click_button 'Create' - expect(URI.parse(current_url).path).to eq '/admin/products/bulk_edit' + expect(URI.parse(current_url).path).to eq spree.admin_products_path expect(flash_message).to eq 'Product "Big Bag Of Apples" has been successfully created!' expect(page).to have_field "product_name", with: 'Big Bag Of Apples' end @@ -705,7 +705,7 @@ feature %q{ p = product_supplied_permitted v = p.variants.first - visit '/admin/products/bulk_edit' + visit spree.admin_products_path find("div#columns-dropdown", :text => "COLUMNS").click find("div#columns-dropdown div.menu div.menu_item", text: "Available On").click find("div#columns-dropdown", :text => "COLUMNS").click @@ -750,7 +750,7 @@ feature %q{ it "displays product images and image upload modal" do quick_login_as_admin - visit '/admin/products/bulk_edit' + visit spree.admin_products_path within "table#listing_products tr#p_#{product.id}" do # Displays product images @@ -758,7 +758,6 @@ feature %q{ # Shows default image when no image set expect(page).to have_css "img[src='/assets/noimage/mini.png']" - @old_thumb_src = page.find("a.image-modal img")['src'] # Click image page.find("a.image-modal").trigger('click') @@ -770,7 +769,6 @@ feature %q{ within "div.reveal-modal.product-image-upload" do # Shows preview of current image expect(page).to have_css "img.preview" - old_image_src = page.find("img.preview")['src'] # Upload a new image file attach_file 'image-upload', Rails.root.join("public/500.jpg"), visible: false @@ -778,14 +776,6 @@ feature %q{ # Shows spinner whilst loading expect(page).to have_css "img.spinner", visible: true expect(page).to_not have_css "img.spinner", visible: true - - # Shows new image when finished - expect(page).to have_css "img.preview" - @new_image_src = page.find("img.preview")['src'] - expect(old_image_src) != @new_image_src - - # Close modal - page.find("a.close-reveal-modal").click end expect(page).to_not have_selector "div.reveal-modal.product-image-upload" @@ -799,24 +789,6 @@ feature %q{ end expect(page).to have_selector "div.reveal-modal.product-image-upload" - - within "div.reveal-modal.product-image-upload" do - # Upload another image file - attach_file 'image-upload', Rails.root.join("public/422.jpg"), visible: false - - # Overwrites existing image - expect(page).to have_css "img.preview" - newer_image_src = page.find("img.preview")['src'] - expect(@new_image_src) != newer_image_src - - page.find("a.close-reveal-modal").click - end - - within "table#listing_products tr#p_#{product.id}" do - # Newer thumbnail is shown in image column - newer_thumb_src = page.find("a.image-modal img")['src'] - expect(@new_thumb_src) != newer_thumb_src - end end end end diff --git a/spec/features/admin/enterprise_roles_spec.rb b/spec/features/admin/enterprise_roles_spec.rb index 5029a8b054..751eef36d7 100644 --- a/spec/features/admin/enterprise_roles_spec.rb +++ b/spec/features/admin/enterprise_roles_spec.rb @@ -111,7 +111,6 @@ feature %q{ it "allows adding new managers" do within 'table.managers' do targetted_select2_search user3.email, from: '#s2id_ignored' - find('a.icon-plus.no-text').click # user3 has been added and has an unconfirmed email address expect(page).to have_css "tr#manager-#{user3.id}" diff --git a/spec/features/admin/enterprise_user_spec.rb b/spec/features/admin/enterprise_user_spec.rb index 635b819471..0a070a80ea 100644 --- a/spec/features/admin/enterprise_user_spec.rb +++ b/spec/features/admin/enterprise_user_spec.rb @@ -33,26 +33,6 @@ feature %q{ end end - describe "product management" do - describe "managing supplied products" do - before do - user.enterprise_roles.create!(enterprise: supplier1) - product1 = create(:product, name: 'Green eggs', supplier: supplier1) - product2 = create(:product, name: 'Ham', supplier: supplier2) - login_to_admin_as user - end - - it "can manage products that I supply" do - visit spree.admin_products_path - - within '#listing_products' do - page.should have_content 'Green eggs' - page.should_not have_content 'Ham' - end - end - end - end - # This case no longer exists as anyone with an enterprise can supply into the system. # Or can they?? There is no producer profile anyway. # TODO discuss what parts of this are still necessary in which cases. @@ -88,11 +68,6 @@ feature %q{ end it "shows me enterprise product info but not payment methods, shipping methods or enterprise fees" do - # Producer product info - page.should have_selector '.producers_tab span', text: 'Total Products' - page.should have_selector '.producers_tab span', text: 'Active Products' - page.should_not have_selector '.producers_tab span', text: 'Products in OCs' - # Payment methods, shipping methods, enterprise fees page.should_not have_selector '.hubs_tab span', text: 'Payment Methods' page.should_not have_selector '.hubs_tab span', text: 'Shipping Methods' diff --git a/spec/features/admin/enterprises_spec.rb b/spec/features/admin/enterprises_spec.rb index 448933bac7..1a35aebffe 100644 --- a/spec/features/admin/enterprises_spec.rb +++ b/spec/features/admin/enterprises_spec.rb @@ -53,7 +53,7 @@ feature %q{ fill_in 'enterprise_address_attributes_address1', :with => '35 Ballantyne St' fill_in 'enterprise_address_attributes_city', :with => 'Thornbury' fill_in 'enterprise_address_attributes_zipcode', :with => '3072' - select2_search 'Australia', :from => 'Country' + # default country (Australia in this test) should be selected by default select2_search 'Victoria', :from => 'State' click_button 'Create' @@ -167,6 +167,7 @@ feature %q{ page.first("input[name='enterprise\[preferred_shopfront_message\]']", visible: false).set('This is my shopfront message.') page.should have_checked_field "enterprise_preferred_shopfront_order_cycle_order_orders_close_at" choose "enterprise_preferred_shopfront_order_cycle_order_orders_open_at" + choose "enterprise_enable_subscriptions_true" click_button 'Update' @@ -195,6 +196,7 @@ feature %q{ page.should have_content 'This is my shopfront message.' page.should have_checked_field "enterprise_preferred_shopfront_order_cycle_order_orders_open_at" expect(page).to have_checked_field "enterprise_require_login_true" + expect(page).to have_checked_field "enterprise_enable_subscriptions_true" end describe "producer properties" do @@ -340,6 +342,8 @@ feature %q{ fill_in 'enterprise_address_attributes_address1', with: 'z' fill_in 'enterprise_address_attributes_city', with: 'z' fill_in 'enterprise_address_attributes_zipcode', with: 'z' + select2_select 'Australia', from: 'enterprise_address_attributes_country_id' + select2_select 'Victoria', from: 'enterprise_address_attributes_state_id' end scenario "without violating rules" do diff --git a/spec/features/admin/order_cycles_spec.rb b/spec/features/admin/order_cycles_spec.rb index eb6fe4bc73..30d49fd39f 100644 --- a/spec/features/admin/order_cycles_spec.rb +++ b/spec/features/admin/order_cycles_spec.rb @@ -24,6 +24,7 @@ feature %q{ oc7 = create(:simple_order_cycle, name: 'oc7', orders_open_at: 2.months.ago, orders_close_at: 5.weeks.ago) schedule1 = create(:schedule, name: 'Schedule1', order_cycles: [oc1, oc3]) + create(:proxy_order, subscription: create(:subscription, schedule: schedule1), order_cycle: oc1) # When I go to the admin order cycles page login_to_admin_section @@ -111,6 +112,10 @@ feature %q{ page.should have_selector "#listing_order_cycles tr.order-cycle-#{oc1.id}" page.should have_selector "#listing_order_cycles tr.order-cycle-#{oc2.id}" page.should have_selector "#listing_order_cycles tr.order-cycle-#{oc3.id}" + + # Attempting to edit dates of an open order cycle with active subscriptions + find("#oc#{oc1.id}_orders_open_at").click + expect(page).to have_selector "#confirm-dialog .message", text: I18n.t('admin.order_cycles.date_warning.msg', n: 1) end describe 'listing order cycles with other locales' do diff --git a/spec/features/admin/overview_spec.rb b/spec/features/admin/overview_spec.rb index c1119a252e..530363929c 100644 --- a/spec/features/admin/overview_spec.rb +++ b/spec/features/admin/overview_spec.rb @@ -56,19 +56,24 @@ feature %q{ context "with multiple enterprises" do let(:d1) { create(:distributor_enterprise) } let(:d2) { create(:distributor_enterprise) } + let(:non_distributor_enterprise) { create(:enterprise, sells: 'none') } - before :each do + before do @enterprise_user.enterprise_roles.build(enterprise: d1).save @enterprise_user.enterprise_roles.build(enterprise: d2).save + @enterprise_user + .enterprise_roles.build(enterprise: non_distributor_enterprise).save end it "displays information about the enterprise" do visit '/admin' - page.should have_selector ".dashboard_item#enterprises h3", text: "My Enterprises" - page.should have_selector ".dashboard_item#products" - page.should have_selector ".dashboard_item#order_cycles" - page.should have_selector ".dashboard_item#enterprises .list-item", text: d1.name - page.should have_selector ".dashboard_item#enterprises .button.bottom", text: "MANAGE MY ENTERPRISES" + + expect(page).to have_selector ".dashboard_item#enterprises h3", text: "My Enterprises" + expect(page).to have_selector ".dashboard_item#products" + expect(page).to have_selector ".dashboard_item#order_cycles" + expect(page).to have_selector ".dashboard_item#enterprises .list-item", text: d1.name + expect(page).to have_selector ".dashboard_item#enterprises .list-item", text: non_distributor_enterprise.name + expect(page).to have_selector ".dashboard_item#enterprises .button.bottom", text: "MANAGE MY ENTERPRISES" end context "but no products or order cycles" do diff --git a/spec/features/admin/product_import_spec.rb b/spec/features/admin/product_import_spec.rb index 33334de098..35ebee5b1a 100644 --- a/spec/features/admin/product_import_spec.rb +++ b/spec/features/admin/product_import_spec.rb @@ -7,30 +7,35 @@ feature "Product Import", js: true do let!(:admin) { create(:admin_user) } let!(:user) { create_enterprise_user } + let!(:user2) { create_enterprise_user } let!(:enterprise) { create(:supplier_enterprise, owner: user, name: "User Enterprise") } - let!(:enterprise2) { create(:supplier_enterprise, owner: admin, name: "Another Enterprise") } + let!(:enterprise2) { create(:distributor_enterprise, owner: user2, name: "Another Enterprise") } + let!(:relationship) { create(:enterprise_relationship, parent: enterprise, child: enterprise2, permissions_list: [:create_variant_overrides]) } + let!(:category) { create(:taxon, name: 'Vegetables') } let!(:category2) { create(:taxon, name: 'Cake') } let!(:tax_category) { create(:tax_category) } let!(:tax_category2) { create(:tax_category) } let!(:shipping_category) { create(:shipping_category) } + let!(:product) { create(:simple_product, supplier: enterprise2, name: 'Hypothetical Cake') } 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') } - let!(:product3) { create(:simple_product, supplier: enterprise, on_hand: '100', name: 'Sprouts') } - let!(:product4) { create(:simple_product, supplier: enterprise, on_hand: '100', name: 'Cabbage') } - let!(:product5) { create(:simple_product, supplier: enterprise2, on_hand: '100', name: 'Lettuce') } - + let!(:product3) { create(:simple_product, supplier: enterprise, on_hand: '100', name: 'Sprouts', unit_value: '500') } + let!(:product4) { create(:simple_product, supplier: enterprise, on_hand: '100', name: 'Cabbage', unit_value: '500') } + let!(:product5) { create(:simple_product, supplier: enterprise2, on_hand: '100', name: 'Lettuce', unit_value: '500') } + let!(:variant_override) { create(:variant_override, variant_id: product4.variants.first.id, hub: enterprise2, count_on_hand: 42) } + let!(:variant_override2) { create(:variant_override, variant_id: product5.variants.first.id, hub: enterprise, count_on_hand: 96) } describe "when importing products from uploaded file" do before { quick_login_as_admin } after { File.delete('/tmp/test.csv') } - it "validates entries and saves them if they are all valid" do + it "validates entries and saves them if they are all valid and allows viewing new items in Bulk Products" do csv_data = CSV.generate do |csv| - csv << ["name", "supplier", "category", "on_hand", "price", "unit_value", "variant_unit", "variant_unit_scale"] - csv << ["Carrots", "User Enterprise", "Vegetables", "5", "3.20", "500", "weight", "1"] - csv << ["Potatoes", "User Enterprise", "Vegetables", "6", "6.50", "1000", "weight", "1000"] + csv << ["name", "supplier", "category", "on_hand", "price", "units", "unit_type"] + csv << ["Carrots", "User Enterprise", "Vegetables", "5", "3.20", "500", "g"] + csv << ["Potatoes", "User Enterprise", "Vegetables", "6", "6.50", "1", "kg"] end File.write('/tmp/test.csv', csv_data) @@ -38,60 +43,48 @@ feature "Product Import", js: true do expect(page).to have_content "Select a spreadsheet to upload" attach_file 'file', '/tmp/test.csv' - click_button 'Import' + click_button 'Upload' + + expect(page).to have_selector 'a.button.proceed', visible: true + click_link 'Proceed' + + import_data expect(page).to have_selector '.item-count', text: "2" - expect(page).to have_selector '.invalid-count', text: "0" + expect(page).to_not have_selector '.invalid-count' expect(page).to have_selector '.create-count', text: "2" - expect(page).to have_selector '.update-count', text: "0" + expect(page).to_not have_selector '.update-count' + + expect(page).to have_selector 'a.button.proceed', visible: true + click_link 'Proceed' + + save_data - click_button 'Save' expect(page).to have_selector '.created-count', text: '2' - expect(page).to have_selector '.updated-count', text: '0' + expect(page).to_not have_selector '.updated-count' + carrots = Spree::Product.find_by_name('Carrots') potatoes = Spree::Product.find_by_name('Potatoes') - potatoes.supplier.should == enterprise - potatoes.on_hand.should == 6 - potatoes.price.should == 6.50 - end + expect(potatoes.supplier).to eq enterprise + expect(potatoes.on_hand).to eq 6 + expect(potatoes.price).to eq 6.50 + expect(potatoes.variants.first.import_date).to be_within(1.minute).of Time.zone.now - it "displays info about invalid entries but still allows saving of valid entries" do - csv_data = CSV.generate do |csv| - csv << ["name", "supplier", "category", "on_hand", "price", "unit_value", "variant_unit", "variant_unit_scale"] - csv << ["Good Carrots", "User Enterprise", "Vegetables", "5", "3.20", "500", "weight", "1"] - csv << ["Bad Potatoes", "", "Vegetables", "6", "6.50", "1000", "", "1000"] - end - File.write('/tmp/test.csv', csv_data) + wait_until { page.find("a.button.view").present? } - visit main_app.admin_product_import_path + click_link 'View Products' - expect(page).to have_content "Select a spreadsheet to upload" - attach_file('file', '/tmp/test.csv') - click_button 'Import' - - expect(page).to have_selector '.item-count', text: "2" - expect(page).to have_selector '.invalid-count', text: "1" - expect(page).to have_selector '.create-count', text: "1" - expect(page).to have_selector '.update-count', text: "0" - - expect(page).to have_selector 'input[type=submit][value="Save"]' - click_button 'Save' - - expect(page).to have_selector '.created-count', text: '1' - expect(page).to have_selector '.updated-count', text: '0' - - Spree::Product.find_by_name('Bad Potatoes').should == nil - carrots = Spree::Product.find_by_name('Good Carrots') - carrots.supplier.should == enterprise - carrots.on_hand.should == 5 - carrots.price.should == 3.20 + expect(page).to have_content 'Bulk Edit Products' + wait_until { page.find("#p_#{potatoes.id}").present? } + expect(page).to have_field "product_name", with: carrots.name + expect(page).to have_field "product_name", with: potatoes.name end it "displays info about invalid entries but no save button if all items are invalid" do csv_data = CSV.generate do |csv| - csv << ["name", "supplier", "category", "on_hand", "price", "unit_value", "variant_unit", "variant_unit_scale"] - csv << ["Bad Carrots", "Unkown Enterprise", "Mouldy vegetables", "666", "3.20", "", "weight", ""] - csv << ["Bad Potatoes", "", "Vegetables", "6", "6", "6", "", "1000"] + csv << ["name", "supplier", "category", "on_hand", "price", "units", "unit_type"] + csv << ["Bad Carrots", "Unkown Enterprise", "Mouldy vegetables", "666", "3.20", "", "g"] + csv << ["Bad Potatoes", "", "Vegetables", "6", "6", "6", ""] end File.write('/tmp/test.csv', csv_data) @@ -99,79 +92,248 @@ feature "Product Import", js: true do expect(page).to have_content "Select a spreadsheet to upload" attach_file 'file', '/tmp/test.csv' - click_button 'Import' + click_button 'Upload' + + expect(page).to have_selector 'a.button.proceed', visible: true + click_link 'Proceed' + + import_data expect(page).to have_selector '.item-count', text: "2" expect(page).to have_selector '.invalid-count', text: "2" - expect(page).to have_selector '.create-count', text: "0" - expect(page).to have_selector '.update-count', text: "0" + expect(page).to_not have_selector '.create-count' + expect(page).to_not have_selector '.update-count' expect(page).to_not have_selector 'input[type=submit][value="Save"]' end - it "can add new variants to existing products and update price and stock level of existing products" do + it "handles validation and saving of named tax and shipping categories" do csv_data = CSV.generate do |csv| - csv << ["name", "supplier", "category", "on_hand", "price", "unit_value", "variant_unit", "variant_unit_scale", "display_name"] - csv << ["Hypothetical Cake", "Another Enterprise", "Cake", "5", "5.50", "500", "weight", "1", "Preexisting Banana"] - csv << ["Hypothetical Cake", "Another Enterprise", "Cake", "6", "3.50", "500", "weight", "1", "Emergent Coffee"] + csv << ["name", "supplier", "category", "on_hand", "price", "units", "unit_type", "tax_category", "shipping_category"] + csv << ["Carrots", "User Enterprise", "Vegetables", "5", "3.20", "500", "g", tax_category.name, shipping_category.name] + csv << ["Potatoes", "User Enterprise", "Vegetables", "6", "6.50", "1", "kg", "Unknown Tax Category", shipping_category.name] + csv << ["Peas", "User Enterprise", "Vegetables", "7", "2.50", "1", "kg", tax_category2.name, "Unknown Shipping Category"] end File.write('/tmp/test.csv', csv_data) visit main_app.admin_product_import_path + + expect(page).to have_content "Select a spreadsheet to upload" attach_file 'file', '/tmp/test.csv' - click_button 'Import' + click_button 'Upload' - expect(page).to have_selector '.item-count', text: "2" - expect(page).to have_selector '.invalid-count', text: "0" + expect(page).to have_selector 'a.button.proceed', visible: true + click_link 'Proceed' + + import_data + + expect(page).to have_selector '.item-count', text: "3" + expect(page).to have_selector '.invalid-count', text: "2" expect(page).to have_selector '.create-count', text: "1" - expect(page).to have_selector '.update-count', text: "1" + expect(page).to_not have_selector '.update-count' - click_button 'Save' + expect(page).to have_selector 'a.button.proceed', visible: true + click_link 'Proceed' + + save_data expect(page).to have_selector '.created-count', text: '1' - expect(page).to have_selector '.updated-count', text: '1' + expect(page).to_not have_selector '.updated-count' - added_coffee = Spree::Variant.find_by_display_name('Emergent Coffee') - added_coffee.product.name.should == 'Hypothetical Cake' - added_coffee.price.should == 3.50 - added_coffee.on_hand.should == 6 - - updated_banana = Spree::Variant.find_by_display_name('Preexisting Banana') - updated_banana.product.name.should == 'Hypothetical Cake' - updated_banana.price.should == 5.50 - updated_banana.on_hand.should == 5 + carrots = Spree::Product.find_by_name('Carrots') + expect(carrots.tax_category).to eq tax_category + expect(carrots.shipping_category).to eq shipping_category end - it "can add a new product and sub-variants of that product at the same time" do + it "records a timestamp on import that can be viewed and filtered under Bulk Edit Products" do csv_data = CSV.generate do |csv| - csv << ["name", "supplier", "category", "on_hand", "price", "unit_value", "variant_unit", "variant_unit_scale", "display_name"] - csv << ["Potatoes", "User Enterprise", "Vegetables", "5", "3.50", "500", "weight", "1000", "Small Bag"] - csv << ["Potatoes", "User Enterprise", "Vegetables", "6", "5.50", "2000", "weight", "1000", "Big Bag"] + csv << ["name", "supplier", "category", "on_hand", "price", "units", "unit_type"] + csv << ["Carrots", "User Enterprise", "Vegetables", "5", "3.20", "500", "g"] + csv << ["Potatoes", "User Enterprise", "Vegetables", "6", "6.50", "1", "kg"] + end + File.write('/tmp/test.csv', csv_data) + + visit main_app.admin_product_import_path + + expect(page).to have_content "Select a spreadsheet to upload" + attach_file 'file', '/tmp/test.csv' + click_button 'Upload' + + expect(page).to have_selector 'a.button.proceed', visible: true + click_link 'Proceed' + + import_data + + expect(page).to have_selector 'a.button.proceed', visible: true + click_link 'Proceed' + + save_data + + carrots = Spree::Product.find_by_name('Carrots') + expect(carrots.variants.first.import_date).to be_within(1.minute).of Time.zone.now + potatoes = Spree::Product.find_by_name('Potatoes') + expect(potatoes.variants.first.import_date).to be_within(1.minute).of Time.zone.now + + click_link 'View Products' + + wait_until { page.find("#p_#{carrots.id}").present? } + + expect(page).to have_field "product_name", with: carrots.name + expect(page).to have_field "product_name", with: potatoes.name + find("div#columns-dropdown", text: "COLUMNS").click + find("div#columns-dropdown div.menu div.menu_item", text: "Import").click + find("div#columns-dropdown", text: "COLUMNS").click + + within "tr#p_#{carrots.id} td.import_date" do + expect(page).to have_content Time.zone.now.year + end + + expect(page).to have_selector 'div#s2id_import_date_filter' + import_time = carrots.import_date.to_date.to_formatted_s(:long).gsub(' ', ' ') + select import_time, from: "import_date_filter", visible: false + + expect(page).to have_field "product_name", with: carrots.name + expect(page).to have_field "product_name", with: potatoes.name + expect(page).to_not have_field "product_name", with: product.name + expect(page).to_not have_field "product_name", with: product2.name + end + + it "can import items into inventory" do + csv_data = CSV.generate do |csv| + csv << ["name", "supplier", "producer", "category", "on_hand", "price", "units"] + csv << ["Beans", "Another Enterprise", "User Enterprise", "Vegetables", "5", "3.20", "500"] + csv << ["Sprouts", "Another Enterprise", "User Enterprise", "Vegetables", "6", "6.50", "500"] + csv << ["Cabbage", "Another Enterprise", "User Enterprise", "Vegetables", "2001", "1.50", "500"] end File.write('/tmp/test.csv', csv_data) visit main_app.admin_product_import_path attach_file 'file', '/tmp/test.csv' - click_button 'Import' + click_button 'Upload' - expect(page).to have_selector '.item-count', text: "2" - expect(page).to have_selector '.invalid-count', text: "0" - expect(page).to have_selector '.create-count', text: "2" - expect(page).to have_selector '.update-count', text: "0" + within 'div.import-settings' do + find('div.header-description').click # Import settings tab + select 'Inventories', from: "settings_#{enterprise2.id.to_s}_import_into", visible: false + end - click_button 'Save' - expect(page).to have_selector '.created-count', text: '2' - expect(page).to have_selector '.updated-count', text: '0' + expect(page).to have_selector 'a.button.proceed', visible: true + click_link 'Proceed' - small_bag = Spree::Variant.find_by_display_name('Small Bag') - small_bag.product.name.should == 'Potatoes' - small_bag.price.should == 3.50 - small_bag.on_hand.should == 5 + import_data - big_bag = Spree::Variant.find_by_display_name('Big Bag') - big_bag.product.name.should == 'Potatoes' - big_bag.price.should == 5.50 - big_bag.on_hand.should == 6 + expect(page).to have_selector '.item-count', text: "3" + expect(page).to_not have_selector '.invalid-count' + expect(page).to_not have_selector '.create-count' + expect(page).to_not have_selector '.update-count' + expect(page).to have_selector '.inv-create-count', text: "2" + expect(page).to have_selector '.inv-update-count', text: "1" + + expect(page).to have_selector 'a.button.proceed', visible: true + click_link 'Proceed' + + save_data + + expect(page).to_not have_selector '.created-count' + expect(page).to_not have_selector '.updated-count' + 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 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 + + click_link 'View Inventory' + expect(page).to have_content 'Inventory' + + select enterprise2.name, from: "hub_id", visible: false + + within '#variant-overrides' do + expect(page).to have_content 'Beans' + expect(page).to have_content 'Sprouts' + expect(page).to have_content 'Cabbage' + end + end + + it "can override import fields via the import settings tab" do + csv_data = CSV.generate do |csv| + csv << ["name", "supplier", "category", "on_hand", "price", "units", "unit_type", "tax_category", "shipping_category"] + csv << ["Carrots", "User Enterprise", "Vegetables", "5", "3.20", "500", "g", tax_category.name, shipping_category.name] + csv << ["Potatoes", "User Enterprise", "Vegetables", "6", "6.50", "1", "kg", "Unknown Tax Category", shipping_category.name] + csv << ["Peas", "User Enterprise", "Vegetables", "7", "2.50", "1", "kg", tax_category2.name, "Unknown Shipping Category"] + csv << ["Pumpkin", "User Enterprise", "Vegetables", "3", "3.50", "1", "kg", tax_category.name, ""] + csv << ["Spinach", "User Enterprise", "Vegetables", "7", "3.60", "1", "kg", "", shipping_category.name] + end + File.write('/tmp/test.csv', csv_data) + + visit main_app.admin_product_import_path + + expect(page).to have_content "Select a spreadsheet to upload" + attach_file 'file', '/tmp/test.csv' + click_button 'Upload' + + within 'div.import-settings' do + find('div.panel-header').click + + within 'tr.stock-level.productlist' do + find('input[type="checkbox"]').click + select 'Overwrite all', from: "settings_#{enterprise.id}_defaults_on_hand_mode", visible: false + fill_in "settings_#{enterprise.id}_defaults_on_hand_value", with: 9000 + end + + within 'tr.tax-category' do + find('input[type="checkbox"]').click + select 'Overwrite if empty', from: "settings_#{enterprise.id}_defaults_tax_category_id_mode", visible: false + select tax_category2.name, from: "settings_#{enterprise.id}_defaults_tax_category_id_value", visible: false + end + + within 'tr.shipping-category' do + find('input[type="checkbox"]').click + select 'Overwrite all', from: "settings_#{enterprise.id}_defaults_shipping_category_id_mode", visible: false + select shipping_category.name, from: "settings_#{enterprise.id}_defaults_shipping_category_id_value", visible: false + end + end + + expect(page).to have_selector 'a.button.proceed', visible: true + click_link 'Proceed' + + import_data + + expect(page).to have_selector '.item-count', text: "5" + expect(page).to have_selector '.invalid-count', text: "2" + expect(page).to have_selector '.create-count', text: "3" + expect(page).to_not have_selector '.update-count' + + expect(page).to have_selector 'a.button.proceed', visible: true + click_link 'Proceed' + + save_data + + expect(page).to have_selector '.created-count', text: '3' + expect(page).to_not have_selector '.updated-count' + + carrots = Spree::Product.find_by_name('Carrots') + expect(carrots.tax_category).to eq tax_category + expect(carrots.shipping_category).to eq shipping_category + expect(carrots.count_on_hand).to eq 9000 + + pumpkin = Spree::Product.find_by_name('Pumpkin') + expect(pumpkin.tax_category).to eq tax_category + expect(pumpkin.shipping_category).to eq shipping_category + expect(pumpkin.count_on_hand).to eq 9000 + + spinach = Spree::Product.find_by_name('Spinach') + expect(spinach.tax_category).to eq tax_category2 + expect(spinach.shipping_category).to eq shipping_category + expect(spinach.count_on_hand).to eq 9000 end end @@ -183,7 +345,7 @@ feature "Product Import", js: true do visit main_app.admin_product_import_path attach_file 'file', '/tmp/test.txt' - click_button 'Import' + click_button 'Upload' expect(page).to have_content "Importer could not process file: invalid filetype" expect(page).to_not have_selector 'input[type=submit][value="Save"]' @@ -191,10 +353,10 @@ feature "Product Import", js: true do File.delete('/tmp/test.txt') end - it "returns and error if nothing was uploaded" do + it "returns an error if nothing was uploaded" do visit main_app.admin_product_import_path expect(page).to have_content 'Select a spreadsheet to upload' - click_button 'Import' + click_button 'Upload' expect(flash_message).to eq I18n.t(:product_import_file_not_found_notice) end @@ -204,141 +366,77 @@ feature "Product Import", js: true do visit main_app.admin_product_import_path attach_file 'file', '/tmp/test.csv' - click_button 'Import' + click_button 'Upload' - expect(page).to have_selector '.create-count', text: "0" - expect(page).to have_selector '.update-count', text: "0" + expect(page).to_not have_selector '.create-count' + expect(page).to_not have_selector '.update-count' expect(page).to_not have_selector 'input[type=submit][value="Save"]' File.delete('/tmp/test.csv') end end describe "handling enterprise permissions" do - before { quick_login_as user } + after { File.delete('/tmp/test.csv') } - it "only allows import into enterprises the user is permitted to manage" do + it "only allows product import into enterprises the user is permitted to manage" do csv_data = CSV.generate do |csv| - csv << ["name", "supplier", "category", "on_hand", "price", "unit_value", "variant_unit", "variant_unit_scale"] - csv << ["My Carrots", "User Enterprise", "Vegetables", "5", "3.20", "500", "weight", "1"] - csv << ["Your Potatoes", "Another Enterprise", "Vegetables", "6", "6.50", "1000", "weight", "1000"] + csv << ["name", "supplier", "category", "on_hand", "price", "units", "unit_type"] + csv << ["My Carrots", "User Enterprise", "Vegetables", "5", "3.20", "500", "g"] + csv << ["Your Potatoes", "Another Enterprise", "Vegetables", "6", "6.50", "1", "kg"] end File.write('/tmp/test.csv', csv_data) + quick_login_as user visit main_app.admin_product_import_path attach_file 'file', '/tmp/test.csv' - click_button 'Import' + click_button 'Upload' + expect(page).to have_selector 'a.button.proceed', visible: true + click_link 'Proceed' + + import_data + + expect(page).to have_content I18n.t('admin.product_import.import.validation_overview') expect(page).to have_selector '.item-count', text: "2" expect(page).to have_selector '.invalid-count', text: "1" expect(page).to have_selector '.create-count', text: "1" - expect(page).to have_selector '.update-count', text: "0" expect(page.body).to have_content 'you do not have permission' - click_button 'Save' + expect(page).to have_selector 'a.button.proceed', visible: true + click_link 'Proceed' + + save_data expect(page).to have_selector '.created-count', text: '1' - expect(page).to have_selector '.updated-count', text: '0' - Spree::Product.find_by_name('My Carrots').should be_a Spree::Product - Spree::Product.find_by_name('Your Potatoes').should == nil + expect(Spree::Product.find_by_name('My Carrots')).to be_a Spree::Product + expect(Spree::Product.find_by_name('Your Potatoes')).to be_nil end end - describe "applying settings and defaults on import" do - before { quick_login_as_admin } + private - it "can set all products for an enterprise that are not present in the uploaded file to zero stock" do - csv_data = CSV.generate do |csv| - csv << ["name", "supplier", "category", "on_hand", "price", "unit_value", "variant_unit", "variant_unit_scale"] - csv << ["Carrots", "User Enterprise", "Vegetables", "5", "3.20", "500", "weight", "1"] - csv << ["Beans", "User Enterprise", "Vegetables", "6", "6.50", "500", "weight", "1"] - end - File.write('/tmp/test.csv', csv_data) + def import_data + expect(page).to have_selector 'button.start_import', visible: true + expect(page).to have_selector "button.review[disabled='disabled']" - visit main_app.admin_product_import_path + find('button.start_import').trigger 'click' + wait_until { page.find("button.review:not([disabled='disabled'])").present? } - attach_file 'file', '/tmp/test.csv' - click_button 'Import' + find('button.review').trigger 'click' + expect(page).to have_content I18n.t('admin.product_import.import.validation_overview') + end - expect(page).to have_selector '.item-count', text: "2" - expect(page).to have_selector '.invalid-count', text: "0" - expect(page).to have_selector '.create-count', text: "1" - expect(page).to have_selector '.update-count', text: "1" + def save_data + expect(page).to have_selector 'button.start_save', visible: true + expect(page).to have_selector "button.view_results[disabled='disabled']" - expect(page).to_not have_selector '.reset-count' + find('button.start_save').trigger 'click' + wait_until { page.find("button.view_results:not([disabled='disabled'])").present? } - within 'div.import-settings' do - find('div.header-description').click # Import settings tab - check "settings_#{enterprise.id}_reset_all_absent" - end - - expect(page).to have_selector '.reset-count', text: "2" - - click_button 'Save' - - expect(page).to have_selector '.created-count', text: '1' - expect(page).to have_selector '.updated-count', text: '1' - expect(page).to have_selector '.reset-count', text: '2' - - Spree::Product.find_by_name('Carrots').on_hand.should == 5 # Present in file, added - Spree::Product.find_by_name('Beans').on_hand.should == 6 # Present in file, updated - Spree::Product.find_by_name('Sprouts').on_hand.should == 0 # In enterprise, not in file - Spree::Product.find_by_name('Cabbage').on_hand.should == 0 # In enterprise, not in file - Spree::Product.find_by_name('Lettuce').on_hand.should == 100 # In different enterprise; unchanged - end - - it "overwrites fields with selected defaults" do - csv_data = CSV.generate do |csv| - csv << ["name", "supplier", "category", "on_hand", "price", "unit_value", "variant_unit", "variant_unit_scale", "tax_category_id", "available_on"] - csv << ["Carrots", "User Enterprise", "Vegetables", "5", "3.20", "500", "weight", "1", tax_category.id, ""] - csv << ["Potatoes", "User Enterprise", "Vegetables", "6", "6.50", "1000", "weight", "1000", "", ""] - end - File.write('/tmp/test.csv', csv_data) - - visit main_app.admin_product_import_path - - attach_file 'file', '/tmp/test.csv' - click_button 'Import' - - within 'div.import-settings' do - find('div.header-description').click # Import settings tab - expect(page).to have_selector "#settings_#{enterprise.id}_defaults_on_hand_mode", visible: false - - # Overwrite stock level of all items to 9000 - select 'Overwrite all', from: "settings_#{enterprise.id}_defaults_on_hand_mode", visible: false - fill_in "settings_#{enterprise.id}_defaults_on_hand_value", with: '9000' - - # Overwrite default tax category, but only where field is empty - select 'Overwrite if empty', from: "settings_#{enterprise.id}_defaults_tax_category_id_mode", visible: false - select tax_category2.name, from: "settings_#{enterprise.id}_defaults_tax_category_id_value", visible: false - - # Set default shipping category (field not present in file) - select 'Overwrite all', from: "settings_#{enterprise.id}_defaults_shipping_category_id_mode", visible: false - select shipping_category.name, from: "settings_#{enterprise.id}_defaults_shipping_category_id_value", visible: false - - # Set available_on date - select 'Overwrite all', from: "settings_#{enterprise.id}_defaults_available_on_mode", visible: false - find("input#settings_#{enterprise.id}_defaults_available_on_value").set '2020-01-01' - end - - click_button 'Save' - - expect(page).to have_selector '.created-count', text: '2' - expect(page).to have_selector '.updated-count', text: '0' - - carrots = Spree::Product.find_by_name('Carrots') - carrots.on_hand.should == 9000 - carrots.tax_category_id.should == tax_category.id - carrots.shipping_category_id.should == shipping_category.id - carrots.available_on.should be_within(1.day).of(Time.zone.local(2020, 1, 1)) - - potatoes = Spree::Product.find_by_name('Potatoes') - potatoes.on_hand.should == 9000 - potatoes.tax_category_id.should == tax_category2.id - potatoes.shipping_category_id.should == shipping_category.id - potatoes.available_on.should be_within(1.day).of(Time.zone.local(2020, 1, 1)) - end + find('button.view_results').trigger 'click' + expect(page).to have_content I18n.t('admin.product_import.save.final_results') end end diff --git a/spec/features/admin/products_spec.rb b/spec/features/admin/products_spec.rb index 18dd52ed73..bccc223953 100644 --- a/spec/features/admin/products_spec.rb +++ b/spec/features/admin/products_spec.rb @@ -39,7 +39,7 @@ feature %q{ click_button 'Create' - expect(current_path).to eq spree.bulk_edit_admin_products_path + expect(current_path).to eq spree.admin_products_path flash_message.should == 'Product "A new product !!!" has been successfully created!' product = Spree::Product.find_by_name('A new product !!!') product.supplier.should == @supplier @@ -80,7 +80,7 @@ feature %q{ click_button 'Create' - expect(current_path).to eq spree.bulk_edit_admin_products_path + expect(current_path).to eq spree.admin_products_path product = Spree::Product.find_by_name('Hot Cakes') product.variants.count.should == 1 variant = product.variants.first diff --git a/spec/features/admin/reports_spec.rb b/spec/features/admin/reports_spec.rb index 8a8acc1da1..3435d487e6 100644 --- a/spec/features/admin/reports_spec.rb +++ b/spec/features/admin/reports_spec.rb @@ -17,16 +17,16 @@ feature %q{ it "does not show super admin only reports" do login_to_admin_as user click_link "Reports" - page.should_not have_content "Sales Total" - page.should_not have_content "Users & Enterprises" + expect(page).not_to have_content "Sales Total" + expect(page).not_to have_content "Users & Enterprises" end end context "As an admin user" do it "shows the super admin only reports" do login_to_admin_section click_link "Reports" - page.should have_content "Sales Total" - page.should have_content "Users & Enterprises" + expect(page).to have_content "Sales Total" + expect(page).to have_content "Users & Enterprises" end end end @@ -39,23 +39,26 @@ feature %q{ scenario "customers report" do click_link "Mailing List" expect(page).to have_select('report_type', selected: 'Mailing List') + expect(page).to have_content "click on SEARCH" + click_button "Search" rows = find("table#listing_customers").all("thead tr") table = rows.map { |r| r.all("th").map { |c| c.text.strip } } - table.sort.should == [ + expect(table.sort).to eq([ ["Email", "First Name", "Last Name", "Suburb"] - ].sort + ].sort) end scenario "customers report" do click_link "Addresses" expect(page).to have_select('report_type', selected: 'Addresses') + click_button "Search" rows = find("table#listing_customers").all("thead tr") table = rows.map { |r| r.all("th").map { |c| c.text.strip } } - table.sort.should == [ + expect(table.sort).to eq([ ["First Name", "Last Name", "Billing Address", "Email", "Phone", "Hub", "Hub Address", "Shipping Method"] - ].sort + ].sort) end end @@ -67,20 +70,22 @@ feature %q{ scenario "payment method report" do click_link "Payment Methods Report" + click_button "Search" rows = find("table#listing_ocm_orders").all("thead tr") table = rows.map { |r| r.all("th").map { |c| c.text.strip } } - table.sort.should == [ + expect(table.sort).to eq([ ["First Name", "Last Name", "Hub", "Hub Code", "Email", "Phone", "Shipping Method", "Payment Method", "Amount", "Balance"] - ].sort + ].sort) end scenario "delivery report" do click_link "Delivery Report" + click_button "Search" rows = find("table#listing_ocm_orders").all("thead tr") table = rows.map { |r| r.all("th").map { |c| c.text.strip } } - table.sort.should == [ + expect(table.sort).to eq([ ["First Name", "Last Name", "Hub", "Hub Code", "Delivery Address", "Delivery Postcode", "Phone", "Shipping Method", "Payment Method", "Amount", "Balance", "Temp Controlled Items?", "Special Instructions"] - ].sort + ].sort) end end @@ -119,12 +124,12 @@ feature %q{ #select 'Pack By Customer', from: 'report_type' click_button 'Search' - rows = find("table#listing_orders.index").all("thead tr") + rows = find("table#listing_orders").all("thead tr") table = rows.map { |r| r.all("th").map { |c| c.text.strip } } - table.sort.should == [ + expect(table.sort).to eq([ ["Hub", "Code", "First Name", "Last Name", "Supplier", "Product", "Variant", "Quantity", "TempControlled?"] - ].sort - page.should have_selector 'table#listing_orders tbody tr', count: 5 # Totals row per order + ].sort) + expect(page).to have_selector 'table#listing_orders tbody tr', count: 5 # Totals row per order end scenario "Pack By Supplier" do @@ -136,10 +141,10 @@ feature %q{ rows = find("table#listing_orders").all("thead tr") table = rows.map { |r| r.all("th").map { |c| c.text.strip } } - table.sort.should == [ + expect(table.sort).to eq([ ["Hub", "Supplier", "Code", "First Name", "Last Name", "Product", "Variant", "Quantity", "TempControlled?"] - ].sort - all('table#listing_orders tbody tr').count.should == 4 # Totals row per supplier + ].sort) + expect(all('table#listing_orders tbody tr').count).to eq(4) # Totals row per supplier end end @@ -148,24 +153,27 @@ feature %q{ login_to_admin_section click_link 'Reports' click_link 'Orders And Distributors' + click_button 'Search' - page.should have_content 'Order date' + expect(page).to have_content 'Order date' end scenario "bulk co-op report" do login_to_admin_section click_link 'Reports' click_link 'Bulk Co-Op' + click_button 'Search' - page.should have_content 'Supplier' + expect(page).to have_content 'Supplier' end scenario "payments reports" do login_to_admin_section click_link 'Reports' click_link 'Payment Reports' + click_button 'Search' - page.should have_content 'Payment State' + expect(page).to have_content 'Payment State' end describe "sales tax report" do @@ -204,28 +212,28 @@ feature %q{ it "reports" do # Then it should give me access only to managed enterprises - page.should have_select 'q_distributor_id_eq', with_options: [user1.enterprises.first.name] - page.should_not have_select 'q_distributor_id_eq', with_options: [user2.enterprises.first.name] + expect(page).to have_select 'q_distributor_id_eq', with_options: [user1.enterprises.first.name] + expect(page).not_to have_select 'q_distributor_id_eq', with_options: [user2.enterprises.first.name] # When I filter to just one distributor select user1.enterprises.first.name, from: 'q_distributor_id_eq' click_button 'Search' # Then I should see the relevant order - page.should have_content "#{order1.number}" + expect(page).to have_content "#{order1.number}" # And the totals and sales tax should be correct - page.should have_content "1512.99" # items total - page.should have_content "1500.45" # taxable items total - page.should have_content "250.08" # sales tax - page.should have_content "20.0" # enterprise fee tax + expect(page).to have_content "1512.99" # items total + expect(page).to have_content "1500.45" # taxable items total + expect(page).to have_content "250.08" # sales tax + expect(page).to have_content "20.0" # enterprise fee tax # And the shipping cost and tax should be correct - page.should have_content "100.55" # shipping cost - page.should have_content "16.76" # shipping tax + expect(page).to have_content "100.55" # shipping cost + expect(page).to have_content "16.76" # shipping tax # And the total tax should be correct - page.should have_content "286.84" # total tax + expect(page).to have_content "286.84" # total tax end end @@ -235,7 +243,7 @@ feature %q{ click_link 'Reports' click_link 'Orders & Fulfillment Reports' - page.should have_content 'Supplier' + expect(page).to have_content 'Supplier' end context "with two orders on the same day at different times" do @@ -267,7 +275,7 @@ feature %q{ click_button 'Search' # Then I should see the rows for the first order but not the second - all('table#listing_orders tbody tr').count.should == 2 # Two rows per order + expect(all('table#listing_orders tbody tr').count).to eq(2) # Two rows per order end end @@ -279,7 +287,7 @@ feature %q{ login_to_admin_section visit spree.orders_and_fulfillment_admin_reports_path - page.should have_content "My Order Cycle" + expect(page).to have_content "My Order Cycle" end end @@ -311,23 +319,25 @@ feature %q{ login_to_admin_section click_link 'Reports' - page.should have_content "All products" - page.should have_content "Inventory (on hand)" + expect(page).to have_content "All products" + expect(page).to have_content "Inventory (on hand)" click_link 'Products & Inventory' - page.should have_content "Supplier" - page.should have_table_row ["Supplier", "Producer Suburb", "Product", "Product Properties", "Taxons", "Variant Value", "Price", "Group Buy Unit Quantity", "Amount", "SKU"].map(&:upcase) - page.should have_table_row [product1.supplier.name, product1.supplier.address.city, "Product Name", product1.properties.map(&:presentation).join(", "), product1.primary_taxon.name, "Test", "100.0", product1.group_buy_unit_size.to_s, "", "sku1"] - page.should have_table_row [product1.supplier.name, product1.supplier.address.city, "Product Name", product1.properties.map(&:presentation).join(", "), product1.primary_taxon.name, "Something", "80.0", product1.group_buy_unit_size.to_s, "", "sku2"] - page.should have_table_row [product2.supplier.name, product1.supplier.address.city, "Product 2", product1.properties.map(&:presentation).join(", "), product2.primary_taxon.name, "100g", "99.0", product1.group_buy_unit_size.to_s, "", "product_sku"] + click_button "Search" + expect(page).to have_content "Supplier" + expect(page).to have_table_row ["Supplier", "Producer Suburb", "Product", "Product Properties", "Taxons", "Variant Value", "Price", "Group Buy Unit Quantity", "Amount", "SKU"].map(&:upcase) + expect(page).to have_table_row [product1.supplier.name, product1.supplier.address.city, "Product Name", product1.properties.map(&:presentation).join(", "), product1.primary_taxon.name, "Test", "100.0", product1.group_buy_unit_size.to_s, "", "sku1"] + expect(page).to have_table_row [product1.supplier.name, product1.supplier.address.city, "Product Name", product1.properties.map(&:presentation).join(", "), product1.primary_taxon.name, "Something", "80.0", product1.group_buy_unit_size.to_s, "", "sku2"] + expect(page).to have_table_row [product2.supplier.name, product1.supplier.address.city, "Product 2", product1.properties.map(&:presentation).join(", "), product2.primary_taxon.name, "100g", "99.0", product1.group_buy_unit_size.to_s, "", "product_sku"] end it "shows the LettuceShare report" do login_to_admin_section click_link 'Reports' click_link 'LettuceShare' + click_button "Search" - page.should have_table_row ['PRODUCT', 'Description', 'Qty', 'Pack Size', 'Unit', 'Unit Price', 'Total', 'GST incl.', 'Grower and growing method', 'Taxon'].map(&:upcase) - page.should have_table_row ['Product 2', '100g', '', '100', 'g', '99.0', '', '0', 'Supplier Name (Organic - NASAA 12345)', 'Taxon Name'] + expect(page).to have_table_row ['PRODUCT', 'Description', 'Qty', 'Pack Size', 'Unit', 'Unit Price', 'Total', 'GST incl.', 'Grower and growing method', 'Taxon'].map(&:upcase) + expect(page).to have_table_row ['Product 2', '100g', '', '100', 'g', '99.0', '', '0', 'Supplier Name (Organic - NASAA 12345)', 'Taxon Name'] end end @@ -346,10 +356,12 @@ feature %q{ end it "shows users and enterprises report" do + click_button "Search" + rows = find("table#users_and_enterprises").all("tr") table = rows.map { |r| r.all("th,td").map { |c| c.text.strip }[0..2] } - table.sort.should == [ + expect(table.sort).to eq([ [ "User", "Relationship", "Enterprise" ], [ enterprise1.owner.email, "owns", enterprise1.name ], [ enterprise1.owner.email, "manages", enterprise1.name ], @@ -358,7 +370,7 @@ feature %q{ [ enterprise3.owner.email, "owns", enterprise3.name ], [ enterprise3.owner.email, "manages", enterprise3.name ], [ enterprise1.owner.email, "manages", enterprise3.name ] - ].sort + ].sort) end it "filters the list" do @@ -370,10 +382,10 @@ feature %q{ rows = find("table#users_and_enterprises").all("tr") table = rows.map { |r| r.all("th,td").map { |c| c.text.strip }[0..2] } - table.sort.should == [ + expect(table.sort).to eq([ [ "User", "Relationship", "Enterprise" ], [ enterprise1.owner.email, "manages", enterprise3.name ] - ].sort + ].sort) end end @@ -416,12 +428,13 @@ feature %q{ around do |example| Timecop.travel(Time.zone.local(2015, 4, 26, 14, 0, 0)) do - example.yield + example.run end end it "shows Xero invoices report" do - xero_invoice_table.should match_table [ + click_button "Search" + expect(xero_invoice_table).to match_table [ xero_invoice_header, xero_invoice_summary_row('Total untaxable produce (no tax)', 12.54, 'GST Free Income'), xero_invoice_summary_row('Total taxable produce (tax inclusive)', 1500.45, 'GST on Income'), @@ -442,7 +455,7 @@ feature %q{ opts = {invoice_number: '5', invoice_date: '2015-02-12', due_date: '2015-03-12', account_code: 'abc123'} - xero_invoice_table.should match_table [ + expect(xero_invoice_table).to match_table [ xero_invoice_header, xero_invoice_summary_row('Total untaxable produce (no tax)', 12.54, 'GST Free Income', opts), xero_invoice_summary_row('Total taxable produce (tax inclusive)', 1500.45, 'GST on Income', opts), @@ -460,7 +473,7 @@ feature %q{ opts = {} - xero_invoice_table.should match_table [ + expect(xero_invoice_table).to match_table [ xero_invoice_header, xero_invoice_li_row(line_item1), xero_invoice_li_row(line_item2), @@ -495,7 +508,7 @@ feature %q{ opts = {} - xero_invoice_table.should match_table [ + expect(xero_invoice_table).to match_table [ xero_invoice_header, xero_invoice_account_invoice_row(adjustment) ] diff --git a/spec/features/admin/schedules_spec.rb b/spec/features/admin/schedules_spec.rb index d400d5f087..c569475a9d 100644 --- a/spec/features/admin/schedules_spec.rb +++ b/spec/features/admin/schedules_spec.rb @@ -71,6 +71,7 @@ feature 'Schedules', js: true do find('a', text: "Weekly").click end + expect(page).to have_selector "#schedule-dialog" within "#schedule-dialog" do find("#selected-order-cycles .order-cycle", text: oc3.name).click find("#add-remove-buttons a.remove").click diff --git a/spec/features/admin/subscriptions_spec.rb b/spec/features/admin/subscriptions_spec.rb index 9bc5377bb6..ad115cbb5a 100644 --- a/spec/features/admin/subscriptions_spec.rb +++ b/spec/features/admin/subscriptions_spec.rb @@ -159,7 +159,10 @@ feature 'Subscriptions' do click_button('Next') expect(page).to have_content 'can\'t be blank', count: 2 expect(page).to have_content 'Oops! Please fill in all of the required fields...' - fill_in 'begins_at', with: Time.zone.today.strftime('%F') + find_field('begins_at').click + within(".ui-datepicker-calendar") do + find('.ui-datepicker-today').click + end select2_select card2_option, from: 'credit_card_id' click_button('Next') diff --git a/spec/features/consumer/account/cards_spec.rb b/spec/features/consumer/account/cards_spec.rb index 3a9e32ddd6..2f78511135 100644 --- a/spec/features/consumer/account/cards_spec.rb +++ b/spec/features/consumer/account/cards_spec.rb @@ -4,7 +4,8 @@ feature "Credit Cards", js: true do include AuthenticationWorkflow describe "as a logged in user" do let(:user) { create(:user) } - let!(:card) { create(:credit_card, user_id: user.id, gateway_customer_profile_id: 'cus_AZNMJ') } + let!(:default_card) { create(:credit_card, user_id: user.id, gateway_customer_profile_id: 'cus_AZNMJ', is_default: true) } + let!(:non_default_card) { create(:credit_card, user_id: user.id, gateway_customer_profile_id: 'cus_FDTG') } before do quick_login_as user @@ -20,28 +21,52 @@ feature "Credit Cards", js: true do to_return(:status => 200, :body => JSON.generate(deleted: true, id: "cus_AZNMJ")) end - it "lists saved cards, shows interface for adding new cards" do + it "passes the smoke test" do visit "/account" click_link I18n.t('spree.users.show.tabs.cards') expect(page).to have_content I18n.t(:saved_cards) - within(".card#card#{card.id}") do - expect(page).to have_content card.cc_type.capitalize - expect(page).to have_content card.last_digits + # Lists saved cards + within(".card#card#{default_card.id}") do + expect(page).to have_content default_card.cc_type.capitalize + expect(page).to have_content default_card.last_digits + expect(find_field('default_card')).to be_checked end + within(".card#card#{non_default_card.id}") do + expect(page).to have_content non_default_card.cc_type.capitalize + expect(page).to have_content non_default_card.last_digits + expect(find_field('default_card')).to_not be_checked + end + + # Allows switching of default card + within(".card#card#{non_default_card.id}") do + find_field('default_card').click + expect(find_field('default_card')).to be_checked + end + + expect(page).to have_content I18n.t('js.default_card_updated') + + within(".card#card#{default_card.id}") do + expect(find_field('default_card')).to_not be_checked + end + expect(default_card.reload.is_default).to be false + expect(non_default_card.reload.is_default).to be true + # Shows the interface for adding a card click_button I18n.t(:add_a_card) expect(page).to have_field 'first_name' expect(page).to have_selector '#card-element.StripeElement' # Allows deletion of cards - click_link I18n.t(:delete) + within(".card#card#{default_card.id}") do + click_link I18n.t(:delete) + end - expect(page).to have_content I18n.t(:card_has_been_removed, number: "x-#{card.last_digits}") - expect(page).to have_content I18n.t(:you_have_no_saved_cards) + expect(page).to have_content I18n.t(:card_has_been_removed, number: "x-#{default_card.last_digits}") + expect(page).to_not have_selector ".card#card#{default_card.id}" end end end diff --git a/spec/features/consumer/confirm_invitation_spec.rb b/spec/features/consumer/confirm_invitation_spec.rb new file mode 100644 index 0000000000..9fea517638 --- /dev/null +++ b/spec/features/consumer/confirm_invitation_spec.rb @@ -0,0 +1,30 @@ +require "spec_helper" + +feature "Confirm invitation as manager" do + include UIComponentHelper # for be_logged_in_as + + describe "confirm email and set password" do + let(:email) { "test@example.org" } + let(:user) { Spree::User.create(email: email, unconfirmed_email: email, password: "secret") } + + before do + user.reset_password_token = Devise.friendly_token + user.reset_password_sent_at = Time.now.utc + user.save! + end + + it "allows you to set a password" do + visit spree.spree_user_confirmation_url(confirmation_token: user.confirmation_token) + + expect(user.reload.confirmed?).to be true + expect(page).to have_text "Change my password" + + fill_in "Password", with: "my secret" + fill_in "Password Confirmation", with: "my secret" + click_button + + expect(page).to have_no_text "Reset password token has expired" + expect(page).to be_logged_in_as user + end + end +end diff --git a/spec/features/consumer/registration_spec.rb b/spec/features/consumer/registration_spec.rb index 70d132b11b..b0818cd79b 100644 --- a/spec/features/consumer/registration_spec.rb +++ b/spec/features/consumer/registration_spec.rb @@ -49,7 +49,7 @@ feature "Registration", js: true do fill_in 'enterprise_city', with: 'Northcote' fill_in 'enterprise_zipcode', with: '3070' expect(page).to have_select('enterprise_country', options: %w(Albania Australia), selected: 'Australia') - select 'VIC', from: 'enterprise_state' + select 'Vic', from: 'enterprise_state' perform_and_ensure(:click_button, "Continue", lambda { page.has_content? 'Who is responsible for managing My Awesome Enterprise?' }) diff --git a/spec/features/consumer/shopping/checkout_auth_spec.rb b/spec/features/consumer/shopping/checkout_auth_spec.rb index e23e4c15a8..d940f3290a 100644 --- a/spec/features/consumer/shopping/checkout_auth_spec.rb +++ b/spec/features/consumer/shopping/checkout_auth_spec.rb @@ -7,54 +7,76 @@ feature "As a consumer I want to check out my cart", js: true do include CheckoutWorkflow include UIComponentHelper - let(:distributor) { create(:distributor_enterprise, with_payment_and_shipping: true) } - let(:supplier) { create(:supplier_enterprise) } - let!(:order_cycle) { create(:simple_order_cycle, distributors: [distributor], coordinator: create(:distributor_enterprise), variants: [product.variants.first]) } - let(:product) { create(:simple_product, supplier: supplier) } - let(:order) { create(:order, order_cycle: order_cycle, distributor: distributor) } - let(:address) { create(:address, firstname: "Foo", lastname: "Bar") } - let(:user) { create(:user, bill_address: address, ship_address: address) } - after { Warden.test_reset! } + describe "using the checkout" do + let(:distributor) { create(:distributor_enterprise, with_payment_and_shipping: true) } + let(:supplier) { create(:supplier_enterprise) } + let!(:order_cycle) { create(:simple_order_cycle, distributors: [distributor], coordinator: create(:distributor_enterprise), variants: [product.variants.first]) } + let(:product) { create(:simple_product, supplier: supplier) } + let(:order) { create(:order, order_cycle: order_cycle, distributor: distributor) } + let(:address) { create(:address, firstname: "Foo", lastname: "Bar") } + let(:user) { create(:user, bill_address: address, ship_address: address) } - before do - set_order order - add_product_to_cart order, product - end + after { Warden.test_reset! } - it "does not not render the login form when logged in" do - quick_login_as user - visit checkout_path - within "section[role='main']" do - page.should_not have_content "Login" - page.should have_checkout_details + before do + set_order order + add_product_to_cart order, product end - end - it "renders the login buttons when logged out" do - visit checkout_path - within "section[role='main']" do - page.should have_content "Login" - click_button "Login" + it "does not render the login form when logged in" do + quick_login_as user + visit checkout_path + within "section[role='main']" do + expect(page).to_not have_content "Login" + expect(page).to have_checkout_details + end end - page.should have_login_modal - end - it "populates user details once logged in" do - visit checkout_path - within("section[role='main']") { click_button "Login" } - page.should have_login_modal - fill_in "Email", with: user.email - fill_in "Password", with: user.password - within(".login-modal") { click_button 'Login' } - toggle_details + it "renders the login buttons when logged out" do + visit checkout_path + within "section[role='main']" do + expect(page).to have_content "Login" + click_button "Login" + end + expect(page).to have_login_modal + end - page.should have_field 'First Name', with: 'Foo' - page.should have_field 'Last Name', with: 'Bar' - end + it "populates user details once logged in" do + visit checkout_path + within("section[role='main']") { click_button "Login" } + expect(page).to have_login_modal + fill_in "Email", with: user.email + fill_in "Password", with: user.password + within(".login-modal") { click_button 'Login' } + toggle_details - it "allows user to checkout as guest" do - visit checkout_path - checkout_as_guest - page.should have_checkout_details + expect(page).to have_field 'First Name', with: 'Foo' + expect(page).to have_field 'Last Name', with: 'Bar' + end + + context "using the guest checkout" do + it "allows user to checkout as guest" do + visit checkout_path + checkout_as_guest + expect(page).to have_checkout_details + end + + it "asks the user to log in if they are using a registered email" do + visit checkout_path + checkout_as_guest + + fill_in 'First Name', with: 'Not' + fill_in 'Last Name', with: 'Guest' + fill_in 'Email', with: user.email + fill_in 'Phone', with: '098712736' + + within '#details' do + click_button 'Next' + end + + expect(page).to have_selector 'div.login-modal', visible: true + expect(page).to have_content I18n.t('devise.failure.already_registered') + end + end end end diff --git a/spec/features/consumer/shopping/checkout_spec.rb b/spec/features/consumer/shopping/checkout_spec.rb index ac2a50e411..b69a6aae58 100644 --- a/spec/features/consumer/shopping/checkout_spec.rb +++ b/spec/features/consumer/shopping/checkout_spec.rb @@ -199,10 +199,9 @@ feature "As a consumer I want to check out my cart", js: true, retry: 3 do # shows the saved credit card dropdown expect(page).to have_content I18n.t("spree.checkout.payment.stripe.used_saved_card") - # removes the input fields when a saved card is selected" - expect(page).to have_selector "#card-element.StripeElement" - select "Visa x-1111 Exp:01/2025", from: "selected_card" + # default card is selected, form element is not shown expect(page).to_not have_selector "#card-element.StripeElement" + expect(page).to have_select 'selected_card', selected: "Visa x-1111 Exp:01/2025" # allows checkout place_order diff --git a/spec/features/consumer/shopping/embedded_groups_spec.rb b/spec/features/consumer/shopping/embedded_groups_spec.rb new file mode 100644 index 0000000000..4122fb30ce --- /dev/null +++ b/spec/features/consumer/shopping/embedded_groups_spec.rb @@ -0,0 +1,78 @@ +require 'spec_helper' + +feature "Using embedded shopfront functionality", js: true do + + Capybara.server_port = 9999 + + describe 'embedded groups' do + let(:enterprise) { create(:distributor_enterprise) } + let!(:group) { create(:enterprise_group, enterprises: [enterprise], permalink: 'group1', on_front_page: true) } + + before do + Spree::Config[:enable_embedded_shopfronts] = true + Spree::Config[:embedded_shopfronts_whitelist] = 'test.com' + page.driver.browser.js_errors = false + allow_any_instance_of(ActionDispatch::Request).to receive(:referer).and_return('https://www.test.com') + Capybara.current_session.driver.visit('spec/support/views/group_iframe_test.html') + end + + it "displays in an iframe" do + expect(page).to have_selector 'iframe#group_test_iframe' + + within_frame 'group_test_iframe' do + within 'div#group-page' do + expect(page).to have_content 'About Us' + end + end + end + + it "displays powered by OFN text at bottom of page" do + expect(page).to have_selector 'iframe#group_test_iframe' + + within_frame 'group_test_iframe' do + within 'div#group-page' do + expect(page).to have_selector 'div.powered-by-embedded' + expect(page).to have_css "img[src*='favicon.ico']" + expect(page).to have_content 'Powered by' + expect(page).to have_content 'Open Food Network' + end + end + end + + it "doesn't display contact details when embedded" do + expect(page).to have_selector 'iframe#group_test_iframe' + + within_frame 'group_test_iframe' do + within 'div#group-page' do + + expect(page).to have_no_selector 'div.contact-container' + expect(page).to have_no_content '#{group.address.address1}' + end + end + end + + it "does not display the header when embedded" do + expect(page).to have_selector 'iframe#group_test_iframe' + + within_frame 'group_test_iframe' do + within 'div#group-page' do + expect(page).to have_no_selector 'header' + expect(page).to have_no_selector 'img.group-logo' + expect(page).to have_no_selector 'h2.group-name' + end + end + end + + it 'opens links to shops in a new window' do + expect(page).to have_selector 'iframe#group_test_iframe' + + within_frame 'group_test_iframe' do + within 'div#group-page' do + enterprise_links = page.all(:xpath, "//*[contains(@href, 'enterprise-5/shop')]", :visible => :false).count + enterprise_links_with_target_blank = page.all(:xpath, "//*[contains(@href, 'enterprise-5/shop') and @target = '_blank']", :visible => :false).count + expect(enterprise_links).to equal(enterprise_links_with_target_blank) + end + end + end + end +end diff --git a/spec/features/consumer/shopping/embedded_shopfronts_spec.rb b/spec/features/consumer/shopping/embedded_shopfronts_spec.rb index deb79a5ad2..87b6a7e5ca 100644 --- a/spec/features/consumer/shopping/embedded_shopfronts_spec.rb +++ b/spec/features/consumer/shopping/embedded_shopfronts_spec.rb @@ -47,7 +47,7 @@ feature "Using embedded shopfront functionality", js: true do end end - it "allows shopping and checkout" do + xit "allows shopping and checkout" do within_frame 'test_iframe' do fill_in "variants[#{variant.id}]", with: 1 wait_until_enabled 'input.add_to_cart' diff --git a/spec/javascripts/unit/darkswarm/controllers/checkout/payment_controller_spec.js.coffee b/spec/javascripts/unit/darkswarm/controllers/checkout/payment_controller_spec.js.coffee new file mode 100644 index 0000000000..46b152b00a --- /dev/null +++ b/spec/javascripts/unit/darkswarm/controllers/checkout/payment_controller_spec.js.coffee @@ -0,0 +1,17 @@ +describe "PaymentCtrl", -> + ctrl = null + scope = null + card1 = { id: 1, is_default: false } + card2 = { id: 3, is_default: true } + cards = [card1, card2] + + beforeEach -> + module("Darkswarm") + angular.module('Darkswarm').value('savedCreditCards', cards) + inject ($controller, $rootScope) -> + scope = $rootScope.$new() + scope.secrets = {} + ctrl = $controller 'PaymentCtrl', {$scope: scope} + + it "sets the default card id as the selected_card", -> + expect(scope.secrets.selected_card).toEqual card2.id diff --git a/spec/javascripts/unit/darkswarm/services/checkout_spec.js.coffee b/spec/javascripts/unit/darkswarm/services/checkout_spec.js.coffee index 5ca05a344f..4ff2f0d97a 100644 --- a/spec/javascripts/unit/darkswarm/services/checkout_spec.js.coffee +++ b/spec/javascripts/unit/darkswarm/services/checkout_spec.js.coffee @@ -95,6 +95,12 @@ describe 'Checkout service', -> Checkout.submit() $httpBackend.flush() + it "Redirects to the returned path", -> + $httpBackend.expectPUT("/checkout.json", {order: Checkout.preprocess()}).respond 200, {path: "/test"} + Checkout.submit() + $httpBackend.flush() + expect(Navigation.go).toHaveBeenCalledWith '/test' + describe "when there is an error", -> it "redirects when a redirect is given", -> $httpBackend.expectPUT("/checkout.json").respond 400, {path: 'path'} diff --git a/spec/javascripts/unit/darkswarm/services/credit_cards_spec.js.coffee b/spec/javascripts/unit/darkswarm/services/credit_cards_spec.js.coffee new file mode 100644 index 0000000000..be3106f89c --- /dev/null +++ b/spec/javascripts/unit/darkswarm/services/credit_cards_spec.js.coffee @@ -0,0 +1,50 @@ +describe 'CreditCards service', -> + CreditCards = $httpBackend = RailsFlashLoader = null + + beforeEach -> + module 'Darkswarm' + module ($provide)-> + $provide.value "savedCreditCards", [] + $provide.value "railsFlash", null + null + + inject (_CreditCards_, _$httpBackend_, _RailsFlashLoader_)-> + CreditCards = _CreditCards_ + $httpBackend = _$httpBackend_ + RailsFlashLoader = _RailsFlashLoader_ + + describe "setDefault", -> + card1 = { last4: "1234", is_default: true } + card2 = { last4: "4321", is_default: false } + card3 = { last4: "5555", is_default: false } + ajax = null + + beforeEach -> + CreditCards.saved = [card1, card2, card3] + ajax = $httpBackend.expectPUT("/credit_cards/#{card2.id}") + + it "resets the default value on other cards to false", -> + CreditCards.setDefault(card2) + expect(card1.is_default).toBe false + expect(card2.is_default).toBe true + expect(card3.is_default).toBe false + + describe "when the update request succeeds", -> + beforeEach -> + spyOn(RailsFlashLoader,"loadFlash") + ajax.respond(200) + + it "loads a success flash", -> + CreditCards.setDefault(card2) + $httpBackend.flush() + expect(RailsFlashLoader.loadFlash).toHaveBeenCalledWith({success: t('js.default_card_updated')}) + + describe "when the update request fails", -> + beforeEach -> + spyOn(RailsFlashLoader,"loadFlash") + ajax.respond(400, flash: { error: 'Some error message'}) + + it "loads a error flash", -> + CreditCards.setDefault(card2) + $httpBackend.flush() + expect(RailsFlashLoader.loadFlash).toHaveBeenCalledWith({error: 'Some error message'}) diff --git a/spec/javascripts/unit/darkswarm/services/navigation.js.coffee b/spec/javascripts/unit/darkswarm/services/navigation.js.coffee index 8b5912bff8..f8430655a9 100644 --- a/spec/javascripts/unit/darkswarm/services/navigation.js.coffee +++ b/spec/javascripts/unit/darkswarm/services/navigation.js.coffee @@ -3,7 +3,6 @@ describe 'Navigation service', -> window = location: href: null - pathname: null beforeEach -> module 'Darkswarm', ($provide) -> @@ -24,5 +23,4 @@ describe 'Navigation service', -> it "redirects to paths", -> Navigation.go "/woo/yeah" - expect(window.location.pathname).toEqual "/woo/yeah" - \ No newline at end of file + expect(window.location.href).toEqual "/woo/yeah" diff --git a/spec/jobs/subscription_confirm_job_spec.rb b/spec/jobs/subscription_confirm_job_spec.rb index 8dc425c087..f1b64b1343 100644 --- a/spec/jobs/subscription_confirm_job_spec.rb +++ b/spec/jobs/subscription_confirm_job_spec.rb @@ -16,21 +16,21 @@ describe SubscriptionConfirmJob do expect(proxy_orders).to include proxy_order end + it "returns proxy orders for paused subscriptions" do + subscription.update_attributes!(paused_at: 1.minute.ago) + expect(proxy_orders).to include proxy_order + end + + it "returns proxy orders for cancelled subscriptions" do + subscription.update_attributes!(canceled_at: 1.minute.ago) + expect(proxy_orders).to include proxy_order + end + it "ignores proxy orders where the OC closed more than 1 hour ago" do proxy_order.update_attributes!(order_cycle_id: order_cycle2.id) expect(proxy_orders).to_not include proxy_order end - it "ignores proxy orders for paused subscriptions" do - subscription.update_attributes!(paused_at: 1.minute.ago) - expect(proxy_orders).to_not include proxy_order - end - - it "ignores proxy orders for cancelled subscriptions" do - subscription.update_attributes!(canceled_at: 1.minute.ago) - expect(proxy_orders).to_not include proxy_order - end - it "ignores cancelled proxy orders" do proxy_order.update_attributes!(canceled_at: 5.minutes.ago) expect(proxy_orders).to_not include proxy_order diff --git a/spec/jobs/subscription_placement_job_spec.rb b/spec/jobs/subscription_placement_job_spec.rb index d1f6e1dfd9..b1013081d6 100644 --- a/spec/jobs/subscription_placement_job_spec.rb +++ b/spec/jobs/subscription_placement_job_spec.rb @@ -120,7 +120,12 @@ describe SubscriptionPlacementJob do describe "processing a subscription order" do let(:subscription) { create(:subscription, with_items: true) } + let(:shop) { subscription.shop } let(:proxy_order) { create(:proxy_order, subscription: subscription) } + let(:oc) { proxy_order.order_cycle } + let(:ex) { oc.exchanges.outgoing.find_by_sender_id_and_receiver_id(shop.id, shop.id) } + let(:fee) { create(:enterprise_fee, enterprise: shop, fee_type: 'sales', amount: 10) } + let!(:exchange_fee) { ExchangeFee.create!(exchange: ex, enterprise_fee: fee) } let!(:order) { proxy_order.initialise_order! } before do @@ -147,8 +152,11 @@ describe SubscriptionPlacementJob do allow(job).to receive(:unavailable_stock_lines_for) { order.line_items } end - it "does not place the order, sends an empty_order email" do + it "does not place the order, clears, all adjustments, and sends an empty_order email" do expect{ job.send(:process, order) }.to_not change{ order.reload.completed_at }.from(nil) + expect(order.adjustments).to be_empty + expect(order.total).to eq 0 + expect(order.adjustment_total).to eq 0 expect(job).to_not have_received(:send_placement_email) expect(job).to have_received(:send_empty_email) end diff --git a/spec/lib/open_food_network/bulk_coop_report_spec.rb b/spec/lib/open_food_network/bulk_coop_report_spec.rb index 53c2dd2ffe..7ee263fc55 100644 --- a/spec/lib/open_food_network/bulk_coop_report_spec.rb +++ b/spec/lib/open_food_network/bulk_coop_report_spec.rb @@ -14,7 +14,7 @@ module OpenFoodNetwork context "as a site admin" do let(:user) { create(:admin_user) } - subject { BulkCoopReport.new user } + subject { BulkCoopReport.new user, {}, true } it "fetches completed orders" do o2 = create(:order) @@ -31,7 +31,7 @@ module OpenFoodNetwork context "as a manager of a supplier" do let!(:user) { create(:user) } - subject { BulkCoopReport.new user } + subject { BulkCoopReport.new user, {}, true } let(:s1) { create(:supplier_enterprise) } @@ -70,7 +70,7 @@ module OpenFoodNetwork context "as a manager of a distributor" do let!(:user) { create(:user) } - subject { PackingReport.new user } + subject { PackingReport.new user, {}, true } before do d1.enterprise_roles.create!(user: user) diff --git a/spec/lib/open_food_network/customers_report_spec.rb b/spec/lib/open_food_network/customers_report_spec.rb index 289a32929e..c17d54368d 100644 --- a/spec/lib/open_food_network/customers_report_spec.rb +++ b/spec/lib/open_food_network/customers_report_spec.rb @@ -8,7 +8,7 @@ module OpenFoodNetwork user.spree_roles << Spree::Role.find_or_create_by_name!("admin") user end - subject { CustomersReport.new user } + subject { CustomersReport.new user, {}, true } describe "mailing list report" do before do @@ -81,7 +81,7 @@ module OpenFoodNetwork user end - subject { CustomersReport.new user } + subject { CustomersReport.new user, {}, true } describe "fetching orders" do let(:supplier) { create(:supplier_enterprise) } diff --git a/spec/lib/open_food_network/lettuce_share_report_spec.rb b/spec/lib/open_food_network/lettuce_share_report_spec.rb index f37e15b69e..5a871cfbbb 100644 --- a/spec/lib/open_food_network/lettuce_share_report_spec.rb +++ b/spec/lib/open_food_network/lettuce_share_report_spec.rb @@ -3,7 +3,7 @@ require 'open_food_network/lettuce_share_report' module OpenFoodNetwork describe LettuceShareReport do let(:user) { create(:user) } - let(:report) { LettuceShareReport.new user } + let(:report) { LettuceShareReport.new user, {}, true } let(:v) { create(:variant) } describe "grower and method" do diff --git a/spec/lib/open_food_network/order_and_distributor_report_spec.rb b/spec/lib/open_food_network/order_and_distributor_report_spec.rb index 5b96b20d75..4cd03de91c 100644 --- a/spec/lib/open_food_network/order_and_distributor_report_spec.rb +++ b/spec/lib/open_food_network/order_and_distributor_report_spec.rb @@ -1,48 +1,61 @@ require 'spec_helper' - module OpenFoodNetwork describe OrderAndDistributorReport do - - describe "orders and distributors report" do - - before(:each) do - #normal completed order - @bill_address = create(:address) - @distributor_address = create(:address, :address1 => "distributor address", :city => 'The Shire', :zipcode => "1234") - @distributor = create(:distributor_enterprise, :address => @distributor_address) - product = create(:product) - product_distribution = create(:product_distribution, :product => product, :distributor => @distributor) - @shipping_instructions = "pick up on thursday please!" - @order = create(:order, :distributor => @distributor, :bill_address => @bill_address, :special_instructions => @shipping_instructions) - @payment_method = create(:payment_method, :distributors => [@distributor]) - payment = create(:payment, :payment_method => @payment_method, :order => @order ) - @order.payments << payment - @line_item = create(:line_item, :product => product, :order => @order) - @order.line_items << @line_item - end - - it "should return a header row describing the report" do - subject = OrderAndDistributorReport.new [@order] + describe 'orders and distributors report' do + it 'should return a header row describing the report' do + subject = OrderAndDistributorReport.new nil header = subject.header - header.should == ["Order date", "Order Id", - "Customer Name","Customer Email", "Customer Phone", "Customer City", - "SKU", "Item name", "Variant", "Quantity", "Max Quantity", "Cost", "Shipping Cost", - "Payment Method", - "Distributor", "Distributor address", "Distributor city", "Distributor postcode", "Shipping instructions"] + expect(header).to eq(['Order date', 'Order Id', + 'Customer Name', 'Customer Email', 'Customer Phone', 'Customer City', + 'SKU', 'Item name', 'Variant', 'Quantity', 'Max Quantity', 'Cost', 'Shipping Cost', + 'Payment Method', + 'Distributor', 'Distributor address', 'Distributor city', 'Distributor postcode', 'Shipping instructions']) end - it "should denormalise order and distributor details for display as csv" do - subject = OrderAndDistributorReport.new [@order] + context 'with completed order' do + let(:bill_address) { create(:address) } + let(:distributor) { create(:distributor_enterprise) } + let(:product) { create(:product) } + let(:shipping_instructions) { 'pick up on thursday please!' } + let(:order) { create(:order, state: 'complete', completed_at: Time.zone.now, distributor: distributor, bill_address: bill_address, special_instructions: shipping_instructions) } + let(:payment_method) { create(:payment_method, distributors: [distributor]) } + let(:payment) { create(:payment, payment_method: payment_method, order: order) } + let(:line_item) { create(:line_item, product: product, order: order) } - table = subject.table + before do + order.payments << payment + order.line_items << line_item + end - table[0].should == [@order.created_at, @order.id, - @bill_address.full_name, @order.email, @bill_address.phone, @bill_address.city, - @line_item.product.sku, @line_item.product.name, @line_item.options_text, @line_item.quantity, @line_item.max_quantity, @line_item.price * @line_item.quantity, @line_item.distribution_fee, - @payment_method.name, - @distributor.name, @distributor.address.address1, @distributor.address.city, @distributor.address.zipcode, @shipping_instructions ] + it 'should denormalise order and distributor details for display as csv' do + subject = OrderAndDistributorReport.new create(:admin_user), {}, true + + table = subject.table + + expect(table[0]).to eq([ + order.reload.created_at, + order.id, + bill_address.full_name, + order.email, + bill_address.phone, + bill_address.city, + line_item.product.sku, + line_item.product.name, + line_item.options_text, + line_item.quantity, + line_item.max_quantity, + line_item.price * line_item.quantity, + line_item.distribution_fee, + payment_method.name, + distributor.name, + distributor.address.address1, + distributor.address.city, + distributor.address.zipcode, + shipping_instructions + ]) + end end end end diff --git a/spec/lib/open_food_network/order_cycle_form_applicator_spec.rb b/spec/lib/open_food_network/order_cycle_form_applicator_spec.rb index 9cd5461a37..db02f1656d 100644 --- a/spec/lib/open_food_network/order_cycle_form_applicator_spec.rb +++ b/spec/lib/open_food_network/order_cycle_form_applicator_spec.rb @@ -294,8 +294,8 @@ module OpenFoodNetwork end it "checks whether exchanges exist" do - oc = FactoryGirl.create(:simple_order_cycle) - exchange = FactoryGirl.create(:exchange, order_cycle: oc) + oc = FactoryBot.create(:simple_order_cycle) + exchange = FactoryBot.create(:exchange, order_cycle: oc) applicator = OrderCycleFormApplicator.new(oc, user) applicator.send(:exchange_exists?, exchange.sender_id, exchange.receiver_id, exchange.incoming).should be true @@ -425,9 +425,9 @@ module OpenFoodNetwork end it "does not add exchanges it is not permitted to touch" do - sender = FactoryGirl.create(:enterprise) - receiver = FactoryGirl.create(:enterprise) - oc = FactoryGirl.create(:simple_order_cycle) + sender = FactoryBot.create(:enterprise) + receiver = FactoryBot.create(:enterprise) + oc = FactoryBot.create(:simple_order_cycle) applicator = OrderCycleFormApplicator.new(oc, user) incoming = true @@ -438,13 +438,13 @@ module OpenFoodNetwork end it "does not update exchanges it is not permitted to touch" do - sender = FactoryGirl.create(:enterprise) - receiver = FactoryGirl.create(:enterprise) - oc = FactoryGirl.create(:simple_order_cycle) + sender = FactoryBot.create(:enterprise) + receiver = FactoryBot.create(:enterprise) + oc = FactoryBot.create(:simple_order_cycle) applicator = OrderCycleFormApplicator.new(oc, user) incoming = true - exchange = FactoryGirl.create(:exchange, order_cycle: oc, sender: sender, receiver: receiver, incoming: incoming) - variant1 = FactoryGirl.create(:variant) + exchange = FactoryBot.create(:exchange, order_cycle: oc, sender: sender, receiver: receiver, incoming: incoming) + variant1 = FactoryBot.create(:variant) applicator.send(:touched_exchanges=, []) applicator.send(:update_exchange, sender.id, receiver.id, incoming, {:variant_ids => [variant1.id]}) diff --git a/spec/lib/open_food_network/order_cycle_management_report_spec.rb b/spec/lib/open_food_network/order_cycle_management_report_spec.rb index 99ebf575af..49a9915301 100644 --- a/spec/lib/open_food_network/order_cycle_management_report_spec.rb +++ b/spec/lib/open_food_network/order_cycle_management_report_spec.rb @@ -10,7 +10,7 @@ module OpenFoodNetwork user.spree_roles << Spree::Role.find_or_create_by_name!("admin") user end - subject { OrderCycleManagementReport.new user } + subject { OrderCycleManagementReport.new user, {}, true } describe "fetching orders" do it "fetches completed orders" do @@ -30,7 +30,7 @@ module OpenFoodNetwork context "as an enterprise user" do let!(:user) { create_enterprise_user } - subject { OrderCycleManagementReport.new user } + subject { OrderCycleManagementReport.new user, {}, true } describe "fetching orders" do let(:supplier) { create(:supplier_enterprise) } diff --git a/spec/lib/open_food_network/orders_and_fulfillments_report_spec.rb b/spec/lib/open_food_network/orders_and_fulfillments_report_spec.rb index f37dcefa02..a33722859c 100644 --- a/spec/lib/open_food_network/orders_and_fulfillments_report_spec.rb +++ b/spec/lib/open_food_network/orders_and_fulfillments_report_spec.rb @@ -14,7 +14,7 @@ module OpenFoodNetwork context "as a site admin" do let(:user) { create(:admin_user) } - subject { PackingReport.new user } + subject { PackingReport.new user, {}, true } it "fetches completed orders" do o2 = create(:order) @@ -31,7 +31,7 @@ module OpenFoodNetwork context "as a manager of a supplier" do let!(:user) { create(:user) } - subject { OrdersAndFulfillmentsReport.new user } + subject { OrdersAndFulfillmentsReport.new user, {}, true } let(:s1) { create(:supplier_enterprise) } @@ -70,7 +70,7 @@ module OpenFoodNetwork context "as a manager of a distributor" do let!(:user) { create(:user) } - subject { OrdersAndFulfillmentsReport.new user } + subject { OrdersAndFulfillmentsReport.new user, {}, true } before do d1.enterprise_roles.create!(user: user) diff --git a/spec/lib/open_food_network/packing_report_spec.rb b/spec/lib/open_food_network/packing_report_spec.rb index 6b02a26fed..b9074a92bf 100644 --- a/spec/lib/open_food_network/packing_report_spec.rb +++ b/spec/lib/open_food_network/packing_report_spec.rb @@ -14,7 +14,7 @@ module OpenFoodNetwork context "as a site admin" do let(:user) { create(:admin_user) } - subject { PackingReport.new user } + subject { PackingReport.new user, {}, true } it "fetches completed orders" do o2 = create(:order) @@ -31,7 +31,7 @@ module OpenFoodNetwork context "as a manager of a supplier" do let!(:user) { create(:user) } - subject { PackingReport.new user } + subject { PackingReport.new user, {}, true } let(:s1) { create(:supplier_enterprise) } @@ -70,7 +70,7 @@ module OpenFoodNetwork context "as a manager of a distributor" do let!(:user) { create(:user) } - subject { PackingReport.new user } + subject { PackingReport.new user, {}, true } before do d1.enterprise_roles.create!(user: user) diff --git a/spec/lib/open_food_network/products_and_inventory_report_spec.rb b/spec/lib/open_food_network/products_and_inventory_report_spec.rb index f0fae499fe..80bdaebcf7 100644 --- a/spec/lib/open_food_network/products_and_inventory_report_spec.rb +++ b/spec/lib/open_food_network/products_and_inventory_report_spec.rb @@ -9,7 +9,7 @@ module OpenFoodNetwork user end subject do - ProductsAndInventoryReport.new user + ProductsAndInventoryReport.new user, {}, true end it "Should return headers" do @@ -72,7 +72,7 @@ module OpenFoodNetwork end subject do - ProductsAndInventoryReport.new enterprise_user + ProductsAndInventoryReport.new enterprise_user, {}, true end describe "fetching child variants" do diff --git a/spec/lib/open_food_network/proxy_order_syncer_spec.rb b/spec/lib/open_food_network/proxy_order_syncer_spec.rb index eb7684ad28..9b059d178f 100644 --- a/spec/lib/open_food_network/proxy_order_syncer_spec.rb +++ b/spec/lib/open_food_network/proxy_order_syncer_spec.rb @@ -12,66 +12,387 @@ module OpenFoodNetwork end end - describe "updating proxy_orders on a subscriptions" do + describe "#sync!" do let(:now) { Time.zone.now } - let!(:schedule) { create(:schedule) } - let!(:subscription) { create(:subscription, schedule: schedule, begins_at: now + 1.minute, ends_at: now + 2.minutes) } - let!(:oc1) { create(:simple_order_cycle, schedules: [schedule], orders_open_at: now - 1.minute, orders_close_at: now) } # Closed - let!(:oc2) { create(:simple_order_cycle, schedules: [schedule], orders_open_at: now - 1.minute, orders_close_at: now + 59.seconds) } # Open, but closes before begins at - let!(:oc3) { create(:simple_order_cycle, schedules: [schedule], orders_open_at: now - 1.minute, orders_close_at: now + 1.minute) } # Open + closes on begins at - let!(:oc4) { create(:simple_order_cycle, schedules: [schedule], orders_open_at: now - 1.minute, orders_close_at: now + 2.minutes) } # Open + closes on ends at - let!(:oc5) { create(:simple_order_cycle, schedules: [schedule], orders_open_at: now - 1.minute, orders_close_at: now + 121.seconds) } # Open + closes after ends at + let(:schedule) { create(:schedule) } + let(:closed_oc) { create(:simple_order_cycle, schedules: [schedule], orders_open_at: now - 1.minute, orders_close_at: now) } # Closed + let(:open_oc_closes_before_begins_at_oc) { create(:simple_order_cycle, schedules: [schedule], orders_open_at: now - 1.minute, orders_close_at: now + 59.seconds) } # Open, but closes before begins at + let(:open_oc) { create(:simple_order_cycle, schedules: [schedule], orders_open_at: now - 1.minute, orders_close_at: now + 90.seconds) } # Open & closes between begins at and ends at + let(:upcoming_closes_before_begins_at_oc) { create(:simple_order_cycle, schedules: [schedule], orders_open_at: now + 30.seconds, orders_close_at: now + 59.seconds) } # Upcoming, but closes before begins at + let(:upcoming_closes_on_begins_at_oc) { create(:simple_order_cycle, schedules: [schedule], orders_open_at: now + 30.seconds, orders_close_at: now + 1.minute) } # Upcoming & closes on begins at + let(:upcoming_closes_on_ends_at_oc) { create(:simple_order_cycle, schedules: [schedule], orders_open_at: now + 30.seconds, orders_close_at: now + 2.minutes) } # Upcoming & closes on ends at + let(:upcoming_closes_after_ends_at_oc) { create(:simple_order_cycle, schedules: [schedule], orders_open_at: now + 30.seconds, orders_close_at: now + 121.seconds) } # Upcoming & closes after ends at + let(:subscription) { build(:subscription, schedule: schedule, begins_at: now + 1.minute, ends_at: now + 2.minutes) } + let(:proxy_orders) { subscription.reload.proxy_orders } + let(:order_cycles) { proxy_orders.map(&:order_cycle) } let(:syncer) { ProxyOrderSyncer.new(subscription) } - describe "#sync!" do - let!(:oc6) { create(:simple_order_cycle, schedules: [schedule], orders_open_at: now - 1.minute, orders_close_at: now + 59.seconds) } # Open, but closes before begins at - let!(:oc7) { create(:simple_order_cycle, schedules: [schedule], orders_open_at: now - 1.minute, orders_close_at: now + 61.seconds) } # Open + closes on begins at - let!(:oc8) { create(:simple_order_cycle, schedules: [schedule], orders_open_at: now - 1.minute, orders_close_at: now + 121.seconds) } # Open + closes after ends at - let!(:po1) { create(:proxy_order, subscription: subscription, order_cycle: oc6) } - let!(:po2) { create(:proxy_order, subscription: subscription, order_cycle: oc7) } - let!(:po3) { create(:proxy_order, subscription: subscription, order_cycle: oc8) } + context "when the subscription is not persisted" do + before do + oc # Ensure oc is created before we attempt to sync + expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(0) + end - it "performs both create and remove actions to rectify proxy orders" do - expect(syncer).to receive(:create_proxy_orders!).and_call_original - expect(syncer).to receive(:remove_obsolete_proxy_orders!).and_call_original - syncer.sync! - subscription.reload - expect(subscription.proxy_orders).to include po2 - expect(subscription.proxy_orders).to_not include po1, po3 - expect(subscription.proxy_orders.map(&:order_cycle)).to include oc3, oc4, oc7 - expect(subscription.proxy_orders.map(&:order_cycle)).to_not include oc1, oc2, oc5, oc6, oc8 + context "and the schedule includes a closed oc (ie. closed before opens_at)" do + let(:oc) { closed_oc } + it "does not create a new proxy order for that oc" do + expect{ subscription.save! }.to_not change(ProxyOrder, :count).from(0) + expect(order_cycles).to_not include oc + end + end + + context "and the schedule includes an open oc that closes before begins_at" do + let(:oc) { open_oc_closes_before_begins_at_oc } + it "does not create a new proxy order for that oc" do + expect{ subscription.save! }.to_not change(ProxyOrder, :count).from(0) + expect(order_cycles).to_not include oc + end + end + + context "and the schedule includes an open oc that closes between begins_at and ends_at" do + let(:oc) { open_oc } + it "creates a new proxy order for that oc" do + expect{ subscription.save! }.to change(ProxyOrder, :count).from(0).to(1) + expect(order_cycles).to include oc + end + end + + context "and the schedule includes upcoming oc that closes before begins_at" do + let(:oc) { upcoming_closes_before_begins_at_oc } + it "does not create a new proxy order for that oc" do + expect{ subscription.save! }.to_not change(ProxyOrder, :count).from(0) + expect(order_cycles).to_not include oc + end + end + + context "and the schedule includes upcoming oc that closes on begins_at" do + let(:oc) { upcoming_closes_on_begins_at_oc } + it "creates a new proxy order for that oc" do + expect{ subscription.save! }.to change(ProxyOrder, :count).from(0).to(1) + expect(order_cycles).to include oc + end + end + + context "and the schedule includes upcoming oc that closes after ends_at" do + let(:oc) { upcoming_closes_on_ends_at_oc } + it "creates a new proxy order for that oc" do + expect{ subscription.save! }.to change(ProxyOrder, :count).from(0).to(1) + expect(order_cycles).to include oc + end + end + + context "and the schedule includes upcoming oc that closes after ends_at" do + let(:oc) { upcoming_closes_after_ends_at_oc } + it "does not create a new proxy order for that oc" do + expect{ subscription.save! }.to_not change(ProxyOrder, :count).from(0) + expect(order_cycles).to_not include oc + end end end - describe "#initialise_proxy_orders!" do - let(:new_subscription) { build(:subscription, schedule: schedule, begins_at: now + 1.minute, ends_at: now + 2.minutes) } - it "builds proxy orders for in-range order cycles that are not already closed" do - allow(syncer).to receive(:subscription) { new_subscription } - expect{ syncer.send(:initialise_proxy_orders!) }.to_not change(ProxyOrder, :count).from(0) - expect{ new_subscription.save! }.to change(ProxyOrder, :count).from(0).to(2) - expect(new_subscription.proxy_orders.map(&:order_cycle_id)).to include oc3.id, oc4.id + context "when the subscription is persisted" do + before { expect(subscription.save!).to be true } + + context "when a proxy order exists" do + let!(:proxy_order) { create(:proxy_order, subscription: subscription, order_cycle: oc) } + + context "for an oc included in the relevant schedule" do + context "and the proxy order has already been placed" do + before { proxy_order.update_attributes(placed_at: 5.minutes.ago) } + + context "the oc is closed (ie. closed before opens_at)" do + let(:oc) { closed_oc } + it "keeps the proxy order" do + expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(1) + expect(proxy_orders).to include proxy_order + end + end + + context "and the schedule includes an open oc that closes before begins_at" do + let(:oc) { open_oc_closes_before_begins_at_oc } + it "keeps the proxy order" do + expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(1) + expect(proxy_orders).to include proxy_order + end + end + + context "and the oc is open and closes between begins_at and ends_at" do + let(:oc) { open_oc } + it "keeps the proxy order" do + expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(1) + expect(proxy_orders).to include proxy_order + end + end + + context "and the oc is upcoming and closes before begins_at" do + let(:oc) { upcoming_closes_before_begins_at_oc } + it "keeps the proxy order" do + expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(1) + expect(proxy_orders).to include proxy_order + end + end + + context "and the oc is upcoming and closes on begins_at" do + let(:oc) { upcoming_closes_on_begins_at_oc } + it "keeps the proxy order" do + expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(1) + expect(proxy_orders).to include proxy_order + end + end + + context "and the oc is upcoming and closes on ends_at" do + let(:oc) { upcoming_closes_on_ends_at_oc } + it "keeps the proxy order" do + expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(1) + expect(proxy_orders).to include proxy_order + end + end + + context "and the oc is upcoming and closes after ends_at" do + let(:oc) { upcoming_closes_after_ends_at_oc } + it "keeps the proxy order" do + expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(1) + expect(proxy_orders).to include proxy_order + end + end + end + + context "and the proxy order has not already been placed" do + context "the oc is closed (ie. closed before opens_at)" do + let(:oc) { closed_oc } + it "removes the proxy order" do + expect{ syncer.sync! }.to change(ProxyOrder, :count).from(1).to(0) + expect(proxy_orders).to_not include proxy_order + end + end + + context "and the schedule includes an open oc that closes before begins_at" do + let(:oc) { open_oc_closes_before_begins_at_oc } + it "removes the proxy order" do + expect{ syncer.sync! }.to change(ProxyOrder, :count).from(1).to(0) + expect(proxy_orders).to_not include proxy_order + end + end + + context "and the oc is open and closes between begins_at and ends_at" do + let(:oc) { open_oc } + it "keeps the proxy order" do + expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(1) + expect(proxy_orders).to include proxy_order + end + end + + context "and the oc is upcoming and closes before begins_at" do + let(:oc) { upcoming_closes_before_begins_at_oc } + it "removes the proxy order" do + expect{ syncer.sync! }.to change(ProxyOrder, :count).from(1).to(0) + expect(proxy_orders).to_not include proxy_order + end + end + + context "and the oc is upcoming and closes on begins_at" do + let(:oc) { upcoming_closes_on_begins_at_oc } + it "keeps the proxy order" do + expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(1) + expect(proxy_orders).to include proxy_order + end + end + + context "and the oc is upcoming and closes on ends_at" do + let(:oc) { upcoming_closes_on_ends_at_oc } + it "keeps the proxy order" do + expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(1) + expect(proxy_orders).to include proxy_order + end + end + + context "and the oc is upcoming and closes after ends_at" do + let(:oc) { upcoming_closes_after_ends_at_oc } + it "removes the proxy order" do + expect{ syncer.sync! }.to change(ProxyOrder, :count).from(1).to(0) + expect(proxy_orders).to_not include proxy_order + end + end + end + end + + context "for an oc not included in the relevant schedule" do + let!(:proxy_order) { create(:proxy_order, subscription: subscription, order_cycle: open_oc) } + before do + open_oc.schedule_ids = [] + expect(open_oc.save!).to be true + end + + context "and the proxy order has already been placed" do + before { proxy_order.update_attributes(placed_at: 5.minutes.ago) } + + context "the oc is closed (ie. closed before opens_at)" do + let(:oc) { closed_oc } + it "keeps the proxy order" do + expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(1) + expect(proxy_orders).to include proxy_order + end + end + + context "and the schedule includes an open oc that closes before begins_at" do + let(:oc) { open_oc_closes_before_begins_at_oc } + it "keeps the proxy order" do + expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(1) + expect(proxy_orders).to include proxy_order + end + end + + context "and the oc is open and closes between begins_at and ends_at" do + let(:oc) { open_oc } + it "keeps the proxy order" do + expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(1) + expect(proxy_orders).to include proxy_order + end + end + + context "and the oc is upcoming and closes before begins_at" do + let(:oc) { upcoming_closes_before_begins_at_oc } + it "keeps the proxy order" do + expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(1) + expect(proxy_orders).to include proxy_order + end + end + + context "and the oc is upcoming and closes on begins_at" do + let(:oc) { upcoming_closes_on_begins_at_oc } + it "keeps the proxy order" do + expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(1) + expect(proxy_orders).to include proxy_order + end + end + + context "and the oc is upcoming and closes on ends_at" do + let(:oc) { upcoming_closes_on_ends_at_oc } + it "keeps the proxy order" do + expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(1) + expect(proxy_orders).to include proxy_order + end + end + + context "and the oc is upcoming and closes after ends_at" do + let(:oc) { upcoming_closes_after_ends_at_oc } + it "keeps the proxy order" do + expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(1) + expect(proxy_orders).to include proxy_order + end + end + end + + context "and the proxy order has not already been placed" do + # This shouldn't really happen, but it is possible + context "the oc is closed (ie. closed before opens_at)" do + let(:oc) { closed_oc } + it "removes the proxy order" do + expect{ syncer.sync! }.to change(ProxyOrder, :count).from(1).to(0) + expect(proxy_orders).to_not include proxy_order + end + end + + # This shouldn't really happen, but it is possible + context "and the oc is open and closes between begins_at and ends_at" do + let(:oc) { open_oc } + it "removes the proxy order" do + expect{ syncer.sync! }.to change(ProxyOrder, :count).from(1).to(0) + expect(proxy_orders).to_not include proxy_order + end + end + + context "and the oc is upcoming and closes before begins_at" do + let(:oc) { upcoming_closes_before_begins_at_oc } + it "removes the proxy order" do + expect{ syncer.sync! }.to change(ProxyOrder, :count).from(1).to(0) + expect(proxy_orders).to_not include proxy_order + end + end + + context "and the oc is upcoming and closes on begins_at" do + let(:oc) { upcoming_closes_on_begins_at_oc } + it "removes the proxy order" do + expect{ syncer.sync! }.to change(ProxyOrder, :count).from(1).to(0) + expect(proxy_orders).to_not include proxy_order + end + end + + context "and the oc is upcoming and closes on ends_at" do + let(:oc) { upcoming_closes_on_ends_at_oc } + it "removes the proxy order" do + expect{ syncer.sync! }.to change(ProxyOrder, :count).from(1).to(0) + expect(proxy_orders).to_not include proxy_order + end + end + + context "and the oc is upcoming and closes after ends_at" do + let(:oc) { upcoming_closes_after_ends_at_oc } + it "removes the proxy order" do + expect{ syncer.sync! }.to change(ProxyOrder, :count).from(1).to(0) + expect(proxy_orders).to_not include proxy_order + end + end + end + end end - end - describe "#create_proxy_orders!" do - it "creates proxy orders for in-range order cycles that are not already closed" do - allow(syncer).to receive(:subscription) { subscription } - expect{ syncer.send(:create_proxy_orders!) }.to change(ProxyOrder, :count).from(0).to(2) - expect(subscription.proxy_orders.map(&:order_cycle)).to include oc3, oc4 - end - end + context "when a proxy order does not exist" do + context "and the schedule includes a closed oc (ie. closed before opens_at)" do + let!(:oc) { closed_oc } + it "does not create a new proxy order for that oc" do + expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(0) + expect(order_cycles).to_not include oc + end + end - describe "#remove_obsolete_proxy_orders!" do - let!(:po1) { create(:proxy_order, subscription: subscription, order_cycle: oc1) } - let!(:po2) { create(:proxy_order, subscription: subscription, order_cycle: oc2) } - let!(:po3) { create(:proxy_order, subscription: subscription, order_cycle: oc3) } - let!(:po4) { create(:proxy_order, subscription: subscription, order_cycle: oc4) } - let!(:po5) { create(:proxy_order, subscription: subscription, order_cycle: oc5) } + context "and the schedule includes an open oc that closes before begins_at" do + let(:oc) { open_oc_closes_before_begins_at_oc } + it "does not create a new proxy order for that oc" do + expect{ subscription.save! }.to_not change(ProxyOrder, :count).from(0) + expect(order_cycles).to_not include oc + end + end - it "destroys proxy orders that are closed or out of range" do - allow(syncer).to receive(:subscription) { subscription } - expect{ syncer.send(:remove_obsolete_proxy_orders!) }.to change(ProxyOrder, :count).from(5).to(2) - expect(subscription.proxy_orders.map(&:order_cycle)).to include oc3, oc4 + context "and the schedule includes an open oc that closes between begins_at and ends_at" do + let!(:oc) { open_oc } + it "creates a new proxy order for that oc" do + expect{ syncer.sync! }.to change(ProxyOrder, :count).from(0).to(1) + expect(order_cycles).to include oc + end + end + + context "and the schedule includes upcoming oc that closes before begins_at" do + let!(:oc) { upcoming_closes_before_begins_at_oc } + it "does not create a new proxy order for that oc" do + expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(0) + expect(order_cycles).to_not include oc + end + end + + context "and the schedule includes upcoming oc that closes on begins_at" do + let!(:oc) { upcoming_closes_on_begins_at_oc } + it "creates a new proxy order for that oc" do + expect{ syncer.sync! }.to change(ProxyOrder, :count).from(0).to(1) + expect(order_cycles).to include oc + end + end + + context "and the schedule includes upcoming oc that closes on ends_at" do + let!(:oc) { upcoming_closes_on_ends_at_oc } + it "creates a new proxy order for that oc" do + expect{ syncer.sync! }.to change(ProxyOrder, :count).from(0).to(1) + expect(order_cycles).to include oc + end + end + + context "and the schedule includes upcoming oc that closes after ends_at" do + let!(:oc) { upcoming_closes_after_ends_at_oc } + it "does not create a new proxy order for that oc" do + expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(0) + expect(order_cycles).to_not include oc + end + end end end end diff --git a/spec/lib/open_food_network/sales_tax_report_spec.rb b/spec/lib/open_food_network/sales_tax_report_spec.rb index a6445fa1f9..ac7bc7e47f 100644 --- a/spec/lib/open_food_network/sales_tax_report_spec.rb +++ b/spec/lib/open_food_network/sales_tax_report_spec.rb @@ -3,7 +3,7 @@ require 'open_food_network/sales_tax_report' module OpenFoodNetwork describe SalesTaxReport do let(:user) { create(:user) } - let(:report) { SalesTaxReport.new(user, {}) } + let(:report) { SalesTaxReport.new(user, {}, true) } describe "calculating totals for line items" do let(:li1) { double(:line_item, quantity: 1, amount: 12) } diff --git a/spec/lib/open_food_network/users_and_enterprises_report_spec.rb b/spec/lib/open_food_network/users_and_enterprises_report_spec.rb index 906bef1eef..bd33825722 100644 --- a/spec/lib/open_food_network/users_and_enterprises_report_spec.rb +++ b/spec/lib/open_food_network/users_and_enterprises_report_spec.rb @@ -7,7 +7,7 @@ module OpenFoodNetwork describe "users_and_enterprises" do let!(:owners_and_enterprises) { double(:owners_and_enterprises) } let!(:managers_and_enterprises) { double(:managers_and_enterprises) } - let!(:subject) { OpenFoodNetwork::UsersAndEnterprisesReport.new {} } + let!(:subject) { OpenFoodNetwork::UsersAndEnterprisesReport.new({}, true) } before do subject.stub(:owners_and_enterprises) { owners_and_enterprises } @@ -24,7 +24,7 @@ module OpenFoodNetwork end describe "sorting results" do - let!(:subject) { OpenFoodNetwork::UsersAndEnterprisesReport.new {} } + let!(:subject) { OpenFoodNetwork::UsersAndEnterprisesReport.new({}, true) } it "sorts by creation date" do uae_mock = [ @@ -68,7 +68,7 @@ module OpenFoodNetwork describe "for owners and enterprises" do describe "by enterprise id" do let!(:params) { { enterprise_id_in: [enterprise1.id.to_s] } } - let!(:subject) { OpenFoodNetwork::UsersAndEnterprisesReport.new params } + let!(:subject) { OpenFoodNetwork::UsersAndEnterprisesReport.new params, true } it "excludes enterprises that are not explicitly requested" do results = subject.owners_and_enterprises.to_a.map{ |oae| oae["name"] } @@ -79,7 +79,7 @@ module OpenFoodNetwork describe "by user id" do let!(:params) { { user_id_in: [enterprise1.owner.id.to_s] } } - let!(:subject) { OpenFoodNetwork::UsersAndEnterprisesReport.new params } + let!(:subject) { OpenFoodNetwork::UsersAndEnterprisesReport.new params, true } it "excludes enterprises that are not explicitly requested" do results = subject.owners_and_enterprises.to_a.map{ |oae| oae["name"] } @@ -92,7 +92,7 @@ module OpenFoodNetwork describe "for managers and enterprises" do describe "by enterprise id" do let!(:params) { { enterprise_id_in: [enterprise1.id.to_s] } } - let!(:subject) { OpenFoodNetwork::UsersAndEnterprisesReport.new params } + let!(:subject) { OpenFoodNetwork::UsersAndEnterprisesReport.new params, true } it "excludes enterprises that are not explicitly requested" do results = subject.managers_and_enterprises.to_a.map{ |mae| mae["name"] } @@ -105,7 +105,7 @@ module OpenFoodNetwork let!(:manager1) { create_enterprise_user } let!(:manager2) { create_enterprise_user } let!(:params) { { user_id_in: [manager1.id.to_s] } } - let!(:subject) { OpenFoodNetwork::UsersAndEnterprisesReport.new params } + let!(:subject) { OpenFoodNetwork::UsersAndEnterprisesReport.new params, true } before do enterprise1.enterprise_roles.build(user: manager1).save diff --git a/spec/lib/open_food_network/xero_invoices_report_spec.rb b/spec/lib/open_food_network/xero_invoices_report_spec.rb index af67f8bed9..e5ae74d8d0 100644 --- a/spec/lib/open_food_network/xero_invoices_report_spec.rb +++ b/spec/lib/open_food_network/xero_invoices_report_spec.rb @@ -1,82 +1,83 @@ +require 'spec_helper' require 'open_food_network/xero_invoices_report' module OpenFoodNetwork describe XeroInvoicesReport do - subject { XeroInvoicesReport.new user } + subject { XeroInvoicesReport.new user, {}, true } let(:user) { create(:user) } describe "option defaults" do - let(:report) { XeroInvoicesReport.new user, {initial_invoice_number: '', invoice_date: '', due_date: '', account_code: ''} } + let(:report) { XeroInvoicesReport.new user, initial_invoice_number: '', invoice_date: '', due_date: '', account_code: '' } around { |example| Timecop.travel(Time.zone.local(2015, 5, 5, 14, 0, 0)) { example.run } } it "uses defaults when blank params are passed" do - report.instance_variable_get(:@opts).should == {invoice_date: Date.civil(2015, 5, 5), - due_date: Date.civil(2015, 6, 5), - account_code: 'food sales', - report_type: 'summary'} + expect(report.instance_variable_get(:@opts)).to eq( invoice_date: Date.civil(2015, 5, 5), + due_date: Date.civil(2015, 6, 5), + account_code: 'food sales', + report_type: 'summary' ) end end describe "summary rows" do - let(:report) { XeroInvoicesReport.new user, {initial_invoice_number: '', invoice_date: '', due_date: '', account_code: ''} } + let(:report) { XeroInvoicesReport.new user, initial_invoice_number: '', invoice_date: '', due_date: '', account_code: '' } let(:order) { double(:order) } let(:summary_rows) { report.send(:summary_rows_for_order, order, 1, {}) } before do - report.stub(:produce_summary_rows) { ['produce'] } - report.stub(:fee_summary_rows) { ['fee'] } - report.stub(:shipping_summary_rows) { ['shipping'] } - report.stub(:payment_summary_rows) { ['payment'] } - report.stub(:admin_adjustment_summary_rows) { ['admin'] } - order.stub(:account_invoice?) { false } + allow(report).to receive(:produce_summary_rows) { ['produce'] } + allow(report).to receive(:fee_summary_rows) { ['fee'] } + allow(report).to receive(:shipping_summary_rows) { ['shipping'] } + allow(report).to receive(:payment_summary_rows) { ['payment'] } + allow(report).to receive(:admin_adjustment_summary_rows) { ['admin'] } + allow(order).to receive(:account_invoice?) { false } end it "displays produce summary rows when summary report" do - report.stub(:detail?) { false } - summary_rows.should include 'produce' + allow(report).to receive(:detail?) { false } + expect(summary_rows).to include 'produce' end it "does not display produce summary rows when detail report" do - report.stub(:detail?) { true } - summary_rows.should_not include 'produce' + allow(report).to receive(:detail?) { true } + expect(summary_rows).not_to include 'produce' end it "displays fee summary rows when summary report" do - report.stub(:detail?) { false } - order.stub(:account_invoice?) { true } - summary_rows.should include 'fee' + allow(report).to receive(:detail?) { false } + allow(order).to receive(:account_invoice?) { true } + expect(summary_rows).to include 'fee' end it "displays fee summary rows when this is not an account invoice" do - report.stub(:detail?) { true } - order.stub(:account_invoice?) { false } - summary_rows.should include 'fee' + allow(report).to receive(:detail?) { true } + allow(order).to receive(:account_invoice?) { false } + expect(summary_rows).to include 'fee' end it "does not display fee summary rows when this is a detail report for an account invoice" do - report.stub(:detail?) { true } - order.stub(:account_invoice?) { true } - summary_rows.should_not include 'fee' + allow(report).to receive(:detail?) { true } + allow(order).to receive(:account_invoice?) { true } + expect(summary_rows).not_to include 'fee' end it "always displays shipping summary rows" do - summary_rows.should include 'shipping' + expect(summary_rows).to include 'shipping' end it "displays admin adjustment summary rows when summary report" do - summary_rows.should include 'admin' + expect(summary_rows).to include 'admin' end it "does not display admin adjustment summary rows when detail report" do - report.stub(:detail?) { true } - summary_rows.should_not include 'admin' + allow(report).to receive(:detail?) { true } + expect(summary_rows).not_to include 'admin' end end describe "finding account invoice adjustments" do - let(:report) { XeroInvoicesReport.new user, {initial_invoice_number: '', invoice_date: '', due_date: '', account_code: ''} } + let(:report) { XeroInvoicesReport.new user, initial_invoice_number: '', invoice_date: '', due_date: '', account_code: '' } let!(:order) { create(:order) } let(:billable_period) { create(:billable_period) } let(:shipping_method) { create(:shipping_method) } @@ -84,12 +85,12 @@ module OpenFoodNetwork let!(:adj_shipping) { create(:adjustment, adjustable: order, label: "Shipping", originator: shipping_method) } it "returns BillablePeriod adjustments only" do - report.send(:account_invoice_adjustments, order).should == [adj_invoice] + expect(report.send(:account_invoice_adjustments, order)).to eq([adj_invoice]) end it "excludes adjustments where the source is missing" do billable_period.destroy - report.send(:account_invoice_adjustments, order).should be_empty + expect(report.send(:account_invoice_adjustments, order)).to be_empty end end @@ -98,15 +99,15 @@ module OpenFoodNetwork describe "when no initial invoice number is given" do it "returns the order number" do - subject.send(:invoice_number_for, order, 123).should == 'R731032860' + expect(subject.send(:invoice_number_for, order, 123)).to eq('R731032860') end end describe "when an initial invoice number is given" do - subject { XeroInvoicesReport.new user, {initial_invoice_number: '123'} } + subject { XeroInvoicesReport.new user, initial_invoice_number: '123' } it "increments the number by the index" do - subject.send(:invoice_number_for, order, 456).should == 579 + expect(subject.send(:invoice_number_for, order, 456)).to eq(579) end end end diff --git a/spec/models/cart_spec.rb b/spec/models/cart_spec.rb index 9ef473db3e..08bc4ff3a8 100644 --- a/spec/models/cart_spec.rb +++ b/spec/models/cart_spec.rb @@ -10,11 +10,11 @@ describe Cart do describe 'when adding a product' do let(:cart) { Cart.create(user: user) } - let(:distributor) { FactoryGirl.create(:distributor_enterprise) } - let(:other_distributor) { FactoryGirl.create(:distributor_enterprise) } + let(:distributor) { FactoryBot.create(:distributor_enterprise) } + let(:other_distributor) { FactoryBot.create(:distributor_enterprise) } let(:currency) { "AUD" } - let(:product) { FactoryGirl.create(:product, :distributors => [distributor]) } + let(:product) { FactoryBot.create(:product, :distributors => [distributor]) } let(:product_with_order_cycle) { create(:product) } let(:order_cycle) { create(:simple_order_cycle, distributors: [distributor, other_distributor], variants: [product_with_order_cycle.master]) } @@ -44,11 +44,11 @@ describe Cart do end describe 'to a cart with an order for a distributor' do - let(:product_from_other_distributor) { FactoryGirl.create(:product, :distributors => [other_distributor]) } - let(:order) { FactoryGirl.create(:order, :distributor => distributor) } + let(:product_from_other_distributor) { FactoryBot.create(:product, :distributors => [other_distributor]) } + let(:order) { FactoryBot.create(:order, :distributor => distributor) } before do - FactoryGirl.create(:line_item, :order => order, :product => product) + FactoryBot.create(:line_item, :order => order, :product => product) order.reload subject.orders << order subject.save! @@ -86,7 +86,7 @@ describe Cart do end describe 'existing order for distributor and order cycle' do - let(:order) { FactoryGirl.create(:order, :distributor => distributor, :order_cycle => order_cycle) } + let(:order) { FactoryBot.create(:order, :distributor => distributor, :order_cycle => order_cycle) } before do subject.orders << order diff --git a/spec/models/enterprise_spec.rb b/spec/models/enterprise_spec.rb index 781a60475d..8fc1b57a0b 100644 --- a/spec/models/enterprise_spec.rb +++ b/spec/models/enterprise_spec.rb @@ -112,7 +112,7 @@ describe Enterprise do end describe "validations" do - subject { FactoryGirl.create(:distributor_enterprise) } + subject { FactoryBot.create(:distributor_enterprise) } it { should validate_presence_of(:name) } it { should validate_uniqueness_of(:permalink) } it { should ensure_length_of(:description).is_at_most(255) } @@ -129,17 +129,15 @@ describe Enterprise do it "prevents duplicate names for new records" do e = Enterprise.new name: enterprise.name - e.should_not be_valid - e.errors[:name].first.should == - "has already been taken. If this is your enterprise and you would like to claim ownership, please contact the current manager of this profile at owner@example.com." + expect(e).to_not be_valid + expect(e.errors[:name].first).to include I18n.t('enterprise_name_error', email: owner.email) end it "prevents duplicate names for existing records" do e = create(:enterprise, name: 'foo') e.name = enterprise.name - e.should_not be_valid - e.errors[:name].first.should == - "has already been taken. If this is your enterprise and you would like to claim ownership, please contact the current manager of this profile at owner@example.com." + expect(e).to_not be_valid + expect(e.errors[:name].first).to include I18n.t('enterprise_name_error', email: owner.email) end it "does not prohibit the saving of an enterprise with no name clash" do @@ -188,7 +186,7 @@ describe Enterprise do end describe "delegations" do - #subject { FactoryGirl.create(:distributor_enterprise, :address => FactoryGirl.create(:address)) } + #subject { FactoryBot.create(:distributor_enterprise, :address => FactoryBot.create(:address)) } it { should delegate(:latitude).to(:address) } it { should delegate(:longitude).to(:address) } @@ -553,16 +551,6 @@ describe Enterprise do end end - describe "supplied_and_active_products_on_hand" do - it "find only active products which are in stock" do - supplier = create(:supplier_enterprise) - inactive_product = create(:product, supplier: supplier, on_hand: 1, available_on: Date.tomorrow) - out_of_stock_product = create(:product, supplier: supplier, on_hand: 0, available_on: Date.yesterday) - p1 = create(:product, supplier: supplier, on_hand: 1, available_on: Date.yesterday) - supplier.supplied_and_active_products_on_hand.should == [p1] - end - end - describe "finding variants distributed by the enterprise" do it "finds master and other variants" do d = create(:distributor_enterprise) diff --git a/spec/models/feature_flags_spec.rb b/spec/models/feature_flags_spec.rb new file mode 100644 index 0000000000..3a2d06da30 --- /dev/null +++ b/spec/models/feature_flags_spec.rb @@ -0,0 +1,28 @@ +require 'spec_helper' + +describe FeatureFlags do + describe '.product_import_enabled?' do + let(:user) { build_stubbed(:user) } + let(:feature_flags) { described_class.new(user) } + + context 'when the user is superadmin' do + before do + allow(user).to receive(:has_spree_role?).with('admin') { true } + end + + it 'returns true' do + expect(feature_flags.product_import_enabled?).to eq(true) + end + end + + context 'when the user is not superadmin' do + before do + allow(user).to receive(:has_spree_role?).with('admin') { false } + end + + it 'returns false' do + expect(feature_flags.product_import_enabled?).to eq(false) + end + end + end +end diff --git a/spec/models/product_importer_spec.rb b/spec/models/product_importer_spec.rb index e51bf91cea..465b9809b0 100644 --- a/spec/models/product_importer_spec.rb +++ b/spec/models/product_importer_spec.rb @@ -1,33 +1,682 @@ require 'spec_helper' require 'open_food_network/permissions' -describe ProductImporter do +describe ProductImport::ProductImporter do include AuthenticationWorkflow let!(:admin) { create(:admin_user) } let!(:user) { create_enterprise_user } - let!(:enterprise) { create(:enterprise, owner: user, name: "Test Enterprise") } + let!(:user2) { create_enterprise_user } + let!(:user3) { create_enterprise_user } + let!(:enterprise) { create(:enterprise, owner: user, name: "User Enterprise") } + let!(:enterprise2) { create(:distributor_enterprise, owner: user2, name: "Another Enterprise") } + let!(:enterprise3) { create(:distributor_enterprise, owner: user3, name: "And Another Enterprise") } + let!(:relationship) { create(:enterprise_relationship, parent: enterprise, child: enterprise2, permissions_list: [:create_variant_overrides]) } + let!(:category) { create(:taxon, name: 'Vegetables') } + let!(:category2) { create(:taxon, name: 'Cake') } + let!(:tax_category) { create(:tax_category) } + let!(:tax_category2) { create(:tax_category) } + let!(:shipping_category) { create(:shipping_category) } + + let!(:product) { create(:simple_product, supplier: enterprise2, name: 'Hypothetical Cake') } + 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') } + let!(:product3) { create(:simple_product, supplier: enterprise, on_hand: '100', name: 'Sprouts', unit_value: '500') } + let!(:product4) { create(:simple_product, supplier: enterprise, on_hand: '100', name: 'Cabbage', unit_value: '500') } + let!(:product5) { create(:simple_product, supplier: enterprise2, on_hand: '100', name: 'Lettuce', unit_value: '500') } + 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') } + let!(:product7) { create(:simple_product, supplier: enterprise3, on_hand: '100', name: 'Tomato', unit_value: '500', variant_unit_scale: 1, variant_unit: 'weight') } + let!(:variant_override) { create(:variant_override, variant_id: product4.variants.first.id, hub: enterprise2, count_on_hand: 42) } + let!(:variant_override2) { create(:variant_override, variant_id: product5.variants.first.id, hub: enterprise, count_on_hand: 96) } + let(:permissions) { OpenFoodNetwork::Permissions.new(user) } describe "importing products from a spreadsheet" do + before do + csv_data = CSV.generate do |csv| + csv << ["name", "supplier", "category", "on_hand", "price", "units", "unit_type", "variant_unit_name", "on_demand"] + csv << ["Carrots", "User Enterprise", "Vegetables", "5", "3.20", "500", "g", "", ""] + csv << ["Potatoes", "User Enterprise", "Vegetables", "6", "6.50", "2", "kg", "", ""] + csv << ["Pea Soup", "User Enterprise", "Vegetables", "8", "5.50", "750", "ml", "", "0"] + csv << ["Salad", "User Enterprise", "Vegetables", "7", "4.50", "1", "", "bags", ""] + csv << ["Hot Cross Buns", "User Enterprise", "Cake", "7", "3.50", "1", "", "buns", "1"] + end + File.write('/tmp/test-m.csv', csv_data) + file = File.new('/tmp/test-m.csv') + settings = {enterprise.id.to_s => {'import_into' => 'product_list'}} + @importer = ProductImport::ProductImporter.new(file, admin, start: 1, end: 100, settings: settings) + end after { File.delete('/tmp/test-m.csv') } - it "validates the entries" do + it "returns the number of entries" do + expect(@importer.item_count).to eq(5) + end + + it "validates entries and returns the results as json" do + @importer.validate_entries + entries = JSON.parse(@importer.entries_json) + + expect(filter('valid', entries)).to eq 5 + expect(filter('invalid', entries)).to eq 0 + expect(filter('create_product', entries)).to eq 5 + expect(filter('update_product', entries)).to eq 0 + end + + it "saves the results and returns info on updated products" do + @importer.save_entries + + expect(@importer.products_created_count).to eq 5 + expect(@importer.updated_ids).to be_a(Array) + expect(@importer.updated_ids.count).to eq 5 + + carrots = Spree::Product.find_by_name('Carrots') + expect(carrots.supplier).to eq enterprise + expect(carrots.on_hand).to eq 5 + expect(carrots.price).to eq 3.20 + expect(carrots.unit_value).to eq 500 + expect(carrots.variant_unit).to eq 'weight' + expect(carrots.variant_unit_scale).to eq 1 + expect(carrots.on_demand).to_not eq true + expect(carrots.variants.first.import_date).to be_within(1.minute).of Time.zone.now + + potatoes = Spree::Product.find_by_name('Potatoes') + expect(potatoes.supplier).to eq enterprise + expect(potatoes.on_hand).to eq 6 + expect(potatoes.price).to eq 6.50 + expect(potatoes.unit_value).to eq 2000 + expect(potatoes.variant_unit).to eq 'weight' + expect(potatoes.variant_unit_scale).to eq 1000 + expect(potatoes.on_demand).to_not eq true + expect(potatoes.variants.first.import_date).to be_within(1.minute).of Time.zone.now + + pea_soup = Spree::Product.find_by_name('Pea Soup') + expect(pea_soup.supplier).to eq enterprise + expect(pea_soup.on_hand).to eq 8 + expect(pea_soup.price).to eq 5.50 + expect(pea_soup.unit_value).to eq 0.75 + expect(pea_soup.variant_unit).to eq 'volume' + expect(pea_soup.variant_unit_scale).to eq 0.001 + expect(pea_soup.on_demand).to_not eq true + expect(pea_soup.variants.first.import_date).to be_within(1.minute).of Time.zone.now + + salad = Spree::Product.find_by_name('Salad') + expect(salad.supplier).to eq enterprise + expect(salad.on_hand).to eq 7 + expect(salad.price).to eq 4.50 + expect(salad.unit_value).to eq 1 + expect(salad.variant_unit).to eq 'items' + expect(salad.variant_unit_scale).to eq nil + expect(salad.on_demand).to_not eq true + expect(salad.variants.first.import_date).to be_within(1.minute).of Time.zone.now + + buns = Spree::Product.find_by_name('Hot Cross Buns') + expect(buns.supplier).to eq enterprise + # buns.on_hand).to eq Infinity + expect(buns.price).to eq 3.50 + expect(buns.unit_value).to eq 1 + expect(buns.variant_unit).to eq 'items' + expect(buns.variant_unit_scale).to eq nil + expect(buns.on_demand).to eq true + expect(buns.variants.first.import_date).to be_within(1.minute).of Time.zone.now + end + end + + describe "when uploading a spreadsheet with some invalid entries" do + before do csv_data = CSV.generate do |csv| - csv << ["name", "supplier", "category", "on_hand", "price", "unit_value", "variant_unit", "variant_unit_scale"] - csv << ["Carrots", "Test Enterprise", "Vegetables", "5", "3.20", "500", "weight", "1"] - csv << ["Potatoes", "Test Enterprise", "Vegetables", "6", "6.50", "1000", "weight", "1000"] + csv << ["name", "supplier", "category", "on_hand", "price", "units", "unit_type"] + csv << ["Good Carrots", "User Enterprise", "Vegetables", "5", "3.20", "500", "g"] + csv << ["Bad Potatoes", "", "Vegetables", "6", "6.50", "1", ""] + end + File.write('/tmp/test-m.csv', csv_data) + file = File.new('/tmp/test-m.csv') + settings = {enterprise.id.to_s => {'import_into' => 'product_list'}} + @importer = ProductImport::ProductImporter.new(file, admin, start: 1, end: 100, settings: settings) + end + after { File.delete('/tmp/test-m.csv') } + + it "validates entries" do + @importer.validate_entries + entries = JSON.parse(@importer.entries_json) + + expect(filter('valid', entries)).to eq 1 + expect(filter('invalid', entries)).to eq 1 + expect(filter('create_product', entries)).to eq 1 + expect(filter('update_product', entries)).to eq 0 + end + + it "allows saving of the valid entries" do + @importer.save_entries + + expect(@importer.products_created_count).to eq 1 + expect(@importer.updated_ids).to be_a(Array) + expect(@importer.updated_ids.count).to eq 1 + + carrots = Spree::Product.find_by_name('Good Carrots') + expect(carrots.supplier).to eq enterprise + expect(carrots.on_hand).to eq 5 + expect(carrots.price).to eq 3.20 + expect(carrots.variants.first.import_date).to be_within(1.minute).of Time.zone.now + + expect(Spree::Product.find_by_name('Bad Potatoes')).to eq nil + end + end + + describe "adding new variants to existing products and updating exiting products" do + before do + csv_data = CSV.generate do |csv| + csv << ["name", "supplier", "category", "on_hand", "price", "units", "unit_type", "display_name"] + csv << ["Hypothetical Cake", "Another Enterprise", "Cake", "5", "5.50", "500", "g", "Preexisting Banana"] + csv << ["Hypothetical Cake", "Another Enterprise", "Cake", "6", "3.50", "500", "g", "Emergent Coffee"] + end + File.write('/tmp/test-m.csv', csv_data) + file = File.new('/tmp/test-m.csv') + settings = {enterprise2.id.to_s => {'import_into' => 'product_list'}} + @importer = ProductImport::ProductImporter.new(file, admin, start: 1, end: 100, settings: settings) + end + after { File.delete('/tmp/test-m.csv') } + + it "validates entries" do + @importer.validate_entries + entries = JSON.parse(@importer.entries_json) + + expect(filter('valid', entries)).to eq 2 + expect(filter('invalid', entries)).to eq 0 + expect(filter('create_product', entries)).to eq 1 + expect(filter('update_product', entries)).to eq 1 + end + + it "saves and updates" do + @importer.save_entries + + expect(@importer.products_created_count).to eq 1 + expect(@importer.products_updated_count).to eq 1 + expect(@importer.updated_ids).to be_a(Array) + expect(@importer.updated_ids.count).to eq 2 + + added_coffee = Spree::Variant.find_by_display_name('Emergent Coffee') + expect(added_coffee.product.name).to eq 'Hypothetical Cake' + expect(added_coffee.price).to eq 3.50 + expect(added_coffee.on_hand).to eq 6 + expect(added_coffee.import_date).to be_within(1.minute).of Time.zone.now + + updated_banana = Spree::Variant.find_by_display_name('Preexisting Banana') + expect(updated_banana.product.name).to eq 'Hypothetical Cake' + expect(updated_banana.price).to eq 5.50 + expect(updated_banana.on_hand).to eq 5 + expect(updated_banana.import_date).to be_within(1.minute).of Time.zone.now + end + end + + describe "adding new product and sub-variant at the same time" do + before do + csv_data = CSV.generate do |csv| + csv << ["name", "supplier", "category", "on_hand", "price", "units", "unit_type", "display_name"] + csv << ["Potatoes", "User Enterprise", "Vegetables", "5", "3.50", "500", "g", "Small Bag"] + csv << ["Potatoes", "User Enterprise", "Vegetables", "6", "5.50", "2", "kg", "Big Bag"] + end + File.write('/tmp/test-m.csv', csv_data) + file = File.new('/tmp/test-m.csv') + settings = {enterprise.id.to_s => {'import_into' => 'product_list'}} + @importer = ProductImport::ProductImporter.new(file, admin, start: 1, end: 100, settings: settings) + end + after { File.delete('/tmp/test-m.csv') } + + it "validates entries" do + @importer.validate_entries + entries = JSON.parse(@importer.entries_json) + + expect(filter('valid', entries)).to eq 2 + expect(filter('invalid', entries)).to eq 0 + expect(filter('create_product', entries)).to eq 2 + end + + it "saves and updates" do + @importer.save_entries + + expect(@importer.products_created_count).to eq 2 + expect(@importer.updated_ids).to be_a(Array) + expect(@importer.updated_ids.count).to eq 2 + + small_bag = Spree::Variant.find_by_display_name('Small Bag') + expect(small_bag.product.name).to eq 'Potatoes' + expect(small_bag.price).to eq 3.50 + expect(small_bag.on_hand).to eq 5 + + big_bag = Spree::Variant.find_by_display_name('Big Bag') + expect(big_bag.product.name).to eq 'Potatoes' + expect(big_bag.price).to eq 5.50 + expect(big_bag.on_hand).to eq 6 + end + end + + describe "updating various fields" do + before do + csv_data = CSV.generate do |csv| + csv << ["name", "supplier", "category", "on_hand", "price", "units", "unit_type", "on_demand"] + csv << ["Beetroot", "And Another Enterprise", "Vegetables", "5", "3.50", "500", "g", "0"] + csv << ["Tomato", "And Another Enterprise", "Vegetables", "6", "5.50", "500", "g", "1"] + end + File.write('/tmp/test-m.csv', csv_data) + file = File.new('/tmp/test-m.csv') + settings = {enterprise3.id.to_s => {'import_into' => 'product_list'}} + @importer = ProductImport::ProductImporter.new(file, admin, start: 1, end: 100, settings: settings) + end + after { File.delete('/tmp/test-m.csv') } + + it "validates entries" do + @importer.validate_entries + entries = JSON.parse(@importer.entries_json) + + expect(filter('valid', entries)).to eq 2 + expect(filter('invalid', entries)).to eq 0 + expect(filter('create_product', entries)).to eq 0 + expect(filter('update_product', entries)).to eq 2 + end + + it "saves and updates" do + @importer.save_entries + + expect(@importer.products_created_count).to eq 0 + expect(@importer.products_updated_count).to eq 2 + expect(@importer.updated_ids).to be_a(Array) + expect(@importer.updated_ids.count).to eq 2 + + beetroot = Spree::Product.find_by_name('Beetroot').variants.first + expect(beetroot.price).to eq 3.50 + expect(beetroot.on_demand).to_not eq true + + tomato = Spree::Product.find_by_name('Tomato').variants.first + expect(tomato.price).to eq 5.50 + expect(tomato.on_demand).to eq true + end + end + + describe "importing items into inventory" do + before do + csv_data = CSV.generate do |csv| + csv << ["name", "supplier", "producer", "category", "on_hand", "price", "units"] + csv << ["Beans", "Another Enterprise", "User Enterprise", "Vegetables", "5", "3.20", "500"] + csv << ["Sprouts", "Another Enterprise", "User Enterprise", "Vegetables", "6", "6.50", "500"] + csv << ["Cabbage", "Another Enterprise", "User Enterprise", "Vegetables", "2001", "1.50", "500"] + end + File.write('/tmp/test-m.csv', csv_data) + file = File.new('/tmp/test-m.csv') + settings = {enterprise2.id.to_s => {'import_into' => 'inventories'}} + @importer = ProductImport::ProductImporter.new(file, admin, start: 1, end: 100, settings: settings) + end + after { File.delete('/tmp/test-m.csv') } + + 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 + 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 + + describe "importing items into inventory and product list simultaneously" do + before do + csv_data = CSV.generate do |csv| + csv << ["name", "supplier", "producer", "category", "on_hand", "price", "units", "unit_type"] + csv << ["Beans", "Another Enterprise", "User Enterprise", "Vegetables", "5", "3.20", "500", ""] + csv << ["Sprouts", "Another Enterprise", "User Enterprise", "Vegetables", "6", "6.50", "500", ""] + csv << ["Garbanzos", "User Enterprise", "", "Vegetables", "2001", "1.50", "500", "g"] + end + File.write('/tmp/test-m.csv', csv_data) + file = File.new('/tmp/test-m.csv') + settings = {enterprise.id.to_s => {'import_into' => 'product_list'}, enterprise2.id.to_s => {'import_into' => 'inventories'}} + @importer = ProductImport::ProductImporter.new(file, admin, start: 1, end: 100, settings: settings) + end + after { File.delete('/tmp/test-m.csv') } + + 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('create_product', entries)).to eq 1 + end + + it "saves and updates inventory" do + @importer.save_entries + + expect(@importer.inventory_created_count).to eq 2 + expect(@importer.products_created_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 + garbanzos = Spree::Product.where(name: "Garbanzos").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(garbanzos.price)).to eq 1.50 + expect(garbanzos.count_on_hand).to eq 2001 + end + end + + describe "handling enterprise permissions" do + after { File.delete('/tmp/test-m.csv') } + + it "only allows product import into enterprises the user is permitted to manage" do + csv_data = CSV.generate do |csv| + csv << ["name", "supplier", "category", "on_hand", "price", "units", "unit_type"] + csv << ["My Carrots", "User Enterprise", "Vegetables", "5", "3.20", "500", "g"] + csv << ["Your Potatoes", "Another Enterprise", "Vegetables", "6", "6.50", "1", "kg"] + end + File.write('/tmp/test-m.csv', csv_data) + file = File.new('/tmp/test-m.csv') + settings = {enterprise.id.to_s => {'import_into' => 'product_list'}, enterprise2.id.to_s => {'import_into' => 'product_list'}} + @importer = ProductImport::ProductImporter.new(file, user, start: 1, end: 100, settings: settings) + + @importer.validate_entries + entries = JSON.parse(@importer.entries_json) + + expect(filter('valid', entries)).to eq 1 + expect(filter('invalid', entries)).to eq 1 + expect(filter('create_product', entries)).to eq 1 + + @importer.save_entries + + expect(@importer.products_created_count).to eq 1 + expect(@importer.updated_ids).to be_a(Array) + expect(@importer.updated_ids.count).to eq 1 + + expect(Spree::Product.find_by_name('My Carrots')).to be_a Spree::Product + expect(Spree::Product.find_by_name('Your Potatoes')).to eq nil + end + + it "allows creating inventories for producers that a user's hub has permission for" do + csv_data = CSV.generate do |csv| + csv << ["name", "producer", "supplier", "category", "on_hand", "price", "units"] + csv << ["Beans", "User Enterprise", "Another Enterprise", "Vegetables", "777", "3.20", "500"] + end + File.write('/tmp/test-m.csv', csv_data) + file = File.new('/tmp/test-m.csv') + settings = {enterprise2.id.to_s => {'import_into' => 'inventories'}} + @importer = ProductImport::ProductImporter.new(file, user2, start: 1, end: 100, settings: settings) + + @importer.validate_entries + entries = JSON.parse(@importer.entries_json) + + expect(filter('valid', entries)).to eq 1 + expect(filter('invalid', entries)).to eq 0 + expect(filter('create_inventory', entries)).to eq 1 + + @importer.save_entries + + expect(@importer.inventory_created_count).to eq 1 + expect(@importer.updated_ids).to be_a(Array) + expect(@importer.updated_ids.count).to eq 1 + + beans = VariantOverride.where(variant_id: product2.variants.first.id, hub_id: enterprise2.id).first + expect(beans.count_on_hand).to eq 777 + end + + it "does not allow creating inventories for producers that a user's hubs don't have permission for" do + csv_data = CSV.generate do |csv| + csv << ["name", "supplier", "category", "on_hand", "price", "units"] + csv << ["Beans", "User Enterprise", "Vegetables", "5", "3.20", "500"] + csv << ["Sprouts", "User Enterprise", "Vegetables", "6", "6.50", "500"] + end + File.write('/tmp/test-m.csv', csv_data) + file = File.new('/tmp/test-m.csv') + settings = {enterprise.id.to_s => {'import_into' => 'inventories'}} + @importer = ProductImport::ProductImporter.new(file, user2, start: 1, end: 100, settings: settings) + + @importer.validate_entries + entries = JSON.parse(@importer.entries_json) + + expect(filter('valid', entries)).to eq 0 + expect(filter('invalid', entries)).to eq 2 + expect(filter('create_inventory', entries)).to eq 0 + + @importer.save_entries + + expect(@importer.inventory_created_count).to eq 0 + expect(@importer.updated_ids).to be_a(Array) + expect(@importer.updated_ids.count).to eq 0 + end + end + + describe "applying settings and defaults on import" do + after { File.delete('/tmp/test-m.csv') } + + it "can reset all products for an enterprise that are not present in the uploaded file to zero stock" do + csv_data = CSV.generate do |csv| + csv << ["name", "supplier", "category", "on_hand", "price", "units", "unit_type"] + csv << ["Carrots", "User Enterprise", "Vegetables", "5", "3.20", "500", "g"] + csv << ["Beans", "User Enterprise", "Vegetables", "6", "6.50", "500", "g"] + end + File.write('/tmp/test-m.csv', csv_data) + file = File.new('/tmp/test-m.csv') + settings = {enterprise.id.to_s => {'import_into' => 'product_list', 'reset_all_absent' => true}} + @importer = ProductImport::ProductImporter.new(file, admin, start: 1, end: 100, settings: settings) + + @importer.validate_entries + entries = JSON.parse(@importer.entries_json) + + expect(filter('valid', entries)).to eq 2 + expect(filter('invalid', entries)).to eq 0 + expect(filter('create_product', entries)).to eq 1 + expect(filter('update_product', entries)).to eq 1 + + @importer.save_entries + + expect(@importer.products_created_count).to eq 1 + expect(@importer.products_updated_count).to eq 1 + expect(@importer.updated_ids).to be_a(Array) + expect(@importer.updated_ids.count).to eq 2 + + @importer.reset_absent(@importer.updated_ids) + + expect(@importer.products_reset_count).to eq 2 + + expect(Spree::Product.find_by_name('Carrots').on_hand).to eq 5 # Present in file, added + expect(Spree::Product.find_by_name('Beans').on_hand).to eq 6 # Present in file, updated + expect(Spree::Product.find_by_name('Sprouts').on_hand).to eq 0 # In enterprise, not in file + expect(Spree::Product.find_by_name('Cabbage').on_hand).to eq 0 # In enterprise, not in file + expect(Spree::Product.find_by_name('Lettuce').on_hand).to eq 100 # In different enterprise; unchanged + end + + it "can reset all inventory items for an enterprise that are not present in the uploaded file to zero stock" do + csv_data = CSV.generate do |csv| + csv << ["name", "supplier", "producer", "category", "on_hand", "price", "units"] + csv << ["Beans", "Another Enterprise", "User Enterprise", "Vegetables", "6", "3.20", "500"] + csv << ["Sprouts", "Another Enterprise", "User Enterprise", "Vegetables", "7", "6.50", "500"] + end + File.write('/tmp/test-m.csv', csv_data) + file = File.new('/tmp/test-m.csv') + settings = {enterprise2.id.to_s => {'import_into' => 'inventories', 'reset_all_absent' => true}} + @importer = ProductImport::ProductImporter.new(file, admin, start: 1, end: 100, settings: settings) + + @importer.validate_entries + entries = JSON.parse(@importer.entries_json) + + expect(filter('valid', entries)).to eq 2 + expect(filter('invalid', entries)).to eq 0 + expect(filter('create_inventory', entries)).to eq 2 + + @importer.save_entries + + expect(@importer.inventory_created_count).to eq 2 + expect(@importer.updated_ids).to be_a(Array) + expect(@importer.updated_ids.count).to eq 2 + + @importer.reset_absent(@importer.updated_ids) + + # expect(@importer.products_reset_count).to eq 1 + + beans = VariantOverride.where(variant_id: product2.variants.first.id, hub_id: enterprise2.id).first + sprouts = VariantOverride.where(variant_id: product3.variants.first.id, hub_id: enterprise2.id).first + cabbage = VariantOverride.where(variant_id: product4.variants.first.id, hub_id: enterprise2.id).first + lettuce = VariantOverride.where(variant_id: product5.variants.first.id, hub_id: enterprise.id).first + + expect(beans.count_on_hand).to eq 6 # Present in file, created + expect(sprouts.count_on_hand).to eq 7 # Present in file, created + expect(cabbage.count_on_hand).to eq 0 # In enterprise, not in file (reset) + expect(lettuce.count_on_hand).to eq 96 # In different enterprise; unchanged + end + + it "can overwrite fields with selected defaults when importing to product list" do + csv_data = CSV.generate do |csv| + csv << ["name", "supplier", "category", "on_hand", "price", "units", "unit_type", "tax_category_id", "available_on"] + csv << ["Carrots", "User Enterprise", "Vegetables", "5", "3.20", "500", "g", tax_category.id, ""] + csv << ["Potatoes", "User Enterprise", "Vegetables", "6", "6.50", "1", "kg", "", ""] end File.write('/tmp/test-m.csv', csv_data) file = File.new('/tmp/test-m.csv') - importer = ProductImporter.new(file, permissions.editable_enterprises) + settings = {enterprise.id.to_s => { + 'import_into' => 'product_list', + 'defaults' => { + 'on_hand' => { + 'active' => true, + 'mode' => 'overwrite_all', + 'value' => '9000' + }, + 'tax_category_id' => { + 'active' => true, + 'mode' => 'overwrite_empty', + 'value' => tax_category2.id + }, + 'shipping_category_id' => { + 'active' => true, + 'mode' => 'overwrite_all', + 'value' => shipping_category.id + }, + 'available_on' => { + 'active' => true, + 'mode' => 'overwrite_all', + 'value' => '2020-01-01' + } + } + }} - expect(importer.valid_count).to eq(2) - expect(importer.invalid_count).to eq(0) + @importer = ProductImport::ProductImporter.new(file, admin, start: 1, end: 100, settings: settings) + + @importer.validate_entries + entries = JSON.parse(@importer.entries_json) + + expect(filter('valid', entries)).to eq 2 + expect(filter('invalid', entries)).to eq 0 + expect(filter('create_product', entries)).to eq 2 + + @importer.save_entries + + expect(@importer.products_created_count).to eq 2 + expect(@importer.updated_ids).to be_a(Array) + expect(@importer.updated_ids.count).to eq 2 + + carrots = Spree::Product.find_by_name('Carrots') + expect(carrots.on_hand).to eq 9000 + expect(carrots.tax_category_id).to eq tax_category.id + expect(carrots.shipping_category_id).to eq shipping_category.id + expect(carrots.available_on).to be_within(1.day).of(Time.zone.local(2020, 1, 1)) + + potatoes = Spree::Product.find_by_name('Potatoes') + expect(potatoes.on_hand).to eq 9000 + expect(potatoes.tax_category_id).to eq tax_category2.id + expect(potatoes.shipping_category_id).to eq shipping_category.id + expect(potatoes.available_on).to be_within(1.day).of(Time.zone.local(2020, 1, 1)) + end + + it "can overwrite fields with selected defaults when importing to inventory" do + csv_data = CSV.generate do |csv| + csv << ["name", "producer", "supplier", "category", "on_hand", "price", "units"] + csv << ["Beans", "User Enterprise", "Another Enterprise", "Vegetables", "", "3.20", "500"] + csv << ["Sprouts", "User Enterprise", "Another Enterprise", "Vegetables", "7", "6.50", "500"] + csv << ["Cabbage", "User Enterprise", "Another Enterprise", "Vegetables", "", "1.50", "500"] + end + File.write('/tmp/test-m.csv', csv_data) + file = File.new('/tmp/test-m.csv') + + import_settings = {enterprise2.id.to_s => { + 'import_into' => 'inventories', + 'defaults' => { + 'count_on_hand' => { + 'active' => true, + 'mode' => 'overwrite_empty', + 'value' => '9000' + } + } + }} + + @importer = ProductImport::ProductImporter.new(file, admin, start: 1, end: 100, import_into: 'inventories', settings: import_settings) + + @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 + + @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(beans_override.count_on_hand).to eq 9000 + expect(sprouts_override.count_on_hand).to eq 7 + expect(cabbage_override.count_on_hand).to eq 9000 end end - - # Test handling of filetypes +end + +private + +def filter(type, entries) + valid_count = 0 + entries.each do |_line_number, entry| + validates_as = entry['validates_as'] + + valid_count += 1 if type == 'valid' && (validates_as != '') + valid_count += 1 if type == 'invalid' && (validates_as == '') + valid_count += 1 if type == 'create_product' && (validates_as == 'new_product' || validates_as == 'new_variant') + valid_count += 1 if type == 'update_product' && validates_as == 'existing_variant' + valid_count += 1 if type == 'create_inventory' && validates_as == 'new_inventory_item' + valid_count += 1 if type == 'update_inventory' && validates_as == 'existing_inventory_item' + end + valid_count end diff --git a/spec/models/spree/ability_spec.rb b/spec/models/spree/ability_spec.rb index 71e0c52eb1..7882665618 100644 --- a/spec/models/spree/ability_spec.rb +++ b/spec/models/spree/ability_spec.rb @@ -147,18 +147,18 @@ module Spree let(:order) { create(:order) } it "should be able to read/write their enterprises' products and variants" do - should have_ability([:admin, :read, :update, :product_distributions, :bulk_edit, :bulk_update, :clone, :destroy], for: p1) + should have_ability([:admin, :read, :update, :product_distributions, :bulk_update, :clone, :destroy], for: p1) should have_ability([:admin, :index, :read, :edit, :update, :search, :destroy, :delete], for: p1.master) end it "should be able to read/write related enterprises' products and variants with manage_products permission" do er_ps - should have_ability([:admin, :read, :update, :product_distributions, :bulk_edit, :bulk_update, :clone, :destroy], for: p_related) + should have_ability([:admin, :read, :update, :product_distributions, :bulk_update, :clone, :destroy], for: p_related) should have_ability([:admin, :index, :read, :edit, :update, :search, :destroy, :delete], for: p_related.master) end it "should not be able to read/write other enterprises' products and variants" do - should_not have_ability([:admin, :read, :update, :product_distributions, :bulk_edit, :bulk_update, :clone, :destroy], for: p2) + should_not have_ability([:admin, :read, :update, :product_distributions, :bulk_update, :clone, :destroy], for: p2) should_not have_ability([:admin, :index, :read, :edit, :update, :search, :destroy], for: p2.master) end diff --git a/spec/models/spree/addresses_spec.rb b/spec/models/spree/addresses_spec.rb index 5fc9d442b6..f453218610 100644 --- a/spec/models/spree/addresses_spec.rb +++ b/spec/models/spree/addresses_spec.rb @@ -10,7 +10,7 @@ describe Spree::Address do end describe "geocode address" do - let(:address) { FactoryGirl.build(:address) } + let(:address) { FactoryBot.build(:address) } it "should include address1, address2, zipcode, city, state and country" do address.geocode_address.should include(address.address1) @@ -30,7 +30,7 @@ describe Spree::Address do end describe "full address" do - let(:address) { FactoryGirl.build(:address) } + let(:address) { FactoryBot.build(:address) } it "should include address1, address2, zipcode, city and state" do address.full_address.should include(address.address1) diff --git a/spec/models/spree/credit_card_spec.rb b/spec/models/spree/credit_card_spec.rb new file mode 100644 index 0000000000..9d246944f6 --- /dev/null +++ b/spec/models/spree/credit_card_spec.rb @@ -0,0 +1,80 @@ +require 'spec_helper' + +module Spree + describe CreditCard do + describe "setting default credit card for a user" do + let(:user) { create(:user) } + + context "when a card is already set as the default" do + let!(:card1) { create(:credit_card, user: user, is_default: true) } + + context "and I create a new card" do + let(:attrs) { { user: user } } + let!(:card2) { create(:credit_card, attrs) } + + context "without specifying it as the default" do + it "keeps the existing default" do + expect(card1.reload.is_default).to be true + expect(card2.reload.is_default).to be false + end + end + + context "and I specify it as the default" do + let(:attrs) { { user: user, is_default: true } } + + it "switches the default to the new card" do + expect(card1.reload.is_default).to be false + expect(card2.reload.is_default).to be true + end + end + end + + context "and I update another card" do + let(:attrs) { { user: user } } + let!(:card2) { create(:credit_card, user: user) } + + before do + card2.update_attributes!(attrs) + end + + context "without specifying it as the default" do + it "keeps the existing default" do + expect(card1.reload.is_default).to be true + expect(card2.reload.is_default).to be false + end + end + + context "and I specify it as the default" do + let(:attrs) { { user: user, is_default: true } } + + it "switches the default to the updated card" do + expect(card1.reload.is_default).to be false + expect(card2.reload.is_default).to be true + end + end + end + end + + context "when no card is currently set as the default for a user" do + context "and I create a new card" do + let(:attrs) { { user: user } } + let!(:card1) { create(:credit_card, attrs) } + + context "without specifying it as the default" do + it "sets it as the default anyway" do + expect(card1.reload.is_default).to be true + end + end + + context "and I specify it as the default" do + let(:attrs) { { user: user, is_default: true } } + + it "sets it as the default" do + expect(card1.reload.is_default).to be true + end + end + end + end + end + end +end diff --git a/spec/models/spree/order_spec.rb b/spec/models/spree/order_spec.rb index 7680023463..345cbb661f 100644 --- a/spec/models/spree/order_spec.rb +++ b/spec/models/spree/order_spec.rb @@ -459,32 +459,32 @@ describe Spree::Order do context "validating distributor changes" do it "checks that a distributor is available when changing" do set_feature_toggle :order_cycles, false - order_enterprise = FactoryGirl.create(:enterprise, id: 1, :name => "Order Enterprise") + order_enterprise = FactoryBot.create(:enterprise, id: 1, :name => "Order Enterprise") subject.distributor = order_enterprise - product1 = FactoryGirl.create(:product) - product2 = FactoryGirl.create(:product) - product3 = FactoryGirl.create(:product) - variant11 = FactoryGirl.create(:variant, product: product1) - variant12 = FactoryGirl.create(:variant, product: product1) - variant21 = FactoryGirl.create(:variant, product: product2) - variant31 = FactoryGirl.create(:variant, product: product3) - variant32 = FactoryGirl.create(:variant, product: product3) + product1 = FactoryBot.create(:product) + product2 = FactoryBot.create(:product) + product3 = FactoryBot.create(:product) + variant11 = FactoryBot.create(:variant, product: product1) + variant12 = FactoryBot.create(:variant, product: product1) + variant21 = FactoryBot.create(:variant, product: product2) + variant31 = FactoryBot.create(:variant, product: product3) + variant32 = FactoryBot.create(:variant, product: product3) # Product Distributions # Order Enterprise sells product 1 and product 3 - FactoryGirl.create(:product_distribution, product: product1, distributor: order_enterprise) - FactoryGirl.create(:product_distribution, product: product3, distributor: order_enterprise) + FactoryBot.create(:product_distribution, product: product1, distributor: order_enterprise) + FactoryBot.create(:product_distribution, product: product3, distributor: order_enterprise) # Build the current order - line_item1 = FactoryGirl.create(:line_item, order: subject, variant: variant11) - line_item2 = FactoryGirl.create(:line_item, order: subject, variant: variant12) - line_item3 = FactoryGirl.create(:line_item, order: subject, variant: variant31) + line_item1 = FactoryBot.create(:line_item, order: subject, variant: variant11) + line_item2 = FactoryBot.create(:line_item, order: subject, variant: variant12) + line_item3 = FactoryBot.create(:line_item, order: subject, variant: variant31) subject.reload subject.line_items = [line_item1,line_item2,line_item3] - test_enterprise = FactoryGirl.create(:enterprise, id: 2, :name => "Test Enterprise") + test_enterprise = FactoryBot.create(:enterprise, id: 2, :name => "Test Enterprise") # Test Enterprise sells only product 1 - FactoryGirl.create(:product_distribution, product: product1, distributor: test_enterprise) + FactoryBot.create(:product_distribution, product: product1, distributor: test_enterprise) subject.distributor = test_enterprise subject.should_not be_valid @@ -502,7 +502,7 @@ describe Spree::Order do end it "finds only orders not in specified state" do - o = FactoryGirl.create(:completed_order_with_totals) + o = FactoryBot.create(:completed_order_with_totals) o.cancel! Spree::Order.not_state(:canceled).should_not include o end @@ -673,6 +673,28 @@ describe Spree::Order do end end + describe "when a guest order is placed with a registered email" do + let(:order) { create(:order_with_totals_and_distribution, user: nil) } + let(:payment_method) { create(:payment_method, distributors: [order.distributor]) } + let(:shipping_method) { create(:shipping_method, distributors: [order.distributor]) } + let(:user) { create(:user, email: 'registered@email.com') } + + before do + order.bill_address = create(:address) + order.ship_address = create(:address) + order.shipping_method = shipping_method + order.email = user.email + order.user = nil + order.state = 'cart' + end + + it "returns a validation error" do + expect{order.next}.to change(order.errors, :count).from(0).to(1) + expect(order.errors.messages[:base]).to eq [ I18n.t('devise.failure.already_registered') ] + expect(order.state).to eq 'cart' + end + end + describe "a completed order with shipping and transaction fees" do let(:distributor) { create(:distributor_enterprise, charges_sales_tax: true, allow_order_changes: true) } let(:order) { create(:completed_order_with_fees, distributor: distributor, shipping_fee: shipping_fee, payment_fee: payment_fee) } diff --git a/spec/requests/embedded_shopfronts_headers_spec.rb b/spec/requests/embedded_shopfronts_headers_spec.rb index 8056946f23..9d2c1c523e 100644 --- a/spec/requests/embedded_shopfronts_headers_spec.rb +++ b/spec/requests/embedded_shopfronts_headers_spec.rb @@ -44,7 +44,7 @@ describe "setting response headers for embedded shopfronts", type: :request do context "with a valid whitelist" do before do Spree::Config[:embedded_shopfronts_whitelist] = "example.com external-site.com" - allow_any_instance_of(ActionDispatch::Request).to receive(:referer).and_return('http://www.external-site.com/shop?embedded_shopfront=true') + allow_any_instance_of(ActionDispatch::Request).to receive(:referer).and_return('http://external-site.com/shop?embedded_shopfront=true') end it "allows iframes on certain pages when enabled in configuration" do @@ -61,5 +61,20 @@ describe "setting response headers for embedded shopfronts", type: :request do expect(response.headers['Content-Security-Policy']).to eq "frame-ancestors 'none'" end end + + context "with www prefix" do + before do + Spree::Config[:embedded_shopfronts_whitelist] = "example.com external-site.com" + allow_any_instance_of(ActionDispatch::Request).to receive(:referer).and_return('http://www.external-site.com/shop?embedded_shopfront=true') + end + + it "matches the URL structure in the header" do + get shops_path + + expect(response.status).to be 200 + expect(response.headers['X-Frame-Options']).to be_nil + expect(response.headers['Content-Security-Policy']).to eq "frame-ancestors www.external-site.com" + end + end end end diff --git a/spec/services/order_cycle_form_spec.rb b/spec/services/order_cycle_form_spec.rb new file mode 100644 index 0000000000..476adb3e39 --- /dev/null +++ b/spec/services/order_cycle_form_spec.rb @@ -0,0 +1,140 @@ +describe OrderCycleForm do + describe "save" do + describe "creating a new order cycle from params" do + let(:shop) { create(:enterprise) } + let(:order_cycle) { OrderCycle.new } + let(:form) { OrderCycleForm.new(order_cycle, params, shop.owner) } + + context "when creation is successful" do + let(:params) { { order_cycle: { name: "Test Order Cycle", coordinator_id: shop.id } } } + + it "returns true" do + expect do + expect(form.save).to be true + end.to change(OrderCycle, :count).by(1) + end + end + + context "when creation fails" do + let(:params) { { order_cycle: { name: "Test Order Cycle" } } } + + it "returns false" do + expect do + expect(form.save).to be false + end.to_not change(OrderCycle, :count) + end + end + end + + describe "updating an existing order cycle from params" do + let(:shop) { create(:enterprise) } + let(:order_cycle) { create(:simple_order_cycle, name: "Old Name") } + let(:form) { OrderCycleForm.new(order_cycle, params, shop.owner) } + + context "when update is successful" do + let(:params) { { order_cycle: { name: "Test Order Cycle", coordinator_id: shop.id } } } + + it "returns true" do + expect do + expect(form.save).to be true + end.to change(order_cycle.reload, :name).to("Test Order Cycle") + end + end + + context "when updating fails" do + let(:params) { { order_cycle: { name: nil } } } + + it "returns false" do + expect do + expect(form.save).to be false + end.to_not change{ order_cycle.reload.name } + end + end + end + end + + describe "updating schedules" do + let(:user) { create(:user, enterprise_limit: 10) } + let!(:managed_coordinator) { create(:enterprise, owner: user) } + let!(:managed_enterprise) { create(:enterprise, owner: user) } + let!(:coordinated_order_cycle) { create(:simple_order_cycle, coordinator: managed_coordinator ) } + let!(:coordinated_order_cycle2) { create(:simple_order_cycle, coordinator: managed_enterprise ) } + let!(:uncoordinated_order_cycle) { create(:simple_order_cycle, coordinator: create(:enterprise) ) } + let!(:coordinated_schedule) { create(:schedule, order_cycles: [coordinated_order_cycle] ) } + let!(:coordinated_schedule2) { create(:schedule, order_cycles: [coordinated_order_cycle2] ) } + let!(:uncoordinated_schedule) { create(:schedule, order_cycles: [uncoordinated_order_cycle] ) } + + context "where I manage the order_cycle's coordinator" do + let(:form) { OrderCycleForm.new(coordinated_order_cycle, params, user) } + let(:syncer_mock) { instance_double(OpenFoodNetwork::ProxyOrderSyncer, sync!: true) } + + before do + allow(OpenFoodNetwork::ProxyOrderSyncer).to receive(:new) { syncer_mock } + end + + context "and I add an schedule that I own, and remove another that I own" do + let(:params) { { order_cycle: { schedule_ids: [coordinated_schedule2.id] } } } + + it "associates the order cycle to the schedule" do + expect(form.save).to be true + expect(coordinated_order_cycle.reload.schedules).to include coordinated_schedule2 + expect(coordinated_order_cycle.reload.schedules).to_not include coordinated_schedule + expect(syncer_mock).to have_received(:sync!) + end + end + + context "and I add a schedule that I don't own" do + let(:params) { { order_cycle: { schedule_ids: [coordinated_schedule.id, uncoordinated_schedule.id] } } } + + it "ignores the schedule that I don't own" do + expect(form.save).to be true + expect(coordinated_order_cycle.reload.schedules).to include coordinated_schedule + expect(coordinated_order_cycle.reload.schedules).to_not include uncoordinated_schedule + expect(syncer_mock).to_not have_received(:sync!) + end + end + + context "when I make no changes to the schedule ids" do + let(:params) { { order_cycle: { schedule_ids: [coordinated_schedule.id] } } } + + it "ignores the schedule that I don't own" do + expect(form.save).to be true + expect(coordinated_order_cycle.reload.schedules).to include coordinated_schedule + expect(syncer_mock).to_not have_received(:sync!) + end + end + end + end + + describe "updating exchanges" do + let(:user) { instance_double(Spree::User) } + let(:order_cycle) { create(:simple_order_cycle) } + let(:form_applicator_mock) { instance_double(OpenFoodNetwork::OrderCycleFormApplicator) } + let(:form) { OrderCycleForm.new(order_cycle, params, user) } + let(:params) { { order_cycle: { name: 'Some new name' } } } + + before do + allow(OpenFoodNetwork::OrderCycleFormApplicator).to receive(:new) { form_applicator_mock } + allow(form_applicator_mock).to receive(:go!) + end + + context "when exchange params are provided" do + let(:exchange_params) { { incoming_exchanges: [], outgoing_exchanges: [] } } + before { params[:order_cycle].merge!(exchange_params) } + + it "runs the OrderCycleFormApplicator, and saves other changes" do + expect(form.save).to be true + expect(form_applicator_mock).to have_received(:go!) + expect(order_cycle.name).to eq 'Some new name' + end + end + + context "when no exchange params are provided" do + it "does not run the OrderCycleFormApplicator, but saves other changes" do + expect(form.save).to be true + expect(form_applicator_mock).to_not have_received(:go!) + expect(order_cycle.name).to eq 'Some new name' + end + end + end +end diff --git a/spec/services/subscriptions_count_spec.rb b/spec/services/subscriptions_count_spec.rb new file mode 100644 index 0000000000..2446bcc8bf --- /dev/null +++ b/spec/services/subscriptions_count_spec.rb @@ -0,0 +1,34 @@ +describe SubscriptionsCount do + let(:oc1) { create(:simple_order_cycle) } + let(:oc2) { create(:simple_order_cycle) } + let(:subscriptions_count) { SubscriptionsCount.new(order_cycles) } + + describe "#for" do + context "when the collection has not been set" do + let(:order_cycles) { nil } + it "returns 0" do + expect(subscriptions_count.for(oc1.id)).to eq 0 + end + end + + context "when the collection has been set" do + let(:order_cycles) { OrderCycle.where(id: [oc1]) } + let!(:po1) { create(:proxy_order, order_cycle: oc1) } + let!(:po2) { create(:proxy_order, order_cycle: oc1) } + let!(:po3) { create(:proxy_order, order_cycle: oc2) } + + context "but the requested id is not present in the list of order cycles provided" do + it "returns 0" do + # Note that po3 applies to oc2, but oc2 in not in the collection + expect(subscriptions_count.for(oc2.id)).to eq 0 + end + end + + context "and the requested id is present in the list of order cycles provided" do + it "returns a count of active proxy orders associated with the requested order cycle" do + expect(subscriptions_count.for(oc1.id)).to eq 2 + end + end + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 4d6410ec11..fe9a64de97 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -36,7 +36,7 @@ require 'capybara/poltergeist' Capybara.javascript_driver = :poltergeist Capybara.register_driver :poltergeist do |app| - options = {phantomjs_options: ['--load-images=no'], window_size: [1280, 3600], timeout: 2.minutes} + options = {phantomjs_options: ['--load-images=no', '--ssl-protocol=any'], window_size: [1280, 3600], timeout: 2.minutes} # Extend poltergeist's timeout to allow ample time to use pry in browser thread #options.merge! {timeout: 5.minutes} # Enable the remote inspector: Use page.driver.debug to open a remote debugger in chrome @@ -140,9 +140,9 @@ RSpec.configure do |config| config.include OpenFoodNetwork::DelayedJobHelper config.include OpenFoodNetwork::PerformanceHelper - # FactoryGirl - require 'factory_girl_rails' - config.include FactoryGirl::Syntax::Methods + # FactoryBot + require 'factory_bot_rails' + config.include FactoryBot::Syntax::Methods config.include Paperclip::Shoulda::Matchers diff --git a/spec/support/controller_helper.rb b/spec/support/controller_helper.rb index de0720283a..f49907dbe0 100644 --- a/spec/support/controller_helper.rb +++ b/spec/support/controller_helper.rb @@ -7,7 +7,7 @@ module OpenFoodNetwork user end - controller.stub spree_current_user: @admin_user + allow(controller).to receive_messages(spree_current_user: @admin_user) end def login_as_enterprise_user(enterprises) @@ -20,7 +20,7 @@ module OpenFoodNetwork user end - controller.stub spree_current_user: @enterprise_user + allow(controller).to receive_messages(spree_current_user: @enterprise_user) end end end diff --git a/spec/support/spree/init.rb b/spec/support/spree/init.rb index 0007680de7..dacc2d44dc 100644 --- a/spec/support/spree/init.rb +++ b/spec/support/spree/init.rb @@ -5,6 +5,6 @@ ProductDistribution.class_eval do before_validation :init_enterprise_fee def init_enterprise_fee - self.enterprise_fee ||= EnterpriseFee.where(enterprise_id: distributor).first || FactoryGirl.create(:enterprise_fee, enterprise_id: distributor) + self.enterprise_fee ||= EnterpriseFee.where(enterprise_id: distributor).first || FactoryBot.create(:enterprise_fee, enterprise_id: distributor) end end diff --git a/spec/support/views/group_iframe_test.html b/spec/support/views/group_iframe_test.html new file mode 100644 index 0000000000..dcb4abbceb --- /dev/null +++ b/spec/support/views/group_iframe_test.html @@ -0,0 +1,8 @@ + + + + + + + +