Merge pull request #7805 from Matt-Yorkley/adjustments-admin

[Adjustments] Admin adjustments
This commit is contained in:
Matt-Yorkley
2021-07-27 12:23:29 +02:00
committed by GitHub
27 changed files with 472 additions and 403 deletions

View File

@@ -17,6 +17,7 @@ class ApplicationController < ActionController::Base
helper 'spree/orders'
helper 'spree/payment_methods'
helper 'shared'
helper 'adjustments'
helper 'enterprises'
helper 'order_cycles'
helper 'order'

View File

@@ -5,18 +5,16 @@ module Spree
class AdjustmentsController < ::Admin::ResourceController
belongs_to 'spree/order', find_by: :number
prepend_before_action :set_included_tax, only: [:create, :update]
before_action :set_order_id, only: [:create, :update]
before_action :skip_changing_canceled_orders, only: [:create, :update]
after_action :update_order, only: [:create, :update, :destroy]
before_action :set_default_tax_rate, only: :edit
before_action :enable_updates, only: :update
after_action :apply_tax, only: [:create, :update]
private
def update_order
@order.reload
@order.update_order!
@order.updater.update_totals_and_states
end
def collection
@@ -43,58 +41,13 @@ module Spree
redirect_to admin_order_adjustments_path(@order) if @order.canceled?
end
# Choose a default tax rate to show on the edit form. The adjustment stores its included
# tax in dollars, but doesn't store the source of the tax (ie. TaxRate that generated it).
# We guess which tax rate here, choosing:
# 1. A tax rate that will compute to the same amount as the existing tax
# 2. If that's not present, the first tax rate that's valid for the current order
# When we have to go with 2, we show an error message to ask the admin to check that the
# correct tax is being applied.
def set_default_tax_rate
return if @adjustment.included_tax <= 0
tax_rates = TaxRate.match(@order)
tax_rate_with_matching_tax = find_tax_rate_with_matching_tax(tax_rates)
tax_rate_valid_for_order = tax_rates.first.andand.id
@tax_rate_id = tax_rate_with_matching_tax || tax_rate_valid_for_order
return unless tax_rate_with_matching_tax.nil?
@adjustment.errors.add :tax_rate_id, I18n.t(:adjustments_tax_rate_error)
end
def find_tax_rate_with_matching_tax(tax_rates)
tax_rates_yielding_matching_tax = tax_rates.select do |tr|
tr.compute_tax(@adjustment.amount) == @adjustment.included_tax
end
tax_rates_yielding_matching_tax.first.andand.id
end
def set_included_tax
included_tax = 0
if params[:tax_rate_id].present?
tax_rate = TaxRate.find params[:tax_rate_id]
amount = params[:adjustment][:amount].to_f
included_tax = tax_rate.compute_tax amount
end
params[:adjustment][:included_tax] = included_tax
end
# Spree 2.0 keeps shipping fee adjustments open unless they are manually
# closed. But open adjustments cannot be edited.
# To preserve updates, like changing the amount of the shipping fee,
# we close the adjustment first.
#
# The Spree admin interface allows to open and close adjustments manually
# but we removed that functionality as it had no purpose for us.
def enable_updates
@adjustment.close
def apply_tax
Spree::TaxRate.adjust(@order, [@adjustment])
end
def permitted_resource_params
params.require(:adjustment).permit(
:label, :amount, :included_tax
:label, :amount, :tax_category_id
)
end
end

View File

