diff --git a/Gemfile b/Gemfile index 8ac49e82..83cd4333 100644 --- a/Gemfile +++ b/Gemfile @@ -18,6 +18,7 @@ end gem 'jquery-rails' gem 'select2-rails' gem 'bootstrap-datepicker-rails' +gem 'rails-assets-listjs', '0.2.0.beta.4' # remember to maintain list.*.js plugins and template engines on update gem 'mysql2' gem 'prawn' diff --git a/Gemfile.lock b/Gemfile.lock index a9112511..83e3a43a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -202,6 +202,8 @@ GEM activesupport (= 3.2.14) bundler (~> 1.0) railties (= 3.2.14) + rails-assets-listjs (0.2.0.beta.4) + railties (>= 3.1) rails-settings-cached (0.2.4) rails (>= 3.0.0) railties (3.2.14) @@ -346,6 +348,7 @@ DEPENDENCIES prawn quiet_assets rails (~> 3.2.9) + rails-assets-listjs (= 0.2.0.beta.4) rails-settings-cached (= 0.2.4) resque rspec-core diff --git a/README.md b/README.md index d3107dae..e7be634b 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,9 @@ FoodSoft ========= -[![Build Status](https://travis-ci.org/foodcoops/foodsoft.png?branch=tests-rspec)](https://travis-ci.org/foodcoops/foodsoft) +[![Build Status](https://travis-ci.org/foodcoops/foodsoft.png)](https://travis-ci.org/foodcoops/foodsoft) [![Code Climate](https://codeclimate.com/github/foodcoops/foodsoft.png)](https://codeclimate.com/github/foodcoops/foodsoft) [![Dependency Status](https://gemnasium.com/foodcoops/foodsoft.png)](https://gemnasium.com/foodcoops/foodsoft) +[![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/foodcoops/foodsoft/trend.png)](https://bitdeli.com/free "Bitdeli Badge") Web-based software to manage a non-profit food coop (product catalog, ordering, accounting, job scheduling). diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 81b0f62c..39b99a65 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -7,6 +7,10 @@ //= require bootstrap-datepicker/locales/bootstrap-datepicker.de //= require bootstrap-datepicker/locales/bootstrap-datepicker.nl //= require jquery.observe_field +//= require list +//= require list.unlist +//= require list.delay +//= require list.reset //= require rails.validations //= require rails.validations.simple_form //= require_self diff --git a/app/assets/javascripts/list.delay.js b/app/assets/javascripts/list.delay.js new file mode 100644 index 00000000..72ac425c --- /dev/null +++ b/app/assets/javascripts/list.delay.js @@ -0,0 +1,50 @@ +// for use with listjs 0.2.0 +// https://github.com/javve/list.js + +(function(window, undefined) { + +window.List.prototype.plugins.delay = function(locals, options) { + var list = this; + + this.searchTimeout = undefined; + + var init = { + start: function(options) { + this.defaults(options); + this.callbacks(options); + this.onload(options); + }, + defaults: function(options) { + options.delayedSearchClass = options.delayedSearchClass || 'delayed-search'; + options.delayedSearchTime = options.delayedSearchTime || 500; + }, + callbacks: function(options) { + $('.' + options.delayedSearchClass, list.listContainer).keyup(list.searchDelayStart); + }, + onload: function(options) { + var initialSearchTerm = $('.' + options.delayedSearchClass, list.listContainer).val(); + if('' != initialSearchTerm) { + list.search(initialSearchTerm); + } + } + }; + + this.searchDelayStart = function(searchString, columns) { + // TODO: if keycode corresponds to 'ENTER' ? skip delay + clearTimeout(list.searchTimeout); + list.searchTimeout = window.setTimeout( + function() {list.searchDelayEnd(searchString, columns)}, + options.delayedSearchTime + ); + + $(list.listContainer).trigger('updateComing'); + }; + + this.searchDelayEnd = function(searchString, columns) { + list.search(searchString, columns); + }; + + init.start(options); +} + +})(window); diff --git a/app/assets/javascripts/list.reset.js b/app/assets/javascripts/list.reset.js new file mode 100644 index 00000000..9482f32c --- /dev/null +++ b/app/assets/javascripts/list.reset.js @@ -0,0 +1,42 @@ +// for use with listjs 0.2.0 +// https://github.com/javve/list.js + +(function(window, undefined) { + +window.List.prototype.plugins.reset = function(locals, options) { + var list = this; + + var init = { + start: function(options) { + this.defaults(options); + this.callbacks(options); + }, + defaults: function(options) { + options.highlightClass = options.highlightClass || 'btn-primary'; + options.resetSearchClass = options.resetSearchClass || 'reset-search'; + options.resettableClass = options.resettableClass || 'resettable'; + }, + callbacks: function(options) { + $('.' + options.resetSearchClass, list.listContainer).click(list.resetSearch); + list.on('updated', list.highlightResetButton); + + $(list.listContainer).on('updateComing', function() { + list.highlightResetButton(false); + }); + } + }; + + this.highlightResetButton = function(highlightEnabled) { + highlightEnabled = (undefined === highlightEnabled) ? (list.searched) : (highlightEnabled); + $('.' + options.resetSearchClass, list.listContainer).toggleClass(options.highlightClass, highlightEnabled); + }; + + this.resetSearch = function() { + $('.' + options.resettableClass, list.listContainer).val(''); + list.search(''); + }; + + init.start(options); +} + +})(window); diff --git a/app/assets/javascripts/list.unlist.js b/app/assets/javascripts/list.unlist.js new file mode 100644 index 00000000..b40cb98f --- /dev/null +++ b/app/assets/javascripts/list.unlist.js @@ -0,0 +1,124 @@ +// for use with listjs 0.2.0 +// https://github.com/javve/list.js + +/******************************************************************************* +******************************************************************************** + +The following code is a modification of list.js. It was created by copy-pasting +the original code with the copyright notice below. + +******************************************************************************** +*******************************************************************************/ + + + +/******************************************************************************* +Begin copyright notice of the original code +*******************************************************************************/ + +/* +ListJS Beta 0.2.0 +By Jonny Strömberg (www.jonnystromberg.com, www.listjs.com) + +OBS. The API is not frozen. It MAY change! + +License (MIT) + +Copyright (c) 2011 Jonny Strömberg http://jonnystromberg.com + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of the Software, +and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. +*/ + +/******************************************************************************* +End copyright notice of the original code +*******************************************************************************/ + + + +(function(w, undefined) { +/******************************************************************************* +Begin copy-pasted and modified code +*******************************************************************************/ + +// * template engine which adds class 'unlisted' instead of removing from DOM +// * especially useful in case of formulars +// * uses jQuery's $ +w.List.prototype.templateEngines.unlist = function(list, settings) { + var h = w.ListJsHelpers; + + // start with standard engine, override specific methods afterwards + this.superClass = w.List.prototype.templateEngines.standard; + this.superClass(list, settings); + + // todo refer to listjs code instead of copy-pasting + var listSource = h.getByClass(settings.listClass, list.listContainer, true); + var templater = this; + var ensure = { + created: function(item) { + if(item.elm === undefined) { + templater.create(item); + } + } + }; + + var init = { + start: function(options) { + this.defaults(options); + this.callbacks(options); + }, + defaults: function(options) { + options.listHeadingsClass = options.listHeadingsClass || 'list-heading'; + }, + callbacks: function(options) { + list.on('updated', templater.updateListHeadings); + } + }; + + this.show = function(item) { + ensure.created(item); + listSource.appendChild(item.elm); // append item (or move it to the end) + $(item.elm).removeClass('unlisted'); + }; + this.hide = function(item) { + ensure.created(item); + $(item.elm).addClass('unlisted'); + listSource.appendChild(item.elm); + }; + this.clear = function() { + $(listSource.childNodes).addClass('unlisted'); + }; + + this.updateListHeadings = function() { + var headSel = '.' + settings.listHeadingsClass; + + $(headSel, listSource).each(function() { + var listedCount = $(this).nextUntil(headSel, ':not(.unlisted)').length; + $(this).toggleClass('unlisted', 0==listedCount); + }); + }; + + init.start(settings); +}; + +/******************************************************************************* +End copy-pasted and modified code +*******************************************************************************/ +})(window); diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index 24a5b6c4..674a635c 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -3,4 +3,5 @@ *= require select2 *= require token-input-bootstrappy *= require bootstrap-datepicker +*= require list.unlist */ \ No newline at end of file diff --git a/app/assets/stylesheets/bootstrap_and_overrides.css.less b/app/assets/stylesheets/bootstrap_and_overrides.css.less index 408f6c65..6ad25972 100644 --- a/app/assets/stylesheets/bootstrap_and_overrides.css.less +++ b/app/assets/stylesheets/bootstrap_and_overrides.css.less @@ -237,3 +237,8 @@ tr.unavailable { margin-bottom: 15px } } + +// allow buttons as input add-on (with proper height) +.input-append button.add-on { + height: inherit; +} diff --git a/app/assets/stylesheets/list.unlist.css b/app/assets/stylesheets/list.unlist.css new file mode 100644 index 00000000..9fad9603 --- /dev/null +++ b/app/assets/stylesheets/list.unlist.css @@ -0,0 +1,3 @@ +.list .unlisted:not(.no-unlist) { + display: none; +} \ No newline at end of file diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 96b2e510..9c77fe48 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -26,7 +26,7 @@ class ApplicationController < ActionController::Base def deny_access session[:return_to] = request.original_url - redirect_to login_url, :alert => 'Access denied!' + redirect_to login_url, :alert => I18n.t('application.controller.error_denied') end private @@ -37,7 +37,7 @@ class ApplicationController < ActionController::Base # No user at all: redirect to login page. session[:user_id] = nil session[:return_to] = request.original_url - redirect_to login_url, :alert => 'Authentication required!' + redirect_to login_url, :alert => I18n.t('application.controller.error_authn') else # We have an authenticated user, now check role... # Roles gets the user through his memberships. @@ -83,7 +83,7 @@ class ApplicationController < ActionController::Base def authenticate_membership_or_admin @group = Group.find(params[:id]) unless @group.member?(@current_user) or @current_user.role_admin? - redirect_to root_path, alert: "Diese Aktion ist nur für Mitglieder der Gruppe erlaubt!" + redirect_to root_path, alert: I18n.t('application.controller.error_members_only') end end diff --git a/app/controllers/finance/balancing_controller.rb b/app/controllers/finance/balancing_controller.rb index 8528e4d3..8de1444c 100644 --- a/app/controllers/finance/balancing_controller.rb +++ b/app/controllers/finance/balancing_controller.rb @@ -10,7 +10,7 @@ class Finance::BalancingController < Finance::BaseController flash.now.alert = t('finance.balancing.new.alert') if @order.closed? @comments = @order.comments - @articles = @order.order_articles.ordered.includes(:article, :article_price, + @articles = @order.order_articles.ordered_or_member.includes(:article, :article_price, group_order_articles: {group_order: :ordergroup}) sort_param = params['sort'] || 'name' diff --git a/app/controllers/finance/financial_transactions_controller.rb b/app/controllers/finance/financial_transactions_controller.rb index a129a49d..8865958e 100644 --- a/app/controllers/finance/financial_transactions_controller.rb +++ b/app/controllers/finance/financial_transactions_controller.rb @@ -34,7 +34,7 @@ class Finance::FinancialTransactionsController < ApplicationController @financial_transaction = FinancialTransaction.new(params[:financial_transaction]) @financial_transaction.user = current_user @financial_transaction.add_transaction! - redirect_to finance_ordergroup_transactions_url(@ordergroup), notice: t('finance.financial_transactions.create.notice') + redirect_to finance_ordergroup_transactions_url(@ordergroup), notice: I18n.t('finance.financial_transactions.controller.create.notice') rescue ActiveRecord::RecordInvalid => error flash.now[:alert] = error.message render :action => :new @@ -44,16 +44,16 @@ class Finance::FinancialTransactionsController < ApplicationController end def create_collection - raise "Notiz wird benötigt!" if params[:note].blank? + raise I18n.t('finance.financial_transactions.controller.create_collection.error_note_required') if params[:note].blank? params[:financial_transactions].each do |trans| # ignore empty amount fields ... unless trans[:amount].blank? Ordergroup.find(trans[:ordergroup_id]).add_financial_transaction!(trans[:amount], params[:note], @current_user) end end - redirect_to finance_ordergroups_url, notice: t('finance.create_collection.create.notice') + redirect_to finance_ordergroups_url, notice: I18n.t('finance.financial_transactions.controller.create_collection.notice') rescue => error - redirect_to finance_new_transaction_collection_url, alert: t('finance.create_collection.create.alert', error: error.to_s) + redirect_to finance_new_transaction_collection_url, alert: I18n.t('finance.financial_transactions.controller.create_collection.alert', error: error.to_s) end protected diff --git a/app/controllers/finance/group_order_articles_controller.rb b/app/controllers/finance/group_order_articles_controller.rb index 705e2b2f..3596ec40 100644 --- a/app/controllers/finance/group_order_articles_controller.rb +++ b/app/controllers/finance/group_order_articles_controller.rb @@ -65,7 +65,13 @@ class Finance::GroupOrderArticlesController < ApplicationController def destroy group_order_article = GroupOrderArticle.find(params[:id]) - group_order_article.destroy + # only destroy if quantity and tolerance was zero already, so that we don't + # lose what the user ordered, if any + if group_order_article.quantity > 0 or group_order_article.tolerance >0 + group_order_article.update_attribute(:result, 0) + else + group_order_article.destroy + end update_summaries(group_order_article) @order_article = group_order_article.order_article diff --git a/app/controllers/finance/order_articles_controller.rb b/app/controllers/finance/order_articles_controller.rb index eed715eb..cc3351b0 100644 --- a/app/controllers/finance/order_articles_controller.rb +++ b/app/controllers/finance/order_articles_controller.rb @@ -42,6 +42,14 @@ class Finance::OrderArticlesController < ApplicationController def destroy @order_article = OrderArticle.find(params[:id]) - @order_article.destroy + # only destroy if there are no associated GroupOrders; if we would, the requested + # quantity and tolerance would be gone. Instead of destroying, we set all result + # quantities to zero. + if @order_article.group_order_articles.count == 0 + @order_article.destroy + else + @order_article.group_order_articles.each { |goa| goa.update_attribute(:result, 0) } + @order_article.update_results! + end end end diff --git a/app/controllers/stockit_controller.rb b/app/controllers/stockit_controller.rb index 3e3b43b2..475ac3a3 100644 --- a/app/controllers/stockit_controller.rb +++ b/app/controllers/stockit_controller.rb @@ -31,6 +31,11 @@ class StockitController < ApplicationController end end + def show + @stock_article = StockArticle.find(params[:id]) + @stock_changes = @stock_article.stock_changes.order('stock_changes.created_at DESC') + end + def destroy @article = StockArticle.find(params[:id]) @article.mark_as_deleted @@ -55,9 +60,4 @@ class StockitController < ApplicationController render :partial => 'form', :locals => {:stock_article => stock_article} end - - def history - @stock_article = StockArticle.undeleted.find(params[:stock_article_id]) - @stock_changes = @stock_article.stock_changes.order('stock_changes.created_at DESC').each {|s| s.readonly!} - end end diff --git a/app/documents/order_by_articles.rb b/app/documents/order_by_articles.rb index 705393f5..a6e1b0b5 100644 --- a/app/documents/order_by_articles.rb +++ b/app/documents/order_by_articles.rb @@ -12,20 +12,29 @@ class OrderByArticles < OrderPdf def body @order.order_articles.ordered.each do |order_article| - text "#{order_article.article.name} (#{order_article.article.unit} | #{order_article.price.unit_quantity.to_s} | #{number_with_precision(order_article.price.fc_price, precision: 2)})", - style: :bold, size: 10 rows = [] - rows << I18n.t('documents.order_by_articles.rows') - for goa in order_article.group_order_articles + dimrows = [] + for goa in order_article.group_order_articles.ordered rows << [goa.group_order.ordergroup.name, + "#{goa.quantity} + #{goa.tolerance}", goa.result, number_with_precision(order_article.price.fc_price * goa.result, precision: 2)] + dimrows << rows.length if goa.result == 0 end + next if rows.length == 0 + rows.unshift I18n.t('documents.order_by_articles.rows') # table header - table rows, column_widths: [200,40,40], cell_style: {size: 8, overflow: :shrink_to_fit} do |table| - table.columns(1..2).align = :right + text "#{order_article.article.name} (#{order_article.article.unit} | #{order_article.price.unit_quantity.to_s} | #{number_with_precision(order_article.price.fc_price, precision: 2)})", + style: :bold, size: 10 + table rows, cell_style: {size: 8, overflow: :shrink_to_fit} do |table| + table.column(0).width = 200 + table.columns(1..3).align = :right + table.column(2).font_style = :bold table.cells.border_width = 1 table.cells.border_color = '666666' + table.rows(0).border_bottom_width = 2 + # dim rows which were ordered but not received + dimrows.each { |ri| table.row(ri).text_color = '999999' } end move_down 10 end diff --git a/app/documents/order_by_groups.rb b/app/documents/order_by_groups.rb index 5ee66cca..746a167f 100644 --- a/app/documents/order_by_groups.rb +++ b/app/documents/order_by_groups.rb @@ -12,12 +12,10 @@ class OrderByGroups < OrderPdf def body # Start rendering - @order.group_orders.each do |group_order| - text group_order.ordergroup.name, size: 9, style: :bold - + @order.group_orders.ordered.each do |group_order| total = 0 rows = [] - rows << I18n.t('documents.order_by_groups.rows') # Table Header + dimrows = [] group_order_articles = group_order.group_order_articles.ordered group_order_articles.each do |goa| @@ -25,15 +23,20 @@ class OrderByGroups < OrderPdf sub_total = price * goa.result total += sub_total rows << [goa.order_article.article.name, + "#{goa.quantity} + #{goa.tolerance}", goa.result, number_with_precision(price, precision: 2), goa.order_article.price.unit_quantity, goa.order_article.article.unit, number_with_precision(sub_total, precision: 2)] + dimrows << rows.length if goa.result == 0 end - rows << [ I18n.t('documents.order_by_groups.sum'), nil, nil, nil, nil, number_with_precision(total, precision: 2)] + next if rows.length == 0 + rows << [ I18n.t('documents.order_by_groups.sum'), nil, nil, nil, nil, nil, number_with_precision(total, precision: 2)] + rows.unshift I18n.t('documents.order_by_groups.rows') # Table Header - table rows, column_widths: [250,50,50,50,50,50], cell_style: {size: 8, overflow: :shrink_to_fit} do |table| + text group_order.ordergroup.name, size: 9, style: :bold + table rows, width: 500, cell_style: {size: 8, overflow: :shrink_to_fit} do |table| # borders table.cells.borders = [] table.row(0).borders = [:bottom] @@ -41,8 +44,14 @@ class OrderByGroups < OrderPdf table.cells.border_width = 1 table.cells.border_color = '666666' - table.columns(1..3).align = :right - table.columns(5).align = :right + table.column(0).width = 240 + table.column(2).font_style = :bold + table.columns(1..4).align = :right + table.column(6).align = :right + table.column(6).font_style = :bold + + # dim rows which were ordered but not received + dimrows.each { |ri| table.row(ri).text_color = '999999' } end move_down 15 diff --git a/app/models/article.rb b/app/models/article.rb index 3755a4b2..a3b4af33 100644 --- a/app/models/article.rb +++ b/app/models/article.rb @@ -1,6 +1,5 @@ # encoding: utf-8 class Article < ActiveRecord::Base - extend ActiveSupport::Memoizable # Ability to cache method results. Use memoize :expensive_method # Replace numeric seperator with database format localize_input_of :price, :tax, :deposit @@ -44,11 +43,12 @@ class Article < ActiveRecord::Base # If the article is used in an open Order, the Order will be returned. def in_open_order - order_articles = OrderArticle.all(:conditions => ['order_id IN (?)', Order.open.collect(&:id)]) - order_article = order_articles.detect {|oa| oa.article_id == id } - order_article ? order_article.order : nil + @in_open_order ||= begin + order_articles = OrderArticle.all(:conditions => ['order_id IN (?)', Order.open.collect(&:id)]) + order_article = order_articles.detect {|oa| oa.article_id == id } + order_article ? order_article.order : nil + end end - memoize :in_open_order # Returns true if the article has been ordered in the given order at least once def ordered_in_order?(order) diff --git a/app/models/group_order.rb b/app/models/group_order.rb index 9b5eb764..2a37d970 100644 --- a/app/models/group_order.rb +++ b/app/models/group_order.rb @@ -17,6 +17,8 @@ class GroupOrder < ActiveRecord::Base scope :in_open_orders, joins(:order).merge(Order.open) scope :in_finished_orders, joins(:order).merge(Order.finished_not_closed) + scope :ordered, :include => :ordergroup, :order => 'groups.name' + # Generate some data for the javascript methods in ordering view def load_data data = {} diff --git a/app/models/group_order_article.rb b/app/models/group_order_article.rb index 11f3b447..3b37a990 100644 --- a/app/models/group_order_article.rb +++ b/app/models/group_order_article.rb @@ -2,7 +2,6 @@ # The chronologically order of the Ordergroup - activity are stored in GroupOrderArticleQuantity # class GroupOrderArticle < ActiveRecord::Base - extend ActiveSupport::Memoizable # Ability to cache method results. Use memoize :expensive_method belongs_to :group_order belongs_to :order_article @@ -14,7 +13,7 @@ class GroupOrderArticle < ActiveRecord::Base validates_inclusion_of :tolerance, :in => 0..99 validates_uniqueness_of :order_article_id, :scope => :group_order_id # just once an article per group order - scope :ordered, :conditions => 'result > 0' + scope :ordered, :conditions => 'group_order_articles.result > 0 OR group_order_articles.quantity > 0 OR group_order_articles.tolerance > 0', :include => {:group_order => :ordergroup}, :order => 'groups.name' localize_input_of :result @@ -101,54 +100,55 @@ class GroupOrderArticle < ActiveRecord::Base # # See description of the ordering algorithm in the general application documentation for details. def calculate_result - quantity = tolerance = 0 - stockit = order_article.article.is_a?(StockArticle) + @calculate_result ||= begin + quantity = tolerance = 0 + stockit = order_article.article.is_a?(StockArticle) - # Get total - total = stockit ? order_article.article.quantity : order_article.units_to_order * order_article.price.unit_quantity - logger.debug("<#{order_article.article.name}>.unitsToOrder => items ordered: #{order_article.units_to_order} => #{total}") + # Get total + total = stockit ? order_article.article.quantity : order_article.units_to_order * order_article.price.unit_quantity + logger.debug("<#{order_article.article.name}>.unitsToOrder => items ordered: #{order_article.units_to_order} => #{total}") - if (total > 0) - # In total there are enough units ordered. Now check the individual result for the ordergroup (group_order). - # - # Get all GroupOrderArticleQuantities for this OrderArticle... - order_quantities = GroupOrderArticleQuantity.all( - :conditions => ["group_order_article_id IN (?)", order_article.group_order_article_ids], :order => 'created_on') - logger.debug("GroupOrderArticleQuantity records found: #{order_quantities.size}") + if (total > 0) + # In total there are enough units ordered. Now check the individual result for the ordergroup (group_order). + # + # Get all GroupOrderArticleQuantities for this OrderArticle... + order_quantities = GroupOrderArticleQuantity.all( + :conditions => ["group_order_article_id IN (?)", order_article.group_order_article_ids], :order => 'created_on') + logger.debug("GroupOrderArticleQuantity records found: #{order_quantities.size}") - # Determine quantities to be ordered... - total_quantity = i = 0 - while (i < order_quantities.size && total_quantity < total) - q = (order_quantities[i].quantity <= total - total_quantity ? order_quantities[i].quantity : total - total_quantity) - total_quantity += q - if (order_quantities[i].group_order_article_id == self.id) - logger.debug("increasing quantity by #{q}") - quantity += q - end - i += 1 - end - - # Determine tolerance to be ordered... - if (total_quantity < total) - logger.debug("determining additional items to be ordered from tolerance") - i = 0 + # Determine quantities to be ordered... + total_quantity = i = 0 while (i < order_quantities.size && total_quantity < total) - q = (order_quantities[i].tolerance <= total - total_quantity ? order_quantities[i].tolerance : total - total_quantity) + q = (order_quantities[i].quantity <= total - total_quantity ? order_quantities[i].quantity : total - total_quantity) total_quantity += q if (order_quantities[i].group_order_article_id == self.id) - logger.debug("increasing tolerance by #{q}") - tolerance += q + logger.debug("increasing quantity by #{q}") + quantity += q end i += 1 end + + # Determine tolerance to be ordered... + if (total_quantity < total) + logger.debug("determining additional items to be ordered from tolerance") + i = 0 + while (i < order_quantities.size && total_quantity < total) + q = (order_quantities[i].tolerance <= total - total_quantity ? order_quantities[i].tolerance : total - total_quantity) + total_quantity += q + if (order_quantities[i].group_order_article_id == self.id) + logger.debug("increasing tolerance by #{q}") + tolerance += q + end + i += 1 + end + end + + logger.debug("determined quantity/tolerance/total: #{quantity} / #{tolerance} / #{quantity + tolerance}") end - logger.debug("determined quantity/tolerance/total: #{quantity} / #{tolerance} / #{quantity + tolerance}") + {:quantity => quantity, :tolerance => tolerance, :total => quantity + tolerance} end - - {:quantity => quantity, :tolerance => tolerance, :total => quantity + tolerance} end - memoize :calculate_result # Returns order result, # either calcualted on the fly or fetched from result attribute @@ -167,7 +167,7 @@ class GroupOrderArticle < ActiveRecord::Base # the minimum price depending on configuration. When the order is finished it # will be the value depending of the article results. def total_price(order_article = self.order_article) - unless order_article.order.finished? + if order_article.order.open? if FoodsoftConfig[:tolerance_is_costly] order_article.article.fc_price * (quantity + tolerance) else diff --git a/app/models/order.rb b/app/models/order.rb index 7910dfc3..f4545d02 100644 --- a/app/models/order.rb +++ b/app/models/order.rb @@ -2,6 +2,8 @@ # class Order < ActiveRecord::Base + attr_accessor :ignore_warnings + # Associations has_many :order_articles, :dependent => :destroy has_many :articles, :through => :order_articles @@ -17,6 +19,7 @@ class Order < ActiveRecord::Base # Validations validates_presence_of :starts validate :starts_before_ends, :include_articles + validate :keep_ordered_articles # Callbacks after_save :save_order_articles, :update_price_of_group_orders @@ -55,7 +58,12 @@ class Order < ActiveRecord::Base end def article_ids - @article_ids ||= order_articles.map(&:article_id) + @article_ids ||= order_articles.map { |a| a.article_id.to_s } + end + + # Returns an array of article ids that lead to a validation error. + def erroneous_article_ids + @erroneous_article_ids ||= [] end def open? @@ -209,24 +217,24 @@ class Order < ActiveRecord::Base protected def starts_before_ends - errors.add(:ends, I18n.t('articles.model.error_starts_before_ends')) if (ends && starts && ends <= starts) + errors.add(:ends, I18n.t('orders.model.error_starts_before_ends')) if (ends && starts && ends <= starts) end def include_articles - errors.add(:articles, I18n.t('articles.model.error_nosel')) if article_ids.empty? + errors.add(:articles, I18n.t('orders.model.error_nosel')) if article_ids.empty? + end + + def keep_ordered_articles + chosen_order_articles = order_articles.find_all_by_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 or a.tolerance > 0 } + unless to_be_removed_but_ordered.empty? or 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 end def save_order_articles - #self.articles = Article.find(article_ids) # This doesn't deletes the group_order_articles, belonging to order_articles, - # # see http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#method-i-has_many - # - ## Ensure to delete also the group_order_articles, belonging to order_articles - ## This case is relevant, when removing articles from a running order - #goa_ids = GroupOrderArticle.where(group_order_id: group_order_ids).includes(:order_article). - # select { |goa| goa.order_article.nil? }.map(&:id) - #GroupOrderArticle.delete_all(id: goa_ids) unless goa_ids.empty? - - # fetch selected articles articles_list = Article.find(article_ids) # create new order_articles diff --git a/app/models/order_article.rb b/app/models/order_article.rb index 85a0da79..bd90706b 100644 --- a/app/models/order_article.rb +++ b/app/models/order_article.rb @@ -12,7 +12,8 @@ class OrderArticle < ActiveRecord::Base validate :article_and_price_exist validates_uniqueness_of :article_id, scope: :order_id - scope :ordered, :conditions => "units_to_order >= 1" + scope :ordered, :conditions => "units_to_order > 0" + scope :ordered_or_member, -> { includes(:group_order_articles).where("units_to_order > 0 OR group_order_articles.result > 0") } before_create :init_from_balancing after_destroy :update_ordergroup_prices diff --git a/app/views/articles/sync.html.haml b/app/views/articles/sync.html.haml index d740cb6e..1039aee4 100644 --- a/app/views/articles/sync.html.haml +++ b/app/views/articles/sync.html.haml @@ -1,4 +1,4 @@ -- title 'Artikel mit externer Datenbank synchronisieren' +- title t('.title') = form_tag update_synchronized_supplier_articles_path(@supplier) do %h2= t '.outlist.title' @@ -19,9 +19,8 @@ %h2= t '.update.title' %p %i - %b= @updated_articles.size - = t '.update.update_msg' - = t('.update.body').html_safe + = t '.update.update_msg', count: @updated_articles.size + = t '.update.body' %table.table %thead %tr diff --git a/app/views/deliveries/add_stock_change.js.erb b/app/views/deliveries/add_stock_change.js.erb index 049e6233..3e233bb6 100644 --- a/app/views/deliveries/add_stock_change.js.erb +++ b/app/views/deliveries/add_stock_change.js.erb @@ -5,21 +5,19 @@ $('#stock_changes tr').removeClass('success'); + var quantity = w.prompt('<%= j(t('.how_many_units', :unit => @stock_change.stock_article.unit, :name => @stock_change.stock_article.name)) %>'); + if(null === quantity) { + return false; + } + var stock_change = $( '<%= j(render(:partial => 'stock_change', :locals => {:stock_change => @stock_change})) %>' ).addClass('success'); enablePriceTooltips(stock_change); + $('input.stock-change-quantity', stock_change).val(quantity); $('#stock_changes').append(stock_change); mark_article_for_delivery(<%= @stock_change.stock_article.id %>); updateSort('#stock_changes'); - var quantity = w.prompt('<%= j(t('.how_many_units', :unit => @stock_change.stock_article.unit, :name => @stock_change.stock_article.name)) %>'); <%# how to properly escape here? %> - if(null === quantity) { - stock_change.remove(); - mark_article_for_delivery(<%= @stock_change.stock_article.id %>); - return false; - } - $('input.stock-change-quantity', stock_change).val(quantity); - })(window); diff --git a/app/views/group_orders/_form.html.haml b/app/views/group_orders/_form.html.haml index 2bd2f30b..3c6048c0 100644 --- a/app/views/group_orders/_form.html.haml +++ b/app/views/group_orders/_form.html.haml @@ -7,12 +7,17 @@ setMinimumBalance(#{FoodsoftConfig[:minimum_balance] or 0}); setToleranceBehaviour(#{FoodsoftConfig[:tolerance_is_costly]}); setStockit(#{@order.stockit?}); + // create List for search-feature (using list.js, http://listjs.com) + var listjsResetPlugin = ['reset', {highlightClass: 'btn-primary'}]; + var listjsDelayPlugin = ['delay', {delayedSearchTime: 500}]; + new List(document.body, { valueNames: ['name'], engine: 'unlist', plugins: [listjsResetPlugin, listjsDelayPlugin] }); }); - title t('.title'), false .row-fluid .well.pull-left + %button{type: "button", class: "close", data: {dismiss: 'alert'}}= '×'.html_safe %h2= @order.name %dl.dl-horizontal - unless @order.note.blank? @@ -35,8 +40,17 @@ %dd= number_to_currency(@ordering_data[:available_funds]) .well.pull-right + %button{type: "button", class: "close", data: {dismiss: 'alert'}}= '×'.html_safe = render 'switch_order', current_order: @order +.row-fluid + .well.clear + .form-search + .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 @@ -59,9 +73,9 @@ %th(style="width:20px")= t '.available' %th#col_required= t '.amount' %th{style: "width:15px;"}= t '.sum' - %tbody + %tbody.list - @order.articles_grouped_by_category.each do |category, order_articles| - %tr.article-category + %tr.list-heading.article-category %td = category %i.icon-tag diff --git a/app/views/invites/new.html.haml b/app/views/invites/new.html.haml index eb9081cb..2332db00 100644 --- a/app/views/invites/new.html.haml +++ b/app/views/invites/new.html.haml @@ -4,4 +4,4 @@ = form.hidden_field :group_id = form.input :email = form.submit t('.action') - = link_to t('.back'), :back + = link_to t('ui.or_cancel'), :back diff --git a/app/views/orders/_form.html.haml b/app/views/orders/_form.html.haml index 78cd3ca8..86d24106 100644 --- a/app/views/orders/_form.html.haml +++ b/app/views/orders/_form.html.haml @@ -27,10 +27,14 @@ = category_name %i.icon-tag - for article in articles - / check if the article is selected - - included = @order.article_ids.include?(article.id) - - included_class = included ? ' selected' : '' - %tr{:class => included_class, :id => article.id.to_s } + / check if the article is selected or has an error + - included = @order.article_ids.include?(article.id.to_s) + - row_class = '' + - if included + - row_class = 'selected' + - elsif @order.erroneous_article_ids.include?(article.id) + - row_class = 'error' + %tr{class: row_class, id: article.id} %td= check_box_tag "order[article_ids][]", article.id, included, :id => "checkbox_#{article.id}" %td.click-me{'data-check-this' => "#checkbox_#{article.id}"}= article.name %td=h truncate article.note, :length => 25 @@ -52,3 +56,7 @@ .form-actions = f.submit class: 'btn' = link_to t('ui.or_cancel'), orders_path + - unless @order.erroneous_article_ids.empty? + + = check_box_tag 'order[ignore_warnings]' + = t '.ignore_warnings' diff --git a/app/views/shared/_articles_by_articles.html.haml b/app/views/shared/_articles_by_articles.html.haml index df4528a3..6d952363 100644 --- a/app/views/shared/_articles_by_articles.html.haml +++ b/app/views/shared/_articles_by_articles.html.haml @@ -2,8 +2,10 @@ %thead %tr %th{:style => 'width:70%'}= t '.ordergroup' - %th= t '.ordered' - %th= t '.received' + %th + %acronym{:title => t('shared.articles.ordered_desc')}= t 'shared.articles.ordered' + %th + %acronym{:title => t('shared.articles.received_desc')}= t 'shared.articles.received' %th= t '.price' - for order_article in order.order_articles.ordered.all(:include => [:article, :article_price]) @@ -13,8 +15,8 @@ = order_article.article.name = "(#{order_article.article.unit} | #{order_article.price.unit_quantity} | #{number_to_currency(order_article.price.gross_price)})" %tbody - - for goa in order_article.group_order_articles - %tr{:class => cycle('even', 'odd', :name => 'groups')} + - for goa in order_article.group_order_articles.ordered + %tr{:class => [cycle('even', 'odd', :name => 'groups'), if goa.result == 0 then 'unavailable' end]} %td{:style => "width:70%"}=h goa.group_order.ordergroup.name %td= "#{goa.quantity} + #{goa.tolerance}" %td diff --git a/app/views/shared/_articles_by_groups.html.haml b/app/views/shared/_articles_by_groups.html.haml index c78f5a70..9470fb49 100644 --- a/app/views/shared/_articles_by_groups.html.haml +++ b/app/views/shared/_articles_by_groups.html.haml @@ -3,7 +3,9 @@ %tr %th{:style => "width:40%"}= t '.name' %th - %acronym{:title => t('.units_desc')}= t '.units' + %acronym{:title => t('shared.articles.ordered_desc')}= t 'shared.articles.ordered' + %th + %acronym{:title => t('shared.articles.received_desc')}= t 'shared.articles.received' %th %acronym{:title => t('.fc_price_desc')}= t '.fc_price' %th @@ -11,10 +13,10 @@ %th= t '.unit' %th= t '.price' - - for group_order in order.group_orders.all + - for group_order in order.group_orders.ordered %thead %tr - %th{:colspan => "6"} + %th{:colspan => "7"} %h4= group_order.ordergroup.name %tbody - total = 0 @@ -22,17 +24,19 @@ - fc_price = goa.order_article.price.fc_price - subTotal = fc_price * goa.result - total += subTotal - %tr{:class => cycle('even', 'odd', :name => 'articles')} + %tr{:class => [cycle('even', 'odd', :name => 'articles'), if goa.result == 0 then 'unavailable' end]} %td{:style => "width:40%"}=h goa.order_article.article.name - %td= goa.result + %td= "#{goa.quantity} + #{goa.tolerance}" + %td + %b= goa.result %td= number_to_currency(fc_price) %td= goa.order_article.price.unit_quantity %td= goa.order_article.article.unit %td= number_to_currency(subTotal) %tr{:class => cycle('even', 'odd', :name => 'articles')} - %th{:colspan => "5"} Summe + %th{:colspan => "6"} Summe %th= number_to_currency(total) %tr - %th(colspan="6") + %th(colspan="7") - reset_cycle("articles") diff --git a/app/views/stock_takings/_stock_change.html.haml b/app/views/stock_takings/_stock_change.html.haml index 616be369..3d27c1fa 100644 --- a/app/views/stock_takings/_stock_change.html.haml +++ b/app/views/stock_takings/_stock_change.html.haml @@ -3,5 +3,6 @@ = form.hidden_field :stock_article_id = "Menge (#{stock_change.stock_article.quantity_available})" = form.text_field :quantity, :size => 5, :autocomplete => 'off' - %b= stock_change.stock_article.name - = "(#{number_to_currency(stock_change.stock_article.price)} / #{stock_change.stock_article.unit})" + %span{:data => {:toggle => :tooltip, :title => render(:partial => 'shared/article_price_info', :locals => {:article => stock_change.stock_article})}} + %b= stock_change.stock_article.name + = "(#{number_to_currency(stock_change.stock_article.price)} / #{stock_change.stock_article.unit})" diff --git a/app/views/stock_takings/new.html.haml b/app/views/stock_takings/new.html.haml index 51239ba4..6e4526f9 100644 --- a/app/views/stock_takings/new.html.haml +++ b/app/views/stock_takings/new.html.haml @@ -1,5 +1,20 @@ - title t('.title') +- content_for :javascript do + :javascript + $(function() { + enablePriceTooltips(); + }); + + function enablePriceTooltips(context) { + $('[data-toggle~="tooltip"]', context).tooltip({ + animation: false, + html: true, + placement: 'left', + container: 'body' + }); + } + - content_for :sidebar do %p %i= t('.text_deviations', inv_link: link_to(t('.temp_inventory'), stock_articles_path)).html_safe diff --git a/app/views/stockit/history.haml b/app/views/stockit/history.haml deleted file mode 100644 index f4fe2b07..00000000 --- a/app/views/stockit/history.haml +++ /dev/null @@ -1,17 +0,0 @@ -- title t('.stock_changes', :article_name => @stock_article.name) - -%table.table.table-hover#stock_changes - %thead - %tr - %th= t '.datetime' - %th= t '.reason' - %th= t '.change_quantity' - %th= t '.new_quantity' - %tbody - - reversed_history = @stock_article.quantity_history.reverse - - @stock_changes.each_with_index do |stock_change, index| - %tr - %td= l stock_change.created_at - %td= link_to_stock_change_reason(stock_change) - %td= stock_change.quantity - %td= reversed_history[index] diff --git a/app/views/stockit/index.html.haml b/app/views/stockit/index.html.haml index 477e5816..c2899d9e 100644 --- a/app/views/stockit/index.html.haml +++ b/app/views/stockit/index.html.haml @@ -45,7 +45,7 @@ %tbody - for article in @stock_articles %tr{:class => stock_article_classes(article), :id => "stockArticle-#{article.id}"} - %td=h article.name + %td= link_to article.name, article %td= article.quantity %td= article.quantity - article.quantity_available %th= article.quantity_available @@ -56,7 +56,6 @@ %td= article.article_category.name %td = link_to t('ui.edit'), edit_stock_article_path(article), class: 'btn btn-mini' - = link_to t('ui.history'), stock_article_history_path(article), class: 'btn btn-mini' = link_to t('ui.delete'), article, :method => :delete, :confirm => t('.confirm_delete'), class: 'btn btn-mini btn-danger', :remote => true %p diff --git a/app/views/stockit/show.html.haml b/app/views/stockit/show.html.haml new file mode 100644 index 00000000..5f416bf9 --- /dev/null +++ b/app/views/stockit/show.html.haml @@ -0,0 +1,47 @@ +- title @stock_article.name + +.row-fluid + .span6 + %dl.dl-horizontal + %dt= StockArticle.human_attribute_name 'supplier' + %dd= link_to @stock_article.supplier.name, @stock_article.supplier + %dt= StockArticle.human_attribute_name 'name' + %dd= @stock_article.name + %dt= StockArticle.human_attribute_name 'unit' + %dd= @stock_article.unit + %dt= StockArticle.human_attribute_name 'price' + %dd= number_to_currency @stock_article.price + %dt= StockArticle.human_attribute_name 'tax' + %dd= number_to_percentage @stock_article.tax + %dt= StockArticle.human_attribute_name 'deposit' + %dd= number_to_currency @stock_article.deposit + %dt= StockArticle.human_attribute_name 'fc_price' + %dd= number_to_currency @stock_article.fc_price + %dt= StockArticle.human_attribute_name 'article_category' + %dd= @stock_article.article_category.name + %dt= StockArticle.human_attribute_name 'note' + %dd= @stock_article.note + %dt= StockArticle.human_attribute_name 'quantity' + %dd= @stock_article.quantity + %dt= StockArticle.human_attribute_name 'quantity_available' + %dd= @stock_article.quantity_available + .form-actions + = link_to t('ui.edit'), edit_stock_article_path(@stock_article), class: 'btn' + + .span6 + %h2= t('.stock_changes') + %table.table.table-hover#stock_changes + %thead + %tr + %th= t '.datetime' + %th= t '.reason' + %th= t '.change_quantity' + %th= t '.new_quantity' + %tbody + - reversed_history = @stock_article.quantity_history.reverse + - @stock_changes.each_with_index do |stock_change, index| + %tr + %td= l stock_change.created_at + %td= link_to_stock_change_reason(stock_change) + %td= stock_change.quantity + %td= reversed_history[index] diff --git a/config/initializers/resque.rb b/config/initializers/resque.rb new file mode 100644 index 00000000..28001613 --- /dev/null +++ b/config/initializers/resque.rb @@ -0,0 +1 @@ +Resque.inline = Rails.env.test? diff --git a/config/locales/de.yml b/config/locales/de.yml index c0000036..ab2c694b 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -42,7 +42,10 @@ de: fc_price: Endpreis fc_share: FC-Aufschlag gross_price: Bruttopreis + name: Name + note: Notiz price: Nettopreis + supplier: Lieferantin tax: MwSt unit: Einheit unit_quantity: Gebindegröße @@ -51,6 +54,8 @@ de: note: Notiz stock_article: price: Nettopreis + quantity: Lagerbestand + quantity_available: Verfügbarer Bestand user: first_name: Vorname password: Passwort @@ -212,6 +217,11 @@ de: workgroups: members: Mitglieder name: Name + application: + controller: + error_authn: + error_denied: + error_members_only: Diese Aktion ist nur für Mitglieder der Gruppe erlaubt! article_categories: create: notice: Die Kategorie wurde gespeichert @@ -305,18 +315,20 @@ de: error_nosel: Du hast keine Artikel ausgewählt parse_upload: body:
Bitte überprufe die engelesenen Artikel.
Achtung, momentan gibt es keine Überprüfung auf doppelte Artikel.
+ title: + sync: outlist: body: ! 'Folgende Artikel wurden ausgelistet und werden gelöscht:' body_skip: Es müssen keine Artikel gelöscht werden. title: Auslisten ... price_short: Preis - submit: Alle löschen/aktualisieren + submit: Alle synchronisieren title: Artikel mit externer Datenbank synchronisieren unit_quantity_short: GebGr update: - body:Jeder Artikel wird doppelt angezeigt. Die alten Werte sind grau und die Textfelder sind mit den aktuellen Werten vorausgefüllt.
Abweichungen zu den alten Artikeln sind gelb markiert.
+ body: ! 'Jeder Artikel wird doppelt angezeigt: die alten Werte sind grau und die Textfelder sind mit den aktuellen Werten vorausgefüllt. Abweichungen zu den alten Artikeln sind gelb markiert.' title: Aktualisieren ... - update_msg: ! 'Artikel müssen aktualisiert werden:' + update_msg: ! '%{count} Artikel müssen aktualisiert werden.' upload: body:Die Datei muss eine Textdatei mit der Endung '.csv' sein. Die erste Zeile wird beim Einlesen ignoriert.
Die Felder müssen mit einem Semikolon (';') getrennt und der Text mit doppelten Anführungszeichen ("Text...") umklammert werden.
Als Zeichensatz wird UTF-8 erwartet. Korrekte Reihenfolge der Spalten:
fields: @@ -482,14 +494,16 @@ de: filename: Bestellung %{name}-%{date} - Artikelsortierung rows: - Bestellgruppe - - Menge + - Bestellt + - Bekommen - Preis title: ! 'Artikelsortierung der Bestellung: %{name}, beendet am %{date}' order_by_groups: filename: Bestellung %{name}-%{date} - Gruppensortierung rows: - Artikel - - Menge + - Bestellt + - Bekommen - Preis - GebGr - Einheit @@ -505,6 +519,7 @@ de: - Gebinde - Einheit - Preis/Einheit + - Summe total: Gesamtpreis order_matrix: filename: Bestellung %{name}-%{date} - Sortiermatrix @@ -641,11 +656,13 @@ de: create: notice: Rechnung wurde erstellt. financial_transactions: - create: - notice: Die Transaktion wurde gespeichert. - create_collection: - alert: ! 'Ein Fehler ist aufgetreten: %{error}' - notice: Alle Transaktionen wurden gespeichert. + controller: + create: + notice: Die Transaktion wurde gespeichert. + create_collection: + alert: ! 'Ein Fehler ist aufgetreten: %{error}' + error_note_required: Notiz wird benötigt! + notice: Alle Transaktionen wurden gespeichert. index: balance: ! 'Kontostand: %{balance}' last_updated_at: (zuletzt aktualisiert vor %{when}) @@ -784,6 +801,8 @@ de: new_funds: Neuer Kontostand note: Notiz price: Preis + reset_article_search: Suche zurücksetzen + search_article: Artikel suchen... sum: Summe sum_amount: ! 'Gesamtbestellmenge bisher:' supplier: Lieferant @@ -968,7 +987,6 @@ de: title: Person einladen new: action: Einlading abschicken - back: oder zurück body:Hier kannst du eine Person in die Gruppe %{group} einladen, die noch nicht Mitglied der Foodcoop ist.
success: Benutzerin wurde erfolgreich eingeladen. layouts: @@ -1299,6 +1317,7 @@ de: finish: notice: Die Bestellung wurde beendet. form: + ignore_warnings: Warnungen ignorieren name: Name note: Notiz origin: Herkunft @@ -1326,6 +1345,8 @@ de: error_starts_before_ends: muss nach dem Bestellstart liegen (oder leer bleiben) notice_close: ! 'Bestellung: %{name}, bis %{ends}' stock: Lager + warning_ordered: ! 'Warnung: Die rot markierten Artikel wurden in der laufenden Bestellung bereits bestellt. Wenn Du sie hier abwählst, werden alle bestehenden Bestellungen dieses Artikels gelöscht.' + warning_ordered_stock: ! 'Warnung: Die rot markierten Artikel wurden in der laufenden Lagerbestellung bereits bestellt bzw. gekauft. Wenn Du sie hier abwählst, werden alle bestehenden Bestellungen bzw. Käufe dieses Artikels gelöscht und nicht abgerechnet!' new: title: Neue Bestellung anlegen orders: @@ -1452,11 +1473,14 @@ de: title: Foodsoft anmelden user: Benutzerin shared: + articles: + ordered: Bestellt + ordered_desc: + received: Bekommen + received_desc: articles_by_articles: - ordered: Bestellt (Menge + Toleranz) ordergroup: Bestellgruppe price: Gesamtpreis - received: Bekommen articles_by_groups: fc_price: FC-Preis fc_price_desc: Preis incl. MwSt, Pfand und Foodcoop-Aufschlag @@ -1465,8 +1489,6 @@ de: unit: Einheit unit_quantity: GebGr unit_quantity_desc: Gebindegröße - units: Menge - units_desc: Zugeteilte Einheiten group: access: Zugriff auf activated: aktiviert @@ -1525,7 +1547,7 @@ de: message: private: Nachricht erscheint nicht im Foodsoft Posteingang order_article: - units_to_order: Anzahl gelieferter Gebinde + units_to_order: Wenn Du die Gesamtanzahl gelieferter Gebinde änderst, musst Du auch die individuelle Anzahl der einzelnen Bestellgruppen anpassen, indem Du auf den Artikelnamen klickst. Sie werden nicht automatisch neuberechnet und andernfalls werden den Bestellgruppen Artikel in Rechnung gestellt, die nicht geliefert wurden! update_current_price: Ändert auch den Preis für aktuelle Bestellungen stock_article: copy_stock_article: @@ -1718,15 +1740,6 @@ de: title: Lagerartikel bearbeiten form: price_hint: Um Chaos zu vermeiden können bis auf weiteres die Preise von angelegten Lagerartikeln nicht mehr verändert werden. - history: - change_quantity: Veränderung - datetime: Zeitpunkt - delivery: Lieferung - new_quantity: Neuer Bestand - order: Bestellung - reason: Ereignis - stock_changes: Verlauf anzeigen für »%{article_name}« - stock_taking: Inventur index: article: article: Artikel @@ -1752,6 +1765,15 @@ de: new: search_text: ! 'Suche nache Artikeln aus allen Katalogen:' title: Neuen Lagerartikel anlegen + show: + change_quantity: Veränderung + datetime: Zeitpunkt + delivery: Lieferung + new_quantity: Neuer Bestand + order: Bestellung + reason: Ereignis + stock_changes: Verlauf des Lagerbestands + stock_taking: Inventur stock_create: notice: Lagerartikel wurde gespeichert. stock_update: @@ -1876,7 +1898,6 @@ de: close: Schließen delete: Löschen edit: Bearbeiten - history: Verlauf anzeigen marks: close: ! '×' success: diff --git a/config/locales/en.yml b/config/locales/en.yml index 2fa64013..f7d04466 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -42,7 +42,10 @@ en: fc_price: FC price fc_share: FC share gross_price: gross price + name: name + note: note price: price + supplier: supplier tax: VAT unit: unit unit_quantity: unit quantity @@ -50,7 +53,9 @@ en: amount: amount note: note stock_article: - price: Price + price: price + quantity: quantity + quantity_available: available quantity user: first_name: First name password: Password @@ -212,6 +217,11 @@ en: workgroups: members: members name: name + application: + controller: + error_authn: Authentication required! + error_denied: Access denied! + error_members_only: This action is only available to members of the group! article_categories: create: notice: Category was stored @@ -305,20 +315,22 @@ en: error_nosel: You have selected no articles parse_upload: body:Please verify the articles.
Warning, at the moment there is no check for duplicate articles.
+ title: Upload articles + sync: outlist: - body: ! 'The following articles were outlisted and deleted:' + body: ! 'The following articles were removed from the list and will be deleted:' body_skip: No articles to delete. - title: Outlist ... + title: Remove from list ... price_short: Price - submit: Delete/update all + submit: Synchronize all title: Synchronize articles with external database - unit_quantity_short: unit quantity + unit_quantity_short: Unit quantity update: - body: ! 'Every article is shown twice. The old values are gray and contain the current values.
- -Differences with the old articles are marked yellow.
' + body: ! 'Every article is shown twice: old values are gray, while the text fields contain updated values. Differences with the old articles are marked yellow.' title: Update ... - update_msg: ! 'Articles must be updated:' + update_msg: + one: One article needs to be updated. + other: ! '%{count} articles need to be updated.' upload: body:The file has to be a text file with the ending '.csv' The first line will be ignored when imported
The fields have to be separated with semicolons (';') and the text enclosed by double quotation marks ("text...").
As character set UTF-8 is demanded. Correct order of the column:
fields: @@ -430,7 +442,7 @@ en: create: notice: Delivery was created. Please don’t forget to create invoice! create_stock_article: - notice: The new stock article »%{name}« was saved. + notice: The new stock article "%{name}" was saved. destroy: notice: Delivery was deleted. edit: @@ -478,20 +490,22 @@ en: update: notice: Delivery was updated. update_stock_article: - notice: The stock article »%{name}« was updated. + notice: The stock article "%{name}" was updated. documents: order_by_articles: filename: Order %{name}-%{date} - by articles rows: - Order group - - Amount + - Ordered + - Received - Price title: ! 'Order sorted by articles: %{name}, closed at %{date}' order_by_groups: filename: Order %{name}-%{date} - by group rows: - Article - - Amount + - Ordered + - Received - Price - Unit quantity - Unit @@ -646,11 +660,13 @@ en: create: notice: Invoice was created financial_transactions: - create: - notice: The transaction was saved. - create_collection: - alert: ! 'An error occured: %{error}' - notice: All transactions were saved. + controller: + create: + notice: The transaction was saved. + create_collection: + alert: ! 'An error occured: %{error}' + error_note_required: Note is required! + notice: All transactions were saved. index: balance: ! 'Balance of account: %{balance}' last_updated_at: (last updated %{when} ago) @@ -789,6 +805,8 @@ en: new_funds: New account balance note: Note price: Price + reset_article_search: Reset search + search_article: Search for article... sum: Sum sum_amount: Current amount supplier: Supplier @@ -896,7 +914,7 @@ en: warning: Warning, if you have less then %{threshold} of apple points, you are not allowed to place an order! changes_saved: Changes saved. index: - due_date_format: ! '%A %d %b' + due_date_format: ! '%A %d %B' messages: title: Newest Messages view_all: See all messages @@ -973,7 +991,6 @@ en: title: Invite person new: action: Send invite - back: or go back body:Here you can add a person to the group %{group}, who is not yet a member of the foodcoop.
success: User was invited successfully. layouts: @@ -1304,13 +1321,14 @@ en: finish: notice: The order has been closed. form: + ignore_warnings: Ignore warnings name: Name note: Note origin: Origin prices: Prices (net/FC) select_all: Select all stockit: In stock - supplier: Supplier + supplier: Producer title: Article unit_quantity: Unit quantity index: @@ -1331,6 +1349,8 @@ en: error_starts_before_ends: must be after the start date (or remain empty) notice_close: ! 'Order: %{name}, until %{ends}' stock: Stock + warning_ordered: ! 'Warning: Articles marked red have already been ordered within this open order. If you uncheck them here, all existing orders of these articles will be deleted.' + warning_ordered_stock: ! 'Warning: Articles marked red have already been ordered/ purchased within this open stock order. If you uncheck them here, all existing orders/ purchases of these articles will be deleted and it will not be accounted for them.' new: title: Create new order orders: @@ -1390,7 +1410,7 @@ en: notice: Page was created cshow: error_noexist: Page doesn’t exist! - redirect_notice: Redirected from %{page} .. + redirect_notice: Redirected from %{page} ... destroy: notice: The page '%{page}' and all subpages have been deleted successfully. edit: @@ -1457,11 +1477,14 @@ en: title: Foodsoft login user: User shared: + articles: + ordered: Ordered + ordered_desc: Number of articles as ordered by member (amount + tolerance) + received: Received + received_desc: Number of articles that (will be) received by member articles_by_articles: - ordered: Ordered (Amount + Tolerance) ordergroup: Ordergroup price: Total price - received: Received articles_by_groups: fc_price: FC-Price fc_price_desc: Price including taxes, deposit and Foodcoop-charge @@ -1470,8 +1493,6 @@ en: unit: Unit unit_quantity: Lot quantity unit_quantity_desc: How many units per lot. - units: Amount - units_desc: Assigned units group: access: Access to activated: activated @@ -1530,7 +1551,7 @@ en: message: private: Message doesn’t show in Foodsoft mail inbox order_article: - units_to_order: Amount of delivered units + units_to_order: If you change the total amount of delivered units, you also have to change individual group amounts by clicking on the article name. They will not be automatically recalculated and so ordergroups may be accounted for articles that were not delivered! update_current_price: Also update the price of the current order stock_article: copy_stock_article: @@ -1723,15 +1744,6 @@ en: title: Edit stock articles form: price_hint: To avoid choas, it is not possible to edit the prices of already added stock articles until further notice. - history: - change_quantity: Change - datetime: Time - delivery: Delivery - new_quantity: New quantity - order: Order - reason: Reason - stock_changes: Stock quantity changes of ‘%{article_name}’ - stock_taking: Inventory index: article: article: Article @@ -1757,6 +1769,15 @@ en: new: search_text: ! 'Search for articles in all catalogues:' title: Add new stock article + show: + change_quantity: Change + datetime: Time + delivery: Delivery + new_quantity: New quantity + order: Order + reason: Reason + stock_changes: Stock quantity changes + stock_taking: Inventory stock_create: notice: Stock article was created. stock_update: @@ -1860,7 +1881,7 @@ en: title: Show task update: notice: Task has been updated - notice_converted: Task has been updated and was converted to a regular task + notice_converted: Task has been updated and was converted to a non-repeating task. user: more: Nothing to do? %{tasks_link} are tasks for sure. tasks_link: Here @@ -1881,7 +1902,6 @@ en: close: Close delete: Delete edit: Edit - history: Show history marks: close: ! '×' success: diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 057acd4e..f9b71327 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -42,7 +42,10 @@ fr: fc_price: prix final fc_share: supplément boufcoop gross_price: prix brut + name: + note: price: prix net + supplier: tax: TVA unit: unité unit_quantity: unités par lot @@ -51,6 +54,8 @@ fr: note: note stock_article: price: Prix net + quantity: + quantity_available: user: first_name: Prénom password: Mot de passe @@ -212,6 +217,11 @@ fr: workgroups: members: membres name: nom + application: + controller: + error_authn: + error_denied: + error_members_only: article_categories: create: notice: La catégorie a bien été définie. @@ -307,6 +317,8 @@ fr: body: ! 'Merci de vérifier les articles importés.
Attention, les doublons ne sont pas automatiquement détectés.
.' + title: + sync: outlist: body: ! 'Les articles suivants ne sont plus dans la liste et seront donc supprimés:' body_skip: Aucun article à supprimer. @@ -316,9 +328,9 @@ fr: title: Synchroniser les articles avec la base de données extérieure unit_quantity_short: U/L update: - body:Chaque article apparaît deux fois. Les anciennes données sont rappelées en gris, et les champs du formulaire ont été préremplis avec les nouvelles valeurs.
Les changements sont marqués en jaune.
+ body: ! 'Chaque article apparaît deux fois: les anciennes données sont rappelées en gris, et les champs du formulaire ont été préremplis avec les nouvelles valeurs. Les changements sont marqués en jaune.' title: Mettre à jour... - update_msg: ! 'Ces articles doivent être mis à jour:' + update_msg: upload: body: ! 'Le fichier doit être au format texte et son nom doit se terminer par l''extension ".csv". La première ligne sera ignorée lors de l''importation.
@@ -655,11 +667,13 @@ fr: create: notice: La facture a bien été définie. financial_transactions: - create: - notice: La transaction a été sauvegardée. - create_collection: - alert: ! 'Une erreur s''est produite: %{error}' - notice: Les transactions ont été sauvegardées. + controller: + create: + notice: La transaction a été sauvegardée. + create_collection: + alert: ! 'Une erreur s''est produite: %{error}' + error_note_required: + notice: Les transactions ont été sauvegardées. index: balance: ! 'Solde: %{balance}' last_updated_at: (dernière mise à jour il y a %{when}) @@ -802,6 +816,8 @@ fr: new_funds: Nouveau solde note: Note price: Prix + reset_article_search: + search_article: sum: Prix total sum_amount: ! 'Quantité déjà commandée:' supplier: Fourni par @@ -994,7 +1010,6 @@ fr: title: Engrainer une personne new: action: Engrainer! - back: ou revenir en arrière body:Sur cette page, tu peux engrainer une personne qui ne fait pas encore partie de la Boufcoop à rejoindre la cellule %{group} success: La_le membre a été engrainéE avec succès! layouts: @@ -1288,7 +1303,7 @@ fr: article_count: ! 'Articles commandés:' name: Nom prices: Prix brut/net - prices_sum: Totaux (des prix bruts/nets) + prices_sum: ! 'Totaux (des prix bruts/nets):' unit_quantity: Unités par lots x Lots units_full: Lots complet units_ordered: Unités commandées @@ -1308,6 +1323,7 @@ fr: finish: notice: La commande a été close. form: + ignore_warnings: name: Nom note: Note origin: Origine @@ -1335,6 +1351,8 @@ fr: error_starts_before_ends: doit être postérieur à la date de début de la commande (ou bien être laissé vierge) notice_close: ! 'Commande: %{name}, jusqu''au %{ends}' stock: Stock + warning_ordered: + warning_ordered_stock: new: title: Définir une nouvelle commande orders: @@ -1459,11 +1477,14 @@ fr: title: Te connecter à Foodsoft user: Identifiant shared: + articles: + ordered: Commandé + ordered_desc: + received: Reçu + received_desc: articles_by_articles: - ordered: Commandé (Quantité + Tolérance) ordergroup: Cellul price: Prix total - received: Reçu articles_by_groups: fc_price: Prix coop fc_price_desc: Prix avec TVA, consigne et part de la coop inclus. @@ -1472,8 +1493,6 @@ fr: unit: Unité unit_quantity: U/L unit_quantity_desc: Unités par lot - units: Quantité - units_desc: Unités assignées group: access: Accès à activated: activé @@ -1532,7 +1551,7 @@ fr: message: private: Le message n'apparaîtra pas dans la boîte de réception du Foodsoft order_article: - units_to_order: Nombre de lots livrés + units_to_order: update_current_price: Modifie aussi le prix des commandes en cours stock_article: copy_stock_article: @@ -1727,15 +1746,6 @@ fr: title: Modifier l'article form: price_hint: Pour éviter que ça soit le bazar, les prix des articles en stock ne peuvent plus être modifiés. - history: - change_quantity: Modification - datetime: Temps - delivery: Réapprovisionnement - new_quantity: Nouveau stock - order: Commande - reason: Raison - stock_changes: Afficher l'historique pour "%{article_name}" - stock_taking: Inventaire index: article: article: Article @@ -1761,6 +1771,15 @@ fr: new: search_text: ! 'Rechercher des articles dans tous les catalogues:' title: Ajouter un article au stock + show: + change_quantity: Modification + datetime: Temps + delivery: Réapprovisionnement + new_quantity: Nouveau stock + order: Commande + reason: Raison + stock_changes: Afficher l'historique + stock_taking: Inventaire stock_create: notice: L'article a été sauvegardé. stock_update: @@ -1893,7 +1912,6 @@ fr: close: Fermer delete: Supprimer edit: Modifier - history: Afficher l'historique marks: close: ! '×' success: diff --git a/config/locales/nl.yml b/config/locales/nl.yml index 628bd0e8..2bef7df0 100644 --- a/config/locales/nl.yml +++ b/config/locales/nl.yml @@ -39,10 +39,13 @@ nl: article_category: categorie availability: Artikel leverbaar? deposit: statiegeld - fc_price: - fc_share: + fc_price: prijs foodcoop + fc_share: marge foodcoop gross_price: bruto prijs + name: + note: price: netto prijs + supplier: tax: BTW unit: eenheid unit_quantity: groothandelseenheid @@ -51,6 +54,8 @@ nl: note: notitie stock_article: price: prijs + quantity: + quantity_available: user: first_name: Voornaam password: Wachtwoord @@ -87,7 +92,7 @@ nl: task: attributes: done: - exclusion: + exclusion: gedane taken kunnen niet herhaald worden template: body: ! 'Controleer de volgende velden:' header: @@ -145,7 +150,7 @@ nl: first_paragraph: Hier kun je %{url} toevoegen, bewerken en verwijderen. new_ordergroup: Nieuw huishouden toevoegen new_ordergroups: nieuwe huishoudens - second_paragraph: + 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.' title: Huishoudens workgroup: werkgroep new: @@ -212,6 +217,11 @@ nl: workgroups: members: leden name: naam + application: + controller: + error_authn: Inloggen vereist. + error_denied: Geen toegang. + error_members_only: Deze actie is alleen beschikbaar voor leden van de groep! article_categories: create: notice: Categorie is opgeslagen @@ -263,7 +273,7 @@ nl: notice: Alle artikelen en prijzen zijn bijgewerkt. destroy_active_article: drop: verwijderen - note: + 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.' submit: Alle artikelen bijwerken @@ -305,18 +315,22 @@ nl: 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.
+ title: + sync: outlist: - body: ! 'De volgende artikelen werden uit de lijst gehaald en worden gewist:' - body_skip: Er zijn geen artikelen om te wissen. + body: ! 'De volgende artikelen zijn uit de lijst gehaald en worden verwijderd:' + body_skip: Er zijn geen artikelen om te verwijderen. title: Uit de lijst halen ... price_short: prijs - submit: Alle wissen/bijwerken + submit: Alles synchroniseren title: Artikelen met externe database synchroniseren - unit_quantity_short: + unit_quantity_short: Gr.Eenh. update: - body: - title: - update_msg: + 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 ... + update_msg: + one: Er moet éen artikel bijgewerkt worden. + other: Er moeten %{count} artikelen bijgewerkt worden. upload: body: fields: @@ -426,11 +440,11 @@ nl: add_stock_change: how_many_units: create: - notice: + notice: Levering is aangemaakt. Vergeet niet een factuur te maken! create_stock_article: - notice: + notice: Nieuw voorraadsartikel "%{name}" gemaakt. destroy: - notice: + notice: Levering is verwijdered. edit: title: form: @@ -474,7 +488,7 @@ nl: remove_article: suppliers_overview: update: - notice: + notice: Levering is bijgewerkt. update_stock_article: notice: documents: @@ -498,33 +512,38 @@ nl: title: ! 'Huishoudenslijst van bestelling: %{name}, gesloten op %{date}' order_fax: filename: Bestelling %{name}-%{date} - Fax - rows: ! '[]' + rows: total: Totaal order_matrix: filename: Bestelling %{name}-%{date} - Sorteermatrix heading: Artikeloverzicht - rows: + rows: + - Artikel + - Eenheid + - Gr.Eenh. + - Foodcoop-prijs + - Aantal title: ! 'Sorteermatrix van bestelling: %{name}, gesloten op %{date}' total: one: In totaal éen artikel other: In totaal %{count} artikelen errors: - format: + format: ! '%{attribute} %{message}' general: Er is een probleem opgetreden. - general_again: + general_again: Er is een fout opgetreden. Probeer het opnieuw. general_msg: ! 'Er is een probleem opgetreden: %{msg}' messages: accepted: moet geaccepteerd worden blank: moet ingevuld worden - confirmation: + confirmation: komt niet overeen met de bevestiging empty: moet ingevuld worden equal_to: moet precies %{count} zijn - even: + even: moet even zijn exclusion: moet even zijn greater_than: moet groter dan %{count} zijn - greater_than_or_equal_to: - inclusion: - invalid: + greater_than_or_equal_to: moet groter dan of gelijk zijn aan %{count} + inclusion: geen geldige waarde + invalid: is ongeldig less_than: moet kleiner dan %{count} zijn less_than_or_equal_to: moet groter of gelijk aan %{count} zijn not_a_number: is geen getal @@ -533,17 +552,25 @@ nl: record_invalid: taken: is al in gebruik taken_with_deleted: is al in gebruik (verwijderde groep) - too_long: - too_short: - wrong_length: + too_long: + one: is te lang (niet meer dan éen teken) + other: is te lang (niet meer dan %{count} tekens) + too_short: + one: is te kort (niet minder dan éen teken) + other: is te kort (niet minder dan %{count} tekens) + wrong_length: + one: heeft de verkeerde lengte (moet precies éen zijn) + other: heeft de verkeerde lengte (moet precies %{count} tekens hebben) template: - body: - header: + body: ! 'Controleer alsjeblieft de volgende velden:' + header: + one: ! 'Kon %{model} niet opslaan: éen fout gevonden.' + other: ! 'Kon %{model} niet opslaan: %{count} fouten gevonden.' feedback: create: - notice: + notice: Bericht verstuurd. Vriendelijk bedankt! new: - first_paragraph: + first_paragraph: Probleem gevonden? Voorstel? Idee? Verbeterpunt? We horen graag je feedback. second_paragraph: send: title: @@ -630,11 +657,13 @@ nl: create: notice: Rekening is gemaakt financial_transactions: - create: - notice: De transactie is opgeslagen. - create_collection: - alert: - notice: Alle transacties zijn opgeslagen. + controller: + create: + notice: De transactie is opgeslagen. + create_collection: + alert: + error_note_required: Notitie ontbreekt. + notice: Alle transacties zijn opgeslagen. index: balance: ! 'Tegoed: %{balance}' last_updated_at: (laatst bijgewerkt %{when} geleden) @@ -653,8 +682,8 @@ nl: sidebar: title: ordergroup: - remove: - remove_group: + remove: Verwijderen + remove_group: Huishouden verwijderen transactions: amount: Bedrag date: Datum @@ -708,7 +737,7 @@ nl: title: Tegoeden beheren ordergroups: account_balance: Tegoed - account_statement: + account_statement: Rekeningafschrift contact: name: Naam new_transaction: Nieuwe transactie @@ -717,31 +746,31 @@ nl: foodcoop: ordergroups: index: - name: - only_active: - only_active_desc: - title: + name: Naam ... + only_active: Alleen actieve + only_active_desc: (minstens eenmaal in de laatste 3 maanden besteld) + title: Huishoudens ordergroups: - last_ordered: - name: - user: + last_ordered: laatste besteld + name: Naam + user: Leden users: index: - body: - ph_name: - ph_ordergroup: - profile_link: - title: + body:Hier kun je leden van deze foodcoop een bericht sturen.
Om je eigen contactgegevens te laten zien, moet je die vrijgeven op %{profile_link}.
+ ph_name: Naam ... + ph_ordergroup: Huishouden ... + profile_link: Instellingen + title: Leden workgroups: edit: - invite_link: - invite_new: - title: + invite_link: hier + invite_new: Nieuwe leden kun je %{invite_link} uitnodigen. + title: Groep bewerken index: - body: - title: + body:De groep kan alleen aangepast worden door leden ervan.
Als je lid wilt worden van een groep, stuur dan een bericht aan een van de leden.