Merge remote-tracking branch 'origin/xero-report' into combined/xero-report_show-order-without-distributor

This commit is contained in:
Maikel Linke
2015-06-05 13:48:24 +10:00
12 changed files with 385 additions and 7 deletions

View File

@@ -7,6 +7,7 @@ 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'
require 'open_food_network/xero_invoices_report'
Spree::Admin::ReportsController.class_eval do
@@ -679,7 +680,22 @@ Spree::Admin::ReportsController.class_eval do
render_report(@report.header, @report.table, params[:csv], "users_and_enterprises_#{timestamp}.csv")
end
def render_report (header, table, create_csv, csv_file_name)
def xero_invoices
if request.get?
params[:q] ||= {}
params[:q][:completed_at_gt] = Time.zone.now.beginning_of_month
end
@distributors = Enterprise.is_distributor.managed_by(spree_current_user)
@order_cycles = OrderCycle.active_or_complete.accessible_by(spree_current_user).order('orders_close_at DESC')
@search = Spree::Order.complete.managed_by(spree_current_user).order('id DESC').search(params[:q])
orders = @search.result
@report = OpenFoodNetwork::XeroInvoicesReport.new orders, params
render_report(@report.header, @report.table, params[:csv], "xero_invoices_#{timestamp}.csv")
end
def render_report(header, table, create_csv, csv_file_name)
unless create_csv
render :html => table
else
@@ -716,7 +732,9 @@ Spree::Admin::ReportsController.class_eval do
: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 => ''},
:sales_tax => { :name => "Sales Tax", :description => "Sales Tax For Orders" }
:sales_tax => { :name => "Sales Tax", :description => "Sales Tax For Orders" },
:xero_invoices => { :name => "Xero Invoices", :description => 'Invoices for import into Xero' }
}
# Return only reports the user is authorized to view.
reports.select { |action| can? action, :report }

View File

@@ -4,6 +4,8 @@ module Spree
scope :enterprise_fee, where(originator_type: 'EnterpriseFee')
scope :included_tax, where(originator_type: 'Spree::TaxRate', adjustable_type: 'Spree::LineItem')
scope :with_tax, where('spree_adjustments.included_tax > 0')
scope :without_tax, where('spree_adjustments.included_tax = 0')
attr_accessible :included_tax

View File

@@ -24,6 +24,15 @@ Spree::LineItem.class_eval do
where('spree_products.supplier_id IN (?)', enterprises)
}
scope :with_tax, joins(:adjustments).
where('spree_adjustments.originator_type = ?', 'Spree::TaxRate').
select('DISTINCT spree_line_items.*')
# Line items without a Spree::TaxRate-originated adjustment
scope :without_tax, joins("LEFT OUTER JOIN spree_adjustments ON (spree_adjustments.adjustable_id=spree_line_items.id AND spree_adjustments.adjustable_type = 'Spree::LineItem' AND spree_adjustments.originator_type='Spree::TaxRate')").
where('spree_adjustments.id IS NULL')
def price_with_adjustments
# EnterpriseFee#create_locked_adjustment applies adjustments on line items to their parent order,
# so line_item.adjustments returns an empty array

View File