@@ -14,9 +14,7 @@ module Spree
end
def edit
country_id = Address.default.country.id
@order.build_bill_address(country_id: country_id) if @order.bill_address.nil?
@order.build_ship_address(country_id: country_id) if @order.ship_address.nil?
build_addresses
end
def update
@@ -25,9 +23,10 @@ module Spree
@order.associate_user!(Spree.user_class.find_by(email: @order.email))
end
refresh_shipment_rates
recalculate_taxes
OrderWorkflow.new(@order).advance_to_payment
@order.shipments.map(&:refresh_rates)
flash[:success] = Spree.t('customer_details_updated')
redirect_to spree.admin_order_customer_path(@order)
else
@@ -43,6 +42,25 @@ module Spree
private
def build_addresses
country_id = Address.default.country.id
@order.build_bill_address(country_id: country_id) if @order.bill_address.nil?
@order.build_ship_address(country_id: country_id) if @order.ship_address.nil?
end
def refresh_shipment_rates
@order.shipments.map(&:refresh_rates)
end
def recalculate_taxes
# If the order's address has been changed, the tax zone could be different,
# which means a different set of tax rates might be applicable.
@order.create_tax_charge!
Spree::TaxRate.adjust(@order, @order.adjustments.admin)
@order.updater.update_totals_and_states
end
def order_params
params.require(:order).permit(
:email,

View File

@@ -0,0 +1,19 @@
# frozen_string_literal: true
module AdjustmentsHelper
def display_adjustment_taxes(adjustment)
if adjustment.included_tax_total > 0
amount = Spree::Money.new(adjustment.included_tax_total, currency: adjustment.currency)
I18n.t(:tax_amount_included, amount: amount)
elsif adjustment.additional_tax_total > 0
Spree::Money.new(adjustment.additional_tax_total, currency: adjustment.currency)
else
Spree::Money.new(0.00, currency: adjustment.currency)
end
end
def display_adjustment_total_with_tax(adjustment)
total = adjustment.amount + adjustment.additional_tax_total
Spree::Money.new(total, currency: adjustment.currency)
end
end

View File

@@ -77,8 +77,6 @@ module Spree
scope :enterprise_fee, -> { where(originator_type: 'EnterpriseFee') }
scope :admin, -> { where(originator_type: nil) }
scope :with_tax, -> { where('spree_adjustments.included_tax <> 0') }
scope :without_tax, -> { where('spree_adjustments.included_tax = 0') }
scope :payment_fee, -> { where(AdjustmentScopes::PAYMENT_FEE_SCOPE) }
scope :shipping, -> { where(AdjustmentScopes::SHIPPING_SCOPE) }
scope :eligible, -> { where(AdjustmentScopes::ELIGIBLE_SCOPE) }
@@ -129,20 +127,24 @@ module Spree
state != "open"
end
def set_absolute_included_tax!(tax)
update! included_tax: tax.round(2)
end
def display_included_tax
Spree::Money.new(included_tax, currency: currency)
end
def has_tax?
included_tax.positive?
tax_total.positive?
end
def included_tax_total
adjustments.tax.inclusive.sum(:amount)
end
def additional_tax_total
adjustments.tax.additional.sum(:amount)
end
private
def tax_total
adjustments.tax.sum(:amount)
end
def update_adjustable_adjustment_total
Spree::ItemAdjustments.new(adjustable).update if adjustable
end

View File

@@ -117,25 +117,6 @@ module Spree
Zone.default_tax&.contains?(order.tax_zone) || order.tax_zone == zone
end
# Manually 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(amount)
line_item = LineItem.new quantity: 1
line_item.tax_category = tax_category
line_item.define_singleton_method(:price) { amount }
# Tax on adjustments (represented by the included_tax field) is 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 do
calculator.compute line_item
end
end
private
def create_label(adjustment_amount)
@@ -146,19 +127,5 @@ module Spree
label << " (#{I18n.t('models.tax_rate.included_in_price')})" if included_in_price?
label
end
def with_tax_included_in_price
old_included_in_price = included_in_price
self.included_in_price = true
calculator.calculable.included_in_price = true
result = yield
ensure
self.included_in_price = old_included_in_price
calculator.calculable.included_in_price = old_included_in_price
result
end
end
end

View File

@@ -1,7 +1,6 @@
# frozen_string_literal: true
# This class will be used to get Tax Adjustments related to an order,
# and proceed basic calcultation over them.
# Collects Tax Adjustments related to an order, and returns a hash with a total for each rate.
class OrderTaxAdjustmentsFetcher
def initialize(order)
@@ -9,47 +8,13 @@ class OrderTaxAdjustmentsFetcher
end
def totals
all.each_with_object({}) do |adjustment, hash|
tax_rates_hash = tax_rates_hash(adjustment)
hash.update(tax_rates_hash) { |_tax_rate, amount1, amount2| amount1 + amount2 }
order.all_adjustments.tax.each_with_object({}) do |adjustment, hash|
tax_rate = adjustment.originator
hash[tax_rate] = hash[tax_rate].to_f + adjustment.amount
end
end
private
attr_reader :order
def all
tax_adjustments = order.all_adjustments.tax
admin_adjustments_with_tax = order.all_adjustments.admin.with_tax
tax_adjustments.or(admin_adjustments_with_tax)
end
def tax_rates_hash(adjustment)
tax_rates = TaxRateFinder.tax_rates_of(adjustment)
Hash[tax_rates.collect do |tax_rate|
tax_amount = if tax_rates.one?
adjustment_tax_amount(adjustment)
else
tax_rate.compute_tax(adjustment.amount)
end
[tax_rate, tax_amount]
end]
end
def adjustment_tax_amount(adjustment)
if no_tax_adjustments?(adjustment)
adjustment.included_tax
else
adjustment.amount
end
end
def no_tax_adjustments?(adjustment)
# Admin Adjustments currently do not have tax adjustments.
# The tax amount is stored in the included_tax attribute.
adjustment.originator_type.nil?
end
end

View File

@@ -6,18 +6,12 @@
class TaxRateFinder
# @return [Array<Spree::TaxRate>]
def self.tax_rates_of(adjustment)
new.tax_rates(
adjustment.originator,
adjustment.adjustable,
adjustment.amount,
adjustment.included_tax
)
new.tax_rates(adjustment.originator, adjustment.adjustable)
end
# @return [Array<Spree::TaxRate>]
def tax_rates(originator, adjustable, amount, included_tax)
find_associated_tax_rate(originator, adjustable) ||
find_closest_tax_rates_from_included_tax(amount, included_tax)
def tax_rates(originator, adjustable)
find_associated_tax_rate(originator, adjustable) || []
end
private
@@ -48,37 +42,4 @@ class TaxRateFinder
enterprise_fee.tax_category
end
end
# There are two cases in which a line item is not associated to a tax rate.
#
# 1. Shipping fees and adjustments created from the admin panel have taxes set
# at creation in the included_tax field without relation to the
# corresponding TaxRate.
# 2. Removing line items from an order doesn't always remove the associated
# enterprise fees. These orphaned fees don't have a line item any more to
# find the item's tax rate.
#
# In these cases we try to find the used tax rate based on the included tax.
# For example, if the included tax is 10% of the adjustment, we look for a tax
# rate of 10%. Due to rounding errors, the included tax may be 9.9% of the
# adjustment. That's why we call it an approximation of the tax rate and look
# for the closest and hopefully find the 10% tax rate.
#
# This attempt can fail.
#
# - If an admin created an adjustment with a miscalculated included tax then
# we don't know which tax rate the admin intended to use.
# - An admin may also enter included tax that doesn't correspond to any tax
# rate in the system. They may enter a fee of $1.2 with tax of $0.2, but
# that doesn't mean that there is a 20% tax rate in the database.
# - The used tax rate may also have been deleted. Maybe the tax law changed.
#
# In either of these cases, we will find a tax rate that doesn't correspond
# to the included tax.
def find_closest_tax_rates_from_included_tax(amount, included_tax)
approximation = (included_tax / (amount - included_tax))
return [] if approximation.infinite? || approximation.zero? || approximation.nan?
[Spree::TaxRate.order(Arel.sql("ABS(amount - #{approximation})")).first]
end
end

View File

@@ -4,20 +4,32 @@
%th= "#{t('spree.date')}/#{t('spree.time')}"
%th= t(:description)
%th= t(:amount)
%th= t(:included_tax)
%th= t(:tax_category)
%th= t(:tax)
%th= t(:total_incl_tax)
%th.actions
%tbody
- @collection.each do |adjustment|
- @edit_url = edit_admin_order_adjustment_path(@order, adjustment)
- @delete_url = admin_order_adjustment_path(@order, adjustment)
- taxable = adjustment.adjustable_type == "Spree::Shipment" ? adjustment.adjustable : adjustment
- tr_class = cycle('odd', 'even')
- tr_id = spree_dom_id(adjustment)
%tr{:class => tr_class, "data-hook" => "adjustment_row", :id => tr_id}
%td.align-center.created_at= pretty_time(adjustment.created_at)
%td.align-center.label= adjustment.label
%td.align-center.amount= adjustment.display_amount.to_html
%td.align-center.included-tax= adjustment.display_included_tax.to_html
%td.align-center.created_at
= pretty_time(adjustment.created_at)
%td.align-center.label
= adjustment.label
%td.align-center.amount
= adjustment.display_amount.to_html
%td.align-center.tax-category
= taxable.tax_category&.name || "-"
%td.align-center.tax
= display_adjustment_taxes(taxable)
%td.align-center.total
= display_adjustment_total_with_tax(taxable)
- unless @order.canceled?
%td.actions
= link_to_edit adjustment, no_text: true
= link_to_delete adjustment, no_text: true
- if adjustment.originator_type.nil?
= link_to_edit adjustment, no_text: true
= link_to_delete adjustment, no_text: true

View File

@@ -6,18 +6,14 @@
= f.error_message_on :amount
- if @adjustment.admin?
.four.columns
= f.field_container :included_tax do
= f.label :included_tax, t(:included_tax)
= f.text_field :included_tax, disabled: true, class: 'fullwidth',
value: number_with_precision(f.object.included_tax, precision: 2)
= f.error_message_on :included_tax
.omega.four.columns
= f.field_container :tax_rate_id do
= f.label :tax_rate_id, t(:tax_rate)
= select_tag :tax_rate_id, options_from_collection_for_select(Spree::TaxRate.all, :id, :name, @tax_rate_id), prompt: t(:remove_tax), class: 'select2 fullwidth'
= f.error_message_on :tax_rate_id
= f.field_container :tax_category do
= f.label :tax_category, t(:tax_category)
= select_tag "adjustment[tax_category_id]",
options_from_collection_for_select(Spree::TaxCategory.all, :id, :name, @adjustment.tax_category_id),
prompt: t(:none),
class: 'select2 fullwidth'
= f.error_message_on :tax_category
.row
.alpha.omega.twelve.columns

View File

@@ -1,15 +1,18 @@
.row
.alpha.three.columns
.alpha.four.columns
= f.field_container :amount do
= f.label :amount, raw(t(:amount) + content_tag(:span, " *", :class => "required"))
= text_field :adjustment, :amount, :class => 'fullwidth'
= f.error_message_on :amount
.omega.three.columns
= f.field_container :tax_rate_id do
= f.label :tax_rate_id, t(:tax_rate)
= select_tag :tax_rate_id, options_from_collection_for_select(Spree::TaxRate.all, :id, :name), prompt: t(:none), class: 'select2 fullwidth'
= f.error_message_on :tax_rate_id
.omega.four.columns
= f.field_container :tax_category do
= f.label :tax_category, t(:tax_category)
= select_tag "adjustment[tax_category_id]",
options_from_collection_for_select(Spree::TaxCategory.all, :id, :name),
prompt: t(:none),
class: 'select2 fullwidth'
= f.error_message_on :tax_category
.row
.alpha.omega.twelve.columns

View File

@@ -30,7 +30,7 @@
%td{:align => "right"}
1
%td{:align => "right"}
= adjustment.included_tax > 0 ? adjustment.display_included_tax : ""
= display_adjustment_taxes(adjustment)
%td{:align => "right"}
= adjustment.display_amount
%tfoot

View File

@@ -2164,6 +2164,8 @@ See the %{link} to find out more about %{sitename}'s features and to start using
fundraising_fee: "Fundraising fee"
price_graph: "Price graph"
included_tax: "Included tax"
tax: "Tax"
tax_amount_included: "%{amount} (included)"
remove_tax: "Remove tax"
balance: "Balance"
transaction: "Transaction"

View File

@@ -0,0 +1,61 @@
class MigrateAdminTaxAmounts < ActiveRecord::Migration[6.0]
class Spree::Adjustment < ApplicationRecord
belongs_to :originator, polymorphic: true
belongs_to :adjustable, polymorphic: true
belongs_to :order, class_name: "Spree::Order"
belongs_to :tax_category, class_name: 'Spree::TaxCategory'
has_many :adjustments, as: :adjustable, dependent: :destroy
scope :admin, -> { where(originator_type: nil) }
end
def up
migrate_admin_taxes!
end
def migrate_admin_taxes!
Spree::Adjustment.admin.where('included_tax <> 0').includes(:order).find_each do |adjustment|
tax_rate = find_tax_rate(adjustment)
tax_category = tax_rate&.tax_category
label = tax_adjustment_label(tax_rate)
adjustment.update_columns(tax_category_id: tax_category.id) if tax_category.present?
Spree::Adjustment.create!(
label: label,
amount: adjustment.included_tax,
order_id: adjustment.order_id,
adjustable: adjustment,
originator_type: "Spree::TaxRate",
originator_id: tax_rate&.id,
state: "closed",
included: true
)
end
end
def find_tax_rate(adjustment)
amount = adjustment.amount
included_tax = adjustment.included_tax
approximation = (included_tax / (amount - included_tax))
return if approximation.infinite? || approximation.zero? || approximation.nan?
applicable_rates(adjustment).min_by{ |rate| (rate.amount - approximation).abs }
end
def applicable_rates(adjustment)
return [] unless adjustment.order&.distributor_id.present?
Spree::TaxRate.match(adjustment.order)
end
def tax_adjustment_label(tax_rate)
if tax_rate.nil?
I18n.t('included_tax')
else
"#{tax_rate.name} #{tax_rate.amount * 100}% (#{I18n.t('models.tax_rate.included_in_price')})"
end
end
end

View File

@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2021_05_27_201938) do
ActiveRecord::Schema.define(version: 2021_06_17_203927) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"

