Compare commits

...

5 commits

Author SHA1 Message Date
1933f21835 downloadmultiple as zip 2023-09-21 21:09:50 +02:00
6058b2f239 mark bold sum for vat exempt foodcoops 2023-08-30 10:11:25 +02:00
410479f6cc add ordergroupname to invoice file name 2023-08-30 10:01:29 +02:00
6f7c057a47 make explicit deposit in invoices work 2023-08-22 15:07:49 +02:00
Philipp Rothmann
85bdf28f91 merge automatic group order invoice generation
see https://github.com/foodcoops/foodsoft/pull/907 for reference
and original work by viehlieb

Co-authored-by: viehlieb <pf@pragma-shift.net>

fix PDF Pdf
2023-08-11 10:48:52 +02:00
37 changed files with 988 additions and 69 deletions

View file

@ -274,6 +274,8 @@ Lint/Void:
# Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes. # Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes.
Metrics/AbcSize: Metrics/AbcSize:
Max: 143 Max: 143
Exclude:
- 'app/documents/group_order_invoice_pdf.rb'
# Offense count: 13 # Offense count: 13
# Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns, inherit_mode. # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns, inherit_mode.
@ -407,6 +409,7 @@ RSpec/Capybara/FeatureMethods:
- 'spec/integration/receive_spec.rb' - 'spec/integration/receive_spec.rb'
- 'spec/integration/session_spec.rb' - 'spec/integration/session_spec.rb'
- 'spec/integration/supplier_spec.rb' - 'spec/integration/supplier_spec.rb'
- 'spec/integration/group_order_invoices_spec.rb'
# Offense count: 4 # Offense count: 4
RSpec/Capybara/SpecificMatcher: RSpec/Capybara/SpecificMatcher:

View file

@ -241,6 +241,9 @@ table {
tr.order-article:hover .article-info { tr.order-article:hover .article-info {
display: none; display: none;
} }
tr.order-article:focus .article-info {
display: none;
}
} }
#order-footer { #order-footer {
@ -275,11 +278,13 @@ tr.order-article .article-info {
display: none; display: none;
} }
tr.order-article:hover .article-info { tr.order-article:focus{
background-color: #E4EED6;
}
tr.order-article:focus .article-info {
display: block; display: block;
} }
// ********* Articles // ********* Articles
tr.just-updated { tr.just-updated {

View file

@ -0,0 +1,17 @@
module Concerns::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

View file

@ -5,7 +5,7 @@ class Finance::BalancingController < Finance::BaseController
def new def new
@order = Order.find(params[:order_id]) @order = Order.find(params[:order_id])
flash.now.alert = t('.alert') if @order.closed? flash.now.alert = t('finance.balancing.new.alert') if @order.closed? && flash[:alert].blank?
@comments = @order.comments @comments = @order.comments
@articles = @order.order_articles.ordered_or_member.includes(:article, :article_price, @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]) @order = Order.find(params[:id])
@type = FinancialTransactionType.find_by_id(params.permit(:type)[:type]) @type = FinancialTransactionType.find_by_id(params.permit(:type)[:type])
@order.close!(@current_user, @type) @order.close!(@current_user, @type)
redirect_to finance_order_index_url, notice: t('.notice') note = t('finance.balancing.close.notice')
rescue StandardError => e if @order.closed?
redirect_to new_finance_order_url(order_id: @order.id), alert: t('.alert', message: e.message) 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
# Close the order directly, without automaticly updating ordergroups account balances # Close the order directly, without automaticly updating ordergroups account balances

View file

@ -0,0 +1,87 @@
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
def download_all
order = Order.find(params[:order_id])
invoices = order.group_orders.map(&:group_order_invoice)
pdf = {}
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)
invoice_file = Tempfile.new("#{pdf.filename}")
File.open(invoice_file.path, 'w:ASCII-8BIT') do |file|
file.write(pdf.to_pdf)
end
zipfile.add("#{pdf.filename}", invoice_file.path) unless zipfile.find_entry("#{pdf.filename}")
end
end
zip_data = File.read(temp_file.path)
respond_to do |format|
format.html {
send_data(zip_data, type: 'application/zip', filename: "#{l order.ends, format: :file}-#{order.supplier.name}-#{order.id}.zip", disposition: 'attachment')
}
end
end
end

View file

@ -0,0 +1,264 @@
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
if separate_deposits
total_deposit = 0
total_deposit_gross = 0
tax_hash_deposit_gross = Hash.new(0) # for summing up deposit gross prices grouped into vat percentage
tax_hash_deposit_net = Hash.new(0) # same here with gross prices
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_gross = separate_deposits ? goa.total_price_without_deposit : 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_gross)]
if separate_deposits && order_article.price.deposit > 0.0
goa_deposit = goa.result * order_article.price.deposit
goa_total_deposit = goa.result * order_article.price.fc_deposit_price
data << ["zzgl. Pfand",
goa.result.to_i,
number_to_currency(order_article.price.deposit),
number_to_currency(goa_deposit),
tax.to_s + '%',
number_to_currency(goa_total_deposit)]
total_deposit += goa_deposit
total_deposit_gross += goa_total_deposit
tax_hash_deposit_net[tax.to_i] += goa_deposit
tax_hash_deposit_gross[tax.to_i] += goa_total_deposit
end
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).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 = [[nil, nil, nil, "Netto", "MwSt", "Brutto"]]
[7, 19].each do |key|
sum << [nil, nil, "Produkte mit #{key}%", number_to_currency(tax_hash_net[key]), number_to_currency(tax_hash_gross[key] - tax_hash_net[key]), number_to_currency(tax_hash_gross[key])]
sum << [nil, 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]), number_to_currency(tax_hash_deposit_gross[key])] if separate_deposits
end
total_deposit_gross ||= 0
sum << [nil, nil, nil, nil, I18n.t('documents.group_order_invoice_pdf.sum_to_pay_gross'), number_to_currency(total_gross + total_deposit_gross)]
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..6).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..6).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..6).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

View file

@ -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

View file

@ -70,7 +70,7 @@ class RenderPdf < Prawn::Document
options[:skip_page_creation] = true options[:skip_page_creation] = true
@options = options @options = options
@first_page = true @first_page = true
no_footer = @options&.[](:no_footer) ? true : false
super(options) super(options)
# Use ttf for better utf-8 compability # Use ttf for better utf-8 compability
@ -84,11 +84,11 @@ class RenderPdf < Prawn::Document
) )
header = options[:title] || title 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 = 0
header_size = height_of(header, size: HEADER_FONT_SIZE, font: DEFAULT_FONT) + HEADER_SPACE if header 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) 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 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 text header, size: HEADER_FONT_SIZE, align: :center, overflow: :shrink_to_fit if header
end end
font_size FOOTER_FONT_SIZE do
bounding_box [bounds.left, bounds.bottom - FOOTER_SPACE], width: bounds.width, height: footer_size do unless no_footer
text footer, align: :left, valign: :bottom font_size FOOTER_FONT_SIZE do
end bounding_box [bounds.left, bounds.bottom - FOOTER_SPACE], width: bounds.width, height: footer_size do
bounding_box [bounds.left, bounds.bottom - FOOTER_SPACE], width: bounds.width, height: footer_size do text footer, align: :left, valign: :bottom
text I18n.t('lib.render_pdf.page', number: page_number, count: page_count), align: :right, 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
end end

