mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-01-24 20:36:49 +00:00
Merge branch 'master' into reduce-supplier-list-in-order-cycles
This commit is contained in:
@@ -59,6 +59,7 @@ module Admin
|
||||
|
||||
def load_data
|
||||
@calculators = EnterpriseFee.calculators.sort_by(&:name)
|
||||
@tax_categories = Spree::TaxCategory.order('is_default DESC, name ASC')
|
||||
end
|
||||
|
||||
def collection
|
||||
|
||||
@@ -6,6 +6,7 @@ require 'open_food_network/order_grouper'
|
||||
require 'open_food_network/customers_report'
|
||||
require 'open_food_network/users_and_enterprises_report'
|
||||
require 'open_food_network/order_cycle_management_report'
|
||||
require 'open_food_network/sales_tax_report'
|
||||
|
||||
Spree::Admin::ReportsController.class_eval do
|
||||
|
||||
@@ -75,7 +76,7 @@ Spree::Admin::ReportsController.class_eval do
|
||||
end
|
||||
|
||||
def orders_and_distributors
|
||||
params[:q] = {} unless params[:q]
|
||||
params[:q] ||= {}
|
||||
|
||||
if params[:q][:completed_at_gt].blank?
|
||||
params[:q][:completed_at_gt] = Time.zone.now.beginning_of_month
|
||||
@@ -102,9 +103,39 @@ Spree::Admin::ReportsController.class_eval do
|
||||
send_data csv_string, :filename => "orders_and_distributors_#{timestamp}.csv"
|
||||
end
|
||||
end
|
||||
|
||||
def sales_tax
|
||||
params[:q] ||= {}
|
||||
|
||||
if params[:q][:completed_at_gt].blank?
|
||||
params[:q][:completed_at_gt] = Time.zone.now.beginning_of_month
|
||||
else
|
||||
params[:q][:completed_at_gt] = Time.zone.parse(params[:q][:completed_at_gt]).beginning_of_day rescue Time.zone.now.beginning_of_month
|
||||
end
|
||||
|
||||
if params[:q] && !params[:q][:completed_at_lt].blank?
|
||||
params[:q][:completed_at_lt] = Time.zone.parse(params[:q][:completed_at_lt]).end_of_day rescue ""
|
||||
end
|
||||
params[:q][:meta_sort] ||= "completed_at.desc"
|
||||
|
||||
@search = Spree::Order.complete.not_state(:canceled).managed_by(spree_current_user).search(params[:q])
|
||||
orders = @search.result
|
||||
@distributors = Enterprise.is_distributor.managed_by(spree_current_user)
|
||||
|
||||
@report = OpenFoodNetwork::SalesTaxReport.new orders
|
||||
unless params[:csv]
|
||||
render :html => @report
|
||||
else
|
||||
csv_string = CSV.generate do |csv|
|
||||
csv << @report.header
|
||||
@report.table.each { |row| csv << row }
|
||||
end
|
||||
send_data csv_string, :filename => "sales_tax.csv"
|
||||
end
|
||||
end
|
||||
|
||||
def bulk_coop
|
||||
params[:q] = {} unless params[:q]
|
||||
params[:q] ||= {}
|
||||
|
||||
if params[:q][:completed_at_gt].blank?
|
||||
params[:q][:completed_at_gt] = Time.zone.now.beginning_of_month
|
||||
@@ -257,7 +288,7 @@ Spree::Admin::ReportsController.class_eval do
|
||||
end
|
||||
|
||||
def payments
|
||||
params[:q] = {} unless params[:q]
|
||||
params[:q] ||= {}
|
||||
|
||||
if params[:q][:completed_at_gt].blank?
|
||||
params[:q][:completed_at_gt] = Time.zone.now.beginning_of_month
|
||||
@@ -641,7 +672,8 @@ Spree::Admin::ReportsController.class_eval do
|
||||
:products_and_inventory => {:name => "Products & Inventory", :description => ''},
|
||||
:sales_total => { :name => "Sales Total", :description => "Sales Total For All Orders" },
|
||||
:users_and_enterprises => { :name => "Users & Enterprises", :description => "Enterprise Ownership & Status" },
|
||||
:order_cycle_management => {:name => "Order Cycle Management", :description => ''}
|
||||
:order_cycle_management => {:name => "Order Cycle Management", :description => ''},
|
||||
:sales_tax => { :name => "Sales Tax", :description => "Sales Tax For Orders" }
|
||||
}
|
||||
# Return only reports the user is authorized to view.
|
||||
reports.select { |action| can? action, :report }
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
class EnterpriseFee < ActiveRecord::Base
|
||||
belongs_to :enterprise
|
||||
belongs_to :tax_category, class_name: 'Spree::TaxCategory', foreign_key: 'tax_category_id'
|
||||
has_and_belongs_to_many :order_cycles, join_table: 'coordinator_fees'
|
||||
has_many :exchange_fees, dependent: :destroy
|
||||
has_many :exchanges, through: :exchange_fees
|
||||
@@ -8,7 +9,7 @@ class EnterpriseFee < ActiveRecord::Base
|
||||
|
||||
calculated_adjustments
|
||||
|
||||
attr_accessible :enterprise_id, :fee_type, :name, :calculator_type
|
||||
attr_accessible :enterprise_id, :fee_type, :name, :tax_category_id, :calculator_type
|
||||
|
||||
FEE_TYPES = %w(packing transport admin sales fundraising)
|
||||
PER_ORDER_CALCULATORS = ['Spree::Calculator::FlatRate', 'Spree::Calculator::FlexiRate']
|
||||
|
||||
@@ -153,7 +153,7 @@ class AbilityDecorator
|
||||
end
|
||||
|
||||
# Reports page
|
||||
can [:admin, :index, :customers, :orders_and_distributors, :group_buys, :bulk_coop, :payments, :orders_and_fulfillment, :products_and_inventory, :order_cycle_management], :report
|
||||
can [:admin, :index, :customers, :group_buys, :bulk_coop, :sales_tax, :payments, :orders_and_distributors, :orders_and_fulfillment, :products_and_inventory, :order_cycle_management], :report
|
||||
end
|
||||
|
||||
|
||||
|
||||
@@ -3,5 +3,17 @@ module Spree
|
||||
has_one :metadata, class_name: 'AdjustmentMetadata', dependent: :destroy
|
||||
|
||||
scope :enterprise_fee, where(originator_type: 'EnterpriseFee')
|
||||
scope :included_tax, where(originator_type: 'Spree::TaxRate', adjustable_type: 'Spree::LineItem')
|
||||
|
||||
attr_accessible :included_tax
|
||||
|
||||
def set_included_tax!(rate)
|
||||
tax = amount - (amount / (1 + rate))
|
||||
set_absolute_included_tax! tax
|
||||
end
|
||||
|
||||
def set_absolute_included_tax!(tax)
|
||||
update_attributes! included_tax: tax.round(2)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
Spree::AppConfiguration.class_eval do
|
||||
# This file decorates the existing preferences file defined by Spree.
|
||||
# It allows us to add our own global configuration variables, which
|
||||
# we can allow to be modified in the UI by adding appropriate form
|
||||
# elements to existing or new configuration pages.
|
||||
# This file decorates the existing preferences file defined by Spree.
|
||||
# It allows us to add our own global configuration variables, which
|
||||
# we can allow to be modified in the UI by adding appropriate form
|
||||
# elements to existing or new configuration pages.
|
||||
|
||||
# Tax Preferences
|
||||
preference :products_require_tax_category, :boolean, default: false
|
||||
end
|
||||
# Tax Preferences
|
||||
preference :products_require_tax_category, :boolean, default: false
|
||||
preference :shipping_tax_rate, :decimal, default: 0
|
||||
end
|
||||
|
||||
7
app/models/spree/money_decorator.rb
Normal file
7
app/models/spree/money_decorator.rb
Normal file
@@ -0,0 +1,7 @@
|
||||
Spree::Money.class_eval do
|
||||
|
||||
# return the currency symbol (on it's own) for the current default currency
|
||||
def self.currency_symbol
|
||||
Money.new(0, Spree::Config[:currency]).symbol
|
||||
end
|
||||
end
|
||||
11
app/models/spree/shipment_decorator.rb
Normal file
11
app/models/spree/shipment_decorator.rb
Normal file
@@ -0,0 +1,11 @@
|
||||
module Spree
|
||||
Shipment.class_eval do
|
||||
def ensure_correct_adjustment_with_included_tax
|
||||
ensure_correct_adjustment_without_included_tax
|
||||
|
||||
adjustment.set_included_tax! Config.shipping_tax_rate if Config.shipment_inc_vat
|
||||
end
|
||||
|
||||
alias_method_chain :ensure_correct_adjustment, :included_tax
|
||||
end
|
||||
end
|
||||
11
app/models/spree/tax_rate_decorator.rb
Normal file
11
app/models/spree/tax_rate_decorator.rb
Normal file
@@ -0,0 +1,11 @@
|
||||
Spree::TaxRate.class_eval do
|
||||
def adjust_with_included_tax(order)
|
||||
adjust_without_included_tax(order)
|
||||
|
||||
(order.adjustments.tax + order.price_adjustments).each do |a|
|
||||
a.set_absolute_included_tax! a.amount
|
||||
end
|
||||
end
|
||||
|
||||
alias_method_chain :adjust, :included_tax
|
||||
end
|
||||
@@ -0,0 +1,5 @@
|
||||
/ insert_after "[data-hook='shipment_vat']"
|
||||
|
||||
.field.align-center{ "data-hook" => "shipping_tax_rate" }
|
||||
= number_field_tag "preferences[shipping_tax_rate]", Spree::Config[:shipping_tax_rate].to_f, in: 0.0..1.0, step: 0.01
|
||||
= label_tag nil, t(:shipping_tax_rate)
|
||||
@@ -3,7 +3,7 @@ class EnterpriseFeePresenter
|
||||
@controller, @enterprise_fee, @index = controller, enterprise_fee, index
|
||||
end
|
||||
|
||||
delegate :id, :enterprise_id, :fee_type, :name, :calculator_type, :to => :enterprise_fee
|
||||
delegate :id, :enterprise_id, :fee_type, :name, :tax_category_id, :calculator_type, :to => :enterprise_fee
|
||||
|
||||
def enterprise_fee
|
||||
@enterprise_fee
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
%th Enterprise
|
||||
%th Fee Type
|
||||
%th Name
|
||||
%th Tax Category
|
||||
%th Calculator
|
||||
%th Calculator values
|
||||
%th.actions
|
||||
@@ -24,6 +25,7 @@
|
||||
= f.ng_collection_select :enterprise_id, @enterprises, :id, :name, 'enterprise_fee.enterprise_id', :include_blank => true
|
||||
%td= f.ng_select :fee_type, enterprise_fee_type_options, 'enterprise_fee.fee_type'
|
||||
%td= f.ng_text_field :name, { placeholder: 'e.g. packing fee' }
|
||||
%td= f.ng_collection_select :tax_category_id, @tax_categories, :id, :name, 'enterprise_fee.tax_category_id'
|
||||
%td= f.ng_collection_select :calculator_type, @calculators, :name, :description, 'enterprise_fee.calculator_type', {'class' => 'calculator_type', 'ng-model' => 'calculatorType', 'spree-ensure-calculator-preferences-match-type' => "1"}
|
||||
%td{'ng-bind-html-unsafe-compiled' => 'enterprise_fee.calculator_settings'}
|
||||
%td.actions{'spree-delete-resource' => "1"}
|
||||
|
||||
@@ -4,6 +4,7 @@ r.list_of :enterprise_fees, @presented_collection do
|
||||
r.element :enterprise_name
|
||||
r.element :fee_type
|
||||
r.element :name
|
||||
r.element :tax_category_id
|
||||
r.element :calculator_type
|
||||
r.element :calculator_description
|
||||
r.element :calculator_settings if @include_calculators
|
||||
|
||||
32
app/views/spree/admin/reports/sales_tax.html.haml
Normal file
32
app/views/spree/admin/reports/sales_tax.html.haml
Normal file
@@ -0,0 +1,32 @@
|
||||
= form_for @search, :url => spree.sales_tax_admin_reports_path do |f|
|
||||
= render 'date_range_form', f: f
|
||||
|
||||
.row
|
||||
.four.columns.alpha
|
||||
= label_tag nil, "Distributor:"
|
||||
= f.collection_select(:distributor_id_eq, @distributors, :id, :name, {:include_blank => 'All'}, {:class => "select2 fullwidth"})
|
||||
= check_box_tag :csv
|
||||
= label_tag :csv, "Download as csv"
|
||||
%br
|
||||
= button t(:search)
|
||||
|
||||
%br
|
||||
%br
|
||||
%table#listing_orders.index
|
||||
%thead
|
||||
%tr{'data-hook' => "orders_header"}
|
||||
- @report.header.each do |heading|
|
||||
%th= heading
|
||||
%tbody
|
||||
- @report.table.each do |row|
|
||||
%tr
|
||||
- row.each_with_index do |column, i|
|
||||
- if i == 0
|
||||
%td
|
||||
%a.edit-order{'href' => "/admin/orders/#{column}"}= column
|
||||
- else
|
||||
%td= column
|
||||
- if @report.table.empty?
|
||||
%tr
|
||||
%td{:colspan => @report.header.count}= t(:none)
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
\: #{spree_current_user.email}
|
||||
%li{"data-hook" => "user-account-link"}
|
||||
%i.icon-user
|
||||
= link_to t(:account), spree.edit_user_path(spree_current_user)
|
||||
= link_to t(:account), account_path
|
||||
%li{"data-hook" => "user-logout-link"}
|
||||
%i.icon-signout
|
||||
= link_to t(:logout), spree.logout_path
|
||||
|
||||
@@ -14,8 +14,12 @@ Spree.config do |config|
|
||||
config.checkout_zone = ENV["CHECKOUT_ZONE"]
|
||||
config.address_requires_state = true
|
||||
|
||||
country = Spree::Country.find_by_name(ENV["DEFAULT_COUNTRY"])
|
||||
config.default_country_id = country.id if country.present?
|
||||
if Spree::Country.table_exists?
|
||||
country = Spree::Country.find_by_name(ENV["DEFAULT_COUNTRY"])
|
||||
config.default_country_id = country.id if country.present?
|
||||
else
|
||||
config.default_country_id = 12 # Australia
|
||||
end
|
||||
|
||||
# -- spree_paypal_express
|
||||
# Auto-capture payments. Without this option, payments must be manually captured in the paypal interface.
|
||||
|
||||
@@ -123,7 +123,8 @@ Spree::Core::Engine.routes.prepend do
|
||||
match '/admin/reports/bulk_coop' => 'admin/reports#bulk_coop', :as => "bulk_coop_admin_reports", :via => [:get, :post]
|
||||
match '/admin/reports/payments' => 'admin/reports#payments', :as => "payments_admin_reports", :via => [:get, :post]
|
||||
match '/admin/reports/orders_and_fulfillment' => 'admin/reports#orders_and_fulfillment', :as => "orders_and_fulfillment_admin_reports", :via => [:get, :post]
|
||||
match '/admin/reports/users_and_enterprises' => 'admin/reports#users_and_enterprises', :as => "users_and_enterprises_admin_reports", :via => [:get, :post]
|
||||
match '/admin/reports/users_and_enterprises' => 'admin/reports#users_and_enterprises', :as => "users_and_enterprises_admin_reports", :via => [:get, :post]
|
||||
match '/admin/reports/sales_tax' => 'admin/reports#sales_tax', :as => "sales_tax_admin_reports", :via => [:get, :post]
|
||||
match '/admin/products/bulk_edit' => 'admin/products#bulk_edit', :as => "bulk_edit_admin_products"
|
||||
match '/admin/orders/bulk_management' => 'admin/orders#bulk_management', :as => "admin_bulk_order_management"
|
||||
match '/admin/reports/products_and_inventory' => 'admin/reports#products_and_inventory', :as => "products_and_inventory_admin_reports", :via => [:get, :post]
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
class AddTaxCategoryToEnterpriseFee < ActiveRecord::Migration
|
||||
def change
|
||||
add_column :enterprise_fees, :tax_category_id, :integer
|
||||
add_foreign_key :enterprise_fees, :spree_tax_categories, column: :tax_category_id
|
||||
add_index :enterprise_fees, :tax_category_id
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,5 @@
|
||||
class AddIncludedTaxToAdjustments < ActiveRecord::Migration
|
||||
def change
|
||||
add_column :spree_adjustments, :included_tax, :decimal, precision: 10, scale: 2, null: false, default: 0
|
||||
end
|
||||
end
|
||||
10
db/schema.rb
10
db/schema.rb
@@ -11,7 +11,7 @@
|
||||
#
|
||||
# It's strongly recommended to check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema.define(:version => 20150220035501) do
|
||||
ActiveRecord::Schema.define(:version => 20150225232938) do
|
||||
|
||||
create_table "adjustment_metadata", :force => true do |t|
|
||||
t.integer "adjustment_id"
|
||||
@@ -177,11 +177,13 @@ ActiveRecord::Schema.define(:version => 20150220035501) do
|
||||
t.integer "enterprise_id"
|
||||
t.string "fee_type"
|
||||
t.string "name"
|
||||
t.datetime "created_at", :null => false
|
||||
t.datetime "updated_at", :null => false
|
||||
t.datetime "created_at", :null => false
|
||||
t.datetime "updated_at", :null => false
|
||||
t.integer "tax_category_id"
|
||||
end
|
||||
|
||||
add_index "enterprise_fees", ["enterprise_id"], :name => "index_enterprise_fees_on_enterprise_id"
|
||||
add_index "enterprise_fees", ["tax_category_id"], :name => "index_enterprise_fees_on_tax_category_id"
|
||||
|
||||
create_table "enterprise_groups", :force => true do |t|
|
||||
t.string "name"
|
||||
@@ -414,6 +416,7 @@ ActiveRecord::Schema.define(:version => 20150220035501) do
|
||||
t.string "originator_type"
|
||||
t.boolean "eligible", :default => true
|
||||
t.string "adjustable_type"
|
||||
t.decimal "included_tax", :precision => 10, :scale => 2, :default => 0.0, :null => false
|
||||
end
|
||||
|
||||
add_index "spree_adjustments", ["adjustable_id"], :name => "index_adjustments_on_order_id"
|
||||
@@ -1088,6 +1091,7 @@ ActiveRecord::Schema.define(:version => 20150220035501) do
|
||||
add_foreign_key "distributors_shipping_methods", "spree_shipping_methods", name: "distributors_shipping_methods_shipping_method_id_fk", column: "shipping_method_id"
|
||||
|
||||
add_foreign_key "enterprise_fees", "enterprises", name: "enterprise_fees_enterprise_id_fk"
|
||||
add_foreign_key "enterprise_fees", "spree_tax_categories", name: "enterprise_fees_tax_category_id_fk", column: "tax_category_id"
|
||||
|
||||
add_foreign_key "enterprise_groups", "spree_addresses", name: "enterprise_groups_address_id_fk", column: "address_id"
|
||||
add_foreign_key "enterprise_groups", "spree_users", name: "enterprise_groups_owner_id_fk", column: "owner_id"
|
||||
|
||||
@@ -2,12 +2,18 @@ module OpenFoodNetwork
|
||||
class EnterpriseFeeApplicator < Struct.new(:enterprise_fee, :variant, :role)
|
||||
def create_line_item_adjustment(line_item)
|
||||
a = enterprise_fee.create_locked_adjustment(line_item_adjustment_label, line_item.order, line_item, true)
|
||||
|
||||
AdjustmentMetadata.create! adjustment: a, enterprise: enterprise_fee.enterprise, fee_name: enterprise_fee.name, fee_type: enterprise_fee.fee_type, enterprise_role: role
|
||||
|
||||
a.set_absolute_included_tax! adjustment_tax(line_item.order, a)
|
||||
end
|
||||
|
||||
def create_order_adjustment(order)
|
||||
a = enterprise_fee.create_locked_adjustment(order_adjustment_label, order, order, true)
|
||||
|
||||
AdjustmentMetadata.create! adjustment: a, enterprise: enterprise_fee.enterprise, fee_name: enterprise_fee.name, fee_type: enterprise_fee.fee_type, enterprise_role: role
|
||||
|
||||
a.set_absolute_included_tax! adjustment_tax(order, a)
|
||||
end
|
||||
|
||||
|
||||
@@ -24,6 +30,48 @@ module OpenFoodNetwork
|
||||
def base_adjustment_label
|
||||
"#{enterprise_fee.fee_type} fee by #{role} #{enterprise_fee.enterprise.name}"
|
||||
end
|
||||
|
||||
def adjustment_tax(order, adjustment)
|
||||
tax_rates = enterprise_fee.tax_category ? enterprise_fee.tax_category.tax_rates.match(order) : []
|
||||
|
||||
tax_rates.sum do |rate|
|
||||
compute_tax rate, adjustment.amount
|
||||
end
|
||||
end
|
||||
|
||||
# Apply a TaxRate to a particular amount. TaxRates normally compute against
|
||||
# LineItems or Orders, so we mock out a line item here to fit the interface
|
||||
# that our calculator (usually DefaultTax) expects.
|
||||
def compute_tax(tax_rate, amount)
|
||||
product = OpenStruct.new tax_category: tax_rate.tax_category
|
||||
line_item = Spree::LineItem.new quantity: 1
|
||||
line_item.define_singleton_method(:product) { product }
|
||||
line_item.define_singleton_method(:price) { amount }
|
||||
|
||||
# The enterprise fee adjustments for which we're calculating tax are always inclusive of
|
||||
# tax. However, there's nothing to stop an admin from setting one up with a tax rate
|
||||
# that's marked as not inclusive of tax, and that would result in the DefaultTax
|
||||
# calculator generating a slightly incorrect value. Therefore, we treat the tax
|
||||
# rate as inclusive of tax for the calculations below, regardless of its original
|
||||
# setting.
|
||||
with_tax_included_in_price(tax_rate) do
|
||||
tax_rate.calculator.compute line_item
|
||||
end
|
||||
end
|
||||
|
||||
def with_tax_included_in_price(tax_rate)
|
||||
old_included_in_price = tax_rate.included_in_price
|
||||
|
||||
tax_rate.included_in_price = true
|
||||
tax_rate.calculator.calculable.included_in_price = true
|
||||
|
||||
result = yield
|
||||
|
||||
tax_rate.included_in_price = old_included_in_price
|
||||
tax_rate.calculator.calculable.included_in_price = old_included_in_price
|
||||
|
||||
result
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
77
lib/open_food_network/sales_tax_report.rb
Normal file
77
lib/open_food_network/sales_tax_report.rb
Normal file
@@ -0,0 +1,77 @@
|
||||
module OpenFoodNetwork
|
||||
class SalesTaxReport
|
||||
include Spree::ReportsHelper
|
||||
|
||||
def initialize orders
|
||||
@orders = orders
|
||||
end
|
||||
|
||||
def header
|
||||
["Order number", "Date", "Items", "Items total (#{currency_symbol})", "Taxable Items Total (#{currency_symbol})",
|
||||
"Sales Tax (#{currency_symbol})", "Delivery Charge (#{currency_symbol})", "Tax on Delivery (#{currency_symbol})",
|
||||
"Total Tax (#{currency_symbol})", "Customer", "Distributor"]
|
||||
end
|
||||
|
||||
def table
|
||||
@orders.map do |order|
|
||||
totals = totals_of order.line_items
|
||||
shipping_cost = shipping_cost_for order
|
||||
shipping_tax = shipping_tax_on shipping_cost
|
||||
|
||||
[order.number, order.created_at, totals[:items], totals[:items_total],
|
||||
totals[:taxable_total], totals[:sales_tax], shipping_cost, shipping_tax, totals[:sales_tax] + shipping_tax,
|
||||
order.bill_address.full_name, order.distributor.andand.name]
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
private
|
||||
|
||||
def totals_of(line_items)
|
||||
totals = {items: 0, items_total: 0.0, taxable_total: 0.0, sales_tax: 0.0}
|
||||
|
||||
line_items.each do |line_item|
|
||||
totals[:items] += line_item.quantity
|
||||
totals[:items_total] += line_item.amount
|
||||
|
||||
sales_tax = tax_included_in line_item
|
||||
|
||||
if sales_tax > 0
|
||||
totals[:taxable_total] += line_item.amount
|
||||
totals[:sales_tax] += sales_tax
|
||||
end
|
||||
end
|
||||
|
||||
totals.each_pair do |k, v|
|
||||
totals[k] = totals[k].round(2)
|
||||
end
|
||||
|
||||
totals
|
||||
end
|
||||
|
||||
def shipping_cost_for(order)
|
||||
shipping_cost = order.adjustments.find_by_label("Shipping").andand.amount
|
||||
shipping_cost = shipping_cost.nil? ? 0.0 : shipping_cost
|
||||
end
|
||||
|
||||
def shipping_tax_on(shipping_cost)
|
||||
if shipment_inc_vat && shipping_cost.present?
|
||||
(shipping_cost * shipping_tax_rate / (1 + shipping_tax_rate)).round(2)
|
||||
else
|
||||
0
|
||||
end
|
||||
end
|
||||
|
||||
def tax_included_in(line_item)
|
||||
line_item.adjustments.included_tax.sum &:amount
|
||||
end
|
||||
|
||||
def shipment_inc_vat
|
||||
Spree::Config.shipment_inc_vat
|
||||
end
|
||||
|
||||
def shipping_tax_rate
|
||||
Spree::Config.shipping_tax_rate
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -2,6 +2,9 @@ require 'spec_helper'
|
||||
|
||||
feature "Authentication", js: true do
|
||||
include UIComponentHelper
|
||||
include AuthenticationWorkflow
|
||||
include WebHelper
|
||||
|
||||
let(:user) { create(:user, password: "password", password_confirmation: "password") }
|
||||
|
||||
scenario "logging into admin redirects home, then back to admin" do
|
||||
@@ -16,4 +19,10 @@ feature "Authentication", js: true do
|
||||
page.should have_content "Dashboard"
|
||||
current_path.should == spree.admin_path
|
||||
end
|
||||
|
||||
scenario "viewing my account" do
|
||||
login_to_admin_section
|
||||
click_link "Account"
|
||||
current_path.should == spree.account_path
|
||||
end
|
||||
end
|
||||
|
||||
@@ -6,9 +6,12 @@ feature %q{
|
||||
}, js: true do
|
||||
include AuthenticationWorkflow
|
||||
include WebHelper
|
||||
|
||||
|
||||
let!(:tax_category_gst) { create(:tax_category, name: 'GST') }
|
||||
let!(:tax_category_gst_exempt) { create(:tax_category, name: 'GST exempt') }
|
||||
|
||||
scenario "listing enterprise fees" do
|
||||
fee = create(:enterprise_fee, name: '$0.50 / kg', fee_type: 'packing')
|
||||
fee = create(:enterprise_fee, name: '$0.50 / kg', fee_type: 'packing', tax_category: tax_category_gst)
|
||||
amount = fee.calculator.preferred_amount
|
||||
|
||||
login_to_admin_section
|
||||
@@ -18,6 +21,7 @@ feature %q{
|
||||
page.should have_selector "#enterprise_fee_set_collection_attributes_0_enterprise_id"
|
||||
page.should have_selector "option[selected]", text: 'Packing'
|
||||
page.should have_selector "input[value='$0.50 / kg']"
|
||||
page.should have_selector "option[selected]", text: 'GST'
|
||||
page.should have_selector "option[selected]", text: 'Flat Rate (per item)'
|
||||
page.should have_selector "input[value='#{amount}']"
|
||||
end
|
||||
@@ -35,6 +39,7 @@ feature %q{
|
||||
select 'Feedme', from: 'enterprise_fee_set_collection_attributes_0_enterprise_id'
|
||||
select 'Admin', from: 'enterprise_fee_set_collection_attributes_0_fee_type'
|
||||
fill_in 'enterprise_fee_set_collection_attributes_0_name', with: 'Hello!'
|
||||
select 'GST', from: 'enterprise_fee_set_collection_attributes_0_tax_category_id'
|
||||
select 'Flat Percent', from: 'enterprise_fee_set_collection_attributes_0_calculator_type'
|
||||
click_button 'Update'
|
||||
|
||||
@@ -64,6 +69,7 @@ feature %q{
|
||||
select 'Foo', from: 'enterprise_fee_set_collection_attributes_0_enterprise_id'
|
||||
select 'Admin', from: 'enterprise_fee_set_collection_attributes_0_fee_type'
|
||||
fill_in 'enterprise_fee_set_collection_attributes_0_name', with: 'Greetings!'
|
||||
select 'GST exempt', from: 'enterprise_fee_set_collection_attributes_0_tax_category_id'
|
||||
select 'Flat Percent', from: 'enterprise_fee_set_collection_attributes_0_calculator_type'
|
||||
click_button 'Update'
|
||||
|
||||
@@ -71,6 +77,7 @@ feature %q{
|
||||
page.should have_selector "option[selected]", text: 'Foo'
|
||||
page.should have_selector "option[selected]", text: 'Admin'
|
||||
page.should have_selector "input[value='Greetings!']"
|
||||
page.should have_selector "option[selected]", text: 'GST exempt'
|
||||
page.should have_selector "option[selected]", text: 'Flat Percent'
|
||||
end
|
||||
|
||||
@@ -137,6 +144,7 @@ feature %q{
|
||||
|
||||
select distributor1.name, :from => 'enterprise_fee_set_collection_attributes_0_enterprise_id'
|
||||
fill_in 'enterprise_fee_set_collection_attributes_0_name', :with => 'foo'
|
||||
select 'GST', from: 'enterprise_fee_set_collection_attributes_0_tax_category_id'
|
||||
select 'Flat Percent', :from => 'enterprise_fee_set_collection_attributes_0_calculator_type'
|
||||
click_button 'Update'
|
||||
|
||||
|
||||
@@ -98,6 +98,63 @@ feature %q{
|
||||
|
||||
page.should have_content 'Payment State'
|
||||
end
|
||||
|
||||
describe "Sales tax report" do
|
||||
let(:user1) do
|
||||
create_enterprise_user(enterprises: [create(:distributor_enterprise)])
|
||||
end
|
||||
let(:user2) do
|
||||
create_enterprise_user(enterprises: [create(:distributor_enterprise)])
|
||||
end
|
||||
let(:tax_category1) { create(:tax_category) }
|
||||
let(:tax_category2) { create(:tax_category) }
|
||||
let!(:tax_rate1) { create(:tax_rate, amount: 0.0, calculator: Spree::Calculator::DefaultTax.new, tax_category: tax_category1) }
|
||||
let!(:tax_rate2) { create(:tax_rate, amount: 0.2, calculator: Spree::Calculator::DefaultTax.new, tax_category: tax_category2) }
|
||||
|
||||
let(:product1) { create(:product, price: 12.54, tax_category: tax_category1) }
|
||||
let(:product2) { create(:product, price: 500.15, tax_category: tax_category2) }
|
||||
|
||||
let(:shipping_method) { create(:shipping_method, name: "Shipping", description: "Expensive", calculator: Spree::Calculator::FlatRate.new(preferred_amount: 100.55)) }
|
||||
let(:order1) { create(:order, distributor: user1.enterprises.first, shipping_method: shipping_method, bill_address: create(:address)) }
|
||||
let!(:line_item1) { create(:line_item, variant: product1.master, price: 12.54, quantity: 1, order: order1) }
|
||||
let!(:line_item2) { create(:line_item, variant: product2.master, price: 500.15, quantity: 3, order: order1) }
|
||||
|
||||
let!(:adj_shipping) { create(:adjustment, adjustable: order1, label: "Shipping", amount: 100.55) }
|
||||
let!(:adj_li2_tax) { create(:adjustment, adjustable: line_item2, source: line_item2, originator: tax_rate2, label: "RandomTax", amount: 123.00) }
|
||||
|
||||
before do
|
||||
Spree::Config.shipment_inc_vat = true
|
||||
Spree::Config.shipping_tax_rate = 0.2
|
||||
order1.finalize!
|
||||
|
||||
login_to_admin_as user1
|
||||
click_link "Reports"
|
||||
click_link "Sales Tax"
|
||||
end
|
||||
|
||||
it "reports" do
|
||||
# Then it should give me access only to managed enterprises
|
||||
page.should have_select 'q_distributor_id_eq', with_options: [user1.enterprises.first.name]
|
||||
page.should_not have_select 'q_distributor_id_eq', with_options: [user2.enterprises.first.name]
|
||||
|
||||
# When I filter to just one distributor
|
||||
select user1.enterprises.first.name, from: 'q_distributor_id_eq'
|
||||
click_button 'Search'
|
||||
|
||||
# Then I should see the relevant order
|
||||
page.should have_content "#{order1.number}"
|
||||
|
||||
# And the totals and sales tax should be correct
|
||||
page.should have_content "1512.99" # items total
|
||||
page.should have_content "1500.45" # taxable items total
|
||||
page.should have_content "123.0" # sales tax (from adj_li2_tax, not calculated on the fly)
|
||||
page.should_not have_content "250.08" # the number that would have been calculated on the fly
|
||||
|
||||
# And the shipping cost and tax should be correct
|
||||
page.should have_content "100.55" # shipping cost
|
||||
page.should have_content "16.76" # shipping tax # TODO: do not calculate on the fly
|
||||
end
|
||||
end
|
||||
|
||||
describe "orders & fulfilment reports" do
|
||||
it "loads the report page" do
|
||||
|
||||
@@ -65,4 +65,33 @@ module OpenFoodNetwork
|
||||
efa.send(:order_adjustment_label).should == "Whole order - packing fee by distributor Ballantyne"
|
||||
end
|
||||
end
|
||||
|
||||
describe "ensuring that tax rate is marked as tax included_in_price" do
|
||||
let(:efa) { EnterpriseFeeApplicator.new nil, nil, nil }
|
||||
let(:tax_rate) { create(:tax_rate, included_in_price: false, calculator: Spree::Calculator::DefaultTax.new) }
|
||||
|
||||
it "sets included_in_price to true" do
|
||||
efa.send(:with_tax_included_in_price, tax_rate) do
|
||||
tax_rate.included_in_price.should be_true
|
||||
end
|
||||
end
|
||||
|
||||
it "sets the included_in_price value accessible to the calculator to true" do
|
||||
efa.send(:with_tax_included_in_price, tax_rate) do
|
||||
tax_rate.calculator.calculable.included_in_price.should be_true
|
||||
end
|
||||
end
|
||||
|
||||
it "passes through the return value of the block" do
|
||||
efa.send(:with_tax_included_in_price, tax_rate) do
|
||||
'asdf'
|
||||
end.should == 'asdf'
|
||||
end
|
||||
|
||||
it "restores both values to their original afterwards" do
|
||||
efa.send(:with_tax_included_in_price, tax_rate) {}
|
||||
tax_rate.included_in_price.should be_false
|
||||
tax_rate.calculator.calculable.included_in_price.should be_false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
79
spec/lib/open_food_network/sales_tax_report_spec.rb
Normal file
79
spec/lib/open_food_network/sales_tax_report_spec.rb
Normal file
@@ -0,0 +1,79 @@
|
||||
require 'open_food_network/sales_tax_report'
|
||||
|
||||
module OpenFoodNetwork
|
||||
describe SalesTaxReport do
|
||||
let(:report) { SalesTaxReport.new(nil) }
|
||||
|
||||
describe "calculating totals for line items" do
|
||||
let(:li1) { double(:line_item, quantity: 1, amount: 12) }
|
||||
let(:li2) { double(:line_item, quantity: 2, amount: 24) }
|
||||
let(:totals) { report.send(:totals_of, [li1, li2]) }
|
||||
|
||||
before do
|
||||
report.stub(:tax_included_in).and_return(2, 4)
|
||||
end
|
||||
|
||||
it "calculates total quantity" do
|
||||
totals[:items].should == 3
|
||||
end
|
||||
|
||||
it "calculates total price" do
|
||||
totals[:items_total].should == 36
|
||||
end
|
||||
|
||||
context "when floating point math would result in fractional cents" do
|
||||
let(:li1) { double(:line_item, quantity: 1, amount: 0.11) }
|
||||
let(:li2) { double(:line_item, quantity: 2, amount: 0.12) }
|
||||
|
||||
it "rounds to the nearest cent" do
|
||||
totals[:items_total].should == 0.23
|
||||
end
|
||||
end
|
||||
|
||||
it "calculates the taxable total price" do
|
||||
totals[:taxable_total].should == 36
|
||||
end
|
||||
|
||||
it "calculates sales tax" do
|
||||
totals[:sales_tax].should == 6
|
||||
end
|
||||
|
||||
context "when there is no tax on a line item" do
|
||||
before do
|
||||
report.stub(:tax_included_in) { 0 }
|
||||
end
|
||||
|
||||
it "does not appear in taxable total" do
|
||||
totals[:taxable_total].should == 0
|
||||
end
|
||||
|
||||
it "still appears on items total" do
|
||||
totals[:items_total].should == 36
|
||||
end
|
||||
|
||||
it "does not register sales tax" do
|
||||
totals[:sales_tax].should == 0
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "calculating the shipping tax on a shipping cost" do
|
||||
it "returns zero when shipping does not include VAT" do
|
||||
report.stub(:shipment_inc_vat) { false }
|
||||
report.send(:shipping_tax_on, 12).should == 0
|
||||
end
|
||||
|
||||
it "returns zero when no shipping cost is passed" do
|
||||
report.stub(:shipment_inc_vat) { true }
|
||||
report.send(:shipping_tax_on, nil).should == 0
|
||||
end
|
||||
|
||||
|
||||
it "returns the tax included in the price otherwise" do
|
||||
report.stub(:shipment_inc_vat) { true }
|
||||
report.stub(:shipping_tax_rate) { 0.2 }
|
||||
report.send(:shipping_tax_on, 12).should == 2
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -350,7 +350,7 @@ module Spree
|
||||
end
|
||||
|
||||
it "should be able to read some reports" do
|
||||
should have_ability([:admin, :index, :customers, :group_buys, :bulk_coop, :payments, :orders_and_distributors, :orders_and_fulfillment, :products_and_inventory], for: :report)
|
||||
should have_ability([:admin, :index, :customers, :sales_tax, :group_buys, :bulk_coop, :payments, :orders_and_distributors, :orders_and_fulfillment, :products_and_inventory], for: :report)
|
||||
end
|
||||
|
||||
it "should not be able to read other reports" do
|
||||
|
||||
@@ -4,5 +4,150 @@ module Spree
|
||||
adjustment = create(:adjustment, metadata: create(:adjustment_metadata))
|
||||
adjustment.metadata.should be
|
||||
end
|
||||
|
||||
describe "recording included tax" do
|
||||
describe "TaxRate adjustments" do
|
||||
let!(:zone) { create(:zone, default_tax: true) }
|
||||
let!(:zone_member) { ZoneMember.create!(zone: zone, zoneable: Country.find_by_name('Australia')) }
|
||||
let!(:order) { create(:order) }
|
||||
let!(:line_item) { create(:line_item, order: order) }
|
||||
let(:tax_rate) { create(:tax_rate, included_in_price: true, calculator: Calculator::FlatRate.new(preferred_amount: 0.1)) }
|
||||
let(:adjustment) { line_item.adjustments(:reload).first }
|
||||
|
||||
before do
|
||||
order.reload
|
||||
tax_rate.adjust(order)
|
||||
end
|
||||
|
||||
it "has 100% tax included" do
|
||||
adjustment.amount.should be > 0
|
||||
adjustment.included_tax.should == adjustment.amount
|
||||
end
|
||||
end
|
||||
|
||||
describe "Shipment adjustments" do
|
||||
let!(:order) { create(:order, shipping_method: shipping_method) }
|
||||
let!(:line_item) { create(:line_item, order: order) }
|
||||
let(:shipping_method) { create(:shipping_method, calculator: Calculator::FlatRate.new(preferred_amount: 50.0)) }
|
||||
let(:adjustment) { order.adjustments(:reload).shipping.first }
|
||||
|
||||
it "has a shipping charge of $50" do
|
||||
order.create_shipment!
|
||||
adjustment.amount.should == 50
|
||||
end
|
||||
|
||||
describe "when tax on shipping is disabled" do
|
||||
it "records 0% tax on shipment adjustments" do
|
||||
Config.shipment_inc_vat = false
|
||||
Config.shipping_tax_rate = 0
|
||||
order.create_shipment!
|
||||
|
||||
adjustment.included_tax.should == 0
|
||||
end
|
||||
|
||||
it "records 0% tax on shipments when a rate is set but shipment_inc_vat is false" do
|
||||
Config.shipment_inc_vat = false
|
||||
Config.shipping_tax_rate = 0.25
|
||||
order.create_shipment!
|
||||
|
||||
adjustment.included_tax.should == 0
|
||||
end
|
||||
end
|
||||
|
||||
describe "when tax on shipping is enabled" do
|
||||
before do
|
||||
Config.shipment_inc_vat = true
|
||||
Config.shipping_tax_rate = 0.25
|
||||
order.create_shipment!
|
||||
end
|
||||
|
||||
it "takes the shipment adjustment tax included from the system setting" do
|
||||
# Finding the tax included in an amount that's already inclusive of tax:
|
||||
# total - ( total / (1 + rate) )
|
||||
# 50 - ( 50 / (1 + 0.25) )
|
||||
# = 10
|
||||
adjustment.included_tax.should == 10.00
|
||||
end
|
||||
|
||||
it "records 0% tax on shipments when shipping_tax_rate is not set" do
|
||||
Config.shipment_inc_vat = true
|
||||
Config.shipping_tax_rate = nil
|
||||
order.create_shipment!
|
||||
|
||||
adjustment.included_tax.should == 0
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "EnterpriseFee adjustments" do
|
||||
let!(:zone) { create(:zone, default_tax: true) }
|
||||
let!(:zone_member) { ZoneMember.create!(zone: zone, zoneable: Country.find_by_name('Australia')) }
|
||||
let(:tax_rate) { create(:tax_rate, included_in_price: true, calculator: Calculator::DefaultTax.new, zone: zone, amount: 0.1) }
|
||||
let(:tax_category) { create(:tax_category, tax_rates: [tax_rate]) }
|
||||
|
||||
let(:coordinator) { create(:distributor_enterprise) }
|
||||
let(:variant) { create(:variant) }
|
||||
let(:order_cycle) { create(:simple_order_cycle, coordinator: coordinator, coordinator_fees: [enterprise_fee], distributors: [coordinator], variants: [variant]) }
|
||||
let!(:order) { create(:order, order_cycle: order_cycle, distributor: coordinator) }
|
||||
let!(:line_item) { create(:line_item, order: order, variant: variant) }
|
||||
let(:adjustment) { order.adjustments(:reload).enterprise_fee.first }
|
||||
|
||||
before do
|
||||
order.reload.update_distribution_charge!
|
||||
end
|
||||
|
||||
context "when enterprise fees are taxed per-order" do
|
||||
let(:enterprise_fee) { create(:enterprise_fee, enterprise: coordinator, tax_category: tax_category, calculator: Calculator::FlatRate.new(preferred_amount: 50.0)) }
|
||||
|
||||
it "records the tax on the enterprise fee adjustments" do
|
||||
# The fee is $50, tax is 10%, and the fee is inclusive of tax
|
||||
# Therefore, the included tax should be 0.1/1.1 * 50 = $4.55
|
||||
|
||||
adjustment.included_tax.should == 4.55
|
||||
end
|
||||
|
||||
describe "when the tax rate does not include the tax in the price" do
|
||||
before do
|
||||
tax_rate.update_attribute :included_in_price, false
|
||||
order.update_distribution_charge!
|
||||
end
|
||||
|
||||
it "treats it as inclusive anyway" do
|
||||
adjustment.included_tax.should == 4.55
|
||||
end
|
||||
end
|
||||
|
||||
describe "when enterprise fees have no tax" do
|
||||
before do
|
||||
enterprise_fee.tax_category = nil
|
||||
enterprise_fee.save!
|
||||
order.update_distribution_charge!
|
||||
end
|
||||
|
||||
it "records no tax as charged" do
|
||||
adjustment.included_tax.should == 0
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
context "when enterprise fees are taxed per-item" do
|
||||
let(:enterprise_fee) { create(:enterprise_fee, enterprise: coordinator, tax_category: tax_category, calculator: Calculator::PerItem.new(preferred_amount: 50.0)) }
|
||||
|
||||
it "records the tax on the enterprise fee adjustments" do
|
||||
adjustment.included_tax.should == 4.55
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "setting the included tax by tax rate" do
|
||||
let(:adjustment) { Adjustment.new label: 'foo', amount: 50 }
|
||||
|
||||
it "sets it, rounding to two decimal places" do
|
||||
adjustment.set_included_tax! 0.25
|
||||
adjustment.included_tax.should == 10.00
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user