View File

@@ -67,8 +67,7 @@ module OrderManagement
def update_adjustment_total
order.adjustment_total = all_adjustments.additional.eligible.sum(:amount)
order.additional_tax_total = all_adjustments.tax.additional.sum(:amount)
order.included_tax_total = all_adjustments.tax.inclusive.sum(:amount) +
adjustments.admin.sum(:included_tax)
order.included_tax_total = all_adjustments.tax.inclusive.sum(:amount)
end
def update_order_total

View File

@@ -34,12 +34,11 @@ module OrderManagement
:sum).and_return(20)
allow(order).to receive_message_chain(:all_adjustments, :tax, :inclusive,
:sum).and_return(15)
allow(order).to receive_message_chain(:adjustments, :admin, :sum).and_return(2)
updater.update_adjustment_total
expect(order.adjustment_total).to eq(-5)
expect(order.additional_tax_total).to eq(20)
expect(order.included_tax_total).to eq(17)
expect(order.included_tax_total).to eq(15)
end
end

View File

@@ -219,11 +219,11 @@ module OpenFoodNetwork
end
def total_untaxable_admin_adjustments(order)
order.adjustments.admin.without_tax.sum(:amount)
order.adjustments.admin.where(tax_category: nil).sum(:amount)
end
def total_taxable_admin_adjustments(order)
order.adjustments.admin.with_tax.sum(:amount)
order.adjustments.admin.where.not(tax_category: nil).sum(:amount)
end
def detail?