View file

@ -51,6 +51,18 @@ class Mailer < ActionMailer::Base
subject: I18n.t('mailer.welcome.subject') subject: I18n.t('mailer.welcome.subject')
end 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 # Sends order result for specific Ordergroup
def order_result(user, group_order) def order_result(user, group_order)
@order = group_order.order @order = group_order.order
@ -169,6 +181,11 @@ class Mailer < ActionMailer::Base
attachments['order.csv'] = OrderCsv.new(order, options).to_csv attachments['order.csv'] = OrderCsv.new(order, options).to_csv
end 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 # separate method to allow plugins to mess with the text
def additonal_welcome_text(user); end def additonal_welcome_text(user); end

View file

@ -7,11 +7,27 @@ module PriceCalculation
add_percent(price + deposit, tax) add_percent(price + deposit, tax)
end end
def gross_price_without_deposit
add_percent(price, tax)
end
def gross_deposit_price
add_percent(deposit, tax)
end
# @return [Number] Price for the foodcoop-member. # @return [Number] Price for the foodcoop-member.
def fc_price def fc_price
add_percent(gross_price, FoodsoftConfig[:price_markup].to_i) add_percent(gross_price, FoodsoftConfig[:price_markup].to_i)
end end
def fc_price_without_deposit
add_percent(gross_price_without_deposit, FoodsoftConfig[:price_markup].to_i)
end
def fc_deposit_price
add_percent(gross_deposit_price, FoodsoftConfig[:price_markup].to_i)
end
private private
def add_percent(value, percent) def add_percent(value, percent)

View file

@ -9,6 +9,7 @@ class GroupOrder < ApplicationRecord
has_many :group_order_articles, dependent: :destroy has_many :group_order_articles, dependent: :destroy
has_many :order_articles, through: :group_order_articles has_many :order_articles, through: :group_order_articles
has_one :financial_transaction has_one :financial_transaction
has_one :group_order_invoice
belongs_to :updated_by, optional: true, class_name: 'User', foreign_key: 'updated_by_user_id' belongs_to :updated_by, optional: true, class_name: 'User', foreign_key: 'updated_by_user_id'
validates :order_id, presence: true validates :order_id, presence: true

View file

@ -208,6 +208,18 @@ class GroupOrderArticle < ApplicationRecord
end end
end end
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
# Check if the result deviates from the result_computed # Check if the result deviates from the result_computed
def result_manually_changed? def result_manually_changed?
result != result_computed unless result.nil? result != result_computed unless result.nil?

View file

@ -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

View file

@ -207,7 +207,7 @@ class Order < ApplicationRecord
# :fc, guess what... # :fc, guess what...
def sum(type = :gross) def sum(type = :gross)
total = 0 total = 0
if %i[net gross fc].include?(type) if %i[net gross gross_deposit fc_deposit deposit fc].include?(type)
for oa in order_articles.ordered.includes(:article, :article_price) for oa in order_articles.ordered.includes(:article, :article_price)
quantity = oa.units * oa.price.unit_quantity quantity = oa.units * oa.price.unit_quantity
case type case type
@ -217,6 +217,12 @@ class Order < ApplicationRecord
total += quantity * oa.price.gross_price total += quantity * oa.price.gross_price
when :fc when :fc
total += quantity * oa.price.fc_price total += quantity * oa.price.fc_price
when :gross_deposit
total += quantity * oa.price.gross_deposit_price
when :fc_deposit
total += quantity * oa.price.fc_deposit_price
when :deposit
total += quantity * oa.price.deposit
end end
end end
elsif %i[groups groups_without_markup].include?(type) elsif %i[groups groups_without_markup].include?(type)
@ -224,7 +230,11 @@ class Order < ApplicationRecord
for goa in go.group_order_articles for goa in go.group_order_articles
case type case type
when :groups when :groups
total += goa.result * goa.order_article.price.fc_price total += if FoodsoftConfig[:group_order_invoices]&.[](:separate_deposits)
goa.result * (goa.order_article.price.fc_price + goa.order_article.price.fc_deposit_price)
else
goa.result * goa.order_article.price.fc_price
end
when :groups_without_markup when :groups_without_markup
total += goa.result * goa.order_article.price.gross_price total += goa.result * goa.order_article.price.gross_price
end end

View file

@ -7,4 +7,5 @@
= config_input c, :country, as: :string, input_html: {class: 'input-xlarge'} = 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, :email, required: true, input_html: {class: 'input-xlarge'}
= config_input c, :phone, input_html: {class: 'input-medium'} = 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'} = config_input form, :homepage, required: true, as: :url, input_html: {class: 'input-xlarge'}

View file

@ -13,6 +13,12 @@
= config_input form, :charge_members_manually, as: :boolean = config_input form, :charge_members_manually, as: :boolean
= config_input form, :use_iban, as: :boolean = config_input form, :use_iban, as: :boolean
= config_input form, :use_self_service, 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, :separate_deposits, 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' %h4= t '.schedule_title'
= form.simple_fields_for :order_schedule do |fields| = form.simple_fields_for :order_schedule do |fields|

View file

@ -2,6 +2,7 @@
%p= t('.first_paragraph', url: link_to(t('.here'), new_invite_path(id: @ordergroup.id), remote: true)).html_safe %p= t('.first_paragraph', url: link_to(t('.here'), new_invite_path(id: @ordergroup.id), remote: true)).html_safe
= simple_form_for [:admin, @ordergroup] do |f| = simple_form_for [:admin, @ordergroup] do |f|
- captured = capture do - captured = capture do
= f.input :customer_number
= f.input :contact_person = f.input :contact_person
= f.input :contact_phone = f.input :contact_phone
= f.input :contact_address = f.input :contact_address

View file

@ -9,6 +9,8 @@
%th= t('.end') %th= t('.end')
%th= t('.state') %th= t('.state')
%th= heading_helper Order, :updated_by %th= heading_helper Order, :updated_by
%th= heading_helper GroupOrderInvoice, :name
%th
%th %th
%tbody %tbody
- @orders.each do |order| - @orders.each do |order|
@ -17,6 +19,14 @@
%td=h format_time(order.ends) unless order.ends.nil? %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= order.closed? ? t('.cleared', amount: number_to_currency(order.foodcoop_result)) : t('.ended')
%td= show_user(order.updated_by) %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 %td
- unless order.closed? - unless order.closed?
- if current_user.role_orders? - if current_user.role_orders?