@@ -0,0 +1,44 @@
= form_for @search, url: spree.xero_invoices_admin_reports_path do |f|
= render 'date_range_form', f: f
.row
.four.columns.alpha= label_tag nil, "Hub: "
.four.columns.omega= f.collection_select(:distributor_id_eq, @distributors, :id, :name, {:include_blank => 'All'}, {:class => "select2 fullwidth"})
.row
.four.columns.alpha= label_tag nil, "Order Cycle: "
.four.columns.omega= f.select(:order_cycle_id_eq,
options_for_select(report_order_cycle_options(@order_cycles), params[:q][:order_cycle_id_eq]),
{:include_blank => true}, {:class => "select2 fullwidth"})
.row
.four.columns.alpha= label_tag :initial_invoice_number, "Initial invoice number:"
.twelve.columns.omega= text_field_tag :initial_invoice_number, params[:initial_invoice_number]
.row
.four.columns.alpha= label_tag :invoice_date, "Invoice date:"
.twelve.columns.omega= text_field_tag :invoice_date, params[:invoice_date], class: 'datetimepicker'
.row
.four.columns.alpha= label_tag :due_date, "Due date:"
.twelve.columns.omega= text_field_tag :due_date, params[:due_date], class: 'datetimepicker'
.row
.four.columns.alpha= label_tag :account_code, "Account code:"
.twelve.columns.omega= text_field_tag :account_code, params[:account_code]
.row
.four.columns.alpha= label_tag :csv, "Download as CSV:"
.twelve.columns.omega= check_box_tag :csv
.row
.four.columns.alpha= button t(:search)
%table#listing_invoices.index
%thead
%tr
- @report.header.each do |header|
%th= header
%tbody
- @report.table.each do |row|
%tr
- row.each do |column|
%td= column
- if @report.table.empty?
%tr
%td{:colspan => "2"}= t(:none)

View File

@@ -136,6 +136,7 @@ Spree::Core::Engine.routes.prepend do
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]
match '/admin/reports/customers' => 'admin/reports#customers', :as => "customers_admin_reports", :via => [:get, :post]
match '/admin/reports/xero_invoices' => 'admin/reports#xero_invoices', :as => "xero_invoices_admin_reports", :via => [:get, :post]
match '/admin', :to => 'admin/overview#index', :as => :admin
match '/admin/payment_methods/show_provider_preferences' => 'admin/payment_methods#show_provider_preferences', :via => :get

View File

@@ -1,4 +1,3 @@
module OpenFoodNetwork
class OrderAndDistributorReport
@@ -8,14 +7,15 @@ module OpenFoodNetwork
def header
["Order date", "Order Id",
"Customer Name","Customer Email", "Customer Phone", "Customer City",
"SKU", "Item name", "Variant", "Quantity", "Max Quantity", "Cost", "Shipping cost",
"Payment method",
"Distributor", "Distributor address", "Distributor city", "Distributor postcode", "Shipping instructions"]
"Customer Name","Customer Email", "Customer Phone", "Customer City",
"SKU", "Item name", "Variant", "Quantity", "Max Quantity", "Cost", "Shipping cost",
"Payment method",
"Distributor", "Distributor address", "Distributor city", "Distributor postcode", "Shipping instructions"]
end
def table
order_and_distributor_details = []
@orders.each do |order|
order.line_items.each do |line_item|
order_and_distributor_details << [order.created_at, order.id,
@@ -25,6 +25,7 @@ module OpenFoodNetwork
order.distributor.andand.name, order.distributor.address.address1, order.distributor.address.city, order.distributor.address.zipcode, order.special_instructions ]
end
end
order_and_distributor_details
end
end

View File