View File

@@ -43,71 +43,146 @@ module Spree
end
end
describe "setting included tax" do
describe "setting the adjustment's tax" do
let(:order) { create(:order) }
let(:tax_rate) { create(:tax_rate, amount: 0.1, calculator: ::Calculator::DefaultTax.new) }
let(:zone) { create(:zone_with_member) }
let(:tax_rate) { create(:tax_rate, amount: 0.1, zone: zone, included_in_price: true ) }
describe "creating an adjustment" do
it "sets included tax to zero when no tax rate is specified" do
spree_post :create, order_id: order.number,
adjustment: { label: 'Testing included tax', amount: '110' }, tax_rate_id: ''
expect(response).to redirect_to spree.admin_order_adjustments_path(order)
let(:tax_category_param) { '' }
let(:params) {
{
order_id: order.number,
adjustment: {
label: 'Testing included tax', amount: '110', tax_category_id: tax_category_param
}
}
}
a = Adjustment.last
expect(a.label).to eq('Testing included tax')
expect(a.amount).to eq(110)
expect(a.included_tax).to eq(0)
expect(a.order_id).to eq(order.id)
context "when no tax category is specified" do
it "doesn't apply tax" do
spree_post :create, params
expect(response).to redirect_to spree.admin_order_adjustments_path(order)
expect(order.reload.total).to eq 110
new_adjustment = Adjustment.admin.last
expect(new_adjustment.label).to eq('Testing included tax')
expect(new_adjustment.amount).to eq(110)
expect(new_adjustment.tax_category).to be_nil
expect(new_adjustment.order_id).to eq(order.id)
expect(order.reload.total).to eq 110
expect(order.included_tax_total).to eq 0
end
end
it "calculates included tax when a tax rate is provided" do
spree_post :create, order_id: order.number,
adjustment: { label: 'Testing included tax', amount: '110' }, tax_rate_id: tax_rate.id.to_s
expect(response).to redirect_to spree.admin_order_adjustments_path(order)
context "when a tax category is provided" do
let(:tax_category_param) { tax_rate.tax_category.id.to_s }
a = Adjustment.last
expect(a.label).to eq('Testing included tax')
expect(a.amount).to eq(110)
expect(a.included_tax).to eq(10)
expect(a.order_id).to eq(order.id)
it "applies tax" do
spree_post :create, params
expect(response).to redirect_to spree.admin_order_adjustments_path(order)
expect(order.reload.total).to eq 110
new_adjustment = Adjustment.admin.last
expect(new_adjustment.label).to eq('Testing included tax')
expect(new_adjustment.amount).to eq(110)
expect(new_adjustment.tax_category).to eq tax_rate.tax_category
expect(new_adjustment.order_id).to eq(order.id)
expect(order.reload.total).to eq 110
expect(order.included_tax_total).to eq 10
end
end
context "when the tax category has multiple rates for the same tax zone" do
let(:tax_category) { create(:tax_category) }
let!(:tax_rate1) {
create(:tax_rate, amount: 0.1, zone: zone, included_in_price: false,
tax_category: tax_category )
}
let!(:tax_rate2) {
create(:tax_rate, amount: 0.2, zone: zone, included_in_price: false,
tax_category: tax_category )
}
let(:tax_category_param) { tax_category.id.to_s }
let(:params) {
{
order_id: order.number,
adjustment: {
label: 'Testing multiple rates', amount: '100', tax_category_id: tax_category_param
}
}
}
it "applies both rates" do
spree_post :create, params
expect(response).to redirect_to spree.admin_order_adjustments_path(order)
new_adjustment = Adjustment.admin.last
expect(new_adjustment.amount).to eq(100)
expect(new_adjustment.tax_category).to eq tax_category
expect(new_adjustment.order_id).to eq(order.id)
expect(new_adjustment.adjustments.tax.count).to eq 2
expect(order.reload.total).to eq 130
expect(order.additional_tax_total).to eq 30
end
end
end
describe "updating an adjustment" do
let(:old_tax_category) { create(:tax_category) }
let(:tax_category_param) { '' }
let(:params) {
{
id: adjustment.id,
order_id: order.number,
adjustment: {
label: 'Testing included tax', amount: '110', tax_category_id: tax_category_param
}
}
}
let(:adjustment) {
create(:adjustment, adjustable: order, order: order, amount: 1100, included_tax: 100)
create(:adjustment, adjustable: order, order: order,
amount: 1100, tax_category: old_tax_category)
}
it "sets included tax to zero when no tax rate is specified" do
spree_put :update, order_id: order.number, id: adjustment.id,
adjustment: { label: 'Testing included tax', amount: '110' }, tax_rate_id: ''
expect(response).to redirect_to spree.admin_order_adjustments_path(order)
context "when no tax category is specified" do
it "doesn't apply tax" do
spree_put :update, params
expect(response).to redirect_to spree.admin_order_adjustments_path(order)
a = Adjustment.last
expect(a.label).to eq('Testing included tax')
expect(a.amount).to eq(110)
expect(a.included_tax).to eq(0)
expect(a.order_id).to eq(order.id)
adjustment = Adjustment.admin.last
expect(order.reload.total).to eq 110
expect(adjustment.label).to eq('Testing included tax')
expect(adjustment.amount).to eq(110)
expect(adjustment.tax_category).to be_nil
expect(adjustment.order_id).to eq(order.id)
expect(order.reload.total).to eq 110
expect(order.included_tax_total).to eq 0
end
end
it "calculates included tax when a tax rate is provided" do
spree_put :update, order_id: order.number, id: adjustment.id,
adjustment: { label: 'Testing included tax', amount: '110' }, tax_rate_id: tax_rate.id.to_s
expect(response).to redirect_to spree.admin_order_adjustments_path(order)
context "when a tax category is provided" do
let(:tax_category_param) { tax_rate.tax_category.id.to_s }
a = Adjustment.last
expect(a.label).to eq('Testing included tax')
expect(a.amount).to eq(110)
expect(a.included_tax).to eq(10)
expect(a.order_id).to eq(order.id)
it "applies tax" do
spree_put :update, params
expect(response).to redirect_to spree.admin_order_adjustments_path(order)
expect(order.reload.total).to eq 110
adjustment = Adjustment.admin.last
expect(adjustment.label).to eq('Testing included tax')
expect(adjustment.amount).to eq(110)
expect(adjustment.tax_category).to eq tax_rate.tax_category
expect(adjustment.order_id).to eq(order.id)
expect(order.reload.total).to eq 110
expect(order.included_tax_total).to eq 10
end
end
end
end
@@ -151,7 +226,7 @@ module Spree
let(:order) { create(:completed_order_with_totals) }
let(:tax_rate) { create(:tax_rate, amount: 0.1, calculator: ::Calculator::DefaultTax.new) }
let(:adjustment) {
create(:adjustment, adjustable: order, order: order, amount: 1100, included_tax: 100)
create(:adjustment, adjustable: order, order: order, amount: 1100)
}
before do
@@ -161,7 +236,7 @@ module Spree
it "doesn't create adjustments" do
expect {
spree_post :create, order_id: order.number,
adjustment: { label: "Testing", amount: "110" }, tax_rate_id: ""
adjustment: { label: "Testing", amount: "110" }
}.to_not change { [Adjustment.count, order.reload.total] }
expect(response).to redirect_to spree.admin_order_adjustments_path(order)
@@ -170,7 +245,7 @@ module Spree
it "doesn't change adjustments" do
expect {
spree_put :update, order_id: order.number, id: adjustment.id,
adjustment: { label: "Testing", amount: "110" }, tax_rate_id: ""
adjustment: { label: "Testing", amount: "110" }
}.to_not change { [adjustment.reload.amount, order.reload.total] }
expect(response).to redirect_to spree.admin_order_adjustments_path(order)

