From 3f114af193770740f7bcc55ba1aa293420e15a0d Mon Sep 17 00:00:00 2001 From: Philipp Rothmann Date: Mon, 7 Nov 2022 12:23:54 +0100 Subject: [PATCH 001/105] replace apivore with rswag --- .rubocop_todo.yml | 28 +- Gemfile | 6 +- Gemfile.lock | 21 +- config/initializers/rswag_api.rb | 13 + config/initializers/rswag_ui.rb | 15 + config/routes.rb | 2 + doc/API.md | 6 +- doc/swagger.v1.yml | 1106 ----------------- spec/api/v1/order_articles_spec.rb | 59 - spec/api/v1/swagger_spec.rb | 284 ----- .../v1/user/financial_transactions_spec.rb | 109 -- spec/api/v1/user/group_order_articles_spec.rb | 220 ---- spec/api/v1/user/ordergroup_spec.rb | 55 - spec/app_config.yml | 1 + spec/requests/api/article_categories_spec.rb | 53 + spec/requests/api/configs_spec.rb | 20 + .../api/financial_transaction_classes_spec.rb | 54 + .../api/financial_transaction_types_spec.rb | 52 + .../api/financial_transactions_spec.rb | 56 + spec/requests/api/navigations_spec.rb | 24 + spec/requests/api/order_articles_spec.rb | 115 ++ spec/requests/api/orders_spec.rb | 55 + .../api/user/financial_transactions_spec.rb | 109 ++ .../api/user/group_order_articles_spec.rb | 192 +++ spec/requests/api/user/users_spec.rb | 103 ++ spec/support/api_helper.rb | 77 +- spec/swagger_helper.rb | 513 ++++++++ 27 files changed, 1489 insertions(+), 1859 deletions(-) create mode 100644 config/initializers/rswag_api.rb create mode 100644 config/initializers/rswag_ui.rb delete mode 100644 doc/swagger.v1.yml delete mode 100644 spec/api/v1/order_articles_spec.rb delete mode 100644 spec/api/v1/swagger_spec.rb delete mode 100644 spec/api/v1/user/financial_transactions_spec.rb delete mode 100644 spec/api/v1/user/group_order_articles_spec.rb delete mode 100644 spec/api/v1/user/ordergroup_spec.rb create mode 100644 spec/requests/api/article_categories_spec.rb create mode 100644 spec/requests/api/configs_spec.rb create mode 100644 spec/requests/api/financial_transaction_classes_spec.rb create mode 100644 spec/requests/api/financial_transaction_types_spec.rb create mode 100644 spec/requests/api/financial_transactions_spec.rb create mode 100644 spec/requests/api/navigations_spec.rb create mode 100644 spec/requests/api/order_articles_spec.rb create mode 100644 spec/requests/api/orders_spec.rb create mode 100644 spec/requests/api/user/financial_transactions_spec.rb create mode 100644 spec/requests/api/user/group_order_articles_spec.rb create mode 100644 spec/requests/api/user/users_spec.rb create mode 100644 spec/swagger_helper.rb diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index b7e21eab..1d3cd010 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -266,7 +266,7 @@ Metrics/AbcSize: # Configuration parameters: CountComments, CountAsOne, ExcludedMethods, AllowedMethods, AllowedPatterns, IgnoredMethods, inherit_mode. # AllowedMethods: refine Metrics/BlockLength: - Max: 210 + Max: 212 # Offense count: 6 # Configuration parameters: CountBlocks. @@ -451,6 +451,24 @@ RSpec/DescribedClass: - "spec/models/ordergroup_spec.rb" - "spec/models/user_spec.rb" +# Offense count: 15 +# This cop supports unsafe autocorrection (--autocorrect-all). +RSpec/EmptyExampleGroup: + Exclude: + - 'spec/requests/api/article_categories_spec.rb' + - 'spec/requests/api/configs_spec.rb' + - 'spec/requests/api/financial_transaction_classes_spec.rb' + - 'spec/requests/api/financial_transaction_types_spec.rb' + - 'spec/requests/api/financial_transactions_spec.rb' + - 'spec/requests/api/navigations_spec.rb' + - 'spec/requests/api/order_articles_spec.rb' + - 'spec/requests/api/orders_spec.rb' + - 'spec/requests/api/user/financial_transactions_spec.rb' + - 'spec/requests/api/user/group_order_articles_spec.rb' + - 'spec/requests/api/user/users_spec.rb' + + + # Offense count: 65 # Configuration parameters: CountAsOne. RSpec/ExampleLength: @@ -581,6 +599,14 @@ RSpec/ScatteredSetup: - "spec/integration/balancing_spec.rb" - "spec/integration/login_spec.rb" +# Offense count: 4 +# Configuration parameters: AllowedPatterns, IgnoredPatterns. +# SupportedStyles: snake_case, camelCase +RSpec/VariableName: + EnforcedStyle: snake_case + AllowedPatterns: + - ^Authorization$ + # Offense count: 1 # Configuration parameters: IgnoreNameless, IgnoreSymbolicNames. RSpec/VerifiedDoubles: diff --git a/Gemfile b/Gemfile index a6e27fae..61562099 100644 --- a/Gemfile +++ b/Gemfile @@ -55,6 +55,9 @@ gem 'gaffe' gem 'ruby-filemagic' gem 'mime-types' gem 'midi-smtp-server' +gem 'hashie', '~> 3.4.6', require: false # https://github.com/westfieldlabs/apivore/issues/114 +gem 'rswag-api' +gem 'rswag-ui' # we use the git version of acts_as_versioned, and need to include it in this Gemfile gem 'acts_as_versioned', git: 'https://github.com/technoweenie/acts_as_versioned.git' @@ -116,6 +119,5 @@ group :test do gem 'simplecov', require: false gem 'simplecov-lcov', require: false # api - gem 'apivore', require: false - gem 'hashie', '~> 3.4.6', require: false # https://github.com/westfieldlabs/apivore/issues/114 + gem 'rswag-specs' end diff --git a/Gemfile.lock b/Gemfile.lock index c53687fb..d86fc580 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -109,13 +109,6 @@ GEM activerecord (>= 3.0.0) addressable (2.8.1) public_suffix (>= 2.0.2, < 6.0) - apivore (1.6.2) - actionpack (>= 4, < 6) - hashie (~> 3.3) - json-schema (~> 2.5) - rspec (~> 3) - rspec-expectations (~> 3.1) - rspec-mocks (~> 3.1) apparition (0.6.0) capybara (~> 3.13, < 4) websocket-driver (>= 0.6.5) @@ -430,6 +423,16 @@ GEM rspec-rerun (1.1.0) rspec (~> 3.0) rspec-support (3.11.1) + rswag-api (2.7.0) + railties (>= 3.1, < 7.1) + rswag-specs (2.7.0) + activesupport (>= 3.1, < 7.1) + json-schema (>= 2.2, < 4.0) + railties (>= 3.1, < 7.1) + rspec-core (>= 2.14) + rswag-ui (2.7.0) + actionpack (>= 3.1, < 7.1) + railties (>= 3.1, < 7.1) rubocop (1.36.0) json (~> 2.3) parallel (~> 1.10) @@ -557,7 +560,6 @@ DEPENDENCIES active_model_serializers (~> 0.10.0) acts_as_tree acts_as_versioned! - apivore apparition attribute_normalizer better_errors @@ -617,6 +619,9 @@ DEPENDENCIES rspec-core rspec-rails rspec-rerun + rswag-api + rswag-specs + rswag-ui rubocop rubocop-rails rubocop-rspec diff --git a/config/initializers/rswag_api.rb b/config/initializers/rswag_api.rb new file mode 100644 index 00000000..e4b798f6 --- /dev/null +++ b/config/initializers/rswag_api.rb @@ -0,0 +1,13 @@ +Rswag::Api.configure do |c| + # Specify a root folder where Swagger JSON files are located + # This is used by the Swagger middleware to serve requests for API descriptions + # NOTE: If you're using rswag-specs to generate Swagger, you'll need to ensure + # that it's configured to generate files in the same folder + c.swagger_root = Rails.root.to_s + '/swagger' + + # Inject a lambda function to alter the returned Swagger prior to serialization + # The function will have access to the rack env for the current request + # For example, you could leverage this to dynamically assign the "host" property + # + # c.swagger_filter = lambda { |swagger, env| swagger['host'] = env['HTTP_HOST'] } +end diff --git a/config/initializers/rswag_ui.rb b/config/initializers/rswag_ui.rb new file mode 100644 index 00000000..cc9f2ef8 --- /dev/null +++ b/config/initializers/rswag_ui.rb @@ -0,0 +1,15 @@ +Rswag::Ui.configure do |c| + # List the Swagger endpoints that you want to be documented through the + # swagger-ui. The first parameter is the path (absolute or relative to the UI + # host) to the corresponding endpoint and the second is a title that will be + # displayed in the document selector. + # NOTE: If you're using rspec-api to expose Swagger files + # (under swagger_root) as JSON or YAML endpoints, then the list below should + # correspond to the relative paths for those endpoints. + + c.swagger_endpoint '/api-docs/v1/swagger.yaml', 'API V1 Docs' + + # Add Basic Auth in case your API is private + # c.basic_auth_enabled = true + # c.basic_auth_credentials 'username', 'password' +end diff --git a/config/routes.rb b/config/routes.rb index 5b27eba4..83e65707 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,4 +1,6 @@ Rails.application.routes.draw do + mount Rswag::Ui::Engine => '/api-docs' + mount Rswag::Api::Engine => '/api-docs' get "order_comments/new" get "comments/new" diff --git a/doc/API.md b/doc/API.md index 2e09cfa4..f295e82f 100644 --- a/doc/API.md +++ b/doc/API.md @@ -5,9 +5,11 @@ like listing open orders, updating the ordergroup's order, and listing financial transactions. Not all Foodsoft functionality is available through the API, but we're open for new additions. -The API is documented using [Open API 2.0](https://github.com/OAI/OpenAPI-Specification) -/ [Swagger](https://swagger.io/) in [swagger.v1.yml](swagger.v1.yml). +The API is documented using [Open API 3.0.1](https://github.com/OAI/OpenAPI-Specification) +/ [Swagger](https://swagger.io/) in [swagger.yaml](/swagger/v1/swagger.yaml). This provides a machine-readable reference that is used to provide documentation. +It is generated by [rswag](https://github.com/rswag) wich also provides api-tests. +It can be generated running `RAILS_ENV=test rails rswag`. **Note:** the current OAuth scopes may be subject to change, until the next release of Foodsoft. diff --git a/doc/swagger.v1.yml b/doc/swagger.v1.yml deleted file mode 100644 index d8e793d3..00000000 --- a/doc/swagger.v1.yml +++ /dev/null @@ -1,1106 +0,0 @@ -swagger: '2.0' -info: - title: Foodsoft API v1 - version: '1.0.0' - description: > - [Foodsoft](https://github.com/foodcoops/foodsoft) is web-based software to manage - a non-profit food coop (product catalog, ordering, accounting, job scheduling). - - - This is a description of Foodsoft's API v1. - - - Note that each food cooperative typically has their own instance (on a shared - server or their own installation), and there are just as many APIs (if the Foodsoft - version is recent enough). - This API description points to the default development url with the default - Foodsoft scope - that would be [http://localhost:3000/f](http://localhost:3000/f). - - You may find the search parameters for index endpoints lacking. They are not - documented here, because there are too many combinations. For now, you'll need - to resort to [Ransack](https://github.com/activerecord-hackery/ransack) and - looking at Foodsoft's `ransackable_*` model class methods. -externalDocs: - description: General Foodsoft API documentation - url: https://github.com/foodcoops/foodsoft/blob/master/doc/API.md - -# development url with default scope -host: localhost:3000 -schemes: - - 'http' -basePath: /f/api/v1 - -produces: - - 'application/json' - -paths: - /user: - get: - summary: info about the currently logged-in user - tags: - - 1. User - responses: - 200: - description: success - schema: - type: object - properties: - user: - $ref: '#/definitions/User' - 401: - description: not logged-in - schema: - $ref: '#/definitions/Error401' - 403: - description: missing scope - schema: - $ref: '#/definitions/Error403' - security: - - foodsoft_auth: ['user:read', 'user:write'] - - /user/financial_overview: - get: - summary: financial summary about the currently logged-in user - tags: - - 1. User - - 6. FinancialTransaction - responses: - 200: - description: success - schema: - type: object - properties: - financial_overview: - $ref: '#/definitions/FinanceOverview' - 401: - description: not logged-in - schema: - $ref: '#/definitions/Error401' - 403: - description: missing scope - schema: - $ref: '#/definitions/Error403' - security: - - foodsoft_auth: ['finance:user'] - - /user/financial_transactions: - get: - summary: financial transactions of the member's ordergroup - tags: - - 1. User - - 6. FinancialTransaction - parameters: - - $ref: '#/parameters/page' - - $ref: '#/parameters/per_page' - responses: - 200: - description: success - schema: - type: object - properties: - financial_transactions: - type: array - items: - $ref: '#/definitions/FinancialTransaction' - meta: - $ref: '#/definitions/Meta' - 401: - description: not logged-in - schema: - $ref: '#/definitions/Error401' - 403: - description: user has no ordergroup or missing scope - schema: - $ref: '#/definitions/Error403' - security: - - foodsoft_auth: ['finance:user'] - post: - summary: create new financial transaction (requires enabled self service) - tags: - - 1. User - - 6. FinancialTransaction - parameters: - - in: body - name: body - description: financial transaction to create - required: true - schema: - $ref: '#/definitions/FinancialTransactionForCreate' - responses: - 200: - description: success - schema: - type: object - properties: - financial_transaction: - $ref: '#/definitions/FinancialTransaction' - 401: - description: not logged-in - schema: - $ref: '#/definitions/Error401' - 403: - description: user has no ordergroup, is below minimum balance, self service is disabled, or missing scope - schema: - $ref: '#/definitions/Error403' - 404: - description: financial transaction type not found - schema: - $ref: '#/definitions/Error404' - 422: - description: invalid parameter value - schema: - $ref: '#/definitions/Error422' - /user/financial_transactions/{id}: - parameters: - - $ref: '#/parameters/idInUrl' - get: - summary: find financial transaction by id - tags: - - 1. User - - 6. FinancialTransaction - responses: - 200: - description: success - schema: - type: object - properties: - financial_transaction: - $ref: '#/definitions/FinancialTransaction' - 401: - description: not logged-in - schema: - $ref: '#/definitions/Error401' - 403: - description: user has no ordergroup or missing scope - schema: - $ref: '#/definitions/Error403' - 404: - description: not found - schema: - $ref: '#/definitions/Error404' - security: - - foodsoft_auth: ['finance:user'] - - /user/group_order_articles: - get: - summary: group order articles - tags: - - 1. User - - 2. Order - parameters: - - $ref: '#/parameters/page' - - $ref: '#/parameters/per_page' - - $ref: '#/parameters/q_ordered' - responses: - 200: - description: success - schema: - type: object - properties: - group_order_articles: - type: array - items: - $ref: '#/definitions/GroupOrderArticle' - meta: - $ref: '#/definitions/Meta' - 401: - description: not logged-in - schema: - $ref: '#/definitions/Error401' - 403: - description: user has no ordergroup or missing scope - schema: - $ref: '#/definitions/Error403' - security: - - foodsoft_auth: ['group_orders:user'] - post: - summary: create new group order article - tags: - - 1. User - - 2. Order - parameters: - - in: body - name: body - description: group order article to create - required: true - schema: - $ref: '#/definitions/GroupOrderArticleForCreate' - responses: - 200: - description: success - schema: - type: object - properties: - group_order_article: - $ref: '#/definitions/GroupOrderArticle' - order_article: - $ref: '#/definitions/OrderArticle' - 401: - description: not logged-in - schema: - $ref: '#/definitions/Error401' - 403: - description: user has no ordergroup, order not open, is below minimum balance, has not enough apple points, or missing scope - schema: - $ref: '#/definitions/Error403' - 404: - description: order article not found in open orders - schema: - $ref: '#/definitions/Error404' - 422: - description: invalid parameter value or group order article already exists - schema: - $ref: '#/definitions/Error422' - /user/group_order_articles/{id}: - parameters: - - $ref: '#/parameters/idInUrl' - get: - summary: find group order article by id - tags: - - 1. User - - 2. Order - responses: - 200: - description: success - schema: - type: object - properties: - group_order_article: - $ref: '#/definitions/GroupOrderArticle' - order_article: - $ref: '#/definitions/OrderArticle' - 401: - description: not logged-in - schema: - $ref: '#/definitions/Error401' - 403: - description: user has no ordergroup or missing scope - schema: - $ref: '#/definitions/Error403' - 404: - description: not found - schema: - $ref: '#/definitions/Error404' - security: - - foodsoft_auth: ['group_orders:user'] - patch: - summary: update a group order article (but delete if quantity and tolerance are zero) - tags: - - 1. User - - 2. Order - parameters: - - $ref: '#/parameters/idInUrl' - - in: body - name: body - description: group order article update - required: true - schema: - $ref: '#/definitions/GroupOrderArticleForUpdate' - responses: - 200: - description: success - schema: - type: object - properties: - group_order_article: - $ref: '#/definitions/GroupOrderArticle' - order_article: - $ref: '#/definitions/OrderArticle' - 401: - description: not logged-in - schema: - $ref: '#/definitions/Error401' - 403: - description: user has no ordergroup, order not open, is below minimum balance, has not enough apple points, or missing scope - schema: - $ref: '#/definitions/Error403' - 404: - description: order article not found in open orders - schema: - $ref: '#/definitions/Error404' - 422: - description: invalid parameter value - schema: - $ref: '#/definitions/Error422' - delete: - summary: remove group order article - tags: - - 1. User - - 2. Order - parameters: - - $ref: '#/parameters/idInUrl' - responses: - 200: - description: success - schema: - type: object - properties: - group_order_article: - $ref: '#/definitions/GroupOrderArticle' - order_article: - $ref: '#/definitions/OrderArticle' - 401: - description: not logged-in - schema: - $ref: '#/definitions/Error401' - 403: - description: user has no ordergroup, order not open, is below minimum balance, has not enough apple points, or missing scope - schema: - $ref: '#/definitions/Error403' - 404: - description: order article not found in open orders - schema: - $ref: '#/definitions/Error404' - - /financial_transactions: - get: - summary: financial transactions - tags: - - 6. FinancialTransaction - parameters: - - $ref: '#/parameters/page' - - $ref: '#/parameters/per_page' - responses: - 200: - description: success - schema: - type: object - properties: - financial_transactions: - type: array - items: - $ref: '#/definitions/FinancialTransaction' - meta: - $ref: '#/definitions/Meta' - 401: - description: not logged-in - schema: - $ref: '#/definitions/Error401' - 403: - description: missing scope or no permission - schema: - $ref: '#/definitions/Error403' - security: - - foodsoft_auth: ['finance:read', 'finance:write'] - /financial_transactions/{id}: - parameters: - - $ref: '#/parameters/idInUrl' - get: - summary: find financial transaction by id - tags: - - 6. FinancialTransaction - responses: - 200: - description: success - schema: - type: object - properties: - financial_transaction: - $ref: '#/definitions/FinancialTransaction' - 401: - description: not logged-in - schema: - $ref: '#/definitions/Error401' - 403: - description: missing scope or no permission - schema: - $ref: '#/definitions/Error403' - 404: - description: not found - schema: - $ref: '#/definitions/Error404' - security: - - foodsoft_auth: ['finance:read', 'finance:write'] - /orders: - get: - summary: orders - tags: - - 2. Order - parameters: - - $ref: '#/parameters/page' - - $ref: '#/parameters/per_page' - responses: - 200: - description: success - schema: - type: object - properties: - orders: - type: array - items: - $ref: '#/definitions/Order' - meta: - $ref: '#/definitions/Meta' - 401: - description: not logged-in - schema: - $ref: '#/definitions/Error401' - 403: - description: missing scope or no permission - schema: - $ref: '#/definitions/Error403' - security: - - foodsoft_auth: ['orders:read', 'orders:write'] - /orders/{id}: - parameters: - - $ref: '#/parameters/idInUrl' - get: - summary: find order by id - tags: - - 2. Order - responses: - 200: - description: success - schema: - type: object - properties: - order: - $ref: '#/definitions/Order' - 401: - description: not logged-in - schema: - $ref: '#/definitions/Error401' - 403: - description: missing scope or no permission - schema: - $ref: '#/definitions/Error403' - 404: - description: not found - schema: - $ref: '#/definitions/Error404' - security: - - foodsoft_auth: ['orders:read', 'orders:write'] - /order_articles: - get: - summary: order articles - tags: - - 2. Order - parameters: - - $ref: '#/parameters/page' - - $ref: '#/parameters/per_page' - - $ref: '#/parameters/q_ordered' - responses: - 200: - description: success - schema: - type: object - properties: - order_articles: - type: array - items: - $ref: '#/definitions/OrderArticle' - meta: - $ref: '#/definitions/Meta' - 401: - description: not logged-in - schema: - $ref: '#/definitions/Error401' - 403: - description: missing scope or no permission - schema: - $ref: '#/definitions/Error403' - security: - - foodsoft_auth: ['group_orders:user'] - /order_articles/{id}: - parameters: - - $ref: '#/parameters/idInUrl' - get: - summary: find order article by id - tags: - - 2. Order - responses: - 200: - description: success - schema: - type: object - properties: - order_article: - $ref: '#/definitions/OrderArticle' - 401: - description: not logged-in - schema: - $ref: '#/definitions/Error401' - 403: - description: missing scope or no permission - schema: - $ref: '#/definitions/Error403' - 404: - description: not found - schema: - $ref: '#/definitions/Error404' - security: - - foodsoft_auth: ['orders:read', 'orders:write'] - /article_categories: - get: - summary: article categories - tags: - - 2. Category - parameters: - - $ref: '#/parameters/page' - - $ref: '#/parameters/per_page' - responses: - 200: - description: success - schema: - type: object - properties: - article_categories: - type: array - items: - $ref: '#/definitions/ArticleCategory' - meta: - $ref: '#/definitions/Meta' - 401: - description: not logged-in - schema: - $ref: '#/definitions/Error401' - - security: - - foodsoft_auth: ['all'] - /article_categories/{id}: - parameters: - - $ref: '#/parameters/idInUrl' - get: - summary: find article category by id - tags: - - 2. Category - responses: - 200: - description: success - schema: - type: object - properties: - article_category: - $ref: '#/definitions/ArticleCategory' - 401: - description: not logged-in - schema: - $ref: '#/definitions/Error401' - 404: - description: not found - schema: - $ref: '#/definitions/Error404' - security: - - foodsoft_auth: ['all'] - - /financial_transaction_classes: - get: - summary: financial transaction classes - tags: - - 2. Category - parameters: - - $ref: '#/parameters/page' - - $ref: '#/parameters/per_page' - responses: - 200: - description: success - schema: - type: object - properties: - financial_transaction_classes: - type: array - items: - $ref: '#/definitions/FinancialTransactionClass' - meta: - $ref: '#/definitions/Meta' - 401: - description: not logged-in - schema: - $ref: '#/definitions/Error401' - - security: - - foodsoft_auth: ['all'] - /financial_transaction_classes/{id}: - parameters: - - $ref: '#/parameters/idInUrl' - get: - summary: find financial transaction class by id - tags: - - 2. Category - responses: - 200: - description: success - schema: - type: object - properties: - financial_transaction_class: - $ref: '#/definitions/FinancialTransactionClass' - 401: - description: not logged-in - schema: - $ref: '#/definitions/Error401' - 404: - description: not found - schema: - $ref: '#/definitions/Error404' - security: - - foodsoft_auth: ['all'] - - /financial_transaction_types: - get: - summary: financial transaction types - tags: - - 2. Category - parameters: - - $ref: '#/parameters/page' - - $ref: '#/parameters/per_page' - responses: - 200: - description: success - schema: - type: object - properties: - financial_transaction_types: - type: array - items: - $ref: '#/definitions/FinancialTransactionType' - meta: - $ref: '#/definitions/Meta' - 401: - description: not logged-in - schema: - $ref: '#/definitions/Error401' - - security: - - foodsoft_auth: ['all'] - /financial_transaction_types/{id}: - parameters: - - $ref: '#/parameters/idInUrl' - get: - summary: find financial transaction type by id - tags: - - 2. Category - responses: - 200: - description: success - schema: - type: object - properties: - financial_transaction_type: - $ref: '#/definitions/FinancialTransactionType' - 401: - description: not logged-in - schema: - $ref: '#/definitions/Error401' - 404: - description: not found - schema: - $ref: '#/definitions/Error404' - security: - - foodsoft_auth: ['all'] - - /config: - get: - summary: configuration variables - tags: - - 7. General - responses: - 200: - description: success - schema: - type: object - 401: - description: not logged-in - schema: - $ref: '#/definitions/Error401' - 403: - description: missing scope or no permission - schema: - $ref: '#/definitions/Error403' - security: - - foodsoft_auth: ['config:user', 'config:read', 'config:write'] - /navigation: - get: - summary: navigation - tags: - - 7. General - responses: - 200: - description: success - schema: - type: object - properties: - navigation: - $ref: '#/definitions/Navigation' - 401: - description: not logged-in - schema: - $ref: '#/definitions/Error401' - security: - - foodsoft_auth: [] - -parameters: - # url parameters - idInUrl: - name: id - type: integer - in: path - minimum: 1 - required: true - - # query parameters - page: - name: page - type: integer - in: query - description: page number - minimum: 0 - default: 0 - per_page: - name: per_page - type: integer - in: query - description: items per page - minimum: 0 - default: 20 - - # non-ransack query parameters - q_ordered: - name: q[ordered] - type: string - in: query - description: "'member' show articles ordered by the user's ordergroup, 'all' by all members, and 'supplier' ordered at the supplier" - enum: ['member', 'all', 'supplier'] - -definitions: - # models - User: - type: object - properties: - id: - type: integer - name: - type: string - description: full name - email: - type: string - description: email address - locale: - type: string - description: language code - required: ['id', 'name', 'email'] - - FinancialTransactionForCreate: - type: object - properties: - amount: - type: number - description: amount credited (negative for a debit transaction) - financial_transaction_type_id: - type: integer - description: id of the type of the transaction - note: - type: string - description: note entered with the transaction - required: ['amount', 'financial_transaction_type_id', 'note'] - FinancialTransaction: - allOf: - - $ref: '#/definitions/FinancialTransactionForCreate' - - type: object - properties: - id: - type: integer - user_id: - type: ['integer', 'null'] - description: id of user who entered the transaction (may be null for deleted users or 0 for a system user) - user_name: - type: ['string', 'null'] - description: name of user who entered the transaction (may be null or empty string for deleted users or system users) - financial_transaction_type_name: - type: string - description: name of the type of the transaction - created_at: - type: string - format: date-time - description: when the transaction was entered - required: ['id', 'user_id', 'user_name', 'financial_transaction_type_name', 'created_at'] - - FinancialTransactionClass: - type: object - properties: - id: - type: integer - name: - type: string - description: full name - required: ['id', 'name'] - - FinancialTransactionType: - type: object - properties: - id: - type: integer - name: - type: string - description: full name - name_short: - type: ['string', 'null'] - description: short name (used for bank transfers) - bank_account_id: - type: ['integer', 'null'] - description: id of the bank account used for this transaction type - bank_account_name: - type: ['string', 'null'] - description: name of the bank account used for this transaction type - bank_account_iban: - type: ['string', 'null'] - description: IBAN of the bank account used for this transaction type - financial_transaction_class_id: - type: integer - description: id of the class of the transaction - financial_transaction_class_name: - type: string - description: name of the class of the transaction - required: ['id', 'name', 'financial_transaction_class_id', 'financial_transaction_class_name'] - - FinanceOverview: - type: object - properties: - account_balance: - type: number - description: booked accout balance of ordergroup - available_funds: - type: number - description: fund available to order articles - financial_transaction_class_sums: - type: array - items: - type: object - properties: - id: - type: integer - description: id of the financial transaction class - name: - type: string - description: name of the financial transaction class - amount: - type: number - description: sum of the amounts belonging to the financial transaction class - required: ['id', 'name', 'amount'] - required: ['account_balance', 'available_funds', 'financial_transaction_class_sums'] - - ArticleCategory: - type: object - properties: - id: - type: integer - name: - type: string - required: ['id', 'name'] - - Order: - type: object - properties: - id: - type: integer - name: - type: string - description: name of the order's supplier (or stock) - starts: - type: string - format: date-time - description: when the order was opened - ends: - type: ['string', 'null'] - format: date-time - description: when the order will close or was closed - boxfill: - type: ['string', 'null'] - format: date-time - description: when the order will enter or entered the boxfill phase - pickup: - type: ['string', 'null'] - format: date - description: pickup date - is_open: - type: boolean - description: if the order is currently open or not - is_boxfill: - type: boolean - description: if the order is currently in the boxfill phase or not - - Article: - type: object - properties: - id: - type: integer - name: - type: string - supplier_id: - type: integer - description: id of supplier, or 0 for stock articles - supplier_name: - type: ['string', 'null'] - description: name of the supplier, or null for stock articles - unit: - type: string - description: amount of each unit, e.g. "100 g" or "kg" - unit_quantity: - type: integer - description: units can only be ordered from the supplier in multiples of unit_quantity - note: - type: ['string', 'null'] - description: generic note - manufacturer: - type: ['string', 'null'] - description: manufacturer - origin: - type: ['string', 'null'] - description: origin, preferably (starting with a) 2-letter ISO country code - article_category_id: - type: integer - description: id of article category - quantity_available: - type: integer - description: number of units available (only present on stock articles) - required: ['id', 'name', 'supplier_id', 'supplier_name', 'unit', 'unit_quantity', 'note', 'manufacturer', 'origin', 'article_category_id'] - - OrderArticle: - type: object - properties: - id: - type: integer - order_id: - type: integer - description: id of order this order article belongs to - price: - type: number - format: float - description: foodcoop price - quantity: - type: integer - description: number of units ordered by members - tolerance: - type: integer - description: number of extra units that members are willing to buy to fill a box - units_to_order: - type: integer - description: number of units to order from the supplier - article: - $ref: '#/definitions/Article' - - GroupOrderArticleForUpdate: - type: object - properties: - quantity: - type: integer - description: number of units ordered by the user's ordergroup - tolerance: - type: integer - description: number of extra units the user's ordergroup is willing to buy for filling a box - GroupOrderArticleForCreate: - allOf: - - $ref: '#/definitions/GroupOrderArticleForUpdate' - - type: object - properties: - order_article_id: - type: integer - description: id of order article - GroupOrderArticle: - allOf: - - $ref: '#/definitions/GroupOrderArticleForCreate' - - type: object - properties: - id: - type: integer - result: - type: number - format: float - description: number of units the user's ordergroup will receive or has received - total_price: - type: number - format: float - description: total price of this group order article - - Navigation: - type: array - items: - type: object - properties: - name: - type: string - description: title - url: - type: string - description: link - items: - $ref: '#/definitions/Navigation' - required: ['name'] - minProperties: 2 # name+url or name+items - - # collection meta object in root of a response - Meta: - type: object - properties: - page: - type: integer - description: page number of the returned collection - per_page: - type: integer - description: number of items per page - total_pages: - type: integer - description: total number of pages - total_count: - type: integer - description: total number of items in the collection - required: ['page', 'per_page', 'total_pages', 'total_count'] - - Error: - type: object - properties: - error: - type: string - description: error code - error_description: - type: string - description: human-readable error message (localized) - Error404: - type: object - properties: - error: - type: string - description: 'not_found' - error_description: - $ref: '#/definitions/Error/properties/error_description' - Error401: - type: object - properties: - error: - type: string - description: 'unauthorized' - error_description: - $ref: '#/definitions/Error/properties/error_description' - Error403: - type: object - properties: - error: - type: string - description: 'forbidden or invalid_scope' - error_description: - $ref: '#/definitions/Error/properties/error_description' - Error422: - type: object - properties: - error: - type: string - description: unprocessable entity - error_description: - $ref: '#/definitions/Error/properties/error_description' - - -securityDefinitions: - foodsoft_auth: - type: oauth2 - flow: implicit - authorizationUrl: http://localhost:3000/f/oauth/authorize - scopes: - config:user: reading Foodsoft configuration for regular users - config:read: reading Foodsoft configuration values - config:write: reading and updating Foodsoft configuration values - finance:user: accessing your own financial transactions - finance:read: reading all financial transactions - finance:write: reading and creating financial transactions - user:read: reading your own user profile - user:write: reading and updating your own user profile - offline_access: retain access after user has logged out diff --git a/spec/api/v1/order_articles_spec.rb b/spec/api/v1/order_articles_spec.rb deleted file mode 100644 index e65867db..00000000 --- a/spec/api/v1/order_articles_spec.rb +++ /dev/null @@ -1,59 +0,0 @@ -require 'spec_helper' - -# Most routes are tested in the swagger_spec, this tests (non-ransack) parameters. -describe Api::V1::OrderArticlesController, type: :controller do - include ApiOAuth - let(:api_scopes) { ['orders:read'] } - - let(:json_order_articles) { json_response['order_articles'] } - let(:json_order_article_ids) { json_order_articles.map { |joa| joa["id"] } } - - describe "GET :index" do - context "with param q[ordered]" do - let(:order) { create(:order, article_count: 4) } - let(:order_articles) { order.order_articles } - - before do - order_articles[0].update!(quantity: 0, tolerance: 0, units_to_order: 0) - order_articles[1].update!(quantity: 1, tolerance: 0, units_to_order: 0) - order_articles[2].update!(quantity: 0, tolerance: 1, units_to_order: 0) - order_articles[3].update!(quantity: 0, tolerance: 0, units_to_order: 1) - end - - it "(unset)" do - get :index, params: { foodcoop: 'f' } - expect(json_order_articles.count).to eq 4 - end - - it "all" do - get :index, params: { foodcoop: 'f', q: { ordered: 'all' } } - expect(json_order_article_ids).to match_array order_articles[1..2].map(&:id) - end - - it "supplier" do - get :index, params: { foodcoop: 'f', q: { ordered: 'supplier' } } - expect(json_order_article_ids).to match_array [order_articles[3].id] - end - - it "member" do - get :index, params: { foodcoop: 'f', q: { ordered: 'member' } } - expect(json_order_articles.count).to eq 0 - end - - context "when ordered by user" do - let(:user) { create(:user, :ordergroup) } - let(:go) { create(:group_order, order: order, ordergroup: user.ordergroup) } - - before do - create(:group_order_article, group_order: go, order_article: order_articles[1], quantity: 1) - create(:group_order_article, group_order: go, order_article: order_articles[2], tolerance: 0) - end - - it "member" do - get :index, params: { foodcoop: 'f', q: { ordered: 'member' } } - expect(json_order_article_ids).to match_array order_articles[1..2].map(&:id) - end - end - end - end -end diff --git a/spec/api/v1/swagger_spec.rb b/spec/api/v1/swagger_spec.rb deleted file mode 100644 index 3da37332..00000000 --- a/spec/api/v1/swagger_spec.rb +++ /dev/null @@ -1,284 +0,0 @@ -require 'spec_helper' -require 'apivore' - -# we want to load a local file in YAML-format instead of a served JSON file -class SwaggerCheckerFile < Apivore::SwaggerChecker - def fetch_swagger! - YAML.load(File.read(swagger_path)) - end -end - -describe 'API v1', type: :apivore, order: :defined do - include ApiHelper - - subject { SwaggerCheckerFile.instance_for Rails.root.join('doc', 'swagger.v1.yml') } - - context 'has valid paths' do - context 'user' do - let(:api_scopes) { ['user:read'] } - # create multiple users to make sure we're getting the authenticated user, not just any - let!(:other_user_1) { create :user } - let!(:user) { create :user } - let!(:other_user_2) { create :user } - - it { is_expected.to validate(:get, '/user', 200, api_auth) } - it { is_expected.to validate(:get, '/user', 401) } - - it_handles_invalid_token_and_scope(:get, '/user') - end - - context 'user/financial_overview' do - let(:api_scopes) { ['finance:user'] } - let!(:user) { create :user, :ordergroup } - - it { is_expected.to validate(:get, '/user/financial_overview', 200, api_auth) } - it { is_expected.to validate(:get, '/user/financial_overview', 401) } - - it_handles_invalid_token_and_scope(:get, '/user/financial_overview') - end - - context 'user/financial_transactions' do - let(:api_scopes) { ['finance:user'] } - let(:other_user) { create :user, :ordergroup } - let!(:other_ft_1) { create :financial_transaction, ordergroup: other_user.ordergroup } - - context 'without ordergroup' do - it { is_expected.to validate(:get, '/user/financial_transactions', 403, api_auth) } - it { is_expected.to validate(:get, '/user/financial_transactions/{id}', 403, api_auth({ 'id' => other_ft_1.id })) } - end - - context 'with ordergroup' do - let(:user) { create :user, :ordergroup } - let!(:ft_1) { create :financial_transaction, ordergroup: user.ordergroup } - let!(:ft_2) { create :financial_transaction, ordergroup: user.ordergroup } - let!(:ft_3) { create :financial_transaction, ordergroup: user.ordergroup } - - let(:create_params) { { '_data' => { financial_transaction: { amount: 1, financial_transaction_type_id: ft_1.financial_transaction_type.id, note: 'note' } } } } - - it { is_expected.to validate(:get, '/user/financial_transactions', 200, api_auth) } - it { is_expected.to validate(:get, '/user/financial_transactions/{id}', 200, api_auth({ 'id' => ft_2.id })) } - it { is_expected.to validate(:get, '/user/financial_transactions/{id}', 404, api_auth({ 'id' => other_ft_1.id })) } - it { is_expected.to validate(:get, '/user/financial_transactions/{id}', 404, api_auth({ 'id' => FinancialTransaction.last.id + 1 })) } - - context 'without using self service' do - it { is_expected.to validate(:post, '/user/financial_transactions', 403, api_auth(create_params)) } - end - - context 'with using self service' do - before { FoodsoftConfig[:use_self_service] = true } - - it { is_expected.to validate(:post, '/user/financial_transactions', 200, api_auth(create_params)) } - - context 'with invalid financial transaction type' do - let(:create_params) { { '_data' => { financial_transaction: { amount: 1, financial_transaction_type_id: -1, note: 'note' } } } } - - it { is_expected.to validate(:post, '/user/financial_transactions', 404, api_auth(create_params)) } - end - - context 'without note' do - let(:create_params) { { '_data' => { financial_transaction: { amount: 1, financial_transaction_type_id: ft_1.financial_transaction_type.id } } } } - - it { is_expected.to validate(:post, '/user/financial_transactions', 422, api_auth(create_params)) } - end - - context 'without enough balance' do - before { FoodsoftConfig[:minimum_balance] = 1000 } - - it { is_expected.to validate(:post, '/user/financial_transactions', 403, api_auth(create_params)) } - end - end - - it_handles_invalid_token_and_scope(:get, '/user/financial_transactions') - it_handles_invalid_token_and_scope(:post, '/user/financial_transactions', -> { api_auth(create_params) }) - it_handles_invalid_token_and_scope(:get, '/user/financial_transactions/{id}', -> { api_auth('id' => ft_2.id) }) - end - end - - context 'user/group_order_articles' do - let(:api_scopes) { ['group_orders:user'] } - let(:order) { create(:order, article_count: 2) } - - let(:user_2) { create :user, :ordergroup } - let(:group_order_2) { create(:group_order, order: order, ordergroup: user_2.ordergroup) } - let!(:goa_2) { create :group_order_article, order_article: order.order_articles[0], group_order: group_order_2 } - - before { group_order_2.update_price!; user_2.ordergroup.update_stats! } - - context 'without ordergroup' do - it { is_expected.to validate(:get, '/user/group_order_articles', 403, api_auth) } - it { is_expected.to validate(:get, '/user/group_order_articles/{id}', 403, api_auth({ 'id' => goa_2.id })) } - end - - context 'with ordergroup' do - let(:user) { create :user, :ordergroup } - let(:update_params) { { 'id' => goa.id, '_data' => { group_order_article: { quantity: goa.quantity + 1, tolerance: 0 } } } } - let(:create_params) { { '_data' => { group_order_article: { order_article_id: order.order_articles[1].id, quantity: 1 } } } } - let(:group_order) { create(:group_order, order: order, ordergroup: user.ordergroup) } - let!(:goa) { create :group_order_article, order_article: order.order_articles[0], group_order: group_order } - - before { group_order.update_price!; user.ordergroup.update_stats! } - - it { is_expected.to validate(:get, '/user/group_order_articles', 200, api_auth) } - it { is_expected.to validate(:get, '/user/group_order_articles/{id}', 200, api_auth({ 'id' => goa.id })) } - it { is_expected.to validate(:get, '/user/group_order_articles/{id}', 404, api_auth({ 'id' => goa_2.id })) } - it { is_expected.to validate(:get, '/user/group_order_articles/{id}', 404, api_auth({ 'id' => GroupOrderArticle.last.id + 1 })) } - - it { is_expected.to validate(:post, '/user/group_order_articles', 200, api_auth(create_params)) } - it { is_expected.to validate(:patch, '/user/group_order_articles/{id}', 200, api_auth(update_params)) } - it { is_expected.to validate(:delete, '/user/group_order_articles/{id}', 200, api_auth({ 'id' => goa.id })) } - - context 'with an existing group_order_article' do - let(:create_params) { { '_data' => { group_order_article: { order_article_id: order.order_articles[0].id, quantity: 1 } } } } - - it { is_expected.to validate(:post, '/user/group_order_articles', 422, api_auth(create_params)) } - end - - context 'with invalid parameter values' do - let(:create_params) { { '_data' => { group_order_article: { order_article_id: order.order_articles[0].id, quantity: -1 } } } } - let(:update_params) { { 'id' => goa.id, '_data' => { group_order_article: { quantity: -1, tolerance: 0 } } } } - - it { is_expected.to validate(:post, '/user/group_order_articles', 422, api_auth(create_params)) } - it { is_expected.to validate(:patch, '/user/group_order_articles/{id}', 422, api_auth(update_params)) } - end - - context 'with a closed order' do - let(:order) { create(:order, article_count: 2, state: :finished) } - - it { is_expected.to validate(:post, '/user/group_order_articles', 404, api_auth(create_params)) } - it { is_expected.to validate(:patch, '/user/group_order_articles/{id}', 404, api_auth(update_params)) } - it { is_expected.to validate(:delete, '/user/group_order_articles/{id}', 404, api_auth({ 'id' => goa.id })) } - end - - context 'without enough balance' do - before { FoodsoftConfig[:minimum_balance] = 1000 } - - it { is_expected.to validate(:post, '/user/group_order_articles', 403, api_auth(create_params)) } - it { is_expected.to validate(:patch, '/user/group_order_articles/{id}', 403, api_auth(update_params)) } - it { is_expected.to validate(:delete, '/user/group_order_articles/{id}', 200, api_auth({ 'id' => goa.id })) } - end - - context 'without enough apple points' do - before { allow_any_instance_of(Ordergroup).to receive(:not_enough_apples?).and_return(true) } - - it { is_expected.to validate(:post, '/user/group_order_articles', 403, api_auth(create_params)) } - it { is_expected.to validate(:patch, '/user/group_order_articles/{id}', 403, api_auth(update_params)) } - it { is_expected.to validate(:delete, '/user/group_order_articles/{id}', 200, api_auth({ 'id' => goa.id })) } - end - - it_handles_invalid_token_and_scope(:get, '/user/group_order_articles') - it_handles_invalid_token_and_scope(:post, '/user/group_order_articles', -> { api_auth(create_params) }) - it_handles_invalid_token_and_scope(:get, '/user/group_order_articles/{id}', -> { api_auth({ 'id' => goa.id }) }) - it_handles_invalid_token_and_scope(:patch, '/user/group_order_articles/{id}', -> { api_auth(update_params) }) - it_handles_invalid_token_and_scope(:delete, '/user/group_order_articles/{id}', -> { api_auth({ 'id' => goa.id }) }) - end - end - - context 'config' do - let(:api_scopes) { ['config:user'] } - - it { is_expected.to validate(:get, '/config', 200, api_auth) } - it { is_expected.to validate(:get, '/config', 401) } - - it_handles_invalid_token_and_scope(:get, '/config') - end - - context 'navigation' do - it { is_expected.to validate(:get, '/navigation', 200, api_auth) } - it { is_expected.to validate(:get, '/navigation', 401) } - - it_handles_invalid_token(:get, '/navigation') - end - - context 'financial_transactions' do - let(:api_scopes) { ['finance:read'] } - let(:user) { create(:user, :role_finance) } - let(:other_user) { create :user, :ordergroup } - let!(:ft_1) { create :financial_transaction, ordergroup: other_user.ordergroup } - let!(:ft_2) { create :financial_transaction, ordergroup: other_user.ordergroup } - - it { is_expected.to validate(:get, '/financial_transactions', 200, api_auth) } - it { is_expected.to validate(:get, '/financial_transactions/{id}', 200, api_auth({ 'id' => ft_2.id })) } - it { is_expected.to validate(:get, '/financial_transactions/{id}', 404, api_auth({ 'id' => FinancialTransaction.last.id + 1 })) } - - context 'without role_finance' do - let(:user) { create(:user) } - - it { is_expected.to validate(:get, '/financial_transactions', 403, api_auth) } - it { is_expected.to validate(:get, '/financial_transactions/{id}', 403, api_auth({ 'id' => ft_2.id })) } - end - - it_handles_invalid_token_and_scope(:get, '/financial_transactions') - it_handles_invalid_token_and_scope(:get, '/financial_transactions/{id}', -> { api_auth({ 'id' => ft_2.id }) }) - end - - context 'financial_transaction_classes' do - let!(:cla_1) { create :financial_transaction_class } - let!(:cla_2) { create :financial_transaction_class } - - it { is_expected.to validate(:get, '/financial_transaction_classes', 200, api_auth) } - it { is_expected.to validate(:get, '/financial_transaction_classes/{id}', 200, api_auth({ 'id' => cla_2.id })) } - it { is_expected.to validate(:get, '/financial_transaction_classes/{id}', 404, api_auth({ 'id' => cla_2.id + 1 })) } - - it_handles_invalid_token(:get, '/financial_transaction_classes') - it_handles_invalid_token(:get, '/financial_transaction_classes/{id}', -> { api_auth({ 'id' => cla_1.id }) }) - end - - context 'financial_transaction_types' do - let!(:tpy_1) { create :financial_transaction_type } - let!(:tpy_2) { create :financial_transaction_type } - - it { is_expected.to validate(:get, '/financial_transaction_types', 200, api_auth) } - it { is_expected.to validate(:get, '/financial_transaction_types/{id}', 200, api_auth({ 'id' => tpy_2.id })) } - it { is_expected.to validate(:get, '/financial_transaction_types/{id}', 404, api_auth({ 'id' => tpy_2.id + 1 })) } - - it_handles_invalid_token(:get, '/financial_transaction_types') - it_handles_invalid_token(:get, '/financial_transaction_types/{id}', -> { api_auth({ 'id' => tpy_1.id }) }) - end - - context 'orders' do - let(:api_scopes) { ['orders:read'] } - let!(:order) { create :order } - - it { is_expected.to validate(:get, '/orders', 200, api_auth) } - it { is_expected.to validate(:get, '/orders/{id}', 200, api_auth({ 'id' => order.id })) } - it { is_expected.to validate(:get, '/orders/{id}', 404, api_auth({ 'id' => Order.last.id + 1 })) } - - it_handles_invalid_token_and_scope(:get, '/orders') - it_handles_invalid_token_and_scope(:get, '/orders/{id}', -> { api_auth({ 'id' => order.id }) }) - end - - context 'order_articles' do - let(:api_scopes) { ['orders:read'] } - let!(:order_article) { create(:order, article_count: 1).order_articles.first } - let!(:stock_article) { create(:stock_article) } - let!(:stock_order_article) { create(:stock_order, article_ids: [stock_article.id]).order_articles.first } - - it { is_expected.to validate(:get, '/order_articles', 200, api_auth) } - it { is_expected.to validate(:get, '/order_articles/{id}', 200, api_auth({ 'id' => order_article.id })) } - it { is_expected.to validate(:get, '/order_articles/{id}', 200, api_auth({ 'id' => stock_order_article.id })) } - it { is_expected.to validate(:get, '/order_articles/{id}', 404, api_auth({ 'id' => Article.last.id + 1 })) } - - it_handles_invalid_token_and_scope(:get, '/order_articles') - it_handles_invalid_token_and_scope(:get, '/order_articles/{id}', -> { api_auth({ 'id' => order_article.id }) }) - end - - context 'article_categories' do - let!(:cat_1) { create :article_category } - let!(:cat_2) { create :article_category } - - it { is_expected.to validate(:get, '/article_categories', 200, api_auth) } - it { is_expected.to validate(:get, '/article_categories/{id}', 200, api_auth({ 'id' => cat_2.id })) } - it { is_expected.to validate(:get, '/article_categories/{id}', 404, api_auth({ 'id' => cat_2.id + 1 })) } - - it_handles_invalid_token(:get, '/article_categories') - it_handles_invalid_token(:get, '/article_categories/{id}', -> { api_auth({ 'id' => cat_1.id }) }) - end - end - - # needs to be last context so it is always run at the end - context 'and finally' do - it 'tests all documented routes' do - is_expected.to validate_all_paths - end - end -end diff --git a/spec/api/v1/user/financial_transactions_spec.rb b/spec/api/v1/user/financial_transactions_spec.rb deleted file mode 100644 index c7e8f826..00000000 --- a/spec/api/v1/user/financial_transactions_spec.rb +++ /dev/null @@ -1,109 +0,0 @@ -require 'spec_helper' - -# Most routes are tested in the swagger_spec, this tests endpoints that change data. -describe Api::V1::User::FinancialTransactionsController, type: :controller do - include ApiOAuth - let(:user) { create(:user, :ordergroup) } - let(:api_scopes) { ['finance:user'] } - - let(:ftc1) { create :financial_transaction_class } - let(:ftc2) { create :financial_transaction_class } - let(:ftt1) { create :financial_transaction_type, financial_transaction_class: ftc1 } - let(:ftt2) { create :financial_transaction_type, financial_transaction_class: ftc2 } - let(:ftt3) { create :financial_transaction_type, financial_transaction_class: ftc2 } - - let(:amount) { rand(-100..100) } - let(:note) { Faker::Lorem.sentence } - - let(:json_ft) { json_response['financial_transaction'] } - - shared_examples "financial_transactions endpoint success" do - before { request } - - it "returns status 200" do - expect(response).to have_http_status :ok - end - end - - shared_examples "financial_transactions create/update success" do - include_examples "financial_transactions endpoint success" - - it "returns the financial_transaction" do - expect(json_ft['id']).to be_present - expect(json_ft['financial_transaction_type_id']).to eq ftt1.id - expect(json_ft['financial_transaction_type_name']).to eq ftt1.name - expect(json_ft['amount']).to eq amount - expect(json_ft['note']).to eq note - expect(json_ft['user_id']).to eq user.id - end - - it "updates the financial_transaction" do - resulting_ft = FinancialTransaction.where(id: json_ft['id']).first - expect(resulting_ft).to be_present - expect(resulting_ft.financial_transaction_type).to eq ftt1 - expect(resulting_ft.amount).to eq amount - expect(resulting_ft.note).to eq note - expect(resulting_ft.user).to eq user - end - end - - shared_examples "financial_transactions endpoint failure" do |status| - it "returns status #{status}" do - request - expect(response.status).to eq status - end - - it "does not change the ordergroup" do - expect { request }.to_not change { - user.ordergroup.attributes - } - end - - it "does not change the financial_transactions of ordergroup" do - expect { request }.to_not change { - user.ordergroup.financial_transactions.count - } - end - end - - describe "POST :create" do - let(:ft_params) { { amount: amount, financial_transaction_type_id: ftt1.id, note: note } } - let(:request) { post :create, params: { financial_transaction: ft_params, foodcoop: 'f' } } - - context 'without using self service' do - include_examples "financial_transactions endpoint failure", 403 - end - - context 'with using self service' do - before { FoodsoftConfig[:use_self_service] = true } - - context "with no existing financial transaction" do - include_examples "financial_transactions create/update success" - end - - context "with existing financial transaction" do - before { user.ordergroup.add_financial_transaction! 5000, 'for ordering', user, ftt3 } - - include_examples "financial_transactions create/update success" - end - - context "with invalid financial transaction type" do - let(:ft_params) { { amount: amount, financial_transaction_type_id: -1, note: note } } - - include_examples "financial_transactions endpoint failure", 404 - end - - context "without note" do - let(:ft_params) { { amount: amount, financial_transaction_type_id: ftt1.id } } - - include_examples "financial_transactions endpoint failure", 422 - end - - context 'without enough balance' do - before { FoodsoftConfig[:minimum_balance] = 1000 } - - include_examples "financial_transactions endpoint failure", 403 - end - end - end -end diff --git a/spec/api/v1/user/group_order_articles_spec.rb b/spec/api/v1/user/group_order_articles_spec.rb deleted file mode 100644 index 3bfa299e..00000000 --- a/spec/api/v1/user/group_order_articles_spec.rb +++ /dev/null @@ -1,220 +0,0 @@ -require 'spec_helper' - -# Most routes are tested in the swagger_spec, this tests endpoints that change data. -describe Api::V1::User::GroupOrderArticlesController, type: :controller do - include ApiOAuth - let(:user) { create(:user, :ordergroup) } - let(:json_goa) { json_response['group_order_article'] } - let(:json_oa) { json_response['order_article'] } - let(:api_scopes) { ['group_orders:user'] } - - let(:order) { create(:order, article_count: 1) } - let(:oa_1) { order.order_articles.first } - - let(:other_quantity) { rand(1..10) } - let(:other_tolerance) { rand(1..10) } - let(:user_other) { create(:user, :ordergroup) } - let!(:go_other) { create(:group_order, order: order, ordergroup: user_other.ordergroup) } - let!(:goa_other) { create(:group_order_article, group_order: go_other, order_article: oa_1, quantity: other_quantity, tolerance: other_tolerance) } - - before { go_other.update_price!; user_other.ordergroup.update_stats! } - - shared_examples "group_order_articles endpoint success" do - before { request } - - it "returns status 200" do - expect(response).to have_http_status :ok - end - - it "returns the order_article" do - expect(json_oa['id']).to eq oa_1.id - expect(json_oa['quantity']).to eq new_quantity + other_quantity - expect(json_oa['tolerance']).to eq new_tolerance + other_tolerance - end - - it "updates the group_order" do - go = nil - expect { - request - go = user.ordergroup.group_orders.where(order: order).last - }.to change { go&.updated_by }.to(user) - .and change { go&.price } - end - end - - shared_examples "group_order_articles create/update success" do - include_examples "group_order_articles endpoint success" - - it "returns the group_order_article" do - expect(json_goa['id']).to be_present - expect(json_goa['order_article_id']).to eq oa_1.id - expect(json_goa['quantity']).to eq new_quantity - expect(json_goa['tolerance']).to eq new_tolerance - end - - it "updates the group_order_article" do - resulting_goa = GroupOrderArticle.where(id: json_goa['id']).first - expect(resulting_goa).to be_present - expect(resulting_goa.quantity).to eq new_quantity - expect(resulting_goa.tolerance).to eq new_tolerance - end - end - - shared_examples "group_order_articles endpoint failure" do |status| - it "returns status #{status}" do - request - expect(response.status).to eq status - end - - it "does not change the group_order" do - expect { request }.to_not change { - go = user.ordergroup.group_orders.where(order: order).last - go&.attributes - } - end - - it "does not change the group_order_article" do - expect { request }.to_not change { - goa = GroupOrderArticle.joins(:group_order) - .where(order_article_id: oa_1.id, group_orders: { ordergroup: user.ordergroup }).last - goa&.attributes - } - end - end - - describe "POST :create" do - let(:new_quantity) { rand(1..10) } - let(:new_tolerance) { rand(1..10) } - - let(:goa_params) { { order_article_id: oa_1.id, quantity: new_quantity, tolerance: new_tolerance } } - let(:request) { post :create, params: { group_order_article: goa_params, foodcoop: 'f' } } - - context "with no existing group_order" do - include_examples "group_order_articles create/update success" - end - - context "with an existing group_order" do - let!(:go) { create(:group_order, order: order, ordergroup: user.ordergroup) } - - include_examples "group_order_articles create/update success" - end - - context "with an existing group_order_article" do - let!(:go) { create(:group_order, order: order, ordergroup: user.ordergroup) } - let!(:goa) { create(:group_order_article, group_order: go, order_article: oa_1, quantity: 0, tolerance: 1) } - - before { go.update_price!; user.ordergroup.update_stats! } - - include_examples "group_order_articles endpoint failure", 422 - end - - context "with invalid parameter values" do - let(:goa_params) { { order_article_id: oa_1.id, quantity: -1, tolerance: new_tolerance } } - - include_examples "group_order_articles endpoint failure", 422 - end - - context 'with a closed order' do - let(:order) { create(:order, article_count: 1, state: :finished) } - - include_examples "group_order_articles endpoint failure", 404 - end - - context 'without enough balance' do - before { FoodsoftConfig[:minimum_balance] = 1000 } - - include_examples "group_order_articles endpoint failure", 403 - end - - context 'without enough apple points' do - before { allow_any_instance_of(Ordergroup).to receive(:not_enough_apples?).and_return(true) } - - include_examples "group_order_articles endpoint failure", 403 - end - end - - describe "PATCH :update" do - let(:new_quantity) { rand(2..10) } - let(:goa_params) { { quantity: new_quantity, tolerance: new_tolerance } } - let(:request) { patch :update, params: { id: goa.id, group_order_article: goa_params, foodcoop: 'f' } } - let(:new_tolerance) { rand(2..10) } - - let!(:go) { create(:group_order, order: order, ordergroup: user.ordergroup) } - let!(:goa) { create(:group_order_article, group_order: go, order_article: oa_1, quantity: 1, tolerance: 0) } - - before { go.update_price!; user.ordergroup.update_stats! } - - context "happy flow" do - include_examples "group_order_articles create/update success" - end - - context "with invalid parameter values" do - let(:goa_params) { { order_article_id: oa_1.id, quantity: -1, tolerance: new_tolerance } } - - include_examples "group_order_articles endpoint failure", 422 - end - - context 'with a closed order' do - let(:order) { create(:order, article_count: 1, state: :finished) } - - include_examples "group_order_articles endpoint failure", 404 - end - - context 'without enough balance' do - before { FoodsoftConfig[:minimum_balance] = 1000 } - - include_examples "group_order_articles endpoint failure", 403 - end - - context 'without enough apple points' do - before { allow_any_instance_of(Ordergroup).to receive(:not_enough_apples?).and_return(true) } - - include_examples "group_order_articles endpoint failure", 403 - end - end - - describe "DELETE :destroy" do - let(:new_quantity) { 0 } - let(:request) { delete :destroy, params: { id: goa.id, foodcoop: 'f' } } - let(:new_tolerance) { 0 } - - let!(:go) { create(:group_order, order: order, ordergroup: user.ordergroup) } - let!(:goa) { create(:group_order_article, group_order: go, order_article: oa_1) } - - before { go.update_price!; user.ordergroup.update_stats! } - - shared_examples "group_order_articles destroy success" do - include_examples "group_order_articles endpoint success" - - it "does not return the group_order_article" do - expect(json_goa).to be_nil - end - - it "deletes the group_order_article" do - expect(GroupOrderArticle.where(id: goa.id)).to be_empty - end - end - - context "happy flow" do - include_examples "group_order_articles destroy success" - end - - context 'with a closed order' do - let(:order) { create(:order, article_count: 1, state: :finished) } - - include_examples "group_order_articles endpoint failure", 404 - end - - context 'without enough balance' do - before { FoodsoftConfig[:minimum_balance] = 1000 } - - include_examples "group_order_articles destroy success" - end - - context 'without enough apple points' do - before { allow_any_instance_of(Ordergroup).to receive(:not_enough_apples?).and_return(true) } - - include_examples "group_order_articles destroy success" - end - end -end diff --git a/spec/api/v1/user/ordergroup_spec.rb b/spec/api/v1/user/ordergroup_spec.rb deleted file mode 100644 index 5eacb63e..00000000 --- a/spec/api/v1/user/ordergroup_spec.rb +++ /dev/null @@ -1,55 +0,0 @@ -require 'spec_helper' - -describe Api::V1::User::OrdergroupController, type: :controller do - include ApiOAuth - let(:user) { create :user, :ordergroup } - let(:api_scopes) { ['finance:user'] } - - let(:ftc1) { create :financial_transaction_class } - let(:ftc2) { create :financial_transaction_class } - let(:ftt1) { create :financial_transaction_type, financial_transaction_class: ftc1 } - let(:ftt2) { create :financial_transaction_type, financial_transaction_class: ftc2 } - let(:ftt3) { create :financial_transaction_type, financial_transaction_class: ftc2 } - - describe "GET :financial_overview" do - let(:order) { create(:order, article_count: 1) } - let(:json_financial_overview) { json_response['financial_overview'] } - let(:oa_1) { order.order_articles.first } - - let!(:go) { create(:group_order, order: order, ordergroup: user.ordergroup) } - let!(:goa) { create(:group_order_article, group_order: go, order_article: oa_1, quantity: 1, tolerance: 0) } - - before { go.update_price!; user.ordergroup.update_stats! } - - before do - og = user.ordergroup - og.add_financial_transaction!(-1, '-1', user, ftt1) - og.add_financial_transaction!(2, '2', user, ftt1) - og.add_financial_transaction!(3, '3', user, ftt1) - - og.add_financial_transaction!(-10, '-10', user, ftt2) - og.add_financial_transaction!(20, '20', user, ftt2) - og.add_financial_transaction!(30, '30', user, ftt2) - - og.add_financial_transaction!(-100, '-100', user, ftt3) - og.add_financial_transaction!(200, '200', user, ftt3) - og.add_financial_transaction!(300, '300', user, ftt3) - end - - it "returns correct values" do - get :financial_overview, params: { foodcoop: 'f' } - expect(json_financial_overview['account_balance']).to eq 444 - expect(json_financial_overview['available_funds']).to eq 444 - go.price - - ftcs = Hash[json_financial_overview['financial_transaction_class_sums'].map { |x| [x['id'], x] }] - - ftcs1 = ftcs[ftc1.id] - expect(ftcs1['name']).to eq ftc1.name - expect(ftcs1['amount']).to eq 4 - - ftcs2 = ftcs[ftc2.id] - expect(ftcs2['name']).to eq ftc2.name - expect(ftcs2['amount']).to eq 440 - end - end -end diff --git a/spec/app_config.yml b/spec/app_config.yml index 2e146be9..a9bd72b0 100644 --- a/spec/app_config.yml +++ b/spec/app_config.yml @@ -6,6 +6,7 @@ default: &defaults multi_coop_install: false + use_self_service: true default_scope: 'f' name: FC Minimal diff --git a/spec/requests/api/article_categories_spec.rb b/spec/requests/api/article_categories_spec.rb new file mode 100644 index 00000000..4c079ff2 --- /dev/null +++ b/spec/requests/api/article_categories_spec.rb @@ -0,0 +1,53 @@ +require 'swagger_helper' + +describe 'Article Categories', type: :request do + include ApiHelper + + path '/article_categories' do + get 'article categories' do + tags 'Category' + produces 'application/json' + pagination_param + let(:order_article) { create(:order, article_count: 1).order_articles.first } + let(:stock_article) { create(:stock_article) } + let(:stock_order_article) { create(:stock_order, article_ids: [stock_article.id]).order_articles.first } + + response '200', 'success' do + schema type: :object, properties: { + article_categories: { + type: :array, + items: { + '$ref': '#/components/schemas/ArticleCategory' + } + } + } + run_test! + end + + it_handles_invalid_token + end + end + + path '/article_categories/{id}' do + get 'find article category by id' do + tags 'Category' + produces 'application/json' + id_url_param + + response '200', 'article category found' do + schema type: :object, properties: { + article_categories: { + type: :array, + items: { + '$ref': '#/components/schemas/ArticleCategory' + } + } + } + let(:id) { create(:article_category, name: 'dairy').id } + run_test! + end + it_handles_invalid_token_with_id + it_cannot_find_object + end + end +end diff --git a/spec/requests/api/configs_spec.rb b/spec/requests/api/configs_spec.rb new file mode 100644 index 00000000..75f48ceb --- /dev/null +++ b/spec/requests/api/configs_spec.rb @@ -0,0 +1,20 @@ +require 'swagger_helper' + +describe 'Config', type: :request do + include ApiHelper + + path '/config' do + get 'configuration variables' do + tags 'General' + produces 'application/json' + let(:api_scopes) { ['config:user'] } + + response '200', 'success' do + schema type: :object, properties: {} + run_test! + end + + it_handles_invalid_token_and_scope + end + end +end diff --git a/spec/requests/api/financial_transaction_classes_spec.rb b/spec/requests/api/financial_transaction_classes_spec.rb new file mode 100644 index 00000000..1eaf046f --- /dev/null +++ b/spec/requests/api/financial_transaction_classes_spec.rb @@ -0,0 +1,54 @@ +require 'swagger_helper' + +describe 'Financial Transaction Classes', type: :request do + include ApiHelper + + path '/financial_transaction_classes' do + get 'financial transaction classes' do + tags 'Category' + produces 'application/json' + pagination_param + let(:financial_transaction_class) { create(:financial_transaction_class) } + + response '200', 'success' do + schema type: :object, properties: { + meta: { '$ref' => '#/components/schemas/Meta' }, + financial_transaction_class: { + type: :array, + items: { + '$ref': '#/components/schemas/FinancialTransactionClass' + } + } + } + + run_test! + end + + it_handles_invalid_token + end + end + + path '/financial_transaction_classes/{id}' do + get 'Retrieves a financial transaction class' do + tags 'Category' + produces 'application/json' + id_url_param + + response '200', 'financial transaction class found' do + schema type: :object, properties: { + financial_transaction_classes: { + type: :array, + items: { + '$ref': '#/components/schemas/FinancialTransactionClass' + } + } + } + let(:id) { create(:financial_transaction_class).id } + run_test! + end + + it_handles_invalid_token_with_id + it_cannot_find_object 'financial transaction class not found' + end + end +end diff --git a/spec/requests/api/financial_transaction_types_spec.rb b/spec/requests/api/financial_transaction_types_spec.rb new file mode 100644 index 00000000..82a30f83 --- /dev/null +++ b/spec/requests/api/financial_transaction_types_spec.rb @@ -0,0 +1,52 @@ +require 'swagger_helper' + +describe 'Financial Transaction types', type: :request do + include ApiHelper + + path '/financial_transaction_types' do + get 'financial transaction types' do + tags 'Category' + produces 'application/json' + pagination_param + let(:financial_transaction_type) { create(:financial_transaction_type) } + response '200', 'success' do + schema type: :object, properties: { + meta: { '$ref' => '#/components/schemas/Meta' }, + financial_transaction_type: { + type: :array, + items: { + '$ref': '#/components/schemas/FinancialTransactionType' + } + } + } + run_test! + end + + it_handles_invalid_token + end + end + + path '/financial_transaction_types/{id}' do + get 'find financial transaction type by id' do + tags 'Category' + produces 'application/json' + id_url_param + + response '200', 'financial transaction type found' do + schema type: :object, properties: { + financial_transaction_types: { + type: :array, + items: { + '$ref': '#/components/schemas/FinancialTransactionType' + } + } + } + let(:id) { create(:financial_transaction_type).id } + run_test! + end + + it_handles_invalid_token_with_id + it_cannot_find_object 'financial transaction type not found' + end + end +end diff --git a/spec/requests/api/financial_transactions_spec.rb b/spec/requests/api/financial_transactions_spec.rb new file mode 100644 index 00000000..1d3ef2b9 --- /dev/null +++ b/spec/requests/api/financial_transactions_spec.rb @@ -0,0 +1,56 @@ +require 'swagger_helper' + +describe 'Financial Transaction', type: :request do + include ApiHelper + let!(:finance_user) { create(:user, groups: [create(:workgroup, role_finance: true)]) } + let!(:api_scopes) { ['finance:read', 'finance:write'] } + let(:api_access_token) { create(:oauth2_access_token, resource_owner_id: finance_user.id, scopes: api_scopes&.join(' ')).token } + let(:financial_transaction) { create(:financial_transaction, user: user) } + + path '/financial_transactions' do + get 'financial transactions' do + tags 'Financial Transaction' + produces 'application/json' + pagination_param + + response '200', 'success' do + schema type: :object, properties: { + meta: { '$ref' => '#/components/schemas/Meta' }, + financial_transaction: { + type: :array, + items: { + '$ref': '#/components/schemas/FinancialTransaction' + } + } + } + + run_test! + end + it_handles_invalid_token_and_scope + end + end + + path '/financial_transactions/{id}' do + get 'Retrieves a financial transaction ' do + tags 'Financial Transaction' + produces 'application/json' + id_url_param + + response '200', 'financial transaction found' do + schema type: :object, properties: { + financial_transaction: { + type: :array, + items: { + '$ref': '#/components/schemas/FinancialTransaction' + } + } + } + let(:id) { FinancialTransaction.create(user: user).id } + run_test! + end + it_handles_invalid_token_with_id + it_handles_invalid_scope_with_id + it_cannot_find_object 'financial transaction not found' + end + end +end diff --git a/spec/requests/api/navigations_spec.rb b/spec/requests/api/navigations_spec.rb new file mode 100644 index 00000000..c2312437 --- /dev/null +++ b/spec/requests/api/navigations_spec.rb @@ -0,0 +1,24 @@ +require 'swagger_helper' + +describe 'Navigation', type: :request do + include ApiHelper + + path '/navigation' do + get 'navigation' do + tags 'General' + produces 'application/json' + + response '200', 'success' do + schema type: :object, properties: { + navigation: { + '$ref' => '#/components/schemas/Navigation' + } + } + + run_test! + end + + it_handles_invalid_token + end + end +end diff --git a/spec/requests/api/order_articles_spec.rb b/spec/requests/api/order_articles_spec.rb new file mode 100644 index 00000000..17feefa6 --- /dev/null +++ b/spec/requests/api/order_articles_spec.rb @@ -0,0 +1,115 @@ +require 'swagger_helper' + +describe 'Order Articles', type: :request do + include ApiHelper + + path '/order_articles' do + get 'order articles' do + tags 'Order' + produces 'application/json' + pagination_param + q_ordered_url_param + + let(:api_scopes) { ['orders:read', 'orders:write'] } + let(:order) { create(:order, article_count: 4) } + let(:order_articles) { order.order_articles } + + before do + order_articles[0].update! quantity: 0, tolerance: 0, units_to_order: 0 + order_articles[1].update! quantity: 1, tolerance: 0, units_to_order: 0 + order_articles[2].update! quantity: 0, tolerance: 1, units_to_order: 0 + order_articles[3].update! quantity: 0, tolerance: 0, units_to_order: 1 + end + + response '200', 'success' do + schema type: :object, properties: { + meta: { '$ref' => '#/components/schemas/Meta' }, + order_articles: { + type: :array, + items: { + '$ref': '#/components/schemas/OrderArticle' + } + } + } + describe '(unset)' do + run_test! + end + + describe 'all' do + let(:q) { { q: { ordered: 'all' } } } + + run_test! do |response| + json_order_articles = JSON.parse(response.body)['order_articles'] + json_order_article_ids = json_order_articles.map { |d| d['id'].to_i } + expect(json_order_article_ids).to match_array order_articles[1..2].map(&:id) + end + end + + describe 'when ordered by supplier' do + let(:q) { { q: { ordered: 'supplier' } } } + + run_test! do |response| + json_order_articles = JSON.parse(response.body)['order_articles'] + json_order_article_ids = json_order_articles.map { |d| d['id'].to_i } + expect(json_order_article_ids).to match_array [order_articles[3].id] + end + end + + describe 'when ordered by member' do + let(:q) { { q: { ordered: 'member' } } } + + run_test! do |response| + json_order_articles = JSON.parse(response.body)['order_articles'] + expect(json_order_articles.count).to eq 0 + end + end + + context 'when ordered by user' do + let(:user) { create(:user, :ordergroup) } + let(:go) { create(:group_order, order: order, ordergroup: user.ordergroup) } + + before do + create(:group_order_article, group_order: go, order_article: order_articles[1], quantity: 1) + create(:group_order_article, group_order: go, order_article: order_articles[2], tolerance: 0) + end + + describe 'member' do + let(:q) { { q: { ordered: 'member' } } } + + run_test! do |response| + json_order_articles = JSON.parse(response.body)['order_articles'] + json_order_article_ids = json_order_articles.map { |d| d['id'].to_i } + expect(json_order_article_ids).to match_array order_articles[1..2].map(&:id) + end + end + end + end + + it_handles_invalid_token_and_scope + end + end + + path '/order_articles/{id}' do + get 'order articles' do + tags 'Order' + produces 'application/json' + id_url_param + let(:api_scopes) { ['orders:read', 'orders:write'] } + + response '200', 'success' do + schema type: :object, properties: { + order_article: { + '$ref': '#/components/schemas/OrderArticle' + } + } + let(:order) { create(:order, article_count: 1) } + let(:id) { order.order_articles.first.id } + + run_test! + end + + it_handles_invalid_token_and_scope + it_cannot_find_object 'order article not found' + end + end +end diff --git a/spec/requests/api/orders_spec.rb b/spec/requests/api/orders_spec.rb new file mode 100644 index 00000000..c0505d7f --- /dev/null +++ b/spec/requests/api/orders_spec.rb @@ -0,0 +1,55 @@ +require 'swagger_helper' + +describe 'Orders', type: :request do + include ApiHelper + let(:api_scopes) { ['orders:read'] } + + path '/orders' do + get 'orders' do + tags 'Order' + produces 'application/json' + pagination_param + let(:order) { create(:order) } + + response '200', 'success' do + schema type: :object, properties: { + meta: { '$ref' => '#/components/schemas/Meta' }, + ordes: { + type: :array, + items: { + '$ref': '#/components/schemas/Order' + } + } + } + + run_test! + end + + it_handles_invalid_token_and_scope + end + end + + path '/orders/{id}' do + get 'Order' do + tags 'Order' + produces 'application/json' + id_url_param + + let(:order) { create(:order) } + + response '200', 'success' do + schema type: :object, properties: { + order: { '$ref' => '#/components/schemas/Order' } + } + let(:id) { order.id } + + run_test! do |response| + expect(JSON.parse(response.body)['order']['id']).to eq order.id + end + end + + it_handles_invalid_token_and_scope + it_cannot_find_object 'order not found' + end + end +end diff --git a/spec/requests/api/user/financial_transactions_spec.rb b/spec/requests/api/user/financial_transactions_spec.rb new file mode 100644 index 00000000..4fb69cd6 --- /dev/null +++ b/spec/requests/api/user/financial_transactions_spec.rb @@ -0,0 +1,109 @@ +require 'swagger_helper' + +describe 'User', type: :request do + include ApiHelper + + let(:api_scopes) { ['finance:user'] } + let(:user) { create :user, groups: [create(:ordergroup)] } + let(:other_user2) { create :user } + let(:ft) { create(:financial_transaction, user: user, ordergroup: user.ordergroup) } + + before do + ft + end + + path '/user/financial_transactions' do + post 'create new financial transaction (requires enabled self service)' do + tags 'Financial Transaction' + consumes 'application/json' + produces 'application/json' + + parameter name: :financial_transaction, in: :body, schema: { + type: :object, + properties: { + amount: { type: :integer }, + financial_transaction_type: { type: :integer }, + note: { type: :string } + } + } + + let(:financial_transaction) { { amount: 3, financial_transaction_type_id: create(:financial_transaction_type).id, note: 'lirum larum' } } + + response '200', 'success' do + schema type: :object, properties: { + financial_transaction: { '$ref': '#/components/schemas/FinancialTransaction' } + } + run_test! + end + + it_handles_invalid_token_with_id + it_handles_invalid_scope_with_id 'user has no ordergroup, is below minimum balance, self service is disabled, or missing scope' + + response '404', 'financial transaction type not found' do + schema '$ref' => '#/components/schemas/Error404' + let(:financial_transaction) { { amount: 3, financial_transaction_type_id: 'invalid', note: 'lirum larum' } } + run_test! + end + + # TODO: fix controller to actually send a 422 for invalid params? + # Expected response code '200' to match '422' + # Response body: {"financial_transaction":{"id":316,"user_id":599,"user_name":"Lisbeth ","amount":-3.0,"note":"-2","created_at":"2022-12-12T13:05:32.000+01:00","financial_transaction_type_id":346,"financial_transaction_type_name":"aut est iste #9"}} + # + # response '422', 'invalid parameter value' do + # # schema '$ref' => '#/components/schemas/Error422' + # let(:financial_transaction) { { amount: -3, financial_transaction_type_id: create(:financial_transaction_type).id, note: -2 } } + # run_test! + # end + end + + get "financial transactions of the member's ordergroup" do + tags 'User', 'Financial Transaction' + produces 'application/json' + pagination_param + + response '200', 'success' do + schema type: :object, properties: { + meta: { '$ref': '#/components/schemas/Meta' }, + financial_transaction: { + type: :array, + items: { + '$ref': '#/components/schemas/FinancialTransaction' + } + } + } + + run_test! do |response| + data = JSON.parse(response.body) + expect(data['financial_transactions'].first['id']).to eq(ft.id) + end + end + + it_handles_invalid_token_and_scope + end + end + + path '/user/financial_transactions/{id}' do + get 'find financial transaction by id' do + tags 'User', 'Financial Transaction' + produces 'application/json' + id_url_param + + response '200', 'success' do + schema type: :object, properties: { + financial_transaction: { + '$ref': '#/components/schemas/FinancialTransaction' + } + } + let(:id) { ft.id } + run_test! do |response| + data = JSON.parse(response.body) + expect(data['financial_transaction']['id']).to eq(ft.id) + end + end + + it_handles_invalid_token_with_id + it_handles_invalid_scope_with_id 'user has no ordergroup or missing scope' + it_cannot_find_object 'financial transaction not found' + end + end +end diff --git a/spec/requests/api/user/group_order_articles_spec.rb b/spec/requests/api/user/group_order_articles_spec.rb new file mode 100644 index 00000000..205a4070 --- /dev/null +++ b/spec/requests/api/user/group_order_articles_spec.rb @@ -0,0 +1,192 @@ +require 'swagger_helper' + +describe 'User', type: :request do + include ApiHelper + + let(:api_scopes) { ['group_orders:user'] } + let(:user) { create :user, groups: [create(:ordergroup)] } + let(:other_user2) { create :user } + let(:order) { create(:order, article_count: 4) } + let(:order_articles) { order.order_articles } + let(:group_order) { create :group_order, ordergroup: user.ordergroup, order_id: order.id } + let(:goa) { create :group_order_article, group_order: group_order, order_article: order_articles.first } + + before do + goa + end + + path '/user/group_order_articles' do + get 'group order articles' do + tags 'User', 'Order' + produces 'application/json' + pagination_param + q_ordered_url_param + + response '200', 'success' do + schema type: :object, properties: { + meta: { '$ref': '#/components/schemas/Meta' }, + group_order_article: { + type: :array, + items: { + '$ref': '#/components/schemas/GroupOrderArticle' + } + } + } + + run_test! do |response| + data = JSON.parse(response.body) + expect(data['group_order_articles'].first['id']).to eq(goa.id) + end + end + + it_handles_invalid_token + it_handles_invalid_scope 'user has no ordergroup or missing scope' + end + + post 'create new group order article' do + tags 'User', 'Order' + consumes 'application/json' + produces 'application/json' + parameter name: :group_order_article, in: :body, + description: 'group order article to create', + required: true, + schema: { '$ref': '#/components/schemas/GroupOrderArticleForCreate' } + + let(:group_order_article) { { order_article_id: order_articles.last.id, quantity: 1, tolerance: 2 } } + response '200', 'success' do + schema type: :object, properties: { + group_order_article: { + '$ref': '#/components/schemas/GroupOrderArticle' + }, + order_article: { + '$ref': '#/components/schemas/OrderArticle' + } + } + run_test! + end + + it_handles_invalid_token_with_id + it_handles_invalid_scope_with_id 'user has no ordergroup, order not open, is below minimum balance, has not enough apple points, or missing scope' + + response '404', 'order article not found in open orders' do + let(:group_order_article) { { order_article_id: 'invalid', quantity: 1, tolerance: 2 } } + schema '$ref' => '#/components/schemas/Error404' + run_test! + end + + response '422', 'invalid parameter value or group order article already exists' do + let(:group_order_article) { { order_article_id: goa.order_article_id, quantity: 1, tolerance: 2 } } + schema '$ref' => '#/components/schemas/Error422' + run_test! + end + end + end + + path '/user/group_order_articles/{id}' do + get 'find group order article by id' do + tags 'User', 'Order' + produces 'application/json' + id_url_param + + response '200', 'success' do + schema type: :object, properties: { + group_order_article: { + '$ref': '#/components/schemas/GroupOrderArticle' + }, + order_article: { + '$ref': '#/components/schemas/OrderArticle' + } + } + + let(:id) { goa.id } + run_test! do |response| + data = JSON.parse(response.body) + expect(data['group_order_article']['id']).to eq(goa.id) + end + end + + it_handles_invalid_scope_with_id + it_handles_invalid_token_with_id + it_cannot_find_object 'group order article not found' + end + + patch 'update a group order article (but delete if quantity and tolerance are zero)' do + tags 'User', 'Order' + consumes 'application/json' + produces 'application/json' + id_url_param + parameter name: :group_order_article, in: :body, + description: 'group order article update', + required: true, + schema: { '$ref': '#/components/schemas/GroupOrderArticleForUpdate' } + + let(:id) { goa.id } + let(:group_order_article) { { order_article_id: goa.order_article_id, quantity: 2, tolerance: 2 } } + + response '200', 'success' do + schema type: :object, properties: { + group_order_article: { + '$ref': '#/components/schemas/GroupOrderArticle' + } + } + run_test! + end + + response 401, 'not logged-in' do + schema '$ref' => '#/components/schemas/Error401' + let(:Authorization) { 'abc' } + run_test! + end + + response 403, 'user has no ordergroup, order not open, is below minimum balance, has not enough apple points, or missing scope' do + let(:api_scopes) { ['none'] } + schema '$ref' => '#/components/schemas/Error403' + run_test! + end + + response '404', 'order article not found in open orders' do + schema type: :object, properties: { + group_order_article: { + '$ref': '#/components/schemas/GroupOrderArticle' + } + } + let(:id) { 'invalid' } + run_test! + end + + response '422', 'invalid parameter value' do + let(:group_order_article) { { order_article_id: 'invalid', quantity: -5, tolerance: 'invalid' } } + schema '$ref' => '#/components/schemas/Error422' + run_test! + end + end + + delete 'remove group order article' do + tags 'User', 'Order' + consumes 'application/json' + produces 'application/json' + id_url_param + let(:api_scopes) { ['group_orders:user'] } + + response '200', 'success' do + schema type: :object, properties: { + group_order_article: { + '$ref': '#/components/schemas/GroupOrderArticle' + } + } + let(:id) { goa.id } + run_test! + end + + it_handles_invalid_token_with_id + + response 403, 'user has no ordergroup, order not open, is below minimum balance, has not enough apple points, or missing scope' do + let(:api_scopes) { ['none'] } + schema '$ref' => '#/components/schemas/Error403' + run_test! + end + + it_cannot_find_object 'order article not found in open orders' + end + end +end diff --git a/spec/requests/api/user/users_spec.rb b/spec/requests/api/user/users_spec.rb new file mode 100644 index 00000000..0d3196bc --- /dev/null +++ b/spec/requests/api/user/users_spec.rb @@ -0,0 +1,103 @@ +require 'swagger_helper' + +describe 'User', type: :request do + include ApiHelper + + path '/user' do + get 'info about the currently logged-in user' do + tags 'User' + produces 'application/json' + let(:api_scopes) { ['user:read'] } + let(:other_user1) { create :user } + let(:user) { create :user } + let(:other_user2) { create :user } + + response '200', 'success' do + schema type: :object, + properties: { + user: { + type: :object, + properties: { + id: { + type: :integer + }, + name: { + type: :string, + description: 'full name' + }, + email: { + type: :string, + description: 'email address' + }, + locale: { + type: :string, + description: 'language code' + } + }, + required: %w[id name email] + } + } + + run_test! do |response| + data = JSON.parse(response.body) + expect(data['user']['id']).to eq(user.id) + end + end + + it_handles_invalid_token_and_scope + end + end + + path '/user/financial_overview' do + get 'financial summary about the currently logged-in user' do + tags 'User', 'Financial Transaction' + produces 'application/json' + let(:user) { create :user, :ordergroup } + let(:api_scopes) { ['finance:user'] } + FinancialTransactionClass.create(name: 'TestTransaction') + + response 200, 'success' do + schema type: :object, + properties: { + financial_overview: { + type: :object, + properties: { + + account_balance: { + type: :number, + description: 'booked accout balance of ordergroup' + }, + available_funds: { + type: :number, + description: 'fund available to order articles' + }, + financial_transaction_class_sums: { + type: :array, + properties: { + id: { + type: :integer, + description: 'id of the financial transaction class' + }, + name: { + type: :string, + description: 'name of the financial transaction class' + }, + amount: { + type: :number, + description: 'sum of the amounts belonging to the financial transaction class' + } + }, + required: %w[id name amount] + } + }, + required: %w[account_balance available_funds financial_transaction_class_sums] + } + } + + run_test! + end + + it_handles_invalid_token_and_scope + end + end +end diff --git a/spec/support/api_helper.rb b/spec/support/api_helper.rb index ee0225f5..86e2ca07 100644 --- a/spec/support/api_helper.rb +++ b/spec/support/api_helper.rb @@ -5,21 +5,60 @@ module ApiHelper let(:user) { create(:user) } let(:api_scopes) { [] } # empty scopes for stricter testing (in reality this would be default_scopes) let(:api_access_token) { create(:oauth2_access_token, resource_owner_id: user.id, scopes: api_scopes&.join(' ')).token } - let(:api_authorization) { "Bearer #{api_access_token}" } + let(:Authorization) { "Bearer #{api_access_token}" } - def self.it_handles_invalid_token(method, path, params_block = -> { api_auth }) + def self.it_handles_invalid_token context 'with invalid access token' do - let(:api_access_token) { 'abc' } + let(:Authorization) { 'abc' } - it { is_expected.to validate(method, path, 401, instance_exec(¶ms_block)) } + response 401, 'not logged-in' do + schema '$ref' => '#/components/schemas/Error401' + run_test! + end end end - def self.it_handles_invalid_scope(method, path, params_block = -> { api_auth }) + def self.it_handles_invalid_token_with_id + context 'with invalid access token' do + let(:Authorization) { 'abc' } + let(:id) { 42 } # id doesn't matter here + + response 401, 'not logged-in' do + schema '$ref' => '#/components/schemas/Error401' + run_test! + end + end + end + + def self.it_handles_invalid_scope(description = 'missing scope') context 'with invalid scope' do let(:api_scopes) { ['none'] } - it { is_expected.to validate(method, path, 403, instance_exec(¶ms_block)) } + response 403, description do + schema '$ref' => '#/components/schemas/Error403' + run_test! + end + end + end + + def self.it_handles_invalid_scope_with_id(description = 'missing scope') + context 'with invalid scope' do + let(:api_scopes) { ['none'] } + let(:id) { 42 } # id doesn't matter here + + response 403, description do + schema '$ref' => '#/components/schemas/Error403' + run_test! + end + end + end + + def self.it_cannot_find_object(description = 'not found') + let(:id) { 'invalid' } + + response 404, description do + schema '$ref' => '#/components/schemas/Error404' + run_test! end end @@ -27,13 +66,25 @@ module ApiHelper it_handles_invalid_token(*args) it_handles_invalid_scope(*args) end - end - # Add authentication to parameters for {Swagger::RspecHelpers#validate} - # @param params [Hash] Query parameters - # @return Query parameters with authentication header - # @see Swagger::RspecHelpers#validate - def api_auth(params = {}) - { '_headers' => { 'Authorization' => api_authorization } }.deep_merge(params) + def self.id_url_param + parameter name: :id, in: :path, type: :integer, required: true + end + + def self.pagination_param + parameter name: :per_page, in: :query, type: :integer, required: false + parameter name: :page, in: :query, type: :integer, required: false + end + + def self.q_ordered_url_param + parameter name: :q, in: :query, required: false, + description: "'member' show articles ordered by the user's ordergroup, 'all' by all members, and 'supplier' ordered at the supplier", + schema: { + type: :object, + properties: { + ordered: { '$ref' => '#/components/schemas/q_ordered' } + } + } + end end end diff --git a/spec/swagger_helper.rb b/spec/swagger_helper.rb new file mode 100644 index 00000000..912504b8 --- /dev/null +++ b/spec/swagger_helper.rb @@ -0,0 +1,513 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.configure do |config| + # Specify a root folder where Swagger JSON files are generated + # NOTE: If you're using the rswag-api to serve API descriptions, you'll need + # to ensure that it's configured to serve Swagger from the same folder + config.swagger_root = Rails.root.join('swagger').to_s + + # Define one or more Swagger documents and provide global metadata for each one + # When you run the 'rswag:specs:swaggerize' rake task, the complete Swagger will + # be generated at the provided relative path under swagger_root + # By default, the operations defined in spec files are added to the first + # document below. You can override this behavior by adding a swagger_doc tag to the + # the root example_group in your specs, e.g. describe '...', swagger_doc: 'v2/swagger.json' + config.swagger_docs = { + 'v1/swagger.yaml' => { + openapi: '3.0.3', + info: { + title: 'API V1', + version: 'v1' + }, + paths: {}, + components: { + schemas: { + pagination: { + type: :object, + properties: { + recordCount: { type: :integer }, + pageCount: { type: :integer }, + currentPage: { type: :integer }, + pageSize: { type: :integer } + }, + required: %w(recordCount pageCount currentPage pageSize) + }, + Order: { + type: :object, + properties: { + id: { + type: :integer + }, + name: { + type: :string, + description: "name of the order's supplier (or stock)" + }, + starts: { + type: :string, + format: 'date-time', + description: 'when the order was opened' + }, + ends: { + type: :string, + nullable: true, + format: 'date-time', + description: 'when the order will close or was closed' + }, + boxfill: { + type: :string, + nullable: true, + format: 'date-time', + description: 'when the order will enter or entered the boxfill phase' + }, + pickup: { + type: :string, + nullable: true, + format: :date, + description: 'pickup date' + }, + is_open: { + type: :boolean, + description: 'if the order is currently open or not' + }, + is_boxfill: { + type: :boolean, + description: 'if the order is currently in the boxfill phase or not' + } + } + }, + Article: { + type: :object, + properties: { + id: { + type: :integer + }, + name: { + type: :string + }, + supplier_id: { + type: :integer, + description: 'id of supplier, or 0 for stock articles' + }, + supplier_name: { + type: :string, + nullable: true, + description: 'name of the supplier, or null for stock articles' + }, + unit: { + type: :string, + description: 'amount of each unit, e.g. "100 g" or "kg"' + }, + unit_quantity: { + type: :integer, + description: 'units can only be ordered from the supplier in multiples of unit_quantity' + }, + note: { + type: :string, + nullable: true, + description: 'generic note' + }, + manufacturer: { + type: :string, + nullable: true, + description: 'manufacturer' + }, + origin: { + type: :string, + nullable: true, + description: 'origin, preferably (starting with a) 2-letter ISO country code' + }, + article_category_id: { + type: :integer, + description: 'id of article category' + }, + quantity_available: { + type: :integer, + description: 'number of units available (only present on stock articles)' + } + }, + required: %w[id name supplier_id supplier_name unit unit_quantity note manufacturer origin article_category_id] + }, + OrderArticle: { + type: :object, + properties: { + id: { + type: :integer + }, + order_id: { + type: :integer, + description: 'id of order this order article belongs to' + }, + price: { + type: :number, + format: :float, + description: 'foodcoop price' + }, + quantity: { + type: :integer, + description: 'number of units ordered by members' + }, + tolerance: { + type: :integer, + description: 'number of extra units that members are willing to buy to fill a box' + }, + units_to_order: { + type: :integer, + description: 'number of units to order from the supplier' + }, + article: { + '$ref': '#/components/schemas/Article' + } + } + }, + ArticleCategory: { + type: :object, + properties: { + id: { + type: :integer + }, + name: { + type: :string + } + }, + required: %w[id name] + }, + FinancialTransaction: { + allOf: [ + { '$ref': '#/components/schemas/FinancialTransactionForCreate' }, + { + type: :object, + properties: { + id: { + type: :integer + }, + amount: { + type: :number, + format: :float, + description: 'amount credited (negative for a debit transaction)' + }, + financial_transaction_type_id: { + type: :integer, + description: 'id of the type of the transaction' + }, + note: { + type: :string, + description: 'note entered with the transaction' + }, + user_id: { + type: :integer, + nullable: true, + description: 'id of user who entered the transaction (may be null for deleted users or 0 for a system user)' + }, + user_name: { + type: :string, + nullable: true, + description: 'name of user who entered the transaction (may be null or empty string for deleted users or system users)' + }, + financial_transaction_type_name: { + type: :string, + description: 'name of the type of the transaction' + }, + created_at: { + type: :string, + format: 'date-time', + description: 'when the transaction was entered' + } + }, + required: %w[id user_id user_name financial_transaction_type_name created_at] + } + ] + }, + FinancialTransactionForCreate: { + type: :object, + properties: { + amount: { + type: :number, + format: :float, + description: 'amount credited (negative for a debit transaction)' + }, + financial_transaction_type_id: + { + type: :integer, + description: 'id of the type of the transaction' + }, + note: { + type: :string, + description: 'note entered with the transaction' + } + }, + required: %w[amount note user_id] + }, + FinancialTransactionClass: { + type: :object, + properties: { + id: { + type: :integer + }, + name: { + type: :string + } + }, + required: %w[id name] + }, + FinancialTransactionType: { + type: :object, + properties: { + id: { + type: :integer + }, + name: { + type: :string + }, + name_short: { + type: :string, + nullable: true, + description: 'short name (used for bank transfers)' + }, + bank_account_id: { + type: :integer, + nullable: true, + description: 'id of the bank account used for this transaction type' + }, + bank_account_name: { + type: :string, + nullable: true, + description: 'name of the bank account used for this transaction type' + }, + bank_account_iban: { + type: :string, + nullable: true, + description: 'IBAN of the bank account used for this transaction type' + }, + financial_transaction_class_id: { + type: :integer, + description: 'id of the class of the transaction' + }, + financial_transaction_class_name: { + type: :string, + description: 'name of the class of the transaction' + } + }, + required: %w[id name financial_transaction_class_id financial_transaction_class_name] + }, + GroupOrderArticleForUpdate: { + type: :object, + properties: { + quantity: + { + type: :integer, + description: 'number of units ordered by the users ordergroup' + }, + tolerance: + { + type: :integer, + description: 'number of extra units the users ordergroup is willing to buy for filling a box' + } + } + }, + GroupOrderArticleForCreate: { + allOf: [ + { '$ref': '#/components/schemas/GroupOrderArticleForUpdate' }, + { + type: :object, + properties: { + order_article_id: + { + type: :integer, + description: 'id of order article' + } + } + } + ] + }, + GroupOrderArticle: { + allOf: [ + { '$ref': '#/components/schemas/GroupOrderArticleForCreate' }, + { + type: :object, + properties: { + id: { + type: :integer + }, + result: { + type: :number, + format: :float, + description: 'number of units the users ordergroup will receive or has received' + }, + total_price: + { + type: :number, + format: :float, + description: 'total price of this group order article' + }, + order_article_id: + { + type: :integer, + description: 'id of order article' + } + }, + required: %w[order_article_id] + } + ] + }, + q_ordered: { + type: :object, + properties: { + ordered: { + type: :string, + enum: %w[member all supplier] + } + } + }, + Meta: { + type: :object, + properties: { + page: { + type: :integer, + description: 'page number of the returned collection' + }, + per_page: { + type: :integer, + description: 'number of items per page' + }, + total_pages: { + type: :integer, + description: 'total number of pages' + }, + total_count: { + type: :integer, + description: 'total number of items in the collection' + } + }, + required: %w[page per_page total_pages total_count] + }, + Navigation: { + type: :array, + items: { + type: :object, + properties: { + name: { + type: :string, + description: 'title' + }, + url: { + type: :string, + description: 'link' + }, + items: { + '$ref': "#/components/schemas/Navigation" + } + }, + required: ['name'], + minProperties: 2 # name+url or name+items + } + }, + Error: { + type: :object, + properties: { + error: { + type: :string, + description: 'error code' + }, + error_description: { + type: :string, + description: 'human-readable error message (localized)' + } + } + }, + Error401: { + type: :object, + properties: { + error: { + type: :string, + description: 'unauthorized' + }, + error_description: { + '$ref': '#/components/schemas/Error/properties/error_description' + } + } + }, + Error403: { + type: :object, + properties: { + error: { + type: :string, + description: 'forbidden or invalid_scope' + }, + error_description: { + '$ref': '#/components/schemas/Error/properties/error_description' + } + } + }, + Error404: { + type: :object, + properties: { + error: { + type: :string, + description: 'not_found' + }, + error_description: { + '$ref': '#/components/schemas/Error/properties/error_description' + } + } + }, + Error422: { + type: :object, + properties: { + error: { + type: :string, + description: 'unprocessable entity' + }, + error_description: { + '$ref': '#/components/schemas/Error/properties/error_description' + } + } + } + }, + securitySchemes: { + oauth2: { + type: :oauth2, + flows: { + implicit: { + authorizationUrl: 'http://localhost:3000/f/oauth/authorize', + scopes: { + 'config:user': 'reading Foodsoft configuration for regular users', + 'config:read': 'reading Foodsoft configuration values', + 'config:write': 'reading and updating Foodsoft configuration values', + 'finance:user': 'accessing your own financial transactions', + 'finance:read': 'reading all financial transactions', + 'finance:write': 'reading and creating financial transactions', + 'user:read': 'reading your own user profile', + 'user:write': 'reading and updating your own user profile', + offline_access: 'retain access after user has logged out' + } + } + } + } + } + }, + servers: [ + { + url: 'http://{defaultHost}/f/api/v1', + variables: { + defaultHost: { + default: 'localhost:3000' + } + } + } + ], + security: [ + oauth2: [ + 'user:read' + ] + ] + } + } + + # Specify the format of the output Swagger file when running 'rswag:specs:swaggerize'. + # The swagger_docs configuration option has the filename including format in + # the key, this may want to be changed to avoid putting yaml in json files. + # Defaults to json. Accepts ':json' and ':yaml'. + config.swagger_format = :yaml +end From d16aa19300a194d8e6b06d5dce7b865f77c8727e Mon Sep 17 00:00:00 2001 From: Philipp Rothmann Date: Mon, 5 Dec 2022 10:13:02 +0100 Subject: [PATCH 002/105] Add home controller test Co-authored-by: viehlieb Co-authored-by: Tobias Kneuker --- Gemfile | 1 + Gemfile.lock | 5 + app/controllers/home_controller.rb | 6 +- spec/controllers/home_controller_spec.rb | 199 +++++++++++++++++++++++ spec/spec_helper.rb | 6 +- spec/support/spec_test_helper.rb | 23 +++ 6 files changed, 236 insertions(+), 4 deletions(-) create mode 100644 spec/controllers/home_controller_spec.rb create mode 100644 spec/support/spec_test_helper.rb diff --git a/Gemfile b/Gemfile index 61562099..a357ef9b 100644 --- a/Gemfile +++ b/Gemfile @@ -115,6 +115,7 @@ group :test do gem 'rspec-core' gem 'rspec-rerun' gem 'i18n-spec' + gem 'rails-controller-testing' # code coverage gem 'simplecov', require: false gem 'simplecov-lcov', require: false diff --git a/Gemfile.lock b/Gemfile.lock index d86fc580..7352c8fe 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -343,6 +343,10 @@ GEM sprockets-rails (>= 2.0.0) rails-assets-listjs (0.2.0.beta.4) railties (>= 3.1) + rails-controller-testing (1.0.5) + actionpack (>= 5.0.1.rc1) + actionview (>= 5.0.1.rc1) + activesupport (>= 5.0.1.rc1) rails-dom-testing (2.0.3) activesupport (>= 4.2.0) nokogiri (>= 1.6) @@ -608,6 +612,7 @@ DEPENDENCIES rack-cors rails (~> 5.2) rails-assets-listjs (= 0.2.0.beta.4) + rails-controller-testing rails-i18n rails-settings-cached (= 0.4.3) rails_tokeninput diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb index 86f9e2eb..d01a78ca 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -18,7 +18,7 @@ class HomeController < ApplicationController @bank_accounts = @types.includes(:bank_account).map(&:bank_account).uniq.compact @bank_accounts = [BankAccount.last] if @bank_accounts.empty? else - redirect_to root_url, alert: I18n.t('group_orders.errors.no_member') + redirect_to root_path, alert: I18n.t('group_orders.errors.no_member') end end @@ -26,7 +26,7 @@ class HomeController < ApplicationController if @current_user.update(user_params) @current_user.ordergroup.update(ordergroup_params) if ordergroup_params session[:locale] = @current_user.locale - redirect_to my_profile_url, notice: I18n.t('home.changes_saved') + redirect_to my_profile_path, notice: I18n.t('home.changes_saved') else render :profile end @@ -64,7 +64,7 @@ class HomeController < ApplicationController # cancel personal memberships direct from the myProfile-page def cancel_membership if params[:membership_id] - membership = @current_user.memberships.find!(params[:membership_id]) + membership = @current_user.memberships.find(params[:membership_id]) else membership = @current_user.memberships.find_by_group_id!(params[:group_id]) end diff --git a/spec/controllers/home_controller_spec.rb b/spec/controllers/home_controller_spec.rb new file mode 100644 index 00000000..f3616cd4 --- /dev/null +++ b/spec/controllers/home_controller_spec.rb @@ -0,0 +1,199 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe HomeController, type: :controller do + let(:user) { create :user } + + describe 'GET index' do + describe 'NOT logged in' do + it 'redirects' do + get_with_defaults :profile + expect(response).to have_http_status(:redirect) + expect(response).to redirect_to(login_path) + end + end + + describe 'logegd in' do + before { login user } + + it 'assigns tasks' do + get_with_defaults :index + + expect(assigns(:unaccepted_tasks)).not_to be_nil + expect(assigns(:next_tasks)).not_to be_nil + expect(assigns(:unassigned_tasks)).not_to be_nil + expect(response).to render_template('home/index') + end + end + end + + describe 'GET profile' do + before { login user } + + it 'renders dashboard' do + get_with_defaults :profile + expect(response).to have_http_status(:success) + expect(response).to render_template('home/profile') + end + end + + describe 'GET reference_calculator' do + describe 'with simple user' do + before { login user } + + it 'redirects to home' do + get_with_defaults :reference_calculator + expect(response).to have_http_status(:redirect) + expect(response).to redirect_to(root_path) + end + end + + describe 'with ordergroup user' do + let(:og_user) { create :user, :ordergroup } + + before { login og_user } + + it 'renders reference calculator' do + get_with_defaults :reference_calculator + expect(response).to have_http_status(:success) + expect(response).to render_template('home/reference_calculator') + end + end + end + + describe 'GET update_profile' do + describe 'with simple user' do + let(:unchanged_attributes) { user.attributes.slice('first_name', 'last_name', 'email') } + let(:changed_attributes) { attributes_for :user } + let(:invalid_attributes) { { email: 'e.mail.com' } } + + before { login user } + + it 'renders profile after update with invalid attributes' do + get_with_defaults :update_profile, params: { user: invalid_attributes } + expect(response).to have_http_status(:success) + expect(response).to render_template('home/profile') + expect(assigns(:current_user).errors.present?).to be true + end + + it 'redirects to profile after update with unchanged attributes' do + get_with_defaults :update_profile, params: { user: unchanged_attributes } + expect(response).to have_http_status(:redirect) + expect(response).to redirect_to(my_profile_path) + end + + it 'redirects to profile after update' do + patch :update_profile, params: { foodcoop: FoodsoftConfig[:default_scope], user: changed_attributes } + expect(response).to have_http_status(:redirect) + expect(response).to redirect_to(my_profile_path) + expect(flash[:notice]).to match(/#{I18n.t('home.changes_saved')}/) + expect(user.reload.attributes.slice(:first_name, :last_name, :email)).to eq(changed_attributes.slice('first_name', 'last_name', 'email')) + end + end + + describe 'with ordergroup user' do + let(:og_user) { create :user, :ordergroup } + let(:unchanged_attributes) { og_user.attributes.slice('first_name', 'last_name', 'email') } + let(:changed_attributes) { unchanged_attributes.merge({ ordergroup: { contact_address: 'new Adress 7' } }) } + + before { login og_user } + + it 'redirects to home after update' do + get_with_defaults :update_profile, params: { user: changed_attributes } + expect(response).to have_http_status(:redirect) + expect(response).to redirect_to(my_profile_path) + expect(og_user.reload.ordergroup.contact_address).to eq('new Adress 7') + end + end + end + + describe 'GET ordergroup' do + describe 'with simple user' do + before { login user } + + it 'redirects to home' do + get_with_defaults :ordergroup + expect(response).to have_http_status(:redirect) + expect(response).to redirect_to(root_path) + end + end + + describe 'with ordergroup user' do + let(:og_user) { create :user, :ordergroup } + + before { login og_user } + + it 'renders ordergroup' do + get_with_defaults :ordergroup + expect(response).to have_http_status(:success) + expect(response).to render_template('home/ordergroup') + end + + describe 'assigns sortings' do + let(:fin_trans1) { create :financial_transaction, user: og_user, ordergroup: og_user.ordergroup, note: 'A', amount: 200, created_on: Time.now } + let(:fin_trans2) { create :financial_transaction, user: og_user, ordergroup: og_user.ordergroup, note: 'B', amount: 100, created_on: Time.now + 2.minutes } + let(:fin_trans3) { create :financial_transaction, user: og_user, ordergroup: og_user.ordergroup, note: 'C', amount: 50, created_on: Time.now + 1.minute } + + before do + fin_trans1 + fin_trans2 + fin_trans3 + end + + it 'by criteria' do + sortings = [ + ['date', [fin_trans1, fin_trans3, fin_trans2]], + ['note', [fin_trans1, fin_trans2, fin_trans3]], + ['amount', [fin_trans3, fin_trans2, fin_trans1]], + ['date_reverse', [fin_trans2, fin_trans3, fin_trans1]], + ['note_reverse', [fin_trans3, fin_trans2, fin_trans1]], + ['amount_reverse', [fin_trans1, fin_trans2, fin_trans3]] + ] + sortings.each do |sorting| + get_with_defaults :ordergroup, params: { sort: sorting[0] } + expect(response).to have_http_status(:success) + expect(assigns(:financial_transactions).to_a).to eq(sorting[1]) + end + end + end + end + end + + describe 'GET cancel_membership' do + describe 'with simple user without group' do + before { login user } + + it 'fails' do + expect do + get_with_defaults :cancel_membership + end.to raise_error(ActiveRecord::RecordNotFound) + expect do + get_with_defaults :cancel_membership, params: { membership_id: 424242 } + end.to raise_error(ActiveRecord::RecordNotFound) + end + end + + describe 'with ordergroup user' do + let(:fin_user) { create :user, :role_finance } + + before { login fin_user } + + it 'removes user from group' do + membership = fin_user.memberships.first + get_with_defaults :cancel_membership, params: { group_id: fin_user.groups.first.id } + expect(response).to have_http_status(:redirect) + expect(response).to redirect_to(my_profile_path) + expect(flash[:notice]).to match(/#{I18n.t('home.ordergroup_cancelled', group: membership.group.name)}/) + end + + it 'removes user membership' do + membership = fin_user.memberships.first + get_with_defaults :cancel_membership, params: { membership_id: membership.id } + expect(response).to have_http_status(:redirect) + expect(response).to redirect_to(my_profile_path) + expect(flash[:notice]).to match(/#{I18n.t('home.ordergroup_cancelled', group: membership.group.name)}/) + end + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 88dea423..41894406 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -21,6 +21,10 @@ Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f } RSpec.configure do |config| # We use capybara with webkit, and need database_cleaner + config.before(:suite) do + DatabaseCleaner.clean_with(:truncation) + end + config.before(:each) do DatabaseCleaner.strategy = (RSpec.current_example.metadata[:js] ? :truncation : :transaction) DatabaseCleaner.start @@ -51,8 +55,8 @@ RSpec.configure do |config| # --seed 1234 config.order = "random" + config.include SpecTestHelper, type: :controller config.include SessionHelper, type: :feature - # Automatically determine spec from directory structure, see: # https://www.relishapp.com/rspec/rspec-rails/v/3-0/docs/directory-structure config.infer_spec_type_from_file_location! diff --git a/spec/support/spec_test_helper.rb b/spec/support/spec_test_helper.rb new file mode 100644 index 00000000..f3737c15 --- /dev/null +++ b/spec/support/spec_test_helper.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module SpecTestHelper + def login(user) + user = User.find_by_nick(user.nick) + session[:user_id] = user.id + session[:scope] = FoodsoftConfig[:default_scope] # Save scope in session to not allow switching between foodcoops with one account + session[:locale] = user.locale + end + + def current_user + User.find(session[:user_id]) + end + + def get_with_defaults(action, params: {}, xhr: false, format: nil) + params['foodcoop'] = FoodsoftConfig[:default_scope] + get action, params: params, xhr: xhr, format: format + end +end + +RSpec.configure do |config| + config.include SpecTestHelper, type: :controller +end From d7591d46b9a3c8f512969baa33010bf482693cb3 Mon Sep 17 00:00:00 2001 From: Philipp Rothmann Date: Mon, 28 Nov 2022 11:30:38 +0100 Subject: [PATCH 003/105] Add controller tests Co-authored-by: viehlieb Co-authored-by: Tobias Kneuker seperate expects refactor login user calls add more articles to test sorting with fix: fix test for rails upgrade --- .../application_controller_spec.rb | 12 + spec/controllers/articles_controller_spec.rb | 348 ++++++++++++++++++ .../controllers/concerns/auth_concern_spec.rb | 212 +++++++++++ .../finance/balancing_controller_spec.rb | 211 +++++++++++ .../finance/base_controller_spec.rb | 30 ++ spec/controllers/home_controller_spec.rb | 22 +- spec/controllers/login_controller_spec.rb | 67 ++++ spec/factories/invite.rb | 15 + spec/factories/order_article.rb | 8 + spec/fixtures/files/upload_test.csv | 3 + spec/fixtures/upload_test.csv | 3 + spec/models/supplier_spec.rb | 8 +- spec/support/spec_test_helper.rb | 9 +- 13 files changed, 932 insertions(+), 16 deletions(-) create mode 100644 spec/controllers/application_controller_spec.rb create mode 100644 spec/controllers/articles_controller_spec.rb create mode 100644 spec/controllers/concerns/auth_concern_spec.rb create mode 100644 spec/controllers/finance/balancing_controller_spec.rb create mode 100644 spec/controllers/finance/base_controller_spec.rb create mode 100644 spec/controllers/login_controller_spec.rb create mode 100644 spec/factories/invite.rb create mode 100644 spec/factories/order_article.rb create mode 100644 spec/fixtures/files/upload_test.csv create mode 100644 spec/fixtures/upload_test.csv diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb new file mode 100644 index 00000000..35db7574 --- /dev/null +++ b/spec/controllers/application_controller_spec.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe ApplicationController, type: :controller do + describe 'current' do + it 'returns current ApplicationController' do + described_class.new.send(:store_controller) + expect(described_class.current).to be_instance_of described_class + end + end +end diff --git a/spec/controllers/articles_controller_spec.rb b/spec/controllers/articles_controller_spec.rb new file mode 100644 index 00000000..dae89c70 --- /dev/null +++ b/spec/controllers/articles_controller_spec.rb @@ -0,0 +1,348 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe ArticlesController, type: :controller do + let(:user) { create :user, :role_article_meta } + let(:article_category_a) { create :article_category, name: "AAAA" } + let(:article_category_b) { create :article_category, name: "BBBB" } + let(:article_category_c) { create :article_category, name: "CCCC" } + let(:supplier) { create :supplier} + let(:article_a) { create :article, name: 'AAAA', note: "ZZZZ", unit: '750 g', article_category: article_category_b, availability: false, supplier_id: supplier.id } + let(:article_b) { create :article, name: 'BBBB', note: "XXXX", unit: '500 g', article_category: article_category_a, availability: true, supplier_id: supplier.id } + let(:article_c) { create :article, name: 'CCCC', note: "YYYY", unit: '250 g', article_category: article_category_c, availability: true, supplier_id: supplier.id } + let(:article_no_supplier) { create :article, name: 'no_supplier', note: "no_supplier", unit: '100 g', article_category: article_category_b, availability: true } + + let(:order) { create :order } + let(:order2) { create :order } + + def get_with_supplier(action, params: {}, xhr: false, format: nil) + params['supplier_id'] = supplier.id + get_with_defaults(action, params: params, xhr: xhr, format: format) + end + + def post_with_supplier(action, params: {}, xhr: false, format: nil) + params['supplier_id'] = supplier.id + post_with_defaults(action, params: params, xhr: xhr, format: format) + end + + before { login user } + + describe 'GET index' do + before do + supplier + article_a + article_b + article_c + supplier.reload + end + it 'assigns sorting on articles' do + sortings = [ + ['name', [article_a, article_b, article_c]], + ['name_reverse', [article_c, article_b, article_a]], + ['note', [article_b, article_c, article_a]], + ['note_reverse', [article_a, article_c, article_b]], + ['unit', [article_c, article_b, article_a]], + ['unit_reverse', [article_a, article_b, article_c]], + ['article_category', [article_b, article_a, article_c]], + ['article_category_reverse', [article_c, article_a, article_b]], + ['availability', [article_a, article_b, article_c]], + ['availability_reverse', [article_b, article_c, article_a]] + ] + sortings.each do |sorting| + get_with_supplier :index, params: { sort: sorting[0] } + expect(response).to have_http_status(:success) + expect(assigns(:articles).to_a).to eq(sorting[1]) + end + end + + it 'triggers an article csv' do + get_with_supplier :index, format: :csv + expect(response.header['Content-Type']).to include('text/csv') + expect(response.body).to include(article_a.unit, article_b.unit) + end + end + + describe 'new' do + it 'renders form for a new article' do + get_with_supplier :new, xhr: true + expect(response).to have_http_status(:success) + end + end + + describe 'copy' do + it 'renders form with copy of an article' do + get_with_supplier :copy, params: { article_id: article_a.id }, xhr: true + expect(assigns(:article).attributes).to eq(article_a.dup.attributes) + expect(response).to have_http_status(:success) + end + end + + describe '#create' do + it 'creates a new article' do + valid_attributes = article_a.attributes.except('id') + valid_attributes['name'] = 'ABAB' + get_with_supplier :create, params: { article: valid_attributes }, xhr: true + expect(response).to have_http_status(:success) + end + + it 'fails to create a new article and renders #new' do + get_with_supplier :create, params: { article: { id: nil } }, xhr: true + expect(response).to have_http_status(:success) + expect(response).to render_template('articles/new') + end + end + + describe 'edit' do + it 'opens form to edit article attributes' do + get_with_supplier :edit, params: { id: article_a.id }, xhr: true + expect(response).to have_http_status(:success) + expect(response).to render_template('articles/new') + end + end + + describe '#edit all' do + it 'renders edit_all' do + get_with_supplier :edit_all, xhr: true + expect(response).to have_http_status(:success) + expect(response).to render_template('articles/edit_all') + end + end + + describe '#update' do + it 'updates article attributes' do + get_with_supplier :update, params: { id: article_a.id, article: { unit: '300 g' } }, xhr: true + expect(assigns(:article).unit).to eq('300 g') + expect(response).to have_http_status(:success) + end + + it 'updates article with empty name attribute' do + get_with_supplier :update, params: { id: article_a.id, article: { name: nil } }, xhr: true + expect(response).to render_template('articles/new') + end + end + + describe '#update_all' do + it 'updates all articles' do + get_with_supplier :update_all, params: { articles: { "#{article_a.id}": attributes_for(:article), "#{article_b.id}": attributes_for(:article) } } + expect(response).to have_http_status(:redirect) + end + + it 'fails on updating all articles' do + get_with_supplier :update_all, params: { articles: { "#{article_a.id}": attributes_for(:article, name: 'ab') } } + expect(response).to have_http_status(:success) + expect(response).to render_template('articles/edit_all') + end + end + + describe '#update_selected' do + let(:order_article) { create :order_article, order: order, article: article_no_supplier } + + before do + order_article + end + + it 'updates selected articles' do + get_with_supplier :update_selected, params: { selected_articles: [article_a.id, article_b.id] } + expect(response).to have_http_status(:redirect) + end + + it 'destroys selected articles' do + get_with_supplier :update_selected, params: { selected_articles: [article_a.id, article_b.id], selected_action: 'destroy' } + article_a.reload + article_b.reload + expect(article_a).to be_deleted + expect(article_b).to be_deleted + expect(response).to have_http_status(:redirect) + end + + it 'sets availability false on selected articles' do + get_with_supplier :update_selected, params: { selected_articles: [article_a.id, article_b.id], selected_action: 'setNotAvailable' } + article_a.reload + article_b.reload + expect(article_a).not_to be_availability + expect(article_b).not_to be_availability + expect(response).to have_http_status(:redirect) + end + + it 'sets availability true on selected articles' do + get_with_supplier :update_selected, params: { selected_articles: [article_a.id, article_b.id], selected_action: 'setAvailable' } + article_a.reload + article_b.reload + expect(article_a).to be_availability + expect(article_b).to be_availability + expect(response).to have_http_status(:redirect) + end + + it 'fails deletion if one article is in open order' do + get_with_supplier :update_selected, params: { selected_articles: [article_a.id, article_no_supplier.id], selected_action: 'destroy' } + article_a.reload + article_no_supplier.reload + expect(article_a).not_to be_deleted + expect(article_no_supplier).not_to be_deleted + expect(response).to have_http_status(:redirect) + end + end + + describe '#parse_upload' do + let(:file) { Rack::Test::UploadedFile.new(Rails.root.join('spec/fixtures/files/upload_test.csv'), original_filename: 'upload_test.csv') } + + it 'updates particles from spreadsheet' do + post_with_supplier :parse_upload, params: { articles: { file: file, outlist_absent: '1', convert_units: '1' } } + expect(response).to have_http_status(:success) + end + + it 'missing file not updates particles from spreadsheet' do + post_with_supplier :parse_upload, params: { articles: { file: nil, outlist_absent: '1', convert_units: '1' } } + expect(response).to have_http_status(:redirect) + expect(flash[:alert]).to match(I18n.t('errors.general_msg', msg: "undefined method `original_filename' for \"\":String").to_s) + end + end + + describe '#sync' do + # TODO: double render error in controller + it 'throws double render error' do + expect do + post :sync, params: { foodcoop: FoodsoftConfig[:default_scope], supplier_id: supplier.id } + end.to raise_error(AbstractController::DoubleRenderError) + end + + xit 'updates particles from spreadsheet' do + post :sync, params: { foodcoop: FoodsoftConfig[:default_scope], supplier_id: supplier.id, articles: { '#{article_a.id}': attributes_for(:article), '#{article_b.id}': attributes_for(:article) } } + expect(response).to have_http_status(:redirect) + end + end + + describe '#destroy' do + let(:order_article) { create :order_article, order: order, article: article_no_supplier } + + before do + order_article + end + + it 'does not delete article if order open' do + get_with_supplier :destroy, params: { id: article_no_supplier.id }, xhr: true + expect(assigns(:article)).not_to be_deleted + expect(response).to have_http_status(:success) + expect(response).to render_template('articles/destroy') + end + + it 'deletes article if order closed' do + get_with_supplier :destroy, params: { id: article_b.id }, xhr: true + expect(assigns(:article)).to be_deleted + expect(response).to have_http_status(:success) + expect(response).to render_template('articles/destroy') + end + end + + describe '#update_synchronized' do + let(:order_article) { create :order_article, order: order, article: article_no_supplier } + + before do + order_article + article_a + article_b + article_no_supplier + end + + it 'deletes articles' do + # TODO: double render error in controller + get_with_supplier :update_synchronized, params: { outlisted_articles: { article_a.id => article_a, article_b.id => article_b } } + article_a.reload + article_b.reload + expect(article_a).to be_deleted + expect(article_b).to be_deleted + expect(response).to have_http_status(:redirect) + end + + it 'updates articles' do + get_with_supplier :update_synchronized, params: { articles: { article_a.id => { name: 'NewNameA' }, article_b.id => { name: 'NewNameB' } } } + expect(assigns(:updated_articles).first.name).to eq 'NewNameA' + expect(response).to have_http_status(:redirect) + end + + it 'does not update articles if article with same name exists' do + get_with_supplier :update_synchronized, params: { articles: { article_a.id => { unit: '2000 g' }, article_b.id => { name: 'AAAA' } } } + error_array = [assigns(:updated_articles).first.errors.first, assigns(:updated_articles).last.errors.first] + expect(error_array).to include([:name, 'name is already taken']) + expect(response).to have_http_status(:success) + end + + it 'does update articles if article with same name was deleted before' do + get_with_supplier :update_synchronized, params: { + outlisted_articles: { article_a.id => article_a }, + articles: { + article_a.id => { name: 'NewName' }, + article_b.id => { name: 'AAAA' } + } + } + error_array = [assigns(:updated_articles).first.errors.first, assigns(:updated_articles).last.errors.first] + expect(error_array).not_to be_any + expect(response).to have_http_status(:redirect) + end + + it 'does not delete articles in open order' do + get_with_supplier :update_synchronized, params: { outlisted_articles: { article_no_supplier.id => article_no_supplier } } + article_no_supplier.reload + expect(article_no_supplier).not_to be_deleted + expect(response).to have_http_status(:success) + end + + it 'assigns updated article_pairs on error' do + get_with_supplier :update_synchronized, params: { + articles: { article_a.id => { name: 'EEEE' } }, + outlisted_articles: { article_no_supplier.id => article_no_supplier } + } + expect(assigns(:updated_article_pairs).first).to eq([article_a, { name: 'EEEE' }]) + article_no_supplier.reload + expect(article_no_supplier).not_to be_deleted + expect(response).to have_http_status(:success) + end + + it 'updates articles in open order' do + get_with_supplier :update_synchronized, params: { articles: { article_no_supplier.id => { name: 'EEEE' } } } + article_no_supplier.reload + expect(article_no_supplier.name).to eq 'EEEE' + expect(response).to have_http_status(:redirect) + end + end + + describe '#shared' do + let(:shared_supplier) { create :shared_supplier, shared_articles: [shared_article] } + let(:shared_article) { create :shared_article, name: 'shared' } + let(:article_s) { create :article, name: 'SSSS', note: 'AAAA', unit: '250 g', article_category: article_category_a, availability: false } + + let(:supplier_with_shared) { create :supplier, shared_supplier: shared_supplier } + + it 'renders view with articles' do + get_with_defaults :shared, params: { supplier_id: supplier_with_shared.id, name_cont_all_joined: 'shared' }, xhr: true + expect(assigns(:supplier).shared_supplier.shared_articles).to be_any + expect(assigns(:articles)).to be_any + expect(response).to have_http_status(:success) + end + end + + describe '#import' do + let(:shared_supplier) { create :shared_supplier, shared_articles: [shared_article] } + let(:shared_article) { create :shared_article, name: 'shared' } + + before do + shared_article + article_category_a + end + + it 'fills form with article details' do + get_with_supplier :import, params: { article_category_id: article_category_b.id, direct: 'true', shared_article_id: shared_article.id }, xhr: true + expect(assigns(:article)).not_to be_nil + expect(response).to have_http_status(:success) + expect(response).to render_template(:create) + end + + it 'does redirect to :new if param :direct not set' do + get_with_supplier :import, params: { article_category_id: article_category_b.id, shared_article_id: shared_article.id }, xhr: true + expect(assigns(:article)).not_to be_nil + expect(response).to have_http_status(:success) + expect(response).to render_template(:new) + end + end +end diff --git a/spec/controllers/concerns/auth_concern_spec.rb b/spec/controllers/concerns/auth_concern_spec.rb new file mode 100644 index 00000000..10bf8ec7 --- /dev/null +++ b/spec/controllers/concerns/auth_concern_spec.rb @@ -0,0 +1,212 @@ +# frozen_string_literal: true + +require 'spec_helper' + +class DummyAuthController < ApplicationController; end + +describe 'Auth concern', type: :controller do + controller DummyAuthController do + # Defining a dummy action for an anynomous controller which inherits from the described class. + def authenticate_blank + authenticate + end + + def authenticate_unknown_group + authenticate('nooby') + end + + def authenticate_pickups + authenticate('pickups') + head :ok unless performed? + end + + def authenticate_finance_or_orders + authenticate('finance_or_orders') + head :ok unless performed? + end + + def try_authenticate_membership_or_admin + authenticate_membership_or_admin + end + + def try_authenticate_or_token + authenticate_or_token('xyz') + head :ok unless performed? + end + + def call_deny_access + deny_access + end + + def call_current_user + current_user + end + + def call_login_and_redirect_to_return_to + user = User.find(params[:user_id]) + login_and_redirect_to_return_to(user) + end + + def call_login + user = User.find(params[:user_id]) + login(user) + end + end + + # unit testing protected/private methods + describe 'protected/private methods' do + let(:user) { create :user } + let(:wrong_user) { create :user } + + describe '#current_user' do + before do + login user + routes.draw { get 'call_current_user' => 'dummy_auth#call_current_user' } + end + + describe 'with valid session' do + it 'returns current_user' do + get_with_defaults :call_current_user, params: { user_id: user.id }, format: JSON + expect(assigns(:current_user)).to eq user + end + end + + describe 'with invalid session' do + it 'not returns current_user' do + session[:user_id] = nil + get_with_defaults :call_current_user, params: { user_id: nil }, format: JSON + expect(assigns(:current_user)).to be_nil + end + end + end + + describe '#deny_access' do + it 'redirects to root_url' do + login user + routes.draw { get 'deny_access' => 'dummy_auth#call_deny_access' } + get_with_defaults :call_deny_access + expect(response).to redirect_to(root_url) + end + end + + describe '#login' do + before do + routes.draw { get 'call_login' => 'dummy_auth#call_login' } + end + + it 'sets user in session' do + login wrong_user + get_with_defaults :call_login, params: { user_id: user.id }, format: JSON + expect(session[:user_id]).to eq user.id + expect(session[:scope]).to eq FoodsoftConfig.scope + expect(session[:locale]).to eq user.locale + end + end + + describe '#login_and_redirect_to_return_to' do + it 'redirects to already set target' do + login user + session[:return_to] = my_profile_url + routes.draw { get 'call_login_and_redirect_to_return_to' => 'dummy_auth#call_login_and_redirect_to_return_to' } + get_with_defaults :call_login_and_redirect_to_return_to, params: { user_id: user.id } + expect(session[:return_to]).to be_nil + end + end + end + + describe 'authenticate' do + describe 'not logged in' do + it 'does not authenticate' do + routes.draw { get 'authenticate_blank' => 'dummy_auth#authenticate_blank' } + get_with_defaults :authenticate_blank + expect(response).to have_http_status(:redirect) + expect(response).to redirect_to(login_path) + expect(flash[:alert]).to match(I18n.t('application.controller.error_authn')) + end + end + + describe 'logged in' do + let(:user) { create :user } + let(:pickups_user) { create :user, :role_pickups } + let(:finance_user) { create :user, :role_finance } + let(:orders_user) { create :user, :role_orders } + + it 'does not authenticate with unknown group' do + login user + routes.draw { get 'authenticate_unknown_group' => 'dummy_auth#authenticate_unknown_group' } + get_with_defaults :authenticate_unknown_group + expect(response).to have_http_status(:redirect) + expect(response).to redirect_to(root_path) + expect(flash[:alert]).to match(I18n.t('application.controller.error_denied', sign_in: ActionController::Base.helpers.link_to(I18n.t('application.controller.error_denied_sign_in'), login_path))) + end + + it 'does not authenticate with pickups group' do + login pickups_user + routes.draw { get 'authenticate_pickups' => 'dummy_auth#authenticate_pickups' } + get_with_defaults :authenticate_pickups + expect(response).to have_http_status(:success) + end + + it 'does not authenticate with finance group' do + login finance_user + routes.draw { get 'authenticate_finance_or_orders' => 'dummy_auth#authenticate_finance_or_orders' } + get_with_defaults :authenticate_finance_or_orders + expect(response).to have_http_status(:success) + end + + it 'does not authenticate with orders group' do + login orders_user + routes.draw { get 'authenticate_finance_or_orders' => 'dummy_auth#authenticate_finance_or_orders' } + get_with_defaults :authenticate_finance_or_orders + expect(response).to have_http_status(:success) + end + end + end + + describe 'authenticate_membership_or_admin' do + describe 'logged in' do + let(:pickups_user) { create :user, :role_pickups } + let(:workgroup) { create :workgroup } + + it 'redirects with not permitted group' do + group_id = workgroup.id + login pickups_user + routes.draw { get 'try_authenticate_membership_or_admin' => 'dummy_auth#try_authenticate_membership_or_admin' } + get_with_defaults :try_authenticate_membership_or_admin, params: { id: group_id } + expect(response).to have_http_status(:redirect) + expect(response).to redirect_to(root_path) + expect(flash[:alert]).to match(I18n.t('application.controller.error_members_only')) + end + end + end + + describe 'authenticate_or_token' do + describe 'logged in' do + let(:token_verifier) { TokenVerifier.new('xyz') } + let(:token_msg) { token_verifier.generate } + let(:user) { create :user } + + before { login user } + + it 'authenticates token' do + routes.draw { get 'try_authenticate_or_token' => 'dummy_auth#try_authenticate_or_token' } + get_with_defaults :try_authenticate_or_token, params: { token: token_msg } + expect(response).not_to have_http_status(:redirect) + end + + it 'redirects on faulty token' do + routes.draw { get 'try_authenticate_or_token' => 'dummy_auth#try_authenticate_or_token' } + get_with_defaults :try_authenticate_or_token, params: { token: 'abc' } + expect(response).to have_http_status(:redirect) + expect(response).to redirect_to(root_path) + expect(flash[:alert]).to match(I18n.t('application.controller.error_token')) + end + + it 'authenticates current user on empty token' do + routes.draw { get 'try_authenticate_or_token' => 'dummy_auth#try_authenticate_or_token' } + get_with_defaults :try_authenticate_or_token + expect(response).to have_http_status(:success) + end + end + end +end diff --git a/spec/controllers/finance/balancing_controller_spec.rb b/spec/controllers/finance/balancing_controller_spec.rb new file mode 100644 index 00000000..d62b9974 --- /dev/null +++ b/spec/controllers/finance/balancing_controller_spec.rb @@ -0,0 +1,211 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Finance::BalancingController, type: :controller do + let(:user) { create :user, :role_finance, :role_orders, groups: [create(:ordergroup)] } + + before { login user } + + describe 'GET index' do + let(:order) { create :order } + + it 'renders index page' do + get_with_defaults :index + expect(response).to have_http_status(:success) + expect(response).to render_template('finance/balancing/index') + end + end + + describe 'new balancing' do + let(:supplier) { create :supplier } + let(:article1) { create :article, name: 'AAAA', supplier: supplier, unit_quantity: 1 } + let(:article2) { create :article, name: 'AAAB', supplier: supplier, unit_quantity: 1 } + + let(:order) { create :order, supplier: supplier, article_ids: [article1.id, article2.id] } + + let(:go1) { create :group_order, order: order } + let(:go2) { create :group_order, order: order } + let(:oa1) { order.order_articles.find_by_article_id(article1.id) } + let(:oa2) { order.order_articles.find_by_article_id(article2.id) } + let(:oa3) { order2.order_articles.find_by_article_id(article2.id) } + let(:goa1) { create :group_order_article, group_order: go1, order_article: oa1 } + let(:goa2) { create :group_order_article, group_order: go1, order_article: oa2 } + + before do + goa1.update_quantities(3, 0) + goa2.update_quantities(1, 0) + oa1.update_results! + oa2.update_results! + end + + it 'renders new order page' do + get_with_defaults :new, params: { order_id: order.id } + expect(response).to have_http_status(:success) + expect(response).to render_template('finance/balancing/new') + end + + it 'assigns sorting on articles' do + sortings = [ + ['name', [oa1, oa2]], + ['name_reverse', [oa2, oa1]], + ['order_number', [oa1, oa2]], + ['order_number_reverse', [oa1, oa2]] # just one order + ] + sortings.each do |sorting| + get_with_defaults :new, params: { order_id: order.id, sort: sorting[0] } + expect(response).to have_http_status(:success) + expect(assigns(:articles).to_a).to eq(sorting[1]) + end + end + end + + describe 'update summary' do + let(:order) { create(:order) } + + it 'shows the summary view' do + get_with_defaults :update_summary, params: { id: order.id }, xhr: true + expect(response).to have_http_status(:success) + expect(response).to render_template('finance/balancing/update_summary') + end + end + + describe 'new_on_order' do + let(:order) { create(:order) } + let(:order_article) { order.order_articles.first } + + it 'calls article update' do + get_with_defaults :new_on_order_article_update, params: { id: order.id, order_article_id: order_article.id }, xhr: true + expect(response).not_to render_template(layout: 'application') + expect(response).to render_template('finance/balancing/new_on_order_article_update') + end + + it 'calls article create' do + get_with_defaults :new_on_order_article_create, params: { id: order.id, order_article_id: order_article.id }, xhr: true + expect(response).not_to render_template(layout: 'application') + expect(response).to render_template('finance/balancing/new_on_order_article_create') + end + end + + describe 'edit_note' do + let(:order) { create(:order) } + + it 'updates order note' do + get_with_defaults :edit_note, params: { id: order.id, order: { note: 'Hello' } }, xhr: true + expect(response).to have_http_status(:success) + expect(response).to render_template('finance/balancing/edit_note') + end + end + + describe 'update_note' do + let(:order) { create(:order) } + + it 'updates order note' do + get_with_defaults :update_note, params: { id: order.id, order: { note: 'Hello' } }, xhr: true + expect(response).to have_http_status(:success) + end + + it 'redirects to edit note on failed update' do + get_with_defaults :update_note, params: { id: order.id, order: { article_ids: nil } }, xhr: true + expect(response).to have_http_status(:success) + expect(response).to render_template('finance/balancing/edit_note') + end + end + + describe 'transport' do + let(:order) { create(:order) } + + it 'calls the edit transport view' do + get_with_defaults :edit_transport, params: { id: order.id }, xhr: true + expect(response).to have_http_status(:success) + expect(response).to render_template('finance/balancing/edit_transport') + end + + it 'does redirect if order valid' do + get_with_defaults :update_transport, params: { id: order.id, order: { ends: Time.now } }, xhr: true + expect(response).to have_http_status(:redirect) + expect(assigns(:order).errors.count).to eq(0) + expect(response).to redirect_to(new_finance_order_path(order_id: order.id)) + end + + it 'does redirect if order invalid' do + get_with_defaults :update_transport, params: { id: order.id, order: { starts: Time.now + 2, ends: Time.now } }, xhr: true + expect(assigns(:order).errors.count).to eq(1) + expect(response).to have_http_status(:redirect) + expect(response).to redirect_to(new_finance_order_path(order_id: order.id)) + end + end + + describe 'confirm' do + let(:order) { create(:order) } + + it 'renders the confirm template' do + get_with_defaults :confirm, params: { id: order.id }, xhr: true + expect(response).to have_http_status(:success) + expect(response).to render_template('finance/balancing/confirm') + end + end + + describe 'close and update account balances' do + let(:order) { create(:order) } + let(:order1) { create(:order, ends: Time.now) } + let(:fft) { create(:financial_transaction_type) } + + it 'does not close order if ends not set' do + get_with_defaults :close, params: { id: order.id, type: fft.id } + expect(assigns(:order)).not_to be_closed + expect(response).to have_http_status(:redirect) + expect(response).to redirect_to(new_finance_order_url(order_id: order.id)) + end + + it 'closes order' do + get_with_defaults :close, params: { id: order1.id, type: fft.id } + expect(assigns(:order)).to be_closed + expect(response).to have_http_status(:redirect) + expect(response).to redirect_to(finance_order_index_url) + end + end + + describe 'close direct' do + let(:order) { create(:order) } + + it 'does not close order if already closed' do + order.close_direct!(user) + get_with_defaults :close_direct, params: { id: order.id } + expect(assigns(:order)).to be_closed + end + + it 'closes order directly' do + get_with_defaults :close_direct, params: { id: order.id } + expect(assigns(:order)).to be_closed + end + end + + describe 'close all direct' do + let(:invoice) { create(:invoice) } + let(:invoice1) { create(:invoice) } + let(:order) { create(:order, state: 'finished', ends: Time.now + 2.hours, invoice: invoice) } + let(:order1) { create(:order, state: 'finished', ends: Time.now + 2.hours) } + + before do + order + order1 + end + + it 'does close orders' do + get_with_defaults :close_all_direct_with_invoice + order.reload + expect(order).to be_closed + expect(response).to have_http_status(:redirect) + expect(response).to redirect_to(finance_order_index_url) + end + + it 'does not close orders when invoice not set' do + get_with_defaults :close_all_direct_with_invoice + order1.reload + expect(order1).not_to be_closed + expect(response).to have_http_status(:redirect) + expect(response).to redirect_to(finance_order_index_url) + end + end +end diff --git a/spec/controllers/finance/base_controller_spec.rb b/spec/controllers/finance/base_controller_spec.rb new file mode 100644 index 00000000..388f3a17 --- /dev/null +++ b/spec/controllers/finance/base_controller_spec.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Finance::BaseController, type: :controller do + let(:user) { create :user, :role_finance, :role_orders, :ordergroup } + + before { login user } + + describe 'GET index' do + let(:fin_trans) { create_list :financial_transaction, 3, user: user, ordergroup: user.ordergroup } + let(:orders) { create_list :order, 2, state: 'finished' } + let(:invoices) { create_list :invoice, 4 } + + before do + fin_trans + orders + invoices + end + + it 'renders index page' do + get_with_defaults :index + expect(response).to have_http_status(:success) + expect(response).to render_template('finance/index') + expect(assigns(:financial_transactions).size).to eq(fin_trans.size) + expect(assigns(:orders).size).to eq(orders.size) + expect(assigns(:unpaid_invoices).size).to eq(invoices.size) + end + end +end diff --git a/spec/controllers/home_controller_spec.rb b/spec/controllers/home_controller_spec.rb index f3616cd4..be106282 100644 --- a/spec/controllers/home_controller_spec.rb +++ b/spec/controllers/home_controller_spec.rb @@ -131,24 +131,22 @@ describe HomeController, type: :controller do end describe 'assigns sortings' do - let(:fin_trans1) { create :financial_transaction, user: og_user, ordergroup: og_user.ordergroup, note: 'A', amount: 200, created_on: Time.now } - let(:fin_trans2) { create :financial_transaction, user: og_user, ordergroup: og_user.ordergroup, note: 'B', amount: 100, created_on: Time.now + 2.minutes } - let(:fin_trans3) { create :financial_transaction, user: og_user, ordergroup: og_user.ordergroup, note: 'C', amount: 50, created_on: Time.now + 1.minute } + let(:fin_trans1) { create :financial_transaction, user: og_user, ordergroup: og_user.ordergroup, note: 'A', amount: 100 } + let(:fin_trans2) { create :financial_transaction, user: og_user, ordergroup: og_user.ordergroup, note: 'B', amount: 200, created_on: Time.now + 1.minute } before do fin_trans1 fin_trans2 - fin_trans3 end it 'by criteria' do sortings = [ - ['date', [fin_trans1, fin_trans3, fin_trans2]], - ['note', [fin_trans1, fin_trans2, fin_trans3]], - ['amount', [fin_trans3, fin_trans2, fin_trans1]], - ['date_reverse', [fin_trans2, fin_trans3, fin_trans1]], - ['note_reverse', [fin_trans3, fin_trans2, fin_trans1]], - ['amount_reverse', [fin_trans1, fin_trans2, fin_trans3]] + ['date', [fin_trans1, fin_trans2]], + ['note', [fin_trans1, fin_trans2]], + ['amount', [fin_trans1, fin_trans2]], + ['date_reverse', [fin_trans2, fin_trans1]], + ['note_reverse', [fin_trans2, fin_trans1]], + ['amount_reverse', [fin_trans2, fin_trans1]] ] sortings.each do |sorting| get_with_defaults :ordergroup, params: { sort: sorting[0] } @@ -184,7 +182,7 @@ describe HomeController, type: :controller do get_with_defaults :cancel_membership, params: { group_id: fin_user.groups.first.id } expect(response).to have_http_status(:redirect) expect(response).to redirect_to(my_profile_path) - expect(flash[:notice]).to match(/#{I18n.t('home.ordergroup_cancelled', group: membership.group.name)}/) + expect(flash[:notice]).to match(/#{I18n.t('home.ordergroup_cancelled', :group => membership.group.name)}/) end it 'removes user membership' do @@ -192,7 +190,7 @@ describe HomeController, type: :controller do get_with_defaults :cancel_membership, params: { membership_id: membership.id } expect(response).to have_http_status(:redirect) expect(response).to redirect_to(my_profile_path) - expect(flash[:notice]).to match(/#{I18n.t('home.ordergroup_cancelled', group: membership.group.name)}/) + expect(flash[:notice]).to match(/#{I18n.t('home.ordergroup_cancelled', :group => membership.group.name)}/) end end end diff --git a/spec/controllers/login_controller_spec.rb b/spec/controllers/login_controller_spec.rb new file mode 100644 index 00000000..c824e429 --- /dev/null +++ b/spec/controllers/login_controller_spec.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe LoginController, type: :controller do + let(:invite) { create :invite } + + describe 'GET accept_invitation' do + let(:expired_invite) { create :expired_invite } + + describe 'with valid token' do + it 'accepts invitation' do + get_with_defaults :accept_invitation, params: { token: invite.token } + expect(response).to have_http_status(:success) + expect(response).to render_template('login/accept_invitation') + end + end + + describe 'with invalid token' do + it 'redirects to login' do + get_with_defaults :accept_invitation, params: { token: invite.token + 'XX' } + expect(response).to have_http_status(:redirect) + expect(response).to redirect_to(login_url) + expect(flash[:alert]).to match(I18n.t('login.controller.error_invite_invalid')) + end + end + + describe 'with timed out token' do + it 'redirects to login' do + get_with_defaults :accept_invitation, params: { token: expired_invite.token } + expect(response).to have_http_status(:redirect) + expect(response).to redirect_to(login_url) + expect(flash[:alert]).to match(I18n.t('login.controller.error_invite_invalid')) + end + end + + describe 'without group' do + it 'redirects to login' do + invite.group.destroy + get_with_defaults :accept_invitation, params: { token: invite.token } + expect(response).to have_http_status(:redirect) + expect(response).to redirect_to(login_url) + expect(flash[:alert]).to match(I18n.t('login.controller.error_group_invalid')) + end + end + end + + describe 'POST accept_invitation' do + describe 'with invalid parameters' do + it 'renders accept_invitation view' do + post_with_defaults :accept_invitation, params: { token: invite.token, user: invite.user.slice('first_name') } + expect(response).to have_http_status(:success) + expect(response).to render_template('login/accept_invitation') + expect(assigns(:user).errors.present?).to be true + end + end + + describe 'with valid parameters' do + it 'redirects to login' do + post_with_defaults :accept_invitation, params: { token: invite.token, user: invite.user.slice('first_name', 'password') } + expect(response).to have_http_status(:redirect) + expect(response).to redirect_to(login_url) + expect(flash[:notice]).to match(I18n.t('login.controller.accept_invitation.notice')) + end + end + end +end diff --git a/spec/factories/invite.rb b/spec/factories/invite.rb new file mode 100644 index 00000000..51d48840 --- /dev/null +++ b/spec/factories/invite.rb @@ -0,0 +1,15 @@ +require 'factory_bot' + +FactoryBot.define do + factory :invite do + user { create :user } + group { create :group } + email { Faker::Internet.email } + + factory :expired_invite do + after :create do |invite| + invite.update_column(:expires_at, Time.now.yesterday) + end + end + end +end diff --git a/spec/factories/order_article.rb b/spec/factories/order_article.rb new file mode 100644 index 00000000..99ca8701 --- /dev/null +++ b/spec/factories/order_article.rb @@ -0,0 +1,8 @@ +require 'factory_bot' + +FactoryBot.define do + factory :order_article do + order { create :order } + article { create :article } + end +end diff --git a/spec/fixtures/files/upload_test.csv b/spec/fixtures/files/upload_test.csv new file mode 100644 index 00000000..ac2f59b0 --- /dev/null +++ b/spec/fixtures/files/upload_test.csv @@ -0,0 +1,3 @@ +avail.;Order number;Name;Note;Manufacturer;Origin;Unit;Price (net);VAT;Deposit;Unit quantity;"";"";Category +"";;AAAA;AAAA;;;500 g;25.55;6.0;0.0;1;"";"";AAAA +"";;BBBB;BBBB;;;250 g;12.11;6.0;0.0;2;"";"";BBBB diff --git a/spec/fixtures/upload_test.csv b/spec/fixtures/upload_test.csv new file mode 100644 index 00000000..ac2f59b0 --- /dev/null +++ b/spec/fixtures/upload_test.csv @@ -0,0 +1,3 @@ +avail.;Order number;Name;Note;Manufacturer;Origin;Unit;Price (net);VAT;Deposit;Unit quantity;"";"";Category +"";;AAAA;AAAA;;;500 g;25.55;6.0;0.0;1;"";"";AAAA +"";;BBBB;BBBB;;;250 g;12.11;6.0;0.0;2;"";"";BBBB diff --git a/spec/models/supplier_spec.rb b/spec/models/supplier_spec.rb index 70ba6def..6bcc6e7b 100644 --- a/spec/models/supplier_spec.rb +++ b/spec/models/supplier_spec.rb @@ -19,9 +19,13 @@ describe Supplier do end it 'return correct tolerance' do - supplier = create :supplier, articles: create_list(:article, 1, unit_quantity: 1) + supplier = create :supplier + articles = create_list(:article, 1, unit_quantity: 1, supplier_id: supplier.id) + supplier.reload expect(supplier.has_tolerance?).to be false - supplier2 = create :supplier, articles: create_list(:article, 1, unit_quantity: 2) + supplier2 = create :supplier + articles = create_list(:article, 1, unit_quantity: 2, supplier_id: supplier2.id) + supplier.reload expect(supplier2.has_tolerance?).to be true end diff --git a/spec/support/spec_test_helper.rb b/spec/support/spec_test_helper.rb index f3737c15..58a1c0ef 100644 --- a/spec/support/spec_test_helper.rb +++ b/spec/support/spec_test_helper.rb @@ -2,7 +2,7 @@ module SpecTestHelper def login(user) - user = User.find_by_nick(user.nick) + user = User.where(:nick => user.nick).first if user.is_a?(Symbol) session[:user_id] = user.id session[:scope] = FoodsoftConfig[:default_scope] # Save scope in session to not allow switching between foodcoops with one account session[:locale] = user.locale @@ -16,8 +16,13 @@ module SpecTestHelper params['foodcoop'] = FoodsoftConfig[:default_scope] get action, params: params, xhr: xhr, format: format end + + def post_with_defaults(action, params: {}, xhr: false, format: nil) + params['foodcoop'] = FoodsoftConfig[:default_scope] + post action, params: params, xhr: xhr, format: format + end end RSpec.configure do |config| - config.include SpecTestHelper, type: :controller + config.include SpecTestHelper, :type => :controller end From fb8ccfea4a16c118deadfb8bdc0b22600b1ec859 Mon Sep 17 00:00:00 2001 From: viehlieb Date: Fri, 6 Jan 2023 16:12:41 +0100 Subject: [PATCH 004/105] rails up to 7.0and ruby to 2.7.2 mv lib to app/lib due to upgrade removing concerns from autoload path resolve zeitwerk issues make foodsoft run for dev on rails 7 and ruby 2.7 fix mail file permission bug fix database_config fix articles controller test ActiveModell::Error bump Gemfile.lock --- .ruby-version | 2 +- Dockerfile-dev | 3 +- Gemfile | 16 +- Gemfile.lock | 326 ++++++++++-------- {lib => app/lib}/api/errors.rb | 0 {lib => app/lib}/apple_bar.rb | 16 +- {lib => app/lib}/articles_csv.rb | 6 +- {lib => app/lib}/bank_account_connector.rb | 30 +- .../lib}/bank_account_connector_external.rb | 0 .../lib}/bank_account_information_importer.rb | 0 .../lib}/bank_transaction_reference.rb | 4 +- {lib => app/lib}/bank_transactions_csv.rb | 2 +- .../lib}/date_time_attribute_validate.rb | 24 +- .../lib}/financial_transactions_csv.rb | 2 +- .../lib}/foodsoft/expansion_variables.rb | 4 +- {lib => app/lib}/foodsoft_config.rb | 7 +- {lib => app/lib}/foodsoft_date_util.rb | 11 +- {lib => app/lib}/foodsoft_file.rb | 0 {lib => app/lib}/foodsoft_mail_receiver.rb | 22 +- {lib => app/lib}/invoices_csv.rb | 4 +- {lib => app/lib}/order_csv.rb | 2 +- {lib => app/lib}/order_pdf.rb | 2 +- {lib => app/lib}/order_txt.rb | 6 +- {lib => app/lib}/ordergroups_csv.rb | 8 +- {lib => app/lib}/render_csv.rb | 2 +- {lib => app/lib}/render_pdf.rb | 9 +- {lib => app/lib}/spreadsheet_file.rb | 0 .../templates/haml/scaffold/_form.html.haml | 0 {lib => app/lib}/token_verifier.rb | 4 - {lib => app/lib}/users_csv.rb | 2 +- bin/setup | 29 +- config/application.rb | 11 +- config/environments/production.rb | 16 +- config/environments/test.rb | 40 ++- config/initializers/assets.rb | 2 - .../initializers/content_security_policy.rb | 42 +-- config/initializers/cors.rb | 16 + config/initializers/currency_display.rb | 6 +- .../initializers/filter_parameter_logging.rb | 8 +- config/initializers/mail_receiver.rb | 4 +- config/initializers/new_framework_defaults.rb | 17 - .../new_framework_defaults_5_1.rb | 14 - .../new_framework_defaults_5_2.rb | 38 -- config/initializers/permissions_policy.rb | 11 + config/initializers/rails6_backports.rb | 98 ------ config/initializers/zeitwerk.rb | 5 + ..._to_active_storage_blobs.active_storage.rb | 22 ++ ..._storage_variant_records.active_storage.rb | 28 ++ ...e_storage_blobs_checksum.active_storage.rb | 8 + db/schema.rb | 241 ++++++------- db/seeds/seed_helper.rb | 4 +- docker-compose-dev.yml | 1 + spec/controllers/articles_controller_spec.rb | 2 +- 53 files changed, 583 insertions(+), 594 deletions(-) rename {lib => app/lib}/api/errors.rb (100%) rename {lib => app/lib}/apple_bar.rb (75%) rename {lib => app/lib}/articles_csv.rb (87%) rename {lib => app/lib}/bank_account_connector.rb (90%) rename {lib => app/lib}/bank_account_connector_external.rb (100%) rename {lib => app/lib}/bank_account_information_importer.rb (100%) rename {lib => app/lib}/bank_transaction_reference.rb (85%) rename {lib => app/lib}/bank_transactions_csv.rb (93%) rename {lib => app/lib}/date_time_attribute_validate.rb (82%) rename {lib => app/lib}/financial_transactions_csv.rb (95%) rename {lib => app/lib}/foodsoft/expansion_variables.rb (96%) rename {lib => app/lib}/foodsoft_config.rb (98%) rename {lib => app/lib}/foodsoft_date_util.rb (84%) rename {lib => app/lib}/foodsoft_file.rb (100%) rename {lib => app/lib}/foodsoft_mail_receiver.rb (73%) rename {lib => app/lib}/invoices_csv.rb (95%) rename {lib => app/lib}/order_csv.rb (96%) rename {lib => app/lib}/order_pdf.rb (99%) rename {lib => app/lib}/order_txt.rb (81%) rename {lib => app/lib}/ordergroups_csv.rb (85%) rename {lib => app/lib}/render_csv.rb (98%) rename {lib => app/lib}/render_pdf.rb (97%) rename {lib => app/lib}/spreadsheet_file.rb (100%) rename {lib => app/lib}/templates/haml/scaffold/_form.html.haml (100%) rename {lib => app/lib}/token_verifier.rb (97%) rename {lib => app/lib}/users_csv.rb (97%) create mode 100644 config/initializers/cors.rb delete mode 100644 config/initializers/new_framework_defaults.rb delete mode 100644 config/initializers/new_framework_defaults_5_1.rb delete mode 100644 config/initializers/new_framework_defaults_5_2.rb create mode 100644 config/initializers/permissions_policy.rb delete mode 100644 config/initializers/rails6_backports.rb create mode 100644 config/initializers/zeitwerk.rb create mode 100644 db/migrate/20230106144438_add_service_name_to_active_storage_blobs.active_storage.rb create mode 100644 db/migrate/20230106144439_create_active_storage_variant_records.active_storage.rb create mode 100644 db/migrate/20230106144440_remove_not_null_on_active_storage_blobs_checksum.active_storage.rb diff --git a/.ruby-version b/.ruby-version index d48d3702..37c2961c 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -2.6.9 +2.7.2 diff --git a/Dockerfile-dev b/Dockerfile-dev index ca7865a5..37dce5f6 100644 --- a/Dockerfile-dev +++ b/Dockerfile-dev @@ -1,4 +1,4 @@ -FROM ruby:2.6 +FROM ruby:2.7 # Install dependencies RUN deps='libmagic-dev chromium nodejs' && \ @@ -19,6 +19,7 @@ ENV PORT=3000 \ WORKDIR /app +RUN gem install bundler RUN bundle config build.nokogiri "--use-system-libraries" EXPOSE 3000 diff --git a/Gemfile b/Gemfile index a357ef9b..42ac26db 100644 --- a/Gemfile +++ b/Gemfile @@ -1,11 +1,13 @@ # A sample Gemfile source "https://rubygems.org" -gem "rails", '~> 5.2' +gem "rails", '~> 7.0' +gem 'mail', '~> 2.7.1' # bug with mail 2.8.0 https://github.com/mikel/mail/issues/1489 -gem 'sass-rails' + +gem 'sassc-rails' gem 'less-rails' -gem 'uglifier', '>= 1.0.3' +gem 'uglifier' # See https://github.com/sstephenson/execjs#readme for more supported runtimes gem 'therubyracer', platforms: :ruby @@ -46,7 +48,8 @@ gem 'whenever', require: false # For defining cronjobs, see config/schedule.rb gem 'ruby-units' gem 'attribute_normalizer' gem 'ice_cube' -gem 'recurring_select' +# At time of development 01-06-2022 mmddyyyy necessary fix for config_helper.rb form builder was not in rubygems so we pull from github, see: https://github.com/gregschmit/recurring_select/pull/152 +gem 'recurring_select', git: 'https://github.com/gregschmit/recurring_select' gem 'roo' gem 'roo-xls' gem 'spreadsheet' @@ -55,7 +58,6 @@ gem 'gaffe' gem 'ruby-filemagic' gem 'mime-types' gem 'midi-smtp-server' -gem 'hashie', '~> 3.4.6', require: false # https://github.com/westfieldlabs/apivore/issues/114 gem 'rswag-api' gem 'rswag-ui' @@ -84,7 +86,8 @@ group :development do gem 'binding_of_caller' # gem "rails-i18n-debug" # chrome debugging extension https://github.com/dejan/rails_panel - gem 'meta_request' + # TODO: disabled due to https://github.com/rails/rails/issues/40781 + # gem 'meta_request' # Get infos when not using proper eager loading gem 'bullet' @@ -121,4 +124,5 @@ group :test do gem 'simplecov-lcov', require: false # api gem 'rswag-specs' + gem 'hashie', '~> 3.4.6', require: false # https://github.com/westfieldlabs/apivore/issues/114 end diff --git a/Gemfile.lock b/Gemfile.lock index 7352c8fe..f55e3397 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,3 +1,14 @@ +GIT + remote: https://github.com/gregschmit/recurring_select + revision: 29febc4c4abdd6c30636c33a7d2daecb09973ecf + specs: + recurring_select (3.0.0) + coffee-rails (>= 3.1) + ice_cube (>= 0.11) + jquery-rails (>= 3.0) + rails (>= 5.2) + sass-rails (>= 4.0) + GIT remote: https://github.com/technoweenie/acts_as_versioned.git revision: 63b1fc8529d028fae632fe80ec0cb25df56cd76b @@ -59,52 +70,76 @@ PATH GEM remote: https://rubygems.org/ specs: - actioncable (5.2.8.1) - actionpack (= 5.2.8.1) + actioncable (7.0.4) + actionpack (= 7.0.4) + activesupport (= 7.0.4) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailer (5.2.8.1) - actionpack (= 5.2.8.1) - actionview (= 5.2.8.1) - activejob (= 5.2.8.1) + actionmailbox (7.0.4) + actionpack (= 7.0.4) + activejob (= 7.0.4) + activerecord (= 7.0.4) + activestorage (= 7.0.4) + activesupport (= 7.0.4) + mail (>= 2.7.1) + net-imap + net-pop + net-smtp + actionmailer (7.0.4) + actionpack (= 7.0.4) + actionview (= 7.0.4) + activejob (= 7.0.4) + activesupport (= 7.0.4) mail (~> 2.5, >= 2.5.4) + net-imap + net-pop + net-smtp rails-dom-testing (~> 2.0) - actionpack (5.2.8.1) - actionview (= 5.2.8.1) - activesupport (= 5.2.8.1) - rack (~> 2.0, >= 2.0.8) + actionpack (7.0.4) + actionview (= 7.0.4) + activesupport (= 7.0.4) + rack (~> 2.0, >= 2.2.0) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.0, >= 1.0.2) - actionview (5.2.8.1) - activesupport (= 5.2.8.1) + rails-html-sanitizer (~> 1.0, >= 1.2.0) + actiontext (7.0.4) + actionpack (= 7.0.4) + activerecord (= 7.0.4) + activestorage (= 7.0.4) + activesupport (= 7.0.4) + globalid (>= 0.6.0) + nokogiri (>= 1.8.5) + actionview (7.0.4) + activesupport (= 7.0.4) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.0, >= 1.0.3) + rails-html-sanitizer (~> 1.1, >= 1.2.0) active_model_serializers (0.10.13) actionpack (>= 4.1, < 7.1) activemodel (>= 4.1, < 7.1) case_transform (>= 0.2) jsonapi-renderer (>= 0.1.1.beta1, < 0.3) - activejob (5.2.8.1) - activesupport (= 5.2.8.1) + activejob (7.0.4) + activesupport (= 7.0.4) globalid (>= 0.3.6) - activemodel (5.2.8.1) - activesupport (= 5.2.8.1) - activerecord (5.2.8.1) - activemodel (= 5.2.8.1) - activesupport (= 5.2.8.1) - arel (>= 9.0) - activestorage (5.2.8.1) - actionpack (= 5.2.8.1) - activerecord (= 5.2.8.1) - marcel (~> 1.0.0) - activesupport (5.2.8.1) + activemodel (7.0.4) + activesupport (= 7.0.4) + activerecord (7.0.4) + activemodel (= 7.0.4) + activesupport (= 7.0.4) + activestorage (7.0.4) + actionpack (= 7.0.4) + activejob (= 7.0.4) + activerecord (= 7.0.4) + activesupport (= 7.0.4) + marcel (~> 1.0) + mini_mime (>= 1.1.0) + activesupport (7.0.4) concurrent-ruby (~> 1.0, >= 1.0.2) - i18n (>= 0.7, < 2) - minitest (~> 5.1) - tzinfo (~> 1.1) + i18n (>= 1.6, < 2) + minitest (>= 5.1) + tzinfo (~> 2.0) acts_as_tree (2.9.1) activerecord (>= 3.0.0) addressable (2.8.1) @@ -112,7 +147,6 @@ GEM apparition (0.6.0) capybara (~> 3.13, < 4) websocket-driver (>= 0.6.5) - arel (9.0.0) ast (2.4.2) attribute_normalizer (1.2.0) base32 (0.3.4) @@ -123,15 +157,15 @@ GEM bindex (0.8.1) binding_of_caller (1.0.0) debug_inspector (>= 0.0.1) - bootsnap (1.13.0) + bootsnap (1.15.0) msgpack (~> 1.2) bootstrap-datepicker-rails (1.9.0.1) railties (>= 3.0) builder (3.2.4) - bullet (7.0.3) + bullet (7.0.7) activesupport (>= 3.0.0) uniform_notifier (~> 1.11) - capybara (3.36.0) + capybara (3.38.0) addressable matrix mini_mime (>= 0.1.3) @@ -163,6 +197,7 @@ GEM activerecord (>= 5.a) database_cleaner-core (~> 2.0.0) database_cleaner-core (2.0.1) + date (3.3.3) date_time_attribute (0.1.2) activesupport (>= 3.0.0) debug_inspector (1.1.0) @@ -175,13 +210,13 @@ GEM diff-lcs (1.5.0) diffy (3.4.2) docile (1.4.0) - doorkeeper (5.6.0) + doorkeeper (5.6.2) railties (>= 5) - doorkeeper-i18n (5.2.5) + doorkeeper-i18n (5.2.6) doorkeeper (>= 5.2) email_reply_trimmer (0.1.13) - erubi (1.11.0) - eventmachine (1.2.7) + erubi (1.12.0) + eventmachine (1.0.9.1) exception_notification (4.5.0) actionmailer (>= 5.2, < 8) activesupport (>= 5.2, < 8) @@ -192,14 +227,14 @@ GEM factory_bot_rails (6.2.0) factory_bot (~> 6.2.0) railties (>= 5.0.0) - faker (2.22.0) + faker (3.1.0) i18n (>= 1.8.11, < 2) ffi (1.15.5) gaffe (1.2.0) rails (>= 4.0.0) globalid (1.0.0) activesupport (>= 5.0) - haml (6.0.5) + haml (6.1.1) temple (>= 0.8.2) thor tilt @@ -228,13 +263,13 @@ GEM interception (0.5) iso (0.4.0) i18n - jquery-rails (4.5.0) + jquery-rails (4.5.1) rails-dom-testing (>= 1, < 3) railties (>= 4.2.0) thor (>= 0.14, < 2.0) - json (2.6.2) - json-schema (2.8.1) - addressable (>= 2.4) + json (2.6.3) + json-schema (3.0.0) + addressable (>= 2.8) jsonapi-renderer (0.2.2) kaminari (1.2.2) activesupport (>= 4.1.0) @@ -254,7 +289,7 @@ GEM actionpack (>= 5.0) less (~> 2.6.0) sprockets (~> 3.0) - libv8 (3.16.14.19) + libv8 (3.16.14.19-x86_64-linux) listen (3.7.1) rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) @@ -275,29 +310,33 @@ GEM thin marcel (1.0.2) matrix (0.4.2) - meta_request (0.7.3) - rack-contrib (>= 1.1, < 3) - railties (>= 3.0.0, < 7) method_source (1.0.0) midi-smtp-server (3.0.3) mime-types (3.4.1) mime-types-data (~> 3.2015) mime-types-data (3.2022.0105) mini_mime (1.1.2) - mini_portile2 (2.8.0) - minitest (5.16.3) + minitest (5.17.0) mono_logger (1.1.1) msgpack (1.6.0) multi_json (1.15.0) mustermann (3.0.0) ruby2_keywords (~> 0.0.1) mysql2 (0.5.4) + net-imap (0.3.4) + date + net-protocol + net-pop (0.1.2) + net-protocol + net-protocol (0.2.1) + timeout + net-smtp (0.3.3) + net-protocol nio4r (2.5.8) - nokogiri (1.13.10) - mini_portile2 (~> 2.8.0) + nokogiri (1.13.10-x86_64-linux) racc (~> 1.4) parallel (1.22.1) - parser (3.1.2.1) + parser (3.2.0.0) ast (~> 2.4.1) pdf-core (0.9.0) polyglot (0.3.5) @@ -315,32 +354,31 @@ GEM pry-stack_explorer (0.6.1) binding_of_caller (~> 1.0) pry (~> 0.13) - public_suffix (5.0.0) - puma (5.6.5) + public_suffix (5.0.1) + puma (6.0.2) nio4r (~> 2.0) - racc (1.6.1) - rack (2.2.4) - rack-contrib (2.3.0) - rack (~> 2.0) + racc (1.6.2) + rack (2.2.5) rack-cors (1.1.1) rack (>= 2.0.0) - rack-protection (3.0.4) + rack-protection (3.0.5) rack rack-test (2.0.2) rack (>= 1.3) - rails (5.2.8.1) - actioncable (= 5.2.8.1) - actionmailer (= 5.2.8.1) - actionpack (= 5.2.8.1) - actionview (= 5.2.8.1) - activejob (= 5.2.8.1) - activemodel (= 5.2.8.1) - activerecord (= 5.2.8.1) - activestorage (= 5.2.8.1) - activesupport (= 5.2.8.1) - bundler (>= 1.3.0) - railties (= 5.2.8.1) - sprockets-rails (>= 2.0.0) + rails (7.0.4) + actioncable (= 7.0.4) + actionmailbox (= 7.0.4) + actionmailer (= 7.0.4) + actionpack (= 7.0.4) + actiontext (= 7.0.4) + actionview (= 7.0.4) + activejob (= 7.0.4) + activemodel (= 7.0.4) + activerecord (= 7.0.4) + activestorage (= 7.0.4) + activesupport (= 7.0.4) + bundler (>= 1.15.0) + railties (= 7.0.4) rails-assets-listjs (0.2.0.beta.4) railties (>= 3.1) rails-controller-testing (1.0.5) @@ -352,42 +390,37 @@ GEM nokogiri (>= 1.6) rails-html-sanitizer (1.4.4) loofah (~> 2.19, >= 2.19.1) - rails-i18n (5.1.3) + rails-i18n (7.0.6) i18n (>= 0.7, < 2) - railties (>= 5.0, < 6) + railties (>= 6.0.0, < 8) rails-settings-cached (0.4.3) rails (>= 4.2.0) rails_tokeninput (1.7.0) railties (>= 3.1.0) - railties (5.2.8.1) - actionpack (= 5.2.8.1) - activesupport (= 5.2.8.1) + railties (7.0.4) + actionpack (= 7.0.4) + activesupport (= 7.0.4) method_source - rake (>= 0.8.7) - thor (>= 0.19.0, < 2.0) + rake (>= 12.2) + thor (~> 1.0) + zeitwerk (~> 2.5) rainbow (3.1.1) rake (13.0.6) - ransack (2.5.0) - activerecord (>= 5.2.4) - activesupport (>= 5.2.4) + ransack (3.2.1) + activerecord (>= 6.1.5) + activesupport (>= 6.1.5) i18n rb-fsevent (0.11.2) rb-inotify (0.10.1) ffi (~> 1.0) - recurring_select (3.0.0) - coffee-rails (>= 3.1) - ice_cube (>= 0.11) - jquery-rails (>= 3.0) - rails (>= 5.2) - sass-rails (>= 4.0) redis (5.0.5) redis-client (>= 0.9.0) - redis-client (0.9.0) + redis-client (0.11.2) connection_pool - redis-namespace (1.9.0) + redis-namespace (1.10.0) redis (>= 4) ref (2.0.0) - regexp_parser (2.6.0) + regexp_parser (2.6.1) responders (3.0.1) actionpack (>= 5.0) railties (>= 5.0) @@ -397,36 +430,36 @@ GEM redis-namespace (~> 1.6) sinatra (>= 0.9.2) rexml (3.2.5) - roo (2.8.3) + roo (2.9.0) nokogiri (~> 1) rubyzip (>= 1.3.0, < 3.0.0) roo-xls (1.2.0) nokogiri roo (>= 2.0.0, < 3) spreadsheet (> 0.9.0) - rspec (3.11.0) - rspec-core (~> 3.11.0) - rspec-expectations (~> 3.11.0) - rspec-mocks (~> 3.11.0) - rspec-core (3.11.0) - rspec-support (~> 3.11.0) - rspec-expectations (3.11.1) + rspec (3.12.0) + rspec-core (~> 3.12.0) + rspec-expectations (~> 3.12.0) + rspec-mocks (~> 3.12.0) + rspec-core (3.12.0) + rspec-support (~> 3.12.0) + rspec-expectations (3.12.1) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.11.0) - rspec-mocks (3.11.1) + rspec-support (~> 3.12.0) + rspec-mocks (3.12.1) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.11.0) - rspec-rails (5.1.2) - actionpack (>= 5.2) - activesupport (>= 5.2) - railties (>= 5.2) - rspec-core (~> 3.10) - rspec-expectations (~> 3.10) - rspec-mocks (~> 3.10) - rspec-support (~> 3.10) + rspec-support (~> 3.12.0) + rspec-rails (6.0.1) + actionpack (>= 6.1) + activesupport (>= 6.1) + railties (>= 6.1) + rspec-core (~> 3.11) + rspec-expectations (~> 3.11) + rspec-mocks (~> 3.11) + rspec-support (~> 3.11) rspec-rerun (1.1.0) rspec (~> 3.0) - rspec-support (3.11.1) + rspec-support (3.12.0) rswag-api (2.7.0) railties (>= 3.1, < 7.1) rswag-specs (2.7.0) @@ -437,27 +470,27 @@ GEM rswag-ui (2.7.0) actionpack (>= 3.1, < 7.1) railties (>= 3.1, < 7.1) - rubocop (1.36.0) + rubocop (1.43.0) json (~> 2.3) parallel (~> 1.10) - parser (>= 3.1.2.1) + parser (>= 3.2.0.0) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 1.8, < 3.0) rexml (>= 3.2.5, < 4.0) - rubocop-ast (>= 1.20.1, < 2.0) + rubocop-ast (>= 1.24.1, < 2.0) ruby-progressbar (~> 1.7) - unicode-display_width (>= 1.4.0, < 3.0) - rubocop-ast (1.21.0) + unicode-display_width (>= 2.4.0, < 3.0) + rubocop-ast (1.24.1) parser (>= 3.1.1.0) - rubocop-rails (2.16.1) + rubocop-rails (2.17.4) activesupport (>= 4.2.0) rack (>= 1.1) rubocop (>= 1.33.0, < 2.0) - rubocop-rspec (2.13.2) + rubocop-rspec (2.16.0) rubocop (~> 1.33) ruby-filemagic (0.7.3) ruby-ole (1.2.12.2) - ruby-prof (1.4.3) + ruby-prof (1.4.5) ruby-progressbar (1.11.0) ruby-units (3.0.0) ruby2_keywords (0.0.5) @@ -482,21 +515,21 @@ GEM simple_form (5.1.0) actionpack (>= 5.2) activemodel (>= 5.2) - simplecov (0.21.2) + simplecov (0.22.0) docile (~> 1.1) simplecov-html (~> 0.11) simplecov_json_formatter (~> 0.1) simplecov-html (0.12.3) simplecov-lcov (0.8.0) simplecov_json_formatter (0.1.4) - sinatra (3.0.4) + sinatra (3.0.5) mustermann (~> 3.0) rack (~> 2.2, >= 2.2.4) - rack-protection (= 3.0.4) + rack-protection (= 3.0.5) tilt (~> 2.0) - skinny (0.2.2) - eventmachine (~> 1.0) - thin + skinny (0.2.4) + eventmachine (~> 1.0.0) + thin (>= 1.5, < 1.7) spreadsheet (1.3.0) ruby-ole sprockets (3.7.2) @@ -510,17 +543,17 @@ GEM sqlite3-ruby (1.3.3) sqlite3 (>= 1.3.3) table_print (1.5.7) - temple (0.8.2) + temple (0.9.1) therubyracer (0.12.3) libv8 (~> 3.16.14.15) ref - thin (1.8.1) - daemons (~> 1.0, >= 1.0.9) - eventmachine (~> 1.0, >= 1.0.4) - rack (>= 1, < 3) + thin (1.6.2) + daemons (>= 1.0.9) + eventmachine (>= 1.0.0) + rack (>= 1.0.0) thor (1.2.1) - thread_safe (0.3.6) tilt (2.0.11) + timeout (0.3.1) ttfunk (1.7.0) twitter-bootstrap-rails (2.2.8) actionpack (>= 3.1) @@ -529,20 +562,20 @@ GEM railties (>= 3.1) twitter-text (1.14.7) unf (~> 0.1.0) - tzinfo (1.2.10) - thread_safe (~> 0.1) + tzinfo (2.0.5) + concurrent-ruby (~> 1.0) uglifier (4.2.0) execjs (>= 0.3.0, < 3) unf (0.1.4) unf_ext unf_ext (0.0.8.2) - unicode-display_width (2.3.0) + unicode-display_width (2.4.2) uniform_notifier (1.16.0) - web-console (3.7.0) - actionview (>= 5.0) - activemodel (>= 5.0) + web-console (4.2.0) + actionview (>= 6.0.0) + activemodel (>= 6.0.0) bindex (>= 0.4.0) - railties (>= 5.0) + railties (>= 6.0.0) websocket-driver (0.7.5) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) @@ -556,9 +589,10 @@ GEM twitter-text xpath (3.2.0) nokogiri (~> 1.8) + zeitwerk (2.6.6) PLATFORMS - ruby + x86_64-linux DEPENDENCIES active_model_serializers (~> 0.10.0) @@ -599,8 +633,8 @@ DEPENDENCIES kaminari less-rails listen + mail (~> 2.7.1) mailcatcher - meta_request midi-smtp-server mime-types mysql2 @@ -610,14 +644,14 @@ DEPENDENCIES pry-stack_explorer puma rack-cors - rails (~> 5.2) + rails (~> 7.0) rails-assets-listjs (= 0.2.0.beta.4) rails-controller-testing rails-i18n rails-settings-cached (= 0.4.3) rails_tokeninput ransack - recurring_select + recurring_select! resque roo roo-xls @@ -633,7 +667,7 @@ DEPENDENCIES ruby-filemagic ruby-prof ruby-units - sass-rails + sassc-rails sd_notify select2-rails simple-navigation (~> 3.14.0) @@ -647,9 +681,9 @@ DEPENDENCIES table_print therubyracer twitter-bootstrap-rails (~> 2.2.8) - uglifier (>= 1.0.3) + uglifier web-console whenever BUNDLED WITH - 1.17.3 + 2.4.3 diff --git a/lib/api/errors.rb b/app/lib/api/errors.rb similarity index 100% rename from lib/api/errors.rb rename to app/lib/api/errors.rb diff --git a/lib/apple_bar.rb b/app/lib/apple_bar.rb similarity index 75% rename from lib/apple_bar.rb rename to app/lib/apple_bar.rb index a2176ea3..236417c6 100644 --- a/lib/apple_bar.rb +++ b/app/lib/apple_bar.rb @@ -14,23 +14,23 @@ class AppleBar def group_bar_state if apples >= 100 'success' + elsif FoodsoftConfig[:stop_ordering_under].present? && + (apples >= FoodsoftConfig[:stop_ordering_under]) + 'warning' else - if FoodsoftConfig[:stop_ordering_under].present? and - apples >= FoodsoftConfig[:stop_ordering_under] - 'warning' - else - 'danger' - end + 'danger' end end # Use apples as percentage, but show at least 10 percent def group_bar_width - @ordergroup.apples < 2 ? 2 : @ordergroup.apples + [@ordergroup.apples, 2].max end def mean_order_amount_per_job - (1 / @global_avg).round rescue 0 + (1 / @global_avg).round + rescue + 0 end def apples diff --git a/lib/articles_csv.rb b/app/lib/articles_csv.rb similarity index 87% rename from lib/articles_csv.rb rename to app/lib/articles_csv.rb index 910de9be..55bc7fc5 100644 --- a/lib/articles_csv.rb +++ b/app/lib/articles_csv.rb @@ -1,4 +1,4 @@ -class ArticlesCsv < RenderCSV +class ArticlesCsv < RenderCsv include ApplicationHelper def header @@ -16,7 +16,7 @@ class ArticlesCsv < RenderCSV Article.human_attribute_name(:unit_quantity), '', '', - Article.human_attribute_name(:article_category), + Article.human_attribute_name(:article_category) ] end @@ -36,7 +36,7 @@ class ArticlesCsv < RenderCSV o.unit_quantity, '', '', - o.article_category.try(:name), + o.article_category.try(:name) ] end end diff --git a/lib/bank_account_connector.rb b/app/lib/bank_account_connector.rb similarity index 90% rename from lib/bank_account_connector.rb rename to app/lib/bank_account_connector.rb index 93e7cc7c..b728ebb9 100644 --- a/lib/bank_account_connector.rb +++ b/app/lib/bank_account_connector.rb @@ -8,9 +8,7 @@ class BankAccountConnector nil end - def text - @text - end + attr_reader :text end class TextField @@ -24,13 +22,7 @@ class BankAccountConnector nil end - def name - @name - end - - def value - @value - end + attr_reader :name, :value def label @label || @name.to_s @@ -73,17 +65,7 @@ class BankAccountConnector @bank_account.iban end - def auto_submit - @auto_submit - end - - def controls - @controls - end - - def count - @count - end + attr_reader :auto_submit, :controls, :count def text(data) @controls += [TextItem.new(data)] @@ -142,11 +124,9 @@ class BankAccountConnector @bank_account.save! end - def load(data) - end + def load(data); end - def dump - end + def dump; end def t(key, args = {}) return t(".fields.#{key}") unless key.is_a? String diff --git a/lib/bank_account_connector_external.rb b/app/lib/bank_account_connector_external.rb similarity index 100% rename from lib/bank_account_connector_external.rb rename to app/lib/bank_account_connector_external.rb diff --git a/lib/bank_account_information_importer.rb b/app/lib/bank_account_information_importer.rb similarity index 100% rename from lib/bank_account_information_importer.rb rename to app/lib/bank_account_information_importer.rb diff --git a/lib/bank_transaction_reference.rb b/app/lib/bank_transaction_reference.rb similarity index 85% rename from lib/bank_transaction_reference.rb rename to app/lib/bank_transaction_reference.rb index d033c544..22b9f181 100644 --- a/lib/bank_transaction_reference.rb +++ b/app/lib/bank_transaction_reference.rb @@ -1,7 +1,7 @@ class BankTransactionReference # parses a string from a bank transaction field def self.parse(data) - m = /(^|[^\w\.])FS(?\d+)(\.(?\d+))?(?([A-Za-z]+\d+(\.\d+)?)+)([^\w\.]|$)/.match(data) + m = /(^|[^\w.])FS(?\d+)(\.(?\d+))?(?([A-Za-z]+\d+(\.\d+)?)+)([^\w.]|$)/.match(data) return unless m parts = {} @@ -13,7 +13,7 @@ class BankTransactionReference ret = { group: m[:group].to_i, parts: parts } ret[:user] = m[:user].to_i if m[:user] - return ret + ret end def self.js_code_for_user(user) diff --git a/lib/bank_transactions_csv.rb b/app/lib/bank_transactions_csv.rb similarity index 93% rename from lib/bank_transactions_csv.rb rename to app/lib/bank_transactions_csv.rb index 34c39403..4adbc192 100644 --- a/lib/bank_transactions_csv.rb +++ b/app/lib/bank_transactions_csv.rb @@ -1,6 +1,6 @@ require 'csv' -class BankTransactionsCsv < RenderCSV +class BankTransactionsCsv < RenderCsv include ApplicationHelper def header diff --git a/lib/date_time_attribute_validate.rb b/app/lib/date_time_attribute_validate.rb similarity index 82% rename from lib/date_time_attribute_validate.rb rename to app/lib/date_time_attribute_validate.rb index 08138d02..23127898 100644 --- a/lib/date_time_attribute_validate.rb +++ b/app/lib/date_time_attribute_validate.rb @@ -27,12 +27,20 @@ module DateTimeAttributeValidate define_method("#{attribute}_date_value=") do |val| self.instance_variable_set("@#{attribute}_is_set", true) self.instance_variable_set("@#{attribute}_date_value", val) - self.send("#{attribute}_date=", val) rescue nil + begin + self.send("#{attribute}_date=", val) + rescue + nil + end end define_method("#{attribute}_time_value=") do |val| self.instance_variable_set("@#{attribute}_is_set", true) self.instance_variable_set("@#{attribute}_time_value", val) - self.send("#{attribute}_time=", val) rescue nil + begin + self.send("#{attribute}_time=", val) + rescue + nil + end end # fallback to field when values are not set @@ -48,11 +56,19 @@ module DateTimeAttributeValidate # validate date and time define_method("#{attribute}_datetime_value_valid") do date = self.instance_variable_get("@#{attribute}_date_value") - unless date.blank? || (Date.parse(date) rescue nil) + unless date.blank? || begin + Date.parse(date) + rescue + nil + end errors.add(attribute, "is not a valid date") # @todo I18n end time = self.instance_variable_get("@#{attribute}_time_value") - unless time.blank? || (Time.parse(time) rescue nil) + unless time.blank? || begin + Time.parse(time) + rescue + nil + end errors.add(attribute, "is not a valid time") # @todo I18n end end diff --git a/lib/financial_transactions_csv.rb b/app/lib/financial_transactions_csv.rb similarity index 95% rename from lib/financial_transactions_csv.rb rename to app/lib/financial_transactions_csv.rb index dc21d892..fc12d000 100644 --- a/lib/financial_transactions_csv.rb +++ b/app/lib/financial_transactions_csv.rb @@ -1,6 +1,6 @@ require 'csv' -class FinancialTransactionsCsv < RenderCSV +class FinancialTransactionsCsv < RenderCsv include ApplicationHelper def header diff --git a/lib/foodsoft/expansion_variables.rb b/app/lib/foodsoft/expansion_variables.rb similarity index 96% rename from lib/foodsoft/expansion_variables.rb rename to app/lib/foodsoft/expansion_variables.rb index bcf67e7a..97f7b6bb 100644 --- a/lib/foodsoft/expansion_variables.rb +++ b/app/lib/foodsoft/expansion_variables.rb @@ -54,8 +54,8 @@ module Foodsoft # @param options [Hash] Extra variables to expand # @return [String] Expanded string def self.expand(str, options = {}) - str.gsub /{{([._a-zA-Z0-9]+)}}/ do - options[$1] || self.get($1) + str.gsub(/{{([._a-zA-Z0-9]+)}}/) do + options[::Regexp.last_match(1)] || self.get(::Regexp.last_match(1)) end end diff --git a/lib/foodsoft_config.rb b/app/lib/foodsoft_config.rb similarity index 98% rename from lib/foodsoft_config.rb rename to app/lib/foodsoft_config.rb index 5a370459..6ea166d3 100644 --- a/lib/foodsoft_config.rb +++ b/app/lib/foodsoft_config.rb @@ -44,6 +44,8 @@ class FoodsoftConfig # @return [ActiveSupport::HashWithIndifferentAccess] Current configuration from configuration file. mattr_accessor :config + mattr_accessor :default_config + # Configuration file location. # Taken from environment variable +FOODSOFT_APP_CONFIG+, # or else +config/app_config.yml+. @@ -189,7 +191,7 @@ class FoodsoftConfig # @return [Hash] Full configuration. def to_hash - keys.to_h { |k| [k, self[k]] } + keys.index_with { |k| self[k] } end # for using active_model_serializer in the api/v1/configs controller @@ -216,7 +218,6 @@ class FoodsoftConfig # end # # @return [Hash] Default configuration values - mattr_accessor :default_config private @@ -229,7 +230,7 @@ class FoodsoftConfig end def setup_database - database_config = ActiveRecord::Base.configurations[Rails.env] + database_config = ActiveRecord::Base.configurations.find_db_config(Rails.env).configuration_hash database_config = database_config.merge(config[:database]) if config[:database].present? ActiveRecord::Base.establish_connection(database_config) end diff --git a/lib/foodsoft_date_util.rb b/app/lib/foodsoft_date_util.rb similarity index 84% rename from lib/foodsoft_date_util.rb rename to app/lib/foodsoft_date_util.rb index 98dc1c61..a14ad453 100644 --- a/lib/foodsoft_date_util.rb +++ b/app/lib/foodsoft_date_util.rb @@ -6,7 +6,11 @@ module FoodsoftDateUtil schedule = IceCube::Schedule.new(start) schedule.add_recurrence_rule rule_from(options[:recurr]) # @todo handle ical parse errors - occ = (schedule.next_occurrence(from).to_time rescue nil) + occ = begin + schedule.next_occurrence(from).to_time + rescue + nil + end end if options && options[:time] && occ occ = occ.beginning_of_day.advance(seconds: Time.parse(options[:time]).seconds_since_midnight) @@ -17,9 +21,10 @@ module FoodsoftDateUtil # @param p [String, Symbol, Hash, IceCube::Rule] What to return a rule from. # @return [IceCube::Rule] Recurring rule def self.rule_from(p) - if p.is_a? String + case p + when String IceCube::Rule.from_ical(p) - elsif p.is_a? Hash + when Hash IceCube::Rule.from_hash(p) else p diff --git a/lib/foodsoft_file.rb b/app/lib/foodsoft_file.rb similarity index 100% rename from lib/foodsoft_file.rb rename to app/lib/foodsoft_file.rb diff --git a/lib/foodsoft_mail_receiver.rb b/app/lib/foodsoft_mail_receiver.rb similarity index 73% rename from lib/foodsoft_mail_receiver.rb rename to app/lib/foodsoft_mail_receiver.rb index 560e7edd..18e93be3 100644 --- a/lib/foodsoft_mail_receiver.rb +++ b/app/lib/foodsoft_mail_receiver.rb @@ -19,7 +19,7 @@ class FoodsoftMailReceiver < MidiSmtpServer::Smtpd private - def on_rcpt_to_event(ctx, rcpt_to) + def on_rcpt_to_event(_ctx, rcpt_to) recipient = rcpt_to.gsub(/^\s*<\s*(.*)\s*>\s*$/, '\1') @handlers << self.class.find_handler(recipient) rcpt_to @@ -29,20 +29,18 @@ class FoodsoftMailReceiver < MidiSmtpServer::Smtpd end def on_message_data_event(ctx) - begin - @handlers.each do |handler| - handler.call(ctx[:message][:data]) - end - rescue => error - ExceptionNotifier.notify_exception(error, data: ctx) - raise error - ensure - @handlers.clear + @handlers.each do |handler| + handler.call(ctx[:message][:data]) end + rescue => error + ExceptionNotifier.notify_exception(error, data: ctx) + raise error + ensure + @handlers.clear end def self.find_handler(recipient) - m = /(?[^@\.]+)\.(?
[^@]+)(@(?[^@]+))?/.match recipient + m = /(?[^@.]+)\.(?
[^@]+)(@(?[^@]+))?/.match recipient raise "recipient is missing or has an invalid format" if m.nil? raise "Foodcoop '#{m[:foodcoop]}' could not be found" unless FoodsoftConfig.allowed_foodcoop? m[:foodcoop] @@ -51,7 +49,7 @@ class FoodsoftMailReceiver < MidiSmtpServer::Smtpd @@registered_classes.each do |klass| if match = klass.regexp.match(m[:address]) handler = klass.new match - return lambda { |data| handler.received(data) } + return ->(data) { handler.received(data) } end end diff --git a/lib/invoices_csv.rb b/app/lib/invoices_csv.rb similarity index 95% rename from lib/invoices_csv.rb rename to app/lib/invoices_csv.rb index aa20cd08..eecad298 100644 --- a/lib/invoices_csv.rb +++ b/app/lib/invoices_csv.rb @@ -1,6 +1,6 @@ require 'csv' -class InvoicesCsv < RenderCSV +class InvoicesCsv < RenderCsv include ApplicationHelper def header @@ -32,7 +32,7 @@ class InvoicesCsv < RenderCSV t.deposit, t.deposit_credit, t.paid_on, - t.note, + t.note ] end end diff --git a/lib/order_csv.rb b/app/lib/order_csv.rb similarity index 96% rename from lib/order_csv.rb rename to app/lib/order_csv.rb index 6ec96581..b238f90c 100644 --- a/lib/order_csv.rb +++ b/app/lib/order_csv.rb @@ -1,6 +1,6 @@ require 'csv' -class OrderCsv < RenderCSV +class OrderCsv < RenderCsv def header [ OrderArticle.human_attribute_name(:units_to_order), diff --git a/lib/order_pdf.rb b/app/lib/order_pdf.rb similarity index 99% rename from lib/order_pdf.rb rename to app/lib/order_pdf.rb index 034ca51f..164be66b 100644 --- a/lib/order_pdf.rb +++ b/app/lib/order_pdf.rb @@ -1,4 +1,4 @@ -class OrderPdf < RenderPDF +class OrderPdf < RenderPdf attr_reader :order def initialize(order, options = {}) diff --git a/lib/order_txt.rb b/app/lib/order_txt.rb similarity index 81% rename from lib/order_txt.rb rename to app/lib/order_txt.rb index 5ad1fba6..7f23e705 100644 --- a/lib/order_txt.rb +++ b/app/lib/order_txt.rb @@ -1,5 +1,5 @@ class OrderTxt - def initialize(order, options = {}) + def initialize(order, _options = {}) @order = order end @@ -15,10 +15,10 @@ class OrderTxt text += "****** " + I18n.t('orders.fax.to_address') + "\n\n" text += "#{FoodsoftConfig[:name]}\n#{contact[:street]}\n#{contact[:zip_code]} #{contact[:city]}\n\n" text += "****** " + I18n.t('orders.fax.articles') + "\n\n" - text += "%8s %8s %s\n" % [I18n.t('orders.fax.number'), I18n.t('orders.fax.amount'), I18n.t('orders.fax.name')] + text += format("%8s %8s %s\n", I18n.t('orders.fax.number'), I18n.t('orders.fax.amount'), I18n.t('orders.fax.name')) # now display all ordered articles @order.order_articles.ordered.includes([:article, :article_price]).each do |oa| - text += "%8s %8d %s\n" % [oa.article.order_number, oa.units_to_order.to_i, oa.article.name] + text += format("%8s %8d %s\n", oa.article.order_number, oa.units_to_order.to_i, oa.article.name) end text end diff --git a/lib/ordergroups_csv.rb b/app/lib/ordergroups_csv.rb similarity index 85% rename from lib/ordergroups_csv.rb rename to app/lib/ordergroups_csv.rb index c41d2e83..f6fba00f 100644 --- a/lib/ordergroups_csv.rb +++ b/app/lib/ordergroups_csv.rb @@ -1,4 +1,4 @@ -class OrdergroupsCsv < RenderCSV +class OrdergroupsCsv < RenderCsv include ApplicationHelper def header @@ -14,9 +14,9 @@ class OrdergroupsCsv < RenderCSV Ordergroup.human_attribute_name(:break_start), Ordergroup.human_attribute_name(:break_end), Ordergroup.human_attribute_name(:last_user_activity), - Ordergroup.human_attribute_name(:last_order), + Ordergroup.human_attribute_name(:last_order) ] - row + Ordergroup.custom_fields.map { |f| f[:label] } + row + Ordergroup.custom_fields.pluck(:label) end def data @@ -33,7 +33,7 @@ class OrdergroupsCsv < RenderCSV o.break_start, o.break_end, o.last_user_activity, - o.last_order.try(:starts), + o.last_order.try(:starts) ] yield row + Ordergroup.custom_fields.map { |f| o.settings.custom_fields[f[:name]] } end diff --git a/lib/render_csv.rb b/app/lib/render_csv.rb similarity index 98% rename from lib/render_csv.rb rename to app/lib/render_csv.rb index b900f1f7..1f20b075 100644 --- a/lib/render_csv.rb +++ b/app/lib/render_csv.rb @@ -1,6 +1,6 @@ require 'csv' -class RenderCSV +class RenderCsv include ActionView::Helpers::NumberHelper def initialize(object, options = {}) diff --git a/lib/render_pdf.rb b/app/lib/render_pdf.rb similarity index 97% rename from lib/render_pdf.rb rename to app/lib/render_pdf.rb index a5cde2b6..479dc4a3 100644 --- a/lib/render_pdf.rb +++ b/app/lib/render_pdf.rb @@ -18,7 +18,7 @@ class RotatedCell < Prawn::Table::Cell::Text (height + (border_top_width / 2.0) + (border_bottom_width / 2.0)) / tan_rotation end - def styled_width_of(text) + def styled_width_of(_text) options = @text_options.reject { |k| k == :style } with_font { (@pdf.height_of(@content, options) + padding_top + padding_bottom) / tan_rotation } end @@ -52,7 +52,7 @@ class RotatedCell < Prawn::Table::Cell::Text end end -class RenderPDF < Prawn::Document +class RenderPdf < Prawn::Document include ActionView::Helpers::NumberHelper include ApplicationHelper @@ -156,9 +156,10 @@ class RenderPDF < Prawn::Document def pdf_add_page_breaks?(docid = nil) docid ||= self.class.name.underscore cfg = FoodsoftConfig[:pdf_add_page_breaks] - if cfg.is_a? Array + case cfg + when Array cfg.index(docid.to_s).any? - elsif cfg.is_a? Hash + when Hash cfg[docid.to_s] else cfg diff --git a/lib/spreadsheet_file.rb b/app/lib/spreadsheet_file.rb similarity index 100% rename from lib/spreadsheet_file.rb rename to app/lib/spreadsheet_file.rb diff --git a/lib/templates/haml/scaffold/_form.html.haml b/app/lib/templates/haml/scaffold/_form.html.haml similarity index 100% rename from lib/templates/haml/scaffold/_form.html.haml rename to app/lib/templates/haml/scaffold/_form.html.haml diff --git a/lib/token_verifier.rb b/app/lib/token_verifier.rb similarity index 97% rename from lib/token_verifier.rb rename to app/lib/token_verifier.rb index a8a0f1eb..b481d60f 100644 --- a/lib/token_verifier.rb +++ b/app/lib/token_verifier.rb @@ -21,8 +21,6 @@ class TokenVerifier < ActiveSupport::MessageVerifier # return original message if r.length > 2 r[2] - else - nil end end @@ -32,8 +30,6 @@ class TokenVerifier < ActiveSupport::MessageVerifier class InvalidPrefix < ActiveSupport::MessageVerifier::InvalidSignature; end - protected - def self.secret # secret_key_base for Rails 4, but Rails 3 initializer may still be used Foodsoft::Application.config.secret_key_base || Foodsoft::Application.config.secret_token diff --git a/lib/users_csv.rb b/app/lib/users_csv.rb similarity index 97% rename from lib/users_csv.rb rename to app/lib/users_csv.rb index 56ec3a23..a7d54698 100644 --- a/lib/users_csv.rb +++ b/app/lib/users_csv.rb @@ -1,4 +1,4 @@ -class UsersCsv < RenderCSV +class UsersCsv < RenderCsv include ApplicationHelper def header diff --git a/bin/setup b/bin/setup index 94fd4d79..ec47b79b 100755 --- a/bin/setup +++ b/bin/setup @@ -1,36 +1,33 @@ #!/usr/bin/env ruby -require 'fileutils' -include FileUtils +require "fileutils" # path to your application root. -APP_ROOT = File.expand_path('..', __dir__) +APP_ROOT = File.expand_path("..", __dir__) def system!(*args) system(*args) || abort("\n== Command #{args} failed ==") end -chdir APP_ROOT do - # This script is a starting point to setup your application. +FileUtils.chdir APP_ROOT do + # This script is a way to set up or update your development environment automatically. + # This script is idempotent, so that you can run it at any time and get an expectable outcome. # Add necessary setup steps to this file. - puts '== Installing dependencies ==' - system! 'gem install bundler --conservative' - system('bundle check') || system!('bundle install') - - # Install JavaScript dependencies if using Yarn - # system('bin/yarn') + puts "== Installing dependencies ==" + system! "gem install bundler --conservative" + system("bundle check") || system!("bundle install") # puts "\n== Copying sample files ==" - # unless File.exist?('config/database.yml') - # cp 'config/database.yml.sample', 'config/database.yml' + # unless File.exist?("config/database.yml") + # FileUtils.cp "config/database.yml.sample", "config/database.yml" # end puts "\n== Preparing database ==" - system! 'bin/rails db:setup' + system! "bin/rails db:prepare" puts "\n== Removing old logs and tempfiles ==" - system! 'bin/rails log:clear tmp:clear' + system! "bin/rails log:clear tmp:clear" puts "\n== Restarting application server ==" - system! 'bin/rails restart' + system! "bin/rails restart" end diff --git a/config/application.rb b/config/application.rb index 544e534c..9c0ade99 100644 --- a/config/application.rb +++ b/config/application.rb @@ -9,7 +9,7 @@ Bundler.require(*Rails.groups) module Foodsoft class Application < Rails::Application # Initialize configuration defaults for originally generated Rails version. - config.load_defaults 5.0 + config.load_defaults 7.0 # Settings in config/environments/* take precedence over those specified here. # Application configuration can go into files in config/initializers @@ -36,9 +36,6 @@ module Foodsoft # Configure the default encoding used in templates for Ruby 1.9. config.encoding = "utf-8" - # TODO: Remove this. See CVE-2022-32224 for details. - config.active_record.yaml_column_permitted_classes = [BigDecimal, Date, Symbol, Time] - # Enable escaping HTML in JSON. config.active_support.escape_html_entities_in_json = true @@ -66,6 +63,12 @@ module Foodsoft # Load legacy scripts from vendor config.assets.precompile += ['vendor/assets/javascripts/*.js'] + config.active_record.yaml_column_permitted_classes = [Symbol, BigDecimal] + + config.autoloader = :zeitwerk + + # Ex:- :default =>'' + # CORS for API config.middleware.insert_before 0, Rack::Cors do allow do diff --git a/config/environments/production.rb b/config/environments/production.rb index 0560b38d..d0f06b95 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -1,3 +1,5 @@ +require "active_support/core_ext/integer/time" + # Foodsoft production configuration. # # This file is in the public domain. @@ -34,16 +36,16 @@ Rails.application.configure do config.assets.compile = false # Enable serving of images, stylesheets, and JavaScripts from an asset server. - # config.action_controller.asset_host = 'http://assets.example.com' + # config.asset_host = "http://assets.example.com" # Specifies the header that your server uses for sending files. # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX - # Store uploaded files on the local file system (see config/storage.yml for options) + # Store uploaded files on the local file system (see config/storage.yml for options). config.active_storage.service = :local - # Mount Action Cable outside main process or domain + # Mount Action Cable outside main process or domain. # config.action_cable.mount_path = nil # config.action_cable.url = 'wss://example.com/cable' # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ] @@ -51,6 +53,8 @@ Rails.application.configure do # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. config.force_ssl = ENV["RAILS_FORCE_SSL"] != "false" + # Include generic and useful information about system operation, but avoid logging too much + # information to avoid inadvertent exposure of personally identifiable information (PII). # Set to :debug to see everything in the log. config.log_level = :info @@ -63,6 +67,10 @@ Rails.application.configure do # Use a different cache store in production. # config.cache_store = :mem_cache_store + # Use a real queuing backend for Active Job (and separate queues per environment). + # config.active_job.queue_adapter = :resque + # config.active_job.queue_name_prefix = "foodsoft_production" + config.action_mailer.perform_caching = false # Ignore bad email addresses and do not raise email delivery errors. @@ -98,7 +106,7 @@ Rails.application.configure do end # Use default logging formatter so that PID and timestamp are not suppressed. - config.log_formatter = ::Logger::Formatter.new + config.log_formatter = Logger::Formatter.new # Use a different logger for distributed setups. # require 'syslog/logger' diff --git a/config/environments/test.rb b/config/environments/test.rb index ccf3767f..6ea4d1e7 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -1,30 +1,31 @@ -# Foodsoft test configuration. -# -# This file is in the public domain. +require "active_support/core_ext/integer/time" + +# The test environment is used exclusively to run your application's +# test suite. You never need to work with it otherwise. Remember that +# your test database is "scratch space" for the test suite and is wiped +# and recreated between test runs. Don't rely on the data there! Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. - # The test environment is used exclusively to run your application's - # test suite. You never need to work with it otherwise. Remember that - # your test database is "scratch space" for the test suite and is wiped - # and recreated between test runs. Don't rely on the data there! + # Turn false under Spring and add config.action_view.cache_template_loading = true. config.cache_classes = true - # Do not eager load code on boot. This avoids loading your whole application - # just for the purpose of running a single test. If you are using a tool that - # preloads Rails for running tests, you may have to set it to true. - config.eager_load = false + # Eager loading loads your whole application. When running a single test locally, + # this probably isn't necessary. It's a good idea to do in a continuous integration + # system, or in some way before deploying your code. + config.eager_load = ENV["CI"].present? # Configure public file server for tests with Cache-Control for performance. config.public_file_server.enabled = true config.public_file_server.headers = { - 'Cache-Control' => "public, max-age=#{1.hour.to_i}" + "Cache-Control" => "public, max-age=#{1.hour.to_i}" } # Show full error reports and disable caching. config.consider_all_requests_local = true config.action_controller.perform_caching = false + config.cache_store = :null_store # Raise exceptions instead of rendering exception templates. config.action_dispatch.show_exceptions = false @@ -32,7 +33,7 @@ Rails.application.configure do # Disable request forgery protection in test environment. config.action_controller.allow_forgery_protection = false - # Store uploaded files on the local file system in a temporary directory + # Store uploaded files on the local file system in a temporary directory. config.active_storage.service = :test config.action_mailer.perform_caching = false @@ -45,6 +46,15 @@ Rails.application.configure do # Print deprecation notices to the stderr. config.active_support.deprecation = :stderr - # Raises error for missing translations - # config.action_view.raise_on_missing_translations = true + # Raise exceptions for disallowed deprecations. + config.active_support.disallowed_deprecation = :raise + + # Tell Active Support which deprecation messages to disallow. + config.active_support.disallowed_deprecation_warnings = [] + + # Raises error for missing translations. + # config.i18n.raise_on_missing_translations = true + + # Annotate rendered view with file names. + # config.action_view.annotate_rendered_view_with_filenames = true end diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb index 4b828e80..fe48fc34 100644 --- a/config/initializers/assets.rb +++ b/config/initializers/assets.rb @@ -5,8 +5,6 @@ Rails.application.config.assets.version = '1.0' # Add additional assets to the asset load path. # Rails.application.config.assets.paths << Emoji.images_path -# Add Yarn node_modules folder to the asset load path. -Rails.application.config.assets.paths << Rails.root.join('node_modules') # Precompile additional assets. # application.js, application.css, and all non-JS/CSS in the app/assets diff --git a/config/initializers/content_security_policy.rb b/config/initializers/content_security_policy.rb index d3bcaa5e..54f47cf1 100644 --- a/config/initializers/content_security_policy.rb +++ b/config/initializers/content_security_policy.rb @@ -1,25 +1,25 @@ # Be sure to restart your server when you modify this file. -# Define an application-wide content security policy -# For further information see the following documentation -# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy +# Define an application-wide content security policy. +# See the Securing Rails Applications Guide for more information: +# https://guides.rubyonrails.org/security.html#content-security-policy-header -# Rails.application.config.content_security_policy do |policy| -# policy.default_src :self, :https -# policy.font_src :self, :https, :data -# policy.img_src :self, :https, :data -# policy.object_src :none -# policy.script_src :self, :https -# policy.style_src :self, :https - -# # Specify URI for violation reports -# # policy.report_uri "/csp-violation-report-endpoint" +# Rails.application.configure do +# config.content_security_policy do |policy| +# policy.default_src :self, :https +# policy.font_src :self, :https, :data +# policy.img_src :self, :https, :data +# policy.object_src :none +# policy.script_src :self, :https +# policy.style_src :self, :https +# # Specify URI for violation reports +# # policy.report_uri "/csp-violation-report-endpoint" +# end +# +# # Generate session nonces for permitted importmap and inline scripts +# config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s } +# config.content_security_policy_nonce_directives = %w(script-src) +# +# # Report violations without enforcing the policy. +# # config.content_security_policy_report_only = true # end - -# If you are using UJS then enable automatic nonce generation -# Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) } - -# Report CSP violations to a specified URI -# For further information see the following documentation: -# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only -# Rails.application.config.content_security_policy_report_only = true diff --git a/config/initializers/cors.rb b/config/initializers/cors.rb new file mode 100644 index 00000000..e5a82f16 --- /dev/null +++ b/config/initializers/cors.rb @@ -0,0 +1,16 @@ +# Be sure to restart your server when you modify this file. + +# Avoid CORS issues when API is called from the frontend app. +# Handle Cross-Origin Resource Sharing (CORS) in order to accept cross-origin AJAX requests. + +# Read more: https://github.com/cyu/rack-cors + +# Rails.application.config.middleware.insert_before 0, Rack::Cors do +# allow do +# origins "example.com" +# +# resource "*", +# headers: :any, +# methods: [:get, :post, :put, :patch, :delete, :options, :head] +# end +# end diff --git a/config/initializers/currency_display.rb b/config/initializers/currency_display.rb index 7caa6a64..71d108d2 100644 --- a/config/initializers/currency_display.rb +++ b/config/initializers/currency_display.rb @@ -1,7 +1,7 @@ # remove all currency translations, so that we can set the default language and # have it shown in all other languages too -::I18n.available_locales.each do |locale| - unless locale == ::I18n.default_locale - ::I18n.backend.store_translations(locale, number: { currency: { format: { unit: nil } } }) +I18n.available_locales.each do |locale| + unless locale == I18n.default_locale + I18n.backend.store_translations(locale, number: { currency: { format: { unit: nil } } }) end end diff --git a/config/initializers/filter_parameter_logging.rb b/config/initializers/filter_parameter_logging.rb index 4a994e1e..adc6568c 100644 --- a/config/initializers/filter_parameter_logging.rb +++ b/config/initializers/filter_parameter_logging.rb @@ -1,4 +1,8 @@ # Be sure to restart your server when you modify this file. -# Configure sensitive parameters which will be filtered from the log file. -Rails.application.config.filter_parameters += [:password] +# Configure parameters to be filtered from the log file. Use this to limit dissemination of +# sensitive information. See the ActiveSupport::ParameterFilter documentation for supported +# notations and behaviors. +Rails.application.config.filter_parameters += [ + :passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn +] diff --git a/config/initializers/mail_receiver.rb b/config/initializers/mail_receiver.rb index 67288cc1..088d7c93 100644 --- a/config/initializers/mail_receiver.rb +++ b/config/initializers/mail_receiver.rb @@ -1 +1,3 @@ -FoodsoftMailReceiver.register BounceMailReceiver +Rails.application.config.to_prepare do + FoodsoftMailReceiver.register BounceMailReceiver +end diff --git a/config/initializers/new_framework_defaults.rb b/config/initializers/new_framework_defaults.rb deleted file mode 100644 index fac64e0a..00000000 --- a/config/initializers/new_framework_defaults.rb +++ /dev/null @@ -1,17 +0,0 @@ -# Be sure to restart your server when you modify this file. -# -# This file contains migration options to ease your Rails 5.0 upgrade. -# -# Once upgraded flip defaults one by one to migrate to the new default. -# -# Read the Guide for Upgrading Ruby on Rails for more info on each option. - -# Enable per-form CSRF tokens. Previous versions had false. -Rails.application.config.action_controller.per_form_csrf_tokens = false - -# Enable origin-checking CSRF mitigation. Previous versions had false. -Rails.application.config.action_controller.forgery_protection_origin_check = false - -# Make Ruby 2.4 preserve the timezone of the receiver when calling `to_time`. -# Previous versions had false. -ActiveSupport.to_time_preserves_timezone = false diff --git a/config/initializers/new_framework_defaults_5_1.rb b/config/initializers/new_framework_defaults_5_1.rb deleted file mode 100644 index 9010abd5..00000000 --- a/config/initializers/new_framework_defaults_5_1.rb +++ /dev/null @@ -1,14 +0,0 @@ -# Be sure to restart your server when you modify this file. -# -# This file contains migration options to ease your Rails 5.1 upgrade. -# -# Once upgraded flip defaults one by one to migrate to the new default. -# -# Read the Guide for Upgrading Ruby on Rails for more info on each option. - -# Make `form_with` generate non-remote forms. -Rails.application.config.action_view.form_with_generates_remote_forms = false - -# Unknown asset fallback will return the path passed in when the given -# asset is not present in the asset pipeline. -# Rails.application.config.assets.unknown_asset_fallback = false diff --git a/config/initializers/new_framework_defaults_5_2.rb b/config/initializers/new_framework_defaults_5_2.rb deleted file mode 100644 index 5132a0b1..00000000 --- a/config/initializers/new_framework_defaults_5_2.rb +++ /dev/null @@ -1,38 +0,0 @@ -# Be sure to restart your server when you modify this file. -# -# This file contains migration options to ease your Rails 5.2 upgrade. -# -# Once upgraded flip defaults one by one to migrate to the new default. -# -# Read the Guide for Upgrading Ruby on Rails for more info on each option. - -# Make Active Record use stable #cache_key alongside new #cache_version method. -# This is needed for recyclable cache keys. -# Rails.application.config.active_record.cache_versioning = true - -# Use AES-256-GCM authenticated encryption for encrypted cookies. -# Also, embed cookie expiry in signed or encrypted cookies for increased security. -# -# This option is not backwards compatible with earlier Rails versions. -# It's best enabled when your entire app is migrated and stable on 5.2. -# -# Existing cookies will be converted on read then written with the new scheme. -# Rails.application.config.action_dispatch.use_authenticated_cookie_encryption = true - -# Use AES-256-GCM authenticated encryption as default cipher for encrypting messages -# instead of AES-256-CBC, when use_authenticated_message_encryption is set to true. -# Rails.application.config.active_support.use_authenticated_message_encryption = true - -# Add default protection from forgery to ActionController::Base instead of in -# ApplicationController. -# Rails.application.config.action_controller.default_protect_from_forgery = true - -# Store boolean values are in sqlite3 databases as 1 and 0 instead of 't' and -# 'f' after migrating old data. -Rails.application.config.active_record.sqlite3.represent_boolean_as_integer = true - -# Use SHA-1 instead of MD5 to generate non-sensitive digests, such as the ETag header. -# Rails.application.config.active_support.use_sha1_digests = true - -# Make `form_with` generate id attributes for any generated HTML tags. -# Rails.application.config.action_view.form_with_generates_ids = true diff --git a/config/initializers/permissions_policy.rb b/config/initializers/permissions_policy.rb new file mode 100644 index 00000000..00f64d71 --- /dev/null +++ b/config/initializers/permissions_policy.rb @@ -0,0 +1,11 @@ +# Define an application-wide HTTP permissions policy. For further +# information see https://developers.google.com/web/updates/2018/06/feature-policy +# +# Rails.application.config.permissions_policy do |f| +# f.camera :none +# f.gyroscope :none +# f.microphone :none +# f.usb :none +# f.fullscreen :self +# f.payment :self, "https://secure.example.com" +# end diff --git a/config/initializers/rails6_backports.rb b/config/initializers/rails6_backports.rb deleted file mode 100644 index b72f4220..00000000 --- a/config/initializers/rails6_backports.rb +++ /dev/null @@ -1,98 +0,0 @@ -raise "Remove no-longer-needed #{__FILE__}!" if Rails::VERSION::MAJOR >= 6 - -require "weakref" - -module ActiveRecord - # Backport https://github.com/rails/rails/pull/36998 and https://github.com/rails/rails/pull/36999 - # to avoid `ThreadError: can't create Thread: Resource temporarily unavailable` issues - module ConnectionAdapters - class ConnectionPool - class Reaper - @mutex = Mutex.new - @pools = {} - @threads = {} - - class << self - def register_pool(pool, frequency) # :nodoc: - @mutex.synchronize do - unless @threads[frequency]&.alive? - @threads[frequency] = spawn_thread(frequency) - end - @pools[frequency] ||= [] - @pools[frequency] << WeakRef.new(pool) - end - end - - private - - def spawn_thread(frequency) - Thread.new(frequency) do |t| - running = true - while running - sleep t - @mutex.synchronize do - @pools[frequency].select!(&:weakref_alive?) - @pools[frequency].each do |p| - p.reap - p.flush - rescue WeakRef::RefError - end - - if @pools[frequency].empty? - @pools.delete(frequency) - @threads.delete(frequency) - running = false - end - end - end - end - end - end - - def run - return unless frequency && frequency > 0 - - self.class.register_pool(pool, frequency) - end - end - - def reap - stale_connections = synchronize do - return unless @connections - - @connections.select do |conn| - conn.in_use? && !conn.owner.alive? - end.each(&:steal!) - end - - stale_connections.each do |conn| - if conn.active? - conn.reset! - checkin conn - else - remove conn - end - end - end - - def flush(minimum_idle = @idle_timeout) - return if minimum_idle.nil? - - idle_connections = synchronize do - return unless @connections - - @connections.select do |conn| - !conn.in_use? && conn.seconds_idle >= minimum_idle - end.each do |conn| - conn.lease - - @available.delete conn - @connections.delete conn - end - end - - idle_connections.each(&:disconnect!) - end - end - end -end diff --git a/config/initializers/zeitwerk.rb b/config/initializers/zeitwerk.rb new file mode 100644 index 00000000..9c505a26 --- /dev/null +++ b/config/initializers/zeitwerk.rb @@ -0,0 +1,5 @@ +# config/initializers/zeitwerk.rb +ActiveSupport::Dependencies + .autoload_paths + .delete("#{Rails.root}/app/controllers/concerns") + \ No newline at end of file diff --git a/db/migrate/20230106144438_add_service_name_to_active_storage_blobs.active_storage.rb b/db/migrate/20230106144438_add_service_name_to_active_storage_blobs.active_storage.rb new file mode 100644 index 00000000..a15c6ce8 --- /dev/null +++ b/db/migrate/20230106144438_add_service_name_to_active_storage_blobs.active_storage.rb @@ -0,0 +1,22 @@ +# This migration comes from active_storage (originally 20190112182829) +class AddServiceNameToActiveStorageBlobs < ActiveRecord::Migration[6.0] + def up + return unless table_exists?(:active_storage_blobs) + + unless column_exists?(:active_storage_blobs, :service_name) + add_column :active_storage_blobs, :service_name, :string + + if configured_service = ActiveStorage::Blob.service.name + ActiveStorage::Blob.unscoped.update_all(service_name: configured_service) + end + + change_column :active_storage_blobs, :service_name, :string, null: false + end + end + + def down + return unless table_exists?(:active_storage_blobs) + + remove_column :active_storage_blobs, :service_name + end +end diff --git a/db/migrate/20230106144439_create_active_storage_variant_records.active_storage.rb b/db/migrate/20230106144439_create_active_storage_variant_records.active_storage.rb new file mode 100644 index 00000000..e1020fc9 --- /dev/null +++ b/db/migrate/20230106144439_create_active_storage_variant_records.active_storage.rb @@ -0,0 +1,28 @@ +# This migration comes from active_storage (originally 20191206030411) +class CreateActiveStorageVariantRecords < ActiveRecord::Migration[6.0] + def change + return unless table_exists?(:active_storage_blobs) + + # Use Active Record's configured type for primary key + create_table :active_storage_variant_records, id: primary_key_type, if_not_exists: true do |t| + t.belongs_to :blob, null: false, index: false, type: blobs_primary_key_type + t.string :variation_digest, null: false + + t.index [:blob_id, :variation_digest], name: "index_active_storage_variant_records_uniqueness", unique: true + t.foreign_key :active_storage_blobs, column: :blob_id + end + end + + private + + def primary_key_type + config = Rails.configuration.generators + config.options[config.orm][:primary_key_type] || :primary_key + end + + def blobs_primary_key_type + pkey_name = connection.primary_key(:active_storage_blobs) + pkey_column = connection.columns(:active_storage_blobs).find { |c| c.name == pkey_name } + pkey_column.bigint? ? :bigint : pkey_column.type + end +end diff --git a/db/migrate/20230106144440_remove_not_null_on_active_storage_blobs_checksum.active_storage.rb b/db/migrate/20230106144440_remove_not_null_on_active_storage_blobs_checksum.active_storage.rb new file mode 100644 index 00000000..93c8b85a --- /dev/null +++ b/db/migrate/20230106144440_remove_not_null_on_active_storage_blobs_checksum.active_storage.rb @@ -0,0 +1,8 @@ +# This migration comes from active_storage (originally 20211119233751) +class RemoveNotNullOnActiveStorageBlobsChecksum < ActiveRecord::Migration[6.0] + def change + return unless table_exists?(:active_storage_blobs) + + change_column_null(:active_storage_blobs, :checksum, true) + end +end diff --git a/db/schema.rb b/db/schema.rb index ce812b3f..50c24c41 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -2,54 +2,60 @@ # of editing this file, please use the migrations feature of Active Record to # incrementally modify your database, and then regenerate this schema definition. # -# Note that this schema.rb definition is the authoritative source for your -# database schema. If you need to create the application database on another -# system, you should be using db:schema:load, not running all the migrations -# from scratch. The latter is a flawed and unsustainable approach (the more migrations -# you'll amass, the slower it'll run and the greater likelihood for issues). +# This file is the source Rails uses to define your schema when running `bin/rails +# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to +# be faster and is potentially less error prone than running all of your +# migrations from scratch. Old migrations may fail to apply correctly if those +# migrations use external dependencies or application code. # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2021_02_05_090257) do - - create_table "active_storage_attachments", id: :integer, force: :cascade do |t| +ActiveRecord::Schema[7.0].define(version: 2023_01_06_144440) do + create_table "active_storage_attachments", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.string "name", null: false t.string "record_type", null: false t.bigint "record_id", null: false t.bigint "blob_id", null: false - t.datetime "created_at", null: false + t.datetime "created_at", precision: nil, null: false t.index ["blob_id"], name: "index_active_storage_attachments_on_blob_id" t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true end - create_table "active_storage_blobs", id: :integer, force: :cascade do |t| + create_table "active_storage_blobs", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.string "key", null: false t.string "filename", null: false t.string "content_type" t.text "metadata" t.bigint "byte_size", null: false - t.string "checksum", null: false - t.datetime "created_at", null: false + t.string "checksum" + t.datetime "created_at", precision: nil, null: false + t.string "service_name", null: false t.index ["key"], name: "index_active_storage_blobs_on_key", unique: true end - create_table "article_categories", id: :integer, force: :cascade do |t| + create_table "active_storage_variant_records", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| + t.integer "blob_id", null: false + t.string "variation_digest", null: false + t.index ["blob_id", "variation_digest"], name: "index_active_storage_variant_records_uniqueness", unique: true + end + + create_table "article_categories", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.string "name", default: "", null: false t.string "description" t.index ["name"], name: "index_article_categories_on_name", unique: true end - create_table "article_prices", id: :integer, force: :cascade do |t| + create_table "article_prices", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.integer "article_id", null: false t.decimal "price", precision: 8, scale: 2, default: "0.0", null: false t.decimal "tax", precision: 8, scale: 2, default: "0.0", null: false t.decimal "deposit", precision: 8, scale: 2, default: "0.0", null: false t.integer "unit_quantity" - t.datetime "created_at" + t.datetime "created_at", precision: nil t.index ["article_id"], name: "index_article_prices_on_article_id" end - create_table "articles", id: :integer, force: :cascade do |t| + create_table "articles", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.string "name", default: "", null: false t.integer "supplier_id", default: 0, null: false t.integer "article_category_id", default: 0, null: false @@ -58,15 +64,15 @@ ActiveRecord::Schema.define(version: 2021_02_05_090257) do t.boolean "availability", default: true, null: false t.string "manufacturer" t.string "origin" - t.datetime "shared_updated_on" + t.datetime "shared_updated_on", precision: nil t.decimal "price", precision: 8, scale: 2 t.float "tax" t.decimal "deposit", precision: 8, scale: 2, default: "0.0" t.integer "unit_quantity", default: 1, null: false t.string "order_number" - t.datetime "created_at" - t.datetime "updated_at" - t.datetime "deleted_at" + t.datetime "created_at", precision: nil + t.datetime "updated_at", precision: nil + t.datetime "deleted_at", precision: nil t.string "type" t.integer "quantity", default: 0 t.index ["article_category_id"], name: "index_articles_on_article_category_id" @@ -75,31 +81,31 @@ ActiveRecord::Schema.define(version: 2021_02_05_090257) do t.index ["type"], name: "index_articles_on_type" end - create_table "assignments", id: :integer, force: :cascade do |t| + create_table "assignments", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.integer "user_id", default: 0, null: false t.integer "task_id", default: 0, null: false t.boolean "accepted", default: false t.index ["user_id", "task_id"], name: "index_assignments_on_user_id_and_task_id", unique: true end - create_table "bank_accounts", id: :integer, force: :cascade do |t| + create_table "bank_accounts", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.string "name", null: false t.string "iban" t.string "description" t.decimal "balance", precision: 12, scale: 2, default: "0.0", null: false - t.datetime "last_import" + t.datetime "last_import", precision: nil t.string "import_continuation_point" t.integer "bank_gateway_id" end - create_table "bank_gateways", id: :integer, force: :cascade do |t| + create_table "bank_gateways", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.string "name", null: false t.string "url", null: false t.string "authorization" t.integer "unattended_user_id" end - create_table "bank_transactions", id: :integer, force: :cascade do |t| + create_table "bank_transactions", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.integer "bank_account_id", null: false t.string "external_id" t.date "date" @@ -108,32 +114,32 @@ ActiveRecord::Schema.define(version: 2021_02_05_090257) do t.string "reference" t.text "text" t.text "receipt" - t.binary "image", limit: 16777215 + t.binary "image", size: :medium t.integer "financial_link_id" t.index ["financial_link_id"], name: "index_bank_transactions_on_financial_link_id" end - create_table "documents", id: :integer, force: :cascade do |t| + create_table "documents", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.string "name" t.string "mime" - t.binary "data", limit: 4294967295 + t.binary "data", size: :long t.integer "created_by_user_id" - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", precision: nil + t.datetime "updated_at", precision: nil t.integer "parent_id" t.index ["parent_id"], name: "index_documents_on_parent_id" end - create_table "financial_links", id: :integer, force: :cascade do |t| + create_table "financial_links", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.text "note" end - create_table "financial_transaction_classes", id: :integer, force: :cascade do |t| + create_table "financial_transaction_classes", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.string "name", null: false t.boolean "ignore_for_account_balance", default: false, null: false end - create_table "financial_transaction_types", id: :integer, force: :cascade do |t| + create_table "financial_transaction_types", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.string "name", null: false t.integer "financial_transaction_class_id", null: false t.string "name_short" @@ -141,12 +147,12 @@ ActiveRecord::Schema.define(version: 2021_02_05_090257) do t.index ["name_short"], name: "index_financial_transaction_types_on_name_short" end - create_table "financial_transactions", id: :integer, force: :cascade do |t| + create_table "financial_transactions", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.integer "ordergroup_id" t.decimal "amount", precision: 8, scale: 2, default: "0.0", null: false t.text "note", null: false t.integer "user_id", default: 0, null: false - t.datetime "created_on", null: false + t.datetime "created_on", precision: nil, null: false t.integer "financial_transaction_type_id", null: false t.integer "financial_link_id" t.integer "reverts_id" @@ -155,20 +161,20 @@ ActiveRecord::Schema.define(version: 2021_02_05_090257) do t.index ["reverts_id"], name: "index_financial_transactions_on_reverts_id", unique: true end - create_table "group_order_article_quantities", id: :integer, force: :cascade do |t| + create_table "group_order_article_quantities", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.integer "group_order_article_id", default: 0, null: false t.integer "quantity", default: 0 t.integer "tolerance", default: 0 - t.datetime "created_on", null: false + t.datetime "created_on", precision: nil, null: false t.index ["group_order_article_id"], name: "index_group_order_article_quantities_on_group_order_article_id" end - create_table "group_order_articles", id: :integer, force: :cascade do |t| + create_table "group_order_articles", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.integer "group_order_id", default: 0, null: false t.integer "order_article_id", default: 0, null: false t.integer "quantity", default: 0, null: false t.integer "tolerance", default: 0, null: false - t.datetime "updated_on", null: false + t.datetime "updated_on", precision: nil, null: false t.decimal "result", precision: 8, scale: 3 t.decimal "result_computed", precision: 8, scale: 3 t.index ["group_order_id", "order_article_id"], name: "goa_index", unique: true @@ -176,12 +182,12 @@ ActiveRecord::Schema.define(version: 2021_02_05_090257) do t.index ["order_article_id"], name: "index_group_order_articles_on_order_article_id" end - create_table "group_orders", id: :integer, force: :cascade do |t| + create_table "group_orders", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.integer "ordergroup_id" t.integer "order_id", default: 0, null: false t.decimal "price", precision: 8, scale: 2, default: "0.0", null: false t.integer "lock_version", default: 0, null: false - t.datetime "updated_on", null: false + t.datetime "updated_on", precision: nil, null: false t.integer "updated_by_user_id" t.decimal "transport", precision: 8, scale: 2 t.index ["order_id"], name: "index_group_orders_on_order_id" @@ -189,18 +195,18 @@ ActiveRecord::Schema.define(version: 2021_02_05_090257) do t.index ["ordergroup_id"], name: "index_group_orders_on_ordergroup_id" end - create_table "groups", id: :integer, force: :cascade do |t| + create_table "groups", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.string "type", default: "", null: false t.string "name", default: "", null: false t.string "description" t.decimal "account_balance", precision: 12, scale: 2, default: "0.0", null: false - t.datetime "created_on", null: false + t.datetime "created_on", precision: nil, null: false t.boolean "role_admin", default: false, null: false t.boolean "role_suppliers", default: false, null: false t.boolean "role_article_meta", default: false, null: false t.boolean "role_finance", default: false, null: false t.boolean "role_orders", default: false, null: false - t.datetime "deleted_at" + t.datetime "deleted_at", precision: nil t.string "contact_person" t.string "contact_phone" t.string "contact_address" @@ -214,16 +220,16 @@ ActiveRecord::Schema.define(version: 2021_02_05_090257) do t.index ["name"], name: "index_groups_on_name", unique: true end - create_table "invites", id: :integer, force: :cascade do |t| + create_table "invites", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.string "token", default: "", null: false - t.datetime "expires_at", null: false + t.datetime "expires_at", precision: nil, null: false t.integer "group_id", default: 0, null: false t.integer "user_id", default: 0, null: false t.string "email", default: "", null: false t.index ["token"], name: "index_invites_on_token" end - create_table "invoices", id: :integer, force: :cascade do |t| + create_table "invoices", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.integer "supplier_id" t.string "number" t.date "date" @@ -232,16 +238,16 @@ ActiveRecord::Schema.define(version: 2021_02_05_090257) do t.decimal "amount", precision: 8, scale: 2, default: "0.0", null: false t.decimal "deposit", precision: 8, scale: 2, default: "0.0", null: false t.decimal "deposit_credit", precision: 8, scale: 2, default: "0.0", null: false - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", precision: nil + t.datetime "updated_at", precision: nil t.integer "created_by_user_id" t.string "attachment_mime" - t.binary "attachment_data", limit: 16777215 + t.binary "attachment_data", size: :medium t.integer "financial_link_id" t.index ["supplier_id"], name: "index_invoices_on_supplier_id" end - create_table "links", id: :integer, force: :cascade do |t| + create_table "links", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.string "name", null: false t.string "url", null: false t.integer "workgroup_id" @@ -249,81 +255,81 @@ ActiveRecord::Schema.define(version: 2021_02_05_090257) do t.string "authorization" end - create_table "mail_delivery_status", id: :integer, force: :cascade do |t| - t.datetime "created_at" + create_table "mail_delivery_status", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| + t.datetime "created_at", precision: nil t.string "email", null: false t.string "message", null: false t.string "attachment_mime" - t.binary "attachment_data", limit: 4294967295 + t.binary "attachment_data", size: :long t.index ["email"], name: "index_mail_delivery_status_on_email" end - create_table "memberships", id: :integer, force: :cascade do |t| + create_table "memberships", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.integer "group_id", default: 0, null: false t.integer "user_id", default: 0, null: false t.index ["user_id", "group_id"], name: "index_memberships_on_user_id_and_group_id", unique: true end - create_table "message_recipients", id: :integer, force: :cascade do |t| + create_table "message_recipients", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.integer "message_id", null: false t.integer "user_id", null: false t.integer "email_state", default: 0, null: false - t.datetime "read_at" + t.datetime "read_at", precision: nil t.index ["message_id"], name: "index_message_recipients_on_message_id" t.index ["user_id", "read_at"], name: "index_message_recipients_on_user_id_and_read_at" end - create_table "messages", id: :integer, force: :cascade do |t| + create_table "messages", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.integer "sender_id" t.string "subject", null: false t.text "body" t.boolean "private", default: false - t.datetime "created_at" + t.datetime "created_at", precision: nil t.integer "reply_to" t.integer "group_id" t.string "salt" - t.binary "received_email", limit: 16777215 + t.binary "received_email", size: :medium end - create_table "oauth_access_grants", id: :integer, force: :cascade do |t| + create_table "oauth_access_grants", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.integer "resource_owner_id", null: false t.integer "application_id", null: false t.string "token", null: false t.integer "expires_in", null: false t.text "redirect_uri", null: false - t.datetime "created_at", null: false - t.datetime "revoked_at" + t.datetime "created_at", precision: nil, null: false + t.datetime "revoked_at", precision: nil t.string "scopes" t.index ["token"], name: "index_oauth_access_grants_on_token", unique: true end - create_table "oauth_access_tokens", id: :integer, force: :cascade do |t| + create_table "oauth_access_tokens", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.integer "resource_owner_id" t.integer "application_id" t.string "token", null: false t.string "refresh_token" t.integer "expires_in" - t.datetime "revoked_at" - t.datetime "created_at", null: false + t.datetime "revoked_at", precision: nil + t.datetime "created_at", precision: nil, null: false t.string "scopes" t.index ["refresh_token"], name: "index_oauth_access_tokens_on_refresh_token", unique: true t.index ["resource_owner_id"], name: "index_oauth_access_tokens_on_resource_owner_id" t.index ["token"], name: "index_oauth_access_tokens_on_token", unique: true end - create_table "oauth_applications", id: :integer, force: :cascade do |t| + create_table "oauth_applications", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.string "name", null: false t.string "uid", null: false t.string "secret", null: false t.text "redirect_uri", null: false t.string "scopes", default: "", null: false - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", precision: nil + t.datetime "updated_at", precision: nil t.boolean "confidential", default: true, null: false t.index ["uid"], name: "index_oauth_applications_on_uid", unique: true end - create_table "order_articles", id: :integer, force: :cascade do |t| + create_table "order_articles", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.integer "order_id", default: 0, null: false t.integer "article_id", default: 0, null: false t.integer "quantity", default: 0, null: false @@ -337,45 +343,45 @@ ActiveRecord::Schema.define(version: 2021_02_05_090257) do t.index ["order_id"], name: "index_order_articles_on_order_id" end - create_table "order_comments", id: :integer, force: :cascade do |t| + create_table "order_comments", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.integer "order_id" t.integer "user_id" t.text "text" - t.datetime "created_at" + t.datetime "created_at", precision: nil t.index ["order_id"], name: "index_order_comments_on_order_id" end - create_table "orders", id: :integer, force: :cascade do |t| + create_table "orders", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.integer "supplier_id" t.text "note" - t.datetime "starts" - t.datetime "ends" + t.datetime "starts", precision: nil + t.datetime "ends", precision: nil t.string "state", default: "open" t.integer "lock_version", default: 0, null: false t.integer "updated_by_user_id" t.decimal "foodcoop_result", precision: 8, scale: 2 t.integer "created_by_user_id" - t.datetime "boxfill" + t.datetime "boxfill", precision: nil t.integer "invoice_id" t.date "pickup" - t.datetime "last_sent_mail" + t.datetime "last_sent_mail", precision: nil t.integer "end_action", default: 0, null: false t.decimal "transport", precision: 8, scale: 2 t.index ["state"], name: "index_orders_on_state" end - create_table "page_versions", id: :integer, force: :cascade do |t| + create_table "page_versions", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.integer "page_id" t.integer "lock_version" t.text "body" t.integer "updated_by" t.integer "redirect" t.integer "parent_id" - t.datetime "updated_at" + t.datetime "updated_at", precision: nil t.index ["page_id"], name: "index_page_versions_on_page_id" end - create_table "pages", id: :integer, force: :cascade do |t| + create_table "pages", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.string "title" t.text "body" t.string "permalink" @@ -383,41 +389,41 @@ ActiveRecord::Schema.define(version: 2021_02_05_090257) do t.integer "updated_by" t.integer "redirect" t.integer "parent_id" - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", precision: nil + t.datetime "updated_at", precision: nil t.index ["permalink"], name: "index_pages_on_permalink" t.index ["title"], name: "index_pages_on_title" end - create_table "periodic_task_groups", id: :integer, force: :cascade do |t| + create_table "periodic_task_groups", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.date "next_task_date" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at", precision: nil, null: false + t.datetime "updated_at", precision: nil, null: false end - create_table "poll_choices", id: :integer, force: :cascade do |t| + create_table "poll_choices", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.integer "poll_vote_id", null: false t.integer "choice", null: false t.integer "value", null: false t.index ["poll_vote_id", "choice"], name: "index_poll_choices_on_poll_vote_id_and_choice", unique: true end - create_table "poll_votes", id: :integer, force: :cascade do |t| + create_table "poll_votes", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.integer "poll_id", null: false t.integer "user_id", null: false t.integer "ordergroup_id" t.text "note" - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", precision: nil + t.datetime "updated_at", precision: nil t.index ["poll_id", "user_id", "ordergroup_id"], name: "index_poll_votes_on_poll_id_and_user_id_and_ordergroup_id", unique: true end - create_table "polls", id: :integer, force: :cascade do |t| + create_table "polls", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.integer "created_by_user_id", null: false t.string "name", null: false t.text "description" - t.datetime "starts" - t.datetime "ends" + t.datetime "starts", precision: nil + t.datetime "ends", precision: nil t.boolean "one_vote_per_ordergroup", default: false, null: false t.text "required_ordergroup_custom_fields" t.text "required_user_custom_fields" @@ -427,66 +433,66 @@ ActiveRecord::Schema.define(version: 2021_02_05_090257) do t.integer "multi_select_count", default: 0, null: false t.integer "min_points" t.integer "max_points" - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", precision: nil + t.datetime "updated_at", precision: nil t.index ["final_choice"], name: "index_polls_on_final_choice" end - create_table "printer_job_updates", id: :integer, force: :cascade do |t| + create_table "printer_job_updates", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.integer "printer_job_id", null: false - t.datetime "created_at", null: false + t.datetime "created_at", precision: nil, null: false t.string "state", null: false t.text "message" t.index ["printer_job_id", "created_at"], name: "index_printer_job_updates_on_printer_job_id_and_created_at" end - create_table "printer_jobs", id: :integer, force: :cascade do |t| + create_table "printer_jobs", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.integer "order_id" t.string "document", null: false t.integer "created_by_user_id", null: false t.integer "finished_by_user_id" - t.datetime "finished_at" + t.datetime "finished_at", precision: nil t.index ["finished_at"], name: "index_printer_jobs_on_finished_at" end - create_table "settings", id: :integer, force: :cascade do |t| + create_table "settings", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.string "var", null: false t.text "value" t.integer "thing_id" t.string "thing_type", limit: 30 - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at", precision: nil, null: false + t.datetime "updated_at", precision: nil, null: false t.index ["thing_type", "thing_id", "var"], name: "index_settings_on_thing_type_and_thing_id_and_var", unique: true end - create_table "stock_changes", id: :integer, force: :cascade do |t| + create_table "stock_changes", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.integer "stock_event_id" t.integer "order_id" t.integer "stock_article_id" t.integer "quantity", default: 0 - t.datetime "created_at" + t.datetime "created_at", precision: nil t.index ["stock_article_id"], name: "index_stock_changes_on_stock_article_id" t.index ["stock_event_id"], name: "index_stock_changes_on_stock_event_id" end - create_table "stock_events", id: :integer, force: :cascade do |t| + create_table "stock_events", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.integer "supplier_id" t.date "date" - t.datetime "created_at" + t.datetime "created_at", precision: nil t.text "note" t.integer "invoice_id" t.string "type", null: false t.index ["supplier_id"], name: "index_stock_events_on_supplier_id" end - create_table "supplier_categories", id: :integer, force: :cascade do |t| + create_table "supplier_categories", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.string "name", null: false t.string "description" t.integer "financial_transaction_class_id" t.integer "bank_account_id" end - create_table "suppliers", id: :integer, force: :cascade do |t| + create_table "suppliers", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.string "name", default: "", null: false t.string "address", default: "", null: false t.string "phone", default: "", null: false @@ -501,21 +507,21 @@ ActiveRecord::Schema.define(version: 2021_02_05_090257) do t.string "note" t.integer "shared_supplier_id" t.string "min_order_quantity" - t.datetime "deleted_at" + t.datetime "deleted_at", precision: nil t.string "shared_sync_method" t.string "iban" t.integer "supplier_category_id" t.index ["name"], name: "index_suppliers_on_name", unique: true end - create_table "tasks", id: :integer, force: :cascade do |t| + create_table "tasks", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.string "name", default: "", null: false t.text "description" t.date "due_date" t.boolean "done", default: false t.integer "workgroup_id" - t.datetime "created_on", null: false - t.datetime "updated_on", null: false + t.datetime "created_on", precision: nil, null: false + t.datetime "updated_on", precision: nil, null: false t.integer "required_users", default: 1 t.integer "duration", default: 1 t.integer "periodic_task_group_id" @@ -525,7 +531,7 @@ ActiveRecord::Schema.define(version: 2021_02_05_090257) do t.index ["workgroup_id"], name: "index_tasks_on_workgroup_id" end - create_table "users", id: :integer, force: :cascade do |t| + create_table "users", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.string "nick" t.string "password_hash", default: "", null: false t.string "password_salt", default: "", null: false @@ -533,15 +539,16 @@ ActiveRecord::Schema.define(version: 2021_02_05_090257) do t.string "last_name", default: "", null: false t.string "email", default: "", null: false t.string "phone" - t.datetime "created_on", null: false + t.datetime "created_on", precision: nil, null: false t.string "reset_password_token" - t.datetime "reset_password_expires" - t.datetime "last_login" - t.datetime "last_activity" - t.datetime "deleted_at" + t.datetime "reset_password_expires", precision: nil + t.datetime "last_login", precision: nil + t.datetime "last_activity", precision: nil + t.datetime "deleted_at", precision: nil t.string "iban" t.index ["email"], name: "index_users_on_email", unique: true t.index ["nick"], name: "index_users_on_nick", unique: true end + add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id" end diff --git a/db/seeds/seed_helper.rb b/db/seeds/seed_helper.rb index 574be356..a1f958bf 100644 --- a/db/seeds/seed_helper.rb +++ b/db/seeds/seed_helper.rb @@ -8,10 +8,10 @@ def seed_group_orders # order 3..12 times a random article go = og.group_orders.create!(order: order, updated_by_user_id: 1) - (3 + rand(10)).times do + (rand(10) + 3).times do goa = go.group_order_articles.find_or_create_by!(order_article: order.order_articles.offset(rand(noas)).first) unit_quantity = goa.order_article.price.unit_quantity - goa.update_quantities rand([4, 2 * unit_quantity + 2].max), rand(unit_quantity) + goa.update_quantities rand([4, unit_quantity * 2 + 2].max), rand(unit_quantity) end end # update totals diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index 0a8b3fec..5358430a 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -11,6 +11,7 @@ services: build: context: . dockerfile: Dockerfile-dev + platform: linux/x86_64 command: ./proc-start worker volumes: - bundle:/usr/local/bundle diff --git a/spec/controllers/articles_controller_spec.rb b/spec/controllers/articles_controller_spec.rb index dae89c70..b8772054 100644 --- a/spec/controllers/articles_controller_spec.rb +++ b/spec/controllers/articles_controller_spec.rb @@ -264,7 +264,7 @@ describe ArticlesController, type: :controller do it 'does not update articles if article with same name exists' do get_with_supplier :update_synchronized, params: { articles: { article_a.id => { unit: '2000 g' }, article_b.id => { name: 'AAAA' } } } error_array = [assigns(:updated_articles).first.errors.first, assigns(:updated_articles).last.errors.first] - expect(error_array).to include([:name, 'name is already taken']) + expect(error_array).to include(ActiveModel::Error) expect(response).to have_http_status(:success) end From a7747c9e84ba8df5d5199f3d87dc7fbe2c88c72c Mon Sep 17 00:00:00 2001 From: FGU Date: Thu, 2 Feb 2023 10:14:26 +0100 Subject: [PATCH 005/105] fix docker-compose --- docker-compose-dev.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index 5358430a..b0a325db 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -11,7 +11,7 @@ services: build: context: . dockerfile: Dockerfile-dev - platform: linux/x86_64 + platform: linux/x86_64 command: ./proc-start worker volumes: - bundle:/usr/local/bundle From c487f0368aae4141705027fcd2f259919c8aa8b7 Mon Sep 17 00:00:00 2001 From: Philipp Rothmann Date: Mon, 16 Jan 2023 15:31:13 +0100 Subject: [PATCH 006/105] upgrade dockerfile to rails7 --- Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index c999b3d4..9509c4d3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM ruby:2.6 +FROM ruby:2.7 RUN supercronicUrl=https://github.com/aptible/supercronic/releases/download/v0.1.3/supercronic-linux-amd64 && \ supercronicBin=/usr/local/bin/supercronic && \ @@ -22,6 +22,7 @@ RUN buildDeps='libmagic-dev' && \ apt-get update && \ apt-get install --no-install-recommends -y $buildDeps && \ echo 'gem: --no-document' >> ~/.gemrc && \ + gem install bundler && \ bundle config build.nokogiri "--use-system-libraries" && \ bundle install --deployment --without development test -j 4 && \ apt-get purge -y --auto-remove $buildDeps && \ From 82d4ff028443ebb56eaf9b56518022e80e888dca Mon Sep 17 00:00:00 2001 From: Philipp Rothmann Date: Mon, 16 Jan 2023 21:10:09 +0100 Subject: [PATCH 007/105] improve dockerfile caching --- Dockerfile | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 9509c4d3..95479ce2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,7 +15,9 @@ ENV PORT=3000 \ WORKDIR /usr/src/app -COPY . ./ +COPY Gemfile Gemfile.lock ./ +COPY plugins/ ./plugins +COPY config/ ./config # install dependencies and generate crontab RUN buildDeps='libmagic-dev' && \ @@ -30,6 +32,8 @@ RUN buildDeps='libmagic-dev' && \ \ bundle exec whenever >crontab +COPY . ./ + # compile assets with temporary mysql server RUN export DATABASE_URL=mysql2://localhost/temp?encoding=utf8 && \ export SECRET_KEY_BASE=thisisnotimportantnow && \ From 666e7934a68ba9f652513227472fff5a2e042a03 Mon Sep 17 00:00:00 2001 From: Philipp Rothmann Date: Fri, 27 Jan 2023 11:16:53 +0100 Subject: [PATCH 008/105] introduce importmaps This commit introduces importmaps. They allow to use modern javacript ESM within rails without webpack, yarn etc. see https://github.com/rails/importmap-rails for more details. Co-authored-by: Philipp Rothmann Co-authored-by: FGU --- Gemfile | 2 ++ Gemfile.lock | 6 +++++- .../javascripts/{application.js => application_legacy.js} | 0 app/javascript/application.js | 1 + app/views/layouts/_header.html.haml | 6 ++++-- bin/importmap | 4 ++++ config/importmap.rb | 2 ++ config/initializers/assets.rb | 2 +- vendor/javascript/.keep | 0 9 files changed, 19 insertions(+), 4 deletions(-) rename app/assets/javascripts/{application.js => application_legacy.js} (100%) create mode 100644 app/javascript/application.js create mode 100755 bin/importmap create mode 100644 config/importmap.rb create mode 100644 vendor/javascript/.keep diff --git a/Gemfile b/Gemfile index 42ac26db..4d71513f 100644 --- a/Gemfile +++ b/Gemfile @@ -126,3 +126,5 @@ group :test do gem 'rswag-specs' gem 'hashie', '~> 3.4.6', require: false # https://github.com/westfieldlabs/apivore/issues/114 end + +gem "importmap-rails", "~> 1.1" diff --git a/Gemfile.lock b/Gemfile.lock index f55e3397..88fa2944 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -255,6 +255,9 @@ GEM i18n-spec (0.6.0) iso ice_cube (0.16.4) + importmap-rails (1.1.5) + actionpack (>= 6.0.0) + railties (>= 6.0.0) inherited_resources (1.13.1) actionpack (>= 5.2, < 7.1) has_scope (~> 0.6) @@ -628,6 +631,7 @@ DEPENDENCIES i18n-js (~> 3.0.0.rc8) i18n-spec ice_cube + importmap-rails (~> 1.1) inherited_resources jquery-rails kaminari @@ -686,4 +690,4 @@ DEPENDENCIES whenever BUNDLED WITH - 2.4.3 + 2.4.5 diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application_legacy.js similarity index 100% rename from app/assets/javascripts/application.js rename to app/assets/javascripts/application_legacy.js diff --git a/app/javascript/application.js b/app/javascript/application.js new file mode 100644 index 00000000..beff742e --- /dev/null +++ b/app/javascript/application.js @@ -0,0 +1 @@ +// Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails diff --git a/app/views/layouts/_header.html.haml b/app/views/layouts/_header.html.haml index 974ce8f2..66e14355 100644 --- a/app/views/layouts/_header.html.haml +++ b/app/views/layouts/_header.html.haml @@ -8,10 +8,10 @@ = csrf_meta_tags = stylesheet_link_tag "application", :media => "all" //%link(href="images/favicon.ico" rel="shortcut icon") - = yield(:head) = foodcoop_css_tag + %body = yield @@ -19,7 +19,9 @@ Javascripts \================================================== / Placed at the end of the document so the pages load faster - = javascript_include_tag "application" + = javascript_importmap_tags + = javascript_include_tag "application_legacy" + :javascript I18n.defaultLocale = "#{I18n.default_locale}"; I18n.locale = "#{I18n.locale}"; diff --git a/bin/importmap b/bin/importmap new file mode 100755 index 00000000..36502ab1 --- /dev/null +++ b/bin/importmap @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby + +require_relative "../config/application" +require "importmap/commands" diff --git a/config/importmap.rb b/config/importmap.rb new file mode 100644 index 00000000..050818ab --- /dev/null +++ b/config/importmap.rb @@ -0,0 +1,2 @@ +# Pin npm packages by running ./bin/importmap +pin "application", preload: true \ No newline at end of file diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb index fe48fc34..e1c4d5fa 100644 --- a/config/initializers/assets.rb +++ b/config/initializers/assets.rb @@ -9,4 +9,4 @@ Rails.application.config.assets.version = '1.0' # Precompile additional assets. # application.js, application.css, and all non-JS/CSS in the app/assets # folder are already added. -# Rails.application.config.assets.precompile += %w( admin.js admin.css ) +Rails.application.config.assets.precompile += %w( application_legacy.js jquery.min.js ) diff --git a/vendor/javascript/.keep b/vendor/javascript/.keep new file mode 100644 index 00000000..e69de29b From 78da4feafe2efeebd4ee295ac51d634b4a4aff57 Mon Sep 17 00:00:00 2001 From: Philipp Rothmann Date: Fri, 10 Feb 2023 12:50:59 +0100 Subject: [PATCH 009/105] fix: assets precompile by using terser importmaps broke precompiliation with uglifier see: https://github.com/rails/importmap-rails/issues/5 --- Gemfile | 3 ++- Gemfile.lock | 6 +++--- config/environments/production.rb | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Gemfile b/Gemfile index 4d71513f..01c2cfd7 100644 --- a/Gemfile +++ b/Gemfile @@ -7,7 +7,6 @@ gem 'mail', '~> 2.7.1' # bug with mail 2.8.0 https://github.com/mikel/mail/issue gem 'sassc-rails' gem 'less-rails' -gem 'uglifier' # See https://github.com/sstephenson/execjs#readme for more supported runtimes gem 'therubyracer', platforms: :ruby @@ -128,3 +127,5 @@ group :test do end gem "importmap-rails", "~> 1.1" + +gem "terser", "~> 1.1" diff --git a/Gemfile.lock b/Gemfile.lock index 88fa2944..5b1a9fe7 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -547,6 +547,8 @@ GEM sqlite3 (>= 1.3.3) table_print (1.5.7) temple (0.9.1) + terser (1.1.13) + execjs (>= 0.3.0, < 3) therubyracer (0.12.3) libv8 (~> 3.16.14.15) ref @@ -567,8 +569,6 @@ GEM unf (~> 0.1.0) tzinfo (2.0.5) concurrent-ruby (~> 1.0) - uglifier (4.2.0) - execjs (>= 0.3.0, < 3) unf (0.1.4) unf_ext unf_ext (0.0.8.2) @@ -683,9 +683,9 @@ DEPENDENCIES sprockets (< 4) sqlite3 (~> 1.3.6) table_print + terser (~> 1.1) therubyracer twitter-bootstrap-rails (~> 2.2.8) - uglifier web-console whenever diff --git a/config/environments/production.rb b/config/environments/production.rb index d0f06b95..d08234e5 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -29,7 +29,7 @@ Rails.application.configure do config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present? # Compress JavaScripts and CSS. - config.assets.js_compressor = :uglifier + config.assets.js_compressor = :terser config.assets.css_compressor = :sass # Do not fallback to assets pipeline if a precompiled asset is missed. From 5c04a43f61327973ae06e7481baefaeeae96ac0a Mon Sep 17 00:00:00 2001 From: viehlieb Date: Mon, 20 Feb 2023 19:56:45 +0100 Subject: [PATCH 010/105] update article category implemented adapt tests add translations adapt test fix bug --- app/controllers/articles_controller.rb | 11 ++++---- app/models/article.rb | 32 +++++++++++++----------- app/models/supplier.rb | 10 ++++++-- app/views/articles/_sync_table.html.haml | 3 ++- app/views/articles/upload.html.haml | 3 +++ config/locales/de.yml | 1 + config/locales/en.yml | 1 + config/locales/es.yml | 1 + config/locales/nl.yml | 1 + spec/integration/articles_spec.rb | 25 ++++++++++++++++++ 10 files changed, 66 insertions(+), 22 deletions(-) diff --git a/app/controllers/articles_controller.rb b/app/controllers/articles_controller.rb index 4161e66a..31481f18 100644 --- a/app/controllers/articles_controller.rb +++ b/app/controllers/articles_controller.rb @@ -46,6 +46,11 @@ class ArticlesController < ApplicationController render :layout => false end + def edit + @article = Article.find(params[:id]) + render :action => 'new', :layout => false + end + def create @article = Article.new(params[:article]) if @article.valid? && @article.save @@ -55,11 +60,6 @@ class ArticlesController < ApplicationController end end - def edit - @article = Article.find(params[:id]) - render :action => 'new', :layout => false - end - # Updates one Article and highlights the line if succeded def update @article = Article.find(params[:id]) @@ -151,6 +151,7 @@ class ArticlesController < ApplicationController options = { filename: uploaded_file.original_filename } options[:outlist_absent] = (params[:articles]['outlist_absent'] == '1') options[:convert_units] = (params[:articles]['convert_units'] == '1') + options[:update_category] = (params[:articles]['update_category'] == '1') @updated_article_pairs, @outlisted_articles, @new_articles = @supplier.sync_from_file uploaded_file.tempfile, options if @updated_article_pairs.empty? && @outlisted_articles.empty? && @new_articles.empty? redirect_to supplier_articles_path(@supplier), :notice => I18n.t('articles.controller.parse_upload.notice') diff --git a/app/models/article.rb b/app/models/article.rb index 76a68605..1eca49cd 100644 --- a/app/models/article.rb +++ b/app/models/article.rb @@ -143,20 +143,24 @@ class Article < ApplicationRecord new_unit = new_article.unit end - return Article.compare_attributes( - { - :name => [self.name, new_article.name], - :manufacturer => [self.manufacturer, new_article.manufacturer.to_s], - :origin => [self.origin, new_article.origin], - :unit => [self.unit, new_unit], - :price => [self.price.to_f.round(2), new_price.to_f.round(2)], - :tax => [self.tax, new_article.tax], - :deposit => [self.deposit.to_f.round(2), new_article.deposit.to_f.round(2)], - # take care of different num-objects. - :unit_quantity => [self.unit_quantity.to_s.to_f, new_unit_quantity.to_s.to_f], - :note => [self.note.to_s, new_article.note.to_s] - } - ) + attribute_hash = { + :name => [self.name, new_article.name], + :manufacturer => [self.manufacturer, new_article.manufacturer.to_s], + :origin => [self.origin, new_article.origin], + :unit => [self.unit, new_unit], + :price => [self.price.to_f.round(2), new_price.to_f.round(2)], + :tax => [self.tax, new_article.tax], + :deposit => [self.deposit.to_f.round(2), new_article.deposit.to_f.round(2)], + # take care of different num-objects. + :unit_quantity => [self.unit_quantity.to_s.to_f, new_unit_quantity.to_s.to_f], + :note => [self.note.to_s, new_article.note.to_s] + } + if options[:update_category] == true + new_article_category = new_article.article_category + attribute_hash[:article_category] = [self.article_category, new_article_category] unless new_article_category.blank? + end + + Article.compare_attributes(attribute_hash) end # Compare attributes from two different articles. diff --git a/app/models/supplier.rb b/app/models/supplier.rb index 862f5c24..a4a7456e 100644 --- a/app/models/supplier.rb +++ b/app/models/supplier.rb @@ -81,7 +81,13 @@ class Supplier < ApplicationRecord updated_article_pairs, outlisted_articles, new_articles = [], [], [] FoodsoftFile::parse file, options do |status, new_attrs, line| article = articles.undeleted.where(order_number: new_attrs[:order_number]).first - new_attrs[:article_category] = ArticleCategory.find_match(new_attrs[:article_category]) + + if new_attrs[:article_category].present? && options[:update_category] + new_attrs[:article_category] = ArticleCategory.find_match(new_attrs[:article_category]) || ArticleCategory.create_or_find_by!(name: new_attrs[:article_category]) + else + new_attrs[:article_category] = nil + end + new_attrs[:tax] ||= FoodsoftConfig[:tax_default] new_article = articles.build(new_attrs) @@ -89,7 +95,7 @@ class Supplier < ApplicationRecord if article.nil? new_articles << new_article else - unequal_attributes = article.unequal_attributes(new_article, options.slice(:convert_units)) + unequal_attributes = article.unequal_attributes(new_article, options.slice(:convert_units, :update_category)) unless unequal_attributes.empty? article.attributes = unequal_attributes updated_article_pairs << [article, unequal_attributes] diff --git a/app/views/articles/_sync_table.html.haml b/app/views/articles/_sync_table.html.haml index ac17adfa..62640cbe 100644 --- a/app/views/articles/_sync_table.html.haml +++ b/app/views/articles/_sync_table.html.haml @@ -49,7 +49,8 @@ .input-prepend %span.add-on= t 'number.currency.format.unit' = form.text_field 'deposit', class: 'input-mini', style: 'width: 45px' - %td= form.select :article_category_id, ArticleCategory.all.map {|a| [ a.name, a.id ] }, + %td{:style => highlight_new(attrs, :article_category)} + = form.select :article_category_id, ArticleCategory.all.map {|a| [ a.name, a.id ] }, {include_blank: true}, class: 'input-small' - unless changed_article.errors.empty? %tr.alert diff --git a/app/views/articles/upload.html.haml b/app/views/articles/upload.html.haml index 8f91d790..221e0d1a 100644 --- a/app/views/articles/upload.html.haml +++ b/app/views/articles/upload.html.haml @@ -76,6 +76,9 @@ = f.file_field "file" .control-group + %label(for="articles_update_category") + = f.check_box "update_category" + = t '.options.update_category' %label(for="articles_outlist_absent") = f.check_box "outlist_absent" = t '.options.outlist_absent' diff --git a/config/locales/de.yml b/config/locales/de.yml index 5a1a5b35..3e4e54d5 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -568,6 +568,7 @@ de: options: convert_units: Derzeitige Einheiten beibehalten, berechne Mengeneinheit und Preis (wie Synchronisieren). outlist_absent: Artikel löschen, die nicht in der hochgeladenen Datei sind. + update_category: Kategorien aus der Datei übernehmen und erstellen. sample: juices: Säfte nuts: Nüsse diff --git a/config/locales/en.yml b/config/locales/en.yml index 59e94385..39053a82 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -569,6 +569,7 @@ en: options: convert_units: Keep current units, recompute unit quantity and price (like synchronize). outlist_absent: Delete articles not in uploaded file. + update_category: Create and replace categories from uploaded file. sample: juices: Juices nuts: Nuts diff --git a/config/locales/es.yml b/config/locales/es.yml index 620ec3bb..6363c9d3 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -515,6 +515,7 @@ es: options: convert_units: Mantener unidades actuales, recomputar la cantidad y precio de unidades (como sincronizar). outlist_absent: Borrar artículos que no están en el archivo subido. + update_category: Toma las categorías del archivo subido. sample: juices: Jugos nuts: Nueces diff --git a/config/locales/nl.yml b/config/locales/nl.yml index 4c97dda4..1af1e65d 100644 --- a/config/locales/nl.yml +++ b/config/locales/nl.yml @@ -539,6 +539,7 @@ nl: options: convert_units: Bestaande eenheden behouden, herbereken groothandelseenheid en prijs (net als synchronizeren). outlist_absent: Artikelen die niet in het bestand voorkomen, verwijderen. + upload_category: Categorieën overnemen uit bestand. sample: juices: Sappen nuts: Noten diff --git a/spec/integration/articles_spec.rb b/spec/integration/articles_spec.rb index bbd5e375..1967a617 100644 --- a/spec/integration/articles_spec.rb +++ b/spec/integration/articles_spec.rb @@ -90,6 +90,31 @@ feature ArticlesController do end end + describe "takes over category from file" do + it do + find(:css, '#articles_update_category[value="1"]').set(true) # check take over category from file + expect(ArticleCategory.count).to eq 1 # new Category vegetables should be created from file + find('input[type="submit"]').click # upload file + find('input[type="submit"]').click # submit changes + expect(ArticleCategory.count).to eq 2 # it is + expect(supplier.articles.count).to eq 1 + expect(supplier.articles.first.article_category.name).to eq "Vegetables" + end + end + + describe "overwrites article_category from file" do + let!(:article_category) { create(:article_category, name: "Fruit") } + let(:article) { create(:article, supplier: supplier, name: 'Tomatoes', order_number: 1, unit: '250 g', article_category: article_category) } + + it do + find(:css, '#articles_update_category[value="1"]').set(true) # check take over category from file + find('input[type="submit"]').click #upload file + find('input[type="submit"]').click #submit changes + expect(supplier.articles.count).to eq 1 + expect(supplier.articles.first.article_category.name).to eq "Vegetables" + end + end + describe "can remove an existing article" do let!(:article) { create :article, supplier: supplier, name: 'Foobar', order_number: 99999 } From 46e3794a4e0420c2f48b871c45cf1235c09ae9d0 Mon Sep 17 00:00:00 2001 From: viehlieb Date: Tue, 14 Feb 2023 12:24:11 +0100 Subject: [PATCH 011/105] change .search to .ransack for updated ransack gem --- app/controllers/finance/financial_transactions_controller.rb | 2 +- .../app/controllers/current_orders/articles_controller.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/finance/financial_transactions_controller.rb b/app/controllers/finance/financial_transactions_controller.rb index 930acebe..e0c53e19 100644 --- a/app/controllers/finance/financial_transactions_controller.rb +++ b/app/controllers/finance/financial_transactions_controller.rb @@ -18,7 +18,7 @@ class Finance::FinancialTransactionsController < ApplicationController sort = "created_on DESC" end - @q = FinancialTransaction.search(params[:q]) + @q = FinancialTransaction.ransack(params[:q]) @financial_transactions_all = @q.result(distinct: true).includes(:user).order(sort) @financial_transactions_all = @financial_transactions_all.visible unless params[:show_hidden] @financial_transactions_all = @financial_transactions_all.where(ordergroup_id: @ordergroup.id) if @ordergroup diff --git a/plugins/current_orders/app/controllers/current_orders/articles_controller.rb b/plugins/current_orders/app/controllers/current_orders/articles_controller.rb index 0e4b7dd9..ef23f332 100644 --- a/plugins/current_orders/app/controllers/current_orders/articles_controller.rb +++ b/plugins/current_orders/app/controllers/current_orders/articles_controller.rb @@ -32,7 +32,7 @@ class CurrentOrders::ArticlesController < ApplicationController else @order_articles = OrderArticle.where(order_id: @current_orders.all.map(&:id)) end - @q = OrderArticle.search(params[:q]) + @q = OrderArticle.ransack(params[:q]) @order_articles = @order_articles.ordered.merge(@q.result).includes(:article, :article_price) @order_article = @order_articles.where(id: params[:id]).first end From 0bd04fba41f62e3f8bb38a9e3c66dc571aa11e7f Mon Sep 17 00:00:00 2001 From: viehlieb Date: Tue, 14 Feb 2023 12:25:41 +0100 Subject: [PATCH 012/105] move BigDecimal.new to BigDecimal() --- app/models/group_order.rb | 4 ++-- config/initializers/extensions.rb | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/models/group_order.rb b/app/models/group_order.rb index c789ef4e..f3153c44 100644 --- a/app/models/group_order.rb +++ b/app/models/group_order.rb @@ -32,8 +32,8 @@ class GroupOrder < ApplicationRecord # Generate some data for the javascript methods in ordering view def load_data data = {} - data[:account_balance] = ordergroup.nil? ? BigDecimal.new('+Infinity') : ordergroup.account_balance - data[:available_funds] = ordergroup.nil? ? BigDecimal.new('+Infinity') : ordergroup.get_available_funds(self) + data[:account_balance] = ordergroup.nil? ? BigDecimal('+Infinity') : ordergroup.account_balance + data[:available_funds] = ordergroup.nil? ? BigDecimal('+Infinity') : ordergroup.get_available_funds(self) # load prices and other stuff.... data[:order_articles] = {} diff --git a/config/initializers/extensions.rb b/config/initializers/extensions.rb index 799f52e6..68c7c8f4 100644 --- a/config/initializers/extensions.rb +++ b/config/initializers/extensions.rb @@ -3,7 +3,7 @@ class String # remove comma from decimal inputs def self.delocalized_decimal(string) if !string.blank? and string.is_a?(String) - BigDecimal.new(string.sub(',', '.')) + BigDecimal(string.sub(',', '.')) else string end From 4bb724495da77fa20b13b684735da5d0f8fe8fde Mon Sep 17 00:00:00 2001 From: viehlieb Date: Thu, 23 Feb 2023 00:18:25 +0100 Subject: [PATCH 013/105] downgrade haml to make deface work --- Gemfile | 2 +- Gemfile.lock | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/Gemfile b/Gemfile index 01c2cfd7..e1ed43a7 100644 --- a/Gemfile +++ b/Gemfile @@ -23,7 +23,7 @@ gem 'bootsnap', require: false gem 'mysql2' gem 'prawn' gem 'prawn-table' -gem 'haml' +gem 'haml', '~> 5.0' gem 'haml-rails' gem 'kaminari' gem 'simple_form' diff --git a/Gemfile.lock b/Gemfile.lock index 5b1a9fe7..cfda4050 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -234,9 +234,8 @@ GEM rails (>= 4.0.0) globalid (1.0.0) activesupport (>= 5.0) - haml (6.1.1) - temple (>= 0.8.2) - thor + haml (5.2.2) + temple (>= 0.8.0) tilt haml-rails (2.1.0) actionpack (>= 5.1) @@ -625,7 +624,7 @@ DEPENDENCIES foodsoft_polls! foodsoft_wiki! gaffe - haml + haml (~> 5.0) haml-rails hashie (~> 3.4.6) i18n-js (~> 3.0.0.rc8) From e6e2cdc2c62286c9c7043e048f258f97e64b5d7a Mon Sep 17 00:00:00 2001 From: Philipp Rothmann Date: Thu, 5 Jan 2023 13:46:28 +0100 Subject: [PATCH 014/105] add: drone ci fix: ci fix: .drone docker rails version add .drone caching fix drone ci --- .drone.yml | 145 +++++++++++++++++++++++++ deployment/.env.sample | 65 ++++++++++++ deployment/app_config.yml.tmpl | 168 +++++++++++++++++++++++++++++ deployment/compose.yml | 189 +++++++++++++++++++++++++++++++++ deployment/database.yml.tmpl | 9 ++ deployment/entrypoint.sh.tmpl | 44 ++++++++ 6 files changed, 620 insertions(+) create mode 100644 .drone.yml create mode 100644 deployment/.env.sample create mode 100644 deployment/app_config.yml.tmpl create mode 100644 deployment/compose.yml create mode 100644 deployment/database.yml.tmpl create mode 100644 deployment/entrypoint.sh.tmpl diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 00000000..44065eaa --- /dev/null +++ b/.drone.yml @@ -0,0 +1,145 @@ +kind: pipeline +type: docker +name: build and test + +steps: + - name: rubocop + image: circleci/ruby:2.7-bullseye-node-browsers-legacy + commands: + - sudo apt install --no-install-recommends -y libmagic-dev + - sudo -E bundle install + - sudo -E bundle exec rubocop + volumes: + - name: gem-cache + path: /bundle + - name: tmp + path: /drone/src/tmp + failure: ignore + + + - name: build_test + image: circleci/ruby:2.7-bullseye-node-browsers-legacy + commands: + - sudo apt install --no-install-recommends -y libmagic-dev + - echo 'Wait for db container'; sleep 30 + - bundle config set path '/bundle' + - bundle config set without 'production' + - sudo -E bundle install + - sudo -E bundle exec rake foodsoft:setup_development_docker || true + - sudo -E bundle exec rake rspec-rerun:spec + volumes: + - name: gem-cache + path: /bundle + - name: tmp + path: /drone/src/tmp + environment: + RAILS_LOG_TO_STDOUT: true + RAILS_ENV: test + COVERAGE: lcov + DATABASE_URL: mysql2://user:password@mariadb/test?encoding=utf8mb4 + DATABASE_CLEANER_ALLOW_REMOTE_DATABASE_URL: true + PARALLEL_TEST_PROCESSORS: 60 + +services: + - name: mariadb + image: mariadb + environment: + MYSQL_USER: user + MYSQL_PASSWORD: password + MYSQL_DATABASE: test + MYSQL_ROOT_PASSWORD: password + +volumes: + - name: gem-cache + host: + path: /tmp/cache + - name: tmp + temp: {} +--- + +kind: pipeline +type: docker +name: docker build and deploy +steps: + - name: build and publish docker image + image: plugins/docker + settings: + registry: git.local-it.org + repo: git.local-it.org/foodsoft/foodsoft + username: philipp + password: + from_secret: docker_registry + tags: + - latest + - ${DRONE_BRANCH} + - ${DRONE_COMMIT:0:8} + cache_from: + - "git.local-it.org/foodsoft/foodsoft:latest" + - "git.local-it.org/foodsoft/foodsoft:${DRONE_BRANCH}" + - name: deployment + image: git.local-it.org/philipp/stack-ssh-deply:latest + settings: + stack: "foodsoft_${DRONE_COMMIT:0:8}" + compose: "deployment/compose.yml" + deploy_key: + from_secret: drone_deploy_key + host: "dev.local-it.cloud" + user: "root" + port: 22 + reg_user: philipp + reg_pass: + from_secret: docker_registry + reg_url: git.local-it.org + image: git.local-it.org/foodsoft/foodsoft:${DRONE_COMMIT:0:8} + generate_secrets: true + networks: + - proxy + environment: + IMAGE: git.local-it.org/foodsoft/foodsoft:${DRONE_COMMIT:0:8} + STACK_NAME: "foodsoft_${DRONE_COMMIT:0:8}" + DOMAIN: "${DRONE_COMMIT:0:8}.foodsoft.dev.local-it.cloud" + LETS_ENCRYPT_ENV: production + FOODCOOP_MULTI_INSTALL: true + FOODCOOP_NAME: example + FOODCOOP_CITY: XXX + FOODCOOP_COUNTRY: XXX + FOODCOOP_EMAIL: info@example.org + FOODCOOP_PHONE: XXX + FOODCOOP_STREET: XXX + FOODCOOP_ZIP_CODE: XXX + FOODCOOP_HOMEPAGE: https://order.example.org + FOODCOOP_HELP_URL: https://order.example.org + FOODCOOP_TIME_ZONE: Berlin + FOODCOOP_USE_NICK: true + FOODCOOP_LANGUAGE: de + FOODCOOP_FOOTER: 'example hosted by Your Tech Co-op.' + USE_APPLE_POINTS: false + STOP_ORDERING_UNDER: 75 + MINIMUM_BALANCE: 0 + MYSQL_DB: foodsoft + MYSQL_HOST: db + MYSQL_PORT: 3306 + MYSQL_USER: foodsoft + EMAIL_SENDER: noreply@example.org + EMAIL_ERROR: systems@example.org + SMTP_ADDRESS: mail.example.com + SMTP_AUTHENTICATION: plain + SMTP_DOMAIN: mail.example.com + SMTP_ENABLE_STARTTLS_AUTO: true + SMTP_PORT: 587 + SMTP_USER_NAME: foodsoft + EMAIL_REPLY_DOMAIN: example.org + SMTP_SERVER_HOST: 0.0.0.0 + SMTP_SERVER_PORT: 2525 + SECRET_DB_PASSWORD_VERSION: v1 + SECRET_DB_ROOT_PASSWORD_VERSION: v1 + SECRET_SHARED_LISTS_DB_PASSWORD_VERSION: v1 + SECRET_SMTP_PASSWORD_VERSION: v1 + SECRET_SECRET_KEY_BASE_VERSION: v1 + APP_CONFIG_VERSION: v1 + DB_CONFIG_VERSION: v1 + ENTRYPOINT_VERSION: v1 + PRODUCTION_ENV_VERSION: v1 +trigger: + branch: + - demo diff --git a/deployment/.env.sample b/deployment/.env.sample new file mode 100644 index 00000000..a0d398c6 --- /dev/null +++ b/deployment/.env.sample @@ -0,0 +1,65 @@ +TYPE=foodsoft + +DOMAIN=order.example.org +#EXTRA_DOMAINS=', `www.order.example.com`' +LETS_ENCRYPT_ENV=production +COMPOSE_FILE="compose.yml" + +# app settings +FOODCOOP_MULTI_INSTALL=true # Best for now, see https://github.com/foodcoops/foodsoft/pull/841 +FOODCOOP_NAME=example +FOODCOOP_CITY=XXX +FOODCOOP_COUNTRY=XXX +FOODCOOP_EMAIL=info@example.org +FOODCOOP_PHONE=XXX +FOODCOOP_STREET=XXX +FOODCOOP_ZIP_CODE=XXX +FOODCOOP_HOMEPAGE=https://order.example.org +FOODCOOP_HELP_URL=https://order.example.org +FOODCOOP_TIME_ZONE=Amsterdam +FOODCOOP_USE_NICK=true +FOODCOOP_LANGUAGE=en +FOODCOOP_FOOTER='example hosted by Your Tech Co-op.' +USE_APPLE_POINTS=false +STOP_ORDERING_UNDER=75 +MINIMUM_BALANCE=0 + +# database settings +MYSQL_DB=foodsoft +MYSQL_HOST=db +MYSQL_PORT=3306 +MYSQL_USER=foodsoft + +# shared supplier list settings +# COMPOSE_FILE="$COMPOSE_FILE:compose.sharedlists.yml" +# ENABLE_SHARED_LISTS=0 +# SHARED_LISTS_DB_TYPE=mysql2 +# SHARED_LISTS_HOST=order.otherfoodcoop.org +# SHARED_LISTS_DB_NAME=sharedlists +# SHARED_LISTS_USER=example + +# Group order invoices generation pull request +# https://github.com/foodcoops/foodsoft/pull/907 +# COMPOSE_FILE="$COMPOSE_FILE:compose.groupOrderInvoice.yml" + +# outgoing mail settings +EMAIL_SENDER=noreply@example.org +EMAIL_ERROR=systems@example.org +SMTP_ADDRESS=mail.example.com +SMTP_AUTHENTICATION=plain +SMTP_DOMAIN=mail.example.com +SMTP_ENABLE_STARTTLS_AUTO=true +SMTP_PORT=587 +SMTP_USER_NAME=foodsoft + +# incoming mail settings +EMAIL_REPLY_DOMAIN=example.org +SMTP_SERVER_HOST=0.0.0.0 +SMTP_SERVER_PORT=2525 + +# secret versions +SECRET_DB_PASSWORD_VERSION=v1 +SECRET_DB_ROOT_PASSWORD_VERSION=v1 +SECRET_SHARED_LISTS_DB_PASSWORD_VERSION=v1 +SECRET_SMTP_PASSWORD_VERSION=v1 +SECRET_SECRET_KEY_BASE_VERSION=v1 # length=30 diff --git a/deployment/app_config.yml.tmpl b/deployment/app_config.yml.tmpl new file mode 100644 index 00000000..f1e1a580 --- /dev/null +++ b/deployment/app_config.yml.tmpl @@ -0,0 +1,168 @@ +# {{ env "DOMAIN" }} configuration + +default: &defaults + # If you wanna serve more than one foodcoop with one installation + # Don't forget to setup databases for each foodcoop. See also MULTI_COOP_INSTALL + multi_coop_install: {{ env "FOODCOOP_MULTI_INSTALL" }} + + # If multi_coop_install you have to use a coop name, which you you wanna be selected by default + default_scope: "{{ env "FOODCOOP_NAME" }}" + + # name of this foodcoop + name: "{{ env "FOODCOOP_NAME" }}" + + # foodcoop contact information (used for FAX messages) + contact: + street: "{{ env "FOODCOOP_STREET" }}" + zip_code: "{{ env "FOODCOOP_ZIP_CODE" }}" + city: "{{ env "FOODCOOP_CITY" }}" + country: "{{ env "FOODCOOP_COUNTRY" }}" + email: "{{ env "FOODCOOP_EMAIL" }}" + phone: "{{ env "FOODCOOP_PHONE" }}" + + # Homepage + homepage: "{{ env "FOODCOOP_HOMEPAGE" }}" + + # foodsoft documentation URL + help_url: "{{ env "FOODCOOP_HELP_URL" }}" + + # documentation URL for the apples&pears work system + applepear_url: https://github.com/foodcoops/foodsoft/wiki/%C3%84pfel-u.-Birnen + + # custom foodsoft software URL (used in footer) + foodsoft_url: https://foodcoops.github.io + + # Default language + default_locale: {{ env "FOODCOOP_LANGUAGE" }} + + # By default, foodsoft takes the language from the webbrowser/operating system. + # In case you really want foodsoft in a certain language by default, set this to true. + # When members are logged in, the language from their profile settings is still used. + ignore_browser_locale: false + + # Default timezone, e.g. UTC, Amsterdam, Berlin, etc. + time_zone: "{{ env "FOODCOOP_TIME_ZONE" }}" + + # Currency symbol, and whether to add a whitespace after the unit. + currency_unit: € + #currency_space: true + + # price markup in percent + price_markup: 2.0 + + # default vat percentage for new articles + tax_default: 7.0 + + # tolerance order option: If set to false, article tolerance values do not count + # for total article price as long as the order is not finished. + tolerance_is_costly: false + + # Ordergroups, which have less than 75 apples should not be allowed to make new orders + # Comment out this option to activate this restriction + stop_ordering_under: {{ env "STOP_ORDERING_UNDER" }} + + # Comment out to completely hide apple points (be sure to comment stop_ordering_under) + use_apple_points: {{ env "USE_APPLE_POINTS" }} + + # ordergroups can only order when their balance is higher than or equal to this + # not fully enforced right now, since the check is only client-side + minimum_balance: {{ env "MINIMUM_BALANCE" }} + + # how many days there are between two periodic tasks + #tasks_period_days: 7 + + # how many days upfront periodic tasks are created + #tasks_upfront_days: 49 + + # default order schedule, used to provide initial dates for new orders + # (recurring dates in ical format; no spaces!) + #order_schedule: + # ends: + # recurr: FREQ=WEEKLY;INTERVAL=2;BYDAY=MO + # time: '9:00' + # # reference point, this is generally the first pickup day; empty is often ok + # #initial: + + # When use_nick is enabled, there will be a nickname field in the user form, + # and the option to show a nickname instead of full name to foodcoop members. + # Members of a user's groups and administrators can still see full names. + use_nick: {{ env "FOODCOOP_USE_NICK" }} + + # Most plugins can be enabled/disabled here as well. Messages and wiki are enabled + # by default and need to be set to false to disable. Most other plugins needs to + # be enabled before they do anything. + use_wiki: true + use_messages: true + use_documents: true + use_polls: true + + # Base font size for generated PDF documents + #pdf_font_size: 12 + + # Page size for generated PDF documents + #pdf_page_size: A4 + + # Some documents (like group and article PDFs) can include page breaks + # after each sublist. + #pdf_add_page_breaks: true + + # Alternatively, this can be set for each document. + #pdf_add_page_breaks: + # order_by_groups: true + # order_by_articles: true + + # Page footer (html allowed). Default is a Foodsoft footer. Set to `blank` for no footer. + page_footer: {{ env "FOODCOOP_FOOTER" }} + + # Custom CSS for the foodcoop + #custom_css: 'body { background-color: #fcffba; }' + + # Uncomment to add tracking code for web statistics, e.g. for Piwik. (Added to bottom of page) + #webstats_tracking_code: | + # + # ...... + + # email address to be used as sender + email_sender: "{{ env "EMAIL_SENDER" }}" + + # email address to be used as from + email_from: "{{ env "EMAIL_SENDER" }}" + + # domain to be used for reply emails + reply_email_domain: {{ env "EMAIL_REPLY_DOMAIN" }} + + # If your foodcoop uses a mailing list instead of internal messaging system + #mailing_list: list@example.org + #mailing_list_subscribe: list-subscribe@example.org + + # Config for the exception_notification plugin + notification: + error_recipients: + - "{{ env "EMAIL_ERROR" }}" + sender_address: "\"Foodsoft error\" <{{ env "EMAIL_SENDER" }}>" + email_prefix: "[foodsoft] " + + # http config for this host to generate links in emails (uses environment config when not set) + protocol: https + host: "{{ env "DOMAIN" }}" + #port: 3000 + + {{ if eq (env "ENABLE_SHARED_LISTS") "1" }} + # Access to sharedlists, the external article-database. + # This allows a foodcoop to subscribe to a selection of a supplier's full assortment, + # and makes it possible to share data with several foodcoops. Using this requires installing + # an additional application with a separate database. + shared_lists: + adapter: "{{ env "SHARED_LISTS_DB_TYPE" }}" + host: "{{ env "SHARED_LISTS_HOST" }}" + database: "{{ env "SHARED_LISTS_DB_NAME" }}" + username: "{{ env "SHARED_LISTS_USER" }}" + password: "{{ secret "shared_lists_db_password" }}" + {{ end }} + +# don't remove this, required to run the app +production: + <<: *defaults + +{{ env "FOODCOOP_NAME" }}: + <<: *defaults diff --git a/deployment/compose.yml b/deployment/compose.yml new file mode 100644 index 00000000..b4484d66 --- /dev/null +++ b/deployment/compose.yml @@ -0,0 +1,189 @@ +--- +version: "3.8" + +x-env: &env + CERTBOT_DISABLED: 1 + DOMAIN: + EMAIL_ERROR: + EMAIL_REPLY_DOMAIN: + EMAIL_SENDER: + FOODCOOP_CITY: + FOODCOOP_COUNTRY: + FOODCOOP_EMAIL: + FOODCOOP_FOOTER: + FOODCOOP_HELP_URL: + FOODCOOP_HOMEPAGE: + FOODCOOP_MULTI_INSTALL: + FOODCOOP_NAME: + FOODCOOP_PHONE: + FOODCOOP_STREET: + FOODCOOP_TIME_ZONE: + FOODCOOP_ZIP_CODE: + FOODCOOP_USE_NICK: + FOODCOOP_LANGUAGE: + LOG_LEVEL: + MINIMUM_BALANCE: + MYSQL_DB: + MYSQL_HOST: + MYSQL_PORT: + MYSQL_USER: + QUEUE: foodsoft_notifier + REDIS_URL: redis://cache:6379 + SECRET_KEY_BASE_FILE: /run/secrets/secret_key_base + SMTP_ADDRESS: + SMTP_AUTHENTICATION: + SMTP_DOMAIN: + SMTP_ENABLE_STARTTLS_AUTO: + SMTP_PASSWORD_FILE: /run/secrets/smtp_password + SMTP_PORT: + SMTP_USER_NAME: + STOP_ORDERING_UNDER: + USE_APPLE_POINTS: + +x-configs: &configs + - source: app_config + target: /usr/src/app/config/app_config.yml + - source: db_config + target: /usr/src/app/config/database.yml + - source: entrypoint + target: /usr/src/app/docker-entrypoint.sh + mode: 0555 + +x-secrets: &secrets + - db_password + - secret_key_base + - smtp_password + +services: + app: + image: ${IMAGE} + networks: + - internal + - proxy + secrets: *secrets + configs: *configs + entrypoint: &entrypoint /usr/src/app/docker-entrypoint.sh + environment: + <<: *env + FOODSOFT_SERVICE: app + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:3000"] + interval: 15s + timeout: 10s + retries: 10 + start_period: 1m + deploy: + update_config: + failure_action: rollback + order: start-first + labels: + - "traefik.enable=true" + - "traefik.http.routers.${STACK_NAME}.rule=Host(`${DOMAIN}`${EXTRA_DOMAINS})" + - "traefik.http.routers.${STACK_NAME}.entrypoints=web-secure" + - "traefik.http.routers.${STACK_NAME}.tls.certresolver=${LETS_ENCRYPT_ENV}" + - "traefik.http.services.${STACK_NAME}.loadbalancer.server.port=3000" + - "coop-cloud.${STACK_NAME}.version=1.0.0+4.7.1" + + cron: + image: ${IMAGE} + secrets: *secrets + configs: *configs + entrypoint: *entrypoint + environment: + <<: *env + FOODSOFT_SERVICE: cron + networks: + - internal + + worker: + image: ${IMAGE} + secrets: *secrets + configs: *configs + entrypoint: *entrypoint + environment: + <<: *env + FOODSOFT_SERVICE: worker + networks: + - internal + + smtp: + image: ${IMAGE} + configs: *configs + entrypoint: *entrypoint + secrets: *secrets + environment: + <<: *env + FOODSOFT_SERVICE: smtp + SMTP_SERVER_HOST: + SMTP_SERVER_PORT: + networks: + - proxy + - internal + deploy: + labels: + - "traefik.enable=true" + - "traefik.tcp.routers.foodsoft-smtp.rule=HostSNI(`*`)" + - "traefik.tcp.routers.foodsoft-smtp.entrypoints=foodsoft-smtp" + - "traefik.tcp.services.foodsoft-smtp.loadbalancer.server.port=${SMTP_SERVER_PORT}" + + db: + image: "mariadb:10.6" + command: "mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_520_ci" + environment: + MYSQL_USER: ${MYSQL_USER} + MYSQL_DATABASE: ${MYSQL_DB} + MYSQL_PASSWORD_FILE: /run/secrets/db_password + MYSQL_ROOT_PASSWORD_FILE: /run/secrets/db_root_password + secrets: + - db_password + - db_root_password + volumes: + - "db:/var/lib/mysql" + networks: + - internal + deploy: + labels: + backupbot.backup: "true" + backupbot.backup.pre-hook: 'mkdir -p /tmp/backup/ && mysqldump --single-transaction -u root -p"$$(cat /run/secrets/db_root_password)" $${MYSQL_DATABASE} > /tmp/backup/backup.sql' + backupbot.backup.post-hook: "rm -rf /tmp/backup" + backupbot.backup.path: "/tmp/backup/" + cache: + image: "redis:6" + networks: + - internal + +networks: + internal: + proxy: + external: true + +volumes: + db: + +configs: + app_config: + name: ${STACK_NAME}_app_config_${APP_CONFIG_VERSION} + file: app_config.yml.tmpl + template_driver: golang + db_config: + name: ${STACK_NAME}_db_config_${DB_CONFIG_VERSION} + file: database.yml.tmpl + template_driver: golang + entrypoint: + name: ${STACK_NAME}_entrypoint_${ENTRYPOINT_VERSION} + file: entrypoint.sh.tmpl + template_driver: golang + +secrets: + db_password: + name: ${STACK_NAME}_db_password_${SECRET_DB_PASSWORD_VERSION} + external: true + db_root_password: + name: ${STACK_NAME}_db_root_password_${SECRET_DB_ROOT_PASSWORD_VERSION} + external: true + smtp_password: + name: ${STACK_NAME}_smtp_password_${SECRET_SMTP_PASSWORD_VERSION} + external: true + secret_key_base: + name: ${STACK_NAME}_secret_key_base_${SECRET_SECRET_KEY_BASE_VERSION} + external: true diff --git a/deployment/database.yml.tmpl b/deployment/database.yml.tmpl new file mode 100644 index 00000000..bf64dc72 --- /dev/null +++ b/deployment/database.yml.tmpl @@ -0,0 +1,9 @@ +production: + adapter: "mysql2" + encoding: "utf8mb4" + collation: "utf8mb4_unicode_520_ci" + username: "{{ env "MYSQL_USER" }}" + password: "{{ secret "db_password" }}" + database: "{{ env "MYSQL_DB" }}" + host: "{{ env "MYSQL_HOST" }}" + port: "{{ env "MYSQL_PORT" }}" diff --git a/deployment/entrypoint.sh.tmpl b/deployment/entrypoint.sh.tmpl new file mode 100644 index 00000000..06f27b08 --- /dev/null +++ b/deployment/entrypoint.sh.tmpl @@ -0,0 +1,44 @@ +#!/bin/bash + +set -eu + +file_env() { + local var="$1" + local fileVar="${var}_FILE" + local def="${2:-}" + + if [ "${!var:-}" ] && [ "${!fileVar:-}" ]; then + echo >&2 "error: both $var and $fileVar are set (but are exclusive)" + exit 1 + fi + + local val="$def" + + if [ "${!var:-}" ]; then + val="${!var}" + elif [ "${!fileVar:-}" ]; then + val="$(< "${!fileVar}")" + fi + + export "$var"="$val" + unset "$fileVar" +} + +file_env "SECRET_KEY_BASE" +file_env "SMTP_PASSWORD" + +echo "------------------------------------------------------------------------------" +echo "Running entrypoint commands against '$FOODSOFT_SERVICE' service" +echo "------------------------------------------------------------------------------" + +if [ "$FOODSOFT_SERVICE" == "app" ]; then + bundle exec rake db:setup || true + bundle exec rake db:migrate || true + ./proc-start web +elif [ "$FOODSOFT_SERVICE" == "cron" ]; then + ./proc-start cron +elif [ "$FOODSOFT_SERVICE" == "worker" ]; then + ./proc-start worker +elif [ "$FOODSOFT_SERVICE" == "smtp" ]; then + ./proc-start mail +fi From 69c80eba3e76bef5c73644f789007798191294fa Mon Sep 17 00:00:00 2001 From: Philipp Rothmann Date: Fri, 17 Feb 2023 12:40:26 +0100 Subject: [PATCH 015/105] feat(finance): show sum of ordergroup balances --- .../finance/ordergroups_controller.rb | 5 +- .../ordergroups/_ordergroups.html.haml | 9 ++++ .../finance/ordergroups_controller_spec.rb | 50 +++++++++++++++++++ 3 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 spec/controllers/finance/ordergroups_controller_spec.rb diff --git a/app/controllers/finance/ordergroups_controller.rb b/app/controllers/finance/ordergroups_controller.rb index cb661571..d334f223 100644 --- a/app/controllers/finance/ordergroups_controller.rb +++ b/app/controllers/finance/ordergroups_controller.rb @@ -11,7 +11,10 @@ class Finance::OrdergroupsController < Finance::BaseController @ordergroups = Ordergroup.undeleted.order(sort) @ordergroups = @ordergroups.include_transaction_class_sum @ordergroups = @ordergroups.where('groups.name LIKE ?', "%#{params[:query]}%") unless params[:query].nil? - @ordergroups = @ordergroups.page(params[:page]).per(@per_page) + + @total_balances = FinancialTransactionClass.sorted.each_with_object({}) do |c, tmp| + tmp[c.id] = c.financial_transactions.reduce(0) { | sum, t | sum + t.amount } + end end end diff --git a/app/views/finance/ordergroups/_ordergroups.html.haml b/app/views/finance/ordergroups/_ordergroups.html.haml index 83a05ed2..3e0c99fc 100644 --- a/app/views/finance/ordergroups/_ordergroups.html.haml +++ b/app/views/finance/ordergroups/_ordergroups.html.haml @@ -22,3 +22,12 @@ %td = link_to t('.new_transaction'), new_finance_ordergroup_transaction_path(ordergroup), class: 'btn btn-mini' = link_to t('.account_statement'), finance_ordergroup_transactions_path(ordergroup), class: 'btn btn-mini' + %thead + %tr + %th= t 'Total' + %th + - FinancialTransactionClass.sorted.each do |c| + - name = FinancialTransactionClass.has_multiple_classes ? c.display : heading_helper(Ordergroup, :account_balance) + %th.numeric= format_currency @total_balances[c.id] + %th.numeric + = format_currency @total_balances.values.reduce(:+) \ No newline at end of file diff --git a/spec/controllers/finance/ordergroups_controller_spec.rb b/spec/controllers/finance/ordergroups_controller_spec.rb new file mode 100644 index 00000000..f960c61d --- /dev/null +++ b/spec/controllers/finance/ordergroups_controller_spec.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Finance::OrdergroupsController do + let(:user) { create(:user, :role_finance, :role_orders, :ordergroup) } + let(:fin_trans_type1) { create(:financial_transaction_type) } + let(:fin_trans_type2) { create(:financial_transaction_type) } + let(:fin_trans1) do + create(:financial_transaction, + user: user, + ordergroup: user.ordergroup, + financial_transaction_type: fin_trans_type1) + end + let(:fin_trans2) do + create(:financial_transaction, + user: user, + ordergroup: user.ordergroup, + financial_transaction_type: fin_trans_type1) + end + let(:fin_trans3) do + create(:financial_transaction, + user: user, + ordergroup: user.ordergroup, + financial_transaction_type: fin_trans_type2) + end + + before { login user } + + describe 'GET index' do + before do + fin_trans1 + fin_trans2 + fin_trans3 + end + + it 'renders index page' do + get_with_defaults :index + expect(response).to have_http_status(:success) + expect(response).to render_template('finance/ordergroups/index') + end + + it 'calculates total balance sums correctly' do + get_with_defaults :index + expect(assigns(:total_balances).size).to eq(2) + expect(assigns(:total_balances)[fin_trans_type1.id]).to eq(fin_trans1.amount + fin_trans2.amount) + expect(assigns(:total_balances)[fin_trans_type2.id]).to eq(fin_trans3.amount) + end + end +end From 49a04b226c4a69aabfbaa55f10f0265bd9d39e08 Mon Sep 17 00:00:00 2001 From: Philipp Rothmann Date: Thu, 9 Feb 2023 17:18:25 +0100 Subject: [PATCH 016/105] feat(messages): add html formatting to messages This commit allows users to use the trix editor to send messages with basic formatting and attachements. * add active storage * add actiontext * add richtext field to messages * add imageprocessing for message attachements * add html email layout and adjust translations to use html urls --- Gemfile | 1 + Gemfile.lock | 7 +++++ app/assets/stylesheets/actiontext.css | 31 +++++++++++++++++++ app/assets/stylesheets/application.css | 1 + app/javascript/application.js | 2 ++ app/views/active_storage/blobs/_blob.html.erb | 14 +++++++++ .../action_text/contents/_content.html.erb | 3 ++ app/views/layouts/email.html.haml | 12 +++++++ config/application.rb | 2 ++ config/importmap.rb | 4 ++- config/locales/de.yml | 1 + config/locales/en.yml | 1 + config/locales/es.yml | 1 + config/locales/fr.yml | 1 + config/locales/nl.yml | 1 + ...6_create_action_text_tables.action_text.rb | 26 ++++++++++++++++ db/schema.rb | 12 ++++++- .../app/controllers/messages_controller.rb | 4 +-- .../messages/app/helpers/messages_helper.rb | 2 +- plugins/messages/app/models/message.rb | 2 ++ plugins/messages/app/views/messages/new.haml | 2 +- plugins/messages/app/views/messages/show.haml | 2 +- .../foodsoft_message.html.haml | 11 +++++++ plugins/messages/config/locales/de.yml | 3 ++ plugins/messages/config/locales/en.yml | 3 ++ plugins/messages/config/locales/fr.yml | 3 ++ plugins/messages/config/locales/nl.yml | 3 ++ 27 files changed, 148 insertions(+), 7 deletions(-) create mode 100644 app/assets/stylesheets/actiontext.css create mode 100644 app/views/active_storage/blobs/_blob.html.erb create mode 100644 app/views/layouts/action_text/contents/_content.html.erb create mode 100644 app/views/layouts/email.html.haml create mode 100644 db/migrate/20230209105256_create_action_text_tables.action_text.rb create mode 100644 plugins/messages/app/views/messages_mailer/foodsoft_message.html.haml diff --git a/Gemfile b/Gemfile index e1ed43a7..9ad9ec8f 100644 --- a/Gemfile +++ b/Gemfile @@ -129,3 +129,4 @@ end gem "importmap-rails", "~> 1.1" gem "terser", "~> 1.1" +gem "image_processing", "~> 1.12" diff --git a/Gemfile.lock b/Gemfile.lock index cfda4050..019f9338 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -254,6 +254,9 @@ GEM i18n-spec (0.6.0) iso ice_cube (0.16.4) + image_processing (1.12.2) + mini_magick (>= 4.9.5, < 5) + ruby-vips (>= 2.0.17, < 3) importmap-rails (1.1.5) actionpack (>= 6.0.0) railties (>= 6.0.0) @@ -317,6 +320,7 @@ GEM mime-types (3.4.1) mime-types-data (~> 3.2015) mime-types-data (3.2022.0105) + mini_magick (4.12.0) mini_mime (1.1.2) minitest (5.17.0) mono_logger (1.1.1) @@ -495,6 +499,8 @@ GEM ruby-prof (1.4.5) ruby-progressbar (1.11.0) ruby-units (3.0.0) + ruby-vips (2.1.4) + ffi (~> 1.12) ruby2_keywords (0.0.5) rubyzip (2.3.2) sass-rails (6.0.0) @@ -630,6 +636,7 @@ DEPENDENCIES i18n-js (~> 3.0.0.rc8) i18n-spec ice_cube + image_processing (~> 1.12) importmap-rails (~> 1.1) inherited_resources jquery-rails diff --git a/app/assets/stylesheets/actiontext.css b/app/assets/stylesheets/actiontext.css new file mode 100644 index 00000000..3cfcb2b7 --- /dev/null +++ b/app/assets/stylesheets/actiontext.css @@ -0,0 +1,31 @@ +/* + * Provides a drop-in pointer for the default Trix stylesheet that will format the toolbar and + * the trix-editor content (whether displayed or under editing). Feel free to incorporate this + * inclusion directly in any other asset bundle and remove this file. + * + *= require trix +*/ + +/* + * We need to override trix.css’s image gallery styles to accommodate the + * element we wrap around attachments. Otherwise, + * images in galleries will be squished by the max-width: 33%; rule. +*/ +.trix-content .attachment-gallery > action-text-attachment, +.trix-content .attachment-gallery > .attachment { + flex: 1 0 33%; + padding: 0 0.5em; + max-width: 33%; +} + +.trix-content .attachment-gallery.attachment-gallery--2 > action-text-attachment, +.trix-content .attachment-gallery.attachment-gallery--2 > .attachment, .trix-content .attachment-gallery.attachment-gallery--4 > action-text-attachment, +.trix-content .attachment-gallery.attachment-gallery--4 > .attachment { + flex-basis: 50%; + max-width: 50%; +} + +.trix-content action-text-attachment .attachment { + padding: 0 !important; + max-width: 100% !important; +} diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index 6bdfecd2..01dba421 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -7,4 +7,5 @@ *= require list.unlist *= require list.missing *= require recurring_select +*= require actiontext */ diff --git a/app/javascript/application.js b/app/javascript/application.js index beff742e..ed5cae66 100644 --- a/app/javascript/application.js +++ b/app/javascript/application.js @@ -1 +1,3 @@ // Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails +import "trix" +import "@rails/actiontext" diff --git a/app/views/active_storage/blobs/_blob.html.erb b/app/views/active_storage/blobs/_blob.html.erb new file mode 100644 index 00000000..49ba357d --- /dev/null +++ b/app/views/active_storage/blobs/_blob.html.erb @@ -0,0 +1,14 @@ +
attachment--<%= blob.filename.extension %>"> + <% if blob.representable? %> + <%= image_tag blob.representation(resize_to_limit: local_assigns[:in_gallery] ? [ 800, 600 ] : [ 1024, 768 ]) %> + <% end %> + +
+ <% if caption = blob.try(:caption) %> + <%= caption %> + <% else %> + <%= blob.filename %> + <%= number_to_human_size blob.byte_size %> + <% end %> +
+
diff --git a/app/views/layouts/action_text/contents/_content.html.erb b/app/views/layouts/action_text/contents/_content.html.erb new file mode 100644 index 00000000..9e3c0d0d --- /dev/null +++ b/app/views/layouts/action_text/contents/_content.html.erb @@ -0,0 +1,3 @@ +
+ <%= yield -%> +
diff --git a/app/views/layouts/email.html.haml b/app/views/layouts/email.html.haml new file mode 100644 index 00000000..6bcf3b4a --- /dev/null +++ b/app/views/layouts/email.html.haml @@ -0,0 +1,12 @@ += yield +\ +%hr +%ul + %li + %a{href: root_url} Foodsoft + - if FoodsoftConfig[:homepage] + %li + %a{href: FoodsoftConfig[:homepage]} Foodcoop + - if FoodsoftConfig[:help_url] + %li + %a{href: FoodsoftConfig[:help_url]}= t '.help' \ No newline at end of file diff --git a/config/application.rb b/config/application.rb index 9c0ade99..f76faa95 100644 --- a/config/application.rb +++ b/config/application.rb @@ -67,6 +67,8 @@ module Foodsoft config.autoloader = :zeitwerk + config.active_storage.variant_processor = :mini_magick + # Ex:- :default =>'' # CORS for API diff --git a/config/importmap.rb b/config/importmap.rb index 050818ab..f882664b 100644 --- a/config/importmap.rb +++ b/config/importmap.rb @@ -1,2 +1,4 @@ # Pin npm packages by running ./bin/importmap -pin "application", preload: true \ No newline at end of file +pin "application", preload: true +pin "trix" +pin "@rails/actiontext", to: "actiontext.js" diff --git a/config/locales/de.yml b/config/locales/de.yml index 3e4e54d5..9b569572 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -1222,6 +1222,7 @@ de: footer_2_foodsoft: 'Foodsoft: %{url}' footer_3_homepage: 'Foodcoop: %{url}' footer_4_help: 'Hilfe: %{url}' + help: 'Hilfe' foodsoft: Foodsoft footer: revision: Revision %{revision} diff --git a/config/locales/en.yml b/config/locales/en.yml index 39053a82..30a1fb53 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1225,6 +1225,7 @@ en: footer_2_foodsoft: 'Foodsoft: %{url}' footer_3_homepage: 'Foodcoop: %{url}' footer_4_help: 'Help: %{url}' + help: 'Help' foodsoft: Foodsoft footer: revision: revision %{revision} diff --git a/config/locales/es.yml b/config/locales/es.yml index 6363c9d3..8bbd69d6 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -1083,6 +1083,7 @@ es: layouts: email: footer_4_help: 'Ayuda: %{url}' + help: 'Ayuda' footer: revision: revisión %{revision} header: diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 4dbdb864..b1199dc7 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -834,6 +834,7 @@ fr: email: footer_3_homepage: 'Boufcoop: %{url}' footer_4_help: 'Aide: %{url}' + help: 'Aide' footer: revision: révision %{revision} header: diff --git a/config/locales/nl.yml b/config/locales/nl.yml index 1af1e65d..31179a6d 100644 --- a/config/locales/nl.yml +++ b/config/locales/nl.yml @@ -1195,6 +1195,7 @@ nl: footer_2_foodsoft: 'Foodsoft: %{url}' footer_3_homepage: 'Foodcoop: %{url}' footer_4_help: 'Help: %{url}' + help: 'Help' foodsoft: Foodsoft footer: revision: revisie %{revision} diff --git a/db/migrate/20230209105256_create_action_text_tables.action_text.rb b/db/migrate/20230209105256_create_action_text_tables.action_text.rb new file mode 100644 index 00000000..1be48d70 --- /dev/null +++ b/db/migrate/20230209105256_create_action_text_tables.action_text.rb @@ -0,0 +1,26 @@ +# This migration comes from action_text (originally 20180528164100) +class CreateActionTextTables < ActiveRecord::Migration[6.0] + def change + # Use Active Record's configured type for primary and foreign keys + primary_key_type, foreign_key_type = primary_and_foreign_key_types + + create_table :action_text_rich_texts, id: primary_key_type do |t| + t.string :name, null: false + t.text :body, size: :long + t.references :record, null: false, polymorphic: true, index: false, type: foreign_key_type + + t.timestamps + + t.index [ :record_type, :record_id, :name ], name: "index_action_text_rich_texts_uniqueness", unique: true + end + end + + private + def primary_and_foreign_key_types + config = Rails.configuration.generators + setting = config.options[config.orm][:primary_key_type] + primary_key_type = setting || :primary_key + foreign_key_type = setting || :bigint + [primary_key_type, foreign_key_type] + end +end diff --git a/db/schema.rb b/db/schema.rb index 50c24c41..9ba8eaf3 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,17 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 2023_01_06_144440) do +ActiveRecord::Schema[7.0].define(version: 2023_02_09_105256) do + create_table "action_text_rich_texts", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| + t.string "name", null: false + t.text "body", size: :long + t.string "record_type", null: false + t.bigint "record_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["record_type", "record_id", "name"], name: "index_action_text_rich_texts_uniqueness", unique: true + end + create_table "active_storage_attachments", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.string "name", null: false t.string "record_type", null: false diff --git a/plugins/messages/app/controllers/messages_controller.rb b/plugins/messages/app/controllers/messages_controller.rb index 628f145b..159984ed 100644 --- a/plugins/messages/app/controllers/messages_controller.rb +++ b/plugins/messages/app/controllers/messages_controller.rb @@ -20,8 +20,8 @@ class MessagesController < ApplicationController @message.group_id = original_message.group_id @message.private = original_message.private @message.subject = I18n.t('messages.model.reply_subject', :subject => original_message.subject) - @message.body = I18n.t('messages.model.reply_header', :user => original_message.sender.display, :when => I18n.l(original_message.created_at, :format => :short)) + "\n" - original_message.body.each_line { |l| @message.body += I18n.t('messages.model.reply_indent', :line => l) } + @message.body = I18n.t('messages.model.reply_header', :user => original_message.sender.display, :when => I18n.l(original_message.created_at, :format => :short)) + "\n" \ + + "
" + original_message.body.to_trix_html + "
" else redirect_to new_message_url, alert: I18n.t('messages.new.error_private') end diff --git a/plugins/messages/app/helpers/messages_helper.rb b/plugins/messages/app/helpers/messages_helper.rb index d5371fe4..d386e6df 100644 --- a/plugins/messages/app/helpers/messages_helper.rb +++ b/plugins/messages/app/helpers/messages_helper.rb @@ -5,7 +5,7 @@ module MessagesHelper body = "" else subject = message.subject - body = truncate(message.body, :length => length - subject.length) + body = truncate(message.body.to_plain_text, :length => length - subject.length) end "#{link_to(h(subject), message)} #{h(body)}".html_safe end diff --git a/plugins/messages/app/models/message.rb b/plugins/messages/app/models/message.rb index f6b03c10..b5087d0d 100644 --- a/plugins/messages/app/models/message.rb +++ b/plugins/messages/app/models/message.rb @@ -22,6 +22,8 @@ class Message < ApplicationRecord validates_presence_of :message_recipients, :subject, :body validates_length_of :subject, :in => 1..255 + has_rich_text :body + after_initialize do @recipients_ids ||= [] @send_method ||= 'recipients' diff --git a/plugins/messages/app/views/messages/new.haml b/plugins/messages/app/views/messages/new.haml index 57d6b452..d288cd72 100644 --- a/plugins/messages/app/views/messages/new.haml +++ b/plugins/messages/app/views/messages/new.haml @@ -110,7 +110,7 @@ = f.input :recipient_tokens, :input_html => { 'data-pre' => User.where(id: @message.recipients_ids).map(&:token_attributes).to_json } = f.input :private, inline_label: t('.hint_private') = f.input :subject, input_html: {class: 'input-xxlarge'} - = f.input :body, input_html: {class: 'input-xxlarge', rows: 13} + = f.rich_text_area :body, input_html: {class: 'input-xxlarge', rows: 13} .form-actions = f.submit class: 'btn btn-primary' = link_to t('ui.or_cancel'), :back diff --git a/plugins/messages/app/views/messages/show.haml b/plugins/messages/app/views/messages/show.haml index 36e7b570..8b3f7c1c 100644 --- a/plugins/messages/app/views/messages/show.haml +++ b/plugins/messages/app/views/messages/show.haml @@ -33,7 +33,7 @@ - if @message.can_toggle_private?(current_user) = link_to t('.change_visibility'), toggle_private_message_path(@message), method: :post, class: 'btn btn-mini' %hr/ - %p= simple_format(h(@message.body)) + .trix-content= @message.body %hr/ %p = link_to t('.reply'), new_message_path(:message => {:reply_to => @message.id}), class: 'btn' diff --git a/plugins/messages/app/views/messages_mailer/foodsoft_message.html.haml b/plugins/messages/app/views/messages_mailer/foodsoft_message.html.haml new file mode 100644 index 00000000..7ca572f3 --- /dev/null +++ b/plugins/messages/app/views/messages_mailer/foodsoft_message.html.haml @@ -0,0 +1,11 @@ += raw @message.body +%hr +%ul + - if @message.group + %li= t '.footer_group', group: @message.group.name + %li + %a{href: new_message_url('message[reply_to]' => @message.id)}= t '.reply' + %li + %a{href: message_url(@message)}= t '.see_message_online' + %li + %a{href: my_profile_url}= t '.messaging_options' diff --git a/plugins/messages/config/locales/de.yml b/plugins/messages/config/locales/de.yml index f1615163..eb8cff21 100644 --- a/plugins/messages/config/locales/de.yml +++ b/plugins/messages/config/locales/de.yml @@ -138,6 +138,9 @@ de: Antworten: %{reply_url} Nachricht online einsehen: %{msg_url} Nachrichten-Einstellungen: %{profile_url} + reply: Antworten + see_message_online: Nachricht online einsehen + messaging_options: Nachrichten-Einstellungen footer_group: | Gesendet an Gruppe: %{group} navigation: diff --git a/plugins/messages/config/locales/en.yml b/plugins/messages/config/locales/en.yml index ede3f88c..ccd8bb6c 100644 --- a/plugins/messages/config/locales/en.yml +++ b/plugins/messages/config/locales/en.yml @@ -140,6 +140,9 @@ en: Reply: %{reply_url} See message online: %{msg_url} Messaging options: %{profile_url} + reply: Reply + see_message_online: See message online + messaging_options: Messaging options footer_group: | Sent to group: %{group} navigation: diff --git a/plugins/messages/config/locales/fr.yml b/plugins/messages/config/locales/fr.yml index 54584b48..67d452c5 100644 --- a/plugins/messages/config/locales/fr.yml +++ b/plugins/messages/config/locales/fr.yml @@ -67,6 +67,9 @@ fr: Répondre: %{reply_url} Afficher ce message dans ton navigateur: %{msg_url} Préférences des messages: %{profile_url} + reply: Répondre + see_message_online: Afficher ce message dans ton navigateur + messaging_options: Préférences des messages simple_form: labels: settings: diff --git a/plugins/messages/config/locales/nl.yml b/plugins/messages/config/locales/nl.yml index d3960a23..56738c0b 100644 --- a/plugins/messages/config/locales/nl.yml +++ b/plugins/messages/config/locales/nl.yml @@ -140,6 +140,9 @@ nl: Antwoorden: %{reply_url} Bericht online lezen: %{msg_url} Berichtinstellingen: %{profile_url} + reply: Antwoorden + see_message_online: Bericht online lezen + messaging_options: Berichtinstellingen footer_group: | Verzenden aan groep: %{group} navigation: From 25d4efa71aa767255b9f24028b2b2ffcf0a3e80c Mon Sep 17 00:00:00 2001 From: Philipp Rothmann Date: Wed, 15 Feb 2023 09:59:02 +0100 Subject: [PATCH 017/105] fix(messages): migrate message bodys to richtext --- ...230215085312_migrate_message_body_to_action_text.rb | 10 ++++++++++ db/schema.rb | 3 +-- 2 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 db/migrate/20230215085312_migrate_message_body_to_action_text.rb diff --git a/db/migrate/20230215085312_migrate_message_body_to_action_text.rb b/db/migrate/20230215085312_migrate_message_body_to_action_text.rb new file mode 100644 index 00000000..64e01214 --- /dev/null +++ b/db/migrate/20230215085312_migrate_message_body_to_action_text.rb @@ -0,0 +1,10 @@ +class MigrateMessageBodyToActionText < ActiveRecord::Migration[7.0] + include ActionView::Helpers::TextHelper + def change + rename_column :messages, :body, :body_old + Message.all.each do |message| + message.update_attribute(:body, simple_format(message.body_old)) + end + remove_column :messages, :body_old + end +end diff --git a/db/schema.rb b/db/schema.rb index 9ba8eaf3..4c853039 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 2023_02_09_105256) do +ActiveRecord::Schema[7.0].define(version: 2023_02_15_085312) do create_table "action_text_rich_texts", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.string "name", null: false t.text "body", size: :long @@ -292,7 +292,6 @@ ActiveRecord::Schema[7.0].define(version: 2023_02_09_105256) do create_table "messages", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.integer "sender_id" t.string "subject", null: false - t.text "body" t.boolean "private", default: false t.datetime "created_at", precision: nil t.integer "reply_to" From 28c851823a7832f67cd604f092565b2aa4c8d611 Mon Sep 17 00:00:00 2001 From: Philipp Rothmann Date: Fri, 10 Feb 2023 12:50:59 +0100 Subject: [PATCH 018/105] fix: assets precompile by using terser importmaps broke precompiliation with uglifier see: https://github.com/rails/importmap-rails/issues/5 --- Gemfile | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Gemfile b/Gemfile index 9ad9ec8f..9c00edf3 100644 --- a/Gemfile +++ b/Gemfile @@ -127,6 +127,5 @@ group :test do end gem "importmap-rails", "~> 1.1" - -gem "terser", "~> 1.1" gem "image_processing", "~> 1.12" +gem "terser", "~> 1.1" From b3571515b0e0b5aebf95cb588733ad5961ad9142 Mon Sep 17 00:00:00 2001 From: Philipp Rothmann Date: Mon, 20 Feb 2023 11:05:13 +0100 Subject: [PATCH 019/105] fix: set RAILS_SERVE_STATIC_FILES for deployment --- deployment/compose.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/deployment/compose.yml b/deployment/compose.yml index b4484d66..50543ba4 100644 --- a/deployment/compose.yml +++ b/deployment/compose.yml @@ -66,6 +66,7 @@ services: environment: <<: *env FOODSOFT_SERVICE: app + RAILS_SERVE_STATIC_FILES: 'true' healthcheck: test: ["CMD", "curl", "-f", "http://localhost:3000"] interval: 15s From 936c1ba878f4e2c4cbabcb9c5ef0c90a4c2c1379 Mon Sep 17 00:00:00 2001 From: Philipp Rothmann Date: Mon, 20 Feb 2023 12:40:14 +0100 Subject: [PATCH 020/105] fix: give docker user storge directory permissions for fileupload --- Dockerfile | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 95479ce2..ae57d2f9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -53,9 +53,10 @@ RUN export DATABASE_URL=mysql2://localhost/temp?encoding=utf8 && \ rm -Rf /var/lib/apt/lists/* /var/cache/apt/* # Make relevant dirs and files writable for app user -RUN mkdir -p tmp && \ +RUN mkdir -p tmp storage && \ chown nobody config/app_config.yml && \ - chown nobody tmp + chown nobody tmp && \ + chown nobody storage # Run app as unprivileged user USER nobody From 4b5775e107a4f50e05d3b699ceb3c358045851c7 Mon Sep 17 00:00:00 2001 From: viehlieb Date: Tue, 31 Jan 2023 15:25:36 +0100 Subject: [PATCH 021/105] include foodsoft-article-import use filetypes for manual uploading bnn, odin, foodsoft file use opts in .parse adapt specs to include file format add specs for odin, bnn, foodsoft files adapt localize input to remove ',' separator and replace with '.' remove depr foodsoftfile.rb and spreadsheet.rb remove todo --- Gemfile | 1 + Gemfile.lock | 9 + app/controllers/articles_controller.rb | 3 +- app/lib/foodsoft_file.rb | 25 -- app/lib/spreadsheet_file.rb | 22 -- app/models/concerns/localize_input.rb | 2 +- app/models/supplier.rb | 10 +- app/views/articles/upload.html.haml | 7 +- spec/controllers/articles_controller_spec.rb | 4 +- spec/fixtures/bnn_file_01.bnn | 5 + spec/fixtures/bnn_file_02.bnn | 2 + spec/fixtures/odin_file_01.xml | 273 +++++++++++++++++++ spec/fixtures/odin_file_02.xml | 75 +++++ spec/integration/articles_spec.rb | 172 +++++++----- spec/models/supplier_spec.rb | 2 +- 15 files changed, 492 insertions(+), 120 deletions(-) delete mode 100644 app/lib/foodsoft_file.rb delete mode 100644 app/lib/spreadsheet_file.rb create mode 100644 spec/fixtures/bnn_file_01.bnn create mode 100644 spec/fixtures/bnn_file_02.bnn create mode 100644 spec/fixtures/odin_file_01.xml create mode 100644 spec/fixtures/odin_file_02.xml diff --git a/Gemfile b/Gemfile index 9c00edf3..30662ddb 100644 --- a/Gemfile +++ b/Gemfile @@ -49,6 +49,7 @@ gem 'attribute_normalizer' gem 'ice_cube' # At time of development 01-06-2022 mmddyyyy necessary fix for config_helper.rb form builder was not in rubygems so we pull from github, see: https://github.com/gregschmit/recurring_select/pull/152 gem 'recurring_select', git: 'https://github.com/gregschmit/recurring_select' +gem 'foodsoft_article_import', git: 'https://git.local-it.org/Foodsoft/foodsoft_article_import', tag: 'v1.0' gem 'roo' gem 'roo-xls' gem 'spreadsheet' diff --git a/Gemfile.lock b/Gemfile.lock index 019f9338..6f60ccef 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,3 +1,11 @@ +GIT + remote: https://git.local-it.org/Foodsoft/foodsoft_article_import + revision: 49a0c1ddb3bb67a357c692c63af0cda2db7c45b0 + tag: v1.0 + specs: + foodsoft_article_import (1.0.0) + roo (~> 2.9.0) + GIT remote: https://github.com/gregschmit/recurring_select revision: 29febc4c4abdd6c30636c33a7d2daecb09973ecf @@ -623,6 +631,7 @@ DEPENDENCIES exception_notification factory_bot_rails faker + foodsoft_article_import! foodsoft_discourse! foodsoft_documents! foodsoft_links! diff --git a/app/controllers/articles_controller.rb b/app/controllers/articles_controller.rb index 31481f18..16b506e8 100644 --- a/app/controllers/articles_controller.rb +++ b/app/controllers/articles_controller.rb @@ -148,11 +148,12 @@ class ArticlesController < ApplicationController # Update articles from a spreadsheet def parse_upload uploaded_file = params[:articles]['file'] or raise I18n.t('articles.controller.parse_upload.no_file') + type = params[:articles]['type'] options = { filename: uploaded_file.original_filename } options[:outlist_absent] = (params[:articles]['outlist_absent'] == '1') options[:convert_units] = (params[:articles]['convert_units'] == '1') options[:update_category] = (params[:articles]['update_category'] == '1') - @updated_article_pairs, @outlisted_articles, @new_articles = @supplier.sync_from_file uploaded_file.tempfile, options + @updated_article_pairs, @outlisted_articles, @new_articles = @supplier.sync_from_file uploaded_file.tempfile, type, options if @updated_article_pairs.empty? && @outlisted_articles.empty? && @new_articles.empty? redirect_to supplier_articles_path(@supplier), :notice => I18n.t('articles.controller.parse_upload.notice') end diff --git a/app/lib/foodsoft_file.rb b/app/lib/foodsoft_file.rb deleted file mode 100644 index 95d06c60..00000000 --- a/app/lib/foodsoft_file.rb +++ /dev/null @@ -1,25 +0,0 @@ -# Foodsoft-file import -class FoodsoftFile - # parses a string from a foodsoft-file - # returns two arrays with articles and outlisted_articles - # the parsed article is a simple hash - def self.parse(file, options = {}) - SpreadsheetFile.parse file, options do |row, row_index| - next if row[2].blank? - - article = { :order_number => row[1], - :name => row[2], - :note => row[3], - :manufacturer => row[4], - :origin => row[5], - :unit => row[6], - :price => row[7], - :tax => row[8], - :deposit => (row[9].nil? ? "0" : row[9]), - :unit_quantity => row[10], - :article_category => row[13] } - status = row[0] && row[0].strip.downcase == 'x' ? :outlisted : nil - yield status, article, row_index - end - end -end diff --git a/app/lib/spreadsheet_file.rb b/app/lib/spreadsheet_file.rb deleted file mode 100644 index dbca9c90..00000000 --- a/app/lib/spreadsheet_file.rb +++ /dev/null @@ -1,22 +0,0 @@ -require 'roo' - -class SpreadsheetFile - def self.parse(file, options = {}) - filepath = file.is_a?(String) ? file : file.to_path - filename = options.delete(:filename) || filepath - fileext = File.extname(filename) - options[:csv_options] = { col_sep: ';', encoding: 'utf-8' }.merge(options[:csv_options] || {}) - s = Roo::Spreadsheet.open(filepath, options.merge({ extension: fileext })) - - row_index = 1 - s.each do |row| - if row_index == 1 - # @todo try to detect headers; for now using the index is ok - else - yield row, row_index - end - row_index += 1 - end - row_index - end -end diff --git a/app/models/concerns/localize_input.rb b/app/models/concerns/localize_input.rb index cfb44a44..b6330fcc 100644 --- a/app/models/concerns/localize_input.rb +++ b/app/models/concerns/localize_input.rb @@ -8,7 +8,7 @@ module LocalizeInput separator = I18n.t("separator", scope: "number.format") delimiter = I18n.t("delimiter", scope: "number.format") input.gsub!(delimiter, "") if input.match(/\d+#{Regexp.escape(delimiter)}+\d+#{Regexp.escape(separator)}+\d+/) # Remove delimiter - input.gsub!(separator, ".") # Replace separator with db compatible character + input.gsub!(separator, ".") or input.gsub!(",", ".") # Replace separator with db compatible character input rescue Rails.logger.warn "Can't localize input: #{input}" diff --git a/app/models/supplier.rb b/app/models/supplier.rb index a4a7456e..92201022 100644 --- a/app/models/supplier.rb +++ b/app/models/supplier.rb @@ -1,3 +1,4 @@ +require 'foodsoft_article_import' class Supplier < ApplicationRecord include MarkAsDeletedWithName include CustomFields @@ -73,13 +74,16 @@ class Supplier < ApplicationRecord # Synchronise articles with spreadsheet. # # @param file [File] Spreadsheet file to parse - # @param options [Hash] Options passed to {FoodsoftFile#parse} except when listed here. + # @param options [Hash] Options passed to {FoodsoftArticleImport#parse} except when listed here. # @option options [Boolean] :outlist_absent Set to +true+ to remove articles not in spreadsheet. # @option options [Boolean] :convert_units Omit or set to +true+ to keep current units, recomputing unit quantity and price. - def sync_from_file(file, options = {}) + def sync_from_file(file, type, options = {}) all_order_numbers = [] updated_article_pairs, outlisted_articles, new_articles = [], [], [] - FoodsoftFile::parse file, options do |status, new_attrs, line| + custom_codes_path = File.join(Rails.root, "config", "custom_codes.yml") + opts = options.except(:convert_units, :outlist_absent) + custom_codes_file_path = custom_codes_path if File.exist?(custom_codes_path) + FoodsoftArticleImport.parse(file, custom_file_path: custom_codes_file_path, type: type, **opts) do |new_attrs, status, line| article = articles.undeleted.where(order_number: new_attrs[:order_number]).first if new_attrs[:article_category].present? && options[:update_category] diff --git a/app/views/articles/upload.html.haml b/app/views/articles/upload.html.haml index 221e0d1a..dc32fe3a 100644 --- a/app/views/articles/upload.html.haml +++ b/app/views/articles/upload.html.haml @@ -71,9 +71,14 @@ = form_for :articles, :url => parse_upload_supplier_articles_path(@supplier), :html => { multipart: true, class: "form-horizontal" } do |f| + .control-group - %label(for="articles_file")= t '.file_label' + %label(for="articles_file") + %strong= t '.file_label' = f.file_field "file" + %label(for="articles_file") + %strong="select the file type you are about to upload" + =f.collection_select :type, ["bnn","foodsoft","odin"], :to_s, :to_s .control-group %label(for="articles_update_category") diff --git a/spec/controllers/articles_controller_spec.rb b/spec/controllers/articles_controller_spec.rb index b8772054..e2940f48 100644 --- a/spec/controllers/articles_controller_spec.rb +++ b/spec/controllers/articles_controller_spec.rb @@ -187,8 +187,8 @@ describe ArticlesController, type: :controller do describe '#parse_upload' do let(:file) { Rack::Test::UploadedFile.new(Rails.root.join('spec/fixtures/files/upload_test.csv'), original_filename: 'upload_test.csv') } - it 'updates particles from spreadsheet' do - post_with_supplier :parse_upload, params: { articles: { file: file, outlist_absent: '1', convert_units: '1' } } + it 'updates articles from spreadsheet' do + post_with_supplier :parse_upload, params: { articles: { file: file, outlist_absent: '1', convert_units: '1', type: 'foodsoft' } } expect(response).to have_http_status(:success) end diff --git a/spec/fixtures/bnn_file_01.bnn b/spec/fixtures/bnn_file_01.bnn new file mode 100644 index 00000000..177da7be --- /dev/null +++ b/spec/fixtures/bnn_file_01.bnn @@ -0,0 +1,5 @@ +BNN;3;0;Naturkost Nord, Hamburg;T;Angebot Nr. 0922;EUR;20220905;20221001;20220825;837;1 +29932;;;;4280001958081;4280001958203;Walnoten (ongeroosterd);bio;;;med;;GR;C%;DE-?KO-001;120;1302;10;55;;1;;1;1 kg;1;N;930190;99260;;1,41;;;;1;;;4,49;2,34;J;;2;3;;;;;;;;;;;;;;;;;;;A;;;;;Kg;28,571;; +28391;;;;4280001958081;4280001958203;Pijnboompitten;dem;;;med;;GR;C%;DE-?KO-001;120;1302;10;55;;1;;1;100 g;10;N;930190;99260;;1,41;;;;1;;;5,56;2.89;J;;2;3;;;;;;;;;;;;;;;;;;;A;;;;;Kg;28,571;; +1829;;;;4280001958081;4280001958203;Appelsap (verpakt);;;;med;;GR;C%;DE-?KO-001;120;1302;10;55;;1;4x250 ml;10;4x250 ml;10;N;930190;99260;;3,21;;;;1;;;4,49;2.89;J;;2;3;;;;;;;;;;;;;;;;;;;A;;;;;ml;28,571;; +177813;;;;4280001958081;4280001958203;Tomaten;bio;;;med;;GR;C%;DE-?KO-001;120;1302;10;55;;1;;1;500 g;20;N;930190;99260;;1,20;;;;1;;;4,49;2.89;J;;2;3;;;;;;;;;;;;;;;;;;;A;;;;;g;28,571;; diff --git a/spec/fixtures/bnn_file_02.bnn b/spec/fixtures/bnn_file_02.bnn new file mode 100644 index 00000000..e3dba5bb --- /dev/null +++ b/spec/fixtures/bnn_file_02.bnn @@ -0,0 +1,2 @@ +BNN;3;0;Naturkost Nord, Hamburg;T;Angebot Nr. 0922;EUR;20220905;20221001;20220825;837;1 +1;;;;4280001958081;4280001958203;Tomatoes;organic;;;med;;GR;C%;DE-?KO-001;120;1302;10;55;;1;;20;500 g;1;N;930190;99260;;1,41;;;;1;;;4,49;1,20;J;;2;3;;;;;;;;;;;;;;;;;;;A;;;;;g;28,571;; \ No newline at end of file diff --git a/spec/fixtures/odin_file_01.xml b/spec/fixtures/odin_file_01.xml new file mode 100644 index 00000000..3b60e83e --- /dev/null +++ b/spec/fixtures/odin_file_01.xml @@ -0,0 +1,273 @@ + + + +1039 +1.08 +Estafette Associatie C.V. +Geldermalsen + + +8719325207668 +Walnoten (ongeroosterd) +Nucli rose + +0 +0 +0 +1 +kg +Stuk +0 +Het warme woud +bio + + +NL + +6 +1017515 +29932 +10 +Actief +druiven* +0 +0 +2 +2 +0 +0 +0 +2 +2 +0 +2 +0 +2 +0 +2 +2 +2 +2 +1 +0 +2 +0 +2 +2 + + + +0 +0 +0 +0 +1 + +2 +0 + +adviesprijs +2022-08-18 +2.34 +7.95 + + + +8719325207668 +Pijnboompitten +Nucli rose + +0 +0 +0 +100 +g +Stuk +0 +NELEMAN +dem + + +TR + +6 +1017515 +28391 +10 +Actief +druiven* +0 +0 +2 +2 +0 +0 +0 +2 +2 +0 +2 +0 +2 +0 +2 +2 +2 +2 +1 +0 +2 +0 +2 +2 + + + +0 +0 +0 +0 +1 + +2 +0 + +adviesprijs +2022-08-18 +5.56 +7.95 + + + +8719325207668 +Appelsap (verpakt) +Nucli rose + +0 +0 +0 +4x250 +ml +Stuk +0.4 +Appelgaarde + + + +DE + +6 +1017515 +1829 +10 +Actief +druiven* +0 +0 +2 +2 +0 +0 +0 +2 +2 +0 +2 +0 +2 +0 +2 +2 +2 +2 +1 +0 +2 +0 +2 +2 + + + +0 +0 +0 +0 +1 + +2 +0 + +adviesprijs +2022-08-18 +3.21 +7.95 + + + +8719325207668 +Tomaten +Nucli rose + +0 +0 +0 +500 +g +Stuk +0 +De röde hof +bio + + +DE + +6 +1017515 +177813 +20 +Actief +druiven* +0 +0 +2 +2 +0 +0 +0 +2 +2 +0 +2 +0 +2 +0 +2 +2 +2 +2 +1 +0 +2 +0 +2 +2 + + + +0 +0 +0 +0 +1 + +2 +0 + +adviesprijs +2022-08-18 +1.2 +7.95 + + + \ No newline at end of file diff --git a/spec/fixtures/odin_file_02.xml b/spec/fixtures/odin_file_02.xml new file mode 100644 index 00000000..c732b4d5 --- /dev/null +++ b/spec/fixtures/odin_file_02.xml @@ -0,0 +1,75 @@ + + + +1039 +1.08 +Estafette Associatie C.V. +Geldermalsen + + +8719325207668 +Tomatoes +Nucli rose + +0 +0 +0 +500 +g +Stuk +0 +De röde hof +organic + + +Somewhere, UK + +6 +1017515 +1 +20 +Actief +druiven* +0 +0 +2 +2 +0 +0 +0 +2 +2 +0 +2 +0 +2 +0 +2 +2 +2 +2 +1 +0 +2 +0 +2 +2 + + + +0 +0 +0 +0 +1 + +2 +0 + +adviesprijs +2022-08-18 +1.2 +7.95 + + + \ No newline at end of file diff --git a/spec/integration/articles_spec.rb b/spec/integration/articles_spec.rb index 1967a617..638b1c0a 100644 --- a/spec/integration/articles_spec.rb +++ b/spec/integration/articles_spec.rb @@ -1,9 +1,9 @@ require_relative '../spec_helper' feature ArticlesController do - let(:user) { create :user, groups: [create(:workgroup, role_article_meta: true)] } - let(:supplier) { create :supplier } - let!(:article_category) { create :article_category } + let(:user) { create(:user, groups: [create(:workgroup, role_article_meta: true)]) } + let(:supplier) { create(:supplier) } + let!(:article_category) { create(:article_category) } before { login user } @@ -18,7 +18,7 @@ feature ArticlesController do it 'can create a new article' do click_on I18n.t('articles.index.new') expect(page).to have_selector('form#new_article') - article = build :article, supplier: supplier, article_category: article_category + article = build(:article, supplier: supplier, article_category: article_category) within('#new_article') do fill_in 'article_name', :with => article.name fill_in 'article_unit', :with => article.unit @@ -49,6 +49,7 @@ feature ArticlesController do let(:file) { Rails.root.join(test_file) } it do + find("#articles_type option[value='foodsoft']").select_option find('input[type="submit"]').click expect(find("tr:nth-child(1) #new_articles__note").value).to eq "bio ◎" expect(find("tr:nth-child(2) #new_articles__name").value).to eq "Pijnboompitten" @@ -64,81 +65,124 @@ feature ArticlesController do end end - describe "can update existing article" do - let!(:article) { create :article, supplier: supplier, name: 'Foobar', order_number: 1, unit: '250 g' } + Dir.glob('spec/fixtures/bnn_file_01.*') do |test_file| + describe "can import articles from #{test_file}" do + let(:file) { Rails.root.join(test_file) } - it do - find('input[type="submit"]').click - expect(find("#articles_#{article.id}_name").value).to eq 'Tomatoes' - find('input[type="submit"]').click - article.reload - expect(article.name).to eq 'Tomatoes' - expect([article.unit, article.unit_quantity, article.price]).to eq ['500 g', 20, 1.2] + it do + find("#articles_type option[value='bnn']").select_option + find('input[type="submit"]').click + expect(find("tr:nth-child(1) #new_articles__note").value).to eq "bio" + expect(find("tr:nth-child(1) #new_articles__name").value).to eq "Walnoten (ongeroosterd)" + # set article category + 4.times do |i| + all("tr:nth-child(#{i + 1}) select > option")[1].select_option + end + find('input[type="submit"]').click + + expect(page).to have_content("Pijnboompitten") + + expect(supplier.articles.count).to eq 4 + end end end + end - describe "handles missing data" do - it do - find('input[type="submit"]').click # to overview - find('input[type="submit"]').click # missing category, re-show form - expect(find('tr.alert')).to be_present - expect(supplier.articles.count).to eq 0 + describe "updates" do + file_paths = ['spec/fixtures/foodsoft_file_02.csv', 'spec/fixtures/bnn_file_02.bnn', 'spec/fixtures/odin_file_02.xml'] + let(:filename) { 'foodsoft_file_02.csv' } + let(:file) { Rails.root.join("spec/fixtures/#{filename}") } + let(:val) { 'foodsoft' } + let(:type) { %w[foodsoft bnn odin] } - all("tr select > option")[1].select_option - find('input[type="submit"]').click # now it should succeed - expect(supplier.articles.count).to eq 1 - end + before do + visit upload_supplier_articles_path(supplier_id: supplier.id) + attach_file 'articles_file', file + find("#articles_type option[value='#{val}']").select_option end - describe "takes over category from file" do - it do - find(:css, '#articles_update_category[value="1"]').set(true) # check take over category from file - expect(ArticleCategory.count).to eq 1 # new Category vegetables should be created from file - find('input[type="submit"]').click # upload file - find('input[type="submit"]').click # submit changes - expect(ArticleCategory.count).to eq 2 # it is - expect(supplier.articles.count).to eq 1 - expect(supplier.articles.first.article_category.name).to eq "Vegetables" + file_paths.each_with_index do |test_file, index| + describe "updates article for #{test_file}" do + let(:article) { create(:article, supplier: supplier, name: 'Foobar', order_number: 1, unit: '250 g') } + let(:file) { Rails.root.join(test_file) } + let(:val) { type[index] } + + it do + article.reload + find('input[type="submit"]').click + expect(find("#articles_#{article.id}_name").value).to eq 'Tomatoes' + find('input[type="submit"]').click + article.reload + expect(article.name).to eq 'Tomatoes' + if type[index] == "odin" + expect([article.unit, article.unit_quantity, article.price]).to eq ['500gr', 20, 1.20] + else + expect([article.unit, article.unit_quantity, article.price]).to eq ['500 g', 20, 1.20] + end + end + + it "handles missing data" do + find('input[type="submit"]').click # to overview + find('input[type="submit"]').click # missing category, re-show form + expect(find('tr.alert')).to be_present + expect(supplier.articles.count).to eq 0 + + all("tr select > option")[1].select_option + find('input[type="submit"]').click # now it should succeed + expect(supplier.articles.count).to eq 1 + end end - end - describe "overwrites article_category from file" do - let!(:article_category) { create(:article_category, name: "Fruit") } - let(:article) { create(:article, supplier: supplier, name: 'Tomatoes', order_number: 1, unit: '250 g', article_category: article_category) } - - it do - find(:css, '#articles_update_category[value="1"]').set(true) # check take over category from file - find('input[type="submit"]').click #upload file - find('input[type="submit"]').click #submit changes - expect(supplier.articles.count).to eq 1 - expect(supplier.articles.first.article_category.name).to eq "Vegetables" + describe "takes over category from file" do + it do + find(:css, '#articles_update_category[value="1"]').set(true) # check take over category from file + expect(ArticleCategory.count).to eq 1 # new Category vegetables should be created from file + find('input[type="submit"]').click # upload file + find('input[type="submit"]').click # submit changes + expect(ArticleCategory.count).to eq 2 # it is + expect(supplier.articles.count).to eq 1 + expect(supplier.articles.first.article_category.name).to eq "Vegetables" + end end - end - describe "can remove an existing article" do - let!(:article) { create :article, supplier: supplier, name: 'Foobar', order_number: 99999 } + describe "overwrites article_category from file" do + let!(:article_category) { create(:article_category, name: "Fruit") } + let(:article) { create(:article, supplier: supplier, name: 'Tomatoes', order_number: 1, unit: '250 g', article_category: article_category) } - it do - check('articles_outlist_absent') - find('input[type="submit"]').click - expect(find("#outlisted_articles_#{article.id}", visible: :all)).to be_present - - all("tr select > option")[1].select_option - find('input[type="submit"]').click - expect(article.reload.deleted?).to be true + it do + find(:css, '#articles_update_category[value="1"]').set(true) # check take over category from file + find('input[type="submit"]').click #upload file + find('input[type="submit"]').click #submit changes + expect(supplier.articles.count).to eq 1 + expect(supplier.articles.first.article_category.name).to eq "Vegetables" + end end - end - describe "can convert units when updating" do - let!(:article) { create :article, supplier: supplier, order_number: 1, unit: '250 g' } + describe "can remove an existing article" do + let!(:article) { create(:article, supplier: supplier, name: 'Foobar', order_number: 99999) } - it do - check('articles_convert_units') - find('input[type="submit"]').click - expect(find("#articles_#{article.id}_name").value).to eq 'Tomatoes' - find('input[type="submit"]').click - article.reload - expect([article.unit, article.unit_quantity, article.price]).to eq ['250 g', 40, 0.6] + it do + check('articles_outlist_absent') + find('input[type="submit"]').click + expect(find("#outlisted_articles_#{article.id}", visible: :all)).to be_present + + all("tr select > option")[1].select_option + find('input[type="submit"]').click + expect(article.reload.deleted?).to be true + end + end + + describe "can convert units when updating" do + let!(:article) { create(:article, supplier: supplier, order_number: 1, unit: '250 g') } + + it do + check('articles_convert_units') + find('input[type="submit"]').click + expect(find("#articles_#{article.id}_name").value).to eq 'Tomatoes' + find('input[type="submit"]').click + article.reload + expect([article.unit, article.unit_quantity, article.price]).to eq ['250 g', 40, 0.6] + end end end end diff --git a/spec/models/supplier_spec.rb b/spec/models/supplier_spec.rb index 6bcc6e7b..42b4a304 100644 --- a/spec/models/supplier_spec.rb +++ b/spec/models/supplier_spec.rb @@ -11,7 +11,7 @@ describe Supplier do options = { filename: 'foodsoft_file_01.csv' } options[:outlist_absent] = true options[:convert_units] = true - updated_article_pairs, outlisted_articles, new_articles = supplier.sync_from_file(Rails.root.join('spec/fixtures/foodsoft_file_01.csv'), options) + updated_article_pairs, outlisted_articles, new_articles = supplier.sync_from_file(Rails.root.join('spec/fixtures/foodsoft_file_01.csv'), "foodsoft", options) expect(new_articles.length).to be > 0 expect(updated_article_pairs.first[1][:name]).to eq 'Tomaten' expect(outlisted_articles.first).to eq article2 From d81ae10dc886f2eb8f6fd2ee7803fa2f58560f81 Mon Sep 17 00:00:00 2001 From: viehlieb Date: Tue, 14 Feb 2023 16:51:22 +0100 Subject: [PATCH 022/105] feat(order): export order to custom csv file add custom_csv_collection to orders helper add rute and controller method to orders controller add custom csv to download dropdown add functionality to choose column headers + order for custom csv and append order.sum gross&net to custom csv --- app/controllers/orders_controller.rb | 15 ++++- app/helpers/orders_helper.rb | 12 ++++ app/lib/order_csv.rb | 64 ++++++++++++++----- app/lib/render_csv.rb | 1 + app/views/orders/_custom_csv_form.html.haml | 15 +++++ app/views/orders/custom_csv.js.haml | 3 + .../shared/_order_download_button.html.haml | 1 + config/locales/de.yml | 3 + config/locales/en.yml | 4 ++ config/locales/es.yml | 3 + config/locales/fr.yml | 3 + config/locales/nl.yml | 3 + config/routes.rb | 1 + 13 files changed, 111 insertions(+), 17 deletions(-) create mode 100644 app/views/orders/_custom_csv_form.html.haml create mode 100644 app/views/orders/custom_csv.js.haml diff --git a/app/controllers/orders_controller.rb b/app/controllers/orders_controller.rb index cfa7cef6..1e041bf2 100644 --- a/app/controllers/orders_controller.rb +++ b/app/controllers/orders_controller.rb @@ -49,7 +49,7 @@ class OrdersController < ApplicationController send_order_pdf @order, params[:document] end format.csv do - send_data OrderCsv.new(@order).to_csv, filename: @order.name + '.csv', type: 'text/csv' + send_data OrderCsv.new(@order, options= {custom_csv: params[:custom_csv]}).to_csv, filename: @order.name + '.csv', type: 'text/csv' end format.text do send_data OrderTxt.new(@order).to_txt, filename: @order.name + '.txt', type: 'text/plain' @@ -57,6 +57,19 @@ class OrdersController < ApplicationController end end + def custom_csv + @order = Order.find(params[:id]) + @view = (params[:view] || 'default').gsub(/[^-_a-zA-Z0-9]/, '') + @partial = case @view + when 'default' then 'articles' + when 'groups' then 'shared/articles_by/groups' + when 'articles' then 'shared/articles_by/articles' + else 'articles' + end + + render :layout => false + end + # Page to create a new order. def new if params[:order_id] diff --git a/app/helpers/orders_helper.rb b/app/helpers/orders_helper.rb index ff238730..5f7fb904 100644 --- a/app/helpers/orders_helper.rb +++ b/app/helpers/orders_helper.rb @@ -155,4 +155,16 @@ module OrdersHelper link_to t('orders.index.action_receive'), receive_order_path(order), class: "btn#{' btn-success' unless order.received?} #{options[:class]}" end end + + def custom_csv_collection + [ + OrderArticle.human_attribute_name(:units_to_order), + Article.human_attribute_name(:order_number), + Article.human_attribute_name(:name), + Article.human_attribute_name(:unit), + Article.human_attribute_name(:unit_quantity_short), + ArticlePrice.human_attribute_name(:price), + OrderArticle.human_attribute_name(:total_price) + ] + end end diff --git a/app/lib/order_csv.rb b/app/lib/order_csv.rb index b238f90c..653edf90 100644 --- a/app/lib/order_csv.rb +++ b/app/lib/order_csv.rb @@ -2,28 +2,60 @@ require 'csv' class OrderCsv < RenderCsv def header - [ - OrderArticle.human_attribute_name(:units_to_order), - Article.human_attribute_name(:order_number), - Article.human_attribute_name(:name), - Article.human_attribute_name(:unit), - Article.human_attribute_name(:unit_quantity_short), - ArticlePrice.human_attribute_name(:price), - OrderArticle.human_attribute_name(:total_price) - ] + params = @options[:custom_csv] + arr = if params.nil? + [ + OrderArticle.human_attribute_name(:units_to_order), + Article.human_attribute_name(:order_number), + Article.human_attribute_name(:name), + Article.human_attribute_name(:unit), + Article.human_attribute_name(:unit_quantity_short), + ArticlePrice.human_attribute_name(:price), + OrderArticle.human_attribute_name(:total_price) + ] + else + [ + params[:first], + params[:second], + params[:third], + params[:fourth], + params[:fifth], + params[:sixth], + params[:seventh] + ] + end end def data @object.order_articles.ordered.includes([:article, :article_price]).all.map do |oa| yield [ - oa.units_to_order, - oa.article.order_number, - oa.article.name, - oa.article.unit, - oa.price.unit_quantity > 1 ? oa.price.unit_quantity : nil, - number_to_currency(oa.price.price * oa.price.unit_quantity), - number_to_currency(oa.total_price) + match_params(oa, header[0]), + match_params(oa, header[1]), + match_params(oa, header[2]), + match_params(oa, header[3]), + match_params(oa, header[4]), + match_params(oa, header[5]), + match_params(oa, header[6]) ] end end + + def match_params(object, attribute) + case attribute + when OrderArticle.human_attribute_name(:units_to_order) + object.units_to_order + when Article.human_attribute_name(:order_number) + object.article.order_number + when Article.human_attribute_name(:name) + object.article.name + when Article.human_attribute_name(:unit) + object.article.unit + when Article.human_attribute_name(:unit_quantity_short) + object.price.unit_quantity > 1 ? object.price.unit_quantity : nil + when ArticlePrice.human_attribute_name(:price) + number_to_currency(object.price.price * object.price.unit_quantity) + when OrderArticle.human_attribute_name(:total_price) + number_to_currency(object.total_price) + end + end end diff --git a/app/lib/render_csv.rb b/app/lib/render_csv.rb index 1f20b075..c1fd24db 100644 --- a/app/lib/render_csv.rb +++ b/app/lib/render_csv.rb @@ -20,6 +20,7 @@ class RenderCsv end data { |d| csv << d } end + ret << I18n.t('.orders.articles.prices_sum') << ";" << "#{number_to_currency(@object.sum(:gross))}/#{number_to_currency(@object.sum(:net))}" if @options[:custom_csv] ret.encode(@options[:encoding], invalid: :replace, undef: :replace) end diff --git a/app/views/orders/_custom_csv_form.html.haml b/app/views/orders/_custom_csv_form.html.haml new file mode 100644 index 00000000..87295af0 --- /dev/null +++ b/app/views/orders/_custom_csv_form.html.haml @@ -0,0 +1,15 @@ += simple_form_for :custom_csv,format: :csv, :url => order_path(@order, view: @view, format: :csv), method: :get do |f| + .modal-header + = close_button :modal + .h3=I18n.t('.orders.custom_csv.description') + .modal-body + = f.input :first, as: :select, collection: custom_csv_collection, label: "1. " + I18n.t('.orders.custom_csv.column') + = f.input :second, as: :select, collection: custom_csv_collection, required: false, label: "2. " + I18n.t('.orders.custom_csv.column') + = f.input :third, as: :select, collection: custom_csv_collection, required: false, label: "3. " + I18n.t('.orders.custom_csv.column') + = f.input :fourth, as: :select, collection: custom_csv_collection, required: false, label: "4. " + I18n.t('.orders.custom_csv.column') + = f.input :fifth, as: :select, collection: custom_csv_collection, required: false, label: "5. " + I18n.t('.orders.custom_csv.column') + = f.input :sixth, as: :select, collection: custom_csv_collection, required: false, label: "6. " + I18n.t('.orders.custom_csv.column') + = f.input :seventh, as: :select, collection: custom_csv_collection, required: false, label: "7. " + I18n.t('.orders.custom_csv.column') + .modal-footer + = link_to t('ui.close'), '#', class: 'btn', data: {dismiss: 'modal'} + = f.submit class: 'btn btn-primary' diff --git a/app/views/orders/custom_csv.js.haml b/app/views/orders/custom_csv.js.haml new file mode 100644 index 00000000..41a6ec83 --- /dev/null +++ b/app/views/orders/custom_csv.js.haml @@ -0,0 +1,3 @@ +$('#modalContainer').html('#{j(render("custom_csv_form"))}'); +$('#modalContainer').modal(); +$('#modalContainer').submit(function() {$('#modalContainer').modal('hide');}); \ No newline at end of file diff --git a/app/views/shared/_order_download_button.html.haml b/app/views/shared/_order_download_button.html.haml index 6890c3ca..2c362533 100644 --- a/app/views/shared/_order_download_button.html.haml +++ b/app/views/shared/_order_download_button.html.haml @@ -10,3 +10,4 @@ - unless order.stockit? %li= link_to t('.fax_txt'), order_path(order, format: :txt), {title: t('.download_file')} %li= link_to t('.fax_csv'), order_path(order, format: :csv), {title: t('.download_file')} + %li= link_to t('.custom_csv'), custom_csv_order_path(order), remote: true diff --git a/config/locales/de.yml b/config/locales/de.yml index 9b569572..9d51bdf1 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -1465,6 +1465,9 @@ de: units_ordered: Bestellte Einheiten create: notice: Die Bestellung wurde erstellt. + custom_csv: + description: Wähle die Attribute und deren Reihenfolge für die zu erzeugende CSV Datei + column: Spalte edit: title: 'Bestellung bearbeiten: %{name}' edit_amount: diff --git a/config/locales/en.yml b/config/locales/en.yml index 30a1fb53..ec6baab5 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1475,6 +1475,9 @@ en: units_ordered: Units ordered create: notice: The order was created. + custom_csv: + description: Please choose the order as well as the attributes for the csv file + column: column edit: title: 'Edit order: %{name}' edit_amount: @@ -1628,6 +1631,7 @@ en: who_ordered: Who ordered? order_download_button: article_pdf: Article PDF + custom_csv: Custom CSV download_file: Download file fax_csv: Fax CSV fax_pdf: Fax PDF diff --git a/config/locales/es.yml b/config/locales/es.yml index 8bbd69d6..4004b5c5 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -1261,6 +1261,9 @@ es: units_ordered: Unidades pedidas create: notice: Se ha creado el pedido + custom_csv: + description: Por favor elija el orden de los atributos así como los atributos para el archivo csv + column: columna edit: title: 'Edita pedido: %{name}' edit_amount: diff --git a/config/locales/fr.yml b/config/locales/fr.yml index b1199dc7..304a7b9d 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -1011,6 +1011,9 @@ fr: units_ordered: Unités commandées create: notice: La commande a bien été définie. + custom_csv: + description: Veuillez choisir l'ordre des attributs ainsi que les attributs pour le fichier csv + column: colonne edit: title: 'Modifier la commande: %{name}' edit_amount: diff --git a/config/locales/nl.yml b/config/locales/nl.yml index 31179a6d..facd55d0 100644 --- a/config/locales/nl.yml +++ b/config/locales/nl.yml @@ -1440,6 +1440,9 @@ nl: units_ordered: Bestelde eenheden create: notice: De bestelling is aangemaakt. + custom_csv: + description: Kies de volgorde van de attributen en de attributen voor het csv-bestand + column: kolom edit: title: 'Bestelling aanpassen: %{name}' edit_amount: diff --git a/config/routes.rb b/config/routes.rb index 83e65707..b82699ec 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -47,6 +47,7 @@ Rails.application.routes.draw do get :receive post :receive + get :custom_csv get :receive_on_order_article_create get :receive_on_order_article_update end From 6f2a3b4f5fdf191bb47c5e687e06ef5e429cd7e3 Mon Sep 17 00:00:00 2001 From: viehlieb Date: Tue, 5 Apr 2022 13:20:06 +0200 Subject: [PATCH 023/105] fix behavior - when link is provided in article details not clickable due to hover property solve hover problem for ordering articles --- app/assets/stylesheets/bootstrap_and_overrides.css.less | 9 +++++++-- app/views/group_orders/_form.html.haml | 4 ++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/app/assets/stylesheets/bootstrap_and_overrides.css.less b/app/assets/stylesheets/bootstrap_and_overrides.css.less index 971308c9..ebd30b20 100644 --- a/app/assets/stylesheets/bootstrap_and_overrides.css.less +++ b/app/assets/stylesheets/bootstrap_and_overrides.css.less @@ -241,6 +241,9 @@ table { tr.order-article:hover .article-info { display: none; } + tr.order-article:focus .article-info { + display: none; + } } #order-footer { @@ -275,11 +278,13 @@ tr.order-article .article-info { display: none; } -tr.order-article:hover .article-info { +tr.order-article:focus{ + background-color: #E4EED6; +} +tr.order-article:focus .article-info { display: block; } - // ********* Articles tr.just-updated { diff --git a/app/views/group_orders/_form.html.haml b/app/views/group_orders/_form.html.haml index 3ffd583e..0cd27c76 100644 --- a/app/views/group_orders/_form.html.haml +++ b/app/views/group_orders/_form.html.haml @@ -69,7 +69,7 @@ = f.hidden_field :order_id = f.hidden_field :updated_by_user_id = f.hidden_field :ordergroup_id - %table.table.table-hover + %table.table %thead %tr %th= heading_helper Article, :name @@ -94,7 +94,7 @@ %i.icon-tag %td{colspan: "9"} - order_articles.each do |order_article| - %tr{class: "#{cycle('even', 'odd', name: 'articles')} order-article #{get_missing_units_css_class(@ordering_data[:order_articles][order_article.id][:missing_units])}", valign: "top"} + %tr{class: "#{cycle('even', 'odd', name: 'articles')} order-article #{get_missing_units_css_class(@ordering_data[:order_articles][order_article.id][:missing_units])}", valign: "top", tabindex: "0"} %td.name= order_article.article.name - if @order.stockit? %td= truncate order_article.article.supplier.name, length: 15 From 75bb400d0de7e652f37d3a18511e56f1adb57821 Mon Sep 17 00:00:00 2001 From: Philipp Rothmann Date: Mon, 20 Feb 2023 18:52:13 +0100 Subject: [PATCH 024/105] feat: improve usability of group order remove group order panel close buttons things shouldn't just disapear order article disabled button should be gray roup order swap plus and minus buttons because it's more naturally intuitive like this group order make order details collapse group order pull search to the right group order make 'current orders' more obvious fix switch menu rework group order edit form * make switch order a menu list * table more slim * alert when balance negative instead of making everything red * search to the right wip: rework group order details tried to reduce the amount of informations shown. but needs some user feedback, what information are actually relevant rework group order show view dashboard make show edit current order action more precise group order package indication uses more color-blind friendly color group order fix dancing quantity buttons group order switch menu use show view group order show window with some explanations group order edit title more clear grou order edit show less infos group order switch view next iteration grou order index narrower tables move order details to show again remove unused stuff --- app/assets/javascripts/ordering.js | 10 +- .../bootstrap_and_overrides.css.less | 9 +- app/assets/stylesheets/list.missing.css | 24 +- app/views/group_orders/_explanations.haml | 15 + app/views/group_orders/_form.html.haml | 278 ++++++++---------- .../group_orders/_switch_order.html.haml | 15 +- app/views/group_orders/_total_sum.haml | 19 ++ app/views/group_orders/index.html.haml | 35 ++- app/views/group_orders/show.html.haml | 194 ++++++------ app/views/shared/_open_orders.html.haml | 11 +- config/locales/de.yml | 16 +- config/locales/en.yml | 16 +- config/locales/nl.yml | 3 +- 13 files changed, 353 insertions(+), 292 deletions(-) create mode 100644 app/views/group_orders/_explanations.haml create mode 100644 app/views/group_orders/_total_sum.haml diff --git a/app/assets/javascripts/ordering.js b/app/assets/javascripts/ordering.js index a3dd1050..1097f8a7 100644 --- a/app/assets/javascripts/ordering.js +++ b/app/assets/javascripts/ordering.js @@ -179,17 +179,13 @@ function updateBalance() { var balance = groupBalance - total; $('#new_balance').html(I18n.l("currency", balance)); $('#total_balance').val(I18n.l("currency", balance)); - // determine bgcolor and submit button state according to balance - var bgcolor = ''; if (balance < minimumBalance) { - bgcolor = '#FF0000'; $('#submit_button').attr('disabled', 'disabled') + $('#balance-alert').css('display', 'block') + } else { $('#submit_button').removeAttr('disabled') - } - // update bgcolor - for (i in itemTotal) { - $('#td_price_' + i).css('background-color', bgcolor); + $('#balance-alert').css('display', 'none') } } diff --git a/app/assets/stylesheets/bootstrap_and_overrides.css.less b/app/assets/stylesheets/bootstrap_and_overrides.css.less index ebd30b20..3d98e4a5 100644 --- a/app/assets/stylesheets/bootstrap_and_overrides.css.less +++ b/app/assets/stylesheets/bootstrap_and_overrides.css.less @@ -230,7 +230,7 @@ table { margin: .5em 0; input:disabled { - background-color: red; } + background-color: gray; } } } } @@ -278,13 +278,14 @@ tr.order-article .article-info { display: none; } -tr.order-article:focus{ - background-color: #E4EED6; -} tr.order-article:focus .article-info { display: block; } +tr.order-article:focus { + background-color: #E9E9E9; +} + // ********* Articles tr.just-updated { diff --git a/app/assets/stylesheets/list.missing.css b/app/assets/stylesheets/list.missing.css index 4eea5d78..2dc36577 100644 --- a/app/assets/stylesheets/list.missing.css +++ b/app/assets/stylesheets/list.missing.css @@ -1,11 +1,23 @@ -.list .missing-many td, .list .missing-many:hover td { - background-color: #ebbebe; +.missing-many td { + background-color: #ffc590aa; } -.list .missing-few td, .list .missing-few:hover td { - background-color: #ffee75; +.missing-many:hover td, .missing-many:focus td { + background-color: #ffc590; } -.list .missing-none td, .list .missing-none:hover td { - background-color: #E4EED6; +.missing-few td { + background-color: #fcf488aa; +} + +.missing-few:hover td, .missing-few:focus td { + background-color: #fcf488; +} + +.missing-none td { + background-color: #d0f6ffaa; +} + +.missing-none:hover td, .missing-none:focus td { + background-color: #d0f6ff; } diff --git a/app/views/group_orders/_explanations.haml b/app/views/group_orders/_explanations.haml new file mode 100644 index 00000000..30e5b91c --- /dev/null +++ b/app/views/group_orders/_explanations.haml @@ -0,0 +1,15 @@ +%h4= t '.title' +%hr +%table.table-condensed + %thead + %th= t '.package_fill_level' + %tbody + %tr{class: "missing-none"} + %td= t '.missing_none' + %tr{class: "missing-few"} + %td= t '.missing_few' + %tr{class: "missing-many"} + %td= t '.missing_many' +%hr + %b= t('.tolerance') + ':' + = t '.tolerance_explained' \ No newline at end of file diff --git a/app/views/group_orders/_form.html.haml b/app/views/group_orders/_form.html.haml index 0cd27c76..9d13e525 100644 --- a/app/views/group_orders/_form.html.haml +++ b/app/views/group_orders/_form.html.haml @@ -11,170 +11,142 @@ var listjsResetPlugin = ['reset', {highlightClass: 'btn-primary'}]; var listjsDelayPlugin = ['delay', {delayedSearchTime: 500}]; new List(document.body, { - valueNames: ['name'], - engine: 'unlist', - plugins: [listjsResetPlugin, listjsDelayPlugin], - // make large pages work too (as we don't have paging - articles may disappear!) - page: 10000, - indexAsync: true + valueNames: ['name'], + engine: 'unlist', + plugins: [listjsResetPlugin, listjsDelayPlugin], + // make large pages work too (as we don't have paging - articles may disappear!) + page: 10000, + indexAsync: true }); }); - title t('.title'), false +.alert.alert-error#balance-alert{style: ('display:none')} + =t 'group_orders.errors.balance_alert' .row-fluid - .well.pull-left - = close_button :alert - %h2= @order.name - %dl.dl-horizontal - - unless @order.note.blank? - %dt= heading_helper Order, :note - %dd= @order.note - %dt= heading_helper Order, :created_by - %dd= show_user_link(@order.created_by) - %dt= heading_helper Order, :ends - %dd= format_time(@order.ends) - %dt= heading_helper Order, :pickup - %dd= format_date(@order.pickup) - - unless @order.stockit? or @order.supplier.min_order_quantity.blank? - %dt= heading_helper Supplier, :min_order_quantity, short: true - %dd= @order.supplier.min_order_quantity - %dt= t '.sum_amount' - %dd= number_to_currency @order.sum - - unless @group_order.new_record? - %dt= heading_helper GroupOrder, :updated_by - %dd - = show_user(@group_order.updated_by) - (#{format_time(@group_order.updated_on)}) - %dt= heading_helper Ordergroup, :account_balance - %dd= number_to_currency(@ordering_data[:account_balance]) - - unless FoodsoftConfig[:charge_members_manually] - %dt= heading_helper Ordergroup, :available_funds - %dd= number_to_currency(@ordering_data[:available_funds]) - - .well.pull-right - = close_button :alert - = render 'switch_order', current_order: @order - -.row-fluid - .well.clear - .form-search + .span2 + .well + = render 'switch_order', current_order: @order + .well + = render 'explanations' + .well.span9 + %h2.span9= t '.sub_title', order_name: @order.name + .span3 + %table.table-condensed + -if @order.ends + %tr + %td= heading_helper(Order, :ends) + ': ' + %td= format_time(@order.ends) + - unless @order.stockit? or @order.supplier.min_order_quantity.blank? + %tr + %td= heading_helper(Supplier, :min_order_quantity) + %td= number_to_currency(@order.supplier.min_order_quantity) + %tr + %td= t('group_orders.form.sum_amount') + ':' + %td= number_to_currency(@order.sum) + %hr + .form-search.pull-right .input-append = text_field_tag :article, params[:article], placeholder: t('.search_article'), class: 'search-query delayed-search resettable' %button.add-on.btn.reset-search{:type => :button, :title => t('.reset_article_search')} %i.icon.icon-remove - -= form_for @group_order do |f| - = f.hidden_field :lock_version - = f.hidden_field :order_id - = f.hidden_field :updated_by_user_id - = f.hidden_field :ordergroup_id - %table.table - %thead - %tr - %th= heading_helper Article, :name - - if @order.stockit? - %th{style: 'width:120px'}= heading_helper StockArticle, :supplier - %th{style: "width:13px;"} - %th{style: "width:4.5em;"}= t '.price' - %th{style: "width:4.5em;"}= heading_helper Article, :unit - - unless @order.stockit? - %th{style: "width:70px;"}= heading_helper OrderArticle, :missing_units, short: true - %th#col_required= heading_helper GroupOrderArticle, :quantity - %th#col_tolerance= heading_helper GroupOrderArticle, :tolerance - - else - %th(style="width:20px")= heading_helper StockArticle, :available - %th#col_required= heading_helper GroupOrderArticle, :quantity - %th{style: "width:15px;"}= heading_helper GroupOrderArticle, :total_price - %tbody.list - - @order.articles_grouped_by_category.each do |category, order_articles| - %tr.list-heading.article-category - %td - = category - %i.icon-tag - %td{colspan: "9"} - - order_articles.each do |order_article| - %tr{class: "#{cycle('even', 'odd', name: 'articles')} order-article #{get_missing_units_css_class(@ordering_data[:order_articles][order_article.id][:missing_units])}", valign: "top", tabindex: "0"} - %td.name= order_article.article.name + = form_for @group_order do |f| + = f.hidden_field :lock_version + = f.hidden_field :order_id + = f.hidden_field :updated_by_user_id + = f.hidden_field :ordergroup_id + %table.table + %thead + %tr + %th= heading_helper Article, :name - if @order.stockit? - %td= truncate order_article.article.supplier.name, length: 15 - %td= h order_article.article.origin - %td= number_to_currency(@ordering_data[:order_articles][order_article.id][:price]) - %td= order_article.article.unit - %td - - if @order.stockit? - = @ordering_data[:order_articles][order_article.id][:quantity_available] - - else - %span{id: "missing_units_#{order_article.id}"}= @ordering_data[:order_articles][order_article.id][:missing_units] + %th{style: 'width:120px'}= heading_helper StockArticle, :supplier + %th{style: "width:13px;"} + %th{style: "width:4.5em;"}= t '.price' + %th{style: "width:4.5em;"}= t '.price_per_base_unit' + %th{style: "width:4.5em;"}= heading_helper Article, :unit + - unless @order.stockit? + %th{style: "width:70px;"}= heading_helper OrderArticle, :missing_units, short: true + %th#col_required= heading_helper GroupOrderArticle, :quantity + %th#col_tolerance= heading_helper GroupOrderArticle, :tolerance + - else + %th(style="width:20px")= heading_helper StockArticle, :available + %th#col_required= heading_helper GroupOrderArticle, :quantity + %th{style: "width:15px;"}= heading_helper GroupOrderArticle, :total_price + %tbody.list + - @order.articles_grouped_by_category.each do | category, order_articles| + %tr.list-heading.article-category + %td + = category + %i.icon-tag + %td{colspan: "9"} + - order_articles.each do |order_article| + %tr{class: "#{cycle('even', 'odd', name: 'articles')} order-article #{get_missing_units_css_class(@ordering_data[:order_articles][order_article.id][:missing_units])}", valign: "top", tabindex: "0"} + %td.name= order_article.article.name + - if @order.stockit? + %td= truncate order_article.article.supplier.name, length: 15 + %td= h order_article.article.origin + %td= number_to_currency(@ordering_data[:order_articles][order_article.id][:price]) + %td= price_per_base_unit(article: order_article.article, price: @ordering_data[:order_articles][order_article.id][:price]) + %td= order_article.article.unit + %td + - if @order.stockit? + = @ordering_data[:order_articles][order_article.id][:quantity_available] + - else + %span{id: "missing_units_#{order_article.id}"}= @ordering_data[:order_articles][order_article.id][:missing_units] - %td.quantity - %input{id: "q_#{order_article.id}", name: "group_order[group_order_articles_attributes][#{order_article.id}][quantity]", type: "hidden", value: @ordering_data[:order_articles][order_article.id][:quantity], 'data-min' => (@ordering_data[:order_articles][order_article.id][:quantity] if @order.boxfill?), 'data-max' => (@ordering_data[:order_articles][order_article.id][:quantity]+@ordering_data[:order_articles][order_article.id][:missing_units] if @order.boxfill?)}/ - %span.used{id: "q_used_#{order_article.id}"}= @ordering_data[:order_articles][order_article.id][:used_quantity] - + - %span.unused{id: "q_unused_#{order_article.id}"}= @ordering_data[:order_articles][order_article.id][:quantity] - @ordering_data[:order_articles][order_article.id][:used_quantity] - .btn-group - %a.btn.btn-ordering{'data-increase_quantity' => order_article.id} - %i.icon-plus - %a.btn.btn-ordering{'data-decrease_quantity' => order_article.id} - %i.icon-minus + %td.quantity + .outer{style: "diyplay: inline-block; float: left; width: 50px;"} + %input{id: "q_#{order_article.id}", name: "group_order[group_order_articles_attributes][#{order_article.id}][quantity]", type: "hidden", value: @ordering_data[:order_articles][order_article.id][:quantity], 'data-min' => (@ordering_data[:order_articles][order_article.id][:quantity] if @order.boxfill?), 'data-max' => (@ordering_data[:order_articles][order_article.id][:quantity]+@ordering_data[:order_articles][order_article.id][:missing_units] if @order.boxfill?)}/ + %span.used{id: "q_used_#{order_article.id}"}= @ordering_data[:order_articles][order_article.id][:used_quantity] + + + %span.unused{id: "q_unused_#{order_article.id}"}= @ordering_data[:order_articles][order_article.id][:quantity] - @ordering_data[:order_articles][order_article.id][:used_quantity] + .btn-group + %a.btn.btn-ordering{'data-decrease_quantity' => order_article.id} + %i.icon-minus + %a.btn.btn-ordering{'data-increase_quantity' => order_article.id} + %i.icon-plus - %td.tolerance{style: ('display:none' if @order.stockit?)} - %input{id: "t_#{order_article.id}", name: "group_order[group_order_articles_attributes][#{order_article.id}][tolerance]", type: "hidden", value: @ordering_data[:order_articles][order_article.id][:tolerance], 'data-min' => (@ordering_data[:order_articles][order_article.id][:tolerance] if @order.boxfill?)}/ - - if (@ordering_data[:order_articles][order_article.id][:unit] > 1) - %span.used{id: "t_used_#{order_article.id}"}= @ordering_data[:order_articles][order_article.id][:used_tolerance] - + - %span.unused{id: "t_unused_#{order_article.id}"}= @ordering_data[:order_articles][order_article.id][:tolerance] - @ordering_data[:order_articles][order_article.id][:used_tolerance] - .btn-group - %a.btn.btn-ordering{'data-increase_tolerance' => order_article.id} - %i.icon-plus - %a.btn.btn-ordering{'data-decrease_tolerance' => order_article.id} - %i.icon-minus + %td.tolerance{style: ('display:none' if @order.stockit?)} + %input{id: "t_#{order_article.id}", name: "group_order[group_order_articles_attributes][#{order_article.id}][tolerance]", type: "hidden", value: @ordering_data[:order_articles][order_article.id][:tolerance], 'data-min' => (@ordering_data[:order_articles][order_article.id][:tolerance] if @order.boxfill?)}/ + - if (@ordering_data[:order_articles][order_article.id][:unit] > 1) + %span.used{id: "t_used_#{order_article.id}"}= @ordering_data[:order_articles][order_article.id][:used_tolerance] + + + %span.unused{id: "t_unused_#{order_article.id}"}= @ordering_data[:order_articles][order_article.id][:tolerance] - @ordering_data[:order_articles][order_article.id][:used_tolerance] + .btn-group + %a.btn.btn-ordering{'data-decrease_tolerance' => order_article.id} + %i.icon-minus + %a.btn.btn-ordering{'data-increase_tolerance' => order_article.id} + %i.icon-plus - %td{id: "td_price_#{order_article.id}", style: "text-align:right; padding-right:10px; width:4em"} - %span{id: "price_#{order_article.id}_display"}= number_to_currency(@ordering_data[:order_articles][order_article.id][:total_price]) - .article-info - .article-name= order_article.article.name - .pull-right - = t('.units_full') + ':' - %span{id: "units_#{order_article.id}"}= order_article.units_to_order - %br/ - = t('.units_total') + ':' - %span{id: "q_total_#{order_article.id}"}= @ordering_data[:order_articles][order_article.id][:quantity] + @ordering_data[:order_articles][order_article.id][:others_quantity] - %br/ - = t('.total_tolerance') + ':' - %span{id: "t_total_#{order_article.id}"}= @ordering_data[:order_articles][order_article.id][:tolerance] + @ordering_data[:order_articles][order_article.id][:others_tolerance] - %br/ - .pull-left - #{heading_helper Article, :manufacturer}: #{order_article.article.manufacturer} - %br/ - #{heading_helper Article, :units}: #{@order.stockit? ? order_article.article.quantity_available : @ordering_data[:order_articles][order_article.id][:unit]} * #{h order_article.article.unit} - %br/ - #{heading_helper Article, :note}: #{order_article.article.note} - %br/ - #order-footer - #info-box - #total-sum - %table - %tr - %td= t('.total_sum_amount') + ':' - %td.currency - %span#total_price= number_to_currency(@group_order.price) - %tr - - if FoodsoftConfig[:charge_members_manually] - - old_balance = @ordering_data[:account_balance] - %td= heading_helper(Ordergroup, :account_balance) + ':' - %td.currency= number_to_currency(@ordering_data[:account_balance]) - - else - - old_balance = @ordering_data[:available_funds] - %td= heading_helper(Ordergroup, :available_funds) + ':' - %td.currency= number_to_currency(@ordering_data[:available_funds]) - %tr - %td= t('.new_funds') + ':' - %td.currency - %strong - %span#new_balance= number_to_currency(old_balance - @group_order.price) - #order-button - = submit_tag( t('.action_save'), id: 'submit_button', class: 'btn btn-primary' ) - #{link_to t('ui.or_cancel'), group_orders_path} - %input#total_balance{name: "total_balance", type: "hidden", value: @ordergroup.account_balance - @group_order.price}/ - %input{name: "version", type: "hidden", value: @version}/ + %td{id: "td_price_#{order_article.id}", style: "text-align:right; padding-right:10px; width:4em"} + %span{id: "price_#{order_article.id}_display"}= number_to_currency(@ordering_data[:order_articles][order_article.id][:total_price]) + .article-info + .article-name= order_article.article.name + .pull-right + = t('.units_full') + ':' + %span{id: "units_#{order_article.id}"}= order_article.units_to_order + %br/ + = t('.units_total') + ':' + %span{id: "q_total_#{order_article.id}"}= @ordering_data[:order_articles][order_article.id][:quantity] + @ordering_data[:order_articles][order_article.id][:others_quantity] + %br/ + = t('.total_tolerance') + ':' + %span{id: "t_total_#{order_article.id}"}= @ordering_data[:order_articles][order_article.id][:tolerance] + @ordering_data[:order_articles][order_article.id][:others_tolerance] + %br/ + .pull-left + #{heading_helper Article, :manufacturer}: #{order_article.article.manufacturer} + %br/ + #{heading_helper Article, :units}: #{@order.stockit? ? order_article.article.quantity_available : @ordering_data[:order_articles][order_article.id][:unit]} * #{h order_article.article.unit} + %br/ + #{heading_helper Article, :note}: #{order_article.article.note} + %br/ + #order-footer + #info-box + #total-sum + = render 'total_sum' + #order-button + = submit_tag( t('.action_save'), id: 'submit_button', class: 'btn btn-primary' ) + #{link_to t('ui.or_cancel'), group_orders_path} + %input#total_balance{name: "total_balance", type: "hidden", value: @ordergroup.account_balance - @group_order.price}/ + %input{name: "version", type: "hidden", value: @version}/ diff --git a/app/views/group_orders/_switch_order.html.haml b/app/views/group_orders/_switch_order.html.haml index 76443524..70234b39 100644 --- a/app/views/group_orders/_switch_order.html.haml +++ b/app/views/group_orders/_switch_order.html.haml @@ -1,9 +1,10 @@ -- orders = Order.open.started.reject{ |order| order == current_order } +- orders = Order.open.started - unless orders.empty? - %h2= t '.title' - %ul.unstyled + %ul.nav.nav-pills.nav-stacked + .nav-header= t '.title' + %li= link_to t('ui.overview'), :group_orders - orders.each do |order| - %li - = link_to_ordering(order, 'data-confirm_switch_order' => true) - - if order.ends - = t '.remaining', remaining: time_ago_in_words(order.ends) + .btn-small.pull-right + =link_to_ordering(order, style: (order == current_order ? 'color: white' : '' ), 'data-confirm_switch_order' => true){ t 'ui.edit' } + %li( class="#{ order == current_order ? 'active' : ''}") + =link_to_ordering(order, show: true, 'data-confirm_switch_order' => true) \ No newline at end of file diff --git a/app/views/group_orders/_total_sum.haml b/app/views/group_orders/_total_sum.haml new file mode 100644 index 00000000..28911b32 --- /dev/null +++ b/app/views/group_orders/_total_sum.haml @@ -0,0 +1,19 @@ +%table + %tr + %td= t('group_orders.form.total_sum_amount') + ':' + %td.currency + %span#total_price= number_to_currency(@group_order.price) + %tr + - if FoodsoftConfig[:charge_members_manually] + - old_balance = @ordering_data[:account_balance] + %td= heading_helper(Ordergroup, :account_balance) + ':' + %td.currency= number_to_currency(@ordering_data[:account_balance]) + - else + - old_balance = @ordering_data[:available_funds] + %td= heading_helper(Ordergroup, :available_funds) + ':' + %td.currency= number_to_currency(@ordering_data[:available_funds]) + %tr + %td= t('group_orders.form.new_funds') + ':' + %td.currency + %strong + %span#new_balance= number_to_currency(old_balance - @group_order.price) diff --git a/app/views/group_orders/index.html.haml b/app/views/group_orders/index.html.haml index 158bc06c..55c97c81 100644 --- a/app/views/group_orders/index.html.haml +++ b/app/views/group_orders/index.html.haml @@ -18,22 +18,27 @@ %th= heading_helper Ordergroup, :available_funds %th.numeric= number_to_currency(@ordergroup.get_available_funds) -= render :partial => "shared/open_orders", :locals => {:ordergroup => @ordergroup} - -// finished orders +.row-fluid + .span9 + = render :partial => "shared/open_orders", :locals => {:ordergroup => @ordergroup} + // finished orders - unless @finished_not_closed_orders_including_group_order.empty? - %section - %h2= t '.finished_orders.title' - = render partial: 'orders', locals: {orders: @finished_not_closed_orders_including_group_order, pagination: false} - - if @ordergroup.value_of_finished_orders > 0 - %p - = t('.finished_orders.total_sum') + ':' - %b= number_to_currency(@ordergroup.value_of_finished_orders) + .row-fluid + .span9 + %section + %h2= t '.finished_orders.title' + = render partial: 'orders', locals: {orders: @finished_not_closed_orders_including_group_order, pagination: false} + - if @ordergroup.value_of_finished_orders > 0 + %p + = t('.finished_orders.total_sum') + ':' + %b= number_to_currency(@ordergroup.value_of_finished_orders) // closed orders - unless @closed_orders_including_group_order.empty? - %section - %h2= t '.closed_orders.title' - = render partial: 'orders', locals: {orders: @closed_orders_including_group_order, pagination: false} - %br/ - = link_to t('.closed_orders.more'), archive_group_orders_path + .row-fluid + .span9 + %section + %h2= t '.closed_orders.title' + = render partial: 'orders', locals: {orders: @closed_orders_including_group_order, pagination: false} + %br/ + = link_to t('.closed_orders.more'), archive_group_orders_path diff --git a/app/views/group_orders/show.html.haml b/app/views/group_orders/show.html.haml index 8c9678d7..b9d4f674 100644 --- a/app/views/group_orders/show.html.haml +++ b/app/views/group_orders/show.html.haml @@ -7,107 +7,115 @@ - title t('.title', order: @order.name) .row-fluid - .well.pull-left - // Order summary + + .well.span2 + = render 'switch_order', current_order: @order + .well.span9 + %h2= t '.articles.title' %dl.dl-horizontal + // Name %dt= heading_helper Order, :name %dd= @order.name - %dt= heading_helper Order, :note - %dd= @order.note + // Order Ends %dt= heading_helper Order, :ends %dd= format_time(@order.ends) - %dt= heading_helper Order, :pickup - %dd= format_date(@order.pickup) - %dt= heading_helper GroupOrder, :price - %dd - - if @group_order - = number_to_currency(@group_order.price) - - else - = t '.not_ordered' - - if @group_order && @group_order.transport - %dt= heading_helper GroupOrder, :transport - %dd= number_to_currency(@group_order.transport) - %dt= heading_helper GroupOrder, :total - %dd= number_to_currency(@group_order.total) + // Pickup + - unless @order.pickup.blank? + %dt= heading_helper Order, :pickup + %dd= format_date(@order.pickup) + // Min Order Quantity + - unless @order.stockit? or @order.supplier.min_order_quantity.blank? + %dt= heading_helper Supplier, :min_order_quantity, short: true + %dd= @order.supplier.min_order_quantity + // Group Order Sum Amount + %dt= t 'group_orders.form.sum_amount' + %dd= number_to_currency @order.sum + // Created By + %dt= heading_helper Order, :created_by + %dd= show_user_link(@order.created_by) + // Updated By + - unless @group_order.new_record? + %dt= heading_helper GroupOrder, :updated_by + %dd + = show_user(@group_order.updated_by) + (#{format_time(@group_order.updated_on)}) + // Closed By - if @order.closed? %dt= heading_helper Order, :closed_by %dd= show_user_link @order.updated_by - %p= link_to t('.comment'), "#comments" + // Note + - unless @order.note.blank? + %dt= heading_helper Order, :note + %dd= @order.note - .well.pull-right - = close_button :alert - = render 'switch_order', current_order: @order - -// Article box -%section - %h2= t '.articles.title' - .column_content#result - - if @group_order - %p.pull-right= link_to t('.articles.show_hide'), '#', 'data-toggle-this' => 'tr.ignored' - %p= link_to(t('.articles.edit_order'), edit_group_order_path(@group_order, order_id: @order.id), class: 'btn btn-primary') if @order.open? - %table.table.table-hover - %thead - %tr - %th{style: "width:40%"}= heading_helper Article, :name - %th= heading_helper Article, :units - %th= t '.articles.unit_price' - %th - %abbr{title: t('.articles.ordered_title')}= t '.articles.ordered' - %th - %abbr{title: t('.articles.order_nopen_title')} - - if @order.open? - = t '.articles.order_open' - - else - = t '.articles.order_not_open' - %th= heading_helper GroupOrderArticle, :total_price - %tbody - - for category_name, order_articles in @order.articles_grouped_by_category - %tr.article-category - %td - = category_name - %i.icon-tag - %td{colspan: "9"} - - order_articles.each do |oa| - - # get the order-results for the ordergroup - - r = get_order_results(oa, @group_order.id) - %tr{class: cycle('even', 'odd', name: 'articles') + " " + order_article_class_name(r[:quantity], r[:tolerance], r[:result])} - %td{style: "width:40%"} - = oa.article.name + // Article box + %section + .column_content#result + - if @group_order + %p= link_to t('.articles.show_hide'), '#', 'data-toggle-this' => 'tr.ignored' + %table.table.table-hover + %thead + %tr + %th{style: "width:40%"}= heading_helper Article, :name + %th= heading_helper Article, :units + %th= t '.articles.unit_price' + %th + %abbr{title: t('.articles.ordered_title')}= t '.articles.ordered' + %th + %abbr{title: t('.articles.order_nopen_title')} + - if @order.open? + = t '.articles.order_open' + - else + = t '.articles.order_not_open' + %th= heading_helper GroupOrderArticle, :total_price + %tbody + - for category_name, order_articles in @order.articles_grouped_by_category + %tr.article-category + %td + = category_name + %i.icon-tag + %td{colspan: "9"} + - order_articles.each do |oa| + - # get the order-results for the ordergroup + - r = get_order_results(oa, @group_order.id) + %tr{class: cycle('even', 'odd', name: 'articles') + " " + order_article_class_name(r[:quantity], r[:tolerance], r[:result])} + %td{style: "width:40%"} + = oa.article.name + - unless oa.article.note.blank? + = image_tag("lamp_grey.png", {alt: t('.articles.show_note'), size: "15x16", border: "0", onmouseover: "$('#note_#{oa.id}').show();", onmouseout: "$('#note_#{oa.id}').hide();"}) + %td= "#{oa.price.unit_quantity} x #{oa.article.unit}" + %td= number_to_currency(oa.price.fc_price) + %td + = r[:quantity] + = "+ #{r[:tolerance]}" if oa.price.unit_quantity > 1 + %td= r[:result] > 0 ? r[:result] : "0" + %td= number_to_currency(r[:sub_total]) - unless oa.article.note.blank? - = image_tag("lamp_grey.png", {alt: t('.articles.show_note'), size: "15x16", border: "0", onmouseover: "$('#note_#{oa.id}').show();", onmouseout: "$('#note_#{oa.id}').hide();"}) - %td= "#{oa.price.unit_quantity} x #{oa.article.unit}" - %td= number_to_currency(oa.price.fc_price) - %td - = r[:quantity] - = "+ #{r[:tolerance]}" if oa.price.unit_quantity > 1 - %td= r[:result] > 0 ? r[:result] : "0" - %td= number_to_currency(r[:sub_total]) - - unless oa.article.note.blank? - %tr{id: "note_#{oa.id}", class: "note even", style: "display:none"} - %td{colspan: "6"}=h oa.article.note - %tr{class: cycle('even', 'odd', name: 'articles')} - %th{colspan: "5"}= heading_helper GroupOrder, :price - %th= number_to_currency(@group_order.price) - - if @group_order.transport - %tr{class: cycle('even', 'odd', name: 'articles')} - %td{colspan: "5"}= heading_helper GroupOrder, :transport - %td= number_to_currency(@group_order.transport) - %tr{class: cycle('even', 'odd', name: 'articles')} - %th{colspan: "5"}= heading_helper GroupOrder, :total - %th= number_to_currency(@group_order.total) - %br/ - = link_to_top - - else - - if @order.open? - = t '.articles.not_ordered_msg' - = link_to t('.articles.order_now'), action: "order", id: @order - - else - = t '.articles.order_closed_msg' - + %tr{id: "note_#{oa.id}", class: "note even", style: "display:none"} + %td{colspan: "6"}=h oa.article.note + %tr{class: cycle('even', 'odd', name: 'articles')} + %th{colspan: "5"}= heading_helper GroupOrder, :price + %th= number_to_currency(@group_order.price) + - if @group_order.transport + %tr{class: cycle('even', 'odd', name: 'articles')} + %td{colspan: "5"}= heading_helper GroupOrder, :transport + %td= number_to_currency(@group_order.transport) + %tr{class: cycle('even', 'odd', name: 'articles')} + %th{colspan: "5"}= heading_helper GroupOrder, :total + %th= number_to_currency(@group_order.total) + %br/ + = link_to_top + %p.pull-right= link_to(t('.articles.edit_order'), edit_group_order_path(@group_order, order_id: @order.id), class: 'btn btn-primary') if @order.open? + - else + - if @order.open? + = t '.articles.not_ordered_msg' + = link_to t('.articles.order_now'), action: "order", id: @order + - else + = t '.articles.order_closed_msg' // Comments box -%section - %h2= t '.comments.title' - #comments - = render 'shared/comments', comments: @order.comments - #new_comment= render 'order_comments/form', order_comment: @order.comments.build(user: current_user) - = link_to_top +%hr +%h2= t '.comments.title' +#comments + = render 'shared/comments', comments: @order.comments +#new_comment= render 'order_comments/form', order_comment: @order.comments.build(user: current_user) += link_to_top \ No newline at end of file diff --git a/app/views/shared/_open_orders.html.haml b/app/views/shared/_open_orders.html.haml index cef00797..80e4621f 100644 --- a/app/views/shared/_open_orders.html.haml +++ b/app/views/shared/_open_orders.html.haml @@ -9,6 +9,7 @@ %thead %tr %th= heading_helper Order, :name + %th %th= heading_helper Order, :pickup %th= heading_helper Order, :ends %th= t '.who_ordered' @@ -17,21 +18,23 @@ - total = 0 - orders.each do |order| %tr - %td= link_to_ordering(order) + %td + = link_to_ordering(order, show: true) + %td + .btn-small= link_to_ordering(order){ t 'ui.edit' } %td= format_date(order.pickup) unless order.pickup.nil? %td= format_time(order.ends) unless order.ends.nil? - if group_order = order.group_order(ordergroup) - total += group_order.price %td= "#{show_user group_order.updated_by} (#{format_time(group_order.updated_on)})" %td.numeric - = link_to_ordering(order, show: true) do - = number_to_currency(group_order.price) + = number_to_currency(group_order.price) - else %td{:colspan => 2} - if total > 0 %tfooter %tr - %th(colspan="3") + %th(colspan="4") %th= t('.total_sum') + ':' %th.numeric= number_to_currency(total) - else diff --git a/config/locales/de.yml b/config/locales/de.yml index 9d51bdf1..89a69005 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -1046,9 +1046,23 @@ de: error_stale: In der Zwischenzeit hat jemand anderes auch bestellt, daher konnte die Bestellung nicht aktualisiert werden. notice: Die Bestellung wurde gespeichert. errors: + balance_alert: Kontostand im Minus closed: Diese Bestellung ist bereits abgeschlossen. no_member: Du bist kein Mitglieder einer Bestellgruppe. notfound: Fehlerhafte URL, das ist nicht Deine Bestellung. + explanations: + package_fill_level: | + Gebindefüllstand + missing_none: | + Voll + missing_few: | + Wenig fehlt + missing_many: | + Viel fehlt + title: Erklärungen + tolerance_explained: | + Zusätzliche Menge die du bestellen würdest, damit das Gebinde voll wird. + tolerance: Toleranz form: action_save: Bestellung speichern new_funds: Neuer Kontostand @@ -1057,6 +1071,7 @@ de: search_article: Artikel suchen... sum_amount: Gesamtbestellmenge bisher title: Bestellen + sub_title: Bestellung für %{order_name} aufgeben total_sum_amount: Gesamtbetrag total_tolerance: Gesamt-Toleranz units: Gebinde @@ -1100,7 +1115,6 @@ de: sum: Summe title: Dein Bestellergebnis für %{order} switch_order: - remaining: "noch %{remaining}" title: Laufende Bestellungen update: error_general: Die Bestellung konnte nicht aktualisiert werden, da ein Fehler auftrat. diff --git a/config/locales/en.yml b/config/locales/en.yml index ec6baab5..cb7a54c1 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1048,9 +1048,23 @@ en: error_stale: Someone else has ordered in the meantime, couldn't update the order. notice: The order was saved. errors: + balance_alert: Negative account balance closed: This order is already closed. no_member: You are not a member of an ordergroup. notfound: Incorrect URL, this is not your order. + explanations: + title: Explanations + tolerance: Tolerance + package_fill_level: | + Package Fill Level + missing_none: | + No more missing + missing_few: | + Few missing + missing_many: | + Many missing + tolerance_explained: | + Additional amount you would buy to fill a wholesale package form: action_save: Save order new_funds: New account balance @@ -1059,6 +1073,7 @@ en: search_article: Search for articles... sum_amount: Current amount title: Orders + sub_title: Place order for %{order_name} total_sum_amount: Total amount total_tolerance: Total tolerance units: Units @@ -1102,7 +1117,6 @@ en: sum: Sum title: Your order result for %{order} switch_order: - remaining: "%{remaining} remaining" title: Current orders update: error_general: The order couldn’t be updated due to a bug. diff --git a/config/locales/nl.yml b/config/locales/nl.yml index facd55d0..f441d15d 100644 --- a/config/locales/nl.yml +++ b/config/locales/nl.yml @@ -1018,6 +1018,7 @@ nl: error_stale: In de tussentijd heeft iemand anders ook bestelt, daarom kon de bestelling niet bijgewerkt worden. notice: Bestelling opgeslagen. errors: + balance_alert: Accountsaldo in het rood closed: Deze bestelling is al gesloten. no_member: Je bent geen lid van dit huishouden. notfound: Foute URL, dit is niet jouw bestelling. @@ -1029,6 +1030,7 @@ nl: search_article: Artikelen zoeken... sum_amount: Huidig totaalbedrag title: Bestellen + sub_title: Plaats bestelling voor %{order_name} total_sum_amount: Totalbedrag total_tolerance: Totale tolerantie units: Eenheden @@ -1072,7 +1074,6 @@ nl: sum: Som title: Jouw bestelling voor %{order} switch_order: - remaining: "nog %{remaining}" title: Lopende bestellingen update: error_general: Er is een probleem opgetreden, de bestelling kon niet bijgewerkt worden. From dfe8beae2c7556e6d1eabd04b309cb9decb4ac4f Mon Sep 17 00:00:00 2001 From: Philipp Rothmann Date: Fri, 24 Feb 2023 13:06:32 +0100 Subject: [PATCH 025/105] fix: article category remove option from list --- app/models/supplier.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/supplier.rb b/app/models/supplier.rb index 92201022..06ef36bb 100644 --- a/app/models/supplier.rb +++ b/app/models/supplier.rb @@ -81,7 +81,7 @@ class Supplier < ApplicationRecord all_order_numbers = [] updated_article_pairs, outlisted_articles, new_articles = [], [], [] custom_codes_path = File.join(Rails.root, "config", "custom_codes.yml") - opts = options.except(:convert_units, :outlist_absent) + opts = options.except(:convert_units, :outlist_absent, :update_category) custom_codes_file_path = custom_codes_path if File.exist?(custom_codes_path) FoodsoftArticleImport.parse(file, custom_file_path: custom_codes_file_path, type: type, **opts) do |new_attrs, status, line| article = articles.undeleted.where(order_number: new_attrs[:order_number]).first From 237ef5d38bd05915f1718ffb364f1ac6c9be3028 Mon Sep 17 00:00:00 2001 From: Philipp Rothmann Date: Fri, 24 Feb 2023 13:10:41 +0100 Subject: [PATCH 026/105] fix: article_spec login twice to fix flaky test --- spec/integration/articles_spec.rb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/spec/integration/articles_spec.rb b/spec/integration/articles_spec.rb index 638b1c0a..b6a3a243 100644 --- a/spec/integration/articles_spec.rb +++ b/spec/integration/articles_spec.rb @@ -8,7 +8,10 @@ feature ArticlesController do before { login user } describe ':index', js: true do - before { visit supplier_articles_path(supplier_id: supplier.id) } + before { + login user + visit supplier_articles_path(supplier_id: supplier.id) + } it 'can visit supplier articles path' do expect(page).to have_content(supplier.name) From ee03a2a9af403e3426d54f4bdd806482006e795e Mon Sep 17 00:00:00 2001 From: FGU Date: Fri, 24 Feb 2023 16:49:58 +0100 Subject: [PATCH 027/105] wip on demos seeds --- db/seeds/demo-seeds.rb | 147 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 db/seeds/demo-seeds.rb diff --git a/db/seeds/demo-seeds.rb b/db/seeds/demo-seeds.rb new file mode 100644 index 00000000..efdb7342 --- /dev/null +++ b/db/seeds/demo-seeds.rb @@ -0,0 +1,147 @@ +require_relative 'seed_helper.rb' + +FinancialTransactionClass.create!(:id => 1, :name => 'Standard') +FinancialTransactionClass.create!(:id => 2, :name => 'Foodsoft') +FinancialTransactionType.create!(:id => 1, :name => "Foodcoop", :financial_transaction_class_id => 1) + +alice = User.create!(:id => 1, :nick => "alice", :password => "secret", :first_name => "Alice", :last_name => "Administrator", :email => "admin@foo.test", :phone => "+4421486548", :created_on => 'Wed, 15 Jan 2014 16:15:33 UTC +00:00') +bob = User.create!(:id => 2, :nick => "bob", :password => "secret", :first_name => "Bob", :last_name => "Doe", :email => "bob@doe.test", :created_on => 'Sun, 19 Jan 2014 17:38:22 UTC +00:00') + + +Workgroup.create!(:id => 1, :name => "Administrators", :description => "System administrators.", :account_balance => 0.0, :created_on => 'Wed, 15 Jan 2014 16:15:33 UTC +00:00', :role_admin => true, :role_suppliers => true, :role_article_meta => true, :role_finance => true, :role_orders => true, :next_weekly_tasks_number => 8, :ignore_apple_restriction => false) +Workgroup.create!(:id => 2, :name => "Finances", :account_balance => 0.0, :created_on => 'Sun, 19 Jan 2014 17:40:03 UTC +00:00', :role_admin => false, :role_suppliers => false, :role_article_meta => false, :role_finance => true, :role_orders => false, :next_weekly_tasks_number => 8, :ignore_apple_restriction => false) +Ordergroup.create!(:id => 5, :name => "Alice WG", :account_balance => 0.90E2, :created_on => 'Sat, 18 Jan 2014 00:38:48 UTC +00:00', :role_admin => false, :role_suppliers => false, :role_article_meta => false, :role_finance => false, :role_orders => false, :stats => { :jobs_size => 0, :orders_sum => 1021.74 }, :next_weekly_tasks_number => 8, :ignore_apple_restriction => true) +Ordergroup.create!(:id => 8, :name => "Bob's Family", :account_balance => 0.90E2, :created_on => 'Wed, 09 Apr 2014 12:23:29 UTC +00:00', :role_admin => false, :role_suppliers => false, :role_article_meta => false, :role_finance => false, :role_orders => false, :contact_person => "John Doe", :stats => { :jobs_size => 0, :orders_sum => 0 }, :next_weekly_tasks_number => 8, :ignore_apple_restriction => false) +FinancialTransaction.create!(:ordergroup_id => 5, :amount => 0.90E2, :note => "Bank transfer", :user_id => 2, :created_on => 'Mon, 17 Feb 2014 16:19:34 UTC +00:00', :financial_transaction_type_id => 1) +FinancialTransaction.create!(:ordergroup_id => 8, :amount => 0.90E2, :note => "Bank transfer", :user_id => 2, :created_on => 'Mon, 17 Feb 2014 16:19:34 UTC +00:00', :financial_transaction_type_id => 1) + +Membership.create!(:group_id => 1, :user_id => 1) +Membership.create!(:group_id => 5, :user_id => 1) +Membership.create!(:group_id => 2, :user_id => 2) +Membership.create!(:group_id => 8, :user_id => 2) + +supplier_category = SupplierCategory.create!(:id => 1, :name => "Other", :financial_transaction_class_id => 1) + +chocolate_supplier = Supplier.create!( + name: "Kollektiv CHOCK!", + address: "Grabower Straße 1\n12345 Berlin", + phone: "0123456789", + email: "info@bbakery.test", + supplier_category: supplier_category +) + +nkn_supplier = Supplier.create!( + name: "Naturgut Süd", + address: "Somewhere in Hamburg, maybe St. Pauli?", + phone: "0123434789", + email: "foodsoft@local-it.org", + supplier_category: supplier_category +) + +chocolate_category = ArticleCategory.create!(name: "Schokolade") +obst_category = ArticleCategory.create!(name: "Obst, Gemüse, Sprossen, Pilze") +nudeln_category = ArticleCategory.create!(name: "Nudeln, Trockenfrüchte, Müsli") +reis_category = ArticleCategory.create!(name: "Getreide, Ölsaaten. Nußkerne") + +Article.create!( + name: "Vollmilch-Schokolade", + supplier_id: chocolate_supplier.id, + article_category_id: chocolate_category.id, + manufacturer: "Grabower Süßwaren GmbH", + origin: "D", price: 3.0, tax: 7.0, + unit: "200g", unit_quantity: 5, + note: "bio, fairtrade, 40% Kakao, vegan", + availability: true, order_number: "1") + +Article.create!( + name: "Weiße Schokolade", + supplier_id: chocolate_supplier.id, + article_category_id: chocolate_category.id, + manufacturer: "Grabower Süßwaren GmbH", + origin: "D", price: 3.49, tax: 7.0, + unit: "200g", unit_quantity: 5, + note: "bio, fairtrade, 40% Kakao, vegan", + availability: true, order_number: "2") + +dark_chocolate = Article.create!( + name: "Dunkle Schokolade", + supplier_id: chocolate_supplier.id, + article_category_id: chocolate_category.id, + manufacturer: "Grabower Süßwaren GmbH", + origin: "D", price: 2.89, tax: 7.0, + unit: "200g", unit_quantity: 5, + note: "bio, fairtrade, 40% Kakao, vegan", + availability: true, order_number: "3") + +Article.create!( + name: "Himbeer-Schokolade", + supplier_id: chocolate_supplier.id, + article_category_id: chocolate_category.id, + manufacturer: "Grabower Süßwaren GmbH", + origin: "D", price: 2.89, tax: 7.0, + unit: "170g", unit_quantity: 4, + note: "bio, fairtrade, 40% Kakao, vegan", + availability: true, order_number: "4") + +previous_order = seed_order(supplier_id: chocolate_supplier.id, starts: 10.days.ago, ends: 7.days.ago) + +GroupOrderArticle.create!( + group_order: GroupOrder.create!(order_id: previous_order.id, ordergroup_id: 8), + order_article: previous_order.order_articles.find_by(article_id: dark_chocolate.id), + quantity: 5, tolerance: 0) + +previous_order.close!(alice) + +seed_order(supplier_id: chocolate_supplier.id, starts: 0.days.ago, ends: 7.days.from_now) + + +apple = Article.create!( + name: "Äpfel Elstar", + supplier_id: nkn_supplier.id, + article_category_id: obst_category.id, + manufacturer: "Obsthof Bruno Brugger", + origin: "D", price: 3.49, tax: 7.0, + unit: "1kg", unit_quantity: 10, + note: "lecker, fruchtig, demeter", + availability: true, order_number: "5") + +brokkoli = Article.create!( + name: "Brokkoli", + supplier_id: nkn_supplier.id, + article_category_id: obst_category.id, + manufacturer: "Fattoria degli Orsi", + origin: "IT", price: 2.89, tax: 7.0, + unit: "400g", unit_quantity: 6, + note: "gesund und lecker", + availability: true, order_number: "6") + +tomatoes = Article.create!( + name: "Tomaten", + supplier_id: nkn_supplier.id, + article_category_id: obst_category.id, + manufacturer: "Terra di Puglia", + origin: "IT", price: 2.89, tax: 7.0, + unit: "500g", unit_quantity: 20, + note: "pomodori italianio, demeter", + availability: true, order_number: "7") + +rice = Article.create!( + name: "Reis", + supplier_id: nkn_supplier.id, + article_category_id: reis_category.id, + manufacturer: "Finck", + origin: "D", price: 3.29, tax: 7.0, + unit: "3kg", unit_quantity: 10, + note: "Reis im Vorratssack, demeter", + availability: true, order_number: "8") + +spaghetti = Article.create!( + name: "Spaghetti", + supplier_id: nkn_supplier.id, + article_category_id: nudeln_category.id, + manufacturer: "Pastificio Zanellini spa", + origin: "D", price: 2.89, tax: 7.0, + unit: "500g", unit_quantity: 4, + note: "100% italienisches Hartweizengrieß", + availability: true, order_number: "9") + From 3d71d266e34eeadaa8a7c8ad1c2b2b59a83960b0 Mon Sep 17 00:00:00 2001 From: viehlieb Date: Fri, 24 Feb 2023 16:51:09 +0100 Subject: [PATCH 028/105] add bnn for demo day --- demo_day_nks.bnn | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 demo_day_nks.bnn diff --git a/demo_day_nks.bnn b/demo_day_nks.bnn new file mode 100644 index 00000000..0796c486 --- /dev/null +++ b/demo_day_nks.bnn @@ -0,0 +1,7 @@ +BNN;3;0;Naturkost Nord, Hamburg;T;Angebot Nr. 0922;EUR;20220905;20221001;20220825;837;1 +5;;;;4280001958081;4280001958203;pfel Elstar;erntefrisch und knackig;;;obb;;D;C%;DE-KO-001;120;0301;10;55;;1;10 x1Kg;10;1Kg;1;N;;;;1,41;;;;1;;;4,49;2,89;J;;2;3;;;;;;;;;;;;;;;;;;;A;;;;;Kg;1;; +6;;;;4280001958081;4280001958203;Brokkoli;aus der Erde;;;VIB;;IT;C%;DE-KO-001;120;03;10;55;;1;4 x400g;4;400g;1;N;;;;1,41;;;;1;;;4,49;3,20;J;;2;3;;;;;;;;;;;;;;;;;;;A;;;;;Kg;2,5;; +7;;;;4280001958081;4280001958203;Tomaten;Datteltomaten, demeter;;;TDP;;IT;C%;DE-KO-001;120;03;10;55;;1;20 x500g;20;500g;1;N;;;;1,41;;;;1;;;4,49;3,00;J;;2;3;;;;;;;;;;;;;;;;;;;A;;;;;Kg;2;; +8;;;;4280001958081;4280001958203;Reis;Reispfannen geeignet;;;FIN;;D;C%;DE-KO-001;120;05;10;55;;1;12 x300g;12;300g;1;N;;;;1,41;;;;1;;;4,49;3,00;J;;2;3;;;;;;;;;;;;;;;;;;;A;;;;;Kg;3,333333;; +9;;;;4280001958081;4280001958203;Spaghetti;Vollkorn;;;ZLN;;D;C%;DE-KO-001;120;06;10;55;;1;4 x500g;4;500g;1;N;;;;1,41;;;;1;;;4,49;3,00;J;;2;3;;;;;;;;;;;;;;;;;;;A;;;;;Kg;2;; +10;;;;4280001958081;4280001958203;Kartoffeln;vorwiegend festkochend;;;rsh;;D;C%;DE-KO-001;120;0311;10;55;;1;6 x5Kg;6;5Kg;1;N;;;;1,41;;;;1;;;4,49;3,00;J;;2;3;;;;;;;;;;;;;;;;;;;A;;;;;Kg;0.2;; \ No newline at end of file From 8cb86b2f887c3da0e9cce3a3ee26fd0b34077fc2 Mon Sep 17 00:00:00 2001 From: Philipp Rothmann Date: Fri, 24 Feb 2023 17:00:47 +0100 Subject: [PATCH 029/105] update readme --- README.md | 161 +++++++++++++++++-------- doc/foodcoop-explained.jpg | Bin 0 -> 401079 bytes doc/logo-bmbf.svg | 1 + doc/logo-okfn.svg | 1 + doc/screenshots/balance_sum.png | Bin 0 -> 94463 bytes doc/screenshots/bnn_upload.png | Bin 0 -> 33045 bytes doc/screenshots/custom_csv_export.png | Bin 0 -> 168623 bytes doc/screenshots/message_formatting.png | Bin 0 -> 123435 bytes doc/screenshots/order.png | Bin 0 -> 152665 bytes doc/screenshots/rswag.png | Bin 0 -> 103496 bytes 10 files changed, 112 insertions(+), 51 deletions(-) create mode 100644 doc/foodcoop-explained.jpg create mode 100644 doc/logo-bmbf.svg create mode 100644 doc/logo-okfn.svg create mode 100644 doc/screenshots/balance_sum.png create mode 100644 doc/screenshots/bnn_upload.png create mode 100644 doc/screenshots/custom_csv_export.png create mode 100644 doc/screenshots/message_formatting.png create mode 100644 doc/screenshots/order.png create mode 100644 doc/screenshots/rswag.png diff --git a/README.md b/README.md index a1a9de24..67594931 100644 --- a/README.md +++ b/README.md @@ -1,65 +1,124 @@ Foodsoft ========= -[![Build Status](https://github.com/foodcoops/foodsoft/workflows/Ruby/badge.svg)](https://github.com/foodcoops/foodsoft/actions) -[![Coverage Status](https://coveralls.io/repos/foodcoops/foodsoft/badge.svg?branch=master)](https://coveralls.io/r/foodcoops/foodsoft?branch=master) -[![Docs Status](https://inch-ci.org/github/foodcoops/foodsoft.svg?branch=master)](http://inch-ci.org/github/foodcoops/foodsoft) -[![Code Climate](https://codeclimate.com/github/foodcoops/foodsoft.svg)](https://codeclimate.com/github/foodcoops/foodsoft) -[![Docker Status](https://img.shields.io/docker/cloud/build/foodcoops/foodsoft.svg)](https://hub.docker.com/r/foodcoops/foodsoft) -[![Documentation](https://img.shields.io/badge/yard-docs-blue.svg)](http://rubydoc.info/github/foodcoops/foodsoft) -Web-based software to manage a non-profit food coop (product catalog, ordering, accounting, job scheduling). - -A food cooperative is a group of people that buy food from suppliers of their own choosing. A collective do-it-yourself supermarket. Members order their products online and collect them on a specified day. And all put in a bit of work to make that possible. Foodsoft facilitates the process. - -If you're a food coop considering to use foodsoft, please have a look at the [wiki page for foodcoops](https://github.com/foodcoops/foodsoft/wiki/For-foodcoops). When you'd like to experiment with or develop foodsoft, you can read [how to set it up](https://github.com/foodcoops/foodsoft/blob/master/doc/SETUP_DEVELOPMENT.md) on your own computer. - -More information about using this software and contributing can be found on the [wiki](https://github.com/foodcoops/foodsoft/wiki). +[Website](https://foodsoft.local-it.org) +[Prototypefund](https://prototypefund.de/project/weiterentwicklung-von-foodsoft/) -Developing ----------- +Foodsoft ist ein Tool für [Lebensmittelkooperativen](https://de.wikipedia.org/wiki/Lebensmittelkooperative), welches selbstorganisierte gemeinsame Bestellungen in Großmengen von regionalen und ökologischen Produkten vereinfacht und transparent gestaltet. -Get foodsoft [running locally](doc/SETUP_DEVELOPMENT.md), -then visit our [Developing Guidelines](https://github.com/foodcoops/foodsoft/wiki/Developing-Guidelines) -page on the wiki. - -Get a foodsoft dev-environment running in the browser with Gitpod - -[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/foodcoops/foodsoft) - -Follow these [instructions](doc/SETUP_DEVELOPMENT_GITPOD.md) to complete setup from within the Gitpod workspace. - -Deploying ---------- - -Setup foodsoft to [run in production](doc/SETUP_PRODUCTION.md), or join an existing -[hosting platform](https://foodcoops.net/foodsoft-hosting/). +Foodsoft wurde ursprünglich entwickelt und betrieben von [foodcoops.net](https://foodcoops.net/) -License -------- +#### Zielgruppe -Foodsoft is licensed under the [AGPL](https://www.gnu.org/licenses/agpl-3.0.html) -license (version 3 or later). Practically this means that you are free to use, -adapt and redistribute the software, as long as you publish any changes you -make to the code. +Unsere Zielgruppen sind Bürger:innen, Gruppen und Vereine, die eine Einkauskooperative aufbauen wollen und eine Software, die die Bestellung, Verteilung und Abrechnung erleichtert, benötigen. -For private use, there are no restrictions, but if you give others access to -Foodsoft (like running it open to the internet), you must also make your -changes available under the same license. This can be as easy as -[forking](https://github.com/foodcoops/foodsoft/fork) the project on Github and -pushing your changes. You are not required to integrate your changes back into -the main Foodsoft version (but if you're up for it that would be very welcome). +#### Vorhaben -To make it a little easier, configuration files are exempt, so you can just -install and configure Foodsoft without having to publish your changes. These -files are marked as public domain in the file header. +* ✅ Technische Schuld reduzieren +* ✅ Ruby on Rails Upgrade +* ✅ Artikel Import verbessern + (Großhandelschnitstelle) +* ✅ Userexperience Verbessern + +#### Was ist eine Einkaufskooperative? + +![Wie funktioniert eine Einkauskooperative?](./doc/foodcoop-explained.jpg) + + + +State of this Fork +------------------ + +#### Increase Test Coverage + +1. integration and model tests + * [x] fork + * [x] upstream [#966](https://github.com/foodcoops/foodsoft/pull/966) +1. Controller tests + * [x] [fork](https://git.local-it.org/Foodsoft/foodsoft/src/branch/8_increase_test_coverage_controllers) + * [ ] upstream [#970](https://github.com/foodcoops/foodsoft/pull/970) + +#### Upgrade + +1. Migrate to RSwag API Tests + * [x] [fork](https://git.local-it.org/Foodsoft/foodsoft/src/branch/28_introduce_rswag) + * [ ] upstream [#969](https://github.com/foodcoops/foodsoft/pull/969) +1. Rails v7 + * [x] [fork](https://git.local-it.org/Foodsoft/foodsoft/src/branch/9_rails_v_7) + * [ ] upstream [#979](https://github.com/foodcoops/foodsoft/pull/979) + disussion [#956](https://github.com/foodcoops/foodsoft/issues/956) +1. Javascript Importmap + * [x] [fork](https://git.local-it.org/Foodsoft/foodsoft/src/branch/9_rails_v_7_js_importmap) + * [ ] upstream + +#### Article Order Import/Export + +Updating Articles from large resellers and exporting orders is now much easier! + +1. adds bnn fileformat that is used from large german resellers e.g. naturkost nord + * [x] [fork](https://git.local-it.org/Foodsoft/foodsoft/src/branch/11_bnn_import_article_update) + [gem](https://git.local-it.org/Foodsoft/foodsoft_article_import) + * [ ] upstream +1. Import category field + * [x] [fork](https://git.local-it.org/Foodsoft/foodsoft/src/branch/56_add_update_of_article_category_to_file_import) + * [ ] upstream +1. Export order as a custom csv file + * [x] [fork](https://git.local-it.org/Foodsoft/foodsoft/src/branch/12_generate_custom_csv_file) + * [ ] upstream +1. Naturkostnord Plugin + * [ ] [fork](https://git.local-it.org/Foodsoft/foodsoft/src/branch/12_nkn_file_plugin) + * [ ] upstream + +#### Improve User Experience + +1. Richtext editor for messages. Also allows sending attachements. + * [x] [fork](https://git.local-it.org/Foodsoft/foodsoft/src/branch/16_html_message_templates) + * [ ] upstream +1. Show the sum of all order group balances + * [x] [fork](https://git.local-it.org/Foodsoft/foodsoft/src/branch/47_finance_ordergroup_sums) + * [ ] upstream +1. UI improvements for group order view + * [x] [fork](https://git.local-it.org/Foodsoft/foodsoft/src/branch/uxui_group_order) + * [ ] upstream +1. Favorites + * [ ] [fork](https://git.local-it.org/Foodsoft/foodsoft/src/branch/20_favourites) + * [ ] upstream +1. Show the per kilo / litre price + * [x] [fork](https://git.local-it.org/Foodsoft/foodsoft/src/branch/11_include_kilo_litre_price) + * [ ] upstream + +#### Other + +1. Fix broken plugin mechanism + * [x] [fork](https://git.local-it.org/Foodsoft/foodsoft/src/branch/downgrade-haml) + * [ ] upstream + +#### Screenshots + +![rswag](./doc/screenshots/rswag.png) + +--- + +![bnn upload](./doc/screenshots/bnn_upload.png) + +--- + +![message formatting](./doc/screenshots/message_formatting.png) + +--- + +![balance sum](./doc/screenshots/balance_sum.png) + +--- + +![custom csv export](./doc/screenshots/custom_csv_export.png) +csv export + +--- + +![order](./doc/screenshots/order.png) -If you have any remaining questions, please -[open an issue](https://github.com/foodcoops/foodsoft/issues/new) or open a new -topic at the [forum](https://forum.foodcoops.net). -Please see [LICENSE](LICENSE.md) for the full and authoritative text. Some -bundled third-party components have [other licenses](vendor/README.md). -Thanks to [Icons8](http://icons8.com/) for letting us use their icons. diff --git a/doc/foodcoop-explained.jpg b/doc/foodcoop-explained.jpg new file mode 100644 index 0000000000000000000000000000000000000000..723204f2c05b5e653e7e4dfcbe24ab0d752f6eb5 GIT binary patch literal 401079 zcmeEu2b9y+wzlcL6Kd!X0%ou+TP_34;BL8h*`h`6wk%ujg%)}sp)-(#-a>%DP(z2% zN$4e%p@j~iGxRpU#si^*n|JSj-+O=lwXW8}sGR+2pMCZ@yPT~lI$rb}rKVg0Nhqbr zqeH0<6iU%8N_D;?Xme5=6!Iutbd%CS>j_4K5l@h4&g@91a0QU6%+2D5l$ym9lu{)1 ztN)A6bsFV~Mu}`1EfDF57UOiZ2XR^wC1{L}bQ+~|ZjwOlm^0ek=5%`lU0U8=x2a`w zucJ##7*x?!gwW~n%F*(ULK&Oq-p*cVY02+fqqcgJ@ zyljA*%>tXhI$GvbCeJ#D9jNb>4^9NUIW%*Ek|)#2)q%QExENycid?1!K`JEn619=#QhMV-EWt?;&Dgf95R? zJI(2L;?6)cLJE;V`w$%|o<9gbDEWUtB!zJIUlY1%UflhU-lY*#RhqtP5Mj=G&RAK`R&roFlQMn8(1M!iwL zGoze;=uAdS`Tpp;cZ;Em)y(fL*4}JC8X$)_ndNZ0(3n5kGDjQs2BMv5B~9KXExG$H zVuqvlU*4HixNH?U^Biat6`@gQ4mqWmS#%%^U>F%dHozdK6qU}%rqesq{?zuZj%A(& z9bQ-JPbiFR0LZ2@kXHb28ofs@3_D2%{zU!(Cf;iKA&g+y7!3MzN){04jXpF7I30oJ zpQ=zhDL8T=Y*)fj2JIEidvHci47UYPuRrphe2aztu6RoZ^YwrgKPS*|#96HVU0S}@ z|F_lV&FGV|E1tjE39lpS$pJuihOf77z2WN5L>{Nt?TO|v=-f9{ukQU7*(-&WYsJzx zbgypy6YHUyZHjXTg*4`1rVPvqH%Qmh)L>+JEXicD&-|0kt(L* z^I1B~ujW(f{LCTXlkJNSG5?cRNq#53u9%oSbbJAg%HoSK31$Q2e!kS0xy^UcY<-&wpDsa1%B7RUIOav7fK#_p267dto|8ycgODpDcH6nq^ zC=uw4;$*@mO-AgpB-i!Fzl2yONZ8dPKHW;b!7BBb^b&nA^E?CiN5i*?{>gJ1m+Yv{ z&9{-=h3NnDEsLZ2EB3`agpK`~=eqCB`#6pUcm8lh~O}YTa)}+-QhY67xMf4a1 z$%87n(Bnota#D051Kk`|n-zj^ibd0kGk^?@k&akki64a-Nr%PHgAu?(pm;Q5gTc6! zsb@F>4wlnGa9lD5)y33^Jyulag>Z$Jl~VhHR3Qc$%znaY#gPnP$QFma>Y}rXg``5*ie>8&iv&WkL1hw4OnS8kawADMB2Hxh zgEm=0qLS%Z2DMOPmV51Saau0Yi`*X6U`oMWD@Q35FtrA?Q!aKV9VRT{&U9JsLnKnK z19M1BOm0Y%PNdvK29QqGraf{47F8KdpjPj9SYb0^vq%_D3q$Wma7Pe@iLgz|h}nSu zs{0v0s>m;gVL#|aa3W?8AZ!;F(qIIN20{uv6k?~M;Se=WLrFy=)r2n;7ps_v%jcO` zA&$giefuW^_};zZOAm)l^+Z@M6UMZ1AU8W=Pw;GEE9#UwMrjBL=r~$Px7)M1C3`jY>7Qm$? zxGo&j*mMS8TxOTE!ito~4FOhESUJf?BRI%JZrm zMjBs=xY;yIg3h&RxW1Ig;Yr{KV2#?DDn?p`AV#wf$N+NqPJY^>Hii(8!0qW6$0-(w zO%kO-YKS8ij6ta6)-*>&u*FU(0{a*kPpkF%xO!&PX%Wl4-c*bp3x-2tLsA;d0BTK+ zgo_U{0YAf};tDico{!+-s)Sksry;B659x+2ey@+k1%zB*5+(=}m7fxGBUVOIETnto zTDb$GYcha;P?QYRyO$9ym&x|Aqp2wFp^-0^$T=}RjgKTPQAyaTg;Y9F4^nA7w<8%; zsY}*FmXBfO1=O*G9>7UuHis3MBM55MZh_FjuwqmhPX$Ol48SFUL{cY04FqAI+T(#N zN-(a&K&}L&gLIi9V2C?Q2F2-1#yl=H$mQ~IF6POolz{CEnalxMoNG#pSYn#jWm7u< z58x9S^&BvOX&F?b%af2ITwH09B$Zl-9!cAAZKlhnKt#o|sqp|FNE*~BWi+T0>vgb_ zYn5AAZVe%&nyE5nObNwOikKNj+#$E$7vXCNjZ|eY>cEiED38kM$*^7|&j3P69;~wk zMG*l_3<69M)1wG$|^Barc+h50GUOstfFl%z zjJQ0kwq*cC#srIS@f{q*o0K9!6&zOrPL7{vFwxi{Ur@k75_p(_ak*55B_Xqu%dD4N z8;xOo6y}+PK)}XhtAk7}N5>U$GJqgGZl$4duPDLw@GLqWEg@ubm;#0-5N3!loazv= z3<0fO2Xl=^V@gR*C5|S^=Tm7=8i%|oR2YH-Y#+k**l2JDP-!qSpqSq1*ZCYSD_x^Y zfGotyH5sjDF-SU0W{3BkA00qgLa^Ddv_}~_7eDSz0wlW10<(kG44}$H1PC5YA5E~* z8l9KRGa9Tsn;-MWEK-tN9)kN|IBfR-Rh^ zH&UEn^h`+@9(xOq3 zQIclIG)mN>jY&ZhJ)rhD0Ui(4TO2WimC&0}uLn>IMO?2cmH@3FoT!m8>vuC=@prIDj?$DBMAf|;}gc7r=1h`A?M@8!5{KFNOm)trGBSWp*pwR;;`3FqV>KI8?@!T$hHf^J++YBtURA zU%-&3jS12d(0i3~6UK*OJ)rj3P#d4ebfh4U-ayB~m;yuHW;JP9c(DZot{K0}C#LkXKJf`T!=MMA)@ z%!Ws;V@RcGUj{J5Od29uw~rG5sen})=jc2H3vhrkOq-4wj$k@B&%m+T z3~?^s8q|P%raVpXv;YXPLQu@grV$QpC}Q;}Z5S*=eW6$oE#Xkfjxtqr4-K&dL!@v+ zK|qSq)FC-&wEJvcaU{xh19$}H7~)Eym8T7uR6=S}=?j`o96FSCI7mAavB@Pmr9!F7 zbeYRc%U@gmKMcyhXiG>lqOBg7+T0?i*wMI8{Gsuxi)h-Zir90O=}Sun&K z*7+qeC6C3lV1!-EEt$(v8wVzrL^c{uTUkkOn87tu&1x4*L}zonu%5-Crd3=_MR;I` zTyCX8W}Jtj2mv_bGCIJwyA^zi%G_Z%Vi^t8Q&?3m)K>DW92BgMjps_ z#zPt!kIbCdOm;1ymHT3LCrABSPemR9r1qF_2HTj{G13BAkgZ@L1_VZ62i3ySiUpRE zxt!pkVjEBE$JquIlH!?De!5J{kqH=fFi8!tVIwNDv3XX&AF!tZx*SD>JdTi)j;n?A zP*iSE`@I-w!n9%{3A;0c!j^hHB0%GzTa#{smcd|}bs)oH48*~hOshbISd^P$0+E!F z?GWNoAR-OotO%h@r`X}7f{q#D>OeD9o1`ZF0W5;D)dH_H%Jq>+SVc;mvWIoHh*RYR)h>83r-T@upQK$fMIsRbD0`gEe70BGglXpwW7}JGlq<@NFX+)mzNs*ET*28DA!)gwb9pzCuOo@shB#s2>*_+K`lZIo6>Lo&- z9|DyTM>LfT*%_KJL*_$7noumR#T00e4CdV~Yb4{BWjOSPWmZ0Eg_5kKTrI*=azbSb zr#w6dofU>yz8KpROgR`PGgCk?LMnE^!1IW$91Km#K+*vUrG?G}D3H5UlF+M}Vh*Ee z)MXW!m4G7^PNyY0C=p}_!(wHMNtF_`NIb=MO8tC@iMd!_WtipiVXS~$?NZWknMKZ1 zxEWGTgpk8Aq-1fBBmyCrYxSCCJW0S~#!YHjN~Yq18iy!qVVTl4)Tppy8i?lO+Ho@w z;ilOHm<-T`oLF2*SBGV4wkec=c?qs9o$0d1NxI@DMDDSL_#v)4tRbhWJti;(0lgx` zG>H8So0&lu3Kei70??_ffETwKQb7z+`)x)Q*KXD0ge+kIWk4cY0;mheq(XK)Dk6;$ zBOwg4XZE^ zlOZ0}n8vA5PD;uuiALFgPp$%$Dp+PBojzI66-=jPW~)6S40c8kr$}x4)lcI%P5g9WUWduY%nMj7aVu(6mibb4O(%fbMaTyqJr6ms3 zi~=f45a2*a7zFqdsZMQz1SvU_V5I#%fbjb~T-cP1JM>~F3$rs3m!E~ZgaWS3=|f!_ zF3%?@32)`Rm|lW}Y&Hyu^CW(aQ6#j->_K--gnOYhB!tomlxYoNu?SnA1})~8*2hli zeQ}PDNy-k;q?`(FOl9NgnbZ>Ri_6r|fnb=PG8lwH*lD%~{h)@aL()=_fOMvd9VR=~ zo)p8d5s>l33U!?3bXk%Ou7ug>|zbIByS@HX*vzafNdHikh0Q(aX-!M zPz1d0h}2oEcMe$$kJ-E^FXpx~Y%Gb&o6?8ET(N)(xuUM9N`&#$K`kG1@;Qc#3_)&O zOD0ie=73f$wY$tNvBu({voR*mi&!lFP%0KRSxicADhk?Sask^6Mk0h=;^wo&NJs() z#D1TS5#;MlAd$=f>NTL0=A-b+5pWKXVDWuoimIEct#!M4y#yH zaUdvQfgvZuNsp(vD4%DKS?r)Dkgy{WGE6rZv=Hb3l$vA+)>=vea9IkG8%um=2ARzh zgdxJ=4Y4(9P$q;31W@yXDW_JZuzRFX#80MIBN0D6Y%oR5V$3g%2{m!VWkrAh58Ft)Cn_V%CO1GNtu!gB^ThDBtaM_9R^9U zx#J=VzR1qi33MfkLn;lkqFR3v)R2iCx06&Y7RZErG`={Z(>NI>(2)p8eI}I345WfC zCmB6?xd9Xj7Ymt605*dWX6XEBF;8qZXE=1oC3?Tgi~BSRhS_OJ6h|2`i&7wUFFT&Quu8u-?KGn7jgIN4QOOh z5upVM9udX-{*sxZhf_S0EE#1*94S{k9dt+ty*sIQYXVH3MUW=#n;!GpQ#c`V1>;5q zgBl`ZG7}XK@Ki}rP$i=h2^u|U(@F!h61CB$^+L!Ck%^0tLZJvs_yP_e!IECcOS*nm zF~;%7HW3V0@v|5cYEfZ(Mg> zT&YoTJSvl$$FRjjET76^W*ekVfk#PAMv{n#4@eajM_7rbr3#Y?V#^|WmQPcXc4ol- zfSa^Cw5TKLO9`zJg~rNJhs0sEQ<(N5a15|81bm^!C5+1gNj4Ydm{T+nVaF|Oh|Gg2 z!3cstNzjim;%H`2fEWgZEvf{nPa1K$-Ij`S$?UKmN~8mJo`($M0uDSur;{nUbXv<6 zP}NLw5J56r6$i`?jY-CkGd#hxLa&CQl8D~K;TY^j4uh+v;x-|Sid`W{76voXb{BYqAnRX!yy|?#k6LffT@b( zE;1FYloM%Th-XRCEEYgPcZQ59Hs+Qvp=bm(3;cew!sGI?V`;uU&J8PQX@SOw>RB!) zNh`_#LOw45Djc*BgUO~zg-%#)mWYx#xdcJ@5iia5k~OI#j8M^6(G$VZSyl1#t(OB6fk#4KqxlT3|?thVB`soXEdsRkR$ zb68Ld6*sUgsL_HYoO;-9QAJEDRAo|m{9Yv?Q5f7ddKwZXG;ChN5!IHY`;=6%-I5I9 zPNj^`Or}^)juoe2SOE9w9NM@osbrcQs3wq9Yur9>Tp)qMVYZ&9rH51jffL1zEDw)> zwE&8E*d+nD&Z$;~-8`p`{ zJ|UmXw8$VEq>EE!4j~!1!$J|QB=C`>Wi-AJmQfKI2ocQ8bI=NDseE$O{T>>wryA%S znuyG+awB50Enmo@VPu4!(x%{mi{+pzX?%GJL1s|irsDHCK?_YO5Rk8YLxm;M3WJBI zi;E*14a~Cf+%BP3;)@7)RBJ54fD%>$5z$F^up}{VjUWL?;5GX}oFQe}sUarAtHKF0 zHAZtYZ5lZ}Zlk)yE|XKE4N%=NW(rk^J<$kkV7tOmkCRTi(i$s`6LMK;C240s<3NHj z23Hyc1R;yvm5i~|Y63}sT#_?Xqj`VNB6`c_d~m5DQ|1{NzD}P1gB41I`kbW8WRE78W&sy^y>mobmg1cw zOl#y&ePmF;wYz0LS_Y6W=f>nTx=gJK2j#e5@8uFfLT+;sX|FSxX5oa~AGfMwAVcgE z$_TY78BRsRP9n*Sh{drWK!$Z;6&!)&NX%HWH|q17lqNlfiQ*O;GwnjDa<0N;^;!Xt zW9B;JfXm7<*^{c6LLZB}Y;uRVAOcdi0zm_K@m2w1b?_N#!s=y*!g_#f zb$QrQb_P(bB=fmsQrw`3D5wEQ6Xe^%0f9k5ONg{87*9lbMutHP(?aC-1*j1_AOO|* zVGQ@83djy6(4Zuw)4)K&W?`2^qp(b37N%Sn=~_lHi{E0AM-h6^VyA0^MnQ^95_6<( zTLhLe6#>BK$30|%wpb4ZgjX1lBHnlQGBX^$o`~;ka{l893(K80wT=gaa9T$bh~nfX zm5VLX$az7Ytwe1=fmf!`YMHc%D9RIfLt3#joaUs-w58J`H)F7#uEJb$tH+$uSahyr zTEr33l0KSRX$xzERu`y=s%2y<%1b=Iiwers128n#RYUwABDJ( zh023Mh=41DL}7nSrbD<|LBc>54rr8qM9twQQ*4(>0dlxLu`y|oM@+^PCmx}h{1HqV zVJAyA7YLM!I`l@p(yJwdEP31#C4`t)5h9fg^!Va(*hdp_)fzMEv!(-5IO$I?KpzLz zXe@lANv~zPX-LZGWzu75Mv22o*E0MZqggJHTKRM?%H$g)%rM&)PCHXke)K`$|tL;-4}PNcMwWfWwoL)Ze!-_e7A zQ3Wz;14Crm1c}ofAd6h76-lKUz(S66lu7kzWE>qwkk(^JLhI#oX!JNh6OplR0tEyT z8J%Xw#B|8YmGgCGt$`rj-3*|Hp`!Es3V~J^v+INbi(jjUSs`6IOOxlRFQPaOo>>`TBR#3pr>JFoZJsn(W606 zK$lMI`F5X}tlgl4V!jX4x-=*QSNX^c54pqb^Y|=;T;gN$V~M1JC)Ov;F=x0Wsq2Cq z5gXYy%8n$w1__@-2W4KV#Hiz&(+ZBzr?P^$-N;YjM!i6$62;M|Rc{TFA-@sl*!Ax& zwV5ve7YppW6eW!nY6ltk@M)HiGiqeH$lY-zldGkwOM=G;-G!0{$QUMTGs#L33yVih z^QRrg_ck2=smlytm_{q9Y0Fe&Rx17>+EP13g!M$g@1eu$;?6lwj&F*ao7xih1$71+iU2z$o~Yb346%` zVbuRtn^=s*2Sos#%L7@ri_ZmlbPtGgJk)g!}XfK_n!ZWUgQjWf+#z5DtBSx);eRcB^ule1BKv3})5P*P8d1x(f} zzrOc|JlNm_4!C+r1aaDfvZ7mr( zb!nL(znm>PNfA=YlrBDsf6I_Z+45zJkBa5Xmn&bXV&%$}DpsmgxoVB7m8(>*QmIn4 zTGgu8s9CdC&B|44*Qs5z4tZX)7^GA&Wtp-SNy3^{Dpeux{)Y(jnahtewgH}RH@7t#hIV-70Q+?Ripf!tQve?3r zXnf$f2}?I0JOm2OiCHVR96r*<0NV#moW1o|kuh)1C!ac8gSVZ#bGM;*C{N;?biSA; zlABWH$WNl*@Knq~x$+e%7V~7NMe=Z9Z796$@nR4aUg0b9d_71|F(t8o%XkYvHWJ_SvH4gSMV`78Ovc zmnA`Jm8n&hLdm7vbM_mXU%S_W=&UxM4wtvoe=*^2bz(~Ge$s5|$!~Yg=FM~Uz5Al+ zxf#7{?3nqacE8wf)2nwjT{L$1Y5wVBlYe-5xy+0riecFNc`IYy+XAUx$B_3)E&mrW;LjOO(_uen~cQ=8l` zd>hN1|0*5$Wczp9EfG6xKDCbQwqxXhWD$jbTmC(EX~@+|f4}Lj|03`A_AgrXZ2k9I zg)*yROg?Vgr~7t~`%S2vspihUBWwK0tfNPT!wy5R_QnsNb!@w&R5*VWM|rn#xu4mU zdPa8TYcAIR26jKJa=oC=rtb%wX!l%bxlpIv>&$VOdCpP8I@{YYJW z<~)D3i(TB`JeZ3u?E2l`axsR#UwD6Ha@^m|MvzL=Bm7c&V@#uBE8)C$?0)Z|18(Ft z8g@~0vS+zFm$vtmikp^E-&()H{fOSA!qX{TRJVI1Dx)tte7ER=C(_tIh&&r<& z_xl@aU9nOAo5~$Fu2Bj`-1u2t7T8kv!ie0m1D5%+a0fTW%8OP1Wk>1ry(#tEKb{@k zUu#MC@#r;q;WS~YfAjV7b+Y5Me6RZDtO|~KcdNlaO;L^7yKVIk$EQ47f3;I1mgjzz zbgbtFzO0~8{HJ}wqp9hLdF_h!w!fd+Wz?nTto%dZdFYR|R5jrDuCv-?kAK$UiPtu< zoqEp8UHuTt+!2QPxyzbOoY>Ab;djlAVVCBPu+076X*8=%`@eAN7W{WOeRoJy=6CbwwVJ!U#o#puDw}0Zx?XqG__?R*&g3sl2XnqzacIrw z!_KyA`E*rp8uKxK(fm1mJ9#fg_|-QIuYV-orhEU;*ZrSQSYAZ=dgaTTDfiB==iLVS zciQ;iO76$D-1WInPCuHPbAet&dD6~ZL}6er3i_l*^eb#zxbC)Q6rk%US)`X+|?NXB@?pcXzaUwzY^d^=#U3i&>+HGHzo3!g`xd&wI`0 z{l^z-y*O7y*>`(GyGIR4te+LRPrmGR>+pp=hDYDzl5~ASU%b3iM7feUGvV1+B-F*Z z)1Pn8f8@P;x!top(@DA!daBoea>I__n=S1%@nr8&Ct9>?^7)CjtM;uv z75QrQ&=E_vmy6E)(feZ}LYanL4-QImsgHY(n>ByH?H0!(E88wUH$Qu7?S(bgDVM+0 zb;e^Mt z20hGrP}xh|F4>Vwob1g2b=QnRssUXd~r>$zdodE z(@nay4@VCOwA{Y*2g!GxC3Eks{Gn_G?wIQM_G7f|b&dCz?6_)a1YOMfx&7Y!*>Hn@ z({;%EwWFb?Z3Ejdpz4w#-18fuGOk&$dFwC(1nd8BE=T;~T!weiXlHNNUVMmM<$9ZT zF9!a}lI{vruuDb;XIOf}`3Lmup;MPI$2^$kKAM-msP@Evlh^+_^7ozpygo_i%R##~ zt&_a0l6Rqh<+D`iw<1b!c*676&*yRmy|^h~w{X|7Z~od@?%qsZwok+6nRl#DTSE3_C_~(qC&+e3a zawz|?{d&jTuW|&rb04`)Jl4AN#7$c0nW>; zK1GybHx`1LmQvqZQS8Px{hmyA40+!8%-Wa5p`FTJ9NHD;3?M_h>zB5l{cDN*;j8>V zIeFhb+z-C&sGF~Tho(sXVJYC(`r0gnbK_0{9;Jvo;k8`?fQ|aad10)`^cFq zzwfgJnmgRsy4o*r*_yKE1H15NExz{QZsa|<)LF8%W3B5O2Pr-ZxE>D9A9StjV)^qU zcb!jX0!5TtcZ(?F!Ja8{^_!wO82q)$%^CyuA8eFcp>Lv%F}6Ry>z{H(3)=T+CpKoH z@TIHam-SYYy4d!yZNsDAsua%cb!X?%?)@9|8QfoSL%xdoyL0ouH3)gX0^QzP5Jqoq z?mr@bdYoQhrMB;p_f@%V4=Ei@cSTR8wfbz$C!4AJ;Ah*ud~wE9AlWjf!-RSLbveT| z=No-=r)}*E%kO?dWw*__*LcA3)8C%$x}@hOq3oB}t~De-c67MnPd)t`<;gaPd(FYN zl}s(8cysggSk>sMwKA@=@9~}8FDi`fA0PR=K6Pi#M;p!KH@|$C-|jK=L>{d0tl;xO zR}<#8?JoN+?91L2k(?0jM?PD0xpA*wyKNiH{^((Bey3-gp3EM)ixc)w>2vVr#nmqx zZYiJpl>uGMuFWki`OViO@T^SW!QOsBTBplu-7nt`J);@hlUARzw!xQ84TI)OH!1KL zgXcCc9i>nz7Y5reS~Tg?-cv5@s&k{tjR6CRE7i0Oo0JWS8>|U=POexz5W3rXT;9`$ zKOOb&Zg#$VdjIrivW+{uM}AgL+PZH}{fXV=6Dysl*zWPvYBh$Wo7VsMpq`P}l-O`P zpBc10X*{a^o!_7Ln>D-As;=jO=I`!tXx@V*-M@t!H_L7_ZcRz*7H(Wu zwAhpJ>+|mKDtCEv<8t0+oA_&2K4m>_)6e8e?eH9R_sP1nYQlumt-r6kKk6BlH;c8n z;6Q%+`3t5fpLM>Kn)d4>SB-(^7H|D*L#NI=p6_`1{Vn+m<x%Wt

iJaUY4 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

~$$x$PAt25tVm5F*r#`N!U| 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 U1#k7KRjkGFfK8&o@UHux=(H@kx8umHomK=)t1 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 z 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 z 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 WAoGz`i}z(BQ`^u}#QMNQ<9`5sebbEq literal 0 HcmV?d00001 diff --git a/doc/logo-bmbf.svg b/doc/logo-bmbf.svg new file mode 100644 index 00000000..228001de --- /dev/null +++ b/doc/logo-bmbf.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/doc/logo-okfn.svg b/doc/logo-okfn.svg new file mode 100644 index 00000000..035d7a5d --- /dev/null +++ b/doc/logo-okfn.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/doc/screenshots/balance_sum.png b/doc/screenshots/balance_sum.png new file mode 100644 index 0000000000000000000000000000000000000000..db2f9033bb3ceddaad769ff93496b7f37abe5966 GIT binary patch literal 94463 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 Gu>S&+%WdTV literal 0 HcmV?d00001 diff --git a/doc/screenshots/bnn_upload.png b/doc/screenshots/bnn_upload.png new file mode 100644 index 0000000000000000000000000000000000000000..b22dedd8aa94362f7ecc534feaa2a2a818aac12e GIT binary patch literal 33045 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�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 BX}tgd literal 0 HcmV?d00001 diff --git a/doc/screenshots/custom_csv_export.png b/doc/screenshots/custom_csv_export.png new file mode 100644 index 0000000000000000000000000000000000000000..ba7e12bc3ecf87256433b020225dfec0509e09c1 GIT binary patch literal 168623 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

VchBY)5>c8r2MOc{F48XwPTlYE5wO(?Si^B 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≫G-kMHD_;dG5FkR?LPJ7>AuYwNH`mH2+rTK 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 literal 0 HcmV?d00001 diff --git a/doc/screenshots/order.png b/doc/screenshots/order.png new file mode 100644 index 0000000000000000000000000000000000000000..fd6e0e7c4ce70f023b4c4b51ddeb16feba638505 GIT binary patch literal 152665 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%0D&lmibhZ|*i3 zl09DH>XzF96y+ufj(Q7i(6aB6@^gKX*wF=)ZaA%h+*uE)I(;Q=>Z zwV&s&V!v+n1!ljnWT5vy$Jh3uQ!WDU$Wl~Xg`-(yrjJ(^>I_1lf>QQ)c2c;)Ga+9S z$}I7YdxxVm8rAUZb@Q8hQ&x&NOhW= zEWv$g@~d<~MDO1FwI>UU+uqDPu(sUoE7`3J6!@2E632uUze4QUZacG7$UC1o(~hX- z(V?Ki%!c+d@hXnJz1ZlHA}|m7^hp554{t!j0G@NG()7ysb-Mf#3c^c;dwhY30I>}_ zyd6j*dHm$|ssP5pA?6~ZPfv|s27_FM(maeFgh#LkY8`SLj2FV6So5gltFJy1U`H2y z3@&AQ_|H^6`%sXKpSU;sHe+S%QX%%2v!UYSAop1>d%1HgYfHUZXQ=tIz#=7@x^H)G z%pu~Ho7fOB{ovhH0N>G|k7O5@-Nw)3!SnEX2{!;`r5W+=-4;V<2AR<3?V zFU0{OwoLIv=b9bMyDi?h#APTFx!6!r(@yXL}D&f=fUR$LB1NB?+`9!h) zBp|Ge-RPmbT?vvyKRoqthUux3XdYG*1sf}qtC&i)7!gg09($;i{sX3ds4Iqxd)M5A z&U?;a&YS^1P+!t>)4C0s8rQY~r$6^(m?(#O;Q3fqUJ8!vl};2g(vsr$LPS3E6PGr#eBv47f!55iZ`&+s zA}6mBkz;5z!lN_37PJVQKFOWU0@|gA2d-eN2JFTlmr3|;xN=K*qQ1XWSL1nY`1Zz4 zOU7zM9lbumDxaTE#tKF?_k(Ttad9T{Uwuna=t63B@)}Z^-fP< zS$pr&QV^^%8%BhCC(&-Mk@*lExqKKi>xwZ!eCN6NlDtpXxpT+90`Ssauv8X_G(DPE z%=`uf$@wL&*Oo2>lM?$TyU{mAbL!9xO6=OlQ9Z4)944?XJXt%zQa#bF5e+-_ivnh~ zWQxW`=LW9!uU(5}*_fEV`(Oz~+$Eu+d-?6lJ`t)7A7Zu0(7LI`8&bU^?049m-U6CR za>Lnuzcr~M>1ufG4bSrP8|N20pQ#|&&T4bo{uR`?bi8>Ug4Ec0ToN@4f93Y1QAj{jQh;N<=@CV5=tw z0Tq-uv}0L9`=285D-8R8w5Dq&iX{!E4;}ejx@qiFy<4##uh6Dag`v3^)9I9w?YjoA zueh_QlT{UBO`NRN!;jtcQ!{0U`%}3%2-uBZhuHya)LRu;^tIt~<+c?@V;8a%hm*#< zd=M$tS;2Azh1ikun5AV=dsn@hiKseKr9AEJ*@G+Ud91?IusVf20oJhoA+I7w=n=QH zE1)Hvp_6s&WJ0t7-8+k1+~zBk@FXPWaZ*S&-co11y%xZV|6=XbD!Fy64+Txu???Lp@aoo ziHwYt4`kPPGFypaN;fbi4UUi*1#ZtAnFFzrMbXe&e<}^T3)Pn6D;%YmrCNWPih^py z#hN9m(sf2GDe-H$iAdOHjYhGKU_8gBJX5)Ux{fks&veE*%~ILfWFU~GfDwBpcY84f zz4i4K)rqj%cs4&5`-}R@2MYM?xH+|KF&P<;rTkvlu_s=EY5r?b=EF$tbM97t4ZU<3 z_xJEVO`W1V?i+~wRPq_#z*IV6r-Taj1vhTelnhDfn#%Uaj~vZQ*B?yuvz2&w<)CM# z_}p(o)BQvH=+l*KR74@{@${`R5jN}p=-A0kk@BK4@~FyT`G!`@AJ{=6+A=Fo9F@imUU^4H-%oZEU~ zvfzBSxVJ+rjvD}5N11oA@*sSOB|^ z*~9OdF{thu#YQyJrtyQ)T_#-%bdGL~Y~xRkw(%;X(Q8(^rVF2dzUVxk?Fyr#eTI{Y z>ccPZi#3`b*=@2<%+4Rf6s!-fSrzS?Oo`JRk=);gv5~~qS~=)Z9fyFapKQk_Eotq< zH%b>PAl(Wq1?0|;Ssd4WQr1x(iRZUpHSZzqLFeZqAY{@Mx$nNqqB;NQEzr zx@0DncctQ&y4VrCXI|)vT&Jf<4eY6j$uzRt^_H`Y$)RvjXwQqpj1Y>Ef@C6`F5PB^ z7Q8XF^#io4OimKkZ}y)ilt~(#Kd@2$bWhGKCW3AqaOo(>!(9|~Osr9??MR+y>KimR z;L6TcrlganL(t;>@@AgN@_Sxa6@{z*^{Uv#{BA}vb%85loyH66R11Du-#(pKin!H- zA3=dmCw^=%CN`(Pq>Ti0Z>vU{drMI!!hxgZtCAJ8u8;lgE1YRd#=^{n!0TC;OTVk; z7R|MhRJFVQ9QB#-sB(`Xj`6*(1C6iuUeS4D)w_|8EMIT+W&>5$K-{EtHIVRf(5~7S zLF$rv-el93uZHx#Nt_Z~%wQAhGs-Joo}0rZqdJGqy1AAlg|_v=P9XyT{rmts{ZGtk zyow})ssGC#9iG7(#%_l6Z{*!Ko2ljL1A1HD%Z@=dyK$bx-Hh$h_#IDebDGn?Nxo0_ z`TS|{Gw&sH0lun#!?u{Y#~nkvF-+Lqua+?X`(g9ishlRwU$4K<1%zBjgfzBz%&0j< zhz5@doz41ar{!}W8~nQ>{Dw3Ttp^W$b^OT=b@2C}pn=0eBEUJ?y*1mJ>rT&qWX?aZ6%M z1nGv)X5PHma|BxE&!jm?Ha9Dv)p(5}fzm7hv20!il-(F+zU2C&LSSai5%~lszmU!@Y zQ}WR&!!&I%a*~Oa$}*?yJ>rXrz3Csd09Sa>9kIDToydo=-)?fBV>5Qd-agl;VKz3f z{6YC;uza?5-lNmgdmQj#Eq?!?Z{lG&Tg5vl%kDFm!RL>>Aj-^o^;Qehk^YUVRkwjq z)4^g#*X~DWB3~HS#riGybMwzyzfpwk%5dB%@C&>V&bU}{R87`Q-Z2CY25j*6|1AyAE=GoW<(<^%dWL>wGcT4Dq z)w!-l~H|FYn9Z3xby965<>r!}G6<81ft z0V82Q0aIR{#eD6<5ni{k5cFY3at*s2P%gc&WKH=dh_#|IE?q)jzSd}NBYgjvHxfF3 zFst=+E%`^>*yjLj8Y%r(GQU%1Q;G^lJ(2h-5HEwmSCLj9c-zrgB&mkLu4Ab25q(zL zOHjrwd^-L_s9ZICA}{b^PZVcLyRSvta!-hiR)u;*`SWDqc)(iL!25&Q+)6)4I-Myd z33Lb*$7;NeKwV=W?~m|g*Oq_HXpHy14o&}LIlIN(=4;-Lp7&q&n7`<*ALK4m7|`)3 zsojz77W2E0n!lmFV0cFX8;Q~tA4=J$&`2-{doReLelj-_Ne>9qP9!M%D_BLt$Zx0R zT_~D%FjloV&bVlKUpHQlD-6mm&f)$hu(3lHaN#L$>EWUL^jg zYakEZWKyZI2>iKlwtG8uh8J9}kPQ2)$Q0$$`{fo}EK!bzFnlbZ-I-A?mV|A;(~Y0m z$IB9KChc=uS`Wvkvv@n~w@8EKmY&^FfwQ!RarvRVP0|hD#v+2t=tW{AwC{zt1!lza zehO~fZyA>Dsk2xLaZ7C#`6_2j)V5_k?4X%i-SSZBlKXpI;?4iV-djbry+!|nrL;it zmg4U2Ufdm8+@%zPyL)kWDNfPi7Tk)v1uO3E5S+l|-rL^$n}>OrH4pQjHU9@#D=YaX z=j^k6f6m@(HF3X5XHE_kXx6Y%o~KzI^T#9Or5n)P2)*uDqQ=QOn6?(z=KL_4mclFVe@ zHqDpPD*RAf=J-~_FAGh{lWxB(9VJ@bfh`ROwL10F*$2&vRVXVoqcuggp*{t<68Xk@uBOSXlr=Qn%(U}+hzm9a zS=LwT+-Y2!=aT4%p#Py4ymD&PKK#cCqL ze|+y+B=SmAuhZTKyIA6qYETEcl#ZW?a)kJS-&N>})nI(g$k{2c{QlSNYC3QG=)#!K zU+FSmp)?*gHWfeJu3XV@E+1N9O=%l_Xy@V-`Rz>ZLYrHN^qW#SQ8F#>+qtWz$i_>` zzA>$lxjdq3IBDBUe&Rjd@)^E$EjjDXW%D1h#+@kL zNqeH>QEV~_wPLrLziymtvbA46o4fr^AsWRLayIVm^4)a%uGQsGC31X4v@)GwwXnWr z)eM!sR_q7-$Dwdc_dsC^-qbBW1xNOHX}KtL^w_H1)K(8;1GajWaymlPoqMTk)9XMq z_mg4`H-HD~YTFshxN}PcyH3CL(;tIIVGZwk1tP{~+AY_qFuqmEJ{v8=+^S@vavgK^ z>YzbVB(2N-p+Sj^WGIr=mc_pJ)I^cAH2JYdgWVz2#yWD9vH>7GULu~$>-D@M zz9SAB|KTcMqpps2cnlA~*Uy+P6v9hMDt=J?429DCxUo4};fPby`L$h{3*FD2a$ox$ zWk-@Y?d=_*{!@)#tlxB@LUBuzJgxM1y`n;jSEQ6GE5_3?uMgSE&DP1N6td)u5GD)Q zZ|`@L%21WffOEo3Z9CE*dSH#XhSeXxv(x=KEUL095Gn$#HcwJRpZt1xKlh^|MIMF} z;Y4I3iRdb+)WuGN5{vs-i>|}ctAn{w66MBtiHOT$@1xO3tGG{JtLY}c8)#Ux-NDuR zW}?NZdYmi=jjWdX`-3w%#;&9AH9AO9Her2eGKcBAZB2dhV&@Zl4Fx-A8}OHck59RW za&IKa18eQ@6^2JXW`0%ryG4SW@vK2o$H!MZ3dleErjZCZ^t#@#!)nxbXl~=wYg4wd)ng&G&4I7JBBx0Tc}z)6YvkO{824tjk|r zV2>Hi+6JlG{7Qwrv61k2Vb8fZ0hkN^lE?s<1sirL(Es-E|9CMJ|No3vRK3cmN%qI#%ur&C#i0jTD@kQ;YVUN(TAWZDW?3Zu!f7HMdFK-+PPZWeY3H}=j_M~V!gS!Kub7> zEj`D*$4Y9q%PV-^yvaFT&NG^m^iTDK)}rd|j!X~PXUIXYHd}Yx`Ww;4D!7iyn+x|1Pk%-{U3dLjh7MCv(m=Ej*}2Z#Fx-F!;3>rw^Tk4CLu3?n&SA&Oqf1x3aAz zuxfD@uXpO;5iQlzhLW!QVfsihMxEDd{$5rxW_UNyc#8Bv!N%9{!!dJqSNHCbC_$C$ zDb<(Lg(RI5g5klNYszXe>)cECr+R?q%Ck*h3{Tt#m?~G-5=N_Gk#6FRhwjSn0og0R zwdYfWCbU$ce!n=!?=|z9p!GQp%)6$l*m3M_irH@svYA;LcUv1m;syWAn}3L9=Y6PW zeT>C^Nh;ST(4|Ga)ZJey_swhWqsO&C2KmWDoU-Tzz^g}K4s|oTANBjQpYbo>Gqt4; zrl_Spr;T4;&K>zu0NPu>1PmqY`Oon>|3nW~Vptx@2b}fr?%H)XdO7qhNoTi*fV_WV zMrwomuXT`+OwVVRxzEPQ;U)pun>(sv1ojh?;HUn{Bi9iL zU9VFB1H(#_<9uHgdK>vG;Tzn!v{mvqjCYfn*H7f(2aiK=BNuMN)5d)Qov8=30@R`Cla4|9C- zm3&ifOs&0Dw3wADhuczNiKTZ< z%dPK*y~_C^u(nbtDNSO^egDZ`#W?#979dX7$@?ccH(HLzt{AS1qkv=v80Rj)c4No~ zx2NdKJ0)S=YyKToi+&>jfkJJWvX9rD(Mc0Tu_kSGC`^GHrH_uHf)rlQ=WXwte)n*l z(&oC#;yYqhvOMjob_Ffgpu5+qh(jGL6di7PRXI*XdBJ+UuyWdtkF?m6O;ik7{J~Nu zjp6(5-1hx((AXUNa-rk78>ttjm7BT34sW z>S(FwwBc1`1CIR+$-S4s&p?fk36_`def!JduZ@3|E%O|Phcvc}`~=~u&@L7**$WSN z6+*NFV40dy*2_!cAnQ>X_A^4f1?M)cu9Dr1g*zhq)Vkgave7Mjiuw>&JbLP-@q(1> zK31*cREO@09*ml!a`t`CUvjF14uoi{6IqcNwz!;+c&I1r2`NIhgU*E>-q^eg5`M!nxSBphtBegA2Cn=O}Hac#1j4S~)?nNM&i25G07A3%DU zob)njc8rcfTU~PhfM7=fZ`68sb+D@yy`!-i`*zn(zJ+r<-D0ld?IbUNeWQPMF^LtD^vO%R8D0aGFW3hc zX2QR4WPPu6b?XWj!rNw|W}!BgOD>94n)KWnH`W4799Elybg0#Lr(F#I;@Q)6m4aS+ zLL#Ha?q|owbFQt2v-io27CSzlyMTkf{nzfVWJ0OyZqnKPJq(`g8~5j~(-ik*z4GRVwFkt^Xz$BexA zm9X;gEGVmX!2Ltf;l-o{irKFpL(NyvZa+7v9^$OdixidFs$=W~%7<=}l`s8O(}+1D zQ;3@VqQhR0HqAH1>ZX8ER(|?TM>=ifB59?nsDI`9)U{;gAVW=V-Oa-spCzLGQP{B~W-@IZayj_9ixH*{Hg_aT zFhULY_*bo3Q_HD{+6U*d2A`H%yEiKBI`X=Q%fAG?A@DlNrFv#W3`Tw9?u%EjoYLj8 zb>fV~pL~o17W_y}SP5jga z9B-$fE+vL-DX=MMa3HJup~ELCV?>%hUuNHS8x7w7RHsXlD&BUl6`L+*^S&$Njn}=N z^e*J)yJC}4P0p+j@{+(>h<$b~1M&|+L-Yr#_O1yVDZA~2%~T!DPBoQcIL3u(@`A@X z(6w<7z<_oAGRAE)jDEj%%LUHqLtqG?j5l6d_Z8Q}qP%5&J8~YTr5Huc9olYPWrQSz z`Nuy}HSB{{%9J4(9@bR59s`kW0Aj*mI078k!Jp)rZ$}XYAstm+P`prnRSr4`B)z%@ z_$+MUF?3IyF!|EqLM+_3H3V|g4(KN8La;{Ccw19ITxwGG?VfbJ?fP{B;HfYCaVAkx zj+O>t-Xwi$u^MfXq#V_n4_I&@sq?bpTZ*<#1?ipp>)15e1xa{x898!B8nQ|fL<-PJ zKE7Myb~^vnqouCXn*Q^^Q^^=_0%oLluCJxtij(We4-4@YYMWy9jd!5p&066TSruJO0|19#9Ni{(zzj5DW6BOGPUq}AeqDM>drVS$&4;=_%A z6RFmYNBArQjyU>s0%3}G)gW14y1!EBep`tYG&Cf>c~)jKB{SP;lnKcMvB2FoOO6hU zQCNDy%Ia5)$@fW*X9T!77 z<7>JfPQyWWgPO!(Y>uirSRxwe2Ly9yHu)z_Dm)<6uI`lS!(Upo)ixdrb?+^d!I{6A z=XccwFs<$&zpTH$WFGh-YpR=wIjoLOX$`?ctkr66P0W$S>Ht{3arUrVAQa%a3O2xT zJf+O=9u*;Yd&9ZOv=BsiU#aULr_GJ(X0_sV(d`G`VJud;W@R@`5m{XIWZInDXMbaP zDpd=Ve!cLjhw?-;A~9*?(S+CNdEs!@b#MfCr0@+q_j#VqUoK|;v&p1cVxw&d5Mls3G#cJh5HC6x<&(O4tw7ce+6y4t6#7x-7o_cqu@5Jp5Fl-NC_z)!FTVI z3xN4zn^7^$(R7RtcC-85Kawu#*|DTtLAov#vU>L*fqm@0R)|Vd`GaVU5W(oLu4bp> z*+8BR^V)u!dW_< zFbsT4m+;1nj}D3Br*hD|)9$iA{w)$uDH3^;tHex(npeI@%dX#4x99uMPgCbtjP~2e z5y)s}{1K&(awVhiqAL5aqGYCVRy9A%L{eT;Sa_9J_x$qg^pB$Ykwc^45wdK8qyrdx z=&z|##eOdderT#_(lk|U8c$`f)!prf@Gls}lYbHw6}6~@jwQP6*gw=k7{K+y38`-k zeqK3q#mnaPCz{6`dDweR^j!1AJVfClbPYIq7|Ya!Jn&G*94Mto%BYs>4}r(!_U#u~ zk%#7W=@o=Gu|}m!GrDiXxsvQz9 zfT(X})M%c%(&4l=2+Z&OK7jq}FRQHPOK%GgaK2?r;g|ko_VYn|hVjJ!B1STuVoH0) zdXd^}hiyvVUB8a3pXU2>b~|fWs$*_(>CZQ;7CsJW9nb+Fn$4}`%UAKOwSvD}kzOr@P-{)a6A1nuLF-1o74MVudR?9G-{n_zqe#y1x z+2P3DzJjNi*}XUWyvNyhYgCf@a*Np$o$||kt%ltUpq$sv(6$*U|NU&r*Ku4*2HawS z77qjRm?*W(&^c*}_C0k$%=DLUK3NRtokJ;l&c=2GIbOQ&RhcY#52bodKe3?xs>MG} zD^h7+GtO$;l*}r@9V*{Ntyos?WAV9MY{T&y4a-P%;iW#oG9K!lvpdWX)V&^%uEA@T zp9uVfojqz0^87VhUvrc)q^{u0(X#H{M_Tw5A12s@1~2WuEg}d_2Pb%pIzO}Gq}fqg z_*fcF{oaMm!+r$?JI-MagWq8p3d_FSw_j6g+K8{$^|3y0?j;+&-V9#^xq&-(-JecR ztAP}f7J2Gmf?Eu@BCCBlCXyRJ1H1V$&&C_uj&14!8297rwNQ5EjmI-YeZ2-vEHPi_ zw}7Q{mKa?x)P{bL`DD3Jzn%kDs)i6SX(M6ZbKx$d1}vBb8CHAb_Cy0PWrMr?}VdtLZxL-Vt2&i~1J|@T?_wvt_{?;h2cVldjK z0rA(-P*0etCx%Ki)ubZeJjU0nx3WSYnTb0oZlMM(1adE#)z}&tXWU`!4ddQzV+xkD zZhA~xWFAmCM&$~{YAF!XSkBf2TitI~o4{kQ-zSPm zCZHa%f~|<$*Vq(~^l#MC#K%_!duwMp) znt=ue?kGYX4SxAp5T1>D?8f6V#rnSAD1!UWH=>(v`|y^1k+@EWvy77_(hHJ4E`P&3 zbl%1E7nhW5d}BU=SxahTlDCt%C*^Yj9lzkOeVB*l-*{q@Txj_IMP%G(rRuf&_hZEX zWCulGTo2fE%ap@`c-zeV=@VR#|AT?FOC_seI(7p4x!i25Canq+8MZ32s3-X;sYQ}x z_@W`6f6~d`F^`_-JTHQ`Z`K9{#@C^LMw&ioL!Xll^%H!|{)2+t6Qwr0;QXvzM5Uxp zlU3({-YZ9aZ8(;e6vm*iVD#VT!rn4%oD{p-?QKcjxw?D7ACdH7M-XinWiO3Ky*3Ht zGeqO-x+r<~q9(WaFst<8?(hQhO|i_vWJdQN%molrEi&_x#FvSNS`n_hIipXaiFUCy z$!(h;i;=VD4HdR`NMXwXc*W>@-x^s^86|5(z`kA2{_Nkfoj`a@1Jf{8?XhXw1rcj-5=n}hdUcRn;jok*P!tL?kSSJ#cIUH|4Kp{<6xlG#B&H~?E%0WuHsuDFfujkg zu1TWDWGP>C{gPeLek4`GW-_&BAmJ|Oga`f#NxUug_#(Q&?;?Ls=e0c3^UX$i2=$nRJ!bL#9LTf2>a=a3QRkjWt#(HrU&O1_4 zLPc9Z3rR;_AK9@s0|rc0$Q23oPJ<*+w67;*8em!YJE%JjSw2f8Qyz1S8U4W_E%=X=1KD!kgDK@s(>J?sJ9d|~z0}2ezYFnRx`YlPCGk7)WghQB&A10W1 z!zeb=ryV{n#u2arC$tGAz+ArG20EemJfsdgu3=NtL_To2E}!Anx6-}`ug-t{so{R+KE z$=mNal{Idjw5N1f6S7QLoNJZ6*liBD;E0bay0`eY(|W`kw+x)}8}}cfiPq{6d|jl(x2ZuH4_%k+3QUEMr+qKh?BClekaZ z;Ra=iL260z^xt}G3cbzo#AV>R(v;QvzM5s+6%5lX8(PBDxvQdJo}<^#LV$6Jz(U{~ z({tzEmxby#dCrLM6B)kvAWVX(fXjU(#rd~lP}!IMwh(c-=tR~~(Y4hFltcWcts-fk z&lzw5npM|QuvZAouwIPHxBg;C3He!2P%zo-d}yox@gL=^P5tH5y1Z#H`Y6HAtL2Bu z5~iw>jFDc0)^QCJ_S-54rDz3fkrcC^OKFu&zO#q&3XCQW>$0DJ-WL`VXctK$lpwj= zkNKu%5r9hOX_(A@<_O1uvj|eSgVGUq&&Ml?Hjh;`RqNw9zf>t*W&u%dgNe#Kq%hO_ z#V!Q_GB~k>dfkgi`n!ZA(iY=ytefq{FV$OH?B1S^xcjTQTbsRAt@`2gR_#t`J2^Qj z(^zWZ>!a829Be$-d#p2&bq9-3Q28f-_Uz$jMCA|ALEwn%4lDEw6hy~AnLk_ugtZZS zO=@p*I1Ghb0(jZ#d=6EZ$QO`J4Yfa1mtc;R_>Y*0*(yQ(#%@+!P%Fl<|L4M#@?an; z!SXqd&m4~>zDFHNnO5)I3@IsTuVwqrvVWyp(Hi{N*oU^?R;kDNsF97t13mpfBNXmW-!3ra5=ZC@G1xNLY1qIUcIZjB^>v`$Xe?dU-1X?e)fN>yR@BYMO$`Gwe1AUWwubJeAh(^oy7dx zHYa}k9~e(rC|Cb^gM#vZVm#mdPmJfA|KOu7-(Daf{D|&{ik?qS$tLT|m;U0|xy1h& zwgrh-BEsmez~lSE(gS*zt6V^kYsCkm&5Ao)jCUZRqVDEH#f7-|cY1!m_glP97Kf0B zcVqPVTz5s6GitK5oq|H%N$Vcw4FbZOH-4X@9?-*nJ>(KHBXm#Lvi)A}!s|dIX?~)j zl9KR!6FK+#HFiO!RDf>5sp^ZP%;hZTLlXkEuW4y%#mU~blmW&5HQ^v83S_a*)i^UV zGpZUI7!kc^T$Sx|BIxK*k}`^lRJ^>YW*iop1%%M6-M=a3!s?uCy6-=D; zm*ws%S-yaOtC7e42dfKLe|^gpEL}IBa_RpCyzps$R8zyG zYDR!Gs*bpaEZVq*a_=T%tZvL;$AS)3iCXVIYWu*as#&eS%JOn0N{ZV-CaM!!p6Y;b z{oxsqYd0$WvFePY!FISD@y6!NRYHlMv@)BFs)`=QyO*zu!MK8(+g&hq`mv);Bin&!L3sl%JxLPPn@RdmvY+dn^LcrMt%Ige1L%Bqh- z!mEu}w(H)Q6Eqo+w?}BwGCX}tSz)GlBD9kGt*_ddp>vjTSUkh5N@Ln+*jHO%p#BKZW5LCu+(;#Tlc zdh6vm@(u%iA)7RLgSn*#7{6ydFPObb?ZC7oqS|5zpZ_u273X2*UATp^Ij90&EapS9 z3hKyOlAO=5Q2URsB$-AUPZ7f*6t;~X+iZe9$IkjshKN%;6*+baC^D&cz{8aYh$PX_ zS=jj2d^ZQ?jwi$Fv1XL9!5QlSYbuUR>pAC~~j=M{|I^)v(pQujc_T!247E z6S6HFr_z?Y2*cmbPN);7>v$y=9l-nWTs&oD?V&{#?P(9D(^W<%s*01KCVBl2DM~Be zDXhD?V|7yXq`0A@9>J>y*T{~>D2!}lL)B;w^C-3~rL9l;Lz;DVaKsE5ogudE!$#PU ztkt8r#J%P@V+Qv_R3Fb_x8#}Wa*f`2Tnx93o$+tC+tS*MD-LgGA_UiFEuVTV9qGAv z>JMgyzX=-QPEoh_Bf%=|Gz$@k&W(<>VJdSP!x@Z~Z}bkIZU$ghFC2?I4B7~Og9&&V z4s?|bh2?alK6~=d+n;FH|E$ItfYygyekdGKkKc86;=?KZ4=b%|I2+9@H94m8*po>} zND#O#*+=5Ac0uV;Ho(nH<_-VZG4(^6gB}Tn^?XbxVXlsz32dA?!Sxi(mgGw^^Qe80 z>kD{dwhXbGQ^nfph1on7^?|?#;*C1YI?p$3i|lT{o{;x5>YhYnI6mJ%Lf9O{kBxiu z%YPiEgfqG&S=yE%mA;I>Y+v{oxyn<0}{EY1SnCGL3 z8Jm%$l&&5%7^%+sP=M{$VTTkIi8lYX17I>Hl690cGvp6zJU@fRrciY(imx05QAd!>L+Bc z6~#8{9}daSEb`Ih^Z=XPgZH~9zKh`scyKuGj+sFFtG~y@q->TqPoJ0ko=K)#>u=I~ zIF)R6I18M2O5;)gg9Rx3NI~M7ozS>*AZFGsrIA)J6BUE43_h{W@ag6W-#}NF2RtTn z60kBPNDqr-IDhhOf4VV=%+_mm>9^*&&2Z#p^WB`U9>RsDA8)-BtfS&2Ax&G+t8AaG zK^R#)fh|4|K$ad3IYai~6tf~=y3!+FKI6`|ZLr_O`O-ncobN)^=7TTEXaW=lOTFa1 zUCz(m60cBO7>j8u(Cc_vYH0wiuJ{fFE!nNSNTU|hqh;&N>n}|C-dEW5utbxo~7(+k-wb;uIJn3`m3GnU#7=Z zelzk9cJJs9FG{v5kcDyzWc~=80I;d5L@`^?8wX0r?hv$wDcKf^Q#s4sA3yRxdmp03 zfjPLiF{afe(m|fW4IV~n$Mp^^t_9A^Nd$eGdaBK-c6<1$<7;_grE@MsueXqK`X;Cj zg@{CUY$BH`nFhjC&bFPf`oC5Mju$bJsHbQYTx}<^TfkEL2pNpQGodZUq2|S_j*O_gV>VnqiP6CU1JA}B__Q(5V%ce(Q0I#7i^Yv0K ze)$xv^g++S`mv0@=YmDZYu)sZGb1~Y^Vf&be9kcto}9ZrqpaVIBfhAusaC`Utby*3 zjIRvnNI{)Y!vy~(Ohf$>3I(?tEGea9-zA8in9b*aAFGTeH$OMnjh}*o0{M`+>A1^Z zf`O)7hdqDq>JaztJ~<@@8@u)c>MMG!R)%(;AKmqO^Ng0=KdH+d0Usn*OdPy6qZo{z zHm)2UBo>_tJa>W*B^R8&d#e8|%S8{Z^;#jD4b&T)r%(x>__FtwV8PT8z#-aV1P^+s z=9RBNo8}q|b3OusDh8mYnLy@vZ-I5l zQpExu7%`gNwPn&jQEURc5yuH_(zgP=+Fy*~>JLUGa5>q}U}+?0tW^nP@C-s4cMC1n z!K1H)@t>ad5PL6a=Db#%0d1?t7|bnV;rK!Fs~cVMp-w*n{SYnM_or|p1r*M7q-lQ+ zHnxLu9reC6bgm42OB55!gYt|l#f2SuG6a9d5Csvg0IBPuNvTt1^pLbBj5|Xc=GjPt zY8#%Pl493+zB|cWB>(uTv~N#ZFBQ93tnB_~nh%?i?2VUWd`%nS^e%=;pauXGGEZ(M z|NKNKo%JCCnoK#s<%ir-N8VZsIdqWLhBJQaOrdoCt`L|U@`Q#Rebj!k5v8I2v3i@( z_F}}eHt%s3IE;Mz*%=|QtOD9LtrOBw%|&(xw7??O#36t68L8cL7SAl9&L{3RzL#M^ zms<oHZHSq#s2noGp1uRJfECvL z8hNNfPG^I``%;mq>55ZO|D!Vg=NZ+HKr7Ph81Ho7X9zL>dG7*?Nx;~5zmbth4{RIE z+ST-drC-U{__1lp(7{8$-W85B=wWNmB&MjK#MaNBVJzFfkrou-f==*GNH8rZt2=XE0JU4*nd2+C(u2rKZ^7iSUS6Ca`N{FFkw z@!jn}BNQb4wxsNixo_$U$~1cQiCy7#$IUSAurZr~E2~th;UM!{7_ircQK5RTR<+rD z^DZAzBiiyK*PpnUGL%q#A&d-&V=u`4>Ppo2$y_PA<2UF zJ?(t8t&1D;>j|FwSM78gek^LBQ~Fk7G@Wcf%pO+RL>vIY#1nnYu)pI>g!k+d1n2ia6wAgB<^|07QYZ9@R0%h%w z(jxY+tG5`momV41?dt!$6TrG$+S5u55{{KG`Yu)DySUfP{w}{E=ehHt2H=x8#A%Y4 zoTP#)IXGZ0TeJepb1_}p1O|%$6_#1b5#>zJ4kA=E-&U3zC2X!_kR&A+Be~x}?o@*< zW87o%?v{TrEede|0sP9WszXDvy57!d3U5!&gp=*t;niFg#p915zRe-SaFjOfKW;OM zcn=?`Q}tk9?IS-eA3xr$EtSLT{f>etj)kLNQXPLJm550Bq>YUv z>S#HeoG=p-0KW@GqW^y2uXN0Pa+kl-?P&t>`S#F|!cGICT*pQSvr>`iF;d)-&C#S_ z^B|W?G+uIyXP}P0rhHjFUfPjRq{w{VZ<7$rX)rRJ8W)8b*1v5NE zZ9y)kynnW19Sp_K6TaC@DoeY;EZy<0+m8^Tvqrn8)X4m0U*x;DktD&8#+P#7F)qRO z!QqH_Y5Z>7;yFSGQynyE{N_&Ny&oS0^uJ5QkWNo zQ8d5!_a&t9|Mkq3Oc;}JOMUewHD&Xw-|GV1{9OC$34IN9rAkUlSSBTY2Rrc;`~AbH z^xpJFI5tBX7m>Zv6DO^JJZgs=8U z(BjM6?Ztbt6LDMta*2Q*{ISQuxB;iNTB1nsA8`QZasIF#0IGe0CS1?myq2fgK=mwD zAg|B>~*;qC~t;&%w#txei+^vjS@q!vgCa?C9S%)7*H4Z?<8b07{QlaKYsg z7$G4={72+*zQ2oo@u$5xwz0|O%`m1vH}2|FMUvW~+2~{RafKKbm+vTEi@9ul&5ovK ze-btKtgN3VP+8r>Qf99&mi}<7k5wys1!0%bi-zo7#XqNOZ2UjkCj6h1vXqqnaXNUJ zjz+ULQHi?NY4F_(ZJ^a4k>_=u{pRB<4Jzxb|1!QpBn1VOt>DL6e12}<75-!8@54h=pzr+>6BC`sXCS&CtLR8M zWmbDgVXguuf4m_8@fybTP4M8-(Hpt5wmzE-v`C+g7GAKV9imcyT5f+cd`|@JYG;%KD7dcYY)lTacxx-gaIEI zwApxx=E^R(H{Rqc7~{?D1KaMd1cmE=emVnx{_Lzod9&>g&%B*eX1%OV7Q~PZW;-3T z?rYD3o3OxsJbk(P)b=P?Vz`mnvD-1~Gkitu^hGZF+illwBY1;1(qpRuS!Sj9HI=+q zXN0c%vMy@domcReXY>`zzmL|3Ur_)uBA!BGU8{GNTnsGc%3Le)ei_^lNEOFZDr-T5 zCc3~`Oj^KJrbnYLgAiDagQTI#YI$s|Z;xVR@)`-LBvs?%=*C4XdZ^kgOGi$c`)f4@ zdNd4oHxv!tC{|;QcjnxIkPjz~RfwYM-uHCR?i*@?TDYqR@#kue-Z%$#(U|8hg8qoRZzSw%=e|O1a zw)(W>1gYc8VKbjI3`ufvkF(;jzVXW-**auKY%+Gh9NvHlWMu=XyotwC>98haC|z5b zn`5y&hBgGUAflC-c|6|!+=aeyW!{s0BVTBx|0X4-ypReZv~V|}70F30(f1k#12Na> z!)M}!qluPN)?`L~kyaQ3(#f$9`_9+%UYJLHFv|_p;z$E#%jpp9o8UB;GCcOMAd|)F zRPl&%!@-&~agFnGuZJ79^Us%kXM2vMyQgyrtu(^IHghmw1t%l?dEnS9X={;eQpR^c z$G?x#MF5cL3RBq%=7&F?$_5ztBt#9!UomL2Fmyp+Nw#!fD2fF44mFGh}9O zmLyyxb!BBj(pq!)djdC(B7(&jQyFwD#kEeIY@^3HBrSbBa^%%b=KeN~mH?7;BaM~O zJ4k%}_b1t%wkzAGt1YP!1_bkI6BZ!>M}y=Q7shdknC%^%=mPJ2m!=?M!T!y0@1N7| zL8?WZ-&H!k6I|AI&zHJe-)rA>GG=&8xs|-|mq%?pGL>ZV3H8j%9PzL5dtC3>jCS=cCzv&TAsLFmmYjCbu8L`KHoP;i z(6LECWa*B{o&77?tTA}rmVYQ7Y-3xTd{Ae$ZD)RS2x?soU zKkJ)kOcyTm?rj;^i9Q#K&x3W3eb$AVesmw=BM0E^Lod8$Ye)5 z6g5RxzMmOh@?5o)*V0yGNRt_Ei{5nIJynW0Ra;E^N1LeI*bt4kv8Cmq5sv-nmgJxT zmJq88+zgpHI9}F!KBJ(N9;9oTD+>ivVvFUO3*rj%O z&l~vwP>>hzw(}FOBjsR&!3fM9jL57_B+nDV0>f?mIq{h3b|8G?NmNkTh>g-N6D!>Z z=K^bvFFqU*JDJVss){ByGKh?x;6iII;Xo>D%Wme-39Hj?UZ&=6YV^CTCPz-{R`mxs zNcyc|LMzX&Ev1WOifn*NjDS1;`LalW^(MNt+KAT%v+h+kuc}FdapiZGSSHJ1pQ9rp@XvA-j#*KI_oML?x*tbN?yK_2TjB7wK1~z1*LL){K2%blL>BbE z5z>9W6ZLVOxyHlomX#M9vY+1=(GyZH*6@`7uQ~8vA#K0QI)}34!wXE)%3wE*~Z_rNu&S0)zqUsTgt}SLC*ZFCRG%J9O{* z``sWbv_!5#bI(PQ zc~58D6-OD8`|+Z=jAT`v1THfpZX2dgoSp_xHxNreO6+zu0CoKa;MFFR z$xgHk668~PT8AuMn6e3VJlUXHy^&k(h$*$D&s7^=*rdbt6`B*$>V4zeoVjkDpar>` zdc5k>k-nd*BUm-R+zl_diY9OWmSXe7N9@|-m+sR6uSLJQdHvj$6r#N{;tmOSKm59@ z$-d$7#92ctlk|E-2XA7!>Tib_Ob%rHKI4k3E+g~`>azC+p;xx86T=O@5>_*ReTkI& zQ)e?iCR&(qDR4-W1xm zaTOWu>h)CFQ8GWDi=I5N#XJ>3qNiLJ8`0=UI>wc5wTN?eFi!5la4RKC^7EM4=Z{lU zG)QRlW^Pawxd!Uq=zXGG|p$Unb%Jf4A$yW z#dzy<#2)H47ziQArwmaEvzM4%* zwYH>ktQ+rSE+sMD!SWu1zJ5G^%l-K1(nozfW}osD{)CUhI-hgjg2jGafTo2@HFca= z+H~FMLZLGHSX8+lFJ)S`syV}v<@tyaPHxR}F?4t$zw$X?qJZ?&L~PvG>&^=+F+_ZUxT{y=IT3ZT zZr54i_2luMznpn3pY!vU-0827tSCW4wND7;77h5|58>=mPPrAhWgewErG`VE2?P9l zvZ%+&a_`Ni3tgmJQSjKHc*K)$%JMMBWjn}%YTk)#Plu=udC`LiDGDY=*{4-FPS9f+^9nKl_*^$r`lHQPK7Cf z;yS1O>#+Eh#81p=L#C4IizR8e24`xoDNdWP+mzL7E!2)L2|a<7AMCMeO)=lP4BHwa zsCSMq?|u@a^CV<*u-1Hy9viPMG*tX$-BameVv3l>`N@KA^H7nYGmSGbghMmn;1n2} zYM}8}(z9xHqTW)h;LE}Zd#qy2sNw8_buX4zC$Nhru0DV#$Mb%{zh>UiObD;-3Qn%b zdvK@WUsz)+Fbra}c%E(Lq`BQT2lU6I)W{^Lzk^@ygL0>!(J^+5mWS)E3+`ug#z$eB`O z{8_@m@Amp%AokXeA7nVj#tXlwoi7+#9PZLFFy#EOpu2$_SP|(qJkWKu(IW)N-)Fm` z&{hhJ*K!;5KvF#&ZpW2&eh9AG5&azn-@`0wo0(`*TCY_2r0WZLz(gdgR@VUDjcSYY zR-o_$Sz<^yDR^rNfQF8an#T(pa(x#oDV$p;9n9YB+_I!yYLV>_DxxO}#YITWF1J|A z1ciWT=fiWqb)}Vw?&1g*aA>hQ6JTUcYhoYDVI#iM&|FKXtfelghNBD_gnhjCCHl2@ zMPoTchhT2Vtk)PEUxQ;5E4L}B6U|pr2np#}x<(v!aNP}c9LupyCs5vCcfV(jVROgt zDuJ6L{yzx&>!>)E?f)N!5Zr@%aCi3vcLD@=55e6T2=49{g1fuBTX46*-DQy9oO|x) zcHICvZA_3O_H zC`;%xbo99lCE4 ztemO6wvUfcLte(DBQIrhnr6srYB^s)WD=WBX|WYF_+ zexK$;x1<-5?O0V#`%<1_{76~414R?7aqKtaGrCF*gL(kW!BHU6?Lbc~GF_fKdQIp3VI ztUT2Ga1p5hBUc(9+7)=RXC$~v)_laLxdU?2`3;fFglm38IQ=Y3*p5R}UI@a+!umSD zVjcsICGtKjj7y*rT#G2};n-@q;F{(`#TshK%~mD=4z+^3XK_M=)z9=oZl~_ApzXpo zua!u#^tCZ&7t4pr{50^Mm0&Kp*_02G6+xJ2)I8AsZgqq&_QZ+}ai7)JIYS}xT@Nx- z7}DKhAu6iS4Tmj?P_RFLfl9L%Yrkf>g%+SX&xWuHfWR$y`3}m*k*Aeg;a7nZo+IM# z!&N3%|6_(_9|-?+-9bl1uMwNV8&Y8!v#lDQa%CBs;)#6ER-J)- zjV+Ue-6ZdP|BnUmKwZuRWx2&hqws}$r$b|UJ;9uhM^5AVay%gs43WQi1vTFBN28yr zXuLdL3xK4BKrhAQ(=c8kxu-#PpGqy=pF)T0gsYjyXGG>1vPZAJdGc{o7)+O`nl#wT z>dEkp^VL&PKZhOZ-pl^SGnaQhHjsm50B!*Mz{6qhX*r>Ejjn8UQ?Q?pqXj5offbOT z-j~wR76&h@!gWvv{|l1w_=QPIi_kb;EnZjJis`(QJKL=`_M{V3@Iq@SAi?n7i1qvY z`X>~yzx)&x2Yux%a~_(npFvMYWRcvq)eJJc?oe*<#+UMXV}EzJm$&h4nY6D2K~pC| zX4EVlMYV}idd*{oen*gMEL%}Cot(F5_sQ?HC+x!rnFVqs>(Eb?glgh|^v~#E8?`ye5s(cz~szZS`k5jPEzP!(m9nU1jWwkm=63 zpamWOqv(cQ*?5mo%D;6NE zsOVS=9OMH*4YU6TQJ!=nwg(&~^lwoqMJo3Ddr3GLI97NAq{p@_bKtWvgd=PQZ%4p{1K`hc~b>cJry78$Sih6%rAME|=NJi5B^(3BgN!#}+s{=-X6#jJA z?5sXdSfa(W``v=#cpgwud(({RD%9%Ju+@6Uo6GMo9Sv{M1LywtXUO-$Pc}xDlUOP? zxhLcI?iWD-m2VyxKGPIg_}l@d%;7ty)Mw_Y(bQQvtVksDQDYeM?xvB0R@k2G_XQo@ zWgy#q`EXQHZ;7eg_{f)F;bg@Pf`|QjlB@X$&y;xWD8VN7UTTHCFchxWZ>E)DKbGx= z{W0t(o>Gb%+vKu(f4GiD>F{j%kiCg_R}M2*fl~lo~(hvCDK6e_5Fmix;+(TKMKpF$PDlJcnu<^FUJ_V&ZkV_ft7eC`HSUCq{2LDyh%wOoR;(WWSct zt!T-(fks(P?IgrzLapi*X0i@^hsE$^+6|dV>g;@|p`<19aOJHxrW#JNCkZvicew(0 z?qn#v@q6G<1;6#gJgy)Gn7n@A4Al32$BwI^jmiYE4@rfa_un5&(W3s^f9wrGu@wIn ze0Fn87=R2btuNTyX=a@>b*8AXFQo-a!Q9J>PxxjytHO@EkaO3am)abFcRy+3%(VET zJwv*he6{*jnT^jLya5eMA5_@B!IIEi*j`vrjtn6)!~edcdcB?rj3%&l-;UR{fx*j$4Sp zBrcs-tF?*P*)F;L(qLjE(7_v zq`S)9;QFHb>`?pi;$mQL&oAFW7dw3Lyi(PD@nld=&+NX!ZM=L7=A+s|!JK22^SU;} zBGK4mqweR)BkUh+UZD{PRJP~Dcc=0uZ=?j)!5c%s>h&`17TD|#9G13RSUlm*ff+|N zv}~^`qZTo9Qan%BYUdr4v4iOhEM?~n7$2fm^s}+;Pg-=e(y&7e9B>DGzOjPZ2F|B@ z*A~oIhet#dC!=K=%)<<%{Px`ZT((A{HUtyV*3rR+3Lw$HX2P;mWoG_5 zL5g!dH?(!&f_|7b+=P=rK9O>~UygoYX(JB_wSWq)8Inx?V*Tmj^N?%j3ZdDVnjyMASMjDQsT}!ExJ+~jxJa$^<9{yyZp&jQ>=Mn;Zqh)GCDT3n8OWTM`#^$^KdFp06$@PeKw z^X@o$WR8H9{5?lw%E{?l2=bJ{FCfFV_`bwjn$d^>q!uk=-*AW7=BE2G_!82v#=?7f!M1RLYY`zs9j7m zeH5}iXS7~zLuB3oGymBvaH<8>{pJPpc{u&P018>ji1Rt67zh}w~DGU2kCa`iwpS~jIPrxu0#M(8g1*j%kQ~$~8_g9*# zI1hGk5Zr}9C$)!vpLkZvu9lMLp!_+jxhpTpP4eF-2mVu>*I~?yhftQMP_k}E*U3dt z^YPC~i!X`ZeX5{HEG=nW+NwA{b|k3q{vUJSDd~oWOH9_>{$+fL>M)GbXN58~dhBp< z!^GL}cl5=zBMGbD>)FWKLLM*G)TjvXEhzsn>UTg&*NBP#<0Y1Nzgp>Sk&+sjvtdGF zBCUCh7W;VEXg~O6>%?czLwGV?TSGG(ZoM=QQ}HjgUgf~KNd(|bPfUic*$jTKH>y{= z!J==9Yx!b8>1>upRba90&>Zz%Ju!M@fzF@t&rR{xevSH7k`Y>ZlL2E&A}!FD%HAdU-y4(PY35RCQ3s)R1of3YH4vx+J#*Q!6y& z|Bj3$I#5VyR71&Ao77)wfu97_3`cYe9&Opb(!!5IF2qzD9tK-UEzV8xPbTQ-yqZa1 zJN`%tE)iVNYX1lZ{^$2kOpQPJg@j1?D;xY#6np`)47dpY>)@dIe-p4m*7fDEDX^G- zX#aCvMk-Qo(WPhXU((&G;VGmRxC51L-T&Ce%dQ%v5@bl|o>EXeC37V2mpChH`f1mp z8QkTO>`>GAW5jO`+#tWdWUTz_<8LAOO7YcrGbeJ12csJPC=tspJEx=ye)L07U%pY^ z7IeuR<*|j7Ae9oLz(9Z27`h!!B&y=a4Y7P=B+dQ zrKFJRLgjLjwW)6p-9Ku@QdxdV8E&wmS180t9(6Xc!26G|&dTn&#e}BN-|#>E>(a8C z=%(+T)gtu8^w4U`<}F))tR1SJb!DIywFH;9muY$PR|Ujl6~+X_wQMnsz1*R9X%E%zf0!Nuf)Xg6UuWC(f*_#t3ppro!%6A zEG;V!{Zmy6PSEp~l!c|pj~=5q>)pQ$iDj4i{8?zj3mkQMl9G}tE-7t9JUOa=aIn#j z4uhBVe+sU7j6A_S)D&qAIqM7TyDFDk85imkqnV&vBx3xPnBCB?vX=j@7#W{`&(IY> zMjQ&GadUI8uC9Wc0a!-|{2Go}jQ^=I4luMdG!R|m4?@qsHA>g@JS+x#t>r}zu39!Ha5s|ym2q+xJIE0xA5<$#;@~+f1X1ne> zd_$t1?4IX7H$HMb(*e`l5yu9C){+iXmD}N1u%U$RLhU~kr|f*)?XW5ZZum+ganCY@ z{7470e1(NRz}KGd=eWb#sR@~$kFT987zQZn-p|FzvCT6YTy|h1;dx@`L|#lnE4=Dt z^;Opb_AdE1jMUFV$GpD5`3w>-kD0GL-=|#fP{l$_z>YCFs!OYKt)5mR+3} zeH-T)q5V#vezhRy?LdMfBpmy>V=z19YTTB6OO-$VjsLYN8|v@!Vu3%f;Up`X{88CF zvTF1z(9L?e*VrFfBJfjHzqpGJZ-0=qn;oxBzFEJ-gtAiI>5yYnV=(qiMfEoFRFQuZh$_#%+&9hRiv{d=lZL}6g&4N_?{p!pNE-OV4Ft3lVlbe?|7G2cH z?d;H^Fo!L6;4|QfAu_8U$L7Fn(>$s;CtAvyMpt;=b9_b|#3F@SCpmlloz?`P16W*I!a>fOl9AHkHd)+|Jt=BIv4jAXqAHPSn*R}+=c<8e}u!^{J?i0*e{~%9p72#PkogNtJRK3XODOn`Hcecb+VJJz(nR;Jrq;T7NC?NqE*t98$s7ic^= z0++BC2hr!3=j4G_yRGah)j*U`FxS@BD%aMT2g%#uId+VT+i8g~8z{38BIIN-^s{Kt z94LjXFzy|_#f^xlJXsmcuIA%d#5FKR5h5!XbOau!4cK=_BDdaWa}&S1;LG} zh^xJaFq^-ZYkwhXNPyD#=6?9K;5KUKmfOd`(F4BWLe~Ac*^r|16-M8BdGNK2Ooz&g zb62b5U6;)9XyN7W2NVtanwL{gH(;q;6i2PL@bjdH@1ik(!no0gWbcVV{c1p z>T#|5-18KIW>YuIBh2uW$3DOtJPDIV@5HRgg|&F{XB_s|7L8^1xpt_9)b>DIp|r_g z&hUAAywxBW^m)c4`^M;;;B3gf&s>08-VKHgIWGt7M-;^Maq#5J>yg0^hG3vDMjcHHCWQidB^uq4YVsLx?(gXx#;C^$f|(<5R8z=qQlB8b~u z_*r0|$SwNKyE+Rf;{Q`A-u-cc_@wx@xlI0rD!9RW5SD~_yHVEynB2NWvKrT*^wM;3 zG?8z2 z2+~+%rPDf~2?~ZKhS_vbs;t)9Bb^_2k9182#_V{7@-IC72K3BCNPDNX0tiNqqfrLU z-@J%@xPrHG!yb3j7x!X?>gaWN?)ih`uAHau*)o^b63SYS5S<$R1P@xWJbv@EP5{5nUwC81ibA zs>t#|Wj!{x0|yCjyXtFxj|aYUpYgJLACs33Q652UmV^1?RnCjWNv*NE|+=F##WUVWXw0LbBhk%Dmg_Ln(G~Tu$_DKS2JqleZc)~Z`5!O zqyF_>t8mrfCswycvZo)%P3~5huM(WroDXVDG9Jm!qGlI;;^Xwo8;JwY!6e&_Ig@Os zzpy2oa(IllBQt#RXo)0LMAeeO z7g%b)+HR-;cL|JF02}Mu`xCjzgs%eul=SlJqYpEqLc3#a&J3FRA)fc7NLD^h$@Q5? zT(%>9>kafTzXX1M3_P_LW^q)vxQf;Mu}Aoog_Eg;{@CaC}ZU>e(Za0ad)F1S?A~YX^X0Et;$_&(_35q#;go zSGIQcKGORjjtW0BIUwPQ!k);n&;WOm0V zt9eWh^ku=c3F3T0IYq;jBk#WTFKa%TCz>rkOlo;^jaxU>)~Gm3OkXC%gaAj-Leez7 z*l3^c6~<{a1jFwU=zmnHOk5P6KhQQM%WbP3ueG^2;*_PBkX*mXEWC<)C3B@p>bHh} zT!8HGG^4W}JC;9RX*FJY6J+kFiM;@sT?}dZB~ZQ&g{yQ9E|orsYNgeiUd+917E~}^ z(uC}c2kUL-V);Mp!;y3Wrz+2{ElzF)cpN`43RpEIy4Qk|@^9Sll$emXAmZETKiOww8nzG6N8qE#MH8}Jf zsj1j81P~~=TnuC^v7o2#A62`0Y#fztwqr2Fwma1yuB6rF0oYydK#8eiA2)hlKd@K+ zUeAqrP|P-7cD#lIO=?W=9+R6fF85<`yLLwnch7a!Fq+VC&vNvalKUxUdq%gvkO1oo zTefv^C)*0M$$B}r`_BLFQO3>`Fy+h@BbACxya)RGIM(Dw!;Wh_z;diAsiBTCB zs1j+K3=a!}ZT0PVtlV;Y1x$l31^Dm9vnMxx`CTEmwb;WT%9hf$nqcTptVrZcXKbMg zbnE)RqnG%~D@)tou3!#sap|+Ynv<0)iC3RaFqz=0{Zhw!4pA?bpDG`G)ERd8=Um#7 z!t1hy_meqrh?Uf@fyTxb=a2@rS`Ru#GVlGiBrvgbYZ8ae*V3eOA+^#J3+&%fKK z-3%0-&-v+B2A-q2V$3aiDOIw812l>(*N>-j!EWG~E?zg|-=6e0h%Jw6o4lSidDO6~ zz8^c(-0iX(Y}d*W&xMuj&h6>UH@xWLNmJmYYb@(^y+rFv2i)4VVka7u+hArGaIigcAetl6T>Yu>>W!t>NPs7Nrlg7`{D3=yqV=p4A`OA0YsJSAc63(Qjn^} z;JBFq@fcMhVOn+tt*CWu27du}w~H^@(_p~$>oQW9T>Nk3&g#lFUb!gv9+<`DD1#f{ zM=SbbAN3%`_tR@AfSrZJ^-Qa=@rm9%BPZG{Rvqshh#nLG_Vt6GscMQz`D5Z~`Q*;- z*46{=r!O>F5R&E7Y9gz@=9)5J;BtHDB|4q?Y;m{rD@{b_ziFw<$Byo3Hnasm#t-~v zUqorb_@Jff1(E3iOZZ9^qu1VR&mb&S4L`{3EkD?XcJFMth-J^@s@|a8s-(kqLEnI* z?p1M8-&rGgqN7K=NXQNKZ93VKDQa_qL_OZ)wI8e8Sx&ndl~B6#UfAgJOtz_moPtqX z+U!Q-Nxcy_bxUE=aWbbp8S-K3aQph;a(BN>z9p@6f+*nfIuI@e~;-$l%j&f+x5=10l2-|}ap-a_2-VMCQ8<) zM|973Rx02_&o9%pUdp_-h7y~pd+F6$VW_b{6%!K^zC>G;M5+Pb+CZ%w#qZ6`b8e>r zZLqngSMS-DkdW`_ZoBTlMNRZ;_7pv ze5MFO-I3;L1GDjJ4Ma|c@C02;nS!V|K-tPgq4x~Wv31rf!)z-vSBnPhUKHxcwyN?F zc40^7m;v?jt{(51KXP8m76_;HKH@|f0ew+8uCK~mI!#xSYT0@}o}_ixRqkmyQ8DhW zMFR}_)1aE_iN=eFZ;RI{EGP9GbYWh-jSpRUOjx`;*=Uy2^|4js?Uq_to>%SE0FR99 z#4_fa2lS7ikc$?1LrsDpVMtXG)Y7?k1&BCISEyJ9@Ce)Y$>s0gKN%Ai*+?=uG`oCFk@+^j639?PtZ4f>B5G7Z@}joG znhD(9GlWMfQ*MNQgAk6`4m-UD%vH4BD$>-+O8eY2I-$`)K#J zHLRDzl~+?g0aKx&FMjDSr(Mh2DVZq#LZ>JU;gi{Qi!?~rGWf<5t~+S@if8n@4RGXHSB-$J3gp@2=jA2e8U5v-W28!Yj>*mtZr|^lpDB?1OOhO{7~v zZYN9M-dR&M3(J}Jj9|=a?y$+Ba!AK+VY5z!{ zuKY$!nA&~a)KD|Cn_=iCd`X`U<5oL>-b+NSWfw_?LPWPS1n8D=y z6xhrYI;{h9*3r>K0CT6O&Sv4%YW$u1!bW9fS{35Vt%(5F481wHObJO6VK~uJ%POsT+u^=Z44N|BNI%flA&aaGO}T?y?;wsXB5;p*t-?%? z_>qaF%{vOADXL22sL4iy=veqUvt69I!tG;PIJ-;LGl==|<~WkU?NYOhF>8N-N!5oD z%f@IpP2fa#fQ={bY2<{}^=ePMH}&n@JQcJj`ykOaBtI8XR+b8_Xsd`M7lsf9F2u?@ zUzK0lyPJdEna7NjTqx8P()$+Ax^MAQ)c`Qv?Vt~@(~4soXuXl%X;8qT?hbG9ly1F2ofepGZ&;Ux4UQfu? z*31Q^o$(n!|3#5--UQpJU*7!htPGMUYMpk8GqB?A)l|&K2a%3uqFqQ{#-8P0nsbs# zCh8KsI@!R>xBB-HP5@lfj{EGcrouKvi|@!Si5xqZL@g zdHnU@j`6ImbmpDVjuH$#anFZWkUVC(7K{3SeD1)?&BZB%FB+5%@4&s=%UX|G3u}~_ z+IH}Kg})SlJ|{@pJ~TcPFTYIMk^j3%q#-9<@l|V=OV_Zys60N#zEBdQe9zprK#((k zNARo@uYH>DjP&DHmTRp6Y`D)F;6|C1B!9uq1*b6Z9p z`^}-umf06T>^(kL;$z7Zi|&FwKv`vV=rhq=hU}7IQl{5?Mn^rfT!5&Xjz>{gww;`b zW3Q4xodRv8Rg>jkkT_Mu6f6nM;H$R=uu}($D5Q)2W@pyTDxUYPWn&eaJ(!lWld58X9!zHHOhf+JN z<-@6|0k)0lZjflo+)Ig*X+rl|&l^w?%ysBfJ}k+Z z3-OG!gSM6&Bt3J<{g-zb_qAh#7?V@DL8z9JX7aFh#}ne}uA95+=UXtcuoYRaA4N-v zg>MATUL?_L_vChx)WOr$b=xPc#Jdf-@93ejeYh_te?+_A3!}17m%g7F&%W?eXMc=} z8JVj^xz5{5nP}*)Vw2cF#?KqIDbbX;I-^<*X38IrYAvdH8npu}vlG$IV@afmICq?G3uKX&s*qe?^jxa3W1px1js|0$8TpnXS>Wi^JX` zA;**sdkF5kqJCh}oq~?{8_#;$Bblp@W{~IgM26#~6j`U;^QGs1-|UJ?oOSOo)j~6T zmDJQ+KJ1X^727xK@MJ20X9oNG`=1?EWz8ZOgl$@B5#LT2W%>us^FMZ#cD8VCpg&UIX|%$1c-y6jMUW?C!gX*2QX>JLK`&Qp5x zf;)TpT=1Er&h5OYi;p4n<=kUokbdOF7jrvFk>k`0Jj$D{8X)vU8$~?{G%@ne?Z-16CJL1 z2e+-k=Ore@W!tj-XSS?p1eb1*F01uUxa`o4{4LYxGqPXW^DXZZCkWCEb9Lw#!y~It z|0>1gahI^^rIRRjXu@Ly6 zg1zka{S#kSR@UOF%U{&QsB}3LnS6wV#xPz_Sbr~xYc&4+N!8;H-ov1!hlW79x?Jl# z4bNhS3D%(mK?R}9k0^Or8k{vpg2T#jQ%$h5!cp{4$cyY=!ghan5Wv?KD}9&d-X=)x zN3Nu)$(N4fGiuhGT*l>pK=|d>#W3f#-<7@D0L2l zXS2ECS(F;b!|0RuC+%woE47s4NUkk9_sj(_W4kli^R@K$)UehOBO-Lg+mV@3^Lk0|Hm zrECb~{8^M6eYJgzp}yT~7F(*xe>oHz>(0cjDml2QPK+&;X_;D?$N6vfpIHGrC3Jqj zQrm+l(`7G(Ea6_BkEX5xC)ozG4L+9I0}9Q-+Y7?m`{SOAZMmH7%uVT`j6OyqvIL4= z8QT5RzC9kh^AzV#%yuN`x#sc-f(Gc!F1}Zx4@Hi*Q-17YRSre3V`*5 zpx5oqu~#j5ya=_1fHoTEk?pFBJoAsE>)IW;?Q6*_6TLVBBBrKDK*X_hB-A^_t}(4X zcGR{>Y!N({W5HFvB6q=sg#UUI3QB%{%m(&}#l?S+mX$5HUr{R;fkU*F)mUkGczDQ) z+p!Et%wWhzAp6ac^7zcP-|5v_xR=<;suWDk7O+*-ph|UZU2xJKp<_^ zhuDH{IN%Qp61&f$3--!L&#fqA4GU}yCrgUYerybSeqwz{yQ`&4=3iS2yk#4oB+(od z-m@0o(t!KOOu!ML!rPDka2)aqGiPg<=oVj*ynacrUsg{e>;661`Rj9tc8)@w{|~|e zraU@P{$kOZvi%B;0SjoNT4N*dj|gP) zb0sGJMIWivO0mNGe^tpGiGx8VO;Z`0dM!M3y}oBRdir?e$htqgi-m}sfoVbYQ7M z-NB3%LirzHXTs_)qEAv=)m!gBYJs1GpGBJFS_HVi^-t~`$b&&D2&F(1u$A$@2;%-Z z5fYNYAPctRkECQ|^6CGkdeG7T;v4_uCnlEn8C;bAJm&vj1kwMSz<+o=?imitXT3iu zO~?fq&OWqD{Ux18DN>=_jKEdVQ`;ZgGE(V!X?t0sz`*p^{{m(CY(E{Eb((Z4$o1*} z2f{<@?fOf`1@MRa-$G=nB9~?;>?=hZOKSg7VkkBLsE7{y=z)+lb#WdlV6;#fPfRsP z6(!ie-CYfv^0At6Ww~qaU%$wv1;ddc7i7ziyio$?^;(>QKW9_H9od2$7G=xC+`^I3 zQCenZbJvRQQUWhLFlgq>g0bfF=RVV5Ffv7N~K=pA?_NS(dl{dQiX)=FV=5v1Kv=%AX@MTCu?G1n$hz+*JNQiG|Fj z{-FDdHwD(Ebk6jQU?OjphAUG`F3aWoF)oCIQU()Pr;%METiWrzG+L3ljsl`Qlm_7Y zzZxuy@Bja}$dUOe=sz}u3>7EF$IGt{+~(yMyqlt<2*_%GLc_(K?Me*&s`zh&atJRE z{Km5Fc-}7BkNbcDX4-at$qsJs?5uoB49!UScfpNiA1XjThCD!9icNWig4AgBplro( zskZ3#fI*G49=Z}M$>;oc#hCbbXn{2YF>pf?lAcarV`Bqu2K;$>ku}qX;Qyz_@cM&e zSwn2@f%ide9;tM6s3FLsiWUAv3%MJCj)sKe#BzWK=jTGh7#Ja-(XeJ>&!pjxl!f>1 z^T71^O6LqP)XCpMG$&B$rDLIjHPwCd1`rg#Z*ZJ`&w2|Et?hZHIpADH{8E!T8CAKl z|8hQG%b$yuoX+f5ZjAdjG>&9-2MT6h<_HD|g4iE32QMGU_kXvb{0D*E>J|02*Nq4)6)Dwbd_k(yj^Var9P0D-hyRYpLnT{m>Cx8*szekm68JFB^ zzo#feW;HR+*%H~s)9)@84g8fz(X8xaGxZg!4L=a;L}?dmR>#(2Vz6W?D6@@9@jOe7 z(GM~(l2Q(SHj46x>ZhXGWMHJG1%Fe(JM1?T^=VtCUmJ)^1Y=dOtaWRbMBP#2j*s{J z;Y&$JUEIGAb@_uf7s{|mJ5)Ul<*zU>q-YsP&G(s+Ws$lmKmmZ^bdOW zDzQ*dm?nKUaQ*q1XtjH-d2HdmBHQ*h z%<$^&qbRZJ##n7laMAfOi3_RKhmd?ZTgSkRcy(ag`H&-t9;2lA5wkw=1M|crkV3bb3kx%YM#S)8!C0)st=!mtY$D1CGE}KK+MUFO z`euK6QZ3b>3@99SX0NHkApZHKn9u?5Au^+HH2!H*=~NF%E21S86u}d=jGX66{;(>^ zk9AAnR)5w#H81#6AmfA*yyQq=xF#btuOK-as4#)4@Eg@+yQIJJ-r?_x9+V|Xi6FjSn^C4=8S^(7vz^s~E~5c`e*FA&3be+v?sDH(C6Nl%QxrPDwl z>M7ohWc9@hqDmVT6;SQAvt?U2Erw)9N|01WJ-MDr537DC;GBOkQ+j^s`0Rs{xBCSQ z(FC$TtcXA$k{XljClc@*I-l;>f=D*~*E&((s+|Lg74^kNMa*Gv2A_9!Bd!KFHX{y(%^8AL8CReOxZQf6>n_gtSKs|9H5w@7DlTr zn&Fxeo+}8~;{oqW`V%C(uwSFk8df&*y9g< zt~2u7slHN87E+=X)`K|urlwY5iSmivSVdW%XAz-Sp!xjS;$$}){W>J66R`ttqs~V3 zo*7-Zo~M5$*Qqf;4V?+2Bh7(PAJPE4)$@#-g%@lY&(iCipum;t-g)k))u%1MJXD_t z(OO4^|H|W8uKRNnm0ukhqEAjH9?z3Z1^yf{Nv@5xlBKCd*~_-d9`pKG)|iN#tA3i~ z&1MgQ?beT|Gv{WW#pk(wuUao~FhflsKZ6^0^hb{e+=|vVR7N_0Xu9}`CYVs|(Coc? zY38x&cwdK<>zIwMbJ_SgK=m~xf!LsbH9ED^ia>gaQlss~Vx21PU@O^Syy%Ybb-FM1 zjv#yd^+!Je#;|=8IxhM+?}Fao;`--Kb5OnD9*N!AODv9l`eam`*WIdc?Bgku;gNs& zH`2sC%41Q{7frv{vL1To^VZ z8#}0(%5e+l(qlT*S=_f-wD2DLyr00k=C6yI`LHUSyCA)R-rMld!$H}ZRhXY?J8I%4 ziwJep@!q4Hg=$Q6mXHFaD!JgZp)CbdqN^?s^oyTuQxw}&MJxOGHz41_Pf z?7+71132L|QC5F=*qzP}XE>fXJM`QMU+&wwf5RY|eD)RRcc8J?yhJ1zi&UwgE=cub z_ax75za$m59?e~H>zKBSZ2i|0zoQ+*eet?S!c>g8B3k=DB zipy+vJhMku2!@>mjtPrif8gL`6WIq#JpvP<4=xCcSBH6;z>JJ z4f1OknteMQF8=_K+ut2RtmJN5W<70)vvpb|tf-}UB&ob}E;K(iyk6Hi5o!HBTxunf zn?rlo^f*5#&1m$NN_cX_LB%kI$Fix{=rr#iitT^)@!W~7xH|SEOsa^bY=*s|dNV(B z7Uftb+s`b@cCBYCGTeP-I{Ps<%?N5SW~>`(zY5+;W(Cc2D3qLS8%KyI_Q!Zz=*E`_95CdRb$p?kW~7Q#1IQl5O|1$VRuU2O1a&x^!UEqPmCUyQmQM z1Pi6q1uYg!glu3w*;5RC)k)i44e+@~>K5jT%LHUmm@XKZz2ZxT$$musQI7eZGp|Cc z{R!txcdQPMJI(2MpsRp2z*eB^F8|!{#yi=Zk2mLAXjwXc@@_1)4pjOySD%!P4B0g3 z9!>b7`x?~jpq~_wAgO+NbyJ_g#I~tZC`3EYP4a&%fc*DjAjQCkM8hs|NGBp)=K+2i z-297=<~7!r7RwneOb{^N=NQXd{g$m^9g{thW)r@V^+elOv^9)Jn;ec-Uh76+sk~s< zSr%a6*!z;;NH{R;n3}EYW?ySgBrg9dc_}WevN)q>Sd5LbIaV;}9pFiw35}HbIaS;; z^B4il68iMjkN>mbt$hP$yu&lvidtK5YLB|x{#MW;1T-k=>Ip<3U0W)@Q^nP!RU;J@ zZnQ8xFJSkoyDBrQ?&#)vtBFK>3vay-zeL-Yy)^2Vx8j}$gt(nO9ZRqAJ++W%6Iq@a zEsVk7cdhbLV`#1%2gb&159NF})l1dt6vil^X=8KY*4^)r5{`eDB%0h-65=gvMSYUE z@W3N-!UvLl{U#jgr-uO3_^D+TL`a_IVVsT8P0c)J+J848cX;~F7rQg|HWVYJ z;X+!VzP1ct%tumbBVNjyQ#9Pkem=fo@9p$~cO0^ z5l?~K=;3ZW@WkeZhh4~0p3xG27<%Gi9)S@IQ)4-}9jr zv?JvDew?UcRUhZT+EuGoRjpbz zdro*5*`Epmk||-ixyUvi;A{HXr^OW-<$Z-}lps@1C^uG{&!TATzS7SVR$q2kW^+6= zR!0e1 z>|bDPb>0h91LWp|9dZhyMQRJUI(~$l4MLHXAirv4eT^y7BebVy)*(=4IuR@^n)+^a zB&b;b?kweI**(Yuaj483XiO~viE%S_I)1v29awL*=+w?gm4WYSXb(xTIFX7tZctc_ z?IPtJ>zrtedw+N6ZNuAQWKCiwhW3K8!|;&a^__fqTdpCuM9ydJ13W?cNL}|jR@F?u z6H}aWznv|BvMfEtU=AfDlvYSZxXI-i4I<&AjJqITpfjd1j8-!}zThOuYicH^LLLf- zAq+Etg)*MJUP0B@euS<~iOlNBX)zhSwYfqyv?I}0F*1gn;e3fVO4p#Zo;N)pJpAIq zCvF3l-byWfH&W+DAp*OQE!WM$+@=jqH}u8t2sOdZaooHC(KU(yLCG?l>b~%x)nGlT3uI$EYAy_>dRf^)j-x*?YI3ZQ*w;9_XecZJccRsR-*B>`GlXFMhprMi zFJleZ3xwS}dRq%)2;i zgZ49@^LW`&SciqT=JYU8!IV)MWUrSKW1(z&Z>8_Syr{gj*n6-Oo}zb^;-BGq_bm6p zu}D5XFJJd&;x3_k!s6q&%Fthayo_X`@@XV5HHY8gib*JLPdiv3V zGx&{jTGw(LTL;0n(|6XiDQ>RW#o0{v_{I?6DE&QDHRDSHlu?wS?Kq*!zCd^C`deR9ZS* zPgAd76Xs*3GCX#$VFoM7wm+AG)>-W+)$rtnRdym$*^!Mq|AaDUO}0Khdkt6ah53T-!d_Q%ez zaA3uK%XZEt69-~*xO~5x)XKhE$soO*coyv58clVrJ3%kDYinc8+f^2Xm=wR^M)6pc) z3t-O*v1H-7`p2_l>~>W;v~ee{6qzCXg{<{KoxP3Rr5Lx)owq6qOMB4)(M~)*{%l9oaRY=6!_Njn2q>YmXo2;IIveQp-XB~%I!`!w?$5<`Bf=lt zGq}J7nk>DKn|t%xCTOntcBcuJ)iZKsnHc{#+YL2wTHvTRjEy-F2YX;*C-8^NI2Y+N zM$6g}*n3k7$`BR}3i6P6pw`5VuahAi)G3{u9dmq2X0}MjrMaIZ48FwTI4}PtdgbGm1IE!RZ zDRAK58Z4vQvg;! zbZMu{cFUjbDR=BmoBQeoM(~NJGIoV8vz5BHY9rS`5G%m!<4NLt_P7-y^K-|q{y5T- z<-`Qdnj0rH{}qv~?hVmNa`J_+3XYw9Win(1O~z&%*;`Nh>ya_CW_b<8P*H1VIxYyw z3~Nn-U=^9^7^SxNbs3W66vo}Fn7V71er4|Nl7D@U6|Jt2egDMmK8m)+Kso<+FeK2A%$a3pLas&rd)kfe>@t2X{J7H!joA z*V|Couxz|4?{NVsT>3~)TvIF^YBw71*)!DD#8hh8) zwt5=k=O11z@a&wFzRHXXc@-%M+~QG(ViunZ!H+BrbS6L-?2_K2N2r?NMcA!xlQEX4?Gr4N>lx zTee9|a(*_obQ4FsRX0^#e>`medN%@iy_NI}u$hV#m^sU&-BF+k`LJ z!@1L4$rW>e|LPzA<+m*8-E=I*Xu-v)#<}?t1wJN_pNtwL(wfgGV*Y#H<|rcS+2|_5)LQ2?RBBek|<;bbUo;pQjsI2n7SkM z&JAhl($taN^MXqj!xN3#gtx0wy%!bdd&D7yJ@4_(iGE*yBSD8Wjv59&YZf!Fmr0fT zp7w_ei@?TqY3*z5*m;3t&O#(~lRo|-R1a>%4PFS zuju)b;h59$rt9sQx4^$J)Ct83U zhdz>(or8<-$Ev{z{sjFx(R-oM(MS$t1k~OXKo2wk=@t}a!{?f?IfNX^aaA# z0`Md|GBi23C4`4)CNjoFZH}GM4f^j{%zuHTUVbHF3v#G%7-lZpZmhj)LhsK;9{*nP z;AGWRDu_fz8ditjSq03rJkL>DJ6R}yI4BavUnn6AozKb6HK_mDke3`Vp313Ol$ea3 z8;4FPAy?iqS5w>Fu4i)YHV|J&%=|=3op7ia=^O&w4>Wwv@+y11k=sD%h0f z;kg4g^y4E@MOg4|(>j61QLwYa=}xR&N2`q2B3 z$(bJbcRa~nU6YOe3Apb=mj$YezOz#6hWhYJ%~q+GCyg6l-`phE^ro`EsM2|??QQ^$ zXy(F{fA}|7fk@4Ufl-NwFfQxWxpbSd_>#uGmaR&VIZ|NZnd@EHZatx^zjMM|k5f2j zaBygypg5`&wy~9m>>bKyxZCj_S7En3*!qj-u>W_9=(>8(1YoZj7O9-S*A;cq04b^E1+F3#`?#`Xa{;^!kQ{Oy&_GQ-zZq{|O~Ov)7n@RMSR} z1z}`XvVDRLxs3i1G<>4FUcl2)A0giuXDK|VT3|p_@?@$1Drc)Z&ZMUu&IPv#LuK9h zFXl2=XBeG+$jh$Pg=X$4UvFz>8|hcybZlLN>s&!!*o26W+p)M;yO@*H^BrM#h*evU z2dK#{Y{>p%VCw9pBHc&5UyOYj$#EqLO;ov>Q5)Rp>>!CaFft|Wx9u7LZNVJj%aD|TN3zZtJS)5fsUnPzOlp$bvoaBQvGs%NySUZ~~7 z?uZkv{zgLjrP{=TWH<#kvbBM@Zv(QE%<7O}Ka%kRDV)5$JK7GX;8`!=*}P8#kA!O_ z2;!l0tjxo|WEDfn`+Fp7)P=TtD5=ISSY^ik8ty?#sk*056CQGtR zc1Y?x^7ovA(gszMwRN!<*B9$-K)OIi&Y_BNB%2Qu&vLA1{!Tc!?`!jU>e1n%lyS@< zZ_a<>nH!c>G~_Kc+$VhDP2C_Le8y_c#893zjhxa!NbF= zs;(APQi6kc`k7V4m8?hlPOJ!bPj-4Lh2Gg`+S8pIyOGGi;kxx0U0&)^KK8|WT`+cg za&7HIYnF%cZIv#;rzuRqsJ7;m`SfuQj$0O*i9OqCjk=K#1VRUbpEo5hP^bFN^lt~X!^3!pU`GVBdad&O)>7(&ev?Vu|u%aQ;eE}EC`E*bV+kBCaFMs4Y_GCz% zh-d2!oX5L(q4tSj+hY#B4-bHVTfA{L=Lx77RmQF2Y}J59(zy%feW#2 znfhC)I_aQ$-35-&emCJh!+8seUPw30N%sdF+Z5vxiv_^k*xmmuv`Zw)>UzAs0c*m9ry7(ua>0)Gg62y3-B z-v(KbnZ&aCN|PQ*EziS_g1O|%D+QYB%u%=bQ2R3dwctm z_DCcE22-!N;%KK!(7ca?ld&rF-Ylk1SMyM^AtL`*h!CLVMNcY zZffyfcuZ1}kbye5)eiZ!+_9q?{@#mPs_wrn`W*@_2z;Ra98km-@BYx<&;GD>=pJ&? zz3532{wu0&dj3E8i{CeH7m&ez{mk2W~VO&pF(sk&KH?*7yY!i&ZIdP-(1 ze_A*Ii5(WYtWQY+Eq`VM(TZj({Ld<^(p0b*(|j>9{3al0mCugf{PcIs_n((hkQxkb ztAEucEy+KHFTA~Yjx77}=i?!`C5{nIr%lxX$~i40RVecX6fh9}ph`;6S9LYS%GNyp zbOW%pwadt*NZV#2zh@Kw0Hy7c-vyVmkg6!fuY(sZ z{N0nRF}SgR))DG+Mf{eiiom}*4oPTr71+E#3jua1`_JF5|1U?{4g$XS-jU)J%@q!cg{0-PYOMSGF2YTB@U=hM^&!dJfbkyPpDBU0Y>)r3E;=Pn2 zauYsBEp*xOP3FMeY^gkstXWD>f!P6o4j8iiar-JBnn<@ULmFWA_eV3*JgHxNQA3Sh z{JeSl;mOR>znk^0F)~piuXOi{`RP9;n^VH)i1$*}E{z@0#78n#;C)lFu|4~<+k{@@ zgm}tt5r%R*N}paQ6_w)mKt<{)f%4%@=%R{hN2ioPa>9Q7EyJLmmlsW}FytEVP z1KPiy4*fuC&p#OB6W5X#Ua ztyqq?=CX*!Ui>|)JR>R<1=T8K*8^4729_f$!u~8%G4-R;U$etI2Jn8U=s0N4c{$TP z&(v?Gq<@-r+n6Zy`=iFG)DMLd^Kbv2otl=0hMFEVhM0U+WR|7>bi}@K(f{*=`Df8U z`&m=`o(li07jQ&W*3~Hz-CvM0eg^gB;^V6`UBW>`oW#{+{yakN_b<FMvk!2Z3d$A_}YxDpR{8Y^eVzDT9>bN&BygwbnoI3U zg=vEcJpHaG3B+E@YLI{LaJiUy+C1Ixfm@gu+mgvJdUoeNcYOiMKg-g;*Jq4R6L##5 z-g$5)r-d7zd=J*)Ch#^RNnfSI^IZ4Km-ISeufat;m`{A(QR&S&v#9KhFQO;A?m4bi zmGCi~4ZF&eDQI*6Cm%*XBDqi-EMTrb;wt0qU@eoXWVbj)=vd2;w$0QqRV&?Qo1b;5 z+6+WoDz4iJRWmqpKLUTPymd+AXYEEm=l6X&b?(rtOW=*<1gch0Cl3%l-^~w2#-?JcB*1KuHu_8U zO!0%cS~V>T2#hN1bhU7!dZ`IBGI=LRb5_&um2sjSR`{NpBLu0^&W>ny&ETrh z>7TmQXFrsl$oYVEcoFQFU6@V`h}BrV5p3UDl&gS>`Mib2BjXqOtc(+=9_5|uOqRqS z!NHu!jH;uPL#rQ~NvDmH%kWFjaYj~T0QTpy=Qix#=Rj1N09l1KSG|Z&pFfy49txd+ ztk|EIEF8V(av-l36W@8S4pi7+Z_1eal&jQcuynOR-fdQ5@f`(vwg_7E7Wk? z%_jJ`tYc=8-iErup}UYQ4H}=;3gOej&(dP0eg0o803K}-S=t8at)_FC>ckO7+~aOL z8An!(_Z%pqN+mA47r?zfH(_j@mh$5rRChT<+a1{9k=UZ=NZh$qTlh*RS%$*jbL1;z z8?Oh%$%5Bp?A``2NUTy{85sC-GX&C^^}XNBM*%+Y(Iu`+e9jdD(={*odufit zu`Sol14c|9WtN7WW0f$YyFl_44sWAxw9hcHIQ7!{1iVIWvdb@{>=PuU{fV*!wp~$^ z8~G;e@SH3zSA7LAKEHGyUEJ0g#qU9m-GCZo;q=qTk4&uK3w$z+fZ|_2`G1wjwqAVA zP$C*C-3V1?-E5+F<~^?5JX}b#_8#v>5jfuG?c38fs4qMC`n5)LNBW(*j3k{mSUIrq zQ+;=N1mQ)GTs|}}?!r&Mh0d+lVo<710EjvYYbRXiXl3;5dFOj-2AKPTIHs69zH>_ggfi<@+TVf1Hoxxe;??Wzj?3-&Oq9)n)u%cU)%XT(G&^6t#`|9W zPrlOzgE~%6ofSG_`op!Q6}#d6;UG}fiy;$QtvXK0LYgvvrK}fJ3NuP{zYXvELqRg7 zuh~&xx6iS_@74Gz5VKX(S1pXZk70AGn1EQ$#fr4SNiU{a7BM3GXN-1?dkoi@jjd_5 z(R=o2g&Xv~NIago@|d8`R%OLKy`-)JxP3u0^(t#Nr$vj@d4Obb8Ajxj^!h>7lq{LN zSQj(Xy1c0yCOq9e3TsIm^^Cy@-jnwR!@Jm$EQc(>6NY1n9|l+9usHz3yW_}HKF^eZ zh!0f`y^VI<5b&MB9o^CtrhSze1I(k>%~s0*+l`u7r&gI5dDCE(IJ#B9qrc-#D}%a~ z6mMTN^{*N`88m1$WlK$-;uWCe!XR9JbLj`AV*V@>v8$B@{wlKz#t9N5xjc?-xkhNM z;C0}R`IQRlsv=us#^vg%)nd_&Dry&xx}_s<;bK|-RiZzTZP z+xx1+i;G!Z;bUC3A@f(FvzJowfG! zHm7_zGVU>9ajl4vt?R=@!OPqE<_o_J!-|;8j5Oi zM6I1~DfOuc_CT7$mULL{yAk}mEuM_9<6WlMu`JtRw^c_#z8f<6LWaF0MV(kpiyqBMP)!nqAbV}6spB7YX0nLZ3 zI4K3W97_g1W~nMy&mxltw>CQ0N9^1jG3MRFo#_4-CE-@!UPa3*2C#=M%I~lx-{j70 zVJ&4goONnB`H*1+iw;ieQj>VV3Lw^8ZBM-%FGutq3T>{Jbdq=RGrA5*+3N5(Qz2^) zt~?bH2jtClKWWA>Xx(6QE>@D2hYyw-~ezgO@ zI|-zSh$p|N`*;NLzhUuuMw;4LxLuXAxZjN*L=z$+WtML(S3weI;7GTsLkN)HTq1fl z?5f;n4EG(&rcMA-C*smkp4u$AmE}c6$_f{522xKr9HjLIc2cYFu)D(P5lP(M<6e=U z@$?}14Mbc~#xvf1q3@iprUy?#LPkF|P6?bT_qy7zf_y)9s-fcbPEX4{d32qg{6eyC zpp<@T=75Ph;8YkDE7i9^SZg^;Ibn2Q;8Y@0IXH0r)njto!)i;dfli}`T_Y+ssZigX zBPqrUMYUraiZTIf;Jm&!e?@g2$9glo#wz>3?s_4gM!Pon5D%Waa^RNWo#LV4MyP-6 z+$D2Y1ps-)e~qsplNW2F5X|NhX?$8PxYtInJolbhXSBKxvr?F|^Dx2FHG-H>ePxOR zo)TuUM#sbT^m%#yXR-BP3*dHv2~%@55DtmORYc!weWsE>Rq}tuNB&*|wWVPoK2ZYG zi2l6m-7w6;5;E5Dj|N=yV%o0l-Sbgqjq#qjz5BN5q{9H$`butI3&hlieSOU3cxi1T zR2+Dz>cR0wK5290m&sjSl|n{6zX{-a(K6BRuN(IN4}fHQi%~CC-Vs(B542^D?z%&m z<9u9D`SFf12%6Y;$^LxG38_PuNu*Z;;-#m9Z}QHx#`~3ac}*W@J}rt1$_gKs%$NFX zt@lfY+va|2w1%_95q=y1v($Me5Z!QbD~Ll4OBiZtc2FZQ>t?OOwi1rmEg(T@_d&b@ zBjoAKq<%2^_SMZ1onA&~^oR&-!s&Q6FF{;dpGFQlXuI25OH3vG3IRrxl4mH{Qo}Qa zt*`>t*H7I^;>0@3d8KV{bD`TE2Y)1Y6m6Z|aY2$*c%91M)X z8+A=ic3C*^OmN6=C^GjsrnF3Xfq`|-17_t=7poYh_Z{_fQ79Zs&K>!Pn+p+Y*WH>vvk7!}yYk6V#i+KsHxCuGKT$$Y4e>!p|pyU+bb>hA$pwab6L9YC9e|t^2+c$wvlZh^I zZo%wElILpLVs4aRmJoK%ucVZJqCyV*NhULNJ5 z_Cop+3;qm}hNJzxcQ-C|ZS&6F(`}{2-eZm0n7a$_do9H5dG%EvzDEpdh9Nc3FW`Ap_@iB^9Wi4{tv5R$w@eQ-HETp{TO5cwW?^?LaXx9_<(n�m>pd-I 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 z‡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 XWelkCI Date: Fri, 24 Feb 2023 17:03:27 +0100 Subject: [PATCH 030/105] demo seeds and bnn file in cp850 format --- db/seeds/demo-seeds.rb | 2 +- demo_day_nks.bnn | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/db/seeds/demo-seeds.rb b/db/seeds/demo-seeds.rb index efdb7342..31bcdf98 100644 --- a/db/seeds/demo-seeds.rb +++ b/db/seeds/demo-seeds.rb @@ -122,7 +122,7 @@ tomatoes = Article.create!( manufacturer: "Terra di Puglia", origin: "IT", price: 2.89, tax: 7.0, unit: "500g", unit_quantity: 20, - note: "pomodori italianio, demeter", + note: "pomodori italiani, demeter", availability: true, order_number: "7") rice = Article.create!( diff --git a/demo_day_nks.bnn b/demo_day_nks.bnn index 0796c486..9bb9c733 100644 --- a/demo_day_nks.bnn +++ b/demo_day_nks.bnn @@ -1,7 +1,7 @@ BNN;3;0;Naturkost Nord, Hamburg;T;Angebot Nr. 0922;EUR;20220905;20221001;20220825;837;1 -5;;;;4280001958081;4280001958203;pfel Elstar;erntefrisch und knackig;;;obb;;D;C%;DE-KO-001;120;0301;10;55;;1;10 x1Kg;10;1Kg;1;N;;;;1,41;;;;1;;;4,49;2,89;J;;2;3;;;;;;;;;;;;;;;;;;;A;;;;;Kg;1;; -6;;;;4280001958081;4280001958203;Brokkoli;aus der Erde;;;VIB;;IT;C%;DE-KO-001;120;03;10;55;;1;4 x400g;4;400g;1;N;;;;1,41;;;;1;;;4,49;3,20;J;;2;3;;;;;;;;;;;;;;;;;;;A;;;;;Kg;2,5;; -7;;;;4280001958081;4280001958203;Tomaten;Datteltomaten, demeter;;;TDP;;IT;C%;DE-KO-001;120;03;10;55;;1;20 x500g;20;500g;1;N;;;;1,41;;;;1;;;4,49;3,00;J;;2;3;;;;;;;;;;;;;;;;;;;A;;;;;Kg;2;; -8;;;;4280001958081;4280001958203;Reis;Reispfannen geeignet;;;FIN;;D;C%;DE-KO-001;120;05;10;55;;1;12 x300g;12;300g;1;N;;;;1,41;;;;1;;;4,49;3,00;J;;2;3;;;;;;;;;;;;;;;;;;;A;;;;;Kg;3,333333;; -9;;;;4280001958081;4280001958203;Spaghetti;Vollkorn;;;ZLN;;D;C%;DE-KO-001;120;06;10;55;;1;4 x500g;4;500g;1;N;;;;1,41;;;;1;;;4,49;3,00;J;;2;3;;;;;;;;;;;;;;;;;;;A;;;;;Kg;2;; +5;;;;4280001958081;4280001958203;pfel Elstar;erntefrisch und knackig;;;obb;;D;C%;DE-KO-001;120;0301;10;55;;1;10 x1kg;10;1kg;1;N;;;;1,41;;;;1;;;4,49;2,89;J;;2;3;;;;;;;;;;;;;;;;;;;A;;;;;Kg;1;; +6;;;;4280001958081;4280001958203;Brokkoli;gesund und lecker;;;fig;;IT;C%;DE-KO-001;120;03;10;55;;1;6 x400g;6;400g;1;N;;;;1,41;;;;1;;;4,49;2,99;J;;2;3;;;;;;;;;;;;;;;;;;;A;;;;;Kg;2,5;; +7;;;;4280001958081;4280001958203;Tomaten;pomodori italiani, demeter;;;TDP;;IT;C%;DE-KO-001;120;03;10;55;;1;20 x500g;20;500g;1;N;;;;1,41;;;;1;;;4,49;3,19;J;;2;3;;;;;;;;;;;;;;;;;;;A;;;;;Kg;2;; +8;;;;4280001958081;4280001958203;Reis;Reis im Vorratssack, demeter;;;FIN;;D;C%;DE-KO-001;120;05;10;55;;1;12 x3k;12;3kg;1;N;;;;1,41;;;;1;;;4,49;3,49;J;;2;3;;;;;;;;;;;;;;;;;;;A;;;;;Kg;0,3;; +9;;;;4280001958081;4280001958203;Spaghetti;100% italienisches Hartweizengrie;;;ZLN;;D;C%;DE-KO-001;120;06;10;55;;1;4 x500g;4;500g;1;N;;;;1,41;;;;1;;;4,49;2,99;J;;2;3;;;;;;;;;;;;;;;;;;;A;;;;;Kg;2;; 10;;;;4280001958081;4280001958203;Kartoffeln;vorwiegend festkochend;;;rsh;;D;C%;DE-KO-001;120;0311;10;55;;1;6 x5Kg;6;5Kg;1;N;;;;1,41;;;;1;;;4,49;3,00;J;;2;3;;;;;;;;;;;;;;;;;;;A;;;;;Kg;0.2;; \ No newline at end of file From 2614f095cb768340904bc2f05eca00ba736ae313 Mon Sep 17 00:00:00 2001 From: Philipp Rothmann Date: Fri, 24 Feb 2023 17:46:34 +0100 Subject: [PATCH 031/105] update drone --- .drone.yml | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/.drone.yml b/.drone.yml index 44065eaa..19602902 100644 --- a/.drone.yml +++ b/.drone.yml @@ -79,7 +79,7 @@ steps: - name: deployment image: git.local-it.org/philipp/stack-ssh-deply:latest settings: - stack: "foodsoft_${DRONE_COMMIT:0:8}" + stack: "foodsoft_${DRONE_BRANCH}" compose: "deployment/compose.yml" deploy_key: from_secret: drone_deploy_key @@ -96,23 +96,23 @@ steps: - proxy environment: IMAGE: git.local-it.org/foodsoft/foodsoft:${DRONE_COMMIT:0:8} - STACK_NAME: "foodsoft_${DRONE_COMMIT:0:8}" - DOMAIN: "${DRONE_COMMIT:0:8}.foodsoft.dev.local-it.cloud" + STACK_NAME: "foodsoft_${DRONE_BRANCH}" + DOMAIN: "foodsoft.dev.local-it.cloud" LETS_ENCRYPT_ENV: production FOODCOOP_MULTI_INSTALL: true - FOODCOOP_NAME: example - FOODCOOP_CITY: XXX - FOODCOOP_COUNTRY: XXX - FOODCOOP_EMAIL: info@example.org - FOODCOOP_PHONE: XXX - FOODCOOP_STREET: XXX - FOODCOOP_ZIP_CODE: XXX - FOODCOOP_HOMEPAGE: https://order.example.org - FOODCOOP_HELP_URL: https://order.example.org + FOODCOOP_NAME: Einkaufskooperative Foobar + FOODCOOP_CITY: Berlin + FOODCOOP_COUNTRY: Deutschland + FOODCOOP_EMAIL: foodsoft@local-it.org + FOODCOOP_PHONE: 123456789 + FOODCOOP_STREET: Einkaufsstraße 5 + FOODCOOP_ZIP_CODE: 12345 + FOODCOOP_HOMEPAGE: https://foodsoft.local-it.org + FOODCOOP_HELP_URL: https://git.local-it.org/foodsoft/foodsoft FOODCOOP_TIME_ZONE: Berlin FOODCOOP_USE_NICK: true FOODCOOP_LANGUAGE: de - FOODCOOP_FOOTER: 'example hosted by Your Tech Co-op.' + FOODCOOP_FOOTER: 'Foodsoft hosted by local-it e,V,.' USE_APPLE_POINTS: false STOP_ORDERING_UNDER: 75 MINIMUM_BALANCE: 0 @@ -120,15 +120,15 @@ steps: MYSQL_HOST: db MYSQL_PORT: 3306 MYSQL_USER: foodsoft - EMAIL_SENDER: noreply@example.org - EMAIL_ERROR: systems@example.org - SMTP_ADDRESS: mail.example.com - SMTP_AUTHENTICATION: plain - SMTP_DOMAIN: mail.example.com + EMAIL_SENDER: demo@local-it.org + EMAIL_ERROR: flip@yksflip.de + SMTP_ADDRESS: mail.local-it.org + SMTP_AUTHENTICATION: login + SMTP_DOMAIN: mail.local-it.org SMTP_ENABLE_STARTTLS_AUTO: true SMTP_PORT: 587 - SMTP_USER_NAME: foodsoft - EMAIL_REPLY_DOMAIN: example.org + SMTP_USER_NAME: demo@local-it.org + EMAIL_REPLY_DOMAIN: SMTP_SERVER_HOST: 0.0.0.0 SMTP_SERVER_PORT: 2525 SECRET_DB_PASSWORD_VERSION: v1 From eb719057c4886520a320b8f6a3ff97dcabb64b68 Mon Sep 17 00:00:00 2001 From: Philipp Rothmann Date: Fri, 24 Feb 2023 18:28:42 +0100 Subject: [PATCH 032/105] use demo seeds by default --- db/seeds.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db/seeds.rb b/db/seeds.rb index 37a996ff..eb1f356e 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -1,4 +1,4 @@ # default seed is minimal -require Rails.root.join('db/seeds/minimal.seeds.rb') +require Rails.root.join('db/seeds/demo-seeds.rb') # to generate new seeds, use the seed_dumper gem From ce7b4d7ce4573d0dbeefbaacbb91de2b2e4fb448 Mon Sep 17 00:00:00 2001 From: FGU Date: Fri, 24 Feb 2023 18:12:45 +0100 Subject: [PATCH 033/105] feat: add price per base unit --- app/helpers/group_orders_helper.rb | 8 ++++ app/lib/quantity_unit.rb | 59 ++++++++++++++++++++++++++++++ config/locales/de.yml | 1 + config/locales/en.yml | 1 + config/locales/es.yml | 1 + config/locales/fr.yml | 1 + config/locales/nl.yml | 1 + spec/lib/quantity_unit_spec.rb | 22 +++++++++++ 8 files changed, 94 insertions(+) create mode 100644 app/lib/quantity_unit.rb create mode 100644 spec/lib/quantity_unit_spec.rb diff --git a/app/helpers/group_orders_helper.rb b/app/helpers/group_orders_helper.rb index c5e27c66..4f1d352f 100644 --- a/app/helpers/group_orders_helper.rb +++ b/app/helpers/group_orders_helper.rb @@ -53,4 +53,12 @@ module GroupOrdersHelper return 'missing-many' end end + + def price_per_base_unit(article:, price:) + quantity_unit = QuantityUnit.parse(article.unit) + return nil unless quantity_unit.present? + + scaled_price, base_unit = quantity_unit.scale_price_to_base_unit(price) + "#{number_to_currency(scaled_price)}/#{base_unit}" + end end diff --git a/app/lib/quantity_unit.rb b/app/lib/quantity_unit.rb new file mode 100644 index 00000000..0a910f87 --- /dev/null +++ b/app/lib/quantity_unit.rb @@ -0,0 +1,59 @@ +class QuantityUnit + def initialize(quantity, unit) + @quantity = quantity + @unit = unit + end + + def self.parse(number_with_unit) + # remove whitespace + number_with_unit = number_with_unit.gsub(/\s+/, '') + # to lowercase + number_with_unit = number_with_unit.downcase + # remove numerical part + number = number_with_unit.gsub(/[^0-9.,]/, '') + # remove unit part + unit = number_with_unit.gsub(/[^a-zA-Z]/, '') + # convert comma to dot + number = number.gsub(',', '.') + # convert to float + number = number.to_f + + return nil unless unit.in?(%w[g kg l ml]) + + QuantityUnit.new(number, unit) + end + + def scale_price_to_base_unit(price) + return nil unless price.is_a?(Numeric) + + factor = if @unit == 'kg' || @unit == 'l' + 1 + elsif @unit == 'g' || @unit == 'ml' + 1000 + end + + scaled_price = price / @quantity * factor + scaled_price.round(2) + + base_unit = if @unit == 'kg' || @unit == 'g' + 'kg' + elsif @unit == 'l' || @unit == 'ml' + 'L' + end + + [scaled_price, base_unit] + end + + + def to_s + "#{@quantity} #{@unit}" + end + + def quantity + @quantity + end + + def unit + @unit + end +end \ No newline at end of file diff --git a/config/locales/de.yml b/config/locales/de.yml index 89a69005..d6254d84 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -1067,6 +1067,7 @@ de: action_save: Bestellung speichern new_funds: Neuer Kontostand price: Preis + price_per_base_unit: Grundpreis reset_article_search: Suche zurücksetzen search_article: Artikel suchen... sum_amount: Gesamtbestellmenge bisher diff --git a/config/locales/en.yml b/config/locales/en.yml index cb7a54c1..ea65f309 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1069,6 +1069,7 @@ en: action_save: Save order new_funds: New account balance price: Price + price_per_base_unit: Base price reset_article_search: Reset search search_article: Search for articles... sum_amount: Current amount diff --git a/config/locales/es.yml b/config/locales/es.yml index 4004b5c5..1f594ead 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -930,6 +930,7 @@ es: action_save: Guardar pedido new_funds: Nuevo balance de cuenta price: Precio + price_per_base_unit: Precio de base reset_article_search: Reinicia la búsqueda search_article: Busca artículos... sum_amount: Cantidad actual diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 304a7b9d..04dc03fc 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -678,6 +678,7 @@ fr: action_save: Enregistrer ta commande new_funds: Nouveau solde price: Prix + price_per_base_unit: Prix de base reset_article_search: Réinitialiser la recherche search_article: Rechercher des produits... sum_amount: Quantité déjà commandée diff --git a/config/locales/nl.yml b/config/locales/nl.yml index f441d15d..dd41b666 100644 --- a/config/locales/nl.yml +++ b/config/locales/nl.yml @@ -1026,6 +1026,7 @@ nl: action_save: Bestelling opslaan new_funds: Nieuw tegoed price: Prijs + price_per_base_unit: Basisprjis reset_article_search: Alles tonen search_article: Artikelen zoeken... sum_amount: Huidig totaalbedrag diff --git a/spec/lib/quantity_unit_spec.rb b/spec/lib/quantity_unit_spec.rb new file mode 100644 index 00000000..bbe3d546 --- /dev/null +++ b/spec/lib/quantity_unit_spec.rb @@ -0,0 +1,22 @@ +require_relative '../spec_helper' + +describe QuantityUnit do + it "parses a string correctly" do + qu = QuantityUnit.parse("1.5 k g"); expect([qu.quantity, qu.unit]).to eq([1.5, "kg"]) + qu = QuantityUnit.parse(" 1,5 kg"); expect([qu.quantity, qu.unit]).to eq([1.5, "kg"]) + qu = QuantityUnit.parse("1500 g"); expect([qu.quantity, qu.unit]).to eq([1500, "g"]) + qu = QuantityUnit.parse("1.5L "); expect([qu.quantity, qu.unit]).to eq([1.5, "l"]) + qu = QuantityUnit.parse("2400mL"); expect([qu.quantity, qu.unit]).to eq([2400, "ml"]) + end + + it "scales prices correctly" do + qu = QuantityUnit.new(1.5, "kg") + expect(qu.scale_price_to_base_unit(12.34)).to eq([8.23, "kg"]) + qu = QuantityUnit.new(1500, "g") + expect(qu.scale_price_to_base_unit(12.34)).to eq([8.23, "kg"]) + qu = QuantityUnit.new(1.5, "l") + expect(qu.scale_price_to_base_unit(12.34)).to eq([8.23, "L"]) + qu = QuantityUnit.new(2400, "ml") + expect(qu.scale_price_to_base_unit(12.34)).to eq([5.14, "L"]) + end +end \ No newline at end of file From debce2a635653d9dfbba52651d52199943692dd1 Mon Sep 17 00:00:00 2001 From: decentral1se <1991377+decentral1se@users.noreply.github.com> Date: Sun, 5 Mar 2023 14:07:49 +0100 Subject: [PATCH 034/105] docs: roadmap & call (#984) Co-authored-by: decentral1se --- README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a1a9de24..3f253f86 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ Foodsoft ========= + [![Build Status](https://github.com/foodcoops/foodsoft/workflows/Ruby/badge.svg)](https://github.com/foodcoops/foodsoft/actions) [![Coverage Status](https://coveralls.io/repos/foodcoops/foodsoft/badge.svg?branch=master)](https://coveralls.io/r/foodcoops/foodsoft?branch=master) [![Docs Status](https://inch-ci.org/github/foodcoops/foodsoft.svg?branch=master)](http://inch-ci.org/github/foodcoops/foodsoft) @@ -15,10 +16,16 @@ If you're a food coop considering to use foodsoft, please have a look at the [wi More information about using this software and contributing can be found on the [wiki](https://github.com/foodcoops/foodsoft/wiki). +Roadmap +------- + +If you'd like to see what is currently bring prioritised for development, check [our roadmap](https://github.com/orgs/foodcoops/projects/1). If you'd like to influence the roadmap, please join our [monthly community call](https://forum.foodcoops.net/t/foodsoft-monthly-community-call/573/6). As of March 2023, Foodsoft has limited development capacity but we are trying to build this up once more. For now, we try to prioritise what we work on, in order to focus our efforts. If your proposed changes are waiting for some time without review, please join the community call to discuss. Developing ---------- +> Foodsoft development needs your help! If you want to hack/triage/organise to improve the software, please consider joining our monthly community calls which are announced on [this forum thread](https://forum.foodcoops.net/t/foodsoft-monthly-community-call/573/6). In these calls, we check in with each other, discuss what to prioritise and try to make progress with development and community issues together. + Get foodsoft [running locally](doc/SETUP_DEVELOPMENT.md), then visit our [Developing Guidelines](https://github.com/foodcoops/foodsoft/wiki/Developing-Guidelines) page on the wiki. @@ -35,7 +42,6 @@ Deploying Setup foodsoft to [run in production](doc/SETUP_PRODUCTION.md), or join an existing [hosting platform](https://foodcoops.net/foodsoft-hosting/). - License ------- From 503ed6c3790670d21ad309af1a443a8d46a8a1cd Mon Sep 17 00:00:00 2001 From: Philipp Rothmann <16109235+yksflip@users.noreply.github.com> Date: Sat, 25 Mar 2023 18:20:13 +0100 Subject: [PATCH 035/105] Add home controller test (#972) Co-authored-by: viehlieb Co-authored-by: Tobias Kneuker --- app/controllers/home_controller.rb | 2 +- spec/controllers/home_controller_spec.rb | 162 +++++++++++++++++++++++ spec/spec_helper.rb | 2 +- spec/support/spec_test_helper.rb | 23 ++++ 4 files changed, 187 insertions(+), 2 deletions(-) create mode 100644 spec/controllers/home_controller_spec.rb create mode 100644 spec/support/spec_test_helper.rb diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb index 86f9e2eb..a3d9cd53 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -64,7 +64,7 @@ class HomeController < ApplicationController # cancel personal memberships direct from the myProfile-page def cancel_membership if params[:membership_id] - membership = @current_user.memberships.find!(params[:membership_id]) + membership = @current_user.memberships.find(params[:membership_id]) else membership = @current_user.memberships.find_by_group_id!(params[:group_id]) end diff --git a/spec/controllers/home_controller_spec.rb b/spec/controllers/home_controller_spec.rb new file mode 100644 index 00000000..c5732bd9 --- /dev/null +++ b/spec/controllers/home_controller_spec.rb @@ -0,0 +1,162 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe HomeController, type: :controller do + let(:user) { create :user } + + describe 'GET index' do + describe 'NOT logged in' do + it 'redirects' do + get_with_defaults :profile + expect(response).to have_http_status(:redirect) + expect(response).to redirect_to(login_path) + end + end + + describe 'logged in' do + before { login user } + + it 'succeeds' do + get_with_defaults :index + expect(response).to have_http_status(:success) + end + end + end + + describe 'GET profile' do + before { login user } + + it 'succeeds' do + get_with_defaults :profile + expect(response).to have_http_status(:success) + end + end + + describe 'GET reference_calculator' do + describe 'with simple user' do + before { login user } + + it 'redirects to home' do + get_with_defaults :reference_calculator + expect(response).to have_http_status(:redirect) + expect(response).to redirect_to(root_path) + end + end + + describe 'with ordergroup user' do + let(:og_user) { create :user, :ordergroup } + + before { login og_user } + + it 'succeeds' do + get_with_defaults :reference_calculator + expect(response).to have_http_status(:success) + end + end + end + + describe 'GET update_profile' do + describe 'with simple user' do + let(:unchanged_attributes) { user.attributes.slice('first_name', 'last_name', 'email') } + let(:changed_attributes) { attributes_for :user } + let(:invalid_attributes) { { email: 'e.mail.com' } } + + before { login user } + + it 'stays on profile after update with invalid attributes' do + get_with_defaults :update_profile, params: { user: invalid_attributes } + expect(response).to have_http_status(:success) + end + + it 'redirects to profile after update with unchanged attributes' do + get_with_defaults :update_profile, params: { user: unchanged_attributes } + expect(response).to have_http_status(:redirect) + expect(response).to redirect_to(my_profile_path) + end + + it 'redirects to profile after update' do + patch :update_profile, params: { foodcoop: FoodsoftConfig[:default_scope], user: changed_attributes } + expect(response).to have_http_status(:redirect) + expect(response).to redirect_to(my_profile_path) + expect(flash[:notice]).to match(/#{I18n.t('home.changes_saved')}/) + expect(user.reload.attributes.slice(:first_name, :last_name, :email)).to eq(changed_attributes.slice('first_name', 'last_name', 'email')) + end + end + + describe 'with ordergroup user' do + let(:og_user) { create :user, :ordergroup } + let(:unchanged_attributes) { og_user.attributes.slice('first_name', 'last_name', 'email') } + let(:changed_attributes) { unchanged_attributes.merge({ ordergroup: { contact_address: 'new Adress 7' } }) } + + before { login og_user } + + it 'redirects to home after update' do + get_with_defaults :update_profile, params: { user: changed_attributes } + expect(response).to have_http_status(:redirect) + expect(response).to redirect_to(my_profile_path) + expect(og_user.reload.ordergroup.contact_address).to eq('new Adress 7') + end + end + end + + describe 'GET ordergroup' do + describe 'with simple user' do + before { login user } + + it 'redirects to home' do + get_with_defaults :ordergroup + expect(response).to have_http_status(:redirect) + expect(response).to redirect_to(root_path) + end + end + + describe 'with ordergroup user' do + let(:og_user) { create :user, :ordergroup } + + before { login og_user } + + it 'succeeds' do + get_with_defaults :ordergroup + expect(response).to have_http_status(:success) + end + end + end + + describe 'GET cancel_membership' do + describe 'with simple user without group' do + before { login user } + + it 'fails' do + expect do + get_with_defaults :cancel_membership + end.to raise_error(ActiveRecord::RecordNotFound) + expect do + get_with_defaults :cancel_membership, params: { membership_id: 424242 } + end.to raise_error(ActiveRecord::RecordNotFound) + end + end + + describe 'with ordergroup user' do + let(:fin_user) { create :user, :role_finance } + + before { login fin_user } + + it 'removes user from group' do + membership = fin_user.memberships.first + get_with_defaults :cancel_membership, params: { group_id: fin_user.groups.first.id } + expect(response).to have_http_status(:redirect) + expect(response).to redirect_to(my_profile_path) + expect(flash[:notice]).to match(/#{I18n.t('home.ordergroup_cancelled', group: membership.group.name)}/) + end + + it 'removes user membership' do + membership = fin_user.memberships.first + get_with_defaults :cancel_membership, params: { membership_id: membership.id } + expect(response).to have_http_status(:redirect) + expect(response).to redirect_to(my_profile_path) + expect(flash[:notice]).to match(/#{I18n.t('home.ordergroup_cancelled', group: membership.group.name)}/) + end + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 88dea423..8b1c6ace 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -51,8 +51,8 @@ RSpec.configure do |config| # --seed 1234 config.order = "random" + config.include SpecTestHelper, type: :controller config.include SessionHelper, type: :feature - # Automatically determine spec from directory structure, see: # https://www.relishapp.com/rspec/rspec-rails/v/3-0/docs/directory-structure config.infer_spec_type_from_file_location! diff --git a/spec/support/spec_test_helper.rb b/spec/support/spec_test_helper.rb new file mode 100644 index 00000000..f3737c15 --- /dev/null +++ b/spec/support/spec_test_helper.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module SpecTestHelper + def login(user) + user = User.find_by_nick(user.nick) + session[:user_id] = user.id + session[:scope] = FoodsoftConfig[:default_scope] # Save scope in session to not allow switching between foodcoops with one account + session[:locale] = user.locale + end + + def current_user + User.find(session[:user_id]) + end + + def get_with_defaults(action, params: {}, xhr: false, format: nil) + params['foodcoop'] = FoodsoftConfig[:default_scope] + get action, params: params, xhr: xhr, format: format + end +end + +RSpec.configure do |config| + config.include SpecTestHelper, type: :controller +end From a7a0830d43f071c8cd01f06706dceeb2ff43471b Mon Sep 17 00:00:00 2001 From: kidhab <32387157+kidhab@users.noreply.github.com> Date: Wed, 29 Mar 2023 15:15:59 +0200 Subject: [PATCH 036/105] Show order note as tooltip (#965) --- app/views/orders/index.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/orders/index.html.haml b/app/views/orders/index.html.haml index 1bd870ad..51b426bc 100644 --- a/app/views/orders/index.html.haml +++ b/app/views/orders/index.html.haml @@ -33,7 +33,7 @@ %td= order.name %td= format_date(order.pickup) unless order.pickup.nil? %td= format_time(order.ends) unless order.ends.nil? - %td= truncate(order.note) + %td= truncate(order.note, length: 25, tooltip: true) %td= link_to t('.action_end'), finish_order_path(order), data: {confirm: t('.confirm_end', order: order.name)}, method: :post, class: 'btn btn-small btn-success' From e0f63eebdc9ae73c23a1026566c176b9df3e29d6 Mon Sep 17 00:00:00 2001 From: kidhab <32387157+kidhab@users.noreply.github.com> Date: Wed, 29 Mar 2023 16:00:18 +0200 Subject: [PATCH 037/105] Open external websites in new browser window (#981) Usually the Foodcoop's website and the help pages are external resources. If they load in the same window one could forget to logout from the Foodsoft. --- app/views/layouts/application.html.haml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index 7781096d..c1b1cf00 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -13,9 +13,9 @@ %li= link_to t('.reference_calculator'), home_reference_calculator_path %li= link_to t('.logout'), logout_path %li{class: ('disabled' if FoodsoftConfig[:homepage].blank?)} - = link_to FoodsoftConfig[:name], FoodsoftConfig[:homepage] + = link_to FoodsoftConfig[:name], FoodsoftConfig[:homepage], target: '_blank' - if FoodsoftConfig[:help_url] - %li= link_to t('.help'), FoodsoftConfig[:help_url] + %li= link_to t('.help'), FoodsoftConfig[:help_url], target: '_blank' %li= link_to t('.feedback.title'), new_feedback_path, title: t('.feedback.desc') .clearfix From 8420323c92befb187185a6c8eccd428451929140 Mon Sep 17 00:00:00 2001 From: kidhab <32387157+kidhab@users.noreply.github.com> Date: Wed, 29 Mar 2023 16:01:00 +0200 Subject: [PATCH 038/105] Show a foodcoop's name as subtitle at login screen (#957) --- app/views/sessions/new.html.haml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/views/sessions/new.html.haml b/app/views/sessions/new.html.haml index 76760654..5f147baf 100644 --- a/app/views/sessions/new.html.haml +++ b/app/views/sessions/new.html.haml @@ -6,6 +6,8 @@ - title t('.title') +.lead= FoodsoftConfig[:name] + %noscript .alert.alert-error != t '.nojs', link: link_to(t('.noscript'), "http://noscript.net/") From 5f00a39841f10bace39ba7944e60d25cc12a94ba Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 29 Mar 2023 17:34:38 +0200 Subject: [PATCH 039/105] Bump globalid from 1.0.0 to 1.0.1 (#978) Bumps [globalid](https://github.com/rails/globalid) from 1.0.0 to 1.0.1. - [Release notes](https://github.com/rails/globalid/releases) - [Commits](https://github.com/rails/globalid/compare/v1.0.0...v1.0.1) --- updated-dependencies: - dependency-name: globalid dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Gemfile.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index c53687fb..970bd9c8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -204,7 +204,7 @@ GEM ffi (1.15.5) gaffe (1.2.0) rails (>= 4.0.0) - globalid (1.0.0) + globalid (1.0.1) activesupport (>= 5.0) haml (6.0.5) temple (>= 0.8.2) @@ -292,7 +292,7 @@ GEM mime-types-data (3.2022.0105) mini_mime (1.1.2) mini_portile2 (2.8.0) - minitest (5.16.3) + minitest (5.17.0) mono_logger (1.1.1) msgpack (1.6.0) multi_json (1.15.0) From 67d0492ac494fb628f7695d9f24db7b08323cf2f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 29 Mar 2023 17:37:04 +0200 Subject: [PATCH 040/105] Bump rack from 2.2.4 to 2.2.6.4 (#986) Bumps [rack](https://github.com/rack/rack) from 2.2.4 to 2.2.6.4. - [Release notes](https://github.com/rack/rack/releases) - [Changelog](https://github.com/rack/rack/blob/main/CHANGELOG.md) - [Commits](https://github.com/rack/rack/compare/2.2.4...v2.2.6.4) --- updated-dependencies: - dependency-name: rack dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 970bd9c8..196f9dbd 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -326,7 +326,7 @@ GEM puma (5.6.5) nio4r (~> 2.0) racc (1.6.1) - rack (2.2.4) + rack (2.2.6.4) rack-contrib (2.3.0) rack (~> 2.0) rack-cors (1.1.1) From c01c16ecdb3849ecc6bb94135d8c8d1d8763f5b9 Mon Sep 17 00:00:00 2001 From: kidhab <32387157+kidhab@users.noreply.github.com> Date: Thu, 30 Mar 2023 10:05:47 +0200 Subject: [PATCH 041/105] Specify an URL to redirect after logout via settings (#989) --- app/controllers/sessions_controller.rb | 6 +++++- config/app_config.yml.SAMPLE | 3 +++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index 5b3d0780..f3c50e2a 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -21,7 +21,11 @@ class SessionsController < ApplicationController def destroy logout - redirect_to login_url, :notice => I18n.t('sessions.logged_out') + if FoodsoftConfig[:logout_redirect_url].present? + redirect_to FoodsoftConfig[:logout_redirect_url] + else + redirect_to login_url, :notice => I18n.t('sessions.logged_out') + end end # redirect to root, going to default foodcoop when none given diff --git a/config/app_config.yml.SAMPLE b/config/app_config.yml.SAMPLE index e43705b6..d6f0f8f9 100644 --- a/config/app_config.yml.SAMPLE +++ b/config/app_config.yml.SAMPLE @@ -32,6 +32,9 @@ default: &defaults # custom foodsoft software URL (used in footer) #foodsoft_url: https://github.com/foodcoops/foodsoft + # URL to redirect to after logging out + # logout_redirect_url: https://foodcoop.test + # Default language #default_locale: en # By default, foodsoft takes the language from the webbrowser/operating system. From f2d5936cf07e1bdeb8742c302174f24e9f0957a2 Mon Sep 17 00:00:00 2001 From: nurp Date: Wed, 12 Apr 2023 21:42:03 +0200 Subject: [PATCH 042/105] Turkish language support added (#995) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Added Turkish translation with help of ChatGPT * Changed 'article' and 'item' to 'ürün' and addedtranslations for messages plugin * added translation for the rest of plugins * merge conflicts * fix tr.yml in messages plugin * Corrected more translations --------- Co-authored-by: Nurp <> --- app/assets/javascripts/application.js | 1 + config/locales/de.yml | 1 + config/locales/en.yml | 1 + config/locales/es.yml | 1 + config/locales/fr.yml | 1 + config/locales/nl.yml | 1 + config/locales/tr.yml | 1906 ++++++++++++++++++ plugins/current_orders/config/locales/tr.yml | 73 + plugins/discourse/config/locales/tr.yml | 6 + plugins/documents/config/locales/tr.yml | 40 + plugins/links/config/locales/tr.yml | 23 + plugins/messages/config/locales/tr.yml | 156 ++ plugins/polls/config/locales/tr.yml | 67 + plugins/printer/config/locales/tr.yml | 32 + plugins/wiki/config/locales/tr.yml | 107 + 15 files changed, 2416 insertions(+) create mode 100644 config/locales/tr.yml create mode 100644 plugins/current_orders/config/locales/tr.yml create mode 100644 plugins/discourse/config/locales/tr.yml create mode 100644 plugins/documents/config/locales/tr.yml create mode 100644 plugins/links/config/locales/tr.yml create mode 100644 plugins/messages/config/locales/tr.yml create mode 100644 plugins/polls/config/locales/tr.yml create mode 100644 plugins/printer/config/locales/tr.yml create mode 100644 plugins/wiki/config/locales/tr.yml diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index ebe63685..cd4a7273 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -8,6 +8,7 @@ //= require bootstrap-datepicker/locales/bootstrap-datepicker.es //= require bootstrap-datepicker/locales/bootstrap-datepicker.nl //= require bootstrap-datepicker/locales/bootstrap-datepicker.fr +//= require bootstrap-datepicker/locales/bootstrap-datepicker.tr //= require list //= require list.unlist //= require list.delay diff --git a/config/locales/de.yml b/config/locales/de.yml index 5a1a5b35..5c556357 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -1672,6 +1672,7 @@ de: language: de: Deutsch es: Spanisch + tr: Türkisch fr: Französisch nl: Niederländisch required: diff --git a/config/locales/en.yml b/config/locales/en.yml index 59e94385..10ee1fba 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1693,6 +1693,7 @@ en: es: Spanish fr: French nl: Dutch + tr: Turkish required: mark: "*" text: required diff --git a/config/locales/es.yml b/config/locales/es.yml index 620ec3bb..d3a00a67 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -1440,6 +1440,7 @@ es: es: Español fr: Francés nl: Neerlandés + tr: Turco required: text: requerido 'yes': 'Sí' diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 4dbdb864..a6df1544 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -1196,6 +1196,7 @@ fr: es: Espagnol fr: Français nl: Néerlandais + tr: Turc required: text: requis 'yes': 'Oui' diff --git a/config/locales/nl.yml b/config/locales/nl.yml index 4c97dda4..cacf4a7e 100644 --- a/config/locales/nl.yml +++ b/config/locales/nl.yml @@ -1658,6 +1658,7 @@ nl: es: Spaans fr: Frans nl: Nederlands + tr: Turks required: mark: "*" text: verplicht diff --git a/config/locales/tr.yml b/config/locales/tr.yml new file mode 100644 index 00000000..8fb92b7a --- /dev/null +++ b/config/locales/tr.yml @@ -0,0 +1,1906 @@ +tr: + activerecord: + attributes: + article: + article_category: Kategori + availability: Ürün mevcut mu? + availability_short: mevcut. + deposit: Depozito + fc_price: FoodCoop fiyatı + fc_price_desc: Vergiler, depozito ve Foodcoop ücreti dahil fiyat. + fc_price_short: FC fiyatı + fc_share: FoodCoop marjı + fc_share_short: FC marjı + gross_price: Brüt fiyat + manufacturer: Üretici + name: Adı + note: Not + order_number: Sipariş numarası + order_number_short: Nr. + origin: Menşei + price: Fiyat (net) + supplier: Tedarikçi + tax: KDV + unit: Birim + unit_quantity: Birim miktarı + unit_quantity_short: B.M. + units: Birimler + article_category: + description: İthalat isimleri + name: Adı + article_price: + deposit: Depozito + price: Fiyat (net) + tax: KDV + unit_quantity: Birim miktarı + bank_account: + balance: Bakiye + bank_gateway: Banka geçidi + description: Açıklama + iban: IBAN + name: Adı + bank_gateway: + authorization: Yetkilendirme başlığı + name: Adı + unattended_user: Devre dışı bırakılmış kullanıcı + url: URL + bank_transaction: + amount: Tutar + date: Tarih + external_id: Harici ID + financial_link: Finansal bağlantı + iban: IBAN + reference: Referans + text: Açıklama + delivery: + date: Teslim tarihi + note: Not + supplier: Tedarikçi + document: + created_at: Oluşturulma tarihi + created_by: Oluşturan + data: Veri + mime: MIME tipi + name: Adı + financial_transaction: + amount: Tutar + created_on: Tarih + financial_transaction_class: Finansal işlem sınıfı + financial_transaction_type: Finansal işlem türü + note: Not + ordergroup: Sipariş grubu + user: Giren kullanıcı + financial_transaction_class: + ignore_for_account_balance: Hesap bakiyesi için yoksay + name: Adı + financial_transaction_type: + bank_account: Banka Hesabı + name: Adı + financial_transaction_class: Finansal işlem sınıfı + name_short: Kısa Adı + group_order: + ordergroup: Sipariş grubu + price: Sipariş tutarı + updated_by: Son siparişi veren + group_order_article: + ordered: Sipariş edildi + quantity: Miktar + received: Alındı + result: Sonuç + tolerance: Tolerans + total_price: Toplam + unit_price: Birim fiyatı + invoice: + amount: Tutar + attachment: Ek + created_at: Oluşturulma tarihi + created_by: Oluşturan + date: Fatura tarihi + delete_attachment: Eki sil + deliveries: Stok teslimatı + deposit: Tahsil edilen depozito + deposit_credit: İade edilen depozito + financial_link: Finansal bağlantı + net_amount: İade için düzeltilmiş tutar + note: Not + number: Numara + orders: Sipariş + paid_on: Ödendiği tarih + supplier: Tedarikçi + mail_delivery_status: + created_at: Tarih + email: E-posta + message: Mesaj + order: + boxfill: Kutuları doldurma tarihi + closed_by: Kapatıldı + created_by: Oluşturan + end_action: Otomatik kapanma eylemi + end_actions: + auto_close: Siparişi kapat + auto_close_and_send: Siparişi kapat ve tedarikçiye gönder + auto_close_and_send_min_quantity: Minimum miktarı karşılandığında siparişi kapat ve tedarikçiye gönder + no_end_action: Otomatik eylem yok + ends: Bitiş tarihi + name: Tedarikçi + note: Not + pickup: Teslim alma + starts: Başlangıç tarihi + status: Durum + supplier: Tedarikçi + transport: Taşıma maliyeti + transport_distribution: Taşıma maliyeti dağıtımı + transport_distributions: + articles: Alınan ürün sayısına göre maliyeti dağıt + ordergroup: Her sipariş grubu aynı tutarı öder + price: Sipariş tutarına göre maliyeti dağıt + skip: Maliyeti dağıtma + updated_by: Son düzenleyen + order_article: + article: Ürün + missing_units: Eksik birimler + missing_units_short: Eksik + quantity: Talep edilen miktar + quantity_short: Talep + units_received: Alınan birimler + units_received_short: Alınan + units_to_order: Sipariş edilen birimler + units_to_order_short: Sipariş edildi + update_global_price: Mevcut fiyatı tüm siparişlerde güncelle + order_comment: + text: Bu siparişe yorum ekle ... + ordergroup: + account_balance: Hesap bakiyesi + available_funds: Kredi limiti + break: "(Son) mola" + break_until: kadar + contact: İletişim + contact_address: Adres + contact_person: İlgili kişi + contact_phone: Telefon + description: Açıklama + ignore_apple_restriction: Elma puan sınırlamasını yok say + last_order: Son sipariş + last_user_activity: Son etkinlik + name: Adı + user_tokens: Üyeler + stock_article: + available: Mevcut + price: Fiyat + quantity: Stoktaki miktar + quantity_available: Mevcut miktar + quantity_available_short: Mevcut + quantity_ordered: Sipariş edilen miktar + stock_taking: + date: Tarih + note: Not + supplier: + address: Adres + contact_person: İlgili kişi + customer_number: Müşteri numarası + customer_number_short: Müşteri nr. + delivery_days: Teslimat günleri + email: E-posta + fax: Faks + iban: IBAN + is_subscribed: abone mi? + min_order_quantity: Minimum sipariş miktarı + min_order_quantity_short: Min. miktar + name: Adı + note: Not + order_howto: Nasıl sipariş verilir + phone: Telefon + phone2: Telefon 2 + shared_sync_method: Nasıl senkronize edilir + url: Ana sayfa + supplier_category: + name: Adı + description: Açıklama + financial_transaction_class: Finansal işlem sınıfı + bank_account: Banka hesabı + task: + created_by: Oluşturan + created_on: Oluşturma tarihi + description: Açıklama + done: Yapıldı mı? + due_date: Bitiş tarihi + duration: Süre + name: Aktivite + required_users: Gereken kişi sayısı + user_list: Sorumlu kullanıcılar + workgroup: Çalışma grubu + user: + email: E-posta + first_name: İsim + iban: IBAN + last_activity: Son etkinlik + last_login: Son giriş + last_name: Soyadı + name: Adı + nick: Kullanıcı adı + ordergroup: Sipariş grubu + password: Şifre + password_confirmation: Şifreyi tekrarla + phone: Telefon + workgroup: + one: Çalışma grubu + other: Çalışma grupları + workgroup: + description: Açıklama + name: Adı + role_admin: Yönetim + role_article_meta: Ürün veritabanı + role_finance: Finans + role_invoices: Faturalar + role_orders: Sipariş yönetimi + role_pickups: Teslim günleri + role_suppliers: Tedarikçiler + user_tokens: Üyeler + errors: + has_many_left: Bu %{collection} ile hala ilişkili! + models: + article: + attributes: + name: + taken: isim zaten alınmış + taken_with_unit: isim ve birim zaten alınmış + supplier: + attributes: + shared_sync_method: + included: bu tedarikçi için geçerli bir seçenek değil + task: + attributes: + done: + exclusion: tamamlanmış görevler tekrarlanamaz + models: + article: Ürün + article_category: Kategori + bank_account: Banka hesabı + bank_gateway: Banka geçidi + bank_transaction: Banka işlemi + delivery: Teslimat + financial_transaction: Finansal işlem + financial_transaction_class: Finansal işlem sınıfı + financial_transaction_type: Finansal işlem türü + invoice: Fatura + order: Sipariş + order_article: Sipariş ürünü + order_comment: Sipariş yorumu + ordergroup: + one: Sipariş grubu + other: Sipariş grupları + stock_article: Stok ürünü + stock_taking: Stok sayımı + supplier: Tedarikçi + supplier_category: Tedarikçi kategorisi + task: Görev + user: Kullanıcı + workgroup: Çalışma grubu + admin: + access_to: Erişim sağla + base: + index: + all_ordergroups: Tüm sipariş grupları + all_users: Tüm kullanıcılar + all_workgroups: Tüm çalışma grupları + created_at: oluşturma tarihi + first_paragraph: Burada Foodsoft gruplarını ve kullanıcılarını yönetebilirsiniz. + groupname: grup adı + members: üyeler + name: adı + new_ordergroup: Yeni sipariş grubu + new_user: Yeni kullanıcı + new_workgroup: Yeni çalışma grubu + newest_groups: en yeni gruplar + newest_users: en yeni kullanıcılar + title: Yönetim + type: tür + username: kullanıcı adı + bank_accounts: + form: + title_edit: Banka hesabını düzenle + title_new: Yeni banka hesabı ekle + bank_gateways: + form: + title_edit: Banka geçidini düzenle + title_new: Yeni banka geçidi ekle + configs: + list: + key: Anahtar + title: Konfigürasyon listesi + value: Değer + show: + submit: Kaydet + title: Konfigürasyon + tab_layout: + pdf_title: PDF belgeleri + tab_messages: + emails_title: E-posta gönderimi + tab_payment: + schedule_title: Sipariş takvimi + tab_security: + default_roles_title: Erişim sağlanacak alanlar + default_roles_paragraph: Foodcoop üyesi herkes varsayılan olarak aşağıdaki alanlara erişime sahiptir. + tab_tasks: + periodic_title: Düzenli görevler + tabs: + title: Yapılandırma + update: + notice: Yapılandırma kaydedildi. + confirm: Emin misiniz? + finances: + index: + bank_accounts: Banka hesapları + first_paragraph: Burada finansal işlem sınıflarını ve ilgili finansal işlem türlerini yönetebilirsiniz. Her finansal işlemin bir türü vardır ve birden fazla tür oluşturduysanız, her işlemde seçmeniz gerekmektedir. Finansal işlem sınıfları, finansal işlem türlerini gruplamak için kullanılabilir ve birden fazla oluşturulmuşsa hesap özeti sayfasında ek sütunlar olarak gösterilir. + new_bank_account: Yeni banka hesabı ekle + new_financial_transaction_class: Yeni finansal işlem sınıfı ekle + new_bank_gateway: Yeni banka ağ geçidi ekle + title: Finanslar + transaction_types: Finansal işlem türleri + supplier_categories: Tedarikçi kategorileri + new_supplier_category: Yeni tedarikçi kategorisi + transaction_types: + name: İsim + new_financial_transaction_type: Yeni finansal işlem türü ekle + financial_transaction_classes: + form: + title_edit: Finansal işlem sınıfını düzenle + title_new: Yeni finansal işlem sınıfı ekle + financial_transaction_types: + form: + name_short_desc: Kısa isim, banka işlemlerinde otomatik olarak atanacak finansal işlem türleri için zorunludur. Birden fazla banka hesabı varsa, banka transferleri için tercih edilen hedef hesap seçilebilir. + title_edit: Finansal işlem türünü düzenle + title_new: Yeni finansal işlem türü ekle + mail_delivery_status: + destroy_all: + notice: Tüm e-posta problemleri silindi + index: + destroy_all: Tüm e-posta problemlerini sil + title: E-posta problemleri + ordergroups: + destroy: + error: 'Sipariş grubu silindi olarak işaretlenemedi: %{error}' + notice: Sipariş grubu silindi olarak işaretlendi + edit: + title: Sipariş grubunu düzenle + form: + first_paragraph: Yeni üyeleri %{url} davet edebilirsiniz. + here: buradan + index: + first_paragraph: Burada %{url} grupları ekleyebilir, düzenleyebilir veya silebilirsiniz. + new_ordergroup: Yeni sipariş grubu ekle + new_ordergroups: yeni sipariş grupları + second_paragraph: 'Grup ve sipariş grubu arasındaki farkı şöyle düşünün: Bir sipariş grubu bir hesaba sahiptir ve yiyecek siparişi verebilir. Bir %{url} (örneğin ''sınıflandırma grubu'') içindeki üyeler, görevler ve mesajlar yoluyla birbirleri arasında koordinasyon sağlar. Kullanıcılar sadece bir sipariş grubunda olabilir, ancak birden fazla çalışma grubunda olabilirler.' + title: Sipariş grupları + workgroup: çalışma grubu + new: + title: Yeni sipariş grubu oluştur + show: + confirm: Emin misiniz? + edit: Grup/Üyeleri Düzenle + title: Sipariş grubu %{name} + search_placeholder: isim ... + users: + controller: + sudo_done: Şimdi %{user} olarak giriş yaptınız. Dikkatli olun ve işiniz bittiğinde çıkış yapmayı unutmayın! + destroy: + error: 'Kullanıcı silinemedi: %{error}' + notice: Kullanıcı silindi + edit: + title: Kullanıcıyı Düzenle + form: + create_ordergroup: Aynı adı taşıyan bir sipariş grubu oluşturun ve kullanıcıyı ekleyin. + send_welcome_mail: Kullanıcıya hoş geldiniz e-postası gönderin. + index: + first_paragraph: Burada %{url}, düzenleyebilir ve silebilirsiniz. + new_user: Yeni kullanıcı oluşturabilir + new_users: yeni oluştur + show_deleted: Silinmiş kullanıcıları göster + title: Kullanıcı Yönetimi + new: + title: Yeni kullanıcı oluştur + restore: + error: 'Kullanıcı geri yüklenemedi: %{error}' + notice: Kullanıcı geri yüklendi + show: + confirm_sudo: Devam ederseniz, %{user} kimliğine bürüneceksiniz. İşiniz bittiğinde çıkış yapmayı unutmayın! + groupabos: Grup abonelikleri + member_since: Üye %{time} tarihinden bu yana + person: Kişi + preference: Tercihler + show_email_problems: E-posta sorunlarını göster + sudo: Kimliği kullanarak giriş yap + users: + show_email_problems: E-posta sorunlarını göster + workgroups: + destroy: + error: 'Çalışma grubu silinemedi: %{error}' + notice: Çalışma grubu silindi + edit: + title: Çalışma grubunu düzenle + form: + first_paragraph: Yeni üyeleri %{url} davet edebilirsiniz. + here: buradan + index: + first_paragraph: Burada %{url} oluşturabilir, düzenleyebilir ve silebilirsiniz. + new_workgroup: Yeni çalışma grubu oluştur + new_workgroups: yeni çalışma grupları + ordergroup: sipariş grubu + second_paragraph: 'Çalışma grubu ve sipariş grubu arasındaki farkı dikkate alın: bir %{url} hesabı vardır ve yemek siparişi verebilir. Bir çalışma grubunda (örneğin ''sınıflandırma grubu''), üyeler görevler ve mesajlar aracılığıyla birbirleri arasında koordinasyon sağlarlar. Kullanıcılar yalnızca bir sipariş grubunda olabilir, ancak birden fazla çalışma grubunda olabilirler.' + title: Çalışma Grupları + new: + title: Yeni çalışma grubu oluştur + show: + confirm: Emin misiniz? + edit: Grubu/kullanıcıları düzenle + title: Çalışma Grubu %{name} + workgroups: + members: üyeler + name: isim + supplier_categories: + form: + title_new: Tedarikçi kategorisi ekle + title_edit: Tedarikçi kategorisi düzenle + application: + controller: + error_authn: Kimlik doğrulama gerekiyor! + error_denied: İstenen sayfayı görüntülemeye yetkiniz yok. İlgili izinlere sahip olmanız gerektiğini düşünüyorsanız bir yöneticiye başvurun. Birden fazla kullanıcı hesabına erişiminiz varsa, %{sign_in} deneyin. + error_denied_sign_in: başka bir kullanıcı olarak oturum açmayı + error_feature_disabled: Bu özellik şu anda devre dışı bırakılmış durumda. + error_members_only: Bu eylem, yalnızca grubun üyeleri tarafından kullanılabilir! + error_minimum_balance: Maalesef hesap bakiyeniz %{min} minimumunun altında. + error_token: Erişim reddedildi (geçersiz belirteç)! + article_categories: + create: + notice: Kategori kaydedildi + destroy: + error: 'Kategori silinemiyor: %{message}' + edit: + title: Kategori düzenleme + index: + new: Yeni kategori ekle + title: Ürün kategorileri + new: + title: Yeni kategori ekleme + update: + notice: Kategori güncellendi + articles: + article: + last_update: 'son güncelleme: %{last_update} | brüt: %{gross_price}' + articles: + confirm_delete: Seçilen ürünleri gerçekten silmek istiyor musunuz? + option_available: Ürünleri uygun hale getir + option_delete: Ürünü sil + option_not_available: Ürünleri uygun değil yap + option_select: Bir eylem seçin ... + price_netto: Fiyat + unit_quantity_desc: Birim miktarı + unit_quantity_short: B.M. + controller: + create_from_upload: + notice: "%{count} yeni ürün kaydedildi." + error_invalid: Ürünlerde hatalar var + error_nosel: Seçilmiş bir ürün yok + error_parse: "%{msg} ... satır %{line}'da" + error_update: '%{article} ürünleri güncellerken bir hata oluştu: %{msg}' + parse_upload: + no_file: Lütfen yüklemek için bir dosya seçin. + notice: "%{count} ürün başarıyla analiz edildi." + sync: + notice: Katalog güncel + shared_alert: "%{supplier} harici bir veritabanına bağlı değil" + update_all: + notice: Tüm ürünler ve fiyatlar güncellendi. + update_sel: + notice_avail: Tüm seçili ürünler uygun olarak ayarlandı. + notice_destroy: Tüm seçili ürünler silindi. + notice_noaction: Hiçbir işlem belirtilmedi! + notice_unavail: Tüm seçili ürünler uygun değil olarak ayarlandı. + update_sync: + notice: Tüm ürünler ve fiyatlar güncellendi. + destroy_active_article: + drop: sil + note: "%{article} mevcut siparişlerde kullanılıyor ve silinemez. Lütfen önce ... ürünleri siparişlerden %{drop_link} kaldırın." + edit_all: + note: 'Zorunlu alanlar: ad, birim, (net) fiyat ve sipariş numarası.' + submit: Tüm ürünleri güncelle + title: "%{supplier} tedarikçisinin tüm ürünleri düzenle" + warning: 'Uyarı: tüm ürünler güncellenecek!' + form: + title_edit: Ürün düzenle + title_new: Yeni ürün ekle + import_search_results: + action_import: İçe aktar + already_imported: içe aktarıldı + not_found: Ürün bulunamadı + index: + change_supplier: Tedarikçi değiştir ... + download: Ürünleri indir + edit_all: Tümünü düzenle + ext_db: + import: Ürünü içe aktar + sync: Senkronize et + import: + category: Doğrudan kategoriye aktar + placeholder: Ad ile arama yapın ... + restrict_region: Sadece bölgeye özgü hale getir + title: Ürünü içe aktar + new: Yeni ürün + new_order: Yeni sipariş oluştur + search_placeholder: Ad ... + title: "%{supplier} ürünleri (%{count})" + upload: Ürünleri yükle + model: + error_in_use: "%{article}, bir mevcut siparişin parçası olduğu için silinemiyor!" + error_nosel: Hiçbir ürün seçmediniz + parse_upload: + body: "

Lütfen ürünleri doğrulayın.

Dikkat, tekrar eden ürünler için şu anda herhangi bir kontrol yapılmıyor.

" + submit: Yüklemeyi işle + title: Ürünleri yükle + sync: + outlist: + alert_used: Uyarı, %{article} açık bir siparişte kullanılıyor. Lütfen önce siparişten kaldırın. + body: 'Aşağıdaki ürünler listeden çıkarıldı ve silinecek:' + body_ignored: + one: Sipariş numarası olmayan bir ürün atlandı. + other: "Sipariş numarası olmayan %{count} ürün atlandı." + body_skip: Silinecek ürün yok. + title: Listeden çıkar ... + price_short: Fiyat + submit: Tümünü senkronize et + title: Harici veritabanıyla ürünleri senkronize et + unit_quantity_short: Birim miktarı + update: + body: 'Her ürün iki kez gösterilir: eski değerler gri, metin alanları ise güncellenmiş değerler içerir. Eski ürünlerle farklılıklar sarı renkle işaretlenmiştir.' + title: Güncelle ... + update_msg: + one: Bir ürün güncellenmesi gerekiyor. + other: "%{count} ürün güncellenmesi gerekiyor." + upnew: + body_count: + one: Bir yeni ürün eklemek için. + other: Eklemek için %{count} ürün var. + title: Yeni ekle ... + upload: + fields: + reserved: "(Ayrılmış)" + status: Durum (x=atla) + file_label: Lütfen uyumlu bir dosya seçin + options: + convert_units: Mevcut birimleri koruyun, birim miktarını ve fiyatı yeniden hesaplayın (senkronize gibi). + outlist_absent: Yüklenen dosyada olmayan ürünleri sil. + sample: + juices: Meyve suları + nuts: Kuruyemişler + organic: Organik + supplier_1: Kuruyemişçi + supplier_2: Kahverengi tarladan + supplier_3: Yeşil tarladan + tomato_juice: Domates suyu + walnuts: Cevizler + submit: Dosyayı yükle + text_1: '%{supplier} ürünlerini güncellemek için bir elektronik tablo yükleyebilirsiniz. Excel (xls, xlsx) ve OpenOffice (ods) tabloları kabul edilir, virgülle ayrılmış dosyalar da kabul edilir (csv, utf-8 kodlamalı "; " ile ayrılmış sütunlar). Sadece ilk sayfa içe aktarılacak ve sütunlar aşağıdaki sıraya göre olmalıdır:' + text_2: Burada gösterilen satırlar örneklerdir. İlk sütunda "x" olduğunda, ürün listeden çıkarılır ve silinir. Bu, örneğin tedarikçiyle ürünler müsait olmadığında birçok ürünü hızlı bir şekilde kaldırmak için elektronik tabloyu düzenlemenize ve çıkarmanıza izin verir. Kategori, Foodsoft kategori listenize eşleştirilecek (hem kategori adı hem de içe aktarma adlarıyla). + title: "%{supplier} ürünlerini yükle" + bank_account_connector: + confirm: Lütfen %{code} kodunu onaylayın. + fields: + email: E-Posta + pin: PIN + password: Şifre + tan: TAN + username: Kullanıcı adı + config: + hints: + applepear_url: Görevler için kullanılan elma ve armut sisteminin açıklandığı web sitesi. + charge_members_manually: Üyelerin ne aldığını başka bir yerde takip ettiğinizde (örneğin kağıt üzerinde), bunu Foodsoft'a girmek istemezseniz, bunu seçin. Üye hesaplarını manuel olarak ücretlendirmeniz gerekecektir ("Yeni işlem ekle" kullanarak). Denge ekranında hala siparişleri uzlaştırmanız gerekecek, ancak üye hesaplarına ücret yansıtılmayacak. + contact: + email: Genel iletişim e-posta adresi, web sitesinde ve bazı formlarda gösterilir. + street: Adres, genellikle teslimat ve toplama noktanız olacaktır. + currency_space: Para birimi simgesinin ardından boşluk ekleyip eklemediğinizi belirtir. + currency_unit: Fiyatları görüntülemek için para birimi simgesi. + custom_css: Bu site'nin düzenini değiştirmek için, kaskatı stili (CSS) dilini kullanarak stil değişiklikleri yapabilirsiniz. Varsayılan stili kullanmak için boş bırakın. + email_from: E-postalar bu e-posta adresinden gönderilecek. Foodcoop'un iletişim adresini kullanmak için boş bırakın. + email_replyto: Foodsoft tarafından gönderilen e-postalardan farklı bir adresten yanıt almak istediğinizde bunu ayarlayın. + email_sender: E-postalar bu e-posta adresinden gönderilir. Gönderen e-posta adresinin etki alanının SPF kaydına web sunucusunun kaydedilmesi gerekebilir, böylece gönderilen e-postalar spam olarak sınıflandırılmaz. + help_url: Belgelendirme web sitesi. + homepage: Yiyecek kooperatifinizin web sitesi. + ignore_browser_locale: Kullanıcının henüz bir dil seçmediği zaman kullanıcının bilgisayarının dilini yoksayın. + minimum_balance: Üyeler, hesap bakiyelerinin bu miktarın üzerinde veya eşit olduğu durumlarda sadece sipariş verebilirler. + name: Yiyecek kooperatifinizin adı. + order_schedule: + boxfill: + recurr: Kutuları-doldurma aşamasının varsayılan başlama zamanı. + time: Siparişin başlangıç saati. + ends: + recurr: Varsayılan sipariş kapanma tarihi için plan. + time: Siparişlerin kapatılacağı varsayılan saat. + initial: Program bu tarihte başlar. + page_footer: Her sayfanın altında gösterilir. Tamamen devre dışı bırakmak için "boş" girin. + pdf_add_page_breaks: + order_by_articles: Her ürünü ayrı bir sayfada göster. + order_by_groups: Her sipariş grubunu ayrı bir sayfada göster. + pdf_font_size: PDF belgeleri için temel yazı tipi boyutu (standart 12'dir). + pdf_page_size: PDF belgeleri için sayfa boyutu, genellikle "A4" veya "zarf". + price_markup: Üyelerin toplam fiyatına eklenen yüzde. + stop_ordering_under: Üyeler sadece bu kadar elma puanı olduğunda sipariş verebilirler. + tasks_period_days: İki periyodik görev arasındaki gün sayısı (varsayılan olarak 7, yani bir hafta). + tasks_upfront_days: Periyodik görevleri kaç gün önceden planlamak istediğinize bağlı olarak değişir. + tax_default: Yeni ürünler için varsayılan KDV yüzdesi. + tolerance_is_costly: Üye toleransını maksimum dolduracak kadar sipariş edin (sadece son kutuyu dolduracak kadar ürün eklemek yerine). Açık siparişin toplam tutarına uygulanan tolerans da buna dahildir. + distribution_strategy: Bir sipariş alındıktan sonra ürünlerin nasıl dağıtılacağı. + use_apple_points: Elma puanı sistemi etkinleştirildiğinde, üyeler sipariş vermeye devam edebilmek için bazı görevleri yapmak zorundadırlar. + use_boxfill: Etkinleştirildiğinde, siparişin sonuna doğru, üyeler yalnızca toplam sipariş tutarını artırdığında siparişlerini değiştirebilirler. Bu, kalan kutuları doldurmaya yardımcı olur. Siparişler için bir kutuları-doldurma tarihi belirlemelisiniz. + use_iban: Etkinleştirildiğinde, tedarikçi ve kullanıcı uluslararası banka hesap numaralarını saklayabileceği ek bir alan sunar. + use_nick: Gerçek adlar yerine takma adları göster ve kullan. Bu seçeneği etkinleştirdiğinizde, her kullanıcının bir takma adı olup olmadığını kontrol edin. + use_self_service: Seçili dengeleme (balancing) işlevlerini üyeler kendileri kullanabilirler. + webstats_tracking_code: Web analitiği için takip kodu (Piwik veya Google analytics gibi). Takip etmek istemiyorsanız boş bırakın. + keys: + applepear_url: Elma sistemi yardım URL'si + charge_members_manually: Üyeleri manuel olarak şarj et + contact: + city: Şehir + country: Ülke + email: E-posta + phone: Telefon + street: Cadde/Sokak + zip_code: Posta kodu + currency_space: Boşluk ekle + currency_unit: Para birimi + custom_css: Özel CSS + default_locale: Varsayılan dil + default_role_article_meta: Ürünler + default_role_finance: Finans + default_role_invoices: Faturalar + default_role_orders: Siparişler + default_role_pickups: Alım günleri + default_role_suppliers: Tedarikçiler + disable_invite: Davetiyeleri devre dışı bırak + email_from: Adresinden + email_replyto: Yanıtlanacak adres + email_sender: Gönderen adresi + help_url: Belgeleme URL'si + homepage: Ana sayfa + ignore_browser_locale: Tarayıcı dilini yoksay + minimum_balance: Minimum bakiye + name: İsim + order_schedule: + boxfill: + recurr: Kutu doldurma sonrası + time: zaman + ends: + recurr: Sipariş sonu + time: zaman + initial: Program başlangıcı + page_footer: Sayfa altbilgisi + pdf_add_page_breaks: Sayfa atlamaları + pdf_font_size: Yazı tipi boyutu + pdf_page_size: Sayfa boyutu + price_markup: Foodcoop marjı + stop_ordering_under: Minimum elma puanı + tasks_period_days: Dönem + tasks_upfront_days: Önceden oluştur + tax_default: Varsayılan KDV + time_zone: Zaman dilimi + tolerance_is_costly: Tolerans maliyetlidir + distribution_strategy: Dağıtım stratejisi + distribution_strategy_options: + first_order_first_serve: İlk sipariş edenlere öncelik verin + no_automatic_distribution: Otomatik dağıtım yok + use_apple_points: Elma puanları kullan + use_boxfill: Kutuları-doldurma aşamasını kullan + use_iban: IBAN kullan + use_nick: Takma ad kullan + use_self_service: Kendi kendine (self service) hizmet kullan + webstats_tracking_code: Takip kodu + tabs: + applications: Uygulamalar + foodcoop: Foodcoop + language: Dil + layout: Düzen + list: Liste + messages: Mesajlar + others: Diğerleri + payment: Finans + security: Güvenlik + tasks: Görevler + deliveries: + add_stock_change: + how_many_units: 'Kaç birim (%{unit}) teslim edilecek? Stoğun adı: %{name}.' + create: + notice: Teslimat oluşturuldu. Lütfen fatura eklemeyi unutmayın! + destroy: + notice: Teslimat silindi. + edit: + title: Teslimatı düzenle + form: + confirm_foreign_supplier_reedit: Stok ürünü %{name} başarıyla kaydedildi. Ancak, bu teslimatın tedarikçisinden farklı bir tedarikçiye ait. Stok ürününü tekrar düzenlemek ister misiniz? + create_from_blank: Yeni ürün oluştur + create_stock_article: Stok ürünü oluştur + title_fill_quantities: 2. Teslimat miktarlarını belirle + title_finish_delivery: 3. Teslimatı tamamla + title_select_stock_articles: 1. Stok ürünlerini seç + index: + confirm_delete: Silmek istediğinizden emin misiniz? + new_delivery: '%{supplier} için yeni teslimat oluştur ' + title: "%{supplier}/teslimatlar" + invoice_amount: Fatura tutarı + invoice_net_amount: Fatura net tutarı + new: + title: "%{supplier} için yeni teslimat" + show: + sum: Toplam + sum_diff: Brüt - fatura tutarı + sum_gross: Brüt toplam + sum_net: Net toplam + title: Teslimatı göster + title_articles: Ürünler + stock_article_for_adding: + action_add_to_delivery: Teslimata ekle + action_edit: Düzenle + action_other_price: Kopyala + stock_change_fields: + remove_article: Teslimattan çıkar + suppliers_overview: Tedarikçi genel bakış + update: + notice: Teslimat güncellendi. + documents: + order_by_articles: + filename: Sipariş %{name}-%{date} - ürünlere göre + title: '%{name} için ürünlere göre sıralanmış sipariş, %{date} tarihinde kapanmıştır' + order_by_groups: + filename: Sipariş %{name}-%{date} - gruba göre + sum: Toplam + title: '%{name} için gruba göre sıralanmış sipariş, %{date} tarihinde kapanmıştır' + order_fax: + filename: Sipariş %{name}-%{date} - Faks + rows: + - Sipariş Numarası + - Miktar + - Ad + - Birim miktarı + - Birim + - Birim fiyatı + - Ara toplam + total: Toplam + order_matrix: + filename: Sipariş %{name}-%{date} - sıralama matrisi + heading: Ürün genel bakışı (%{count}) + title: '%{date} tarihinde kapatılan %{name} sipariş sıralama matrisi' + errors: + general: Bir problem oluştu. + general_again: Bir problem oluştu. Lütfen tekrar deneyin. + general_msg: 'Bir problem oluştu: %{msg}' + internal_server_error: + text1: Beklenmeyen bir hata oluştu. Özür dileriz! + text2: Bildirildi. Eğer sorun devam ederse, bize bildirin lütfen. + title: Dahili sunucu hatası + not_found: + text: Bu sayfa mevcut değil, üzgünüz! + title: Sayfa bulunamadı + feedback: + create: + notice: Geri bildiriminiz başarıyla gönderildi. Teşekkür ederiz! + new: + first_paragraph: Bir hata buldunuz mu? Önerileriniz mi var? Fikirleriniz mi? Geri bildirimlerinizi duymaktan mutluluk duyarız. + second_paragraph: Lütfen unutmayın, Foodsoft ekibi yalnızca yazılımın bakımından sorumludur. Foodcoop'unuzun organizasyonuyla ilgili sorularınız için uygun kişiye başvurmanız gerekmektedir. + send: Gönder + title: Geri Bildirim Ver + finance: + balancing: + close: + alert: 'Muhasebe yapılırken bir hata oluştu: %{message}' + notice: Sipariş başarıyla tamamlandı, hesap bakiyesi güncellendi. + close_all_direct_with_invoice: + notice: '%{count} sipariş tamamlandı.' + close_direct: + alert: 'Sipariş tamamlanamadı: %{message}' + notice: Sipariş tamamlandı. + confirm: + clear: Hesapla + first_paragraph: 'Sipariş tamamlandığında, tüm grup hesapları güncellenecektir.
Hesaplar şu şekilde tahsil edilecektir:' + or_cancel: ya da muhasebeye geri dön + title: Siparişi tamamla + edit_note: + title: Sipariş notunu düzenle + edit_results_by_articles: + add_article: Ürün ekle + amount: Miktar + edit_transport: Taşıma masraflarını düzenle + gross: Brüt + net: Net + edit_transport: + title: Taşıma maliyetlerini dağıt + group_order_articles: + add_group: Grup ekle + total: Toplam maliyet + total_fc: Toplam (FC fiyat) + units: Birimler + index: + close_all_direct_with_invoice: Hepsini faturayla kapat + title: Kapatılan siparişler + invoice: + edit: Faturayı düzenle + invoice_amount: 'Fatura tutarı:' + invoice_date: 'Fatura tarihi:' + invoice_number: 'Fatura numarası:' + minus_refund_calculated: "- Tahsil edilen depozito:" + new: Yeni fatura oluştur + new_body: 'Bu sipariş için bir fatura oluştur:' + plus_refund_credited: "+ İade edilen depozito:" + refund_adjusted_amount: 'iade için düzeltilen tutar:' + new: + alert: Dikkat, sipariş zaten hesaba katılmış + articles_overview: Ürünlere genel bakışı + close_direct: Ödemeyi atla + close_direct_confirm: Üye hesaplarını ücretlendirmeden siparişi tamamla. Üye hesaplarını zaten manuel olarak borçlandırdıysanız veya gerçekten ne yaptığınızı biliyorsanız bunu yapın. + comment_on_transaction: Muhasebenize bir yorum ekleyebilirsiniz. + comments: Yorumlar + confirm_order: Siparişi tamamla + create_invoice: Fatura ekle + edit_note: Notu düzenle + edit_order: Siparişi düzenle + groups_overview: Gruplara genel bakış + invoice: Fatura + notes_and_journal: Sipariş Notları + summary: Özet + title: Hesap defteri %{name} + view_options: Görüntüleme seçenekleri + order_article: + confirm: Emin misiniz? + orders: + clear: Hesapla + cleared: Hesaplandı (%{amount}) + end: Son + ended: Kapatıldı + name: Tedarikçi + no_closed_orders: Şu anda kapatılmış bir sipariş yok. + state: Durum + summary: + changed: Veri değiştirildi! + duration: "%{starts} ile %{ends} arası" + fc_amount: 'Satış değeri:' + fc_profit: Foodcoop'tan artan (surplus) + gross_amount: 'Brüt değer:' + groups_amount: 'Sipariş grupları toplamı:' + net_amount: 'Net değer:' + reload: Özeti yeniden yükle + with_extra_charge: 'ek ücretle birlikte:' + without_extra_charge: 'ek ücretsiz:' + bank_accounts: + assign_unlinked_transactions: + notice: '%{count} işlem atanmıştır' + import: + notice: '%{count} yeni işlem içe aktarıldı' + no_import_method: Bu banka hesabı için içe aktarma yöntemi yapılandırılmamıştır. + submit: İçe aktar + title: '%{name} için banka işlemlerini içe aktar' + index: + title: Banka Hesapları + bank_transactions: + index: + assign_unlinked_transactions: İşlemleri Ata + import_transactions: İçe Aktar + title: '%{name} için Banka İşlemleri (%{balance})' + show: + add_financial_link: Finansal bağlantı ekle + belongs_to_supplier: tedarikçiye ait + belongs_to_user: kullanıcıya ait + in_ordergroup: sipariş grubunda + transactions: + add_financial_link: Bağlantı ekle + create: + notice: Fatura oluşturuldu. + financial_links: + add_bank_transaction: + notice: Bağlantı banka işlemine eklendi. + add_financial_transaction: + notice: Bağlantı finansal işleme eklendi. + add_invoice: + notice: Bağlantı faturaya eklendi. + create: + notice: Yeni finansal bağlantı oluşturuldu. + create_financial_transaction: + notice: Finansal işlem eklendi. + index_bank_transaction: + title: Banka işlemi ekle + index_financial_transaction: + title: Finansal işlem ekle + index_invoice: + title: Fatura ekle + new_financial_transaction: + title: Finansal işlem ekle + remove_bank_transaction: + notice: Bağlantı banka işleminden kaldırıldı. + remove_financial_transaction: + notice: Bağlantı finansal işlemden kaldırıldı. + remove_invoice: + notice: Bağlantı faturadan kaldırıldı. + show: + add_bank_transaction: Banka işlemi ekle + add_financial_transaction: Finansal işlem ekle + add_invoice: Fatura ekle + amount: Miktar + date: Tarih + description: Açıklama + new_financial_transaction: Yeni finansal işlem + title: Finansal bağlantı %{number} + type: Tip + financial_transactions: + controller: + create: + notice: İşlem kaydedildi. + create_collection: + alert: 'Bir hata oluştu: %{error}' + error_note_required: Not doldurulması gereklidir! + notice: Tüm işlemler kaydedildi. + destroy: + notice: İşlem kaldırıldı. + index: + balance: '%{balance} hesap bakiyesi' + last_updated_at: "(son güncelleme %{when} tarihinden önce)" + new_transaction: Yeni işlem oluştur + title: '%{name} için hesap özeti' + index_collection: + show_groups: Hesapları yönet + title: Finansal işlemler + new: + paragraph: Burada, %{name} için para yatırabilir veya çekebilirsiniz. + paragraph_foodcoop: Burada, gıda kooperatifi için para yatırabilir veya çekebilirsiniz. + title: Yeni işlem + new_collection: + add_all_ordergroups: Tüm sipariş gruplarını ekle + add_all_ordergroups_custom_field: '%{label} etiketi ile tüm sipariş gruplarını ekle' + create_financial_link: Yeni işlemler için ortak finansal bağlantı oluşturun. + create_foodcoop_transaction: Gıda kooperatifi için ters toplam tutarlı bir işlem oluşturun ("çift taraflı muhasebe" durumunda) + new_ordergroup: Yeni sipariş grubu ekle + save: İşlemi kaydet + set_balance: Sipariş grubunun bakiyesini girilen tutara ayarlayın. + sidebar: Burada aynı anda birden fazla hesabı güncelleyebilirsiniz. Örneğin, bir hesap özetinden tüm sipariş grubu transferlerini. + title: Birden fazla hesap güncelleme + ordergroup: + remove: Kaldır + remove_group: Grubu kaldır + transactions: + confirm_revert: '%{name} işlemi geri almak istediğinizden emin misiniz? Bu durumda, tersine çevrilen bir miktarla yeni bir işlem oluşturulacak ve orijinal işlemle birleştirilecektir. Bu gizli işlemler, "Gizli işlemleri göster" seçeneği aracılığıyla sadece görüntülenebilir ve normal kullanıcılara hiç görünmez.' + revert_title: Normal kullanıcılardan gizleyecek şekilde işlemi geri alın. + transactions_search: + show_hidden: Gizli işlemleri göster + index: + amount_fc: Tutar(FC) + end: Son + everything_cleared: Harika, her şey hesaplandı... + last_transactions: Son işlemler + open_transactions: Tamamlanmamış siparişler + show_all: tümünü göster + title: Finanslar + unpaid_invoices: Ödenmemiş faturalar + invoices: + edit: + title: Faturayı düzenle + form: + attachment_hint: Sadece JPEG ve PDF dosyaları kabul edilir. + index: + action_new: Yeni fatura oluştur + show_unpaid: Ödenmemiş faturaları göster + title: Faturalar + new: + title: Yeni fatura oluştur + show: + title: Fatura %{number} + unpaid: + invoices_sum: Toplam tutar + invoices_text: Referans + title: Ödenmemiş faturalar + ordergroups: + index: + new_financial_link: Yeni finansal bağlantı ekle + new_transaction: Yeni işlem ekle + show_all: Tüm işlemler + show_foodcoop: Gıda kooperatifi işlemleri + title: Hesapları yönet + ordergroups: + account_statement: Hesap özeti + new_transaction: Yeni işlem + update: + notice: Fatura güncellendi + foodcoop: + ordergroups: + index: + name: İsim ... + only_active: Sadece aktif gruplar + only_active_desc: "(son 3 ayda en az bir kez sipariş vermiş olanlar)" + title: Sipariş Grupları + ordergroups: + break: "%{start} - %{end}" + users: + index: + body: "

Burada Gıda Kooperatifinizin üyelerine bir mesaj yazabilirsiniz. Diğer üyelerin sizinle iletişim kurmasını isterseniz, bunu %{profile_link} bölümünden etkinleştirin.

" + ph_name: İsim ... + ph_ordergroup: Sipariş grubu ... + profile_link: seçenekler + title: Kullanıcılar + workgroups: + edit: + invite_link: burada + invite_new: Yeni üyeleri %{invite_link} davet edebilirsiniz. + title: Grubu Düzenle + index: + body: "

Bir grubu düzenlemek yalnızca grubun üyeleri tarafından yapılabilir.
Bir gruba katılmak istiyorsanız, lütfen üyelere bir mesaj gönderin.

" + title: Çalışma Grupları + workgroup: + edit: Grubu Düzenle + show_tasks: Tüm Görevleri Göster + group_order_articles: + form: + amount_change_for: '%{article} için miktarı değiştirin' + result_hint: 'Birim: %{unit}' + group_orders: + archive: + desc: Tüm %{link} burada görüntüleyebilirsiniz. + open_orders: Mevcut siparişler + title: '%{group} Siparişleri' + title_closed: Hesaplandı + title_open: Hesaplanmadı/Kapatılmadı + create: + error_general: Sipariş hatası nedeniyle güncellenemedi. + error_stale: Başkası sipariş vermiş olabilir, sipariş güncellenemedi. + notice: Sipariş kaydedildi. + errors: + closed: Bu sipariş zaten kapatıldı. + no_member: Bir sipariş grubunun üyesi değilsiniz. + notfound: Yanlış URL, bu sizin siparişiniz değil. + form: + action_save: Siparişi kaydet + new_funds: Yeni hesap bakiyesi + price: Fiyat + reset_article_search: Arama sıfırla + search_article: Ürün ara... + sum_amount: Mevcut miktar + title: Siparişler + total_sum_amount: Toplam miktar + total_tolerance: Toplam tolerans + units: Birimler + units_full: Dolu birimler + units_total: Toplam birimler + index: + closed_orders: + more: daha fazla... + title: Kapanmış siparişler + finished_orders: + title: Unsettled orders + total_sum: Total sum + funds: + finished_orders: Tamamlanmamış siparişler + open_orders: Mevcut siparişler + title: Kredi + title: Siparişler genel bakışı + messages: + not_enough_apples: Sipariş vermek için en az %{stop_ordering_under} elma puanınız olmalıdır. Şu anda sipariş grubunuzda sadece %{apples} elma puanı var. + order: + title: Ürünler + show: + articles: + edit_order: Siparişi Düzenle + not_ordered_msg: Henüz sipariş vermediniz. + order_closed_msg: Maalesef, bu sipariş kapandı. + order_nopen_title: Tüm grupların güncel siparişleri göz önüne alındığında + order_not_open: Alındı + order_now: İşte şansın! + order_open: Mevcut + ordered: Sipariş edildi + ordered_title: Tutar + tolerans + show_hide: Sipariş edilmemiş ürünleri göster/gizle + show_note: Notu göster + title: Ürün genel bakışı + unit_price: Birim fiyatı + comment: Yorum + comments: + title: Yorumlar + not_ordered: Sipariş vermediniz. + sum: Toplam + title: '%{order} için sipariş sonucunuz' + switch_order: + remaining: "%{remaining} kaldı" + title: Mevcut siparişler + update: + error_general: Sipariş bir hatadan dolayı güncellenemedi. + error_stale: Bu sırada başka birisi sipariş vermiş, sipariş güncellenemedi. + notice: Sipariş kaydedildi. + helpers: + application: + edit_user: Kullanıcıyı düzenle + nick_fallback: "(kullanıcı adı yok)" + role_admin: Yönetici + role_article_meta: Ürünler + role_finance: Finans + role_invoices: Faturalar + role_orders: Siparişler + role_pickups: Teslimat günleri + role_suppliers: Tedarikçiler + show_google_maps: Google Haritalarda göster + sort_by: 'Şuna göre sırala: %{text}' + deliveries: + new_invoice: Yeni fatura + show_invoice: Faturayı göster + orders: + old_price: Eski fiyat + option_choose: Tedarikçi/Depo seçin + option_stock: Depo + order_pdf: PDF oluştur + submit: + invite: + create: davetiye gönder + tasks: + required_users: "%{count} üye daha gerekiyor!" + task_title: "%{name} (%{duration} saat)" + home: + apple_bar: + desc: 'Bu, sipariş grubunuzdaki tamamlanan görevlerin sipariş hacmi ile Foodcoop ortalaması arasındaki oranını gösterir. Uygulamada: Her %{amount} toplam sipariş için bir görev yapmalısınız!' + more_info: Daha fazla bilgi + points: 'Mevcut elma puanınız: %{points}' + warning: Uyarı, elma puanınız %{threshold} değerinden azsa, sipariş vermenize izin verilmez! + changes_saved: Değişiklikler kaydedildi. + index: + due_date_format: "%A %d %B" + my_ordergroup: + last_update: Son güncelleme %{when} tarihinden önce yapıldı + title: Benim sipariş grubum + transactions: + title: Son işlemler + view: Hesap özetini göster + ordergroup: + title: Sipariş grubunun katılımı + tasks_move: + action: Görevleri üstlen / görevleri reddet + desc: Bu görevlerden siz sorumlusunuz. + title: Görevleri üstlen + tasks_open: + title: Açık görevler + view_all: Tüm görevleri göster + title: Ana Sayfa + your_tasks: Görevleriniz + no_ordergroups: Maalesef bir sipariş grubu üyesi değilsiniz. + ordergroup: + account_summary: Hesap özeti + invite: Yeni kişi davet et + search: Ara ... + title: Benim sipariş grubum + ordergroup_cancelled: '%{group} grubundaki üyeliğinizi iptal ettiniz.' + profile: + groups: + cancel: Grubu terk et + cancel_confirm: Bu grubu terk etmek istediğinizden emin misiniz? + invite: Yeni üye davet et + title: Grup üyeliğiniz + title: Profilim + user: + since: "(%{when} üyesi)" + title: "%{user}" + reference_calculator: + transaction_types_headline: Amaç + placeholder: Bu işlem için kullanmanız gereken referansı görmek için önce lütfen her alan için aktarmak istediğiniz miktarları girin. + text0: Lütfen şu miktarı transfer edin + text1: referans numarası ile birlikte + text2: şu banka hesabına + title: Referans Hesaplayıcı + start_nav: + admin: Yönetim + finances: + accounts: Hesapları güncelle + settle: Hesap siparişleri + title: Finanslar + foodcoop: Gıda kooperatifi + members: Üyeler + new_ordergroup: Yeni sipariş grubu + new_user: Yeni üye + orders: + end: Siparişleri kapat + overview: Sipariş özeti + title: Siparişler + products: + edit: Ürünleri güncelle + edit_stock: Stokları güncelle + edit_suppliers: Tedarikçileri güncelle + title: Ürünler + tasks: Görevlerim + title: Direkt olarak... + invites: + errors: + already_member: kullanımda. Kişi zaten bu Foodcoop'un üyesi. + modal_form: + body: "

Burada, Foodcoop'un üyesi olmayan bir kişiyi <%{group}> gruplarına davet edebilirsiniz. Davet kabul edildikten sonra, kişi siparişinize ürün ekleyebilecek (ve kaldırabilecek).

Bu, birini foodcoop'a tanıtmak veya aynı evde birden fazla kişiyle sipariş vermeye yardımcı olmak için harika bir yoldur.

" + title: Kişi davet et + new: + action: Davet gönder + body: "

Burada, henüz Foodcoop üyesi olmayan bir kişiyi <%{group}> grubuna ekleyebilirsiniz.

" + success: Kullanıcı başarıyla davet edildi. + js: + ordering: + confirm_change: Bu siparişe yapılan değişiklikler kaybolacak. Değişikliklerinizi kaybetmek ve devam etmek istiyor musunuz? + layouts: + email: + footer_1_separator: "--" + footer_2_foodsoft: 'Foodsoft: %{url}' + footer_3_homepage: 'Foodcoop: %{url}' + footer_4_help: 'Yardım: %{url}' + foodsoft: Foodsoft + footer: + revision: revizyon %{revision} + header: + feedback: + desc: Bir hata mı buldunuz? Öneriler? Fikirler? İnceleme? + title: Geri bildirim + help: Yardım + logout: Çıkış yap + ordergroup: Benim sipariş grubum + profile: Profili düzenle + reference_calculator: Referans Hesaplayıcı + logo: "foodsoft" + lib: + render_pdf: + page: "%{count} sayfasının %{number}. sayfası" + login: + accept_invitation: + body: "

%{foodcoop} gıda kooperatifinin %{group} grubunun bir üyesi olarak davet edildiniz.

Katılmak isterseniz, lütfen bu formu doldurun.

Doğal olarak, kişisel bilgileriniz herhangi bir nedenle üçüncü taraflarla paylaşılmayacaktır. Tüm'ü, tüm Gıda Kooperatifleri üyeleri için görünür olacak şekilde kişisel bilgilerinizin ne kadarının görünür olacağını siz belirleyebilirsiniz. Lütfen not edin ki, yöneticiler bilgilerinize erişebilirler.

" + submit: Bir Foodsoft hesabı oluşturun + title: "%{name} için davet" + controller: + accept_invitation: + notice: Tebrikler, hesabınız başarıyla oluşturuldu. Şimdi giriş yapabilirsiniz. + error_group_invalid: Davet edildiğiniz grup artık mevcut değil. + error_invite_invalid: Davetiniz geçersiz (artık geçerli değil). + error_token_invalid: Geçersiz veya süresi dolmuş belirteç (token). Lütfen tekrar deneyin. + reset_password: + notice: Eğer e-postanız kayıtlıysa, şifrenizi sıfırlamak için bir bağlantı içeren bir mesaj alacaksınız. Spam klasörünüzü kontrol etmeniz gerekebilir. + update_password: + notice: Şifreniz güncellendi. Artık giriş yapabilirsiniz. + forgot_password: + body: "

Sorun değil, yeni bir şifre seçebilirsiniz.

Lütfen burada kayıtlı olan e-posta adresinizi girin. Daha fazla talimat için bir e-posta alacaksınız.

" + submit: Yeni şifre iste + title: Şifremi unuttum? + new_password: + body: "

%{user} için yeni şifreyi girin.

" + submit: Yeni şifreyi kaydet + title: Yeni şifre + mailer: + dateformat: "%d %b" + feedback: + header: "%{user} tarafından %{date} tarihinde yazıldı:" + subject: Foodsoft için geri bildirim + from_via_foodsoft: "%{name} Foodsoft aracılığıyla" + invite: + subject: Foodcoop Davetiyesi + text: | + Merhaba! + + %{user} <%{mail}> seni "%{group}" grubuna katılmaya davet etti. + Davetiye kabul etmek ve foodcoop'a katılmak için lütfen bu bağlantıyı takip et: %{link} + Bu bağlantı sadece bir kez kullanılabilir ve %{expires} tarihinde süresi dolacaktır. + + + Sevgiler, Foodsoft Ekibi! + negative_balance: + subject: Negatif hesap bakiyesi + text: | + Sayın %{group}, + + Hesap bakiyeniz %{when} tarihinde yapılan %{amount} TL'lik işlem nedeniyle sıfırın altına düştü: "%{balance}" + + "%{user}" tarafından "%{note}" için %{amount} ücret alındı. + + Lütfen mümkün olan en kısa sürede hesabınıza para yatırınız. + + + + %{foodcoop} adına saygılar. + not_enough_users_assigned: + subject: '"%{task}" için hala kişilere ihtiyaç var!' + text: | + Sevgili %{user}, + + Çalışma grubunun '%{task}' görevi %{when} tarihinde tamamlanacak + ve daha fazla katılımcıya ihtiyaç duyuyor! + + Eğer henüz bu göreve atanmadıysanız, şimdi fırsatınız var: + + %{workgroup_tasks_url} + + Görevleriniz: %{user_tasks_url} + order_result: + subject: '%{name} siparişi kapatıldı' + text0: | + Sevgili %{ordergroup}, + + "%{order}" siparişi %{user} tarafından %{when} tarihinde kapatıldı. + text1: | + Tahmini olarak %{pickup} tarihinde teslim edilebilir. + text2: | + Sipariş grubunuz için aşağıdaki ürünler sipariş edildi: + text3: |- + o Toplam tutar: %{sum} + + Siparişi çevrimiçi olarak görüntüleyebilirsiniz: %{order_url} + + + %{foodcoop} adına sevgiler. + order_received: + subject: '%{name} için sipariş teslimi kaydedildi' + text0: | + Sevgili %{ordergroup}, + + "%{order}" için sipariş teslimi kaydedilmiştir. + abundant_articles: Fazla alındı + scarce_articles: Az alındı + article_details: | + o %{name}: + -- Sipariş edilen: %{ordered} x %{unit} + -- Alınan: %{received} x %{unit} + order_result_supplier: + subject: '%{name} için yeni sipariş' + text: | + Merhaba! + + %{foodcoop} Foodcoop'u sipariş vermek istiyor. + + Lütfen ekli PDF ve hesap tablosunu inceleyiniz. + + Saygılarımızla, + %{user} + %{foodcoop} + reset_password: + subject: '%{username} için yeni şifre' + text: | + Merhaba %{user}, + + Yeni bir şifre istediniz (veya başka birisi istedi). + Yeni bir şifre belirlemek için bu linke tıklayın: %{link} + Bu link sadece bir kez kullanılabilir ve %{expires} tarihinde geçersiz olacaktır. + Eğer şifrenizi değiştirmek istemiyorsanız, bu mesajı görmezden gelebilirsiniz. Şifreniz henüz değiştirilmedi. + + + Saygılarımızla, Foodsoft Ekibi! + upcoming_tasks: + nextweek: 'Gelecek hafta için görevler:' + subject: Görevler teslim edilmeli! + text0: | + Sayın %{user}, + + %{task} görevi size atanmıştır. Bu görev yarın (%{when}) teslim edilmelidir! + text1: | + Görevlerim: %{user_tasks_url} + + + %{foodcoop} adına saygılarımızla. + welcome: + subject: "%{foodcoop} Hoş Geldiniz" + text0: | + Sayın %{user}, + + %{foodcoop} için yeni bir Foodsoft hesabı oluşturuldu. + text1: | + Yeni bir şifre belirlemek için lütfen şu bağlantıyı takip edin: %{link} + Bu bağlantı sadece bir kez kullanılabilir ve %{expires} tarihinde geçerliliğini yitirir. + Her zaman "Şifrenizi mi unuttunuz?" seçeneğini kullanarak yeni bir bağlantı alabilirsiniz. + + + %{foodcoop} adına saygılarımızla. + messages_mailer: + foodsoft_message: + footer: | + Yanıtla: %{reply_url} + Mesajı çevrimiçi görüntüle: %{msg_url} + Mesaj seçenekleri: %{profile_url} + footer_group: | + Gruba gönderildi: %{group} + model: + delivery: + each_stock_article_must_be_unique: Her stok ürünü bir kez listelenmeli. + financial_transaction: + foodcoop_name: Gıda kooperatifi + financial_transaction_type: + no_delete_last: En az bir finansal işlem türü bulunmalıdır. + group_order: + stock_ordergroup_name: Stok (%{user}) + invoice: + invalid_mime: geçersiz bir MIME türüne sahip (%{mime}) + membership: + no_admin_delete: Son kalan yönetici olduğunuz için üyelikten çıkılamaz. + order_article: + error_price: belirtilmeli ve geçerli bir fiyata sahip olmalıdır + user: + no_ordergroup: sıfır sipariş grubu + group_order_article: + order_closed: Sipariş kapatıldı ve değiştirilemez. + navigation: + admin: + config: Konfigürasyon + finance: Finans + home: Genel Bakış + mail_delivery_status: E-posta sorunları + ordergroups: Sipariş Grupları + title: Yönetim + users: Kullanıcılar + workgroups: Çalışma Grupları + articles: + categories: Kategoriler + stock: Stok + suppliers: Tedarikçiler/ürünler + title: Ürünler + dashboard: Kontrol Paneli + finances: + accounts: Hesapları Yönet + balancing: Hesap siparişleri + bank_accounts: Banka Hesapları + home: Genel Bakış + invoices: Faturalar + title: Finanslar + foodcoop: Gıda Kooperatifi + members: Üyeler + ordergroups: Sipariş Grupları + orders: + archive: Benim Siparişlerim + manage: Siparişleri Yönet + ordering: Sipariş Ver! + pickups: Teslim Günleri + title: Siparişler + tasks: Görevler + workgroups: Çalışma Grupları + number: + percentage: + format: + strip_insignificant_zeros: true + order_articles: + edit: + stock_alert: Stok ürünlerinin fiyatı değiştirilemez! + title: Ürünü güncelle + new: + title: Teslim edilen ürünü siparişe ekle + ordergroups: + edit: + title: Sipariş gruplarını düzenle + index: + title: Sipariş grupları + model: + error_single_group: "%{user}, başka bir sipariş grubunun üyesidir" + invalid_balance: geçerli bir sayı değil + orders: + articles: + article_count: 'Sipariş edilen ürünler:' + prices: Net/brüt fiyatı + prices_sum: 'Toplam (net/brüt fiyat):' + units_full: Tam birimler + units_ordered: Sipariş edilen birimler + create: + notice: Sipariş oluşturuldu. + edit: + title: 'Siparişi düzenle: %{name}' + edit_amount: + field_locked_title: Bu ürünün sipariş grupları arasındaki dağılımı manuel olarak değiştirildi. Bu alan, bu değişiklikleri korumak için kilitlidir. Yeni bir dağılım yapmak ve bu değişiklikleri üzerine yazmak için kilidi açın ve miktarı değiştirin. + field_unlocked_title: Bu ürünün sipariş grupları arasındaki dağılımı manuel olarak değiştirildi. Miktarı değiştirirken, bu manuel değişiklikler üzerine yazılacaktır. + edit_amounts: + no_articles_available: Eklenecek ürün yok. + set_all_to_zero: Tümünü sıfıra ayarla + fax: + amount: Miktar + articles: Ürünler + delivery_day: Teslim günü + heading: "%{name} için sipariş" + name: İsim + number: Numara + to_address: Gönderim adresi + finish: + notice: Sipariş kapatıldı. + form: + ignore_warnings: Uyarıları yok say + prices: Fiyatlar (net/FC) + select_all: Hepsini seç + stockit: Stokta + title: Ürün + index: + action_end: Kapat + action_receive: Teslim al + confirm_delete: Siparişi gerçekten silmek istiyor musunuz? + confirm_end: Siparişi gerçekten kapatmak istiyor musunuz %{order}? Geri dönüş yok. + new_order: Yeni sipariş oluştur + no_open_or_finished_orders: Şu anda açık veya kapalı sipariş yok. + orders_finished: Kapatıldı + orders_open: Açık + orders_settled: Düzenlendi + title: Siparişleri yönet + model: + close_direct_message: Üye hesaplarına ücret yansıtılmadan sipariş kapatıldı. + error_boxfill_before_ends: Kutu doldurma tarihi son tarihten önce olmalıdır (veya boş bırakılmalıdır). + error_closed: Sipariş zaten kapatılmış + error_nosel: En az bir ürün seçilmelidir. Ya da belki siparişi silmek istiyor olabilirsiniz? + error_starts_before_boxfill: Başlangıç tarihi kutu doldurma tarihinden sonra olmalıdır (veya boş bırakılmalıdır). + error_starts_before_ends: Başlangıç tarihi bitiş tarihinden sonra olmalıdır (veya boş bırakılmalıdır). + notice_close: '%{name} siparişi, %{ends} kadar.' + stock: Stok + warning_ordered: 'Uyarı: Kırmızı olarak işaretlenen ürünler bu açık siparişte zaten sipariş edildi. Burada işaretini kaldırırsanız, tüm bu ürünlerin mevcut siparişleri silinecektir. Devam etmek için aşağıdaki onaylayın.' + warning_ordered_stock: 'Uyarı: Kırmızı olarak işaretlenen ürünler bu açık stok siparişinde zaten sipariş edildi/satın alındı. Burada seçimleri kaldırırsanız, tüm bu ürünlerin mevcut siparişleri/satı nalımları silinecek ve bunlar hesaba katılmayacaktır. Devam etmek için aşağıda onay verin.' + new: + title: Yeni sipariş oluştur + receive: + add_article: Ürün ekle + consider_member_tolerance: toleransı dikkate al + notice: '%{msg} siparişi alındı.' + notice_none: Teslim alınacak yeni bir ürün yok + paragraph: Sipariş edilen ve alınan miktar aynıysa, ilgili alanlar boş bırakılabilir. Yine de tüm alanların girilmesi iyi olur, çünkü bu, tüm ürünlerin kontrol edildiğinin anlaşılmasını sağlar. + rest_to_stock: stokta kalanlar + submit: Siparişi Al + surplus_options: 'Dağıtım Seçenekleri:' + title: "%{order} Siparişini Al" + send_to_supplier: + notice: Sipariş tedarikçiye gönderildi. + show: + action_end: Kapat! + amounts: 'Net/Brüt toplam:' + articles: Ürün özeti + articles_ordered: 'Sipariş edilen üründür:' + comments: + title: Yorumlar + comments_link: Yorumlar + confirm_delete: Siparişi gerçekten silmek istiyor musunuz? + confirm_end: |- + Siparişi gerçekten kapatmak istiyor musunuz %{order}? + Geri dönüşü yok. + confirm_send_to_supplier: Sipariş %{when} tarihinde zaten tedarikçiye gönderildi. Yeniden göndermek istiyor musunuz? + create_invoice: Fatura Ekle + description1_order: "%{who} tarafından açılan %{supplier} siparişi, %{state}," + description1_period: + pickup: alınabileceği tarih %{pickup} + starts: '%{starts} tarihinden itibaren açık' + starts_ends: '%{starts} tarihinden %{ends} tarihine kadar açık' + description2: "%{ordergroups} %{article_count} adet ürün sipariş verdi, toplam değeri %{net_sum} / %{gross_sum} (net / brüt)." + group_orders: 'Grup siparişleri:' + search_placeholder: + articles: Ürün ara... + default: Arama yap... + groups: Sipariş grupları ara... + search_reset: Aramayı sıfırla + send_to_supplier: Tedarikçiye gönder + show_invoice: Faturayı göster + sort_article: Ürüne göre sırala + sort_group: Gruba göre sırala + stock_order: Stok Siparişi + title: '%{name} Siparişi' + warn_not_closed: Uyarı, sipariş henüz kapatılmadı. + state: + closed: kapatıldı + finished: tamamlandı + open: açık + received: alındı + update: + notice: Sipariş güncellendi. + update_order_amounts: + msg1: "%{count} adet (%{units} birim) güncellendi" + msg2: "%{count} (%{units}) tolerans kullanılarak güncellendi" + msg4: "%{count} (%{units}) fazla kaldı" + pickups: + document: + empty_selection: En az bir sipariş seçmelisiniz. + filename: "%{date} Teslimatı" + invalid_document: Geçersiz belge türü + title: "%{date} Teslimatı" + index: + article_pdf: Ürün PDF'i + group_pdf: Grup PDF'i + matrix_pdf: Matris PDF'i + title: Teslimat günleri + sessions: + logged_in: Giriş yapıldı! + logged_out: Çıkış yapıldı! + login_invalid_email: Geçersiz e-posta adresi veya şifre + login_invalid_nick: Geçersiz kullanıcı adı veya şifre + new: + forgot_password: Şifremi unuttum? + login: Giriş Yap + nojs: Dikkat, çerezlerin ve javascript'in etkinleştirilmesi gerekiyor! Lütfen %{link} kapatın. + noscript: NoScript + title: Foodsoft Girişi + shared: + articles: + ordered: Sipariş edilen + ordered_desc: Üye tarafından sipariş edilen ürün sayısı (miktar + tolerans) + received: Alınan + received_desc: Üye tarafından alınan ürün sayısı + articles_by: + price: Toplam fiyat + price_sum: Toplam + group: + access: Erişim + activated: aktifleştirildi + apple_limit: Elma puanı sipariş sınırı + break: "%{start} - %{end} arası" + deactivated: devre dışı bırakıldı + group_form_fields: + search: Ara ... + search_user: Kullanıcı ara + user_not_found: Kullanıcı bulunamadı + open_orders: + no_open_orders: Şu anda açık sipariş yok + not_enough_apples: Dikkat! Sipariş grubunuzun yeterli miktarda elma puanı bulunmamaktadır. + title: Mevcut siparişler + total_sum: Toplam tutar + who_ordered: Kim sipariş verdi? + order_download_button: + article_pdf: Ürün PDF'i + download_file: Dosya indir + fax_csv: Faks CSV'si + fax_pdf: Faks PDF'i + fax_txt: Faks metni + group_pdf: Grup PDF'i + matrix_pdf: Matris PDF'i + title: İndir + task_list: + accept_task: Görevi kabul et + done: Tamamlandı + done_q: Tamamlandı mı? + mark_done: Görevi tamamlandı olarak işaretle + reject_task: Görevi reddet + who: Kim yapıyor? + who_hint: "(Ne kadarı hala gerekiyor?)" + user_form_fields: + contact_address_hint: Sipariş grubunuzun adresi. Burayı güncellerseniz, diğer üyeler de güncellenir. + messagegroups: Mesaj gruplarına katılın veya ayrılın + workgroup_members: + title: Grup üyelikleri + simple_form: + error_notification: + default_message: Hatalar bulundu. Lütfen formu kontrol edin. + hints: + article: + unit: Örn. KG veya 1L veya 500g + article_category: + description: İçe aktarım/senkronizasyonda tanınan kategori adlarının virgülle ayrılmış listesi + order_article: + units_to_order: Teslim edilen toplam birim miktarını değiştirirseniz, ayrı ayrı grup miktarlarını değiştirmek için ürün adına tıklamanız gerekir. Bunlar otomatik olarak yeniden hesaplanmayacağından, sipariş grupları teslim edilmemiş ürünler için borçlu kalabilirler! + update_global_price: Ayrıca gelecekteki siparişlerin fiyatını da güncelleyin + stock_article: + copy: + name: Lütfen değiştirin + edit_stock_article: + price: "
  • Fiyat değişiklikleri yasaktır.
  • Gerektiğinde, %{stock_article_copy_link}.
" + supplier: + min_order_quantity: Sipariş vermek için gerekli minimum miktar sipariş sırasında gösterilir ve sipariş vermenizi teşvik etmelidir. + task: + duration: Görev ne kadar sürede tamamlanacak, 1-3 saat + required_users: Toplam kaç kullanıcıya ihtiyaç var? + tax: Yüzde olarak, standart 7,0'dır. + labels: + settings: + notify: + negative_balance: Sipariş grubumun negatif bakiyesi olduğunda beni bilgilendir. + order_finished: Siparişim tamamlandığında sipariş sonucum hakkında beni bilgilendir. + order_received: Teslimat detayları hakkında bilgilendirildiğimden emin ol. + upcoming_tasks: Yaklaşan görevler hakkında hatırlatmada bulun. + profile: + email_is_public: E-postam diğer üyeler tarafından görülebilir. + language: Dil + name_is_public: Adım diğer üyeler tarafından görülebilir. + phone_is_public: Telefon numaram diğer üyeler tarafından görülebilir. + settings_group: + messages: Mesajlar + privacy: Gizlilik + 'hayir': 'Hayir' + options: + settings: + profile: + language: + de: Almanca + en: İngilizce + es: İspanyolca + fr: Fransızca + nl: Hollandaca + tr: Türkçe + required: + mark: "*" + text: zorunlu + 'yes': 'Evet' + stock_takings: + create: + notice: Envanter başarıyla oluşturuldu. + edit: + title: Envanteri düzenle + index: + new_inventory: Yeni envanter oluştur + title: Envanter genel bakışı + new: + amount: Miktar + create: oluştur + stock_articles: Stok ürünleri + temp_inventory: geçici envanter + text_deviations: "%{inv_link} için tüm fazla sapmaları doldurunuz. Azaltma için negatif bir sayı kullanın." + text_need_articles: "Burada kullanmadan önce, şuradan yeni bir stok ürün oluşturmanız gerekiyor: %{create_link}" + title: Yeni envanter oluştur + show: + amount: Miktar + article: Ürün + confirm_delete: Envateri silmek istediğinize emin misiniz? + date: Tarih + note: Not + overview: Envanter genel bakışı + supplier: Tedarikçi + title: Envanteri göster + unit: Birim + stock_takings: + confirm_delete: Silmek istediğinize emin misiniz? + date: Tarih + note: Not + update: + notice: Envanter güncellendi. + stockit: + check: + not_empty: "%{name} silinemedi, envanter sıfır değil." + copy: + title: Stok ürünü kopyala + create: + notice: Yeni stok ürünü "%{name}" oluşturuldu. + derive: + title: Şablon kullanarak stok ürünü ekle + destroy: + notice: "%{name} ürünü silindi." + edit: + title: Stok ürünlerini düzenle + form: + copy_stock_article: stok ürünü kopyala + price_hint: Karmaşayı önlemek için, mevcut stok ürünlerinin fiyatlarını şimdilik düzenlemek mümkün değildir. + index: + confirm_delete: Silmek istediğinizden emin misiniz? + new_delivery: Yeni teslimat ... + new_stock_article: Yeni stok ürünü ekle + new_stock_taking: Envanter ekle + order_online: Stok siparişini çevrimiçi olarak ver + show_stock_takings: Envanter genel bakışı + stock_count: 'Ürün sayısı:' + stock_worth: 'Geçerli stok değeri:' + title: Stok (%{article_count}) + toggle_unavailable: Kullanılamayan ürünleri göster/gizle + view_options: Görünüm seçenekleri + new: + search_text: 'Tüm kataloglarda ürün arayın:' + title: Yeni stok ürünü ekle + show: + change_quantity: Değiştir + datetime: Zaman + new_quantity: Yeni miktar + reason: Sebep + stock_changes: Stok miktarı değişiklikleri + update: + notice: Stok ürünü %{name} kaydedildi. + suppliers: + create: + notice: Tedarikçi oluşturuldu + destroy: + notice: Tedarikçi silindi + edit: + title: Tedarikçi düzenle + index: + action_import: Dış veritabanından tedarikçi içe aktar + action_new: Yeni tedarikçi oluştur + articles: ürünler (%{count}) + confirm_del: "%{name} tedarikçisini gerçekten silmek istiyor musunuz?" + deliveries: teslimatlar (%{count}) + stock: stokta (%{count}) + title: Tedarikçiler + new: + title: Yeni tedarikçi + shared_supplier_methods: + all_available: Tüm ürünler (yeni mevcut) + all_unavailable: Tüm ürünler (yeni mevcut değil) + import: İçe aktarılacak ürünleri seçin + shared_supplier_note: Tedarikçi harici veritabanına bağlıdır. + shared_suppliers: + body: "

Harici veritabanındaki tedarikçiler burada görüntülenir.

Dış tedarikçileri abone olarak içe aktarabilirsiniz (aşağıya bakın).

Yeni bir tedarikçi oluşturulacak ve harici veritabanına bağlanacaktır.

" + subscribe: Abone ol + subscribe_again: Tekrar abone ol + supplier: Tedarikçi + title: Harici listeler + show: + last_deliveries: Son teslimatlar + last_orders: Son siparişler + new_delivery: Yeni teslimat + show_deliveries: Tümünü göster + update: + notice: Tedarikçi güncellendi + tasks: + accept: + notice: Görevi kabul ettiniz + archive: + title: Görev arşivi + create: + notice: Görev oluşturuldu + destroy: + notice: Görev silindi + edit: + submit_periodic: Tekrarlayan görevi kaydet + title: Görevi düzenle + title_periodic: Tekrarlayan görevi düzenle + warning_periodic: "Uyarı: Bu görev, tekrar eden görevler grubunun bir parçasıdır. Kaydedildiğinde gruptan çıkarılacak ve bir normal göreve dönüştürülecektir." + error_not_found: Hiçbir çalışma grubu bulunamadı + form: + search: + hint: Kullanıcı ara + noresult: Kullanıcı bulunamadı + placeholder: Ara ... + submit: + periodic: Tekrarlayan görevi kaydet + index: + show_group_tasks: Grup görevlerini göster + title: Görevler + title_non_group: Tüm görevler + nav: + all_tasks: Tüm görevler + archive: Tamamlanmış görevler (arşiv) + group_tasks: Grup görevleri + my_tasks: Görevlerim + new_task: Yeni görev oluştur + pages: Sayfalar + new: + submit_periodic: Yinelenen görev oluştur + title: Yeni görev oluştur + repeated: Görev yineleniyor + set_done: + notice: Görev durumu güncellendi + show: + accept_task: Görevi kabul et + confirm_delete_group: Bu ve sonraki tüm görevleri gerçekten silmek istiyor musunuz? + confirm_delete_single: Görevi silmek istediğinizden emin misiniz? + confirm_delete_single_from_group: Bu görevi (ilişkili yinelenen görevleri tutarak) silmek istediğinizden emin misiniz? + delete_group: Görevi ve takip edenleri sil + edit_group: Tekrarlayanı düzenle + hours: "%{count}s" + mark_done: Görevi tamamlandı olarak işaretle + reject_task: Görevi reddet + title: Görevi göster + update: + notice: Görev güncellendi. + notice_converted: Görev güncellendi ve tekrarlanmayan bir göreve dönüştürüldü. + user: + more: Yapacak hiçbir şey yok mu? %{tasks_link} kesinlikle görevler var. + tasks_link: Burada + title: Görevlerim + title_accepted: Kabul edilen görevler + title_open: Açık görevler + workgroup: + title: '%{workgroup} için görevler' + title_all: Tüm grup görevleri + ui: + actions: İşlemler + back: Geri + cancel: İptal + close: Kapat + confirm_delete: "%{name} silmek istediğinizden emin misiniz?" + confirm_restore: "%{name} geri yüklemek istediğinizden emin misiniz?" + copy: Kopyala + delete: Sil + download: İndir + edit: Düzenle + marks: + close: "×" + success: + move: Taşı + or_cancel: ya da iptal + please_wait: Lütfen bekleyin... + restore: Geri yükle + save: Kaydet + search_placeholder: Ara... + show: Göster + views: + pagination: + first: "«" + last: "»" + next: "›" + previous: "‹" + truncate: "..." + workgroups: + edit: + title: Çalışma grubunu düzenle + error_last_admin_group: Yönetici hakları olan son grup silinemez + error_last_admin_role: Yönetici hakları olan son gruptan yönetici rolü geri alınamaz + index: + title: Çalışma Grupları + update: + notice: Çalışma grubu güncellendi. + \ No newline at end of file diff --git a/plugins/current_orders/config/locales/tr.yml b/plugins/current_orders/config/locales/tr.yml new file mode 100644 index 00000000..7087f541 --- /dev/null +++ b/plugins/current_orders/config/locales/tr.yml @@ -0,0 +1,73 @@ +tr: + config: + hints: + use_current_orders: Şimdiki_siparişler eklentisini aktif et. Sipariş izni olan üyelerin, Siparişler menüsünde üç yeni ekran kullanarak birden çok siparişte üye miktarlarını değiştirmesine olanak tanır. Özellikle teslim alma günleri için faydalıdır. + keys: + use_current_orders: Ekstra dağıtım ekranları + current_orders: + articles: + article: + counts: '%{ordergroups} sipariş grubu, %{articles} farklı ürün sipariş etti.' + no_selection: Hangi üyenin ne sipariş ettiğini görmek için bir ürün seçin, veya sağ taraftan toplama listelerini indirin. + article_info: + origin_in: '%{origin} içinde' + supplied_by: '%{supplier} tarafından' + supplied_and_made_by: '%{manufacturer} tarafından yapıldı' + supplied_by_made_by: '%{supplier} tarafından sağlandı, %{manufacturer} tarafından yapıldı' + unit: '%{unit} başına' + from: '%{supplier} tarafından' + form: + article_placeholder: Ürünleri arayın... + current_orders: Tüm güncel siparişler + index: + title: Ürünleri dağıt + ordergroups: + piece: adet + unit: birim + add_new: Bir sipariş grubu ekleyin... + show: + title: ! '%{name}' + navigation: + receive: Teslim al + articles: Dağıtım + ordergroups: Üye siparişleri + group_orders: + index: + title: Mevcut siparişleriniz + ordergroups: + articles: + add_new: Yeni bir ürün ekleyin... + no_selection: Hangi sipariş grubunu göstermek istediğinizi seçin. + form: + ordergroup_placeholder: Bir sipariş grubu seçin... + index: + title: Ürünler - sipariş grubu için + payment_bar: + account_balance: Hesap bakiyesi + new_pin: PIN + new_transaction: Yeni işlem + payment: ! 'Ödeme:' + show: + title: '%{name} adına ürünler' + orders: + receive: + title: Siparişleri al + no_finished_orders: Şu anda alınacak sipariş yok. + documents: + multiple_orders_by_articles: + filename: Ürüne göre mevcut siparişler + title: Mevcut siparişler - ürüne göre + multiple_orders_by_groups: + filename: Gruba göre mevcut siparişler + title: Mevcut siparişler - gruba göre + helpers: + current_orders: + pay_done: Tamamen ödendi + pay_none: Ödenecek bir şey yok + pay_amount: Ödenecek tutar %{amount} + js: + current_orders: + articles: + above: '%{count} adet mevcut stoktan fazla' + below: '%{count} adet mevcut stokta kaldı' + equal: Tümü dağıtıldı diff --git a/plugins/discourse/config/locales/tr.yml b/plugins/discourse/config/locales/tr.yml new file mode 100644 index 00000000..0cb231c8 --- /dev/null +++ b/plugins/discourse/config/locales/tr.yml @@ -0,0 +1,6 @@ +tr: + discourse: + callback: + invalid_nonce: Geçersiz nonce + invalid_signature: Geçersiz imza + logged_in: Giriş yapıldı! diff --git a/plugins/documents/config/locales/tr.yml b/plugins/documents/config/locales/tr.yml new file mode 100644 index 00000000..05003fdf --- /dev/null +++ b/plugins/documents/config/locales/tr.yml @@ -0,0 +1,40 @@ +tr: + activerecord: + attributes: + document: + created_at: Oluşturulma tarihi + created_by: Tarafından oluşturuldu + data: Veri + mime: MIME türü + name: Ad + config: + hints: + documents_allowed_extension: İzin verilen dosya uzantılarının boşluklarla ayrılmış bir listesi. + use_documents: Gıda kooperatifi menüsüne temel bir belge paylaşım sayfası ekleyin. + keys: + documents_allowed_extension: İzin verilen uzantılar + use_documents: Belgeleri etkinleştir + navigation: + documents: Belgeler + documents: + create: + error: 'Belge veya klasör oluşturulamadı: %{error}' + not_allowed_mime: '"%{mime}" dosya türüne izin verilmiyor. Lütfen bunu beyaz listeye almak için bir yöneticiyle iletişime geçin.' + notice: Belge veya klasör oluşturuldu + destroy: + error: 'Belge veya klasör silinemedi: %{error}' + no_right: Belgeyi veya klasörü silmek için yeterli yetkiniz yok + notice: Belge veya klasör silindi + form: + new: Yeni belge + new_folder: Yeni klasör + submit: Oluştur + index: + new: Yeni belge yükle + new_folder: Yeni klasör oluştur + title: Belgeler + move: + root_folder: Başlangıç + title: Taşı + update: + notice: Belge veya klasör taşındı diff --git a/plugins/links/config/locales/tr.yml b/plugins/links/config/locales/tr.yml new file mode 100644 index 00000000..2de2dc20 --- /dev/null +++ b/plugins/links/config/locales/tr.yml @@ -0,0 +1,23 @@ +tr: + activerecord: + attributes: + link: + name: Adı + url: URL + workgroup: Çalışma Grubu + indirect: Dolaylı + authorization: Yetkilendirme Başlığı + admin: + links: + index: + title: Linkler + new_link: Yeni link ekle + form: + description: Bir çalışma grubu seçildiğinde, link yalnızca grubun üyelerine görünür. 'Dolaylı' seçeneği, Foodsoft'un kullanıcıları URL tarafından döndürülen adrese yönlendirmesine izin verir. Bu seçenek, yalnızca tam işlevselliği doğru anlaşıldığında etkinleştirilmelidir. + links: + show: + indirect_no_location: Yapılandırılmış URL, yönlendirme için bir Konum başlığı döndürmedi. + navigation: + admin: + links: Linkler + links: Linkler diff --git a/plugins/messages/config/locales/tr.yml b/plugins/messages/config/locales/tr.yml new file mode 100644 index 00000000..002d1c9b --- /dev/null +++ b/plugins/messages/config/locales/tr.yml @@ -0,0 +1,156 @@ +tr: + activerecord: + attributes: + message: + body: Mesaj İçeriği + messagegroup_id: Mesaj Grubu + order_id: Sipariş + ordergroup_id: Sipariş Grubu + private: Özel + recipient_tokens: (Ek) alıcılar + send_method: + all: Tüm üyelere gönder + recipients: Belirli üyelere gönder + order: Bir siparişe katılan üyelere gönder + ordergroup: Bir sipariş grubunun üyelerine gönder + messagegroup: Bir mesaj grubunun üyelerine gönder + workgroup: Bir iş grubunun üyelerine gönder + send_to_all: Tüm üyelere gönder + subject: Konu + workgroup_id: İş Grubu + messagegroup: + description: Açıklama + name: Ad + user_tokens: Üyeler + models: + message: Mesaj + messagegroup: Mesaj grubu + admin: + ordergroups: + show: + send_message: Mesaj gönder + users: + show: + send_message: Mesaj gönder + config: + hints: + mailing_list: Tüm üyelere için mesajlaşma sistemi yerine kullanılabilecek posta listesi e-posta adresi. + mailing_list_subscribe: Üyelerin abone olmak için bir e-posta gönderebileceği e-posta adresi. + use_messages: Üyelerin Foodsoft içinde birbirleriyle iletişim kurmasına izin ver. + keys: + use_messages: Mesajlar + mailing_list: Posta Listesi + mailing_list_subscribe: Posta Listesi Aboneliği + helpers: + messages: + write_message: Mesaj yaz + submit: + message: + create: Mesaj gönder + home: + index: + messages: + title: En yeni mesajlar + view_all: + text: '%{messages} veya %{threads} göster' + messages: tüm mesajlar + threads: konular + start_nav: + write_message: Mesaj yaz + messagegroups: + index: + body: 'Bir mesaj grubu, bir posta listesi gibi: o gruba üye olabilir (veya çıkabilir) ve o gruba gönderilen güncellemeleri alabilirsiniz.' + title: Mesaj grupları + join: + error: 'Mesaj grubuna katılamadı: %{error}' + notice: Mesaj grubuna katıldınız + leave: + error: 'Mesaj grubu terk edilemedi: %{error}' + notice: Mesaj grubundan ayrıldınız + messagegroup: + join: Mesaj grubuna katıl + leave: Mesaj grubundan ayrıl + messages: + actionbar: + message_threads: Konu olarak göster + messagegroups: Gruplara abone ol + messages: Liste olarak göster + new: Yeni mesaj + create: + notice: Mesaj kaydedildi ve gönderilecek. + index: + title: Mesajlar + messages: + reply: Yanıtla + model: + reply_header: ! '%{user} %{when} tarihinde yazdı:' + reply_indent: ! '> %{line}' + reply_subject: ! 'Yanıt: %{subject}' + new: + error_private: Üzgünüz, bu mesaj özel. + hint_private: Mesaj Foodsoft posta kutusunda gösterilmez. + list: + desc: ! 'Lütfen tüm mesajları şu mailing-liste gönderin: %{list}' + mail: örneğin %{email} adresine bir e-posta ile. + subscribe: 'E-posta listesi hakkında daha fazla bilgi edinebilirsiniz: %{link}.' + subscribe_msg: Önce e-posta listesine kaydolmanız gerekebilir. + wiki: Wiki (page Posta-listesi) + message: mesaj + no_user_found: Kullanıcı bulunamadı. + order_item: "%{supplier_name} (Pickup: %{pickup})" + reply_to: Bu mesaj, başka bir %{link} yanıtıdır. + search: Ara ... + search_user: Kullanıcı ara + title: Yeni mesaj + show: + all_messages: Tüm mesajlar + change_visibility: 'Değiştir' + from: ! 'Kimden:' + group: 'Grup:' + reply: Yanıtla + reply_to: 'Yanıtla:' + sent_on: ! 'Gönderildi:' + subject: ! 'Konu:' + title: Mesajı Göster + to: 'Kime:' + visibility: 'Görünürlük:' + visibility_private: 'Özel' + visibility_public: 'Genel' + thread: + all_message_threads: Tüm mesaj konuları + reply: Yanıtla + toggle_private: + not_allowed: Mesajın görünürlüğünü değiştiremezsiniz. + message_threads: + groupmessage_threads: + show_message_threads: tümünü göster + index: + general: Genel + title: Mesaj Konuları + message_threads: + last_reply_at: Son yanıt tarihi + last_reply_by: Son yanıtlayan + started_at: Başlangıç tarihi + started_by: Başlatan + show: + general: Genel + messages_mailer: + foodsoft_message: + footer: | + Yanıt: %{reply_url} + Mesajı çevrimiçi görüntüle: %{msg_url} + Mesajlaşma seçenekleri:: %{profile_url} + footer_group: | + Gruba gönderildi: %{group} + navigation: + admin: + messagegroups: Mesaj grupları + messages: Mesajlar + shared: + user_form_fields: + messagegroups: Mesaj gruplarına katıl veya ayrıl + simple_form: + labels: + settings: + messages: + send_as_email: Mesajları e-posta olarak al diff --git a/plugins/polls/config/locales/tr.yml b/plugins/polls/config/locales/tr.yml new file mode 100644 index 00000000..ed2ea4c0 --- /dev/null +++ b/plugins/polls/config/locales/tr.yml @@ -0,0 +1,67 @@ +tr: +activerecord: + attributes: + poll: + choices: Seçenekler + created_at: Oluşturulma tarihi + created_by: Oluşturan + description: Açıklama + ends: Bitiş tarihi + name: Adı + max_points: Maksimum puan + min_points: Minimum puan + multi_select_count: Maksimum seçim sayısı + one_vote_per_ordergroup: Sadece bir oylama her sipariş grubu için + starts: Başlangıç tarihi + voting_method: Oylama yöntemi + voting_methods: + event: Etkinlik + single_select: Tek seçim + multi_select: Birden fazla seçim + points: Puanlar + resistance_points: Direniş puanları + poll_vote: + name: Adı + note: Not + updated_at: Son güncelleme + models: + poll: Oylama +config: + hints: + use_polls: Basit anketleri etkinleştirin. + keys: + use_polls: Anketleri etkinleştirin +navigation: + polls: Anketler +polls: + choice: + remove: Sil + create: + error: 'Anket oluşturulamadı: %{error}' + notice: Anket oluşturuldu + edit: + title: Anketi düzenle + form: + already_voted: Oylama yapıldığından seçenekler değiştirilemez. + new_choice: Yeni seçenek + required_ordergroup_custom_field: 'Sipariş grubunun ''%{label}'' alanı boş olamaz.' + required_user_custom_field: 'Kullanıcının ''%{label}'' alanı boş olamaz.' + destroy: + error: 'Anket silinemedi: %{error}' + no_right: Bu anketi silme izniniz yok + notice: Anket silindi + index: + new_poll: Yeni anket + title: Anketler + new: + title: Yeni anket + polls: + vote: Oyla + show: + vote: Oyla + update: + error: 'Anket güncellenemedi: %{error}' + notice: Anket güncellendi + vote: + submit: Oyla + no_right: Bu ankete katılamazsınız diff --git a/plugins/printer/config/locales/tr.yml b/plugins/printer/config/locales/tr.yml new file mode 100644 index 00000000..93346360 --- /dev/null +++ b/plugins/printer/config/locales/tr.yml @@ -0,0 +1,32 @@ +tr: + config: + keys: + printer_print_order_articles: Sipariş makbuzu PDF'lerini yazdır + printer_print_order_fax: Fax PDF'lerini yazdır + printer_print_order_groups: Grup PDF'lerini yazdır + printer_print_order_matrix: Matris PDF'lerini yazdır + printer_token: Gizli token + use_printer: Yazıcı kullan + helpers: + submit: + printer_job: + create: Yazıcı görevi oluştur + navigation: + orders: + printer_jobs: Yazıcı görevleri + orders: + show: + confirm_create_printer_job: Bu sipariş için bir yazıcı görevi zaten oluşturuldu. Yeni bir tane oluşturmak istiyor musunuz? + printer_jobs: + create: + notice: '%{count} yazıcı görevi oluşturuldu.' + destroy: + notice: Yazıcı görevi silindi. + index: + finished: Tamamlandı + pending: Beklemede + queued: Siparişin kapatılması bekleniyor + requeued: Yeniden sıraya konuldu + title: Yazıcı görevleri + show: + title: Yazıcı görevi %{id} diff --git a/plugins/wiki/config/locales/tr.yml b/plugins/wiki/config/locales/tr.yml new file mode 100644 index 00000000..077c08e1 --- /dev/null +++ b/plugins/wiki/config/locales/tr.yml @@ -0,0 +1,107 @@ +tr: + activerecord: + attributes: + page: + body: İçerik + parent_id: Üst sayfa + title: Başlık + config: + hints: + use_wiki: Düzenlenebilir wiki sayfalarını etkinleştirin. + keys: + use_wiki: Wiki'yi etkinleştir + model: + page: + redirect: '[[%{title}]] sayfasına yönlendiriliyor...' + navigation: + wiki: + all_pages: Tüm Sayfalar + home: Ana Sayfa + title: Wiki + pages: + all: + new_page: Yeni sayfa oluştur + recent_changes: Son değişiklikler + search: + action: Ara + placeholder: Sayfa başlığı .. + site_map: Site Haritası + title: Tüm Wiki sayfaları + title_list: Sayfa listesi + body: + title_toc: İçindekiler + wikicloth_exception: 'Üzgünüm, wiki sayfasını yorumlarken bir hata oluştu: %{msg}. Lütfen düzeltin ve sayfayı tekrar kaydedin.' + create: + notice: Sayfa oluşturuldu. + cshow: + error_noexist: Sayfa mevcut değil! + redirect_notice: '%{page} sayfasından yönlendirildi ...' + destroy: + notice: Sayfa '%{page}' ve tüm alt sayfaları başarıyla silindi. + diff: + title: "%{title} - %{old} versiyonundan %{new} versiyonuna değişiklikler" + edit: + title: Sayfayı düzenle + error_stale_object: Uyarı, sayfa başka biri tarafından düzenlendi. Lütfen tekrar deneyin. + form: + help: + bold: kalın + external_link_ex: Dış bağlantı + external_links: Dış + heading: '%{level}. düzey' + headings: Başlık + image_link_title: Resim başlığı + image_links: Resimler + italic: italik + link_lists: Liste hakkında daha fazlası + link_table: Tablo biçimlendirmesi + link_templates: Şablonlar + link_variables: Foodsoft değişkenleri + list_item_1: İlk liste maddesi + list_item_2: İkinci liste maddesi + noformat: Biçimlendirme yok + ordered_list: Numaralı liste + section_block: Blok biçimlendirmesi + section_character: Karakter biçimlendirmesi + section_link: Bağlantı biçimlendirmesi + section_more: Daha fazla konu + section_table: Tablo biçimlendirmesi + see_tables: '%{tables_link} göz atın' + tables_link: Tablolar + text: metin + title: Hızlı biçimlendirme yardımı + unordered_list: Madde listesi + wiki_link_ex: Foodsoft Wiki Sayfası + wiki_links: Wiki bağlantıları + preview: Önizleme + last_updated: Son güncelleme + new: + title: Yeni bir wiki sayfası oluşturun + page_list_item: + date_format: ! '%d %B %Y, %H:%M:%S' + show: + date_format: ! '%d-%m-%Y %H:%M' + delete: Sayfayı sil + delete_confirm: Tüm alt sayfaların da silineceği uyarısını dikkate alarak devam etmek istediğinizden emin misiniz? + diff: Versiyonları karşılaştır + edit: Sayfayı düzenle + last_updated: Son güncelleme %{when} tarihinde %{user} tarafından yapıldı + subpages: alt sayfalar + title_versions: Versiyonlar + versions: Versiyonlar (%{count}) + title: Başlık + update: + notice: Sayfa güncellendi + variables: + description: Değişkenler bilgiyi başka bir yerden getirir. Değişkeni kullandığınızda, görüntülendiğinde değeriyle değiştirilir. Foodsoft'un adınız, adresiniz, yazılım sürümü ve üye ve tedarikçi sayısı gibi birçok önceden tanımlanmış değişkeni vardır. Tüm değişkenler için aşağıdaki tabloya bakın. Bunları wiki sayfalarında ve ayak kısımlarında (yapılandırma ekranından) kullanabilirsiniz. + title: Foodsoft değişkenleri + value: Güncel değer + variable: Değişken + version: + author: 'Yazar: %{user}' + date_format: ! '%a, %d-%m-%Y, %H:%M' + revert: Bu sürüme geri dön + title: ! '%{title} - sürüm %{version}' + title_version: Sürüm + view_current: Şu anki sürümü gör + From 8604e27fe9e64175d83ab97c138c1dadd12e70af Mon Sep 17 00:00:00 2001 From: hamaryns Date: Fri, 21 Apr 2023 18:18:39 +0200 Subject: [PATCH 043/105] Spelfouten, maar ook verbeteringen in Nederlands (#954) * Spelfouten, maar ook verbeteringen in Nederlands Correct spelling errors and improvements of Dutch * Update nl.yml * Update nl.yml some more Dutch improvements --- config/locales/nl.yml | 160 +++++++++++++++++++++--------------------- 1 file changed, 80 insertions(+), 80 deletions(-) diff --git a/config/locales/nl.yml b/config/locales/nl.yml index cacf4a7e..384e8839 100644 --- a/config/locales/nl.yml +++ b/config/locales/nl.yml @@ -6,19 +6,19 @@ nl: availability: Artikel leverbaar? availability_short: leverb. deposit: Statiegeld - fc_price: Foodcoop prijs - fc_price_desc: Prijs inclusief belasting, statiegeld en foodcoop marge. - fc_price_short: FC prijs - fc_share: Foodcoop marge - fc_share_short: FC marge - gross_price: Bruto prijs + fc_price: Foodcoop-prijs + fc_price_desc: Prijs inclusief belasting, statiegeld en foodcoop-marge. + fc_price_short: FC-prijs + fc_share: Foodcoop-marge + fc_share_short: FC-marge + gross_price: Brutoprijs manufacturer: Producent name: Naam note: Notitie order_number: Artikelnummer order_number_short: Nr. origin: Herkomst - price: Netto prijs + price: Nettoprijs supplier: Leverancier tax: BTW unit: Eenheid @@ -26,11 +26,11 @@ nl: unit_quantity_short: Colli units: Eenheden article_category: - description: Import namen + description: Namen importeren name: Naam article_price: deposit: Statiegeld - price: Netto prijs + price: Nettoprijs tax: BTW unit_quantity: Colligrootte bank_account: @@ -69,7 +69,7 @@ nl: financial_transaction_type: bank_account: Bankrekening name: Naam - financial_transaction_class: Financiële transactie klasse + financial_transaction_class: Klasse financiële transactie name_short: Verkorte naam group_order: ordergroup: Huishouden @@ -89,7 +89,7 @@ nl: created_at: Gemaakt op created_by: Gemaakt door date: Factuurdatum - delete_attachment: Verwijder bijlage + delete_attachment: Bijlage verwijderen deliveries: Voorraad levering deposit: Statiegeld in rekening gebracht deposit_credit: Statiegeld teruggekregen @@ -102,7 +102,7 @@ nl: supplier: Leverancier mail_delivery_status: created_at: Tijd - email: Email + email: E-mail message: Bericht order: boxfill: Dozen vullen na @@ -111,8 +111,8 @@ nl: end_action: Sluitingsactie end_actions: auto_close: Bestelling sluiten - auto_close_and_send: Sluit de bestelling en verzend het naar de leverancier - auto_close_and_send_min_quantity: Sluit de bestelling en verzend het naar de leverancier als de minimum bestelhoeveelheid gehaald is + auto_close_and_send: De bestelling sluiten en naar de leverancier verzenden + auto_close_and_send_min_quantity: De bestelling sluiten en naar de leverancier verzenden als de minimale bestelhoeveelheid gehaald is no_end_action: Geen automatische actie ends: Sluit op name: Leverancier @@ -124,10 +124,10 @@ nl: transport: Vervoerskosten transport_distribution: Verdeling van vervoerskosten transport_distributions: - articles: Verdeel de kosten over het aantal ontvangen artikelen - ordergroup: Ieder huishouden betaald hetzelfde bedrag - price: Verdeel de kosten over het orderbedrag - skip: Verdeel de kosten niet + articles: De kosten over het aantal ontvangen artikelen verdelen + ordergroup: Ieder huishouden betaalt hetzelfde bedrag + price: De kosten over het orderbedrag verdelen + skip: De kosten niet verdelen updated_by: Laatst aangepast door order_article: article: Artikel @@ -141,7 +141,7 @@ nl: units_to_order_short: Besteld update_global_price: Huidige prijs overal bijwerken order_comment: - text: Commentaar voor deze bestelling toevoegen ... + text: Commentaar voor deze bestelling toevoegen… ordergroup: account_balance: Tegoed available_funds: Beschikbaar tegoed @@ -173,7 +173,7 @@ nl: customer_number: Klantnummer customer_number_short: Klantnr. delivery_days: Bezorgdagen - email: Email + email: E-mail fax: Fax iban: IBAN is_subscribed: geabonneerd? @@ -198,11 +198,11 @@ nl: user_list: Verantwoordelijken workgroup: Werkgroep user: - email: Email + email: E-mail first_name: Voornaam iban: IBAN last_activity: Laatst actief - last_login: Laatste login + last_login: Laatste aanmelding last_name: Achternaam name: Naam nick: Gebruikersnaam @@ -246,11 +246,11 @@ nl: bank_transaction: Banktransactie delivery: Levering financial_transaction: Financiële transactie - financial_transaction_class: Financiële transactie klasse - financial_transaction_type: Financiële transactie type + financial_transaction_class: Klasse financiële transactie + financial_transaction_type: Type financiële transactie invoice: Factuur order: Bestelling - order_article: Bestellingsartikel + order_article: Bestelartikel order_comment: Commentaar ordergroup: one: Huishouden @@ -269,8 +269,8 @@ nl: all_users: Alle gebruikers all_workgroups: Alle werkgroepen created_at: gemaakt op - first_paragraph: Hier kun je de groepen en gebruiker van Foodsoft beheren. - groupname: Groepnaam + first_paragraph: Hier kun je de groepen en gebruikers van Foodsoft beheren. + groupname: Groepsnaam members: leden name: naam new_ordergroup: Nieuw huishouden @@ -290,14 +290,14 @@ nl: submit: Opslaan title: Configuratie tab_layout: - pdf_title: PDF documenten + pdf_title: PDF-documenten tab_messages: - emails_title: Emailinstellingen + emails_title: E-mailinstellingen tab_payment: schedule_title: Bestelrooster tab_security: default_roles_title: Toegang tot - default_roles_paragraph: Ieder lid van de foodcoop heeft standaard toegang tot de volgende onderdelen. + default_roles_paragraph: "Ieder lid van de foodcoop heeft standaard toegang tot de volgende onderdelen:" tab_tasks: periodic_title: Periodieke taken tabs: @@ -308,29 +308,29 @@ nl: finances: index: bank_accounts: Bankrekeningen - first_paragraph: Hier kunt u de financiële transactieklassen en de bijbehorende financiële transactietypes beheren. Elke financiële transactie heeft een type, die je bij elke transactie moet selecteren, als je meer dan één type hebt gemaakt. De financiële transactieklassen kunnen worden gebruikt om de financiële transactietypes te groeperen en zullen worden weergegeven als extra kolommen in het rekeningoverzicht, als er meer dan één is gecreëerd. + first_paragraph: Hier kunt u de klassen van financiële transacties en de bijbehorende typen financiële transacties beheren. Elke financiële transactie heeft een type, die je bij elke transactie moet selecteren, als je meer dan één type hebt gemaakt. De klassen financiële transacties kunnen worden gebruikt om de types financiële transacties te groeperen en zullen worden weergegeven als extra kolommen in het rekeningoverzicht, als er meer dan één is gecreëerd. new_bank_account: Nieuwe bankrekening toevoegen - new_financial_transaction_class: Nieuwe financiële transactie klasse toevoegen + new_financial_transaction_class: Nieuwe klasse voor financiële transacties toevoegen title: Financiën - transaction_types: Financiële transactie typen + transaction_types: Typen financiële transacties transaction_types: name: Naam - new_financial_transaction_type: Nieuw financiëel transactie type toevoegen + new_financial_transaction_type: Nieuw type financiële transactie toevoegen financial_transaction_classes: form: - title_edit: Financiële transactie klasse bewerken - title_new: Nieuw financiëel transactie type toevoegen + title_edit: Klasse voor financiële transactie bewerken + title_new: Nieuw type financiële transactie toevoegen financial_transaction_types: form: - name_short_desc: De korte naam is verplicht voor financiële transactietypes die automatisch moeten kunnen worden toegewezen bij banktransacties. Als er meerdere bankrekeningen zijn, kan de voorkeursrekening voor bankoverschrijvingen worden geselecteerd. - title_edit: Financiëel transactie type bewerken - title_new: Nieuw financiëel transactie type toevoegen + name_short_desc: De korte naam is verplicht voor typen financiële transacties die automatisch moeten kunnen worden toegewezen bij banktransacties. Als er meerdere bankrekeningen zijn, kan de voorkeursrekening voor bankoverschrijvingen worden geselecteerd. + title_edit: Type financiële transactie bewerken + title_new: Nieuw type financiëel transactie toevoegen mail_delivery_status: destroy_all: - notice: Alle emailproblemen zijn verwijderd + notice: Alle e-mailproblemen zijn verwijderd index: - destroy_all: Alle emailproblemen verwijderen - title: Emailproblemen + destroy_all: Alle e-mailproblemen verwijderen + title: E-mailproblemen ordergroups: destroy: error: 'Huishouden kon niet als verwijderd gemarkeerd worden: %{error}' @@ -342,9 +342,9 @@ nl: here: hier index: first_paragraph: Hier kun je %{url} toevoegen, bewerken en verwijderen. - new_ordergroup: Nieuw huishouden + new_ordergroup: een nieuw huishouden new_ordergroups: nieuwe huishoudens - second_paragraph: 'Bedenk het onderscheid tussen werkgroep en huishouden: een huishouden heeft een rekening en kan bestellen. in een %{url} (bijv. sorteergroep) werken leden samen om taken te vervullen. Leden kunnen slechts lid zijn van éen huishouden, maar van meerdere werkgroepen.' + second_paragraph: 'Let op het onderscheid tussen werkgroep en huishouden: een huishouden heeft een rekening en kan bestellen; in een %{url} (bijv. sorteergroep) werken leden samen om taken te vervullen. Leden kunnen slechts lid zijn van één huishouden, maar van meerdere werkgroepen.' title: Huishoudens workgroup: werkgroep new: @@ -353,39 +353,39 @@ nl: confirm: Weet je het zeker? edit: Groep/leden bewerken title: Huishouden %{name} - search_placeholder: naam ... + search_placeholder: naam… users: controller: - sudo_done: Je bent nu ingelogd als %{user}. Wees voorzichtig, en vergeet niet uit te loggen als je klaar bent! + sudo_done: Je bent nu aangemeld als %{user}. Wees voorzichtig, en vergeet niet af te melden als je klaar bent! destroy: error: 'Gebruiker kon niet verwijderd worden: %{error}' notice: Gebruiker is verwijderd edit: title: Lid bewerken form: - create_ordergroup: Maak een huishouden met dezelfde naam en voeg een gebruiker toe. - send_welcome_mail: Verstuur een welkomstmail naar de gebruiker. + create_ordergroup: Een huishouden met dezelfde naam aanmaken en een gebruiker toevoegen. + send_welcome_mail: Een welkomstmail naar de gebruiker versturen. index: first_paragraph: Hier kun je gebruikers %{url}, bewerken en wissen. new_user: Nieuwe gebruiker new_users: toevoegen show_deleted: Verwijderde gebruikers tonen - title: Gebruikers admin + title: Gebruikersbeheer new: title: Nieuwe gebruiker toevoegen restore: error: 'Gebruiker kon niet opnieuw actief gemaakt worden: %{error}' notice: Gebruiker is opnieuw actief show: - confirm_sudo: Als je doorgaat, neem je de identiteit aan van gebruiker %{user}. Vergeet hierna niet uit te loggen! + confirm_sudo: Als je doorgaat, neem je de identiteit aan van gebruiker %{user}. Vergeet hierna niet af te melden! groupabos: Groepslidmaatschappen member_since: Lid sinds %{time} person: Persoon preference: Voorkeuren - show_email_problems: Bekijk emailproblemen - sudo: Inloggen als + show_email_problems: E-mailproblemen bekijken + sudo: Aanmelden als users: - show_email_problems: Bekijk emailproblemen + show_email_problems: E-mailproblemen bekijken workgroups: destroy: error: 'Werkgroep kon niet verwijderd worden: %{error}' @@ -400,7 +400,7 @@ nl: new_workgroup: Nieuwe werkgroep new_workgroups: nieuwe werkgroepen ordergroup: huishouden - second_paragraph: 'Let op het verschil tussen een groep en een huishouden: een %{url} heeft een tegoed en kan bestellen. In een werkgroep (bijv. ''sorteergroep'') organizeren zich de leden met behulp van taken en berichten. Gebruikers kunnen slechts lid zijn van één huishouden, maar van meerdere werkgroepen.' + second_paragraph: 'Let op het verschil tussen een groep en een huishouden: een %{url} heeft een tegoed en kan bestellen. In een werkgroep (bijv. ''sorteergroep'') organiseren zich de leden met behulp van taken en berichten. Gebruikers kunnen slechts lid zijn van één huishouden, maar van meerdere werkgroepen.' title: Werkgroepen new: title: Werkgroep toevoegen @@ -413,9 +413,9 @@ nl: name: naam application: controller: - error_authn: Inloggen vereist! - error_denied: Je hebt geen toegang tot de gevraagde pagina. Als je denkt dat je dat wel zou moeten hebben, vraag dan een beheerder je die rechten te geven. Als je meerdere accounts hebt, wil je mogelijk %{sign_in}. - error_denied_sign_in: inloggen als een andere gebruiker + error_authn: Aanmelden vereist! + error_denied: Je hebt geen toegang tot de gevraagde pagina. Als je denkt dat je dat wel zou moeten hebben, vraag dan een beheerder je die rechten te geven. Als je meerdere accounts hebt, wil je je mogelijk %{sign_in}. + error_denied_sign_in: aanmelden als een andere gebruiker error_feature_disabled: Deze optie is momenteel niet actief. error_members_only: Deze actie is alleen beschikbaar voor leden van de groep! error_minimum_balance: Sorry, je tegoed is lager dan het minimum van %{min}. @@ -429,7 +429,7 @@ nl: title: Categorie bewerken index: new: Nieuwe categorie - title: Categoriën + title: Categorieën new: title: Nieuwe categorie maken update: @@ -440,9 +440,9 @@ nl: articles: confirm_delete: Weet je zeker dat je alle artikelen wilt verwijderen? option_available: Artikelen beschikbaar maken - option_delete: Verwijder artikel + option_delete: Artikel verwijderen option_not_available: Artikelen onbeschikbaar maken - option_select: Kies actie ... + option_select: Actie kiezen… price_netto: Prijs unit_quantity_desc: Aantal eenheden per doos (colli) unit_quantity_short: Colli @@ -451,10 +451,10 @@ nl: notice: "Er zijn %{count} nieuwe artikelen opgeslagen." error_invalid: Er zijn artikelen die een fout hebben error_nosel: Geen artikelen geselecteerd - error_parse: "%{msg} ... in regel %{line}" + error_parse: "%{msg} … in regel %{line}" error_update: 'Er trad een fout op bij het bijwerken van artikel ''%{article}'': %{msg}' parse_upload: - no_file: Kies een bestand om te uploaden. + no_file: Een bestand om te uploaden kiezen. notice: "%{count} artikelen zijn geanalyseerd" sync: notice: Catalogus is bijgewerkt @@ -472,7 +472,7 @@ nl: drop: verwijderen note: "%{article} is deel van een lopende bestelling en kan niet verwijderd worden. Het artikel graag eerst uit de bestelling(en) %{drop_link}." edit_all: - note: 'Verplichte velden zijn: Naam, eenheid, (netto) prijs en bestellingsnummer.' + note: 'Verplichte velden zijn: Naam, eenheid, (netto) prijs en bestelnummer.' submit: Alle artikelen bijwerken title: Alle artikelen van %{supplier} bewerken warning: 'Let op, alle artikelen worden bijgewerkt!' @@ -484,7 +484,7 @@ nl: already_imported: geïmporteerd not_found: Geen artikelen gevonden index: - change_supplier: Leverancier wisselen ... + change_supplier: Leverancier wisselen… download: Artikelen downloaden edit_all: Alles bewerken ext_db: @@ -497,14 +497,14 @@ nl: title: Artikel importeren new: Nieuw artikel new_order: Nieuwe bestelling - search_placeholder: Naam ... + search_placeholder: Naam… title: Artikelen van %{supplier} (%{count}) upload: Artikelen uploaden model: error_in_use: "%{article} kan niet gewist worden, want deze is deel van een lopende bestelling!" error_nosel: Je hebt geen artikelen geselecteerd parse_upload: - body: "

Ingelezen artikelen graag controleren.

Let op, momenteel vind er geen controle op dubbele artikelen plaats.

" + body: "

Ingelezen artikelen graag controleren.

Let op, momenteel vindt er geen controle op dubbele artikelen plaats.

" submit: Upload verwerken title: Artikelen uploaden sync: @@ -512,32 +512,32 @@ nl: alert_used: Opgelet, %{article} wordt gebruikt in een lopende bestelling. Haal het eerst uit de bestelling. body: 'De volgende artikelen zijn uit de lijst gehaald en worden verwijderd:' body_ignored: - one: Er is één artikel zonder artikelnummer overslagen. + one: Er is één artikel zonder artikelnummer overgeslagen. other: "%{count} artikelen zonder artikelnummer zijn overgeslagen." body_skip: Er zijn geen artikelen om te verwijderen. - title: Uit de lijst halen ... + title: Uit de lijst halen… price_short: prijs submit: Alles synchroniseren title: Artikelen met externe database synchroniseren unit_quantity_short: Colli update: body: 'Ieder artikel wordt tweemaal getoond: oude waarden zijn grijs, en de tekstvelden bevatten de nieuwe waarden. Verschillen met de oude artikelen zijn geel gemarkeerd.' - title: Bijwerken ... + title: Bijwerken… update_msg: - one: Er moet éen artikel bijgewerkt worden. + one: Er moet één artikel bijgewerkt worden. other: "Er moeten %{count} artikelen bijgewerkt worden." upnew: body_count: - one: Er is éen nieuw artikel. + one: Er is één nieuw artikel. other: Er zijn %{count} nieuwe artikelen. - title: Toevoegen ... + title: Toevoegen… upload: fields: reserved: "(Leeg)" status: Status (x=overslaan) - file_label: Graag een compatibel bestand uitkiezen + file_label: Kies een compatibel bestand uit options: - convert_units: Bestaande eenheden behouden, herbereken groothandelseenheid en prijs (net als synchronizeren). + convert_units: Bestaande eenheden behouden, groothandelseenheid en prijs herberekenen (net als synchroniseren). outlist_absent: Artikelen die niet in het bestand voorkomen, verwijderen. sample: juices: Sappen @@ -549,21 +549,21 @@ nl: tomato_juice: Tomatensap walnuts: Walnoten submit: Bestand uploaden - text_1: 'Hier kun je een spreadsheet uploaden om de artikelen van %{supplier} bij te werken. Zowel Excel (xls, xlsx) als OpenOffice (ods) spreadsheets worden gelezen, evenals csv-bestanden (kolommen geschieden door ";", utf-8 encoding). Alleen de eerste sheet wordt geïmporteerd, en kolommen worden verwacht in deze volgorde:' - text_2: De rijen hier getoond zijn voorbeelden. Een "x" in de eerste kolom geeft aan dat het artikel niet meer beschikbaar is en zal worden verwijderd. Hiermee kun je snel meerdere artikelen tegelijk verwijderen. De categorie wordt gematched met de Foodsoft categorielijst (zowel met de categorienaam als de bijbehorende importnamen). + text_1: 'Hier kun je een spreadsheet uploaden om de artikelen van %{supplier} bij te werken. Spreadsheets van zowel Excel (xls, xlsx) als OpenOffice (ods) worden gelezen, evenals csv-bestanden (kolommen gescheiden door ";", utf-8 encoding). Alleen de eerste sheet wordt geïmporteerd, en kolommen worden verwacht in deze volgorde:' + text_2: De rijen hier getoond zijn voorbeelden. Een “x” in de eerste kolom geeft aan dat het artikel niet meer beschikbaar is en zal worden verwijderd. Hiermee kun je snel meerdere artikelen tegelijk verwijderen. De categorie wordt vergeleken met de categorielijst van Foodsoft (zowel met de categorienaam als de bijbehorende importnamen). title: Artikelen uploaden voor %{supplier} bank_account_connector: - confirm: Bevestig de code %{code}. + confirm: Code %{code} bevestigen. fields: - email: E-Mail + email: E-mail pin: PIN password: Wachtwoord tan: TAN username: Gebruikersnaam config: hints: - applepear_url: Website waar het appelpunten systeem wordt uitgelegd. - charge_members_manually: Kies deze optie als je elders bijhoudt wie welke producten heeft gekregen (bijvoorbeeld op papier), en dat ook niet naderhand in Foodsoft invoert. Na het afrekenen van bestellingen moet je dan iedere keer bij leden handmatig het in rekening te brengen bedrag afschrijven (gebruik "Nieuwe transacties toevoegen"). Het blijft wel nodig bestellingen af te rekenen, maar dat brengt dan niets in rekening bij leden. + applepear_url: Website waar het appelpuntensysteem wordt uitgelegd. + charge_members_manually: Kies deze optie als je elders bijhoudt wie welke producten heeft gekregen (bijvoorbeeld op papier), en dat ook niet naderhand in Foodsoft invoert. Na het afrekenen van bestellingen moet je dan iedere keer bij leden handmatig het in rekening te brengen bedrag afschrijven (gebruik “Nieuwe transacties toevoegen”). Het blijft wel nodig bestellingen af te rekenen, maar dat brengt dan niets in rekening bij leden. contact: email: Algemeen contactadres, zowel voor op de website als in formulieren. street: Adres, meestal is dit het aflever- en ophaaladres. From c67e9b5be86b3c4d589986d2316aaae7c8a66a43 Mon Sep 17 00:00:00 2001 From: Philipp Rothmann <16109235+yksflip@users.noreply.github.com> Date: Fri, 12 May 2023 11:11:48 +0200 Subject: [PATCH 044/105] Replace apivore with rswag for api tests (#969) * Replace apivore api tests with rswag * move to OpenAPI Spec 3.0.1 * a swagger UI is now reachable at http://localhost:3000/api-docs/index.html * swagger file is generated by running `RAILS_ENV=test rails rswag` and it was moved from /docs/swagger.v1.yml to /swagger/v1/swagger.yml --------- Co-authored-by: viehlieb --- .rubocop_todo.yml | 17 + Gemfile | 6 +- Gemfile.lock | 21 +- config/initializers/rswag_api.rb | 13 + config/initializers/rswag_ui.rb | 15 + config/routes.rb | 4 + doc/API.md | 6 +- doc/swagger.v1.yml | 1106 ----------------- spec/api/v1/order_articles_spec.rb | 59 - spec/api/v1/swagger_spec.rb | 284 ----- .../v1/user/financial_transactions_spec.rb | 109 -- spec/api/v1/user/group_order_articles_spec.rb | 220 ---- spec/api/v1/user/ordergroup_spec.rb | 55 - spec/app_config.yml | 1 + spec/requests/api/article_categories_spec.rb | 53 + spec/requests/api/configs_spec.rb | 20 + .../api/financial_transaction_classes_spec.rb | 54 + .../api/financial_transaction_types_spec.rb | 52 + .../api/financial_transactions_spec.rb | 56 + spec/requests/api/navigations_spec.rb | 24 + spec/requests/api/order_articles_spec.rb | 115 ++ spec/requests/api/orders_spec.rb | 55 + .../api/user/financial_transactions_spec.rb | 106 ++ .../api/user/group_order_articles_spec.rb | 192 +++ spec/requests/api/user/users_spec.rb | 103 ++ spec/support/api_helper.rb | 77 +- spec/swagger_helper.rb | 513 ++++++++ 27 files changed, 1478 insertions(+), 1858 deletions(-) create mode 100644 config/initializers/rswag_api.rb create mode 100644 config/initializers/rswag_ui.rb delete mode 100644 doc/swagger.v1.yml delete mode 100644 spec/api/v1/order_articles_spec.rb delete mode 100644 spec/api/v1/swagger_spec.rb delete mode 100644 spec/api/v1/user/financial_transactions_spec.rb delete mode 100644 spec/api/v1/user/group_order_articles_spec.rb delete mode 100644 spec/api/v1/user/ordergroup_spec.rb create mode 100644 spec/requests/api/article_categories_spec.rb create mode 100644 spec/requests/api/configs_spec.rb create mode 100644 spec/requests/api/financial_transaction_classes_spec.rb create mode 100644 spec/requests/api/financial_transaction_types_spec.rb create mode 100644 spec/requests/api/financial_transactions_spec.rb create mode 100644 spec/requests/api/navigations_spec.rb create mode 100644 spec/requests/api/order_articles_spec.rb create mode 100644 spec/requests/api/orders_spec.rb create mode 100644 spec/requests/api/user/financial_transactions_spec.rb create mode 100644 spec/requests/api/user/group_order_articles_spec.rb create mode 100644 spec/requests/api/user/users_spec.rb create mode 100644 spec/swagger_helper.rb diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index b7e21eab..2303bab6 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -451,6 +451,15 @@ RSpec/DescribedClass: - "spec/models/ordergroup_spec.rb" - "spec/models/user_spec.rb" +# Offense count: 15 +# This cop supports unsafe autocorrection (--autocorrect-all). +RSpec/EmptyExampleGroup: + Exclude: + # exclude for rswag tests: + - 'spec/requests/api/**/*_spec.rb' + + + # Offense count: 65 # Configuration parameters: CountAsOne. RSpec/ExampleLength: @@ -581,6 +590,14 @@ RSpec/ScatteredSetup: - "spec/integration/balancing_spec.rb" - "spec/integration/login_spec.rb" +# Offense count: 4 +# Configuration parameters: AllowedPatterns, IgnoredPatterns. +# SupportedStyles: snake_case, camelCase +RSpec/VariableName: + EnforcedStyle: snake_case + AllowedPatterns: + - ^Authorization$ + # Offense count: 1 # Configuration parameters: IgnoreNameless, IgnoreSymbolicNames. RSpec/VerifiedDoubles: diff --git a/Gemfile b/Gemfile index a6e27fae..61562099 100644 --- a/Gemfile +++ b/Gemfile @@ -55,6 +55,9 @@ gem 'gaffe' gem 'ruby-filemagic' gem 'mime-types' gem 'midi-smtp-server' +gem 'hashie', '~> 3.4.6', require: false # https://github.com/westfieldlabs/apivore/issues/114 +gem 'rswag-api' +gem 'rswag-ui' # we use the git version of acts_as_versioned, and need to include it in this Gemfile gem 'acts_as_versioned', git: 'https://github.com/technoweenie/acts_as_versioned.git' @@ -116,6 +119,5 @@ group :test do gem 'simplecov', require: false gem 'simplecov-lcov', require: false # api - gem 'apivore', require: false - gem 'hashie', '~> 3.4.6', require: false # https://github.com/westfieldlabs/apivore/issues/114 + gem 'rswag-specs' end diff --git a/Gemfile.lock b/Gemfile.lock index 196f9dbd..ebc7a49f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -109,13 +109,6 @@ GEM activerecord (>= 3.0.0) addressable (2.8.1) public_suffix (>= 2.0.2, < 6.0) - apivore (1.6.2) - actionpack (>= 4, < 6) - hashie (~> 3.3) - json-schema (~> 2.5) - rspec (~> 3) - rspec-expectations (~> 3.1) - rspec-mocks (~> 3.1) apparition (0.6.0) capybara (~> 3.13, < 4) websocket-driver (>= 0.6.5) @@ -430,6 +423,16 @@ GEM rspec-rerun (1.1.0) rspec (~> 3.0) rspec-support (3.11.1) + rswag-api (2.7.0) + railties (>= 3.1, < 7.1) + rswag-specs (2.7.0) + activesupport (>= 3.1, < 7.1) + json-schema (>= 2.2, < 4.0) + railties (>= 3.1, < 7.1) + rspec-core (>= 2.14) + rswag-ui (2.7.0) + actionpack (>= 3.1, < 7.1) + railties (>= 3.1, < 7.1) rubocop (1.36.0) json (~> 2.3) parallel (~> 1.10) @@ -557,7 +560,6 @@ DEPENDENCIES active_model_serializers (~> 0.10.0) acts_as_tree acts_as_versioned! - apivore apparition attribute_normalizer better_errors @@ -617,6 +619,9 @@ DEPENDENCIES rspec-core rspec-rails rspec-rerun + rswag-api + rswag-specs + rswag-ui rubocop rubocop-rails rubocop-rspec diff --git a/config/initializers/rswag_api.rb b/config/initializers/rswag_api.rb new file mode 100644 index 00000000..e4b798f6 --- /dev/null +++ b/config/initializers/rswag_api.rb @@ -0,0 +1,13 @@ +Rswag::Api.configure do |c| + # Specify a root folder where Swagger JSON files are located + # This is used by the Swagger middleware to serve requests for API descriptions + # NOTE: If you're using rswag-specs to generate Swagger, you'll need to ensure + # that it's configured to generate files in the same folder + c.swagger_root = Rails.root.to_s + '/swagger' + + # Inject a lambda function to alter the returned Swagger prior to serialization + # The function will have access to the rack env for the current request + # For example, you could leverage this to dynamically assign the "host" property + # + # c.swagger_filter = lambda { |swagger, env| swagger['host'] = env['HTTP_HOST'] } +end diff --git a/config/initializers/rswag_ui.rb b/config/initializers/rswag_ui.rb new file mode 100644 index 00000000..cc9f2ef8 --- /dev/null +++ b/config/initializers/rswag_ui.rb @@ -0,0 +1,15 @@ +Rswag::Ui.configure do |c| + # List the Swagger endpoints that you want to be documented through the + # swagger-ui. The first parameter is the path (absolute or relative to the UI + # host) to the corresponding endpoint and the second is a title that will be + # displayed in the document selector. + # NOTE: If you're using rspec-api to expose Swagger files + # (under swagger_root) as JSON or YAML endpoints, then the list below should + # correspond to the relative paths for those endpoints. + + c.swagger_endpoint '/api-docs/v1/swagger.yaml', 'API V1 Docs' + + # Add Basic Auth in case your API is private + # c.basic_auth_enabled = true + # c.basic_auth_credentials 'username', 'password' +end diff --git a/config/routes.rb b/config/routes.rb index 5b27eba4..764dcab0 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,4 +1,7 @@ +# rubocop:disable Metrics/BlockLength Rails.application.routes.draw do + mount Rswag::Ui::Engine => '/api-docs' + mount Rswag::Api::Engine => '/api-docs' get "order_comments/new" get "comments/new" @@ -290,3 +293,4 @@ Rails.application.routes.draw do resources :users, only: [:index] end # End of /:foodcoop scope end +# rubocop:enable Metrics/BlockLength diff --git a/doc/API.md b/doc/API.md index 2e09cfa4..f295e82f 100644 --- a/doc/API.md +++ b/doc/API.md @@ -5,9 +5,11 @@ like listing open orders, updating the ordergroup's order, and listing financial transactions. Not all Foodsoft functionality is available through the API, but we're open for new additions. -The API is documented using [Open API 2.0](https://github.com/OAI/OpenAPI-Specification) -/ [Swagger](https://swagger.io/) in [swagger.v1.yml](swagger.v1.yml). +The API is documented using [Open API 3.0.1](https://github.com/OAI/OpenAPI-Specification) +/ [Swagger](https://swagger.io/) in [swagger.yaml](/swagger/v1/swagger.yaml). This provides a machine-readable reference that is used to provide documentation. +It is generated by [rswag](https://github.com/rswag) wich also provides api-tests. +It can be generated running `RAILS_ENV=test rails rswag`. **Note:** the current OAuth scopes may be subject to change, until the next release of Foodsoft. diff --git a/doc/swagger.v1.yml b/doc/swagger.v1.yml deleted file mode 100644 index d8e793d3..00000000 --- a/doc/swagger.v1.yml +++ /dev/null @@ -1,1106 +0,0 @@ -swagger: '2.0' -info: - title: Foodsoft API v1 - version: '1.0.0' - description: > - [Foodsoft](https://github.com/foodcoops/foodsoft) is web-based software to manage - a non-profit food coop (product catalog, ordering, accounting, job scheduling). - - - This is a description of Foodsoft's API v1. - - - Note that each food cooperative typically has their own instance (on a shared - server or their own installation), and there are just as many APIs (if the Foodsoft - version is recent enough). - This API description points to the default development url with the default - Foodsoft scope - that would be [http://localhost:3000/f](http://localhost:3000/f). - - You may find the search parameters for index endpoints lacking. They are not - documented here, because there are too many combinations. For now, you'll need - to resort to [Ransack](https://github.com/activerecord-hackery/ransack) and - looking at Foodsoft's `ransackable_*` model class methods. -externalDocs: - description: General Foodsoft API documentation - url: https://github.com/foodcoops/foodsoft/blob/master/doc/API.md - -# development url with default scope -host: localhost:3000 -schemes: - - 'http' -basePath: /f/api/v1 - -produces: - - 'application/json' - -paths: - /user: - get: - summary: info about the currently logged-in user - tags: - - 1. User - responses: - 200: - description: success - schema: - type: object - properties: - user: - $ref: '#/definitions/User' - 401: - description: not logged-in - schema: - $ref: '#/definitions/Error401' - 403: - description: missing scope - schema: - $ref: '#/definitions/Error403' - security: - - foodsoft_auth: ['user:read', 'user:write'] - - /user/financial_overview: - get: - summary: financial summary about the currently logged-in user - tags: - - 1. User - - 6. FinancialTransaction - responses: - 200: - description: success - schema: - type: object - properties: - financial_overview: - $ref: '#/definitions/FinanceOverview' - 401: - description: not logged-in - schema: - $ref: '#/definitions/Error401' - 403: - description: missing scope - schema: - $ref: '#/definitions/Error403' - security: - - foodsoft_auth: ['finance:user'] - - /user/financial_transactions: - get: - summary: financial transactions of the member's ordergroup - tags: - - 1. User - - 6. FinancialTransaction - parameters: - - $ref: '#/parameters/page' - - $ref: '#/parameters/per_page' - responses: - 200: - description: success - schema: - type: object - properties: - financial_transactions: - type: array - items: - $ref: '#/definitions/FinancialTransaction' - meta: - $ref: '#/definitions/Meta' - 401: - description: not logged-in - schema: - $ref: '#/definitions/Error401' - 403: - description: user has no ordergroup or missing scope - schema: - $ref: '#/definitions/Error403' - security: - - foodsoft_auth: ['finance:user'] - post: - summary: create new financial transaction (requires enabled self service) - tags: - - 1. User - - 6. FinancialTransaction - parameters: - - in: body - name: body - description: financial transaction to create - required: true - schema: - $ref: '#/definitions/FinancialTransactionForCreate' - responses: - 200: - description: success - schema: - type: object - properties: - financial_transaction: - $ref: '#/definitions/FinancialTransaction' - 401: - description: not logged-in - schema: - $ref: '#/definitions/Error401' - 403: - description: user has no ordergroup, is below minimum balance, self service is disabled, or missing scope - schema: - $ref: '#/definitions/Error403' - 404: - description: financial transaction type not found - schema: - $ref: '#/definitions/Error404' - 422: - description: invalid parameter value - schema: - $ref: '#/definitions/Error422' - /user/financial_transactions/{id}: - parameters: - - $ref: '#/parameters/idInUrl' - get: - summary: find financial transaction by id - tags: - - 1. User - - 6. FinancialTransaction - responses: - 200: - description: success - schema: - type: object - properties: - financial_transaction: - $ref: '#/definitions/FinancialTransaction' - 401: - description: not logged-in - schema: - $ref: '#/definitions/Error401' - 403: - description: user has no ordergroup or missing scope - schema: - $ref: '#/definitions/Error403' - 404: - description: not found - schema: - $ref: '#/definitions/Error404' - security: - - foodsoft_auth: ['finance:user'] - - /user/group_order_articles: - get: - summary: group order articles - tags: - - 1. User - - 2. Order - parameters: - - $ref: '#/parameters/page' - - $ref: '#/parameters/per_page' - - $ref: '#/parameters/q_ordered' - responses: - 200: - description: success - schema: - type: object - properties: - group_order_articles: - type: array - items: - $ref: '#/definitions/GroupOrderArticle' - meta: - $ref: '#/definitions/Meta' - 401: - description: not logged-in - schema: - $ref: '#/definitions/Error401' - 403: - description: user has no ordergroup or missing scope - schema: - $ref: '#/definitions/Error403' - security: - - foodsoft_auth: ['group_orders:user'] - post: - summary: create new group order article - tags: - - 1. User - - 2. Order - parameters: - - in: body - name: body - description: group order article to create - required: true - schema: - $ref: '#/definitions/GroupOrderArticleForCreate' - responses: - 200: - description: success - schema: - type: object - properties: - group_order_article: - $ref: '#/definitions/GroupOrderArticle' - order_article: - $ref: '#/definitions/OrderArticle' - 401: - description: not logged-in - schema: - $ref: '#/definitions/Error401' - 403: - description: user has no ordergroup, order not open, is below minimum balance, has not enough apple points, or missing scope - schema: - $ref: '#/definitions/Error403' - 404: - description: order article not found in open orders - schema: - $ref: '#/definitions/Error404' - 422: - description: invalid parameter value or group order article already exists - schema: - $ref: '#/definitions/Error422' - /user/group_order_articles/{id}: - parameters: - - $ref: '#/parameters/idInUrl' - get: - summary: find group order article by id - tags: - - 1. User - - 2. Order - responses: - 200: - description: success - schema: - type: object - properties: - group_order_article: - $ref: '#/definitions/GroupOrderArticle' - order_article: - $ref: '#/definitions/OrderArticle' - 401: - description: not logged-in - schema: - $ref: '#/definitions/Error401' - 403: - description: user has no ordergroup or missing scope - schema: - $ref: '#/definitions/Error403' - 404: - description: not found - schema: - $ref: '#/definitions/Error404' - security: - - foodsoft_auth: ['group_orders:user'] - patch: - summary: update a group order article (but delete if quantity and tolerance are zero) - tags: - - 1. User - - 2. Order - parameters: - - $ref: '#/parameters/idInUrl' - - in: body - name: body - description: group order article update - required: true - schema: - $ref: '#/definitions/GroupOrderArticleForUpdate' - responses: - 200: - description: success - schema: - type: object - properties: - group_order_article: - $ref: '#/definitions/GroupOrderArticle' - order_article: - $ref: '#/definitions/OrderArticle' - 401: - description: not logged-in - schema: - $ref: '#/definitions/Error401' - 403: - description: user has no ordergroup, order not open, is below minimum balance, has not enough apple points, or missing scope - schema: - $ref: '#/definitions/Error403' - 404: - description: order article not found in open orders - schema: - $ref: '#/definitions/Error404' - 422: - description: invalid parameter value - schema: - $ref: '#/definitions/Error422' - delete: - summary: remove group order article - tags: - - 1. User - - 2. Order - parameters: - - $ref: '#/parameters/idInUrl' - responses: - 200: - description: success - schema: - type: object - properties: - group_order_article: - $ref: '#/definitions/GroupOrderArticle' - order_article: - $ref: '#/definitions/OrderArticle' - 401: - description: not logged-in - schema: - $ref: '#/definitions/Error401' - 403: - description: user has no ordergroup, order not open, is below minimum balance, has not enough apple points, or missing scope - schema: - $ref: '#/definitions/Error403' - 404: - description: order article not found in open orders - schema: - $ref: '#/definitions/Error404' - - /financial_transactions: - get: - summary: financial transactions - tags: - - 6. FinancialTransaction - parameters: - - $ref: '#/parameters/page' - - $ref: '#/parameters/per_page' - responses: - 200: - description: success - schema: - type: object - properties: - financial_transactions: - type: array - items: - $ref: '#/definitions/FinancialTransaction' - meta: - $ref: '#/definitions/Meta' - 401: - description: not logged-in - schema: - $ref: '#/definitions/Error401' - 403: - description: missing scope or no permission - schema: - $ref: '#/definitions/Error403' - security: - - foodsoft_auth: ['finance:read', 'finance:write'] - /financial_transactions/{id}: - parameters: - - $ref: '#/parameters/idInUrl' - get: - summary: find financial transaction by id - tags: - - 6. FinancialTransaction - responses: - 200: - description: success - schema: - type: object - properties: - financial_transaction: - $ref: '#/definitions/FinancialTransaction' - 401: - description: not logged-in - schema: - $ref: '#/definitions/Error401' - 403: - description: missing scope or no permission - schema: - $ref: '#/definitions/Error403' - 404: - description: not found - schema: - $ref: '#/definitions/Error404' - security: - - foodsoft_auth: ['finance:read', 'finance:write'] - /orders: - get: - summary: orders - tags: - - 2. Order - parameters: - - $ref: '#/parameters/page' - - $ref: '#/parameters/per_page' - responses: - 200: - description: success - schema: - type: object - properties: - orders: - type: array - items: - $ref: '#/definitions/Order' - meta: - $ref: '#/definitions/Meta' - 401: - description: not logged-in - schema: - $ref: '#/definitions/Error401' - 403: - description: missing scope or no permission - schema: - $ref: '#/definitions/Error403' - security: - - foodsoft_auth: ['orders:read', 'orders:write'] - /orders/{id}: - parameters: - - $ref: '#/parameters/idInUrl' - get: - summary: find order by id - tags: - - 2. Order - responses: - 200: - description: success - schema: - type: object - properties: - order: - $ref: '#/definitions/Order' - 401: - description: not logged-in - schema: - $ref: '#/definitions/Error401' - 403: - description: missing scope or no permission - schema: - $ref: '#/definitions/Error403' - 404: - description: not found - schema: - $ref: '#/definitions/Error404' - security: - - foodsoft_auth: ['orders:read', 'orders:write'] - /order_articles: - get: - summary: order articles - tags: - - 2. Order - parameters: - - $ref: '#/parameters/page' - - $ref: '#/parameters/per_page' - - $ref: '#/parameters/q_ordered' - responses: - 200: - description: success - schema: - type: object - properties: - order_articles: - type: array - items: - $ref: '#/definitions/OrderArticle' - meta: - $ref: '#/definitions/Meta' - 401: - description: not logged-in - schema: - $ref: '#/definitions/Error401' - 403: - description: missing scope or no permission - schema: - $ref: '#/definitions/Error403' - security: - - foodsoft_auth: ['group_orders:user'] - /order_articles/{id}: - parameters: - - $ref: '#/parameters/idInUrl' - get: - summary: find order article by id - tags: - - 2. Order - responses: - 200: - description: success - schema: - type: object - properties: - order_article: - $ref: '#/definitions/OrderArticle' - 401: - description: not logged-in - schema: - $ref: '#/definitions/Error401' - 403: - description: missing scope or no permission - schema: - $ref: '#/definitions/Error403' - 404: - description: not found - schema: - $ref: '#/definitions/Error404' - security: - - foodsoft_auth: ['orders:read', 'orders:write'] - /article_categories: - get: - summary: article categories - tags: - - 2. Category - parameters: - - $ref: '#/parameters/page' - - $ref: '#/parameters/per_page' - responses: - 200: - description: success - schema: - type: object - properties: - article_categories: - type: array - items: - $ref: '#/definitions/ArticleCategory' - meta: - $ref: '#/definitions/Meta' - 401: - description: not logged-in - schema: - $ref: '#/definitions/Error401' - - security: - - foodsoft_auth: ['all'] - /article_categories/{id}: - parameters: - - $ref: '#/parameters/idInUrl' - get: - summary: find article category by id - tags: - - 2. Category - responses: - 200: - description: success - schema: - type: object - properties: - article_category: - $ref: '#/definitions/ArticleCategory' - 401: - description: not logged-in - schema: - $ref: '#/definitions/Error401' - 404: - description: not found - schema: - $ref: '#/definitions/Error404' - security: - - foodsoft_auth: ['all'] - - /financial_transaction_classes: - get: - summary: financial transaction classes - tags: - - 2. Category - parameters: - - $ref: '#/parameters/page' - - $ref: '#/parameters/per_page' - responses: - 200: - description: success - schema: - type: object - properties: - financial_transaction_classes: - type: array - items: - $ref: '#/definitions/FinancialTransactionClass' - meta: - $ref: '#/definitions/Meta' - 401: - description: not logged-in - schema: - $ref: '#/definitions/Error401' - - security: - - foodsoft_auth: ['all'] - /financial_transaction_classes/{id}: - parameters: - - $ref: '#/parameters/idInUrl' - get: - summary: find financial transaction class by id - tags: - - 2. Category - responses: - 200: - description: success - schema: - type: object - properties: - financial_transaction_class: - $ref: '#/definitions/FinancialTransactionClass' - 401: - description: not logged-in - schema: - $ref: '#/definitions/Error401' - 404: - description: not found - schema: - $ref: '#/definitions/Error404' - security: - - foodsoft_auth: ['all'] - - /financial_transaction_types: - get: - summary: financial transaction types - tags: - - 2. Category - parameters: - - $ref: '#/parameters/page' - - $ref: '#/parameters/per_page' - responses: - 200: - description: success - schema: - type: object - properties: - financial_transaction_types: - type: array - items: - $ref: '#/definitions/FinancialTransactionType' - meta: - $ref: '#/definitions/Meta' - 401: - description: not logged-in - schema: - $ref: '#/definitions/Error401' - - security: - - foodsoft_auth: ['all'] - /financial_transaction_types/{id}: - parameters: - - $ref: '#/parameters/idInUrl' - get: - summary: find financial transaction type by id - tags: - - 2. Category - responses: - 200: - description: success - schema: - type: object - properties: - financial_transaction_type: - $ref: '#/definitions/FinancialTransactionType' - 401: - description: not logged-in - schema: - $ref: '#/definitions/Error401' - 404: - description: not found - schema: - $ref: '#/definitions/Error404' - security: - - foodsoft_auth: ['all'] - - /config: - get: - summary: configuration variables - tags: - - 7. General - responses: - 200: - description: success - schema: - type: object - 401: - description: not logged-in - schema: - $ref: '#/definitions/Error401' - 403: - description: missing scope or no permission - schema: - $ref: '#/definitions/Error403' - security: - - foodsoft_auth: ['config:user', 'config:read', 'config:write'] - /navigation: - get: - summary: navigation - tags: - - 7. General - responses: - 200: - description: success - schema: - type: object - properties: - navigation: - $ref: '#/definitions/Navigation' - 401: - description: not logged-in - schema: - $ref: '#/definitions/Error401' - security: - - foodsoft_auth: [] - -parameters: - # url parameters - idInUrl: - name: id - type: integer - in: path - minimum: 1 - required: true - - # query parameters - page: - name: page - type: integer - in: query - description: page number - minimum: 0 - default: 0 - per_page: - name: per_page - type: integer - in: query - description: items per page - minimum: 0 - default: 20 - - # non-ransack query parameters - q_ordered: - name: q[ordered] - type: string - in: query - description: "'member' show articles ordered by the user's ordergroup, 'all' by all members, and 'supplier' ordered at the supplier" - enum: ['member', 'all', 'supplier'] - -definitions: - # models - User: - type: object - properties: - id: - type: integer - name: - type: string - description: full name - email: - type: string - description: email address - locale: - type: string - description: language code - required: ['id', 'name', 'email'] - - FinancialTransactionForCreate: - type: object - properties: - amount: - type: number - description: amount credited (negative for a debit transaction) - financial_transaction_type_id: - type: integer - description: id of the type of the transaction - note: - type: string - description: note entered with the transaction - required: ['amount', 'financial_transaction_type_id', 'note'] - FinancialTransaction: - allOf: - - $ref: '#/definitions/FinancialTransactionForCreate' - - type: object - properties: - id: - type: integer - user_id: - type: ['integer', 'null'] - description: id of user who entered the transaction (may be null for deleted users or 0 for a system user) - user_name: - type: ['string', 'null'] - description: name of user who entered the transaction (may be null or empty string for deleted users or system users) - financial_transaction_type_name: - type: string - description: name of the type of the transaction - created_at: - type: string - format: date-time - description: when the transaction was entered - required: ['id', 'user_id', 'user_name', 'financial_transaction_type_name', 'created_at'] - - FinancialTransactionClass: - type: object - properties: - id: - type: integer - name: - type: string - description: full name - required: ['id', 'name'] - - FinancialTransactionType: - type: object - properties: - id: - type: integer - name: - type: string - description: full name - name_short: - type: ['string', 'null'] - description: short name (used for bank transfers) - bank_account_id: - type: ['integer', 'null'] - description: id of the bank account used for this transaction type - bank_account_name: - type: ['string', 'null'] - description: name of the bank account used for this transaction type - bank_account_iban: - type: ['string', 'null'] - description: IBAN of the bank account used for this transaction type - financial_transaction_class_id: - type: integer - description: id of the class of the transaction - financial_transaction_class_name: - type: string - description: name of the class of the transaction - required: ['id', 'name', 'financial_transaction_class_id', 'financial_transaction_class_name'] - - FinanceOverview: - type: object - properties: - account_balance: - type: number - description: booked accout balance of ordergroup - available_funds: - type: number - description: fund available to order articles - financial_transaction_class_sums: - type: array - items: - type: object - properties: - id: - type: integer - description: id of the financial transaction class - name: - type: string - description: name of the financial transaction class - amount: - type: number - description: sum of the amounts belonging to the financial transaction class - required: ['id', 'name', 'amount'] - required: ['account_balance', 'available_funds', 'financial_transaction_class_sums'] - - ArticleCategory: - type: object - properties: - id: - type: integer - name: - type: string - required: ['id', 'name'] - - Order: - type: object - properties: - id: - type: integer - name: - type: string - description: name of the order's supplier (or stock) - starts: - type: string - format: date-time - description: when the order was opened - ends: - type: ['string', 'null'] - format: date-time - description: when the order will close or was closed - boxfill: - type: ['string', 'null'] - format: date-time - description: when the order will enter or entered the boxfill phase - pickup: - type: ['string', 'null'] - format: date - description: pickup date - is_open: - type: boolean - description: if the order is currently open or not - is_boxfill: - type: boolean - description: if the order is currently in the boxfill phase or not - - Article: - type: object - properties: - id: - type: integer - name: - type: string - supplier_id: - type: integer - description: id of supplier, or 0 for stock articles - supplier_name: - type: ['string', 'null'] - description: name of the supplier, or null for stock articles - unit: - type: string - description: amount of each unit, e.g. "100 g" or "kg" - unit_quantity: - type: integer - description: units can only be ordered from the supplier in multiples of unit_quantity - note: - type: ['string', 'null'] - description: generic note - manufacturer: - type: ['string', 'null'] - description: manufacturer - origin: - type: ['string', 'null'] - description: origin, preferably (starting with a) 2-letter ISO country code - article_category_id: - type: integer - description: id of article category - quantity_available: - type: integer - description: number of units available (only present on stock articles) - required: ['id', 'name', 'supplier_id', 'supplier_name', 'unit', 'unit_quantity', 'note', 'manufacturer', 'origin', 'article_category_id'] - - OrderArticle: - type: object - properties: - id: - type: integer - order_id: - type: integer - description: id of order this order article belongs to - price: - type: number - format: float - description: foodcoop price - quantity: - type: integer - description: number of units ordered by members - tolerance: - type: integer - description: number of extra units that members are willing to buy to fill a box - units_to_order: - type: integer - description: number of units to order from the supplier - article: - $ref: '#/definitions/Article' - - GroupOrderArticleForUpdate: - type: object - properties: - quantity: - type: integer - description: number of units ordered by the user's ordergroup - tolerance: - type: integer - description: number of extra units the user's ordergroup is willing to buy for filling a box - GroupOrderArticleForCreate: - allOf: - - $ref: '#/definitions/GroupOrderArticleForUpdate' - - type: object - properties: - order_article_id: - type: integer - description: id of order article - GroupOrderArticle: - allOf: - - $ref: '#/definitions/GroupOrderArticleForCreate' - - type: object - properties: - id: - type: integer - result: - type: number - format: float - description: number of units the user's ordergroup will receive or has received - total_price: - type: number - format: float - description: total price of this group order article - - Navigation: - type: array - items: - type: object - properties: - name: - type: string - description: title - url: - type: string - description: link - items: - $ref: '#/definitions/Navigation' - required: ['name'] - minProperties: 2 # name+url or name+items - - # collection meta object in root of a response - Meta: - type: object - properties: - page: - type: integer - description: page number of the returned collection - per_page: - type: integer - description: number of items per page - total_pages: - type: integer - description: total number of pages - total_count: - type: integer - description: total number of items in the collection - required: ['page', 'per_page', 'total_pages', 'total_count'] - - Error: - type: object - properties: - error: - type: string - description: error code - error_description: - type: string - description: human-readable error message (localized) - Error404: - type: object - properties: - error: - type: string - description: 'not_found' - error_description: - $ref: '#/definitions/Error/properties/error_description' - Error401: - type: object - properties: - error: - type: string - description: 'unauthorized' - error_description: - $ref: '#/definitions/Error/properties/error_description' - Error403: - type: object - properties: - error: - type: string - description: 'forbidden or invalid_scope' - error_description: - $ref: '#/definitions/Error/properties/error_description' - Error422: - type: object - properties: - error: - type: string - description: unprocessable entity - error_description: - $ref: '#/definitions/Error/properties/error_description' - - -securityDefinitions: - foodsoft_auth: - type: oauth2 - flow: implicit - authorizationUrl: http://localhost:3000/f/oauth/authorize - scopes: - config:user: reading Foodsoft configuration for regular users - config:read: reading Foodsoft configuration values - config:write: reading and updating Foodsoft configuration values - finance:user: accessing your own financial transactions - finance:read: reading all financial transactions - finance:write: reading and creating financial transactions - user:read: reading your own user profile - user:write: reading and updating your own user profile - offline_access: retain access after user has logged out diff --git a/spec/api/v1/order_articles_spec.rb b/spec/api/v1/order_articles_spec.rb deleted file mode 100644 index e65867db..00000000 --- a/spec/api/v1/order_articles_spec.rb +++ /dev/null @@ -1,59 +0,0 @@ -require 'spec_helper' - -# Most routes are tested in the swagger_spec, this tests (non-ransack) parameters. -describe Api::V1::OrderArticlesController, type: :controller do - include ApiOAuth - let(:api_scopes) { ['orders:read'] } - - let(:json_order_articles) { json_response['order_articles'] } - let(:json_order_article_ids) { json_order_articles.map { |joa| joa["id"] } } - - describe "GET :index" do - context "with param q[ordered]" do - let(:order) { create(:order, article_count: 4) } - let(:order_articles) { order.order_articles } - - before do - order_articles[0].update!(quantity: 0, tolerance: 0, units_to_order: 0) - order_articles[1].update!(quantity: 1, tolerance: 0, units_to_order: 0) - order_articles[2].update!(quantity: 0, tolerance: 1, units_to_order: 0) - order_articles[3].update!(quantity: 0, tolerance: 0, units_to_order: 1) - end - - it "(unset)" do - get :index, params: { foodcoop: 'f' } - expect(json_order_articles.count).to eq 4 - end - - it "all" do - get :index, params: { foodcoop: 'f', q: { ordered: 'all' } } - expect(json_order_article_ids).to match_array order_articles[1..2].map(&:id) - end - - it "supplier" do - get :index, params: { foodcoop: 'f', q: { ordered: 'supplier' } } - expect(json_order_article_ids).to match_array [order_articles[3].id] - end - - it "member" do - get :index, params: { foodcoop: 'f', q: { ordered: 'member' } } - expect(json_order_articles.count).to eq 0 - end - - context "when ordered by user" do - let(:user) { create(:user, :ordergroup) } - let(:go) { create(:group_order, order: order, ordergroup: user.ordergroup) } - - before do - create(:group_order_article, group_order: go, order_article: order_articles[1], quantity: 1) - create(:group_order_article, group_order: go, order_article: order_articles[2], tolerance: 0) - end - - it "member" do - get :index, params: { foodcoop: 'f', q: { ordered: 'member' } } - expect(json_order_article_ids).to match_array order_articles[1..2].map(&:id) - end - end - end - end -end diff --git a/spec/api/v1/swagger_spec.rb b/spec/api/v1/swagger_spec.rb deleted file mode 100644 index 3da37332..00000000 --- a/spec/api/v1/swagger_spec.rb +++ /dev/null @@ -1,284 +0,0 @@ -require 'spec_helper' -require 'apivore' - -# we want to load a local file in YAML-format instead of a served JSON file -class SwaggerCheckerFile < Apivore::SwaggerChecker - def fetch_swagger! - YAML.load(File.read(swagger_path)) - end -end - -describe 'API v1', type: :apivore, order: :defined do - include ApiHelper - - subject { SwaggerCheckerFile.instance_for Rails.root.join('doc', 'swagger.v1.yml') } - - context 'has valid paths' do - context 'user' do - let(:api_scopes) { ['user:read'] } - # create multiple users to make sure we're getting the authenticated user, not just any - let!(:other_user_1) { create :user } - let!(:user) { create :user } - let!(:other_user_2) { create :user } - - it { is_expected.to validate(:get, '/user', 200, api_auth) } - it { is_expected.to validate(:get, '/user', 401) } - - it_handles_invalid_token_and_scope(:get, '/user') - end - - context 'user/financial_overview' do - let(:api_scopes) { ['finance:user'] } - let!(:user) { create :user, :ordergroup } - - it { is_expected.to validate(:get, '/user/financial_overview', 200, api_auth) } - it { is_expected.to validate(:get, '/user/financial_overview', 401) } - - it_handles_invalid_token_and_scope(:get, '/user/financial_overview') - end - - context 'user/financial_transactions' do - let(:api_scopes) { ['finance:user'] } - let(:other_user) { create :user, :ordergroup } - let!(:other_ft_1) { create :financial_transaction, ordergroup: other_user.ordergroup } - - context 'without ordergroup' do - it { is_expected.to validate(:get, '/user/financial_transactions', 403, api_auth) } - it { is_expected.to validate(:get, '/user/financial_transactions/{id}', 403, api_auth({ 'id' => other_ft_1.id })) } - end - - context 'with ordergroup' do - let(:user) { create :user, :ordergroup } - let!(:ft_1) { create :financial_transaction, ordergroup: user.ordergroup } - let!(:ft_2) { create :financial_transaction, ordergroup: user.ordergroup } - let!(:ft_3) { create :financial_transaction, ordergroup: user.ordergroup } - - let(:create_params) { { '_data' => { financial_transaction: { amount: 1, financial_transaction_type_id: ft_1.financial_transaction_type.id, note: 'note' } } } } - - it { is_expected.to validate(:get, '/user/financial_transactions', 200, api_auth) } - it { is_expected.to validate(:get, '/user/financial_transactions/{id}', 200, api_auth({ 'id' => ft_2.id })) } - it { is_expected.to validate(:get, '/user/financial_transactions/{id}', 404, api_auth({ 'id' => other_ft_1.id })) } - it { is_expected.to validate(:get, '/user/financial_transactions/{id}', 404, api_auth({ 'id' => FinancialTransaction.last.id + 1 })) } - - context 'without using self service' do - it { is_expected.to validate(:post, '/user/financial_transactions', 403, api_auth(create_params)) } - end - - context 'with using self service' do - before { FoodsoftConfig[:use_self_service] = true } - - it { is_expected.to validate(:post, '/user/financial_transactions', 200, api_auth(create_params)) } - - context 'with invalid financial transaction type' do - let(:create_params) { { '_data' => { financial_transaction: { amount: 1, financial_transaction_type_id: -1, note: 'note' } } } } - - it { is_expected.to validate(:post, '/user/financial_transactions', 404, api_auth(create_params)) } - end - - context 'without note' do - let(:create_params) { { '_data' => { financial_transaction: { amount: 1, financial_transaction_type_id: ft_1.financial_transaction_type.id } } } } - - it { is_expected.to validate(:post, '/user/financial_transactions', 422, api_auth(create_params)) } - end - - context 'without enough balance' do - before { FoodsoftConfig[:minimum_balance] = 1000 } - - it { is_expected.to validate(:post, '/user/financial_transactions', 403, api_auth(create_params)) } - end - end - - it_handles_invalid_token_and_scope(:get, '/user/financial_transactions') - it_handles_invalid_token_and_scope(:post, '/user/financial_transactions', -> { api_auth(create_params) }) - it_handles_invalid_token_and_scope(:get, '/user/financial_transactions/{id}', -> { api_auth('id' => ft_2.id) }) - end - end - - context 'user/group_order_articles' do - let(:api_scopes) { ['group_orders:user'] } - let(:order) { create(:order, article_count: 2) } - - let(:user_2) { create :user, :ordergroup } - let(:group_order_2) { create(:group_order, order: order, ordergroup: user_2.ordergroup) } - let!(:goa_2) { create :group_order_article, order_article: order.order_articles[0], group_order: group_order_2 } - - before { group_order_2.update_price!; user_2.ordergroup.update_stats! } - - context 'without ordergroup' do - it { is_expected.to validate(:get, '/user/group_order_articles', 403, api_auth) } - it { is_expected.to validate(:get, '/user/group_order_articles/{id}', 403, api_auth({ 'id' => goa_2.id })) } - end - - context 'with ordergroup' do - let(:user) { create :user, :ordergroup } - let(:update_params) { { 'id' => goa.id, '_data' => { group_order_article: { quantity: goa.quantity + 1, tolerance: 0 } } } } - let(:create_params) { { '_data' => { group_order_article: { order_article_id: order.order_articles[1].id, quantity: 1 } } } } - let(:group_order) { create(:group_order, order: order, ordergroup: user.ordergroup) } - let!(:goa) { create :group_order_article, order_article: order.order_articles[0], group_order: group_order } - - before { group_order.update_price!; user.ordergroup.update_stats! } - - it { is_expected.to validate(:get, '/user/group_order_articles', 200, api_auth) } - it { is_expected.to validate(:get, '/user/group_order_articles/{id}', 200, api_auth({ 'id' => goa.id })) } - it { is_expected.to validate(:get, '/user/group_order_articles/{id}', 404, api_auth({ 'id' => goa_2.id })) } - it { is_expected.to validate(:get, '/user/group_order_articles/{id}', 404, api_auth({ 'id' => GroupOrderArticle.last.id + 1 })) } - - it { is_expected.to validate(:post, '/user/group_order_articles', 200, api_auth(create_params)) } - it { is_expected.to validate(:patch, '/user/group_order_articles/{id}', 200, api_auth(update_params)) } - it { is_expected.to validate(:delete, '/user/group_order_articles/{id}', 200, api_auth({ 'id' => goa.id })) } - - context 'with an existing group_order_article' do - let(:create_params) { { '_data' => { group_order_article: { order_article_id: order.order_articles[0].id, quantity: 1 } } } } - - it { is_expected.to validate(:post, '/user/group_order_articles', 422, api_auth(create_params)) } - end - - context 'with invalid parameter values' do - let(:create_params) { { '_data' => { group_order_article: { order_article_id: order.order_articles[0].id, quantity: -1 } } } } - let(:update_params) { { 'id' => goa.id, '_data' => { group_order_article: { quantity: -1, tolerance: 0 } } } } - - it { is_expected.to validate(:post, '/user/group_order_articles', 422, api_auth(create_params)) } - it { is_expected.to validate(:patch, '/user/group_order_articles/{id}', 422, api_auth(update_params)) } - end - - context 'with a closed order' do - let(:order) { create(:order, article_count: 2, state: :finished) } - - it { is_expected.to validate(:post, '/user/group_order_articles', 404, api_auth(create_params)) } - it { is_expected.to validate(:patch, '/user/group_order_articles/{id}', 404, api_auth(update_params)) } - it { is_expected.to validate(:delete, '/user/group_order_articles/{id}', 404, api_auth({ 'id' => goa.id })) } - end - - context 'without enough balance' do - before { FoodsoftConfig[:minimum_balance] = 1000 } - - it { is_expected.to validate(:post, '/user/group_order_articles', 403, api_auth(create_params)) } - it { is_expected.to validate(:patch, '/user/group_order_articles/{id}', 403, api_auth(update_params)) } - it { is_expected.to validate(:delete, '/user/group_order_articles/{id}', 200, api_auth({ 'id' => goa.id })) } - end - - context 'without enough apple points' do - before { allow_any_instance_of(Ordergroup).to receive(:not_enough_apples?).and_return(true) } - - it { is_expected.to validate(:post, '/user/group_order_articles', 403, api_auth(create_params)) } - it { is_expected.to validate(:patch, '/user/group_order_articles/{id}', 403, api_auth(update_params)) } - it { is_expected.to validate(:delete, '/user/group_order_articles/{id}', 200, api_auth({ 'id' => goa.id })) } - end - - it_handles_invalid_token_and_scope(:get, '/user/group_order_articles') - it_handles_invalid_token_and_scope(:post, '/user/group_order_articles', -> { api_auth(create_params) }) - it_handles_invalid_token_and_scope(:get, '/user/group_order_articles/{id}', -> { api_auth({ 'id' => goa.id }) }) - it_handles_invalid_token_and_scope(:patch, '/user/group_order_articles/{id}', -> { api_auth(update_params) }) - it_handles_invalid_token_and_scope(:delete, '/user/group_order_articles/{id}', -> { api_auth({ 'id' => goa.id }) }) - end - end - - context 'config' do - let(:api_scopes) { ['config:user'] } - - it { is_expected.to validate(:get, '/config', 200, api_auth) } - it { is_expected.to validate(:get, '/config', 401) } - - it_handles_invalid_token_and_scope(:get, '/config') - end - - context 'navigation' do - it { is_expected.to validate(:get, '/navigation', 200, api_auth) } - it { is_expected.to validate(:get, '/navigation', 401) } - - it_handles_invalid_token(:get, '/navigation') - end - - context 'financial_transactions' do - let(:api_scopes) { ['finance:read'] } - let(:user) { create(:user, :role_finance) } - let(:other_user) { create :user, :ordergroup } - let!(:ft_1) { create :financial_transaction, ordergroup: other_user.ordergroup } - let!(:ft_2) { create :financial_transaction, ordergroup: other_user.ordergroup } - - it { is_expected.to validate(:get, '/financial_transactions', 200, api_auth) } - it { is_expected.to validate(:get, '/financial_transactions/{id}', 200, api_auth({ 'id' => ft_2.id })) } - it { is_expected.to validate(:get, '/financial_transactions/{id}', 404, api_auth({ 'id' => FinancialTransaction.last.id + 1 })) } - - context 'without role_finance' do - let(:user) { create(:user) } - - it { is_expected.to validate(:get, '/financial_transactions', 403, api_auth) } - it { is_expected.to validate(:get, '/financial_transactions/{id}', 403, api_auth({ 'id' => ft_2.id })) } - end - - it_handles_invalid_token_and_scope(:get, '/financial_transactions') - it_handles_invalid_token_and_scope(:get, '/financial_transactions/{id}', -> { api_auth({ 'id' => ft_2.id }) }) - end - - context 'financial_transaction_classes' do - let!(:cla_1) { create :financial_transaction_class } - let!(:cla_2) { create :financial_transaction_class } - - it { is_expected.to validate(:get, '/financial_transaction_classes', 200, api_auth) } - it { is_expected.to validate(:get, '/financial_transaction_classes/{id}', 200, api_auth({ 'id' => cla_2.id })) } - it { is_expected.to validate(:get, '/financial_transaction_classes/{id}', 404, api_auth({ 'id' => cla_2.id + 1 })) } - - it_handles_invalid_token(:get, '/financial_transaction_classes') - it_handles_invalid_token(:get, '/financial_transaction_classes/{id}', -> { api_auth({ 'id' => cla_1.id }) }) - end - - context 'financial_transaction_types' do - let!(:tpy_1) { create :financial_transaction_type } - let!(:tpy_2) { create :financial_transaction_type } - - it { is_expected.to validate(:get, '/financial_transaction_types', 200, api_auth) } - it { is_expected.to validate(:get, '/financial_transaction_types/{id}', 200, api_auth({ 'id' => tpy_2.id })) } - it { is_expected.to validate(:get, '/financial_transaction_types/{id}', 404, api_auth({ 'id' => tpy_2.id + 1 })) } - - it_handles_invalid_token(:get, '/financial_transaction_types') - it_handles_invalid_token(:get, '/financial_transaction_types/{id}', -> { api_auth({ 'id' => tpy_1.id }) }) - end - - context 'orders' do - let(:api_scopes) { ['orders:read'] } - let!(:order) { create :order } - - it { is_expected.to validate(:get, '/orders', 200, api_auth) } - it { is_expected.to validate(:get, '/orders/{id}', 200, api_auth({ 'id' => order.id })) } - it { is_expected.to validate(:get, '/orders/{id}', 404, api_auth({ 'id' => Order.last.id + 1 })) } - - it_handles_invalid_token_and_scope(:get, '/orders') - it_handles_invalid_token_and_scope(:get, '/orders/{id}', -> { api_auth({ 'id' => order.id }) }) - end - - context 'order_articles' do - let(:api_scopes) { ['orders:read'] } - let!(:order_article) { create(:order, article_count: 1).order_articles.first } - let!(:stock_article) { create(:stock_article) } - let!(:stock_order_article) { create(:stock_order, article_ids: [stock_article.id]).order_articles.first } - - it { is_expected.to validate(:get, '/order_articles', 200, api_auth) } - it { is_expected.to validate(:get, '/order_articles/{id}', 200, api_auth({ 'id' => order_article.id })) } - it { is_expected.to validate(:get, '/order_articles/{id}', 200, api_auth({ 'id' => stock_order_article.id })) } - it { is_expected.to validate(:get, '/order_articles/{id}', 404, api_auth({ 'id' => Article.last.id + 1 })) } - - it_handles_invalid_token_and_scope(:get, '/order_articles') - it_handles_invalid_token_and_scope(:get, '/order_articles/{id}', -> { api_auth({ 'id' => order_article.id }) }) - end - - context 'article_categories' do - let!(:cat_1) { create :article_category } - let!(:cat_2) { create :article_category } - - it { is_expected.to validate(:get, '/article_categories', 200, api_auth) } - it { is_expected.to validate(:get, '/article_categories/{id}', 200, api_auth({ 'id' => cat_2.id })) } - it { is_expected.to validate(:get, '/article_categories/{id}', 404, api_auth({ 'id' => cat_2.id + 1 })) } - - it_handles_invalid_token(:get, '/article_categories') - it_handles_invalid_token(:get, '/article_categories/{id}', -> { api_auth({ 'id' => cat_1.id }) }) - end - end - - # needs to be last context so it is always run at the end - context 'and finally' do - it 'tests all documented routes' do - is_expected.to validate_all_paths - end - end -end diff --git a/spec/api/v1/user/financial_transactions_spec.rb b/spec/api/v1/user/financial_transactions_spec.rb deleted file mode 100644 index c7e8f826..00000000 --- a/spec/api/v1/user/financial_transactions_spec.rb +++ /dev/null @@ -1,109 +0,0 @@ -require 'spec_helper' - -# Most routes are tested in the swagger_spec, this tests endpoints that change data. -describe Api::V1::User::FinancialTransactionsController, type: :controller do - include ApiOAuth - let(:user) { create(:user, :ordergroup) } - let(:api_scopes) { ['finance:user'] } - - let(:ftc1) { create :financial_transaction_class } - let(:ftc2) { create :financial_transaction_class } - let(:ftt1) { create :financial_transaction_type, financial_transaction_class: ftc1 } - let(:ftt2) { create :financial_transaction_type, financial_transaction_class: ftc2 } - let(:ftt3) { create :financial_transaction_type, financial_transaction_class: ftc2 } - - let(:amount) { rand(-100..100) } - let(:note) { Faker::Lorem.sentence } - - let(:json_ft) { json_response['financial_transaction'] } - - shared_examples "financial_transactions endpoint success" do - before { request } - - it "returns status 200" do - expect(response).to have_http_status :ok - end - end - - shared_examples "financial_transactions create/update success" do - include_examples "financial_transactions endpoint success" - - it "returns the financial_transaction" do - expect(json_ft['id']).to be_present - expect(json_ft['financial_transaction_type_id']).to eq ftt1.id - expect(json_ft['financial_transaction_type_name']).to eq ftt1.name - expect(json_ft['amount']).to eq amount - expect(json_ft['note']).to eq note - expect(json_ft['user_id']).to eq user.id - end - - it "updates the financial_transaction" do - resulting_ft = FinancialTransaction.where(id: json_ft['id']).first - expect(resulting_ft).to be_present - expect(resulting_ft.financial_transaction_type).to eq ftt1 - expect(resulting_ft.amount).to eq amount - expect(resulting_ft.note).to eq note - expect(resulting_ft.user).to eq user - end - end - - shared_examples "financial_transactions endpoint failure" do |status| - it "returns status #{status}" do - request - expect(response.status).to eq status - end - - it "does not change the ordergroup" do - expect { request }.to_not change { - user.ordergroup.attributes - } - end - - it "does not change the financial_transactions of ordergroup" do - expect { request }.to_not change { - user.ordergroup.financial_transactions.count - } - end - end - - describe "POST :create" do - let(:ft_params) { { amount: amount, financial_transaction_type_id: ftt1.id, note: note } } - let(:request) { post :create, params: { financial_transaction: ft_params, foodcoop: 'f' } } - - context 'without using self service' do - include_examples "financial_transactions endpoint failure", 403 - end - - context 'with using self service' do - before { FoodsoftConfig[:use_self_service] = true } - - context "with no existing financial transaction" do - include_examples "financial_transactions create/update success" - end - - context "with existing financial transaction" do - before { user.ordergroup.add_financial_transaction! 5000, 'for ordering', user, ftt3 } - - include_examples "financial_transactions create/update success" - end - - context "with invalid financial transaction type" do - let(:ft_params) { { amount: amount, financial_transaction_type_id: -1, note: note } } - - include_examples "financial_transactions endpoint failure", 404 - end - - context "without note" do - let(:ft_params) { { amount: amount, financial_transaction_type_id: ftt1.id } } - - include_examples "financial_transactions endpoint failure", 422 - end - - context 'without enough balance' do - before { FoodsoftConfig[:minimum_balance] = 1000 } - - include_examples "financial_transactions endpoint failure", 403 - end - end - end -end diff --git a/spec/api/v1/user/group_order_articles_spec.rb b/spec/api/v1/user/group_order_articles_spec.rb deleted file mode 100644 index 3bfa299e..00000000 --- a/spec/api/v1/user/group_order_articles_spec.rb +++ /dev/null @@ -1,220 +0,0 @@ -require 'spec_helper' - -# Most routes are tested in the swagger_spec, this tests endpoints that change data. -describe Api::V1::User::GroupOrderArticlesController, type: :controller do - include ApiOAuth - let(:user) { create(:user, :ordergroup) } - let(:json_goa) { json_response['group_order_article'] } - let(:json_oa) { json_response['order_article'] } - let(:api_scopes) { ['group_orders:user'] } - - let(:order) { create(:order, article_count: 1) } - let(:oa_1) { order.order_articles.first } - - let(:other_quantity) { rand(1..10) } - let(:other_tolerance) { rand(1..10) } - let(:user_other) { create(:user, :ordergroup) } - let!(:go_other) { create(:group_order, order: order, ordergroup: user_other.ordergroup) } - let!(:goa_other) { create(:group_order_article, group_order: go_other, order_article: oa_1, quantity: other_quantity, tolerance: other_tolerance) } - - before { go_other.update_price!; user_other.ordergroup.update_stats! } - - shared_examples "group_order_articles endpoint success" do - before { request } - - it "returns status 200" do - expect(response).to have_http_status :ok - end - - it "returns the order_article" do - expect(json_oa['id']).to eq oa_1.id - expect(json_oa['quantity']).to eq new_quantity + other_quantity - expect(json_oa['tolerance']).to eq new_tolerance + other_tolerance - end - - it "updates the group_order" do - go = nil - expect { - request - go = user.ordergroup.group_orders.where(order: order).last - }.to change { go&.updated_by }.to(user) - .and change { go&.price } - end - end - - shared_examples "group_order_articles create/update success" do - include_examples "group_order_articles endpoint success" - - it "returns the group_order_article" do - expect(json_goa['id']).to be_present - expect(json_goa['order_article_id']).to eq oa_1.id - expect(json_goa['quantity']).to eq new_quantity - expect(json_goa['tolerance']).to eq new_tolerance - end - - it "updates the group_order_article" do - resulting_goa = GroupOrderArticle.where(id: json_goa['id']).first - expect(resulting_goa).to be_present - expect(resulting_goa.quantity).to eq new_quantity - expect(resulting_goa.tolerance).to eq new_tolerance - end - end - - shared_examples "group_order_articles endpoint failure" do |status| - it "returns status #{status}" do - request - expect(response.status).to eq status - end - - it "does not change the group_order" do - expect { request }.to_not change { - go = user.ordergroup.group_orders.where(order: order).last - go&.attributes - } - end - - it "does not change the group_order_article" do - expect { request }.to_not change { - goa = GroupOrderArticle.joins(:group_order) - .where(order_article_id: oa_1.id, group_orders: { ordergroup: user.ordergroup }).last - goa&.attributes - } - end - end - - describe "POST :create" do - let(:new_quantity) { rand(1..10) } - let(:new_tolerance) { rand(1..10) } - - let(:goa_params) { { order_article_id: oa_1.id, quantity: new_quantity, tolerance: new_tolerance } } - let(:request) { post :create, params: { group_order_article: goa_params, foodcoop: 'f' } } - - context "with no existing group_order" do - include_examples "group_order_articles create/update success" - end - - context "with an existing group_order" do - let!(:go) { create(:group_order, order: order, ordergroup: user.ordergroup) } - - include_examples "group_order_articles create/update success" - end - - context "with an existing group_order_article" do - let!(:go) { create(:group_order, order: order, ordergroup: user.ordergroup) } - let!(:goa) { create(:group_order_article, group_order: go, order_article: oa_1, quantity: 0, tolerance: 1) } - - before { go.update_price!; user.ordergroup.update_stats! } - - include_examples "group_order_articles endpoint failure", 422 - end - - context "with invalid parameter values" do - let(:goa_params) { { order_article_id: oa_1.id, quantity: -1, tolerance: new_tolerance } } - - include_examples "group_order_articles endpoint failure", 422 - end - - context 'with a closed order' do - let(:order) { create(:order, article_count: 1, state: :finished) } - - include_examples "group_order_articles endpoint failure", 404 - end - - context 'without enough balance' do - before { FoodsoftConfig[:minimum_balance] = 1000 } - - include_examples "group_order_articles endpoint failure", 403 - end - - context 'without enough apple points' do - before { allow_any_instance_of(Ordergroup).to receive(:not_enough_apples?).and_return(true) } - - include_examples "group_order_articles endpoint failure", 403 - end - end - - describe "PATCH :update" do - let(:new_quantity) { rand(2..10) } - let(:goa_params) { { quantity: new_quantity, tolerance: new_tolerance } } - let(:request) { patch :update, params: { id: goa.id, group_order_article: goa_params, foodcoop: 'f' } } - let(:new_tolerance) { rand(2..10) } - - let!(:go) { create(:group_order, order: order, ordergroup: user.ordergroup) } - let!(:goa) { create(:group_order_article, group_order: go, order_article: oa_1, quantity: 1, tolerance: 0) } - - before { go.update_price!; user.ordergroup.update_stats! } - - context "happy flow" do - include_examples "group_order_articles create/update success" - end - - context "with invalid parameter values" do - let(:goa_params) { { order_article_id: oa_1.id, quantity: -1, tolerance: new_tolerance } } - - include_examples "group_order_articles endpoint failure", 422 - end - - context 'with a closed order' do - let(:order) { create(:order, article_count: 1, state: :finished) } - - include_examples "group_order_articles endpoint failure", 404 - end - - context 'without enough balance' do - before { FoodsoftConfig[:minimum_balance] = 1000 } - - include_examples "group_order_articles endpoint failure", 403 - end - - context 'without enough apple points' do - before { allow_any_instance_of(Ordergroup).to receive(:not_enough_apples?).and_return(true) } - - include_examples "group_order_articles endpoint failure", 403 - end - end - - describe "DELETE :destroy" do - let(:new_quantity) { 0 } - let(:request) { delete :destroy, params: { id: goa.id, foodcoop: 'f' } } - let(:new_tolerance) { 0 } - - let!(:go) { create(:group_order, order: order, ordergroup: user.ordergroup) } - let!(:goa) { create(:group_order_article, group_order: go, order_article: oa_1) } - - before { go.update_price!; user.ordergroup.update_stats! } - - shared_examples "group_order_articles destroy success" do - include_examples "group_order_articles endpoint success" - - it "does not return the group_order_article" do - expect(json_goa).to be_nil - end - - it "deletes the group_order_article" do - expect(GroupOrderArticle.where(id: goa.id)).to be_empty - end - end - - context "happy flow" do - include_examples "group_order_articles destroy success" - end - - context 'with a closed order' do - let(:order) { create(:order, article_count: 1, state: :finished) } - - include_examples "group_order_articles endpoint failure", 404 - end - - context 'without enough balance' do - before { FoodsoftConfig[:minimum_balance] = 1000 } - - include_examples "group_order_articles destroy success" - end - - context 'without enough apple points' do - before { allow_any_instance_of(Ordergroup).to receive(:not_enough_apples?).and_return(true) } - - include_examples "group_order_articles destroy success" - end - end -end diff --git a/spec/api/v1/user/ordergroup_spec.rb b/spec/api/v1/user/ordergroup_spec.rb deleted file mode 100644 index 5eacb63e..00000000 --- a/spec/api/v1/user/ordergroup_spec.rb +++ /dev/null @@ -1,55 +0,0 @@ -require 'spec_helper' - -describe Api::V1::User::OrdergroupController, type: :controller do - include ApiOAuth - let(:user) { create :user, :ordergroup } - let(:api_scopes) { ['finance:user'] } - - let(:ftc1) { create :financial_transaction_class } - let(:ftc2) { create :financial_transaction_class } - let(:ftt1) { create :financial_transaction_type, financial_transaction_class: ftc1 } - let(:ftt2) { create :financial_transaction_type, financial_transaction_class: ftc2 } - let(:ftt3) { create :financial_transaction_type, financial_transaction_class: ftc2 } - - describe "GET :financial_overview" do - let(:order) { create(:order, article_count: 1) } - let(:json_financial_overview) { json_response['financial_overview'] } - let(:oa_1) { order.order_articles.first } - - let!(:go) { create(:group_order, order: order, ordergroup: user.ordergroup) } - let!(:goa) { create(:group_order_article, group_order: go, order_article: oa_1, quantity: 1, tolerance: 0) } - - before { go.update_price!; user.ordergroup.update_stats! } - - before do - og = user.ordergroup - og.add_financial_transaction!(-1, '-1', user, ftt1) - og.add_financial_transaction!(2, '2', user, ftt1) - og.add_financial_transaction!(3, '3', user, ftt1) - - og.add_financial_transaction!(-10, '-10', user, ftt2) - og.add_financial_transaction!(20, '20', user, ftt2) - og.add_financial_transaction!(30, '30', user, ftt2) - - og.add_financial_transaction!(-100, '-100', user, ftt3) - og.add_financial_transaction!(200, '200', user, ftt3) - og.add_financial_transaction!(300, '300', user, ftt3) - end - - it "returns correct values" do - get :financial_overview, params: { foodcoop: 'f' } - expect(json_financial_overview['account_balance']).to eq 444 - expect(json_financial_overview['available_funds']).to eq 444 - go.price - - ftcs = Hash[json_financial_overview['financial_transaction_class_sums'].map { |x| [x['id'], x] }] - - ftcs1 = ftcs[ftc1.id] - expect(ftcs1['name']).to eq ftc1.name - expect(ftcs1['amount']).to eq 4 - - ftcs2 = ftcs[ftc2.id] - expect(ftcs2['name']).to eq ftc2.name - expect(ftcs2['amount']).to eq 440 - end - end -end diff --git a/spec/app_config.yml b/spec/app_config.yml index 2e146be9..a9bd72b0 100644 --- a/spec/app_config.yml +++ b/spec/app_config.yml @@ -6,6 +6,7 @@ default: &defaults multi_coop_install: false + use_self_service: true default_scope: 'f' name: FC Minimal diff --git a/spec/requests/api/article_categories_spec.rb b/spec/requests/api/article_categories_spec.rb new file mode 100644 index 00000000..4c079ff2 --- /dev/null +++ b/spec/requests/api/article_categories_spec.rb @@ -0,0 +1,53 @@ +require 'swagger_helper' + +describe 'Article Categories', type: :request do + include ApiHelper + + path '/article_categories' do + get 'article categories' do + tags 'Category' + produces 'application/json' + pagination_param + let(:order_article) { create(:order, article_count: 1).order_articles.first } + let(:stock_article) { create(:stock_article) } + let(:stock_order_article) { create(:stock_order, article_ids: [stock_article.id]).order_articles.first } + + response '200', 'success' do + schema type: :object, properties: { + article_categories: { + type: :array, + items: { + '$ref': '#/components/schemas/ArticleCategory' + } + } + } + run_test! + end + + it_handles_invalid_token + end + end + + path '/article_categories/{id}' do + get 'find article category by id' do + tags 'Category' + produces 'application/json' + id_url_param + + response '200', 'article category found' do + schema type: :object, properties: { + article_categories: { + type: :array, + items: { + '$ref': '#/components/schemas/ArticleCategory' + } + } + } + let(:id) { create(:article_category, name: 'dairy').id } + run_test! + end + it_handles_invalid_token_with_id + it_cannot_find_object + end + end +end diff --git a/spec/requests/api/configs_spec.rb b/spec/requests/api/configs_spec.rb new file mode 100644 index 00000000..75f48ceb --- /dev/null +++ b/spec/requests/api/configs_spec.rb @@ -0,0 +1,20 @@ +require 'swagger_helper' + +describe 'Config', type: :request do + include ApiHelper + + path '/config' do + get 'configuration variables' do + tags 'General' + produces 'application/json' + let(:api_scopes) { ['config:user'] } + + response '200', 'success' do + schema type: :object, properties: {} + run_test! + end + + it_handles_invalid_token_and_scope + end + end +end diff --git a/spec/requests/api/financial_transaction_classes_spec.rb b/spec/requests/api/financial_transaction_classes_spec.rb new file mode 100644 index 00000000..1eaf046f --- /dev/null +++ b/spec/requests/api/financial_transaction_classes_spec.rb @@ -0,0 +1,54 @@ +require 'swagger_helper' + +describe 'Financial Transaction Classes', type: :request do + include ApiHelper + + path '/financial_transaction_classes' do + get 'financial transaction classes' do + tags 'Category' + produces 'application/json' + pagination_param + let(:financial_transaction_class) { create(:financial_transaction_class) } + + response '200', 'success' do + schema type: :object, properties: { + meta: { '$ref' => '#/components/schemas/Meta' }, + financial_transaction_class: { + type: :array, + items: { + '$ref': '#/components/schemas/FinancialTransactionClass' + } + } + } + + run_test! + end + + it_handles_invalid_token + end + end + + path '/financial_transaction_classes/{id}' do + get 'Retrieves a financial transaction class' do + tags 'Category' + produces 'application/json' + id_url_param + + response '200', 'financial transaction class found' do + schema type: :object, properties: { + financial_transaction_classes: { + type: :array, + items: { + '$ref': '#/components/schemas/FinancialTransactionClass' + } + } + } + let(:id) { create(:financial_transaction_class).id } + run_test! + end + + it_handles_invalid_token_with_id + it_cannot_find_object 'financial transaction class not found' + end + end +end diff --git a/spec/requests/api/financial_transaction_types_spec.rb b/spec/requests/api/financial_transaction_types_spec.rb new file mode 100644 index 00000000..82a30f83 --- /dev/null +++ b/spec/requests/api/financial_transaction_types_spec.rb @@ -0,0 +1,52 @@ +require 'swagger_helper' + +describe 'Financial Transaction types', type: :request do + include ApiHelper + + path '/financial_transaction_types' do + get 'financial transaction types' do + tags 'Category' + produces 'application/json' + pagination_param + let(:financial_transaction_type) { create(:financial_transaction_type) } + response '200', 'success' do + schema type: :object, properties: { + meta: { '$ref' => '#/components/schemas/Meta' }, + financial_transaction_type: { + type: :array, + items: { + '$ref': '#/components/schemas/FinancialTransactionType' + } + } + } + run_test! + end + + it_handles_invalid_token + end + end + + path '/financial_transaction_types/{id}' do + get 'find financial transaction type by id' do + tags 'Category' + produces 'application/json' + id_url_param + + response '200', 'financial transaction type found' do + schema type: :object, properties: { + financial_transaction_types: { + type: :array, + items: { + '$ref': '#/components/schemas/FinancialTransactionType' + } + } + } + let(:id) { create(:financial_transaction_type).id } + run_test! + end + + it_handles_invalid_token_with_id + it_cannot_find_object 'financial transaction type not found' + end + end +end diff --git a/spec/requests/api/financial_transactions_spec.rb b/spec/requests/api/financial_transactions_spec.rb new file mode 100644 index 00000000..1d3ef2b9 --- /dev/null +++ b/spec/requests/api/financial_transactions_spec.rb @@ -0,0 +1,56 @@ +require 'swagger_helper' + +describe 'Financial Transaction', type: :request do + include ApiHelper + let!(:finance_user) { create(:user, groups: [create(:workgroup, role_finance: true)]) } + let!(:api_scopes) { ['finance:read', 'finance:write'] } + let(:api_access_token) { create(:oauth2_access_token, resource_owner_id: finance_user.id, scopes: api_scopes&.join(' ')).token } + let(:financial_transaction) { create(:financial_transaction, user: user) } + + path '/financial_transactions' do + get 'financial transactions' do + tags 'Financial Transaction' + produces 'application/json' + pagination_param + + response '200', 'success' do + schema type: :object, properties: { + meta: { '$ref' => '#/components/schemas/Meta' }, + financial_transaction: { + type: :array, + items: { + '$ref': '#/components/schemas/FinancialTransaction' + } + } + } + + run_test! + end + it_handles_invalid_token_and_scope + end + end + + path '/financial_transactions/{id}' do + get 'Retrieves a financial transaction ' do + tags 'Financial Transaction' + produces 'application/json' + id_url_param + + response '200', 'financial transaction found' do + schema type: :object, properties: { + financial_transaction: { + type: :array, + items: { + '$ref': '#/components/schemas/FinancialTransaction' + } + } + } + let(:id) { FinancialTransaction.create(user: user).id } + run_test! + end + it_handles_invalid_token_with_id + it_handles_invalid_scope_with_id + it_cannot_find_object 'financial transaction not found' + end + end +end diff --git a/spec/requests/api/navigations_spec.rb b/spec/requests/api/navigations_spec.rb new file mode 100644 index 00000000..c2312437 --- /dev/null +++ b/spec/requests/api/navigations_spec.rb @@ -0,0 +1,24 @@ +require 'swagger_helper' + +describe 'Navigation', type: :request do + include ApiHelper + + path '/navigation' do + get 'navigation' do + tags 'General' + produces 'application/json' + + response '200', 'success' do + schema type: :object, properties: { + navigation: { + '$ref' => '#/components/schemas/Navigation' + } + } + + run_test! + end + + it_handles_invalid_token + end + end +end diff --git a/spec/requests/api/order_articles_spec.rb b/spec/requests/api/order_articles_spec.rb new file mode 100644 index 00000000..17feefa6 --- /dev/null +++ b/spec/requests/api/order_articles_spec.rb @@ -0,0 +1,115 @@ +require 'swagger_helper' + +describe 'Order Articles', type: :request do + include ApiHelper + + path '/order_articles' do + get 'order articles' do + tags 'Order' + produces 'application/json' + pagination_param + q_ordered_url_param + + let(:api_scopes) { ['orders:read', 'orders:write'] } + let(:order) { create(:order, article_count: 4) } + let(:order_articles) { order.order_articles } + + before do + order_articles[0].update! quantity: 0, tolerance: 0, units_to_order: 0 + order_articles[1].update! quantity: 1, tolerance: 0, units_to_order: 0 + order_articles[2].update! quantity: 0, tolerance: 1, units_to_order: 0 + order_articles[3].update! quantity: 0, tolerance: 0, units_to_order: 1 + end + + response '200', 'success' do + schema type: :object, properties: { + meta: { '$ref' => '#/components/schemas/Meta' }, + order_articles: { + type: :array, + items: { + '$ref': '#/components/schemas/OrderArticle' + } + } + } + describe '(unset)' do + run_test! + end + + describe 'all' do + let(:q) { { q: { ordered: 'all' } } } + + run_test! do |response| + json_order_articles = JSON.parse(response.body)['order_articles'] + json_order_article_ids = json_order_articles.map { |d| d['id'].to_i } + expect(json_order_article_ids).to match_array order_articles[1..2].map(&:id) + end + end + + describe 'when ordered by supplier' do + let(:q) { { q: { ordered: 'supplier' } } } + + run_test! do |response| + json_order_articles = JSON.parse(response.body)['order_articles'] + json_order_article_ids = json_order_articles.map { |d| d['id'].to_i } + expect(json_order_article_ids).to match_array [order_articles[3].id] + end + end + + describe 'when ordered by member' do + let(:q) { { q: { ordered: 'member' } } } + + run_test! do |response| + json_order_articles = JSON.parse(response.body)['order_articles'] + expect(json_order_articles.count).to eq 0 + end + end + + context 'when ordered by user' do + let(:user) { create(:user, :ordergroup) } + let(:go) { create(:group_order, order: order, ordergroup: user.ordergroup) } + + before do + create(:group_order_article, group_order: go, order_article: order_articles[1], quantity: 1) + create(:group_order_article, group_order: go, order_article: order_articles[2], tolerance: 0) + end + + describe 'member' do + let(:q) { { q: { ordered: 'member' } } } + + run_test! do |response| + json_order_articles = JSON.parse(response.body)['order_articles'] + json_order_article_ids = json_order_articles.map { |d| d['id'].to_i } + expect(json_order_article_ids).to match_array order_articles[1..2].map(&:id) + end + end + end + end + + it_handles_invalid_token_and_scope + end + end + + path '/order_articles/{id}' do + get 'order articles' do + tags 'Order' + produces 'application/json' + id_url_param + let(:api_scopes) { ['orders:read', 'orders:write'] } + + response '200', 'success' do + schema type: :object, properties: { + order_article: { + '$ref': '#/components/schemas/OrderArticle' + } + } + let(:order) { create(:order, article_count: 1) } + let(:id) { order.order_articles.first.id } + + run_test! + end + + it_handles_invalid_token_and_scope + it_cannot_find_object 'order article not found' + end + end +end diff --git a/spec/requests/api/orders_spec.rb b/spec/requests/api/orders_spec.rb new file mode 100644 index 00000000..c0505d7f --- /dev/null +++ b/spec/requests/api/orders_spec.rb @@ -0,0 +1,55 @@ +require 'swagger_helper' + +describe 'Orders', type: :request do + include ApiHelper + let(:api_scopes) { ['orders:read'] } + + path '/orders' do + get 'orders' do + tags 'Order' + produces 'application/json' + pagination_param + let(:order) { create(:order) } + + response '200', 'success' do + schema type: :object, properties: { + meta: { '$ref' => '#/components/schemas/Meta' }, + ordes: { + type: :array, + items: { + '$ref': '#/components/schemas/Order' + } + } + } + + run_test! + end + + it_handles_invalid_token_and_scope + end + end + + path '/orders/{id}' do + get 'Order' do + tags 'Order' + produces 'application/json' + id_url_param + + let(:order) { create(:order) } + + response '200', 'success' do + schema type: :object, properties: { + order: { '$ref' => '#/components/schemas/Order' } + } + let(:id) { order.id } + + run_test! do |response| + expect(JSON.parse(response.body)['order']['id']).to eq order.id + end + end + + it_handles_invalid_token_and_scope + it_cannot_find_object 'order not found' + end + end +end diff --git a/spec/requests/api/user/financial_transactions_spec.rb b/spec/requests/api/user/financial_transactions_spec.rb new file mode 100644 index 00000000..aca9d7cd --- /dev/null +++ b/spec/requests/api/user/financial_transactions_spec.rb @@ -0,0 +1,106 @@ +require 'swagger_helper' + +describe 'User', type: :request do + include ApiHelper + + let(:api_scopes) { ['finance:user'] } + let(:user) { create :user, groups: [create(:ordergroup)] } + let(:other_user2) { create :user } + let(:ft) { create(:financial_transaction, user: user, ordergroup: user.ordergroup) } + + before do + ft + end + + path '/user/financial_transactions' do + post 'create new financial transaction (requires enabled self service)' do + tags 'Financial Transaction' + consumes 'application/json' + produces 'application/json' + + parameter name: :financial_transaction, in: :body, schema: { + type: :object, + properties: { + amount: { type: :integer }, + financial_transaction_type: { type: :integer }, + note: { type: :string } + } + } + + let(:financial_transaction) { { amount: 3, financial_transaction_type_id: create(:financial_transaction_type).id, note: 'lirum larum' } } + + response '200', 'success' do + schema type: :object, properties: { + financial_transaction: { '$ref': '#/components/schemas/FinancialTransaction' } + } + run_test! + end + + it_handles_invalid_token_with_id + it_handles_invalid_scope_with_id 'user has no ordergroup, is below minimum balance, self service is disabled, or missing scope' + + response '404', 'financial transaction type not found' do + schema '$ref' => '#/components/schemas/Error404' + let(:financial_transaction) { { amount: 3, financial_transaction_type_id: 'invalid', note: 'lirum larum' } } + run_test! + end + + response '422', 'invalid parameter value' do + xit 'TODO: fix controller to actually send a 422 for invalid params: https://github.com/foodcoops/foodsoft/issues/999' + # schema '$ref' => '#/components/schemas/Error422' + # let(:financial_transaction) { { amount: -3, financial_transaction_type_id: create(:financial_transaction_type).id, note: -2 } } + # run_test! + end + end + + get "financial transactions of the member's ordergroup" do + tags 'User', 'Financial Transaction' + produces 'application/json' + pagination_param + + response '200', 'success' do + schema type: :object, properties: { + meta: { '$ref': '#/components/schemas/Meta' }, + financial_transaction: { + type: :array, + items: { + '$ref': '#/components/schemas/FinancialTransaction' + } + } + } + + run_test! do |response| + data = JSON.parse(response.body) + expect(data['financial_transactions'].first['id']).to eq(ft.id) + end + end + + it_handles_invalid_token_and_scope + end + end + + path '/user/financial_transactions/{id}' do + get 'find financial transaction by id' do + tags 'User', 'Financial Transaction' + produces 'application/json' + id_url_param + + response '200', 'success' do + schema type: :object, properties: { + financial_transaction: { + '$ref': '#/components/schemas/FinancialTransaction' + } + } + let(:id) { ft.id } + run_test! do |response| + data = JSON.parse(response.body) + expect(data['financial_transaction']['id']).to eq(ft.id) + end + end + + it_handles_invalid_token_with_id + it_handles_invalid_scope_with_id 'user has no ordergroup or missing scope' + it_cannot_find_object 'financial transaction not found' + end + end +end diff --git a/spec/requests/api/user/group_order_articles_spec.rb b/spec/requests/api/user/group_order_articles_spec.rb new file mode 100644 index 00000000..205a4070 --- /dev/null +++ b/spec/requests/api/user/group_order_articles_spec.rb @@ -0,0 +1,192 @@ +require 'swagger_helper' + +describe 'User', type: :request do + include ApiHelper + + let(:api_scopes) { ['group_orders:user'] } + let(:user) { create :user, groups: [create(:ordergroup)] } + let(:other_user2) { create :user } + let(:order) { create(:order, article_count: 4) } + let(:order_articles) { order.order_articles } + let(:group_order) { create :group_order, ordergroup: user.ordergroup, order_id: order.id } + let(:goa) { create :group_order_article, group_order: group_order, order_article: order_articles.first } + + before do + goa + end + + path '/user/group_order_articles' do + get 'group order articles' do + tags 'User', 'Order' + produces 'application/json' + pagination_param + q_ordered_url_param + + response '200', 'success' do + schema type: :object, properties: { + meta: { '$ref': '#/components/schemas/Meta' }, + group_order_article: { + type: :array, + items: { + '$ref': '#/components/schemas/GroupOrderArticle' + } + } + } + + run_test! do |response| + data = JSON.parse(response.body) + expect(data['group_order_articles'].first['id']).to eq(goa.id) + end + end + + it_handles_invalid_token + it_handles_invalid_scope 'user has no ordergroup or missing scope' + end + + post 'create new group order article' do + tags 'User', 'Order' + consumes 'application/json' + produces 'application/json' + parameter name: :group_order_article, in: :body, + description: 'group order article to create', + required: true, + schema: { '$ref': '#/components/schemas/GroupOrderArticleForCreate' } + + let(:group_order_article) { { order_article_id: order_articles.last.id, quantity: 1, tolerance: 2 } } + response '200', 'success' do + schema type: :object, properties: { + group_order_article: { + '$ref': '#/components/schemas/GroupOrderArticle' + }, + order_article: { + '$ref': '#/components/schemas/OrderArticle' + } + } + run_test! + end + + it_handles_invalid_token_with_id + it_handles_invalid_scope_with_id 'user has no ordergroup, order not open, is below minimum balance, has not enough apple points, or missing scope' + + response '404', 'order article not found in open orders' do + let(:group_order_article) { { order_article_id: 'invalid', quantity: 1, tolerance: 2 } } + schema '$ref' => '#/components/schemas/Error404' + run_test! + end + + response '422', 'invalid parameter value or group order article already exists' do + let(:group_order_article) { { order_article_id: goa.order_article_id, quantity: 1, tolerance: 2 } } + schema '$ref' => '#/components/schemas/Error422' + run_test! + end + end + end + + path '/user/group_order_articles/{id}' do + get 'find group order article by id' do + tags 'User', 'Order' + produces 'application/json' + id_url_param + + response '200', 'success' do + schema type: :object, properties: { + group_order_article: { + '$ref': '#/components/schemas/GroupOrderArticle' + }, + order_article: { + '$ref': '#/components/schemas/OrderArticle' + } + } + + let(:id) { goa.id } + run_test! do |response| + data = JSON.parse(response.body) + expect(data['group_order_article']['id']).to eq(goa.id) + end + end + + it_handles_invalid_scope_with_id + it_handles_invalid_token_with_id + it_cannot_find_object 'group order article not found' + end + + patch 'update a group order article (but delete if quantity and tolerance are zero)' do + tags 'User', 'Order' + consumes 'application/json' + produces 'application/json' + id_url_param + parameter name: :group_order_article, in: :body, + description: 'group order article update', + required: true, + schema: { '$ref': '#/components/schemas/GroupOrderArticleForUpdate' } + + let(:id) { goa.id } + let(:group_order_article) { { order_article_id: goa.order_article_id, quantity: 2, tolerance: 2 } } + + response '200', 'success' do + schema type: :object, properties: { + group_order_article: { + '$ref': '#/components/schemas/GroupOrderArticle' + } + } + run_test! + end + + response 401, 'not logged-in' do + schema '$ref' => '#/components/schemas/Error401' + let(:Authorization) { 'abc' } + run_test! + end + + response 403, 'user has no ordergroup, order not open, is below minimum balance, has not enough apple points, or missing scope' do + let(:api_scopes) { ['none'] } + schema '$ref' => '#/components/schemas/Error403' + run_test! + end + + response '404', 'order article not found in open orders' do + schema type: :object, properties: { + group_order_article: { + '$ref': '#/components/schemas/GroupOrderArticle' + } + } + let(:id) { 'invalid' } + run_test! + end + + response '422', 'invalid parameter value' do + let(:group_order_article) { { order_article_id: 'invalid', quantity: -5, tolerance: 'invalid' } } + schema '$ref' => '#/components/schemas/Error422' + run_test! + end + end + + delete 'remove group order article' do + tags 'User', 'Order' + consumes 'application/json' + produces 'application/json' + id_url_param + let(:api_scopes) { ['group_orders:user'] } + + response '200', 'success' do + schema type: :object, properties: { + group_order_article: { + '$ref': '#/components/schemas/GroupOrderArticle' + } + } + let(:id) { goa.id } + run_test! + end + + it_handles_invalid_token_with_id + + response 403, 'user has no ordergroup, order not open, is below minimum balance, has not enough apple points, or missing scope' do + let(:api_scopes) { ['none'] } + schema '$ref' => '#/components/schemas/Error403' + run_test! + end + + it_cannot_find_object 'order article not found in open orders' + end + end +end diff --git a/spec/requests/api/user/users_spec.rb b/spec/requests/api/user/users_spec.rb new file mode 100644 index 00000000..0d3196bc --- /dev/null +++ b/spec/requests/api/user/users_spec.rb @@ -0,0 +1,103 @@ +require 'swagger_helper' + +describe 'User', type: :request do + include ApiHelper + + path '/user' do + get 'info about the currently logged-in user' do + tags 'User' + produces 'application/json' + let(:api_scopes) { ['user:read'] } + let(:other_user1) { create :user } + let(:user) { create :user } + let(:other_user2) { create :user } + + response '200', 'success' do + schema type: :object, + properties: { + user: { + type: :object, + properties: { + id: { + type: :integer + }, + name: { + type: :string, + description: 'full name' + }, + email: { + type: :string, + description: 'email address' + }, + locale: { + type: :string, + description: 'language code' + } + }, + required: %w[id name email] + } + } + + run_test! do |response| + data = JSON.parse(response.body) + expect(data['user']['id']).to eq(user.id) + end + end + + it_handles_invalid_token_and_scope + end + end + + path '/user/financial_overview' do + get 'financial summary about the currently logged-in user' do + tags 'User', 'Financial Transaction' + produces 'application/json' + let(:user) { create :user, :ordergroup } + let(:api_scopes) { ['finance:user'] } + FinancialTransactionClass.create(name: 'TestTransaction') + + response 200, 'success' do + schema type: :object, + properties: { + financial_overview: { + type: :object, + properties: { + + account_balance: { + type: :number, + description: 'booked accout balance of ordergroup' + }, + available_funds: { + type: :number, + description: 'fund available to order articles' + }, + financial_transaction_class_sums: { + type: :array, + properties: { + id: { + type: :integer, + description: 'id of the financial transaction class' + }, + name: { + type: :string, + description: 'name of the financial transaction class' + }, + amount: { + type: :number, + description: 'sum of the amounts belonging to the financial transaction class' + } + }, + required: %w[id name amount] + } + }, + required: %w[account_balance available_funds financial_transaction_class_sums] + } + } + + run_test! + end + + it_handles_invalid_token_and_scope + end + end +end diff --git a/spec/support/api_helper.rb b/spec/support/api_helper.rb index ee0225f5..86e2ca07 100644 --- a/spec/support/api_helper.rb +++ b/spec/support/api_helper.rb @@ -5,21 +5,60 @@ module ApiHelper let(:user) { create(:user) } let(:api_scopes) { [] } # empty scopes for stricter testing (in reality this would be default_scopes) let(:api_access_token) { create(:oauth2_access_token, resource_owner_id: user.id, scopes: api_scopes&.join(' ')).token } - let(:api_authorization) { "Bearer #{api_access_token}" } + let(:Authorization) { "Bearer #{api_access_token}" } - def self.it_handles_invalid_token(method, path, params_block = -> { api_auth }) + def self.it_handles_invalid_token context 'with invalid access token' do - let(:api_access_token) { 'abc' } + let(:Authorization) { 'abc' } - it { is_expected.to validate(method, path, 401, instance_exec(¶ms_block)) } + response 401, 'not logged-in' do + schema '$ref' => '#/components/schemas/Error401' + run_test! + end end end - def self.it_handles_invalid_scope(method, path, params_block = -> { api_auth }) + def self.it_handles_invalid_token_with_id + context 'with invalid access token' do + let(:Authorization) { 'abc' } + let(:id) { 42 } # id doesn't matter here + + response 401, 'not logged-in' do + schema '$ref' => '#/components/schemas/Error401' + run_test! + end + end + end + + def self.it_handles_invalid_scope(description = 'missing scope') context 'with invalid scope' do let(:api_scopes) { ['none'] } - it { is_expected.to validate(method, path, 403, instance_exec(¶ms_block)) } + response 403, description do + schema '$ref' => '#/components/schemas/Error403' + run_test! + end + end + end + + def self.it_handles_invalid_scope_with_id(description = 'missing scope') + context 'with invalid scope' do + let(:api_scopes) { ['none'] } + let(:id) { 42 } # id doesn't matter here + + response 403, description do + schema '$ref' => '#/components/schemas/Error403' + run_test! + end + end + end + + def self.it_cannot_find_object(description = 'not found') + let(:id) { 'invalid' } + + response 404, description do + schema '$ref' => '#/components/schemas/Error404' + run_test! end end @@ -27,13 +66,25 @@ module ApiHelper it_handles_invalid_token(*args) it_handles_invalid_scope(*args) end - end - # Add authentication to parameters for {Swagger::RspecHelpers#validate} - # @param params [Hash] Query parameters - # @return Query parameters with authentication header - # @see Swagger::RspecHelpers#validate - def api_auth(params = {}) - { '_headers' => { 'Authorization' => api_authorization } }.deep_merge(params) + def self.id_url_param + parameter name: :id, in: :path, type: :integer, required: true + end + + def self.pagination_param + parameter name: :per_page, in: :query, type: :integer, required: false + parameter name: :page, in: :query, type: :integer, required: false + end + + def self.q_ordered_url_param + parameter name: :q, in: :query, required: false, + description: "'member' show articles ordered by the user's ordergroup, 'all' by all members, and 'supplier' ordered at the supplier", + schema: { + type: :object, + properties: { + ordered: { '$ref' => '#/components/schemas/q_ordered' } + } + } + end end end diff --git a/spec/swagger_helper.rb b/spec/swagger_helper.rb new file mode 100644 index 00000000..912504b8 --- /dev/null +++ b/spec/swagger_helper.rb @@ -0,0 +1,513 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.configure do |config| + # Specify a root folder where Swagger JSON files are generated + # NOTE: If you're using the rswag-api to serve API descriptions, you'll need + # to ensure that it's configured to serve Swagger from the same folder + config.swagger_root = Rails.root.join('swagger').to_s + + # Define one or more Swagger documents and provide global metadata for each one + # When you run the 'rswag:specs:swaggerize' rake task, the complete Swagger will + # be generated at the provided relative path under swagger_root + # By default, the operations defined in spec files are added to the first + # document below. You can override this behavior by adding a swagger_doc tag to the + # the root example_group in your specs, e.g. describe '...', swagger_doc: 'v2/swagger.json' + config.swagger_docs = { + 'v1/swagger.yaml' => { + openapi: '3.0.3', + info: { + title: 'API V1', + version: 'v1' + }, + paths: {}, + components: { + schemas: { + pagination: { + type: :object, + properties: { + recordCount: { type: :integer }, + pageCount: { type: :integer }, + currentPage: { type: :integer }, + pageSize: { type: :integer } + }, + required: %w(recordCount pageCount currentPage pageSize) + }, + Order: { + type: :object, + properties: { + id: { + type: :integer + }, + name: { + type: :string, + description: "name of the order's supplier (or stock)" + }, + starts: { + type: :string, + format: 'date-time', + description: 'when the order was opened' + }, + ends: { + type: :string, + nullable: true, + format: 'date-time', + description: 'when the order will close or was closed' + }, + boxfill: { + type: :string, + nullable: true, + format: 'date-time', + description: 'when the order will enter or entered the boxfill phase' + }, + pickup: { + type: :string, + nullable: true, + format: :date, + description: 'pickup date' + }, + is_open: { + type: :boolean, + description: 'if the order is currently open or not' + }, + is_boxfill: { + type: :boolean, + description: 'if the order is currently in the boxfill phase or not' + } + } + }, + Article: { + type: :object, + properties: { + id: { + type: :integer + }, + name: { + type: :string + }, + supplier_id: { + type: :integer, + description: 'id of supplier, or 0 for stock articles' + }, + supplier_name: { + type: :string, + nullable: true, + description: 'name of the supplier, or null for stock articles' + }, + unit: { + type: :string, + description: 'amount of each unit, e.g. "100 g" or "kg"' + }, + unit_quantity: { + type: :integer, + description: 'units can only be ordered from the supplier in multiples of unit_quantity' + }, + note: { + type: :string, + nullable: true, + description: 'generic note' + }, + manufacturer: { + type: :string, + nullable: true, + description: 'manufacturer' + }, + origin: { + type: :string, + nullable: true, + description: 'origin, preferably (starting with a) 2-letter ISO country code' + }, + article_category_id: { + type: :integer, + description: 'id of article category' + }, + quantity_available: { + type: :integer, + description: 'number of units available (only present on stock articles)' + } + }, + required: %w[id name supplier_id supplier_name unit unit_quantity note manufacturer origin article_category_id] + }, + OrderArticle: { + type: :object, + properties: { + id: { + type: :integer + }, + order_id: { + type: :integer, + description: 'id of order this order article belongs to' + }, + price: { + type: :number, + format: :float, + description: 'foodcoop price' + }, + quantity: { + type: :integer, + description: 'number of units ordered by members' + }, + tolerance: { + type: :integer, + description: 'number of extra units that members are willing to buy to fill a box' + }, + units_to_order: { + type: :integer, + description: 'number of units to order from the supplier' + }, + article: { + '$ref': '#/components/schemas/Article' + } + } + }, + ArticleCategory: { + type: :object, + properties: { + id: { + type: :integer + }, + name: { + type: :string + } + }, + required: %w[id name] + }, + FinancialTransaction: { + allOf: [ + { '$ref': '#/components/schemas/FinancialTransactionForCreate' }, + { + type: :object, + properties: { + id: { + type: :integer + }, + amount: { + type: :number, + format: :float, + description: 'amount credited (negative for a debit transaction)' + }, + financial_transaction_type_id: { + type: :integer, + description: 'id of the type of the transaction' + }, + note: { + type: :string, + description: 'note entered with the transaction' + }, + user_id: { + type: :integer, + nullable: true, + description: 'id of user who entered the transaction (may be null for deleted users or 0 for a system user)' + }, + user_name: { + type: :string, + nullable: true, + description: 'name of user who entered the transaction (may be null or empty string for deleted users or system users)' + }, + financial_transaction_type_name: { + type: :string, + description: 'name of the type of the transaction' + }, + created_at: { + type: :string, + format: 'date-time', + description: 'when the transaction was entered' + } + }, + required: %w[id user_id user_name financial_transaction_type_name created_at] + } + ] + }, + FinancialTransactionForCreate: { + type: :object, + properties: { + amount: { + type: :number, + format: :float, + description: 'amount credited (negative for a debit transaction)' + }, + financial_transaction_type_id: + { + type: :integer, + description: 'id of the type of the transaction' + }, + note: { + type: :string, + description: 'note entered with the transaction' + } + }, + required: %w[amount note user_id] + }, + FinancialTransactionClass: { + type: :object, + properties: { + id: { + type: :integer + }, + name: { + type: :string + } + }, + required: %w[id name] + }, + FinancialTransactionType: { + type: :object, + properties: { + id: { + type: :integer + }, + name: { + type: :string + }, + name_short: { + type: :string, + nullable: true, + description: 'short name (used for bank transfers)' + }, + bank_account_id: { + type: :integer, + nullable: true, + description: 'id of the bank account used for this transaction type' + }, + bank_account_name: { + type: :string, + nullable: true, + description: 'name of the bank account used for this transaction type' + }, + bank_account_iban: { + type: :string, + nullable: true, + description: 'IBAN of the bank account used for this transaction type' + }, + financial_transaction_class_id: { + type: :integer, + description: 'id of the class of the transaction' + }, + financial_transaction_class_name: { + type: :string, + description: 'name of the class of the transaction' + } + }, + required: %w[id name financial_transaction_class_id financial_transaction_class_name] + }, + GroupOrderArticleForUpdate: { + type: :object, + properties: { + quantity: + { + type: :integer, + description: 'number of units ordered by the users ordergroup' + }, + tolerance: + { + type: :integer, + description: 'number of extra units the users ordergroup is willing to buy for filling a box' + } + } + }, + GroupOrderArticleForCreate: { + allOf: [ + { '$ref': '#/components/schemas/GroupOrderArticleForUpdate' }, + { + type: :object, + properties: { + order_article_id: + { + type: :integer, + description: 'id of order article' + } + } + } + ] + }, + GroupOrderArticle: { + allOf: [ + { '$ref': '#/components/schemas/GroupOrderArticleForCreate' }, + { + type: :object, + properties: { + id: { + type: :integer + }, + result: { + type: :number, + format: :float, + description: 'number of units the users ordergroup will receive or has received' + }, + total_price: + { + type: :number, + format: :float, + description: 'total price of this group order article' + }, + order_article_id: + { + type: :integer, + description: 'id of order article' + } + }, + required: %w[order_article_id] + } + ] + }, + q_ordered: { + type: :object, + properties: { + ordered: { + type: :string, + enum: %w[member all supplier] + } + } + }, + Meta: { + type: :object, + properties: { + page: { + type: :integer, + description: 'page number of the returned collection' + }, + per_page: { + type: :integer, + description: 'number of items per page' + }, + total_pages: { + type: :integer, + description: 'total number of pages' + }, + total_count: { + type: :integer, + description: 'total number of items in the collection' + } + }, + required: %w[page per_page total_pages total_count] + }, + Navigation: { + type: :array, + items: { + type: :object, + properties: { + name: { + type: :string, + description: 'title' + }, + url: { + type: :string, + description: 'link' + }, + items: { + '$ref': "#/components/schemas/Navigation" + } + }, + required: ['name'], + minProperties: 2 # name+url or name+items + } + }, + Error: { + type: :object, + properties: { + error: { + type: :string, + description: 'error code' + }, + error_description: { + type: :string, + description: 'human-readable error message (localized)' + } + } + }, + Error401: { + type: :object, + properties: { + error: { + type: :string, + description: 'unauthorized' + }, + error_description: { + '$ref': '#/components/schemas/Error/properties/error_description' + } + } + }, + Error403: { + type: :object, + properties: { + error: { + type: :string, + description: 'forbidden or invalid_scope' + }, + error_description: { + '$ref': '#/components/schemas/Error/properties/error_description' + } + } + }, + Error404: { + type: :object, + properties: { + error: { + type: :string, + description: 'not_found' + }, + error_description: { + '$ref': '#/components/schemas/Error/properties/error_description' + } + } + }, + Error422: { + type: :object, + properties: { + error: { + type: :string, + description: 'unprocessable entity' + }, + error_description: { + '$ref': '#/components/schemas/Error/properties/error_description' + } + } + } + }, + securitySchemes: { + oauth2: { + type: :oauth2, + flows: { + implicit: { + authorizationUrl: 'http://localhost:3000/f/oauth/authorize', + scopes: { + 'config:user': 'reading Foodsoft configuration for regular users', + 'config:read': 'reading Foodsoft configuration values', + 'config:write': 'reading and updating Foodsoft configuration values', + 'finance:user': 'accessing your own financial transactions', + 'finance:read': 'reading all financial transactions', + 'finance:write': 'reading and creating financial transactions', + 'user:read': 'reading your own user profile', + 'user:write': 'reading and updating your own user profile', + offline_access: 'retain access after user has logged out' + } + } + } + } + } + }, + servers: [ + { + url: 'http://{defaultHost}/f/api/v1', + variables: { + defaultHost: { + default: 'localhost:3000' + } + } + } + ], + security: [ + oauth2: [ + 'user:read' + ] + ] + } + } + + # Specify the format of the output Swagger file when running 'rswag:specs:swaggerize'. + # The swagger_docs configuration option has the filename including format in + # the key, this may want to be changed to avoid putting yaml in json files. + # Defaults to json. Accepts ':json' and ':yaml'. + config.swagger_format = :yaml +end From 3d81dd6b57d7bd10b626e00ac36775122bb2d363 Mon Sep 17 00:00:00 2001 From: viehlieb Date: Fri, 6 Jan 2023 16:12:41 +0100 Subject: [PATCH 045/105] rails up to 7.0and ruby to 2.7.2 --- .ruby-version | 2 +- Gemfile | 12 +- Gemfile.lock | 318 ++++++++++-------- bin/setup | 29 +- config/application.rb | 11 +- config/environments/production.rb | 16 +- config/environments/test.rb | 40 ++- config/initializers/assets.rb | 2 - .../initializers/content_security_policy.rb | 42 +-- config/initializers/cors.rb | 16 + config/initializers/currency_display.rb | 6 +- .../initializers/filter_parameter_logging.rb | 8 +- config/initializers/mail_receiver.rb | 4 +- config/initializers/new_framework_defaults.rb | 17 - .../new_framework_defaults_5_1.rb | 14 - .../new_framework_defaults_5_2.rb | 38 --- config/initializers/permissions_policy.rb | 11 + config/initializers/rails6_backports.rb | 98 ------ ..._to_active_storage_blobs.active_storage.rb | 22 ++ ..._storage_variant_records.active_storage.rb | 28 ++ ...e_storage_blobs_checksum.active_storage.rb | 8 + db/schema.rb | 241 ++++++------- db/seeds/seed_helper.rb | 4 +- 23 files changed, 485 insertions(+), 502 deletions(-) create mode 100644 config/initializers/cors.rb delete mode 100644 config/initializers/new_framework_defaults.rb delete mode 100644 config/initializers/new_framework_defaults_5_1.rb delete mode 100644 config/initializers/new_framework_defaults_5_2.rb create mode 100644 config/initializers/permissions_policy.rb delete mode 100644 config/initializers/rails6_backports.rb create mode 100644 db/migrate/20230106144438_add_service_name_to_active_storage_blobs.active_storage.rb create mode 100644 db/migrate/20230106144439_create_active_storage_variant_records.active_storage.rb create mode 100644 db/migrate/20230106144440_remove_not_null_on_active_storage_blobs_checksum.active_storage.rb diff --git a/.ruby-version b/.ruby-version index d48d3702..37c2961c 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -2.6.9 +2.7.2 diff --git a/Gemfile b/Gemfile index 61562099..81e30a0a 100644 --- a/Gemfile +++ b/Gemfile @@ -1,11 +1,11 @@ # A sample Gemfile source "https://rubygems.org" -gem "rails", '~> 5.2' +gem "rails", '~> 7.0' -gem 'sass-rails' +gem 'sassc-rails' gem 'less-rails' -gem 'uglifier', '>= 1.0.3' +gem 'uglifier' # See https://github.com/sstephenson/execjs#readme for more supported runtimes gem 'therubyracer', platforms: :ruby @@ -46,7 +46,8 @@ gem 'whenever', require: false # For defining cronjobs, see config/schedule.rb gem 'ruby-units' gem 'attribute_normalizer' gem 'ice_cube' -gem 'recurring_select' +# At time of development 01-06-2022 mmddyyyy necessary fix for config_helper.rb form builder was not in rubygems so we pull from github, see: https://github.com/gregschmit/recurring_select/pull/152 +gem 'recurring_select', git: 'https://github.com/gregschmit/recurring_select' gem 'roo' gem 'roo-xls' gem 'spreadsheet' @@ -84,7 +85,8 @@ group :development do gem 'binding_of_caller' # gem "rails-i18n-debug" # chrome debugging extension https://github.com/dejan/rails_panel - gem 'meta_request' + # TODO: disabled due to https://github.com/rails/rails/issues/40781 + # gem 'meta_request' # Get infos when not using proper eager loading gem 'bullet' diff --git a/Gemfile.lock b/Gemfile.lock index ebc7a49f..e7bf33e7 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,3 +1,14 @@ +GIT + remote: https://github.com/gregschmit/recurring_select + revision: 29febc4c4abdd6c30636c33a7d2daecb09973ecf + specs: + recurring_select (3.0.0) + coffee-rails (>= 3.1) + ice_cube (>= 0.11) + jquery-rails (>= 3.0) + rails (>= 5.2) + sass-rails (>= 4.0) + GIT remote: https://github.com/technoweenie/acts_as_versioned.git revision: 63b1fc8529d028fae632fe80ec0cb25df56cd76b @@ -59,52 +70,76 @@ PATH GEM remote: https://rubygems.org/ specs: - actioncable (5.2.8.1) - actionpack (= 5.2.8.1) + actioncable (7.0.4) + actionpack (= 7.0.4) + activesupport (= 7.0.4) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailer (5.2.8.1) - actionpack (= 5.2.8.1) - actionview (= 5.2.8.1) - activejob (= 5.2.8.1) + actionmailbox (7.0.4) + actionpack (= 7.0.4) + activejob (= 7.0.4) + activerecord (= 7.0.4) + activestorage (= 7.0.4) + activesupport (= 7.0.4) + mail (>= 2.7.1) + net-imap + net-pop + net-smtp + actionmailer (7.0.4) + actionpack (= 7.0.4) + actionview (= 7.0.4) + activejob (= 7.0.4) + activesupport (= 7.0.4) mail (~> 2.5, >= 2.5.4) + net-imap + net-pop + net-smtp rails-dom-testing (~> 2.0) - actionpack (5.2.8.1) - actionview (= 5.2.8.1) - activesupport (= 5.2.8.1) - rack (~> 2.0, >= 2.0.8) + actionpack (7.0.4) + actionview (= 7.0.4) + activesupport (= 7.0.4) + rack (~> 2.0, >= 2.2.0) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.0, >= 1.0.2) - actionview (5.2.8.1) - activesupport (= 5.2.8.1) + rails-html-sanitizer (~> 1.0, >= 1.2.0) + actiontext (7.0.4) + actionpack (= 7.0.4) + activerecord (= 7.0.4) + activestorage (= 7.0.4) + activesupport (= 7.0.4) + globalid (>= 0.6.0) + nokogiri (>= 1.8.5) + actionview (7.0.4) + activesupport (= 7.0.4) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.0, >= 1.0.3) + rails-html-sanitizer (~> 1.1, >= 1.2.0) active_model_serializers (0.10.13) actionpack (>= 4.1, < 7.1) activemodel (>= 4.1, < 7.1) case_transform (>= 0.2) jsonapi-renderer (>= 0.1.1.beta1, < 0.3) - activejob (5.2.8.1) - activesupport (= 5.2.8.1) + activejob (7.0.4) + activesupport (= 7.0.4) globalid (>= 0.3.6) - activemodel (5.2.8.1) - activesupport (= 5.2.8.1) - activerecord (5.2.8.1) - activemodel (= 5.2.8.1) - activesupport (= 5.2.8.1) - arel (>= 9.0) - activestorage (5.2.8.1) - actionpack (= 5.2.8.1) - activerecord (= 5.2.8.1) - marcel (~> 1.0.0) - activesupport (5.2.8.1) + activemodel (7.0.4) + activesupport (= 7.0.4) + activerecord (7.0.4) + activemodel (= 7.0.4) + activesupport (= 7.0.4) + activestorage (7.0.4) + actionpack (= 7.0.4) + activejob (= 7.0.4) + activerecord (= 7.0.4) + activesupport (= 7.0.4) + marcel (~> 1.0) + mini_mime (>= 1.1.0) + activesupport (7.0.4) concurrent-ruby (~> 1.0, >= 1.0.2) - i18n (>= 0.7, < 2) - minitest (~> 5.1) - tzinfo (~> 1.1) + i18n (>= 1.6, < 2) + minitest (>= 5.1) + tzinfo (~> 2.0) acts_as_tree (2.9.1) activerecord (>= 3.0.0) addressable (2.8.1) @@ -112,7 +147,6 @@ GEM apparition (0.6.0) capybara (~> 3.13, < 4) websocket-driver (>= 0.6.5) - arel (9.0.0) ast (2.4.2) attribute_normalizer (1.2.0) base32 (0.3.4) @@ -123,15 +157,15 @@ GEM bindex (0.8.1) binding_of_caller (1.0.0) debug_inspector (>= 0.0.1) - bootsnap (1.13.0) + bootsnap (1.15.0) msgpack (~> 1.2) bootstrap-datepicker-rails (1.9.0.1) railties (>= 3.0) builder (3.2.4) - bullet (7.0.3) + bullet (7.0.7) activesupport (>= 3.0.0) uniform_notifier (~> 1.11) - capybara (3.36.0) + capybara (3.38.0) addressable matrix mini_mime (>= 0.1.3) @@ -163,6 +197,7 @@ GEM activerecord (>= 5.a) database_cleaner-core (~> 2.0.0) database_cleaner-core (2.0.1) + date (3.3.3) date_time_attribute (0.1.2) activesupport (>= 3.0.0) debug_inspector (1.1.0) @@ -175,13 +210,13 @@ GEM diff-lcs (1.5.0) diffy (3.4.2) docile (1.4.0) - doorkeeper (5.6.0) + doorkeeper (5.6.2) railties (>= 5) - doorkeeper-i18n (5.2.5) + doorkeeper-i18n (5.2.6) doorkeeper (>= 5.2) email_reply_trimmer (0.1.13) - erubi (1.11.0) - eventmachine (1.2.7) + erubi (1.12.0) + eventmachine (1.0.9.1) exception_notification (4.5.0) actionmailer (>= 5.2, < 8) activesupport (>= 5.2, < 8) @@ -192,14 +227,14 @@ GEM factory_bot_rails (6.2.0) factory_bot (~> 6.2.0) railties (>= 5.0.0) - faker (2.22.0) + faker (3.1.0) i18n (>= 1.8.11, < 2) ffi (1.15.5) gaffe (1.2.0) rails (>= 4.0.0) globalid (1.0.1) activesupport (>= 5.0) - haml (6.0.5) + haml (6.1.1) temple (>= 0.8.2) thor tilt @@ -228,13 +263,11 @@ GEM interception (0.5) iso (0.4.0) i18n - jquery-rails (4.5.0) + jquery-rails (4.5.1) rails-dom-testing (>= 1, < 3) railties (>= 4.2.0) thor (>= 0.14, < 2.0) - json (2.6.2) - json-schema (2.8.1) - addressable (>= 2.4) + json (2.6.3) jsonapi-renderer (0.2.2) kaminari (1.2.2) activesupport (>= 4.1.0) @@ -254,15 +287,18 @@ GEM actionpack (>= 5.0) less (~> 2.6.0) sprockets (~> 3.0) - libv8 (3.16.14.19) + libv8 (3.16.14.19-x86_64-linux) listen (3.7.1) rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) loofah (2.19.1) crass (~> 1.0.2) nokogiri (>= 1.5.9) - mail (2.7.1) + mail (2.8.0) mini_mime (>= 0.1.1) + net-imap + net-pop + net-smtp mailcatcher (0.2.4) eventmachine haml @@ -275,16 +311,12 @@ GEM thin marcel (1.0.2) matrix (0.4.2) - meta_request (0.7.3) - rack-contrib (>= 1.1, < 3) - railties (>= 3.0.0, < 7) method_source (1.0.0) midi-smtp-server (3.0.3) mime-types (3.4.1) mime-types-data (~> 3.2015) mime-types-data (3.2022.0105) mini_mime (1.1.2) - mini_portile2 (2.8.0) minitest (5.17.0) mono_logger (1.1.1) msgpack (1.6.0) @@ -292,12 +324,20 @@ GEM mustermann (3.0.0) ruby2_keywords (~> 0.0.1) mysql2 (0.5.4) + net-imap (0.3.4) + date + net-protocol + net-pop (0.1.2) + net-protocol + net-protocol (0.2.1) + timeout + net-smtp (0.3.3) + net-protocol nio4r (2.5.8) - nokogiri (1.13.10) - mini_portile2 (~> 2.8.0) + nokogiri (1.13.10-x86_64-linux) racc (~> 1.4) parallel (1.22.1) - parser (3.1.2.1) + parser (3.2.0.0) ast (~> 2.4.1) pdf-core (0.9.0) polyglot (0.3.5) @@ -315,32 +355,31 @@ GEM pry-stack_explorer (0.6.1) binding_of_caller (~> 1.0) pry (~> 0.13) - public_suffix (5.0.0) - puma (5.6.5) + public_suffix (5.0.1) + puma (6.0.2) nio4r (~> 2.0) - racc (1.6.1) - rack (2.2.6.4) - rack-contrib (2.3.0) - rack (~> 2.0) + racc (1.6.2) + rack (2.2.5) rack-cors (1.1.1) rack (>= 2.0.0) - rack-protection (3.0.4) + rack-protection (3.0.5) rack rack-test (2.0.2) rack (>= 1.3) - rails (5.2.8.1) - actioncable (= 5.2.8.1) - actionmailer (= 5.2.8.1) - actionpack (= 5.2.8.1) - actionview (= 5.2.8.1) - activejob (= 5.2.8.1) - activemodel (= 5.2.8.1) - activerecord (= 5.2.8.1) - activestorage (= 5.2.8.1) - activesupport (= 5.2.8.1) - bundler (>= 1.3.0) - railties (= 5.2.8.1) - sprockets-rails (>= 2.0.0) + rails (7.0.4) + actioncable (= 7.0.4) + actionmailbox (= 7.0.4) + actionmailer (= 7.0.4) + actionpack (= 7.0.4) + actiontext (= 7.0.4) + actionview (= 7.0.4) + activejob (= 7.0.4) + activemodel (= 7.0.4) + activerecord (= 7.0.4) + activestorage (= 7.0.4) + activesupport (= 7.0.4) + bundler (>= 1.15.0) + railties (= 7.0.4) rails-assets-listjs (0.2.0.beta.4) railties (>= 3.1) rails-dom-testing (2.0.3) @@ -348,42 +387,37 @@ GEM nokogiri (>= 1.6) rails-html-sanitizer (1.4.4) loofah (~> 2.19, >= 2.19.1) - rails-i18n (5.1.3) + rails-i18n (7.0.6) i18n (>= 0.7, < 2) - railties (>= 5.0, < 6) + railties (>= 6.0.0, < 8) rails-settings-cached (0.4.3) rails (>= 4.2.0) rails_tokeninput (1.7.0) railties (>= 3.1.0) - railties (5.2.8.1) - actionpack (= 5.2.8.1) - activesupport (= 5.2.8.1) + railties (7.0.4) + actionpack (= 7.0.4) + activesupport (= 7.0.4) method_source - rake (>= 0.8.7) - thor (>= 0.19.0, < 2.0) + rake (>= 12.2) + thor (~> 1.0) + zeitwerk (~> 2.5) rainbow (3.1.1) rake (13.0.6) - ransack (2.5.0) - activerecord (>= 5.2.4) - activesupport (>= 5.2.4) + ransack (3.2.1) + activerecord (>= 6.1.5) + activesupport (>= 6.1.5) i18n rb-fsevent (0.11.2) rb-inotify (0.10.1) ffi (~> 1.0) - recurring_select (3.0.0) - coffee-rails (>= 3.1) - ice_cube (>= 0.11) - jquery-rails (>= 3.0) - rails (>= 5.2) - sass-rails (>= 4.0) redis (5.0.5) redis-client (>= 0.9.0) - redis-client (0.9.0) + redis-client (0.11.2) connection_pool - redis-namespace (1.9.0) + redis-namespace (1.10.0) redis (>= 4) ref (2.0.0) - regexp_parser (2.6.0) + regexp_parser (2.6.1) responders (3.0.1) actionpack (>= 5.0) railties (>= 5.0) @@ -393,33 +427,33 @@ GEM redis-namespace (~> 1.6) sinatra (>= 0.9.2) rexml (3.2.5) - roo (2.8.3) + roo (2.9.0) nokogiri (~> 1) rubyzip (>= 1.3.0, < 3.0.0) roo-xls (1.2.0) nokogiri roo (>= 2.0.0, < 3) spreadsheet (> 0.9.0) - rspec (3.11.0) - rspec-core (~> 3.11.0) - rspec-expectations (~> 3.11.0) - rspec-mocks (~> 3.11.0) - rspec-core (3.11.0) - rspec-support (~> 3.11.0) - rspec-expectations (3.11.1) + rspec (3.12.0) + rspec-core (~> 3.12.0) + rspec-expectations (~> 3.12.0) + rspec-mocks (~> 3.12.0) + rspec-core (3.12.0) + rspec-support (~> 3.12.0) + rspec-expectations (3.12.1) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.11.0) - rspec-mocks (3.11.1) + rspec-support (~> 3.12.0) + rspec-mocks (3.12.1) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.11.0) - rspec-rails (5.1.2) - actionpack (>= 5.2) - activesupport (>= 5.2) - railties (>= 5.2) - rspec-core (~> 3.10) - rspec-expectations (~> 3.10) - rspec-mocks (~> 3.10) - rspec-support (~> 3.10) + rspec-support (~> 3.12.0) + rspec-rails (6.0.1) + actionpack (>= 6.1) + activesupport (>= 6.1) + railties (>= 6.1) + rspec-core (~> 3.11) + rspec-expectations (~> 3.11) + rspec-mocks (~> 3.11) + rspec-support (~> 3.11) rspec-rerun (1.1.0) rspec (~> 3.0) rspec-support (3.11.1) @@ -440,20 +474,20 @@ GEM rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 1.8, < 3.0) rexml (>= 3.2.5, < 4.0) - rubocop-ast (>= 1.20.1, < 2.0) + rubocop-ast (>= 1.24.1, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 1.4.0, < 3.0) - rubocop-ast (1.21.0) + rubocop-ast (1.24.1) parser (>= 3.1.1.0) - rubocop-rails (2.16.1) + rubocop-rails (2.17.4) activesupport (>= 4.2.0) rack (>= 1.1) rubocop (>= 1.33.0, < 2.0) - rubocop-rspec (2.13.2) + rubocop-rspec (2.16.0) rubocop (~> 1.33) ruby-filemagic (0.7.3) ruby-ole (1.2.12.2) - ruby-prof (1.4.3) + ruby-prof (1.4.5) ruby-progressbar (1.11.0) ruby-units (3.0.0) ruby2_keywords (0.0.5) @@ -478,21 +512,21 @@ GEM simple_form (5.1.0) actionpack (>= 5.2) activemodel (>= 5.2) - simplecov (0.21.2) + simplecov (0.22.0) docile (~> 1.1) simplecov-html (~> 0.11) simplecov_json_formatter (~> 0.1) simplecov-html (0.12.3) simplecov-lcov (0.8.0) simplecov_json_formatter (0.1.4) - sinatra (3.0.4) + sinatra (3.0.5) mustermann (~> 3.0) rack (~> 2.2, >= 2.2.4) - rack-protection (= 3.0.4) + rack-protection (= 3.0.5) tilt (~> 2.0) - skinny (0.2.2) - eventmachine (~> 1.0) - thin + skinny (0.2.4) + eventmachine (~> 1.0.0) + thin (>= 1.5, < 1.7) spreadsheet (1.3.0) ruby-ole sprockets (3.7.2) @@ -506,17 +540,17 @@ GEM sqlite3-ruby (1.3.3) sqlite3 (>= 1.3.3) table_print (1.5.7) - temple (0.8.2) + temple (0.9.1) therubyracer (0.12.3) libv8 (~> 3.16.14.15) ref - thin (1.8.1) - daemons (~> 1.0, >= 1.0.9) - eventmachine (~> 1.0, >= 1.0.4) - rack (>= 1, < 3) + thin (1.6.2) + daemons (>= 1.0.9) + eventmachine (>= 1.0.0) + rack (>= 1.0.0) thor (1.2.1) - thread_safe (0.3.6) tilt (2.0.11) + timeout (0.3.1) ttfunk (1.7.0) twitter-bootstrap-rails (2.2.8) actionpack (>= 3.1) @@ -525,20 +559,20 @@ GEM railties (>= 3.1) twitter-text (1.14.7) unf (~> 0.1.0) - tzinfo (1.2.10) - thread_safe (~> 0.1) + tzinfo (2.0.5) + concurrent-ruby (~> 1.0) uglifier (4.2.0) execjs (>= 0.3.0, < 3) unf (0.1.4) unf_ext unf_ext (0.0.8.2) - unicode-display_width (2.3.0) + unicode-display_width (2.4.2) uniform_notifier (1.16.0) - web-console (3.7.0) - actionview (>= 5.0) - activemodel (>= 5.0) + web-console (4.2.0) + actionview (>= 6.0.0) + activemodel (>= 6.0.0) bindex (>= 0.4.0) - railties (>= 5.0) + railties (>= 6.0.0) websocket-driver (0.7.5) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) @@ -552,9 +586,10 @@ GEM twitter-text xpath (3.2.0) nokogiri (~> 1.8) + zeitwerk (2.6.6) PLATFORMS - ruby + x86_64-linux DEPENDENCIES active_model_serializers (~> 0.10.0) @@ -596,7 +631,6 @@ DEPENDENCIES less-rails listen mailcatcher - meta_request midi-smtp-server mime-types mysql2 @@ -606,13 +640,13 @@ DEPENDENCIES pry-stack_explorer puma rack-cors - rails (~> 5.2) + rails (~> 7.0) rails-assets-listjs (= 0.2.0.beta.4) rails-i18n rails-settings-cached (= 0.4.3) rails_tokeninput ransack - recurring_select + recurring_select! resque roo roo-xls @@ -628,7 +662,7 @@ DEPENDENCIES ruby-filemagic ruby-prof ruby-units - sass-rails + sassc-rails sd_notify select2-rails simple-navigation (~> 3.14.0) @@ -642,9 +676,9 @@ DEPENDENCIES table_print therubyracer twitter-bootstrap-rails (~> 2.2.8) - uglifier (>= 1.0.3) + uglifier web-console whenever BUNDLED WITH - 1.17.3 + 2.4.2 diff --git a/bin/setup b/bin/setup index 94fd4d79..ec47b79b 100755 --- a/bin/setup +++ b/bin/setup @@ -1,36 +1,33 @@ #!/usr/bin/env ruby -require 'fileutils' -include FileUtils +require "fileutils" # path to your application root. -APP_ROOT = File.expand_path('..', __dir__) +APP_ROOT = File.expand_path("..", __dir__) def system!(*args) system(*args) || abort("\n== Command #{args} failed ==") end -chdir APP_ROOT do - # This script is a starting point to setup your application. +FileUtils.chdir APP_ROOT do + # This script is a way to set up or update your development environment automatically. + # This script is idempotent, so that you can run it at any time and get an expectable outcome. # Add necessary setup steps to this file. - puts '== Installing dependencies ==' - system! 'gem install bundler --conservative' - system('bundle check') || system!('bundle install') - - # Install JavaScript dependencies if using Yarn - # system('bin/yarn') + puts "== Installing dependencies ==" + system! "gem install bundler --conservative" + system("bundle check") || system!("bundle install") # puts "\n== Copying sample files ==" - # unless File.exist?('config/database.yml') - # cp 'config/database.yml.sample', 'config/database.yml' + # unless File.exist?("config/database.yml") + # FileUtils.cp "config/database.yml.sample", "config/database.yml" # end puts "\n== Preparing database ==" - system! 'bin/rails db:setup' + system! "bin/rails db:prepare" puts "\n== Removing old logs and tempfiles ==" - system! 'bin/rails log:clear tmp:clear' + system! "bin/rails log:clear tmp:clear" puts "\n== Restarting application server ==" - system! 'bin/rails restart' + system! "bin/rails restart" end diff --git a/config/application.rb b/config/application.rb index 544e534c..9c0ade99 100644 --- a/config/application.rb +++ b/config/application.rb @@ -9,7 +9,7 @@ Bundler.require(*Rails.groups) module Foodsoft class Application < Rails::Application # Initialize configuration defaults for originally generated Rails version. - config.load_defaults 5.0 + config.load_defaults 7.0 # Settings in config/environments/* take precedence over those specified here. # Application configuration can go into files in config/initializers @@ -36,9 +36,6 @@ module Foodsoft # Configure the default encoding used in templates for Ruby 1.9. config.encoding = "utf-8" - # TODO: Remove this. See CVE-2022-32224 for details. - config.active_record.yaml_column_permitted_classes = [BigDecimal, Date, Symbol, Time] - # Enable escaping HTML in JSON. config.active_support.escape_html_entities_in_json = true @@ -66,6 +63,12 @@ module Foodsoft # Load legacy scripts from vendor config.assets.precompile += ['vendor/assets/javascripts/*.js'] + config.active_record.yaml_column_permitted_classes = [Symbol, BigDecimal] + + config.autoloader = :zeitwerk + + # Ex:- :default =>'' + # CORS for API config.middleware.insert_before 0, Rack::Cors do allow do diff --git a/config/environments/production.rb b/config/environments/production.rb index 0560b38d..d0f06b95 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -1,3 +1,5 @@ +require "active_support/core_ext/integer/time" + # Foodsoft production configuration. # # This file is in the public domain. @@ -34,16 +36,16 @@ Rails.application.configure do config.assets.compile = false # Enable serving of images, stylesheets, and JavaScripts from an asset server. - # config.action_controller.asset_host = 'http://assets.example.com' + # config.asset_host = "http://assets.example.com" # Specifies the header that your server uses for sending files. # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX - # Store uploaded files on the local file system (see config/storage.yml for options) + # Store uploaded files on the local file system (see config/storage.yml for options). config.active_storage.service = :local - # Mount Action Cable outside main process or domain + # Mount Action Cable outside main process or domain. # config.action_cable.mount_path = nil # config.action_cable.url = 'wss://example.com/cable' # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ] @@ -51,6 +53,8 @@ Rails.application.configure do # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. config.force_ssl = ENV["RAILS_FORCE_SSL"] != "false" + # Include generic and useful information about system operation, but avoid logging too much + # information to avoid inadvertent exposure of personally identifiable information (PII). # Set to :debug to see everything in the log. config.log_level = :info @@ -63,6 +67,10 @@ Rails.application.configure do # Use a different cache store in production. # config.cache_store = :mem_cache_store + # Use a real queuing backend for Active Job (and separate queues per environment). + # config.active_job.queue_adapter = :resque + # config.active_job.queue_name_prefix = "foodsoft_production" + config.action_mailer.perform_caching = false # Ignore bad email addresses and do not raise email delivery errors. @@ -98,7 +106,7 @@ Rails.application.configure do end # Use default logging formatter so that PID and timestamp are not suppressed. - config.log_formatter = ::Logger::Formatter.new + config.log_formatter = Logger::Formatter.new # Use a different logger for distributed setups. # require 'syslog/logger' diff --git a/config/environments/test.rb b/config/environments/test.rb index ccf3767f..6ea4d1e7 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -1,30 +1,31 @@ -# Foodsoft test configuration. -# -# This file is in the public domain. +require "active_support/core_ext/integer/time" + +# The test environment is used exclusively to run your application's +# test suite. You never need to work with it otherwise. Remember that +# your test database is "scratch space" for the test suite and is wiped +# and recreated between test runs. Don't rely on the data there! Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. - # The test environment is used exclusively to run your application's - # test suite. You never need to work with it otherwise. Remember that - # your test database is "scratch space" for the test suite and is wiped - # and recreated between test runs. Don't rely on the data there! + # Turn false under Spring and add config.action_view.cache_template_loading = true. config.cache_classes = true - # Do not eager load code on boot. This avoids loading your whole application - # just for the purpose of running a single test. If you are using a tool that - # preloads Rails for running tests, you may have to set it to true. - config.eager_load = false + # Eager loading loads your whole application. When running a single test locally, + # this probably isn't necessary. It's a good idea to do in a continuous integration + # system, or in some way before deploying your code. + config.eager_load = ENV["CI"].present? # Configure public file server for tests with Cache-Control for performance. config.public_file_server.enabled = true config.public_file_server.headers = { - 'Cache-Control' => "public, max-age=#{1.hour.to_i}" + "Cache-Control" => "public, max-age=#{1.hour.to_i}" } # Show full error reports and disable caching. config.consider_all_requests_local = true config.action_controller.perform_caching = false + config.cache_store = :null_store # Raise exceptions instead of rendering exception templates. config.action_dispatch.show_exceptions = false @@ -32,7 +33,7 @@ Rails.application.configure do # Disable request forgery protection in test environment. config.action_controller.allow_forgery_protection = false - # Store uploaded files on the local file system in a temporary directory + # Store uploaded files on the local file system in a temporary directory. config.active_storage.service = :test config.action_mailer.perform_caching = false @@ -45,6 +46,15 @@ Rails.application.configure do # Print deprecation notices to the stderr. config.active_support.deprecation = :stderr - # Raises error for missing translations - # config.action_view.raise_on_missing_translations = true + # Raise exceptions for disallowed deprecations. + config.active_support.disallowed_deprecation = :raise + + # Tell Active Support which deprecation messages to disallow. + config.active_support.disallowed_deprecation_warnings = [] + + # Raises error for missing translations. + # config.i18n.raise_on_missing_translations = true + + # Annotate rendered view with file names. + # config.action_view.annotate_rendered_view_with_filenames = true end diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb index 4b828e80..fe48fc34 100644 --- a/config/initializers/assets.rb +++ b/config/initializers/assets.rb @@ -5,8 +5,6 @@ Rails.application.config.assets.version = '1.0' # Add additional assets to the asset load path. # Rails.application.config.assets.paths << Emoji.images_path -# Add Yarn node_modules folder to the asset load path. -Rails.application.config.assets.paths << Rails.root.join('node_modules') # Precompile additional assets. # application.js, application.css, and all non-JS/CSS in the app/assets diff --git a/config/initializers/content_security_policy.rb b/config/initializers/content_security_policy.rb index d3bcaa5e..54f47cf1 100644 --- a/config/initializers/content_security_policy.rb +++ b/config/initializers/content_security_policy.rb @@ -1,25 +1,25 @@ # Be sure to restart your server when you modify this file. -# Define an application-wide content security policy -# For further information see the following documentation -# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy +# Define an application-wide content security policy. +# See the Securing Rails Applications Guide for more information: +# https://guides.rubyonrails.org/security.html#content-security-policy-header -# Rails.application.config.content_security_policy do |policy| -# policy.default_src :self, :https -# policy.font_src :self, :https, :data -# policy.img_src :self, :https, :data -# policy.object_src :none -# policy.script_src :self, :https -# policy.style_src :self, :https - -# # Specify URI for violation reports -# # policy.report_uri "/csp-violation-report-endpoint" +# Rails.application.configure do +# config.content_security_policy do |policy| +# policy.default_src :self, :https +# policy.font_src :self, :https, :data +# policy.img_src :self, :https, :data +# policy.object_src :none +# policy.script_src :self, :https +# policy.style_src :self, :https +# # Specify URI for violation reports +# # policy.report_uri "/csp-violation-report-endpoint" +# end +# +# # Generate session nonces for permitted importmap and inline scripts +# config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s } +# config.content_security_policy_nonce_directives = %w(script-src) +# +# # Report violations without enforcing the policy. +# # config.content_security_policy_report_only = true # end - -# If you are using UJS then enable automatic nonce generation -# Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) } - -# Report CSP violations to a specified URI -# For further information see the following documentation: -# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only -# Rails.application.config.content_security_policy_report_only = true diff --git a/config/initializers/cors.rb b/config/initializers/cors.rb new file mode 100644 index 00000000..e5a82f16 --- /dev/null +++ b/config/initializers/cors.rb @@ -0,0 +1,16 @@ +# Be sure to restart your server when you modify this file. + +# Avoid CORS issues when API is called from the frontend app. +# Handle Cross-Origin Resource Sharing (CORS) in order to accept cross-origin AJAX requests. + +# Read more: https://github.com/cyu/rack-cors + +# Rails.application.config.middleware.insert_before 0, Rack::Cors do +# allow do +# origins "example.com" +# +# resource "*", +# headers: :any, +# methods: [:get, :post, :put, :patch, :delete, :options, :head] +# end +# end diff --git a/config/initializers/currency_display.rb b/config/initializers/currency_display.rb index 7caa6a64..71d108d2 100644 --- a/config/initializers/currency_display.rb +++ b/config/initializers/currency_display.rb @@ -1,7 +1,7 @@ # remove all currency translations, so that we can set the default language and # have it shown in all other languages too -::I18n.available_locales.each do |locale| - unless locale == ::I18n.default_locale - ::I18n.backend.store_translations(locale, number: { currency: { format: { unit: nil } } }) +I18n.available_locales.each do |locale| + unless locale == I18n.default_locale + I18n.backend.store_translations(locale, number: { currency: { format: { unit: nil } } }) end end diff --git a/config/initializers/filter_parameter_logging.rb b/config/initializers/filter_parameter_logging.rb index 4a994e1e..adc6568c 100644 --- a/config/initializers/filter_parameter_logging.rb +++ b/config/initializers/filter_parameter_logging.rb @@ -1,4 +1,8 @@ # Be sure to restart your server when you modify this file. -# Configure sensitive parameters which will be filtered from the log file. -Rails.application.config.filter_parameters += [:password] +# Configure parameters to be filtered from the log file. Use this to limit dissemination of +# sensitive information. See the ActiveSupport::ParameterFilter documentation for supported +# notations and behaviors. +Rails.application.config.filter_parameters += [ + :passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn +] diff --git a/config/initializers/mail_receiver.rb b/config/initializers/mail_receiver.rb index 67288cc1..088d7c93 100644 --- a/config/initializers/mail_receiver.rb +++ b/config/initializers/mail_receiver.rb @@ -1 +1,3 @@ -FoodsoftMailReceiver.register BounceMailReceiver +Rails.application.config.to_prepare do + FoodsoftMailReceiver.register BounceMailReceiver +end diff --git a/config/initializers/new_framework_defaults.rb b/config/initializers/new_framework_defaults.rb deleted file mode 100644 index fac64e0a..00000000 --- a/config/initializers/new_framework_defaults.rb +++ /dev/null @@ -1,17 +0,0 @@ -# Be sure to restart your server when you modify this file. -# -# This file contains migration options to ease your Rails 5.0 upgrade. -# -# Once upgraded flip defaults one by one to migrate to the new default. -# -# Read the Guide for Upgrading Ruby on Rails for more info on each option. - -# Enable per-form CSRF tokens. Previous versions had false. -Rails.application.config.action_controller.per_form_csrf_tokens = false - -# Enable origin-checking CSRF mitigation. Previous versions had false. -Rails.application.config.action_controller.forgery_protection_origin_check = false - -# Make Ruby 2.4 preserve the timezone of the receiver when calling `to_time`. -# Previous versions had false. -ActiveSupport.to_time_preserves_timezone = false diff --git a/config/initializers/new_framework_defaults_5_1.rb b/config/initializers/new_framework_defaults_5_1.rb deleted file mode 100644 index 9010abd5..00000000 --- a/config/initializers/new_framework_defaults_5_1.rb +++ /dev/null @@ -1,14 +0,0 @@ -# Be sure to restart your server when you modify this file. -# -# This file contains migration options to ease your Rails 5.1 upgrade. -# -# Once upgraded flip defaults one by one to migrate to the new default. -# -# Read the Guide for Upgrading Ruby on Rails for more info on each option. - -# Make `form_with` generate non-remote forms. -Rails.application.config.action_view.form_with_generates_remote_forms = false - -# Unknown asset fallback will return the path passed in when the given -# asset is not present in the asset pipeline. -# Rails.application.config.assets.unknown_asset_fallback = false diff --git a/config/initializers/new_framework_defaults_5_2.rb b/config/initializers/new_framework_defaults_5_2.rb deleted file mode 100644 index 5132a0b1..00000000 --- a/config/initializers/new_framework_defaults_5_2.rb +++ /dev/null @@ -1,38 +0,0 @@ -# Be sure to restart your server when you modify this file. -# -# This file contains migration options to ease your Rails 5.2 upgrade. -# -# Once upgraded flip defaults one by one to migrate to the new default. -# -# Read the Guide for Upgrading Ruby on Rails for more info on each option. - -# Make Active Record use stable #cache_key alongside new #cache_version method. -# This is needed for recyclable cache keys. -# Rails.application.config.active_record.cache_versioning = true - -# Use AES-256-GCM authenticated encryption for encrypted cookies. -# Also, embed cookie expiry in signed or encrypted cookies for increased security. -# -# This option is not backwards compatible with earlier Rails versions. -# It's best enabled when your entire app is migrated and stable on 5.2. -# -# Existing cookies will be converted on read then written with the new scheme. -# Rails.application.config.action_dispatch.use_authenticated_cookie_encryption = true - -# Use AES-256-GCM authenticated encryption as default cipher for encrypting messages -# instead of AES-256-CBC, when use_authenticated_message_encryption is set to true. -# Rails.application.config.active_support.use_authenticated_message_encryption = true - -# Add default protection from forgery to ActionController::Base instead of in -# ApplicationController. -# Rails.application.config.action_controller.default_protect_from_forgery = true - -# Store boolean values are in sqlite3 databases as 1 and 0 instead of 't' and -# 'f' after migrating old data. -Rails.application.config.active_record.sqlite3.represent_boolean_as_integer = true - -# Use SHA-1 instead of MD5 to generate non-sensitive digests, such as the ETag header. -# Rails.application.config.active_support.use_sha1_digests = true - -# Make `form_with` generate id attributes for any generated HTML tags. -# Rails.application.config.action_view.form_with_generates_ids = true diff --git a/config/initializers/permissions_policy.rb b/config/initializers/permissions_policy.rb new file mode 100644 index 00000000..00f64d71 --- /dev/null +++ b/config/initializers/permissions_policy.rb @@ -0,0 +1,11 @@ +# Define an application-wide HTTP permissions policy. For further +# information see https://developers.google.com/web/updates/2018/06/feature-policy +# +# Rails.application.config.permissions_policy do |f| +# f.camera :none +# f.gyroscope :none +# f.microphone :none +# f.usb :none +# f.fullscreen :self +# f.payment :self, "https://secure.example.com" +# end diff --git a/config/initializers/rails6_backports.rb b/config/initializers/rails6_backports.rb deleted file mode 100644 index b72f4220..00000000 --- a/config/initializers/rails6_backports.rb +++ /dev/null @@ -1,98 +0,0 @@ -raise "Remove no-longer-needed #{__FILE__}!" if Rails::VERSION::MAJOR >= 6 - -require "weakref" - -module ActiveRecord - # Backport https://github.com/rails/rails/pull/36998 and https://github.com/rails/rails/pull/36999 - # to avoid `ThreadError: can't create Thread: Resource temporarily unavailable` issues - module ConnectionAdapters - class ConnectionPool - class Reaper - @mutex = Mutex.new - @pools = {} - @threads = {} - - class << self - def register_pool(pool, frequency) # :nodoc: - @mutex.synchronize do - unless @threads[frequency]&.alive? - @threads[frequency] = spawn_thread(frequency) - end - @pools[frequency] ||= [] - @pools[frequency] << WeakRef.new(pool) - end - end - - private - - def spawn_thread(frequency) - Thread.new(frequency) do |t| - running = true - while running - sleep t - @mutex.synchronize do - @pools[frequency].select!(&:weakref_alive?) - @pools[frequency].each do |p| - p.reap - p.flush - rescue WeakRef::RefError - end - - if @pools[frequency].empty? - @pools.delete(frequency) - @threads.delete(frequency) - running = false - end - end - end - end - end - end - - def run - return unless frequency && frequency > 0 - - self.class.register_pool(pool, frequency) - end - end - - def reap - stale_connections = synchronize do - return unless @connections - - @connections.select do |conn| - conn.in_use? && !conn.owner.alive? - end.each(&:steal!) - end - - stale_connections.each do |conn| - if conn.active? - conn.reset! - checkin conn - else - remove conn - end - end - end - - def flush(minimum_idle = @idle_timeout) - return if minimum_idle.nil? - - idle_connections = synchronize do - return unless @connections - - @connections.select do |conn| - !conn.in_use? && conn.seconds_idle >= minimum_idle - end.each do |conn| - conn.lease - - @available.delete conn - @connections.delete conn - end - end - - idle_connections.each(&:disconnect!) - end - end - end -end diff --git a/db/migrate/20230106144438_add_service_name_to_active_storage_blobs.active_storage.rb b/db/migrate/20230106144438_add_service_name_to_active_storage_blobs.active_storage.rb new file mode 100644 index 00000000..a15c6ce8 --- /dev/null +++ b/db/migrate/20230106144438_add_service_name_to_active_storage_blobs.active_storage.rb @@ -0,0 +1,22 @@ +# This migration comes from active_storage (originally 20190112182829) +class AddServiceNameToActiveStorageBlobs < ActiveRecord::Migration[6.0] + def up + return unless table_exists?(:active_storage_blobs) + + unless column_exists?(:active_storage_blobs, :service_name) + add_column :active_storage_blobs, :service_name, :string + + if configured_service = ActiveStorage::Blob.service.name + ActiveStorage::Blob.unscoped.update_all(service_name: configured_service) + end + + change_column :active_storage_blobs, :service_name, :string, null: false + end + end + + def down + return unless table_exists?(:active_storage_blobs) + + remove_column :active_storage_blobs, :service_name + end +end diff --git a/db/migrate/20230106144439_create_active_storage_variant_records.active_storage.rb b/db/migrate/20230106144439_create_active_storage_variant_records.active_storage.rb new file mode 100644 index 00000000..e1020fc9 --- /dev/null +++ b/db/migrate/20230106144439_create_active_storage_variant_records.active_storage.rb @@ -0,0 +1,28 @@ +# This migration comes from active_storage (originally 20191206030411) +class CreateActiveStorageVariantRecords < ActiveRecord::Migration[6.0] + def change + return unless table_exists?(:active_storage_blobs) + + # Use Active Record's configured type for primary key + create_table :active_storage_variant_records, id: primary_key_type, if_not_exists: true do |t| + t.belongs_to :blob, null: false, index: false, type: blobs_primary_key_type + t.string :variation_digest, null: false + + t.index [:blob_id, :variation_digest], name: "index_active_storage_variant_records_uniqueness", unique: true + t.foreign_key :active_storage_blobs, column: :blob_id + end + end + + private + + def primary_key_type + config = Rails.configuration.generators + config.options[config.orm][:primary_key_type] || :primary_key + end + + def blobs_primary_key_type + pkey_name = connection.primary_key(:active_storage_blobs) + pkey_column = connection.columns(:active_storage_blobs).find { |c| c.name == pkey_name } + pkey_column.bigint? ? :bigint : pkey_column.type + end +end diff --git a/db/migrate/20230106144440_remove_not_null_on_active_storage_blobs_checksum.active_storage.rb b/db/migrate/20230106144440_remove_not_null_on_active_storage_blobs_checksum.active_storage.rb new file mode 100644 index 00000000..93c8b85a --- /dev/null +++ b/db/migrate/20230106144440_remove_not_null_on_active_storage_blobs_checksum.active_storage.rb @@ -0,0 +1,8 @@ +# This migration comes from active_storage (originally 20211119233751) +class RemoveNotNullOnActiveStorageBlobsChecksum < ActiveRecord::Migration[6.0] + def change + return unless table_exists?(:active_storage_blobs) + + change_column_null(:active_storage_blobs, :checksum, true) + end +end diff --git a/db/schema.rb b/db/schema.rb index ce812b3f..50c24c41 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -2,54 +2,60 @@ # of editing this file, please use the migrations feature of Active Record to # incrementally modify your database, and then regenerate this schema definition. # -# Note that this schema.rb definition is the authoritative source for your -# database schema. If you need to create the application database on another -# system, you should be using db:schema:load, not running all the migrations -# from scratch. The latter is a flawed and unsustainable approach (the more migrations -# you'll amass, the slower it'll run and the greater likelihood for issues). +# This file is the source Rails uses to define your schema when running `bin/rails +# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to +# be faster and is potentially less error prone than running all of your +# migrations from scratch. Old migrations may fail to apply correctly if those +# migrations use external dependencies or application code. # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2021_02_05_090257) do - - create_table "active_storage_attachments", id: :integer, force: :cascade do |t| +ActiveRecord::Schema[7.0].define(version: 2023_01_06_144440) do + create_table "active_storage_attachments", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.string "name", null: false t.string "record_type", null: false t.bigint "record_id", null: false t.bigint "blob_id", null: false - t.datetime "created_at", null: false + t.datetime "created_at", precision: nil, null: false t.index ["blob_id"], name: "index_active_storage_attachments_on_blob_id" t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true end - create_table "active_storage_blobs", id: :integer, force: :cascade do |t| + create_table "active_storage_blobs", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.string "key", null: false t.string "filename", null: false t.string "content_type" t.text "metadata" t.bigint "byte_size", null: false - t.string "checksum", null: false - t.datetime "created_at", null: false + t.string "checksum" + t.datetime "created_at", precision: nil, null: false + t.string "service_name", null: false t.index ["key"], name: "index_active_storage_blobs_on_key", unique: true end - create_table "article_categories", id: :integer, force: :cascade do |t| + create_table "active_storage_variant_records", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| + t.integer "blob_id", null: false + t.string "variation_digest", null: false + t.index ["blob_id", "variation_digest"], name: "index_active_storage_variant_records_uniqueness", unique: true + end + + create_table "article_categories", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.string "name", default: "", null: false t.string "description" t.index ["name"], name: "index_article_categories_on_name", unique: true end - create_table "article_prices", id: :integer, force: :cascade do |t| + create_table "article_prices", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.integer "article_id", null: false t.decimal "price", precision: 8, scale: 2, default: "0.0", null: false t.decimal "tax", precision: 8, scale: 2, default: "0.0", null: false t.decimal "deposit", precision: 8, scale: 2, default: "0.0", null: false t.integer "unit_quantity" - t.datetime "created_at" + t.datetime "created_at", precision: nil t.index ["article_id"], name: "index_article_prices_on_article_id" end - create_table "articles", id: :integer, force: :cascade do |t| + create_table "articles", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.string "name", default: "", null: false t.integer "supplier_id", default: 0, null: false t.integer "article_category_id", default: 0, null: false @@ -58,15 +64,15 @@ ActiveRecord::Schema.define(version: 2021_02_05_090257) do t.boolean "availability", default: true, null: false t.string "manufacturer" t.string "origin" - t.datetime "shared_updated_on" + t.datetime "shared_updated_on", precision: nil t.decimal "price", precision: 8, scale: 2 t.float "tax" t.decimal "deposit", precision: 8, scale: 2, default: "0.0" t.integer "unit_quantity", default: 1, null: false t.string "order_number" - t.datetime "created_at" - t.datetime "updated_at" - t.datetime "deleted_at" + t.datetime "created_at", precision: nil + t.datetime "updated_at", precision: nil + t.datetime "deleted_at", precision: nil t.string "type" t.integer "quantity", default: 0 t.index ["article_category_id"], name: "index_articles_on_article_category_id" @@ -75,31 +81,31 @@ ActiveRecord::Schema.define(version: 2021_02_05_090257) do t.index ["type"], name: "index_articles_on_type" end - create_table "assignments", id: :integer, force: :cascade do |t| + create_table "assignments", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.integer "user_id", default: 0, null: false t.integer "task_id", default: 0, null: false t.boolean "accepted", default: false t.index ["user_id", "task_id"], name: "index_assignments_on_user_id_and_task_id", unique: true end - create_table "bank_accounts", id: :integer, force: :cascade do |t| + create_table "bank_accounts", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.string "name", null: false t.string "iban" t.string "description" t.decimal "balance", precision: 12, scale: 2, default: "0.0", null: false - t.datetime "last_import" + t.datetime "last_import", precision: nil t.string "import_continuation_point" t.integer "bank_gateway_id" end - create_table "bank_gateways", id: :integer, force: :cascade do |t| + create_table "bank_gateways", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.string "name", null: false t.string "url", null: false t.string "authorization" t.integer "unattended_user_id" end - create_table "bank_transactions", id: :integer, force: :cascade do |t| + create_table "bank_transactions", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.integer "bank_account_id", null: false t.string "external_id" t.date "date" @@ -108,32 +114,32 @@ ActiveRecord::Schema.define(version: 2021_02_05_090257) do t.string "reference" t.text "text" t.text "receipt" - t.binary "image", limit: 16777215 + t.binary "image", size: :medium t.integer "financial_link_id" t.index ["financial_link_id"], name: "index_bank_transactions_on_financial_link_id" end - create_table "documents", id: :integer, force: :cascade do |t| + create_table "documents", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.string "name" t.string "mime" - t.binary "data", limit: 4294967295 + t.binary "data", size: :long t.integer "created_by_user_id" - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", precision: nil + t.datetime "updated_at", precision: nil t.integer "parent_id" t.index ["parent_id"], name: "index_documents_on_parent_id" end - create_table "financial_links", id: :integer, force: :cascade do |t| + create_table "financial_links", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.text "note" end - create_table "financial_transaction_classes", id: :integer, force: :cascade do |t| + create_table "financial_transaction_classes", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.string "name", null: false t.boolean "ignore_for_account_balance", default: false, null: false end - create_table "financial_transaction_types", id: :integer, force: :cascade do |t| + create_table "financial_transaction_types", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.string "name", null: false t.integer "financial_transaction_class_id", null: false t.string "name_short" @@ -141,12 +147,12 @@ ActiveRecord::Schema.define(version: 2021_02_05_090257) do t.index ["name_short"], name: "index_financial_transaction_types_on_name_short" end - create_table "financial_transactions", id: :integer, force: :cascade do |t| + create_table "financial_transactions", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.integer "ordergroup_id" t.decimal "amount", precision: 8, scale: 2, default: "0.0", null: false t.text "note", null: false t.integer "user_id", default: 0, null: false - t.datetime "created_on", null: false + t.datetime "created_on", precision: nil, null: false t.integer "financial_transaction_type_id", null: false t.integer "financial_link_id" t.integer "reverts_id" @@ -155,20 +161,20 @@ ActiveRecord::Schema.define(version: 2021_02_05_090257) do t.index ["reverts_id"], name: "index_financial_transactions_on_reverts_id", unique: true end - create_table "group_order_article_quantities", id: :integer, force: :cascade do |t| + create_table "group_order_article_quantities", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.integer "group_order_article_id", default: 0, null: false t.integer "quantity", default: 0 t.integer "tolerance", default: 0 - t.datetime "created_on", null: false + t.datetime "created_on", precision: nil, null: false t.index ["group_order_article_id"], name: "index_group_order_article_quantities_on_group_order_article_id" end - create_table "group_order_articles", id: :integer, force: :cascade do |t| + create_table "group_order_articles", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.integer "group_order_id", default: 0, null: false t.integer "order_article_id", default: 0, null: false t.integer "quantity", default: 0, null: false t.integer "tolerance", default: 0, null: false - t.datetime "updated_on", null: false + t.datetime "updated_on", precision: nil, null: false t.decimal "result", precision: 8, scale: 3 t.decimal "result_computed", precision: 8, scale: 3 t.index ["group_order_id", "order_article_id"], name: "goa_index", unique: true @@ -176,12 +182,12 @@ ActiveRecord::Schema.define(version: 2021_02_05_090257) do t.index ["order_article_id"], name: "index_group_order_articles_on_order_article_id" end - create_table "group_orders", id: :integer, force: :cascade do |t| + create_table "group_orders", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.integer "ordergroup_id" t.integer "order_id", default: 0, null: false t.decimal "price", precision: 8, scale: 2, default: "0.0", null: false t.integer "lock_version", default: 0, null: false - t.datetime "updated_on", null: false + t.datetime "updated_on", precision: nil, null: false t.integer "updated_by_user_id" t.decimal "transport", precision: 8, scale: 2 t.index ["order_id"], name: "index_group_orders_on_order_id" @@ -189,18 +195,18 @@ ActiveRecord::Schema.define(version: 2021_02_05_090257) do t.index ["ordergroup_id"], name: "index_group_orders_on_ordergroup_id" end - create_table "groups", id: :integer, force: :cascade do |t| + create_table "groups", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.string "type", default: "", null: false t.string "name", default: "", null: false t.string "description" t.decimal "account_balance", precision: 12, scale: 2, default: "0.0", null: false - t.datetime "created_on", null: false + t.datetime "created_on", precision: nil, null: false t.boolean "role_admin", default: false, null: false t.boolean "role_suppliers", default: false, null: false t.boolean "role_article_meta", default: false, null: false t.boolean "role_finance", default: false, null: false t.boolean "role_orders", default: false, null: false - t.datetime "deleted_at" + t.datetime "deleted_at", precision: nil t.string "contact_person" t.string "contact_phone" t.string "contact_address" @@ -214,16 +220,16 @@ ActiveRecord::Schema.define(version: 2021_02_05_090257) do t.index ["name"], name: "index_groups_on_name", unique: true end - create_table "invites", id: :integer, force: :cascade do |t| + create_table "invites", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.string "token", default: "", null: false - t.datetime "expires_at", null: false + t.datetime "expires_at", precision: nil, null: false t.integer "group_id", default: 0, null: false t.integer "user_id", default: 0, null: false t.string "email", default: "", null: false t.index ["token"], name: "index_invites_on_token" end - create_table "invoices", id: :integer, force: :cascade do |t| + create_table "invoices", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.integer "supplier_id" t.string "number" t.date "date" @@ -232,16 +238,16 @@ ActiveRecord::Schema.define(version: 2021_02_05_090257) do t.decimal "amount", precision: 8, scale: 2, default: "0.0", null: false t.decimal "deposit", precision: 8, scale: 2, default: "0.0", null: false t.decimal "deposit_credit", precision: 8, scale: 2, default: "0.0", null: false - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", precision: nil + t.datetime "updated_at", precision: nil t.integer "created_by_user_id" t.string "attachment_mime" - t.binary "attachment_data", limit: 16777215 + t.binary "attachment_data", size: :medium t.integer "financial_link_id" t.index ["supplier_id"], name: "index_invoices_on_supplier_id" end - create_table "links", id: :integer, force: :cascade do |t| + create_table "links", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.string "name", null: false t.string "url", null: false t.integer "workgroup_id" @@ -249,81 +255,81 @@ ActiveRecord::Schema.define(version: 2021_02_05_090257) do t.string "authorization" end - create_table "mail_delivery_status", id: :integer, force: :cascade do |t| - t.datetime "created_at" + create_table "mail_delivery_status", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| + t.datetime "created_at", precision: nil t.string "email", null: false t.string "message", null: false t.string "attachment_mime" - t.binary "attachment_data", limit: 4294967295 + t.binary "attachment_data", size: :long t.index ["email"], name: "index_mail_delivery_status_on_email" end - create_table "memberships", id: :integer, force: :cascade do |t| + create_table "memberships", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.integer "group_id", default: 0, null: false t.integer "user_id", default: 0, null: false t.index ["user_id", "group_id"], name: "index_memberships_on_user_id_and_group_id", unique: true end - create_table "message_recipients", id: :integer, force: :cascade do |t| + create_table "message_recipients", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.integer "message_id", null: false t.integer "user_id", null: false t.integer "email_state", default: 0, null: false - t.datetime "read_at" + t.datetime "read_at", precision: nil t.index ["message_id"], name: "index_message_recipients_on_message_id" t.index ["user_id", "read_at"], name: "index_message_recipients_on_user_id_and_read_at" end - create_table "messages", id: :integer, force: :cascade do |t| + create_table "messages", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.integer "sender_id" t.string "subject", null: false t.text "body" t.boolean "private", default: false - t.datetime "created_at" + t.datetime "created_at", precision: nil t.integer "reply_to" t.integer "group_id" t.string "salt" - t.binary "received_email", limit: 16777215 + t.binary "received_email", size: :medium end - create_table "oauth_access_grants", id: :integer, force: :cascade do |t| + create_table "oauth_access_grants", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.integer "resource_owner_id", null: false t.integer "application_id", null: false t.string "token", null: false t.integer "expires_in", null: false t.text "redirect_uri", null: false - t.datetime "created_at", null: false - t.datetime "revoked_at" + t.datetime "created_at", precision: nil, null: false + t.datetime "revoked_at", precision: nil t.string "scopes" t.index ["token"], name: "index_oauth_access_grants_on_token", unique: true end - create_table "oauth_access_tokens", id: :integer, force: :cascade do |t| + create_table "oauth_access_tokens", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.integer "resource_owner_id" t.integer "application_id" t.string "token", null: false t.string "refresh_token" t.integer "expires_in" - t.datetime "revoked_at" - t.datetime "created_at", null: false + t.datetime "revoked_at", precision: nil + t.datetime "created_at", precision: nil, null: false t.string "scopes" t.index ["refresh_token"], name: "index_oauth_access_tokens_on_refresh_token", unique: true t.index ["resource_owner_id"], name: "index_oauth_access_tokens_on_resource_owner_id" t.index ["token"], name: "index_oauth_access_tokens_on_token", unique: true end - create_table "oauth_applications", id: :integer, force: :cascade do |t| + create_table "oauth_applications", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.string "name", null: false t.string "uid", null: false t.string "secret", null: false t.text "redirect_uri", null: false t.string "scopes", default: "", null: false - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", precision: nil + t.datetime "updated_at", precision: nil t.boolean "confidential", default: true, null: false t.index ["uid"], name: "index_oauth_applications_on_uid", unique: true end - create_table "order_articles", id: :integer, force: :cascade do |t| + create_table "order_articles", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.integer "order_id", default: 0, null: false t.integer "article_id", default: 0, null: false t.integer "quantity", default: 0, null: false @@ -337,45 +343,45 @@ ActiveRecord::Schema.define(version: 2021_02_05_090257) do t.index ["order_id"], name: "index_order_articles_on_order_id" end - create_table "order_comments", id: :integer, force: :cascade do |t| + create_table "order_comments", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.integer "order_id" t.integer "user_id" t.text "text" - t.datetime "created_at" + t.datetime "created_at", precision: nil t.index ["order_id"], name: "index_order_comments_on_order_id" end - create_table "orders", id: :integer, force: :cascade do |t| + create_table "orders", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.integer "supplier_id" t.text "note" - t.datetime "starts" - t.datetime "ends" + t.datetime "starts", precision: nil + t.datetime "ends", precision: nil t.string "state", default: "open" t.integer "lock_version", default: 0, null: false t.integer "updated_by_user_id" t.decimal "foodcoop_result", precision: 8, scale: 2 t.integer "created_by_user_id" - t.datetime "boxfill" + t.datetime "boxfill", precision: nil t.integer "invoice_id" t.date "pickup" - t.datetime "last_sent_mail" + t.datetime "last_sent_mail", precision: nil t.integer "end_action", default: 0, null: false t.decimal "transport", precision: 8, scale: 2 t.index ["state"], name: "index_orders_on_state" end - create_table "page_versions", id: :integer, force: :cascade do |t| + create_table "page_versions", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.integer "page_id" t.integer "lock_version" t.text "body" t.integer "updated_by" t.integer "redirect" t.integer "parent_id" - t.datetime "updated_at" + t.datetime "updated_at", precision: nil t.index ["page_id"], name: "index_page_versions_on_page_id" end - create_table "pages", id: :integer, force: :cascade do |t| + create_table "pages", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.string "title" t.text "body" t.string "permalink" @@ -383,41 +389,41 @@ ActiveRecord::Schema.define(version: 2021_02_05_090257) do t.integer "updated_by" t.integer "redirect" t.integer "parent_id" - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", precision: nil + t.datetime "updated_at", precision: nil t.index ["permalink"], name: "index_pages_on_permalink" t.index ["title"], name: "index_pages_on_title" end - create_table "periodic_task_groups", id: :integer, force: :cascade do |t| + create_table "periodic_task_groups", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.date "next_task_date" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at", precision: nil, null: false + t.datetime "updated_at", precision: nil, null: false end - create_table "poll_choices", id: :integer, force: :cascade do |t| + create_table "poll_choices", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.integer "poll_vote_id", null: false t.integer "choice", null: false t.integer "value", null: false t.index ["poll_vote_id", "choice"], name: "index_poll_choices_on_poll_vote_id_and_choice", unique: true end - create_table "poll_votes", id: :integer, force: :cascade do |t| + create_table "poll_votes", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.integer "poll_id", null: false t.integer "user_id", null: false t.integer "ordergroup_id" t.text "note" - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", precision: nil + t.datetime "updated_at", precision: nil t.index ["poll_id", "user_id", "ordergroup_id"], name: "index_poll_votes_on_poll_id_and_user_id_and_ordergroup_id", unique: true end - create_table "polls", id: :integer, force: :cascade do |t| + create_table "polls", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.integer "created_by_user_id", null: false t.string "name", null: false t.text "description" - t.datetime "starts" - t.datetime "ends" + t.datetime "starts", precision: nil + t.datetime "ends", precision: nil t.boolean "one_vote_per_ordergroup", default: false, null: false t.text "required_ordergroup_custom_fields" t.text "required_user_custom_fields" @@ -427,66 +433,66 @@ ActiveRecord::Schema.define(version: 2021_02_05_090257) do t.integer "multi_select_count", default: 0, null: false t.integer "min_points" t.integer "max_points" - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", precision: nil + t.datetime "updated_at", precision: nil t.index ["final_choice"], name: "index_polls_on_final_choice" end - create_table "printer_job_updates", id: :integer, force: :cascade do |t| + create_table "printer_job_updates", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.integer "printer_job_id", null: false - t.datetime "created_at", null: false + t.datetime "created_at", precision: nil, null: false t.string "state", null: false t.text "message" t.index ["printer_job_id", "created_at"], name: "index_printer_job_updates_on_printer_job_id_and_created_at" end - create_table "printer_jobs", id: :integer, force: :cascade do |t| + create_table "printer_jobs", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.integer "order_id" t.string "document", null: false t.integer "created_by_user_id", null: false t.integer "finished_by_user_id" - t.datetime "finished_at" + t.datetime "finished_at", precision: nil t.index ["finished_at"], name: "index_printer_jobs_on_finished_at" end - create_table "settings", id: :integer, force: :cascade do |t| + create_table "settings", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.string "var", null: false t.text "value" t.integer "thing_id" t.string "thing_type", limit: 30 - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at", precision: nil, null: false + t.datetime "updated_at", precision: nil, null: false t.index ["thing_type", "thing_id", "var"], name: "index_settings_on_thing_type_and_thing_id_and_var", unique: true end - create_table "stock_changes", id: :integer, force: :cascade do |t| + create_table "stock_changes", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.integer "stock_event_id" t.integer "order_id" t.integer "stock_article_id" t.integer "quantity", default: 0 - t.datetime "created_at" + t.datetime "created_at", precision: nil t.index ["stock_article_id"], name: "index_stock_changes_on_stock_article_id" t.index ["stock_event_id"], name: "index_stock_changes_on_stock_event_id" end - create_table "stock_events", id: :integer, force: :cascade do |t| + create_table "stock_events", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.integer "supplier_id" t.date "date" - t.datetime "created_at" + t.datetime "created_at", precision: nil t.text "note" t.integer "invoice_id" t.string "type", null: false t.index ["supplier_id"], name: "index_stock_events_on_supplier_id" end - create_table "supplier_categories", id: :integer, force: :cascade do |t| + create_table "supplier_categories", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.string "name", null: false t.string "description" t.integer "financial_transaction_class_id" t.integer "bank_account_id" end - create_table "suppliers", id: :integer, force: :cascade do |t| + create_table "suppliers", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.string "name", default: "", null: false t.string "address", default: "", null: false t.string "phone", default: "", null: false @@ -501,21 +507,21 @@ ActiveRecord::Schema.define(version: 2021_02_05_090257) do t.string "note" t.integer "shared_supplier_id" t.string "min_order_quantity" - t.datetime "deleted_at" + t.datetime "deleted_at", precision: nil t.string "shared_sync_method" t.string "iban" t.integer "supplier_category_id" t.index ["name"], name: "index_suppliers_on_name", unique: true end - create_table "tasks", id: :integer, force: :cascade do |t| + create_table "tasks", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.string "name", default: "", null: false t.text "description" t.date "due_date" t.boolean "done", default: false t.integer "workgroup_id" - t.datetime "created_on", null: false - t.datetime "updated_on", null: false + t.datetime "created_on", precision: nil, null: false + t.datetime "updated_on", precision: nil, null: false t.integer "required_users", default: 1 t.integer "duration", default: 1 t.integer "periodic_task_group_id" @@ -525,7 +531,7 @@ ActiveRecord::Schema.define(version: 2021_02_05_090257) do t.index ["workgroup_id"], name: "index_tasks_on_workgroup_id" end - create_table "users", id: :integer, force: :cascade do |t| + create_table "users", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.string "nick" t.string "password_hash", default: "", null: false t.string "password_salt", default: "", null: false @@ -533,15 +539,16 @@ ActiveRecord::Schema.define(version: 2021_02_05_090257) do t.string "last_name", default: "", null: false t.string "email", default: "", null: false t.string "phone" - t.datetime "created_on", null: false + t.datetime "created_on", precision: nil, null: false t.string "reset_password_token" - t.datetime "reset_password_expires" - t.datetime "last_login" - t.datetime "last_activity" - t.datetime "deleted_at" + t.datetime "reset_password_expires", precision: nil + t.datetime "last_login", precision: nil + t.datetime "last_activity", precision: nil + t.datetime "deleted_at", precision: nil t.string "iban" t.index ["email"], name: "index_users_on_email", unique: true t.index ["nick"], name: "index_users_on_nick", unique: true end + add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id" end diff --git a/db/seeds/seed_helper.rb b/db/seeds/seed_helper.rb index 574be356..a1f958bf 100644 --- a/db/seeds/seed_helper.rb +++ b/db/seeds/seed_helper.rb @@ -8,10 +8,10 @@ def seed_group_orders # order 3..12 times a random article go = og.group_orders.create!(order: order, updated_by_user_id: 1) - (3 + rand(10)).times do + (rand(10) + 3).times do goa = go.group_order_articles.find_or_create_by!(order_article: order.order_articles.offset(rand(noas)).first) unit_quantity = goa.order_article.price.unit_quantity - goa.update_quantities rand([4, 2 * unit_quantity + 2].max), rand(unit_quantity) + goa.update_quantities rand([4, unit_quantity * 2 + 2].max), rand(unit_quantity) end end # update totals From 4ff44aed4ce1797043df3a52da7e244bfe00f46d Mon Sep 17 00:00:00 2001 From: viehlieb Date: Fri, 6 Jan 2023 16:27:41 +0100 Subject: [PATCH 046/105] mv lib to app/lib due to upgrade --- {lib => app/lib}/api/errors.rb | 0 {lib => app/lib}/apple_bar.rb | 16 +++++----- {lib => app/lib}/articles_csv.rb | 4 +-- {lib => app/lib}/bank_account_connector.rb | 30 ++++--------------- .../lib}/bank_account_connector_external.rb | 0 .../lib}/bank_account_information_importer.rb | 0 .../lib}/bank_transaction_reference.rb | 4 +-- {lib => app/lib}/bank_transactions_csv.rb | 0 .../lib}/date_time_attribute_validate.rb | 24 ++++++++++++--- .../lib}/financial_transactions_csv.rb | 0 .../lib}/foodsoft/expansion_variables.rb | 4 +-- {lib => app/lib}/foodsoft_config.rb | 5 ++-- {lib => app/lib}/foodsoft_date_util.rb | 11 +++++-- {lib => app/lib}/foodsoft_file.rb | 0 {lib => app/lib}/foodsoft_mail_receiver.rb | 22 +++++++------- {lib => app/lib}/invoices_csv.rb | 2 +- {lib => app/lib}/order_csv.rb | 0 {lib => app/lib}/order_pdf.rb | 8 ++--- {lib => app/lib}/order_txt.rb | 6 ++-- {lib => app/lib}/ordergroups_csv.rb | 6 ++-- {lib => app/lib}/render_csv.rb | 0 {lib => app/lib}/render_pdf.rb | 7 +++-- {lib => app/lib}/spreadsheet_file.rb | 0 .../templates/haml/scaffold/_form.html.haml | 0 {lib => app/lib}/token_verifier.rb | 4 --- {lib => app/lib}/users_csv.rb | 0 26 files changed, 75 insertions(+), 78 deletions(-) rename {lib => app/lib}/api/errors.rb (100%) rename {lib => app/lib}/apple_bar.rb (75%) rename {lib => app/lib}/articles_csv.rb (90%) rename {lib => app/lib}/bank_account_connector.rb (90%) rename {lib => app/lib}/bank_account_connector_external.rb (100%) rename {lib => app/lib}/bank_account_information_importer.rb (100%) rename {lib => app/lib}/bank_transaction_reference.rb (85%) rename {lib => app/lib}/bank_transactions_csv.rb (100%) rename {lib => app/lib}/date_time_attribute_validate.rb (82%) rename {lib => app/lib}/financial_transactions_csv.rb (100%) rename {lib => app/lib}/foodsoft/expansion_variables.rb (96%) rename {lib => app/lib}/foodsoft_config.rb (99%) rename {lib => app/lib}/foodsoft_date_util.rb (84%) rename {lib => app/lib}/foodsoft_file.rb (100%) rename {lib => app/lib}/foodsoft_mail_receiver.rb (73%) rename {lib => app/lib}/invoices_csv.rb (98%) rename {lib => app/lib}/order_csv.rb (100%) rename {lib => app/lib}/order_pdf.rb (96%) rename {lib => app/lib}/order_txt.rb (81%) rename {lib => app/lib}/ordergroups_csv.rb (88%) rename {lib => app/lib}/render_csv.rb (100%) rename {lib => app/lib}/render_pdf.rb (98%) rename {lib => app/lib}/spreadsheet_file.rb (100%) rename {lib => app/lib}/templates/haml/scaffold/_form.html.haml (100%) rename {lib => app/lib}/token_verifier.rb (97%) rename {lib => app/lib}/users_csv.rb (100%) diff --git a/lib/api/errors.rb b/app/lib/api/errors.rb similarity index 100% rename from lib/api/errors.rb rename to app/lib/api/errors.rb diff --git a/lib/apple_bar.rb b/app/lib/apple_bar.rb similarity index 75% rename from lib/apple_bar.rb rename to app/lib/apple_bar.rb index a2176ea3..236417c6 100644 --- a/lib/apple_bar.rb +++ b/app/lib/apple_bar.rb @@ -14,23 +14,23 @@ class AppleBar def group_bar_state if apples >= 100 'success' + elsif FoodsoftConfig[:stop_ordering_under].present? && + (apples >= FoodsoftConfig[:stop_ordering_under]) + 'warning' else - if FoodsoftConfig[:stop_ordering_under].present? and - apples >= FoodsoftConfig[:stop_ordering_under] - 'warning' - else - 'danger' - end + 'danger' end end # Use apples as percentage, but show at least 10 percent def group_bar_width - @ordergroup.apples < 2 ? 2 : @ordergroup.apples + [@ordergroup.apples, 2].max end def mean_order_amount_per_job - (1 / @global_avg).round rescue 0 + (1 / @global_avg).round + rescue + 0 end def apples diff --git a/lib/articles_csv.rb b/app/lib/articles_csv.rb similarity index 90% rename from lib/articles_csv.rb rename to app/lib/articles_csv.rb index 910de9be..9e6b4f40 100644 --- a/lib/articles_csv.rb +++ b/app/lib/articles_csv.rb @@ -16,7 +16,7 @@ class ArticlesCsv < RenderCSV Article.human_attribute_name(:unit_quantity), '', '', - Article.human_attribute_name(:article_category), + Article.human_attribute_name(:article_category) ] end @@ -36,7 +36,7 @@ class ArticlesCsv < RenderCSV o.unit_quantity, '', '', - o.article_category.try(:name), + o.article_category.try(:name) ] end end diff --git a/lib/bank_account_connector.rb b/app/lib/bank_account_connector.rb similarity index 90% rename from lib/bank_account_connector.rb rename to app/lib/bank_account_connector.rb index 93e7cc7c..b728ebb9 100644 --- a/lib/bank_account_connector.rb +++ b/app/lib/bank_account_connector.rb @@ -8,9 +8,7 @@ class BankAccountConnector nil end - def text - @text - end + attr_reader :text end class TextField @@ -24,13 +22,7 @@ class BankAccountConnector nil end - def name - @name - end - - def value - @value - end + attr_reader :name, :value def label @label || @name.to_s @@ -73,17 +65,7 @@ class BankAccountConnector @bank_account.iban end - def auto_submit - @auto_submit - end - - def controls - @controls - end - - def count - @count - end + attr_reader :auto_submit, :controls, :count def text(data) @controls += [TextItem.new(data)] @@ -142,11 +124,9 @@ class BankAccountConnector @bank_account.save! end - def load(data) - end + def load(data); end - def dump - end + def dump; end def t(key, args = {}) return t(".fields.#{key}") unless key.is_a? String diff --git a/lib/bank_account_connector_external.rb b/app/lib/bank_account_connector_external.rb similarity index 100% rename from lib/bank_account_connector_external.rb rename to app/lib/bank_account_connector_external.rb diff --git a/lib/bank_account_information_importer.rb b/app/lib/bank_account_information_importer.rb similarity index 100% rename from lib/bank_account_information_importer.rb rename to app/lib/bank_account_information_importer.rb diff --git a/lib/bank_transaction_reference.rb b/app/lib/bank_transaction_reference.rb similarity index 85% rename from lib/bank_transaction_reference.rb rename to app/lib/bank_transaction_reference.rb index d033c544..22b9f181 100644 --- a/lib/bank_transaction_reference.rb +++ b/app/lib/bank_transaction_reference.rb @@ -1,7 +1,7 @@ class BankTransactionReference # parses a string from a bank transaction field def self.parse(data) - m = /(^|[^\w\.])FS(?\d+)(\.(?\d+))?(?([A-Za-z]+\d+(\.\d+)?)+)([^\w\.]|$)/.match(data) + m = /(^|[^\w.])FS(?\d+)(\.(?\d+))?(?([A-Za-z]+\d+(\.\d+)?)+)([^\w.]|$)/.match(data) return unless m parts = {} @@ -13,7 +13,7 @@ class BankTransactionReference ret = { group: m[:group].to_i, parts: parts } ret[:user] = m[:user].to_i if m[:user] - return ret + ret end def self.js_code_for_user(user) diff --git a/lib/bank_transactions_csv.rb b/app/lib/bank_transactions_csv.rb similarity index 100% rename from lib/bank_transactions_csv.rb rename to app/lib/bank_transactions_csv.rb diff --git a/lib/date_time_attribute_validate.rb b/app/lib/date_time_attribute_validate.rb similarity index 82% rename from lib/date_time_attribute_validate.rb rename to app/lib/date_time_attribute_validate.rb index 08138d02..23127898 100644 --- a/lib/date_time_attribute_validate.rb +++ b/app/lib/date_time_attribute_validate.rb @@ -27,12 +27,20 @@ module DateTimeAttributeValidate define_method("#{attribute}_date_value=") do |val| self.instance_variable_set("@#{attribute}_is_set", true) self.instance_variable_set("@#{attribute}_date_value", val) - self.send("#{attribute}_date=", val) rescue nil + begin + self.send("#{attribute}_date=", val) + rescue + nil + end end define_method("#{attribute}_time_value=") do |val| self.instance_variable_set("@#{attribute}_is_set", true) self.instance_variable_set("@#{attribute}_time_value", val) - self.send("#{attribute}_time=", val) rescue nil + begin + self.send("#{attribute}_time=", val) + rescue + nil + end end # fallback to field when values are not set @@ -48,11 +56,19 @@ module DateTimeAttributeValidate # validate date and time define_method("#{attribute}_datetime_value_valid") do date = self.instance_variable_get("@#{attribute}_date_value") - unless date.blank? || (Date.parse(date) rescue nil) + unless date.blank? || begin + Date.parse(date) + rescue + nil + end errors.add(attribute, "is not a valid date") # @todo I18n end time = self.instance_variable_get("@#{attribute}_time_value") - unless time.blank? || (Time.parse(time) rescue nil) + unless time.blank? || begin + Time.parse(time) + rescue + nil + end errors.add(attribute, "is not a valid time") # @todo I18n end end diff --git a/lib/financial_transactions_csv.rb b/app/lib/financial_transactions_csv.rb similarity index 100% rename from lib/financial_transactions_csv.rb rename to app/lib/financial_transactions_csv.rb diff --git a/lib/foodsoft/expansion_variables.rb b/app/lib/foodsoft/expansion_variables.rb similarity index 96% rename from lib/foodsoft/expansion_variables.rb rename to app/lib/foodsoft/expansion_variables.rb index bcf67e7a..97f7b6bb 100644 --- a/lib/foodsoft/expansion_variables.rb +++ b/app/lib/foodsoft/expansion_variables.rb @@ -54,8 +54,8 @@ module Foodsoft # @param options [Hash] Extra variables to expand # @return [String] Expanded string def self.expand(str, options = {}) - str.gsub /{{([._a-zA-Z0-9]+)}}/ do - options[$1] || self.get($1) + str.gsub(/{{([._a-zA-Z0-9]+)}}/) do + options[::Regexp.last_match(1)] || self.get(::Regexp.last_match(1)) end end diff --git a/lib/foodsoft_config.rb b/app/lib/foodsoft_config.rb similarity index 99% rename from lib/foodsoft_config.rb rename to app/lib/foodsoft_config.rb index 5a370459..2893a3e3 100644 --- a/lib/foodsoft_config.rb +++ b/app/lib/foodsoft_config.rb @@ -44,6 +44,8 @@ class FoodsoftConfig # @return [ActiveSupport::HashWithIndifferentAccess] Current configuration from configuration file. mattr_accessor :config + mattr_accessor :default_config + # Configuration file location. # Taken from environment variable +FOODSOFT_APP_CONFIG+, # or else +config/app_config.yml+. @@ -189,7 +191,7 @@ class FoodsoftConfig # @return [Hash] Full configuration. def to_hash - keys.to_h { |k| [k, self[k]] } + keys.index_with { |k| self[k] } end # for using active_model_serializer in the api/v1/configs controller @@ -216,7 +218,6 @@ class FoodsoftConfig # end # # @return [Hash] Default configuration values - mattr_accessor :default_config private diff --git a/lib/foodsoft_date_util.rb b/app/lib/foodsoft_date_util.rb similarity index 84% rename from lib/foodsoft_date_util.rb rename to app/lib/foodsoft_date_util.rb index 98dc1c61..a14ad453 100644 --- a/lib/foodsoft_date_util.rb +++ b/app/lib/foodsoft_date_util.rb @@ -6,7 +6,11 @@ module FoodsoftDateUtil schedule = IceCube::Schedule.new(start) schedule.add_recurrence_rule rule_from(options[:recurr]) # @todo handle ical parse errors - occ = (schedule.next_occurrence(from).to_time rescue nil) + occ = begin + schedule.next_occurrence(from).to_time + rescue + nil + end end if options && options[:time] && occ occ = occ.beginning_of_day.advance(seconds: Time.parse(options[:time]).seconds_since_midnight) @@ -17,9 +21,10 @@ module FoodsoftDateUtil # @param p [String, Symbol, Hash, IceCube::Rule] What to return a rule from. # @return [IceCube::Rule] Recurring rule def self.rule_from(p) - if p.is_a? String + case p + when String IceCube::Rule.from_ical(p) - elsif p.is_a? Hash + when Hash IceCube::Rule.from_hash(p) else p diff --git a/lib/foodsoft_file.rb b/app/lib/foodsoft_file.rb similarity index 100% rename from lib/foodsoft_file.rb rename to app/lib/foodsoft_file.rb diff --git a/lib/foodsoft_mail_receiver.rb b/app/lib/foodsoft_mail_receiver.rb similarity index 73% rename from lib/foodsoft_mail_receiver.rb rename to app/lib/foodsoft_mail_receiver.rb index 560e7edd..18e93be3 100644 --- a/lib/foodsoft_mail_receiver.rb +++ b/app/lib/foodsoft_mail_receiver.rb @@ -19,7 +19,7 @@ class FoodsoftMailReceiver < MidiSmtpServer::Smtpd private - def on_rcpt_to_event(ctx, rcpt_to) + def on_rcpt_to_event(_ctx, rcpt_to) recipient = rcpt_to.gsub(/^\s*<\s*(.*)\s*>\s*$/, '\1') @handlers << self.class.find_handler(recipient) rcpt_to @@ -29,20 +29,18 @@ class FoodsoftMailReceiver < MidiSmtpServer::Smtpd end def on_message_data_event(ctx) - begin - @handlers.each do |handler| - handler.call(ctx[:message][:data]) - end - rescue => error - ExceptionNotifier.notify_exception(error, data: ctx) - raise error - ensure - @handlers.clear + @handlers.each do |handler| + handler.call(ctx[:message][:data]) end + rescue => error + ExceptionNotifier.notify_exception(error, data: ctx) + raise error + ensure + @handlers.clear end def self.find_handler(recipient) - m = /(?[^@\.]+)\.(?
[^@]+)(@(?[^@]+))?/.match recipient + m = /(?[^@.]+)\.(?
[^@]+)(@(?[^@]+))?/.match recipient raise "recipient is missing or has an invalid format" if m.nil? raise "Foodcoop '#{m[:foodcoop]}' could not be found" unless FoodsoftConfig.allowed_foodcoop? m[:foodcoop] @@ -51,7 +49,7 @@ class FoodsoftMailReceiver < MidiSmtpServer::Smtpd @@registered_classes.each do |klass| if match = klass.regexp.match(m[:address]) handler = klass.new match - return lambda { |data| handler.received(data) } + return ->(data) { handler.received(data) } end end diff --git a/lib/invoices_csv.rb b/app/lib/invoices_csv.rb similarity index 98% rename from lib/invoices_csv.rb rename to app/lib/invoices_csv.rb index aa20cd08..ebd1f0a9 100644 --- a/lib/invoices_csv.rb +++ b/app/lib/invoices_csv.rb @@ -32,7 +32,7 @@ class InvoicesCsv < RenderCSV t.deposit, t.deposit_credit, t.paid_on, - t.note, + t.note ] end end diff --git a/lib/order_csv.rb b/app/lib/order_csv.rb similarity index 100% rename from lib/order_csv.rb rename to app/lib/order_csv.rb diff --git a/lib/order_pdf.rb b/app/lib/order_pdf.rb similarity index 96% rename from lib/order_pdf.rb rename to app/lib/order_pdf.rb index 034ca51f..8e30ea84 100644 --- a/lib/order_pdf.rb +++ b/app/lib/order_pdf.rb @@ -1,4 +1,4 @@ -class OrderPdf < RenderPDF +class OrderPDF < RenderPDF attr_reader :order def initialize(order, options = {}) @@ -55,7 +55,7 @@ class OrderPdf < RenderPDF end def group_order_article_quantity_with_tolerance(goa) - goa.tolerance > 0 ? "#{goa.quantity} + #{goa.tolerance}" : "#{goa.quantity}" + goa.tolerance > 0 ? "#{goa.quantity} + #{goa.tolerance}" : goa.quantity.to_s end def group_order_article_result(goa) @@ -88,7 +88,7 @@ class OrderPdf < RenderPDF .pluck('groups.name', 'SUM(group_orders.price)', 'ordergroup_id', 'SUM(group_orders.transport)') result.map do |item| - [item.first || stock_ordergroup_name] + item[1..-1] + [item.first || stock_ordergroup_name] + item[1..] end end @@ -103,7 +103,7 @@ class OrderPdf < RenderPDF def each_ordergroup_batch(batch_size) offset = 0 - while true + loop do go_records = ordergroups(offset, batch_size) break unless go_records.any? diff --git a/lib/order_txt.rb b/app/lib/order_txt.rb similarity index 81% rename from lib/order_txt.rb rename to app/lib/order_txt.rb index 5ad1fba6..7f23e705 100644 --- a/lib/order_txt.rb +++ b/app/lib/order_txt.rb @@ -1,5 +1,5 @@ class OrderTxt - def initialize(order, options = {}) + def initialize(order, _options = {}) @order = order end @@ -15,10 +15,10 @@ class OrderTxt text += "****** " + I18n.t('orders.fax.to_address') + "\n\n" text += "#{FoodsoftConfig[:name]}\n#{contact[:street]}\n#{contact[:zip_code]} #{contact[:city]}\n\n" text += "****** " + I18n.t('orders.fax.articles') + "\n\n" - text += "%8s %8s %s\n" % [I18n.t('orders.fax.number'), I18n.t('orders.fax.amount'), I18n.t('orders.fax.name')] + text += format("%8s %8s %s\n", I18n.t('orders.fax.number'), I18n.t('orders.fax.amount'), I18n.t('orders.fax.name')) # now display all ordered articles @order.order_articles.ordered.includes([:article, :article_price]).each do |oa| - text += "%8s %8d %s\n" % [oa.article.order_number, oa.units_to_order.to_i, oa.article.name] + text += format("%8s %8d %s\n", oa.article.order_number, oa.units_to_order.to_i, oa.article.name) end text end diff --git a/lib/ordergroups_csv.rb b/app/lib/ordergroups_csv.rb similarity index 88% rename from lib/ordergroups_csv.rb rename to app/lib/ordergroups_csv.rb index c41d2e83..71a9aaa7 100644 --- a/lib/ordergroups_csv.rb +++ b/app/lib/ordergroups_csv.rb @@ -14,9 +14,9 @@ class OrdergroupsCsv < RenderCSV Ordergroup.human_attribute_name(:break_start), Ordergroup.human_attribute_name(:break_end), Ordergroup.human_attribute_name(:last_user_activity), - Ordergroup.human_attribute_name(:last_order), + Ordergroup.human_attribute_name(:last_order) ] - row + Ordergroup.custom_fields.map { |f| f[:label] } + row + Ordergroup.custom_fields.pluck(:label) end def data @@ -33,7 +33,7 @@ class OrdergroupsCsv < RenderCSV o.break_start, o.break_end, o.last_user_activity, - o.last_order.try(:starts), + o.last_order.try(:starts) ] yield row + Ordergroup.custom_fields.map { |f| o.settings.custom_fields[f[:name]] } end diff --git a/lib/render_csv.rb b/app/lib/render_csv.rb similarity index 100% rename from lib/render_csv.rb rename to app/lib/render_csv.rb diff --git a/lib/render_pdf.rb b/app/lib/render_pdf.rb similarity index 98% rename from lib/render_pdf.rb rename to app/lib/render_pdf.rb index a5cde2b6..aed04011 100644 --- a/lib/render_pdf.rb +++ b/app/lib/render_pdf.rb @@ -18,7 +18,7 @@ class RotatedCell < Prawn::Table::Cell::Text (height + (border_top_width / 2.0) + (border_bottom_width / 2.0)) / tan_rotation end - def styled_width_of(text) + def styled_width_of(_text) options = @text_options.reject { |k| k == :style } with_font { (@pdf.height_of(@content, options) + padding_top + padding_bottom) / tan_rotation } end @@ -156,9 +156,10 @@ class RenderPDF < Prawn::Document def pdf_add_page_breaks?(docid = nil) docid ||= self.class.name.underscore cfg = FoodsoftConfig[:pdf_add_page_breaks] - if cfg.is_a? Array + case cfg + when Array cfg.index(docid.to_s).any? - elsif cfg.is_a? Hash + when Hash cfg[docid.to_s] else cfg diff --git a/lib/spreadsheet_file.rb b/app/lib/spreadsheet_file.rb similarity index 100% rename from lib/spreadsheet_file.rb rename to app/lib/spreadsheet_file.rb diff --git a/lib/templates/haml/scaffold/_form.html.haml b/app/lib/templates/haml/scaffold/_form.html.haml similarity index 100% rename from lib/templates/haml/scaffold/_form.html.haml rename to app/lib/templates/haml/scaffold/_form.html.haml diff --git a/lib/token_verifier.rb b/app/lib/token_verifier.rb similarity index 97% rename from lib/token_verifier.rb rename to app/lib/token_verifier.rb index a8a0f1eb..b481d60f 100644 --- a/lib/token_verifier.rb +++ b/app/lib/token_verifier.rb @@ -21,8 +21,6 @@ class TokenVerifier < ActiveSupport::MessageVerifier # return original message if r.length > 2 r[2] - else - nil end end @@ -32,8 +30,6 @@ class TokenVerifier < ActiveSupport::MessageVerifier class InvalidPrefix < ActiveSupport::MessageVerifier::InvalidSignature; end - protected - def self.secret # secret_key_base for Rails 4, but Rails 3 initializer may still be used Foodsoft::Application.config.secret_key_base || Foodsoft::Application.config.secret_token diff --git a/lib/users_csv.rb b/app/lib/users_csv.rb similarity index 100% rename from lib/users_csv.rb rename to app/lib/users_csv.rb From ea248a5f28df5e9c37f7c1cb5886267402f790ea Mon Sep 17 00:00:00 2001 From: viehlieb Date: Fri, 6 Jan 2023 16:29:08 +0100 Subject: [PATCH 047/105] removing concerns from autoload path --- config/initializers/zeitwerk.rb | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 config/initializers/zeitwerk.rb diff --git a/config/initializers/zeitwerk.rb b/config/initializers/zeitwerk.rb new file mode 100644 index 00000000..155d7702 --- /dev/null +++ b/config/initializers/zeitwerk.rb @@ -0,0 +1,3 @@ +ActiveSupport::Dependencies + .autoload_paths + .delete("#{Rails.root}/app/controllers/concerns") From 50bf879fbfc015e2d9c516a85de3b077c1309d4e Mon Sep 17 00:00:00 2001 From: viehlieb Date: Fri, 6 Jan 2023 16:55:40 +0100 Subject: [PATCH 048/105] resolve zeitwerk issues --- app/lib/articles_csv.rb | 2 +- app/lib/bank_transactions_csv.rb | 2 +- app/lib/financial_transactions_csv.rb | 2 +- app/lib/invoices_csv.rb | 2 +- app/lib/order_csv.rb | 2 +- app/lib/order_pdf.rb | 8 ++++---- app/lib/ordergroups_csv.rb | 2 +- app/lib/render_csv.rb | 2 +- app/lib/render_pdf.rb | 2 +- app/lib/users_csv.rb | 2 +- config/initializers/zeitwerk.rb | 2 ++ 11 files changed, 15 insertions(+), 13 deletions(-) diff --git a/app/lib/articles_csv.rb b/app/lib/articles_csv.rb index 9e6b4f40..55bc7fc5 100644 --- a/app/lib/articles_csv.rb +++ b/app/lib/articles_csv.rb @@ -1,4 +1,4 @@ -class ArticlesCsv < RenderCSV +class ArticlesCsv < RenderCsv include ApplicationHelper def header diff --git a/app/lib/bank_transactions_csv.rb b/app/lib/bank_transactions_csv.rb index 34c39403..4adbc192 100644 --- a/app/lib/bank_transactions_csv.rb +++ b/app/lib/bank_transactions_csv.rb @@ -1,6 +1,6 @@ require 'csv' -class BankTransactionsCsv < RenderCSV +class BankTransactionsCsv < RenderCsv include ApplicationHelper def header diff --git a/app/lib/financial_transactions_csv.rb b/app/lib/financial_transactions_csv.rb index dc21d892..fc12d000 100644 --- a/app/lib/financial_transactions_csv.rb +++ b/app/lib/financial_transactions_csv.rb @@ -1,6 +1,6 @@ require 'csv' -class FinancialTransactionsCsv < RenderCSV +class FinancialTransactionsCsv < RenderCsv include ApplicationHelper def header diff --git a/app/lib/invoices_csv.rb b/app/lib/invoices_csv.rb index ebd1f0a9..eecad298 100644 --- a/app/lib/invoices_csv.rb +++ b/app/lib/invoices_csv.rb @@ -1,6 +1,6 @@ require 'csv' -class InvoicesCsv < RenderCSV +class InvoicesCsv < RenderCsv include ApplicationHelper def header diff --git a/app/lib/order_csv.rb b/app/lib/order_csv.rb index 6ec96581..b238f90c 100644 --- a/app/lib/order_csv.rb +++ b/app/lib/order_csv.rb @@ -1,6 +1,6 @@ require 'csv' -class OrderCsv < RenderCSV +class OrderCsv < RenderCsv def header [ OrderArticle.human_attribute_name(:units_to_order), diff --git a/app/lib/order_pdf.rb b/app/lib/order_pdf.rb index 8e30ea84..164be66b 100644 --- a/app/lib/order_pdf.rb +++ b/app/lib/order_pdf.rb @@ -1,4 +1,4 @@ -class OrderPDF < RenderPDF +class OrderPdf < RenderPdf attr_reader :order def initialize(order, options = {}) @@ -55,7 +55,7 @@ class OrderPDF < RenderPDF end def group_order_article_quantity_with_tolerance(goa) - goa.tolerance > 0 ? "#{goa.quantity} + #{goa.tolerance}" : goa.quantity.to_s + goa.tolerance > 0 ? "#{goa.quantity} + #{goa.tolerance}" : "#{goa.quantity}" end def group_order_article_result(goa) @@ -88,7 +88,7 @@ class OrderPDF < RenderPDF .pluck('groups.name', 'SUM(group_orders.price)', 'ordergroup_id', 'SUM(group_orders.transport)') result.map do |item| - [item.first || stock_ordergroup_name] + item[1..] + [item.first || stock_ordergroup_name] + item[1..-1] end end @@ -103,7 +103,7 @@ class OrderPDF < RenderPDF def each_ordergroup_batch(batch_size) offset = 0 - loop do + while true go_records = ordergroups(offset, batch_size) break unless go_records.any? diff --git a/app/lib/ordergroups_csv.rb b/app/lib/ordergroups_csv.rb index 71a9aaa7..f6fba00f 100644 --- a/app/lib/ordergroups_csv.rb +++ b/app/lib/ordergroups_csv.rb @@ -1,4 +1,4 @@ -class OrdergroupsCsv < RenderCSV +class OrdergroupsCsv < RenderCsv include ApplicationHelper def header diff --git a/app/lib/render_csv.rb b/app/lib/render_csv.rb index b900f1f7..1f20b075 100644 --- a/app/lib/render_csv.rb +++ b/app/lib/render_csv.rb @@ -1,6 +1,6 @@ require 'csv' -class RenderCSV +class RenderCsv include ActionView::Helpers::NumberHelper def initialize(object, options = {}) diff --git a/app/lib/render_pdf.rb b/app/lib/render_pdf.rb index aed04011..479dc4a3 100644 --- a/app/lib/render_pdf.rb +++ b/app/lib/render_pdf.rb @@ -52,7 +52,7 @@ class RotatedCell < Prawn::Table::Cell::Text end end -class RenderPDF < Prawn::Document +class RenderPdf < Prawn::Document include ActionView::Helpers::NumberHelper include ApplicationHelper diff --git a/app/lib/users_csv.rb b/app/lib/users_csv.rb index 56ec3a23..a7d54698 100644 --- a/app/lib/users_csv.rb +++ b/app/lib/users_csv.rb @@ -1,4 +1,4 @@ -class UsersCsv < RenderCSV +class UsersCsv < RenderCsv include ApplicationHelper def header diff --git a/config/initializers/zeitwerk.rb b/config/initializers/zeitwerk.rb index 155d7702..9c505a26 100644 --- a/config/initializers/zeitwerk.rb +++ b/config/initializers/zeitwerk.rb @@ -1,3 +1,5 @@ +# config/initializers/zeitwerk.rb ActiveSupport::Dependencies .autoload_paths .delete("#{Rails.root}/app/controllers/concerns") + \ No newline at end of file From 5fb10ec68688e43226a24e1bd3e9a4a8feb19145 Mon Sep 17 00:00:00 2001 From: viehlieb Date: Fri, 6 Jan 2023 17:01:15 +0100 Subject: [PATCH 049/105] make foodsoft run for dev on rails 7 and ruby 2.7 --- Dockerfile-dev | 3 ++- docker-compose-dev.yml | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Dockerfile-dev b/Dockerfile-dev index ca7865a5..37dce5f6 100644 --- a/Dockerfile-dev +++ b/Dockerfile-dev @@ -1,4 +1,4 @@ -FROM ruby:2.6 +FROM ruby:2.7 # Install dependencies RUN deps='libmagic-dev chromium nodejs' && \ @@ -19,6 +19,7 @@ ENV PORT=3000 \ WORKDIR /app +RUN gem install bundler RUN bundle config build.nokogiri "--use-system-libraries" EXPOSE 3000 diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index 0a8b3fec..5358430a 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -11,6 +11,7 @@ services: build: context: . dockerfile: Dockerfile-dev + platform: linux/x86_64 command: ./proc-start worker volumes: - bundle:/usr/local/bundle From 34e238466f3300671bb99967e2b95c14a33162b9 Mon Sep 17 00:00:00 2001 From: Philipp Rothmann Date: Mon, 16 Jan 2023 18:21:43 +0100 Subject: [PATCH 050/105] fix mail file permission bug --- Gemfile | 2 ++ Gemfile.lock | 6 ++---- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Gemfile b/Gemfile index 81e30a0a..70baa906 100644 --- a/Gemfile +++ b/Gemfile @@ -2,6 +2,8 @@ source "https://rubygems.org" gem "rails", '~> 7.0' +gem 'mail', '~> 2.7.1' # bug with mail 2.8.0 https://github.com/mikel/mail/issues/1489 + gem 'sassc-rails' gem 'less-rails' diff --git a/Gemfile.lock b/Gemfile.lock index e7bf33e7..0dd210d4 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -294,11 +294,8 @@ GEM loofah (2.19.1) crass (~> 1.0.2) nokogiri (>= 1.5.9) - mail (2.8.0) + mail (2.7.1) mini_mime (>= 0.1.1) - net-imap - net-pop - net-smtp mailcatcher (0.2.4) eventmachine haml @@ -630,6 +627,7 @@ DEPENDENCIES kaminari less-rails listen + mail (~> 2.7.1) mailcatcher midi-smtp-server mime-types From 5cbe8dd9685b688c87b49cc9aff68b48fce6f52c Mon Sep 17 00:00:00 2001 From: Philipp Rothmann Date: Tue, 17 Jan 2023 14:37:10 +0100 Subject: [PATCH 051/105] fix database_config --- app/lib/foodsoft_config.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/lib/foodsoft_config.rb b/app/lib/foodsoft_config.rb index 2893a3e3..6ea166d3 100644 --- a/app/lib/foodsoft_config.rb +++ b/app/lib/foodsoft_config.rb @@ -230,7 +230,7 @@ class FoodsoftConfig end def setup_database - database_config = ActiveRecord::Base.configurations[Rails.env] + database_config = ActiveRecord::Base.configurations.find_db_config(Rails.env).configuration_hash database_config = database_config.merge(config[:database]) if config[:database].present? ActiveRecord::Base.establish_connection(database_config) end From 808baa5a987a3da537a8a48097e64a040403bf17 Mon Sep 17 00:00:00 2001 From: viehlieb Date: Tue, 14 Feb 2023 12:24:11 +0100 Subject: [PATCH 052/105] change .search to .ransack for updated ransack gem --- app/controllers/finance/financial_transactions_controller.rb | 2 +- .../app/controllers/current_orders/articles_controller.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/finance/financial_transactions_controller.rb b/app/controllers/finance/financial_transactions_controller.rb index 930acebe..e0c53e19 100644 --- a/app/controllers/finance/financial_transactions_controller.rb +++ b/app/controllers/finance/financial_transactions_controller.rb @@ -18,7 +18,7 @@ class Finance::FinancialTransactionsController < ApplicationController sort = "created_on DESC" end - @q = FinancialTransaction.search(params[:q]) + @q = FinancialTransaction.ransack(params[:q]) @financial_transactions_all = @q.result(distinct: true).includes(:user).order(sort) @financial_transactions_all = @financial_transactions_all.visible unless params[:show_hidden] @financial_transactions_all = @financial_transactions_all.where(ordergroup_id: @ordergroup.id) if @ordergroup diff --git a/plugins/current_orders/app/controllers/current_orders/articles_controller.rb b/plugins/current_orders/app/controllers/current_orders/articles_controller.rb index 0e4b7dd9..ef23f332 100644 --- a/plugins/current_orders/app/controllers/current_orders/articles_controller.rb +++ b/plugins/current_orders/app/controllers/current_orders/articles_controller.rb @@ -32,7 +32,7 @@ class CurrentOrders::ArticlesController < ApplicationController else @order_articles = OrderArticle.where(order_id: @current_orders.all.map(&:id)) end - @q = OrderArticle.search(params[:q]) + @q = OrderArticle.ransack(params[:q]) @order_articles = @order_articles.ordered.merge(@q.result).includes(:article, :article_price) @order_article = @order_articles.where(id: params[:id]).first end From 45ae1928914e89fa6886110b5026b7e8b2d80f27 Mon Sep 17 00:00:00 2001 From: viehlieb Date: Tue, 14 Feb 2023 12:25:41 +0100 Subject: [PATCH 053/105] move BigDecimal.new to BigDecimal() --- app/models/group_order.rb | 4 ++-- config/initializers/extensions.rb | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/models/group_order.rb b/app/models/group_order.rb index c789ef4e..f3153c44 100644 --- a/app/models/group_order.rb +++ b/app/models/group_order.rb @@ -32,8 +32,8 @@ class GroupOrder < ApplicationRecord # Generate some data for the javascript methods in ordering view def load_data data = {} - data[:account_balance] = ordergroup.nil? ? BigDecimal.new('+Infinity') : ordergroup.account_balance - data[:available_funds] = ordergroup.nil? ? BigDecimal.new('+Infinity') : ordergroup.get_available_funds(self) + data[:account_balance] = ordergroup.nil? ? BigDecimal('+Infinity') : ordergroup.account_balance + data[:available_funds] = ordergroup.nil? ? BigDecimal('+Infinity') : ordergroup.get_available_funds(self) # load prices and other stuff.... data[:order_articles] = {} diff --git a/config/initializers/extensions.rb b/config/initializers/extensions.rb index 799f52e6..68c7c8f4 100644 --- a/config/initializers/extensions.rb +++ b/config/initializers/extensions.rb @@ -3,7 +3,7 @@ class String # remove comma from decimal inputs def self.delocalized_decimal(string) if !string.blank? and string.is_a?(String) - BigDecimal.new(string.sub(',', '.')) + BigDecimal(string.sub(',', '.')) else string end From 6e721db6543c208c04a217e55227a46c341233fb Mon Sep 17 00:00:00 2001 From: Philipp Rothmann Date: Mon, 16 Jan 2023 15:31:13 +0100 Subject: [PATCH 054/105] upgrade dockerfile to rails7 --- Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index c999b3d4..9509c4d3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM ruby:2.6 +FROM ruby:2.7 RUN supercronicUrl=https://github.com/aptible/supercronic/releases/download/v0.1.3/supercronic-linux-amd64 && \ supercronicBin=/usr/local/bin/supercronic && \ @@ -22,6 +22,7 @@ RUN buildDeps='libmagic-dev' && \ apt-get update && \ apt-get install --no-install-recommends -y $buildDeps && \ echo 'gem: --no-document' >> ~/.gemrc && \ + gem install bundler && \ bundle config build.nokogiri "--use-system-libraries" && \ bundle install --deployment --without development test -j 4 && \ apt-get purge -y --auto-remove $buildDeps && \ From b06656ba804af8f06d2773e4d51e077f300d7060 Mon Sep 17 00:00:00 2001 From: FGU Date: Thu, 2 Feb 2023 10:14:26 +0100 Subject: [PATCH 055/105] fix docker-compose --- docker-compose-dev.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index 5358430a..b0a325db 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -11,7 +11,7 @@ services: build: context: . dockerfile: Dockerfile-dev - platform: linux/x86_64 + platform: linux/x86_64 command: ./proc-start worker volumes: - bundle:/usr/local/bundle From a7775f5a986ba7a7c659444cc203172479c813c4 Mon Sep 17 00:00:00 2001 From: Philipp Rothmann Date: Sat, 6 May 2023 11:46:45 +0200 Subject: [PATCH 056/105] add setup_storage to stock_config --- lib/tasks/foodsoft_setup.rake | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/tasks/foodsoft_setup.rake b/lib/tasks/foodsoft_setup.rake index baa483d1..be2309ff 100644 --- a/lib/tasks/foodsoft_setup.rake +++ b/lib/tasks/foodsoft_setup.rake @@ -53,6 +53,7 @@ namespace :foodsoft do desc "Initialize stock configuration" task :stock_config do setup_app_config + setup_storage setup_development end end From f6fb804bbe4fd24e98718036e5a9897dfa98170b Mon Sep 17 00:00:00 2001 From: Philipp Rothmann Date: Fri, 12 May 2023 12:40:27 +0200 Subject: [PATCH 057/105] chore: update Gemfile.lock --- Gemfile.lock | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 0dd210d4..3896da95 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -268,6 +268,8 @@ GEM railties (>= 4.2.0) thor (>= 0.14, < 2.0) json (2.6.3) + json-schema (3.0.0) + addressable (>= 2.8) jsonapi-renderer (0.2.2) kaminari (1.2.2) activesupport (>= 4.1.0) @@ -333,8 +335,8 @@ GEM nio4r (2.5.8) nokogiri (1.13.10-x86_64-linux) racc (~> 1.4) - parallel (1.22.1) - parser (3.2.0.0) + parallel (1.23.0) + parser (3.2.2.1) ast (~> 2.4.1) pdf-core (0.9.0) polyglot (0.3.5) @@ -414,7 +416,7 @@ GEM redis-namespace (1.10.0) redis (>= 4) ref (2.0.0) - regexp_parser (2.6.1) + regexp_parser (2.8.0) responders (3.0.1) actionpack (>= 5.0) railties (>= 5.0) @@ -453,10 +455,10 @@ GEM rspec-support (~> 3.11) rspec-rerun (1.1.0) rspec (~> 3.0) - rspec-support (3.11.1) + rspec-support (3.12.0) rswag-api (2.7.0) railties (>= 3.1, < 7.1) - rswag-specs (2.7.0) + rswag-specs (2.9.0) activesupport (>= 3.1, < 7.1) json-schema (>= 2.2, < 4.0) railties (>= 3.1, < 7.1) @@ -464,18 +466,18 @@ GEM rswag-ui (2.7.0) actionpack (>= 3.1, < 7.1) railties (>= 3.1, < 7.1) - rubocop (1.36.0) + rubocop (1.50.2) json (~> 2.3) parallel (~> 1.10) - parser (>= 3.1.2.1) + parser (>= 3.2.0.0) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 1.8, < 3.0) rexml (>= 3.2.5, < 4.0) - rubocop-ast (>= 1.24.1, < 2.0) + rubocop-ast (>= 1.28.0, < 2.0) ruby-progressbar (~> 1.7) - unicode-display_width (>= 1.4.0, < 3.0) - rubocop-ast (1.24.1) - parser (>= 3.1.1.0) + unicode-display_width (>= 2.4.0, < 3.0) + rubocop-ast (1.28.1) + parser (>= 3.2.1.0) rubocop-rails (2.17.4) activesupport (>= 4.2.0) rack (>= 1.1) @@ -485,7 +487,7 @@ GEM ruby-filemagic (0.7.3) ruby-ole (1.2.12.2) ruby-prof (1.4.5) - ruby-progressbar (1.11.0) + ruby-progressbar (1.13.0) ruby-units (3.0.0) ruby2_keywords (0.0.5) rubyzip (2.3.2) @@ -679,4 +681,4 @@ DEPENDENCIES whenever BUNDLED WITH - 2.4.2 + 2.4.13 From fb2b4d8a8a6ec656c854133fe4329a5f02cfb237 Mon Sep 17 00:00:00 2001 From: Philipp Rothmann Date: Fri, 12 May 2023 13:01:12 +0200 Subject: [PATCH 058/105] chore: rubocop chore: fix api test conventions chore: rubocop -A spec/ chore: more rubocop -A fix failing test rubocop fixes removes helper methods that are in my opinion dead code more rubocop fixes rubocop -a --auto-gen-config --- .rubocop_todo.yml | 1829 ++++++----------- Gemfile | 93 +- Rakefile | 2 +- .../admin/bank_accounts_controller.rb | 26 +- .../admin/bank_gateways_controller.rb | 10 +- app/controllers/admin/configs_controller.rb | 26 +- app/controllers/admin/finances_controller.rb | 8 +- ...inancial_transaction_classes_controller.rb | 26 +- .../financial_transaction_types_controller.rb | 26 +- .../admin/mail_delivery_status_controller.rb | 12 +- .../admin/ordergroups_controller.rb | 15 +- .../admin/supplier_categories_controller.rb | 10 +- app/controllers/admin/users_controller.rb | 20 +- .../admin/workgroups_controller.rb | 8 +- app/controllers/api/v1/base_controller.rb | 35 +- .../user/financial_transactions_controller.rb | 3 +- .../user/group_order_articles_controller.rb | 7 +- .../api/v1/user/ordergroup_controller.rb | 4 +- app/controllers/application_controller.rb | 18 +- .../article_categories_controller.rb | 8 +- app/controllers/articles_controller.rb | 128 +- app/controllers/concerns/auth.rb | 34 +- app/controllers/concerns/auth_api.rb | 14 +- app/controllers/concerns/foodcoop_scope.rb | 4 +- app/controllers/concerns/locale.rb | 14 +- app/controllers/concerns/send_order_pdf.rb | 2 +- app/controllers/deliveries_controller.rb | 20 +- app/controllers/feedback_controller.rb | 7 +- .../finance/balancing_controller.rb | 44 +- .../finance/bank_accounts_controller.rb | 8 +- .../finance/bank_transactions_controller.rb | 30 +- .../finance/financial_links_controller.rb | 16 +- .../financial_transactions_controller.rb | 78 +- .../finance/invoices_controller.rb | 30 +- .../finance/ordergroups_controller.rb | 4 +- .../foodcoop/ordergroups_controller.rb | 12 +- app/controllers/foodcoop/users_controller.rb | 9 +- .../foodcoop/workgroups_controller.rb | 8 +- .../group_order_articles_controller.rb | 2 +- app/controllers/group_orders_controller.rb | 70 +- app/controllers/home_controller.rb | 54 +- app/controllers/invites_controller.rb | 8 +- app/controllers/login_controller.rb | 23 +- app/controllers/order_articles_controller.rb | 22 +- app/controllers/order_comments_controller.rb | 6 +- app/controllers/orders_controller.rb | 110 +- app/controllers/sessions_controller.rb | 6 +- app/controllers/stock_takings_controller.rb | 10 +- app/controllers/stockit_controller.rb | 104 +- app/controllers/styles_controller.rb | 2 +- app/controllers/suppliers_controller.rb | 18 +- app/controllers/tasks_controller.rb | 58 +- app/controllers/users_controller.rb | 2 +- app/documents/order_by_articles.rb | 6 +- app/documents/order_by_groups.rb | 6 +- app/documents/order_fax.rb | 26 +- app/documents/order_matrix.rb | 10 +- app/helpers/admin/configs_helper.rb | 19 +- app/helpers/admin/ordergroups_helper.rb | 4 +- app/helpers/application_helper.rb | 88 +- app/helpers/articles_helper.rb | 8 +- app/helpers/deliveries_helper.rb | 18 +- app/helpers/finance/balancing_helper.rb | 6 +- app/helpers/finance/invoices_helper.rb | 4 +- app/helpers/group_order_articles_helper.rb | 6 +- app/helpers/group_orders_helper.rb | 25 +- app/helpers/order_articles_helper.rb | 2 +- app/helpers/orders_helper.rb | 40 +- app/helpers/stockit_helper.rb | 10 +- app/helpers/tasks_helper.rb | 12 +- app/inputs/delta_input.rb | 2 +- app/lib/apple_bar.rb | 2 +- app/lib/bank_account_connector.rb | 6 +- app/lib/bank_account_information_importer.rb | 10 +- app/lib/date_time_attribute_validate.rb | 48 +- app/lib/foodsoft/expansion_variables.rb | 8 +- app/lib/foodsoft_config.rb | 24 +- app/lib/foodsoft_date_util.rb | 18 +- app/lib/foodsoft_file.rb | 22 +- app/lib/foodsoft_mail_receiver.rb | 14 +- app/lib/order_csv.rb | 2 +- app/lib/order_pdf.rb | 8 +- app/lib/order_txt.rb | 18 +- app/lib/render_csv.rb | 8 +- app/lib/render_pdf.rb | 13 +- app/lib/token_verifier.rb | 6 +- app/mailers/mailer.rb | 18 +- app/models/article.rb | 165 +- app/models/article_category.rb | 21 +- app/models/article_price.rb | 8 +- app/models/bank_account.rb | 14 +- app/models/bank_gateway.rb | 2 +- app/models/bank_transaction.rb | 12 +- app/models/concerns/custom_fields.rb | 2 +- app/models/concerns/find_each_with_order.rb | 4 +- app/models/concerns/localize_input.rb | 10 +- .../concerns/mark_as_deleted_with_name.rb | 2 +- app/models/concerns/price_calculation.rb | 2 +- app/models/delivery.rb | 12 +- app/models/financial_link.rb | 4 +- app/models/financial_transaction.rb | 20 +- app/models/financial_transaction_class.rb | 2 +- app/models/financial_transaction_type.rb | 10 +- app/models/group.rb | 6 +- app/models/group_order.rb | 55 +- app/models/group_order_article.rb | 73 +- app/models/group_order_article_quantity.rb | 2 +- app/models/invite.rb | 20 +- app/models/invoice.rb | 36 +- app/models/membership.rb | 2 +- app/models/order.rb | 176 +- app/models/order_article.rb | 66 +- app/models/order_comment.rb | 4 +- app/models/ordergroup.rb | 75 +- app/models/periodic_task_group.rb | 16 +- app/models/shared_article.rb | 26 +- app/models/shared_supplier.rb | 13 +- app/models/stock_article.rb | 6 +- app/models/stock_change.rb | 6 +- app/models/stock_event.rb | 2 +- app/models/supplier.rb | 70 +- app/models/task.rb | 50 +- app/models/user.rb | 130 +- app/models/workgroup.rb | 18 +- config.ru | 2 +- config/application.rb | 14 +- config/environments/production.rb | 21 +- config/environments/test.rb | 6 +- config/initializers/currency_display.rb | 3 +- config/initializers/doorkeeper.rb | 2 +- config/initializers/exception_notification.rb | 8 +- config/initializers/extensions.rb | 4 +- .../initializers/filter_parameter_logging.rb | 4 +- config/initializers/rack.rb | 2 +- config/initializers/ruby_units.rb | 14 +- config/initializers/secret_token.rb | 4 +- config/initializers/session_store.rb | 2 +- config/initializers/simple_form_bootstrap.rb | 4 +- config/initializers/zeitwerk.rb | 3 +- config/navigation.rb | 33 +- config/puma.rb | 8 +- config/routes.rb | 52 +- config/schedule.rb | 16 +- config/spring.rb | 4 +- db/migrate/001_create_users.rb | 31 +- db/migrate/002_create_groups.rb | 33 +- db/migrate/003_create_suppliers.rb | 20 +- db/migrate/004_create_article_meta.rb | 12 +- .../005_create_financial_transactions.rb | 18 +- db/migrate/006_create_articles.rb | 28 +- db/migrate/007_create_article_prices.rb | 22 +- db/migrate/008_create_orders.rb | 93 +- db/migrate/009_create_order_results.rb | 34 +- db/migrate/011_create_comments.rb | 18 +- db/migrate/012_create_order_clearing.rb | 6 +- db/migrate/013_add_messaging.rb | 14 +- db/migrate/014_create_tasks.rb | 20 +- db/migrate/015_change_result_quantities.rb | 8 +- db/migrate/018_create_invites.rb | 10 +- .../019_remove_uniqueness_of_article_name.rb | 6 +- db/migrate/021_remove_table_article_prices.rb | 70 +- db/migrate/022_add_required_user_for_task.rb | 4 +- db/migrate/024_add_deposit_defaults.rb | 3 +- db/migrate/025_extend_comments.rb | 4 +- .../20090120184410_road_to_version_three.rb | 62 +- .../20090317175355_add_profit_to_orders.rb | 2 +- ...75756_create_pages.foodsoft_wiki_engine.rb | 2 +- ...31156_modify_group_order_article_result.rb | 2 +- .../20090907120012_add_missing_indexes.rb | 39 +- .../20110507184920_add_duration_to_tasks.rb | 2 +- ...7192928_add_task_duration_to_workgroups.rb | 2 +- ..._next_weekly_tasks_number_to_workgroups.rb | 2 +- .../20130622095040_move_weekly_tasks.rb | 30 +- ...0130702113610_update_group_order_totals.rb | 17 +- db/migrate/20130718183100_create_settings.rb | 2 +- .../20130718183101_migrate_user_settings.rb | 3 +- .../20130920201529_allow_missing_nick.rb | 4 +- ...result_computed_to_group_order_articles.rb | 2 +- ...73000_delete_empty_group_order_articles.rb | 3 +- ...20140921104907_remove_stale_memberships.rb | 2 +- ...160217194036_add_role_invoices_to_group.rb | 2 +- ...0160218151041_add_attachment_to_invoice.rb | 2 +- ...e_financial_transaction_class_and_types.rb | 8 +- .../20160224201529_allow_stock_group_order.rb | 4 +- ...ail_to_message.foodsoft_messages_engine.rb | 2 +- ...70801000000_create_mail_delivery_status.rb | 4 +- ...0000_create_polls.foodsoft_polls_engine.rb | 4 +- ...ease_choices_size.foodsoft_polls_engine.rb | 2 +- ...te_printer_jobs.foodsoft_printer_engine.rb | 2 +- ...te_message_recipients.foodsoft_messages.rb | 2 +- ...te_active_storage_tables.active_storage.rb | 3 +- ...0257_introduce_received_state_in_orders.rb | 2 +- ..._to_active_storage_blobs.active_storage.rb | 12 +- ..._storage_variant_records.active_storage.rb | 2 +- db/seeds/minimal.seeds.rb | 36 +- db/seeds/seed_helper.rb | 4 +- db/seeds/small.en.seeds.rb | 453 ++-- db/seeds/small.nl.seeds.rb | 452 ++-- lib/tasks/foodsoft.rake | 60 +- lib/tasks/foodsoft_setup.rake | 85 +- lib/tasks/multicoops.rake | 16 +- lib/tasks/resque.rake | 26 +- lib/tasks/rspec.rake | 2 +- .../current_orders/articles_controller.rb | 8 +- .../current_orders/group_orders_controller.rb | 12 +- .../current_orders/ordergroups_controller.rb | 9 +- .../documents/multiple_orders_by_articles.rb | 10 +- .../documents/multiple_orders_by_groups.rb | 10 +- .../app/helpers/current_orders_helper.rb | 4 +- plugins/current_orders/config/routes.rb | 8 +- .../foodsoft_current_orders.gemspec | 23 +- .../lib/foodsoft_current_orders.rb | 4 +- .../lib/foodsoft_current_orders/engine.rb | 11 +- .../lib/foodsoft_current_orders/version.rb | 2 +- plugins/discourse/Rakefile | 4 +- .../app/controllers/discourse_controller.rb | 4 +- .../controllers/discourse_login_controller.rb | 6 +- .../controllers/discourse_sso_controller.rb | 4 +- plugins/discourse/foodsoft_discourse.gemspec | 23 +- .../foodsoft_discourse/redirect_to_login.rb | 4 +- .../lib/foodsoft_discourse/version.rb | 2 +- plugins/documents/Rakefile | 4 +- .../app/controllers/documents_controller.rb | 32 +- plugins/documents/foodsoft_documents.gemspec | 25 +- .../lib/foodsoft_documents/engine.rb | 6 +- .../lib/foodsoft_documents/version.rb | 2 +- plugins/links/Rakefile | 4 +- .../app/controllers/admin/links_controller.rb | 4 +- .../links/app/controllers/links_controller.rb | 10 +- plugins/links/foodsoft_links.gemspec | 23 +- plugins/links/lib/foodsoft_links/engine.rb | 20 +- plugins/links/lib/foodsoft_links/version.rb | 2 +- plugins/messages/Rakefile | 4 +- .../admin/messagegroups_controller.rb | 6 +- .../controllers/messagegroups_controller.rb | 6 +- .../app/controllers/messages_controller.rb | 39 +- .../messages/app/helpers/messages_helper.rb | 6 +- .../mail_receivers/messages_mail_receiver.rb | 44 +- plugins/messages/app/models/message.rb | 38 +- .../messages/app/models/message_recipient.rb | 2 +- plugins/messages/app/models/messagegroup.rb | 2 - plugins/messages/config/routes.rb | 4 +- .../20160226000000_add_email_to_message.rb | 2 +- plugins/messages/foodsoft_messages.gemspec | 31 +- plugins/messages/lib/foodsoft_messages.rb | 8 +- .../messages/lib/foodsoft_messages/engine.rb | 19 +- .../lib/foodsoft_messages/user_link.rb | 4 +- .../messages/lib/foodsoft_messages/version.rb | 2 +- plugins/polls/Rakefile | 4 +- .../polls/app/controllers/polls_controller.rb | 42 +- .../db/migrate/20181110000000_create_polls.rb | 4 +- .../20181120000000_increase_choices_size.rb | 2 +- plugins/polls/foodsoft_polls.gemspec | 23 +- plugins/polls/lib/foodsoft_polls/engine.rb | 6 +- plugins/polls/lib/foodsoft_polls/version.rb | 2 +- plugins/printer/Rakefile | 4 +- .../app/controllers/printer_controller.rb | 4 +- .../controllers/printer_jobs_controller.rb | 6 +- plugins/printer/config/routes.rb | 2 +- .../20181201000000_create_printer_jobs.rb | 2 +- plugins/printer/foodsoft_printer.gemspec | 25 +- .../printer/lib/foodsoft_printer/engine.rb | 13 +- .../foodsoft_printer/order_printer_jobs.rb | 10 +- .../printer/lib/foodsoft_printer/version.rb | 2 +- plugins/uservoice/foodsoft_uservoice.gemspec | 23 +- plugins/uservoice/lib/foodsoft_uservoice.rb | 10 +- .../lib/foodsoft_uservoice/version.rb | 2 +- plugins/wiki/Rakefile | 4 +- .../wiki/app/controllers/pages_controller.rb | 84 +- plugins/wiki/app/helpers/pages_helper.rb | 62 +- plugins/wiki/app/models/page.rb | 59 +- plugins/wiki/app/views/pages/all.rss.builder | 12 +- plugins/wiki/config/routes.rb | 10 +- .../db/migrate/20090325175756_create_pages.rb | 2 +- plugins/wiki/foodsoft_wiki.gemspec | 25 +- plugins/wiki/lib/foodsoft_wiki/engine.rb | 10 +- plugins/wiki/lib/foodsoft_wiki/mailer.rb | 10 +- plugins/wiki/lib/foodsoft_wiki/version.rb | 2 +- plugins/wiki/lib/foodsoft_wiki/wiki_parser.rb | 8 +- script/rails | 4 +- spec/controllers/home_controller_spec.rb | 19 +- spec/factories/delivery.rb | 2 +- spec/factories/group_order.rb | 2 +- spec/factories/invoice.rb | 4 +- spec/factories/order.rb | 6 +- spec/factories/supplier.rb | 6 +- spec/factories/user.rb | 16 +- spec/integration/articles_spec.rb | 46 +- spec/integration/balancing_spec.rb | 49 +- spec/integration/config_spec.rb | 8 +- spec/integration/home_spec.rb | 2 +- spec/integration/login_spec.rb | 4 +- spec/integration/order_spec.rb | 10 +- .../product_distribution_example_spec.rb | 16 +- spec/integration/receive_spec.rb | 24 +- spec/integration/session_spec.rb | 10 +- spec/integration/supplier_spec.rb | 16 +- .../bank_account_information_importer_spec.rb | 12 +- spec/lib/bank_transaction_reference_spec.rb | 33 +- spec/lib/foodsoft_mail_receiver_spec.rb | 90 +- spec/lib/token_verifier_spec.rb | 8 +- spec/models/article_spec.rb | 40 +- spec/models/bank_transaction_spec.rb | 66 +- spec/models/delivery_spec.rb | 4 +- spec/models/group_order_article_spec.rb | 23 +- spec/models/group_order_spec.rb | 6 +- spec/models/order_article_spec.rb | 65 +- spec/models/order_spec.rb | 59 +- spec/models/ordergroup_spec.rb | 76 +- spec/models/supplier_spec.rb | 46 +- spec/models/user_spec.rb | 92 +- .../article_categories_controller_spec.rb} | 2 +- .../configs_controller_spec.rb} | 2 +- ...al_transaction_classes_controller_spec.rb} | 2 +- ...cial_transaction_types_controller_spec.rb} | 2 +- ...financial_transactions_controller_spec.rb} | 6 +- .../navigations_controller_spec.rb} | 2 +- .../order_articles_controller_spec.rb} | 2 +- .../orders_controller_spec.rb} | 2 +- .../user/financial_transactions_spec.rb | 10 +- .../user/group_order_articles_spec.rb | 18 +- spec/requests/api/{ => v1}/user/users_spec.rb | 10 +- spec/spec_helper.rb | 16 +- spec/support/api_helper.rb | 10 +- spec/support/api_oauth.rb | 2 +- spec/support/coverage.rb | 4 +- spec/support/faker.rb | 2 +- spec/support/integration.rb | 2 +- spec/support/session_helper.rb | 9 +- spec/support/shared_database.rb | 2 +- spec/swagger_helper.rb | 7 +- 331 files changed, 4263 insertions(+), 4507 deletions(-) rename spec/requests/api/{article_categories_spec.rb => v1/article_categories_controller_spec.rb} (96%) rename spec/requests/api/{configs_spec.rb => v1/configs_controller_spec.rb} (90%) rename spec/requests/api/{financial_transaction_classes_spec.rb => v1/financial_transaction_classes_controller_spec.rb} (95%) rename spec/requests/api/{financial_transaction_types_spec.rb => v1/financial_transaction_types_controller_spec.rb} (95%) rename spec/requests/api/{financial_transactions_spec.rb => v1/financial_transactions_controller_spec.rb} (88%) rename spec/requests/api/{navigations_spec.rb => v1/navigations_controller_spec.rb} (90%) rename spec/requests/api/{order_articles_spec.rb => v1/order_articles_controller_spec.rb} (98%) rename spec/requests/api/{orders_spec.rb => v1/orders_controller_spec.rb} (96%) rename spec/requests/api/{ => v1}/user/financial_transactions_spec.rb (92%) rename spec/requests/api/{ => v1}/user/group_order_articles_spec.rb (88%) rename spec/requests/api/{ => v1}/user/users_spec.rb (94%) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 2303bab6..7995a1d5 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,297 +1,310 @@ # This configuration was generated by # `rubocop --auto-gen-config` -# on 2022-10-08 12:00:00 UTC using RuboCop version 1.36.0. +# on 2023-05-26 14:15:44 UTC using RuboCop version 1.50.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 # versions of RuboCop, may require this file to be generated again. -# Offense count: 28 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: TreatCommentsAsGroupSeparators, ConsiderPunctuation, Include. -# Include: **/*.gemfile, **/Gemfile, **/gems.rb -Bundler/OrderedGems: +# Offense count: 2 +# Configuration parameters: EnforcedStyle, AllowedGems, Include. +# SupportedStyles: Gemfile, gems.rb, gemspec +# Include: **/*.gemspec, **/Gemfile, **/gems.rb +Gemspec/DevelopmentDependencies: Exclude: - - "Gemfile" + - 'plugins/messages/foodsoft_messages.gemspec' + - 'plugins/wiki/foodsoft_wiki.gemspec' # Offense count: 9 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: Include. -# Include: **/*.gemspec -Gemspec/RequireMFA: - Exclude: - - "plugins/current_orders/foodsoft_current_orders.gemspec" - - "plugins/discourse/foodsoft_discourse.gemspec" - - "plugins/documents/foodsoft_documents.gemspec" - - "plugins/links/foodsoft_links.gemspec" - - "plugins/messages/foodsoft_messages.gemspec" - - "plugins/polls/foodsoft_polls.gemspec" - - "plugins/printer/foodsoft_printer.gemspec" - - "plugins/uservoice/foodsoft_uservoice.gemspec" - - "plugins/wiki/foodsoft_wiki.gemspec" - -# Offense count: 9 -# Configuration parameters: Include. +# Configuration parameters: Severity, Include. # Include: **/*.gemspec Gemspec/RequiredRubyVersion: Exclude: - - "plugins/current_orders/foodsoft_current_orders.gemspec" - - "plugins/discourse/foodsoft_discourse.gemspec" - - "plugins/documents/foodsoft_documents.gemspec" - - "plugins/links/foodsoft_links.gemspec" - - "plugins/messages/foodsoft_messages.gemspec" - - "plugins/polls/foodsoft_polls.gemspec" - - "plugins/printer/foodsoft_printer.gemspec" - - "plugins/uservoice/foodsoft_uservoice.gemspec" - - "plugins/wiki/foodsoft_wiki.gemspec" + - 'plugins/current_orders/foodsoft_current_orders.gemspec' + - 'plugins/discourse/foodsoft_discourse.gemspec' + - 'plugins/documents/foodsoft_documents.gemspec' + - 'plugins/links/foodsoft_links.gemspec' + - 'plugins/messages/foodsoft_messages.gemspec' + - 'plugins/polls/foodsoft_polls.gemspec' + - 'plugins/printer/foodsoft_printer.gemspec' + - 'plugins/uservoice/foodsoft_uservoice.gemspec' + - 'plugins/wiki/foodsoft_wiki.gemspec' -# Offense count: 8 -# Configuration parameters: AllowedMethods, AllowedPatterns, IgnoredMethods. -Lint/AmbiguousBlockAssociation: - Exclude: - - "lib/foodsoft/expansion_variables.rb" - - "spec/api/v1/user/financial_transactions_spec.rb" - - "spec/api/v1/user/group_order_articles_spec.rb" - - "spec/models/article_spec.rb" - -# Offense count: 4 +# Offense count: 12 # This cop supports safe autocorrection (--autocorrect). -Lint/AmbiguousOperatorPrecedence: +# Configuration parameters: EnforcedStyle, IndentationWidth. +# SupportedStyles: with_first_argument, with_fixed_indentation +Layout/ArgumentAlignment: Exclude: - - "app/models/concerns/price_calculation.rb" - - "db/seeds/seed_helper.rb" - - "plugins/messages/app/mail_receivers/messages_mail_receiver.rb" - - "plugins/wiki/app/helpers/pages_helper.rb" + - 'app/controllers/articles_controller.rb' + - 'app/models/ordergroup.rb' + - 'config/initializers/currency_display.rb' + - 'db/migrate/001_create_users.rb' + - 'db/migrate/002_create_groups.rb' + - 'db/migrate/008_create_orders.rb' + - 'plugins/current_orders/app/helpers/current_orders_helper.rb' + +# Offense count: 1 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyleAlignWith. +# SupportedStylesAlignWith: either, start_of_block, start_of_line +Layout/BlockAlignment: + Exclude: + - 'app/lib/foodsoft_config.rb' + +# Offense count: 1 +# This cop supports safe autocorrection (--autocorrect). +Layout/BlockEndNewline: + Exclude: + - 'app/lib/foodsoft_config.rb' + +# Offense count: 1 +# This cop supports safe autocorrection (--autocorrect). +Layout/ClosingParenthesisIndentation: + Exclude: + - 'app/controllers/concerns/auth.rb' # Offense count: 2 # This cop supports safe autocorrection (--autocorrect). -Lint/AmbiguousRegexpLiteral: +Layout/EmptyLineAfterGuardClause: Exclude: - - "app/models/article_category.rb" - - "lib/foodsoft/expansion_variables.rb" + - 'db/migrate/20130622095040_move_weekly_tasks.rb' + - 'db/migrate/20230106144438_add_service_name_to_active_storage_blobs.active_storage.rb' -# Offense count: 40 +# Offense count: 1 +# This cop supports safe autocorrection (--autocorrect). +Layout/EmptyLinesAroundMethodBody: + Exclude: + - 'db/migrate/20230106144438_add_service_name_to_active_storage_blobs.active_storage.rb' + +# Offense count: 2 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: AllowForAlignment, AllowBeforeTrailingComments, ForceEqualSignAlignment. +Layout/ExtraSpacing: + Exclude: + - 'db/migrate/021_remove_table_article_prices.rb' + +# Offense count: 1 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyle, IndentationWidth. +# SupportedStyles: consistent, consistent_relative_to_receiver, special_for_inner_method_call, special_for_inner_method_call_in_parentheses +Layout/FirstArgumentIndentation: + Exclude: + - 'app/controllers/concerns/auth.rb' + +# Offense count: 12 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: AllowMultipleStyles, EnforcedHashRocketStyle, EnforcedColonStyle, EnforcedLastArgumentHashStyle. +# SupportedHashRocketStyles: key, separator, table +# SupportedColonStyles: key, separator, table +# SupportedLastArgumentHashStyles: always_inspect, always_ignore, ignore_implicit, ignore_explicit +Layout/HashAlignment: + Exclude: + - 'app/controllers/admin/ordergroups_controller.rb' + - 'app/controllers/orders_controller.rb' + - 'app/documents/order_fax.rb' + - 'db/migrate/001_create_users.rb' + - 'db/migrate/002_create_groups.rb' + - 'db/migrate/008_create_orders.rb' + - 'db/migrate/20190101000000_create_active_storage_tables.active_storage.rb' + - 'spec/lib/bank_transaction_reference_spec.rb' + +# Offense count: 6 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyle. +# SupportedStyles: normal, indented_internal_methods +Layout/IndentationConsistency: + Exclude: + - 'db/migrate/20090120184410_road_to_version_three.rb' + - 'db/migrate/20230106144438_add_service_name_to_active_storage_blobs.active_storage.rb' + +# Offense count: 2 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: Width, AllowedPatterns. +Layout/IndentationWidth: + Exclude: + - 'app/lib/foodsoft_config.rb' + +# Offense count: 1 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyle. +# SupportedStyles: symmetrical, new_line, same_line +Layout/MultilineMethodCallBraceLayout: + Exclude: + - 'app/controllers/concerns/auth.rb' + +# Offense count: 15 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: AllowInHeredoc. +Layout/TrailingWhitespace: + Exclude: + - 'app/controllers/admin/ordergroups_controller.rb' + - 'app/controllers/articles_controller.rb' + - 'app/controllers/orders_controller.rb' + - 'app/documents/order_fax.rb' + - 'app/models/ordergroup.rb' + - 'config/initializers/currency_display.rb' + - 'db/migrate/001_create_users.rb' + - 'db/migrate/002_create_groups.rb' + - 'db/migrate/008_create_orders.rb' + - 'db/migrate/20190101000000_create_active_storage_tables.active_storage.rb' + - 'db/migrate/20230106144438_add_service_name_to_active_storage_blobs.active_storage.rb' + - 'plugins/current_orders/app/helpers/current_orders_helper.rb' + - 'spec/lib/bank_transaction_reference_spec.rb' + +# Offense count: 41 +# This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: AllowSafeAssignment. Lint/AssignmentInCondition: Enabled: false -# Offense count: 3 -# This cop supports safe autocorrection (--autocorrect). -Lint/BigDecimalNew: - Exclude: - - "app/models/group_order.rb" - - "config/initializers/extensions.rb" - # Offense count: 1 # This cop supports unsafe autocorrection (--autocorrect-all). Lint/BooleanSymbol: Exclude: - - "app/models/delivery.rb" + - 'app/models/delivery.rb' -# Offense count: 2 -# This cop supports safe autocorrection (--autocorrect). -Lint/DeprecatedClassMethods: - Exclude: - - "config/initializers/secret_token.rb" - - "lib/tasks/foodsoft_setup.rake" - -# Offense count: 3 +# Offense count: 4 # Configuration parameters: IgnoreLiteralBranches, IgnoreConstantBranches. Lint/DuplicateBranch: Exclude: - - "app/controllers/concerns/auth_api.rb" - - "app/controllers/orders_controller.rb" + - 'app/controllers/concerns/auth_api.rb' + - 'app/controllers/orders_controller.rb' + - 'plugins/wiki/app/controllers/pages_controller.rb' # Offense count: 3 Lint/DuplicateMethods: Exclude: - - "app/models/invoice.rb" - - "plugins/messages/app/models/message.rb" + - 'app/models/invoice.rb' + - 'plugins/messages/app/models/message.rb' # Offense count: 2 # Configuration parameters: AllowComments, AllowEmptyLambdas. Lint/EmptyBlock: Exclude: - - "spec/factories/group_order_article.rb" - - "spec/factories/group_order_article_quantity.rb" + - 'spec/factories/group_order_article.rb' + - 'spec/factories/group_order_article_quantity.rb' # Offense count: 1 # This cop supports safe autocorrection (--autocorrect). Lint/EnsureReturn: Exclude: - - "app/controllers/finance/bank_accounts_controller.rb" + - 'app/controllers/finance/bank_accounts_controller.rb' -# Offense count: 2 +# Offense count: 1 Lint/IneffectiveAccessModifier: Exclude: - - "lib/foodsoft_mail_receiver.rb" - - "lib/token_verifier.rb" + - 'app/lib/foodsoft_mail_receiver.rb' # Offense count: 1 # This cop supports unsafe autocorrection (--autocorrect-all). Lint/Loop: Exclude: - - "app/models/concerns/mark_as_deleted_with_name.rb" + - 'app/models/concerns/mark_as_deleted_with_name.rb' # Offense count: 2 Lint/MixedRegexpCaptureTypes: Exclude: - - "lib/bank_transaction_reference.rb" - - "lib/foodsoft_mail_receiver.rb" + - 'app/lib/bank_transaction_reference.rb' + - 'app/lib/foodsoft_mail_receiver.rb' # Offense count: 1 # This cop supports unsafe autocorrection (--autocorrect-all). Lint/NonDeterministicRequireOrder: Exclude: - - "spec/spec_helper.rb" + - 'spec/spec_helper.rb' # Offense count: 1 Lint/ReturnInVoidContext: Exclude: - - "lib/foodsoft_config.rb" - -# Offense count: 4 -# This cop supports safe autocorrection (--autocorrect). -Lint/SendWithMixinArgument: - Exclude: - - "plugins/discourse/lib/foodsoft_discourse/redirect_to_login.rb" - - "plugins/messages/lib/foodsoft_messages/user_link.rb" - - "plugins/uservoice/lib/foodsoft_uservoice.rb" - - "plugins/wiki/lib/foodsoft_wiki/mailer.rb" + - 'app/lib/foodsoft_config.rb' # Offense count: 1 # Configuration parameters: IgnoreImplicitReferences. Lint/ShadowedArgument: Exclude: - - "app/helpers/deliveries_helper.rb" + - 'app/helpers/deliveries_helper.rb' -# Offense count: 8 +# Offense count: 6 Lint/ShadowingOuterLocalVariable: Exclude: - - "app/documents/order_matrix.rb" - - "app/helpers/group_orders_helper.rb" - - "app/models/group_order.rb" - - "app/models/group_order_article.rb" - - "plugins/discourse/app/controllers/discourse_login_controller.rb" - - "plugins/polls/app/controllers/polls_controller.rb" - - "spec/integration/config_spec.rb" + - 'app/documents/order_matrix.rb' + - 'app/helpers/group_orders_helper.rb' + - 'app/models/group_order.rb' + - 'app/models/group_order_article.rb' + - 'plugins/discourse/app/controllers/discourse_login_controller.rb' + - 'plugins/polls/app/controllers/polls_controller.rb' -# Offense count: 3 +# Offense count: 2 # Configuration parameters: AllowComments, AllowNil. Lint/SuppressedException: Exclude: - - "config/initializers/rails6_backports.rb" - - "lib/foodsoft_config.rb" - - "lib/tasks/rspec.rake" + - 'app/lib/foodsoft_config.rb' + - 'lib/tasks/rspec.rake' # Offense count: 1 # Configuration parameters: AllowKeywordBlockArguments. Lint/UnderscorePrefixedVariableName: Exclude: - - "app/models/order_article.rb" - -# Offense count: 16 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: IgnoreEmptyBlocks, AllowUnusedKeywordArguments. -Lint/UnusedBlockArgument: - Exclude: - - "app/models/article.rb" - - "app/models/group_order.rb" - - "config/initializers/exception_notification.rb" - - "plugins/printer/lib/foodsoft_printer/engine.rb" - - "plugins/uservoice/lib/foodsoft_uservoice.rb" - - "plugins/wiki/lib/foodsoft_wiki/wiki_parser.rb" - - "spec/factories/supplier.rb" - - "spec/factories/user.rb" - - "spec/integration/config_spec.rb" - - "spec/models/article_spec.rb" - -# Offense count: 23 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AllowUnusedKeywordArguments, IgnoreEmptyMethods, IgnoreNotImplementedMethods. -Lint/UnusedMethodArgument: - Exclude: - - "app/controllers/api/v1/base_controller.rb" - - "app/controllers/concerns/foodcoop_scope.rb" - - "app/helpers/application_helper.rb" - - "app/models/article.rb" - - "app/models/article_category.rb" - - "app/models/financial_transaction.rb" - - "app/models/group_order.rb" - - "app/models/group_order_article.rb" - - "app/models/order.rb" - - "app/models/order_article.rb" - - "app/models/supplier.rb" - - "lib/foodsoft_mail_receiver.rb" - - "lib/order_txt.rb" - - "lib/render_pdf.rb" - - "plugins/wiki/lib/foodsoft_wiki/mailer.rb" - -# Offense count: 2 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: ContextCreatingMethods, MethodCreatingMethods. -Lint/UselessAccessModifier: - Exclude: - - "lib/token_verifier.rb" - - "plugins/messages/app/models/messagegroup.rb" + - 'app/models/order_article.rb' # Offense count: 14 Lint/UselessAssignment: Exclude: - - "app/controllers/admin/ordergroups_controller.rb" - - "app/helpers/admin/configs_helper.rb" - - "app/inputs/date_picker_time_input.rb" - - "app/models/order_article.rb" - - "db/migrate/003_create_suppliers.rb" - - "db/migrate/004_create_article_meta.rb" - - "db/migrate/005_create_financial_transactions.rb" - - "db/migrate/008_create_orders.rb" - - "db/migrate/20181201000100_create_message_recipients.foodsoft_messages.rb" - - "plugins/current_orders/app/documents/multiple_orders_by_articles.rb" - - "plugins/current_orders/app/documents/multiple_orders_by_groups.rb" - - "spec/lib/foodsoft_config_spec.rb" + - 'app/controllers/admin/ordergroups_controller.rb' + - 'app/helpers/admin/configs_helper.rb' + - 'app/inputs/date_picker_time_input.rb' + - 'app/models/order_article.rb' + - 'db/migrate/003_create_suppliers.rb' + - 'db/migrate/004_create_article_meta.rb' + - 'db/migrate/005_create_financial_transactions.rb' + - 'db/migrate/008_create_orders.rb' + - 'db/migrate/20181201000100_create_message_recipients.foodsoft_messages.rb' + - 'plugins/current_orders/app/documents/multiple_orders_by_articles.rb' + - 'plugins/current_orders/app/documents/multiple_orders_by_groups.rb' + - 'spec/lib/foodsoft_config_spec.rb' # Offense count: 1 # Configuration parameters: CheckForMethodsWithNoSideEffects. Lint/Void: Exclude: - - "lib/foodsoft_config.rb" + - 'app/lib/foodsoft_config.rb' -# Offense count: 160 -# Configuration parameters: AllowedMethods, AllowedPatterns, IgnoredMethods, CountRepeatedAttributes. +# Offense count: 161 +# Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes. Metrics/AbcSize: Max: 143 -# Offense count: 17 -# Configuration parameters: CountComments, CountAsOne, ExcludedMethods, AllowedMethods, AllowedPatterns, IgnoredMethods, inherit_mode. +# Offense count: 13 +# Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns, inherit_mode. # AllowedMethods: refine Metrics/BlockLength: - Max: 210 + Max: 66 -# Offense count: 6 +# Offense count: 2 # Configuration parameters: CountBlocks. Metrics/BlockNesting: - Max: 5 + Max: 4 -# Offense count: 18 +# Offense count: 19 # Configuration parameters: CountComments, CountAsOne. Metrics/ClassLength: - Max: 288 + Max: 294 -# Offense count: 52 -# Configuration parameters: AllowedMethods, AllowedPatterns, IgnoredMethods. +# Offense count: 51 +# Configuration parameters: AllowedMethods, AllowedPatterns. Metrics/CyclomaticComplexity: - Max: 22 + Max: 20 -# Offense count: 163 -# Configuration parameters: CountComments, CountAsOne, ExcludedMethods, AllowedMethods, AllowedPatterns, IgnoredMethods. +# Offense count: 164 +# Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns. Metrics/MethodLength: Max: 112 # Offense count: 4 # Configuration parameters: CountComments, CountAsOne. Metrics/ModuleLength: - Max: 190 + Max: 191 # Offense count: 1 # Configuration parameters: CountKeywordArgs, MaxOptionalParameters. @@ -299,47 +312,45 @@ Metrics/ParameterLists: Max: 6 # Offense count: 36 -# Configuration parameters: AllowedMethods, AllowedPatterns, IgnoredMethods. +# Configuration parameters: AllowedMethods, AllowedPatterns. Metrics/PerceivedComplexity: - Max: 23 + Max: 21 # Offense count: 6 Naming/AccessorMethodName: Exclude: - - "app/controllers/admin/configs_controller.rb" - - "lib/bank_account_connector.rb" - - "lib/foodsoft_config.rb" - - "spec/integration/config_spec.rb" + - 'app/controllers/admin/configs_controller.rb' + - 'app/lib/bank_account_connector.rb' + - 'app/lib/foodsoft_config.rb' + - 'spec/integration/config_spec.rb' # Offense count: 1 # Configuration parameters: MinNameLength, AllowNamesEndingInNumbers, AllowedNames, ForbiddenNames. Naming/BlockParameterName: Exclude: - - "db/migrate/008_create_orders.rb" + - 'db/migrate/008_create_orders.rb' # Offense count: 1 # Configuration parameters: EnforcedStyleForLeadingUnderscores. # SupportedStylesForLeadingUnderscores: disallowed, required, optional Naming/MemoizedInstanceVariableName: Exclude: - - "plugins/messages/app/models/message.rb" + - 'plugins/messages/app/models/message.rb' -# Offense count: 19 +# Offense count: 16 # Configuration parameters: MinNameLength, AllowNamesEndingInNumbers, AllowedNames, ForbiddenNames. -# AllowedNames: as, at, by, db, id, in, io, ip, of, on, os, pp, to +# AllowedNames: as, at, by, cc, db, id, if, in, io, ip, of, on, os, pp, to Naming/MethodParameterName: Exclude: - - "app/controllers/api/v1/base_controller.rb" - - "app/controllers/api/v1/user/group_order_articles_controller.rb" - - "app/helpers/application_helper.rb" - - "app/helpers/orders_helper.rb" - - "app/models/user.rb" - - "lib/foodsoft_date_util.rb" - - "lib/render_pdf.rb" - - "spec/integration/config_spec.rb" - - "spec/integration/receive_spec.rb" - - "spec/models/order_article_spec.rb" - - "spec/support/shared_database.rb" + - 'app/controllers/api/v1/base_controller.rb' + - 'app/controllers/api/v1/user/group_order_articles_controller.rb' + - 'app/helpers/application_helper.rb' + - 'app/helpers/orders_helper.rb' + - 'app/models/user.rb' + - 'spec/integration/config_spec.rb' + - 'spec/integration/receive_spec.rb' + - 'spec/models/order_article_spec.rb' + - 'spec/support/shared_database.rb' # Offense count: 11 # Configuration parameters: NamePrefix, ForbiddenPrefixes, AllowedMethods, MethodDefinitionMacros. @@ -349,217 +360,165 @@ Naming/MethodParameterName: # MethodDefinitionMacros: define_method, define_singleton_method Naming/PredicateName: Exclude: - - "app/models/financial_transaction_class.rb" - - "app/models/financial_transaction_type.rb" - - "app/models/order.rb" - - "app/models/periodic_task_group.rb" - - "app/models/supplier.rb" - - "app/models/task.rb" - - "app/models/user.rb" - - "app/serializers/order_serializer.rb" - - "plugins/messages/app/models/message.rb" + - 'app/models/financial_transaction_class.rb' + - 'app/models/financial_transaction_type.rb' + - 'app/models/order.rb' + - 'app/models/periodic_task_group.rb' + - 'app/models/supplier.rb' + - 'app/models/task.rb' + - 'app/models/user.rb' + - 'app/serializers/order_serializer.rb' + - 'plugins/messages/app/models/message.rb' -# Offense count: 45 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: PreferredName. -Naming/RescuedExceptionsVariableName: - Enabled: false - -# Offense count: 22 +# Offense count: 17 # Configuration parameters: EnforcedStyle, AllowedIdentifiers, AllowedPatterns. # SupportedStyles: snake_case, camelCase Naming/VariableName: Exclude: - - "app/controllers/concerns/auth.rb" - - "app/helpers/application_helper.rb" - - "db/migrate/008_create_orders.rb" - - "lib/bank_account_information_importer.rb" - -# Offense count: 23 -# Configuration parameters: EnforcedStyle, CheckMethodNames, CheckSymbols, AllowedIdentifiers, AllowedPatterns. -# SupportedStyles: snake_case, normalcase, non_integer -# AllowedIdentifiers: capture3, iso8601, rfc1123_date, rfc822, rfc2822, rfc3339 -Naming/VariableNumber: - Exclude: - - "app/documents/order_matrix.rb" - - "spec/api/v1/swagger_spec.rb" - - "spec/api/v1/user/group_order_articles_spec.rb" - - "spec/api/v1/user/ordergroup_spec.rb" + - 'app/controllers/concerns/auth.rb' + - 'app/helpers/application_helper.rb' + - 'db/migrate/008_create_orders.rb' # Offense count: 4 -RSpec/AnyInstance: +# Configuration parameters: EnforcedStyle, CheckMethodNames, CheckSymbols, AllowedIdentifiers, AllowedPatterns. +# SupportedStyles: snake_case, normalcase, non_integer +# AllowedIdentifiers: capture3, iso8601, rfc1123_date, rfc822, rfc2822, rfc3339, x86_64 +Naming/VariableNumber: Exclude: - - "spec/api/v1/swagger_spec.rb" - - "spec/api/v1/user/group_order_articles_spec.rb" + - 'app/documents/order_matrix.rb' # Offense count: 2 RSpec/BeforeAfterAll: Exclude: - - "spec/lib/foodsoft_mail_receiver_spec.rb" + - 'spec/lib/foodsoft_mail_receiver_spec.rb' -# Offense count: 9 +# Offense count: 10 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnabledMethods. RSpec/Capybara/FeatureMethods: Exclude: - - "spec/integration/articles_spec.rb" - - "spec/integration/balancing_spec.rb" - - "spec/integration/config_spec.rb" - - "spec/integration/home_spec.rb" - - "spec/integration/login_spec.rb" - - "spec/integration/order_spec.rb" - - "spec/integration/product_distribution_example_spec.rb" - - "spec/integration/receive_spec.rb" - - "spec/integration/session_spec.rb" - - "spec/integration/supplier_spec.rb" + - 'spec/integration/articles_spec.rb' + - 'spec/integration/balancing_spec.rb' + - 'spec/integration/config_spec.rb' + - 'spec/integration/home_spec.rb' + - 'spec/integration/login_spec.rb' + - 'spec/integration/order_spec.rb' + - 'spec/integration/product_distribution_example_spec.rb' + - 'spec/integration/receive_spec.rb' + - 'spec/integration/session_spec.rb' + - 'spec/integration/supplier_spec.rb' # Offense count: 4 RSpec/Capybara/SpecificMatcher: Exclude: - - "spec/integration/login_spec.rb" - - "spec/integration/session_spec.rb" + - 'spec/integration/login_spec.rb' + - 'spec/integration/session_spec.rb' -# Offense count: 27 +# Offense count: 12 # Configuration parameters: Prefixes, AllowedPatterns. # Prefixes: when, with, without RSpec/ContextWording: Exclude: - - "spec/api/v1/swagger_spec.rb" - - "spec/api/v1/user/group_order_articles_spec.rb" - - "spec/models/order_article_spec.rb" - - "spec/models/supplier_spec.rb" + - 'spec/models/order_article_spec.rb' + - 'spec/models/supplier_spec.rb' -# Offense count: 1 +# Offense count: 7 # Configuration parameters: IgnoredMetadata. RSpec/DescribeClass: Exclude: - - "spec/api/v1/swagger_spec.rb" + - 'spec/integration/balancing_spec.rb' + - 'spec/integration/config_spec.rb' + - 'spec/integration/home_spec.rb' + - 'spec/integration/product_distribution_example_spec.rb' + - 'spec/integration/receive_spec.rb' + - 'spec/integration/session_spec.rb' + - 'spec/integration/supplier_spec.rb' -# Offense count: 126 +# Offense count: 128 # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: SkipBlocks, EnforcedStyle. # SupportedStyles: described_class, explicit RSpec/DescribedClass: Exclude: - - "spec/lib/bank_transaction_reference_spec.rb" - - "spec/lib/foodsoft_config_spec.rb" - - "spec/lib/foodsoft_mail_receiver_spec.rb" - - "spec/lib/token_verifier_spec.rb" - - "spec/models/group_order_article_spec.rb" - - "spec/models/order_article_spec.rb" - - "spec/models/order_spec.rb" - - "spec/models/ordergroup_spec.rb" - - "spec/models/user_spec.rb" + - 'spec/integration/order_spec.rb' + - 'spec/lib/bank_transaction_reference_spec.rb' + - 'spec/lib/foodsoft_config_spec.rb' + - 'spec/lib/foodsoft_mail_receiver_spec.rb' + - 'spec/lib/token_verifier_spec.rb' + - 'spec/models/group_order_article_spec.rb' + - 'spec/models/order_article_spec.rb' + - 'spec/models/order_spec.rb' + - 'spec/models/ordergroup_spec.rb' + - 'spec/models/user_spec.rb' -# Offense count: 15 +# Offense count: 14 # This cop supports unsafe autocorrection (--autocorrect-all). RSpec/EmptyExampleGroup: Exclude: - # exclude for rswag tests: - - 'spec/requests/api/**/*_spec.rb' + - 'spec/requests/api/v1/article_categories_controller_spec.rb' + - 'spec/requests/api/v1/configs_controller_spec.rb' + - 'spec/requests/api/v1/financial_transaction_classes_controller_spec.rb' + - 'spec/requests/api/v1/financial_transaction_types_controller_spec.rb' + - 'spec/requests/api/v1/financial_transactions_controller_spec.rb' + - 'spec/requests/api/v1/navigations_controller_spec.rb' + - 'spec/requests/api/v1/order_articles_controller_spec.rb' + - 'spec/requests/api/v1/orders_controller_spec.rb' + - 'spec/requests/api/v1/user/group_order_articles_spec.rb' + - 'spec/requests/api/v1/user/users_spec.rb' - - -# Offense count: 65 +# Offense count: 69 # Configuration parameters: CountAsOne. RSpec/ExampleLength: Max: 81 -# Offense count: 7 +# Offense count: 3 # Configuration parameters: Include, CustomTransform, IgnoreMethods, SpecSuffixOnly. # Include: **/*_spec*rb*, **/spec/**/* RSpec/FilePath: Exclude: - - "spec/api/v1/order_articles_spec.rb" - - "spec/api/v1/user/financial_transactions_spec.rb" - - "spec/api/v1/user/group_order_articles_spec.rb" - - "spec/api/v1/user/ordergroup_spec.rb" - - "spec/integration/articles_spec.rb" - - "spec/integration/login_spec.rb" - - "spec/lib/bank_account_information_importer_spec.rb" - -# Offense count: 3 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: implicit, each, example -RSpec/HookArgument: - Exclude: - - "spec/spec_helper.rb" - -# Offense count: 3 -# This cop supports safe autocorrection (--autocorrect). -RSpec/HooksBeforeExamples: - Exclude: - - "spec/integration/balancing_spec.rb" - - "spec/lib/foodsoft_mail_receiver_spec.rb" - -# Offense count: 1 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: single_line_only, single_statement_only, disallow -RSpec/ImplicitSubject: - Exclude: - - "spec/api/v1/swagger_spec.rb" + - 'spec/integration/articles_spec.rb' + - 'spec/integration/login_spec.rb' + - 'spec/lib/bank_account_information_importer_spec.rb' # Offense count: 6 # Configuration parameters: AssignmentOnly. RSpec/InstanceVariable: Exclude: - - "spec/lib/foodsoft_mail_receiver_spec.rb" + - 'spec/lib/foodsoft_mail_receiver_spec.rb' -# Offense count: 2 +# Offense count: 3 RSpec/IteratedExpectation: Exclude: - - "spec/models/order_spec.rb" - - "spec/models/supplier_spec.rb" + - 'spec/models/order_spec.rb' + - 'spec/models/supplier_spec.rb' -# Offense count: 13 +# Offense count: 3 RSpec/LetSetup: Exclude: - - "spec/api/v1/swagger_spec.rb" - - "spec/api/v1/user/group_order_articles_spec.rb" - - "spec/api/v1/user/ordergroup_spec.rb" - - "spec/models/bank_transaction_spec.rb" - - "spec/models/group_order_article_spec.rb" - - "spec/models/supplier_spec.rb" + - 'spec/models/bank_transaction_spec.rb' + - 'spec/models/group_order_article_spec.rb' + - 'spec/models/supplier_spec.rb' # Offense count: 3 RSpec/MissingExampleGroupArgument: Exclude: - - "spec/models/group_order_article_spec.rb" - - "spec/models/group_order_spec.rb" - - "spec/models/user_spec.rb" + - 'spec/models/group_order_article_spec.rb' + - 'spec/models/group_order_spec.rb' + - 'spec/models/user_spec.rb' -# Offense count: 88 +# Offense count: 103 RSpec/MultipleExpectations: Max: 22 -# Offense count: 83 +# Offense count: 36 # Configuration parameters: AllowSubject. RSpec/MultipleMemoizedHelpers: - Max: 17 + Max: 15 -# Offense count: 29 +# Offense count: 8 # Configuration parameters: AllowedGroups. RSpec/NestedGroups: - Max: 6 - -# Offense count: 31 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: not_to, to_not -RSpec/NotToNot: - Exclude: - - "spec/api/v1/user/financial_transactions_spec.rb" - - "spec/api/v1/user/group_order_articles_spec.rb" - - "spec/integration/balancing_spec.rb" - - "spec/integration/login_spec.rb" - - "spec/integration/receive_spec.rb" - - "spec/integration/session_spec.rb" - - "spec/lib/token_verifier_spec.rb" - - "spec/models/article_spec.rb" - - "spec/models/order_spec.rb" - - "spec/models/supplier_spec.rb" + Max: 4 # Offense count: 8 # This cop supports unsafe autocorrection (--autocorrect-all). @@ -567,92 +526,61 @@ RSpec/NotToNot: # SupportedStyles: inflected, explicit RSpec/PredicateMatcher: Exclude: - - "spec/models/article_spec.rb" - - "spec/models/user_spec.rb" + - 'spec/models/article_spec.rb' + - 'spec/models/user_spec.rb' # Offense count: 6 RSpec/RepeatedDescription: Exclude: - - "spec/lib/bank_account_information_importer_spec.rb" - - "spec/lib/bank_transaction_reference_spec.rb" - - "spec/lib/foodsoft_mail_receiver_spec.rb" + - 'spec/lib/bank_account_information_importer_spec.rb' + - 'spec/lib/bank_transaction_reference_spec.rb' + - 'spec/lib/foodsoft_mail_receiver_spec.rb' # Offense count: 4 RSpec/RepeatedExample: Exclude: - - "spec/lib/bank_transaction_reference_spec.rb" - - "spec/lib/foodsoft_mail_receiver_spec.rb" + - 'spec/lib/bank_transaction_reference_spec.rb' + - 'spec/lib/foodsoft_mail_receiver_spec.rb' -# Offense count: 7 +# Offense count: 5 RSpec/ScatteredSetup: Exclude: - - "spec/api/v1/user/ordergroup_spec.rb" - - "spec/integration/balancing_spec.rb" - - "spec/integration/login_spec.rb" - -# Offense count: 4 -# Configuration parameters: AllowedPatterns, IgnoredPatterns. -# SupportedStyles: snake_case, camelCase -RSpec/VariableName: - EnforcedStyle: snake_case - AllowedPatterns: - - ^Authorization$ + - 'spec/integration/balancing_spec.rb' + - 'spec/integration/login_spec.rb' # Offense count: 1 # Configuration parameters: IgnoreNameless, IgnoreSymbolicNames. RSpec/VerifiedDoubles: Exclude: - - "spec/support/api_oauth.rb" - -# Offense count: 45 -# This cop supports unsafe autocorrection (--autocorrect-all). -Rails/ActiveRecordAliases: - Enabled: false - -# Offense count: 3 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: Include. -# Include: app/models/**/*.rb -Rails/ActiveRecordCallbacksOrder: - Exclude: - - "app/models/financial_transaction_type.rb" - - "app/models/order.rb" - - "app/models/stock_change.rb" + - 'spec/support/api_oauth.rb' # Offense count: 1 # This cop supports unsafe autocorrection (--autocorrect-all). Rails/ApplicationMailer: Exclude: - - "app/mailers/mailer.rb" + - 'app/mailers/mailer.rb' # Offense count: 20 # This cop supports unsafe autocorrection (--autocorrect-all). Rails/ApplicationRecord: Exclude: - - "app/models/supplier_category.rb" - - "db/migrate/20130718183101_migrate_user_settings.rb" - - "db/migrate/20181201000100_create_message_recipients.foodsoft_messages.rb" - - "db/migrate/20181201000301_change_ordergroup_default_in_financial_transaction.rb" - - "db/migrate/20181201000302_change_stock_supplier_to_null_in_order.rb" - - "db/migrate/20181201000305_ensure_article_for_article_price.rb" - - "db/migrate/20181201000400_create_supplier_categories.rb" - - "db/migrate/20181204000000_clear_invalid_invoices_from_orders.rb" - - "db/migrate/20181204070000_create_stock_events.rb" - - "plugins/messages/app/models/message_recipient.rb" - - "plugins/polls/app/models/poll.rb" - - "plugins/polls/app/models/poll_choice.rb" - - "plugins/polls/app/models/poll_vote.rb" - - "plugins/printer/app/models/printer_job.rb" - - "plugins/printer/app/models/printer_job_update.rb" + - 'app/models/supplier_category.rb' + - 'db/migrate/20130718183101_migrate_user_settings.rb' + - 'db/migrate/20181201000100_create_message_recipients.foodsoft_messages.rb' + - 'db/migrate/20181201000301_change_ordergroup_default_in_financial_transaction.rb' + - 'db/migrate/20181201000302_change_stock_supplier_to_null_in_order.rb' + - 'db/migrate/20181201000305_ensure_article_for_article_price.rb' + - 'db/migrate/20181201000400_create_supplier_categories.rb' + - 'db/migrate/20181204000000_clear_invalid_invoices_from_orders.rb' + - 'db/migrate/20181204070000_create_stock_events.rb' + - 'plugins/messages/app/models/message_recipient.rb' + - 'plugins/polls/app/models/poll.rb' + - 'plugins/polls/app/models/poll_choice.rb' + - 'plugins/polls/app/models/poll_vote.rb' + - 'plugins/printer/app/models/printer_job.rb' + - 'plugins/printer/app/models/printer_job_update.rb' -# Offense count: 1 -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: NilOrEmpty, NotPresent, UnlessPresent. -Rails/Blank: - Exclude: - - "app/controllers/api/v1/base_controller.rb" - -# Offense count: 34 +# Offense count: 35 # Configuration parameters: Include. # Include: db/migrate/*.rb Rails/CreateTableWithTimestamps: @@ -663,146 +591,94 @@ Rails/CreateTableWithTimestamps: # SupportedStyles: strict, flexible Rails/Date: Exclude: - - "app/controllers/deliveries_controller.rb" - - "app/documents/order_fax.rb" - - "app/models/periodic_task_group.rb" - - "spec/integration/order_spec.rb" - - "spec/models/order_spec.rb" + - 'app/controllers/deliveries_controller.rb' + - 'app/documents/order_fax.rb' + - 'app/models/periodic_task_group.rb' + - 'spec/integration/order_spec.rb' + - 'spec/models/order_spec.rb' -# Offense count: 67 +# Offense count: 68 # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: Whitelist, AllowedMethods, AllowedReceivers. -# Whitelist: find_by_sql -# AllowedMethods: find_by_sql -# AllowedReceivers: Gem::Specification +# Whitelist: find_by_sql, find_by_token_for +# AllowedMethods: find_by_sql, find_by_token_for +# AllowedReceivers: Gem::Specification, page Rails/DynamicFindBy: Enabled: false -# Offense count: 1 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: Include. -# Include: app/models/**/*.rb -Rails/EnumHash: - Exclude: - - "app/models/order.rb" - -# Offense count: 8 +# Offense count: 4 # Configuration parameters: EnforcedStyle. # SupportedStyles: slashes, arguments Rails/FilePath: Exclude: - - "config/application.rb" - - "config/initializers/secret_token.rb" - - "lib/order_txt.rb" - - "lib/render_csv.rb" - - "lib/render_pdf.rb" - - "plugins/current_orders/app/documents/multiple_orders_by_groups.rb" - - "spec/api/v1/swagger_spec.rb" - -# Offense count: 7 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: Include, AllowedMethods, AllowedPatterns, IgnoredMethods. -# Include: app/models/**/*.rb -# AllowedMethods: order, limit, select, lock -# IgnoredMethods: order, limit, select, lock -Rails/FindEach: - Exclude: - - "app/models/bank_account.rb" - - "app/models/order.rb" - - "app/models/ordergroup.rb" - - "app/models/periodic_task_group.rb" - - "app/models/task.rb" + - 'config/application.rb' + - 'config/initializers/secret_token.rb' + - 'plugins/current_orders/app/documents/multiple_orders_by_groups.rb' # Offense count: 26 # Configuration parameters: Include. # Include: app/models/**/*.rb Rails/HasManyOrHasOneDependent: Exclude: - - "app/models/article.rb" - - "app/models/article_category.rb" - - "app/models/article_price.rb" - - "app/models/financial_link.rb" - - "app/models/financial_transaction.rb" - - "app/models/group_order.rb" - - "app/models/order.rb" - - "app/models/ordergroup.rb" - - "app/models/shared_supplier.rb" - - "app/models/stock_article.rb" - - "app/models/supplier.rb" - - "app/models/supplier_category.rb" - - "app/models/user.rb" - - "app/models/workgroup.rb" + - 'app/models/article.rb' + - 'app/models/article_category.rb' + - 'app/models/article_price.rb' + - 'app/models/financial_link.rb' + - 'app/models/financial_transaction.rb' + - 'app/models/group_order.rb' + - 'app/models/order.rb' + - 'app/models/ordergroup.rb' + - 'app/models/shared_supplier.rb' + - 'app/models/stock_article.rb' + - 'app/models/supplier.rb' + - 'app/models/supplier_category.rb' + - 'app/models/user.rb' + - 'app/models/workgroup.rb' # Offense count: 14 # Configuration parameters: Include. # Include: app/helpers/**/*.rb Rails/HelperInstanceVariable: Exclude: - - "app/helpers/admin/configs_helper.rb" - - "app/helpers/application_helper.rb" - - "app/helpers/orders_helper.rb" + - 'app/helpers/admin/configs_helper.rb' + - 'app/helpers/application_helper.rb' + - 'app/helpers/orders_helper.rb' -# Offense count: 14 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: numeric, symbolic -Rails/HttpStatus: - Exclude: - - "app/controllers/admin/bank_accounts_controller.rb" - - "app/controllers/admin/financial_transaction_classes_controller.rb" - - "app/controllers/admin/financial_transaction_types_controller.rb" - - "app/controllers/api/v1/base_controller.rb" - - "app/controllers/styles_controller.rb" - - "plugins/links/app/controllers/links_controller.rb" - -# Offense count: 2 -# This cop supports safe autocorrection (--autocorrect). -Rails/IndexBy: - Exclude: - - "app/models/order.rb" - - "spec/api/v1/user/ordergroup_spec.rb" - -# Offense count: 23 +# Offense count: 22 # Configuration parameters: IgnoreScopes, Include. # Include: app/models/**/*.rb Rails/InverseOf: Exclude: - - "app/models/article.rb" - - "app/models/bank_transaction.rb" - - "app/models/financial_transaction.rb" - - "app/models/group_order.rb" - - "app/models/invoice.rb" - - "app/models/mail_delivery_status.rb" - - "app/models/order.rb" - - "app/models/shared_article.rb" - - "app/models/shared_supplier.rb" - - "app/models/stock_change.rb" - - "app/models/supplier.rb" - - "app/models/task.rb" - - "app/models/user.rb" - - "app/models/workgroup.rb" + - 'app/models/article.rb' + - 'app/models/bank_transaction.rb' + - 'app/models/financial_transaction.rb' + - 'app/models/group_order.rb' + - 'app/models/invoice.rb' + - 'app/models/mail_delivery_status.rb' + - 'app/models/order.rb' + - 'app/models/shared_article.rb' + - 'app/models/shared_supplier.rb' + - 'app/models/stock_change.rb' + - 'app/models/supplier.rb' + - 'app/models/task.rb' + - 'app/models/user.rb' + - 'app/models/workgroup.rb' # Offense count: 2 # Configuration parameters: Include. # Include: app/controllers/**/*.rb, app/mailers/**/*.rb Rails/LexicallyScopedActionFilter: Exclude: - - "app/controllers/group_orders_controller.rb" - - "app/controllers/suppliers_controller.rb" - -# Offense count: 1 -# This cop supports safe autocorrection (--autocorrect). -Rails/LinkToBlank: - Exclude: - - "app/helpers/application_helper.rb" + - 'app/controllers/group_orders_controller.rb' + - 'app/controllers/suppliers_controller.rb' # Offense count: 3 # This cop supports unsafe autocorrection (--autocorrect-all). Rails/NegateInclude: Exclude: - - "app/helpers/application_helper.rb" - - "app/models/supplier.rb" - - "lib/tasks/foodsoft_setup.rake" + - 'app/helpers/application_helper.rb' + - 'app/models/supplier.rb' + - 'lib/tasks/foodsoft_setup.rake' # Offense count: 34 # This cop supports unsafe autocorrection (--autocorrect-all). @@ -810,55 +686,36 @@ Rails/NegateInclude: # Include: app/**/*.rb, config/**/*.rb, db/**/*.rb, lib/**/*.rb Rails/Output: Exclude: - - "config/initializers/resque.rb" - - "config/initializers/secret_token.rb" - - "db/migrate/001_create_users.rb" - - "db/migrate/002_create_groups.rb" - - "db/migrate/003_create_suppliers.rb" - - "db/migrate/004_create_article_meta.rb" - - "db/migrate/005_create_financial_transactions.rb" - - "db/migrate/006_create_articles.rb" - - "db/migrate/007_create_article_prices.rb" - - "db/migrate/008_create_orders.rb" - - "db/migrate/021_remove_table_article_prices.rb" - - "db/migrate/20090120184410_road_to_version_three.rb" - - "db/migrate/20130622095040_move_weekly_tasks.rb" + - 'config/initializers/resque.rb' + - 'config/initializers/secret_token.rb' + - 'db/migrate/001_create_users.rb' + - 'db/migrate/002_create_groups.rb' + - 'db/migrate/003_create_suppliers.rb' + - 'db/migrate/004_create_article_meta.rb' + - 'db/migrate/005_create_financial_transactions.rb' + - 'db/migrate/006_create_articles.rb' + - 'db/migrate/007_create_article_prices.rb' + - 'db/migrate/008_create_orders.rb' + - 'db/migrate/021_remove_table_article_prices.rb' + - 'db/migrate/20090120184410_road_to_version_three.rb' + - 'db/migrate/20130622095040_move_weekly_tasks.rb' # Offense count: 28 Rails/OutputSafety: Exclude: - - "app/helpers/admin/configs_helper.rb" - - "app/helpers/application_helper.rb" - - "app/helpers/deliveries_helper.rb" - - "app/helpers/orders_helper.rb" - - "app/helpers/tasks_helper.rb" - - "plugins/messages/app/helpers/messages_helper.rb" - - "plugins/wiki/app/helpers/pages_helper.rb" - -# Offense count: 1 -# This cop supports safe autocorrection (--autocorrect). -Rails/Pluck: - Exclude: - - "lib/ordergroups_csv.rb" - -# Offense count: 2 -# This cop supports safe autocorrection (--autocorrect). -Rails/PluralizationGrammar: - Exclude: - - "app/controllers/application_controller.rb" - - "lib/tasks/foodsoft.rake" + - 'app/helpers/admin/configs_helper.rb' + - 'app/helpers/application_helper.rb' + - 'app/helpers/deliveries_helper.rb' + - 'app/helpers/orders_helper.rb' + - 'app/helpers/tasks_helper.rb' + - 'plugins/messages/app/helpers/messages_helper.rb' + - 'plugins/wiki/app/helpers/pages_helper.rb' # Offense count: 1 # This cop supports safe autocorrection (--autocorrect). Rails/Presence: Exclude: - - "db/migrate/021_remove_table_article_prices.rb" - -# Offense count: 36 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: NotNilAndNotEmpty, NotBlank, UnlessBlank. -Rails/Present: - Enabled: false + - 'db/migrate/021_remove_table_article_prices.rb' # Offense count: 6 # This cop supports unsafe autocorrection (--autocorrect-all). @@ -866,30 +723,34 @@ Rails/Present: # Include: **/Rakefile, **/*.rake Rails/RakeEnvironment: Exclude: - - "lib/tasks/foodsoft_setup.rake" - - "lib/tasks/resque.rake" + - 'lib/tasks/foodsoft_setup.rake' + - 'lib/tasks/resque.rake' -# Offense count: 3 -# This cop supports safe autocorrection (--autocorrect). -Rails/RedundantForeignKey: - Exclude: - - "app/models/financial_transaction.rb" - - "plugins/messages/app/models/message.rb" - -# Offense count: 1 +# Offense count: 14 # This cop supports unsafe autocorrection (--autocorrect-all). Rails/RedundantPresenceValidationOnBelongsTo: Exclude: - - "app/models/financial_transaction_type.rb" + - 'app/models/article.rb' + - 'app/models/bank_transaction.rb' + - 'app/models/delivery.rb' + - 'app/models/financial_transaction.rb' + - 'app/models/financial_transaction_type.rb' + - 'app/models/group_order.rb' + - 'app/models/group_order_article.rb' + - 'app/models/group_order_article_quantity.rb' + - 'app/models/invite.rb' + - 'app/models/invoice.rb' + - 'app/models/order_article.rb' + - 'app/models/order_comment.rb' + - 'app/models/stock_change.rb' # Offense count: 1 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: ConvertTry. -Rails/SafeNavigation: +# This cop supports unsafe autocorrection (--autocorrect-all). +Rails/RootPathnameMethods: Exclude: - - "app/models/group_order_article.rb" + - 'lib/tasks/foodsoft_setup.rake' -# Offense count: 63 +# Offense count: 64 # Configuration parameters: ForbiddenMethods, AllowedMethods. # ForbiddenMethods: decrement!, decrement_counter, increment!, increment_counter, insert, insert!, insert_all, insert_all!, toggle!, touch, touch_all, update_all, update_attribute, update_column, update_columns, update_counters, upsert, upsert_all Rails/SkipsModelValidations: @@ -899,11 +760,11 @@ Rails/SkipsModelValidations: # This cop supports unsafe autocorrection (--autocorrect-all). Rails/SquishedSQLHeredocs: Exclude: - - "app/controllers/finance/financial_links_controller.rb" - - "app/models/financial_link.rb" - - "db/migrate/20181201000305_ensure_article_for_article_price.rb" + - 'app/controllers/finance/financial_links_controller.rb' + - 'app/models/financial_link.rb' + - 'db/migrate/20181201000305_ensure_article_for_article_price.rb' -# Offense count: 41 +# Offense count: 42 # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: EnforcedStyle. # SupportedStyles: strict, flexible @@ -913,37 +774,34 @@ Rails/TimeZone: # Offense count: 1 Rails/TransactionExitStatement: Exclude: - - "app/models/bank_transaction.rb" + - 'app/models/bank_transaction.rb' -# Offense count: 3 +# Offense count: 8 # Configuration parameters: Include. # Include: app/models/**/*.rb Rails/UniqueValidationWithoutIndex: Exclude: - - "app/models/bank_account.rb" - - "app/models/supplier_category.rb" + - 'app/models/bank_account.rb' + - 'app/models/financial_transaction_class.rb' + - 'app/models/financial_transaction_type.rb' + - 'app/models/supplier.rb' + - 'app/models/supplier_category.rb' + - 'app/models/user.rb' # Offense count: 2 # Configuration parameters: Environments. # Environments: development, test, production Rails/UnknownEnv: Exclude: - - "config/initializers/gaffe.rb" - - "config/initializers/secret_token.rb" - -# Offense count: 69 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: Include. -# Include: app/models/**/*.rb -Rails/Validation: - Enabled: false + - 'config/initializers/gaffe.rb' + - 'config/initializers/secret_token.rb' # Offense count: 2 # This cop supports unsafe autocorrection (--autocorrect-all). Rails/WhereEquals: Exclude: - - "app/controllers/finance/invoices_controller.rb" - - "app/models/financial_transaction.rb" + - 'app/controllers/finance/invoices_controller.rb' + - 'app/models/financial_transaction.rb' # Offense count: 1 # This cop supports unsafe autocorrection (--autocorrect-all). @@ -951,84 +809,54 @@ Rails/WhereEquals: # SupportedStyles: exists, where Rails/WhereExists: Exclude: - - "app/models/concerns/mark_as_deleted_with_name.rb" + - 'app/models/concerns/mark_as_deleted_with_name.rb' # Offense count: 2 # This cop supports safe autocorrection (--autocorrect). Rails/WhereNot: Exclude: - - "db/migrate/20140921104907_remove_stale_memberships.rb" - - "db/migrate/20210205090257_introduce_received_state_in_orders.rb" + - 'db/migrate/20140921104907_remove_stale_memberships.rb' + - 'db/migrate/20210205090257_introduce_received_state_in_orders.rb' -# Offense count: 5 +# Offense count: 4 # This cop supports unsafe autocorrection (--autocorrect-all). Security/YAMLLoad: Exclude: - - "app/controllers/finance/bank_accounts_controller.rb" - - "db/migrate/20130718183101_migrate_user_settings.rb" - - "db/migrate/20181201000100_create_message_recipients.foodsoft_messages.rb" - - "lib/foodsoft_config.rb" - - "spec/api/v1/swagger_spec.rb" + - 'app/controllers/finance/bank_accounts_controller.rb' + - 'app/lib/foodsoft_config.rb' + - 'db/migrate/20130718183101_migrate_user_settings.rb' + - 'db/migrate/20181201000100_create_message_recipients.foodsoft_messages.rb' # Offense count: 3 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: prefer_alias, prefer_alias_method -Style/Alias: - Exclude: - - "config/initializers/session_store.rb" - - "plugins/discourse/lib/foodsoft_discourse/redirect_to_login.rb" - - "plugins/printer/lib/foodsoft_printer/order_printer_jobs.rb" - -# Offense count: 4 # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: EnforcedStyle. # SupportedStyles: always, conditionals Style/AndOr: Exclude: - - "config/initializers/extensions.rb" - - "lib/apple_bar.rb" - - "plugins/documents/app/controllers/documents_controller.rb" - - "spec/support/coverage.rb" + - 'config/initializers/extensions.rb' + - 'plugins/documents/app/controllers/documents_controller.rb' + - 'spec/support/coverage.rb' -# Offense count: 19 +# Offense count: 10 # This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle, ProceduralMethods, FunctionalMethods, AllowedMethods, AllowedPatterns, IgnoredMethods, AllowBracesOnProceduralOneLiners, BracesRequiredMethods. +# Configuration parameters: EnforcedStyle, ProceduralMethods, FunctionalMethods, AllowedMethods, AllowedPatterns, AllowBracesOnProceduralOneLiners, BracesRequiredMethods. # SupportedStyles: line_count_based, semantic, braces_for_chaining, always_braces # ProceduralMethods: benchmark, bm, bmbm, create, each_with_object, measure, new, realtime, tap, with_object # FunctionalMethods: let, let!, subject, watch # AllowedMethods: lambda, proc, it Style/BlockDelimiters: Exclude: - - "app/controllers/api/v1/user/ordergroup_controller.rb" - - "app/helpers/group_orders_helper.rb" - - "app/helpers/orders_helper.rb" - - "app/models/order.rb" - - "db/migrate/008_create_orders.rb" - - "lib/tasks/resque.rake" - - "spec/api/v1/user/group_order_articles_spec.rb" - - "spec/factories/user.rb" - - "spec/lib/foodsoft_mail_receiver_spec.rb" - - "spec/support/coverage.rb" + - 'app/lib/foodsoft_config.rb' + - 'db/migrate/008_create_orders.rb' + - 'spec/factories/user.rb' + - 'spec/support/coverage.rb' # Offense count: 1 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AllowOnConstant, AllowOnSelfClass. Style/CaseEquality: Exclude: - - "lib/tasks/foodsoft_setup.rake" - -# Offense count: 7 -# This cop supports unsafe autocorrection (--autocorrect-all). -Style/CaseLikeIf: - Exclude: - - "app/helpers/admin/configs_helper.rb" - - "app/helpers/group_orders_helper.rb" - - "app/models/order.rb" - - "lib/foodsoft_date_util.rb" - - "lib/render_pdf.rb" - - "lib/tasks/foodsoft_setup.rake" - - "plugins/uservoice/lib/foodsoft_uservoice.rb" + - 'lib/tasks/foodsoft_setup.rake' # Offense count: 55 # This cop supports unsafe autocorrection (--autocorrect-all). @@ -1038,95 +866,23 @@ Style/ClassAndModuleChildren: Enabled: false # Offense count: 1 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: is_a?, kind_of? -Style/ClassCheck: - Exclude: - - "app/helpers/orders_helper.rb" - -# Offense count: 1 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AllowedMethods, AllowedPatterns, IgnoredMethods. -# AllowedMethods: ==, equal?, eql? -Style/ClassEqualityComparison: - Exclude: - - "spec/factories/supplier.rb" - -# Offense count: 3 Style/ClassVars: Exclude: - - "lib/bank_account_connector.rb" - - "lib/foodsoft/expansion_variables.rb" - - "lib/foodsoft_mail_receiver.rb" - -# Offense count: 3 -# This cop supports safe autocorrection (--autocorrect). -Style/ColonMethodCall: - Exclude: - - "app/models/supplier.rb" - - "plugins/discourse/app/controllers/discourse_controller.rb" - - "plugins/messages/app/mail_receivers/messages_mail_receiver.rb" - -# Offense count: 7 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle, AllowInnerBackticks. -# SupportedStyles: backticks, percent_x, mixed -Style/CommandLiteral: - Exclude: - - "lib/tasks/foodsoft_setup.rake" - -# Offense count: 10 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: Keywords, RequireColon. -# Keywords: TODO, FIXME, OPTIMIZE, HACK, REVIEW, NOTE -Style/CommentAnnotation: - Exclude: - - "app/controllers/admin/configs_controller.rb" - - "app/inputs/delta_input.rb" - - "app/models/order_article.rb" - - "app/models/shared_supplier.rb" - - "config/application.rb" - - "spec/models/article_spec.rb" - - "spec/models/order_spec.rb" - - "spec/support/shared_database.rb" + - 'app/lib/foodsoft_mail_receiver.rb' # Offense count: 12 # This cop supports unsafe autocorrection (--autocorrect-all). Style/CommentedKeyword: Exclude: - - "app/controllers/deliveries_controller.rb" - - "app/controllers/finance/balancing_controller.rb" - - "app/controllers/orders_controller.rb" - - "app/controllers/stock_takings_controller.rb" - - "app/controllers/stockit_controller.rb" - - "config/routes.rb" - - "db/migrate/20090120184410_road_to_version_three.rb" + - 'app/controllers/deliveries_controller.rb' + - 'app/controllers/finance/balancing_controller.rb' + - 'app/controllers/orders_controller.rb' + - 'app/controllers/stock_takings_controller.rb' + - 'app/controllers/stockit_controller.rb' + - 'config/routes.rb' + - 'db/migrate/20090120184410_road_to_version_three.rb' -# Offense count: 13 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle, SingleLineConditionsOnly, IncludeTernaryExpressions. -# SupportedStyles: assign_to_condition, assign_inside_condition -Style/ConditionalAssignment: - Exclude: - - "app/controllers/application_controller.rb" - - "app/controllers/articles_controller.rb" - - "app/controllers/concerns/locale.rb" - - "app/controllers/finance/bank_transactions_controller.rb" - - "app/controllers/finance/financial_transactions_controller.rb" - - "app/controllers/home_controller.rb" - - "app/controllers/orders_controller.rb" - - "plugins/documents/app/controllers/documents_controller.rb" - - "plugins/messages/app/mail_receivers/messages_mail_receiver.rb" - - "plugins/wiki/app/controllers/pages_controller.rb" - -# Offense count: 1 -# This cop supports safe autocorrection (--autocorrect). -Style/DefWithParentheses: - Exclude: - - "app/models/user.rb" - -# Offense count: 322 +# Offense count: 337 # Configuration parameters: AllowedConstants. Style/Documentation: Enabled: false @@ -1137,60 +893,20 @@ Style/Documentation: # SupportedStyles: allowed_in_returns, forbidden Style/DoubleNegation: Exclude: - - "app/controllers/tasks_controller.rb" + - 'app/controllers/tasks_controller.rb' -# Offense count: 5 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle, AllowComments. -# SupportedStyles: empty, nil, both -Style/EmptyElse: - Exclude: - - "app/helpers/application_helper.rb" - - "app/models/article.rb" - - "app/models/order_article.rb" - - "app/models/user.rb" - - "lib/token_verifier.rb" - -# Offense count: 1 -# This cop supports safe autocorrection (--autocorrect). -Style/EmptyLiteral: - Exclude: - - "plugins/wiki/app/helpers/pages_helper.rb" - -# Offense count: 14 +# Offense count: 6 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle. # SupportedStyles: compact, expanded Style/EmptyMethod: Exclude: - - "app/controllers/articles_controller.rb" - - "app/controllers/feedback_controller.rb" - - "app/controllers/finance/invoices_controller.rb" - - "app/controllers/home_controller.rb" - - "app/controllers/login_controller.rb" - - "app/mailers/mailer.rb" - - "db/migrate/024_add_deposit_defaults.rb" - - "db/migrate/20090120184410_road_to_version_three.rb" - - "db/migrate/20090907120012_add_missing_indexes.rb" - - "db/migrate/20130702113610_update_group_order_totals.rb" - - "db/migrate/20130718183101_migrate_user_settings.rb" - - "db/migrate/20140318173000_delete_empty_group_order_articles.rb" - - "lib/bank_account_connector.rb" - -# Offense count: 21 -# This cop supports safe autocorrection (--autocorrect). -Style/ExpandPathArguments: - Enabled: false - -# Offense count: 7 -# This cop supports safe autocorrection (--autocorrect). -Style/ExplicitBlockArgument: - Exclude: - - "app/documents/order_fax.rb" - - "app/helpers/admin/configs_helper.rb" - - "app/models/concerns/find_each_with_order.rb" - - "plugins/current_orders/app/documents/multiple_orders_by_articles.rb" - - "plugins/current_orders/app/documents/multiple_orders_by_groups.rb" + - 'db/migrate/024_add_deposit_defaults.rb' + - 'db/migrate/20090120184410_road_to_version_three.rb' + - 'db/migrate/20090907120012_add_missing_indexes.rb' + - 'db/migrate/20130702113610_update_group_order_totals.rb' + - 'db/migrate/20130718183101_migrate_user_settings.rb' + - 'db/migrate/20140318173000_delete_empty_group_order_articles.rb' # Offense count: 1 # This cop supports unsafe autocorrection (--autocorrect-all). @@ -1198,7 +914,7 @@ Style/ExplicitBlockArgument: # SupportedStyles: left_coerce, right_coerce, single_coerce, fdiv Style/FloatDivision: Exclude: - - "app/models/ordergroup.rb" + - 'app/models/ordergroup.rb' # Offense count: 18 # This cop supports unsafe autocorrection (--autocorrect-all). @@ -1206,33 +922,25 @@ Style/FloatDivision: # SupportedStyles: each, for Style/For: Exclude: - - "app/controllers/admin/configs_controller.rb" - - "app/models/delivery.rb" - - "app/models/group_order.rb" - - "app/models/order.rb" - - "app/models/stock_taking.rb" - - "app/models/supplier.rb" - - "db/migrate/005_create_financial_transactions.rb" - - "lib/tasks/foodsoft.rake" - - "plugins/messages/app/mail_receivers/messages_mail_receiver.rb" - - "plugins/wiki/app/views/pages/all.rss.builder" - -# Offense count: 2 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: format, sprintf, percent -Style/FormatString: - Exclude: - - "lib/order_txt.rb" + - 'app/controllers/admin/configs_controller.rb' + - 'app/models/delivery.rb' + - 'app/models/group_order.rb' + - 'app/models/order.rb' + - 'app/models/stock_taking.rb' + - 'app/models/supplier.rb' + - 'db/migrate/005_create_financial_transactions.rb' + - 'lib/tasks/foodsoft.rake' + - 'plugins/messages/app/mail_receivers/messages_mail_receiver.rb' + - 'plugins/wiki/app/views/pages/all.rss.builder' # Offense count: 6 # This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: MaxUnannotatedPlaceholdersAllowed, AllowedMethods, AllowedPatterns, IgnoredMethods. +# Configuration parameters: MaxUnannotatedPlaceholdersAllowed, AllowedMethods, AllowedPatterns. # SupportedStyles: annotated, template, unannotated Style/FormatStringToken: EnforcedStyle: unannotated -# Offense count: 498 +# Offense count: 511 # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: EnforcedStyle. # SupportedStyles: always, always_true, never @@ -1243,48 +951,38 @@ Style/FrozenStringLiteralComment: # This cop supports unsafe autocorrection (--autocorrect-all). Style/GlobalStdStream: Exclude: - - "config/environments/production.rb" - - "lib/tasks/foodsoft.rake" - - "lib/tasks/foodsoft_setup.rake" + - 'config/environments/production.rb' + - 'lib/tasks/foodsoft.rake' + - 'lib/tasks/foodsoft_setup.rake' -# Offense count: 61 +# Offense count: 2 +# This cop supports safe autocorrection (--autocorrect). # Configuration parameters: MinBodyLength, AllowConsecutiveConditionals. Style/GuardClause: - Enabled: false - -# Offense count: 1 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: braces, no_braces -Style/HashAsLastArrayItem: Exclude: - - "app/models/order.rb" + - 'db/migrate/20230106144438_add_service_name_to_active_storage_blobs.active_storage.rb' + - 'plugins/wiki/app/controllers/pages_controller.rb' -# Offense count: 5 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AllowSplatArgument. -Style/HashConversion: +# Offense count: 2 +# This cop supports unsafe autocorrection (--autocorrect-all). +Style/HashExcept: Exclude: - - "app/helpers/application_helper.rb" - - "app/models/article.rb" - - "app/models/order.rb" - - "plugins/wiki/app/controllers/pages_controller.rb" - - "spec/api/v1/user/ordergroup_spec.rb" + - 'spec/models/article_spec.rb' # Offense count: 8 # Configuration parameters: MinBranchesCount. Style/HashLikeCase: Exclude: - - "app/controllers/articles_controller.rb" - - "app/controllers/finance/bank_transactions_controller.rb" - - "app/controllers/finance/financial_transactions_controller.rb" - - "app/controllers/home_controller.rb" - - "app/controllers/orders_controller.rb" - - "app/helpers/finance/balancing_helper.rb" - - "plugins/documents/app/controllers/documents_controller.rb" - - "plugins/wiki/app/controllers/pages_controller.rb" + - 'app/controllers/articles_controller.rb' + - 'app/controllers/finance/bank_transactions_controller.rb' + - 'app/controllers/finance/financial_transactions_controller.rb' + - 'app/controllers/home_controller.rb' + - 'app/controllers/orders_controller.rb' + - 'app/helpers/finance/balancing_helper.rb' + - 'plugins/documents/app/controllers/documents_controller.rb' + - 'plugins/wiki/app/controllers/pages_controller.rb' -# Offense count: 3904 +# Offense count: 375 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle, EnforcedShorthandSyntax, UseHashRocketsWithSymbolValues, PreferHashRocketsForNonAlnumEndingSymbols. # SupportedStyles: ruby19, hash_rockets, no_mixed_keys, ruby19_no_mixed_keys @@ -1292,122 +990,64 @@ Style/HashLikeCase: Style/HashSyntax: Enabled: false -# Offense count: 5 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AllowIfModifier. -Style/IfInsideElse: - Exclude: - - "app/models/article.rb" - - "app/models/task.rb" - - "lib/apple_bar.rb" - - "plugins/wiki/app/controllers/pages_controller.rb" - -# Offense count: 61 +# Offense count: 1 # This cop supports safe autocorrection (--autocorrect). Style/IfUnlessModifier: - Enabled: false + Exclude: + - 'db/migrate/20090120184410_road_to_version_three.rb' -# Offense count: 2 +# Offense count: 1 # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: AllowedMethods. # AllowedMethods: nonzero? Style/IfWithBooleanLiteralBranches: Exclude: - - "app/models/order_article.rb" - - "app/models/task.rb" - -# Offense count: 1 -# This cop supports unsafe autocorrection (--autocorrect-all). -Style/InfiniteLoop: - Exclude: - - "lib/order_pdf.rb" + - 'app/models/order_article.rb' # Offense count: 3 # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: InverseMethods, InverseBlocks. Style/InverseMethods: Exclude: - - "app/helpers/application_helper.rb" - - "app/helpers/deliveries_helper.rb" - - "spec/support/coverage.rb" - -# Offense count: 4 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: line_count_dependent, lambda, literal -Style/Lambda: - Exclude: - - "app/models/financial_link.rb" - - "lib/foodsoft_mail_receiver.rb" - - "plugins/messages/app/models/message.rb" + - 'app/helpers/application_helper.rb' + - 'app/helpers/deliveries_helper.rb' + - 'spec/support/coverage.rb' # Offense count: 5 # This cop supports unsafe autocorrection (--autocorrect-all). Style/LineEndConcatenation: Exclude: - - "db/migrate/20130702113610_update_group_order_totals.rb" - - "plugins/current_orders/app/documents/multiple_orders_by_articles.rb" + - 'db/migrate/20130702113610_update_group_order_totals.rb' + - 'plugins/current_orders/app/documents/multiple_orders_by_articles.rb' # Offense count: 1 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AllowedMethods, AllowedPatterns, IgnoredMethods. -Style/MethodCallWithoutArgsParentheses: +# This cop supports unsafe autocorrection (--autocorrect-all). +Style/MapToHash: Exclude: - - "plugins/discourse/app/controllers/discourse_login_controller.rb" - -# Offense count: 5 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: require_parentheses, require_no_parentheses, require_no_parentheses_except_multiline -Style/MethodDefParentheses: - Exclude: - - "app/controllers/concerns/send_order_pdf.rb" - - "app/helpers/application_helper.rb" - - "app/helpers/finance/invoices_helper.rb" - - "plugins/discourse/app/controllers/discourse_controller.rb" + - 'app/models/article.rb' # Offense count: 1 Style/MixinUsage: Exclude: - - "lib/tasks/foodsoft_setup.rake" - -# Offense count: 3 -Style/MultilineBlockChain: - Exclude: - - "app/helpers/group_orders_helper.rb" - - "app/models/order.rb" - - "config/initializers/rails6_backports.rb" + - 'lib/tasks/foodsoft_setup.rake' # Offense count: 2 +Style/MultilineBlockChain: + Exclude: + - 'app/helpers/group_orders_helper.rb' + - 'app/models/order.rb' + +# Offense count: 7 # This cop supports safe autocorrection (--autocorrect). Style/MultilineIfModifier: Exclude: - - "app/models/user.rb" - - "plugins/current_orders/app/controllers/current_orders/ordergroups_controller.rb" - -# Offense count: 1 -# This cop supports safe autocorrection (--autocorrect). -Style/MultilineIfThen: - Exclude: - - "app/controllers/finance/financial_links_controller.rb" - -# Offense count: 12 -# This cop supports safe autocorrection (--autocorrect). -Style/MultilineWhenThen: - Exclude: - - "app/controllers/finance/balancing_controller.rb" - - "app/helpers/application_helper.rb" - - "app/helpers/finance/balancing_helper.rb" - - "app/models/order.rb" - -# Offense count: 5 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AllowMethodComparison. -Style/MultipleComparison: - Exclude: - - "app/models/order.rb" - - "app/models/order_article.rb" - - "spec/models/article_spec.rb" + - 'app/controllers/admin/ordergroups_controller.rb' + - 'app/controllers/articles_controller.rb' + - 'app/controllers/orders_controller.rb' + - 'app/documents/order_fax.rb' + - 'app/lib/foodsoft_config.rb' + - 'app/models/ordergroup.rb' + - 'config/initializers/currency_display.rb' # Offense count: 24 # This cop supports unsafe autocorrection (--autocorrect-all). @@ -1416,75 +1056,23 @@ Style/MultipleComparison: Style/MutableConstant: Enabled: false -# Offense count: 2 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: both, prefix, postfix -Style/NegatedIf: - Exclude: - - "app/controllers/orders_controller.rb" - - "app/helpers/articles_helper.rb" - -# Offense count: 4 -# This cop supports safe autocorrection (--autocorrect). -Style/NegatedIfElseCondition: - Exclude: - - "app/controllers/articles_controller.rb" - - "app/controllers/concerns/auth.rb" - - "app/models/article.rb" - -# Offense count: 8 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AllowedMethods. -# AllowedMethods: be, be_a, be_an, be_between, be_falsey, be_kind_of, be_instance_of, be_truthy, be_within, eq, eql, end_with, include, match, raise_error, respond_to, start_with -Style/NestedParenthesizedCalls: - Exclude: - - "app/models/user.rb" - - "spec/models/order_article_spec.rb" - -# Offense count: 7 +# Offense count: 1 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle, MinBodyLength. # SupportedStyles: skip_modifier_ifs, always Style/Next: Exclude: - - "app/controllers/finance/financial_transactions_controller.rb" - - "app/controllers/orders_controller.rb" - - "app/helpers/orders_helper.rb" - - "db/migrate/20130622095040_move_weekly_tasks.rb" - - "lib/tasks/foodsoft.rake" + - 'db/migrate/20130622095040_move_weekly_tasks.rb' # Offense count: 2 # This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: predicate, comparison -Style/NilComparison: - Exclude: - - "app/controllers/application_controller.rb" - - "plugins/wiki/app/helpers/pages_helper.rb" - -# Offense count: 10 -# This cop supports safe autocorrection (--autocorrect). -Style/Not: - Exclude: - - "app/controllers/concerns/auth.rb" - - "app/controllers/orders_controller.rb" - - "app/helpers/deliveries_helper.rb" - - "app/models/group_order_article.rb" - - "app/models/order_article.rb" - - "app/models/supplier.rb" - - "app/models/task.rb" - - "spec/support/coverage.rb" - -# Offense count: 6 -# This cop supports safe autocorrection (--autocorrect). # Configuration parameters: Strict, AllowedNumbers, AllowedPatterns. Style/NumericLiterals: - MinDigits: 7 + MinDigits: 6 -# Offense count: 61 +# Offense count: 60 # This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: EnforcedStyle, AllowedMethods, AllowedPatterns, IgnoredMethods. +# Configuration parameters: EnforcedStyle, AllowedMethods, AllowedPatterns. # SupportedStyles: predicate, comparison Style/NumericPredicate: Enabled: false @@ -1494,51 +1082,10 @@ Style/NumericPredicate: # AllowedMethods: respond_to_missing? Style/OptionalBooleanParameter: Exclude: - - "app/helpers/application_helper.rb" - - "app/helpers/orders_helper.rb" - - "app/models/order_article.rb" - - "lib/tasks/foodsoft_setup.rake" - -# Offense count: 1 -# This cop supports safe autocorrection (--autocorrect). -Style/OrAssignment: - Exclude: - - "app/controllers/articles_controller.rb" - -# Offense count: 8 -# This cop supports safe autocorrection (--autocorrect). -Style/ParallelAssignment: - Exclude: - - "app/models/article.rb" - - "app/models/group_order_article.rb" - - "app/models/supplier.rb" - - "app/models/user.rb" - - "spec/models/group_order_article_spec.rb" - - "spec/support/session_helper.rb" - -# Offense count: 12 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AllowSafeAssignment, AllowInMultilineConditions. -Style/ParenthesesAroundCondition: - Exclude: - - "app/controllers/login_controller.rb" - - "app/helpers/application_helper.rb" - - "app/helpers/group_orders_helper.rb" - - "app/models/group_order_article.rb" - - "plugins/wiki/app/controllers/pages_controller.rb" - -# Offense count: 41 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: PreferredDelimiters. -Style/PercentLiteralDelimiters: - Enabled: false - -# Offense count: 5 -# This cop supports safe autocorrection (--autocorrect). -Style/PerlBackrefs: - Exclude: - - "lib/foodsoft/expansion_variables.rb" - - "plugins/wiki/app/helpers/pages_helper.rb" + - 'app/helpers/application_helper.rb' + - 'app/helpers/orders_helper.rb' + - 'app/models/order_article.rb' + - 'lib/tasks/foodsoft_setup.rake' # Offense count: 2 # This cop supports unsafe autocorrection (--autocorrect-all). @@ -1546,207 +1093,77 @@ Style/PerlBackrefs: # SupportedStyles: short, verbose Style/PreferredHashMethods: Exclude: - - "app/helpers/admin/configs_helper.rb" - - "app/helpers/articles_helper.rb" + - 'app/helpers/admin/configs_helper.rb' + - 'app/helpers/articles_helper.rb' -# Offense count: 14 -# This cop supports safe autocorrection (--autocorrect). -Style/Proc: - Exclude: - - "app/helpers/deliveries_helper.rb" - - "app/models/user.rb" - - "config/navigation.rb" - - "plugins/current_orders/lib/foodsoft_current_orders/engine.rb" - - "plugins/links/lib/foodsoft_links/engine.rb" - -# Offense count: 6 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle, AllowedCompactTypes. -# SupportedStyles: compact, exploded -Style/RaiseArgs: - Exclude: - - "app/controllers/api/v1/base_controller.rb" - - "app/controllers/concerns/auth_api.rb" - - "app/controllers/concerns/foodcoop_scope.rb" - -# Offense count: 5 +# Offense count: 4 # This cop supports safe autocorrection (--autocorrect). Style/RandomWithOffset: Exclude: - - "db/migrate/007_create_article_prices.rb" - - "db/migrate/008_create_orders.rb" - - "db/seeds/seed_helper.rb" + - 'db/migrate/007_create_article_prices.rb' + - 'db/migrate/008_create_orders.rb' # Offense count: 1 # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: Methods. Style/RedundantArgument: Exclude: - - "app/controllers/articles_controller.rb" - -# Offense count: 8 -# This cop supports safe autocorrection (--autocorrect). -Style/RedundantBegin: - Exclude: - - "app/controllers/articles_controller.rb" - - "app/models/order.rb" - - "lib/foodsoft_mail_receiver.rb" - - "lib/tasks/multicoops.rake" - - "spec/lib/foodsoft_mail_receiver_spec.rb" - -# Offense count: 1 -# This cop supports safe autocorrection (--autocorrect). -Style/RedundantConditional: - Exclude: - - "app/models/task.rb" + - 'app/controllers/articles_controller.rb' # Offense count: 3 # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: SafeForConstants. Style/RedundantFetchBlock: Exclude: - - "config/puma.rb" + - 'config/puma.rb' -# Offense count: 2 -# This cop supports safe autocorrection (--autocorrect). -Style/RedundantFileExtensionInRequire: - Exclude: - - "db/seeds/small.en.seeds.rb" - - "db/seeds/small.nl.seeds.rb" - -# Offense count: 5 +# Offense count: 4 # This cop supports unsafe autocorrection (--autocorrect-all). Style/RedundantInterpolation: Exclude: - - "db/migrate/20130718183101_migrate_user_settings.rb" - - "lib/order_pdf.rb" - - "spec/i18n_spec.rb" - - "spec/models/user_spec.rb" + - 'db/migrate/20130718183101_migrate_user_settings.rb' + - 'spec/i18n_spec.rb' + - 'spec/models/user_spec.rb' # Offense count: 1 # This cop supports safe autocorrection (--autocorrect). -Style/RedundantRegexpCharacterClass: +Style/RedundantParentheses: Exclude: - - "plugins/wiki/app/helpers/pages_helper.rb" - -# Offense count: 7 -# This cop supports safe autocorrection (--autocorrect). -Style/RedundantRegexpEscape: - Exclude: - - "lib/bank_transaction_reference.rb" - - "lib/foodsoft_mail_receiver.rb" - - "plugins/documents/app/controllers/documents_controller.rb" - - "plugins/wiki/app/models/page.rb" - -# Offense count: 15 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AllowMultipleReturnValues. -Style/RedundantReturn: - Exclude: - - "app/controllers/concerns/auth_api.rb" - - "app/helpers/application_helper.rb" - - "app/helpers/deliveries_helper.rb" - - "app/helpers/group_orders_helper.rb" - - "app/helpers/orders_helper.rb" - - "app/models/article.rb" - - "app/models/bank_transaction.rb" - - "app/models/periodic_task_group.rb" - - "app/models/supplier.rb" - - "lib/bank_transaction_reference.rb" - -# Offense count: 83 -# This cop supports safe autocorrection (--autocorrect). -Style/RedundantSelf: - Enabled: false + - 'db/migrate/021_remove_table_article_prices.rb' # Offense count: 1 # This cop supports unsafe autocorrection (--autocorrect-all). Style/RedundantSort: Exclude: - - "app/models/article_category.rb" + - 'app/models/article_category.rb' -# Offense count: 3 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle, AllowInnerSlashes. -# SupportedStyles: slashes, percent_r, mixed -Style/RegexpLiteral: - Exclude: - - "plugins/wiki/app/models/page.rb" - - "spec/support/coverage.rb" - -# Offense count: 16 -# This cop supports safe autocorrection (--autocorrect). -Style/RescueModifier: - Exclude: - - "app/controllers/invites_controller.rb" - - "app/models/article.rb" - - "app/models/order.rb" - - "app/models/ordergroup.rb" - - "config/application.rb" - - "lib/apple_bar.rb" - - "lib/date_time_attribute_validate.rb" - - "lib/foodsoft_date_util.rb" - - "plugins/messages/app/models/message.rb" - -# Offense count: 51 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: implicit, explicit -Style/RescueStandardError: - Enabled: false - -# Offense count: 9 +# Offense count: 8 # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: ConvertCodeThatCanStartToReturnNil, AllowedMethods, MaxChainLength. # AllowedMethods: present?, blank?, presence, try, try! Style/SafeNavigation: Exclude: - - "app/controllers/concerns/auth_api.rb" - - "app/controllers/group_order_articles_controller.rb" - - "app/models/article_category.rb" - - "app/models/financial_transaction.rb" - - "app/models/ordergroup.rb" - - "app/models/user.rb" - - "plugins/printer/app/controllers/printer_controller.rb" - - "spec/factories/order.rb" + - 'app/controllers/concerns/auth_api.rb' + - 'app/controllers/group_order_articles_controller.rb' + - 'app/models/article_category.rb' + - 'app/models/financial_transaction.rb' + - 'app/models/user.rb' + - 'plugins/printer/app/controllers/printer_controller.rb' + - 'spec/factories/order.rb' -# Offense count: 1 -# This cop supports safe autocorrection (--autocorrect). -Style/SelfAssignment: - Exclude: - - "app/helpers/application_helper.rb" - -# Offense count: 16 +# Offense count: 3 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AllowAsExpressionSeparator. Style/Semicolon: Exclude: - - "app/controllers/finance/bank_transactions_controller.rb" - - "app/controllers/finance/financial_transactions_controller.rb" - - "app/controllers/finance/invoices_controller.rb" - - "app/controllers/orders_controller.rb" - - "app/helpers/group_orders_helper.rb" - - "db/migrate/20090120184410_road_to_version_three.rb" - - "spec/api/v1/swagger_spec.rb" - - "spec/api/v1/user/group_order_articles_spec.rb" - - "spec/api/v1/user/ordergroup_spec.rb" - - "spec/models/order_article_spec.rb" + - 'db/migrate/20090120184410_road_to_version_three.rb' -# Offense count: 5 +# Offense count: 4 # This cop supports unsafe autocorrection (--autocorrect-all). Style/SlicingWithRange: Exclude: - - "app/helpers/admin/configs_helper.rb" - - "config/initializers/session_store.rb" - - "lib/order_pdf.rb" - -# Offense count: 2 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AllowModifier. -Style/SoleNestedConditional: - Exclude: - - "app/controllers/articles_controller.rb" - - "app/controllers/concerns/auth.rb" + - 'app/helpers/admin/configs_helper.rb' + - 'config/initializers/session_store.rb' # Offense count: 9 # This cop supports unsafe autocorrection (--autocorrect-all). @@ -1755,125 +1172,65 @@ Style/SoleNestedConditional: Style/SpecialGlobalVars: EnforcedStyle: use_perl_names -# Offense count: 33 +# Offense count: 34 # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: Mode. Style/StringConcatenation: Enabled: false -# Offense count: 1855 +# Offense count: 140 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle, ConsistentQuotesInMultiline. # SupportedStyles: single_quotes, double_quotes Style/StringLiterals: Enabled: false -# Offense count: 80 +# Offense count: 19 # This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: MinSize. +# Configuration parameters: . # SupportedStyles: percent, brackets Style/SymbolArray: - EnforcedStyle: brackets + EnforcedStyle: percent + MinSize: 5 -# Offense count: 19 +# Offense count: 20 # This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: AllowMethodsWithArguments, AllowedMethods, AllowedPatterns, IgnoredMethods, AllowComments. -# AllowedMethods: respond_to, define_method +# Configuration parameters: AllowMethodsWithArguments, AllowedMethods, AllowedPatterns, AllowComments. +# AllowedMethods: define_method, mail, respond_to Style/SymbolProc: Exclude: - - "app/controllers/pickups_controller.rb" - - "app/helpers/orders_helper.rb" - - "app/models/delivery.rb" - - "app/models/financial_transaction_class.rb" - - "app/models/financial_transaction_type.rb" - - "app/models/group_order_article.rb" - - "app/models/order.rb" - - "app/models/order_article.rb" - - "app/models/stock_article.rb" - - "app/models/user.rb" - - "db/migrate/20090731132547_add_stats_to_groups.rb" - - "spec/factories/order.rb" - -# Offense count: 4 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle, AllowSafeAssignment. -# SupportedStyles: require_parentheses, require_no_parentheses, require_parentheses_when_complex -Style/TernaryParentheses: - Exclude: - - "app/models/order_article.rb" - -# Offense count: 5 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyleForMultiline. -# SupportedStylesForMultiline: comma, consistent_comma, no_comma -Style/TrailingCommaInArrayLiteral: - Exclude: - - "lib/articles_csv.rb" - - "lib/invoices_csv.rb" - - "lib/ordergroups_csv.rb" - -# Offense count: 2 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyleForMultiline. -# SupportedStylesForMultiline: comma, consistent_comma, no_comma -Style/TrailingCommaInHashLiteral: - Exclude: - - "app/controllers/finance/financial_transactions_controller.rb" - - "config/initializers/exception_notification.rb" - -# Offense count: 8 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: ExactNameMatch, AllowPredicates, AllowDSLWriters, IgnoreClassMethods, AllowedMethods. -# AllowedMethods: to_ary, to_a, to_c, to_enum, to_h, to_hash, to_i, to_int, to_io, to_open, to_path, to_proc, to_r, to_regexp, to_str, to_s, to_sym -Style/TrivialAccessors: - Exclude: - - "app/models/order.rb" - - "lib/bank_account_connector.rb" - - "plugins/messages/app/models/message.rb" - -# Offense count: 5 -# This cop supports safe autocorrection (--autocorrect). -Style/UnlessElse: - Exclude: - - "app/controllers/home_controller.rb" - - "app/controllers/orders_controller.rb" - - "app/helpers/group_order_articles_helper.rb" - - "plugins/current_orders/app/controllers/current_orders/articles_controller.rb" - - "plugins/wiki/app/helpers/pages_helper.rb" + - 'app/controllers/pickups_controller.rb' + - 'app/helpers/orders_helper.rb' + - 'app/models/delivery.rb' + - 'app/models/financial_transaction_class.rb' + - 'app/models/financial_transaction_type.rb' + - 'app/models/group_order_article.rb' + - 'app/models/order.rb' + - 'app/models/order_article.rb' + - 'app/models/stock_article.rb' + - 'app/models/user.rb' + - 'db/migrate/20090731132547_add_stats_to_groups.rb' + - 'spec/factories/order.rb' # Offense count: 1 # This cop supports safe autocorrection (--autocorrect). -Style/WhileUntilModifier: - Exclude: - - "app/models/periodic_task_group.rb" - -# Offense count: 11 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle, MinSize, WordRegex. +# Configuration parameters: WordRegex. # SupportedStyles: percent, brackets Style/WordArray: - Exclude: - - "app/documents/order_matrix.rb" - - "app/helpers/application_helper.rb" - - "app/models/supplier.rb" - - "db/migrate/006_create_articles.rb" - - "lib/tasks/foodsoft_setup.rake" - - "plugins/current_orders/app/controllers/current_orders/group_orders_controller.rb" - - "plugins/wiki/app/controllers/pages_controller.rb" - - "plugins/wiki/app/helpers/pages_helper.rb" - - "spec/support/faker.rb" + EnforcedStyle: percent + MinSize: 4 # Offense count: 3 # This cop supports unsafe autocorrection (--autocorrect-all). Style/ZeroLengthPredicate: Exclude: - - "app/models/group_order_article.rb" - - "plugins/current_orders/app/documents/multiple_orders_by_articles.rb" - - "plugins/current_orders/app/documents/multiple_orders_by_groups.rb" + - 'app/models/group_order_article.rb' + - 'plugins/current_orders/app/documents/multiple_orders_by_articles.rb' + - 'plugins/current_orders/app/documents/multiple_orders_by_groups.rb' -# Offense count: 446 +# Offense count: 282 # This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, AllowedPatterns, IgnoredPatterns. +# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, AllowedPatterns. # URISchemes: http, https Layout/LineLength: - Max: 420 + Max: 320 diff --git a/Gemfile b/Gemfile index 70baa906..2cda86f3 100644 --- a/Gemfile +++ b/Gemfile @@ -1,75 +1,74 @@ # A sample Gemfile -source "https://rubygems.org" +source 'https://rubygems.org' -gem "rails", '~> 7.0' gem 'mail', '~> 2.7.1' # bug with mail 2.8.0 https://github.com/mikel/mail/issues/1489 +gem 'rails', '~> 7.0' - -gem 'sassc-rails' gem 'less-rails' +gem 'sassc-rails' gem 'uglifier' # See https://github.com/sstephenson/execjs#readme for more supported runtimes gem 'therubyracer', platforms: :ruby -gem 'jquery-rails' -gem 'select2-rails' -gem 'rails_tokeninput' +gem 'bootsnap', require: false gem 'bootstrap-datepicker-rails' gem 'date_time_attribute' -gem 'rails-assets-listjs', '0.2.0.beta.4' # remember to maintain list.*.js plugins and template engines on update gem 'i18n-js', '~> 3.0.0.rc8' +gem 'jquery-rails' +gem 'rails-assets-listjs', '0.2.0.beta.4' # remember to maintain list.*.js plugins and template engines on update gem 'rails-i18n' -gem 'bootsnap', require: false +gem 'rails_tokeninput' +gem 'select2-rails' -gem 'mysql2' -gem 'prawn' -gem 'prawn-table' -gem 'haml' -gem 'haml-rails' -gem 'kaminari' -gem 'simple_form' -gem 'inherited_resources' +gem 'active_model_serializers', '~> 0.10.0' +gem 'acts_as_tree' +gem 'attribute_normalizer' gem 'daemons' gem 'doorkeeper' gem 'doorkeeper-i18n' +gem 'haml' +gem 'haml-rails' +gem 'ice_cube' +gem 'inherited_resources' +gem 'kaminari' +gem 'mysql2' +gem 'prawn' +gem 'prawn-table' +gem 'puma' gem 'rack-cors', require: 'rack/cors' -gem 'active_model_serializers', '~> 0.10.0' -gem 'twitter-bootstrap-rails', '~> 2.2.8' +gem 'rails-settings-cached', '= 0.4.3' # caching breaks tests until Rails 5 https://github.com/huacnlee/rails-settings-cached/issues/73 +gem 'ransack' +gem 'resque' +gem 'ruby-units' +gem 'sd_notify' +gem 'simple_form' gem 'simple-navigation', '~> 3.14.0' # 3.x for simple_navigation_bootstrap gem 'simple-navigation-bootstrap' gem 'sprockets', '< 4' -gem 'ransack' -gem 'acts_as_tree' -gem 'rails-settings-cached', '= 0.4.3' # caching breaks tests until Rails 5 https://github.com/huacnlee/rails-settings-cached/issues/73 -gem 'resque' -gem 'puma' -gem 'sd_notify' +gem 'twitter-bootstrap-rails', '~> 2.2.8' gem 'whenever', require: false # For defining cronjobs, see config/schedule.rb -gem 'ruby-units' -gem 'attribute_normalizer' -gem 'ice_cube' # At time of development 01-06-2022 mmddyyyy necessary fix for config_helper.rb form builder was not in rubygems so we pull from github, see: https://github.com/gregschmit/recurring_select/pull/152 +gem 'exception_notification' +gem 'gaffe' +gem 'hashie', '~> 3.4.6', require: false # https://github.com/westfieldlabs/apivore/issues/114 +gem 'midi-smtp-server' +gem 'mime-types' gem 'recurring_select', git: 'https://github.com/gregschmit/recurring_select' gem 'roo' gem 'roo-xls' -gem 'spreadsheet' -gem 'exception_notification' -gem 'gaffe' -gem 'ruby-filemagic' -gem 'mime-types' -gem 'midi-smtp-server' -gem 'hashie', '~> 3.4.6', require: false # https://github.com/westfieldlabs/apivore/issues/114 gem 'rswag-api' gem 'rswag-ui' +gem 'ruby-filemagic' +gem 'spreadsheet' # we use the git version of acts_as_versioned, and need to include it in this Gemfile gem 'acts_as_versioned', git: 'https://github.com/technoweenie/acts_as_versioned.git' -gem 'foodsoft_wiki', path: 'plugins/wiki' -gem 'foodsoft_messages', path: 'plugins/messages' -gem 'foodsoft_documents', path: 'plugins/documents' gem 'foodsoft_discourse', path: 'plugins/discourse' +gem 'foodsoft_documents', path: 'plugins/documents' gem 'foodsoft_links', path: 'plugins/links' +gem 'foodsoft_messages', path: 'plugins/messages' gem 'foodsoft_polls', path: 'plugins/polls' +gem 'foodsoft_wiki', path: 'plugins/wiki' # plugins not enabled by default # gem 'foodsoft_current_orders', path: 'plugins/current_orders' @@ -77,10 +76,10 @@ gem 'foodsoft_polls', path: 'plugins/polls' # gem 'foodsoft_uservoice', path: 'plugins/uservoice' group :development do - gem 'sqlite3', '~> 1.3.6' - gem 'mailcatcher' - gem 'web-console' gem 'listen' + gem 'mailcatcher' + gem 'sqlite3', '~> 1.3.6' + gem 'web-console' # Better error output gem 'better_errors' @@ -108,17 +107,17 @@ group :development, :test do end group :test do - gem 'rspec-rails' + gem 'apparition' # Capybara javascript driver + gem 'capybara' + gem 'connection_pool' + gem 'database_cleaner' gem 'factory_bot_rails' gem 'faker' - gem 'capybara' - gem 'apparition' # Capybara javascript driver - gem 'database_cleaner' - gem 'connection_pool' + gem 'rspec-rails' # need to include rspec components before i18n-spec or rake fails in test environment + gem 'i18n-spec' gem 'rspec-core' gem 'rspec-rerun' - gem 'i18n-spec' # code coverage gem 'simplecov', require: false gem 'simplecov-lcov', require: false diff --git a/Rakefile b/Rakefile index 835180b2..e59b186f 100755 --- a/Rakefile +++ b/Rakefile @@ -1,7 +1,7 @@ #!/usr/bin/env rake # Add your own tasks in files placed in lib/tasks ending in .rake, -require File.expand_path('../config/application', __FILE__) +require File.expand_path('config/application', __dir__) require 'rake' require 'rspec-rerun/tasks' if defined?(RSpec) # http://stackoverflow.com/a/16853615/2866660 diff --git a/app/controllers/admin/bank_accounts_controller.rb b/app/controllers/admin/bank_accounts_controller.rb index e23b03b2..d37f57e8 100644 --- a/app/controllers/admin/bank_accounts_controller.rb +++ b/app/controllers/admin/bank_accounts_controller.rb @@ -3,39 +3,39 @@ class Admin::BankAccountsController < Admin::BaseController def new @bank_account = BankAccount.new(params[:bank_account]) - render :layout => false + render layout: false + end + + def edit + @bank_account = BankAccount.find(params[:id]) + render action: 'new', layout: false end def create @bank_account = BankAccount.new(params[:bank_account]) if @bank_account.valid? && @bank_account.save - redirect_to update_bank_accounts_admin_finances_url, :status => 303 + redirect_to update_bank_accounts_admin_finances_url, status: :see_other else - render :action => 'new', :layout => false + render action: 'new', layout: false end end - def edit - @bank_account = BankAccount.find(params[:id]) - render :action => 'new', :layout => false - end - def update @bank_account = BankAccount.find(params[:id]) if @bank_account.update(params[:bank_account]) - redirect_to update_bank_accounts_admin_finances_url, :status => 303 + redirect_to update_bank_accounts_admin_finances_url, status: :see_other else - render :action => 'new', :layout => false + render action: 'new', layout: false end end def destroy @bank_account = BankAccount.find(params[:id]) @bank_account.destroy - redirect_to update_bank_accounts_admin_finances_url, :status => 303 - rescue => error - flash.now[:alert] = error.message + redirect_to update_bank_accounts_admin_finances_url, status: :see_other + rescue StandardError => e + flash.now[:alert] = e.message render template: 'shared/alert' end end diff --git a/app/controllers/admin/bank_gateways_controller.rb b/app/controllers/admin/bank_gateways_controller.rb index 3965c91b..c7ca5516 100644 --- a/app/controllers/admin/bank_gateways_controller.rb +++ b/app/controllers/admin/bank_gateways_controller.rb @@ -6,6 +6,11 @@ class Admin::BankGatewaysController < Admin::BaseController render layout: false end + def edit + @bank_gateway = BankGateway.find(params[:id]) + render action: 'new', layout: false + end + def create @bank_gateway = BankGateway.new(params[:bank_gateway]) if @bank_gateway.valid? && @bank_gateway.save @@ -15,11 +20,6 @@ class Admin::BankGatewaysController < Admin::BaseController end end - def edit - @bank_gateway = BankGateway.find(params[:id]) - render action: 'new', layout: false - end - def update @bank_gateway = BankGateway.find(params[:id]) diff --git a/app/controllers/admin/configs_controller.rb b/app/controllers/admin/configs_controller.rb index 516113af..500c1b87 100644 --- a/app/controllers/admin/configs_controller.rb +++ b/app/controllers/admin/configs_controller.rb @@ -1,5 +1,5 @@ class Admin::ConfigsController < Admin::BaseController - before_action :get_tabs, only: [:show, :list] + before_action :get_tabs, only: %i[show list] def show @current_tab = @tabs.include?(params[:tab]) ? params[:tab] : @tabs.first @@ -16,7 +16,7 @@ class Admin::ConfigsController < Admin::BaseController def update parse_recurring_selects! params[:config][:order_schedule] ActiveRecord::Base.transaction do - # TODO support nested configuration keys + # TODO: support nested configuration keys params[:config].each do |key, val| FoodsoftConfig[key] = convert_config_value val end @@ -29,7 +29,7 @@ class Admin::ConfigsController < Admin::BaseController # Set configuration tab names as `@tabs` def get_tabs - @tabs = %w(foodcoop payment tasks messages layout language security others) + @tabs = %w[foodcoop payment tasks messages layout language security others] # allow engines to modify this list engines = Rails::Engine.subclasses.map(&:instance).select { |e| e.respond_to?(:configuration) } engines.each { |e| e.configuration(@tabs, self) } @@ -38,16 +38,16 @@ class Admin::ConfigsController < Admin::BaseController # turn recurring rules into something palatable def parse_recurring_selects!(config) - if config - for k in [:pickup, :boxfill, :ends] do - if config[k] - # allow clearing it using dummy value '{}' ('' would break recurring_select) - if config[k][:recurr].present? && config[k][:recurr] != '{}' - config[k][:recurr] = ActiveSupport::JSON.decode(config[k][:recurr]) - config[k][:recurr] = FoodsoftDateUtil.rule_from(config[k][:recurr]).to_ical if config[k][:recurr] - else - config[k] = nil - end + return unless config + + for k in %i[pickup boxfill ends] do + if config[k] + # allow clearing it using dummy value '{}' ('' would break recurring_select) + if config[k][:recurr].present? && config[k][:recurr] != '{}' + config[k][:recurr] = ActiveSupport::JSON.decode(config[k][:recurr]) + config[k][:recurr] = FoodsoftDateUtil.rule_from(config[k][:recurr]).to_ical if config[k][:recurr] + else + config[k] = nil end end end diff --git a/app/controllers/admin/finances_controller.rb b/app/controllers/admin/finances_controller.rb index 5aae587b..75bb7456 100644 --- a/app/controllers/admin/finances_controller.rb +++ b/app/controllers/admin/finances_controller.rb @@ -10,21 +10,21 @@ class Admin::FinancesController < Admin::BaseController def update_bank_accounts @bank_accounts = BankAccount.order('name') - render :layout => false + render layout: false end def update_bank_gateways @bank_gateways = BankGateway.order('name') - render :layout => false + render layout: false end def update_transaction_types @financial_transaction_classes = FinancialTransactionClass.includes(:financial_transaction_types).order('name ASC') - render :layout => false + render layout: false end def update_supplier_categories @supplier_categories = SupplierCategory.order('name') - render :layout => false + render layout: false end end diff --git a/app/controllers/admin/financial_transaction_classes_controller.rb b/app/controllers/admin/financial_transaction_classes_controller.rb index e5d27efd..132e9038 100644 --- a/app/controllers/admin/financial_transaction_classes_controller.rb +++ b/app/controllers/admin/financial_transaction_classes_controller.rb @@ -6,25 +6,25 @@ class Admin::FinancialTransactionClassesController < Admin::BaseController render layout: false end - def create - @financial_transaction_class = FinancialTransactionClass.new(params[:financial_transaction_class]) - if @financial_transaction_class.save - redirect_to update_transaction_types_admin_finances_url, status: 303 - else - render action: 'new', layout: false - end - end - def edit @financial_transaction_class = FinancialTransactionClass.find(params[:id]) render action: 'new', layout: false end + def create + @financial_transaction_class = FinancialTransactionClass.new(params[:financial_transaction_class]) + if @financial_transaction_class.save + redirect_to update_transaction_types_admin_finances_url, status: :see_other + else + render action: 'new', layout: false + end + end + def update @financial_transaction_class = FinancialTransactionClass.find(params[:id]) if @financial_transaction_class.update(params[:financial_transaction_class]) - redirect_to update_transaction_types_admin_finances_url, status: 303 + redirect_to update_transaction_types_admin_finances_url, status: :see_other else render action: 'new', layout: false end @@ -33,9 +33,9 @@ class Admin::FinancialTransactionClassesController < Admin::BaseController def destroy @financial_transaction_class = FinancialTransactionClass.find(params[:id]) @financial_transaction_class.destroy! - redirect_to update_transaction_types_admin_finances_url, status: 303 - rescue => error - flash.now[:alert] = error.message + redirect_to update_transaction_types_admin_finances_url, status: :see_other + rescue StandardError => e + flash.now[:alert] = e.message render template: 'shared/alert' end end diff --git a/app/controllers/admin/financial_transaction_types_controller.rb b/app/controllers/admin/financial_transaction_types_controller.rb index 2710bd6e..322451e4 100644 --- a/app/controllers/admin/financial_transaction_types_controller.rb +++ b/app/controllers/admin/financial_transaction_types_controller.rb @@ -7,25 +7,25 @@ class Admin::FinancialTransactionTypesController < Admin::BaseController render layout: false end - def create - @financial_transaction_type = FinancialTransactionType.new(params[:financial_transaction_type]) - if @financial_transaction_type.save - redirect_to update_transaction_types_admin_finances_url, status: 303 - else - render action: 'new', layout: false - end - end - def edit @financial_transaction_type = FinancialTransactionType.find(params[:id]) render action: 'new', layout: false end + def create + @financial_transaction_type = FinancialTransactionType.new(params[:financial_transaction_type]) + if @financial_transaction_type.save + redirect_to update_transaction_types_admin_finances_url, status: :see_other + else + render action: 'new', layout: false + end + end + def update @financial_transaction_type = FinancialTransactionType.find(params[:id]) if @financial_transaction_type.update(params[:financial_transaction_type]) - redirect_to update_transaction_types_admin_finances_url, status: 303 + redirect_to update_transaction_types_admin_finances_url, status: :see_other else render action: 'new', layout: false end @@ -34,9 +34,9 @@ class Admin::FinancialTransactionTypesController < Admin::BaseController def destroy @financial_transaction_type = FinancialTransactionType.find(params[:id]) @financial_transaction_type.destroy! - redirect_to update_transaction_types_admin_finances_url, status: 303 - rescue => error - flash.now[:alert] = error.message + redirect_to update_transaction_types_admin_finances_url, status: :see_other + rescue StandardError => e + flash.now[:alert] = e.message render template: 'shared/alert' end end diff --git a/app/controllers/admin/mail_delivery_status_controller.rb b/app/controllers/admin/mail_delivery_status_controller.rb index 52a4db92..c0086044 100644 --- a/app/controllers/admin/mail_delivery_status_controller.rb +++ b/app/controllers/admin/mail_delivery_status_controller.rb @@ -3,28 +3,28 @@ class Admin::MailDeliveryStatusController < Admin::BaseController def index @maildeliverystatus = MailDeliveryStatus.order(created_at: :desc) - @maildeliverystatus = @maildeliverystatus.where(email: params[:email]) unless params[:email].blank? + @maildeliverystatus = @maildeliverystatus.where(email: params[:email]) if params[:email].present? @maildeliverystatus = @maildeliverystatus.page(params[:page]).per(@per_page) end def show @maildeliverystatus = MailDeliveryStatus.find(params[:id]) filename = "maildeliverystatus_#{params[:id]}.#{MIME::Types[@maildeliverystatus.attachment_mime].first.preferred_extension}" - send_data(@maildeliverystatus.attachment_data, :filename => filename, :type => @maildeliverystatus.attachment_mime) + send_data(@maildeliverystatus.attachment_data, filename: filename, type: @maildeliverystatus.attachment_mime) end def destroy_all @maildeliverystatus = MailDeliveryStatus.delete_all redirect_to admin_mail_delivery_status_index_path, notice: t('.notice') - rescue => error - redirect_to admin_mail_delivery_status_index_path, alert: I18n.t('errors.general_msg', msg: error.message) + rescue StandardError => e + redirect_to admin_mail_delivery_status_index_path, alert: I18n.t('errors.general_msg', msg: e.message) end def destroy @maildeliverystatus = MailDeliveryStatus.find(params[:id]) @maildeliverystatus.destroy redirect_to admin_mail_delivery_status_index_path, notice: t('.notice') - rescue => error - redirect_to admin_mail_delivery_status_index_path, alert: I18n.t('errors.general_msg', msg: error.message) + rescue StandardError => e + redirect_to admin_mail_delivery_status_index_path, alert: I18n.t('errors.general_msg', msg: e.message) end end diff --git a/app/controllers/admin/ordergroups_controller.rb b/app/controllers/admin/ordergroups_controller.rb index d9dabe1e..213f3a0d 100644 --- a/app/controllers/admin/ordergroups_controller.rb +++ b/app/controllers/admin/ordergroups_controller.rb @@ -2,16 +2,15 @@ class Admin::OrdergroupsController < Admin::BaseController inherit_resources def index - @ordergroups = Ordergroup.undeleted.sort_by_param(params["sort"]) + @ordergroups = Ordergroup.undeleted.sort_by_param(params['sort']) if request.format.csv? - send_data OrdergroupsCsv.new(@ordergroups).to_csv, filename: 'ordergroups.csv', type: 'text/csv' + send_data OrdergroupsCsv.new(@ordergroups).to_csv, filename: 'ordergroups.csv', + type: 'text/csv' end # if somebody uses the search field: - unless params[:query].blank? - @ordergroups = @ordergroups.where('name LIKE ?', "%#{params[:query]}%") - end + @ordergroups = @ordergroups.where('name LIKE ?', "%#{params[:query]}%") if params[:query].present? @ordergroups = @ordergroups.page(params[:page]).per(@per_page) end @@ -19,8 +18,8 @@ class Admin::OrdergroupsController < Admin::BaseController def destroy @ordergroup = Ordergroup.find(params[:id]) @ordergroup.mark_as_deleted - redirect_to admin_ordergroups_url, notice: t('admin.ordergroups.destroy.notice') - rescue => error - redirect_to admin_ordergroups_url, alert: t('admin.ordergroups.destroy.error') + redirect_to admin_ordergroups_url, notice: t('.notice') + rescue StandardError => e + redirect_to admin_ordergroups_url, alert: t('.error') end end diff --git a/app/controllers/admin/supplier_categories_controller.rb b/app/controllers/admin/supplier_categories_controller.rb index f5768a21..f119dfb6 100644 --- a/app/controllers/admin/supplier_categories_controller.rb +++ b/app/controllers/admin/supplier_categories_controller.rb @@ -6,6 +6,11 @@ class Admin::SupplierCategoriesController < Admin::BaseController render layout: false end + def edit + @supplier_category = SupplierCategory.find(params[:id]) + render action: 'new', layout: false + end + def create @supplier_category = SupplierCategory.new(params[:supplier_category]) if @supplier_category.valid? && @supplier_category.save @@ -15,11 +20,6 @@ class Admin::SupplierCategoriesController < Admin::BaseController end end - def edit - @supplier_category = SupplierCategory.find(params[:id]) - render action: 'new', layout: false - end - def update @supplier_category = SupplierCategory.find(params[:id]) diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index 18bbbc1d..7d7e9295 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -3,16 +3,14 @@ class Admin::UsersController < Admin::BaseController def index @users = params[:show_deleted] ? User.deleted : User.undeleted - @users = @users.sort_by_param(params["sort"]) + @users = @users.sort_by_param(params['sort']) @users = @users.includes(:mail_delivery_status) - if request.format.csv? - send_data UsersCsv.new(@users).to_csv, filename: 'users.csv', type: 'text/csv' - end + send_data UsersCsv.new(@users).to_csv, filename: 'users.csv', type: 'text/csv' if request.format.csv? # if somebody uses the search field: - @users = @users.natural_search(params[:user_name]) unless params[:user_name].blank? + @users = @users.natural_search(params[:user_name]) if params[:user_name].present? @users = @users.page(params[:page]).per(@per_page) end @@ -20,17 +18,17 @@ class Admin::UsersController < Admin::BaseController def destroy @user = User.find(params[:id]) @user.mark_as_deleted - redirect_to admin_users_url, notice: t('admin.users.destroy.notice') - rescue => error - redirect_to admin_users_url, alert: t('admin.users.destroy.error', error: error.message) + redirect_to admin_users_url, notice: t('.notice') + rescue StandardError => e + redirect_to admin_users_url, alert: t('.error', error: e.message) end def restore @user = User.find(params[:id]) @user.restore - redirect_to admin_users_url, notice: t('admin.users.restore.notice') - rescue => error - redirect_to admin_users_url, alert: t('admin.users.restore.error', error: error.message) + redirect_to admin_users_url, notice: t('.notice') + rescue StandardError => e + redirect_to admin_users_url, alert: t('.error', error: e.message) end def sudo diff --git a/app/controllers/admin/workgroups_controller.rb b/app/controllers/admin/workgroups_controller.rb index 184000bd..f5a9c2a3 100644 --- a/app/controllers/admin/workgroups_controller.rb +++ b/app/controllers/admin/workgroups_controller.rb @@ -4,7 +4,7 @@ class Admin::WorkgroupsController < Admin::BaseController def index @workgroups = Workgroup.order('name ASC') # if somebody uses the search field: - @workgroups = @workgroups.where('name LIKE ?', "%#{params[:query]}%") unless params[:query].blank? + @workgroups = @workgroups.where('name LIKE ?', "%#{params[:query]}%") if params[:query].present? @workgroups = @workgroups.page(params[:page]).per(@per_page) end @@ -12,8 +12,8 @@ class Admin::WorkgroupsController < Admin::BaseController def destroy @workgroup = Workgroup.find(params[:id]) @workgroup.destroy - redirect_to admin_workgroups_url, notice: t('admin.workgroups.destroy.notice') - rescue => error - redirect_to admin_workgroups_url, alert: t('admin.workgroups.destroy.error', error: error.message) + redirect_to admin_workgroups_url, notice: t('.notice') + rescue StandardError => e + redirect_to admin_workgroups_url, alert: t('.error', error: e.message) end end diff --git a/app/controllers/api/v1/base_controller.rb b/app/controllers/api/v1/base_controller.rb index 13e903f1..8bed20ec 100644 --- a/app/controllers/api/v1/base_controller.rb +++ b/app/controllers/api/v1/base_controller.rb @@ -20,29 +20,30 @@ class Api::V1::BaseController < ApplicationController def require_ordergroup authenticate - unless current_ordergroup.present? - raise Api::Errors::PermissionRequired.new('Forbidden, must be in an ordergroup') - end + return if current_ordergroup.present? + + raise Api::Errors::PermissionRequired, 'Forbidden, must be in an ordergroup' end def require_minimum_balance minimum_balance = FoodsoftConfig[:minimum_balance] or return - if current_ordergroup.account_balance < minimum_balance - raise Api::Errors::PermissionRequired.new(t('application.controller.error_minimum_balance', min: minimum_balance)) - end + return unless current_ordergroup.account_balance < minimum_balance + + raise Api::Errors::PermissionRequired, t('application.controller.error_minimum_balance', min: minimum_balance) end def require_enough_apples - if current_ordergroup.not_enough_apples? - s = t('group_orders.messages.not_enough_apples', apples: current_ordergroup.apples, stop_ordering_under: FoodsoftConfig[:stop_ordering_under]) - raise Api::Errors::PermissionRequired.new(s) - end + return unless current_ordergroup.not_enough_apples? + + s = t('group_orders.messages.not_enough_apples', apples: current_ordergroup.apples, + stop_ordering_under: FoodsoftConfig[:stop_ordering_under]) + raise Api::Errors::PermissionRequired, s end def require_config_enabled(config) - unless FoodsoftConfig[config] - raise Api::Errors::PermissionRequired.new(t('application.controller.error_not_enabled', config: config)) - end + return if FoodsoftConfig[config] + + raise Api::Errors::PermissionRequired, t('application.controller.error_not_enabled', config: config) end def skip_session @@ -52,12 +53,12 @@ class Api::V1::BaseController < ApplicationController def not_found_handler(e) # remove where-clauses from error message (not suitable for end-users) msg = e.message.try { |m| m.sub(/\s*\[.*?\]\s*$/, '') } || 'Not found' - render status: 404, json: { error: 'not_found', error_description: msg } + render status: :not_found, json: { error: 'not_found', error_description: msg } end def not_acceptable_handler(e) msg = e.message || 'Data not acceptable' - render status: 422, json: { error: 'not_acceptable', error_description: msg } + render status: :unprocessable_entity, json: { error: 'not_acceptable', error_description: msg } end def doorkeeper_unauthorized_render_options(error:) @@ -70,11 +71,11 @@ class Api::V1::BaseController < ApplicationController def permission_required_handler(e) msg = e.message || 'Forbidden, user has no access' - render status: 403, json: { error: 'forbidden', error_description: msg } + render status: :forbidden, json: { error: 'forbidden', error_description: msg } end # @todo something with ApplicationHelper#show_user - def show_user(user = current_user, **options) + def show_user(user = current_user, **_options) user.display end end diff --git a/app/controllers/api/v1/user/financial_transactions_controller.rb b/app/controllers/api/v1/user/financial_transactions_controller.rb index 96b32e28..3de38de9 100644 --- a/app/controllers/api/v1/user/financial_transactions_controller.rb +++ b/app/controllers/api/v1/user/financial_transactions_controller.rb @@ -16,7 +16,8 @@ class Api::V1::User::FinancialTransactionsController < Api::V1::BaseController def create transaction_type = FinancialTransactionType.find(create_params[:financial_transaction_type_id]) - ft = current_ordergroup.add_financial_transaction!(create_params[:amount], create_params[:note], current_user, transaction_type) + ft = current_ordergroup.add_financial_transaction!(create_params[:amount], create_params[:note], current_user, + transaction_type) render json: ft end diff --git a/app/controllers/api/v1/user/group_order_articles_controller.rb b/app/controllers/api/v1/user/group_order_articles_controller.rb index ce258898..4b65a61d 100644 --- a/app/controllers/api/v1/user/group_order_articles_controller.rb +++ b/app/controllers/api/v1/user/group_order_articles_controller.rb @@ -4,8 +4,8 @@ class Api::V1::User::GroupOrderArticlesController < Api::V1::BaseController before_action -> { doorkeeper_authorize! 'group_orders:user' } before_action :require_ordergroup - before_action :require_minimum_balance, only: [:create, :update] # destroy is ok - before_action :require_enough_apples, only: [:create, :update] # destroy is ok + before_action :require_minimum_balance, only: %i[create update] # destroy is ok + before_action :require_enough_apples, only: %i[create update] # destroy is ok # @todo allow decreasing amounts when minimum balance isn't met def index @@ -35,7 +35,8 @@ class Api::V1::User::GroupOrderArticlesController < Api::V1::BaseController goa = nil GroupOrderArticle.transaction do goa = scope_for_update.includes(:group_order_article_quantities).find(params.require(:id)) - goa.update_quantities((update_params[:quantity] || goa.quantity).to_i, (update_params[:tolerance] || goa.tolerance).to_i) + goa.update_quantities((update_params[:quantity] || goa.quantity).to_i, + (update_params[:tolerance] || goa.tolerance).to_i) goa.order_article.update_results! goa.group_order.update_price! goa.group_order.update!(updated_by: current_user) diff --git a/app/controllers/api/v1/user/ordergroup_controller.rb b/app/controllers/api/v1/user/ordergroup_controller.rb index 08c12b4c..23889fe8 100644 --- a/app/controllers/api/v1/user/ordergroup_controller.rb +++ b/app/controllers/api/v1/user/ordergroup_controller.rb @@ -8,13 +8,13 @@ class Api::V1::User::OrdergroupController < Api::V1::BaseController financial_overview: { account_balance: ordergroup.account_balance.to_f, available_funds: ordergroup.get_available_funds.to_f, - financial_transaction_class_sums: FinancialTransactionClass.sorted.map { |c| + financial_transaction_class_sums: FinancialTransactionClass.sorted.map do |c| { id: c.id, name: c.display, amount: ordergroup["sum_of_class_#{c.id}"].to_f } - } + end } } end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index eb90f9b4..3537f8c4 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -19,10 +19,10 @@ class ApplicationController < ActionController::Base private def set_user_last_activity - if current_user && (session[:last_activity] == nil || session[:last_activity] < 1.minutes.ago) - current_user.update_attribute(:last_activity, Time.now) - session[:last_activity] = Time.now - end + return unless current_user && (session[:last_activity].nil? || session[:last_activity] < 1.minute.ago) + + current_user.update_attribute(:last_activity, Time.now) + session[:last_activity] = Time.now end # Many plugins can be turned on and off on the fly with a `use_` configuration option. @@ -64,11 +64,11 @@ class ApplicationController < ActionController::Base end def items_per_page - if params[:per_page] && params[:per_page].to_i > 0 && params[:per_page].to_i <= 500 - @per_page = params[:per_page].to_i - else - @per_page = 20 - end + @per_page = if params[:per_page] && params[:per_page].to_i > 0 && params[:per_page].to_i <= 500 + params[:per_page].to_i + else + 20 + end end # Set timezone according to foodcoop preference. diff --git a/app/controllers/article_categories_controller.rb b/app/controllers/article_categories_controller.rb index bfa601d3..810bb3ce 100644 --- a/app/controllers/article_categories_controller.rb +++ b/app/controllers/article_categories_controller.rb @@ -4,17 +4,17 @@ class ArticleCategoriesController < ApplicationController before_action :authenticate_article_meta def create - create!(:notice => I18n.t('article_categories.create.notice')) { article_categories_path } + create!(notice: I18n.t('article_categories.create.notice')) { article_categories_path } end def update - update!(:notice => I18n.t('article_categories.update.notice')) { article_categories_path } + update!(notice: I18n.t('article_categories.update.notice')) { article_categories_path } end def destroy destroy! - rescue => error - redirect_to article_categories_path, alert: I18n.t('article_categories.destroy.error', message: error.message) + rescue StandardError => e + redirect_to article_categories_path, alert: I18n.t('article_categories.destroy.error', message: e.message) end protected diff --git a/app/controllers/articles_controller.rb b/app/controllers/articles_controller.rb index 4161e66a..232391cf 100644 --- a/app/controllers/articles_controller.rb +++ b/app/controllers/articles_controller.rb @@ -2,24 +2,24 @@ class ArticlesController < ApplicationController before_action :authenticate_article_meta, :find_supplier def index - if params['sort'] - sort = case params['sort'] - when "name" then "articles.name" - when "unit" then "articles.unit" - when "article_category" then "article_categories.name" - when "note" then "articles.note" - when "availability" then "articles.availability" - when "name_reverse" then "articles.name DESC" - when "unit_reverse" then "articles.unit DESC" - when "article_category_reverse" then "article_categories.name DESC" - when "note_reverse" then "articles.note DESC" - when "availability_reverse" then "articles.availability DESC" + sort = if params['sort'] + case params['sort'] + when 'name' then 'articles.name' + when 'unit' then 'articles.unit' + when 'article_category' then 'article_categories.name' + when 'note' then 'articles.note' + when 'availability' then 'articles.availability' + when 'name_reverse' then 'articles.name DESC' + when 'unit_reverse' then 'articles.unit DESC' + when 'article_category_reverse' then 'article_categories.name DESC' + when 'note_reverse' then 'articles.note DESC' + when 'availability_reverse' then 'articles.availability DESC' end - else - sort = "article_categories.name, articles.name" - end + else + 'article_categories.name, articles.name' + end - @articles = Article.undeleted.where(supplier_id: @supplier, :type => nil).includes(:article_category).order(sort) + @articles = Article.undeleted.where(supplier_id: @supplier, type: nil).includes(:article_category).order(sort) if request.format.csv? send_data ArticlesCsv.new(@articles, encoding: 'utf-8').to_csv, filename: 'articles.csv', type: 'text/csv' @@ -32,42 +32,42 @@ class ArticlesController < ApplicationController respond_to do |format| format.html - format.js { render :layout => false } + format.js { render layout: false } end end def new - @article = @supplier.articles.build(:tax => FoodsoftConfig[:tax_default]) - render :layout => false + @article = @supplier.articles.build(tax: FoodsoftConfig[:tax_default]) + render layout: false end def copy @article = @supplier.articles.find(params[:article_id]).dup - render :layout => false + render layout: false + end + + def edit + @article = Article.find(params[:id]) + render action: 'new', layout: false end def create @article = Article.new(params[:article]) if @article.valid? && @article.save - render :layout => false + render layout: false else - render :action => 'new', :layout => false + render action: 'new', layout: false end end - def edit - @article = Article.find(params[:id]) - render :action => 'new', :layout => false - end - # Updates one Article and highlights the line if succeded def update @article = Article.find(params[:id]) if @article.update(params[:article]) - render :layout => false + render layout: false else - render :action => 'new', :layout => false + render action: 'new', layout: false end end @@ -75,7 +75,7 @@ class ArticlesController < ApplicationController def destroy @article = Article.find(params[:id]) @article.mark_as_deleted unless @order = @article.in_open_order # If article is in an active Order, the Order will be returned - render :layout => false + render layout: false end # Renders a form for editing all articles from a supplier @@ -87,19 +87,17 @@ class ArticlesController < ApplicationController def update_all invalid_articles = false - begin - Article.transaction do - unless params[:articles].blank? - # Update other article attributes... - @articles = Article.find(params[:articles].keys) - @articles.each do |article| - unless article.update(params[:articles][article.id.to_s]) - invalid_articles = true unless invalid_articles # Remember that there are validation errors - end + Article.transaction do + if params[:articles].present? + # Update other article attributes... + @articles = Article.find(params[:articles].keys) + @articles.each do |article| + unless article.update(params[:articles][article.id.to_s]) + invalid_articles ||= true # Remember that there are validation errors end - - raise ActiveRecord::Rollback if invalid_articles # Rollback all changes end + + raise ActiveRecord::Rollback if invalid_articles # Rollback all changes end end @@ -134,16 +132,15 @@ class ArticlesController < ApplicationController end end # action succeded - redirect_to supplier_articles_url(@supplier, :per_page => params[:per_page]) - rescue => error - redirect_to supplier_articles_url(@supplier, :per_page => params[:per_page]), - :alert => I18n.t('errors.general_msg', :msg => error) + redirect_to supplier_articles_url(@supplier, per_page: params[:per_page]) + rescue StandardError => e + redirect_to supplier_articles_url(@supplier, per_page: params[:per_page]), + alert: I18n.t('errors.general_msg', msg: e) end # lets start with parsing articles from uploaded file, yeah # Renders the upload form - def upload - end + def upload; end # Update articles from a spreadsheet def parse_upload @@ -151,13 +148,15 @@ class ArticlesController < ApplicationController options = { filename: uploaded_file.original_filename } options[:outlist_absent] = (params[:articles]['outlist_absent'] == '1') options[:convert_units] = (params[:articles]['convert_units'] == '1') - @updated_article_pairs, @outlisted_articles, @new_articles = @supplier.sync_from_file uploaded_file.tempfile, options + @updated_article_pairs, @outlisted_articles, @new_articles = @supplier.sync_from_file uploaded_file.tempfile, + options if @updated_article_pairs.empty? && @outlisted_articles.empty? && @new_articles.empty? - redirect_to supplier_articles_path(@supplier), :notice => I18n.t('articles.controller.parse_upload.notice') + redirect_to supplier_articles_path(@supplier), + notice: I18n.t('articles.controller.parse_upload.notice') end @ignored_article_count = 0 - rescue => error - redirect_to upload_supplier_articles_path(@supplier), :alert => I18n.t('errors.general_msg', :msg => error.message) + rescue StandardError => e + redirect_to upload_supplier_articles_path(@supplier), alert: I18n.t('errors.general_msg', msg: e.message) end # sync all articles with the external database @@ -165,13 +164,14 @@ class ArticlesController < ApplicationController def sync # check if there is an shared_supplier unless @supplier.shared_supplier - redirect_to supplier_articles_url(@supplier), :alert => I18n.t('articles.controller.sync.shared_alert', :supplier => @supplier.name) + redirect_to supplier_articles_url(@supplier), + alert: I18n.t('articles.controller.sync.shared_alert', supplier: @supplier.name) end # sync articles against external database @updated_article_pairs, @outlisted_articles, @new_articles = @supplier.sync_all - if @updated_article_pairs.empty? && @outlisted_articles.empty? && @new_articles.empty? - redirect_to supplier_articles_path(@supplier), :notice => I18n.t('articles.controller.sync.notice') - end + return unless @updated_article_pairs.empty? && @outlisted_articles.empty? && @new_articles.empty? + + redirect_to supplier_articles_path(@supplier), notice: I18n.t('articles.controller.sync.notice') end # Updates, deletes articles when upload or sync form is submitted @@ -186,7 +186,7 @@ class ArticlesController < ApplicationController # delete articles begin @outlisted_articles.each(&:mark_as_deleted) - rescue + rescue StandardError # raises an exception when used in current order has_error = true end @@ -198,15 +198,15 @@ class ArticlesController < ApplicationController raise ActiveRecord::Rollback if has_error end - if !has_error - redirect_to supplier_articles_path(@supplier), notice: I18n.t('articles.controller.update_sync.notice') - else + if has_error @updated_article_pairs = @updated_articles.map do |article| orig_article = Article.find(article.id) [article, orig_article.unequal_attributes(article)] end flash.now.alert = I18n.t('articles.controller.error_invalid') render params[:from_action] == 'sync' ? :sync : :parse_upload + else + redirect_to supplier_articles_path(@supplier), notice: I18n.t('articles.controller.update_sync.notice') end end @@ -218,18 +218,18 @@ class ArticlesController < ApplicationController q[:name_cont_all] = params.fetch(:name_cont_all_joined, '').split(' ') search = @supplier.shared_supplier.shared_articles.ransack(q) @articles = search.result.page(params[:page]).per(10) - render :layout => false + render layout: false end # fills a form whith values of the selected shared_article # when the direct parameter is set and the article is valid, it is imported directly def import @article = SharedArticle.find(params[:shared_article_id]).build_new_article(@supplier) - @article.article_category_id = params[:article_category_id] unless params[:article_category_id].blank? - if params[:direct] && !params[:article_category_id].blank? && @article.valid? && @article.save - render :action => 'create', :layout => false + @article.article_category_id = params[:article_category_id] if params[:article_category_id].present? + if params[:direct] && params[:article_category_id].present? && @article.valid? && @article.save + render action: 'create', layout: false else - render :action => 'new', :layout => false + render action: 'new', layout: false end end diff --git a/app/controllers/concerns/auth.rb b/app/controllers/concerns/auth.rb index 277acd69..edf6ec6f 100644 --- a/app/controllers/concerns/auth.rb +++ b/app/controllers/concerns/auth.rb @@ -9,15 +9,19 @@ module Concerns::Auth def current_user # check if there is a valid session and return the logged-in user (its object) - if session[:user_id] && params[:foodcoop] - # for shared-host installations. check if the cookie-subdomain fits to request. - @current_user ||= User.undeleted.find_by_id(session[:user_id]) if session[:scope] == FoodsoftConfig.scope - end + return unless session[:user_id] && params[:foodcoop] + + # for shared-host installations. check if the cookie-subdomain fits to request. + @current_user ||= User.undeleted.find_by_id(session[:user_id]) if session[:scope] == FoodsoftConfig.scope end def deny_access session[:return_to] = request.original_url - redirect_to root_url, alert: I18n.t('application.controller.error_denied', sign_in: ActionController::Base.helpers.link_to(t('application.controller.error_denied_sign_in'), login_path)) + redirect_to root_url, + alert: I18n.t('application.controller.error_denied', + sign_in: ActionController::Base.helpers.link_to( + t('application.controller.error_denied_sign_in'), login_path + )) end private @@ -47,12 +51,7 @@ module Concerns::Auth def authenticate(role = 'any') # Attempt to retrieve authenticated user from controller instance or session... - if !current_user - # No user at all: redirect to login page. - logout - session[:return_to] = request.original_url - redirect_to_login :alert => I18n.t('application.controller.error_authn') - else + if current_user # We have an authenticated user, now check role... # Roles gets the user through his memberships. hasRole = case role @@ -73,6 +72,11 @@ module Concerns::Auth else deny_access end + else + # No user at all: redirect to login page. + logout + session[:return_to] = request.original_url + redirect_to_login alert: I18n.t('application.controller.error_authn') end end @@ -116,13 +120,13 @@ module Concerns::Auth # if fails the user will redirected to startpage def authenticate_membership_or_admin(group_id = params[:id]) @group = Group.find(group_id) - unless @group.member?(@current_user) || @current_user.role_admin? - redirect_to root_path, alert: I18n.t('application.controller.error_members_only') - end + return if @group.member?(@current_user) || @current_user.role_admin? + + redirect_to root_path, alert: I18n.t('application.controller.error_members_only') end def authenticate_or_token(prefix, role = 'any') - if not params[:token].blank? + if params[:token].present? begin TokenVerifier.new(prefix).verify(params[:token]) rescue ActiveSupport::MessageVerifier::InvalidSignature diff --git a/app/controllers/concerns/auth_api.rb b/app/controllers/concerns/auth_api.rb index 2c80dddf..fc16a2c2 100644 --- a/app/controllers/concerns/auth_api.rb +++ b/app/controllers/concerns/auth_api.rb @@ -36,9 +36,9 @@ module Concerns::AuthApi # Make sure that at least one the given OAuth scopes is valid for the current user's permissions. # @raise Api::Errors::PermissionsRequired def doorkeeper_authorize_roles!(*scopes) - unless scopes.any? { |scope| doorkeeper_scope_permitted?(scope) } - raise Api::Errors::PermissionRequired.new('Forbidden, no permission') - end + return if scopes.any? { |scope| doorkeeper_scope_permitted?(scope) } + + raise Api::Errors::PermissionRequired, 'Forbidden, no permission' end # Check whether a given OAuth scope is permitted for the current user. @@ -48,9 +48,7 @@ module Concerns::AuthApi def doorkeeper_scope_permitted?(scope) scope_parts = scope.split(':') # user sub-scopes like +config:user+ are always permitted - if scope_parts.last == 'user' - return true - end + return true if scope_parts.last == 'user' case scope_parts.first when 'user' then return true # access to the current user's own profile @@ -64,8 +62,8 @@ module Concerns::AuthApi end case scope - when 'orders:read' then return true - when 'orders:write' then return current_user.role_orders? + when 'orders:read' then true + when 'orders:write' then current_user.role_orders? end end end diff --git a/app/controllers/concerns/foodcoop_scope.rb b/app/controllers/concerns/foodcoop_scope.rb index 0a8e382e..7a99adf9 100644 --- a/app/controllers/concerns/foodcoop_scope.rb +++ b/app/controllers/concerns/foodcoop_scope.rb @@ -24,12 +24,12 @@ module Concerns::FoodcoopScope elsif FoodsoftConfig.allowed_foodcoop? foodcoop FoodsoftConfig.select_foodcoop foodcoop else - raise ActionController::RoutingError.new 'Foodcoop Not Found' + raise ActionController::RoutingError, 'Foodcoop Not Found' end end # Always stay in foodcoop url scope - def default_url_options(options = {}) + def default_url_options(_options = {}) super().merge({ foodcoop: FoodsoftConfig.scope }) end end diff --git a/app/controllers/concerns/locale.rb b/app/controllers/concerns/locale.rb index 22686c15..6a9736fb 100644 --- a/app/controllers/concerns/locale.rb +++ b/app/controllers/concerns/locale.rb @@ -18,7 +18,7 @@ module Concerns::Locale end def browser_language - request.env['HTTP_ACCEPT_LANGUAGE'] ? request.env['HTTP_ACCEPT_LANGUAGE'].scan(/^[a-z]{2}/).first : nil + request.env['HTTP_ACCEPT_LANGUAGE']&.scan(/^[a-z]{2}/)&.first end def default_language @@ -30,7 +30,7 @@ module Concerns::Locale def select_language_according_to_priority language = explicitly_requested_language || session_language || user_settings_language language ||= browser_language unless FoodsoftConfig[:ignore_browser_locale] - language.presence&.to_sym unless language.blank? + language.presence&.to_sym if language.present? end def available_locales @@ -38,11 +38,11 @@ module Concerns::Locale end def set_locale - if available_locales.include?(select_language_according_to_priority) - ::I18n.locale = select_language_according_to_priority - else - ::I18n.locale = default_language - end + ::I18n.locale = if available_locales.include?(select_language_according_to_priority) + select_language_according_to_priority + else + default_language + end locale = session[:locale] = ::I18n.locale logger.info("Set locale to #{locale}") diff --git a/app/controllers/concerns/send_order_pdf.rb b/app/controllers/concerns/send_order_pdf.rb index 09225b7c..283512da 100644 --- a/app/controllers/concerns/send_order_pdf.rb +++ b/app/controllers/concerns/send_order_pdf.rb @@ -3,7 +3,7 @@ module Concerns::SendOrderPdf protected - def send_order_pdf order, document + def send_order_pdf(order, document) klass = case document when 'groups' then OrderByGroups when 'articles' then OrderByArticles diff --git a/app/controllers/deliveries_controller.rb b/app/controllers/deliveries_controller.rb index 0ecacc9c..15900022 100644 --- a/app/controllers/deliveries_controller.rb +++ b/app/controllers/deliveries_controller.rb @@ -1,5 +1,5 @@ class DeliveriesController < ApplicationController - before_action :find_supplier, :exclude => :fill_new_stock_article_form + before_action :find_supplier, exclude: :fill_new_stock_article_form def index @deliveries = @supplier.deliveries.order('date DESC') @@ -15,6 +15,10 @@ class DeliveriesController < ApplicationController @delivery.date = Date.today # TODO: move to model/database end + def edit + @delivery = Delivery.find(params[:id]) + end + def create @delivery = Delivery.new(params[:delivery]) @@ -22,14 +26,10 @@ class DeliveriesController < ApplicationController flash[:notice] = I18n.t('deliveries.create.notice') redirect_to [@supplier, @delivery] else - render :action => "new" + render action: 'new' end end - def edit - @delivery = Delivery.find(params[:id]) - end - def update @delivery = Delivery.find(params[:id]) @@ -37,7 +37,7 @@ class DeliveriesController < ApplicationController flash[:notice] = I18n.t('deliveries.update.notice') redirect_to [@supplier, @delivery] else - render :action => "edit" + render action: 'edit' end end @@ -52,18 +52,18 @@ class DeliveriesController < ApplicationController def add_stock_change @stock_change = StockChange.new @stock_change.stock_article = StockArticle.find(params[:stock_article_id]) - render :layout => false + render layout: false end def form_on_stock_article_create # See publish/subscribe design pattern in /doc. @stock_article = StockArticle.find(params[:id]) - render :layout => false + render layout: false end def form_on_stock_article_update # See publish/subscribe design pattern in /doc. @stock_article = StockArticle.find(params[:id]) - render :layout => false + render layout: false end end diff --git a/app/controllers/feedback_controller.rb b/app/controllers/feedback_controller.rb index ada72859..b4e5ea7e 100644 --- a/app/controllers/feedback_controller.rb +++ b/app/controllers/feedback_controller.rb @@ -1,13 +1,12 @@ class FeedbackController < ApplicationController - def new - end + def new; end def create if params[:message].present? Mailer.feedback(current_user, params[:message]).deliver_now - redirect_to root_url, notice: t('feedback.create.notice') + redirect_to root_url, notice: t('.notice') else - render :action => 'new' + render action: 'new' end end end diff --git a/app/controllers/finance/balancing_controller.rb b/app/controllers/finance/balancing_controller.rb index 4f23ac4f..e1a2dafb 100644 --- a/app/controllers/finance/balancing_controller.rb +++ b/app/controllers/finance/balancing_controller.rb @@ -5,7 +5,7 @@ class Finance::BalancingController < Finance::BaseController def new @order = Order.find(params[:order_id]) - flash.now.alert = t('finance.balancing.new.alert') if @order.closed? + flash.now.alert = t('.alert') if @order.closed? @comments = @order.comments @articles = @order.order_articles.ordered_or_member.includes(:article, :article_price, @@ -13,13 +13,13 @@ class Finance::BalancingController < Finance::BaseController sort_param = params['sort'] || 'name' @articles = case sort_param - when 'name' then + when 'name' @articles.order('articles.name ASC') - when 'name_reverse' then + when 'name_reverse' @articles.order('articles.name DESC') - when 'order_number' then + when 'order_number' @articles.order('articles.order_number ASC') - when 'order_number_reverse' then + when 'order_number_reverse' @articles.order('articles.order_number DESC') else @articles @@ -31,13 +31,13 @@ class Finance::BalancingController < Finance::BaseController def new_on_order_article_create # See publish/subscribe design pattern in /doc. @order_article = OrderArticle.find(params[:order_article_id]) - render :layout => false + render layout: false end def new_on_order_article_update # See publish/subscribe design pattern in /doc. @order_article = OrderArticle.find(params[:order_article_id]) - render :layout => false + render layout: false end def update_summary @@ -46,29 +46,29 @@ class Finance::BalancingController < Finance::BaseController def edit_note @order = Order.find(params[:id]) - render :layout => false + render layout: false end def update_note @order = Order.find(params[:id]) if @order.update(params[:order]) - render :layout => false + render layout: false else - render :action => :edit_note, :layout => false + render action: :edit_note, layout: false end end def edit_transport @order = Order.find(params[:id]) - render :layout => false + render layout: false end def update_transport @order = Order.find(params[:id]) @order.update!(params[:order]) redirect_to new_finance_order_path(order_id: @order.id) - rescue => error - redirect_to new_finance_order_path(order_id: @order.id), alert: t('errors.general_msg', msg: error.message) + rescue StandardError => e + redirect_to new_finance_order_path(order_id: @order.id), alert: t('errors.general_msg', msg: e.message) end # before the order will booked, a view lists all Ordergroups and its order_prices @@ -81,18 +81,18 @@ class Finance::BalancingController < Finance::BaseController @order = Order.find(params[:id]) @type = FinancialTransactionType.find_by_id(params.permit(:type)[:type]) @order.close!(@current_user, @type) - redirect_to finance_order_index_url, notice: t('finance.balancing.close.notice') - rescue => error - redirect_to new_finance_order_url(order_id: @order.id), alert: t('finance.balancing.close.alert', message: error.message) + redirect_to finance_order_index_url, notice: t('.notice') + rescue StandardError => e + redirect_to new_finance_order_url(order_id: @order.id), alert: t('.alert', message: e.message) end # Close the order directly, without automaticly updating ordergroups account balances def close_direct @order = Order.find(params[:id]) @order.close_direct!(@current_user) - redirect_to finance_order_index_url, notice: t('finance.balancing.close_direct.notice') - rescue => error - redirect_to finance_order_index_url, alert: t('finance.balancing.close_direct.alert', message: error.message) + redirect_to finance_order_index_url, notice: t('.notice') + rescue StandardError => e + redirect_to finance_order_index_url, alert: t('.alert', message: e.message) end def close_all_direct_with_invoice @@ -103,8 +103,8 @@ class Finance::BalancingController < Finance::BaseController count += 1 end end - redirect_to finance_order_index_url, notice: t('finance.balancing.close_all_direct_with_invoice.notice', count: count) - rescue => error - redirect_to finance_order_index_url, alert: t('errors.general_msg', msg: error.message) + redirect_to finance_order_index_url, notice: t('.notice', count: count) + rescue StandardError => e + redirect_to finance_order_index_url, alert: t('errors.general_msg', msg: e.message) end end diff --git a/app/controllers/finance/bank_accounts_controller.rb b/app/controllers/finance/bank_accounts_controller.rb index 66d9fddd..81403f6a 100644 --- a/app/controllers/finance/bank_accounts_controller.rb +++ b/app/controllers/finance/bank_accounts_controller.rb @@ -8,8 +8,8 @@ class Finance::BankAccountsController < Finance::BaseController @bank_account = BankAccount.find(params[:id]) count = @bank_account.assign_unlinked_transactions redirect_to finance_bank_account_transactions_url(@bank_account), notice: t('.notice', count: count) - rescue => error - redirect_to finance_bank_account_transactions_url(@bank_account), alert: t('errors.general_msg', msg: error.message) + rescue StandardError => e + redirect_to finance_bank_account_transactions_url(@bank_account), alert: t('errors.general_msg', msg: e.message) end def import @@ -33,8 +33,8 @@ class Finance::BankAccountsController < Finance::BaseController end needs_redirect = ok - rescue => error - flash.alert = t('errors.general_msg', msg: error.message) + rescue StandardError => e + flash.alert = t('errors.general_msg', msg: e.message) needs_redirect = true ensure return unless needs_redirect diff --git a/app/controllers/finance/bank_transactions_controller.rb b/app/controllers/finance/bank_transactions_controller.rb index 53c35168..22b38d06 100644 --- a/app/controllers/finance/bank_transactions_controller.rb +++ b/app/controllers/finance/bank_transactions_controller.rb @@ -3,26 +3,30 @@ class Finance::BankTransactionsController < ApplicationController inherit_resources def index - if params["sort"] - sort = case params["sort"] - when "date" then "date" - when "amount" then "amount" - when "financial_link" then "financial_link_id" - when "date_reverse" then "date DESC" - when "amount_reverse" then "amount DESC" - when "financial_link_reverse" then "financial_link_id DESC" + sort = if params['sort'] + case params['sort'] + when 'date' then 'date' + when 'amount' then 'amount' + when 'financial_link' then 'financial_link_id' + when 'date_reverse' then 'date DESC' + when 'amount_reverse' then 'amount DESC' + when 'financial_link_reverse' then 'financial_link_id DESC' end - else - sort = "date DESC" - end + else + 'date DESC' + end @bank_account = BankAccount.find(params[:bank_account_id]) @bank_transactions_all = @bank_account.bank_transactions.order(sort).includes(:financial_link) - @bank_transactions_all = @bank_transactions_all.where('reference LIKE ? OR text LIKE ?', "%#{params[:query]}%", "%#{params[:query]}%") unless params[:query].nil? + unless params[:query].nil? + @bank_transactions_all = @bank_transactions_all.where('reference LIKE ? OR text LIKE ?', "%#{params[:query]}%", + "%#{params[:query]}%") + end @bank_transactions = @bank_transactions_all.page(params[:page]).per(@per_page) respond_to do |format| - format.js; format.html { render } + format.js + format.html { render } format.csv do send_data BankTransactionsCsv.new(@bank_transactions_all).to_csv, filename: 'transactions.csv', type: 'text/csv' end diff --git a/app/controllers/finance/financial_links_controller.rb b/app/controllers/finance/financial_links_controller.rb index 17d8399a..c78a79b3 100644 --- a/app/controllers/finance/financial_links_controller.rb +++ b/app/controllers/finance/financial_links_controller.rb @@ -1,5 +1,5 @@ class Finance::FinancialLinksController < Finance::BaseController - before_action :find_financial_link, except: [:create, :incomplete] + before_action :find_financial_link, except: %i[create incomplete] def show @items = @financial_link.bank_transactions.map do |bt| @@ -37,7 +37,7 @@ class Finance::FinancialLinksController < Finance::BaseController def create @financial_link = FinancialLink.first_unused_or_create - if params[:bank_transaction] then + if params[:bank_transaction] bank_transaction = BankTransaction.find(params[:bank_transaction]) bank_transaction.update_attribute :financial_link, @financial_link end @@ -72,14 +72,16 @@ class Finance::FinancialLinksController < Finance::BaseController def create_financial_transaction financial_transaction = FinancialTransaction.new(financial_transaction_params) - financial_transaction.ordergroup.add_financial_transaction! financial_transaction.amount, financial_transaction.note, current_user, financial_transaction.financial_transaction_type, @financial_link + financial_transaction.ordergroup.add_financial_transaction! financial_transaction.amount, + financial_transaction.note, current_user, financial_transaction.financial_transaction_type, @financial_link redirect_to finance_link_url(@financial_link), notice: t('.notice') - rescue => error - redirect_to finance_link_url(@financial_link), alert: t('errors.general_msg', msg: error) + rescue StandardError => e + redirect_to finance_link_url(@financial_link), alert: t('errors.general_msg', msg: e) end def index_financial_transaction - @financial_transactions = FinancialTransaction.without_financial_link.includes(:financial_transaction_type, :ordergroup) + @financial_transactions = FinancialTransaction.without_financial_link.includes(:financial_transaction_type, + :ordergroup) end def add_financial_transaction @@ -123,7 +125,7 @@ class Finance::FinancialLinksController < Finance::BaseController end def find_best_fitting_ordergroup_id_for_financial_link(financial_link_id) - FinancialTransaction.joins(<<-SQL).order(created_on: :desc).pluck(:ordergroup_id).first + FinancialTransaction.joins(<<-SQL).order(created_on: :desc).pick(:ordergroup_id) JOIN bank_transactions a ON financial_transactions.financial_link_id = a.financial_link_id JOIN bank_transactions b ON a.iban = b.iban AND b.financial_link_id = #{financial_link_id.to_i} SQL diff --git a/app/controllers/finance/financial_transactions_controller.rb b/app/controllers/finance/financial_transactions_controller.rb index e0c53e19..6b06cbee 100644 --- a/app/controllers/finance/financial_transactions_controller.rb +++ b/app/controllers/finance/financial_transactions_controller.rb @@ -1,22 +1,22 @@ class Finance::FinancialTransactionsController < ApplicationController before_action :authenticate_finance - before_action :find_ordergroup, :except => [:new_collection, :create_collection, :index_collection] + before_action :find_ordergroup, except: %i[new_collection create_collection index_collection] inherit_resources # belongs_to :ordergroup def index - if params['sort'] - sort = case params['sort'] - when "date" then "created_on" - when "note" then "note" - when "amount" then "amount" - when "date_reverse" then "created_on DESC" - when "note_reverse" then "note DESC" - when "amount_reverse" then "amount DESC" + sort = if params['sort'] + case params['sort'] + when 'date' then 'created_on' + when 'note' then 'note' + when 'amount' then 'amount' + when 'date_reverse' then 'created_on DESC' + when 'note_reverse' then 'note DESC' + when 'amount_reverse' then 'amount DESC' end - else - sort = "created_on DESC" - end + else + 'created_on DESC' + end @q = FinancialTransaction.ransack(params[:q]) @financial_transactions_all = @q.result(distinct: true).includes(:user).order(sort) @@ -26,9 +26,11 @@ class Finance::FinancialTransactionsController < ApplicationController @financial_transactions = @financial_transactions_all.page(params[:page]).per(@per_page) respond_to do |format| - format.js; format.html { render } + format.js + format.html { render } format.csv do - send_data FinancialTransactionsCsv.new(@financial_transactions_all).to_csv, filename: 'transactions.csv', type: 'text/csv' + send_data FinancialTransactionsCsv.new(@financial_transactions_all).to_csv, filename: 'transactions.csv', + type: 'text/csv' end end end @@ -38,11 +40,11 @@ class Finance::FinancialTransactionsController < ApplicationController end def new - if @ordergroup - @financial_transaction = @ordergroup.financial_transactions.build - else - @financial_transaction = FinancialTransaction.new - end + @financial_transaction = if @ordergroup + @ordergroup.financial_transactions.build + else + FinancialTransaction.new + end end def create @@ -53,16 +55,18 @@ class Finance::FinancialTransactionsController < ApplicationController else @financial_transaction.save! end - redirect_to finance_group_transactions_path(@ordergroup), notice: I18n.t('finance.financial_transactions.controller.create.notice') - rescue ActiveRecord::RecordInvalid => error - flash.now[:alert] = error.message - render :action => :new + redirect_to finance_group_transactions_path(@ordergroup), + notice: I18n.t('finance.financial_transactions.controller.create.notice') + rescue ActiveRecord::RecordInvalid => e + flash.now[:alert] = e.message + render action: :new end def destroy transaction = FinancialTransaction.find(params[:id]) transaction.revert!(current_user) - redirect_to finance_group_transactions_path(transaction.ordergroup), notice: t('finance.financial_transactions.controller.destroy.notice') + redirect_to finance_group_transactions_path(transaction.ordergroup), + notice: t('finance.financial_transactions.controller.destroy.notice') end def new_collection @@ -88,17 +92,17 @@ class Finance::FinancialTransactionsController < ApplicationController params[:financial_transactions].each do |trans| # ignore empty amount fields ... - unless trans[:amount].blank? - amount = LocalizeInput.parse(trans[:amount]).to_f - note = params[:note] - ordergroup = Ordergroup.find(trans[:ordergroup_id]) - if params[:set_balance] - note += " (#{amount})" - amount -= ordergroup.financial_transaction_class_balance(type.financial_transaction_class) - end - ordergroup.add_financial_transaction!(amount, note, @current_user, type, financial_link) - foodcoop_amount -= amount + next if trans[:amount].blank? + + amount = LocalizeInput.parse(trans[:amount]).to_f + note = params[:note] + ordergroup = Ordergroup.find(trans[:ordergroup_id]) + if params[:set_balance] + note += " (#{amount})" + amount -= ordergroup.financial_transaction_class_balance(type.financial_transaction_class) end + ordergroup.add_financial_transaction!(amount, note, @current_user, type, financial_link) + foodcoop_amount -= amount end if params[:create_foodcoop_transaction] @@ -107,7 +111,7 @@ class Finance::FinancialTransactionsController < ApplicationController user: @current_user, amount: foodcoop_amount, note: params[:note], - financial_link: financial_link, + financial_link: financial_link }) ft.save! end @@ -117,8 +121,8 @@ class Finance::FinancialTransactionsController < ApplicationController url = financial_link ? finance_link_url(financial_link.id) : finance_ordergroups_url redirect_to url, notice: I18n.t('finance.financial_transactions.controller.create_collection.notice') - rescue => error - flash.now[:alert] = error.message + rescue StandardError => e + flash.now[:alert] = e.message render action: :new_collection end diff --git a/app/controllers/finance/invoices_controller.rb b/app/controllers/finance/invoices_controller.rb index d981277b..d70b92ec 100644 --- a/app/controllers/finance/invoices_controller.rb +++ b/app/controllers/finance/invoices_controller.rb @@ -1,15 +1,16 @@ class Finance::InvoicesController < ApplicationController before_action :authenticate_finance_or_invoices - before_action :find_invoice, only: [:show, :edit, :update, :destroy] - before_action :ensure_can_edit, only: [:edit, :update, :destroy] + before_action :find_invoice, only: %i[show edit update destroy] + before_action :ensure_can_edit, only: %i[edit update destroy] def index @invoices_all = Invoice.includes(:supplier, :deliveries, :orders).order('date DESC') @invoices = @invoices_all.page(params[:page]).per(@per_page) respond_to do |format| - format.js; format.html { render } + format.js + format.html { render } format.csv do send_data InvoicesCsv.new(@invoices_all).to_csv, filename: 'invoices.csv', type: 'text/csv' end @@ -20,11 +21,10 @@ class Finance::InvoicesController < ApplicationController @suppliers = Supplier.includes(:invoices).where('invoices.paid_on IS NULL').references(:invoices) end - def show - end + def show; end def new - @invoice = Invoice.new :supplier_id => params[:supplier_id] + @invoice = Invoice.new supplier_id: params[:supplier_id] @invoice.deliveries << Delivery.find_by_id(params[:delivery_id]) if params[:delivery_id] @invoice.orders << Order.find_by_id(params[:order_id]) if params[:order_id] fill_deliveries_and_orders_collection @invoice.id, @invoice.supplier_id @@ -36,12 +36,14 @@ class Finance::InvoicesController < ApplicationController def form_on_supplier_id_change fill_deliveries_and_orders_collection params[:invoice_id], params[:supplier_id] - render :layout => false + render layout: false end def fill_deliveries_and_orders_collection(invoice_id, supplier_id) - @deliveries_collection = Delivery.where('invoice_id = ? OR (invoice_id IS NULL AND supplier_id = ?)', invoice_id, supplier_id).order(date: :desc).limit(25) - @orders_collection = Order.where('invoice_id = ? OR (invoice_id IS NULL AND supplier_id = ?)', invoice_id, supplier_id).order(ends: :desc).limit(25) + @deliveries_collection = Delivery.where('invoice_id = ? OR (invoice_id IS NULL AND supplier_id = ?)', invoice_id, + supplier_id).order(date: :desc).limit(25) + @orders_collection = Order.where('invoice_id = ? OR (invoice_id IS NULL AND supplier_id = ?)', invoice_id, + supplier_id).order(ends: :desc).limit(25) end def create @@ -58,7 +60,7 @@ class Finance::InvoicesController < ApplicationController end else fill_deliveries_and_orders_collection @invoice.id, @invoice.supplier_id - render :action => "new" + render action: 'new' end end @@ -81,7 +83,7 @@ class Finance::InvoicesController < ApplicationController @invoice = Invoice.find(params[:invoice_id]) type = MIME::Types[@invoice.attachment_mime].first filename = "invoice_#{@invoice.id}_attachment.#{type.preferred_extension}" - send_data(@invoice.attachment_data, :filename => filename, :type => type) + send_data(@invoice.attachment_data, filename: filename, type: type) end private @@ -92,8 +94,8 @@ class Finance::InvoicesController < ApplicationController # Returns true if @current_user can edit the invoice.. def ensure_can_edit - unless @invoice.user_can_edit?(current_user) - deny_access - end + return if @invoice.user_can_edit?(current_user) + + deny_access end end diff --git a/app/controllers/finance/ordergroups_controller.rb b/app/controllers/finance/ordergroups_controller.rb index cb661571..a8836f6b 100644 --- a/app/controllers/finance/ordergroups_controller.rb +++ b/app/controllers/finance/ordergroups_controller.rb @@ -1,11 +1,11 @@ class Finance::OrdergroupsController < Finance::BaseController def index - m = /^(?name|sum_of_class_\d+)(?_reverse)?$/.match params["sort"] + m = /^(?name|sum_of_class_\d+)(?_reverse)?$/.match params['sort'] if m sort = m[:col] sort += ' DESC' if m[:reverse] else - sort = "name" + sort = 'name' end @ordergroups = Ordergroup.undeleted.order(sort) diff --git a/app/controllers/foodcoop/ordergroups_controller.rb b/app/controllers/foodcoop/ordergroups_controller.rb index 6940a376..05dfe9cb 100644 --- a/app/controllers/foodcoop/ordergroups_controller.rb +++ b/app/controllers/foodcoop/ordergroups_controller.rb @@ -1,20 +1,16 @@ class Foodcoop::OrdergroupsController < ApplicationController def index - @ordergroups = Ordergroup.undeleted.sort_by_param(params["sort"]) + @ordergroups = Ordergroup.undeleted.sort_by_param(params['sort']) - unless params[:name].blank? # Search by name - @ordergroups = @ordergroups.where('name LIKE ?', "%#{params[:name]}%") - end + @ordergroups = @ordergroups.where('name LIKE ?', "%#{params[:name]}%") if params[:name].present? # Search by name - if params[:only_active] # Select only active groups - @ordergroups = @ordergroups.active - end + @ordergroups = @ordergroups.active if params[:only_active] # Select only active groups @ordergroups = @ordergroups.page(params[:page]).per(@per_page) respond_to do |format| format.html # index.html.erb - format.js { render :layout => false } + format.js { render layout: false } end end end diff --git a/app/controllers/foodcoop/users_controller.rb b/app/controllers/foodcoop/users_controller.rb index 196f1be8..17da7ccf 100644 --- a/app/controllers/foodcoop/users_controller.rb +++ b/app/controllers/foodcoop/users_controller.rb @@ -1,19 +1,20 @@ class Foodcoop::UsersController < ApplicationController def index - @users = User.undeleted.sort_by_param(params["sort"]) + @users = User.undeleted.sort_by_param(params['sort']) # if somebody uses the search field: - @users = @users.natural_search(params[:user_name]) unless params[:user_name].blank? + @users = @users.natural_search(params[:user_name]) if params[:user_name].present? if params[:ordergroup_name] - @users = @users.joins(:groups).where("groups.type = 'Ordergroup' AND groups.name LIKE ?", "%#{params[:ordergroup_name]}%") + @users = @users.joins(:groups).where("groups.type = 'Ordergroup' AND groups.name LIKE ?", + "%#{params[:ordergroup_name]}%") end @users = @users.page(params[:page]).per(@per_page) respond_to do |format| format.html # index.html.haml - format.js { render :layout => false } # index.js.erb + format.js { render layout: false } # index.js.erb end end end diff --git a/app/controllers/foodcoop/workgroups_controller.rb b/app/controllers/foodcoop/workgroups_controller.rb index e0f571be..8fd5f423 100644 --- a/app/controllers/foodcoop/workgroups_controller.rb +++ b/app/controllers/foodcoop/workgroups_controller.rb @@ -1,9 +1,9 @@ class Foodcoop::WorkgroupsController < ApplicationController before_action :authenticate_membership_or_admin, - :except => [:index] + except: [:index] def index - @workgroups = Workgroup.order("name") + @workgroups = Workgroup.order('name') end def edit @@ -13,9 +13,9 @@ class Foodcoop::WorkgroupsController < ApplicationController def update @workgroup = Workgroup.find(params[:id]) if @workgroup.update(params[:workgroup]) - redirect_to foodcoop_workgroups_url, :notice => I18n.t('workgroups.update.notice') + redirect_to foodcoop_workgroups_url, notice: I18n.t('workgroups.update.notice') else - render :action => 'edit' + render action: 'edit' end end end diff --git a/app/controllers/group_order_articles_controller.rb b/app/controllers/group_order_articles_controller.rb index 5aa50a87..5f58c48a 100644 --- a/app/controllers/group_order_articles_controller.rb +++ b/app/controllers/group_order_articles_controller.rb @@ -1,6 +1,6 @@ class GroupOrderArticlesController < ApplicationController before_action :authenticate_finance - before_action :find_group_order_article, except: [:new, :create] + before_action :find_group_order_article, except: %i[new create] layout false # We only use this controller to server js snippets, no need for layout rendering diff --git a/app/controllers/group_orders_controller.rb b/app/controllers/group_orders_controller.rb index 686f0617..e5a442aa 100644 --- a/app/controllers/group_orders_controller.rb +++ b/app/controllers/group_orders_controller.rb @@ -3,9 +3,9 @@ class GroupOrdersController < ApplicationController # Security before_action :ensure_ordergroup_member - before_action :ensure_open_order, :only => [:new, :create, :edit, :update, :order, :stock_order, :saveOrder] - before_action :ensure_my_group_order, only: [:show, :edit, :update] - before_action :enough_apples?, only: [:new, :create] + before_action :ensure_open_order, only: %i[new create edit update order stock_order saveOrder] + before_action :ensure_my_group_order, only: %i[show edit update] + before_action :enough_apples?, only: %i[new create] # Index page. def index @@ -13,9 +13,17 @@ class GroupOrdersController < ApplicationController @finished_not_closed_orders_including_group_order = Order.finished_not_closed.ordergroup_group_orders_map(@ordergroup) end + def show + @order = @group_order.order + end + def new ordergroup = params[:stock_order] ? nil : @ordergroup - @group_order = @order.group_orders.build(:ordergroup => ordergroup, :updated_by => current_user) + @group_order = @order.group_orders.build(ordergroup: ordergroup, updated_by: current_user) + @ordering_data = @group_order.load_data + end + + def edit @ordering_data = @group_order.load_data end @@ -23,34 +31,26 @@ class GroupOrdersController < ApplicationController @group_order = GroupOrder.new(params[:group_order]) begin @group_order.save_ordering! - redirect_to group_order_url(@group_order), :notice => I18n.t('group_orders.create.notice') + redirect_to group_order_url(@group_order), notice: I18n.t('group_orders.create.notice') rescue ActiveRecord::StaleObjectError - redirect_to group_orders_url, :alert => I18n.t('group_orders.create.error_stale') - rescue => exception - logger.error('Failed to update order: ' + exception.message) - redirect_to group_orders_url, :alert => I18n.t('group_orders.create.error_general') + redirect_to group_orders_url, alert: I18n.t('group_orders.create.error_stale') + rescue StandardError => e + logger.error('Failed to update order: ' + e.message) + redirect_to group_orders_url, alert: I18n.t('group_orders.create.error_general') end end - def show - @order = @group_order.order - end - - def edit - @ordering_data = @group_order.load_data - end - def update @group_order.attributes = params[:group_order] @group_order.updated_by = current_user begin @group_order.save_ordering! - redirect_to group_order_url(@group_order), :notice => I18n.t('group_orders.update.notice') + redirect_to group_order_url(@group_order), notice: I18n.t('group_orders.update.notice') rescue ActiveRecord::StaleObjectError - redirect_to group_orders_url, :alert => I18n.t('group_orders.update.error_stale') - rescue => exception - logger.error('Failed to update order: ' + exception.message) - redirect_to group_orders_url, :alert => I18n.t('group_orders.update.error_general') + redirect_to group_orders_url, alert: I18n.t('group_orders.update.error_stale') + rescue StandardError => e + logger.error('Failed to update order: ' + e.message) + redirect_to group_orders_url, alert: I18n.t('group_orders.update.error_general') end end @@ -74,16 +74,16 @@ class GroupOrdersController < ApplicationController # Used as a :before_action by OrdersController. def ensure_ordergroup_member @ordergroup = @current_user.ordergroup - if @ordergroup.nil? - redirect_to root_url, :alert => I18n.t('group_orders.errors.no_member') - end + return unless @ordergroup.nil? + + redirect_to root_url, alert: I18n.t('group_orders.errors.no_member') end def ensure_open_order - @order = Order.includes([:supplier, :order_articles]).find(order_id_param) + @order = Order.includes(%i[supplier order_articles]).find(order_id_param) unless @order.open? flash[:notice] = I18n.t('group_orders.errors.closed') - redirect_to :action => 'index' + redirect_to action: 'index' end rescue ActiveRecord::RecordNotFound redirect_to group_orders_url, alert: I18n.t('group_orders.errors.notfound') @@ -91,17 +91,17 @@ class GroupOrdersController < ApplicationController def ensure_my_group_order @group_order = GroupOrder.find(params[:id]) - if @group_order.ordergroup != @ordergroup && (@group_order.ordergroup || !current_user.role_orders?) - redirect_to group_orders_url, alert: I18n.t('group_orders.errors.notfound') - end + return unless @group_order.ordergroup != @ordergroup && (@group_order.ordergroup || !current_user.role_orders?) + + redirect_to group_orders_url, alert: I18n.t('group_orders.errors.notfound') end def enough_apples? - if @ordergroup.not_enough_apples? - redirect_to group_orders_url, - alert: t('not_enough_apples', scope: 'group_orders.messages', apples: @ordergroup.apples, - stop_ordering_under: FoodsoftConfig[:stop_ordering_under]) - end + return unless @ordergroup.not_enough_apples? + + redirect_to group_orders_url, + alert: t('not_enough_apples', scope: 'group_orders.messages', apples: @ordergroup.apples, + stop_ordering_under: FoodsoftConfig[:stop_ordering_under]) end def order_id_param diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb index a3d9cd53..f40fb6fb 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -9,8 +9,7 @@ class HomeController < ApplicationController @unassigned_tasks = Task.order(:due_date).next_unassigned_tasks_for(current_user) end - def profile - end + def profile; end def reference_calculator if current_user.ordergroup @@ -36,40 +35,43 @@ class HomeController < ApplicationController @user = @current_user @ordergroup = @user.ordergroup - unless @ordergroup.nil? + if @ordergroup.nil? + redirect_to root_path, alert: I18n.t('home.no_ordergroups') + else @ordergroup = Ordergroup.include_transaction_class_sum.find(@ordergroup.id) - if params['sort'] - sort = case params['sort'] - when "date" then "created_on" - when "note" then "note" - when "amount" then "amount" - when "date_reverse" then "created_on DESC" - when "note_reverse" then "note DESC" - when "amount_reverse" then "amount DESC" + sort = if params['sort'] + case params['sort'] + when 'date' then 'created_on' + when 'note' then 'note' + when 'amount' then 'amount' + when 'date_reverse' then 'created_on DESC' + when 'note_reverse' then 'note DESC' + when 'amount_reverse' then 'amount DESC' end - else - sort = "created_on DESC" - end + else + 'created_on DESC' + end @financial_transactions = @ordergroup.financial_transactions.visible.page(params[:page]).per(@per_page).order(sort) - @financial_transactions = @financial_transactions.where('financial_transactions.note LIKE ?', "%#{params[:query]}%") if params[:query].present? + if params[:query].present? + @financial_transactions = @financial_transactions.where('financial_transactions.note LIKE ?', + "%#{params[:query]}%") + end - else - redirect_to root_path, alert: I18n.t('home.no_ordergroups') end end # cancel personal memberships direct from the myProfile-page def cancel_membership - if params[:membership_id] - membership = @current_user.memberships.find(params[:membership_id]) - else - membership = @current_user.memberships.find_by_group_id!(params[:group_id]) - end + membership = if params[:membership_id] + @current_user.memberships.find(params[:membership_id]) + else + @current_user.memberships.find_by_group_id!(params[:group_id]) + end membership.destroy - redirect_to my_profile_path, notice: I18n.t('home.ordergroup_cancelled', :group => membership.group.name) + redirect_to my_profile_path, notice: I18n.t('home.ordergroup_cancelled', group: membership.group.name) end protected @@ -82,8 +84,8 @@ class HomeController < ApplicationController end def ordergroup_params - if params[:user][:ordergroup] - params.require(:user).require(:ordergroup).permit(:contact_address) - end + return unless params[:user][:ordergroup] + + params.require(:user).require(:ordergroup).permit(:contact_address) end end diff --git a/app/controllers/invites_controller.rb b/app/controllers/invites_controller.rb index 37fc757b..266a1de5 100644 --- a/app/controllers/invites_controller.rb +++ b/app/controllers/invites_controller.rb @@ -3,7 +3,7 @@ class InvitesController < ApplicationController before_action -> { require_config_disabled :disable_invite } def new - @invite = Invite.new(:user => @current_user, :group => @group) + @invite = Invite.new(user: @current_user, group: @group) end def create @@ -27,6 +27,10 @@ class InvitesController < ApplicationController protected def authenticate_membership_or_admin_for_invites - authenticate_membership_or_admin((params[:invite][:group_id] rescue params[:id])) + authenticate_membership_or_admin(begin + params[:invite][:group_id] + rescue StandardError + params[:id] + end) end end diff --git a/app/controllers/login_controller.rb b/app/controllers/login_controller.rb index 052231c5..4c2fd95b 100644 --- a/app/controllers/login_controller.rb +++ b/app/controllers/login_controller.rb @@ -1,6 +1,6 @@ class LoginController < ApplicationController skip_before_action :authenticate # no authentication since this is the login page - before_action :validate_token, :only => [:new_password, :update_password] + before_action :validate_token, only: %i[new_password update_password] # Display the form to enter an email address requesting a token to set a new password. def forgot_password @@ -9,20 +9,17 @@ class LoginController < ApplicationController # Sends an email to a user with the token that allows setting a new password through action "password". def reset_password - if request.get? || params[:user].nil? # Catch for get request and give better error message. - redirect_to forgot_password_url, alert: I18n.t('errors.general_again') and return - end + redirect_to forgot_password_url, alert: I18n.t('errors.general_again') and return if request.get? || params[:user].nil? # Catch for get request and give better error message. if (user = User.undeleted.find_by_email(params[:user][:email])) user.request_password_reset! end - redirect_to login_url, :notice => I18n.t('login.controller.reset_password.notice') + redirect_to login_url, notice: I18n.t('login.controller.reset_password.notice') end # Set a new password with a token from the password reminder email. # Called with params :id => User.id and :token => User.reset_password_token to specify a new password. - def new_password - end + def new_password; end # Sets a new password. # Called with params :id => User.id and :token => User.reset_password_token to specify a new password. @@ -32,7 +29,7 @@ class LoginController < ApplicationController @user.reset_password_token = nil @user.reset_password_expires = nil @user.save - redirect_to login_url, :notice => I18n.t('login.controller.update_password.notice') + redirect_to login_url, notice: I18n.t('login.controller.update_password.notice') else render :new_password end @@ -50,14 +47,14 @@ class LoginController < ApplicationController @user = User.new(params[:user]) @user.email = @invite.email if @user.save - Membership.new(:user => @user, :group => @invite.group).save! + Membership.new(user: @user, group: @invite.group).save! @invite.destroy session[:locale] = @user.locale redirect_to login_url, notice: I18n.t('login.controller.accept_invitation.notice') end end else - @user = User.new(:email => @invite.email) + @user = User.new(email: @invite.email) end end @@ -65,8 +62,8 @@ class LoginController < ApplicationController def validate_token @user = User.find_by_id_and_reset_password_token(params[:id], params[:token]) - if (@user.nil? || @user.reset_password_expires < Time.now) - redirect_to forgot_password_url, alert: I18n.t('login.controller.error_token_invalid') - end + return unless @user.nil? || @user.reset_password_expires < Time.now + + redirect_to forgot_password_url, alert: I18n.t('login.controller.error_token_invalid') end end diff --git a/app/controllers/order_articles_controller.rb b/app/controllers/order_articles_controller.rb index 0552269d..43a0ea14 100644 --- a/app/controllers/order_articles_controller.rb +++ b/app/controllers/order_articles_controller.rb @@ -1,7 +1,7 @@ class OrderArticlesController < ApplicationController before_action :fetch_order, except: :destroy - before_action :authenticate_finance_or_invoices, except: [:new, :create] - before_action :authenticate_finance_orders_or_pickup, except: [:edit, :update, :destroy] + before_action :authenticate_finance_or_invoices, except: %i[new create] + before_action :authenticate_finance_orders_or_pickup, except: %i[edit update destroy] layout false # We only use this controller to serve js snippets, no need for layout rendering @@ -9,28 +9,26 @@ class OrderArticlesController < ApplicationController @order_article = @order.order_articles.build(params[:order_article]) end + def edit + @order_article = OrderArticle.find(params[:id]) + end + def create # The article may be ordered with zero units - in that case do not complain. # If order_article is ordered and a new order_article is created, an error message will be # given mentioning that the article already exists, which is desired. - @order_article = @order.order_articles.where(:article_id => params[:order_article][:article_id]).first - unless @order_article && @order_article.units_to_order == 0 - @order_article = @order.order_articles.build(params[:order_article]) - end + @order_article = @order.order_articles.where(article_id: params[:order_article][:article_id]).first + @order_article = @order.order_articles.build(params[:order_article]) unless @order_article && @order_article.units_to_order == 0 @order_article.save! - rescue + rescue StandardError render action: :new end - def edit - @order_article = OrderArticle.find(params[:id]) - end - def update @order_article = OrderArticle.find(params[:id]) begin @order_article.update_article_and_price!(params[:order_article], params[:article], params[:article_price]) - rescue + rescue StandardError render action: :edit end end diff --git a/app/controllers/order_comments_controller.rb b/app/controllers/order_comments_controller.rb index 39067577..3583bb0e 100644 --- a/app/controllers/order_comments_controller.rb +++ b/app/controllers/order_comments_controller.rb @@ -1,15 +1,15 @@ class OrderCommentsController < ApplicationController def new @order = Order.find(params[:order_id]) - @order_comment = @order.comments.build(:user => current_user) + @order_comment = @order.comments.build(user: current_user) end def create @order_comment = OrderComment.new(params[:order_comment]) if @order_comment.save - render :layout => false + render layout: false else - render :action => :new, :layout => false + render action: :new, layout: false end end end diff --git a/app/controllers/orders_controller.rb b/app/controllers/orders_controller.rb index cfa7cef6..bc2d9195 100644 --- a/app/controllers/orders_controller.rb +++ b/app/controllers/orders_controller.rb @@ -5,25 +5,26 @@ class OrdersController < ApplicationController include Concerns::SendOrderPdf before_action :authenticate_pickups_or_orders - before_action :authenticate_orders, except: [:receive, :receive_on_order_article_create, :receive_on_order_article_update, :show] - before_action :remove_empty_article, only: [:create, :update] + before_action :authenticate_orders, + except: %i[receive receive_on_order_article_create receive_on_order_article_update show] + before_action :remove_empty_article, only: %i[create update] # List orders def index @open_orders = Order.open.includes(:supplier) @finished_orders = Order.finished_not_closed.includes(:supplier) @per_page = 15 - if params['sort'] - sort = case params['sort'] - when "supplier" then "suppliers.name, ends DESC" - when "pickup" then "pickup DESC" - when "ends" then "ends DESC" - when "supplier_reverse" then "suppliers.name DESC" - when "ends_reverse" then "ends" + sort = if params['sort'] + case params['sort'] + when 'supplier' then 'suppliers.name, ends DESC' + when 'pickup' then 'pickup DESC' + when 'ends' then 'ends DESC' + when 'supplier_reverse' then 'suppliers.name DESC' + when 'ends_reverse' then 'ends' end - else - sort = "ends DESC" - end + else + 'ends DESC' + end @suppliers = Supplier.having_articles.order('suppliers.name') @orders = Order.closed.includes(:supplier).reorder(sort).page(params[:page]).per(@per_page) end @@ -43,7 +44,7 @@ class OrdersController < ApplicationController respond_to do |format| format.html format.js do - render :layout => false + render layout: false end format.pdf do send_order_pdf @order, params[:document] @@ -66,8 +67,14 @@ class OrdersController < ApplicationController else @order = Order.new(supplier_id: params[:supplier_id]).init_dates end - rescue => error - redirect_to orders_url, alert: t('errors.general_msg', msg: error.message) + rescue StandardError => e + redirect_to orders_url, alert: t('errors.general_msg', msg: e.message) + end + + # Page to edit an exsiting order. + # editing finished orders is done in FinanceController + def edit + @order = Order.includes(:articles).find(params[:id]) end # Save a new order. @@ -81,31 +88,25 @@ class OrdersController < ApplicationController redirect_to @order else logger.debug "[debug] order errors: #{@order.errors.messages}" - render :action => 'new' + render action: 'new' end end - # Page to edit an exsiting order. - # editing finished orders is done in FinanceController - def edit - @order = Order.includes(:articles).find(params[:id]) - end - # Update an existing order. def update @order = Order.find params[:id] if @order.update(params[:order].merge(updated_by: current_user)) flash[:notice] = I18n.t('orders.update.notice') - redirect_to :action => 'show', :id => @order + redirect_to action: 'show', id: @order else - render :action => 'edit' + render action: 'edit' end end # Delete an order. def destroy Order.find(params[:id]).destroy - redirect_to :action => 'index' + redirect_to action: 'index' end # Finish a current order. @@ -113,8 +114,8 @@ class OrdersController < ApplicationController order = Order.find(params[:id]) order.finish!(@current_user) redirect_to order, notice: I18n.t('orders.finish.notice') - rescue => error - redirect_to orders_url, alert: I18n.t('errors.general_msg', :msg => error.message) + rescue StandardError => e + redirect_to orders_url, alert: I18n.t('errors.general_msg', msg: e.message) end # Send a order to the supplier. @@ -122,20 +123,18 @@ class OrdersController < ApplicationController order = Order.find(params[:id]) order.send_to_supplier!(@current_user) redirect_to order, notice: I18n.t('orders.send_to_supplier.notice') - rescue => error - redirect_to order, alert: I18n.t('errors.general_msg', :msg => error.message) + rescue StandardError => e + redirect_to order, alert: I18n.t('errors.general_msg', msg: e.message) end def receive @order = Order.find(params[:id]) - unless request.post? - @order_articles = @order.order_articles.ordered_or_member.includes(:article).order('articles.order_number, articles.name') - else + if request.post? Order.transaction do s = update_order_amounts @order.update_attribute(:state, 'received') if @order.state != 'received' - flash[:notice] = (s ? I18n.t('orders.receive.notice', :msg => s) : I18n.t('orders.receive.notice_none')) + flash[:notice] = (s ? I18n.t('orders.receive.notice', msg: s) : I18n.t('orders.receive.notice_none')) end NotifyReceivedOrderJob.perform_later(@order) if current_user.role_orders? || current_user.role_finance? @@ -145,23 +144,25 @@ class OrdersController < ApplicationController else redirect_to receive_order_path(@order) end + else + @order_articles = @order.order_articles.ordered_or_member.includes(:article).order('articles.order_number, articles.name') end end def receive_on_order_article_create # See publish/subscribe design pattern in /doc. @order_article = OrderArticle.find(params[:order_article_id]) - render :layout => false + render layout: false end def receive_on_order_article_update # See publish/subscribe design pattern in /doc. @order_article = OrderArticle.find(params[:order_article_id]) - render :layout => false + render layout: false end protected def update_order_amounts - return if not params[:order_articles] + return unless params[:order_articles] # where to leave remainder during redistribution rest_to = [] @@ -176,35 +177,42 @@ class OrdersController < ApplicationController # "MySQL lock timeout exceeded" errors. It's ok to do # this article-by-article anway. params[:order_articles].each do |oa_id, oa_params| - unless oa_params.blank? - oa = OrderArticle.find(oa_id) - # update attributes; don't use update_attribute because it calls save - # which makes received_changed? not work anymore - oa.attributes = oa_params - if oa.units_received_changed? - counts[0] += 1 - unless oa.units_received.blank? - cunits[0] += oa.units_received * oa.article.unit_quantity - oacounts = oa.redistribute oa.units_received * oa.price.unit_quantity, rest_to - oacounts.each_with_index { |c, i| cunits[i + 1] += c; counts[i + 1] += 1 if c > 0 } + next if oa_params.blank? + + oa = OrderArticle.find(oa_id) + # update attributes; don't use update_attribute because it calls save + # which makes received_changed? not work anymore + oa.attributes = oa_params + if oa.units_received_changed? + counts[0] += 1 + if oa.units_received.present? + cunits[0] += oa.units_received * oa.article.unit_quantity + oacounts = oa.redistribute oa.units_received * oa.price.unit_quantity, rest_to + oacounts.each_with_index do |c, i| + cunits[i + 1] += c + counts[i + 1] += 1 if c > 0 end end - oa.save! end + oa.save! end return nil if counts[0] == 0 notice = [] notice << I18n.t('orders.update_order_amounts.msg1', count: counts[0], units: cunits[0]) - notice << I18n.t('orders.update_order_amounts.msg2', count: counts[1], units: cunits[1]) if params[:rest_to_tolerance] + if params[:rest_to_tolerance] + notice << I18n.t('orders.update_order_amounts.msg2', count: counts[1], + units: cunits[1]) + end notice << I18n.t('orders.update_order_amounts.msg3', count: counts[2], units: cunits[2]) if params[:rest_to_stock] if counts[3] > 0 || cunits[3] > 0 - notice << I18n.t('orders.update_order_amounts.msg4', count: counts[3], units: cunits[3]) + notice << I18n.t('orders.update_order_amounts.msg4', count: counts[3], + units: cunits[3]) end notice.join(', ') end def remove_empty_article - params[:order][:article_ids].reject!(&:blank?) if params[:order] && params[:order][:article_ids] + params[:order][:article_ids].compact_blank! if params[:order] && params[:order][:article_ids] end end diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index f3c50e2a..22750360 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -12,10 +12,10 @@ class SessionsController < ApplicationController user = User.authenticate(params[:nick], params[:password]) if user user.update_attribute(:last_login, Time.now) - login_and_redirect_to_return_to user, :notice => I18n.t('sessions.logged_in') + login_and_redirect_to_return_to user, notice: I18n.t('sessions.logged_in') else flash.now.alert = I18n.t(FoodsoftConfig[:use_nick] ? 'sessions.login_invalid_nick' : 'sessions.login_invalid_email') - render "new" + render 'new' end end @@ -24,7 +24,7 @@ class SessionsController < ApplicationController if FoodsoftConfig[:logout_redirect_url].present? redirect_to FoodsoftConfig[:logout_redirect_url] else - redirect_to login_url, :notice => I18n.t('sessions.logged_out') + redirect_to login_url, notice: I18n.t('sessions.logged_out') end end diff --git a/app/controllers/stock_takings_controller.rb b/app/controllers/stock_takings_controller.rb index bdf1dc77..e12af6f9 100644 --- a/app/controllers/stock_takings_controller.rb +++ b/app/controllers/stock_takings_controller.rb @@ -7,21 +7,21 @@ class StockTakingsController < ApplicationController def new @stock_taking = StockTaking.new - StockArticle.undeleted.each { |a| @stock_taking.stock_changes.build(:stock_article => a) } + StockArticle.undeleted.each { |a| @stock_taking.stock_changes.build(stock_article: a) } end def new_on_stock_article_create # See publish/subscribe design pattern in /doc. stock_article = StockArticle.find(params[:stock_article_id]) - @stock_change = StockChange.new(:stock_article => stock_article) + @stock_change = StockChange.new(stock_article: stock_article) - render :layout => false + render layout: false end def create - create!(:notice => I18n.t('stock_takings.create.notice')) + create!(notice: I18n.t('stock_takings.create.notice')) end def update - update!(:notice => I18n.t('stock_takings.update.notice')) + update!(notice: I18n.t('stock_takings.update.notice')) end end diff --git a/app/controllers/stockit_controller.rb b/app/controllers/stockit_controller.rb index 6dd1511e..8f9b3b3d 100644 --- a/app/controllers/stockit_controller.rb +++ b/app/controllers/stockit_controller.rb @@ -7,57 +7,13 @@ class StockitController < ApplicationController def index_on_stock_article_create # See publish/subscribe design pattern in /doc. @stock_article = StockArticle.find(params[:id]) - render :layout => false + render layout: false end def index_on_stock_article_update # See publish/subscribe design pattern in /doc. @stock_article = StockArticle.find(params[:id]) - render :layout => false - end - - # three possibilites to fill a new_stock_article form - # (1) start from blank or use params - def new - @stock_article = StockArticle.new(params[:stock_article]) - - render :layout => false - end - - # (2) StockArticle as template - def copy - @stock_article = StockArticle.find(params[:stock_article_id]).dup - - render :layout => false - end - - # (3) non-stock Article as template - def derive - @stock_article = Article.find(params[:old_article_id]).becomes(StockArticle).dup - - render :layout => false - end - - def create - @stock_article = StockArticle.new({ quantity: 0 }.merge(params[:stock_article])) - @stock_article.save! - render :layout => false - rescue ActiveRecord::RecordInvalid - render :action => 'new', :layout => false - end - - def edit - @stock_article = StockArticle.find(params[:id]) - - render :layout => false - end - - def update - @stock_article = StockArticle.find(params[:id]) - @stock_article.update!(params[:stock_article]) - render :layout => false - rescue ActiveRecord::RecordInvalid - render :action => 'edit', :layout => false + render layout: false end def show @@ -65,24 +21,68 @@ class StockitController < ApplicationController @stock_changes = @stock_article.stock_changes.order('stock_changes.created_at DESC') end + # three possibilites to fill a new_stock_article form + # (1) start from blank or use params + def new + @stock_article = StockArticle.new(params[:stock_article]) + + render layout: false + end + + # (2) StockArticle as template + def copy + @stock_article = StockArticle.find(params[:stock_article_id]).dup + + render layout: false + end + + # (3) non-stock Article as template + def derive + @stock_article = Article.find(params[:old_article_id]).becomes(StockArticle).dup + + render layout: false + end + + def edit + @stock_article = StockArticle.find(params[:id]) + + render layout: false + end + + def create + @stock_article = StockArticle.new({ quantity: 0 }.merge(params[:stock_article])) + @stock_article.save! + render layout: false + rescue ActiveRecord::RecordInvalid + render action: 'new', layout: false + end + + def update + @stock_article = StockArticle.find(params[:id]) + @stock_article.update!(params[:stock_article]) + render layout: false + rescue ActiveRecord::RecordInvalid + render action: 'edit', layout: false + end + def show_on_stock_article_update # See publish/subscribe design pattern in /doc. @stock_article = StockArticle.find(params[:id]) - render :layout => false + render layout: false end def destroy @stock_article = StockArticle.find(params[:id]) @stock_article.mark_as_deleted - render :layout => false - rescue => error - render :partial => "destroy_fail", :layout => false, - :locals => { :fail_msg => I18n.t('errors.general_msg', :msg => error.message) } + render layout: false + rescue StandardError => e + render partial: 'destroy_fail', layout: false, + locals: { fail_msg: I18n.t('errors.general_msg', msg: e.message) } end # TODO: Fix this!! def articles_search @articles = Article.not_in_stock.limit(8).where('name LIKE ?', "%#{params[:term]}%") - render :json => @articles.map(&:name) + render json: @articles.map(&:name) end end diff --git a/app/controllers/styles_controller.rb b/app/controllers/styles_controller.rb index 5636ec03..6d3a9fd1 100644 --- a/app/controllers/styles_controller.rb +++ b/app/controllers/styles_controller.rb @@ -9,7 +9,7 @@ class StylesController < ApplicationController def foodcoop css = FoodsoftConfig[:custom_css] if css.blank? - render body: nil, content_type: 'text/css', status: 404 + render body: nil, content_type: 'text/css', status: :not_found else expires_in 1.week, public: true if params[:md5].present? render body: css, content_type: 'text/css' diff --git a/app/controllers/suppliers_controller.rb b/app/controllers/suppliers_controller.rb index e5188f8b..1f1e055d 100644 --- a/app/controllers/suppliers_controller.rb +++ b/app/controllers/suppliers_controller.rb @@ -1,5 +1,5 @@ class SuppliersController < ApplicationController - before_action :authenticate_suppliers, :except => [:index, :list] + before_action :authenticate_suppliers, except: %i[index list] helper :deliveries def index @@ -24,6 +24,10 @@ class SuppliersController < ApplicationController end end + def edit + @supplier = Supplier.find(params[:id]) + end + def create @supplier = Supplier.new(supplier_params) @supplier.supplier_category ||= SupplierCategory.first @@ -31,21 +35,17 @@ class SuppliersController < ApplicationController flash[:notice] = I18n.t('suppliers.create.notice') redirect_to suppliers_path else - render :action => 'new' + render action: 'new' end end - def edit - @supplier = Supplier.find(params[:id]) - end - def update @supplier = Supplier.find(params[:id]) if @supplier.update(supplier_params) flash[:notice] = I18n.t('suppliers.update.notice') redirect_to @supplier else - render :action => 'edit' + render action: 'edit' end end @@ -54,8 +54,8 @@ class SuppliersController < ApplicationController @supplier.mark_as_deleted flash[:notice] = I18n.t('suppliers.destroy.notice') redirect_to suppliers_path - rescue => e - flash[:error] = I18n.t('errors.general_msg', :msg => e.message) + rescue StandardError => e + flash[:error] = I18n.t('errors.general_msg', msg: e.message) redirect_to @supplier end diff --git a/app/controllers/tasks_controller.rb b/app/controllers/tasks_controller.rb index db4ca1ab..352c71ae 100644 --- a/app/controllers/tasks_controller.rb +++ b/app/controllers/tasks_controller.rb @@ -11,35 +11,33 @@ class TasksController < ApplicationController @accepted_tasks = Task.accepted_tasks_for(current_user) end - def new - @task = Task.new(current_user_id: current_user.id) - end - - def create - @task = Task.new(current_user_id: current_user.id) - @task.created_by = current_user - @task.attributes = (task_params) - if params[:periodic] - @task.periodic_task_group = PeriodicTaskGroup.new - end - if @task.save - @task.periodic_task_group.create_tasks_for_upfront_days if params[:periodic] - redirect_to tasks_url, :notice => I18n.t('tasks.create.notice') - else - render :template => "tasks/new" - end - end - def show @task = Task.find(params[:id]) end + def new + @task = Task.new(current_user_id: current_user.id) + end + def edit @task = Task.find(params[:id]) @periodic = !!params[:periodic] @task.current_user_id = current_user.id end + def create + @task = Task.new(current_user_id: current_user.id) + @task.created_by = current_user + @task.attributes = (task_params) + @task.periodic_task_group = PeriodicTaskGroup.new if params[:periodic] + if @task.save + @task.periodic_task_group.create_tasks_for_upfront_days if params[:periodic] + redirect_to tasks_url, notice: I18n.t('tasks.create.notice') + else + render template: 'tasks/new' + end + end + def update @task = Task.find(params[:id]) task_group = @task.periodic_task_group @@ -50,16 +48,14 @@ class TasksController < ApplicationController if @task.errors.empty? && @task.save task_group.update_tasks_including(@task, prev_due_date) if params[:periodic] flash[:notice] = I18n.t('tasks.update.notice') - if was_periodic && !@task.periodic? - flash[:notice] = I18n.t('tasks.update.notice_converted') - end + flash[:notice] = I18n.t('tasks.update.notice_converted') if was_periodic && !@task.periodic? if @task.workgroup redirect_to workgroup_tasks_url(workgroup_id: @task.workgroup_id) else redirect_to tasks_url end else - render :template => "tasks/edit" + render template: 'tasks/edit' end end @@ -75,7 +71,7 @@ class TasksController < ApplicationController end task.update_ordergroup_stats(user_ids) - redirect_to tasks_url, :notice => I18n.t('tasks.destroy.notice') + redirect_to tasks_url, notice: I18n.t('tasks.destroy.notice') end # assign current_user to the task and set the assignment to "accepted" @@ -85,20 +81,20 @@ class TasksController < ApplicationController if ass = task.is_assigned?(current_user) ass.update_attribute(:accepted, true) else - task.assignments.create(:user => current_user, :accepted => true) + task.assignments.create(user: current_user, accepted: true) end - redirect_to user_tasks_path, :notice => I18n.t('tasks.accept.notice') + redirect_to user_tasks_path, notice: I18n.t('tasks.accept.notice') end # deletes assignment between current_user and given taskcurrent_user_id: current_user.id def reject Task.find(params[:id]).users.delete(current_user) - redirect_to :action => "index" + redirect_to action: 'index' end def set_done Task.find(params[:id]).update_attribute :done, true - redirect_to tasks_url, :notice => I18n.t('tasks.set_done.notice') + redirect_to tasks_url, notice: I18n.t('tasks.set_done.notice') end # Shows all tasks, which are already done @@ -109,9 +105,9 @@ class TasksController < ApplicationController # shows workgroup (normal group) to edit weekly_tasks_template def workgroup @group = Group.find(params[:workgroup_id]) - if @group.is_a? Ordergroup - redirect_to tasks_url, :alert => I18n.t('tasks.error_not_found') - end + return unless @group.is_a? Ordergroup + + redirect_to tasks_url, alert: I18n.t('tasks.error_not_found') end private diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 503bc79b..df56ade0 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -3,7 +3,7 @@ class UsersController < ApplicationController def index @users = User.undeleted.natural_search(params[:q]) respond_to do |format| - format.json { render :json => @users.map(&:token_attributes).to_json } + format.json { render json: @users.map(&:token_attributes).to_json } end end end diff --git a/app/documents/order_by_articles.rb b/app/documents/order_by_articles.rb index 84fb5c00..b1a68a11 100644 --- a/app/documents/order_by_articles.rb +++ b/app/documents/order_by_articles.rb @@ -1,11 +1,11 @@ class OrderByArticles < OrderPdf def filename - I18n.t('documents.order_by_articles.filename', :name => order.name, :date => order.ends.to_date) + '.pdf' + I18n.t('documents.order_by_articles.filename', name: order.name, date: order.ends.to_date) + '.pdf' end def title - I18n.t('documents.order_by_articles.title', :name => order.name, - :date => order.ends.strftime(I18n.t('date.formats.default'))) + I18n.t('documents.order_by_articles.title', name: order.name, + date: order.ends.strftime(I18n.t('date.formats.default'))) end def body diff --git a/app/documents/order_by_groups.rb b/app/documents/order_by_groups.rb index d6711731..e5a72c35 100644 --- a/app/documents/order_by_groups.rb +++ b/app/documents/order_by_groups.rb @@ -1,11 +1,11 @@ class OrderByGroups < OrderPdf def filename - I18n.t('documents.order_by_groups.filename', :name => order.name, :date => order.ends.to_date) + '.pdf' + I18n.t('documents.order_by_groups.filename', name: order.name, date: order.ends.to_date) + '.pdf' end def title - I18n.t('documents.order_by_groups.title', :name => order.name, - :date => order.ends.strftime(I18n.t('date.formats.default'))) + I18n.t('documents.order_by_groups.title', name: order.name, + date: order.ends.strftime(I18n.t('date.formats.default'))) end def body diff --git a/app/documents/order_fax.rb b/app/documents/order_fax.rb index b4b50577..e881c93f 100644 --- a/app/documents/order_fax.rb +++ b/app/documents/order_fax.rb @@ -2,7 +2,7 @@ class OrderFax < OrderPdf BATCH_SIZE = 250 def filename - I18n.t('documents.order_fax.filename', :name => order.name, :date => order.ends.to_date) + '.pdf' + I18n.t('documents.order_fax.filename', name: order.name, date: order.ends.to_date) + '.pdf' end def title @@ -20,16 +20,18 @@ class OrderFax < OrderPdf move_down 5 text "#{contact[:zip_code]} #{contact[:city]}", size: fontsize(9), align: :right move_down 5 - unless order.supplier.try(:customer_number).blank? - text "#{Supplier.human_attribute_name :customer_number}: #{order.supplier[:customer_number]}", size: fontsize(9), align: :right + if order.supplier.try(:customer_number).present? + text "#{Supplier.human_attribute_name :customer_number}: #{order.supplier[:customer_number]}", + size: fontsize(9), align: :right move_down 5 end - unless contact[:phone].blank? + if contact[:phone].present? text "#{Supplier.human_attribute_name :phone}: #{contact[:phone]}", size: fontsize(9), align: :right move_down 5 end - unless contact[:email].blank? - text "#{Supplier.human_attribute_name :email}: #{contact[:email]}", size: fontsize(9), align: :right + if contact[:email].present? + text "#{Supplier.human_attribute_name :email}: #{contact[:email]}", size: fontsize(9), + align: :right end end @@ -38,7 +40,7 @@ class OrderFax < OrderPdf text order.name move_down 5 text order.supplier.try(:address).to_s - unless order.supplier.try(:fax).blank? + if order.supplier.try(:fax).present? move_down 5 text "#{Supplier.human_attribute_name :fax}: #{order.supplier[:fax]}" end @@ -50,7 +52,7 @@ class OrderFax < OrderPdf move_down 10 text "#{Delivery.human_attribute_name :date}:" move_down 10 - unless order.supplier.try(:contact_person).blank? + if order.supplier.try(:contact_person).present? text "#{Supplier.human_attribute_name :contact_person}: #{order.supplier[:contact_person]}" move_down 10 end @@ -78,8 +80,8 @@ class OrderFax < OrderPdf table.row(0).border_bottom_width = 2 table.columns(1).align = :right table.columns(3..6).align = :right - table.row(data.length - 1).columns(0..5).borders = [:top, :bottom] - table.row(data.length - 1).columns(0).borders = [:top, :bottom, :left] + table.row(data.length - 1).columns(0..5).borders = %i[top bottom] + table.row(data.length - 1).columns(0).borders = %i[top bottom left] table.row(data.length - 1).border_top_width = 2 end # font_size: fontsize(8), @@ -98,7 +100,7 @@ class OrderFax < OrderPdf .preload(:article, :article_price) end - def each_order_article - order_articles.find_each_with_order(batch_size: BATCH_SIZE) { |oa| yield oa } + def each_order_article(&block) + order_articles.find_each_with_order(batch_size: BATCH_SIZE, &block) end end diff --git a/app/documents/order_matrix.rb b/app/documents/order_matrix.rb index 7269feaf..c45ca5fd 100644 --- a/app/documents/order_matrix.rb +++ b/app/documents/order_matrix.rb @@ -3,12 +3,12 @@ class OrderMatrix < OrderPdf PLACEHOLDER_CHAR = 'X' def filename - I18n.t('documents.order_matrix.filename', :name => @order.name, :date => @order.ends.to_date) + '.pdf' + I18n.t('documents.order_matrix.filename', name: @order.name, date: @order.ends.to_date) + '.pdf' end def title - I18n.t('documents.order_matrix.title', :name => @order.name, - :date => @order.ends.strftime(I18n.t('date.formats.default'))) + I18n.t('documents.order_matrix.title', name: @order.name, + date: @order.ends.strftime(I18n.t('date.formats.default'))) end def body @@ -87,7 +87,7 @@ class OrderMatrix < OrderPdf table.cells.border_width = 0.5 table.cells.border_color = '666666' - table.row(0).borders = [:bottom, :left] + table.row(0).borders = %i[bottom left] table.row(0).padding = [2, 0, 2, 0] table.row(1..-1).height = row_height_1 table.column(0..1).borders = [] @@ -106,7 +106,7 @@ class OrderMatrix < OrderPdf table.column(2 + idx).border_width = 2 end - table.row_colors = ['dddddd', 'ffffff'] + table.row_colors = %w[dddddd ffffff] end first_page = false diff --git a/app/helpers/admin/configs_helper.rb b/app/helpers/admin/configs_helper.rb index 0185a0df..3c1da9f0 100644 --- a/app/helpers/admin/configs_helper.rb +++ b/app/helpers/admin/configs_helper.rb @@ -28,7 +28,11 @@ module Admin::ConfigsHelper options[:default] = options[:input_html].delete(:value) return form.input key, options, &block end - block ||= proc { config_input_field form, key, options.merge(options[:input_html]) } if options[:as] == :select_recurring + if options[:as] == :select_recurring + block ||= proc { + config_input_field form, key, options.merge(options[:input_html]) + } + end form.input key, options, &block end @@ -57,11 +61,12 @@ module Admin::ConfigsHelper unchecked_value = options.delete(:unchecked_value) || 'false' options[:checked] = 'checked' if v = options.delete(:value) && v != 'false' # different key for hidden field so that allow clocking on label focuses the control - form.hidden_field(key, id: "#{key}_", value: unchecked_value, as: :hidden) + form.check_box(key, options, checked_value, false) + form.hidden_field(key, id: "#{key}_", value: unchecked_value, + as: :hidden) + form.check_box(key, options, checked_value, false) elsif options[:as] == :select_recurring options[:value] = FoodsoftDateUtil.rule_from(options[:value]) options[:rules] ||= [] - options[:rules].unshift options[:value] unless options[:value].blank? + options[:rules].unshift options[:value] if options[:value].present? options[:rules].push [I18n.t('recurring_select.not_recurring'), '{}'] if options.delete(:allow_blank) # blank after current value form.select_recurring key, options.delete(:rules).uniq, options else @@ -73,7 +78,7 @@ module Admin::ConfigsHelper # @param form [ActionView::Helpers::FormBuilder] Form object. # @param key [Symbol, String] Configuration key of a boolean (e.g. +use_messages+). # @option options [String] :label Label to show - def config_use_heading(form, key, options = {}) + def config_use_heading(form, key, options = {}, &block) head = content_tag :label do lbl = options[:label] || config_input_label(form, key) field = config_input_field(form, key, as: :boolean, boolean_style: :inline, @@ -83,9 +88,7 @@ module Admin::ConfigsHelper content_tag :span, (lbl + field).html_safe, config_input_tooltip_options(form, key, {}) end end - fields = content_tag(:fieldset, id: "#{key}-fields", class: "collapse#{' in' if @cfg[key]}") do - yield - end + fields = content_tag(:fieldset, id: "#{key}-fields", class: "collapse#{' in' if @cfg[key]}", &block) head + fields end @@ -127,7 +130,7 @@ module Admin::ConfigsHelper # tooltip with help info to the right cfg_path = form.lookup_model_names[1..-1] + [key] tooltip = I18n.t("config.hints.#{cfg_path.map(&:to_s).join('.')}", default: '') - unless tooltip.blank? + if tooltip.present? options[:data] ||= {} options[:data][:toggle] ||= 'tooltip' options[:data][:placement] ||= 'right' diff --git a/app/helpers/admin/ordergroups_helper.rb b/app/helpers/admin/ordergroups_helper.rb index e74fdde5..ecb4bd39 100644 --- a/app/helpers/admin/ordergroups_helper.rb +++ b/app/helpers/admin/ordergroups_helper.rb @@ -2,9 +2,7 @@ module Admin::OrdergroupsHelper def ordergroup_members_title(ordergroup) s = '' s += ordergroup.users.map(&:name).join(', ') if ordergroup.users.any? - if ordergroup.contact_person.present? - s += "\n" + Ordergroup.human_attribute_name(:contact) + ": " + ordergroup.contact_person - end + s += "\n" + Ordergroup.human_attribute_name(:contact) + ': ' + ordergroup.contact_person if ordergroup.contact_person.present? s end end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index de207901..b962507b 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -4,7 +4,7 @@ module ApplicationHelper include PathHelper def format_time(time = Time.now) - I18n.l(time, :format => "%d.%m.%Y %H:%M") unless time.nil? + I18n.l(time, format: '%d.%m.%Y %H:%M') unless time.nil? end def format_date(time = Time.now) @@ -16,7 +16,7 @@ module ApplicationHelper end def format_datetime_timespec(time, format) - I18n.l(time, :format => format) unless (time.nil? || format.nil?) + I18n.l(time, format: format) unless time.nil? || format.nil? end def format_currency(amount) @@ -26,28 +26,28 @@ module ApplicationHelper # Splits an IBAN into groups of 4 digits displayed with margins in between def format_iban(iban) - iban && iban.scan(/..?.?.?/).map { |item| content_tag(:span, item, style: "margin-right: 0.5em;") }.join.html_safe + iban && iban.scan(/..?.?.?/).map { |item| content_tag(:span, item, style: 'margin-right: 0.5em;') }.join.html_safe end # Creates ajax-controlled-links for pagination def pagination_links_remote(collection, options = {}) per_page = options[:per_page] || @per_page params = options[:params] || {} - params = params.merge({ :per_page => per_page }) - paginate collection, :params => params, :remote => true + params = params.merge({ per_page: per_page }) + paginate collection, params: params, remote: true end # Link-collection for per_page-options when using the pagination-plugin def items_per_page(options = {}) per_page_options = options[:per_page_options] || [20, 50, 100, 500] current = options[:current] || @per_page - params = params || {} + params ||= {} links = per_page_options.map do |per_page| - params.merge!({ :per_page => per_page }) + params.merge!({ per_page: per_page }) link_class = 'btn' link_class << ' disabled' if per_page == current - link_to(per_page, params, :remote => true, class: link_class) + link_to(per_page, params, remote: true, class: link_class) end if options[:wrap] == false @@ -63,21 +63,19 @@ module ApplicationHelper # Hmtl options remote = options[:remote].nil? ? true : options[:remote] class_name = case params[:sort] - when key then + when key 'sortup' - when key + '_reverse' then + when key + '_reverse' 'sortdown' - else - nil end html_options = { - :title => I18n.t('helpers.application.sort_by', text: text), - :remote => remote, - :class => class_name + title: I18n.t('helpers.application.sort_by', text: text), + remote: remote, + class: class_name } # Url options - key += "_reverse" if params[:sort] == key + key += '_reverse' if params[:sort] == key per_page = options[:per_page] || @per_page url_options = params.merge(per_page: per_page, sort: key) url_options.merge!({ page: params[:page] }) if params[:page] @@ -95,14 +93,16 @@ module ApplicationHelper # be overridden by the option 'desc'. # Other options are passed through to I18n. def heading_helper(model, attribute, options = {}) - i18nopts = { count: 2 }.merge(options.select { |a| !['short', 'desc'].include?(a) }) + i18nopts = { count: 2 }.merge(options.select { |a| !%w[short desc].include?(a) }) s = model.human_attribute_name(attribute, i18nopts) if options[:short] desc = options[:desc] - desc ||= model.human_attribute_name("#{attribute}_desc".to_sym, options.merge({ fallback: true, default: '', count: 2 })) + desc ||= model.human_attribute_name("#{attribute}_desc".to_sym, + options.merge({ fallback: true, default: '', count: 2 })) desc.blank? && desc = s - sshort = model.human_attribute_name("#{attribute}_short".to_sym, options.merge({ fallback: true, default: '', count: 2 })) - s = raw "#{sshort}" unless sshort.blank? + sshort = model.human_attribute_name("#{attribute}_short".to_sym, + options.merge({ fallback: true, default: '', count: 2 })) + s = raw "#{sshort}" if sshort.present? end s end @@ -117,7 +117,7 @@ module ApplicationHelper # Returns the weekday. 0 is sunday, 1 is monday and so on def weekday(dayNumber) weekdays = I18n.t('date.day_names') - return weekdays[dayNumber] + weekdays[dayNumber] end # to set a title for both the h1-tag and the title in the header @@ -136,13 +136,13 @@ module ApplicationHelper def icon(name, options = {}) icons = { - :delete => { :file => 'b_drop.png', :alt => I18n.t('ui.delete') }, - :edit => { :file => 'b_edit.png', :alt => I18n.t('ui.edit') }, - :members => { :file => 'b_users.png', :alt => I18n.t('helpers.application.edit_user') } + delete: { file: 'b_drop.png', alt: I18n.t('ui.delete') }, + edit: { file: 'b_edit.png', alt: I18n.t('ui.edit') }, + members: { file: 'b_users.png', alt: I18n.t('helpers.application.edit_user') } } options[:alt] ||= icons[name][:alt] options[:title] ||= icons[name][:title] - options.merge!({ :size => '16x16', :border => "0" }) + options.merge!({ size: '16x16', border: '0' }) image_tag icons[name][:file], options end @@ -150,27 +150,29 @@ module ApplicationHelper # Remote links with default 'loader'.gif during request def remote_link_to(text, options = {}) remote_options = { - :before => "Element.show('loader')", - :success => "Element.hide('loader')", - :method => :get + before: "Element.show('loader')", + success: "Element.hide('loader')", + method: :get } link_to(text, options[:url], remote_options.merge(options)) end def format_roles(record, icon = false) - roles = %w(suppliers article_meta orders pickups finance invoices admin) + roles = %w[suppliers article_meta orders pickups finance invoices admin] roles.select! { |role| record.send "role_#{role}?" } - names = Hash[roles.map { |r| [r, I18n.t("helpers.application.role_#{r}")] }] + names = roles.index_with { |r| I18n.t("helpers.application.role_#{r}") } if icon - roles.map { |r| image_tag("role-#{r}.png", size: '22x22', border: 0, alt: names[r], title: names[r]) }.join(' ').html_safe + roles.map do |r| + image_tag("role-#{r}.png", size: '22x22', border: 0, alt: names[r], title: names[r]) + end.join(' ').html_safe else roles.map { |r| names[r] }.join(', ') end end def link_to_gmaps(address) - link_to h(address), "http://maps.google.com/?q=#{h(address)}", :title => I18n.t('helpers.application.show_google_maps'), - :target => "_blank" + link_to h(address), "http://maps.google.com/?q=#{h(address)}", title: I18n.t('helpers.application.show_google_maps'), + target: '_blank', rel: 'noopener' end # Returns flash messages html. @@ -186,8 +188,8 @@ module ApplicationHelper type = :success if type == 'notice' type = :error if type == 'alert' text = content_tag(:div, - content_tag(:button, I18n.t('ui.marks.close').html_safe, :class => "close", "data-dismiss" => "alert") + - message, :class => "alert fade in alert-#{type}") + content_tag(:button, I18n.t('ui.marks.close').html_safe, :class => 'close', 'data-dismiss' => 'alert') + + message, class: "alert fade in alert-#{type}") flash_messages << text if message end flash_messages.join("\n").html_safe @@ -195,17 +197,17 @@ module ApplicationHelper # render base errors in a form after failed validation # http://railsapps.github.io/twitter-bootstrap-rails.html - def base_errors resource + def base_errors(resource) return '' if resource.errors.empty? || resource.errors[:base].empty? messages = resource.errors[:base].map { |msg| content_tag(:li, msg) }.join - render :partial => 'shared/base_errors', :locals => { :error_messages => messages } + render partial: 'shared/base_errors', locals: { error_messages: messages } end # show a user, depending on settings def show_user(user = @current_user, options = {}) if user.nil? - "?" + '?' elsif FoodsoftConfig[:use_nick] if options[:full] && options[:markup] raw "#{h user.nick} (#{h user.first_name} #{h user.last_name})" @@ -216,7 +218,7 @@ module ApplicationHelper user.nick.nil? ? I18n.t('helpers.application.nick_fallback') : user.nick end else - "#{user.first_name} #{user.last_name}" + (options[:unique] ? " (\##{user.id})" : '') + "#{user.first_name} #{user.last_name}" + (options[:unique] ? " (##{user.id})" : '') end end @@ -258,9 +260,9 @@ module ApplicationHelper # @return [String] stylesheet tag for foodcoop CSS style (+custom_css+ foodcoop config) # @see #foodcoop_css_path - def foodcoop_css_tag(options = {}) - unless FoodsoftConfig[:custom_css].blank? - stylesheet_link_tag foodcoop_css_path, media: 'all' - end + def foodcoop_css_tag(_options = {}) + return if FoodsoftConfig[:custom_css].blank? + + stylesheet_link_tag foodcoop_css_path, media: 'all' end end diff --git a/app/helpers/articles_helper.rb b/app/helpers/articles_helper.rb index add1c6ba..ebad29a4 100644 --- a/app/helpers/articles_helper.rb +++ b/app/helpers/articles_helper.rb @@ -3,13 +3,13 @@ module ArticlesHelper def highlight_new(unequal_attributes, attribute) return unless unequal_attributes - unequal_attributes.has_key?(attribute) ? "background-color: yellow" : "" + unequal_attributes.has_key?(attribute) ? 'background-color: yellow' : '' end def row_classes(article) classes = [] - classes << "unavailable" if !article.availability - classes << "just-updated" if article.recently_updated && article.availability - classes.join(" ") + classes << 'unavailable' unless article.availability + classes << 'just-updated' if article.recently_updated && article.availability + classes.join(' ') end end diff --git a/app/helpers/deliveries_helper.rb b/app/helpers/deliveries_helper.rb index a97a7df7..ac6e4b35 100644 --- a/app/helpers/deliveries_helper.rb +++ b/app/helpers/deliveries_helper.rb @@ -11,11 +11,11 @@ module DeliveriesHelper def articles_for_select2(articles, except = [], &block) articles = articles.reorder('articles.name ASC') - articles = articles.reject { |a| not except.index(a.id).nil? } if except - block_given? or block = Proc.new { |a| "#{a.name} (#{number_to_currency a.price}/#{a.unit})" } + articles = articles.reject { |a| !except.index(a.id).nil? } if except + block_given? or block = proc { |a| "#{a.name} (#{number_to_currency a.price}/#{a.unit})" } articles.map do |a| - { :id => a.id, :text => block.call(a) } - end.unshift({ :id => '', :text => '' }) + { id: a.id, text: block.call(a) } + end.unshift({ id: '', text: '' }) end def articles_for_table(articles) @@ -23,10 +23,14 @@ module DeliveriesHelper end def stock_change_remove_link(stock_change_form) - return link_to t('deliveries.stock_change_fields.remove_article'), "#", :class => 'remove_new_stock_change btn btn-small' if stock_change_form.object.new_record? + if stock_change_form.object.new_record? + return link_to t('deliveries.stock_change_fields.remove_article'), '#', + class: 'remove_new_stock_change btn btn-small' + end output = stock_change_form.hidden_field :_destroy - output += link_to t('deliveries.stock_change_fields.remove_article'), "#", :class => 'destroy_stock_change btn btn-small' - return output.html_safe + output += link_to t('deliveries.stock_change_fields.remove_article'), '#', + class: 'destroy_stock_change btn btn-small' + output.html_safe end end diff --git a/app/helpers/finance/balancing_helper.rb b/app/helpers/finance/balancing_helper.rb index bc528f04..a123b161 100644 --- a/app/helpers/finance/balancing_helper.rb +++ b/app/helpers/finance/balancing_helper.rb @@ -2,11 +2,11 @@ module Finance::BalancingHelper def balancing_view_partial view = params[:view] || 'edit_results' case view - when 'edit_results' then + when 'edit_results' 'edit_results_by_articles' - when 'groups_overview' then + when 'groups_overview' 'shared/articles_by/groups' - when 'articles_overview' then + when 'articles_overview' 'shared/articles_by/articles' end end diff --git a/app/helpers/finance/invoices_helper.rb b/app/helpers/finance/invoices_helper.rb index ef01a275..0644b501 100644 --- a/app/helpers/finance/invoices_helper.rb +++ b/app/helpers/finance/invoices_helper.rb @@ -1,9 +1,9 @@ module Finance::InvoicesHelper - def format_delivery_item delivery + def format_delivery_item(delivery) format_date(delivery.date) end - def format_order_item order + def format_order_item(order) "#{format_date(order.ends)} (#{number_to_currency(order.sum)})" end end diff --git a/app/helpers/group_order_articles_helper.rb b/app/helpers/group_order_articles_helper.rb index ff003731..3a7efc33 100644 --- a/app/helpers/group_order_articles_helper.rb +++ b/app/helpers/group_order_articles_helper.rb @@ -2,12 +2,12 @@ module GroupOrderArticlesHelper # return an edit field for a GroupOrderArticle result def group_order_article_edit_result(goa) result = number_with_precision goa.result, strip_insignificant_zeros: true - unless goa.group_order.order.finished? && current_user.role_finance? - result - else + if goa.group_order.order.finished? && current_user.role_finance? simple_form_for goa, remote: true, html: { 'data-submit-onchange' => 'changed', class: 'delta-input' } do |f| f.input_field :result, as: :delta, class: 'input-nano', data: { min: 0 }, id: "r_#{goa.id}", value: result end + else + result end end end diff --git a/app/helpers/group_orders_helper.rb b/app/helpers/group_orders_helper.rb index c5e27c66..a09a066c 100644 --- a/app/helpers/group_orders_helper.rb +++ b/app/helpers/group_orders_helper.rb @@ -1,10 +1,11 @@ module GroupOrdersHelper def data_to_js(ordering_data) - ordering_data[:order_articles].map { |id, data| - [id, data[:price], data[:unit], data[:total_price], data[:others_quantity], data[:others_tolerance], data[:used_quantity], data[:quantity_available]] - }.map { |row| + ordering_data[:order_articles].map do |id, data| + [id, data[:price], data[:unit], data[:total_price], data[:others_quantity], data[:others_tolerance], + data[:used_quantity], data[:quantity_available]] + end.map do |row| "addData(#{row.join(', ')});" - }.join("\n") + end.join("\n") end # Returns a link to the page where a group_order can be edited. @@ -14,9 +15,9 @@ module GroupOrdersHelper path = if options[:show] && group_order group_order_path(group_order) elsif group_order - edit_group_order_path(group_order, :order_id => order.id) + edit_group_order_path(group_order, order_id: order.id) else - new_group_order_path(:order_id => order.id) + new_group_order_path(order_id: order.id) end options.delete(:show) name = block_given? ? capture(&block) : order.name @@ -26,7 +27,7 @@ module GroupOrdersHelper # Return css class names for order result table def order_article_class_name(quantity, tolerance, result) - if (quantity + tolerance > 0) + if quantity + tolerance > 0 result > 0 ? 'success' : 'failed' else 'ignored' @@ -45,12 +46,12 @@ module GroupOrdersHelper end def get_missing_units_css_class(quantity_missing) - if (quantity_missing == 1) - return 'missing-few'; - elsif (quantity_missing == 0) - return '' + if quantity_missing == 1 + 'missing-few' + elsif quantity_missing == 0 + '' else - return 'missing-many' + 'missing-many' end end end diff --git a/app/helpers/order_articles_helper.rb b/app/helpers/order_articles_helper.rb index b4290e84..7af4b409 100644 --- a/app/helpers/order_articles_helper.rb +++ b/app/helpers/order_articles_helper.rb @@ -1,6 +1,6 @@ module OrderArticlesHelper def article_label_with_unit(article) pkg_info = pkg_helper(article, plain: true) - "#{article.name} (#{[article.unit, pkg_info].reject(&:blank?).join(' ')})" + "#{article.name} (#{[article.unit, pkg_info].compact_blank.join(' ')})" end end diff --git a/app/helpers/orders_helper.rb b/app/helpers/orders_helper.rb index ff238730..d629ccb1 100644 --- a/app/helpers/orders_helper.rb +++ b/app/helpers/orders_helper.rb @@ -18,7 +18,7 @@ module OrdersHelper def options_for_suppliers_to_select options = [[I18n.t('helpers.orders.option_choose')]] - options += Supplier.map { |s| [s.name, url_for(action: "new", supplier_id: s.id)] } + options += Supplier.map { |s| [s.name, url_for(action: 'new', supplier_id: s.id)] } options += [[I18n.t('helpers.orders.option_stock'), url_for(action: 'new', supplier_id: nil)]] options_for_select(options) end @@ -29,13 +29,13 @@ module OrdersHelper nil else units_info = [] - [:units_to_order, :units_billed, :units_received].map do |unit| - if n = order_article.send(unit) - line = n.to_s + ' ' - line += pkg_helper(order_article.price, options) + ' ' unless n == 0 - line += OrderArticle.human_attribute_name("#{unit}_short", count: n) - units_info << line - end + %i[units_to_order units_billed units_received].map do |unit| + next unless n = order_article.send(unit) + + line = n.to_s + ' ' + line += pkg_helper(order_article.price, options) + ' ' unless n == 0 + line += OrderArticle.human_attribute_name("#{unit}_short", count: n) + units_info << line end units_info.join(', ').html_safe end @@ -67,8 +67,8 @@ module OrdersHelper def pkg_helper_icon(c = nil, options = {}) options = { tag: 'i', class: '' }.merge(options) if c.nil? - c = " ".html_safe - options[:class] += " icon-only" + c = ' '.html_safe + options[:class] += ' icon-only' end content_tag(options[:tag], c, class: "package #{options[:class]}").html_safe end @@ -94,11 +94,12 @@ module OrdersHelper autocomplete: 'off' if order_article.result_manually_changed? - input_html = content_tag(:span, class: 'input-prepend intable', title: t('orders.edit_amount.field_locked_title', default: '')) { + input_html = content_tag(:span, class: 'input-prepend intable', + title: t('orders.edit_amount.field_locked_title', default: '')) do button_tag(nil, type: :button, class: 'btn unlocker') { content_tag(:i, nil, class: 'icon icon-unlock') } + input_html - } + end end input_html.html_safe @@ -109,18 +110,16 @@ module OrdersHelper def ordergroup_count(order) group_orders = order.group_orders.includes(:ordergroup) txt = "#{group_orders.count} #{Ordergroup.model_name.human count: group_orders.count}" - if group_orders.count == 0 - return txt - else - desc = group_orders.includes(:ordergroup).map { |g| g.ordergroup_name }.join(', ') - content_tag(:abbr, txt, title: desc).html_safe - end + return txt if group_orders.count == 0 + + desc = group_orders.includes(:ordergroup).map { |g| g.ordergroup_name }.join(', ') + content_tag(:abbr, txt, title: desc).html_safe end # @param order_or_supplier [Order, Supplier] Order or supplier to link to # @return [String] Link to order or supplier, showing its name. def supplier_link(order_or_supplier) - if order_or_supplier.kind_of?(Order) && order_or_supplier.stockit? + if order_or_supplier.is_a?(Order) && order_or_supplier.stockit? link_to(order_or_supplier.name, stock_articles_path).html_safe else link_to(@order.supplier.name, supplier_path(@order.supplier)).html_safe @@ -152,7 +151,8 @@ module OrdersHelper if order.stockit? content_tag :div, t('orders.index.action_receive'), class: "btn disabled #{options[:class]}" else - link_to t('orders.index.action_receive'), receive_order_path(order), class: "btn#{' btn-success' unless order.received?} #{options[:class]}" + link_to t('orders.index.action_receive'), receive_order_path(order), + class: "btn#{' btn-success' unless order.received?} #{options[:class]}" end end end diff --git a/app/helpers/stockit_helper.rb b/app/helpers/stockit_helper.rb index a08e8335..9848198d 100644 --- a/app/helpers/stockit_helper.rb +++ b/app/helpers/stockit_helper.rb @@ -1,8 +1,8 @@ module StockitHelper def stock_article_classes(article) class_names = [] - class_names << "unavailable" if article.quantity_available <= 0 - class_names.join(" ") + class_names << 'unavailable' if article.quantity_available <= 0 + class_names.join(' ') end def link_to_stock_change_reason(stock_change) @@ -17,8 +17,8 @@ module StockitHelper def stock_article_price_hint(stock_article) t('simple_form.hints.stock_article.edit_stock_article.price', - :stock_article_copy_link => link_to(t('stockit.form.copy_stock_article'), - stock_article_copy_path(stock_article), - :remote => true)) + stock_article_copy_link: link_to(t('stockit.form.copy_stock_article'), + stock_article_copy_path(stock_article), + remote: true)) end end diff --git a/app/helpers/tasks_helper.rb b/app/helpers/tasks_helper.rb index f6f1fa14..4b12f7a8 100644 --- a/app/helpers/tasks_helper.rb +++ b/app/helpers/tasks_helper.rb @@ -1,16 +1,16 @@ module TasksHelper def task_assignments(task) task.assignments.map do |ass| - content_tag :span, show_user(ass.user), :class => (ass.accepted? ? 'accepted' : 'unaccepted') - end.join(", ").html_safe + content_tag :span, show_user(ass.user), class: (ass.accepted? ? 'accepted' : 'unaccepted') + end.join(', ').html_safe end # generate colored number of still required users def highlighted_required_users(task) - unless task.enough_users_assigned? - content_tag :span, task.still_required_users, class: 'badge badge-important', - title: I18n.t('helpers.tasks.required_users', :count => task.still_required_users) - end + return if task.enough_users_assigned? + + content_tag :span, task.still_required_users, class: 'badge badge-important', + title: I18n.t('helpers.tasks.required_users', count: task.still_required_users) end def task_title(task) diff --git a/app/inputs/delta_input.rb b/app/inputs/delta_input.rb index adc08960..f4ce1a2b 100644 --- a/app/inputs/delta_input.rb +++ b/app/inputs/delta_input.rb @@ -6,7 +6,7 @@ class DeltaInput < SimpleForm::Inputs::StringInput options[:data] ||= {} options[:data][:delta] ||= 1 options[:autocomplete] ||= 'off' - # TODO get generated id, don't know how yet - `add_default_name_and_id_for_value` might be an option + # TODO: get generated id, don't know how yet - `add_default_name_and_id_for_value` might be an option template.content_tag :div, class: 'delta-input input-prepend input-append' do delta_button(content_tag(:i, nil, class: 'icon icon-minus'), -1, options) + diff --git a/app/lib/apple_bar.rb b/app/lib/apple_bar.rb index 236417c6..fb6fcef7 100644 --- a/app/lib/apple_bar.rb +++ b/app/lib/apple_bar.rb @@ -29,7 +29,7 @@ class AppleBar def mean_order_amount_per_job (1 / @global_avg).round - rescue + rescue StandardError 0 end diff --git a/app/lib/bank_account_connector.rb b/app/lib/bank_account_connector.rb index b728ebb9..5e18a816 100644 --- a/app/lib/bank_account_connector.rb +++ b/app/lib/bank_account_connector.rb @@ -41,14 +41,14 @@ class BankAccountConnector end end - @@registered_classes = Set.new + @registered_classes = Set.new def self.register(klass) - @@registered_classes.add klass + @registered_classes.add klass end def self.find(iban) - @@registered_classes.each do |klass| + @registered_classes.each do |klass| return klass if klass.handles(iban) end nil diff --git a/app/lib/bank_account_information_importer.rb b/app/lib/bank_account_information_importer.rb index bebc1ff4..a83c53f7 100644 --- a/app/lib/bank_account_information_importer.rb +++ b/app/lib/bank_account_information_importer.rb @@ -17,16 +17,16 @@ class BankAccountInformationImporter ret = 0 booked.each do |t| amount = parse_account_information_amount t[:transactionAmount] - entityName = amount < 0 ? t[:creditorName] : t[:debtorName] - entityAccount = amount < 0 ? t[:creditorAccount] : t[:debtorAccount] + entity_name = amount < 0 ? t[:creditorName] : t[:debtorName] + entity_account = amount < 0 ? t[:creditorAccount] : t[:debtorAccount] reference = [t[:endToEndId], t[:remittanceInformationUnstructured]].join("\n").strip @bank_account.bank_transactions.where(external_id: t[:transactionId]).first_or_create.update({ date: t[:bookingDate], amount: amount, - iban: entityAccount && entityAccount[:iban], + iban: entity_account && entity_account[:iban], reference: reference, - text: entityName, + text: entity_name, receipt: t[:additionalInformation] }) ret += 1 @@ -34,7 +34,7 @@ class BankAccountInformationImporter balances = (data[:balances] ? data[:balances].map { |b| [b[:balanceType], b[:balanceAmount]] } : []).to_h balance = balances.values.first - %w(closingBooked expected authorised openingBooked interimAvailable forwardAvailable nonInvoiced).each do |type| + %w[closingBooked expected authorised openingBooked interimAvailable forwardAvailable nonInvoiced].each do |type| value = balances[type] if value balance = value diff --git a/app/lib/date_time_attribute_validate.rb b/app/lib/date_time_attribute_validate.rb index 23127898..fa53c361 100644 --- a/app/lib/date_time_attribute_validate.rb +++ b/app/lib/date_time_attribute_validate.rb @@ -10,66 +10,68 @@ module DateTimeAttributeValidate super attributes.each do |attribute| - validate -> { self.send("#{attribute}_datetime_value_valid") } + validate -> { send("#{attribute}_datetime_value_valid") } # allow resetting the field to nil before_validation do - if self.instance_variable_get("@#{attribute}_is_set") - date = self.instance_variable_get("@#{attribute}_date_value") - time = self.instance_variable_get("@#{attribute}_time_value") - if date.blank? && time.blank? - self.send("#{attribute}=", nil) - end + if instance_variable_get("@#{attribute}_is_set") + date = instance_variable_get("@#{attribute}_date_value") + time = instance_variable_get("@#{attribute}_time_value") + send("#{attribute}=", nil) if date.blank? && time.blank? end end # remember old date and time values define_method("#{attribute}_date_value=") do |val| - self.instance_variable_set("@#{attribute}_is_set", true) - self.instance_variable_set("@#{attribute}_date_value", val) + instance_variable_set("@#{attribute}_is_set", true) + instance_variable_set("@#{attribute}_date_value", val) begin - self.send("#{attribute}_date=", val) - rescue + send("#{attribute}_date=", val) + rescue StandardError nil end end define_method("#{attribute}_time_value=") do |val| - self.instance_variable_set("@#{attribute}_is_set", true) - self.instance_variable_set("@#{attribute}_time_value", val) + instance_variable_set("@#{attribute}_is_set", true) + instance_variable_set("@#{attribute}_time_value", val) begin - self.send("#{attribute}_time=", val) - rescue + send("#{attribute}_time=", val) + rescue StandardError nil end end # fallback to field when values are not set define_method("#{attribute}_date_value") do - self.instance_variable_get("@#{attribute}_date_value") || self.send("#{attribute}_date").try { |e| e.strftime('%Y-%m-%d') } + instance_variable_get("@#{attribute}_date_value") || send("#{attribute}_date").try do |e| + e.strftime('%Y-%m-%d') + end end define_method("#{attribute}_time_value") do - self.instance_variable_get("@#{attribute}_time_value") || self.send("#{attribute}_time").try { |e| e.strftime('%H:%M') } + instance_variable_get("@#{attribute}_time_value") || send("#{attribute}_time").try do |e| + e.strftime('%H:%M') + end end private # validate date and time define_method("#{attribute}_datetime_value_valid") do - date = self.instance_variable_get("@#{attribute}_date_value") + date = instance_variable_get("@#{attribute}_date_value") unless date.blank? || begin Date.parse(date) - rescue + rescue StandardError nil end - errors.add(attribute, "is not a valid date") # @todo I18n + errors.add(attribute, 'is not a valid date') # @todo I18n end - time = self.instance_variable_get("@#{attribute}_time_value") + time = instance_variable_get("@#{attribute}_time_value") unless time.blank? || begin Time.parse(time) - rescue + rescue StandardError nil end - errors.add(attribute, "is not a valid time") # @todo I18n + errors.add(attribute, 'is not a valid time') # @todo I18n end end end diff --git a/app/lib/foodsoft/expansion_variables.rb b/app/lib/foodsoft/expansion_variables.rb index 97f7b6bb..a4a8153e 100644 --- a/app/lib/foodsoft/expansion_variables.rb +++ b/app/lib/foodsoft/expansion_variables.rb @@ -14,7 +14,7 @@ module Foodsoft cattr_accessor :variables # Hash of variables. Note that keys are Strings. - @@variables = { + @variables = { 'scope' => -> { FoodsoftConfig.scope }, 'name' => -> { FoodsoftConfig[:name] }, 'contact.street' => -> { FoodsoftConfig[:contact][:street] }, @@ -39,13 +39,13 @@ module Foodsoft 'supplier_count' => -> { Supplier.undeleted.count }, 'active_supplier_count' => -> { active_supplier_count }, 'active_suppliers' => -> { active_suppliers }, - 'first_order_date' => -> { I18n.l Order.first.try { |o| o.starts.to_date } } + 'first_order_date' => -> { I18n.l(Order.first.try { |o| o.starts.to_date }) } } # Return expanded variable # @return [String] Expanded variable def self.get(var) - s = @@variables[var.to_s] + s = @variables[var.to_s] s.respond_to?(:call) ? s.call : s.to_s end @@ -55,7 +55,7 @@ module Foodsoft # @return [String] Expanded string def self.expand(str, options = {}) str.gsub(/{{([._a-zA-Z0-9]+)}}/) do - options[::Regexp.last_match(1)] || self.get(::Regexp.last_match(1)) + options[::Regexp.last_match(1)] || get(::Regexp.last_match(1)) end end diff --git a/app/lib/foodsoft_config.rb b/app/lib/foodsoft_config.rb index 6ea166d3..c7dda590 100644 --- a/app/lib/foodsoft_config.rb +++ b/app/lib/foodsoft_config.rb @@ -70,7 +70,7 @@ class FoodsoftConfig # Load initial config from development or production set_config Rails.env # Overwrite scope to have a better namescope than 'production' - self.scope = config[:default_scope] or raise "No default_scope is set" + self.scope = config[:default_scope] or raise 'No default_scope is set' # Set defaults for backward-compatibility set_missing # Make sure relevant configuration is applied, also in single coops mode, @@ -79,7 +79,7 @@ class FoodsoftConfig end def init_mailing - [:protocol, :host, :port, :script_name].each do |k| + %i[protocol host port script_name].each do |k| ActionMailer::Base.default_url_options[k] = self[k] if self[k] end end @@ -117,7 +117,7 @@ class FoodsoftConfig # @return [Object] Value of the key. def [](key) if RailsSettings::CachedSettings.table_exists? && allowed_key?(key) - value = RailsSettings::CachedSettings["foodcoop.#{self.scope}.#{key}"] + value = RailsSettings::CachedSettings["foodcoop.#{scope}.#{key}"] value = config[key] if value.nil? value else @@ -139,20 +139,20 @@ class FoodsoftConfig if config[key] == value || (config[key].nil? && value == false) # delete (ok if it was already deleted) begin - RailsSettings::CachedSettings.destroy "foodcoop.#{self.scope}.#{key}" + RailsSettings::CachedSettings.destroy "foodcoop.#{scope}.#{key}" rescue RailsSettings::Settings::SettingNotFound end else # or store - RailsSettings::CachedSettings["foodcoop.#{self.scope}.#{key}"] = value + RailsSettings::CachedSettings["foodcoop.#{scope}.#{key}"] = value end true end # @return [Array] Configuration keys that are set (either in +app_config.yml+ or database). def keys - keys = RailsSettings::CachedSettings.get_all("foodcoop.#{self.scope}.").try(:keys) || [] - keys.map! { |k| k.gsub(/^foodcoop\.#{self.scope}\./, '') } + keys = RailsSettings::CachedSettings.get_all("foodcoop.#{scope}.").try(:keys) || [] + keys.map! { |k| k.gsub(/^foodcoop\.#{scope}\./, '') } keys += config.keys keys.map(&:to_s).uniq end @@ -181,10 +181,10 @@ class FoodsoftConfig # @return [Boolean] Whether this key may be set in the database def allowed_key?(key) # fast check for keys without nesting - if self.config[:protected].include? key - !self.config[:protected][key] + if config[:protected].include? key + !config[:protected][key] else - !self.config[:protected][:all] + !config[:protected][:all] end # @todo allow to check nested keys as well end @@ -287,7 +287,9 @@ class FoodsoftConfig def normalize_value(value) value = value.map { |v| normalize_value(v) } if value.is_a? Array if value.is_a? Hash - value = ActiveSupport::HashWithIndifferentAccess[value.to_a.map { |a| [a[0], normalize_value(a[1])] }] + value = ActiveSupport::HashWithIndifferentAccess[value.to_a.map do |a| + [a[0], normalize_value(a[1])] + end] end case value when 'true' then true diff --git a/app/lib/foodsoft_date_util.rb b/app/lib/foodsoft_date_util.rb index a14ad453..38dbc6be 100644 --- a/app/lib/foodsoft_date_util.rb +++ b/app/lib/foodsoft_date_util.rb @@ -8,26 +8,24 @@ module FoodsoftDateUtil # @todo handle ical parse errors occ = begin schedule.next_occurrence(from).to_time - rescue + rescue StandardError nil end end - if options && options[:time] && occ - occ = occ.beginning_of_day.advance(seconds: Time.parse(options[:time]).seconds_since_midnight) - end + occ = occ.beginning_of_day.advance(seconds: Time.parse(options[:time]).seconds_since_midnight) if options && options[:time] && occ occ end - # @param p [String, Symbol, Hash, IceCube::Rule] What to return a rule from. + # @param rule [String, Symbol, Hash, IceCube::Rule] What to return a rule from. # @return [IceCube::Rule] Recurring rule - def self.rule_from(p) - case p + def self.rule_from(rule) + case rule when String - IceCube::Rule.from_ical(p) + IceCube::Rule.from_ical(rule) when Hash - IceCube::Rule.from_hash(p) + IceCube::Rule.from_hash(rule) else - p + rule end end end diff --git a/app/lib/foodsoft_file.rb b/app/lib/foodsoft_file.rb index 95d06c60..0a7128ed 100644 --- a/app/lib/foodsoft_file.rb +++ b/app/lib/foodsoft_file.rb @@ -7,17 +7,17 @@ class FoodsoftFile SpreadsheetFile.parse file, options do |row, row_index| next if row[2].blank? - article = { :order_number => row[1], - :name => row[2], - :note => row[3], - :manufacturer => row[4], - :origin => row[5], - :unit => row[6], - :price => row[7], - :tax => row[8], - :deposit => (row[9].nil? ? "0" : row[9]), - :unit_quantity => row[10], - :article_category => row[13] } + article = { order_number: row[1], + name: row[2], + note: row[3], + manufacturer: row[4], + origin: row[5], + unit: row[6], + price: row[7], + tax: row[8], + deposit: (row[9].nil? ? '0' : row[9]), + unit_quantity: row[10], + article_category: row[13] } status = row[0] && row[0].strip.downcase == 'x' ? :outlisted : nil yield status, article, row_index end diff --git a/app/lib/foodsoft_mail_receiver.rb b/app/lib/foodsoft_mail_receiver.rb index 18e93be3..c5ec2edb 100644 --- a/app/lib/foodsoft_mail_receiver.rb +++ b/app/lib/foodsoft_mail_receiver.rb @@ -23,8 +23,8 @@ class FoodsoftMailReceiver < MidiSmtpServer::Smtpd recipient = rcpt_to.gsub(/^\s*<\s*(.*)\s*>\s*$/, '\1') @handlers << self.class.find_handler(recipient) rcpt_to - rescue => error - logger.info("Can not accept mail for '#{rcpt_to}': #{error}") + rescue StandardError => e + logger.info("Can not accept mail for '#{rcpt_to}': #{e}") raise MidiSmtpServer::Smtpd550Exception end @@ -32,16 +32,16 @@ class FoodsoftMailReceiver < MidiSmtpServer::Smtpd @handlers.each do |handler| handler.call(ctx[:message][:data]) end - rescue => error - ExceptionNotifier.notify_exception(error, data: ctx) - raise error + rescue StandardError => e + ExceptionNotifier.notify_exception(e, data: ctx) + raise e ensure @handlers.clear end def self.find_handler(recipient) m = /(?[^@.]+)\.(?
[^@]+)(@(?[^@]+))?/.match recipient - raise "recipient is missing or has an invalid format" if m.nil? + raise 'recipient is missing or has an invalid format' if m.nil? raise "Foodcoop '#{m[:foodcoop]}' could not be found" unless FoodsoftConfig.allowed_foodcoop? m[:foodcoop] FoodsoftConfig.select_multifoodcoop m[:foodcoop] @@ -53,6 +53,6 @@ class FoodsoftMailReceiver < MidiSmtpServer::Smtpd end end - raise "invalid format for recipient" + raise 'invalid format for recipient' end end diff --git a/app/lib/order_csv.rb b/app/lib/order_csv.rb index b238f90c..e2449596 100644 --- a/app/lib/order_csv.rb +++ b/app/lib/order_csv.rb @@ -14,7 +14,7 @@ class OrderCsv < RenderCsv end def data - @object.order_articles.ordered.includes([:article, :article_price]).all.map do |oa| + @object.order_articles.ordered.includes(%i[article article_price]).all.map do |oa| yield [ oa.units_to_order, oa.article.order_number, diff --git a/app/lib/order_pdf.rb b/app/lib/order_pdf.rb index 164be66b..655e5fbe 100644 --- a/app/lib/order_pdf.rb +++ b/app/lib/order_pdf.rb @@ -55,7 +55,7 @@ class OrderPdf < RenderPdf end def group_order_article_quantity_with_tolerance(goa) - goa.tolerance > 0 ? "#{goa.quantity} + #{goa.tolerance}" : "#{goa.quantity}" + goa.tolerance > 0 ? "#{goa.quantity} + #{goa.tolerance}" : goa.quantity.to_s end def group_order_article_result(goa) @@ -88,7 +88,7 @@ class OrderPdf < RenderPdf .pluck('groups.name', 'SUM(group_orders.price)', 'ordergroup_id', 'SUM(group_orders.transport)') result.map do |item| - [item.first || stock_ordergroup_name] + item[1..-1] + [item.first || stock_ordergroup_name] + item[1..] end end @@ -103,7 +103,7 @@ class OrderPdf < RenderPdf def each_ordergroup_batch(batch_size) offset = 0 - while true + loop do go_records = ordergroups(offset, batch_size) break unless go_records.any? @@ -136,7 +136,7 @@ class OrderPdf < RenderPdf group_order_articles(ordergroup) .includes(order_article: { article: [:supplier] }) .order('suppliers.name, articles.name') - .preload(order_article: [:article_price, :order]) + .preload(order_article: %i[article_price order]) .each(&block) end diff --git a/app/lib/order_txt.rb b/app/lib/order_txt.rb index 7f23e705..320e429f 100644 --- a/app/lib/order_txt.rb +++ b/app/lib/order_txt.rb @@ -8,23 +8,19 @@ class OrderTxt def to_txt supplier = @order.supplier contact = FoodsoftConfig[:contact].symbolize_keys - text = I18n.t('orders.fax.heading', :name => FoodsoftConfig[:name]) - text += "\n#{Supplier.human_attribute_name(:customer_number)}: #{supplier.customer_number}" unless supplier.customer_number.blank? + text = I18n.t('orders.fax.heading', name: FoodsoftConfig[:name]) + text += "\n#{Supplier.human_attribute_name(:customer_number)}: #{supplier.customer_number}" if supplier.customer_number.present? text += "\n" + I18n.t('orders.fax.delivery_day') text += "\n\n#{supplier.name}\n#{supplier.address}\n#{Supplier.human_attribute_name(:fax)}: #{supplier.fax}\n\n" - text += "****** " + I18n.t('orders.fax.to_address') + "\n\n" + text += '****** ' + I18n.t('orders.fax.to_address') + "\n\n" text += "#{FoodsoftConfig[:name]}\n#{contact[:street]}\n#{contact[:zip_code]} #{contact[:city]}\n\n" - text += "****** " + I18n.t('orders.fax.articles') + "\n\n" - text += format("%8s %8s %s\n", I18n.t('orders.fax.number'), I18n.t('orders.fax.amount'), I18n.t('orders.fax.name')) + text += '****** ' + I18n.t('orders.fax.articles') + "\n\n" + text += format("%8s %8s %s\n", I18n.t('orders.fax.number'), I18n.t('orders.fax.amount'), + I18n.t('orders.fax.name')) # now display all ordered articles - @order.order_articles.ordered.includes([:article, :article_price]).each do |oa| + @order.order_articles.ordered.includes(%i[article article_price]).each do |oa| text += format("%8s %8d %s\n", oa.article.order_number, oa.units_to_order.to_i, oa.article.name) end text end - - # Helper method to test pdf via rails console: OrderTxt.new(order).save_tmp - def save_tmp - File.write("#{Rails.root}/tmp/#{self.class.to_s.underscore}.txt", to_csv.force_encoding("UTF-8")) - end end diff --git a/app/lib/render_csv.rb b/app/lib/render_csv.rb index 1f20b075..76d77f11 100644 --- a/app/lib/render_csv.rb +++ b/app/lib/render_csv.rb @@ -13,7 +13,7 @@ class RenderCsv end def to_csv - options = @options.select { |k| %w(col_sep row_sep).include? k.to_s } + options = @options.select { |k| %w[col_sep row_sep].include? k.to_s } ret = CSV.generate options do |csv| if h = header csv << h @@ -31,12 +31,6 @@ class RenderCsv yield [] end - # Helper method to test pdf via rails console: OrderCsv.new(order).save_tmp - def save_tmp - encoding = @options[:encoding] || 'UTF-8' - File.write("#{Rails.root}/tmp/#{self.class.to_s.underscore}.csv", to_csv.force_encoding(encoding)) - end - # XXX disable unit to avoid encoding problems, both in unit and whitespace. Also allows computations in spreadsheet. def number_to_currency(number, options = {}) super(number, options.merge({ unit: '' })) diff --git a/app/lib/render_pdf.rb b/app/lib/render_pdf.rb index 479dc4a3..2311e646 100644 --- a/app/lib/render_pdf.rb +++ b/app/lib/render_pdf.rb @@ -28,9 +28,9 @@ class RotatedCell < Prawn::Table::Cell::Text with_font { (@pdf.width_of(@content, options) + padding_top + padding_bottom) * tan_rotation } end - def draw_borders(pt) + def draw_borders(point) @pdf.mask(:line_width, :stroke_color) do - x, y = pt + x, y = point from = [[x - skew, y + (border_top_width / 2.0)], to = [x, y - height - (border_bottom_width / 2.0)]] @@ -118,11 +118,6 @@ class RenderPdf < Prawn::Document render # Render pdf end - # Helper method to test pdf via rails console: OrderByGroups.new(order).save_tmp - def save_tmp - File.write("#{Rails.root}/tmp/#{self.class.to_s.underscore}.pdf", to_pdf.force_encoding("UTF-8")) - end - # @todo avoid underscore instead of unicode whitespace in pdf :/ def number_to_currency(number, options = {}) super(number, options).gsub("\u202f", ' ') if number @@ -148,8 +143,8 @@ class RenderPdf < Prawn::Document protected - def fontsize(n) - n + def fontsize(size) + size end # return whether pagebreak or vertical whitespace is used for breaks diff --git a/app/lib/token_verifier.rb b/app/lib/token_verifier.rb index b481d60f..5f389943 100644 --- a/app/lib/token_verifier.rb +++ b/app/lib/token_verifier.rb @@ -19,9 +19,9 @@ class TokenVerifier < ActiveSupport::MessageVerifier raise InvalidPrefix unless r[1] == @_prefix # return original message - if r.length > 2 - r[2] - end + return unless r.length > 2 + + r[2] end class InvalidMessage < ActiveSupport::MessageVerifier::InvalidSignature; end diff --git a/app/mailers/mailer.rb b/app/mailers/mailer.rb index 52e1354f..90c8a062 100644 --- a/app/mailers/mailer.rb +++ b/app/mailers/mailer.rb @@ -81,7 +81,7 @@ class Mailer < ActionMailer::Base add_order_result_attachments order, options - subject = I18n.t('mailer.order_result_supplier.subject', :name => order.supplier.name) + subject = I18n.t('mailer.order_result_supplier.subject', name: order.supplier.name) subject += " (#{I18n.t('activerecord.attributes.order.pickup')}: #{format_date(order.pickup)})" if order.pickup mail to: order.supplier.email, @@ -122,10 +122,11 @@ class Mailer < ActionMailer::Base if args[:from].is_a? User args[:reply_to] ||= args[:from] - args[:from] = format_address(FoodsoftConfig[:email_sender], I18n.t('mailer.from_via_foodsoft', name: show_user(args[:from]))) + args[:from] = + format_address(FoodsoftConfig[:email_sender], I18n.t('mailer.from_via_foodsoft', name: show_user(args[:from]))) end - [:bcc, :cc, :reply_to, :sender, :to].each do |k| + %i[bcc cc reply_to sender to].each do |k| user = args[k] args[k] = format_address(user.email, show_user(user)) if user.is_a? User end @@ -145,21 +146,21 @@ class Mailer < ActionMailer::Base def self.deliver_now_with_user_locale(user, &block) I18n.with_locale(user.settings['profile']['language']) do - self.deliver_now(&block) + deliver_now(&block) end end def self.deliver_now_with_default_locale(&block) I18n.with_locale(FoodsoftConfig[:default_locale]) do - self.deliver_now(&block) + deliver_now(&block) end end def self.deliver_now message = yield message.deliver_now - rescue => error - MailDeliveryStatus.create email: message.to[0], message: error.message + rescue StandardError => e + MailDeliveryStatus.create email: message.to[0], message: e.message end # separate method to allow plugins to mess with the attachments @@ -169,8 +170,7 @@ class Mailer < ActionMailer::Base end # separate method to allow plugins to mess with the text - def additonal_welcome_text(user) - end + def additonal_welcome_text(user); end private diff --git a/app/models/article.rb b/app/models/article.rb index 76a68605..53cc2708 100644 --- a/app/models/article.rb +++ b/app/models/article.rb @@ -42,7 +42,7 @@ class Article < ApplicationRecord belongs_to :supplier # @!attribute article_prices # @return [Array] Price history (current price first). - has_many :article_prices, -> { order("created_at DESC") } + has_many :article_prices, -> { order('created_at DESC') } # @!attribute order_articles # @return [Array] Order articles for this article. has_many :order_articles @@ -60,16 +60,16 @@ class Article < ApplicationRecord scope :not_in_stock, -> { where(type: nil) } # Validations - validates_presence_of :name, :unit, :price, :tax, :deposit, :unit_quantity, :supplier_id, :article_category - validates_length_of :name, :in => 4..60 - validates_length_of :unit, :in => 1..15 - validates_length_of :note, :maximum => 255 - validates_length_of :origin, :maximum => 255 - validates_length_of :manufacturer, :maximum => 255 - validates_length_of :order_number, :maximum => 255 - validates_numericality_of :price, :greater_than_or_equal_to => 0 - validates_numericality_of :unit_quantity, :greater_than => 0 - validates_numericality_of :deposit, :tax + validates :name, :unit, :price, :tax, :deposit, :unit_quantity, :supplier_id, :article_category, presence: true + validates :name, length: { in: 4..60 } + validates :unit, length: { in: 1..15 } + validates :note, length: { maximum: 255 } + validates :origin, length: { maximum: 255 } + validates :manufacturer, length: { maximum: 255 } + validates :order_number, length: { maximum: 255 } + validates :price, numericality: { greater_than_or_equal_to: 0 } + validates :unit_quantity, numericality: { greater_than: 0 } + validates :deposit, :tax, numericality: true # validates_uniqueness_of :name, :scope => [:supplier_id, :deleted_at, :type], if: Proc.new {|a| a.supplier.shared_sync_method.blank? or a.supplier.shared_sync_method == 'import' } # validates_uniqueness_of :name, :scope => [:supplier_id, :deleted_at, :type, :unit, :unit_quantity] validate :uniqueness_of_name @@ -78,12 +78,12 @@ class Article < ApplicationRecord before_save :update_price_history before_destroy :check_article_in_use - def self.ransackable_attributes(auth_object = nil) - %w(id name supplier_id article_category_id unit note manufacturer origin unit_quantity order_number) + def self.ransackable_attributes(_auth_object = nil) + %w[id name supplier_id article_category_id unit note manufacturer origin unit_quantity order_number] end - def self.ransackable_associations(auth_object = nil) - %w(article_category supplier order_articles orders) + def self.ransackable_associations(_auth_object = nil) + %w[article_category supplier order_articles orders] end # Returns true if article has been updated at least 2 days ago @@ -96,7 +96,7 @@ class Article < ApplicationRecord @in_open_order ||= begin order_articles = OrderArticle.where(order_id: Order.open.collect(&:id)) order_article = order_articles.detect { |oa| oa.article_id == id } - order_article ? order_article.order : nil + order_article&.order end end @@ -112,15 +112,15 @@ class Article < ApplicationRecord def shared_article_changed?(supplier = self.supplier) # skip early if the timestamp hasn't changed shared_article = self.shared_article(supplier) - unless shared_article.nil? || self.shared_updated_on == shared_article.updated_on - attrs = unequal_attributes(shared_article) - if attrs.empty? - # when attributes not changed, update timestamp of article - self.update_attribute(:shared_updated_on, shared_article.updated_on) - false - else - attrs - end + return if shared_article.nil? || shared_updated_on == shared_article.updated_on + + attrs = unequal_attributes(shared_article) + if attrs.empty? + # when attributes not changed, update timestamp of article + update_attribute(:shared_updated_on, shared_article.updated_on) + false + else + attrs end end @@ -131,30 +131,31 @@ class Article < ApplicationRecord def unequal_attributes(new_article, options = {}) # try to convert different units when desired if options[:convert_units] == false - new_price, new_unit_quantity = nil, nil + new_price = nil + new_unit_quantity = nil else new_price, new_unit_quantity = convert_units(new_article) end if new_price && new_unit_quantity - new_unit = self.unit + new_unit = unit else new_price = new_article.price new_unit_quantity = new_article.unit_quantity new_unit = new_article.unit end - return Article.compare_attributes( + Article.compare_attributes( { - :name => [self.name, new_article.name], - :manufacturer => [self.manufacturer, new_article.manufacturer.to_s], - :origin => [self.origin, new_article.origin], - :unit => [self.unit, new_unit], - :price => [self.price.to_f.round(2), new_price.to_f.round(2)], - :tax => [self.tax, new_article.tax], - :deposit => [self.deposit.to_f.round(2), new_article.deposit.to_f.round(2)], + name: [name, new_article.name], + manufacturer: [manufacturer, new_article.manufacturer.to_s], + origin: [origin, new_article.origin], + unit: [unit, new_unit], + price: [price.to_f.round(2), new_price.to_f.round(2)], + tax: [tax, new_article.tax], + deposit: [deposit.to_f.round(2), new_article.deposit.to_f.round(2)], # take care of different num-objects. - :unit_quantity => [self.unit_quantity.to_s.to_f, new_unit_quantity.to_s.to_f], - :note => [self.note.to_s, new_article.note.to_s] + unit_quantity: [unit_quantity.to_s.to_f, new_unit_quantity.to_s.to_f], + note: [note.to_s, new_article.note.to_s] } ) end @@ -165,14 +166,20 @@ class Article < ApplicationRecord # @param attributes [Hash] Attributes with old and new values # @return [Hash] Changed attributes with new values def self.compare_attributes(attributes) - unequal_attributes = attributes.select { |name, values| values[0] != values[1] && !(values[0].blank? && values[1].blank?) } - Hash[unequal_attributes.to_a.map { |a| [a[0], a[1].last] }] + unequal_attributes = attributes.select do |_name, values| + values[0] != values[1] && !(values[0].blank? && values[1].blank?) + end + unequal_attributes.to_a.map { |a| [a[0], a[1].last] }.to_h end # to get the correspondent shared article def shared_article(supplier = self.supplier) - self.order_number.blank? and return nil - @shared_article ||= supplier.shared_supplier.find_article_by_number(self.order_number) rescue nil + order_number.blank? and return nil + @shared_article ||= begin + supplier.shared_supplier.find_article_by_number(order_number) + rescue StandardError + nil + end end # convert units in foodcoop-size @@ -181,31 +188,37 @@ class Article < ApplicationRecord # returns false if units aren't foodsoft-compatible # returns nil if units are eqal def convert_units(new_article = shared_article) - if unit != new_article.unit - # legacy, used by foodcoops in Germany - if new_article.unit == "KI" && unit == "ST" # 'KI' means a box, with a different amount of items in it - # try to match the size out of its name, e.g. "banana 10-12 St" => 10 - new_unit_quantity = /[0-9\-\s]+(St)/.match(new_article.name).to_s.to_i - if new_unit_quantity && new_unit_quantity > 0 - new_price = (new_article.price / new_unit_quantity.to_f).round(2) - [new_price, new_unit_quantity] - else - false - end - else # use ruby-units to convert - fc_unit = (::Unit.new(unit) rescue nil) - supplier_unit = (::Unit.new(new_article.unit) rescue nil) - if fc_unit && supplier_unit && fc_unit =~ supplier_unit - conversion_factor = (supplier_unit / fc_unit).to_base.to_r - new_price = new_article.price / conversion_factor - new_unit_quantity = new_article.unit_quantity * conversion_factor - [new_price, new_unit_quantity] - else - false - end + return unless unit != new_article.unit + + # legacy, used by foodcoops in Germany + if new_article.unit == 'KI' && unit == 'ST' # 'KI' means a box, with a different amount of items in it + # try to match the size out of its name, e.g. "banana 10-12 St" => 10 + new_unit_quantity = /[0-9\-\s]+(St)/.match(new_article.name).to_s.to_i + if new_unit_quantity && new_unit_quantity > 0 + new_price = (new_article.price / new_unit_quantity.to_f).round(2) + [new_price, new_unit_quantity] + else + false + end + else # use ruby-units to convert + fc_unit = begin + ::Unit.new(unit) + rescue StandardError + nil + end + supplier_unit = begin + ::Unit.new(new_article.unit) + rescue StandardError + nil + end + if fc_unit && supplier_unit && fc_unit =~ supplier_unit + conversion_factor = (supplier_unit / fc_unit).to_base.to_r + new_price = new_article.price / conversion_factor + new_unit_quantity = new_article.unit_quantity * conversion_factor + [new_price, new_unit_quantity] + else + false end - else - nil end end @@ -222,19 +235,19 @@ class Article < ApplicationRecord # Checks if the article is in use before it will deleted def check_article_in_use - raise I18n.t('articles.model.error_in_use', :article => self.name.to_s) if self.in_open_order + raise I18n.t('articles.model.error_in_use', article: name.to_s) if in_open_order end # Create an ArticlePrice, when the price-attr are changed. def update_price_history - if price_changed? - article_prices.build( - :price => price, - :tax => tax, - :deposit => deposit, - :unit_quantity => unit_quantity - ) - end + return unless price_changed? + + article_prices.build( + price: price, + tax: tax, + deposit: deposit, + unit_quantity: unit_quantity + ) end def price_changed? @@ -250,8 +263,8 @@ class Article < ApplicationRecord # supplier should always be there - except, perhaps, on initialization (on seeding) if supplier && (supplier.shared_sync_method.blank? || supplier.shared_sync_method == 'import') errors.add :name, :taken if matches.any? - else - errors.add :name, :taken_with_unit if matches.where(unit: unit, unit_quantity: unit_quantity).any? + elsif matches.where(unit: unit, unit_quantity: unit_quantity).any? + errors.add :name, :taken_with_unit end end end diff --git a/app/models/article_category.rb b/app/models/article_category.rb index 28597a59..1574b5d5 100644 --- a/app/models/article_category.rb +++ b/app/models/article_category.rb @@ -17,16 +17,16 @@ class ArticleCategory < ApplicationRecord normalize_attributes :name, :description - validates :name, :presence => true, :uniqueness => true, :length => { :minimum => 2 } + validates :name, presence: true, uniqueness: true, length: { minimum: 2 } before_destroy :check_for_associated_articles - def self.ransackable_attributes(auth_object = nil) - %w(id name) + def self.ransackable_attributes(_auth_object = nil) + %w[id name] end - def self.ransackable_associations(auth_object = nil) - %w(articles order_articles orders) + def self.ransackable_associations(_auth_object = nil) + %w[articles order_articles orders] end # Find a category that matches a category name; may return nil. @@ -40,7 +40,11 @@ class ArticleCategory < ApplicationRecord # case-insensitive substring match (take the closest match = shortest) c = ArticleCategory.where('name LIKE ?', "%#{category}%") unless c && c.any? # case-insensitive phrase present in category description - c = ArticleCategory.where('description LIKE ?', "%#{category}%").select { |s| s.description.match /(^|,)\s*#{category}\s*(,|$)/i } unless c && c.any? + unless c && c.any? + c = ArticleCategory.where('description LIKE ?', "%#{category}%").select do |s| + s.description.match(/(^|,)\s*#{category}\s*(,|$)/i) + end + end # return closest match if there are multiple c = c.sort_by { |s| s.name.length }.first if c.respond_to? :sort_by c @@ -50,6 +54,9 @@ class ArticleCategory < ApplicationRecord # Deny deleting the category when there are associated articles. def check_for_associated_articles - raise I18n.t('activerecord.errors.has_many_left', collection: Article.model_name.human) if articles.undeleted.exists? + return unless articles.undeleted.exists? + + raise I18n.t('activerecord.errors.has_many_left', + collection: Article.model_name.human) end end diff --git a/app/models/article_price.rb b/app/models/article_price.rb index f6879eac..ac3b2b4c 100644 --- a/app/models/article_price.rb +++ b/app/models/article_price.rb @@ -24,8 +24,8 @@ class ArticlePrice < ApplicationRecord localize_input_of :price, :tax, :deposit - validates_presence_of :price, :tax, :deposit, :unit_quantity - validates_numericality_of :price, :greater_than_or_equal_to => 0 - validates_numericality_of :unit_quantity, :greater_than => 0 - validates_numericality_of :deposit, :tax + validates :price, :tax, :deposit, :unit_quantity, presence: true + validates :price, numericality: { greater_than_or_equal_to: 0 } + validates :unit_quantity, numericality: { greater_than: 0 } + validates :deposit, :tax, numericality: true end diff --git a/app/models/bank_account.rb b/app/models/bank_account.rb index de15ee4b..f433b48a 100644 --- a/app/models/bank_account.rb +++ b/app/models/bank_account.rb @@ -5,10 +5,10 @@ class BankAccount < ApplicationRecord normalize_attributes :name, :iban, :description - validates :name, :presence => true, :uniqueness => true, :length => { :minimum => 2 } - validates :iban, :presence => true, :uniqueness => true - validates_format_of :iban, :with => /\A[A-Z]{2}[0-9]{2}[0-9A-Z]{,30}\z/ - validates_numericality_of :balance, :message => I18n.t('bank_account.model.invalid_balance') + validates :name, presence: true, uniqueness: true, length: { minimum: 2 } + validates :iban, presence: true, uniqueness: true + validates :iban, format: { with: /\A[A-Z]{2}[0-9]{2}[0-9A-Z]{,30}\z/ } + validates :balance, numericality: { message: I18n.t('bank_account.model.invalid_balance') } # @return [Function] Method wich can be called to import transaction from a bank or nil if unsupported def find_connector @@ -18,10 +18,8 @@ class BankAccount < ApplicationRecord def assign_unlinked_transactions count = 0 - bank_transactions.without_financial_link.includes(:supplier, :user).each do |t| - if t.assign_to_ordergroup || t.assign_to_invoice - count += 1 - end + bank_transactions.without_financial_link.includes(:supplier, :user).find_each do |t| + count += 1 if t.assign_to_ordergroup || t.assign_to_invoice end count end diff --git a/app/models/bank_gateway.rb b/app/models/bank_gateway.rb index 3811f128..f8043755 100644 --- a/app/models/bank_gateway.rb +++ b/app/models/bank_gateway.rb @@ -4,5 +4,5 @@ class BankGateway < ApplicationRecord scope :with_unattended_support, -> { where.not(unattended_user: nil) } - validates_presence_of :name, :url + validates :name, :url, presence: true end diff --git a/app/models/bank_transaction.rb b/app/models/bank_transaction.rb index 5d9d6c04..0f74d1e0 100644 --- a/app/models/bank_transaction.rb +++ b/app/models/bank_transaction.rb @@ -22,8 +22,8 @@ class BankTransaction < ApplicationRecord belongs_to :supplier, optional: true, foreign_key: 'iban', primary_key: 'iban' belongs_to :user, optional: true, foreign_key: 'iban', primary_key: 'iban' - validates_presence_of :date, :amount, :bank_account_id - validates_numericality_of :amount + validates :date, :amount, :bank_account_id, presence: true + validates :amount, numericality: true scope :without_financial_link, -> { where(financial_link: nil) } @@ -31,13 +31,13 @@ class BankTransaction < ApplicationRecord localize_input_of :amount def image_url - 'data:image/png;base64,' + Base64.encode64(self.image) + 'data:image/png;base64,' + Base64.encode64(image) end def assign_to_invoice return false unless supplier - content = text || "" + content = text || '' content += "\n" + reference if reference.present? invoices = supplier.invoices.unpaid.select { |i| content.include? i.number } invoices_sum = invoices.map(&:amount).sum @@ -49,7 +49,7 @@ class BankTransaction < ApplicationRecord update_attribute :financial_link, link end - return true + true end def assign_to_ordergroup @@ -78,6 +78,6 @@ class BankTransaction < ApplicationRecord update_attribute :financial_link, link end - return true + true end end diff --git a/app/models/concerns/custom_fields.rb b/app/models/concerns/custom_fields.rb index d54cebe5..aafec389 100644 --- a/app/models/concerns/custom_fields.rb +++ b/app/models/concerns/custom_fields.rb @@ -10,7 +10,7 @@ module CustomFields end after_save do - self.settings.custom_fields = custom_fields if custom_fields + settings.custom_fields = custom_fields if custom_fields end end end diff --git a/app/models/concerns/find_each_with_order.rb b/app/models/concerns/find_each_with_order.rb index 0e7cd5cd..faf545b2 100644 --- a/app/models/concerns/find_each_with_order.rb +++ b/app/models/concerns/find_each_with_order.rb @@ -3,9 +3,9 @@ module FindEachWithOrder extend ActiveSupport::Concern class_methods do - def find_each_with_order(options = {}) + def find_each_with_order(options = {}, &block) find_in_batches_with_order(options) do |records| - records.each { |record| yield record } + records.each(&block) end end diff --git a/app/models/concerns/localize_input.rb b/app/models/concerns/localize_input.rb index cfb44a44..296c4c17 100644 --- a/app/models/concerns/localize_input.rb +++ b/app/models/concerns/localize_input.rb @@ -5,12 +5,12 @@ module LocalizeInput return input unless input.is_a? String Rails.logger.debug { "Input: #{input.inspect}" } - separator = I18n.t("separator", scope: "number.format") - delimiter = I18n.t("delimiter", scope: "number.format") - input.gsub!(delimiter, "") if input.match(/\d+#{Regexp.escape(delimiter)}+\d+#{Regexp.escape(separator)}+\d+/) # Remove delimiter - input.gsub!(separator, ".") # Replace separator with db compatible character + separator = I18n.t('separator', scope: 'number.format') + delimiter = I18n.t('delimiter', scope: 'number.format') + input.gsub!(delimiter, '') if input.match(/\d+#{Regexp.escape(delimiter)}+\d+#{Regexp.escape(separator)}+\d+/) # Remove delimiter + input.gsub!(separator, '.') # Replace separator with db compatible character input - rescue + rescue StandardError Rails.logger.warn "Can't localize input: #{input}" input end diff --git a/app/models/concerns/mark_as_deleted_with_name.rb b/app/models/concerns/mark_as_deleted_with_name.rb index 4b888438..fb0aa590 100644 --- a/app/models/concerns/mark_as_deleted_with_name.rb +++ b/app/models/concerns/mark_as_deleted_with_name.rb @@ -3,7 +3,7 @@ module MarkAsDeletedWithName def mark_as_deleted # get maximum length of name - max_length = 100000 + max_length = 100_000 if lenval = self.class.validators_on(:name).detect { |v| v.is_a?(ActiveModel::Validations::LengthValidator) } max_length = lenval.options[:maximum] end diff --git a/app/models/concerns/price_calculation.rb b/app/models/concerns/price_calculation.rb index 03b9a7ad..a78191c0 100644 --- a/app/models/concerns/price_calculation.rb +++ b/app/models/concerns/price_calculation.rb @@ -15,6 +15,6 @@ module PriceCalculation private def add_percent(value, percent) - (value * (percent * 0.01 + 1)).round(2) + (value * ((percent * 0.01) + 1)).round(2) end end diff --git a/app/models/delivery.rb b/app/models/delivery.rb index ab5ca5ec..bb2aed45 100644 --- a/app/models/delivery.rb +++ b/app/models/delivery.rb @@ -4,10 +4,10 @@ class Delivery < StockEvent scope :recent, -> { order('created_at DESC').limit(10) } - validates_presence_of :supplier_id + validates :supplier_id, presence: true validate :stock_articles_must_be_unique - accepts_nested_attributes_for :stock_changes, :allow_destroy => :true + accepts_nested_attributes_for :stock_changes, allow_destroy: :true def new_stock_changes=(stock_change_attributes) for attributes in stock_change_attributes @@ -16,7 +16,7 @@ class Delivery < StockEvent end def includes_article?(article) - self.stock_changes.map { |stock_change| stock_change.stock_article.id }.include? article.id + stock_changes.map { |stock_change| stock_change.stock_article.id }.include? article.id end def sum(type = :gross) @@ -39,8 +39,8 @@ class Delivery < StockEvent protected def stock_articles_must_be_unique - unless stock_changes.reject { |sc| sc.marked_for_destruction? }.map { |sc| sc.stock_article.id }.uniq!.nil? - errors.add(:base, I18n.t('model.delivery.each_stock_article_must_be_unique')) - end + return if stock_changes.reject { |sc| sc.marked_for_destruction? }.map { |sc| sc.stock_article.id }.uniq!.nil? + + errors.add(:base, I18n.t('model.delivery.each_stock_article_must_be_unique')) end end diff --git a/app/models/financial_link.rb b/app/models/financial_link.rb index 30a1955c..51108cd2 100644 --- a/app/models/financial_link.rb +++ b/app/models/financial_link.rb @@ -4,13 +4,13 @@ class FinancialLink < ApplicationRecord has_many :invoices scope :incomplete, -> { with_full_sum.where.not('full_sums.full_sum' => 0) } - scope :unused, -> { + scope :unused, lambda { includes(:bank_transactions, :financial_transactions, :invoices) .where(bank_transactions: { financial_link_id: nil }) .where(financial_transactions: { financial_link_id: nil }) .where(invoices: { financial_link_id: nil }) } - scope :with_full_sum, -> { + scope :with_full_sum, lambda { select(:id, :note, :full_sum).joins(<<-SQL) LEFT JOIN ( SELECT id, COALESCE(bt_sum, 0) - COALESCE(ft_sum, 0) + COALESCE(i_sum, 0) AS full_sum diff --git a/app/models/financial_transaction.rb b/app/models/financial_transaction.rb index bd2c4e58..1556ecbe 100644 --- a/app/models/financial_transaction.rb +++ b/app/models/financial_transaction.rb @@ -8,14 +8,16 @@ class FinancialTransaction < ApplicationRecord belongs_to :financial_link, optional: true belongs_to :financial_transaction_type belongs_to :group_order, optional: true - belongs_to :reverts, optional: true, class_name: 'FinancialTransaction', foreign_key: 'reverts_id' + belongs_to :reverts, optional: true, class_name: 'FinancialTransaction' has_one :reverted_by, class_name: 'FinancialTransaction', foreign_key: 'reverts_id' - validates_presence_of :amount, :note, :user_id - validates_numericality_of :amount, greater_then: -100_000, - less_than: 100_000 + validates :amount, :note, :user_id, presence: true + validates :amount, numericality: { greater_then: -100_000, + less_than: 100_000 } - scope :visible, -> { joins('LEFT JOIN financial_transactions r ON financial_transactions.id = r.reverts_id').where('r.id IS NULL').where(reverts: nil) } + scope :visible, lambda { + joins('LEFT JOIN financial_transactions r ON financial_transactions.id = r.reverts_id').where('r.id IS NULL').where(reverts: nil) + } scope :without_financial_link, -> { where(financial_link: nil) } scope :with_ordergroup, -> { where.not(ordergroup: nil) } @@ -28,12 +30,12 @@ class FinancialTransaction < ApplicationRecord # @todo remove alias (and rename created_on to created_at below) after #575 ransack_alias :created_at, :created_on - def self.ransackable_attributes(auth_object = nil) - %w(id amount note created_on user_id) + def self.ransackable_attributes(_auth_object = nil) + %w[id amount note created_on user_id] end - def self.ransackable_associations(auth_object = nil) - %w() # none, and certainly not user until we've secured that more + def self.ransackable_associations(_auth_object = nil) + %w[] # none, and certainly not user until we've secured that more end # Use this save method instead of simple save and after callback diff --git a/app/models/financial_transaction_class.rb b/app/models/financial_transaction_class.rb index 43ded5fd..0c924993 100644 --- a/app/models/financial_transaction_class.rb +++ b/app/models/financial_transaction_class.rb @@ -5,7 +5,7 @@ class FinancialTransactionClass < ApplicationRecord has_many :ordergroups, -> { distinct }, through: :financial_transactions validates :name, presence: true - validates_uniqueness_of :name + validates :name, uniqueness: true after_save :update_balance_of_ordergroups diff --git a/app/models/financial_transaction_type.rb b/app/models/financial_transaction_type.rb index 392a1a95..97ed7979 100644 --- a/app/models/financial_transaction_type.rb +++ b/app/models/financial_transaction_type.rb @@ -5,13 +5,13 @@ class FinancialTransactionType < ApplicationRecord has_many :ordergroups, -> { distinct }, through: :financial_transactions validates :name, presence: true - validates_uniqueness_of :name - validates_uniqueness_of :name_short, allow_blank: true, allow_nil: true - validates_format_of :name_short, :with => /\A[A-Za-z]*\z/ + validates :name, uniqueness: true + validates :name_short, uniqueness: { allow_blank: true } + validates :name_short, format: { with: /\A[A-Za-z]*\z/ } validates :financial_transaction_class, presence: true - after_save :update_balance_of_ordergroups before_destroy :restrict_deleting_last_financial_transaction_type + after_save :update_balance_of_ordergroups scope :with_name_short, -> { where.not(name_short: [nil, '']) } @@ -20,7 +20,7 @@ class FinancialTransactionType < ApplicationRecord end def self.has_multiple_types - self.count > 1 + count > 1 end protected diff --git a/app/models/group.rb b/app/models/group.rb index a667ea5a..a4a770eb 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -7,8 +7,8 @@ class Group < ApplicationRecord has_many :memberships, dependent: :destroy has_many :users, -> { where(deleted_at: nil) }, through: :memberships - validates :name, :presence => true, :length => { :in => 1..25 } - validates_uniqueness_of :name + validates :name, presence: true, length: { in: 1..25 } + validates :name, uniqueness: true attr_reader :user_tokens @@ -25,7 +25,7 @@ class Group < ApplicationRecord end def user_tokens=(ids) - self.user_ids = ids.split(",") + self.user_ids = ids.split(',') end def deleted? diff --git a/app/models/group_order.rb b/app/models/group_order.rb index f3153c44..183b663a 100644 --- a/app/models/group_order.rb +++ b/app/models/group_order.rb @@ -6,14 +6,14 @@ class GroupOrder < ApplicationRecord belongs_to :order belongs_to :ordergroup, optional: true - has_many :group_order_articles, :dependent => :destroy - has_many :order_articles, :through => :group_order_articles + has_many :group_order_articles, dependent: :destroy + has_many :order_articles, through: :group_order_articles has_one :financial_transaction belongs_to :updated_by, optional: true, class_name: 'User', foreign_key: 'updated_by_user_id' - validates_presence_of :order_id - validates_numericality_of :price - validates_uniqueness_of :ordergroup_id, :scope => :order_id # order groups can only order once per order + validates :order_id, presence: true + validates :price, numericality: true + validates :ordergroup_id, uniqueness: { scope: :order_id } # order groups can only order once per order scope :in_open_orders, -> { joins(:order).merge(Order.open) } scope :in_finished_orders, -> { joins(:order).merge(Order.finished_not_closed) } @@ -21,12 +21,12 @@ class GroupOrder < ApplicationRecord scope :ordered, -> { includes(:ordergroup).order('groups.name') } - def self.ransackable_attributes(auth_object = nil) - %w(id price) + def self.ransackable_attributes(_auth_object = nil) + %w[id price] end - def self.ransackable_associations(auth_object = nil) - %w(order group_order_articles) + def self.ransackable_associations(_auth_object = nil) + %w[order group_order_articles] end # Generate some data for the javascript methods in ordering view @@ -37,24 +37,24 @@ class GroupOrder < ApplicationRecord # load prices and other stuff.... data[:order_articles] = {} - order.articles_grouped_by_category.each do |article_category, order_articles| + order.articles_grouped_by_category.each do |_article_category, order_articles| order_articles.each do |order_article| # Get the result of last time ordering, if possible goa = group_order_articles.detect { |goa| goa.order_article_id == order_article.id } # Build hash with relevant data data[:order_articles][order_article.id] = { - :price => order_article.article.fc_price, - :unit => order_article.article.unit_quantity, - :quantity => (goa ? goa.quantity : 0), - :others_quantity => order_article.quantity - (goa ? goa.quantity : 0), - :used_quantity => (goa ? goa.result(:quantity) : 0), - :tolerance => (goa ? goa.tolerance : 0), - :others_tolerance => order_article.tolerance - (goa ? goa.tolerance : 0), - :used_tolerance => (goa ? goa.result(:tolerance) : 0), - :total_price => (goa ? goa.total_price : 0), - :missing_units => order_article.missing_units, - :quantity_available => (order.stockit? ? order_article.article.quantity_available : 0) + price: order_article.article.fc_price, + unit: order_article.article.unit_quantity, + quantity: (goa ? goa.quantity : 0), + others_quantity: order_article.quantity - (goa ? goa.quantity : 0), + used_quantity: (goa ? goa.result(:quantity) : 0), + tolerance: (goa ? goa.tolerance : 0), + others_tolerance: order_article.tolerance - (goa ? goa.tolerance : 0), + used_tolerance: (goa ? goa.result(:tolerance) : 0), + total_price: (goa ? goa.total_price : 0), + missing_units: order_article.missing_units, + quantity_available: (order.stockit? ? order_article.article.quantity_available : 0) } end end @@ -69,12 +69,12 @@ class GroupOrder < ApplicationRecord # Get ordered quantities and update group_order_articles/_quantities... if group_order_articles_attributes - quantities = group_order_articles_attributes.fetch(order_article.id.to_s, { :quantity => 0, :tolerance => 0 }) + quantities = group_order_articles_attributes.fetch(order_article.id.to_s, { quantity: 0, tolerance: 0 }) group_order_article.update_quantities(quantities[:quantity].to_i, quantities[:tolerance].to_i) end # Also update results for the order_article - logger.debug "[save_group_order_articles] update order_article.results!" + logger.debug '[save_group_order_articles] update order_article.results!' order_article.update_results! end @@ -83,7 +83,7 @@ class GroupOrder < ApplicationRecord # Updates the "price" attribute. def update_price! - total = group_order_articles.includes(:order_article => [:article, :article_price]).to_a.sum(&:total_price) + total = group_order_articles.includes(order_article: %i[article article_price]).to_a.sum(&:total_price) update_attribute(:price, total) end @@ -97,7 +97,12 @@ class GroupOrder < ApplicationRecord end def ordergroup_name - ordergroup ? ordergroup.name : I18n.t('model.group_order.stock_ordergroup_name', :user => updated_by.try(:name) || '?') + if ordergroup + ordergroup.name + else + I18n.t('model.group_order.stock_ordergroup_name', + user: updated_by.try(:name) || '?') + end end def total diff --git a/app/models/group_order_article.rb b/app/models/group_order_article.rb index 5a02734d..7b95d462 100644 --- a/app/models/group_order_article.rb +++ b/app/models/group_order_article.rb @@ -8,21 +8,21 @@ class GroupOrderArticle < ApplicationRecord belongs_to :order_article has_many :group_order_article_quantities, dependent: :destroy - validates_presence_of :group_order, :order_article - validates_uniqueness_of :order_article_id, :scope => :group_order_id # just once an article per group order + validates :group_order, :order_article, presence: true + validates :order_article_id, uniqueness: { scope: :group_order_id } # just once an article per group order validate :check_order_not_closed # don't allow changes to closed (aka settled) orders validates :quantity, :tolerance, numericality: { only_integer: true, greater_than_or_equal_to: 0 } - scope :ordered, -> { includes(:group_order => :ordergroup).order('groups.name') } + scope :ordered, -> { includes(group_order: :ordergroup).order('groups.name') } localize_input_of :result - def self.ransackable_attributes(auth_object = nil) - %w(id quantity tolerance result) + def self.ransackable_attributes(_auth_object = nil) + %w[id quantity tolerance result] end - def self.ransackable_associations(auth_object = nil) - %w(order_article group_order) + def self.ransackable_associations(_auth_object = nil) + %w[order_article group_order] end # Setter used in group_order_article#new @@ -32,7 +32,7 @@ class GroupOrderArticle < ApplicationRecord end def ordergroup_id - group_order.try!(:ordergroup_id) + group_order&.ordergroup_id end # Updates the quantity/tolerance for this GroupOrderArticle by updating both GroupOrderArticle properties @@ -45,7 +45,7 @@ class GroupOrderArticle < ApplicationRecord # When quantity and tolerance are zero, we don't serve any purpose if quantity == 0 && tolerance == 0 - logger.debug("Self-destructing since requested quantity and tolerance are zero") + logger.debug('Self-destructing since requested quantity and tolerance are zero') destroy! return end @@ -54,26 +54,28 @@ class GroupOrderArticle < ApplicationRecord quantities = group_order_article_quantities.order('created_on DESC').to_a logger.debug("GroupOrderArticleQuantity items found: #{quantities.size}") - if (quantities.size == 0) + if quantities.size == 0 # There is no GroupOrderArticleQuantity item yet, just insert with desired quantities... - logger.debug("No quantities entry at all, inserting a new one with the desired quantities") - quantities.push(GroupOrderArticleQuantity.new(:group_order_article => self, :quantity => quantity, :tolerance => tolerance)) - self.quantity, self.tolerance = quantity, tolerance + logger.debug('No quantities entry at all, inserting a new one with the desired quantities') + quantities.push(GroupOrderArticleQuantity.new(group_order_article: self, quantity: quantity, + tolerance: tolerance)) + self.quantity = quantity + self.tolerance = tolerance else # Decrease quantity/tolerance if necessary by going through the existing items and decreasing their values... i = 0 - while (i < quantities.size && (quantity < self.quantity || tolerance < self.tolerance)) + while i < quantities.size && (quantity < self.quantity || tolerance < self.tolerance) logger.debug("Need to decrease quantities for GroupOrderArticleQuantity[#{quantities[i].id}]") - if (quantity < self.quantity && quantities[i].quantity > 0) + if quantity < self.quantity && quantities[i].quantity > 0 delta = self.quantity - quantity - delta = (delta > quantities[i].quantity ? quantities[i].quantity : delta) + delta = [delta, quantities[i].quantity].min logger.debug("Decreasing quantity by #{delta}") quantities[i].quantity -= delta self.quantity -= delta end - if (tolerance < self.tolerance && quantities[i].tolerance > 0) + if tolerance < self.tolerance && quantities[i].tolerance > 0 delta = self.tolerance - tolerance - delta = (delta > quantities[i].tolerance ? quantities[i].tolerance : delta) + delta = [delta, quantities[i].tolerance].min logger.debug("Decreasing tolerance by #{delta}") quantities[i].tolerance -= delta self.tolerance -= delta @@ -81,12 +83,12 @@ class GroupOrderArticle < ApplicationRecord i += 1 end # If there is at least one increased value: insert a new GroupOrderArticleQuantity object - if (quantity > self.quantity || tolerance > self.tolerance) - logger.debug("Inserting a new GroupOrderArticleQuantity") + if quantity > self.quantity || tolerance > self.tolerance + logger.debug('Inserting a new GroupOrderArticleQuantity') quantities.insert(0, GroupOrderArticleQuantity.new( - :group_order_article => self, - :quantity => (quantity > self.quantity ? quantity - self.quantity : 0), - :tolerance => (tolerance > self.tolerance ? tolerance - self.tolerance : 0) + group_order_article: self, + quantity: (quantity > self.quantity ? quantity - self.quantity : 0), + tolerance: (tolerance > self.tolerance ? tolerance - self.tolerance : 0) )) # Recalc totals: self.quantity += quantities[0].quantity @@ -95,8 +97,9 @@ class GroupOrderArticle < ApplicationRecord end # Check if something went terribly wrong and quantites have not been adjusted as desired. - if (self.quantity != quantity || self.tolerance != tolerance) - raise ActiveRecord::RecordNotSaved.new('Unable to update GroupOrderArticle/-Quantities to desired quantities!', self) + if self.quantity != quantity || self.tolerance != tolerance + raise ActiveRecord::RecordNotSaved.new('Unable to update GroupOrderArticle/-Quantities to desired quantities!', + self) end # Remove zero-only items. @@ -121,7 +124,7 @@ class GroupOrderArticle < ApplicationRecord quantity = tolerance = total_quantity = 0 # Get total - if not total.nil? + if !total.nil? logger.debug "<#{order_article.article.name}> => #{total} (given)" elsif order_article.article.is_a?(StockArticle) total = order_article.article.quantity @@ -145,7 +148,7 @@ class GroupOrderArticle < ApplicationRecord q = goaq.quantity q = [q, total - total_quantity].min if first_order_first_serve total_quantity += q - if goaq.group_order_article_id == self.id + if goaq.group_order_article_id == id logger.debug "increasing quantity by #{q}" quantity += q end @@ -154,11 +157,11 @@ class GroupOrderArticle < ApplicationRecord # Determine tolerance to be ordered... if total_quantity < total - logger.debug "determining additional items to be ordered from tolerance" + logger.debug 'determining additional items to be ordered from tolerance' order_quantities.each do |goaq| q = [goaq.tolerance, total - total_quantity].min total_quantity += q - if goaq.group_order_article_id == self.id + if goaq.group_order_article_id == id logger.debug "increasing tolerance by #{q}" tolerance += q end @@ -170,7 +173,7 @@ class GroupOrderArticle < ApplicationRecord end # memoize result unless a total is given - r = { :quantity => quantity, :tolerance => tolerance, :total => quantity + tolerance } + r = { quantity: quantity, tolerance: tolerance, total: quantity + tolerance } @calculate_result = r if total.nil? r end @@ -185,8 +188,8 @@ class GroupOrderArticle < ApplicationRecord # This is used for automatic distribution, e.g., in order.finish! or when receiving orders def save_results!(article_total = nil) new_result = calculate_result(article_total)[:total] - self.update_attribute(:result_computed, new_result) - self.update_attribute(:result, new_result) + update_attribute(:result_computed, new_result) + update_attribute(:result, new_result) end # Returns total price for this individual article @@ -213,8 +216,8 @@ class GroupOrderArticle < ApplicationRecord private def check_order_not_closed - if order_article.order.closed? - errors.add(:order_article, I18n.t('model.group_order_article.order_closed')) - end + return unless order_article.order.closed? + + errors.add(:order_article, I18n.t('model.group_order_article.order_closed')) end end diff --git a/app/models/group_order_article_quantity.rb b/app/models/group_order_article_quantity.rb index 1e29985f..12832b2c 100644 --- a/app/models/group_order_article_quantity.rb +++ b/app/models/group_order_article_quantity.rb @@ -4,5 +4,5 @@ class GroupOrderArticleQuantity < ApplicationRecord belongs_to :group_order_article - validates_presence_of :group_order_article_id + validates :group_order_article_id, presence: true end diff --git a/app/models/invite.rb b/app/models/invite.rb index e37a8a18..d471aa50 100644 --- a/app/models/invite.rb +++ b/app/models/invite.rb @@ -5,12 +5,12 @@ class Invite < ApplicationRecord belongs_to :user belongs_to :group - validates_format_of :email, :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i - validates_presence_of :user - validates_presence_of :group - validates_presence_of :token - validates_presence_of :expires_at - validate :email_not_already_registered, :on => :create + validates :email, format: { with: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i } + validates :user, presence: true + validates :group, presence: true + validates :token, presence: true + validates :expires_at, presence: true + validate :email_not_already_registered, on: :create before_validation :set_token_and_expires_at @@ -19,15 +19,15 @@ class Invite < ApplicationRecord # Before validation, set token and expires_at. def set_token_and_expires_at self.token = Digest::SHA1.hexdigest(Time.now.to_s + rand(100).to_s) - self.expires_at = Time.now.advance(:days => 7) + self.expires_at = Time.now.advance(days: 7) end private # Custom validation: check that email does not already belong to a registered user. def email_not_already_registered - unless User.find_by_email(self.email).nil? - errors.add(:email, I18n.t('invites.errors.already_member')) - end + return if User.find_by_email(email).nil? + + errors.add(:email, I18n.t('invites.errors.already_member')) end end diff --git a/app/models/invoice.rb b/app/models/invoice.rb index f2a8866f..2bf3aaee 100644 --- a/app/models/invoice.rb +++ b/app/models/invoice.rb @@ -3,13 +3,13 @@ class Invoice < ApplicationRecord include LocalizeInput belongs_to :supplier - belongs_to :created_by, :class_name => 'User', :foreign_key => 'created_by_user_id' + belongs_to :created_by, class_name: 'User', foreign_key: 'created_by_user_id' belongs_to :financial_link, optional: true has_many :deliveries, dependent: :nullify has_many :orders, dependent: :nullify - validates_presence_of :supplier_id - validates_numericality_of :amount, :deposit, :deposit_credit + validates :supplier_id, presence: true + validates :amount, :deposit, :deposit_credit, numericality: true validate :valid_attachment scope :unpaid, -> { where(paid_on: nil) } @@ -23,18 +23,18 @@ class Invoice < ApplicationRecord def attachment=(incoming_file) self.attachment_data = incoming_file.read # allow to soft-fail when FileMagic isn't present and removed from Gemfile (e.g. Heroku) - self.attachment_mime = defined?(FileMagic) ? FileMagic.new(FileMagic::MAGIC_MIME).buffer(self.attachment_data) : 'application/octet-stream' + self.attachment_mime = defined?(FileMagic) ? FileMagic.new(FileMagic::MAGIC_MIME).buffer(attachment_data) : 'application/octet-stream' end def delete_attachment=(value) - if value == '1' - self.attachment_data = nil - self.attachment_mime = nil - end + return unless value == '1' + + self.attachment_data = nil + self.attachment_mime = nil end def user_can_edit?(user) - user.role_finance? || (user.role_invoices? && !self.paid_on && self.created_by.try(:id) == user.id) + user.role_finance? || (user.role_invoices? && !paid_on && created_by.try(:id) == user.id) end # Amount without deposit @@ -45,9 +45,9 @@ class Invoice < ApplicationRecord def orders_sum orders .joins(order_articles: [:article_price]) - .sum("COALESCE(order_articles.units_received, order_articles.units_billed, order_articles.units_to_order)" \ - + "* article_prices.unit_quantity" \ - + "* ROUND((article_prices.price + article_prices.deposit) * (100 + article_prices.tax) / 100, 2)") + .sum('COALESCE(order_articles.units_received, order_articles.units_billed, order_articles.units_to_order)' \ + + '* article_prices.unit_quantity' \ + + '* ROUND((article_prices.price + article_prices.deposit) * (100 + article_prices.tax) / 100, 2)') end def orders_transport_sum @@ -63,11 +63,11 @@ class Invoice < ApplicationRecord protected def valid_attachment - if attachment_data - mime = MIME::Type.simplified(attachment_mime) - unless ['application/pdf', 'image/jpeg'].include? mime - errors.add :attachment, I18n.t('model.invoice.invalid_mime', :mime => mime) - end - end + return unless attachment_data + + mime = MIME::Type.simplified(attachment_mime) + return if ['application/pdf', 'image/jpeg'].include? mime + + errors.add :attachment, I18n.t('model.invoice.invalid_mime', mime: mime) end end diff --git a/app/models/membership.rb b/app/models/membership.rb index bebf00e2..4ebc061c 100644 --- a/app/models/membership.rb +++ b/app/models/membership.rb @@ -8,6 +8,6 @@ class Membership < ApplicationRecord # check if this is the last admin-membership and deny def check_last_admin - raise I18n.t('model.membership.no_admin_delete') if self.group.role_admin? && self.group.memberships.size == 1 && Group.where(role_admin: true).count == 1 + raise I18n.t('model.membership.no_admin_delete') if group.role_admin? && group.memberships.size == 1 && Group.where(role_admin: true).count == 1 end end diff --git a/app/models/order.rb b/app/models/order.rb index e83307f3..ada62e59 100644 --- a/app/models/order.rb +++ b/app/models/order.rb @@ -2,29 +2,29 @@ class Order < ApplicationRecord attr_accessor :ignore_warnings, :transport_distribution # Associations - has_many :order_articles, :dependent => :destroy - has_many :articles, :through => :order_articles - has_many :group_orders, :dependent => :destroy - has_many :ordergroups, :through => :group_orders - has_many :users_ordered, :through => :ordergroups, :source => :users - has_many :comments, -> { order('created_at') }, :class_name => "OrderComment" + has_many :order_articles, dependent: :destroy + has_many :articles, through: :order_articles + has_many :group_orders, dependent: :destroy + has_many :ordergroups, through: :group_orders + has_many :users_ordered, through: :ordergroups, source: :users + has_many :comments, -> { order('created_at') }, class_name: 'OrderComment' has_many :stock_changes belongs_to :invoice, optional: true belongs_to :supplier, optional: true - belongs_to :updated_by, :class_name => 'User', :foreign_key => 'updated_by_user_id' - belongs_to :created_by, :class_name => 'User', :foreign_key => 'created_by_user_id' + belongs_to :updated_by, class_name: 'User', foreign_key: 'updated_by_user_id' + belongs_to :created_by, class_name: 'User', foreign_key: 'created_by_user_id' enum end_action: { no_end_action: 0, auto_close: 1, auto_close_and_send: 2, auto_close_and_send_min_quantity: 3 } - enum transport_distribution: [:skip, :ordergroup, :price, :articles] + enum transport_distribution: { skip: 0, ordergroup: 1, price: 2, articles: 3 } # Validations - validates_presence_of :starts + validates :starts, presence: true validate :starts_before_ends, :include_articles validate :keep_ordered_articles + before_validation :distribute_transport # Callbacks after_save :save_order_articles, :update_price_of_group_orders! - before_validation :distribute_transport # Finders scope :started, -> { where('starts <= ?', Time.now) } @@ -49,12 +49,12 @@ class Order < ApplicationRecord include DateTimeAttributeValidate date_time_attribute :starts, :boxfill, :ends - def self.ransackable_attributes(auth_object = nil) - %w(id state supplier_id starts boxfill ends pickup) + def self.ransackable_attributes(_auth_object = nil) + %w[id state supplier_id starts boxfill ends pickup] end - def self.ransackable_associations(auth_object = nil) - %w(supplier articles order_articles) + def self.ransackable_associations(_auth_object = nil) + %w[supplier articles order_articles] end def stockit? @@ -70,9 +70,9 @@ class Order < ApplicationRecord # make sure to include those articles which are no longer available # but which have already been ordered in this stock order StockArticle.available.includes(:article_category) - .order('article_categories.name', 'articles.name').reject { |a| + .order('article_categories.name', 'articles.name').reject do |a| a.quantity_available <= 0 && !a.ordered_in_order?(self) - }.group_by { |a| a.article_category.name } + end.group_by { |a| a.article_category.name } else supplier.articles.available.group_by { |a| a.article_category.name } end @@ -87,9 +87,7 @@ class Order < ApplicationRecord end # Save ids, and create/delete order_articles after successfully saved the order - def article_ids=(ids) - @article_ids = ids - end + attr_writer :article_ids def article_ids @article_ids ||= order_articles.map { |a| a.article_id.to_s } @@ -101,19 +99,19 @@ class Order < ApplicationRecord end def open? - state == "open" + state == 'open' end def finished? - state == "finished" || state == "received" + state == 'finished' || state == 'received' end def received? - state == "received" + state == 'received' end def closed? - state == "closed" + state == 'closed' end def boxfill? @@ -134,11 +132,18 @@ class Order < ApplicationRecord self.starts ||= Time.now if FoodsoftConfig[:order_schedule] # try to be smart when picking a reference day - last = (DateTime.parse(FoodsoftConfig[:order_schedule][:initial]) rescue nil) + last = begin + DateTime.parse(FoodsoftConfig[:order_schedule][:initial]) + rescue StandardError + nil + end last ||= Order.finished.reorder(:starts).first.try(:starts) last ||= self.starts # adjust boxfill and end date - self.boxfill ||= FoodsoftDateUtil.next_occurrence last, self.starts, FoodsoftConfig[:order_schedule][:boxfill] if is_boxfill_useful? + if is_boxfill_useful? + self.boxfill ||= FoodsoftDateUtil.next_occurrence last, self.starts, + FoodsoftConfig[:order_schedule][:boxfill] + end self.ends ||= FoodsoftDateUtil.next_occurrence last, self.starts, FoodsoftConfig[:order_schedule][:ends] end self @@ -149,7 +154,7 @@ class Order < ApplicationRecord def self.ordergroup_group_orders_map(ordergroup) orders = includes(:supplier) group_orders = GroupOrder.where(ordergroup_id: ordergroup.id, order_id: orders.map(&:id)) - group_orders_hash = Hash[group_orders.collect { |go| [go.order_id, go] }] + group_orders_hash = group_orders.index_by { |go| go.order_id } orders.map do |order| { order: order, @@ -160,11 +165,11 @@ class Order < ApplicationRecord # search GroupOrder of given Ordergroup def group_order(ordergroup) - group_orders.where(:ordergroup_id => ordergroup.id).first + group_orders.where(ordergroup_id: ordergroup.id).first end def stock_group_order - group_orders.where(:ordergroup_id => nil).first + group_orders.where(ordergroup_id: nil).first end # Returns OrderArticles in a nested Array, grouped by category and ordered by article name. @@ -172,7 +177,7 @@ class Order < ApplicationRecord # e.g: [["drugs",[teethpaste, toiletpaper]], ["fruits" => [apple, banana, lemon]]] def articles_grouped_by_category @articles_grouped_by_category ||= order_articles - .includes([:article_price, :group_order_articles, :article => :article_category]) + .includes([:article_price, :group_order_articles, { article: :article_category }]) .order('articles.name') .group_by { |a| a.article.article_category.name } .sort { |a, b| a[0] <=> b[0] } @@ -189,10 +194,10 @@ class Order < ApplicationRecord # FIXME: Consider order.foodcoop_result def profit(options = {}) markup = options[:without_markup] || false - if invoice - groups_sum = markup ? sum(:groups_without_markup) : sum(:groups) - groups_sum - invoice.net_amount - end + return unless invoice + + groups_sum = markup ? sum(:groups_without_markup) : sum(:groups) + groups_sum - invoice.net_amount end # Returns the all round price of a finished order @@ -202,7 +207,7 @@ class Order < ApplicationRecord # :fc, guess what... def sum(type = :gross) total = 0 - if type == :net || type == :gross || type == :fc + if %i[net gross fc].include?(type) for oa in order_articles.ordered.includes(:article, :article_price) quantity = oa.units * oa.price.unit_quantity case type @@ -214,8 +219,8 @@ class Order < ApplicationRecord total += quantity * oa.price.fc_price end end - elsif type == :groups || type == :groups_without_markup - for go in group_orders.includes(group_order_articles: { order_article: [:article, :article_price] }) + elsif %i[groups groups_without_markup].include?(type) + for go in group_orders.includes(group_order_articles: { order_article: %i[article article_price] }) for goa in go.group_order_articles case type when :groups @@ -232,36 +237,36 @@ class Order < ApplicationRecord # Finishes this order. This will set the order state to "finish" and the end property to the current time. # Ignored if the order is already finished. def finish!(user) - unless finished? - Order.transaction do - # set new order state (needed by notify_order_finished) - update!(state: 'finished', ends: Time.now, updated_by: user) + return if finished? - # Update order_articles. Save the current article_price to keep price consistency - # Also save results for each group_order_result - # Clean up - order_articles.includes(:article).each do |oa| - oa.update_attribute(:article_price, oa.article.article_prices.first) - oa.group_order_articles.each do |goa| - goa.save_results! - # Delete no longer required order-history (group_order_article_quantities) and - # TODO: Do we need articles, which aren't ordered? (units_to_order == 0 ?) - # A: Yes, we do - for redistributing articles when the number of articles - # delivered changes, and for statistics on popular articles. Records - # with both tolerance and quantity zero can be deleted. - # goa.group_order_article_quantities.clear - end + Order.transaction do + # set new order state (needed by notify_order_finished) + update!(state: 'finished', ends: Time.now, updated_by: user) + + # Update order_articles. Save the current article_price to keep price consistency + # Also save results for each group_order_result + # Clean up + order_articles.includes(:article).find_each do |oa| + oa.update_attribute(:article_price, oa.article.article_prices.first) + oa.group_order_articles.each do |goa| + goa.save_results! + # Delete no longer required order-history (group_order_article_quantities) and + # TODO: Do we need articles, which aren't ordered? (units_to_order == 0 ?) + # A: Yes, we do - for redistributing articles when the number of articles + # delivered changes, and for statistics on popular articles. Records + # with both tolerance and quantity zero can be deleted. + # goa.group_order_article_quantities.clear end - - # Update GroupOrder prices - group_orders.each(&:update_price!) - - # Stats - ordergroups.each(&:update_stats!) - - # Notifications - NotifyFinishedOrderJob.perform_later(self) end + + # Update GroupOrder prices + group_orders.each(&:update_price!) + + # Stats + ordergroups.each(&:update_stats!) + + # Notifications + NotifyFinishedOrderJob.perform_later(self) end end @@ -277,11 +282,11 @@ class Order < ApplicationRecord if stockit? # Decreases the quantity of stock_articles for oa in order_articles.includes(:article) oa.update_results! # Update units_to_order of order_article - stock_changes.create! :stock_article => oa.article, :quantity => oa.units_to_order * -1 + stock_changes.create! stock_article: oa.article, quantity: oa.units_to_order * -1 end end - self.update!(state: 'closed', updated_by: user, foodcoop_result: profit) + update!(state: 'closed', updated_by: user, foodcoop_result: profit) end end @@ -289,7 +294,10 @@ class Order < ApplicationRecord def close_direct!(user) raise I18n.t('orders.model.error_closed') if closed? - comments.create(user: user, text: I18n.t('orders.model.close_direct_message')) unless FoodsoftConfig[:charge_members_manually] + unless FoodsoftConfig[:charge_members_manually] + comments.create(user: user, + text: I18n.t('orders.model.close_direct_message')) + end update!(state: 'closed', updated_by: user) end @@ -313,13 +321,12 @@ class Order < ApplicationRecord end def self.finish_ended! - orders = Order.where.not(end_action: Order.end_actions[:no_end_action]).where(state: 'open').where('ends <= ?', DateTime.now) + orders = Order.where.not(end_action: Order.end_actions[:no_end_action]).where(state: 'open').where('ends <= ?', + DateTime.now) orders.each do |order| - begin - order.do_end_action! - rescue => error - ExceptionNotifier.notify_exception(error, data: { foodcoop: FoodsoftConfig.scope, order_id: order.id }) - end + order.do_end_action! + rescue StandardError => e + ExceptionNotifier.notify_exception(e, data: { foodcoop: FoodsoftConfig.scope, order_id: order.id }) end end @@ -329,7 +336,10 @@ class Order < ApplicationRecord delta = Rails.env.test? ? 1 : 0 # since Rails 4.2 tests appear to have time differences, with this validation failing errors.add(:ends, I18n.t('orders.model.error_starts_before_ends')) if ends && starts && ends <= (starts - delta) errors.add(:ends, I18n.t('orders.model.error_boxfill_before_ends')) if ends && boxfill && ends <= (boxfill - delta) - errors.add(:boxfill, I18n.t('orders.model.error_starts_before_boxfill')) if boxfill && starts && boxfill <= (starts - delta) + return unless boxfill && starts && boxfill <= (starts - delta) + + errors.add(:boxfill, + I18n.t('orders.model.error_starts_before_boxfill')) end def include_articles @@ -340,17 +350,17 @@ class Order < ApplicationRecord chosen_order_articles = order_articles.where(article_id: article_ids) to_be_removed = order_articles - chosen_order_articles to_be_removed_but_ordered = to_be_removed.select { |a| a.quantity > 0 || a.tolerance > 0 } - unless to_be_removed_but_ordered.empty? || ignore_warnings - errors.add(:articles, I18n.t(stockit? ? 'orders.model.warning_ordered_stock' : 'orders.model.warning_ordered')) - @erroneous_article_ids = to_be_removed_but_ordered.map { |a| a.article_id } - end + return if to_be_removed_but_ordered.empty? || ignore_warnings + + errors.add(:articles, I18n.t(stockit? ? 'orders.model.warning_ordered_stock' : 'orders.model.warning_ordered')) + @erroneous_article_ids = to_be_removed_but_ordered.map { |a| a.article_id } end def save_order_articles # fetch selected articles articles_list = Article.find(article_ids) # create new order_articles - (articles_list - articles).each { |article| order_articles.create(:article => article) } + (articles_list - articles).each { |article| order_articles.create(article: article) } # delete old order_articles articles.reject { |article| articles_list.include?(article) }.each do |article| order_articles.detect { |order_article| order_article.article_id == article.id }.destroy @@ -363,17 +373,17 @@ class Order < ApplicationRecord return unless group_orders.any? case transport_distribution.try(&:to_i) - when Order.transport_distributions[:ordergroup] then + when Order.transport_distributions[:ordergroup] amount = transport / group_orders.size group_orders.each do |go| go.transport = amount.ceil(2) end - when Order.transport_distributions[:price] then + when Order.transport_distributions[:price] amount = transport / group_orders.sum(:price) group_orders.each do |go| go.transport = (amount * go.price).ceil(2) end - when Order.transport_distributions[:articles] then + when Order.transport_distributions[:articles] amount = transport / group_orders.includes(:group_order_articles).sum(:result) group_orders.each do |go| go.transport = (amount * go.group_order_articles.sum(:result)).ceil(2) @@ -389,7 +399,7 @@ class Order < ApplicationRecord def charge_group_orders!(user, transaction_type = nil) note = transaction_note - group_orders.includes(:ordergroup).each do |group_order| + group_orders.includes(:ordergroup).find_each do |group_order| if group_order.ordergroup price = group_order.total * -1 # decrease! account balance group_order.ordergroup.add_financial_transaction!(price, note, user, transaction_type, nil, group_order) diff --git a/app/models/order_article.rb b/app/models/order_article.rb index cda24ae2..14193d15 100644 --- a/app/models/order_article.rb +++ b/app/models/order_article.rb @@ -7,25 +7,27 @@ class OrderArticle < ApplicationRecord belongs_to :order belongs_to :article belongs_to :article_price, optional: true - has_many :group_order_articles, :dependent => :destroy + has_many :group_order_articles, dependent: :destroy - validates_presence_of :order_id, :article_id + validates :order_id, :article_id, presence: true validate :article_and_price_exist - validates_uniqueness_of :article_id, scope: :order_id + validates :article_id, uniqueness: { scope: :order_id } - _ordered_sql = "order_articles.units_to_order > 0 OR order_articles.units_billed > 0 OR order_articles.units_received > 0" + _ordered_sql = 'order_articles.units_to_order > 0 OR order_articles.units_billed > 0 OR order_articles.units_received > 0' scope :ordered, -> { where(_ordered_sql) } - scope :ordered_or_member, -> { includes(:group_order_articles).where("#{_ordered_sql} OR order_articles.quantity > 0 OR group_order_articles.result > 0") } + scope :ordered_or_member, lambda { + includes(:group_order_articles).where("#{_ordered_sql} OR order_articles.quantity > 0 OR group_order_articles.result > 0") + } before_create :init_from_balancing after_destroy :update_ordergroup_prices - def self.ransackable_attributes(auth_object = nil) - %w(id order_id article_id quantity tolerance units_to_order) + def self.ransackable_attributes(_auth_object = nil) + %w[id order_id article_id quantity tolerance units_to_order] end - def self.ransackable_associations(auth_object = nil) - %w(order article) + def self.ransackable_associations(_auth_object = nil) + %w[order article] end # This method returns either the ArticlePrice or the Article @@ -46,7 +48,7 @@ class OrderArticle < ApplicationRecord # In balancing this can differ from ordered (by supplier) quantity for this article. def group_orders_sum quantity = group_order_articles.collect(&:result).sum - { :quantity => quantity, :price => quantity * price.fc_price } + { quantity: quantity, price: quantity * price.fc_price } end # Update quantity/tolerance/units_to_order from group_order_articles @@ -97,15 +99,13 @@ class OrderArticle < ApplicationRecord units * price.unit_quantity * price.gross_price end - def ordered_quantities_different_from_group_orders?(ordered_mark = "!", billed_mark = "?", received_mark = "?") - if not units_received.nil? - ((units_received * price.unit_quantity) == group_orders_sum[:quantity]) ? false : received_mark - elsif not units_billed.nil? - ((units_billed * price.unit_quantity) == group_orders_sum[:quantity]) ? false : billed_mark - elsif not units_to_order.nil? - ((units_to_order * price.unit_quantity) == group_orders_sum[:quantity]) ? false : ordered_mark - else - nil # can happen in integration tests + def ordered_quantities_different_from_group_orders?(ordered_mark = '!', billed_mark = '?', received_mark = '?') + if !units_received.nil? + (units_received * price.unit_quantity) == group_orders_sum[:quantity] ? false : received_mark + elsif !units_billed.nil? + (units_billed * price.unit_quantity) == group_orders_sum[:quantity] ? false : billed_mark + elsif !units_to_order.nil? + (units_to_order * price.unit_quantity) == group_orders_sum[:quantity] ? false : ordered_mark end end @@ -124,7 +124,7 @@ class OrderArticle < ApplicationRecord if surplus.index(:tolerance).nil? qty_for_members = [qty_left, self.quantity].min else - qty_for_members = [qty_left, self.quantity + self.tolerance].min + qty_for_members = [qty_left, self.quantity + tolerance].min counts[surplus.index(:tolerance)] = [0, qty_for_members - self.quantity].max end @@ -139,9 +139,7 @@ class OrderArticle < ApplicationRecord # 2) if not found, create new stock article # avoiding duplicate stock article names end - if qty_left > 0 && surplus.index(nil) - counts[surplus.index(nil)] = qty_left - end + counts[surplus.index(nil)] = qty_left if qty_left > 0 && surplus.index(nil) # Update GroupOrder prices & Ordergroup stats # TODO only affected group_orders, and once after redistributing all articles @@ -150,7 +148,7 @@ class OrderArticle < ApplicationRecord order.ordergroups.each(&:update_stats!) end - # TODO notifications + # TODO: notifications counts end @@ -159,7 +157,7 @@ class OrderArticle < ApplicationRecord def update_article_and_price!(order_article_attributes, article_attributes, price_attributes = nil) OrderArticle.transaction do # Updates self - self.update!(order_article_attributes) + update!(order_article_attributes) # Updates article article.update!(article_attributes) @@ -186,7 +184,7 @@ class OrderArticle < ApplicationRecord end def update_global_price=(value) - @update_global_price = (value == true || value == '1') ? true : false + @update_global_price = [true, '1'].include?(value) ? true : false end # @return [Number] Units missing for the last +unit_quantity+ of the article. @@ -210,16 +208,19 @@ class OrderArticle < ApplicationRecord private def article_and_price_exist - errors.add(:article, I18n.t('model.order_article.error_price')) if !(article = Article.find(article_id)) || article.fc_price.nil? - rescue + if !(article = Article.find(article_id)) || article.fc_price.nil? + errors.add(:article, + I18n.t('model.order_article.error_price')) + end + rescue StandardError errors.add(:article, I18n.t('model.order_article.error_price')) end # Associate with current article price if created in a finished order def init_from_balancing - if order.present? && order.finished? - self.article_price = article.article_prices.first - end + return unless order.present? && order.finished? + + self.article_price = article.article_prices.first end def update_ordergroup_prices @@ -241,7 +242,8 @@ class OrderArticle < ApplicationRecord unless (delta_q == 0 && delta_t >= 0) || (delta_mis < 0 && delta_box >= 0 && delta_t >= 0) || (delta_q > 0 && delta_q == -delta_t) - raise ActiveRecord::RecordNotSaved.new("Change not acceptable in boxfill phase for '#{article.name}', sorry.", self) + raise ActiveRecord::RecordNotSaved.new("Change not acceptable in boxfill phase for '#{article.name}', sorry.", + self) end end diff --git a/app/models/order_comment.rb b/app/models/order_comment.rb index 5f35d98c..b11388b0 100644 --- a/app/models/order_comment.rb +++ b/app/models/order_comment.rb @@ -2,6 +2,6 @@ class OrderComment < ApplicationRecord belongs_to :order belongs_to :user - validates_presence_of :order_id, :user_id, :text - validates_length_of :text, :minimum => 3 + validates :order_id, :user_id, :text, presence: true + validates :text, length: { minimum: 3 } end diff --git a/app/models/ordergroup.rb b/app/models/ordergroup.rb index c29ec762..6770fc55 100644 --- a/app/models/ordergroup.rb +++ b/app/models/ordergroup.rb @@ -15,7 +15,7 @@ class Ordergroup < Group has_many :orders, through: :group_orders has_many :group_order_articles, through: :group_orders - validates_numericality_of :account_balance, :message => I18n.t('ordergroups.model.invalid_balance') + validates :account_balance, numericality: { message: I18n.t('ordergroups.model.invalid_balance') } validate :uniqueness_of_name, :uniqueness_of_members after_create :update_stats! @@ -32,7 +32,7 @@ class Ordergroup < Group def self.include_transaction_class_sum columns = ['groups.*'] - FinancialTransactionClass.all.each do |c| + FinancialTransactionClass.all.find_each do |c| columns << "sum(CASE financial_transaction_types.financial_transaction_class_id WHEN #{c.id} THEN financial_transactions.amount ELSE 0 END) AS sum_of_class_#{c.id}" end @@ -51,9 +51,9 @@ class Ordergroup < Group def last_user_activity last_active_user = users.order('users.last_activity DESC').first - if last_active_user - last_active_user.last_activity - end + return unless last_active_user + + last_active_user.last_activity end # the most recent order this ordergroup was participating in @@ -86,12 +86,14 @@ class Ordergroup < Group # Throws an exception if it fails. def add_financial_transaction!(amount, note, user, transaction_type, link = nil, group_order = nil) transaction do - t = FinancialTransaction.new(ordergroup: self, amount: amount, note: note, user: user, financial_transaction_type: transaction_type, financial_link: link, group_order: group_order) + t = FinancialTransaction.new(ordergroup: self, amount: amount, note: note, user: user, + financial_transaction_type: transaction_type, financial_link: link, group_order: group_order) t.save! update_balance! # Notify only when order group had a positive balance before the last transaction: - if t.amount < 0 && self.account_balance < 0 && self.account_balance - t.amount >= 0 - NotifyNegativeBalanceJob.perform_later(self, t) + if t.amount < 0 && account_balance < 0 && account_balance - t.amount >= 0 + NotifyNegativeBalanceJob.perform_later(self, + t) end t end @@ -101,10 +103,11 @@ class Ordergroup < Group # Get hours for every job of each user in period jobs = users.to_a.sum { |u| u.tasks.done.where('updated_on > ?', APPLE_MONTH_AGO.month.ago).sum(:duration) } # Get group_order.price for every finished order in this period - orders_sum = group_orders.includes(:order).merge(Order.finished).where('orders.ends >= ?', APPLE_MONTH_AGO.month.ago).references(:orders).sum(:price) + orders_sum = group_orders.includes(:order).merge(Order.finished).where('orders.ends >= ?', + APPLE_MONTH_AGO.month.ago).references(:orders).sum(:price) @readonly = false # Dirty hack, avoid getting RecordReadOnly exception when called in task after_save callback. A rails bug? - update_attribute(:stats, { :jobs_size => jobs, :orders_sum => orders_sum }) + update_attribute(:stats, { jobs_size: jobs, orders_sum: orders_sum }) end def update_balance! @@ -116,13 +119,17 @@ class Ordergroup < Group end def avg_jobs_per_euro - stats[:jobs_size].to_f / stats[:orders_sum].to_f rescue 0 + stats[:jobs_size].to_f / stats[:orders_sum].to_f + rescue StandardError + 0 end # This is the ordergroup job per euro performance # in comparison to the hole foodcoop average def apples - ((avg_jobs_per_euro / Ordergroup.avg_jobs_per_euro) * 100).to_i rescue 0 + ((avg_jobs_per_euro / Ordergroup.avg_jobs_per_euro) * 100).to_i + rescue StandardError + 0 end # If the the option stop_ordering_under is set, the ordergroup is only allowed to participate in an order, @@ -141,7 +148,11 @@ class Ordergroup < Group # Global average def self.avg_jobs_per_euro stats = Ordergroup.pluck(:stats) - stats.sum { |s| s[:jobs_size].to_f } / stats.sum { |s| s[:orders_sum].to_f } rescue 0 + begin + stats.sum { |s| s[:jobs_size].to_f } / stats.sum { |s| s[:orders_sum].to_f } + rescue StandardError + 0 + end end def account_updated @@ -149,22 +160,22 @@ class Ordergroup < Group end def self.sort_by_param(param) - param ||= "name" + param ||= 'name' sort_param_map = { - "name" => "name", - "name_reverse" => "name DESC", - "members_count" => "count(users.id)", - "members_count_reverse" => "count(users.id) DESC", - "last_user_activity" => "max(users.last_activity)", - "last_user_activity_reverse" => "max(users.last_activity) DESC", - "last_order" => "max(orders.starts)", - "last_order_reverse" => "max(orders.starts) DESC" + 'name' => 'name', + 'name_reverse' => 'name DESC', + 'members_count' => 'count(users.id)', + 'members_count_reverse' => 'count(users.id) DESC', + 'last_user_activity' => 'max(users.last_activity)', + 'last_user_activity_reverse' => 'max(users.last_activity) DESC', + 'last_order' => 'max(orders.starts)', + 'last_order_reverse' => 'max(orders.starts) DESC' } result = self - result = result.left_joins(:users).group("groups.id") if param.starts_with?("members_count", "last_user_activity") - result = result.left_joins(:orders).group("groups.id") if param.starts_with?("last_order") + result = result.left_joins(:users).group('groups.id') if param.starts_with?('members_count', 'last_user_activity') + result = result.left_joins(:orders).group('groups.id') if param.starts_with?('last_order') # Never pass user input data to Arel.sql() because of SQL Injection vulnerabilities. # This case here is okay, as param is mapped to the actual order string. @@ -176,17 +187,21 @@ class Ordergroup < Group # Make sure, that a user can only be in one ordergroup def uniqueness_of_members users.each do |user| - errors.add :user_tokens, I18n.t('ordergroups.model.error_single_group', :user => user.display) if user.groups.where(:type => 'Ordergroup').size > 1 + next unless user.groups.where(type: 'Ordergroup').size > 1 + + errors.add :user_tokens, + I18n.t('ordergroups.model.error_single_group', + user: user.display) end end # Make sure, the name is uniq, add usefull message if uniq group is already deleted def uniqueness_of_name group = Ordergroup.where(name: name) - group = group.where.not(id: self.id) unless new_record? - if group.exists? - message = group.first.deleted? ? :taken_with_deleted : :taken - errors.add :name, message - end + group = group.where.not(id: id) unless new_record? + return unless group.exists? + + message = group.first.deleted? ? :taken_with_deleted : :taken + errors.add :name, message end end diff --git a/app/models/periodic_task_group.rb b/app/models/periodic_task_group.rb index c0a2b10f..f9e9f249 100644 --- a/app/models/periodic_task_group.rb +++ b/app/models/periodic_task_group.rb @@ -5,7 +5,7 @@ class PeriodicTaskGroup < ApplicationRecord return false if tasks.empty? return false if tasks.first.due_date.nil? - return true + true end def create_next_task @@ -18,15 +18,13 @@ class PeriodicTaskGroup < ApplicationRecord next_task.save self.next_task_date += period_days - self.save + save end def create_tasks_until(create_until) - if has_next_task? - while next_task_date.nil? || next_task_date < create_until - create_next_task - end - end + return unless has_next_task? + + create_next_task while next_task_date.nil? || next_task_date < create_until end def create_tasks_for_upfront_days @@ -36,7 +34,7 @@ class PeriodicTaskGroup < ApplicationRecord end def exclude_tasks_before(task) - tasks.where("due_date < '#{task.due_date}'").each do |t| + tasks.where("due_date < '#{task.due_date}'").find_each do |t| t.update_attribute(:periodic_task_group, nil) end end @@ -53,7 +51,7 @@ class PeriodicTaskGroup < ApplicationRecord due_date: task.due_date + due_date_delta) end group_tasks.each do |task| - task.update_columns(periodic_task_group_id: self.id) + task.update_columns(periodic_task_group_id: id) end end diff --git a/app/models/shared_article.rb b/app/models/shared_article.rb index 238b48f0..c390a021 100644 --- a/app/models/shared_article.rb +++ b/app/models/shared_article.rb @@ -4,23 +4,23 @@ class SharedArticle < ApplicationRecord # set correct table_name in external DB self.table_name = 'articles' - belongs_to :shared_supplier, :foreign_key => :supplier_id + belongs_to :shared_supplier, foreign_key: :supplier_id def build_new_article(supplier) supplier.articles.build( - :name => name, - :unit => unit, - :note => note, - :manufacturer => manufacturer, - :origin => origin, - :price => price, - :tax => tax, - :deposit => deposit, - :unit_quantity => unit_quantity, - :order_number => number, - :article_category => ArticleCategory.find_match(category), + name: name, + unit: unit, + note: note, + manufacturer: manufacturer, + origin: origin, + price: price, + tax: tax, + deposit: deposit, + unit_quantity: unit_quantity, + order_number: number, + article_category: ArticleCategory.find_match(category), # convert to db-compatible-string - :shared_updated_on => updated_on.to_formatted_s(:db) + shared_updated_on: updated_on.to_fs(:db) ) end end diff --git a/app/models/shared_supplier.rb b/app/models/shared_supplier.rb index 29c9c1ab..e2b23805 100644 --- a/app/models/shared_supplier.rb +++ b/app/models/shared_supplier.rb @@ -5,10 +5,10 @@ class SharedSupplier < ApplicationRecord self.table_name = 'suppliers' has_many :suppliers, -> { undeleted } - has_many :shared_articles, :foreign_key => :supplier_id + has_many :shared_articles, foreign_key: :supplier_id def find_article_by_number(order_number) - # note that `shared_articles` uses number instead order_number + # NOTE: that `shared_articles` uses number instead order_number cached_articles.detect { |a| a.number == order_number } end @@ -19,15 +19,18 @@ class SharedSupplier < ApplicationRecord # These set of attributes are used to autofill attributes of new supplier, # when created by import from shared supplier feature. def autofill_attributes - whitelist = %w(name address phone fax email url delivery_days note) + whitelist = %w[name address phone fax email url delivery_days note] attributes.select { |k, _v| whitelist.include?(k) } end # return list of synchronisation methods available for this supplier def shared_sync_methods methods = [] - methods += %w(all_available all_unavailable) if shared_articles.count < FoodsoftConfig[:shared_supplier_article_sync_limit] - methods += %w(import) + if shared_articles.count < FoodsoftConfig[:shared_supplier_article_sync_limit] + methods += %w[all_available + all_unavailable] + end + methods += %w[import] methods end end diff --git a/app/models/stock_article.rb b/app/models/stock_article.rb index 42a06d49..14b8d5ef 100644 --- a/app/models/stock_article.rb +++ b/app/models/stock_article.rb @@ -10,11 +10,11 @@ class StockArticle < Article ransack_alias :quantity_available, :quantity # in-line with {StockArticleSerializer} def self.ransackable_attributes(auth_object = nil) - super(auth_object) - %w(supplier_id) + %w(quantity) + super(auth_object) - %w[supplier_id] + %w[quantity] end def self.ransackable_associations(auth_object = nil) - super(auth_object) - %w(supplier) + super(auth_object) - %w[supplier] end # Update the quantity of items in stock @@ -48,7 +48,7 @@ class StockArticle < Article protected def check_quantity - raise I18n.t('stockit.check.not_empty', :name => name) unless quantity == 0 + raise I18n.t('stockit.check.not_empty', name: name) unless quantity == 0 end # Overwrite Price history of Article. For StockArticles isn't it necessary. diff --git a/app/models/stock_change.rb b/app/models/stock_change.rb index 4cbd8939..03d92c74 100644 --- a/app/models/stock_change.rb +++ b/app/models/stock_change.rb @@ -4,11 +4,11 @@ class StockChange < ApplicationRecord belongs_to :stock_taking, optional: true, foreign_key: 'stock_event_id' belongs_to :stock_article - validates_presence_of :stock_article_id, :quantity - validates_numericality_of :quantity + validates :stock_article_id, :quantity, presence: true + validates :quantity, numericality: true - after_save :update_article_quantity after_destroy :update_article_quantity + after_save :update_article_quantity protected diff --git a/app/models/stock_event.rb b/app/models/stock_event.rb index 4fd82864..7134f7b0 100644 --- a/app/models/stock_event.rb +++ b/app/models/stock_event.rb @@ -2,5 +2,5 @@ class StockEvent < ApplicationRecord has_many :stock_changes, dependent: :destroy has_many :stock_articles, through: :stock_changes - validates_presence_of :date + validates :date, presence: true end diff --git a/app/models/supplier.rb b/app/models/supplier.rb index 862f5c24..56999be1 100644 --- a/app/models/supplier.rb +++ b/app/models/supplier.rb @@ -2,7 +2,9 @@ class Supplier < ApplicationRecord include MarkAsDeletedWithName include CustomFields - has_many :articles, -> { where(:type => nil).includes(:article_category).order('article_categories.name', 'articles.name') } + has_many :articles, lambda { + where(type: nil).includes(:article_category).order('article_categories.name', 'articles.name') + } has_many :stock_articles, -> { includes(:article_category).order('article_categories.name', 'articles.name') } has_many :orders has_many :deliveries @@ -10,24 +12,24 @@ class Supplier < ApplicationRecord belongs_to :supplier_category belongs_to :shared_supplier, optional: true # for the sharedLists-App - validates :name, :presence => true, :length => { :in => 4..30 } - validates :phone, :presence => true, :length => { :in => 8..25 } - validates :address, :presence => true, :length => { :in => 8..50 } - validates_format_of :iban, :with => /\A[A-Z]{2}[0-9]{2}[0-9A-Z]{,30}\z/, :allow_blank => true - validates_uniqueness_of :iban, :case_sensitive => false, :allow_blank => true - validates_length_of :order_howto, :note, maximum: 250 + validates :name, presence: true, length: { in: 4..30 } + validates :phone, presence: true, length: { in: 8..25 } + validates :address, presence: true, length: { in: 8..50 } + validates :iban, format: { with: /\A[A-Z]{2}[0-9]{2}[0-9A-Z]{,30}\z/, allow_blank: true } + validates :iban, uniqueness: { case_sensitive: false, allow_blank: true } + validates :order_howto, :note, length: { maximum: 250 } validate :valid_shared_sync_method validate :uniqueness_of_name scope :undeleted, -> { where(deleted_at: nil) } scope :having_articles, -> { where(id: Article.undeleted.select(:supplier_id).distinct) } - def self.ransackable_attributes(auth_object = nil) - %w(id name) + def self.ransackable_attributes(_auth_object = nil) + %w[id name] end - def self.ransackable_associations(auth_object = nil) - %w(articles stock_articles orders) + def self.ransackable_associations(_auth_object = nil) + %w[articles stock_articles orders] end # sync all articles with the external database @@ -35,7 +37,9 @@ class Supplier < ApplicationRecord # also returns an array with outlisted_articles, which should be deleted # also returns an array with new articles, which should be added (depending on shared_sync_method) def sync_all - updated_article_pairs, outlisted_articles, new_articles = [], [], [] + updated_article_pairs = [] + outlisted_articles = [] + new_articles = [] existing_articles = Set.new for article in articles.undeleted # try to find the associated shared_article @@ -44,30 +48,28 @@ class Supplier < ApplicationRecord if shared_article # article will be updated existing_articles.add(shared_article.id) unequal_attributes = article.shared_article_changed?(self) - unless unequal_attributes.blank? # skip if shared_article has not been changed + if unequal_attributes.present? # skip if shared_article has not been changed article.attributes = unequal_attributes updated_article_pairs << [article, unequal_attributes] end # Articles with no order number can be used to put non-shared articles # in a shared supplier, with sync keeping them. - elsif not article.order_number.blank? + elsif article.order_number.present? # article isn't in external database anymore outlisted_articles << article end end # Find any new articles, unless the import is manual - if ['all_available', 'all_unavailable'].include?(shared_sync_method) + if %w[all_available all_unavailable].include?(shared_sync_method) # build new articles shared_supplier .shared_articles .where.not(id: existing_articles.to_a) .find_each { |new_shared_article| new_articles << new_shared_article.build_new_article(self) } # make them unavailable when desired - if shared_sync_method == 'all_unavailable' - new_articles.each { |new_article| new_article.availability = false } - end + new_articles.each { |new_article| new_article.availability = false } if shared_sync_method == 'all_unavailable' end - return [updated_article_pairs, outlisted_articles, new_articles] + [updated_article_pairs, outlisted_articles, new_articles] end # Synchronise articles with spreadsheet. @@ -78,8 +80,10 @@ class Supplier < ApplicationRecord # @option options [Boolean] :convert_units Omit or set to +true+ to keep current units, recomputing unit quantity and price. def sync_from_file(file, options = {}) all_order_numbers = [] - updated_article_pairs, outlisted_articles, new_articles = [], [], [] - FoodsoftFile::parse file, options do |status, new_attrs, line| + updated_article_pairs = [] + outlisted_articles = [] + new_articles = [] + FoodsoftFile.parse file, options do |status, new_attrs, line| article = articles.undeleted.where(order_number: new_attrs[:order_number]).first new_attrs[:article_category] = ArticleCategory.find_match(new_attrs[:article_category]) new_attrs[:tax] ||= FoodsoftConfig[:tax_default] @@ -101,15 +105,13 @@ class Supplier < ApplicationRecord # stop when there is a parsing error elsif status.is_a? String # @todo move I18n key to model - raise I18n.t('articles.model.error_parse', :msg => status, :line => line.to_s) + raise I18n.t('articles.model.error_parse', msg: status, line: line.to_s) end all_order_numbers << article.order_number if article end - if options[:outlist_absent] - outlisted_articles += articles.undeleted.where.not(order_number: all_order_numbers + [nil]) - end - return [updated_article_pairs, outlisted_articles, new_articles] + outlisted_articles += articles.undeleted.where.not(order_number: all_order_numbers + [nil]) if options[:outlist_absent] + [updated_article_pairs, outlisted_articles, new_articles] end # default value @@ -140,18 +142,18 @@ class Supplier < ApplicationRecord # make sure the shared_sync_method is allowed for the shared supplier def valid_shared_sync_method - if shared_supplier && !shared_supplier.shared_sync_methods.include?(shared_sync_method) - errors.add :shared_sync_method, :included - end + return unless shared_supplier && !shared_supplier.shared_sync_methods.include?(shared_sync_method) + + errors.add :shared_sync_method, :included end # Make sure, the name is uniq, add usefull message if uniq group is already deleted def uniqueness_of_name supplier = Supplier.where(name: name) - supplier = supplier.where.not(id: self.id) unless new_record? - if supplier.exists? - message = supplier.first.deleted? ? :taken_with_deleted : :taken - errors.add :name, message - end + supplier = supplier.where.not(id: id) unless new_record? + return unless supplier.exists? + + message = supplier.first.deleted? ? :taken_with_deleted : :taken + errors.add :name, message end end diff --git a/app/models/task.rb b/app/models/task.rb index cd748eb3..1343b8f4 100644 --- a/app/models/task.rb +++ b/app/models/task.rb @@ -1,9 +1,9 @@ class Task < ApplicationRecord - has_many :assignments, :dependent => :destroy - has_many :users, :through => :assignments + has_many :assignments, dependent: :destroy + has_many :users, through: :assignments belongs_to :workgroup, optional: true belongs_to :periodic_task_group, optional: true - belongs_to :created_by, :class_name => 'User', :foreign_key => 'created_by_user_id', optional: true + belongs_to :created_by, class_name: 'User', foreign_key: 'created_by_user_id', optional: true scope :non_group, -> { where(workgroup_id: nil, done: false) } scope :done, -> { where(done: true) } @@ -11,12 +11,12 @@ class Task < ApplicationRecord attr_accessor :current_user_id - validates :name, :presence => true, :length => { :minimum => 3 } - validates :required_users, :presence => true - validates_numericality_of :duration, :required_users, :only_integer => true, :greater_than => 0 - validates_length_of :description, maximum: 250 + validates :name, presence: true, length: { minimum: 3 } + validates :required_users, presence: true + validates :duration, :required_users, numericality: { only_integer: true, greater_than: 0 } + validates :description, length: { maximum: 250 } validates :done, exclusion: { in: [true] }, if: :periodic?, on: :create - validates_presence_of :due_date, if: :periodic? + validates :due_date, presence: { if: :periodic? } before_save :exclude_from_periodic_task_group, if: :changed?, unless: :new_record? after_save :update_ordergroup_stats @@ -35,7 +35,7 @@ class Task < ApplicationRecord # find all tasks in the period (or another number of days) def self.next_assigned_tasks_for(user, number = FoodsoftConfig[:tasks_period_days].to_i) user.tasks.undone.where(assignments: { accepted: true }) - .where(["tasks.due_date >= ? AND tasks.due_date <= ?", Time.now, number.days.from_now]) + .where(['tasks.due_date >= ? AND tasks.due_date <= ?', Time.now, number.days.from_now]) end # count tasks with not enough responsible people @@ -49,7 +49,7 @@ class Task < ApplicationRecord def self.next_unassigned_tasks_for(user, max = 2) periodic_task_group_count = {} - self.unassigned_tasks_for(user).reject do |item| + unassigned_tasks_for(user).reject do |item| next false unless item.periodic_task_group count = periodic_task_group_count[item.periodic_task_group] || 0 @@ -59,19 +59,19 @@ class Task < ApplicationRecord end def periodic? - not periodic_task_group.nil? + !periodic_task_group.nil? end def is_assigned?(user) - self.assignments.detect { |ass| ass.user_id == user.id } + assignments.detect { |ass| ass.user_id == user.id } end def is_accepted?(user) - self.assignments.detect { |ass| ass.user_id == user.id && ass.accepted } + assignments.detect { |ass| ass.user_id == user.id && ass.accepted } end def enough_users_assigned? - assignments.to_a.count(&:accepted) >= required_users ? true : false + assignments.to_a.count(&:accepted) >= required_users end def still_required_users @@ -82,39 +82,35 @@ class Task < ApplicationRecord # and makes the users responsible for the task # TODO: check for maximal number of users def user_list=(ids) - list = ids.split(",").map(&:to_i) + list = ids.split(',').map(&:to_i) new_users = (list - users.collect(&:id)).uniq old_users = users.reject { |user| list.include?(user.id) } self.class.transaction do # delete old assignments - if old_users.any? - assignments.where(user_id: old_users.map(&:id)).each(&:destroy) - end + assignments.where(user_id: old_users.map(&:id)).find_each(&:destroy) if old_users.any? # create new assignments new_users.each do |id| user = User.find(id) if user.blank? errors.add(:user_list) + elsif id == current_user_id.to_i + assignments.build user: user, accepted: true + # current_user will accept, when he puts himself to the list of users else - if id == current_user_id.to_i - # current_user will accept, when he puts himself to the list of users - self.assignments.build :user => user, :accepted => true - else - # normal assignement - self.assignments.build :user => user - end + # normal assignement + assignments.build user: user end end end end def user_list - @user_list ||= users.collect(&:id).join(", ") + @user_list ||= users.collect(&:id).join(', ') end def update_ordergroup_stats(user_ids = self.user_ids) - Ordergroup.joins(:users).where(users: { id: user_ids }).each(&:update_stats!) + Ordergroup.joins(:users).where(users: { id: user_ids }).find_each(&:update_stats!) end def exclude_from_periodic_task_group diff --git a/app/models/user.rb b/app/models/user.rb index 05a67547..12d457b0 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -4,19 +4,19 @@ class User < ApplicationRecord include CustomFields # TODO: acts_as_paraniod ?? - has_many :memberships, :dependent => :destroy - has_many :groups, :through => :memberships + has_many :memberships, dependent: :destroy + has_many :groups, through: :memberships # has_one :ordergroup, :through => :memberships, :source => :group, :class_name => "Ordergroup" def ordergroup - Ordergroup.joins(:memberships).where(memberships: { user_id: self.id }).first + Ordergroup.joins(:memberships).where(memberships: { user_id: id }).first end - has_many :workgroups, :through => :memberships, :source => :group, :class_name => "Workgroup" - has_many :assignments, :dependent => :destroy - has_many :tasks, :through => :assignments - has_many :send_messages, :class_name => "Message", :foreign_key => "sender_id" - has_many :created_orders, :class_name => 'Order', :foreign_key => 'created_by_user_id', :dependent => :nullify - has_many :mail_delivery_status, :class_name => 'MailDeliveryStatus', :foreign_key => 'email', :primary_key => 'email' + has_many :workgroups, through: :memberships, source: :group, class_name: 'Workgroup' + has_many :assignments, dependent: :destroy + has_many :tasks, through: :assignments + has_many :send_messages, class_name: 'Message', foreign_key: 'sender_id' + has_many :created_orders, class_name: 'Order', foreign_key: 'created_by_user_id', dependent: :nullify + has_many :mail_delivery_status, class_name: 'MailDeliveryStatus', foreign_key: 'email', primary_key: 'email' attr_accessor :create_ordergroup, :password, :send_welcome_mail, :settings_attributes @@ -26,22 +26,22 @@ class User < ApplicationRecord # makes the current_user (logged-in-user) available in models cattr_accessor :current_user - validates_presence_of :email - validates_presence_of :password, :on => :create - validates_format_of :email, :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i - validates_uniqueness_of :email, :case_sensitive => false - validates_presence_of :first_name # for simple_form validations - validates_length_of :first_name, :in => 2..50 - validates_confirmation_of :password - validates_length_of :password, :in => 5..50, :allow_blank => true + validates :email, presence: true + validates :password, presence: { on: :create } + validates :email, format: { with: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i } + validates :email, uniqueness: { case_sensitive: false } + validates :first_name, presence: true # for simple_form validations + validates :first_name, length: { in: 2..50 } + validates :password, confirmation: true + validates :password, length: { in: 5..50, allow_blank: true } # allow nick to be nil depending on foodcoop config # TODO Rails 4 may have a more beautiful way # http://stackoverflow.com/questions/19845910/conditional-allow-nil-part-of-validation - validates_length_of :nick, :in => 2..25, :allow_nil => true, :unless => Proc.new { FoodsoftConfig[:use_nick] } - validates_length_of :nick, :in => 2..25, :allow_nil => false, :if => Proc.new { FoodsoftConfig[:use_nick] } - validates_uniqueness_of :nick, :case_sensitive => false, :allow_nil => true # allow_nil in length validation - validates_format_of :iban, :with => /\A[A-Z]{2}[0-9]{2}[0-9A-Z]{,30}\z/, :allow_blank => true - validates_uniqueness_of :iban, :case_sensitive => false, :allow_blank => true + validates :nick, length: { in: 2..25, allow_nil: true, unless: proc { FoodsoftConfig[:use_nick] } } + validates :nick, length: { in: 2..25, allow_nil: false, if: proc { FoodsoftConfig[:use_nick] } } + validates :nick, uniqueness: { case_sensitive: false, allow_nil: true } # allow_nil in length validation + validates :iban, format: { with: /\A[A-Z]{2}[0-9]{2}[0-9A-Z]{,30}\z/, allow_blank: true } + validates :iban, uniqueness: { case_sensitive: false, allow_blank: true } before_validation :set_password after_initialize do @@ -58,17 +58,19 @@ class User < ApplicationRecord end after_save do - settings_attributes.each do |key, value| - value.each do |k, v| - case v - when '1' - value[k] = true - when '0' - value[k] = false + if settings_attributes + settings_attributes.each do |key, value| + value.each do |k, v| + case v + when '1' + value[k] = true + when '0' + value[k] = false + end end + settings.merge!(key, value) end - self.settings.merge!(key, value) - end if settings_attributes + end if ActiveModel::Type::Boolean.new.cast(create_ordergroup) og = Ordergroup.new({ name: name }) @@ -103,7 +105,7 @@ class User < ApplicationRecord match_name = q.split.map do |a| users[:first_name].matches("%#{a}%").or users[:last_name].matches("%#{a}%") end.reduce(:and) - User.where(match_nick.or match_name) + User.where(match_nick.or(match_name)) end def locale @@ -111,7 +113,7 @@ class User < ApplicationRecord end def name - [first_name, last_name].join(" ") + [first_name, last_name].join(' ') end def receive_email? @@ -120,22 +122,24 @@ class User < ApplicationRecord # Sets the user's password. It will be stored encrypted along with a random salt. def set_password - unless password.blank? - salt = [Array.new(6) { rand(256).chr }.join].pack("m").chomp - self.password_hash, self.password_salt = Digest::SHA1.hexdigest(password + salt), salt - end + return if password.blank? + + salt = [Array.new(6) { rand(256).chr }.join].pack('m').chomp + self.password_hash = Digest::SHA1.hexdigest(password + salt) + self.password_salt = salt end # Returns true if the password argument matches the user's password. def has_password(password) - Digest::SHA1.hexdigest(password + self.password_salt) == self.password_hash + Digest::SHA1.hexdigest(password + password_salt) == password_hash end # Returns a random password. def new_random_password(size = 6) - c = %w(b c d f g h j k l m n p qu r s t v w x z ch cr fr nd ng nk nt ph pr rd sh sl sp st th tr) - v = %w(a e i o u y) - f, r = true, '' + c = %w[b c d f g h j k l m n p qu r s t v w x z ch cr fr nd ng nk nt ph pr rd sh sl sp st th tr] + v = %w[a e i o u y] + f = true + r = '' (size * 2).times do r << (f ? c[rand * c.size] : v[rand * v.size]) f = !f @@ -198,12 +202,12 @@ class User < ApplicationRecord # returns true if user is a member of a given group def member_of?(group) - group.users.exists?(self.id) + group.users.exists?(id) end # Returns an array with the users groups (but without the Ordergroups -> because tpye=>"") - def member_of_groups() - self.groups.where(type: '') + def member_of_groups + groups.where(type: '') end def deleted? @@ -220,11 +224,9 @@ class User < ApplicationRecord def self.authenticate(login, password) user = find_by_nick(login) || find_by_email(login) - if user && password && user.has_password(password) - user - else - nil - end + return unless user && password && user.has_password(password) + + user end def self.custom_fields @@ -248,29 +250,29 @@ class User < ApplicationRecord def token_attributes # would be sensible to match ApplicationController#show_user # this should not be part of the model anyway - { :id => id, :name => "#{display} (#{ordergroup.try(:name)})" } + { id: id, name: "#{display} (#{ordergroup.try(:name)})" } end def self.sort_by_param(param) - param ||= "name" + param ||= 'name' sort_param_map = { - "nick" => "nick", - "nick_reverse" => "nick DESC", - "name" => "first_name, last_name", - "name_reverse" => "first_name DESC, last_name DESC", - "email" => "users.email", - "email_reverse" => "users.email DESC", - "phone" => "phone", - "phone_reverse" => "phone DESC", - "last_activity" => "last_activity", - "last_activity_reverse" => "last_activity DESC", - "ordergroup" => "IFNULL(groups.type, '') <> 'Ordergroup', groups.name", - "ordergroup_reverse" => "IFNULL(groups.type, '') <> 'Ordergroup', groups.name DESC" + 'nick' => 'nick', + 'nick_reverse' => 'nick DESC', + 'name' => 'first_name, last_name', + 'name_reverse' => 'first_name DESC, last_name DESC', + 'email' => 'users.email', + 'email_reverse' => 'users.email DESC', + 'phone' => 'phone', + 'phone_reverse' => 'phone DESC', + 'last_activity' => 'last_activity', + 'last_activity_reverse' => 'last_activity DESC', + 'ordergroup' => "IFNULL(groups.type, '') <> 'Ordergroup', groups.name", + 'ordergroup_reverse' => "IFNULL(groups.type, '') <> 'Ordergroup', groups.name DESC" } # Never pass user input data to Arel.sql() because of SQL Injection vulnerabilities. # This case here is okay, as param is mapped to the actual order string. - self.eager_load(:groups).order(Arel.sql(sort_param_map[param])) # eager_load is like left_join but without duplicates + eager_load(:groups).order(Arel.sql(sort_param_map[param])) # eager_load is like left_join but without duplicates end end diff --git a/app/models/workgroup.rb b/app/models/workgroup.rb index bf50c27b..271dec8d 100644 --- a/app/models/workgroup.rb +++ b/app/models/workgroup.rb @@ -3,26 +3,26 @@ class Workgroup < Group has_many :tasks # returns all non-finished tasks - has_many :open_tasks, -> { where(:done => false).order('due_date', 'name') }, :class_name => 'Task' + has_many :open_tasks, -> { where(done: false).order('due_date', 'name') }, class_name: 'Task' - validates_uniqueness_of :name - validate :last_admin_on_earth, :on => :update + validates :name, uniqueness: true + validate :last_admin_on_earth, on: :update before_destroy :check_last_admin_group protected # Check before destroy a group, if this is the last group with admin role def check_last_admin_group - if role_admin && Workgroup.where(role_admin: true).size == 1 - raise I18n.t('workgroups.error_last_admin_group') - end + return unless role_admin && Workgroup.where(role_admin: true).size == 1 + + raise I18n.t('workgroups.error_last_admin_group') end # add validation check on update # Return an error if this is the last group with admin role and role_admin should set to false def last_admin_on_earth - if !role_admin && !Workgroup.where(role_admin: true).where.not(id: id).exists? - errors.add(:role_admin, I18n.t('workgroups.error_last_admin_role')) - end + return unless !role_admin && !Workgroup.where(role_admin: true).where.not(id: id).exists? + + errors.add(:role_admin, I18n.t('workgroups.error_last_admin_role')) end end diff --git a/config.ru b/config.ru index f986eadb..eeab736b 100644 --- a/config.ru +++ b/config.ru @@ -1,6 +1,6 @@ # This file is used by Rack-based servers to start the application. -require ::File.expand_path('../config/environment', __FILE__) +require File.expand_path('config/environment', __dir__) # https://gist.github.com/ebeigarts/5450422 map ENV.fetch('RAILS_RELATIVE_URL_ROOT', '/') do diff --git a/config/application.rb b/config/application.rb index 9c0ade99..71883b57 100644 --- a/config/application.rb +++ b/config/application.rb @@ -29,12 +29,14 @@ module Foodsoft # Internationalization. config.i18n.load_path += Dir[Rails.root.join('config', 'locales', '*.yml')] - config.i18n.available_locales = Pathname.glob(Rails.root.join('config', 'locales', '{??,???}{-*,}.yml')).map { |p| p.basename('.yml').to_s } + config.i18n.available_locales = Pathname.glob(Rails.root.join('config', 'locales', '{??,???}{-*,}.yml')).map do |p| + p.basename('.yml').to_s + end config.i18n.default_locale = :en config.i18n.fallbacks = [:en] # Configure the default encoding used in templates for Ruby 1.9. - config.encoding = "utf-8" + config.encoding = 'utf-8' # Enable escaping HTML in JSON. config.active_support.escape_html_entities_in_json = true @@ -44,7 +46,7 @@ module Foodsoft # like if you have constraints or database-specific column types # config.active_record.schema_format = :sql - # TODO Disable this. Uncommenting this line will currently cause rspec to fail. + # TODO: Disable this. Uncommenting this line will currently cause rspec to fail. config.action_controller.permit_all_parameters = true config.active_job.queue_adapter = :resque @@ -82,5 +84,9 @@ module Foodsoft # Foodsoft version VERSION = Rails.root.join('VERSION').read.strip # Current revision, or +nil+ - REVISION = (Rails.root.join('REVISION').read.strip rescue nil) + REVISION = begin + Rails.root.join('REVISION').read.strip + rescue StandardError + nil + end end diff --git a/config/environments/production.rb b/config/environments/production.rb index d0f06b95..e1a4e97f 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -1,4 +1,4 @@ -require "active_support/core_ext/integer/time" +require 'active_support/core_ext/integer/time' # Foodsoft production configuration. # @@ -51,7 +51,7 @@ Rails.application.configure do # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ] # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. - config.force_ssl = ENV["RAILS_FORCE_SSL"] != "false" + config.force_ssl = ENV['RAILS_FORCE_SSL'] != 'false' # Include generic and useful information about system operation, but avoid logging too much # information to avoid inadvertent exposure of personally identifiable information (PII). @@ -97,9 +97,18 @@ Rails.application.configure do config.action_mailer.smtp_settings[:domain] = ENV['SMTP_DOMAIN'] if ENV['SMTP_DOMAIN'].present? config.action_mailer.smtp_settings[:user_name] = ENV['SMTP_USER_NAME'] if ENV['SMTP_USER_NAME'].present? config.action_mailer.smtp_settings[:password] = ENV['SMTP_PASSWORD'] if ENV['SMTP_PASSWORD'].present? - config.action_mailer.smtp_settings[:authentication] = ENV['SMTP_AUTHENTICATION'] if ENV['SMTP_AUTHENTICATION'].present? - config.action_mailer.smtp_settings[:enable_starttls_auto] = ENV['SMTP_ENABLE_STARTTLS_AUTO'] == 'true' if ENV['SMTP_ENABLE_STARTTLS_AUTO'].present? - config.action_mailer.smtp_settings[:openssl_verify_mode] = ENV['SMTP_OPENSSL_VERIFY_MODE'] if ENV['SMTP_OPENSSL_VERIFY_MODE'].present? + if ENV['SMTP_AUTHENTICATION'].present? + config.action_mailer.smtp_settings[:authentication] = + ENV['SMTP_AUTHENTICATION'] + end + if ENV['SMTP_ENABLE_STARTTLS_AUTO'].present? + config.action_mailer.smtp_settings[:enable_starttls_auto] = + ENV['SMTP_ENABLE_STARTTLS_AUTO'] == 'true' + end + if ENV['SMTP_OPENSSL_VERIFY_MODE'].present? + config.action_mailer.smtp_settings[:openssl_verify_mode] = + ENV['SMTP_OPENSSL_VERIFY_MODE'] + end else # Use sendmail as default to avoid ssl cert problems config.action_mailer.delivery_method = :sendmail @@ -112,7 +121,7 @@ Rails.application.configure do # require 'syslog/logger' # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name') - if ENV["RAILS_LOG_TO_STDOUT"].present? + if ENV['RAILS_LOG_TO_STDOUT'].present? logger = ActiveSupport::Logger.new(STDOUT) logger.formatter = config.log_formatter config.logger = ActiveSupport::TaggedLogging.new(logger) diff --git a/config/environments/test.rb b/config/environments/test.rb index 6ea4d1e7..5f6cef4d 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -1,4 +1,4 @@ -require "active_support/core_ext/integer/time" +require 'active_support/core_ext/integer/time' # The test environment is used exclusively to run your application's # test suite. You never need to work with it otherwise. Remember that @@ -14,12 +14,12 @@ Rails.application.configure do # Eager loading loads your whole application. When running a single test locally, # this probably isn't necessary. It's a good idea to do in a continuous integration # system, or in some way before deploying your code. - config.eager_load = ENV["CI"].present? + config.eager_load = ENV['CI'].present? # Configure public file server for tests with Cache-Control for performance. config.public_file_server.enabled = true config.public_file_server.headers = { - "Cache-Control" => "public, max-age=#{1.hour.to_i}" + 'Cache-Control' => "public, max-age=#{1.hour.to_i}" } # Show full error reports and disable caching. diff --git a/config/initializers/currency_display.rb b/config/initializers/currency_display.rb index 71d108d2..24ceeb8b 100644 --- a/config/initializers/currency_display.rb +++ b/config/initializers/currency_display.rb @@ -2,6 +2,7 @@ # have it shown in all other languages too I18n.available_locales.each do |locale| unless locale == I18n.default_locale - I18n.backend.store_translations(locale, number: { currency: { format: { unit: nil } } }) + I18n.backend.store_translations(locale, + number: { currency: { format: { unit: nil } } }) end end diff --git a/config/initializers/doorkeeper.rb b/config/initializers/doorkeeper.rb index 83293820..d01c9a5f 100644 --- a/config/initializers/doorkeeper.rb +++ b/config/initializers/doorkeeper.rb @@ -100,7 +100,7 @@ Doorkeeper.configure do # http://tools.ietf.org/html/rfc6819#section-4.4.2 # http://tools.ietf.org/html/rfc6819#section-4.4.3 # - grant_flows %w(authorization_code implicit password) + grant_flows %w[authorization_code implicit password] # Under some circumstances you might want to have applications auto-approved, # so that the user skips the authorization step. diff --git a/config/initializers/exception_notification.rb b/config/initializers/exception_notification.rb index 10107865..3d342465 100644 --- a/config/initializers/exception_notification.rb +++ b/config/initializers/exception_notification.rb @@ -14,7 +14,7 @@ ExceptionNotification.configure do |config| # Adds a condition to decide when an exception must be ignored or not. # The ignore_if method can be invoked multiple times to add extra conditions. - config.ignore_if do |exception, options| + config.ignore_if do |_exception, _options| Rails.env.development? || Rails.env.test? end @@ -23,9 +23,9 @@ ExceptionNotification.configure do |config| # Email notifier sends notifications by email. if notification = FoodsoftConfig[:notification] config.add_notifier :email, { - :email_prefix => notification[:email_prefix], - :sender_address => notification[:sender_address], - :exception_recipients => notification[:error_recipients], + email_prefix: notification[:email_prefix], + sender_address: notification[:sender_address], + exception_recipients: notification[:error_recipients] } end diff --git a/config/initializers/extensions.rb b/config/initializers/extensions.rb index 68c7c8f4..d276aecb 100644 --- a/config/initializers/extensions.rb +++ b/config/initializers/extensions.rb @@ -2,7 +2,7 @@ class String # remove comma from decimal inputs def self.delocalized_decimal(string) - if !string.blank? and string.is_a?(String) + if string.present? and string.is_a?(String) BigDecimal(string.sub(',', '.')) else string @@ -13,6 +13,6 @@ end class Array def cumulative_sum csum = 0 - self.map { |val| csum += val } + map { |val| csum += val } end end diff --git a/config/initializers/filter_parameter_logging.rb b/config/initializers/filter_parameter_logging.rb index adc6568c..166997c5 100644 --- a/config/initializers/filter_parameter_logging.rb +++ b/config/initializers/filter_parameter_logging.rb @@ -3,6 +3,6 @@ # Configure parameters to be filtered from the log file. Use this to limit dissemination of # sensitive information. See the ActiveSupport::ParameterFilter documentation for supported # notations and behaviors. -Rails.application.config.filter_parameters += [ - :passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn +Rails.application.config.filter_parameters += %i[ + passw secret token _key crypt salt certificate otp ssn ] diff --git a/config/initializers/rack.rb b/config/initializers/rack.rb index 30970ec9..aa462561 100644 --- a/config/initializers/rack.rb +++ b/config/initializers/rack.rb @@ -1,3 +1,3 @@ # Increase key space for post request. # Warning, this is dangerous. See http://stackoverflow.com/questions/12243694/getting-error-exceeded-available-parameter-key-space -Rack::Utils.key_space_limit = 262144 +Rack::Utils.key_space_limit = 262_144 diff --git a/config/initializers/ruby_units.rb b/config/initializers/ruby_units.rb index b8b56cca..af422fcb 100644 --- a/config/initializers/ruby_units.rb +++ b/config/initializers/ruby_units.rb @@ -2,28 +2,28 @@ if defined? RubyUnits RubyUnits::Unit.redefine!('liter') do |unit| - unit.aliases += %w{ltr} + unit.aliases += %w[ltr] end RubyUnits::Unit.redefine!('kilogram') do |unit| - unit.aliases += %w{KG} + unit.aliases += %w[KG] end RubyUnits::Unit.redefine!('gram') do |unit| - unit.aliases += %w{gr} + unit.aliases += %w[gr] end RubyUnits::Unit.define('piece') do |unit| unit.definition = RubyUnits::Unit.new('1 each') - unit.aliases = %w{pc pcs piece pieces} # locale: en - unit.aliases += %w{st stuk stuks} # locale: nl + unit.aliases = %w[pc pcs piece pieces] # locale: en + unit.aliases += %w[st stuk stuks] # locale: nl unit.kind = :counting end RubyUnits::Unit.define('bag') do |unit| unit.definition = RubyUnits::Unit.new('1 each') - unit.aliases = %w{bag bags blt sachet sachets} # locale: en - unit.aliases += %w{zak zakken zakje zakjes} # locale: nl + unit.aliases = %w[bag bags blt sachet sachets] # locale: en + unit.aliases += %w[zak zakken zakje zakjes] # locale: nl unit.kind = :counting end diff --git a/config/initializers/secret_token.rb b/config/initializers/secret_token.rb index 4f01f173..fe62df4a 100644 --- a/config/initializers/secret_token.rb +++ b/config/initializers/secret_token.rb @@ -8,12 +8,12 @@ Foodsoft::Application.config.secret_key_base = begin if (token = ENV.fetch('SECRET_KEY_BASE', nil)).present? token elsif Rails.env.production? || Rails.env.staging? - raise "You must set SECRET_KEY_BASE" + raise 'You must set SECRET_KEY_BASE' elsif Rails.env.test? SecureRandom.hex(30) # doesn't really matter else sf = Rails.root.join('tmp', 'secret_key_base') - if File.exists?(sf) + if File.exist?(sf) File.read(sf) else puts "=> Generating initial SECRET_KEY_BASE in #{sf}" diff --git a/config/initializers/session_store.rb b/config/initializers/session_store.rb index d7841180..370a202e 100644 --- a/config/initializers/session_store.rb +++ b/config/initializers/session_store.rb @@ -3,7 +3,7 @@ module ActionDispatch module Session class SlugCookieStore < CookieStore - alias_method :orig_set_cookie, :set_cookie + alias orig_set_cookie set_cookie def set_cookie(request, session_id, cookie) if script_name = FoodsoftConfig[:script_name] diff --git a/config/initializers/simple_form_bootstrap.rb b/config/initializers/simple_form_bootstrap.rb index a95278d0..1292ac83 100644 --- a/config/initializers/simple_form_bootstrap.rb +++ b/config/initializers/simple_form_bootstrap.rb @@ -11,7 +11,7 @@ SimpleForm.setup do |config| end end - config.wrappers :prepend, tag: 'div', class: "control-group", error_class: 'error' do |b| + config.wrappers :prepend, tag: 'div', class: 'control-group', error_class: 'error' do |b| b.use :html5 b.use :placeholder b.use :label @@ -24,7 +24,7 @@ SimpleForm.setup do |config| end end - config.wrappers :append, tag: 'div', class: "control-group", error_class: 'error' do |b| + config.wrappers :append, tag: 'div', class: 'control-group', error_class: 'error' do |b| b.use :html5 b.use :placeholder b.use :label diff --git a/config/initializers/zeitwerk.rb b/config/initializers/zeitwerk.rb index 9c505a26..ede05b16 100644 --- a/config/initializers/zeitwerk.rb +++ b/config/initializers/zeitwerk.rb @@ -1,5 +1,4 @@ # config/initializers/zeitwerk.rb ActiveSupport::Dependencies .autoload_paths - .delete("#{Rails.root}/app/controllers/concerns") - \ No newline at end of file + .delete(Rails.root.join('app/controllers/concerns').to_s) diff --git a/config/navigation.rb b/config/navigation.rb index c8b7d088..857b931f 100644 --- a/config/navigation.rb +++ b/config/navigation.rb @@ -21,31 +21,44 @@ SimpleNavigation::Configuration.run do |navigation| primary.item :orders, I18n.t('navigation.orders.title'), '#' do |subnav| subnav.item :ordering, I18n.t('navigation.orders.ordering'), group_orders_path subnav.item :ordering_archive, I18n.t('navigation.orders.archive'), archive_group_orders_path - subnav.item :orders, I18n.t('navigation.orders.manage'), orders_path, if: Proc.new { current_user.role_orders? } - subnav.item :pickups, I18n.t('navigation.orders.pickups'), pickups_path, if: Proc.new { current_user.role_pickups? } + subnav.item :orders, I18n.t('navigation.orders.manage'), orders_path, if: proc { current_user.role_orders? } + subnav.item :pickups, I18n.t('navigation.orders.pickups'), pickups_path, if: proc { + current_user.role_pickups? + } end primary.item :articles, I18n.t('navigation.articles.title'), '#', - if: Proc.new { current_user.role_article_meta? or current_user.role_suppliers? } do |subnav| + if: proc { current_user.role_article_meta? or current_user.role_suppliers? } do |subnav| subnav.item :suppliers, I18n.t('navigation.articles.suppliers'), suppliers_path subnav.item :stockit, I18n.t('navigation.articles.stock'), stock_articles_path subnav.item :categories, I18n.t('navigation.articles.categories'), article_categories_path end - primary.item :finance, I18n.t('navigation.finances.title'), '#', if: Proc.new { current_user.role_finance? || current_user.role_invoices? } do |subnav| - subnav.item :finance_home, I18n.t('navigation.finances.home'), finance_root_path, if: Proc.new { current_user.role_finance? } - subnav.item :finance_home, I18n.t('navigation.finances.bank_accounts'), finance_bank_accounts_path, if: Proc.new { current_user.role_finance? } - subnav.item :accounts, I18n.t('navigation.finances.accounts'), finance_ordergroups_path, if: Proc.new { current_user.role_finance? } - subnav.item :balancing, I18n.t('navigation.finances.balancing'), finance_order_index_path, if: Proc.new { current_user.role_finance? } + primary.item :finance, I18n.t('navigation.finances.title'), '#', if: proc { + current_user.role_finance? || current_user.role_invoices? + } do |subnav| + subnav.item :finance_home, I18n.t('navigation.finances.home'), finance_root_path, if: proc { + current_user.role_finance? + } + subnav.item :finance_home, I18n.t('navigation.finances.bank_accounts'), finance_bank_accounts_path, if: proc { + current_user.role_finance? + } + subnav.item :accounts, I18n.t('navigation.finances.accounts'), finance_ordergroups_path, if: proc { + current_user.role_finance? + } + subnav.item :balancing, I18n.t('navigation.finances.balancing'), finance_order_index_path, if: proc { + current_user.role_finance? + } subnav.item :invoices, I18n.t('navigation.finances.invoices'), finance_invoices_path end - primary.item :admin, I18n.t('navigation.admin.title'), '#', if: Proc.new { current_user.role_admin? } do |subnav| + primary.item :admin, I18n.t('navigation.admin.title'), '#', if: proc { current_user.role_admin? } do |subnav| subnav.item :admin_home, I18n.t('navigation.admin.home'), admin_root_path subnav.item :users, I18n.t('navigation.admin.users'), admin_users_path subnav.item :ordergroups, I18n.t('navigation.admin.ordergroups'), admin_ordergroups_path subnav.item :workgroups, I18n.t('navigation.admin.workgroups'), admin_workgroups_path - subnav.item :mail_delivery_status, I18n.t('navigation.admin.mail_delivery_status'), admin_mail_delivery_status_index_path + subnav.item :mail_delivery_status, I18n.t('navigation.admin.mail_delivery_status'), + admin_mail_delivery_status_index_path subnav.item :finances, I18n.t('navigation.admin.finance'), admin_finances_path subnav.item :config, I18n.t('navigation.admin.config'), admin_config_path end diff --git a/config/puma.rb b/config/puma.rb index 4f4fd1cf..609df0ad 100644 --- a/config/puma.rb +++ b/config/puma.rb @@ -4,19 +4,19 @@ # the maximum value specified for Puma. Default is set to 5 threads for minimum # and maximum; this matches the default thread size of Active Record. # -threads_count = ENV.fetch("RAILS_MAX_THREADS") { 1 } +threads_count = ENV.fetch('RAILS_MAX_THREADS') { 1 } threads threads_count, threads_count # Specifies the `port` that Puma will listen on to receive requests; default is 3000. # -port ENV.fetch("PORT") { 3000 } +port ENV.fetch('PORT') { 3000 } # Bind automatically to all systemd activated sockets bind_to_activated_sockets # Specifies the `environment` that Puma will run in. # -environment ENV.fetch("RAILS_ENV") { "development" } +environment ENV.fetch('RAILS_ENV') { 'development' } # Specifies the number of `workers` to boot in clustered mode. # Workers are forked webserver processes. If using threads and workers together @@ -24,7 +24,7 @@ environment ENV.fetch("RAILS_ENV") { "development" } # Workers do not work on JRuby or Windows (both of which do not support # processes). # -workers ENV.fetch("WEB_CONCURRENCY") { 8 } +workers ENV.fetch('WEB_CONCURRENCY') { 8 } # Use the `preload_app!` method when specifying a `workers` number. # This directive tells Puma to first boot the application and load code diff --git a/config/routes.rb b/config/routes.rb index 764dcab0..8fea34b0 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -2,11 +2,11 @@ Rails.application.routes.draw do mount Rswag::Ui::Engine => '/api-docs' mount Rswag::Api::Engine => '/api-docs' - get "order_comments/new" + get 'order_comments/new' - get "comments/new" + get 'comments/new' - get "sessions/new" + get 'sessions/new' root to: 'sessions#redirect_to_foodcoop', as: nil @@ -24,8 +24,8 @@ Rails.application.routes.draw do post '/login/reset_password' => 'login#reset_password', as: :reset_password get '/login/new_password' => 'login#new_password', as: :new_password patch '/login/update_password' => 'login#update_password', as: :update_password - match '/login/accept_invitation/:token' => 'login#accept_invitation', as: :accept_invitation, via: [:get, :post] - resources :sessions, only: [:new, :create, :destroy] + match '/login/accept_invitation/:token' => 'login#accept_invitation', as: :accept_invitation, via: %i[get post] + resources :sessions, only: %i[new create destroy] get '/foodcoop.css' => 'styles#foodcoop', as: 'foodcoop_css' @@ -65,11 +65,11 @@ Rails.application.routes.draw do resources :group_order_articles - resources :order_comments, only: [:new, :create] + resources :order_comments, only: %i[new create] ############ Foodcoop orga - resources :invites, only: [:new, :create] + resources :invites, only: %i[new create] resources :tasks do collection do @@ -91,7 +91,7 @@ Rails.application.routes.draw do resources :ordergroups, only: [:index] - resources :workgroups, only: [:index, :edit, :update] + resources :workgroups, only: %i[index edit update] end ########### Article management @@ -175,7 +175,7 @@ Rails.application.routes.draw do get :unpaid, on: :collection end - resources :links, controller: 'financial_links', only: [:create, :show] do + resources :links, controller: 'financial_links', only: %i[create show] do collection do get :incomplete end @@ -185,8 +185,10 @@ Rails.application.routes.draw do delete 'bank_transactions/:bank_transaction', action: 'remove_bank_transaction', as: 'remove_bank_transaction' get :index_financial_transaction - put 'financial_transactions/:financial_transaction', action: 'add_financial_transaction', as: 'add_financial_transaction' - delete 'financial_transactions/:financial_transaction', action: 'remove_financial_transaction', as: 'remove_financial_transaction' + put 'financial_transactions/:financial_transaction', action: 'add_financial_transaction', + as: 'add_financial_transaction' + delete 'financial_transactions/:financial_transaction', action: 'remove_financial_transaction', + as: 'remove_financial_transaction' get :index_invoice put 'invoices/:invoice', action: 'add_invoice', as: 'add_invoice' @@ -200,12 +202,14 @@ Rails.application.routes.draw do resources :ordergroups, only: [:index] do resources :financial_transactions, as: :transactions end - resources :financial_transactions, as: :foodcoop_financial_transactions, path: 'foodcoop/financial_transactions', only: [:index, :new, :create] + resources :financial_transactions, as: :foodcoop_financial_transactions, path: 'foodcoop/financial_transactions', + only: %i[index new create] get :transactions, controller: :financial_transactions, action: :index_collection delete 'transactions/:id', controller: :financial_transactions, action: :destroy, as: :transaction get 'transactions/new_collection' => 'financial_transactions#new_collection', as: 'new_transaction_collection' - post 'transactions/create_collection' => 'financial_transactions#create_collection', as: 'create_transaction_collection' + post 'transactions/create_collection' => 'financial_transactions#create_collection', + as: 'create_transaction_collection' resources :bank_accounts, only: [:index] do member do @@ -217,7 +221,7 @@ Rails.application.routes.draw do resources :bank_transactions, as: :transactions end - resources :bank_transactions, only: [:index, :show] + resources :bank_transactions, only: %i[index show] end ########### Administration @@ -251,11 +255,11 @@ Rails.application.routes.draw do get :memberships, on: :member end - resources :mail_delivery_status, only: [:index, :show, :destroy] do + resources :mail_delivery_status, only: %i[index show destroy] do delete :index, on: :collection, action: :destroy_all end - resource :config, only: [:show, :update] do + resource :config, only: %i[show update] do get :list end end @@ -270,23 +274,23 @@ Rails.application.routes.draw do namespace :user do root to: 'users#show' get :financial_overview, to: 'ordergroup#financial_overview' - resources :financial_transactions, only: [:index, :show, :create] + resources :financial_transactions, only: %i[index show create] resources :group_order_articles end - resources :financial_transaction_classes, only: [:index, :show] - resources :financial_transaction_types, only: [:index, :show] - resources :financial_transactions, only: [:index, :show] - resources :orders, only: [:index, :show] - resources :order_articles, only: [:index, :show] + resources :financial_transaction_classes, only: %i[index show] + resources :financial_transaction_types, only: %i[index show] + resources :financial_transactions, only: %i[index show] + resources :orders, only: %i[index show] + resources :order_articles, only: %i[index show] resources :group_order_articles - resources :article_categories, only: [:index, :show] + resources :article_categories, only: %i[index show] end end ############## Feedback - resource :feedback, only: [:new, :create], controller: 'feedback' + resource :feedback, only: %i[new create], controller: 'feedback' ############## The rest diff --git a/config/schedule.rb b/config/schedule.rb index f22c1348..72e3cbcc 100644 --- a/config/schedule.rb +++ b/config/schedule.rb @@ -2,22 +2,22 @@ # Learn more: http://github.com/javan/whenever # Upcoming tasks notifier -every :day, :at => '7:20 am' do - rake "multicoops:run TASK=foodsoft:notify_upcoming_tasks" - rake "multicoops:run TASK=foodsoft:notify_users_of_weekly_task" +every :day, at: '7:20 am' do + rake 'multicoops:run TASK=foodsoft:notify_upcoming_tasks' + rake 'multicoops:run TASK=foodsoft:notify_users_of_weekly_task' end # Import and assign bank transactions -every :weekday, :at => %w(5:56am 6:04pm) do - rake "multicoops:run TASK=foodsoft:import_and_assign_bank_transactions" +every :weekday, at: %w[5:56am 6:04pm] do + rake 'multicoops:run TASK=foodsoft:import_and_assign_bank_transactions' end # Weekly taks -every :sunday, :at => '7:14 am' do - rake "multicoops:run TASK=foodsoft:create_upcoming_periodic_tasks" +every :sunday, at: '7:14 am' do + rake 'multicoops:run TASK=foodsoft:create_upcoming_periodic_tasks' end # Finish ended orders every 1.minute do - rake "multicoops:run TASK=foodsoft:finish_ended_orders" + rake 'multicoops:run TASK=foodsoft:finish_ended_orders' end diff --git a/config/spring.rb b/config/spring.rb index c9119b40..9fa7863f 100644 --- a/config/spring.rb +++ b/config/spring.rb @@ -1,6 +1,6 @@ -%w( +%w[ .ruby-version .rbenv-vars tmp/restart.txt tmp/caching-dev.txt -).each { |path| Spring.watch(path) } +].each { |path| Spring.watch(path) } diff --git a/db/migrate/001_create_users.rb b/db/migrate/001_create_users.rb index ab4a560a..e530bdbf 100644 --- a/db/migrate/001_create_users.rb +++ b/db/migrate/001_create_users.rb @@ -4,30 +4,31 @@ class CreateUsers < ActiveRecord::Migration[4.2] def self.up create_table :users do |t| - t.column :nick, :string, :null => false - t.column :password_hash, :string, :null => false - t.column :password_salt, :string, :null => false - t.column :first_name, :string, :null => false - t.column :last_name, :string, :null => false - t.column :email, :string, :null => false + t.column :nick, :string, null: false + t.column :password_hash, :string, null: false + t.column :password_salt, :string, null: false + t.column :first_name, :string, null: false + t.column :last_name, :string, null: false + t.column :email, :string, null: false t.column :phone, :string t.column :address, :string - t.column :created_on, :timestamp, :null => false + t.column :created_on, :timestamp, null: false end - add_index(:users, :nick, :unique => true) - add_index(:users, :email, :unique => true) + add_index(:users, :nick, unique: true) + add_index(:users, :email, unique: true) # Create the default admin user... puts "Creating user #{USER_ADMIN} with password 'secret'..." - user = User.new(:nick => USER_ADMIN, :first_name => "Anton", :last_name => "Administrator", :email => "admin@foo.test") - user.password = "secret" - raise "Failed!" unless user.save && User.find_by_nick(USER_ADMIN).has_password("secret") + user = User.new(nick: USER_ADMIN, first_name: 'Anton', last_name: 'Administrator', + email: 'admin@foo.test') + user.password = 'secret' + raise 'Failed!' unless user.save && User.find_by_nick(USER_ADMIN).has_password('secret') # Create a normal user... puts "Creating user #{USER_TEST} with password 'foobar'..." - user = User.new(:nick => USER_TEST, :first_name => "Tim", :last_name => "Tester", :email => "test@foo.test") - user.password = "foobar" - raise "Failed!" unless user.save && User.find_by_nick(USER_TEST).has_password("foobar") + user = User.new(nick: USER_TEST, first_name: 'Tim', last_name: 'Tester', email: 'test@foo.test') + user.password = 'foobar' + raise 'Failed!' unless user.save && User.find_by_nick(USER_TEST).has_password('foobar') end def self.down diff --git a/db/migrate/002_create_groups.rb b/db/migrate/002_create_groups.rb index bb7427b9..31104a1e 100644 --- a/db/migrate/002_create_groups.rb +++ b/db/migrate/002_create_groups.rb @@ -4,47 +4,48 @@ class CreateGroups < ActiveRecord::Migration[4.2] def self.up create_table :groups do |t| - t.column :type, :string, :null => false # inheritance, types: Group, OrderGroup - t.column :name, :string, :null => false + t.column :type, :string, null: false # inheritance, types: Group, OrderGroup + t.column :name, :string, null: false t.column :description, :string t.column :actual_size, :integer # OrderGroup column - t.column :account_balance, :decimal, :precision => 8, :scale => 2, :null => false, :default => 0 # OrderGroup column + t.column :account_balance, :decimal, precision: 8, scale: 2, null: false, default: 0 # OrderGroup column t.column :account_updated, :timestamp # OrderGroup column - t.column :created_on, :timestamp, :null => false - t.column :role_admin, :boolean, :default => false, :null => false + t.column :created_on, :timestamp, null: false + t.column :role_admin, :boolean, default: false, null: false end - add_index(:groups, :name, :unique => true) + add_index(:groups, :name, unique: true) create_table :memberships do |t| - t.column :group_id, :integer, :null => false - t.column :user_id, :integer, :null => false + t.column :group_id, :integer, null: false + t.column :user_id, :integer, null: false end - add_index(:memberships, [:user_id, :group_id], :unique => true) + add_index(:memberships, %i[user_id group_id], unique: true) # Create the default "Administrators" group... puts "Creating group #{GROUP_ADMIN}..." - Group.create(:name => GROUP_ADMIN, :description => "System administrators.", :role_admin => true) + Group.create(name: GROUP_ADMIN, description: 'System administrators.', role_admin: true) raise 'Failed!' unless administrators = Group.find_by_name(GROUP_ADMIN) # Create a sample order group... puts "Creating order group #{GROUP_ORDER}..." - ordergroup = OrderGroup.create!(:name => GROUP_ORDER, :description => "A sample order group created by the migration.", :actual_size => 1, :account_updated => Time.now) - raise "Wrong type created!" unless ordergroup.is_a?(OrderGroup) + ordergroup = OrderGroup.create!(name: GROUP_ORDER, + description: 'A sample order group created by the migration.', actual_size: 1, account_updated: Time.now) + raise 'Wrong type created!' unless ordergroup.is_a?(OrderGroup) # Get the admin user and join the admin group... raise "User #{CreateUsers::USER_ADMIN} not found, cannot join group '#{administrators.name}'!" unless admin = User.find_by_nick(CreateUsers::USER_ADMIN) puts "Joining #{CreateUsers::USER_ADMIN} user to new '#{administrators.name}' group as a group admin..." - membership = Membership.create(:group => administrators, :user => admin) - raise "Failed!" unless admin.memberships.first == membership + membership = Membership.create(group: administrators, user: admin) + raise 'Failed!' unless admin.memberships.first == membership raise "User #{CreateUsers::USER_ADMIN} has no admin_roles" unless admin.role_admin? # Get the test user and join the order group... raise "User #{CreateUsers::USER_TEST} not found, cannot join group '#{ordergroup.name}'!" unless test = User.find_by_nick(CreateUsers::USER_TEST) puts "Joining #{CreateUsers::USER_TEST} user to new '#{ordergroup.name}' group as a group admin..." - membership = Membership.create(:group => ordergroup, :user => test) - raise "Failed!" unless test.memberships.first == membership + membership = Membership.create(group: ordergroup, user: test) + raise 'Failed!' unless test.memberships.first == membership end def self.down diff --git a/db/migrate/003_create_suppliers.rb b/db/migrate/003_create_suppliers.rb index 2b38c9c1..72e148b8 100644 --- a/db/migrate/003_create_suppliers.rb +++ b/db/migrate/003_create_suppliers.rb @@ -2,17 +2,17 @@ class CreateSuppliers < ActiveRecord::Migration[4.2] SUPPLIER_SAMPLE = 'Sample Supplier' def self.up - add_column :groups, :role_suppliers, :boolean, :default => false, :null => false + add_column :groups, :role_suppliers, :boolean, default: false, null: false Group.reset_column_information puts "Give #{CreateGroups::GROUP_ADMIN} the role supplier .." - raise "Failed" unless Group.find_by_name(CreateGroups::GROUP_ADMIN).update_attribute(:role_suppliers, true) - raise "Cannot find admin user!" unless admin = User.find_by_nick(CreateUsers::USER_ADMIN) - raise "Failed to enable role_suppliers with admin user!" unless admin.role_suppliers? + raise 'Failed' unless Group.find_by_name(CreateGroups::GROUP_ADMIN).update_attribute(:role_suppliers, true) + raise 'Cannot find admin user!' unless admin = User.find_by_nick(CreateUsers::USER_ADMIN) + raise 'Failed to enable role_suppliers with admin user!' unless admin.role_suppliers? create_table :suppliers do |t| - t.column :name, :string, :null => false - t.column :address, :string, :null => false - t.column :phone, :string, :null => false + t.column :name, :string, null: false + t.column :address, :string, null: false + t.column :phone, :string, null: false t.column :phone2, :string t.column :fax, :string t.column :email, :string @@ -23,12 +23,12 @@ class CreateSuppliers < ActiveRecord::Migration[4.2] t.column :order_howto, :string t.column :note, :string end - add_index(:suppliers, :name, :unique => true) + add_index(:suppliers, :name, unique: true) # Create sample supplier... puts "Creating sample supplier '#{SUPPLIER_SAMPLE}'..." - Supplier.create(:name => SUPPLIER_SAMPLE, :address => "Organic City", :phone => "0123-555555") - raise "Failed!" unless supplier = Supplier.find_by_name(SUPPLIER_SAMPLE) + Supplier.create(name: SUPPLIER_SAMPLE, address: 'Organic City', phone: '0123-555555') + raise 'Failed!' unless supplier = Supplier.find_by_name(SUPPLIER_SAMPLE) end def self.down diff --git a/db/migrate/004_create_article_meta.rb b/db/migrate/004_create_article_meta.rb index eb81f550..36c22f65 100644 --- a/db/migrate/004_create_article_meta.rb +++ b/db/migrate/004_create_article_meta.rb @@ -5,24 +5,24 @@ class CreateArticleMeta < ActiveRecord::Migration[4.2] def self.up # Add user roles... - add_column :groups, :role_article_meta, :boolean, :default => false, :null => false + add_column :groups, :role_article_meta, :boolean, default: false, null: false Group.reset_column_information puts "Give #{CreateGroups::GROUP_ADMIN} the role article_meta .." - raise "Failed" unless Group.find_by_name(CreateGroups::GROUP_ADMIN).update_attribute(:role_article_meta, true) + raise 'Failed' unless Group.find_by_name(CreateGroups::GROUP_ADMIN).update_attribute(:role_article_meta, true) raise 'Cannot find admin user!' unless admin = User.find_by_nick(CreateUsers::USER_ADMIN) raise 'Failed to enable role_article_meta with admin user!' unless admin.role_article_meta? # ArticleCategories create_table :article_categories do |t| - t.column :name, :string, :null => false + t.column :name, :string, null: false t.column :description, :string end - add_index(:article_categories, :name, :unique => true) + add_index(:article_categories, :name, unique: true) # Create sample category... puts "Creating sample article category '#{CATEGORY_SAMPLE}'..." - ArticleCategory.create(:name => CATEGORY_SAMPLE, :description => "This is just a sample article category.") - raise "Failed!" unless category = ArticleCategory.find_by_name(CATEGORY_SAMPLE) + ArticleCategory.create(name: CATEGORY_SAMPLE, description: 'This is just a sample article category.') + raise 'Failed!' unless category = ArticleCategory.find_by_name(CATEGORY_SAMPLE) end def self.down diff --git a/db/migrate/005_create_financial_transactions.rb b/db/migrate/005_create_financial_transactions.rb index 0b1cef89..7ca0e83c 100644 --- a/db/migrate/005_create_financial_transactions.rb +++ b/db/migrate/005_create_financial_transactions.rb @@ -2,19 +2,19 @@ class CreateFinancialTransactions < ActiveRecord::Migration[4.2] def self.up # Create Financial Transactions create_table :financial_transactions do |t| - t.column :order_group_id, :integer, :null => false - t.column :amount, :decimal, :precision => 8, :scale => 2, :null => false - t.column :note, :text, :null => false - t.column :user_id, :integer, :null => false - t.column :created_on, :datetime, :null => false + t.column :order_group_id, :integer, null: false + t.column :amount, :decimal, precision: 8, scale: 2, null: false + t.column :note, :text, null: false + t.column :user_id, :integer, null: false + t.column :created_on, :datetime, null: false end # add column for the finance role puts 'add column in "groups" for the finance role' - add_column :groups, :role_finance, :boolean, :default => false, :null => false + add_column :groups, :role_finance, :boolean, default: false, null: false Group.reset_column_information puts "Give #{CreateGroups::GROUP_ADMIN} the role finance .." - raise "Failed" unless Group.find_by_name(CreateGroups::GROUP_ADMIN).update_attribute(:role_finance, true) + raise 'Failed' unless Group.find_by_name(CreateGroups::GROUP_ADMIN).update_attribute(:role_finance, true) raise 'Cannot find admin user!' unless admin = User.find_by_nick(CreateUsers::USER_ADMIN) raise 'Failed to enable role_finance with admin user!' unless admin.role_finance? @@ -27,8 +27,8 @@ class CreateFinancialTransactions < ActiveRecord::Migration[4.2] ordergroup.addFinancialTransaction(i, "Sample Transaction Nr. #{i}", admin) balance += i end - raise "Failed!" unless financial_transaction = FinancialTransaction.find_by_note('Sample Transaction Nr. 1') - raise "Failed to update account_balance!" unless OrderGroup.find(ordergroup.id).account_balance == balance + raise 'Failed!' unless financial_transaction = FinancialTransaction.find_by_note('Sample Transaction Nr. 1') + raise 'Failed to update account_balance!' unless OrderGroup.find(ordergroup.id).account_balance == balance end def self.down diff --git a/db/migrate/006_create_articles.rb b/db/migrate/006_create_articles.rb index 9a43c8dc..fd11e659 100644 --- a/db/migrate/006_create_articles.rb +++ b/db/migrate/006_create_articles.rb @@ -1,17 +1,17 @@ class CreateArticles < ActiveRecord::Migration[4.2] - SAMPLE_ARTICLE_NAMES = ['banana', 'kiwi', 'strawberry'] + SAMPLE_ARTICLE_NAMES = %w[banana kiwi strawberry] def self.up create_table :articles do |t| - t.column :name, :string, :null => false - t.column :supplier_id, :integer, :null => false - t.column :article_category_id, :integer, :null => false - t.column :unit, :string, :null => false + t.column :name, :string, null: false + t.column :supplier_id, :integer, null: false + t.column :article_category_id, :integer, null: false + t.column :unit, :string, null: false t.column :note, :string - t.column :availability, :boolean, :default => true, :null => false + t.column :availability, :boolean, default: true, null: false t.column :current_price_id, :integer end - add_index(:articles, :name, :unique => true) + add_index(:articles, :name, unique: true) # Create 30 sample articles... puts "Create 3 articles of the supplier '#{CreateSuppliers::SUPPLIER_SAMPLE}'..." @@ -20,14 +20,14 @@ class CreateArticles < ActiveRecord::Migration[4.2] SAMPLE_ARTICLE_NAMES.each do |a| puts 'Create Article ' + a - Article.create(:name => a, - :supplier => supplier, - :article_category => category, - :unit => '500g', - :note => 'delicious', - :availability => true) + Article.create(name: a, + supplier: supplier, + article_category: category, + unit: '500g', + note: 'delicious', + availability: true) end - raise "Failed!" unless Article.find(:all).length == SAMPLE_ARTICLE_NAMES.length + raise 'Failed!' unless Article.find(:all).length == SAMPLE_ARTICLE_NAMES.length end def self.down diff --git a/db/migrate/007_create_article_prices.rb b/db/migrate/007_create_article_prices.rb index ed5b0793..56167a1a 100644 --- a/db/migrate/007_create_article_prices.rb +++ b/db/migrate/007_create_article_prices.rb @@ -1,13 +1,13 @@ class CreateArticlePrices < ActiveRecord::Migration[4.2] def self.up create_table :article_prices do |t| - t.column :article_id, :int, :null => false - t.column :clear_price, :decimal, :precision => 8, :scale => 2, :null => false - t.column :gross_price, :decimal, :precision => 8, :scale => 2, :null => false # gross price, incl. vat, refund and price markup - t.column :tax, :float, :null => false, :default => 0 - t.column :refund, :decimal, :precision => 8, :scale => 2, :null => false, :default => 0 + t.column :article_id, :int, null: false + t.column :clear_price, :decimal, precision: 8, scale: 2, null: false + t.column :gross_price, :decimal, precision: 8, scale: 2, null: false # gross price, incl. vat, refund and price markup + t.column :tax, :float, null: false, default: 0 + t.column :refund, :decimal, precision: 8, scale: 2, null: false, default: 0 t.column :updated_on, :datetime - t.column :unit_quantity, :int, :default => 1, :null => false + t.column :unit_quantity, :int, default: 1, null: false t.column :order_number, :string end add_index(:article_prices, :article_id) @@ -18,11 +18,11 @@ class CreateArticlePrices < ActiveRecord::Migration[4.2] puts 'Create Price for article ' + a raise "article #{a} not found!" unless article = Article.find_by_name(a) - new_price = ArticlePrice.new(:clear_price => rand(4) + 1, - :tax => 7.0, - :refund => 0, - :unit_quantity => rand(10) + 1, - :order_number => rand(9999)) + new_price = ArticlePrice.new(clear_price: rand(1..4), + tax: 7.0, + refund: 0, + unit_quantity: rand(1..10), + order_number: rand(9999)) article.add_price(new_price) raise 'Failed!' unless ArticlePrice.find_by_article_id(article.id) end diff --git a/db/migrate/008_create_orders.rb b/db/migrate/008_create_orders.rb index 6eb8c921..4e138899 100644 --- a/db/migrate/008_create_orders.rb +++ b/db/migrate/008_create_orders.rb @@ -4,28 +4,28 @@ class CreateOrders < ActiveRecord::Migration[4.2] def self.up # Order role - add_column :groups, :role_orders, :boolean, :default => false, :null => false + add_column :groups, :role_orders, :boolean, default: false, null: false Group.reset_column_information puts "Give #{CreateGroups::GROUP_ADMIN} the role finance .." - raise "Failed" unless Group.find_by_name(CreateGroups::GROUP_ADMIN).update_attribute(:role_orders, true) + raise 'Failed' unless Group.find_by_name(CreateGroups::GROUP_ADMIN).update_attribute(:role_orders, true) raise 'Cannot find admin user!' unless admin = User.find_by_nick(CreateUsers::USER_ADMIN) raise 'Failed to enable role_orders with admin user!' unless admin.role_orders? # Create the default "Order" group... puts 'Creating group "Orders"...' - Group.create(:name => GROUP_ORDER, :description => "working group for managing orders", :role_orders => true) - raise "Failed!" unless Group.find_by_name(GROUP_ORDER) + Group.create(name: GROUP_ORDER, description: 'working group for managing orders', role_orders: true) + raise 'Failed!' unless Group.find_by_name(GROUP_ORDER) # Order create_table :orders do |t| - t.column :name, :string, :null => false - t.column :supplier_id, :integer, :null => false - t.column :starts, :datetime, :null => false + t.column :name, :string, null: false + t.column :supplier_id, :integer, null: false + t.column :starts, :datetime, null: false t.column :ends, :datetime t.column :note, :string - t.column :finished, :boolean, :default => false, :null => false - t.column :booked, :boolean, :null => false, :default => false - t.column :lock_version, :integer, :null => false, :default => 0 + t.column :finished, :boolean, default: false, null: false + t.column :booked, :boolean, null: false, default: false + t.column :lock_version, :integer, null: false, default: 0 t.column :updated_by_user_id, :integer end add_index(:orders, :starts) @@ -35,74 +35,77 @@ class CreateOrders < ActiveRecord::Migration[4.2] puts "Creating order '#{ORDER_TEST}'..." raise "Supplier '#{CreateSuppliers::SUPPLIER_SAMPLE}' not found!" unless supplier = Supplier.find_by_name(CreateSuppliers::SUPPLIER_SAMPLE) - Order.create(:name => ORDER_TEST, :supplier => supplier, :starts => Time.now) + Order.create(name: ORDER_TEST, supplier: supplier, starts: Time.now) raise 'Creating test order failed!' unless order = Order.find_by_name(ORDER_TEST) # OrderArticle create_table :order_articles do |t| - t.column :order_id, :integer, :null => false - t.column :article_id, :integer, :null => false - t.column :quantity, :integer, :null => false, :default => 0 - t.column :tolerance, :integer, :null => false, :default => 0 - t.column :units_to_order, :integer, :null => false, :default => 0 - t.column :lock_version, :integer, :null => false, :default => 0 + t.column :order_id, :integer, null: false + t.column :article_id, :integer, null: false + t.column :quantity, :integer, null: false, default: 0 + t.column :tolerance, :integer, null: false, default: 0 + t.column :units_to_order, :integer, null: false, default: 0 + t.column :lock_version, :integer, null: false, default: 0 end - add_index(:order_articles, [:order_id, :article_id], :unique => true) + add_index(:order_articles, %i[order_id article_id], unique: true) puts 'Adding articles to the order...' - CreateArticles::SAMPLE_ARTICLE_NAMES.each { |a| + CreateArticles::SAMPLE_ARTICLE_NAMES.each do |a| puts "Article #{a}..." raise 'Article not found!' unless article = Article.find_by_name(a) raise 'No price found for article!' unless price = article.current_price - OrderArticle.create(:order => order, :article => article) + OrderArticle.create(order: order, article: article) raise 'Creating OrderArticle failed!' unless OrderArticle.find_by_order_id_and_article_id(order.id, article.id) - } + end raise 'Creating OrderArticles failed!' unless order.articles.size == CreateArticles::SAMPLE_ARTICLE_NAMES.length # GroupOrder create_table :group_orders do |t| - t.column :order_group_id, :integer, :null => false - t.column :order_id, :integer, :null => false - t.column :price, :decimal, :precision => 8, :scale => 2, :null => false, :default => 0 - t.column :lock_version, :integer, :null => false, :default => 0 - t.column :updated_on, :timestamp, :null => false - t.column :updated_by_user_id, :integer, :null => false + t.column :order_group_id, :integer, null: false + t.column :order_id, :integer, null: false + t.column :price, :decimal, precision: 8, scale: 2, null: false, default: 0 + t.column :lock_version, :integer, null: false, default: 0 + t.column :updated_on, :timestamp, null: false + t.column :updated_by_user_id, :integer, null: false end - add_index(:group_orders, [:order_group_id, :order_id], :unique => true) + add_index(:group_orders, %i[order_group_id order_id], unique: true) puts 'Adding group order...' raise "Cannot find user #{CreateUsers::USER_TEST}" unless user = User.find_by_nick(CreateUsers::USER_TEST) raise "Cannot find OrderGroup '#{CreateGroups::GROUP_ORDER}'!" unless orderGroup = OrderGroup.find_by_name(CreateGroups::GROUP_ORDER) - GroupOrder.create(:order_group => orderGroup, :order => order, :price => 0, :updated_by => user) - raise 'Retrieving group order failed!' unless groupOrder = orderGroup.group_orders.find(:first, :conditions => "order_id = #{order.id}") + GroupOrder.create(order_group: orderGroup, order: order, price: 0, updated_by: user) + raise 'Retrieving group order failed!' unless groupOrder = orderGroup.group_orders.find(:first, + conditions: "order_id = #{order.id}") # GroupOrderArticles create_table :group_order_articles do |t| - t.column :group_order_id, :integer, :null => false - t.column :order_article_id, :integer, :null => false - t.column :quantity, :integer, :null => false - t.column :tolerance, :integer, :null => false - t.column :updated_on, :timestamp, :null => false + t.column :group_order_id, :integer, null: false + t.column :order_article_id, :integer, null: false + t.column :quantity, :integer, null: false + t.column :tolerance, :integer, null: false + t.column :updated_on, :timestamp, null: false end - add_index(:group_order_articles, [:group_order_id, :order_article_id], :unique => true, :name => "goa_index") + add_index(:group_order_articles, %i[group_order_id order_article_id], unique: true, name: 'goa_index') # GroupOrderArticleQuantity create_table :group_order_article_quantities do |t| - t.column :group_order_article_id, :int, :null => false - t.column :quantity, :int, :default => 0 - t.column :tolerance, :int, :default => 0 - t.column :created_on, :timestamp, :null => false + t.column :group_order_article_id, :int, null: false + t.column :quantity, :int, default: 0 + t.column :tolerance, :int, default: 0 + t.column :created_on, :timestamp, null: false end puts 'Adding articles to group order...' - order.order_articles.each { |orderArticle| + order.order_articles.each do |orderArticle| puts "Article #{orderArticle.article.name}..." - GroupOrderArticle.create(:group_order => groupOrder, :order_article => orderArticle, :quantity => 0, :tolerance => 0) - raise 'Failed to create order!' unless article = GroupOrderArticle.find(:first, :conditions => "group_order_id = #{groupOrder.id} AND order_article_id = #{orderArticle.id}") + GroupOrderArticle.create(group_order: groupOrder, order_article: orderArticle, quantity: 0, + tolerance: 0) + raise 'Failed to create order!' unless article = GroupOrderArticle.find(:first, + conditions: "group_order_id = #{groupOrder.id} AND order_article_id = #{orderArticle.id}") - article.updateQuantities(rand(6) + 1, rand(4) + 1) - } + article.updateQuantities(rand(1..6), rand(1..4)) + end raise 'Failed to create orders!' unless groupOrder.order_articles.size == order.order_articles.size groupOrder.updatePrice diff --git a/db/migrate/009_create_order_results.rb b/db/migrate/009_create_order_results.rb index 20b75193..6b1cc65a 100644 --- a/db/migrate/009_create_order_results.rb +++ b/db/migrate/009_create_order_results.rb @@ -1,32 +1,32 @@ class CreateOrderResults < ActiveRecord::Migration[4.2] def self.up create_table :group_order_results do |t| - t.column :order_id, :int, :null => false - t.column :group_name, :string, :null => false - t.column :price, :decimal, :precision => 8, :scale => 2, :null => false, :default => 0 + t.column :order_id, :int, null: false + t.column :group_name, :string, null: false + t.column :price, :decimal, precision: 8, scale: 2, null: false, default: 0 end - add_index(:group_order_results, [:group_name, :order_id], :unique => true) + add_index(:group_order_results, %i[group_name order_id], unique: true) create_table :order_article_results do |t| - t.column :order_id, :int, :null => false - t.column :name, :string, :null => false - t.column :unit, :string, :null => false + t.column :order_id, :int, null: false + t.column :name, :string, null: false + t.column :unit, :string, null: false t.column :note, :string - t.column :clear_price, :decimal, :precision => 8, :scale => 2, :null => false - t.column :gross_price, :decimal, :precision => 8, :scale => 2, :null => false - t.column :tax, :float, :null => false, :default => 0 - t.column :refund, :decimal, :precision => 8, :scale => 2 - t.column :fc_markup, :float, :null => false + t.column :clear_price, :decimal, precision: 8, scale: 2, null: false + t.column :gross_price, :decimal, precision: 8, scale: 2, null: false + t.column :tax, :float, null: false, default: 0 + t.column :refund, :decimal, precision: 8, scale: 2 + t.column :fc_markup, :float, null: false t.column :order_number, :string - t.column :unit_quantity, :int, :null => false - t.column :units_to_order, :int, :null => false + t.column :unit_quantity, :int, null: false + t.column :units_to_order, :int, null: false end add_index(:order_article_results, :order_id) create_table :group_order_article_results do |t| - t.column :order_article_result_id, :int, :null => false - t.column :group_order_result_id, :int, :null => false - t.column :quantity, :int, :null => false + t.column :order_article_result_id, :int, null: false + t.column :group_order_result_id, :int, null: false + t.column :quantity, :int, null: false t.column :tolerance, :int end add_index(:group_order_article_results, :order_article_result_id) diff --git a/db/migrate/011_create_comments.rb b/db/migrate/011_create_comments.rb index 28fc0428..55148d08 100644 --- a/db/migrate/011_create_comments.rb +++ b/db/migrate/011_create_comments.rb @@ -1,16 +1,16 @@ class CreateComments < ActiveRecord::Migration[4.2] def self.up - create_table :comments, :force => true do |t| - t.column :title, :string, :limit => 50, :default => "" - t.column :comment, :string, :default => "" - t.column :created_at, :datetime, :null => false - t.column :commentable_id, :integer, :default => 0, :null => false - t.column :commentable_type, :string, :limit => 15, - :default => "", :null => false - t.column :user_id, :integer, :default => 0, :null => false + create_table :comments, force: true do |t| + t.column :title, :string, limit: 50, default: '' + t.column :comment, :string, default: '' + t.column :created_at, :datetime, null: false + t.column :commentable_id, :integer, default: 0, null: false + t.column :commentable_type, :string, limit: 15, + default: '', null: false + t.column :user_id, :integer, default: 0, null: false end - add_index :comments, ["user_id"], :name => "fk_comments_user" + add_index :comments, ['user_id'], name: 'fk_comments_user' end def self.down diff --git a/db/migrate/012_create_order_clearing.rb b/db/migrate/012_create_order_clearing.rb index 1d3133fd..9ddb4ad3 100644 --- a/db/migrate/012_create_order_clearing.rb +++ b/db/migrate/012_create_order_clearing.rb @@ -1,8 +1,8 @@ class CreateOrderClearing < ActiveRecord::Migration[4.2] def self.up - add_column :orders, :invoice_amount, :decimal, :precision => 8, :scale => 2, :null => false, :default => 0 - add_column :orders, :refund, :decimal, :precision => 8, :scale => 2, :null => false, :default => 0 - add_column :orders, :refund_credit, :decimal, :precision => 8, :scale => 2, :null => false, :default => 0 + add_column :orders, :invoice_amount, :decimal, precision: 8, scale: 2, null: false, default: 0 + add_column :orders, :refund, :decimal, precision: 8, scale: 2, null: false, default: 0 + add_column :orders, :refund_credit, :decimal, precision: 8, scale: 2, null: false, default: 0 add_column :orders, :invoice_number, :string add_column :orders, :invoice_date, :string end diff --git a/db/migrate/013_add_messaging.rb b/db/migrate/013_add_messaging.rb index 84ba8d6f..7f01b2dd 100644 --- a/db/migrate/013_add_messaging.rb +++ b/db/migrate/013_add_messaging.rb @@ -3,13 +3,13 @@ class AddMessaging < ActiveRecord::Migration[4.2] # Table that holds the messages: create_table :messages do |t| t.column :sender_id, :integer - t.column :recipient_id, :integer, :null => false - t.column :recipients, :string, :null => false - t.column :subject, :string, :null => false - t.column :body, :text, :null => false - t.column :read, :boolean, :null => false, :default => false - t.column :email_state, :integer, :null => false - t.column :created_on, :timestamp, :null => false + t.column :recipient_id, :integer, null: false + t.column :recipients, :string, null: false + t.column :subject, :string, null: false + t.column :body, :text, null: false + t.column :read, :boolean, null: false, default: false + t.column :email_state, :integer, null: false + t.column :created_on, :timestamp, null: false end add_index(:messages, :sender_id) add_index(:messages, :recipient_id) diff --git a/db/migrate/014_create_tasks.rb b/db/migrate/014_create_tasks.rb index db878546..8872523e 100644 --- a/db/migrate/014_create_tasks.rb +++ b/db/migrate/014_create_tasks.rb @@ -1,26 +1,26 @@ class CreateTasks < ActiveRecord::Migration[4.2] def self.up create_table :tasks do |t| - t.column :name, :string, :null => false + t.column :name, :string, null: false t.column :description, :string t.column :due_date, :date - t.column :done, :boolean, :default => false + t.column :done, :boolean, default: false t.column :group_id, :integer - t.column :assigned, :boolean, :default => false - t.column :created_on, :datetime, :null => false - t.column :updated_on, :datetime, :null => false + t.column :assigned, :boolean, default: false + t.column :created_on, :datetime, null: false + t.column :updated_on, :datetime, null: false end add_index :tasks, :name add_index :tasks, :due_date create_table :assignments do |t| - t.column :user_id, :integer, :null => false - t.column :task_id, :integer, :null => false - t.column :accepted, :boolean, :default => false + t.column :user_id, :integer, null: false + t.column :task_id, :integer, null: false + t.column :accepted, :boolean, default: false end - add_index :assignments, [:user_id, :task_id], :unique => true + add_index :assignments, %i[user_id task_id], unique: true - add_column :groups, :weekly_task, :boolean, :default => false # if group has an job for every week + add_column :groups, :weekly_task, :boolean, default: false # if group has an job for every week add_column :groups, :weekday, :integer # e.g. 1 means monday, 2 = tuesday an so on add_column :groups, :task_name, :string # the name of the weekly task add_column :groups, :task_description, :string diff --git a/db/migrate/015_change_result_quantities.rb b/db/migrate/015_change_result_quantities.rb index 23731334..56020eab 100644 --- a/db/migrate/015_change_result_quantities.rb +++ b/db/migrate/015_change_result_quantities.rb @@ -1,11 +1,11 @@ class ChangeResultQuantities < ActiveRecord::Migration[4.2] def self.up - change_column :group_order_article_results, :quantity, :decimal, :precision => 6, :scale => 3 - change_column :order_article_results, :units_to_order, :decimal, :precision => 6, :scale => 3, :null => false + change_column :group_order_article_results, :quantity, :decimal, precision: 6, scale: 3 + change_column :order_article_results, :units_to_order, :decimal, precision: 6, scale: 3, null: false end def self.down - change_column :group_order_article_results, :quantity, :integer, :null => false - change_column :order_article_results, :units_to_order, :integer, :default => 0, :null => false + change_column :group_order_article_results, :quantity, :integer, null: false + change_column :order_article_results, :units_to_order, :integer, default: 0, null: false end end diff --git a/db/migrate/018_create_invites.rb b/db/migrate/018_create_invites.rb index cc8a1ebc..49c3edf9 100644 --- a/db/migrate/018_create_invites.rb +++ b/db/migrate/018_create_invites.rb @@ -1,11 +1,11 @@ class CreateInvites < ActiveRecord::Migration[4.2] def self.up create_table :invites do |t| - t.column :token, :string, :null => false - t.column :expires_at, :timestamp, :null => false - t.column :group_id, :integer, :null => false - t.column :user_id, :integer, :null => false - t.column :email, :string, :null => false + t.column :token, :string, null: false + t.column :expires_at, :timestamp, null: false + t.column :group_id, :integer, null: false + t.column :user_id, :integer, null: false + t.column :email, :string, null: false end add_index :invites, :token end diff --git a/db/migrate/019_remove_uniqueness_of_article_name.rb b/db/migrate/019_remove_uniqueness_of_article_name.rb index 7504a66a..50170cb8 100644 --- a/db/migrate/019_remove_uniqueness_of_article_name.rb +++ b/db/migrate/019_remove_uniqueness_of_article_name.rb @@ -1,11 +1,11 @@ class RemoveUniquenessOfArticleName < ActiveRecord::Migration[4.2] def self.up remove_index :articles, :name - add_index :articles, [:name, :supplier_id] + add_index :articles, %i[name supplier_id] end def self.down - remove_index :articles, [:name, :supplier_id] - add_index :articles, :name, :unique => true + remove_index :articles, %i[name supplier_id] + add_index :articles, :name, unique: true end end diff --git a/db/migrate/021_remove_table_article_prices.rb b/db/migrate/021_remove_table_article_prices.rb index 7f172065..9b586fca 100644 --- a/db/migrate/021_remove_table_article_prices.rb +++ b/db/migrate/021_remove_table_article_prices.rb @@ -1,19 +1,19 @@ class RemoveTableArticlePrices < ActiveRecord::Migration[4.2] def self.up - puts "create columns in articles ..." - add_column "articles", "clear_price", :decimal, :precision => 8, :scale => 2, :default => 0.0, :null => false - add_column "articles", "gross_price", :decimal, :precision => 8, :scale => 2, :default => 0.0, :null => false - add_column "articles", "tax", :float - add_column "articles", "refund", :decimal, :precision => 8, :scale => 2, :default => 0.0, :null => false - add_column "articles", "unit_quantity", :integer, :default => 1, :null => false - add_column "articles", "order_number", :string - add_column "articles", "created_at", :datetime - add_column "articles", "updated_at", :datetime + puts 'create columns in articles ...' + add_column 'articles', 'clear_price', :decimal, precision: 8, scale: 2, default: 0.0, null: false + add_column 'articles', 'gross_price', :decimal, precision: 8, scale: 2, default: 0.0, null: false + add_column 'articles', 'tax', :float + add_column 'articles', 'refund', :decimal, precision: 8, scale: 2, default: 0.0, null: false + add_column 'articles', 'unit_quantity', :integer, default: 1, null: false + add_column 'articles', 'order_number', :string + add_column 'articles', 'created_at', :datetime + add_column 'articles', 'updated_at', :datetime # stop auto-updating the timestamps to make the data-copy safe! Article.record_timestamps = false - puts "now copy values of article_prices into new articles-columns..." + puts 'now copy values of article_prices into new articles-columns...' Article.find(:all).each do |article| price = article.current_price article.update!(clear_price: price.clear_price, @@ -26,46 +26,46 @@ class RemoveTableArticlePrices < ActiveRecord::Migration[4.2] created_at: price.updated_on) end - puts "delete article_prices, current_price attribute" + puts 'delete article_prices, current_price attribute' drop_table :article_prices remove_column :articles, :current_price_id end def self.down add_column :articles, :current_price_id, :integer - create_table "article_prices", :force => true do |t| - t.integer "article_id", :default => 0, :null => false - t.decimal "clear_price", :precision => 8, :scale => 2, :default => 0.0, :null => false - t.decimal "gross_price", :precision => 8, :scale => 2, :default => 0.0, :null => false - t.float "tax", :default => 0.0, :null => false - t.decimal "refund", :precision => 8, :scale => 2, :default => 0.0, :null => false - t.datetime "updated_on" - t.integer "unit_quantity", :default => 1, :null => false - t.string "order_number" + create_table 'article_prices', force: true do |t| + t.integer 'article_id', default: 0, null: false + t.decimal 'clear_price', precision: 8, scale: 2, default: 0.0, null: false + t.decimal 'gross_price', precision: 8, scale: 2, default: 0.0, null: false + t.float 'tax', default: 0.0, null: false + t.decimal 'refund', precision: 8, scale: 2, default: 0.0, null: false + t.datetime 'updated_on' + t.integer 'unit_quantity', default: 1, null: false + t.string 'order_number' end # copy data from article now into old ArticlePrice-object Article.find(:all).each do |article| - price = ArticlePrice.create(:clear_price => article.clear_price, - :gross_price => article.gross_price, - :tax => article.tax, - :refund => article.refund, - :unit_quantity => article.unit_quantity, - :order_number => article.order_number.blank? ? nil : article.order_number, - :updated_on => article.updated_at) + price = ArticlePrice.create(clear_price: article.clear_price, + gross_price: article.gross_price, + tax: article.tax, + refund: article.refund, + unit_quantity: article.unit_quantity, + order_number: article.order_number.presence, + updated_on: article.updated_at) article.update_attribute(:current_price, price) price.update_attribute(:article, article) end # remove new columns - remove_column "articles", "clear_price" - remove_column "articles", "gross_price" - remove_column "articles", "tax" - remove_column "articles", "refund" - remove_column "articles", "unit_quantity" - remove_column "articles", "order_number" - remove_column "articles", "created_at" - remove_column "articles", "updated_at" + remove_column 'articles', 'clear_price' + remove_column 'articles', 'gross_price' + remove_column 'articles', 'tax' + remove_column 'articles', 'refund' + remove_column 'articles', 'unit_quantity' + remove_column 'articles', 'order_number' + remove_column 'articles', 'created_at' + remove_column 'articles', 'updated_at' end end diff --git a/db/migrate/022_add_required_user_for_task.rb b/db/migrate/022_add_required_user_for_task.rb index 9e8d9621..105e1593 100644 --- a/db/migrate/022_add_required_user_for_task.rb +++ b/db/migrate/022_add_required_user_for_task.rb @@ -1,7 +1,7 @@ class AddRequiredUserForTask < ActiveRecord::Migration[4.2] def self.up - add_column :tasks, :required_users, :integer, :default => 1 - add_column :groups, :task_required_users, :integer, :default => 1 + add_column :tasks, :required_users, :integer, default: 1 + add_column :groups, :task_required_users, :integer, default: 1 # add default values to every task and group Task.find(:all).each { |task| task.update_attribute :required_users, 1 } Group.workgroups.each { |group| group.update_attribute :task_required_users, 1 } diff --git a/db/migrate/024_add_deposit_defaults.rb b/db/migrate/024_add_deposit_defaults.rb index ed9da063..68afdf5a 100644 --- a/db/migrate/024_add_deposit_defaults.rb +++ b/db/migrate/024_add_deposit_defaults.rb @@ -7,6 +7,5 @@ class AddDepositDefaults < ActiveRecord::Migration[4.2] change_column_default :orders, :deposit_credit, 0.0 end - def self.down - end + def self.down; end end diff --git a/db/migrate/025_extend_comments.rb b/db/migrate/025_extend_comments.rb index 662b92fd..3b1b1da2 100644 --- a/db/migrate/025_extend_comments.rb +++ b/db/migrate/025_extend_comments.rb @@ -1,9 +1,9 @@ class ExtendComments < ActiveRecord::Migration[4.2] def self.up - change_column :comments, :comment, :text, :default => "" + change_column :comments, :comment, :text, default: '' end def self.down - change_column :comments, :comment, :string, :default => "" + change_column :comments, :comment, :string, default: '' end end diff --git a/db/migrate/20090120184410_road_to_version_three.rb b/db/migrate/20090120184410_road_to_version_three.rb index 4bacff24..d271aee3 100644 --- a/db/migrate/20090120184410_road_to_version_three.rb +++ b/db/migrate/20090120184410_road_to_version_three.rb @@ -10,10 +10,10 @@ class RoadToVersionThree < ActiveRecord::Migration[4.2] create_table :messages do |t| t.references :sender t.text :recipients_ids - t.string :subject, :null => false + t.string :subject, null: false t.text :body - t.integer :email_state, :default => 0, :null => false - t.boolean :private, :default => false + t.integer :email_state, default: 0, null: false + t.boolean :private, default: false t.datetime :created_at end @@ -23,9 +23,9 @@ class RoadToVersionThree < ActiveRecord::Migration[4.2] add_column :groups, :deleted_at, :datetime # == Workgroups - puts "Migrate all groups to workgroups.." - Group.find(:all, :conditions => { :type => "" }).each do |workgroup| - workgroup.update_attribute(:type, "Workgroup") + puts 'Migrate all groups to workgroups..' + Group.find(:all, conditions: { type: '' }).each do |workgroup| + workgroup.update_attribute(:type, 'Workgroup') end # == Ordergroups @@ -34,11 +34,11 @@ class RoadToVersionThree < ActiveRecord::Migration[4.2] rename_column :financial_transactions, :order_group_id, :ordergroup_id rename_column :group_orders, :order_group_id, :ordergroup_id rename_column :tasks, :group_id, :workgroup_id - remove_index :group_orders, :name => "index_group_orders_on_order_group_id_and_order_id" - add_index :group_orders, [:ordergroup_id, :order_id], :unique => true + remove_index :group_orders, name: 'index_group_orders_on_order_group_id_and_order_id' + add_index :group_orders, %i[ordergroup_id order_id], unique: true - Group.find(:all, :conditions => { :type => "OrderGroup" }).each do |ordergroup| - ordergroup.update_attribute(:type, "Ordergroup") + Group.find(:all, conditions: { type: 'OrderGroup' }).each do |ordergroup| + ordergroup.update_attribute(:type, 'Ordergroup') end # move contact-infos from users to ordergroups add_column :groups, :contact_person, :string @@ -46,9 +46,7 @@ class RoadToVersionThree < ActiveRecord::Migration[4.2] add_column :groups, :contact_address, :string Ordergroup.all.each do |ordergroup| contact = ordergroup.users.first - if contact - ordergroup.update(contact_person: contact.name, contact_phone: contact.phone, contact_address: contact.address) - end + ordergroup.update(contact_person: contact.name, contact_phone: contact.phone, contact_address: contact.address) if contact end remove_column :users, :address @@ -57,15 +55,18 @@ class RoadToVersionThree < ActiveRecord::Migration[4.2] drop_table :group_order_results drop_table :order_article_results drop_table :group_order_article_results - GroupOrder.delete_all; OrderArticle.delete_all; GroupOrderArticle.delete_all; GroupOrderArticleQuantity.delete_all + GroupOrder.delete_all + OrderArticle.delete_all + GroupOrderArticle.delete_all + GroupOrderArticleQuantity.delete_all create_table :orders do |t| t.references :supplier t.text :note t.datetime :starts t.datetime :ends - t.string :state, :default => "open" # Statemachine ... open -> finished -> closed - t.integer :lock_version, :default => 0, :null => false + t.string :state, default: 'open' # Statemachine ... open -> finished -> closed + t.integer :lock_version, default: 0, null: false t.integer :updated_by_user_id end @@ -78,9 +79,9 @@ class RoadToVersionThree < ActiveRecord::Migration[4.2] t.date :date t.date :paid_on t.text :note - t.decimal :amount, :null => false, :precision => 8, :scale => 2, :default => 0.0 - t.decimal :deposit, :precision => 8, :scale => 2, :default => 0.0, :null => false - t.decimal :deposit_credit, :precision => 8, :scale => 2, :default => 0.0, :null => false + t.decimal :amount, null: false, precision: 8, scale: 2, default: 0.0 + t.decimal :deposit, precision: 8, scale: 2, default: 0.0, null: false + t.decimal :deposit_credit, precision: 8, scale: 2, default: 0.0, null: false t.timestamps end @@ -107,41 +108,41 @@ class RoadToVersionThree < ActiveRecord::Migration[4.2] # == ArticlePrice create_table :article_prices do |t| t.references :article - t.decimal :price, :precision => 8, :scale => 2, :default => 0.0, :null => false - t.decimal :tax, :precision => 8, :scale => 2, :default => 0.0, :null => false - t.decimal :deposit, :precision => 8, :scale => 2, :default => 0.0, :null => false + t.decimal :price, precision: 8, scale: 2, default: 0.0, null: false + t.decimal :tax, precision: 8, scale: 2, default: 0.0, null: false + t.decimal :deposit, precision: 8, scale: 2, default: 0.0, null: false t.integer :unit_quantity t.datetime :created_at end # Create price history for every Article Article.all.each do |a| - a.article_prices.create :price => a.price, :tax => a.tax, - :deposit => a.deposit, :unit_quantity => a.unit_quantity + a.article_prices.create price: a.price, tax: a.tax, + deposit: a.deposit, unit_quantity: a.unit_quantity end # Every Article has now a Category. Fix it if neccessary. - Article.all(:conditions => { :article_category_id => nil }).each do |article| + Article.all(conditions: { article_category_id: nil }).each do |article| article.update_attribute(:article_category, ArticleCategory.first) end # order-articles add_column :order_articles, :article_price_id, :integer # == GroupOrder - change_column :group_orders, :updated_by_user_id, :integer, :default => nil, :null => true + change_column :group_orders, :updated_by_user_id, :integer, default: nil, null: true # == GroupOrderArticle # The total order result in ordergroup is now saved! - add_column :group_order_articles, :result, :integer, :default => nil + add_column :group_order_articles, :result, :integer, default: nil # == StockArticle add_column :articles, :type, :string - add_column :articles, :quantity, :integer, :default => 0 + add_column :articles, :quantity, :integer, default: 0 # == StockChanges create_table :stock_changes do |t| t.references :delivery t.references :order t.references :stock_article - t.integer :quantity, :default => 0 + t.integer :quantity, default: 0 t.datetime :created_at end @@ -158,6 +159,5 @@ class RoadToVersionThree < ActiveRecord::Migration[4.2] User.all.each { |u| u.settings['notify.upcoming_tasks'] = 1 } end - def self.down - end + def self.down; end end diff --git a/db/migrate/20090317175355_add_profit_to_orders.rb b/db/migrate/20090317175355_add_profit_to_orders.rb index 59f79609..78013f7d 100644 --- a/db/migrate/20090317175355_add_profit_to_orders.rb +++ b/db/migrate/20090317175355_add_profit_to_orders.rb @@ -1,6 +1,6 @@ class AddProfitToOrders < ActiveRecord::Migration[4.2] def self.up - add_column :orders, :foodcoop_result, :decimal, :precision => 8, :scale => 2 + add_column :orders, :foodcoop_result, :decimal, precision: 8, scale: 2 Order.closed.each do |order| order.update_attribute(:foodcoop_result, order.profit) diff --git a/db/migrate/20090325175756_create_pages.foodsoft_wiki_engine.rb b/db/migrate/20090325175756_create_pages.foodsoft_wiki_engine.rb index c5692f35..d2221013 100644 --- a/db/migrate/20090325175756_create_pages.foodsoft_wiki_engine.rb +++ b/db/migrate/20090325175756_create_pages.foodsoft_wiki_engine.rb @@ -5,7 +5,7 @@ class CreatePages < ActiveRecord::Migration[4.2] t.string :title t.text :body t.string :permalink - t.integer :lock_version, :default => 0 + t.integer :lock_version, default: 0 t.integer :updated_by t.integer :redirect t.integer :parent_id diff --git a/db/migrate/20090405131156_modify_group_order_article_result.rb b/db/migrate/20090405131156_modify_group_order_article_result.rb index 44e0dea8..b979422e 100644 --- a/db/migrate/20090405131156_modify_group_order_article_result.rb +++ b/db/migrate/20090405131156_modify_group_order_article_result.rb @@ -1,6 +1,6 @@ class ModifyGroupOrderArticleResult < ActiveRecord::Migration[4.2] def self.up - change_column :group_order_articles, :result, :decimal, :precision => 8, :scale => 3 + change_column :group_order_articles, :result, :decimal, precision: 8, scale: 3 end def self.down diff --git a/db/migrate/20090907120012_add_missing_indexes.rb b/db/migrate/20090907120012_add_missing_indexes.rb index 189b0417..93ea8771 100644 --- a/db/migrate/20090907120012_add_missing_indexes.rb +++ b/db/migrate/20090907120012_add_missing_indexes.rb @@ -1,35 +1,34 @@ class AddMissingIndexes < ActiveRecord::Migration[4.2] def self.up - add_index "article_prices", ["article_id"] + add_index 'article_prices', ['article_id'] - add_index "articles", ["supplier_id"] - add_index "articles", ["article_category_id"] - add_index "articles", ["type"] + add_index 'articles', ['supplier_id'] + add_index 'articles', ['article_category_id'] + add_index 'articles', ['type'] - add_index "deliveries", ["supplier_id"] + add_index 'deliveries', ['supplier_id'] - add_index "financial_transactions", ["ordergroup_id"] + add_index 'financial_transactions', ['ordergroup_id'] - add_index "group_order_article_quantities", ["group_order_article_id"] - add_index "group_orders", ["order_id"] - add_index "group_orders", ["ordergroup_id"] + add_index 'group_order_article_quantities', ['group_order_article_id'] + add_index 'group_orders', ['order_id'] + add_index 'group_orders', ['ordergroup_id'] - add_index "invoices", ["supplier_id"] - add_index "invoices", ["delivery_id"] + add_index 'invoices', ['supplier_id'] + add_index 'invoices', ['delivery_id'] - add_index "order_articles", ["order_id"] + add_index 'order_articles', ['order_id'] - add_index "order_comments", ["order_id"] + add_index 'order_comments', ['order_id'] - add_index "orders", ["state"] + add_index 'orders', ['state'] - add_index "stock_changes", ["delivery_id"] - add_index "stock_changes", ["stock_article_id"] - add_index "stock_changes", ["stock_taking_id"] + add_index 'stock_changes', ['delivery_id'] + add_index 'stock_changes', ['stock_article_id'] + add_index 'stock_changes', ['stock_taking_id'] - add_index "tasks", ["workgroup_id"] + add_index 'tasks', ['workgroup_id'] end - def self.down - end + def self.down; end end diff --git a/db/migrate/20110507184920_add_duration_to_tasks.rb b/db/migrate/20110507184920_add_duration_to_tasks.rb index 33a11494..86347508 100644 --- a/db/migrate/20110507184920_add_duration_to_tasks.rb +++ b/db/migrate/20110507184920_add_duration_to_tasks.rb @@ -1,6 +1,6 @@ class AddDurationToTasks < ActiveRecord::Migration[4.2] def self.up - add_column :tasks, :duration, :integer, :default => 1 + add_column :tasks, :duration, :integer, default: 1 end def self.down diff --git a/db/migrate/20110507192928_add_task_duration_to_workgroups.rb b/db/migrate/20110507192928_add_task_duration_to_workgroups.rb index c5b4844b..fd703d17 100644 --- a/db/migrate/20110507192928_add_task_duration_to_workgroups.rb +++ b/db/migrate/20110507192928_add_task_duration_to_workgroups.rb @@ -1,6 +1,6 @@ class AddTaskDurationToWorkgroups < ActiveRecord::Migration[4.2] def self.up - add_column :groups, :task_duration, :integer, :default => 1 + add_column :groups, :task_duration, :integer, default: 1 end def self.down diff --git a/db/migrate/20120622094337_add_next_weekly_tasks_number_to_workgroups.rb b/db/migrate/20120622094337_add_next_weekly_tasks_number_to_workgroups.rb index b8ac8c81..eeca92b3 100644 --- a/db/migrate/20120622094337_add_next_weekly_tasks_number_to_workgroups.rb +++ b/db/migrate/20120622094337_add_next_weekly_tasks_number_to_workgroups.rb @@ -1,6 +1,6 @@ class AddNextWeeklyTasksNumberToWorkgroups < ActiveRecord::Migration[4.2] def self.up - add_column :groups, :next_weekly_tasks_number, :integer, :default => 8 + add_column :groups, :next_weekly_tasks_number, :integer, default: 8 end def self.down diff --git a/db/migrate/20130622095040_move_weekly_tasks.rb b/db/migrate/20130622095040_move_weekly_tasks.rb index 3865a498..d57f3108 100644 --- a/db/migrate/20130622095040_move_weekly_tasks.rb +++ b/db/migrate/20130622095040_move_weekly_tasks.rb @@ -15,21 +15,21 @@ class MoveWeeklyTasks < ActiveRecord::Migration[4.2] def down PeriodicTaskGroup.all.each do |task_group| - unless task_group.tasks.empty? - task = task_group.tasks.first - workgroup = task.workgroup - puts "Writing task data of group #{task_group.id} to workgroup #{workgroup.name}" - workgroup_attributes = { - weekly_task: true, - weekday: task.due_date.days_to_week_start(:sunday), - task_name: task.name, - task_description: task.description, - task_required_users: task.required_users, - task_duration: task.duration - } - workgroup.update(workgroup_attributes) - task_group.tasks.update_all weekly: true - end + next if task_group.tasks.empty? + + task = task_group.tasks.first + workgroup = task.workgroup + puts "Writing task data of group #{task_group.id} to workgroup #{workgroup.name}" + workgroup_attributes = { + weekly_task: true, + weekday: task.due_date.days_to_week_start(:sunday), + task_name: task.name, + task_description: task.description, + task_required_users: task.required_users, + task_duration: task.duration + } + workgroup.update(workgroup_attributes) + task_group.tasks.update_all weekly: true end end diff --git a/db/migrate/20130702113610_update_group_order_totals.rb b/db/migrate/20130702113610_update_group_order_totals.rb index da57126a..52edbad4 100644 --- a/db/migrate/20130702113610_update_group_order_totals.rb +++ b/db/migrate/20130702113610_update_group_order_totals.rb @@ -1,18 +1,17 @@ class UpdateGroupOrderTotals < ActiveRecord::Migration[4.2] def self.up say "If you have ever modified an order after it was settled, the group_order's " + - "price may be calculated incorrectly. This can take a lot of time on a " + - "large database." + 'price may be calculated incorrectly. This can take a lot of time on a ' + + 'large database.' - say "If you do want to update the ordergroup totals, open the rails console " + - "(by running `rails c`), and enter:" + say 'If you do want to update the ordergroup totals, open the rails console ' + + '(by running `rails c`), and enter:' - say "GroupOrder.all.each { |go| go.order.closed? and go.update_price! }", subitem: true + say 'GroupOrder.all.each { |go| go.order.closed? and go.update_price! }', subitem: true - say "You may want to check first that no undesired accounting issues are introduced. " + - "It may be wise to discuss this with those responsible for the ordering finances." + say 'You may want to check first that no undesired accounting issues are introduced. ' + + 'It may be wise to discuss this with those responsible for the ordering finances.' end - def self.down - end + def self.down; end end diff --git a/db/migrate/20130718183100_create_settings.rb b/db/migrate/20130718183100_create_settings.rb index 9928a9b3..90639d71 100644 --- a/db/migrate/20130718183100_create_settings.rb +++ b/db/migrate/20130718183100_create_settings.rb @@ -8,7 +8,7 @@ class CreateSettings < ActiveRecord::Migration[4.2] t.timestamps end - add_index :settings, [:thing_type, :thing_id, :var], unique: true + add_index :settings, %i[thing_type thing_id var], unique: true end def self.down diff --git a/db/migrate/20130718183101_migrate_user_settings.rb b/db/migrate/20130718183101_migrate_user_settings.rb index 04cfaeb8..2d0e3c56 100644 --- a/db/migrate/20130718183101_migrate_user_settings.rb +++ b/db/migrate/20130718183101_migrate_user_settings.rb @@ -46,8 +46,7 @@ class MigrateUserSettings < ActiveRecord::Migration[4.2] drop_table :configurable_settings end - def down - end + def down; end end # this is the base class of all configurable settings diff --git a/db/migrate/20130920201529_allow_missing_nick.rb b/db/migrate/20130920201529_allow_missing_nick.rb index ed818860..fcf1d8c8 100644 --- a/db/migrate/20130920201529_allow_missing_nick.rb +++ b/db/migrate/20130920201529_allow_missing_nick.rb @@ -1,9 +1,9 @@ class AllowMissingNick < ActiveRecord::Migration[4.2] def self.up - change_column :users, :nick, :string, :default => nil, :null => true + change_column :users, :nick, :string, default: nil, null: true end def self.down - change_column :users, :nick, :string, :default => "", :null => false + change_column :users, :nick, :string, default: '', null: false end end diff --git a/db/migrate/20140102170431_add_result_computed_to_group_order_articles.rb b/db/migrate/20140102170431_add_result_computed_to_group_order_articles.rb index dd9fc407..0bb885d9 100644 --- a/db/migrate/20140102170431_add_result_computed_to_group_order_articles.rb +++ b/db/migrate/20140102170431_add_result_computed_to_group_order_articles.rb @@ -1,6 +1,6 @@ class AddResultComputedToGroupOrderArticles < ActiveRecord::Migration[4.2] def change add_column :group_order_articles, :result_computed, - :decimal, :precision => 8, :scale => 3 + :decimal, precision: 8, scale: 3 end end diff --git a/db/migrate/20140318173000_delete_empty_group_order_articles.rb b/db/migrate/20140318173000_delete_empty_group_order_articles.rb index 1e053c3c..c5b396ed 100644 --- a/db/migrate/20140318173000_delete_empty_group_order_articles.rb +++ b/db/migrate/20140318173000_delete_empty_group_order_articles.rb @@ -4,6 +4,5 @@ class DeleteEmptyGroupOrderArticles < ActiveRecord::Migration[4.2] GroupOrderArticle.where(quantity: 0, tolerance: 0, result: [0, nil], result_computed: [0, nil]).delete_all end - def down - end + def down; end end diff --git a/db/migrate/20140921104907_remove_stale_memberships.rb b/db/migrate/20140921104907_remove_stale_memberships.rb index de5b719b..26b6c834 100644 --- a/db/migrate/20140921104907_remove_stale_memberships.rb +++ b/db/migrate/20140921104907_remove_stale_memberships.rb @@ -1,5 +1,5 @@ class RemoveStaleMemberships < ActiveRecord::Migration[4.2] def up - Membership.where("group_id NOT IN (?)", Group.ids).delete_all + Membership.where.not(group_id: Group.ids).delete_all end end diff --git a/db/migrate/20160217194036_add_role_invoices_to_group.rb b/db/migrate/20160217194036_add_role_invoices_to_group.rb index 6946fe05..5a86f425 100644 --- a/db/migrate/20160217194036_add_role_invoices_to_group.rb +++ b/db/migrate/20160217194036_add_role_invoices_to_group.rb @@ -1,5 +1,5 @@ class AddRoleInvoicesToGroup < ActiveRecord::Migration[4.2] def change - add_column :groups, :role_invoices, :boolean, :default => false, :null => false + add_column :groups, :role_invoices, :boolean, default: false, null: false end end diff --git a/db/migrate/20160218151041_add_attachment_to_invoice.rb b/db/migrate/20160218151041_add_attachment_to_invoice.rb index 58bac66d..1767905c 100644 --- a/db/migrate/20160218151041_add_attachment_to_invoice.rb +++ b/db/migrate/20160218151041_add_attachment_to_invoice.rb @@ -1,6 +1,6 @@ class AddAttachmentToInvoice < ActiveRecord::Migration[4.2] def change add_column :invoices, :attachment_mime, :string - add_column :invoices, :attachment_data, :binary, :limit => 8.megabyte + add_column :invoices, :attachment_data, :binary, limit: 8.megabyte end end diff --git a/db/migrate/20160219123220_create_financial_transaction_class_and_types.rb b/db/migrate/20160219123220_create_financial_transaction_class_and_types.rb index 5fcf318b..3c05035d 100644 --- a/db/migrate/20160219123220_create_financial_transaction_class_and_types.rb +++ b/db/migrate/20160219123220_create_financial_transaction_class_and_types.rb @@ -1,12 +1,12 @@ class CreateFinancialTransactionClassAndTypes < ActiveRecord::Migration[4.2] def change create_table :financial_transaction_classes do |t| - t.string :name, :null => false + t.string :name, null: false end create_table :financial_transaction_types do |t| - t.string :name, :null => false - t.references :financial_transaction_class, :null => false + t.string :name, null: false + t.references :financial_transaction_class, null: false end change_table :financial_transactions do |t| @@ -17,7 +17,7 @@ class CreateFinancialTransactionClassAndTypes < ActiveRecord::Migration[4.2] dir.up do execute "INSERT INTO financial_transaction_classes (id, name) VALUES (1, 'Standard')" execute "INSERT INTO financial_transaction_types (id, name, financial_transaction_class_id) VALUES (1, 'Foodsoft', 1)" - execute "UPDATE financial_transactions SET financial_transaction_type_id = 1" + execute 'UPDATE financial_transactions SET financial_transaction_type_id = 1' end end diff --git a/db/migrate/20160224201529_allow_stock_group_order.rb b/db/migrate/20160224201529_allow_stock_group_order.rb index a77879e3..6c9197f0 100644 --- a/db/migrate/20160224201529_allow_stock_group_order.rb +++ b/db/migrate/20160224201529_allow_stock_group_order.rb @@ -1,9 +1,9 @@ class AllowStockGroupOrder < ActiveRecord::Migration[4.2] def self.up - change_column :group_orders, :ordergroup_id, :integer, :default => nil, :null => true + change_column :group_orders, :ordergroup_id, :integer, default: nil, null: true end def self.down - change_column :group_orders, :ordergroup_id, :integer, :default => 0, :null => false + change_column :group_orders, :ordergroup_id, :integer, default: 0, null: false end end diff --git a/db/migrate/20160226000000_add_email_to_message.foodsoft_messages_engine.rb b/db/migrate/20160226000000_add_email_to_message.foodsoft_messages_engine.rb index 95b35273..ceeafa15 100644 --- a/db/migrate/20160226000000_add_email_to_message.foodsoft_messages_engine.rb +++ b/db/migrate/20160226000000_add_email_to_message.foodsoft_messages_engine.rb @@ -2,6 +2,6 @@ class AddEmailToMessage < ActiveRecord::Migration[4.2] def change add_column :messages, :salt, :string - add_column :messages, :received_email, :binary, :limit => 1.megabyte + add_column :messages, :received_email, :binary, limit: 1.megabyte end end diff --git a/db/migrate/20170801000000_create_mail_delivery_status.rb b/db/migrate/20170801000000_create_mail_delivery_status.rb index 2fd40674..69fa1b75 100644 --- a/db/migrate/20170801000000_create_mail_delivery_status.rb +++ b/db/migrate/20170801000000_create_mail_delivery_status.rb @@ -2,8 +2,8 @@ class CreateMailDeliveryStatus < ActiveRecord::Migration[4.2] def change create_table :mail_delivery_status do |t| t.datetime :created_at - t.string :email, :null => false - t.string :message, :null => false + t.string :email, null: false + t.string :message, null: false t.string :attachment_mime t.binary :attachment_data, limit: 16.megabyte diff --git a/db/migrate/20181110000000_create_polls.foodsoft_polls_engine.rb b/db/migrate/20181110000000_create_polls.foodsoft_polls_engine.rb index 990e75f0..120b7eef 100644 --- a/db/migrate/20181110000000_create_polls.foodsoft_polls_engine.rb +++ b/db/migrate/20181110000000_create_polls.foodsoft_polls_engine.rb @@ -24,14 +24,14 @@ class CreatePolls < ActiveRecord::Migration[4.2] t.references :ordergroup t.text :note t.timestamps - t.index [:poll_id, :user_id, :ordergroup_id], unique: true + t.index %i[poll_id user_id ordergroup_id], unique: true end create_table :poll_choices do |t| t.references :poll_vote, null: false t.integer :choice, null: false t.integer :value, null: false - t.index [:poll_vote_id, :choice], unique: true + t.index %i[poll_vote_id choice], unique: true end end end diff --git a/db/migrate/20181120000000_increase_choices_size.foodsoft_polls_engine.rb b/db/migrate/20181120000000_increase_choices_size.foodsoft_polls_engine.rb index d809e3ea..621863dd 100644 --- a/db/migrate/20181120000000_increase_choices_size.foodsoft_polls_engine.rb +++ b/db/migrate/20181120000000_increase_choices_size.foodsoft_polls_engine.rb @@ -1,5 +1,5 @@ class IncreaseChoicesSize < ActiveRecord::Migration[4.2] def up - change_column :polls, :choices, :text, limit: 65535 + change_column :polls, :choices, :text, limit: 65_535 end end diff --git a/db/migrate/20181201000000_create_printer_jobs.foodsoft_printer_engine.rb b/db/migrate/20181201000000_create_printer_jobs.foodsoft_printer_engine.rb index ee7665e4..36d175c5 100644 --- a/db/migrate/20181201000000_create_printer_jobs.foodsoft_printer_engine.rb +++ b/db/migrate/20181201000000_create_printer_jobs.foodsoft_printer_engine.rb @@ -15,6 +15,6 @@ class CreatePrinterJobs < ActiveRecord::Migration[4.2] t.text :message end - add_index :printer_job_updates, [:printer_job_id, :created_at] + add_index :printer_job_updates, %i[printer_job_id created_at] end end diff --git a/db/migrate/20181201000100_create_message_recipients.foodsoft_messages.rb b/db/migrate/20181201000100_create_message_recipients.foodsoft_messages.rb index e931f748..b1d0d51c 100644 --- a/db/migrate/20181201000100_create_message_recipients.foodsoft_messages.rb +++ b/db/migrate/20181201000100_create_message_recipients.foodsoft_messages.rb @@ -14,7 +14,7 @@ class CreateMessageRecipients < ActiveRecord::Migration[4.2] t.datetime :read_at end - add_index :message_recipients, [:user_id, :read_at] + add_index :message_recipients, %i[user_id read_at] Message.all.each do |m| recipients = YAML.load(m.recipients_ids).map do |r| diff --git a/db/migrate/20190101000000_create_active_storage_tables.active_storage.rb b/db/migrate/20190101000000_create_active_storage_tables.active_storage.rb index 3739c2e8..df49a7b0 100644 --- a/db/migrate/20190101000000_create_active_storage_tables.active_storage.rb +++ b/db/migrate/20190101000000_create_active_storage_tables.active_storage.rb @@ -20,7 +20,8 @@ class CreateActiveStorageTables < ActiveRecord::Migration[4.2][5.2] t.datetime :created_at, null: false - t.index [:record_type, :record_id, :name, :blob_id], name: "index_active_storage_attachments_uniqueness", unique: true + t.index %i[record_type record_id name blob_id], name: 'index_active_storage_attachments_uniqueness', + unique: true t.foreign_key :active_storage_blobs, column: :blob_id end end diff --git a/db/migrate/20210205090257_introduce_received_state_in_orders.rb b/db/migrate/20210205090257_introduce_received_state_in_orders.rb index ffeff588..ca7ce999 100644 --- a/db/migrate/20210205090257_introduce_received_state_in_orders.rb +++ b/db/migrate/20210205090257_introduce_received_state_in_orders.rb @@ -1,7 +1,7 @@ class IntroduceReceivedStateInOrders < ActiveRecord::Migration[5.2] def up Order.where(state: 'finished').each do |order| - order.update_attribute(:state, 'received') if order.order_articles.where('units_received IS NOT NULL').any? + order.update_attribute(:state, 'received') if order.order_articles.where.not(units_received: nil).any? end end diff --git a/db/migrate/20230106144438_add_service_name_to_active_storage_blobs.active_storage.rb b/db/migrate/20230106144438_add_service_name_to_active_storage_blobs.active_storage.rb index a15c6ce8..379003ed 100644 --- a/db/migrate/20230106144438_add_service_name_to_active_storage_blobs.active_storage.rb +++ b/db/migrate/20230106144438_add_service_name_to_active_storage_blobs.active_storage.rb @@ -3,15 +3,15 @@ class AddServiceNameToActiveStorageBlobs < ActiveRecord::Migration[6.0] def up return unless table_exists?(:active_storage_blobs) - unless column_exists?(:active_storage_blobs, :service_name) - add_column :active_storage_blobs, :service_name, :string + return if column_exists?(:active_storage_blobs, :service_name) - if configured_service = ActiveStorage::Blob.service.name - ActiveStorage::Blob.unscoped.update_all(service_name: configured_service) - end + add_column :active_storage_blobs, :service_name, :string - change_column :active_storage_blobs, :service_name, :string, null: false + if configured_service = ActiveStorage::Blob.service.name + ActiveStorage::Blob.unscoped.update_all(service_name: configured_service) end + + change_column :active_storage_blobs, :service_name, :string, null: false end def down diff --git a/db/migrate/20230106144439_create_active_storage_variant_records.active_storage.rb b/db/migrate/20230106144439_create_active_storage_variant_records.active_storage.rb index e1020fc9..58cc779a 100644 --- a/db/migrate/20230106144439_create_active_storage_variant_records.active_storage.rb +++ b/db/migrate/20230106144439_create_active_storage_variant_records.active_storage.rb @@ -8,7 +8,7 @@ class CreateActiveStorageVariantRecords < ActiveRecord::Migration[6.0] t.belongs_to :blob, null: false, index: false, type: blobs_primary_key_type t.string :variation_digest, null: false - t.index [:blob_id, :variation_digest], name: "index_active_storage_variant_records_uniqueness", unique: true + t.index %i[blob_id variation_digest], name: 'index_active_storage_variant_records_uniqueness', unique: true t.foreign_key :active_storage_blobs, column: :blob_id end end diff --git a/db/seeds/minimal.seeds.rb b/db/seeds/minimal.seeds.rb index d38ef10e..bbf97e10 100644 --- a/db/seeds/minimal.seeds.rb +++ b/db/seeds/minimal.seeds.rb @@ -2,30 +2,30 @@ # Create working group with full rights administrators = Workgroup.create!( - :name => "Administrators", - :description => "System administrators.", - :role_admin => true, - :role_finance => true, - :role_article_meta => true, - :role_pickups => true, - :role_suppliers => true, - :role_orders => true + name: 'Administrators', + description: 'System administrators.', + role_admin: true, + role_finance: true, + role_article_meta: true, + role_pickups: true, + role_suppliers: true, + role_orders: true ) # Create admin user User.create!( - :nick => "admin", - :first_name => "Anton", - :last_name => "Administrator", - :email => "admin@foo.test", - :password => "secret", - :groups => [administrators] + nick: 'admin', + first_name: 'Anton', + last_name: 'Administrator', + email: 'admin@foo.test', + password: 'secret', + groups: [administrators] ) # First entry for financial transaction types -financial_transaction_class = FinancialTransactionClass.create!(:name => "Other") -FinancialTransactionType.create!(:name => "Foodcoop", :financial_transaction_class_id => financial_transaction_class.id) +financial_transaction_class = FinancialTransactionClass.create!(name: 'Other') +FinancialTransactionType.create!(name: 'Foodcoop', financial_transaction_class_id: financial_transaction_class.id) # First entry for article categories -SupplierCategory.create!(:name => "Other", :financial_transaction_class_id => financial_transaction_class.id) -ArticleCategory.create!(:name => "Other", :description => "other, misc, unknown") +SupplierCategory.create!(name: 'Other', financial_transaction_class_id: financial_transaction_class.id) +ArticleCategory.create!(name: 'Other', description: 'other, misc, unknown') diff --git a/db/seeds/seed_helper.rb b/db/seeds/seed_helper.rb index a1f958bf..05272319 100644 --- a/db/seeds/seed_helper.rb +++ b/db/seeds/seed_helper.rb @@ -8,10 +8,10 @@ def seed_group_orders # order 3..12 times a random article go = og.group_orders.create!(order: order, updated_by_user_id: 1) - (rand(10) + 3).times do + rand(3..12).times do goa = go.group_order_articles.find_or_create_by!(order_article: order.order_articles.offset(rand(noas)).first) unit_quantity = goa.order_article.price.unit_quantity - goa.update_quantities rand([4, unit_quantity * 2 + 2].max), rand(unit_quantity) + goa.update_quantities rand([4, (unit_quantity * 2) + 2].max), rand(unit_quantity) end end # update totals diff --git a/db/seeds/small.en.seeds.rb b/db/seeds/small.en.seeds.rb index 52f0b9db..6c832e1d 100644 --- a/db/seeds/small.en.seeds.rb +++ b/db/seeds/small.en.seeds.rb @@ -1,174 +1,300 @@ -require_relative 'seed_helper.rb' +require_relative 'seed_helper' ## Financial transaction classes -FinancialTransactionClass.create!(:id => 1, :name => 'Standard') -FinancialTransactionClass.create!(:id => 2, :name => 'Foodsoft') +FinancialTransactionClass.create!(id: 1, name: 'Standard') +FinancialTransactionClass.create!(id: 2, name: 'Foodsoft') ## Suppliers & articles -SupplierCategory.create!(:id => 1, :name => "Other", :financial_transaction_class_id => 1) +SupplierCategory.create!(id: 1, name: 'Other', financial_transaction_class_id: 1) Supplier.create!([ - { :id => 1, :name => "Beautiful bakery", :supplier_category_id => 1, :address => "Smallstreet 1, Cookilage", :phone => "0123456789", :email => "info@bbakery.test", :min_order_quantity => "100" }, - { :id => 2, :name => "Chocolatiers", :supplier_category_id => 1, :address => "Multatuliroad 1, Amsterdam", :phone => "0123456789", :email => "info@chocolatiers.test", :url => "http://www.chocolatiers.test/", :contact_person => "Max Pure", :delivery_days => "Tue, Fr (Amsterdam)" }, - { :id => 3, :name => "Cheesemaker", :supplier_category_id => 1, :address => "Cheesestreet 5, London", :phone => "0123456789", :url => "http://www.cheesemaker.test/" }, - { :id => 4, :name => "The Nuthome", :supplier_category_id => 1, :address => "Alexanderplatz, Berlin", :phone => "0123456789", :email => "info@thenuthome.test", :url => "http://www.thenuthome.test/", :note => "delivery in Berlin; €9 delivery costs for orders under €123" } + { id: 1, name: 'Beautiful bakery', supplier_category_id: 1, + address: 'Smallstreet 1, Cookilage', phone: '0123456789', email: 'info@bbakery.test', min_order_quantity: '100' }, + { id: 2, name: 'Chocolatiers', supplier_category_id: 1, + address: 'Multatuliroad 1, Amsterdam', phone: '0123456789', email: 'info@chocolatiers.test', url: 'http://www.chocolatiers.test/', contact_person: 'Max Pure', delivery_days: 'Tue, Fr (Amsterdam)' }, + { id: 3, name: 'Cheesemaker', supplier_category_id: 1, + address: 'Cheesestreet 5, London', phone: '0123456789', url: 'http://www.cheesemaker.test/' }, + { id: 4, name: 'The Nuthome', supplier_category_id: 1, + address: 'Alexanderplatz, Berlin', phone: '0123456789', email: 'info@thenuthome.test', url: 'http://www.thenuthome.test/', note: 'delivery in Berlin; €9 delivery costs for orders under €123' } ]) -ArticleCategory.create!(:id => 1, :name => "Other", :description => "other, misc, unknown") -ArticleCategory.create!(:id => 2, :name => "Fruit") -ArticleCategory.create!(:id => 3, :name => "Vegetables") -ArticleCategory.create!(:id => 4, :name => "Potatoes & onions") -ArticleCategory.create!(:id => 5, :name => "Bread & Bakery") -ArticleCategory.create!(:id => 6, :name => "Drinks", :description => "juice, fruit juice, vegetable juice, soda") -ArticleCategory.create!(:id => 7, :name => "Herbs & Spices") -ArticleCategory.create!(:id => 8, :name => "Milk & products", :description => "milk, butter, cream, yoghurt, cheese, eggs, milk substitutes") -ArticleCategory.create!(:id => 9, :name => "Fish & Sea") -ArticleCategory.create!(:id => 10, :name => "Meat") -ArticleCategory.create!(:id => 11, :name => "Oils & Fats") -ArticleCategory.create!(:id => 12, :name => "Grains & Legumes") -ArticleCategory.create!(:id => 13, :name => "Nuts & Seeds") -ArticleCategory.create!(:id => 14, :name => "Sugar & Sweets") +ArticleCategory.create!(id: 1, name: 'Other', description: 'other, misc, unknown') +ArticleCategory.create!(id: 2, name: 'Fruit') +ArticleCategory.create!(id: 3, name: 'Vegetables') +ArticleCategory.create!(id: 4, name: 'Potatoes & onions') +ArticleCategory.create!(id: 5, name: 'Bread & Bakery') +ArticleCategory.create!(id: 6, name: 'Drinks', description: 'juice, fruit juice, vegetable juice, soda') +ArticleCategory.create!(id: 7, name: 'Herbs & Spices') +ArticleCategory.create!(id: 8, name: 'Milk & products', + description: 'milk, butter, cream, yoghurt, cheese, eggs, milk substitutes') +ArticleCategory.create!(id: 9, name: 'Fish & Sea') +ArticleCategory.create!(id: 10, name: 'Meat') +ArticleCategory.create!(id: 11, name: 'Oils & Fats') +ArticleCategory.create!(id: 12, name: 'Grains & Legumes') +ArticleCategory.create!(id: 13, name: 'Nuts & Seeds') +ArticleCategory.create!(id: 14, name: 'Sugar & Sweets') -Article.create!(:name => "Brown whole", :supplier_id => 1, :article_category_id => 5, :unit => "pc", :note => "organic", :availability => true, :manufacturer => "The Baker", :origin => "NL", :price => 0.22E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "Brown half", :supplier_id => 1, :article_category_id => 5, :unit => "pc", :note => "organic", :availability => true, :manufacturer => "The Baker", :origin => "NL", :price => 0.11E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "Brown sesame whole", :supplier_id => 1, :article_category_id => 5, :unit => "pc", :note => "organic", :availability => true, :manufacturer => "The Baker", :origin => "NL", :price => 0.22E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "Brown sesame half", :supplier_id => 1, :article_category_id => 5, :unit => "pc", :note => "organic", :availability => true, :manufacturer => "The Baker", :origin => "NL", :price => 0.11E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "Light wheat whole", :supplier_id => 1, :article_category_id => 5, :unit => "pc", :note => "organic", :availability => true, :manufacturer => "The Baker", :origin => "NL", :price => 0.22E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "Light wheat half", :supplier_id => 1, :article_category_id => 5, :unit => "pc", :note => "organic", :availability => true, :manufacturer => "The Baker", :origin => "NL", :price => 0.11E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "Bread with sunflower seeds whole", :supplier_id => 1, :article_category_id => 5, :unit => "pc", :note => "organic", :availability => true, :manufacturer => "The Baker", :origin => "NL", :price => 0.33E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "Bread with sunflower seeds half", :supplier_id => 1, :article_category_id => 5, :unit => "pc", :note => "organic", :availability => true, :manufacturer => "The Baker", :origin => "NL", :price => 0.11E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "Bread with walnuts whole", :supplier_id => 1, :article_category_id => 5, :unit => "pc", :note => "organic", :availability => true, :manufacturer => "The Baker", :origin => "NL", :price => 0.33E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "Bread with walnuts half", :supplier_id => 1, :article_category_id => 5, :unit => "pc", :note => "organic", :availability => true, :manufacturer => "The Baker", :origin => "NL", :price => 0.11E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "Kennemerlandbread whole", :supplier_id => 1, :article_category_id => 5, :unit => "pc", :note => "organic", :availability => true, :manufacturer => "The Baker", :origin => "NL", :price => 0.33E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "Kennemerlandbread half", :supplier_id => 1, :article_category_id => 5, :unit => "pc", :note => "organic", :availability => true, :manufacturer => "The Baker", :origin => "NL", :price => 0.11E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "Maize bread whole", :supplier_id => 1, :article_category_id => 5, :unit => "pc", :note => "organic", :availability => true, :manufacturer => "The Baker", :origin => "NL", :price => 0.33E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "Maize bread half", :supplier_id => 1, :article_category_id => 5, :unit => "pc", :note => "organic", :availability => true, :manufacturer => "The Baker", :origin => "NL", :price => 0.11E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "Oberlander 1200 gram whole", :supplier_id => 1, :article_category_id => 5, :unit => "pc", :note => "organic", :availability => true, :manufacturer => "The Baker", :origin => "NL", :price => 0.33E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "Oberlander 1200 gram half", :supplier_id => 1, :article_category_id => 5, :unit => "pc", :note => "organic", :availability => true, :manufacturer => "The Baker", :origin => "NL", :price => 0.11E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "Oberlander 900 gram whole", :supplier_id => 1, :article_category_id => 5, :unit => "pc", :note => "organic", :availability => true, :manufacturer => "The Baker", :origin => "NL", :price => 0.33E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "Oberlander 900 gram half", :supplier_id => 1, :article_category_id => 5, :unit => "pc", :note => "organic", :availability => true, :manufacturer => "The Baker", :origin => "NL", :price => 0.11E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "Speltbread whole", :supplier_id => 1, :article_category_id => 5, :unit => "pc", :note => "organic", :availability => true, :manufacturer => "The Baker", :origin => "NL", :price => 0.33E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "Speltbread half", :supplier_id => 1, :article_category_id => 5, :unit => "pc", :note => "organic", :availability => true, :manufacturer => "The Baker", :origin => "NL", :price => 0.11E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "Country bread 900gram whole", :supplier_id => 1, :article_category_id => 5, :unit => "pc", :note => "organic", :availability => true, :manufacturer => "The Baker", :origin => "NL", :price => 0.33E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "Country bread 900gram half", :supplier_id => 1, :article_category_id => 5, :unit => "pc", :note => "organic", :availability => true, :manufacturer => "The Baker", :origin => "NL", :price => 0.11E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "White whole", :supplier_id => 1, :article_category_id => 5, :unit => "pc", :note => "organic", :availability => true, :manufacturer => "The Baker", :origin => "NL", :price => 0.33E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "White half", :supplier_id => 1, :article_category_id => 5, :unit => "pc", :note => "organic", :availability => true, :manufacturer => "The Baker", :origin => "NL", :price => 0.11E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "White with poppy seeds whole", :supplier_id => 1, :article_category_id => 5, :unit => "pc", :note => "organic", :availability => true, :manufacturer => "The Baker", :origin => "NL", :price => 0.33E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "White with poppy seeds half", :supplier_id => 1, :article_category_id => 5, :unit => "pc", :note => "organic", :availability => true, :manufacturer => "The Baker", :origin => "NL", :price => 0.11E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "Fig bread whole", :supplier_id => 1, :article_category_id => 5, :unit => "pc", :note => "organic", :availability => true, :manufacturer => "The Baker", :origin => "NL", :price => 0.33E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "Fig bread half", :supplier_id => 1, :article_category_id => 5, :unit => "pc", :note => "organic", :availability => true, :manufacturer => "The Baker", :origin => "NL", :price => 0.11E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "Beer-based bread whole", :supplier_id => 1, :article_category_id => 5, :unit => "pc", :note => "organic", :availability => true, :manufacturer => "The Baker", :origin => "NL", :price => 0.33E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "Beer-based bread half", :supplier_id => 1, :article_category_id => 5, :unit => "pc", :note => "organic", :availability => true, :manufacturer => "The Baker", :origin => "NL", :price => 0.22E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "Raisin bun", :supplier_id => 1, :article_category_id => 5, :unit => "pc", :note => "organic", :availability => true, :manufacturer => "The Baker", :origin => "NL", :price => 0.99E0, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "Muesli bun", :supplier_id => 1, :article_category_id => 5, :unit => "pc", :note => "organic", :availability => true, :manufacturer => "The Baker", :origin => "NL", :price => 0.11E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "Brioche", :supplier_id => 1, :article_category_id => 5, :unit => "pc", :note => "organic", :availability => true, :manufacturer => "The Baker", :origin => "NL", :price => 0.99E0, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "Brown croissant", :supplier_id => 1, :article_category_id => 5, :unit => "pc", :note => "organic", :availability => true, :manufacturer => "The Baker", :origin => "NL", :price => 0.11E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "Croissants", :supplier_id => 1, :article_category_id => 5, :unit => "pc", :note => "organic", :availability => true, :manufacturer => "The Baker", :origin => "NL", :price => 0.11E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "Cheese croissants", :supplier_id => 1, :article_category_id => 5, :unit => "pc", :note => "organic", :availability => true, :manufacturer => "The Baker", :origin => "NL", :price => 0.11E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "Chocolatecroissants", :supplier_id => 1, :article_category_id => 5, :unit => "pc", :note => "organic", :availability => true, :manufacturer => "The Baker", :origin => "NL", :price => 0.11E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "Soepstengels white", :supplier_id => 1, :article_category_id => 5, :unit => "pc", :note => "organic", :availability => true, :manufacturer => "The Baker", :origin => "NL", :price => 0.11E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "Soepstengels volkoren", :supplier_id => 1, :article_category_id => 5, :unit => "pc", :note => "organic", :availability => true, :manufacturer => "The Baker", :origin => "NL", :price => 0.99E0, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "Pumpkin-seed buns", :supplier_id => 1, :article_category_id => 5, :unit => "pc", :note => "organic", :availability => true, :manufacturer => "The Baker", :origin => "NL", :price => 0.88E0, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "White buns", :supplier_id => 1, :article_category_id => 5, :unit => "pc", :note => "organic", :availability => true, :manufacturer => "The Baker", :origin => "NL", :price => 0.66E0, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "Brown buns", :supplier_id => 1, :article_category_id => 5, :unit => "pc", :note => "organic", :availability => true, :manufacturer => "The Baker", :origin => "NL", :price => 0.66E0, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "Tomato-feta bread", :supplier_id => 1, :article_category_id => 5, :unit => "pc", :note => "organic", :availability => true, :manufacturer => "The Baker", :origin => "NL", :price => 0.11E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "Chocolate Bar Milk (37%)", :supplier_id => 2, :article_category_id => 14, :unit => "90gr", :note => "organic", :availability => true, :manufacturer => "Chocolatemakers", :origin => "NL", :price => 0.11E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "Chocolate Bar Pure (68%)", :supplier_id => 2, :article_category_id => 14, :unit => "90gr", :note => "organic", :availability => true, :manufacturer => "Chocolatemakers", :origin => "NL", :price => 0.11E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "Chocolate Bar Milk (40%)", :supplier_id => 2, :article_category_id => 14, :unit => "90gr", :note => "organic", :availability => true, :manufacturer => "Chocolatemakers", :origin => "NL", :price => 0.22E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "Chocolate Bar Pure (75%)", :supplier_id => 2, :article_category_id => 14, :unit => "90gr", :note => "organic", :availability => true, :manufacturer => "Chocolatemakers", :origin => "NL", :price => 0.22E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "Chocolate Bar Swan Pure (75%)", :supplier_id => 2, :article_category_id => 14, :unit => "120gr", :note => "organic", :availability => true, :manufacturer => "Chocolatemakers", :origin => "NL", :price => 0.66E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "Cacao nibs", :supplier_id => 2, :article_category_id => 14, :unit => "1 kg", :note => "organic", :availability => true, :manufacturer => "Chocolatemakers", :origin => "NL", :price => 0.11E2, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "Cheese Cow-young", :supplier_id => 3, :article_category_id => 8, :unit => "kg", :note => "organic", :availability => true, :manufacturer => "Cheesefarm", :origin => "NL", :price => 0.88E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 8) -Article.create!(:name => "Cheese cow- young matured", :supplier_id => 3, :article_category_id => 8, :unit => "kg", :note => "organic", :availability => true, :manufacturer => "Cheesefarm", :origin => "NL", :price => 0.99E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 8) -Article.create!(:name => "Cheese cow- matured", :supplier_id => 3, :article_category_id => 8, :unit => "kg", :note => "organic", :availability => true, :manufacturer => "Cheesefarm", :origin => "NL", :price => 0.11E2, :tax => 6.0, :deposit => 0.0, :unit_quantity => 12) -Article.create!(:name => "Cheese cow- extra matured", :supplier_id => 3, :article_category_id => 8, :unit => "kg", :note => "organic", :availability => true, :manufacturer => "Cheesefarm", :origin => "NL", :price => 0.12E2, :tax => 6.0, :deposit => 0.0, :unit_quantity => 8) -Article.create!(:name => "cheese Cow- old", :supplier_id => 3, :article_category_id => 8, :unit => "kg", :note => "organic", :availability => true, :manufacturer => "Cheesefarm", :origin => "NL", :price => 0.11E2, :tax => 6.0, :deposit => 0.0, :unit_quantity => 8) -Article.create!(:name => "cheese cow -very old", :supplier_id => 3, :article_category_id => 8, :unit => "kg", :note => "organic", :availability => true, :manufacturer => "Cheesefarm", :origin => "NL", :price => 0.12E2, :tax => 6.0, :deposit => 0.0, :unit_quantity => 8) -Article.create!(:name => "Cheese Cow-nettle young", :supplier_id => 3, :article_category_id => 8, :unit => "kg", :note => "organic", :availability => true, :manufacturer => "Cheesefarm", :origin => "NL", :price => 0.99E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 8) -Article.create!(:name => "Cheese cow- nettle young matured", :supplier_id => 3, :article_category_id => 8, :unit => "kg", :note => "organic", :availability => true, :manufacturer => "Cheesefarm", :origin => "NL", :price => 0.1075E2, :tax => 6.0, :deposit => 0.0, :unit_quantity => 8) -Article.create!(:name => "Cheese cow- nettle matured", :supplier_id => 3, :article_category_id => 8, :unit => "kg", :note => "organic", :availability => true, :manufacturer => "Cheesefarm", :origin => "NL", :price => 0.11E2, :tax => 6.0, :deposit => 0.0, :unit_quantity => 8) -Article.create!(:name => "Cheese Cow-cumin young", :supplier_id => 3, :article_category_id => 8, :unit => "kg", :note => "organic", :availability => true, :manufacturer => "Cheesefarm", :origin => "NL", :price => 0.99E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 8) -Article.create!(:name => "Cheese cow- cumin young matured", :supplier_id => 3, :article_category_id => 8, :unit => "kg", :note => "organic", :availability => true, :manufacturer => "Cheesefarm", :origin => "NL", :price => 0.1075E2, :tax => 6.0, :deposit => 0.0, :unit_quantity => 8) -Article.create!(:name => "Cheese cow- cumin matured", :supplier_id => 3, :article_category_id => 8, :unit => "kg", :note => "organic", :availability => true, :manufacturer => "Cheesefarm", :origin => "NL", :price => 0.11E2, :tax => 6.0, :deposit => 0.0, :unit_quantity => 8) -Article.create!(:name => "Cashew nuts", :supplier_id => 4, :article_category_id => 13, :unit => "kg", :note => "organic", :availability => true, :price => 0.4444E2, :tax => 6.0, :deposit => 0.0, :unit_quantity => 22, :order_number => ":b936051") -Article.create!(:name => "Hazel white", :supplier_id => 4, :article_category_id => 13, :unit => "kg", :note => "organic", :availability => true, :price => 0.3333E2, :tax => 6.0, :deposit => 0.0, :unit_quantity => 10, :order_number => ":9e3f85b") -Article.create!(:name => "Hazel brown", :supplier_id => 4, :article_category_id => 13, :unit => "kg", :note => "organic", :availability => true, :price => 0.1111E2, :tax => 6.0, :deposit => 0.0, :unit_quantity => 10, :order_number => ":d278041") -Article.create!(:name => "Almond Brown Spanish", :supplier_id => 4, :article_category_id => 13, :unit => "kg", :note => "organic", :availability => true, :price => 0.999E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 10, :order_number => ":0b51a8d") -Article.create!(:name => "Brazil nuts (organic)", :supplier_id => 4, :article_category_id => 13, :unit => "kg", :note => "organic", :availability => true, :price => 0.6666E2, :tax => 6.0, :deposit => 0.0, :unit_quantity => 20, :order_number => ":01e59e3") -Article.create!(:name => "Organic walnut light halves", :supplier_id => 4, :article_category_id => 13, :unit => "kg", :note => "organic", :availability => true, :price => 0.333E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 10, :order_number => ":7ff8587") -Article.create!(:name => "Pinenuts", :supplier_id => 4, :article_category_id => 13, :unit => "kg", :note => "organic", :availability => true, :price => 0.888E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 25, :order_number => ":aa88d9f") -Article.create!(:name => "Pumpkin", :supplier_id => 4, :article_category_id => 13, :unit => "kg", :note => "organic", :availability => true, :price => 0.1111E2, :tax => 6.0, :deposit => 0.0, :unit_quantity => 25, :order_number => ":e63069b") -Article.create!(:name => "Sunflower seeds (organic)", :supplier_id => 4, :article_category_id => 13, :unit => "kg", :note => "organic", :availability => true, :price => 0.999E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 25, :order_number => ":0428388") -Article.create!(:name => "Amandel White Spaans", :supplier_id => 4, :article_category_id => 13, :unit => "kg", :note => "organic", :availability => true, :price => 0.66666E3, :tax => 6.0, :deposit => 0.0, :unit_quantity => 10, :order_number => ":a8f0734") -Article.create!(:name => "Cashew", :supplier_id => 4, :article_category_id => 13, :unit => "kg", :availability => true, :price => 0.6666E2, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1, :order_number => ":1d26958") -Article.create!(:name => "Almonds blanched", :supplier_id => 4, :article_category_id => 13, :unit => "kg", :availability => true, :price => 0.333E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1, :order_number => ":31439e2") -Article.create!(:name => "Almonds natural", :supplier_id => 4, :article_category_id => 13, :unit => "kg", :availability => true, :price => 0.1111E2, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1, :order_number => ":9c49374") -Article.create!(:name => "Walnut ELH halves", :supplier_id => 4, :article_category_id => 13, :unit => "kg", :availability => true, :price => 0.4444E2, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1, :order_number => ":92907d1") -Article.create!(:name => "Walnut ELP parts", :supplier_id => 4, :article_category_id => 13, :unit => "kg", :availability => true, :price => 0.8888E2, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1, :order_number => ":395640e") -Article.create!(:name => "Brazil nuts", :supplier_id => 4, :article_category_id => 13, :unit => "kg", :availability => true, :price => 0.8888E2, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1, :order_number => ":710acbb") -Article.create!(:name => "Macadamia type 0", :supplier_id => 4, :article_category_id => 13, :unit => "kg", :availability => true, :price => 0.3333E2, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1, :order_number => ":bbaf40b") -Article.create!(:name => "Pecan", :supplier_id => 4, :article_category_id => 13, :unit => "kg", :availability => true, :price => 0.55555E3, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1, :order_number => ":7958183") -Article.create!(:name => "Hazelnuts natural", :supplier_id => 4, :article_category_id => 13, :unit => "kg", :availability => true, :price => 0.6666E2, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1, :order_number => ":50392a8") -Article.create!(:name => "Hazelnuts blanched", :supplier_id => 4, :article_category_id => 13, :unit => "kg", :availability => true, :price => 0.3333E2, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1, :order_number => ":4fe6525") -Article.create!(:name => "Mixed Nuts", :supplier_id => 4, :article_category_id => 13, :unit => "kg", :availability => true, :price => 0.333E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1, :order_number => ":c051b22") -Article.create!(:name => "Peanuts", :supplier_id => 4, :article_category_id => 13, :unit => "kg", :availability => true, :price => 0.777E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1, :order_number => ":f507577") -Article.create!(:name => "Small peanuts", :supplier_id => 4, :article_category_id => 13, :unit => "kg", :availability => true, :price => 0.8888E2, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1, :order_number => ":ce563bb") -Article.create!(:name => "Medjoul dates", :supplier_id => 4, :article_category_id => 13, :unit => "kg", :availability => true, :price => 0.3333E2, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1, :order_number => ":8232061") -Article.create!(:name => "Turkish apricots natural", :supplier_id => 4, :article_category_id => 13, :unit => "kg", :availability => true, :price => 0.888E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1, :order_number => ":185084f") -Article.create!(:name => "Turkish apricots sulfurised", :supplier_id => 4, :article_category_id => 13, :unit => "kg", :availability => true, :price => 0.1111E2, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1, :order_number => ":2b2fb20") -Article.create!(:name => "Spanish Figs", :supplier_id => 4, :article_category_id => 13, :unit => "kg", :availability => true, :price => 0.444E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1, :order_number => ":82590b1") -Article.create!(:name => "Turkish Figs", :supplier_id => 4, :article_category_id => 13, :unit => "kg", :availability => true, :price => 0.555E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1, :order_number => ":cabeeb6") -Article.create!(:name => "Sour Apricots South-Africa", :supplier_id => 4, :article_category_id => 13, :unit => "kg", :availability => true, :price => 0.1111E2, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1, :order_number => ":2ac18b7") -Article.create!(:name => "Blue raisins Flames", :supplier_id => 4, :article_category_id => 13, :unit => "kg", :availability => true, :price => 0.1111E2, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1, :order_number => ":16bfa75") -Article.create!(:name => "Yellow Raisins", :supplier_id => 4, :article_category_id => 13, :unit => "kg", :availability => true, :price => 0.2222E2, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1, :order_number => ":1c59324") -Article.create!(:name => "Red Raisins", :supplier_id => 4, :article_category_id => 13, :unit => "kg", :availability => true, :price => 0.1111E2, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1, :order_number => ":c3fcd84") -Article.create!(:name => "Cranberries whole", :supplier_id => 4, :article_category_id => 13, :unit => "kg", :availability => true, :price => 0.222E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1, :order_number => ":921c168") -Article.create!(:name => "Dried apples", :supplier_id => 4, :article_category_id => 13, :unit => "kg", :availability => true, :price => 0.555E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1, :order_number => ":902c67b") -Article.create!(:name => "Dried plums without core", :supplier_id => 4, :article_category_id => 13, :unit => "kg", :availability => true, :price => 0.222E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1, :order_number => ":a847f91") -Article.create!(:name => "Pumpkin seeds", :supplier_id => 4, :article_category_id => 13, :unit => "kg", :availability => true, :price => 0.111E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1, :order_number => ":535645f") -Article.create!(:name => "Sunflower seeds", :supplier_id => 4, :article_category_id => 13, :unit => "kg", :availability => true, :price => 0.666E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1, :order_number => ":4ab9a83") -Article.create!(:name => "Linseed", :supplier_id => 4, :article_category_id => 13, :unit => "kg", :availability => true, :price => 0.55E0, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1, :order_number => ":04be223") -Article.create!(:name => "Poppy seeds", :supplier_id => 4, :article_category_id => 13, :unit => "kg", :availability => true, :price => 0.7777E2, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1, :order_number => ":ec5b2b9") -Article.create!(:name => "Pine nuts medium china", :supplier_id => 4, :article_category_id => 13, :unit => "kg", :availability => true, :price => 0.2222E2, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1, :order_number => ":0e5b0b8") -Article.create!(:name => "Goji berries", :supplier_id => 4, :article_category_id => 13, :unit => "kg", :availability => true, :price => 0.888E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1, :order_number => ":d52ee00") -Article.create!(:name => "Mulberries", :supplier_id => 4, :article_category_id => 13, :unit => "kg", :availability => true, :price => 0.5555E2, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1, :order_number => ":5f46bd5") -Article.create!(:name => "Peeled Hemp", :supplier_id => 4, :article_category_id => 13, :unit => "kg", :availability => true, :price => 0.5555E2, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1, :order_number => ":c39165b") -Article.create!(:name => "Incaberries", :supplier_id => 4, :article_category_id => 13, :unit => "kg", :availability => true, :price => 0.888E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1, :order_number => ":8d44fe7") -Article.create!(:name => "Blueberries", :supplier_id => 4, :article_category_id => 13, :unit => "kg", :availability => true, :price => 0.2222E2, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1, :order_number => ":9a95422") -Article.create!(:name => "Chia seeds", :supplier_id => 4, :article_category_id => 13, :unit => "kg", :availability => true, :price => 0.55555E3, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1, :order_number => ":416d57b") -Article.create!(:name => "Coconut grated", :supplier_id => 4, :article_category_id => 13, :unit => "kg", :availability => true, :price => 0.55E0, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1, :order_number => ":b3f65e4") +Article.create!(name: 'Brown whole', supplier_id: 1, article_category_id: 5, unit: 'pc', + note: 'organic', availability: true, manufacturer: 'The Baker', origin: 'NL', price: 0.22E1, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'Brown half', supplier_id: 1, article_category_id: 5, unit: 'pc', note: 'organic', + availability: true, manufacturer: 'The Baker', origin: 'NL', price: 0.11E1, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'Brown sesame whole', supplier_id: 1, article_category_id: 5, unit: 'pc', + note: 'organic', availability: true, manufacturer: 'The Baker', origin: 'NL', price: 0.22E1, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'Brown sesame half', supplier_id: 1, article_category_id: 5, unit: 'pc', + note: 'organic', availability: true, manufacturer: 'The Baker', origin: 'NL', price: 0.11E1, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'Light wheat whole', supplier_id: 1, article_category_id: 5, unit: 'pc', + note: 'organic', availability: true, manufacturer: 'The Baker', origin: 'NL', price: 0.22E1, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'Light wheat half', supplier_id: 1, article_category_id: 5, unit: 'pc', + note: 'organic', availability: true, manufacturer: 'The Baker', origin: 'NL', price: 0.11E1, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'Bread with sunflower seeds whole', supplier_id: 1, article_category_id: 5, + unit: 'pc', note: 'organic', availability: true, manufacturer: 'The Baker', origin: 'NL', price: 0.33E1, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'Bread with sunflower seeds half', supplier_id: 1, article_category_id: 5, + unit: 'pc', note: 'organic', availability: true, manufacturer: 'The Baker', origin: 'NL', price: 0.11E1, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'Bread with walnuts whole', supplier_id: 1, article_category_id: 5, unit: 'pc', + note: 'organic', availability: true, manufacturer: 'The Baker', origin: 'NL', price: 0.33E1, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'Bread with walnuts half', supplier_id: 1, article_category_id: 5, unit: 'pc', + note: 'organic', availability: true, manufacturer: 'The Baker', origin: 'NL', price: 0.11E1, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'Kennemerlandbread whole', supplier_id: 1, article_category_id: 5, unit: 'pc', + note: 'organic', availability: true, manufacturer: 'The Baker', origin: 'NL', price: 0.33E1, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'Kennemerlandbread half', supplier_id: 1, article_category_id: 5, unit: 'pc', + note: 'organic', availability: true, manufacturer: 'The Baker', origin: 'NL', price: 0.11E1, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'Maize bread whole', supplier_id: 1, article_category_id: 5, unit: 'pc', + note: 'organic', availability: true, manufacturer: 'The Baker', origin: 'NL', price: 0.33E1, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'Maize bread half', supplier_id: 1, article_category_id: 5, unit: 'pc', + note: 'organic', availability: true, manufacturer: 'The Baker', origin: 'NL', price: 0.11E1, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'Oberlander 1200 gram whole', supplier_id: 1, article_category_id: 5, unit: 'pc', + note: 'organic', availability: true, manufacturer: 'The Baker', origin: 'NL', price: 0.33E1, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'Oberlander 1200 gram half', supplier_id: 1, article_category_id: 5, unit: 'pc', + note: 'organic', availability: true, manufacturer: 'The Baker', origin: 'NL', price: 0.11E1, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'Oberlander 900 gram whole', supplier_id: 1, article_category_id: 5, unit: 'pc', + note: 'organic', availability: true, manufacturer: 'The Baker', origin: 'NL', price: 0.33E1, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'Oberlander 900 gram half', supplier_id: 1, article_category_id: 5, unit: 'pc', + note: 'organic', availability: true, manufacturer: 'The Baker', origin: 'NL', price: 0.11E1, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'Speltbread whole', supplier_id: 1, article_category_id: 5, unit: 'pc', + note: 'organic', availability: true, manufacturer: 'The Baker', origin: 'NL', price: 0.33E1, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'Speltbread half', supplier_id: 1, article_category_id: 5, unit: 'pc', + note: 'organic', availability: true, manufacturer: 'The Baker', origin: 'NL', price: 0.11E1, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'Country bread 900gram whole', supplier_id: 1, article_category_id: 5, unit: 'pc', + note: 'organic', availability: true, manufacturer: 'The Baker', origin: 'NL', price: 0.33E1, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'Country bread 900gram half', supplier_id: 1, article_category_id: 5, unit: 'pc', + note: 'organic', availability: true, manufacturer: 'The Baker', origin: 'NL', price: 0.11E1, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'White whole', supplier_id: 1, article_category_id: 5, unit: 'pc', + note: 'organic', availability: true, manufacturer: 'The Baker', origin: 'NL', price: 0.33E1, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'White half', supplier_id: 1, article_category_id: 5, unit: 'pc', + note: 'organic', availability: true, manufacturer: 'The Baker', origin: 'NL', price: 0.11E1, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'White with poppy seeds whole', supplier_id: 1, article_category_id: 5, unit: 'pc', + note: 'organic', availability: true, manufacturer: 'The Baker', origin: 'NL', price: 0.33E1, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'White with poppy seeds half', supplier_id: 1, article_category_id: 5, unit: 'pc', + note: 'organic', availability: true, manufacturer: 'The Baker', origin: 'NL', price: 0.11E1, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'Fig bread whole', supplier_id: 1, article_category_id: 5, unit: 'pc', + note: 'organic', availability: true, manufacturer: 'The Baker', origin: 'NL', price: 0.33E1, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'Fig bread half', supplier_id: 1, article_category_id: 5, unit: 'pc', + note: 'organic', availability: true, manufacturer: 'The Baker', origin: 'NL', price: 0.11E1, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'Beer-based bread whole', supplier_id: 1, article_category_id: 5, unit: 'pc', + note: 'organic', availability: true, manufacturer: 'The Baker', origin: 'NL', price: 0.33E1, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'Beer-based bread half', supplier_id: 1, article_category_id: 5, unit: 'pc', + note: 'organic', availability: true, manufacturer: 'The Baker', origin: 'NL', price: 0.22E1, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'Raisin bun', supplier_id: 1, article_category_id: 5, unit: 'pc', note: 'organic', + availability: true, manufacturer: 'The Baker', origin: 'NL', price: 0.99E0, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'Muesli bun', supplier_id: 1, article_category_id: 5, unit: 'pc', note: 'organic', + availability: true, manufacturer: 'The Baker', origin: 'NL', price: 0.11E1, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'Brioche', supplier_id: 1, article_category_id: 5, unit: 'pc', note: 'organic', + availability: true, manufacturer: 'The Baker', origin: 'NL', price: 0.99E0, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'Brown croissant', supplier_id: 1, article_category_id: 5, unit: 'pc', + note: 'organic', availability: true, manufacturer: 'The Baker', origin: 'NL', price: 0.11E1, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'Croissants', supplier_id: 1, article_category_id: 5, unit: 'pc', note: 'organic', + availability: true, manufacturer: 'The Baker', origin: 'NL', price: 0.11E1, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'Cheese croissants', supplier_id: 1, article_category_id: 5, unit: 'pc', + note: 'organic', availability: true, manufacturer: 'The Baker', origin: 'NL', price: 0.11E1, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'Chocolatecroissants', supplier_id: 1, article_category_id: 5, unit: 'pc', + note: 'organic', availability: true, manufacturer: 'The Baker', origin: 'NL', price: 0.11E1, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'Soepstengels white', supplier_id: 1, article_category_id: 5, unit: 'pc', + note: 'organic', availability: true, manufacturer: 'The Baker', origin: 'NL', price: 0.11E1, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'Soepstengels volkoren', supplier_id: 1, article_category_id: 5, unit: 'pc', + note: 'organic', availability: true, manufacturer: 'The Baker', origin: 'NL', price: 0.99E0, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'Pumpkin-seed buns', supplier_id: 1, article_category_id: 5, unit: 'pc', + note: 'organic', availability: true, manufacturer: 'The Baker', origin: 'NL', price: 0.88E0, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'White buns', supplier_id: 1, article_category_id: 5, unit: 'pc', note: 'organic', + availability: true, manufacturer: 'The Baker', origin: 'NL', price: 0.66E0, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'Brown buns', supplier_id: 1, article_category_id: 5, unit: 'pc', note: 'organic', + availability: true, manufacturer: 'The Baker', origin: 'NL', price: 0.66E0, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'Tomato-feta bread', supplier_id: 1, article_category_id: 5, unit: 'pc', + note: 'organic', availability: true, manufacturer: 'The Baker', origin: 'NL', price: 0.11E1, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'Chocolate Bar Milk (37%)', supplier_id: 2, article_category_id: 14, unit: '90gr', + note: 'organic', availability: true, manufacturer: 'Chocolatemakers', origin: 'NL', price: 0.11E1, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'Chocolate Bar Pure (68%)', supplier_id: 2, article_category_id: 14, unit: '90gr', + note: 'organic', availability: true, manufacturer: 'Chocolatemakers', origin: 'NL', price: 0.11E1, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'Chocolate Bar Milk (40%)', supplier_id: 2, article_category_id: 14, unit: '90gr', + note: 'organic', availability: true, manufacturer: 'Chocolatemakers', origin: 'NL', price: 0.22E1, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'Chocolate Bar Pure (75%)', supplier_id: 2, article_category_id: 14, unit: '90gr', + note: 'organic', availability: true, manufacturer: 'Chocolatemakers', origin: 'NL', price: 0.22E1, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'Chocolate Bar Swan Pure (75%)', supplier_id: 2, article_category_id: 14, + unit: '120gr', note: 'organic', availability: true, manufacturer: 'Chocolatemakers', origin: 'NL', price: 0.66E1, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'Cacao nibs', supplier_id: 2, article_category_id: 14, unit: '1 kg', + note: 'organic', availability: true, manufacturer: 'Chocolatemakers', origin: 'NL', price: 0.11E2, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'Cheese Cow-young', supplier_id: 3, article_category_id: 8, unit: 'kg', + note: 'organic', availability: true, manufacturer: 'Cheesefarm', origin: 'NL', price: 0.88E1, tax: 6.0, deposit: 0.0, unit_quantity: 8) +Article.create!(name: 'Cheese cow- young matured', supplier_id: 3, article_category_id: 8, unit: 'kg', + note: 'organic', availability: true, manufacturer: 'Cheesefarm', origin: 'NL', price: 0.99E1, tax: 6.0, deposit: 0.0, unit_quantity: 8) +Article.create!(name: 'Cheese cow- matured', supplier_id: 3, article_category_id: 8, unit: 'kg', + note: 'organic', availability: true, manufacturer: 'Cheesefarm', origin: 'NL', price: 0.11E2, tax: 6.0, deposit: 0.0, unit_quantity: 12) +Article.create!(name: 'Cheese cow- extra matured', supplier_id: 3, article_category_id: 8, unit: 'kg', + note: 'organic', availability: true, manufacturer: 'Cheesefarm', origin: 'NL', price: 0.12E2, tax: 6.0, deposit: 0.0, unit_quantity: 8) +Article.create!(name: 'cheese Cow- old', supplier_id: 3, article_category_id: 8, unit: 'kg', + note: 'organic', availability: true, manufacturer: 'Cheesefarm', origin: 'NL', price: 0.11E2, tax: 6.0, deposit: 0.0, unit_quantity: 8) +Article.create!(name: 'cheese cow -very old', supplier_id: 3, article_category_id: 8, unit: 'kg', + note: 'organic', availability: true, manufacturer: 'Cheesefarm', origin: 'NL', price: 0.12E2, tax: 6.0, deposit: 0.0, unit_quantity: 8) +Article.create!(name: 'Cheese Cow-nettle young', supplier_id: 3, article_category_id: 8, unit: 'kg', + note: 'organic', availability: true, manufacturer: 'Cheesefarm', origin: 'NL', price: 0.99E1, tax: 6.0, deposit: 0.0, unit_quantity: 8) +Article.create!(name: 'Cheese cow- nettle young matured', supplier_id: 3, article_category_id: 8, + unit: 'kg', note: 'organic', availability: true, manufacturer: 'Cheesefarm', origin: 'NL', price: 0.1075E2, tax: 6.0, deposit: 0.0, unit_quantity: 8) +Article.create!(name: 'Cheese cow- nettle matured', supplier_id: 3, article_category_id: 8, unit: 'kg', + note: 'organic', availability: true, manufacturer: 'Cheesefarm', origin: 'NL', price: 0.11E2, tax: 6.0, deposit: 0.0, unit_quantity: 8) +Article.create!(name: 'Cheese Cow-cumin young', supplier_id: 3, article_category_id: 8, unit: 'kg', + note: 'organic', availability: true, manufacturer: 'Cheesefarm', origin: 'NL', price: 0.99E1, tax: 6.0, deposit: 0.0, unit_quantity: 8) +Article.create!(name: 'Cheese cow- cumin young matured', supplier_id: 3, article_category_id: 8, + unit: 'kg', note: 'organic', availability: true, manufacturer: 'Cheesefarm', origin: 'NL', price: 0.1075E2, tax: 6.0, deposit: 0.0, unit_quantity: 8) +Article.create!(name: 'Cheese cow- cumin matured', supplier_id: 3, article_category_id: 8, unit: 'kg', + note: 'organic', availability: true, manufacturer: 'Cheesefarm', origin: 'NL', price: 0.11E2, tax: 6.0, deposit: 0.0, unit_quantity: 8) +Article.create!(name: 'Cashew nuts', supplier_id: 4, article_category_id: 13, unit: 'kg', + note: 'organic', availability: true, price: 0.4444E2, tax: 6.0, deposit: 0.0, unit_quantity: 22, order_number: ':b936051') +Article.create!(name: 'Hazel white', supplier_id: 4, article_category_id: 13, unit: 'kg', + note: 'organic', availability: true, price: 0.3333E2, tax: 6.0, deposit: 0.0, unit_quantity: 10, order_number: ':9e3f85b') +Article.create!(name: 'Hazel brown', supplier_id: 4, article_category_id: 13, unit: 'kg', + note: 'organic', availability: true, price: 0.1111E2, tax: 6.0, deposit: 0.0, unit_quantity: 10, order_number: ':d278041') +Article.create!(name: 'Almond Brown Spanish', supplier_id: 4, article_category_id: 13, unit: 'kg', + note: 'organic', availability: true, price: 0.999E1, tax: 6.0, deposit: 0.0, unit_quantity: 10, order_number: ':0b51a8d') +Article.create!(name: 'Brazil nuts (organic)', supplier_id: 4, article_category_id: 13, unit: 'kg', + note: 'organic', availability: true, price: 0.6666E2, tax: 6.0, deposit: 0.0, unit_quantity: 20, order_number: ':01e59e3') +Article.create!(name: 'Organic walnut light halves', supplier_id: 4, article_category_id: 13, unit: 'kg', + note: 'organic', availability: true, price: 0.333E1, tax: 6.0, deposit: 0.0, unit_quantity: 10, order_number: ':7ff8587') +Article.create!(name: 'Pinenuts', supplier_id: 4, article_category_id: 13, unit: 'kg', note: 'organic', + availability: true, price: 0.888E1, tax: 6.0, deposit: 0.0, unit_quantity: 25, order_number: ':aa88d9f') +Article.create!(name: 'Pumpkin', supplier_id: 4, article_category_id: 13, unit: 'kg', note: 'organic', + availability: true, price: 0.1111E2, tax: 6.0, deposit: 0.0, unit_quantity: 25, order_number: ':e63069b') +Article.create!(name: 'Sunflower seeds (organic)', supplier_id: 4, article_category_id: 13, unit: 'kg', + note: 'organic', availability: true, price: 0.999E1, tax: 6.0, deposit: 0.0, unit_quantity: 25, order_number: ':0428388') +Article.create!(name: 'Amandel White Spaans', supplier_id: 4, article_category_id: 13, unit: 'kg', + note: 'organic', availability: true, price: 0.66666E3, tax: 6.0, deposit: 0.0, unit_quantity: 10, order_number: ':a8f0734') +Article.create!(name: 'Cashew', supplier_id: 4, article_category_id: 13, unit: 'kg', availability: true, + price: 0.6666E2, tax: 6.0, deposit: 0.0, unit_quantity: 1, order_number: ':1d26958') +Article.create!(name: 'Almonds blanched', supplier_id: 4, article_category_id: 13, unit: 'kg', + availability: true, price: 0.333E1, tax: 6.0, deposit: 0.0, unit_quantity: 1, order_number: ':31439e2') +Article.create!(name: 'Almonds natural', supplier_id: 4, article_category_id: 13, unit: 'kg', + availability: true, price: 0.1111E2, tax: 6.0, deposit: 0.0, unit_quantity: 1, order_number: ':9c49374') +Article.create!(name: 'Walnut ELH halves', supplier_id: 4, article_category_id: 13, unit: 'kg', + availability: true, price: 0.4444E2, tax: 6.0, deposit: 0.0, unit_quantity: 1, order_number: ':92907d1') +Article.create!(name: 'Walnut ELP parts', supplier_id: 4, article_category_id: 13, unit: 'kg', + availability: true, price: 0.8888E2, tax: 6.0, deposit: 0.0, unit_quantity: 1, order_number: ':395640e') +Article.create!(name: 'Brazil nuts', supplier_id: 4, article_category_id: 13, unit: 'kg', + availability: true, price: 0.8888E2, tax: 6.0, deposit: 0.0, unit_quantity: 1, order_number: ':710acbb') +Article.create!(name: 'Macadamia type 0', supplier_id: 4, article_category_id: 13, unit: 'kg', + availability: true, price: 0.3333E2, tax: 6.0, deposit: 0.0, unit_quantity: 1, order_number: ':bbaf40b') +Article.create!(name: 'Pecan', supplier_id: 4, article_category_id: 13, unit: 'kg', availability: true, + price: 0.55555E3, tax: 6.0, deposit: 0.0, unit_quantity: 1, order_number: ':7958183') +Article.create!(name: 'Hazelnuts natural', supplier_id: 4, article_category_id: 13, unit: 'kg', + availability: true, price: 0.6666E2, tax: 6.0, deposit: 0.0, unit_quantity: 1, order_number: ':50392a8') +Article.create!(name: 'Hazelnuts blanched', supplier_id: 4, article_category_id: 13, unit: 'kg', + availability: true, price: 0.3333E2, tax: 6.0, deposit: 0.0, unit_quantity: 1, order_number: ':4fe6525') +Article.create!(name: 'Mixed Nuts', supplier_id: 4, article_category_id: 13, unit: 'kg', + availability: true, price: 0.333E1, tax: 6.0, deposit: 0.0, unit_quantity: 1, order_number: ':c051b22') +Article.create!(name: 'Peanuts', supplier_id: 4, article_category_id: 13, unit: 'kg', + availability: true, price: 0.777E1, tax: 6.0, deposit: 0.0, unit_quantity: 1, order_number: ':f507577') +Article.create!(name: 'Small peanuts', supplier_id: 4, article_category_id: 13, unit: 'kg', + availability: true, price: 0.8888E2, tax: 6.0, deposit: 0.0, unit_quantity: 1, order_number: ':ce563bb') +Article.create!(name: 'Medjoul dates', supplier_id: 4, article_category_id: 13, unit: 'kg', + availability: true, price: 0.3333E2, tax: 6.0, deposit: 0.0, unit_quantity: 1, order_number: ':8232061') +Article.create!(name: 'Turkish apricots natural', supplier_id: 4, article_category_id: 13, unit: 'kg', + availability: true, price: 0.888E1, tax: 6.0, deposit: 0.0, unit_quantity: 1, order_number: ':185084f') +Article.create!(name: 'Turkish apricots sulfurised', supplier_id: 4, article_category_id: 13, unit: 'kg', + availability: true, price: 0.1111E2, tax: 6.0, deposit: 0.0, unit_quantity: 1, order_number: ':2b2fb20') +Article.create!(name: 'Spanish Figs', supplier_id: 4, article_category_id: 13, unit: 'kg', + availability: true, price: 0.444E1, tax: 6.0, deposit: 0.0, unit_quantity: 1, order_number: ':82590b1') +Article.create!(name: 'Turkish Figs', supplier_id: 4, article_category_id: 13, unit: 'kg', + availability: true, price: 0.555E1, tax: 6.0, deposit: 0.0, unit_quantity: 1, order_number: ':cabeeb6') +Article.create!(name: 'Sour Apricots South-Africa', supplier_id: 4, article_category_id: 13, unit: 'kg', + availability: true, price: 0.1111E2, tax: 6.0, deposit: 0.0, unit_quantity: 1, order_number: ':2ac18b7') +Article.create!(name: 'Blue raisins Flames', supplier_id: 4, article_category_id: 13, unit: 'kg', + availability: true, price: 0.1111E2, tax: 6.0, deposit: 0.0, unit_quantity: 1, order_number: ':16bfa75') +Article.create!(name: 'Yellow Raisins', supplier_id: 4, article_category_id: 13, unit: 'kg', + availability: true, price: 0.2222E2, tax: 6.0, deposit: 0.0, unit_quantity: 1, order_number: ':1c59324') +Article.create!(name: 'Red Raisins', supplier_id: 4, article_category_id: 13, unit: 'kg', + availability: true, price: 0.1111E2, tax: 6.0, deposit: 0.0, unit_quantity: 1, order_number: ':c3fcd84') +Article.create!(name: 'Cranberries whole', supplier_id: 4, article_category_id: 13, unit: 'kg', + availability: true, price: 0.222E1, tax: 6.0, deposit: 0.0, unit_quantity: 1, order_number: ':921c168') +Article.create!(name: 'Dried apples', supplier_id: 4, article_category_id: 13, unit: 'kg', + availability: true, price: 0.555E1, tax: 6.0, deposit: 0.0, unit_quantity: 1, order_number: ':902c67b') +Article.create!(name: 'Dried plums without core', supplier_id: 4, article_category_id: 13, unit: 'kg', + availability: true, price: 0.222E1, tax: 6.0, deposit: 0.0, unit_quantity: 1, order_number: ':a847f91') +Article.create!(name: 'Pumpkin seeds', supplier_id: 4, article_category_id: 13, unit: 'kg', + availability: true, price: 0.111E1, tax: 6.0, deposit: 0.0, unit_quantity: 1, order_number: ':535645f') +Article.create!(name: 'Sunflower seeds', supplier_id: 4, article_category_id: 13, unit: 'kg', + availability: true, price: 0.666E1, tax: 6.0, deposit: 0.0, unit_quantity: 1, order_number: ':4ab9a83') +Article.create!(name: 'Linseed', supplier_id: 4, article_category_id: 13, unit: 'kg', + availability: true, price: 0.55E0, tax: 6.0, deposit: 0.0, unit_quantity: 1, order_number: ':04be223') +Article.create!(name: 'Poppy seeds', supplier_id: 4, article_category_id: 13, unit: 'kg', + availability: true, price: 0.7777E2, tax: 6.0, deposit: 0.0, unit_quantity: 1, order_number: ':ec5b2b9') +Article.create!(name: 'Pine nuts medium china', supplier_id: 4, article_category_id: 13, unit: 'kg', + availability: true, price: 0.2222E2, tax: 6.0, deposit: 0.0, unit_quantity: 1, order_number: ':0e5b0b8') +Article.create!(name: 'Goji berries', supplier_id: 4, article_category_id: 13, unit: 'kg', + availability: true, price: 0.888E1, tax: 6.0, deposit: 0.0, unit_quantity: 1, order_number: ':d52ee00') +Article.create!(name: 'Mulberries', supplier_id: 4, article_category_id: 13, unit: 'kg', + availability: true, price: 0.5555E2, tax: 6.0, deposit: 0.0, unit_quantity: 1, order_number: ':5f46bd5') +Article.create!(name: 'Peeled Hemp', supplier_id: 4, article_category_id: 13, unit: 'kg', + availability: true, price: 0.5555E2, tax: 6.0, deposit: 0.0, unit_quantity: 1, order_number: ':c39165b') +Article.create!(name: 'Incaberries', supplier_id: 4, article_category_id: 13, unit: 'kg', + availability: true, price: 0.888E1, tax: 6.0, deposit: 0.0, unit_quantity: 1, order_number: ':8d44fe7') +Article.create!(name: 'Blueberries', supplier_id: 4, article_category_id: 13, unit: 'kg', + availability: true, price: 0.2222E2, tax: 6.0, deposit: 0.0, unit_quantity: 1, order_number: ':9a95422') +Article.create!(name: 'Chia seeds', supplier_id: 4, article_category_id: 13, unit: 'kg', + availability: true, price: 0.55555E3, tax: 6.0, deposit: 0.0, unit_quantity: 1, order_number: ':416d57b') +Article.create!(name: 'Coconut grated', supplier_id: 4, article_category_id: 13, unit: 'kg', + availability: true, price: 0.55E0, tax: 6.0, deposit: 0.0, unit_quantity: 1, order_number: ':b3f65e4') ## Members & groups -User.create!(:id => 1, :nick => "admin", :password => "secret", :first_name => "Anton", :last_name => "Administrator", :email => "admin@foo.test", :phone => "+4421486548", :created_on => 'Wed, 15 Jan 2014 16:15:33 UTC +00:00') -User.create!(:id => 2, :nick => "john", :password => "secret", :first_name => "John", :last_name => "Doe", :email => "john@doe.test", :created_on => 'Sun, 19 Jan 2014 17:38:22 UTC +00:00') -User.create!(:id => 3, :nick => "peter", :password => "secret", :first_name => "Peter", :last_name => "Peters", :email => "peter@peters.test", :phone => "+4711235486811", :created_on => 'Sat, 25 Jan 2014 20:20:36 UTC +00:00') -User.create!(:id => 4, :nick => "jan", :password => "secret", :first_name => "Jan", :last_name => "Lou", :email => "jan@lou.test", :created_on => 'Mon, 27 Jan 2014 16:22:14 UTC +00:00') -User.create!(:id => 5, :nick => "mary", :password => "secret", :first_name => "Mary", :last_name => "Lou", :email => "marie@lou.test", :created_on => 'Mon, 03 Feb 2014 11:47:17 UTC +00:00') -User.find_by_nick("mary").update(last_activity: 5.days.ago) +User.create!(id: 1, nick: 'admin', password: 'secret', first_name: 'Anton', last_name: 'Administrator', + email: 'admin@foo.test', phone: '+4421486548', created_on: 'Wed, 15 Jan 2014 16:15:33 UTC +00:00') +User.create!(id: 2, nick: 'john', password: 'secret', first_name: 'John', last_name: 'Doe', + email: 'john@doe.test', created_on: 'Sun, 19 Jan 2014 17:38:22 UTC +00:00') +User.create!(id: 3, nick: 'peter', password: 'secret', first_name: 'Peter', last_name: 'Peters', + email: 'peter@peters.test', phone: '+4711235486811', created_on: 'Sat, 25 Jan 2014 20:20:36 UTC +00:00') +User.create!(id: 4, nick: 'jan', password: 'secret', first_name: 'Jan', last_name: 'Lou', + email: 'jan@lou.test', created_on: 'Mon, 27 Jan 2014 16:22:14 UTC +00:00') +User.create!(id: 5, nick: 'mary', password: 'secret', first_name: 'Mary', last_name: 'Lou', + email: 'marie@lou.test', created_on: 'Mon, 03 Feb 2014 11:47:17 UTC +00:00') +User.find_by_nick('mary').update(last_activity: 5.days.ago) -Workgroup.create!(:id => 1, :name => "Administrators", :description => "System administrators.", :account_balance => 0.0, :created_on => 'Wed, 15 Jan 2014 16:15:33 UTC +00:00', :role_admin => true, :role_suppliers => true, :role_article_meta => true, :role_finance => true, :role_orders => true, :next_weekly_tasks_number => 8, :ignore_apple_restriction => false) -Workgroup.create!(:id => 2, :name => "Finances", :account_balance => 0.0, :created_on => 'Sun, 19 Jan 2014 17:40:03 UTC +00:00', :role_admin => false, :role_suppliers => false, :role_article_meta => false, :role_finance => true, :role_orders => false, :next_weekly_tasks_number => 8, :ignore_apple_restriction => false) -Workgroup.create!(:id => 3, :name => "Ordering", :account_balance => 0.0, :created_on => 'Thu, 20 Feb 2014 14:44:47 UTC +00:00', :role_admin => false, :role_suppliers => false, :role_article_meta => true, :role_finance => false, :role_orders => true, :next_weekly_tasks_number => 8, :ignore_apple_restriction => false) -Workgroup.create!(:id => 4, :name => "Assortment", :account_balance => 0.0, :created_on => 'Wed, 09 Apr 2014 12:24:55 UTC +00:00', :role_admin => false, :role_suppliers => true, :role_article_meta => true, :role_finance => false, :role_orders => false, :next_weekly_tasks_number => 8, :ignore_apple_restriction => false) -Ordergroup.create!(:id => 5, :name => "Admin Administrator", :account_balance => 0.0, :created_on => 'Sat, 18 Jan 2014 00:38:48 UTC +00:00', :role_admin => false, :role_suppliers => false, :role_article_meta => false, :role_finance => false, :role_orders => false, :stats => { :jobs_size => 0, :orders_sum => 1021.74 }, :next_weekly_tasks_number => 8, :ignore_apple_restriction => true) -Ordergroup.create!(:id => 6, :name => "Pete's house", :account_balance => -0.35E2, :created_on => 'Sat, 25 Jan 2014 20:20:37 UTC +00:00', :role_admin => false, :role_suppliers => false, :role_article_meta => false, :role_finance => false, :role_orders => false, :contact_person => "Piet Pieterssen", :stats => { :jobs_size => 0, :orders_sum => 60.96 }, :next_weekly_tasks_number => 8, :ignore_apple_restriction => false) -Ordergroup.create!(:id => 7, :name => "Jan Klaassen", :account_balance => -0.35E2, :created_on => 'Mon, 27 Jan 2014 16:22:14 UTC +00:00', :role_admin => false, :role_suppliers => false, :role_article_meta => false, :role_finance => false, :role_orders => false, :contact_person => "Jan Klaassen", :stats => { :jobs_size => 0, :orders_sum => 0 }, :next_weekly_tasks_number => 8, :ignore_apple_restriction => false) -Ordergroup.create!(:id => 8, :name => "John Doe", :account_balance => 0.90E2, :created_on => 'Wed, 09 Apr 2014 12:23:29 UTC +00:00', :role_admin => false, :role_suppliers => false, :role_article_meta => false, :role_finance => false, :role_orders => false, :contact_person => "John Doe", :stats => { :jobs_size => 0, :orders_sum => 0 }, :next_weekly_tasks_number => 8, :ignore_apple_restriction => false) +Workgroup.create!(id: 1, name: 'Administrators', description: 'System administrators.', + account_balance: 0.0, created_on: 'Wed, 15 Jan 2014 16:15:33 UTC +00:00', role_admin: true, role_suppliers: true, role_article_meta: true, role_finance: true, role_orders: true, next_weekly_tasks_number: 8, ignore_apple_restriction: false) +Workgroup.create!(id: 2, name: 'Finances', account_balance: 0.0, + created_on: 'Sun, 19 Jan 2014 17:40:03 UTC +00:00', role_admin: false, role_suppliers: false, role_article_meta: false, role_finance: true, role_orders: false, next_weekly_tasks_number: 8, ignore_apple_restriction: false) +Workgroup.create!(id: 3, name: 'Ordering', account_balance: 0.0, + created_on: 'Thu, 20 Feb 2014 14:44:47 UTC +00:00', role_admin: false, role_suppliers: false, role_article_meta: true, role_finance: false, role_orders: true, next_weekly_tasks_number: 8, ignore_apple_restriction: false) +Workgroup.create!(id: 4, name: 'Assortment', account_balance: 0.0, + created_on: 'Wed, 09 Apr 2014 12:24:55 UTC +00:00', role_admin: false, role_suppliers: true, role_article_meta: true, role_finance: false, role_orders: false, next_weekly_tasks_number: 8, ignore_apple_restriction: false) +Ordergroup.create!(id: 5, name: 'Admin Administrator', account_balance: 0.0, + created_on: 'Sat, 18 Jan 2014 00:38:48 UTC +00:00', role_admin: false, role_suppliers: false, role_article_meta: false, role_finance: false, role_orders: false, stats: { jobs_size: 0, orders_sum: 1021.74 }, next_weekly_tasks_number: 8, ignore_apple_restriction: true) +Ordergroup.create!(id: 6, name: "Pete's house", account_balance: -0.35E2, + created_on: 'Sat, 25 Jan 2014 20:20:37 UTC +00:00', role_admin: false, role_suppliers: false, role_article_meta: false, role_finance: false, role_orders: false, contact_person: 'Piet Pieterssen', stats: { jobs_size: 0, orders_sum: 60.96 }, next_weekly_tasks_number: 8, ignore_apple_restriction: false) +Ordergroup.create!(id: 7, name: 'Jan Klaassen', account_balance: -0.35E2, + created_on: 'Mon, 27 Jan 2014 16:22:14 UTC +00:00', role_admin: false, role_suppliers: false, role_article_meta: false, role_finance: false, role_orders: false, contact_person: 'Jan Klaassen', stats: { jobs_size: 0, orders_sum: 0 }, next_weekly_tasks_number: 8, ignore_apple_restriction: false) +Ordergroup.create!(id: 8, name: 'John Doe', account_balance: 0.90E2, + created_on: 'Wed, 09 Apr 2014 12:23:29 UTC +00:00', role_admin: false, role_suppliers: false, role_article_meta: false, role_finance: false, role_orders: false, contact_person: 'John Doe', stats: { jobs_size: 0, orders_sum: 0 }, next_weekly_tasks_number: 8, ignore_apple_restriction: false) -Membership.create!(:group_id => 1, :user_id => 1) -Membership.create!(:group_id => 5, :user_id => 1) -Membership.create!(:group_id => 2, :user_id => 2) -Membership.create!(:group_id => 8, :user_id => 2) -Membership.create!(:group_id => 6, :user_id => 3) -Membership.create!(:group_id => 7, :user_id => 4) -Membership.create!(:group_id => 8, :user_id => 4) -Membership.create!(:group_id => 3, :user_id => 4) -Membership.create!(:group_id => 7, :user_id => 5) -Membership.create!(:group_id => 3, :user_id => 5) -Membership.create!(:group_id => 4, :user_id => 5) +Membership.create!(group_id: 1, user_id: 1) +Membership.create!(group_id: 5, user_id: 1) +Membership.create!(group_id: 2, user_id: 2) +Membership.create!(group_id: 8, user_id: 2) +Membership.create!(group_id: 6, user_id: 3) +Membership.create!(group_id: 7, user_id: 4) +Membership.create!(group_id: 8, user_id: 4) +Membership.create!(group_id: 3, user_id: 4) +Membership.create!(group_id: 7, user_id: 5) +Membership.create!(group_id: 3, user_id: 5) +Membership.create!(group_id: 4, user_id: 5) ## Orders & OrderArticles @@ -182,10 +308,15 @@ seed_group_orders ## Finances -FinancialTransactionType.create!(:id => 1, :name => "Foodcoop", :financial_transaction_class_id => 1) +FinancialTransactionType.create!(id: 1, name: 'Foodcoop', financial_transaction_class_id: 1) -FinancialTransaction.create!(:id => 1, :ordergroup_id => 5, :amount => -0.35E2, :note => "Membership fee for ordergroup", :user_id => 1, :created_on => 'Sat, 18 Jan 2014 00:38:48 UTC +00:00', :financial_transaction_type_id => 1) -FinancialTransaction.create!(:id => 3, :ordergroup_id => 6, :amount => -0.35E2, :note => "Membership fee for ordergroup", :user_id => 1, :created_on => 'Sat, 25 Jan 2014 20:20:37 UTC +00:00', :financial_transaction_type_id => 1) -FinancialTransaction.create!(:id => 4, :ordergroup_id => 7, :amount => -0.35E2, :note => "Membership fee for ordergroup", :user_id => 1, :created_on => 'Mon, 27 Jan 2014 16:22:14 UTC +00:00', :financial_transaction_type_id => 1) -FinancialTransaction.create!(:id => 5, :ordergroup_id => 5, :amount => 0.35E2, :note => "payment", :user_id => 2, :created_on => 'Wed, 05 Feb 2014 16:49:24 UTC +00:00', :financial_transaction_type_id => 1) -FinancialTransaction.create!(:id => 6, :ordergroup_id => 8, :amount => 0.90E2, :note => "Bank transfer", :user_id => 2, :created_on => 'Mon, 17 Feb 2014 16:19:34 UTC +00:00', :financial_transaction_type_id => 1) +FinancialTransaction.create!(id: 1, ordergroup_id: 5, amount: -0.35E2, + note: 'Membership fee for ordergroup', user_id: 1, created_on: 'Sat, 18 Jan 2014 00:38:48 UTC +00:00', financial_transaction_type_id: 1) +FinancialTransaction.create!(id: 3, ordergroup_id: 6, amount: -0.35E2, + note: 'Membership fee for ordergroup', user_id: 1, created_on: 'Sat, 25 Jan 2014 20:20:37 UTC +00:00', financial_transaction_type_id: 1) +FinancialTransaction.create!(id: 4, ordergroup_id: 7, amount: -0.35E2, + note: 'Membership fee for ordergroup', user_id: 1, created_on: 'Mon, 27 Jan 2014 16:22:14 UTC +00:00', financial_transaction_type_id: 1) +FinancialTransaction.create!(id: 5, ordergroup_id: 5, amount: 0.35E2, note: 'payment', user_id: 2, + created_on: 'Wed, 05 Feb 2014 16:49:24 UTC +00:00', financial_transaction_type_id: 1) +FinancialTransaction.create!(id: 6, ordergroup_id: 8, amount: 0.90E2, note: 'Bank transfer', user_id: 2, + created_on: 'Mon, 17 Feb 2014 16:19:34 UTC +00:00', financial_transaction_type_id: 1) diff --git a/db/seeds/small.nl.seeds.rb b/db/seeds/small.nl.seeds.rb index afa9cc04..1dd39992 100644 --- a/db/seeds/small.nl.seeds.rb +++ b/db/seeds/small.nl.seeds.rb @@ -1,173 +1,300 @@ -require_relative 'seed_helper.rb' +require_relative 'seed_helper' ## Financial transaction classes -FinancialTransactionClass.create!(:id => 1, :name => 'Standaard') -FinancialTransactionClass.create!(:id => 2, :name => 'Foodsoft') +FinancialTransactionClass.create!(id: 1, name: 'Standaard') +FinancialTransactionClass.create!(id: 2, name: 'Foodsoft') ## Suppliers & articles -SupplierCategory.create!(:id => 1, :name => "Other", :financial_transaction_class_id => 1) +SupplierCategory.create!(id: 1, name: 'Other', financial_transaction_class_id: 1) Supplier.create!([ - { :id => 1, :name => "Koekenbakker", :supplier_category_id => 1, :address => "Dorpsstraat 1, Koekange", :phone => "012 3456789", :email => "info@dekoekenbakker.test", :min_order_quantity => "100" }, - { :id => 2, :name => "Chocolademakkers", :supplier_category_id => 1, :address => "Multatuliweg 1, Amsterdam", :phone => "012 3456789", :email => "info@chocolademakkers.test", :url => "http://www.chocolademakkers.test/", :contact_person => "Max Puur", :delivery_days => "di, vr (Amsterdam)" }, - { :id => 3, :name => "Kaasmaker", :supplier_category_id => 1, :address => "Waagplein, Alkmaar", :phone => "012 3456789", :url => "http://www.kaaskamer.test/" }, - { :id => 4, :name => "Notenhuis", :supplier_category_id => 1, :address => "Damrak 1, Amsterdam", :phone => "012 3456789", :email => "info@notenhuis.test", :url => "http://www.notenhuis.test/", :note => "leveren in Amsterdam; €9 leverkosten bij bestellingen onder €123" } + { id: 1, name: 'Koekenbakker', supplier_category_id: 1, + address: 'Dorpsstraat 1, Koekange', phone: '012 3456789', email: 'info@dekoekenbakker.test', min_order_quantity: '100' }, + { id: 2, name: 'Chocolademakkers', supplier_category_id: 1, + address: 'Multatuliweg 1, Amsterdam', phone: '012 3456789', email: 'info@chocolademakkers.test', url: 'http://www.chocolademakkers.test/', contact_person: 'Max Puur', delivery_days: 'di, vr (Amsterdam)' }, + { id: 3, name: 'Kaasmaker', supplier_category_id: 1, address: 'Waagplein, Alkmaar', + phone: '012 3456789', url: 'http://www.kaaskamer.test/' }, + { id: 4, name: 'Notenhuis', supplier_category_id: 1, address: 'Damrak 1, Amsterdam', + phone: '012 3456789', email: 'info@notenhuis.test', url: 'http://www.notenhuis.test/', note: 'leveren in Amsterdam; €9 leverkosten bij bestellingen onder €123' } ]) -ArticleCategory.create!(:id => 1, :name => "Other", :description => "overig, anders, onbekend") -ArticleCategory.create!(:id => 2, :name => "Fruit") -ArticleCategory.create!(:id => 3, :name => "Groenten") -ArticleCategory.create!(:id => 4, :name => "Aardappels & uien") -ArticleCategory.create!(:id => 5, :name => "Brood & Bakkerij") -ArticleCategory.create!(:id => 6, :name => "Dranken", :description => "sap, fruitsap, groentesap, frisdrank") -ArticleCategory.create!(:id => 7, :name => "Kruiden", :description => "kruiden, specerijen, conserveringsmiddelen, extracten") -ArticleCategory.create!(:id => 8, :name => "Zuivel", :description => "melk, boter, room, yoghurt, kaas, eieren, zuivelvervangers") -ArticleCategory.create!(:id => 9, :name => "Vis & Zee", :description => "vis, schaaldieren, schelpdieren") -ArticleCategory.create!(:id => 10, :name => "Vlees & Gevogelte") -ArticleCategory.create!(:id => 11, :name => "Oliën & Vetten") -ArticleCategory.create!(:id => 12, :name => "Graan & Peulvruchten") -ArticleCategory.create!(:id => 13, :name => "Noten & Zaden") -ArticleCategory.create!(:id => 14, :name => "Zoetwaren & Zoetstof") +ArticleCategory.create!(id: 1, name: 'Other', description: 'overig, anders, onbekend') +ArticleCategory.create!(id: 2, name: 'Fruit') +ArticleCategory.create!(id: 3, name: 'Groenten') +ArticleCategory.create!(id: 4, name: 'Aardappels & uien') +ArticleCategory.create!(id: 5, name: 'Brood & Bakkerij') +ArticleCategory.create!(id: 6, name: 'Dranken', description: 'sap, fruitsap, groentesap, frisdrank') +ArticleCategory.create!(id: 7, name: 'Kruiden', + description: 'kruiden, specerijen, conserveringsmiddelen, extracten') +ArticleCategory.create!(id: 8, name: 'Zuivel', + description: 'melk, boter, room, yoghurt, kaas, eieren, zuivelvervangers') +ArticleCategory.create!(id: 9, name: 'Vis & Zee', description: 'vis, schaaldieren, schelpdieren') +ArticleCategory.create!(id: 10, name: 'Vlees & Gevogelte') +ArticleCategory.create!(id: 11, name: 'Oliën & Vetten') +ArticleCategory.create!(id: 12, name: 'Graan & Peulvruchten') +ArticleCategory.create!(id: 13, name: 'Noten & Zaden') +ArticleCategory.create!(id: 14, name: 'Zoetwaren & Zoetstof') -Article.create!(:name => "Volkoren heel", :supplier_id => 1, :article_category_id => 5, :unit => "stuk", :note => "bio", :availability => true, :manufacturer => "De Bakker", :origin => "NL", :price => 0.22E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "Volkoren half", :supplier_id => 1, :article_category_id => 5, :unit => "stuk", :note => "bio", :availability => true, :manufacturer => "De Bakker", :origin => "NL", :price => 0.11E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "Volkoren sesam heel", :supplier_id => 1, :article_category_id => 5, :unit => "stuk", :note => "bio", :availability => true, :manufacturer => "De Bakker", :origin => "NL", :price => 0.22E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "Volkoren sesam half", :supplier_id => 1, :article_category_id => 5, :unit => "stuk", :note => "bio", :availability => true, :manufacturer => "De Bakker", :origin => "NL", :price => 0.11E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "Licht tarwe heel", :supplier_id => 1, :article_category_id => 5, :unit => "stuk", :note => "bio", :availability => true, :manufacturer => "De Bakker", :origin => "NL", :price => 0.22E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "Licht tarwe half", :supplier_id => 1, :article_category_id => 5, :unit => "stuk", :note => "bio", :availability => true, :manufacturer => "De Bakker", :origin => "NL", :price => 0.11E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "Zonnebloempitbrood heel", :supplier_id => 1, :article_category_id => 5, :unit => "stuk", :note => "bio", :availability => true, :manufacturer => "De Bakker", :origin => "NL", :price => 0.33E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "Zonnebloempitbrood half", :supplier_id => 1, :article_category_id => 5, :unit => "stuk", :note => "bio", :availability => true, :manufacturer => "De Bakker", :origin => "NL", :price => 0.11E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "Walnoten vloer heel", :supplier_id => 1, :article_category_id => 5, :unit => "stuk", :note => "bio", :availability => true, :manufacturer => "De Bakker", :origin => "NL", :price => 0.33E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "Walnoten vloer half", :supplier_id => 1, :article_category_id => 5, :unit => "stuk", :note => "bio", :availability => true, :manufacturer => "De Bakker", :origin => "NL", :price => 0.11E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "Kennemerlandbrood heel", :supplier_id => 1, :article_category_id => 5, :unit => "stuk", :note => "bio", :availability => true, :manufacturer => "De Bakker", :origin => "NL", :price => 0.33E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "Kennemerlandbrood half", :supplier_id => 1, :article_category_id => 5, :unit => "stuk", :note => "bio", :availability => true, :manufacturer => "De Bakker", :origin => "NL", :price => 0.11E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "Maisbrood heel", :supplier_id => 1, :article_category_id => 5, :unit => "stuk", :note => "bio", :availability => true, :manufacturer => "De Bakker", :origin => "NL", :price => 0.33E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "Maisbrood half", :supplier_id => 1, :article_category_id => 5, :unit => "stuk", :note => "bio", :availability => true, :manufacturer => "De Bakker", :origin => "NL", :price => 0.11E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "Oberlander 1200 gram heel", :supplier_id => 1, :article_category_id => 5, :unit => "stuk", :note => "bio", :availability => true, :manufacturer => "De Bakker", :origin => "NL", :price => 0.33E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "Oberlander 1200 gram half", :supplier_id => 1, :article_category_id => 5, :unit => "stuk", :note => "bio", :availability => true, :manufacturer => "De Bakker", :origin => "NL", :price => 0.11E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "Oberlander 900 gram heel", :supplier_id => 1, :article_category_id => 5, :unit => "stuk", :note => "bio", :availability => true, :manufacturer => "De Bakker", :origin => "NL", :price => 0.33E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "Oberlander 900 gram half", :supplier_id => 1, :article_category_id => 5, :unit => "stuk", :note => "bio", :availability => true, :manufacturer => "De Bakker", :origin => "NL", :price => 0.11E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "Speltbrood heel", :supplier_id => 1, :article_category_id => 5, :unit => "stuk", :note => "bio", :availability => true, :manufacturer => "De Bakker", :origin => "NL", :price => 0.33E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "Speltbrood half", :supplier_id => 1, :article_category_id => 5, :unit => "stuk", :note => "bio", :availability => true, :manufacturer => "De Bakker", :origin => "NL", :price => 0.11E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "Landbrood 900gram heel", :supplier_id => 1, :article_category_id => 5, :unit => "stuk", :note => "bio", :availability => true, :manufacturer => "De Bakker", :origin => "NL", :price => 0.33E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "Landbrood 900gram half", :supplier_id => 1, :article_category_id => 5, :unit => "stuk", :note => "bio", :availability => true, :manufacturer => "De Bakker", :origin => "NL", :price => 0.11E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "Wit heel", :supplier_id => 1, :article_category_id => 5, :unit => "stuk", :note => "bio", :availability => true, :manufacturer => "De Bakker", :origin => "NL", :price => 0.33E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "Wit half", :supplier_id => 1, :article_category_id => 5, :unit => "stuk", :note => "bio", :availability => true, :manufacturer => "De Bakker", :origin => "NL", :price => 0.11E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "Wit met maanzaad heel", :supplier_id => 1, :article_category_id => 5, :unit => "stuk", :note => "bio", :availability => true, :manufacturer => "De Bakker", :origin => "NL", :price => 0.33E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "Wit met maanzaad half", :supplier_id => 1, :article_category_id => 5, :unit => "stuk", :note => "bio", :availability => true, :manufacturer => "De Bakker", :origin => "NL", :price => 0.11E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "Vijgenbrood heel", :supplier_id => 1, :article_category_id => 5, :unit => "stuk", :note => "bio", :availability => true, :manufacturer => "De Bakker", :origin => "NL", :price => 0.33E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "Vijgenbrood half", :supplier_id => 1, :article_category_id => 5, :unit => "stuk", :note => "bio", :availability => true, :manufacturer => "De Bakker", :origin => "NL", :price => 0.11E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "Bierborstelbrood heel", :supplier_id => 1, :article_category_id => 5, :unit => "stuk", :note => "bio", :availability => true, :manufacturer => "De Bakker", :origin => "NL", :price => 0.33E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "Bierborstelbrood half", :supplier_id => 1, :article_category_id => 5, :unit => "stuk", :note => "bio", :availability => true, :manufacturer => "De Bakker", :origin => "NL", :price => 0.11E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "Krentenbol", :supplier_id => 1, :article_category_id => 5, :unit => "stuk", :note => "bio", :availability => true, :manufacturer => "De Bakker", :origin => "NL", :price => 0.99E0, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "Mueslibol", :supplier_id => 1, :article_category_id => 5, :unit => "stuk", :note => "bio", :availability => true, :manufacturer => "De Bakker", :origin => "NL", :price => 0.11E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "Brioche", :supplier_id => 1, :article_category_id => 5, :unit => "stuk", :note => "bio", :availability => true, :manufacturer => "De Bakker", :origin => "NL", :price => 0.91E0, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "Volkoren croissant", :supplier_id => 1, :article_category_id => 5, :unit => "stuk", :note => "bio", :availability => true, :manufacturer => "De Bakker", :origin => "NL", :price => 0.11E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "Croissants", :supplier_id => 1, :article_category_id => 5, :unit => "stuk", :note => "bio", :availability => true, :manufacturer => "De Bakker", :origin => "NL", :price => 0.11E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "Kaas croissants", :supplier_id => 1, :article_category_id => 5, :unit => "stuk", :note => "bio", :availability => true, :manufacturer => "De Bakker", :origin => "NL", :price => 0.11E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "Chocoladecroissants", :supplier_id => 1, :article_category_id => 5, :unit => "stuk", :note => "bio", :availability => true, :manufacturer => "De Bakker", :origin => "NL", :price => 0.11E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "Soepstengels wit", :supplier_id => 1, :article_category_id => 5, :unit => "stuk", :note => "bio", :availability => true, :manufacturer => "De Bakker", :origin => "NL", :price => 0.11E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "Soepstengels volkoren", :supplier_id => 1, :article_category_id => 5, :unit => "stuk", :note => "bio", :availability => true, :manufacturer => "De Bakker", :origin => "NL", :price => 0.99E0, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "Pompoenpitten broodjes", :supplier_id => 1, :article_category_id => 5, :unit => "stuk", :note => "bio", :availability => true, :manufacturer => "De Bakker", :origin => "NL", :price => 0.88E0, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "Witte kadetjes", :supplier_id => 1, :article_category_id => 5, :unit => "stuk", :note => "bio", :availability => true, :manufacturer => "De Bakker", :origin => "NL", :price => 0.66E0, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "Bruine kadetjes", :supplier_id => 1, :article_category_id => 5, :unit => "stuk", :note => "bio", :availability => true, :manufacturer => "De Bakker", :origin => "NL", :price => 0.66E0, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "Tomaten feta broodje", :supplier_id => 1, :article_category_id => 5, :unit => "stuk", :note => "bio", :availability => true, :manufacturer => "De Bakker", :origin => "NL", :price => 0.11E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "Chocoladereep Melk (37%)", :supplier_id => 2, :article_category_id => 14, :unit => "90gr", :note => "bio", :availability => true, :manufacturer => "Chocolademakkers", :origin => "NL", :price => 0.22E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "Chocoladereep Puur (68%)", :supplier_id => 2, :article_category_id => 14, :unit => "90gr", :note => "bio", :availability => true, :manufacturer => "Chocolademakkers", :origin => "NL", :price => 0.22E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "Chocoladereep Drie Mensen Melk (40%)", :supplier_id => 2, :article_category_id => 14, :unit => "90gr", :note => "bio", :availability => true, :manufacturer => "Chocolademakkers", :origin => "NL", :price => 0.22E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "Chocoladereep Drie Mensen Puur (75%)", :supplier_id => 2, :article_category_id => 14, :unit => "90gr", :note => "bio", :availability => true, :manufacturer => "Chocolademakkers", :origin => "NL", :price => 0.22E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "Chocoladereep Zwaan Puur (75%)", :supplier_id => 2, :article_category_id => 14, :unit => "120gr", :note => "bio", :availability => true, :manufacturer => "Chocolademakkers", :origin => "NL", :price => 0.66E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "Cacao nibs", :supplier_id => 2, :article_category_id => 14, :unit => "1 kg", :note => "bio", :availability => true, :manufacturer => "Chocolademakkers", :origin => "NL", :price => 0.10E2, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1) -Article.create!(:name => "Kaas Koe-jong", :supplier_id => 3, :article_category_id => 8, :unit => "kg", :note => "bio", :availability => true, :manufacturer => "Kaasboerderij", :origin => "NL", :price => 0.88E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 8) -Article.create!(:name => "Kaas koe- jong belegen", :supplier_id => 3, :article_category_id => 8, :unit => "kg", :note => "bio", :availability => true, :manufacturer => "Kaasboerderij", :origin => "NL", :price => 0.99E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 8) -Article.create!(:name => "Kaas koe- belegen", :supplier_id => 3, :article_category_id => 8, :unit => "kg", :note => "bio", :availability => true, :manufacturer => "Kaasboerderij", :origin => "NL", :price => 0.11E2, :tax => 6.0, :deposit => 0.0, :unit_quantity => 12) -Article.create!(:name => "Kaas koe- extra belegen", :supplier_id => 3, :article_category_id => 8, :unit => "kg", :note => "bio", :availability => true, :manufacturer => "Kaasboerderij", :origin => "NL", :price => 0.11E2, :tax => 6.0, :deposit => 0.0, :unit_quantity => 8) -Article.create!(:name => "kaas Koe- oud", :supplier_id => 3, :article_category_id => 8, :unit => "kg", :note => "bio", :availability => true, :manufacturer => "Kaasboerderij", :origin => "NL", :price => 0.1375E2, :tax => 6.0, :deposit => 0.0, :unit_quantity => 8) -Article.create!(:name => "kaas koe -overjarig", :supplier_id => 3, :article_category_id => 8, :unit => "kg", :note => "bio", :availability => true, :manufacturer => "Kaasboerderij", :origin => "NL", :price => 0.11E2, :tax => 6.0, :deposit => 0.0, :unit_quantity => 8) -Article.create!(:name => "Kaas Koe-brandnetel jong", :supplier_id => 3, :article_category_id => 8, :unit => "kg", :note => "bio", :availability => true, :manufacturer => "Kaasboerderij", :origin => "NL", :price => 0.99E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 8) -Article.create!(:name => "Kaas koe- brandnetel jong belegen", :supplier_id => 3, :article_category_id => 8, :unit => "kg", :note => "bio", :availability => true, :manufacturer => "Kaasboerderij", :origin => "NL", :price => 0.11E2, :tax => 6.0, :deposit => 0.0, :unit_quantity => 8) -Article.create!(:name => "Kaas koe- brandnetel belegen", :supplier_id => 3, :article_category_id => 8, :unit => "kg", :note => "bio", :availability => true, :manufacturer => "Kaasboerderij", :origin => "NL", :price => 0.11E2, :tax => 6.0, :deposit => 0.0, :unit_quantity => 8) -Article.create!(:name => "Kaas Koe-komijn jong", :supplier_id => 3, :article_category_id => 8, :unit => "kg", :note => "bio", :availability => true, :manufacturer => "Kaasboerderij", :origin => "NL", :price => 0.99E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 8) -Article.create!(:name => "Kaas koe- komijn jong belegen", :supplier_id => 3, :article_category_id => 8, :unit => "kg", :note => "bio", :availability => true, :manufacturer => "Kaasboerderij", :origin => "NL", :price => 0.11E2, :tax => 6.0, :deposit => 0.0, :unit_quantity => 8) -Article.create!(:name => "Kaas koe- komijn belegen", :supplier_id => 3, :article_category_id => 8, :unit => "kg", :note => "bio", :availability => true, :manufacturer => "Kaasboerderij", :origin => "NL", :price => 0.11E2, :tax => 6.0, :deposit => 0.0, :unit_quantity => 8) -Article.create!(:name => "Cashewnoten", :supplier_id => 4, :article_category_id => 13, :unit => "kg", :note => "bio", :availability => true, :price => 0.4444E2, :tax => 6.0, :deposit => 0.0, :unit_quantity => 22, :order_number => ":b936051") -Article.create!(:name => "Hazel wit", :supplier_id => 4, :article_category_id => 13, :unit => "kg", :note => "bio", :availability => true, :price => 0.3333E2, :tax => 6.0, :deposit => 0.0, :unit_quantity => 10, :order_number => ":9e3f85b") -Article.create!(:name => "Hazel bruin", :supplier_id => 4, :article_category_id => 13, :unit => "kg", :note => "bio", :availability => true, :price => 0.1111E2, :tax => 6.0, :deposit => 0.0, :unit_quantity => 10, :order_number => ":d278041") -Article.create!(:name => "Amandel Bruin Spaans", :supplier_id => 4, :article_category_id => 13, :unit => "kg", :note => "bio", :availability => true, :price => 0.999E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 10, :order_number => ":0b51a8d") -Article.create!(:name => "Paranoten (bio)", :supplier_id => 4, :article_category_id => 13, :unit => "kg", :note => "bio", :availability => true, :price => 0.6666E2, :tax => 6.0, :deposit => 0.0, :unit_quantity => 20, :order_number => ":01e59e3") -Article.create!(:name => "Bio walnoten light halfjes", :supplier_id => 4, :article_category_id => 13, :unit => "kg", :note => "bio", :availability => true, :price => 0.333E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 10, :order_number => ":7ff8587") -Article.create!(:name => "Pijnboompitten", :supplier_id => 4, :article_category_id => 13, :unit => "kg", :note => "bio", :availability => true, :price => 0.888E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 25, :order_number => ":aa88d9f") -Article.create!(:name => "Pompoen", :supplier_id => 4, :article_category_id => 13, :unit => "kg", :note => "bio", :availability => true, :price => 0.1111E2, :tax => 6.0, :deposit => 0.0, :unit_quantity => 25, :order_number => ":e63069b") -Article.create!(:name => "Zonnepitten (bio)", :supplier_id => 4, :article_category_id => 13, :unit => "kg", :note => "bio", :availability => true, :price => 0.999E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 25, :order_number => ":0428388") -Article.create!(:name => "Amandel Wit Spaans", :supplier_id => 4, :article_category_id => 13, :unit => "kg", :note => "bio", :availability => true, :price => 0.66666E3, :tax => 6.0, :deposit => 0.0, :unit_quantity => 10, :order_number => ":a8f0734") -Article.create!(:name => "Cashew", :supplier_id => 4, :article_category_id => 13, :unit => "kg", :availability => true, :price => 0.6666E2, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1, :order_number => ":1d26958") -Article.create!(:name => "Amandelen geblancheerd", :supplier_id => 4, :article_category_id => 13, :unit => "kg", :availability => true, :price => 0.333E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1, :order_number => ":31439e2") -Article.create!(:name => "Amandelen naturel", :supplier_id => 4, :article_category_id => 13, :unit => "kg", :availability => true, :price => 0.1111E2, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1, :order_number => ":9c49374") -Article.create!(:name => "Walnoot ELH hafjes", :supplier_id => 4, :article_category_id => 13, :unit => "kg", :availability => true, :price => 0.4444E2, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1, :order_number => ":92907d1") -Article.create!(:name => "Walnoot ELP stukjes", :supplier_id => 4, :article_category_id => 13, :unit => "kg", :availability => true, :price => 0.8888E2, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1, :order_number => ":395640e") -Article.create!(:name => "Paranoten", :supplier_id => 4, :article_category_id => 13, :unit => "kg", :availability => true, :price => 0.8888E2, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1, :order_number => ":710acbb") -Article.create!(:name => "Macadamia Stijl 0", :supplier_id => 4, :article_category_id => 13, :unit => "kg", :availability => true, :price => 0.3333E2, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1, :order_number => ":bbaf40b") -Article.create!(:name => "Pecan", :supplier_id => 4, :article_category_id => 13, :unit => "kg", :availability => true, :price => 0.55555E3, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1, :order_number => ":7958183") -Article.create!(:name => "Hazelnoten naturel", :supplier_id => 4, :article_category_id => 13, :unit => "kg", :availability => true, :price => 0.6666E2, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1, :order_number => ":50392a8") -Article.create!(:name => "Hazelnoten geblancheerd", :supplier_id => 4, :article_category_id => 13, :unit => "kg", :availability => true, :price => 0.3333E2, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1, :order_number => ":4fe6525") -Article.create!(:name => "Gemengde Noten", :supplier_id => 4, :article_category_id => 13, :unit => "kg", :availability => true, :price => 0.333E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1, :order_number => ":c051b22") -Article.create!(:name => "Pinda's", :supplier_id => 4, :article_category_id => 13, :unit => "kg", :availability => true, :price => 0.777E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1, :order_number => ":f507577") -Article.create!(:name => "Vliespinda's klein", :supplier_id => 4, :article_category_id => 13, :unit => "kg", :availability => true, :price => 0.8888E2, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1, :order_number => ":ce563bb") -Article.create!(:name => "Medjoul dadels", :supplier_id => 4, :article_category_id => 13, :unit => "kg", :availability => true, :price => 0.3333E2, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1, :order_number => ":8232061") -Article.create!(:name => "Turkse Abrikozen ongezwaveld", :supplier_id => 4, :article_category_id => 13, :unit => "kg", :availability => true, :price => 0.888E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1, :order_number => ":185084f") -Article.create!(:name => "Turkse Abrikozen gezwaveld", :supplier_id => 4, :article_category_id => 13, :unit => "kg", :availability => true, :price => 0.1111E2, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1, :order_number => ":2b2fb20") -Article.create!(:name => "Spaanse Vijgen", :supplier_id => 4, :article_category_id => 13, :unit => "kg", :availability => true, :price => 0.444E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1, :order_number => ":82590b1") -Article.create!(:name => "Turkse Vijgen", :supplier_id => 4, :article_category_id => 13, :unit => "kg", :availability => true, :price => 0.555E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1, :order_number => ":cabeeb6") -Article.create!(:name => "Zure Abrikozen Zuid Afrika", :supplier_id => 4, :article_category_id => 13, :unit => "kg", :availability => true, :price => 0.1111E2, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1, :order_number => ":2ac18b7") -Article.create!(:name => "Blauwe rozijnen Flames", :supplier_id => 4, :article_category_id => 13, :unit => "kg", :availability => true, :price => 0.1111E2, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1, :order_number => ":16bfa75") -Article.create!(:name => "Gele Rozijnen", :supplier_id => 4, :article_category_id => 13, :unit => "kg", :availability => true, :price => 0.2222E2, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1, :order_number => ":1c59324") -Article.create!(:name => "Rode Rozijnen", :supplier_id => 4, :article_category_id => 13, :unit => "kg", :availability => true, :price => 0.1111E2, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1, :order_number => ":c3fcd84") -Article.create!(:name => "Cranberries heel", :supplier_id => 4, :article_category_id => 13, :unit => "kg", :availability => true, :price => 0.222E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1, :order_number => ":921c168") -Article.create!(:name => "Gedroogde Appeltjes", :supplier_id => 4, :article_category_id => 13, :unit => "kg", :availability => true, :price => 0.555E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1, :order_number => ":902c67b") -Article.create!(:name => "Gedroogde pruimen zonder pit", :supplier_id => 4, :article_category_id => 13, :unit => "kg", :availability => true, :price => 0.222E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1, :order_number => ":a847f91") -Article.create!(:name => "Pompoenpitten", :supplier_id => 4, :article_category_id => 13, :unit => "kg", :availability => true, :price => 0.111E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1, :order_number => ":535645f") -Article.create!(:name => "Zonnenbloepitten", :supplier_id => 4, :article_category_id => 13, :unit => "kg", :availability => true, :price => 0.666E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1, :order_number => ":4ab9a83") -Article.create!(:name => "Lijnzaad", :supplier_id => 4, :article_category_id => 13, :unit => "kg", :availability => true, :price => 0.55E0, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1, :order_number => ":04be223") -Article.create!(:name => "Maanzaad", :supplier_id => 4, :article_category_id => 13, :unit => "kg", :availability => true, :price => 0.7777E2, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1, :order_number => ":ec5b2b9") -Article.create!(:name => "Pijnboompitten medium china", :supplier_id => 4, :article_category_id => 13, :unit => "kg", :availability => true, :price => 0.2222E2, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1, :order_number => ":0e5b0b8") -Article.create!(:name => "Goji bessen", :supplier_id => 4, :article_category_id => 13, :unit => "kg", :availability => true, :price => 0.888E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1, :order_number => ":d52ee00") -Article.create!(:name => "Mulberries", :supplier_id => 4, :article_category_id => 13, :unit => "kg", :availability => true, :price => 0.5555E2, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1, :order_number => ":5f46bd5") -Article.create!(:name => "Gepelde Hennep", :supplier_id => 4, :article_category_id => 13, :unit => "kg", :availability => true, :price => 0.5555E2, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1, :order_number => ":c39165b") -Article.create!(:name => "Incaberries", :supplier_id => 4, :article_category_id => 13, :unit => "kg", :availability => true, :price => 0.888E1, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1, :order_number => ":8d44fe7") -Article.create!(:name => "Blueberries", :supplier_id => 4, :article_category_id => 13, :unit => "kg", :availability => true, :price => 0.2222E2, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1, :order_number => ":9a95422") -Article.create!(:name => "Chia zaad", :supplier_id => 4, :article_category_id => 13, :unit => "kg", :availability => true, :price => 0.55555E3, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1, :order_number => ":416d57b") -Article.create!(:name => "Cocos Rasp", :supplier_id => 4, :article_category_id => 13, :unit => "kg", :availability => true, :price => 0.55E0, :tax => 6.0, :deposit => 0.0, :unit_quantity => 1, :order_number => ":b3f65e4") +Article.create!(name: 'Volkoren heel', supplier_id: 1, article_category_id: 5, unit: 'stuk', + note: 'bio', availability: true, manufacturer: 'De Bakker', origin: 'NL', price: 0.22E1, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'Volkoren half', supplier_id: 1, article_category_id: 5, unit: 'stuk', + note: 'bio', availability: true, manufacturer: 'De Bakker', origin: 'NL', price: 0.11E1, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'Volkoren sesam heel', supplier_id: 1, article_category_id: 5, unit: 'stuk', + note: 'bio', availability: true, manufacturer: 'De Bakker', origin: 'NL', price: 0.22E1, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'Volkoren sesam half', supplier_id: 1, article_category_id: 5, unit: 'stuk', + note: 'bio', availability: true, manufacturer: 'De Bakker', origin: 'NL', price: 0.11E1, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'Licht tarwe heel', supplier_id: 1, article_category_id: 5, unit: 'stuk', + note: 'bio', availability: true, manufacturer: 'De Bakker', origin: 'NL', price: 0.22E1, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'Licht tarwe half', supplier_id: 1, article_category_id: 5, unit: 'stuk', + note: 'bio', availability: true, manufacturer: 'De Bakker', origin: 'NL', price: 0.11E1, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'Zonnebloempitbrood heel', supplier_id: 1, article_category_id: 5, unit: 'stuk', + note: 'bio', availability: true, manufacturer: 'De Bakker', origin: 'NL', price: 0.33E1, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'Zonnebloempitbrood half', supplier_id: 1, article_category_id: 5, unit: 'stuk', + note: 'bio', availability: true, manufacturer: 'De Bakker', origin: 'NL', price: 0.11E1, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'Walnoten vloer heel', supplier_id: 1, article_category_id: 5, unit: 'stuk', + note: 'bio', availability: true, manufacturer: 'De Bakker', origin: 'NL', price: 0.33E1, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'Walnoten vloer half', supplier_id: 1, article_category_id: 5, unit: 'stuk', + note: 'bio', availability: true, manufacturer: 'De Bakker', origin: 'NL', price: 0.11E1, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'Kennemerlandbrood heel', supplier_id: 1, article_category_id: 5, unit: 'stuk', + note: 'bio', availability: true, manufacturer: 'De Bakker', origin: 'NL', price: 0.33E1, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'Kennemerlandbrood half', supplier_id: 1, article_category_id: 5, unit: 'stuk', + note: 'bio', availability: true, manufacturer: 'De Bakker', origin: 'NL', price: 0.11E1, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'Maisbrood heel', supplier_id: 1, article_category_id: 5, unit: 'stuk', + note: 'bio', availability: true, manufacturer: 'De Bakker', origin: 'NL', price: 0.33E1, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'Maisbrood half', supplier_id: 1, article_category_id: 5, unit: 'stuk', + note: 'bio', availability: true, manufacturer: 'De Bakker', origin: 'NL', price: 0.11E1, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'Oberlander 1200 gram heel', supplier_id: 1, article_category_id: 5, unit: 'stuk', + note: 'bio', availability: true, manufacturer: 'De Bakker', origin: 'NL', price: 0.33E1, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'Oberlander 1200 gram half', supplier_id: 1, article_category_id: 5, unit: 'stuk', + note: 'bio', availability: true, manufacturer: 'De Bakker', origin: 'NL', price: 0.11E1, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'Oberlander 900 gram heel', supplier_id: 1, article_category_id: 5, unit: 'stuk', + note: 'bio', availability: true, manufacturer: 'De Bakker', origin: 'NL', price: 0.33E1, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'Oberlander 900 gram half', supplier_id: 1, article_category_id: 5, unit: 'stuk', + note: 'bio', availability: true, manufacturer: 'De Bakker', origin: 'NL', price: 0.11E1, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'Speltbrood heel', supplier_id: 1, article_category_id: 5, unit: 'stuk', + note: 'bio', availability: true, manufacturer: 'De Bakker', origin: 'NL', price: 0.33E1, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'Speltbrood half', supplier_id: 1, article_category_id: 5, unit: 'stuk', + note: 'bio', availability: true, manufacturer: 'De Bakker', origin: 'NL', price: 0.11E1, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'Landbrood 900gram heel', supplier_id: 1, article_category_id: 5, unit: 'stuk', + note: 'bio', availability: true, manufacturer: 'De Bakker', origin: 'NL', price: 0.33E1, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'Landbrood 900gram half', supplier_id: 1, article_category_id: 5, unit: 'stuk', + note: 'bio', availability: true, manufacturer: 'De Bakker', origin: 'NL', price: 0.11E1, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'Wit heel', supplier_id: 1, article_category_id: 5, unit: 'stuk', note: 'bio', + availability: true, manufacturer: 'De Bakker', origin: 'NL', price: 0.33E1, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'Wit half', supplier_id: 1, article_category_id: 5, unit: 'stuk', note: 'bio', + availability: true, manufacturer: 'De Bakker', origin: 'NL', price: 0.11E1, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'Wit met maanzaad heel', supplier_id: 1, article_category_id: 5, unit: 'stuk', + note: 'bio', availability: true, manufacturer: 'De Bakker', origin: 'NL', price: 0.33E1, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'Wit met maanzaad half', supplier_id: 1, article_category_id: 5, unit: 'stuk', + note: 'bio', availability: true, manufacturer: 'De Bakker', origin: 'NL', price: 0.11E1, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'Vijgenbrood heel', supplier_id: 1, article_category_id: 5, unit: 'stuk', + note: 'bio', availability: true, manufacturer: 'De Bakker', origin: 'NL', price: 0.33E1, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'Vijgenbrood half', supplier_id: 1, article_category_id: 5, unit: 'stuk', + note: 'bio', availability: true, manufacturer: 'De Bakker', origin: 'NL', price: 0.11E1, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'Bierborstelbrood heel', supplier_id: 1, article_category_id: 5, unit: 'stuk', + note: 'bio', availability: true, manufacturer: 'De Bakker', origin: 'NL', price: 0.33E1, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'Bierborstelbrood half', supplier_id: 1, article_category_id: 5, unit: 'stuk', + note: 'bio', availability: true, manufacturer: 'De Bakker', origin: 'NL', price: 0.11E1, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'Krentenbol', supplier_id: 1, article_category_id: 5, unit: 'stuk', note: 'bio', + availability: true, manufacturer: 'De Bakker', origin: 'NL', price: 0.99E0, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'Mueslibol', supplier_id: 1, article_category_id: 5, unit: 'stuk', note: 'bio', + availability: true, manufacturer: 'De Bakker', origin: 'NL', price: 0.11E1, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'Brioche', supplier_id: 1, article_category_id: 5, unit: 'stuk', note: 'bio', + availability: true, manufacturer: 'De Bakker', origin: 'NL', price: 0.91E0, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'Volkoren croissant', supplier_id: 1, article_category_id: 5, unit: 'stuk', + note: 'bio', availability: true, manufacturer: 'De Bakker', origin: 'NL', price: 0.11E1, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'Croissants', supplier_id: 1, article_category_id: 5, unit: 'stuk', note: 'bio', + availability: true, manufacturer: 'De Bakker', origin: 'NL', price: 0.11E1, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'Kaas croissants', supplier_id: 1, article_category_id: 5, unit: 'stuk', + note: 'bio', availability: true, manufacturer: 'De Bakker', origin: 'NL', price: 0.11E1, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'Chocoladecroissants', supplier_id: 1, article_category_id: 5, unit: 'stuk', + note: 'bio', availability: true, manufacturer: 'De Bakker', origin: 'NL', price: 0.11E1, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'Soepstengels wit', supplier_id: 1, article_category_id: 5, unit: 'stuk', + note: 'bio', availability: true, manufacturer: 'De Bakker', origin: 'NL', price: 0.11E1, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'Soepstengels volkoren', supplier_id: 1, article_category_id: 5, unit: 'stuk', + note: 'bio', availability: true, manufacturer: 'De Bakker', origin: 'NL', price: 0.99E0, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'Pompoenpitten broodjes', supplier_id: 1, article_category_id: 5, unit: 'stuk', + note: 'bio', availability: true, manufacturer: 'De Bakker', origin: 'NL', price: 0.88E0, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'Witte kadetjes', supplier_id: 1, article_category_id: 5, unit: 'stuk', + note: 'bio', availability: true, manufacturer: 'De Bakker', origin: 'NL', price: 0.66E0, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'Bruine kadetjes', supplier_id: 1, article_category_id: 5, unit: 'stuk', + note: 'bio', availability: true, manufacturer: 'De Bakker', origin: 'NL', price: 0.66E0, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'Tomaten feta broodje', supplier_id: 1, article_category_id: 5, unit: 'stuk', + note: 'bio', availability: true, manufacturer: 'De Bakker', origin: 'NL', price: 0.11E1, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'Chocoladereep Melk (37%)', supplier_id: 2, article_category_id: 14, unit: '90gr', + note: 'bio', availability: true, manufacturer: 'Chocolademakkers', origin: 'NL', price: 0.22E1, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'Chocoladereep Puur (68%)', supplier_id: 2, article_category_id: 14, unit: '90gr', + note: 'bio', availability: true, manufacturer: 'Chocolademakkers', origin: 'NL', price: 0.22E1, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'Chocoladereep Drie Mensen Melk (40%)', supplier_id: 2, article_category_id: 14, + unit: '90gr', note: 'bio', availability: true, manufacturer: 'Chocolademakkers', origin: 'NL', price: 0.22E1, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'Chocoladereep Drie Mensen Puur (75%)', supplier_id: 2, article_category_id: 14, + unit: '90gr', note: 'bio', availability: true, manufacturer: 'Chocolademakkers', origin: 'NL', price: 0.22E1, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'Chocoladereep Zwaan Puur (75%)', supplier_id: 2, article_category_id: 14, + unit: '120gr', note: 'bio', availability: true, manufacturer: 'Chocolademakkers', origin: 'NL', price: 0.66E1, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'Cacao nibs', supplier_id: 2, article_category_id: 14, unit: '1 kg', note: 'bio', + availability: true, manufacturer: 'Chocolademakkers', origin: 'NL', price: 0.10E2, tax: 6.0, deposit: 0.0, unit_quantity: 1) +Article.create!(name: 'Kaas Koe-jong', supplier_id: 3, article_category_id: 8, unit: 'kg', note: 'bio', + availability: true, manufacturer: 'Kaasboerderij', origin: 'NL', price: 0.88E1, tax: 6.0, deposit: 0.0, unit_quantity: 8) +Article.create!(name: 'Kaas koe- jong belegen', supplier_id: 3, article_category_id: 8, unit: 'kg', + note: 'bio', availability: true, manufacturer: 'Kaasboerderij', origin: 'NL', price: 0.99E1, tax: 6.0, deposit: 0.0, unit_quantity: 8) +Article.create!(name: 'Kaas koe- belegen', supplier_id: 3, article_category_id: 8, unit: 'kg', + note: 'bio', availability: true, manufacturer: 'Kaasboerderij', origin: 'NL', price: 0.11E2, tax: 6.0, deposit: 0.0, unit_quantity: 12) +Article.create!(name: 'Kaas koe- extra belegen', supplier_id: 3, article_category_id: 8, unit: 'kg', + note: 'bio', availability: true, manufacturer: 'Kaasboerderij', origin: 'NL', price: 0.11E2, tax: 6.0, deposit: 0.0, unit_quantity: 8) +Article.create!(name: 'kaas Koe- oud', supplier_id: 3, article_category_id: 8, unit: 'kg', note: 'bio', + availability: true, manufacturer: 'Kaasboerderij', origin: 'NL', price: 0.1375E2, tax: 6.0, deposit: 0.0, unit_quantity: 8) +Article.create!(name: 'kaas koe -overjarig', supplier_id: 3, article_category_id: 8, unit: 'kg', + note: 'bio', availability: true, manufacturer: 'Kaasboerderij', origin: 'NL', price: 0.11E2, tax: 6.0, deposit: 0.0, unit_quantity: 8) +Article.create!(name: 'Kaas Koe-brandnetel jong', supplier_id: 3, article_category_id: 8, unit: 'kg', + note: 'bio', availability: true, manufacturer: 'Kaasboerderij', origin: 'NL', price: 0.99E1, tax: 6.0, deposit: 0.0, unit_quantity: 8) +Article.create!(name: 'Kaas koe- brandnetel jong belegen', supplier_id: 3, article_category_id: 8, + unit: 'kg', note: 'bio', availability: true, manufacturer: 'Kaasboerderij', origin: 'NL', price: 0.11E2, tax: 6.0, deposit: 0.0, unit_quantity: 8) +Article.create!(name: 'Kaas koe- brandnetel belegen', supplier_id: 3, article_category_id: 8, unit: 'kg', + note: 'bio', availability: true, manufacturer: 'Kaasboerderij', origin: 'NL', price: 0.11E2, tax: 6.0, deposit: 0.0, unit_quantity: 8) +Article.create!(name: 'Kaas Koe-komijn jong', supplier_id: 3, article_category_id: 8, unit: 'kg', + note: 'bio', availability: true, manufacturer: 'Kaasboerderij', origin: 'NL', price: 0.99E1, tax: 6.0, deposit: 0.0, unit_quantity: 8) +Article.create!(name: 'Kaas koe- komijn jong belegen', supplier_id: 3, article_category_id: 8, unit: 'kg', + note: 'bio', availability: true, manufacturer: 'Kaasboerderij', origin: 'NL', price: 0.11E2, tax: 6.0, deposit: 0.0, unit_quantity: 8) +Article.create!(name: 'Kaas koe- komijn belegen', supplier_id: 3, article_category_id: 8, unit: 'kg', + note: 'bio', availability: true, manufacturer: 'Kaasboerderij', origin: 'NL', price: 0.11E2, tax: 6.0, deposit: 0.0, unit_quantity: 8) +Article.create!(name: 'Cashewnoten', supplier_id: 4, article_category_id: 13, unit: 'kg', note: 'bio', + availability: true, price: 0.4444E2, tax: 6.0, deposit: 0.0, unit_quantity: 22, order_number: ':b936051') +Article.create!(name: 'Hazel wit', supplier_id: 4, article_category_id: 13, unit: 'kg', note: 'bio', + availability: true, price: 0.3333E2, tax: 6.0, deposit: 0.0, unit_quantity: 10, order_number: ':9e3f85b') +Article.create!(name: 'Hazel bruin', supplier_id: 4, article_category_id: 13, unit: 'kg', note: 'bio', + availability: true, price: 0.1111E2, tax: 6.0, deposit: 0.0, unit_quantity: 10, order_number: ':d278041') +Article.create!(name: 'Amandel Bruin Spaans', supplier_id: 4, article_category_id: 13, unit: 'kg', + note: 'bio', availability: true, price: 0.999E1, tax: 6.0, deposit: 0.0, unit_quantity: 10, order_number: ':0b51a8d') +Article.create!(name: 'Paranoten (bio)', supplier_id: 4, article_category_id: 13, unit: 'kg', + note: 'bio', availability: true, price: 0.6666E2, tax: 6.0, deposit: 0.0, unit_quantity: 20, order_number: ':01e59e3') +Article.create!(name: 'Bio walnoten light halfjes', supplier_id: 4, article_category_id: 13, unit: 'kg', + note: 'bio', availability: true, price: 0.333E1, tax: 6.0, deposit: 0.0, unit_quantity: 10, order_number: ':7ff8587') +Article.create!(name: 'Pijnboompitten', supplier_id: 4, article_category_id: 13, unit: 'kg', + note: 'bio', availability: true, price: 0.888E1, tax: 6.0, deposit: 0.0, unit_quantity: 25, order_number: ':aa88d9f') +Article.create!(name: 'Pompoen', supplier_id: 4, article_category_id: 13, unit: 'kg', note: 'bio', + availability: true, price: 0.1111E2, tax: 6.0, deposit: 0.0, unit_quantity: 25, order_number: ':e63069b') +Article.create!(name: 'Zonnepitten (bio)', supplier_id: 4, article_category_id: 13, unit: 'kg', + note: 'bio', availability: true, price: 0.999E1, tax: 6.0, deposit: 0.0, unit_quantity: 25, order_number: ':0428388') +Article.create!(name: 'Amandel Wit Spaans', supplier_id: 4, article_category_id: 13, unit: 'kg', + note: 'bio', availability: true, price: 0.66666E3, tax: 6.0, deposit: 0.0, unit_quantity: 10, order_number: ':a8f0734') +Article.create!(name: 'Cashew', supplier_id: 4, article_category_id: 13, unit: 'kg', availability: true, + price: 0.6666E2, tax: 6.0, deposit: 0.0, unit_quantity: 1, order_number: ':1d26958') +Article.create!(name: 'Amandelen geblancheerd', supplier_id: 4, article_category_id: 13, unit: 'kg', + availability: true, price: 0.333E1, tax: 6.0, deposit: 0.0, unit_quantity: 1, order_number: ':31439e2') +Article.create!(name: 'Amandelen naturel', supplier_id: 4, article_category_id: 13, unit: 'kg', + availability: true, price: 0.1111E2, tax: 6.0, deposit: 0.0, unit_quantity: 1, order_number: ':9c49374') +Article.create!(name: 'Walnoot ELH hafjes', supplier_id: 4, article_category_id: 13, unit: 'kg', + availability: true, price: 0.4444E2, tax: 6.0, deposit: 0.0, unit_quantity: 1, order_number: ':92907d1') +Article.create!(name: 'Walnoot ELP stukjes', supplier_id: 4, article_category_id: 13, unit: 'kg', + availability: true, price: 0.8888E2, tax: 6.0, deposit: 0.0, unit_quantity: 1, order_number: ':395640e') +Article.create!(name: 'Paranoten', supplier_id: 4, article_category_id: 13, unit: 'kg', + availability: true, price: 0.8888E2, tax: 6.0, deposit: 0.0, unit_quantity: 1, order_number: ':710acbb') +Article.create!(name: 'Macadamia Stijl 0', supplier_id: 4, article_category_id: 13, unit: 'kg', + availability: true, price: 0.3333E2, tax: 6.0, deposit: 0.0, unit_quantity: 1, order_number: ':bbaf40b') +Article.create!(name: 'Pecan', supplier_id: 4, article_category_id: 13, unit: 'kg', availability: true, + price: 0.55555E3, tax: 6.0, deposit: 0.0, unit_quantity: 1, order_number: ':7958183') +Article.create!(name: 'Hazelnoten naturel', supplier_id: 4, article_category_id: 13, unit: 'kg', + availability: true, price: 0.6666E2, tax: 6.0, deposit: 0.0, unit_quantity: 1, order_number: ':50392a8') +Article.create!(name: 'Hazelnoten geblancheerd', supplier_id: 4, article_category_id: 13, unit: 'kg', + availability: true, price: 0.3333E2, tax: 6.0, deposit: 0.0, unit_quantity: 1, order_number: ':4fe6525') +Article.create!(name: 'Gemengde Noten', supplier_id: 4, article_category_id: 13, unit: 'kg', + availability: true, price: 0.333E1, tax: 6.0, deposit: 0.0, unit_quantity: 1, order_number: ':c051b22') +Article.create!(name: "Pinda's", supplier_id: 4, article_category_id: 13, unit: 'kg', + availability: true, price: 0.777E1, tax: 6.0, deposit: 0.0, unit_quantity: 1, order_number: ':f507577') +Article.create!(name: "Vliespinda's klein", supplier_id: 4, article_category_id: 13, unit: 'kg', + availability: true, price: 0.8888E2, tax: 6.0, deposit: 0.0, unit_quantity: 1, order_number: ':ce563bb') +Article.create!(name: 'Medjoul dadels', supplier_id: 4, article_category_id: 13, unit: 'kg', + availability: true, price: 0.3333E2, tax: 6.0, deposit: 0.0, unit_quantity: 1, order_number: ':8232061') +Article.create!(name: 'Turkse Abrikozen ongezwaveld', supplier_id: 4, article_category_id: 13, unit: 'kg', + availability: true, price: 0.888E1, tax: 6.0, deposit: 0.0, unit_quantity: 1, order_number: ':185084f') +Article.create!(name: 'Turkse Abrikozen gezwaveld', supplier_id: 4, article_category_id: 13, unit: 'kg', + availability: true, price: 0.1111E2, tax: 6.0, deposit: 0.0, unit_quantity: 1, order_number: ':2b2fb20') +Article.create!(name: 'Spaanse Vijgen', supplier_id: 4, article_category_id: 13, unit: 'kg', + availability: true, price: 0.444E1, tax: 6.0, deposit: 0.0, unit_quantity: 1, order_number: ':82590b1') +Article.create!(name: 'Turkse Vijgen', supplier_id: 4, article_category_id: 13, unit: 'kg', + availability: true, price: 0.555E1, tax: 6.0, deposit: 0.0, unit_quantity: 1, order_number: ':cabeeb6') +Article.create!(name: 'Zure Abrikozen Zuid Afrika', supplier_id: 4, article_category_id: 13, unit: 'kg', + availability: true, price: 0.1111E2, tax: 6.0, deposit: 0.0, unit_quantity: 1, order_number: ':2ac18b7') +Article.create!(name: 'Blauwe rozijnen Flames', supplier_id: 4, article_category_id: 13, unit: 'kg', + availability: true, price: 0.1111E2, tax: 6.0, deposit: 0.0, unit_quantity: 1, order_number: ':16bfa75') +Article.create!(name: 'Gele Rozijnen', supplier_id: 4, article_category_id: 13, unit: 'kg', + availability: true, price: 0.2222E2, tax: 6.0, deposit: 0.0, unit_quantity: 1, order_number: ':1c59324') +Article.create!(name: 'Rode Rozijnen', supplier_id: 4, article_category_id: 13, unit: 'kg', + availability: true, price: 0.1111E2, tax: 6.0, deposit: 0.0, unit_quantity: 1, order_number: ':c3fcd84') +Article.create!(name: 'Cranberries heel', supplier_id: 4, article_category_id: 13, unit: 'kg', + availability: true, price: 0.222E1, tax: 6.0, deposit: 0.0, unit_quantity: 1, order_number: ':921c168') +Article.create!(name: 'Gedroogde Appeltjes', supplier_id: 4, article_category_id: 13, unit: 'kg', + availability: true, price: 0.555E1, tax: 6.0, deposit: 0.0, unit_quantity: 1, order_number: ':902c67b') +Article.create!(name: 'Gedroogde pruimen zonder pit', supplier_id: 4, article_category_id: 13, unit: 'kg', + availability: true, price: 0.222E1, tax: 6.0, deposit: 0.0, unit_quantity: 1, order_number: ':a847f91') +Article.create!(name: 'Pompoenpitten', supplier_id: 4, article_category_id: 13, unit: 'kg', + availability: true, price: 0.111E1, tax: 6.0, deposit: 0.0, unit_quantity: 1, order_number: ':535645f') +Article.create!(name: 'Zonnenbloepitten', supplier_id: 4, article_category_id: 13, unit: 'kg', + availability: true, price: 0.666E1, tax: 6.0, deposit: 0.0, unit_quantity: 1, order_number: ':4ab9a83') +Article.create!(name: 'Lijnzaad', supplier_id: 4, article_category_id: 13, unit: 'kg', + availability: true, price: 0.55E0, tax: 6.0, deposit: 0.0, unit_quantity: 1, order_number: ':04be223') +Article.create!(name: 'Maanzaad', supplier_id: 4, article_category_id: 13, unit: 'kg', + availability: true, price: 0.7777E2, tax: 6.0, deposit: 0.0, unit_quantity: 1, order_number: ':ec5b2b9') +Article.create!(name: 'Pijnboompitten medium china', supplier_id: 4, article_category_id: 13, unit: 'kg', + availability: true, price: 0.2222E2, tax: 6.0, deposit: 0.0, unit_quantity: 1, order_number: ':0e5b0b8') +Article.create!(name: 'Goji bessen', supplier_id: 4, article_category_id: 13, unit: 'kg', + availability: true, price: 0.888E1, tax: 6.0, deposit: 0.0, unit_quantity: 1, order_number: ':d52ee00') +Article.create!(name: 'Mulberries', supplier_id: 4, article_category_id: 13, unit: 'kg', + availability: true, price: 0.5555E2, tax: 6.0, deposit: 0.0, unit_quantity: 1, order_number: ':5f46bd5') +Article.create!(name: 'Gepelde Hennep', supplier_id: 4, article_category_id: 13, unit: 'kg', + availability: true, price: 0.5555E2, tax: 6.0, deposit: 0.0, unit_quantity: 1, order_number: ':c39165b') +Article.create!(name: 'Incaberries', supplier_id: 4, article_category_id: 13, unit: 'kg', + availability: true, price: 0.888E1, tax: 6.0, deposit: 0.0, unit_quantity: 1, order_number: ':8d44fe7') +Article.create!(name: 'Blueberries', supplier_id: 4, article_category_id: 13, unit: 'kg', + availability: true, price: 0.2222E2, tax: 6.0, deposit: 0.0, unit_quantity: 1, order_number: ':9a95422') +Article.create!(name: 'Chia zaad', supplier_id: 4, article_category_id: 13, unit: 'kg', + availability: true, price: 0.55555E3, tax: 6.0, deposit: 0.0, unit_quantity: 1, order_number: ':416d57b') +Article.create!(name: 'Cocos Rasp', supplier_id: 4, article_category_id: 13, unit: 'kg', + availability: true, price: 0.55E0, tax: 6.0, deposit: 0.0, unit_quantity: 1, order_number: ':b3f65e4') ## Members & groups -User.create!(:id => 1, :nick => "admin", :password => "secret", :first_name => "Anton", :last_name => "Administrator", :email => "admin@foo.test", :created_on => 'Wed, 15 Jan 2014 16:15:33 UTC +00:00') -User.create!(:id => 2, :nick => "john", :password => "secret", :first_name => "John", :last_name => "Doe", :email => "john@doe.test", :created_on => 'Sun, 19 Jan 2014 17:38:22 UTC +00:00') -User.create!(:id => 3, :nick => "peter", :password => "secret", :first_name => "Peter", :last_name => "Pieterssen", :email => "peter@pieterssen.test", :created_on => 'Sat, 25 Jan 2014 20:20:36 UTC +00:00') -User.create!(:id => 4, :nick => "jan", :password => "secret", :first_name => "Jan", :last_name => "Klaassen", :email => "jan@klaassen.test", :created_on => 'Mon, 27 Jan 2014 16:22:14 UTC +00:00') -User.create!(:id => 5, :nick => "mary", :password => "secret", :first_name => "Marie", :last_name => "Klaassen", :email => "mary@klaassen.test", :created_on => 'Mon, 03 Feb 2014 11:47:17 UTC +00:00') +User.create!(id: 1, nick: 'admin', password: 'secret', first_name: 'Anton', last_name: 'Administrator', + email: 'admin@foo.test', created_on: 'Wed, 15 Jan 2014 16:15:33 UTC +00:00') +User.create!(id: 2, nick: 'john', password: 'secret', first_name: 'John', last_name: 'Doe', + email: 'john@doe.test', created_on: 'Sun, 19 Jan 2014 17:38:22 UTC +00:00') +User.create!(id: 3, nick: 'peter', password: 'secret', first_name: 'Peter', last_name: 'Pieterssen', + email: 'peter@pieterssen.test', created_on: 'Sat, 25 Jan 2014 20:20:36 UTC +00:00') +User.create!(id: 4, nick: 'jan', password: 'secret', first_name: 'Jan', last_name: 'Klaassen', + email: 'jan@klaassen.test', created_on: 'Mon, 27 Jan 2014 16:22:14 UTC +00:00') +User.create!(id: 5, nick: 'mary', password: 'secret', first_name: 'Marie', last_name: 'Klaassen', + email: 'mary@klaassen.test', created_on: 'Mon, 03 Feb 2014 11:47:17 UTC +00:00') -Workgroup.create!(:id => 1, :name => "Admins", :description => "Beheerders", :account_balance => 0.0, :created_on => 'Wed, 15 Jan 2014 16:15:33 UTC +00:00', :role_admin => true, :role_suppliers => true, :role_article_meta => true, :role_finance => true, :role_orders => true, :next_weekly_tasks_number => 8, :ignore_apple_restriction => false) -Workgroup.create!(:id => 2, :name => "Financiën", :account_balance => 0.0, :created_on => 'Sun, 19 Jan 2014 17:40:03 UTC +00:00', :role_admin => false, :role_suppliers => false, :role_article_meta => false, :role_finance => true, :role_orders => false, :next_weekly_tasks_number => 8, :ignore_apple_restriction => false) -Workgroup.create!(:id => 3, :name => "Bestellen", :account_balance => 0.0, :created_on => 'Thu, 20 Feb 2014 14:44:47 UTC +00:00', :role_admin => false, :role_suppliers => false, :role_article_meta => true, :role_finance => false, :role_orders => true, :next_weekly_tasks_number => 8, :ignore_apple_restriction => false) -Workgroup.create!(:id => 4, :name => "Assortiment", :account_balance => 0.0, :created_on => 'Wed, 09 Apr 2014 12:24:55 UTC +00:00', :role_admin => false, :role_suppliers => true, :role_article_meta => true, :role_finance => false, :role_orders => false, :next_weekly_tasks_number => 8, :ignore_apple_restriction => false) -Ordergroup.create!(:id => 5, :name => "Admin Administrator", :account_balance => 0.0, :created_on => 'Sat, 18 Jan 2014 00:38:48 UTC +00:00', :role_admin => false, :role_suppliers => false, :role_article_meta => false, :role_finance => false, :role_orders => false, :stats => { :jobs_size => 0, :orders_sum => 1021.74 }, :next_weekly_tasks_number => 8, :ignore_apple_restriction => true) -Ordergroup.create!(:id => 6, :name => "Peter's huis", :account_balance => -0.35E2, :created_on => 'Sat, 25 Jan 2014 20:20:37 UTC +00:00', :role_admin => false, :role_suppliers => false, :role_article_meta => false, :role_finance => false, :role_orders => false, :contact_person => "Piet Pieterssen", :stats => { :jobs_size => 0, :orders_sum => 60.96 }, :next_weekly_tasks_number => 8, :ignore_apple_restriction => false) -Ordergroup.create!(:id => 7, :name => "Jan Klaassen", :account_balance => -0.35E2, :created_on => 'Mon, 27 Jan 2014 16:22:14 UTC +00:00', :role_admin => false, :role_suppliers => false, :role_article_meta => false, :role_finance => false, :role_orders => false, :contact_person => "Jan Klaassen", :stats => { :jobs_size => 0, :orders_sum => 0 }, :next_weekly_tasks_number => 8, :ignore_apple_restriction => false) -Ordergroup.create!(:id => 8, :name => "John Doe", :account_balance => 0.90E2, :created_on => 'Wed, 09 Apr 2014 12:23:29 UTC +00:00', :role_admin => false, :role_suppliers => false, :role_article_meta => false, :role_finance => false, :role_orders => false, :contact_person => "John Doe", :stats => { :jobs_size => 0, :orders_sum => 0 }, :next_weekly_tasks_number => 8, :ignore_apple_restriction => false) +Workgroup.create!(id: 1, name: 'Admins', description: 'Beheerders', account_balance: 0.0, + created_on: 'Wed, 15 Jan 2014 16:15:33 UTC +00:00', role_admin: true, role_suppliers: true, role_article_meta: true, role_finance: true, role_orders: true, next_weekly_tasks_number: 8, ignore_apple_restriction: false) +Workgroup.create!(id: 2, name: 'Financiën', account_balance: 0.0, + created_on: 'Sun, 19 Jan 2014 17:40:03 UTC +00:00', role_admin: false, role_suppliers: false, role_article_meta: false, role_finance: true, role_orders: false, next_weekly_tasks_number: 8, ignore_apple_restriction: false) +Workgroup.create!(id: 3, name: 'Bestellen', account_balance: 0.0, + created_on: 'Thu, 20 Feb 2014 14:44:47 UTC +00:00', role_admin: false, role_suppliers: false, role_article_meta: true, role_finance: false, role_orders: true, next_weekly_tasks_number: 8, ignore_apple_restriction: false) +Workgroup.create!(id: 4, name: 'Assortiment', account_balance: 0.0, + created_on: 'Wed, 09 Apr 2014 12:24:55 UTC +00:00', role_admin: false, role_suppliers: true, role_article_meta: true, role_finance: false, role_orders: false, next_weekly_tasks_number: 8, ignore_apple_restriction: false) +Ordergroup.create!(id: 5, name: 'Admin Administrator', account_balance: 0.0, + created_on: 'Sat, 18 Jan 2014 00:38:48 UTC +00:00', role_admin: false, role_suppliers: false, role_article_meta: false, role_finance: false, role_orders: false, stats: { jobs_size: 0, orders_sum: 1021.74 }, next_weekly_tasks_number: 8, ignore_apple_restriction: true) +Ordergroup.create!(id: 6, name: "Peter's huis", account_balance: -0.35E2, + created_on: 'Sat, 25 Jan 2014 20:20:37 UTC +00:00', role_admin: false, role_suppliers: false, role_article_meta: false, role_finance: false, role_orders: false, contact_person: 'Piet Pieterssen', stats: { jobs_size: 0, orders_sum: 60.96 }, next_weekly_tasks_number: 8, ignore_apple_restriction: false) +Ordergroup.create!(id: 7, name: 'Jan Klaassen', account_balance: -0.35E2, + created_on: 'Mon, 27 Jan 2014 16:22:14 UTC +00:00', role_admin: false, role_suppliers: false, role_article_meta: false, role_finance: false, role_orders: false, contact_person: 'Jan Klaassen', stats: { jobs_size: 0, orders_sum: 0 }, next_weekly_tasks_number: 8, ignore_apple_restriction: false) +Ordergroup.create!(id: 8, name: 'John Doe', account_balance: 0.90E2, + created_on: 'Wed, 09 Apr 2014 12:23:29 UTC +00:00', role_admin: false, role_suppliers: false, role_article_meta: false, role_finance: false, role_orders: false, contact_person: 'John Doe', stats: { jobs_size: 0, orders_sum: 0 }, next_weekly_tasks_number: 8, ignore_apple_restriction: false) -Membership.create!(:group_id => 1, :user_id => 1) -Membership.create!(:group_id => 5, :user_id => 1) -Membership.create!(:group_id => 2, :user_id => 2) -Membership.create!(:group_id => 8, :user_id => 2) -Membership.create!(:group_id => 6, :user_id => 3) -Membership.create!(:group_id => 7, :user_id => 4) -Membership.create!(:group_id => 8, :user_id => 4) -Membership.create!(:group_id => 3, :user_id => 4) -Membership.create!(:group_id => 7, :user_id => 5) -Membership.create!(:group_id => 3, :user_id => 5) -Membership.create!(:group_id => 4, :user_id => 5) +Membership.create!(group_id: 1, user_id: 1) +Membership.create!(group_id: 5, user_id: 1) +Membership.create!(group_id: 2, user_id: 2) +Membership.create!(group_id: 8, user_id: 2) +Membership.create!(group_id: 6, user_id: 3) +Membership.create!(group_id: 7, user_id: 4) +Membership.create!(group_id: 8, user_id: 4) +Membership.create!(group_id: 3, user_id: 4) +Membership.create!(group_id: 7, user_id: 5) +Membership.create!(group_id: 3, user_id: 5) +Membership.create!(group_id: 4, user_id: 5) ## Orders & OrderArticles @@ -181,10 +308,15 @@ seed_group_orders ## Finances -FinancialTransactionType.create!(:id => 1, :name => "Foodcoop", :financial_transaction_class_id => 1) +FinancialTransactionType.create!(id: 1, name: 'Foodcoop', financial_transaction_class_id: 1) -FinancialTransaction.create!(:id => 1, :ordergroup_id => 5, :amount => -0.35E2, :note => "Membership fee for ordergroup", :user_id => 1, :created_on => 'Sat, 18 Jan 2014 00:38:48 UTC +00:00', :financial_transaction_type_id => 1) -FinancialTransaction.create!(:id => 3, :ordergroup_id => 6, :amount => -0.35E2, :note => "Membership fee for ordergroup", :user_id => 1, :created_on => 'Sat, 25 Jan 2014 20:20:37 UTC +00:00', :financial_transaction_type_id => 1) -FinancialTransaction.create!(:id => 4, :ordergroup_id => 7, :amount => -0.35E2, :note => "Membership fee for ordergroup", :user_id => 1, :created_on => 'Mon, 27 Jan 2014 16:22:14 UTC +00:00', :financial_transaction_type_id => 1) -FinancialTransaction.create!(:id => 5, :ordergroup_id => 5, :amount => 0.35E2, :note => "payment", :user_id => 2, :created_on => 'Wed, 05 Feb 2014 16:49:24 UTC +00:00', :financial_transaction_type_id => 1) -FinancialTransaction.create!(:id => 6, :ordergroup_id => 8, :amount => 0.90E2, :note => "Bank transfer", :user_id => 2, :created_on => 'Mon, 17 Feb 2014 16:19:34 UTC +00:00', :financial_transaction_type_id => 1) +FinancialTransaction.create!(id: 1, ordergroup_id: 5, amount: -0.35E2, + note: 'Membership fee for ordergroup', user_id: 1, created_on: 'Sat, 18 Jan 2014 00:38:48 UTC +00:00', financial_transaction_type_id: 1) +FinancialTransaction.create!(id: 3, ordergroup_id: 6, amount: -0.35E2, + note: 'Membership fee for ordergroup', user_id: 1, created_on: 'Sat, 25 Jan 2014 20:20:37 UTC +00:00', financial_transaction_type_id: 1) +FinancialTransaction.create!(id: 4, ordergroup_id: 7, amount: -0.35E2, + note: 'Membership fee for ordergroup', user_id: 1, created_on: 'Mon, 27 Jan 2014 16:22:14 UTC +00:00', financial_transaction_type_id: 1) +FinancialTransaction.create!(id: 5, ordergroup_id: 5, amount: 0.35E2, note: 'payment', user_id: 2, + created_on: 'Wed, 05 Feb 2014 16:49:24 UTC +00:00', financial_transaction_type_id: 1) +FinancialTransaction.create!(id: 6, ordergroup_id: 8, amount: 0.90E2, note: 'Bank transfer', user_id: 2, + created_on: 'Mon, 17 Feb 2014 16:19:34 UTC +00:00', financial_transaction_type_id: 1) diff --git a/lib/tasks/foodsoft.rake b/lib/tasks/foodsoft.rake index 760cd5bc..218bb39f 100644 --- a/lib/tasks/foodsoft.rake +++ b/lib/tasks/foodsoft.rake @@ -1,61 +1,61 @@ # put in here all foodsoft tasks # => :environment loads the environment an gives easy access to the application namespace :foodsoft do - desc "Finish ended orders" - task :finish_ended_orders => :environment do + desc 'Finish ended orders' + task finish_ended_orders: :environment do Order.finish_ended! end - desc "Notify users of upcoming tasks" - task :notify_upcoming_tasks => :environment do + desc 'Notify users of upcoming tasks' + task notify_upcoming_tasks: :environment do tasks = Task.where(done: false, due_date: 1.day.from_now.to_date) for task in tasks rake_say "Send notifications for #{task.name} to .." for user in task.users - if user.settings.notify['upcoming_tasks'] - Mailer.deliver_now_with_user_locale user do - Mailer.upcoming_tasks(user, task) - end + next unless user.settings.notify['upcoming_tasks'] + + Mailer.deliver_now_with_user_locale user do + Mailer.upcoming_tasks(user, task) end end end end - desc "Notify workgroup of upcoming weekly task" - task :notify_users_of_weekly_task => :environment do - tasks = Task.where(done: false, due_date: 7.day.from_now.to_date) + desc 'Notify workgroup of upcoming weekly task' + task notify_users_of_weekly_task: :environment do + tasks = Task.where(done: false, due_date: 7.days.from_now.to_date) for task in tasks - unless task.enough_users_assigned? - workgroup = task.workgroup - if workgroup - rake_say "Notify workgroup: #{workgroup.name} for task #{task.name}" - for user in workgroup.users - if user.receive_email? - Mailer.deliver_now_with_user_locale user do - Mailer.not_enough_users_assigned(task, user) - end - end - end + next if task.enough_users_assigned? + + workgroup = task.workgroup + next unless workgroup + + rake_say "Notify workgroup: #{workgroup.name} for task #{task.name}" + for user in workgroup.users + next unless user.receive_email? + + Mailer.deliver_now_with_user_locale user do + Mailer.not_enough_users_assigned(task, user) end end end end - desc "Create upcoming periodic tasks" - task :create_upcoming_periodic_tasks => :environment do + desc 'Create upcoming periodic tasks' + task create_upcoming_periodic_tasks: :environment do for tg in PeriodicTaskGroup.all created_until = tg.create_tasks_for_upfront_days rake_say "created until #{created_until}" end end - desc "Parse incoming email on stdin (options: RECIPIENT=foodcoop.handling)" - task :parse_reply_email => :environment do + desc 'Parse incoming email on stdin (options: RECIPIENT=foodcoop.handling)' + task parse_reply_email: :environment do FoodsoftMailReceiver.received ENV.fetch('RECIPIENT', nil), STDIN.read end - desc "Start STMP server for incoming email (options: SMTP_SERVER_PORT=2525, SMTP_SERVER_HOST=0.0.0.0)" - task :reply_email_smtp_server => :environment do + desc 'Start STMP server for incoming email (options: SMTP_SERVER_PORT=2525, SMTP_SERVER_HOST=0.0.0.0)' + task reply_email_smtp_server: :environment do port = ENV['SMTP_SERVER_PORT'].present? ? ENV['SMTP_SERVER_PORT'].to_i : 2525 host = ENV.fetch('SMTP_SERVER_HOST', nil) rake_say "Started SMTP server for incoming email on port #{port}." @@ -64,8 +64,8 @@ namespace :foodsoft do server.join end - desc "Import and assign bank transactions" - task :import_and_assign_bank_transactions => :environment do + desc 'Import and assign bank transactions' + task import_and_assign_bank_transactions: :environment do BankAccount.find_each do |ba| importer = ba.find_connector next unless importer diff --git a/lib/tasks/foodsoft_setup.rake b/lib/tasks/foodsoft_setup.rake index be2309ff..e7fe0a3b 100644 --- a/lib/tasks/foodsoft_setup.rake +++ b/lib/tasks/foodsoft_setup.rake @@ -9,14 +9,14 @@ module Colors end { - :black => 30, - :red => 31, - :green => 32, - :yellow => 33, - :blue => 34, - :magenta => 35, - :cyan => 36, - :white => 37 + black: 30, + red: 31, + green: 32, + yellow: 33, + blue: 34, + magenta: 35, + cyan: 36, + white: 37 }.each do |key, color_code| define_method key do |text| colorize(text, color_code) @@ -26,31 +26,31 @@ end include Colors namespace :foodsoft do - desc "Setup foodsoft" + desc 'Setup foodsoft' task :setup_development do - puts yellow "This task will help you get your foodcoop running in development." + puts yellow 'This task will help you get your foodcoop running in development.' setup_bundler setup_app_config setup_development setup_database setup_storage start_mailcatcher - puts yellow "All done! Your foodsoft setup should be running smoothly." + puts yellow 'All done! Your foodsoft setup should be running smoothly.' start_server end - desc "Setup foodsoft" + desc 'Setup foodsoft' task :setup_development_docker do - puts yellow "This task will help you get your foodcoop running in development via docker." + puts yellow 'This task will help you get your foodcoop running in development via docker.' setup_app_config setup_development setup_storage setup_run_rake_db_setup - puts yellow "All done! Your foodsoft setup should be running smoothly via docker." + puts yellow 'All done! Your foodsoft setup should be running smoothly via docker.' end namespace :setup do - desc "Initialize stock configuration" + desc 'Initialize stock configuration' task :stock_config do setup_app_config setup_storage @@ -60,39 +60,39 @@ namespace :foodsoft do end def setup_bundler - puts yellow "Installing bundler if not installed..." + puts yellow 'Installing bundler if not installed...' %x(if [ -z `which bundle` ]; then gem install bundler --no-rdoc --no-ri; fi) - puts yellow "Executing bundle install..." - %x(bundle install) + puts yellow 'Executing bundle install...' + `bundle install` end def setup_database file = 'config/database.yml' if ENV['DATABASE_URL'] - puts blue "DATABASE_URL found, please remember to also set it when running Foodsoft" + puts blue 'DATABASE_URL found, please remember to also set it when running Foodsoft' return nil end return nil if skip?(file) - database = ask("What kind of database do you use?\nOptions:\n(1) MySQL\n(2) SQLite", ["1", "2"]) - if database == "1" - puts yellow "Using MySQL..." - %x(cp -p #{Rails.root.join("#{file}.MySQL_SAMPLE")} #{Rails.root.join(file)}) - elsif database == "2" - puts yellow "Using SQLite..." - %x(cp -p #{Rails.root.join("#{file}.SQLite_SAMPLE")} #{Rails.root.join(file)}) + database = ask("What kind of database do you use?\nOptions:\n(1) MySQL\n(2) SQLite", %w[1 2]) + if database == '1' + puts yellow 'Using MySQL...' + `cp -p #{Rails.root.join("#{file}.MySQL_SAMPLE")} #{Rails.root.join(file)}` + elsif database == '2' + puts yellow 'Using SQLite...' + `cp -p #{Rails.root.join("#{file}.SQLite_SAMPLE")} #{Rails.root.join(file)}` end reminder(file) - puts blue "IMPORTANT: Edit (rake-generated) config/database.yml with valid username and password for EACH env before continuing!" - finished = ask("Finished?\nOptions:\n(y) Yes", ["y"]) + puts blue 'IMPORTANT: Edit (rake-generated) config/database.yml with valid username and password for EACH env before continuing!' + finished = ask("Finished?\nOptions:\n(y) Yes", ['y']) setup_run_rake_db_setup if finished end def setup_run_rake_db_setup - Rake::Task["db:setup"].reenable - db_setup = capture_stdout { Rake::Task["db:setup"].invoke } + Rake::Task['db:setup'].reenable + db_setup = capture_stdout { Rake::Task['db:setup'].invoke } puts db_setup end @@ -102,7 +102,7 @@ def setup_app_config return nil if skip?(file) puts yellow "Copying #{file}..." - %x(cp -p #{sample} #{Rails.root.join(file)}) + `cp -p #{sample} #{Rails.root.join(file)}` reminder(file) end @@ -111,7 +111,7 @@ def setup_development return nil if skip?(file) puts yellow "Copying #{file}..." - %x(cp -p #{Rails.root.join("#{file}.SAMPLE")} #{Rails.root.join(file)}) + `cp -p #{Rails.root.join("#{file}.SAMPLE")} #{Rails.root.join(file)}` reminder(file) end @@ -120,18 +120,18 @@ def setup_storage return nil if skip?(file) puts yellow "Copying #{file}..." - %x(cp -p #{Rails.root.join("#{file}.SAMPLE")} #{Rails.root.join(file)}) + `cp -p #{Rails.root.join("#{file}.SAMPLE")} #{Rails.root.join(file)}` reminder(file) end def start_mailcatcher return nil if ENV['MAILCATCHER_PORT'] # skip when it has an existing Docker container - mailcatcher = ask("Do you want to start mailcatcher?\nOptions:\n(y) Yes\n(n) No", ["y", "n"]) - if mailcatcher === "y" - puts yellow "Starting mailcatcher at http://localhost:1080..." - %x(mailcatcher) - end + mailcatcher = ask("Do you want to start mailcatcher?\nOptions:\n(y) Yes\n(n) No", %w[y n]) + return unless mailcatcher === 'y' + + puts yellow 'Starting mailcatcher at http://localhost:1080...' + `mailcatcher` end def start_server @@ -144,7 +144,7 @@ def ask(question, answers = false) puts question input = STDIN.gets.chomp if input.blank? || (answers && !answers.include?(input)) - puts red "Your Input is not valid. Try again!" + puts red 'Your Input is not valid. Try again!' input = ask(question, answers) end input @@ -152,8 +152,11 @@ end def skip?(file) output = false - skip = ask(cyan("We found #{file}!\nOptions:\n(1) Skip step\n(2) Force rewrite"), ["1", "2"]) if File.exists?(Rails.root.join(file)) - output = true if skip == "1" + if File.exist?(Rails.root.join(file)) + skip = ask(cyan("We found #{file}!\nOptions:\n(1) Skip step\n(2) Force rewrite"), + %w[1 2]) + end + output = true if skip == '1' output end diff --git a/lib/tasks/multicoops.rake b/lib/tasks/multicoops.rake index f0c76b8f..1e5ef44d 100644 --- a/lib/tasks/multicoops.rake +++ b/lib/tasks/multicoops.rake @@ -4,23 +4,21 @@ namespace :multicoops do desc 'Runs a specific rake task for each registered foodcoop, use rake multicoops:run TASK=db:migrate' - task :run => :environment do + task run: :environment do task_to_run = ENV.fetch('TASK', nil) last_error = nil FoodsoftConfig.each_coop do |coop| - begin - rake_say "Run '#{task_to_run}' for #{coop}" - Rake::Task[task_to_run].execute - rescue => error - last_error = error - ExceptionNotifier.notify_exception(error, data: { foodcoop: coop }) - end + rake_say "Run '#{task_to_run}' for #{coop}" + Rake::Task[task_to_run].execute + rescue StandardError => e + last_error = e + ExceptionNotifier.notify_exception(e, data: { foodcoop: coop }) end raise last_error if last_error end desc 'Runs a specific rake task for a single coop, use rake mutlicoops:run_single TASK=db:migrate FOODCOOP=demo' - task :run_single => :environment do + task run_single: :environment do task_to_run = ENV.fetch('TASK', nil) FoodsoftConfig.select_foodcoop ENV.fetch('FOODCOOP', nil) rake_say "Run '#{task_to_run}' for #{ENV.fetch('FOODCOOP', nil)}" diff --git a/lib/tasks/resque.rake b/lib/tasks/resque.rake index 2c50bf69..8705035a 100644 --- a/lib/tasks/resque.rake +++ b/lib/tasks/resque.rake @@ -1,32 +1,32 @@ -require "resque/tasks" +require 'resque/tasks' def run_worker(queue, count = 1) puts "Starting #{count} worker(s) with QUEUE: #{queue}" - ops = { :pgroup => true, :err => ["log/resque_worker_foodsoft_notifier.log", "a"], - :out => ["log/resque_worker_foodsoft_notifier.log", "a"] } - env_vars = { "QUEUE" => queue.to_s, "PIDFILE" => "tmp/pids/resque_worker_foodsoft_notifier.pid" } - count.times { + ops = { pgroup: true, err: ['log/resque_worker_foodsoft_notifier.log', 'a'], + out: ['log/resque_worker_foodsoft_notifier.log', 'a'] } + env_vars = { 'QUEUE' => queue.to_s, 'PIDFILE' => 'tmp/pids/resque_worker_foodsoft_notifier.pid' } + count.times do ## Using Kernel.spawn and Process.detach because regular system() call would ## cause the processes to quit when capistrano finishes - pid = spawn(env_vars, "bundle exec rake resque:work", ops) + pid = spawn(env_vars, 'bundle exec rake resque:work', ops) Process.detach(pid) - } + end end namespace :resque do - task :setup => :environment + task setup: :environment - desc "Restart running workers" + desc 'Restart running workers' task :restart_workers do Rake::Task['resque:stop_workers'].invoke Rake::Task['resque:start_workers'].invoke end - desc "Quit running workers" + desc 'Quit running workers' task :stop_workers do pids = File.read('tmp/pids/resque_worker_foodsoft_notifier.pid').split("\n") if pids.empty? - puts "No workers to kill" + puts 'No workers to kill' else syscmd = "kill -s QUIT #{pids.join(' ')}" puts "Running syscmd: #{syscmd}" @@ -34,8 +34,8 @@ namespace :resque do end end - desc "Start workers" + desc 'Start workers' task :start_workers do - run_worker("foodsoft_notifier") + run_worker('foodsoft_notifier') end end diff --git a/lib/tasks/rspec.rake b/lib/tasks/rspec.rake index 4f0c4081..a90fa407 100644 --- a/lib/tasks/rspec.rake +++ b/lib/tasks/rspec.rake @@ -2,7 +2,7 @@ begin require 'rspec/core/rake_task' task(:spec).clear RSpec::Core::RakeTask.new(:spec) - task :default => :spec + task default: :spec # Use `rspec` to run a single test. When a test fails in rake but not # with rspec, you can use the following to run a single test using rake: diff --git a/plugins/current_orders/app/controllers/current_orders/articles_controller.rb b/plugins/current_orders/app/controllers/current_orders/articles_controller.rb index ef23f332..51111f3f 100644 --- a/plugins/current_orders/app/controllers/current_orders/articles_controller.rb +++ b/plugins/current_orders/app/controllers/current_orders/articles_controller.rb @@ -1,6 +1,6 @@ class CurrentOrders::ArticlesController < ApplicationController before_action :authenticate_orders - before_action :find_order_and_order_article, only: [:index, :show] + before_action :find_order_and_order_article, only: %i[index show] def index # sometimes need to pass id as parameter for forms @@ -26,11 +26,11 @@ class CurrentOrders::ArticlesController < ApplicationController def find_order_and_order_article @current_orders = Order.finished_not_closed - unless params[:order_id].blank? + if params[:order_id].blank? + @order_articles = OrderArticle.where(order_id: @current_orders.all.map(&:id)) + else @order = Order.find(params[:order_id]) @order_articles = @order.order_articles - else - @order_articles = OrderArticle.where(order_id: @current_orders.all.map(&:id)) end @q = OrderArticle.ransack(params[:q]) @order_articles = @order_articles.ordered.merge(@q.result).includes(:article, :article_price) diff --git a/plugins/current_orders/app/controllers/current_orders/group_orders_controller.rb b/plugins/current_orders/app/controllers/current_orders/group_orders_controller.rb index 717ebba8..945bfd30 100644 --- a/plugins/current_orders/app/controllers/current_orders/group_orders_controller.rb +++ b/plugins/current_orders/app/controllers/current_orders/group_orders_controller.rb @@ -4,10 +4,10 @@ class CurrentOrders::GroupOrdersController < ApplicationController def index # XXX code duplication lib/foodsoft_current_orders/app/controllers/current_orders/ordergroups_controller.rb - @order_ids = Order.where(state: ['open', 'finished']).all.map(&:id) - @goas = GroupOrderArticle.includes(:group_order => :ordergroup).includes(:order_article) + @order_ids = Order.where(state: %w[open finished]).all.map(&:id) + @goas = GroupOrderArticle.includes(group_order: :ordergroup).includes(:order_article) .where(group_orders: { order_id: @order_ids, ordergroup_id: @ordergroup.id }).ordered - @articles_grouped_by_category = @goas.includes(:order_article => { :article => :article_category }) + @articles_grouped_by_category = @goas.includes(order_article: { article: :article_category }) .order('articles.name') .group_by { |a| a.order_article.article.article_category.name } .sort { |a, b| a[0] <=> b[0] } @@ -18,8 +18,8 @@ class CurrentOrders::GroupOrdersController < ApplicationController # XXX code duplication from GroupOrdersController def ensure_ordergroup_member @ordergroup = @current_user.ordergroup - if @ordergroup.nil? - redirect_to root_url, :alert => I18n.t('group_orders.errors.no_member') - end + return unless @ordergroup.nil? + + redirect_to root_url, alert: I18n.t('group_orders.errors.no_member') end end diff --git a/plugins/current_orders/app/controllers/current_orders/ordergroups_controller.rb b/plugins/current_orders/app/controllers/current_orders/ordergroups_controller.rb index 708016a9..94390a0a 100644 --- a/plugins/current_orders/app/controllers/current_orders/ordergroups_controller.rb +++ b/plugins/current_orders/app/controllers/current_orders/ordergroups_controller.rb @@ -1,6 +1,6 @@ class CurrentOrders::OrdergroupsController < ApplicationController before_action :authenticate_orders - before_action :find_group_orders, only: [:index, :show] + before_action :find_group_orders, only: %i[index show] def index # sometimes need to pass id as parameter for forms @@ -34,8 +34,11 @@ class CurrentOrders::OrdergroupsController < ApplicationController @all_ordergroups.sort_by! { |o| @ordered_group_ids.include?(o.id) ? o.name : "ZZZZZ#{o.name}" } @ordergroup = Ordergroup.find(params[:id]) unless params[:id].nil? - @goas = GroupOrderArticle.includes(:group_order, :order_article => [:article, :article_price]) - .where(group_orders: { order_id: @order_ids, ordergroup_id: @ordergroup.id }).ordered.all unless @ordergroup.nil? + return if @ordergroup.nil? + + @goas = GroupOrderArticle.includes(:group_order, order_article: %i[article article_price]) + .where(group_orders: { order_id: @order_ids, + ordergroup_id: @ordergroup.id }).ordered.all end helper_method \ diff --git a/plugins/current_orders/app/documents/multiple_orders_by_articles.rb b/plugins/current_orders/app/documents/multiple_orders_by_articles.rb index 48d8a058..95b2e3b6 100644 --- a/plugins/current_orders/app/documents/multiple_orders_by_articles.rb +++ b/plugins/current_orders/app/documents/multiple_orders_by_articles.rb @@ -77,11 +77,11 @@ class MultipleOrdersByArticles < OrderPdf .includes(:article).references(:article) .reorder('order_articles.order_id, articles.name') .preload(:article_price) # preload not join, just in case it went missing - .preload(:order, :group_order_articles => { :group_order => :ordergroup }) + .preload(:order, group_order_articles: { group_order: :ordergroup }) end - def each_order_article - order_articles.find_each_with_order(batch_size: BATCH_SIZE) { |oa| yield oa } + def each_order_article(&block) + order_articles.find_each_with_order(batch_size: BATCH_SIZE, &block) end def group_order_articles_for(order_article) @@ -90,7 +90,7 @@ class MultipleOrdersByArticles < OrderPdf goas end - def each_group_order_article_for(group_order) - group_order_articles_for(group_order).each { |goa| yield goa } + def each_group_order_article_for(group_order, &block) + group_order_articles_for(group_order).each(&block) end end diff --git a/plugins/current_orders/app/documents/multiple_orders_by_groups.rb b/plugins/current_orders/app/documents/multiple_orders_by_groups.rb index 0c9eefa9..a09ef1d4 100644 --- a/plugins/current_orders/app/documents/multiple_orders_by_groups.rb +++ b/plugins/current_orders/app/documents/multiple_orders_by_groups.rb @@ -113,7 +113,7 @@ class MultipleOrdersByGroups < OrderPdf s end - def each_ordergroup + def each_ordergroup(&block) ordergroups.find_in_batches_with_order(batch_size: BATCH_SIZE) do |ordergroups| @group_order_article_batch = GroupOrderArticle .joins(:group_order) @@ -121,8 +121,8 @@ class MultipleOrdersByGroups < OrderPdf .where(group_orders: { ordergroup_id: ordergroups.map(&:id) }) .order('group_orders.order_id, group_order_articles.id') .preload(group_orders: { order: :supplier }) - .preload(order_article: [:article, :article_price, :order]) - ordergroups.each { |ordergroup| yield ordergroup } + .preload(order_article: %i[article article_price order]) + ordergroups.each(&block) end end @@ -130,7 +130,7 @@ class MultipleOrdersByGroups < OrderPdf @group_order_article_batch.select { |goa| goa.group_order.ordergroup_id == ordergroup.id } end - def each_group_order_article_for(ordergroup) - group_order_articles_for(ordergroup).each { |goa| yield goa } + def each_group_order_article_for(ordergroup, &block) + group_order_articles_for(ordergroup).each(&block) end end diff --git a/plugins/current_orders/app/helpers/current_orders_helper.rb b/plugins/current_orders/app/helpers/current_orders_helper.rb index 3bbab482..c62493a1 100644 --- a/plugins/current_orders/app/helpers/current_orders_helper.rb +++ b/plugins/current_orders/app/helpers/current_orders_helper.rb @@ -6,7 +6,9 @@ module CurrentOrdersHelper elsif funds == 0 I18n.t('helpers.current_orders.pay_none') else - content_tag :b, I18n.t('helpers.current_orders.pay_amount', amount: number_to_currency(-ordergroup.get_available_funds)) + content_tag :b, + I18n.t('helpers.current_orders.pay_amount', + amount: number_to_currency(-ordergroup.get_available_funds)) end end end diff --git a/plugins/current_orders/config/routes.rb b/plugins/current_orders/config/routes.rb index f642fc31..aeb2b014 100644 --- a/plugins/current_orders/config/routes.rb +++ b/plugins/current_orders/config/routes.rb @@ -1,27 +1,27 @@ Rails.application.routes.draw do scope '/:foodcoop' do namespace :current_orders do - resources :ordergroups, :only => [:index, :show] do + resources :ordergroups, only: %i[index show] do collection do get :show_on_group_order_article_create get :show_on_group_order_article_update end end - resources :articles, :only => [:index, :show] do + resources :articles, only: %i[index show] do collection do get :show_on_group_order_article_create end end - resource :orders, :only => [:show] do + resource :orders, only: [:show] do collection do get :my get :receive end end - resources :group_orders, :only => [:index] + resources :group_orders, only: [:index] end end end diff --git a/plugins/current_orders/foodsoft_current_orders.gemspec b/plugins/current_orders/foodsoft_current_orders.gemspec index c444fbec..f8e22ce6 100644 --- a/plugins/current_orders/foodsoft_current_orders.gemspec +++ b/plugins/current_orders/foodsoft_current_orders.gemspec @@ -1,20 +1,21 @@ -$:.push File.expand_path("../lib", __FILE__) +$:.push File.expand_path('lib', __dir__) # Maintain your gem's version: -require "foodsoft_current_orders/version" +require 'foodsoft_current_orders/version' # Describe your gem and declare its dependencies: Gem::Specification.new do |s| - s.name = "foodsoft_current_orders" + s.name = 'foodsoft_current_orders' s.version = FoodsoftCurrentOrders::VERSION - s.authors = ["wvengen"] - s.email = ["dev-voko@willem.engen.nl"] - s.homepage = "https://github.com/foodcoop-adam/foodsoft" - s.summary = "Quick support for working on all currently active orders in foodsoft." - s.description = "" + s.authors = ['wvengen'] + s.email = ['dev-voko@willem.engen.nl'] + s.homepage = 'https://github.com/foodcoop-adam/foodsoft' + s.summary = 'Quick support for working on all currently active orders in foodsoft.' + s.description = '' - s.files = Dir["{app,config,db,lib}/**/*"] + ["Rakefile", "README.md"] + s.files = Dir['{app,config,db,lib}/**/*'] + ['Rakefile', 'README.md'] - s.add_dependency "rails" - s.add_dependency "deface", "~> 1.0" + s.add_dependency 'rails' + s.add_dependency 'deface', '~> 1.0' + s.metadata['rubygems_mfa_required'] = 'true' end diff --git a/plugins/current_orders/lib/foodsoft_current_orders.rb b/plugins/current_orders/lib/foodsoft_current_orders.rb index 5a9a7c82..8227bea4 100644 --- a/plugins/current_orders/lib/foodsoft_current_orders.rb +++ b/plugins/current_orders/lib/foodsoft_current_orders.rb @@ -1,5 +1,5 @@ -require "deface" -require "foodsoft_current_orders/engine" +require 'deface' +require 'foodsoft_current_orders/engine' module FoodsoftCurrentOrders def self.enabled? diff --git a/plugins/current_orders/lib/foodsoft_current_orders/engine.rb b/plugins/current_orders/lib/foodsoft_current_orders/engine.rb index 07427b56..c6236acb 100644 --- a/plugins/current_orders/lib/foodsoft_current_orders/engine.rb +++ b/plugins/current_orders/lib/foodsoft_current_orders/engine.rb @@ -4,12 +4,15 @@ module FoodsoftCurrentOrders return unless FoodsoftCurrentOrders.enabled? return if primary[:orders].nil? - cond = Proc.new { current_user.role_orders? } + cond = proc { current_user.role_orders? } [ SimpleNavigation::Item.new(primary, :stage_divider, nil, nil, class: 'divider', if: cond), - SimpleNavigation::Item.new(primary, :current_orders_receive, I18n.t('current_orders.navigation.receive'), context.receive_current_orders_orders_path, if: cond), - SimpleNavigation::Item.new(primary, :current_orders_articles, I18n.t('current_orders.navigation.articles'), context.current_orders_articles_path, if: cond), - SimpleNavigation::Item.new(primary, :current_orders_ordergroups, I18n.t('current_orders.navigation.ordergroups'), context.current_orders_ordergroups_path, if: cond) + SimpleNavigation::Item.new(primary, :current_orders_receive, I18n.t('current_orders.navigation.receive'), + context.receive_current_orders_orders_path, if: cond), + SimpleNavigation::Item.new(primary, :current_orders_articles, I18n.t('current_orders.navigation.articles'), + context.current_orders_articles_path, if: cond), + SimpleNavigation::Item.new(primary, :current_orders_ordergroups, + I18n.t('current_orders.navigation.ordergroups'), context.current_orders_ordergroups_path, if: cond) ].each { |i| primary[:orders].sub_navigation.items << i } end end diff --git a/plugins/current_orders/lib/foodsoft_current_orders/version.rb b/plugins/current_orders/lib/foodsoft_current_orders/version.rb index af58aa9c..9e916ba6 100644 --- a/plugins/current_orders/lib/foodsoft_current_orders/version.rb +++ b/plugins/current_orders/lib/foodsoft_current_orders/version.rb @@ -1,3 +1,3 @@ module FoodsoftCurrentOrders - VERSION = "0.0.1" + VERSION = '0.0.1' end diff --git a/plugins/discourse/Rakefile b/plugins/discourse/Rakefile index cb56e2e5..abec3d42 100755 --- a/plugins/discourse/Rakefile +++ b/plugins/discourse/Rakefile @@ -20,7 +20,7 @@ RDoc::Task.new(:rdoc) do |rdoc| rdoc.rdoc_files.include('lib/**/*.rb') end -APP_RAKEFILE = File.expand_path("../test/dummy/Rakefile", __FILE__) +APP_RAKEFILE = File.expand_path('test/dummy/Rakefile', __dir__) load 'rails/tasks/engine.rake' Bundler::GemHelper.install_tasks @@ -34,4 +34,4 @@ Rake::TestTask.new(:test) do |t| t.verbose = false end -task :default => :test +task default: :test diff --git a/plugins/discourse/app/controllers/discourse_controller.rb b/plugins/discourse/app/controllers/discourse_controller.rb index 5a65f61c..0e62f14d 100644 --- a/plugins/discourse/app/controllers/discourse_controller.rb +++ b/plugins/discourse/app/controllers/discourse_controller.rb @@ -11,7 +11,7 @@ class DiscourseController < ApplicationController def redirect_to_with_payload(url, payload) base64_payload = Base64.strict_encode64 payload.to_query - sso = CGI::escape base64_payload + sso = CGI.escape base64_payload sig = get_hmac_hex_string base64_payload redirect_to "#{url}#{url.include?('?') ? '&' : '?'}sso=#{sso}&sig=#{sig}" end @@ -21,7 +21,7 @@ class DiscourseController < ApplicationController payload.symbolize_keys! end - def get_hmac_hex_string payload + def get_hmac_hex_string(payload) discourse_sso_secret = FoodsoftConfig[:discourse_sso_secret] OpenSSL::HMAC.hexdigest 'sha256', discourse_sso_secret, payload end diff --git a/plugins/discourse/app/controllers/discourse_login_controller.rb b/plugins/discourse/app/controllers/discourse_login_controller.rb index 1c8fe938..bd7a81e3 100644 --- a/plugins/discourse/app/controllers/discourse_login_controller.rb +++ b/plugins/discourse/app/controllers/discourse_login_controller.rb @@ -5,7 +5,7 @@ class DiscourseLoginController < DiscourseController def initiate discourse_url = FoodsoftConfig[:discourse_url] - nonce = SecureRandom.hex() + nonce = SecureRandom.hex return_sso_url = url_for(action: :callback, only_path: false) session[:discourse_sso_nonce] = nonce @@ -36,7 +36,7 @@ class DiscourseLoginController < DiscourseController user.save! login_and_redirect_to_return_to user, notice: I18n.t('discourse.callback.logged_in') - rescue => error - redirect_to login_url, alert: error.to_s + rescue StandardError => e + redirect_to login_url, alert: e.to_s end end diff --git a/plugins/discourse/app/controllers/discourse_sso_controller.rb b/plugins/discourse/app/controllers/discourse_sso_controller.rb index e8f742b6..04dc8d1c 100644 --- a/plugins/discourse/app/controllers/discourse_sso_controller.rb +++ b/plugins/discourse/app/controllers/discourse_sso_controller.rb @@ -17,7 +17,7 @@ class DiscourseSsoController < DiscourseController external_id: "#{FoodsoftConfig.scope}/#{current_user.id}", username: current_user.nick, name: current_user.name - rescue => error - redirect_to root_url, alert: error.to_s + rescue StandardError => e + redirect_to root_url, alert: e.to_s end end diff --git a/plugins/discourse/foodsoft_discourse.gemspec b/plugins/discourse/foodsoft_discourse.gemspec index 4400bcf4..1e6113ce 100644 --- a/plugins/discourse/foodsoft_discourse.gemspec +++ b/plugins/discourse/foodsoft_discourse.gemspec @@ -1,20 +1,21 @@ -$:.push File.expand_path("../lib", __FILE__) +$:.push File.expand_path('lib', __dir__) # Maintain your gem's version: -require "foodsoft_discourse/version" +require 'foodsoft_discourse/version' # Describe your gem and declare its dependencies: Gem::Specification.new do |s| - s.name = "foodsoft_discourse" + s.name = 'foodsoft_discourse' s.version = FoodsoftDiscourse::VERSION - s.authors = ["paroga"] - s.email = ["paroga@paroga.com"] - s.homepage = "https://github.com/foodcoops/foodsoft" - s.summary = "Discourse plugin for foodsoft." - s.description = "Allow SSO login via Discourse" + s.authors = ['paroga'] + s.email = ['paroga@paroga.com'] + s.homepage = 'https://github.com/foodcoops/foodsoft' + s.summary = 'Discourse plugin for foodsoft.' + s.description = 'Allow SSO login via Discourse' - s.files = Dir["{app,config,db,lib}/**/*"] + ["Rakefile", "README.md"] + s.files = Dir['{app,config,db,lib}/**/*'] + ['Rakefile', 'README.md'] - s.add_dependency "rails" - s.add_dependency "deface", "~> 1.0" + s.add_dependency 'rails' + s.add_dependency 'deface', '~> 1.0' + s.metadata['rubygems_mfa_required'] = 'true' end diff --git a/plugins/discourse/lib/foodsoft_discourse/redirect_to_login.rb b/plugins/discourse/lib/foodsoft_discourse/redirect_to_login.rb index 901979b1..6ddfdb13 100644 --- a/plugins/discourse/lib/foodsoft_discourse/redirect_to_login.rb +++ b/plugins/discourse/lib/foodsoft_discourse/redirect_to_login.rb @@ -2,7 +2,7 @@ module FoodsoftDiscourse module RedirectToLogin def self.included(base) # :nodoc: base.class_eval do - alias foodsoft_discourse_orig_redirect_to_login redirect_to_login + alias_method :foodsoft_discourse_orig_redirect_to_login, :redirect_to_login def redirect_to_login(options = {}) if FoodsoftDiscourse.enabled? && !FoodsoftConfig[:discourse_sso] @@ -18,5 +18,5 @@ end # modify existing helper ActiveSupport.on_load(:after_initialize) do - Concerns::Auth.send :include, FoodsoftDiscourse::RedirectToLogin + Concerns::Auth.include FoodsoftDiscourse::RedirectToLogin end diff --git a/plugins/discourse/lib/foodsoft_discourse/version.rb b/plugins/discourse/lib/foodsoft_discourse/version.rb index 2b8d4138..60d38a51 100644 --- a/plugins/discourse/lib/foodsoft_discourse/version.rb +++ b/plugins/discourse/lib/foodsoft_discourse/version.rb @@ -1,3 +1,3 @@ module FoodsoftDiscourse - VERSION = "0.0.1" + VERSION = '0.0.1' end diff --git a/plugins/documents/Rakefile b/plugins/documents/Rakefile index 2834c5f3..861a530a 100755 --- a/plugins/documents/Rakefile +++ b/plugins/documents/Rakefile @@ -20,7 +20,7 @@ RDoc::Task.new(:rdoc) do |rdoc| rdoc.rdoc_files.include('lib/**/*.rb') end -APP_RAKEFILE = File.expand_path("../test/dummy/Rakefile", __FILE__) +APP_RAKEFILE = File.expand_path('test/dummy/Rakefile', __dir__) load 'rails/tasks/engine.rake' Bundler::GemHelper.install_tasks @@ -34,4 +34,4 @@ Rake::TestTask.new(:test) do |t| t.verbose = false end -task :default => :test +task default: :test diff --git a/plugins/documents/app/controllers/documents_controller.rb b/plugins/documents/app/controllers/documents_controller.rb index b97470a5..7290ef3c 100644 --- a/plugins/documents/app/controllers/documents_controller.rb +++ b/plugins/documents/app/controllers/documents_controller.rb @@ -4,16 +4,16 @@ class DocumentsController < ApplicationController before_action -> { require_plugin_enabled FoodsoftDocuments } def index - if params["sort"] - sort = case params["sort"] - when "name" then "data IS NULL DESC, name" - when "created_at" then "created_at" - when "name_reverse" then "data IS NULL, name DESC" - when "created_at_reverse" then "created_at DESC" + sort = if params['sort'] + case params['sort'] + when 'name' then 'data IS NULL DESC, name' + when 'created_at' then 'created_at' + when 'name_reverse' then 'data IS NULL, name DESC' + when 'created_at_reverse' then 'created_at DESC' end - else - sort = "data IS NULL DESC, name" - end + else + 'data IS NULL DESC, name' + end @documents = Document.where(parent: @document).page(params[:page]).per(@per_page).order(sort) end @@ -34,22 +34,22 @@ class DocumentsController < ApplicationController if @document.name.empty? name = File.basename(data.original_filename) - @document.name = name.gsub(/[^\w\.\-]/, '_') + @document.name = name.gsub(/[^\w.-]/, '_') end end @document.created_by = current_user @document.save! redirect_to @document.parent || documents_path, notice: t('.notice') - rescue => error - redirect_to @document.parent || documents_path, alert: t('.error', error: error.message) + rescue StandardError => e + redirect_to @document.parent || documents_path, alert: t('.error', error: e.message) end def update @document = Document.find(params[:id]) @document.update_attribute(:parent_id, params[:parent_id]) redirect_to @document.parent || documents_path, notice: t('.notice') - rescue => error - redirect_to @document.parent || documents_path, alert: t('errors.general_msg', msg: error.message) + rescue StandardError => e + redirect_to @document.parent || documents_path, alert: t('errors.general_msg', msg: e.message) end def destroy @@ -60,8 +60,8 @@ class DocumentsController < ApplicationController else redirect_to documents_path, alert: t('.no_right') end - rescue => error - redirect_to documents_path, alert: t('.error', error: error.message) + rescue StandardError => e + redirect_to documents_path, alert: t('.error', error: e.message) end def show diff --git a/plugins/documents/foodsoft_documents.gemspec b/plugins/documents/foodsoft_documents.gemspec index 1301fa11..63d3b428 100644 --- a/plugins/documents/foodsoft_documents.gemspec +++ b/plugins/documents/foodsoft_documents.gemspec @@ -1,21 +1,22 @@ -$:.push File.expand_path("../lib", __FILE__) +$:.push File.expand_path('lib', __dir__) # Maintain your gem's version: -require "foodsoft_documents/version" +require 'foodsoft_documents/version' # Describe your gem and declare its dependencies: Gem::Specification.new do |s| - s.name = "foodsoft_documents" + s.name = 'foodsoft_documents' s.version = FoodsoftDocuments::VERSION - s.authors = ["paroga"] - s.email = ["paroga@paroga.com"] - s.homepage = "https://github.com/foodcoops/foodsoft" - s.summary = "Documents plugin for foodsoft." - s.description = "Adds simple document management to foodsoft." + s.authors = ['paroga'] + s.email = ['paroga@paroga.com'] + s.homepage = 'https://github.com/foodcoops/foodsoft' + s.summary = 'Documents plugin for foodsoft.' + s.description = 'Adds simple document management to foodsoft.' - s.files = Dir["{app,config,db,lib}/**/*"] + ["Rakefile", "README.md"] + s.files = Dir['{app,config,db,lib}/**/*'] + ['Rakefile', 'README.md'] - s.add_dependency "rails" - s.add_dependency "deface", "~> 1.0" - s.add_dependency "ruby-filemagic" + s.add_dependency 'rails' + s.add_dependency 'deface', '~> 1.0' + s.add_dependency 'ruby-filemagic' + s.metadata['rubygems_mfa_required'] = 'true' end diff --git a/plugins/documents/lib/foodsoft_documents/engine.rb b/plugins/documents/lib/foodsoft_documents/engine.rb index 970b3aa5..de81904c 100644 --- a/plugins/documents/lib/foodsoft_documents/engine.rb +++ b/plugins/documents/lib/foodsoft_documents/engine.rb @@ -8,9 +8,9 @@ module FoodsoftDocuments sub_nav.items << SimpleNavigation::Item.new(primary, :documents, I18n.t('navigation.documents'), context.documents_path) # move to right before tasks item - if i = sub_nav.items.index(sub_nav[:tasks]) - sub_nav.items.insert(i, sub_nav.items.delete_at(-1)) - end + return unless i = sub_nav.items.index(sub_nav[:tasks]) + + sub_nav.items.insert(i, sub_nav.items.delete_at(-1)) end def default_foodsoft_config(cfg) diff --git a/plugins/documents/lib/foodsoft_documents/version.rb b/plugins/documents/lib/foodsoft_documents/version.rb index 6e57dbb3..7096e468 100644 --- a/plugins/documents/lib/foodsoft_documents/version.rb +++ b/plugins/documents/lib/foodsoft_documents/version.rb @@ -1,3 +1,3 @@ module FoodsoftDocuments - VERSION = "0.0.1" + VERSION = '0.0.1' end diff --git a/plugins/links/Rakefile b/plugins/links/Rakefile index fb6356f8..a9902fd3 100755 --- a/plugins/links/Rakefile +++ b/plugins/links/Rakefile @@ -20,7 +20,7 @@ RDoc::Task.new(:rdoc) do |rdoc| rdoc.rdoc_files.include('lib/**/*.rb') end -APP_RAKEFILE = File.expand_path("../test/dummy/Rakefile", __FILE__) +APP_RAKEFILE = File.expand_path('test/dummy/Rakefile', __dir__) load 'rails/tasks/engine.rake' Bundler::GemHelper.install_tasks @@ -34,4 +34,4 @@ Rake::TestTask.new(:test) do |t| t.verbose = false end -task :default => :test +task default: :test diff --git a/plugins/links/app/controllers/admin/links_controller.rb b/plugins/links/app/controllers/admin/links_controller.rb index 2b6a7a35..dc8b359e 100644 --- a/plugins/links/app/controllers/admin/links_controller.rb +++ b/plugins/links/app/controllers/admin/links_controller.rb @@ -37,8 +37,8 @@ class Admin::LinksController < Admin::BaseController link = Link.find(params[:id]) link.destroy! redirect_to admin_links_path - rescue => error - redirect_to admin_links_path, I18n.t('errors.general_msg', msg: error.message) + rescue StandardError => e + redirect_to admin_links_path, I18n.t('errors.general_msg', msg: e.message) end private diff --git a/plugins/links/app/controllers/links_controller.rb b/plugins/links/app/controllers/links_controller.rb index 143fc63d..27615517 100644 --- a/plugins/links/app/controllers/links_controller.rb +++ b/plugins/links/app/controllers/links_controller.rb @@ -5,9 +5,7 @@ class LinksController < ApplicationController link = Link.find(params[:id]) url = link.url - if link.workgroup && !current_user.role_admin? && !link.workgroup.member?(current_user) - return deny_access - end + return deny_access if link.workgroup && !current_user.role_admin? && !link.workgroup.member?(current_user) if link.indirect uri = URI.parse url @@ -19,11 +17,9 @@ class LinksController < ApplicationController url = result.header['Location'] - unless url - return redirect_to root_url, alert: t('.indirect_no_location') - end + return redirect_to root_url, alert: t('.indirect_no_location') unless url end - redirect_to url, status: 302 + redirect_to url, status: :found end end diff --git a/plugins/links/foodsoft_links.gemspec b/plugins/links/foodsoft_links.gemspec index e899c082..c879f4b1 100644 --- a/plugins/links/foodsoft_links.gemspec +++ b/plugins/links/foodsoft_links.gemspec @@ -1,20 +1,21 @@ -$:.push File.expand_path("../lib", __FILE__) +$:.push File.expand_path('lib', __dir__) # Maintain your gem's version: -require "foodsoft_links/version" +require 'foodsoft_links/version' # Describe your gem and declare its dependencies: Gem::Specification.new do |s| - s.name = "foodsoft_links" + s.name = 'foodsoft_links' s.version = FoodsoftLinks::VERSION - s.authors = ["paroga"] - s.email = ["paroga@paroga.com"] - s.homepage = "https://github.com/foodcoops/foodsoft" - s.summary = "Links plugin for foodsoft." - s.description = "Adds simple link management to foodsoft." + s.authors = ['paroga'] + s.email = ['paroga@paroga.com'] + s.homepage = 'https://github.com/foodcoops/foodsoft' + s.summary = 'Links plugin for foodsoft.' + s.description = 'Adds simple link management to foodsoft.' - s.files = Dir["{app,config,db,lib}/**/*"] + ["Rakefile", "README.md"] + s.files = Dir['{app,config,db,lib}/**/*'] + ['Rakefile', 'README.md'] - s.add_dependency "rails" - s.add_dependency "deface", "~> 1.0" + s.add_dependency 'rails' + s.add_dependency 'deface', '~> 1.0' + s.metadata['rubygems_mfa_required'] = 'true' end diff --git a/plugins/links/lib/foodsoft_links/engine.rb b/plugins/links/lib/foodsoft_links/engine.rb index ab6d9175..52672597 100644 --- a/plugins/links/lib/foodsoft_links/engine.rb +++ b/plugins/links/lib/foodsoft_links/engine.rb @@ -1,7 +1,7 @@ module FoodsoftLinks class Engine < ::Rails::Engine def navigation(primary, context) - primary.item :links, I18n.t('navigation.links'), '#', if: Proc.new { visble_links(context).any? } do |subnav| + primary.item :links, I18n.t('navigation.links'), '#', if: proc { visble_links(context).any? } do |subnav| visble_links(context).each do |link| subnav.item link.id, link.name, context.link_path(link) end @@ -11,15 +11,15 @@ module FoodsoftLinks primary.items.insert(i, primary.items.delete_at(-1)) end - unless primary[:admin].nil? - sub_nav = primary[:admin].sub_navigation - sub_nav.items << - SimpleNavigation::Item.new(primary, :links, I18n.t('navigation.admin.links'), context.admin_links_path) - # move to right before config item - if i = sub_nav.items.index(sub_nav[:config]) - sub_nav.items.insert(i, sub_nav.items.delete_at(-1)) - end - end + return if primary[:admin].nil? + + sub_nav = primary[:admin].sub_navigation + sub_nav.items << + SimpleNavigation::Item.new(primary, :links, I18n.t('navigation.admin.links'), context.admin_links_path) + # move to right before config item + return unless i = sub_nav.items.index(sub_nav[:config]) + + sub_nav.items.insert(i, sub_nav.items.delete_at(-1)) end def visble_links(context) diff --git a/plugins/links/lib/foodsoft_links/version.rb b/plugins/links/lib/foodsoft_links/version.rb index 20341ca4..707cfdb3 100644 --- a/plugins/links/lib/foodsoft_links/version.rb +++ b/plugins/links/lib/foodsoft_links/version.rb @@ -1,3 +1,3 @@ module FoodsoftLinks - VERSION = "0.0.1" + VERSION = '0.0.1' end diff --git a/plugins/messages/Rakefile b/plugins/messages/Rakefile index ac014bdd..9e2cbeab 100755 --- a/plugins/messages/Rakefile +++ b/plugins/messages/Rakefile @@ -20,7 +20,7 @@ RDoc::Task.new(:rdoc) do |rdoc| rdoc.rdoc_files.include('lib/**/*.rb') end -APP_RAKEFILE = File.expand_path("../test/dummy/Rakefile", __FILE__) +APP_RAKEFILE = File.expand_path('test/dummy/Rakefile', __dir__) load 'rails/tasks/engine.rake' Bundler::GemHelper.install_tasks @@ -34,4 +34,4 @@ Rake::TestTask.new(:test) do |t| t.verbose = false end -task :default => :test +task default: :test diff --git a/plugins/messages/app/controllers/admin/messagegroups_controller.rb b/plugins/messages/app/controllers/admin/messagegroups_controller.rb index cce57474..76fbbfb9 100644 --- a/plugins/messages/app/controllers/admin/messagegroups_controller.rb +++ b/plugins/messages/app/controllers/admin/messagegroups_controller.rb @@ -4,7 +4,7 @@ class Admin::MessagegroupsController < Admin::BaseController def index @messagegroups = Messagegroup.order('name ASC') # if somebody uses the search field: - @messagegroups = @messagegroups.where('name LIKE ?', "%#{params[:query]}%") unless params[:query].blank? + @messagegroups = @messagegroups.where('name LIKE ?', "%#{params[:query]}%") if params[:query].present? @messagegroups = @messagegroups.page(params[:page]).per(@per_page) end @@ -13,7 +13,7 @@ class Admin::MessagegroupsController < Admin::BaseController @messagegroup = Messagegroup.find(params[:id]) @messagegroup.destroy redirect_to admin_messagegroups_url, notice: t('admin.messagegroups.destroy.notice') - rescue => error - redirect_to admin_messagegroups_url, alert: t('admin.messagegroups.destroy.error', error: error.message) + rescue StandardError => e + redirect_to admin_messagegroups_url, alert: t('admin.messagegroups.destroy.error', error: e.message) end end diff --git a/plugins/messages/app/controllers/messagegroups_controller.rb b/plugins/messages/app/controllers/messagegroups_controller.rb index e9ba6770..7629eab8 100644 --- a/plugins/messages/app/controllers/messagegroups_controller.rb +++ b/plugins/messages/app/controllers/messagegroups_controller.rb @@ -1,17 +1,17 @@ class MessagegroupsController < ApplicationController def index - @messagegroups = Messagegroup.order("name") + @messagegroups = Messagegroup.order('name') end def join @messagegroup = Messagegroup.find(params[:id]) @messagegroup.users << current_user - redirect_to messagegroups_url, :notice => I18n.t('messagegroups.join.notice') + redirect_to messagegroups_url, notice: I18n.t('messagegroups.join.notice') end def leave @messagegroup = Messagegroup.find(params[:id]) @messagegroup.users.destroy(current_user) - redirect_to messagegroups_url, :notice => I18n.t('messagegroups.leave.notice') + redirect_to messagegroups_url, notice: I18n.t('messagegroups.leave.notice') end end diff --git a/plugins/messages/app/controllers/messages_controller.rb b/plugins/messages/app/controllers/messages_controller.rb index 628f145b..3a003752 100644 --- a/plugins/messages/app/controllers/messages_controller.rb +++ b/plugins/messages/app/controllers/messages_controller.rb @@ -10,21 +10,20 @@ class MessagesController < ApplicationController def new @message = Message.new(params[:message]) - if @message.reply_to - original_message = Message.find(@message.reply_to) - if original_message.reply_to - @message.reply_to = original_message.reply_to - end - if original_message.is_readable_for?(current_user) - @message.add_recipients [original_message.sender_id] - @message.group_id = original_message.group_id - @message.private = original_message.private - @message.subject = I18n.t('messages.model.reply_subject', :subject => original_message.subject) - @message.body = I18n.t('messages.model.reply_header', :user => original_message.sender.display, :when => I18n.l(original_message.created_at, :format => :short)) + "\n" - original_message.body.each_line { |l| @message.body += I18n.t('messages.model.reply_indent', :line => l) } - else - redirect_to new_message_url, alert: I18n.t('messages.new.error_private') - end + return unless @message.reply_to + + original_message = Message.find(@message.reply_to) + @message.reply_to = original_message.reply_to if original_message.reply_to + if original_message.is_readable_for?(current_user) + @message.add_recipients [original_message.sender_id] + @message.group_id = original_message.group_id + @message.private = original_message.private + @message.subject = I18n.t('messages.model.reply_subject', subject: original_message.subject) + @message.body = I18n.t('messages.model.reply_header', user: original_message.sender.display, + when: I18n.l(original_message.created_at, format: :short)) + "\n" + original_message.body.each_line { |l| @message.body += I18n.t('messages.model.reply_indent', line: l) } + else + redirect_to new_message_url, alert: I18n.t('messages.new.error_private') end end @@ -33,18 +32,18 @@ class MessagesController < ApplicationController @message = @current_user.send_messages.new(params[:message]) if @message.save DeliverMessageJob.perform_later(@message) - redirect_to messages_url, :notice => I18n.t('messages.create.notice') + redirect_to messages_url, notice: I18n.t('messages.create.notice') else - render :action => 'new' + render action: 'new' end end # Shows a single message. def show @message = Message.find(params[:id]) - unless @message.is_readable_for?(current_user) - redirect_to messages_url, alert: I18n.t('messages.new.error_private') - end + return if @message.is_readable_for?(current_user) + + redirect_to messages_url, alert: I18n.t('messages.new.error_private') end def toggle_private diff --git a/plugins/messages/app/helpers/messages_helper.rb b/plugins/messages/app/helpers/messages_helper.rb index d5371fe4..adb8fe88 100644 --- a/plugins/messages/app/helpers/messages_helper.rb +++ b/plugins/messages/app/helpers/messages_helper.rb @@ -1,11 +1,11 @@ module MessagesHelper def format_subject(message, length) if message.subject.length > length - subject = truncate(message.subject, :length => length) - body = "" + subject = truncate(message.subject, length: length) + body = '' else subject = message.subject - body = truncate(message.body, :length => length - subject.length) + body = truncate(message.body, length: length - subject.length) end "#{link_to(h(subject), message)} #{h(body)}".html_safe end diff --git a/plugins/messages/app/mail_receivers/messages_mail_receiver.rb b/plugins/messages/app/mail_receivers/messages_mail_receiver.rb index e9ca99f3..006c3f8d 100644 --- a/plugins/messages/app/mail_receivers/messages_mail_receiver.rb +++ b/plugins/messages/app/mail_receivers/messages_mail_receiver.rb @@ -1,4 +1,4 @@ -require "email_reply_trimmer" +require 'email_reply_trimmer' class MessagesMailReceiver def self.regexp @@ -9,29 +9,25 @@ class MessagesMailReceiver @message = Message.find_by_id(match[:message_id]) @user = User.find_by_id(match[:user_id]) - raise "Message could not be found" if @message.nil? - raise "User could not be found" if @user.nil? + raise 'Message could not be found' if @message.nil? + raise 'User could not be found' if @user.nil? hash = @message.mail_hash_for_user(@user) - raise "Hash does not match expectations" unless hash.casecmp(match[:hash]) == 0 + raise 'Hash does not match expectations' unless hash.casecmp(match[:hash]) == 0 end def received(data) mail = Mail.new data mail_part = get_mail_part(mail) - raise "No valid content could be found" if mail_part.nil? + raise 'No valid content could be found' if mail_part.nil? body = mail_part.body.decoded - unless mail_part.content_type_parameters.nil? - body = body.force_encoding mail_part.content_type_parameters[:charset] - end + body = body.force_encoding mail_part.content_type_parameters[:charset] unless mail_part.content_type_parameters.nil? - if MIME::Type.simplified(mail_part.content_type) == "text/html" - body = Nokogiri::HTML(body).text - end + body = Nokogiri::HTML(body).text if MIME::Type.simplified(mail_part.content_type) == 'text/html' - body.encode!(Encoding::default_internal) + body.encode!(Encoding.default_internal) body = EmailReplyTrimmer.trim(body) raise BlankBodyException if body.empty? @@ -39,16 +35,16 @@ class MessagesMailReceiver group: @message.group, private: @message.private, received_email: data - if @message.reply_to - message.reply_to_message = @message.reply_to_message - else - message.reply_to_message = @message - end - if mail.subject - message.subject = mail.subject.gsub("[#{FoodsoftConfig[:name]}] ", "") - else - message.subject = I18n.t('messages.model.reply_subject', subject: message.reply_to_message.subject) - end + message.reply_to_message = if @message.reply_to + @message.reply_to_message + else + @message + end + message.subject = if mail.subject + mail.subject.gsub("[#{FoodsoftConfig[:name]}] ", '') + else + I18n.t('messages.model.reply_subject', subject: message.reply_to_message.subject) + end message.add_recipients [@message.sender_id] message.save! @@ -64,9 +60,7 @@ class MessagesMailReceiver for part in mail.parts part = get_mail_part(part) content_type = MIME::Type.simplified(part.content_type) - if content_type == "text/plain" || !mail_part && content_type == "text/html" - mail_part = part - end + mail_part = part if content_type == 'text/plain' || (!mail_part && content_type == 'text/html') end mail_part end diff --git a/plugins/messages/app/models/message.rb b/plugins/messages/app/models/message.rb index f6b03c10..b6554322 100644 --- a/plugins/messages/app/models/message.rb +++ b/plugins/messages/app/models/message.rb @@ -1,17 +1,17 @@ -require "base32" +require 'base32' class Message < ApplicationRecord - belongs_to :sender, class_name: 'User', foreign_key: 'sender_id' - belongs_to :group, optional: true, class_name: 'Group', foreign_key: 'group_id' + belongs_to :sender, class_name: 'User' + belongs_to :group, optional: true, class_name: 'Group' belongs_to :reply_to_message, optional: true, class_name: 'Message', foreign_key: 'reply_to' has_many :message_recipients, dependent: :destroy has_many :recipients, through: :message_recipients, source: :user attr_accessor :send_method, :recipient_tokens, :order_id - scope :threads, -> { where(:reply_to => nil) } - scope :thread, ->(id) { where("id = ? OR reply_to = ?", id, id) } - scope :readable_for, ->(user) { + scope :threads, -> { where(reply_to: nil) } + scope :thread, ->(id) { where('id = ? OR reply_to = ?', id, id) } + scope :readable_for, lambda { |user| user_id = user.try(&:id) joins(:message_recipients) @@ -20,7 +20,7 @@ class Message < ApplicationRecord } validates_presence_of :message_recipients, :subject, :body - validates_length_of :subject, :in => 1..255 + validates_length_of :subject, in: 1..255 after_initialize do @recipients_ids ||= [] @@ -33,7 +33,7 @@ class Message < ApplicationRecord def create_message_recipients user_ids = @recipients_ids user_ids += User.undeleted.pluck(:id) if send_method == 'all' - user_ids += Group.find(group_id).users.pluck(:id) unless group_id.blank? + user_ids += Group.find(group_id).users.pluck(:id) if group_id.present? user_ids += Order.find(order_id).users_ordered.pluck(:id) if send_method == 'order' user_ids.uniq.each do |user_id| @@ -47,7 +47,7 @@ class Message < ApplicationRecord end def group_id=(group_id) - group = Group.find(group_id) unless group_id.blank? + group = Group.find(group_id) if group_id.present? if group @send_method = 'workgroup' if group.type == 'Workgroup' @send_method = 'ordergroup' if group.type == 'Ordergroup' @@ -96,29 +96,29 @@ class Message < ApplicationRecord def mail_hash_for_user(user) digest = Digest::SHA1.new - digest.update self.id.to_s - digest.update ":" + digest.update id.to_s + digest.update ':' digest.update salt - digest.update ":" + digest.update ':' digest.update user.id.to_s Base32.encode digest.digest end # Returns true if this message is a system message, i.e. was sent automatically by Foodsoft itself. def system_message? - self.sender_id.nil? + sender_id.nil? end def sender_name - system_message? ? I18n.t('layouts.foodsoft') : sender.display rescue "?" + system_message? ? I18n.t('layouts.foodsoft') : sender.display + rescue StandardError + '?' end - def recipients_ids - @recipients_ids - end + attr_reader :recipients_ids def last_reply - Message.where(reply_to: self.id).order(:created_at).last + Message.where(reply_to: id).order(:created_at).last end def is_readable_for?(user) @@ -135,6 +135,6 @@ class Message < ApplicationRecord private def create_salt - self.salt = [Array.new(6) { rand(256).chr }.join].pack("m").chomp + self.salt = [Array.new(6) { rand(256).chr }.join].pack('m').chomp end end diff --git a/plugins/messages/app/models/message_recipient.rb b/plugins/messages/app/models/message_recipient.rb index e205ea5b..671b557d 100644 --- a/plugins/messages/app/models/message_recipient.rb +++ b/plugins/messages/app/models/message_recipient.rb @@ -2,5 +2,5 @@ class MessageRecipient < ActiveRecord::Base belongs_to :message belongs_to :user - enum email_state: [:pending, :sent, :skipped] + enum email_state: %i[pending sent skipped] end diff --git a/plugins/messages/app/models/messagegroup.rb b/plugins/messages/app/models/messagegroup.rb index 7c7f6c03..93666dd5 100644 --- a/plugins/messages/app/models/messagegroup.rb +++ b/plugins/messages/app/models/messagegroup.rb @@ -1,5 +1,3 @@ class Messagegroup < Group validates_uniqueness_of :name - - protected end diff --git a/plugins/messages/config/routes.rb b/plugins/messages/config/routes.rb index d66eebdd..6d276428 100644 --- a/plugins/messages/config/routes.rb +++ b/plugins/messages/config/routes.rb @@ -1,13 +1,13 @@ Rails.application.routes.draw do scope '/:foodcoop' do - resources :messages, :only => [:index, :show, :new, :create] do + resources :messages, only: %i[index show new create] do member do get :thread post :toggle_private end end - resources :message_threads, :only => [:index, :show] + resources :message_threads, only: %i[index show] resources :messagegroups, only: [:index] do member do diff --git a/plugins/messages/db/migrate/20160226000000_add_email_to_message.rb b/plugins/messages/db/migrate/20160226000000_add_email_to_message.rb index 411600c7..034b023b 100644 --- a/plugins/messages/db/migrate/20160226000000_add_email_to_message.rb +++ b/plugins/messages/db/migrate/20160226000000_add_email_to_message.rb @@ -1,6 +1,6 @@ class AddEmailToMessage < ActiveRecord::Migration[4.2] def change add_column :messages, :salt, :string - add_column :messages, :received_email, :binary, :limit => 1.megabyte + add_column :messages, :received_email, :binary, limit: 1.megabyte end end diff --git a/plugins/messages/foodsoft_messages.gemspec b/plugins/messages/foodsoft_messages.gemspec index 0dfc7163..e7967191 100644 --- a/plugins/messages/foodsoft_messages.gemspec +++ b/plugins/messages/foodsoft_messages.gemspec @@ -1,25 +1,26 @@ -$:.push File.expand_path("../lib", __FILE__) +$:.push File.expand_path('lib', __dir__) # Maintain your gem's version: -require "foodsoft_messages/version" +require 'foodsoft_messages/version' # Describe your gem and declare its dependencies: Gem::Specification.new do |s| - s.name = "foodsoft_messages" + s.name = 'foodsoft_messages' s.version = FoodsoftMessages::VERSION - s.authors = ["robwa"] - s.email = ["foodsoft-messages@ini.tiative.net"] - s.homepage = "https://github.com/foodcoops/foodsoft" - s.summary = "Messaging plugin for foodsoft." - s.description = "Adds the ability to exchange messages to foodsoft." + s.authors = ['robwa'] + s.email = ['foodsoft-messages@ini.tiative.net'] + s.homepage = 'https://github.com/foodcoops/foodsoft' + s.summary = 'Messaging plugin for foodsoft.' + s.description = 'Adds the ability to exchange messages to foodsoft.' - s.files = Dir["{app,config,db,lib}/**/*"] + ["Rakefile", "README.md"] + s.files = Dir['{app,config,db,lib}/**/*'] + ['Rakefile', 'README.md'] - s.add_dependency "rails" - s.add_dependency "base32" - s.add_dependency "deface", "~> 1.0" - s.add_dependency "email_reply_trimmer" - s.add_dependency "mail" + s.add_dependency 'rails' + s.add_dependency 'base32' + s.add_dependency 'deface', '~> 1.0' + s.add_dependency 'email_reply_trimmer' + s.add_dependency 'mail' - s.add_development_dependency "sqlite3" + s.add_development_dependency 'sqlite3' + s.metadata['rubygems_mfa_required'] = 'true' end diff --git a/plugins/messages/lib/foodsoft_messages.rb b/plugins/messages/lib/foodsoft_messages.rb index b457c8f1..9fc71928 100644 --- a/plugins/messages/lib/foodsoft_messages.rb +++ b/plugins/messages/lib/foodsoft_messages.rb @@ -1,7 +1,7 @@ -require "foodsoft_messages/engine" -require "foodsoft_messages/mail_receiver" -require "foodsoft_messages/user_link" -require "deface" +require 'foodsoft_messages/engine' +require 'foodsoft_messages/mail_receiver' +require 'foodsoft_messages/user_link' +require 'deface' module FoodsoftMessages # Return whether messages are used or not. diff --git a/plugins/messages/lib/foodsoft_messages/engine.rb b/plugins/messages/lib/foodsoft_messages/engine.rb index 0f67abb7..f054ada6 100644 --- a/plugins/messages/lib/foodsoft_messages/engine.rb +++ b/plugins/messages/lib/foodsoft_messages/engine.rb @@ -12,15 +12,16 @@ module FoodsoftMessages sub_nav.items.insert(i, sub_nav.items.delete_at(-1)) end end - unless primary[:admin].nil? - sub_nav = primary[:admin].sub_navigation - sub_nav.items << - SimpleNavigation::Item.new(primary, :messagegroups, I18n.t('navigation.admin.messagegroups'), context.admin_messagegroups_path) - # move to right before config item - if i = sub_nav.items.index(sub_nav[:config]) - sub_nav.items.insert(i, sub_nav.items.delete_at(-1)) - end - end + return if primary[:admin].nil? + + sub_nav = primary[:admin].sub_navigation + sub_nav.items << + SimpleNavigation::Item.new(primary, :messagegroups, I18n.t('navigation.admin.messagegroups'), + context.admin_messagegroups_path) + # move to right before config item + return unless i = sub_nav.items.index(sub_nav[:config]) + + sub_nav.items.insert(i, sub_nav.items.delete_at(-1)) end def default_foodsoft_config(cfg) diff --git a/plugins/messages/lib/foodsoft_messages/user_link.rb b/plugins/messages/lib/foodsoft_messages/user_link.rb index bfab42b6..6fcf99c4 100644 --- a/plugins/messages/lib/foodsoft_messages/user_link.rb +++ b/plugins/messages/lib/foodsoft_messages/user_link.rb @@ -8,7 +8,7 @@ module FoodsoftMessages show_user user else link_to show_user(user), new_message_path('message[mail_to]' => user.id), - :title => I18n.t('helpers.messages.write_message') + title: I18n.t('helpers.messages.write_message') end end end @@ -18,5 +18,5 @@ end # modify existing helper ActiveSupport.on_load(:after_initialize) do - ApplicationHelper.send :include, FoodsoftMessages::UserLink + ApplicationHelper.include FoodsoftMessages::UserLink end diff --git a/plugins/messages/lib/foodsoft_messages/version.rb b/plugins/messages/lib/foodsoft_messages/version.rb index 2da75575..6209100d 100644 --- a/plugins/messages/lib/foodsoft_messages/version.rb +++ b/plugins/messages/lib/foodsoft_messages/version.rb @@ -1,3 +1,3 @@ module FoodsoftMessages - VERSION = "0.0.1" + VERSION = '0.0.1' end diff --git a/plugins/polls/Rakefile b/plugins/polls/Rakefile index 2834c5f3..861a530a 100755 --- a/plugins/polls/Rakefile +++ b/plugins/polls/Rakefile @@ -20,7 +20,7 @@ RDoc::Task.new(:rdoc) do |rdoc| rdoc.rdoc_files.include('lib/**/*.rb') end -APP_RAKEFILE = File.expand_path("../test/dummy/Rakefile", __FILE__) +APP_RAKEFILE = File.expand_path('test/dummy/Rakefile', __dir__) load 'rails/tasks/engine.rake' Bundler::GemHelper.install_tasks @@ -34,4 +34,4 @@ Rake::TestTask.new(:test) do |t| t.verbose = false end -task :default => :test +task default: :test diff --git a/plugins/polls/app/controllers/polls_controller.rb b/plugins/polls/app/controllers/polls_controller.rb index b0c1a9eb..110c4d3f 100644 --- a/plugins/polls/app/controllers/polls_controller.rb +++ b/plugins/polls/app/controllers/polls_controller.rb @@ -27,9 +27,9 @@ class PollsController < ApplicationController def edit @poll = Poll.find(params[:id]) - if user_has_no_right - redirect_to polls_path, alert: t('.no_right') - end + return unless user_has_no_right + + redirect_to polls_path, alert: t('.no_right') end def update @@ -53,8 +53,8 @@ class PollsController < ApplicationController @poll.destroy redirect_to polls_path, notice: t('.notice') end - rescue => error - redirect_to polls_path, alert: t('.error', error: error.message) + rescue StandardError => e + redirect_to polls_path, alert: t('.error', error: e.message) end def vote @@ -73,25 +73,25 @@ class PollsController < ApplicationController @poll_vote = @poll.poll_votes.where(attributes).first_or_initialize - if request.post? - @poll_vote.update!(note: params[:note], user: current_user) + return unless request.post? - if @poll.single_select? - choices = {} - choice = params[:choice] - choices[choice] = '1' if choice - else - choices = params[:choices].try(:to_h) || {} - end + @poll_vote.update!(note: params[:note], user: current_user) - @poll_vote.poll_choices = choices.map do |choice, value| - poll_choice = @poll_vote.poll_choices.where(choice: choice).first_or_initialize - poll_choice.update!(value: value) - poll_choice - end - - redirect_to @poll + if @poll.single_select? + choices = {} + choice = params[:choice] + choices[choice] = '1' if choice + else + choices = params[:choices].try(:to_h) || {} end + + @poll_vote.poll_choices = choices.map do |choice, value| + poll_choice = @poll_vote.poll_choices.where(choice: choice).first_or_initialize + poll_choice.update!(value: value) + poll_choice + end + + redirect_to @poll end private diff --git a/plugins/polls/db/migrate/20181110000000_create_polls.rb b/plugins/polls/db/migrate/20181110000000_create_polls.rb index 990e75f0..120b7eef 100644 --- a/plugins/polls/db/migrate/20181110000000_create_polls.rb +++ b/plugins/polls/db/migrate/20181110000000_create_polls.rb @@ -24,14 +24,14 @@ class CreatePolls < ActiveRecord::Migration[4.2] t.references :ordergroup t.text :note t.timestamps - t.index [:poll_id, :user_id, :ordergroup_id], unique: true + t.index %i[poll_id user_id ordergroup_id], unique: true end create_table :poll_choices do |t| t.references :poll_vote, null: false t.integer :choice, null: false t.integer :value, null: false - t.index [:poll_vote_id, :choice], unique: true + t.index %i[poll_vote_id choice], unique: true end end end diff --git a/plugins/polls/db/migrate/20181120000000_increase_choices_size.rb b/plugins/polls/db/migrate/20181120000000_increase_choices_size.rb index d809e3ea..621863dd 100644 --- a/plugins/polls/db/migrate/20181120000000_increase_choices_size.rb +++ b/plugins/polls/db/migrate/20181120000000_increase_choices_size.rb @@ -1,5 +1,5 @@ class IncreaseChoicesSize < ActiveRecord::Migration[4.2] def up - change_column :polls, :choices, :text, limit: 65535 + change_column :polls, :choices, :text, limit: 65_535 end end diff --git a/plugins/polls/foodsoft_polls.gemspec b/plugins/polls/foodsoft_polls.gemspec index 63e7db29..607a1276 100644 --- a/plugins/polls/foodsoft_polls.gemspec +++ b/plugins/polls/foodsoft_polls.gemspec @@ -1,20 +1,21 @@ -$:.push File.expand_path("../lib", __FILE__) +$:.push File.expand_path('lib', __dir__) # Maintain your gem's version: -require "foodsoft_polls/version" +require 'foodsoft_polls/version' # Describe your gem and declare its dependencies: Gem::Specification.new do |s| - s.name = "foodsoft_polls" + s.name = 'foodsoft_polls' s.version = FoodsoftPolls::VERSION - s.authors = ["paroga"] - s.email = ["paroga@paroga.com"] - s.homepage = "https://github.com/foodcoops/foodsoft" - s.summary = "Polls plugin for foodsoft." - s.description = "Adds possibility to do polls with foodsoft." + s.authors = ['paroga'] + s.email = ['paroga@paroga.com'] + s.homepage = 'https://github.com/foodcoops/foodsoft' + s.summary = 'Polls plugin for foodsoft.' + s.description = 'Adds possibility to do polls with foodsoft.' - s.files = Dir["{app,config,db,lib}/**/*"] + ["Rakefile", "README.md"] + s.files = Dir['{app,config,db,lib}/**/*'] + ['Rakefile', 'README.md'] - s.add_dependency "rails" - s.add_dependency "deface", "~> 1.0" + s.add_dependency 'rails' + s.add_dependency 'deface', '~> 1.0' + s.metadata['rubygems_mfa_required'] = 'true' end diff --git a/plugins/polls/lib/foodsoft_polls/engine.rb b/plugins/polls/lib/foodsoft_polls/engine.rb index a76399f0..e4812345 100644 --- a/plugins/polls/lib/foodsoft_polls/engine.rb +++ b/plugins/polls/lib/foodsoft_polls/engine.rb @@ -8,9 +8,9 @@ module FoodsoftPolls sub_nav.items << SimpleNavigation::Item.new(primary, :polls, I18n.t('navigation.polls'), context.polls_path) # move to right before tasks item - if i = sub_nav.items.index(sub_nav[:tasks]) - sub_nav.items.insert(i, sub_nav.items.delete_at(-1)) - end + return unless i = sub_nav.items.index(sub_nav[:tasks]) + + sub_nav.items.insert(i, sub_nav.items.delete_at(-1)) end end end diff --git a/plugins/polls/lib/foodsoft_polls/version.rb b/plugins/polls/lib/foodsoft_polls/version.rb index 5f3fb96d..84369283 100644 --- a/plugins/polls/lib/foodsoft_polls/version.rb +++ b/plugins/polls/lib/foodsoft_polls/version.rb @@ -1,3 +1,3 @@ module FoodsoftPolls - VERSION = "0.0.1" + VERSION = '0.0.1' end diff --git a/plugins/printer/Rakefile b/plugins/printer/Rakefile index 1c9d9839..fbf50e1d 100755 --- a/plugins/printer/Rakefile +++ b/plugins/printer/Rakefile @@ -20,7 +20,7 @@ RDoc::Task.new(:rdoc) do |rdoc| rdoc.rdoc_files.include('lib/**/*.rb') end -APP_RAKEFILE = File.expand_path("../test/dummy/Rakefile", __FILE__) +APP_RAKEFILE = File.expand_path('test/dummy/Rakefile', __dir__) load 'rails/tasks/engine.rake' Bundler::GemHelper.install_tasks @@ -34,4 +34,4 @@ Rake::TestTask.new(:test) do |t| t.verbose = false end -task :default => :test +task default: :test diff --git a/plugins/printer/app/controllers/printer_controller.rb b/plugins/printer/app/controllers/printer_controller.rb index 178787da..78f13377 100644 --- a/plugins/printer/app/controllers/printer_controller.rb +++ b/plugins/printer/app/controllers/printer_controller.rb @@ -37,9 +37,7 @@ class PrinterController < ApplicationController job = PrinterJob.unfinished.find_by_id(json[:id]) return unless job - if json[:state] - job.add_update! json[:state], json[:message] - end + job.add_update! json[:state], json[:message] if json[:state] job.finish! if json[:finish] end diff --git a/plugins/printer/app/controllers/printer_jobs_controller.rb b/plugins/printer/app/controllers/printer_jobs_controller.rb index 37c864e9..4ba13803 100644 --- a/plugins/printer/app/controllers/printer_jobs_controller.rb +++ b/plugins/printer/app/controllers/printer_jobs_controller.rb @@ -15,7 +15,7 @@ class PrinterJobsController < ApplicationController state = order.open? ? 'queued' : 'ready' count = 0 PrinterJob.transaction do - %w(articles fax groups matrix).each do |document| + %w[articles fax groups matrix].each do |document| next unless FoodsoftConfig["printer_print_order_#{document}"] job = PrinterJob.create! order: order, document: document, created_by: current_user @@ -47,7 +47,7 @@ class PrinterJobsController < ApplicationController job = PrinterJob.find(params[:id]) job.finish! current_user redirect_to printer_jobs_path, notice: t('.notice') - rescue => error - redirect_to printer_jobs_path, t('errors.general_msg', msg: error.message) + rescue StandardError => e + redirect_to printer_jobs_path, t('errors.general_msg', msg: e.message) end end diff --git a/plugins/printer/config/routes.rb b/plugins/printer/config/routes.rb index c81fc786..298ddfea 100644 --- a/plugins/printer/config/routes.rb +++ b/plugins/printer/config/routes.rb @@ -4,7 +4,7 @@ Rails.application.routes.draw do get :socket, on: :collection end - resources :printer_jobs, only: [:index, :create, :show, :destroy] do + resources :printer_jobs, only: %i[index create show destroy] do post :requeue, on: :member get :document, on: :member end diff --git a/plugins/printer/db/migrate/20181201000000_create_printer_jobs.rb b/plugins/printer/db/migrate/20181201000000_create_printer_jobs.rb index ee7665e4..36d175c5 100644 --- a/plugins/printer/db/migrate/20181201000000_create_printer_jobs.rb +++ b/plugins/printer/db/migrate/20181201000000_create_printer_jobs.rb @@ -15,6 +15,6 @@ class CreatePrinterJobs < ActiveRecord::Migration[4.2] t.text :message end - add_index :printer_job_updates, [:printer_job_id, :created_at] + add_index :printer_job_updates, %i[printer_job_id created_at] end end diff --git a/plugins/printer/foodsoft_printer.gemspec b/plugins/printer/foodsoft_printer.gemspec index d0eea89a..a6e54455 100644 --- a/plugins/printer/foodsoft_printer.gemspec +++ b/plugins/printer/foodsoft_printer.gemspec @@ -1,21 +1,22 @@ -$:.push File.expand_path("../lib", __FILE__) +$:.push File.expand_path('lib', __dir__) # Maintain your gem's version: -require "foodsoft_printer/version" +require 'foodsoft_printer/version' # Describe your gem and declare its dependencies: Gem::Specification.new do |s| - s.name = "foodsoft_printer" + s.name = 'foodsoft_printer' s.version = FoodsoftPrinter::VERSION - s.authors = ["paroga"] - s.email = ["paroga@paroga.com"] - s.homepage = "https://github.com/foodcoops/foodsoft" - s.summary = "Printer plugin for foodsoft." - s.description = "Add a printer queue to foodsoft." + s.authors = ['paroga'] + s.email = ['paroga@paroga.com'] + s.homepage = 'https://github.com/foodcoops/foodsoft' + s.summary = 'Printer plugin for foodsoft.' + s.description = 'Add a printer queue to foodsoft.' - s.files = Dir["{app,config,db,lib}/**/*"] + ["Rakefile", "README.md"] + s.files = Dir['{app,config,db,lib}/**/*'] + ['Rakefile', 'README.md'] - s.add_dependency "rails" - s.add_dependency "deface", "~> 1.0" - s.add_dependency "tubesock" + s.add_dependency 'rails' + s.add_dependency 'deface', '~> 1.0' + s.add_dependency 'tubesock' + s.metadata['rubygems_mfa_required'] = 'true' end diff --git a/plugins/printer/lib/foodsoft_printer/engine.rb b/plugins/printer/lib/foodsoft_printer/engine.rb index 22144e30..8f1f00cc 100644 --- a/plugins/printer/lib/foodsoft_printer/engine.rb +++ b/plugins/printer/lib/foodsoft_printer/engine.rb @@ -3,18 +3,19 @@ module FoodsoftPrinter def navigation(primary, context) return unless FoodsoftPrinter.enabled? - unless primary[:orders].nil? - sub_nav = primary[:orders].sub_navigation - sub_nav.items << - SimpleNavigation::Item.new(primary, :printer_jobs, I18n.t('navigation.orders.printer_jobs'), context.printer_jobs_path) - end + return if primary[:orders].nil? + + sub_nav = primary[:orders].sub_navigation + sub_nav.items << + SimpleNavigation::Item.new(primary, :printer_jobs, I18n.t('navigation.orders.printer_jobs'), + context.printer_jobs_path) end def default_foodsoft_config(cfg) cfg[:use_printer] = false end - initializer 'foodsoft_printer.order_printer_jobs' do |app| + initializer 'foodsoft_printer.order_printer_jobs' do |_app| if Rails.configuration.cache_classes OrderPrinterJobs.install else diff --git a/plugins/printer/lib/foodsoft_printer/order_printer_jobs.rb b/plugins/printer/lib/foodsoft_printer/order_printer_jobs.rb index 7501a69e..4c7eeeaa 100644 --- a/plugins/printer/lib/foodsoft_printer/order_printer_jobs.rb +++ b/plugins/printer/lib/foodsoft_printer/order_printer_jobs.rb @@ -4,14 +4,14 @@ module FoodsoftPrinter base.class_eval do has_many :printer_jobs, dependent: :destroy - alias foodsoft_printer_orig_finish! finish! + alias_method :foodsoft_printer_orig_finish!, :finish! def finish!(user) foodsoft_printer_orig_finish!(user) - unless finished? - printer_jobs.unfinished.each do |job| - job.add_update! 'ready' - end + return if finished? + + printer_jobs.unfinished.each do |job| + job.add_update! 'ready' end end end diff --git a/plugins/printer/lib/foodsoft_printer/version.rb b/plugins/printer/lib/foodsoft_printer/version.rb index 17bd39cb..e9d2ad84 100644 --- a/plugins/printer/lib/foodsoft_printer/version.rb +++ b/plugins/printer/lib/foodsoft_printer/version.rb @@ -1,3 +1,3 @@ module FoodsoftPrinter - VERSION = "0.0.1" + VERSION = '0.0.1' end diff --git a/plugins/uservoice/foodsoft_uservoice.gemspec b/plugins/uservoice/foodsoft_uservoice.gemspec index f33760fb..4defe395 100644 --- a/plugins/uservoice/foodsoft_uservoice.gemspec +++ b/plugins/uservoice/foodsoft_uservoice.gemspec @@ -1,20 +1,21 @@ -$:.push File.expand_path("../lib", __FILE__) +$:.push File.expand_path('lib', __dir__) # Maintain your gem's version: -require "foodsoft_uservoice/version" +require 'foodsoft_uservoice/version' # Describe your gem and declare its dependencies: Gem::Specification.new do |s| - s.name = "foodsoft_uservoice" + s.name = 'foodsoft_uservoice' s.version = FoodsoftUservoice::VERSION - s.authors = ["wvengen"] - s.email = ["dev-foodsoft@willem.engen.nl"] - s.homepage = "https://github.com/foodcoops/foodsoft" - s.summary = "Uservoice plugin for foodsoft." - s.description = "Adds a uservoice feedback button to foodsoft." + s.authors = ['wvengen'] + s.email = ['dev-foodsoft@willem.engen.nl'] + s.homepage = 'https://github.com/foodcoops/foodsoft' + s.summary = 'Uservoice plugin for foodsoft.' + s.description = 'Adds a uservoice feedback button to foodsoft.' - s.files = Dir["{app,config,db,lib}/**/*"] + ["README.md"] + s.files = Dir['{app,config,db,lib}/**/*'] + ['README.md'] - s.add_dependency "rails" - s.add_dependency "content_for_in_controllers" + s.add_dependency 'rails' + s.add_dependency 'content_for_in_controllers' + s.metadata['rubygems_mfa_required'] = 'true' end diff --git a/plugins/uservoice/lib/foodsoft_uservoice.rb b/plugins/uservoice/lib/foodsoft_uservoice.rb index b4718445..2d5b764b 100644 --- a/plugins/uservoice/lib/foodsoft_uservoice.rb +++ b/plugins/uservoice/lib/foodsoft_uservoice.rb @@ -1,5 +1,5 @@ -require "content_for_in_controllers" -require "foodsoft_uservoice/engine" +require 'content_for_in_controllers' +require 'foodsoft_uservoice/engine' module FoodsoftUservoice # enabled when configured, but can still be disabled by use_uservoice option @@ -19,11 +19,11 @@ module FoodsoftUservoice # include uservoice javascript api_key = FoodsoftConfig[:uservoice]['api_key'] - js_pre = "UserVoice=window.UserVoice||[];" + js_pre = 'UserVoice=window.UserVoice||[];' js_load = "var uv=document.createElement('script');uv.type='text/javascript';uv.async=true;uv.src='//widget.uservoice.com/#{view_context.j api_key}.js';var s=document.getElementsByTagName('script')[0];s.parentNode.insertBefore(uv,s);" # configuration - sections = FoodsoftConfig[:uservoice].reject { |k, v| k == 'api_key' } + sections = FoodsoftConfig[:uservoice].except('api_key') sections.each_pair do |k, v| if k == 'identify' v['id'] = current_user.try(:id) if v.include?('id') @@ -48,5 +48,5 @@ module FoodsoftUservoice end ActiveSupport.on_load(:after_initialize) do - ApplicationController.send :include, FoodsoftUservoice::LoadUservoice + ApplicationController.include FoodsoftUservoice::LoadUservoice end diff --git a/plugins/uservoice/lib/foodsoft_uservoice/version.rb b/plugins/uservoice/lib/foodsoft_uservoice/version.rb index 8d78e3de..e806ff1d 100644 --- a/plugins/uservoice/lib/foodsoft_uservoice/version.rb +++ b/plugins/uservoice/lib/foodsoft_uservoice/version.rb @@ -1,3 +1,3 @@ module FoodsoftUservoice - VERSION = "0.0.1" + VERSION = '0.0.1' end diff --git a/plugins/wiki/Rakefile b/plugins/wiki/Rakefile index 5d2e31db..dd14bed8 100755 --- a/plugins/wiki/Rakefile +++ b/plugins/wiki/Rakefile @@ -20,7 +20,7 @@ RDoc::Task.new(:rdoc) do |rdoc| rdoc.rdoc_files.include('lib/**/*.rb') end -APP_RAKEFILE = File.expand_path("../test/dummy/Rakefile", __FILE__) +APP_RAKEFILE = File.expand_path('test/dummy/Rakefile', __dir__) load 'rails/tasks/engine.rake' Bundler::GemHelper.install_tasks @@ -34,4 +34,4 @@ Rake::TestTask.new(:test) do |t| t.verbose = false end -task :default => :test +task default: :test diff --git a/plugins/wiki/app/controllers/pages_controller.rb b/plugins/wiki/app/controllers/pages_controller.rb index c065abe7..0e28e2d5 100644 --- a/plugins/wiki/app/controllers/pages_controller.rb +++ b/plugins/wiki/app/controllers/pages_controller.rb @@ -1,20 +1,20 @@ class PagesController < ApplicationController before_action -> { require_plugin_enabled FoodsoftWiki } - before_action :catch_special_pages, only: [:show, :new] + before_action :catch_special_pages, only: %i[show new] - skip_before_action :authenticate, :only => :all - before_action :only => :all do - authenticate_or_token(['wiki', 'all']) + skip_before_action :authenticate, only: :all + before_action only: :all do + authenticate_or_token(%w[wiki all]) end before_action do content_for :head, view_context.rss_meta_tag end def index - @page = Page.find_by_permalink "Home" + @page = Page.find_by_permalink 'Home' if @page - render :action => 'show' + render action: 'show' else redirect_to all_pages_path end @@ -34,11 +34,11 @@ class PagesController < ApplicationController end if @page.nil? - redirect_to new_page_path(:title => params[:permalink]) + redirect_to new_page_path(title: params[:permalink]) elsif @page.redirect? page = Page.find_by_id(@page.redirect) unless page.nil? - flash[:notice] = I18n.t('pages.cshow.redirect_notice', :page => @page.title) + flash[:notice] = I18n.t('pages.cshow.redirect_notice', page: @page.title) redirect_to wiki_page_path(page.permalink) end end @@ -46,12 +46,12 @@ class PagesController < ApplicationController def new @page = Page.new - @page.title = params[:title].gsub("_", " ") if params[:title] + @page.title = params[:title].gsub('_', ' ') if params[:title] @page.parent = Page.find_by_permalink(params[:parent]) if params[:parent] respond_to do |format| format.html # new.html.erb - format.xml { render :xml => @page } + format.xml { render xml: @page } end end @@ -60,36 +60,32 @@ class PagesController < ApplicationController end def create - @page = Page.new(params[:page].merge({ :user => current_user })) + @page = Page.new(params[:page].merge({ user: current_user })) if params[:preview] - render :action => 'new' + render action: 'new' + elsif @page.save + flash[:notice] = I18n.t('pages.create.notice') + redirect_to(wiki_page_path(@page.permalink)) else - if @page.save - flash[:notice] = I18n.t('pages.create.notice') - redirect_to(wiki_page_path(@page.permalink)) - else - render :action => "new" - end + render action: 'new' end end def update @page = Page.find(params[:id]) - @page.attributes = params[:page].merge({ :user => current_user }) + @page.attributes = params[:page].merge({ user: current_user }) if params[:preview] @page.attributes = params[:page] - render :action => 'edit' + render action: 'edit' + elsif @page.save + @page.parent_id = parent_id if params[:parent_id].present? \ + && params[:parent_id] != @page_id + flash[:notice] = I18n.t('pages.update.notice') + redirect_to wiki_page_path(@page.permalink) else - if @page.save - @page.parent_id = parent_id if (!params[:parent_id].blank? \ - && params[:parent_id] != @page_id) - flash[:notice] = I18n.t('pages.update.notice') - redirect_to wiki_page_path(@page.permalink) - else - render :action => "edit" - end + render action: 'edit' end rescue ActiveRecord::StaleObjectError flash[:error] = I18n.t('pages.error_stale_object') @@ -100,7 +96,7 @@ class PagesController < ApplicationController @page = Page.find(params[:id]) @page.destroy - flash[:notice] = I18n.t('pages.destroy.notice', :page => @page.title) + flash[:notice] = I18n.t('pages.destroy.notice', page: @page.title) redirect_to wiki_path end @@ -109,23 +105,23 @@ class PagesController < ApplicationController @partial = params[:view] || 'site_map' if params[:name] - @pages = @pages.where("title LIKE ?", "%#{params[:name]}%").limit(20) + @pages = @pages.where('title LIKE ?', "%#{params[:name]}%").limit(20) @partial = 'title_list' end - if params[:sort] - sort = case params[:sort] - when "title" then "title" - when "title_reverse" then "title DESC" - when "last_updated" then "updated_at DESC" - when "last_updated_reverse" then "updated_at" + sort = if params[:sort] + case params[:sort] + when 'title' then 'title' + when 'title_reverse' then 'title DESC' + when 'last_updated' then 'updated_at DESC' + when 'last_updated_reverse' then 'updated_at' end - else - sort = "title" - end + else + 'title' + end @pages = @pages.order(sort) respond_to do |format| format.html - format.rss { render :layout => false } + format.rss { render layout: false } end end @@ -150,15 +146,15 @@ class PagesController < ApplicationController def variables keys = Foodsoft::ExpansionVariables.variables.keys - @variables = Hash[keys.map { |k| [k, Foodsoft::ExpansionVariables.get(k)] }] + @variables = keys.index_with { |k| Foodsoft::ExpansionVariables.get(k) } render 'variables' end private def catch_special_pages - if params[:id] == 'Help:Foodsoft_variables' - variables - end + return unless params[:id] == 'Help:Foodsoft_variables' + + variables end end diff --git a/plugins/wiki/app/helpers/pages_helper.rb b/plugins/wiki/app/helpers/pages_helper.rb index 2c1479f3..869f59d1 100644 --- a/plugins/wiki/app/helpers/pages_helper.rb +++ b/plugins/wiki/app/helpers/pages_helper.rb @@ -2,70 +2,70 @@ module PagesHelper include WikiCloth def rss_meta_tag - tag.link(rel: "alternate", type: "application/rss+xml", title: "RSS", href: all_pages_rss_url).html_safe + tag.link(rel: 'alternate', type: 'application/rss+xml', title: 'RSS', href: all_pages_rss_url).html_safe end def wikified_body(body, title = nil) FoodsoftWiki::WikiParser.new(data: body + "\n", params: { referer: title }).to_html(noedit: true).html_safe - rescue => e + rescue StandardError => e # try the following with line breaks: === one === == two == = three = content_tag :span, class: 'alert alert-error' do - I18n.t '.wikicloth_exception', :msg => e + I18n.t '.wikicloth_exception', msg: e end.html_safe end def link_to_wikipage(page, text = nil) - if text == nil - link_to page.title, wiki_page_path(:permalink => page.permalink) + if text.nil? + link_to page.title, wiki_page_path(permalink: page.permalink) else - link_to text, wiki_page_path(:permalink => page.permalink) + link_to text, wiki_page_path(permalink: page.permalink) end end def link_to_wikipage_by_permalink(permalink, text = nil) - unless permalink.blank? - page = Page.find_by_permalink(permalink) - if page.nil? - if text.nil? - link_to permalink, new_page_path(:title => permalink) - else - link_to text, new_page_path(:title => permalink) - end + return if permalink.blank? + + page = Page.find_by_permalink(permalink) + if page.nil? + if text.nil? + link_to permalink, new_page_path(title: permalink) else - link_to_wikipage(page, text) + link_to text, new_page_path(title: permalink) end + else + link_to_wikipage(page, text) end end def generate_toc(body) toc = String.new - body.gsub(/^([=]{1,6})\s*(.*?)\s*(\1)/) do - number = $1.length - 1 - name = $2 + body.gsub(/^(={1,6})\s*(.*?)\s*(\1)/) do + number = ::Regexp.last_match(1).length - 1 + name = ::Regexp.last_match(2) - toc << "*" * number + " #{name}\n" + toc << (('*' * number) + " #{name}\n") end - unless toc.blank? - FoodsoftWiki::WikiParser.new(data: toc).to_html.gsub(/
  • ([^<>\n]*)/) do - name = $1 - anchor = name.gsub(/\s/, '_').gsub(/[^a-zA-Z_]/, '') - "
  • #{name.truncate(20)}" - end.html_safe - end + return if toc.blank? + + FoodsoftWiki::WikiParser.new(data: toc).to_html.gsub(/
  • ([^<>\n]*)/) do + name = ::Regexp.last_match(1) + anchor = name.gsub(/\s/, '_').gsub(/[^a-zA-Z_]/, '') + "
  • #{name.truncate(20)}" + end.html_safe end def parent_pages_to_select(current_page) - unless current_page.homepage? # Homepage is the page trees root! + if current_page.homepage? + [] + else # Homepage is the page trees root! Page.non_redirected.reject { |p| p == current_page || p.ancestors.include?(current_page) } - else - Array.new end end # return url for all_pages rss feed def all_pages_rss_url(options = {}) - token = TokenVerifier.new(['wiki', 'all']).generate - all_pages_url({ :format => 'rss', :token => token }.merge(options)) + token = TokenVerifier.new(%w[wiki all]).generate + all_pages_url({ format: 'rss', token: token }.merge(options)) end end diff --git a/plugins/wiki/app/models/page.rb b/plugins/wiki/app/models/page.rb index e773afa7..26d2393d 100644 --- a/plugins/wiki/app/models/page.rb +++ b/plugins/wiki/app/models/page.rb @@ -1,61 +1,62 @@ class Page < ApplicationRecord include ActsAsTree - belongs_to :user, :foreign_key => 'updated_by' + belongs_to :user, foreign_key: 'updated_by' acts_as_versioned version_column: :lock_version - self.non_versioned_columns += %w(permalink created_at title) + self.non_versioned_columns += %w[permalink created_at title] - acts_as_tree :order => "title" + acts_as_tree order: 'title' attr_accessor :old_title # Save title to create redirect page when editing title validates_presence_of :title, :body validates_uniqueness_of :permalink, :title - before_validation :set_permalink, :on => :create - before_validation :update_permalink, :on => :update + before_validation :set_permalink, on: :create + before_validation :update_permalink, on: :update after_update :create_redirect - scope :non_redirected, -> { where(:redirect => nil) } - scope :no_parent, -> { where(:parent_id => nil) } + scope :non_redirected, -> { where(redirect: nil) } + scope :no_parent, -> { where(parent_id: nil) } def self.permalink(title) - title.gsub(/[\/\.,;@\s]/, "_").gsub(/[\"\']/, "") + title.gsub(%r{[/.,;@\s]}, '_').gsub(/["']/, '') end def homepage? - permalink == "Home" + permalink == 'Home' end def self.dashboard - where(permalink: "Dashboard").first + where(permalink: 'Dashboard').first end def self.public_front_page - where(permalink: "Public_frontpage").first + where(permalink: 'Public_frontpage').first end def self.welcome_mail - where(permalink: "Welcome_mail").first + where(permalink: 'Welcome_mail').first end def set_permalink - unless title.blank? - self.permalink = Page.count == 0 ? "Home" : Page.permalink(title) - end + return if title.blank? + + self.permalink = Page.count == 0 ? 'Home' : Page.permalink(title) end def diff current = versions.latest - old = versions.where(["page_id = ? and lock_version < ?", current.page_id, current.lock_version]).order('lock_version DESC').first + old = versions.where(['page_id = ? and lock_version < ?', current.page_id, + current.lock_version]).order('lock_version DESC').first if old o = '' Diffy::Diff.new(old.body, current.body).each do |line| case line - when /^\+/ then o += "#{line.chomp}
    " unless line.chomp == "+" - when /^-/ then o += "#{line.chomp}
    " unless line.chomp == "-" + when /^\+/ then o += "#{line.chomp}
    " unless line.chomp == '+' + when /^-/ then o += "#{line.chomp}
    " unless line.chomp == '-' end end o @@ -67,19 +68,19 @@ class Page < ApplicationRecord protected def update_permalink - if changed.include?("title") - set_permalink - self.old_title = changes["title"].first # Save title for creating redirect - end + return unless changed.include?('title') + + set_permalink + self.old_title = changes['title'].first # Save title for creating redirect end def create_redirect - unless old_title.blank? - Page.create :redirect => id, - :title => old_title, - :body => I18n.t('model.page.redirect', :title => title), - :permalink => Page.permalink(old_title), - :updated_by => updated_by - end + return if old_title.blank? + + Page.create redirect: id, + title: old_title, + body: I18n.t('model.page.redirect', title: title), + permalink: Page.permalink(old_title), + updated_by: updated_by end end diff --git a/plugins/wiki/app/views/pages/all.rss.builder b/plugins/wiki/app/views/pages/all.rss.builder index f7194763..7f0b4e10 100644 --- a/plugins/wiki/app/views/pages/all.rss.builder +++ b/plugins/wiki/app/views/pages/all.rss.builder @@ -1,16 +1,16 @@ -xml.instruct! :xml, :version => "1.0" -xml.rss :version => "2.0" do +xml.instruct! :xml, version: '1.0' +xml.rss version: '2.0' do xml.channel do - xml.title FoodsoftConfig[:name] + " Wiki" - xml.description "" + xml.title FoodsoftConfig[:name] + ' Wiki' + xml.description '' xml.link FoodsoftConfig[:homepage] for page in @pages xml.item do xml.title page.title - xml.description page.diff, :type => "html" + xml.description page.diff, type: 'html' xml.author User.find_by_id(page.updated_by).try(:display) - xml.pubDate page.updated_at.to_s(:rfc822) + xml.pubDate page.updated_at.to_fs(:rfc822) xml.link wiki_page_path(page.permalink) xml.guid page.updated_at.to_i end diff --git a/plugins/wiki/config/routes.rb b/plugins/wiki/config/routes.rb index 4ebad572..ad713366 100644 --- a/plugins/wiki/config/routes.rb +++ b/plugins/wiki/config/routes.rb @@ -1,12 +1,12 @@ Rails.application.routes.draw do scope '/:foodcoop' do resources :pages do - get :all, :on => :collection - get :version, :on => :member - get :revert, :on => :member - get :diff, :on => :member + get :all, on: :collection + get :version, on: :member + get :revert, on: :member + get :diff, on: :member end get '/wiki/:permalink' => 'pages#show', :as => 'wiki_page' # , :constraints => {:permalink => /[^\s]+/} - get '/wiki' => 'pages#show', :defaults => { :permalink => 'Home' }, :as => 'wiki' + get '/wiki' => 'pages#show', :defaults => { permalink: 'Home' }, :as => 'wiki' end end diff --git a/plugins/wiki/db/migrate/20090325175756_create_pages.rb b/plugins/wiki/db/migrate/20090325175756_create_pages.rb index 846decf8..cdd00e2b 100644 --- a/plugins/wiki/db/migrate/20090325175756_create_pages.rb +++ b/plugins/wiki/db/migrate/20090325175756_create_pages.rb @@ -4,7 +4,7 @@ class CreatePages < ActiveRecord::Migration[4.2] t.string :title t.text :body t.string :permalink - t.integer :lock_version, :default => 0 + t.integer :lock_version, default: 0 t.integer :updated_by t.integer :redirect t.integer :parent_id diff --git a/plugins/wiki/foodsoft_wiki.gemspec b/plugins/wiki/foodsoft_wiki.gemspec index 371872c5..58be331d 100644 --- a/plugins/wiki/foodsoft_wiki.gemspec +++ b/plugins/wiki/foodsoft_wiki.gemspec @@ -1,26 +1,27 @@ -$:.push File.expand_path("../lib", __FILE__) +$:.push File.expand_path('lib', __dir__) # Maintain your gem's version: -require "foodsoft_wiki/version" +require 'foodsoft_wiki/version' # Describe your gem and declare its dependencies: Gem::Specification.new do |s| - s.name = "foodsoft_wiki" + s.name = 'foodsoft_wiki' s.version = FoodsoftWiki::VERSION - s.authors = ["wvengen"] - s.email = ["dev-foodsoft@willem.engen.nl"] - s.homepage = "https://github.com/foodcoops/foodsoft" - s.summary = "Wiki plugin for foodsoft." - s.description = "Adds a wiki to foodsoft." + s.authors = ['wvengen'] + s.email = ['dev-foodsoft@willem.engen.nl'] + s.homepage = 'https://github.com/foodcoops/foodsoft' + s.summary = 'Wiki plugin for foodsoft.' + s.description = 'Adds a wiki to foodsoft.' - s.files = Dir["{app,config,db,lib}/**/*"] + ["Rakefile", "README.md"] + s.files = Dir['{app,config,db,lib}/**/*'] + ['Rakefile', 'README.md'] - s.add_dependency "rails" + s.add_dependency 'rails' s.add_dependency 'wikicloth' s.add_dependency 'twitter-text', '~> 1.14' # wikicloth doesn't support version 2 s.add_dependency 'acts_as_versioned' # need git version, make sure that is included in foodsoft's Gemfile - s.add_dependency "deface", "~> 1.0" + s.add_dependency 'deface', '~> 1.0' s.add_dependency 'diffy' s.add_dependency 'content_for_in_controllers' - s.add_development_dependency "sqlite3" + s.add_development_dependency 'sqlite3' + s.metadata['rubygems_mfa_required'] = 'true' end diff --git a/plugins/wiki/lib/foodsoft_wiki/engine.rb b/plugins/wiki/lib/foodsoft_wiki/engine.rb index 4cc20f6a..ae2ce462 100644 --- a/plugins/wiki/lib/foodsoft_wiki/engine.rb +++ b/plugins/wiki/lib/foodsoft_wiki/engine.rb @@ -8,17 +8,17 @@ module FoodsoftWiki subnav.item :all_pages, I18n.t('navigation.wiki.all_pages'), ctx.all_pages_path, id: nil end # move this last added item to just after the foodcoop menu - if i = primary.items.index(primary[:foodcoop]) - primary.items.insert(i + 1, primary.items.delete_at(-1)) - end + return unless i = primary.items.index(primary[:foodcoop]) + + primary.items.insert(i + 1, primary.items.delete_at(-1)) end def default_foodsoft_config(cfg) cfg[:use_wiki] = true end - initializer "foodsoft_wiki.assets.precompile" do |app| - app.config.assets.precompile += %w(icons/feed-icon-14x14.png) + initializer 'foodsoft_wiki.assets.precompile' do |app| + app.config.assets.precompile += %w[icons/feed-icon-14x14.png] end end end diff --git a/plugins/wiki/lib/foodsoft_wiki/mailer.rb b/plugins/wiki/lib/foodsoft_wiki/mailer.rb index 83a110f1..4b7a892d 100644 --- a/plugins/wiki/lib/foodsoft_wiki/mailer.rb +++ b/plugins/wiki/lib/foodsoft_wiki/mailer.rb @@ -3,10 +3,10 @@ module FoodsoftWiki def self.included(base) # :nodoc: base.class_eval do # modify user presentation link to writing a message for the user - def additonal_welcome_text(user) - if FoodsoftWiki.enabled? && (page = Page.welcome_mail) - page.body - end + def additonal_welcome_text(_user) + return unless FoodsoftWiki.enabled? && (page = Page.welcome_mail) + + page.body end end end @@ -15,5 +15,5 @@ end # modify existing helper ActiveSupport.on_load(:after_initialize) do - Mailer.send :include, FoodsoftWiki::Mailer + Mailer.include FoodsoftWiki::Mailer end diff --git a/plugins/wiki/lib/foodsoft_wiki/version.rb b/plugins/wiki/lib/foodsoft_wiki/version.rb index 2a67a94e..580ee3ed 100644 --- a/plugins/wiki/lib/foodsoft_wiki/version.rb +++ b/plugins/wiki/lib/foodsoft_wiki/version.rb @@ -1,3 +1,3 @@ module FoodsoftWiki - VERSION = "0.0.1" + VERSION = '0.0.1' end diff --git a/plugins/wiki/lib/foodsoft_wiki/wiki_parser.rb b/plugins/wiki/lib/foodsoft_wiki/wiki_parser.rb index 37e58465..6e14d2a8 100644 --- a/plugins/wiki/lib/foodsoft_wiki/wiki_parser.rb +++ b/plugins/wiki/lib/foodsoft_wiki/wiki_parser.rb @@ -10,7 +10,7 @@ module FoodsoftWiki link_attributes_for do |page| permalink = Page.permalink(page) - if Page.exists?(:permalink => permalink) + if Page.exists?(permalink: permalink) { href: url_for(:wiki_page_path, permalink: permalink) } elsif page.include? '#' # If "Foo#Bar" does not exist then consider "Foo" with anchor. @@ -20,8 +20,8 @@ module FoodsoftWiki end end - section_link do |section| - "" + section_link do |_section| + '' end def to_html(render_options = {}) @@ -41,7 +41,7 @@ module FoodsoftWiki return { href: '#' + anchor } if page.empty? permalink = Page.permalink(page) - if Page.exists?(:permalink => permalink) + if Page.exists?(permalink: permalink) { href: url_for(:wiki_page_path, permalink: permalink, anchor: anchor) } else # Do not suggest to use number signs in the title. diff --git a/script/rails b/script/rails index bd79dce5..3c234a25 100755 --- a/script/rails +++ b/script/rails @@ -1,6 +1,6 @@ #!/usr/bin/env ruby # This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application. -APP_PATH = File.expand_path('../../config/application', __FILE__) -require File.expand_path('../../config/boot', __FILE__) +APP_PATH = File.expand_path('../config/application', __dir__) +require File.expand_path('../config/boot', __dir__) require 'rails/commands' diff --git a/spec/controllers/home_controller_spec.rb b/spec/controllers/home_controller_spec.rb index c5732bd9..525c27e4 100644 --- a/spec/controllers/home_controller_spec.rb +++ b/spec/controllers/home_controller_spec.rb @@ -2,8 +2,8 @@ require 'spec_helper' -describe HomeController, type: :controller do - let(:user) { create :user } +describe HomeController do + let(:user) { create(:user) } describe 'GET index' do describe 'NOT logged in' do @@ -45,7 +45,7 @@ describe HomeController, type: :controller do end describe 'with ordergroup user' do - let(:og_user) { create :user, :ordergroup } + let(:og_user) { create(:user, :ordergroup) } before { login og_user } @@ -59,7 +59,7 @@ describe HomeController, type: :controller do describe 'GET update_profile' do describe 'with simple user' do let(:unchanged_attributes) { user.attributes.slice('first_name', 'last_name', 'email') } - let(:changed_attributes) { attributes_for :user } + let(:changed_attributes) { attributes_for(:user) } let(:invalid_attributes) { { email: 'e.mail.com' } } before { login user } @@ -80,12 +80,13 @@ describe HomeController, type: :controller do expect(response).to have_http_status(:redirect) expect(response).to redirect_to(my_profile_path) expect(flash[:notice]).to match(/#{I18n.t('home.changes_saved')}/) - expect(user.reload.attributes.slice(:first_name, :last_name, :email)).to eq(changed_attributes.slice('first_name', 'last_name', 'email')) + expect(user.reload.attributes.slice(:first_name, :last_name, + :email)).to eq(changed_attributes.slice('first_name', 'last_name', 'email')) end end describe 'with ordergroup user' do - let(:og_user) { create :user, :ordergroup } + let(:og_user) { create(:user, :ordergroup) } let(:unchanged_attributes) { og_user.attributes.slice('first_name', 'last_name', 'email') } let(:changed_attributes) { unchanged_attributes.merge({ ordergroup: { contact_address: 'new Adress 7' } }) } @@ -112,7 +113,7 @@ describe HomeController, type: :controller do end describe 'with ordergroup user' do - let(:og_user) { create :user, :ordergroup } + let(:og_user) { create(:user, :ordergroup) } before { login og_user } @@ -132,13 +133,13 @@ describe HomeController, type: :controller do get_with_defaults :cancel_membership end.to raise_error(ActiveRecord::RecordNotFound) expect do - get_with_defaults :cancel_membership, params: { membership_id: 424242 } + get_with_defaults :cancel_membership, params: { membership_id: 424_242 } end.to raise_error(ActiveRecord::RecordNotFound) end end describe 'with ordergroup user' do - let(:fin_user) { create :user, :role_finance } + let(:fin_user) { create(:user, :role_finance) } before { login fin_user } diff --git a/spec/factories/delivery.rb b/spec/factories/delivery.rb index 5d27d870..e3b37d0a 100644 --- a/spec/factories/delivery.rb +++ b/spec/factories/delivery.rb @@ -2,7 +2,7 @@ require 'factory_bot' FactoryBot.define do factory :delivery do - supplier { create :supplier } + supplier { create(:supplier) } date { Time.now } end end diff --git a/spec/factories/group_order.rb b/spec/factories/group_order.rb index f7e910df..d62172ea 100644 --- a/spec/factories/group_order.rb +++ b/spec/factories/group_order.rb @@ -4,6 +4,6 @@ FactoryBot.define do # requires order factory :group_order do ordergroup { create(:user, groups: [FactoryBot.create(:ordergroup)]).ordergroup } - updated_by { create :user } + updated_by { create(:user) } end end diff --git a/spec/factories/invoice.rb b/spec/factories/invoice.rb index b3e65a17..3564d977 100644 --- a/spec/factories/invoice.rb +++ b/spec/factories/invoice.rb @@ -3,9 +3,9 @@ require 'factory_bot' FactoryBot.define do factory :invoice do supplier - number { rand(1..99999) } + number { rand(1..99_999) } amount { rand(0.1..26.0).round(2) } - created_by { create :user } + created_by { create(:user) } after :create do |invoice| invoice.supplier.reload diff --git a/spec/factories/order.rb b/spec/factories/order.rb index 87febae2..970bd040 100644 --- a/spec/factories/order.rb +++ b/spec/factories/order.rb @@ -3,10 +3,10 @@ require 'factory_bot' FactoryBot.define do factory :order do starts { Time.now } - supplier { create :supplier, article_count: (article_count.nil? ? true : article_count) } + supplier { create(:supplier, article_count: (article_count.nil? ? true : article_count)) } article_ids { supplier.articles.map(&:id) unless supplier.nil? } - created_by { create :user } - updated_by { create :user } + created_by { create(:user) } + updated_by { create(:user) } transient do article_count { true } diff --git a/spec/factories/supplier.rb b/spec/factories/supplier.rb index 67ba3528..ef592a60 100644 --- a/spec/factories/supplier.rb +++ b/spec/factories/supplier.rb @@ -10,8 +10,8 @@ FactoryBot.define do article_count { 0 } end - before :create do |supplier, evaluator| - next if supplier.class == SharedSupplier + before :create do |supplier, _evaluator| + next if supplier.instance_of?(SharedSupplier) next if supplier.supplier_category_id? supplier.supplier_category = create :supplier_category @@ -20,7 +20,7 @@ FactoryBot.define do after :create do |supplier, evaluator| article_count = evaluator.article_count article_count = rand(1..99) if article_count == true - create_list :article, article_count, supplier: supplier + create_list(:article, article_count, supplier: supplier) end factory :shared_supplier, class: 'SharedSupplier' diff --git a/spec/factories/user.rb b/spec/factories/user.rb index a6067e6b..9563c15c 100644 --- a/spec/factories/user.rb +++ b/spec/factories/user.rb @@ -10,21 +10,21 @@ FactoryBot.define do factory :admin do sequence(:nick) { |n| "admin#{n}" } first_name { 'Administrator' } - after :create do |user, evaluator| - create :workgroup, role_admin: true, user_ids: [user.id] + after :create do |user, _evaluator| + create(:workgroup, role_admin: true, user_ids: [user.id]) end end trait :ordergroup do - after :create do |user, evaluator| - create :ordergroup, user_ids: [user.id] + after :create do |user, _evaluator| + create(:ordergroup, user_ids: [user.id]) end end - [:ordergroup, :finance, :invoices, :article_meta, :suppliers, :pickups, :orders].each do |role| + %i[ordergroup finance invoices article_meta suppliers pickups orders].each do |role| trait "role_#{role}".to_sym do - after :create do |user, evaluator| - create :workgroup, "role_#{role}" => true, user_ids: [user.id] + after :create do |user, _evaluator| + create(:workgroup, "role_#{role}" => true, user_ids: [user.id]) end end end @@ -37,7 +37,7 @@ FactoryBot.define do type { 'Workgroup' } end - factory :ordergroup, class: "Ordergroup" do + factory :ordergroup, class: 'Ordergroup' do type { 'Ordergroup' } sequence(:name) { |n| "Order group ##{n}" } # workaround to avoid needing to save the ordergroup diff --git a/spec/integration/articles_spec.rb b/spec/integration/articles_spec.rb index bbd5e375..bddd80d6 100644 --- a/spec/integration/articles_spec.rb +++ b/spec/integration/articles_spec.rb @@ -1,9 +1,9 @@ require_relative '../spec_helper' feature ArticlesController do - let(:user) { create :user, groups: [create(:workgroup, role_article_meta: true)] } - let(:supplier) { create :supplier } - let!(:article_category) { create :article_category } + let(:user) { create(:user, groups: [create(:workgroup, role_article_meta: true)]) } + let(:supplier) { create(:supplier) } + let!(:article_category) { create(:article_category) } before { login user } @@ -18,15 +18,15 @@ feature ArticlesController do it 'can create a new article' do click_on I18n.t('articles.index.new') expect(page).to have_selector('form#new_article') - article = build :article, supplier: supplier, article_category: article_category + article = build(:article, supplier: supplier, article_category: article_category) within('#new_article') do - fill_in 'article_name', :with => article.name - fill_in 'article_unit', :with => article.unit - select article.article_category.name, :from => 'article_article_category_id' - fill_in 'article_price', :with => article.price - fill_in 'article_unit_quantity', :with => article.unit_quantity - fill_in 'article_tax', :with => article.tax - fill_in 'article_deposit', :with => article.deposit + fill_in 'article_name', with: article.name + fill_in 'article_unit', with: article.unit + select article.article_category.name, from: 'article_article_category_id' + fill_in 'article_price', with: article.price + fill_in 'article_unit_quantity', with: article.unit_quantity + fill_in 'article_tax', with: article.tax + fill_in 'article_deposit', with: article.deposit # "Element cannot be scrolled into view" error, js as workaround # find('input[type="submit"]').click page.execute_script('$("form#new_article").submit();') @@ -50,22 +50,22 @@ feature ArticlesController do it do find('input[type="submit"]').click - expect(find("tr:nth-child(1) #new_articles__note").value).to eq "bio ◎" - expect(find("tr:nth-child(2) #new_articles__name").value).to eq "Pijnboompitten" + expect(find('tr:nth-child(1) #new_articles__note').value).to eq 'bio ◎' + expect(find('tr:nth-child(2) #new_articles__name').value).to eq 'Pijnboompitten' 4.times do |i| all("tr:nth-child(#{i + 1}) select > option")[1].select_option end find('input[type="submit"]').click - expect(page).to have_content("Pijnboompitten") + expect(page).to have_content('Pijnboompitten') expect(supplier.articles.count).to eq 4 end end end - describe "can update existing article" do - let!(:article) { create :article, supplier: supplier, name: 'Foobar', order_number: 1, unit: '250 g' } + describe 'can update existing article' do + let!(:article) { create(:article, supplier: supplier, name: 'Foobar', order_number: 1, unit: '250 g') } it do find('input[type="submit"]').click @@ -77,35 +77,35 @@ feature ArticlesController do end end - describe "handles missing data" do + describe 'handles missing data' do it do find('input[type="submit"]').click # to overview find('input[type="submit"]').click # missing category, re-show form expect(find('tr.alert')).to be_present expect(supplier.articles.count).to eq 0 - all("tr select > option")[1].select_option + all('tr select > option')[1].select_option find('input[type="submit"]').click # now it should succeed expect(supplier.articles.count).to eq 1 end end - describe "can remove an existing article" do - let!(:article) { create :article, supplier: supplier, name: 'Foobar', order_number: 99999 } + describe 'can remove an existing article' do + let!(:article) { create(:article, supplier: supplier, name: 'Foobar', order_number: 99_999) } it do check('articles_outlist_absent') find('input[type="submit"]').click expect(find("#outlisted_articles_#{article.id}", visible: :all)).to be_present - all("tr select > option")[1].select_option + all('tr select > option')[1].select_option find('input[type="submit"]').click expect(article.reload.deleted?).to be true end end - describe "can convert units when updating" do - let!(:article) { create :article, supplier: supplier, order_number: 1, unit: '250 g' } + describe 'can convert units when updating' do + let!(:article) { create(:article, supplier: supplier, order_number: 1, unit: '250 g') } it do check('articles_convert_units') diff --git a/spec/integration/balancing_spec.rb b/spec/integration/balancing_spec.rb index 556d102d..d8e58e6d 100644 --- a/spec/integration/balancing_spec.rb +++ b/spec/integration/balancing_spec.rb @@ -1,17 +1,17 @@ require_relative '../spec_helper' feature 'settling an order', js: true do - let(:ftt) { create :financial_transaction_type } - let(:admin) { create :user, groups: [create(:workgroup, role_finance: true)] } - let(:user) { create :user, groups: [create(:ordergroup)] } - let(:supplier) { create :supplier } - let(:article) { create :article, supplier: supplier, unit_quantity: 1 } - let(:order) { create :order, supplier: supplier, article_ids: [article.id] } # need to ref article - let(:go1) { create :group_order, order: order } - let(:go2) { create :group_order, order: order } + let(:ftt) { create(:financial_transaction_type) } + let(:admin) { create(:user, groups: [create(:workgroup, role_finance: true)]) } + let(:user) { create(:user, groups: [create(:ordergroup)]) } + let(:supplier) { create(:supplier) } + let(:article) { create(:article, supplier: supplier, unit_quantity: 1) } + let(:order) { create(:order, supplier: supplier, article_ids: [article.id]) } # need to ref article + let(:go1) { create(:group_order, order: order) } + let(:go2) { create(:group_order, order: order) } let(:oa) { order.order_articles.find_by_article_id(article.id) } - let(:goa1) { create :group_order_article, group_order: go1, order_article: oa } - let(:goa2) { create :group_order_article, group_order: go2, order_article: oa } + let(:goa1) { create(:group_order_article, group_order: go1, order_article: oa) } + let(:goa2) { create(:group_order_article, group_order: go2, order_article: oa) } before do goa1.update_quantities(3, 0) @@ -22,6 +22,9 @@ feature 'settling an order', js: true do goa2.reload end + before { visit new_finance_order_path(order_id: order.id) } + before { login admin } + it 'has correct order result' do expect(oa.quantity).to eq(4) expect(oa.tolerance).to eq(0) @@ -29,10 +32,6 @@ feature 'settling an order', js: true do expect(goa2.result).to eq(1) end - before { login admin } - - before { visit new_finance_order_path(order_id: order.id) } - it 'has product ordered visible' do expect(page).to have_content(article.name) expect(page).to have_selector("#order_article_#{oa.id}") @@ -59,7 +58,7 @@ feature 'settling an order', js: true do click_link I18n.t('ui.delete') end end - expect(page).to_not have_selector("#order_article_#{oa.id}") + expect(page).not_to have_selector("#order_article_#{oa.id}") expect(OrderArticle.exists?(oa.id)).to be true oa.reload expect(oa.quantity).to eq(4) @@ -77,7 +76,7 @@ feature 'settling an order', js: true do click_link I18n.t('ui.delete') end end - expect(page).to_not have_selector("#order_article_#{oa.id}") + expect(page).not_to have_selector("#order_article_#{oa.id}") expect(OrderArticle.exists?(oa.id)).to be false end @@ -87,7 +86,7 @@ feature 'settling an order', js: true do within("#group_order_article_#{goa1.id}") do click_link I18n.t('ui.delete') end - expect(page).to_not have_selector("#group_order_article_#{goa1.id}") + expect(page).not_to have_selector("#group_order_article_#{goa1.id}") expect(OrderArticle.exists?(oa.id)).to be true expect(GroupOrderArticle.exists?(goa1.id)).to be true goa1.reload @@ -103,7 +102,7 @@ feature 'settling an order', js: true do within("#group_order_article_#{goa1.id}") do click_link I18n.t('ui.delete') end - expect(page).to_not have_selector("#group_order_article_#{goa1.id}") + expect(page).not_to have_selector("#group_order_article_#{goa1.id}") expect(OrderArticle.exists?(oa.id)).to be true expect(GroupOrderArticle.exists?(goa1.id)).to be false end @@ -134,15 +133,15 @@ feature 'settling an order', js: true do end expect(page).to have_selector('form#new_group_order_article') within('#new_group_order_article') do - select user.ordergroup.name, :from => 'group_order_article_ordergroup_id' + select user.ordergroup.name, from: 'group_order_article_ordergroup_id' find_by_id('group_order_article_result').set(8) sleep 0.25 find('input[type="submit"]').click end - expect(page).to_not have_selector('form#new_group_order_article') + expect(page).not_to have_selector('form#new_group_order_article') expect(page).to have_content(user.ordergroup.name) goa = GroupOrderArticle.last - expect(goa).to_not be_nil + expect(goa).not_to be_nil expect(goa.result).to eq 8 expect(page).to have_selector("#group_order_article_#{goa.id}") expect(find("#r_#{goa.id}").value.to_f).to eq 8 @@ -169,8 +168,8 @@ feature 'settling an order', js: true do end it 'can add an article' do - new_article = create :article, supplier: supplier - expect(page).to_not have_content(new_article.name) + new_article = create(:article, supplier: supplier) + expect(page).not_to have_content(new_article.name) click_link I18n.t('finance.balancing.edit_results_by_articles.add_article') expect(page).to have_selector('form#new_order_article') within('#new_order_article') do @@ -178,8 +177,8 @@ feature 'settling an order', js: true do sleep 0.25 find('input[type="submit"]').click end - expect(page).to_not have_selector('form#new_order_article') + expect(page).not_to have_selector('form#new_order_article') expect(page).to have_content(new_article.name) - expect(order.order_articles.where(article_id: new_article.id)).to_not be_nil + expect(order.order_articles.where(article_id: new_article.id)).not_to be_nil end end diff --git a/spec/integration/config_spec.rb b/spec/integration/config_spec.rb index 91f376dd..4f67802a 100644 --- a/spec/integration/config_spec.rb +++ b/spec/integration/config_spec.rb @@ -3,7 +3,7 @@ require_relative '../spec_helper' feature 'admin/configs' do let(:name) { Faker::Lorem.words(number: rand(2..4)).join(' ') } let(:email) { Faker::Internet.email } - let(:admin) { create :admin } + let(:admin) { create(:admin) } before { login admin } @@ -51,13 +51,13 @@ feature 'admin/configs' do end def compact_hash_deep!(h) - h.each do |k, v| + h.each do |_k, v| if v.is_a? Hash compact_hash_deep!(v) - v.reject! { |k, v| v.blank? } + v.compact_blank! end end - h.reject! { |k, v| v.blank? } + h.compact_blank! h end end diff --git a/spec/integration/home_spec.rb b/spec/integration/home_spec.rb index 313b9afe..87390bd9 100644 --- a/spec/integration/home_spec.rb +++ b/spec/integration/home_spec.rb @@ -1,7 +1,7 @@ require_relative '../spec_helper' feature 'my profile page' do - let(:user) { create :user } + let(:user) { create(:user) } before { login user } diff --git a/spec/integration/login_spec.rb b/spec/integration/login_spec.rb index 49af6852..747d170f 100644 --- a/spec/integration/login_spec.rb +++ b/spec/integration/login_spec.rb @@ -1,7 +1,7 @@ require_relative '../spec_helper' feature LoginController do - let(:user) { create :user } + let(:user) { create(:user) } describe 'forgot password' do before { visit forgot_password_path } @@ -36,7 +36,7 @@ feature LoginController do it 'is not accessible' do expect(page).to have_selector '.alert-error' - expect(page).to_not have_selector 'input[type=password]' + expect(page).not_to have_selector 'input[type=password]' end end diff --git a/spec/integration/order_spec.rb b/spec/integration/order_spec.rb index dd768997..37b9e60a 100644 --- a/spec/integration/order_spec.rb +++ b/spec/integration/order_spec.rb @@ -1,12 +1,12 @@ require_relative '../spec_helper' feature Order, js: true do - let(:admin) { create :user, groups: [create(:workgroup, role_orders: true)] } - let(:article) { create :article, unit_quantity: 1 } - let(:order) { create :order, supplier: article.supplier, article_ids: [article.id] } # need to ref article - let(:go1) { create :group_order, order: order } + let(:admin) { create(:user, groups: [create(:workgroup, role_orders: true)]) } + let(:article) { create(:article, unit_quantity: 1) } + let(:order) { create(:order, supplier: article.supplier, article_ids: [article.id]) } # need to ref article + let(:go1) { create(:group_order, order: order) } let(:oa) { order.order_articles.find_by_article_id(article.id) } - let(:goa1) { create :group_order_article, group_order: go1, order_article: oa } + let(:goa1) { create(:group_order_article, group_order: go1, order_article: oa) } before { login admin } diff --git a/spec/integration/product_distribution_example_spec.rb b/spec/integration/product_distribution_example_spec.rb index e15642f1..2c1af327 100644 --- a/spec/integration/product_distribution_example_spec.rb +++ b/spec/integration/product_distribution_example_spec.rb @@ -1,12 +1,12 @@ require_relative '../spec_helper' feature 'product distribution', js: true do - let(:ftt) { create :financial_transaction_type } - let(:admin) { create :admin } - let(:user_a) { create :user, groups: [create(:ordergroup)] } - let(:user_b) { create :user, groups: [create(:ordergroup)] } - let(:supplier) { create :supplier } - let(:article) { create :article, supplier: supplier, unit_quantity: 5 } + let(:ftt) { create(:financial_transaction_type) } + let(:admin) { create(:admin) } + let(:user_a) { create(:user, groups: [create(:ordergroup)]) } + let(:user_b) { create(:user, groups: [create(:ordergroup)]) } + let(:supplier) { create(:supplier) } + let(:article) { create(:article, supplier: supplier, unit_quantity: 5) } let(:order) { create(:order, supplier: supplier, article_ids: [article.id]) } let(:oa) { order.order_articles.first } @@ -50,10 +50,10 @@ feature 'product distribution', js: true do expect(oa.quantity).to eq(6) expect(oa.tolerance).to eq(1) # Gruppe a bekommt 3 einheiten. - goa_a = oa.group_order_articles.joins(:group_order).where(:group_orders => { :ordergroup_id => user_a.ordergroup.id }).first + goa_a = oa.group_order_articles.joins(:group_order).where(group_orders: { ordergroup_id: user_a.ordergroup.id }).first expect(goa_a.result).to eq(3) # gruppe b bekommt 2 einheiten. - goa_b = oa.group_order_articles.joins(:group_order).where(:group_orders => { :ordergroup_id => user_b.ordergroup.id }).first + goa_b = oa.group_order_articles.joins(:group_order).where(group_orders: { ordergroup_id: user_b.ordergroup.id }).first expect(goa_b.result).to eq(2) end end diff --git a/spec/integration/receive_spec.rb b/spec/integration/receive_spec.rb index 3b65107e..6bf021e8 100644 --- a/spec/integration/receive_spec.rb +++ b/spec/integration/receive_spec.rb @@ -1,15 +1,15 @@ require_relative '../spec_helper' feature 'receiving an order', js: true do - let(:admin) { create :user, groups: [create(:workgroup, role_orders: true)] } - let(:supplier) { create :supplier } - let(:article) { create :article, supplier: supplier, unit_quantity: 3 } - let(:order) { create :order, supplier: supplier, article_ids: [article.id] } # need to ref article - let(:go1) { create :group_order, order: order } - let(:go2) { create :group_order, order: order } + let(:admin) { create(:user, groups: [create(:workgroup, role_orders: true)]) } + let(:supplier) { create(:supplier) } + let(:article) { create(:article, supplier: supplier, unit_quantity: 3) } + let(:order) { create(:order, supplier: supplier, article_ids: [article.id]) } # need to ref article + let(:go1) { create(:group_order, order: order) } + let(:go2) { create(:group_order, order: order) } let(:oa) { order.order_articles.find_by_article_id(article.id) } - let(:goa1) { create :group_order_article, group_order: go1, order_article: oa } - let(:goa2) { create :group_order_article, group_order: go2, order_article: oa } + let(:goa1) { create(:group_order_article, group_order: go1, order_article: oa) } + let(:goa2) { create(:group_order_article, group_order: go2, order_article: oa) } # set quantities of group_order_articles def set_quantities(q1, q2) @@ -46,7 +46,7 @@ feature 'receiving an order', js: true do it 'has product not ordered invisible' do set_quantities [0, 0], [0, 0] visit receive_order_path(id: order.id) - expect(page).to_not have_selector("#order_article_#{oa.id}") + expect(page).not_to have_selector("#order_article_#{oa.id}") end it 'is not received by default' do @@ -58,7 +58,7 @@ feature 'receiving an order', js: true do it 'does not change anything when received is ordered' do set_quantities [2, 0], [3, 2] visit receive_order_path(id: order.id) - fill_in "order_articles_#{oa.id}_units_received", :with => oa.units_to_order + fill_in "order_articles_#{oa.id}_units_received", with: oa.units_to_order find('input[type="submit"]').click expect(page).to have_selector('body') check_quantities 2, 2, 4 @@ -67,7 +67,7 @@ feature 'receiving an order', js: true do it 'redistributes properly when received is more' do set_quantities [2, 0], [3, 2] visit receive_order_path(id: order.id) - fill_in "order_articles_#{oa.id}_units_received", :with => 3 + fill_in "order_articles_#{oa.id}_units_received", with: 3 find('input[type="submit"]').click expect(page).to have_selector('body') check_quantities 3, 2, 5 @@ -76,7 +76,7 @@ feature 'receiving an order', js: true do it 'redistributes properly when received is less' do set_quantities [2, 0], [3, 2] visit receive_order_path(id: order.id) - fill_in "order_articles_#{oa.id}_units_received", :with => 1 + fill_in "order_articles_#{oa.id}_units_received", with: 1 find('input[type="submit"]').click expect(page).to have_selector('body') check_quantities 1, 2, 1 diff --git a/spec/integration/session_spec.rb b/spec/integration/session_spec.rb index 2571ccab..e264efcb 100644 --- a/spec/integration/session_spec.rb +++ b/spec/integration/session_spec.rb @@ -1,7 +1,7 @@ require_relative '../spec_helper' feature 'the session' do - let(:user) { create :user } + let(:user) { create(:user) } describe 'login page' do it 'is accessible' do @@ -11,7 +11,7 @@ feature 'the session' do it 'logs me in' do login user - expect(page).to_not have_selector('.alert-error') + expect(page).not_to have_selector('.alert-error') end it 'does not log me in with wrong password' do @@ -21,10 +21,10 @@ feature 'the session' do it 'can log me in using an email address' do visit login_path - fill_in 'nick', :with => user.email - fill_in 'password', :with => user.password + fill_in 'nick', with: user.email + fill_in 'password', with: user.password find('input[type=submit]').click - expect(page).to_not have_selector('.alert-error') + expect(page).not_to have_selector('.alert-error') end end end diff --git a/spec/integration/supplier_spec.rb b/spec/integration/supplier_spec.rb index 178892b8..5683d8da 100644 --- a/spec/integration/supplier_spec.rb +++ b/spec/integration/supplier_spec.rb @@ -1,20 +1,20 @@ require_relative '../spec_helper' feature 'supplier' do - let(:supplier) { create :supplier } - let(:user) { create :user, :role_suppliers } + let(:supplier) { create(:supplier) } + let(:user) { create(:user, :role_suppliers) } before { login user } describe 'create new' do it 'can be created' do - create :supplier_category + create(:supplier_category) visit new_supplier_path - supplier = build :supplier + supplier = build(:supplier) within('#new_supplier') do - fill_in 'supplier_name', :with => supplier.name - fill_in 'supplier_address', :with => supplier.address - fill_in 'supplier_phone', :with => supplier.phone + fill_in 'supplier_name', with: supplier.name + fill_in 'supplier_address', with: supplier.address + fill_in 'supplier_phone', with: supplier.phone find('input[type="submit"]').click end expect(page).to have_content(supplier.name) @@ -38,7 +38,7 @@ feature 'supplier' do end it 'can be updated' do - new_supplier = build :supplier + new_supplier = build(:supplier) supplier visit edit_supplier_path(id: supplier.id) fill_in 'supplier_name', with: new_supplier.name diff --git a/spec/lib/bank_account_information_importer_spec.rb b/spec/lib/bank_account_information_importer_spec.rb index 40c3b1ea..cbe48fe2 100644 --- a/spec/lib/bank_account_information_importer_spec.rb +++ b/spec/lib/bank_account_information_importer_spec.rb @@ -1,7 +1,7 @@ require_relative '../spec_helper' describe BankTransaction do - let(:bank_account) { create :bank_account } + let(:bank_account) { create(:bank_account) } it 'empty content' do content = '' @@ -188,7 +188,7 @@ describe BankTransaction do expect(bt.date).to eq('2019-02-13'.to_date) expect(bt.text).to eq('Deutsche Bundesbahn') expect(bt.iban).to eq('DE72957284895783674747') - expect(bt.reference).to eq("743574386368 Muenchen-Hamburg 27.03.2019") + expect(bt.reference).to eq('743574386368 Muenchen-Hamburg 27.03.2019') expect(bt.receipt).to eq('Lastschrift') end @@ -277,7 +277,7 @@ describe BankTransaction do expect(bt.date).to eq('2019-02-14'.to_date) expect(bt.text).to eq('superbank AG') expect(bt.iban).to be_nil - expect(bt.reference).to eq("Überweisung US, Wechselspesen u Provision") + expect(bt.reference).to eq('Überweisung US, Wechselspesen u Provision') expect(bt.receipt).to eq('Spesen/Gebühren') end @@ -384,7 +384,7 @@ describe BankTransaction do expect(bank_account.last_transaction_date).to eq('2020-01-01'.to_date) expect(bank_account.balance).to eq(22) - bt1 = bank_account.bank_transactions.find_by_external_id("T1") + bt1 = bank_account.bank_transactions.find_by_external_id('T1') expect(bt1.amount).to eq(11) expect(bt1.date).to eq('2020-01-01'.to_date) expect(bt1.text).to eq('DN1') @@ -392,7 +392,7 @@ describe BankTransaction do expect(bt1.reference).to eq('') expect(bt1.receipt).to eq('AI1') - bt2 = bank_account.bank_transactions.find_by_external_id("T2") + bt2 = bank_account.bank_transactions.find_by_external_id('T2') expect(bt2.amount).to eq(-22) expect(bt2.date).to eq('2010-02-01'.to_date) expect(bt2.text).to eq('CN2') @@ -400,7 +400,7 @@ describe BankTransaction do expect(bt2.reference).to eq('RI2') expect(bt2.receipt).to be_nil - bt3 = bank_account.bank_transactions.find_by_external_id("T3") + bt3 = bank_account.bank_transactions.find_by_external_id('T3') expect(bt3.amount).to eq(33) expect(bt3.date).to eq('2000-03-01'.to_date) expect(bt3.text).to eq('DN3') diff --git a/spec/lib/bank_transaction_reference_spec.rb b/spec/lib/bank_transaction_reference_spec.rb index a03e901b..de999906 100644 --- a/spec/lib/bank_transaction_reference_spec.rb +++ b/spec/lib/bank_transaction_reference_spec.rb @@ -34,62 +34,65 @@ describe BankTransactionReference do end it 'returns correct value for FS1A1' do - expect(BankTransactionReference.parse('FS1A1')).to match({ group: 1, parts: { "A" => 1 } }) + expect(BankTransactionReference.parse('FS1A1')).to match({ group: 1, parts: { 'A' => 1 } }) end it 'returns correct value for FS1.2A3' do - expect(BankTransactionReference.parse('FS1.2A3')).to match({ group: 1, user: 2, parts: { "A" => 3 } }) + expect(BankTransactionReference.parse('FS1.2A3')).to match({ group: 1, user: 2, parts: { 'A' => 3 } }) end it 'returns correct value for FS1A2B3C4' do - expect(BankTransactionReference.parse('FS1A2B3C4')).to match({ group: 1, parts: { "A" => 2, "B" => 3, "C" => 4 } }) + expect(BankTransactionReference.parse('FS1A2B3C4')).to match({ group: 1, parts: { 'A' => 2, 'B' => 3, 'C' => 4 } }) end it 'returns correct value for FS1A2B3A4' do - expect(BankTransactionReference.parse('FS1A2B3A4')).to match({ group: 1, parts: { "A" => 6, "B" => 3 } }) + expect(BankTransactionReference.parse('FS1A2B3A4')).to match({ group: 1, parts: { 'A' => 6, 'B' => 3 } }) end it 'returns correct value for FS1A2.34B5.67C8.90' do - expect(BankTransactionReference.parse('FS1A2.34B5.67C8.90')).to match({ group: 1, parts: { "A" => 2.34, "B" => 5.67, "C" => 8.90 } }) + expect(BankTransactionReference.parse('FS1A2.34B5.67C8.90')).to match({ group: 1, + parts: { 'A' => 2.34, 'B' => 5.67, + 'C' => 8.90 } }) end it 'returns correct value for FS123A456 with comma-separated prefix' do - expect(BankTransactionReference.parse('x,FS123A456')).to match({ group: 123, parts: { "A" => 456 } }) + expect(BankTransactionReference.parse('x,FS123A456')).to match({ group: 123, parts: { 'A' => 456 } }) end it 'returns correct value for FS123A456 with minus-separated prefix' do - expect(BankTransactionReference.parse('x-FS123A456')).to match({ group: 123, parts: { "A" => 456 } }) + expect(BankTransactionReference.parse('x-FS123A456')).to match({ group: 123, parts: { 'A' => 456 } }) end it 'returns correct value for FS123A456 with semicolon-separated prefix' do - expect(BankTransactionReference.parse('x;FS123A456')).to match({ group: 123, parts: { "A" => 456 } }) + expect(BankTransactionReference.parse('x;FS123A456')).to match({ group: 123, parts: { 'A' => 456 } }) end it 'returns correct value for FS123A456 with space-separated prefix' do - expect(BankTransactionReference.parse('x FS123A456')).to match({ group: 123, parts: { "A" => 456 } }) + expect(BankTransactionReference.parse('x FS123A456')).to match({ group: 123, parts: { 'A' => 456 } }) end it 'returns correct value for FS234A567 with comma-separated suffix' do - expect(BankTransactionReference.parse('FS234A567,x')).to match({ group: 234, parts: { "A" => 567 } }) + expect(BankTransactionReference.parse('FS234A567,x')).to match({ group: 234, parts: { 'A' => 567 } }) end it 'returns correct value for FS234A567 with minus-separated suffix' do - expect(BankTransactionReference.parse('FS234A567-x')).to match({ group: 234, parts: { "A" => 567 } }) + expect(BankTransactionReference.parse('FS234A567-x')).to match({ group: 234, parts: { 'A' => 567 } }) end it 'returns correct value for FS234A567 with space-separated suffix' do - expect(BankTransactionReference.parse('FS234A567 x')).to match({ group: 234, parts: { "A" => 567 } }) + expect(BankTransactionReference.parse('FS234A567 x')).to match({ group: 234, parts: { 'A' => 567 } }) end it 'returns correct value for FS234A567 with semicolon-separated suffix' do - expect(BankTransactionReference.parse('FS234A567;x')).to match({ group: 234, parts: { "A" => 567 } }) + expect(BankTransactionReference.parse('FS234A567;x')).to match({ group: 234, parts: { 'A' => 567 } }) end it 'returns correct value for FS234A567 with minus-separated suffix' do - expect(BankTransactionReference.parse('FS234A567-x')).to match({ group: 234, parts: { "A" => 567 } }) + expect(BankTransactionReference.parse('FS234A567-x')).to match({ group: 234, parts: { 'A' => 567 } }) end it 'returns correct value for FS34.56A67.89 with prefix and suffix' do - expect(BankTransactionReference.parse('prefix FS34.56A67.89, suffix')).to match({ group: 34, user: 56, parts: { "A" => 67.89 } }) + expect(BankTransactionReference.parse('prefix FS34.56A67.89, suffix')).to match({ group: 34, user: 56, + parts: { 'A' => 67.89 } }) end end diff --git a/spec/lib/foodsoft_mail_receiver_spec.rb b/spec/lib/foodsoft_mail_receiver_spec.rb index 59eab47b..47ddb57d 100644 --- a/spec/lib/foodsoft_mail_receiver_spec.rb +++ b/spec/lib/foodsoft_mail_receiver_spec.rb @@ -6,55 +6,6 @@ describe FoodsoftMailReceiver do @server.start end - it 'does not accept empty addresses' do - begin - FoodsoftMailReceiver.received('', 'body') - rescue => error - expect(error.to_s).to include 'missing' - end - end - - it 'does not accept invalid addresses' do - begin - FoodsoftMailReceiver.received('invalid', 'body') - rescue => error - expect(error.to_s).to include 'has an invalid format' - end - end - - it 'does not accept invalid scope in address' do - begin - FoodsoftMailReceiver.received('invalid.invalid', 'body') - rescue => error - expect(error.to_s).to include 'could not be found' - end - end - - it 'does not accept address without handler' do - begin - address = "#{FoodsoftConfig[:default_scope]}.invalid" - FoodsoftMailReceiver.received(address, 'body') - rescue => error - expect(error.to_s).to include 'invalid format for recipient' - end - end - - it 'does not accept invalid addresses via SMTP' do - expect { - Net::SMTP.start(@server.hosts.first, @server.ports.first) do |smtp| - smtp.send_message 'body', 'from@example.com', 'invalid' - end - }.to raise_error(Net::SMTPFatalError) - end - - it 'does not accept invalid addresses via SMTP' do - expect { - Net::SMTP.start(@server.hosts.first, @server.ports.first) do |smtp| - smtp.send_message 'body', 'from@example.com', 'invalid' - end - }.to raise_error(Net::SMTPFatalError) - end - # TODO: Reanable this test. # It raised "Mysql2::Error: Lock wait timeout exceeded" at time of writing. # it 'accepts bounce mails via SMTP' do @@ -74,4 +25,45 @@ describe FoodsoftMailReceiver do after :all do @server.shutdown end + + it 'does not accept empty addresses' do + FoodsoftMailReceiver.received('', 'body') + rescue StandardError => e + expect(e.to_s).to include 'missing' + end + + it 'does not accept invalid addresses' do + FoodsoftMailReceiver.received('invalid', 'body') + rescue StandardError => e + expect(e.to_s).to include 'has an invalid format' + end + + it 'does not accept invalid scope in address' do + FoodsoftMailReceiver.received('invalid.invalid', 'body') + rescue StandardError => e + expect(e.to_s).to include 'could not be found' + end + + it 'does not accept address without handler' do + address = "#{FoodsoftConfig[:default_scope]}.invalid" + FoodsoftMailReceiver.received(address, 'body') + rescue StandardError => e + expect(e.to_s).to include 'invalid format for recipient' + end + + it 'does not accept invalid addresses via SMTP' do + expect do + Net::SMTP.start(@server.hosts.first, @server.ports.first) do |smtp| + smtp.send_message 'body', 'from@example.com', 'invalid' + end + end.to raise_error(Net::SMTPFatalError) + end + + it 'does not accept invalid addresses via SMTP' do + expect do + Net::SMTP.start(@server.hosts.first, @server.ports.first) do |smtp| + smtp.send_message 'body', 'from@example.com', 'invalid' + end + end.to raise_error(Net::SMTPFatalError) + end end diff --git a/spec/lib/token_verifier_spec.rb b/spec/lib/token_verifier_spec.rb index d4a7e5ea..3097e888 100644 --- a/spec/lib/token_verifier_spec.rb +++ b/spec/lib/token_verifier_spec.rb @@ -6,12 +6,12 @@ describe TokenVerifier do let(:msg) { v.generate } it 'validates' do - expect { v.verify(msg) }.to_not raise_error + expect { v.verify(msg) }.not_to raise_error end it 'validates when recreated' do v2 = TokenVerifier.new(prefix) - expect { v2.verify(msg) }.to_not raise_error + expect { v2.verify(msg) }.not_to raise_error end it 'does not validate with a different prefix' do @@ -32,7 +32,9 @@ describe TokenVerifier do end it 'does not validate a random string' do - expect { v.verify(Faker::Lorem.characters(number: 100)) }.to raise_error(ActiveSupport::MessageVerifier::InvalidSignature) + expect do + v.verify(Faker::Lorem.characters(number: 100)) + end.to raise_error(ActiveSupport::MessageVerifier::InvalidSignature) end it 'returns the message' do diff --git a/spec/models/article_spec.rb b/spec/models/article_spec.rb index 519f5b64..f104c101 100644 --- a/spec/models/article_spec.rb +++ b/spec/models/article_spec.rb @@ -1,11 +1,11 @@ require_relative '../spec_helper' describe Article do - let(:supplier) { create :supplier } - let(:article) { create :article, supplier: supplier } + let(:supplier) { create(:supplier) } + let(:article) { create(:article, supplier: supplier) } it 'has a unique name' do - article2 = build :article, supplier: supplier, name: article.name + article2 = build(:article, supplier: supplier, name: article.name) expect(article2).to be_invalid end @@ -21,21 +21,21 @@ describe Article do end it 'returns false when invalid unit' do - article1 = build :article, supplier: supplier, unit: 'invalid' + article1 = build(:article, supplier: supplier, unit: 'invalid') expect(article.convert_units(article1)).to be false end it 'converts from ST to KI (german foodcoops legacy)' do - article1 = build :article, supplier: supplier, unit: 'ST' - article2 = build :article, supplier: supplier, name: 'banana 10-12 St', price: 12.34, unit: 'KI' + article1 = build(:article, supplier: supplier, unit: 'ST') + article2 = build(:article, supplier: supplier, name: 'banana 10-12 St', price: 12.34, unit: 'KI') new_price, new_unit_quantity = article1.convert_units(article2) expect(new_unit_quantity).to eq 10 expect(new_price).to eq 1.23 end it 'converts from g to kg' do - article1 = build :article, supplier: supplier, unit: 'kg' - article2 = build :article, supplier: supplier, unit: 'g', price: 0.12, unit_quantity: 1500 + article1 = build(:article, supplier: supplier, unit: 'kg') + article2 = build(:article, supplier: supplier, unit: 'g', price: 0.12, unit_quantity: 1500) new_price, new_unit_quantity = article1.convert_units(article2) expect(new_unit_quantity).to eq 1.5 expect(new_price).to eq 120 @@ -43,7 +43,7 @@ describe Article do end it 'computes changed article attributes' do - article2 = build :article, supplier: supplier, name: 'banana' + article2 = build(:article, supplier: supplier, name: 'banana') expect(article.unequal_attributes(article2)[:name]).to eq 'banana' end @@ -83,7 +83,7 @@ describe Article do end it 'is knows its open order' do - order = create :order, supplier: supplier, article_ids: [article.id] + order = create(:order, supplier: supplier, article_ids: [article.id]) expect(article.in_open_order).to eq(order) end @@ -91,26 +91,26 @@ describe Article do expect(article.shared_article).to be_nil end - describe 'connected to a shared database', :type => :feature do - let(:shared_article) { create :shared_article } - let(:supplier) { create :supplier, shared_supplier_id: shared_article.supplier_id } - let(:article) { create :article, supplier: supplier, order_number: shared_article.order_number } + describe 'connected to a shared database', type: :feature do + let(:shared_article) { create(:shared_article) } + let(:supplier) { create(:supplier, shared_supplier_id: shared_article.supplier_id) } + let(:article) { create(:article, supplier: supplier, order_number: shared_article.order_number) } it 'can be found in the shared database' do - expect(article.shared_article).to_not be_nil + expect(article.shared_article).not_to be_nil end it 'can find updates' do changed = article.shared_article_changed? - expect(changed).to_not be_falsey + expect(changed).not_to be_falsey expect(changed.length).to be > 1 end it 'can be synchronised' do - # TODO move article sync from supplier to article + # TODO: move article sync from supplier to article article # need to reference for it to exist when syncing updated_article = supplier.sync_all[0].select { |s| s[0].id == article.id }.first[0] - article.update(updated_article.attributes.reject { |k, v| k == 'id' or k == 'type' }) + article.update(updated_article.attributes.reject { |k, _v| %w[id type].include?(k) }) expect(article.name).to eq(shared_article.name) # now synchronising shouldn't change anything anymore expect(article.shared_article_changed?).to be_falsey @@ -131,9 +131,9 @@ describe Article do article.unit = '200g' article.shared_updated_on -= 1 # to make update do something article.save! - # TODO get sync functionality in article + # TODO: get sync functionality in article updated_article = supplier.sync_all[0].select { |s| s[0].id == article.id }.first[0] - article.update!(updated_article.attributes.reject { |k, v| k == 'id' or k == 'type' }) + article.update!(updated_article.attributes.reject { |k, _v| %w[id type].include?(k) }) expect(article.unit).to eq '200g' expect(article.unit_quantity).to eq 5 expect(article.price).to be_within(0.005).of(shared_article.price / 5) diff --git a/spec/models/bank_transaction_spec.rb b/spec/models/bank_transaction_spec.rb index 21d6185e..14172676 100644 --- a/spec/models/bank_transaction_spec.rb +++ b/spec/models/bank_transaction_spec.rb @@ -1,24 +1,32 @@ require_relative '../spec_helper' describe BankTransaction do - let(:bank_account) { create :bank_account } - let(:ordergroup) { create :ordergroup } - let(:supplier) { create :supplier, iban: Faker::Bank.iban } - let!(:user) { create :user, groups: [ordergroup] } - let!(:ftt_a) { create :financial_transaction_type, name_short: 'A' } - let!(:ftt_b) { create :financial_transaction_type, name_short: 'B' } + let(:bank_account) { create(:bank_account) } + let(:ordergroup) { create(:ordergroup) } + let(:supplier) { create(:supplier, iban: Faker::Bank.iban) } + let!(:user) { create(:user, groups: [ordergroup]) } + let!(:ftt_a) { create(:financial_transaction_type, name_short: 'A') } + let!(:ftt_b) { create(:financial_transaction_type, name_short: 'B') } describe 'supplier' do - let!(:invoice1) { create :invoice, supplier: supplier, number: '11', amount: 10 } - let!(:invoice2) { create :invoice, supplier: supplier, number: '22', amount: 20 } - let!(:invoice3) { create :invoice, supplier: supplier, number: '33', amount: 30 } - let!(:invoice4) { create :invoice, supplier: supplier, number: '44', amount: 40 } - let!(:invoice5) { create :invoice, supplier: supplier, number: '55', amount: 50 } + let!(:invoice1) { create(:invoice, supplier: supplier, number: '11', amount: 10) } + let!(:invoice2) { create(:invoice, supplier: supplier, number: '22', amount: 20) } + let!(:invoice3) { create(:invoice, supplier: supplier, number: '33', amount: 30) } + let!(:invoice4) { create(:invoice, supplier: supplier, number: '44', amount: 40) } + let!(:invoice5) { create(:invoice, supplier: supplier, number: '55', amount: 50) } - let!(:bank_transaction1) { create :bank_transaction, bank_account: bank_account, iban: supplier.iban, reference: '11', amount: 10 } - let!(:bank_transaction2) { create :bank_transaction, bank_account: bank_account, iban: supplier.iban, reference: '22', amount: -20 } - let!(:bank_transaction3) { create :bank_transaction, bank_account: bank_account, iban: supplier.iban, reference: '33,44', amount: -70 } - let!(:bank_transaction4) { create :bank_transaction, bank_account: bank_account, iban: supplier.iban, text: '55', amount: -50 } + let!(:bank_transaction1) do + create(:bank_transaction, bank_account: bank_account, iban: supplier.iban, reference: '11', amount: 10) + end + let!(:bank_transaction2) do + create(:bank_transaction, bank_account: bank_account, iban: supplier.iban, reference: '22', amount: -20) + end + let!(:bank_transaction3) do + create(:bank_transaction, bank_account: bank_account, iban: supplier.iban, reference: '33,44', amount: -70) + end + let!(:bank_transaction4) do + create(:bank_transaction, bank_account: bank_account, iban: supplier.iban, text: '55', amount: -50) + end it 'ignores invoices with invalid amount' do expect(bank_transaction1.assign_to_invoice).to be false @@ -49,14 +57,26 @@ describe BankTransaction do end describe 'ordergroup' do - let!(:bank_transaction1) { create :bank_transaction, bank_account: bank_account, reference: "invalid", amount: 10 } - let!(:bank_transaction2) { create :bank_transaction, bank_account: bank_account, reference: "FS99A10", amount: 10 } - let!(:bank_transaction3) { create :bank_transaction, bank_account: bank_account, reference: "FS#{ordergroup.id}.99A10", amount: 10 } - let!(:bank_transaction4) { create :bank_transaction, bank_account: bank_account, reference: "FS#{ordergroup.id}A10", amount: 99 } - let!(:bank_transaction5) { create :bank_transaction, bank_account: bank_account, reference: "FS#{ordergroup.id}A10", amount: 10 } - let!(:bank_transaction6) { create :bank_transaction, bank_account: bank_account, reference: "FS#{ordergroup.id}A10B20", amount: 30 } - let!(:bank_transaction7) { create :bank_transaction, bank_account: bank_account, reference: "FS#{ordergroup.id}.#{user.id}A10", amount: 10 } - let!(:bank_transaction8) { create :bank_transaction, bank_account: bank_account, reference: "FS#{ordergroup.id}X10", amount: 10 } + let!(:bank_transaction1) { create(:bank_transaction, bank_account: bank_account, reference: 'invalid', amount: 10) } + let!(:bank_transaction2) { create(:bank_transaction, bank_account: bank_account, reference: 'FS99A10', amount: 10) } + let!(:bank_transaction3) do + create(:bank_transaction, bank_account: bank_account, reference: "FS#{ordergroup.id}.99A10", amount: 10) + end + let!(:bank_transaction4) do + create(:bank_transaction, bank_account: bank_account, reference: "FS#{ordergroup.id}A10", amount: 99) + end + let!(:bank_transaction5) do + create(:bank_transaction, bank_account: bank_account, reference: "FS#{ordergroup.id}A10", amount: 10) + end + let!(:bank_transaction6) do + create(:bank_transaction, bank_account: bank_account, reference: "FS#{ordergroup.id}A10B20", amount: 30) + end + let!(:bank_transaction7) do + create(:bank_transaction, bank_account: bank_account, reference: "FS#{ordergroup.id}.#{user.id}A10", amount: 10) + end + let!(:bank_transaction8) do + create(:bank_transaction, bank_account: bank_account, reference: "FS#{ordergroup.id}X10", amount: 10) + end it 'ignores transaction with invalid reference' do expect(bank_transaction1.assign_to_ordergroup).to be_nil diff --git a/spec/models/delivery_spec.rb b/spec/models/delivery_spec.rb index b48449ab..80f4c2ed 100644 --- a/spec/models/delivery_spec.rb +++ b/spec/models/delivery_spec.rb @@ -1,8 +1,8 @@ require_relative '../spec_helper' describe Delivery do - let(:delivery) { create :delivery } - let(:stock_article) { create :stock_article, price: 3 } + let(:delivery) { create(:delivery) } + let(:stock_article) { create(:stock_article, price: 3) } it 'creates new stock_changes' do delivery.new_stock_changes = ([ diff --git a/spec/models/group_order_article_spec.rb b/spec/models/group_order_article_spec.rb index 42178a6c..434f9a29 100644 --- a/spec/models/group_order_article_spec.rb +++ b/spec/models/group_order_article_spec.rb @@ -1,10 +1,10 @@ require_relative '../spec_helper' describe GroupOrderArticle do - let(:user) { create :user, groups: [create(:ordergroup)] } + let(:user) { create(:user, groups: [create(:ordergroup)]) } let(:order) { create(:order) } - let(:go) { create :group_order, order: order, ordergroup: user.ordergroup } - let(:goa) { create :group_order_article, group_order: go, order_article: order.order_articles.first } + let(:go) { create(:group_order, order: order, ordergroup: user.ordergroup) } + let(:goa) { create(:group_order_article, group_order: go, order_article: order.order_articles.first) } it 'has zero quantity by default' do expect(goa.quantity).to eq(0) end it 'has zero tolerance by default' do expect(goa.tolerance).to eq(0) end @@ -12,9 +12,9 @@ describe GroupOrderArticle do it 'has zero total price by default' do expect(goa.total_price).to eq(0) end describe do - let(:article) { create :article, supplier: order.supplier, unit_quantity: 1 } - let(:oa) { order.order_articles.create(:article => article) } - let(:goa) { create :group_order_article, group_order: go, order_article: oa } + let(:article) { create(:article, supplier: order.supplier, unit_quantity: 1) } + let(:oa) { order.order_articles.create(article: article) } + let(:goa) { create(:group_order_article, group_order: go, order_article: oa) } it 'can be ordered by piece' do goa.update_quantities(1, 0) @@ -23,7 +23,8 @@ describe GroupOrderArticle do end it 'can be ordered in larger amounts' do - quantity, tolerance = rand(13..99), rand(0..99) + quantity = rand(13..99) + tolerance = rand(0..99) goa.update_quantities(quantity, tolerance) expect(goa.quantity).to eq(quantity) expect(goa.tolerance).to eq(tolerance) @@ -52,10 +53,10 @@ describe GroupOrderArticle do end describe 'distribution strategy' do - let(:article) { create :article, supplier: order.supplier, unit_quantity: 1 } - let(:oa) { order.order_articles.create(:article => article) } - let(:goa) { create :group_order_article, group_order: go, order_article: oa } - let!(:goaq) { create :group_order_article_quantity, group_order_article: goa, quantity: 4, tolerance: 6 } + let(:article) { create(:article, supplier: order.supplier, unit_quantity: 1) } + let(:oa) { order.order_articles.create(article: article) } + let(:goa) { create(:group_order_article, group_order: go, order_article: oa) } + let!(:goaq) { create(:group_order_article_quantity, group_order_article: goa, quantity: 4, tolerance: 6) } it 'can calculate the result for the distribution strategy "first order first serve"' do res = goa.calculate_result(2) diff --git a/spec/models/group_order_spec.rb b/spec/models/group_order_spec.rb index a2b8a2c5..648fa100 100644 --- a/spec/models/group_order_spec.rb +++ b/spec/models/group_order_spec.rb @@ -1,8 +1,8 @@ require_relative '../spec_helper' describe GroupOrder do - let(:user) { create :user, groups: [create(:ordergroup)] } - let(:order) { create :order } + let(:user) { create(:user, groups: [create(:ordergroup)]) } + let(:order) { create(:order) } # the following two tests are currently disabled - https://github.com/foodcoops/foodsoft/issues/158 @@ -15,7 +15,7 @@ describe GroupOrder do # end describe do - let(:go) { create :group_order, order: order, ordergroup: user.ordergroup } + let(:go) { create(:group_order, order: order, ordergroup: user.ordergroup) } it 'has zero price initially' do expect(go.price).to eq(0) diff --git a/spec/models/order_article_spec.rb b/spec/models/order_article_spec.rb index 829678a9..cef0e09c 100644 --- a/spec/models/order_article_spec.rb +++ b/spec/models/order_article_spec.rb @@ -1,14 +1,14 @@ require 'spec_helper' describe OrderArticle do - let(:order) { create :order, article_count: 1 } + let(:order) { create(:order, article_count: 1) } let(:oa) { order.order_articles.first } it 'is not ordered by default' do expect(OrderArticle.ordered.count).to eq 0 end - [:units_to_order, :units_billed, :units_received].each do |units| + %i[units_to_order units_billed units_received].each do |units| it "is ordered when there are #{units.to_s.gsub '_', ' '}" do oa.update_attribute units, rand(1..99) expect(OrderArticle.ordered.count).to eq 1 @@ -46,15 +46,15 @@ describe OrderArticle do end describe 'redistribution' do - let(:admin) { create :user, groups: [create(:workgroup, role_finance: true)] } - let(:article) { create :article, unit_quantity: 3 } - let(:order) { create :order, article_ids: [article.id] } - let(:go1) { create :group_order, order: order } - let(:go2) { create :group_order, order: order } - let(:go3) { create :group_order, order: order } - let(:goa1) { create :group_order_article, group_order: go1, order_article: oa } - let(:goa2) { create :group_order_article, group_order: go2, order_article: oa } - let(:goa3) { create :group_order_article, group_order: go3, order_article: oa } + let(:admin) { create(:user, groups: [create(:workgroup, role_finance: true)]) } + let(:article) { create(:article, unit_quantity: 3) } + let(:order) { create(:order, article_ids: [article.id]) } + let(:go1) { create(:group_order, order: order) } + let(:go2) { create(:group_order, order: order) } + let(:go3) { create(:group_order, order: order) } + let(:goa1) { create(:group_order_article, group_order: go1, order_article: oa) } + let(:goa2) { create(:group_order_article, group_order: go2, order_article: oa) } + let(:goa3) { create(:group_order_article, group_order: go3, order_article: oa) } # set quantities of group_order_articles def set_quantities(q1, q2, q3) @@ -79,21 +79,21 @@ describe OrderArticle do it 'does nothing when nothing has changed' do set_quantities [3, 2], [1, 3], [1, 0] - expect(oa.redistribute 6, [:tolerance, nil]).to eq [1, 0] + expect(oa.redistribute(6, [:tolerance, nil])).to eq [1, 0] goa_reload expect([goa1, goa2, goa3].map(&:result).map(&:to_i)).to eq [4, 1, 1] end it 'works when there is nothing to distribute' do set_quantities [3, 2], [1, 3], [1, 0] - expect(oa.redistribute 0, [:tolerance, nil]).to eq [0, 0] + expect(oa.redistribute(0, [:tolerance, nil])).to eq [0, 0] goa_reload expect([goa1, goa2, goa3].map(&:result)).to eq [0, 0, 0] end it 'works when quantity needs to be reduced' do set_quantities [3, 2], [1, 3], [1, 0] - expect(oa.redistribute 4, [:tolerance, nil]).to eq [0, 0] + expect(oa.redistribute(4, [:tolerance, nil])).to eq [0, 0] goa_reload expect([goa1, goa2, goa3].map(&:result)).to eq [3, 1, 0] end @@ -101,28 +101,28 @@ describe OrderArticle do it 'works when quantity is increased within quantity' do set_quantities [3, 0], [2, 0], [2, 0] expect([goa1, goa2, goa3].map(&:result)).to eq [3, 2, 1] - expect(oa.redistribute 7, [:tolerance, nil]).to eq [0, 0] + expect(oa.redistribute(7, [:tolerance, nil])).to eq [0, 0] goa_reload expect([goa1, goa2, goa3].map(&:result).map(&:to_i)).to eq [3, 2, 2] end it 'works when there is just one for the first' do set_quantities [3, 2], [1, 3], [1, 0] - expect(oa.redistribute 1, [:tolerance, nil]).to eq [0, 0] + expect(oa.redistribute(1, [:tolerance, nil])).to eq [0, 0] goa_reload expect([goa1, goa2, goa3].map(&:result)).to eq [1, 0, 0] end it 'works when there is tolerance and left-over' do set_quantities [3, 2], [1, 1], [1, 0] - expect(oa.redistribute 10, [:tolerance, nil]).to eq [3, 2] + expect(oa.redistribute(10, [:tolerance, nil])).to eq [3, 2] goa_reload expect([goa1, goa2, goa3].map(&:result)).to eq [5, 2, 1] end it 'works when redistributing without tolerance' do set_quantities [3, 2], [1, 3], [1, 0] - expect(oa.redistribute 8, [nil]).to eq [3] + expect(oa.redistribute(8, [nil])).to eq [3] goa_reload expect([goa1, goa2, goa3].map(&:result)).to eq [3, 1, 1] end @@ -131,17 +131,18 @@ describe OrderArticle do describe 'boxfill' do before { FoodsoftConfig[:use_boxfill] = true } - let(:article) { create :article, unit_quantity: 6 } - let(:order) { create :order, article_ids: [article.id], starts: 1.week.ago } + let(:article) { create(:article, unit_quantity: 6) } + let(:order) { create(:order, article_ids: [article.id], starts: 1.week.ago) } let(:oa) { order.order_articles.first } - let(:go) { create :group_order, order: order } - let(:goa) { create :group_order_article, group_order: go, order_article: oa } + let(:go) { create(:group_order, order: order) } + let(:goa) { create(:group_order_article, group_order: go, order_article: oa) } - shared_examples "boxfill" do |success, q| + shared_examples 'boxfill' do |success, q| # initial situation before do goa.update_quantities(*q.keys[0]) - oa.update_results!; oa.reload + oa.update_results! + oa.reload end # check starting condition @@ -172,11 +173,11 @@ describe OrderArticle do let(:boxfill_from) { 1.hour.from_now } context 'decreasing the missing units' do - include_examples "boxfill", true, [6, 0] => [5, 0], [6, 0, 0] => [5, 0, 1] + include_examples 'boxfill', true, [6, 0] => [5, 0], [6, 0, 0] => [5, 0, 1] end context 'decreasing the tolerance' do - include_examples "boxfill", true, [1, 2] => [1, 1], [1, 2, 3] => [1, 1, 4] + include_examples 'boxfill', true, [1, 2] => [1, 1], [1, 2, 3] => [1, 1, 4] end end @@ -184,27 +185,27 @@ describe OrderArticle do let(:boxfill_from) { 1.second.ago } context 'changing nothing in particular' do - include_examples "boxfill", true, [4, 1] => [4, 1], [4, 1, 1] => [4, 1, 1] + include_examples 'boxfill', true, [4, 1] => [4, 1], [4, 1, 1] => [4, 1, 1] end context 'increasing missing units' do - include_examples "boxfill", false, [3, 0] => [2, 0], [3, 0, 3] => [3, 0, 3] + include_examples 'boxfill', false, [3, 0] => [2, 0], [3, 0, 3] => [3, 0, 3] end context 'increasing tolerance' do - include_examples "boxfill", true, [2, 1] => [2, 2], [2, 1, 3] => [2, 2, 2] + include_examples 'boxfill', true, [2, 1] => [2, 2], [2, 1, 3] => [2, 2, 2] end context 'decreasing quantity to fix missing units' do - include_examples "boxfill", true, [7, 0] => [6, 0], [7, 0, 5] => [6, 0, 0] + include_examples 'boxfill', true, [7, 0] => [6, 0], [7, 0, 5] => [6, 0, 0] end context 'decreasing quantity keeping missing units equal' do - include_examples "boxfill", false, [7, 0] => [1, 0], [7, 0, 5] => [7, 0, 5] + include_examples 'boxfill', false, [7, 0] => [1, 0], [7, 0, 5] => [7, 0, 5] end context 'moving tolerance to quantity' do - include_examples "boxfill", true, [4, 2] => [6, 0], [4, 2, 0] => [6, 0, 0] + include_examples 'boxfill', true, [4, 2] => [6, 0], [4, 2, 0] => [6, 0, 0] end # @todo enable test when tolerance doesn't count in missing_units # context 'decreasing tolerance' do diff --git a/spec/models/order_spec.rb b/spec/models/order_spec.rb index aee48f33..71c46a84 100644 --- a/spec/models/order_spec.rb +++ b/spec/models/order_spec.rb @@ -1,14 +1,14 @@ require_relative '../spec_helper' describe Order do - let!(:ftt) { create :financial_transaction_type } - let(:user) { create :user, groups: [create(:ordergroup)] } + let!(:ftt) { create(:financial_transaction_type) } + let(:user) { create(:user, groups: [create(:ordergroup)]) } it 'automaticly finishes ended' do - create :order, created_by: user, starts: Date.yesterday, ends: 1.hour.from_now - create :order, created_by: user, starts: Date.yesterday, ends: 1.hour.ago - create :order, created_by: user, starts: Date.yesterday, ends: 1.hour.from_now, end_action: :auto_close - order = create :order, created_by: user, starts: Date.yesterday, ends: 1.hour.ago, end_action: :auto_close + create(:order, created_by: user, starts: Date.yesterday, ends: 1.hour.from_now) + create(:order, created_by: user, starts: Date.yesterday, ends: 1.hour.ago) + create(:order, created_by: user, starts: Date.yesterday, ends: 1.hour.from_now, end_action: :auto_close) + order = create(:order, created_by: user, starts: Date.yesterday, ends: 1.hour.ago, end_action: :auto_close) Order.finish_ended! order.reload @@ -19,10 +19,10 @@ describe Order do end describe 'state scopes and boolean getters' do - let!(:open_order) { create :order, state: 'open' } - let!(:finished_order) { create :order, state: 'finished' } - let!(:received_order) { create :order, state: 'received' } - let!(:closed_order) { create :order, state: 'closed' } + let!(:open_order) { create(:order, state: 'open') } + let!(:finished_order) { create(:order, state: 'finished') } + let!(:received_order) { create(:order, state: 'received') } + let!(:closed_order) { create(:order, state: 'closed') } it 'retrieves open orders in the "open" scope' do expect(Order.open.count).to eq(1) @@ -72,8 +72,9 @@ describe Order do end it 'sends mail if min_order_quantity has been reached' do - create :user, groups: [create(:ordergroup)] - create :order, created_by: user, starts: Date.yesterday, ends: 1.hour.ago, end_action: :auto_close_and_send_min_quantity + create(:user, groups: [create(:ordergroup)]) + create(:order, created_by: user, starts: Date.yesterday, ends: 1.hour.ago, + end_action: :auto_close_and_send_min_quantity) Order.finish_ended! expect(ActionMailer::Base.deliveries.count).to eq 1 @@ -84,7 +85,7 @@ describe Order do end it 'needs order articles' do - supplier = create :supplier, article_count: 0 + supplier = create(:supplier, article_count: 0) expect(build(:order, supplier: supplier)).to be_invalid end @@ -93,35 +94,35 @@ describe Order do end describe 'with articles' do - let(:order) { create :order } + let(:order) { create(:order) } it 'is open by default' do expect(order).to be_open end - it 'is not finished by default' do expect(order).to_not be_finished end - it 'is not closed by default' do expect(order).to_not be_closed end + it 'is not finished by default' do expect(order).not_to be_finished end + it 'is not closed by default' do expect(order).not_to be_closed end it 'has valid order articles' do order.order_articles.each { |oa| expect(oa).to be_valid } end it 'can be finished' do - # TODO randomise user + # TODO: randomise user order.finish!(user) - expect(order).to_not be_open + expect(order).not_to be_open expect(order).to be_finished - expect(order).to_not be_closed + expect(order).not_to be_closed end it 'can be closed' do - # TODO randomise user + # TODO: randomise user order.finish!(user) order.close!(user) - expect(order).to_not be_open + expect(order).not_to be_open expect(order).to be_closed end end describe 'with a default end date' do - let(:order) { create :order } + let(:order) { create(:order) } before do FoodsoftConfig[:order_schedule] = { ends: { recurr: 'FREQ=WEEKLY;BYDAY=MO', time: '9:00' } } @@ -138,10 +139,10 @@ describe Order do end describe 'mapped to GroupOrders' do - let!(:user) { create :user, groups: [create(:ordergroup)] } - let!(:order) { create :order } - let!(:order2) { create :order } - let!(:go) { create :group_order, order: order, ordergroup: user.ordergroup } + let!(:user) { create(:user, groups: [create(:ordergroup)]) } + let!(:order) { create(:order) } + let!(:order2) { create(:order) } + let!(:go) { create(:group_order, order: order, ordergroup: user.ordergroup) } it 'to map a user\'s GroupOrders to a list of Orders' do orders = Order.ordergroup_group_orders_map(user.ordergroup) @@ -156,10 +157,10 @@ describe Order do describe 'balancing charges correct amounts' do let!(:transport) { rand(0.1..26.0).round(2) } - let!(:order) { create :order, article_count: 1 } + let!(:order) { create(:order, article_count: 1) } let!(:oa) { order.order_articles.first } - let!(:go) { create :group_order, order: order, transport: transport } - let!(:goa) { create :group_order_article, group_order: go, order_article: oa, quantity: 1 } + let!(:go) { create(:group_order, order: order, transport: transport) } + let!(:goa) { create(:group_order_article, group_order: go, order_article: oa, quantity: 1) } before do goa.update_quantities(1, 0) diff --git a/spec/models/ordergroup_spec.rb b/spec/models/ordergroup_spec.rb index a7f0c94a..cdac353d 100644 --- a/spec/models/ordergroup_spec.rb +++ b/spec/models/ordergroup_spec.rb @@ -1,22 +1,22 @@ require_relative '../spec_helper' describe Ordergroup do - let(:ftc1) { create :financial_transaction_class } - let(:ftc2) { create :financial_transaction_class } - let(:ftt1) { create :financial_transaction_type, financial_transaction_class: ftc1 } - let(:ftt2) { create :financial_transaction_type, financial_transaction_class: ftc2 } - let(:ftt3) { create :financial_transaction_type, financial_transaction_class: ftc2 } - let(:user) { create :user, groups: [create(:ordergroup)] } + let(:ftc1) { create(:financial_transaction_class) } + let(:ftc2) { create(:financial_transaction_class) } + let(:ftt1) { create(:financial_transaction_type, financial_transaction_class: ftc1) } + let(:ftt2) { create(:financial_transaction_type, financial_transaction_class: ftc2) } + let(:ftt3) { create(:financial_transaction_type, financial_transaction_class: ftc2) } + let(:user) { create(:user, groups: [create(:ordergroup)]) } it 'shows no active ordergroups when all orders are older than 3 months' do - order = create :order, starts: 4.months.ago + order = create(:order, starts: 4.months.ago) user.ordergroup.group_orders.create!(order: order) expect(Ordergroup.active).to be_empty end it 'shows active ordergroups when there are recent orders' do - order = create :order, starts: 2.days.ago + order = create(:order, starts: 2.days.ago) user.ordergroup.group_orders.create!(order: order) expect(Ordergroup.active).not_to be_empty @@ -24,17 +24,17 @@ describe Ordergroup do describe 'sort correctly' do it 'by name' do - group_b = create :ordergroup, name: 'bbb' - group_a = create :ordergroup, name: 'aaa' - group_c = create :ordergroup, name: 'ccc' + group_b = create(:ordergroup, name: 'bbb') + group_a = create(:ordergroup, name: 'aaa') + group_c = create(:ordergroup, name: 'ccc') expect(Ordergroup.sort_by_param('name')).to eq([group_a, group_b, group_c]) end it 'reverse by name' do - group_b = create :ordergroup, name: 'bbb' - group_a = create :ordergroup, name: 'aaa' - group_c = create :ordergroup, name: 'ccc' + group_b = create(:ordergroup, name: 'bbb') + group_a = create(:ordergroup, name: 'aaa') + group_c = create(:ordergroup, name: 'ccc') expect(Ordergroup.sort_by_param('name_reverse')).to eq([group_c, group_b, group_a]) end @@ -43,9 +43,9 @@ describe Ordergroup do users_b = [create(:user)] users_a = [] users_c = [create(:user), create(:user), create(:user)] - group_b = create :ordergroup, name: 'bbb', user_ids: users_b.map(&:id) - group_a = create :ordergroup, name: 'aaa', user_ids: users_a.map(&:id) - group_c = create :ordergroup, name: 'ccc', user_ids: users_c.map(&:id) + group_b = create(:ordergroup, name: 'bbb', user_ids: users_b.map(&:id)) + group_a = create(:ordergroup, name: 'aaa', user_ids: users_a.map(&:id)) + group_c = create(:ordergroup, name: 'ccc', user_ids: users_c.map(&:id)) expect(Ordergroup.sort_by_param('members_count')).to eq([group_a, group_b, group_c]) end @@ -54,39 +54,39 @@ describe Ordergroup do users_b = [create(:user)] users_a = [] users_c = [create(:user), create(:user), create(:user)] - group_b = create :ordergroup, name: 'bbb', user_ids: users_b.map(&:id) - group_a = create :ordergroup, name: 'aaa', user_ids: users_a.map(&:id) - group_c = create :ordergroup, name: 'ccc', user_ids: users_c.map(&:id) + group_b = create(:ordergroup, name: 'bbb', user_ids: users_b.map(&:id)) + group_a = create(:ordergroup, name: 'aaa', user_ids: users_a.map(&:id)) + group_c = create(:ordergroup, name: 'ccc', user_ids: users_c.map(&:id)) expect(Ordergroup.sort_by_param('members_count_reverse')).to eq([group_c, group_b, group_a]) end it 'by last_user_activity' do - user_b = create :user, last_activity: 3.days.ago - user_a = create :user, last_activity: 5.days.ago - user_c = create :user, last_activity: Time.now - group_b = create :ordergroup, name: 'bbb', user_ids: [user_b.id] - group_a = create :ordergroup, name: 'aaa', user_ids: [user_a.id] - group_c = create :ordergroup, name: 'ccc', user_ids: [user_c.id] + user_b = create(:user, last_activity: 3.days.ago) + user_a = create(:user, last_activity: 5.days.ago) + user_c = create(:user, last_activity: Time.now) + group_b = create(:ordergroup, name: 'bbb', user_ids: [user_b.id]) + group_a = create(:ordergroup, name: 'aaa', user_ids: [user_a.id]) + group_c = create(:ordergroup, name: 'ccc', user_ids: [user_c.id]) expect(Ordergroup.sort_by_param('last_user_activity')).to eq([group_a, group_b, group_c]) end it 'reverse by last_user_activity' do - user_b = create :user, last_activity: 3.days.ago - user_a = create :user, last_activity: 5.days.ago - user_c = create :user, last_activity: Time.now - group_b = create :ordergroup, name: 'bbb', user_ids: [user_b.id] - group_a = create :ordergroup, name: 'aaa', user_ids: [user_a.id] - group_c = create :ordergroup, name: 'ccc', user_ids: [user_c.id] + user_b = create(:user, last_activity: 3.days.ago) + user_a = create(:user, last_activity: 5.days.ago) + user_c = create(:user, last_activity: Time.now) + group_b = create(:ordergroup, name: 'bbb', user_ids: [user_b.id]) + group_a = create(:ordergroup, name: 'aaa', user_ids: [user_a.id]) + group_c = create(:ordergroup, name: 'ccc', user_ids: [user_c.id]) expect(Ordergroup.sort_by_param('last_user_activity_reverse')).to eq([group_c, group_b, group_a]) end it 'by last_order' do - group_b = create :ordergroup, name: 'bbb' - group_a = create :ordergroup, name: 'aaa' - group_c = create :ordergroup, name: 'ccc' + group_b = create(:ordergroup, name: 'bbb') + group_a = create(:ordergroup, name: 'aaa') + group_c = create(:ordergroup, name: 'ccc') group_b.group_orders.create! order: create(:order, starts: 6.days.ago) group_a.group_orders.create! order: create(:order, starts: 4.months.ago) group_c.group_orders.create! order: create(:order, starts: Time.now) @@ -95,9 +95,9 @@ describe Ordergroup do end it 'reverse by last_order' do - group_b = create :ordergroup, name: 'bbb' - group_a = create :ordergroup, name: 'aaa' - group_c = create :ordergroup, name: 'ccc' + group_b = create(:ordergroup, name: 'bbb') + group_a = create(:ordergroup, name: 'aaa') + group_c = create(:ordergroup, name: 'ccc') group_b.group_orders.create! order: create(:order, starts: 6.days.ago) group_a.group_orders.create! order: create(:order, starts: 4.months.ago) group_c.group_orders.create! order: create(:order, starts: Time.now) diff --git a/spec/models/supplier_spec.rb b/spec/models/supplier_spec.rb index 70ba6def..5216b8e9 100644 --- a/spec/models/supplier_spec.rb +++ b/spec/models/supplier_spec.rb @@ -1,17 +1,19 @@ require_relative '../spec_helper' describe Supplier do - let(:supplier) { create :supplier } + let(:supplier) { create(:supplier) } context 'syncs from file' do it 'imports and updates articles' do - article1 = create(:article, supplier: supplier, order_number: 177813, unit: '250 g', price: 0.1) - article2 = create(:article, supplier: supplier, order_number: 12345) + article1 = create(:article, supplier: supplier, order_number: 177_813, unit: '250 g', price: 0.1) + article2 = create(:article, supplier: supplier, order_number: 12_345) supplier.articles = [article1, article2] options = { filename: 'foodsoft_file_01.csv' } options[:outlist_absent] = true options[:convert_units] = true - updated_article_pairs, outlisted_articles, new_articles = supplier.sync_from_file(Rails.root.join('spec/fixtures/foodsoft_file_01.csv'), options) + updated_article_pairs, outlisted_articles, new_articles = supplier.sync_from_file( + Rails.root.join('spec/fixtures/foodsoft_file_01.csv'), options + ) expect(new_articles.length).to be > 0 expect(updated_article_pairs.first[1][:name]).to eq 'Tomaten' expect(outlisted_articles.first).to eq article2 @@ -19,14 +21,16 @@ describe Supplier do end it 'return correct tolerance' do - supplier = create :supplier, articles: create_list(:article, 1, unit_quantity: 1) + supplier = create(:supplier) + supplier.articles = create_list(:article, 1, unit_quantity: 1) expect(supplier.has_tolerance?).to be false - supplier2 = create :supplier, articles: create_list(:article, 1, unit_quantity: 2) + supplier2 = create(:supplier) + supplier2.articles = create_list(:article, 1, unit_quantity: 2) expect(supplier2.has_tolerance?).to be true end it 'deletes the supplier and its articles' do - supplier = create :supplier, article_count: 3 + supplier = create(:supplier, article_count: 3) supplier.articles.each { |a| allow(a).to receive(:mark_as_deleted) } supplier.mark_as_deleted supplier.articles.each { |a| expect(a).to have_received(:mark_as_deleted) } @@ -34,29 +38,29 @@ describe Supplier do end it 'has a unique name' do - supplier2 = build :supplier, name: supplier.name + supplier2 = build(:supplier, name: supplier.name) expect(supplier2).to be_invalid end it 'has valid articles' do - supplier = create :supplier, article_count: true + supplier = create(:supplier, article_count: true) supplier.articles.each { |a| expect(a).to be_valid } end context 'connected to a shared supplier' do let(:shared_sync_method) { nil } - let(:shared_supplier) { create :shared_supplier } - let(:supplier) { create :supplier, shared_supplier: shared_supplier, shared_sync_method: shared_sync_method } + let(:shared_supplier) { create(:shared_supplier) } + let(:supplier) { create(:supplier, shared_supplier: shared_supplier, shared_sync_method: shared_sync_method) } - let!(:synced_shared_article) { create :shared_article, shared_supplier: shared_supplier } - let!(:updated_shared_article) { create :shared_article, shared_supplier: shared_supplier } - let!(:new_shared_article) { create :shared_article, shared_supplier: shared_supplier } + let!(:synced_shared_article) { create(:shared_article, shared_supplier: shared_supplier) } + let!(:updated_shared_article) { create(:shared_article, shared_supplier: shared_supplier) } + let!(:new_shared_article) { create(:shared_article, shared_supplier: shared_supplier) } - let!(:removed_article) { create :article, supplier: supplier, order_number: '10001-ABC' } + let!(:removed_article) { create(:article, supplier: supplier, order_number: '10001-ABC') } let!(:updated_article) do updated_shared_article.build_new_article(supplier).tap do |article| article.article_category = create :article_category - article.origin = "FubarX1" + article.origin = 'FubarX1' article.shared_updated_on = 1.day.ago article.save! end @@ -75,7 +79,7 @@ describe Supplier do it 'returns the expected articles' do updated_article_pairs, outlisted_articles, new_articles = supplier.sync_all - expect(updated_article_pairs).to_not be_empty + expect(updated_article_pairs).not_to be_empty expect(updated_article_pairs[0][0].id).to eq updated_article.id expect(updated_article_pairs[0][1].keys).to include :origin @@ -91,13 +95,13 @@ describe Supplier do it 'returns the expected articles' do updated_article_pairs, outlisted_articles, new_articles = supplier.sync_all - expect(updated_article_pairs).to_not be_empty + expect(updated_article_pairs).not_to be_empty expect(updated_article_pairs[0][0].id).to eq updated_article.id expect(updated_article_pairs[0][1].keys).to include :origin expect(outlisted_articles).to eq [removed_article] - expect(new_articles).to_not be_empty + expect(new_articles).not_to be_empty expect(new_articles[0].order_number).to eq new_shared_article.number expect(new_articles[0].availability?).to be true end @@ -109,13 +113,13 @@ describe Supplier do it 'returns the expected articles' do updated_article_pairs, outlisted_articles, new_articles = supplier.sync_all - expect(updated_article_pairs).to_not be_empty + expect(updated_article_pairs).not_to be_empty expect(updated_article_pairs[0][0].id).to eq updated_article.id expect(updated_article_pairs[0][1].keys).to include :origin expect(outlisted_articles).to eq [removed_article] - expect(new_articles).to_not be_empty + expect(new_articles).not_to be_empty expect(new_articles[0].order_number).to eq new_shared_article.number expect(new_articles[0].availability?).to be false end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 59a797de..def2d1f8 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -2,9 +2,9 @@ require_relative '../spec_helper' describe User do it 'is correctly created' do - user = create :user, + user = create(:user, nick: 'johnnydoe', first_name: 'Johnny', last_name: 'DoeBar', - email: 'johnnydoe@foodcoop.test', phone: '+1234567890' + email: 'johnnydoe@foodcoop.test', phone: '+1234567890') expect(user.nick).to eq('johnnydoe') expect(user.first_name).to eq('Johnny') expect(user.last_name).to eq('DoeBar') @@ -14,7 +14,7 @@ describe User do end describe 'does not have the role' do - let(:user) { create :user } + let(:user) { create(:user) } it 'admin' do expect(user.role_admin?).to be_falsey end it 'finance' do expect(user.role_finance?).to be_falsey end @@ -24,7 +24,7 @@ describe User do end describe do - let(:user) { create :user, password: 'blahblahblah' } + let(:user) { create(:user, password: 'blahblahblah') } it 'can authenticate with correct password' do expect(User.authenticate(user.nick, 'blahblahblah')).to be_truthy @@ -74,124 +74,124 @@ describe User do end describe 'admin' do - let(:user) { create :admin } + let(:user) { create(:admin) } it 'default admin role' do expect(user.role_admin?).to be_truthy end end describe 'sort correctly' do it 'by nick' do - user_b = create :user, nick: 'bbb' - user_a = create :user, nick: 'aaa' - user_c = create :user, nick: 'ccc' + user_b = create(:user, nick: 'bbb') + user_a = create(:user, nick: 'aaa') + user_c = create(:user, nick: 'ccc') expect(User.sort_by_param('nick')).to eq([user_a, user_b, user_c]) end it 'reverse by nick' do - user_b = create :user, nick: 'bbb' - user_a = create :user, nick: 'aaa' - user_c = create :user, nick: 'ccc' + user_b = create(:user, nick: 'bbb') + user_a = create(:user, nick: 'aaa') + user_c = create(:user, nick: 'ccc') expect(User.sort_by_param('nick_reverse')).to eq([user_c, user_b, user_a]) end it 'by name' do - user_b = create :user, first_name: 'aaa', last_name: 'bbb' - user_a = create :user, first_name: 'aaa', last_name: 'aaa' - user_c = create :user, first_name: 'ccc', last_name: 'aaa' + user_b = create(:user, first_name: 'aaa', last_name: 'bbb') + user_a = create(:user, first_name: 'aaa', last_name: 'aaa') + user_c = create(:user, first_name: 'ccc', last_name: 'aaa') expect(User.sort_by_param('name')).to eq([user_a, user_b, user_c]) end it 'reverse by name' do - user_b = create :user, first_name: 'aaa', last_name: 'bbb' - user_a = create :user, first_name: 'aaa', last_name: 'aaa' - user_c = create :user, first_name: 'ccc', last_name: 'aaa' + user_b = create(:user, first_name: 'aaa', last_name: 'bbb') + user_a = create(:user, first_name: 'aaa', last_name: 'aaa') + user_c = create(:user, first_name: 'ccc', last_name: 'aaa') expect(User.sort_by_param('name_reverse')).to eq([user_c, user_b, user_a]) end it 'by email' do - user_b = create :user, email: 'bbb@dummy.com' - user_a = create :user, email: 'aaa@dummy.com' - user_c = create :user, email: 'ccc@dummy.com' + user_b = create(:user, email: 'bbb@dummy.com') + user_a = create(:user, email: 'aaa@dummy.com') + user_c = create(:user, email: 'ccc@dummy.com') expect(User.sort_by_param('email')).to eq([user_a, user_b, user_c]) end it 'reverse by email' do - user_b = create :user, email: 'bbb@dummy.com' - user_a = create :user, email: 'aaa@dummy.com' - user_c = create :user, email: 'ccc@dummy.com' + user_b = create(:user, email: 'bbb@dummy.com') + user_a = create(:user, email: 'aaa@dummy.com') + user_c = create(:user, email: 'ccc@dummy.com') expect(User.sort_by_param('email_reverse')).to eq([user_c, user_b, user_a]) end it 'by phone' do - user_b = create :user, phone: 'bbb' - user_a = create :user, phone: 'aaa' - user_c = create :user, phone: 'ccc' + user_b = create(:user, phone: 'bbb') + user_a = create(:user, phone: 'aaa') + user_c = create(:user, phone: 'ccc') expect(User.sort_by_param('phone')).to eq([user_a, user_b, user_c]) end it 'reverse by phone' do - user_b = create :user, phone: 'bbb' - user_a = create :user, phone: 'aaa' - user_c = create :user, phone: 'ccc' + user_b = create(:user, phone: 'bbb') + user_a = create(:user, phone: 'aaa') + user_c = create(:user, phone: 'ccc') expect(User.sort_by_param('phone_reverse')).to eq([user_c, user_b, user_a]) end it 'by last_activity' do - user_b = create :user, last_activity: 3.days.ago - user_a = create :user, last_activity: 5.days.ago - user_c = create :user, last_activity: Time.now + user_b = create(:user, last_activity: 3.days.ago) + user_a = create(:user, last_activity: 5.days.ago) + user_c = create(:user, last_activity: Time.now) expect(User.sort_by_param('last_activity')).to eq([user_a, user_b, user_c]) end it 'reverse by last_activity' do - user_b = create :user, last_activity: 3.days.ago - user_a = create :user, last_activity: 5.days.ago - user_c = create :user, last_activity: Time.now + user_b = create(:user, last_activity: 3.days.ago) + user_a = create(:user, last_activity: 5.days.ago) + user_c = create(:user, last_activity: Time.now) expect(User.sort_by_param('last_activity_reverse')).to eq([user_c, user_b, user_a]) end it 'by ordergroup' do - user_b = create :user, groups: [create(:workgroup, name: 'a'), create(:ordergroup, name: 'bb')] - user_a = create :user, groups: [create(:workgroup, name: 'b'), create(:ordergroup, name: 'aa')] - user_c = create :user, groups: [create(:workgroup, name: 'c'), create(:ordergroup, name: 'cc')] + user_b = create(:user, groups: [create(:workgroup, name: 'a'), create(:ordergroup, name: 'bb')]) + user_a = create(:user, groups: [create(:workgroup, name: 'b'), create(:ordergroup, name: 'aa')]) + user_c = create(:user, groups: [create(:workgroup, name: 'c'), create(:ordergroup, name: 'cc')]) expect(User.sort_by_param('ordergroup')).to eq([user_a, user_b, user_c]) end it 'reverse by ordergroup' do - user_b = create :user, groups: [create(:workgroup, name: 'a'), create(:ordergroup, name: 'bb')] - user_a = create :user, groups: [create(:workgroup, name: 'b'), create(:ordergroup, name: 'aa')] - user_c = create :user, groups: [create(:workgroup, name: 'c'), create(:ordergroup, name: 'cc')] + user_b = create(:user, groups: [create(:workgroup, name: 'a'), create(:ordergroup, name: 'bb')]) + user_a = create(:user, groups: [create(:workgroup, name: 'b'), create(:ordergroup, name: 'aa')]) + user_c = create(:user, groups: [create(:workgroup, name: 'c'), create(:ordergroup, name: 'cc')]) expect(User.sort_by_param('ordergroup_reverse')).to eq([user_c, user_b, user_a]) end it 'and users are only listed once' do - create :user + create(:user) expect(User.sort_by_param('ordergroup').size).to eq(1) end it 'and users belonging to a workgroup are only listed once' do - create :admin + create(:admin) expect(User.sort_by_param('ordergroup').size).to eq(1) end it 'and users belonging to 2 ordergroups are only listed once' do - user = create :user - create :ordergroup, user_ids: [user.id] - create :ordergroup, user_ids: [user.id] + user = create(:user) + create(:ordergroup, user_ids: [user.id]) + create(:ordergroup, user_ids: [user.id]) expect(User.sort_by_param('ordergroup').size).to eq(1) end diff --git a/spec/requests/api/article_categories_spec.rb b/spec/requests/api/v1/article_categories_controller_spec.rb similarity index 96% rename from spec/requests/api/article_categories_spec.rb rename to spec/requests/api/v1/article_categories_controller_spec.rb index 4c079ff2..209349a2 100644 --- a/spec/requests/api/article_categories_spec.rb +++ b/spec/requests/api/v1/article_categories_controller_spec.rb @@ -1,6 +1,6 @@ require 'swagger_helper' -describe 'Article Categories', type: :request do +describe Api::V1::ArticleCategoriesController do include ApiHelper path '/article_categories' do diff --git a/spec/requests/api/configs_spec.rb b/spec/requests/api/v1/configs_controller_spec.rb similarity index 90% rename from spec/requests/api/configs_spec.rb rename to spec/requests/api/v1/configs_controller_spec.rb index 75f48ceb..1809065a 100644 --- a/spec/requests/api/configs_spec.rb +++ b/spec/requests/api/v1/configs_controller_spec.rb @@ -1,6 +1,6 @@ require 'swagger_helper' -describe 'Config', type: :request do +describe Api::V1::ConfigsController do include ApiHelper path '/config' do diff --git a/spec/requests/api/financial_transaction_classes_spec.rb b/spec/requests/api/v1/financial_transaction_classes_controller_spec.rb similarity index 95% rename from spec/requests/api/financial_transaction_classes_spec.rb rename to spec/requests/api/v1/financial_transaction_classes_controller_spec.rb index 1eaf046f..4db7f2b7 100644 --- a/spec/requests/api/financial_transaction_classes_spec.rb +++ b/spec/requests/api/v1/financial_transaction_classes_controller_spec.rb @@ -1,6 +1,6 @@ require 'swagger_helper' -describe 'Financial Transaction Classes', type: :request do +describe Api::V1::FinancialTransactionClassesController do include ApiHelper path '/financial_transaction_classes' do diff --git a/spec/requests/api/financial_transaction_types_spec.rb b/spec/requests/api/v1/financial_transaction_types_controller_spec.rb similarity index 95% rename from spec/requests/api/financial_transaction_types_spec.rb rename to spec/requests/api/v1/financial_transaction_types_controller_spec.rb index 82a30f83..d061214e 100644 --- a/spec/requests/api/financial_transaction_types_spec.rb +++ b/spec/requests/api/v1/financial_transaction_types_controller_spec.rb @@ -1,6 +1,6 @@ require 'swagger_helper' -describe 'Financial Transaction types', type: :request do +describe Api::V1::FinancialTransactionTypesController do include ApiHelper path '/financial_transaction_types' do diff --git a/spec/requests/api/financial_transactions_spec.rb b/spec/requests/api/v1/financial_transactions_controller_spec.rb similarity index 88% rename from spec/requests/api/financial_transactions_spec.rb rename to spec/requests/api/v1/financial_transactions_controller_spec.rb index 1d3ef2b9..915f3891 100644 --- a/spec/requests/api/financial_transactions_spec.rb +++ b/spec/requests/api/v1/financial_transactions_controller_spec.rb @@ -1,10 +1,12 @@ require 'swagger_helper' -describe 'Financial Transaction', type: :request do +describe Api::V1::FinancialTransactionsController do include ApiHelper let!(:finance_user) { create(:user, groups: [create(:workgroup, role_finance: true)]) } let!(:api_scopes) { ['finance:read', 'finance:write'] } - let(:api_access_token) { create(:oauth2_access_token, resource_owner_id: finance_user.id, scopes: api_scopes&.join(' ')).token } + let(:api_access_token) do + create(:oauth2_access_token, resource_owner_id: finance_user.id, scopes: api_scopes&.join(' ')).token + end let(:financial_transaction) { create(:financial_transaction, user: user) } path '/financial_transactions' do diff --git a/spec/requests/api/navigations_spec.rb b/spec/requests/api/v1/navigations_controller_spec.rb similarity index 90% rename from spec/requests/api/navigations_spec.rb rename to spec/requests/api/v1/navigations_controller_spec.rb index c2312437..13c0a449 100644 --- a/spec/requests/api/navigations_spec.rb +++ b/spec/requests/api/v1/navigations_controller_spec.rb @@ -1,6 +1,6 @@ require 'swagger_helper' -describe 'Navigation', type: :request do +describe Api::V1::NavigationsController do include ApiHelper path '/navigation' do diff --git a/spec/requests/api/order_articles_spec.rb b/spec/requests/api/v1/order_articles_controller_spec.rb similarity index 98% rename from spec/requests/api/order_articles_spec.rb rename to spec/requests/api/v1/order_articles_controller_spec.rb index 17feefa6..97fea3bb 100644 --- a/spec/requests/api/order_articles_spec.rb +++ b/spec/requests/api/v1/order_articles_controller_spec.rb @@ -1,6 +1,6 @@ require 'swagger_helper' -describe 'Order Articles', type: :request do +describe Api::V1::OrderArticlesController do include ApiHelper path '/order_articles' do diff --git a/spec/requests/api/orders_spec.rb b/spec/requests/api/v1/orders_controller_spec.rb similarity index 96% rename from spec/requests/api/orders_spec.rb rename to spec/requests/api/v1/orders_controller_spec.rb index c0505d7f..0ad4131e 100644 --- a/spec/requests/api/orders_spec.rb +++ b/spec/requests/api/v1/orders_controller_spec.rb @@ -1,6 +1,6 @@ require 'swagger_helper' -describe 'Orders', type: :request do +describe Api::V1::OrdersController do include ApiHelper let(:api_scopes) { ['orders:read'] } diff --git a/spec/requests/api/user/financial_transactions_spec.rb b/spec/requests/api/v1/user/financial_transactions_spec.rb similarity index 92% rename from spec/requests/api/user/financial_transactions_spec.rb rename to spec/requests/api/v1/user/financial_transactions_spec.rb index aca9d7cd..63603d66 100644 --- a/spec/requests/api/user/financial_transactions_spec.rb +++ b/spec/requests/api/v1/user/financial_transactions_spec.rb @@ -1,11 +1,11 @@ require 'swagger_helper' -describe 'User', type: :request do +describe 'User' do include ApiHelper let(:api_scopes) { ['finance:user'] } - let(:user) { create :user, groups: [create(:ordergroup)] } - let(:other_user2) { create :user } + let(:user) { create(:user, groups: [create(:ordergroup)]) } + let(:other_user2) { create(:user) } let(:ft) { create(:financial_transaction, user: user, ordergroup: user.ordergroup) } before do @@ -27,7 +27,9 @@ describe 'User', type: :request do } } - let(:financial_transaction) { { amount: 3, financial_transaction_type_id: create(:financial_transaction_type).id, note: 'lirum larum' } } + let(:financial_transaction) do + { amount: 3, financial_transaction_type_id: create(:financial_transaction_type).id, note: 'lirum larum' } + end response '200', 'success' do schema type: :object, properties: { diff --git a/spec/requests/api/user/group_order_articles_spec.rb b/spec/requests/api/v1/user/group_order_articles_spec.rb similarity index 88% rename from spec/requests/api/user/group_order_articles_spec.rb rename to spec/requests/api/v1/user/group_order_articles_spec.rb index 205a4070..e93c7ecf 100644 --- a/spec/requests/api/user/group_order_articles_spec.rb +++ b/spec/requests/api/v1/user/group_order_articles_spec.rb @@ -1,15 +1,15 @@ require 'swagger_helper' -describe 'User', type: :request do +describe 'User' do include ApiHelper let(:api_scopes) { ['group_orders:user'] } - let(:user) { create :user, groups: [create(:ordergroup)] } - let(:other_user2) { create :user } + let(:user) { create(:user, groups: [create(:ordergroup)]) } + let(:other_user2) { create(:user) } let(:order) { create(:order, article_count: 4) } let(:order_articles) { order.order_articles } - let(:group_order) { create :group_order, ordergroup: user.ordergroup, order_id: order.id } - let(:goa) { create :group_order_article, group_order: group_order, order_article: order_articles.first } + let(:group_order) { create(:group_order, ordergroup: user.ordergroup, order_id: order.id) } + let(:goa) { create(:group_order_article, group_order: group_order, order_article: order_articles.first) } before do goa @@ -134,11 +134,12 @@ describe 'User', type: :request do response 401, 'not logged-in' do schema '$ref' => '#/components/schemas/Error401' - let(:Authorization) { 'abc' } + let(:Authorization) { 'abc' } # rubocop:disable RSpec/VariableName run_test! end - response 403, 'user has no ordergroup, order not open, is below minimum balance, has not enough apple points, or missing scope' do + response 403, + 'user has no ordergroup, order not open, is below minimum balance, has not enough apple points, or missing scope' do let(:api_scopes) { ['none'] } schema '$ref' => '#/components/schemas/Error403' run_test! @@ -180,7 +181,8 @@ describe 'User', type: :request do it_handles_invalid_token_with_id - response 403, 'user has no ordergroup, order not open, is below minimum balance, has not enough apple points, or missing scope' do + response 403, + 'user has no ordergroup, order not open, is below minimum balance, has not enough apple points, or missing scope' do let(:api_scopes) { ['none'] } schema '$ref' => '#/components/schemas/Error403' run_test! diff --git a/spec/requests/api/user/users_spec.rb b/spec/requests/api/v1/user/users_spec.rb similarity index 94% rename from spec/requests/api/user/users_spec.rb rename to spec/requests/api/v1/user/users_spec.rb index 0d3196bc..90e343fa 100644 --- a/spec/requests/api/user/users_spec.rb +++ b/spec/requests/api/v1/user/users_spec.rb @@ -1,6 +1,6 @@ require 'swagger_helper' -describe 'User', type: :request do +describe 'User' do include ApiHelper path '/user' do @@ -8,9 +8,9 @@ describe 'User', type: :request do tags 'User' produces 'application/json' let(:api_scopes) { ['user:read'] } - let(:other_user1) { create :user } - let(:user) { create :user } - let(:other_user2) { create :user } + let(:other_user1) { create(:user) } + let(:user) { create(:user) } + let(:other_user2) { create(:user) } response '200', 'success' do schema type: :object, @@ -52,7 +52,7 @@ describe 'User', type: :request do get 'financial summary about the currently logged-in user' do tags 'User', 'Financial Transaction' produces 'application/json' - let(:user) { create :user, :ordergroup } + let(:user) { create(:user, :ordergroup) } let(:api_scopes) { ['finance:user'] } FinancialTransactionClass.create(name: 'TestTransaction') diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 8b1c6ace..3bbf03ea 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,8 +1,8 @@ # This file is copied to spec/ when you run 'rails generate rspec:install' -ENV["RAILS_ENV"] ||= 'test' -ENV["FOODSOFT_APP_CONFIG"] ||= 'spec/app_config.yml' # load special foodsoft config +ENV['RAILS_ENV'] ||= 'test' +ENV['FOODSOFT_APP_CONFIG'] ||= 'spec/app_config.yml' # load special foodsoft config require_relative 'support/coverage' # needs to be first -require File.expand_path("../../config/environment", __FILE__) +require File.expand_path('../config/environment', __dir__) require 'rspec/rails' require 'capybara/rails' require 'capybara/apparition' @@ -17,17 +17,17 @@ end # Requires supporting ruby files with custom matchers and macros, etc, # in spec/support/ and its subdirectories. -Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f } +Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f } RSpec.configure do |config| # We use capybara with webkit, and need database_cleaner - config.before(:each) do + config.before do DatabaseCleaner.strategy = (RSpec.current_example.metadata[:js] ? :truncation : :transaction) DatabaseCleaner.start # clean slate mail queues, not sure why needed - https://github.com/rspec/rspec-rails/issues/661 ActionMailer::Base.deliveries.clear end - config.after(:each) do + config.after do DatabaseCleaner.clean # Need to clear cache for RailsSettings::CachedSettings Rails.cache.clear @@ -35,7 +35,7 @@ RSpec.configure do |config| # reload foodsoft configuration, so that tests can use FoodsoftConfig.config[:foo]=x # without messing up tests run after that - config.before(:each) do + config.before do FoodsoftConfig.init FoodsoftConfig.init_mailing end @@ -49,7 +49,7 @@ RSpec.configure do |config| # order dependency and want to debug it, you can fix the order by providing # the seed, which is printed after each run. # --seed 1234 - config.order = "random" + config.order = 'random' config.include SpecTestHelper, type: :controller config.include SessionHelper, type: :feature diff --git a/spec/support/api_helper.rb b/spec/support/api_helper.rb index 86e2ca07..3a6e7894 100644 --- a/spec/support/api_helper.rb +++ b/spec/support/api_helper.rb @@ -4,12 +4,14 @@ module ApiHelper included do let(:user) { create(:user) } let(:api_scopes) { [] } # empty scopes for stricter testing (in reality this would be default_scopes) - let(:api_access_token) { create(:oauth2_access_token, resource_owner_id: user.id, scopes: api_scopes&.join(' ')).token } - let(:Authorization) { "Bearer #{api_access_token}" } + let(:api_access_token) do + create(:oauth2_access_token, resource_owner_id: user.id, scopes: api_scopes&.join(' ')).token + end + let(:Authorization) { "Bearer #{api_access_token}" } # rubocop:disable RSpec/VariableName def self.it_handles_invalid_token context 'with invalid access token' do - let(:Authorization) { 'abc' } + let(:Authorization) { 'abc' } # rubocop:disable RSpec/VariableName response 401, 'not logged-in' do schema '$ref' => '#/components/schemas/Error401' @@ -20,7 +22,7 @@ module ApiHelper def self.it_handles_invalid_token_with_id context 'with invalid access token' do - let(:Authorization) { 'abc' } + let(:Authorization) { 'abc' } # rubocop:disable RSpec/VariableName let(:id) { 42 } # id doesn't matter here response 401, 'not logged-in' do diff --git a/spec/support/api_oauth.rb b/spec/support/api_oauth.rb index 00d5318d..da8a56c1 100644 --- a/spec/support/api_oauth.rb +++ b/spec/support/api_oauth.rb @@ -5,7 +5,7 @@ module ApiOAuth included do let(:user) { build(:user) } let(:api_scopes) { [] } # empty scopes for stricter testing (in reality this would be default_scopes) - let(:api_access_token) { double(:acceptable? => true, :accessible? => true, scopes: api_scopes) } + let(:api_access_token) { double(acceptable?: true, accessible?: true, scopes: api_scopes) } before { allow(controller).to receive(:doorkeeper_token) { api_access_token } } before { allow(controller).to receive(:current_user) { user } } diff --git a/spec/support/coverage.rb b/spec/support/coverage.rb index 20bbdcf3..b4142c3d 100644 --- a/spec/support/coverage.rb +++ b/spec/support/coverage.rb @@ -11,7 +11,7 @@ if ENV['COVERAGE'] or ENV['COVERALLS'] # slightly tweaked coverage reporting def cov_no_plugins(source_file, path) - source_file.filename =~ /#{path}/ and not source_file.filename =~ /\/lib\/foodsoft_.*\// + source_file.filename =~ /#{path}/ and !(source_file.filename =~ %r{/lib/foodsoft_.*/}) end SimpleCov.start do add_filter '/spec/' @@ -21,6 +21,6 @@ if ENV['COVERAGE'] or ENV['COVERALLS'] add_group 'Helpers' do |s| cov_no_plugins s, '/app/helpers/' end add_group 'Documents' do |s| cov_no_plugins s, '/app/documents/' end add_group 'Libraries' do |s| cov_no_plugins s, '/lib/' end - add_group 'Plugins' do |s| s.filename =~ /\/lib\/foodsoft_.*\// end + add_group 'Plugins' do |s| s.filename =~ %r{/lib/foodsoft_.*/} end end end diff --git a/spec/support/faker.rb b/spec/support/faker.rb index 47441ca8..6516fa92 100644 --- a/spec/support/faker.rb +++ b/spec/support/faker.rb @@ -2,7 +2,7 @@ module Faker class Unit class << self def unit - ['kg', '1L', '100ml', 'piece', 'bunch', '500g'].sample + %w[kg 1L 100ml piece bunch 500g].sample end end end diff --git a/spec/support/integration.rb b/spec/support/integration.rb index 26add35a..6882ac5a 100644 --- a/spec/support/integration.rb +++ b/spec/support/integration.rb @@ -1,4 +1,4 @@ # @see http://stackoverflow.com/a/11048669/2866660 def scrolldown - page.execute_script "window.scrollBy(0,10000)" + page.execute_script 'window.scrollBy(0,10000)' end diff --git a/spec/support/session_helper.rb b/spec/support/session_helper.rb index 31fb0946..1075695e 100644 --- a/spec/support/session_helper.rb +++ b/spec/support/session_helper.rb @@ -1,14 +1,15 @@ module SessionHelper def login(user = nil, password = nil) visit login_path - user = FactoryBot.create :user if user.nil? + user = FactoryBot.create(:user) if user.nil? if user.instance_of? ::User - nick, password = user.nick, user.password + nick = user.nick + password = user.password else nick = user end - fill_in 'nick', :with => nick - fill_in 'password', :with => password + fill_in 'nick', with: nick + fill_in 'password', with: password find('input[type=submit]').click end end diff --git a/spec/support/shared_database.rb b/spec/support/shared_database.rb index f6c0ff0a..180877ec 100644 --- a/spec/support/shared_database.rb +++ b/spec/support/shared_database.rb @@ -4,7 +4,7 @@ ActiveSupport.on_load(:after_initialize) do # But take care when designing tests using the shared database. SharedSupplier.establish_connection Rails.env.to_sym SharedArticle.establish_connection Rails.env.to_sym - # hack for different structure of shared database + # HACK: for different structure of shared database SharedArticle.class_eval do belongs_to :supplier, class_name: 'SharedSupplier' alias_attribute :number, :order_number diff --git a/spec/swagger_helper.rb b/spec/swagger_helper.rb index 912504b8..dbe5a912 100644 --- a/spec/swagger_helper.rb +++ b/spec/swagger_helper.rb @@ -32,7 +32,7 @@ RSpec.configure do |config| currentPage: { type: :integer }, pageSize: { type: :integer } }, - required: %w(recordCount pageCount currentPage pageSize) + required: %w[recordCount pageCount currentPage pageSize] }, Order: { type: :object, @@ -127,7 +127,8 @@ RSpec.configure do |config| description: 'number of units available (only present on stock articles)' } }, - required: %w[id name supplier_id supplier_name unit unit_quantity note manufacturer origin article_category_id] + required: %w[id name supplier_id supplier_name unit unit_quantity note manufacturer origin + article_category_id] }, OrderArticle: { type: :object, @@ -396,7 +397,7 @@ RSpec.configure do |config| description: 'link' }, items: { - '$ref': "#/components/schemas/Navigation" + '$ref': '#/components/schemas/Navigation' } }, required: ['name'], From 285441cb4bb65c2259f6e5e90005a1842bf3389d Mon Sep 17 00:00:00 2001 From: Philipp Rothmann Date: Thu, 8 Jun 2023 13:18:06 +0200 Subject: [PATCH 059/105] fix group order matrix pdf --- app/lib/order_pdf.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/lib/order_pdf.rb b/app/lib/order_pdf.rb index 655e5fbe..869cb0e8 100644 --- a/app/lib/order_pdf.rb +++ b/app/lib/order_pdf.rb @@ -113,7 +113,7 @@ class OrderPdf < RenderPdf # get quantity for each article and ordergroup goa_records = group_order_articles(group_ids) .group('group_order_articles.order_article_id, group_orders.ordergroup_id') - .pluck('group_order_articles.order_article_id', 'group_orders.ordergroup_id', 'SUM(COALESCE(group_order_articles.result, group_order_articles.quantity))') + .pluck('group_order_articles.order_article_id', 'group_orders.ordergroup_id', Arel.sql('SUM(COALESCE(group_order_articles.result, group_order_articles.quantity))')) # transform the flat list of results in a hash (with the article as key), which contains an array for all ordergroups results = goa_records.group_by(&:first).transform_values do |value| From 91e07ab660d7419db3eaa238a861456c57fc9040 Mon Sep 17 00:00:00 2001 From: Philipp Rothmann Date: Thu, 8 Jun 2023 14:03:20 +0200 Subject: [PATCH 060/105] fix external link allow_other_host --- plugins/links/app/controllers/links_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/links/app/controllers/links_controller.rb b/plugins/links/app/controllers/links_controller.rb index 27615517..df38bee3 100644 --- a/plugins/links/app/controllers/links_controller.rb +++ b/plugins/links/app/controllers/links_controller.rb @@ -20,6 +20,6 @@ class LinksController < ApplicationController return redirect_to root_url, alert: t('.indirect_no_location') unless url end - redirect_to url, status: :found + redirect_to url, status: :found, allow_other_host: true end end From 20a67becf5ee24fb7a253799985b07200a54c21c Mon Sep 17 00:00:00 2001 From: Philipp Rothmann Date: Fri, 10 Feb 2023 12:50:59 +0100 Subject: [PATCH 061/105] fix: assets precompile by using terser --- Gemfile | 3 ++- Gemfile.lock | 6 +++--- config/environments/production.rb | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Gemfile b/Gemfile index 2cda86f3..d8b48a97 100644 --- a/Gemfile +++ b/Gemfile @@ -6,7 +6,6 @@ gem 'rails', '~> 7.0' gem 'less-rails' gem 'sassc-rails' -gem 'uglifier' # See https://github.com/sstephenson/execjs#readme for more supported runtimes gem 'therubyracer', platforms: :ruby @@ -124,3 +123,5 @@ group :test do # api gem 'rswag-specs' end + +gem "terser", "~> 1.1" diff --git a/Gemfile.lock b/Gemfile.lock index 3896da95..3a241188 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -540,6 +540,8 @@ GEM sqlite3 (>= 1.3.3) table_print (1.5.7) temple (0.9.1) + terser (1.1.13) + execjs (>= 0.3.0, < 3) therubyracer (0.12.3) libv8 (~> 3.16.14.15) ref @@ -560,8 +562,6 @@ GEM unf (~> 0.1.0) tzinfo (2.0.5) concurrent-ruby (~> 1.0) - uglifier (4.2.0) - execjs (>= 0.3.0, < 3) unf (0.1.4) unf_ext unf_ext (0.0.8.2) @@ -674,9 +674,9 @@ DEPENDENCIES sprockets (< 4) sqlite3 (~> 1.3.6) table_print + terser (~> 1.1) therubyracer twitter-bootstrap-rails (~> 2.2.8) - uglifier web-console whenever diff --git a/config/environments/production.rb b/config/environments/production.rb index e1a4e97f..bb846bd7 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -29,7 +29,7 @@ Rails.application.configure do config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present? # Compress JavaScripts and CSS. - config.assets.js_compressor = :uglifier + config.assets.js_compressor = :terser config.assets.css_compressor = :sass # Do not fallback to assets pipeline if a precompiled asset is missed. From 4bfa87d258793069ad7222814fda4bc7eec34f8e Mon Sep 17 00:00:00 2001 From: Philipp Rothmann Date: Thu, 8 Jun 2023 17:45:22 +0200 Subject: [PATCH 062/105] move CORS setup to initializer --- config/application.rb | 11 ----------- config/initializers/cors.rb | 16 +++++++--------- 2 files changed, 7 insertions(+), 20 deletions(-) diff --git a/config/application.rb b/config/application.rb index 71883b57..5a7edb93 100644 --- a/config/application.rb +++ b/config/application.rb @@ -68,17 +68,6 @@ module Foodsoft config.active_record.yaml_column_permitted_classes = [Symbol, BigDecimal] config.autoloader = :zeitwerk - - # Ex:- :default =>'' - - # CORS for API - config.middleware.insert_before 0, Rack::Cors do - allow do - origins '*' - # this restricts Foodsoft scopes to certain characters - let's discuss it when it becomes an actual problem - resource %r{\A/[-a-zA-Z0-9_]+/api/v1/}, headers: :any, methods: :any - end - end end # Foodsoft version diff --git a/config/initializers/cors.rb b/config/initializers/cors.rb index e5a82f16..24ec0662 100644 --- a/config/initializers/cors.rb +++ b/config/initializers/cors.rb @@ -5,12 +5,10 @@ # Read more: https://github.com/cyu/rack-cors -# Rails.application.config.middleware.insert_before 0, Rack::Cors do -# allow do -# origins "example.com" -# -# resource "*", -# headers: :any, -# methods: [:get, :post, :put, :patch, :delete, :options, :head] -# end -# end +Rails.application.config.middleware.insert_before 0, Rack::Cors do + allow do + origins '*' + # this restricts Foodsoft scopes to certain characters - let's discuss it when it becomes an actual problem + resource %r{\A/[-a-zA-Z0-9_]+/api/v1/}, headers: :any, methods: :any + end +end From 8b0e03ff605fa0aaec7483cee95437524e0a7998 Mon Sep 17 00:00:00 2001 From: viehlieb Date: Thu, 23 Feb 2023 00:18:25 +0100 Subject: [PATCH 063/105] downgrade haml to make deface work --- Gemfile | 2 +- Gemfile.lock | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/Gemfile b/Gemfile index d8b48a97..0332aaf6 100644 --- a/Gemfile +++ b/Gemfile @@ -25,7 +25,7 @@ gem 'attribute_normalizer' gem 'daemons' gem 'doorkeeper' gem 'doorkeeper-i18n' -gem 'haml' +gem 'haml', '~> 5.0' gem 'haml-rails' gem 'ice_cube' gem 'inherited_resources' diff --git a/Gemfile.lock b/Gemfile.lock index 3a241188..84357e6d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -234,9 +234,8 @@ GEM rails (>= 4.0.0) globalid (1.0.1) activesupport (>= 5.0) - haml (6.1.1) - temple (>= 0.8.2) - thor + haml (5.2.2) + temple (>= 0.8.0) tilt haml-rails (2.1.0) actionpack (>= 5.1) @@ -618,7 +617,7 @@ DEPENDENCIES foodsoft_polls! foodsoft_wiki! gaffe - haml + haml (~> 5.0) haml-rails hashie (~> 3.4.6) i18n-js (~> 3.0.0.rc8) From 7fe5fb45926d81629bf56675b241f62dbdd400f7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 9 Jun 2023 19:39:32 +0200 Subject: [PATCH 064/105] Bump rack from 2.2.5 to 2.2.7 (#1004) Bumps [rack](https://github.com/rack/rack) from 2.2.5 to 2.2.7. - [Release notes](https://github.com/rack/rack/releases) - [Changelog](https://github.com/rack/rack/blob/main/CHANGELOG.md) - [Commits](https://github.com/rack/rack/compare/v2.2.5...v2.2.7) --- updated-dependencies: - dependency-name: rack dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 84357e6d..f6f275d2 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -357,7 +357,7 @@ GEM puma (6.0.2) nio4r (~> 2.0) racc (1.6.2) - rack (2.2.5) + rack (2.2.7) rack-cors (1.1.1) rack (>= 2.0.0) rack-protection (3.0.5) From 64b99038e65c3adabcc136563d090abc6699c8be Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 9 Jun 2023 20:06:14 +0200 Subject: [PATCH 065/105] Bump nokogiri from 1.13.10 to 1.15.2 (#1005) Bumps [nokogiri](https://github.com/sparklemotion/nokogiri) from 1.13.10 to 1.15.2. - [Release notes](https://github.com/sparklemotion/nokogiri/releases) - [Changelog](https://github.com/sparklemotion/nokogiri/blob/main/CHANGELOG.md) - [Commits](https://github.com/sparklemotion/nokogiri/compare/v1.13.10...v1.15.2) --- updated-dependencies: - dependency-name: nokogiri dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Gemfile.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index f6f275d2..9ecdd514 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -332,7 +332,7 @@ GEM net-smtp (0.3.3) net-protocol nio4r (2.5.8) - nokogiri (1.13.10-x86_64-linux) + nokogiri (1.15.2-x86_64-linux) racc (~> 1.4) parallel (1.23.0) parser (3.2.2.1) @@ -356,7 +356,7 @@ GEM public_suffix (5.0.1) puma (6.0.2) nio4r (~> 2.0) - racc (1.6.2) + racc (1.7.0) rack (2.2.7) rack-cors (1.1.1) rack (>= 2.0.0) From 075f3cfa1a112514cee16a5dbc1b0a5d5a1e1d67 Mon Sep 17 00:00:00 2001 From: kidhab <32387157+kidhab@users.noreply.github.com> Date: Sat, 10 Jun 2023 10:31:22 +0200 Subject: [PATCH 066/105] Make date configurable via locales (#997) --- app/helpers/application_helper.rb | 2 +- config/initializers/time_formats.rb | 1 + config/locales/de.yml | 3 +++ config/locales/en.yml | 3 +++ config/locales/es.yml | 3 +++ config/locales/fr.yml | 3 +++ config/locales/nl.yml | 3 +++ config/locales/tr.yml | 4 +++- 8 files changed, 20 insertions(+), 2 deletions(-) create mode 100644 config/initializers/time_formats.rb diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index b962507b..8b8a5f95 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -4,7 +4,7 @@ module ApplicationHelper include PathHelper def format_time(time = Time.now) - I18n.l(time, format: '%d.%m.%Y %H:%M') unless time.nil? + I18n.l(time, format: :foodsoft_datetime) unless time.nil? end def format_date(time = Time.now) diff --git a/config/initializers/time_formats.rb b/config/initializers/time_formats.rb new file mode 100644 index 00000000..b0447b7e --- /dev/null +++ b/config/initializers/time_formats.rb @@ -0,0 +1 @@ +Time::DATE_FORMATS[:foodsoft_datetime] = "%d.%m.%Y %H:%M" diff --git a/config/locales/de.yml b/config/locales/de.yml index 5c556357..12033f57 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -1876,3 +1876,6 @@ de: title: Arbeitsgruppen update: notice: Arbeitsgruppe wurde aktualisiert + time: + formats: + foodsoft_datetime: "%d.%m.%Y %H:%M" diff --git a/config/locales/en.yml b/config/locales/en.yml index 10ee1fba..66323cad 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1903,3 +1903,6 @@ en: title: Workgroups update: notice: Workgroup was updated + time: + formats: + foodsoft_datetime: "%Y-%m-%d %H:%M" diff --git a/config/locales/es.yml b/config/locales/es.yml index d3a00a67..44812b83 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -1618,3 +1618,6 @@ es: title: Grupos de trabajo update: notice: El grupo de trabajo se actualizó. + time: + formats: + foodsoft_datetime: "%d/%b/%Y %H:%M" diff --git a/config/locales/fr.yml b/config/locales/fr.yml index a6df1544..5b615b38 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -1375,3 +1375,6 @@ fr: title: Équipes update: notice: L'équipe a été mise à jour + time: + formats: + foodsoft_datetime: "%d/%m/%Y %H:%M" diff --git a/config/locales/nl.yml b/config/locales/nl.yml index 384e8839..f34a8883 100644 --- a/config/locales/nl.yml +++ b/config/locales/nl.yml @@ -1868,3 +1868,6 @@ nl: title: Werkgroepen update: notice: Werkgroep is bijgewerkt + time: + formats: + foodsoft_datetime: "%d-%m-%Y %H:%M" diff --git a/config/locales/tr.yml b/config/locales/tr.yml index 8fb92b7a..b66d5c06 100644 --- a/config/locales/tr.yml +++ b/config/locales/tr.yml @@ -1903,4 +1903,6 @@ tr: title: Çalışma Grupları update: notice: Çalışma grubu güncellendi. - \ No newline at end of file + time: + formats: + foodsoft_datetime: "%d.%b.%Y %H:%M" From c50ba6eda56a325fefbb164352f5f397df5afc92 Mon Sep 17 00:00:00 2001 From: kidhab <32387157+kidhab@users.noreply.github.com> Date: Sat, 10 Jun 2023 10:32:16 +0200 Subject: [PATCH 067/105] feat: Disable member list via configuration (#990) --- app/controllers/foodcoop/users_controller.rb | 2 ++ app/views/admin/configs/_tab_others.html.haml | 1 + app/views/home/_start_nav.haml | 3 ++- config/app_config.yml.SAMPLE | 3 +++ config/locales/de.yml | 1 + config/locales/en.yml | 1 + config/locales/es.yml | 1 + config/locales/fr.yml | 1 + config/locales/nl.yml | 1 + config/navigation.rb | 2 +- 10 files changed, 14 insertions(+), 2 deletions(-) diff --git a/app/controllers/foodcoop/users_controller.rb b/app/controllers/foodcoop/users_controller.rb index 17da7ccf..5dfe0c6f 100644 --- a/app/controllers/foodcoop/users_controller.rb +++ b/app/controllers/foodcoop/users_controller.rb @@ -1,4 +1,6 @@ class Foodcoop::UsersController < ApplicationController + before_action -> { require_config_disabled :disable_members_overview } + def index @users = User.undeleted.sort_by_param(params['sort']) diff --git a/app/views/admin/configs/_tab_others.html.haml b/app/views/admin/configs/_tab_others.html.haml index 907cf840..93e1be2d 100644 --- a/app/views/admin/configs/_tab_others.html.haml +++ b/app/views/admin/configs/_tab_others.html.haml @@ -4,5 +4,6 @@ = config_input form, :distribution_strategy, as: :select, collection: distribution_strategy_options, include_blank: false, input_html: {class: 'input-xxlarge'}, label_method: ->(s){ t("config.keys.distribution_strategy_options.#{s}") } = config_input form, :disable_invite, as: :boolean += config_input form, :disable_members_overview, as: :boolean = config_input form, :help_url, as: :url, input_html: {class: 'input-xlarge'} = config_input form, :webstats_tracking_code, as: :text, input_html: {class: 'input-xxlarge', rows: 3} diff --git a/app/views/home/_start_nav.haml b/app/views/home/_start_nav.haml index 708e4c85..96313861 100644 --- a/app/views/home/_start_nav.haml +++ b/app/views/home/_start_nav.haml @@ -2,7 +2,8 @@ %h3= t '.title' %ul.nav.nav-list %li.nav-header= t '.foodcoop' - %li= link_to t('.members'), foodcoop_users_path + - unless FoodsoftConfig[:disable_members_overview] + %li= link_to t('.members'), foodcoop_users_path %li= link_to t('.tasks'), user_tasks_path - has_ordergroup = !@current_user.ordergroup.nil? diff --git a/config/app_config.yml.SAMPLE b/config/app_config.yml.SAMPLE index d6f0f8f9..33a6b356 100644 --- a/config/app_config.yml.SAMPLE +++ b/config/app_config.yml.SAMPLE @@ -93,6 +93,9 @@ default: &defaults #use_wiki: true #use_messages: true + # When enabled only administrators can access the member list. + #disable_members_overview: true + # Base font size for generated PDF documents #pdf_font_size: 12 # Page size for generated PDF documents diff --git a/config/locales/de.yml b/config/locales/de.yml index 12033f57..7fe377ba 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -655,6 +655,7 @@ de: default_role_pickups: Abholtage default_role_suppliers: Lieferanten disable_invite: Einladungen deaktivieren + disable_members_overview: Mitgliederliste deaktivieren email_from: Absenderadresse email_replyto: Antwortadresse email_sender: Senderadresse diff --git a/config/locales/en.yml b/config/locales/en.yml index 66323cad..bb00e997 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -656,6 +656,7 @@ en: default_role_pickups: Pickup days default_role_suppliers: Suppliers disable_invite: Disable invites + disable_members_overview: Disable members list email_from: From address email_replyto: Reply-to address email_sender: Sender address diff --git a/config/locales/es.yml b/config/locales/es.yml index 44812b83..2be287bf 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -580,6 +580,7 @@ es: custom_css: CSS adicional default_locale: Idioma por defecto disable_invite: Desactivar invitaciones + disable_members_overview: Desactivar la lista de miembros email_from: Dirección de email de origen email_replyto: Dirección reply-to email_sender: Dirección del remitente diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 5b615b38..491408dc 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -418,6 +418,7 @@ fr: zip_code: Code postal currency_unit: Monnaie name: Nom + disable_members_overview: Désactiver la liste des membres distribution_strategy: Stratégie de distribution distribution_strategy_options: first_order_first_serve: Distribuez d'abord à ceux qui ont commandé en premier diff --git a/config/locales/nl.yml b/config/locales/nl.yml index f34a8883..e2e8da98 100644 --- a/config/locales/nl.yml +++ b/config/locales/nl.yml @@ -626,6 +626,7 @@ nl: default_role_pickups: Ophaaldagen default_role_suppliers: Leveranciers disable_invite: Uitnodigingen deactiveren + disable_members_overview: Ledenlijst deactiveren email_from: From adres email_replyto: Reply-to adres email_sender: Sender adres diff --git a/config/navigation.rb b/config/navigation.rb index 857b931f..7f2b7b3a 100644 --- a/config/navigation.rb +++ b/config/navigation.rb @@ -12,7 +12,7 @@ SimpleNavigation::Configuration.run do |navigation| primary.item :dashboard_nav_item, I18n.t('navigation.dashboard'), root_path(anchor: '') primary.item :foodcoop, I18n.t('navigation.foodcoop'), '#' do |subnav| - subnav.item :members, I18n.t('navigation.members'), foodcoop_users_path + subnav.item :members, I18n.t('navigation.members'), foodcoop_users_path, unless: Proc.new { FoodsoftConfig[:disable_members_overview] } subnav.item :workgroups, I18n.t('navigation.workgroups'), foodcoop_workgroups_path subnav.item :ordergroups, I18n.t('navigation.ordergroups'), foodcoop_ordergroups_path subnav.item :tasks, I18n.t('navigation.tasks'), tasks_path From e4f91ef67a97ad0beeb53da73f37737d5b47c6ad Mon Sep 17 00:00:00 2001 From: kidhab <32387157+kidhab@users.noreply.github.com> Date: Sat, 10 Jun 2023 10:47:47 +0200 Subject: [PATCH 068/105] Fill availability column at article export closes #884 --- app/lib/articles_csv.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/lib/articles_csv.rb b/app/lib/articles_csv.rb index 55bc7fc5..61f5743f 100644 --- a/app/lib/articles_csv.rb +++ b/app/lib/articles_csv.rb @@ -23,7 +23,7 @@ class ArticlesCsv < RenderCsv def data @object.each do |o| yield [ - '', + o.availability ? I18n.t('simple_form.yes') : I18n.t('simple_form.no'), o.order_number, o.name, o.note, From 20dc8b8b8202792a980bfcaad1e73ca7f0337360 Mon Sep 17 00:00:00 2001 From: kidhab Date: Sat, 10 Jun 2023 10:54:03 +0200 Subject: [PATCH 069/105] Bump Ruby version to latest in 2.7 series --- .ruby-version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ruby-version b/.ruby-version index 37c2961c..6a81b4c8 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -2.7.2 +2.7.8 From 2151835afb84e9335852498c7adf0cb4f83b52dc Mon Sep 17 00:00:00 2001 From: Philipp Rothmann Date: Mon, 12 Jun 2023 13:08:36 +0200 Subject: [PATCH 070/105] fix: rubocop violation --- config/navigation.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/navigation.rb b/config/navigation.rb index 7f2b7b3a..f483989f 100644 --- a/config/navigation.rb +++ b/config/navigation.rb @@ -12,7 +12,7 @@ SimpleNavigation::Configuration.run do |navigation| primary.item :dashboard_nav_item, I18n.t('navigation.dashboard'), root_path(anchor: '') primary.item :foodcoop, I18n.t('navigation.foodcoop'), '#' do |subnav| - subnav.item :members, I18n.t('navigation.members'), foodcoop_users_path, unless: Proc.new { FoodsoftConfig[:disable_members_overview] } + subnav.item :members, I18n.t('navigation.members'), foodcoop_users_path, unless: proc { FoodsoftConfig[:disable_members_overview] } subnav.item :workgroups, I18n.t('navigation.workgroups'), foodcoop_workgroups_path subnav.item :ordergroups, I18n.t('navigation.ordergroups'), foodcoop_ordergroups_path subnav.item :tasks, I18n.t('navigation.tasks'), tasks_path From c76c148f997d978d4010414499242a80f13f3f73 Mon Sep 17 00:00:00 2001 From: Philipp Rothmann Date: Tue, 13 Jun 2023 11:35:13 +0200 Subject: [PATCH 071/105] update readme --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 67594931..8eb17918 100644 --- a/README.md +++ b/README.md @@ -44,10 +44,10 @@ State of this Fork 1. Migrate to RSwag API Tests * [x] [fork](https://git.local-it.org/Foodsoft/foodsoft/src/branch/28_introduce_rswag) - * [ ] upstream [#969](https://github.com/foodcoops/foodsoft/pull/969) + * [x] upstream [#969](https://github.com/foodcoops/foodsoft/pull/969) 1. Rails v7 * [x] [fork](https://git.local-it.org/Foodsoft/foodsoft/src/branch/9_rails_v_7) - * [ ] upstream [#979](https://github.com/foodcoops/foodsoft/pull/979) + * [x] upstream [#979](https://github.com/foodcoops/foodsoft/pull/979) disussion [#956](https://github.com/foodcoops/foodsoft/issues/956) 1. Javascript Importmap * [x] [fork](https://git.local-it.org/Foodsoft/foodsoft/src/branch/9_rails_v_7_js_importmap) @@ -93,7 +93,7 @@ Updating Articles from large resellers and exporting orders is now much easier! 1. Fix broken plugin mechanism * [x] [fork](https://git.local-it.org/Foodsoft/foodsoft/src/branch/downgrade-haml) - * [ ] upstream + * [x] upstream #### Screenshots From a8b2f387db27bd8113ef07f459ae8bb690addc78 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 14 Jun 2023 08:18:33 +0200 Subject: [PATCH 072/105] Bump doorkeeper from 5.6.2 to 5.6.6 (#1010) Bumps [doorkeeper](https://github.com/doorkeeper-gem/doorkeeper) from 5.6.2 to 5.6.6. - [Release notes](https://github.com/doorkeeper-gem/doorkeeper/releases) - [Changelog](https://github.com/doorkeeper-gem/doorkeeper/blob/main/CHANGELOG.md) - [Commits](https://github.com/doorkeeper-gem/doorkeeper/compare/v5.6.2...v5.6.6) --- updated-dependencies: - dependency-name: doorkeeper dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Gemfile.lock | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 9ecdd514..e4cecb5f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -186,7 +186,7 @@ GEM execjs coffee-script-source (1.12.2) commonjs (0.2.7) - concurrent-ruby (1.1.10) + concurrent-ruby (1.2.2) connection_pool (2.3.0) content_for_in_controllers (0.0.2) crass (1.0.6) @@ -210,7 +210,7 @@ GEM diff-lcs (1.5.0) diffy (3.4.2) docile (1.4.0) - doorkeeper (5.6.2) + doorkeeper (5.6.6) railties (>= 5) doorkeeper-i18n (5.2.6) doorkeeper (>= 5.2) @@ -247,7 +247,7 @@ GEM activesupport (>= 5.2) hashie (3.4.6) htmlentities (4.3.4) - i18n (1.12.0) + i18n (1.14.1) concurrent-ruby (~> 1.0) i18n-js (3.0.11) i18n (>= 0.6.6, < 2) @@ -292,9 +292,9 @@ GEM listen (3.7.1) rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) - loofah (2.19.1) + loofah (2.21.3) crass (~> 1.0.2) - nokogiri (>= 1.5.9) + nokogiri (>= 1.12.0) mail (2.7.1) mini_mime (>= 0.1.1) mailcatcher (0.2.4) @@ -315,7 +315,7 @@ GEM mime-types-data (~> 3.2015) mime-types-data (3.2022.0105) mini_mime (1.1.2) - minitest (5.17.0) + minitest (5.18.0) mono_logger (1.1.1) msgpack (1.6.0) multi_json (1.15.0) @@ -362,7 +362,7 @@ GEM rack (>= 2.0.0) rack-protection (3.0.5) rack - rack-test (2.0.2) + rack-test (2.1.0) rack (>= 1.3) rails (7.0.4) actioncable (= 7.0.4) @@ -383,8 +383,9 @@ GEM rails-dom-testing (2.0.3) activesupport (>= 4.2.0) nokogiri (>= 1.6) - rails-html-sanitizer (1.4.4) - loofah (~> 2.19, >= 2.19.1) + rails-html-sanitizer (1.6.0) + loofah (~> 2.21) + nokogiri (~> 1.14) rails-i18n (7.0.6) i18n (>= 0.7, < 2) railties (>= 6.0.0, < 8) @@ -548,7 +549,7 @@ GEM daemons (>= 1.0.9) eventmachine (>= 1.0.0) rack (>= 1.0.0) - thor (1.2.1) + thor (1.2.2) tilt (2.0.11) timeout (0.3.1) ttfunk (1.7.0) @@ -559,7 +560,7 @@ GEM railties (>= 3.1) twitter-text (1.14.7) unf (~> 0.1.0) - tzinfo (2.0.5) + tzinfo (2.0.6) concurrent-ruby (~> 1.0) unf (0.1.4) unf_ext @@ -584,7 +585,7 @@ GEM twitter-text xpath (3.2.0) nokogiri (~> 1.8) - zeitwerk (2.6.6) + zeitwerk (2.6.8) PLATFORMS x86_64-linux From 026c3a62853585accc448811c620db957208c303 Mon Sep 17 00:00:00 2001 From: Philipp Rothmann <16109235+yksflip@users.noreply.github.com> Date: Wed, 14 Jun 2023 13:29:31 +0200 Subject: [PATCH 073/105] introduce importmaps (#983) * introduce importmaps This commit introduces importmaps. They allow to use modern javacript ESM within rails without webpack, yarn etc. see https://github.com/rails/importmap-rails for more details. Co-authored-by: Philipp Rothmann Co-authored-by: FGU * fix: rubocop violations --------- Co-authored-by: FGU --- Gemfile | 1 + Gemfile.lock | 4 ++++ .../javascripts/{application.js => application_legacy.js} | 0 app/javascript/application.js | 1 + app/views/layouts/_header.html.haml | 6 ++++-- bin/importmap | 4 ++++ config/importmap.rb | 2 ++ config/initializers/assets.rb | 2 +- vendor/javascript/.keep | 0 9 files changed, 17 insertions(+), 3 deletions(-) rename app/assets/javascripts/{application.js => application_legacy.js} (100%) create mode 100644 app/javascript/application.js create mode 100755 bin/importmap create mode 100644 config/importmap.rb create mode 100644 vendor/javascript/.keep diff --git a/Gemfile b/Gemfile index 0332aaf6..42699250 100644 --- a/Gemfile +++ b/Gemfile @@ -124,4 +124,5 @@ group :test do gem 'rswag-specs' end +gem "importmap-rails", "~> 1.1" gem "terser", "~> 1.1" diff --git a/Gemfile.lock b/Gemfile.lock index e4cecb5f..e0348ac5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -254,6 +254,9 @@ GEM i18n-spec (0.6.0) iso ice_cube (0.16.4) + importmap-rails (1.1.5) + actionpack (>= 6.0.0) + railties (>= 6.0.0) inherited_resources (1.13.1) actionpack (>= 5.2, < 7.1) has_scope (~> 0.6) @@ -624,6 +627,7 @@ DEPENDENCIES i18n-js (~> 3.0.0.rc8) i18n-spec ice_cube + importmap-rails (~> 1.1) inherited_resources jquery-rails kaminari diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application_legacy.js similarity index 100% rename from app/assets/javascripts/application.js rename to app/assets/javascripts/application_legacy.js diff --git a/app/javascript/application.js b/app/javascript/application.js new file mode 100644 index 00000000..beff742e --- /dev/null +++ b/app/javascript/application.js @@ -0,0 +1 @@ +// Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails diff --git a/app/views/layouts/_header.html.haml b/app/views/layouts/_header.html.haml index 974ce8f2..66e14355 100644 --- a/app/views/layouts/_header.html.haml +++ b/app/views/layouts/_header.html.haml @@ -8,10 +8,10 @@ = csrf_meta_tags = stylesheet_link_tag "application", :media => "all" //%link(href="images/favicon.ico" rel="shortcut icon") - = yield(:head) = foodcoop_css_tag + %body = yield @@ -19,7 +19,9 @@ Javascripts \================================================== / Placed at the end of the document so the pages load faster - = javascript_include_tag "application" + = javascript_importmap_tags + = javascript_include_tag "application_legacy" + :javascript I18n.defaultLocale = "#{I18n.default_locale}"; I18n.locale = "#{I18n.locale}"; diff --git a/bin/importmap b/bin/importmap new file mode 100755 index 00000000..36502ab1 --- /dev/null +++ b/bin/importmap @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby + +require_relative "../config/application" +require "importmap/commands" diff --git a/config/importmap.rb b/config/importmap.rb new file mode 100644 index 00000000..3ed27f76 --- /dev/null +++ b/config/importmap.rb @@ -0,0 +1,2 @@ +# Pin npm packages by running ./bin/importmap +pin "application", preload: true diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb index fe48fc34..d507a355 100644 --- a/config/initializers/assets.rb +++ b/config/initializers/assets.rb @@ -9,4 +9,4 @@ Rails.application.config.assets.version = '1.0' # Precompile additional assets. # application.js, application.css, and all non-JS/CSS in the app/assets # folder are already added. -# Rails.application.config.assets.precompile += %w( admin.js admin.css ) +Rails.application.config.assets.precompile += %w[application_legacy.js jquery.min.js] diff --git a/vendor/javascript/.keep b/vendor/javascript/.keep new file mode 100644 index 00000000..e69de29b From a1682932ac133493002c1db9b343cdb05aea978e Mon Sep 17 00:00:00 2001 From: Philipp Rothmann Date: Fri, 16 Jun 2023 13:04:03 +0200 Subject: [PATCH 074/105] fix: price_markup with value nil gives exception fixes #1011 --- app/models/concerns/price_calculation.rb | 2 +- spec/models/article_spec.rb | 12 +++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/app/models/concerns/price_calculation.rb b/app/models/concerns/price_calculation.rb index a78191c0..8d56d671 100644 --- a/app/models/concerns/price_calculation.rb +++ b/app/models/concerns/price_calculation.rb @@ -9,7 +9,7 @@ module PriceCalculation # @return [Number] Price for the foodcoop-member. def fc_price - add_percent(gross_price, FoodsoftConfig[:price_markup]) + add_percent(gross_price, FoodsoftConfig[:price_markup].to_i) end private diff --git a/spec/models/article_spec.rb b/spec/models/article_spec.rb index f104c101..9b58abeb 100644 --- a/spec/models/article_spec.rb +++ b/spec/models/article_spec.rb @@ -59,10 +59,16 @@ describe Article do expect(article.gross_price).to be >= article.price end - it 'computes the fc price correctly' do - expect(article.fc_price).to eq((article.gross_price * 1.05).round(2)) + [[nil, 1], + [0, 1], + [5, 1.05], + [42, 1.42], + [100, 2]].each do |price_markup, percent| + it "computes the fc price with price_markup #{price_markup} correctly" do + FoodsoftConfig.config['price_markup'] = price_markup + expect(article.fc_price).to eq((article.gross_price * percent).round(2)) + end end - it 'knows when it is deleted' do expect(supplier.deleted?).to be false supplier.mark_as_deleted From 37b3b4523aa04698ee66245b06c97b8571d204c3 Mon Sep 17 00:00:00 2001 From: Philipp Rothmann Date: Fri, 16 Jun 2023 13:33:21 +0200 Subject: [PATCH 075/105] fix: github action mysqladmin -> mariadb-admin ping --- .github/workflows/ruby.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml index 34b5ce06..2b56df0b 100644 --- a/.github/workflows/ruby.yml +++ b/.github/workflows/ruby.yml @@ -15,7 +15,7 @@ jobs: MYSQL_DATABASE: test MYSQL_ROOT_PASSWORD: password options: >- - --health-cmd "mysqladmin ping" + --health-cmd "mariadb-admin ping" --health-interval 10s --health-timeout 5s --health-retries 5 From 4ac5bcae06a25d83437dcf322c6db7971782a203 Mon Sep 17 00:00:00 2001 From: kidhab Date: Sat, 17 Jun 2023 10:31:13 +0200 Subject: [PATCH 076/105] Update Ruby version and add info about dev packages --- doc/SETUP_DEVELOPMENT.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/doc/SETUP_DEVELOPMENT.md b/doc/SETUP_DEVELOPMENT.md index 319c2787..ac086416 100644 --- a/doc/SETUP_DEVELOPMENT.md +++ b/doc/SETUP_DEVELOPMENT.md @@ -12,7 +12,7 @@ If instead you just want to run Foodsoft without changing its code, please refer **System requirements**: [rbenv](https://github.com/rbenv/rbenv), -[Ruby 2.6+](https://www.ruby-lang.org/en/downloads/), +[Ruby 2.7+](https://www.ruby-lang.org/en/downloads/), [Bundler](http://bundler.io/), [MySQL](http://mysql.com/) / [SQLite](http://sqlite.org/), [Redis](http://redis.io/) (optional). @@ -32,6 +32,10 @@ If instead you just want to run Foodsoft without changing its code, please refer Have a look how to avoid that in the [Docker Development Setup](./SETUP_DEVELOPMENT_DOCKER.md#prerequisites-windows-only) instructions. +1. Install developement packages. For Debian/Ubuntu: + + sudo apt install default-libmysqlclient-dev libmagic-dev libxml2-dev libxslt-dev + 1. Install and setup rbenv and Bundler. For Debian/Ubuntu: sudo apt install rbenv @@ -175,4 +179,4 @@ within a docker image. While the default [`Dockerfile`](../Dockerfile) is setup use docker-compose (using [`docker-compose-dev.yml`](../docker-compose-dev.yml)) to setup the whole stack at once. -See [Setup Development Docker](./SETUP_DEVELOPMENT_DOCKER.md) for a detailed description. \ No newline at end of file +See [Setup Development Docker](./SETUP_DEVELOPMENT_DOCKER.md) for a detailed description. From 913136bb72ad41b04c8e893058d39b4b95d2440d Mon Sep 17 00:00:00 2001 From: Philipp Rothmann Date: Sat, 17 Jun 2023 13:30:07 +0200 Subject: [PATCH 077/105] fix: invalid params request test fixes #999 --- spec/requests/api/v1/user/financial_transactions_spec.rb | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/spec/requests/api/v1/user/financial_transactions_spec.rb b/spec/requests/api/v1/user/financial_transactions_spec.rb index 63603d66..c37d5b22 100644 --- a/spec/requests/api/v1/user/financial_transactions_spec.rb +++ b/spec/requests/api/v1/user/financial_transactions_spec.rb @@ -48,10 +48,9 @@ describe 'User' do end response '422', 'invalid parameter value' do - xit 'TODO: fix controller to actually send a 422 for invalid params: https://github.com/foodcoops/foodsoft/issues/999' - # schema '$ref' => '#/components/schemas/Error422' - # let(:financial_transaction) { { amount: -3, financial_transaction_type_id: create(:financial_transaction_type).id, note: -2 } } - # run_test! + schema '$ref' => '#/components/schemas/Error422' + let(:financial_transaction) { { amount: "abc", financial_transaction_type_id: create(:financial_transaction_type).id, note: "foo bar" } } + run_test! end end From 5f2130ca44e4fead5b860e258990c2533e1e474c Mon Sep 17 00:00:00 2001 From: Philipp Rothmann Date: Sat, 17 Jun 2023 13:44:21 +0200 Subject: [PATCH 078/105] fix: rubocop todo EmptyExampleGroup wildcard --- .rubocop_todo.yml | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 7995a1d5..cbbec263 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -455,16 +455,7 @@ RSpec/DescribedClass: # This cop supports unsafe autocorrection (--autocorrect-all). RSpec/EmptyExampleGroup: Exclude: - - 'spec/requests/api/v1/article_categories_controller_spec.rb' - - 'spec/requests/api/v1/configs_controller_spec.rb' - - 'spec/requests/api/v1/financial_transaction_classes_controller_spec.rb' - - 'spec/requests/api/v1/financial_transaction_types_controller_spec.rb' - - 'spec/requests/api/v1/financial_transactions_controller_spec.rb' - - 'spec/requests/api/v1/navigations_controller_spec.rb' - - 'spec/requests/api/v1/order_articles_controller_spec.rb' - - 'spec/requests/api/v1/orders_controller_spec.rb' - - 'spec/requests/api/v1/user/group_order_articles_spec.rb' - - 'spec/requests/api/v1/user/users_spec.rb' + - 'spec/requests/api/**/*_spec.rb' # Offense count: 69 # Configuration parameters: CountAsOne. From 45e2668cea64f3692078beca58457000945cc050 Mon Sep 17 00:00:00 2001 From: kidhab Date: Sat, 17 Jun 2023 10:21:58 +0200 Subject: [PATCH 079/105] Update mail gem to .8.1 which fixes the permission error Revert libv8 version --- Gemfile | 1 - Gemfile.lock | 11 +++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/Gemfile b/Gemfile index 42699250..4cc600b0 100644 --- a/Gemfile +++ b/Gemfile @@ -1,7 +1,6 @@ # A sample Gemfile source 'https://rubygems.org' -gem 'mail', '~> 2.7.1' # bug with mail 2.8.0 https://github.com/mikel/mail/issues/1489 gem 'rails', '~> 7.0' gem 'less-rails' diff --git a/Gemfile.lock b/Gemfile.lock index e0348ac5..debba0f2 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -298,8 +298,11 @@ GEM loofah (2.21.3) crass (~> 1.0.2) nokogiri (>= 1.12.0) - mail (2.7.1) + mail (2.8.1) mini_mime (>= 0.1.1) + net-imap + net-pop + net-smtp mailcatcher (0.2.4) eventmachine haml @@ -318,6 +321,7 @@ GEM mime-types-data (~> 3.2015) mime-types-data (3.2022.0105) mini_mime (1.1.2) + mini_portile2 (2.8.2) minitest (5.18.0) mono_logger (1.1.1) msgpack (1.6.0) @@ -335,6 +339,9 @@ GEM net-smtp (0.3.3) net-protocol nio4r (2.5.8) + nokogiri (1.15.2) + mini_portile2 (~> 2.8.2) + racc (~> 1.4) nokogiri (1.15.2-x86_64-linux) racc (~> 1.4) parallel (1.23.0) @@ -591,6 +598,7 @@ GEM zeitwerk (2.6.8) PLATFORMS + ruby x86_64-linux DEPENDENCIES @@ -633,7 +641,6 @@ DEPENDENCIES kaminari less-rails listen - mail (~> 2.7.1) mailcatcher midi-smtp-server mime-types From 33034e66b88968dedc5289425e1eff847ee67e12 Mon Sep 17 00:00:00 2001 From: Philipp Rothmann Date: Thu, 22 Jun 2023 22:45:21 +0200 Subject: [PATCH 080/105] fix: add null checks for articles convert_units Prevents division by zero exception because of a unit beeing 0. A Unit becomes also zero e.g. when a comma symbol is used Unit.new("0,9kg") == 0 fixes #1014 --- app/models/article.rb | 2 +- spec/models/article_spec.rb | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/app/models/article.rb b/app/models/article.rb index 53cc2708..561deaf8 100644 --- a/app/models/article.rb +++ b/app/models/article.rb @@ -211,7 +211,7 @@ class Article < ApplicationRecord rescue StandardError nil end - if fc_unit && supplier_unit && fc_unit =~ supplier_unit + if fc_unit != 0 && supplier_unit != 0 && fc_unit && supplier_unit && fc_unit =~ supplier_unit conversion_factor = (supplier_unit / fc_unit).to_base.to_r new_price = new_article.price / conversion_factor new_unit_quantity = new_article.unit_quantity * conversion_factor diff --git a/spec/models/article_spec.rb b/spec/models/article_spec.rb index 9b58abeb..3a810827 100644 --- a/spec/models/article_spec.rb +++ b/spec/models/article_spec.rb @@ -25,6 +25,18 @@ describe Article do expect(article.convert_units(article1)).to be false end + it 'returns false if unit = 0' do + article1 = build(:article, supplier: supplier, unit: '1kg', price: 2, unit_quantity: 1) + article2 = build(:article, supplier: supplier, unit: '0kg', price: 2, unit_quantity: 1) + expect(article1.convert_units(article2)).to be false + end + + it 'returns false if unit becomes zero because of , symbol in unit format' do + article1 = build(:article, supplier: supplier, unit: '0,8kg', price: 2, unit_quantity: 1) + article2 = build(:article, supplier: supplier, unit: '0,9kg', price: 2, unit_quantity: 1) + expect(article1.convert_units(article2)).to be false + end + it 'converts from ST to KI (german foodcoops legacy)' do article1 = build(:article, supplier: supplier, unit: 'ST') article2 = build(:article, supplier: supplier, name: 'banana 10-12 St', price: 12.34, unit: 'KI') From c442327275ae0553670204ef320de9d53690ea0c Mon Sep 17 00:00:00 2001 From: Harald Reingruber Date: Wed, 30 Oct 2019 15:29:16 +0100 Subject: [PATCH 081/105] Fix line endings for Windows docker environment --- .gitattributes | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..a0a0693c --- /dev/null +++ b/.gitattributes @@ -0,0 +1,4 @@ +* text=auto +*.sh text eol=lf +proc-start text eol=lf +Rakefile text eol=lf \ No newline at end of file From b07653b34f6fb29fdc2008ce2d78debcf4d66c71 Mon Sep 17 00:00:00 2001 From: Harald Reingruber Date: Thu, 5 May 2022 23:30:25 +0200 Subject: [PATCH 082/105] Add explanation comment to .gitattributes --- .gitattributes | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitattributes b/.gitattributes index a0a0693c..f2a26b4c 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,4 +1,5 @@ +# Fixes line endings for Windows (Docker) environment, which are by default converted to crlf * text=auto *.sh text eol=lf proc-start text eol=lf -Rakefile text eol=lf \ No newline at end of file +Rakefile text eol=lf From 7f23b4784cf10c731315c396bf0bddf8800bac3e Mon Sep 17 00:00:00 2001 From: Philipp Rothmann Date: Fri, 17 Feb 2023 12:40:26 +0100 Subject: [PATCH 083/105] feat(finance): show sum of ordergroup balances --- .../finance/ordergroups_controller.rb | 5 +- .../ordergroups/_ordergroups.html.haml | 9 ++++ .../finance/ordergroups_controller_spec.rb | 50 +++++++++++++++++++ 3 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 spec/controllers/finance/ordergroups_controller_spec.rb diff --git a/app/controllers/finance/ordergroups_controller.rb b/app/controllers/finance/ordergroups_controller.rb index a8836f6b..393262a3 100644 --- a/app/controllers/finance/ordergroups_controller.rb +++ b/app/controllers/finance/ordergroups_controller.rb @@ -11,7 +11,10 @@ class Finance::OrdergroupsController < Finance::BaseController @ordergroups = Ordergroup.undeleted.order(sort) @ordergroups = @ordergroups.include_transaction_class_sum @ordergroups = @ordergroups.where('groups.name LIKE ?', "%#{params[:query]}%") unless params[:query].nil? - @ordergroups = @ordergroups.page(params[:page]).per(@per_page) + + @total_balances = FinancialTransactionClass.sorted.each_with_object({}) do |c, tmp| + tmp[c.id] = c.financial_transactions.reduce(0) { | sum, t | sum + t.amount } + end end end diff --git a/app/views/finance/ordergroups/_ordergroups.html.haml b/app/views/finance/ordergroups/_ordergroups.html.haml index 83a05ed2..3e0c99fc 100644 --- a/app/views/finance/ordergroups/_ordergroups.html.haml +++ b/app/views/finance/ordergroups/_ordergroups.html.haml @@ -22,3 +22,12 @@ %td = link_to t('.new_transaction'), new_finance_ordergroup_transaction_path(ordergroup), class: 'btn btn-mini' = link_to t('.account_statement'), finance_ordergroup_transactions_path(ordergroup), class: 'btn btn-mini' + %thead + %tr + %th= t 'Total' + %th + - FinancialTransactionClass.sorted.each do |c| + - name = FinancialTransactionClass.has_multiple_classes ? c.display : heading_helper(Ordergroup, :account_balance) + %th.numeric= format_currency @total_balances[c.id] + %th.numeric + = format_currency @total_balances.values.reduce(:+) \ No newline at end of file diff --git a/spec/controllers/finance/ordergroups_controller_spec.rb b/spec/controllers/finance/ordergroups_controller_spec.rb new file mode 100644 index 00000000..f960c61d --- /dev/null +++ b/spec/controllers/finance/ordergroups_controller_spec.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Finance::OrdergroupsController do + let(:user) { create(:user, :role_finance, :role_orders, :ordergroup) } + let(:fin_trans_type1) { create(:financial_transaction_type) } + let(:fin_trans_type2) { create(:financial_transaction_type) } + let(:fin_trans1) do + create(:financial_transaction, + user: user, + ordergroup: user.ordergroup, + financial_transaction_type: fin_trans_type1) + end + let(:fin_trans2) do + create(:financial_transaction, + user: user, + ordergroup: user.ordergroup, + financial_transaction_type: fin_trans_type1) + end + let(:fin_trans3) do + create(:financial_transaction, + user: user, + ordergroup: user.ordergroup, + financial_transaction_type: fin_trans_type2) + end + + before { login user } + + describe 'GET index' do + before do + fin_trans1 + fin_trans2 + fin_trans3 + end + + it 'renders index page' do + get_with_defaults :index + expect(response).to have_http_status(:success) + expect(response).to render_template('finance/ordergroups/index') + end + + it 'calculates total balance sums correctly' do + get_with_defaults :index + expect(assigns(:total_balances).size).to eq(2) + expect(assigns(:total_balances)[fin_trans_type1.id]).to eq(fin_trans1.amount + fin_trans2.amount) + expect(assigns(:total_balances)[fin_trans_type2.id]).to eq(fin_trans3.amount) + end + end +end From e80ec9c1ce1a4166aa4193f0ebb85a2f50f3de0d Mon Sep 17 00:00:00 2001 From: Philipp Rothmann Date: Thu, 13 Jul 2023 16:03:45 +0200 Subject: [PATCH 084/105] change tests to use assert_select --- app/controllers/finance/ordergroups_controller.rb | 2 +- .../finance/ordergroups/_ordergroups.html.haml | 4 ++-- .../finance/ordergroups_controller_spec.rb | 15 +++++++++++---- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/app/controllers/finance/ordergroups_controller.rb b/app/controllers/finance/ordergroups_controller.rb index 393262a3..58ba0c36 100644 --- a/app/controllers/finance/ordergroups_controller.rb +++ b/app/controllers/finance/ordergroups_controller.rb @@ -14,7 +14,7 @@ class Finance::OrdergroupsController < Finance::BaseController @ordergroups = @ordergroups.page(params[:page]).per(@per_page) @total_balances = FinancialTransactionClass.sorted.each_with_object({}) do |c, tmp| - tmp[c.id] = c.financial_transactions.reduce(0) { | sum, t | sum + t.amount } + tmp[c.id] = c.financial_transactions.reduce(0) { |sum, t| sum + t.amount } end end end diff --git a/app/views/finance/ordergroups/_ordergroups.html.haml b/app/views/finance/ordergroups/_ordergroups.html.haml index 3e0c99fc..6cf12c16 100644 --- a/app/views/finance/ordergroups/_ordergroups.html.haml +++ b/app/views/finance/ordergroups/_ordergroups.html.haml @@ -28,6 +28,6 @@ %th - FinancialTransactionClass.sorted.each do |c| - name = FinancialTransactionClass.has_multiple_classes ? c.display : heading_helper(Ordergroup, :account_balance) - %th.numeric= format_currency @total_balances[c.id] - %th.numeric + %th.numeric{:id => "total_balance#{c.id}"}= format_currency @total_balances[c.id] + %th.numeric#total_balance_sum = format_currency @total_balances.values.reduce(:+) \ No newline at end of file diff --git a/spec/controllers/finance/ordergroups_controller_spec.rb b/spec/controllers/finance/ordergroups_controller_spec.rb index f960c61d..3750016d 100644 --- a/spec/controllers/finance/ordergroups_controller_spec.rb +++ b/spec/controllers/finance/ordergroups_controller_spec.rb @@ -3,24 +3,30 @@ require 'spec_helper' describe Finance::OrdergroupsController do + include ActionView::Helpers::NumberHelper + render_views + let(:user) { create(:user, :role_finance, :role_orders, :ordergroup) } let(:fin_trans_type1) { create(:financial_transaction_type) } let(:fin_trans_type2) { create(:financial_transaction_type) } let(:fin_trans1) do create(:financial_transaction, user: user, + amount: 100, ordergroup: user.ordergroup, financial_transaction_type: fin_trans_type1) end let(:fin_trans2) do create(:financial_transaction, user: user, + amount: 200, ordergroup: user.ordergroup, financial_transaction_type: fin_trans_type1) end let(:fin_trans3) do create(:financial_transaction, user: user, + amount: 42.23, ordergroup: user.ordergroup, financial_transaction_type: fin_trans_type2) end @@ -37,14 +43,15 @@ describe Finance::OrdergroupsController do it 'renders index page' do get_with_defaults :index expect(response).to have_http_status(:success) - expect(response).to render_template('finance/ordergroups/index') end it 'calculates total balance sums correctly' do get_with_defaults :index - expect(assigns(:total_balances).size).to eq(2) - expect(assigns(:total_balances)[fin_trans_type1.id]).to eq(fin_trans1.amount + fin_trans2.amount) - expect(assigns(:total_balances)[fin_trans_type2.id]).to eq(fin_trans3.amount) + expect(response).to have_http_status(:success) + + assert_select "#total_balance#{fin_trans_type1.id}", number_to_currency(300) + assert_select "#total_balance#{fin_trans_type2.id}", number_to_currency(42.23) + assert_select '#total_balance_sum', number_to_currency(342.23) end end end From 817e409a2b142acea41520a88d8e7ad7017668a3 Mon Sep 17 00:00:00 2001 From: Philipp Rothmann Date: Thu, 13 Jul 2023 16:10:53 +0200 Subject: [PATCH 085/105] fix test --- spec/controllers/finance/ordergroups_controller_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/controllers/finance/ordergroups_controller_spec.rb b/spec/controllers/finance/ordergroups_controller_spec.rb index 3750016d..73c1f3bb 100644 --- a/spec/controllers/finance/ordergroups_controller_spec.rb +++ b/spec/controllers/finance/ordergroups_controller_spec.rb @@ -49,8 +49,8 @@ describe Finance::OrdergroupsController do get_with_defaults :index expect(response).to have_http_status(:success) - assert_select "#total_balance#{fin_trans_type1.id}", number_to_currency(300) - assert_select "#total_balance#{fin_trans_type2.id}", number_to_currency(42.23) + assert_select "#total_balance#{fin_trans_type1.financial_transaction_class_id}", number_to_currency(300) + assert_select "#total_balance#{fin_trans_type2.financial_transaction_class_id}", number_to_currency(42.23) assert_select '#total_balance_sum', number_to_currency(342.23) end end From 9282590c063b340226c9d6302a7b2bf274c3b941 Mon Sep 17 00:00:00 2001 From: Philipp Rothmann Date: Fri, 4 Aug 2023 11:19:29 +0200 Subject: [PATCH 086/105] fix: update setup-chromedriver github action --- .github/workflows/ruby.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml index 2b56df0b..fbfe268f 100644 --- a/.github/workflows/ruby.yml +++ b/.github/workflows/ruby.yml @@ -35,7 +35,9 @@ jobs: - name: Checkout source code uses: actions/checkout@v2 - name: Setup chromedriver - uses: nanasess/setup-chromedriver@v1.0.1 + uses: nanasess/setup-chromedriver@v2 + with: + chromedriver-version: '115.0.5790.170' # https://github.com/nanasess/setup-chromedriver/issues/200 - name: Setup ruby uses: ruby/setup-ruby@v1 with: From c4a53caf5231658c040bba92342a113b3a152ded Mon Sep 17 00:00:00 2001 From: Philipp Rothmann Date: Fri, 4 Aug 2023 10:52:30 +0200 Subject: [PATCH 087/105] feat: add actiontext and trix editor --- Dockerfile | 5 +-- Gemfile | 6 ++-- Gemfile.lock | 13 +++++--- app/assets/stylesheets/actiontext.css | 31 +++++++++++++++++++ app/assets/stylesheets/application.css | 1 + app/javascript/application.js | 3 ++ .../action_text/contents/_content.html.erb | 3 ++ config/application.rb | 2 ++ config/importmap.rb | 2 ++ ...6_create_action_text_tables.action_text.rb | 27 ++++++++++++++++ db/schema.rb | 12 ++++++- 11 files changed, 94 insertions(+), 11 deletions(-) create mode 100644 app/assets/stylesheets/actiontext.css create mode 100644 app/views/layouts/action_text/contents/_content.html.erb create mode 100644 db/migrate/20230209105256_create_action_text_tables.action_text.rb diff --git a/Dockerfile b/Dockerfile index 9509c4d3..e8f6a4c0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -49,9 +49,10 @@ RUN export DATABASE_URL=mysql2://localhost/temp?encoding=utf8 && \ rm -Rf /var/lib/apt/lists/* /var/cache/apt/* # Make relevant dirs and files writable for app user -RUN mkdir -p tmp && \ +RUN mkdir -p tmp storage && \ chown nobody config/app_config.yml && \ - chown nobody tmp + chown nobody tmp && \ + chown nobody storage # Run app as unprivileged user USER nobody diff --git a/Gemfile b/Gemfile index 4cc600b0..97422021 100644 --- a/Gemfile +++ b/Gemfile @@ -49,6 +49,8 @@ gem 'whenever', require: false # For defining cronjobs, see config/schedule.rb gem 'exception_notification' gem 'gaffe' gem 'hashie', '~> 3.4.6', require: false # https://github.com/westfieldlabs/apivore/issues/114 +gem "image_processing", "~> 1.12" +gem "importmap-rails", "~> 1.1" gem 'midi-smtp-server' gem 'mime-types' gem 'recurring_select', git: 'https://github.com/gregschmit/recurring_select' @@ -58,6 +60,7 @@ gem 'rswag-api' gem 'rswag-ui' gem 'ruby-filemagic' gem 'spreadsheet' +gem "terser", "~> 1.1" # we use the git version of acts_as_versioned, and need to include it in this Gemfile gem 'acts_as_versioned', git: 'https://github.com/technoweenie/acts_as_versioned.git' @@ -122,6 +125,3 @@ group :test do # api gem 'rswag-specs' end - -gem "importmap-rails", "~> 1.1" -gem "terser", "~> 1.1" diff --git a/Gemfile.lock b/Gemfile.lock index debba0f2..c66901cf 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -254,6 +254,9 @@ GEM i18n-spec (0.6.0) iso ice_cube (0.16.4) + image_processing (1.12.2) + mini_magick (>= 4.9.5, < 5) + ruby-vips (>= 2.0.17, < 3) importmap-rails (1.1.5) actionpack (>= 6.0.0) railties (>= 6.0.0) @@ -320,8 +323,8 @@ GEM mime-types (3.4.1) mime-types-data (~> 3.2015) mime-types-data (3.2022.0105) + mini_magick (4.12.0) mini_mime (1.1.2) - mini_portile2 (2.8.2) minitest (5.18.0) mono_logger (1.1.1) msgpack (1.6.0) @@ -339,9 +342,6 @@ GEM net-smtp (0.3.3) net-protocol nio4r (2.5.8) - nokogiri (1.15.2) - mini_portile2 (~> 2.8.2) - racc (~> 1.4) nokogiri (1.15.2-x86_64-linux) racc (~> 1.4) parallel (1.23.0) @@ -499,6 +499,8 @@ GEM ruby-prof (1.4.5) ruby-progressbar (1.13.0) ruby-units (3.0.0) + ruby-vips (2.1.4) + ffi (~> 1.12) ruby2_keywords (0.0.5) rubyzip (2.3.2) sass-rails (6.0.0) @@ -635,6 +637,7 @@ DEPENDENCIES i18n-js (~> 3.0.0.rc8) i18n-spec ice_cube + image_processing (~> 1.12) importmap-rails (~> 1.1) inherited_resources jquery-rails @@ -692,4 +695,4 @@ DEPENDENCIES whenever BUNDLED WITH - 2.4.13 + 2.4.5 diff --git a/app/assets/stylesheets/actiontext.css b/app/assets/stylesheets/actiontext.css new file mode 100644 index 00000000..3cfcb2b7 --- /dev/null +++ b/app/assets/stylesheets/actiontext.css @@ -0,0 +1,31 @@ +/* + * Provides a drop-in pointer for the default Trix stylesheet that will format the toolbar and + * the trix-editor content (whether displayed or under editing). Feel free to incorporate this + * inclusion directly in any other asset bundle and remove this file. + * + *= require trix +*/ + +/* + * We need to override trix.css’s image gallery styles to accommodate the + * element we wrap around attachments. Otherwise, + * images in galleries will be squished by the max-width: 33%; rule. +*/ +.trix-content .attachment-gallery > action-text-attachment, +.trix-content .attachment-gallery > .attachment { + flex: 1 0 33%; + padding: 0 0.5em; + max-width: 33%; +} + +.trix-content .attachment-gallery.attachment-gallery--2 > action-text-attachment, +.trix-content .attachment-gallery.attachment-gallery--2 > .attachment, .trix-content .attachment-gallery.attachment-gallery--4 > action-text-attachment, +.trix-content .attachment-gallery.attachment-gallery--4 > .attachment { + flex-basis: 50%; + max-width: 50%; +} + +.trix-content action-text-attachment .attachment { + padding: 0 !important; + max-width: 100% !important; +} diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index 6bdfecd2..01dba421 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -7,4 +7,5 @@ *= require list.unlist *= require list.missing *= require recurring_select +*= require actiontext */ diff --git a/app/javascript/application.js b/app/javascript/application.js index beff742e..1ba4a01a 100644 --- a/app/javascript/application.js +++ b/app/javascript/application.js @@ -1 +1,4 @@ // Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails +import "trix" +import "@rails/actiontext" +import "./trix-editor-overrides" diff --git a/app/views/layouts/action_text/contents/_content.html.erb b/app/views/layouts/action_text/contents/_content.html.erb new file mode 100644 index 00000000..9e3c0d0d --- /dev/null +++ b/app/views/layouts/action_text/contents/_content.html.erb @@ -0,0 +1,3 @@ +
    + <%= yield -%> +
    diff --git a/config/application.rb b/config/application.rb index 5a7edb93..696d6647 100644 --- a/config/application.rb +++ b/config/application.rb @@ -68,6 +68,8 @@ module Foodsoft config.active_record.yaml_column_permitted_classes = [Symbol, BigDecimal] config.autoloader = :zeitwerk + + config.active_storage.variant_processor = :mini_magick end # Foodsoft version diff --git a/config/importmap.rb b/config/importmap.rb index 3ed27f76..f882664b 100644 --- a/config/importmap.rb +++ b/config/importmap.rb @@ -1,2 +1,4 @@ # Pin npm packages by running ./bin/importmap pin "application", preload: true +pin "trix" +pin "@rails/actiontext", to: "actiontext.js" diff --git a/db/migrate/20230209105256_create_action_text_tables.action_text.rb b/db/migrate/20230209105256_create_action_text_tables.action_text.rb new file mode 100644 index 00000000..e9c30fac --- /dev/null +++ b/db/migrate/20230209105256_create_action_text_tables.action_text.rb @@ -0,0 +1,27 @@ +# This migration comes from action_text (originally 20180528164100) +class CreateActionTextTables < ActiveRecord::Migration[6.0] + def change + # Use Active Record's configured type for primary and foreign keys + primary_key_type, foreign_key_type = primary_and_foreign_key_types + + create_table :action_text_rich_texts, id: primary_key_type do |t| + t.string :name, null: false + t.text :body, size: :long + t.references :record, null: false, polymorphic: true, index: false, type: foreign_key_type + + t.timestamps + + t.index [:record_type, :record_id, :name], name: "index_action_text_rich_texts_uniqueness", unique: true + end + end + + private + + def primary_and_foreign_key_types + config = Rails.configuration.generators + setting = config.options[config.orm][:primary_key_type] + primary_key_type = setting || :primary_key + foreign_key_type = setting || :bigint + [primary_key_type, foreign_key_type] + end +end diff --git a/db/schema.rb b/db/schema.rb index 50c24c41..9ee6a522 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,17 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 2023_01_06_144440) do +ActiveRecord::Schema[7.0].define(version: 2023_02_15_085312) do + create_table "action_text_rich_texts", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| + t.string "name", null: false + t.text "body", size: :long + t.string "record_type", null: false + t.bigint "record_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["record_type", "record_id", "name"], name: "index_action_text_rich_texts_uniqueness", unique: true + end + create_table "active_storage_attachments", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.string "name", null: false t.string "record_type", null: false From ef6d6aa368b18954365204e1171b3df3e0ead7de Mon Sep 17 00:00:00 2001 From: Philipp Rothmann Date: Fri, 4 Aug 2023 10:55:31 +0200 Subject: [PATCH 088/105] feat(messages): use trix editor in messages --- .../active_storage/blobs/_blob.html.haml | 9 ++++++ app/views/layouts/email.html.haml | 12 +++++++ ...312_migrate_message_body_to_action_text.rb | 32 +++++++++++++++++++ db/schema.rb | 1 - .../app/controllers/messages_controller.rb | 3 +- .../messages/app/helpers/messages_helper.rb | 2 +- plugins/messages/app/models/message.rb | 2 ++ plugins/messages/app/views/messages/new.haml | 2 +- plugins/messages/app/views/messages/show.haml | 2 +- .../foodsoft_message.html.haml | 11 +++++++ plugins/messages/config/locales/de.yml | 3 ++ plugins/messages/config/locales/en.yml | 3 ++ plugins/messages/config/locales/fr.yml | 3 ++ plugins/messages/config/locales/nl.yml | 3 ++ 14 files changed, 83 insertions(+), 5 deletions(-) create mode 100644 app/views/active_storage/blobs/_blob.html.haml create mode 100644 app/views/layouts/email.html.haml create mode 100644 db/migrate/20230215085312_migrate_message_body_to_action_text.rb create mode 100644 plugins/messages/app/views/messages_mailer/foodsoft_message.html.haml diff --git a/app/views/active_storage/blobs/_blob.html.haml b/app/views/active_storage/blobs/_blob.html.haml new file mode 100644 index 00000000..6ddb2e08 --- /dev/null +++ b/app/views/active_storage/blobs/_blob.html.haml @@ -0,0 +1,9 @@ +%figure{class: "attachment attachment--#{blob.representable? ? "preview" : "file"} attachment--#{blob.filename.extension}"} + - if blob.representable? + = image_tag blob.representation(resize_to_limit: local_assigns[:in_gallery] ? [ 800, 600 ] : [ 1024, 768 ]) + %figcaption.attachment__caption + - if caption = blob.try(:caption) + = caption + - else + %span.attachment__name= link_to blob.filename, blob + %span.attachment__size= number_to_human_size blob.byte_size diff --git a/app/views/layouts/email.html.haml b/app/views/layouts/email.html.haml new file mode 100644 index 00000000..6bcf3b4a --- /dev/null +++ b/app/views/layouts/email.html.haml @@ -0,0 +1,12 @@ += yield +\ +%hr +%ul + %li + %a{href: root_url} Foodsoft + - if FoodsoftConfig[:homepage] + %li + %a{href: FoodsoftConfig[:homepage]} Foodcoop + - if FoodsoftConfig[:help_url] + %li + %a{href: FoodsoftConfig[:help_url]}= t '.help' \ No newline at end of file diff --git a/db/migrate/20230215085312_migrate_message_body_to_action_text.rb b/db/migrate/20230215085312_migrate_message_body_to_action_text.rb new file mode 100644 index 00000000..37f8a69c --- /dev/null +++ b/db/migrate/20230215085312_migrate_message_body_to_action_text.rb @@ -0,0 +1,32 @@ +class MigrateMessageBodyToActionText < ActiveRecord::Migration[7.0] + include ActionView::Helpers::TextHelper + + class Message < ApplicationRecord + has_rich_text :body + end + + def change + reversible do |dir| + dir.up do + rename_column :messages, :body, :body_old + Message.all.each do |message| + message.update(body: simple_format(message.body_old)) + message.body.update(record_type: :Message) # action_text_rich_texts uses STI record_type field and has to be set to the real model + end + remove_column :messages, :body_old, :text + end + dir.down do + execute "ALTER TABLE `messages` ADD `body_old` text" + execute "UPDATE `messages` m + INNER JOIN `action_text_rich_texts` a + ON m.id = a.record_id + set m.body_old = a.body" + Message.all.each do |message| + message.update(body_old: strip_tags(message.body_old)) + end + execute "DELETE FROM `action_text_rich_texts` WHERE `action_text_rich_texts`.`record_type` = 'Message'" + execute "ALTER TABLE `messages` CHANGE `body_old` `body` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL;" + end + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 9ee6a522..4c853039 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -292,7 +292,6 @@ ActiveRecord::Schema[7.0].define(version: 2023_02_15_085312) do create_table "messages", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.integer "sender_id" t.string "subject", null: false - t.text "body" t.boolean "private", default: false t.datetime "created_at", precision: nil t.integer "reply_to" diff --git a/plugins/messages/app/controllers/messages_controller.rb b/plugins/messages/app/controllers/messages_controller.rb index 3a003752..aadaad77 100644 --- a/plugins/messages/app/controllers/messages_controller.rb +++ b/plugins/messages/app/controllers/messages_controller.rb @@ -21,7 +21,8 @@ class MessagesController < ApplicationController @message.subject = I18n.t('messages.model.reply_subject', subject: original_message.subject) @message.body = I18n.t('messages.model.reply_header', user: original_message.sender.display, when: I18n.l(original_message.created_at, format: :short)) + "\n" - original_message.body.each_line { |l| @message.body += I18n.t('messages.model.reply_indent', line: l) } + @message.body = I18n.t('messages.model.reply_header', user: original_message.sender.display, when: I18n.l(original_message.created_at, format: :short)) + "\n" \ + + "
    " + original_message.body.to_trix_html + "
    " else redirect_to new_message_url, alert: I18n.t('messages.new.error_private') end diff --git a/plugins/messages/app/helpers/messages_helper.rb b/plugins/messages/app/helpers/messages_helper.rb index adb8fe88..c385a17f 100644 --- a/plugins/messages/app/helpers/messages_helper.rb +++ b/plugins/messages/app/helpers/messages_helper.rb @@ -5,7 +5,7 @@ module MessagesHelper body = '' else subject = message.subject - body = truncate(message.body, length: length - subject.length) + body = truncate(message.body.to_plain_text, length: length - subject.length) end "#{link_to(h(subject), message)} #{h(body)}".html_safe end diff --git a/plugins/messages/app/models/message.rb b/plugins/messages/app/models/message.rb index b6554322..0dd1db19 100644 --- a/plugins/messages/app/models/message.rb +++ b/plugins/messages/app/models/message.rb @@ -22,6 +22,8 @@ class Message < ApplicationRecord validates_presence_of :message_recipients, :subject, :body validates_length_of :subject, in: 1..255 + has_rich_text :body + after_initialize do @recipients_ids ||= [] @send_method ||= 'recipients' diff --git a/plugins/messages/app/views/messages/new.haml b/plugins/messages/app/views/messages/new.haml index 57d6b452..d288cd72 100644 --- a/plugins/messages/app/views/messages/new.haml +++ b/plugins/messages/app/views/messages/new.haml @@ -110,7 +110,7 @@ = f.input :recipient_tokens, :input_html => { 'data-pre' => User.where(id: @message.recipients_ids).map(&:token_attributes).to_json } = f.input :private, inline_label: t('.hint_private') = f.input :subject, input_html: {class: 'input-xxlarge'} - = f.input :body, input_html: {class: 'input-xxlarge', rows: 13} + = f.rich_text_area :body, input_html: {class: 'input-xxlarge', rows: 13} .form-actions = f.submit class: 'btn btn-primary' = link_to t('ui.or_cancel'), :back diff --git a/plugins/messages/app/views/messages/show.haml b/plugins/messages/app/views/messages/show.haml index 36e7b570..8b3f7c1c 100644 --- a/plugins/messages/app/views/messages/show.haml +++ b/plugins/messages/app/views/messages/show.haml @@ -33,7 +33,7 @@ - if @message.can_toggle_private?(current_user) = link_to t('.change_visibility'), toggle_private_message_path(@message), method: :post, class: 'btn btn-mini' %hr/ - %p= simple_format(h(@message.body)) + .trix-content= @message.body %hr/ %p = link_to t('.reply'), new_message_path(:message => {:reply_to => @message.id}), class: 'btn' diff --git a/plugins/messages/app/views/messages_mailer/foodsoft_message.html.haml b/plugins/messages/app/views/messages_mailer/foodsoft_message.html.haml new file mode 100644 index 00000000..7ca572f3 --- /dev/null +++ b/plugins/messages/app/views/messages_mailer/foodsoft_message.html.haml @@ -0,0 +1,11 @@ += raw @message.body +%hr +%ul + - if @message.group + %li= t '.footer_group', group: @message.group.name + %li + %a{href: new_message_url('message[reply_to]' => @message.id)}= t '.reply' + %li + %a{href: message_url(@message)}= t '.see_message_online' + %li + %a{href: my_profile_url}= t '.messaging_options' diff --git a/plugins/messages/config/locales/de.yml b/plugins/messages/config/locales/de.yml index f1615163..eb8cff21 100644 --- a/plugins/messages/config/locales/de.yml +++ b/plugins/messages/config/locales/de.yml @@ -138,6 +138,9 @@ de: Antworten: %{reply_url} Nachricht online einsehen: %{msg_url} Nachrichten-Einstellungen: %{profile_url} + reply: Antworten + see_message_online: Nachricht online einsehen + messaging_options: Nachrichten-Einstellungen footer_group: | Gesendet an Gruppe: %{group} navigation: diff --git a/plugins/messages/config/locales/en.yml b/plugins/messages/config/locales/en.yml index ede3f88c..ccd8bb6c 100644 --- a/plugins/messages/config/locales/en.yml +++ b/plugins/messages/config/locales/en.yml @@ -140,6 +140,9 @@ en: Reply: %{reply_url} See message online: %{msg_url} Messaging options: %{profile_url} + reply: Reply + see_message_online: See message online + messaging_options: Messaging options footer_group: | Sent to group: %{group} navigation: diff --git a/plugins/messages/config/locales/fr.yml b/plugins/messages/config/locales/fr.yml index 54584b48..67d452c5 100644 --- a/plugins/messages/config/locales/fr.yml +++ b/plugins/messages/config/locales/fr.yml @@ -67,6 +67,9 @@ fr: Répondre: %{reply_url} Afficher ce message dans ton navigateur: %{msg_url} Préférences des messages: %{profile_url} + reply: Répondre + see_message_online: Afficher ce message dans ton navigateur + messaging_options: Préférences des messages simple_form: labels: settings: diff --git a/plugins/messages/config/locales/nl.yml b/plugins/messages/config/locales/nl.yml index d3960a23..56738c0b 100644 --- a/plugins/messages/config/locales/nl.yml +++ b/plugins/messages/config/locales/nl.yml @@ -140,6 +140,9 @@ nl: Antwoorden: %{reply_url} Bericht online lezen: %{msg_url} Berichtinstellingen: %{profile_url} + reply: Antwoorden + see_message_online: Bericht online lezen + messaging_options: Berichtinstellingen footer_group: | Verzenden aan groep: %{group} navigation: From bcf47ec92bf2807602adde40e1a0c8a2520e77ed Mon Sep 17 00:00:00 2001 From: Philipp Rothmann Date: Fri, 4 Aug 2023 10:56:21 +0200 Subject: [PATCH 089/105] feat(messages): add max file size for attachments --- app/javascript/trix-editor-overrides.js | 7 +++++++ config/locales/de.yml | 3 +++ config/locales/en.yml | 3 +++ config/locales/es.yml | 3 +++ config/locales/fr.yml | 3 +++ config/locales/nl.yml | 3 +++ config/locales/tr.yml | 2 ++ 7 files changed, 24 insertions(+) create mode 100644 app/javascript/trix-editor-overrides.js diff --git a/app/javascript/trix-editor-overrides.js b/app/javascript/trix-editor-overrides.js new file mode 100644 index 00000000..64cecbef --- /dev/null +++ b/app/javascript/trix-editor-overrides.js @@ -0,0 +1,7 @@ +// app/javascript/trix-editor-overrides.js +window.addEventListener("trix-file-accept", function(event) { + if (event.file.size > 1024 * 1024 * 512) { + event.preventDefault() + alert(I18n.t('js.trix_editor.file_size_alert')) + } +}) \ No newline at end of file diff --git a/config/locales/de.yml b/config/locales/de.yml index 7fe377ba..b7f77c5d 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -1216,12 +1216,15 @@ de: js: ordering: confirm_change: Änderungen an dieser Bestellung gehen verloren, wenn zu einer anderen Bestellung gewechselt wird. Möchtest Du trotzdem wechseln? + trix_editor: + file_size_alert: Der Dateianhang ist zu groß! Die maximale Größe beträgt 512Mb layouts: email: footer_1_separator: "--" footer_2_foodsoft: 'Foodsoft: %{url}' footer_3_homepage: 'Foodcoop: %{url}' footer_4_help: 'Hilfe: %{url}' + help: 'Hilfe' foodsoft: Foodsoft footer: revision: Revision %{revision} diff --git a/config/locales/en.yml b/config/locales/en.yml index bb00e997..b4f41c5c 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1219,12 +1219,15 @@ en: js: ordering: confirm_change: Modifications to this order will be lost when you change the order. Do you want to lose the changes you made and continue? + trix_editor: + file_size_alert: The file is to large! The supported file size is 512Mb! layouts: email: footer_1_separator: "--" footer_2_foodsoft: 'Foodsoft: %{url}' footer_3_homepage: 'Foodcoop: %{url}' footer_4_help: 'Help: %{url}' + help: 'Help' foodsoft: Foodsoft footer: revision: revision %{revision} diff --git a/config/locales/es.yml b/config/locales/es.yml index 2be287bf..6cacb564 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -1080,9 +1080,12 @@ es: js: ordering: confirm_change: Las modificaciones sobre este pedido se perderán cuando cambies el pedido. ¿Quieres perder los cambios que has hecho y continuar? + trix_editor: + file_size_alert: ¡El archivo adjunto es demasiado grande! El tamaño máximo es de 512Mb layouts: email: footer_4_help: 'Ayuda: %{url}' + help: 'Ayuda' footer: revision: revisión %{revision} header: diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 491408dc..cd0971da 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -831,10 +831,13 @@ fr: js: ordering: confirm_change: Les changements apportés à cette commande vont être perdus. Est-ce que tu veux vraiment continuer? + trix_editor: + file_size_alert: Le fichier joint est trop volumineux ! La taille maximale est de 512Mb layouts: email: footer_3_homepage: 'Boufcoop: %{url}' footer_4_help: 'Aide: %{url}' + help: 'Aide' footer: revision: révision %{revision} header: diff --git a/config/locales/nl.yml b/config/locales/nl.yml index e2e8da98..d972c088 100644 --- a/config/locales/nl.yml +++ b/config/locales/nl.yml @@ -1189,12 +1189,15 @@ nl: js: ordering: confirm_change: Als je naar een andere bestelling gaat, gaan je aanpassingen in deze bestelling verloren. Wijzigingen vergeten en naar de andere bestelling gaan? + trix_editor: + file_size_alert: De bestandsbijlage is te groot! De maximale grootte is 512Mb! layouts: email: footer_1_separator: "--" footer_2_foodsoft: 'Foodsoft: %{url}' footer_3_homepage: 'Foodcoop: %{url}' footer_4_help: 'Help: %{url}' + help: 'Help' foodsoft: Foodsoft footer: revision: revisie %{revision} diff --git a/config/locales/tr.yml b/config/locales/tr.yml index b66d5c06..76408463 100644 --- a/config/locales/tr.yml +++ b/config/locales/tr.yml @@ -1218,6 +1218,8 @@ tr: js: ordering: confirm_change: Bu siparişe yapılan değişiklikler kaybolacak. Değişikliklerinizi kaybetmek ve devam etmek istiyor musunuz? + trix_editor: + file_size_alert: Dosya eki çok büyük! Maksimum boyut 512Mb layouts: email: footer_1_separator: "--" From a96f21134e877e799f5dce6cfb399e725bce27e2 Mon Sep 17 00:00:00 2001 From: Philipp Rothmann Date: Fri, 4 Aug 2023 10:57:11 +0200 Subject: [PATCH 090/105] feat(messages): attachment retention task --- config/app_config.yml.SAMPLE | 3 +++ config/schedule.rb | 3 ++- lib/tasks/foodsoft.rake | 15 ++++++++++++++- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/config/app_config.yml.SAMPLE b/config/app_config.yml.SAMPLE index 33a6b356..b43b7935 100644 --- a/config/app_config.yml.SAMPLE +++ b/config/app_config.yml.SAMPLE @@ -173,6 +173,9 @@ default: &defaults # default to allow automatically adding new articles on sync only when less than 200 articles in total #shared_supplier_article_sync_limit: 200 + # number of days after which attachment files get deleted + #attachment_retention_days: 365 + development: <<: *defaults diff --git a/config/schedule.rb b/config/schedule.rb index 72e3cbcc..6d424324 100644 --- a/config/schedule.rb +++ b/config/schedule.rb @@ -12,9 +12,10 @@ every :weekday, at: %w[5:56am 6:04pm] do rake 'multicoops:run TASK=foodsoft:import_and_assign_bank_transactions' end -# Weekly taks +# Weekly tasks every :sunday, at: '7:14 am' do rake 'multicoops:run TASK=foodsoft:create_upcoming_periodic_tasks' + rake 'multicoops:run TASKS=foodsoft:prune_old_attachments' end # Finish ended orders diff --git a/lib/tasks/foodsoft.rake b/lib/tasks/foodsoft.rake index 218bb39f..caa54a1a 100644 --- a/lib/tasks/foodsoft.rake +++ b/lib/tasks/foodsoft.rake @@ -1,6 +1,6 @@ # put in here all foodsoft tasks # => :environment loads the environment an gives easy access to the application -namespace :foodsoft do +namespace :foodsoft do # rubocop:disable Metrics/BlockLength desc 'Finish ended orders' task finish_ended_orders: :environment do Order.finish_ended! @@ -79,6 +79,19 @@ namespace :foodsoft do rake_say "#{ba.name}: imported #{importer.count}, assigned #{assign_count}" end end + + desc 'Prune attachments older than maximum age' + task prune_old_attachments: :environment do + if FoodsoftConfig[:attachment_retention_days] + rake_say "Pruning attachments older than #{FoodsoftConfig[:attachment_retention_days]} days" + ActiveStorage::Attachment.where("created_at < ?", FoodsoftConfig[:attachment_retention_days].days.ago).each do |attachment| + rake_say attachment.inspect + attachment.purge_later + end + else + rake_say "Please configure your app_config.yml accordingly:\nattachment_retention_days: " + end + end end # Helper From 1e63c59a8af120da718bd8919795408a682b3f5b Mon Sep 17 00:00:00 2001 From: Philipp Rothmann Date: Wed, 23 Aug 2023 12:17:32 +0200 Subject: [PATCH 091/105] fix: loading trix editor overwrite in production --- app/javascript/application.js | 2 +- config/importmap.rb | 1 + config/initializers/assets.rb | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/javascript/application.js b/app/javascript/application.js index 1ba4a01a..141d3cae 100644 --- a/app/javascript/application.js +++ b/app/javascript/application.js @@ -1,4 +1,4 @@ // Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails import "trix" import "@rails/actiontext" -import "./trix-editor-overrides" +import "trix-editor-overrides" diff --git a/config/importmap.rb b/config/importmap.rb index f882664b..d6d39932 100644 --- a/config/importmap.rb +++ b/config/importmap.rb @@ -2,3 +2,4 @@ pin "application", preload: true pin "trix" pin "@rails/actiontext", to: "actiontext.js" +pin "trix-editor-overrides" \ No newline at end of file diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb index d507a355..d52cecaa 100644 --- a/config/initializers/assets.rb +++ b/config/initializers/assets.rb @@ -9,4 +9,4 @@ Rails.application.config.assets.version = '1.0' # Precompile additional assets. # application.js, application.css, and all non-JS/CSS in the app/assets # folder are already added. -Rails.application.config.assets.precompile += %w[application_legacy.js jquery.min.js] +Rails.application.config.assets.precompile += %w[application_legacy.js jquery.min.js trix-editor-overrides.js] From caa32de30c8404537aaaacb338a8d0638bbc4613 Mon Sep 17 00:00:00 2001 From: Philipp Rothmann Date: Wed, 23 Aug 2023 12:47:58 +0200 Subject: [PATCH 092/105] fix: rubocop violation --- config/importmap.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/importmap.rb b/config/importmap.rb index d6d39932..3ba2318b 100644 --- a/config/importmap.rb +++ b/config/importmap.rb @@ -2,4 +2,4 @@ pin "application", preload: true pin "trix" pin "@rails/actiontext", to: "actiontext.js" -pin "trix-editor-overrides" \ No newline at end of file +pin "trix-editor-overrides" From eb6cf00f945d6fd57f7706a3a425fec60720c6bf Mon Sep 17 00:00:00 2001 From: Philipp Rothmann Date: Wed, 23 Aug 2023 15:19:03 +0200 Subject: [PATCH 093/105] update README --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 8eb17918..ccaa4db3 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ State of this Fork disussion [#956](https://github.com/foodcoops/foodsoft/issues/956) 1. Javascript Importmap * [x] [fork](https://git.local-it.org/Foodsoft/foodsoft/src/branch/9_rails_v_7_js_importmap) - * [ ] upstream + * [x] upstream #### Article Order Import/Export @@ -75,10 +75,10 @@ Updating Articles from large resellers and exporting orders is now much easier! 1. Richtext editor for messages. Also allows sending attachements. * [x] [fork](https://git.local-it.org/Foodsoft/foodsoft/src/branch/16_html_message_templates) - * [ ] upstream + * [x] upstream 1. Show the sum of all order group balances * [x] [fork](https://git.local-it.org/Foodsoft/foodsoft/src/branch/47_finance_ordergroup_sums) - * [ ] upstream + * [x] upstream 1. UI improvements for group order view * [x] [fork](https://git.local-it.org/Foodsoft/foodsoft/src/branch/uxui_group_order) * [ ] upstream From 91f27a0a4888542ec96838e5e656734578e1d67e Mon Sep 17 00:00:00 2001 From: Philipp Rothmann Date: Sun, 3 Sep 2023 22:13:59 +0200 Subject: [PATCH 094/105] chore: update chrowdin translations --- config/locales/de.yml | 26 +- config/locales/es.yml | 299 ++++++++++++++++++- config/locales/fr.yml | 12 +- config/locales/nl.yml | 108 ++++--- crowdin.yml | 2 + plugins/current_orders/config/locales/de.yml | 4 + plugins/current_orders/config/locales/es.yml | 5 + plugins/discourse/config/locales/es.yml | 1 + plugins/documents/config/locales/de.yml | 1 + plugins/documents/config/locales/es.yml | 9 + plugins/messages/config/locales/de.yml | 2 + plugins/messages/config/locales/es.yml | 30 +- plugins/wiki/config/locales/de.yml | 4 + plugins/wiki/config/locales/es.yml | 48 +++ 14 files changed, 497 insertions(+), 54 deletions(-) diff --git a/config/locales/de.yml b/config/locales/de.yml index b7f77c5d..6a957ec2 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -448,6 +448,7 @@ de: error_denied_sign_in: als ein anderer Benutzer anmelden error_feature_disabled: Diese Funktion ist derzeit nicht aktiviert. error_members_only: Diese Aktion ist nur für Mitglieder der Gruppe erlaubt! + error_minimum_balance: Ihr Kontostand liegt leider unter dem Minimum von %{min}. error_token: Zugriff verweigert (ungültiger Token)! article_categories: create: @@ -697,6 +698,7 @@ de: applications: Apps foodcoop: Foodcoop language: Sprache + layout: Layout list: Liste messages: Nachrichten others: Sonstiges @@ -1132,6 +1134,7 @@ de: create: Einladung verschicken tasks: required_users: "Es fehlen %{count} Mitstreiterinnen!" + task_title: "%{name} (%{duration}h)" home: apple_bar: desc: 'Abgebildet ist das Verhältnis von erledigten Aufgaben zu dem Bestellvolumen Deiner Bestellgruppe im Vergleich zum Durchschnitt in der Foodcoop. Konkret: Pro %{amount} Bestellsumme solltest Du eine Aufgabe machen!' @@ -1231,6 +1234,7 @@ de: header: feedback: desc: Fehler gefunden? Vorschlag? Idee? Kritik? + title: Feedback help: Hilfe logout: Abmelden ordergroup: Meine Bestellgruppe @@ -1268,6 +1272,7 @@ de: feedback: header: "%{user} schrieb am %{date}:" subject: Feedback zur Foodsoft + from_via_foodsoft: "%{name} via Foodsoft" invite: subject: Einladung in die Foodcoop text: | @@ -1423,6 +1428,7 @@ de: stock: Lager suppliers: Lieferanten/Artikel title: Artikel + dashboard: Übersichtsseite finances: accounts: Konten verwalten balancing: Bestellungen abrechnen @@ -1430,6 +1436,7 @@ de: home: Übersicht invoices: Rechnungen title: Finanzen + foodcoop: Foodcoop members: Mitglieder ordergroups: Bestellgruppen orders: @@ -1480,6 +1487,7 @@ de: articles: Artikel delivery_day: Liefertag heading: Bestellung für %{name} + name: Name number: Nummer to_address: Versandaddresse finish: @@ -1579,6 +1587,7 @@ de: index: article_pdf: Artikel PDF group_pdf: Gruppen PDF + matrix_pdf: Matrix PDF title: Abholtage sessions: logged_in: Angemeldet! @@ -1589,6 +1598,7 @@ de: forgot_password: Passwort vergessen? login: Anmelden nojs: Achtung, Cookies und Javascript müssen aktiviert sein! %{link} bitte abschalten. + noscript: NoScript title: Foodsoft anmelden shared: articles: @@ -1617,8 +1627,13 @@ de: who_ordered: Wer hat bestellt? order_download_button: article_pdf: Artikel PDF + download_file: Datei herunterladen + fax_csv: Fax CSV + fax_pdf: Fax PDF fax_txt: Fax Text group_pdf: Gruppen PDF + matrix_pdf: Matrix PDF + title: Herunterladen task_list: accept_task: Aufgabe übernehmen done: Erledigt @@ -1675,10 +1690,11 @@ de: profile: language: de: Deutsch + en: Englisch es: Spanisch - tr: Türkisch fr: Französisch nl: Niederländisch + tr: Türkisch required: mark: "*" text: benötigt @@ -1835,6 +1851,7 @@ de: confirm_delete_single_from_group: Bist Du sicher, dass Du diese Aufgabe löschen möchtest (und in Bezug stehende wiederkehrende Aufgabe behalten möchtest)? delete_group: Aufgabe und folgende löschen edit_group: Wiederkehrende ändern + hours: "%{count}h" mark_done: Als erledigt markieren reject_task: Aufgabe ablehnen title: Aufgabe anzeigen @@ -1861,6 +1878,9 @@ de: delete: Löschen download: Herunterladen edit: Bearbeiten + marks: + close: "×" + success: move: Verschieben or_cancel: oder abbrechen please_wait: Bitte warten... @@ -1870,6 +1890,10 @@ de: show: Anzeigen views: pagination: + first: "«" + last: "»" + next: "›" + previous: "‹" truncate: "..." workgroups: edit: diff --git a/config/locales/es.yml b/config/locales/es.yml index 6cacb564..d722a872 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -35,9 +35,15 @@ es: unit_quantity: Cantidad de unidades bank_account: balance: Saldo + bank_gateway: Pasarela bancaria description: Descripción iban: IBAN name: Nombre + bank_gateway: + authorization: Cabecera-Autorización + name: Nombre + unattended_user: Usuario desatendido + url: Enlace bank_transaction: amount: Cantidad date: Fecha @@ -65,6 +71,7 @@ es: ordergroup: Grupo de pedido user: Ingresado por financial_transaction_class: + ignore_for_account_balance: Ignorar en el saldo de la cuenta name: Nombre financial_transaction_type: bank_account: Cuenta bancaria @@ -186,6 +193,11 @@ es: phone2: teléfono 2 shared_sync_method: Cómo sincronizar url: Web + supplier_category: + name: Nombre + description: Descripción + financial_transaction_class: Clase de transacciones financieras + bank_account: Cuenta bancaria task: created_by: Creado por created_on: Creado en @@ -243,6 +255,8 @@ es: models: article: Artículo article_category: Categoría + bank_account: Cuenta bancaria + bank_gateway: Pasarela bancaria bank_transaction: Transacción bancaria delivery: Entrega financial_transaction: Transacción financiara @@ -258,6 +272,7 @@ es: stock_article: Artículo de stock stock_taking: Toma de inventario supplier: Proveedor + supplier_category: Categoría del proveedor task: Tarea user: Usuario workgroup: Grupo de trabajo @@ -268,7 +283,7 @@ es: all_ordergroups: Todos los grupos de pedido all_users: Todos los usuarios all_workgroups: Todos los grupos de trabajo - created_at: creado + created_at: creado first_paragraph: Aquí puedes administrar grupos y usuarios de Foodsoft. groupname: nombre del grupo members: miembros @@ -281,6 +296,14 @@ es: title: Administración type: tipo username: nombre de usuario + bank_accounts: + form: + title_edit: Editar cuenta bancaria + title_new: Añadir nueva cuenta bancaria + bank_gateways: + form: + title_edit: Editar pasarela bancaria + title_new: Añadir nueva pasarela bancaria configs: list: key: Clave @@ -308,6 +331,26 @@ es: finances: index: bank_accounts: Cuentas bancarias + first_paragraph: Aquí puede administrar las clases de transacciones financieras y los tipos de transacciones financieras correspondientes. Cada transacción financiera tiene un tipo, que usted tiene que seleccionar en cada transacción, si usted creó más de un tipo. Las clases de transacciones financieras pueden utilizarse para agrupar los tipos de transacciones financieras y se mostrarán como columnas adicionales en el resumen de la cuenta, si se ha creado más de una. + new_bank_account: Añadir nueva cuenta bancaria + new_financial_transaction_class: Añadir nueva clase de transacción financiera + new_bank_gateway: Añadir nueva pasarela bancaria + title: Finanzas + transaction_types: Tipos de transacciones financieras + supplier_categories: Categorías de proveedores + new_supplier_category: Nueva categoría de proveedor + transaction_types: + name: Nombre + new_financial_transaction_type: Añadir nuevo tipo de transacción financiera + financial_transaction_classes: + form: + title_edit: Editar clase de transacción financiera + title_new: Añadir nueva clase de transacción financiera + financial_transaction_types: + form: + name_short_desc: El nombre corto es obligatorio para los tipos de transacciones financieras que deben ser asignados automáticamente en las transacciones bancarias. Si hay varias cuentas bancarias, se puede seleccionar la cuenta preferida para las transferencias bancarias. + title_edit: Editar tipo de transacción financiera + title_new: Añadir nuevo tipo de transacción financiera mail_delivery_status: destroy_all: notice: Se han borrado todos los problemas de email @@ -345,11 +388,15 @@ es: notice: El usuario/a ha sido borrado edit: title: Edita usuario/a + form: + create_ordergroup: Crear grupo de pedido con el mismo nombre y añadir usuario. + send_welcome_mail: Enviar un correo de bienvenida al usuario/a. index: first_paragraph: Aquí puedes %{url}, editar y borar usuarios. new_user: Crea nuevo usuario/a new_users: crea nuevo show_deleted: Muestra usuarios borrados + title: Usuario/a administrador new: title: Crea nuevo usuario restore: @@ -390,6 +437,10 @@ es: workgroups: members: miembros name: nombre + supplier_categories: + form: + title_new: Añadir categoría de proveedor + title_edit: Editar categoría de proveedor application: controller: error_authn: Es necesaria la autenticación! @@ -397,6 +448,7 @@ es: error_denied_sign_in: entra como otro usuario/a error_feature_disabled: Esta opción está actualmente deshabilitada error_members_only: Esta acción está sólo disponible para miembros de un grupo. + error_minimum_balance: Lo sentimos, el saldo de tu cuenta está por debajo del mínimo de %{min}. error_token: Acceso denegado (invalid token). article_categories: create: @@ -433,6 +485,7 @@ es: error_update: 'Ha ocurrido un error miebtras se actualizaba el artículo ''%{article}'': %{msg}' parse_upload: no_file: Elige un archivo para subir. + notice: "%{count} artículos fueron analizados con éxito." sync: notice: El catálogo está actualizado shared_alert: "%{supplier} no está conectado a una base de datos externa" @@ -462,6 +515,7 @@ es: not_found: No se han encontrado articulos index: change_supplier: Cambiar proveedor ... + download: Descargar artículos edit_all: Editar todos ext_db: import: Importar artículo @@ -513,18 +567,29 @@ es: status: Estado (x=saltar) file_label: Por favor elige un archivo compatible options: - convert_units: Mantener unidades actuales, recomputar la cantidad y precio de unidades (como sincronizar). + convert_units: Mantener unidades actuales, recomputar la cantidad y precio de unidades (como sincronizar). outlist_absent: Borrar artículos que no están en el archivo subido. sample: juices: Jugos nuts: Nueces organic: Orgánico + supplier_1: Nuttyfarm + supplier_2: Brownfields + supplier_3: Greenfields tomato_juice: Jugo de tomate walnuts: Nogal submit: Subir archivo text_1: 'Aquí puedes subir una hoja de cálculo para actualizar los artículos de %{supplier}. Se aceptan los formatos Excel (xls, xlsx) y OpenOffice (ods), al igual que archivos CSV (con columnas separadas por ";" con codificación UTF-8). Solo se importará la primera hoja y las columnas deben estar en el siguiente orden:' text_2: Las hileras que se muestran aquí son ejemplos. Cuando hay una "x" en la primera columna, el artículo se sacará de la lista. Esto te permite editar la hoja de cálculo y rápidamente sacar muchos artículos a la vez, por ejemplo cuando los artículos ya no están disponibles con el proveedor. La categoría se hará coincidir con tu lista de categorías de Foodsoft (tanto por nombre de categoría como nombre de importación). title: Subir artículos de %{supplier} + bank_account_connector: + confirm: Por favor, confirme el código %{code}. + fields: + email: E-Mail + pin: PIN + password: Contraseña + tan: TAN + username: Nombre de Usuario/a config: hints: applepear_url: Web donde se explica el sistema de manzanas y peras. @@ -546,7 +611,9 @@ es: order_schedule: boxfill: recurr: Programa cuándo la fase de llenado de cajas comienza por defecto. + time: Tiempo por defecto cuando comienza la fase de llenado de caja del pedido. ends: + recurr: Programa para la fecha predeterminada de cierre de pedidos. time: Fecha por defecto cuando se cierran los pedidos. initial: La agenda comienza en esta fecha. page_footer: Se muestra en cada página en la parte inferior. Dejar vacío para desactivar el pie de página por completo. @@ -566,12 +633,15 @@ es: use_boxfill: Cuando está activado, cerca del cierre de un pedido los miembros no podrán cambiar su pedido a menos que se incremente el valor pedido total. Esto ayudará a llenar las cajas que faltan. Igualmente deberás decidir una fecha de llenado de cajas para los pedidos. use_iban: Cuando esta opción está habilitada, el proveedor y el usuario pueden guardan también su número de cuenta bancaria internacional (IBAN). use_nick: Muestra y utiliza apodos en lugar de nombres reales. Cuando activas esto debes chequear que todos los usuarios tengan apodo. + use_self_service: Cuando está activado, los miembros pueden usar las funciones de balance seleccionadas por sí mismos. + webstats_tracking_code: Código de seguimiento para analíticas web (como Piwik o Google analytics). Dejar en blanco si no usa estas analíticas. keys: applepear_url: Enlace de ayuda para el sistema de puntos-manzana charge_members_manually: Cambia los miembros manualmente contact: city: Ciudad country: País + email: E-mail phone: Teléfono street: Calle zip_code: Código postal @@ -579,6 +649,12 @@ es: currency_unit: Moneda custom_css: CSS adicional default_locale: Idioma por defecto + default_role_article_meta: Artículos + default_role_finance: Finanzas + default_role_invoices: Facturas + default_role_orders: Pedidos + default_role_pickups: Días de recogida + default_role_suppliers: Proveedores disable_invite: Desactivar invitaciones disable_members_overview: Desactivar la lista de miembros email_from: Dirección de email de origen @@ -604,6 +680,7 @@ es: price_markup: Margen de la cooperativa stop_ordering_under: Puntos-manzana mínimos tasks_period_days: Periodo + tasks_upfront_days: Crear de antemano tax_default: IVA por defecto time_zone: Zona horaria tolerance_is_costly: La tolerancia es prioritaria @@ -615,8 +692,10 @@ es: use_boxfill: Fase de llenar las cajas use_iban: Usar IBAN use_nick: Usa apodos + use_self_service: Usar auto servicio webstats_tracking_code: Código de seguimiento tabs: + applications: Aplicaciones foodcoop: Cooperativa language: Idioma layout: Disposición @@ -624,6 +703,7 @@ es: messages: Mensajes others: Otro payment: Finanzas + security: Seguridad tasks: Tareas deliveries: add_stock_change: @@ -683,9 +763,11 @@ es: - Unidad - Precio/Unidad - Subtotal + total: Total order_matrix: filename: Pedido %{name}-%{date} - matrix para ordenar heading: Descripción del artículo (%{count}) + title: 'Matriz de ordenamiento de pedidos: %{name}, cerrada a las %{date}' errors: general: Ha ocurrido un problema. general_again: Ha ocurrido un problema. Por favor inténtalo de nuevo. @@ -702,6 +784,7 @@ es: notice: Tus comentarios fueron enviados con éxito. ¡Muchas gracias! new: first_paragraph: '¿Encontraste un error? ¿Tienes sugerencias, ideas o comentarios? Nos gustaría recibir tus comentarios.' + second_paragraph: Tenga en cuenta que el equipo de Foodsoft es el único responsable del mantenimiento del software. Para preguntas relacionadas con la organización de tu Foodcoop, por favor contacta a la persona de contacto apropiada. send: Enviar title: Enviar comentarios finance: @@ -709,6 +792,8 @@ es: close: alert: 'Ocurrió un error en la contabilidad: %{message}' notice: El pedido se ha cerrado con éxito, el balance de la cuenta ha sido actualizado. + close_all_direct_with_invoice: + notice: '%{count} pedidos han sido liquidados.' close_direct: alert: 'El pedido no se puede cerrar: %{message}' notice: El pedido ha sido cerrado @@ -717,17 +802,23 @@ es: first_paragraph: 'Cuando el pedido se cierre se actualizarán todas las cuentas del grupo.
    Las cuentas serán cargadas así:' or_cancel: o vuelve a contabilidad title: Cierra el pedido + edit_note: + title: Editar nota de pedido edit_results_by_articles: add_article: Añadir artículo amount: Importe + edit_transport: Editar transporte gross: Bruto net: Neto + edit_transport: + title: Distribuir costes de transporte group_order_articles: add_group: Añadir grupo total: Costes totales total_fc: Suma (precio al grupo) units: Unidades index: + close_all_direct_with_invoice: Cerrar todo con factura title: Pedidos cerrados invoice: edit: Editar factura @@ -778,7 +869,10 @@ es: with_extra_charge: 'con cargo extra:' without_extra_charge: 'sin cargo extra:' bank_accounts: + assign_unlinked_transactions: + notice: '%{count} transacciones han sido asignadas' import: + notice: '%{count} nuevas transacciones han sido importadas' no_import_method: Para esta cuenta bancaria no se ha configurado ningún método de importación. submit: Importar title: Importar transacciones bancarias para %{name} @@ -807,12 +901,16 @@ es: notice: El enlace a la factura ha sido añadido. create: notice: Se ha creado un nuevo enlance financiero. + create_financial_transaction: + notice: La transacción financiera ha sido añadida. index_bank_transaction: title: Añadir transacción bancaria index_financial_transaction: title: Añadir transacción financiera index_invoice: title: Añadir factura + new_financial_transaction: + title: Añadir transacción financiera remove_bank_transaction: notice: Se ha eliminado el enlace a la transacción bancaria. remove_financial_transaction: @@ -820,7 +918,15 @@ es: remove_invoice: notice: El enlace a la factura ha sido eliminado. show: + add_bank_transaction: Añadir transacción bancaria + add_financial_transaction: Añadir transacción financiera + add_invoice: Añade factura + amount: Cantidad + date: Fecha + description: Descripción + new_financial_transaction: Nueva transacción financiera title: Enlace financiero %{number} + type: Tipo financial_transactions: controller: create: @@ -829,7 +935,11 @@ es: alert: 'Ha ocurrido un error: %{error}' error_note_required: Note se requiere! notice: Se han guardado todas las transacciones + destroy: + notice: La transacción ha sido eliminada. index: + balance: 'Saldo de la cuenta: %{balance}' + last_updated_at: "(última actualización hace %{when})" new_transaction: Crea nueva transacción title: Balance de cuentas para %{name} index_collection: @@ -837,16 +947,26 @@ es: title: Transacciones financieras new: paragraph: Aquí puedes poner o quitar dinero del grupo de pedido %{name}. + paragraph_foodcoop: Aquí puedes poner y quitar dinero para el foodcoop. title: Nueva transacción new_collection: add_all_ordergroups: Añade todos los grupos de pedido + add_all_ordergroups_custom_field: Añadir todos los pedidos de grupo con %{label} + create_financial_link: Crear un vínculo financiero común para las nuevas transacciones. + create_foodcoop_transaction: Crear una transacción con la suma inversa para el foodcoop (en el caso de "doble entrada de cuenta") new_ordergroup: Añade nuevo grupo de pedido save: Guarda transacción + set_balance: Ajuste el saldo del grupo de pedido a la cantidad introducida. sidebar: Aquí puedes actualizar más cuentas al mismo tiempo. Por ejemplo, todas las transferencias del grupo de pedido de un balance de cuenta. title: Actualizar más cuentas ordergroup: remove: Remover remove_group: Remover grupo + transactions: + confirm_revert: '¿Estás seguro de que quieres revertir %{name}? En este caso se creará una nueva transacción con una cantidad invertida y en combinación con la transacción original ocultada. Estas transacciones ocultas sólo son visibles a través de la opción ''Mostrar oculto'' y no son visibles para los usuarios normales en absoluto.' + revert_title: Revertir la transacción, que la ocultará a los usuarios normales. + transactions_search: + show_hidden: Mostrar transacciones ocultas index: amount_fc: Importe(FC) end: Fin @@ -863,6 +983,7 @@ es: attachment_hint: Sólo se permiten los formatos JPEG y PDF. index: action_new: Crea nueva factura + show_unpaid: Mostrar facturas no pagadas title: Facturas new: title: Crea nueva factura @@ -874,8 +995,10 @@ es: title: Facturas impagas ordergroups: index: + new_financial_link: Nuevo enlace financiero new_transaction: Añade nuevas transacciones show_all: Todas las transacciones + show_foodcoop: Transacciones de Foodcoop title: Maneja los grupos ordergroups: account_statement: Balance de cuenta @@ -889,6 +1012,8 @@ es: only_active: Sólo grupos activos only_active_desc: "(han hecho al menos un pedido en los últimos 3 meses)" title: Grupo de pedido + ordergroups: + break: "%{start} - %{end}" users: index: body: "

    Desde aquí puedes escribir un mensaje a los miembros de tu cooperativa Foodcoop. Recuerda habilitar en %{profile_link} tus detalles de contacto para que sean visibles.

    " @@ -987,10 +1112,12 @@ es: application: edit_user: Edita usuario nick_fallback: "(no tiene apodo)" + role_admin: Admin role_article_meta: Artículos role_finance: Finanzas role_invoices: Facturas role_orders: Pedidos + role_pickups: Días de recogida role_suppliers: Proveedores show_google_maps: Muéstralo en Google maps sort_by: Ordena por %{text} @@ -1000,12 +1127,14 @@ es: orders: old_price: Precio anterior option_choose: Elige proveedor/stock + option_stock: Existencias order_pdf: Crea PDF submit: invite: create: envía invitación tasks: required_users: "Aún se necesitan %{count} miembros!" + task_title: "%{name} (%{duration}h)" home: apple_bar: desc: 'Esto muestra la proporción de tareas completadas respecto al volumen de pedidos de tu grupo de pedido en comparación con el promedio en Foodcoop. En práctica: por cada %{amount} de pedidos totales, tú deberías hacer una tarea!' @@ -1014,8 +1143,9 @@ es: warning: Cuidado, si tienes menos de %{threshold} puntos-manzana no puedes hacer un pedido! changes_saved: Guarda los cambios. index: + due_date_format: "%A %d %B" my_ordergroup: - last_update: La última actualización fue hace %{when} + last_update: La última actualización fue hace %{when} title: Mi grupo de pedido transactions: title: Últimas transacciones @@ -1047,12 +1177,21 @@ es: title: Mi Perfil user: since: "(miembro para %{when})" + title: "%{user}" + reference_calculator: + transaction_types_headline: Propósito + placeholder: Por favor, introduzca primero las cantidades que desea transferir en cada campo, para ver la referencia que debe utilizar para esa transacción. + text0: Por favor transfiera + text1: con la referencia + text2: a la cuenta bancaria + title: Calculador de referencia start_nav: admin: Administración finances: accounts: Actualizar cuentas settle: Pedidos de la cuenta title: Finanzas + foodcoop: Foodcoop members: Miembros new_ordergroup: Nuevo grupo de pedido new_user: Nuevo miembro @@ -1081,20 +1220,30 @@ es: ordering: confirm_change: Las modificaciones sobre este pedido se perderán cuando cambies el pedido. ¿Quieres perder los cambios que has hecho y continuar? trix_editor: - file_size_alert: ¡El archivo adjunto es demasiado grande! El tamaño máximo es de 512Mb + file_size_alert: '¡El archivo adjunto es demasiado grande! El tamaño máximo es de 512Mb' layouts: email: + footer_1_separator: "--" + footer_2_foodsoft: 'Foodsoft: %{url}' + footer_3_homepage: 'Foodcoop: %{url}' footer_4_help: 'Ayuda: %{url}' help: 'Ayuda' + foodsoft: Foodsoft footer: revision: revisión %{revision} header: feedback: desc: '¿Encontrase algún error? ¿Sugerencias? ¿Ideas?' + title: Sugerencias help: Ayuda logout: Salir ordergroup: Mis grupos de pedido profile: Edita perfil + reference_calculator: Calculador de referencia + logo: "foodsoft" + lib: + render_pdf: + page: Página %{number} de %{count} login: accept_invitation: body: "

    Has sido invitado a formar parte de %{foodcoop} como miembro del grupo %{group}.

    Si quieres participar, es necesario que completes este formulario.

    Tu información no será compartida con terceros bajo ninguna razón. Puedes decidir qué información personal será visible. 'Todos' hace referencia a todos los miembros de Foodcoop. Sólo los administradores tienen acceso a tu información.

    " @@ -1107,7 +1256,7 @@ es: error_invite_invalid: Tu invitación no es válida. error_token_invalid: La sesión ha expirado o no es válida. Prueba de nuevo. reset_password: - notice: Si tu email está ya registrado aquí, recibirás un mensaje con un enlace para + notice: Si tu email está ya registrado aquí, recibirás un mensaje con un enlace para update_password: notice: Tu contraseña ha sido actualizada. Prueba a conectarte ahora. forgot_password: @@ -1119,9 +1268,13 @@ es: submit: Guardar la nueva contraseña title: Nueva contraseña mailer: + dateformat: "%d %b" feedback: header: "%{user} escribió %{date}:" + subject: Comentarios para Foodsoft + from_via_foodsoft: "%{name} vía Foodsoft" invite: + subject: Invitación al Foodcoop text: | Hola! @@ -1164,6 +1317,8 @@ es: Queridos miebros de %{ordergroup}, El pedido de "%{order}" ha sido cerrado por %{user} en %{when}. + text1: | + Puede ser posiblemente recogido en %{pickup}. text2: | Los siguientes artículos se han pedido para tu grupo de pedido: text3: |- @@ -1173,6 +1328,18 @@ es: Abrazos %{foodcoop}. + order_received: + subject: 'Envío de pedido registrado: %{name}' + text0: | + Estimado %{ordergroup}, + + el pedido de "%{order}" ha sido recibido. + abundant_articles: Recibido demasiado + scarce_articles: Recibido muy poco + article_details: | + o %{name}: + -- Solicitado: %{ordered} x %{unit} + -- Recibido: %{received} x %{unit} order_result_supplier: subject: Nuevo pedido para %{name} text: | @@ -1187,6 +1354,16 @@ es: %{foodcoop} reset_password: subject: Hay tareas que se deben hacer ya! + text: | + Hola %{user}, + + Has (o alguien más) solicitado una nueva contraseña. + Para elegir una nueva contraseña, siga este enlace: %{link} + Este enlace funciona sólo una vez y expira el %{expires}. + Si no quieres cambiar tu contraseña, simplemente ignora este mensaje. Tu contraseña no ha sido cambiada aún. + + + ¡Saludos, tu equipo de Foodsoft! upcoming_tasks: nextweek: 'Tareas para la semana que viene:' subject: Tareas que hay que hacer ya! @@ -1199,16 +1376,50 @@ es: Saludos de %{foodcoop}. + welcome: + subject: Bienvenido al Foodcoop + text0: | + Estimado %{user}, + + se ha creado una nueva cuenta Foodsoft para ti. + text1: | + Para elegir una nueva contraseña, siga este enlace: %{link} + Este enlace solo funciona una vez y caduca el %{expires}. + Siempre puedes usar "¿Olvidaste la contraseña?" para obtener un nuevo enlace. + + + Saludos de %{foodcoop}. + messages_mailer: + foodsoft_message: + footer: | + Respuesta: %{reply_url} + Ver mensaje en línea: %{msg_url} + Opciones de mensaje: %{profile_url} + footer_group: | + Enviado al grupo: %{group} model: delivery: each_stock_article_must_be_unique: Los artículos de stock no pueden ser listados más de una vez. + financial_transaction: + foodcoop_name: Foodcoop + financial_transaction_type: + no_delete_last: Debe existir al menos un tipo de transacción financiera. + group_order: + stock_ordergroup_name: Existencias (%{user}) + invoice: + invalid_mime: tiene un tipo de MIME inválido (%{mime}) membership: no_admin_delete: No te puedes salir de este grupo porque eres el último adimistrador/a. + order_article: + error_price: debe especificarse y tener un precio actual user: no_ordergroup: no hay célula + group_order_article: + order_closed: El pedido está cerrado y no se puede modificar navigation: admin: config: Configuración + finance: Finanzas home: Resumen mail_delivery_status: Problemas de email ordergroups: Grupos de pedido @@ -1217,12 +1428,14 @@ es: workgroups: grupos de trabajo articles: categories: Categorías + stock: Existencias suppliers: Proveedores/artículos title: Artículos dashboard: Escritorio finances: accounts: Administrar cuentas balancing: Pedidos de cuenta + bank_accounts: Cuentas bancarias home: Resumen invoices: Facturas title: Finanzas @@ -1233,6 +1446,7 @@ es: archive: Mis Pedidos manage: Gestionar pedidos ordering: Hacer pedido! + pickups: Días de recogida title: Pedidos tasks: Tareas workgroups: Grupos de trabajo @@ -1270,6 +1484,7 @@ es: field_unlocked_title: La distribución de este artículo entre los grupos de pedido se ha cambiado a mano. Cuando cambies las cantidades, esos cambios manuales se perderán. edit_amounts: no_articles_available: Ningún artículo para añadir. + set_all_to_zero: Poner todo a cero fax: amount: Cantidad articles: Artículos @@ -1303,7 +1518,9 @@ es: error_closed: El pedido ya estaba cerrado error_nosel: Debes seleccionar al menos un artículo. Quizás quieres borrar el pedido? error_starts_before_boxfill: tiene que ser después de la fecha de comienzo (o estar vacío) + error_starts_before_ends: debe ser después de la fecha de inicio (o permanecer vacío) notice_close: 'Pedido: %{name}, hasta %{ends}' + stock: Existencias warning_ordered: 'Cuidado: los artículos marcados en rojo ya han sido pedidos en este pedido abierto. Si los deseleccionas aquí, todos los pedidos actuales de estos artículos se borrarán. Para proceder, confirma abajo.' warning_ordered_stock: 'Cuidado: Los artículos marcados en rojo ya han sido pedidos en este pedido de stock. Si los deseleccionas aquí, todos los pedidos y compras de estos artículos se borrarán y no estarán en la contabilidad. Para proceder, confirma abajo.' new: @@ -1313,6 +1530,8 @@ es: consider_member_tolerance: considera la tolerancia notice: 'Pedido recibido: %{msg}' notice_none: Ningún nuevo artículo para recibir + paragraph: Si el pedido y el importe recibido son los mismos, los campos correspondientes pueden estar vacíos. Sigue siendo buena práctica entrar en todos los campos, ya que esto proporciona una forma fácil de verificar que todos los artículos han sido seleccionados. + rest_to_stock: restablecer a valores en existencias submit: recibe pedido surplus_options: 'Opciones de distribución:' title: Recibiendo %{order} @@ -1326,8 +1545,15 @@ es: comments: title: Comentarios comments_link: Comentarios + confirm_delete: '¿Estás seguro/a de que quieres borrar el pedido?' + confirm_end: |- + ¿Realmente desea cerrar el pedido %{order}? + No hay vuelta atrás. + confirm_send_to_supplier: El pedido ya ha sido enviado al proveedor el %{when}. ¿Realmente desea enviarlo de nuevo? create_invoice: Añade factura + description1_order: "%{state} pedido de %{supplier} abierto por %{who}" description1_period: + pickup: y puede ser recogido en %{pickup} starts: abierto desde %{starts} starts_ends: abierto desde %{starts} hasta %{ends} description2: "%{ordergroups} ha pedido %{article_count} artículos, con un valor total de %{net_sum} / %{gross_sum} (neto / bruto)." @@ -1353,14 +1579,30 @@ es: notice: Se actualizó el pedido. update_order_amounts: msg1: "%{count} artículos (%{units} units) actualizados" + msg2: "%{count} (%{units}) usando tolerancia" + msg4: "%{count} (%{units}) sobra" + pickups: + document: + empty_selection: Debe seleccionar al menos un pedido. + filename: Recogida para %{date} + invalid_document: Tipo de documento inválido + title: Recogida para %{date} + index: + article_pdf: Artículo PDF + group_pdf: Grupo PDF + matrix_pdf: Matrix PDF + title: Días de recogida sessions: logged_in: '¡Te has conectado!' + logged_out: '¡Te has desconectado!' login_invalid_email: Dirección de email o contraseña no válidas login_invalid_nick: Usuario o contraseña no válidos new: forgot_password: '¿Has olvidado la contraseña?' login: Entra nojs: Atención, las cookies y el javascript deben ser activados! Por favor desactiva %{link}. + noscript: NoScript + title: Inicio de sesión Foodsoft shared: articles: ordered: Pedidos @@ -1389,6 +1631,11 @@ es: order_download_button: article_pdf: Artículos PDF download_file: Descargar archivo + fax_csv: Fax CSV + fax_pdf: Fax PDF + fax_txt: Texto fax + group_pdf: Grupo PDF + matrix_pdf: Matrix PDF title: Descargar task_list: accept_task: Acepta la tarea @@ -1404,9 +1651,13 @@ es: workgroup_members: title: Membresías de grupo simple_form: + error_notification: + default_message: Se han encontrado errores. Por favor, compruebe el formulario. hints: article: unit: por ej. KG o 1L o 500g + article_category: + description: lista separada por comas de nombres de categorías reconocidos en la importación/sincronización order_article: units_to_order: Si cambias la cantidad total de unidades enviadas también tendrás que cambiar los valores individuales de grupo haciendo click en el nombre del artículo. No serán recalculados automáticamente, así que a los otros grupos de pedido se les podrían ser cobrar artículos que no llegarán! update_global_price: Actualizar el precio para futuros pedidos @@ -1426,6 +1677,7 @@ es: notify: negative_balance: Infórmame cuando mi grupo de pedido tenga un balance negativo. order_finished: Infórmame acerca del resultado de mi pedido (cuando se cierre). + order_received: Infórmame sobre los datos de entrega (después de recibir el pedido). upcoming_tasks: Recordarme las tareas incompletas. profile: email_is_public: El email es visible para otros miembros @@ -1435,6 +1687,7 @@ es: settings_group: messages: Mensajes privacy: Privacidad + 'no': 'No' options: settings: profile: @@ -1446,6 +1699,7 @@ es: nl: Neerlandés tr: Turco required: + mark: "*" text: requerido 'yes': 'Sí' stock_takings: @@ -1459,22 +1713,34 @@ es: new: amount: Cantidad create: crea + stock_articles: Artículos en existencias + temp_inventory: inventario temporal + text_deviations: Por favor, rellene todas las desviaciones excedentes del %{inv_link}. Para la reducción, utilice un número negativo. + text_need_articles: Tienes que %{create_link} un nuevo artículo de existencias antes de poder usarlo aquí. + title: Crear nuevo inventario show: amount: Cantidad article: Artículo + confirm_delete: '¿Realmente deseas eliminar el inventario?' date: Fecha note: Nota overview: Inventario supplier: Proveedor title: Muestra inventario unit: Unidad + stock_takings: + confirm_delete: '¿Estás seguro que quieres borrar esto?' + date: Fecha + note: Nota + update: + notice: Inventario actualizado. stockit: check: not_empty: "%{name} no se pudo borrar, el inventario no es cero." copy: title: Copia artículo de stock create: - notice: Se ha creado el nuevo producto en stock "%{name}" + notice: Se ha creado el nuevo producto en stock "%{name}" derive: title: Añade un artículo en stock desde plantilla destroy: @@ -1493,6 +1759,7 @@ es: show_stock_takings: Resumen del inventario stock_count: 'Número de artículos' stock_worth: 'Valor actual del stock:' + title: Existencias (%{article_count}) toggle_unavailable: Muestra/esconde los artículos no disponibles view_options: Ver opciones new: @@ -1500,6 +1767,7 @@ es: title: Añade mi nuevo artículo de stock show: change_quantity: Cambia + datetime: Hora new_quantity: Nueva cantidad reason: Razón stock_changes: Cambio de cantidades en stock @@ -1517,6 +1785,7 @@ es: action_new: Crea un nuevo proveedor/a articles: artículos (%{count}) confirm_del: Estas seguro de que quieres borrar al proveedor %{name}? + deliveries: entregas (%{count}) stock: en stock (%{count}) title: Proveedores new: @@ -1582,9 +1851,10 @@ es: accept_task: Aceptar tarea confirm_delete_group: Estás seguro/a de que quieres borrar esta tarea y todas las tareas subsecuentes? confirm_delete_single: Estás seguro/a de que quieres borrar esta tarea? - confirm_delete_single_from_group: Estás seguro/a de que quieres borrar esta tarea (y mantener las tareas recurrentes relacionadas)? + confirm_delete_single_from_group: Estás seguro/a de que quieres borrar esta tarea (y mantener las tareas recurrentes relacionadas)? delete_group: Borrar esta tarea y las subsecuentes edit_group: Edita recurrencia + hours: "%{count}h" mark_done: Marca tarea como hecha reject_task: Rechaza tarea title: Muestra tarea @@ -1605,19 +1875,34 @@ es: back: Volver cancel: Cancelar close: Cerrar + confirm_delete: '¿Realmente desea eliminar %{name}?' + confirm_restore: '¿Realmente desea restaurar %{name}?' copy: Copia delete: Eliminar download: Descarga edit: Editar + marks: + close: "×" + success: + move: Mover or_cancel: o cancelar please_wait: Espera... restore: Restaura save: Guardar search_placeholder: Busca ... show: Mostrar + views: + pagination: + first: "«" + last: "»" + next: "›" + previous: "‹" + truncate: "..." workgroups: edit: title: Edita grupo de trabajo + error_last_admin_group: El último grupo con derechos de administrador no debe ser eliminado + error_last_admin_role: El rol de administrador del último grupo con derechos de administrador no puede ser retirado index: title: Grupos de trabajo update: diff --git a/config/locales/fr.yml b/config/locales/fr.yml index cd0971da..dd79dab3 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -14,7 +14,7 @@ fr: gross_price: Prix TTC manufacturer: Product-rice-eur name: Nom - order_number: Numéro + order_number: Numéro order_number_short: Numéro origin: Lieu de production price: Prix HT @@ -417,8 +417,8 @@ fr: street: Rue zip_code: Code postal currency_unit: Monnaie - name: Nom disable_members_overview: Désactiver la liste des membres + name: Nom distribution_strategy: Stratégie de distribution distribution_strategy_options: first_order_first_serve: Distribuez d'abord à ceux qui ont commandé en premier @@ -864,7 +864,7 @@ fr: error_invite_invalid: Ton invitation n'est pas ou plus valide. error_token_invalid: Ton jeton de connexion n'est pas ou plus valide, essaie de cliquer à nouveau sur le lien. reset_password: - notice: Tu vas maintenant recevoir un message contenant un lien qui te permettra de réinitialiser ton mot de passe. + notice: Tu vas maintenant recevoir un message contenant un lien qui te permettra de réinitialiser ton mot de passe. update_password: notice: Ton mot de passe a été mis à jour. Tu peux maintenant de connecter. forgot_password: @@ -1097,7 +1097,7 @@ fr: closed: décomptée finished: clôturée open: en cours - received: reçu + received: reçu update: notice: La commande a été mise à jour. update_order_amounts: @@ -1349,7 +1349,7 @@ fr: notice: La description du boulot a été mise à jour. notice_converted: Le boulot a été converti en boulot ordinaire (sans répétition). user: - more: Tu t'ennuies en ce moment? Il y aura sûrement du boulot pour toi %{tasks_link}. + more: Tu t'ennuies en ce moment? Il y aura sûrement du boulot pour toi %{tasks_link}. tasks_link: par là-bas title: Ton boulot title_accepted: Boulots acceptés @@ -1374,7 +1374,7 @@ fr: edit: title: Modifier l'équipe error_last_admin_group: Impossible de supprimer la dernière cellule avec privilèges administratrices. - error_last_admin_role: Les privilèges administratrices ne peuvent pas être retirés à la dernière cellule qui les possède. + error_last_admin_role: Les privilèges administratrices ne peuvent pas être retirés à la dernière cellule qui les possède. index: title: Équipes update: diff --git a/config/locales/nl.yml b/config/locales/nl.yml index d972c088..1faaea62 100644 --- a/config/locales/nl.yml +++ b/config/locales/nl.yml @@ -35,13 +35,19 @@ nl: unit_quantity: Colligrootte bank_account: balance: Tegoed + bank_gateway: Bank gateway description: Omschrijving iban: IBAN name: Naam + bank_gateway: + authorization: Autorisatiekoptekst + name: Naam + unattended_user: Gebruiker zonder toezicht + url: URL bank_transaction: amount: Bedrag date: Datum - external_id: Extern ID + external_id: Externe ID financial_link: Financiële link iban: IBAN reference: Referentie @@ -51,26 +57,27 @@ nl: note: Notitie supplier: Leverancier document: - created_at: Upload op - created_by: Upload door - data: Data + created_at: Aangemaakt op + created_by: Aangemaakt door + data: Gegevens mime: MIME-type name: Naam financial_transaction: amount: Bedrag created_on: Datum - financial_transaction_class: Financiële transactie klasse - financial_transaction_type: Financiële transactie type + financial_transaction_class: Financiële-transactieklasse + financial_transaction_type: Financiële-transactietype note: Notitie ordergroup: Huishouden - user: Ingevuld door + user: Ingevoerd door financial_transaction_class: + ignore_for_account_balance: Negeren voor rekeningsaldo name: Naam financial_transaction_type: bank_account: Bankrekening name: Naam financial_transaction_class: Klasse financiële transactie - name_short: Verkorte naam + name_short: Korte naam group_order: ordergroup: Huishouden price: Totaal bestelling @@ -81,16 +88,16 @@ nl: received: Ontvangen result: Hoeveelheid tolerance: Tolerantie - total_price: Som + total_price: Totaal unit_price: Prijs/Eenheid invoice: amount: Bedrag attachment: Bijlage - created_at: Gemaakt op - created_by: Gemaakt door + created_at: Aangemaakt op + created_by: Aangemaakt door date: Factuurdatum delete_attachment: Bijlage verwijderen - deliveries: Voorraad levering + deliveries: Voorraadlevering deposit: Statiegeld in rekening gebracht deposit_credit: Statiegeld teruggekregen financial_link: Financiële link @@ -151,10 +158,10 @@ nl: contact_address: Adres contact_person: Contactpersoon contact_phone: Telefoon - description: Omschrijving + description: Beschrijving ignore_apple_restriction: Bestelstop vanwege appelpunten negeren last_order: Laatste bestelling - last_user_activity: Laatst actief + last_user_activity: Laatste activiteit name: Naam user_tokens: Leden stock_article: @@ -186,9 +193,14 @@ nl: phone2: Telefoon 2 shared_sync_method: Hoe synchroniseren url: Homepage + supplier_category: + name: Naam + description: Beschrijving + financial_transaction_class: Financiële-transactieklasse + bank_account: Bankrekening task: - created_by: Gemaakt door - created_on: Gemaakt op + created_by: Aangemaakt door + created_on: Aangemaakt op description: Beschrijving done: Gedaan? due_date: Voor wanneer? @@ -201,7 +213,7 @@ nl: email: E-mail first_name: Voornaam iban: IBAN - last_activity: Laatst actief + last_activity: Laatste activiteit last_login: Laatste aanmelding last_name: Achternaam name: Naam @@ -214,13 +226,13 @@ nl: one: Werkgroep other: Werkgroepen workgroup: - description: Omschrijving + description: Beschrijving name: Naam role_admin: Beheer role_article_meta: Artikelen role_finance: Financiën role_invoices: Facturen - role_orders: Bestellingen + role_orders: Beheer bestellingen role_pickups: Ophaaldagen role_suppliers: Leveranciers user_tokens: Leden @@ -243,6 +255,8 @@ nl: models: article: Artikel article_category: Categorie + bank_account: Bankrekening + bank_gateway: Betalingsdienst bank_transaction: Banktransactie delivery: Levering financial_transaction: Financiële transactie @@ -254,10 +268,11 @@ nl: order_comment: Commentaar ordergroup: one: Huishouden - other: Huishouden + other: Huishoudens stock_article: Voorraadartikel stock_taking: Inventaris supplier: Leverancier + supplier_category: Leverancierscategorie task: Taak user: Gebruiker workgroup: Werkgroep @@ -268,7 +283,7 @@ nl: all_ordergroups: Alle huishoudens all_users: Alle gebruikers all_workgroups: Alle werkgroepen - created_at: gemaakt op + created_at: aangemaakt op first_paragraph: Hier kun je de groepen en gebruikers van Foodsoft beheren. groupname: Groepsnaam members: leden @@ -276,16 +291,24 @@ nl: new_ordergroup: Nieuw huishouden new_user: Nieuwe gebruiker new_workgroup: Nieuwe werkgroep - newest_groups: Nieuwste groepen - newest_users: Nieuwste gebruikers - title: Administratie - type: Type - username: Gebruikersnaam + newest_groups: nieuwste groepen + newest_users: nieuwste gebruikers + title: Beheer + type: type + username: gebruikersnaam + bank_accounts: + form: + title_edit: Bankrekening bewerken + title_new: Nieuwe bankrekening toevoegen + bank_gateways: + form: + title_edit: Betalingsdienst bewerken + title_new: Nieuwe betalingsdienst toevoegen configs: list: key: Sleutel title: Configuratielijst - value: Inhoud + value: Waarde show: submit: Opslaan title: Configuratie @@ -297,7 +320,7 @@ nl: schedule_title: Bestelrooster tab_security: default_roles_title: Toegang tot - default_roles_paragraph: "Ieder lid van de foodcoop heeft standaard toegang tot de volgende onderdelen:" + default_roles_paragraph: 'Ieder lid van de foodcoop heeft standaard toegang tot de volgende onderdelen:' tab_tasks: periodic_title: Periodieke taken tabs: @@ -311,8 +334,11 @@ nl: first_paragraph: Hier kunt u de klassen van financiële transacties en de bijbehorende typen financiële transacties beheren. Elke financiële transactie heeft een type, die je bij elke transactie moet selecteren, als je meer dan één type hebt gemaakt. De klassen financiële transacties kunnen worden gebruikt om de types financiële transacties te groeperen en zullen worden weergegeven als extra kolommen in het rekeningoverzicht, als er meer dan één is gecreëerd. new_bank_account: Nieuwe bankrekening toevoegen new_financial_transaction_class: Nieuwe klasse voor financiële transacties toevoegen + new_bank_gateway: Nieuwe betalingsdienst toevoegen title: Financiën transaction_types: Typen financiële transacties + supplier_categories: Leverancierscategorieën + new_supplier_category: Nieuwe leverancierscategorie transaction_types: name: Naam new_financial_transaction_type: Nieuw type financiële transactie toevoegen @@ -411,6 +437,10 @@ nl: workgroups: members: leden name: naam + supplier_categories: + form: + title_new: Leverancierscategorie toevoegen + title_edit: Leverancierscategorie bewerken application: controller: error_authn: Aanmelden vereist! @@ -569,8 +599,8 @@ nl: street: Adres, meestal is dit het aflever- en ophaaladres. currency_space: Spatie toevoegen na valutasymbool. currency_unit: Valutasymbool voor het tonen van prijzen. - custom_css: De layout van deze site kan gewijzigd worden door hier cascading stylesheets (CSS) in te voeren. Laat het leeg voor de standaardstijl. - email_from: Emails zullen lijken verzonden te zijn vanaf dit email adres. Laat het veld leeg om het contactadres van de foodcoop te gebruiken. + custom_css: Om de lay-out van deze site aan te passen, kunt u stijlwijzigingen invoeren met behulp van cascading stylesheets (CSS). Laat leeg voor de standaardstijl. + email_from: Het zal lijken alsof e-mails verzonden zijn vanaf dit e-mailadres. Laat het veld leeg om het contactadres van de foodcoop te gebruiken. email_replyto: Vul dit in als je antwoord op mails van Foodsoft wilt ontvangen op een ander adres dan het bovenstaande. email_sender: Emails worden verzonden vanaf dit emailadres. Om te voorkomen dat emails als spam worden tegengehouden, is het te adviseren het adres van de webserver op te nemen in het SPF record van het email domein. help_url: Documentatie website. @@ -625,13 +655,13 @@ nl: default_role_orders: Bestellingen default_role_pickups: Ophaaldagen default_role_suppliers: Leveranciers - disable_invite: Uitnodigingen deactiveren + disable_invite: Uitnodigingen uitschakelen disable_members_overview: Ledenlijst deactiveren - email_from: From adres - email_replyto: Reply-to adres - email_sender: Sender adres - help_url: Documentatie URL - homepage: Homepage + email_from: Adres afzender + email_replyto: Antwoord-adres + email_sender: Adres afzender + help_url: URL documentatie + homepage: Hoofdpagina ignore_browser_locale: Browsertaal negeren minimum_balance: Minimum tegoed name: Naam @@ -1310,11 +1340,11 @@ nl: order_result_supplier: subject: Nieuwe bestelling voor %{name} text: | - Beste mijnheer/mevrouw, + Goeiedag, - Foodcoop %{foodcoop} wil graag een bestelling plaatsen. + %{foodcoop} wil graag een bestelling plaatsen. - Een PDF en spreadsheet vind u meegestuurd. + Een PDF en spreadsheet vindt u in bijlage. Met vriendelijke groet, %{user} diff --git a/crowdin.yml b/crowdin.yml index 5e81eec5..f939ea23 100644 --- a/crowdin.yml +++ b/crowdin.yml @@ -1,5 +1,7 @@ project_id: 357447 api_token_env: CROWDIN_API_KEY +preserve_hierarchy: true +base_path: "." files: - source: /config/locales/en.yml translation: /config/locales/%two_letters_code%.yml diff --git a/plugins/current_orders/config/locales/de.yml b/plugins/current_orders/config/locales/de.yml index 4df36568..3e1cfeb8 100644 --- a/plugins/current_orders/config/locales/de.yml +++ b/plugins/current_orders/config/locales/de.yml @@ -10,6 +10,7 @@ de: counts: '%{ordergroups} Bestellgruppen bestellten %{articles} verschiedene Produkte.' no_selection: Wähle einen Artikel, um anzuzeigen wer ihn bestellt hat oder downloade eine Abholliste rechts. article_info: + origin_in: in %{origin} supplied_by: von %{supplier} supplied_and_made_by: produziert von %{manufacturer} supplied_by_made_by: von %{supplier} produziert von %{manufacturer} @@ -24,6 +25,8 @@ de: piece: St. unit: Einheit add_new: Bestellgruppe hinzufügen + show: + title: ! '%{name}' navigation: receive: In Empfang nehmen articles: Verteilen @@ -41,6 +44,7 @@ de: title: Artikel für Bestellgruppe payment_bar: account_balance: Kontostand + new_pin: PIN new_transaction: Neue Transaktion payment: ! 'Zahlung:' show: diff --git a/plugins/current_orders/config/locales/es.yml b/plugins/current_orders/config/locales/es.yml index 04b672b2..3ff1468a 100644 --- a/plugins/current_orders/config/locales/es.yml +++ b/plugins/current_orders/config/locales/es.yml @@ -1,5 +1,7 @@ es: config: + hints: + use_current_orders: Activar el plugin de current_orders. Permite a los miembros con el permiso de la orden cambiar la cantidad de miembros en múltiples pedidos, usando tres nuevas pantallas en el menú de pedidos. Especialmente útil para los días de recogida. keys: use_current_orders: Pantallas extra de distribución current_orders: @@ -23,6 +25,8 @@ es: piece: pieza unit: unidad add_new: Añade un grupo de pedido... + show: + title: ! '%{name}' navigation: receive: Recibe articles: Distribuye @@ -40,6 +44,7 @@ es: title: Artículos por grupo de pedido payment_bar: account_balance: Balance de cuenta + new_pin: PIN new_transaction: Nueva transacción payment: ! 'Pago:' show: diff --git a/plugins/discourse/config/locales/es.yml b/plugins/discourse/config/locales/es.yml index f1e055d2..0da76a94 100644 --- a/plugins/discourse/config/locales/es.yml +++ b/plugins/discourse/config/locales/es.yml @@ -1,5 +1,6 @@ es: discourse: callback: + invalid_nonce: Nonce inválido invalid_signature: Firma inválida logged_in: Estás adentro! diff --git a/plugins/documents/config/locales/de.yml b/plugins/documents/config/locales/de.yml index 39bbcb6f..be56700d 100644 --- a/plugins/documents/config/locales/de.yml +++ b/plugins/documents/config/locales/de.yml @@ -6,6 +6,7 @@ de: created_by: Erstellt von data: Daten mime: MIME-Typ + name: Name config: hints: documents_allowed_extension: Eine Liste an erlaubten Dateiendungen getrennt durch Leerzeichen. diff --git a/plugins/documents/config/locales/es.yml b/plugins/documents/config/locales/es.yml index fe092fdc..02103cc6 100644 --- a/plugins/documents/config/locales/es.yml +++ b/plugins/documents/config/locales/es.yml @@ -19,6 +19,7 @@ es: documents: create: error: 'El documento no puede ser creado: %{error}' + not_allowed_mime: El tipo de archivo "%{mime}" no está permitido. Póngase en contacto con un administrador/a para incluirlo en la lista blanca. notice: Se ha creado el documento destroy: error: 'El documento no puede ser borrado: %{error}' @@ -26,6 +27,14 @@ es: notice: Se ha borrado el documento form: new: Nuevo Documento + new_folder: Carpeta nueva + submit: Crear index: new: Sube nuevo documento + new_folder: Crear una nueva carpeta title: Documentos + move: + root_folder: Iniciar + title: Mover + update: + notice: Documento o carpeta fue movido diff --git a/plugins/messages/config/locales/de.yml b/plugins/messages/config/locales/de.yml index eb8cff21..2f423dc1 100644 --- a/plugins/messages/config/locales/de.yml +++ b/plugins/messages/config/locales/de.yml @@ -20,6 +20,7 @@ de: workgroup_id: Arbeitsgruppe messagegroup: description: Beschreibung + name: Name user_tokens: Mitglieder models: message: Nachricht @@ -84,6 +85,7 @@ de: model: reply_header: ! '%{user} schrieb am %{when}:' reply_indent: ! '> %{line}' + reply_subject: ! 'Re: %{subject}' new: error_private: Nachricht ist privat! hint_private: Nachricht erscheint nicht im Foodsoft Posteingang diff --git a/plugins/messages/config/locales/es.yml b/plugins/messages/config/locales/es.yml index 3b505e79..81a9a206 100644 --- a/plugins/messages/config/locales/es.yml +++ b/plugins/messages/config/locales/es.yml @@ -59,6 +59,7 @@ es: write_message: Escribir un mensaje messagegroups: index: + body: 'Un grupo de mensajes es como una lista de correo: puedes unirte (o salir) a cualquiera de ellos para recibir las actualizaciones enviadas a ese grupo.' title: Grupos de mensaje join: error: 'No pudo unirse al grupo de mensaje: %{error}' @@ -72,27 +73,38 @@ es: messages: actionbar: message_threads: Muestra como hilos + messagegroups: Suscribirse a este grupo messages: Muestra como lista new: Nuevo mensaje + create: + notice: El mensaje ha sido guardado y será enviado. index: title: Mensajes messages: reply: Responde + model: + reply_header: ! '%{user} escribió en %{when}:' + reply_indent: ! '> %{line}' + reply_subject: ! 'Re: %{subject}' new: error_private: Lo siento, este mensaje es privado. + hint_private: El mensaje no se muestra en el buzón de correo Foodsoft list: desc: ! 'Envía mensajes a todos los miembros a través de la lista de correo: %{list}' mail: por ejemplo con un email a %{email}. subscribe: Puedes leer más sobre la lista de correos en %{link}. subscribe_msg: Quizás tengas que suscribirte a la lista primero. + wiki: Wiki (lista de correo de páginas) message: mensaje no_user_found: No se ha encontrado el usuario + order_item: "%{supplier_name} (Recoger: %{pickup})" reply_to: Este mensaje es una respuesta a otro %{link}. search: Busca ... search_user: Busca usuario title: Nuevo mensaje show: all_messages: Todos los mensajes + change_visibility: 'Cambiar' from: ! 'De:' group: 'Grupo:' reply: Responde @@ -101,19 +113,35 @@ es: subject: ! 'Asunto:' title: Muestra mensaje to: 'A:' + visibility: 'Visibilidad:' + visibility_private: 'Privado' + visibility_public: 'Público' thread: all_message_threads: Todos los hilos de mensaje reply: Responde + toggle_private: + not_allowed: No puede cambiar la visibilidad del mensaje. message_threads: groupmessage_threads: show_message_threads: muestra todos index: + general: General title: Hilos de mensaje message_threads: last_reply_at: Última respuesta el - last_reply_by: Última respuesta de + last_reply_by: Última respuesta de started_at: Comenzado el started_by: Comenzado por + show: + general: General + messages_mailer: + foodsoft_message: + footer: | + Respuesta: %{reply_url} + Ver mensaje en línea: %{msg_url} + Opciones de mensaje: %{profile_url} + footer_group: | + Enviado al grupo: %{group} navigation: admin: messagegroups: Grupos de mensaje diff --git a/plugins/wiki/config/locales/de.yml b/plugins/wiki/config/locales/de.yml index cb02e461..dfdf8da4 100644 --- a/plugins/wiki/config/locales/de.yml +++ b/plugins/wiki/config/locales/de.yml @@ -17,6 +17,7 @@ de: wiki: all_pages: Alle Seiten home: Startseite + title: Wiki pages: all: new_page: Neue Seite anlegen @@ -67,6 +68,7 @@ de: section_table: Tabellenformatierung see_tables: siehe %{tables_link} tables_link: Tabellen + text: Text title: Schnelle Formatierungshilfe unordered_list: Listen mit Punkten wiki_link_ex: Foodsoft Wiki Seite @@ -94,9 +96,11 @@ de: description: Variablen geben Informationen wieder, die an anderer Stelle definiert wurden. Wenn Du die Variable benutzt, wird sie mit ihrem Wert ersetzt, wenn sie angezeigt wird. Foodsoft hat eine Reihe von vordefinierten Variablen, wie zum Beispiel Name und Adresse deiner Foodcoop, Softwareversion und die Anzahl der Mitglieder und Lieferanten. In der Tabelle unten sind alle Variablen aufgeführt. Du kannst diese in Wiki-Seiten und der Fusszeile (in den Einstellungen) nutzen. title: Foodsoft-Variablen value: Aktueller Wert + variable: Variable version: author: ! 'Autor: %{user}' date_format: ! '%a, %d.%m.%Y, %H:%M Uhr' revert: Auf diese Version zurücksetzen title: ! '%{title} - Version %{version}' + title_version: Version view_current: Aktuelle Version sehen diff --git a/plugins/wiki/config/locales/es.yml b/plugins/wiki/config/locales/es.yml index 71761e80..a7391e94 100644 --- a/plugins/wiki/config/locales/es.yml +++ b/plugins/wiki/config/locales/es.yml @@ -16,24 +16,36 @@ es: navigation: wiki: all_pages: Todas las páginas + home: Inicio + title: Wiki pages: all: new_page: Crea nueva página recent_changes: Cambios recientes search: action: Busca + placeholder: Título de página .. + site_map: Mapa web + title: Todas las páginas Wiki title_list: Lista de páginas body: title_toc: Contenido + wikicloth_exception: 'Sentimos informar de que ha ocurrido un error al interpretar la página wiki: %{msg}. Por favor, intenta arreglarla y guarda la página de nuevo.' create: notice: La página ha sido creada cshow: error_noexist: La página no existe + redirect_notice: Redirigido desde %{page}... + destroy: + notice: La página '%{page}' y todas las subpáginas se han eliminado correctamente. + diff: + title: "%{title} - cambios de versión %{old} a %{new}" edit: title: Editar página error_stale_object: Cuidado, la página ha sido editada por otra persona. Por favor, intenta de nuevo. form: help: + bold: negrita external_link_ex: Enlace externo external_links: Externo heading: nivel %{level} @@ -41,18 +53,54 @@ es: image_link_title: Título de la imagen image_links: Imágenes italic: itálicas + link_lists: Más en listas + link_table: Formato de tabla + link_templates: Plantillas + link_variables: Variables Foodsoft + list_item_1: Primer elemento de lista + list_item_2: Segundo elemento de lista + noformat: Sin Formato ordered_list: Lista numerada section_block: Estilo del párrafo section_character: Estilo de las letras section_link: Estilo de los enlaces section_more: Más temas + section_table: Formato de tabla + see_tables: ver %{tables_link} + tables_link: Tablas + text: texto + title: Ayuda de formato rápido + unordered_list: Lista de artículos + wiki_link_ex: Página Wiki de Foodsoft + wiki_links: Enlaces Wiki preview: Previsualizar + last_updated: Última actualización new: title: Crear una nueva página en la wiki + page_list_item: + date_format: ! '%a, %d %B %Y %H:%M:%S' show: + date_format: ! '%d-%m-%y %H:%M' delete: Eliminar página + delete_confirm: ! 'Advertencia: todas las subpáginas serán eliminadas también. ¿Estás seguro?' + diff: Comparar versiones + edit: Editar página + last_updated: Última actualización por %{user} el %{when} + subpages: subpáginas + title_versions: Versiones + versions: Versiones (%{count}) title: Título + update: + notice: La página fue actualizada + variables: + description: Las variables devuelven información de otro lugar. Cuando utilice la variable, se reemplazará por su valor cuando se muestre. Foodsoft tiene un número de variables predefinidas, como el nombre y la dirección de tu cooperativa, la versión del software, y el número de miembros y proveedores. Vea la siguiente tabla para todas las variables. Puede usarlas en páginas wiki así como en el pie de página (desde la pantalla de configuración). + title: Variables Foodsoft + value: Valor actual + variable: Variable version: author: ! 'Autor/a: %{user}' + date_format: ! '%a, %d-%m-%Y, %H:%M' + revert: Revertir a esta versión + title: ! '%{title} - versión %{version}' title_version: Versión view_current: Ver versión actual From e1b582483038c9357886a0979c505377b2c27a7c Mon Sep 17 00:00:00 2001 From: Philipp Rothmann Date: Thu, 13 Jul 2023 19:00:30 +0200 Subject: [PATCH 095/105] update changelog v4.8 --- CHANGELOG.md | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f88a1d03..8474e7fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,36 @@ +# Foodsoft 4.8.0 + +* feat: Show total sums for ordergroup finances [#1017](https://github.com/foodcoops/foodsoft/pull/1017) +* feat: Richtext Messages and Attachments with Actiontext [#918](https://github.com/foodcoops/foodsoft/issues/918) +* feat: Make date configurable via locales [#997](https://github.com/foodcoops/foodsoft/pull/997) +* feat: Turkish language support added [#995](https://github.com/foodcoops/foodsoft/pull/995) +* feat: Disable member list via configuration [#990](https://github.com/foodcoops/foodsoft/pull/990) +* feat: Specify an URL to redirect after logout via settings #989 +* feat: introduce importmaps [#983](https://github.com/foodcoops/foodsoft/pull/983) +* feat: ruby 2.7.2 and rails 7 upgrade [#979](https://github.com/foodcoops/foodsoft/pull/979) +* feat: Add home controller test [#972](https://github.com/foodcoops/foodsoft/pull/972) +* feat: Replace apivore with rswag for api tests [#969](https://github.com/foodcoops/foodsoft/pull/969) +* feat: increase test coverage [#966](https://github.com/foodcoops/foodsoft/pull/966) +* feat: Show order note as tooltip [#965](https://github.com/foodcoops/foodsoft/pull/965) +* feat: Add sd_notify [#961](https://github.com/foodcoops/foodsoft/pull/961) +* feat: Show instance name at login screen [#957](https://github.com/foodcoops/foodsoft/pull/957) +* feat: Enabled systemd socket activation [#942](https://github.com/foodcoops/foodsoft/pull/942) +* feat: Add table_print gem for debugging ActiveRecord queries in the console [#935](https://github.com/foodcoops/foodsoft/pull/935) +* feat: Add admin UI for SupplierCategories (supplier_categories) [#930](https://github.com/foodcoops/foodsoft/pull/930) + +* fix: add null checks for articles convert_units [33034e6](https://github.com/foodcoops/foodsoft/commit/33034e66b88968dedc5289425e1eff847ee67e12) +* fix: downgrade haml to make deface work [#1003](https://github.com/foodcoops/foodsoft/pull/1003) +* fix: dutch translation errors [#954](https://github.com/foodcoops/foodsoft/pull/954) +* fix: Fixe filtering of active ordergroups [#934](https://github.com/foodcoops/foodsoft/pull/934) +* fix: Change password validation to allow longer passwords [#923](https://github.com/foodcoops/foodsoft/pull/923) +* fix: Invoice: change label "delivery" to "stock delivery" [#922](https://github.com/foodcoops/foodsoft/pull/922) +* fix: Allow decimal numbers in transaction collections [#921](https://github.com/foodcoops/foodsoft/pull/921) +* fix: Add validation of more article fields [#917](https://github.com/foodcoops/foodsoft/pull/917/files) +* fix: Add default time_zone [#912](https://github.com/foodcoops/foodsoft/pull/912) +* fix: Rename Piwik to Matomo [#911](https://github.com/foodcoops/foodsoft/pull/911/files) +* fix: Change instructions to rbenv [#910](https://github.com/foodcoops/foodsoft/pull/910/files) + + # Foodsoft 4.7.1 (31 December 2020) From e194c683978dd783972ef75b45319a6bbf7ad62b Mon Sep 17 00:00:00 2001 From: Philipp Rothmann Date: Sat, 9 Sep 2023 10:49:15 +0200 Subject: [PATCH 096/105] chore: bump version to 4.8.0 --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 3d512a04..88f18119 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -4.7.99 +4.8.0 From 55234b4e271814827c8084a84814ffec0f0f4a43 Mon Sep 17 00:00:00 2001 From: Philipp Rothmann Date: Sat, 9 Sep 2023 17:01:48 +0200 Subject: [PATCH 097/105] continue development after release --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 88f18119..5003783a 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -4.8.0 +4.8.99 From 6abf998b563bfdc8f8fa63db01e8bb78b13fc291 Mon Sep 17 00:00:00 2001 From: Philipp Rothmann Date: Mon, 2 Oct 2023 22:48:24 +0200 Subject: [PATCH 098/105] fix: documents sort sql needs Arel.sql --- plugins/documents/app/controllers/documents_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/documents/app/controllers/documents_controller.rb b/plugins/documents/app/controllers/documents_controller.rb index 7290ef3c..8950f347 100644 --- a/plugins/documents/app/controllers/documents_controller.rb +++ b/plugins/documents/app/controllers/documents_controller.rb @@ -14,7 +14,7 @@ class DocumentsController < ApplicationController else 'data IS NULL DESC, name' end - + sort = Arel.sql(sort) # this is okay as we don't use user params directly @documents = Document.where(parent: @document).page(params[:page]).per(@per_page).order(sort) end From 93143c28f255f4b18a3a5001e2583fb50e08e19b Mon Sep 17 00:00:00 2001 From: Philipp Rothmann Date: Mon, 24 Jul 2023 10:50:35 +0200 Subject: [PATCH 099/105] merge automatic group order invoice generation see https://github.com/foodcoops/foodsoft/pull/907 for reference and original work by viehlieb Co-authored-by: viehlieb fix PDF Pdf make explicit deposit in invoices work add ordergroupname to invoice file name mark bold sum for vat exempt foodcoops download multiple group order invoice as zip --- .rubocop_todo.yml | 3 + .../bootstrap_and_overrides.css.less | 9 +- .../concerns/send_group_order_invoice_pdf.rb | 17 ++ .../finance/balancing_controller.rb | 23 +- .../group_order_invoices_controller.rb | 87 ++++++ app/documents/group_order_invoice_pdf.rb | 264 ++++++++++++++++++ app/jobs/notify_group_order_invoice_job.rb | 10 + app/lib/render_pdf.rb | 21 +- app/mailers/mailer.rb | 17 ++ app/models/concerns/price_calculation.rb | 16 ++ app/models/group_order.rb | 1 + app/models/group_order_article.rb | 12 + app/models/group_order_invoice.rb | 58 ++++ app/models/order.rb | 14 +- .../admin/configs/_tab_foodcoop.html.haml | 1 + .../admin/configs/_tab_payment.html.haml | 6 + app/views/admin/ordergroups/_form.html.haml | 1 + app/views/finance/balancing/_orders.html.haml | 10 + app/views/finance/balancing/_summary.haml | 10 + app/views/finance/balancing/index.html.haml | 1 - .../group_order_invoices/_links.html.haml | 29 ++ app/views/group_order_invoices/create.js.erb | 1 + .../create_multiple.js.erb | 1 + app/views/group_order_invoices/destroy.js.erb | 1 + app/views/group_orders/_form.html.haml | 4 +- .../mailer/group_order_invoice.text.haml | 1 + app/views/ordergroups/edit.html.haml | 4 + app/views/shared/_group.html.haml | 2 + config/locales/de.yml | 82 ++++++ config/locales/en.yml | 79 ++++++ config/routes.rb | 7 + ...11208142719_create_group_order_invoices.rb | 13 + ...0822120005_add_customer_number_to_group.rb | 5 + db/schema.rb | 109 ++++---- spec/factories/group_order_invoice.rb | 7 + spec/integration/group_order_invoices_spec.rb | 72 +++++ spec/models/group_order_invoice_spec.rb | 59 ++++ 37 files changed, 988 insertions(+), 69 deletions(-) create mode 100644 app/controllers/concerns/send_group_order_invoice_pdf.rb create mode 100644 app/controllers/group_order_invoices_controller.rb create mode 100644 app/documents/group_order_invoice_pdf.rb create mode 100644 app/jobs/notify_group_order_invoice_job.rb create mode 100644 app/models/group_order_invoice.rb create mode 100644 app/views/group_order_invoices/_links.html.haml create mode 100644 app/views/group_order_invoices/create.js.erb create mode 100644 app/views/group_order_invoices/create_multiple.js.erb create mode 100644 app/views/group_order_invoices/destroy.js.erb create mode 100644 app/views/mailer/group_order_invoice.text.haml create mode 100644 db/migrate/20211208142719_create_group_order_invoices.rb create mode 100644 db/migrate/20230822120005_add_customer_number_to_group.rb create mode 100644 spec/factories/group_order_invoice.rb create mode 100644 spec/integration/group_order_invoices_spec.rb create mode 100644 spec/models/group_order_invoice_spec.rb diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index cbbec263..5276a743 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -274,6 +274,8 @@ Lint/Void: # Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes. Metrics/AbcSize: Max: 143 + Exclude: + - 'app/documents/group_order_invoice_pdf.rb' # Offense count: 13 # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns, inherit_mode. @@ -407,6 +409,7 @@ RSpec/Capybara/FeatureMethods: - 'spec/integration/receive_spec.rb' - 'spec/integration/session_spec.rb' - 'spec/integration/supplier_spec.rb' + - 'spec/integration/group_order_invoices_spec.rb' # Offense count: 4 RSpec/Capybara/SpecificMatcher: diff --git a/app/assets/stylesheets/bootstrap_and_overrides.css.less b/app/assets/stylesheets/bootstrap_and_overrides.css.less index 971308c9..ebd30b20 100644 --- a/app/assets/stylesheets/bootstrap_and_overrides.css.less +++ b/app/assets/stylesheets/bootstrap_and_overrides.css.less @@ -241,6 +241,9 @@ table { tr.order-article:hover .article-info { display: none; } + tr.order-article:focus .article-info { + display: none; + } } #order-footer { @@ -275,11 +278,13 @@ tr.order-article .article-info { display: none; } -tr.order-article:hover .article-info { +tr.order-article:focus{ + background-color: #E4EED6; +} +tr.order-article:focus .article-info { display: block; } - // ********* Articles tr.just-updated { diff --git a/app/controllers/concerns/send_group_order_invoice_pdf.rb b/app/controllers/concerns/send_group_order_invoice_pdf.rb new file mode 100644 index 00000000..76e71c99 --- /dev/null +++ b/app/controllers/concerns/send_group_order_invoice_pdf.rb @@ -0,0 +1,17 @@ +module Concerns::SendGroupOrderInvoicePdf + extend ActiveSupport::Concern + + protected + + def create_invoice_pdf(group_order_invoice) + invoice_data = group_order_invoice.load_data_for_invoice + invoice_data[:title] = t('documents.group_order_invoice_pdf.title', supplier: invoice_data[:supplier]) + invoice_data[:no_footer] = true + GroupOrderInvoicePdf.new invoice_data + end + + def send_group_order_invoice_pdf(group_order_invoice) + pdf = create_invoice_pdf(group_order_invoice) + send_data pdf.to_pdf, filename: pdf.filename, type: 'application/pdf' + end +end diff --git a/app/controllers/finance/balancing_controller.rb b/app/controllers/finance/balancing_controller.rb index e1a2dafb..29320c64 100644 --- a/app/controllers/finance/balancing_controller.rb +++ b/app/controllers/finance/balancing_controller.rb @@ -5,7 +5,7 @@ class Finance::BalancingController < Finance::BaseController def new @order = Order.find(params[:order_id]) - flash.now.alert = t('.alert') if @order.closed? + flash.now.alert = t('finance.balancing.new.alert') if @order.closed? && flash[:alert].blank? @comments = @order.comments @articles = @order.order_articles.ordered_or_member.includes(:article, :article_price, @@ -81,9 +81,24 @@ class Finance::BalancingController < Finance::BaseController @order = Order.find(params[:id]) @type = FinancialTransactionType.find_by_id(params.permit(:type)[:type]) @order.close!(@current_user, @type) - redirect_to finance_order_index_url, notice: t('.notice') - rescue StandardError => e - redirect_to new_finance_order_url(order_id: @order.id), alert: t('.alert', message: e.message) + note = t('finance.balancing.close.notice') + if @order.closed? + alert = t('finance.balancing.close.alert') + if FoodsoftConfig[:group_order_invoices]&.[](:use_automatic_invoices) + @order.group_orders.each do |go| + alert = t('finance.balancing.close.settings_not_set') + goi = GroupOrderInvoice.find_or_create_by!(group_order_id: go.id) + if goi.save! + NotifyGroupOrderInvoiceJob.perform_later(goi) + note = t('finance.balancing.close.notice_mail') + end + end + end + end + alert ||= t('finance.balancing.close.alert') + redirect_to finance_order_index_url, notice: note + rescue => error + redirect_to new_finance_order_url(order_id: @order.id), notice: note, alert: alert, msg: error.message end # Close the order directly, without automaticly updating ordergroups account balances diff --git a/app/controllers/group_order_invoices_controller.rb b/app/controllers/group_order_invoices_controller.rb new file mode 100644 index 00000000..2e5a8408 --- /dev/null +++ b/app/controllers/group_order_invoices_controller.rb @@ -0,0 +1,87 @@ +class GroupOrderInvoicesController < ApplicationController + include Concerns::SendGroupOrderInvoicePdf + before_action :authenticate_finance + + def show + begin + @group_order_invoice = GroupOrderInvoice.find(params[:id]) + if FoodsoftConfig[:contact][:tax_number] + respond_to do |format| + format.pdf do + send_group_order_invoice_pdf @group_order_invoice if FoodsoftConfig[:contact][:tax_number] + end + end + else + raise RecordInvalid + end + rescue ActiveRecord::RecordInvalid => error + redirect_back fallback_location: root_path, notice: 'Something went wrong', alert: I18n.t('errors.general_msg', msg: "#{error} " + I18n.t('errors.check_tax_number')) + end + end + + def destroy + goi = GroupOrderInvoice.find(params[:id]) + @order = goi.group_order.order + goi.destroy + respond_to do |format| + format.js + format.json { head :no_content } + end + end + + def create_multiple + invoice_date = params[:group_order_invoice][:invoice_date] + order_id = params[:group_order_invoice][:order_id] + @order = Order.find(order_id) + gos = GroupOrder.where("order_id = ?", order_id) + gos.each do |go| + goi = GroupOrderInvoice.find_or_create_by!(group_order_id: go.id) + goi.invoice_date = invoice_date + goi.invoice_number = goi.generate_invoice_number(1) + goi.save! + end + respond_to do |format| + format.js + end + end + + def create + go = GroupOrder.find(params[:group_order]) + @order = go.order + GroupOrderInvoice.find_or_create_by!(group_order_id: go.id) + respond_to do |format| + format.js + end + redirect_back fallback_location: root_path + rescue => error + redirect_back fallback_location: root_path, notice: 'Something went wrong', :alert => I18n.t('errors.general_msg', :msg => error) + end + + def download_all + order = Order.find(params[:order_id]) + + invoices = order.group_orders.map(&:group_order_invoice) + pdf = {} + + temp_file = Tempfile.new("all_invoices_for_order_#{order.id}.zip") + + Zip::File.open(temp_file.path, Zip::File::CREATE) do |zipfile| + invoices.each do |invoice| + pdf = create_invoice_pdf(invoice) + invoice_file = Tempfile.new("#{pdf.filename}") + File.open(invoice_file.path, 'w:ASCII-8BIT') do |file| + file.write(pdf.to_pdf) + end + zipfile.add("#{pdf.filename}", invoice_file.path) unless zipfile.find_entry("#{pdf.filename}") + end + end + + zip_data = File.read(temp_file.path) + + respond_to do |format| + format.html { + send_data(zip_data, type: 'application/zip', filename: "#{l order.ends, format: :file}-#{order.supplier.name}-#{order.id}.zip", disposition: 'attachment') + } + end + end +end diff --git a/app/documents/group_order_invoice_pdf.rb b/app/documents/group_order_invoice_pdf.rb new file mode 100644 index 00000000..899d6cf8 --- /dev/null +++ b/app/documents/group_order_invoice_pdf.rb @@ -0,0 +1,264 @@ +class GroupOrderInvoicePdf < RenderPdf + def filename + ordergroup_name = @options[:ordergroup].name || "OrderGroup" + "#{ordergroup_name}_" + I18n.t('documents.group_order_invoice_pdf.filename', :number => @options[:invoice_number]) + '.pdf' + end + + def title + I18n.t('documents.group_order_invoice_pdf.title', :supplier => @options[:supplier]) + end + + def body + contact = FoodsoftConfig[:contact].symbolize_keys + ordergroup = @options[:ordergroup] + + # From paragraph + bounding_box [margin_box.right - 200, margin_box.top - 20], width: 200 do + text I18n.t('documents.group_order_invoice_pdf.invoicer') + move_down 7 + text FoodsoftConfig[:name], size: fontsize(9), align: :left + move_down 5 + text contact[:street], size: fontsize(9), align: :left + move_down 5 + text "#{contact[:zip_code]} #{contact[:city]}", size: fontsize(9), align: :left + move_down 5 + if contact[:phone].present? + text "#{Supplier.human_attribute_name :phone}: #{contact[:phone]}", size: fontsize(9), align: :left + move_down 5 + end + text "#{Supplier.human_attribute_name :email}: #{contact[:email]}", size: fontsize(9), align: :left if contact[:email].present? + move_down 5 + text I18n.t('documents.group_order_invoice_pdf.tax_number', :number => @options[:tax_number]), size: fontsize(9), align: :left + end + + # Receiving Ordergroup + bounding_box [margin_box.left, margin_box.top - 20], width: 200 do + text I18n.t('documents.group_order_invoice_pdf.invoicee') + move_down 7 + text I18n.t('documents.group_order_invoice_pdf.ordergroup.name', ordergroup: ordergroup.name.to_s), size: fontsize(9) + move_down 5 + if ordergroup.contact_address.present? + text I18n.t('documents.group_order_invoice_pdf.ordergroup.contact_address', contact_address: ordergroup.contact_address.to_s), size: fontsize(9) + move_down 5 + end + if ordergroup.contact_phone.present? + text I18n.t('documents.group_order_invoice_pdf.ordergroup.contact_phone', contact_phone: ordergroup.contact_phone.to_s), size: fontsize(9) + move_down 5 + end + if ordergroup.customer_number.present? + text I18n.t('documents.group_order_invoice_pdf.ordergroup.customer_number', customer_number: ordergroup.customer_number.to_s), size: fontsize(9) + move_down 5 + end + end + + # invoice Date and nnvoice number + bounding_box [margin_box.right - 200, margin_box.top - 150], width: 200 do + text I18n.t('documents.group_order_invoice_pdf.invoice_date', invoice_date: @options[:invoice_date].strftime(I18n.t('date.formats.default'))), align: :left + move_down 5 + text I18n.t('documents.group_order_invoice_pdf.invoice_number', invoice_number: @options[:invoice_number]), align: :left + end + + move_down 15 + + # kind of the "body" of the invoice + text I18n.t('documents.group_order_invoice_pdf.payment_method', payment_method: @options[:payment_method]) + move_down 15 + text I18n.t('documents.group_order_invoice_pdf.table_headline') + move_down 5 + + #------------- Table Data ----------------------- + + @group_order = GroupOrder.find(@options[:group_order].id) + + if FoodsoftConfig[:group_order_invoices][:vat_exempt] + body_for_vat_exempt + else + body_with_vat + end + end + + def body_for_vat_exempt + total_gross = 0 + data = [I18n.t('documents.group_order_invoice_pdf.vat_exempt_rows')] + move_down 10 + group_order_articles = GroupOrderArticle.where(group_order_id: @group_order.id) + separate_deposits = FoodsoftConfig[:group_order_invoices]&.[](:separate_deposits) + group_order_articles.each do |goa| + # if no unit is received, nothing is to be charged + next if goa.result.to_i == 0 + + goa_total_price = separate_deposits ? goa.total_price_without_deposit : goa.total_price + data << [goa.order_article.article.name, + goa.result.to_i, + number_to_currency(goa.order_article.price.fc_price_without_deposit), + number_to_currency(goa_total_price)] + total_gross += goa_total_price + next unless separate_deposits && goa.order_article.price.deposit > 0.0 + + goa_total_deposit = goa.result * goa.order_article.price.fc_deposit_price + data << ["zzgl. Pfand", + goa.result.to_i, + number_to_currency(goa.order_article.article.fc_deposit_price), + number_to_currency(goa_total_deposit)] + total_gross += goa_total_deposit + end + + table data, cell_style: { size: fontsize(8), overflow: :shrink_to_fit } do |table| + table.header = true + table.position = :center + table.cells.border_width = 1 + table.cells.border_color = '666666' + + table.row(0).column(0..4).width = 80 + table.row(0).border_bottom_width = 2 + table.columns(1).align = :right + table.columns(1..6).align = :right + end + + move_down 5 + sum = [] + sum << [nil, nil, I18n.t('documents.group_order_invoice_pdf.sum_to_pay_gross'), number_to_currency(total_gross)] + # table for sum + table sum, cell_style: { size: fontsize(8), overflow: :shrink_to_fit } do |table| + table.header = true + table.position = :center + table.cells.border_width = 1 + table.cells.border_color = '666666' + table.row(0).columns(2..4).style(align: :bottom) + table.row(0).border_bottom_width = 2 + table.row(0..-1).columns(0..1).border_width = 0 + + table.rows(0..-1).columns(0..4).width = 80 + table.row(0).column(-1).style(font_style: :bold) + table.row(0).column(-2).style(font_style: :bold) + table.row(0).column(-1).size = fontsize(10) + table.row(0).column(-2).size = fontsize(10) + + table.columns(1).align = :right + table.columns(1..6).align = :right + end + + move_down 25 + text I18n.t('documents.group_order_invoice_pdf.small_business_regulation') + move_down 10 + end + + def body_with_vat + separate_deposits = FoodsoftConfig[:group_order_invoices]&.[](:separate_deposits) + total_gross = 0 + total_net = 0 + # Articles + + tax_hash_net = Hash.new(0) # for summing up article net prices grouped into vat percentage + tax_hash_gross = Hash.new(0) # same here with gross prices + + if separate_deposits + total_deposit = 0 + total_deposit_gross = 0 + + tax_hash_deposit_gross = Hash.new(0) # for summing up deposit gross prices grouped into vat percentage + tax_hash_deposit_net = Hash.new(0) # same here with gross prices + end + + marge = FoodsoftConfig[:price_markup] + + # data table looks different when price_markup > 0 + data = if marge == 0 + [I18n.t('documents.group_order_invoice_pdf.no_price_markup_rows')] + else + [I18n.t('documents.group_order_invoice_pdf.price_markup_rows', marge: marge)] + end + goa_tax_hash = GroupOrderArticle.where(group_order_id: @group_order.id).find_each.group_by { |oat| oat.order_article.price.tax } + goa_tax_hash.each do |tax, group_order_articles| + group_order_articles.each do |goa| + # if no unit is received, nothing is to be charged + next if goa.result.to_i == 0 + + order_article = goa.order_article + goa_total_net = goa.result * order_article.price.price + + goa_total_gross = separate_deposits ? goa.total_price_without_deposit : goa.total_price + + data << [order_article.article.name, + goa.result.to_i, + number_to_currency(order_article.price.price), + number_to_currency(goa_total_net), + tax.to_s + '%', + number_to_currency(goa_total_gross)] + + if separate_deposits && order_article.price.deposit > 0.0 + goa_deposit = goa.result * order_article.price.deposit + goa_total_deposit = goa.result * order_article.price.fc_deposit_price + + data << ["zzgl. Pfand", + goa.result.to_i, + number_to_currency(order_article.price.deposit), + number_to_currency(goa_deposit), + tax.to_s + '%', + number_to_currency(goa_total_deposit)] + + total_deposit += goa_deposit + total_deposit_gross += goa_total_deposit + + tax_hash_deposit_net[tax.to_i] += goa_deposit + tax_hash_deposit_gross[tax.to_i] += goa_total_deposit + end + + tax_hash_net[tax.to_i] += goa_total_net + tax_hash_gross[tax.to_i] += goa_total_gross + + total_net += goa_total_net + total_gross += goa_total_gross + end + end + + # Two separate tables for sum and individual data + # article information + data + table data, cell_style: { size: fontsize(8), overflow: :shrink_to_fit } do |table| + table.header = true + table.position = :center + table.cells.border_width = 1 + table.cells.border_color = '666666' + table.row(0).columns(0..6).style(background_color: 'cccccc', font_style: :bold) + table.rows(0..-1).columns(0..6).width = 80 + table.row(0).border_bottom_width = 2 + table.columns(1).align = :right + table.columns(1..6).align = :right + end + + sum = [[nil, nil, nil, "Netto", "MwSt", "Brutto"]] + [7, 19].each do |key| + sum << [nil, nil, "Produkte mit #{key}%", number_to_currency(tax_hash_net[key]), number_to_currency(tax_hash_gross[key] - tax_hash_net[key]), number_to_currency(tax_hash_gross[key])] + sum << [nil, nil, "Pfand mit #{key}%", number_to_currency(tax_hash_deposit_net[key]), number_to_currency(tax_hash_deposit_gross[key] - tax_hash_deposit_net[key]), number_to_currency(tax_hash_deposit_gross[key])] if separate_deposits + end + + total_deposit_gross ||= 0 + sum << [nil, nil, nil, nil, I18n.t('documents.group_order_invoice_pdf.sum_to_pay_gross'), number_to_currency(total_gross + total_deposit_gross)] + + move_down 10 + table sum, cell_style: { size: fontsize(8), overflow: :shrink_to_fit } do |table| + table.header = true + table.position = :center + table.cells.border_width = 1 + table.cells.border_color = '666666' + table.row(0).columns(2..6).style(align: :bottom) + table.row(0).border_bottom_width = 2 + table.row(0..-1).columns(0..1).border_width = 0 + + table.rows(0..-1).columns(0..6).width = 80 + table.row(-1).column(-1).style(font_style: :bold) + table.row(-1).column(-2).style(font_style: :bold) + table.row(-1).column(-1).size = fontsize(10) + table.row(-1).column(-2).size = fontsize(10) + + table.columns(1).align = :right + table.columns(1..6).align = :right + end + + if FoodsoftConfig[:group_order_invoices][:vat_exempt] + move_down 15 + text I18n.t('documents.group_order_invoice_pdf.small_business_regulation') + end + move_down 10 + end +end diff --git a/app/jobs/notify_group_order_invoice_job.rb b/app/jobs/notify_group_order_invoice_job.rb new file mode 100644 index 00000000..1a17fe9a --- /dev/null +++ b/app/jobs/notify_group_order_invoice_job.rb @@ -0,0 +1,10 @@ +class NotifyGroupOrderInvoiceJob < ApplicationJob + def perform(group_order_invoice) + ordergroup = group_order_invoice.group_order.ordergroup + ordergroup.users.each do |user| + Mailer.deliver_now_with_user_locale user do + Mailer.group_order_invoice(group_order_invoice, user) + end + end + end +end diff --git a/app/lib/render_pdf.rb b/app/lib/render_pdf.rb index 2311e646..a4974fdf 100644 --- a/app/lib/render_pdf.rb +++ b/app/lib/render_pdf.rb @@ -70,7 +70,7 @@ class RenderPdf < Prawn::Document options[:skip_page_creation] = true @options = options @first_page = true - + no_footer = @options&.[](:no_footer) ? true : false super(options) # Use ttf for better utf-8 compability @@ -84,11 +84,11 @@ class RenderPdf < Prawn::Document ) header = options[:title] || title - footer = I18n.l(Time.now, format: :long) + footer = I18n.l(Time.now, format: :long) unless no_footer header_size = 0 header_size = height_of(header, size: HEADER_FONT_SIZE, font: DEFAULT_FONT) + HEADER_SPACE if header - footer_size = height_of(footer, size: FOOTER_FONT_SIZE, font: DEFAULT_FONT) + FOOTER_SPACE + footer_size = no_footer ? 0 : height_of(footer, size: FOOTER_FONT_SIZE, font: DEFAULT_FONT) + FOOTER_SPACE start_new_page(top_margin: TOP_MARGIN + header_size, bottom_margin: BOTTOM_MARGIN + footer_size) @@ -98,12 +98,15 @@ class RenderPdf < Prawn::Document bounding_box [bounds.left, bounds.top + header_size], width: bounds.width, height: header_size do text header, size: HEADER_FONT_SIZE, align: :center, overflow: :shrink_to_fit if header end - font_size FOOTER_FONT_SIZE do - bounding_box [bounds.left, bounds.bottom - FOOTER_SPACE], width: bounds.width, height: footer_size do - text footer, align: :left, valign: :bottom - end - bounding_box [bounds.left, bounds.bottom - FOOTER_SPACE], width: bounds.width, height: footer_size do - text I18n.t('lib.render_pdf.page', number: page_number, count: page_count), align: :right, valign: :bottom + + unless no_footer + font_size FOOTER_FONT_SIZE do + bounding_box [bounds.left, bounds.bottom - FOOTER_SPACE], width: bounds.width, height: footer_size do + text footer, align: :left, valign: :bottom + end + bounding_box [bounds.left, bounds.bottom - FOOTER_SPACE], width: bounds.width, height: footer_size do + text I18n.t('lib.render_pdf.page', number: page_number, count: page_count), align: :right, valign: :bottom + end end end end diff --git a/app/mailers/mailer.rb b/app/mailers/mailer.rb index 90c8a062..2b06ce8e 100644 --- a/app/mailers/mailer.rb +++ b/app/mailers/mailer.rb @@ -51,6 +51,18 @@ class Mailer < ActionMailer::Base subject: I18n.t('mailer.welcome.subject') end + # Sends automatically generated invoicesfor group orders to ordergroup members + def group_order_invoice(group_order_invoice, user) + @user = user + @group_order_invoice = group_order_invoice + @group_order = group_order_invoice.group_order + @supplier = @group_order.order.supplier.name + @group = @group_order.ordergroup + add_group_order_invoice_attachments(group_order_invoice) + mail to: user, + subject: I18n.t('mailer.group_order_invoice.subject', group: @group.name, supplier: @supplier) + end + # Sends order result for specific Ordergroup def order_result(user, group_order) @order = group_order.order @@ -169,6 +181,11 @@ class Mailer < ActionMailer::Base attachments['order.csv'] = OrderCsv.new(order, options).to_csv end + def add_group_order_invoice_attachments(group_order_invoice) + attachment_name = group_order_invoice.name + '.pdf' + attachments[attachment_name] = GroupOrderInvoicePdf.new(group_order_invoice.load_data_for_invoice).to_pdf + end + # separate method to allow plugins to mess with the text def additonal_welcome_text(user); end diff --git a/app/models/concerns/price_calculation.rb b/app/models/concerns/price_calculation.rb index 8d56d671..47c28bc6 100644 --- a/app/models/concerns/price_calculation.rb +++ b/app/models/concerns/price_calculation.rb @@ -7,11 +7,27 @@ module PriceCalculation add_percent(price + deposit, tax) end + def gross_price_without_deposit + add_percent(price, tax) + end + + def gross_deposit_price + add_percent(deposit, tax) + end + # @return [Number] Price for the foodcoop-member. def fc_price add_percent(gross_price, FoodsoftConfig[:price_markup].to_i) end + def fc_price_without_deposit + add_percent(gross_price_without_deposit, FoodsoftConfig[:price_markup].to_i) + end + + def fc_deposit_price + add_percent(gross_deposit_price, FoodsoftConfig[:price_markup].to_i) + end + private def add_percent(value, percent) diff --git a/app/models/group_order.rb b/app/models/group_order.rb index 183b663a..4768a0bd 100644 --- a/app/models/group_order.rb +++ b/app/models/group_order.rb @@ -9,6 +9,7 @@ class GroupOrder < ApplicationRecord has_many :group_order_articles, dependent: :destroy has_many :order_articles, through: :group_order_articles has_one :financial_transaction + has_one :group_order_invoice belongs_to :updated_by, optional: true, class_name: 'User', foreign_key: 'updated_by_user_id' validates :order_id, presence: true diff --git a/app/models/group_order_article.rb b/app/models/group_order_article.rb index 7b95d462..f5f42789 100644 --- a/app/models/group_order_article.rb +++ b/app/models/group_order_article.rb @@ -208,6 +208,18 @@ class GroupOrderArticle < ApplicationRecord end end + def total_price_without_deposit(order_article = self.order_article) + if order_article.order.open? + if FoodsoftConfig[:tolerance_is_costly] + order_article.price.fc_price_without_deposit * (quantity + tolerance) + else + order_article.price.fc_price_without_deposit * quantity + end + else + order_article.price.fc_price_without_deposit * result + end + end + # Check if the result deviates from the result_computed def result_manually_changed? result != result_computed unless result.nil? diff --git a/app/models/group_order_invoice.rb b/app/models/group_order_invoice.rb new file mode 100644 index 00000000..21557161 --- /dev/null +++ b/app/models/group_order_invoice.rb @@ -0,0 +1,58 @@ +class GroupOrderInvoice < ApplicationRecord + belongs_to :group_order + validates_presence_of :group_order + validates_uniqueness_of :invoice_number + validate :tax_number_set + after_initialize :init, unless: :persisted? + + def generate_invoice_number(count) + trailing_number = count.to_s.rjust(4, '0') + if GroupOrderInvoice.find_by(invoice_number: self.invoice_date.strftime("%Y%m%d") + trailing_number) + generate_invoice_number(count.to_i + 1) + else + self.invoice_date.strftime("%Y%m%d") + trailing_number + end + end + + def tax_number_set + if FoodsoftConfig[:contact][:tax_number].blank? + errors.add(:group_order_invoice, "Keine Steuernummer in FoodsoftConfig :contact gesetzt") + end + end + + def init + self.invoice_date = Time.now unless invoice_date + self.invoice_number = generate_invoice_number(1) unless self.invoice_number + self.payment_method = FoodsoftConfig[:group_order_invoices]&.[](:payment_method) || I18n.t('activerecord.attributes.group_order_invoice.payment_method') unless self.payment_method + end + + def name + I18n.t('activerecord.attributes.group_order_invoice.name') + "_#{invoice_number}" + end + + def load_data_for_invoice + invoice_data = {} + order = group_order.order + invoice_data[:supplier] = order.supplier.name + invoice_data[:ordergroup] = group_order.ordergroup + invoice_data[:group_order] = group_order + invoice_data[:invoice_number] = invoice_number + invoice_data[:invoice_date] = invoice_date + invoice_data[:tax_number] = FoodsoftConfig[:contact][:tax_number] + invoice_data[:payment_method] = payment_method + invoice_data[:order_articles] = {} + group_order.order_articles.each do |order_article| + # Get the result of last time ordering, if possible + goa = group_order.group_order_articles.detect { |tmp_goa| tmp_goa.order_article_id == order_article.id } + + # Build hash with relevant data + invoice_data[:order_articles][order_article.id] = { + :price => order_article.article.fc_price, + :quantity => (goa ? goa.quantity : 0), + :total_price => (goa ? goa.total_price : 0), + :tax => order_article.article.tax + } + end + invoice_data + end +end diff --git a/app/models/order.rb b/app/models/order.rb index ada62e59..e275519b 100644 --- a/app/models/order.rb +++ b/app/models/order.rb @@ -207,7 +207,7 @@ class Order < ApplicationRecord # :fc, guess what... def sum(type = :gross) total = 0 - if %i[net gross fc].include?(type) + if %i[net gross gross_deposit fc_deposit deposit fc].include?(type) for oa in order_articles.ordered.includes(:article, :article_price) quantity = oa.units * oa.price.unit_quantity case type @@ -217,6 +217,12 @@ class Order < ApplicationRecord total += quantity * oa.price.gross_price when :fc total += quantity * oa.price.fc_price + when :gross_deposit + total += quantity * oa.price.gross_deposit_price + when :fc_deposit + total += quantity * oa.price.fc_deposit_price + when :deposit + total += quantity * oa.price.deposit end end elsif %i[groups groups_without_markup].include?(type) @@ -224,7 +230,11 @@ class Order < ApplicationRecord for goa in go.group_order_articles case type when :groups - total += goa.result * goa.order_article.price.fc_price + total += if FoodsoftConfig[:group_order_invoices]&.[](:separate_deposits) + goa.result * (goa.order_article.price.fc_price + goa.order_article.price.fc_deposit_price) + else + goa.result * goa.order_article.price.fc_price + end when :groups_without_markup total += goa.result * goa.order_article.price.gross_price end diff --git a/app/views/admin/configs/_tab_foodcoop.html.haml b/app/views/admin/configs/_tab_foodcoop.html.haml index 5aea80c2..efea4d81 100644 --- a/app/views/admin/configs/_tab_foodcoop.html.haml +++ b/app/views/admin/configs/_tab_foodcoop.html.haml @@ -7,4 +7,5 @@ = config_input c, :country, as: :string, input_html: {class: 'input-xlarge'} = config_input c, :email, required: true, input_html: {class: 'input-xlarge'} = config_input c, :phone, input_html: {class: 'input-medium'} + = config_input c, :tax_number, input_html: {class: 'input-medium'} = config_input form, :homepage, required: true, as: :url, input_html: {class: 'input-xlarge'} diff --git a/app/views/admin/configs/_tab_payment.html.haml b/app/views/admin/configs/_tab_payment.html.haml index 3fd7ca0a..70291110 100644 --- a/app/views/admin/configs/_tab_payment.html.haml +++ b/app/views/admin/configs/_tab_payment.html.haml @@ -13,6 +13,12 @@ = config_input form, :charge_members_manually, as: :boolean = config_input form, :use_iban, as: :boolean = config_input form, :use_self_service, as: :boolean +%h4= t '.group_order_invoices' += form.fields_for :group_order_invoices do |field| + = config_input field, :use_automatic_invoices, as: :boolean + = config_input field, :separate_deposits, as: :boolean + = config_input field, :vat_exempt, as: :boolean + = config_input field, :payment_method, as: :string, input_html: {class: 'input-medium'} %h4= t '.schedule_title' = form.simple_fields_for :order_schedule do |fields| diff --git a/app/views/admin/ordergroups/_form.html.haml b/app/views/admin/ordergroups/_form.html.haml index 3eb3a9f5..128338bb 100644 --- a/app/views/admin/ordergroups/_form.html.haml +++ b/app/views/admin/ordergroups/_form.html.haml @@ -2,6 +2,7 @@ %p= t('.first_paragraph', url: link_to(t('.here'), new_invite_path(id: @ordergroup.id), remote: true)).html_safe = simple_form_for [:admin, @ordergroup] do |f| - captured = capture do + = f.input :customer_number = f.input :contact_person = f.input :contact_phone = f.input :contact_address diff --git a/app/views/finance/balancing/_orders.html.haml b/app/views/finance/balancing/_orders.html.haml index 3f20d850..dddb00c2 100644 --- a/app/views/finance/balancing/_orders.html.haml +++ b/app/views/finance/balancing/_orders.html.haml @@ -9,6 +9,8 @@ %th= t('.end') %th= t('.state') %th= heading_helper Order, :updated_by + %th= heading_helper GroupOrderInvoice, :name + %th %th %tbody - @orders.each do |order| @@ -17,6 +19,14 @@ %td=h format_time(order.ends) unless order.ends.nil? %td= order.closed? ? t('.cleared', amount: number_to_currency(order.foodcoop_result)) : t('.ended') %td= show_user(order.updated_by) + %td{id: "generate-invoice#{order.id}"} + - if order.closed? + -if FoodsoftConfig[:contact][:tax_number] && order.ordergroups.present? + = render :partial => 'group_order_invoices/links', locals:{order: order} + -else + = I18n.t('activerecord.attributes.group_order_invoice.tax_number_not_set') + - else + = t('orders.index.not_closed') %td - unless order.closed? - if current_user.role_orders? diff --git a/app/views/finance/balancing/_summary.haml b/app/views/finance/balancing/_summary.haml index e466727f..6516aa69 100644 --- a/app/views/finance/balancing/_summary.haml +++ b/app/views/finance/balancing/_summary.haml @@ -12,6 +12,16 @@ %tr %td= t('.fc_amount') %td.numeric= number_to_currency(order.sum(:fc)) + - if FoodsoftConfig[:group_order_invoices]&.[](:separate_deposits) + %tr + %td= t('.deposit') + %td.numeric= number_to_currency(order.sum(:deposit)) + %tr + %td= t('.gross_deposit') + %td.numeric= number_to_currency(order.sum(:gross_deposit)) + %tr + %td= t('.fc_deposit') + %td.numeric= number_to_currency(order.sum(:fc_deposit)) %tr %td= t('.groups_amount') %td.numeric= number_to_currency(order.sum(:groups)) diff --git a/app/views/finance/balancing/index.html.haml b/app/views/finance/balancing/index.html.haml index 1d1fd8b5..4a7fd119 100644 --- a/app/views/finance/balancing/index.html.haml +++ b/app/views/finance/balancing/index.html.haml @@ -1,5 +1,4 @@ - title t('.title') - - content_for :actionbar do - if FoodsoftConfig[:charge_members_manually] = link_to t('.close_all_direct_with_invoice'), close_all_direct_with_invoice_finance_order_index_path, method: :post, class: 'btn' diff --git a/app/views/group_order_invoices/_links.html.haml b/app/views/group_order_invoices/_links.html.haml new file mode 100644 index 00000000..9e55cedf --- /dev/null +++ b/app/views/group_order_invoices/_links.html.haml @@ -0,0 +1,29 @@ +.row + .column.small-12 + - show_generate_with_date = true + - order.group_orders.each do |go| + - if go.group_order_invoice.present? + - show_generate_with_date = false + - if show_generate_with_date + = form_for :group_order_invoice, url: url_for('group_order_invoice#create_multiple'), remote: true do |f| + = f.label :invoice_date, I18n.t('activerecord.attributes.group_order_invoice.links.invoice_date') + = f.date_field :invoice_date, {value: Date.today, max: Date.today, required: true} + = f.hidden_field :order_id, value: order.id + = f.submit I18n.t('activerecord.attributes.group_order_invoice.links.generate_with_date'), class: 'btn btn small' + +- order.group_orders.includes([:group_order_invoice, :ordergroup]).each do |go| + .row + .column.small-3 + = label_tag go.ordergroup.name + - if go.group_order_invoice + .column.small-3 + = link_to I18n.t('activerecord.attributes.group_order_invoice.links.download'), group_order_invoice_path(go.group_order_invoice, :format => 'pdf'), class: 'btn btn-small' + .column.small-3 + = link_to I18n.t('activerecord.attributes.group_order_invoice.links.delete'), go.group_order_invoice, method: :delete, class: 'btn btn-danger btn-small', remote: true + - else + = button_to I18n.t('activerecord.attributes.group_order_invoice.links.generate'), group_order_invoices_path(:method => :post, group_order: go) ,class: 'btn btn-small', params: {id: order.id}, remote: true +- if order.group_orders.map(&:group_order_invoice).compact.present? + %br/ + .row + .column.small-3 + = link_to I18n.t('activerecord.attributes.group_order_invoice.links.download_all_zip'), download_all_group_order_invoices_path(order), class: 'btn btn-small' diff --git a/app/views/group_order_invoices/create.js.erb b/app/views/group_order_invoices/create.js.erb new file mode 100644 index 00000000..5a43e85d --- /dev/null +++ b/app/views/group_order_invoices/create.js.erb @@ -0,0 +1 @@ +$("#generate-invoice<%= params[:id] %>").html("<%= escape_javascript(render partial: 'links', locals: {order: @order}) %>"); diff --git a/app/views/group_order_invoices/create_multiple.js.erb b/app/views/group_order_invoices/create_multiple.js.erb new file mode 100644 index 00000000..11ebbe45 --- /dev/null +++ b/app/views/group_order_invoices/create_multiple.js.erb @@ -0,0 +1 @@ +$("#generate-invoice<%= @order.id %>").html("<%= escape_javascript(render partial: 'links', locals: {order: @order}) %>"); diff --git a/app/views/group_order_invoices/destroy.js.erb b/app/views/group_order_invoices/destroy.js.erb new file mode 100644 index 00000000..30ce5985 --- /dev/null +++ b/app/views/group_order_invoices/destroy.js.erb @@ -0,0 +1 @@ +$("#generate-invoice<%= @order.id %>").html("<%= escape_javascript(render partial: 'links', locals: {order: @order}) %>"); \ No newline at end of file diff --git a/app/views/group_orders/_form.html.haml b/app/views/group_orders/_form.html.haml index 3ffd583e..0cd27c76 100644 --- a/app/views/group_orders/_form.html.haml +++ b/app/views/group_orders/_form.html.haml @@ -69,7 +69,7 @@ = f.hidden_field :order_id = f.hidden_field :updated_by_user_id = f.hidden_field :ordergroup_id - %table.table.table-hover + %table.table %thead %tr %th= heading_helper Article, :name @@ -94,7 +94,7 @@ %i.icon-tag %td{colspan: "9"} - order_articles.each do |order_article| - %tr{class: "#{cycle('even', 'odd', name: 'articles')} order-article #{get_missing_units_css_class(@ordering_data[:order_articles][order_article.id][:missing_units])}", valign: "top"} + %tr{class: "#{cycle('even', 'odd', name: 'articles')} order-article #{get_missing_units_css_class(@ordering_data[:order_articles][order_article.id][:missing_units])}", valign: "top", tabindex: "0"} %td.name= order_article.article.name - if @order.stockit? %td= truncate order_article.article.supplier.name, length: 15 diff --git a/app/views/mailer/group_order_invoice.text.haml b/app/views/mailer/group_order_invoice.text.haml new file mode 100644 index 00000000..75948fbe --- /dev/null +++ b/app/views/mailer/group_order_invoice.text.haml @@ -0,0 +1 @@ += raw t '.text', group: @group.name, supplier: @supplier , foodcoop: FoodsoftConfig[:name] diff --git a/app/views/ordergroups/edit.html.haml b/app/views/ordergroups/edit.html.haml index 1cba43e6..9e964c89 100644 --- a/app/views/ordergroups/edit.html.haml +++ b/app/views/ordergroups/edit.html.haml @@ -57,6 +57,10 @@ = f.label :contact_person %br/ = f.text_field :contact_person + %p + = f.label :customer_number + %br/ + = f.text_field :customer_number %p = f.label :contact_phone %br/ diff --git a/app/views/shared/_group.html.haml b/app/views/shared/_group.html.haml index 3386aaab..c4d00679 100644 --- a/app/views/shared/_group.html.haml +++ b/app/views/shared/_group.html.haml @@ -6,6 +6,8 @@ %dd=h group.contact %dt= heading_helper(Ordergroup, :contact_address) + ':' %dd= link_to_gmaps group.contact_address + %dt= heading_helper(Ordergroup, :customer_number) + ':' + %dd=h group.customer_number - if group.break_start? or group.break_end? %dt= heading_helper(Ordergroup, :break) + ':' %dd= raw t '.break', start: format_date(group.break_start), end: format_date(group.break_end) diff --git a/config/locales/de.yml b/config/locales/de.yml index 6a957ec2..960ddf53 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -90,6 +90,17 @@ de: tolerance: Toleranz total_price: Summe unit_price: Preis/Einheit + group_order_invoice: + name: Bestellgruppenrechnung + links: + delete: Rechnung löschen + download: Rechnung herunterladen + generate: Rechnung erzeugen + invoice_date: Datum der Bestellgruppenrechnung + generate_with_date: setzen & erzeugen + download_all_zip: Alle Rechnungen herunterladen (zip) + payment_method: Guthaben + tax_number_not_set: Steuernummer in den Einstellungen nicht gesetzt invoice: amount: Betrag attachment: Anhang @@ -158,6 +169,7 @@ de: contact_address: Adresse contact_person: Kontaktperson contact_phone: Telefon + customer_number: Kundennummer description: Beschreibung ignore_apple_restriction: Bestellstop bei zu wenig Äpfeln ignorieren last_order: Zuletzt bestellt @@ -318,6 +330,7 @@ de: emails_title: E-Mails versenden tab_payment: schedule_title: Bestellschema + group_order_invoices: Bestellgruppenrechnungen tab_security: default_roles_title: Zugriff auf default_roles_paragraph: Jedes Mitglied der Foodcoop hat automatisch Zugriff auf folgende Bereiche. @@ -603,6 +616,10 @@ de: email_from: E-Mails werden so aussehen, als ob sie von dieser Adresse gesendet wurden. Kann leer gelassen werden, um die Kontaktadresse der Foodcoop zu benutzen. email_replyto: Setze diese Adresse, wenn Du Antworten auf Foodsoft E-Mails auf eine andere, als die oben angegebene Absenderadresse bekommen möchtest. email_sender: E-Mails werden so aussehen, als ob sie von dieser Adresse versendet wurden. Um zu vermeiden, dass E-Mails dadurch als Spam eingeordnet werden, muss der Webserver möglicherweise im SPF Eintrag der Domain der E-Mail Adresse eingetragen werden. + group_order_invoices: + use_automativ_go_invoices: Es werden auf die Bestellgruppen zugeschnittene Rechnungen für die jeweilige Bestellung beim Klicken auf "abrechnen" an alle Bestellgruppenmitglieder per Mail versendet. + payment_method: Zahlungsart wird auf der Bestellgruppenrechnung deklariert + vat_exempt: Eine Auflistung der Rechnungsartikel erfolgt ohne explizite Ausweisung der MwSt. und die Rechnung erhält den notwendigen Zusatz bzgl. der Kleinunternehmerregelung §19 (FoodCoop Marge ebenfalls nicht in Rechnung enthalten) help_url: Link zur Dokumentationsseite homepage: Webseite der Foodcoop ignore_browser_locale: Ignoriere die Sprache des Computers des Anwenders, wenn der Anwender noch keine Sprache gewählt hat. @@ -645,6 +662,7 @@ de: phone: Telefon street: Straße zip_code: Postleitzahl + tax_number: Steuernummer currency_space: Leerzeichen hinzufügen currency_unit: Währung custom_css: Angepasstes CSS @@ -660,6 +678,11 @@ de: email_from: Absenderadresse email_replyto: Antwortadresse email_sender: Senderadresse + group_order_invoices: + use_automatic_invoices: Automatisch bei Abrechnung per Mail versenden + separate_deposits: Pfand getrennt abrechnen + payment_method: Zahlungsart + vat_exempt: Diese Foodcoop ist MwSt. befreit help_url: URL Dokumentation homepage: Webseite ignore_browser_locale: Browsersprache ignorieren @@ -746,6 +769,47 @@ de: update: notice: Lieferung wurde aktualisiert. documents: + group_order_invoice_pdf: + filename: Rechnung%{number} + invoicer: Rechnungsteller*in + invoicee: Rechnungsempfänger*in + invoice_date: 'Rechnungsdatum: %{invoice_date}' + invoice_number: 'Rechnungsnummer: %{invoice_number}' + markup_included: zzgl. Foodcoop Marge auf brutto Preis %{marge}% + ordergroup: + contact_phone: 'Telefonnummer: %{contact_phone}' + contact_address: 'Adresse : %{contact_address}' + customer_number: 'Kundennummer: %{customer_number}' + name: Bestellgruppe %{ordergroup} + payment_method: 'Zahlungsart: %{payment_method}' + sum_to_pay: Zu zahlen gesamt + sum_to_pay_net: Zu zahlen gesamt (netto) + sum_to_pay_gross: Gesamt + small_business_regulation: Als Kleinunternehmer*in im Sinne von §19 Abs. 1 Umsatzsteuergesetz (UStG) wird keine Umsatzsteuer berechnet. + table_headline: 'Für die Bestellung fallen folgende Posten an:' + tax_excluded: exkl. MwSt. + tax_included: zzgl. Gesamtsumme MwSt. %{tax}% + tax_number: 'Steuernummer: %{number}' + title: Rechnung für die Bestellung bei %{supplier} + vat_exempt_rows: + - Name + - Anzahl + - Einzelpreis + - Artikel Gesamtpreis + no_price_markup_rows: + - Name + - Anzahl + - Einzelpreis (netto) + - Artikel Gesamtpreis (netto) + - MwSt. + - Artikel Gesamtpreis (brutto) + price_markup_rows: + - Name + - Anzahl + - Einzelpreis (netto) + - Artikel Gesamtpreis (netto) + - MwSt. + - Artikel Gesamtpreis (brutto) inkl. Foodcoopmarge %{marge}% order_by_articles: filename: Bestellung %{name}-%{date} - Artikelsortierung title: 'Artikelsortierung der Bestellung: %{name}, beendet am %{date}' @@ -769,6 +833,7 @@ de: heading: Artikelübersicht (%{count}) title: 'Sortiermatrix der Bestellung: %{name}, beendet am %{date}' errors: + check_tax_number: Überprüft, ob die Steuernummer der Foodcoop richtig gesetzt ist general: Ein Problem ist aufgetreten. general_again: Ein Fehler ist aufgetreten. Bitte erneut versuchen. general_msg: 'Ein Fehler ist aufgetreten: %{msg}' @@ -792,6 +857,8 @@ de: close: alert: 'Ein Fehler ist beim Abrechnen aufgetreten: %{message}' notice: Bestellung wurde erfolgreich abgerechnet, die Kontostände aktualisiert. + notice_mail: Bestellung wurde erfolgreich abgerechnet, die Kontostände aktualisiert. Außerdem wurden automatisch Rechnungen an die Bestellgruppenmitglieder geschickt. + settings_not_set: Keine Emails mit Bestellgruppenrechnungen versendet. Bitte überprüfe die Einstellungen. Steuernummer gesetzt? close_all_direct_with_invoice: notice: 'Es wurden %{count} Bestellung abgerechnet.' close_direct: @@ -856,11 +923,15 @@ de: ended: beendet name: Lieferantin no_closed_orders: Derzeit gibt es keine beendeten Bestellungen. + state: Status summary: changed: Daten wurden verändert! duration: von %{starts} bis %{ends} fc_amount: 'FC-Betrag:' + deposit: 'Pfand netto:' + gross_deposit: 'Pfand brutto:' + fc_deposit: 'Pfand FC-Betrag:' fc_profit: FC Gewinn gross_amount: 'Bruttobetrag:' groups_amount: 'Gruppenbeträge:' @@ -1273,6 +1344,15 @@ de: header: "%{user} schrieb am %{date}:" subject: Feedback zur Foodsoft from_via_foodsoft: "%{name} via Foodsoft" + group_order_invoice: + subject: Bestellgruppenrechnung für %{group} bei %{supplier} + text: | + Liebe Bestellgruppe %{group}, + + Die Sammelbestellung bei %{supplier} wurde soeben abgerechnet und für die jeweiligen Bestellgruppen Rechnungen angelegt. + Im Anhang befindet sich daher eure Rechnung. + + Viele Grüße von %{foodcoop} invite: subject: Einladung in die Foodcoop text: | @@ -1508,6 +1588,7 @@ de: orders_finished: Beendet orders_open: Laufend orders_settled: Abgerechnet + not_closed: Bestellung noch nicht abgerechnet title: Bestellungen verwalten model: close_direct_message: Die Bestellung wurde abgechlossen, ohne die Mitgliederkonten zu belasten. @@ -1907,3 +1988,4 @@ de: time: formats: foodsoft_datetime: "%d.%m.%Y %H:%M" + file: "%Y-%d-%B" \ No newline at end of file diff --git a/config/locales/en.yml b/config/locales/en.yml index b4f41c5c..1ee30987 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -90,6 +90,18 @@ en: tolerance: Tolerance total_price: Sum unit_price: Price/Unit + group_order_invoice: + name: Group order invoice + links: + delete: delete invoice + download: download invoice + invoice_date: date of group order invoice + generate: generate invoice + generate_with_date: set & generate + download_all_zip: download all invoices as zip + + payment_method: Credit + tax_number_not_set: Tax number not set in configs invoice: amount: Amount attachment: Attachment @@ -158,6 +170,7 @@ en: contact_address: Address contact_person: Contact person contact_phone: Phone + customer_number: Customer number description: Description ignore_apple_restriction: Ignore order stop by apple points restriction last_order: Last order @@ -318,6 +331,7 @@ en: emails_title: Sending email tab_payment: schedule_title: Ordering schedule + group_order_invoices: Group order invoices tab_security: default_roles_title: Access to default_roles_paragraph: By default every member of the foodcoop has access to the following areas. @@ -603,6 +617,9 @@ en: email_from: Emails will appear to be from this email address. Leave empty to use the foodcoop's contact address. email_replyto: Set this when you want to receive replies from emails sent by Foodsoft on a different address than the above. email_sender: Emails will appear to be sent from this email address. To avoid emails sent being classified as spam, the webserver may need to be registered in the SPF record of the email address's domain. + use_automatic_invoices: A listing of the invoice items is made without explicit display of VAT and the invoice receives the necessary addition regarding the small business regulation §19 (applies to Germany) + payment_method: Payment type is declared on the order group invoice + vat_exempt: A listing of the invoice items is made without explicit display of VAT and the invoice contains the necessary addition regarding the German Kleinunternehmerregelung §19 UStG (attention! FoodCoop marge not included in nvoice). help_url: Documentation website. homepage: Website of your foodcoop. ignore_browser_locale: Ignore the language of user's computer when the user has not chosen a language yet. @@ -630,6 +647,8 @@ en: tolerance_is_costly: Order as much of the member tolerance as possible (compared to only as much needed to fill the last box). Enabling this also includes the tolerance in the total price of the open member order. distribution_strategy: How articles should be distributed after an order has been received. use_apple_points: When the apple point system is enabled, members are required to do some tasks to be able to keep ordering. + use_automatic_invoices: When an order is settled, invoices for the individual order groups are automatically sent by mail + payment_method: Payment Method for group order invoices use_boxfill: When enabled, near end of an order, members are only able to change their order when increases the total amount ordered. This helps to fill any remaining boxes. You still need to set a box-fill date for the orders. use_iban: When enabled, supplier and user provide an additonal field for storing the international bank account number. use_nick: Show and use nicknames instead of real names. When enabling this, please check that each user has a nickname. @@ -645,6 +664,7 @@ en: phone: Phone street: Street zip_code: Postcode + tax_number: Tax number currency_space: add space currency_unit: Currency custom_css: Custom CSS @@ -689,6 +709,11 @@ en: first_order_first_serve: First distribute to those who ordered first no_automatic_distribution: No automatic distribution use_apple_points: Apple points + group_order_invoices: + use_automatic_invoices: Send automatically via mail after oder settlement + payment_method: Payment method + separate_deposits: Separate deposits on invoice + vat_exempt: This foodcoopis VAT exempt use_boxfill: Box-fill phase use_iban: Use IBAN use_nick: Use nicknames @@ -746,6 +771,47 @@ en: update: notice: Delivery was updated. documents: + group_order_invoice_pdf: + ordergroup: + contact_phone: 'Phone: %{contact_phone}' + contact_address: 'Adress : %{contact_address}' + customer_number: 'Customer number: %{customer_number}' + name: 'Ordergroup: %{ordergroup}' + filename: Invoice%{number} + invoicee: Invoicee + invoicer: Invoicer + invoice_date: 'Invoice date: %{invoice_date}' + invoice_number: 'Invoice number: %{invoice_number}' + markup_included: incl Foodcoop Marge on gross price %{marge}% + payment_method: 'Payment_method: %{payment_method}' + small_business_regulation: As a small entrepreneur in the sense of §19 para. 1 of the Umsatzsteuergesetz (UStG), no value added tax is charged. + sum_to_pay: Total sum + sum_to_pay_net: Total sum (net) + sum_to_pay_gross: Total sum (gross) + table_headline: 'The following items will be charged for the order:' + tax_excluded: excl. MwSt. + tax_included: incl. VAT %{tax}% + tax_number: 'Tax number: %{number}' + title: Invoice for order at %{supplier} + vat_exempt_rows: + - Name + - Quantity + - Unit price + - Total price + no_price_markup_rows: + - Name + - Quantity + - Unit price (net) + - Total price (net) + - VAT + - Total price (gross) + price_markup_rows: + - Name + - Quantity + - Unit price (net) + - Total price (net) + - VAT + - Total price (gross) incl. foodcoop margin order_by_articles: filename: Order %{name}-%{date} - by articles title: 'Order sorted by articles: %{name}, closed at %{date}' @@ -769,6 +835,7 @@ en: heading: Article overview (%{count}) title: 'Order sorting matrix: %{name}, closed at %{date}' errors: + check_tax_number: Please check whether the foodcoop's tax number is set correctly. general: A problem has occured. general_again: A problem has occured. Please try again. general_msg: 'A problem has occured: %{msg}' @@ -792,6 +859,7 @@ en: close: alert: 'An error occured while accounting: %{message}' notice: Order was settled succesfully, the balance of the account was updated. + settings_not_set: No emails with order group invoices sent. Please check the settings. Tax number set? close_all_direct_with_invoice: notice: '%{count} orders have been settled.' close_direct: @@ -1272,6 +1340,15 @@ en: feedback: header: "%{user} wrote at %{date}:" subject: Feedback for Foodsoft + group_order_invoice: + subject: Order group invoice for %{group} at %{supplier} + text: | + Dear order group %{group}, + + The collective order at %{supplier} has just been settled and invoices have been created for the respective order groups. + Attached you will find your invoice. + + Best regards from %{foodcoop} from_via_foodsoft: "%{name} via Foodsoft" invite: subject: Invitation to the Foodcoop @@ -1511,6 +1588,7 @@ en: orders_finished: Closed orders_open: Open orders_settled: Settled + not_closed: Order not yet settled title: Manage orders model: close_direct_message: Order settled without charging member accounts. @@ -1910,3 +1988,4 @@ en: time: formats: foodsoft_datetime: "%Y-%m-%d %H:%M" + file: "%Y-%d-%B" diff --git a/config/routes.rb b/config/routes.rb index 8fea34b0..de68ce73 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -143,6 +143,13 @@ Rails.application.routes.draw do end end + + post 'finance/group_order_invoice', to: 'group_order_invoices#create_multiple' + + get 'orders/:order_id/group_order_invoices/download_all', to: 'group_order_invoices#download_all', as: 'download_all_group_order_invoices' + + resources :group_order_invoices + resources :article_categories ########### Finance diff --git a/db/migrate/20211208142719_create_group_order_invoices.rb b/db/migrate/20211208142719_create_group_order_invoices.rb new file mode 100644 index 00000000..b0aa13f7 --- /dev/null +++ b/db/migrate/20211208142719_create_group_order_invoices.rb @@ -0,0 +1,13 @@ +class CreateGroupOrderInvoices < ActiveRecord::Migration[5.2] + def change + create_table :group_order_invoices do |t| + t.integer :group_order_id + t.bigint :invoice_number, unique: true, limit: 8 + t.date :invoice_date + t.string :payment_method + + t.timestamps + end + add_index :group_order_invoices, :group_order_id, unique: true + end +end diff --git a/db/migrate/20230822120005_add_customer_number_to_group.rb b/db/migrate/20230822120005_add_customer_number_to_group.rb new file mode 100644 index 00000000..9b4c2278 --- /dev/null +++ b/db/migrate/20230822120005_add_customer_number_to_group.rb @@ -0,0 +1,5 @@ +class AddCustomerNumberToGroup < ActiveRecord::Migration[7.0] + def change + add_column :groups, :customer_number, :string, unique: true + end +end diff --git a/db/schema.rb b/db/schema.rb index 4c853039..e024426f 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,8 +10,8 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 2023_02_15_085312) do - create_table "action_text_rich_texts", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| +ActiveRecord::Schema[7.0].define(version: 2023_08_22_120005) do + create_table "action_text_rich_texts", charset: "utf8mb4", force: :cascade do |t| t.string "name", null: false t.text "body", size: :long t.string "record_type", null: false @@ -21,7 +21,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_02_15_085312) do t.index ["record_type", "record_id", "name"], name: "index_action_text_rich_texts_uniqueness", unique: true end - create_table "active_storage_attachments", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| + create_table "active_storage_attachments", id: :integer, charset: "utf8mb4", force: :cascade do |t| t.string "name", null: false t.string "record_type", null: false t.bigint "record_id", null: false @@ -31,7 +31,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_02_15_085312) do t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true end - create_table "active_storage_blobs", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| + create_table "active_storage_blobs", id: :integer, charset: "utf8mb4", force: :cascade do |t| t.string "key", null: false t.string "filename", null: false t.string "content_type" @@ -43,19 +43,19 @@ ActiveRecord::Schema[7.0].define(version: 2023_02_15_085312) do t.index ["key"], name: "index_active_storage_blobs_on_key", unique: true end - create_table "active_storage_variant_records", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| + create_table "active_storage_variant_records", charset: "utf8mb4", force: :cascade do |t| t.integer "blob_id", null: false t.string "variation_digest", null: false t.index ["blob_id", "variation_digest"], name: "index_active_storage_variant_records_uniqueness", unique: true end - create_table "article_categories", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| + create_table "article_categories", id: :integer, charset: "utf8mb4", force: :cascade do |t| t.string "name", default: "", null: false t.string "description" t.index ["name"], name: "index_article_categories_on_name", unique: true end - create_table "article_prices", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| + create_table "article_prices", id: :integer, charset: "utf8mb4", force: :cascade do |t| t.integer "article_id", null: false t.decimal "price", precision: 8, scale: 2, default: "0.0", null: false t.decimal "tax", precision: 8, scale: 2, default: "0.0", null: false @@ -65,7 +65,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_02_15_085312) do t.index ["article_id"], name: "index_article_prices_on_article_id" end - create_table "articles", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| + create_table "articles", id: :integer, charset: "utf8mb4", force: :cascade do |t| t.string "name", default: "", null: false t.integer "supplier_id", default: 0, null: false t.integer "article_category_id", default: 0, null: false @@ -91,14 +91,14 @@ ActiveRecord::Schema[7.0].define(version: 2023_02_15_085312) do t.index ["type"], name: "index_articles_on_type" end - create_table "assignments", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| + create_table "assignments", id: :integer, charset: "utf8mb4", force: :cascade do |t| t.integer "user_id", default: 0, null: false t.integer "task_id", default: 0, null: false t.boolean "accepted", default: false t.index ["user_id", "task_id"], name: "index_assignments_on_user_id_and_task_id", unique: true end - create_table "bank_accounts", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| + create_table "bank_accounts", id: :integer, charset: "utf8mb4", force: :cascade do |t| t.string "name", null: false t.string "iban" t.string "description" @@ -108,14 +108,14 @@ ActiveRecord::Schema[7.0].define(version: 2023_02_15_085312) do t.integer "bank_gateway_id" end - create_table "bank_gateways", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| + create_table "bank_gateways", id: :integer, charset: "utf8mb4", force: :cascade do |t| t.string "name", null: false t.string "url", null: false t.string "authorization" t.integer "unattended_user_id" end - create_table "bank_transactions", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| + create_table "bank_transactions", id: :integer, charset: "utf8mb4", force: :cascade do |t| t.integer "bank_account_id", null: false t.string "external_id" t.date "date" @@ -129,7 +129,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_02_15_085312) do t.index ["financial_link_id"], name: "index_bank_transactions_on_financial_link_id" end - create_table "documents", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| + create_table "documents", id: :integer, charset: "utf8mb4", force: :cascade do |t| t.string "name" t.string "mime" t.binary "data", size: :long @@ -140,16 +140,16 @@ ActiveRecord::Schema[7.0].define(version: 2023_02_15_085312) do t.index ["parent_id"], name: "index_documents_on_parent_id" end - create_table "financial_links", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| + create_table "financial_links", id: :integer, charset: "utf8mb4", force: :cascade do |t| t.text "note" end - create_table "financial_transaction_classes", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| + create_table "financial_transaction_classes", id: :integer, charset: "utf8mb4", force: :cascade do |t| t.string "name", null: false t.boolean "ignore_for_account_balance", default: false, null: false end - create_table "financial_transaction_types", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| + create_table "financial_transaction_types", id: :integer, charset: "utf8mb4", force: :cascade do |t| t.string "name", null: false t.integer "financial_transaction_class_id", null: false t.string "name_short" @@ -157,7 +157,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_02_15_085312) do t.index ["name_short"], name: "index_financial_transaction_types_on_name_short" end - create_table "financial_transactions", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| + create_table "financial_transactions", id: :integer, charset: "utf8mb4", force: :cascade do |t| t.integer "ordergroup_id" t.decimal "amount", precision: 8, scale: 2, default: "0.0", null: false t.text "note", null: false @@ -171,7 +171,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_02_15_085312) do t.index ["reverts_id"], name: "index_financial_transactions_on_reverts_id", unique: true end - create_table "group_order_article_quantities", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| + create_table "group_order_article_quantities", id: :integer, charset: "utf8mb4", force: :cascade do |t| t.integer "group_order_article_id", default: 0, null: false t.integer "quantity", default: 0 t.integer "tolerance", default: 0 @@ -179,7 +179,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_02_15_085312) do t.index ["group_order_article_id"], name: "index_group_order_article_quantities_on_group_order_article_id" end - create_table "group_order_articles", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| + create_table "group_order_articles", id: :integer, charset: "utf8mb4", force: :cascade do |t| t.integer "group_order_id", default: 0, null: false t.integer "order_article_id", default: 0, null: false t.integer "quantity", default: 0, null: false @@ -192,7 +192,17 @@ ActiveRecord::Schema[7.0].define(version: 2023_02_15_085312) do t.index ["order_article_id"], name: "index_group_order_articles_on_order_article_id" end - create_table "group_orders", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| + create_table "group_order_invoices", charset: "utf8mb4", force: :cascade do |t| + t.integer "group_order_id" + t.bigint "invoice_number" + t.date "invoice_date" + t.string "payment_method" + t.datetime "created_at", precision: nil, null: false + t.datetime "updated_at", precision: nil, null: false + t.index ["group_order_id"], name: "index_group_order_invoices_on_group_order_id", unique: true + end + + create_table "group_orders", id: :integer, charset: "utf8mb4", force: :cascade do |t| t.integer "ordergroup_id" t.integer "order_id", default: 0, null: false t.decimal "price", precision: 8, scale: 2, default: "0.0", null: false @@ -205,7 +215,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_02_15_085312) do t.index ["ordergroup_id"], name: "index_group_orders_on_ordergroup_id" end - create_table "groups", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| + create_table "groups", id: :integer, charset: "utf8mb4", force: :cascade do |t| t.string "type", default: "", null: false t.string "name", default: "", null: false t.string "description" @@ -227,10 +237,11 @@ ActiveRecord::Schema[7.0].define(version: 2023_02_15_085312) do t.date "break_end" t.boolean "role_invoices", default: false, null: false t.boolean "role_pickups", default: false, null: false + t.string "customer_number" t.index ["name"], name: "index_groups_on_name", unique: true end - create_table "invites", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| + create_table "invites", id: :integer, charset: "utf8mb4", force: :cascade do |t| t.string "token", default: "", null: false t.datetime "expires_at", precision: nil, null: false t.integer "group_id", default: 0, null: false @@ -239,7 +250,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_02_15_085312) do t.index ["token"], name: "index_invites_on_token" end - create_table "invoices", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| + create_table "invoices", id: :integer, charset: "utf8mb4", force: :cascade do |t| t.integer "supplier_id" t.string "number" t.date "date" @@ -257,7 +268,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_02_15_085312) do t.index ["supplier_id"], name: "index_invoices_on_supplier_id" end - create_table "links", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| + create_table "links", id: :integer, charset: "utf8mb4", force: :cascade do |t| t.string "name", null: false t.string "url", null: false t.integer "workgroup_id" @@ -265,7 +276,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_02_15_085312) do t.string "authorization" end - create_table "mail_delivery_status", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| + create_table "mail_delivery_status", id: :integer, charset: "utf8mb4", force: :cascade do |t| t.datetime "created_at", precision: nil t.string "email", null: false t.string "message", null: false @@ -274,13 +285,13 @@ ActiveRecord::Schema[7.0].define(version: 2023_02_15_085312) do t.index ["email"], name: "index_mail_delivery_status_on_email" end - create_table "memberships", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| + create_table "memberships", id: :integer, charset: "utf8mb4", force: :cascade do |t| t.integer "group_id", default: 0, null: false t.integer "user_id", default: 0, null: false t.index ["user_id", "group_id"], name: "index_memberships_on_user_id_and_group_id", unique: true end - create_table "message_recipients", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| + create_table "message_recipients", id: :integer, charset: "utf8mb4", force: :cascade do |t| t.integer "message_id", null: false t.integer "user_id", null: false t.integer "email_state", default: 0, null: false @@ -289,7 +300,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_02_15_085312) do t.index ["user_id", "read_at"], name: "index_message_recipients_on_user_id_and_read_at" end - create_table "messages", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| + create_table "messages", id: :integer, charset: "utf8mb4", force: :cascade do |t| t.integer "sender_id" t.string "subject", null: false t.boolean "private", default: false @@ -300,7 +311,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_02_15_085312) do t.binary "received_email", size: :medium end - create_table "oauth_access_grants", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| + create_table "oauth_access_grants", id: :integer, charset: "utf8mb4", force: :cascade do |t| t.integer "resource_owner_id", null: false t.integer "application_id", null: false t.string "token", null: false @@ -312,7 +323,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_02_15_085312) do t.index ["token"], name: "index_oauth_access_grants_on_token", unique: true end - create_table "oauth_access_tokens", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| + create_table "oauth_access_tokens", id: :integer, charset: "utf8mb4", force: :cascade do |t| t.integer "resource_owner_id" t.integer "application_id" t.string "token", null: false @@ -326,7 +337,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_02_15_085312) do t.index ["token"], name: "index_oauth_access_tokens_on_token", unique: true end - create_table "oauth_applications", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| + create_table "oauth_applications", id: :integer, charset: "utf8mb4", force: :cascade do |t| t.string "name", null: false t.string "uid", null: false t.string "secret", null: false @@ -338,7 +349,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_02_15_085312) do t.index ["uid"], name: "index_oauth_applications_on_uid", unique: true end - create_table "order_articles", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| + create_table "order_articles", id: :integer, charset: "utf8mb4", force: :cascade do |t| t.integer "order_id", default: 0, null: false t.integer "article_id", default: 0, null: false t.integer "quantity", default: 0, null: false @@ -352,7 +363,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_02_15_085312) do t.index ["order_id"], name: "index_order_articles_on_order_id" end - create_table "order_comments", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| + create_table "order_comments", id: :integer, charset: "utf8mb4", force: :cascade do |t| t.integer "order_id" t.integer "user_id" t.text "text" @@ -360,7 +371,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_02_15_085312) do t.index ["order_id"], name: "index_order_comments_on_order_id" end - create_table "orders", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| + create_table "orders", id: :integer, charset: "utf8mb4", force: :cascade do |t| t.integer "supplier_id" t.text "note" t.datetime "starts", precision: nil @@ -379,7 +390,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_02_15_085312) do t.index ["state"], name: "index_orders_on_state" end - create_table "page_versions", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| + create_table "page_versions", id: :integer, charset: "utf8mb4", force: :cascade do |t| t.integer "page_id" t.integer "lock_version" t.text "body" @@ -390,7 +401,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_02_15_085312) do t.index ["page_id"], name: "index_page_versions_on_page_id" end - create_table "pages", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| + create_table "pages", id: :integer, charset: "utf8mb4", force: :cascade do |t| t.string "title" t.text "body" t.string "permalink" @@ -404,20 +415,20 @@ ActiveRecord::Schema[7.0].define(version: 2023_02_15_085312) do t.index ["title"], name: "index_pages_on_title" end - create_table "periodic_task_groups", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| + create_table "periodic_task_groups", id: :integer, charset: "utf8mb4", force: :cascade do |t| t.date "next_task_date" t.datetime "created_at", precision: nil, null: false t.datetime "updated_at", precision: nil, null: false end - create_table "poll_choices", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| + create_table "poll_choices", id: :integer, charset: "utf8mb4", force: :cascade do |t| t.integer "poll_vote_id", null: false t.integer "choice", null: false t.integer "value", null: false t.index ["poll_vote_id", "choice"], name: "index_poll_choices_on_poll_vote_id_and_choice", unique: true end - create_table "poll_votes", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| + create_table "poll_votes", id: :integer, charset: "utf8mb4", force: :cascade do |t| t.integer "poll_id", null: false t.integer "user_id", null: false t.integer "ordergroup_id" @@ -427,7 +438,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_02_15_085312) do t.index ["poll_id", "user_id", "ordergroup_id"], name: "index_poll_votes_on_poll_id_and_user_id_and_ordergroup_id", unique: true end - create_table "polls", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| + create_table "polls", id: :integer, charset: "utf8mb4", force: :cascade do |t| t.integer "created_by_user_id", null: false t.string "name", null: false t.text "description" @@ -447,7 +458,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_02_15_085312) do t.index ["final_choice"], name: "index_polls_on_final_choice" end - create_table "printer_job_updates", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| + create_table "printer_job_updates", id: :integer, charset: "utf8mb4", force: :cascade do |t| t.integer "printer_job_id", null: false t.datetime "created_at", precision: nil, null: false t.string "state", null: false @@ -455,7 +466,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_02_15_085312) do t.index ["printer_job_id", "created_at"], name: "index_printer_job_updates_on_printer_job_id_and_created_at" end - create_table "printer_jobs", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| + create_table "printer_jobs", id: :integer, charset: "utf8mb4", force: :cascade do |t| t.integer "order_id" t.string "document", null: false t.integer "created_by_user_id", null: false @@ -464,7 +475,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_02_15_085312) do t.index ["finished_at"], name: "index_printer_jobs_on_finished_at" end - create_table "settings", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| + create_table "settings", id: :integer, charset: "utf8mb4", force: :cascade do |t| t.string "var", null: false t.text "value" t.integer "thing_id" @@ -474,7 +485,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_02_15_085312) do t.index ["thing_type", "thing_id", "var"], name: "index_settings_on_thing_type_and_thing_id_and_var", unique: true end - create_table "stock_changes", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| + create_table "stock_changes", id: :integer, charset: "utf8mb4", force: :cascade do |t| t.integer "stock_event_id" t.integer "order_id" t.integer "stock_article_id" @@ -484,7 +495,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_02_15_085312) do t.index ["stock_event_id"], name: "index_stock_changes_on_stock_event_id" end - create_table "stock_events", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| + create_table "stock_events", id: :integer, charset: "utf8mb4", force: :cascade do |t| t.integer "supplier_id" t.date "date" t.datetime "created_at", precision: nil @@ -494,14 +505,14 @@ ActiveRecord::Schema[7.0].define(version: 2023_02_15_085312) do t.index ["supplier_id"], name: "index_stock_events_on_supplier_id" end - create_table "supplier_categories", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| + create_table "supplier_categories", id: :integer, charset: "utf8mb4", force: :cascade do |t| t.string "name", null: false t.string "description" t.integer "financial_transaction_class_id" t.integer "bank_account_id" end - create_table "suppliers", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| + create_table "suppliers", id: :integer, charset: "utf8mb4", force: :cascade do |t| t.string "name", default: "", null: false t.string "address", default: "", null: false t.string "phone", default: "", null: false @@ -523,7 +534,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_02_15_085312) do t.index ["name"], name: "index_suppliers_on_name", unique: true end - create_table "tasks", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| + create_table "tasks", id: :integer, charset: "utf8mb4", force: :cascade do |t| t.string "name", default: "", null: false t.text "description" t.date "due_date" @@ -540,7 +551,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_02_15_085312) do t.index ["workgroup_id"], name: "index_tasks_on_workgroup_id" end - create_table "users", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| + create_table "users", id: :integer, charset: "utf8mb4", force: :cascade do |t| t.string "nick" t.string "password_hash", default: "", null: false t.string "password_salt", default: "", null: false diff --git a/spec/factories/group_order_invoice.rb b/spec/factories/group_order_invoice.rb new file mode 100644 index 00000000..89723873 --- /dev/null +++ b/spec/factories/group_order_invoice.rb @@ -0,0 +1,7 @@ +require 'factory_bot' + +FactoryBot.define do + factory :group_order_invoice do + group_order { create :group_order } + end +end diff --git a/spec/integration/group_order_invoices_spec.rb b/spec/integration/group_order_invoices_spec.rb new file mode 100644 index 00000000..f6ece77d --- /dev/null +++ b/spec/integration/group_order_invoices_spec.rb @@ -0,0 +1,72 @@ +require_relative '../spec_helper' + +feature GroupOrderInvoice, js: true do + let(:admin) { create :user, groups: [create(:workgroup, role_finance: true)] } + let(:user) { create :user, groups: [create(:ordergroup)] } + let(:article) { create :article, unit_quantity: 1 } + let(:order) { create :order, supplier: article.supplier, article_ids: [article.id], ends: Time.now } # need to ref article + let(:go) { create :group_order, order: order, ordergroup: user.ordergroup} + let(:oa) { order.order_articles.find_by_article_id(article.id) } + let(:ftt) { create :financial_transaction_type } + let(:goa) { create :group_order_article, group_order: go, order_article: oa } + + include ActiveJob::TestHelper + + before { login admin } + + after { clear_enqueued_jobs } + + it 'does not enqueue MailerJob when order is settled if tax_number or options not set' do + goa.update_quantities 2, 0 + oa.update_results! + visit confirm_finance_order_path(id: order.id) + click_link_or_button I18n.t('finance.balancing.confirm.clear') + expect(NotifyGroupOrderInvoiceJob).not_to have_been_enqueued + end + + it 'enqueues MailerJob when order is settled if tax_number or options are set' do + goa.update_quantities 2, 0 + oa.update_results! + order.reload + FoodsoftConfig[:group_order_invoices] = { use_automatic_invoices: true } + FoodsoftConfig[:contact][:tax_number] = 12_345_678 + visit confirm_finance_order_path(id: order.id, type: ftt) + expect(page).to have_selector(:link_or_button, I18n.t('finance.balancing.confirm.clear')) + click_link_or_button I18n.t('finance.balancing.confirm.clear') + expect(NotifyGroupOrderInvoiceJob).to have_been_enqueued + end + + it 'generates Group Order Invoice when order is closed if tax_number is set' do + goa.update_quantities 2, 0 + oa.update_results! + FoodsoftConfig[:contact][:tax_number] = 12_345_678 + order.update!(state: 'closed') + go.reload + order.reload + visit finance_order_index_path + expect(page).to have_selector(:link_or_button, I18n.t('activerecord.attributes.group_order_invoice.links.generate')) + click_link_or_button I18n.t('activerecord.attributes.group_order_invoice.links.generate') + expect(GroupOrderInvoice.all.count).to eq(1) + end + + it 'generates multiple Group Order Invoice for order when order is closed if tax_number is set' do + goa.update_quantities 2, 0 + oa.update_results! + FoodsoftConfig[:contact][:tax_number] = 12_345_678 + order.update!(state: 'closed') + order.reload + visit finance_order_index_path + expect(page).to have_selector(:link_or_button, I18n.t('activerecord.attributes.group_order_invoice.links.generate_with_date')) + click_link_or_button I18n.t('activerecord.attributes.group_order_invoice.links.generate_with_date') + expect(GroupOrderInvoice.all.count).to eq(1) + end + + it 'does not generate Group Order Invoice when order is closed if tax_number not set' do + goa.update_quantities 2, 0 + oa.update_results! + order.update!(state: 'closed') + order.reload + visit finance_order_index_path + expect(page).to have_content(I18n.t('activerecord.attributes.group_order_invoice.tax_number_not_set')) + end +end diff --git a/spec/models/group_order_invoice_spec.rb b/spec/models/group_order_invoice_spec.rb new file mode 100644 index 00000000..24bfcf7e --- /dev/null +++ b/spec/models/group_order_invoice_spec.rb @@ -0,0 +1,59 @@ +require_relative '../spec_helper' + +describe GroupOrderInvoice do + let(:user) { create :user, groups: [create(:ordergroup)] } + let(:supplier) { create :supplier } + let(:article) { create :article, supplier: supplier } + let(:order) { create :order } + let(:group_order) { create :group_order, order: order, ordergroup: user.ordergroup } + + describe 'erroneous group order invoice' do + let(:goi) { create :group_order_invoice, group_order_id: group_order.id } + it 'does not create group order invoice if tax_number not set' do + expect { goi }.to raise_error(ActiveRecord::RecordInvalid, /.*/) + end + end + + describe 'valid group order invoice' do + before do + FoodsoftConfig[:contact][:tax_number] = 123_457_8 + end + + invoice_number1 = Time.now.strftime("%Y%m%d") + '0001' + invoice_number2 = Time.now.strftime("%Y%m%d") + '0002' + + let(:user2) { create :user, groups: [create(:ordergroup)] } + + let(:goi1) { create :group_order_invoice, group_order_id: group_order.id } + let(:goi2) { create :group_order_invoice, group_order_id: group_order.id } + + let(:group_order2) { create :group_order, order: order, ordergroup: user2.ordergroup } + + let(:goi3) { create :group_order_invoice, group_order_id: group_order2.id } + let(:goi4) { create :group_order_invoice, group_order_id: group_order2.id, invoice_number: invoice_number1 } + + it 'creates group order invoice if tax_number is set' do + expect(goi1).to be_valid + end + + it 'sets invoice_number according to date' do + number = Time.now.strftime("%Y%m%d") + '0001' + expect(goi1.invoice_number).to eq(number.to_i) + end + + it 'fails to create if group_order_id is used multiple times for creation' do + expect(goi1.group_order.id).to eq(group_order.id) + expect { goi2 }.to raise_error(ActiveRecord::RecordNotUnique) + end + + it 'creates two different group order invoice with different invoice_numbers' do + expect(goi1.invoice_number).to eq(invoice_number1.to_i) + expect(goi3.invoice_number).to eq(invoice_number2.to_i) + end + + it 'fails to create two different group order invoice with same invoice_numbers' do + goi1 + expect { goi4 }.to raise_error(ActiveRecord::RecordInvalid) + end + end +end From f29ab603b604dc428f1742e71abfa2ce5b852760 Mon Sep 17 00:00:00 2001 From: viehlieb Date: Thu, 28 Sep 2023 15:43:12 +0200 Subject: [PATCH 100/105] repair garbage collected tempfile --- .../group_order_invoices_controller.rb | 62 +++++++++---------- 1 file changed, 30 insertions(+), 32 deletions(-) diff --git a/app/controllers/group_order_invoices_controller.rb b/app/controllers/group_order_invoices_controller.rb index 2e5a8408..885082ee 100644 --- a/app/controllers/group_order_invoices_controller.rb +++ b/app/controllers/group_order_invoices_controller.rb @@ -3,20 +3,28 @@ class GroupOrderInvoicesController < ApplicationController before_action :authenticate_finance def show - begin - @group_order_invoice = GroupOrderInvoice.find(params[:id]) - if FoodsoftConfig[:contact][:tax_number] - respond_to do |format| - format.pdf do - send_group_order_invoice_pdf @group_order_invoice if FoodsoftConfig[:contact][:tax_number] - end - end - else - raise RecordInvalid + @group_order_invoice = GroupOrderInvoice.find(params[:id]) + raise RecordInvalid unless FoodsoftConfig[:contact][:tax_number] + + respond_to do |format| + format.pdf do + send_group_order_invoice_pdf @group_order_invoice if FoodsoftConfig[:contact][:tax_number] end - rescue ActiveRecord::RecordInvalid => error - redirect_back fallback_location: root_path, notice: 'Something went wrong', alert: I18n.t('errors.general_msg', msg: "#{error} " + I18n.t('errors.check_tax_number')) end + rescue ActiveRecord::RecordInvalid => e + redirect_back fallback_location: root_path, notice: 'Something went wrong', alert: I18n.t('errors.general_msg', msg: "#{e} " + I18n.t('errors.check_tax_number')) + end + + def create + go = GroupOrder.find(params[:group_order]) + @order = go.order + GroupOrderInvoice.find_or_create_by!(group_order_id: go.id) + respond_to do |format| + format.js + end + redirect_back fallback_location: root_path + rescue StandardError => e + redirect_back fallback_location: root_path, notice: 'Something went wrong', :alert => I18n.t('errors.general_msg', :msg => e) end def destroy @@ -45,43 +53,33 @@ class GroupOrderInvoicesController < ApplicationController end end - def create - go = GroupOrder.find(params[:group_order]) - @order = go.order - GroupOrderInvoice.find_or_create_by!(group_order_id: go.id) - respond_to do |format| - format.js - end - redirect_back fallback_location: root_path - rescue => error - redirect_back fallback_location: root_path, notice: 'Something went wrong', :alert => I18n.t('errors.general_msg', :msg => error) - end - def download_all order = Order.find(params[:order_id]) invoices = order.group_orders.map(&:group_order_invoice) pdf = {} - + file_paths = [] temp_file = Tempfile.new("all_invoices_for_order_#{order.id}.zip") - Zip::File.open(temp_file.path, Zip::File::CREATE) do |zipfile| invoices.each do |invoice| pdf = create_invoice_pdf(invoice) - invoice_file = Tempfile.new("#{pdf.filename}") - File.open(invoice_file.path, 'w:ASCII-8BIT') do |file| + file_path = File.join("tmp", pdf.filename) + File.open(file_path, 'w:ASCII-8BIT') do |file| file.write(pdf.to_pdf) end - zipfile.add("#{pdf.filename}", invoice_file.path) unless zipfile.find_entry("#{pdf.filename}") + file_paths << file_path + zipfile.add(pdf.filename, file_path) unless zipfile.find_entry(pdf.filename) end end zip_data = File.read(temp_file.path) - + file_paths.each do |file_path| + File.delete(file_path) + end respond_to do |format| - format.html { + format.html do send_data(zip_data, type: 'application/zip', filename: "#{l order.ends, format: :file}-#{order.supplier.name}-#{order.id}.zip", disposition: 'attachment') - } + end end end end From 90e06a475ffd7dd4a10821ffefe0beafe1c2a8b7 Mon Sep 17 00:00:00 2001 From: viehlieb Date: Thu, 28 Sep 2023 22:42:52 +0200 Subject: [PATCH 101/105] fix deposit is net value --- app/documents/group_order_invoice_pdf.rb | 4 ++-- app/models/concerns/price_calculation.rb | 12 ++++++---- app/models/order.rb | 16 ++++++------- app/models/order_article.rb | 8 +++++++ .../balancing/_order_article.html.haml | 18 +++++++++++---- app/views/finance/balancing/_summary.haml | 23 ++++++++++++------- app/views/orders/_articles.html.haml | 9 +++++++- app/views/orders/show.html.haml | 4 ++-- config/locales/de.yml | 10 ++++---- spec/models/article_spec.rb | 5 +++- 10 files changed, 75 insertions(+), 34 deletions(-) diff --git a/app/documents/group_order_invoice_pdf.rb b/app/documents/group_order_invoice_pdf.rb index 899d6cf8..902fd03e 100644 --- a/app/documents/group_order_invoice_pdf.rb +++ b/app/documents/group_order_invoice_pdf.rb @@ -187,12 +187,12 @@ class GroupOrderInvoicePdf < RenderPdf number_to_currency(goa_total_gross)] if separate_deposits && order_article.price.deposit > 0.0 - goa_deposit = goa.result * order_article.price.deposit + goa_deposit = goa.result * order_article.price.net_deposit_price goa_total_deposit = goa.result * order_article.price.fc_deposit_price data << ["zzgl. Pfand", goa.result.to_i, - number_to_currency(order_article.price.deposit), + number_to_currency(order_article.price.net_deposit_price), number_to_currency(goa_deposit), tax.to_s + '%', number_to_currency(goa_total_deposit)] diff --git a/app/models/concerns/price_calculation.rb b/app/models/concerns/price_calculation.rb index 47c28bc6..1bd6aa7e 100644 --- a/app/models/concerns/price_calculation.rb +++ b/app/models/concerns/price_calculation.rb @@ -4,15 +4,15 @@ module PriceCalculation # Gross price = net price + deposit + tax. # @return [Number] Gross price. def gross_price - add_percent(price + deposit, tax) + add_percent(price, tax) + deposit end def gross_price_without_deposit add_percent(price, tax) end - def gross_deposit_price - add_percent(deposit, tax) + def net_deposit_price + remove_percent(deposit, tax) end # @return [Number] Price for the foodcoop-member. @@ -25,11 +25,15 @@ module PriceCalculation end def fc_deposit_price - add_percent(gross_deposit_price, FoodsoftConfig[:price_markup].to_i) + add_percent(deposit, FoodsoftConfig[:price_markup].to_i) end private + def remove_percent(value, percent) + (value / ((percent * 0.01) + 1)).round(2) + end + def add_percent(value, percent) (value * ((percent * 0.01) + 1)).round(2) end diff --git a/app/models/order.rb b/app/models/order.rb index e275519b..e071bee0 100644 --- a/app/models/order.rb +++ b/app/models/order.rb @@ -207,7 +207,7 @@ class Order < ApplicationRecord # :fc, guess what... def sum(type = :gross) total = 0 - if %i[net gross gross_deposit fc_deposit deposit fc].include?(type) + if %i[net gross net_deposit gross_without_deposit fc_without_deposit fc_deposit deposit fc].include?(type) for oa in order_articles.ordered.includes(:article, :article_price) quantity = oa.units * oa.price.unit_quantity case type @@ -215,10 +215,14 @@ class Order < ApplicationRecord total += quantity * oa.price.price when :gross total += quantity * oa.price.gross_price + when :gross_without_deposit + total += quantity * oa.price.gross_price_without_deposit when :fc total += quantity * oa.price.fc_price - when :gross_deposit - total += quantity * oa.price.gross_deposit_price + when :fc_without_deposit + total += quantity * oa.price.fc_price_without_deposit + when :net_deposit + total += quantity * oa.price.net_deposit_price when :fc_deposit total += quantity * oa.price.fc_deposit_price when :deposit @@ -230,11 +234,7 @@ class Order < ApplicationRecord for goa in go.group_order_articles case type when :groups - total += if FoodsoftConfig[:group_order_invoices]&.[](:separate_deposits) - goa.result * (goa.order_article.price.fc_price + goa.order_article.price.fc_deposit_price) - else - goa.result * goa.order_article.price.fc_price - end + total += goa.result * goa.order_article.price.fc_price when :groups_without_markup total += goa.result * goa.order_article.price.gross_price end diff --git a/app/models/order_article.rb b/app/models/order_article.rb index 14193d15..2c16a56a 100644 --- a/app/models/order_article.rb +++ b/app/models/order_article.rb @@ -99,6 +99,14 @@ class OrderArticle < ApplicationRecord units * price.unit_quantity * price.gross_price end + def total_gross_price_without_deposit + units * price.unit_quantity * price.gross_price_without_deposit + end + + def total_deposit_price + units * price.unit_quantity * price.deposit + end + def ordered_quantities_different_from_group_orders?(ordered_mark = '!', billed_mark = '?', received_mark = '?') if !units_received.nil? (units_received * price.unit_quantity) == group_orders_sum[:quantity] ? false : received_mark diff --git a/app/views/finance/balancing/_order_article.html.haml b/app/views/finance/balancing/_order_article.html.haml index 47db3e31..b48321bc 100644 --- a/app/views/finance/balancing/_order_article.html.haml +++ b/app/views/finance/balancing/_order_article.html.haml @@ -13,12 +13,22 @@ / = number_to_currency(order_article.total_price, :unit => "") %td - = number_to_currency(order_article.price.gross_price, :unit => "") + - if FoodsoftConfig[:group_order_invoices]&.[](:separate_deposits) + = number_to_currency(order_article.price.gross_price_without_deposit, :unit => "") + :plain + / + = number_to_currency(order_article.total_gross_price_without_deposit, :unit => "") + -else + = number_to_currency(order_article.price.gross_price, :unit => "") + :plain + / + = number_to_currency(order_article.total_gross_price, :unit => "") +%td= number_to_percentage(order_article.price.tax) unless order_article.price.tax.zero? +%td + = number_to_currency(order_article.price.deposit, :unit => "") unless order_article.price.deposit.zero? :plain / - = number_to_currency(order_article.total_gross_price, :unit => "") -%td= number_to_percentage(order_article.price.tax) unless order_article.price.tax.zero? -%td= number_to_currency(order_article.price.deposit, :unit => "") unless order_article.price.deposit.zero? + = number_to_currency(order_article.total_deposit_price, :unit => "") unless order_article.price.deposit.zero? %td = link_to t('ui.edit'), edit_order_order_article_path(order_article.order, order_article), remote: true, class: 'btn btn-mini' unless order_article.order.closed? diff --git a/app/views/finance/balancing/_summary.haml b/app/views/finance/balancing/_summary.haml index 6516aa69..42f3e3ed 100644 --- a/app/views/finance/balancing/_summary.haml +++ b/app/views/finance/balancing/_summary.haml @@ -6,22 +6,29 @@ %tr %td= t('.net_amount') %td.numeric= number_to_currency(order.sum(:net)) - %tr - %td= t('.gross_amount') - %td.numeric= number_to_currency(order.sum(:gross)) - %tr - %td= t('.fc_amount') - %td.numeric= number_to_currency(order.sum(:fc)) - if FoodsoftConfig[:group_order_invoices]&.[](:separate_deposits) + %tr + %td= t('.gross_amount') + %td.numeric= number_to_currency(order.sum(:gross_without_deposit)) + %tr + %td= t('.fc_amount') + %td.numeric= number_to_currency(order.sum(:fc_without_deposit)) %tr %td= t('.deposit') %td.numeric= number_to_currency(order.sum(:deposit)) %tr - %td= t('.gross_deposit') - %td.numeric= number_to_currency(order.sum(:gross_deposit)) + %td= t('.net_deposit') + %td.numeric= number_to_currency(order.sum(:net_deposit)) %tr %td= t('.fc_deposit') %td.numeric= number_to_currency(order.sum(:fc_deposit)) + - else + %tr + %td= t('.gross_amount') + %td.numeric= number_to_currency(order.sum(:gross)) + %tr + %td= t('.fc_amount') + %td.numeric= number_to_currency(order.sum(:fc)) %tr %td= t('.groups_amount') %td.numeric= number_to_currency(order.sum(:groups)) diff --git a/app/views/orders/_articles.html.haml b/app/views/orders/_articles.html.haml index 1c800cc7..ff010144 100644 --- a/app/views/orders/_articles.html.haml +++ b/app/views/orders/_articles.html.haml @@ -6,6 +6,8 @@ %th= t '.prices' - if order.stockit? %th= t '.units_ordered' + - if FoodsoftConfig[:group_order_invoices]&.[](:separate_deposits) + %th= t '.deposit' - else %th= 'Members' %th= t '.units_full' @@ -19,7 +21,10 @@ %td{:colspan => "9"} - order_articles.each do |order_article| - net_price = order_article.price.price - - gross_price = order_article.price.gross_price + - if FoodsoftConfig[:group_order_invoices]&.[](:separate_deposits) + - gross_price = order_article.price.gross_price_without_deposit + - else + - gross_price = order_article.price.gross_price - unit_quantity = order_article.price.unit_quantity - units = order_article.units - total_net += units * unit_quantity * net_price @@ -28,6 +33,8 @@ %td.name=h order_article.article.name %td= order_article.article.unit %td= "#{number_to_currency(net_price)} / #{number_to_currency(gross_price)}" + - if FoodsoftConfig[:group_order_invoices]&.[](:separate_deposits) + %td= "#{number_to_currency(order_article.price.deposit)}" - if order.stockit? %td= units - else diff --git a/app/views/orders/show.html.haml b/app/views/orders/show.html.haml index e76db249..1587f8e1 100644 --- a/app/views/orders/show.html.haml +++ b/app/views/orders/show.html.haml @@ -32,8 +32,8 @@ = raw t '.description2', ordergroups: ordergroup_count(@order), article_count: @order.order_articles.ordered.count, - net_sum: number_to_currency(@order.sum(:net)), - gross_sum: number_to_currency(@order.sum(:gross)) + net_sum: number_to_currency(@order.sum(:net) + @order.sum(:net_deposit)), + gross_sum: number_to_currency(@order.sum(:fc)) - unless @order.comments.blank? = link_to t('.comments_link'), '#comments' diff --git a/config/locales/de.yml b/config/locales/de.yml index 960ddf53..13a2e3dc 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -5,7 +5,7 @@ de: article_category: Kategorie availability: Artikel ist verfügbar? availability_short: verf. - deposit: Pfand + deposit: Pfand (brutto) fc_price: Endpreis fc_price_desc: Preis incl. MwSt, Pfand und Foodcoop-Aufschlag. fc_price_short: FC-Preis @@ -29,7 +29,7 @@ de: description: Beschreibung name: Name article_price: - deposit: Pfand + deposit: Pfand (brutto) price: Nettopreis tax: MwSt unit_quantity: Gebindegröße @@ -929,10 +929,12 @@ de: changed: Daten wurden verändert! duration: von %{starts} bis %{ends} fc_amount: 'FC-Betrag:' - deposit: 'Pfand netto:' + deposit: 'Pfand brutto:' gross_deposit: 'Pfand brutto:' + net_deposit: 'Pfand netto:' fc_deposit: 'Pfand FC-Betrag:' fc_profit: FC Gewinn + fc_amount: FC-Betrag (brutto) gross_amount: 'Bruttobetrag:' groups_amount: 'Gruppenbeträge:' net_amount: 'Nettobetrag:' @@ -1634,7 +1636,7 @@ de: pickup: und kann am %{pickup} abgeholt werden starts: läuft von %{starts} starts_ends: läuft von %{starts} bis %{ends} - description2: "%{ordergroups} haben %{article_count} Artikel mit einem Gesamtwert von %{net_sum} / %{gross_sum} (netto / brutto) bestellt." + description2: "%{ordergroups} haben %{article_count} Artikel mit einem Gesamtwert von %{net_sum} / %{gross_sum} (netto / brutto + fc + Pfand) bestellt." group_orders: 'Gruppenbestellungen:' search_placeholder: articles: Suche nach Artikeln ... diff --git a/spec/models/article_spec.rb b/spec/models/article_spec.rb index 3a810827..91a2f8ea 100644 --- a/spec/models/article_spec.rb +++ b/spec/models/article_spec.rb @@ -64,7 +64,10 @@ describe Article do article.tax = 12 expect(article.gross_price).to eq((article.price * 1.12).round(2)) article.deposit = 1.20 - expect(article.gross_price).to eq(((article.price + 1.20) * 1.12).round(2)) + if FoodsoftConfig[:group_order_invoices]&.[](:separate_deposits) + expect(article.gross_price_without_deposit).to eq((article.price * 1.12 + 1.20).round(2)) + expect(article.gross_price).to eq(((article.price + 1.20) * 1.12).round(2)) + end end it 'gross price >= net price' do From c3d56cdf3b8b6246844dff4203b350f21b376b18 Mon Sep 17 00:00:00 2001 From: viehlieb Date: Wed, 18 Oct 2023 23:25:14 +0200 Subject: [PATCH 102/105] fix sum table is agnostic to percentage on goi pdf add pickup to goi pdf add seeds tiny fixes --- Gemfile.lock | 3 +- app/documents/group_order_invoice_pdf.rb | 66 +++++++--- app/models/group_order_invoice.rb | 1 + app/views/finance/balancing/_summary.haml | 8 +- config/locales/de.yml | 5 +- db/seeds/demo.seeds.rb | 147 ++++++++++++++++++++++ docker-compose-dev.yml | 3 +- 7 files changed, 207 insertions(+), 26 deletions(-) create mode 100644 db/seeds/demo.seeds.rb diff --git a/Gemfile.lock b/Gemfile.lock index c66901cf..502e0589 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -600,7 +600,6 @@ GEM zeitwerk (2.6.8) PLATFORMS - ruby x86_64-linux DEPENDENCIES @@ -695,4 +694,4 @@ DEPENDENCIES whenever BUNDLED WITH - 2.4.5 + 2.3.8 diff --git a/app/documents/group_order_invoice_pdf.rb b/app/documents/group_order_invoice_pdf.rb index 902fd03e..9341eb18 100644 --- a/app/documents/group_order_invoice_pdf.rb +++ b/app/documents/group_order_invoice_pdf.rb @@ -53,16 +53,18 @@ class GroupOrderInvoicePdf < RenderPdf # invoice Date and nnvoice number bounding_box [margin_box.right - 200, margin_box.top - 150], width: 200 do - text I18n.t('documents.group_order_invoice_pdf.invoice_date', invoice_date: @options[:invoice_date].strftime(I18n.t('date.formats.default'))), align: :left - move_down 5 text I18n.t('documents.group_order_invoice_pdf.invoice_number', invoice_number: @options[:invoice_number]), align: :left + move_down 5 + text I18n.t('documents.group_order_invoice_pdf.invoice_date', invoice_date: @options[:invoice_date].strftime(I18n.t('date.formats.default'))), align: :left + if @options[:pickup] + move_down 5 + text I18n.t('documents.group_order_invoice_pdf.pickup_date', invoice_date: @options[:pickup].strftime(I18n.t('date.formats.default'))) + end end - move_down 15 - - # kind of the "body" of the invoice + move_down 20 text I18n.t('documents.group_order_invoice_pdf.payment_method', payment_method: @options[:payment_method]) - move_down 15 + text I18n.t('documents.group_order_invoice_pdf.table_headline') move_down 5 @@ -151,6 +153,7 @@ class GroupOrderInvoicePdf < RenderPdf tax_hash_net = Hash.new(0) # for summing up article net prices grouped into vat percentage tax_hash_gross = Hash.new(0) # same here with gross prices + tax_hash_fc = Hash.new(0) # same here with fc prices if separate_deposits total_deposit = 0 @@ -158,6 +161,7 @@ class GroupOrderInvoicePdf < RenderPdf tax_hash_deposit_gross = Hash.new(0) # for summing up deposit gross prices grouped into vat percentage tax_hash_deposit_net = Hash.new(0) # same here with gross prices + tax_hash_deposit_fc = Hash.new(0) # same here with fc prices end marge = FoodsoftConfig[:price_markup] @@ -177,38 +181,42 @@ class GroupOrderInvoicePdf < RenderPdf order_article = goa.order_article goa_total_net = goa.result * order_article.price.price - goa_total_gross = separate_deposits ? goa.total_price_without_deposit : goa.total_price + goa_total_fc = separate_deposits ? goa.total_price_without_deposit : goa.total_price + goa_total_gross = separate_deposits ? goa.result * order_article.price.gross_price_without_deposit : goa.result * order_article.price.gross_price data << [order_article.article.name, goa.result.to_i, number_to_currency(order_article.price.price), number_to_currency(goa_total_net), tax.to_s + '%', - number_to_currency(goa_total_gross)] + number_to_currency(goa_total_fc)] if separate_deposits && order_article.price.deposit > 0.0 - goa_deposit = goa.result * order_article.price.net_deposit_price + goa_net_deposit = goa.result * order_article.price.net_deposit_price + goa_deposit = goa.result * order_article.price.deposit goa_total_deposit = goa.result * order_article.price.fc_deposit_price data << ["zzgl. Pfand", goa.result.to_i, number_to_currency(order_article.price.net_deposit_price), - number_to_currency(goa_deposit), + number_to_currency(goa_net_deposit), tax.to_s + '%', number_to_currency(goa_total_deposit)] total_deposit += goa_deposit total_deposit_gross += goa_total_deposit - tax_hash_deposit_net[tax.to_i] += goa_deposit - tax_hash_deposit_gross[tax.to_i] += goa_total_deposit + tax_hash_deposit_net[tax.to_i] += goa_net_deposit + tax_hash_deposit_gross[tax.to_i] += goa_deposit + tax_hash_deposit_fc[tax.to_i] += goa_total_deposit end tax_hash_net[tax.to_i] += goa_total_net tax_hash_gross[tax.to_i] += goa_total_gross + tax_hash_fc[tax.to_i] += goa_total_fc total_net += goa_total_net - total_gross += goa_total_gross + total_gross += goa_total_fc end end @@ -226,10 +234,34 @@ class GroupOrderInvoicePdf < RenderPdf table.columns(1..6).align = :right end - sum = [[nil, nil, nil, "Netto", "MwSt", "Brutto"]] - [7, 19].each do |key| - sum << [nil, nil, "Produkte mit #{key}%", number_to_currency(tax_hash_net[key]), number_to_currency(tax_hash_gross[key] - tax_hash_net[key]), number_to_currency(tax_hash_gross[key])] - sum << [nil, nil, "Pfand mit #{key}%", number_to_currency(tax_hash_deposit_net[key]), number_to_currency(tax_hash_deposit_gross[key] - tax_hash_deposit_net[key]), number_to_currency(tax_hash_deposit_gross[key])] if separate_deposits + if marge > 0 + sum = [[nil, nil, "Netto", "MwSt", "FC-Marge", "Brutto"]] + else + sum = [[nil, nil, nil, "Netto", "MwSt", "Brutto"]] + end + + tax_hash_gross.keys.each do |key| + tmp_sum = [nil, "Produkte mit #{key}%", number_to_currency(tax_hash_net[key])] + if marge <= 0 + tmp_sum.unshift(nil) + end + tmp_sum << number_to_currency(tax_hash_gross[key] - tax_hash_net[key]) + if marge > 0 + tmp_sum << number_to_currency(tax_hash_fc[key] - tax_hash_gross[key]) + end + tmp_sum << number_to_currency(tax_hash_fc[key]) + sum << tmp_sum + + tmp_sum = [nil, "Pfand mit #{key}%", number_to_currency(tax_hash_deposit_net[key])] + if marge <= 0 + tmp_sum.unshift(nil) + end + tmp_sum << number_to_currency(tax_hash_deposit_gross[key] - tax_hash_deposit_net[key]) + if marge > 0 + tmp_sum << number_to_currency(tax_hash_deposit_fc[key] - tax_hash_deposit_gross[key]) + end + tmp_sum << number_to_currency(tax_hash_deposit_fc[key]) + sum << tmp_sum end total_deposit_gross ||= 0 diff --git a/app/models/group_order_invoice.rb b/app/models/group_order_invoice.rb index 21557161..867b3046 100644 --- a/app/models/group_order_invoice.rb +++ b/app/models/group_order_invoice.rb @@ -33,6 +33,7 @@ class GroupOrderInvoice < ApplicationRecord def load_data_for_invoice invoice_data = {} order = group_order.order + invoice_data[:pickup] = order.pickup invoice_data[:supplier] = order.supplier.name invoice_data[:ordergroup] = group_order.ordergroup invoice_data[:group_order] = group_order diff --git a/app/views/finance/balancing/_summary.haml b/app/views/finance/balancing/_summary.haml index 42f3e3ed..88b7f3c1 100644 --- a/app/views/finance/balancing/_summary.haml +++ b/app/views/finance/balancing/_summary.haml @@ -11,14 +11,14 @@ %td= t('.gross_amount') %td.numeric= number_to_currency(order.sum(:gross_without_deposit)) %tr - %td= t('.fc_amount') + %td= t('.fc_amount_without_deposit') %td.numeric= number_to_currency(order.sum(:fc_without_deposit)) - %tr - %td= t('.deposit') - %td.numeric= number_to_currency(order.sum(:deposit)) %tr %td= t('.net_deposit') %td.numeric= number_to_currency(order.sum(:net_deposit)) + %tr + %td= t('.deposit') + %td.numeric= number_to_currency(order.sum(:deposit)) %tr %td= t('.fc_deposit') %td.numeric= number_to_currency(order.sum(:fc_deposit)) diff --git a/config/locales/de.yml b/config/locales/de.yml index 13a2e3dc..447d827e 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -782,6 +782,7 @@ de: customer_number: 'Kundennummer: %{customer_number}' name: Bestellgruppe %{ordergroup} payment_method: 'Zahlungsart: %{payment_method}' + pickup_date: 'Lieferdatum: %{invoice_date}' sum_to_pay: Zu zahlen gesamt sum_to_pay_net: Zu zahlen gesamt (netto) sum_to_pay_gross: Gesamt @@ -928,13 +929,13 @@ de: summary: changed: Daten wurden verändert! duration: von %{starts} bis %{ends} - fc_amount: 'FC-Betrag:' + fc_amount: 'FC-Gesamtbetrag:' + fc_amount_without_deposit: 'FC-Betrag (ohne Pfand):' deposit: 'Pfand brutto:' gross_deposit: 'Pfand brutto:' net_deposit: 'Pfand netto:' fc_deposit: 'Pfand FC-Betrag:' fc_profit: FC Gewinn - fc_amount: FC-Betrag (brutto) gross_amount: 'Bruttobetrag:' groups_amount: 'Gruppenbeträge:' net_amount: 'Nettobetrag:' diff --git a/db/seeds/demo.seeds.rb b/db/seeds/demo.seeds.rb new file mode 100644 index 00000000..31bcdf98 --- /dev/null +++ b/db/seeds/demo.seeds.rb @@ -0,0 +1,147 @@ +require_relative 'seed_helper.rb' + +FinancialTransactionClass.create!(:id => 1, :name => 'Standard') +FinancialTransactionClass.create!(:id => 2, :name => 'Foodsoft') +FinancialTransactionType.create!(:id => 1, :name => "Foodcoop", :financial_transaction_class_id => 1) + +alice = User.create!(:id => 1, :nick => "alice", :password => "secret", :first_name => "Alice", :last_name => "Administrator", :email => "admin@foo.test", :phone => "+4421486548", :created_on => 'Wed, 15 Jan 2014 16:15:33 UTC +00:00') +bob = User.create!(:id => 2, :nick => "bob", :password => "secret", :first_name => "Bob", :last_name => "Doe", :email => "bob@doe.test", :created_on => 'Sun, 19 Jan 2014 17:38:22 UTC +00:00') + + +Workgroup.create!(:id => 1, :name => "Administrators", :description => "System administrators.", :account_balance => 0.0, :created_on => 'Wed, 15 Jan 2014 16:15:33 UTC +00:00', :role_admin => true, :role_suppliers => true, :role_article_meta => true, :role_finance => true, :role_orders => true, :next_weekly_tasks_number => 8, :ignore_apple_restriction => false) +Workgroup.create!(:id => 2, :name => "Finances", :account_balance => 0.0, :created_on => 'Sun, 19 Jan 2014 17:40:03 UTC +00:00', :role_admin => false, :role_suppliers => false, :role_article_meta => false, :role_finance => true, :role_orders => false, :next_weekly_tasks_number => 8, :ignore_apple_restriction => false) +Ordergroup.create!(:id => 5, :name => "Alice WG", :account_balance => 0.90E2, :created_on => 'Sat, 18 Jan 2014 00:38:48 UTC +00:00', :role_admin => false, :role_suppliers => false, :role_article_meta => false, :role_finance => false, :role_orders => false, :stats => { :jobs_size => 0, :orders_sum => 1021.74 }, :next_weekly_tasks_number => 8, :ignore_apple_restriction => true) +Ordergroup.create!(:id => 8, :name => "Bob's Family", :account_balance => 0.90E2, :created_on => 'Wed, 09 Apr 2014 12:23:29 UTC +00:00', :role_admin => false, :role_suppliers => false, :role_article_meta => false, :role_finance => false, :role_orders => false, :contact_person => "John Doe", :stats => { :jobs_size => 0, :orders_sum => 0 }, :next_weekly_tasks_number => 8, :ignore_apple_restriction => false) +FinancialTransaction.create!(:ordergroup_id => 5, :amount => 0.90E2, :note => "Bank transfer", :user_id => 2, :created_on => 'Mon, 17 Feb 2014 16:19:34 UTC +00:00', :financial_transaction_type_id => 1) +FinancialTransaction.create!(:ordergroup_id => 8, :amount => 0.90E2, :note => "Bank transfer", :user_id => 2, :created_on => 'Mon, 17 Feb 2014 16:19:34 UTC +00:00', :financial_transaction_type_id => 1) + +Membership.create!(:group_id => 1, :user_id => 1) +Membership.create!(:group_id => 5, :user_id => 1) +Membership.create!(:group_id => 2, :user_id => 2) +Membership.create!(:group_id => 8, :user_id => 2) + +supplier_category = SupplierCategory.create!(:id => 1, :name => "Other", :financial_transaction_class_id => 1) + +chocolate_supplier = Supplier.create!( + name: "Kollektiv CHOCK!", + address: "Grabower Straße 1\n12345 Berlin", + phone: "0123456789", + email: "info@bbakery.test", + supplier_category: supplier_category +) + +nkn_supplier = Supplier.create!( + name: "Naturgut Süd", + address: "Somewhere in Hamburg, maybe St. Pauli?", + phone: "0123434789", + email: "foodsoft@local-it.org", + supplier_category: supplier_category +) + +chocolate_category = ArticleCategory.create!(name: "Schokolade") +obst_category = ArticleCategory.create!(name: "Obst, Gemüse, Sprossen, Pilze") +nudeln_category = ArticleCategory.create!(name: "Nudeln, Trockenfrüchte, Müsli") +reis_category = ArticleCategory.create!(name: "Getreide, Ölsaaten. Nußkerne") + +Article.create!( + name: "Vollmilch-Schokolade", + supplier_id: chocolate_supplier.id, + article_category_id: chocolate_category.id, + manufacturer: "Grabower Süßwaren GmbH", + origin: "D", price: 3.0, tax: 7.0, + unit: "200g", unit_quantity: 5, + note: "bio, fairtrade, 40% Kakao, vegan", + availability: true, order_number: "1") + +Article.create!( + name: "Weiße Schokolade", + supplier_id: chocolate_supplier.id, + article_category_id: chocolate_category.id, + manufacturer: "Grabower Süßwaren GmbH", + origin: "D", price: 3.49, tax: 7.0, + unit: "200g", unit_quantity: 5, + note: "bio, fairtrade, 40% Kakao, vegan", + availability: true, order_number: "2") + +dark_chocolate = Article.create!( + name: "Dunkle Schokolade", + supplier_id: chocolate_supplier.id, + article_category_id: chocolate_category.id, + manufacturer: "Grabower Süßwaren GmbH", + origin: "D", price: 2.89, tax: 7.0, + unit: "200g", unit_quantity: 5, + note: "bio, fairtrade, 40% Kakao, vegan", + availability: true, order_number: "3") + +Article.create!( + name: "Himbeer-Schokolade", + supplier_id: chocolate_supplier.id, + article_category_id: chocolate_category.id, + manufacturer: "Grabower Süßwaren GmbH", + origin: "D", price: 2.89, tax: 7.0, + unit: "170g", unit_quantity: 4, + note: "bio, fairtrade, 40% Kakao, vegan", + availability: true, order_number: "4") + +previous_order = seed_order(supplier_id: chocolate_supplier.id, starts: 10.days.ago, ends: 7.days.ago) + +GroupOrderArticle.create!( + group_order: GroupOrder.create!(order_id: previous_order.id, ordergroup_id: 8), + order_article: previous_order.order_articles.find_by(article_id: dark_chocolate.id), + quantity: 5, tolerance: 0) + +previous_order.close!(alice) + +seed_order(supplier_id: chocolate_supplier.id, starts: 0.days.ago, ends: 7.days.from_now) + + +apple = Article.create!( + name: "Äpfel Elstar", + supplier_id: nkn_supplier.id, + article_category_id: obst_category.id, + manufacturer: "Obsthof Bruno Brugger", + origin: "D", price: 3.49, tax: 7.0, + unit: "1kg", unit_quantity: 10, + note: "lecker, fruchtig, demeter", + availability: true, order_number: "5") + +brokkoli = Article.create!( + name: "Brokkoli", + supplier_id: nkn_supplier.id, + article_category_id: obst_category.id, + manufacturer: "Fattoria degli Orsi", + origin: "IT", price: 2.89, tax: 7.0, + unit: "400g", unit_quantity: 6, + note: "gesund und lecker", + availability: true, order_number: "6") + +tomatoes = Article.create!( + name: "Tomaten", + supplier_id: nkn_supplier.id, + article_category_id: obst_category.id, + manufacturer: "Terra di Puglia", + origin: "IT", price: 2.89, tax: 7.0, + unit: "500g", unit_quantity: 20, + note: "pomodori italiani, demeter", + availability: true, order_number: "7") + +rice = Article.create!( + name: "Reis", + supplier_id: nkn_supplier.id, + article_category_id: reis_category.id, + manufacturer: "Finck", + origin: "D", price: 3.29, tax: 7.0, + unit: "3kg", unit_quantity: 10, + note: "Reis im Vorratssack, demeter", + availability: true, order_number: "8") + +spaghetti = Article.create!( + name: "Spaghetti", + supplier_id: nkn_supplier.id, + article_category_id: nudeln_category.id, + manufacturer: "Pastificio Zanellini spa", + origin: "D", price: 2.89, tax: 7.0, + unit: "500g", unit_quantity: 4, + note: "100% italienisches Hartweizengrieß", + availability: true, order_number: "9") + diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index b0a325db..1f93e6ec 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -6,12 +6,13 @@ services: command: ./proc-start web ports: - "3000:3000" + depends_on: + - mariadb foodsoft_worker: build: context: . dockerfile: Dockerfile-dev - platform: linux/x86_64 command: ./proc-start worker volumes: - bundle:/usr/local/bundle From f979face11c144429380636634486b5a8131005d Mon Sep 17 00:00:00 2001 From: viehlieb Date: Fri, 20 Oct 2023 10:02:01 +0200 Subject: [PATCH 103/105] fix nil when not separate_deposits --- app/documents/group_order_invoice_pdf.rb | 21 ++++++++++++--------- db/seeds/demo.seeds.rb | 2 +- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/app/documents/group_order_invoice_pdf.rb b/app/documents/group_order_invoice_pdf.rb index 9341eb18..1df35686 100644 --- a/app/documents/group_order_invoice_pdf.rb +++ b/app/documents/group_order_invoice_pdf.rb @@ -252,16 +252,19 @@ class GroupOrderInvoicePdf < RenderPdf tmp_sum << number_to_currency(tax_hash_fc[key]) sum << tmp_sum - tmp_sum = [nil, "Pfand mit #{key}%", number_to_currency(tax_hash_deposit_net[key])] - if marge <= 0 - tmp_sum.unshift(nil) + + if separate_deposits + tmp_sum = [nil, "Pfand mit #{key}%", number_to_currency(tax_hash_deposit_net[key])] + if marge <= 0 + tmp_sum.unshift(nil) + end + tmp_sum << number_to_currency(tax_hash_deposit_gross[key] - tax_hash_deposit_net[key]) + if marge > 0 + tmp_sum << number_to_currency(tax_hash_deposit_fc[key] - tax_hash_deposit_gross[key]) + end + tmp_sum << number_to_currency(tax_hash_deposit_fc[key]) + sum << tmp_sum end - tmp_sum << number_to_currency(tax_hash_deposit_gross[key] - tax_hash_deposit_net[key]) - if marge > 0 - tmp_sum << number_to_currency(tax_hash_deposit_fc[key] - tax_hash_deposit_gross[key]) - end - tmp_sum << number_to_currency(tax_hash_deposit_fc[key]) - sum << tmp_sum end total_deposit_gross ||= 0 diff --git a/db/seeds/demo.seeds.rb b/db/seeds/demo.seeds.rb index 31bcdf98..6d2bc135 100644 --- a/db/seeds/demo.seeds.rb +++ b/db/seeds/demo.seeds.rb @@ -34,7 +34,7 @@ nkn_supplier = Supplier.create!( name: "Naturgut Süd", address: "Somewhere in Hamburg, maybe St. Pauli?", phone: "0123434789", - email: "foodsoft@local-it.org", + email: "foodsoft@test.org", supplier_category: supplier_category ) From 505cf8c2f3a1620803662a3c63328673a4af6ac3 Mon Sep 17 00:00:00 2001 From: viehlieb Date: Fri, 20 Oct 2023 10:30:50 +0200 Subject: [PATCH 104/105] enlarge articles column in goi pdf --- app/documents/group_order_invoice_pdf.rb | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/app/documents/group_order_invoice_pdf.rb b/app/documents/group_order_invoice_pdf.rb index 1df35686..f69c36c9 100644 --- a/app/documents/group_order_invoice_pdf.rb +++ b/app/documents/group_order_invoice_pdf.rb @@ -112,6 +112,7 @@ class GroupOrderInvoicePdf < RenderPdf table.cells.border_color = '666666' table.row(0).column(0..4).width = 80 + table.row(0).column(0).width = 180 table.row(0).border_bottom_width = 2 table.columns(1).align = :right table.columns(1..6).align = :right @@ -131,6 +132,7 @@ class GroupOrderInvoicePdf < RenderPdf table.row(0..-1).columns(0..1).border_width = 0 table.rows(0..-1).columns(0..4).width = 80 + table.row(0).column(0).width = 180 table.row(0).column(-1).style(font_style: :bold) table.row(0).column(-2).style(font_style: :bold) table.row(0).column(-1).size = fontsize(10) @@ -228,7 +230,11 @@ class GroupOrderInvoicePdf < RenderPdf table.cells.border_width = 1 table.cells.border_color = '666666' table.row(0).columns(0..6).style(background_color: 'cccccc', font_style: :bold) - table.rows(0..-1).columns(0..6).width = 80 + table.rows(0..-1).columns(2..6).width = 80 + table.rows(0..-1).column(0).width = 160 + table.rows(0..-1).column(1).width = 40 + table.rows(0..-1).column(4).width = 60 + table.rows(0..-1).column(5).width = 90 table.row(0).border_bottom_width = 2 table.columns(1).align = :right table.columns(1..6).align = :right @@ -278,9 +284,14 @@ class GroupOrderInvoicePdf < RenderPdf table.cells.border_color = '666666' table.row(0).columns(2..6).style(align: :bottom) table.row(0).border_bottom_width = 2 - table.row(0..-1).columns(0..1).border_width = 0 + table.row(0..-1).columns(0).border_width = 0 + table.row(0..-1).columns(1).border_width = 0 if marge <= 0 + table.rows(0..-1).columns(2..6).width = 80 + table.rows(0..-1).column(0).width = 100 + table.rows(0..-1).column(1).width = 100 + table.rows(0..-1).column(4).width = 60 + table.rows(0..-1).column(5).width = 90 - table.rows(0..-1).columns(0..6).width = 80 table.row(-1).column(-1).style(font_style: :bold) table.row(-1).column(-2).style(font_style: :bold) table.row(-1).column(-1).size = fontsize(10) From de6643722a9d4c3120ea4e63919704a67dda4ae3 Mon Sep 17 00:00:00 2001 From: viehlieb Date: Fri, 20 Oct 2023 10:38:58 +0200 Subject: [PATCH 105/105] enlarge column width even moregoi pdf --- app/documents/group_order_invoice_pdf.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/documents/group_order_invoice_pdf.rb b/app/documents/group_order_invoice_pdf.rb index f69c36c9..86512b8a 100644 --- a/app/documents/group_order_invoice_pdf.rb +++ b/app/documents/group_order_invoice_pdf.rb @@ -231,7 +231,7 @@ class GroupOrderInvoicePdf < RenderPdf table.cells.border_color = '666666' table.row(0).columns(0..6).style(background_color: 'cccccc', font_style: :bold) table.rows(0..-1).columns(2..6).width = 80 - table.rows(0..-1).column(0).width = 160 + table.rows(0..-1).column(0).width = 170 table.rows(0..-1).column(1).width = 40 table.rows(0..-1).column(4).width = 60 table.rows(0..-1).column(5).width = 90 @@ -287,7 +287,7 @@ class GroupOrderInvoicePdf < RenderPdf table.row(0..-1).columns(0).border_width = 0 table.row(0..-1).columns(1).border_width = 0 if marge <= 0 table.rows(0..-1).columns(2..6).width = 80 - table.rows(0..-1).column(0).width = 100 + table.rows(0..-1).column(0).width = 110 table.rows(0..-1).column(1).width = 100 table.rows(0..-1).column(4).width = 60 table.rows(0..-1).column(5).width = 90