View file

@ -12,6 +12,16 @@
%tr %tr
%td= t('.fc_amount') %td= t('.fc_amount')
%td.numeric= number_to_currency(order.sum(:fc)) %td.numeric= number_to_currency(order.sum(:fc))
- if FoodsoftConfig[:group_order_invoices]&.[](:separate_deposits)
%tr
%td= t('.deposit')
%td.numeric= number_to_currency(order.sum(:deposit))
%tr
%td= t('.gross_deposit')
%td.numeric= number_to_currency(order.sum(:gross_deposit))
%tr
%td= t('.fc_deposit')
%td.numeric= number_to_currency(order.sum(:fc_deposit))
%tr %tr
%td= t('.groups_amount') %td= t('.groups_amount')
%td.numeric= number_to_currency(order.sum(:groups)) %td.numeric= number_to_currency(order.sum(:groups))

View file

@ -1,5 +1,4 @@
- title t('.title') - title t('.title')
- content_for :actionbar do - content_for :actionbar do
- if FoodsoftConfig[:charge_members_manually] - 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' = link_to t('.close_all_direct_with_invoice'), close_all_direct_with_invoice_finance_order_index_path, method: :post, class: 'btn'

View file

@ -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'

View file

@ -0,0 +1 @@
$("#generate-invoice<%= params[:id] %>").html("<%= escape_javascript(render partial: 'links', locals: {order: @order}) %>");

View file

@ -0,0 +1 @@
$("#generate-invoice<%= @order.id %>").html("<%= escape_javascript(render partial: 'links', locals: {order: @order}) %>");

View file

@ -0,0 +1 @@
$("#generate-invoice<%= @order.id %>").html("<%= escape_javascript(render partial: 'links', locals: {order: @order}) %>");

View file

@ -69,7 +69,7 @@
= f.hidden_field :order_id = f.hidden_field :order_id
= f.hidden_field :updated_by_user_id = f.hidden_field :updated_by_user_id
= f.hidden_field :ordergroup_id = f.hidden_field :ordergroup_id
%table.table.table-hover %table.table
%thead %thead
%tr %tr
%th= heading_helper Article, :name %th= heading_helper Article, :name
@ -94,7 +94,7 @@
%i.icon-tag %i.icon-tag
%td{colspan: "9"} %td{colspan: "9"}
- order_articles.each do |order_article| - 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 %td.name= order_article.article.name
- if @order.stockit? - if @order.stockit?
%td= truncate order_article.article.supplier.name, length: 15 %td= truncate order_article.article.supplier.name, length: 15

View file

@ -0,0 +1 @@
= raw t '.text', group: @group.name, supplier: @supplier , foodcoop: FoodsoftConfig[:name]

View file

@ -57,6 +57,10 @@
= f.label :contact_person = f.label :contact_person
%br/ %br/
= f.text_field :contact_person = f.text_field :contact_person
%p
= f.label :customer_number
%br/
= f.text_field :customer_number
%p %p
= f.label :contact_phone = f.label :contact_phone
%br/ %br/

View file

@ -6,6 +6,8 @@
%dd=h group.contact %dd=h group.contact
%dt= heading_helper(Ordergroup, :contact_address) + ':' %dt= heading_helper(Ordergroup, :contact_address) + ':'
%dd= link_to_gmaps group.contact_address %dd= link_to_gmaps group.contact_address
%dt= heading_helper(Ordergroup, :customer_number) + ':'
%dd=h group.customer_number
- if group.break_start? or group.break_end? - if group.break_start? or group.break_end?
%dt= heading_helper(Ordergroup, :break) + ':' %dt= heading_helper(Ordergroup, :break) + ':'
%dd= raw t '.break', start: format_date(group.break_start), end: format_date(group.break_end) %dd= raw t '.break', start: format_date(group.break_start), end: format_date(group.break_end)

View file