View File

@@ -17,9 +17,10 @@ feature '
create(:order_with_totals_and_distribution, user: user, distributor: distributor,
order_cycle: order_cycle, state: 'complete', payment_state: 'balance_due')
}
let!(:tax_category) { create(:tax_category, name: 'GST') }
let!(:tax_rate) {
create(:tax_rate, name: 'GST', calculator: build(:calculator, preferred_amount: 10),
zone: create(:zone_with_member))
zone: create(:zone_with_member), tax_category: tax_category)
}
before do
@@ -37,19 +38,19 @@ feature '
click_link 'New Adjustment'
fill_in 'adjustment_amount', with: 110
fill_in 'adjustment_label', with: 'Late fee'
select2_select 'GST', from: 'tax_rate_id'
select2_select 'GST', from: 'adjustment_tax_category_id'
click_button 'Continue'
# Then I should see the adjustment, with the correct tax
expect(page).to have_selector 'td.label', text: 'Late fee'
expect(page).to have_selector 'td.amount', text: '110'
expect(page).to have_selector 'td.included-tax', text: '10'
expect(page).to have_selector 'td.amount', text: '110.00'
expect(page).to have_selector 'td.tax', text: '10.00'
end
scenario "modifying taxed adjustments on an order" do
# Given a taxed adjustment
adjustment = create(:adjustment, label: "Extra Adjustment", adjustable: order,
amount: 110, included_tax: 10, order: order)
amount: 110, tax_category: tax_category, order: order)
# When I go to the adjustments page for the order
login_as_admin_and_visit spree.admin_orders_path
@@ -57,23 +58,21 @@ feature '
click_link 'Adjustments'
page.find('tr', text: 'Extra Adjustment').find('a.icon-edit').click
# Then I should see the uneditable included tax and our tax rate as the default
expect(page).to have_field :adjustment_included_tax, with: '10.00', disabled: true
expect(page).to have_select2 :tax_rate_id, selected: 'GST'
expect(page).to have_select2 :adjustment_tax_category_id, selected: 'GST'
# When I edit the adjustment, removing the tax
select2_select 'Remove tax', from: :tax_rate_id
select2_select 'None', from: :adjustment_tax_category_id
click_button 'Continue'
# Then the adjustment tax should be cleared
expect(page).to have_selector 'td.amount', text: '110'
expect(page).to have_selector 'td.included-tax', text: '0'
expect(page).to have_selector 'td.amount', text: '110.00'
expect(page).to have_selector 'td.tax', text: '0.00'
end
scenario "modifying an untaxed adjustment on an order" do
# Given an untaxed adjustment
adjustment = create(:adjustment, label: "Extra Adjustment", adjustable: order,
amount: 110, included_tax: 0, order: order)
amount: 110, tax_category: nil, order: order)
# When I go to the adjustments page for the order
login_as_admin_and_visit spree.admin_orders_path
@@ -81,23 +80,21 @@ feature '
click_link 'Adjustments'
page.find('tr', text: 'Extra Adjustment').find('a.icon-edit').click
# Then I should see the uneditable included tax and 'Remove tax' as the default tax rate
expect(page).to have_field :adjustment_included_tax, with: '0.00', disabled: true
expect(page).to have_select2 :tax_rate_id, selected: []
expect(page).to have_select2 :adjustment_tax_category_id, selected: []
# When I edit the adjustment, setting a tax rate
select2_select 'GST', from: :tax_rate_id
select2_select 'GST', from: :adjustment_tax_category_id
click_button 'Continue'
# Then the adjustment tax should be recalculated
expect(page).to have_selector 'td.amount', text: '110'
expect(page).to have_selector 'td.included-tax', text: '10'
expect(page).to have_selector 'td.amount', text: '110.00'
expect(page).to have_selector 'td.tax', text: '10.00'
end
scenario "viewing adjustments on a canceled order" do
# Given a taxed adjustment
adjustment = create(:adjustment, label: "Extra Adjustment", adjustable: order,
amount: 110, included_tax: 10, order: order)
amount: 110, tax_category: tax_category, order: order)
order.cancel!
login_as_admin_and_visit spree.edit_admin_order_path(order)

