diff --git a/Dockerfile-dev b/Dockerfile-dev index 37dce5f6..f25c2062 100644 --- a/Dockerfile-dev +++ b/Dockerfile-dev @@ -1,4 +1,4 @@ -FROM ruby:2.7 +FROM ruby:2.7.8 # Install dependencies RUN deps='libmagic-dev chromium nodejs' && \ @@ -19,6 +19,7 @@ ENV PORT=3000 \ WORKDIR /app +RUN gem update --system RUN gem install bundler RUN bundle config build.nokogiri "--use-system-libraries" diff --git a/Gemfile b/Gemfile index 5dc92369..a8d33863 100644 --- a/Gemfile +++ b/Gemfile @@ -71,6 +71,7 @@ gem 'foodsoft_links', path: 'plugins/links' gem 'foodsoft_messages', path: 'plugins/messages' gem 'foodsoft_polls', path: 'plugins/polls' gem 'foodsoft_wiki', path: 'plugins/wiki' +gem 'foodsoft_automatic_invoices', path: 'plugins/automatic_invoices' # plugins not enabled by default # gem 'foodsoft_current_orders', path: 'plugins/current_orders' diff --git a/Gemfile.lock b/Gemfile.lock index 7665a67f..74d736a6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -16,6 +16,13 @@ GIT acts_as_versioned (0.6.0) activerecord (>= 3.0.9) +PATH + remote: plugins/automatic_invoices + specs: + foodsoft_automatic_invoices (0.0.1) + deface (~> 1.9) + rails + PATH remote: plugins/discourse specs: @@ -630,6 +637,7 @@ DEPENDENCIES exception_notification factory_bot_rails faker + foodsoft_automatic_invoices! foodsoft_discourse! foodsoft_documents! foodsoft_links! diff --git a/app/controllers/finance/balancing_controller.rb b/app/controllers/finance/balancing_controller.rb index e1a2dafb..fad07684 100644 --- a/app/controllers/finance/balancing_controller.rb +++ b/app/controllers/finance/balancing_controller.rb @@ -8,6 +8,7 @@ class Finance::BalancingController < Finance::BaseController flash.now.alert = t('.alert') if @order.closed? @comments = @order.comments + @articles = @order.order_articles.ordered_or_member.includes(:article, :article_price, group_order_articles: { group_order: :ordergroup }) @@ -24,7 +25,6 @@ class Finance::BalancingController < Finance::BaseController else @articles end - render layout: false if request.xhr? end diff --git a/app/views/finance/balancing/_article_results.haml b/app/views/finance/balancing/_article_results.haml new file mode 100644 index 00000000..acae2a1c --- /dev/null +++ b/app/views/finance/balancing/_article_results.haml @@ -0,0 +1 @@ += render partial: 'finance/balancing/edit_results_by_articles', locals: {order: @order, articles: @articles, comments: @comments } diff --git a/app/views/finance/balancing/_edit_results_by_articles.html.haml b/app/views/finance/balancing/_edit_results_by_articles.html.haml index 61ada1b8..4518223b 100644 --- a/app/views/finance/balancing/_edit_results_by_articles.html.haml +++ b/app/views/finance/balancing/_edit_results_by_articles.html.haml @@ -36,28 +36,28 @@ %th= heading_helper Article, :tax %th= heading_helper Article, :deposit %th{:colspan => "2"} - - unless @order.closed? + - unless order.closed? .btn-group - = link_to t('.add_article'), new_order_order_article_path(@order), remote: true, + = link_to t('.add_article'), new_order_order_article_path(order), remote: true, class: 'btn btn-small' = link_to '#', data: {toggle: 'dropdown'}, class: 'btn btn-small dropdown-toggle' do %span.caret %ul.dropdown-menu - %li= link_to t('.add_article'), new_order_order_article_path(@order), remote: true - %li= link_to t('.edit_transport'), edit_transport_finance_order_path(@order), remote: true + %li= link_to t('.add_article'), new_order_order_article_path(order), remote: true + %li= link_to t('.edit_transport'), edit_transport_finance_order_path(order), remote: true %tbody.list#result_table - - for order_article in @articles.select { |oa| oa.units > 0 } + - for order_article in articles.select { |oa| oa.units > 0 } = render :partial => "order_article_result", :locals => {:order_article => order_article} %tr %td{ colspan: 10 } The following were not ordered - - for order_article in @articles.select { |oa| oa.units == 0 } + - for order_article in articles.select { |oa| oa.units == 0 } = render :partial => "order_article_result", :locals => {:order_article => order_article} - - if @order.transport + - if order.transport %tr %td{ colspan: 5 }= heading_helper Order, :transport - %td{ colspan: 3, data: {value: @order.transport} }= number_to_currency(@order.transport) - %td= link_to t('ui.edit'), edit_transport_finance_order_path(@order), remote: true, + %td{ colspan: 3, data: {value: order.transport} }= number_to_currency(order.transport) + %td= link_to t('ui.edit'), edit_transport_finance_order_path(order), remote: true, class: 'btn btn-mini' unless order_article.order.closed? diff --git a/app/views/finance/balancing/_order_article.html.haml b/app/views/finance/balancing/_order_article.html.haml index 47db3e31..0f388799 100644 --- a/app/views/finance/balancing/_order_article.html.haml +++ b/app/views/finance/balancing/_order_article.html.haml @@ -1,6 +1,7 @@ %td.closed.name = link_to order_article.article.name, '#', 'data-toggle-this' => "#group_order_articles_#{order_article.id}" %td= order_article.article.order_number +-# :plain => true destroys deface functionality %td{title: units_history_line(order_article, :plain => true)} = order_article.units = pkg_helper order_article.article_price @@ -22,6 +23,6 @@ %td = link_to t('ui.edit'), edit_order_order_article_path(order_article.order, order_article), remote: true, class: 'btn btn-mini' unless order_article.order.closed? -%td +%td.end = link_to t('ui.delete'), order_order_article_path(order_article.order, order_article), method: :delete, remote: true, data: {confirm: t('.confirm')}, class: 'btn btn-danger btn-mini' unless order_article.order.closed? diff --git a/app/views/finance/balancing/_summary.haml b/app/views/finance/balancing/_summary.haml index e466727f..92805ad2 100644 --- a/app/views/finance/balancing/_summary.haml +++ b/app/views/finance/balancing/_summary.haml @@ -6,7 +6,7 @@ %tr %td= t('.net_amount') %td.numeric= number_to_currency(order.sum(:net)) - %tr + %tr.gross-amount %td= t('.gross_amount') %td.numeric= number_to_currency(order.sum(:gross)) %tr diff --git a/app/views/finance/balancing/new.html.haml b/app/views/finance/balancing/new.html.haml index 7e143e08..d1e91731 100644 --- a/app/views/finance/balancing/new.html.haml +++ b/app/views/finance/balancing/new.html.haml @@ -77,5 +77,6 @@ remote: true %section#results - = render 'edit_results_by_articles' + = render partial: 'article_results', locals: { order: @order, articles: @articles, comments: @comments } + %p= link_to_top diff --git a/app/views/group_orders/new.html.haml b/app/views/group_orders/new.html.haml index 86842fe7..bcc58327 100644 --- a/app/views/group_orders/new.html.haml +++ b/app/views/group_orders/new.html.haml @@ -1 +1 @@ -= render 'form' \ No newline at end of file += render 'form' diff --git a/app/views/orders/_articles.html.haml b/app/views/orders/_articles.html.haml index 1c800cc7..1ce7cee0 100644 --- a/app/views/orders/_articles.html.haml +++ b/app/views/orders/_articles.html.haml @@ -35,9 +35,8 @@ %td= "#{order_article.quantity} + #{order_article.tolerance}" - else %td= "#{order_article.quantity}" - %td{title: units_history_line(order_article, plain: true)} - = units - = pkg_helper order_article.price + = render "units_history", order_article: order_article, units: units + %p = t '.prices_sum' = "#{number_to_currency(total_net)} / #{number_to_currency(total_gross)}" diff --git a/app/views/orders/_units_history.haml b/app/views/orders/_units_history.haml new file mode 100644 index 00000000..9c519b23 --- /dev/null +++ b/app/views/orders/_units_history.haml @@ -0,0 +1,3 @@ +%td{title: units_history_line(order_article, plain: true)} + = units + = pkg_helper order_article.price \ No newline at end of file diff --git a/config/database.yml.MySQL_SAMPLE b/config/database.yml.MySQL_SAMPLE deleted file mode 100644 index b31b1006..00000000 --- a/config/database.yml.MySQL_SAMPLE +++ /dev/null @@ -1,46 +0,0 @@ -# Foodsoft database configuration for MySQL -# -# This file is in the public domain -# -# -# MySQL versions 4.1 and 5.0 are recommended. -# -# Install the MYSQL driver -# gem install mysql2 -# -# Ensure the MySQL gem is defined in your Gemfile -# gem 'mysql2' -# -# And be sure to use new-style password hashing: -# http://dev.mysql.com/doc/refman/5.0/en/old-client.html -development: - adapter: mysql2 - encoding: utf8mb4 - reconnect: false - database: foodsoft_development - pool: 5 - host: localhost -# socket: /tmp/mysql.sock - -# Warning: The database defined as "test" will be erased and -# re-generated from your development database when you run "rake". -# Do not set this db to the same as development or production. -test: - adapter: mysql2 - encoding: utf8mb4 - reconnect: false - database: foodsoft_test - pool: 5 - host: localhost -# socket: /tmp/mysql.sock - -production: - adapter: mysql2 - encoding: utf8mb4 - reconnect: false - pool: 5 - host: <%= ENV['FOODSOFT_DB_HOST'] %> - database: <%= ENV['FOODSOFT_DB_NAME'] %> - username: <%= ENV['FOODSOFT_DB_USER'] %> - password: <%= ENV['FOODSOFT_DB_PASSWORD'] %> -# socket: /tmp/mysql.sock diff --git a/config/locales/de.yml b/config/locales/de.yml index 4aadc284..65dd7061 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -860,7 +860,8 @@ de: summary: changed: Daten wurden verändert! duration: von %{starts} bis %{ends} - fc_amount: 'FC-Betrag:' + fc_amount: 'FC-Gesamtbetrag:' + fc_amount_without_deposit: 'FC-Betrag (ohne Pfand):' fc_profit: FC Gewinn gross_amount: 'Bruttobetrag:' groups_amount: 'Gruppenbeträge:' @@ -1554,6 +1555,7 @@ de: starts: läuft von %{starts} starts_ends: läuft von %{starts} bis %{ends} description2: "%{ordergroups} haben %{article_count} Artikel mit einem Gesamtwert von %{net_sum} / %{gross_sum} (netto / brutto) bestellt." + description3: " Zuzüglich Pfand %{net_deposit} / %{deposit} (netto / brutto)." group_orders: 'Gruppenbestellungen:' search_placeholder: articles: Suche nach Artikeln ... diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index b0a325db..0a8b3fec 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -11,7 +11,6 @@ services: build: context: . dockerfile: Dockerfile-dev - platform: linux/x86_64 command: ./proc-start worker volumes: - bundle:/usr/local/bundle diff --git a/plugins/automatic_invoices/README.md b/plugins/automatic_invoices/README.md new file mode 100644 index 00000000..0cc40a83 --- /dev/null +++ b/plugins/automatic_invoices/README.md @@ -0,0 +1,42 @@ +FoodsoftAutomaticInvoices +===================== + +Foodsoft is currently designed to work with one order at a time. In practice, +however there can be multiple orders open at the same time, with one pickup +day. The proper solution to this is to introduce the notion of order cycles, +with each order belonging to a cycle. Until that time, we have this plugin, +with screens for working on all orders that are closed-but-not-finished. + +Important: be sure to settle orders from the previous order cycle, before +you close any. If you don't, articles from previous and current dates start +to mix up (if you do, settle the old ones asap). + +* `current_orders/orders/receive` for a list of orders that can be received. +* `current_orders/orders.pdf?document=(groups|articles)` for PDFs for all + orders that are closed but not settled. +* `current_orders/articles` to edit an order article's ordergroups in all + orders that are closed but not settled. +* `current_orders/ordergroups` to edit an ordergroup's order articles in all + orders that are closed but not settled. +* `current_orders/group_orders` for all articles in the user's group orders + from orders that are not settled. Can be used as a "shopping-cart overview" + or "checkout" page. + +New menu items will be added in the "Orders" menu. Please note that members +with _Orders_ permission will now be able to edit the amounts members received +in some of these screens, something that was previously restricted to the +_Finance_ permission. + +This plugin is not enabled by default. To install it, add uncomment the +corresponding line in the `Gemfile`, or add: + +```Gemfile +gem 'foodsoft_current_orders', path: 'plugins/current_orders' +``` + +This plugin introduces the foodcoop config option `use_current_orders`, which +needs to be set to `true` to enable the plugin. This can be done in the +configuration screen or `config/app_config.yml`. + +This plugin is part of the foodsoft package and uses the AGPL-3 license (see +foodsoft's LICENSE for the full license text). diff --git a/plugins/automatic_invoices/app/assets/stylesheets/group_orders.css.less b/plugins/automatic_invoices/app/assets/stylesheets/group_orders.css.less new file mode 100644 index 00000000..651805ad --- /dev/null +++ b/plugins/automatic_invoices/app/assets/stylesheets/group_orders.css.less @@ -0,0 +1,70 @@ +#order-footer-override, .article-info { + text-align: left; + z-index: 1; + position: fixed; + bottom: 0; + background-color: #E4EED6; + border-top: 2px solid #78B74E; + + #total-sum { + width: 22em; + margin: .5em 2em 0 0; + float: right; + #order-button { + margin: .5em 0; + + input:disabled { + background-color: red; } + } + } +} + +/* Hide the orders article info for small screens + to prevent the "save order" button to disappear */ +@media only screen and (max-width: 950px) { + tr.order-article:hover .article-info { + display: none; + } + tr.order-article:focus .article-info { + display: none; + } +} + +#order-footer-override { + width: 100%; + right: 0; + left: 0; +} + +.article-info { + z-index: 2; + width: 40em; + height: 8em; + border: none; + left: 30px; + + .article-name { + text-align: center; + margin: 2px 0; + margin-bottom: 5px; + width: 100%; + font-weight: bold; + } + .pull-right { + width: 35%; + } + .pull-left { + width: 60%; + } +} + +tr.order-article .article-info { + display: none; +} + +tr.order-article:focus{ + background-color: #E4EED6; +} +tr.order-article:focus .article-info { + display: block; +} diff --git a/plugins/automatic_invoices/app/controllers/group_order_invoices_controller.rb b/plugins/automatic_invoices/app/controllers/group_order_invoices_controller.rb new file mode 100644 index 00000000..4a14ac16 --- /dev/null +++ b/plugins/automatic_invoices/app/controllers/group_order_invoices_controller.rb @@ -0,0 +1,85 @@ +class GroupOrderInvoicesController < ApplicationController + include Concerns::SendOrderPdf + before_action :authenticate_finance + + def show + @group_order_invoice = GroupOrderInvoice.find(params[:id]) + raise RecordInvalid unless FoodsoftConfig[:contact][:tax_number] + + respond_to do |format| + format.pdf do + send_group_order_invoice_pdf @group_order_invoice if FoodsoftConfig[:contact][:tax_number] + end + end + rescue ActiveRecord::RecordInvalid => e + redirect_back fallback_location: root_path, notice: 'Something went wrong', alert: I18n.t('errors.general_msg', msg: "#{e} " + I18n.t('errors.check_tax_number')) + end + + def create + go = GroupOrder.find(params[:group_order]) + @order = go.order + GroupOrderInvoice.find_or_create_by!(group_order_id: go.id) + respond_to do |format| + format.js + end + redirect_back fallback_location: root_path + rescue StandardError => e + redirect_back fallback_location: root_path, notice: 'Something went wrong', :alert => I18n.t('errors.general_msg', :msg => e) + end + + def destroy + goi = GroupOrderInvoice.find(params[:id]) + @order = goi.group_order.order + goi.destroy + respond_to do |format| + format.js + format.json { head :no_content } + end + end + + def create_multiple + invoice_date = params[:group_order_invoice][:invoice_date] + order_id = params[:group_order_invoice][:order_id] + @order = Order.find(order_id) + gos = GroupOrder.where("order_id = ?", order_id) + gos.each do |go| + goi = GroupOrderInvoice.find_or_create_by!(group_order_id: go.id) + goi.invoice_date = invoice_date + goi.invoice_number = goi.generate_invoice_number(1) + goi.save! + end + respond_to do |format| + format.js + end + end + + def download_all + order = Order.find(params[:order_id]) + + invoices = order.group_orders.map(&:group_order_invoice) + pdf = {} + file_paths = [] + temp_file = Tempfile.new("all_invoices_for_order_#{order.id}.zip") + Zip::File.open(temp_file.path, Zip::File::CREATE) do |zipfile| + invoices.each do |invoice| + pdf = create_invoice_pdf(invoice) + file_path = File.join("tmp", pdf.filename) + File.open(file_path, 'w:ASCII-8BIT') do |file| + file.write(pdf.to_pdf) + end + file_paths << file_path + zipfile.add(pdf.filename, file_path) unless zipfile.find_entry(pdf.filename) + end + end + + zip_data = File.read(temp_file.path) + file_paths.each do |file_path| + File.delete(file_path) + end + respond_to do |format| + format.html do + send_data(zip_data, type: 'application/zip', filename: "#{l order.ends, format: :file}-#{order.supplier.name}-#{order.id}.zip", disposition: 'attachment') + end + end + end +end diff --git a/plugins/automatic_invoices/app/documents/group_order_invoice_pdf.rb b/plugins/automatic_invoices/app/documents/group_order_invoice_pdf.rb new file mode 100644 index 00000000..daf8ff0b --- /dev/null +++ b/plugins/automatic_invoices/app/documents/group_order_invoice_pdf.rb @@ -0,0 +1,301 @@ +class GroupOrderInvoicePdf < RenderPdf + def filename + ordergroup_name = @options[:ordergroup].name || "OrderGroup" + "#{ordergroup_name}_" + I18n.t('documents.group_order_invoice_pdf.filename', :number => @options[:invoice_number]) + '.pdf' + end + + def title + I18n.t('documents.group_order_invoice_pdf.title', :supplier => @options[:supplier]) + end + + def body + contact = FoodsoftConfig[:contact].symbolize_keys + ordergroup = @options[:ordergroup] + + # From paragraph + bounding_box [margin_box.right - 200, margin_box.top - 20], width: 200 do + text I18n.t('documents.group_order_invoice_pdf.invoicer') + move_down 7 + text FoodsoftConfig[:name], size: fontsize(9), align: :left + move_down 5 + text contact[:street], size: fontsize(9), align: :left + move_down 5 + text "#{contact[:zip_code]} #{contact[:city]}", size: fontsize(9), align: :left + move_down 5 + if contact[:phone].present? + text "#{Supplier.human_attribute_name :phone}: #{contact[:phone]}", size: fontsize(9), align: :left + move_down 5 + end + text "#{Supplier.human_attribute_name :email}: #{contact[:email]}", size: fontsize(9), align: :left if contact[:email].present? + move_down 5 + text I18n.t('documents.group_order_invoice_pdf.tax_number', :number => @options[:tax_number]), size: fontsize(9), align: :left + end + + # Receiving Ordergroup + bounding_box [margin_box.left, margin_box.top - 20], width: 200 do + text I18n.t('documents.group_order_invoice_pdf.invoicee') + move_down 7 + text I18n.t('documents.group_order_invoice_pdf.ordergroup.name', ordergroup: ordergroup.name.to_s), size: fontsize(9) + move_down 5 + if ordergroup.contact_address.present? + text I18n.t('documents.group_order_invoice_pdf.ordergroup.contact_address', contact_address: ordergroup.contact_address.to_s), size: fontsize(9) + move_down 5 + end + if ordergroup.contact_phone.present? + text I18n.t('documents.group_order_invoice_pdf.ordergroup.contact_phone', contact_phone: ordergroup.contact_phone.to_s), size: fontsize(9) + move_down 5 + end + if ordergroup.customer_number.present? + text I18n.t('documents.group_order_invoice_pdf.ordergroup.customer_number', customer_number: ordergroup.customer_number.to_s), size: fontsize(9) + move_down 5 + end + end + + # invoice Date and nnvoice number + bounding_box [margin_box.right - 200, margin_box.top - 150], width: 200 do + text I18n.t('documents.group_order_invoice_pdf.invoice_date', invoice_date: @options[:invoice_date].strftime(I18n.t('date.formats.default'))), align: :left + move_down 5 + text I18n.t('documents.group_order_invoice_pdf.invoice_number', invoice_number: @options[:invoice_number]), align: :left + end + + move_down 15 + + # kind of the "body" of the invoice + text I18n.t('documents.group_order_invoice_pdf.payment_method', payment_method: @options[:payment_method]) + move_down 15 + text I18n.t('documents.group_order_invoice_pdf.table_headline') + move_down 5 + + #------------- Table Data ----------------------- + + @group_order = GroupOrder.find(@options[:group_order].id) + + if FoodsoftConfig[:group_order_invoices][:vat_exempt] + body_for_vat_exempt + else + body_with_vat + end + end + + def body_for_vat_exempt + total_gross = 0 + data = [I18n.t('documents.group_order_invoice_pdf.vat_exempt_rows')] + move_down 10 + group_order_articles = GroupOrderArticle.where(group_order_id: @group_order.id) + separate_deposits = FoodsoftConfig[:group_order_invoices]&.[](:separate_deposits) + group_order_articles.each do |goa| + # if no unit is received, nothing is to be charged + next if goa.result.to_i == 0 + + goa_total_price = separate_deposits ? goa.total_price_without_deposit : goa.total_price + data << [goa.order_article.article.name, + goa.result.to_i, + number_to_currency(goa.order_article.price.fc_price_without_deposit), + number_to_currency(goa_total_price)] + total_gross += goa_total_price + + next unless separate_deposits && goa.order_article.price.deposit > 0.0 + + goa_total_deposit = goa.result * goa.order_article.price.fc_deposit_price + data << ["zzgl. Pfand", + goa.result.to_i, + number_to_currency(goa.order_article.article.fc_deposit_price), + number_to_currency(goa_total_deposit)] + total_gross += goa_total_deposit + end + + table data, cell_style: { size: fontsize(8), overflow: :shrink_to_fit } do |table| + table.header = true + table.position = :center + table.cells.border_width = 1 + table.cells.border_color = '666666' + + table.row(0).column(0..4).width = 80 + table.row(0).border_bottom_width = 2 + table.columns(1).align = :right + table.columns(1..6).align = :right + end + + move_down 5 + sum = [] + sum << [nil, nil, I18n.t('documents.group_order_invoice_pdf.sum_to_pay_gross'), number_to_currency(total_gross)] + # table for sum + table sum, cell_style: { size: fontsize(8), overflow: :shrink_to_fit } do |table| + table.header = true + table.position = :center + table.cells.border_width = 1 + table.cells.border_color = '666666' + table.row(0).columns(2..4).style(align: :bottom) + table.row(0).border_bottom_width = 2 + table.row(0..-1).columns(0..1).border_width = 0 + + table.rows(0..-1).columns(0..4).width = 80 + table.row(0).column(-1).style(font_style: :bold) + table.row(0).column(-2).style(font_style: :bold) + table.row(0).column(-1).size = fontsize(10) + table.row(0).column(-2).size = fontsize(10) + + table.columns(1).align = :right + table.columns(1..6).align = :right + end + + move_down 25 + text I18n.t('documents.group_order_invoice_pdf.small_business_regulation') + move_down 10 + end + + def body_with_vat + separate_deposits = FoodsoftConfig[:group_order_invoices]&.[](:separate_deposits) + total_gross = 0 + total_net = 0 + # Articles + + tax_hash_net = Hash.new(0) # for summing up article net prices grouped into vat percentage + tax_hash_gross = Hash.new(0) # same here with gross prices + tax_hash_fc = Hash.new(0) + tax_hash_deposit_total = Hash.new(0) # for summing up deposit prices grouped into vat percentage + if separate_deposits + total_deposit = 0 + total_deposit_fc = 0 + total_deposit_gross = 0 + + tax_hash_deposit_net = Hash.new(0) # same here with gross prices + tax_hash_deposit_gross = Hash.new(0) # for summing up deposit gross prices grouped into vat percentage + tax_hash_deposit_fc = Hash.new(0) + end + + marge = FoodsoftConfig[:price_markup] + + # data table looks different when price_markup > 0 + data = if marge == 0 + [I18n.t('documents.group_order_invoice_pdf.no_price_markup_rows')] + else + [I18n.t('documents.group_order_invoice_pdf.price_markup_rows', marge: marge)] + end + goa_tax_hash = GroupOrderArticle.where(group_order_id: @group_order.id).find_each.group_by { |oat| oat.order_article.price.tax } + goa_tax_hash.each do |tax, group_order_articles| + group_order_articles.each do |goa| + # if no unit is received, nothing is to be charged + next if goa.result.to_i == 0 + + order_article = goa.order_article + goa_total_net = goa.result * order_article.price.price + goa_total_fc = separate_deposits ? goa.total_price_without_deposit : goa.total_price + goa_total = goa.total_price + + data << [order_article.article.name, + goa.result.to_i, + number_to_currency(order_article.price.price), + number_to_currency(goa_total_net), + tax.to_s + '%', + number_to_currency(goa_total_fc)] + + if separate_deposits && order_article.price.deposit > 0.0 + goa_deposit = goa.result * order_article.price.net_deposit_price + goa_total_deposit = goa.result * order_article.price.fc_deposit_price + + data << ["zzgl. Pfand", + goa.result.to_i, + number_to_currency(order_article.price.net_deposit_price), + number_to_currency(goa_deposit), + tax.to_s + '%', + number_to_currency(goa_total_deposit)] + + total_deposit += goa_deposit + total_deposit_fc += goa_total_deposit + total_deposit_gross += goa.result * order_article.price.gross_price + + tax_hash_deposit_net[tax.to_i] += goa_deposit + tax_hash_deposit_gross[tax.to_i] += goa.result * order_article.price.deposit + tax_hash_deposit_total[tax.to_i] += goa_total_deposit + end + + tax_hash_net[tax.to_i] += goa_total_net + tax_hash_fc[tax.to_i] += goa_total + tax_hash_gross[tax.to_i] += goa.result * order_article.price.gross_price + + total_net += goa_total_net + total_gross += goa_total_fc + end + end + + # Two separate tables for sum and individual data + # article information + data + table data, cell_style: { size: fontsize(8), overflow: :shrink_to_fit } do |table| + table.header = true + table.position = :center + table.cells.border_width = 1 + table.cells.border_color = '666666' + table.row(0).columns(0..6).style(background_color: 'cccccc', font_style: :bold) + table.rows(0..-1).columns(0..6).width = 80 + table.row(0).border_bottom_width = 2 + table.columns(1).align = :right + table.columns(1..6).align = :right + end + + sum = if marge > 0 + [[nil, nil, "Netto", "MwSt", "FC marge", "Brutto"]] + else + [[nil, nil, nil, "Netto", "MwSt", "Brutto"]] + end + + tax_hash_net.each_key do |key| + if tax_hash_gross[key] > 0 + tmp_sum_array = [nil, "Produkte mit #{key}%", number_to_currency(tax_hash_net[key]), number_to_currency(tax_hash_gross[key] - tax_hash_net[key])] + if marge > 0 + tmp_sum_array << number_to_currency(tax_hash_fc[key] - tax_hash_gross[key]) + else + tmp_sum_array.unshift(nil) + end + tmp_sum_array << number_to_currency(tax_hash_fc[key]) + sum << tmp_sum_array + end + + if separate_deposits && (tax_hash_deposit_total[key] > 0) + tmp_sum_array = [nil, "Pfand mit #{key}%", number_to_currency(tax_hash_deposit_net[key]), number_to_currency(tax_hash_deposit_gross[key] - tax_hash_deposit_net[key])] + if marge > 0 + tmp_sum_array << number_to_currency(tax_hash_deposit_total[key] - tax_hash_deposit_gross[key]) + else + tmp_sum_array.unshift(nil) + end + tmp_sum_array << number_to_currency(tax_hash_deposit_total[key]) + sum << tmp_sum_array + end + end + + total_deposit_fc ||= 0 + tmp_total_sum = [nil, nil, nil, nil] + + tmp_total_sum << I18n.t('documents.group_order_invoice_pdf.sum_to_pay_gross') + tmp_total_sum << number_to_currency(total_gross + total_deposit_fc) + sum << tmp_total_sum + + move_down 10 + table sum, cell_style: { size: fontsize(8), overflow: :shrink_to_fit } do |table| + table.header = true + table.position = :center + table.cells.border_width = 1 + table.cells.border_color = '666666' + table.row(0).columns(2..7).style(align: :bottom) + table.row(0).border_bottom_width = 2 + table.row(0..-1).column(0).border_width = 0 + + table.rows(0..-1).columns(0..7).width = 80 + + + table.row(-1).column(-1).style(font_style: :bold) + table.row(-1).column(-2).style(font_style: :bold) + table.row(-1).column(-1).size = fontsize(10) + table.row(-1).column(-2).size = fontsize(10) + + table.columns(1).align = :right + table.columns(1..7).align = :right + end + + if FoodsoftConfig[:group_order_invoices][:vat_exempt] + move_down 15 + text I18n.t('documents.group_order_invoice_pdf.small_business_regulation') + end + move_down 10 + end +end diff --git a/plugins/automatic_invoices/app/jobs/notify_group_order_invoice_job.rb b/plugins/automatic_invoices/app/jobs/notify_group_order_invoice_job.rb new file mode 100644 index 00000000..1a17fe9a --- /dev/null +++ b/plugins/automatic_invoices/app/jobs/notify_group_order_invoice_job.rb @@ -0,0 +1,10 @@ +class NotifyGroupOrderInvoiceJob < ApplicationJob + def perform(group_order_invoice) + ordergroup = group_order_invoice.group_order.ordergroup + ordergroup.users.each do |user| + Mailer.deliver_now_with_user_locale user do + Mailer.group_order_invoice(group_order_invoice, user) + end + end + end +end diff --git a/plugins/automatic_invoices/app/lib/render_pdf.rb b/plugins/automatic_invoices/app/lib/render_pdf.rb new file mode 100644 index 00000000..a4974fdf --- /dev/null +++ b/plugins/automatic_invoices/app/lib/render_pdf.rb @@ -0,0 +1,170 @@ +require 'prawn/measurement_extensions' + +class RotatedCell < Prawn::Table::Cell::Text + def initialize(pdf, text, options = {}) + options[:content] = text + options[:valign] = :center + options[:align] = :center + options[:rotate_around] = :center + @rotation = -options[:rotate] || 0 + super(pdf, [0, pdf.cursor], options) + end + + def tan_rotation + Math.tan(Math::PI * @rotation / 180) + end + + def skew + (height + (border_top_width / 2.0) + (border_bottom_width / 2.0)) / tan_rotation + end + + def styled_width_of(_text) + options = @text_options.reject { |k| k == :style } + with_font { (@pdf.height_of(@content, options) + padding_top + padding_bottom) / tan_rotation } + end + + def natural_content_height + options = @text_options.reject { |k| k == :style } + with_font { (@pdf.width_of(@content, options) + padding_top + padding_bottom) * tan_rotation } + end + + def draw_borders(point) + @pdf.mask(:line_width, :stroke_color) do + x, y = point + from = [[x - skew, y + (border_top_width / 2.0)], + to = [x, y - height - (border_bottom_width / 2.0)]] + + @pdf.line_width = @border_widths[3] + @pdf.stroke_color = @border_colors[3] + @pdf.stroke_line(from, to) + @pdf.undash + end + end + + def draw_content + with_font do + with_text_color do + text_box(width: spanned_content_width + FPTolerance + skew, + height: spanned_content_height + FPTolerance, + at: [1 - skew, @pdf.cursor]).render + end + end + end +end + +class RenderPdf < Prawn::Document + include ActionView::Helpers::NumberHelper + include ApplicationHelper + + TOP_MARGIN = 36 + BOTTOM_MARGIN = 23 + HEADER_SPACE = 9 + FOOTER_SPACE = 3 + HEADER_FONT_SIZE = 16 + FOOTER_FONT_SIZE = 8 + DEFAULT_FONT = 'OpenSans' + + def initialize(options = {}) + options[:font_size] ||= FoodsoftConfig[:pdf_font_size].try(:to_f) || 12 + options[:page_size] ||= FoodsoftConfig[:pdf_page_size] || 'A4' + options[:skip_page_creation] = true + @options = options + @first_page = true + no_footer = @options&.[](:no_footer) ? true : false + super(options) + + # Use ttf for better utf-8 compability + font_families.update( + 'OpenSans' => { + bold: font_path('OpenSans-Bold.ttf'), + italic: font_path('OpenSans-Italic.ttf'), + bold_italic: font_path('OpenSans-BoldItalic.ttf'), + normal: font_path('OpenSans-Regular.ttf') + } + ) + + header = options[:title] || title + footer = I18n.l(Time.now, format: :long) unless no_footer + + header_size = 0 + header_size = height_of(header, size: HEADER_FONT_SIZE, font: DEFAULT_FONT) + HEADER_SPACE if header + footer_size = no_footer ? 0 : height_of(footer, size: FOOTER_FONT_SIZE, font: DEFAULT_FONT) + FOOTER_SPACE + + start_new_page(top_margin: TOP_MARGIN + header_size, bottom_margin: BOTTOM_MARGIN + footer_size) + + font DEFAULT_FONT + + repeat :all, dynamic: true do + bounding_box [bounds.left, bounds.top + header_size], width: bounds.width, height: header_size do + text header, size: HEADER_FONT_SIZE, align: :center, overflow: :shrink_to_fit if header + end + + unless no_footer + font_size FOOTER_FONT_SIZE do + bounding_box [bounds.left, bounds.bottom - FOOTER_SPACE], width: bounds.width, height: footer_size do + text footer, align: :left, valign: :bottom + end + bounding_box [bounds.left, bounds.bottom - FOOTER_SPACE], width: bounds.width, height: footer_size do + text I18n.t('lib.render_pdf.page', number: page_number, count: page_count), align: :right, valign: :bottom + end + end + end + end + end + + def title + nil + end + + def to_pdf + body # Add content, which is defined in subclasses + render # Render pdf + end + + # @todo avoid underscore instead of unicode whitespace in pdf :/ + def number_to_currency(number, options = {}) + super(number, options).gsub("\u202f", ' ') if number + end + + def font_size(points = nil, &block) + points *= @options[:font_size] / 12 if points + super(points, &block) + end + + # add pagebreak or vertical whitespace, depending on configuration + def down_or_page(space = 10) + if @first_page + @first_page = false + return + end + if pdf_add_page_breaks? + start_new_page + else + move_down space + end + end + + protected + + def fontsize(size) + size + end + + # return whether pagebreak or vertical whitespace is used for breaks + def pdf_add_page_breaks?(docid = nil) + docid ||= self.class.name.underscore + cfg = FoodsoftConfig[:pdf_add_page_breaks] + case cfg + when Array + cfg.index(docid.to_s).any? + when Hash + cfg[docid.to_s] + else + cfg + end + end + + def font_path(name) + Rails.root.join('vendor', 'assets', 'fonts', name) + end +end diff --git a/plugins/automatic_invoices/app/models/group_order_invoice.rb b/plugins/automatic_invoices/app/models/group_order_invoice.rb new file mode 100644 index 00000000..21557161 --- /dev/null +++ b/plugins/automatic_invoices/app/models/group_order_invoice.rb @@ -0,0 +1,58 @@ +class GroupOrderInvoice < ApplicationRecord + belongs_to :group_order + validates_presence_of :group_order + validates_uniqueness_of :invoice_number + validate :tax_number_set + after_initialize :init, unless: :persisted? + + def generate_invoice_number(count) + trailing_number = count.to_s.rjust(4, '0') + if GroupOrderInvoice.find_by(invoice_number: self.invoice_date.strftime("%Y%m%d") + trailing_number) + generate_invoice_number(count.to_i + 1) + else + self.invoice_date.strftime("%Y%m%d") + trailing_number + end + end + + def tax_number_set + if FoodsoftConfig[:contact][:tax_number].blank? + errors.add(:group_order_invoice, "Keine Steuernummer in FoodsoftConfig :contact gesetzt") + end + end + + def init + self.invoice_date = Time.now unless invoice_date + self.invoice_number = generate_invoice_number(1) unless self.invoice_number + self.payment_method = FoodsoftConfig[:group_order_invoices]&.[](:payment_method) || I18n.t('activerecord.attributes.group_order_invoice.payment_method') unless self.payment_method + end + + def name + I18n.t('activerecord.attributes.group_order_invoice.name') + "_#{invoice_number}" + end + + def load_data_for_invoice + invoice_data = {} + order = group_order.order + invoice_data[:supplier] = order.supplier.name + invoice_data[:ordergroup] = group_order.ordergroup + invoice_data[:group_order] = group_order + invoice_data[:invoice_number] = invoice_number + invoice_data[:invoice_date] = invoice_date + invoice_data[:tax_number] = FoodsoftConfig[:contact][:tax_number] + invoice_data[:payment_method] = payment_method + invoice_data[:order_articles] = {} + group_order.order_articles.each do |order_article| + # Get the result of last time ordering, if possible + goa = group_order.group_order_articles.detect { |tmp_goa| tmp_goa.order_article_id == order_article.id } + + # Build hash with relevant data + invoice_data[:order_articles][order_article.id] = { + :price => order_article.article.fc_price, + :quantity => (goa ? goa.quantity : 0), + :total_price => (goa ? goa.total_price : 0), + :tax => order_article.article.tax + } + end + invoice_data + end +end diff --git a/plugins/automatic_invoices/app/overrides/admin/configs/_tab_foodcoop/insert_config_option.html.haml.deface b/plugins/automatic_invoices/app/overrides/admin/configs/_tab_foodcoop/insert_config_option.html.haml.deface new file mode 100644 index 00000000..9846f674 --- /dev/null +++ b/plugins/automatic_invoices/app/overrides/admin/configs/_tab_foodcoop/insert_config_option.html.haml.deface @@ -0,0 +1,3 @@ +/ insert_after 'erb:contains("phone")' +- if FoodsoftAutomaticInvoices.enabled? + = config_input c, :tax_number, input_html: {class: 'input-medium'} \ No newline at end of file diff --git a/plugins/automatic_invoices/app/overrides/admin/configs/_tab_payment/insert_config_option.html.haml.deface b/plugins/automatic_invoices/app/overrides/admin/configs/_tab_payment/insert_config_option.html.haml.deface new file mode 100644 index 00000000..171fe4ea --- /dev/null +++ b/plugins/automatic_invoices/app/overrides/admin/configs/_tab_payment/insert_config_option.html.haml.deface @@ -0,0 +1,8 @@ +/ insert_after 'erb:contains(":use_self_service")' +- if FoodsoftAutomaticInvoices.enabled? + %h4= t '.group_order_invoices' + = form.fields_for :group_order_invoices do |field| + = config_input field, :use_automatic_invoices, as: :boolean + = config_input field, :separate_deposits, as: :boolean + = config_input field, :vat_exempt, as: :boolean + = config_input field, :payment_method, as: :string, input_html: {class: 'input-medium'} diff --git a/plugins/automatic_invoices/app/overrides/admin/ordergroups/_form/insert_customer_number.html.haml.deface b/plugins/automatic_invoices/app/overrides/admin/ordergroups/_form/insert_customer_number.html.haml.deface new file mode 100644 index 00000000..f266509b --- /dev/null +++ b/plugins/automatic_invoices/app/overrides/admin/ordergroups/_form/insert_customer_number.html.haml.deface @@ -0,0 +1,3 @@ +/ insert_after 'erb:contains(":contact_person")' +- if FoodsoftAutomaticInvoices.enabled? + = f.input :customer_number diff --git a/plugins/automatic_invoices/app/overrides/controllers/finance/balancing_controller.rb b/plugins/automatic_invoices/app/overrides/controllers/finance/balancing_controller.rb new file mode 100644 index 00000000..b62734b8 --- /dev/null +++ b/plugins/automatic_invoices/app/overrides/controllers/finance/balancing_controller.rb @@ -0,0 +1,27 @@ +if FoodsoftAutomaticInvoices.enabled? + Finance::BalancingController.class_eval do + def close + @order = Order.find(params[:id]) + @type = FinancialTransactionType.find_by_id(params.permit(:type)[:type]) + @order.close!(@current_user, @type) + note = t('finance.balancing.close.notice') + if @order.closed? + alert = t('finance.balancing.close.alert') + if FoodsoftConfig[:group_order_invoices]&.[](:use_automatic_invoices) + @order.group_orders.each do |go| + alert = t('finance.balancing.close.settings_not_set') + goi = GroupOrderInvoice.find_or_create_by!(group_order_id: go.id) + if goi.save! + NotifyGroupOrderInvoiceJob.perform_later(goi) + note = t('finance.balancing.close.notice_mail') + end + end + end + end + alert ||= t('finance.balancing.close.alert') + redirect_to finance_order_index_url, notice: note + rescue => error + redirect_to new_finance_order_url(order_id: @order.id), notice: note, alert: alert, msg: error.message + end + end +end diff --git a/plugins/automatic_invoices/app/overrides/finance/balancing/_orders/insert_generate_invoice.html.haml.deface b/plugins/automatic_invoices/app/overrides/finance/balancing/_orders/insert_generate_invoice.html.haml.deface new file mode 100644 index 00000000..f76b4b0a --- /dev/null +++ b/plugins/automatic_invoices/app/overrides/finance/balancing/_orders/insert_generate_invoice.html.haml.deface @@ -0,0 +1,4 @@ +/ insert_after 'erb:contains(":updated_by")' +- if FoodsoftAutomaticInvoices.enabled? + %th= heading_helper GroupOrderInvoice, :name + %th \ No newline at end of file diff --git a/plugins/automatic_invoices/app/overrides/finance/balancing/_orders/insert_links.html.haml.deface b/plugins/automatic_invoices/app/overrides/finance/balancing/_orders/insert_links.html.haml.deface new file mode 100644 index 00000000..a25ceb8b --- /dev/null +++ b/plugins/automatic_invoices/app/overrides/finance/balancing/_orders/insert_links.html.haml.deface @@ -0,0 +1,10 @@ +/ insert_after 'erb:contains("show_user(order.updated_by)")' +- if FoodsoftAutomaticInvoices.enabled? + %td + - if order.closed? + -if FoodsoftConfig[:contact][:tax_number] && order.ordergroups.present? + = render :partial => 'group_order_invoices/links', locals:{order: order} + -else + = I18n.t('activerecord.attributes.group_order_invoice.tax_number_not_set') + - else + = t('orders.index.not_closed') diff --git a/plugins/automatic_invoices/app/overrides/finance/balancing/_results_section/replace_contents_order_article.html.haml.deface b/plugins/automatic_invoices/app/overrides/finance/balancing/_results_section/replace_contents_order_article.html.haml.deface new file mode 100644 index 00000000..720768af --- /dev/null +++ b/plugins/automatic_invoices/app/overrides/finance/balancing/_results_section/replace_contents_order_article.html.haml.deface @@ -0,0 +1,3 @@ +/ replace 'erb:contains("edit_results_by_articles")' +- if FoodsoftAutomaticInvoices.enabled? + = render :partial => 'finance/balancing/edit_results_by_articles_override' \ No newline at end of file diff --git a/plugins/automatic_invoices/app/overrides/finance/balancing/_summary/replace_gross_amount.html.haml.deface b/plugins/automatic_invoices/app/overrides/finance/balancing/_summary/replace_gross_amount.html.haml.deface new file mode 100644 index 00000000..b0b86a4c --- /dev/null +++ b/plugins/automatic_invoices/app/overrides/finance/balancing/_summary/replace_gross_amount.html.haml.deface @@ -0,0 +1,21 @@ +/ replace_contents "tr.gross-amount" +- if FoodsoftConfig[:group_order_invoices]&.[](:separate_deposits) + %tr + %td= t('.gross_amount') + %td.numeric= number_to_currency(order.sum(:gross_without_deposit)) + %tr + %td= t('.fc_amount_without_deposit') + %td.numeric= number_to_currency(order.sum(:fc_without_deposit)) + %tr + %td= t('.deposit') + %td.numeric= number_to_currency(order.sum(:deposit)) + %tr + %td= t('.net_deposit') + %td.numeric= number_to_currency(order.sum(:net_deposit)) + %tr + %td= t('.fc_deposit') + %td.numeric= number_to_currency(order.sum(:fc_deposit)) +- else + %tr + %td= t('.gross_amount') + %td.numeric= number_to_currency(order.sum(:gross)) \ No newline at end of file diff --git a/plugins/automatic_invoices/app/overrides/helpers/finance/balancing_helper_override.rb b/plugins/automatic_invoices/app/overrides/helpers/finance/balancing_helper_override.rb new file mode 100644 index 00000000..2a151ae2 --- /dev/null +++ b/plugins/automatic_invoices/app/overrides/helpers/finance/balancing_helper_override.rb @@ -0,0 +1,15 @@ +if FoodsoftAutomaticInvoices.enabled? + Finance::BalancingHelper.class_eval do + def balancing_view_partial + view = params[:view] || 'edit_results' + case view + when 'edit_results' + 'edit_results_by_articles_override' + when 'groups_overview' + 'shared/articles_by/groups' + when 'articles_overview' + 'shared/articles_by/articles' + end + end + end +end diff --git a/plugins/automatic_invoices/app/overrides/mailers/mailer_override.rb b/plugins/automatic_invoices/app/overrides/mailers/mailer_override.rb new file mode 100644 index 00000000..9102d960 --- /dev/null +++ b/plugins/automatic_invoices/app/overrides/mailers/mailer_override.rb @@ -0,0 +1,20 @@ +if FoodsoftAutomaticInvoices.enabled? + Mailer.class_eval do + # Sends automatically generated invoicesfor group orders to ordergroup members + def add_group_order_invoice_attachments(group_order_invoice) + attachment_name = group_order_invoice.name + '.pdf' + attachments[attachment_name] = GroupOrderInvoicePdf.new(group_order_invoice.load_data_for_invoice).to_pdf + end + + def group_order_invoice(group_order_invoice, user) + @user = user + @group_order_invoice = group_order_invoice + @group_order = group_order_invoice.group_order + @supplier = @group_order.order.supplier.name + @group = @group_order.ordergroup + add_group_order_invoice_attachments(group_order_invoice) + mail to: user, + subject: I18n.t('mailer.group_order_invoice.subject', group: @group.name, supplier: @supplier) + end + end +end diff --git a/plugins/automatic_invoices/app/overrides/models/concerns/price_calculation_override.rb b/plugins/automatic_invoices/app/overrides/models/concerns/price_calculation_override.rb new file mode 100644 index 00000000..23411028 --- /dev/null +++ b/plugins/automatic_invoices/app/overrides/models/concerns/price_calculation_override.rb @@ -0,0 +1,31 @@ +if FoodsoftAutomaticInvoices.enabled? + + PriceCalculation.class_eval do + # deposit is always gross + def gross_price + add_percent(price, tax) + deposit + end + + def gross_price_without_deposit + add_percent(price, tax) + end + + def net_deposit_price + remove_percent(deposit, tax) + end + + def fc_price_without_deposit + add_percent(gross_price_without_deposit, FoodsoftConfig[:price_markup].to_i) + end + + def fc_deposit_price + add_percent(deposit, FoodsoftConfig[:price_markup].to_i) + end + + private + + def remove_percent(value, percent) + (value / ((percent * 0.01) + 1)).round(2) + end + end +end diff --git a/plugins/automatic_invoices/app/overrides/models/group_order_article_override.rb b/plugins/automatic_invoices/app/overrides/models/group_order_article_override.rb new file mode 100644 index 00000000..887ed7ae --- /dev/null +++ b/plugins/automatic_invoices/app/overrides/models/group_order_article_override.rb @@ -0,0 +1,15 @@ +if FoodsoftAutomaticInvoices.enabled? + GroupOrderArticle.class_eval do + def total_price_without_deposit(order_article = self.order_article) + if order_article.order.open? + if FoodsoftConfig[:tolerance_is_costly] + order_article.price.fc_price_without_deposit * (quantity + tolerance) + else + order_article.price.fc_price_without_deposit * quantity + end + else + order_article.price.fc_price_without_deposit * result + end + end + end +end diff --git a/plugins/automatic_invoices/app/overrides/models/group_order_override.rb b/plugins/automatic_invoices/app/overrides/models/group_order_override.rb new file mode 100644 index 00000000..e560af56 --- /dev/null +++ b/plugins/automatic_invoices/app/overrides/models/group_order_override.rb @@ -0,0 +1,5 @@ +if FoodsoftAutomaticInvoices.enabled? + GroupOrder.class_eval do + has_one :group_order_invoice + end +end diff --git a/plugins/automatic_invoices/app/overrides/models/order_article_override.rb b/plugins/automatic_invoices/app/overrides/models/order_article_override.rb new file mode 100644 index 00000000..bd6be1b6 --- /dev/null +++ b/plugins/automatic_invoices/app/overrides/models/order_article_override.rb @@ -0,0 +1,15 @@ +if FoodsoftAutomaticInvoices.enabled? + OrderArticle.class_eval do + def total_gross_price_without_deposit + units * price.unit_quantity * price.gross_price_without_deposit + end + + def total_deposit_price + units * price.unit_quantity * price.deposit + end + + def total_price_without_deposit + units * price.unit_quantity * price.fc_price_without_deposit + end + end +end diff --git a/plugins/automatic_invoices/app/overrides/models/order_override.rb b/plugins/automatic_invoices/app/overrides/models/order_override.rb new file mode 100644 index 00000000..c27d1480 --- /dev/null +++ b/plugins/automatic_invoices/app/overrides/models/order_override.rb @@ -0,0 +1,50 @@ +if FoodsoftAutomaticInvoices.enabled? + class Order < ApplicationRecord + # Returns the all round price of a finished order + # :groups returns the sum of all GroupOrders + # :clear returns the price without tax, deposit and markup + # :gross includes tax and deposit(gross). this amount should be equal to suppliers bill + # :gross_without_deposit excludes the depost from the gross price + # :net_deposit returns the deposit without tax (deposit entered by user is default gross) + # :fc_deposit_price returns the deposit with markup + # :fc, guess what... + def sum(type = :gross) + total = 0 + if %i[net gross net_deposit gross_without_deposit fc_without_deposit fc_deposit deposit fc].include?(type) + for oa in order_articles.ordered.includes(:article, :article_price) + quantity = oa.units * oa.price.unit_quantity + case type + when :net + total += quantity * oa.price.price + when :gross + total += quantity * oa.price.gross_price + when :gross_without_deposit + total += quantity * oa.price.gross_price_without_deposit + when :fc + total += quantity * oa.price.fc_price + when :fc_without_deposit + total += quantity * oa.price.fc_price_without_deposit + when :net_deposit + total += quantity * oa.price.net_deposit_price + when :fc_deposit + total += quantity * oa.price.fc_deposit_price + when :deposit + total += quantity * oa.price.deposit + end + end + elsif %i[groups groups_without_markup].include?(type) + for go in group_orders.includes(group_order_articles: { order_article: %i[article article_price] }) + for goa in go.group_order_articles + case type + when :groups + total += goa.result * goa.order_article.price.fc_price + when :groups_without_markup + total += goa.result * goa.order_article.price.gross_price + end + end + end + end + total + end + end +end diff --git a/plugins/automatic_invoices/app/overrides/ordergroups/edit/insert_customer_number_.html.haml.deface b/plugins/automatic_invoices/app/overrides/ordergroups/edit/insert_customer_number_.html.haml.deface new file mode 100644 index 00000000..0bf025fa --- /dev/null +++ b/plugins/automatic_invoices/app/overrides/ordergroups/edit/insert_customer_number_.html.haml.deface @@ -0,0 +1,6 @@ +/ insert_after 'erb:contains(":contact_person")' +- if FoodsoftAutomaticInvoices.enabled? + %p + = f.label :customer_number + %br/ + = f.text_field :customer_number \ No newline at end of file diff --git a/plugins/automatic_invoices/app/overrides/orders/_articles/insert_deposit_changes.html.haml.deface b/plugins/automatic_invoices/app/overrides/orders/_articles/insert_deposit_changes.html.haml.deface new file mode 100644 index 00000000..13b62bd1 --- /dev/null +++ b/plugins/automatic_invoices/app/overrides/orders/_articles/insert_deposit_changes.html.haml.deface @@ -0,0 +1,5 @@ +/ insert_before 'erb:contains("t \'.prices\'")' +- if FoodsoftAutomaticInvoices.enabled? + - if FoodsoftConfig[:group_order_invoices]&.[](:separate_deposits) + %th= t '.deposit' + diff --git a/plugins/automatic_invoices/app/overrides/orders/_articles/insert_deposit_data.html.haml.deface b/plugins/automatic_invoices/app/overrides/orders/_articles/insert_deposit_data.html.haml.deface new file mode 100644 index 00000000..c4bccf11 --- /dev/null +++ b/plugins/automatic_invoices/app/overrides/orders/_articles/insert_deposit_data.html.haml.deface @@ -0,0 +1,5 @@ + +/ insert_after 'erb:contains("number_to_currency(gross_price)")' +- if FoodsoftAutomaticInvoices.enabled? + - if FoodsoftConfig[:group_order_invoices]&.[](:separate_deposits) + %td= number_to_currency(order_article.price.deposit) \ No newline at end of file diff --git a/plugins/automatic_invoices/app/overrides/orders/show/insert_deposit_info.html.haml.deface b/plugins/automatic_invoices/app/overrides/orders/show/insert_deposit_info.html.haml.deface new file mode 100644 index 00000000..05ec5b6c --- /dev/null +++ b/plugins/automatic_invoices/app/overrides/orders/show/insert_deposit_info.html.haml.deface @@ -0,0 +1,5 @@ + +/ insert_after 'erb:contains(".description2")' +- if FoodsoftAutomaticInvoices.enabled? + - if FoodsoftConfig[:group_order_invoices]&.[](:separate_deposits) + = t '.description3', net_deposit: number_to_currency(@order.sum(:net_deposit)), deposit: number_to_currency(@order.sum(:deposit)) \ No newline at end of file diff --git a/plugins/automatic_invoices/app/overrides/shared/_group/insert_customer_number.html.haml.deface b/plugins/automatic_invoices/app/overrides/shared/_group/insert_customer_number.html.haml.deface new file mode 100644 index 00000000..290ee31d --- /dev/null +++ b/plugins/automatic_invoices/app/overrides/shared/_group/insert_customer_number.html.haml.deface @@ -0,0 +1,4 @@ +/ insert_after 'erb:contains(" group.contact_address")' +- if FoodsoftAutomaticInvoices.enabled? + %dt= heading_helper(Ordergroup, :customer_number) + ':' + %dd=h group.customer_number diff --git a/plugins/automatic_invoices/app/views/finance/balancing/_edit_results_by_articles_override.html.haml b/plugins/automatic_invoices/app/views/finance/balancing/_edit_results_by_articles_override.html.haml new file mode 100644 index 00000000..87c56111 --- /dev/null +++ b/plugins/automatic_invoices/app/views/finance/balancing/_edit_results_by_articles_override.html.haml @@ -0,0 +1,65 @@ +-# :javascript destroy deface functionality +- content_for :javascript do + :javascript + $(function() { + // create List for search-feature (using list.js, http://listjs.com) + var listjsResetPlugin = ['reset', {highlightClass: 'btn-primary'}]; + var listjsDelayPlugin = ['delay', {delayedSearchTime: 500}]; + new List(document.body, { + valueNames: ['name'], + engine: 'unlist', + plugins: [listjsResetPlugin, listjsDelayPlugin], + // make large pages work too (as we don't have paging - articles may disappear!) + page: 10000, + indexAsync: true + }); + $('input').keydown(function(event){ + if(event.keyCode == 13) { + event.preventDefault(); + return false; + } + }); + }); + + + +%table.ordered-articles.table.table-striped + %thead + %tr + %th + .input-append + = text_field_tag :article, params[:article], placeholder: (heading_helper Article, :name), class: 'delayed-search resettable search-query' + %th= heading_helper Article, :order_number + %th= t('.amount') + %th= heading_helper Article, :unit + %th= t('.net') + %th= t('.gross') + %th= t('.fc') + %th= heading_helper Article, :deposit + %th= heading_helper Article, :tax + %th{:colspan => "2"} + - unless @order.closed? + .btn-group + = link_to t('.add_article'), new_order_order_article_path(@order), remote: true, + class: 'btn btn-small' + = link_to '#', data: {toggle: 'dropdown'}, class: 'btn btn-small dropdown-toggle' do + %span.caret + %ul.dropdown-menu + %li= link_to t('.add_article'), new_order_order_article_path(@order), remote: true + %li= link_to t('.edit_transport'), edit_transport_finance_order_path(@order), remote: true + %tbody.list#result_table + - for order_article in @articles.select { |oa| oa.units > 0 } + = render :partial => "order_article_result_override", :locals => {:order_article => order_article} + + %tr + %td{ colspan: 10 } The following were not ordered + + - for order_article in @articles.select { |oa| oa.units == 0 } + = render :partial => "order_article_result_override", :locals => {:order_article => order_article} + + - if @order.transport + %tr + %td{ colspan: 5 }= heading_helper Order, :transport + %td{ colspan: 3, data: {value: @order.transport} }= number_to_currency(@order.transport) + %td= link_to t('ui.edit'), edit_transport_finance_order_path(@order), remote: true, + class: 'btn btn-mini' unless order_article.order.closed? diff --git a/plugins/automatic_invoices/app/views/finance/balancing/_order_article_override.html.haml b/plugins/automatic_invoices/app/views/finance/balancing/_order_article_override.html.haml new file mode 100644 index 00000000..0fd3a8f8 --- /dev/null +++ b/plugins/automatic_invoices/app/views/finance/balancing/_order_article_override.html.haml @@ -0,0 +1,42 @@ +%td.closed.name + = link_to order_article.article.name, '#', 'data-toggle-this' => "#group_order_articles_#{order_article.id}" +%td= order_article.article.order_number +%td{title: units_history_line(order_article, :plain => true)} + = order_article.units + = pkg_helper order_article.article_price + - if s=order_article.ordered_quantities_different_from_group_orders? + %span{:style => "color:red;font-weight: bold"}= s +%td #{order_article.article.unit} +%td + = number_to_currency(order_article.price.price, :unit => "") + :plain + / + = number_to_currency(order_article.total_price, :unit => "") +%td + - if FoodsoftConfig[:group_order_invoices]&.[](:separate_deposits) + = number_to_currency(order_article.price.gross_price_without_deposit, :unit => "") + :plain + / + = number_to_currency(order_article.total_gross_price_without_deposit, :unit => "") + -else + = number_to_currency(order_article.price.gross_price, :unit => "") + :plain + / + = number_to_currency(order_article.total_gross_price, :unit => "") +%td + = number_to_currency(order_article.price.fc_price_without_deposit, :unit => "") + :plain + / + = number_to_currency(order_article.total_price_without_deposit, :unit => "") +%td + = number_to_currency(order_article.price.deposit, :unit => "") unless order_article.price.deposit.zero? + :plain + / + = number_to_currency(order_article.total_deposit_price, :unit => "") unless order_article.price.deposit.zero? +%td= number_to_percentage(order_article.price.tax) unless order_article.price.tax.zero? +%td + = link_to t('ui.edit'), edit_order_order_article_path(order_article.order, order_article), remote: true, + class: 'btn btn-mini' unless order_article.order.closed? +%td + = link_to t('ui.delete'), order_order_article_path(order_article.order, order_article), method: :delete, + remote: true, data: {confirm: t('.confirm')}, class: 'btn btn-danger btn-mini' unless order_article.order.closed? diff --git a/plugins/automatic_invoices/app/views/finance/balancing/_order_article_result_override.html.haml b/plugins/automatic_invoices/app/views/finance/balancing/_order_article_result_override.html.haml new file mode 100644 index 00000000..2f531977 --- /dev/null +++ b/plugins/automatic_invoices/app/views/finance/balancing/_order_article_result_override.html.haml @@ -0,0 +1,5 @@ +%tr[order_article] + = render :partial => 'finance/balancing/order_article_override', :locals => {:order_article => order_article} + +%tr{:id => "group_order_articles_#{order_article.id}", :class => "results", :style => "display:none"} + = render :partial => 'finance/balancing/group_order_articles', :locals => {:order_article => order_article} \ No newline at end of file diff --git a/plugins/automatic_invoices/app/views/group_order_invoices/_links.html.haml b/plugins/automatic_invoices/app/views/group_order_invoices/_links.html.haml new file mode 100644 index 00000000..9e55cedf --- /dev/null +++ b/plugins/automatic_invoices/app/views/group_order_invoices/_links.html.haml @@ -0,0 +1,29 @@ +.row + .column.small-12 + - show_generate_with_date = true + - order.group_orders.each do |go| + - if go.group_order_invoice.present? + - show_generate_with_date = false + - if show_generate_with_date + = form_for :group_order_invoice, url: url_for('group_order_invoice#create_multiple'), remote: true do |f| + = f.label :invoice_date, I18n.t('activerecord.attributes.group_order_invoice.links.invoice_date') + = f.date_field :invoice_date, {value: Date.today, max: Date.today, required: true} + = f.hidden_field :order_id, value: order.id + = f.submit I18n.t('activerecord.attributes.group_order_invoice.links.generate_with_date'), class: 'btn btn small' + +- order.group_orders.includes([:group_order_invoice, :ordergroup]).each do |go| + .row + .column.small-3 + = label_tag go.ordergroup.name + - if go.group_order_invoice + .column.small-3 + = link_to I18n.t('activerecord.attributes.group_order_invoice.links.download'), group_order_invoice_path(go.group_order_invoice, :format => 'pdf'), class: 'btn btn-small' + .column.small-3 + = link_to I18n.t('activerecord.attributes.group_order_invoice.links.delete'), go.group_order_invoice, method: :delete, class: 'btn btn-danger btn-small', remote: true + - else + = button_to I18n.t('activerecord.attributes.group_order_invoice.links.generate'), group_order_invoices_path(:method => :post, group_order: go) ,class: 'btn btn-small', params: {id: order.id}, remote: true +- if order.group_orders.map(&:group_order_invoice).compact.present? + %br/ + .row + .column.small-3 + = link_to I18n.t('activerecord.attributes.group_order_invoice.links.download_all_zip'), download_all_group_order_invoices_path(order), class: 'btn btn-small' diff --git a/plugins/automatic_invoices/app/views/group_order_invoices/create.js.erb b/plugins/automatic_invoices/app/views/group_order_invoices/create.js.erb new file mode 100644 index 00000000..5a43e85d --- /dev/null +++ b/plugins/automatic_invoices/app/views/group_order_invoices/create.js.erb @@ -0,0 +1 @@ +$("#generate-invoice<%= params[:id] %>").html("<%= escape_javascript(render partial: 'links', locals: {order: @order}) %>"); diff --git a/plugins/automatic_invoices/app/views/group_order_invoices/create_multiple.js.erb b/plugins/automatic_invoices/app/views/group_order_invoices/create_multiple.js.erb new file mode 100644 index 00000000..11ebbe45 --- /dev/null +++ b/plugins/automatic_invoices/app/views/group_order_invoices/create_multiple.js.erb @@ -0,0 +1 @@ +$("#generate-invoice<%= @order.id %>").html("<%= escape_javascript(render partial: 'links', locals: {order: @order}) %>"); diff --git a/plugins/automatic_invoices/app/views/group_order_invoices/destroy.js.erb b/plugins/automatic_invoices/app/views/group_order_invoices/destroy.js.erb new file mode 100644 index 00000000..30ce5985 --- /dev/null +++ b/plugins/automatic_invoices/app/views/group_order_invoices/destroy.js.erb @@ -0,0 +1 @@ +$("#generate-invoice<%= @order.id %>").html("<%= escape_javascript(render partial: 'links', locals: {order: @order}) %>"); \ No newline at end of file diff --git a/plugins/automatic_invoices/app/views/mailer/group_order_invoice.text.haml b/plugins/automatic_invoices/app/views/mailer/group_order_invoice.text.haml new file mode 100644 index 00000000..75948fbe --- /dev/null +++ b/plugins/automatic_invoices/app/views/mailer/group_order_invoice.text.haml @@ -0,0 +1 @@ += raw t '.text', group: @group.name, supplier: @supplier , foodcoop: FoodsoftConfig[:name] diff --git a/plugins/automatic_invoices/config/locales/de.yml b/plugins/automatic_invoices/config/locales/de.yml new file mode 100644 index 00000000..fcc338a2 --- /dev/null +++ b/plugins/automatic_invoices/config/locales/de.yml @@ -0,0 +1,1997 @@ +de: + activerecord: + attributes: + article: + article_category: Kategorie + availability: Artikel ist verfügbar? + availability_short: verf. + deposit: Pfand (brutto) + fc_price: Endpreis + fc_price_desc: Preis incl. MwSt, Pfand und Foodcoop-Aufschlag. + fc_price_short: FC-Preis + fc_share: FoodCoop-Aufschlag + fc_share_short: FC-Aufschlag + gross_price: Bruttopreis + manufacturer: Produzent + name: Name + note: Notiz + order_number: Bestellnummer + order_number_short: Nr. + origin: Herkunft + price: Nettopreis + supplier: Lieferantin + tax: MwSt + unit: Einheit + unit_quantity: Gebindegröße + unit_quantity_short: GebGr + units: Gebinde + article_category: + description: Beschreibung + name: Name + article_price: + deposit: Pfand (brutto) + price: Nettopreis + tax: MwSt + unit_quantity: Gebindegröße + bank_account: + balance: Kontostand + bank_gateway: Bankgateway + description: Beschreibung + iban: IBAN + name: Name + bank_gateway: + authorization: Authorization-Header + name: Name + unattended_user: Bedienerlos-Benutzer_in + url: URL + bank_transaction: + amount: Betrag + date: Datum + external_id: Externe ID + financial_link: Finanzlink + iban: IBAN + reference: Zahlungsreferenz + text: Beschreibung + delivery: + date: Lieferdatum + note: Notiz + supplier: Lieferantin + document: + created_at: Erstellt am + created_by: Erstellt von + data: Daten + mime: MIME-Typ + name: Name + financial_transaction: + amount: Betrag + created_on: Datum + financial_transaction_class: Kontotransaktionsklasse + financial_transaction_type: Kontotransaktionstyp + note: Notiz + ordergroup: Bestellgruppe + user: Eingetragen von + financial_transaction_class: + ignore_for_account_balance: Für Kontostand ignorieren + name: Name + financial_transaction_type: + bank_account: Bankkonto + name: Name + financial_transaction_class: Kontotransaktionsklasse + name_short: Kurzname + group_order: + ordergroup: Bestellgruppe + price: Bestellsumme + updated_by: Zuletzt bestellt + group_order_article: + ordered: Bestellt + quantity: Menge + received: Bekommen + result: Menge + tolerance: Toleranz + total_price: Summe + unit_price: Preis/Einheit + group_order_invoice: + name: Bestellgruppenrechnung + links: + delete: Rechnung löschen + download: Rechnung herunterladen + generate: Rechnung erzeugen + invoice_date: Datum der Bestellgruppenrechnung + generate_with_date: setzen & erzeugen + download_all_zip: Alle Rechnungen herunterladen (zip) + payment_method: Guthaben + tax_number_not_set: Steuernummer in den Einstellungen nicht gesetzt + invoice: + amount: Betrag + attachment: Anhang + created_at: Erstellt am + created_by: Erstellt von + date: Rechnungsdatum + delete_attachment: Anhang löschen + deliveries: Lager-Lieferung + deposit: Pfand berechnet + deposit_credit: Pfand gutgeschrieben + financial_link: Finanzlink + net_amount: Pfandbereinigter Betrag + note: Notiz + number: Nummer + orders: Bestellung + paid_on: Bezahlt am + supplier: Lieferant + mail_delivery_status: + created_at: Zeitpunkt + email: E-Mail + message: Nachicht + order: + boxfill: Kistenfüllen ab + closed_by: Abgerechnet von + created_by: Erstellt von + end_action: Endeaktion + end_actions: + auto_close: Bestellung beenden + auto_close_and_send: Bestellung beenden und an Lieferantin schicken + auto_close_and_send_min_quantity: Bestellung beenden und an Lieferantin schicken sofern die Mindestbestellmenge erreicht wurde + no_end_action: Keine automatische Aktion + ends: Endet am + name: Lieferant + note: Notiz + pickup: Abholung + starts: Läuft vom + status: Status + supplier: Lieferant + transport: Transportkosten + transport_distribution: Transportkostenverteilung + transport_distributions: + articles: Kosten anhand der Anzahl an erhaltenen Artiklen verteilen + ordergroup: Jede Bestellgruppe zahlt gleich viel + price: Kosten anhand der Bestellsumme aufteilen + skip: Kosten nicht auf die Bestellgruppen aufteilen + updated_by: Zuletzt geändert von + order_article: + article: Artikel + missing_units: Fehlende Einheiten + missing_units_short: Fehlend + quantity: Gewünschte Einheiten + quantity_short: Gewünscht + units_received: Gelieferte Gebinde + units_received_short: Geliefert + units_to_order: Bestellte Gebinde + units_to_order_short: Bestellt + update_global_price: Globalen Preis aktualisieren + order_comment: + text: Kommentiere diese Bestellung ... + ordergroup: + account_balance: Kontostand + available_funds: Verfügbares Guthaben + break: "(Letzte) Pause" + break_until: bis + contact: Kontakt + contact_address: Adresse + contact_person: Kontaktperson + contact_phone: Telefon + customer_number: Kundennummer + description: Beschreibung + ignore_apple_restriction: Bestellstop bei zu wenig Äpfeln ignorieren + last_order: Zuletzt bestellt + last_user_activity: Zuletzt aktiv + name: Name + user_tokens: Mitglieder + stock_article: + available: Verfügbar + price: Nettopreis + quantity: im Lager + quantity_available: Verfügbarer Bestand + quantity_available_short: Verf. + quantity_ordered: Davon bestellt + stock_taking: + date: Datum + note: Notiz + supplier: + address: Adresse + contact_person: Ansprechpartner + customer_number: Kundennummer + customer_number_short: Kundennr. + delivery_days: Liefertage + email: E-Mail + fax: Fax + iban: IBAN + is_subscribed: abonniert? + min_order_quantity: Mindestbestellmenge + min_order_quantity_short: Menge (mind.) + name: Name + note: Notiz + order_howto: Kurzanleitung Bestellen + phone: Telefon + phone2: Telefon 2 + shared_sync_method: Syncronisierungsmethode + url: Webseite + supplier_category: + name: Name + description: Beschreibung + financial_transaction_class: Kontotransaktionsklasse + bank_account: Bankkonto + task: + created_by: Erstellt von + created_on: Erstellt am + description: Beschreibung + done: Erledigt? + due_date: Wann erledigen? + duration: Dauer + name: Aufgabe + required_users: Anzahl + user_list: Verantwortliche + workgroup: Arbeitsgruppe + user: + email: E-Mail + first_name: Vorname + iban: IBAN + last_activity: Letzte Aktivität + last_login: Letzter login + last_name: Nachname + name: Name + nick: Benutzername + ordergroup: Bestellgruppe + password: Passwort + password_confirmation: Passwort wiederholen + phone: Telefon + workgroup: + one: Arbeitsgruppe + other: Arbeitsgruppen + workgroup: + description: Beschreibung + name: Name + role_admin: Administration + role_article_meta: Artikeldatenbank + role_finance: Finanzen + role_invoices: Rechnungen + role_orders: Bestellverwaltung + role_pickups: Abholtage + role_suppliers: Lieferanten + user_tokens: Mitglieder + errors: + has_many_left: ist noch mit einem/r %{collection} verknüpft! + models: + article: + attributes: + name: + taken: Name ist bereits vergeben + taken_with_unit: Name und Einheit sind bereits vergeben + supplier: + attributes: + shared_sync_method: + included: ist keine gültige Auswahl für diesen Lieferanten + task: + attributes: + done: + exclusion: erledigte Aufgaben können nicht wöchentlich wiederholt werden + models: + article: Artikel + article_category: Kategorie + bank_account: Bankkonto + bank_gateway: Bankgateway + bank_transaction: Banktransaktion + delivery: Lieferung + financial_transaction: Kontotransaktion + financial_transaction_class: Kontotransaktionsklasse + financial_transaction_type: Kontotransaktionstyp + invoice: Rechnung + order: Bestellung + order_article: Bestell-Artikel + order_comment: Kommentar + ordergroup: + one: Bestellgruppe + other: Bestellgruppen + stock_article: Lagerartikel + stock_taking: Inventur + supplier: Lieferant + supplier_category: Lieferantenkategorie + task: Aufgabe + user: Benutzerinnen + workgroup: Arbeitsgruppe + admin: + access_to: Zugriff auf + base: + index: + all_ordergroups: Alle Bestellgruppen + all_users: Alle Benutzerinnen + all_workgroups: Alle Arbeitsgruppen + created_at: Erstellt am + first_paragraph: Hier kannst Du die Gruppen und Benutzerinnen der Foodsoft verwalten. + groupname: Gruppenname + members: Mitglieder + name: Name + new_ordergroup: Neue Bestellgruppe + new_user: Neue Benutzerin + new_workgroup: Neue Arbeitsgruppe + newest_groups: Neueste Gruppen + newest_users: Neueste Benutzer/innen + title: Administration + type: Typ + username: Benutzername + bank_accounts: + form: + title_edit: Bankkonto bearbeiten + title_new: Bankkonto anlegen + bank_gateways: + form: + title_edit: Bankgateway bearbeiten + title_new: Bankgateway anlegen + configs: + list: + key: Schlüssel + title: Liste der Einstellungen + value: Wert + show: + submit: Speichern + title: Einstellungen + tab_layout: + pdf_title: PDF-Dokumente + tab_messages: + emails_title: E-Mails versenden + tab_payment: + schedule_title: Bestellschema + group_order_invoices: Bestellgruppenrechnungen + tab_security: + default_roles_title: Zugriff auf + default_roles_paragraph: Jedes Mitglied der Foodcoop hat automatisch Zugriff auf folgende Bereiche. + tab_tasks: + periodic_title: Wiederkehrende Aufgaben + tabs: + title: Einstellungen + update: + notice: Einstellungen gespeichert. + confirm: Bist Du sicher? + finances: + index: + bank_accounts: Bankkonten + first_paragraph: Hier kannst du die Kontotransaktionsklassen und die dazugehörigen Kontotransaktionstypen verwalten. Jede Finanztansaktion hat einen bestimmten Typ, den du bei jeder Transaktion auswählen muss, falls du mehr als einen Typ angelegt hast. Die Kontotransaktionsklassen können zur Gruppierung der Kontotransaktionstypen verwendet werden und werden in der Kontoübersicht als weitere Spalten angezeigt, falls mehrere angelegt wurden. + new_bank_account: Neues Bankkonto anlegen + new_financial_transaction_class: Neue Kontotransaktionsklasse anlegen + new_bank_gateway: Neuen Bank Gateway anlegen + title: Finanzen + transaction_types: Kontotransaktionstypen + supplier_categories: Lieferantenkategorien + new_supplier_category: Neue Lieferantenkategorie anlegen + transaction_types: + name: Name + new_financial_transaction_type: Neuen Kontotransaktionstyp anlegen + financial_transaction_classes: + form: + title_edit: Kontotransaktionsklasse bearbeiten + title_new: Kontotransaktionsklasse anlegen + financial_transaction_types: + form: + name_short_desc: Der Kurzname ist notwendig, falls der Kontotransaktionstyp in Banktransaktionen automatisch erkannt werden soll. Falls es mehrere Bankkonten gibt, kann das gewünschte Zielkonto für Überweisungen ausgewählt werden. + title_edit: Kontotransaktionstyp bearbeiten + title_new: Kontotransaktionstyp anlegen + mail_delivery_status: + destroy_all: + notice: Alle E-Mail Probleme wurden gelöscht + index: + destroy_all: Alle E-Mail Probleme löschen + title: E-Mail Probleme + ordergroups: + destroy: + error: 'Bestellgruppe konnte nicht als gelöscht markiert werden: %{error}' + notice: Bestellgruppe wurde als gelöscht markiert + edit: + title: Bestellgruppe bearbeiten + form: + first_paragraph: Neue Mitglieder kannst du %{url} einladen. + here: hier + index: + first_paragraph: Hier kannst du %{url} anlegen, Gruppen bearbeiten und löschen. + new_ordergroup: Neue Bestellgruppe anlegen + new_ordergroups: neue Bestellgruppen + second_paragraph: 'Beachte dabei den Unterschied zwischen Gruppe und Bestellgruppe: Eine Bestellgruppe hat ein Konto und kann Essen bestellen. In einer %{url} (z.b. ''Sortiergruppe'') koordinieren sich die Mitglieder mittels Aufgaben und Nachrichten. Nutzer/innen können immer nur einer Bestellgruppe, aber beliebig vielen anderen Gruppen angehören.' + title: Bestellgruppen + workgroup: Arbeitsgruppe + new: + title: Bestellgruppe anlegen + show: + confirm: Bist Du sicher? + edit: Gruppe/Mitglieder bearbeiten + title: Bestellgruppe %{name} + search_placeholder: Name ... + users: + controller: + sudo_done: Du bist jetzt als Benutzer %{user} angemeldet. Sei vorsichtig und melde dich ab, wenn du fertig bist! + destroy: + error: 'Benutzer/in konnte nicht gelöscht werden: %{error}' + notice: Benutzer/in wurde gelöscht + edit: + title: Benutzer/in bearbeiten + form: + create_ordergroup: Bestellgruppe mit dem selben Namen erstellen und Benutzer_in hinzufügen. + send_welcome_mail: Eine Willkommen-Nachricht an Benutzer_in senden. + index: + first_paragraph: Hier kannst du Benutzer/innen %{url}, bearbeiten und natürlich auch löschen. + new_user: Neue/n Benutzer/in anlegen + new_users: neu anlegen + show_deleted: Gelöschte Benutzer anzeigen + title: Benutzer/innen verwalten + new: + title: Neue/n Benutzer/in anlegen + restore: + error: 'Benutzer/in konnte nicht wiederhergestellt werden: %{error}' + notice: Benutzer/in wurde wiederhergestellt + show: + confirm_sudo: Wenn du fortsetzt, dann wirst du als Benutzer %{user} angemeldet. Sei vorsichtig und melde dich ab, wenn du fertig bist! + groupabos: Gruppenabos + member_since: Mitglied seit %{time} + person: Person + preference: Einstellungen + show_email_problems: Zeige E-Mail Probleme + sudo: Als anderer Benutzer anmelden + users: + show_email_problems: Zeige E-Mail Probleme + workgroups: + destroy: + error: 'Arbeitsgruppe konnte nicht gelöscht werden: %{error}' + notice: Arbeitsgruppe wurde gelöscht + edit: + title: Arbeitsgruppe bearbeiten + form: + first_paragraph: Neue Mitglieder kannst du %{url} einladen. + here: hier + index: + first_paragraph: Hier kannst du %{url} anlegen, bearbeiten und löschen. + new_workgroup: Neue Arbeitsgruppe anlegen + new_workgroups: neue Arbeitsgruppen + ordergroup: Bestellgruppe + second_paragraph: 'Beachte dabei den Unterschied zwischen Gruppe und Bestellgruppe: eine %{url} hat ein Konto und kann Essen bestellen. In einer Arbeitsgruppe (z.b. ''Sortiergruppe'') koordinieren sich die Mitglieder mittels Aufgaben und Nachrichten. Benutzer/innen können immer nur einer Bestellgruppe, aber beliebig vielen anderen Gruppen angehören.' + title: Arbeitsgruppen + new: + title: Arbeitsgruppe anlegen + show: + confirm: Bist Du sicher? + edit: Gruppe/Mitglieder bearbeiten + title: Arbeitsgruppe %{name} + workgroups: + members: Mitglieder + name: Name + supplier_categories: + form: + title_new: Neue Lieferantenkategorie anlegen + title_edit: Lieferantenkategorie bearbeiten + application: + controller: + error_authn: Anmeldung erforderlich! + error_denied: Du darfst die gewünschte Seite nicht sehen. Wenn Du denkst, dass Du dürfen solltest, frage eine/n Administrator/in, dass sie/er Dir die entsprechenden Rechte einräumt. Falls Du Zugang zu mehreren Benutzerkonten hast, möchtest Du Dich vielleicht %{sign_in}. + error_denied_sign_in: als ein anderer Benutzer anmelden + error_feature_disabled: Diese Funktion ist derzeit nicht aktiviert. + error_members_only: Diese Aktion ist nur für Mitglieder der Gruppe erlaubt! + error_minimum_balance: Ihr Kontostand liegt leider unter dem Minimum von %{min}. + error_token: Zugriff verweigert (ungültiger Token)! + article_categories: + create: + notice: Die Kategorie wurde gespeichert + destroy: + error: 'Kategorie konnte nicht gelöscht werden: %{message}' + edit: + title: Kategorie ändern + index: + new: Neue Kategorie anlegen + title: Artikelkategorien + new: + title: Neue Kategorie anlegen + update: + notice: Die Kategorie wurde aktualisiert + articles: + article: + last_update: 'zuletzt geändert: %{last_update} | Brutto: %{gross_price}' + articles: + confirm_delete: Willst Du wirklich alle gewählten Artikel löschen? + option_available: Artikel sind verfügbar + option_delete: Artikel löschen + option_not_available: Artikel sind nicht mehr verfügbar + option_select: Aktion wählen ... + price_netto: Preis + unit_quantity_desc: Gebindegröße + unit_quantity_short: GebGr + controller: + create_from_upload: + notice: "Es wurden %{count} neue Artikel gespeichert." + error_invalid: Artikel sind fehlerhaft + error_nosel: Du hast keine Artikel ausgewählt + error_parse: "%{msg} ... in Zeile %{line}" + error_update: 'Es trat ein Fehler beim Aktualisieren des Artikels ''%{article}'' auf: %{msg}' + parse_upload: + no_file: Bitte eine Datei zum hochladen auswählen. + notice: "%{count} Artikel sind erfolgreich analysiert." + sync: + notice: Der Katalog ist aktuell + shared_alert: "%{supplier} ist nicht mit einer externen Datenbank verknüpft." + update_all: + notice: Alle Artikel und Preise wurden aktualisiert + update_sel: + notice_avail: Alle gewählten Artikel wurden auf "verfügbar" gesetzt + notice_destroy: Alle gewählten Artikel wurden gelöscht + notice_noaction: Keine Aktion ausgewählt! + notice_unavail: Alle gewählten Artikel wurden auf "nicht verfügbar" gesetzt + update_sync: + notice: Alle Artikel und Preise wurden aktualisiert + destroy_active_article: + drop: entfernen + note: "%{article} wird in laufenden Bestellungen verwendet und kann nicht gelöscht werden. Bitte zuerst den Artikel aus den Bestellungen %{drop_link}." + edit_all: + note: 'Pflichtfelder sind: Name, Einheit, (netto) Preis und Bestellnummer.' + submit: Alle Artikel aktualisieren + title: Alle Artikel von %{supplier} bearbeiten + warning: 'Achtung, alle Artikel werden aktualisiert!' + form: + title_edit: Artikel bearbeiten + title_new: Neuen Artikel einfügen + import_search_results: + action_import: importieren + already_imported: schon importiert + not_found: Keine Artikel gefunden + index: + change_supplier: Lieferant wechseln ... + download: Artikel herunterladen + edit_all: Alle bearbeiten + ext_db: + import: Suchen/Importieren + sync: Synchronisieren + import: + category: Direkt in Kategorie importieren + placeholder: Name ... + restrict_region: Nur aus der Region + title: Artikel importieren + new: Neuer Artikel + new_order: Bestellung anlegen + search_placeholder: Name ... + title: Artikel von %{supplier} (%{count}) + upload: Artikel hochladen + model: + error_in_use: "%{article} kann nicht gelöscht werden. Der Artikel befindet sich in einer laufenden Bestellung!" + error_nosel: Du hast keine Artikel ausgewählt + parse_upload: + body: "
Bitte überprufe die engelesenen Artikel.
Achtung, momentan gibt es keine Überprüfung auf doppelte Artikel.
" + submit: Hochladen + title: Artikel hochladen + sync: + outlist: + alert_used: Achtung, %{article} wird gerade in einer laufenden Bestellung verwendet. Bitte erst Bestellung anpassen. + body: 'Folgende Artikel wurden ausgelistet und werden gelöscht:' + body_ignored: + one: Ein Artikel ohne Bestellnummer wurde übersprungen. + other: "%{count} Artikel ohne Bestellnummer wurden übersprungen." + body_skip: Es müssen keine Artikel gelöscht werden. + title: Auslisten ... + price_short: Preis + submit: Alle synchronisieren + title: Artikel mit externer Datenbank synchronisieren + unit_quantity_short: GebGr + update: + body: 'Jeder Artikel wird doppelt angezeigt: die alten Werte sind grau und die Textfelder sind mit den aktuellen Werten vorausgefüllt. Abweichungen zu den alten Artikeln sind gelb markiert.' + title: Aktualisieren ... + update_msg: + one: Ein Artikel muss aktualisiert werden. + other: "%{count} Artikel müssen aktualisiert werden." + upnew: + body_count: + one: Es gibt einen neuen Artikel hinzuzufügen. + other: Es gibt %{count} Artikel hinzuzufügen. + title: Neue hinzufügen ... + upload: + fields: + reserved: "(geschützt)" + status: Status (x=ausgelistet) + file_label: Bitte wähle eine kompatible Datei aus + options: + convert_units: Derzeitige Einheiten beibehalten, berechne Mengeneinheit und Preis (wie Synchronisieren). + outlist_absent: Artikel löschen, die nicht in der hochgeladenen Datei sind. + sample: + juices: Säfte + nuts: Nüsse + organic: Bio + supplier_1: Nussfarm + supplier_2: Braune Felder + supplier_3: Grüne Felder + tomato_juice: Tomatensaft + walnuts: Walnüsse + submit: Datei hochladen + text_1: 'Du kannst hier eine Tabelle hochladen, um die Artikel des Lieferanten %{supplier} zu aktualisieren. Excel (xls, xlsx) und OpenOffice (ods) Tabellen werden akzeptiert, darüber hinaus Dateien im Format "csv" (comma-separated values, mit dem Spaltentrennzeichen ";" und utf-8 Kodierung). Nur das erste Tabellenblatt wird importiert und die Spalten müssen in der folgenden Anordnung vorliegen:' + text_2: Die hier gezeigten Spalten sind Beispiele. Ist ein "x" in der ersten Spalte, wird der Artikel aussortiert und entfernt. Das erlaubt Dir die Tabelle zu ändern und schnell viele Artikel auf ein Mal zu entfernen, zum Beispiel wenn Artkiel des Lieferanten nicht mehr verfügbar sind. Die Kategorie wird der Foodsoft Kategorie zugeordnet (durch die Kategorienamen und die Importnamen). + title: Artikel des Lieferanten %{supplier} hochladen + bank_account_connector: + confirm: Bitte bestätige den Code %{code}. + fields: + email: E-Mail + pin: PIN + password: Passwort + tan: TAN + username: Benutzername + config: + hints: + applepear_url: Seite, auf der das Äpfel- und Birnensystem für Aufgaben erklärt wird. + charge_members_manually: Wann die Aufzeichnungen über was wer bekommen hat wo anders (z.B. auf Papier) geführt werden und nicht in die Foosoft eingetragen werden, sollte diese Option aktiviert werden. Die Abrechnung muss dann manuell (mittels "Neue Überweisungen eingeben") eingetragen werden. Bestellungen müssen nach wie vor abgerechnet werden, aber das belastet nicht die Konten der Mitglieder. + contact: + email: E-Mail Adresse zur allgemeinen Kontaktaufnahme, die auf der Webseite und in einigen Formularen gezeigt wird. + street: Anschrift; üblicherweise ist dies Euer Liefer- und Abholort. + currency_space: Leerzeichen beim Währungssymbol hinzufügen. + currency_unit: Währungssymbol zur Anzeige von Preisen. + custom_css: Um das Layout der Seite anzupassen, kannst Du CSS verwenden. Kann leer gelassen werden, um den Standard Stil zu verwenden. + email_from: E-Mails werden so aussehen, als ob sie von dieser Adresse gesendet wurden. Kann leer gelassen werden, um die Kontaktadresse der Foodcoop zu benutzen. + email_replyto: Setze diese Adresse, wenn Du Antworten auf Foodsoft E-Mails auf eine andere, als die oben angegebene Absenderadresse bekommen möchtest. + email_sender: E-Mails werden so aussehen, als ob sie von dieser Adresse versendet wurden. Um zu vermeiden, dass E-Mails dadurch als Spam eingeordnet werden, muss der Webserver möglicherweise im SPF Eintrag der Domain der E-Mail Adresse eingetragen werden. + group_order_invoices: + use_automativ_go_invoices: Es werden auf die Bestellgruppen zugeschnittene Rechnungen für die jeweilige Bestellung beim Klicken auf "abrechnen" an alle Bestellgruppenmitglieder per Mail versendet. + payment_method: Zahlungsart wird auf der Bestellgruppenrechnung deklariert + vat_exempt: Eine Auflistung der Rechnungsartikel erfolgt ohne explizite Ausweisung der MwSt. und die Rechnung erhält den notwendigen Zusatz bzgl. der Kleinunternehmerregelung §19 (FoodCoop Marge ebenfalls nicht in Rechnung enthalten) + help_url: Link zur Dokumentationsseite + homepage: Webseite der Foodcoop + ignore_browser_locale: Ignoriere die Sprache des Computers des Anwenders, wenn der Anwender noch keine Sprache gewählt hat. + minimum_balance: Mitglieder können nur bestellen, wenn ihr Kontostand mindestens diesem Betrag entspricht. + name: Der Name Deiner Foodcoop + order_schedule: + boxfill: + recurr: Standarddatum für den Begin der Kistenauffüllphase + time: Standardzeit für den Begin der Kistenauffüllphase + ends: + recurr: Standarddatum für den Bestellschluss + time: Standardzeit für den Bestellschluss + initial: Bestellungen starten an diesem Datum + page_footer: Dies wird in der Fußzeile jeder Seite angezeigt. Wird es leer gelassen, wird die Fußzeile komplett deaktiviert. + pdf_add_page_breaks: + order_by_articles: Jeden Artikel auf eine eigene Seite bringen + order_by_groups: Jede Bestellgruppe auf eine eigene Seite bringen + pdf_font_size: Standardmäßige Schriftgröße für PDF Dokumente (12 ist der Standardwert) + pdf_page_size: Seitengröße für PDF Dokumente, normalerweise "A4" + price_markup: Prozentsatz, welcher als Aufschlag auf alle Preise addiert wird + stop_ordering_under: Mitglieder können nur bestellen, wenn sie mindestens diese Anzahl von Apfel-Punkten haben. + tasks_period_days: Anzahl der Tage zwischen Bestellungen (Standard 7 - für eine Woche) + tasks_upfront_days: Anzahl der Tage, für welche du im Voraus wiederkehrende Aufgaben definieren möchtest + tax_default: Standard Mehrwertsteuersatz für neue Artikel + tolerance_is_costly: Eine möglichst große Menge im Rahmen der Tolerenz bestellen. Wenn dies nicht aktiviert ist, wird im Rahmen der Toleranz nur so viel bestellt, dass damit komplette Einheiten (Boxen) bestellt werden können. Die Option wirkt sich auch auf die Toleranz des Gesamtpreises einer offenen Mitgliederbestellung aus. + distribution_strategy: Wie bei der Verteilung von Artikeln nach dem Empfangen einer Bestellung vorgegangen werden soll. + use_apple_points: Wenn das Apfel Punktesystem aktiviert ist, ist es erforderlich, dass Mitglieder Aufgaben erledigen um bestellen zu können. + use_boxfill: Wenn aktiviert, können Benutzer nahe am Ende der Bestellung diese nur mehr so verändern, dass sich die Gesamtsumme erhöht. Dies hilft beim auffüllen der verbleibenden Kisten. Es muss trotzdem noch das Kistenauffülldatum bei der Bestellung gesetzt werden. + use_iban: Zusätzlich Feld für die internationale Kontonummer bei Benutzern und Lieferanten anzeigen + use_nick: Benutzernamen anstatt reale Namen zeigen und verwenden, jeder Benutzer muss dazu einen Benutzernamen (Spitznamen) haben. + use_self_service: Wenn aktiviert, können Benutzer_innen selbständig dafür freigegebene Abrechungsfunktionen nutzen. + webstats_tracking_code: Tracking Code für Webseitenanalyse (wie Piwik oder Google Analytics), leer lassen wenn keine Analyse erfolgt + keys: + applepear_url: Hilfe URL für das Äpfel Punktesystem zum Engagement + charge_members_manually: Mitglieder manuell abrechnen + contact: + city: Stadt + country: Land + email: E-Mail + phone: Telefon + street: Straße + zip_code: Postleitzahl + tax_number: Steuernummer + currency_space: Leerzeichen hinzufügen + currency_unit: Währung + custom_css: Angepasstes CSS + default_locale: Standardsprache + default_role_article_meta: Artikeldatenbank + default_role_finance: Finanzen + default_role_invoices: Rechnungen + default_role_orders: Bestellverwaltung + default_role_pickups: Abholtage + default_role_suppliers: Lieferanten + disable_invite: Einladungen deaktivieren + disable_members_overview: Mitgliederliste deaktivieren + email_from: Absenderadresse + email_replyto: Antwortadresse + email_sender: Senderadresse + group_order_invoices: + use_automatic_invoices: Automatisch bei Abrechnung per Mail versenden + separate_deposits: Pfand getrennt abrechnen + payment_method: Zahlungsart + vat_exempt: Diese Foodcoop ist MwSt. befreit + help_url: URL Dokumentation + homepage: Webseite + ignore_browser_locale: Browsersprache ignorieren + minimum_balance: Minimaler Kontostand + name: Name + order_schedule: + boxfill: + recurr: Kistenauffüllen ab + time: Zeit + ends: + recurr: Bestellschluss + time: Zeit + initial: Bestellstart + page_footer: Fußzeile Webseite + pdf_add_page_breaks: Seitenwechsel + pdf_font_size: Schriftgrösse + pdf_page_size: Seitenformat + price_markup: Foodcoop Marge + stop_ordering_under: Apfelpunkte Minimum + tasks_period_days: Zeitintervall + tasks_upfront_days: Im Voraus + tax_default: Mehrwertsteuer + time_zone: Zeitzone + tolerance_is_costly: Bestelltoleranz maximal ausnutzen, um möglichst große Mengen zu bestellen + distribution_strategy: Verteilungs-Strategie + distribution_strategy_options: + first_order_first_serve: Zuerst an die verteilen, die zuerst bestellt haben + no_automatic_distribution: Keine automatische Verteilung + use_apple_points: Apfelpunkte verwenden + use_boxfill: Kistenauffüllphase + use_iban: IBAN verwenden + use_nick: Benutzernamen verwenden + use_self_service: Selbstbedienung verwenden + webstats_tracking_code: Code für Websiteanalysetool + tabs: + applications: Apps + foodcoop: Foodcoop + language: Sprache + layout: Layout + list: Liste + messages: Nachrichten + others: Sonstiges + payment: Finanzen + security: Sicherheit + tasks: Aufgaben + deliveries: + add_stock_change: + how_many_units: 'Wie viele Einheiten (%{unit}) des Artikels »%{name}« liefern?' + create: + notice: Lieferung wurde erstellt. Bitte nicht vergessen die Rechnung anzulegen! + destroy: + notice: Lieferung wurde gelöscht. + edit: + title: Lieferung bearbeiten + form: + confirm_foreign_supplier_reedit: Der Lagerartikel »%{name}« wurde erfolgreich gespeichert. Er gehört jedoch nicht zu dem Lieferanten dieser Lieferung. Möchtest Du diesen Lagerartikel erneut bearbeiten? + create_from_blank: Ohne Vorlage anlegen + create_stock_article: Lagerartikel anlegen + title_fill_quantities: 2. Liefermenge angeben + title_finish_delivery: 3. Lieferung abschließen + title_select_stock_articles: 1. Lagerartikel auswählen + index: + confirm_delete: Bist Du sicher? + new_delivery: 'Neue Lieferung für %{supplier} anlegen' + title: "%{supplier}/Lieferungen" + invoice_amount: Rechnungsbetrag + invoice_net_amount: bereinigter Rechnungsbetrag + new: + title: Neue Lieferung von %{supplier} + show: + sum: Summe + sum_diff: Brutto - bereinigter Rechnungsbetrag + sum_gross: Bruttosumme + sum_net: Nettosumme + title: Lieferung anzeigen + title_articles: Artikel + stock_article_for_adding: + action_add_to_delivery: Liefern + action_edit: Bearbeiten + action_other_price: Kopieren + stock_change_fields: + remove_article: Artikel aus Lieferung entfernen + suppliers_overview: Lieferantenübersicht + update: + notice: Lieferung wurde aktualisiert. + documents: + group_order_invoice_pdf: + filename: Rechnung%{number} + invoicer: Rechnungsteller*in + invoicee: Rechnungsempfänger*in + invoice_date: 'Rechnungsdatum: %{invoice_date}' + invoice_number: 'Rechnungsnummer: %{invoice_number}' + markup_included: zzgl. Foodcoop Marge auf brutto Preis %{marge}% + ordergroup: + contact_phone: 'Telefonnummer: %{contact_phone}' + contact_address: 'Adresse : %{contact_address}' + customer_number: 'Kundennummer: %{customer_number}' + name: Bestellgruppe %{ordergroup} + payment_method: 'Zahlungsart: %{payment_method}' + sum_to_pay: Zu zahlen gesamt + sum_to_pay_net: Zu zahlen gesamt (netto) + sum_to_pay_gross: Gesamt + small_business_regulation: Als Kleinunternehmer*in im Sinne von §19 Abs. 1 Umsatzsteuergesetz (UStG) wird keine Umsatzsteuer berechnet. + table_headline: 'Für die Bestellung fallen folgende Posten an:' + tax_excluded: exkl. MwSt. + tax_included: zzgl. Gesamtsumme MwSt. %{tax}% + tax_number: 'Steuernummer: %{number}' + title: Rechnung für die Bestellung bei %{supplier} + vat_exempt_rows: + - Name + - Anzahl + - Einzelpreis + - Artikel Gesamtpreis + no_price_markup_rows: + - Name + - Anzahl + - Einzelpreis (netto) + - Artikel Gesamtpreis (netto) + - MwSt. + - Artikel Gesamtpreis (brutto) + price_markup_rows: + - Name + - Anzahl + - Einzelpreis (netto) + - Artikel Gesamtpreis (netto) + - MwSt. + - Artikel Gesamtpreis (brutto) inkl. Foodcoopmarge %{marge}% + order_by_articles: + filename: Bestellung %{name}-%{date} - Artikelsortierung + title: 'Artikelsortierung der Bestellung: %{name}, beendet am %{date}' + order_by_groups: + filename: Bestellung %{name}-%{date} - Gruppensortierung + sum: Summe + title: 'Gruppensortierung der Bestellung: %{name}, beendet am %{date}' + order_fax: + filename: Bestellung %{name}-%{date} - Fax + rows: + - BestellNr. + - Menge + - Name + - Gebinde + - Einheit + - Preis/Einheit + - Summe + total: Gesamtpreis + order_matrix: + filename: Bestellung %{name}-%{date} - Sortiermatrix + heading: Artikelübersicht (%{count}) + title: 'Sortiermatrix der Bestellung: %{name}, beendet am %{date}' + errors: + check_tax_number: Überprüft, ob die Steuernummer der Foodcoop richtig gesetzt ist + general: Ein Problem ist aufgetreten. + general_again: Ein Fehler ist aufgetreten. Bitte erneut versuchen. + general_msg: 'Ein Fehler ist aufgetreten: %{msg}' + internal_server_error: + text1: Ein unerwarteter Fehler ist aufgetreten. Entschuldigung! + text2: Wir wurden benachrichtigt. Fall das Problem weiterhin besteht, gib uns bitte bescheid. + title: Interner Server Fehler + not_found: + text: Diese Seite existiert anscheinend nicht. Entschuldigung! + title: Seite nicht gefunden + feedback: + create: + notice: Das Feedback wurde erfolgreich verschickt. Vielen Dank! + new: + first_paragraph: Fehler gefunden? Vorschlag? Idee? Kritik? Wir freuen uns über jegliches Feedback. + second_paragraph: Bitte beachte, dass das Foodsoft Team nur die Software wartet. Bei Fragen zur Organisation in Deiner Foodcoop, kontaktiere besser die entsprechenden Ansprechpartner. + send: Absenden + title: Gib Feedback + finance: + balancing: + close: + alert: 'Ein Fehler ist beim Abrechnen aufgetreten: %{message}' + notice: Bestellung wurde erfolgreich abgerechnet, die Kontostände aktualisiert. + notice_mail: Bestellung wurde erfolgreich abgerechnet, die Kontostände aktualisiert. Außerdem wurden automatisch Rechnungen an die Bestellgruppenmitglieder geschickt. + settings_not_set: Keine Emails mit Bestellgruppenrechnungen versendet. Bitte überprüfe die Einstellungen. Steuernummer gesetzt? + close_all_direct_with_invoice: + notice: 'Es wurden %{count} Bestellung abgerechnet.' + close_direct: + alert: 'Bestellung kann nicht abgerechnet werden: %{message}' + notice: Bestellung wurde abgerechnet. + confirm: + clear: Abrechnen + first_paragraph: 'Wenn die Bestellung abgerechnet wird, werden ebenfalls alle Gruppenkonten aktualisiert.Hier kannst Du den Mitgliedern Deiner Foodcoop eine Nachricht schreiben. Damit Deine Kontaktdaten einzusehen sind, musst Du sie unter %{profile_link} freigeben.
" + ph_name: Name ... + ph_ordergroup: Bestellgruppe ... + profile_link: Einstellungen + title: Mitglieder + workgroups: + edit: + invite_link: hier + invite_new: Neue Mitglieder kannst du %{invite_link} einladen. + title: Gruppe bearbeiten + index: + body: "Das bearbeiten von Gruppen ist nur für Mitglieder der Gruppe möglich.
Wenn du einer Gruppe beitreten willst, dann schreib doch den Mitgliedern eine Nachricht.
Hier kannst Du jemanden einladen, der nicht Teil der Foodcoop ist, deiner Bestellgruppe %{group} beizutreten. Nachdem die Einladung angenommen wurde, wird die Person in der Lage sein, Artikel zu deiner Bestellung hinzuzufügen (und zu entfernen).
Dies ist eine praktische Funktion, um jemanden in die Foodcoop einzuladen oder mit mehreren Leuten in einem Haushalt zu bestellen.
" + title: Person einladen + new: + action: Einladung abschicken + body: "Hier kannst du eine Person in die Gruppe %{group} einladen, die noch nicht Mitglied der Foodcoop ist.
" + success: Benutzer/in wurde erfolgreich eingeladen. + js: + ordering: + confirm_change: Änderungen an dieser Bestellung gehen verloren, wenn zu einer anderen Bestellung gewechselt wird. Möchtest Du trotzdem wechseln? + trix_editor: + file_size_alert: Der Dateianhang ist zu groß! Die maximale Größe beträgt 512Mb + layouts: + email: + footer_1_separator: "--" + footer_2_foodsoft: 'Foodsoft: %{url}' + footer_3_homepage: 'Foodcoop: %{url}' + footer_4_help: 'Hilfe: %{url}' + help: 'Hilfe' + foodsoft: Foodsoft + footer: + revision: Revision %{revision} + header: + feedback: + desc: Fehler gefunden? Vorschlag? Idee? Kritik? + title: Feedback + help: Hilfe + logout: Abmelden + ordergroup: Meine Bestellgruppe + profile: Profil bearbeiten + reference_calculator: Zahlungsreferenz-Rechner + logo: "foodsoft" + lib: + render_pdf: + page: Seite %{number} von %{count} + login: + accept_invitation: + body: "Du bist eingeladen worden als Mitglied der Gruppe %{group} in der Foodcoop %{foodcoop} mitzumachen.
Wenn Du mitmachen möchtest, dann fülle bitte dieses Formular aus.
Deine Daten werden selbstverständlich nicht an Dritte, aus was auch immer für Gründen, weitergeben. Du kannst auch entscheiden, wieviel deiner persönlichen Daten für alle einsehbar sein sollen. 'Alle' bedeutet hier alle Foodcoop-Mitglieder. Die Administratoren haben aber jederzeit Zugriff auf deine Daten.
" + submit: Foodsoft Account erstellen + title: Einladung in die %{name} + controller: + accept_invitation: + notice: Herzlichen Glückwunsch, Dein Account wurde erstellt. Du kannst Dich nun einloggen. + error_group_invalid: Die Gruppe, in die Du eingeladen wurdest, existiert leider nicht mehr. + error_invite_invalid: Deine Einladung ist nicht (mehr) gültig. + error_token_invalid: Ungültiger oder abgelaufener Token. Bitte versuch es erneut. + reset_password: + notice: Wenn Deine E-Mail hier registiert ist bekommst Du jetzt eine Nachricht mit einem Passwort-Zurücksetzen-Link. + update_password: + notice: Dein Passwort wurde aktualisiert. Du kannst Dich jetzt anmelden. + forgot_password: + body: "Kein Problem, Du kannst dir einfach ein neues Passwort zulegen.
Dazu musst hier die E-Mail-Adresse eingeben, mit der Du in der Foodsoft angemeldet bist. Du erhälst dann eine E-Mail mit weiteren Instruktionen.
" + submit: Neues Passwort anfordern + title: Passwort vergessen? + new_password: + body: "Bitte neues Passwort für %{user} eingeben.
" + submit: Neues passwort speichern + title: Neues Passwort + mailer: + dateformat: "%d. %b" + feedback: + header: "%{user} schrieb am %{date}:" + subject: Feedback zur Foodsoft + from_via_foodsoft: "%{name} via Foodsoft" + group_order_invoice: + subject: Bestellgruppenrechnung für %{group} bei %{supplier} + text: | + Liebe Bestellgruppe %{group}, + + Die Sammelbestellung bei %{supplier} wurde soeben abgerechnet und für die jeweiligen Bestellgruppen Rechnungen angelegt. + Im Anhang befindet sich daher eure Rechnung. + + Viele Grüße von %{foodcoop} + invite: + subject: Einladung in die Foodcoop + text: | + Hallo! + + %{user} <%{mail}> hat dich in die Gruppe "%{group}" eingeladen. + Um die Einladung anzunehmen und der Foodcoop beizutreten, gehe zu: %{link} + Dieser Link kann nur einmal aufgerufen werden und ist nur bis %{expires} gültig. + + + Grüße sendet die Foodsoft! + negative_balance: + subject: Gruppenkonto im Minus + text: | + Liebe Bestellgruppe %{group}, + + euer Kontostand is durch eine Buchung am %{when} ins Minus gerutscht: %{balance} + Es wurden %{amount} für "%{note}" abgebucht, die Buchung wurde von %{user} erstellt. + Bitte zahlt so bald wie möglich wieder Geld ein, un das Gruppenkonto auszugleichen. + + + Viele Grüße von %{foodcoop} + not_enough_users_assigned: + subject: '"%{task}" braucht noch Leute!' + text: | + Liebe(r) %{user}, + + die Aufgabe '%{task}' deiner Arbeitsgruppe ist am %{when} fällig + und es fehlen noch Mitstreiter_innen! + + Sofern du dich noch nicht für diese Aufgabe eingetragen hast ist des jetzt die Chance: + + %{workgroup_tasks_url} + + Deine Aufgaben: %{user_tasks_url} + order_result: + subject: 'Bestellung beendet: %{name}' + text0: | + Liebe Bestellgruppe %{ordergroup}, + + die Bestellung für "%{order}" wurde am %{when} von %{user} beendet. + text1: | + Sie kann voraussichtlich am %{pickup} abgeholt werden. + text2: | + Für euch wurden die folgenden Artikel bestellt: + text3: |- + o Gesamtpreis: %{sum} + + Bestellung online einsehen: %{order_url} + + + Viele Grüße von %{foodcoop} + order_received: + subject: 'Bestellung entgegengenommen: %{name}' + text0: | + Liebe Bestellgruppe %{ordergroup}, + + die Bestellung für "%{order}" wurde geliefert. + abundant_articles: Zuviel geliefert + scarce_articles: Zuwenig geliefert + article_details: | + o %{name}: + -- Bestellt: %{ordered} x %{unit} + -- Geliefert: %{received} x %{unit} + order_result_supplier: + subject: Neue Bestellung für %{name} + text: | + Guten Tag, + + die Foodcoop %{foodcoop} möchte gerne eine Bestellung abgeben. + + Im Anhang befinden sich ein PDF und eine CSV-Tabelle. + + Mit freundlichen Grüßen + %{user} + %{foodcoop} + reset_password: + subject: Neues Password für %{username} + text: | + Hallo %{user}, + + du (oder jemand anderes) hat auf der Foodsoft-Website ein neues Passwort angefordert. + Um ein neues Passwort einzugeben, gehe zu: %{link} + Dieser Link kann nur einmal aufgerufen werden und läuft am %{expires} ab. + Wenn du das Passwort nicht ändern möchtest oder diese E-Mail nicht ausgelöst hast, brauchst du nichts zu tun. + Dein bisheriges Passwort wurde nicht geändert. + + + Grüße sendet die Foodsoft! :) + upcoming_tasks: + nextweek: 'Aufgaben für die nächtste Woche:' + subject: Aufgaben werden fällig! + text0: | + Liebe(r) %{user}, + + Du bist für "%{task}" eingetragen. Die Aufgabe ist morgen (%{when}) fällig! + text1: | + Deine Aufgaben: %{user_tasks_url} + + + Viele Grüße von %{foodcoop} + welcome: + subject: Willkommen in der Foodcoop + text0: | + Liebe(r) %{user}, + + für dich wurde ein neuer Foodsoft-Zugang erstellt. + text1: | + Um ein neues Passwort einzugeben, gehe zu: %{link} + Dieser Link kann nur einmal aufgerufen werden und läuft am %{expires} ab. + Du kannst jederzeit "Passwort vergessen?" benutzen, um einen neuen Link zu erhalten. + + Viele Grüße von %{foodcoop}. + messages_mailer: + foodsoft_message: + footer: | + Antworten: %{reply_url} + Nachricht online einsehen: %{msg_url} + Nachrichten-Einstellungen: %{profile_url} + footer_group: | + Gesendet an Gruppe: %{group} + model: + delivery: + each_stock_article_must_be_unique: Lieferung darf jeden Lagerartikel höchstens einmal auflisten. + financial_transaction: + foodcoop_name: Foodcoop + financial_transaction_type: + no_delete_last: Es muss mindestens ein Kontotransaktionstyp existieren. + group_order: + stock_ordergroup_name: Lager (%{user}) + invoice: + invalid_mime: hat einen ungültigen MIME-Typ (%{mime}) + membership: + no_admin_delete: Mitgliedschaft kann nicht beendet werden. Du bist die letzte Administratorin + order_article: + error_price: muss angegeben sein und einen aktuellen Preis haben + user: + no_ordergroup: keine Bestellgruppe + group_order_article: + order_closed: Bestellung ist geschlossen und kann nicht geändert werden + navigation: + admin: + config: Einstellungen + finance: Finanzen + home: Übersicht + mail_delivery_status: E-Mail Probleme + ordergroups: Bestellgruppen + title: Administration + users: Benutzer/innen + workgroups: Arbeitsgruppen + articles: + categories: Kategorien + stock: Lager + suppliers: Lieferanten/Artikel + title: Artikel + dashboard: Übersichtsseite + finances: + accounts: Konten verwalten + balancing: Bestellungen abrechnen + bank_accounts: Bankkonten + home: Übersicht + invoices: Rechnungen + title: Finanzen + foodcoop: Foodcoop + members: Mitglieder + ordergroups: Bestellgruppen + orders: + archive: Meine Bestellungen + manage: Bestellverwaltung + ordering: Bestellen! + pickups: Abholtage + title: Bestellungen + tasks: Aufgaben + workgroups: Arbeitsgruppen + number: + percentage: + format: + strip_insignificant_zeros: true + order_articles: + edit: + stock_alert: Preise von Lagerartikeln können nicht geändert werden! + title: Artikel aktualisieren + new: + title: Neuer gelieferter Artikel der Bestellung + ordergroups: + edit: + title: Bestellgruppe bearbeiten + index: + title: Bestellgruppen + model: + error_single_group: "%{user} ist schon in einer anderen Bestellgruppe" + invalid_balance: ist keine gültige Zahl + orders: + articles: + article_count: 'Bestellte Artikel:' + prices: Netto-/Bruttopreis + prices_sum: 'Summe (Netto/Brutto-Preise):' + units_full: Volle Gebinde + members: Mitglieder + deposit: Pfand + units_ordered: Bestellte Einheiten + create: + notice: Die Bestellung wurde erstellt. + edit: + title: 'Bestellung bearbeiten: %{name}' + edit_amount: + field_locked_title: Die Verteilung dieses Artikels auf die einzelnen Bestellgruppen wurde manuell angepasst. Das Eingabefeld wurde gesperrt, um die manuellen Änderungen zu bewahren. Um den Artikel neu zu verteilen, drücke den Entsperrknopf und ändere die gelieferte Menge. + field_unlocked_title: Die Verteilung dieses Artikels auf die einzelnen Bestellgruppen wurde manuell angepasst. Wenn du die gelieferte Menge änderst, werden die vorherigen manuellen Änderungen überschrieben. + edit_amounts: + no_articles_available: Keine Artikel zum hinzufügen verfügbar + set_all_to_zero: Alle auf Null setzen + fax: + amount: Menge + articles: Artikel + delivery_day: Liefertag + heading: Bestellung für %{name} + name: Name + number: Nummer + to_address: Versandaddresse + finish: + notice: Die Bestellung wurde beendet. + form: + ignore_warnings: Warnungen ignorieren + prices: Price (netto/FC) + select_all: Alle auswählen + stockit: Verfügbar + title: Artikel + index: + action_end: Beenden + action_receive: In Empfang nehmen + confirm_delete: Willst Du wirklich die Bestellung löschen? + confirm_end: Willst Du wirklich die Bestellung %{order} beenden? Es gibt kein zurück. + new_order: Neue Bestellung anlegen + no_open_or_finished_orders: Derzeit gibt es keine laufende oder beendete Bestellungen. + orders_finished: Beendet + orders_open: Laufend + orders_settled: Abgerechnet + not_closed: Bestellung noch nicht abgerechnet + title: Bestellungen verwalten + model: + close_direct_message: Die Bestellung wurde abgechlossen, ohne die Mitgliederkonten zu belasten. + error_boxfill_before_ends: muss nach dem Kistenauffülldatum liegen (oder leer bleiben) + error_closed: Bestellung wurde schon abgerechnet + error_nosel: Es muss mindestens ein Artikel ausgewählt sein. + error_starts_before_boxfill: muss nach dem Bestellstart liegen (oder leer bleiben) + error_starts_before_ends: muss nach dem Bestellstart liegen (oder leer bleiben) + notice_close: 'Bestellung: %{name}, bis %{ends}' + stock: Lager + warning_ordered: 'Warnung: Die rot markierten Artikel wurden in der laufenden Bestellung bereits bestellt. Wenn Du sie hier abwählst, werden alle bestehenden Bestellungen dieses Artikels gelöscht.' + warning_ordered_stock: 'Warnung: Die rot markierten Artikel wurden in der laufenden Lagerbestellung bereits bestellt bzw. gekauft. Wenn Du sie hier abwählst, werden alle bestehenden Bestellungen bzw. Käufe dieses Artikels gelöscht und nicht abgerechnet!' + new: + title: Neue Bestellung anlegen + receive: + add_article: Artikel hinzufügen + consider_member_tolerance: Toleranz berücksichtigen + notice: 'Bestellung in Empfang genommen: %{msg}' + notice_none: Keine neuen Artikel für den Empfang ausgewählt. + paragraph: Wenn die Bestellmenge mit der Liefermenge übereinstimmt, können die entsprechenden Felder frei gelassen werden. Es empfiehlt sich aber dennoch alle Felder auszufüllen, da daurch einfach nachvollziebar ist ob alle Mengen kontrolliert wurden. + rest_to_stock: Rest ins Lager + submit: Bestellung in Empfang nehmen + surplus_options: 'Verteilungsoptionen:' + title: '»%{order}« in Empfang nehmen' + send_to_supplier: + notice: Die Bestellung wurde an die Lieferantin geschickt. + show: + action_end: Beenden! + amounts: 'Netto/Bruttosumme:' + articles: Artikelübersicht + articles_ordered: 'Bestellte Artikel:' + comments: + title: Kommentare + comments_link: Kommentare + confirm_delete: Willst Du wirklich die Bestellung löschen? + confirm_end: |- + Willst Du wirklich die Bestellung %{order} beenden? + Es gibt kein zurück. + confirm_send_to_supplier: Die Bestellung wurde bereit am %{when} zur Lieferantin geschickt. Willst du sie wirklich erneut schicken? + create_invoice: Rechnung anlegen + description1_order: "%{state} Bestellung von %{supplier} angelegt von %{who}," + description1_period: + pickup: und kann am %{pickup} abgeholt werden + starts: läuft von %{starts} + starts_ends: läuft von %{starts} bis %{ends} + description2: "%{ordergroups} haben %{article_count} Artikel mit einem Gesamtwert von %{net_sum} / %{gross_sum} (netto / brutto + fc + Pfand) bestellt." + description3: " Zuzüglich Pfand %{net_deposit} / %{deposit} (netto / brutto)." + group_orders: 'Gruppenbestellungen:' + search_placeholder: + articles: Suche nach Artikeln ... + default: Suche nach Artikeln ... + groups: Suche nach Bestellgruppen ... + search_reset: Suche zurücksetzen + send_to_supplier: An Lieferantin schicken + show_invoice: Rechnung anzeigen + sort_article: Sortiert nach Artikeln + sort_group: Sortiert nach Gruppen + stock_order: Lagerbestellung + title: 'Bestellung: %{name}' + warn_not_closed: Achtung, Bestellung wurde noch nicht abgerechnet. + state: + closed: abgerechnet + finished: beendet + open: laufend + received: in Empfang genommen + update: + notice: Die Bestellung wurde aktualisiert. + update_order_amounts: + msg1: "%{count} Artikel (%{units} Einheiten) aktualisiert" + msg2: "%{count} (%{units}) Toleranzmenge" + msg4: "%{count} (%{units}) übrig" + pickups: + document: + empty_selection: Es muss zumindest eine Bestellung ausgewählt sein. + filename: Abholungen für %{date} + invalid_document: Ungültiger Dokumententyp + title: Abholungen für %{date} + index: + article_pdf: Artikel PDF + group_pdf: Gruppen PDF + matrix_pdf: Matrix PDF + title: Abholtage + sessions: + logged_in: Angemeldet! + logged_out: Abgemeldet! + login_invalid_email: Ungültige E-Mail Adresse oder Passwort + login_invalid_nick: Benutzername oder Passwort ungültig + new: + forgot_password: Passwort vergessen? + login: Anmelden + nojs: Achtung, Cookies und Javascript müssen aktiviert sein! %{link} bitte abschalten. + noscript: NoScript + title: Foodsoft anmelden + shared: + articles: + ordered: Bestellt + ordered_desc: Anzahl der Artikel die durch das Mitglied bestellt wurden (Menge + Toleranz) + received: Bekommen + received_desc: Anzahl der Artikel die ein Mitglied erhält + articles_by: + price: Gesamtpreis + price_sum: Summe + group: + access: Zugriff auf + activated: aktiviert + apple_limit: Äpfel-Bestellbeschränkung + break: von %{start} bis %{end} + deactivated: deaktiviert + group_form_fields: + search: Suche ... + search_user: Nach Nutzerin suchen + user_not_found: Keine Nutzerin gefunden + open_orders: + no_open_orders: Derzeit gibt es keine laufenden Bestellungen + not_enough_apples: Achtung, Deine Bestellgruppe hat zu wenig Äpfel um Bestellen zu können! + title: Laufende Bestellungen + total_sum: Gesamtsumme + who_ordered: Wer hat bestellt? + order_download_button: + article_pdf: Artikel PDF + download_file: Datei herunterladen + fax_csv: Fax CSV + fax_pdf: Fax PDF + fax_txt: Fax Text + group_pdf: Gruppen PDF + matrix_pdf: Matrix PDF + title: Herunterladen + task_list: + accept_task: Aufgabe übernehmen + done: Erledigt + done_q: Erledigt? + mark_done: Aufgabe als erledigt markieren + reject_task: Aufgabe ablehnen + who: Wer machts? + who_hint: "(Wie viele werden noch benötigt?)" + user_form_fields: + contact_address_hint: Die Adresse deiner Bestellgruppe. Wenn du sie aktualisierst, wird sie auch bei den anderen Mitgliedern deiner Bestellgruppe aktualisiert. + messagegroups: Nachrichtengruppen beitreten oder verlassen + workgroup_members: + title: Mitglieder der Gruppen + simple_form: + error_notification: + default_message: Fehler wurden gefunden. Bitte das Formular überprüfen. + hints: + article: + unit: z.B. KG oder 1L oder 500g + article_category: + description: Kommagetrennte Liste von Kategorienamen die beim Import/Datenabgleich erkannt wurden + order_article: + units_to_order: Wenn Du die Gesamtanzahl gelieferter Gebinde änderst, musst Du auch die individuelle Anzahl der einzelnen Bestellgruppen anpassen, indem Du auf den Artikelnamen klickst. Sie werden nicht automatisch neuberechnet und andernfalls werden den Bestellgruppen Artikel in Rechnung gestellt, die nicht geliefert wurden! + update_global_price: Ändert auch den Preis für zukünftige Bestellungen + stock_article: + copy: + name: Bitte ändern + edit_stock_article: + price: "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: + last_deliveries: Letzte Lieferungen + last_orders: Letzte Bestellungen + new_delivery: Neue Lieferung anlegen + show_deliveries: Zeige alle Lieferungen + update: + notice: Lieferant wurde aktualisiert + tasks: + accept: + notice: Du hast die Aufgabe übernommen + archive: + title: Aufgabenarchiv + create: + notice: Aufgabe wurde erstellt + destroy: + notice: Aufgabe wurde gelöscht + edit: + submit_periodic: Wiederkehrende Aufgabe speichern + title: Aufgabe bearbeiten + title_periodic: Wiederkehrende Aufgabe ändern + warning_periodic: "Warnung: Diese Aufgabe ist Teil einer Gruppe von wöchentlichen Aufgaben. Beim Speichern wird sie aus der Gruppe ausgeschlossen und in eine gewöhnliche Aufgabe umgewandelt." + error_not_found: Keine Arbeitsgruppe gefunden + form: + search: + hint: Nach Nutzerin suchen + noresult: Keine Nutzerin gefunden + placeholder: Suche ... + submit: + periodic: Wiederkehrende Aufgabe speichern + index: + show_group_tasks: Gruppenaufgaben anzeigen + title: Aufgaben + title_non_group: Aufgaben für alle! + nav: + all_tasks: Alle Aufgaben + archive: Erledigte Aufgaben (Archiv) + group_tasks: Gruppenaufgaben + my_tasks: Meine Aufgaben + new_task: Neue Aufgabe erstellen + pages: Seiten + new: + submit_periodic: Wiederkehrende Aufgabe anlegen + title: Neue Aufgabe erstellen + repeated: Aufgabe wird wiederholt + set_done: + notice: Aufgabenstatus wurde aktualisiert + show: + accept_task: Aufgabe übernehmen + confirm_delete_group: Diese und alle folgenden wöchentlichen Aufgaben wirklich löschen? + confirm_delete_single: Die Aufgabe wirklich löschen? + confirm_delete_single_from_group: Bist Du sicher, dass Du diese Aufgabe löschen möchtest (und in Bezug stehende wiederkehrende Aufgabe behalten möchtest)? + delete_group: Aufgabe und folgende löschen + edit_group: Wiederkehrende ändern + hours: "%{count}h" + mark_done: Als erledigt markieren + reject_task: Aufgabe ablehnen + title: Aufgabe anzeigen + update: + notice: Aufgabe wurde aktualisiert + notice_converted: Aufgabe wurde aktualisiert und in eine gewöhnliche Aufgabe umgewandelt + user: + more: Nichts zu tun? %{tasks_link} gibt es bestimmt Arbeit + tasks_link: Hier + title: Meine Aufgaben + title_accepted: Anstehende Aufgaben + title_open: Offene Aufgaben + workgroup: + title: Aufgaben für %{workgroup} + title_all: Alle Aufgaben der Gruppe + ui: + actions: Aktionen + back: Zurück + cancel: Abbrechen + close: Schließen + confirm_delete: Willst du %{name} wirklich löschen? + confirm_restore: Willst du %{name} wirklich wiederherstellen? + copy: Kopieren + delete: Löschen + download: Herunterladen + edit: Bearbeiten + marks: + close: "×" + success: + move: Verschieben + or_cancel: oder abbrechen + please_wait: Bitte warten... + restore: Wiederherstellen + save: Speichern + search_placeholder: Suchen ... + show: Anzeigen + views: + pagination: + first: "«" + last: "»" + next: "›" + previous: "‹" + truncate: "..." + workgroups: + edit: + title: Arbeitsgruppe bearbeiten + error_last_admin_group: Die letzte Gruppe mit Admin-Rechten darf nicht gelöscht werden + error_last_admin_role: Der letzten Gruppe mit Admin-Rechten darf die Admin-Rolle nicht entzogen werden + index: + title: Arbeitsgruppen + update: + notice: Arbeitsgruppe wurde aktualisiert + time: + formats: + foodsoft_datetime: "%d.%m.%Y %H:%M" + file: "%Y-%d-%B" \ No newline at end of file diff --git a/plugins/automatic_invoices/config/locales/en.yml b/plugins/automatic_invoices/config/locales/en.yml new file mode 100644 index 00000000..b259b68b --- /dev/null +++ b/plugins/automatic_invoices/config/locales/en.yml @@ -0,0 +1,1992 @@ +en: + activerecord: + attributes: + article: + article_category: Category + availability: Is article available? + availability_short: avail. + deposit: Deposit + fc_price: FoodCoop price + fc_price_desc: Price including taxes, deposit and Foodcoop-charge. + fc_price_short: FC price + fc_share: FoodCoop margin + fc_share_short: FC margin + gross_price: Gross price + manufacturer: Manufacturer + name: Name + note: Note + order_number: Order number + order_number_short: Nr. + origin: Origin + price: Price (net) + supplier: Supplier + tax: VAT + unit: Unit + unit_quantity: Unit quantity + unit_quantity_short: U.Q. + units: Units + article_category: + description: Import names + name: Name + article_price: + deposit: Deposit + price: Price (net) + tax: VAT + unit_quantity: Unit quantity + bank_account: + balance: Balance + bank_gateway: Bank gateway + description: Description + iban: IBAN + name: Name + bank_gateway: + authorization: Authorization-Header + name: Name + unattended_user: Unattended user + url: URL + bank_transaction: + amount: Amount + date: Date + external_id: External ID + financial_link: Financial link + iban: IBAN + reference: Referenz + text: Description + delivery: + date: Delivery date + note: Note + supplier: Supplier + document: + created_at: Created at + created_by: Created by + data: Data + mime: MIME type + name: Name + financial_transaction: + amount: Amount + created_on: Date + financial_transaction_class: Financial transaction class + financial_transaction_type: Financial transaction type + note: Note + ordergroup: Ordergroup + user: Entered by + financial_transaction_class: + ignore_for_account_balance: Ignore for account balance + name: Name + financial_transaction_type: + bank_account: Bank Account + name: Name + financial_transaction_class: Financial transaction class + name_short: Short Name + group_order: + ordergroup: Ordergroup + price: Order sum + updated_by: Last ordered by + group_order_article: + ordered: Ordered + quantity: Amount + received: Received + result: Amount + tolerance: Tolerance + total_price: Sum + unit_price: Price/Unit + group_order_invoice: + name: Group order invoice + links: + delete: delete invoice + download: download invoice + invoice_date: date of group order invoice + generate: generate invoice + generate_with_date: set & generate + download_all_zip: download all invoices as zip + + payment_method: Credit + tax_number_not_set: Tax number not set in configs + invoice: + amount: Amount + attachment: Attachment + created_at: Created at + created_by: Created by + date: Billing date + delete_attachment: Delete attachment + deliveries: Stock delivery + deposit: Deposit charged + deposit_credit: Deposit returned + financial_link: Financial link + net_amount: Amount adjusted for refund + note: Note + number: Number + orders: Order + paid_on: Paid on + supplier: Supplier + mail_delivery_status: + created_at: Time + email: Email + message: Message + order: + boxfill: Fill boxes after + closed_by: Settled by + created_by: Created by + end_action: End action + end_actions: + auto_close: Close the order + auto_close_and_send: Close the order and send it to the supplier + auto_close_and_send_min_quantity: Close the order and send it to the supplier if the minimum quantity has been reached + no_end_action: No automatic action + ends: Ends at + name: Supplier + note: Note + pickup: Pickup + starts: Starts at + status: Status + supplier: Supplier + transport: Transport costs + transport_distribution: Transport costs distribution + transport_distributions: + articles: Distribute the costs among the number of received articles + ordergroup: Every ordergroup pays the same amount + price: Distribute the costs among the order sum + skip: Do not distribute the costs + updated_by: Last edited by + order_article: + article: Article + missing_units: Missing units + missing_units_short: Missing + quantity: Desired amount + quantity_short: Desired + units_received: Received units + units_received_short: Received + units_to_order: Ordered units + units_to_order_short: Ordered + update_global_price: Globally update current price + order_comment: + text: Add comment to this order ... + ordergroup: + account_balance: Account balance + available_funds: Available credit + break: "(Last) break" + break_until: until + contact: Contact + contact_address: Address + contact_person: Contact person + contact_phone: Phone + customer_number: Customer number + description: Description + ignore_apple_restriction: Ignore order stop by apple points restriction + last_order: Last order + last_user_activity: Last activity + name: Name + user_tokens: Members + stock_article: + available: Available + price: Price + quantity: In stock + quantity_available: Available quantity + quantity_available_short: Avail. + quantity_ordered: Ordered + stock_taking: + date: Date + note: Note + supplier: + address: Address + contact_person: Contact person + customer_number: Customer number + customer_number_short: Cust.nr. + delivery_days: Delivery days + email: Email + fax: Fax + iban: IBAN + is_subscribed: subscribed? + min_order_quantity: Minimum order quantity + min_order_quantity_short: Min. quantity + name: Name + note: Note + order_howto: How to order + phone: Phone + phone2: Phone 2 + shared_sync_method: How to synchronize + url: Homepage + supplier_category: + name: Name + description: Description + financial_transaction_class: Financial transaction class + bank_account: Bank account + task: + created_by: Created by + created_on: Created at + description: Description + done: Done? + due_date: Due date + duration: Duration + name: Activity + required_users: People required + user_list: Responsible users + workgroup: Workgroup + user: + email: Email + first_name: First name + iban: IBAN + last_activity: Last activity + last_login: Last login + last_name: Last name + name: Name + nick: Username + ordergroup: Ordergroup + password: Password + password_confirmation: Repeat password + phone: Telephone + workgroup: + one: Workgroup + other: Workgroups + workgroup: + description: Description + name: Name + role_admin: Administration + role_article_meta: Article database + role_finance: Finances + role_invoices: Invoices + role_orders: Order management + role_pickups: Pickup days + role_suppliers: Suppliers + user_tokens: Members + errors: + has_many_left: is still associated with a %{collection}! + models: + article: + attributes: + name: + taken: name is already taken + taken_with_unit: name and unit are already taken + supplier: + attributes: + shared_sync_method: + included: is not a valid option for this supplier + task: + attributes: + done: + exclusion: finished tasks may not be repeated + models: + article: Article + article_category: Category + bank_account: Bank account + bank_gateway: Bank gateway + bank_transaction: Bank transaction + delivery: Delivery + financial_transaction: Financial transaction + financial_transaction_class: Financial transaction class + financial_transaction_type: Financial transaction type + invoice: Invoice + order: Order + order_article: Order article + order_comment: Order comment + ordergroup: + one: Ordergroup + other: Ordergroups + stock_article: Stock article + stock_taking: Stock taking + supplier: Supplier + supplier_category: Supplier category + task: Task + user: User + workgroup: Workgroup + admin: + access_to: access to + base: + index: + all_ordergroups: All ordergroups + all_users: All users + all_workgroups: All workgroups + created_at: created at + first_paragraph: Here you can administer Foodsoft groups and users. + groupname: group name + members: members + name: name + new_ordergroup: New ordergroup + new_user: New user + new_workgroup: New workgroup + newest_groups: newest groups + newest_users: newest users + title: Administration + type: type + username: username + bank_accounts: + form: + title_edit: Edit bank account + title_new: Add new bank account + bank_gateways: + form: + title_edit: Edit bank gateway + title_new: Add new bank gateway + configs: + list: + key: Key + title: Configuration list + value: Value + show: + submit: Save + title: Configuration + tab_layout: + pdf_title: PDF documents + tab_messages: + emails_title: Sending email + tab_payment: + schedule_title: Ordering schedule + group_order_invoices: Group order invoices + tab_security: + default_roles_title: Access to + default_roles_paragraph: By default every member of the foodcoop has access to the following areas. + tab_tasks: + periodic_title: Periodic tasks + tabs: + title: Configuration + update: + notice: Configuration saved. + confirm: Are you sure? + finances: + index: + bank_accounts: Bank accounts + first_paragraph: Here you can manage the financial transaction classes and the corresponding financial transaction types. Every financial transaction has a type, which you have to select at every transaction, if you created more than one type. The financial transaction classes can be use to group the financial transaction types and will be shown as additional columns in the account overview, if there have been created more than one. + new_bank_account: Add new bank account + new_financial_transaction_class: Add new financial transaction class + new_bank_gateway: Add new bank gateway + title: Finances + transaction_types: Financial transaction types + supplier_categories: Supplier categories + new_supplier_category: New supplier category + transaction_types: + name: Name + new_financial_transaction_type: Add new financial transaction type + financial_transaction_classes: + form: + title_edit: Edit financial transaction class + title_new: Add new financial transaction class + financial_transaction_types: + form: + name_short_desc: The short name is mandatory for financial transaction types which should be automatically assignable in bank transactions. If there are multiple bank accounts, the preferred target account for bank transfers can be selected. + title_edit: Edit financial transaction type + title_new: Add new financial transaction type + mail_delivery_status: + destroy_all: + notice: All email problems were deleted + index: + destroy_all: Delete all email problems + title: Email problems + ordergroups: + destroy: + error: 'Ordergroup could not be marked as deleted: %{error}' + notice: Ordergroup was marked as deleted + edit: + title: Edit ordergroup + form: + first_paragraph: You can invite new members %{url}. + here: here + index: + first_paragraph: Here you can add %{url}, and edit or delete groups. + new_ordergroup: Add new ordergroup + new_ordergroups: new ordergroups + second_paragraph: 'Consider the difference between group and ordergroup: An ordergroup has an account and can order food. In a %{url} (for example ''sorting group''), members coordinate with each other via tasks and messages. Users can only be in one ordergroup, but can be in multiple workgroups.' + title: Ordergroups + workgroup: workgroup + new: + title: Create ordergroup + show: + confirm: Are you sure? + edit: Edit group/members + title: Ordergroup %{name} + search_placeholder: name ... + users: + controller: + sudo_done: You are now logged in as %{user}. Be careful, and do not forget to log out when done! + destroy: + error: 'User could not be deleted: %{error}' + notice: User was deleted + edit: + title: Edit user + form: + create_ordergroup: Create ordergroup with the same name and add user. + send_welcome_mail: Send a welcome mail to the user. + index: + first_paragraph: Here you can %{url}, edit and delete users. + new_user: Create new user + new_users: create new + show_deleted: Show deleted users + title: User admin + new: + title: Create new user + restore: + error: 'User could not be restored: %{error}' + notice: User was restored + show: + confirm_sudo: If you continue, you will take on the identity of %{user}. Do not forget to log out when you're done! + groupabos: Group subscriptions + member_since: Member since %{time} + person: Person + preference: Preferences + show_email_problems: Show email problems + sudo: Take on identity + users: + show_email_problems: Show email problems + workgroups: + destroy: + error: 'Workgroup could not be deleted: %{error}' + notice: Workgroup was deleted + edit: + title: Edit workgroup + form: + first_paragraph: You can invite new members %{url}. + here: here + index: + first_paragraph: Here you can create %{url}, edit and delete them. + new_workgroup: Create new workgroup + new_workgroups: new workgroups + ordergroup: ordergroup + second_paragraph: 'Consider the difference between a workgroup and an ordergroup: an %{url} has an account and can order food. In a workgroup (for example ''sorting group''), members coordinate with each other via tasks and messages. Users can only be in one ordergroup, but can be in multiple workgroups.' + title: Workgroups + new: + title: Create workgroup + show: + confirm: Are you sure? + edit: Edit group/members + title: Workgroup %{name} + workgroups: + members: members + name: name + supplier_categories: + form: + title_new: Add supplier category + title_edit: Edit supplier category + application: + controller: + error_authn: Authentication required! + error_denied: You are not allowed to view the requested page. If you think you should, ask an administrator to give you appropriate permissions. If you have access to multiple user accounts, you might want to %{sign_in}. + error_denied_sign_in: sign in as another user + error_feature_disabled: This feature is currently disabled. + error_members_only: This action is only available to members of the group! + error_minimum_balance: Sorry, your account balance is below the minimum of %{min}. + error_token: Access denied (invalid token)! + article_categories: + create: + notice: Category was stored + destroy: + error: 'Category could not be deleted: %{message}' + edit: + title: Edit category + index: + new: Add new category + title: Article categories + new: + title: Add new category + update: + notice: Category was updated + articles: + article: + last_update: 'last updated: %{last_update} | gross: %{gross_price}' + articles: + confirm_delete: Do you really want to delete all selected articles? + option_available: Make articles available + option_delete: Delete article + option_not_available: Make articles unavailable + option_select: Select action ... + price_netto: Price + unit_quantity_desc: Unit quantity + unit_quantity_short: U.Q. + controller: + create_from_upload: + notice: "%{count} new articles were saved." + error_invalid: There are errors in articles + error_nosel: No articles selected + error_parse: "%{msg} ... in line %{line}" + error_update: 'An error occured when updating article ''%{article}'': %{msg}' + parse_upload: + no_file: Please select a file to upload. + notice: "%{count} articles were succesfully analysed." + sync: + notice: Catalog is up to date + shared_alert: "%{supplier} is not linked to an external database" + update_all: + notice: All articles and prices were updated. + update_sel: + notice_avail: All selected articles were set to be available. + notice_destroy: All selected articles were deleted. + notice_noaction: No action specified! + notice_unavail: All selected articles were set to be unavailable. + update_sync: + notice: All articles and prices were updated. + destroy_active_article: + drop: delete + note: "%{article} is used in current orders and can not be deleted Please first ... the article from orders %{drop_link}." + edit_all: + note: 'Mandatory fields are: name, unit, (net) price and order number.' + submit: Update all articles + title: Edit all articles from %{supplier} + warning: 'Warning: all articles will be updated!' + form: + title_edit: Edit article + title_new: Add new article + import_search_results: + action_import: import + already_imported: imported + not_found: No articles found + index: + change_supplier: Change supplier ... + download: Download articles + edit_all: Edit all + ext_db: + import: Import article + sync: Synchronise + import: + category: Directly import into category + placeholder: Search by name ... + restrict_region: Restrict to region only + title: Import article + new: New article + new_order: Create new order + search_placeholder: Name ... + title: Articles from %{supplier} (%{count}) + upload: Upload articles + model: + error_in_use: "%{article} can not be deleted because the article is part of a current order!" + error_nosel: You have selected no articles + parse_upload: + body: "Please verify the articles.
Warning, at the moment there is no check for duplicate articles.
" + submit: Process upload + title: Upload articles + sync: + outlist: + alert_used: Warning, %{article} is used in an open order. Please remove it from the order first. + body: 'The following articles were removed from the list and will be deleted:' + body_ignored: + one: One article without order number was skipped. + other: "%{count} articles without order number were skipped." + body_skip: No articles to delete. + title: Remove from list ... + price_short: Price + submit: Synchronize all + title: Synchronize articles with external database + unit_quantity_short: Unit quantity + update: + body: 'Every article is shown twice: old values are gray, while the text fields contain updated values. Differences with the old articles are marked yellow.' + title: Update ... + update_msg: + one: One article needs to be updated. + other: "%{count} articles need to be updated." + upnew: + body_count: + one: There is one new article to add. + other: There are %{count} articles to add. + title: Add new ... + upload: + fields: + reserved: "(Reserved)" + status: Status (x=skip) + file_label: Please choose a compatible file + options: + convert_units: Keep current units, recompute unit quantity and price (like synchronize). + outlist_absent: Delete articles not in uploaded file. + sample: + juices: Juices + nuts: Nuts + organic: Organic + supplier_1: Nuttyfarm + supplier_2: Brownfields + supplier_3: Greenfields + tomato_juice: Tomato juice + walnuts: Walnuts + submit: Upload file + text_1: 'Here you can upload a spreadsheet to update the articles of %{supplier}. Excel (xls, xlsx) and OpenOffice (ods) spreadsheets are accepted, as well as comma-separated files (csv, columns separated by ";" with utf-8 encoding). Only the first sheet will be imported, and columns must be in the following order:' + text_2: The rows shown here are examples. When there is an "x" in the first column, the article is outlisted and will be removed. This allows you to edit the spreadsheet and quickly remove many articles at once, for example when articles become unavailable with the supplier. The category will be matched to your Foodsoft category list (both by category name and import names). + title: Upload articles of %{supplier} + bank_account_connector: + confirm: Please confirum the code %{code}. + fields: + email: E-Mail + pin: PIN + password: Password + tan: TAN + username: Username + config: + hints: + applepear_url: Website where the apple and pear system for tasks is explained. + charge_members_manually: When you keep track of who received what elsewhere (e.g. on paper), and you don't want to enter this into Foodsoft, select this. You'll need to charge member accounts manually (using "Add new transactions"). You still need to settle orders in the balancing screen, but this will not charge any member accounts. + contact: + email: General contact email address, shown on website as well as some forms. + street: Address, typically this will be your delivery and pick-up location. + currency_space: Whether to add whitespace after the currency symbol. + currency_unit: Currency symbol for displaying prices. + custom_css: To modify the layout of this site, you can enter style modifications using the cascading stylesheets (CSS) language. Leave blank for the default style. + email_from: Emails will appear to be from this email address. Leave empty to use the foodcoop's contact address. + email_replyto: Set this when you want to receive replies from emails sent by Foodsoft on a different address than the above. + email_sender: Emails will appear to be sent from this email address. To avoid emails sent being classified as spam, the webserver may need to be registered in the SPF record of the email address's domain. + use_automatic_invoices: A listing of the invoice items is made without explicit display of VAT and the invoice receives the necessary addition regarding the small business regulation §19 (applies to Germany) + payment_method: Payment type is declared on the order group invoice + vat_exempt: A listing of the invoice items is made without explicit display of VAT and the invoice contains the necessary addition regarding the German Kleinunternehmerregelung §19 UStG (attention! FoodCoop marge not included in nvoice). + help_url: Documentation website. + homepage: Website of your foodcoop. + ignore_browser_locale: Ignore the language of user's computer when the user has not chosen a language yet. + minimum_balance: Members can only order when their account balance is above or equal to this amount. + name: The name of your foodcoop. + order_schedule: + boxfill: + recurr: Schedule for when the box-fill phase starts by default. + time: Default time when the box-fill phase of the ordering starts. + ends: + recurr: Schedule for default order closing date. + time: Default time when orders are closed. + initial: Schedule starts at this date. + page_footer: Shown on each page at the bottom. Enter "blank" to disable the footer completely. + pdf_add_page_breaks: + order_by_articles: Put each article on a separate page. + order_by_groups: Put each ordergroup on a separate page. + pdf_font_size: Base font size for PDF documents (12 is standard). + pdf_page_size: Page size for PDF documents, typically "A4" or "letter". + price_markup: Percentage that is added to the gross price for foodcoop members. + stop_ordering_under: Members can only order when they have at least this many apple points. + tasks_period_days: Number of days between two periodic tasks (default 7, which is a week). + tasks_upfront_days: For how many days in advance you would like to schedule periodic tasks. + tax_default: Default VAT percentage for new articles. + tolerance_is_costly: Order as much of the member tolerance as possible (compared to only as much needed to fill the last box). Enabling this also includes the tolerance in the total price of the open member order. + distribution_strategy: How articles should be distributed after an order has been received. + use_apple_points: When the apple point system is enabled, members are required to do some tasks to be able to keep ordering. + use_automatic_invoices: When an order is settled, invoices for the individual order groups are automatically sent by mail + payment_method: Payment Method for group order invoices + use_boxfill: When enabled, near end of an order, members are only able to change their order when increases the total amount ordered. This helps to fill any remaining boxes. You still need to set a box-fill date for the orders. + use_iban: When enabled, supplier and user provide an additonal field for storing the international bank account number. + use_nick: Show and use nicknames instead of real names. When enabling this, please check that each user has a nickname. + use_self_service: When enabled, members are able to use selected balancing functions on their own. + webstats_tracking_code: Tracking code for web analytics (like Piwik or Google analytics). Leave empty for no tracking. + keys: + applepear_url: Apple system help URL + charge_members_manually: Charge members manually + contact: + city: City + country: Country + email: Email + phone: Phone + street: Street + zip_code: Postcode + tax_number: Tax number + currency_space: add space + currency_unit: Currency + custom_css: Custom CSS + default_locale: Default language + default_role_article_meta: Articles + default_role_finance: Finance + default_role_invoices: Invoices + default_role_orders: Orders + default_role_pickups: Pickup days + default_role_suppliers: Suppliers + disable_invite: Disable invites + disable_members_overview: Disable members list + email_from: From address + email_replyto: Reply-to address + email_sender: Sender address + help_url: Documentation URL + homepage: Homepage + ignore_browser_locale: Ignore browser language + minimum_balance: Minimum balance + name: Name + order_schedule: + boxfill: + recurr: Box fill after + time: time + ends: + recurr: Order ends + time: time + initial: Schedule start + page_footer: Page footer + pdf_add_page_breaks: Page breaks + pdf_font_size: Font size + pdf_page_size: Page size + price_markup: Foodcoop margin + stop_ordering_under: Minimum apple points + tasks_period_days: Period + tasks_upfront_days: Create upfront + tax_default: Default VAT + time_zone: Time zone + tolerance_is_costly: Tolerance is costly + distribution_strategy: Distribution strategy + distribution_strategy_options: + first_order_first_serve: First distribute to those who ordered first + no_automatic_distribution: No automatic distribution + use_apple_points: Apple points + group_order_invoices: + use_automatic_invoices: Send automatically via mail after oder settlement + payment_method: Payment method + separate_deposits: Separate deposits on invoice + vat_exempt: This foodcoopis VAT exempt + use_boxfill: Box-fill phase + use_iban: Use IBAN + use_nick: Use nicknames + use_self_service: Use self service + webstats_tracking_code: Tracking code + tabs: + applications: Apps + foodcoop: Foodcoop + language: Language + layout: Layout + list: List + messages: Messages + others: Other + payment: Finances + security: Security + tasks: Tasks + deliveries: + add_stock_change: + how_many_units: 'How many units (%{unit}) to deliver? Stock article name: %{name}.' + create: + notice: Delivery was created. Please don’t forget to add an invoice! + destroy: + notice: Delivery was deleted. + edit: + title: Edit delivery + form: + confirm_foreign_supplier_reedit: The stock article %{name} was successfully saved. However, it belongs to a different supplier than this delivery. Would you like to edit the stock article again? + create_from_blank: Create new article + create_stock_article: Create stock article + title_fill_quantities: 2. Set delivery quantities + title_finish_delivery: 3. Finish delivery + title_select_stock_articles: 1. Select stock articles + index: + confirm_delete: Are you sure? + new_delivery: 'Create new delivery for %{supplier} ' + title: "%{supplier}/deliveries" + invoice_amount: Invoice amount + invoice_net_amount: Invoice net amount + new: + title: New delivery from %{supplier} + show: + sum: Sum + sum_diff: Gross - adjusted invoice amount + sum_gross: Gross sum + sum_net: Net sum + title: Show delivery + title_articles: Articles + stock_article_for_adding: + action_add_to_delivery: Add to delivery + action_edit: Edit + action_other_price: Copy + stock_change_fields: + remove_article: Remove article from delivery + suppliers_overview: Supplier overview + update: + notice: Delivery was updated. + documents: + group_order_invoice_pdf: + ordergroup: + contact_phone: 'Phone: %{contact_phone}' + contact_address: 'Adress : %{contact_address}' + customer_number: 'Customer number: %{customer_number}' + name: 'Ordergroup: %{ordergroup}' + filename: Invoice%{number} + invoicee: Invoicee + invoicer: Invoicer + invoice_date: 'Invoice date: %{invoice_date}' + invoice_number: 'Invoice number: %{invoice_number}' + markup_included: incl Foodcoop Marge on gross price %{marge}% + payment_method: 'Payment_method: %{payment_method}' + small_business_regulation: As a small entrepreneur in the sense of §19 para. 1 of the Umsatzsteuergesetz (UStG), no value added tax is charged. + sum_to_pay: Total sum + sum_to_pay_net: Total sum (net) + sum_to_pay_gross: Total sum (gross) + table_headline: 'The following items will be charged for the order:' + tax_excluded: excl. MwSt. + tax_included: incl. VAT %{tax}% + tax_number: 'Tax number: %{number}' + title: Invoice for order at %{supplier} + vat_exempt_rows: + - Name + - Quantity + - Unit price + - Total price + no_price_markup_rows: + - Name + - Quantity + - Unit price (net) + - Total price (net) + - VAT + - Total price (gross) + price_markup_rows: + - Name + - Quantity + - Unit price (net) + - Total price (net) + - VAT + - Total price (gross) incl. foodcoop margin + order_by_articles: + filename: Order %{name}-%{date} - by articles + title: 'Order sorted by articles: %{name}, closed at %{date}' + order_by_groups: + filename: Order %{name}-%{date} - by group + sum: Sum + title: 'Order sorted by group: %{name}, closed at %{date}' + order_fax: + filename: Order %{name}-%{date} - Fax + rows: + - Order Number + - Amount + - Name + - Unit quantity + - Unit + - Price/Unit + - Subtotal + total: Total + order_matrix: + filename: Order %{name}-%{date} - sorting matrix + heading: Article overview (%{count}) + title: 'Order sorting matrix: %{name}, closed at %{date}' + errors: + check_tax_number: Please check whether the foodcoop's tax number is set correctly. + general: A problem has occured. + general_again: A problem has occured. Please try again. + general_msg: 'A problem has occured: %{msg}' + internal_server_error: + text1: An unexpected error has occured. Sorry! + text2: We've been notified. If this remains a problem, please tell us so. + title: Internal server error + not_found: + text: This page does not appear to exist, sorry! + title: Page not found + feedback: + create: + notice: Your feedback was sent successfully. Thanks a lot! + new: + first_paragraph: Found a bug? Suggestions? Ideas? Reviews? We are happy to hear any feedback. + second_paragraph: Please be aware that the Foodsoft Team is only responsible for the maintenance of the software. For questions regarding the organisation of your Foodcoop, please contact the appropriate contact person. + send: Send + title: Give feedback + finance: + balancing: + close: + alert: 'An error occured while accounting: %{message}' + notice: Order was settled succesfully, the balance of the account was updated. + settings_not_set: No emails with order group invoices sent. Please check the settings. Tax number set? + close_all_direct_with_invoice: + notice: '%{count} orders have been settled.' + close_direct: + alert: 'Order can not be settled: %{message}' + notice: Order was settled. + confirm: + clear: Settle + first_paragraph: 'When the order is settled, all group accounts will be updated.Here you can write a message to the members of your Foodcoop. If you'd like others to contact you, please enable it in your %{profile_link}.
" + ph_name: Name ... + ph_ordergroup: Ordergroup ... + profile_link: options + title: Users + workgroups: + edit: + invite_link: here + invite_new: You can invite new members %{invite_link}. + title: Edit group + index: + body: "Editing a group is only available to members of the group.
If you want to join a group, please send the members a message.
Here you can invite someone who isn't part of the foodcoop to join your ordergroup %{group}. After the invitation is accepted, the person will be able to add (and remove) articles to your order.
This is a great way to introduce someone to the foodcoop, or to order with multiple people in the same household.
" + title: Invite person + new: + action: Send invite + body: "Here you can add a person to the group %{group}, who is not yet a member of the foodcoop.
" + success: User was invited successfully. + js: + ordering: + confirm_change: Modifications to this order will be lost when you change the order. Do you want to lose the changes you made and continue? + trix_editor: + file_size_alert: The file is to large! The supported file size is 512Mb! + layouts: + email: + footer_1_separator: "--" + footer_2_foodsoft: 'Foodsoft: %{url}' + footer_3_homepage: 'Foodcoop: %{url}' + footer_4_help: 'Help: %{url}' + help: 'Help' + foodsoft: Foodsoft + footer: + revision: revision %{revision} + header: + feedback: + desc: Found a bug? Suggestions? Ideas? Review? + title: Feedback + help: Help + logout: Logout + ordergroup: My ordergroup + profile: Edit profile + reference_calculator: Reference Calculator + logo: "foodsoft" + lib: + render_pdf: + page: Page %{number} of %{count} + login: + accept_invitation: + body: "You are invited to be part of the foodcoop %{foodcoop} as a member of the group %{group}.
If you want to participate, please fill in this form.
Naturally, your information wll not be shared with third parties for any reason. You can decide how much of your personal information should be visible for everyone. 'All' means all Foodcoop-members. Please note that the administrators do have access to your information.
" + submit: Create a Foodsoft account + title: Invitation to %{name} + controller: + accept_invitation: + notice: Congratulations, your account was created. You can login now. + error_group_invalid: The group in which you were invited doesn’t exist anymore. + error_invite_invalid: Your invite is not valid (anymore). + error_token_invalid: Invalid or expired token. Please try again. + reset_password: + notice: If your email is registered here, you will receive a message with a link to reset your password. You may need to check your spam folder. + update_password: + notice: Your password was updated. You can login now. + forgot_password: + body: "No problem, you can choose a new password.
Please fill in the email address with which you are registered here. Then you will receive an email with further instructions.
" + submit: Request new password + title: Forgot password? + new_password: + body: "Please fill in the new password for %{user}
" + submit: Save new password + title: New password + mailer: + dateformat: "%d %b" + feedback: + header: "%{user} wrote at %{date}:" + subject: Feedback for Foodsoft + group_order_invoice: + subject: Order group invoice for %{group} at %{supplier} + text: | + Dear order group %{group}, + + The collective order at %{supplier} has just been settled and invoices have been created for the respective order groups. + Attached you will find your invoice. + + Best regards from %{foodcoop} + from_via_foodsoft: "%{name} via Foodsoft" + invite: + subject: Invitation to the Foodcoop + text: | + Hi! + + %{user} <%{mail}> has invited you to join the group "%{group}". + To accept the invitation and to join the foodcoop please follow this link: %{link} + This link works only once and expires on %{expires}. + + + Greetings, your Foodsoft Team! + negative_balance: + subject: Negative account balance + text: | + Dear %{group}, + + Your account balance has dropped below zero due to a booking on %{when}: %{balance} + + There was a charge of %{amount} for "%{note}" by %{user}. + + Please deposit your account as soon as possible. + + + + Kind regards from %{foodcoop}. + not_enough_users_assigned: + subject: '"%{task}" still needs people!' + text: | + Dear %{user}, + + The Task '%{task}' of your working group is due on %{when}, + and could use some more contributors! + + If you haven’t assigned yourself to this task yet it’s your chance now: + + %{workgroup_tasks_url} + + Your Tasks: %{user_tasks_url} + order_result: + subject: 'Order closed: %{name}' + text0: | + Dear %{ordergroup}, + + The order for "%{order}" has been closed by %{user} on %{when}. + text1: | + It can be presumable be picked up on %{pickup}. + text2: | + The following articles have been ordered for your ordergroup: + text3: |- + o Total sum: %{sum} + + You can view the order online: %{order_url} + + + Kind regards from %{foodcoop}. + order_received: + subject: 'Order delivery registered: %{name}' + text0: | + Dear %{ordergroup}, + + the order for "%{order}" has been received. + abundant_articles: Received too much + scarce_articles: Received too little + article_details: | + o %{name}: + -- Ordered: %{ordered} x %{unit} + -- Received: %{received} x %{unit} + order_result_supplier: + subject: New order for %{name} + text: | + Hi! + + Foodcoop %{foodcoop} would like to place an order. + + Please find a PDF and spreadsheet attached. + + Kind regards, + %{user} + %{foodcoop} + reset_password: + subject: New password for %{username} + text: | + Hi %{user}, + + You have (or someone else has) requested a new password. + In order to choose a new password follow this link: %{link} + This link works only once and expires on %{expires}. + If you don't want to change your password, just ignore this message. Your password hasn't been changed yet. + + + Greetings, your Foodsoft Team! + upcoming_tasks: + nextweek: 'Tasks for the next week:' + subject: Tasks are due! + text0: | + Dear %{user}, + + You are asigned to the task "%{task}". This task is due by tomorrow (%{when})! + text1: | + My tasks: %{user_tasks_url} + + + Kind regards from %{foodcoop}. + welcome: + subject: Welcome to the Foodcoop + text0: | + Dear %{user}, + + a new Foodsoft account has been created for you. + text1: | + In order to choose a new password follow this link: %{link} + This link works only once and expires on %{expires}. + You can always use "Forgot password?" to get a new link. + + + Kind regards from %{foodcoop}. + messages_mailer: + foodsoft_message: + footer: | + Reply: %{reply_url} + See message online: %{msg_url} + Messaging options: %{profile_url} + footer_group: | + Sent to group: %{group} + model: + delivery: + each_stock_article_must_be_unique: Each stock article must not be listed more than once. + financial_transaction: + foodcoop_name: Foodcoop + financial_transaction_type: + no_delete_last: At least one financial transaction type must exist. + group_order: + stock_ordergroup_name: Stock (%{user}) + invoice: + invalid_mime: has an invalid MIME type (%{mime}) + membership: + no_admin_delete: Membership can not be withdrawn as you are the last administrator. + order_article: + error_price: must be specified and have a current price price + user: + no_ordergroup: no ordergroup + group_order_article: + order_closed: Order is closed and cannot be modified + navigation: + admin: + config: Configuration + finance: Finances + home: Overview + mail_delivery_status: Email problems + ordergroups: Ordergroups + title: Administration + users: Users + workgroups: Workgroups + articles: + categories: Categories + stock: Stock + suppliers: Suppliers/articles + title: Articles + dashboard: Dashboard + finances: + accounts: Manage accounts + balancing: Account orders + bank_accounts: Bank Accounts + home: Overview + invoices: Invoices + title: Finances + foodcoop: Foodcoop + members: Members + ordergroups: Ordergroups + orders: + archive: My Orders + manage: Manage orders + ordering: Place order! + pickups: Pickup days + title: Orders + tasks: Tasks + workgroups: Workgroups + number: + percentage: + format: + strip_insignificant_zeros: true + order_articles: + edit: + stock_alert: The price of stock articles cannot be changed! + title: Update article + new: + title: Add delivered article to order + ordergroups: + edit: + title: Edit ordergroups + index: + title: Ordergroups + model: + error_single_group: "%{user} is already a member of another ordergroup" + invalid_balance: is not a valid number + orders: + articles: + article_count: 'Ordered articles:' + prices: Net/gross price + prices_sum: 'Sum (net/gross price):' + units_full: Full units + units_ordered: Units ordered + create: + notice: The order was created. + edit: + title: 'Edit order: %{name}' + edit_amount: + field_locked_title: The distribution of this article among the ordergroups was changed manually. This field is locked to protect those changes. To redistribute and overwrite those changes, press the unlock button and change the amount. + field_unlocked_title: The distribution of this article among the ordergroups was changed manually. When you change the amount, those manual changes will be overwritten. + edit_amounts: + no_articles_available: No articles to add. + set_all_to_zero: Set all to zero + fax: + amount: Amount + articles: Articles + delivery_day: Delivery day + heading: Order for %{name} + name: Name + number: Number + to_address: Shipping address + finish: + notice: The order has been closed. + form: + ignore_warnings: Ignore warnings + prices: Prices (net/FC) + select_all: Select all + stockit: In stock + title: Article + index: + action_end: Close + action_receive: Receive + confirm_delete: Do you really want to delete the order? + confirm_end: Do you really want to close the order %{order}? There is no going back. + new_order: Create new order + no_open_or_finished_orders: There are currently no open or closed orders. + orders_finished: Closed + orders_open: Open + orders_settled: Settled + not_closed: Order not yet settled + title: Manage orders + model: + close_direct_message: Order settled without charging member accounts. + error_boxfill_before_ends: must be after the box-fill date (or remain empty) + error_closed: Order was already settled + error_nosel: At least one article must be selected. You may want to delete the order instead? + error_starts_before_boxfill: must be after the start date (or remain empty) + error_starts_before_ends: must be after the start date (or remain empty) + notice_close: 'Order: %{name}, until %{ends}' + stock: Stock + warning_ordered: 'Warning: articles marked red have already been ordered within this open order. If you uncheck them here, all existing orders of these articles will be deleted. To proceed, confirm below.' + warning_ordered_stock: 'Warning: Articles marked red have already been ordered/purchased within this open stock order. If you uncheck them here, all existing orders/purchases of these articles will be deleted and it will not be accounted for them. Confirm below to proceed.' + new: + title: Create new order + receive: + add_article: Add article + consider_member_tolerance: consider tolerance + notice: 'Order received: %{msg}' + notice_none: No new articles to receive + paragraph: If the ordered and received amount are the same, the corresponding fields can be empty. It's still good practice to enter all fields, since this provides an easy way to verify that all articles have been checked. + rest_to_stock: rest to stock + submit: Receive order + surplus_options: 'Distribution options:' + title: Receiving %{order} + send_to_supplier: + notice: The order has been sent to the supplier. + show: + action_end: Close! + amounts: 'Net/gross sum:' + articles: Article overview + articles_ordered: 'Ordered articles:' + comments: + title: Comments + comments_link: Comments + confirm_delete: Do you really want to delete the order? + confirm_end: |- + Do you really want to close the order %{order}? + There is no going back. + confirm_send_to_supplier: The order has been sent to the supplier already on %{when}. Do you really want to send it again? + create_invoice: Add invoice + description1_order: "%{state} order from %{supplier} opened by %{who}," + description1_period: + pickup: and can be picked up on %{pickup} + starts: open from %{starts} + starts_ends: open from %{starts} until %{ends} + description2: "%{ordergroups} ordered %{article_count} articles, with a total value of %{net_sum} / %{gross_sum} (net / gross)." + description3: " Additional deposit of %{net_deposit} / %{deposit} (net / gross)." + group_orders: 'Group orders:' + search_placeholder: + articles: Search for articles... + default: Search for articles... + groups: Search for ordergroups... + search_reset: Reset search + send_to_supplier: Send to supplier + show_invoice: Show invoice + sort_article: Sorted in articles + sort_group: Sorted in groups + stock_order: Stock Order + title: 'Order: %{name}' + warn_not_closed: Warning, order is not yet settled. + state: + closed: settled + finished: closed + open: open + received: received + update: + notice: The order was updated. + update_order_amounts: + msg1: "%{count} articles (%{units} units) updated" + msg2: "%{count} (%{units}) using tolerance" + msg4: "%{count} (%{units}) left over" + pickups: + document: + empty_selection: At least one order must be selected. + filename: Pickup for %{date} + invalid_document: Invalid document type + title: Pickup for %{date} + index: + article_pdf: Article PDF + group_pdf: Group PDF + matrix_pdf: Matrix PDF + title: Pickup days + sessions: + logged_in: Logged in! + logged_out: Logged out! + login_invalid_email: Invalid email address or password + login_invalid_nick: Invalid username or password + new: + forgot_password: Forgot password? + login: Login + nojs: Attention, cookies and javascript have to be activated! Please switch off %{link}. + noscript: NoScript + title: Foodsoft login + shared: + articles: + ordered: Ordered + ordered_desc: Number of articles as ordered by member (amount + tolerance) + received: Received + received_desc: Number of articles that (will be) received by member + articles_by: + price: Total price + price_sum: Sum + group: + access: Access to + activated: activated + apple_limit: Apple points order limit + break: from %{start} to %{end} + deactivated: deactivated + group_form_fields: + search: Search ... + search_user: Search user + user_not_found: No user found + open_orders: + no_open_orders: There are no current orders + not_enough_apples: Attention your ordergroup has too few apple points to place an order! + title: Current orders + total_sum: Total sum + who_ordered: Who ordered? + order_download_button: + article_pdf: Article PDF + download_file: Download file + fax_csv: Fax CSV + fax_pdf: Fax PDF + fax_txt: Fax text + group_pdf: Group PDF + matrix_pdf: Matrix PDF + title: Download + task_list: + accept_task: Accept task + done: Done + done_q: Done? + mark_done: Mark task as done + reject_task: Reject task + who: Who is doing it? + who_hint: "(How much are still needed?)" + user_form_fields: + contact_address_hint: The address of your ordergroup. If you update this, it changes for other members of the ordergroup as well. + messagegroups: Join or leave message groups + workgroup_members: + title: Group memberships + simple_form: + error_notification: + default_message: Errors were found. Please check the form. + hints: + article: + unit: e.g. KG or 1L or 500g + article_category: + description: comma-separated list of category names recognised at import/synchronisation + order_article: + units_to_order: If you change the total amount of delivered units, you also have to change individual group amounts by clicking on the article name. They will not be automatically recalculated and so ordergroups may be accounted for articles that were not delivered! + update_global_price: Also update the price of future orders + stock_article: + copy: + name: Please modify + edit_stock_article: + price: "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: + last_deliveries: Recent deliveries + last_orders: Last orders + new_delivery: New delivery + show_deliveries: Show all + update: + notice: Supplier was updated + tasks: + accept: + notice: You have accepted the task + archive: + title: Task archive + create: + notice: Task has been created + destroy: + notice: Task has been deleted + edit: + submit_periodic: Save recurring task + title: Edit task + title_periodic: Edit recurring task + warning_periodic: "Warning: This task is part of a group of recurring tasks. When saving it will be excluded from the group and it will be converted to a regular task." + error_not_found: No workgroup found + form: + search: + hint: Search for user + noresult: No user found + placeholder: Search ... + submit: + periodic: Save recurring task + index: + show_group_tasks: Show group tasks + title: Tasks + title_non_group: Tasks for all! + nav: + all_tasks: All tasks + archive: Completed tasks (archive) + group_tasks: Group tasks + my_tasks: My tasks + new_task: Create new task + pages: Pages + new: + submit_periodic: Create recurring task + title: Create new task + repeated: Task is recurring + set_done: + notice: The state of the task has been updated + show: + accept_task: Accept task + confirm_delete_group: Really delete this and all subsequent tasks? + confirm_delete_single: Are you sure you want to delete the task? + confirm_delete_single_from_group: Are you sure you want to delete this task (and keep related recurring tasks)? + delete_group: Delete task and subsequent + edit_group: Edit recurring + hours: "%{count}h" + mark_done: Mark task as done + reject_task: Reject task + title: Show task + update: + notice: Task has been updated + notice_converted: Task has been updated and was converted to a non-repeating task. + user: + more: Nothing to do? %{tasks_link} are tasks for sure. + tasks_link: Here + title: My tasks + title_accepted: Accepted tasks + title_open: Open tasks + workgroup: + title: Tasks for %{workgroup} + title_all: All group tasks + ui: + actions: Actions + back: Back + cancel: Cancel + close: Close + confirm_delete: Do you really want to delete %{name}? + confirm_restore: Do you really want to restore %{name}? + copy: Copy + delete: Delete + download: Download + edit: Edit + marks: + close: "×" + success: + move: Move + or_cancel: or cancel + please_wait: Please wait... + restore: Restore + save: Save + search_placeholder: Search ... + show: Show + views: + pagination: + first: "«" + last: "»" + next: "›" + previous: "‹" + truncate: "..." + workgroups: + edit: + title: Edit workgroup + error_last_admin_group: The last group with admin rights may not be deleted + error_last_admin_role: Admin role for the last group with admin rights cannot be withdrawn + index: + title: Workgroups + update: + notice: Workgroup was updated + time: + formats: + foodsoft_datetime: "%Y-%m-%d %H:%M" + file: "%Y-%d-%B" diff --git a/plugins/automatic_invoices/config/routes.rb b/plugins/automatic_invoices/config/routes.rb new file mode 100644 index 00000000..caa5da8d --- /dev/null +++ b/plugins/automatic_invoices/config/routes.rb @@ -0,0 +1,7 @@ +Rails.application.routes.draw do + post 'finance/group_order_invoice', to: 'group_order_invoices#create_multiple' + + get 'orders/:order_id/group_order_invoices/download_all', to: 'group_order_invoices#download_all', as: 'download_all_group_order_invoices' + + resources :group_order_invoices +end diff --git a/plugins/automatic_invoices/db/migrate/20211208142719_create_group_order_invoices.rb b/plugins/automatic_invoices/db/migrate/20211208142719_create_group_order_invoices.rb new file mode 100644 index 00000000..b0aa13f7 --- /dev/null +++ b/plugins/automatic_invoices/db/migrate/20211208142719_create_group_order_invoices.rb @@ -0,0 +1,13 @@ +class CreateGroupOrderInvoices < ActiveRecord::Migration[5.2] + def change + create_table :group_order_invoices do |t| + t.integer :group_order_id + t.bigint :invoice_number, unique: true, limit: 8 + t.date :invoice_date + t.string :payment_method + + t.timestamps + end + add_index :group_order_invoices, :group_order_id, unique: true + end +end diff --git a/plugins/automatic_invoices/db/migrate/20230822120005_add_customer_number_to_group.rb b/plugins/automatic_invoices/db/migrate/20230822120005_add_customer_number_to_group.rb new file mode 100644 index 00000000..9b4c2278 --- /dev/null +++ b/plugins/automatic_invoices/db/migrate/20230822120005_add_customer_number_to_group.rb @@ -0,0 +1,5 @@ +class AddCustomerNumberToGroup < ActiveRecord::Migration[7.0] + def change + add_column :groups, :customer_number, :string, unique: true + end +end diff --git a/plugins/automatic_invoices/foodsoft_automatic_invoices.gemspec b/plugins/automatic_invoices/foodsoft_automatic_invoices.gemspec new file mode 100644 index 00000000..acf82674 --- /dev/null +++ b/plugins/automatic_invoices/foodsoft_automatic_invoices.gemspec @@ -0,0 +1,19 @@ +$:.push File.expand_path('lib', __dir__) + +# Maintain your gem's version: +require 'foodsoft_automatic_invoices/version' + +# Describe your gem and declare its dependencies: +Gem::Specification.new do |s| + s.name = 'foodsoft_automatic_invoices' + s.version = FoodsoftAutomaticInvoices::VERSION + s.authors = ['viehlieb'] + s.email = ['pf@pragma-shift.net'] + s.summary = "Foodsoft plugin to enhance foodsoft's accounting capabilities and to create and automatically deliver invoice pdfs for accounted orders." + s.description = '' + + s.files = Dir['{app,config,db,spec,lib}/**/**/**/*'] + ['Rakefile', 'README.md'] + + s.add_dependency 'rails' + s.add_dependency 'deface', '~> 1.9' +end diff --git a/plugins/automatic_invoices/lib/foodsoft_automatic_invoices.rb b/plugins/automatic_invoices/lib/foodsoft_automatic_invoices.rb new file mode 100644 index 00000000..0132b685 --- /dev/null +++ b/plugins/automatic_invoices/lib/foodsoft_automatic_invoices.rb @@ -0,0 +1,10 @@ +require 'deface' +require 'foodsoft_automatic_invoices/engine' +require 'foodsoft_automatic_invoices/send_group_order_invoice_pdf' + + +module FoodsoftAutomaticInvoices + def self.enabled? + FoodsoftConfig[:use_automatic_invoices] + end +end diff --git a/plugins/automatic_invoices/lib/foodsoft_automatic_invoices/engine.rb b/plugins/automatic_invoices/lib/foodsoft_automatic_invoices/engine.rb new file mode 100644 index 00000000..1f726353 --- /dev/null +++ b/plugins/automatic_invoices/lib/foodsoft_automatic_invoices/engine.rb @@ -0,0 +1,12 @@ +module FoodsoftAutomaticInvoices + class Engine < ::Rails::Engine + + initializer 'automatic_invoices.assets.precompile' do |app| + app.config.assets.precompile += %w(group_orders.css.less) + end + + def default_foodsoft_config(cfg) + cfg[:use_automatic_invoices] = false + end + end +end diff --git a/plugins/automatic_invoices/lib/foodsoft_automatic_invoices/send_group_order_invoice_pdf.rb b/plugins/automatic_invoices/lib/foodsoft_automatic_invoices/send_group_order_invoice_pdf.rb new file mode 100644 index 00000000..dd2178fb --- /dev/null +++ b/plugins/automatic_invoices/lib/foodsoft_automatic_invoices/send_group_order_invoice_pdf.rb @@ -0,0 +1,24 @@ +module FoodsoftAutomaticInvoices + module SendGroupOrderInvoicePdf + + extend ActiveSupport::Concern + + protected + + def create_invoice_pdf(group_order_invoice) + invoice_data = group_order_invoice.load_data_for_invoice + invoice_data[:title] = t('documents.group_order_invoice_pdf.title', supplier: invoice_data[:supplier]) + invoice_data[:no_footer] = true + GroupOrderInvoicePdf.new invoice_data + end + + def send_group_order_invoice_pdf(group_order_invoice) + pdf = create_invoice_pdf(group_order_invoice) + send_data pdf.to_pdf, filename: pdf.filename, type: 'application/pdf' + end + end +end + +ActiveSupport.on_load(:after_initialize) do + Concerns::SendOrderPdf.include FoodsoftAutomaticInvoices::SendGroupOrderInvoicePdf +end \ No newline at end of file diff --git a/plugins/automatic_invoices/lib/foodsoft_automatic_invoices/version.rb b/plugins/automatic_invoices/lib/foodsoft_automatic_invoices/version.rb new file mode 100644 index 00000000..8fc0e68a --- /dev/null +++ b/plugins/automatic_invoices/lib/foodsoft_automatic_invoices/version.rb @@ -0,0 +1,3 @@ +module FoodsoftAutomaticInvoices + VERSION = '0.0.1' +end diff --git a/plugins/automatic_invoices/spec/factories/group_order_invoice.rb b/plugins/automatic_invoices/spec/factories/group_order_invoice.rb new file mode 100644 index 00000000..89723873 --- /dev/null +++ b/plugins/automatic_invoices/spec/factories/group_order_invoice.rb @@ -0,0 +1,7 @@ +require 'factory_bot' + +FactoryBot.define do + factory :group_order_invoice do + group_order { create :group_order } + end +end diff --git a/plugins/automatic_invoices/spec/integration/group_order_invoices_spec.rb b/plugins/automatic_invoices/spec/integration/group_order_invoices_spec.rb new file mode 100644 index 00000000..f6ece77d --- /dev/null +++ b/plugins/automatic_invoices/spec/integration/group_order_invoices_spec.rb @@ -0,0 +1,72 @@ +require_relative '../spec_helper' + +feature GroupOrderInvoice, js: true do + let(:admin) { create :user, groups: [create(:workgroup, role_finance: true)] } + let(:user) { create :user, groups: [create(:ordergroup)] } + let(:article) { create :article, unit_quantity: 1 } + let(:order) { create :order, supplier: article.supplier, article_ids: [article.id], ends: Time.now } # need to ref article + let(:go) { create :group_order, order: order, ordergroup: user.ordergroup} + let(:oa) { order.order_articles.find_by_article_id(article.id) } + let(:ftt) { create :financial_transaction_type } + let(:goa) { create :group_order_article, group_order: go, order_article: oa } + + include ActiveJob::TestHelper + + before { login admin } + + after { clear_enqueued_jobs } + + it 'does not enqueue MailerJob when order is settled if tax_number or options not set' do + goa.update_quantities 2, 0 + oa.update_results! + visit confirm_finance_order_path(id: order.id) + click_link_or_button I18n.t('finance.balancing.confirm.clear') + expect(NotifyGroupOrderInvoiceJob).not_to have_been_enqueued + end + + it 'enqueues MailerJob when order is settled if tax_number or options are set' do + goa.update_quantities 2, 0 + oa.update_results! + order.reload + FoodsoftConfig[:group_order_invoices] = { use_automatic_invoices: true } + FoodsoftConfig[:contact][:tax_number] = 12_345_678 + visit confirm_finance_order_path(id: order.id, type: ftt) + expect(page).to have_selector(:link_or_button, I18n.t('finance.balancing.confirm.clear')) + click_link_or_button I18n.t('finance.balancing.confirm.clear') + expect(NotifyGroupOrderInvoiceJob).to have_been_enqueued + end + + it 'generates Group Order Invoice when order is closed if tax_number is set' do + goa.update_quantities 2, 0 + oa.update_results! + FoodsoftConfig[:contact][:tax_number] = 12_345_678 + order.update!(state: 'closed') + go.reload + order.reload + visit finance_order_index_path + expect(page).to have_selector(:link_or_button, I18n.t('activerecord.attributes.group_order_invoice.links.generate')) + click_link_or_button I18n.t('activerecord.attributes.group_order_invoice.links.generate') + expect(GroupOrderInvoice.all.count).to eq(1) + end + + it 'generates multiple Group Order Invoice for order when order is closed if tax_number is set' do + goa.update_quantities 2, 0 + oa.update_results! + FoodsoftConfig[:contact][:tax_number] = 12_345_678 + order.update!(state: 'closed') + order.reload + visit finance_order_index_path + expect(page).to have_selector(:link_or_button, I18n.t('activerecord.attributes.group_order_invoice.links.generate_with_date')) + click_link_or_button I18n.t('activerecord.attributes.group_order_invoice.links.generate_with_date') + expect(GroupOrderInvoice.all.count).to eq(1) + end + + it 'does not generate Group Order Invoice when order is closed if tax_number not set' do + goa.update_quantities 2, 0 + oa.update_results! + order.update!(state: 'closed') + order.reload + visit finance_order_index_path + expect(page).to have_content(I18n.t('activerecord.attributes.group_order_invoice.tax_number_not_set')) + end +end diff --git a/plugins/automatic_invoices/spec/models/article_spec.rb b/plugins/automatic_invoices/spec/models/article_spec.rb new file mode 100644 index 00000000..91a2f8ea --- /dev/null +++ b/plugins/automatic_invoices/spec/models/article_spec.rb @@ -0,0 +1,168 @@ +require_relative '../spec_helper' + +describe Article do + let(:supplier) { create(:supplier) } + let(:article) { create(:article, supplier: supplier) } + + it 'has a unique name' do + article2 = build(:article, supplier: supplier, name: article.name) + expect(article2).to be_invalid + end + + it 'can be deleted' do + expect(article).not_to be_deleted + article.mark_as_deleted + expect(article).to be_deleted + end + + describe 'convert units' do + it 'returns nil when equal' do + expect(article.convert_units(article)).to be_nil + end + + it 'returns false when invalid unit' do + article1 = build(:article, supplier: supplier, unit: 'invalid') + expect(article.convert_units(article1)).to be false + end + + it 'returns false if unit = 0' do + article1 = build(:article, supplier: supplier, unit: '1kg', price: 2, unit_quantity: 1) + article2 = build(:article, supplier: supplier, unit: '0kg', price: 2, unit_quantity: 1) + expect(article1.convert_units(article2)).to be false + end + + it 'returns false if unit becomes zero because of , symbol in unit format' do + article1 = build(:article, supplier: supplier, unit: '0,8kg', price: 2, unit_quantity: 1) + article2 = build(:article, supplier: supplier, unit: '0,9kg', price: 2, unit_quantity: 1) + expect(article1.convert_units(article2)).to be false + end + + it 'converts from ST to KI (german foodcoops legacy)' do + article1 = build(:article, supplier: supplier, unit: 'ST') + article2 = build(:article, supplier: supplier, name: 'banana 10-12 St', price: 12.34, unit: 'KI') + new_price, new_unit_quantity = article1.convert_units(article2) + expect(new_unit_quantity).to eq 10 + expect(new_price).to eq 1.23 + end + + it 'converts from g to kg' do + article1 = build(:article, supplier: supplier, unit: 'kg') + article2 = build(:article, supplier: supplier, unit: 'g', price: 0.12, unit_quantity: 1500) + new_price, new_unit_quantity = article1.convert_units(article2) + expect(new_unit_quantity).to eq 1.5 + expect(new_price).to eq 120 + end + end + + it 'computes changed article attributes' do + article2 = build(:article, supplier: supplier, name: 'banana') + expect(article.unequal_attributes(article2)[:name]).to eq 'banana' + 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 + if FoodsoftConfig[:group_order_invoices]&.[](:separate_deposits) + expect(article.gross_price_without_deposit).to eq((article.price * 1.12 + 1.20).round(2)) + expect(article.gross_price).to eq(((article.price + 1.20) * 1.12).round(2)) + end + end + + it 'gross price >= net price' do + expect(article.gross_price).to be >= article.price + end + + [[nil, 1], + [0, 1], + [5, 1.05], + [42, 1.42], + [100, 2]].each do |price_markup, percent| + it "computes the fc price with price_markup #{price_markup} correctly" do + FoodsoftConfig.config['price_markup'] = price_markup + expect(article.fc_price).to eq((article.gross_price * percent).round(2)) + end + end + it 'knows when it is deleted' do + expect(supplier.deleted?).to be false + supplier.mark_as_deleted + expect(supplier.deleted?).to be true + end + + it 'keeps a price history' do + expect(article.article_prices.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.reload.map(&:price)).to eq([article.price, oldprice]) + end + + it 'is not in an open order by default' do + expect(article.in_open_order).to be_nil + end + + it 'is knows its open order' do + order = create(:order, supplier: supplier, article_ids: [article.id]) + expect(article.in_open_order).to eq(order) + end + + it 'has no shared article by default' do + expect(article.shared_article).to be_nil + end + + describe 'connected to a shared database', type: :feature do + let(:shared_article) { create(:shared_article) } + let(:supplier) { create(:supplier, shared_supplier_id: shared_article.supplier_id) } + let(:article) { create(:article, supplier: supplier, order_number: shared_article.order_number) } + + it 'can be found in the shared database' do + expect(article.shared_article).not_to be_nil + end + + it 'can find updates' do + changed = article.shared_article_changed? + expect(changed).not_to be_falsey + expect(changed.length).to be > 1 + end + + it 'can be synchronised' do + # TODO: move article sync from supplier to article + article # need to reference for it to exist when syncing + updated_article = supplier.sync_all[0].select { |s| s[0].id == article.id }.first[0] + article.update(updated_article.attributes.reject { |k, _v| %w[id type].include?(k) }) + expect(article.name).to eq(shared_article.name) + # now synchronising shouldn't change anything anymore + expect(article.shared_article_changed?).to be_falsey + end + + it 'does not need to synchronise an imported article' do + article = shared_article.build_new_article(supplier) + article.article_category = create :article_category + expect(article.shared_article_changed?).to be_falsey + end + + it 'adapts to foodcoop units when synchronising' do + shared_article.unit = '1kg' + shared_article.unit_quantity = 1 + shared_article.save! + article = shared_article.build_new_article(supplier) + article.article_category = create :article_category + article.unit = '200g' + article.shared_updated_on -= 1 # to make update do something + article.save! + # TODO: get sync functionality in article + updated_article = supplier.sync_all[0].select { |s| s[0].id == article.id }.first[0] + article.update!(updated_article.attributes.reject { |k, _v| %w[id type].include?(k) }) + expect(article.unit).to eq '200g' + expect(article.unit_quantity).to eq 5 + expect(article.price).to be_within(0.005).of(shared_article.price / 5) + end + + it 'does not synchronise when it has no order number' do + article.update(order_number: nil) + expect(supplier.sync_all).to eq [[], [], []] + end + end +end diff --git a/plugins/automatic_invoices/spec/models/group_order_invoice_spec.rb b/plugins/automatic_invoices/spec/models/group_order_invoice_spec.rb new file mode 100644 index 00000000..24bfcf7e --- /dev/null +++ b/plugins/automatic_invoices/spec/models/group_order_invoice_spec.rb @@ -0,0 +1,59 @@ +require_relative '../spec_helper' + +describe GroupOrderInvoice do + let(:user) { create :user, groups: [create(:ordergroup)] } + let(:supplier) { create :supplier } + let(:article) { create :article, supplier: supplier } + let(:order) { create :order } + let(:group_order) { create :group_order, order: order, ordergroup: user.ordergroup } + + describe 'erroneous group order invoice' do + let(:goi) { create :group_order_invoice, group_order_id: group_order.id } + it 'does not create group order invoice if tax_number not set' do + expect { goi }.to raise_error(ActiveRecord::RecordInvalid, /.*/) + end + end + + describe 'valid group order invoice' do + before do + FoodsoftConfig[:contact][:tax_number] = 123_457_8 + end + + invoice_number1 = Time.now.strftime("%Y%m%d") + '0001' + invoice_number2 = Time.now.strftime("%Y%m%d") + '0002' + + let(:user2) { create :user, groups: [create(:ordergroup)] } + + let(:goi1) { create :group_order_invoice, group_order_id: group_order.id } + let(:goi2) { create :group_order_invoice, group_order_id: group_order.id } + + let(:group_order2) { create :group_order, order: order, ordergroup: user2.ordergroup } + + let(:goi3) { create :group_order_invoice, group_order_id: group_order2.id } + let(:goi4) { create :group_order_invoice, group_order_id: group_order2.id, invoice_number: invoice_number1 } + + it 'creates group order invoice if tax_number is set' do + expect(goi1).to be_valid + end + + it 'sets invoice_number according to date' do + number = Time.now.strftime("%Y%m%d") + '0001' + expect(goi1.invoice_number).to eq(number.to_i) + end + + it 'fails to create if group_order_id is used multiple times for creation' do + expect(goi1.group_order.id).to eq(group_order.id) + expect { goi2 }.to raise_error(ActiveRecord::RecordNotUnique) + end + + it 'creates two different group order invoice with different invoice_numbers' do + expect(goi1.invoice_number).to eq(invoice_number1.to_i) + expect(goi3.invoice_number).to eq(invoice_number2.to_i) + end + + it 'fails to create two different group order invoice with same invoice_numbers' do + goi1 + expect { goi4 }.to raise_error(ActiveRecord::RecordInvalid) + end + end +end diff --git a/spec/integration/balancing_spec.rb b/spec/integration/balancing_spec.rb index d8e58e6d..0925f6ae 100644 --- a/spec/integration/balancing_spec.rb +++ b/spec/integration/balancing_spec.rb @@ -14,25 +14,32 @@ feature 'settling an order', js: true do let(:goa2) { create(:group_order_article, group_order: go2, order_article: oa) } before do + Rails.cache.clear + login admin goa1.update_quantities(3, 0) goa2.update_quantities(1, 0) oa.update_results! + oa.reload + order.reload order.finish!(admin) goa1.reload goa2.reload + visit new_finance_order_path(order_id: order.id) end - before { visit new_finance_order_path(order_id: order.id) } - before { login admin } + after do + Rails.cache.clear + end it 'has correct order result' do + oa.reload 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 - it 'has product ordered visible' do + it 'has product ordered visible', js: true do expect(page).to have_content(article.name) expect(page).to have_selector("#order_article_#{oa.id}") end diff --git a/spec/models/order_spec.rb b/spec/models/order_spec.rb index 71c46a84..23d176a7 100644 --- a/spec/models/order_spec.rb +++ b/spec/models/order_spec.rb @@ -170,8 +170,10 @@ describe Order do oa.update_results! order.finish!(user) + goa.reload order.reload order.close!(user, ftt) + goa.reload end it 'creates financial transaction with correct amount' do diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 3bbf03ea..1a456332 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -40,6 +40,13 @@ RSpec.configure do |config| FoodsoftConfig.init_mailing end + config.before(:each, type: :feature) do + Rails.cache.clear + end + config.after(:each, type: :feature) do + Rails.cache.clear + 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.