mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-01-12 18:36:49 +00:00
Compare commits
173 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fc9f61ecf8 | ||
|
|
07d4528276 | ||
|
|
4ace780431 | ||
|
|
51df8de64f | ||
|
|
d4a5829858 | ||
|
|
ff5fe66994 | ||
|
|
37e50a68e4 | ||
|
|
a72c662b97 | ||
|
|
ff2db0c5f8 | ||
|
|
e9c60a33b9 | ||
|
|
8e059d3c69 | ||
|
|
806ba94a2e | ||
|
|
4bec583bff | ||
|
|
90256f9c28 | ||
|
|
eb284c1742 | ||
|
|
b614e17f48 | ||
|
|
5259eaae5f | ||
|
|
b0ad0fccfa | ||
|
|
2a83ad8689 | ||
|
|
c127110192 | ||
|
|
0470725112 | ||
|
|
0623bab084 | ||
|
|
4a0df684c7 | ||
|
|
7dccb5ba90 | ||
|
|
5a4be24df0 | ||
|
|
5cb5967977 | ||
|
|
aeb8d30dae | ||
|
|
1822fd97a6 | ||
|
|
4ff3e9fe10 | ||
|
|
a63994440d | ||
|
|
f6d0de1454 | ||
|
|
9b0e27a9d1 | ||
|
|
415d88f302 | ||
|
|
f9c98ea9a1 | ||
|
|
369a5a8a2f | ||
|
|
62341c6381 | ||
|
|
fa1becb791 | ||
|
|
50a1704994 | ||
|
|
302538c370 | ||
|
|
0f80b6ce12 | ||
|
|
1df8fc903e | ||
|
|
9a2dcb89af | ||
|
|
1661591f6c | ||
|
|
6dde720039 | ||
|
|
a54b725d6d | ||
|
|
265e76e8ca | ||
|
|
1516069888 | ||
|
|
cd263b761c | ||
|
|
c952ad16ad | ||
|
|
ca09c58f26 | ||
|
|
e876a25d59 | ||
|
|
2a780151be | ||
|
|
9d2009d2af | ||
|
|
f887533dda | ||
|
|
bef3f154d6 | ||
|
|
6fb775d5ed | ||
|
|
b5a8563725 | ||
|
|
1a9ade6de9 | ||
|
|
48df853ff5 | ||
|
|
2137a2addb | ||
|
|
e6a7239716 | ||
|
|
25bed92f2e | ||
|
|
909cd407dd | ||
|
|
f2d25748b1 | ||
|
|
6396e6e970 | ||
|
|
e52f813dae | ||
|
|
9ab2eec30c | ||
|
|
f96b37dae3 | ||
|
|
5b68b2f707 | ||
|
|
ff634bd870 | ||
|
|
7d21d88dc9 | ||
|
|
31b62d6296 | ||
|
|
5e68604f11 | ||
|
|
c4edd3a683 | ||
|
|
574781e901 | ||
|
|
8ea4f933da | ||
|
|
1e9820f291 | ||
|
|
34ed86cf2d | ||
|
|
2dfcedad56 | ||
|
|
706168f2f0 | ||
|
|
3ecb5c0c75 | ||
|
|
249a3c4e18 | ||
|
|
2b8ebba233 | ||
|
|
758394464b | ||
|
|
d3c624ae10 | ||
|
|
163c65849e | ||
|
|
99ff714913 | ||
|
|
c2f302450f | ||
|
|
9186bcd455 | ||
|
|
3d074b530f | ||
|
|
20783db373 | ||
|
|
b9ddb39edc | ||
|
|
53496ff9eb | ||
|
|
c1248857b8 | ||
|
|
e2d61f5e89 | ||
|
|
5a1ef04c67 | ||
|
|
e9e73ef0e4 | ||
|
|
001e3688da | ||
|
|
e5a9606449 | ||
|
|
1217811402 | ||
|
|
bf2c1a0c1d | ||
|
|
5d83414e9b | ||
|
|
2f5b0a5afb | ||
|
|
b3728568a8 | ||
|
|
6ba98b4b2c | ||
|
|
4aa6c673ff | ||
|
|
aa3c1aa0fe | ||
|
|
31bac9641f | ||
|
|
60bdde6349 | ||
|
|
5faf33fabe | ||
|
|
f3b1a5dd35 | ||
|
|
07ccbf7f98 | ||
|
|
b7f7038934 | ||
|
|
6c054e6078 | ||
|
|
18974c68e1 | ||
|
|
78ab852141 | ||
|
|
4497173213 | ||
|
|
4d74d246e8 | ||
|
|
cc51537e93 | ||
|
|
07aececdcf | ||
|
|
c3fbf9cdf9 | ||
|
|
180598c603 | ||
|
|
69a5527e24 | ||
|
|
e4a6b3880f | ||
|
|
96ce4deb45 | ||
|
|
a3c179bd3f | ||
|
|
a57504ba1f | ||
|
|
25451eed6b | ||
|
|
50765563f8 | ||
|
|
2ae75ce13e | ||
|
|
18aa16650d | ||
|
|
314ed50e0f | ||
|
|
7346a49982 | ||
|
|
5182286218 | ||
|
|
a267848394 | ||
|
|
104bd31f9b | ||
|
|
8bc9985edb | ||
|
|
6dfc927730 | ||
|
|
3771e26eba | ||
|
|
fd21d35aee | ||
|
|
1417b924d2 | ||
|
|
2912c1b87d | ||
|
|
e746a0db7d | ||
|
|
84a2886003 | ||
|
|
c668677b8a | ||
|
|
2490cbfccb | ||
|
|
20a46a791c | ||
|
|
0e4fe08ac4 | ||
|
|
cf0f716534 | ||
|
|
b70cfa5968 | ||
|
|
f77beb50ff | ||
|
|
a941280982 | ||
|
|
9d40ee49e6 | ||
|
|
6abbdecb97 | ||
|
|
660ce92c27 | ||
|
|
c5bcef6ae4 | ||
|
|
d26a0b6b73 | ||
|
|
ec6f6056a8 | ||
|
|
c464b21d76 | ||
|
|
c83d249147 | ||
|
|
a11562e4dd | ||
|
|
2d872c25bf | ||
|
|
986837d601 | ||
|
|
353d6fbc5f | ||
|
|
0a88738faa | ||
|
|
4d6af57f79 | ||
|
|
110fd3ecdf | ||
|
|
1cb065f829 | ||
|
|
1cfa499b0e | ||
|
|
3fc0d4a666 | ||
|
|
de6c96d138 | ||
|
|
11974689ef | ||
|
|
4398ea12b8 |
@@ -49,7 +49,6 @@ Metrics/LineLength:
|
||||
- app/controllers/spree/admin/orders_controller_decorator.rb
|
||||
- app/controllers/spree/admin/payments_controller_decorator.rb
|
||||
- app/controllers/spree/admin/payment_methods_controller_decorator.rb
|
||||
- app/controllers/spree/admin/products_controller_decorator.rb
|
||||
- app/controllers/spree/admin/reports_controller_decorator.rb
|
||||
- app/controllers/spree/api/products_controller_decorator.rb
|
||||
- app/controllers/spree/credit_cards_controller.rb
|
||||
@@ -437,7 +436,6 @@ Metrics/AbcSize:
|
||||
- app/models/spree/order_decorator.rb
|
||||
- app/models/spree/payment_decorator.rb
|
||||
- app/models/spree/product_decorator.rb
|
||||
- app/models/spree/product_set.rb
|
||||
- app/models/spree/taxon_decorator.rb
|
||||
- app/serializers/api/admin/enterprise_serializer.rb
|
||||
- app/serializers/api/product_serializer.rb
|
||||
@@ -559,6 +557,7 @@ Metrics/CyclomaticComplexity:
|
||||
- app/helpers/checkout_helper.rb
|
||||
- app/helpers/i18n_helper.rb
|
||||
- app/helpers/order_cycles_helper.rb
|
||||
- app/helpers/spree/admin/navigation_helper_decorator.rb
|
||||
- app/models/enterprise.rb
|
||||
- app/models/enterprise_relationship.rb
|
||||
- app/models/product_import/entry_processor.rb
|
||||
@@ -566,7 +565,6 @@ Metrics/CyclomaticComplexity:
|
||||
- app/models/spree/ability_decorator.rb
|
||||
- app/models/spree/payment_decorator.rb
|
||||
- app/models/spree/product_decorator.rb
|
||||
- app/models/spree/product_set.rb
|
||||
- app/models/variant_override_set.rb
|
||||
- app/services/cart_service.rb
|
||||
- lib/discourse/single_sign_on.rb
|
||||
@@ -594,7 +592,6 @@ Metrics/PerceivedComplexity:
|
||||
- app/models/spree/ability_decorator.rb
|
||||
- app/models/spree/order_decorator.rb
|
||||
- app/models/spree/product_decorator.rb
|
||||
- app/models/spree/product_set.rb
|
||||
- lib/discourse/single_sign_on.rb
|
||||
- lib/open_food_network/bulk_coop_report.rb
|
||||
- lib/open_food_network/enterprise_issue_validator.rb
|
||||
@@ -650,7 +647,6 @@ Metrics/MethodLength:
|
||||
- app/models/spree/payment_decorator.rb
|
||||
- app/models/spree/payment_method_decorator.rb
|
||||
- app/models/spree/product_decorator.rb
|
||||
- app/models/spree/product_set.rb
|
||||
- app/serializers/api/admin/order_cycle_serializer.rb
|
||||
- app/serializers/api/cached_enterprise_serializer.rb
|
||||
- app/services/order_cycle_form.rb
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# This configuration was generated by
|
||||
# `rubocop --auto-gen-config --exclude-limit 1400`
|
||||
# on 2019-05-28 16:29:07 +0100 using RuboCop version 0.57.2.
|
||||
# on 2019-07-23 14:09:18 +0100 using RuboCop version 0.57.2.
|
||||
# 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
|
||||
@@ -32,15 +32,6 @@ Layout/EndAlignment:
|
||||
Layout/IndentHash:
|
||||
EnforcedStyle: consistent
|
||||
|
||||
# Offense count: 7
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: EnforcedStyle, IndentationWidth.
|
||||
# SupportedStyles: aligned, indented, indented_relative_to_receiver
|
||||
Layout/MultilineMethodCallIndentation:
|
||||
Exclude:
|
||||
- 'app/models/spree/line_item_decorator.rb'
|
||||
- 'app/models/spree/product_decorator.rb'
|
||||
|
||||
# Offense count: 4
|
||||
Lint/AmbiguousOperator:
|
||||
Exclude:
|
||||
@@ -55,7 +46,7 @@ Lint/DuplicateMethods:
|
||||
- 'lib/discourse/single_sign_on.rb'
|
||||
- 'lib/open_food_network/subscription_summary.rb'
|
||||
|
||||
# Offense count: 15
|
||||
# Offense count: 8
|
||||
Lint/IneffectiveAccessModifier:
|
||||
Exclude:
|
||||
- 'app/models/column_preference.rb'
|
||||
@@ -79,7 +70,13 @@ Lint/UnderscorePrefixedVariableName:
|
||||
Exclude:
|
||||
- 'spec/support/cancan_helper.rb'
|
||||
|
||||
# Offense count: 6
|
||||
# Offense count: 1
|
||||
# Cop supports --auto-correct.
|
||||
Lint/UnneededCopDisableDirective:
|
||||
Exclude:
|
||||
- 'app/models/product_import/entry_validator.rb'
|
||||
|
||||
# Offense count: 5
|
||||
# Configuration parameters: ContextCreatingMethods, MethodCreatingMethods.
|
||||
Lint/UselessAccessModifier:
|
||||
Exclude:
|
||||
@@ -89,28 +86,47 @@ Lint/UselessAccessModifier:
|
||||
- 'lib/open_food_network/reports/bulk_coop_report.rb'
|
||||
- 'spec/lib/open_food_network/reports/report_spec.rb'
|
||||
|
||||
# Offense count: 91
|
||||
# Offense count: 8
|
||||
# Configuration parameters: CheckForMethodsWithNoSideEffects.
|
||||
Lint/Void:
|
||||
Exclude:
|
||||
- 'app/serializers/api/enterprise_serializer.rb'
|
||||
- 'spec/features/admin/bulk_product_update_spec.rb'
|
||||
- 'spec/features/admin/enterprise_groups_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/payment_method_spec.rb'
|
||||
- 'spec/features/admin/products_spec.rb'
|
||||
- 'spec/features/admin/variant_overrides_spec.rb'
|
||||
- 'spec/features/admin/variants_spec.rb'
|
||||
- 'spec/features/consumer/shopping/checkout_spec.rb'
|
||||
- 'spec/features/consumer/shopping/shopping_spec.rb'
|
||||
- 'spec/features/consumer/shopping/variant_overrides_spec.rb'
|
||||
|
||||
# Offense count: 109
|
||||
# Offense count: 15
|
||||
Metrics/AbcSize:
|
||||
Max: 36
|
||||
|
||||
# Offense count: 13
|
||||
# Configuration parameters: CountComments, ExcludedMethods.
|
||||
Metrics/BlockLength:
|
||||
Max: 774
|
||||
Max: 115
|
||||
|
||||
# Offense count: 2
|
||||
# Configuration parameters: CountComments.
|
||||
Metrics/ClassLength:
|
||||
Max: 169
|
||||
|
||||
# Offense count: 1
|
||||
Metrics/CyclomaticComplexity:
|
||||
Max: 8
|
||||
|
||||
# Offense count: 8
|
||||
# Configuration parameters: CountComments.
|
||||
Metrics/MethodLength:
|
||||
Max: 31
|
||||
|
||||
# Offense count: 2
|
||||
# Configuration parameters: CountComments.
|
||||
Metrics/ModuleLength:
|
||||
Max: 208
|
||||
|
||||
# Offense count: 2
|
||||
Metrics/PerceivedComplexity:
|
||||
Max: 11
|
||||
|
||||
# Offense count: 7
|
||||
Naming/AccessorMethodName:
|
||||
@@ -160,7 +176,7 @@ Naming/PredicateName:
|
||||
- 'lib/open_food_network/packing_report.rb'
|
||||
- 'lib/tasks/data.rake'
|
||||
|
||||
# Offense count: 12
|
||||
# Offense count: 11
|
||||
# Configuration parameters: MinNameLength, AllowNamesEndingInNumbers, AllowedNames, ForbiddenNames.
|
||||
# AllowedNames: io, id, to, by, on, in, at
|
||||
Naming/UncommunicativeMethodParamName:
|
||||
@@ -288,13 +304,12 @@ Style/CaseEquality:
|
||||
- 'app/helpers/angular_form_helper.rb'
|
||||
- 'spec/models/spree/payment_spec.rb'
|
||||
|
||||
# Offense count: 79
|
||||
# Offense count: 78
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: AutoCorrect, EnforcedStyle.
|
||||
# SupportedStyles: nested, compact
|
||||
Style/ClassAndModuleChildren:
|
||||
Exclude:
|
||||
- 'app/controllers/spree/store_controller_decorator.rb'
|
||||
- 'app/helpers/angular_form_helper.rb'
|
||||
- 'app/models/calculator/flat_percent_per_item.rb'
|
||||
- 'app/models/spree/concerns/payment_method_distributors.rb'
|
||||
@@ -379,11 +394,27 @@ Style/CommentedKeyword:
|
||||
Exclude:
|
||||
- 'app/controllers/application_controller.rb'
|
||||
|
||||
# Offense count: 4
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: EnforcedStyle, SingleLineConditionsOnly, IncludeTernaryExpressions.
|
||||
# SupportedStyles: assign_to_condition, assign_inside_condition
|
||||
Style/ConditionalAssignment:
|
||||
Exclude:
|
||||
- 'app/controllers/spree/api/products_controller.rb'
|
||||
- 'app/controllers/spree/api/taxons_controller.rb'
|
||||
- 'app/controllers/spree/api/variants_controller.rb'
|
||||
|
||||
# Offense count: 2
|
||||
Style/DateTime:
|
||||
Exclude:
|
||||
- 'lib/open_food_network/users_and_enterprises_report.rb'
|
||||
|
||||
# Offense count: 1
|
||||
# Cop supports --auto-correct.
|
||||
Style/EachWithObject:
|
||||
Exclude:
|
||||
- 'app/controllers/spree/api/base_controller.rb'
|
||||
|
||||
# Offense count: 5
|
||||
# Configuration parameters: EnforcedStyle.
|
||||
# SupportedStyles: annotated, template, unannotated
|
||||
@@ -393,7 +424,7 @@ Style/FormatStringToken:
|
||||
- 'lib/open_food_network/sales_tax_report.rb'
|
||||
- 'spec/models/enterprise_spec.rb'
|
||||
|
||||
# Offense count: 69
|
||||
# Offense count: 68
|
||||
# Configuration parameters: MinBodyLength.
|
||||
Style/GuardClause:
|
||||
Exclude:
|
||||
@@ -410,7 +441,9 @@ Style/GuardClause:
|
||||
- 'app/controllers/spree/admin/products_controller_decorator.rb'
|
||||
- 'app/controllers/spree/admin/resource_controller_decorator.rb'
|
||||
- 'app/controllers/spree/admin/variants_controller_decorator.rb'
|
||||
- 'app/controllers/spree/orders_controller_decorator.rb'
|
||||
- 'app/controllers/spree/api/base_controller.rb'
|
||||
- 'app/controllers/spree/checkout_controller.rb'
|
||||
- 'app/controllers/spree/orders_controller.rb'
|
||||
- 'app/controllers/spree/paypal_controller_decorator.rb'
|
||||
- 'app/jobs/products_cache_integrity_checker_job.rb'
|
||||
- 'app/models/enterprise.rb'
|
||||
@@ -434,12 +467,23 @@ Style/GuardClause:
|
||||
- 'spec/support/request/distribution_helper.rb'
|
||||
- 'spec/support/request/shop_workflow.rb'
|
||||
|
||||
# Offense count: 3
|
||||
# Offense count: 6
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: EnforcedStyle, UseHashRocketsWithSymbolValues, PreferHashRocketsForNonAlnumEndingSymbols.
|
||||
# SupportedStyles: ruby19, hash_rockets, no_mixed_keys, ruby19_no_mixed_keys
|
||||
Style/HashSyntax:
|
||||
Exclude:
|
||||
- 'app/controllers/spree/api/base_controller.rb'
|
||||
- 'app/controllers/spree/checkout_controller.rb'
|
||||
- 'app/controllers/spree/orders_controller.rb'
|
||||
|
||||
# Offense count: 4
|
||||
Style/IfInsideElse:
|
||||
Exclude:
|
||||
- 'app/controllers/admin/column_preferences_controller.rb'
|
||||
- 'app/controllers/admin/variant_overrides_controller.rb'
|
||||
- 'app/controllers/spree/admin/products_controller_decorator.rb'
|
||||
- 'app/controllers/spree/api/taxons_controller.rb'
|
||||
|
||||
# Offense count: 1
|
||||
Style/MissingRespondToMissing:
|
||||
@@ -492,9 +536,10 @@ Style/RegexpLiteral:
|
||||
- 'spec/mailers/subscription_mailer_spec.rb'
|
||||
- 'spec/models/content_configuration_spec.rb'
|
||||
|
||||
# Offense count: 243
|
||||
# Offense count: 244
|
||||
Style/Send:
|
||||
Exclude:
|
||||
- 'app/controllers/spree/checkout_controller.rb'
|
||||
- 'app/models/spree/shipping_method_decorator.rb'
|
||||
- 'spec/controllers/admin/subscriptions_controller_spec.rb'
|
||||
- 'spec/controllers/checkout_controller_spec.rb'
|
||||
@@ -541,3 +586,9 @@ Style/Send:
|
||||
Style/StructInheritance:
|
||||
Exclude:
|
||||
- 'lib/open_food_network/enterprise_fee_applicator.rb'
|
||||
|
||||
# Offense count: 1
|
||||
# Cop supports --auto-correct.
|
||||
Style/UnlessElse:
|
||||
Exclude:
|
||||
- 'app/controllers/spree/api/variants_controller.rb'
|
||||
|
||||
4
Gemfile
4
Gemfile
@@ -49,6 +49,10 @@ gem 'delayed_job_web'
|
||||
# When merged, revert to upstream gem
|
||||
gem 'simple_form', github: 'RohanM/simple_form'
|
||||
|
||||
# Spree's default pagination gem (locked to the current version used by Spree)
|
||||
# We use it's methods in OFN code as well, so this is a direct dependency
|
||||
gem 'kaminari', '~> 0.14.1'
|
||||
|
||||
gem 'andand'
|
||||
gem 'angularjs-rails', '1.5.5'
|
||||
gem 'aws-sdk'
|
||||
|
||||
15
Gemfile.lock
15
Gemfile.lock
@@ -231,10 +231,10 @@ GEM
|
||||
nokogiri (~> 1.6.0)
|
||||
polyglot
|
||||
rails (>= 3.1)
|
||||
delayed_job (4.1.5)
|
||||
activesupport (>= 3.0, < 5.3)
|
||||
delayed_job_active_record (4.1.3)
|
||||
activerecord (>= 3.0, < 5.3)
|
||||
delayed_job (4.1.8)
|
||||
activesupport (>= 3.0, < 6.1)
|
||||
delayed_job_active_record (4.1.4)
|
||||
activerecord (>= 3.0, < 6.1)
|
||||
delayed_job (>= 3.0, < 5)
|
||||
delayed_job_web (1.4.3)
|
||||
activerecord (> 3.0.0)
|
||||
@@ -485,7 +485,7 @@ GEM
|
||||
actionpack (>= 3.0.0)
|
||||
activesupport (>= 3.0.0)
|
||||
kgio (2.11.2)
|
||||
knapsack (1.17.2)
|
||||
knapsack (1.18.0)
|
||||
rake
|
||||
launchy (2.4.3)
|
||||
addressable (~> 2.3)
|
||||
@@ -512,7 +512,7 @@ GEM
|
||||
multi_xml (0.6.0)
|
||||
multipart-post (2.1.1)
|
||||
nenv (0.3.0)
|
||||
net-http-persistent (3.0.1)
|
||||
net-http-persistent (3.1.0)
|
||||
connection_pool (~> 2.2)
|
||||
newrelic_rpm (3.18.1.330)
|
||||
nokogiri (1.6.8.1)
|
||||
@@ -701,7 +701,7 @@ GEM
|
||||
tilt (~> 1.1, != 1.3.0)
|
||||
state_machine (1.2.0)
|
||||
stringex (1.5.1)
|
||||
stripe (4.19.0)
|
||||
stripe (4.24.0)
|
||||
faraday (~> 0.13)
|
||||
net-http-persistent (~> 3.0)
|
||||
therubyracer (0.12.0)
|
||||
@@ -808,6 +808,7 @@ DEPENDENCIES
|
||||
jquery-rails (= 3.0.4)
|
||||
json_spec (~> 1.1.4)
|
||||
jwt (~> 2.2)
|
||||
kaminari (~> 0.14.1)
|
||||
knapsack
|
||||
letter_opener (>= 1.4.1)
|
||||
listen (= 3.0.8)
|
||||
|
||||
@@ -1,267 +1,287 @@
|
||||
angular.module("ofn.admin").controller "AdminProductEditCtrl", ($scope, $timeout, $http, $window, BulkProducts, DisplayProperties, dataFetcher, DirtyProducts, VariantUnitManager, StatusMessage, producers, Taxons, SpreeApiAuth, Columns, tax_categories) ->
|
||||
$scope.loading = true
|
||||
$scope.loadingAllPages = true
|
||||
angular.module("ofn.admin").controller "AdminProductEditCtrl", ($scope, $timeout, $filter, $http, $window, BulkProducts, DisplayProperties, DirtyProducts, VariantUnitManager, StatusMessage, producers, Taxons, SpreeApiAuth, Columns, tax_categories, RequestMonitor) ->
|
||||
$scope.StatusMessage = StatusMessage
|
||||
|
||||
$scope.StatusMessage = StatusMessage
|
||||
$scope.columns = Columns.columns
|
||||
|
||||
$scope.columns = Columns.columns
|
||||
$scope.variant_unit_options = VariantUnitManager.variantUnitOptions()
|
||||
|
||||
$scope.variant_unit_options = VariantUnitManager.variantUnitOptions()
|
||||
$scope.RequestMonitor = RequestMonitor
|
||||
$scope.pagination = BulkProducts.pagination
|
||||
$scope.per_page_options = [
|
||||
{id: 15, name: t('js.admin.orders.index.per_page', results: 15)},
|
||||
{id: 50, name: t('js.admin.orders.index.per_page', results: 50)},
|
||||
{id: 100, name: t('js.admin.orders.index.per_page', results: 100)}
|
||||
]
|
||||
|
||||
$scope.filterableColumns = [
|
||||
{ name: t("label_producers"), db_column: "producer_name" },
|
||||
{ name: t("name"), db_column: "name" }
|
||||
]
|
||||
$scope.filterableColumns = [
|
||||
{ name: t("label_producers"), db_column: "producer_name" },
|
||||
{ name: t("name"), db_column: "name" }
|
||||
]
|
||||
|
||||
$scope.filterTypes = [
|
||||
{ name: t("equals"), predicate: "eq" },
|
||||
{ name: t("contains"), predicate: "cont" }
|
||||
]
|
||||
$scope.filterTypes = [
|
||||
{ name: t("equals"), predicate: "eq" },
|
||||
{ name: t("contains"), predicate: "cont" }
|
||||
]
|
||||
|
||||
$scope.optionTabs =
|
||||
filters: { title: t("filter_products"), visible: false }
|
||||
$scope.optionTabs =
|
||||
filters: { title: t("filter_products"), visible: false }
|
||||
|
||||
$scope.producers = producers
|
||||
$scope.taxons = Taxons.all
|
||||
$scope.tax_categories = tax_categories
|
||||
$scope.producerFilter = ""
|
||||
$scope.categoryFilter = ""
|
||||
$scope.importDateFilter = ""
|
||||
$scope.page = 1
|
||||
$scope.per_page = 15
|
||||
$scope.products = BulkProducts.products
|
||||
$scope.query = ""
|
||||
$scope.DisplayProperties = DisplayProperties
|
||||
|
||||
$scope.initialise = ->
|
||||
SpreeApiAuth.authorise()
|
||||
.then ->
|
||||
$scope.spree_api_key_ok = true
|
||||
$scope.fetchProducts()
|
||||
.catch (message) ->
|
||||
$scope.api_error_msg = message
|
||||
|
||||
$scope.$watchCollection '[query, producerFilter, categoryFilter, importDateFilter, per_page]', ->
|
||||
$scope.page = 1 # Reset page when changing filters for new search
|
||||
|
||||
$scope.changePage = (newPage) ->
|
||||
$scope.page = newPage
|
||||
$scope.fetchProducts()
|
||||
|
||||
$scope.fetchProducts = ->
|
||||
removeClearedValues()
|
||||
params = {
|
||||
'q[name_cont]': $scope.query,
|
||||
'q[supplier_id_eq]': $scope.producerFilter,
|
||||
'q[primary_taxon_id_eq]': $scope.categoryFilter,
|
||||
import_date: $scope.importDateFilter,
|
||||
page: $scope.page,
|
||||
per_page: $scope.per_page
|
||||
}
|
||||
RequestMonitor.load(BulkProducts.fetch(params).$promise).then ->
|
||||
$scope.resetProducts()
|
||||
|
||||
removeClearedValues = ->
|
||||
delete $scope.producerFilter if $scope.producerFilter == "0"
|
||||
delete $scope.categoryFilter if $scope.categoryFilter == "0"
|
||||
delete $scope.importDateFilter if $scope.importDateFilter == "0"
|
||||
|
||||
$timeout ->
|
||||
if $scope.showLatestImport
|
||||
$scope.importDateFilter = $scope.importDates[1].id
|
||||
|
||||
$scope.resetProducts = ->
|
||||
DirtyProducts.clear()
|
||||
StatusMessage.clear()
|
||||
|
||||
$scope.updateOnHand = (product) ->
|
||||
on_demand_variants = []
|
||||
if product.variants
|
||||
on_demand_variants = (variant for id, variant of product.variants when variant.on_demand)
|
||||
|
||||
unless product.on_demand || on_demand_variants.length > 0
|
||||
product.on_hand = $scope.onHand(product)
|
||||
|
||||
|
||||
$scope.producers = producers
|
||||
$scope.taxons = Taxons.all
|
||||
$scope.tax_categories = tax_categories
|
||||
$scope.filterProducers = [{id: "0", name: ""}].concat $scope.producers
|
||||
$scope.filterTaxons = [{id: "0", name: ""}].concat $scope.taxons
|
||||
$scope.onHand = (product) ->
|
||||
onHand = 0
|
||||
if product.hasOwnProperty("variants") and product.variants instanceof Object
|
||||
for id, variant of product.variants
|
||||
onHand = onHand + parseInt(if variant.on_hand > 0 then variant.on_hand else 0)
|
||||
else
|
||||
onHand = "error"
|
||||
onHand
|
||||
|
||||
$scope.shiftTab = (tab) ->
|
||||
$scope.visibleTab.visible = false unless $scope.visibleTab == tab || $scope.visibleTab == undefined
|
||||
tab.visible = !tab.visible
|
||||
$scope.visibleTab = tab
|
||||
|
||||
$scope.resetSelectFilters = ->
|
||||
$scope.query = ""
|
||||
$scope.producerFilter = "0"
|
||||
$scope.categoryFilter = "0"
|
||||
$scope.importDateFilter = "0"
|
||||
$scope.products = BulkProducts.products
|
||||
$scope.filteredProducts = []
|
||||
$scope.currentFilters = []
|
||||
$scope.limit = 15
|
||||
$scope.query = ""
|
||||
$scope.DisplayProperties = DisplayProperties
|
||||
|
||||
$scope.initialise = ->
|
||||
SpreeApiAuth.authorise()
|
||||
.then ->
|
||||
$scope.spree_api_key_ok = true
|
||||
$scope.fetchProducts()
|
||||
.catch (message) ->
|
||||
$scope.api_error_msg = message
|
||||
|
||||
$scope.$watchCollection '[query, producerFilter, categoryFilter, importDateFilter]', ->
|
||||
$scope.limit = 15 # Reset limit whenever searching
|
||||
|
||||
$scope.fetchProducts = ->
|
||||
$scope.loading = true
|
||||
$scope.loadingAllPages = true
|
||||
BulkProducts.fetch($scope.currentFilters, ->
|
||||
$scope.loadingAllPages = false
|
||||
).then ->
|
||||
$scope.resetProducts()
|
||||
$scope.loading = false
|
||||
|
||||
$timeout ->
|
||||
if $scope.showLatestImport
|
||||
$scope.importDateFilter = $scope.importDates[1].id
|
||||
|
||||
$scope.resetProducts = ->
|
||||
DirtyProducts.clear()
|
||||
StatusMessage.clear()
|
||||
|
||||
$scope.updateOnHand = (product) ->
|
||||
on_demand_variants = []
|
||||
if product.variants
|
||||
on_demand_variants = (variant for id, variant of product.variants when variant.on_demand)
|
||||
|
||||
unless product.on_demand || on_demand_variants.length > 0
|
||||
product.on_hand = $scope.onHand(product)
|
||||
$scope.editWarn = (product, variant) ->
|
||||
if (DirtyProducts.count() > 0 and confirm(t("unsaved_changes_confirmation"))) or (DirtyProducts.count() == 0)
|
||||
window.location = "/admin/products/" + product.permalink_live + ((if variant then "/variants/" + variant.id else "")) + "/edit"
|
||||
|
||||
|
||||
$scope.onHand = (product) ->
|
||||
onHand = 0
|
||||
if product.hasOwnProperty("variants") and product.variants instanceof Object
|
||||
for id, variant of product.variants
|
||||
onHand = onHand + parseInt(if variant.on_hand > 0 then variant.on_hand else 0)
|
||||
else
|
||||
onHand = "error"
|
||||
onHand
|
||||
$scope.toggleShowAllVariants = ->
|
||||
showVariants = !DisplayProperties.showVariants 0
|
||||
$scope.products.forEach (product) ->
|
||||
DisplayProperties.setShowVariants product.id, showVariants
|
||||
DisplayProperties.setShowVariants 0, showVariants
|
||||
|
||||
$scope.shiftTab = (tab) ->
|
||||
$scope.visibleTab.visible = false unless $scope.visibleTab == tab || $scope.visibleTab == undefined
|
||||
tab.visible = !tab.visible
|
||||
$scope.visibleTab = tab
|
||||
|
||||
$scope.resetSelectFilters = ->
|
||||
$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)
|
||||
window.location = "/admin/products/" + product.permalink_live + ((if variant then "/variants/" + variant.id else "")) + "/edit"
|
||||
$scope.addVariant = (product) ->
|
||||
product.variants.push
|
||||
id: $scope.nextVariantId()
|
||||
unit_value: null
|
||||
unit_description: null
|
||||
on_demand: false
|
||||
display_as: null
|
||||
display_name: null
|
||||
on_hand: null
|
||||
price: null
|
||||
DisplayProperties.setShowVariants product.id, true
|
||||
|
||||
|
||||
$scope.toggleShowAllVariants = ->
|
||||
showVariants = !DisplayProperties.showVariants 0
|
||||
$scope.filteredProducts.forEach (product) ->
|
||||
DisplayProperties.setShowVariants product.id, showVariants
|
||||
DisplayProperties.setShowVariants 0, showVariants
|
||||
$scope.nextVariantId = ->
|
||||
$scope.variantIdCounter = 0 unless $scope.variantIdCounter?
|
||||
$scope.variantIdCounter -= 1
|
||||
$scope.variantIdCounter
|
||||
|
||||
$scope.addVariant = (product) ->
|
||||
product.variants.push
|
||||
id: $scope.nextVariantId()
|
||||
unit_value: null
|
||||
unit_description: null
|
||||
on_demand: false
|
||||
display_as: null
|
||||
display_name: null
|
||||
on_hand: null
|
||||
price: null
|
||||
DisplayProperties.setShowVariants product.id, true
|
||||
|
||||
|
||||
$scope.nextVariantId = ->
|
||||
$scope.variantIdCounter = 0 unless $scope.variantIdCounter?
|
||||
$scope.variantIdCounter -= 1
|
||||
$scope.variantIdCounter
|
||||
|
||||
$scope.deleteProduct = (product) ->
|
||||
if confirm("Are you sure?")
|
||||
$http(
|
||||
method: "DELETE"
|
||||
url: "/api/products/" + product.id + "/soft_delete"
|
||||
).success (data) ->
|
||||
$scope.products.splice $scope.products.indexOf(product), 1
|
||||
DirtyProducts.deleteProduct product.id
|
||||
$scope.displayDirtyProducts()
|
||||
|
||||
|
||||
$scope.deleteVariant = (product, variant) ->
|
||||
if product.variants.length > 1
|
||||
if !$scope.variantSaved(variant)
|
||||
$scope.removeVariant(product, variant)
|
||||
else
|
||||
if confirm(t("are_you_sure"))
|
||||
$http(
|
||||
method: "DELETE"
|
||||
url: "/api/products/" + product.permalink_live + "/variants/" + variant.id + "/soft_delete"
|
||||
).success (data) ->
|
||||
$scope.removeVariant(product, variant)
|
||||
else
|
||||
alert(t("delete_product_variant"))
|
||||
|
||||
$scope.removeVariant = (product, variant) ->
|
||||
product.variants.splice product.variants.indexOf(variant), 1
|
||||
DirtyProducts.deleteVariant product.id, variant.id
|
||||
$scope.displayDirtyProducts()
|
||||
|
||||
|
||||
$scope.cloneProduct = (product) ->
|
||||
BulkProducts.cloneProduct product
|
||||
|
||||
$scope.hasVariants = (product) ->
|
||||
product.variants.length > 0
|
||||
|
||||
|
||||
$scope.hasUnit = (product) ->
|
||||
product.variant_unit_with_scale?
|
||||
|
||||
|
||||
$scope.variantSaved = (variant) ->
|
||||
variant.hasOwnProperty('id') && variant.id > 0
|
||||
|
||||
|
||||
$scope.hasOnDemandVariants = (product) ->
|
||||
(variant for id, variant of product.variants when variant.on_demand).length > 0
|
||||
|
||||
|
||||
$scope.submitProducts = ->
|
||||
# Pack pack $scope.products, so they will match the list returned from the server,
|
||||
# then pack $scope.dirtyProducts, ensuring that the correct product info is sent to the server.
|
||||
$scope.packProduct product for id, product of $scope.products
|
||||
$scope.packProduct product for id, product of DirtyProducts.all()
|
||||
|
||||
productsToSubmit = filterSubmitProducts(DirtyProducts.all())
|
||||
if productsToSubmit.length > 0
|
||||
$scope.updateProducts productsToSubmit # Don't submit an empty list
|
||||
else
|
||||
StatusMessage.display 'alert', t("products_change")
|
||||
|
||||
|
||||
$scope.updateProducts = (productsToSubmit) ->
|
||||
$scope.displayUpdating()
|
||||
$scope.deleteProduct = (product) ->
|
||||
if confirm("Are you sure?")
|
||||
$http(
|
||||
method: "POST"
|
||||
url: "/admin/products/bulk_update"
|
||||
data:
|
||||
products: productsToSubmit
|
||||
filters: $scope.currentFilters
|
||||
).success((data) ->
|
||||
DirtyProducts.clear()
|
||||
BulkProducts.updateVariantLists(data.products || [])
|
||||
$timeout -> $scope.displaySuccess()
|
||||
).error (data, status) ->
|
||||
if status == 400 && data.errors? && data.errors.length > 0
|
||||
errors = error + "\n" for error in data.errors
|
||||
alert t("products_update_error") + "\n" + errors
|
||||
$scope.displayFailure t("products_update_error")
|
||||
else
|
||||
$scope.displayFailure t("products_update_error_data") + status
|
||||
method: "DELETE"
|
||||
url: "/api/products/" + product.id + "/soft_delete"
|
||||
).success (data) ->
|
||||
$scope.products.splice $scope.products.indexOf(product), 1
|
||||
DirtyProducts.deleteProduct product.id
|
||||
$scope.displayDirtyProducts()
|
||||
|
||||
$scope.cancel = (destination) ->
|
||||
$window.location = destination
|
||||
|
||||
$scope.packProduct = (product) ->
|
||||
if product.variant_unit_with_scale
|
||||
match = product.variant_unit_with_scale.match(/^([^_]+)_([\d\.]+)$/)
|
||||
if match
|
||||
product.variant_unit = match[1]
|
||||
product.variant_unit_scale = parseFloat(match[2])
|
||||
else
|
||||
product.variant_unit = product.variant_unit_with_scale
|
||||
product.variant_unit_scale = null
|
||||
$scope.deleteVariant = (product, variant) ->
|
||||
if product.variants.length > 1
|
||||
if !$scope.variantSaved(variant)
|
||||
$scope.removeVariant(product, variant)
|
||||
else
|
||||
product.variant_unit = product.variant_unit_scale = null
|
||||
if confirm(t("are_you_sure"))
|
||||
$http(
|
||||
method: "DELETE"
|
||||
url: "/api/products/" + product.permalink_live + "/variants/" + variant.id + "/soft_delete"
|
||||
).success (data) ->
|
||||
$scope.removeVariant(product, variant)
|
||||
else
|
||||
alert(t("delete_product_variant"))
|
||||
|
||||
$scope.packVariant product, product.master if product.master
|
||||
|
||||
if product.variants
|
||||
for id, variant of product.variants
|
||||
$scope.packVariant product, variant
|
||||
$scope.removeVariant = (product, variant) ->
|
||||
product.variants.splice product.variants.indexOf(variant), 1
|
||||
DirtyProducts.deleteVariant product.id, variant.id
|
||||
$scope.displayDirtyProducts()
|
||||
|
||||
|
||||
$scope.packVariant = (product, variant) ->
|
||||
if variant.hasOwnProperty("unit_value_with_description")
|
||||
match = variant.unit_value_with_description.match(/^([\d\.]+(?= |$)|)( |)(.*)$/)
|
||||
if match
|
||||
product = BulkProducts.find product.id
|
||||
variant.unit_value = parseFloat(match[1])
|
||||
variant.unit_value = null if isNaN(variant.unit_value)
|
||||
variant.unit_value *= product.variant_unit_scale if variant.unit_value && product.variant_unit_scale
|
||||
variant.unit_description = match[3]
|
||||
$scope.cloneProduct = (product) ->
|
||||
BulkProducts.cloneProduct product
|
||||
|
||||
$scope.incrementLimit = ->
|
||||
if $scope.limit < $scope.products.length
|
||||
$scope.limit = $scope.limit + 5
|
||||
$scope.hasVariants = (product) ->
|
||||
product.variants.length > 0
|
||||
|
||||
|
||||
$scope.displayUpdating = ->
|
||||
StatusMessage.display 'progress', t("saving")
|
||||
$scope.hasUnit = (product) ->
|
||||
product.variant_unit_with_scale?
|
||||
|
||||
|
||||
$scope.displaySuccess = ->
|
||||
StatusMessage.display 'success',t("products_changes_saved")
|
||||
$scope.bulk_product_form.$setPristine()
|
||||
$scope.variantSaved = (variant) ->
|
||||
variant.hasOwnProperty('id') && variant.id > 0
|
||||
|
||||
|
||||
$scope.displayFailure = (failMessage) ->
|
||||
StatusMessage.display 'failure', t("products_update_error_msg") + "#{failMessage}"
|
||||
$scope.hasOnDemandVariants = (product) ->
|
||||
(variant for id, variant of product.variants when variant.on_demand).length > 0
|
||||
|
||||
|
||||
$scope.displayDirtyProducts = ->
|
||||
count = DirtyProducts.count()
|
||||
switch count
|
||||
when 0 then StatusMessage.clear()
|
||||
when 1 then StatusMessage.display 'notice', t("one_product_unsaved")
|
||||
else StatusMessage.display 'notice', t("products_unsaved", n: count)
|
||||
$scope.submitProducts = ->
|
||||
# Pack pack $scope.products, so they will match the list returned from the server,
|
||||
# then pack $scope.dirtyProducts, ensuring that the correct product info is sent to the server.
|
||||
$scope.packProduct product for id, product of $scope.products
|
||||
$scope.packProduct product for id, product of DirtyProducts.all()
|
||||
|
||||
productsToSubmit = filterSubmitProducts(DirtyProducts.all())
|
||||
if productsToSubmit.length > 0
|
||||
$scope.updateProducts productsToSubmit # Don't submit an empty list
|
||||
else
|
||||
StatusMessage.display 'alert', t("products_change")
|
||||
|
||||
|
||||
$scope.updateProducts = (productsToSubmit) ->
|
||||
$scope.displayUpdating()
|
||||
$http(
|
||||
method: "POST"
|
||||
url: "/admin/products/bulk_update"
|
||||
data:
|
||||
products: productsToSubmit
|
||||
filters:
|
||||
'q[name_cont]': $scope.query
|
||||
'q[supplier_id_eq]': $scope.producerFilter
|
||||
'q[primary_taxon_id_eq]': $scope.categoryFilter
|
||||
import_date: $scope.importDateFilter
|
||||
page: $scope.page
|
||||
per_page: $scope.per_page
|
||||
).success((data) ->
|
||||
DirtyProducts.clear()
|
||||
BulkProducts.updateVariantLists(data.products || [])
|
||||
$timeout -> $scope.displaySuccess()
|
||||
).error (data, status) ->
|
||||
if status == 400 && data.errors? && data.errors.length > 0
|
||||
errors = error + "\n" for error in data.errors
|
||||
alert t("products_update_error") + "\n" + errors
|
||||
$scope.displayFailure t("products_update_error")
|
||||
else
|
||||
$scope.displayFailure t("products_update_error_data") + status
|
||||
|
||||
$scope.cancel = (destination) ->
|
||||
$window.location = destination
|
||||
|
||||
$scope.packProduct = (product) ->
|
||||
if product.variant_unit_with_scale
|
||||
match = product.variant_unit_with_scale.match(/^([^_]+)_([\d\.]+)$/)
|
||||
if match
|
||||
product.variant_unit = match[1]
|
||||
product.variant_unit_scale = parseFloat(match[2])
|
||||
else
|
||||
product.variant_unit = product.variant_unit_with_scale
|
||||
product.variant_unit_scale = null
|
||||
else
|
||||
product.variant_unit = product.variant_unit_scale = null
|
||||
|
||||
$scope.packVariant product, product.master if product.master
|
||||
|
||||
if product.variants
|
||||
for id, variant of product.variants
|
||||
$scope.packVariant product, variant
|
||||
|
||||
|
||||
$scope.packVariant = (product, variant) ->
|
||||
if variant.hasOwnProperty("unit_value_with_description")
|
||||
match = variant.unit_value_with_description.match(/^([\d\.]+(?= |$)|)( |)(.*)$/)
|
||||
if match
|
||||
product = BulkProducts.find product.id
|
||||
variant.unit_value = parseFloat(match[1])
|
||||
variant.unit_value = null if isNaN(variant.unit_value)
|
||||
variant.unit_value *= product.variant_unit_scale if variant.unit_value && product.variant_unit_scale
|
||||
variant.unit_description = match[3]
|
||||
|
||||
$scope.incrementLimit = ->
|
||||
if $scope.limit < $scope.products.length
|
||||
$scope.limit = $scope.limit + 5
|
||||
|
||||
|
||||
$scope.displayUpdating = ->
|
||||
StatusMessage.display 'progress', t("saving")
|
||||
|
||||
|
||||
$scope.displaySuccess = ->
|
||||
StatusMessage.display 'success',t("products_changes_saved")
|
||||
$scope.bulk_product_form.$setPristine()
|
||||
|
||||
|
||||
$scope.displayFailure = (failMessage) ->
|
||||
StatusMessage.display 'failure', t("products_update_error_msg") + "#{failMessage}"
|
||||
|
||||
|
||||
$scope.displayDirtyProducts = ->
|
||||
count = DirtyProducts.count()
|
||||
switch count
|
||||
when 0 then StatusMessage.clear()
|
||||
when 1 then StatusMessage.display 'notice', t("one_product_unsaved")
|
||||
else StatusMessage.display 'notice', t("products_unsaved", n: count)
|
||||
|
||||
|
||||
filterSubmitProducts = (productsToFilter) ->
|
||||
|
||||
@@ -23,7 +23,7 @@ angular.module("admin.orders").controller "ordersCtrl", ($scope, RequestMonitor,
|
||||
|
||||
$scope.fetchResults = (page=1) ->
|
||||
$scope.resetSelected()
|
||||
Orders.index({
|
||||
params = {
|
||||
'q[completed_at_lt]': $scope['q']['completed_at_lt'],
|
||||
'q[completed_at_gt]': $scope['q']['completed_at_gt'],
|
||||
'q[state_eq]': $scope['q']['state_eq'],
|
||||
@@ -39,7 +39,8 @@ angular.module("admin.orders").controller "ordersCtrl", ($scope, RequestMonitor,
|
||||
'q[s]': $scope.sorting || 'completed_at desc',
|
||||
per_page: $scope.per_page,
|
||||
page: page
|
||||
})
|
||||
}
|
||||
RequestMonitor.load(Orders.index(params).$promise)
|
||||
|
||||
$scope.resetSelected = ->
|
||||
$scope.selected_orders.length = 0
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
angular.module("admin.products").directive "setOnDemand", ->
|
||||
link: (scope, element, attr) ->
|
||||
onHand = element.context.querySelector("#variant_on_hand")
|
||||
onDemand = element.context.querySelector("#variant_on_demand")
|
||||
|
||||
disableOnHandIfOnDemand = ->
|
||||
if onDemand.checked
|
||||
onHand.disabled = 'disabled'
|
||||
onHand.dataStock = onHand.value
|
||||
onHand.value = t('admin.products.variants.infinity')
|
||||
|
||||
disableOnHandIfOnDemand()
|
||||
|
||||
onDemand.addEventListener 'change', (event) ->
|
||||
disableOnHandIfOnDemand()
|
||||
|
||||
if !onDemand.checked
|
||||
onHand.removeAttribute('disabled')
|
||||
onHand.value = onHand.dataStock
|
||||
@@ -0,0 +1,6 @@
|
||||
angular.module("admin.resources").factory 'ProductResource', ($resource) ->
|
||||
$resource('/admin/product/:id/:action.json', {}, {
|
||||
'index':
|
||||
url: '/api/products/bulk_products.json'
|
||||
method: 'GET'
|
||||
})
|
||||
@@ -1,15 +1,13 @@
|
||||
angular.module("ofn.admin").factory "BulkProducts", (PagedFetcher, dataFetcher, $http) ->
|
||||
angular.module("ofn.admin").factory "BulkProducts", (ProductResource, dataFetcher, $http) ->
|
||||
new class BulkProducts
|
||||
products: []
|
||||
pagination: {}
|
||||
|
||||
fetch: (filters, onComplete) ->
|
||||
queryString = filters.reduce (qs,f) ->
|
||||
return qs + "q[#{f.property.db_column}_#{f.predicate.predicate}]=#{f.value};"
|
||||
, ""
|
||||
|
||||
url = "/api/products/bulk_products?page=::page::;per_page=20;#{queryString}"
|
||||
processData = (data) => @addProducts data.products
|
||||
PagedFetcher.fetch url, processData, onComplete
|
||||
fetch: (params) ->
|
||||
ProductResource.index params, (data) =>
|
||||
@products.length = 0
|
||||
@addProducts data.products
|
||||
angular.extend(@pagination, data.pagination)
|
||||
|
||||
cloneProduct: (product) ->
|
||||
$http.post("/api/products/" + product.id + "/clone").success (data) =>
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
@API_DATETIME_FORMAT = "YYYY-MM-DD HH:mm:SS Z"
|
||||
|
||||
Darkswarm.filter "date_in_words", ->
|
||||
(date) ->
|
||||
moment(date).fromNow()
|
||||
(date, dateFormat) ->
|
||||
dateFormat ?= @API_DATETIME_FORMAT
|
||||
moment(date, dateFormat).fromNow()
|
||||
|
||||
Darkswarm.filter "sensible_timeframe", (date_in_wordsFilter)->
|
||||
(date) ->
|
||||
if moment().add(2, 'days') < moment(date)
|
||||
(date, dateFormat) ->
|
||||
dateFormat ?= @API_DATETIME_FORMAT
|
||||
|
||||
if moment().add(2, 'days') < moment(date, dateFormat)
|
||||
t 'orders_open'
|
||||
else
|
||||
t('closing') + date_in_wordsFilter(date)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
.select2-container {
|
||||
.select2-choice {
|
||||
.select2-search-choice-close {
|
||||
display: none;
|
||||
display: none !important;
|
||||
}
|
||||
.select2-arrow {
|
||||
width: 22px;
|
||||
|
||||
143
app/controllers/api/products_controller.rb
Normal file
143
app/controllers/api/products_controller.rb
Normal file
@@ -0,0 +1,143 @@
|
||||
require 'open_food_network/permissions'
|
||||
|
||||
module Api
|
||||
class ProductsController < Api::BaseController
|
||||
respond_to :json
|
||||
DEFAULT_PAGE = 1
|
||||
DEFAULT_PER_PAGE = 15
|
||||
|
||||
skip_authorization_check only: [:show, :bulk_products, :overridable]
|
||||
|
||||
def show
|
||||
@product = find_product(params[:id])
|
||||
render json: @product, serializer: Api::Admin::ProductSerializer
|
||||
end
|
||||
|
||||
def create
|
||||
authorize! :create, Spree::Product
|
||||
params[:product][:available_on] ||= Time.zone.now
|
||||
@product = Spree::Product.new(params[:product])
|
||||
begin
|
||||
if @product.save
|
||||
render json: @product, serializer: Api::Admin::ProductSerializer, status: 201
|
||||
else
|
||||
invalid_resource!(@product)
|
||||
end
|
||||
rescue ActiveRecord::RecordNotUnique
|
||||
@product.permalink = nil
|
||||
retry
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
authorize! :update, Spree::Product
|
||||
@product = find_product(params[:id])
|
||||
if @product.update_attributes(params[:product])
|
||||
render json: @product, serializer: Api::Admin::ProductSerializer, status: 200
|
||||
else
|
||||
invalid_resource!(@product)
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
authorize! :delete, Spree::Product
|
||||
@product = find_product(params[:id])
|
||||
@product.update_attribute(:deleted_at, Time.zone.now)
|
||||
@product.variants_including_master.update_all(deleted_at: Time.zone.now)
|
||||
render json: @product, serializer: Api::Admin::ProductSerializer, status: 204
|
||||
end
|
||||
|
||||
# TODO: This should be named 'managed'. Is the action above used? Maybe we should remove it.
|
||||
def bulk_products
|
||||
product_query = OpenFoodNetwork::Permissions.new(current_api_user).
|
||||
editable_products.merge(product_scope)
|
||||
|
||||
if params[:import_date].present?
|
||||
product_query = product_query.imported_on(params[:import_date]).group_by_products_id
|
||||
end
|
||||
|
||||
@products = product_query.order('created_at DESC').
|
||||
ransack(params[:q]).result.
|
||||
page(params[:page] || DEFAULT_PAGE).per(params[:per_page] || DEFAULT_PER_PAGE)
|
||||
|
||||
render_paged_products @products
|
||||
end
|
||||
|
||||
def overridable
|
||||
producers = OpenFoodNetwork::Permissions.new(current_api_user).
|
||||
variant_override_producers.by_name
|
||||
|
||||
@products = paged_products_for_producers producers
|
||||
|
||||
render_paged_products @products
|
||||
end
|
||||
|
||||
def soft_delete
|
||||
authorize! :delete, Spree::Product
|
||||
@product = find_product(params[:product_id])
|
||||
authorize! :delete, @product
|
||||
@product.destroy
|
||||
render json: @product, serializer: Api::Admin::ProductSerializer, status: 204
|
||||
end
|
||||
|
||||
# POST /api/products/:product_id/clone
|
||||
#
|
||||
def clone
|
||||
authorize! :create, Spree::Product
|
||||
original_product = find_product(params[:product_id])
|
||||
authorize! :update, original_product
|
||||
|
||||
@product = original_product.duplicate
|
||||
|
||||
render json: @product, serializer: Api::Admin::ProductSerializer, status: 201
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Copied and modified from SpreeApi::BaseController to allow
|
||||
# enterprise users to access inactive products
|
||||
def product_scope
|
||||
# This line modified
|
||||
if current_api_user.has_spree_role?("admin") || current_api_user.enterprises.present?
|
||||
scope = Spree::Product
|
||||
if params[:show_deleted]
|
||||
scope = scope.with_deleted
|
||||
end
|
||||
else
|
||||
scope = Spree::Product.active
|
||||
end
|
||||
|
||||
scope.includes(:master)
|
||||
end
|
||||
|
||||
def paged_products_for_producers(producers)
|
||||
Spree::Product.scoped.
|
||||
merge(product_scope).
|
||||
where(supplier_id: producers).
|
||||
by_producer.by_name.
|
||||
ransack(params[:q]).result.
|
||||
page(params[:page]).per(params[:per_page])
|
||||
end
|
||||
|
||||
def render_paged_products(products)
|
||||
serializer = ActiveModel::ArraySerializer.new(
|
||||
products,
|
||||
each_serializer: ::Api::Admin::ProductSerializer
|
||||
)
|
||||
|
||||
render text: {
|
||||
products: serializer,
|
||||
pagination: pagination_data(products)
|
||||
}.to_json
|
||||
end
|
||||
|
||||
def pagination_data(results)
|
||||
{
|
||||
results: results.total_count,
|
||||
pages: results.num_pages,
|
||||
page: (params[:page] || DEFAULT_PAGE).to_i,
|
||||
per_page: (params[:per_page] || DEFAULT_PER_PAGE).to_i
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
79
app/controllers/api/variants_controller.rb
Normal file
79
app/controllers/api/variants_controller.rb
Normal file
@@ -0,0 +1,79 @@
|
||||
module Api
|
||||
class VariantsController < Api::BaseController
|
||||
respond_to :json
|
||||
|
||||
skip_authorization_check only: [:index, :show]
|
||||
before_filter :product
|
||||
|
||||
def index
|
||||
@variants = scope.includes(:option_values).ransack(params[:q]).result
|
||||
render json: @variants, each_serializer: Api::VariantSerializer
|
||||
end
|
||||
|
||||
def show
|
||||
@variant = scope.includes(:option_values).find(params[:id])
|
||||
render json: @variant, serializer: Api::VariantSerializer
|
||||
end
|
||||
|
||||
def create
|
||||
authorize! :create, Spree::Variant
|
||||
@variant = scope.new(params[:variant])
|
||||
if @variant.save
|
||||
render json: @variant, serializer: Api::VariantSerializer, status: 201
|
||||
else
|
||||
invalid_resource!(@variant)
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
authorize! :update, Spree::Variant
|
||||
@variant = scope.find(params[:id])
|
||||
if @variant.update_attributes(params[:variant])
|
||||
render json: @variant, serializer: Api::VariantSerializer, status: 200
|
||||
else
|
||||
invalid_resource!(@product)
|
||||
end
|
||||
end
|
||||
|
||||
def soft_delete
|
||||
@variant = scope.find(params[:variant_id])
|
||||
authorize! :delete, @variant
|
||||
|
||||
VariantDeleter.new.delete(@variant)
|
||||
render json: @variant, serializer: Api::VariantSerializer, status: 204
|
||||
end
|
||||
|
||||
def destroy
|
||||
authorize! :delete, Spree::Variant
|
||||
@variant = scope.find(params[:id])
|
||||
@variant.destroy
|
||||
render json: @variant, serializer: Api::VariantSerializer, status: 204
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def product
|
||||
@product ||= Spree::Product.find_by_permalink(params[:product_id]) if params[:product_id]
|
||||
end
|
||||
|
||||
def scope
|
||||
if @product
|
||||
unless current_api_user.has_spree_role?("admin") || params[:show_deleted]
|
||||
variants = @product.variants_including_master
|
||||
else
|
||||
variants = @product.variants_including_master.with_deleted
|
||||
end
|
||||
else
|
||||
variants = Spree::Variant.scoped
|
||||
if current_api_user.has_spree_role?("admin")
|
||||
unless params[:show_deleted]
|
||||
variants = Spree::Variant.active
|
||||
end
|
||||
else
|
||||
variants = variants.active
|
||||
end
|
||||
end
|
||||
variants
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,5 +1,6 @@
|
||||
require 'open_food_network/spree_api_key_loader'
|
||||
require 'open_food_network/referer_parser'
|
||||
require 'open_food_network/permissions'
|
||||
|
||||
Spree::Admin::ProductsController.class_eval do
|
||||
include OpenFoodNetwork::SpreeApiKeyLoader
|
||||
@@ -48,19 +49,13 @@ Spree::Admin::ProductsController.class_eval do
|
||||
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)
|
||||
|
||||
params[:filters] ||= {}
|
||||
bulk_index_query = params[:filters].reduce("") do |string, filter|
|
||||
"#{string}q[#{filter[:property][:db_column]}_#{filter[:predicate][:predicate]}]=#{filter[:value]};"
|
||||
end
|
||||
product_set = product_set_from_params(params)
|
||||
|
||||
# Ensure we're authorised to update all products
|
||||
product_set.collection.each { |p| authorize! :update, p }
|
||||
|
||||
if product_set.save
|
||||
redirect_to "/api/products/bulk_products?page=1;per_page=500;#{bulk_index_query}"
|
||||
redirect_to main_app.bulk_products_api_products_path( bulk_index_query(params) )
|
||||
else
|
||||
if product_set.errors.present?
|
||||
render json: { errors: product_set.errors }, status: :bad_request
|
||||
@@ -73,7 +68,8 @@ Spree::Admin::ProductsController.class_eval do
|
||||
protected
|
||||
|
||||
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
|
||||
# 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.
|
||||
# TODO: There has to be a better way!!!
|
||||
return @collection if @collection.present?
|
||||
@@ -108,14 +104,32 @@ Spree::Admin::ProductsController.class_eval do
|
||||
|
||||
private
|
||||
|
||||
def product_set_from_params(params)
|
||||
collection_hash = Hash[params[:products].each_with_index.map { |p, i| [i, p] }]
|
||||
Spree::ProductSet.new(collection_attributes: collection_hash)
|
||||
end
|
||||
|
||||
def bulk_index_query(params)
|
||||
params[:filters].to_h.merge(page: params[:page], per_page: params[:per_page])
|
||||
end
|
||||
|
||||
def load_form_data
|
||||
@producers = OpenFoodNetwork::Permissions.new(spree_current_user).managed_product_enterprises.is_primary_producer.by_name
|
||||
@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.
|
||||
options = [{ id: '0', name: '' }]
|
||||
product_import_dates_query.collect(&:import_date).
|
||||
map { |i| options.push(id: i.to_date, name: i.to_date.to_formatted_s(:long)) }
|
||||
|
||||
options
|
||||
end
|
||||
|
||||
def product_import_dates_query
|
||||
Spree::Variant.
|
||||
select('DISTINCT spree_variants.import_date').
|
||||
joins(:product).
|
||||
where('spree_products.supplier_id IN (?)', editable_enterprises.collect(&:id)).
|
||||
@@ -123,18 +137,14 @@ Spree::Admin::ProductsController.class_eval do
|
||||
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
|
||||
unless spree_current_user.admin? || params[:product][:product_properties_attributes].nil?
|
||||
names = Spree::Property.pluck(:name)
|
||||
params[:product][:product_properties_attributes].each do |key, property|
|
||||
params[:product][:product_properties_attributes].delete key unless names.include? property[:property_name]
|
||||
return if spree_current_user.admin? || params[:product][:product_properties_attributes].nil?
|
||||
names = Spree::Property.pluck(:name)
|
||||
params[:product][:product_properties_attributes].each do |key, property|
|
||||
unless names.include? property[:property_name]
|
||||
params[:product][:product_properties_attributes].delete key
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -149,12 +159,32 @@ Spree::Admin::ProductsController.class_eval do
|
||||
end
|
||||
|
||||
def set_stock_levels(product, on_hand, on_demand)
|
||||
variant = product.master
|
||||
if product.variants.any?
|
||||
variant = product.variants.first
|
||||
variant = product_variant(product)
|
||||
|
||||
begin
|
||||
variant.on_demand = on_demand if on_demand.present?
|
||||
variant.on_hand = on_hand.to_i if on_hand.present?
|
||||
rescue StandardError => error
|
||||
notify_bugsnag(error, product, variant)
|
||||
raise error
|
||||
end
|
||||
end
|
||||
|
||||
def notify_bugsnag(error, product, variant)
|
||||
Bugsnag.notify(error) do |report|
|
||||
report.add_tab(:product, product.attributes)
|
||||
report.add_tab(:product_error, product.errors.first) unless product.valid?
|
||||
report.add_tab(:variant, variant.attributes)
|
||||
report.add_tab(:variant_error, variant.errors.first) unless variant.valid?
|
||||
end
|
||||
end
|
||||
|
||||
def product_variant(product)
|
||||
if product.variants.any?
|
||||
product.variants.first
|
||||
else
|
||||
product.master
|
||||
end
|
||||
variant.on_demand = on_demand if on_demand.present?
|
||||
variant.on_hand = on_hand.to_i if on_hand.present?
|
||||
end
|
||||
|
||||
def set_product_master_variant_price_to_zero
|
||||
|
||||
130
app/controllers/spree/api/base_controller.rb
Normal file
130
app/controllers/spree/api/base_controller.rb
Normal file
@@ -0,0 +1,130 @@
|
||||
require_dependency 'spree/api/controller_setup'
|
||||
|
||||
module Spree
|
||||
module Api
|
||||
class BaseController < ActionController::Metal
|
||||
include Spree::Api::ControllerSetup
|
||||
include Spree::Core::ControllerHelpers::SSL
|
||||
include ::ActionController::Head
|
||||
|
||||
self.responder = Spree::Api::Responders::AppResponder
|
||||
|
||||
respond_to :json
|
||||
|
||||
attr_accessor :current_api_user
|
||||
|
||||
before_filter :set_content_type
|
||||
before_filter :check_for_user_or_api_key, :if => :requires_authentication?
|
||||
before_filter :authenticate_user
|
||||
after_filter :set_jsonp_format
|
||||
|
||||
rescue_from Exception, :with => :error_during_processing
|
||||
rescue_from CanCan::AccessDenied, :with => :unauthorized
|
||||
rescue_from ActiveRecord::RecordNotFound, :with => :not_found
|
||||
|
||||
helper Spree::Api::ApiHelpers
|
||||
|
||||
ssl_allowed
|
||||
|
||||
def set_jsonp_format
|
||||
if params[:callback] && request.get?
|
||||
self.response_body = "#{params[:callback]}(#{response_body})"
|
||||
headers["Content-Type"] = 'application/javascript'
|
||||
end
|
||||
end
|
||||
|
||||
def map_nested_attributes_keys(klass, attributes)
|
||||
nested_keys = klass.nested_attributes_options.keys
|
||||
attributes.inject({}) do |h, (k, v)|
|
||||
key = nested_keys.include?(k.to_sym) ? "#{k}_attributes" : k
|
||||
h[key] = v
|
||||
h
|
||||
end.with_indifferent_access
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_content_type
|
||||
content_type = case params[:format]
|
||||
when "json"
|
||||
"application/json"
|
||||
when "xml"
|
||||
"text/xml"
|
||||
end
|
||||
headers["Content-Type"] = content_type
|
||||
end
|
||||
|
||||
def check_for_user_or_api_key
|
||||
# User is already authenticated with Spree, make request this way instead.
|
||||
return true if @current_api_user = try_spree_current_user ||
|
||||
!requires_authentication?
|
||||
|
||||
return if api_key.present?
|
||||
render("spree/api/errors/must_specify_api_key", status: :unauthorized) && return
|
||||
end
|
||||
|
||||
def authenticate_user
|
||||
return if @current_api_user
|
||||
|
||||
if requires_authentication? || api_key.present?
|
||||
unless @current_api_user = Spree.user_class.find_by_spree_api_key(api_key.to_s)
|
||||
render("spree/api/errors/invalid_api_key", status: :unauthorized) && return
|
||||
end
|
||||
else
|
||||
# An anonymous user
|
||||
@current_api_user = Spree.user_class.new
|
||||
end
|
||||
end
|
||||
|
||||
def unauthorized
|
||||
render("spree/api/errors/unauthorized", status: :unauthorized) && return
|
||||
end
|
||||
|
||||
def error_during_processing(exception)
|
||||
render(text: { exception: exception.message }.to_json,
|
||||
status: :unprocessable_entity) && return
|
||||
end
|
||||
|
||||
def requires_authentication?
|
||||
true
|
||||
end
|
||||
|
||||
def not_found
|
||||
render("spree/api/errors/not_found", status: :not_found) && return
|
||||
end
|
||||
|
||||
def current_ability
|
||||
Spree::Ability.new(current_api_user)
|
||||
end
|
||||
|
||||
def invalid_resource!(resource)
|
||||
@resource = resource
|
||||
render "spree/api/errors/invalid_resource", status: :unprocessable_entity
|
||||
end
|
||||
|
||||
def api_key
|
||||
request.headers["X-Spree-Token"] || params[:token]
|
||||
end
|
||||
helper_method :api_key
|
||||
|
||||
def find_product(id)
|
||||
product_scope.find_by_permalink!(id.to_s)
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
product_scope.find(id)
|
||||
end
|
||||
|
||||
def product_scope
|
||||
if current_api_user.has_spree_role?("admin")
|
||||
scope = Product
|
||||
if params[:show_deleted]
|
||||
scope = scope.with_deleted
|
||||
end
|
||||
else
|
||||
scope = Product.active
|
||||
end
|
||||
|
||||
scope.includes(:master)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,13 +0,0 @@
|
||||
Spree::Api::LineItemsController.class_eval do
|
||||
around_filter :apply_enterprise_fees_with_lock, only: :update
|
||||
|
||||
private
|
||||
|
||||
def apply_enterprise_fees_with_lock
|
||||
authorize! :read, order
|
||||
order.with_lock do
|
||||
yield
|
||||
order.update_distribution_charge!
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,86 +0,0 @@
|
||||
require 'open_food_network/permissions'
|
||||
|
||||
Spree::Api::ProductsController.class_eval do
|
||||
def managed
|
||||
authorize! :admin, Spree::Product
|
||||
authorize! :read, Spree::Product
|
||||
|
||||
@products = product_scope.ransack(params[:q]).result.managed_by(current_api_user).page(params[:page]).per(params[:per_page])
|
||||
respond_with(@products, default_template: :index)
|
||||
end
|
||||
|
||||
# TODO: This should be named 'managed'. Is the action above used? Maybe we should remove it.
|
||||
def bulk_products
|
||||
@products = OpenFoodNetwork::Permissions.new(current_api_user).editable_products.
|
||||
merge(product_scope).
|
||||
order('created_at DESC').
|
||||
ransack(params[:q]).result.
|
||||
page(params[:page]).per(params[:per_page])
|
||||
|
||||
render_paged_products @products
|
||||
end
|
||||
|
||||
def overridable
|
||||
producers = OpenFoodNetwork::Permissions.new(current_api_user).
|
||||
variant_override_producers.by_name
|
||||
|
||||
@products = paged_products_for_producers producers
|
||||
|
||||
render_paged_products @products
|
||||
end
|
||||
|
||||
def soft_delete
|
||||
authorize! :delete, Spree::Product
|
||||
@product = find_product(params[:product_id])
|
||||
authorize! :delete, @product
|
||||
@product.destroy
|
||||
respond_with(@product, status: 204)
|
||||
end
|
||||
|
||||
# POST /api/products/:product_id/clone
|
||||
#
|
||||
def clone
|
||||
authorize! :create, Spree::Product
|
||||
original_product = find_product(params[:product_id])
|
||||
authorize! :update, original_product
|
||||
|
||||
@product = original_product.duplicate
|
||||
|
||||
respond_with(@product, status: 201, default_template: :show)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Copied and modified from Spree::Api::BaseController to allow
|
||||
# enterprise users to access inactive products
|
||||
def product_scope
|
||||
if current_api_user.has_spree_role?("admin") || current_api_user.enterprises.present? # This line modified
|
||||
scope = Spree::Product
|
||||
if params[:show_deleted]
|
||||
scope = scope.with_deleted
|
||||
end
|
||||
else
|
||||
scope = Spree::Product.active
|
||||
end
|
||||
|
||||
scope.includes(:master)
|
||||
end
|
||||
|
||||
def paged_products_for_producers(producers)
|
||||
Spree::Product.scoped.
|
||||
merge(product_scope).
|
||||
where(supplier_id: producers).
|
||||
by_producer.by_name.
|
||||
ransack(params[:q]).result.
|
||||
page(params[:page]).per(params[:per_page])
|
||||
end
|
||||
|
||||
def render_paged_products(products)
|
||||
serializer = ActiveModel::ArraySerializer.new(
|
||||
products,
|
||||
each_serializer: Api::Admin::ProductSerializer
|
||||
)
|
||||
|
||||
render text: { products: serializer, pages: products.num_pages }.to_json
|
||||
end
|
||||
end
|
||||
108
app/controllers/spree/api/shipments_controller.rb
Normal file
108
app/controllers/spree/api/shipments_controller.rb
Normal file
@@ -0,0 +1,108 @@
|
||||
require 'open_food_network/scope_variant_to_hub'
|
||||
|
||||
module Spree
|
||||
module Api
|
||||
class ShipmentsController < Spree::Api::BaseController
|
||||
respond_to :json
|
||||
|
||||
before_filter :find_order
|
||||
before_filter :find_and_update_shipment, only: [:ship, :ready, :add, :remove]
|
||||
|
||||
def create
|
||||
variant = scoped_variant(params[:variant_id])
|
||||
quantity = params[:quantity].to_i
|
||||
@shipment = get_or_create_shipment(params[:stock_location_id])
|
||||
|
||||
@order.contents.add(variant, quantity, nil, @shipment)
|
||||
|
||||
@shipment.refresh_rates
|
||||
@shipment.save!
|
||||
|
||||
respond_with(@shipment.reload, default_template: :show)
|
||||
end
|
||||
|
||||
def update
|
||||
authorize! :read, Shipment
|
||||
@shipment = @order.shipments.find_by_number!(params[:id])
|
||||
params[:shipment] ||= []
|
||||
unlock = params[:shipment].delete(:unlock)
|
||||
|
||||
if unlock == 'yes'
|
||||
@shipment.adjustment.open
|
||||
end
|
||||
|
||||
@shipment.update_attributes(params[:shipment])
|
||||
|
||||
if unlock == 'yes'
|
||||
@shipment.adjustment.close
|
||||
end
|
||||
|
||||
@shipment.reload
|
||||
respond_with(@shipment, default_template: :show)
|
||||
end
|
||||
|
||||
def ready
|
||||
authorize! :read, Shipment
|
||||
unless @shipment.ready?
|
||||
if @shipment.can_ready?
|
||||
@shipment.ready!
|
||||
else
|
||||
render "spree/api/shipments/cannot_ready_shipment", status: :unprocessable_entity
|
||||
return
|
||||
end
|
||||
end
|
||||
respond_with(@shipment, default_template: :show)
|
||||
end
|
||||
|
||||
def ship
|
||||
authorize! :read, Shipment
|
||||
unless @shipment.shipped?
|
||||
@shipment.ship!
|
||||
end
|
||||
respond_with(@shipment, default_template: :show)
|
||||
end
|
||||
|
||||
def add
|
||||
variant = scoped_variant(params[:variant_id])
|
||||
quantity = params[:quantity].to_i
|
||||
|
||||
@order.contents.add(variant, quantity, nil, @shipment)
|
||||
|
||||
respond_with(@shipment, default_template: :show)
|
||||
end
|
||||
|
||||
def remove
|
||||
variant = scoped_variant(params[:variant_id])
|
||||
quantity = params[:quantity].to_i
|
||||
|
||||
@order.contents.remove(variant, quantity, @shipment)
|
||||
@shipment.reload if @shipment.persisted?
|
||||
|
||||
respond_with(@shipment, default_template: :show)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def find_order
|
||||
@order = Spree::Order.find_by_number!(params[:order_id])
|
||||
authorize! :read, @order
|
||||
end
|
||||
|
||||
def find_and_update_shipment
|
||||
@shipment = @order.shipments.find_by_number!(params[:id])
|
||||
@shipment.update_attributes(params[:shipment])
|
||||
@shipment.reload
|
||||
end
|
||||
|
||||
def scoped_variant(variant_id)
|
||||
variant = Spree::Variant.find(variant_id)
|
||||
OpenFoodNetwork::ScopeVariantToHub.new(@order.distributor).scope(variant)
|
||||
variant
|
||||
end
|
||||
|
||||
def get_or_create_shipment(stock_location_id)
|
||||
@order.shipment || @order.shipments.create(stock_location_id: stock_location_id)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,47 +0,0 @@
|
||||
require 'open_food_network/scope_variant_to_hub'
|
||||
|
||||
Spree::Api::ShipmentsController.class_eval do
|
||||
def create
|
||||
variant = scoped_variant(params[:variant_id])
|
||||
quantity = params[:quantity].to_i
|
||||
@shipment = get_or_create_shipment(params[:stock_location_id])
|
||||
|
||||
@order.contents.add(variant, quantity, nil, @shipment)
|
||||
|
||||
@shipment.refresh_rates
|
||||
@shipment.save!
|
||||
|
||||
respond_with(@shipment.reload, default_template: :show)
|
||||
end
|
||||
|
||||
def add
|
||||
variant = scoped_variant(params[:variant_id])
|
||||
quantity = params[:quantity].to_i
|
||||
|
||||
@order.contents.add(variant, quantity, nil, @shipment)
|
||||
|
||||
respond_with(@shipment, default_template: :show)
|
||||
end
|
||||
|
||||
def remove
|
||||
variant = scoped_variant(params[:variant_id])
|
||||
quantity = params[:quantity].to_i
|
||||
|
||||
@order.contents.remove(variant, quantity, @shipment)
|
||||
@shipment.reload if @shipment.persisted?
|
||||
|
||||
respond_with(@shipment, default_template: :show)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def scoped_variant(variant_id)
|
||||
variant = Spree::Variant.find(variant_id)
|
||||
OpenFoodNetwork::ScopeVariantToHub.new(@order.distributor).scope(variant)
|
||||
variant
|
||||
end
|
||||
|
||||
def get_or_create_shipment(stock_location_id)
|
||||
@order.shipment || @order.shipments.create(stock_location_id: stock_location_id)
|
||||
end
|
||||
end
|
||||
75
app/controllers/spree/api/taxons_controller.rb
Normal file
75
app/controllers/spree/api/taxons_controller.rb
Normal file
@@ -0,0 +1,75 @@
|
||||
module Spree
|
||||
module Api
|
||||
class TaxonsController < Spree::Api::BaseController
|
||||
respond_to :json
|
||||
|
||||
def index
|
||||
if taxonomy
|
||||
@taxons = taxonomy.root.children
|
||||
else
|
||||
if params[:ids]
|
||||
@taxons = Taxon.where(id: params[:ids].split(","))
|
||||
else
|
||||
@taxons = Taxon.ransack(params[:q]).result
|
||||
end
|
||||
end
|
||||
respond_with(@taxons)
|
||||
end
|
||||
|
||||
def show
|
||||
@taxon = taxon
|
||||
respond_with(@taxon)
|
||||
end
|
||||
|
||||
def jstree
|
||||
show
|
||||
end
|
||||
|
||||
def create
|
||||
authorize! :create, Taxon
|
||||
@taxon = Taxon.new(params[:taxon])
|
||||
@taxon.taxonomy_id = params[:taxonomy_id]
|
||||
taxonomy = Taxonomy.find_by_id(params[:taxonomy_id])
|
||||
|
||||
if taxonomy.nil?
|
||||
@taxon.errors[:taxonomy_id] = I18n.t(:invalid_taxonomy_id, scope: 'spree.api')
|
||||
invalid_resource!(@taxon) && return
|
||||
end
|
||||
|
||||
@taxon.parent_id = taxonomy.root.id unless params[:taxon][:parent_id]
|
||||
|
||||
if @taxon.save
|
||||
respond_with(@taxon, status: 201, default_template: :show)
|
||||
else
|
||||
invalid_resource!(@taxon)
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
authorize! :update, Taxon
|
||||
if taxon.update_attributes(params[:taxon])
|
||||
respond_with(taxon, status: 200, default_template: :show)
|
||||
else
|
||||
invalid_resource!(taxon)
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
authorize! :delete, Taxon
|
||||
taxon.destroy
|
||||
respond_with(taxon, status: 204)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def taxonomy
|
||||
return if params[:taxonomy_id].blank?
|
||||
@taxonomy ||= Taxonomy.find(params[:taxonomy_id])
|
||||
end
|
||||
|
||||
def taxon
|
||||
@taxon ||= taxonomy.taxons.find(params[:id])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,9 +0,0 @@
|
||||
Spree::Api::VariantsController.class_eval do
|
||||
def soft_delete
|
||||
@variant = scope.find(params[:variant_id])
|
||||
authorize! :delete, @variant
|
||||
|
||||
VariantDeleter.new.delete(@variant)
|
||||
respond_with @variant, status: 204
|
||||
end
|
||||
end
|
||||
@@ -29,7 +29,7 @@ module Spree
|
||||
|
||||
def load_order
|
||||
@order = current_order
|
||||
redirect_to main_app.cart_path && return unless @order
|
||||
redirect_to(main_app.cart_path) && return unless @order
|
||||
|
||||
if params[:state]
|
||||
redirect_to checkout_state_path(@order.state) if @order.can_go_to_state?(params[:state])
|
||||
|
||||
@@ -18,6 +18,7 @@ module Spree
|
||||
klass = Spree::Order if klass == :bulk_order_management
|
||||
klass = EnterpriseGroup if klass == :group
|
||||
klass = VariantOverride if klass == :Inventory
|
||||
klass = ProductImport::ProductImporter if klass == :import
|
||||
klass = Spree::Admin::ReportsController if klass == :report
|
||||
klass
|
||||
end
|
||||
|
||||
120
app/helpers/spree/api/api_helpers.rb
Normal file
120
app/helpers/spree/api/api_helpers.rb
Normal file
@@ -0,0 +1,120 @@
|
||||
module Spree
|
||||
module Api
|
||||
module ApiHelpers
|
||||
def required_fields_for(model)
|
||||
required_fields = model._validators.select do |_field, validations|
|
||||
validations.any? { |v| v.is_a?(ActiveModel::Validations::PresenceValidator) }
|
||||
end.map(&:first) # get fields that are invalid
|
||||
# Permalinks presence is validated, but are really automatically generated
|
||||
# Therefore we shouldn't tell API clients that they MUST send one through
|
||||
required_fields.map!(&:to_s).delete("permalink")
|
||||
required_fields
|
||||
end
|
||||
|
||||
def product_attributes
|
||||
[:id, :name, :description, :price, :available_on, :permalink, :meta_description,
|
||||
:meta_keywords, :shipping_category_id, :taxon_ids]
|
||||
end
|
||||
|
||||
def product_property_attributes
|
||||
[:id, :product_id, :property_id, :value, :property_name]
|
||||
end
|
||||
|
||||
def variant_attributes
|
||||
[:id, :name, :sku, :price, :weight, :height, :width, :depth,
|
||||
:is_master, :cost_price, :permalink]
|
||||
end
|
||||
|
||||
def image_attributes
|
||||
[:id, :position, :attachment_content_type, :attachment_file_name,
|
||||
:type, :attachment_updated_at, :attachment_width, :attachment_height, :alt]
|
||||
end
|
||||
|
||||
def option_value_attributes
|
||||
[:id, :name, :presentation, :option_type_name, :option_type_id]
|
||||
end
|
||||
|
||||
def order_attributes
|
||||
[:id, :number, :item_total, :total, :state, :adjustment_total, :user_id,
|
||||
:created_at, :updated_at, :completed_at, :payment_total,
|
||||
:shipment_state, :payment_state, :email, :special_instructions, :token]
|
||||
end
|
||||
|
||||
def line_item_attributes
|
||||
[:id, :quantity, :price, :variant_id]
|
||||
end
|
||||
|
||||
def option_type_attributes
|
||||
[:id, :name, :presentation, :position]
|
||||
end
|
||||
|
||||
def payment_attributes
|
||||
[:id, :source_type, :source_id, :amount, :payment_method_id,
|
||||
:response_code, :state, :avs_response, :created_at, :updated_at]
|
||||
end
|
||||
|
||||
def payment_method_attributes
|
||||
[:id, :name, :description]
|
||||
end
|
||||
|
||||
def shipment_attributes
|
||||
[:id, :tracking, :number, :cost, :shipped_at, :state]
|
||||
end
|
||||
|
||||
def taxonomy_attributes
|
||||
[:id, :name]
|
||||
end
|
||||
|
||||
def taxon_attributes
|
||||
[:id, :name, :pretty_name, :permalink, :position, :parent_id, :taxonomy_id]
|
||||
end
|
||||
|
||||
def inventory_unit_attributes
|
||||
[:id, :lock_version, :state, :variant_id, :shipment_id, :return_authorization_id]
|
||||
end
|
||||
|
||||
def return_authorization_attributes
|
||||
[:id, :number, :state, :amount, :order_id, :reason, :created_at, :updated_at]
|
||||
end
|
||||
|
||||
def country_attributes
|
||||
[:id, :iso_name, :iso, :iso3, :name, :numcode]
|
||||
end
|
||||
|
||||
def state_attributes
|
||||
[:id, :name, :abbr, :country_id]
|
||||
end
|
||||
|
||||
def adjustment_attributes
|
||||
[:id, :source_type, :source_id, :adjustable_type, :adjustable_id, :originator_type,
|
||||
:originator_id, :amount, :label, :mandatory, :locked, :eligible, :created_at, :updated_at]
|
||||
end
|
||||
|
||||
def creditcard_attributes
|
||||
[:id, :month, :year, :cc_type, :last_digits, :first_name, :last_name,
|
||||
:gateway_customer_profile_id, :gateway_payment_profile_id]
|
||||
end
|
||||
|
||||
def user_attributes
|
||||
[:id, :email, :created_at, :updated_at]
|
||||
end
|
||||
|
||||
def property_attributes
|
||||
[:id, :name, :presentation]
|
||||
end
|
||||
|
||||
def stock_location_attributes
|
||||
[:id, :name, :address1, :address2, :city,
|
||||
:state_id, :state_name, :country_id, :zipcode, :phone, :active]
|
||||
end
|
||||
|
||||
def stock_movement_attributes
|
||||
[:id, :quantity, :stock_item_id]
|
||||
end
|
||||
|
||||
def stock_item_attributes
|
||||
[:id, :count_on_hand, :backorderable, :lock_version, :stock_location_id, :variant_id]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -59,7 +59,7 @@ class SubscriptionPlacementJob
|
||||
end
|
||||
|
||||
def move_to_completion(order)
|
||||
until order.completed? do order.next! end
|
||||
AdvanceOrderService.new(order).call!
|
||||
end
|
||||
|
||||
def unavailable_stock_lines_for(order)
|
||||
|
||||
@@ -90,7 +90,6 @@ class Enterprise < ActiveRecord::Base
|
||||
validates :permalink, uniqueness: true, presence: true
|
||||
validate :shopfront_taxons
|
||||
validate :enforce_ownership_limit, if: lambda { owner_id_changed? && !owner_id.nil? }
|
||||
validates :description, length: { maximum: 255 }
|
||||
|
||||
before_validation :initialize_permalink, if: lambda { permalink.nil? }
|
||||
before_validation :ensure_owner_is_manager, if: lambda { owner_id_changed? && !owner_id.nil? }
|
||||
@@ -464,9 +463,11 @@ class Enterprise < ActiveRecord::Base
|
||||
self.permalink = Enterprise.find_available_permalink(name)
|
||||
end
|
||||
|
||||
# Touch distributors without them touching their distributors.
|
||||
# We avoid an infinite loop and don't need to touch the whole distributor tree.
|
||||
def touch_distributors
|
||||
Enterprise.distributing_products(supplied_products.select(:id)).
|
||||
where('enterprises.id != ?', id).
|
||||
find_each(&:touch)
|
||||
update_all(updated_at: Time.zone.now)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
# 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?
|
||||
user.superadmin?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :user
|
||||
end
|
||||
@@ -4,6 +4,12 @@ module Spree
|
||||
|
||||
private
|
||||
|
||||
def check_price
|
||||
if currency.nil?
|
||||
self.currency = Spree::Config[:currency]
|
||||
end
|
||||
end
|
||||
|
||||
def refresh_products_cache
|
||||
variant.andand.refresh_products_cache
|
||||
end
|
||||
|
||||
@@ -56,6 +56,12 @@ Spree::Product.class_eval do
|
||||
ON (o_order_cycles.id = o_exchanges.order_cycle_id)")
|
||||
}
|
||||
|
||||
scope :imported_on, lambda { |import_date|
|
||||
import_date = Time.zone.parse import_date if import_date.is_a? String
|
||||
import_date = import_date.to_date
|
||||
joins(:variants).merge(Spree::Variant.where(import_date: import_date.beginning_of_day..import_date.end_of_day))
|
||||
}
|
||||
|
||||
scope :with_order_cycles_inner, -> {
|
||||
joins(variants_including_master: { exchanges: :order_cycle })
|
||||
}
|
||||
|
||||
@@ -17,9 +17,7 @@ class Spree::ProductSet < ModelSet
|
||||
# variant.update_attributes( { price: xx.x } )
|
||||
#
|
||||
def update_attributes(attributes)
|
||||
if attributes[:taxon_ids].present?
|
||||
attributes[:taxon_ids] = attributes[:taxon_ids].split(',')
|
||||
end
|
||||
split_taxon_ids!(attributes)
|
||||
|
||||
found_model = @collection.find do |model|
|
||||
model.id.to_s == attributes[:id].to_s && model.persisted?
|
||||
@@ -28,12 +26,20 @@ class Spree::ProductSet < ModelSet
|
||||
if found_model.nil?
|
||||
@klass.new(attributes).save unless @reject_if.andand.call(attributes)
|
||||
else
|
||||
update_product_only_attributes(found_model, attributes) &&
|
||||
update_product_variants(found_model, attributes) &&
|
||||
update_product_master(found_model, attributes)
|
||||
update_product(found_model, attributes)
|
||||
end
|
||||
end
|
||||
|
||||
def split_taxon_ids!(attributes)
|
||||
attributes[:taxon_ids] = attributes[:taxon_ids].split(',') if attributes[:taxon_ids].present?
|
||||
end
|
||||
|
||||
def update_product(found_model, attributes)
|
||||
update_product_only_attributes(found_model, attributes) &&
|
||||
update_product_variants(found_model, attributes) &&
|
||||
update_product_master(found_model, attributes)
|
||||
end
|
||||
|
||||
def update_product_only_attributes(product, attributes)
|
||||
variant_related_attrs = [:id, :variants_attributes, :master_attributes]
|
||||
product_related_attrs = attributes.except(*variant_related_attrs)
|
||||
@@ -90,8 +96,23 @@ class Spree::ProductSet < ModelSet
|
||||
|
||||
variant = product.variants.create(variant_attributes)
|
||||
|
||||
variant.on_demand = on_demand if on_demand.present?
|
||||
variant.on_hand = on_hand.to_i if on_hand.present?
|
||||
begin
|
||||
variant.on_demand = on_demand if on_demand.present?
|
||||
variant.on_hand = on_hand.to_i if on_hand.present?
|
||||
rescue StandardError => error
|
||||
notify_bugsnag(error, product, variant, variant_attributes)
|
||||
raise error
|
||||
end
|
||||
end
|
||||
|
||||
def notify_bugsnag(error, product, variant, variant_attributes)
|
||||
Bugsnag.notify(error) do |report|
|
||||
report.add_tab(:product, product.attributes)
|
||||
report.add_tab(:product_error, product.errors.first) unless product.valid?
|
||||
report.add_tab(:variant_attributes, variant_attributes)
|
||||
report.add_tab(:variant, variant.attributes)
|
||||
report.add_tab(:variant_error, variant.errors.first) unless variant.valid?
|
||||
end
|
||||
end
|
||||
|
||||
def collection_attributes=(attributes)
|
||||
|
||||
10
app/models/spree/stock_movement_decorator.rb
Normal file
10
app/models/spree/stock_movement_decorator.rb
Normal file
@@ -0,0 +1,10 @@
|
||||
Spree::StockMovement.class_eval do
|
||||
after_save :refresh_products_cache
|
||||
|
||||
private
|
||||
|
||||
def refresh_products_cache
|
||||
return if stock_item.variant.blank?
|
||||
OpenFoodNetwork::ProductsCache.variant_changed stock_item.variant
|
||||
end
|
||||
end
|
||||
@@ -136,6 +136,16 @@ module Spree
|
||||
has_spree_role?('admin')
|
||||
end
|
||||
|
||||
def generate_spree_api_key!
|
||||
self.spree_api_key = SecureRandom.hex(24)
|
||||
save!
|
||||
end
|
||||
|
||||
def clear_spree_api_key!
|
||||
self.spree_api_key = nil
|
||||
save!
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def password_required?
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
/ replace_contents "td.property_name"
|
||||
|
||||
- if spree_current_user.admin?
|
||||
= f.text_field :property_name, :class => 'autocomplete'
|
||||
- else
|
||||
= f.select :property_name, @properties, { :include_blank => true }, { class: 'select2 fullwidth' }
|
||||
@@ -1,26 +0,0 @@
|
||||
/ insert_after 'table.index.sortable'
|
||||
|
||||
=f.check_box :inherits_properties
|
||||
=f.label :inherits_properties, t(".inherits_properties_checkbox_hint", supplier: @product.supplier.name)
|
||||
%br
|
||||
%br
|
||||
|
||||
#inherited_properties
|
||||
%table.index
|
||||
%thead
|
||||
%tr{"data-hook" => "producer_properties_header"}
|
||||
%th= t('admin.products.properties.inherited_property')
|
||||
%th= t('admin.description')
|
||||
%th.actions
|
||||
%tbody#producer_properties{"data-hook" => ""}
|
||||
- @product.supplier.producer_properties.each do |producer_property|
|
||||
%tr
|
||||
%td= producer_property.property.presentation
|
||||
%td= producer_property.value
|
||||
%td.actions
|
||||
|
||||
:coffee
|
||||
$(document).ready ->
|
||||
$("#inherited_properties").toggle $("input#product_inherits_properties").is(':checked')
|
||||
$("input#product_inherits_properties").change ->
|
||||
$("#inherited_properties").toggle $(this).is(':checked')
|
||||
@@ -1,5 +0,0 @@
|
||||
/ replace "tr[data-hook='product_properties_header']"
|
||||
%tr{"data-hook" => "product_properties_header"}
|
||||
%th= t('admin.products.properties.property_name')
|
||||
%th= t('admin.description')
|
||||
%th.actions
|
||||
@@ -1,2 +0,0 @@
|
||||
add_to_attributes '[data-hook="admin_variant_form_fields"]'
|
||||
attributes 'ng-app' => 'admin.products'
|
||||
@@ -1,25 +0,0 @@
|
||||
/ insert_top "[data-hook='admin_variant_form_fields']"
|
||||
|
||||
.field
|
||||
= f.label :display_name, t(:display_name)
|
||||
= f.text_field :display_name, class: "fullwidth"
|
||||
.field
|
||||
= f.label :display_as, t(:display_as)
|
||||
= f.text_field :display_as, class: "fullwidth"
|
||||
|
||||
- if product_has_variant_unit_option_type?(@product)
|
||||
- if @product.variant_unit != 'items'
|
||||
.field{"data-hook" => "unit_value", 'ng-controller' => 'variantUnitsCtrl'}
|
||||
= f.label :unit_value, "#{t('admin.'+@product.variant_unit)} ({{unitName(#{@product.variant_unit_scale}, '#{@product.variant_unit}')}})"
|
||||
= hidden_field_tag 'product_variant_unit_scale', @product.variant_unit_scale
|
||||
= text_field_tag :unit_value_human, nil, {class: "fullwidth", 'ng-model' => 'unit_value_human', 'ng-change' => 'updateValue()'}
|
||||
= f.text_field :unit_value, {hidden: true, 'ng-value' => 'unit_value'}
|
||||
|
||||
.field{"data-hook" => "unit_description"}
|
||||
= f.label :unit_description, t(:spree_admin_unit_description)
|
||||
= f.text_field :unit_description, class: "fullwidth", placeholder: t('admin.products.unit_name_placeholder')
|
||||
|
||||
:javascript
|
||||
angular.element(document.getElementById("new_variant")).ready(function() {
|
||||
angular.bootstrap(document.getElementById("new_variant"), ['admin.products']);
|
||||
});
|
||||
@@ -1,11 +0,0 @@
|
||||
/ insert_bottom "[data-hook='admin_variant_form_fields']"
|
||||
|
||||
- if Spree::Config[:track_inventory_levels]
|
||||
.field.checkbox
|
||||
%label
|
||||
= f.check_box :on_demand
|
||||
= t(:on_demand)
|
||||
.field
|
||||
= f.label :on_hand, t(:on_hand)
|
||||
.fullwidth
|
||||
= f.text_field :on_hand
|
||||
@@ -1,10 +0,0 @@
|
||||
/ replace "[data-hook='presentation']"
|
||||
|
||||
- unless variant_unit_option_type?(option_type)
|
||||
.field{"data-hook" => "presentation"}
|
||||
= label :new_variant, option_type.presentation
|
||||
- if @variant.new_record?
|
||||
= select(:new_variant, option_type.presentation, option_type.option_values.collect {|ov| [ ov.presentation, ov.id ] }, {}, {:class => 'select2 fullwidth'})
|
||||
- else
|
||||
- if opt = @variant.option_values.detect {|o| o.option_type == option_type }.try(:presentation)
|
||||
= text_field(:new_variant, option_type.presentation, :value => opt, :disabled => 'disabled', :class => 'fullwidth')
|
||||
@@ -1,33 +0,0 @@
|
||||
/ insert_bottom "[data-hook='admin_variant_form_fields']"
|
||||
:javascript
|
||||
|
||||
$(document).ready(function() {
|
||||
|
||||
var on_demand = $('input#variant_on_demand');
|
||||
var on_hand = $('input#variant_on_hand');
|
||||
|
||||
disableOnHandIfOnDemand = function() {
|
||||
on_demand_checked = on_demand.attr('checked')
|
||||
if ( on_demand_checked == undefined )
|
||||
on_demand_checked = false;
|
||||
|
||||
on_hand.attr('disabled', on_demand_checked);
|
||||
if(on_demand_checked) {
|
||||
on_hand.attr('data-stock', on_hand.val());
|
||||
on_hand.val(t('admin.products.variants.infinity'));
|
||||
}
|
||||
}
|
||||
|
||||
disableOnHandIfOnDemand();
|
||||
|
||||
on_demand.change(function(){
|
||||
disableOnHandIfOnDemand();
|
||||
if(!this.checked) {
|
||||
if(on_hand.attr('data-stock') !== undefined) {
|
||||
on_hand.val(on_hand.attr('data-stock'));
|
||||
} else {
|
||||
on_hand.val("0");
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,3 +0,0 @@
|
||||
/ insert_bottom "[data-hook='on_demand']"
|
||||
%div{'ofn-with-tip' => t('admin.products.variants.to_order_tip')}
|
||||
%a= t('admin.whats_this')
|
||||
@@ -1,14 +0,0 @@
|
||||
/ replace "[data-hook='admin_variant_form_additional_fields']"
|
||||
|
||||
.right.six.columns.omega.label-block{"data-hook" => "admin_variant_form_additional_fields"}
|
||||
- if @product.variant_unit != 'weight'
|
||||
.field{"data-hook" => 'weight'}
|
||||
= f.label 'weight', t('weight')+' (kg)'
|
||||
- value = number_with_precision(@variant.weight, :precision => 2)
|
||||
= f.text_field 'weight', :value => value, :class => 'fullwidth'
|
||||
|
||||
- [:height, :width, :depth].each do |field|
|
||||
.field{"data-hook" => field}
|
||||
= f.label field, t(field)
|
||||
- value = number_with_precision(@variant.send(field), :precision => 2)
|
||||
= f.text_field field, :value => value, :class => 'fullwidth'
|
||||
@@ -1,3 +0,0 @@
|
||||
/ replace "code[erb-loud]:contains('variant.options_text')"
|
||||
|
||||
= variant.full_name
|
||||
@@ -1,6 +1,8 @@
|
||||
class Api::VariantSerializer < ActiveModel::Serializer
|
||||
attributes :id, :is_master, :on_hand, :name_to_display, :unit_to_display, :unit_value
|
||||
attributes :options_text, :on_demand, :price, :fees, :price_with_fees, :product_name
|
||||
attributes :id, :is_master, :product_name, :sku
|
||||
attributes :options_text, :unit_value, :unit_description, :unit_to_display
|
||||
attributes :display_as, :display_name, :name_to_display
|
||||
attributes :price, :on_demand, :on_hand, :fees, :price_with_fees
|
||||
attributes :tag_list
|
||||
|
||||
delegate :price, to: :object
|
||||
|
||||
@@ -5,4 +5,11 @@
|
||||
|
||||
= render 'upload_sidebar'
|
||||
|
||||
%h5
|
||||
= t('.notice')
|
||||
%br
|
||||
%p
|
||||
= t('.beta_notice')
|
||||
%br
|
||||
|
||||
= render 'upload_form'
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
object @enterprise
|
||||
|
||||
attributes :id, :name
|
||||
@@ -1,5 +1,5 @@
|
||||
.per-page{'ng-show' => '!RequestMonitor.loading && orders.length > 0'}
|
||||
%input.per-page-select.ofn-select2{type: 'number', data: 'per_page_options', 'ng-model' => 'per_page', 'ng-change' => 'fetchResults()'}
|
||||
%input.per-page-select.ofn-select2{type: 'number', data: 'per_page_options', 'min-search' => 999, 'ng-model' => 'per_page', 'ng-change' => 'fetchResults()'}
|
||||
|
||||
%span.per-page-feedback
|
||||
{{ 'spree.admin.orders.index.results_found' | t:{number: pagination.results} }}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
%tr.product_property.fields{"data-hook" => "product_property", id: "spree_#{dom_id(f.object)}"}
|
||||
%td.no-border
|
||||
%span.handle
|
||||
= f.hidden_field :id
|
||||
%td.property_name
|
||||
- if spree_current_user.admin?
|
||||
= f.text_field :property_name, class: 'autocomplete'
|
||||
- else
|
||||
= f.select :property_name, @properties, { include_blank: true }, { class: 'select2 fullwidth' }
|
||||
%td.value
|
||||
= f.text_field :value, class: 'autocomplete'
|
||||
%td.actions
|
||||
- if f.object.persisted?
|
||||
= link_to_delete f.object, no_text: true
|
||||
71
app/views/spree/admin/product_properties/index.html.haml
Normal file
71
app/views/spree/admin/product_properties/index.html.haml
Normal file
@@ -0,0 +1,71 @@
|
||||
= render partial: 'spree/admin/shared/product_sub_menu'
|
||||
|
||||
= render partial: 'spree/admin/shared/product_tabs', locals: { current: 'Product Properties' }
|
||||
|
||||
= render partial: 'spree/shared/error_messages', locals: { target: @product }
|
||||
|
||||
- content_for :page_actions do
|
||||
%ul.tollbar.inline-menu
|
||||
%li
|
||||
= link_to_add_fields Spree.t(:add_product_properties), 'tbody#product_properties', class: 'icon-plus button'
|
||||
%li
|
||||
%span#new_ptype_link
|
||||
= link_to Spree.t(:select_from_prototype), available_admin_prototypes_url, remote: true, 'data-update' => 'prototypes', class: 'button icon-copy'
|
||||
|
||||
= form_for @product, url: admin_product_url(@product), method: :put do |f|
|
||||
%fieldset.no-border-top
|
||||
.add_product_properties
|
||||
#prototypes
|
||||
= image_tag 'select2-spinner.gif', plugin: 'spree', style: 'display:none;', id: 'busy_indicator'
|
||||
|
||||
%table.index.sortable{"data-sortable-link" => update_positions_admin_product_product_properties_url}
|
||||
%thead
|
||||
%tr
|
||||
%th.no-border
|
||||
%th= t('admin.products.properties.property_name')
|
||||
%th= t('admin.description')
|
||||
%th.actions
|
||||
|
||||
%tbody#product_properties
|
||||
= f.fields_for :product_properties do |pp_form|
|
||||
= render partial: 'product_property_fields', locals: { f: pp_form }
|
||||
|
||||
= f.check_box :inherits_properties
|
||||
= f.label :inherits_properties, t(".inherits_properties_checkbox_hint", supplier: @product.supplier.name)
|
||||
%br
|
||||
%br
|
||||
|
||||
#inherited_properties
|
||||
%table.index
|
||||
%thead
|
||||
%tr
|
||||
%th= t('admin.products.properties.inherited_property')
|
||||
%th= t('admin.description')
|
||||
%th.actions
|
||||
%tbody#producer_properties
|
||||
- @product.supplier.producer_properties.each do |producer_property|
|
||||
%tr
|
||||
%td= producer_property.property.presentation
|
||||
%td= producer_property.value
|
||||
%td.actions
|
||||
|
||||
|
||||
= render partial: 'spree/admin/shared/edit_resource_links'
|
||||
|
||||
= hidden_field_tag 'clear_product_properties', 'true'
|
||||
|
||||
:coffee
|
||||
$(document).ready ->
|
||||
$("#inherited_properties").toggle $("input#product_inherits_properties").is(':checked')
|
||||
$("input#product_inherits_properties").change ->
|
||||
$("#inherited_properties").toggle $(this).is(':checked')
|
||||
|
||||
:javascript
|
||||
var properties = #{raw(@properties.to_json)};
|
||||
$("#product_properties input.autocomplete").live("keydown", function(){
|
||||
already_auto_completed = $(this).is('ac_input');
|
||||
if (!already_auto_completed) {
|
||||
$(this).autocomplete({source: properties});
|
||||
$(this).focus();
|
||||
}
|
||||
});
|
||||
@@ -4,7 +4,9 @@
|
||||
%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'
|
||||
|
||||
%div{'ng-show' => "!RequestMonitor.loading && products.length > 0" }
|
||||
= render partial: 'admin/shared/angular_pagination'
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
.controls.sixteen.columns.alpha{ 'ng-hide' => 'loading || products.length == 0' }
|
||||
.thirteen.columns
|
||||
%columns-dropdown{ action: "#{controller_name}_#{action_name}" }
|
||||
.controls.sixteen.columns.alpha{ 'ng-hide' => 'RequestMonitor.loading || products.length == 0' }
|
||||
.ten.columns.alpha.index-controls
|
||||
.per-page{'ng-show' => '!RequestMonitor.loading && products.length > 0'}
|
||||
%input.per-page-select.ofn-select2{type: 'number', data: 'per_page_options', 'min-search' => 999, 'ng-model' => 'per_page', 'ng-change' => 'fetchProducts()'}
|
||||
|
||||
%span.per-page-feedback
|
||||
{{ 'spree.admin.orders.index.results_found' | t:{number: pagination.results} }}
|
||||
{{ 'spree.admin.orders.index.viewing' | t:{start: ((pagination.page -1) * pagination.per_page) +1, end: ((pagination.page -1) * pagination.per_page) + products.length} }}
|
||||
|
||||
.six.columns.omega
|
||||
%columns-dropdown{ action: "#{controller_name}_#{action_name}" }
|
||||
|
||||
@@ -1,26 +1,35 @@
|
||||
.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?
|
||||
%fieldset
|
||||
%legend{align: 'center'}= t(:search)
|
||||
|
||||
.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 producers'} }
|
||||
.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 taxons'} }
|
||||
.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" }}
|
||||
%option{value: "{{date.id}}", ng: {repeat: "date in importDates" }}
|
||||
{{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()" }
|
||||
.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()" }
|
||||
|
||||
.clearfix
|
||||
|
||||
.actions.filter-actions
|
||||
%div
|
||||
%a.button.icon-search{'ng-click' => 'fetchProducts()'}
|
||||
= t(:filter_results)
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
%div{ 'ng-show' => '!spree_api_key_ok' }
|
||||
{{ api_error_msg }}
|
||||
|
||||
%div.sixteen.columns.alpha#loading{ 'ng-if' => 'loading' }
|
||||
%div.sixteen.columns.alpha#loading{ 'ng-if' => 'RequestMonitor.loading' }
|
||||
%br
|
||||
%img.spinner{ src: "/assets/spinning-circles.svg" }
|
||||
%h1= t('.title')
|
||||
|
||||
%div.sixteen.columns.alpha{ 'ng-show' => '!loadingAllPages && filteredProducts.length == 0 && query.length==0' }
|
||||
%div.sixteen.columns.alpha{ 'ng-show' => '!RequestMonitor.loading && products.length == 0 && query.length==0' }
|
||||
%h1#no_results= t('.no_products')
|
||||
|
||||
%div.sixteen.columns.alpha{ 'ng-show' => '!loadingAllPages && filteredProducts.length == 0 && query.length!=0' }
|
||||
%div.sixteen.columns.alpha{ 'ng-show' => '!RequestMonitor.loading && products.length == 0 && query.length!=0' }
|
||||
%h1#no_results
|
||||
= t('.no_results')
|
||||
'
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
%div.sixteen.columns.alpha{ 'ng-hide' => 'loading || filteredProducts.length == 0' }
|
||||
%div.sixteen.columns.alpha{ 'ng-hide' => 'RequestMonitor.loading || products.length == 0' }
|
||||
%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('#{admin_products_path}')" }
|
||||
|
||||
%table.index#listing_products.bulk{ "infinite-scroll" => "incrementLimit()", "infinite-scroll-distance" => "1" }
|
||||
%table.index#listing_products.bulk
|
||||
|
||||
= render 'spree/admin/products/index/products_head'
|
||||
|
||||
%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'" }
|
||||
%tbody{ 'ng-repeat' => 'product in products', 'ng-class-even' => "'even'", 'ng-class-odd' => "'odd'" }
|
||||
|
||||
= render 'spree/admin/products/index/products_product'
|
||||
= render 'spree/admin/products/index/products_variant'
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
%div.sixteen.columns.alpha{ 'ng-hide' => 'loading || products.length == 0', style: "margin-bottom: 10px" }
|
||||
%div.sixteen.columns.alpha{ 'ng-hide' => 'RequestMonitor.loading || products.length == 0', style: "margin-bottom: 10px" }
|
||||
%div.four.columns.alpha
|
||||
%input.four.columns.alpha{ :type => 'button', :value => t(:save_changes), 'ng-click' => 'submitProducts()'}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
.form-buttons.filter-actions.actions
|
||||
= button Spree.t('actions.update'), 'icon-refresh'
|
||||
%span.or= Spree.t(:or)
|
||||
= button_link_to Spree.t('actions.cancel'), collection_url, icon: 'icon-remove'
|
||||
6
app/views/spree/admin/shared/_product_sub_menu.html.haml
Normal file
6
app/views/spree/admin/shared/_product_sub_menu.html.haml
Normal file
@@ -0,0 +1,6 @@
|
||||
- content_for :sub_menu do
|
||||
%ul#sub_nav.inline-menu
|
||||
= tab :products, match_path: '/products'
|
||||
= tab :option_types, match_path: '/option_types'
|
||||
= tab :properties
|
||||
= tab :prototypes
|
||||
@@ -3,11 +3,6 @@
|
||||
:variants_search => spree.admin_search_variants_path(:format => 'json'),
|
||||
:taxons_search => spree.api_taxons_path(:format => 'json'),
|
||||
:user_search => spree.admin_search_users_path(:format => 'json'),
|
||||
:product_search => spree.api_products_path(:format => 'json'),
|
||||
:option_type_search => spree.api_option_types_path(:format => 'json'),
|
||||
:states_search => spree.api_states_path(:format => 'json'),
|
||||
:orders_api => spree.api_orders_path(:format => 'json'),
|
||||
:stock_locations_api => spree.api_stock_locations_path(:format => 'json'),
|
||||
:variants_api => spree.api_variants_path(:format => 'json')
|
||||
:orders_api => spree.api_orders_path(:format => 'json')
|
||||
}.to_json %>;
|
||||
</script>
|
||||
|
||||
67
app/views/spree/admin/variants/_form.html.haml
Normal file
67
app/views/spree/admin/variants/_form.html.haml
Normal file
@@ -0,0 +1,67 @@
|
||||
.label-block.left.six.columns.alpha{'ng-app' => 'admin.products'}
|
||||
.field
|
||||
= f.label :display_name, t(:display_name)
|
||||
= f.text_field :display_name, class: "fullwidth"
|
||||
.field
|
||||
= f.label :display_as, t(:display_as)
|
||||
= f.text_field :display_as, class: "fullwidth"
|
||||
|
||||
- if product_has_variant_unit_option_type?(@product)
|
||||
- if @product.variant_unit != 'items'
|
||||
.field{'ng-controller' => 'variantUnitsCtrl'}
|
||||
= f.label :unit_value, "#{t('admin.'+@product.variant_unit)} ({{unitName(#{@product.variant_unit_scale}, '#{@product.variant_unit}')}})"
|
||||
= hidden_field_tag 'product_variant_unit_scale', @product.variant_unit_scale
|
||||
= text_field_tag :unit_value_human, nil, {class: "fullwidth", 'ng-model' => 'unit_value_human', 'ng-change' => 'updateValue()'}
|
||||
= f.text_field :unit_value, {hidden: true, 'ng-value' => 'unit_value'}
|
||||
|
||||
.field
|
||||
= f.label :unit_description, t(:spree_admin_unit_description)
|
||||
= f.text_field :unit_description, class: "fullwidth", placeholder: t('admin.products.unit_name_placeholder')
|
||||
|
||||
%div
|
||||
- @product.option_types.each do |option_type|
|
||||
- unless variant_unit_option_type?(option_type)
|
||||
.field
|
||||
= label :new_variant, option_type.presentation
|
||||
- if @variant.new_record?
|
||||
= select(:new_variant, option_type.presentation, option_type.option_values.collect {|ov| [ ov.presentation, ov.id ] }, {}, {class: 'select2 fullwidth'})
|
||||
- else
|
||||
- if opt = @variant.option_values.detect {|o| o.option_type == option_type }.try(:presentation)
|
||||
= text_field(:new_variant, option_type.presentation, value: opt, disabled: 'disabled', class: 'fullwidth')
|
||||
.field
|
||||
= f.label :sku, Spree.t(:sku)
|
||||
= f.text_field :sku, class: 'fullwidth'
|
||||
.field
|
||||
= f.label :price, Spree.t(:price)
|
||||
= f.text_field :price, value: number_to_currency(@variant.price, unit: ''), class: 'fullwidth'
|
||||
.field
|
||||
= f.label :cost_price, Spree.t(:cost_price)
|
||||
= f.text_field :cost_price, value: number_to_currency(@variant.cost_price, unit: ''), class: 'fullwidth'
|
||||
|
||||
- if Spree::Config[:track_inventory_levels]
|
||||
%div{ 'set-on-demand' => '' }
|
||||
.field.checkbox
|
||||
%label
|
||||
= f.check_box :on_demand
|
||||
= t(:on_demand)
|
||||
%div{'ofn-with-tip' => t('admin.products.variants.to_order_tip')}
|
||||
%a= t('admin.whats_this')
|
||||
.field
|
||||
= f.label :on_hand, t(:on_hand)
|
||||
.fullwidth
|
||||
= f.text_field :on_hand
|
||||
|
||||
.right.six.columns.omega.label-block
|
||||
- if @product.variant_unit != 'weight'
|
||||
.field
|
||||
= f.label 'weight', t('weight')+' (kg)'
|
||||
- value = number_with_precision(@variant.weight, precision: 2)
|
||||
= f.text_field 'weight', value: value, class: 'fullwidth'
|
||||
|
||||
- [:height, :width, :depth].each do |field|
|
||||
.field
|
||||
= f.label field, t(field)
|
||||
- value = number_with_precision(@variant.send(field), precision: 2)
|
||||
= f.text_field field, value: value, class: 'fullwidth'
|
||||
|
||||
.clear
|
||||
11
app/views/spree/admin/variants/edit.html.haml
Normal file
11
app/views/spree/admin/variants/edit.html.haml
Normal file
@@ -0,0 +1,11 @@
|
||||
= render partial: 'spree/admin/shared/product_sub_menu'
|
||||
|
||||
= render partial: 'spree/admin/shared/product_tabs', locals: { current: 'Variants' }
|
||||
|
||||
= render partial: 'spree/shared/error_messages', locals: { target: @variant }
|
||||
|
||||
= form_for [:admin, @product, @variant] do |f|
|
||||
%fieldset.no-border-top
|
||||
%div
|
||||
= render partial: 'form', locals: { f: f }
|
||||
= render partial: 'spree/admin/shared/edit_resource_links'
|
||||
54
app/views/spree/admin/variants/index.html.haml
Normal file
54
app/views/spree/admin/variants/index.html.haml
Normal file
@@ -0,0 +1,54 @@
|
||||
= render partial: 'spree/admin/shared/product_sub_menu'
|
||||
|
||||
= render partial: 'spree/admin/shared/product_tabs', locals: {current: 'Variants'}
|
||||
|
||||
#new_variant
|
||||
- if @variants.any?
|
||||
%table.index.sortable{"data-sortable-link" => update_positions_admin_product_variants_path(@product)}
|
||||
%colgroup
|
||||
%col{style: "width: 5%"}/
|
||||
%col{style: "width: 25%"}/
|
||||
%col{style: "width: 20%"}/
|
||||
%col{style: "width: 20%"}/
|
||||
%col{style: "width: 15%"}/
|
||||
%col{style: "width: 15%"}/
|
||||
%thead
|
||||
%tr
|
||||
%th{colspan: "2"}= Spree.t(:options)
|
||||
%th= Spree.t(:price)
|
||||
%th= Spree.t(:sku)
|
||||
%th.actions
|
||||
%tbody
|
||||
- @variants.each do |variant|
|
||||
%tr{id: spree_dom_id(variant), class: cycle('odd', 'even'), style: "#{"color:red;" if variant.deleted? }" }
|
||||
%td.no-border
|
||||
%span.handle
|
||||
%td= variant.full_name
|
||||
%td.align-center= variant.display_price.to_html
|
||||
%td.align-center= variant.sku
|
||||
%td.actions
|
||||
= link_to_edit(variant, no_text: true) unless variant.deleted?
|
||||
= link_to_delete(variant, no_text: true) unless variant.deleted?
|
||||
- unless @product.has_variants?
|
||||
%tr
|
||||
%td{colspan: "5"}= Spree.t(:none)
|
||||
|
||||
- else
|
||||
.alpha.twelve.columns.no-objects-found
|
||||
= Spree.t(:no_results)
|
||||
\.
|
||||
|
||||
- if @product.empty_option_values?
|
||||
%p.first_add_option_types.no-objects-found
|
||||
= Spree.t(:to_add_variants_you_must_first_define)
|
||||
= link_to Spree.t(:option_types), admin_product_url(@product)
|
||||
= Spree.t(:and)
|
||||
= link_to Spree.t(:option_values), admin_option_types_url
|
||||
|
||||
- else
|
||||
- content_for :page_actions do
|
||||
%ul.inline-menu
|
||||
%li#new_var_link
|
||||
= link_to_with_icon('icon-plus', Spree.t(:new_variant), new_admin_product_variant_url(@product), remote: true, 'data-update' => 'new_variant', class: 'button')
|
||||
|
||||
%li= link_to_with_icon('icon-filter', @deleted.blank? ? Spree.t(:show_deleted) : Spree.t(:show_active), admin_product_variants_url(@product, deleted: @deleted.blank? ? "on" : "off"), class: 'button')
|
||||
7
app/views/spree/admin/variants/new.html.haml
Normal file
7
app/views/spree/admin/variants/new.html.haml
Normal file
@@ -0,0 +1,7 @@
|
||||
= render partial: 'spree/shared/error_messages', locals: { target: @variant }
|
||||
|
||||
= form_for [:admin, @product, @variant] do |f|
|
||||
%fieldset{'data-hook' => "admin_variant_new_form"}
|
||||
%legend{align: "center"}= Spree.t(:new_variant)
|
||||
= render partial: 'form', locals: { f: f }
|
||||
= render partial: 'spree/admin/shared/new_resource_links'
|
||||
3
app/views/spree/admin/variants/new.js.erb
Normal file
3
app/views/spree/admin/variants/new.js.erb
Normal file
@@ -0,0 +1,3 @@
|
||||
$("#new_variant").html('<%= escape_javascript(render template: 'spree/admin/variants/new', formats: [:html], handlers: [:erb]) %>');
|
||||
$(".select2").select2();
|
||||
$("#new_var_link").hide();
|
||||
@@ -1,2 +0,0 @@
|
||||
collection @products.order('id ASC')
|
||||
extends "spree/api/products/bulk_show"
|
||||
@@ -1,27 +0,0 @@
|
||||
object @product
|
||||
|
||||
# TODO: This is used by bulk product edit when a product is cloned.
|
||||
# But the list of products is serialized by Api::Admin::ProductSerializer.
|
||||
# This should probably be unified.
|
||||
|
||||
attributes :id, :name, :sku, :variant_unit, :variant_unit_scale, :variant_unit_name, :on_demand, :inherits_properties
|
||||
attributes :on_hand, :price, :available_on, :permalink_live, :tax_category_id
|
||||
|
||||
# Infinity is not a valid JSON object, but Rails encodes it anyway
|
||||
node( :taxon_ids ) { |p| p.taxons.map(&:id).join(",") }
|
||||
node( :on_hand ) { |p| p.on_hand.nil? ? 0 : p.on_hand.to_f.finite? ? p.on_hand : t(:on_demand) }
|
||||
node( :price ) { |p| p.price.nil? ? '0.0' : p.price }
|
||||
|
||||
node( :available_on ) { |p| p.available_on.blank? ? "" : p.available_on.strftime("%F %T") }
|
||||
node( :permalink_live, &:permalink )
|
||||
node( :producer_id, &:supplier_id )
|
||||
node( :category_id, &:primary_taxon_id )
|
||||
node( :supplier ) do |p|
|
||||
partial 'api/enterprises/bulk_show', object: p.supplier
|
||||
end
|
||||
node( :variants ) do |p|
|
||||
partial 'spree/api/variants/bulk_index', object: p.variants.reorder('spree_variants.id ASC')
|
||||
end
|
||||
node( :master ) do |p|
|
||||
partial 'spree/api/variants/bulk_show', object: p.master
|
||||
end
|
||||
@@ -1,2 +0,0 @@
|
||||
collection @variants
|
||||
extends "spree/api/variants/bulk_show"
|
||||
@@ -1,7 +0,0 @@
|
||||
object @variant
|
||||
|
||||
attributes :id, :options_text, :unit_value, :unit_description, :on_demand, :display_as, :display_name
|
||||
|
||||
# Infinity is not a valid JSON object, but Rails encodes it anyway
|
||||
node( :on_hand ) { |v| v.on_hand.nil? ? 0 : ( v.on_hand.to_f.finite? ? v.on_hand : t(:on_demand) ) }
|
||||
node( :price ) { |v| v.price.nil? ? 0.to_f : v.price }
|
||||
@@ -1,4 +0,0 @@
|
||||
= address.address1
|
||||
= ", #{address.address2}" unless address.address2.blank?
|
||||
%br/
|
||||
= [address.city, address.state_text, address.zipcode, address.country.name].compact.join ', '
|
||||
11
app/views/spree/shared/_error_messages.html.haml
Normal file
11
app/views/spree/shared/_error_messages.html.haml
Normal file
@@ -0,0 +1,11 @@
|
||||
- if target && target.errors.any?
|
||||
#errorExplanation.errorExplanation
|
||||
%h2
|
||||
= Spree.t(:errors_prohibited_this_record_from_being_saved, count: target.errors.count)
|
||||
\:
|
||||
%p
|
||||
= Spree.t(:there_were_problems_with_the_following_fields)
|
||||
\:
|
||||
%ul
|
||||
- target.errors.full_messages.each do |msg|
|
||||
%li= msg
|
||||
@@ -1,15 +0,0 @@
|
||||
- filters = @taxon ? @taxon.applicable_filters : []
|
||||
- unless filters.empty?
|
||||
%nav#filters
|
||||
- params[:search] ||= {}
|
||||
- filters.each do |filter|
|
||||
- labels = filter[:labels] || filter[:conds].map {|m,c| [m,m]}
|
||||
- next if labels.empty?
|
||||
|
||||
%h6.filter_name= "Shop by #{filter[:name]}"
|
||||
|
||||
%ul.filter_choices
|
||||
- labels.each do |nm,val|
|
||||
%li.nowrap
|
||||
- active = params[:search][filter[:scope]] && params[:search][filter[:scope]].include?(val.to_s)
|
||||
= link_to nm, "?search[#{filter[:scope].to_s}][]=#{CGI.escape(val)}"
|
||||
@@ -1,23 +0,0 @@
|
||||
- paginated_products = @searcher.retrieve_products if params.key?(:keywords)
|
||||
- paginated_products ||= products
|
||||
|
||||
- if products.empty?
|
||||
= t(:no_products_found)
|
||||
- elsif params.key?(:keywords)
|
||||
%h6.search-results-title= t(:search_results, :keywords => h(params[:keywords]))
|
||||
|
||||
- if products.any?
|
||||
%ul#products.inline.product-listing{"data-hook" => ""}
|
||||
- reset_cycle('default')
|
||||
- products.each do |product|
|
||||
- if Spree::Config[:show_zero_stock_products] || product.has_stock?
|
||||
%li{:class => "columns three #{cycle("alpha", "secondary", "", "omega secondary")}", "data-hook" => "products_list_item", :id => "product_#{product.id}", :itemscope => "", :itemtype => "http://schema.org/Product"}
|
||||
.product-image
|
||||
= link_to small_image(product, :itemprop => "image"), product, :itemprop => 'url'
|
||||
= link_to truncate(product.name, :length => 50), product, :class => 'info', :itemprop => "name", :title => product.name
|
||||
%span.price.selling{:itemprop => "price"}= spree_number_to_currency(product.price)
|
||||
|
||||
- if paginated_products.respond_to?(:num_pages)
|
||||
- params.delete(:search)
|
||||
- params.delete(:taxon)
|
||||
= paginate paginated_products
|
||||
@@ -113,6 +113,7 @@ module Openfoodnetwork
|
||||
)
|
||||
|
||||
config.paths["config/routes"] = %w(
|
||||
config/routes/api.rb
|
||||
config/routes.rb
|
||||
config/routes/admin.rb
|
||||
config/routes/spree.rb
|
||||
|
||||
5
config/initializers/kaminari.rb
Normal file
5
config/initializers/kaminari.rb
Normal file
@@ -0,0 +1,5 @@
|
||||
# Sets a maximum number of returned records when Kaminari pagination is used on a query but no
|
||||
# per_page value has been passed to the #per method.
|
||||
Kaminari.configure do |config|
|
||||
config.max_per_page = 100
|
||||
end
|
||||
@@ -244,6 +244,7 @@ ca:
|
||||
reset_password_token: Reinicia el token de contrasenya
|
||||
expired: ha caducat, si us plau, sol·liciteu-ne un de nou
|
||||
back_to_payments_list: "Torna a la llista de pagaments"
|
||||
maestro_or_solo_cards: "Targetes Maestro/Solo"
|
||||
actions:
|
||||
create_and_add_another: "Crea i afegeix-ne una altra"
|
||||
create: "Crear"
|
||||
@@ -452,6 +453,8 @@ ca:
|
||||
encoding_error: "Comproveu la configuració de l'idioma del vostre fitxer d'origen i assegureu-vos que es desa amb la codificació UTF-8"
|
||||
unexpected_error: "La importació de productes ha detectat un error inesperat mentre obria el fitxer: %{error_message}"
|
||||
index:
|
||||
notice: "Avís"
|
||||
beta_notice: "Aquesta funcionalitat continua en fase beta: podeu experimentar alguns errors mentre l'utilitzeu. No dubteu en contactar amb nosaltres."
|
||||
select_file: Selecciona un full de càlcul per pujar-lo
|
||||
spreadsheet: Full de càlcul
|
||||
choose_import_type: Selecciona el tipus d'importació
|
||||
|
||||
@@ -507,6 +507,8 @@ en:
|
||||
encoding_error: "Please check the language setting of your source file and ensure it is saved with UTF-8 encoding"
|
||||
unexpected_error: "Product Import encountered an unexpected error whilst opening the file: %{error_message}"
|
||||
index:
|
||||
notice: "Notice"
|
||||
beta_notice: "This feature is still in beta: you may experience some errors while using it. Please don't hesitate to contact support."
|
||||
select_file: Select a spreadsheet to upload
|
||||
spreadsheet: Spreadsheet
|
||||
choose_import_type: Select import type
|
||||
|
||||
@@ -94,6 +94,7 @@ en_AU:
|
||||
user_passwords:
|
||||
spree_user:
|
||||
updated_not_active: "Your password has been reset, but your email has not been confirmed yet."
|
||||
updated: "Your password was changed successfully. You are now signed in."
|
||||
send_instructions: "You will receive an email with instructions about how to confirm your account in a few minutes."
|
||||
models:
|
||||
order_cycle:
|
||||
@@ -243,6 +244,7 @@ en_AU:
|
||||
reset_password_token: Reset password token
|
||||
expired: has expired, please request a new one
|
||||
back_to_payments_list: "Back to Payments List"
|
||||
maestro_or_solo_cards: "Maestro/Solo cards"
|
||||
actions:
|
||||
create_and_add_another: "Create and Add Another"
|
||||
create: "Create"
|
||||
@@ -451,6 +453,8 @@ en_AU:
|
||||
encoding_error: "Please check the language setting of your source file and ensure it is saved with UTF-8 encoding"
|
||||
unexpected_error: "Product Import encountered an unexpected error whilst opening the file: %{error_message}"
|
||||
index:
|
||||
notice: "Notice"
|
||||
beta_notice: "This feature is still in beta: you may experience some errors while using it. Please don't hesitate to contact support."
|
||||
select_file: Select a spreadsheet to upload
|
||||
spreadsheet: Spreadsheet
|
||||
choose_import_type: Select import type
|
||||
@@ -1027,6 +1031,7 @@ en_AU:
|
||||
saved: "SAVED"
|
||||
product_already_in_order: This product has already been added to the order. Please edit the quantity directly.
|
||||
stock:
|
||||
insufficient_stock: "Insufficient stock available"
|
||||
out_of_stock: "Out of Stock"
|
||||
orders:
|
||||
number: Number
|
||||
@@ -1035,6 +1040,7 @@ en_AU:
|
||||
cancel_failure_msg: "Sorry, cancellation failed!"
|
||||
confirm_pause_msg: "Are you sure you want to pause this subscription?"
|
||||
pause_failure_msg: "Sorry, pausing failed!"
|
||||
confirm_unpause_msg: "If you have an open Order Cycle in this subscription's schedule, an order will be created for this customer. Are you sure you want to unpause this subscription?"
|
||||
unpause_failure_msg: "Sorry, unpausing failed!"
|
||||
confirm_cancel_open_orders_msg: "Some orders for this subscription are currently open. The customer has already been notified that the order will be placed. Would you like to cancel these order(s) or keep them?"
|
||||
resume_canceled_orders_msg: "Some orders for this subscription can be resumed right now. You can resume them from the orders dropdown."
|
||||
@@ -2779,6 +2785,7 @@ en_AU:
|
||||
inventory: Inventory
|
||||
zipcode: Postcode
|
||||
weight: Weight (per kg)
|
||||
error_user_destroy_with_orders: "Users with completed orders may not be deleted"
|
||||
actions:
|
||||
update: "Update"
|
||||
errors:
|
||||
|
||||
@@ -94,6 +94,7 @@ en_CA:
|
||||
user_passwords:
|
||||
spree_user:
|
||||
updated_not_active: "Your password has been reset, but your email has not been confirmed yet."
|
||||
updated: "Your password was changed successfully. You are now signed in."
|
||||
send_instructions: "You will receive an email with instructions about how to confirm your account in a few minutes."
|
||||
models:
|
||||
order_cycle:
|
||||
@@ -243,6 +244,7 @@ en_CA:
|
||||
reset_password_token: Reset password token
|
||||
expired: has expired, please request a new one
|
||||
back_to_payments_list: "Back to Payments List"
|
||||
maestro_or_solo_cards: "Maestro/Solo cards"
|
||||
actions:
|
||||
create_and_add_another: "Create and Add Another"
|
||||
create: "Create"
|
||||
@@ -451,6 +453,8 @@ en_CA:
|
||||
encoding_error: "Please check the language setting of your source file and ensure it is saved with UTF-8 encoding"
|
||||
unexpected_error: "Product Import encountered an unexpected error whilst opening the file: %{error_message}"
|
||||
index:
|
||||
notice: "Notice"
|
||||
beta_notice: "This feature is still in beta: you may experience some errors while using it. Please don't hesitate to contact support."
|
||||
select_file: Select a spreadsheet to upload
|
||||
spreadsheet: Spreadsheet
|
||||
choose_import_type: Select import type
|
||||
@@ -1027,6 +1031,7 @@ en_CA:
|
||||
saved: "SAVED"
|
||||
product_already_in_order: This product has already been added to the order. Please edit the quantity directly.
|
||||
stock:
|
||||
insufficient_stock: "Insufficient stock available"
|
||||
out_of_stock: "Out of Stock"
|
||||
orders:
|
||||
number: Number
|
||||
@@ -1035,6 +1040,7 @@ en_CA:
|
||||
cancel_failure_msg: "Sorry, cancellation failed!"
|
||||
confirm_pause_msg: "Are you sure you want to pause this subscription?"
|
||||
pause_failure_msg: "Sorry, pausing failed!"
|
||||
confirm_unpause_msg: "If you have an open Order Cycle in this subscription's schedule, an order will be created for this customer. Are you sure you want to unpause this subscription?"
|
||||
unpause_failure_msg: "Sorry, unpausing failed!"
|
||||
confirm_cancel_open_orders_msg: "Some orders for this subscription are currently open. The customer has already been notified that the order will be placed. Would you like to cancel these order(s) or keep them?"
|
||||
resume_canceled_orders_msg: "Some orders for this subscription can be resumed right now. You can resume them from the orders dropdown."
|
||||
@@ -2779,6 +2785,7 @@ en_CA:
|
||||
inventory: Inventory
|
||||
zipcode: Postal Code
|
||||
weight: Weight (per kg)
|
||||
error_user_destroy_with_orders: "Users with completed orders may not be deleted"
|
||||
actions:
|
||||
update: "Update"
|
||||
errors:
|
||||
@@ -2882,9 +2889,11 @@ en_CA:
|
||||
new_shipping_method: "New Shipping Method"
|
||||
back_to_shipping_methods_list: "Back to Shipping Methods List"
|
||||
edit:
|
||||
editing_shipping_method: "Editing Shipping Method"
|
||||
new: "New"
|
||||
back_to_shipping_methods_list: "Back to Shipping Methods List"
|
||||
form:
|
||||
categories: "Categories"
|
||||
zones: "Zones"
|
||||
both: "Both"
|
||||
front_end: "Front End"
|
||||
@@ -2915,6 +2924,7 @@ en_CA:
|
||||
error_saving_payment: Error saving payment
|
||||
submitting_payment: Submitting payment...
|
||||
products:
|
||||
image_upload_error: "The product image was not recognised. Please upload an image in PNG or JPG format."
|
||||
new:
|
||||
title: 'New Product'
|
||||
unit_name_placeholder: 'eg. bunches'
|
||||
@@ -3015,6 +3025,7 @@ en_CA:
|
||||
line_item:
|
||||
insufficient_stock: "Insufficient stock available, only %{on_hand} remaining"
|
||||
out_of_stock: "Out of Stock"
|
||||
unavailable_item: "Currently unavailable"
|
||||
shipment_states:
|
||||
backorder: backorder
|
||||
partial: partial
|
||||
@@ -3034,6 +3045,8 @@ en_CA:
|
||||
invalid: invalid
|
||||
order_mailer:
|
||||
cancel_email:
|
||||
customer_greeting: "Hi %{name}!"
|
||||
instructions: "Your order has been CANCELED. Please retain this cancellation information for your records."
|
||||
order_summary_canceled: "Order Summary [CANCELED]"
|
||||
subject: "Cancellation of Order"
|
||||
confirm_email:
|
||||
|
||||
@@ -244,6 +244,7 @@ en_GB:
|
||||
reset_password_token: Reset password
|
||||
expired: has expired, please request a new one
|
||||
back_to_payments_list: "Back to Payments List"
|
||||
maestro_or_solo_cards: "Maestro/Solo cards"
|
||||
actions:
|
||||
create_and_add_another: "Create and Add Another"
|
||||
create: "Create"
|
||||
@@ -452,6 +453,8 @@ en_GB:
|
||||
encoding_error: "Please check the language setting of your source file and ensure it is saved with UTF-8 encoding"
|
||||
unexpected_error: "Product Import encountered an unexpected error whilst opening the file: %{error_message}"
|
||||
index:
|
||||
notice: "Notice"
|
||||
beta_notice: "This feature is still in beta: you may experience some errors while using it. Please don't hesitate to contact support."
|
||||
select_file: Select a spreadsheet to upload
|
||||
spreadsheet: Spreadsheet
|
||||
choose_import_type: Select import type
|
||||
@@ -1028,6 +1031,7 @@ en_GB:
|
||||
saved: "SAVED"
|
||||
product_already_in_order: This product has already been added to the order. Please edit the quantity directly.
|
||||
stock:
|
||||
insufficient_stock: "Insufficient stock available"
|
||||
out_of_stock: "Out of Stock"
|
||||
orders:
|
||||
number: Number
|
||||
@@ -1036,6 +1040,7 @@ en_GB:
|
||||
cancel_failure_msg: "Sorry, cancellation failed!"
|
||||
confirm_pause_msg: "Are you sure you want to pause this subscription?"
|
||||
pause_failure_msg: "Sorry, pausing failed!"
|
||||
confirm_unpause_msg: "If you have an open Order Cycle in this subscription's schedule, an order will be created for this customer. Are you sure you want to unpause this subscription?"
|
||||
unpause_failure_msg: "Sorry, unpausing failed!"
|
||||
confirm_cancel_open_orders_msg: "Some orders for this subscription are currently open. The customer has already been notified that the order will be placed. Would you like to cancel these order(s) or keep them?"
|
||||
resume_canceled_orders_msg: "Some orders for this subscription can be resumed right now. You can resume them from the orders dropdown."
|
||||
@@ -1374,7 +1379,7 @@ en_GB:
|
||||
email_confirmed: "Thank you for confirming your email address."
|
||||
email_registered: "is now part of"
|
||||
email_userguide_html: "The User Guide with detailed support for setting up your Producer or Hub is here: %{link}"
|
||||
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_admin_html: "You can manage your account by logging into the %{link} or by clicking on 'Profile' 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_confirmation_activate_account: "Before we can activate your new account, we need to confirm your email address."
|
||||
|
||||
@@ -94,6 +94,7 @@ en_ZA:
|
||||
user_passwords:
|
||||
spree_user:
|
||||
updated_not_active: "Your password has been reset, but your email has not been confirmed yet."
|
||||
updated: "Your password was changed successfully. You are now signed in."
|
||||
send_instructions: "You will receive an email with instructions about how to confirm your account in a few minutes."
|
||||
models:
|
||||
order_cycle:
|
||||
@@ -243,6 +244,7 @@ en_ZA:
|
||||
reset_password_token: Reset password
|
||||
expired: has expired, please request a new one
|
||||
back_to_payments_list: "Back to Payments List"
|
||||
maestro_or_solo_cards: "Maestro/Solo cards"
|
||||
actions:
|
||||
create_and_add_another: "Create and Add Another"
|
||||
create: "Create"
|
||||
@@ -451,6 +453,8 @@ en_ZA:
|
||||
encoding_error: "Please check the language setting of your source file and ensure it is saved with UTF-8 encoding"
|
||||
unexpected_error: "Product Import encountered an unexpected error whilst opening the file: %{error_message}"
|
||||
index:
|
||||
notice: "Notice"
|
||||
beta_notice: "This feature is still in beta: you may experience some errors while using it. Please don't hesitate to contact support."
|
||||
select_file: Select a spreadsheet to upload
|
||||
spreadsheet: Spreadsheet
|
||||
choose_import_type: Select import type
|
||||
@@ -1027,6 +1031,7 @@ en_ZA:
|
||||
saved: "SAVED"
|
||||
product_already_in_order: This product has already been added to the order. Please edit the quantity directly.
|
||||
stock:
|
||||
insufficient_stock: "Insufficient stock available"
|
||||
out_of_stock: "Out of Stock"
|
||||
orders:
|
||||
number: Number
|
||||
@@ -1035,6 +1040,7 @@ en_ZA:
|
||||
cancel_failure_msg: "Sorry, cancellation failed!"
|
||||
confirm_pause_msg: "Are you sure you want to pause this subscription?"
|
||||
pause_failure_msg: "Sorry, pausing failed!"
|
||||
confirm_unpause_msg: "If you have an open Order Cycle in this subscription's schedule, an order will be created for this customer. Are you sure you want to unpause this subscription?"
|
||||
unpause_failure_msg: "Sorry, unpausing failed!"
|
||||
confirm_cancel_open_orders_msg: "Some orders for this subscription are currently open. The customer has already been notified that the order will be placed. Would you like to cancel these order(s) or keep them?"
|
||||
resume_canceled_orders_msg: "Some orders for this subscription can be resumed right now. You can resume them from the orders dropdown."
|
||||
@@ -2680,9 +2686,9 @@ en_ZA:
|
||||
check_for_spree_alerts: "Check for Spree alerts"
|
||||
currency_decimal_mark: "Currency decimal mark"
|
||||
currency_settings: "Currency Settings"
|
||||
currency_symbol_position: Put "currency symbol before or after pound amount?"
|
||||
currency_symbol_position: Put "currency symbol before or after Rand amount?"
|
||||
currency_thousands_separator: "Currency thousands separator"
|
||||
hide_cents: "Hide pence"
|
||||
hide_cents: "Hide cents"
|
||||
display_currency: "Display currency"
|
||||
choose_currency: "Choose Currency"
|
||||
mail_method_settings: "Mail Method Settings"
|
||||
@@ -2728,7 +2734,7 @@ en_ZA:
|
||||
default_tax: "Default Tax"
|
||||
default_tax_zone: "Default Tax Zone"
|
||||
country_based: "Country Based"
|
||||
state_based: "County Based"
|
||||
state_based: "Province Based"
|
||||
countries: "Countries"
|
||||
listing_countries: "Listing Countries"
|
||||
iso_name: "ISO Name"
|
||||
@@ -2786,6 +2792,7 @@ en_ZA:
|
||||
inventory: Inventory
|
||||
zipcode: Postcode
|
||||
weight: Weight (per kg)
|
||||
error_user_destroy_with_orders: "Users with completed orders may not be deleted"
|
||||
actions:
|
||||
update: "Update"
|
||||
errors:
|
||||
|
||||
@@ -244,6 +244,7 @@ es:
|
||||
reset_password_token: token de restablecimiento de contraseña
|
||||
expired: ha expirado, por favor solicite una nueva
|
||||
back_to_payments_list: "Volver a la lista de pagos"
|
||||
maestro_or_solo_cards: "Tarjetas Maestro/Solo"
|
||||
actions:
|
||||
create_and_add_another: "Crear y agregar otro"
|
||||
create: "Crear"
|
||||
@@ -452,6 +453,8 @@ es:
|
||||
encoding_error: "Verifique la configuración de idioma de su archivo fuente y asegúrese de que esté guardado con la codificación UTF-8"
|
||||
unexpected_error: "La importación de productos encontró un error inesperado al abrir el archivo: %{error_message}"
|
||||
index:
|
||||
notice: "aviso"
|
||||
beta_notice: "Esta funcionalidad aún está en versión beta: puede experimentar algunos errores mientras la usa. Por favor no dude en ponerse en contacto con nosotros."
|
||||
select_file: Selecciona una hoja de cálculo para subir
|
||||
spreadsheet: Hoja de cálculo
|
||||
choose_import_type: Seleccionar tipo de importación
|
||||
|
||||
@@ -5,9 +5,9 @@ fr:
|
||||
spree/order:
|
||||
payment_state: Statut du paiement
|
||||
shipment_state: Statut livraison
|
||||
completed_at: 'Passée à '
|
||||
completed_at: Date
|
||||
number: N° commande
|
||||
state: Etat
|
||||
state: Statut
|
||||
email: Email acheteur
|
||||
spree/payment:
|
||||
amount: Montant
|
||||
@@ -216,7 +216,7 @@ fr:
|
||||
unused: inutilisé
|
||||
admin_and_handling: Frais si par commande
|
||||
profile: Profil
|
||||
supplier_only: Uniquement Producteur
|
||||
supplier_only: ne gère pas de boutique
|
||||
has_shopfront: gère une boutique
|
||||
weight: Poids
|
||||
volume: Volume
|
||||
@@ -244,6 +244,7 @@ fr:
|
||||
reset_password_token: La demande de réinitialisation du mot de passe
|
||||
expired: a expiré, veuillez faire une nouvelle demande.
|
||||
back_to_payments_list: "Retour à la liste des paiements"
|
||||
maestro_or_solo_cards: "Cartes Maestro/Solo"
|
||||
actions:
|
||||
create_and_add_another: "Créer et ajouter nouveau"
|
||||
create: "Créer"
|
||||
@@ -277,7 +278,7 @@ fr:
|
||||
shipping_method: Option d'expédition
|
||||
shop: Boutique
|
||||
sku: Référence produit
|
||||
status_state: Etat
|
||||
status_state: Statut
|
||||
tags: Tags
|
||||
variant: Variante
|
||||
weight: Poids
|
||||
@@ -452,6 +453,8 @@ fr:
|
||||
encoding_error: "Veuillez vérifier le paramètre de langue de votre code source et vous assurer qu'il est encodé en UTF-8"
|
||||
unexpected_error: "L'import de fichier produits à rencontré une erreur inconnue à l'ouverture du fichier : %{error_message}"
|
||||
index:
|
||||
notice: "Notice"
|
||||
beta_notice: "Cette fonctionnalité est en mode bêta : il est possible que vous rencontriez des erreurs en l'utilisant. N'hésitez pas à contacter le support utilisateurs."
|
||||
select_file: Sélectionner le fichier (tableur sous format csv) à importer
|
||||
spreadsheet: Tableur csv
|
||||
choose_import_type: Choisir le type d'import
|
||||
@@ -555,7 +558,7 @@ fr:
|
||||
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: "Passée le"
|
||||
order_date: "Date"
|
||||
max: "Max"
|
||||
product_unit: "Produit: Unité"
|
||||
weight_volume: "Poids/Volume"
|
||||
@@ -725,7 +728,7 @@ fr:
|
||||
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'
|
||||
add_new_tag: '+ Ajouter une règle pour un autre tag'
|
||||
users:
|
||||
email_confirmation_notice_html: "L'email de confirmation n'a pas encore été validé. Il a été envoyé à %{email}."
|
||||
resend: Renvoyer
|
||||
@@ -923,7 +926,7 @@ fr:
|
||||
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 boutique (en stock)
|
||||
inventory: Produits disponibles (stock positif)
|
||||
lettuce_share: LettuceShare
|
||||
mailing_list: Liste de mails
|
||||
addresses: Adresses
|
||||
@@ -947,7 +950,7 @@ fr:
|
||||
customers:
|
||||
name: Acheteurs
|
||||
products_and_inventory:
|
||||
name: Produits et Catalogues
|
||||
name: Listings produits
|
||||
sales_total:
|
||||
name: Total des Ventes
|
||||
description: Total des Ventes pour toutes les Commandes
|
||||
@@ -1032,7 +1035,7 @@ fr:
|
||||
insufficient_stock: "Stock disponible insuffisant"
|
||||
out_of_stock: "En rupture de stock"
|
||||
orders:
|
||||
number: Nombre
|
||||
number: N° commande
|
||||
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é!"
|
||||
@@ -1855,7 +1858,7 @@ fr:
|
||||
transaction_date: "Date"
|
||||
payment_state: "Statut du paiement"
|
||||
shipping_state: "Statut de la livraison"
|
||||
value: "Nb unités"
|
||||
value: "Montant"
|
||||
balance_due: "Montant dû"
|
||||
credit: "Créditer"
|
||||
Paid: "Payé"
|
||||
@@ -2405,13 +2408,13 @@ fr:
|
||||
description: Description
|
||||
resolve: Résoudre
|
||||
tag_rules:
|
||||
shipping_method_tagged_top: "Les méthodes de livraison taggées"
|
||||
shipping_method_tagged_top: "Les méthodes de livraison taguées"
|
||||
shipping_method_tagged_bottom: "sont:"
|
||||
payment_method_tagged_top: "Les méthodes de payments taguées"
|
||||
payment_method_tagged_bottom: "sont:"
|
||||
order_cycle_tagged_top: "Les cycles de vente taggés"
|
||||
order_cycle_tagged_top: "Les cycles de vente tagués"
|
||||
order_cycle_tagged_bottom: "sont:"
|
||||
inventory_tagged_top: "Les variantes du catalogue boutique taggées"
|
||||
inventory_tagged_top: "Les variantes du catalogue boutique taguées"
|
||||
inventory_tagged_bottom: "sont:"
|
||||
new_tag_rule_dialog:
|
||||
select_rule_type: "Choisir le type de règle:"
|
||||
@@ -2430,7 +2433,7 @@ fr:
|
||||
address: "adresse"
|
||||
adjustments: "ajustements"
|
||||
awaiting_return: "attente du retour"
|
||||
canceled: "annulé"
|
||||
canceled: "annulée"
|
||||
cart: "panier"
|
||||
complete: "finalisée"
|
||||
confirm: "confirmer"
|
||||
@@ -2520,8 +2523,8 @@ fr:
|
||||
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_shipping: 'Afficher ou Masquer les méthodes de livraison lors de la finalisation de commande'
|
||||
show_hide_payment: 'Afficher ou Masquer 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
|
||||
@@ -2556,7 +2559,7 @@ fr:
|
||||
close_date_not_set: Date de fin non renseignée
|
||||
spree:
|
||||
users:
|
||||
order: "Commandes à venir"
|
||||
order: "Commande"
|
||||
registration:
|
||||
welcome_to_ofn: "Bienvenue sur Open Food France !"
|
||||
signup_or_login: "Commencez par vous inscrire (ou connexion)"
|
||||
@@ -2604,7 +2607,7 @@ fr:
|
||||
tax_category_name: "TVA applicable"
|
||||
total_amount: "€ total"
|
||||
invalid_filter_parameters: "Les filtres sélectionnés pour ce rapport sont invalides."
|
||||
order: "Commandes à venir"
|
||||
order: "Commande"
|
||||
distribution: "Distribution"
|
||||
order_details: "Détails de la commande"
|
||||
customer_details: "Informations acheteur"
|
||||
@@ -2848,8 +2851,8 @@ fr:
|
||||
payment_state: "Statut du Paiement"
|
||||
shipment_state: "Statut livraison"
|
||||
completed_at: "Date"
|
||||
number: "Nombre"
|
||||
state: "Etat"
|
||||
number: "N° commande"
|
||||
state: "Statut"
|
||||
email: "Email acheteur"
|
||||
invoice:
|
||||
issued_on: "Editée le"
|
||||
@@ -3067,7 +3070,7 @@ fr:
|
||||
address: adresse
|
||||
adjustments: ajustements
|
||||
awaiting_return: attente du retour
|
||||
canceled: annulé
|
||||
canceled: annulée
|
||||
cart: panier
|
||||
complete: finalisée
|
||||
confirm: confirmer
|
||||
@@ -3123,7 +3126,7 @@ fr:
|
||||
closed: Fermée
|
||||
until: Jusqu'à
|
||||
past_orders:
|
||||
order: Commandes à venir
|
||||
order: Commande
|
||||
shop: Boutique
|
||||
completed_at: Date
|
||||
items: Produits
|
||||
@@ -3134,7 +3137,7 @@ fr:
|
||||
default?: Carte utilisée par défaut?
|
||||
delete?: Supprimer?
|
||||
cards:
|
||||
authorised_shops: Boutiques autorisées.
|
||||
authorised_shops: Boutiques autorisées
|
||||
authorised_shops_popover: Voilà la liste des boutiques que vous avez autorisées à débiter votre carte de paiement par défaut dans le cadre de vos abonnements en cours (commandes récurrentes). Les informations concernant votre carte de paiement sont sécurisées et ne sont pas accessibles par le gérant de la boutique. Vous recevrez systématiquement une notification avant tout débit sur votre carte.
|
||||
saved_cards_popover: Voilà la liste des cartes de paiement que vous avez enregistrées. Votre carte par défaut sera automatiquement sélectionnée au moment de la finalisation d'une commande, et pourra être débitée par les boutiques auxquelles vous avez donné cette autorisation (voir à droite).
|
||||
authorised_shops:
|
||||
|
||||
@@ -14,7 +14,9 @@ fr_CA:
|
||||
spree/product:
|
||||
primary_taxon: "Catégorie Produit"
|
||||
supplier: "Fournisseur"
|
||||
shipping_category_id: "Condition de transport"
|
||||
variant_unit: "Unité"
|
||||
variant_unit_name: "Unité de la variante"
|
||||
order_cycle:
|
||||
orders_close_at: Date de fermeture
|
||||
errors:
|
||||
@@ -75,6 +77,8 @@ fr_CA:
|
||||
user_confirmations:
|
||||
spree_user:
|
||||
send_instructions: "Un email a été envoyé avec des instructions pour confirmer votre adresse email. Vérifiez votre boite mail!"
|
||||
confirmation_sent: "L'email de confirmation a bien été envoyé"
|
||||
confirmation_not_sent: "Une erreur est survenue lors de l'envoi de l'email de confirmation"
|
||||
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."
|
||||
@@ -86,9 +90,12 @@ fr_CA:
|
||||
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."
|
||||
success:
|
||||
logged_in_succesfully: "Vous êtes désormais connecté !"
|
||||
user_passwords:
|
||||
spree_user:
|
||||
updated_not_active: "Votre mot de passe a été mis à jour, mais votre email n'a pas encore été confirmé."
|
||||
updated: "Votre mot de passer a été changé avec succès. Vous êtes maintenant connecté(e)."
|
||||
send_instructions: "Un email a été envoyé avec des instructions pour confirmer votre adresse email. Vérifiez votre boite mail!"
|
||||
models:
|
||||
order_cycle:
|
||||
@@ -238,6 +245,7 @@ fr_CA:
|
||||
reset_password_token: La demande de réinitialisation du mot de passe
|
||||
expired: a expiré, veuillez faire une nouvelle demande.
|
||||
back_to_payments_list: "Retour à la liste des paiements"
|
||||
maestro_or_solo_cards: "Cartes Maestro/Solo"
|
||||
actions:
|
||||
create_and_add_another: "Créer et ajouter nouveau"
|
||||
create: "Créer"
|
||||
@@ -322,6 +330,7 @@ fr_CA:
|
||||
invoice_settings:
|
||||
edit:
|
||||
title: "Paramètres de facturation"
|
||||
enable_invoices?: "Autoriser l'émission de factures ?"
|
||||
invoice_style2?: "Utiliser le modèle de facture alternatif qui détaille le montant de Taxe agrégé par taux et l'information du taux de taxe 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?"
|
||||
stripe_connect_settings:
|
||||
@@ -390,6 +399,7 @@ fr_CA:
|
||||
calculator: "Calculateur"
|
||||
calculator_values: "Valeurs applicables"
|
||||
search: "Chercher"
|
||||
name_placeholder: "ex.: marge de conditionnement"
|
||||
enterprise_groups:
|
||||
index:
|
||||
new_button: Nouveau groupe d'entreprises
|
||||
@@ -419,6 +429,7 @@ fr_CA:
|
||||
property_name: Nom du label
|
||||
inherited_property: Label producteur appliqué par défaut
|
||||
variants:
|
||||
infinity: "Infini"
|
||||
to_order_tip: "Les articles fabriqués sur commande n'ont pas un niveau de stock défini, comme des pains faits à la main."
|
||||
group_buy_options: "Options d'achat par lot"
|
||||
back_to_products_list: "Retour à la liste produits"
|
||||
@@ -443,6 +454,8 @@ fr_CA:
|
||||
encoding_error: "Veuillez vérifier le paramètre de langue de votre code source et vous assurer qu'il est encodé en UTF-8"
|
||||
unexpected_error: "L'import de fichier produits à rencontré une erreur inconnue à l'ouverture du fichier : %{error_message}"
|
||||
index:
|
||||
notice: "Notice"
|
||||
beta_notice: "Cette fonctionnalité est en mode bêta : il est possible que vous rencontriez des erreurs en l'utilisant. N'hésitez pas à contacter le support utilisateurs."
|
||||
select_file: Sélectionner une feuille de calcul à uploader
|
||||
spreadsheet: Feuille de calcul
|
||||
choose_import_type: Choisir le type d'import
|
||||
@@ -648,6 +661,8 @@ fr_CA:
|
||||
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.
|
||||
ofn_uid: ID OFN
|
||||
ofn_uid_tip: L'identifiant unique pour les comptes entreprises sur Open Food Network.
|
||||
shipping_methods:
|
||||
name: Nom
|
||||
applies: S'applique?
|
||||
@@ -677,6 +692,8 @@ fr_CA:
|
||||
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_message_link_tooltip: "Insérer / modifier un lien"
|
||||
shopfront_message_link_prompt: "Veuillez entrer l'URL à insérer"
|
||||
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
|
||||
@@ -806,6 +823,9 @@ fr_CA:
|
||||
user_already_exists: "Le compte existe déjà"
|
||||
error: "Un problème est survenu"
|
||||
order_cycles:
|
||||
loading_flash:
|
||||
loading_order_cycles: Cycles de vente en cours de chargement
|
||||
loading: Chargement en cours...
|
||||
edit:
|
||||
advanced_settings: Paramétrages avancés
|
||||
update_and_close: Mettre à jour et fermer
|
||||
@@ -824,6 +844,7 @@ fr_CA:
|
||||
add_distributor: 'Ajouter un distributeur'
|
||||
advanced_settings:
|
||||
title: Paramétrages avancés
|
||||
choose_product_tip: Vous pouvez restreindre les produits entrants et sortants uniquement au catalogue boutique de %{inventory}
|
||||
preferred_product_selection_from_coordinator_inventory_only_here: Uniquement le catalogue boutique 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
|
||||
@@ -959,6 +980,8 @@ fr_CA:
|
||||
pause_subscription: Mettre en pause Abonnement
|
||||
unpause_subscription: Reprendre Abonnement
|
||||
cancel_subscription: Annuler Abonnement
|
||||
filters:
|
||||
query_placeholder: "Recherche par email"
|
||||
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"
|
||||
@@ -994,6 +1017,8 @@ fr_CA:
|
||||
charges_not_allowed: Le débit automatique sur carte de paiement n'a pas été autorisé par l'acheteur
|
||||
no_default_card: L'acheteur n'a pas de carte de paiement disponible pour le débit
|
||||
card_ok: L'acheteur a une carte de paiement disponible pour le débit
|
||||
begins_at_placeholder: "Sélectionnez une date"
|
||||
ends_at_placeholder: "Facultatif"
|
||||
loading_flash:
|
||||
loading: Abonnements en cours de chargement
|
||||
review:
|
||||
@@ -1007,6 +1032,7 @@ fr_CA:
|
||||
saved: "Enregistré"
|
||||
product_already_in_order: Ce produit a déjà été ajouté à la commande. Veuillez directement modifier la quantité.
|
||||
stock:
|
||||
insufficient_stock: "Stock disponible insuffisant"
|
||||
out_of_stock: "En rupture de stock"
|
||||
orders:
|
||||
number: Nombre
|
||||
@@ -1015,6 +1041,7 @@ fr_CA:
|
||||
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: "Si vous avez un cycle de vente ouvert pour ce rythme d'abonnement, une commande sera créée pour cet acheteur. Êtes-vous sûr de vouloir relancer 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."
|
||||
@@ -1052,8 +1079,12 @@ fr_CA:
|
||||
show_on_map: "Tout afficher sur la carte"
|
||||
shared:
|
||||
menu:
|
||||
cart:
|
||||
cart: "Panier"
|
||||
signed_in:
|
||||
profile: "Profil"
|
||||
mobile_menu:
|
||||
cart: "Panier"
|
||||
joyride:
|
||||
checkout: "Passer la commande"
|
||||
already_ordered_products: "Déjà commandé dans ce cycle de vente"
|
||||
@@ -1090,7 +1121,11 @@ fr_CA:
|
||||
shop:
|
||||
messages:
|
||||
login: "se connecter"
|
||||
signup: "inscrivez-vous"
|
||||
contact: "contact"
|
||||
require_customer_login: "Seul les acheteurs autorisés peuvent accéder à cette boutique."
|
||||
require_login_html: "Si vous êtes déjà autorisé à accéder à la boutique, %{login}ou %{signup} pour continuer. Si vous voulez demander à y accéder, veuillez %{contact} %{enterprise}. "
|
||||
require_customer_html: "Si vous voulez demander à y accéder, veuillez %{contact} %{enterprise}."
|
||||
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}"
|
||||
@@ -1127,6 +1162,7 @@ fr_CA:
|
||||
menu_4_title: "Groupes"
|
||||
menu_4_url: "/groups"
|
||||
menu_5_title: "A propos"
|
||||
menu_5_url: "https://about.openfoodnetwork.org.au/"
|
||||
menu_6_title: "Se connecter"
|
||||
menu_6_url: "https://openfoodnetwork.org/au/connect/"
|
||||
menu_7_title: "Apprendre"
|
||||
@@ -1563,6 +1599,7 @@ fr_CA:
|
||||
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: "L'inscription sur OFFrance est gratuite. Opening and running a shop on OFN is free up to $500 of monthly sales. If you sell more we will invoice you for 2% of sales to a maximum of $100/month. For more detail on pricing visit the Software Platform section via the About link in the top menu."
|
||||
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
|
||||
@@ -1599,7 +1636,7 @@ fr_CA:
|
||||
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_your_order_has_been_cancelled: "VotrVotre commande a été annuléee 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:
|
||||
@@ -1678,6 +1715,7 @@ fr_CA:
|
||||
introduction:
|
||||
registration_greeting: "Bonjour!"
|
||||
registration_intro: "Vous pouvez maintenant créer votre profil \"Producteur\" ou \"Hub\""
|
||||
registration_checklist: "De quoi ai-je besoin ?"
|
||||
registration_time: "5-10 minutes"
|
||||
registration_enterprise_address: "L'adresse de l'entreprise"
|
||||
registration_contact_details: "Les détails du contact référent"
|
||||
@@ -1932,6 +1970,7 @@ fr_CA:
|
||||
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)
|
||||
unit_name: "Unité"
|
||||
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 / ...\""
|
||||
@@ -2167,6 +2206,7 @@ fr_CA:
|
||||
payment_methods: "Méthodes de paiement"
|
||||
payment_method_fee: "Frais de transaction"
|
||||
payment_processing_failed: "Le paiement n'a pas pu être traité, veuillez vérifier les informations saisies"
|
||||
payment_method_not_supported: "Cette méthode de paiement n'est pas maintenue. Veuillez en sélectionner une autre."
|
||||
payment_updated: "Paiement mis à jour"
|
||||
inventory_settings: "Paramètres catalogue boutique"
|
||||
tag_rules: "Règles de tag"
|
||||
@@ -2367,8 +2407,20 @@ fr_CA:
|
||||
severity: Gravité
|
||||
description: Description
|
||||
resolve: Résoudre
|
||||
tag_rules:
|
||||
shipping_method_tagged_top: "Les méthodes de livraison taggées"
|
||||
shipping_method_tagged_bottom: "sont:"
|
||||
payment_method_tagged_top: "Les méthodes de payments taguées"
|
||||
payment_method_tagged_bottom: "sont:"
|
||||
order_cycle_tagged_top: "Les cycles de vente taggés"
|
||||
order_cycle_tagged_bottom: "sont:"
|
||||
inventory_tagged_top: "Les variantes du catalogue boutique taggées"
|
||||
inventory_tagged_bottom: "sont:"
|
||||
new_tag_rule_dialog:
|
||||
select_rule_type: "Choisir le type de règle:"
|
||||
add_rule: "Ajouter une règle"
|
||||
enterprise_fees:
|
||||
inherit_from_product: "Hériter du produit"
|
||||
orders:
|
||||
index:
|
||||
per_page: " %{results}par page"
|
||||
@@ -2429,6 +2481,7 @@ fr_CA:
|
||||
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)"
|
||||
available: "Disponible"
|
||||
selected: "sélectionné"
|
||||
customers:
|
||||
index:
|
||||
add_customer: "Ajouter un acheteur"
|
||||
@@ -2623,6 +2676,7 @@ fr_CA:
|
||||
fill_in_customer_info: "Veuillez saisir les informations acheteur"
|
||||
new_payment: "Nouveau paiement"
|
||||
capture: "Payée"
|
||||
void: "Annulé"
|
||||
configurations: "Configurations"
|
||||
general_settings: "Configurations générales"
|
||||
site_name: "Nom du site"
|
||||
@@ -2724,7 +2778,16 @@ fr_CA:
|
||||
no_results: "Pas de résultats"
|
||||
create: "Créer"
|
||||
loading: "Chargement en cours"
|
||||
flat_percent: "Pourcentage net"
|
||||
per_kg: "Par kg"
|
||||
amount: "Montant"
|
||||
currency: "Devise"
|
||||
first_item: "Coût du premier produit"
|
||||
additional_item: "Coût du premier produit"
|
||||
max_items: "Produits max."
|
||||
minimal_amount: "Montant minimal"
|
||||
normal_amount: "Montant classique"
|
||||
discount_amount: "Réduction"
|
||||
email: Email
|
||||
account_updated: "Compte mis à jour!"
|
||||
my_account: "Mon compte"
|
||||
@@ -2734,6 +2797,7 @@ fr_CA:
|
||||
inventory: Catalogue boutique
|
||||
zipcode: Code postal
|
||||
weight: Poids (au kg)
|
||||
error_user_destroy_with_orders: "Les utilisateurs avec des commandes finalisées pourraient ne pas être supprimés"
|
||||
actions:
|
||||
update: "Mettre à jour"
|
||||
errors:
|
||||
@@ -2801,6 +2865,8 @@ fr_CA:
|
||||
title: "Distribution"
|
||||
distributor: "Distributeur : "
|
||||
order_cycle: "Cycle de vente : "
|
||||
line_item_adjustments: "Ajustements sur la ligne produit"
|
||||
order_adjustments: "Ajustements sur la commande"
|
||||
order_total: "Total Commande:"
|
||||
overview:
|
||||
enterprises_header:
|
||||
@@ -2821,15 +2887,29 @@ fr_CA:
|
||||
shipping_methods:
|
||||
index:
|
||||
shipping_methods: "Méthodes de livraison"
|
||||
new_shipping_method: "Nouvelle méthode de livraison"
|
||||
name: "Nom"
|
||||
products_distributor: "Hub-distributeur"
|
||||
zone: "Zone"
|
||||
calculator: "Calculateur"
|
||||
display: "Afficher"
|
||||
both: "Les deux"
|
||||
front_end: "Front End"
|
||||
back_end: "Back End"
|
||||
no_shipping_methods_found: "Aucune méthode de livraison trouvée"
|
||||
new:
|
||||
new_shipping_method: "Nouvelle méthode de livraison"
|
||||
back_to_shipping_methods_list: "Retour à la liste des méthodes de livraison"
|
||||
edit:
|
||||
editing_shipping_method: "Modifier la méthode de livraison"
|
||||
new: "Nouveau"
|
||||
back_to_shipping_methods_list: "Retour à la liste des méthodes de livraison"
|
||||
form:
|
||||
categories: "Conditions de transport"
|
||||
zones: "Zones"
|
||||
both: "Les deux"
|
||||
front_end: "Front End"
|
||||
back_end: "Back End"
|
||||
payment_methods:
|
||||
new:
|
||||
new_payment_method: "Nouvelle méthode de paiement"
|
||||
@@ -2856,6 +2936,7 @@ fr_CA:
|
||||
error_saving_payment: Erreur à l'enregistrement du paiement
|
||||
submitting_payment: Envoi du paiement...
|
||||
products:
|
||||
image_upload_error: "L'image du produit n'a pas été reconnue. Veuillez importer une image au format PNG ou JPG."
|
||||
new:
|
||||
title: 'Nouveau Produit'
|
||||
unit_name_placeholder: 'ex: botte'
|
||||
@@ -2956,6 +3037,7 @@ fr_CA:
|
||||
line_item:
|
||||
insufficient_stock: "Stock disponible insuffisant, il n'en reste que %{on_hand}"
|
||||
out_of_stock: "En rupture de stock"
|
||||
unavailable_item: "Non disponible"
|
||||
shipment_states:
|
||||
backorder: réapprovisionnement
|
||||
partial: partiel
|
||||
@@ -2975,6 +3057,8 @@ fr_CA:
|
||||
invalid: invalide
|
||||
order_mailer:
|
||||
cancel_email:
|
||||
customer_greeting: "Bonjour %{name}!"
|
||||
instructions: "Votre commande a été ANNULÉE. Vous trouverez ci-dessous les informations concernant cette commande. "
|
||||
order_summary_canceled: "Résumé de la commande [ANNULEE]"
|
||||
subject: "Annulation de Commande"
|
||||
confirm_email:
|
||||
|
||||
@@ -244,6 +244,7 @@ nb:
|
||||
reset_password_token: Tilbakestill passordtoken
|
||||
expired: er utløpt, vennligst be om en ny
|
||||
back_to_payments_list: "Tilbake til betalingsliste"
|
||||
maestro_or_solo_cards: "Maestro/Solo kort"
|
||||
actions:
|
||||
create_and_add_another: "Opprett og legg til en annen"
|
||||
create: "Opprett"
|
||||
@@ -452,6 +453,8 @@ nb:
|
||||
encoding_error: "Vennligst sjekk språkinnstillingen til kildefilen din og kontroller at den er lagret med UTF-8-koding"
|
||||
unexpected_error: "Produktimport støtte på en uventet feil ved åpning av filen: %{error_message}"
|
||||
index:
|
||||
notice: "Beskjed"
|
||||
beta_notice: "Denne funksjonen er fremdeles i beta: du kan oppleve noen feil mens du bruker den. Ikke nøl med å kontakte support."
|
||||
select_file: Velg et regneark for å laste opp
|
||||
spreadsheet: Regneark
|
||||
choose_import_type: Velg importtype
|
||||
|
||||
@@ -88,38 +88,6 @@ Openfoodnetwork::Application.routes.draw do
|
||||
get '/:id/shop', to: 'enterprises#shop', as: 'enterprise_shop'
|
||||
get "/enterprises/:permalink", to: redirect("/") # Legacy enterprise URL
|
||||
|
||||
namespace :api do
|
||||
resources :enterprises do
|
||||
post :update_image, on: :member
|
||||
get :managed, on: :collection
|
||||
get :accessible, on: :collection
|
||||
|
||||
resource :logo, only: [:destroy]
|
||||
resource :promo_image, only: [:destroy]
|
||||
|
||||
member do
|
||||
get :shopfront
|
||||
end
|
||||
end
|
||||
|
||||
resources :order_cycles do
|
||||
get :managed, on: :collection
|
||||
get :accessible, on: :collection
|
||||
end
|
||||
|
||||
resources :orders, only: [:index]
|
||||
|
||||
resource :status do
|
||||
get :job_queue
|
||||
end
|
||||
|
||||
resources :customers, only: [:index, :update]
|
||||
|
||||
resources :enterprise_fees, only: [:destroy]
|
||||
|
||||
post '/product_images/:product_id', to: 'product_images#update_product_image'
|
||||
end
|
||||
|
||||
get 'sitemap.xml', to: 'sitemap#index', defaults: { format: 'xml' }
|
||||
|
||||
# Mount engine routes
|
||||
|
||||
48
config/routes/api.rb
Normal file
48
config/routes/api.rb
Normal file
@@ -0,0 +1,48 @@
|
||||
Openfoodnetwork::Application.routes.draw do
|
||||
namespace :api do
|
||||
resources :products do
|
||||
collection do
|
||||
get :bulk_products
|
||||
get :overridable
|
||||
end
|
||||
delete :soft_delete
|
||||
post :clone
|
||||
|
||||
resources :variants do
|
||||
delete :soft_delete
|
||||
end
|
||||
end
|
||||
|
||||
resources :variants, :only => [:index]
|
||||
|
||||
resources :enterprises do
|
||||
post :update_image, on: :member
|
||||
get :managed, on: :collection
|
||||
get :accessible, on: :collection
|
||||
|
||||
resource :logo, only: [:destroy]
|
||||
resource :promo_image, only: [:destroy]
|
||||
|
||||
member do
|
||||
get :shopfront
|
||||
end
|
||||
end
|
||||
|
||||
resources :order_cycles do
|
||||
get :managed, on: :collection
|
||||
get :accessible, on: :collection
|
||||
end
|
||||
|
||||
resources :orders, only: [:index]
|
||||
|
||||
resource :status do
|
||||
get :job_queue
|
||||
end
|
||||
|
||||
resources :customers, only: [:index, :update]
|
||||
|
||||
resources :enterprise_fees, only: [:destroy]
|
||||
|
||||
post '/product_images/:product_id', to: 'product_images#update_product_image'
|
||||
end
|
||||
end
|
||||
@@ -62,22 +62,27 @@ Spree::Core::Engine.routes.prepend do
|
||||
get :authorise_api, on: :collection
|
||||
end
|
||||
|
||||
resources :products do
|
||||
collection do
|
||||
get :managed
|
||||
get :bulk_products
|
||||
get :overridable
|
||||
end
|
||||
delete :soft_delete
|
||||
post :clone
|
||||
resources :orders do
|
||||
get :managed, on: :collection
|
||||
|
||||
resources :variants do
|
||||
delete :soft_delete
|
||||
resources :shipments, :only => [:create, :update] do
|
||||
member do
|
||||
put :ready
|
||||
put :ship
|
||||
put :add
|
||||
put :remove
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
resources :orders do
|
||||
get :managed, on: :collection
|
||||
resources :taxons, :only => [:index]
|
||||
|
||||
resources :taxonomies do
|
||||
resources :taxons do
|
||||
member do
|
||||
get :jstree
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -105,6 +110,13 @@ Spree::Core::Engine.routes.prepend do
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
resources :users do
|
||||
member do
|
||||
put :generate_api_key
|
||||
put :clear_api_key
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
resources :orders do
|
||||
|
||||
17
db/migrate/20190701002454_convert_string_fields_to_text.rb
Normal file
17
db/migrate/20190701002454_convert_string_fields_to_text.rb
Normal file
@@ -0,0 +1,17 @@
|
||||
class ConvertStringFieldsToText < ActiveRecord::Migration
|
||||
def up
|
||||
change_column :enterprises, :description, :text
|
||||
change_column :enterprises, :pickup_times, :text
|
||||
change_column :exchanges, :pickup_time, :text
|
||||
change_column :exchanges, :pickup_instructions, :text
|
||||
change_column :exchanges, :receival_instructions, :text
|
||||
end
|
||||
|
||||
def down
|
||||
change_column :enterprises, :description, :string
|
||||
change_column :enterprises, :pickup_times, :string
|
||||
change_column :exchanges, :pickup_time, :string
|
||||
change_column :exchanges, :pickup_instructions, :string
|
||||
change_column :exchanges, :receival_instructions, :string
|
||||
end
|
||||
end
|
||||
12
db/schema.rb
12
db/schema.rb
@@ -11,7 +11,7 @@
|
||||
#
|
||||
# It's strongly recommended to check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema.define(:version => 20190506194625) do
|
||||
ActiveRecord::Schema.define(:version => 20190701002454) do
|
||||
|
||||
create_table "adjustment_metadata", :force => true do |t|
|
||||
t.integer "adjustment_id"
|
||||
@@ -175,7 +175,7 @@ ActiveRecord::Schema.define(:version => 20190506194625) do
|
||||
|
||||
create_table "enterprises", :force => true do |t|
|
||||
t.string "name"
|
||||
t.string "description"
|
||||
t.text "description"
|
||||
t.text "long_description"
|
||||
t.boolean "is_primary_producer"
|
||||
t.string "contact_name"
|
||||
@@ -185,7 +185,7 @@ ActiveRecord::Schema.define(:version => 20190506194625) do
|
||||
t.string "abn"
|
||||
t.string "acn"
|
||||
t.integer "address_id"
|
||||
t.string "pickup_times"
|
||||
t.text "pickup_times"
|
||||
t.string "next_collection_at"
|
||||
t.datetime "created_at", :null => false
|
||||
t.datetime "updated_at", :null => false
|
||||
@@ -247,12 +247,12 @@ ActiveRecord::Schema.define(:version => 20190506194625) do
|
||||
t.integer "order_cycle_id"
|
||||
t.integer "sender_id"
|
||||
t.integer "receiver_id"
|
||||
t.string "pickup_time"
|
||||
t.string "pickup_instructions"
|
||||
t.text "pickup_time"
|
||||
t.text "pickup_instructions"
|
||||
t.datetime "created_at", :null => false
|
||||
t.datetime "updated_at", :null => false
|
||||
t.boolean "incoming", :default => false, :null => false
|
||||
t.string "receival_instructions"
|
||||
t.text "receival_instructions"
|
||||
end
|
||||
|
||||
add_index "exchanges", ["order_cycle_id"], :name => "index_exchanges_on_order_cycle_id"
|
||||
|
||||
33
lib/spree/api/controller_setup.rb
Normal file
33
lib/spree/api/controller_setup.rb
Normal file
@@ -0,0 +1,33 @@
|
||||
require 'spree/api/responders'
|
||||
|
||||
module Spree
|
||||
module Api
|
||||
module ControllerSetup
|
||||
def self.included(klass)
|
||||
klass.class_eval do
|
||||
include AbstractController::Rendering
|
||||
include AbstractController::ViewPaths
|
||||
include AbstractController::Callbacks
|
||||
include AbstractController::Helpers
|
||||
|
||||
include ActiveSupport::Rescuable
|
||||
|
||||
include ActionController::Rendering
|
||||
include ActionController::ImplicitRender
|
||||
include ActionController::Rescue
|
||||
include ActionController::MimeResponds
|
||||
include ActionController::Head
|
||||
|
||||
include CanCan::ControllerAdditions
|
||||
include Spree::Core::ControllerHelpers::Auth
|
||||
|
||||
prepend_view_path Rails.root + "app/views"
|
||||
append_view_path File.expand_path("../../../app/views", File.dirname(__FILE__))
|
||||
|
||||
self.responder = Spree::Api::Responders::AppResponder
|
||||
respond_to :json
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,7 +0,0 @@
|
||||
require 'spree/api/testing_support/helpers'
|
||||
|
||||
Spree::Api::TestingSupport::Helpers.class_eval do
|
||||
def current_api_user
|
||||
@current_api_user ||= Spree::LegacyUser.new(email: "spree@example.com", enterprises: [])
|
||||
end
|
||||
end
|
||||
@@ -3,7 +3,6 @@ require 'spec_helper'
|
||||
module Api
|
||||
describe CustomersController, type: :controller do
|
||||
include AuthenticationWorkflow
|
||||
include OpenFoodNetwork::ApiHelper
|
||||
render_views
|
||||
|
||||
let(:user) { create(:user) }
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
require 'spec_helper'
|
||||
require 'spree/api/testing_support/helpers'
|
||||
|
||||
module Api
|
||||
describe OrdersController, type: :controller do
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
require 'spec_helper'
|
||||
require 'spree/api/testing_support/helpers'
|
||||
|
||||
module Api
|
||||
describe ProductImagesController, type: :controller do
|
||||
|
||||
287
spec/controllers/api/products_controller_spec.rb
Normal file
287
spec/controllers/api/products_controller_spec.rb
Normal file
@@ -0,0 +1,287 @@
|
||||
require 'spec_helper'
|
||||
|
||||
describe Api::ProductsController, type: :controller do
|
||||
render_views
|
||||
|
||||
let(:supplier) { create(:supplier_enterprise) }
|
||||
let(:supplier2) { create(:supplier_enterprise) }
|
||||
let!(:product) { create(:product, supplier: supplier) }
|
||||
let!(:inactive_product) { create(:product, available_on: Time.zone.now.tomorrow, name: "inactive") }
|
||||
let(:product_other_supplier) { create(:product, supplier: supplier2) }
|
||||
let(:product_with_image) { create(:product_with_image, supplier: supplier) }
|
||||
let(:attributes) { ["id", "name", "supplier", "price", "on_hand", "available_on", "permalink_live"] }
|
||||
let(:all_attributes) { ["id", "name", "price", "available_on", "variants"] }
|
||||
let(:variants_attributes) { ["id", "options_text", "unit_value", "unit_description", "unit_to_display", "on_demand", "display_as", "display_name", "name_to_display", "sku", "on_hand", "price"] }
|
||||
|
||||
let(:current_api_user) { build(:user) }
|
||||
|
||||
before do
|
||||
allow(controller).to receive(:spree_current_user) { current_api_user }
|
||||
end
|
||||
|
||||
context "as a normal user" do
|
||||
before do
|
||||
allow(current_api_user)
|
||||
.to receive(:has_spree_role?).with("admin").and_return(false)
|
||||
end
|
||||
|
||||
it "gets a single product" do
|
||||
product.master.images.create!(attachment: image("thinking-cat.jpg"))
|
||||
product.variants.create!(unit_value: "1", unit_description: "thing")
|
||||
product.variants.first.images.create!(attachment: image("thinking-cat.jpg"))
|
||||
product.set_property("spree", "rocks")
|
||||
api_get :show, id: product.to_param
|
||||
|
||||
expect(all_attributes.all?{ |attr| json_response.keys.include? attr }).to eq(true)
|
||||
expect(variants_attributes.all?{ |attr| json_response['variants'].first.keys.include? attr }).to eq(true)
|
||||
end
|
||||
|
||||
context "finds a product by permalink first then by id" do
|
||||
let!(:other_product) { create(:product, permalink: "these-are-not-the-droids-you-are-looking-for") }
|
||||
|
||||
before do
|
||||
product.update_attribute(:permalink, "#{other_product.id}-and-1-ways")
|
||||
end
|
||||
|
||||
specify do
|
||||
api_get :show, id: product.to_param
|
||||
|
||||
expect(json_response["permalink_live"]).to match(/and-1-ways/)
|
||||
product.destroy
|
||||
|
||||
api_get :show, id: other_product.id
|
||||
expect(json_response["permalink_live"]).to match(/droids/)
|
||||
end
|
||||
end
|
||||
|
||||
it "cannot see inactive products" do
|
||||
api_get :show, id: inactive_product.to_param
|
||||
|
||||
expect(json_response["error"]).to eq("The resource you were looking for could not be found.")
|
||||
expect(response.status).to eq(404)
|
||||
end
|
||||
|
||||
it "returns a 404 error when it cannot find a product" do
|
||||
api_get :show, id: "non-existant"
|
||||
|
||||
expect(json_response["error"]).to eq("The resource you were looking for could not be found.")
|
||||
expect(response.status).to eq(404)
|
||||
end
|
||||
|
||||
include_examples "modifying product actions are restricted"
|
||||
end
|
||||
|
||||
context "as an enterprise user" do
|
||||
let(:current_api_user) { supplier_enterprise_user(supplier) }
|
||||
|
||||
it "soft deletes my products" do
|
||||
spree_delete :soft_delete, product_id: product.to_param, format: :json
|
||||
|
||||
expect(response.status).to eq(204)
|
||||
expect { product.reload }.not_to raise_error
|
||||
expect(product.deleted_at).not_to be_nil
|
||||
end
|
||||
|
||||
it "is denied access to soft deleting another enterprises' product" do
|
||||
spree_delete :soft_delete, product_id: product_other_supplier.to_param, format: :json
|
||||
|
||||
assert_unauthorized!
|
||||
expect { product_other_supplier.reload }.not_to raise_error
|
||||
expect(product_other_supplier.deleted_at).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context "as an administrator" do
|
||||
before do
|
||||
allow(current_api_user)
|
||||
.to receive(:has_spree_role?).with("admin").and_return(true)
|
||||
end
|
||||
|
||||
it "soft deletes a product" do
|
||||
spree_delete :soft_delete, product_id: product.to_param, format: :json
|
||||
|
||||
expect(response.status).to eq(204)
|
||||
expect { product.reload }.not_to raise_error
|
||||
expect(product.deleted_at).not_to be_nil
|
||||
end
|
||||
|
||||
it "can create a new product" do
|
||||
api_post :create, product: { name: "The Other Product",
|
||||
price: 19.99,
|
||||
shipping_category_id: create(:shipping_category).id,
|
||||
supplier_id: supplier.id,
|
||||
primary_taxon_id: FactoryBot.create(:taxon).id,
|
||||
variant_unit: "items",
|
||||
variant_unit_name: "things",
|
||||
unit_description: "things" }
|
||||
|
||||
expect(all_attributes.all?{ |attr| json_response.keys.include? attr }).to eq(true)
|
||||
expect(response.status).to eq(201)
|
||||
end
|
||||
|
||||
it "cannot create a new product with invalid attributes" do
|
||||
api_post :create, product: {}
|
||||
|
||||
expect(response.status).to eq(422)
|
||||
expect(json_response["error"]).to eq("Invalid resource. Please fix errors and try again.")
|
||||
errors = json_response["errors"]
|
||||
expect(errors.keys).to match_array(["name", "price", "primary_taxon", "shipping_category_id", "supplier", "variant_unit"])
|
||||
end
|
||||
|
||||
it "can update a product" do
|
||||
api_put :update, id: product.to_param, product: { name: "New and Improved Product!" }
|
||||
|
||||
expect(response.status).to eq(200)
|
||||
end
|
||||
|
||||
it "cannot update a product with an invalid attribute" do
|
||||
api_put :update, id: product.to_param, product: { name: "" }
|
||||
|
||||
expect(response.status).to eq(422)
|
||||
expect(json_response["error"]).to eq("Invalid resource. Please fix errors and try again.")
|
||||
expect(json_response["errors"]["name"]).to eq(["can't be blank"])
|
||||
end
|
||||
|
||||
it "can delete a product" do
|
||||
expect(product.deleted_at).to be_nil
|
||||
api_delete :destroy, id: product.to_param
|
||||
|
||||
expect(response.status).to eq(204)
|
||||
expect(product.reload.deleted_at).not_to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
describe '#clone' do
|
||||
context 'as a normal user' do
|
||||
before do
|
||||
allow(current_api_user)
|
||||
.to receive(:has_spree_role?).with("admin").and_return(false)
|
||||
end
|
||||
|
||||
it 'denies access' do
|
||||
spree_post :clone, product_id: product.id, format: :json
|
||||
|
||||
assert_unauthorized!
|
||||
end
|
||||
end
|
||||
|
||||
context 'as an enterprise user' do
|
||||
let(:current_api_user) { supplier_enterprise_user(supplier) }
|
||||
|
||||
it 'responds with a successful response' do
|
||||
spree_post :clone, product_id: product.id, format: :json
|
||||
|
||||
expect(response.status).to eq(201)
|
||||
end
|
||||
|
||||
it 'clones the product' do
|
||||
spree_post :clone, product_id: product.id, format: :json
|
||||
|
||||
expect(json_response['name']).to eq("COPY OF #{product.name}")
|
||||
end
|
||||
|
||||
it 'clones a product with image' do
|
||||
spree_post :clone, product_id: product_with_image.id, format: :json
|
||||
|
||||
expect(response.status).to eq(201)
|
||||
expect(json_response['name']).to eq("COPY OF #{product_with_image.name}")
|
||||
end
|
||||
end
|
||||
|
||||
context 'as an administrator' do
|
||||
before do
|
||||
allow(current_api_user)
|
||||
.to receive(:has_spree_role?).with("admin").and_return(true)
|
||||
end
|
||||
|
||||
it 'responds with a successful response' do
|
||||
spree_post :clone, product_id: product.id, format: :json
|
||||
|
||||
expect(response.status).to eq(201)
|
||||
end
|
||||
|
||||
it 'clones the product' do
|
||||
spree_post :clone, product_id: product.id, format: :json
|
||||
|
||||
expect(json_response['name']).to eq("COPY OF #{product.name}")
|
||||
end
|
||||
|
||||
it 'clones a product with image' do
|
||||
spree_post :clone, product_id: product_with_image.id, format: :json
|
||||
|
||||
expect(response.status).to eq(201)
|
||||
expect(json_response['name']).to eq("COPY OF #{product_with_image.name}")
|
||||
end
|
||||
end
|
||||
|
||||
describe '#bulk_products' do
|
||||
context "as an enterprise user" do
|
||||
let!(:taxon) { create(:taxon) }
|
||||
let!(:product2) { create(:product, supplier: supplier, primary_taxon: taxon) }
|
||||
let!(:product3) { create(:product, supplier: supplier2, primary_taxon: taxon) }
|
||||
let!(:product4) { create(:product, supplier: supplier2) }
|
||||
let(:current_api_user) { supplier_enterprise_user(supplier) }
|
||||
|
||||
before { current_api_user.enterprise_roles.create(enterprise: supplier2) }
|
||||
|
||||
it "returns a list of products" do
|
||||
api_get :bulk_products, { page: 1, per_page: 15 }, format: :json
|
||||
expect(returned_product_ids).to eq [product4.id, product3.id, product2.id, inactive_product.id, product.id]
|
||||
end
|
||||
|
||||
it "returns pagination data" do
|
||||
api_get :bulk_products, { page: 1, per_page: 15 }, format: :json
|
||||
expect(json_response['pagination']).to eq "results" => 5, "pages" => 1, "page" => 1, "per_page" => 15
|
||||
end
|
||||
|
||||
it "uses defaults when page and per_page are not supplied" do
|
||||
api_get :bulk_products, format: :json
|
||||
expect(json_response['pagination']).to eq "results" => 5, "pages" => 1, "page" => 1, "per_page" => 15
|
||||
end
|
||||
|
||||
it "returns paginated products by page" do
|
||||
api_get :bulk_products, { page: 1, per_page: 2 }, format: :json
|
||||
expect(returned_product_ids).to eq [product4.id, product3.id]
|
||||
|
||||
api_get :bulk_products, { page: 2, per_page: 2 }, format: :json
|
||||
expect(returned_product_ids).to eq [product2.id, inactive_product.id]
|
||||
end
|
||||
|
||||
it "filters results by supplier" do
|
||||
api_get :bulk_products, { page: 1, per_page: 15, q: {supplier_id_eq: supplier.id} }, format: :json
|
||||
expect(returned_product_ids).to eq [product2.id, inactive_product.id, product.id]
|
||||
end
|
||||
|
||||
it "filters results by product category" do
|
||||
api_get :bulk_products, { page: 1, per_page: 15, q: {primary_taxon_id_eq: taxon.id} }, format: :json
|
||||
expect(returned_product_ids).to eq [product3.id, product2.id]
|
||||
end
|
||||
|
||||
it "filters results by import_date" do
|
||||
product.variants.first.import_date = 1.day.ago
|
||||
product2.variants.first.import_date = 2.days.ago
|
||||
product3.variants.first.import_date = 1.day.ago
|
||||
|
||||
product.save
|
||||
product2.save
|
||||
product3.save
|
||||
|
||||
api_get :bulk_products, { page: 1, per_page: 15, import_date: 1.day.ago.to_date.to_s }, format: :json
|
||||
expect(returned_product_ids).to eq [product3.id, product.id]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def supplier_enterprise_user(enterprise)
|
||||
user = create(:user)
|
||||
user.enterprise_roles.create(enterprise: enterprise)
|
||||
user
|
||||
end
|
||||
|
||||
def returned_product_ids
|
||||
json_response['products'].map{ |obj| obj['id'] }
|
||||
end
|
||||
end
|
||||
197
spec/controllers/api/variants_controller_spec.rb
Normal file
197
spec/controllers/api/variants_controller_spec.rb
Normal file
@@ -0,0 +1,197 @@
|
||||
require 'spec_helper'
|
||||
|
||||
describe Api::VariantsController, type: :controller do
|
||||
render_views
|
||||
|
||||
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
|
||||
allow(controller).to receive(:spree_current_user) { current_api_user }
|
||||
end
|
||||
|
||||
context "as a normal user" do
|
||||
sign_in_as_user!
|
||||
|
||||
let!(:product) { create(:product) }
|
||||
let!(:variant) do
|
||||
variant = product.master
|
||||
variant.option_values << create(:option_value)
|
||||
variant
|
||||
end
|
||||
|
||||
it "retrieves a list of variants with appropriate attributes" do
|
||||
spree_get :index, format: :json
|
||||
|
||||
keys = json_response.first.keys.map(&:to_sym)
|
||||
expect(attributes.all?{ |attr| keys.include? attr }).to eq(true)
|
||||
end
|
||||
|
||||
it "is denied access when trying to delete a variant" do
|
||||
product = create(:product)
|
||||
variant = product.master
|
||||
spree_delete :soft_delete, variant_id: variant.to_param, product_id: product.to_param, format: :json
|
||||
|
||||
assert_unauthorized!
|
||||
expect { variant.reload }.not_to raise_error
|
||||
expect(variant.deleted_at).to be_nil
|
||||
end
|
||||
|
||||
it 'can query the results through a parameter' do
|
||||
expected_result = create(:variant, sku: 'FOOBAR')
|
||||
api_get :index, q: { sku_cont: 'FOO' }
|
||||
|
||||
expect(json_response.size).to eq(1)
|
||||
expect(json_response.first['sku']).to eq expected_result.sku
|
||||
end
|
||||
|
||||
# Regression test for spree#2141
|
||||
context "a deleted variant" do
|
||||
before do
|
||||
variant.update_column(:deleted_at, Time.zone.now)
|
||||
end
|
||||
|
||||
it "is not returned in the results" do
|
||||
api_get :index
|
||||
expect(json_response.count).to eq(10) # there are 11 variants
|
||||
end
|
||||
|
||||
it "is not returned even when show_deleted is passed" do
|
||||
api_get :index, show_deleted: true
|
||||
expect(json_response.count).to eq(10) # there are 11 variants
|
||||
end
|
||||
end
|
||||
|
||||
it "can see a single variant" do
|
||||
api_get :show, id: variant.to_param
|
||||
|
||||
keys = json_response.keys.map(&:to_sym)
|
||||
expect((attributes).all?{ |attr| keys.include? attr }).to eq(true)
|
||||
end
|
||||
|
||||
it "cannot create a new variant if not an admin" do
|
||||
api_post :create, variant: { sku: "12345" }
|
||||
|
||||
assert_unauthorized!
|
||||
end
|
||||
|
||||
it "cannot update a variant" do
|
||||
api_put :update, id: variant.to_param, variant: { sku: "12345" }
|
||||
|
||||
assert_unauthorized!
|
||||
end
|
||||
|
||||
it "cannot delete a variant" do
|
||||
api_delete :destroy, id: variant.to_param
|
||||
|
||||
assert_unauthorized!
|
||||
expect { variant.reload }.not_to raise_error
|
||||
end
|
||||
end
|
||||
|
||||
context "as an enterprise user" do
|
||||
sign_in_as_enterprise_user! [:supplier]
|
||||
let(:supplier_other) { create(:supplier_enterprise) }
|
||||
let(:product) { create(:product, supplier: supplier) }
|
||||
let(:variant) { product.master }
|
||||
let(:product_other) { create(:product, supplier: supplier_other) }
|
||||
let(:variant_other) { product_other.master }
|
||||
|
||||
it "soft deletes a variant" do
|
||||
spree_delete :soft_delete, variant_id: variant.to_param, product_id: product.to_param, format: :json
|
||||
|
||||
expect(response.status).to eq(204)
|
||||
expect { variant.reload }.not_to raise_error
|
||||
expect(variant.deleted_at).to be_present
|
||||
end
|
||||
|
||||
it "is denied access to soft deleting another enterprises' variant" do
|
||||
spree_delete :soft_delete, variant_id: variant_other.to_param, product_id: product_other.to_param, format: :json
|
||||
|
||||
assert_unauthorized!
|
||||
expect { variant.reload }.not_to raise_error
|
||||
expect(variant.deleted_at).to be_nil
|
||||
end
|
||||
|
||||
context 'when the variant is not the master' do
|
||||
before { variant.update_attribute(:is_master, false) }
|
||||
|
||||
it 'refreshes the cache' do
|
||||
expect(OpenFoodNetwork::ProductsCache).to receive(:variant_destroyed).with(variant)
|
||||
spree_delete :soft_delete, variant_id: variant.id, product_id: variant.product.permalink, format: :json
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "as an administrator" do
|
||||
sign_in_as_admin!
|
||||
|
||||
let(:product) { create(:product) }
|
||||
let(:variant) { product.master }
|
||||
let(:resource_scoping) { { product_id: variant.product.to_param } }
|
||||
|
||||
it "soft deletes a variant" do
|
||||
spree_delete :soft_delete, variant_id: variant.to_param, product_id: product.to_param, format: :json
|
||||
|
||||
expect(response.status).to eq(204)
|
||||
expect { variant.reload }.not_to raise_error
|
||||
expect(variant.deleted_at).not_to be_nil
|
||||
end
|
||||
|
||||
it "doesn't delete the only variant of the product" do
|
||||
product = create(:product)
|
||||
variant = product.variants.first
|
||||
spree_delete :soft_delete, variant_id: variant.to_param, product_id: product.to_param, format: :json
|
||||
|
||||
expect(variant.reload).to_not be_deleted
|
||||
expect(assigns(:variant).errors[:product]).to include "must have at least one variant"
|
||||
end
|
||||
|
||||
context 'when the variant is not the master' do
|
||||
before { variant.update_attribute(:is_master, false) }
|
||||
|
||||
it 'refreshes the cache' do
|
||||
expect(OpenFoodNetwork::ProductsCache).to receive(:variant_destroyed).with(variant)
|
||||
spree_delete :soft_delete, variant_id: variant.id, product_id: variant.product.permalink, format: :json
|
||||
end
|
||||
end
|
||||
|
||||
context "deleted variants" do
|
||||
before do
|
||||
variant.update_column(:deleted_at, Time.zone.now)
|
||||
end
|
||||
|
||||
it "are visible by admin" do
|
||||
api_get :index, show_deleted: 1
|
||||
|
||||
expect(json_response.count).to eq(2)
|
||||
end
|
||||
end
|
||||
|
||||
it "can create a new variant" do
|
||||
original_number_of_variants = variant.product.variants.count
|
||||
api_post :create, variant: { sku: "12345", unit_value: "weight", unit_description: "L" }
|
||||
|
||||
expect(attributes.all?{ |attr| json_response.include? attr.to_s }).to eq(true)
|
||||
expect(response.status).to eq(201)
|
||||
expect(json_response["sku"]).to eq("12345")
|
||||
expect(variant.product.variants.count).to eq(original_number_of_variants + 1)
|
||||
end
|
||||
|
||||
it "can update a variant" do
|
||||
api_put :update, id: variant.to_param, variant: { sku: "12345" }
|
||||
|
||||
expect(response.status).to eq(200)
|
||||
end
|
||||
|
||||
it "can delete a variant" do
|
||||
api_delete :destroy, id: variant.to_param
|
||||
|
||||
expect(response.status).to eq(204)
|
||||
expect { Spree::Variant.find(variant.id) }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -150,15 +150,16 @@ describe Spree::Admin::ProductsController, type: :controller do
|
||||
|
||||
describe "when user uploads an image in an unsupported format" do
|
||||
it "does not throw an exception" do
|
||||
product_image = ActionDispatch::Http::UploadedFile.new({
|
||||
:filename => 'unsupported_image_format.exr',
|
||||
:content_type => 'application/octet-stream',
|
||||
:tempfile => Tempfile.new('unsupported_image_format.exr')
|
||||
})
|
||||
product_image = ActionDispatch::Http::UploadedFile.new(
|
||||
filename: 'unsupported_image_format.exr',
|
||||
content_type: 'application/octet-stream',
|
||||
tempfile: Tempfile.new('unsupported_image_format.exr')
|
||||
)
|
||||
product_attrs_with_image = product_attrs.merge(
|
||||
images_attributes: {
|
||||
'0' => { attachment: product_image }
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
expect do
|
||||
spree_put :create, product: product_attrs_with_image
|
||||
@@ -168,7 +169,7 @@ describe Spree::Admin::ProductsController, type: :controller do
|
||||
end
|
||||
end
|
||||
|
||||
describe "updating" do
|
||||
describe "updating a product" do
|
||||
let(:producer) { create(:enterprise) }
|
||||
let!(:product) { create(:simple_product, supplier: producer) }
|
||||
|
||||
@@ -176,6 +177,23 @@ describe Spree::Admin::ProductsController, type: :controller do
|
||||
login_as_enterprise_user [producer]
|
||||
end
|
||||
|
||||
describe "product stock setting with errors" do
|
||||
it "notifies bugsnag and still raise error" do
|
||||
# forces an error in the variant
|
||||
product.variants.first.stock_items = []
|
||||
|
||||
expect(Bugsnag).to receive(:notify)
|
||||
|
||||
expect do
|
||||
spree_put :update,
|
||||
id: product,
|
||||
product: {
|
||||
on_hand: 1
|
||||
}
|
||||
end.to raise_error(StandardError)
|
||||
end
|
||||
end
|
||||
|
||||
describe "product variant unit is items" do
|
||||
it "clears unit description of all variants of the product" do
|
||||
product.variants.first.update_attribute :unit_description, "grams"
|
||||
|
||||
64
spec/controllers/spree/api/base_controller_spec.rb
Normal file
64
spec/controllers/spree/api/base_controller_spec.rb
Normal file
@@ -0,0 +1,64 @@
|
||||
require 'spec_helper'
|
||||
|
||||
describe Spree::Api::BaseController do
|
||||
render_views
|
||||
controller(Spree::Api::BaseController) do
|
||||
def index
|
||||
render text: { "products" => [] }.to_json
|
||||
end
|
||||
|
||||
def spree_current_user; end
|
||||
end
|
||||
|
||||
context "signed in as a user using an authentication extension" do
|
||||
before do
|
||||
allow(controller).to receive_messages try_spree_current_user:
|
||||
double(email: "spree@example.com")
|
||||
end
|
||||
|
||||
it "can make a request" do
|
||||
api_get :index
|
||||
expect(json_response).to eq( "products" => [] )
|
||||
expect(response.status).to eq(200)
|
||||
end
|
||||
end
|
||||
|
||||
context "cannot make a request to the API" do
|
||||
it "without an API key" do
|
||||
api_get :index
|
||||
expect(json_response).to eq( "error" => "You must specify an API key." )
|
||||
expect(response.status).to eq(401)
|
||||
end
|
||||
|
||||
it "with an invalid API key" do
|
||||
request.env["X-Spree-Token"] = "fake_key"
|
||||
get :index, {}
|
||||
expect(json_response).to eq( "error" => "Invalid API key (fake_key) specified." )
|
||||
expect(response.status).to eq(401)
|
||||
end
|
||||
|
||||
it "using an invalid token param" do
|
||||
get :index, token: "fake_key"
|
||||
expect(json_response).to eq( "error" => "Invalid API key (fake_key) specified." )
|
||||
end
|
||||
end
|
||||
|
||||
it 'handles exceptions' do
|
||||
expect(subject).to receive(:authenticate_user).and_return(true)
|
||||
expect(subject).to receive(:index).and_raise(Exception.new("no joy"))
|
||||
get :index, token: "fake_key"
|
||||
expect(json_response).to eq( "exception" => "no joy" )
|
||||
end
|
||||
|
||||
it "maps symantec keys to nested_attributes keys" do
|
||||
klass = double(nested_attributes_options: { line_items: {},
|
||||
bill_address: {} })
|
||||
attributes = { 'line_items' => { id: 1 },
|
||||
'bill_address' => { id: 2 },
|
||||
'name' => 'test order' }
|
||||
|
||||
mapped = subject.map_nested_attributes_keys(klass, attributes)
|
||||
expect(mapped.key?('line_items_attributes')).to be_truthy
|
||||
expect(mapped.key?('name')).to be_truthy
|
||||
end
|
||||
end
|
||||
@@ -1,35 +0,0 @@
|
||||
require 'spec_helper'
|
||||
|
||||
module Spree
|
||||
describe Spree::Api::LineItemsController, type: :controller do
|
||||
render_views
|
||||
|
||||
before do
|
||||
allow(controller).to receive(:spree_current_user) { current_api_user }
|
||||
end
|
||||
|
||||
# test that when a line item is updated, an order's fees are updated too
|
||||
context "as an admin user" do
|
||||
sign_in_as_admin!
|
||||
|
||||
let(:order) { FactoryBot.create(:order, state: 'complete', completed_at: Time.zone.now) }
|
||||
let(:line_item) { FactoryBot.create(:line_item_with_shipment, order: order, final_weight_volume: 500) }
|
||||
|
||||
context "as a line item is updated" do
|
||||
before { allow(controller).to receive(:order) { order } }
|
||||
|
||||
it "update distribution charge on the order" do
|
||||
line_item_params = {
|
||||
order_id: order.number,
|
||||
id: line_item.id,
|
||||
line_item: { id: line_item.id, final_weight_volume: 520 },
|
||||
format: :json
|
||||
}
|
||||
|
||||
expect(order).to receive(:update_distribution_charge!)
|
||||
spree_post :update, line_item_params
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,180 +0,0 @@
|
||||
require 'spec_helper'
|
||||
|
||||
module Spree
|
||||
describe Spree::Api::ProductsController, type: :controller do
|
||||
render_views
|
||||
|
||||
let(:supplier) { create(:supplier_enterprise) }
|
||||
let(:supplier2) { create(:supplier_enterprise) }
|
||||
let!(:product1) { create(:product, supplier: supplier) }
|
||||
let(:product_other_supplier) { create(:product, supplier: supplier2) }
|
||||
let(:product_with_image) { create(:product_with_image, supplier: supplier) }
|
||||
let(:attributes) { [:id, :name, :supplier, :price, :on_hand, :available_on, :permalink_live] }
|
||||
|
||||
let(:current_api_user) { build(:user) }
|
||||
|
||||
before do
|
||||
allow(controller).to receive(:spree_current_user) { current_api_user }
|
||||
end
|
||||
|
||||
context "as a normal user" do
|
||||
before do
|
||||
allow(current_api_user)
|
||||
.to receive(:has_spree_role?).with("admin").and_return(false)
|
||||
end
|
||||
|
||||
it "should deny me access to managed products" do
|
||||
spree_get :managed, template: 'bulk_index', format: :json
|
||||
assert_unauthorized!
|
||||
end
|
||||
end
|
||||
|
||||
context "as an enterprise user" do
|
||||
let(:current_api_user) do
|
||||
user = create(:user)
|
||||
user.enterprise_roles.create(enterprise: supplier)
|
||||
user
|
||||
end
|
||||
|
||||
it "retrieves a list of managed products" do
|
||||
spree_get :managed, template: 'bulk_index', format: :json
|
||||
keys = json_response.first.keys.map(&:to_sym)
|
||||
expect(attributes.all?{ |attr| keys.include? attr }).to eq(true)
|
||||
end
|
||||
|
||||
it "soft deletes my products" do
|
||||
spree_delete :soft_delete, product_id: product1.to_param, format: :json
|
||||
expect(response.status).to eq(204)
|
||||
expect { product1.reload }.not_to raise_error
|
||||
expect(product1.deleted_at).not_to be_nil
|
||||
end
|
||||
|
||||
it "is denied access to soft deleting another enterprises' product" do
|
||||
spree_delete :soft_delete, product_id: product_other_supplier.to_param, format: :json
|
||||
assert_unauthorized!
|
||||
expect { product_other_supplier.reload }.not_to raise_error
|
||||
expect(product_other_supplier.deleted_at).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context "as an administrator" do
|
||||
before do
|
||||
allow(current_api_user)
|
||||
.to receive(:has_spree_role?).with("admin").and_return(true)
|
||||
end
|
||||
|
||||
it "retrieves a list of managed products" do
|
||||
spree_get :managed, template: 'bulk_index', format: :json
|
||||
keys = json_response.first.keys.map(&:to_sym)
|
||||
expect(attributes.all?{ |attr| keys.include? attr }).to eq(true)
|
||||
end
|
||||
|
||||
it "retrieves a list of products with appropriate attributes" do
|
||||
spree_get :index, template: 'bulk_index', format: :json
|
||||
keys = json_response.first.keys.map(&:to_sym)
|
||||
expect(attributes.all?{ |attr| keys.include? attr }).to eq(true)
|
||||
end
|
||||
|
||||
it "sorts products in ascending id order" do
|
||||
FactoryBot.create(:product, supplier: supplier)
|
||||
FactoryBot.create(:product, supplier: supplier)
|
||||
|
||||
spree_get :index, template: 'bulk_index', format: :json
|
||||
|
||||
ids = json_response.map{ |product| product['id'] }
|
||||
expect(ids[0]).to be < ids[1]
|
||||
expect(ids[1]).to be < ids[2]
|
||||
end
|
||||
|
||||
it "formats available_on to 'yyyy-mm-dd hh:mm'" do
|
||||
spree_get :index, template: 'bulk_index', format: :json
|
||||
expect(json_response.map{ |product| product['available_on'] }.all?{ |a| a.match("^\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}$") }).to eq(true)
|
||||
end
|
||||
|
||||
it "returns permalink as permalink_live" do
|
||||
spree_get :index, template: 'bulk_index', format: :json
|
||||
expect(json_response.detect{ |product| product['id'] == product1.id }['permalink_live']).to eq(product1.permalink)
|
||||
end
|
||||
|
||||
it "should allow available_on to be nil" do
|
||||
spree_get :index, template: 'bulk_index', format: :json
|
||||
expect(json_response.size).to eq(1)
|
||||
|
||||
product5 = FactoryBot.create(:product)
|
||||
product5.available_on = nil
|
||||
product5.save!
|
||||
|
||||
spree_get :index, template: 'bulk_index', format: :json
|
||||
expect(json_response.size).to eq(2)
|
||||
end
|
||||
|
||||
it "soft deletes a product" do
|
||||
spree_delete :soft_delete, product_id: product1.to_param, format: :json
|
||||
expect(response.status).to eq(204)
|
||||
expect { product1.reload }.not_to raise_error
|
||||
expect(product1.deleted_at).not_to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
describe '#clone' do
|
||||
context 'as a normal user' do
|
||||
before do
|
||||
allow(current_api_user)
|
||||
.to receive(:has_spree_role?).with("admin").and_return(false)
|
||||
end
|
||||
|
||||
it 'denies access' do
|
||||
spree_post :clone, product_id: product1.id, format: :json
|
||||
assert_unauthorized!
|
||||
end
|
||||
end
|
||||
|
||||
context 'as an enterprise user' do
|
||||
let(:current_api_user) do
|
||||
user = create(:user)
|
||||
user.enterprise_roles.create(enterprise: supplier)
|
||||
user
|
||||
end
|
||||
|
||||
it 'responds with a successful response' do
|
||||
spree_post :clone, product_id: product1.id, format: :json
|
||||
expect(response.status).to eq(201)
|
||||
end
|
||||
|
||||
it 'clones the product' do
|
||||
spree_post :clone, product_id: product1.id, format: :json
|
||||
expect(json_response['name']).to eq("COPY OF #{product1.name}")
|
||||
end
|
||||
|
||||
it 'clones a product with image' do
|
||||
spree_post :clone, product_id: product_with_image.id, format: :json
|
||||
expect(response.status).to eq(201)
|
||||
expect(json_response['name']).to eq("COPY OF #{product_with_image.name}")
|
||||
end
|
||||
end
|
||||
|
||||
context 'as an administrator' do
|
||||
before do
|
||||
allow(current_api_user)
|
||||
.to receive(:has_spree_role?).with("admin").and_return(true)
|
||||
end
|
||||
|
||||
it 'responds with a successful response' do
|
||||
spree_post :clone, product_id: product1.id, format: :json
|
||||
expect(response.status).to eq(201)
|
||||
end
|
||||
|
||||
it 'clones the product' do
|
||||
spree_post :clone, product_id: product1.id, format: :json
|
||||
expect(json_response['name']).to eq("COPY OF #{product1.name}")
|
||||
end
|
||||
|
||||
it 'clones a product with image' do
|
||||
spree_post :clone, product_id: product_with_image.id, format: :json
|
||||
expect(response.status).to eq(201)
|
||||
expect(json_response['name']).to eq("COPY OF #{product_with_image.name}")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -5,12 +5,27 @@ describe Spree::Api::ShipmentsController, type: :controller do
|
||||
|
||||
let!(:shipment) { create(:shipment) }
|
||||
let!(:attributes) { [:id, :tracking, :number, :cost, :shipped_at, :stock_location_name, :order_id, :shipping_rates, :shipping_method, :inventory_units] }
|
||||
let!(:resource_scoping) { { order_id: shipment.order.to_param, id: shipment.to_param } }
|
||||
let(:current_api_user) { build(:user) }
|
||||
|
||||
before do
|
||||
allow(controller).to receive(:spree_current_user) { current_api_user }
|
||||
end
|
||||
|
||||
context "as a non-admin" do
|
||||
it "cannot make a shipment ready" do
|
||||
api_put :ready
|
||||
assert_unauthorized!
|
||||
end
|
||||
|
||||
it "cannot make a shipment shipped" do
|
||||
api_put :ship
|
||||
assert_unauthorized!
|
||||
end
|
||||
end
|
||||
|
||||
context "as an admin" do
|
||||
let(:current_api_user) { build(:admin_user) }
|
||||
let!(:order) { shipment.order }
|
||||
let(:order_ship_address) { create(:address) }
|
||||
let!(:stock_location) { create(:stock_location_with_items) }
|
||||
@@ -30,8 +45,6 @@ describe Spree::Api::ShipmentsController, type: :controller do
|
||||
shipment.shipping_method.distributors << variant.product.supplier
|
||||
end
|
||||
|
||||
sign_in_as_admin!
|
||||
|
||||
context '#create' do
|
||||
it 'creates a shipment if order does not have a shipment' do
|
||||
order.shipment.destroy
|
||||
@@ -77,6 +90,62 @@ describe Spree::Api::ShipmentsController, type: :controller do
|
||||
end
|
||||
end
|
||||
|
||||
it "can make a shipment ready" do
|
||||
allow_any_instance_of(Spree::Order).to receive_messages(paid?: true, complete?: true)
|
||||
api_put :ready
|
||||
|
||||
expect(attributes.all?{ |attr| json_response.key? attr.to_s }).to be_truthy
|
||||
expect(json_response["state"]).to eq("ready")
|
||||
expect(shipment.reload.state).to eq("ready")
|
||||
end
|
||||
|
||||
it "cannot make a shipment ready if the order is unpaid" do
|
||||
allow_any_instance_of(Spree::Order).to receive_messages(paid?: false)
|
||||
api_put :ready
|
||||
|
||||
expect(json_response["error"]).to eq("Cannot ready shipment.")
|
||||
expect(response.status).to eq(422)
|
||||
end
|
||||
|
||||
context 'for completed shipments' do
|
||||
let(:order) { create :completed_order_with_totals }
|
||||
let!(:resource_scoping) { { order_id: order.to_param, id: order.shipments.first.to_param } }
|
||||
|
||||
it 'adds a variant to a shipment' do
|
||||
api_put :add, variant_id: variant.to_param, quantity: 2
|
||||
expect(response.status).to eq(200)
|
||||
expect(json_response['inventory_units'].select { |h| h['variant_id'] == variant.id }.size).to eq(2)
|
||||
end
|
||||
|
||||
it 'removes a variant from a shipment' do
|
||||
order.contents.add(variant, 2)
|
||||
|
||||
api_put :remove, variant_id: variant.to_param, quantity: 1
|
||||
expect(response.status).to eq(200)
|
||||
expect(json_response['inventory_units'].select { |h| h['variant_id'] == variant.id }.size).to eq(1)
|
||||
end
|
||||
end
|
||||
|
||||
context "can transition a shipment from ready to ship" do
|
||||
before do
|
||||
allow_any_instance_of(Spree::Order).to receive_messages(paid?: true, complete?: true)
|
||||
# For the shipment notification email
|
||||
Spree::Config[:mails_from] = "spree@example.com"
|
||||
|
||||
shipment.update!(shipment.order)
|
||||
expect(shipment.state).to eq("ready")
|
||||
allow_any_instance_of(Spree::ShippingRate).to receive_messages(cost: 5)
|
||||
end
|
||||
|
||||
it "can transition a shipment from ready to ship" do
|
||||
shipment.reload
|
||||
api_put :ship, order_id: shipment.order.to_param, id: shipment.to_param, shipment: { tracking: "123123" }
|
||||
|
||||
expect(attributes.all?{ |attr| json_response.key? attr.to_s }).to be_truthy
|
||||
expect(json_response["state"]).to eq("shipped")
|
||||
end
|
||||
end
|
||||
|
||||
context 'for a completed order with shipment' do
|
||||
let(:order) { create :completed_order_with_totals }
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user