View File

@@ -409,15 +409,14 @@ feature '
expect(page).to have_content test_tracking_number
end
scenario "editing shipping fees" do
scenario "viewing shipping fees" do
shipping_fee = order.shipment_adjustments.first
click_link "Adjustments"
shipping_adjustment_tr_selector = "tr#spree_adjustment_#{order.shipment_adjustments.first.id}"
page.find("#{shipping_adjustment_tr_selector} td.actions a.icon-edit").click
fill_in "Amount", with: "5"
click_button "Continue"
expect(page.find("#{shipping_adjustment_tr_selector} td.amount")).to have_content "5.00"
expect(page).to have_selector "tr#spree_adjustment_#{shipping_fee.id}"
expect(page).to have_selector 'td.amount', text: shipping_fee.amount.to_s
expect(page).to have_selector 'td.tax', text: shipping_fee.included_tax_total.to_s
end
context "when an included variant has been deleted" do

View File

@@ -504,13 +504,13 @@ feature '
create(:adjustment, order: order1, adjustable: adj_fee2, originator: tax_rate, amount: 3,
state: "closed")
}
let!(:adj_manual1) {
let!(:adj_admin1) {
create(:adjustment, order: order1, adjustable: order1, originator: nil,
label: "Manual adjustment", amount: 30)
}
let!(:adj_manual2) {
let!(:adj_admin2) {
create(:adjustment, order: order1, adjustable: order1, originator: nil,
label: "Manual adjustment", amount: 40, included_tax: 3)
label: "Manual adjustment", amount: 40, tax_category: tax_category)
}
before do
@@ -590,8 +590,8 @@ feature '
xero_invoice_header,
xero_invoice_li_row(line_item1),
xero_invoice_li_row(line_item2),
xero_invoice_adjustment_row(adj_manual1),
xero_invoice_adjustment_row(adj_manual2),
xero_invoice_adjustment_row(adj_admin1),
xero_invoice_adjustment_row(adj_admin2),
xero_invoice_summary_row('Total untaxable fees (no tax)', 10.0,
'GST Free Income', opts),
xero_invoice_summary_row('Total taxable fees (tax inclusive)', 20.0,

View File

@@ -0,0 +1,129 @@
# frozen_string_literal: true
require 'spec_helper'
require_relative '../../db/migrate/20210617203927_migrate_admin_tax_amounts'
describe MigrateAdminTaxAmounts do
subject { MigrateAdminTaxAmounts.new }
let(:tax_category10) { create(:tax_category) }
let(:tax_category50) { create(:tax_category) }
let!(:tax_rate10) { create(:tax_rate, amount: 0.1, tax_category: tax_category10) }
let!(:tax_rate50) { create(:tax_rate, amount: 0.5, tax_category: tax_category50) }
let(:adjustment10) { create(:adjustment, amount: 100, included_tax: 10) }
let(:adjustment50) { create(:adjustment, amount: 100, included_tax: 50) }
describe '#migrate_admin_taxes!' do
context "when the adjustment has no tax" do
let!(:adjustment_without_tax) { create(:adjustment, included_tax: 0) }
it "doesn't move the tax to an adjustment" do
expect { subject.migrate_admin_taxes! }.to_not change {
Spree::Adjustment.count
}
end
end
context "when the adjustments have tax" do
before do
adjustment10; adjustment50
allow(subject).to receive(:applicable_rates) { [tax_rate10, tax_rate50] }
end
it "moves the tax to an adjustment" do
expect(Spree::Adjustment).to receive(:create!).at_least(:once).and_call_original
subject.migrate_admin_taxes!
expect(adjustment10.reload.tax_category).to eq tax_category10
expect(adjustment50.reload.tax_category).to eq tax_category50
tax_adjustment10 = Spree::Adjustment.tax.where(adjustable_id: adjustment10).first
expect(tax_adjustment10.amount).to eq adjustment10.included_tax
expect(tax_adjustment10.adjustable).to eq adjustment10
expect(tax_adjustment10.originator).to eq tax_rate10
expect(tax_adjustment10.state).to eq "closed"
expect(tax_adjustment10.included).to eq true
tax_adjustment50 = Spree::Adjustment.tax.where(adjustable_id: adjustment50).first
expect(tax_adjustment50.amount).to eq adjustment50.included_tax
expect(tax_adjustment50.adjustable).to eq adjustment50
expect(tax_adjustment50.originator).to eq tax_rate50
expect(tax_adjustment50.state).to eq "closed"
expect(tax_adjustment50.included).to eq true
end
end
end
describe "#find_tax_rate" do
before do
allow(subject).to receive(:applicable_rates) { [tax_rate10, tax_rate50] }
end
it "matches rates correctly" do
expect(subject.find_tax_rate(adjustment10)).to eq(tax_rate10)
expect(subject.find_tax_rate(adjustment50)).to eq(tax_rate50)
end
context "without a perfect match" do
let(:adjustment45) { create(:adjustment, amount: 100, included_tax: 45) }
it "finds the closest match" do
expect(subject.find_tax_rate(adjustment45)).to eq(tax_rate50)
end
end
end
describe "#applicabe_rates" do
let(:distributor) { create(:enterprise) }
let(:order) { create(:order, distributor: distributor) }
let!(:adjustment) { create(:adjustment, order: order) }
context "when the order is nil" do
let(:order) { nil }
it "returns an empty array" do
expect(Spree::TaxRate).to_not receive(:match)
expect(subject.applicable_rates(adjustment)).to eq []
end
end
context "when the order has no distributor" do
let(:distributor) { nil }
it "returns an empty array" do
expect(Spree::TaxRate).to_not receive(:match)
expect(subject.applicable_rates(adjustment)).to eq []
end
end
context "when the order has a distributor" do
it "calls TaxRate#match for an array of applicable taxes for the order" do
expect(Spree::TaxRate).to receive(:match) { [tax_rate10] }
expect(subject.applicable_rates(adjustment)).to eq [tax_rate10]
end
end
end
describe '#tax_adjustment_label' do
let(:tax_rate) { create(:tax_rate, name: "Test Rate", amount: 0.20) }
context "when a tax rate is given" do
it "makes a detailed label" do
expect(subject.tax_adjustment_label(tax_rate)).
to eq("Test Rate 20.0% (Included in price)")
end
end
context "when the tax rate is nil" do
it "makes a basic label" do
expect(subject.tax_adjustment_label(nil)).to eq("Included tax")
end
end
end
end

View File

@@ -148,33 +148,6 @@ module Spree
expect(adjustment.metadata).to be
end
describe "querying included tax" do
let!(:adjustment_with_tax) { create(:adjustment, included_tax: 123) }
let!(:adjustment_without_tax) { create(:adjustment, included_tax: 0) }
describe "finding adjustments with and without tax included" do
it "finds adjustments with tax" do
expect(Adjustment.with_tax).to include adjustment_with_tax
expect(Adjustment.with_tax).not_to include adjustment_without_tax
end
it "finds adjustments without tax" do
expect(Adjustment.without_tax).to include adjustment_without_tax
expect(Adjustment.without_tax).not_to include adjustment_with_tax
end
end
describe "checking if an adjustment includes tax" do
it "returns true when it has > 0 tax" do
expect(adjustment_with_tax).to have_tax
end
it "returns false when it has 0 tax" do
expect(adjustment_without_tax).not_to have_tax
end
end
end
describe "recording included tax" do
describe "TaxRate adjustments" do
let!(:zone) { create(:zone_with_member) }

View File

@@ -37,45 +37,6 @@ module Spree
end
end
describe "ensuring that tax rate is marked as tax included_in_price" do
let(:tax_rate) {
create(:tax_rate, included_in_price: false, calculator: ::Calculator::DefaultTax.new)
}
it "sets included_in_price to true" do
tax_rate.send(:with_tax_included_in_price) do
expect(tax_rate.included_in_price).to be true
end
end
it "sets the included_in_price value accessible to the calculator to true" do
tax_rate.send(:with_tax_included_in_price) do
expect(tax_rate.calculator.calculable.included_in_price).to be true
end
end
it "passes through the return value of the block" do
expect(tax_rate.send(:with_tax_included_in_price) do
'asdf'
end).to eq('asdf')
end
it "restores both values to their original afterwards" do
tax_rate.send(:with_tax_included_in_price) {}
expect(tax_rate.included_in_price).to be false
expect(tax_rate.calculator.calculable.included_in_price).to be false
end
it "restores both values when an exception is raised" do
expect do
tax_rate.send(:with_tax_included_in_price) { raise StandardError, 'oops' }
end.to raise_error 'oops'
expect(tax_rate.included_in_price).to be false
expect(tax_rate.calculator.calculable.included_in_price).to be false
end
end
context "original Spree::TaxRate specs" do
context "match" do
let(:order) { create(:order) }

View File

@@ -52,8 +52,10 @@ describe OrderTaxAdjustmentsFetcher do
calculator: Calculator::FlatRate.new(preferred_amount: 48.0))
end
let(:admin_adjustment) do
create(:adjustment, order: order, amount: 50.0, included_tax: tax_rate25.compute_tax(50.0),
label: "Admin Adjustment")
create(:adjustment, order: order, amount: 50.0, tax_category: tax_category25,
label: "Admin Adjustment").tap do |adjustment|
Spree::TaxRate.adjust(order, [adjustment])
end
end
let(:order_cycle) do

View File

@@ -5,71 +5,46 @@ require 'spec_helper'
describe TaxRateFinder do
describe "getting the corresponding tax rate" do
let(:amount) { BigDecimal(120) }
let(:included_tax) { BigDecimal(20) }
let(:tax_rate) { create_rate(0.2) }
let(:tax_rate) {
create(:tax_rate, amount: 0.2, calculator: Calculator::DefaultTax.new, zone: zone)
}
let(:tax_category) { create(:tax_category, tax_rates: [tax_rate]) }
let(:zone) { create(:zone_with_member) }
let(:shipment) { create(:shipment) }
let(:line_item) { create(:line_item) }
let(:enterprise_fee) { create(:enterprise_fee, tax_category: tax_category) }
let(:order) { create(:order_with_taxes, zone: zone) }
subject { TaxRateFinder.new }
it "finds the tax rate of a shipping fee" do
rates = TaxRateFinder.new.tax_rates(
tax_rate,
shipment,
amount,
included_tax
)
rates = subject.tax_rates(tax_rate, shipment)
expect(rates).to eq [tax_rate]
end
it "finds a close match" do
it "deals with soft-deleted tax rates" do
tax_rate.destroy
close_tax_rate = create_rate(tax_rate.amount + 0.05)
# other tax rates, not as close to the real one
create_rate(tax_rate.amount + 0.06)
create_rate(tax_rate.amount - 0.06)
rates = TaxRateFinder.new.tax_rates(
nil,
shipment,
amount,
included_tax
)
expect(rates).to eq [close_tax_rate]
rates = subject.tax_rates(tax_rate, shipment)
expect(rates).to eq [tax_rate]
end
it "finds the tax rate of an enterprise fee" do
rates = TaxRateFinder.new.tax_rates(
enterprise_fee,
order,
amount,
included_tax
)
rates = subject.tax_rates(enterprise_fee, order)
expect(rates).to eq [tax_rate]
end
# There is a bug that leaves orphan adjustments on an order after
# associated line items have been removed.
# https://github.com/openfoodfoundation/openfoodnetwork/issues/3127
it "deals with a missing line item" do
rates = TaxRateFinder.new.tax_rates(
enterprise_fee,
nil,
amount,
included_tax
)
it "deals with a soft-deleted line item" do
line_item.destroy
rates = subject.tax_rates(enterprise_fee, line_item)
expect(rates).to eq [tax_rate]
end
def create_rate(amount)
create(
:tax_rate,
amount: amount,
calculator: Calculator::DefaultTax.new,
zone: zone
)
context "when the given adjustment has no associated tax" do
let(:adjustment) { create(:adjustment) }
it "returns an empty array" do
expect(subject.tax_rates(adjustment.originator, adjustment.adjustable)).to eq []
end
end
end
end