@ -90,6 +90,17 @@ de:
tolerance: Toleranz tolerance: Toleranz
total_price: Summe total_price: Summe
unit_price: Preis/Einheit 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: invoice:
amount: Betrag amount: Betrag
attachment: Anhang attachment: Anhang
@ -158,6 +169,7 @@ de:
contact_address: Adresse contact_address: Adresse
contact_person: Kontaktperson contact_person: Kontaktperson
contact_phone: Telefon contact_phone: Telefon
customer_number: Kundennummer
description: Beschreibung description: Beschreibung
ignore_apple_restriction: Bestellstop bei zu wenig Äpfeln ignorieren ignore_apple_restriction: Bestellstop bei zu wenig Äpfeln ignorieren
last_order: Zuletzt bestellt last_order: Zuletzt bestellt
@ -318,6 +330,7 @@ de:
emails_title: E-Mails versenden emails_title: E-Mails versenden
tab_payment: tab_payment:
schedule_title: Bestellschema schedule_title: Bestellschema
group_order_invoices: Bestellgruppenrechnungen
tab_security: tab_security:
default_roles_title: Zugriff auf default_roles_title: Zugriff auf
default_roles_paragraph: Jedes Mitglied der Foodcoop hat automatisch Zugriff auf folgende Bereiche. default_roles_paragraph: Jedes Mitglied der Foodcoop hat automatisch Zugriff auf folgende Bereiche.
@ -602,6 +615,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_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_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. 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 help_url: Link zur Dokumentationsseite
homepage: Webseite der Foodcoop homepage: Webseite der Foodcoop
ignore_browser_locale: Ignoriere die Sprache des Computers des Anwenders, wenn der Anwender noch keine Sprache gewählt hat. ignore_browser_locale: Ignoriere die Sprache des Computers des Anwenders, wenn der Anwender noch keine Sprache gewählt hat.
@ -644,6 +661,7 @@ de:
phone: Telefon phone: Telefon
street: Straße street: Straße
zip_code: Postleitzahl zip_code: Postleitzahl
tax_number: Steuernummer
currency_space: Leerzeichen hinzufügen currency_space: Leerzeichen hinzufügen
currency_unit: Währung currency_unit: Währung
custom_css: Angepasstes CSS custom_css: Angepasstes CSS
@ -659,6 +677,11 @@ de:
email_from: Absenderadresse email_from: Absenderadresse
email_replyto: Antwortadresse email_replyto: Antwortadresse
email_sender: Senderadresse 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 help_url: URL Dokumentation
homepage: Webseite homepage: Webseite
ignore_browser_locale: Browsersprache ignorieren ignore_browser_locale: Browsersprache ignorieren
@ -744,6 +767,47 @@ de:
update: update:
notice: Lieferung wurde aktualisiert. notice: Lieferung wurde aktualisiert.
documents: 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: order_by_articles:
filename: Bestellung %{name}-%{date} - Artikelsortierung filename: Bestellung %{name}-%{date} - Artikelsortierung
title: 'Artikelsortierung der Bestellung: %{name}, beendet am %{date}' title: 'Artikelsortierung der Bestellung: %{name}, beendet am %{date}'
@ -767,6 +831,7 @@ de:
heading: Artikelübersicht (%{count}) heading: Artikelübersicht (%{count})
title: 'Sortiermatrix der Bestellung: %{name}, beendet am %{date}' title: 'Sortiermatrix der Bestellung: %{name}, beendet am %{date}'
errors: errors:
check_tax_number: Überprüft, ob die Steuernummer der Foodcoop richtig gesetzt ist
general: Ein Problem ist aufgetreten. general: Ein Problem ist aufgetreten.
general_again: Ein Fehler ist aufgetreten. Bitte erneut versuchen. general_again: Ein Fehler ist aufgetreten. Bitte erneut versuchen.
general_msg: 'Ein Fehler ist aufgetreten: %{msg}' general_msg: 'Ein Fehler ist aufgetreten: %{msg}'
@ -790,6 +855,8 @@ de:
close: close:
alert: 'Ein Fehler ist beim Abrechnen aufgetreten: %{message}' alert: 'Ein Fehler ist beim Abrechnen aufgetreten: %{message}'
notice: Bestellung wurde erfolgreich abgerechnet, die Kontostände aktualisiert. 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: close_all_direct_with_invoice:
notice: 'Es wurden %{count} Bestellung abgerechnet.' notice: 'Es wurden %{count} Bestellung abgerechnet.'
close_direct: close_direct:
@ -854,11 +921,15 @@ de:
ended: beendet ended: beendet
name: Lieferantin name: Lieferantin
no_closed_orders: Derzeit gibt es keine beendeten Bestellungen. no_closed_orders: Derzeit gibt es keine beendeten Bestellungen.
state: Status state: Status
summary: summary:
changed: Daten wurden verändert! changed: Daten wurden verändert!
duration: von %{starts} bis %{ends} duration: von %{starts} bis %{ends}
fc_amount: 'FC-Betrag:' fc_amount: 'FC-Betrag:'
deposit: 'Pfand netto:'
gross_deposit: 'Pfand brutto:'
fc_deposit: 'Pfand FC-Betrag:'
fc_profit: FC Gewinn fc_profit: FC Gewinn
gross_amount: 'Bruttobetrag:' gross_amount: 'Bruttobetrag:'
groups_amount: 'Gruppenbeträge:' groups_amount: 'Gruppenbeträge:'
@ -1268,6 +1339,15 @@ de:
feedback: feedback:
header: "%{user} schrieb am %{date}:" header: "%{user} schrieb am %{date}:"
subject: Feedback zur Foodsoft 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: invite:
subject: Einladung in die Foodcoop subject: Einladung in die Foodcoop
text: | text: |
@ -1500,6 +1580,7 @@ de:
orders_finished: Beendet orders_finished: Beendet
orders_open: Laufend orders_open: Laufend
orders_settled: Abgerechnet orders_settled: Abgerechnet
not_closed: Bestellung noch nicht abgerechnet
title: Bestellungen verwalten title: Bestellungen verwalten
model: model:
close_direct_message: Die Bestellung wurde abgechlossen, ohne die Mitgliederkonten zu belasten. close_direct_message: Die Bestellung wurde abgechlossen, ohne die Mitgliederkonten zu belasten.
@ -1883,3 +1964,4 @@ de:
time: time:
formats: formats:
foodsoft_datetime: "%d.%m.%Y %H:%M" foodsoft_datetime: "%d.%m.%Y %H:%M"
file: "%Y-%d-%B"

View file

@ -90,6 +90,18 @@ en:
tolerance: Tolerance tolerance: Tolerance
total_price: Sum total_price: Sum
unit_price: Price/Unit 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: invoice:
amount: Amount amount: Amount
attachment: Attachment attachment: Attachment
@ -158,6 +170,7 @@ en:
contact_address: Address contact_address: Address
contact_person: Contact person contact_person: Contact person
contact_phone: Phone contact_phone: Phone
customer_number: Customer number
description: Description description: Description
ignore_apple_restriction: Ignore order stop by apple points restriction ignore_apple_restriction: Ignore order stop by apple points restriction
last_order: Last order last_order: Last order
@ -318,6 +331,7 @@ en:
emails_title: Sending email emails_title: Sending email
tab_payment: tab_payment:
schedule_title: Ordering schedule schedule_title: Ordering schedule
group_order_invoices: Group order invoices
tab_security: tab_security:
default_roles_title: Access to default_roles_title: Access to
default_roles_paragraph: By default every member of the foodcoop has access to the following areas. default_roles_paragraph: By default every member of the foodcoop has access to the following areas.
@ -603,6 +617,9 @@ en:
email_from: Emails will appear to be from this email address. Leave empty to use the foodcoop's contact address. 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_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. 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. help_url: Documentation website.
homepage: Website of your foodcoop. homepage: Website of your foodcoop.
ignore_browser_locale: Ignore the language of user's computer when the user has not chosen a language yet. ignore_browser_locale: Ignore the language of user's computer when the user has not chosen a language yet.
@ -630,6 +647,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. 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. 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_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_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_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_nick: Show and use nicknames instead of real names. When enabling this, please check that each user has a nickname.
@ -645,6 +664,7 @@ en:
phone: Phone phone: Phone
street: Street street: Street
zip_code: Postcode zip_code: Postcode
tax_number: Tax number
currency_space: add space currency_space: add space
currency_unit: Currency currency_unit: Currency
custom_css: Custom CSS custom_css: Custom CSS
@ -689,6 +709,11 @@ en:
first_order_first_serve: First distribute to those who ordered first first_order_first_serve: First distribute to those who ordered first
no_automatic_distribution: No automatic distribution no_automatic_distribution: No automatic distribution
use_apple_points: Apple points 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_boxfill: Box-fill phase
use_iban: Use IBAN use_iban: Use IBAN
use_nick: Use nicknames use_nick: Use nicknames
@ -746,6 +771,47 @@ en:
update: update:
notice: Delivery was updated. notice: Delivery was updated.
documents: 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: order_by_articles:
filename: Order %{name}-%{date} - by articles filename: Order %{name}-%{date} - by articles
title: 'Order sorted by articles: %{name}, closed at %{date}' title: 'Order sorted by articles: %{name}, closed at %{date}'
@ -769,6 +835,7 @@ en:
heading: Article overview (%{count}) heading: Article overview (%{count})
title: 'Order sorting matrix: %{name}, closed at %{date}' title: 'Order sorting matrix: %{name}, closed at %{date}'
errors: errors:
check_tax_number: Please check whether the foodcoop's tax number is set correctly.
general: A problem has occured. general: A problem has occured.
general_again: A problem has occured. Please try again. general_again: A problem has occured. Please try again.
general_msg: 'A problem has occured: %{msg}' general_msg: 'A problem has occured: %{msg}'
@ -792,6 +859,7 @@ en:
close: close:
alert: 'An error occured while accounting: %{message}' alert: 'An error occured while accounting: %{message}'
notice: Order was settled succesfully, the balance of the account was updated. 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: close_all_direct_with_invoice:
notice: '%{count} orders have been settled.' notice: '%{count} orders have been settled.'
close_direct: close_direct:
@ -1272,6 +1340,15 @@ en:
feedback: feedback:
header: "%{user} wrote at %{date}:" header: "%{user} wrote at %{date}:"
subject: Feedback for Foodsoft 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" from_via_foodsoft: "%{name} via Foodsoft"
invite: invite:
subject: Invitation to the Foodcoop subject: Invitation to the Foodcoop
@ -1511,6 +1588,7 @@ en:
orders_finished: Closed orders_finished: Closed
orders_open: Open orders_open: Open
orders_settled: Settled orders_settled: Settled
not_closed: Order not yet settled
title: Manage orders title: Manage orders
model: model:
close_direct_message: Order settled without charging member accounts. close_direct_message: Order settled without charging member accounts.
@ -1910,3 +1988,4 @@ en:
time: time:
formats: formats:
foodsoft_datetime: "%Y-%m-%d %H:%M" foodsoft_datetime: "%Y-%m-%d %H:%M"
file: "%Y-%d-%B"

