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 7341cf00..8ac49e82 100644 --- a/Gemfile +++ b/Gemfile @@ -53,10 +53,6 @@ group :development do gem 'binding_of_caller' # gem "rails-i18n-debug" - # Re-enable rails benchmarker/profiler - gem 'ruby-prof' - gem 'test-unit' - # Get infos when not using proper eager loading gem 'bullet' @@ -71,6 +67,22 @@ group :development do gem 'thin' end -# Gems left for backwards compatibility -gem 'acts_as_configurable', git: 'git://github.com/bwalding/acts_as_configurable.git' # user settings migration needs it +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 d64eec91..3ced3672 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 @@ -71,6 +64,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.1) client_side_validations (3.2.5) client_side_validations-simple_form (2.1.0) @@ -86,7 +87,9 @@ GEM coffee-script-source (1.6.3) commonjs (0.2.6) daemons (1.1.9) + database_cleaner (0.7.1) debug_inspector (0.0.2) + diff-lcs (1.2.4) erubis (2.7.0) eventmachine (1.0.3) exception_notification (4.0.0) @@ -95,6 +98,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 (4.0.3) tilt haml-rails (0.4) @@ -107,9 +118,13 @@ GEM highline (1.6.19) hike (1.2.3) i18n (0.6.1) + i18n-spec (0.4.0) + iso inherited_resources (1.4.0) has_scope (~> 0.5.0) responders (~> 0.9) + iso (0.2.0) + i18n journey (1.0.4) jquery-rails (3.0.4) railties (>= 3.0, < 5.0) @@ -142,6 +157,7 @@ GEM activesupport (~> 3.1) polyamorous (~> 0.5.0) mime-types (1.23) + mini_portile (0.5.1) mono_logger (1.1.0) multi_json (1.7.7) mysql2 (0.3.11) @@ -152,6 +168,8 @@ GEM net-ssh (2.6.8) net-ssh-gateway (1.2.0) net-ssh (>= 2.6.5) + nokogiri (1.6.0) + mini_portile (~> 0.5.0) pdf-reader (1.3.3) Ascii85 (~> 1.0.0) afm (~> 0.2.0) @@ -196,7 +214,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) ref (1.0.5) responders (0.9.3) @@ -207,8 +225,20 @@ GEM redis-namespace (~> 1.2) sinatra (>= 0.9.2) vegas (~> 0.1.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.9) sass-rails (3.2.6) railties (~> 3.2.0) @@ -217,6 +247,11 @@ GEM select2-rails (3.4.3) 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.11.0) activesupport (>= 2.3.2) simple-navigation-bootstrap (1.0.0) @@ -225,6 +260,10 @@ GEM 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.4.3) rack (~> 1.4) rack-protection (~> 1.4) @@ -238,7 +277,6 @@ GEM rack (~> 1.0) tilt (~> 1.1, != 1.3.0) sqlite3 (1.3.7) - test-unit (2.5.5) therubyracer (0.11.4) libv8 (~> 3.11.8.12) ref @@ -264,18 +302,20 @@ GEM uniform_notifier (1.2.0) vegas (0.1.11) rack (>= 1.0.0) + websocket (1.0.7) whenever (0.8.3) 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 @@ -284,12 +324,17 @@ DEPENDENCIES bullet capistrano (= 2.13.5) capistrano-ext + capybara (~> 2.1.0) client_side_validations client_side_validations-simple_form 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 @@ -303,14 +348,18 @@ DEPENDENCIES 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/controllers/articles_controller.rb b/app/controllers/articles_controller.rb index fdf4a46a..b29fd594 100644 --- a/app/controllers/articles_controller.rb +++ b/app/controllers/articles_controller.rb @@ -209,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/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/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/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/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/supplier.rb b/app/models/supplier.rb index 4ac1572e..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) } diff --git a/app/models/user.rb b/app/models/user.rb index ab11da32..2b4a399b 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -66,7 +66,7 @@ class User < ActiveRecord::Base end def receive_email? - settings.messages['send_as_email'] == "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 819c75bd..3bcf124d 100644 --- a/app/models/workgroup.rb +++ b/app/models/workgroup.rb @@ -3,7 +3,7 @@ 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 validate :last_admin_on_earth, :on => :update 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/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/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/index.html.haml b/app/views/stockit/index.html.haml index 4673b967..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() { 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/_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/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/config/locales/de.yml b/config/locales/de.yml index 11abe693..c0000036 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -505,6 +505,7 @@ de: - Gebinde - Einheit - Preis/Einheit + total: Gesamtpreis order_matrix: filename: Bestellung %{name}-%{date} - Sortiermatrix heading: Artikelübersicht @@ -719,6 +720,7 @@ de: ordergroups: account_balance: Kontostand account_statement: Kontoauszug + contact: Kontakt name: Name new_transaction: Neue Transaktion update: @@ -1602,6 +1604,7 @@ de: contact_person: Kontaktperson contact_phone: Telefon ignore_apple_restriction: Bestellstop bei zu wenig Äpfeln ignorieren + name: Name page: body: Inhalt parent_id: Oberseite @@ -1669,6 +1672,7 @@ de: language: de: Deutsch en: English + fr: Französisch nl: Niederländisch required: mark: ! '*' @@ -1742,6 +1746,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: @@ -1772,6 +1777,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: @@ -1832,6 +1838,7 @@ 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 diff --git a/config/locales/en.yml b/config/locales/en.yml index c9eecb77..2fa64013 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -236,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 @@ -440,7 +440,7 @@ en: article: Article category: Category create_from_blank: Create new article - create_stock_article: Create stock articles + create_stock_article: Create stock article price: Netprice quantity: Quantity title_fill_quantities: 2. Set delivery quantities @@ -493,7 +493,7 @@ en: - Article - Amount - Price - - Unit Quantity + - Unit quantity - Unit - Sum sum: Sum @@ -504,20 +504,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. @@ -721,6 +725,7 @@ en: ordergroups: account_balance: Account balance account_statement: Account statement + contact: Contact name: Name new_transaction: New transaction update: @@ -1604,6 +1609,7 @@ 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 @@ -1671,6 +1677,7 @@ en: language: de: German en: English + fr: French nl: Dutch required: mark: ! '*' @@ -1744,6 +1751,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: @@ -1774,6 +1782,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: @@ -1834,6 +1843,7 @@ 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 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 4b50508d..628bd0e8 100644 --- a/config/locales/nl.yml +++ b/config/locales/nl.yml @@ -479,48 +479,60 @@ nl: 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: - rows: + filename: Bestelling %{name}-%{date} - Fax + rows: ! '[]' + total: Totaal order_matrix: - filename: - heading: + filename: Bestelling %{name}-%{date} - Sorteermatrix + heading: Artikeloverzicht rows: - title: - total: + title: ! 'Sorteermatrix van bestelling: %{name}, gesloten op %{date}' + total: + one: In totaal éen artikel + other: In totaal %{count} artikelen errors: format: - general: + general: Er is een probleem opgetreden. general_again: - general_msg: + general_msg: ! 'Er is een probleem opgetreden: %{msg}' messages: - accepted: - blank: + accepted: moet geaccepteerd worden + blank: moet ingevuld worden confirmation: - empty: - equal_to: + empty: moet ingevuld worden + equal_to: moet precies %{count} zijn even: - exclusion: - greater_than: + exclusion: moet even zijn + greater_than: moet groter dan %{count} zijn greater_than_or_equal_to: inclusion: invalid: - less_than: - less_than_or_equal_to: - not_a_number: - not_an_integer: - odd: + 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: + taken: is al in gebruik + taken_with_deleted: is al in gebruik (verwijderde groep) too_long: too_short: wrong_length: @@ -697,6 +709,7 @@ nl: ordergroups: account_balance: Tegoed account_statement: + contact: name: Naam new_transaction: Nieuwe transactie update: @@ -1493,6 +1506,7 @@ nl: contact_person: Contactpersoon contact_phone: Telefoon ignore_apple_restriction: + name: page: body: parent_id: @@ -1560,6 +1574,7 @@ nl: language: de: Duits en: Engels + fr: Frans nl: Nederlands required: mark: ! '*' @@ -1633,6 +1648,7 @@ nl: show_stock_takings: stock_count: stock_worth: + title: toggle_unavailable: view_options: new: @@ -1663,6 +1679,7 @@ nl: shared_suppliers: body: subscribe: + subscribe_again: supplier: title: show: @@ -1723,6 +1740,7 @@ nl: group_tasks: my_tasks: new_task: + pages: new: title: repeated: @@ -1763,9 +1781,9 @@ nl: history: marks: close: ! '×' - success: + success: or_cancel: of annuleren - please_wait: + please_wait: Een moment alstublieft... save: Opslaan show: Tonen views: diff --git a/db/migrate/20130622095040_move_weekly_tasks.rb b/db/migrate/20130622095040_move_weekly_tasks.rb index be316c5c..e8dc8384 100644 --- a/db/migrate/20130622095040_move_weekly_tasks.rb +++ b/db/migrate/20130622095040_move_weekly_tasks.rb @@ -35,6 +35,8 @@ class MoveWeeklyTasks < ActiveRecord::Migration private def weekly_task?(workgroup, task) + return false if task.due_date.nil? + group_task = { weekday: workgroup.weekday, name: workgroup.task_name, diff --git a/db/migrate/20130718183101_migrate_user_settings.rb b/db/migrate/20130718183101_migrate_user_settings.rb index a9722f1b..73f30acf 100644 --- a/db/migrate/20130718183101_migrate_user_settings.rb +++ b/db/migrate/20130718183101_migrate_user_settings.rb @@ -1,34 +1,55 @@ class MigrateUserSettings < ActiveRecord::Migration def up - old_settings = ConfigurableSetting.all - - old_settings.each do |old_setting| - # get target (user) - type = old_setting.configurable_type - id = old_setting.configurable_id - user = type.constantize.find(id) - - # 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 + 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 + + # 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 03d86aba..0d5b1326 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -66,18 +66,6 @@ ActiveRecord::Schema.define(:version => 20130718183101) 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" 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_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 2b95ff7b..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..19f41529 --- /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 { FactoryGirl.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..0b9983e6 --- /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 { FactoryGirl.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..f3bf1bde --- /dev/null +++ b/spec/factories/order.rb @@ -0,0 +1,31 @@ +require 'factory_girl' + +FactoryGirl.define do + + factory :order do + starts { Time.now } + supplier { FactoryGirl.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..e4a1c18b --- /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 + FactoryGirl.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..6f7e9e4d --- /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| + FactoryGirl.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..75133131 --- /dev/null +++ b/spec/integration/balancing_spec.rb @@ -0,0 +1,55 @@ +require 'spec_helper' + +describe 'settling an order', :type => :feature do + let(:admin) { FactoryGirl.create :user, groups:[FactoryGirl.create(:workgroup, role_finance: true)] } + let(:supplier) { FactoryGirl.create :supplier } + let(:article) { FactoryGirl.create :article, supplier: supplier, unit_quantity: 1 } + let(:order) { FactoryGirl.create :order, supplier: supplier, article_ids: [article.id] } # need to ref article + let(:go1) { FactoryGirl.create :group_order, order: order } + let(:go2) { FactoryGirl.create :group_order, order: order } + let(:oa) { order.order_articles.find_by_article_id(article.id) } + let(:goa1) { FactoryGirl.create :group_order_article, group_order: go1, order_article: oa } + let(:goa2) { FactoryGirl.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 + + 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..12fd84f5 --- /dev/null +++ b/spec/integration/product_distribution_example_spec.rb @@ -0,0 +1,57 @@ +require 'spec_helper' + +describe 'product distribution', :type => :feature do + let(:admin) { FactoryGirl.create :admin } + let(:user_a) { FactoryGirl.create :user, groups: [FactoryGirl.create(:ordergroup)] } + let(:user_b) { FactoryGirl.create :user, groups: [FactoryGirl.create(:ordergroup)] } + let(:supplier) { FactoryGirl.create :supplier } + let(:article) { FactoryGirl.create :article, supplier: supplier, unit_quantity: 5 } + let(:order) { FactoryGirl.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..f86c2e59 --- /dev/null +++ b/spec/integration/session_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +describe 'the session', :type => :feature do + let(:user) { FactoryGirl.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..c93a810c --- /dev/null +++ b/spec/integration/supplier_spec.rb @@ -0,0 +1,63 @@ +require 'spec_helper' + +describe 'supplier', :type => :feature do + let(:supplier) { FactoryGirl.create :supplier } + + describe :type => :feature, :js => true do + let(:user) { FactoryGirl.create :user, groups:[FactoryGirl.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 = FactoryGirl.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) { FactoryGirl.create :article_category } + let(:user) { FactoryGirl.create :user, groups:[FactoryGirl.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..45b802b4 --- /dev/null +++ b/spec/models/article_spec.rb @@ -0,0 +1,47 @@ +require 'spec_helper' + +describe Article do + let(:supplier) { FactoryGirl.create :supplier } + let(:article) { FactoryGirl.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..160b312f --- /dev/null +++ b/spec/models/group_order_article_spec.rb @@ -0,0 +1,48 @@ +require 'spec_helper' + +describe GroupOrderArticle do + let(:user) { FactoryGirl.create :user, groups: [FactoryGirl.create(:ordergroup)] } + let(:order) { FactoryGirl.create(:order).reload } + let(:go) { FactoryGirl.create :group_order, order: order, ordergroup: user.ordergroup } + let(:goa) { FactoryGirl.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) { FactoryGirl.create :article, supplier: order.supplier, unit_quantity: 1 } + let(:oa) { order.order_articles.create(:article => article) } + let(:goa) { FactoryGirl.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..04f69c33 --- /dev/null +++ b/spec/models/group_order_spec.rb @@ -0,0 +1,25 @@ +require 'spec_helper' + +describe GroupOrder do + let(:user) { FactoryGirl.create :user, groups: [FactoryGirl.create(:ordergroup)] } + let(:order) { FactoryGirl.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) { FactoryGirl.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..4a8b7ebc --- /dev/null +++ b/spec/models/order_spec.rb @@ -0,0 +1,47 @@ +require 'spec_helper' + +describe Order do + + it 'needs a supplier' do + expect(FactoryGirl.build(:order, supplier: nil)).to be_invalid + end + + it 'needs order articles' do + supplier = FactoryGirl.create :supplier, article_count: 0 + expect(FactoryGirl.build(:order, supplier: supplier)).to be_invalid + end + + it 'can be created' do + expect(FactoryGirl.build(:order, article_count: 1)).to be_valid + end + + describe 'with articles' do + let(:order) { FactoryGirl.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..db01b148 --- /dev/null +++ b/spec/models/supplier_spec.rb @@ -0,0 +1,16 @@ +require 'spec_helper' + +describe Supplier do + let(:supplier) { FactoryGirl.create :supplier } + + it 'has a unique name' do + supplier2 = FactoryGirl.build :supplier, name: supplier.name + expect(supplier2).to be_invalid + end + + it 'has valid articles' do + supplier = FactoryGirl.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..d679bea8 --- /dev/null +++ b/spec/models/user_spec.rb @@ -0,0 +1,59 @@ +require 'spec_helper' + +describe User do + + it 'is correctly created' do + user = FactoryGirl.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) { FactoryGirl.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) { FactoryGirl.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(FactoryGirl.build(:user, nick: user.nick, email: "x-#{user.email}")).to be_invalid + end + it 'has a unique email' do + expect(FactoryGirl.build(:user, email: "#{user.email}")).to be_invalid + end + end + + describe 'admin' do + let(:user) { FactoryGirl.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..1324abca --- /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 '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/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