@@ -0,0 +1,104 @@
module OpenFoodNetwork
class XeroInvoicesReport
def initialize(orders, opts={})
@orders = orders
@opts = opts.
reject { |k, v| v.blank? }.
reverse_merge({invoice_date: Date.today,
due_date: 2.weeks.from_now.to_date,
account_code: 'food sales'})
end
def header
%w(*ContactName EmailAddress POAddressLine1 POAddressLine2 POAddressLine3 POAddressLine4 POCity PORegion POPostalCode POCountry *InvoiceNumber Reference *InvoiceDate *DueDate InventoryItemCode *Description *Quantity *UnitAmount Discount *AccountCode *TaxType TrackingName1 TrackingOption1 TrackingName2 TrackingOption2 Currency BrandingTheme Paid?)
end
def table
rows = []
@orders.each_with_index do |order, i|
invoice_number = invoice_number_for(order, i)
rows += rows_for_order(order, invoice_number, @opts)
end
rows
end
private
def invoice_number_for(order, i)
@opts[:initial_invoice_number] ? @opts[:initial_invoice_number].to_i+i : order.number
end
def rows_for_order(order, invoice_number, opts)
[
summary_row(order, 'Total untaxable produce (no tax)', total_untaxable_products(order), invoice_number, 'GST Free Income', opts),
summary_row(order, 'Total taxable produce (tax inclusive)', total_taxable_products(order), invoice_number, 'GST on Income', opts),
summary_row(order, 'Total untaxable fees (no tax)', total_untaxable_fees(order), invoice_number, 'GST Free Income', opts),
summary_row(order, 'Total taxable fees (tax inclusive)', total_taxable_fees(order), invoice_number, 'GST on Income', opts),
summary_row(order, 'Delivery Shipping Cost (tax inclusive)', total_shipping(order), invoice_number, tax_on_shipping_s(order), opts)
].compact
end
def summary_row(order, description, amount, invoice_number, tax_type, opts={})
return nil if amount == 0
[order.bill_address.full_name,
order.email,
order.bill_address.address1,
order.bill_address.address2,
'',
'',
order.bill_address.city,
order.bill_address.state,
order.bill_address.zipcode,
order.bill_address.country.andand.name,
invoice_number,
order.number,
opts[:invoice_date],
opts[:due_date],
'',
description,
'1',
amount,
'',
opts[:account_code],
tax_type,
'',
'',
'',
'',
Spree::Config.currency,
'',
order.paid? ? 'Y' : 'N'
]
end
def total_untaxable_products(order)
order.line_items.without_tax.sum &:amount
end
def total_taxable_products(order)
order.line_items.with_tax.sum &:amount
end
def total_untaxable_fees(order)
order.adjustments.enterprise_fee.without_tax.sum &:amount
end
def total_taxable_fees(order)
order.adjustments.enterprise_fee.with_tax.sum &:amount
end
def total_shipping(order)
order.adjustments.shipping.sum &:amount
end
def tax_on_shipping_s(order)
tax_on_shipping = order.adjustments.shipping.sum(&:included_tax) > 0
tax_on_shipping ? 'GST on Income' : 'GST Free Income'
end
end
end

View File