View file

@ -143,6 +143,13 @@ Rails.application.routes.draw do
end end
end end
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
resources :article_categories resources :article_categories
########### Finance ########### Finance

View file

@ -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

View file

@ -0,0 +1,5 @@
class AddCustomerNumberToGroup < ActiveRecord::Migration[7.0]
def change
add_column :groups, :customer_number, :string, unique: true
end
end

View file

@ -10,8 +10,8 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.0].define(version: 2023_02_15_085312) do ActiveRecord::Schema[7.0].define(version: 2023_08_22_120005) do
create_table "action_text_rich_texts", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| create_table "action_text_rich_texts", charset: "utf8mb4", force: :cascade do |t|
t.string "name", null: false t.string "name", null: false
t.text "body", size: :long t.text "body", size: :long
t.string "record_type", null: false t.string "record_type", null: false
@ -21,7 +21,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_02_15_085312) do
t.index ["record_type", "record_id", "name"], name: "index_action_text_rich_texts_uniqueness", unique: true t.index ["record_type", "record_id", "name"], name: "index_action_text_rich_texts_uniqueness", unique: true
end end
create_table "active_storage_attachments", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| create_table "active_storage_attachments", id: :integer, charset: "utf8mb4", force: :cascade do |t|
t.string "name", null: false t.string "name", null: false
t.string "record_type", null: false t.string "record_type", null: false
t.bigint "record_id", null: false t.bigint "record_id", null: false
@ -31,7 +31,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_02_15_085312) do
t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true
end end
create_table "active_storage_blobs", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| create_table "active_storage_blobs", id: :integer, charset: "utf8mb4", force: :cascade do |t|
t.string "key", null: false t.string "key", null: false
t.string "filename", null: false t.string "filename", null: false
t.string "content_type" t.string "content_type"
@ -43,19 +43,19 @@ ActiveRecord::Schema[7.0].define(version: 2023_02_15_085312) do
t.index ["key"], name: "index_active_storage_blobs_on_key", unique: true t.index ["key"], name: "index_active_storage_blobs_on_key", unique: true
end end
create_table "active_storage_variant_records", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| create_table "active_storage_variant_records", charset: "utf8mb4", force: :cascade do |t|
t.integer "blob_id", null: false t.integer "blob_id", null: false
t.string "variation_digest", null: false t.string "variation_digest", null: false
t.index ["blob_id", "variation_digest"], name: "index_active_storage_variant_records_uniqueness", unique: true t.index ["blob_id", "variation_digest"], name: "index_active_storage_variant_records_uniqueness", unique: true
end end
create_table "article_categories", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| create_table "article_categories", id: :integer, charset: "utf8mb4", force: :cascade do |t|
t.string "name", default: "", null: false t.string "name", default: "", null: false
t.string "description" t.string "description"
t.index ["name"], name: "index_article_categories_on_name", unique: true t.index ["name"], name: "index_article_categories_on_name", unique: true
end end
create_table "article_prices", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| create_table "article_prices", id: :integer, charset: "utf8mb4", force: :cascade do |t|
t.integer "article_id", null: false t.integer "article_id", null: false
t.decimal "price", precision: 8, scale: 2, default: "0.0", null: false t.decimal "price", precision: 8, scale: 2, default: "0.0", null: false
t.decimal "tax", precision: 8, scale: 2, default: "0.0", null: false t.decimal "tax", precision: 8, scale: 2, default: "0.0", null: false
@ -65,7 +65,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_02_15_085312) do
t.index ["article_id"], name: "index_article_prices_on_article_id" t.index ["article_id"], name: "index_article_prices_on_article_id"
end end
create_table "articles", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| create_table "articles", id: :integer, charset: "utf8mb4", force: :cascade do |t|
t.string "name", default: "", null: false t.string "name", default: "", null: false
t.integer "supplier_id", default: 0, null: false t.integer "supplier_id", default: 0, null: false
t.integer "article_category_id", default: 0, null: false t.integer "article_category_id", default: 0, null: false
@ -91,14 +91,14 @@ ActiveRecord::Schema[7.0].define(version: 2023_02_15_085312) do
t.index ["type"], name: "index_articles_on_type" t.index ["type"], name: "index_articles_on_type"
end end
create_table "assignments", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| create_table "assignments", id: :integer, charset: "utf8mb4", force: :cascade do |t|
t.integer "user_id", default: 0, null: false t.integer "user_id", default: 0, null: false
t.integer "task_id", default: 0, null: false t.integer "task_id", default: 0, null: false
t.boolean "accepted", default: false t.boolean "accepted", default: false
t.index ["user_id", "task_id"], name: "index_assignments_on_user_id_and_task_id", unique: true t.index ["user_id", "task_id"], name: "index_assignments_on_user_id_and_task_id", unique: true
end end
create_table "bank_accounts", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| create_table "bank_accounts", id: :integer, charset: "utf8mb4", force: :cascade do |t|
t.string "name", null: false t.string "name", null: false
t.string "iban" t.string "iban"
t.string "description" t.string "description"
@ -108,14 +108,14 @@ ActiveRecord::Schema[7.0].define(version: 2023_02_15_085312) do
t.integer "bank_gateway_id" t.integer "bank_gateway_id"
end end
create_table "bank_gateways", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| create_table "bank_gateways", id: :integer, charset: "utf8mb4", force: :cascade do |t|
t.string "name", null: false t.string "name", null: false
t.string "url", null: false t.string "url", null: false
t.string "authorization" t.string "authorization"
t.integer "unattended_user_id" t.integer "unattended_user_id"
end end
create_table "bank_transactions", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| create_table "bank_transactions", id: :integer, charset: "utf8mb4", force: :cascade do |t|
t.integer "bank_account_id", null: false t.integer "bank_account_id", null: false
t.string "external_id" t.string "external_id"
t.date "date" t.date "date"
@ -129,7 +129,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_02_15_085312) do
t.index ["financial_link_id"], name: "index_bank_transactions_on_financial_link_id" t.index ["financial_link_id"], name: "index_bank_transactions_on_financial_link_id"
end end
create_table "documents", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| create_table "documents", id: :integer, charset: "utf8mb4", force: :cascade do |t|
t.string "name" t.string "name"
t.string "mime" t.string "mime"
t.binary "data", size: :long t.binary "data", size: :long
@ -140,16 +140,16 @@ ActiveRecord::Schema[7.0].define(version: 2023_02_15_085312) do
t.index ["parent_id"], name: "index_documents_on_parent_id" t.index ["parent_id"], name: "index_documents_on_parent_id"
end end
create_table "financial_links", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| create_table "financial_links", id: :integer, charset: "utf8mb4", force: :cascade do |t|
t.text "note" t.text "note"
end end
create_table "financial_transaction_classes", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| create_table "financial_transaction_classes", id: :integer, charset: "utf8mb4", force: :cascade do |t|
t.string "name", null: false t.string "name", null: false
t.boolean "ignore_for_account_balance", default: false, null: false t.boolean "ignore_for_account_balance", default: false, null: false
end end
create_table "financial_transaction_types", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| create_table "financial_transaction_types", id: :integer, charset: "utf8mb4", force: :cascade do |t|
t.string "name", null: false t.string "name", null: false
t.integer "financial_transaction_class_id", null: false t.integer "financial_transaction_class_id", null: false
t.string "name_short" t.string "name_short"
@ -157,7 +157,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_02_15_085312) do
t.index ["name_short"], name: "index_financial_transaction_types_on_name_short" t.index ["name_short"], name: "index_financial_transaction_types_on_name_short"
end end
create_table "financial_transactions", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| create_table "financial_transactions", id: :integer, charset: "utf8mb4", force: :cascade do |t|
t.integer "ordergroup_id" t.integer "ordergroup_id"
t.decimal "amount", precision: 8, scale: 2, default: "0.0", null: false t.decimal "amount", precision: 8, scale: 2, default: "0.0", null: false
t.text "note", null: false t.text "note", null: false
@ -171,7 +171,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_02_15_085312) do
t.index ["reverts_id"], name: "index_financial_transactions_on_reverts_id", unique: true t.index ["reverts_id"], name: "index_financial_transactions_on_reverts_id", unique: true
end end
create_table "group_order_article_quantities", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| create_table "group_order_article_quantities", id: :integer, charset: "utf8mb4", force: :cascade do |t|
t.integer "group_order_article_id", default: 0, null: false t.integer "group_order_article_id", default: 0, null: false
t.integer "quantity", default: 0 t.integer "quantity", default: 0
t.integer "tolerance", default: 0 t.integer "tolerance", default: 0
@ -179,7 +179,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_02_15_085312) do
t.index ["group_order_article_id"], name: "index_group_order_article_quantities_on_group_order_article_id" t.index ["group_order_article_id"], name: "index_group_order_article_quantities_on_group_order_article_id"
end end
create_table "group_order_articles", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| create_table "group_order_articles", id: :integer, charset: "utf8mb4", force: :cascade do |t|
t.integer "group_order_id", default: 0, null: false t.integer "group_order_id", default: 0, null: false
t.integer "order_article_id", default: 0, null: false t.integer "order_article_id", default: 0, null: false
t.integer "quantity", default: 0, null: false t.integer "quantity", default: 0, null: false
@ -192,7 +192,17 @@ ActiveRecord::Schema[7.0].define(version: 2023_02_15_085312) do
t.index ["order_article_id"], name: "index_group_order_articles_on_order_article_id" t.index ["order_article_id"], name: "index_group_order_articles_on_order_article_id"
end end
create_table "group_orders", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| create_table "group_order_invoices", charset: "utf8mb4", 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", force: :cascade do |t|
t.integer "ordergroup_id" t.integer "ordergroup_id"
t.integer "order_id", default: 0, null: false t.integer "order_id", default: 0, null: false
t.decimal "price", precision: 8, scale: 2, default: "0.0", null: false t.decimal "price", precision: 8, scale: 2, default: "0.0", null: false
@ -205,7 +215,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_02_15_085312) do
t.index ["ordergroup_id"], name: "index_group_orders_on_ordergroup_id" t.index ["ordergroup_id"], name: "index_group_orders_on_ordergroup_id"
end end
create_table "groups", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| create_table "groups", id: :integer, charset: "utf8mb4", force: :cascade do |t|
t.string "type", default: "", null: false t.string "type", default: "", null: false
t.string "name", default: "", null: false t.string "name", default: "", null: false
t.string "description" t.string "description"
@ -227,10 +237,11 @@ ActiveRecord::Schema[7.0].define(version: 2023_02_15_085312) do
t.date "break_end" t.date "break_end"
t.boolean "role_invoices", default: false, null: false t.boolean "role_invoices", default: false, null: false
t.boolean "role_pickups", default: false, null: false t.boolean "role_pickups", default: false, null: false
t.string "customer_number"
t.index ["name"], name: "index_groups_on_name", unique: true t.index ["name"], name: "index_groups_on_name", unique: true
end end
create_table "invites", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| create_table "invites", id: :integer, charset: "utf8mb4", force: :cascade do |t|
t.string "token", default: "", null: false t.string "token", default: "", null: false
t.datetime "expires_at", precision: nil, null: false t.datetime "expires_at", precision: nil, null: false
t.integer "group_id", default: 0, null: false t.integer "group_id", default: 0, null: false
@ -239,7 +250,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_02_15_085312) do
t.index ["token"], name: "index_invites_on_token" t.index ["token"], name: "index_invites_on_token"
end end
create_table "invoices", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| create_table "invoices", id: :integer, charset: "utf8mb4", force: :cascade do |t|
t.integer "supplier_id" t.integer "supplier_id"
t.string "number" t.string "number"
t.date "date" t.date "date"
@ -257,7 +268,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_02_15_085312) do
t.index ["supplier_id"], name: "index_invoices_on_supplier_id" t.index ["supplier_id"], name: "index_invoices_on_supplier_id"
end end
create_table "links", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| create_table "links", id: :integer, charset: "utf8mb4", force: :cascade do |t|
t.string "name", null: false t.string "name", null: false
t.string "url", null: false t.string "url", null: false
t.integer "workgroup_id" t.integer "workgroup_id"
@ -265,7 +276,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_02_15_085312) do
t.string "authorization" t.string "authorization"
end end
create_table "mail_delivery_status", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| create_table "mail_delivery_status", id: :integer, charset: "utf8mb4", force: :cascade do |t|
t.datetime "created_at", precision: nil t.datetime "created_at", precision: nil
t.string "email", null: false t.string "email", null: false
t.string "message", null: false t.string "message", null: false
@ -274,13 +285,13 @@ ActiveRecord::Schema[7.0].define(version: 2023_02_15_085312) do
t.index ["email"], name: "index_mail_delivery_status_on_email" t.index ["email"], name: "index_mail_delivery_status_on_email"
end end
create_table "memberships", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| create_table "memberships", id: :integer, charset: "utf8mb4", force: :cascade do |t|
t.integer "group_id", default: 0, null: false t.integer "group_id", default: 0, null: false
t.integer "user_id", default: 0, null: false t.integer "user_id", default: 0, null: false
t.index ["user_id", "group_id"], name: "index_memberships_on_user_id_and_group_id", unique: true t.index ["user_id", "group_id"], name: "index_memberships_on_user_id_and_group_id", unique: true
end end
create_table "message_recipients", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| create_table "message_recipients", id: :integer, charset: "utf8mb4", force: :cascade do |t|
t.integer "message_id", null: false t.integer "message_id", null: false
t.integer "user_id", null: false t.integer "user_id", null: false
t.integer "email_state", default: 0, null: false t.integer "email_state", default: 0, null: false
@ -289,7 +300,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_02_15_085312) do
t.index ["user_id", "read_at"], name: "index_message_recipients_on_user_id_and_read_at" t.index ["user_id", "read_at"], name: "index_message_recipients_on_user_id_and_read_at"
end end
create_table "messages", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| create_table "messages", id: :integer, charset: "utf8mb4", force: :cascade do |t|
t.integer "sender_id" t.integer "sender_id"
t.string "subject", null: false t.string "subject", null: false
t.boolean "private", default: false t.boolean "private", default: false
@ -300,7 +311,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_02_15_085312) do
t.binary "received_email", size: :medium t.binary "received_email", size: :medium
end end
create_table "oauth_access_grants", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| create_table "oauth_access_grants", id: :integer, charset: "utf8mb4", force: :cascade do |t|
t.integer "resource_owner_id", null: false t.integer "resource_owner_id", null: false
t.integer "application_id", null: false t.integer "application_id", null: false
t.string "token", null: false t.string "token", null: false
@ -312,7 +323,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_02_15_085312) do
t.index ["token"], name: "index_oauth_access_grants_on_token", unique: true t.index ["token"], name: "index_oauth_access_grants_on_token", unique: true
end end
create_table "oauth_access_tokens", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| create_table "oauth_access_tokens", id: :integer, charset: "utf8mb4", force: :cascade do |t|
t.integer "resource_owner_id" t.integer "resource_owner_id"
t.integer "application_id" t.integer "application_id"
t.string "token", null: false t.string "token", null: false
@ -326,7 +337,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_02_15_085312) do
t.index ["token"], name: "index_oauth_access_tokens_on_token", unique: true t.index ["token"], name: "index_oauth_access_tokens_on_token", unique: true
end end
create_table "oauth_applications", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| create_table "oauth_applications", id: :integer, charset: "utf8mb4", force: :cascade do |t|
t.string "name", null: false t.string "name", null: false
t.string "uid", null: false t.string "uid", null: false
t.string "secret", null: false t.string "secret", null: false
@ -338,7 +349,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_02_15_085312) do
t.index ["uid"], name: "index_oauth_applications_on_uid", unique: true t.index ["uid"], name: "index_oauth_applications_on_uid", unique: true
end end
create_table "order_articles", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| create_table "order_articles", id: :integer, charset: "utf8mb4", force: :cascade do |t|
t.integer "order_id", default: 0, null: false t.integer "order_id", default: 0, null: false
t.integer "article_id", default: 0, null: false t.integer "article_id", default: 0, null: false
t.integer "quantity", default: 0, null: false t.integer "quantity", default: 0, null: false
@ -352,7 +363,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_02_15_085312) do
t.index ["order_id"], name: "index_order_articles_on_order_id" t.index ["order_id"], name: "index_order_articles_on_order_id"
end end
create_table "order_comments", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| create_table "order_comments", id: :integer, charset: "utf8mb4", force: :cascade do |t|
t.integer "order_id" t.integer "order_id"
t.integer "user_id" t.integer "user_id"
t.text "text" t.text "text"
@ -360,7 +371,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_02_15_085312) do
t.index ["order_id"], name: "index_order_comments_on_order_id" t.index ["order_id"], name: "index_order_comments_on_order_id"
end end
create_table "orders", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| create_table "orders", id: :integer, charset: "utf8mb4", force: :cascade do |t|
t.integer "supplier_id" t.integer "supplier_id"
t.text "note" t.text "note"
t.datetime "starts", precision: nil t.datetime "starts", precision: nil
@ -379,7 +390,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_02_15_085312) do
t.index ["state"], name: "index_orders_on_state" t.index ["state"], name: "index_orders_on_state"
end end
create_table "page_versions", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| create_table "page_versions", id: :integer, charset: "utf8mb4", force: :cascade do |t|
t.integer "page_id" t.integer "page_id"
t.integer "lock_version" t.integer "lock_version"
t.text "body" t.text "body"
@ -390,7 +401,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_02_15_085312) do
t.index ["page_id"], name: "index_page_versions_on_page_id" t.index ["page_id"], name: "index_page_versions_on_page_id"
end end
create_table "pages", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| create_table "pages", id: :integer, charset: "utf8mb4", force: :cascade do |t|
t.string "title" t.string "title"
t.text "body" t.text "body"
t.string "permalink" t.string "permalink"
@ -404,20 +415,20 @@ ActiveRecord::Schema[7.0].define(version: 2023_02_15_085312) do
t.index ["title"], name: "index_pages_on_title" t.index ["title"], name: "index_pages_on_title"
end end
create_table "periodic_task_groups", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| create_table "periodic_task_groups", id: :integer, charset: "utf8mb4", force: :cascade do |t|
t.date "next_task_date" t.date "next_task_date"
t.datetime "created_at", precision: nil, null: false t.datetime "created_at", precision: nil, null: false
t.datetime "updated_at", precision: nil, null: false t.datetime "updated_at", precision: nil, null: false
end end
create_table "poll_choices", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| create_table "poll_choices", id: :integer, charset: "utf8mb4", force: :cascade do |t|
t.integer "poll_vote_id", null: false t.integer "poll_vote_id", null: false
t.integer "choice", null: false t.integer "choice", null: false
t.integer "value", null: false t.integer "value", null: false
t.index ["poll_vote_id", "choice"], name: "index_poll_choices_on_poll_vote_id_and_choice", unique: true t.index ["poll_vote_id", "choice"], name: "index_poll_choices_on_poll_vote_id_and_choice", unique: true
end end
create_table "poll_votes", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| create_table "poll_votes", id: :integer, charset: "utf8mb4", force: :cascade do |t|
t.integer "poll_id", null: false t.integer "poll_id", null: false
t.integer "user_id", null: false t.integer "user_id", null: false
t.integer "ordergroup_id" t.integer "ordergroup_id"
@ -427,7 +438,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_02_15_085312) do
t.index ["poll_id", "user_id", "ordergroup_id"], name: "index_poll_votes_on_poll_id_and_user_id_and_ordergroup_id", unique: true t.index ["poll_id", "user_id", "ordergroup_id"], name: "index_poll_votes_on_poll_id_and_user_id_and_ordergroup_id", unique: true
end end
create_table "polls", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| create_table "polls", id: :integer, charset: "utf8mb4", force: :cascade do |t|
t.integer "created_by_user_id", null: false t.integer "created_by_user_id", null: false
t.string "name", null: false t.string "name", null: false
t.text "description" t.text "description"
@ -447,7 +458,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_02_15_085312) do
t.index ["final_choice"], name: "index_polls_on_final_choice" t.index ["final_choice"], name: "index_polls_on_final_choice"
end end
create_table "printer_job_updates", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| create_table "printer_job_updates", id: :integer, charset: "utf8mb4", force: :cascade do |t|
t.integer "printer_job_id", null: false t.integer "printer_job_id", null: false
t.datetime "created_at", precision: nil, null: false t.datetime "created_at", precision: nil, null: false
t.string "state", null: false t.string "state", null: false
@ -455,7 +466,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_02_15_085312) do
t.index ["printer_job_id", "created_at"], name: "index_printer_job_updates_on_printer_job_id_and_created_at" t.index ["printer_job_id", "created_at"], name: "index_printer_job_updates_on_printer_job_id_and_created_at"
end end
create_table "printer_jobs", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| create_table "printer_jobs", id: :integer, charset: "utf8mb4", force: :cascade do |t|
t.integer "order_id" t.integer "order_id"
t.string "document", null: false t.string "document", null: false
t.integer "created_by_user_id", null: false t.integer "created_by_user_id", null: false
@ -464,7 +475,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_02_15_085312) do
t.index ["finished_at"], name: "index_printer_jobs_on_finished_at" t.index ["finished_at"], name: "index_printer_jobs_on_finished_at"
end end
create_table "settings", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| create_table "settings", id: :integer, charset: "utf8mb4", force: :cascade do |t|
t.string "var", null: false t.string "var", null: false
t.text "value" t.text "value"
t.integer "thing_id" t.integer "thing_id"
@ -474,7 +485,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_02_15_085312) do
t.index ["thing_type", "thing_id", "var"], name: "index_settings_on_thing_type_and_thing_id_and_var", unique: true t.index ["thing_type", "thing_id", "var"], name: "index_settings_on_thing_type_and_thing_id_and_var", unique: true
end end
create_table "stock_changes", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| create_table "stock_changes", id: :integer, charset: "utf8mb4", force: :cascade do |t|
t.integer "stock_event_id" t.integer "stock_event_id"
t.integer "order_id" t.integer "order_id"
t.integer "stock_article_id" t.integer "stock_article_id"
@ -484,7 +495,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_02_15_085312) do
t.index ["stock_event_id"], name: "index_stock_changes_on_stock_event_id" t.index ["stock_event_id"], name: "index_stock_changes_on_stock_event_id"
end end
create_table "stock_events", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| create_table "stock_events", id: :integer, charset: "utf8mb4", force: :cascade do |t|
t.integer "supplier_id" t.integer "supplier_id"
t.date "date" t.date "date"
t.datetime "created_at", precision: nil t.datetime "created_at", precision: nil
@ -494,14 +505,14 @@ ActiveRecord::Schema[7.0].define(version: 2023_02_15_085312) do
t.index ["supplier_id"], name: "index_stock_events_on_supplier_id" t.index ["supplier_id"], name: "index_stock_events_on_supplier_id"
end end
create_table "supplier_categories", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| create_table "supplier_categories", id: :integer, charset: "utf8mb4", force: :cascade do |t|
t.string "name", null: false t.string "name", null: false
t.string "description" t.string "description"
t.integer "financial_transaction_class_id" t.integer "financial_transaction_class_id"
t.integer "bank_account_id" t.integer "bank_account_id"
end end
create_table "suppliers", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| create_table "suppliers", id: :integer, charset: "utf8mb4", force: :cascade do |t|
t.string "name", default: "", null: false t.string "name", default: "", null: false
t.string "address", default: "", null: false t.string "address", default: "", null: false
t.string "phone", default: "", null: false t.string "phone", default: "", null: false
@ -523,7 +534,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_02_15_085312) do
t.index ["name"], name: "index_suppliers_on_name", unique: true t.index ["name"], name: "index_suppliers_on_name", unique: true
end end
create_table "tasks", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| create_table "tasks", id: :integer, charset: "utf8mb4", force: :cascade do |t|
t.string "name", default: "", null: false t.string "name", default: "", null: false
t.text "description" t.text "description"
t.date "due_date" t.date "due_date"
@ -540,7 +551,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_02_15_085312) do
t.index ["workgroup_id"], name: "index_tasks_on_workgroup_id" t.index ["workgroup_id"], name: "index_tasks_on_workgroup_id"
end end
create_table "users", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| create_table "users", id: :integer, charset: "utf8mb4", force: :cascade do |t|
t.string "nick" t.string "nick"
t.string "password_hash", default: "", null: false t.string "password_hash", default: "", null: false
t.string "password_salt", default: "", null: false t.string "password_salt", default: "", null: false

View file

@ -0,0 +1,7 @@
require 'factory_bot'
FactoryBot.define do
factory :group_order_invoice do
group_order { create :group_order }
end
end

View file

@ -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

View file

@ -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