diff --git a/.gitignore b/.gitignore index 2bd55feb..d26b8ce7 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ doc/app/ Capfile config/deploy.rb config/deploy/* +.localeapp \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..3e24f134 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,14 @@ +language: ruby +rvm: + - 1.9.3 +services: + - redis-server +before_install: + - "export DISPLAY=:99.0" + - "sh -e /etc/init.d/xvfb start" +before_script: + - "bundle exec rake foodsoft:setup:stock_config" + - "mysql -e 'create database foodsoft_test;'" + - 'printf "test:\n adapter: mysql2\n database: foodsoft_test\n username: travis\n encoding: utf8\n" >config/database.yml' + - 'bundle exec rake db:schema:load RAILS_ENV=test' +script: bundle exec rake spec diff --git a/Gemfile b/Gemfile index f6d953c5..e80314ed 100644 --- a/Gemfile +++ b/Gemfile @@ -17,6 +17,8 @@ group :assets do end gem 'jquery-rails' +gem 'select2-rails' +gem 'bootstrap-datepicker-rails' gem 'mysql2' @@ -35,7 +37,7 @@ gem 'simple-navigation-bootstrap' gem 'meta_search' gem 'acts_as_versioned', git: 'git://github.com/technoweenie/acts_as_versioned.git' # Use this instead of rubygem gem 'acts_as_tree' -gem 'acts_as_configurable', git: 'git://github.com/bwalding/acts_as_configurable.git' +gem "rails-settings-cached", "0.2.4" gem 'resque' gem 'whenever', require: false # For defining cronjobs, see config/schedule.rb @@ -50,11 +52,8 @@ group :development do # Better error output gem 'better_errors' gem 'binding_of_caller' - - # Re-enable rails benchmarker/profiler - gem 'ruby-prof' - gem 'test-unit' - + # gem "rails-i18n-debug" + # Get infos when not using proper eager loading gem 'bullet' @@ -68,3 +67,23 @@ group :development do # Avoid having content-length warnings gem 'thin' end + +group :development, :test do + gem 'ruby-prof' +end + +group :test do + gem 'rspec-rails' + gem 'factory_girl_rails', '~> 4.0' + gem 'faker' + # version requirements to avoid problem http://stackoverflow.com/questions/18114544 + gem 'capybara', '~> 2.1.0' + # webkit and poltergeist don't seem to work yet + gem 'selenium-webdriver', '~> 2.35.1' + gem 'database_cleaner' + gem 'simplecov', require: false + # need to include rspec components before i18n-spec or rake fails in test environment + gem 'rspec-core' + gem 'rspec-expectations' + gem 'i18n-spec' +end diff --git a/Gemfile.lock b/Gemfile.lock index 79296224..c816b8c4 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -4,13 +4,6 @@ GIT specs: localize_input (0.1.0) -GIT - remote: git://github.com/bwalding/acts_as_configurable.git - revision: cdf6f6f979019275b523d10684b748f08e2dd8e8 - specs: - acts_as_configurable (0.0.1) - rake - GIT remote: git://github.com/technoweenie/acts_as_versioned.git revision: 63b1fc8529d028fae632fe80ec0cb25df56cd76b @@ -56,6 +49,8 @@ GEM coderay (>= 1.0.0) erubis (>= 2.7.0) binding_of_caller (0.6.8) + bootstrap-datepicker-rails (1.1.1.1) + railties (>= 3.0) builder (3.0.4) bullet (4.3.0) uniform_notifier @@ -67,6 +62,14 @@ GEM net-ssh-gateway (>= 1.1.0) capistrano-ext (1.2.1) capistrano (>= 1.0.0) + capybara (2.1.0) + mime-types (>= 1.16) + nokogiri (>= 1.3.3) + rack (>= 1.0.0) + rack-test (>= 0.5.4) + xpath (~> 2.0) + childprocess (0.3.9) + ffi (~> 1.0, >= 1.0.11) chronic (0.9.0) client_side_validations (3.1.4) coderay (1.0.8) @@ -79,6 +82,8 @@ GEM coffee-script-source (1.3.3) commonjs (0.2.6) daemons (1.1.9) + database_cleaner (0.7.1) + diff-lcs (1.2.4) erubis (2.7.0) eventmachine (1.0.3) exception_notification (2.6.1) @@ -86,6 +91,14 @@ GEM execjs (1.4.0) multi_json (~> 1.0) expression_parser (0.9.0) + factory_girl (4.2.0) + activesupport (>= 3.0.0) + factory_girl_rails (4.2.1) + factory_girl (~> 4.2.0) + railties (>= 3.0.0) + faker (1.1.2) + i18n (~> 0.5) + ffi (1.9.0) haml (3.1.7) haml-rails (0.3.5) actionpack (>= 3.1, < 4.1) @@ -95,11 +108,15 @@ GEM has_scope (0.5.1) hashery (2.0.1) highline (1.6.19) - hike (1.2.1) + hike (1.2.3) i18n (0.6.1) + i18n-spec (0.4.0) + iso inherited_resources (1.3.1) has_scope (~> 0.5.0) responders (~> 0.6) + iso (0.2.0) + i18n journey (1.0.4) jquery-rails (2.1.3) railties (>= 3.1.0, < 5.0) @@ -133,8 +150,9 @@ GEM activesupport (~> 3.1) polyamorous (~> 0.5.0) mime-types (1.21) + mini_portile (0.5.1) mono_logger (1.1.0) - multi_json (1.7.3) + multi_json (1.7.9) mysql2 (0.3.11) net-scp (1.1.1) net-ssh (>= 2.6.5) @@ -143,6 +161,8 @@ GEM net-ssh (2.6.7) net-ssh-gateway (1.2.0) net-ssh (>= 2.6.5) + nokogiri (1.6.0) + mini_portile (~> 0.5.0) pdf-reader (1.2.0) Ascii85 (~> 1.0.0) hashery (~> 2.0) @@ -172,6 +192,8 @@ GEM activesupport (= 3.2.13) bundler (~> 1.0) railties (= 3.2.13) + rails-settings-cached (0.2.4) + rails (>= 3.0.0) railties (3.2.13) actionpack (= 3.2.13) activesupport (= 3.2.13) @@ -183,7 +205,7 @@ GEM rdoc (3.12.2) json (~> 1.4) redis (3.0.4) - redis-namespace (1.3.0) + redis-namespace (1.3.1) redis (~> 3.0.0) responders (0.9.3) railties (~> 3.1) @@ -193,20 +215,44 @@ GEM redis-namespace (~> 1.2) sinatra (>= 0.9.2) vegas (~> 0.1.2) - ruby-prof (0.11.2) + rspec-core (2.14.2) + rspec-expectations (2.14.0) + diff-lcs (>= 1.1.3, < 2.0) + rspec-mocks (2.14.1) + rspec-rails (2.14.0) + actionpack (>= 3.0) + activesupport (>= 3.0) + railties (>= 3.0) + rspec-core (~> 2.14.0) + rspec-expectations (~> 2.14.0) + rspec-mocks (~> 2.14.0) + ruby-prof (0.13.0) ruby-rc4 (0.1.5) + rubyzip (0.9.9) sass (3.2.1) sass-rails (3.2.5) railties (~> 3.2.0) sass (>= 3.1.10) tilt (~> 1.3) + select2-rails (3.4.2) + sass-rails + thor (~> 0.14) + selenium-webdriver (2.35.1) + childprocess (>= 0.2.5) + multi_json (~> 1.0) + rubyzip (< 1.0.0) + websocket (~> 1.0.4) simple-navigation (3.9.0) activesupport (>= 2.3.2) simple-navigation-bootstrap (0.0.4) simple-navigation (>= 3.7.0) - simple_form (2.0.3) + simple_form (2.1.0) actionpack (~> 3.0) activemodel (~> 3.0) + simplecov (0.7.1) + multi_json (~> 1.0) + simplecov-html (~> 0.7.1) + simplecov-html (0.7.1) sinatra (1.3.6) rack (~> 1.4) rack-protection (~> 1.3) @@ -220,7 +266,6 @@ GEM rack (~> 1.0) tilt (~> 1.1, != 1.3.0) sqlite3 (1.3.6) - test-unit (2.5.3) therubyracer (0.10.2) libv8 (~> 3.3.10) thin (1.5.1) @@ -245,30 +290,38 @@ GEM uniform_notifier (1.1.1) vegas (0.1.11) rack (>= 1.0.0) + websocket (1.0.7) whenever (0.8.1) activesupport (>= 2.3.4) chronic (>= 0.6.3) wikicloth (0.8.0) builder expression_parser + xpath (2.0.0) + nokogiri (~> 1.3) PLATFORMS ruby DEPENDENCIES - acts_as_configurable! acts_as_tree acts_as_versioned! better_errors binding_of_caller + bootstrap-datepicker-rails bullet capistrano (= 2.13.5) capistrano-ext + capybara (~> 2.1.0) client_side_validations coffee-rails (~> 3.2.1) daemons + database_cleaner exception_notification + factory_girl_rails (~> 4.0) + faker haml-rails + i18n-spec inherited_resources jquery-rails kaminari @@ -279,14 +332,20 @@ DEPENDENCIES prawn quiet_assets rails (~> 3.2.9) + rails-settings-cached (= 0.2.4) resque + rspec-core + rspec-expectations + rspec-rails ruby-prof sass-rails (~> 3.2.3) + select2-rails + selenium-webdriver (~> 2.35.1) simple-navigation simple-navigation-bootstrap simple_form + simplecov sqlite3 - test-unit therubyracer thin twitter-bootstrap-rails diff --git a/README.md b/README.md index 7f447972..d3107dae 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,6 @@ -Important --------- - -We changed the branch structure. The rails3 branch is now master. But you can safely send pull requests to rails3. It'll remain there for a couple of weeks. - FoodSoft ========= +[![Build Status](https://travis-ci.org/foodcoops/foodsoft.png?branch=tests-rspec)](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) diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 377d3b0a..c6d6e36a 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -1,14 +1,17 @@ //= require jquery //= require jquery-ui //= require jquery_ujs +//= require select2 //= require twitter/bootstrap //= require jquery.tokeninput -//= require bootstrap-datepicker -//= require bootstrap-datepicker.de +//= require bootstrap-datepicker/core +//= require bootstrap-datepicker/locales/bootstrap-datepicker.de +//= require bootstrap-datepicker/locales/bootstrap-datepicker.nl //= require jquery.observe_field //= require rails.validations //= require_self //= require ordering +//= require stupidtable // allow touch devices to work on click events // http://stackoverflow.com/a/16221066 @@ -112,9 +115,16 @@ $(function() { }); // Use bootstrap datepicker for dateinput - $('.datepicker').datepicker({format: 'yyyy-mm-dd', weekStart: 1, language: 'de'}); + $('.datepicker').datepicker({format: 'yyyy-mm-dd', language: I18n.locale}); + + // See stupidtable.js for initialization of local table sorting }); +// retrigger last local table sorting +function updateSort(table) { + $('.sorting-asc, .sorting-desc', table).toggleClass('.sorting-asc .sorting-desc') + .removeData('sort-dir').trigger('click'); // CAUTION: removing data field of plugin +} // gives the row an yellow background function highlightRow(checkbox) { diff --git a/app/assets/javascripts/ordering.js b/app/assets/javascripts/ordering.js index 1a1421bd..159370f2 100644 --- a/app/assets/javascripts/ordering.js +++ b/app/assets/javascripts/ordering.js @@ -10,6 +10,7 @@ var groupBalance = 0; // available group money var currencySeparator = "."; // default decimal separator var currencyPrecision = 2; // default digits behind comma var currencyUnit = "€"; // default currency +var minimumBalance = 0; // minimum group balance for the order to be succesful var toleranceIsCostly = true; // default tolerance behaviour var isStockit = false; // Wheter the order is from stock oder normal supplier @@ -40,6 +41,10 @@ function setGroupBalance(amount) { groupBalance = amount; } +function setMinimumBalance(amount) { + minimumBalance = amount; +} + function addData(orderArticleId, itemPrice, itemUnit, itemSubtotal, itemQuantityOthers, itemToleranceOthers, allocated, available) { var i = orderArticleId; price[i] = itemPrice; @@ -159,7 +164,7 @@ function updateBalance() { $('#total_balance').val(asMoney(balance)); // determine bgcolor and submit button state according to balance var bgcolor = ''; - if (balance < 0) { + if (balance < minimumBalance) { bgcolor = '#FF0000'; $('#submit_button').attr('disabled', 'disabled') } else { diff --git a/app/assets/javascripts/stupidtable.js b/app/assets/javascripts/stupidtable.js new file mode 100644 index 00000000..4fe62acb --- /dev/null +++ b/app/assets/javascripts/stupidtable.js @@ -0,0 +1,186 @@ +// Stupid jQuery table plugin. + +// Call on a table +// sortFns: Sort functions for your datatypes. +(function($) { + + $.fn.stupidtable = function(sortFns) { + return this.each(function() { + var $table = $(this); + sortFns = sortFns || {}; + + // ==================================================== // + // Utility functions // + // ==================================================== // + + // Merge sort functions with some default sort functions. + sortFns = $.extend({}, { + "int": function(a, b) { + return parseInt(a, 10) - parseInt(b, 10); + }, + "float": function(a, b) { + return parseFloat(a) - parseFloat(b); + }, + "string": function(a, b) { + if (a < b) return -1; + if (a > b) return +1; + return 0; + }, + "string-ins": function(a, b) { + a = a.toLowerCase(); + b = b.toLowerCase(); + if (a < b) return -1; + if (a > b) return +1; + return 0; + } + }, sortFns); + + // Return the resulting indexes of a sort so we can apply + // this result elsewhere. This returns an array of index numbers. + // return[0] = x means "arr's 0th element is now at x" + var sort_map = function(arr, sort_function, reverse_column) { + var map = []; + var index = 0; + if (reverse_column) { + for (var i = arr.length-1; i >= 0; i--) { + map.push(i); + } + } + else { + var sorted = arr.slice(0).sort(sort_function); + for (var i=0; i'); + $('.stupidlink', stupidtables).on('click', function(e) {e.preventDefault();}); + + // Init stupidtable sorting + stupidtables.stupidtable(); + + // Update class of sort link after sort to match foodsoft style + stupidtables.on('aftertablesort', function(e, data) { + // Ignore data and use the updated classes in DOM + var stupidthead = $('thead', this); + $('a.stupidlink', stupidthead).removeClass('sortup sortdown'); + $('th.sorting-asc a.stupidlink', stupidthead).addClass('sortup'); + $('th.sorting-desc a.stupidlink', stupidthead).addClass('sortdown'); + }); + + // Sort tables with a default sort + $('.default-sort', stupidtables).trigger('click'); + } +}); diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index 2b84d28d..24a5b6c4 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -1,4 +1,6 @@ /* *= require bootstrap_and_overrides +*= require select2 *= require token-input-bootstrappy +*= require bootstrap-datepicker */ \ 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 52f2f108..5d595bc2 100644 --- a/app/assets/stylesheets/bootstrap_and_overrides.css.less +++ b/app/assets/stylesheets/bootstrap_and_overrides.css.less @@ -31,10 +31,6 @@ body { // Example: // @linkColor: #ff0000; - -// Bootstrap datepicker -@import "datepicker"; - // Custom styles // Fix empty dd tags in horizontal dl, see https://github.com/twitter/bootstrap/issues/4062 @@ -42,6 +38,22 @@ body { dd { .clearfix(); } } +// Do not use additional margin for input in table +.form-horizontal .control-group.control-group-intable, +.form-horizontal .controls.controls-intable { + margin: 0; +} + +// Light tooltips without empty space below tables +.tooltip-inner { + color: #000; + background-color: rgb(245,245,245); + border: 1px solid #ccc; +} +.tooltip-inner .table { + margin-bottom: 0; +} + @mainRedColor: #ED0606; .logo { @@ -208,3 +220,21 @@ tr.unavailable { dt { width: 160px; } dd { margin-left: 170px; } } + +.settings { + .settings-group { + margin-bottom: 10px; + .control-label { + margin: 5px 0 0 0; + } + } + .control-group { + margin-bottom: 5px; + } + .control-group.h_wrapper { + margin-bottom: 5px; + } + .control-group.select { + margin-bottom: 15px + } +} diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 29b752a0..96b2e510 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,10 +1,13 @@ # encoding: utf-8 class ApplicationController < ActionController::Base + include Foodsoft::ControllerExtensions::Locale + helper_method :available_locales protect_from_forgery - before_filter :select_language, :select_foodcoop, :authenticate, :store_controller, :items_per_page, :set_redirect_to + before_filter :select_foodcoop, :authenticate, :store_controller, :items_per_page, :set_redirect_to after_filter :remove_controller + # Returns the controller handling the current request. def self.current Thread.current[:application_controller] @@ -26,7 +29,7 @@ class ApplicationController < ActionController::Base redirect_to login_url, :alert => 'Access denied!' end - private + private def authenticate(role = 'any') # Attempt to retrieve authenticated user from controller instance or session... @@ -141,9 +144,5 @@ class ApplicationController < ActionController::Base def default_url_options(options = {}) {foodcoop: FoodsoftConfig.scope} end - - # Used to prevent accidently switching to :en in production mode. - def select_language - I18n.locale = :de - end + end diff --git a/app/controllers/articles_controller.rb b/app/controllers/articles_controller.rb index 2e834ab5..b29fd594 100644 --- a/app/controllers/articles_controller.rb +++ b/app/controllers/articles_controller.rb @@ -32,7 +32,7 @@ class ArticlesController < ApplicationController end def new - @article = @supplier.articles.build(:tax => 7.0) + @article = @supplier.articles.build(:tax => FoodsoftConfig[:tax_default]) render :layout => false end @@ -145,19 +145,22 @@ class ArticlesController < ApplicationController begin @articles = Array.new articles, outlisted_articles = FoodsoftFile::parse(params[:articles]["file"]) + no_category = ArticleCategory.new articles.each do |row| + # fallback to Others category + category = (ArticleCategory.find_by_name(row[:category]) or no_category) # creates a new article and price article = Article.new( :name => row[:name], :note => row[:note], :manufacturer => row[:manufacturer], :origin => row[:origin], :unit => row[:unit], - :article_category => ArticleCategory.find_by_name(row[:category]), + :article_category => category, :price => row[:price], :unit_quantity => row[:unit_quantity], :order_number => row[:number], :deposit => row[:deposit], - :tax => row[:tax]) + :tax => (row[:tax] or FoodsoftConfig[:tax_default])) # stop parsing, when an article isn't valid unless article.valid? raise I18n.t('articles.controller.error_parse', :msg => article.errors.full_messages.join(", "), :line => (articles.index(row) + 2).to_s) @@ -206,7 +209,7 @@ class ArticlesController < ApplicationController # fills a form whith values of the selected shared_article def import - @article = SharedArticle.find(params[:shared_article_id]).build_new_article + @article = SharedArticle.find(params[:shared_article_id]).build_new_article(@supplier) render :action => 'new', :layout => false end diff --git a/app/controllers/deliveries_controller.rb b/app/controllers/deliveries_controller.rb index d8876c16..86eedc14 100644 --- a/app/controllers/deliveries_controller.rb +++ b/app/controllers/deliveries_controller.rb @@ -5,43 +5,25 @@ class DeliveriesController < ApplicationController def index @deliveries = @supplier.deliveries.all :order => 'delivered_on DESC' - - respond_to do |format| - format.html # index.html.erb - format.xml { render :xml => @deliveries } - end end def show @delivery = Delivery.find(params[:id]) - - respond_to do |format| - format.html # show.html.erb - format.xml { render :xml => @delivery } - end end def new @delivery = @supplier.deliveries.build - - respond_to do |format| - format.html # new.html.erb - format.xml { render :xml => @delivery } - end + @delivery.delivered_on = Date.today #TODO: move to model/database end def create @delivery = Delivery.new(params[:delivery]) - - respond_to do |format| - if @delivery.save - flash[:notice] = I18n.t('deliveries.create.notice') - format.html { redirect_to([@supplier,@delivery]) } - format.xml { render :xml => @delivery, :status => :created, :location => @delivery } - else - format.html { render :action => "new" } - format.xml { render :xml => @delivery.errors, :status => :unprocessable_entity } - end + + if @delivery.save + flash[:notice] = I18n.t('deliveries.create.notice') + redirect_to [@supplier, @delivery] + else + render :action => "new" end end @@ -52,15 +34,11 @@ class DeliveriesController < ApplicationController def update @delivery = Delivery.find(params[:id]) - respond_to do |format| - if @delivery.update_attributes(params[:delivery]) - flash[:notice] = I18n.t('deliveries.update.notice') - format.html { redirect_to([@supplier,@delivery]) } - format.xml { head :ok } - else - format.html { render :action => "edit" } - format.xml { render :xml => @delivery.errors, :status => :unprocessable_entity } - end + if @delivery.update_attributes(params[:delivery]) + flash[:notice] = I18n.t('deliveries.update.notice') + redirect_to [@supplier,@delivery] + else + render :action => "edit" end end @@ -69,40 +47,60 @@ class DeliveriesController < ApplicationController @delivery.destroy flash[:notice] = I18n.t('deliveries.destroy.notice') - respond_to do |format| - format.html { redirect_to(supplier_deliveries_url(@supplier)) } - format.xml { head :ok } + redirect_to supplier_deliveries_url(@supplier) + end + + # three possibilites to fill a new_stock_article form + # (1) start from blank or use params + def new_stock_article + @stock_article = @supplier.stock_articles.build(params[:stock_article]) + + render :layout => false + end + + # (2) StockArticle as template + def copy_stock_article + @stock_article = StockArticle.find(params[:old_stock_article_id]).dup + + render :layout => false + end + + # (3) non-stock Article as template + def derive_stock_article + @stock_article = Article.find(params[:old_article_id]).becomes(StockArticle).dup + + render :layout => false + end + + def create_stock_article + @stock_article = StockArticle.new(params[:stock_article]) + + if @stock_article.valid? and @stock_article.save + render :layout => false + else + render :action => 'new_stock_article', :layout => false end end - def add_stock_article - article = @supplier.stock_articles.build(params[:stock_article]) - render :update do |page| - if article.save - logger.debug "new StockArticle: #{article.id}" - page.insert_html :bottom, 'stock_changes', :partial => 'stock_change', - :locals => {:stock_change => article.stock_changes.build, :supplier => @supplier} + def edit_stock_article + @stock_article = StockArticle.find(params[:stock_article_id]) + render :layout => false + end - page.replace_html 'new_stock_article', :partial => 'stock_article_form', - :locals => {:stock_article => @supplier.stock_articles.build} - else - page.replace_html 'new_stock_article', :partial => 'stock_article_form', - :locals => {:stock_article => article} - end + def update_stock_article + @stock_article = StockArticle.find(params[:stock_article][:id]) + + if @stock_article.update_attributes(params[:stock_article]) + render :layout => false + else + render :action => 'edit_stock_article', :layout => false end end def add_stock_change + @stock_change = StockChange.new + @stock_change.stock_article = StockArticle.find(params[:stock_article_id]) render :layout => false end - def fill_new_stock_article_form - article = Article.find(params[:article_id]) - @supplier = article.supplier - stock_article = @supplier.stock_articles.build( - article.attributes.reject { |attr| attr == ('id' || 'type')} - ) - - render :partial => 'stock_article_form', :locals => {:stock_article => stock_article} - end end 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/home_controller.rb b/app/controllers/home_controller.rb index 55afb7b2..99f108ce 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -16,6 +16,7 @@ class HomeController < ApplicationController def update_profile if @current_user.update_attributes(params[:user]) + session[:locale] = @current_user.locale redirect_to my_profile_url, notice: I18n.t('home.changes_saved') else render :profile diff --git a/app/controllers/login_controller.rb b/app/controllers/login_controller.rb index 745d5e58..3aab74c5 100644 --- a/app/controllers/login_controller.rb +++ b/app/controllers/login_controller.rb @@ -58,6 +58,7 @@ class LoginController < ApplicationController if @user.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 diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index c89b818d..7de1b9de 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -11,6 +11,8 @@ class SessionsController < ApplicationController if user session[:user_id] = user.id session[:scope] = FoodsoftConfig.scope # Save scope in session to not allow switching between foodcoops with one account + session[:locale] = user.locale + if session[:return_to].present? redirect_to_url = session[:return_to] session[:return_to] = nil diff --git a/app/controllers/stockit_controller.rb b/app/controllers/stockit_controller.rb index 9ee40efe..3e3b43b2 100644 --- a/app/controllers/stockit_controller.rb +++ b/app/controllers/stockit_controller.rb @@ -55,4 +55,9 @@ 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/controllers/suppliers_controller.rb b/app/controllers/suppliers_controller.rb index 088243f8..65b77e1b 100644 --- a/app/controllers/suppliers_controller.rb +++ b/app/controllers/suppliers_controller.rb @@ -18,7 +18,7 @@ class SuppliersController < ApplicationController def new if params[:shared_supplier_id] shared_supplier = SharedSupplier.find(params[:shared_supplier_id]) - @supplier = shared_supplier.build_supplier(shared_supplier.autofill_attributes) + @supplier = shared_supplier.suppliers.new(shared_supplier.autofill_attributes) else @supplier = Supplier.new end diff --git a/app/controllers/tasks_controller.rb b/app/controllers/tasks_controller.rb index 16fc0f73..ee4c05cd 100644 --- a/app/controllers/tasks_controller.rb +++ b/app/controllers/tasks_controller.rb @@ -18,6 +18,9 @@ class TasksController < ApplicationController def create @task = Task.new(params[:task]) + if params[:periodic] + @task.periodic_task_group = PeriodicTaskGroup.new + end if @task.save redirect_to tasks_url, :notice => I18n.t('tasks.create.notice') else @@ -32,13 +35,20 @@ class TasksController < ApplicationController def edit @task = Task.find(params[:id]) @task.current_user_id = current_user.id + if @task.periodic? + flash.now[:alert] = I18n.t('tasks.edit.warning_periodic').html_safe + end end def update @task = Task.find(params[:id]) + was_periodic = @task.periodic? @task.attributes=(params[:task]) if @task.errors.empty? && @task.save flash[:notice] = I18n.t('tasks.update.notice') + if was_periodic and not @task.periodic? + flash[:notice] = I18n.t('tasks.update.notice_converted') + end if @task.workgroup redirect_to workgroup_tasks_url(workgroup_id: @task.workgroup_id) else @@ -53,7 +63,12 @@ class TasksController < ApplicationController task = Task.find(params[:id]) # Save user_ids to update apple statistics after destroy user_ids = task.user_ids - task.destroy + if params[:periodic] + task.periodic_task_group.exclude_tasks_before(task) + task.periodic_task_group.destroy + else + task.destroy + end task.update_ordergroup_stats(user_ids) redirect_to tasks_url, :notice => I18n.t('tasks.destroy.notice') 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/documents/order_fax.rb b/app/documents/order_fax.rb index 0caaaa4e..f2182944 100644 --- a/app/documents/order_fax.rb +++ b/app/documents/order_fax.rb @@ -20,11 +20,17 @@ class OrderFax < OrderPdf move_down 5 text "#{contact[:zip_code]} #{contact[:city]}", size: 9, align: :right move_down 5 - text "#{I18n.t('simple_form.labels.supplier.customer_number')}: #{@order.supplier.try(:customer_number)}", size: 9, align: :right - move_down 5 - text "#{I18n.t('simple_form.labels.supplier.phone')}: #{contact[:phone]}", size: 9, align: :right - move_down 5 - text "#{I18n.t('simple_form.labels.supplier.email')}: #{contact[:email]}", size: 9, align: :right + unless @order.supplier.try(:customer_number).blank? + text "#{I18n.t('simple_form.labels.supplier.customer_number')}: #{@order.supplier[:customer_number]}", size: 9, align: :right + move_down 5 + end + unless contact[:phone].blank? + text "#{I18n.t('simple_form.labels.supplier.phone')}: #{contact[:phone]}", size: 9, align: :right + move_down 5 + end + unless contact[:email].blank? + text "#{I18n.t('simple_form.labels.supplier.email')}: #{contact[:email]}", size: 9, align: :right + end end # Recipient @@ -32,8 +38,10 @@ class OrderFax < OrderPdf text @order.name move_down 5 text @order.supplier.try(:address).to_s - move_down 5 - text "#{I18n.t('simple_form.labels.supplier.fax')}: #{@order.supplier.try(:fax)}" + unless @order.supplier.try(:fax).blank? + move_down 5 + text "#{I18n.t('simple_form.labels.supplier.fax')}: #{@order.supplier[:fax]}" + end end move_down 5 @@ -42,25 +50,37 @@ class OrderFax < OrderPdf move_down 10 text "#{I18n.t('simple_form.labels.delivery.delivered_on')}:" move_down 10 - text "#{I18n.t('simple_form.labels.supplier.contact_person')}: #{@order.supplier.try(:contact_person)}" - move_down 10 + unless @order.supplier.try(:contact_person).blank? + text "#{I18n.t('simple_form.labels.supplier.contact_person')}: #{@order.supplier[:contact_person]}" + move_down 10 + end # Articles + total = 0 data = [I18n.t('documents.order_fax.rows')] data += @order.order_articles.ordered.all(include: :article).collect do |a| + subtotal = a.units_to_order * a.price.unit_quantity * a.price.price + total += subtotal [a.article.order_number, a.units_to_order, a.article.name, a.price.unit_quantity, a.article.unit, - a.price.price] + number_to_currency(a.price.price), + number_to_currency(subtotal)] end + data << [I18n.t('documents.order_fax.total'), nil, nil, nil, nil, nil, number_to_currency(total)] table data, cell_style: {size: 8, overflow: :shrink_to_fit} do |table| + table.header = true table.cells.border_width = 1 table.cells.border_color = '666666' + table.row(0).border_bottom_width = 2 table.columns(1).align = :right - table.columns(3..5).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).border_top_width = 2 end #font_size: 8, #vertical_padding: 3, diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 78a2fc40..167a558d 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -158,5 +158,13 @@ module ApplicationHelper end flash_messages.join("\n").html_safe end + + # render base errors in a form after failed validation + # http://railsapps.github.io/twitter-bootstrap-rails.html + def base_errors resource + return '' if (resource.errors.empty?) or (resource.errors[:base].empty?) + messages = resource.errors[:base].map { |msg| content_tag(:li, msg) }.join + render :partial => 'shared/base_errors', :locals => {:error_messages => messages} + end end diff --git a/app/helpers/deliveries_helper.rb b/app/helpers/deliveries_helper.rb index 6780479b..b23107cc 100644 --- a/app/helpers/deliveries_helper.rb +++ b/app/helpers/deliveries_helper.rb @@ -1,5 +1,5 @@ module DeliveriesHelper - + def link_to_invoice(delivery) if delivery.invoice link_to number_to_currency(delivery.invoice.amount), [:finance, delivery.invoice], @@ -9,9 +9,29 @@ module DeliveriesHelper class: 'btn btn-mini' end end - - def stock_articles_for_select(supplier) - supplier.stock_articles.undeleted.reorder('articles.name ASC').map {|a| ["#{a.name} (#{number_to_currency a.price}/#{a.unit})", a.id] } + + def articles_for_select2(supplier) + supplier.articles.undeleted.reorder('articles.name ASC').map {|a| {:id => a.id, :text => "#{a.name} (#{number_to_currency a.price}/#{a.unit})"} } end - + + def stock_articles_for_table(supplier) + supplier.stock_articles.undeleted.reorder('articles.name ASC') + end + + def stock_change_remove_link(stock_change_form) + return link_to t('.remove_article'), "#", :class => 'remove_new_stock_change btn btn-small' if stock_change_form.object.new_record? + output = stock_change_form.hidden_field :_destroy + output += link_to t('.remove_article'), "#", :class => 'destroy_stock_change btn btn-small' + return output.html_safe + end + + def stock_article_price_hint(stock_article) + t('simple_form.hints.stock_article.edit_stock_article.price', + :stock_article_copy_link => link_to(t('.copy_stock_article'), + copy_stock_article_supplier_deliveries_path(@supplier, :old_stock_article_id => stock_article.id), + :remote => true + ) + ) + end + end diff --git a/app/helpers/finance/order_articles_helper.rb b/app/helpers/finance/order_articles_helper.rb index 548dfc4f..0ebbc835 100644 --- a/app/helpers/finance/order_articles_helper.rb +++ b/app/helpers/finance/order_articles_helper.rb @@ -4,7 +4,7 @@ module Finance::OrderArticlesHelper if @order.stockit? StockArticle.order('articles.name') else - @order.supplier.articles.order('articles.name') + @order.supplier.articles.undeleted.order('articles.name') end end end diff --git a/app/helpers/stockit_helper.rb b/app/helpers/stockit_helper.rb index 2888603c..f6a0a1fc 100644 --- a/app/helpers/stockit_helper.rb +++ b/app/helpers/stockit_helper.rb @@ -4,4 +4,14 @@ module StockitHelper class_names << "unavailable" if article.quantity_available <= 0 class_names.join(" ") end + + def link_to_stock_change_reason(stock_change) + if stock_change.delivery_id + link_to t('.delivery'), supplier_delivery_path(stock_change.delivery.supplier, stock_change.delivery) + elsif stock_change.order_id + link_to t('.order'), order_path(stock_change.order) + elsif stock_change.stock_taking_id + link_to t('.stock_taking'), stock_taking_path(stock_change.stock_taking) + end + end end diff --git a/app/helpers/suppliers_helper.rb b/app/helpers/suppliers_helper.rb new file mode 100644 index 00000000..9876f11d --- /dev/null +++ b/app/helpers/suppliers_helper.rb @@ -0,0 +1,6 @@ +module SuppliersHelper + + def associated_supplier_names(shared_supplier) + "(#{shared_supplier.suppliers.map(&:name).join(', ')})" + end +end \ No newline at end of file diff --git a/app/models/delivery.rb b/app/models/delivery.rb index 3add6fdf..91ee4ca8 100644 --- a/app/models/delivery.rb +++ b/app/models/delivery.rb @@ -2,11 +2,15 @@ class Delivery < ActiveRecord::Base belongs_to :supplier has_one :invoice - has_many :stock_changes, :dependent => :destroy + has_many :stock_changes, + :dependent => :destroy, + :include => 'stock_article', + :order => 'articles.name ASC' scope :recent, :order => 'created_at DESC', :limit => 10 - validates_presence_of :supplier_id + validates_presence_of :supplier_id, :delivered_on + validate :stock_articles_must_be_unique accepts_nested_attributes_for :stock_changes, :allow_destroy => :true @@ -15,6 +19,18 @@ class Delivery < ActiveRecord::Base stock_changes.build(attributes) unless attributes[:quantity].to_i == 0 end end + + def includes_article?(article) + self.stock_changes.map{|stock_change| stock_change.stock_article.id}.include? article.id + end + + 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 + end end 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 36e67828..f90ecef4 100644 --- a/app/models/group_order_article.rb +++ b/app/models/group_order_article.rb @@ -14,7 +14,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 diff --git a/app/models/ordergroup.rb b/app/models/ordergroup.rb index afa90211..6e1b65dc 100644 --- a/app/models/ordergroup.rb +++ b/app/models/ordergroup.rb @@ -108,13 +108,13 @@ class Ordergroup < Group # Make sure, the name is uniq, add usefull message if uniq group is already deleted def uniqueness_of_name - id = new_record? ? nil : self.id - group = Ordergroup.where('groups.id != ? AND groups.name = ?', id, name).first - if group.present? - message = group.deleted? ? :taken_with_deleted : :taken + group = Ordergroup.where('groups.name = ?', name) + group = group.where('groups.id != ?', self.id) unless new_record? + if group.exists? + message = group.first.deleted? ? :taken_with_deleted : :taken errors.add :name, message end end - + end diff --git a/app/models/periodic_task_group.rb b/app/models/periodic_task_group.rb new file mode 100644 index 00000000..b92de76c --- /dev/null +++ b/app/models/periodic_task_group.rb @@ -0,0 +1,29 @@ +class PeriodicTaskGroup < ActiveRecord::Base + has_many :tasks, dependent: :destroy + + PeriodDays = 7 + + def has_next_task? + return false if tasks.empty? + return false if tasks.first.due_date.nil? + return true + end + + def create_next_task + template_task = tasks.first + self.next_task_date ||= template_task.due_date + PeriodDays + + next_task = template_task.dup + next_task.due_date = next_task_date + next_task.save + + self.next_task_date += PeriodDays + self.save + end + + def exclude_tasks_before(task) + tasks.where("due_date < '#{task.due_date}'").each do |t| + t.update_attribute(:periodic_task_group, nil) + end + end +end diff --git a/app/models/shared_article.rb b/app/models/shared_article.rb index 777b7f77..440842ec 100644 --- a/app/models/shared_article.rb +++ b/app/models/shared_article.rb @@ -7,8 +7,8 @@ class SharedArticle < ActiveRecord::Base belongs_to :shared_supplier, :foreign_key => :supplier_id - def build_new_article - shared_supplier.supplier.articles.build( + def build_new_article(supplier) + supplier.articles.build( :name => name, :unit => unit, :note => note, diff --git a/app/models/shared_supplier.rb b/app/models/shared_supplier.rb index 04eb290c..86eebda3 100644 --- a/app/models/shared_supplier.rb +++ b/app/models/shared_supplier.rb @@ -5,7 +5,7 @@ class SharedSupplier < ActiveRecord::Base # set correct table_name in external DB self.table_name = 'suppliers' - has_one :supplier + has_many :suppliers has_many :shared_articles, :foreign_key => :supplier_id # These set of attributes are used to autofill attributes of new supplier, diff --git a/app/models/stock_article.rb b/app/models/stock_article.rb index 0124219a..5d45802e 100644 --- a/app/models/stock_article.rb +++ b/app/models/stock_article.rb @@ -17,6 +17,10 @@ class StockArticle < Article quantity - OrderArticle.where(article_id: id). joins(:order).where("orders.state = 'open' OR orders.state = 'finished'").sum(:units_to_order) end + + def quantity_history + stock_changes.reorder('stock_changes.created_at ASC').map{|s| s.quantity}.cumulative_sum + end def self.stock_value available.collect { |a| a.quantity * a.gross_price }.sum diff --git a/app/models/stock_change.rb b/app/models/stock_change.rb index 6a7adc75..9c4bf082 100644 --- a/app/models/stock_change.rb +++ b/app/models/stock_change.rb @@ -1,6 +1,7 @@ class StockChange < ActiveRecord::Base belongs_to :delivery belongs_to :order + belongs_to :stock_taking belongs_to :stock_article validates_presence_of :stock_article_id, :quantity diff --git a/app/models/supplier.rb b/app/models/supplier.rb index 01f93d35..0dba2a1e 100644 --- a/app/models/supplier.rb +++ b/app/models/supplier.rb @@ -13,11 +13,9 @@ class Supplier < ActiveRecord::Base :delivery_days, :order_howto, :note, :shared_supplier_id, :min_order_quantity validates :name, :presence => true, :length => { :in => 4..30 } - validates :phone, :presence => true, :length => { :in => 8..20 } + validates :phone, :presence => true, :length => { :in => 8..25 } validates :address, :presence => true, :length => { :in => 8..50 } validates_length_of :order_howto, :note, maximum: 250 - validates_length_of :phone, :in => 8..20 - validates_length_of :address, :in => 8..50 validate :uniqueness_of_name scope :undeleted, -> { where(deleted_at: nil) } @@ -82,10 +80,10 @@ class Supplier < ActiveRecord::Base # Make sure, the name is uniq, add usefull message if uniq group is already deleted def uniqueness_of_name - id = new_record? ? nil : self.id - supplier = Supplier.where('suppliers.id != ? AND suppliers.name = ?', id, name).first - if supplier.present? - message = supplier.deleted? ? :taken_with_deleted : :taken + supplier = Supplier.where('suppliers.name = ?', name) + supplier = supplier.where('suppliers.id != ?', self.id) unless new_record? + if 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 64c5ff20..bd2f23d6 100644 --- a/app/models/task.rb +++ b/app/models/task.rb @@ -1,7 +1,9 @@ +# -*- coding: utf-8 -*- class Task < ActiveRecord::Base has_many :assignments, :dependent => :destroy has_many :users, :through => :assignments belongs_to :workgroup + belongs_to :periodic_task_group scope :non_group, where(workgroup_id: nil, done: false) scope :done, where(done: true) @@ -16,7 +18,9 @@ class Task < ActiveRecord::Base validates :required_users, :presence => true validates_numericality_of :duration, :required_users, :only_integer => true, :greater_than => 0 validates_length_of :description, maximum: 250 + validates :done, exclusion: { in: [true] }, if: :periodic?, on: :create + before_save :exclude_from_periodic_task_group, if: :changed?, unless: :new_record? after_save :update_ordergroup_stats # Find all tasks, for which the current user should be responsible @@ -46,6 +50,10 @@ class Task < ActiveRecord::Base end end + def periodic? + not periodic_task_group.nil? + end + def is_assigned?(user) self.assignments.detect {|ass| ass.user_id == user.id } end @@ -100,5 +108,10 @@ class Task < ActiveRecord::Base def update_ordergroup_stats(user_ids = self.user_ids) Ordergroup.joins(:users).where(users: {id: user_ids}).each(&:update_stats!) end + + def exclude_from_periodic_task_group + self.periodic_task_group = nil + true + end end diff --git a/app/models/user.rb b/app/models/user.rb index 359bf51c..2b4a399b 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -3,6 +3,7 @@ require 'digest/sha1' # specific user rights through memberships (see Group) class User < ActiveRecord::Base + include RailsSettings::Extend #TODO: acts_as_paraniod ?? has_many :memberships, :dependent => :destroy @@ -19,8 +20,11 @@ class User < ActiveRecord::Base has_many :pages, :foreign_key => 'updated_by' has_many :created_orders, :class_name => 'Order', :foreign_key => 'created_by_user_id', :dependent => :nullify - attr_accessor :password, :setting_attributes - + attr_accessor :password, :settings_attributes + + # makes the current_user (logged-in-user) available in models + cattr_accessor :current_user + validates_presence_of :nick, :email validates_presence_of :password, :on => :create validates_length_of :nick, :in => 2..25 @@ -32,53 +36,37 @@ class User < ActiveRecord::Base validates_length_of :password, :in => 5..25, :allow_blank => true before_validation :set_password - after_save :update_settings - - # Adds support for configuration settings (through "settings" attribute). - acts_as_configurable - - # makes the current_user (logged-in-user) available in models - cattr_accessor :current_user - - # User settings keys - # returns the User-settings and the translated description - def self.setting_keys - { - "notify.orderFinished" => I18n.t('model.user.notify.order_finished'), - "notify.negativeBalance" => I18n.t('model.user.notify.negative_balance'), - "notify.upcoming_tasks" => I18n.t('model.user.notify.upcoming_tasks'), - "messages.sendAsEmail" => I18n.t('model.user.notify.send_as_email'), - "profile.phoneIsPublic" => I18n.t('model.user.notify.phone_is_public'), - "profile.emailIsPublic" => I18n.t('model.user.notify.email_is_public'), - "profile.nameIsPublic" => I18n.t('model.user.notify.name_is_public') - } + after_initialize do + settings.defaults['profile'] = { 'language' => I18n.default_locale } unless settings.profile + settings.defaults['messages'] = { 'send_as_email' => true } unless settings.messages + settings.defaults['notify'] = { 'upcoming_tasks' => true } unless settings.notify end - # retuns the default setting for a NEW user - # for old records nil will returned - # TODO: integrate default behaviour in acts_as_configurable plugin - def settings_default(setting) - # define a default for the settings - defaults = { - "messages.sendAsEmail" => true, - "notify.upcoming_tasks" => true - } - return true if self.new_record? && defaults[setting] - end - - def update_settings - unless setting_attributes.nil? - for setting in User::setting_keys.keys - self.settings[setting] = setting_attributes[setting] && setting_attributes[setting] == '1' ? '1' : nil + + after_save do + return if settings_attributes.nil? + 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 + self.settings.merge!(key, value) end end + def locale + settings.profile['language'] + end + def name [first_name, last_name].join(" ") end def receive_email? - settings['messages.sendAsEmail'] == "1" && email.present? + settings.messages['send_as_email'] && email.present? end # Sets the user's password. It will be stored encrypted along with a random salt. diff --git a/app/models/workgroup.rb b/app/models/workgroup.rb index 6c93d693..3bcf124d 100644 --- a/app/models/workgroup.rb +++ b/app/models/workgroup.rb @@ -3,54 +3,12 @@ class Workgroup < Group has_many :tasks # returns all non-finished tasks - has_many :open_tasks, :class_name => 'Task', :conditions => ['done = ?', false], :order => 'due_date ASC' + has_many :open_tasks, :class_name => 'Task', :conditions => ['done = ?', false], order: 'due_date ASC, name ASC' validates_uniqueness_of :name - validates_presence_of :task_name, :weekday, :task_required_users, :next_weekly_tasks_number, - :if => :weekly_task - validates_numericality_of :next_weekly_tasks_number, :greater_than => 0, :less_than => 21, :only_integer => true, - :if => :weekly_task - validates_length_of :task_description, maximum: 250 validate :last_admin_on_earth, :on => :update before_destroy :check_last_admin_group - def self.weekdays - days = I18n.t('date.day_names') - (0..days.length-1).map {|i| [days[i], i.to_s]} - end - - # Returns an Array with date-objects to represent the next weekly-tasks - def next_weekly_tasks - # our system starts from 0 (sunday) to 6 (saturday) - # get difference between groups weekday and now - diff = self.weekday - Time.now.wday - if diff >= 0 - # weektask is in current week - nextTask = diff.day.from_now - else - # weektask is in the next week - nextTask = (diff + 7).day.from_now - end - # now generate the Array - nextTasks = Array.new - next_weekly_tasks_number.times do - nextTasks << nextTask.to_date - nextTask = 1.week.from_now(nextTask) - end - return nextTasks - end - - def task_attributes(date) - { - :name => task_name, - :description => task_description, - :due_date => date, - :required_users => task_required_users, - :duration => task_duration, - :weekly => true - } - end - protected # Check before destroy a group, if this is the last group with admin role diff --git a/app/views/admin/users/show.html.haml b/app/views/admin/users/show.html.haml index a954b7d5..4d0cd1ab 100644 --- a/app/views/admin/users/show.html.haml +++ b/app/views/admin/users/show.html.haml @@ -20,10 +20,10 @@ .well %h4= t '.preference' %table.table - - for setting in User::setting_keys.keys + - @user.settings.profile.each do |key, setting| %tr - %td= User::setting_keys[setting] - %td= @user.settings[setting] == '1' ? t('simple_form.yes') : t('simple_form.no') + %td= t("simple_form.labels.settings.profile.#{key}") + %td= (setting != true and setting != false) ? setting : (setting === true ? t('simple_form.yes') : t('simple_form.no')) .span3 .well %h4= t '.groupabos' diff --git a/app/views/articles/_edit_all_table.html.haml b/app/views/articles/_edit_all_table.html.haml index 3cbf1412..bd9441f7 100644 --- a/app/views/articles/_edit_all_table.html.haml +++ b/app/views/articles/_edit_all_table.html.haml @@ -19,7 +19,9 @@ - @articles.each_with_index do |article, index| = fields_for "articles[#{article.id || index}]", article do |form| %tr - %td= form.check_box 'availability' + %td + = yield form # allow to add hidden fields to form + = form.check_box 'availability' %td= form.text_field 'name', class: 'input-medium' %td= form.text_field 'unit', class: 'input-mini' %td= form.text_field 'price', class: 'input-mini' diff --git a/app/views/articles/parse_upload.html.haml b/app/views/articles/parse_upload.html.haml index edd34e88..9f322946 100644 --- a/app/views/articles/parse_upload.html.haml +++ b/app/views/articles/parse_upload.html.haml @@ -2,7 +2,9 @@ %p= t('.body').html_safe = form_tag(create_from_upload_supplier_articles_path(@supplier)) do - = render 'edit_all_table' + = render layout: 'edit_all_table' do |form| + = form.hidden_field :manufacturer + = form.hidden_field :origin .form-actions = submit_tag t('.submit', supplier: @supplier.name), class: 'btn btn-primary' = link_to t('ui.or_cancel'), upload_supplier_articles_path(@supplier) diff --git a/app/views/deliveries/_form.html.haml b/app/views/deliveries/_form.html.haml index 5eff702d..c1702708 100644 --- a/app/views/deliveries/_form.html.haml +++ b/app/views/deliveries/_form.html.haml @@ -1,45 +1,134 @@ - content_for :javascript do :javascript $(function() { - $('.destroy_stock_change').live('click', function() { - $(this).prev('input').val('1').parent().hide(); + $('#stock_changes').on('click', '.destroy_stock_change', function() { + $(this).prev('input').val('1'); // check for destruction + + var stock_change = $(this).closest('tr'); + stock_change.hide(); // do not remove (to ensure destruction) + stock_change.removeAttr('id'); // remove id to allow re-adding + mark_article_for_delivery( stock_change.data('id') ); return false; }); - $('.remove_new_stock_change').live('click', function() { - $(this).parent().remove(); + $('#stock_changes').on('click', '.remove_new_stock_change', function() { + var stock_change = $(this).closest('tr'); + stock_change.remove(); + mark_article_for_delivery( stock_change.data('id') ); return false; }) + + $('#new_stock_article').removeAttr('disabled').select2({ + placeholder: '#{t '.create_stock_article'}', + data: #{articles_for_select2(@supplier).to_json}, + createSearchChoice: function(term) { + return { + id: 'new', + text: term + }; + }, + formatResult: function(result, container, query, escapeMarkup) { + if(result.id == 'new') { + return result.text + ' (#{t '.create_from_blank'})'; + } + var markup=[]; + Select2.util.markMatch(result.text, query.term, markup, escapeMarkup); + return markup.join(""); + } + }).on('change', function(e) { + var selectedArticle = $(e.currentTarget).select2('data'); + if(!selectedArticle) { + return false; + } + if('new' == selectedArticle.id) { + $.ajax({ + url: '#{new_stock_article_supplier_deliveries_path(@supplier)}', + type: 'get', + data: {stock_article: {name: selectedArticle.text}}, + contentType: 'application/json; charset=UTF-8' + }); + $('#new_stock_article').select2('data', null); + return true; + } + if('' != selectedArticle.id) { + $.ajax({ + url: '#{derive_stock_article_supplier_deliveries_path(@supplier)}', + type: 'get', + data: {old_article_id: selectedArticle.id}, + contentType: 'application/json; charset=UTF-8' + }); + $('#new_stock_article').select2('data', null); + return true; + } + }); + + enablePriceTooltips(); }); + + function mark_article_for_delivery(stock_article_id) { + var articleTr = $('#stock_article_' + stock_article_id); + if( is_article_available_for_delivery(stock_article_id) ) { + articleTr.removeClass('unavailable'); + $('.button-add-stock-change', articleTr).removeAttr('disabled'); + } + else { + articleTr.addClass('unavailable'); + $('.button-add-stock-change', articleTr).attr('disabled', 'disabled'); + } + } + function is_article_available_for_delivery(stock_article_id) { + return ( 0 == $('#stock_change_stock_article_' + stock_article_id).length ); + } + + function enablePriceTooltips(context) { + $('[data-toggle~="tooltip"]', context).tooltip({ + animation: false, + html: true, + placement: 'left' + }); + } = simple_form_for [@supplier, @delivery], validate: true do |f| - = f.hidden_field :supplier_id - #stock_changes - = f.fields_for :stock_changes do |stock_change_form| - %p - = stock_change_form.select :stock_article_id, stock_articles_for_select(@supplier) - Menge - = stock_change_form.text_field :quantity, size: 5, autocomplete: 'off' - = stock_change_form.hidden_field :_destroy - = link_to t('.remove_article'), "#", class: 'destroy_stock_change' - %p - = link_to t('.add_article'), {action: 'add_stock_change', supplier_id: @supplier.id}, remote: true - %p - %small= t('.note_new_article', new_link: link_to(t('.note_new_article_link'), new_stock_article_path)).html_safe - %hr/ + = f.error_notification + = base_errors f.object + = f.association :supplier, :as => :hidden + + %h2= t '.title_select_stock_articles' + %table#stock_articles_for_adding.table.table-hover.stupidtable + %thead + %tr + %th.default-sort{:data => {:sort => 'string'}}= t '.article' + %th= t '.price' + %th= t '.unit' + %th= t '.category' + %th= t '.actions' + %tfoot + %tr + %th{:colspan => 5} + - if articles_for_select2(@supplier).empty? + = link_to t('.create_stock_article'), new_stock_article_supplier_deliveries_path(@supplier), :remote => true, :class => 'btn' + - else + %input#new_stock_article{:style => 'width: 500px;'} + %tbody + - for article in stock_articles_for_table(@supplier) + = render :partial => 'stock_article_for_adding', :locals => {:article => article} + + %h2= t '.title_fill_quantities' + %table.table#stock_changes.stupidtable + %thead + %tr + %th.default-sort{:data => {:sort => 'string'}}= t '.article' + %th= t '.price' + %th= t '.unit' + %th= t '.quantity' + %th= t '.actions' + %tbody + = f.simple_fields_for :stock_changes do |stock_change_form| + = render :partial => 'stock_change_fields', :locals => {:f => stock_change_form} + + %h2= t '.title_finish_delivery' = f.input :delivered_on, as: :date_picker = f.input :note, input_html: {size: '35x4'} .form-actions = f.submit class: 'btn btn-primary' = link_to t('ui.or_cancel'), supplier_deliveries_path(@supplier) - -/ - TODO: Fix this!! - .span6 - %h2= t '.new_article.title' - %p - = t('.new_article.search', supplier: @supplier.name).html_safe + ': ' - = text_field_tag 'article_name' - %hr/ - #stock_article_form - = render 'stock_article_form', stock_article: @supplier.stock_articles.build diff --git a/app/views/deliveries/_stock_article_for_adding.html.haml b/app/views/deliveries/_stock_article_for_adding.html.haml new file mode 100644 index 00000000..b43df782 --- /dev/null +++ b/app/views/deliveries/_stock_article_for_adding.html.haml @@ -0,0 +1,11 @@ +- css_class = ( @delivery and @delivery.includes_article? article ) ? ( 'unavailable' ) : ( false ) +%tr{:id => "stock_article_#{article.id}", :class => css_class} + %td= article.name + %td{:data => {:toggle => :tooltip, :title => render(:partial => 'shared/article_price_info', :locals => {:article => article})}}= number_to_currency article.price + %td= article.unit + %td= article.article_category.name + %td + = link_to t('.action_edit'), edit_stock_article_supplier_deliveries_path(@supplier, :stock_article_id => article.id), remote: true, class: 'btn btn-mini' + = link_to t('.action_other_price'), copy_stock_article_supplier_deliveries_path(@supplier, :old_stock_article_id => article.id), remote: true, class: 'btn btn-mini' + - deliver_button_disabled = ( @delivery and @delivery.includes_article? article ) ? ( 'disabled' ) : ( false ) + = link_to t('.action_add_to_delivery'), add_stock_change_supplier_deliveries_path(@supplier, :stock_article_id => article.id), :method => :post, remote: true, class: 'button-add-stock-change btn btn-mini btn-primary', disabled: deliver_button_disabled diff --git a/app/views/deliveries/_stock_article_form.html.haml b/app/views/deliveries/_stock_article_form.html.haml index 757383af..21c5b216 100644 --- a/app/views/deliveries/_stock_article_form.html.haml +++ b/app/views/deliveries/_stock_article_form.html.haml @@ -1,14 +1,23 @@ -= simple_form_for stock_article, url: add_stock_article_supplier_deliveries_path(@supplier), remote: true, - validate: true do |f| - = f.hidden_field :supplier_id - = f.input :name - = f.input :unit - = f.input :note - = f.input :price - = f.input :tax, :wrapper => :append do - = f.input_field :tax - %span.add-on % - -# untested, because this view is currently not included (?) - = f.input :deposit - = f.association :article_category - = f.submit class: 'btn' +- url = ( stock_article.new_record? ) ? ( create_stock_article_supplier_deliveries_path(@supplier) ) : ( update_stock_article_supplier_deliveries_path(@supplier) ) += simple_form_for stock_article, url: url, remote: true, validate: true do |f| + = f.association :supplier, :as => :hidden + = f.hidden_field :id unless stock_article.new_record? + .modal-header + = link_to t('ui.marks.close').html_safe, '#', class: 'close', data: {dismiss: 'modal'} + %h3= t 'activerecord.models.stock_article' + .modal-body + = f.input :name + = f.input :unit + = f.input :note + - if stock_article.new_record? + = f.input :price + = f.input :tax, :wrapper => :append do + = f.input_field :tax + %span.add-on % + = f.input :deposit + - else + = f.input :price, :input_html => {:disabled => 'disabled'}, :hint => stock_article_price_hint(stock_article) + = f.association :article_category + .modal-footer + = link_to t('ui.close'), '#', class: 'btn', data: {dismiss: 'modal'} + = f.submit :class => 'btn btn-primary', 'data-disable-with' => t('ui.please_wait') diff --git a/app/views/deliveries/_stock_change.html.haml b/app/views/deliveries/_stock_change.html.haml index 3c5dfd5a..a4a91a78 100644 --- a/app/views/deliveries/_stock_change.html.haml +++ b/app/views/deliveries/_stock_change.html.haml @@ -1,6 +1,6 @@ -%p - = fields_for "delivery[new_stock_changes][]", stock_change do |form| - = form.select :stock_article_id, stock_articles_for_select(supplier) - Menge - = form.text_field :quantity, :size => 5, :autocomplete => 'off' - = link_to t('.remove_article'), "#", :class => 'remove_new_stock_change' +- if stock_change.stock_article.new_record? + = simple_fields_for "delivery[new_stock_changes_new_stock_article][]", stock_change do |f| + = render :partial => 'stock_change_fields', :locals => {:f => f} +- else + = simple_fields_for "delivery[new_stock_changes][]", stock_change do |f| + = render :partial => 'stock_change_fields', :locals => {:f => f} diff --git a/app/views/deliveries/_stock_change_fields.html.haml b/app/views/deliveries/_stock_change_fields.html.haml new file mode 100644 index 00000000..6ff19ac9 --- /dev/null +++ b/app/views/deliveries/_stock_change_fields.html.haml @@ -0,0 +1,10 @@ +- stock_change = f.object +- stock_article = stock_change.stock_article +%tr{:id => "stock_change_stock_article_#{stock_article.id}", :data => {:id => stock_article.id}} + %td + %span.stock_article_name= stock_change.stock_article.name + = f.association :stock_article, :as => :hidden + %td.price{:data => {:toggle => :tooltip, :title => render(:partial => 'shared/article_price_info', :locals => {:article => stock_article})}}= number_to_currency stock_article.price + %td.unit= stock_change.stock_article.unit + %td= f.input :quantity, :wrapper => :intable, :input_html => {:class => 'stock-change-quantity', :autocomplete => :off} + %td= stock_change_remove_link f diff --git a/app/views/deliveries/add_stock_change.js.erb b/app/views/deliveries/add_stock_change.js.erb new file mode 100644 index 00000000..049e6233 --- /dev/null +++ b/app/views/deliveries/add_stock_change.js.erb @@ -0,0 +1,25 @@ +(function(w) { + if(!is_article_available_for_delivery(<%= @stock_change.stock_article.id %>)) { + return false; + } + + $('#stock_changes tr').removeClass('success'); + + var stock_change = $( + '<%= j(render(:partial => 'stock_change', :locals => {:stock_change => @stock_change})) %>' + ).addClass('success'); + enablePriceTooltips(stock_change); + + $('#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/deliveries/add_stock_change.js.haml b/app/views/deliveries/add_stock_change.js.haml deleted file mode 100644 index 43659e20..00000000 --- a/app/views/deliveries/add_stock_change.js.haml +++ /dev/null @@ -1 +0,0 @@ -$('#stock_changes').append('#{escape_javascript(render(:partial => 'stock_change', :locals => {:stock_change => StockChange.new, :supplier => @supplier}))}'); diff --git a/app/views/deliveries/copy_stock_article.js.erb b/app/views/deliveries/copy_stock_article.js.erb new file mode 100644 index 00000000..de5d260e --- /dev/null +++ b/app/views/deliveries/copy_stock_article.js.erb @@ -0,0 +1,5 @@ +$('#modalContainer').html( + '<%= j(render(:partial => "stock_article_form", :locals => {:stock_article => @stock_article})) %>' +); + +$('#modalContainer').modal(); diff --git a/app/views/deliveries/create_stock_article.js.erb b/app/views/deliveries/create_stock_article.js.erb new file mode 100644 index 00000000..5e2893a9 --- /dev/null +++ b/app/views/deliveries/create_stock_article.js.erb @@ -0,0 +1,17 @@ +$('div.container-fluid').prepend( + '<%= j(render(:partial => 'shared/alert_success', :locals => {:alert_message => t('.notice', :name => @stock_article.name)})) %>' +); + +(function() { + $('#stock_articles_for_adding tr').removeClass('success'); + + var stock_article_for_adding = $( + '<%= j(render(:partial => 'stock_article_for_adding', :locals => {:article => @stock_article})) %>' + ).addClass('success'); + enablePriceTooltips(stock_article_for_adding); + + $('#stock_articles_for_adding tbody').append(stock_article_for_adding); + updateSort('#stock_articles_for_adding'); +})(); + +$('#modalContainer').modal('hide'); diff --git a/app/views/deliveries/derive_stock_article.js.erb b/app/views/deliveries/derive_stock_article.js.erb new file mode 100644 index 00000000..de5d260e --- /dev/null +++ b/app/views/deliveries/derive_stock_article.js.erb @@ -0,0 +1,5 @@ +$('#modalContainer').html( + '<%= j(render(:partial => "stock_article_form", :locals => {:stock_article => @stock_article})) %>' +); + +$('#modalContainer').modal(); diff --git a/app/views/deliveries/edit_stock_article.js.erb b/app/views/deliveries/edit_stock_article.js.erb new file mode 100644 index 00000000..de5d260e --- /dev/null +++ b/app/views/deliveries/edit_stock_article.js.erb @@ -0,0 +1,5 @@ +$('#modalContainer').html( + '<%= j(render(:partial => "stock_article_form", :locals => {:stock_article => @stock_article})) %>' +); + +$('#modalContainer').modal(); diff --git a/app/views/deliveries/new_stock_article.js.erb b/app/views/deliveries/new_stock_article.js.erb new file mode 100644 index 00000000..de5d260e --- /dev/null +++ b/app/views/deliveries/new_stock_article.js.erb @@ -0,0 +1,5 @@ +$('#modalContainer').html( + '<%= j(render(:partial => "stock_article_form", :locals => {:stock_article => @stock_article})) %>' +); + +$('#modalContainer').modal(); diff --git a/app/views/deliveries/update_stock_article.js.erb b/app/views/deliveries/update_stock_article.js.erb new file mode 100644 index 00000000..ef46636d --- /dev/null +++ b/app/views/deliveries/update_stock_article.js.erb @@ -0,0 +1,33 @@ +$('div.container-fluid').prepend( + '<%= j(render(:partial => 'shared/alert_success', :locals => {:alert_message => t('.notice', :name => @stock_article.name)})) %>' +); + +(function() { + // update entry in stock_article table + + $('#stock_articles_for_adding tr').removeClass('success'); + + var stock_article_for_adding = $( + '<%= j(render(:partial => 'stock_article_for_adding', :locals => {:article => @stock_article, :delivery => @delivery})) %>' + ).addClass('success'); + enablePriceTooltips(stock_article_for_adding); + + $('#stock_article_<%= @stock_article.id %>').replaceWith(stock_article_for_adding); + updateSort('#stock_articles_for_adding'); + + mark_article_for_delivery(<%= @stock_article.id %>); + + // update entry in stock_changes table + + $('#stock_changes tr').removeClass('success'); + + var stock_change_entry = $('#stock_change_stock_article_<%= @stock_article.id %>'); + $('.stock_article_name', stock_change_entry).text('<%= j(@stock_article.name) %>'); + $('.unit', stock_change_entry).text('<%= j(@stock_article.unit) %>'); + + stock_change_entry.addClass('success'); + + updateSort('#stock_changes'); +})(); + +$('#modalContainer').modal('hide'); diff --git a/app/views/finance/balancing/new.html.haml b/app/views/finance/balancing/new.html.haml index 7384f50a..b8f08129 100644 --- a/app/views/finance/balancing/new.html.haml +++ b/app/views/finance/balancing/new.html.haml @@ -12,7 +12,7 @@ .well.well-small %h3= t('.notes_and_journal') #note - - unless @order.note.empty? + - unless @order.note.blank? = simple_format @order.note - else %p= t('.comment_on_transaction') diff --git a/app/views/finance/ordergroups/_ordergroups.html.haml b/app/views/finance/ordergroups/_ordergroups.html.haml index a273979d..4e67df73 100644 --- a/app/views/finance/ordergroups/_ordergroups.html.haml +++ b/app/views/finance/ordergroups/_ordergroups.html.haml @@ -5,7 +5,7 @@ %thead %tr %th= sort_link_helper t('.name'), "name", :per_page => @per_page - %th Kontakt + %th= t '.contact' %th.numeric= sort_link_helper t('.account_balance'), "account_balance", :per_page => @per_page %th %tbody @@ -17,4 +17,4 @@ %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' - \ No newline at end of file + diff --git a/app/views/foodcoop/users/_users.html.haml b/app/views/foodcoop/users/_users.html.haml index 487f15e4..90b1de3a 100644 --- a/app/views/foodcoop/users/_users.html.haml +++ b/app/views/foodcoop/users/_users.html.haml @@ -14,9 +14,9 @@ - for user in @users %tr %td= user.nick - %td= user.name if @current_user.role_admin? || user.settings["profile.nameIsPublic"] == '1' - %td= user.email if @current_user.role_admin? || user.settings["profile.emailIsPublic"] == '1' - %td= user.phone if @current_user.role_admin? || user.settings["profile.phoneIsPublic"] == '1' + %td= user.name if @current_user.role_admin? || user.settings.profile["name_is_public"] + %td= user.email if @current_user.role_admin? || user.settings.profile["email_is_public"] + %td= user.phone if @current_user.role_admin? || user.settings.profile["phone_is_public"] %td= user.ordergroup_name %td= user.workgroups.collect(&:name).join(', ') %td= link_to_new_message(message_params: {mail_to: user.id}) diff --git a/app/views/group_orders/_form.html.haml b/app/views/group_orders/_form.html.haml index 2939a689..2bd2f30b 100644 --- a/app/views/group_orders/_form.html.haml +++ b/app/views/group_orders/_form.html.haml @@ -4,6 +4,7 @@ #{data_to_js(@ordering_data)} setGroupBalance(#{@ordering_data[:available_funds]}); setCurrencyFormat("#{t('number.currency.format.separator')}", #{t('number.currency.format.precision')}, "#{t('number.currency.format.unit')}"); + setMinimumBalance(#{FoodsoftConfig[:minimum_balance] or 0}); setToleranceBehaviour(#{FoodsoftConfig[:tolerance_is_costly]}); setStockit(#{@order.stockit?}); }); diff --git a/app/views/layouts/_header.html.haml b/app/views/layouts/_header.html.haml index c3c16327..dfd5f2f5 100644 --- a/app/views/layouts/_header.html.haml +++ b/app/views/layouts/_header.html.haml @@ -22,5 +22,7 @@ Javascripts \================================================== / Placed at the end of the document so the pages load faster + :javascript + I18n = {locale: '#{j(I18n.locale.to_s)}'} = javascript_include_tag "application" = yield(:javascript) diff --git a/app/views/ordergroups/edit.html.haml b/app/views/ordergroups/edit.html.haml index 3a3eacba..7fec2484 100644 --- a/app/views/ordergroups/edit.html.haml +++ b/app/views/ordergroups/edit.html.haml @@ -45,26 +45,6 @@ = f.label :role_orders %br/ = f.check_box :role_orders - %p - = f.label :weekly_task - %br/ - = f.check_box :weekly_task - %p - = f.label :weekday - %br/ - = f.text_field :weekday - %p - = f.label :task_name - %br/ - = f.text_field :task_name - %p - = f.label :task_description - %br/ - = f.text_field :task_description - %p - = f.label :task_required_users - %br/ - = f.text_field :task_required_users %p = f.label :deleted_at %br/ diff --git a/app/views/ordergroups/index.html.haml b/app/views/ordergroups/index.html.haml index a436cbe1..35c1b4ac 100644 --- a/app/views/ordergroups/index.html.haml +++ b/app/views/ordergroups/index.html.haml @@ -12,11 +12,6 @@ %th Role Article Meta %th Role Finance %th Role Orders - %th Weekly Task - %th Weekday - %th Task Name - %th Task Description - %th Task Required Users %th Deleted At %th Contact Person %th Contact Phone @@ -34,11 +29,6 @@ %td= h ordergroup.role_article_meta %td= h ordergroup.role_finance %td= h ordergroup.role_orders - %td= h ordergroup.weekly_task - %td= h ordergroup.weekday - %td= h ordergroup.task_name - %td= h ordergroup.task_description - %td= h ordergroup.task_required_users %td= h ordergroup.deleted_at %td= h ordergroup.contact_person %td= h ordergroup.contact_phone diff --git a/app/views/orders/_articles.html.haml b/app/views/orders/_articles.html.haml index 3f176128..8508e6e8 100644 --- a/app/views/orders/_articles.html.haml +++ b/app/views/orders/_articles.html.haml @@ -29,7 +29,10 @@ - if order.stockit? %td= units - else - %td= "#{order_article.quantity} + #{order_article.tolerance}" if unit_quantity > 1 + - if unit_quantity > 1 or order_article.tolerance > 0 + %td= "#{order_article.quantity} + #{order_article.tolerance}" + - else + %td= "#{order_article.quantity}" %td= units %p = t '.prices_sum' diff --git a/app/views/pages/_form.html.haml b/app/views/pages/_form.html.haml index a3de5652..5b84baa9 100644 --- a/app/views/pages/_form.html.haml +++ b/app/views/pages/_form.html.haml @@ -56,7 +56,7 @@ %pre * #{t '.help.list_item_1'} %pre - ** #{t '.help_list_item_2'} + ** #{t '.help.list_item_2'} %tr %td= t '.help.ordered_list' %td diff --git a/app/views/shared/_alert_success.haml b/app/views/shared/_alert_success.haml new file mode 100644 index 00000000..3a2f5fd6 --- /dev/null +++ b/app/views/shared/_alert_success.haml @@ -0,0 +1,5 @@ +.alert.fade.in.alert-success + %a.close{:href => '#', :data => {:dismiss => 'alert'}} + = t('ui.marks.close').html_safe + = t('ui.marks.success').html_safe + = alert_message diff --git a/app/views/shared/_article_price_info.html.haml b/app/views/shared/_article_price_info.html.haml new file mode 100644 index 00000000..00e25a3a --- /dev/null +++ b/app/views/shared/_article_price_info.html.haml @@ -0,0 +1,17 @@ +%table.table.table-condensed + %tr + %th= t 'activerecord.attributes.article.price' + %td.numeric= number_to_currency article.price + %tr + %th= t 'activerecord.attributes.article.deposit' + %td.numeric= number_to_currency article.deposit + %tr + %th= t 'activerecord.attributes.article.tax' + %td.numeric= number_to_percentage article.tax + - unless article.fc_price == article.gross_price + %tr + %th= t 'activerecord.attributes.article.fc_share' + %td.numeric= number_to_currency(article.fc_price-article.gross_price) + %tr + %th= t 'activerecord.attributes.article.fc_price' + %td.numeric= number_to_currency article.fc_price 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/shared/_base_errors.haml b/app/views/shared/_base_errors.haml new file mode 100644 index 00000000..9dd2dd82 --- /dev/null +++ b/app/views/shared/_base_errors.haml @@ -0,0 +1,5 @@ +.alert.alert-error.alert-block + %a.close{:href => '#', :data => {:dismiss => 'alert'}} + = t('ui.marks.close').html_safe + %ul + = error_messages.html_safe diff --git a/app/views/shared/_group.html.haml b/app/views/shared/_group.html.haml index eebe4cac..b9b914ce 100644 --- a/app/views/shared/_group.html.haml +++ b/app/views/shared/_group.html.haml @@ -13,13 +13,6 @@ - members = group.users = "(#{members.size})" = members.collect(&:nick).join(", ") - - if group.is_a?(Workgroup) - %dt= t('.weekly_job') + ':' - %dd - - if group.weekly_task - =h "#{group.task_name} am #{weekday(group.weekday)}" - - else - = t '.no_weekly_job' - - else + - unless group.is_a?(Workgroup) %dt= t '.apple_limit' %dd= group.ignore_apple_restriction ? t('.deactivated') : t('.activated') diff --git a/app/views/shared/_group_form_fields.html.haml b/app/views/shared/_group_form_fields.html.haml index f13b0054..44376eaf 100644 --- a/app/views/shared/_group_form_fields.html.haml +++ b/app/views/shared/_group_form_fields.html.haml @@ -3,17 +3,6 @@ = yield -- if f.object.is_a?(Workgroup) - %h3= t '.title' - = f.input :weekly_task - #weekly_task_fields - = f.input :weekday, as: :select, collection: Workgroup.weekdays - = f.input :task_name - = f.input :task_required_users - = f.input :task_duration, :as => :select, :collection => (1..3) - = f.input :task_description, as: :text, input_html: {rows: 5} - = f.input :next_weekly_tasks_number - = f.input :user_tokens, :as => :string, :input_html => { 'data-pre' => f.object.users.map { |u| u.token_attributes }.to_json } diff --git a/app/views/shared/_user_form_fields.html.haml b/app/views/shared/_user_form_fields.html.haml index fa11b393..bfd27f49 100644 --- a/app/views/shared/_user_form_fields.html.haml +++ b/app/views/shared/_user_form_fields.html.haml @@ -5,11 +5,31 @@ = f.input :phone = f.input :password, :required => f.object.new_record? = f.input :password_confirmation -.control-group - .controls - - for setting in User::setting_keys.keys - %label.checkbox{:for => "user[setting_attributes][#{setting}]"} - = hidden_field_tag "user[setting_attributes][#{setting}]", '0' - = check_box_tag "user[setting_attributes][#{setting}]", '1', - f.object.settings[setting] == '1' || f.object.settings_default(setting) - = User::setting_keys[setting] + += f.simple_fields_for :settings_attributes do |s| + = s.simple_fields_for :profile, defaults: { inline_label: true } do |profile| + = profile.input 'language', as: :select, collection: available_locales, required: false, selected: f.object.settings.profile['language'] + + .settings + .settings-group + = s.simple_fields_for :profile, defaults: { inline_label: true } do |profile| + + %div{class: 'control-group h_wrapper'} + %h5{class: 'controls'} + = t 'simple_form.labels.settings.settings_group.privacy' + = profile.input 'phone_is_public', as: :boolean, label: false, input_html: { checked: f.object.settings.profile['phone_is_public'] } + = profile.input 'email_is_public', as: :boolean, label: false, input_html: { checked: f.object.settings.profile['email_is_public'] } + = profile.input 'name_is_public', as: :boolean, label: false, input_html: { checked: f.object.settings.profile['name_is_public'] } + + .settings-group + %div{class: 'control-group'} + %h5{class: 'controls'} + = t 'simple_form.labels.settings.settings_group.messages' + + = s.simple_fields_for :messages, defaults: { inline_label: true, label: false } do |messages| + = messages.input 'send_as_email', as: :boolean, input_html: { checked: f.object.settings.messages['send_as_email'] } + = s.simple_fields_for :notify, defaults: { inline_label: true, label: false } do |notify| + = notify.input 'order_finished', as: :boolean, input_html: { checked: f.object.settings.notify['order_finished'] } + = notify.input 'negative_balance', as: :boolean, input_html: { checked: f.object.settings.notify['negative_balance'] } + = notify.input 'upcoming_tasks', as: :boolean, input_html: { checked: f.object.settings.notify['upcoming_tasks'] } + diff --git a/app/views/stock_takings/edit.html.haml b/app/views/stock_takings/edit.html.haml index 09312f6e..66f5752d 100644 --- a/app/views/stock_takings/edit.html.haml +++ b/app/views/stock_takings/edit.html.haml @@ -4,4 +4,4 @@ = f.input :date = f.input :note = f.submit - = link_to t('ui.cancel'), stock_takings_path + = link_to t('ui.or_cancel'), stock_takings_path diff --git a/app/views/stock_takings/new.html.haml b/app/views/stock_takings/new.html.haml index dfa9e03b..51239ba4 100644 --- a/app/views/stock_takings/new.html.haml +++ b/app/views/stock_takings/new.html.haml @@ -14,4 +14,4 @@ = render :partial => 'stock_change', :collection => @stock_taking.stock_changes .form-actions = f.submit class: 'btn' - = link_to t('ui.cancel'), stock_takings_path + = link_to t('ui.or_cancel'), stock_takings_path diff --git a/app/views/stockit/destroy.js.haml b/app/views/stockit/destroy.js.haml index b70b44ca..8bbde9ed 100644 --- a/app/views/stockit/destroy.js.haml +++ b/app/views/stockit/destroy.js.haml @@ -4,11 +4,5 @@ var successDiv = $('
@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 777bb4b1..477e5816 100644 --- a/app/views/stockit/index.html.haml +++ b/app/views/stockit/index.html.haml @@ -1,4 +1,4 @@ -- title "Lager (#{StockArticle.available.count})" +- title t('.title', article_count: StockArticle.available.count) - content_for :javascript do :javascript $(function() { @@ -56,6 +56,7 @@ %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/suppliers/shared_suppliers.haml b/app/views/suppliers/shared_suppliers.haml index 6d36149c..22cf67da 100644 --- a/app/views/suppliers/shared_suppliers.haml +++ b/app/views/suppliers/shared_suppliers.haml @@ -17,7 +17,9 @@ %td= shared_supplier.note %td= shared_supplier.delivery_days %td - - if shared_supplier.supplier + - if shared_supplier.suppliers.any? %i.icon-ok + = associated_supplier_names(shared_supplier) + = link_to t('.subscribe_again'), new_supplier_path(:shared_supplier_id => shared_supplier), class: 'btn' - else = link_to t('.subscribe'), new_supplier_path(:shared_supplier_id => shared_supplier), class: 'btn' diff --git a/app/views/tasks/_form.html.haml b/app/views/tasks/_form.html.haml index 3878f89f..95f3b134 100644 --- a/app/views/tasks/_form.html.haml +++ b/app/views/tasks/_form.html.haml @@ -25,5 +25,7 @@ = f.input :due_date, as: :date_picker = f.input :done .form-actions - = f.submit class: 'btn' + = f.submit class: 'btn btn-primary' + - if @task.new_record? + = f.submit t('.submit.periodic'), name: 'periodic', class: 'btn' = link_to t('ui.or_cancel'), :back diff --git a/app/views/tasks/_list.haml b/app/views/tasks/_list.haml index a0b2af22..da91cec8 100644 --- a/app/views/tasks/_list.haml +++ b/app/views/tasks/_list.haml @@ -2,6 +2,7 @@ %thead %tr %th= t '.due_date' + %th %th= t '.task' %th{:colspan => '2'} = t '.who' @@ -11,6 +12,9 @@ - done = task.done ? " done" : "" %tr{:class => done } %td= format_date(task.due_date) unless task.due_date.nil? + %td + - if task.periodic? + %i.icon-repeat{title: t('tasks.repeated')} %td= link_to t('.task_format', name: task.name, duration: task.duration), task_path(task) %td = task_assignments task diff --git a/app/views/tasks/_nav.haml b/app/views/tasks/_nav.haml index 8fec6188..ecf28c36 100644 --- a/app/views/tasks/_nav.haml +++ b/app/views/tasks/_nav.haml @@ -4,7 +4,7 @@ - content_for :sidebar do .well.well-small %ul.nav.nav-list - %li.nav-header Seiten + %li.nav-header= t '.pages' %li= link_to t('.my_tasks'), user_tasks_path %li= link_to t('.all_tasks'), tasks_path %li= link_to t('.archive'), archive_tasks_path diff --git a/app/views/tasks/show.haml b/app/views/tasks/show.haml index 23b843c7..8777d15d 100644 --- a/app/views/tasks/show.haml +++ b/app/views/tasks/show.haml @@ -10,7 +10,10 @@ %dd= simple_format(@task.description) - if @task.due_date.present? %dt= t '.due_date' - %dd= format_date(@task.due_date) + %dd + = format_date(@task.due_date) + - if @task.periodic? + %i.icon-repeat{title: t('tasks.repeated')} %dt= t 'simple_form.labels.task.duration' %dd= t('.hours', count: @task.duration) %dt= t 'simple_form.labels.task.user_list' @@ -29,3 +32,6 @@ = link_to t('ui.edit'), edit_task_path(@task), class: 'btn' = link_to t('ui.delete'), task_path(@task), :method => :delete, :confirm => "Die Aufgabe wirklich löschen?", class: 'btn btn-danger' + - if @task.periodic? + = link_to t('.delete_group'), task_path(@task, periodic: true), method: :delete, + confirm: t('.confirm_delete_group'), class: 'btn btn-danger' diff --git a/app/views/tasks/user.html.haml b/app/views/tasks/user.html.haml index fd13ce2a..095cd1de 100644 --- a/app/views/tasks/user.html.haml +++ b/app/views/tasks/user.html.haml @@ -1,4 +1,4 @@ -- title "Meine Aufgaben" +- title t('.title') = render 'nav' - unless @unaccepted_tasks.empty? diff --git a/app/views/tasks/workgroup.haml b/app/views/tasks/workgroup.haml index a5525d90..b9f81b7f 100644 --- a/app/views/tasks/workgroup.haml +++ b/app/views/tasks/workgroup.haml @@ -1,16 +1,6 @@ - title t('.title', workgroup: @group.name) = render 'nav' -%section.well - %h3= t '.weekly.title' - - if @group.weekly_task - = t('.weekly.desc', weekday: weekday(@group.weekday), task: @group.task_name).html_safe - - else - = t('.weekly.empty').html_safe - - - if @current_user.member_of?(@group) or @current_user.role_admin? - = link_to t('.weekly.edit'), edit_foodcoop_workgroup_path(@group), class: 'btn' - %section %h3= t '.title_all' = render 'list', tasks: @group.open_tasks diff --git a/app/views/workgroups/edit.html.haml b/app/views/workgroups/edit.html.haml index 9346430a..896c8a91 100644 --- a/app/views/workgroups/edit.html.haml +++ b/app/views/workgroups/edit.html.haml @@ -45,26 +45,6 @@ = f.label :role_orders %br/ = f.check_box :role_orders - %p - = f.label :weekly_task - %br/ - = f.check_box :weekly_task - %p - = f.label :weekday - %br/ - = f.text_field :weekday - %p - = f.label :task_name - %br/ - = f.text_field :task_name - %p - = f.label :task_description - %br/ - = f.text_field :task_description - %p - = f.label :task_required_users - %br/ - = f.text_field :task_required_users %p = f.label :deleted_at %br/ diff --git a/app/views/workgroups/index.html.haml b/app/views/workgroups/index.html.haml index ab1ca879..ed688695 100644 --- a/app/views/workgroups/index.html.haml +++ b/app/views/workgroups/index.html.haml @@ -12,11 +12,6 @@ %th Role Article Meta %th Role Finance %th Role Orders - %th Weekly Task - %th Weekday - %th Task Name - %th Task Description - %th Task Required Users %th Deleted At %th Contact Person %th Contact Phone @@ -34,11 +29,6 @@ %td= h workgroup.role_article_meta %td= h workgroup.role_finance %td= h workgroup.role_orders - %td= h workgroup.weekly_task - %td= h workgroup.weekday - %td= h workgroup.task_name - %td= h workgroup.task_description - %td= h workgroup.task_required_users %td= h workgroup.deleted_at %td= h workgroup.contact_person %td= h workgroup.contact_phone diff --git a/app/workers/user_notifier.rb b/app/workers/user_notifier.rb index 7be2d17c..55041465 100644 --- a/app/workers/user_notifier.rb +++ b/app/workers/user_notifier.rb @@ -18,7 +18,7 @@ class UserNotifier Order.find(order_id).group_orders.each do |group_order| group_order.ordergroup.users.each do |user| begin - Mailer.order_result(user, group_order).deliver if user.settings["notify.orderFinished"] == '1' + Mailer.order_result(user, group_order).deliver if user.settings.notify["order_finished"] rescue Rails.logger.warn "Can't deliver mail to #{user.email}" end @@ -34,7 +34,7 @@ class UserNotifier Ordergroup.find(ordergroup_id).users.each do |user| begin - Mailer.negative_balance(user, transaction).deliver if user.settings["notify.negativeBalance"] == '1' + Mailer.negative_balance(user, transaction).deliver if user.settings.notify["negative_balance"] rescue Rails.logger.warn "Can't deliver mail to #{user.email}" end diff --git a/config/app_config.yml.SAMPLE b/config/app_config.yml.SAMPLE index 2436577f..594b9c70 100644 --- a/config/app_config.yml.SAMPLE +++ b/config/app_config.yml.SAMPLE @@ -37,6 +37,9 @@ default: &defaults # 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 @@ -45,6 +48,10 @@ default: &defaults # Comment out this option to activate this restriction # stop_ordering_under: 75 + # 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: 0 + # email address to be used as sender email_sender: foodsoft@foodcoop.test diff --git a/config/application.rb b/config/application.rb index 32ea295b..6d519277 100644 --- a/config/application.rb +++ b/config/application.rb @@ -31,7 +31,7 @@ module Foodsoft # Internationalization. config.i18n.load_path += Dir[Rails.root.join('config', 'locales', '**', '*.{rb,yml}')] - config.i18n.default_locale = :de + config.i18n.default_locale = :en # Configure the default encoding used in templates for Ruby 1.9. config.encoding = "utf-8" diff --git a/config/initializers/extensions.rb b/config/initializers/extensions.rb index a6577545..1f71d542 100644 --- a/config/initializers/extensions.rb +++ b/config/initializers/extensions.rb @@ -9,4 +9,11 @@ class String string end end -end \ No newline at end of file +end + +class Array + def cumulative_sum + csum = 0 + self.map{|val| csum += val} + end +end diff --git a/config/initializers/simple_form_bootstrap.rb b/config/initializers/simple_form_bootstrap.rb index 1a229676..5c581107 100644 --- a/config/initializers/simple_form_bootstrap.rb +++ b/config/initializers/simple_form_bootstrap.rb @@ -36,7 +36,18 @@ SimpleForm.setup do |config| input.use :error, :wrap_with => { :tag => 'span', :class => 'help-inline' } end end - + + # Do not use the label in tables + config.wrappers :intable, :tag => 'div', :class => 'control-group control-group-intable', :error_class => 'error' do |b| + b.use :html5 + b.use :placeholder + b.wrapper :tag => 'div', :class => 'controls controls-intable' do |ba| + ba.use :input + ba.use :error, :wrap_with => { :tag => 'span', :class => 'help-inline' } + ba.use :hint, :wrap_with => { :tag => 'p', :class => 'help-block' } + end + end + # Wrappers for forms and inputs using the Twitter Bootstrap toolkit. # Check the Bootstrap docs (http://twitter.github.com/bootstrap) # to learn about the different styles for forms and inputs, diff --git a/config/locales/de.yml b/config/locales/de.yml index 648881e0..c77f4dcb 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -39,6 +39,8 @@ de: article_category: Kategorie availability: Artikel ist verfügbar? deposit: Pfand + fc_price: Endpreis + fc_share: FC-Aufschlag gross_price: Bruttopreis price: Nettopreis tax: MwSt @@ -81,6 +83,11 @@ de: too_long: ist zu lang (nicht mehr als %{count} Zeichen) too_short: ist zu kurz (nicht weniger als %{count} Zeichen) wrong_length: hat die falsche Länge (muss genau %{count} Zeichen haben) + models: + task: + attributes: + done: + exclusion: erledigte Aufgaben können nicht wöchentlich wiederholt werden template: body: ! 'Bitte überprüfen Sie die folgenden Felder:' header: @@ -416,20 +423,28 @@ de: second: Sekunden year: Jahr deliveries: + add_stock_change: + how_many_units: Wie viele Einheiten (%{unit}) des Artikels »%{name}« liefern? create: notice: Lieferung wurde erstellt. Bitte nicht vergessen die Rechnung anzulegen! + create_stock_article: + notice: Neuer Lagerartikel »%{name}« gespeichert. destroy: notice: Lieferung wurde gelöscht. edit: title: Lieferung bearbeiten form: - add_article: Lagerartikel der Lieferung hinzufügen - new_article: - search: Suche nach Artikeln aus dem %{supplier} Katalog - title: Neuen Lagerartikel anlegen - note_new_article: Ist ein Artikel noch nicht in der Lagerverwaltung, muss er erst %{new_link} werden. - note_new_article_link: neu angelegt - remove_article: Artikel aus Lieferung entfernen + actions: Optionen + article: Artikel + category: Kategorie + create_from_blank: Ohne Vorlage anlegen + create_stock_article: Lagerartikel anlegen + price: Nettopreis + quantity: Menge + title_fill_quantities: 2. Liefermenge angeben + title_finish_delivery: 3. Lieferung abschließen + title_select_stock_articles: 1. Lagerartikel auswählen + unit: Einheit index: confirm_delete: Bist Du sicher? new_delivery: Neue Lieferung für %{supplier} anlegen @@ -449,24 +464,34 @@ de: title: Lieferung anzeigen title_articles: Artikel unit: Einheit - stock_change: + stock_article_for_adding: + action_add_to_delivery: Liefern + action_edit: Bearbeiten + action_other_price: Kopieren + stock_article_form: + copy_stock_article: Lagerartikel kopieren + stock_change_fields: remove_article: Artikel aus Lieferung entfernen suppliers_overview: Lieferantenübersicht update: notice: Lieferung wurde aktualisiert. + update_stock_article: + notice: Lagerartikel »%{name}« aktualisiert. documents: order_by_articles: 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 @@ -482,6 +507,8 @@ de: - Gebinde - Einheit - Preis/Einheit + - Summe + total: Gesamtpreis order_matrix: filename: Bestellung %{name}-%{date} - Sortiermatrix heading: Artikelübersicht @@ -696,6 +723,7 @@ de: ordergroups: account_balance: Kontostand account_statement: Kontoauszug + contact: Kontakt name: Name new_transaction: Neue Transaktion update: @@ -1146,6 +1174,8 @@ de: subject: ! 'Betreff:' title: Nachricht anzeigen model: + delivery: + each_stock_article_must_be_unique: Lieferung darf jeden Lagerartikel höchstens einmal auflisten. membership: no_admin_delete: Mitgliedschaft kann nicht beendet werden. Du bist die letzte Administratorin order_article: @@ -1154,14 +1184,6 @@ de: redirect: Weiterleiting auf [[%{title}]]... user: no_ordergroup: keine Bestellgruppe - notify: - email_is_public: E-Mail ist für Mitglieder sichtbar - name_is_public: Name ist für Mitglieder sichtbar - negative_balance: Informiere mich, falls meine Bestellgruppe ins Minus rutscht. - order_finished: Informier mich über meine Bestellergebnisse (nach Ende der Bestellung). - phone_is_public: Telefon ist für Mitglieder sichtbar - send_as_email: Bekomme Nachrichten als Emails. - upcoming_tasks: Erinnere mich an anstehende Aufgaben. navigation: admin: home: Übersicht @@ -1423,7 +1445,7 @@ de: sessions: logged_in: Angemeldet! logged_out: Abgemeldet! - login_invalid: + login_invalid: Ungültiger Benutzername oder Passwort new: forgot_password: Passwort vergessen? login: Anmelden @@ -1509,6 +1531,10 @@ de: units_to_order: Anzahl gelieferter Gebinde update_current_price: Ändert auch den Preis für aktuelle Bestellungen stock_article: + copy_stock_article: + name: Bitte ändern + edit_stock_article: + price:
  • Preisänderung gesperrt.
  • Bei Bedarf %{stock_article_copy_link}.
supplier: supplier: min_order_quantity: Die Mindestbestellmenge wird während der Bestellung angezeigt und soll motivieren @@ -1581,9 +1607,25 @@ de: contact_person: Kontaktperson contact_phone: Telefon ignore_apple_restriction: Bestellstop bei zu wenig Äpfeln ignorieren + name: Name page: body: Inhalt parent_id: Oberseite + settings: + messages: + send_as_email: Bekomme Nachrichten als Emails. + notify: + negative_balance: Informiere mich, falls meine Bestellgruppe ins Minus rutscht. + order_finished: Informier mich über meine Bestellergebnisse (nach Ende der Bestellung). + upcoming_tasks: Erinnere mich an anstehende Aufgaben. + profile: + email_is_public: E-Mail ist für Mitglieder sichtbar. + language: Sprache + name_is_public: Name ist für Mitglieder sichtbar. + phone_is_public: Telefon ist für Mitglieder sichtbar. + settings_group: + messages: Nachrichten + privacy: Privatsphäre stock_article: supplier: Lieferant supplier: @@ -1626,13 +1668,15 @@ de: role_finance: Finanzen role_orders: Bestellverwaltung role_suppliers: Lieferanten - task_description: Beschreibung - task_duration: Vor. Dauer in Stunden - task_name: Name für Job - task_required_users: Benötige Verantwortliche - weekday: Wochentag - weekly_task: Monatlichen Job definieren? 'no': Nein + options: + settings: + profile: + language: + de: Deutsch + en: English + fr: Französisch + nl: Niederländisch required: mark: ! '*' text: benötigt @@ -1677,6 +1721,15 @@ 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 @@ -1696,6 +1749,7 @@ de: show_stock_takings: Inventurübersicht stock_count: ! 'Artikelanzahl:' stock_worth: ! 'Aktueller Lagerwert:' + title: Lager (%{article_count}) toggle_unavailable: Nicht verfügbare Artikel zeigen/verstecken view_options: Ansichtsoptionen new: @@ -1726,6 +1780,7 @@ de: shared_suppliers: body:

Hier werden die Lieferantinnen der externen Datenbank angezeigt.

Ihr könnt externe Lieferantinnen importieren, indem ihr sie einfach abonniert. (siehe unten)

Damit wird eine neue Lieferantin angelegt und mit der externen Datenbank verknüpft.

subscribe: abonnieren + subscribe_again: erneut abonnieren supplier: Lieferantin title: Externe Listen show: @@ -1756,12 +1811,15 @@ de: notice: Aufgabe wurde gelöscht edit: title: Aufgabe bearbeiten + warning_periodic: Warnung: Diese Aufgabe ist Teil einer Gruppe von wöchentlichen Aufgaben. Beim Speichern wird sie aus der Gruppe ausgeschlossen und in eine gewöhnliche Aufgabe umgewandelt. error_not_found: Keine Arbeitsgruppe gefunden form: search: hint: Nach Nutzerin suchen noresult: Keine Nutzerin gefunden placeholder: Suche ... + submit: + periodic: Wöchentliche Aufgabe speichern index: show_group_tasks: Gruppenaufgaben anzeigen title: Aufgaben @@ -1783,12 +1841,16 @@ de: group_tasks: Gruppenaufgaben my_tasks: Meine Aufgaben new_task: Neue Aufgabe erstellen + pages: Seiten new: title: Neue Aufgabe erstellen + repeated: Aufgabe wird wöchentlich wiederholt set_done: notice: Aufgabenstatus wurde aktualisiert show: accept_task: Aufgabe übernehmen + confirm_delete_group: Diese und alle folgenden wöchentlichen Aufgaben wirklich löschen? + delete_group: Aufgabe und folgende löschen due_date: Fälligkeitsdatum hours: ! '%{count}h' mark_done: Als erledigt markieren @@ -1796,6 +1858,7 @@ de: title: Aufgabe anzeigen update: notice: Aufgabe wurde aktualisiert + notice_converted: Aufgabe wurde aktualisiert und in eine gewöhnliche Aufgabe umgewandelt user: more: Nichts zu tun? %{tasks_link} gibt es bestimmt Arbeit tasks_link: Hier @@ -1805,11 +1868,6 @@ de: workgroup: title: Aufgaben für %{workgroup} title_all: Alle Aufgaben der Gruppe - weekly: - desc: ! '

Jeden %{weekday} hat diese Arbeitsgruppe folgenden Job: %{task}

Die Wochenaufgaben werden von der Foodsoft automatisch erstellt. Eintragen müsst Ihr Euch aber selber.

' - edit: Wöchentliche Aufgaben anpassen - empty: Noch keine Wochenaufgaben angelegt. - title: Wöchentliche Aufgaben time: am: vormittags formats: @@ -1821,9 +1879,12 @@ de: close: Schließen delete: Löschen edit: Bearbeiten + history: Verlauf anzeigen marks: close: ! '×' + success: or_cancel: oder abbrechen + please_wait: Bitte warten... save: Speichern show: Anzeigen views: diff --git a/config/locales/en.yml b/config/locales/en.yml index 8205bb75..3618f4bc 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -39,6 +39,8 @@ en: article_category: article category availability: Is article available? deposit: deposit + fc_price: FC price + fc_share: FC share gross_price: gross price price: price tax: VAT @@ -81,6 +83,11 @@ en: too_long: is too long (no more than %{count} characters) too_short: is too short (use more than %{count} characters) wrong_length: is the wrong length (has to have exactly %{count} characters) + models: + task: + attributes: + done: + exclusion: finished tasks may not be repeated weekly template: body: ! 'Please check the following fields:' header: @@ -229,7 +236,7 @@ en: option_available: Make articles available option_delete: Delete article option_not_available: Make articles unavailable - option_select: Choose special offer ... + option_select: Select action ... price_netto: Price unit_quantity_desc: Unit quantity unit_quantity_short: Quantity @@ -307,7 +314,7 @@ en: title: Synchronize articles with external database unit_quantity_short: unit quantity update: - body: ! '

Every article is shown twice. The old values are gray and contain the current values.

+ body: ! '

Every article is shown twice. The old values are gray and the text fields contain the current values.

Differences with the old articles are marked yellow.

' title: Update ... @@ -418,20 +425,28 @@ en: second: seconds year: years deliveries: + add_stock_change: + how_many_units: ! 'How many units (%{unit}) to deliver? Stock article name: %{name}.' create: notice: Delivery was created. Please don’t forget to create invoice! + create_stock_article: + notice: The new stock article »%{name}« was saved. destroy: notice: Delivery was deleted. edit: - title: Edit suppliers + title: Edit delivery form: - add_article: Add stock article to delivery - new_article: - search: Search for articles in the %{supplier} catalogue - title: Create new stock article - note_new_article: When an article is not yet in the inventory, you have to %{new_link} it first. - note_new_article_link: create - remove_article: Remove article from delivery + actions: Tasks + article: Article + category: Category + create_from_blank: Create new article + create_stock_article: Create stock article + price: Netprice + quantity: Quantity + title_fill_quantities: 2. Set delivery quantities + title_finish_delivery: 3. Finish delivery + title_select_stock_articles: 1. Select stock articles + unit: Unit index: confirm_delete: Are you sure? new_delivery: ! 'Create new delivery for %{supplier} ' @@ -451,26 +466,36 @@ en: title: Show delivery title_articles: Article unit: Unit - stock_change: - remove_article: Remove articles from delivery + stock_article_for_adding: + action_add_to_delivery: Add to delivery + action_edit: Edit + action_other_price: Copy + stock_article_form: + copy_stock_article: Copy stock article + stock_change_fields: + remove_article: Remove article from delivery suppliers_overview: Supplier overview update: notice: Delivery was updated. + update_stock_article: + 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 quantity - Unit - Sum sum: Sum @@ -481,20 +506,24 @@ en: - Order Number - Amount - Name - - Barrel + - Unit quantity - Unit - Price/Unit + - Subtotal + total: Total order_matrix: filename: Order %{name}-%{date} - sorting matrix heading: Article overview rows: - Article - Unit - - Barrel + - Unit quantity - FC-Price - Amount title: ! 'Order sorting matrix: %{name}, closed at %{date}' - total: ! '%{count} articles in total' + total: + one: One article in total + other: ! '%{count} articles in total' errors: format: ! '%{attribute} %{message}' general: A problem has occured. @@ -698,6 +727,7 @@ en: ordergroups: account_balance: Account balance account_statement: Account statement + contact: Contact name: Name new_transaction: New transaction update: @@ -1148,6 +1178,8 @@ en: subject: ! 'Subject:' title: Show message model: + delivery: + each_stock_article_must_be_unique: Each stock article must not be listed more than once. membership: no_admin_delete: Membership can not be withdrawn as you are the last administrator. order_article: @@ -1156,14 +1188,6 @@ en: redirect: Redirect to [[%{title}]]... user: no_ordergroup: no order group - notify: - email_is_public: Email is visible for other members. - name_is_public: Name is visible for other members. - negative_balance: inform me when by order group has a negative balance. - order_finished: Inform me about my order result (when the order is closed). - phone_is_public: Phone number is visible for other members. - send_as_email: Receive messages as emails. - upcoming_tasks: Remind me of upcoming tasks. navigation: admin: home: Overview @@ -1435,10 +1459,15 @@ 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 + ordered: Ordered (Amount + Tolerance) received: Received articles_by_groups: fc_price: FC-Price @@ -1511,6 +1540,10 @@ en: units_to_order: Amount of delivered units update_current_price: Also update the price of the current order stock_article: + copy_stock_article: + name: Please modify + edit_stock_article: + price:
  • Price changes are forbidden.
  • If necessary, %{stock_article_copy_link}.
supplier: supplier: min_order_quantity: The minimum amount which has to be orderd will be shown during the order process and should motivate ordering @@ -1583,9 +1616,25 @@ en: contact_person: Contact person contact_phone: Phone ignore_apple_restriction: Ignore order stop by apple points restriction + name: Name page: body: Body parent_id: Parent page + settings: + messages: + send_as_email: Receive messages as emails. + notify: + negative_balance: Inform me when my order group has a negative balance. + order_finished: Inform me about my order result (when the order is closed). + upcoming_tasks: Remind me of upcoming tasks. + profile: + email_is_public: Email is visible for other members. + language: Language + name_is_public: Name is visible for other members. + phone_is_public: Phone number is visible for other members. + settings_group: + messages: Messages + privacy: Privacy stock_article: supplier: Supplier supplier: @@ -1628,13 +1677,15 @@ en: role_finance: Finances role_orders: Order management role_suppliers: Suppliers - task_description: Description - task_duration: Duration in hours - task_name: Task name - task_required_users: People required - weekday: Weekday - weekly_task: Define monthly task? 'no': 'No' + options: + settings: + profile: + language: + de: German + en: English + fr: French + nl: Dutch required: mark: ! '*' text: required @@ -1679,6 +1730,15 @@ 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 @@ -1698,6 +1758,7 @@ en: show_stock_takings: Inventory overview stock_count: ! 'Number of articles:' stock_worth: ! 'Current stock value:' + title: Stock (%{article_count}) toggle_unavailable: Show/hide unavailable articles view_options: View options new: @@ -1728,6 +1789,7 @@ en: shared_suppliers: body:

Suppliers of the external database are displayed here.

You can import external suppliers by subscribing (see below).

A new supplier will be created and connected to the external database.

subscribe: Subscribe + subscribe_again: Subscribe again supplier: Supplier title: External lists show: @@ -1758,12 +1820,15 @@ en: notice: Task has been deleted edit: title: Edit task + warning_periodic: Warning: This task is part of a group of weekly tasks. When saving it will be excluded from the group and it will be converted to a regular task. error_not_found: No workgroup found form: search: hint: Search for user noresult: No user found placeholder: Search ... + submit: + periodic: Save weekly task index: show_group_tasks: Show group tasks title: Tasks @@ -1785,12 +1850,16 @@ en: group_tasks: Group tasks my_tasks: My tasks new_task: Create new task + pages: Pages new: title: Create new tasks + repeated: Task is repeated weekly set_done: notice: The state of the task has been updated show: accept_task: Accept task + confirm_delete_group: Really delete this and all subsequent tasks? + delete_group: Delete task and subsequent due_date: Due date hours: ! '%{count}h' mark_done: Mark task as done @@ -1798,6 +1867,7 @@ en: title: Show task update: notice: Task has been updated + notice_converted: Task has been updated and was converted to a regular task user: more: Nothing to do? %{tasks_link} are tasks for sure. tasks_link: Here @@ -1807,11 +1877,6 @@ en: workgroup: title: Tasks for %{workgroup} title_all: All group tasks - weekly: - desc: ! '

Every %{weekday} this workgroup has the following job: %{task}

The weektask has been created by Foodsoft automatically. You still have to sign up for it yourself.

' - edit: Edit weekly tasks - empty: No weekly tasks created yet. - title: Weekly tasks time: am: morning formats: @@ -1823,9 +1888,12 @@ en: close: Close delete: Delete edit: Edit + history: Show history marks: close: ! '×' + success: or_cancel: or cancel + please_wait: Please wait... save: Save show: Show views: diff --git a/config/locales/fr.yml b/config/locales/fr.yml new file mode 100644 index 00000000..057acd4e --- /dev/null +++ b/config/locales/fr.yml @@ -0,0 +1,1919 @@ +fr: + activemodel: + errors: + format: ! '%{attribute} %{message}' + general: Un problème a été rencontré. + general_again: Une erreur s'est produite. Merci de réessayer. + general_msg: ! 'Une erreur s''est produite: %{msg}' + messages: + accepted: doit obligatoirement être accepté + blank: doit obligatoirement être complété + confirmation: ne correspond pas avec le champ de confirmation + empty: doit obligatoirement être complété + equal_to: doit obligatoirement être égal à %{count} + even: doit obligatoirement être pair + exclusion: n'est pas disponible + greater_than: doit obligatoirement être supérieur à %{count} + greater_than_or_equal_to: doit obligatoirement être supérieur ou égal à %{count} + inclusion: n'est pas une valeur valide + invalid: n'est pas valide + less_than: doit obligatoirement être inférieur à %{count} + less_than_or_equal_to: doit obligatoirement être inférieur à %{count} + not_a_number: n'est pas un nombre + not_an_integer: doit être un nombre entier + odd: doit obligatoirement être impair + record_invalid: ! 'la vérification a échoué: %{errors}' + taken: a déjà été attribué + taken_with_deleted: a déjà été attribué (à une cellule supprimée depuis) + too_long: est trop long (%{count} signes autorisés au maximum) + too_short: est trop court (%{count} signes au minimum doivent être présents) + wrong_length: n'est pas de la bonne longueur (exactement %{count} signes doivent être présents) + template: + body: ! 'Merci de contrôler le contenu des champs suivants:' + header: + one: ! '%{model} n''a pas pu être sauvegardé à cause de la présence d''une erreur.' + other: ! '%{model} n''a pas pu être sauvegardé car %{count} erreurs ont été trouvées.' + activerecord: + attributes: + article: + article_category: catégorie + availability: l'article est-il disponible? + deposit: consigne + fc_price: prix final + fc_share: supplément boufcoop + gross_price: prix brut + price: prix net + tax: TVA + unit: unité + unit_quantity: unités par lot + financial_transaction: + amount: montant + note: note + stock_article: + price: Prix net + user: + first_name: Prénom + password: Mot de passe + errors: + format: ! '%{attribute} %{message}' + general: Une problème a été rencontré. + general_again: Une erreur s'est produite. Merci de réessayer. + general_msg: ! 'Un problème a été rencontré: %{msg}' + has_many_left: est encore associé à une %{collection}! + messages: + accepted: doit obligatoirement être accepté + blank: doit obligatoirement être complété + confirmation: ne correspond pas au champ de confirmation + empty: doit obligatoirement être complété + equal_to: doit obligatoirement être égal à %{count} + even: doit être un nombre pair + exclusion: n'est pas disponible + greater_than: doit obligatoirement être supérieur à %{count} + greater_than_or_equal_to: doit obligatoirement être supérieur ou égal à %{count} + inclusion: n'est pas un valeur valide + invalid: est invalide + less_than: doit obligatoirement être inférieur à %{count} + less_than_or_equal_to: doit obligatoirement être inférieur ou égal à %{count} + not_a_number: n'est pas un nombre + not_an_integer: doit obligatoirement être un nombre entier + odd: doit obligaroirement être un nombre impair + record_invalid: ! 'la vérification a échoué: %{errors}' + taken: a déjà été attribué + taken_with_deleted: a déjà été attribué (à une cellule supprimée depuis) + too_long: est trop long (au maximum %{count} signes sont autorisés) + too_short: est trop court (au minimum %{count} signes doivent être présents) + wrong_length: n'a pas la bonne longueur (exactement %{count} signes doivent être présents) + models: + task: + attributes: + done: + exclusion: répétition hebdomadaire invalide pour un boulot déjà effectué + template: + body: ! 'Merci de vérifier le contenu des champs suivants:' + header: + one: ! '%{model} n''a pu être sauvegardé à cause de la présence d''une erreur.' + other: ! '%{model} n''a pu être sauvegardé à cause de %{count} erreurs.' + models: + article: Article + article_category: la nouvelle catégorie + delivery: le nouveau réapprovisionnement + financial_transaction: la transaction + invoice: la nouvelle facture + message: Message + order: la nouvelle commande + order_article: Article à commander + order_comment: un nouveau commentaire + ordergroup: la nouvelle cellule + stock_article: l'article à stocker + stock_taking: Inventaire + supplier: FournisseusE_r + task: comme nouveau boulot + user: le nouveau membre + workgroup: la nouvelle équipe + admin: + access_to: accès à + actions: Actions + base: + index: + all_ordergroups: Toutes les cellules + all_users: TouTEs les membres + all_workgroups: Toutes les équipes + created_at: créé le + first_paragraph: Les cellules et les membres de la boufcoop peuvent être administrés sur cette page. + groupname: nom de la cellule + members: membres + name: nom + new_ordergroup: Nouvelle cellule + new_user: NouveLLE_eau membre + new_workgroup: Nouvelle équipe + newest_groups: Cellules les plus récentes + newest_users: Membres les plus récents + title: Administration + type: type + username: identifiant + confirm: Veux-tu vraiment supprimer %{name}? + ordergroups: + destroy: + error: ! 'La cellule n''a pas pu être dissoute: %{error}' + notice: La cellule a été supprimée + edit: + title: Modifier les informations sur la cellule + form: + first_paragraph: Invite de nouveaux membres %{url}. + here: ici + index: + first_paragraph: Sur cette page, des cellules peuvent être %{url}, modifiées ou bien dissoutes. + new_ordergroup: Définir une nouvelle cellule + new_ordergroups: créées + second_paragraph: ! 'Attention à bien noter la différence entre une équipe et une cellule: chaque membre fait partie d''une cellule, qui possède un certain crédit servant à payer les commandes, tandis qu''une %{url} (par exemple l''équipe distribution) s''occupe des boulots utiles à la boufcoop. Les membres appartiennent toujours à une et une seule cellule, mais peuvent faire partie de plusieurs équipes.' + title: Cellules + workgroup: équipe + new: + title: Définir une nouvelle cellule + ordergroups: + address: Adresse + contact: Contact + members: Membres + name: Nom + show: + confirm: T'es sûrE de ton coup? + edit: Modifier les données sur les cellules et/ou leurs membres + send_message: Envoyer un message + title: Cellule %{name} + search_placeholder: nom ... + users: + edit: + title: modifier les données sur le_la membre + index: + first_paragraph: Sur cette page, tu peux %{url}, modifier ou bien retirer des membres. + new_user: Ajouter unE nouveLLE_eau membre + new_users: ajouter + title: Administration des membres + new: + title: Ajouter unE nouveLLE_eau membre + show: + confirm: Veux-tu vraiment expulser %{user}? + email: Email + groupabos: Participation à des équipes + member_since: Membre depuis %{time} + name: Nom + nick: Identifiant + person: Personne + phone: Numéro de téléphone + preference: Préférences + send_message: Envoyer un message + users: + email: email + last_login: dernière connection + login: identifiant + name: nom + workgroups: + destroy: + error: ! 'Cette équipe n''a pas pu être supprimée: %{error}' + notice: L'équipe a bien été supprimée + edit: + title: Modifier les données sur l'équipe + form: + first_paragraph: De nouveaux membres peuvent être invités %{url}. + here: ici + index: + first_paragraph: Sur cette page, tu peux ajouter, modifier et supprimer %{url}. + new_workgroup: Définir une nouvelle équipe + new_workgroups: nouvelles équipes + ordergroup: cellule + second_paragraph: ! 'Attention à bien noter la différence entre une équipe et une cellule: chaque membre fait partie d''une %{url}, qui possède un certain crédit servant à payer les commandes, tandis qu''une équipe (par exemple l''équipe distribution) s''occupe des boulots utiles à la boufcoop. Les membres appartiennent toujours à une et une seule cellule, mais peuvent faire partie de plusieurs équipes.' + title: Équipes + new: + title: Définir une nouvelle équipe + show: + confirm: T'es sûrE de ton coup? + edit: Modifier les données sur l'équipe et/ou ses membres + title: Équipe %{name} + workgroups: + members: membres + name: nom + article_categories: + create: + notice: La catégorie a bien été définie. + destroy: + error: ! 'Cette catégorie n''a pas pu être supprimée: %{message}' + edit: + title: Modifier la catégorie + index: + confirm_delete: T'es sûrE de ton coup? + new: Créer une nouvelle catérogie + title: Catégories d'articles + new: + title: Créer une nouvelle catégorie + update: + notice: La catégorie a été mise à jour + articles: + article: + confirm_delete: T'es sûrE de ton coup? + last_update: ! 'dernière modification: %{last_update} | brut: %{gross_price}' + articles: + confirm_delete: Tu veux vraiment supprimer tous ces articles? + option_available: Marquer comme disponible(s) + option_delete: Supprimer + option_not_available: Marquer comme indisponible(s) + option_select: Choisir une action... + price_netto: Prix + unit_quantity_desc: Unités par lot + unit_quantity_short: U/L + controller: + create_from_upload: + notice: ! '%{count} nouveaux articles on été sauvegardés.' + error_invalid: La description des articles comporte des erreurs. + error_nosel: Aucun article n'a été sélectionné + error_parse: ! '%{msg} ... à la ligne %{line}' + error_update: ! 'Une erreur s''est produite lors de la mise à jour de l''article "%{article}": %{msg}' + parse_upload: + notice: ! '%{count} articles ont été examinés avec succès.' + sync: + notice: Le catalogue est à jour + shared_alert: ! '%{supplier} n''est pas associé à une base de données extérieure.' + update_all: + notice: Les articles et les prix ont été mis à jour. + update_sel: + notice_avail: Les articles sélectionnés ont été marqués comme disponibles. + notice_destroy: Les articles sélectionnés ont été supprimés. + notice_noaction: Aucune action n'a été spécifiée! + notice_unavail: Les articles sélectionnés ont été marqués comme indisponibles. + update_sync: + notice: Les articles et les prix ont été mis à jour. + destroy_active_article: + drop: supprimer + note: ! '%{article} apparaît dans des listes de commande en cours et ne peut donc être supprimé. Il faut d''abord %{drop_link} des listes de commande.' + edit_all: + note: ! 'Les champs obligatoires sont: le nom, l''unité, le prix net et le numéro de commande.' + submit: Mettre à jour tous les articles + title: Modifier tous les articles de %{supplier} + warning: Attention, tous les articles sont en train d'être mis à jour! + edit_all_table: + available_desc: disponible + available_short: disp. + order_number_desc: numéro de commande + order_number_short: n° + price_desc: Prix net + price_short: Prix + unit_quantity_desc: Unités par lot + unit_quantity_short: U/L + form: + title: Ajouter un nouvel article + import_search_results: + action_import: importer + already_imported: déjà importé + not_found: Aucun article n'a été trouvé + index: + change_supplier: Changer de fournisseusE_r... + edit_all: Tout modifier + ext_db: + import: Rechercher/Importer + sync: Synchroniser + title: Base de données externe + import: + placeholder: Nom... + restrict_region: Seulement les articles régionaux + title: Importer cet article + new: Nouvel article + new_order: Définir une nouvelle commande + search_placeholder: Nom... + title: Articles de %{supplier} (%{count}) + upload: Transférer les articles + model: + error_in_use: ! '%{article} ne peut pas être supprimé car il fait partie d''une liste de commande en cours!' + error_nosel: Aucun article n'a été sélectionné + parse_upload: + body: ! '

Merci de vérifier les articles importés.

+ +

Attention, les doublons ne sont pas automatiquement détectés.

.' + outlist: + body: ! 'Les articles suivants ne sont plus dans la liste et seront donc supprimés:' + body_skip: Aucun article à supprimer. + title: Exclure de la liste... + price_short: Prix + submit: Tout supprimer ou mettre à jour. + 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.

+ title: Mettre à jour... + update_msg: ! 'Ces articles doivent être mis à jour:' + 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.

+ +

Les champs doivent être délimités par des points-virgules ('';''), et le texte compris entre guillemets ("texte...")

+ +

L''encodage du fichier doit être UTF-8. L''ordre des colonnes est:

' + fields: + season_amount: Quantité échelonnée + season_price: Prix échelonné + status: Statut (x=exclu) + file_label: Merci de choisir un fichier compatible + submit: Transférer le fichier + title: ! '%{supplier} / Transférer les données sur l''article' + date: + abbr_day_names: + - Lun + - Mar + - Mer + - Jeu + - Ven + - Sam + - Dim + abbr_month_names: + - + - Janvier + - Février + - Mars + - Avril + - Mai + - Juin + - Juillet + - Août + - Septembre + - Octobre + - Novembre + - Décembre + day_names: + - Dimanche + - Lundi + - Mardi + - Mercredi + - Jeudi + - Vendredi + - Samedi + formats: + default: ! '%d.%m.%Y' + long: ! '%e.%B %Y' + short: ! '%e. %b' + month_names: + - + - Janvier + - Février + - Mars + - Avril + - Mai + - Juin + - Juillet + - Août + - Septembre + - Octobre + - Novembre + - Décembre + order: + - :day + - :month + - :year + datetime: + distance_in_words: + about_x_hours: + one: environ une heure + other: environ %{count} heures + about_x_months: + one: environ un mois + other: environ %{count} mois + about_x_years: + one: environ un an + other: environ %{count} ans + almost_x_years: + one: presque un an + other: presque %{count} ans + half_a_minute: une demi-minute + less_than_x_minutes: + one: moins d'une minute + other: moins de %{count} minutes + less_than_x_seconds: + one: moins d'une seconde + other: moins de %{count} secondes + over_x_years: + one: plus d'un an + other: plus de %{count} ans + x_days: + one: un jour + other: ! '%{count} jours' + x_minutes: + one: une minute + other: ! '%{count} minutes' + x_months: + one: un mois + other: ! '%{count} mois' + x_seconds: + one: une seconde + other: ! '%{count} secondes' + prompts: + day: jour + hour: heures + minute: minute + month: mois + second: secondes + year: an + deliveries: + add_stock_change: + how_many_units: Combien d unités (%{unit}) de l article %{name} doivent-elles être livrées? + create: + notice: Le réapprovisionnement a bien a été défini. Attention à ne pas oublier de déposer la facture correspondante! + create_stock_article: + notice: L'article "%{name}" a été ajouté au stock. + destroy: + notice: Le réapprovisionnement a été annulé. + edit: + title: Modifier le réapprovisionnement + form: + actions: Options + article: Article + category: Catégorie + create_from_blank: Ajouter un nouvel article quelconque + create_stock_article: Ajouter un article au stock + price: Prix net + quantity: Quantité + title_fill_quantities: 2. Définir la quantité à livrer + title_finish_delivery: 3. Clore le réapprovisionnement + title_select_stock_articles: 1. Choisir les articles en stock + unit: Unité + index: + confirm_delete: T'es sûrE de ton coup? + new_delivery: Réapprovisionner le stock par %{supplier} + title: ! '%{supplier}/Réapprovisionnements' + invoice_amount: Montant de la facture + invoice_net_amount: Montant net de la facture + new: + title: Réapprovisionner le stock par %{supplier} + show: + amount: Quantité + article: Article + price: Prix net + sum: Prix total + sum_diff: montant brut - montant net + sum_gross: prix total brut + sum_net: prix total net + title: Afficher le réapprovisionnement + title_articles: Article + unit: Unité + stock_article_for_adding: + action_add_to_delivery: Commander + action_edit: Modifier + action_other_price: Copier + stock_article_form: + copy_stock_article: Copier l'article + stock_change_fields: + remove_article: Retirer l'article de cette commande + suppliers_overview: Liste des fournisseusEs_rs + update: + notice: La commande a été actualisée + update_stock_article: + notice: Les données de l'article "%{name}" ont été mises à jour. + documents: + order_by_articles: + filename: Commande %{name}-%{date} - Trier par + rows: + - Cellule + - Quantité + - Prix + title: ! 'Ordre des articles pour la commande: %{name}, close le %{date}' + order_by_groups: + filename: Commande %{name}-%{date} - Répartition par cellules + rows: + - Nom de l'article + - Quantité + - Prix unitaire + - Unités par lot + - Unité + - Prix total + sum: prix total + title: ! 'Répartition par cellules pour la commande: %{name}, close le %{date}' + order_fax: + filename: Commande %{name}-%{date} - Fax + rows: + - Numéro + - Quantité + - Nom + - Nombre de lots + - Unité + - Prix unitaire + total: + order_matrix: + filename: Commande %{name}-%{date} - Tableau de répartition + heading: Liste des articles + rows: + - Article + - Unité + - Nombre de lots + - Prix coop + - Quantité + title: ! 'Tableau de répartition pour la commande: %{name}; close le %{date}' + total: + one: Un seul article + other: ! '%{count} articles au total' + errors: + format: ! '%{attribute} %{message}' + general: Un problème a été rencontré. + general_again: Une erreur s'est produite. Merci de réessayer. + general_msg: ! 'Une erreur s''est produite: %{msg}' + messages: + accepted: doit obligatoirement être accepté + blank: doit obligatoirement être complété + confirmation: ! ' ne correspond pas au champ de confirmatio' + empty: doit obligatoirement être complété + equal_to: doit obligatoirement être égal à %{count} + even: doit obligatoirement être un nombre pair + exclusion: n'est pas disponible + greater_than: doit obligatoirement être supérieur à %{count} + greater_than_or_equal_to: doit obligatoirement être supérieur ou égal à %{count} + inclusion: n'est pas une valeur valide + invalid: n'est pas valide + less_than: doit obligatoirement être inférieur à %{count} + less_than_or_equal_to: doit obligatoirement être inférieur ou égal à %{count} + not_a_number: n'est pas un nombre + not_an_integer: doit obligatoirement être un nombre entier + odd: doit obligatoirement être un nombre impair + record_invalid: ! 'La vérification a échoué: %{errors}' + taken: a déjà été attribué + taken_with_deleted: a déjà été attribué (à une cellule supprimée depuis) + too_long: est trop long (au maximum %{count} signes sont autorisés) + too_short: est trop court (au minimum %{count} signes doivent être présents) + wrong_length: n'a pas la bonne longueur (exactement %{count} signes doivent être présents) + template: + body: ! 'Merci de contrôler le contenu des champs suivants:' + header: + one: ! '%{model} n''a pas pu être sauvegardé: une erreur trouvée.' + other: ! '%{model} n''a pas pu être sauvegardé: %{count} erreurs trouvées.' + feedback: + create: + notice: Ton commentaire a été transmis avec succès. Merci + new: + first_paragraph: Tu as trouvé une erreur? Tu as une proposition, une idée, une critique? Envoie un commentaire! + second_paragraph: ! 'Petite remarque: l''équipe de Foodsoft s''occupe seulement de la maintenance du logiciel. + + Pour les questions concernants l''organisation de ta boufcoop, il faut contacter les personnes concernées.' + send: Transmettre + title: Laisser un commentaire + finance: + balancing: + close: + alert: ! 'Une erreur s''est produite lors du décompte: %{message}' + notice: La commande a été décomptée avec succès, et les comptes des membres ont été mis à jour. + close_direct: + alert: ! 'Impossible de clore cette commande: %{message}' + notice: La commande a été close avec succès. + confirm: + clear: Terminer + first_paragraph: ! 'Lorsque la commande sera close, les comptes seront mis à jour en conséquence. + +
+ + Les décomptes pour cette commande s''élèveront comme suit: ' + or_cancel: ou retourner au décompte + title: Décompter la commande + edit_results_by_articles: + add_article: Ajouter un article + amount: Quantité + amount_per_unit: Poids d'un lot + article: Article + gross: Brut + net: Net + number: Numéro + refund: Consigne + tax: TVA + group_order_articles: + add_group: Créer un nouveau groupe + group: Groupe + total: Prix total + total_fc: Prix total (pour la boufcoop) + units: Nombre d'unités + index: + title: Commandes closes + invoice: + edit: Modifier la facture + invoice_amount: ! 'Montant de la facture:' + invoice_date: ! 'Date de la facture:' + invoice_number: ! 'Numéro de la facture:' + minus_refund_calculated: ! 'Prix de la consigne:' + new: Saisir une nouvelle facture + new_body: Ajouter une facture pour cette commande + plus_refund_credited: Remboursement de la consigne + refund_adjusted_amount: Montant recalculé en excluant les consignes + new: + alert: Attention, cette commande a déjà été décomptée + articles_overview: Aperçu des articles + comment_on_transaction: Ici, tu peux faire part de tes commentaires concernant le décompte de la facture + comments: Commentaire + confirm_order: Terminer la commande + create_invoice: Ajouter une facture + edit_note: Modifier la note + edit_order: Modifier la commande + groups_overview: Aperçu des cellule + invoice: Facture + notes_and_journal: Notes/Remarques + summary: Résumé + title: décompter %{name} + view_options: Préférences d'affichage + order_article: + confirm: T'es sûrE de ton coup? + orders: + clear: décompter + cleared: déjà décompté (%{amount}) + close: fermer maintenant + confirm: Veux-tu vraiment terminer la commande? + end: Fin + ended: closes + last_edited_by: Dernières modifications effectuées par + name: FournisseusE_r + no_closed_orders: Aucune commande n'a encore été close. + state: Statut + summary: + changed: Les données ont été modifiées! + duration: von %{starts} bis %{ends} + fc_amount: ! 'Montant boufcoop:' + fc_profit: Gain de la boufcoop + gross_amount: Montant brut + groups_amount: ! 'Montant pour chaque cellule:' + net_amount: ! 'Montant net:' + reload: Actualiser le résumé + with_extra_charge: ! 'avec supplément:' + without_extra_charge: ! 'sans supplément:' + 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. + index: + balance: ! 'Solde: %{balance}' + last_updated_at: (dernière mise à jour il y a %{when}) + new_transaction: Ajouter une nouvelle transaction + search_placeholder: Rechercher ... + title: Relevé de compte pour %{name} + new: + paragraph: Cet espace permet de rajouter ou d'enlever du crédit à la cellule %{name}. + title: Nouvelle transaction + new_collection: + amount: Montant + new_ordergroup: Créer d'autres cellules + note: Note + ordergroup: Cellule + save: Sauvegarder les transactions + sidebar: ! "Cet espace permet de mettre à jour plusieurs comptes simultanément, \npar exemple pour saisir les versements des cellules sur leurs comptes à partir d'un relevé." + title: Mettre à jour plusieurs comptes + ordergroup: + remove: Supprimer + remove_group: Supprimer cette cellule + transactions: + amount: Montant + date: Date + note: Note + who: Qui? + group_order_articles: + form: + amount_change_for: Modification de la quantité de %{article} + index: + amount: Montant + amount_fc: Montant(boufcoop) + clear: Décompter + date: Date + end: Fin + everything_cleared: Super, tout est a déjà été décompté! + group: Cellule + last_transactions: Dernières transactions + note: Note + open_transactions: à décompter + show_all: tout afficher + supplier: FournisseusE_r + title: Espace trésorerie + unpaid_invoices: Factures à régler + invoices: + edit: + title: Modifier cette facture + index: + action_new: Définir une nouvelle facture + title: Factures + invoices: + confirm_delete: T'es sûrE de ton coup? + delivery: Réapprovisionnement + linked: Cette facture est associée à %{what_link}. + linked_delivery: un réapprovisionnement + linked_order: une commande + new: + back: Retour + title: Définir une nouvelle facture + show: + back: Retour + title: Facture %{number} + order_articles: + edit: + title: Mettre à jour la liste des article + new: + title: + ordergroups: + index: + new_transaction: Saisir une nouvelle transaction + search_placeholder: Rechercher ... + title: Crédits des cellules + ordergroups: + account_balance: Crédit disponible + account_statement: Relevé de compte + contact: + name: Nom + new_transaction: Nouvelle transaction + update: + notice: La facture a été mise à jour. + foodcoop: + ordergroups: + index: + name: Rechercher... + only_active: Seulement les cellules en activité + only_active_desc: (ayant commandé au moins une fois au cours des 3 derniers mois) + title: Cellules + ordergroups: + last_ordered: dernière commande + name: Nom + user: Membres + users: + index: + body: ! '

Cette page sert à envoyer des messages aux autres membres de la coop.

+ +

Si tu veux que tes coordonnées soient visibles par les autres, il faut le spécifier dans tes %{profile_link}.

' + ph_name: Nom ... + ph_ordergroup: Cellule ... + profile_link: préférences + title: Membres + workgroups: + edit: + invite_link: ici + invite_new: Tu peux engrainer de nouveaux membres %{invite_link}. + title: Modifier cette équipe + index: + body: ! '

Seuls les membres d''une équipe peuvent la modifier.

+ +

Tu peux rejoindre une équipe en contactant un de ses membres.

' + title: Équipes + workgroup: + edit: Modifier la cellule + show_tasks: Afficher tous les boulots + group_orders: + archive: + desc: Accéder à toutes les %{link}. + open_orders: commandes en cours + title: commandes de %{group} + title_closed: déjà décomptées + title_open: closes/pas encore décomptées + create: + error_general: Suite à une erreur, la commande n'a pu être mise à jour. + error_stale: La commande n'a pas pu être mise à jour car quelqu'un d'autre a commandé entre temps. + notice: Ta commande a bien été enregistrée. + errors: + closed: La commande est déjà close. + no_member: Tu n'es encore membre d'aucune cellule. + notfound: ! ' Mauvaise adresse, ce n''est pas ta commande.' + form: + action_save: Enregistrer ta commande + amount: Quantité + available: Disponible + available_funds: Crédit disponible + created_by: Établi par + ending: Clôture le + funds: Crédit + last_update: Dernière commande + manufacturer: Produit par + min_quantity: Quantité minimale + name: Nom + new_funds: Nouveau solde + note: Note + price: Prix + sum: Prix total + sum_amount: ! 'Quantité déjà commandée:' + supplier: Fourni par + title: Commander + tolerance: Tolérance + total_sum_amount: Montant total + total_tolerance: Tolérance totale + unit: Unité + unit_missing: Unités manquantes + units: Lots + units_full: Lots complet + units_total: Unités déjà commandées + index: + closed_orders: + more: suite... + title: Commandes décomptées + finished_orders: + title: Commandes par encore décomptées + total_sum: Total + funds: + account_balance: Crédit initial + available_funds: Crédit disponible + finished_orders: montant prévu des commandes non décomptées + open_orders: montant des commandes en cours + title: Crédit + title: Aperçu des commandes + messages: + not_enough_apples: ! 'Il faut que ta cellule ait au moins %{stop_ordering_under} glands pour pouvoir commander, + + alors que vous n''en avez que %{apples} pour le moment.' + order: + title: Article + orders: + ending: Clôture le + sum: Total + supplier: FournisseusE_r + show: + articles: + edit_order: Modifier ta commande + name: Nom + not_ordered_msg: Tu n'as pas encore commandé + order_closed_msg: Désolé, cette commande a déjà été fermée. Il faudra te réveiller plus tôt la prochaine fois + order_nopen_title: En tenant compte des commandes en cours de toutes les cellules + order_not_open: Déjà reçu + order_now: Voilà ta chance! + order_open: Quantité à prévoir + ordered: Quantité souhaitée + ordered_title: Quantité + tolérance + show_hide: Montrer/cacher les articles non commandés + sum: Total + title: Aperçu des articles + total_price: Prix + unit_price: Prix unitaire + units: Lots + closed_by: Décompté par %{user} + comment: Lire/écrire des commentaire + comments: + title: Commentaire + ending: Clôture le + not_ordered: Tu n'as pas commandé. + note: Note + order_sum: Total de la commande + sum: Total + supplier: Fourni par + title: Ta part de la commande %{order} + switch_order: + remaining: encore %{remaining} + title: Commandes en cours + update: + error_general: Suite à une erreur, la commande n'a pu être mise à jour. + error_stale: La commande n'a pas pu être mise à jour, car quelqu'un d'autre a commandé entre temps. + notice: Ta commande a bien été enregistrée. + helpers: + application: + edit_user: Modifier la liste des membres + role_admin: Administrateur + role_article_meta: Article + role_finance: Finances + role_orders: Commande + role_suppliers: FournisseusEs_rs + show_google_maps: Afficher la position sur Google maps + sort_by: Trier par %{text} + write_message: Écrire un message + deliveries: + new_invoice: Ajouter une nouvelle facture + show_invoice: Afficher la facture + orders: + option_choose: Choix d'unE fournisseusE_r + option_stock: Stock + order_pdf: Générer un PDF + select: + prompt: Faire un choix + submit: + create: Définir %{model} + invite: + create: Envoyer une invitatio + message: + create: Envoyer un message + update: Sauvergarder les modifications + tasks: + required_users: Il manque encore %{count} camarades! + home: + apple_bar: + desc: ! 'Ce système de glands sert à comparer la durée du travail collectif auquel ta cellule a contribué (rapportée à la quantité commandée) avec + + la moyenne du travail effectué par toutes les cellules. + + Actuellement, cette moyenne est d''une heure de boulot pour %{amount} commandés.' + more_info: Plus d'informations + points: ! 'Nombre de glands: %{points}' + warning: ! 'Attention, si ta cellule a moins de %{threshold} glands, tu ne pourras plus commander! + + (ce seuil est fixé par la coop)' + changes_saved: Les modifications ont été sauvegardées. + index: + due_date_format: ! '%A, %d. %b' + messages: + title: Derniers messages reçus + view_all: Afficher tous les messages + my_ordergroup: + funds: ! '| Crédit disponible:' + last_update: La dernière mise à jour date du %{when} + title: Ta cellule + transactions: + amount: Montant + note: Note + title: Dernière transactions + view: Afficher un relevé de compte + when: Quand? + where: Qui? + ordergroup: + title: Niveau de participation de ta cellule + tasks_move: + action: accepter/refuser des boulots + desc: Tu as du boulot de prévu. + title: Accepter des boulot + tasks_open: + action: boulot(s) disponible(s) + desc: Il y a %{size} + title: Boulots disponibles + title: Page d'accueil + your_tasks: Voilà le boulot que tu as accepté en ce moment + no_ordergroups: Tu ne fais encore partie d'aucune cellule + ordergroup: + account_summary: Relevé de compte + description: Description + funds: ! 'Crédit disponible:' + invite: Engrainer une nouvelle personne + people: Personnes + search: Rechercher ... + title: Ta cellule + ordergroup_cancelled: Tu ne fais plus partie de la cellule %{group}. + profile: + groups: + cancel: Quitter la coop + cancel_confirm: T'es sûrE de vouloir partir? + invite: Engrainer de nouveaux membres + title: Tu fais partie des équipes suivantes + title: Ton profil + user: + since: ! '(Membre depuis: %{when})' + title: ! '%{user}' + start_nav: + admin: Administration + finances: + accounts: Mettre à jour les compte + settle: Décompter des commandes + title: Espace trésorerie + foodcoop: Boufcoop + members: Membres + new_ordergroup: Définir une nouvelle cellule + new_user: Ajouter un nouveau membre + orders: + end: Terminer des commandes + overview: Aperçu des commandes + title: Commandes + products: + edit: Mettre à jour les articles + edit_stock: Gérer les stocks + edit_suppliers: Gérer les fournisseusEs_rs + title: Gérer les articles + tasks: Ton boulot + title: Aller à... + write_message: Écrire un message + invites: + errors: + already_member: est déjà membre de la Boufcoop. + modal_form: + body: Sur cette page, tu peux inviter une personne dans la cellule %{group} qui n'est pas encore membre de la Boufcoop.

Après sa première connexion elle sera automatiquement membre de la cellule.

+ 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: + application1: + title: Foodsoft - %{title} + email: + footer: ! '-- + + Foodsoft: %{foodsoft} + + Accueil de la Boufcoop: %{foodcoop} + + Aide: %{help}' + foodsoft: Foodsoft + header: + feedback: + desc: Tu as trouvé une erreur? Tu as des propositions, des idées, des critiques? + title: Retours + footer: Foodsoft, un logiciel libre pour gérer les Boufcoops. + help: Aide + logout: Déconnexion + ordergroup: Ta cellule + profile: Ton profil + logo: coop + lib: + order_pdf: + page: page %{number} + login: + accept_invitation: + body: ! '

Tu viens d''être invité à rejoindre la cellule "%{group}" de la coopérative d''approvisionnement (bouffe-coop) %{foodcoop} !

+ +

Remplis ce formulaire si tu es d''accord pour être de la partie.

+ +

Ces données ne seront en aucun cas publiées, ou transmises à quiconque d''extérieur à la coop. + + Pour des raisons techniques, les membres de la coop qui s''occupent du site internet ont accès aux données, + + mais en ce qui concerne les autres membres de la coop, tu pourras choisir quelles données leurs sont visibles. + +' + submit: Créer un compte Foodsoft + title: Invitation chez les %{name} + controller: + accept_invitation: + notice: Ton compte vient d'être créé. Bienvenue! Tu peux maintenant te connecter. + error_group_invalid: Désolé, la cellule par laquelle tu as été invité a été supprimée entre temps. + 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. ' + update_password: + notice: Ton mot de passe a été mis à jour. Tu peux maintenant de connecter. + forgot_password: + body: ! '

Pas de problème, nous pouvons générer un nouveau mot de passe pour toi.

+ +

Pour cela, commence par saisir ci-dessous l''adresse email que tu as donné lors de ton inscription. + + Tu recevras ensuite un message avec de plus amples instructions.

' + submit: Demander un nouveau mot de passe + title: Mot de passe oublié? + new_password: + body:

Merci de saisir le nouveau mot de passe souhaité pour %{user}

+ submit: Sauvegarder le nouveau mot de passe + title: Nouveau mot de passe + mailer: + dateformat: ! '%d %b' + feedback: + header: ! 'Le %{date}, %{user} a écrit:' + subject: Retour de %{email} + foodsoft_message: + footer: ! 'Répondre: %{reply_url} + + Afficher ce message dans ton navigateur: %{msg_url} + + Préférences des messages: %{profile_url}' + invite: + subject: Invitation à participer à une Bouffecoop + text: ! 'Salut! + + %{user} <%{mail}> vient de t''engrainer à rejoindre la cellule "%{group}". + + Pour accepter cet engrenage et ainsi faire partie de notre Boufcoop, visite: %{link} + + Ce lien ne sera valide que pour une seule visite et s''autodétruira le %{expires}.' + negative_balance: + subject: Compte dans le rouge! + text: ! 'CherEs %{group}, + + + Votre compte sur la bouffecoop est passé au rouge le %{when}, et son solde actuel est %{balance}. + + %{amount} ont été prélevés par %{user} en règlement de "%{note}". + + Il faudrait penser rapidement à remettre du crédit! + + + Message automatisé de %{foodcoop}' + not_enough_users_assigned: + subject: Il y a encore besoin de monde pour "%{task}" + text: ! 'CherE %{user}, + + + Il manque encore du monde le %{when} pour le boulot "%{task}" dont ton équipe est responsable. + + Si tu es dispo et ne t''es pas encore inscritE, c''est le moment de le faire: + + %{workgroup_tasks_url} + + + Pour voir ton agenda complet: %{user_tasks_url} + +' + order_result: + subject: ! 'Commande close: %{name}' + text0: ! 'CherEs %{ordergroup}, + + + La commande pour "%{order}" a été fermée le %{when} par %{user}. + + Voilà la liste des articles qui ont été commandés pour vous:' + text1: ! 'Prix total: %{sum} + + Afficher sur le site: %{order_url} + + + Message envoyé automatiquement par %{foodcoop}' + reset_password: + subject: Nouveau mot de passe pour %{username} + text: ! 'Salut %{user}, + + + Toi (ou quelqu''un d''autre) vient de demander un nouveau mot de passe sur le site de la bouffecoop. + + Pour le choisir, va sur la page suivante: %{link}. + + Ce lien n''est valide que pour une seule utilisation et expirera le %{expires}. + + Si tu as changé d''avis ou si tu n''es pas à l''origine de ce mail, aucune action n''est requise de ta part, et ton mot de passe restera inchangé. + + + Message automatiquement envoyé par Foodsoft.' + upcoming_tasks: + nextweek: ! 'Agenda de la semaine prochaine:' + subject: Tu as du boulot! + text0: ! 'CherE %{user}, + + + Tu t''es inscritE pour le boulot "%{task}", qui aura lieu demain (%{when}). + +' + text1: ! 'Ton agenda: %{user_tasks_url} + + + Ceci est un rappel automatisé envoyé par %{foodcoop}.' + messages: + create: + notice: Le message a bien été sauvegardé et est en cours d'envoi. + index: + new: Nouveau message + title: Messages + messages: + reply: Répondre + model: + reply_header: ! 'Le %{when}, %{user} a écrit:' + reply_indent: ! '> %{line}' + reply_subject: ! 'Re: %{subject}' + new: + list: + desc: Pour envoyer un message à tout le monde, passe par la mailing list "%{list}" + mail: par exemple en envoyant un email à %{email} + subscribe: Pour plus d'explications concernant la mailing list, consulte le %{link} + subscribe_msg: Il faudra peut être d'abord t'inscrire à la mailing list. + wiki: wiki (section mailing list) + no_user_found: ! 'AucunE membre correspondantE n''a été trouvéE ' + search: Rechercher ... + search_user: Rechercher unE membre + title: Nouveau message + show: + all_messages: Aperçu des messages + from: ! 'De:' + reply: Répondre + sent_on: ! 'Envoyé le:' + subject: ! 'Sujet:' + title: Afficher le contenu du message + model: + delivery: + each_stock_article_must_be_unique: Chaque article à stocker ne peut apparaître qu'une seule fois dans la commande. + membership: + no_admin_delete: ! 'Pas moyen de quitter le navire: tu es le ou la dernièrE administratrice à bord!' + order_article: + error_price: doit être saisi et avoir un prix à jour + page: + redirect: Redirection vers [[%{title}]]... + user: + no_ordergroup: aucune cellule + navigation: + admin: + home: Aperçu + ordergroups: Cellules + title: Administration + users: Membres + workgroups: Équipes + articles: + categories: Catégories + stock: Gestion du stock + suppliers: FournisseusEs_rs/Articles + title: Articles + dashboard: Tableau de bord + finances: + accounts: Crédits des cellules + balancing: Décompte des commandes + home: Aperçu + invoices: Factures + title: Trésorerie + foodcoop: Boufcoop + members: Membres + messages: Messages + ordergroups: Cellules + orders: + archive: Historique des commandes + manage: Gestion des commandes + ordering: Passer une commande + title: Commandes + tasks: Boulot + wiki: + all_pages: Toutes les pages + home: Page d'accueil + title: Wiki + workgroups: Équipes + number: + currency: + format: + delimiter: . + format: ! '%n %u' + precision: 2 + separator: ! ',' + significant: false + strip_insignificant_zeros: false + unit: € + format: + delimiter: . + precision: 2 + separator: ! ',' + significant: false + strip_insignificant_zeros: false + human: + decimal_units: + format: ! '%n %u' + units: + billion: + one: milliard + other: milliards + million: millions + quadrillion: + one: billiard + other: billiards + thousand: mille + trillion: billions + unit: + format: + delimiter: + precision: 1 + significant: true + strip_insignificant_zeros: true + storage_units: + format: ! '%n %u' + units: + byte: + one: Octet + other: Octets + gb: Go + kb: kB + mb: MB + tb: TB + percentage: + format: + delimiter: + precision: + format: + delimiter: + ordergroups: + edit: + title: Modifier les cellules + index: + title: Cellule + model: + error_single_group: ! '%{user} fait déjà partie d''une autre cellule' + invalid_balance: n'est pas un nombre valide + orders: + articles: + article_count: ! 'Articles commandés:' + name: Nom + prices: Prix brut/net + 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 + create: + notice: La commande a bien été définie. + edit: + title: Modifier la commande + fax: + amount: Quantité + articles: Articles + customer_number: Numéro de client de la coop + delivery_day: Jour de livraison + heading: Commande pour %{name} + name: Nom + number: Numéro + to_address: Adresse du destinataire + finish: + notice: La commande a été close. + form: + name: Nom + note: Note + origin: Origine + prices: Prix (net/coop) + select_all: Tout sélectionner + stockit: Disponible + supplier: Productrice_teur + title: Article + unit_quantity: Lots + index: + action_end: Terminer + confirm_delete: Vraiment supprimer la commande? + confirm_end: Veux tu vraiment mettre fin à la commande %{order}? Attention, il n'y aura pas d'annulation possible. + ended_orders: Commandes closes + ending: Clôture le + new_order: Définir une nouvelle commande + no_open_orders: Il n'y a aucune commande en cours en ce moment. + note: Note + open_orders: Commandes en cours + supplier: FournisseusE_r + title: Gestion des commandes + model: + error_closed: Cette commande a déjà été décomptée + error_nosel: Au minimum un article doit être sélectionné + 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 + new: + title: Définir une nouvelle commande + orders: + ending: Clôture le + start: Début + status: Statut + supplier: FournisseurE + show: + action_end: Clore! + amounts: ! 'Total net/brut:' + articles: Aperçu des articles + articles_ordered: ! 'Articles commandés:' + begin: ! 'Début:' + comments: + title: Commentaire + comments_link: Commenaire + confirm_delete: Veux-tu vraiment supprimer la commande? + confirm_end: Veux tu vraiment terminer la commande %{order}? Pas d'annulation possible. + created_by: ! 'Créée par:' + download: + article_pdf: Liste des articles en PDF + download_file: Télécharger + fax_pdf: Fax au format PDF + fax_txt: Fax au format texte + group_pdf: Liste des cellules en PDF + matrix_pdf: Matrice de distribution en PDF + title: Télécharger + ending: ! 'Clôture le:' + group_orders: ! 'Commandes des cellules:' + note: ! 'Note:' + sort_article: Trié par article + sort_group: Trié par cellules + supplier: FournisseurE + title: ! 'Commande: %{name}' + warn_not_closed: Attention, cette commande n'a pas encore été décomptée! + state: + closed: décomptée + finished: close + open: en cours + update: + notice: La commande a été mise à jour. + pages: + all: + new_page: Créer une nouvelle page + recent_changes: Changement récents + search: + action: Recherche + placeholder: Titre de la page ... + site_map: Aide à la navigation + title: Toutes les pages du wiki + title_list: Liste des pages + body: + title_toc: Contenu + create: + notice: La page a bien été créée. + cshow: + error_noexist: Cette page n'existe pas! + redirect_notice: Redirigé à partir de %{page}... + destroy: + notice: La page '%{page}' et toutes ses descendantes ont bien été supprimées. + edit: + title: Modifier cette page + error_stale_object: Cette page est en cours de modification par unE autre membre. Merci de réessayer plus tard. + form: + help: + bold: gra + external_link_ex: Pages extérieures + external_links: Liens externes + heading: Plan %{level} + headings: En-tête + italic: italique + list_item_1: Premier item + list_item_2: Deuxième item + noformat: Pas de formatage + ordered_list: Énumérations + section_block: Format de paragraphe + section_character: Format de charactère + section_link: Format des liens + section_table: Format des tableaux + see_tables: Voir %{tables_link} + tables_link: Tableaux + text: texte + title: Assistant de mise en forme rapide + unordered_list: Liste non ordonnée + wiki_link_ex: Page de wiki du Foodsoft + wiki_links: Liens Wiki + preview: Aperçu + last_updated: Dernièrement mis à jour + new: + title: Ajouter une nouvelle page au Wiki + page_list_item: + date_format: ! '%a, %d. %B %Y %H:%M:%S' + show: + date_format: ! '%d.%m.%y %H:%M' + delete: Supprimer la page + delete_confirm: Attention, tous les contenus seront aussi supprimés. T'es sûrE de ton coup? + edit: Modifier la page + last_updated: Dernière modification le %{when} par %{user} + subpages: Sous-pages + title_versions: Versions + versions: Versions (%{count}) + title: Titre + update: + notice: La page a été mise à jour + version: + author: ! 'Auteur: %{user}' + date_format: ! '%a, %d.%m.%Y, %H:%M heure' + revert: Revenir à cette version + title: ! '%{title} - Version %{version}' + title_version: Versio + view_current: Afficher la version actuelle + sessions: + logged_in: Connecté! + logged_out: Déconnecté! + login_invalid: Identifiant ou mot de passe invalide + new: + forgot_password: Mot de passe oublié? + login: Te connecter + nojs: Attention, les cookies et le javascript doivent être activés! Merci de désactiver %{link}. + noscript: NoScript + password: Mot de passe + title: Te connecter à Foodsoft + user: Identifiant + shared: + 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. + name: Nom + price: Prix total + 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é + address: Adresse + apple_limit: Minimum de glands + contact: Contact + deactivated: désactivé + description: Description + members: Membre + no_weekly_job: aucun boulot hebdomadaire n'a été défini + weekly_job: Boulot hebdomadaire + group_form_fields: + search: Recherche... + search_user: Rechercher par utilisatrice + title: Boulots hebdomadaires + user_not_found: Aucune utilisatrice n'a été trouvée. + loginInfo: + edit_profile: Modifier le profil + feedback: + desc: Tu as détecté une erreur? Tu as des propositions, des idées, des critiques? + title: Retours + help: Aide + homepage_title: Vers la page d'accueil de la bouffecoop + logout: Te déconnecter + profile: Profil + memberships: + current_members: + drop: désinscrire + no_members: ! '%{group} n''a aucun membre pour le moment.' + members: + already_members: Sont déjà membre + desc: Sur cette page, tu peux gérer les membres de l'équipe, et aussi %{link} un nouveau membre. + invite: engrainer + invite_someone: Inviter quelqu'unE + no_members_yet: Ne sont pas encore membre + title: Membre de %{group} + non_members: + add: ajouter + open_orders: + ending: Clôture le + no_open_orders: Il n'y a aucune commande en cours en ce moment + not_enough_apples: Désolé, ta cellule n'a pas assez de glands pour pouvoir commander! + supplier: FournisseusE_r + title: Commandes en cours + total: Total + total_sum: Total + who_ordered: Qui a commandé? + workgroup_members: + title: Membres des équipes + simple_form: + error_notification: + default_message: Une erreur s'est produite. Merci de vérifier le formulaire. + hints: + article: + unit: par exemple. kg ou 1l ou 500g + 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 + update_current_price: Modifie aussi le prix des commandes en cours + stock_article: + copy_stock_article: + name: Merci de modifier + edit_stock_article: + price:
  • Modification du prix enregistrée.
  • Si nécessaire %{stock_article_copy_link}.
+ supplier: + supplier: + min_order_quantity: La quantité minimum à commander est affichée pendant la commande et doit motiver + task: + duration: Combien de temps dure le boulot, 1 à 3 heures + required_users: De combien de personnes avons-nous besoin au total? + tax: En pourcentage, le standard est de 7,0 + labels: + article: + article_category: Catégorie + manufacturer: ProductRICE_eur + name: Nom + note: Note + origin: Lieu de production + unit: Unité + article_category: + description: Description + name: Nom + defaults: + amount: Montant + date: Date + deposit: Consigne + description: Description + email: Email + note: Note + order_number: ! 'Numéro ' + ordergroup: Cellule + password: Mot de passe + password_confirmation: Confirmation du mot de passe + phone: Téléphone + price: Prix (net) + tax: TVA + title: Titre + unit_quantity: Unités par lot + user_tokens: Membres + delivery: + delivered_on: Date de réapprovisionnement + supplier: Fournisseuse_r + group_order_article: + ordergroup_id: Cellul + result: Quantité + invoice: + amount: Montant + date: Date de facturation + delivery: Réapprovisionnement + deposit: Consigne facturée + deposit_credit: Consigne remboursée + note: Note + number: Numéro + order: Commande + paid_on: Payée le + supplier: Fournisseuse_r + message: + body: Contenu + group_id: Cellule ou équipe + private: Privé + recipient_tokens: Destinataires + sent_to_all: Envoyer à tous les membres + subject: Sujet + order: + ends: Clôture le + starts: Ouverture le + order_article: + units_to_order: Quantité + update_current_price: Mettre à jour le prix global + order_comment: + text: Commenter cette commande... + ordergroup: + contact_address: Adresse + contact_person: Personne à contacter + contact_phone: Téléphone + ignore_apple_restriction: Pour cette cellule, ne pas bloquer les commandes en cas de manque de glands + name: + page: + body: Contenu + parent_id: Page parente + settings: + messages: + send_as_email: Transmettre les messages de la boufcoop par email + notify: + negative_balance: Envoyer un avertissement si la cellule est dans le rouge. + order_finished: Envoyer un résumé de la commande finale (après la fermeture) + upcoming_tasks: Envoyer un rappel des prochains boulots + profile: + email_is_public: Permettre aux autres membres de voir l'adresse email. + language: Langue + name_is_public: Permettre aux autres membres de voir le nom. + phone_is_public: Permettre aux autres membres de voir le numéro de téléphone. + settings_group: + messages: Messages + privacy: Confidentialité + stock_article: + supplier: FournisseurE + supplier: + address: Adresse + contact_person: Contact + customer_number: Numéro de client de la coop + delivery_days: Jours de livraison + email: Email + fax: Fa + is_subscribed: abonné? + min_order_quantity: Quantité minimale à commander + name: Nom + note: Note + order_howto: Comment commander + phone: Téléphone + phone2: Autre téléphone + url: Site web + task: + done: Fait? + due_date: Pour quand? + duration: Durée + name: Nom + required_users: Nombre de personnes nécessaires + user_list: Responsables inscritEs + workgroup: Équipe + user: + email: Email + last_name: Nom de famille + name: Nom + nick: Identifiant + ordergroup: Cellule + phone: Téléphone + workgroup: + one: Équipe + other: Équipes + workgroup: + next_weekly_tasks_number: Combien de temps en avance les boulots doivent ils être annoncés sur le site? + role_admin: Administration + role_article_meta: Base de données des articles + role_finance: Trésorerie + role_orders: Gestion des commandes + role_suppliers: Contact avec les fournisseusEs_rs + 'no': Non + options: + settings: + profile: + language: + de: Allemand + en: Anglais + fr: Français + nl: Néerlandais + required: + mark: ! '*' + text: requis + 'yes': Oui + stock_takings: + create: + notice: L'inventaire a été créé avec succès. + edit: + title: Modifier les données de l'inventaire + index: + new_inventory: Inventorier le stock + title: Aperçu de l'inventaire + new: + create: ajouter + stock_articles: Articles en stock + temp_inventory: l'inventaire courant + text_deviations: ! 'Saisir ici les déviations constatées par rapport à %{inv_link}. + + En cas de manque, utiliser un signe ''-''.' + text_need_articles: Si certains articles n'apparaissent pas sur l'inventaire courant, il faut les y %{create_link} directement. + title: Inventorier le stock + show: + amount: Quantité + article: Article + confirm_delete: Vraiment annuler cet inventaire? + date: Date + note: Note + overview: Aperçu de l'inventaire + supplier: FournisseusE_r + title: Afficher l'inventaire + unit: Unité + stock_takings: + confirm_delete: T'es sûrE de ton coup? + date: Date + note: Note + update: + notice: Les données de l'inventaire ont été mises à jour. + stockit: + check: + not_empty: ! '%{name} ne peut pas être supprimé, car il y en a encore en stock.' + destroy: + notice: L'article %{name} a bien été supprimé du stock. + edit: + 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 + available: disponible + category: Catégorie + ordered: commandés + price: Prix + stock: en Stock + supplier: FournisseusE_r + unit: Unité + vat: TVA + confirm_delete: T'es sûrE de ton coup? + new_delivery: Réapprovisionner le stock... + new_stock_article: Ajouter un article au stock + new_stock_taking: Inventorier le stock + order_online: Définir une commande à partir du stock + show_stock_takings: Historique des inventaires + stock_count: ! 'Nombre d''articles:' + stock_worth: ! 'Valeur actuelle du stock:' + title: + toggle_unavailable: Afficher/cacher les articles actuellement indisponibles + view_options: Préférences d'affichage + new: + search_text: ! 'Rechercher des articles dans tous les catalogues:' + title: Ajouter un article au stock + stock_create: + notice: L'article a été sauvegardé. + stock_update: + notice: L'article a été sauvegardé. + suppliers: + create: + notice: FournisseusE_r misE à jour + destroy: + notice: FournisseusE_r suppriméE + edit: + title: Modifier le/la fournisseurE + index: + action_import: Importer unE fournisseurE d'une base de données extérieure + action_new: Ajouter unE fournisseurE + articles: Articles (%{count}) + confirm_del: Attention, veux-tu vraiment supprimer le/la fournisseurE %{name}? + deliveries: Livraisons (%{count}) + stock: en stock (%{count}) + title: FournisseurEs + new: + title: Ajouter unE fournisseurE + shared_supplier_note: Le/la fournisseurE a été associé a la base de données extérieure. + shared_suppliers: + body: ! '

Les fournisseurEs de la base de données extérieure sont affichéEs ici.

+ +

Tu peux encore en importer des nouveaux, en t''y abonnant ci-dessous.

+ +

De cette façon, le/la fournisseurE est automatiquement ajoutéE à la base de données.

' + subscribe: s'abonner + subscribe_again: s'abonner à nouveau + supplier: FournisseurE + title: Listes externes + show: + confirm_delete: Tu es sûrE de ton coup? + last_deliveries: Dernières livraisons + new_delivery: Réapprovisionner le stock + show_deliveries: Afficher tous les réapprovisionnements + update: + notice: Les données du_de la fournisseurE ont été mises à jour + support: + array: + last_word_connector: et + two_words_connector: et + words_connector: ! ',' + tasks: + accept: + notice: Tu as accepté ce boulot + archive: + title: Historique du boulot + archive_tasks: + due_date: Échéance + task: Sujet + task_format: ! '%{name} (%{duration}h)' + who: Personnes en charge + create: + notice: Le boulot a bien été défini. + destroy: + notice: Le boulot a été supprimé + edit: + title: Modifier les données du boulot + warning_periodic: ! 'Avertissement: + + Ce boulot fait partie d''une série de boulots hebdomadaires. + + Si les modifications sont enregistrées, il sera retiré de la série et converti en boulot ordinaire.' + error_not_found: Aucune équipe n'a été trouvée. + form: + search: + hint: Rechercher unE membre + noresult: AucunE membre n'a été trouvéE + placeholder: Recherche... + submit: + periodic: Définir comme boulot hebdomadaire + index: + show_group_tasks: Afficher l'agenda de cette équipe + title: Agenda + title_non_group: Boulots ouverts à tous + list: + accept_task: Te charger de ce boulot + done: Effectué + done_q: Effectué? + due_date: À faire pour le + mark_done: Marquer ce boulot comme étant effectué + reject_task: Refuser ce boulot + task: Description + task_format: ! '%{name} (%{duration}h)' + who: Qui le fait? + who_hint: (Combien manquent encore?) + nav: + all_tasks: L'agenda de la boufcoop + archive: Boulots déjà effectués (archives) + group_tasks: Sélectionner une équipe + my_tasks: Ton agenda + new_task: Définir un nouveau boulot + pages: + new: + title: Définition d'un nouveau boulot + repeated: Ce boulot a lieu toutes les semaines. + set_done: + notice: L'agenda a été mis à jour. + show: + accept_task: Te charger de ce boulot. + confirm_delete_group: Veux-tu vraiment supprimer ce boulot hebdomadaire? + delete_group: Supprimer ce boulot + due_date: Echéance + hours: ! '%{count}h' + mark_done: Marquer comme effectué + reject_task: Refuser ce boulot + title: Afficher la description du boulot + update: + 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}. ' + tasks_link: par là-bas + title: Ton boulot + title_accepted: Boulots acceptés + title_open: Boulots disponibles + workgroup: + title: Agenda de l'%{workgroup} + title_all: Boulot prévu pour l'équipe + time: + am: le matin + formats: + default: ! '%A, %d. %B %Y, %Hh%M' + long: ! '%A, %d. %B %Y, %Hh%M' + short: ! '%d. %B, %Hh%M ' + pm: après-midi + ui: + close: Fermer + delete: Supprimer + edit: Modifier + history: Afficher l'historique + marks: + close: ! '×' + success: + or_cancel: ou annuler + please_wait: Merci de patienter... + save: Sauvegarder + show: Afficher + views: + pagination: + first: ! '«' + last: ! '»' + next: ! '›' + previous: ! '‹' + truncate: ! '...' + workgroups: + 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. ' + index: + title: Équipes + update: + notice: L'équipe a été mise à jour diff --git a/config/locales/nl.yml b/config/locales/nl.yml index d059f462..4b632cc5 100644 --- a/config/locales/nl.yml +++ b/config/locales/nl.yml @@ -39,6 +39,8 @@ nl: article_category: categorie availability: Artikel leverbaar? deposit: statiegeld + fc_price: prijs foodcoop + fc_share: marge foodcoop gross_price: bruto prijs price: netto prijs tax: BTW @@ -81,6 +83,11 @@ nl: too_long: is te lang (niet meer dan %{count} tekens) too_short: is te kort (niet minder dan %{count} tekens) wrong_length: heeft de verkeerde lengte (moet precies %{count} tekens zijn) + models: + task: + attributes: + done: + exclusion: gedane taken kunnen niet herhaald worden template: body: ! 'Controleer de volgende velden:' header: @@ -138,7 +145,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: @@ -256,7 +263,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 @@ -265,10 +272,10 @@ nl: edit_all_table: available_desc: beschikbaar available_short: besch. - order_number_desc: bestelnummer - order_number_short: best.nr. - price_desc: netto prijs - price_short: prijs + order_number_desc: Bestelnummer + order_number_short: Best.nr. + price_desc: Netto prijs + price_short: Prijs unit_quantity_desc: Groothandelsverpakkingsgrootte unit_quantity_short: Gr.Eenh. form: @@ -305,11 +312,13 @@ nl: price_short: prijs submit: Alle wissen/bijwerken 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. De oude waarden zijn grijs, en de tekstvelden bevatten de huidige waarden.

+ +

Verschillen met de oude artikelen zijn geel gemarkeerd.

' + title: Bijwerken ... + update_msg: ! 'De volgende artikelen moeten bijgewerkt worden: ' upload: body: fields: @@ -416,20 +425,28 @@ nl: second: seconden year: jaren deliveries: + add_stock_change: + how_many_units: create: notice: + create_stock_article: + notice: destroy: notice: edit: title: form: - add_article: - new_article: - search: - title: - note_new_article: - note_new_article_link: - remove_article: + actions: + article: + category: + create_from_blank: + create_stock_article: + price: + quantity: + title_fill_quantities: + title_finish_delivery: + title_select_stock_articles: + unit: index: confirm_delete: new_delivery: @@ -449,66 +466,99 @@ nl: title: title_articles: unit: - stock_change: + stock_article_for_adding: + action_add_to_delivery: + action_edit: + action_other_price: + stock_article_form: + copy_stock_article: + stock_change_fields: remove_article: suppliers_overview: update: notice: + update_stock_article: + notice: documents: order_by_articles: - filename: - rows: - title: + filename: Bestelling %{name}-%{date} - Artikellijst + rows: + - Huishouden + - Hoeveelheid + - Prijs + title: ! 'Artikellijst van bestelling: %{name}, gesloten op %{date}' order_by_groups: - filename: - rows: - sum: - title: + filename: Bestelling %{name}-%{date} - Huishoudenslijst + rows: + - Artikel + - Hoeveelheid + - Prijs + - Gr.Eenh. + - Eenheid + - Som + sum: Som + title: ! 'Huishoudenslijst van bestelling: %{name}, gesloten op %{date}' order_fax: - filename: + filename: Bestelling %{name}-%{date} - Fax rows: + total: Totaal order_matrix: - filename: - heading: - rows: - title: - total: + filename: Bestelling %{name}-%{date} - Sorteermatrix + heading: Artikeloverzicht + 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: - general: - general_again: - general_msg: + format: ! '%{attribute} %{message}' + general: Er is een probleem opgetreden. + general_again: Er is een fout opgetreden. Probeer het opnieuw. + general_msg: ! 'Er is een probleem opgetreden: %{msg}' messages: - accepted: - blank: - confirmation: - empty: - equal_to: - even: - exclusion: - greater_than: - greater_than_or_equal_to: - inclusion: - invalid: - less_than: - less_than_or_equal_to: - not_a_number: - not_an_integer: - odd: + accepted: moet geaccepteerd worden + blank: moet ingevuld worden + confirmation: komt niet overeen met de bevestiging + empty: moet ingevuld worden + equal_to: moet precies %{count} zijn + even: moet even zijn + exclusion: moet even zijn + greater_than: moet groter dan %{count} zijn + 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 + not_an_integer: moet een geheel getal zijn + odd: moet oneven zijn record_invalid: - taken: - taken_with_deleted: - too_long: - too_short: - wrong_length: + taken: is al in gebruik + taken_with_deleted: is al in gebruik (verwijderde groep) + 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: @@ -542,7 +592,7 @@ nl: total_fc: Som (FC-prijs) units: Eenheden index: - title: Gesloten orders + title: Gesloten bestellingen invoice: edit: Factuur bewerken invoice_amount: ! 'Factuurbedrag:' @@ -573,8 +623,8 @@ nl: orders: clear: afrekenen cleared: afgerekend (%{amount}) - close: direct afsluiten - confirm: Weet je zeker dat de je bestelling wilt afsluiten? + close: direct afrekenen + confirm: Weet je zeker dat de je bestelling wilt afrekenen? end: Einde ended: gesloten last_edited_by: Laatst aangepast door @@ -673,7 +723,8 @@ nl: title: Tegoeden beheren ordergroups: account_balance: Tegoed - account_statement: + account_statement: Rekeningafschrift + contact: name: Naam new_transaction: Nieuwe transactie update: @@ -681,31 +732,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.

+ title: Werkgroepen workgroup: - edit: + edit: Groep bewerken show_tasks: group_orders: archive: @@ -755,9 +806,9 @@ nl: title: Afgerekende bestellingen finished_orders: title: Niet afgerekende bestellingen - total_sum: + total_sum: Totaal funds: - account_balance: + account_balance: Tegoed available_funds: finished_orders: niet afgerekende bestellingen open_orders: Lopende bestellingen @@ -853,12 +904,12 @@ nl: last_update: title: transactions: - amount: - note: - title: - view: - when: - where: + amount: Bedrag + note: Notitie + title: Laatste transacties + view: Rekeningafschrift tonen + when: Wanneer + where: Wie ordergroup: title: tasks_move: @@ -894,7 +945,7 @@ nl: start_nav: admin: finances: - accounts: + accounts: Tegoeden bijwerken settle: title: Financiën foodcoop: Foodcoop @@ -1039,6 +1090,8 @@ nl: subject: title: model: + delivery: + each_stock_article_must_be_unique: membership: no_admin_delete: order_article: @@ -1047,14 +1100,6 @@ nl: redirect: user: no_ordergroup: - notify: - email_is_public: - name_is_public: - negative_balance: - order_finished: Informeer me over mijn bestelresultaat (wanneer de bestelling gesloten wordt). - phone_is_public: - send_as_email: - upcoming_tasks: navigation: admin: home: Overzicht @@ -1092,7 +1137,7 @@ nl: number: currency: format: - delimiter: ! ',' + delimiter: ! ' ' format: ! '%n %u' precision: 2 separator: ! ',' @@ -1400,6 +1445,10 @@ nl: units_to_order: update_current_price: stock_article: + copy_stock_article: + name: + edit_stock_article: + price: supplier: supplier: min_order_quantity: @@ -1425,7 +1474,7 @@ nl: description: Omschrijving email: Email note: Notitie - order_number: Order nummer + order_number: Bestelnummer ordergroup: Huishouden password: Wachtwoord password_confirmation: Wachtwoord herhalen @@ -1450,7 +1499,7 @@ nl: note: Notitie number: Nummer order: Bestelling - paid_on: Betaalt op + paid_on: Betaald op supplier: Leverancier message: body: @@ -1472,9 +1521,25 @@ nl: contact_person: Contactpersoon contact_phone: Telefoon ignore_apple_restriction: + name: page: body: parent_id: + settings: + messages: + send_as_email: Berichten als emails ontvangen. + notify: + negative_balance: Informeer me wanneer mijn huishouden een negatief saldo krijgt. + order_finished: Informeer me over mijn bestelresultaat (wanneer de bestelling gesloten wordt). + upcoming_tasks: Herinner me aan aankomende taken. + profile: + email_is_public: E-mail is zichtbaar voor andere leden. + language: Taal + name_is_public: Naam is zichtbaar voor andere leden. + phone_is_public: Telefoonnummer is zichtbaar voor andere leden. + settings_group: + messages: Berichten + privacy: Privacy stock_article: supplier: supplier: @@ -1513,17 +1578,19 @@ nl: workgroup: next_weekly_tasks_number: role_admin: - role_article_meta: - role_finance: + role_article_meta: Artikelbestand + role_finance: Financiën role_orders: - role_suppliers: - task_description: - task_duration: - task_name: - task_required_users: - weekday: - weekly_task: + role_suppliers: Leveranciers 'no': Nee + options: + settings: + profile: + language: + de: Duits + en: Engels + fr: Frans + nl: Nederlands required: mark: ! '*' text: verplicht @@ -1568,6 +1635,15 @@ nl: title: form: price_hint: + history: + change_quantity: + datetime: + delivery: + new_quantity: + order: + reason: + stock_changes: + stock_taking: index: article: article: @@ -1587,6 +1663,7 @@ nl: show_stock_takings: stock_count: stock_worth: + title: toggle_unavailable: view_options: new: @@ -1617,6 +1694,7 @@ nl: shared_suppliers: body: subscribe: + subscribe_again: supplier: title: show: @@ -1647,12 +1725,15 @@ nl: notice: edit: title: + warning_periodic: error_not_found: form: search: hint: noresult: placeholder: + submit: + periodic: index: show_group_tasks: title: @@ -1674,12 +1755,16 @@ nl: group_tasks: my_tasks: new_task: + pages: new: title: + repeated: set_done: notice: show: accept_task: + confirm_delete_group: + delete_group: due_date: hours: mark_done: @@ -1687,6 +1772,7 @@ nl: title: update: notice: + notice_converted: user: more: tasks_link: @@ -1696,11 +1782,6 @@ nl: workgroup: title: title_all: - weekly: - desc: - edit: - empty: - title: time: am: morgen formats: @@ -1712,9 +1793,12 @@ nl: close: Sluiten delete: Verwijder edit: Bewerk + history: marks: close: ! '×' + success: or_cancel: of annuleren + please_wait: Een moment alstublieft... save: Opslaan show: Tonen views: diff --git a/config/routes.rb b/config/routes.rb index cbcf2828..0f96d7fa 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -8,6 +8,7 @@ Foodsoft::Application.routes.draw do root :to => redirect("/#{FoodsoftConfig.scope}") + scope '/:foodcoop' do # Root path @@ -96,14 +97,23 @@ Foodsoft::Application.routes.draw do get :articles_search get :fill_new_stock_article_form end + + get :history end resources :suppliers do get :shared_suppliers, :on => :collection resources :deliveries do - post :drop_stock_change, :on => :member - post :add_stock_article, :on => :collection + post :add_stock_change, :on => :collection + + get :new_stock_article, :on => :collection + get :copy_stock_article, :on => :collection + get :derive_stock_article, :on => :collection + post :create_stock_article, :on => :collection + + get :edit_stock_article, :on => :collection + put :update_stock_article, :on => :collection end resources :articles do @@ -178,7 +188,7 @@ Foodsoft::Application.routes.draw do ############## Feedback resource :feedback, :only => [:new, :create], :controller => 'feedback' - + ############## The rest resources :users, :only => [:index] diff --git a/config/schedule.rb b/config/schedule.rb index 631e3d28..26e2d1f2 100644 --- a/config/schedule.rb +++ b/config/schedule.rb @@ -3,13 +3,11 @@ # Upcoming tasks notifier every :day, :at => '7:20 am' do - rake "multicoops:run foodsoft:notify_upcoming_tasks" + rake "multicoops:run TASK=foodsoft:notify_upcoming_tasks" end # Weekly taks every :sunday, :at => '7:14 am' do - rake "multicoops:run foodsoft:create_upcoming_weekly_tasks" - rake "multicoops:run foodsoft:notify_users_of_weekly_task" -end - - + rake "multicoops:run TASK=foodsoft:create_upcoming_periodic_tasks" + rake "multicoops:run TASK=foodsoft:notify_users_of_weekly_task" +end \ No newline at end of file diff --git a/db/migrate/20130615073715_create_periodic_task_groups.rb b/db/migrate/20130615073715_create_periodic_task_groups.rb new file mode 100644 index 00000000..87cf560b --- /dev/null +++ b/db/migrate/20130615073715_create_periodic_task_groups.rb @@ -0,0 +1,13 @@ +class CreatePeriodicTaskGroups < ActiveRecord::Migration + def change + create_table :periodic_task_groups do |t| + t.date :next_task_date + + t.timestamps + end + + change_table :tasks do |t| + t.references :periodic_task_group + end + end +end diff --git a/db/migrate/20130622095040_move_weekly_tasks.rb b/db/migrate/20130622095040_move_weekly_tasks.rb new file mode 100644 index 00000000..e8dc8384 --- /dev/null +++ b/db/migrate/20130622095040_move_weekly_tasks.rb @@ -0,0 +1,62 @@ +class MoveWeeklyTasks < ActiveRecord::Migration + def up + Workgroup.where(weekly_task: true).each do |workgroup| + task_group = PeriodicTaskGroup.create + puts "Moving weekly task for workgroup #{workgroup.name} to group #{task_group.id}" + workgroup.tasks.undone.each do |task| + task.update_column(:periodic_task_group_id, task_group.id) if weekly_task?(workgroup, task) + end + tasks = task_group.tasks.order(:due_date) + task_group.next_task_date = tasks.last.due_date + PeriodicTaskGroup::PeriodDays unless tasks.empty? + task_group.save! + puts "Associated #{tasks.count} tasks with group and set next_task_date to #{task_group.next_task_date}" + end + end + + 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_attributes workgroup_attributes + task_group.tasks.update_all weekly: true + end + end + end + +private + def weekly_task?(workgroup, task) + return false if task.due_date.nil? + + group_task = { + weekday: workgroup.weekday, + name: workgroup.task_name, + description: workgroup.task_description, + required_users: workgroup.task_required_users, + duration: workgroup.task_duration, + weekly: true, + done: false, + workgroup_id: workgroup.id + } + task_task = { + weekday: task.due_date.days_to_week_start(:sunday), + name: task.name, + description: task.description, + required_users: task.required_users, + duration: task.duration, + weekly: task.weekly, + done: task.done, + workgroup_id: task.workgroup_id + } + group_task == task_task + end +end diff --git a/db/migrate/20130624084223_remove_weekly_from_tasks.rb b/db/migrate/20130624084223_remove_weekly_from_tasks.rb new file mode 100644 index 00000000..4fefcb5b --- /dev/null +++ b/db/migrate/20130624084223_remove_weekly_from_tasks.rb @@ -0,0 +1,9 @@ +class RemoveWeeklyFromTasks < ActiveRecord::Migration + def up + remove_column :tasks, :weekly + end + + def down + add_column :tasks, :weekly, :boolean + end +end diff --git a/db/migrate/20130624085246_remove_weekly_task_from_groups.rb b/db/migrate/20130624085246_remove_weekly_task_from_groups.rb new file mode 100644 index 00000000..665651d9 --- /dev/null +++ b/db/migrate/20130624085246_remove_weekly_task_from_groups.rb @@ -0,0 +1,19 @@ +class RemoveWeeklyTaskFromGroups < ActiveRecord::Migration + def up + remove_column :groups, :weekly_task + remove_column :groups, :weekday + remove_column :groups, :task_name + remove_column :groups, :task_description + remove_column :groups, :task_required_users + remove_column :groups, :task_duration + end + + def down + add_column :groups, :task_duration, :integer + add_column :groups, :task_required_users, :integer + add_column :groups, :task_description, :string + add_column :groups, :task_name, :string + add_column :groups, :weekday, :integer + add_column :groups, :weekly_task, :boolean + end +end diff --git a/db/migrate/20130718183100_create_settings.rb b/db/migrate/20130718183100_create_settings.rb new file mode 100644 index 00000000..90a0f527 --- /dev/null +++ b/db/migrate/20130718183100_create_settings.rb @@ -0,0 +1,17 @@ +class CreateSettings < ActiveRecord::Migration + def self.up + create_table :settings do |t| + t.string :var, null: false + t.text :value, null: true + t.integer :thing_id, null: true + t.string :thing_type, limit: 30, null: true + t.timestamps + end + + add_index :settings, [ :thing_type, :thing_id, :var ], unique: true + end + + def self.down + drop_table :settings + end +end diff --git a/db/migrate/20130718183101_migrate_user_settings.rb b/db/migrate/20130718183101_migrate_user_settings.rb new file mode 100644 index 00000000..73f30acf --- /dev/null +++ b/db/migrate/20130718183101_migrate_user_settings.rb @@ -0,0 +1,55 @@ +class MigrateUserSettings < ActiveRecord::Migration + def up + say_with_time 'Save old user settings in new RailsSettings module' do + + # Allow setting default locale via env parameter + # This is used, when setting users language settings + default_locale = I18n.default_locale + tmp_locale = ENV['DEFAULT_LOCALE'].present? ? ENV['DEFAULT_LOCALE'].to_sym : default_locale + I18n.default_locale = tmp_locale + + old_settings = ConfigurableSetting.all + + old_settings.each do |old_setting| + # get target (user) + type = old_setting.configurable_type + id = old_setting.configurable_id + begin + user = type.constantize.find(id) + rescue ActiveRecord::RecordNotFound + Rails.logger.debug "Can't find configurable object with type: #{type.inspect}, id: #{id.inspect}" + next + end + + # get the data (settings) + name = old_setting.name + namespace = name.split('.')[0] + key = name.split('.')[1].underscore # Camelcase to underscore + + # prepare value + value = YAML.load(old_setting.value) + value = value.nil? ? false : value + + # set the settings_attributes (thanks to settings.merge! we can set them one by one) + user.settings_attributes = { + "#{namespace}" => { + "#{key}" => value + } + } + + # save the user to apply after_save callback + user.save + end + + I18n.default_locale = default_locale + end + + drop_table :configurable_settings + end + + def down + end +end + +# this is the base class of all configurable settings +class ConfigurableSetting < ActiveRecord::Base; end \ No newline at end of file diff --git a/db/schema.rb b/db/schema.rb index 59f35c66..0d5b1326 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended to check this file into your version control system. -ActiveRecord::Schema.define(:version => 20130715233410) do +ActiveRecord::Schema.define(:version => 20130718183101) do create_table "article_categories", :force => true do |t| t.string "name", :default => "", :null => false @@ -66,18 +66,6 @@ ActiveRecord::Schema.define(:version => 20130715233410) do add_index "assignments", ["user_id", "task_id"], :name => "index_assignments_on_user_id_and_task_id", :unique => true - create_table "configurable_settings", :force => true do |t| - t.integer "configurable_id" - t.string "configurable_type" - t.integer "targetable_id" - t.string "targetable_type" - t.string "name", :default => "", :null => false - t.string "value_type" - t.text "value" - end - - add_index "configurable_settings", ["name"], :name => "index_configurable_settings_on_name" - create_table "deliveries", :force => true do |t| t.integer "supplier_id" t.date "delivered_on" @@ -143,17 +131,11 @@ ActiveRecord::Schema.define(:version => 20130715233410) do 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.boolean "weekly_task", :default => false - t.integer "weekday" - t.string "task_name" - t.string "task_description" - t.integer "task_required_users", :default => 1 t.datetime "deleted_at" t.string "contact_person" t.string "contact_phone" t.string "contact_address" t.text "stats" - t.integer "task_duration", :default => 1 t.integer "next_weekly_tasks_number", :default => 8 t.boolean "ignore_apple_restriction", :default => false end @@ -268,6 +250,23 @@ ActiveRecord::Schema.define(:version => 20130715233410) do add_index "pages", ["permalink"], :name => "index_pages_on_permalink" add_index "pages", ["title"], :name => "index_pages_on_title" + create_table "periodic_task_groups", :force => true do |t| + t.date "next_task_date" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + end + + create_table "settings", :force => true 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 + end + + add_index "settings", ["thing_type", "thing_id", "var"], :name => "index_settings_on_thing_type_and_thing_id_and_var", :unique => true + create_table "stock_changes", :force => true do |t| t.integer "delivery_id" t.integer "order_id" @@ -308,16 +307,16 @@ ActiveRecord::Schema.define(:version => 20130715233410) do add_index "suppliers", ["name"], :name => "index_suppliers_on_name", :unique => true create_table "tasks", :force => true do |t| - t.string "name", :default => "", :null => false + t.string "name", :default => "", :null => false t.string "description" t.date "due_date" - t.boolean "done", :default => false + t.boolean "done", :default => false t.integer "workgroup_id" - t.datetime "created_on", :null => false - t.datetime "updated_on", :null => false - t.integer "required_users", :default => 1 - t.boolean "weekly" - t.integer "duration", :default => 1 + t.datetime "created_on", :null => false + t.datetime "updated_on", :null => false + t.integer "required_users", :default => 1 + t.integer "duration", :default => 1 + t.integer "periodic_task_group_id" end add_index "tasks", ["due_date"], :name => "index_tasks_on_due_date" diff --git a/lib/foodsoft/controller_extensions.rb b/lib/foodsoft/controller_extensions.rb new file mode 100644 index 00000000..e0b64aa6 --- /dev/null +++ b/lib/foodsoft/controller_extensions.rb @@ -0,0 +1,55 @@ +# -*- encoding : utf-8 -*- +module Foodsoft + module ControllerExtensions + module Locale + extend ActiveSupport::Concern + + included do + before_filter :set_locale + end + + def explicitly_requested_language + params[:locale] + end + + def user_settings_language + current_user.locale if current_user + end + + def session_language + session[:locale] + end + + def browser_language + request.env['HTTP_ACCEPT_LANGUAGE'] ? request.env['HTTP_ACCEPT_LANGUAGE'].scan(/^[a-z]{2}/).first : nil + end + + def default_language + ::I18n.default_locale + end + + protected + + def select_language_according_to_priority + language = explicitly_requested_language || session_language || user_settings_language || browser_language + language.to_sym unless language.blank? + end + + def available_locales + ::I18n.available_locales + 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 + + locale = session[:locale] = ::I18n.locale + logger.info("Set locale to #{locale}") + end + + end + end +end \ No newline at end of file diff --git a/lib/foodsoft_file.rb b/lib/foodsoft_file.rb index 1aa40dfb..0ef6367c 100644 --- a/lib/foodsoft_file.rb +++ b/lib/foodsoft_file.rb @@ -11,7 +11,7 @@ module FoodsoftFile def self.parse(file) articles, outlisted_articles = Array.new, Array.new row_index = 2 - ::CSV.parse(file.read, {:col_sep => ";", :headers => true}) do |row| + ::CSV.parse(file.read.force_encoding('utf-8'), {:col_sep => ";", :headers => true}) do |row| # check if the line is empty unless row[2] == "" || row[2].nil? article = {:number => row[1], diff --git a/lib/tasks/foodsoft.rake b/lib/tasks/foodsoft.rake index fd96cf9c..04db66f9 100644 --- a/lib/tasks/foodsoft.rake +++ b/lib/tasks/foodsoft.rake @@ -8,7 +8,7 @@ namespace :foodsoft do puts "Send notifications for #{task.name} to .." for user in task.users begin - Mailer.upcoming_tasks(user, task).deliver if user.settings['notify.upcoming_tasks'] == 1 + Mailer.upcoming_tasks(user, task).deliver if user.settings.notify['upcoming_tasks'] == 1 rescue puts "deliver aborted for #{user.email}.." end @@ -16,21 +16,6 @@ namespace :foodsoft do end end - desc "Create upcoming workgroups tasks (next 3 to 7 weeks)" - task :create_upcoming_weekly_tasks => :environment do - workgroups = Workgroup.where(weekly_task: true) - for workgroup in workgroups - puts "Create weekly tasks for #{workgroup.name}" - # Loop through next tasks weekly tasks method, - # skip the next 3 weeks, to allow manually deleting tasks - workgroup.next_weekly_tasks[3..-1].each do |date| - unless workgroup.tasks.exists?({:due_date => date, :weekly => true}) - workgroup.tasks.create(workgroup.task_attributes(date)) - end - end - end - end - desc "Notify workgroup of upcoming weekly task" task :notify_users_of_weekly_task => :environment do for workgroup in Workgroup.all @@ -38,7 +23,7 @@ namespace :foodsoft do unless task.enough_users_assigned? puts "Notify workgroup: #{workgroup.name} for task #{task.name}" for user in workgroup.users - if user.settings['messages.sendAsEmail'] == "1" && !user.email.blank? + if user.settings.messages['send_as_email'] == "1" && !user.email.blank? begin Mailer.not_enough_users_assigned(task, user).deliver rescue @@ -50,4 +35,15 @@ namespace :foodsoft do end end end + + desc "Create upcoming periodic tasks" + task :create_upcoming_periodic_tasks => :environment do + for tg in PeriodicTaskGroup.all + if tg.has_next_task? + while tg.next_task_date.nil? or tg.next_task_date < Date.today + 50 + tg.create_next_task + end + end + end + end end diff --git a/lib/tasks/foodsoft_setup.rake b/lib/tasks/foodsoft_setup.rake index 6a239c63..5e7ba088 100644 --- a/lib/tasks/foodsoft_setup.rake +++ b/lib/tasks/foodsoft_setup.rake @@ -38,6 +38,15 @@ namespace :foodsoft do puts yellow "All done! Your foodcoft should be running smoothly." start_server end + + namespace :setup do + desc "Initialize stock configuration" + task :stock_config do + setup_app_config + setup_development + setup_secret_token + end + end end def setup_bundler diff --git a/lib/tasks/multicoops.rake b/lib/tasks/multicoops.rake index ccd16f6f..621a0d54 100644 --- a/lib/tasks/multicoops.rake +++ b/lib/tasks/multicoops.rake @@ -1,17 +1,21 @@ +# This namespace is used for a collection of tasks to maintain a hosting environment with multiple foodcoops +# This tasks are a kind of wrapper for other tasks. The wrapper makes sure, that the appropriate database and config +# for each foodcoop is used. + namespace :multicoops do - desc 'Runs a specific rake task for each registered foodcoop, use rake multicoops:run db:migrate' + desc 'Runs a specific rake task for each registered foodcoop, use rake multicoops:run TASK=db:migrate' task :run => :environment do - task_to_run = ARGV[1] + task_to_run = ENV['TASK'] FoodsoftConfig.each_coop do |coop| puts "Run '#{task_to_run}' for #{coop}" Rake::Task[task_to_run].execute end end - desc 'Runs a specific rake task for a single coop, use rake mutlicoops:run_single db:migrate FOODCOOP=demo' + 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_to_run = ARGV[1] + task_to_run = ENV['TASK'] FoodsoftConfig.select_foodcoop ENV['FOODCOOP'] puts "Run '#{task_to_run}' for #{ENV['FOODCOOP']}" Rake::Task[task_to_run].execute diff --git a/lib/tasks/rspec.rake b/lib/tasks/rspec.rake new file mode 100644 index 00000000..45a78604 --- /dev/null +++ b/lib/tasks/rspec.rake @@ -0,0 +1,3 @@ +require 'rspec/core/rake_task' +RSpec::Core::RakeTask.new(:spec) +task :default => :spec diff --git a/script/heroku_deploy b/script/heroku_deploy index ca5ad9d3..df7da72d 100755 --- a/script/heroku_deploy +++ b/script/heroku_deploy @@ -55,12 +55,16 @@ fi sed -i "s|^\\(\\s*gem\\s\\+'sqlite3'\\)|#\1|" Gemfile sed -i "s|^\\(\\s*sqlite3\\b\)|#\1|" Gemfile.lock # make sure postgresql db is present, as it is the default heroku db -echo "\ngem 'pg'" >>Gemfile -echo "\ngem 'localeapp'" >>Gemfile +echo " +gem 'pg'" >>Gemfile # always use unicorn -echo "\ngem 'unicorn'" >>Gemfile +echo " +gem 'unicorn'" >>Gemfile echo 'web: bundle exec unicorn -p $PORT -E $RACK_ENV' >Procfile -bundle install --quiet # to update Gemfile.lock +# don't complain when mail cannot be sent, +# XXX when you're hosting a production instance, use a real smtp server instead +sed -i 's|\(#\s*\)\?\(config\.action_mailer\.raise_delivery_errors\)\s*=.*|\2 = false|' config/environments/${RAILS_ENV}.rb +sed -i 's|\(#\s*\)\?\(config\.action_mailer\.delivery_method\)\s*=.*|\2 = :smtp|' config/environments/${RAILS_ENV}.rb # do not ignore deployment files sed -i 's|^\(config/\*.yml\)|#\1|' .gitignore sed -i 's|^\(config/initializers/secret_token.rb\)|#\1|' .gitignore @@ -92,9 +96,13 @@ Localeapp.configure do |config| config.polling_environments = ['$RAILS_ENV'] end EOF + echo " +gem 'localeapp'" >>Gemfile # also do not cache so we get locale updates - sed -i 's|config\.cache_classes\s*=.*|config.cache_classes = false|' config/environments/${RAILS_ENV}.rb + sed -i 's|\(#\s*\)\?\(config\.cache_classes\)\s*=.*|\2 = false|' config/environments/${RAILS_ENV}.rb fi +# update Gemfile.lock after Gemfile updates (required by heroku) +bundle install --quiet # TODO add more extensive database seed # and push = deploy diff --git a/spec/factories/article.rb b/spec/factories/article.rb new file mode 100644 index 00000000..d79535fc --- /dev/null +++ b/spec/factories/article.rb @@ -0,0 +1,20 @@ +require 'factory_girl' + +FactoryGirl.define do + + factory :article do + sequence(:name) { |n| Faker::Lorem.words(rand(2..4)).join(' ') + " ##{n}" } + unit { Faker::Unit.unit } + price { rand(2600) / 100 } + tax { [6, 21].sample } + deposit { rand(10) < 8 ? 0 : [0.0, 0.80, 1.20, 12.00].sample } + unit_quantity { rand(5) < 3 ? 1 : rand(1..20) } + #supplier_id + article_category { create :article_category } + end + + factory :article_category do + sequence(:name) { |n| Faker::Lorem.characters(rand(2..12)) + " ##{n}" } + end + +end diff --git a/spec/factories/group_order.rb b/spec/factories/group_order.rb new file mode 100644 index 00000000..1a665ca4 --- /dev/null +++ b/spec/factories/group_order.rb @@ -0,0 +1,10 @@ +require 'factory_girl' + +FactoryGirl.define do + + # requires order + factory :group_order do + ordergroup { create(:user, groups: [FactoryGirl.create(:ordergroup)]).ordergroup } + end + +end diff --git a/spec/factories/group_order_article.rb b/spec/factories/group_order_article.rb new file mode 100644 index 00000000..8b2982d8 --- /dev/null +++ b/spec/factories/group_order_article.rb @@ -0,0 +1,9 @@ +require 'factory_girl' + +FactoryGirl.define do + + # requires order_article + factory :group_order_article do + end + +end diff --git a/spec/factories/order.rb b/spec/factories/order.rb new file mode 100644 index 00000000..4bf63b00 --- /dev/null +++ b/spec/factories/order.rb @@ -0,0 +1,31 @@ +require 'factory_girl' + +FactoryGirl.define do + + factory :order do + starts { Time.now } + supplier { create :supplier, article_count: (article_count.nil? ? true : article_count) } + article_ids { supplier.articles.map(&:id) unless supplier.nil? } + + ignore do + article_count true + end + + # for an order from stock; need to add articles + factory :stock_order do + supplier_id 0 + # article_ids needs to be supplied + end + + # In the order's after_save callback order articles are created, so + # until the order is saved, these articles do not yet exist. + after :create do |order| + order.reload + end + end + + # requires order and article + factory :order_article do + end + +end diff --git a/spec/factories/supplier.rb b/spec/factories/supplier.rb new file mode 100644 index 00000000..4f67d91c --- /dev/null +++ b/spec/factories/supplier.rb @@ -0,0 +1,21 @@ +require 'factory_girl' + +FactoryGirl.define do + + factory :supplier do + name { Faker::Company.name.truncate(30) } + phone { Faker::PhoneNumber.phone_number } + address { Faker::Address.street_address } + + ignore do + article_count 0 + end + + 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 + end + end + +end diff --git a/spec/factories/user.rb b/spec/factories/user.rb new file mode 100644 index 00000000..f70c48c8 --- /dev/null +++ b/spec/factories/user.rb @@ -0,0 +1,36 @@ +require 'factory_girl' + +FactoryGirl.define do + + factory :user do + sequence(:nick) { |n| "user#{n}"} + first_name { Faker::Name.first_name } + email { Faker::Internet.email } + password { new_random_password } + + 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] + end + end + end + + factory :group do + sequence(:name) {|n| "Group ##{n}"} + + factory :workgroup do + type '' + end + + factory :ordergroup do + type 'Ordergroup' + sequence(:name) {|n| "Order group ##{n}"} + # workaround to avoid needing to save the ordergroup + # avoids e.g. error after logging in related to applebar + after :create do |group| Ordergroup.find(group.id).update_stats! end + end + end + +end diff --git a/spec/i18n_spec.rb b/spec/i18n_spec.rb new file mode 100644 index 00000000..e07d74b9 --- /dev/null +++ b/spec/i18n_spec.rb @@ -0,0 +1,12 @@ +require 'spec_helper' +require 'i18n-spec' + +Dir.glob('config/locales/*.yml').each do |locale_file| + describe "#{locale_file}" do + it_behaves_like 'a valid locale file', locale_file + # We're currently allowing both German and English as source language + # besides, we're using localeapp, so that it's ok if pull requests + # don't have this - a localapp pull will fix that right away. + #it { expect(locale_file).to be_a_subset_of 'config/locales/en.yml' } + end +end diff --git a/spec/integration/balancing_spec.rb b/spec/integration/balancing_spec.rb new file mode 100644 index 00000000..2bf860be --- /dev/null +++ b/spec/integration/balancing_spec.rb @@ -0,0 +1,108 @@ +require_relative '../spec_helper' + +describe 'settling an order', :type => :feature do + let(:admin) { create :user, groups:[create(:workgroup, role_finance: true)] } + 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 } + before do + goa1.update_quantities(3, 0) + goa2.update_quantities(1, 0) + oa.update_results! + order.finish!(admin) + goa1.reload + goa2.reload + end + + it 'has correct order result' do + expect(oa.quantity).to eq(4) + expect(oa.tolerance).to eq(0) + expect(goa1.result).to eq(3) + expect(goa2.result).to eq(1) + end + + describe :type => :feature, :js => true do + 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}") + end + + it 'shows order result' do + click_link article.name + expect(page).to have_selector("#group_order_articles_#{oa.id}") + within("#group_order_articles_#{oa.id}") do + # make sure these ordergroup names are in the list for this product + expect(page).to have_content(go1.ordergroup.name) + expect(page).to have_content(go2.ordergroup.name) + # and that their order results match what we expect + expect(page).to have_selector("#group_order_article_#{goa1.id}_quantity") + expect(find("#group_order_article_#{goa1.id}_quantity").text.to_f).to eq(3) + expect(page).to have_selector("#group_order_article_#{goa2.id}_quantity") + expect(find("#group_order_article_#{goa2.id}_quantity").text.to_f).to eq(1) + end + end + + it 'keeps ordered quantities when article is deleted from resulting order' do + within("#order_article_#{oa.id}") do + click_link I18n.t('ui.delete') + page.driver.browser.switch_to.alert.accept + end + expect(page).to_not have_selector("#order_article_#{oa.id}") + expect(OrderArticle.exists?(oa.id)).to be_true + oa.reload + expect(oa.quantity).to eq(4) + expect(oa.tolerance).to eq(0) + expect(oa.units_to_order).to eq(0) + expect(goa1.reload.result).to eq(0) + expect(goa2.reload.result).to eq(0) + end + + it 'deletes an OrderArticle with no GroupOrderArticles' do + goa1.destroy + goa2.destroy + within("#order_article_#{oa.id}") do + click_link I18n.t('ui.delete') + page.driver.browser.switch_to.alert.accept + end + expect(page).to_not have_selector("#order_article_#{oa.id}") + expect(OrderArticle.exists?(oa.id)).to be_false + end + + it 'keeps ordered quantities when GroupOrderArticle is deleted from resulting order' do + click_link article.name + expect(page).to have_selector("#group_order_article_#{goa1.id}") + 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(OrderArticle.exists?(oa.id)).to be_true + expect(GroupOrderArticle.exists?(goa1.id)).to be_true + goa1.reload + expect(goa1.result).to eq(0) + expect(goa1.quantity).to eq(3) + expect(goa1.tolerance).to eq(0) + end + + it 'deletes a GroupOrderArticle with no ordered amounts' do + goa1.update_attributes({:quantity => 0, :tolerance => 0}) + click_link article.name + expect(page).to have_selector("#group_order_article_#{goa1.id}") + 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(OrderArticle.exists?(oa.id)).to be_true + expect(GroupOrderArticle.exists?(goa1.id)).to be_false + end + + end + +end diff --git a/spec/integration/product_distribution_example_spec.rb b/spec/integration/product_distribution_example_spec.rb new file mode 100644 index 00000000..2d8cc229 --- /dev/null +++ b/spec/integration/product_distribution_example_spec.rb @@ -0,0 +1,57 @@ +require_relative '../spec_helper' + +describe 'product distribution', :type => :feature do + 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 } + + describe :type => :feature do + # make sure users have enough money to order + before do + [user_a, user_b].each do |user| + ordergroup = Ordergroup.find(user.ordergroup.id) + ordergroup.add_financial_transaction! 5000, 'for ordering', admin + end + end + + it 'agrees to documented example', :js => true do + # gruppe a bestellt 2(3), weil sie auf jeden fall was von x bekommen will + login user_a + visit new_group_order_path(:order_id => order.id) + 2.times { find("[data-increase_quantity='#{oa.id}']").click } + 3.times { find("[data-increase_tolerance='#{oa.id}']").click } + find('input[type=submit]').click + expect(page).to have_selector('body') + # gruppe b bestellt 2(0) + login user_b + visit new_group_order_path(:order_id => order.id) + 2.times { find("[data-increase_quantity='#{oa.id}']").click } + find('input[type=submit]').click + expect(page).to have_selector('body') + # gruppe a faellt ein dass sie doch noch mehr braucht von x und aendert auf 4(1). + login user_a + visit edit_group_order_path(order.group_order(user_a.ordergroup), :order_id => order.id) + 2.times { find("[data-increase_quantity='#{oa.id}']").click } + 2.times { find("[data-decrease_tolerance='#{oa.id}']").click } + find('input[type=submit]').click + expect(page).to have_selector('body') + # die zuteilung + order.finish!(admin) + oa.reload + # Endstand: insg. Bestellt wurden 6(1) + 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 + 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 + expect(goa_b.result).to eq(2) + end + end + +end diff --git a/spec/integration/session_spec.rb b/spec/integration/session_spec.rb new file mode 100644 index 00000000..d6942e94 --- /dev/null +++ b/spec/integration/session_spec.rb @@ -0,0 +1,21 @@ +require_relative '../spec_helper' + +describe 'the session', :type => :feature do + let(:user) { create :user } + + describe 'login page', :type => :feature do + it 'is accesible' do + get login_path + expect(response).to be_success + end + it 'logs me in' do + login user + expect(page).to_not have_selector('.alert-error') + end + it 'does not log me in with wrong password' do + login user.nick, 'XX'+user.password + expect(page).to have_selector('.alert-error') + end + end + +end diff --git a/spec/integration/supplier_spec.rb b/spec/integration/supplier_spec.rb new file mode 100644 index 00000000..e50a8e25 --- /dev/null +++ b/spec/integration/supplier_spec.rb @@ -0,0 +1,63 @@ +require_relative '../spec_helper' + +describe 'supplier', :type => :feature do + let(:supplier) { create :supplier } + + describe :type => :feature, :js => true do + let(:user) { create :user, groups:[create(:workgroup, role_suppliers: true)] } + before { login user } + + it 'can be created' do + visit suppliers_path + click_on I18n.t('suppliers.index.action_new') + 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 + find('input[type="submit"]').click + end + expect(page).to have_content(supplier.name) + end + + it 'is included in supplier list' do + supplier + visit suppliers_path + expect(page).to have_content(supplier.name) + end + end + + describe :type => :feature, :js => true do + let(:article_category) { create :article_category } + let(:user) { create :user, groups:[create(:workgroup, role_article_meta: true)] } + before { login user } + + it 'can visit supplier articles path' do + visit supplier_articles_path(supplier) + expect(page).to have_content(supplier.name) + expect(page).to have_content(I18n.t('articles.index.edit_all')) + end + + it 'can create a new article' do + article_category.save! + visit supplier_articles_path(supplier) + click_on I18n.t('articles.index.new') + expect(page).to have_selector('form#new_article') + article = FactoryGirl.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 + # "Element cannot be scrolled into view" error, js as workaround + #find('input[type="submit"]').click + page.execute_script('$("form#new_article").submit();') + end + expect(page).to have_content(article.name) + end + end + +end diff --git a/spec/models/article_spec.rb b/spec/models/article_spec.rb new file mode 100644 index 00000000..056b8dd6 --- /dev/null +++ b/spec/models/article_spec.rb @@ -0,0 +1,46 @@ +require_relative '../spec_helper' + +describe Article do + let(:supplier) { create :supplier } + let(:article) { create :article, supplier: supplier } + + it 'has a unique name' do + article2 = FactoryGirl.build :article, supplier: supplier, name: article.name + expect(article2).to be_invalid + end + + it 'computes the gross price correctly' do + article.deposit = 0 + 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)) + end + + it 'gross price >= net price' do + expect(article.gross_price).to be >= article.price + end + + it 'fc-price >= gross price' do + if article.gross_price > 0 + expect(article.fc_price).to be > article.gross_price + else + expect(article.fc_price).to be >= article.gross_price + end + end + + it 'knows when it is deleted' do + expect(supplier.deleted?).to be_false + supplier.mark_as_deleted + expect(supplier.deleted?).to be_true + end + + it 'keeps a price history' do + expect(article.article_prices.all.map(&:price)).to eq([article.price]) + oldprice = article.price + sleep 1 # so that the new price really has a later creation time + article.price += 1 + article.save! + expect(article.article_prices.all.map(&:price)).to eq([article.price, oldprice]) + end +end diff --git a/spec/models/group_order_article_spec.rb b/spec/models/group_order_article_spec.rb new file mode 100644 index 00000000..3c332429 --- /dev/null +++ b/spec/models/group_order_article_spec.rb @@ -0,0 +1,47 @@ +require_relative '../spec_helper' + +describe GroupOrderArticle do + 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 } + + 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 + it 'has zero result by default' do expect(goa.result).to eq(0) end + it 'is not ordered by default' do expect(GroupOrderArticle.ordered.where(:id => goa.id).exists?).to be_false end + 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 } + + it 'can be ordered by piece' do + goa.update_quantities(1, 0) + expect(goa.quantity).to eq(1) + expect(goa.tolerance).to eq(0) + end + + it 'can be ordered in larger amounts' do + quantity, tolerance = rand(13..99), rand(0..99) + goa.update_quantities(quantity, tolerance) + expect(goa.quantity).to eq(quantity) + expect(goa.tolerance).to eq(tolerance) + end + + it 'has a proper total price' do + quantity = rand(1..99) + goa.update_quantities(quantity, 0) + expect(goa.total_price).to eq(quantity * goa.order_article.price.fc_price) + end + + it 'can unorder a product' do + goa.update_quantities(rand(1..99), rand(0..99)) + goa.update_quantities(0, 0) + expect(goa.quantity).to eq(0) + expect(goa.tolerance).to eq(0) + end + end + +end diff --git a/spec/models/group_order_spec.rb b/spec/models/group_order_spec.rb new file mode 100644 index 00000000..1dab91ed --- /dev/null +++ b/spec/models/group_order_spec.rb @@ -0,0 +1,25 @@ +require_relative '../spec_helper' + +describe GroupOrder do + 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 + + #it 'needs an order' do + # expect(FactoryGirl.build(:group_order, ordergroup: user.ordergroup)).to be_invalid + #end + + #it 'needs an ordergroup' do + # expect(FactoryGirl.build(:group_order, order: order)).to be_invalid + #end + + describe do + let(:go) { create :group_order, order: order, ordergroup: user.ordergroup } + + it 'has zero price initially' do + expect(go.price).to eq(0) + end + end + +end diff --git a/spec/models/order_spec.rb b/spec/models/order_spec.rb new file mode 100644 index 00000000..81c58bd5 --- /dev/null +++ b/spec/models/order_spec.rb @@ -0,0 +1,47 @@ +require_relative '../spec_helper' + +describe Order do + + it 'needs a supplier' do + expect(build(:order, supplier: nil)).to be_invalid + end + + it 'needs order articles' do + supplier = create :supplier, article_count: 0 + expect(build(:order, supplier: supplier)).to be_invalid + end + + it 'can be created' do + expect(build(:order, article_count: 1)).to be_valid + end + + describe 'with articles' do + 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 'has valid order articles' do + order.order_articles.all.each {|oa| expect(oa).to be_valid } + end + + it 'can be finished' do + # TODO randomise user + order.finish!(User.first) + expect(order).to_not be_open + expect(order).to be_finished + expect(order).to_not be_closed + end + + it 'can be closed' do + # TODO randomise user + order.finish!(User.first) + order.close!(User.first) + expect(order).to_not be_open + expect(order).to be_closed + end + + end + +end diff --git a/spec/models/supplier_spec.rb b/spec/models/supplier_spec.rb new file mode 100644 index 00000000..b3536105 --- /dev/null +++ b/spec/models/supplier_spec.rb @@ -0,0 +1,16 @@ +require_relative '../spec_helper' + +describe Supplier do + let(:supplier) { create :supplier } + + it 'has a unique name' do + supplier2 = build :supplier, name: supplier.name + expect(supplier2).to be_invalid + end + + it 'has valid articles' do + supplier = create :supplier, article_count: true + supplier.articles.all.each {|a| expect(a).to be_valid } + end + +end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb new file mode 100644 index 00000000..bc90fa01 --- /dev/null +++ b/spec/models/user_spec.rb @@ -0,0 +1,59 @@ +require_relative '../spec_helper' + +describe User do + + it 'is correctly created' do + user = create :user, + nick: 'johnnydoe', first_name: 'Johnny', last_name: 'DoeBar', + 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') + expect(user.name).to eq('Johnny DoeBar') + expect(user.email).to eq('johnnydoe@foodcoop.test') + expect(user.phone).to eq('+1234567890') + end + + describe 'does not have the role' do + let(:user) { create :user } + it 'admin' do expect(user.role_admin?).to be_false end + it 'finance' do expect(user.role_finance?).to be_false end + it 'article_meta' do expect(user.role_article_meta?).to be_false end + it 'suppliers' do expect(user.role_suppliers?).to be_false end + it 'orders' do expect(user.role_orders?).to be_false end + end + + describe do + let(:user) { create :user, password: 'blahblah' } + + it 'can authenticate with correct password' do + expect(User.authenticate(user.nick, 'blahblah')).to be_true + end + it 'can not authenticate with incorrect password' do + expect(User.authenticate(user.nick, 'foobar')).to be_nil + end + it 'can not set a password without matching confirmation' do + user.password = 'abcdefghij' + user.password_confirmation = 'foobarxyz' + expect(user).to be_invalid + end + it 'can set a password with matching confirmation' do + user.password = 'abcdefghij' + user.password_confirmation = 'abcdefghij' + expect(user).to be_valid + end + + it 'has a unique nick' do + expect(build(:user, nick: user.nick, email: "x-#{user.email}")).to be_invalid + end + it 'has a unique email' do + expect(build(:user, email: "#{user.email}")).to be_invalid + end + end + + describe 'admin' do + let(:user) { create :admin } + it 'default admin role' do expect(user.role_admin?).to be_true end + end + +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 00000000..adbc4248 --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,67 @@ +# This file is copied to spec/ when you run 'rails generate rspec:install' +ENV["RAILS_ENV"] ||= 'test' +require_relative 'support/coverage' # needs to be first +require File.expand_path("../../config/environment", __FILE__) +require 'rspec/rails' +require 'rspec/autorun' + +require 'capybara/rails' +require 'capybara/rspec' + +# 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 } + +RSpec.configure do |config| + # ## Mock Framework + # + # If you prefer to use mocha, flexmock or RR, uncomment the appropriate line: + # + # config.mock_with :mocha + # config.mock_with :flexmock + # config.mock_with :rr + + # If you're not using ActiveRecord, or you'd prefer not to run each of your + # examples within a transaction, remove the following line or assign false + # instead of true. + #config.use_transactional_fixtures = true + # We use capybara with selenium, and need database_cleaner + config.before(:each) do + DatabaseCleaner.strategy = (example.metadata[:js] ? :truncation : :transaction) + DatabaseCleaner.start + end + config.after(:each) do + DatabaseCleaner.clean + end + + # If true, the base class of anonymous controllers will be inferred + # automatically. This will be the default behavior in future versions of + # rspec-rails. + config.infer_base_class_for_anonymous_controllers = false + + # Run specs in random order to surface order dependencies. If you find an + # 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.include SessionHelper +end + +module Faker + class Unit + class << self + def unit + ['kg', '1L', '100ml', 'piece', 'bunch', '500g'].sample + end + end + end +end + +# include default foodsoft scope in urls, so that *_path works +ActionDispatch::Integration::Runner.class_eval do + undef default_url_options + def default_url_options(options={}) + {foodcoop: FoodsoftConfig.scope}.merge(options) + end +end diff --git a/spec/support/coverage.rb b/spec/support/coverage.rb new file mode 100644 index 00000000..c67e3172 --- /dev/null +++ b/spec/support/coverage.rb @@ -0,0 +1,14 @@ +# optional test coverage +# needs to be loaded first, e.g. add a require at top of spec_helper +if ENV['COVERAGE'] + require 'simplecov' + SimpleCov.start do + add_filter '/spec/' + add_filter '/test/' + add_group 'Models', '/app/models/' + add_group 'Controllers', '/app/controllers/' + add_group 'Helpers', '/app/helpers/' + add_group 'Documents', '/app/documents/' + add_group 'Libraries', '/lib/' + end +end diff --git a/spec/support/factory_girl.rb b/spec/support/factory_girl.rb new file mode 100644 index 00000000..34b6e657 --- /dev/null +++ b/spec/support/factory_girl.rb @@ -0,0 +1,4 @@ +RSpec.configure do |config| + # load FactoryGirl shortcuts create(), etc. + config.include FactoryGirl::Syntax::Methods +end \ No newline at end of file diff --git a/spec/support/session_helper.rb b/spec/support/session_helper.rb new file mode 100644 index 00000000..dcfddeac --- /dev/null +++ b/spec/support/session_helper.rb @@ -0,0 +1,17 @@ + +module SessionHelper + + def login(user=nil, password=nil) + visit login_path + user = FactoryGirl.create :user if user.nil? + if user.instance_of? ::User + nick, password = user.nick, user.password + else + nick = user + end + fill_in 'nick', :with => nick + fill_in 'password', :with => password + find('input[type=submit]').click + end + +end diff --git a/vendor/assets/javascripts/bootstrap-datepicker.de.js b/vendor/assets/javascripts/bootstrap-datepicker.de.js deleted file mode 100644 index ca0c33f8..00000000 --- a/vendor/assets/javascripts/bootstrap-datepicker.de.js +++ /dev/null @@ -1,14 +0,0 @@ -/** - * German translation for bootstrap-datepicker - * Sam Zurcher - */ -;(function($){ - $.fn.datepicker.dates['de'] = { - days: ["Sonntag", "Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag", "Sonntag"], - daysShort: ["Son", "Mon", "Die", "Mit", "Don", "Fre", "Sam", "Son"], - daysMin: ["So", "Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"], - months: ["Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"], - monthsShort: ["Jan", "Feb", "Mär", "Apr", "Mai", "Jun", "Jul", "Aug", "Sep", "Okt", "Nov", "Dez"], - today: "Heute" - }; -}(jQuery)); diff --git a/vendor/assets/javascripts/bootstrap-datepicker.js b/vendor/assets/javascripts/bootstrap-datepicker.js deleted file mode 100644 index 378642cd..00000000 --- a/vendor/assets/javascripts/bootstrap-datepicker.js +++ /dev/null @@ -1,818 +0,0 @@ -/* ========================================================= - * bootstrap-datepicker.js - * http://www.eyecon.ro/bootstrap-datepicker - * ========================================================= - * Copyright 2012 Stefan Petre - * Improvements by Andrew Rowls - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * ========================================================= */ - -!function( $ ) { - - function UTCDate(){ - return new Date(Date.UTC.apply(Date, arguments)); - } - function UTCToday(){ - var today = new Date(); - return UTCDate(today.getUTCFullYear(), today.getUTCMonth(), today.getUTCDate()); - } - - // Picker object - - var Datepicker = function(element, options) { - var that = this; - - this.element = $(element); - this.language = options.language||this.element.data('date-language')||"en"; - this.language = this.language in dates ? this.language : "en"; - this.format = DPGlobal.parseFormat(options.format||this.element.data('date-format')||'mm/dd/yyyy'); - this.picker = $(DPGlobal.template) - .appendTo('body') - .on({ - click: $.proxy(this.click, this) - }); - this.isInput = this.element.is('input'); - this.component = this.element.is('.date') ? this.element.find('.add-on') : false; - this.hasInput = this.component && this.element.find('input').length; - if(this.component && this.component.length === 0) - this.component = false; - - if (this.isInput) { - this.element.on({ - focus: $.proxy(this.show, this), - keyup: $.proxy(this.update, this), - keydown: $.proxy(this.keydown, this) - }); - } else { - if (this.component && this.hasInput){ - // For components that are not readonly, allow keyboard nav - this.element.find('input').on({ - focus: $.proxy(this.show, this), - keyup: $.proxy(this.update, this), - keydown: $.proxy(this.keydown, this) - }); - - this.component.on('click', $.proxy(this.show, this)); - } else { - this.element.on('click', $.proxy(this.show, this)); - } - } - - $(document).on('mousedown', function (e) { - // Clicked outside the datepicker, hide it - if ($(e.target).closest('.datepicker').length == 0) { - that.hide(); - } - }); - - this.autoclose = false; - if ('autoclose' in options) { - this.autoclose = options.autoclose; - } else if ('dateAutoclose' in this.element.data()) { - this.autoclose = this.element.data('date-autoclose'); - } - - this.keyboardNavigation = true; - if ('keyboardNavigation' in options) { - this.keyboardNavigation = options.keyboardNavigation; - } else if ('dateKeyboardNavigation' in this.element.data()) { - this.keyboardNavigation = this.element.data('date-keyboard-navigation'); - } - - switch(options.startView || this.element.data('date-start-view')){ - case 2: - case 'decade': - this.viewMode = this.startViewMode = 2; - break; - case 1: - case 'year': - this.viewMode = this.startViewMode = 1; - break; - case 0: - case 'month': - default: - this.viewMode = this.startViewMode = 0; - break; - } - - this.todayBtn = (options.todayBtn||this.element.data('date-today-btn')||false); - this.todayHighlight = (options.todayHighlight||this.element.data('date-today-highlight')||false); - - this.weekStart = ((options.weekStart||this.element.data('date-weekstart')||dates[this.language].weekStart||0) % 7); - this.weekEnd = ((this.weekStart + 6) % 7); - this.startDate = -Infinity; - this.endDate = Infinity; - this.setStartDate(options.startDate||this.element.data('date-startdate')); - this.setEndDate(options.endDate||this.element.data('date-enddate')); - this.fillDow(); - this.fillMonths(); - this.update(); - this.showMode(); - }; - - Datepicker.prototype = { - constructor: Datepicker, - - show: function(e) { - this.picker.show(); - this.height = this.component ? this.component.outerHeight() : this.element.outerHeight(); - this.update(); - this.place(); - $(window).on('resize', $.proxy(this.place, this)); - if (e ) { - e.stopPropagation(); - e.preventDefault(); - } - this.element.trigger({ - type: 'show', - date: this.date - }); - }, - - hide: function(e){ - this.picker.hide(); - $(window).off('resize', this.place); - this.viewMode = this.startViewMode; - this.showMode(); - if (!this.isInput) { - $(document).off('mousedown', this.hide); - } - if (e && e.currentTarget.value) - this.setValue(); - this.element.trigger({ - type: 'hide', - date: this.date - }); - }, - - setValue: function() { - var formatted = DPGlobal.formatDate(this.date, this.format, this.language); - if (!this.isInput) { - if (this.component){ - this.element.find('input').prop('value', formatted); - } - this.element.data('date', formatted); - } else { - this.element.prop('value', formatted); - } - }, - - setStartDate: function(startDate){ - this.startDate = startDate||-Infinity; - if (this.startDate !== -Infinity) { - this.startDate = DPGlobal.parseDate(this.startDate, this.format, this.language); - } - this.update(); - this.updateNavArrows(); - }, - - setEndDate: function(endDate){ - this.endDate = endDate||Infinity; - if (this.endDate !== Infinity) { - this.endDate = DPGlobal.parseDate(this.endDate, this.format, this.language); - } - this.update(); - this.updateNavArrows(); - }, - - place: function(){ - var zIndex = parseInt(this.element.parents().filter(function() { - return $(this).css('z-index') != 'auto'; - }).first().css('z-index'))+10; - var offset = this.component ? this.component.offset() : this.element.offset(); - this.picker.css({ - top: offset.top + this.height, - left: offset.left, - zIndex: zIndex - }); - }, - - update: function(){ - this.date = DPGlobal.parseDate( - this.isInput ? this.element.prop('value') : this.element.data('date') || this.element.find('input').prop('value'), - this.format, this.language - ); - if (this.date < this.startDate) { - this.viewDate = new Date(this.startDate); - } else if (this.date > this.endDate) { - this.viewDate = new Date(this.endDate); - } else { - this.viewDate = new Date(this.date); - } - this.fill(); - }, - - fillDow: function(){ - var dowCnt = this.weekStart; - var html = ''; - while (dowCnt < this.weekStart + 7) { - html += ''+dates[this.language].daysMin[(dowCnt++)%7]+''; - } - html += ''; - this.picker.find('.datepicker-days thead').append(html); - }, - - fillMonths: function(){ - var html = ''; - var i = 0 - while (i < 12) { - html += ''+dates[this.language].monthsShort[i++]+''; - } - this.picker.find('.datepicker-months td').html(html); - }, - - fill: function() { - var d = new Date(this.viewDate), - year = d.getUTCFullYear(), - month = d.getUTCMonth(), - startYear = this.startDate !== -Infinity ? this.startDate.getUTCFullYear() : -Infinity, - startMonth = this.startDate !== -Infinity ? this.startDate.getUTCMonth() : -Infinity, - endYear = this.endDate !== Infinity ? this.endDate.getUTCFullYear() : Infinity, - endMonth = this.endDate !== Infinity ? this.endDate.getUTCMonth() : Infinity, - currentDate = this.date.valueOf(), - today = new Date(); - this.picker.find('.datepicker-days thead th:eq(1)') - .text(dates[this.language].months[month]+' '+year); - this.picker.find('tfoot th.today') - .text(dates[this.language].today) - .toggle(this.todayBtn); - this.updateNavArrows(); - this.fillMonths(); - var prevMonth = UTCDate(year, month-1, 28,0,0,0,0), - day = DPGlobal.getDaysInMonth(prevMonth.getUTCFullYear(), prevMonth.getUTCMonth()); - prevMonth.setUTCDate(day); - prevMonth.setUTCDate(day - (prevMonth.getUTCDay() - this.weekStart + 7)%7); - var nextMonth = new Date(prevMonth); - nextMonth.setUTCDate(nextMonth.getUTCDate() + 42); - nextMonth = nextMonth.valueOf(); - var html = []; - var clsName; - while(prevMonth.valueOf() < nextMonth) { - if (prevMonth.getUTCDay() == this.weekStart) { - html.push(''); - } - clsName = ''; - if (prevMonth.getUTCFullYear() < year || (prevMonth.getUTCFullYear() == year && prevMonth.getUTCMonth() < month)) { - clsName += ' old'; - } else if (prevMonth.getUTCFullYear() > year || (prevMonth.getUTCFullYear() == year && prevMonth.getUTCMonth() > month)) { - clsName += ' new'; - } - // Compare internal UTC date with local today, not UTC today - if (this.todayHighlight && - prevMonth.getUTCFullYear() == today.getFullYear() && - prevMonth.getUTCMonth() == today.getMonth() && - prevMonth.getUTCDate() == today.getDate()) { - clsName += ' today'; - } - if (prevMonth.valueOf() == currentDate) { - clsName += ' active'; - } - if (prevMonth.valueOf() < this.startDate || prevMonth.valueOf() > this.endDate) { - clsName += ' disabled'; - } - html.push(''+prevMonth.getUTCDate() + ''); - if (prevMonth.getUTCDay() == this.weekEnd) { - html.push(''); - } - prevMonth.setUTCDate(prevMonth.getUTCDate()+1); - } - this.picker.find('.datepicker-days tbody').empty().append(html.join('')); - var currentYear = this.date.getUTCFullYear(); - - var months = this.picker.find('.datepicker-months') - .find('th:eq(1)') - .text(year) - .end() - .find('span').removeClass('active'); - if (currentYear == year) { - months.eq(this.date.getUTCMonth()).addClass('active'); - } - if (year < startYear || year > endYear) { - months.addClass('disabled'); - } - if (year == startYear) { - months.slice(0, startMonth).addClass('disabled'); - } - if (year == endYear) { - months.slice(endMonth+1).addClass('disabled'); - } - - html = ''; - year = parseInt(year/10, 10) * 10; - var yearCont = this.picker.find('.datepicker-years') - .find('th:eq(1)') - .text(year + '-' + (year + 9)) - .end() - .find('td'); - year -= 1; - for (var i = -1; i < 11; i++) { - html += ''+year+''; - year += 1; - } - yearCont.html(html); - }, - - updateNavArrows: function() { - var d = new Date(this.viewDate), - year = d.getUTCFullYear(), - month = d.getUTCMonth(); - switch (this.viewMode) { - case 0: - if (this.startDate !== -Infinity && year <= this.startDate.getUTCFullYear() && month <= this.startDate.getUTCMonth()) { - this.picker.find('.prev').css({visibility: 'hidden'}); - } else { - this.picker.find('.prev').css({visibility: 'visible'}); - } - if (this.endDate !== Infinity && year >= this.endDate.getUTCFullYear() && month >= this.endDate.getUTCMonth()) { - this.picker.find('.next').css({visibility: 'hidden'}); - } else { - this.picker.find('.next').css({visibility: 'visible'}); - } - break; - case 1: - case 2: - if (this.startDate !== -Infinity && year <= this.startDate.getUTCFullYear()) { - this.picker.find('.prev').css({visibility: 'hidden'}); - } else { - this.picker.find('.prev').css({visibility: 'visible'}); - } - if (this.endDate !== Infinity && year >= this.endDate.getUTCFullYear()) { - this.picker.find('.next').css({visibility: 'hidden'}); - } else { - this.picker.find('.next').css({visibility: 'visible'}); - } - break; - } - }, - - click: function(e) { - e.stopPropagation(); - e.preventDefault(); - var target = $(e.target).closest('span, td, th'); - if (target.length == 1) { - switch(target[0].nodeName.toLowerCase()) { - case 'th': - switch(target[0].className) { - case 'switch': - this.showMode(1); - break; - case 'prev': - case 'next': - var dir = DPGlobal.modes[this.viewMode].navStep * (target[0].className == 'prev' ? -1 : 1); - switch(this.viewMode){ - case 0: - this.viewDate = this.moveMonth(this.viewDate, dir); - break; - case 1: - case 2: - this.viewDate = this.moveYear(this.viewDate, dir); - break; - } - this.fill(); - break; - case 'today': - var date = new Date(); - date.setUTCHours(0); - date.setUTCMinutes(0); - date.setUTCSeconds(0); - date.setUTCMilliseconds(0); - - this.showMode(-2); - var which = this.todayBtn == 'linked' ? null : 'view'; - this._setDate(date, which); - break; - } - break; - case 'span': - if (!target.is('.disabled')) { - this.viewDate.setUTCDate(1); - if (target.is('.month')) { - var month = target.parent().find('span').index(target); - this.viewDate.setUTCMonth(month); - this.element.trigger({ - type: 'changeMonth', - date: this.viewDate - }); - } else { - var year = parseInt(target.text(), 10)||0; - this.viewDate.setUTCFullYear(year); - this.element.trigger({ - type: 'changeYear', - date: this.viewDate - }); - } - this.showMode(-1); - this.fill(); - } - break; - case 'td': - if (target.is('.day') && !target.is('.disabled')){ - var day = parseInt(target.text(), 10)||1; - var year = this.viewDate.getUTCFullYear(), - month = this.viewDate.getUTCMonth(); - if (target.is('.old')) { - if (month == 0) { - month = 11; - year -= 1; - } else { - month -= 1; - } - } else if (target.is('.new')) { - if (month == 11) { - month = 0; - year += 1; - } else { - month += 1; - } - } - this._setDate(UTCDate(year, month, day,0,0,0,0)); - } - break; - } - } - }, - - _setDate: function(date, which){ - if (!which || which == 'date') - this.date = date; - if (!which || which == 'view') - this.viewDate = date; - this.fill(); - this.setValue(); - this.element.trigger({ - type: 'changeDate', - date: this.date - }); - var element; - if (this.isInput) { - element = this.element; - } else if (this.component){ - element = this.element.find('input'); - } - if (element) { - element.change(); - if (this.autoclose) { - this.hide(); - } - } - }, - - moveMonth: function(date, dir){ - if (!dir) return date; - var new_date = new Date(date.valueOf()), - day = new_date.getUTCDate(), - month = new_date.getUTCMonth(), - mag = Math.abs(dir), - new_month, test; - dir = dir > 0 ? 1 : -1; - if (mag == 1){ - test = dir == -1 - // If going back one month, make sure month is not current month - // (eg, Mar 31 -> Feb 31 == Feb 28, not Mar 02) - ? function(){ return new_date.getUTCMonth() == month; } - // If going forward one month, make sure month is as expected - // (eg, Jan 31 -> Feb 31 == Feb 28, not Mar 02) - : function(){ return new_date.getUTCMonth() != new_month; }; - new_month = month + dir; - new_date.setUTCMonth(new_month); - // Dec -> Jan (12) or Jan -> Dec (-1) -- limit expected date to 0-11 - if (new_month < 0 || new_month > 11) - new_month = (new_month + 12) % 12; - } else { - // For magnitudes >1, move one month at a time... - for (var i=0; i= this.startDate && date <= this.endDate; - }, - - keydown: function(e){ - if (this.picker.is(':not(:visible)')){ - if (e.keyCode == 27) // allow escape to hide and re-show picker - this.show(); - return; - } - var dateChanged = false, - dir, day, month, - newDate, newViewDate; - switch(e.keyCode){ - case 27: // escape - this.hide(); - e.preventDefault(); - break; - case 37: // left - case 39: // right - if (!this.keyboardNavigation) break; - dir = e.keyCode == 37 ? -1 : 1; - if (e.ctrlKey){ - newDate = this.moveYear(this.date, dir); - newViewDate = this.moveYear(this.viewDate, dir); - } else if (e.shiftKey){ - newDate = this.moveMonth(this.date, dir); - newViewDate = this.moveMonth(this.viewDate, dir); - } else { - newDate = new Date(this.date); - newDate.setUTCDate(this.date.getUTCDate() + dir); - newViewDate = new Date(this.viewDate); - newViewDate.setUTCDate(this.viewDate.getUTCDate() + dir); - } - if (this.dateWithinRange(newDate)){ - this.date = newDate; - this.viewDate = newViewDate; - this.setValue(); - this.update(); - e.preventDefault(); - dateChanged = true; - } - break; - case 38: // up - case 40: // down - if (!this.keyboardNavigation) break; - dir = e.keyCode == 38 ? -1 : 1; - if (e.ctrlKey){ - newDate = this.moveYear(this.date, dir); - newViewDate = this.moveYear(this.viewDate, dir); - } else if (e.shiftKey){ - newDate = this.moveMonth(this.date, dir); - newViewDate = this.moveMonth(this.viewDate, dir); - } else { - newDate = new Date(this.date); - newDate.setUTCDate(this.date.getUTCDate() + dir * 7); - newViewDate = new Date(this.viewDate); - newViewDate.setUTCDate(this.viewDate.getUTCDate() + dir * 7); - } - if (this.dateWithinRange(newDate)){ - this.date = newDate; - this.viewDate = newViewDate; - this.setValue(); - this.update(); - e.preventDefault(); - dateChanged = true; - } - break; - case 13: // enter - this.hide(); - e.preventDefault(); - break; - case 9: // tab - this.hide(); - break; - } - if (dateChanged){ - this.element.trigger({ - type: 'changeDate', - date: this.date - }); - var element; - if (this.isInput) { - element = this.element; - } else if (this.component){ - element = this.element.find('input'); - } - if (element) { - element.change(); - } - } - }, - - showMode: function(dir) { - if (dir) { - this.viewMode = Math.max(0, Math.min(2, this.viewMode + dir)); - } - this.picker.find('>div').hide().filter('.datepicker-'+DPGlobal.modes[this.viewMode].clsName).show(); - this.updateNavArrows(); - } - }; - - $.fn.datepicker = function ( option ) { - var args = Array.apply(null, arguments); - args.shift(); - return this.each(function () { - var $this = $(this), - data = $this.data('datepicker'), - options = typeof option == 'object' && option; - if (!data) { - $this.data('datepicker', (data = new Datepicker(this, $.extend({}, $.fn.datepicker.defaults,options)))); - } - if (typeof option == 'string' && typeof data[option] == 'function') { - data[option].apply(data, args); - } - }); - }; - - $.fn.datepicker.defaults = { - }; - $.fn.datepicker.Constructor = Datepicker; - var dates = $.fn.datepicker.dates = { - en: { - days: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"], - daysShort: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"], - daysMin: ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"], - months: ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"], - monthsShort: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"], - today: "Today" - } - } - - var DPGlobal = { - modes: [ - { - clsName: 'days', - navFnc: 'Month', - navStep: 1 - }, - { - clsName: 'months', - navFnc: 'FullYear', - navStep: 1 - }, - { - clsName: 'years', - navFnc: 'FullYear', - navStep: 10 - }], - isLeapYear: function (year) { - return (((year % 4 === 0) && (year % 100 !== 0)) || (year % 400 === 0)) - }, - getDaysInMonth: function (year, month) { - return [31, (DPGlobal.isLeapYear(year) ? 29 : 28), 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month] - }, - validParts: /dd?|mm?|MM?|yy(?:yy)?/g, - nonpunctuation: /[^ -\/:-@\[-`{-~\t\n\r]+/g, - parseFormat: function(format){ - // IE treats \0 as a string end in inputs (truncating the value), - // so it's a bad format delimiter, anyway - var separators = format.replace(this.validParts, '\0').split('\0'), - parts = format.match(this.validParts); - if (!separators || !separators.length || !parts || parts.length == 0){ - throw new Error("Invalid date format."); - } - return {separators: separators, parts: parts}; - }, - parseDate: function(date, format, language) { - if (date instanceof Date) return date; - if (/^[-+]\d+[dmwy]([\s,]+[-+]\d+[dmwy])*$/.test(date)) { - var part_re = /([-+]\d+)([dmwy])/, - parts = date.match(/([-+]\d+)([dmwy])/g), - part, dir; - date = new Date(); - for (var i=0; i'+ - ''+ - ''+ - ''+ - ''+ - ''+ - '', - contTemplate: '', - footTemplate: '' - }; - DPGlobal.template = ''; -}( window.jQuery );