@@ -300,4 +300,94 @@ feature %q{
].sort
end
end
describe "Xero invoices report" do
let(:distributor1) { create(:distributor_enterprise, with_payment_and_shipping: true, charges_sales_tax: true) }
let(:distributor2) { create(:distributor_enterprise, with_payment_and_shipping: true, charges_sales_tax: true) }
let(:user1) { create_enterprise_user enterprises: [distributor1] }
let(:user2) { create_enterprise_user enterprises: [distributor2] }
let(:shipping_method) { create(:shipping_method, name: "Shipping", description: "Expensive", calculator: Spree::Calculator::FlatRate.new(preferred_amount: 100.55)) }
let(:enterprise_fee1) { create(:enterprise_fee, enterprise: user1.enterprises.first, tax_category: product2.tax_category, calculator: Spree::Calculator::FlatRate.new(preferred_amount: 10)) }
let(:enterprise_fee2) { create(:enterprise_fee, enterprise: user1.enterprises.first, tax_category: product2.tax_category, calculator: Spree::Calculator::FlatRate.new(preferred_amount: 20)) }
let(:order_cycle) { create(:simple_order_cycle, coordinator: distributor1, coordinator_fees: [enterprise_fee1, enterprise_fee2], distributors: [distributor1], variants: [product1.master]) }
let!(:zone) { create(:zone_with_member) }
let(:country) { Spree::Country.find Spree::Config.default_country_id }
let(:bill_address) { create(:address, firstname: 'Customer', lastname: 'Name', address1: 'customer l1', address2: '', city: 'customer city', zipcode: 1234, country: country) }
let(:order1) { create(:order, order_cycle: order_cycle, distributor: user1.enterprises.first, shipping_method: shipping_method, bill_address: bill_address) }
let(:product1) { create(:taxed_product, zone: zone, price: 12.54, tax_rate_amount: 0) }
let(:product2) { create(:taxed_product, zone: zone, price: 500.15, tax_rate_amount: 0.2) }
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", originator: shipping_method, amount: 100.55, included_tax: 10.06) }
let!(:adj_fee1) { create(:adjustment, adjustable: order1, originator: enterprise_fee1, label: "Enterprise fee untaxed", amount: 10, included_tax: 0) }
let!(:adj_fee2) { create(:adjustment, adjustable: order1, originator: enterprise_fee2, label: "Enterprise fee taxed", amount: 20, included_tax: 2) }
before do
order1.update_attribute :email, 'customer@email.com'
Timecop.travel(Time.zone.local(2015, 4, 25, 14, 0, 0)) { order1.finalize! }
login_to_admin_section
click_link 'Reports'
click_link 'Xero Invoices'
end
around do |example|
Timecop.travel(Time.zone.local(2015, 4, 26, 14, 0, 0)) do
example.yield
end
end
it "shows Xero invoices report" do
xero_invoice_table.should match_table [
xero_invoice_header,
xero_invoice_row('Total untaxable produce (no tax)', 12.54, 'GST Free Income'),
xero_invoice_row('Total taxable produce (tax inclusive)', 1500.45, 'GST on Income'),
xero_invoice_row('Total untaxable fees (no tax)', 10.0, 'GST Free Income'),
xero_invoice_row('Total taxable fees (tax inclusive)', 20.0, 'GST on Income'),
xero_invoice_row('Delivery Shipping Cost (tax inclusive)', 100.55, 'GST on Income')
]
end
it "can customise a number of fields" do
fill_in 'initial_invoice_number', with: '5'
fill_in 'invoice_date', with: '2015-02-12'
fill_in 'due_date', with: '2015-03-12'
fill_in 'account_code', with: 'abc123'
click_button 'Search'
opts = {invoice_number: '5', invoice_date: '2015-02-12', due_date: '2015-03-12', account_code: 'abc123'}
xero_invoice_table.should match_table [
xero_invoice_header,
xero_invoice_row('Total untaxable produce (no tax)', 12.54, 'GST Free Income', opts),
xero_invoice_row('Total taxable produce (tax inclusive)', 1500.45, 'GST on Income', opts),
xero_invoice_row('Total untaxable fees (no tax)', 10.0, 'GST Free Income', opts),
xero_invoice_row('Total taxable fees (tax inclusive)', 20.0, 'GST on Income', opts),
xero_invoice_row('Delivery Shipping Cost (tax inclusive)', 100.55, 'GST on Income', opts)
]
end
private
def xero_invoice_table
find("table#listing_invoices")
end
def xero_invoice_header
%w(*ContactName EmailAddress POAddressLine1 POAddressLine2 POAddressLine3 POAddressLine4 POCity PORegion POPostalCode POCountry *InvoiceNumber Reference *InvoiceDate *DueDate InventoryItemCode *Description *Quantity *UnitAmount Discount *AccountCode *TaxType TrackingName1 TrackingOption1 TrackingName2 TrackingOption2 Currency BrandingTheme Paid?)
end
def xero_invoice_row(description, amount, tax_type, opts={})
opts.reverse_merge!({invoice_number: order1.number, invoice_date: '2015-04-26', due_date: '2015-05-10', account_code: 'food sales'})
['Customer Name', 'customer@email.com', 'customer l1', '', '', '', 'customer city', 'Victoria', '1234', country.name, opts[:invoice_number], order1.number, opts[:invoice_date], opts[:due_date], '', description, '1', amount.to_s, '', opts[:account_code], tax_type, '', '', '', '', Spree::Config.currency, '', 'N']
end
end
end

View File

