diff --git a/.drone.yml b/.drone.yml
new file mode 100644
index 00000000..f69ca630
--- /dev/null
+++ b/.drone.yml
@@ -0,0 +1,146 @@
+kind: pipeline
+type: docker
+name: build and test
+
+steps:
+ - name: rubocop
+ image: circleci/ruby:2.7-bullseye-node-browsers-legacy
+ commands:
+ - sudo apt install --no-install-recommends -y libmagic-dev
+ - sudo -E bundle install
+ - sudo -E bundle exec rubocop
+ volumes:
+ - name: gem-cache
+ path: /bundle
+ - name: tmp
+ path: /drone/src/tmp
+ failure: ignore
+
+
+ - name: build_test
+ image: circleci/ruby:2.7-bullseye-node-browsers-legacy
+ commands:
+ - sudo apt install --no-install-recommends -y libmagic-dev
+ - echo 'Wait for db container'; sleep 30
+ - bundle config set path '/bundle'
+ - bundle config set without 'production'
+ - sudo -E bundle install
+ - sudo -E bundle exec rake foodsoft:setup_development_docker || true
+ - sudo -E bundle exec rake rspec-rerun:spec
+ volumes:
+ - name: gem-cache
+ path: /bundle
+ - name: tmp
+ path: /drone/src/tmp
+ environment:
+ RAILS_LOG_TO_STDOUT: true
+ RAILS_ENV: test
+ COVERAGE: lcov
+ DATABASE_URL: mysql2://user:password@mariadb/test?encoding=utf8mb4
+ DATABASE_CLEANER_ALLOW_REMOTE_DATABASE_URL: true
+ PARALLEL_TEST_PROCESSORS: 60
+
+services:
+ - name: mariadb
+ image: mariadb
+ environment:
+ MYSQL_USER: user
+ MYSQL_PASSWORD: password
+ MYSQL_DATABASE: test
+ MYSQL_ROOT_PASSWORD: password
+
+volumes:
+ - name: gem-cache
+ host:
+ path: /tmp/cache
+ - name: tmp
+ temp: {}
+---
+
+kind: pipeline
+type: docker
+name: docker build and deploy
+steps:
+ - name: build and publish docker image
+ image: plugins/docker
+ settings:
+ registry: git.local-it.org
+ repo: git.local-it.org/foodsoft/foodsoft
+ username: philipp
+ password:
+ from_secret: docker_registry
+ tags:
+ - latest
+ - ${DRONE_BRANCH}
+ - ${DRONE_COMMIT:0:8}
+ cache_from:
+ - "git.local-it.org/foodsoft/foodsoft:latest"
+ - "git.local-it.org/foodsoft/foodsoft:${DRONE_BRANCH}"
+ - name: deployment
+ image: git.local-it.org/philipp/stack-ssh-deply:latest
+ settings:
+ stack: "foodsoft_tantewandel"
+ compose: "deployment/compose.yml"
+ deploy_key:
+ from_secret: drone_deploy_key
+ host: "dev.local-it.cloud"
+ user: "root"
+ port: 22
+ reg_user: philipp
+ reg_pass:
+ from_secret: docker_registry
+ reg_url: git.local-it.org
+ image: git.local-it.org/foodsoft/foodsoft:${DRONE_COMMIT:0:8}
+ generate_secrets: true
+ networks:
+ - proxy
+ environment:
+ IMAGE: git.local-it.org/foodsoft/foodsoft:${DRONE_COMMIT:0:8}
+ STACK_NAME: "foodsoft_tantewandel"
+ DOMAIN: "tantewandel.dev.local-it.cloud"
+ LETS_ENCRYPT_ENV: production
+ FOODCOOP_MULTI_INSTALL: true
+ FOODCOOP_NAME: Tantewandel
+ FOODCOOP_CITY: Mechow
+ FOODCOOP_COUNTRY: Deutschland
+ FOODCOOP_EMAIL: info@tantewandel.de
+ FOODCOOP_PHONE:
+ FOODCOOP_STREET:
+ FOODCOOP_ZIP_CODE:
+ FOODCOOP_HOMEPAGE: https://tantewandel.de
+ FOODCOOP_HELP_URL: https://tantewandel.de
+ FOODCOOP_TIME_ZONE: Berlin
+ FOODCOOP_USE_NICK: true
+ FOODCOOP_LANGUAGE: de
+ FOODCOOP_FOOTER: 'example hosted by Your Tech Co-op.'
+ USE_APPLE_POINTS: false
+ STOP_ORDERING_UNDER: 75
+ MINIMUM_BALANCE: 0
+ MYSQL_DB: foodsoft
+ MYSQL_HOST: db
+ MYSQL_PORT: 3306
+ MYSQL_USER: foodsoft
+ EMAIL_SENDER: demo@local-it.org
+ EMAIL_ERROR: flip@yksflip.de
+ SMTP_ADDRESS: mail.local-it.org
+ SMTP_AUTHENTICATION: login
+ SMTP_DOMAIN: mail.local-it.org
+ SMTP_ENABLE_STARTTLS_AUTO: true
+ SMTP_PORT: 587
+ SMTP_USER_NAME: demo@local-it.org
+ EMAIL_REPLY_DOMAIN:
+ SMTP_SERVER_HOST: 0.0.0.0
+ SMTP_SERVER_PORT: 2525
+ SECRET_DB_PASSWORD_VERSION: v1
+ SECRET_DB_ROOT_PASSWORD_VERSION: v1
+ SECRET_SHARED_LISTS_DB_PASSWORD_VERSION: v1
+ SECRET_SMTP_PASSWORD_VERSION: v1
+ SECRET_SECRET_KEY_BASE_VERSION: v1
+ APP_CONFIG_VERSION: v1
+ DB_CONFIG_VERSION: v1
+ ENTRYPOINT_VERSION: v1
+ PRODUCTION_ENV_VERSION: v1
+trigger:
+ branch:
+ - demo
+ - tantewandel
diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml
index 1d3cd010..c07c62eb 100644
--- a/.rubocop_todo.yml
+++ b/.rubocop_todo.yml
@@ -261,6 +261,8 @@ Lint/Void:
# Configuration parameters: AllowedMethods, AllowedPatterns, IgnoredMethods, CountRepeatedAttributes.
Metrics/AbcSize:
Max: 143
+ Exclude:
+ - 'app/documents/group_order_invoice_pdf.rb'
# Offense count: 17
# Configuration parameters: CountComments, CountAsOne, ExcludedMethods, AllowedMethods, AllowedPatterns, IgnoredMethods, inherit_mode.
@@ -402,22 +404,16 @@ RSpec/BeforeAfterAll:
# Configuration parameters: EnabledMethods.
RSpec/Capybara/FeatureMethods:
Exclude:
- - "spec/integration/articles_spec.rb"
- - "spec/integration/balancing_spec.rb"
- - "spec/integration/config_spec.rb"
- - "spec/integration/home_spec.rb"
- - "spec/integration/login_spec.rb"
- - "spec/integration/order_spec.rb"
- - "spec/integration/product_distribution_example_spec.rb"
- - "spec/integration/receive_spec.rb"
- - "spec/integration/session_spec.rb"
- - "spec/integration/supplier_spec.rb"
-
-# Offense count: 4
-RSpec/Capybara/SpecificMatcher:
- Exclude:
- - "spec/integration/login_spec.rb"
- - "spec/integration/session_spec.rb"
+ - 'spec/integration/articles_spec.rb'
+ - 'spec/integration/balancing_spec.rb'
+ - 'spec/integration/config_spec.rb'
+ - 'spec/integration/login_spec.rb'
+ - 'spec/integration/order_spec.rb'
+ - 'spec/integration/product_distribution_example_spec.rb'
+ - 'spec/integration/receive_spec.rb'
+ - 'spec/integration/session_spec.rb'
+ - 'spec/integration/supplier_spec.rb'
+ - 'spec/integration/group_order_invoices_spec.rb'
# Offense count: 27
# Configuration parameters: Prefixes, AllowedPatterns.
diff --git a/Gemfile b/Gemfile
index 4d71513f..68b64ea8 100644
--- a/Gemfile
+++ b/Gemfile
@@ -7,7 +7,6 @@ gem 'mail', '~> 2.7.1' # bug with mail 2.8.0 https://github.com/mikel/mail/issue
gem 'sassc-rails'
gem 'less-rails'
-gem 'uglifier'
# See https://github.com/sstephenson/execjs#readme for more supported runtimes
gem 'therubyracer', platforms: :ruby
@@ -128,3 +127,5 @@ group :test do
end
gem "importmap-rails", "~> 1.1"
+gem "image_processing", "~> 1.12"
+gem "terser", "~> 1.1"
diff --git a/Gemfile.lock b/Gemfile.lock
index 88fa2944..36d231b5 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -255,6 +255,9 @@ GEM
i18n-spec (0.6.0)
iso
ice_cube (0.16.4)
+ image_processing (1.12.2)
+ mini_magick (>= 4.9.5, < 5)
+ ruby-vips (>= 2.0.17, < 3)
importmap-rails (1.1.5)
actionpack (>= 6.0.0)
railties (>= 6.0.0)
@@ -318,6 +321,7 @@ GEM
mime-types (3.4.1)
mime-types-data (~> 3.2015)
mime-types-data (3.2022.0105)
+ mini_magick (4.12.0)
mini_mime (1.1.2)
minitest (5.17.0)
mono_logger (1.1.1)
@@ -496,6 +500,8 @@ GEM
ruby-prof (1.4.5)
ruby-progressbar (1.11.0)
ruby-units (3.0.0)
+ ruby-vips (2.1.4)
+ ffi (~> 1.12)
ruby2_keywords (0.0.5)
rubyzip (2.3.2)
sass-rails (6.0.0)
@@ -547,6 +553,8 @@ GEM
sqlite3 (>= 1.3.3)
table_print (1.5.7)
temple (0.9.1)
+ terser (1.1.13)
+ execjs (>= 0.3.0, < 3)
therubyracer (0.12.3)
libv8 (~> 3.16.14.15)
ref
@@ -567,8 +575,6 @@ GEM
unf (~> 0.1.0)
tzinfo (2.0.5)
concurrent-ruby (~> 1.0)
- uglifier (4.2.0)
- execjs (>= 0.3.0, < 3)
unf (0.1.4)
unf_ext
unf_ext (0.0.8.2)
@@ -631,6 +637,7 @@ DEPENDENCIES
i18n-js (~> 3.0.0.rc8)
i18n-spec
ice_cube
+ image_processing (~> 1.12)
importmap-rails (~> 1.1)
inherited_resources
jquery-rails
@@ -683,9 +690,9 @@ DEPENDENCIES
sprockets (< 4)
sqlite3 (~> 1.3.6)
table_print
+ terser (~> 1.1)
therubyracer
twitter-bootstrap-rails (~> 2.2.8)
- uglifier
web-console
whenever
diff --git a/app/assets/stylesheets/actiontext.css b/app/assets/stylesheets/actiontext.css
new file mode 100644
index 00000000..3cfcb2b7
--- /dev/null
+++ b/app/assets/stylesheets/actiontext.css
@@ -0,0 +1,31 @@
+/*
+ * Provides a drop-in pointer for the default Trix stylesheet that will format the toolbar and
+ * the trix-editor content (whether displayed or under editing). Feel free to incorporate this
+ * inclusion directly in any other asset bundle and remove this file.
+ *
+ *= require trix
+*/
+
+/*
+ * We need to override trix.css’s image gallery styles to accommodate the
+ * element we wrap around attachments. Otherwise,
+ * images in galleries will be squished by the max-width: 33%; rule.
+*/
+.trix-content .attachment-gallery > action-text-attachment,
+.trix-content .attachment-gallery > .attachment {
+ flex: 1 0 33%;
+ padding: 0 0.5em;
+ max-width: 33%;
+}
+
+.trix-content .attachment-gallery.attachment-gallery--2 > action-text-attachment,
+.trix-content .attachment-gallery.attachment-gallery--2 > .attachment, .trix-content .attachment-gallery.attachment-gallery--4 > action-text-attachment,
+.trix-content .attachment-gallery.attachment-gallery--4 > .attachment {
+ flex-basis: 50%;
+ max-width: 50%;
+}
+
+.trix-content action-text-attachment .attachment {
+ padding: 0 !important;
+ max-width: 100% !important;
+}
diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css
index 6bdfecd2..01dba421 100644
--- a/app/assets/stylesheets/application.css
+++ b/app/assets/stylesheets/application.css
@@ -7,4 +7,5 @@
*= require list.unlist
*= require list.missing
*= require recurring_select
+*= require actiontext
*/
diff --git a/app/assets/stylesheets/bootstrap_and_overrides.css.less b/app/assets/stylesheets/bootstrap_and_overrides.css.less
index 971308c9..ebd30b20 100644
--- a/app/assets/stylesheets/bootstrap_and_overrides.css.less
+++ b/app/assets/stylesheets/bootstrap_and_overrides.css.less
@@ -241,6 +241,9 @@ table {
tr.order-article:hover .article-info {
display: none;
}
+ tr.order-article:focus .article-info {
+ display: none;
+ }
}
#order-footer {
@@ -275,11 +278,13 @@ tr.order-article .article-info {
display: none;
}
-tr.order-article:hover .article-info {
+tr.order-article:focus{
+ background-color: #E4EED6;
+}
+tr.order-article:focus .article-info {
display: block;
}
-
// ********* Articles
tr.just-updated {
diff --git a/app/controllers/concerns/send_group_order_invoice_pdf.rb b/app/controllers/concerns/send_group_order_invoice_pdf.rb
new file mode 100644
index 00000000..0e7ad08a
--- /dev/null
+++ b/app/controllers/concerns/send_group_order_invoice_pdf.rb
@@ -0,0 +1,13 @@
+module Concerns::SendGroupOrderInvoicePdf
+ extend ActiveSupport::Concern
+
+ protected
+
+ def send_group_order_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
+ pdf = GroupOrderInvoicePdf.new invoice_data
+ send_data pdf.to_pdf, filename: pdf.filename, type: 'application/pdf'
+ end
+end
diff --git a/app/controllers/finance/balancing_controller.rb b/app/controllers/finance/balancing_controller.rb
index 4f23ac4f..c52be4b7 100644
--- a/app/controllers/finance/balancing_controller.rb
+++ b/app/controllers/finance/balancing_controller.rb
@@ -5,7 +5,7 @@ class Finance::BalancingController < Finance::BaseController
def new
@order = Order.find(params[:order_id])
- flash.now.alert = t('finance.balancing.new.alert') if @order.closed?
+ flash.now.alert = t('finance.balancing.new.alert') if @order.closed? && flash[:alert].blank?
@comments = @order.comments
@articles = @order.order_articles.ordered_or_member.includes(:article, :article_price,
@@ -81,9 +81,24 @@ class Finance::BalancingController < Finance::BaseController
@order = Order.find(params[:id])
@type = FinancialTransactionType.find_by_id(params.permit(:type)[:type])
@order.close!(@current_user, @type)
- redirect_to finance_order_index_url, notice: t('finance.balancing.close.notice')
+ 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), alert: t('finance.balancing.close.alert', message: error.message)
+ redirect_to new_finance_order_url(order_id: @order.id), notice: note, alert: alert, msg: error.message
end
# Close the order directly, without automaticly updating ordergroups account balances
diff --git a/app/controllers/group_order_invoices_controller.rb b/app/controllers/group_order_invoices_controller.rb
new file mode 100644
index 00000000..5fcca2d7
--- /dev/null
+++ b/app/controllers/group_order_invoices_controller.rb
@@ -0,0 +1,59 @@
+class GroupOrderInvoicesController < ApplicationController
+ include Concerns::SendGroupOrderInvoicePdf
+ before_action :authenticate_finance
+
+ def show
+ begin
+ @group_order_invoice = GroupOrderInvoice.find(params[:id])
+ if FoodsoftConfig[:contact][:tax_number]
+ respond_to do |format|
+ format.pdf do
+ send_group_order_invoice_pdf @group_order_invoice if FoodsoftConfig[:contact][:tax_number]
+ end
+ end
+ else
+ raise RecordInvalid
+ end
+ rescue ActiveRecord::RecordInvalid => error
+ redirect_back fallback_location: root_path, notice: 'Something went wrong', alert: I18n.t('errors.general_msg', msg: "#{error} " + I18n.t('errors.check_tax_number'))
+ end
+ end
+
+ def destroy
+ goi = GroupOrderInvoice.find(params[:id])
+ @order = goi.group_order.order
+ goi.destroy
+ respond_to do |format|
+ format.js
+ format.json { head :no_content }
+ end
+ end
+
+ def create_multiple
+ invoice_date = params[:group_order_invoice][:invoice_date]
+ order_id = params[:group_order_invoice][:order_id]
+ @order = Order.find(order_id)
+ gos = GroupOrder.where("order_id = ?", order_id)
+ gos.each do |go|
+ goi = GroupOrderInvoice.find_or_create_by!(group_order_id: go.id)
+ goi.invoice_date = invoice_date
+ goi.invoice_number = goi.generate_invoice_number(1)
+ goi.save!
+ end
+ respond_to do |format|
+ format.js
+ end
+ end
+
+ def create
+ go = GroupOrder.find(params[:group_order])
+ @order = go.order
+ GroupOrderInvoice.find_or_create_by!(group_order_id: go.id)
+ respond_to do |format|
+ format.js
+ end
+ redirect_back fallback_location: root_path
+ rescue => error
+ redirect_back fallback_location: root_path, notice: 'Something went wrong', :alert => I18n.t('errors.general_msg', :msg => error)
+ end
+end
diff --git a/app/documents/group_order_invoice_pdf.rb b/app/documents/group_order_invoice_pdf.rb
new file mode 100644
index 00000000..bc6c5c43
--- /dev/null
+++ b/app/documents/group_order_invoice_pdf.rb
@@ -0,0 +1,202 @@
+class GroupOrderInvoicePdf < RenderPdf
+ def filename
+ 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
+ unless contact[:phone].blank?
+ text "#{Supplier.human_attribute_name :phone}: #{contact[:phone]}", size: fontsize(9), align: :left
+ move_down 5
+ end
+ unless contact[:email].blank?
+ text "#{Supplier.human_attribute_name :email}: #{contact[:email]}", size: fontsize(9), align: :left
+ end
+ 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
+ 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
+ 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
+ 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)
+ 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_gross = goa.result * goa.order_article.price.gross_price
+ data << [goa.order_article.article.name,
+ goa.result.to_i,
+ number_to_currency(goa.order_article.price.gross_price),
+ number_to_currency(goa.total_price)]
+ total_gross += goa_total_gross
+ end
+
+ table data, position: :left, 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(1).width = 40
+ 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'), number_to_currency(total_gross)]
+ # table for sum
+ indent(200) do
+ table sum, position: :center, cell_style: { size: fontsize(8), overflow: :shrink_to_fit } do |table|
+ sum.length.times do |count|
+ table.row(count).columns(0..3).borders = []
+ end
+ table.row(sum.length - 1).columns(0..2).borders = []
+ table.row(sum.length - 1).border_bottom_width = 2
+ table.row(sum.length - 1).columns(3).borders = [:bottom]
+ end
+ end
+
+ move_down 25
+ text I18n.t('documents.group_order_invoice_pdf.small_business_regulation')
+ move_down 10
+ end
+
+ def body_with_vat
+ 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
+
+ marge = FoodsoftConfig[:price_markup]
+
+ # data table looks different when price_markup > 0
+ data = if marge == 0
+ [I18n.t('documents.group_order_invoice_pdf.no_price_markup_rows')]
+ else
+ [I18n.t('documents.group_order_invoice_pdf.price_markup_rows', marge: marge)]
+ end
+ goa_tax_hash = GroupOrderArticle.where(group_order_id: @group_order.id).find_each.group_by { |oat| oat.order_article.price.tax }
+ goa_tax_hash.each do |tax, group_order_articles|
+ group_order_articles.each do |goa|
+ # if no unit is received, nothing is to be charged
+ next if goa.result.to_i == 0
+
+ order_article = goa.order_article
+ goa_total_net = goa.result * order_article.price.price
+ goa_total_gross = goa.result * order_article.price.gross_price
+ data << [order_article.article.name,
+ goa.result.to_i,
+ number_to_currency(order_article.price.price),
+ number_to_currency(goa_total_net),
+ tax.to_s + '%',
+ number_to_currency(goa.total_price)]
+ tax_hash_net[tax.to_i] += goa_total_net
+ tax_hash_gross[tax.to_i] += goa_total_gross
+ total_net += goa_total_net
+ total_gross += goa_total_gross
+ end
+ end
+
+ # Two separate tables for sum and individual data
+ # article information + data
+ table data, cell_style: { size: fontsize(8), overflow: :shrink_to_fit } do |table|
+ table.header = true
+ table.position = :center
+ table.cells.border_width = 1
+ table.cells.border_color = '666666'
+
+ table.row(0).column(1).width = 40
+ table.row(0).border_bottom_width = 2
+ table.columns(1).align = :right
+ table.columns(1..6).align = :right
+ end
+
+ sum = []
+ sum << [nil, nil, nil, nil, I18n.t('documents.group_order_invoice_pdf.sum_to_pay_net'), number_to_currency(total_net)]
+ tax_hash_net.each_key.each do |tax|
+ sum << [nil, nil, nil, nil, I18n.t('documents.group_order_invoice_pdf.tax_included', tax: tax), number_to_currency(tax_hash_gross[tax] - tax_hash_net[tax])]
+ end
+ unless marge == 0
+ sum << [nil, nil, nil, nil, I18n.t('documents.group_order_invoice_pdf.markup_included', marge: marge), number_to_currency(total_gross * marge / 100.0)]
+ end
+ end_sum = total_gross * (1 + marge / 100.0)
+ sum << [nil, nil, nil, nil, I18n.t('documents.group_order_invoice_pdf.sum_to_pay_gross'), number_to_currency(end_sum)]
+ # table for sum
+ table sum, position: :right, cell_style: { size: fontsize(8), overflow: :shrink_to_fit } do |table|
+ sum.length.times do |count|
+ table.row(count).columns(0..5).borders = []
+ end
+ table.row(sum.length - 1).columns(0..4).borders = []
+ table.row(sum.length - 1).border_bottom_width = 2
+ table.row(sum.length - 1).columns(5).borders = [:bottom]
+ end
+
+ if(FoodsoftConfig[:group_order_invoices][:vat_exempt])
+ move_down 15
+ text I18n.t('documents.group_order_invoice_pdf.small_business_regulation')
+ end
+ move_down 10
+ end
+end
diff --git a/app/javascript/application.js b/app/javascript/application.js
index beff742e..ed5cae66 100644
--- a/app/javascript/application.js
+++ b/app/javascript/application.js
@@ -1 +1,3 @@
// Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails
+import "trix"
+import "@rails/actiontext"
diff --git a/app/jobs/notify_group_order_invoice_job.rb b/app/jobs/notify_group_order_invoice_job.rb
new file mode 100644
index 00000000..1a17fe9a
--- /dev/null
+++ b/app/jobs/notify_group_order_invoice_job.rb
@@ -0,0 +1,10 @@
+class NotifyGroupOrderInvoiceJob < ApplicationJob
+ def perform(group_order_invoice)
+ ordergroup = group_order_invoice.group_order.ordergroup
+ ordergroup.users.each do |user|
+ Mailer.deliver_now_with_user_locale user do
+ Mailer.group_order_invoice(group_order_invoice, user)
+ end
+ end
+ end
+end
diff --git a/app/lib/render_pdf.rb b/app/lib/render_pdf.rb
index 479dc4a3..51dd4291 100644
--- a/app/lib/render_pdf.rb
+++ b/app/lib/render_pdf.rb
@@ -70,7 +70,7 @@ class RenderPdf < Prawn::Document
options[:skip_page_creation] = true
@options = options
@first_page = true
-
+ no_footer = @options&.[](:no_footer) ? true : false
super(options)
# Use ttf for better utf-8 compability
@@ -84,11 +84,11 @@ class RenderPdf < Prawn::Document
)
header = options[:title] || title
- footer = I18n.l(Time.now, format: :long)
+ footer = I18n.l(Time.now, format: :long) unless no_footer
header_size = 0
header_size = height_of(header, size: HEADER_FONT_SIZE, font: DEFAULT_FONT) + HEADER_SPACE if header
- footer_size = height_of(footer, size: FOOTER_FONT_SIZE, font: DEFAULT_FONT) + FOOTER_SPACE
+ footer_size = no_footer ? 0 : height_of(footer, size: FOOTER_FONT_SIZE, font: DEFAULT_FONT) + FOOTER_SPACE
start_new_page(top_margin: TOP_MARGIN + header_size, bottom_margin: BOTTOM_MARGIN + footer_size)
@@ -98,12 +98,15 @@ class RenderPdf < Prawn::Document
bounding_box [bounds.left, bounds.top + header_size], width: bounds.width, height: header_size do
text header, size: HEADER_FONT_SIZE, align: :center, overflow: :shrink_to_fit if header
end
- font_size FOOTER_FONT_SIZE do
- bounding_box [bounds.left, bounds.bottom - FOOTER_SPACE], width: bounds.width, height: footer_size do
- text footer, align: :left, valign: :bottom
- end
- bounding_box [bounds.left, bounds.bottom - FOOTER_SPACE], width: bounds.width, height: footer_size do
- text I18n.t('lib.render_pdf.page', number: page_number, count: page_count), align: :right, valign: :bottom
+
+ unless no_footer
+ font_size FOOTER_FONT_SIZE do
+ bounding_box [bounds.left, bounds.bottom - FOOTER_SPACE], width: bounds.width, height: footer_size do
+ text footer, align: :left, valign: :bottom
+ end
+ bounding_box [bounds.left, bounds.bottom - FOOTER_SPACE], width: bounds.width, height: footer_size do
+ text I18n.t('lib.render_pdf.page', number: page_number, count: page_count), align: :right, valign: :bottom
+ end
end
end
end
diff --git a/app/mailers/mailer.rb b/app/mailers/mailer.rb
index 52e1354f..45ded3cb 100644
--- a/app/mailers/mailer.rb
+++ b/app/mailers/mailer.rb
@@ -51,6 +51,18 @@ class Mailer < ActionMailer::Base
subject: I18n.t('mailer.welcome.subject')
end
+ # Sends automatically generated invoicesfor group orders to ordergroup members
+ def group_order_invoice(group_order_invoice, user)
+ @user = user
+ @group_order_invoice = group_order_invoice
+ @group_order = group_order_invoice.group_order
+ @supplier = @group_order.order.supplier.name
+ @group = @group_order.ordergroup
+ add_group_order_invoice_attachments(group_order_invoice)
+ mail to: user,
+ subject: I18n.t('mailer.group_order_invoice.subject', group: @group.name, supplier: @supplier)
+ end
+
# Sends order result for specific Ordergroup
def order_result(user, group_order)
@order = group_order.order
@@ -168,6 +180,11 @@ class Mailer < ActionMailer::Base
attachments['order.csv'] = OrderCsv.new(order, options).to_csv
end
+ def add_group_order_invoice_attachments(group_order_invoice)
+ attachment_name = group_order_invoice.name + '.pdf'
+ attachments[attachment_name] = GroupOrderInvoicePdf.new(group_order_invoice.load_data_for_invoice).to_pdf
+ end
+
# separate method to allow plugins to mess with the text
def additonal_welcome_text(user)
end
diff --git a/app/models/group_order.rb b/app/models/group_order.rb
index c789ef4e..3e74754c 100644
--- a/app/models/group_order.rb
+++ b/app/models/group_order.rb
@@ -9,6 +9,7 @@ class GroupOrder < ApplicationRecord
has_many :group_order_articles, :dependent => :destroy
has_many :order_articles, :through => :group_order_articles
has_one :financial_transaction
+ has_one :group_order_invoice
belongs_to :updated_by, optional: true, class_name: 'User', foreign_key: 'updated_by_user_id'
validates_presence_of :order_id
diff --git a/app/models/group_order_invoice.rb b/app/models/group_order_invoice.rb
new file mode 100644
index 00000000..21557161
--- /dev/null
+++ b/app/models/group_order_invoice.rb
@@ -0,0 +1,58 @@
+class GroupOrderInvoice < ApplicationRecord
+ belongs_to :group_order
+ validates_presence_of :group_order
+ validates_uniqueness_of :invoice_number
+ validate :tax_number_set
+ after_initialize :init, unless: :persisted?
+
+ def generate_invoice_number(count)
+ trailing_number = count.to_s.rjust(4, '0')
+ if GroupOrderInvoice.find_by(invoice_number: self.invoice_date.strftime("%Y%m%d") + trailing_number)
+ generate_invoice_number(count.to_i + 1)
+ else
+ self.invoice_date.strftime("%Y%m%d") + trailing_number
+ end
+ end
+
+ def tax_number_set
+ if FoodsoftConfig[:contact][:tax_number].blank?
+ errors.add(:group_order_invoice, "Keine Steuernummer in FoodsoftConfig :contact gesetzt")
+ end
+ end
+
+ def init
+ self.invoice_date = Time.now unless invoice_date
+ self.invoice_number = generate_invoice_number(1) unless self.invoice_number
+ self.payment_method = FoodsoftConfig[:group_order_invoices]&.[](:payment_method) || I18n.t('activerecord.attributes.group_order_invoice.payment_method') unless self.payment_method
+ end
+
+ def name
+ I18n.t('activerecord.attributes.group_order_invoice.name') + "_#{invoice_number}"
+ end
+
+ def load_data_for_invoice
+ invoice_data = {}
+ order = group_order.order
+ invoice_data[:supplier] = order.supplier.name
+ invoice_data[:ordergroup] = group_order.ordergroup
+ invoice_data[:group_order] = group_order
+ invoice_data[:invoice_number] = invoice_number
+ invoice_data[:invoice_date] = invoice_date
+ invoice_data[:tax_number] = FoodsoftConfig[:contact][:tax_number]
+ invoice_data[:payment_method] = payment_method
+ invoice_data[:order_articles] = {}
+ group_order.order_articles.each do |order_article|
+ # Get the result of last time ordering, if possible
+ goa = group_order.group_order_articles.detect { |tmp_goa| tmp_goa.order_article_id == order_article.id }
+
+ # Build hash with relevant data
+ invoice_data[:order_articles][order_article.id] = {
+ :price => order_article.article.fc_price,
+ :quantity => (goa ? goa.quantity : 0),
+ :total_price => (goa ? goa.total_price : 0),
+ :tax => order_article.article.tax
+ }
+ end
+ invoice_data
+ end
+end
diff --git a/app/views/active_storage/blobs/_blob.html.erb b/app/views/active_storage/blobs/_blob.html.erb
new file mode 100644
index 00000000..49ba357d
--- /dev/null
+++ b/app/views/active_storage/blobs/_blob.html.erb
@@ -0,0 +1,14 @@
+ attachment--<%= blob.filename.extension %>">
+ <% if blob.representable? %>
+ <%= image_tag blob.representation(resize_to_limit: local_assigns[:in_gallery] ? [ 800, 600 ] : [ 1024, 768 ]) %>
+ <% end %>
+
+
+ <% if caption = blob.try(:caption) %>
+ <%= caption %>
+ <% else %>
+ <%= blob.filename %>
+ <%= number_to_human_size blob.byte_size %>
+ <% end %>
+
+
diff --git a/app/views/admin/configs/_tab_foodcoop.html.haml b/app/views/admin/configs/_tab_foodcoop.html.haml
index 5aea80c2..efea4d81 100644
--- a/app/views/admin/configs/_tab_foodcoop.html.haml
+++ b/app/views/admin/configs/_tab_foodcoop.html.haml
@@ -7,4 +7,5 @@
= config_input c, :country, as: :string, input_html: {class: 'input-xlarge'}
= config_input c, :email, required: true, input_html: {class: 'input-xlarge'}
= config_input c, :phone, input_html: {class: 'input-medium'}
+ = config_input c, :tax_number, input_html: {class: 'input-medium'}
= config_input form, :homepage, required: true, as: :url, input_html: {class: 'input-xlarge'}
diff --git a/app/views/admin/configs/_tab_payment.html.haml b/app/views/admin/configs/_tab_payment.html.haml
index 3fd7ca0a..70502c90 100644
--- a/app/views/admin/configs/_tab_payment.html.haml
+++ b/app/views/admin/configs/_tab_payment.html.haml
@@ -13,6 +13,11 @@
= config_input form, :charge_members_manually, as: :boolean
= config_input form, :use_iban, as: :boolean
= config_input form, :use_self_service, as: :boolean
+%h4= t '.group_order_invoices'
+= form.fields_for :group_order_invoices do |field|
+ = config_input field, :use_automatic_invoices, as: :boolean
+ = config_input field, :vat_exempt, as: :boolean
+ = config_input field, :payment_method, as: :string, input_html: {class: 'input-medium'}
%h4= t '.schedule_title'
= form.simple_fields_for :order_schedule do |fields|
diff --git a/app/views/finance/balancing/_orders.html.haml b/app/views/finance/balancing/_orders.html.haml
index 3f20d850..dddb00c2 100644
--- a/app/views/finance/balancing/_orders.html.haml
+++ b/app/views/finance/balancing/_orders.html.haml
@@ -9,6 +9,8 @@
%th= t('.end')
%th= t('.state')
%th= heading_helper Order, :updated_by
+ %th= heading_helper GroupOrderInvoice, :name
+ %th
%th
%tbody
- @orders.each do |order|
@@ -17,6 +19,14 @@
%td=h format_time(order.ends) unless order.ends.nil?
%td= order.closed? ? t('.cleared', amount: number_to_currency(order.foodcoop_result)) : t('.ended')
%td= show_user(order.updated_by)
+ %td{id: "generate-invoice#{order.id}"}
+ - if order.closed?
+ -if FoodsoftConfig[:contact][:tax_number] && order.ordergroups.present?
+ = render :partial => 'group_order_invoices/links', locals:{order: order}
+ -else
+ = I18n.t('activerecord.attributes.group_order_invoice.tax_number_not_set')
+ - else
+ = t('orders.index.not_closed')
%td
- unless order.closed?
- if current_user.role_orders?
diff --git a/app/views/finance/balancing/index.html.haml b/app/views/finance/balancing/index.html.haml
index 1d1fd8b5..0cfeccbd 100644
--- a/app/views/finance/balancing/index.html.haml
+++ b/app/views/finance/balancing/index.html.haml
@@ -1,5 +1,5 @@
- title t('.title')
-
+- puts params
- content_for :actionbar do
- if FoodsoftConfig[:charge_members_manually]
= link_to t('.close_all_direct_with_invoice'), close_all_direct_with_invoice_finance_order_index_path, method: :post, class: 'btn'
diff --git a/app/views/group_order_invoices/_links.html.haml b/app/views/group_order_invoices/_links.html.haml
new file mode 100644
index 00000000..34bd6271
--- /dev/null
+++ b/app/views/group_order_invoices/_links.html.haml
@@ -0,0 +1,25 @@
+.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
+
diff --git a/app/views/group_order_invoices/create.js.erb b/app/views/group_order_invoices/create.js.erb
new file mode 100644
index 00000000..5a43e85d
--- /dev/null
+++ b/app/views/group_order_invoices/create.js.erb
@@ -0,0 +1 @@
+$("#generate-invoice<%= params[:id] %>").html("<%= escape_javascript(render partial: 'links', locals: {order: @order}) %>");
diff --git a/app/views/group_order_invoices/create_multiple.js.erb b/app/views/group_order_invoices/create_multiple.js.erb
new file mode 100644
index 00000000..11ebbe45
--- /dev/null
+++ b/app/views/group_order_invoices/create_multiple.js.erb
@@ -0,0 +1 @@
+$("#generate-invoice<%= @order.id %>").html("<%= escape_javascript(render partial: 'links', locals: {order: @order}) %>");
diff --git a/app/views/group_order_invoices/destroy.js.erb b/app/views/group_order_invoices/destroy.js.erb
new file mode 100644
index 00000000..30ce5985
--- /dev/null
+++ b/app/views/group_order_invoices/destroy.js.erb
@@ -0,0 +1 @@
+$("#generate-invoice<%= @order.id %>").html("<%= escape_javascript(render partial: 'links', locals: {order: @order}) %>");
\ No newline at end of file
diff --git a/app/views/group_orders/_form.html.haml b/app/views/group_orders/_form.html.haml
index 3ffd583e..0cd27c76 100644
--- a/app/views/group_orders/_form.html.haml
+++ b/app/views/group_orders/_form.html.haml
@@ -69,7 +69,7 @@
= f.hidden_field :order_id
= f.hidden_field :updated_by_user_id
= f.hidden_field :ordergroup_id
- %table.table.table-hover
+ %table.table
%thead
%tr
%th= heading_helper Article, :name
@@ -94,7 +94,7 @@
%i.icon-tag
%td{colspan: "9"}
- order_articles.each do |order_article|
- %tr{class: "#{cycle('even', 'odd', name: 'articles')} order-article #{get_missing_units_css_class(@ordering_data[:order_articles][order_article.id][:missing_units])}", valign: "top"}
+ %tr{class: "#{cycle('even', 'odd', name: 'articles')} order-article #{get_missing_units_css_class(@ordering_data[:order_articles][order_article.id][:missing_units])}", valign: "top", tabindex: "0"}
%td.name= order_article.article.name
- if @order.stockit?
%td= truncate order_article.article.supplier.name, length: 15
diff --git a/app/views/layouts/action_text/contents/_content.html.erb b/app/views/layouts/action_text/contents/_content.html.erb
new file mode 100644
index 00000000..9e3c0d0d
--- /dev/null
+++ b/app/views/layouts/action_text/contents/_content.html.erb
@@ -0,0 +1,3 @@
+
+ <%= yield -%>
+
diff --git a/app/views/layouts/email.html.haml b/app/views/layouts/email.html.haml
new file mode 100644
index 00000000..6bcf3b4a
--- /dev/null
+++ b/app/views/layouts/email.html.haml
@@ -0,0 +1,12 @@
+= yield
+\
+%hr
+%ul
+ %li
+ %a{href: root_url} Foodsoft
+ - if FoodsoftConfig[:homepage]
+ %li
+ %a{href: FoodsoftConfig[:homepage]} Foodcoop
+ - if FoodsoftConfig[:help_url]
+ %li
+ %a{href: FoodsoftConfig[:help_url]}= t '.help'
\ No newline at end of file
diff --git a/app/views/mailer/group_order_invoice.text.haml b/app/views/mailer/group_order_invoice.text.haml
new file mode 100644
index 00000000..75948fbe
--- /dev/null
+++ b/app/views/mailer/group_order_invoice.text.haml
@@ -0,0 +1 @@
+= raw t '.text', group: @group.name, supplier: @supplier , foodcoop: FoodsoftConfig[:name]
diff --git a/config/application.rb b/config/application.rb
index 9c0ade99..f76faa95 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -67,6 +67,8 @@ module Foodsoft
config.autoloader = :zeitwerk
+ config.active_storage.variant_processor = :mini_magick
+
# Ex:- :default =>''
# CORS for API
diff --git a/config/environments/production.rb b/config/environments/production.rb
index d0f06b95..d08234e5 100644
--- a/config/environments/production.rb
+++ b/config/environments/production.rb
@@ -29,7 +29,7 @@ Rails.application.configure do
config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present?
# Compress JavaScripts and CSS.
- config.assets.js_compressor = :uglifier
+ config.assets.js_compressor = :terser
config.assets.css_compressor = :sass
# Do not fallback to assets pipeline if a precompiled asset is missed.
diff --git a/config/importmap.rb b/config/importmap.rb
index 050818ab..f882664b 100644
--- a/config/importmap.rb
+++ b/config/importmap.rb
@@ -1,2 +1,4 @@
# Pin npm packages by running ./bin/importmap
-pin "application", preload: true
\ No newline at end of file
+pin "application", preload: true
+pin "trix"
+pin "@rails/actiontext", to: "actiontext.js"
diff --git a/config/locales/de.yml b/config/locales/de.yml
index 5a1a5b35..f400227d 100644
--- a/config/locales/de.yml
+++ b/config/locales/de.yml
@@ -90,6 +90,17 @@ de:
tolerance: Toleranz
total_price: Summe
unit_price: Preis/Einheit
+ group_order_invoice:
+ name: Bestellgruppenrechnung
+ links:
+ delete: Rechnung löschen
+ download: Rechnung herunterladen
+ generate: Rechnung erzeugen
+ invoice_date: Datum der Bestellgruppenrechnung
+ generate_with_date: setzen & erzeugen
+
+ payment_method: Guthaben
+ tax_number_not_set: Steuernummer in den Einstellungen nicht gesetzt
invoice:
amount: Betrag
attachment: Anhang
@@ -318,6 +329,7 @@ de:
emails_title: E-Mails versenden
tab_payment:
schedule_title: Bestellschema
+ group_order_invoices: Bestellgruppenrechnungen
tab_security:
default_roles_title: Zugriff auf
default_roles_paragraph: Jedes Mitglied der Foodcoop hat automatisch Zugriff auf folgende Bereiche.
@@ -602,6 +614,10 @@ de:
email_from: E-Mails werden so aussehen, als ob sie von dieser Adresse gesendet wurden. Kann leer gelassen werden, um die Kontaktadresse der Foodcoop zu benutzen.
email_replyto: Setze diese Adresse, wenn Du Antworten auf Foodsoft E-Mails auf eine andere, als die oben angegebene Absenderadresse bekommen möchtest.
email_sender: E-Mails werden so aussehen, als ob sie von dieser Adresse versendet wurden. Um zu vermeiden, dass E-Mails dadurch als Spam eingeordnet werden, muss der Webserver möglicherweise im SPF Eintrag der Domain der E-Mail Adresse eingetragen werden.
+ group_order_invoices:
+ use_automativ_go_invoices: Es werden auf die Bestellgruppen zugeschnittene Rechnungen für die jeweilige Bestellung beim Klicken auf "abrechnen" an alle Bestellgruppenmitglieder per Mail versendet.
+ payment_method: Zahlungsart wird auf der Bestellgruppenrechnung deklariert
+ vat_exempt: Eine Auflistung der Rechnungsartikel erfolgt ohne explizite Ausweisung der MwSt. und die Rechnung erhält den notwendigen Zusatz bzgl. der Kleinunternehmerregelung §19 (FoodCoop Marge ebenfalls nicht in Rechnung enthalten)
help_url: Link zur Dokumentationsseite
homepage: Webseite der Foodcoop
ignore_browser_locale: Ignoriere die Sprache des Computers des Anwenders, wenn der Anwender noch keine Sprache gewählt hat.
@@ -644,6 +660,7 @@ de:
phone: Telefon
street: Straße
zip_code: Postleitzahl
+ tax_number: Steuernummer
currency_space: Leerzeichen hinzufügen
currency_unit: Währung
custom_css: Angepasstes CSS
@@ -658,6 +675,10 @@ de:
email_from: Absenderadresse
email_replyto: Antwortadresse
email_sender: Senderadresse
+ group_order_invoices:
+ use_automatic_invoices: Automatisch bei Abrechnung per Mail versenden
+ payment_method: Zahlungsart
+ vat_exempt: Diese Foodcoop ist MwSt. befreit
help_url: URL Dokumentation
homepage: Webseite
ignore_browser_locale: Browsersprache ignorieren
@@ -743,6 +764,46 @@ de:
update:
notice: Lieferung wurde aktualisiert.
documents:
+ group_order_invoice_pdf:
+ filename: Rechnung%{number}
+ invoicer: Rechnungsteller*in
+ invoicee: Rechnungsempfänger*in
+ invoice_date: 'Rechnungsdatum: %{invoice_date}'
+ invoice_number: 'Rechnungsnummer: %{invoice_number}'
+ markup_included: zzgl. Foodcoop Marge auf brutto Preis %{marge}%
+ ordergroup:
+ contact_phone: 'Telefonnummer: %{contact_phone}'
+ contact_address: 'Adresse : %{contact_address}'
+ 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: Zu zahlen gesamt (brutto)
+ 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}'
@@ -766,6 +827,7 @@ de:
heading: Artikelübersicht (%{count})
title: 'Sortiermatrix der Bestellung: %{name}, beendet am %{date}'
errors:
+ check_tax_number: Überprüft, ob die Steuernummer der Foodcoop richtig gesetzt ist
general: Ein Problem ist aufgetreten.
general_again: Ein Fehler ist aufgetreten. Bitte erneut versuchen.
general_msg: 'Ein Fehler ist aufgetreten: %{msg}'
@@ -789,6 +851,8 @@ de:
close:
alert: 'Ein Fehler ist beim Abrechnen aufgetreten: %{message}'
notice: Bestellung wurde erfolgreich abgerechnet, die Kontostände aktualisiert.
+ notice_mail: Bestellung wurde erfolgreich abgerechnet, die Kontostände aktualisiert. Außerdem wurden automatisch Rechnungen an die Bestellgruppenmitglieder geschickt.
+ settings_not_set: Keine Emails mit Bestellgruppenrechnungen versendet. Bitte überprüfe die Einstellungen. Steuernummer gesetzt?
close_all_direct_with_invoice:
notice: 'Es wurden %{count} Bestellung abgerechnet.'
close_direct:
@@ -853,6 +917,7 @@ de:
ended: beendet
name: Lieferantin
no_closed_orders: Derzeit gibt es keine beendeten Bestellungen.
+
state: Status
summary:
changed: Daten wurden verändert!
@@ -1221,6 +1286,7 @@ de:
footer_2_foodsoft: 'Foodsoft: %{url}'
footer_3_homepage: 'Foodcoop: %{url}'
footer_4_help: 'Hilfe: %{url}'
+ help: 'Hilfe'
foodsoft: Foodsoft
footer:
revision: Revision %{revision}
@@ -1264,6 +1330,15 @@ de:
feedback:
header: "%{user} schrieb am %{date}:"
subject: Feedback zur 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: |
@@ -1496,6 +1571,7 @@ de:
orders_finished: Beendet
orders_open: Laufend
orders_settled: Abgerechnet
+ not_closed: Bestellung noch nicht abgerechnet
title: Bestellungen verwalten
model:
close_direct_message: Die Bestellung wurde abgechlossen, ohne die Mitgliederkonten zu belasten.
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 59e94385..ab8f4233 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -90,6 +90,16 @@ en:
tolerance: Tolerance
total_price: Sum
unit_price: Price/Unit
+ group_order_invoice:
+ name: Group order invoice
+ links:
+ delete: delete invoice
+ download: download invoice
+ invoice_date: date of group order invoice
+ generate: generate invoice
+ generate_with_date: set & generate
+ payment_method: Credit
+ tax_number_not_set: Tax number not set in configs
invoice:
amount: Amount
attachment: Attachment
@@ -318,6 +328,7 @@ en:
emails_title: Sending email
tab_payment:
schedule_title: Ordering schedule
+ group_order_invoices: Group order invoices
tab_security:
default_roles_title: Access to
default_roles_paragraph: By default every member of the foodcoop has access to the following areas.
@@ -603,6 +614,9 @@ en:
email_from: Emails will appear to be from this email address. Leave empty to use the foodcoop's contact address.
email_replyto: Set this when you want to receive replies from emails sent by Foodsoft on a different address than the above.
email_sender: Emails will appear to be sent from this email address. To avoid emails sent being classified as spam, the webserver may need to be registered in the SPF record of the email address's domain.
+ use_automatic_invoices: A listing of the invoice items is made without explicit display of VAT and the invoice receives the necessary addition regarding the small business regulation §19 (applies to Germany)
+ payment_method: Payment type is declared on the order group invoice
+ vat_exempt: A listing of the invoice items is made without explicit display of VAT and the invoice contains the necessary addition regarding the German Kleinunternehmerregelung §19 UStG (attention! FoodCoop marge not included in nvoice).
help_url: Documentation website.
homepage: Website of your foodcoop.
ignore_browser_locale: Ignore the language of user's computer when the user has not chosen a language yet.
@@ -630,6 +644,8 @@ en:
tolerance_is_costly: Order as much of the member tolerance as possible (compared to only as much needed to fill the last box). Enabling this also includes the tolerance in the total price of the open member order.
distribution_strategy: How articles should be distributed after an order has been received.
use_apple_points: When the apple point system is enabled, members are required to do some tasks to be able to keep ordering.
+ use_automatic_invoices: When an order is settled, invoices for the individual order groups are automatically sent by mail
+ payment_method: Payment Method for group order invoices
use_boxfill: When enabled, near end of an order, members are only able to change their order when increases the total amount ordered. This helps to fill any remaining boxes. You still need to set a box-fill date for the orders.
use_iban: When enabled, supplier and user provide an additonal field for storing the international bank account number.
use_nick: Show and use nicknames instead of real names. When enabling this, please check that each user has a nickname.
@@ -645,6 +661,7 @@ en:
phone: Phone
street: Street
zip_code: Postcode
+ tax_number: Tax number
currency_space: add space
currency_unit: Currency
custom_css: Custom CSS
@@ -688,6 +705,10 @@ en:
first_order_first_serve: First distribute to those who ordered first
no_automatic_distribution: No automatic distribution
use_apple_points: Apple points
+ group_order_invoices:
+ use_automatic_invoices: Send automatically via mail after oder settlement
+ payment_method: Payment method
+ vat_exempt: This foodcoopis VAT exempt
use_boxfill: Box-fill phase
use_iban: Use IBAN
use_nick: Use nicknames
@@ -745,6 +766,46 @@ en:
update:
notice: Delivery was updated.
documents:
+ group_order_invoice_pdf:
+ ordergroup:
+ contact_phone: 'Phone: %{contact_phone}'
+ contact_address: 'Adress : %{contact_address}'
+ 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}'
@@ -768,6 +829,7 @@ en:
heading: Article overview (%{count})
title: 'Order sorting matrix: %{name}, closed at %{date}'
errors:
+ check_tax_number: Please check whether the foodcoop's tax number is set correctly.
general: A problem has occured.
general_again: A problem has occured. Please try again.
general_msg: 'A problem has occured: %{msg}'
@@ -791,6 +853,7 @@ en:
close:
alert: 'An error occured while accounting: %{message}'
notice: Order was settled succesfully, the balance of the account was updated.
+ settings_not_set: No emails with order group invoices sent. Please check the settings. Tax number set?
close_all_direct_with_invoice:
notice: '%{count} orders have been settled.'
close_direct:
@@ -1224,6 +1287,7 @@ en:
footer_2_foodsoft: 'Foodsoft: %{url}'
footer_3_homepage: 'Foodcoop: %{url}'
footer_4_help: 'Help: %{url}'
+ help: 'Help'
foodsoft: Foodsoft
footer:
revision: revision %{revision}
@@ -1268,6 +1332,15 @@ en:
feedback:
header: "%{user} wrote at %{date}:"
subject: Feedback for Foodsoft
+ group_order_invoice:
+ subject: Order group invoice for %{group} at %{supplier}
+ text: |
+ Dear order group %{group},
+
+ The collective order at %{supplier} has just been settled and invoices have been created for the respective order groups.
+ Attached you will find your invoice.
+
+ Best regards from %{foodcoop}
from_via_foodsoft: "%{name} via Foodsoft"
invite:
subject: Invitation to the Foodcoop
@@ -1507,6 +1580,7 @@ en:
orders_finished: Closed
orders_open: Open
orders_settled: Settled
+ not_closed: Order not yet settled
title: Manage orders
model:
close_direct_message: Order settled without charging member accounts.
diff --git a/config/locales/es.yml b/config/locales/es.yml
index 620ec3bb..c713dd24 100644
--- a/config/locales/es.yml
+++ b/config/locales/es.yml
@@ -1082,6 +1082,7 @@ es:
layouts:
email:
footer_4_help: 'Ayuda: %{url}'
+ help: 'Ayuda'
footer:
revision: revisión %{revision}
header:
diff --git a/config/locales/fr.yml b/config/locales/fr.yml
index 4dbdb864..b1199dc7 100644
--- a/config/locales/fr.yml
+++ b/config/locales/fr.yml
@@ -834,6 +834,7 @@ fr:
email:
footer_3_homepage: 'Boufcoop: %{url}'
footer_4_help: 'Aide: %{url}'
+ help: 'Aide'
footer:
revision: révision %{revision}
header:
diff --git a/config/locales/nl.yml b/config/locales/nl.yml
index 4c97dda4..e66bae80 100644
--- a/config/locales/nl.yml
+++ b/config/locales/nl.yml
@@ -1194,6 +1194,7 @@ nl:
footer_2_foodsoft: 'Foodsoft: %{url}'
footer_3_homepage: 'Foodcoop: %{url}'
footer_4_help: 'Help: %{url}'
+ help: 'Help'
foodsoft: Foodsoft
footer:
revision: revisie %{revision}
diff --git a/config/routes.rb b/config/routes.rb
index 83e65707..9b98f17a 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -142,10 +142,14 @@ Rails.application.routes.draw do
end
end
+
+ post 'finance/group_order_invoice', to: 'group_order_invoices#create_multiple'
+ resources :group_order_invoices
+
resources :article_categories
-
+
########### Finance
-
+
namespace :finance do
root to: 'base#index'
@@ -174,6 +178,7 @@ Rails.application.routes.draw do
get :unpaid, on: :collection
end
+
resources :links, controller: 'financial_links', only: [:create, :show] do
collection do
get :incomplete
diff --git a/db/migrate/20211208142719_create_group_order_invoices.rb b/db/migrate/20211208142719_create_group_order_invoices.rb
new file mode 100644
index 00000000..b0aa13f7
--- /dev/null
+++ b/db/migrate/20211208142719_create_group_order_invoices.rb
@@ -0,0 +1,13 @@
+class CreateGroupOrderInvoices < ActiveRecord::Migration[5.2]
+ def change
+ create_table :group_order_invoices do |t|
+ t.integer :group_order_id
+ t.bigint :invoice_number, unique: true, limit: 8
+ t.date :invoice_date
+ t.string :payment_method
+
+ t.timestamps
+ end
+ add_index :group_order_invoices, :group_order_id, unique: true
+ end
+end
diff --git a/db/migrate/20230209105256_create_action_text_tables.action_text.rb b/db/migrate/20230209105256_create_action_text_tables.action_text.rb
new file mode 100644
index 00000000..1be48d70
--- /dev/null
+++ b/db/migrate/20230209105256_create_action_text_tables.action_text.rb
@@ -0,0 +1,26 @@
+# This migration comes from action_text (originally 20180528164100)
+class CreateActionTextTables < ActiveRecord::Migration[6.0]
+ def change
+ # Use Active Record's configured type for primary and foreign keys
+ primary_key_type, foreign_key_type = primary_and_foreign_key_types
+
+ create_table :action_text_rich_texts, id: primary_key_type do |t|
+ t.string :name, null: false
+ t.text :body, size: :long
+ t.references :record, null: false, polymorphic: true, index: false, type: foreign_key_type
+
+ t.timestamps
+
+ t.index [ :record_type, :record_id, :name ], name: "index_action_text_rich_texts_uniqueness", unique: true
+ end
+ end
+
+ private
+ def primary_and_foreign_key_types
+ config = Rails.configuration.generators
+ setting = config.options[config.orm][:primary_key_type]
+ primary_key_type = setting || :primary_key
+ foreign_key_type = setting || :bigint
+ [primary_key_type, foreign_key_type]
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 50c24c41..af6ecc24 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,17 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema[7.0].define(version: 2023_01_06_144440) do
+ActiveRecord::Schema[7.0].define(version: 2023_02_09_105256) do
+ create_table "action_text_rich_texts", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t|
+ t.string "name", null: false
+ t.text "body", size: :long
+ t.string "record_type", null: false
+ t.bigint "record_id", null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.index ["record_type", "record_id", "name"], name: "index_action_text_rich_texts_uniqueness", unique: true
+ end
+
create_table "active_storage_attachments", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t|
t.string "name", null: false
t.string "record_type", null: false
@@ -182,6 +192,16 @@ ActiveRecord::Schema[7.0].define(version: 2023_01_06_144440) do
t.index ["order_article_id"], name: "index_group_order_articles_on_order_article_id"
end
+ create_table "group_order_invoices", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t|
+ t.integer "group_order_id"
+ t.bigint "invoice_number"
+ t.date "invoice_date"
+ t.string "payment_method"
+ t.datetime "created_at", precision: nil, null: false
+ t.datetime "updated_at", precision: nil, null: false
+ t.index ["group_order_id"], name: "index_group_order_invoices_on_group_order_id", unique: true
+ end
+
create_table "group_orders", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t|
t.integer "ordergroup_id"
t.integer "order_id", default: 0, null: false
diff --git a/deployment/.env.sample b/deployment/.env.sample
new file mode 100644
index 00000000..a0d398c6
--- /dev/null
+++ b/deployment/.env.sample
@@ -0,0 +1,65 @@
+TYPE=foodsoft
+
+DOMAIN=order.example.org
+#EXTRA_DOMAINS=', `www.order.example.com`'
+LETS_ENCRYPT_ENV=production
+COMPOSE_FILE="compose.yml"
+
+# app settings
+FOODCOOP_MULTI_INSTALL=true # Best for now, see https://github.com/foodcoops/foodsoft/pull/841
+FOODCOOP_NAME=example
+FOODCOOP_CITY=XXX
+FOODCOOP_COUNTRY=XXX
+FOODCOOP_EMAIL=info@example.org
+FOODCOOP_PHONE=XXX
+FOODCOOP_STREET=XXX
+FOODCOOP_ZIP_CODE=XXX
+FOODCOOP_HOMEPAGE=https://order.example.org
+FOODCOOP_HELP_URL=https://order.example.org
+FOODCOOP_TIME_ZONE=Amsterdam
+FOODCOOP_USE_NICK=true
+FOODCOOP_LANGUAGE=en
+FOODCOOP_FOOTER='example hosted by Your Tech Co-op.'
+USE_APPLE_POINTS=false
+STOP_ORDERING_UNDER=75
+MINIMUM_BALANCE=0
+
+# database settings
+MYSQL_DB=foodsoft
+MYSQL_HOST=db
+MYSQL_PORT=3306
+MYSQL_USER=foodsoft
+
+# shared supplier list settings
+# COMPOSE_FILE="$COMPOSE_FILE:compose.sharedlists.yml"
+# ENABLE_SHARED_LISTS=0
+# SHARED_LISTS_DB_TYPE=mysql2
+# SHARED_LISTS_HOST=order.otherfoodcoop.org
+# SHARED_LISTS_DB_NAME=sharedlists
+# SHARED_LISTS_USER=example
+
+# Group order invoices generation pull request
+# https://github.com/foodcoops/foodsoft/pull/907
+# COMPOSE_FILE="$COMPOSE_FILE:compose.groupOrderInvoice.yml"
+
+# outgoing mail settings
+EMAIL_SENDER=noreply@example.org
+EMAIL_ERROR=systems@example.org
+SMTP_ADDRESS=mail.example.com
+SMTP_AUTHENTICATION=plain
+SMTP_DOMAIN=mail.example.com
+SMTP_ENABLE_STARTTLS_AUTO=true
+SMTP_PORT=587
+SMTP_USER_NAME=foodsoft
+
+# incoming mail settings
+EMAIL_REPLY_DOMAIN=example.org
+SMTP_SERVER_HOST=0.0.0.0
+SMTP_SERVER_PORT=2525
+
+# secret versions
+SECRET_DB_PASSWORD_VERSION=v1
+SECRET_DB_ROOT_PASSWORD_VERSION=v1
+SECRET_SHARED_LISTS_DB_PASSWORD_VERSION=v1
+SECRET_SMTP_PASSWORD_VERSION=v1
+SECRET_SECRET_KEY_BASE_VERSION=v1 # length=30
diff --git a/deployment/app_config.yml.tmpl b/deployment/app_config.yml.tmpl
new file mode 100644
index 00000000..f1e1a580
--- /dev/null
+++ b/deployment/app_config.yml.tmpl
@@ -0,0 +1,168 @@
+# {{ env "DOMAIN" }} configuration
+
+default: &defaults
+ # If you wanna serve more than one foodcoop with one installation
+ # Don't forget to setup databases for each foodcoop. See also MULTI_COOP_INSTALL
+ multi_coop_install: {{ env "FOODCOOP_MULTI_INSTALL" }}
+
+ # If multi_coop_install you have to use a coop name, which you you wanna be selected by default
+ default_scope: "{{ env "FOODCOOP_NAME" }}"
+
+ # name of this foodcoop
+ name: "{{ env "FOODCOOP_NAME" }}"
+
+ # foodcoop contact information (used for FAX messages)
+ contact:
+ street: "{{ env "FOODCOOP_STREET" }}"
+ zip_code: "{{ env "FOODCOOP_ZIP_CODE" }}"
+ city: "{{ env "FOODCOOP_CITY" }}"
+ country: "{{ env "FOODCOOP_COUNTRY" }}"
+ email: "{{ env "FOODCOOP_EMAIL" }}"
+ phone: "{{ env "FOODCOOP_PHONE" }}"
+
+ # Homepage
+ homepage: "{{ env "FOODCOOP_HOMEPAGE" }}"
+
+ # foodsoft documentation URL
+ help_url: "{{ env "FOODCOOP_HELP_URL" }}"
+
+ # documentation URL for the apples&pears work system
+ applepear_url: https://github.com/foodcoops/foodsoft/wiki/%C3%84pfel-u.-Birnen
+
+ # custom foodsoft software URL (used in footer)
+ foodsoft_url: https://foodcoops.github.io
+
+ # Default language
+ default_locale: {{ env "FOODCOOP_LANGUAGE" }}
+
+ # By default, foodsoft takes the language from the webbrowser/operating system.
+ # In case you really want foodsoft in a certain language by default, set this to true.
+ # When members are logged in, the language from their profile settings is still used.
+ ignore_browser_locale: false
+
+ # Default timezone, e.g. UTC, Amsterdam, Berlin, etc.
+ time_zone: "{{ env "FOODCOOP_TIME_ZONE" }}"
+
+ # Currency symbol, and whether to add a whitespace after the unit.
+ currency_unit: €
+ #currency_space: true
+
+ # price markup in percent
+ price_markup: 2.0
+
+ # default vat percentage for new articles
+ tax_default: 7.0
+
+ # tolerance order option: If set to false, article tolerance values do not count
+ # for total article price as long as the order is not finished.
+ tolerance_is_costly: false
+
+ # Ordergroups, which have less than 75 apples should not be allowed to make new orders
+ # Comment out this option to activate this restriction
+ stop_ordering_under: {{ env "STOP_ORDERING_UNDER" }}
+
+ # Comment out to completely hide apple points (be sure to comment stop_ordering_under)
+ use_apple_points: {{ env "USE_APPLE_POINTS" }}
+
+ # ordergroups can only order when their balance is higher than or equal to this
+ # not fully enforced right now, since the check is only client-side
+ minimum_balance: {{ env "MINIMUM_BALANCE" }}
+
+ # how many days there are between two periodic tasks
+ #tasks_period_days: 7
+
+ # how many days upfront periodic tasks are created
+ #tasks_upfront_days: 49
+
+ # default order schedule, used to provide initial dates for new orders
+ # (recurring dates in ical format; no spaces!)
+ #order_schedule:
+ # ends:
+ # recurr: FREQ=WEEKLY;INTERVAL=2;BYDAY=MO
+ # time: '9:00'
+ # # reference point, this is generally the first pickup day; empty is often ok
+ # #initial:
+
+ # When use_nick is enabled, there will be a nickname field in the user form,
+ # and the option to show a nickname instead of full name to foodcoop members.
+ # Members of a user's groups and administrators can still see full names.
+ use_nick: {{ env "FOODCOOP_USE_NICK" }}
+
+ # Most plugins can be enabled/disabled here as well. Messages and wiki are enabled
+ # by default and need to be set to false to disable. Most other plugins needs to
+ # be enabled before they do anything.
+ use_wiki: true
+ use_messages: true
+ use_documents: true
+ use_polls: true
+
+ # Base font size for generated PDF documents
+ #pdf_font_size: 12
+
+ # Page size for generated PDF documents
+ #pdf_page_size: A4
+
+ # Some documents (like group and article PDFs) can include page breaks
+ # after each sublist.
+ #pdf_add_page_breaks: true
+
+ # Alternatively, this can be set for each document.
+ #pdf_add_page_breaks:
+ # order_by_groups: true
+ # order_by_articles: true
+
+ # Page footer (html allowed). Default is a Foodsoft footer. Set to `blank` for no footer.
+ page_footer: {{ env "FOODCOOP_FOOTER" }}
+
+ # Custom CSS for the foodcoop
+ #custom_css: 'body { background-color: #fcffba; }'
+
+ # Uncomment to add tracking code for web statistics, e.g. for Piwik. (Added to bottom of page)
+ #webstats_tracking_code: |
+ #
+ # ......
+
+ # email address to be used as sender
+ email_sender: "{{ env "EMAIL_SENDER" }}"
+
+ # email address to be used as from
+ email_from: "{{ env "EMAIL_SENDER" }}"
+
+ # domain to be used for reply emails
+ reply_email_domain: {{ env "EMAIL_REPLY_DOMAIN" }}
+
+ # If your foodcoop uses a mailing list instead of internal messaging system
+ #mailing_list: list@example.org
+ #mailing_list_subscribe: list-subscribe@example.org
+
+ # Config for the exception_notification plugin
+ notification:
+ error_recipients:
+ - "{{ env "EMAIL_ERROR" }}"
+ sender_address: "\"Foodsoft error\" <{{ env "EMAIL_SENDER" }}>"
+ email_prefix: "[foodsoft] "
+
+ # http config for this host to generate links in emails (uses environment config when not set)
+ protocol: https
+ host: "{{ env "DOMAIN" }}"
+ #port: 3000
+
+ {{ if eq (env "ENABLE_SHARED_LISTS") "1" }}
+ # Access to sharedlists, the external article-database.
+ # This allows a foodcoop to subscribe to a selection of a supplier's full assortment,
+ # and makes it possible to share data with several foodcoops. Using this requires installing
+ # an additional application with a separate database.
+ shared_lists:
+ adapter: "{{ env "SHARED_LISTS_DB_TYPE" }}"
+ host: "{{ env "SHARED_LISTS_HOST" }}"
+ database: "{{ env "SHARED_LISTS_DB_NAME" }}"
+ username: "{{ env "SHARED_LISTS_USER" }}"
+ password: "{{ secret "shared_lists_db_password" }}"
+ {{ end }}
+
+# don't remove this, required to run the app
+production:
+ <<: *defaults
+
+{{ env "FOODCOOP_NAME" }}:
+ <<: *defaults
diff --git a/deployment/compose.yml b/deployment/compose.yml
new file mode 100644
index 00000000..b4484d66
--- /dev/null
+++ b/deployment/compose.yml
@@ -0,0 +1,189 @@
+---
+version: "3.8"
+
+x-env: &env
+ CERTBOT_DISABLED: 1
+ DOMAIN:
+ EMAIL_ERROR:
+ EMAIL_REPLY_DOMAIN:
+ EMAIL_SENDER:
+ FOODCOOP_CITY:
+ FOODCOOP_COUNTRY:
+ FOODCOOP_EMAIL:
+ FOODCOOP_FOOTER:
+ FOODCOOP_HELP_URL:
+ FOODCOOP_HOMEPAGE:
+ FOODCOOP_MULTI_INSTALL:
+ FOODCOOP_NAME:
+ FOODCOOP_PHONE:
+ FOODCOOP_STREET:
+ FOODCOOP_TIME_ZONE:
+ FOODCOOP_ZIP_CODE:
+ FOODCOOP_USE_NICK:
+ FOODCOOP_LANGUAGE:
+ LOG_LEVEL:
+ MINIMUM_BALANCE:
+ MYSQL_DB:
+ MYSQL_HOST:
+ MYSQL_PORT:
+ MYSQL_USER:
+ QUEUE: foodsoft_notifier
+ REDIS_URL: redis://cache:6379
+ SECRET_KEY_BASE_FILE: /run/secrets/secret_key_base
+ SMTP_ADDRESS:
+ SMTP_AUTHENTICATION:
+ SMTP_DOMAIN:
+ SMTP_ENABLE_STARTTLS_AUTO:
+ SMTP_PASSWORD_FILE: /run/secrets/smtp_password
+ SMTP_PORT:
+ SMTP_USER_NAME:
+ STOP_ORDERING_UNDER:
+ USE_APPLE_POINTS:
+
+x-configs: &configs
+ - source: app_config
+ target: /usr/src/app/config/app_config.yml
+ - source: db_config
+ target: /usr/src/app/config/database.yml
+ - source: entrypoint
+ target: /usr/src/app/docker-entrypoint.sh
+ mode: 0555
+
+x-secrets: &secrets
+ - db_password
+ - secret_key_base
+ - smtp_password
+
+services:
+ app:
+ image: ${IMAGE}
+ networks:
+ - internal
+ - proxy
+ secrets: *secrets
+ configs: *configs
+ entrypoint: &entrypoint /usr/src/app/docker-entrypoint.sh
+ environment:
+ <<: *env
+ FOODSOFT_SERVICE: app
+ healthcheck:
+ test: ["CMD", "curl", "-f", "http://localhost:3000"]
+ interval: 15s
+ timeout: 10s
+ retries: 10
+ start_period: 1m
+ deploy:
+ update_config:
+ failure_action: rollback
+ order: start-first
+ labels:
+ - "traefik.enable=true"
+ - "traefik.http.routers.${STACK_NAME}.rule=Host(`${DOMAIN}`${EXTRA_DOMAINS})"
+ - "traefik.http.routers.${STACK_NAME}.entrypoints=web-secure"
+ - "traefik.http.routers.${STACK_NAME}.tls.certresolver=${LETS_ENCRYPT_ENV}"
+ - "traefik.http.services.${STACK_NAME}.loadbalancer.server.port=3000"
+ - "coop-cloud.${STACK_NAME}.version=1.0.0+4.7.1"
+
+ cron:
+ image: ${IMAGE}
+ secrets: *secrets
+ configs: *configs
+ entrypoint: *entrypoint
+ environment:
+ <<: *env
+ FOODSOFT_SERVICE: cron
+ networks:
+ - internal
+
+ worker:
+ image: ${IMAGE}
+ secrets: *secrets
+ configs: *configs
+ entrypoint: *entrypoint
+ environment:
+ <<: *env
+ FOODSOFT_SERVICE: worker
+ networks:
+ - internal
+
+ smtp:
+ image: ${IMAGE}
+ configs: *configs
+ entrypoint: *entrypoint
+ secrets: *secrets
+ environment:
+ <<: *env
+ FOODSOFT_SERVICE: smtp
+ SMTP_SERVER_HOST:
+ SMTP_SERVER_PORT:
+ networks:
+ - proxy
+ - internal
+ deploy:
+ labels:
+ - "traefik.enable=true"
+ - "traefik.tcp.routers.foodsoft-smtp.rule=HostSNI(`*`)"
+ - "traefik.tcp.routers.foodsoft-smtp.entrypoints=foodsoft-smtp"
+ - "traefik.tcp.services.foodsoft-smtp.loadbalancer.server.port=${SMTP_SERVER_PORT}"
+
+ db:
+ image: "mariadb:10.6"
+ command: "mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_520_ci"
+ environment:
+ MYSQL_USER: ${MYSQL_USER}
+ MYSQL_DATABASE: ${MYSQL_DB}
+ MYSQL_PASSWORD_FILE: /run/secrets/db_password
+ MYSQL_ROOT_PASSWORD_FILE: /run/secrets/db_root_password
+ secrets:
+ - db_password
+ - db_root_password
+ volumes:
+ - "db:/var/lib/mysql"
+ networks:
+ - internal
+ deploy:
+ labels:
+ backupbot.backup: "true"
+ backupbot.backup.pre-hook: 'mkdir -p /tmp/backup/ && mysqldump --single-transaction -u root -p"$$(cat /run/secrets/db_root_password)" $${MYSQL_DATABASE} > /tmp/backup/backup.sql'
+ backupbot.backup.post-hook: "rm -rf /tmp/backup"
+ backupbot.backup.path: "/tmp/backup/"
+ cache:
+ image: "redis:6"
+ networks:
+ - internal
+
+networks:
+ internal:
+ proxy:
+ external: true
+
+volumes:
+ db:
+
+configs:
+ app_config:
+ name: ${STACK_NAME}_app_config_${APP_CONFIG_VERSION}
+ file: app_config.yml.tmpl
+ template_driver: golang
+ db_config:
+ name: ${STACK_NAME}_db_config_${DB_CONFIG_VERSION}
+ file: database.yml.tmpl
+ template_driver: golang
+ entrypoint:
+ name: ${STACK_NAME}_entrypoint_${ENTRYPOINT_VERSION}
+ file: entrypoint.sh.tmpl
+ template_driver: golang
+
+secrets:
+ db_password:
+ name: ${STACK_NAME}_db_password_${SECRET_DB_PASSWORD_VERSION}
+ external: true
+ db_root_password:
+ name: ${STACK_NAME}_db_root_password_${SECRET_DB_ROOT_PASSWORD_VERSION}
+ external: true
+ smtp_password:
+ name: ${STACK_NAME}_smtp_password_${SECRET_SMTP_PASSWORD_VERSION}
+ external: true
+ secret_key_base:
+ name: ${STACK_NAME}_secret_key_base_${SECRET_SECRET_KEY_BASE_VERSION}
+ external: true
diff --git a/deployment/database.yml.tmpl b/deployment/database.yml.tmpl
new file mode 100644
index 00000000..bf64dc72
--- /dev/null
+++ b/deployment/database.yml.tmpl
@@ -0,0 +1,9 @@
+production:
+ adapter: "mysql2"
+ encoding: "utf8mb4"
+ collation: "utf8mb4_unicode_520_ci"
+ username: "{{ env "MYSQL_USER" }}"
+ password: "{{ secret "db_password" }}"
+ database: "{{ env "MYSQL_DB" }}"
+ host: "{{ env "MYSQL_HOST" }}"
+ port: "{{ env "MYSQL_PORT" }}"
diff --git a/deployment/entrypoint.sh.tmpl b/deployment/entrypoint.sh.tmpl
new file mode 100644
index 00000000..06f27b08
--- /dev/null
+++ b/deployment/entrypoint.sh.tmpl
@@ -0,0 +1,44 @@
+#!/bin/bash
+
+set -eu
+
+file_env() {
+ local var="$1"
+ local fileVar="${var}_FILE"
+ local def="${2:-}"
+
+ if [ "${!var:-}" ] && [ "${!fileVar:-}" ]; then
+ echo >&2 "error: both $var and $fileVar are set (but are exclusive)"
+ exit 1
+ fi
+
+ local val="$def"
+
+ if [ "${!var:-}" ]; then
+ val="${!var}"
+ elif [ "${!fileVar:-}" ]; then
+ val="$(< "${!fileVar}")"
+ fi
+
+ export "$var"="$val"
+ unset "$fileVar"
+}
+
+file_env "SECRET_KEY_BASE"
+file_env "SMTP_PASSWORD"
+
+echo "------------------------------------------------------------------------------"
+echo "Running entrypoint commands against '$FOODSOFT_SERVICE' service"
+echo "------------------------------------------------------------------------------"
+
+if [ "$FOODSOFT_SERVICE" == "app" ]; then
+ bundle exec rake db:setup || true
+ bundle exec rake db:migrate || true
+ ./proc-start web
+elif [ "$FOODSOFT_SERVICE" == "cron" ]; then
+ ./proc-start cron
+elif [ "$FOODSOFT_SERVICE" == "worker" ]; then
+ ./proc-start worker
+elif [ "$FOODSOFT_SERVICE" == "smtp" ]; then
+ ./proc-start mail
+fi
diff --git a/plugins/messages/app/controllers/messages_controller.rb b/plugins/messages/app/controllers/messages_controller.rb
index 628f145b..159984ed 100644
--- a/plugins/messages/app/controllers/messages_controller.rb
+++ b/plugins/messages/app/controllers/messages_controller.rb
@@ -20,8 +20,8 @@ class MessagesController < ApplicationController
@message.group_id = original_message.group_id
@message.private = original_message.private
@message.subject = I18n.t('messages.model.reply_subject', :subject => original_message.subject)
- @message.body = I18n.t('messages.model.reply_header', :user => original_message.sender.display, :when => I18n.l(original_message.created_at, :format => :short)) + "\n"
- original_message.body.each_line { |l| @message.body += I18n.t('messages.model.reply_indent', :line => l) }
+ @message.body = I18n.t('messages.model.reply_header', :user => original_message.sender.display, :when => I18n.l(original_message.created_at, :format => :short)) + "\n" \
+ + "
" + original_message.body.to_trix_html + "
"
else
redirect_to new_message_url, alert: I18n.t('messages.new.error_private')
end
diff --git a/plugins/messages/app/helpers/messages_helper.rb b/plugins/messages/app/helpers/messages_helper.rb
index d5371fe4..d386e6df 100644
--- a/plugins/messages/app/helpers/messages_helper.rb
+++ b/plugins/messages/app/helpers/messages_helper.rb
@@ -5,7 +5,7 @@ module MessagesHelper
body = ""
else
subject = message.subject
- body = truncate(message.body, :length => length - subject.length)
+ body = truncate(message.body.to_plain_text, :length => length - subject.length)
end
"#{link_to(h(subject), message)}#{h(body)}".html_safe
end
diff --git a/plugins/messages/app/models/message.rb b/plugins/messages/app/models/message.rb
index f6b03c10..b5087d0d 100644
--- a/plugins/messages/app/models/message.rb
+++ b/plugins/messages/app/models/message.rb
@@ -22,6 +22,8 @@ class Message < ApplicationRecord
validates_presence_of :message_recipients, :subject, :body
validates_length_of :subject, :in => 1..255
+ has_rich_text :body
+
after_initialize do
@recipients_ids ||= []
@send_method ||= 'recipients'
diff --git a/plugins/messages/app/views/messages/new.haml b/plugins/messages/app/views/messages/new.haml
index 57d6b452..d288cd72 100644
--- a/plugins/messages/app/views/messages/new.haml
+++ b/plugins/messages/app/views/messages/new.haml
@@ -110,7 +110,7 @@
= f.input :recipient_tokens, :input_html => { 'data-pre' => User.where(id: @message.recipients_ids).map(&:token_attributes).to_json }
= f.input :private, inline_label: t('.hint_private')
= f.input :subject, input_html: {class: 'input-xxlarge'}
- = f.input :body, input_html: {class: 'input-xxlarge', rows: 13}
+ = f.rich_text_area :body, input_html: {class: 'input-xxlarge', rows: 13}
.form-actions
= f.submit class: 'btn btn-primary'
= link_to t('ui.or_cancel'), :back
diff --git a/plugins/messages/app/views/messages/show.haml b/plugins/messages/app/views/messages/show.haml
index 36e7b570..8b3f7c1c 100644
--- a/plugins/messages/app/views/messages/show.haml
+++ b/plugins/messages/app/views/messages/show.haml
@@ -33,7 +33,7 @@
- if @message.can_toggle_private?(current_user)
= link_to t('.change_visibility'), toggle_private_message_path(@message), method: :post, class: 'btn btn-mini'
%hr/
- %p= simple_format(h(@message.body))
+ .trix-content= @message.body
%hr/
%p
= link_to t('.reply'), new_message_path(:message => {:reply_to => @message.id}), class: 'btn'
diff --git a/plugins/messages/app/views/messages_mailer/foodsoft_message.html.haml b/plugins/messages/app/views/messages_mailer/foodsoft_message.html.haml
new file mode 100644
index 00000000..7ca572f3
--- /dev/null
+++ b/plugins/messages/app/views/messages_mailer/foodsoft_message.html.haml
@@ -0,0 +1,11 @@
+= raw @message.body
+%hr
+%ul
+ - if @message.group
+ %li= t '.footer_group', group: @message.group.name
+ %li
+ %a{href: new_message_url('message[reply_to]' => @message.id)}= t '.reply'
+ %li
+ %a{href: message_url(@message)}= t '.see_message_online'
+ %li
+ %a{href: my_profile_url}= t '.messaging_options'
diff --git a/plugins/messages/config/locales/de.yml b/plugins/messages/config/locales/de.yml
index f1615163..eb8cff21 100644
--- a/plugins/messages/config/locales/de.yml
+++ b/plugins/messages/config/locales/de.yml
@@ -138,6 +138,9 @@ de:
Antworten: %{reply_url}
Nachricht online einsehen: %{msg_url}
Nachrichten-Einstellungen: %{profile_url}
+ reply: Antworten
+ see_message_online: Nachricht online einsehen
+ messaging_options: Nachrichten-Einstellungen
footer_group: |
Gesendet an Gruppe: %{group}
navigation:
diff --git a/plugins/messages/config/locales/en.yml b/plugins/messages/config/locales/en.yml
index ede3f88c..ccd8bb6c 100644
--- a/plugins/messages/config/locales/en.yml
+++ b/plugins/messages/config/locales/en.yml
@@ -140,6 +140,9 @@ en:
Reply: %{reply_url}
See message online: %{msg_url}
Messaging options: %{profile_url}
+ reply: Reply
+ see_message_online: See message online
+ messaging_options: Messaging options
footer_group: |
Sent to group: %{group}
navigation:
diff --git a/plugins/messages/config/locales/fr.yml b/plugins/messages/config/locales/fr.yml
index 54584b48..67d452c5 100644
--- a/plugins/messages/config/locales/fr.yml
+++ b/plugins/messages/config/locales/fr.yml
@@ -67,6 +67,9 @@ fr:
Répondre: %{reply_url}
Afficher ce message dans ton navigateur: %{msg_url}
Préférences des messages: %{profile_url}
+ reply: Répondre
+ see_message_online: Afficher ce message dans ton navigateur
+ messaging_options: Préférences des messages
simple_form:
labels:
settings:
diff --git a/plugins/messages/config/locales/nl.yml b/plugins/messages/config/locales/nl.yml
index d3960a23..56738c0b 100644
--- a/plugins/messages/config/locales/nl.yml
+++ b/plugins/messages/config/locales/nl.yml
@@ -140,6 +140,9 @@ nl:
Antwoorden: %{reply_url}
Bericht online lezen: %{msg_url}
Berichtinstellingen: %{profile_url}
+ reply: Antwoorden
+ see_message_online: Bericht online lezen
+ messaging_options: Berichtinstellingen
footer_group: |
Verzenden aan groep: %{group}
navigation:
diff --git a/spec/factories/group_order_invoice.rb b/spec/factories/group_order_invoice.rb
new file mode 100644
index 00000000..89723873
--- /dev/null
+++ b/spec/factories/group_order_invoice.rb
@@ -0,0 +1,7 @@
+require 'factory_bot'
+
+FactoryBot.define do
+ factory :group_order_invoice do
+ group_order { create :group_order }
+ end
+end
diff --git a/spec/integration/group_order_invoices_spec.rb b/spec/integration/group_order_invoices_spec.rb
new file mode 100644
index 00000000..a84d9bc7
--- /dev/null
+++ b/spec/integration/group_order_invoices_spec.rb
@@ -0,0 +1,71 @@
+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')
+ order.reload
+ visit finance_order_index_path
+ expect(page).to have_selector(:link_or_button, I18n.t('activerecord.attributes.group_order_invoice.links.generate'))
+ click_link_or_button I18n.t('activerecord.attributes.group_order_invoice.links.generate')
+ expect(GroupOrderInvoice.all.count).to eq(1)
+ end
+
+ it 'generates multiple Group Order Invoice for order when order is closed if tax_number is set' do
+ goa.update_quantities 2, 0
+ oa.update_results!
+ FoodsoftConfig[:contact][:tax_number] = 12_345_678
+ order.update!(state: 'closed')
+ order.reload
+ visit finance_order_index_path
+ expect(page).to have_selector(:link_or_button, I18n.t('activerecord.attributes.group_order_invoice.links.generate_with_date'))
+ click_link_or_button I18n.t('activerecord.attributes.group_order_invoice.links.generate_with_date')
+ expect(GroupOrderInvoice.all.count).to eq(1)
+ end
+
+ it 'does not generate Group Order Invoice when order is closed if tax_number not set' do
+ goa.update_quantities 2, 0
+ oa.update_results!
+ order.update!(state: 'closed')
+ order.reload
+ visit finance_order_index_path
+ expect(page).to have_content(I18n.t('activerecord.attributes.group_order_invoice.tax_number_not_set'))
+ end
+end
diff --git a/spec/models/group_order_invoice_spec.rb b/spec/models/group_order_invoice_spec.rb
new file mode 100644
index 00000000..24bfcf7e
--- /dev/null
+++ b/spec/models/group_order_invoice_spec.rb
@@ -0,0 +1,59 @@
+require_relative '../spec_helper'
+
+describe GroupOrderInvoice do
+ let(:user) { create :user, groups: [create(:ordergroup)] }
+ let(:supplier) { create :supplier }
+ let(:article) { create :article, supplier: supplier }
+ let(:order) { create :order }
+ let(:group_order) { create :group_order, order: order, ordergroup: user.ordergroup }
+
+ describe 'erroneous group order invoice' do
+ let(:goi) { create :group_order_invoice, group_order_id: group_order.id }
+ it 'does not create group order invoice if tax_number not set' do
+ expect { goi }.to raise_error(ActiveRecord::RecordInvalid, /.*/)
+ end
+ end
+
+ describe 'valid group order invoice' do
+ before do
+ FoodsoftConfig[:contact][:tax_number] = 123_457_8
+ end
+
+ invoice_number1 = Time.now.strftime("%Y%m%d") + '0001'
+ invoice_number2 = Time.now.strftime("%Y%m%d") + '0002'
+
+ let(:user2) { create :user, groups: [create(:ordergroup)] }
+
+ let(:goi1) { create :group_order_invoice, group_order_id: group_order.id }
+ let(:goi2) { create :group_order_invoice, group_order_id: group_order.id }
+
+ let(:group_order2) { create :group_order, order: order, ordergroup: user2.ordergroup }
+
+ let(:goi3) { create :group_order_invoice, group_order_id: group_order2.id }
+ let(:goi4) { create :group_order_invoice, group_order_id: group_order2.id, invoice_number: invoice_number1 }
+
+ it 'creates group order invoice if tax_number is set' do
+ expect(goi1).to be_valid
+ end
+
+ it 'sets invoice_number according to date' do
+ number = Time.now.strftime("%Y%m%d") + '0001'
+ expect(goi1.invoice_number).to eq(number.to_i)
+ end
+
+ it 'fails to create if group_order_id is used multiple times for creation' do
+ expect(goi1.group_order.id).to eq(group_order.id)
+ expect { goi2 }.to raise_error(ActiveRecord::RecordNotUnique)
+ end
+
+ it 'creates two different group order invoice with different invoice_numbers' do
+ expect(goi1.invoice_number).to eq(invoice_number1.to_i)
+ expect(goi3.invoice_number).to eq(invoice_number2.to_i)
+ end
+
+ it 'fails to create two different group order invoice with same invoice_numbers' do
+ goi1
+ expect { goi4 }.to raise_error(ActiveRecord::RecordInvalid)
+ end
+ end
+end