wip move automatic invoices to plugin
This commit is contained in:
parent
42a1773a87
commit
78bf494182
33 changed files with 7197 additions and 0 deletions
42
plugins/automatic_invoices/README.md
Normal file
42
plugins/automatic_invoices/README.md
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
FoodsoftAutomaticInvoices
|
||||||
|
=====================
|
||||||
|
|
||||||
|
Foodsoft is currently designed to work with one order at a time. In practice,
|
||||||
|
however there can be multiple orders open at the same time, with one pickup
|
||||||
|
day. The proper solution to this is to introduce the notion of order cycles,
|
||||||
|
with each order belonging to a cycle. Until that time, we have this plugin,
|
||||||
|
with screens for working on all orders that are closed-but-not-finished.
|
||||||
|
|
||||||
|
Important: be sure to settle orders from the previous order cycle, before
|
||||||
|
you close any. If you don't, articles from previous and current dates start
|
||||||
|
to mix up (if you do, settle the old ones asap).
|
||||||
|
|
||||||
|
* `current_orders/orders/receive` for a list of orders that can be received.
|
||||||
|
* `current_orders/orders.pdf?document=(groups|articles)` for PDFs for all
|
||||||
|
orders that are closed but not settled.
|
||||||
|
* `current_orders/articles` to edit an order article's ordergroups in all
|
||||||
|
orders that are closed but not settled.
|
||||||
|
* `current_orders/ordergroups` to edit an ordergroup's order articles in all
|
||||||
|
orders that are closed but not settled.
|
||||||
|
* `current_orders/group_orders` for all articles in the user's group orders
|
||||||
|
from orders that are not settled. Can be used as a "shopping-cart overview"
|
||||||
|
or "checkout" page.
|
||||||
|
|
||||||
|
New menu items will be added in the "Orders" menu. Please note that members
|
||||||
|
with _Orders_ permission will now be able to edit the amounts members received
|
||||||
|
in some of these screens, something that was previously restricted to the
|
||||||
|
_Finance_ permission.
|
||||||
|
|
||||||
|
This plugin is not enabled by default. To install it, add uncomment the
|
||||||
|
corresponding line in the `Gemfile`, or add:
|
||||||
|
|
||||||
|
```Gemfile
|
||||||
|
gem 'foodsoft_current_orders', path: 'plugins/current_orders'
|
||||||
|
```
|
||||||
|
|
||||||
|
This plugin introduces the foodcoop config option `use_current_orders`, which
|
||||||
|
needs to be set to `true` to enable the plugin. This can be done in the
|
||||||
|
configuration screen or `config/app_config.yml`.
|
||||||
|
|
||||||
|
This plugin is part of the foodsoft package and uses the AGPL-3 license (see
|
||||||
|
foodsoft's LICENSE for the full license text).
|
|
@ -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
|
|
@ -0,0 +1,85 @@
|
||||||
|
class GroupOrderInvoicesController < ApplicationController
|
||||||
|
include Concerns::SendOrderPdf
|
||||||
|
before_action :authenticate_finance
|
||||||
|
|
||||||
|
def show
|
||||||
|
@group_order_invoice = GroupOrderInvoice.find(params[:id])
|
||||||
|
raise RecordInvalid unless FoodsoftConfig[:contact][:tax_number]
|
||||||
|
|
||||||
|
respond_to do |format|
|
||||||
|
format.pdf do
|
||||||
|
send_group_order_invoice_pdf @group_order_invoice if FoodsoftConfig[:contact][:tax_number]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
rescue ActiveRecord::RecordInvalid => e
|
||||||
|
redirect_back fallback_location: root_path, notice: 'Something went wrong', alert: I18n.t('errors.general_msg', msg: "#{e} " + I18n.t('errors.check_tax_number'))
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
go = GroupOrder.find(params[:group_order])
|
||||||
|
@order = go.order
|
||||||
|
GroupOrderInvoice.find_or_create_by!(group_order_id: go.id)
|
||||||
|
respond_to do |format|
|
||||||
|
format.js
|
||||||
|
end
|
||||||
|
redirect_back fallback_location: root_path
|
||||||
|
rescue StandardError => e
|
||||||
|
redirect_back fallback_location: root_path, notice: 'Something went wrong', :alert => I18n.t('errors.general_msg', :msg => e)
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
goi = GroupOrderInvoice.find(params[:id])
|
||||||
|
@order = goi.group_order.order
|
||||||
|
goi.destroy
|
||||||
|
respond_to do |format|
|
||||||
|
format.js
|
||||||
|
format.json { head :no_content }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_multiple
|
||||||
|
invoice_date = params[:group_order_invoice][:invoice_date]
|
||||||
|
order_id = params[:group_order_invoice][:order_id]
|
||||||
|
@order = Order.find(order_id)
|
||||||
|
gos = GroupOrder.where("order_id = ?", order_id)
|
||||||
|
gos.each do |go|
|
||||||
|
goi = GroupOrderInvoice.find_or_create_by!(group_order_id: go.id)
|
||||||
|
goi.invoice_date = invoice_date
|
||||||
|
goi.invoice_number = goi.generate_invoice_number(1)
|
||||||
|
goi.save!
|
||||||
|
end
|
||||||
|
respond_to do |format|
|
||||||
|
format.js
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def download_all
|
||||||
|
order = Order.find(params[:order_id])
|
||||||
|
|
||||||
|
invoices = order.group_orders.map(&:group_order_invoice)
|
||||||
|
pdf = {}
|
||||||
|
file_paths = []
|
||||||
|
temp_file = Tempfile.new("all_invoices_for_order_#{order.id}.zip")
|
||||||
|
Zip::File.open(temp_file.path, Zip::File::CREATE) do |zipfile|
|
||||||
|
invoices.each do |invoice|
|
||||||
|
pdf = create_invoice_pdf(invoice)
|
||||||
|
file_path = File.join("tmp", pdf.filename)
|
||||||
|
File.open(file_path, 'w:ASCII-8BIT') do |file|
|
||||||
|
file.write(pdf.to_pdf)
|
||||||
|
end
|
||||||
|
file_paths << file_path
|
||||||
|
zipfile.add(pdf.filename, file_path) unless zipfile.find_entry(pdf.filename)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
zip_data = File.read(temp_file.path)
|
||||||
|
file_paths.each do |file_path|
|
||||||
|
File.delete(file_path)
|
||||||
|
end
|
||||||
|
respond_to do |format|
|
||||||
|
format.html do
|
||||||
|
send_data(zip_data, type: 'application/zip', filename: "#{l order.ends, format: :file}-#{order.supplier.name}-#{order.id}.zip", disposition: 'attachment')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
File diff suppressed because it is too large
Load diff
|
@ -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.net_deposit_price
|
||||||
|
goa_total_deposit = goa.result * order_article.price.fc_deposit_price
|
||||||
|
|
||||||
|
data << ["zzgl. Pfand",
|
||||||
|
goa.result.to_i,
|
||||||
|
number_to_currency(order_article.price.net_deposit_price),
|
||||||
|
number_to_currency(goa_deposit),
|
||||||
|
tax.to_s + '%',
|
||||||
|
number_to_currency(goa_total_deposit)]
|
||||||
|
|
||||||
|
total_deposit += goa_deposit
|
||||||
|
total_deposit_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
|
|
@ -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
|
170
plugins/automatic_invoices/app/lib/render_pdf.rb
Normal file
170
plugins/automatic_invoices/app/lib/render_pdf.rb
Normal file
|
@ -0,0 +1,170 @@
|
||||||
|
require 'prawn/measurement_extensions'
|
||||||
|
|
||||||
|
class RotatedCell < Prawn::Table::Cell::Text
|
||||||
|
def initialize(pdf, text, options = {})
|
||||||
|
options[:content] = text
|
||||||
|
options[:valign] = :center
|
||||||
|
options[:align] = :center
|
||||||
|
options[:rotate_around] = :center
|
||||||
|
@rotation = -options[:rotate] || 0
|
||||||
|
super(pdf, [0, pdf.cursor], options)
|
||||||
|
end
|
||||||
|
|
||||||
|
def tan_rotation
|
||||||
|
Math.tan(Math::PI * @rotation / 180)
|
||||||
|
end
|
||||||
|
|
||||||
|
def skew
|
||||||
|
(height + (border_top_width / 2.0) + (border_bottom_width / 2.0)) / tan_rotation
|
||||||
|
end
|
||||||
|
|
||||||
|
def styled_width_of(_text)
|
||||||
|
options = @text_options.reject { |k| k == :style }
|
||||||
|
with_font { (@pdf.height_of(@content, options) + padding_top + padding_bottom) / tan_rotation }
|
||||||
|
end
|
||||||
|
|
||||||
|
def natural_content_height
|
||||||
|
options = @text_options.reject { |k| k == :style }
|
||||||
|
with_font { (@pdf.width_of(@content, options) + padding_top + padding_bottom) * tan_rotation }
|
||||||
|
end
|
||||||
|
|
||||||
|
def draw_borders(point)
|
||||||
|
@pdf.mask(:line_width, :stroke_color) do
|
||||||
|
x, y = point
|
||||||
|
from = [[x - skew, y + (border_top_width / 2.0)],
|
||||||
|
to = [x, y - height - (border_bottom_width / 2.0)]]
|
||||||
|
|
||||||
|
@pdf.line_width = @border_widths[3]
|
||||||
|
@pdf.stroke_color = @border_colors[3]
|
||||||
|
@pdf.stroke_line(from, to)
|
||||||
|
@pdf.undash
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def draw_content
|
||||||
|
with_font do
|
||||||
|
with_text_color do
|
||||||
|
text_box(width: spanned_content_width + FPTolerance + skew,
|
||||||
|
height: spanned_content_height + FPTolerance,
|
||||||
|
at: [1 - skew, @pdf.cursor]).render
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class RenderPdf < Prawn::Document
|
||||||
|
include ActionView::Helpers::NumberHelper
|
||||||
|
include ApplicationHelper
|
||||||
|
|
||||||
|
TOP_MARGIN = 36
|
||||||
|
BOTTOM_MARGIN = 23
|
||||||
|
HEADER_SPACE = 9
|
||||||
|
FOOTER_SPACE = 3
|
||||||
|
HEADER_FONT_SIZE = 16
|
||||||
|
FOOTER_FONT_SIZE = 8
|
||||||
|
DEFAULT_FONT = 'OpenSans'
|
||||||
|
|
||||||
|
def initialize(options = {})
|
||||||
|
options[:font_size] ||= FoodsoftConfig[:pdf_font_size].try(:to_f) || 12
|
||||||
|
options[:page_size] ||= FoodsoftConfig[:pdf_page_size] || 'A4'
|
||||||
|
options[:skip_page_creation] = true
|
||||||
|
@options = options
|
||||||
|
@first_page = true
|
||||||
|
no_footer = @options&.[](:no_footer) ? true : false
|
||||||
|
super(options)
|
||||||
|
|
||||||
|
# Use ttf for better utf-8 compability
|
||||||
|
font_families.update(
|
||||||
|
'OpenSans' => {
|
||||||
|
bold: font_path('OpenSans-Bold.ttf'),
|
||||||
|
italic: font_path('OpenSans-Italic.ttf'),
|
||||||
|
bold_italic: font_path('OpenSans-BoldItalic.ttf'),
|
||||||
|
normal: font_path('OpenSans-Regular.ttf')
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
header = options[:title] || title
|
||||||
|
footer = I18n.l(Time.now, format: :long) unless no_footer
|
||||||
|
|
||||||
|
header_size = 0
|
||||||
|
header_size = height_of(header, size: HEADER_FONT_SIZE, font: DEFAULT_FONT) + HEADER_SPACE if header
|
||||||
|
footer_size = no_footer ? 0 : height_of(footer, size: FOOTER_FONT_SIZE, font: DEFAULT_FONT) + FOOTER_SPACE
|
||||||
|
|
||||||
|
start_new_page(top_margin: TOP_MARGIN + header_size, bottom_margin: BOTTOM_MARGIN + footer_size)
|
||||||
|
|
||||||
|
font DEFAULT_FONT
|
||||||
|
|
||||||
|
repeat :all, dynamic: true do
|
||||||
|
bounding_box [bounds.left, bounds.top + header_size], width: bounds.width, height: header_size do
|
||||||
|
text header, size: HEADER_FONT_SIZE, align: :center, overflow: :shrink_to_fit if header
|
||||||
|
end
|
||||||
|
|
||||||
|
unless no_footer
|
||||||
|
font_size FOOTER_FONT_SIZE do
|
||||||
|
bounding_box [bounds.left, bounds.bottom - FOOTER_SPACE], width: bounds.width, height: footer_size do
|
||||||
|
text footer, align: :left, valign: :bottom
|
||||||
|
end
|
||||||
|
bounding_box [bounds.left, bounds.bottom - FOOTER_SPACE], width: bounds.width, height: footer_size do
|
||||||
|
text I18n.t('lib.render_pdf.page', number: page_number, count: page_count), align: :right, valign: :bottom
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def title
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_pdf
|
||||||
|
body # Add content, which is defined in subclasses
|
||||||
|
render # Render pdf
|
||||||
|
end
|
||||||
|
|
||||||
|
# @todo avoid underscore instead of unicode whitespace in pdf :/
|
||||||
|
def number_to_currency(number, options = {})
|
||||||
|
super(number, options).gsub("\u202f", ' ') if number
|
||||||
|
end
|
||||||
|
|
||||||
|
def font_size(points = nil, &block)
|
||||||
|
points *= @options[:font_size] / 12 if points
|
||||||
|
super(points, &block)
|
||||||
|
end
|
||||||
|
|
||||||
|
# add pagebreak or vertical whitespace, depending on configuration
|
||||||
|
def down_or_page(space = 10)
|
||||||
|
if @first_page
|
||||||
|
@first_page = false
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if pdf_add_page_breaks?
|
||||||
|
start_new_page
|
||||||
|
else
|
||||||
|
move_down space
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
protected
|
||||||
|
|
||||||
|
def fontsize(size)
|
||||||
|
size
|
||||||
|
end
|
||||||
|
|
||||||
|
# return whether pagebreak or vertical whitespace is used for breaks
|
||||||
|
def pdf_add_page_breaks?(docid = nil)
|
||||||
|
docid ||= self.class.name.underscore
|
||||||
|
cfg = FoodsoftConfig[:pdf_add_page_breaks]
|
||||||
|
case cfg
|
||||||
|
when Array
|
||||||
|
cfg.index(docid.to_s).any?
|
||||||
|
when Hash
|
||||||
|
cfg[docid.to_s]
|
||||||
|
else
|
||||||
|
cfg
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def font_path(name)
|
||||||
|
Rails.root.join('vendor', 'assets', 'fonts', name)
|
||||||
|
end
|
||||||
|
end
|
58
plugins/automatic_invoices/app/models/group_order_invoice.rb
Normal file
58
plugins/automatic_invoices/app/models/group_order_invoice.rb
Normal 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
|
|
@ -0,0 +1,3 @@
|
||||||
|
/ insert_after 'erb:contains("phone")'
|
||||||
|
- if FoodsoftAutomaticInvoices.enabled?
|
||||||
|
= config_input c, :tax_number, input_html: {class: 'input-medium'}
|
|
@ -0,0 +1,8 @@
|
||||||
|
/ insert_after 'erb:contains(":use_self_service")'
|
||||||
|
- if FoodsoftAutomaticInvoices.enabled?
|
||||||
|
%h4= t '.group_order_invoices'
|
||||||
|
= form.fields_for :group_order_invoices do |field|
|
||||||
|
= config_input field, :use_automatic_invoices, as: :boolean
|
||||||
|
= config_input field, :separate_deposits, as: :boolean
|
||||||
|
= config_input field, :vat_exempt, as: :boolean
|
||||||
|
= config_input field, :payment_method, as: :string, input_html: {class: 'input-medium'}
|
|
@ -0,0 +1,3 @@
|
||||||
|
/ insert_after 'erb:contains(":updated_by")'
|
||||||
|
%th= heading_helper GroupOrderInvoice, :name
|
||||||
|
%th
|
|
@ -0,0 +1,9 @@
|
||||||
|
/ insert_after 'erb:contains("show_user(order.updated_by)")'
|
||||||
|
%td
|
||||||
|
- if order.closed?
|
||||||
|
-if FoodsoftConfig[:contact][:tax_number] && order.ordergroups.present?
|
||||||
|
= render :partial => 'group_order_invoices/links', locals:{order: order}
|
||||||
|
-else
|
||||||
|
= I18n.t('activerecord.attributes.group_order_invoice.tax_number_not_set')
|
||||||
|
- else
|
||||||
|
= t('orders.index.not_closed')
|
|
@ -0,0 +1,21 @@
|
||||||
|
/ replace_contents "tr.gross-amount"
|
||||||
|
- if FoodsoftConfig[:group_order_invoices]&.[](:separate_deposits)
|
||||||
|
%tr
|
||||||
|
%td= t('.gross_amount')
|
||||||
|
%td.numeric= number_to_currency(order.sum(:gross_without_deposit))
|
||||||
|
%tr
|
||||||
|
%td= t('.fc_amount')
|
||||||
|
%td.numeric= number_to_currency(order.sum(:fc_without_deposit))
|
||||||
|
%tr
|
||||||
|
%td= t('.deposit')
|
||||||
|
%td.numeric= number_to_currency(order.sum(:deposit))
|
||||||
|
%tr
|
||||||
|
%td= t('.net_deposit')
|
||||||
|
%td.numeric= number_to_currency(order.sum(:net_deposit))
|
||||||
|
%tr
|
||||||
|
%td= t('.fc_deposit')
|
||||||
|
%td.numeric= number_to_currency(order.sum(:fc_deposit))
|
||||||
|
- else
|
||||||
|
%tr
|
||||||
|
%td= t('.gross_amount')
|
||||||
|
%td.numeric= number_to_currency(order.sum(:gross))
|
|
@ -0,0 +1,15 @@
|
||||||
|
if FoodsoftAutomaticInvoices.enabled?
|
||||||
|
Mailer.class_eval do
|
||||||
|
# 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
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,31 @@
|
||||||
|
if FoodsoftAutomaticInvoices.enabled?
|
||||||
|
|
||||||
|
PriceCalculation.class_eval do
|
||||||
|
# deposit is always gross
|
||||||
|
def gross_price
|
||||||
|
add_percent(price, tax) + deposit
|
||||||
|
end
|
||||||
|
|
||||||
|
def gross_price_without_deposit
|
||||||
|
add_percent(price, tax)
|
||||||
|
end
|
||||||
|
|
||||||
|
def net_deposit_price
|
||||||
|
remove_percent(deposit, tax)
|
||||||
|
end
|
||||||
|
|
||||||
|
def fc_price_without_deposit
|
||||||
|
add_percent(gross_price_without_deposit, FoodsoftConfig[:price_markup].to_i)
|
||||||
|
end
|
||||||
|
|
||||||
|
def fc_deposit_price
|
||||||
|
add_percent(deposit, FoodsoftConfig[:price_markup].to_i)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def remove_percent(value, percent)
|
||||||
|
(value / ((percent * 0.01) + 1)).round(2)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,15 @@
|
||||||
|
if FoodsoftAutomaticInvoices.enabled?
|
||||||
|
GroupOrderArticle.class_eval do
|
||||||
|
def total_price_without_deposit(order_article = self.order_article)
|
||||||
|
if order_article.order.open?
|
||||||
|
if FoodsoftConfig[:tolerance_is_costly]
|
||||||
|
order_article.price.fc_price_without_deposit * (quantity + tolerance)
|
||||||
|
else
|
||||||
|
order_article.price.fc_price_without_deposit * quantity
|
||||||
|
end
|
||||||
|
else
|
||||||
|
order_article.price.fc_price_without_deposit * result
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,5 @@
|
||||||
|
if FoodsoftAutomaticInvoices.enabled?
|
||||||
|
GroupOrder.class_eval do
|
||||||
|
has_one :group_order_invoice
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,11 @@
|
||||||
|
if FoodsoftAutomaticInvoices.enabled?
|
||||||
|
OrderArticle.class_eval do
|
||||||
|
def total_gross_price_without_deposit
|
||||||
|
units * price.unit_quantity * price.gross_price_without_deposit
|
||||||
|
end
|
||||||
|
|
||||||
|
def total_deposit_price
|
||||||
|
units * price.unit_quantity * price.deposit
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,50 @@
|
||||||
|
if FoodsoftAutomaticInvoices.enabled?
|
||||||
|
class Order < ApplicationRecord
|
||||||
|
# Returns the all round price of a finished order
|
||||||
|
# :groups returns the sum of all GroupOrders
|
||||||
|
# :clear returns the price without tax, deposit and markup
|
||||||
|
# :gross includes tax and deposit(gross). this amount should be equal to suppliers bill
|
||||||
|
# :gross_without_deposit excludes the depost from the gross price
|
||||||
|
# :net_deposit returns the deposit without tax (deposit entered by user is default gross)
|
||||||
|
# :fc_deposit_price returns the deposit with markup
|
||||||
|
# :fc, guess what...
|
||||||
|
def sum(type = :gross)
|
||||||
|
total = 0
|
||||||
|
if %i[net gross net_deposit gross_without_deposit fc_without_deposit fc_deposit deposit fc].include?(type)
|
||||||
|
for oa in order_articles.ordered.includes(:article, :article_price)
|
||||||
|
quantity = oa.units * oa.price.unit_quantity
|
||||||
|
case type
|
||||||
|
when :net
|
||||||
|
total += quantity * oa.price.price
|
||||||
|
when :gross
|
||||||
|
total += quantity * oa.price.gross_price
|
||||||
|
when :gross_without_deposit
|
||||||
|
total += quantity * oa.price.gross_price_without_deposit
|
||||||
|
when :fc
|
||||||
|
total += quantity * oa.price.fc_price
|
||||||
|
when :fc_without_deposit
|
||||||
|
total += quantity * oa.price.fc_price_without_deposit
|
||||||
|
when :net_deposit
|
||||||
|
total += quantity * oa.price.net_deposit_price
|
||||||
|
when :fc_deposit
|
||||||
|
total += quantity * oa.price.fc_deposit_price
|
||||||
|
when :deposit
|
||||||
|
total += quantity * oa.price.deposit
|
||||||
|
end
|
||||||
|
end
|
||||||
|
elsif %i[groups groups_without_markup].include?(type)
|
||||||
|
for go in group_orders.includes(group_order_articles: { order_article: %i[article article_price] })
|
||||||
|
for goa in go.group_order_articles
|
||||||
|
case type
|
||||||
|
when :groups
|
||||||
|
total += goa.result * goa.order_article.price.fc_price
|
||||||
|
when :groups_without_markup
|
||||||
|
total += goa.result * goa.order_article.price.gross_price
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
total
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
1993
plugins/automatic_invoices/config/locales/de.yml
Normal file
1993
plugins/automatic_invoices/config/locales/de.yml
Normal file
File diff suppressed because it is too large
Load diff
1991
plugins/automatic_invoices/config/locales/en.yml
Normal file
1991
plugins/automatic_invoices/config/locales/en.yml
Normal file
File diff suppressed because it is too large
Load diff
7
plugins/automatic_invoices/config/routes.rb
Normal file
7
plugins/automatic_invoices/config/routes.rb
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
Rails.application.routes.draw do
|
||||||
|
post 'finance/group_order_invoice', to: 'group_order_invoices#create_multiple'
|
||||||
|
|
||||||
|
get 'orders/:order_id/group_order_invoices/download_all', to: 'group_order_invoices#download_all', as: 'download_all_group_order_invoices'
|
||||||
|
|
||||||
|
resources :group_order_invoices
|
||||||
|
end
|
|
@ -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
|
|
@ -0,0 +1,5 @@
|
||||||
|
class AddCustomerNumberToGroup < ActiveRecord::Migration[7.0]
|
||||||
|
def change
|
||||||
|
add_column :groups, :customer_number, :string, unique: true
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,19 @@
|
||||||
|
$:.push File.expand_path('lib', __dir__)
|
||||||
|
|
||||||
|
# Maintain your gem's version:
|
||||||
|
require 'foodsoft_automatic_invoices/version'
|
||||||
|
|
||||||
|
# Describe your gem and declare its dependencies:
|
||||||
|
Gem::Specification.new do |s|
|
||||||
|
s.name = 'foodsoft_automatic_invoices'
|
||||||
|
s.version = FoodsoftAutomaticInvoices::VERSION
|
||||||
|
s.authors = ['viehlieb']
|
||||||
|
s.email = ['pf@pragma-shift.net']
|
||||||
|
s.summary = "Foodsoft plugin to enhance foodsoft's accounting capabilities and to create and automatically deliver invoice pdfs for accounted orders."
|
||||||
|
s.description = ''
|
||||||
|
|
||||||
|
s.files = Dir['{app,config,db,spec,lib}/**/*'] + ['Rakefile', 'README.md','/app/controllers/concerns/send_group_order_invoice_pdf.rb']
|
||||||
|
|
||||||
|
s.add_dependency 'rails'
|
||||||
|
s.add_dependency 'deface', '~> 1.0'
|
||||||
|
end
|
|
@ -0,0 +1,9 @@
|
||||||
|
require 'deface'
|
||||||
|
require 'foodsoft_automatic_invoices/engine'
|
||||||
|
require 'foodsoft_automatic_invoices/send_group_order_invoice_pdf'
|
||||||
|
|
||||||
|
module FoodsoftAutomaticInvoices
|
||||||
|
def self.enabled?
|
||||||
|
FoodsoftConfig[:use_automatic_invoices]
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,7 @@
|
||||||
|
module FoodsoftAutomaticInvoices
|
||||||
|
class Engine < ::Rails::Engine
|
||||||
|
def default_foodsoft_config(cfg)
|
||||||
|
cfg[:use_automatic_invoices] = false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,24 @@
|
||||||
|
module FoodsoftAutomaticInvoices
|
||||||
|
module SendGroupOrderInvoicePdf
|
||||||
|
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
protected
|
||||||
|
|
||||||
|
def create_invoice_pdf(group_order_invoice)
|
||||||
|
invoice_data = group_order_invoice.load_data_for_invoice
|
||||||
|
invoice_data[:title] = t('documents.group_order_invoice_pdf.title', supplier: invoice_data[:supplier])
|
||||||
|
invoice_data[:no_footer] = true
|
||||||
|
GroupOrderInvoicePdf.new invoice_data
|
||||||
|
end
|
||||||
|
|
||||||
|
def send_group_order_invoice_pdf(group_order_invoice)
|
||||||
|
pdf = create_invoice_pdf(group_order_invoice)
|
||||||
|
send_data pdf.to_pdf, filename: pdf.filename, type: 'application/pdf'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
ActiveSupport.on_load(:after_initialize) do
|
||||||
|
Concerns::SendOrderPdf.include FoodsoftAutomaticInvoices::SendGroupOrderInvoicePdf
|
||||||
|
end
|
|
@ -0,0 +1,3 @@
|
||||||
|
module FoodsoftAutomaticInvoices
|
||||||
|
VERSION = '0.0.1'
|
||||||
|
end
|
|
@ -0,0 +1,7 @@
|
||||||
|
require 'factory_bot'
|
||||||
|
|
||||||
|
FactoryBot.define do
|
||||||
|
factory :group_order_invoice do
|
||||||
|
group_order { create :group_order }
|
||||||
|
end
|
||||||
|
end
|
|
@ -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
|
168
plugins/automatic_invoices/spec/models/article_spec.rb
Normal file
168
plugins/automatic_invoices/spec/models/article_spec.rb
Normal file
|
@ -0,0 +1,168 @@
|
||||||
|
require_relative '../spec_helper'
|
||||||
|
|
||||||
|
describe Article do
|
||||||
|
let(:supplier) { create(:supplier) }
|
||||||
|
let(:article) { create(:article, supplier: supplier) }
|
||||||
|
|
||||||
|
it 'has a unique name' do
|
||||||
|
article2 = build(:article, supplier: supplier, name: article.name)
|
||||||
|
expect(article2).to be_invalid
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'can be deleted' do
|
||||||
|
expect(article).not_to be_deleted
|
||||||
|
article.mark_as_deleted
|
||||||
|
expect(article).to be_deleted
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'convert units' do
|
||||||
|
it 'returns nil when equal' do
|
||||||
|
expect(article.convert_units(article)).to be_nil
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns false when invalid unit' do
|
||||||
|
article1 = build(:article, supplier: supplier, unit: 'invalid')
|
||||||
|
expect(article.convert_units(article1)).to be false
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns false if unit = 0' do
|
||||||
|
article1 = build(:article, supplier: supplier, unit: '1kg', price: 2, unit_quantity: 1)
|
||||||
|
article2 = build(:article, supplier: supplier, unit: '0kg', price: 2, unit_quantity: 1)
|
||||||
|
expect(article1.convert_units(article2)).to be false
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns false if unit becomes zero because of , symbol in unit format' do
|
||||||
|
article1 = build(:article, supplier: supplier, unit: '0,8kg', price: 2, unit_quantity: 1)
|
||||||
|
article2 = build(:article, supplier: supplier, unit: '0,9kg', price: 2, unit_quantity: 1)
|
||||||
|
expect(article1.convert_units(article2)).to be false
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'converts from ST to KI (german foodcoops legacy)' do
|
||||||
|
article1 = build(:article, supplier: supplier, unit: 'ST')
|
||||||
|
article2 = build(:article, supplier: supplier, name: 'banana 10-12 St', price: 12.34, unit: 'KI')
|
||||||
|
new_price, new_unit_quantity = article1.convert_units(article2)
|
||||||
|
expect(new_unit_quantity).to eq 10
|
||||||
|
expect(new_price).to eq 1.23
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'converts from g to kg' do
|
||||||
|
article1 = build(:article, supplier: supplier, unit: 'kg')
|
||||||
|
article2 = build(:article, supplier: supplier, unit: 'g', price: 0.12, unit_quantity: 1500)
|
||||||
|
new_price, new_unit_quantity = article1.convert_units(article2)
|
||||||
|
expect(new_unit_quantity).to eq 1.5
|
||||||
|
expect(new_price).to eq 120
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'computes changed article attributes' do
|
||||||
|
article2 = build(:article, supplier: supplier, name: 'banana')
|
||||||
|
expect(article.unequal_attributes(article2)[:name]).to eq 'banana'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'computes the gross price correctly' do
|
||||||
|
article.deposit = 0
|
||||||
|
article.tax = 12
|
||||||
|
expect(article.gross_price).to eq((article.price * 1.12).round(2))
|
||||||
|
article.deposit = 1.20
|
||||||
|
if FoodsoftConfig[:group_order_invoices]&.[](:separate_deposits)
|
||||||
|
expect(article.gross_price_without_deposit).to eq((article.price * 1.12 + 1.20).round(2))
|
||||||
|
expect(article.gross_price).to eq(((article.price + 1.20) * 1.12).round(2))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'gross price >= net price' do
|
||||||
|
expect(article.gross_price).to be >= article.price
|
||||||
|
end
|
||||||
|
|
||||||
|
[[nil, 1],
|
||||||
|
[0, 1],
|
||||||
|
[5, 1.05],
|
||||||
|
[42, 1.42],
|
||||||
|
[100, 2]].each do |price_markup, percent|
|
||||||
|
it "computes the fc price with price_markup #{price_markup} correctly" do
|
||||||
|
FoodsoftConfig.config['price_markup'] = price_markup
|
||||||
|
expect(article.fc_price).to eq((article.gross_price * percent).round(2))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
it 'knows when it is deleted' do
|
||||||
|
expect(supplier.deleted?).to be false
|
||||||
|
supplier.mark_as_deleted
|
||||||
|
expect(supplier.deleted?).to be true
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'keeps a price history' do
|
||||||
|
expect(article.article_prices.map(&:price)).to eq([article.price])
|
||||||
|
oldprice = article.price
|
||||||
|
sleep 1 # so that the new price really has a later creation time
|
||||||
|
article.price += 1
|
||||||
|
article.save!
|
||||||
|
expect(article.article_prices.reload.map(&:price)).to eq([article.price, oldprice])
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'is not in an open order by default' do
|
||||||
|
expect(article.in_open_order).to be_nil
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'is knows its open order' do
|
||||||
|
order = create(:order, supplier: supplier, article_ids: [article.id])
|
||||||
|
expect(article.in_open_order).to eq(order)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'has no shared article by default' do
|
||||||
|
expect(article.shared_article).to be_nil
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'connected to a shared database', type: :feature do
|
||||||
|
let(:shared_article) { create(:shared_article) }
|
||||||
|
let(:supplier) { create(:supplier, shared_supplier_id: shared_article.supplier_id) }
|
||||||
|
let(:article) { create(:article, supplier: supplier, order_number: shared_article.order_number) }
|
||||||
|
|
||||||
|
it 'can be found in the shared database' do
|
||||||
|
expect(article.shared_article).not_to be_nil
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'can find updates' do
|
||||||
|
changed = article.shared_article_changed?
|
||||||
|
expect(changed).not_to be_falsey
|
||||||
|
expect(changed.length).to be > 1
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'can be synchronised' do
|
||||||
|
# TODO: move article sync from supplier to article
|
||||||
|
article # need to reference for it to exist when syncing
|
||||||
|
updated_article = supplier.sync_all[0].select { |s| s[0].id == article.id }.first[0]
|
||||||
|
article.update(updated_article.attributes.reject { |k, _v| %w[id type].include?(k) })
|
||||||
|
expect(article.name).to eq(shared_article.name)
|
||||||
|
# now synchronising shouldn't change anything anymore
|
||||||
|
expect(article.shared_article_changed?).to be_falsey
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not need to synchronise an imported article' do
|
||||||
|
article = shared_article.build_new_article(supplier)
|
||||||
|
article.article_category = create :article_category
|
||||||
|
expect(article.shared_article_changed?).to be_falsey
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'adapts to foodcoop units when synchronising' do
|
||||||
|
shared_article.unit = '1kg'
|
||||||
|
shared_article.unit_quantity = 1
|
||||||
|
shared_article.save!
|
||||||
|
article = shared_article.build_new_article(supplier)
|
||||||
|
article.article_category = create :article_category
|
||||||
|
article.unit = '200g'
|
||||||
|
article.shared_updated_on -= 1 # to make update do something
|
||||||
|
article.save!
|
||||||
|
# TODO: get sync functionality in article
|
||||||
|
updated_article = supplier.sync_all[0].select { |s| s[0].id == article.id }.first[0]
|
||||||
|
article.update!(updated_article.attributes.reject { |k, _v| %w[id type].include?(k) })
|
||||||
|
expect(article.unit).to eq '200g'
|
||||||
|
expect(article.unit_quantity).to eq 5
|
||||||
|
expect(article.price).to be_within(0.005).of(shared_article.price / 5)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not synchronise when it has no order number' do
|
||||||
|
article.update(order_number: nil)
|
||||||
|
expect(supplier.sync_all).to eq [[], [], []]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -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
|
Loading…
Reference in a new issue