@@ -0,0 +1,37 @@
require 'open_food_network/xero_invoices_report'
module OpenFoodNetwork
describe XeroInvoicesReport do
subject { XeroInvoicesReport.new [] }
describe "option defaults" do
let(:report) { XeroInvoicesReport.new [], {initial_invoice_number: '', invoice_date: '', due_date: '', account_code: ''} }
around { |example| Timecop.travel(Time.zone.local(2015, 5, 5, 14, 0, 0)) { example.run } }
it "uses defaults when blank params are passed" do
report.instance_variable_get(:@opts).should == {invoice_date: Date.civil(2015, 5, 5),
due_date: Date.civil(2015, 5, 19),
account_code: 'food sales'}
end
end
describe "generating invoice numbers" do
let(:order) { double(:order, number: 'R731032860') }
describe "when no initial invoice number is given" do
it "returns the order number" do
subject.send(:invoice_number_for, order, 123).should == 'R731032860'
end
end
describe "when an initial invoice number is given" do
subject { XeroInvoicesReport.new [], {initial_invoice_number: '123'} }
it "increments the number by the index" do
subject.send(:invoice_number_for, order, 456).should == 579
end
end
end
end
end

View File

@@ -5,6 +5,21 @@ module Spree
adjustment.metadata.should be
end
describe "finding adjustments with and without tax included" do
let!(:adjustment_with_tax) { create(:adjustment, included_tax: 123) }
let!(:adjustment_without_tax) { create(:adjustment, included_tax: 0) }
it "finds adjustments with tax" do
Adjustment.with_tax.should include adjustment_with_tax
Adjustment.with_tax.should_not include adjustment_without_tax
end
it "finds adjustments without tax" do
Adjustment.without_tax.should include adjustment_without_tax
Adjustment.without_tax.should_not include adjustment_with_tax
end
end
describe "recording included tax" do
describe "TaxRate adjustments" do
let!(:zone) { create(:zone_with_member) }

View File

@@ -24,6 +24,22 @@ module Spree
LineItem.supplied_by_any([s2]).should == [li2]
LineItem.supplied_by_any([s1, s2]).should match_array [li1, li2]
end
describe "finding line items with and without tax" do
let(:tax_rate) { create(:tax_rate, calculator: Spree::Calculator::DefaultTax.new) }
let!(:adjustment1) { create(:adjustment, adjustable: li1, originator: tax_rate, label: "TR", amount: 123, included_tax: 10.00) }
let!(:adjustment2) { create(:adjustment, adjustable: li1, originator: tax_rate, label: "TR", amount: 123, included_tax: 10.00) }
before { li1; li2 }
it "finds line items with tax" do
LineItem.with_tax.should == [li1]
end
it "finds line items without tax" do
LineItem.without_tax.should == [li2]
end
end
end
describe "calculating price with adjustments" do

View File

@@ -26,3 +26,44 @@ RSpec::Matchers.define :have_table_row do |row|
node.all('tr').map { |tr| tr.all('th, td').map(&:text) }
end
end
# find("#my-table").should match_table [[...]]
RSpec::Matchers.define :match_table do |expected_table|
match_for_should do |node|
rows = node.
all("tr").
map { |r| r.all("th,td").map { |c| c.text.strip } }
if rows.count != expected_table.count
@failure_message = "found table with #{rows.count} rows, expected #{expected_table.count}"
else
rows.each_with_index do |row, i|
expected_row = expected_table[i]
if row.count != expected_row.count
@failure_message = "row #{i} has #{row.count} columns, expected #{expected_row.count}"
break
elsif row != expected_row
row.each_with_index do |cell, j|
if cell != expected_row[j]
@failure_message = "cell [#{i}, #{j}] has content '#{cell}', expected '#{expected_row[j]}'"
break
end
end
break if @failure_message
end
end
end
@failure_message.nil?
end
failure_message_for_should do |text|
@failure_message
end
end